summaryrefslogtreecommitdiff
path: root/src/lib
diff options
context:
space:
mode:
Diffstat (limited to 'src/lib')
-rw-r--r--src/lib/audio_analysis.cc9
-rw-r--r--src/lib/audio_filter_graph.cc19
-rw-r--r--src/lib/audio_filter_graph.h3
-rw-r--r--src/lib/audio_mapping.cc9
-rw-r--r--src/lib/butler.cc2
-rw-r--r--src/lib/cinema.cc24
-rw-r--r--src/lib/cinema.h23
-rw-r--r--src/lib/config.cc59
-rw-r--r--src/lib/config.h34
-rw-r--r--src/lib/content_video.h11
-rw-r--r--src/lib/cpu_j2k_encoder_thread.cc62
-rw-r--r--src/lib/cpu_j2k_encoder_thread.h16
-rw-r--r--src/lib/cross.h16
-rw-r--r--src/lib/cross_common.cc97
-rw-r--r--src/lib/cross_osx.cc107
-rw-r--r--src/lib/dcp_content.cc74
-rw-r--r--src/lib/dcp_content.h9
-rw-r--r--src/lib/dcp_decoder.cc6
-rw-r--r--src/lib/dcp_encoder.cc39
-rw-r--r--src/lib/dcp_encoder.h7
-rw-r--r--src/lib/dcp_video.cc24
-rw-r--r--src/lib/dcp_video.h7
-rw-r--r--src/lib/dcpomatic_time.h4
-rw-r--r--src/lib/dkdm_recipient.cc20
-rw-r--r--src/lib/dkdm_recipient.h12
-rw-r--r--src/lib/dkdm_wrapper.cc8
-rw-r--r--src/lib/encode_server.cc3
-rw-r--r--src/lib/encode_server.h6
-rw-r--r--src/lib/encode_server_finder.h3
-rw-r--r--src/lib/encoder.h2
-rw-r--r--src/lib/ffmpeg_decoder.cc9
-rw-r--r--src/lib/ffmpeg_examiner.cc20
-rw-r--r--src/lib/ffmpeg_file_encoder.cc5
-rw-r--r--src/lib/film.cc59
-rw-r--r--src/lib/film.h7
-rw-r--r--src/lib/grok/context.h291
-rw-r--r--src/lib/grok/messenger.h906
-rw-r--r--src/lib/grok_j2k_encoder_thread.cc72
-rw-r--r--src/lib/grok_j2k_encoder_thread.h41
-rw-r--r--src/lib/image_decoder.cc2
-rw-r--r--src/lib/j2k_encoder.cc385
-rw-r--r--src/lib/j2k_encoder.h35
-rw-r--r--src/lib/j2k_encoder_thread.cc58
-rw-r--r--src/lib/j2k_encoder_thread.h53
-rw-r--r--src/lib/j2k_sync_encoder_thread.cc65
-rw-r--r--src/lib/j2k_sync_encoder_thread.h32
-rw-r--r--src/lib/job.cc4
-rw-r--r--src/lib/job.h3
-rw-r--r--src/lib/kdm_cli.cc68
-rw-r--r--src/lib/make_dcp.cc11
-rw-r--r--src/lib/make_dcp.h2
-rw-r--r--src/lib/overlaps.cc3
-rw-r--r--src/lib/overlaps.h7
-rw-r--r--src/lib/player.cc270
-rw-r--r--src/lib/player.h11
-rw-r--r--src/lib/player_video.cc11
-rw-r--r--src/lib/player_video.h6
-rw-r--r--src/lib/remote_j2k_encoder_thread.cc84
-rw-r--r--src/lib/remote_j2k_encoder_thread.h21
-rw-r--r--src/lib/screen.cc14
-rw-r--r--src/lib/screen.h7
-rw-r--r--src/lib/shuffler.cc18
-rw-r--r--src/lib/state.cc2
-rw-r--r--src/lib/text_content.cc9
-rw-r--r--src/lib/transcode_job.cc14
-rw-r--r--src/lib/transcode_job.h6
-rw-r--r--src/lib/util.cc44
-rw-r--r--src/lib/util.h16
-rw-r--r--src/lib/video_content.cc22
-rw-r--r--src/lib/video_content.h2
-rw-r--r--src/lib/video_decoder.cc111
-rw-r--r--src/lib/video_decoder.h2
-rw-r--r--src/lib/video_mxf_decoder.cc6
-rw-r--r--src/lib/writer.h1
-rw-r--r--src/lib/wscript7
75 files changed, 2667 insertions, 870 deletions
diff --git a/src/lib/audio_analysis.cc b/src/lib/audio_analysis.cc
index b8c2e072d..c50a2bb9d 100644
--- a/src/lib/audio_analysis.cc
+++ b/src/lib/audio_analysis.cc
@@ -84,11 +84,8 @@ AudioAnalysis::AudioAnalysis (boost::filesystem::path filename)
}
for (auto i: f.node_children ("SamplePeak")) {
- _sample_peak.push_back (
- PeakTime(
- dcp::raw_convert<float>(i->content()), DCPTime(i->number_attribute<Frame>("Time"))
- )
- );
+ auto const time = number_attribute<Frame>(i, "Time", "time");
+ _sample_peak.push_back(PeakTime(dcp::raw_convert<float>(i->content()), DCPTime(time)));
}
for (auto i: f.node_children("TruePeak")) {
@@ -155,7 +152,7 @@ AudioAnalysis::write (boost::filesystem::path filename)
for (size_t i = 0; i < _sample_peak.size(); ++i) {
auto n = root->add_child("SamplePeak");
n->add_child_text (raw_convert<string> (_sample_peak[i].peak));
- n->set_attribute ("Time", raw_convert<string> (_sample_peak[i].time.get()));
+ n->set_attribute("time", raw_convert<string> (_sample_peak[i].time.get()));
}
for (auto i: _true_peak) {
diff --git a/src/lib/audio_filter_graph.cc b/src/lib/audio_filter_graph.cc
index 4e3052d57..cb888c162 100644
--- a/src/lib/audio_filter_graph.cc
+++ b/src/lib/audio_filter_graph.cc
@@ -48,11 +48,7 @@ AudioFilterGraph::AudioFilterGraph (int sample_rate, int channels)
/* FFmpeg doesn't know any channel layouts for any counts between 8 and 16,
so we need to tell it we're using 16 channels if we are using more than 8.
*/
- if (_channels > 8) {
- _channel_layout = av_get_default_channel_layout (16);
- } else {
- _channel_layout = av_get_default_channel_layout (_channels);
- }
+ av_channel_layout_default(&_channel_layout, _channels > 8 ? 16 : _channels);
_in_frame = av_frame_alloc ();
if (_in_frame == nullptr) {
@@ -69,7 +65,7 @@ string
AudioFilterGraph::src_parameters () const
{
char layout[64];
- av_get_channel_layout_string (layout, sizeof(layout), 0, _channel_layout);
+ av_channel_layout_describe(&_channel_layout, layout, sizeof(layout));
char buffer[256];
snprintf (
@@ -88,8 +84,9 @@ AudioFilterGraph::set_parameters (AVFilterContext* context) const
int r = av_opt_set_int_list (context, "sample_fmts", sample_fmts, AV_SAMPLE_FMT_NONE, AV_OPT_SEARCH_CHILDREN);
DCPOMATIC_ASSERT (r >= 0);
- int64_t channel_layouts[] = { _channel_layout, -1 };
- r = av_opt_set_int_list (context, "channel_layouts", channel_layouts, -1, AV_OPT_SEARCH_CHILDREN);
+ char ch_layout[64];
+ av_channel_layout_describe(&_channel_layout, ch_layout, sizeof(ch_layout));
+ r = av_opt_set(context, "ch_layouts", ch_layout, AV_OPT_SEARCH_CHILDREN);
DCPOMATIC_ASSERT (r >= 0);
int sample_rates[] = { _sample_rate, -1 };
@@ -114,7 +111,7 @@ void
AudioFilterGraph::process (shared_ptr<AudioBuffers> buffers)
{
DCPOMATIC_ASSERT (buffers->frames() > 0);
- int const process_channels = av_get_channel_layout_nb_channels (_channel_layout);
+ int const process_channels = _channel_layout.nb_channels;
DCPOMATIC_ASSERT (process_channels >= buffers->channels());
if (buffers->channels() < process_channels) {
@@ -144,8 +141,10 @@ AudioFilterGraph::process (shared_ptr<AudioBuffers> buffers)
_in_frame->nb_samples = buffers->frames ();
_in_frame->format = AV_SAMPLE_FMT_FLTP;
_in_frame->sample_rate = _sample_rate;
- _in_frame->channel_layout = _channel_layout;
+ _in_frame->ch_layout = _channel_layout;
+LIBDCP_DISABLE_WARNINGS
_in_frame->channels = process_channels;
+LIBDCP_ENABLE_WARNINGS
int r = av_buffersrc_write_frame (_buffer_src_context, _in_frame);
diff --git a/src/lib/audio_filter_graph.h b/src/lib/audio_filter_graph.h
index e5c55fa27..6d09f15be 100644
--- a/src/lib/audio_filter_graph.h
+++ b/src/lib/audio_filter_graph.h
@@ -26,6 +26,7 @@
#include "filter_graph.h"
extern "C" {
#include <libavfilter/buffersink.h>
+#include <libavutil/channel_layout.h>
}
class AudioBuffers;
@@ -47,7 +48,7 @@ protected:
private:
int _sample_rate;
int _channels;
- int64_t _channel_layout;
+ AVChannelLayout _channel_layout;
AVFrame* _in_frame;
};
diff --git a/src/lib/audio_mapping.cc b/src/lib/audio_mapping.cc
index b8aa6249f..6e8c4e30d 100644
--- a/src/lib/audio_mapping.cc
+++ b/src/lib/audio_mapping.cc
@@ -24,6 +24,7 @@
#include "constants.h"
#include "dcpomatic_assert.h"
#include "digester.h"
+#include "util.h"
#include <dcp/raw_convert.h>
#include <dcp/warnings.h>
#include <libcxml/cxml.h>
@@ -169,8 +170,8 @@ AudioMapping::AudioMapping (cxml::ConstNodePtr node, int state_version)
);
} else {
set (
- i->number_attribute<int>("Input"),
- i->number_attribute<int>("Output"),
+ number_attribute<int>(i, "Input", "input"),
+ number_attribute<int>(i, "Output", "output"),
raw_convert<float>(i->content())
);
}
@@ -230,8 +231,8 @@ AudioMapping::as_xml (xmlpp::Node* node) const
for (int c = 0; c < input; ++c) {
for (int d = 0; d < output; ++d) {
auto t = node->add_child ("Gain");
- t->set_attribute ("Input", raw_convert<string> (c));
- t->set_attribute ("Output", raw_convert<string> (d));
+ t->set_attribute("input", raw_convert<string>(c));
+ t->set_attribute("output", raw_convert<string>(d));
t->add_child_text (raw_convert<string> (get (c, d)));
}
}
diff --git a/src/lib/butler.cc b/src/lib/butler.cc
index b2fbc6c60..dd9874587 100644
--- a/src/lib/butler.cc
+++ b/src/lib/butler.cc
@@ -138,7 +138,7 @@ Butler::should_run () const
{
if (_video.size() >= MAXIMUM_VIDEO_READAHEAD * 10) {
/* This is way too big */
- optional<DCPTime> pos = _audio.peek();
+ auto pos = _audio.peek();
if (pos) {
throw ProgrammingError
(__FILE__, __LINE__, String::compose ("Butler video buffers reached %1 frames (audio is %2 at %3)", _video.size(), _audio.size(), pos->get()));
diff --git a/src/lib/cinema.cc b/src/lib/cinema.cc
index 3b4b9d7b6..7388dbc2f 100644
--- a/src/lib/cinema.cc
+++ b/src/lib/cinema.cc
@@ -41,14 +41,6 @@ Cinema::Cinema (cxml::ConstNodePtr node)
for (auto i: node->node_children("Email")) {
emails.push_back (i->content ());
}
-
- if (node->optional_number_child<int>("UTCOffset")) {
- _utc_offset_hour = node->number_child<int>("UTCOffset");
- } else {
- _utc_offset_hour = node->optional_number_child<int>("UTCOffsetHour").get_value_or (0);
- }
-
- _utc_offset_minute = node->optional_number_child<int>("UTCOffsetMinute").get_value_or (0);
}
/* This is necessary so that we can use shared_from_this in add_screen (which cannot be done from
@@ -73,9 +65,6 @@ Cinema::as_xml (xmlpp::Element* parent) const
parent->add_child("Notes")->add_child_text (notes);
- parent->add_child("UTCOffsetHour")->add_child_text (raw_convert<string> (_utc_offset_hour));
- parent->add_child("UTCOffsetMinute")->add_child_text (raw_convert<string> (_utc_offset_minute));
-
for (auto i: _screens) {
i->as_xml (parent->add_child ("Screen"));
}
@@ -97,16 +86,3 @@ Cinema::remove_screen (shared_ptr<Screen> s)
}
}
-void
-Cinema::set_utc_offset_hour (int h)
-{
- DCPOMATIC_ASSERT (h >= -11 && h <= 12);
- _utc_offset_hour = h;
-}
-
-void
-Cinema::set_utc_offset_minute (int m)
-{
- DCPOMATIC_ASSERT (m >= 0 && m <= 59);
- _utc_offset_minute = m;
-}
diff --git a/src/lib/cinema.h b/src/lib/cinema.h
index 6c202a7bf..7008659d7 100644
--- a/src/lib/cinema.h
+++ b/src/lib/cinema.h
@@ -44,12 +44,10 @@ namespace dcpomatic {
class Cinema : public std::enable_shared_from_this<Cinema>
{
public:
- Cinema(std::string const & name_, std::vector<std::string> const & e, std::string notes_, int utc_offset_hour, int utc_offset_minute)
+ Cinema(std::string const & name_, std::vector<std::string> const & e, std::string notes_)
: name (name_)
, emails (e)
, notes (notes_)
- , _utc_offset_hour (utc_offset_hour)
- , _utc_offset_minute (utc_offset_minute)
{}
explicit Cinema (cxml::ConstNodePtr);
@@ -61,33 +59,14 @@ public:
void add_screen (std::shared_ptr<dcpomatic::Screen>);
void remove_screen (std::shared_ptr<dcpomatic::Screen>);
- void set_utc_offset_hour (int h);
- void set_utc_offset_minute (int m);
-
std::string name;
std::vector<std::string> emails;
std::string notes;
- int utc_offset_hour () const {
- return _utc_offset_hour;
- }
-
- int utc_offset_minute () const {
- return _utc_offset_minute;
- }
-
std::vector<std::shared_ptr<dcpomatic::Screen>> screens() const {
return _screens;
}
private:
std::vector<std::shared_ptr<dcpomatic::Screen>> _screens;
- /** Offset such that the equivalent time in UTC can be determined
- by subtracting the offset from the local time.
- */
- int _utc_offset_hour;
- /** Additional minutes to add to _utc_offset_hour if _utc_offset_hour is
- positive, or to subtract if _utc_offset_hour is negative.
- */
- int _utc_offset_minute;
};
diff --git a/src/lib/config.cc b/src/lib/config.cc
index 384db5cde..8dce6237a 100644
--- a/src/lib/config.cc
+++ b/src/lib/config.cc
@@ -220,6 +220,10 @@ Config::set_defaults ()
set_notification_email_to_default ();
set_cover_sheet_to_default ();
+#ifdef DCPOMATIC_GROK
+ _grok = boost::none;
+#endif
+
_main_divider_sash_position = {};
_main_content_divider_sash_position = {};
@@ -494,7 +498,7 @@ try
of the nags.
*/
for (auto i: f.node_children("Nagged")) {
- auto const id = i->number_attribute<int>("Id");
+ auto const id = number_attribute<int>(i, "Id", "id");
if (id >= 0 && id < NAG_COUNT) {
_nagged[id] = raw_convert<int>(i->content());
}
@@ -566,7 +570,7 @@ try
_default_notify = f.optional_bool_child("DefaultNotify").get_value_or(false);
for (auto i: f.node_children("Notification")) {
- int const id = i->number_attribute<int>("Id");
+ int const id = number_attribute<int>(i, "Id", "id");
if (id >= 0 && id < NOTIFICATION_COUNT) {
_notification[id] = raw_convert<int>(i->content());
}
@@ -642,6 +646,12 @@ 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);
+#ifdef DCPOMATIC_GROK
+ if (auto grok = f.optional_node_child("Grok")) {
+ _grok = Grok(grok);
+ }
+#endif
+
_export.read(f.optional_node_child("Export"));
}
catch (...) {
@@ -964,7 +974,7 @@ Config::write_config () const
/* [XML] Nagged 1 if a particular nag screen has been shown and should not be shown again, otherwise 0. */
for (int i = 0; i < NAG_COUNT; ++i) {
xmlpp::Element* e = root->add_child ("Nagged");
- e->set_attribute ("Id", raw_convert<string>(i));
+ e->set_attribute("id", raw_convert<string>(i));
e->add_child_text (_nagged[i] ? "1" : "0");
}
/* [XML] PreviewSound 1 to use sound in the GUI preview and player, otherwise 0. */
@@ -1019,7 +1029,7 @@ Config::write_config () const
/* [XML] Notification 1 if a notification type is enabled, otherwise 0. */
for (int i = 0; i < NOTIFICATION_COUNT; ++i) {
xmlpp::Element* e = root->add_child ("Notification");
- e->set_attribute ("Id", raw_convert<string>(i));
+ e->set_attribute("id", raw_convert<string>(i));
e->add_child_text (_notification[i] ? "1" : "0");
}
@@ -1128,6 +1138,12 @@ Config::write_config () const
/* [XML] ISDCFNamePartLength Maximum length of the "name" part of an ISDCF name, which should be 14 according to the standard */
root->add_child("ISDCFNamePartLength")->add_child_text(raw_convert<string>(_isdcf_name_part_length));
+#ifdef DCPOMATIC_GROK
+ if (_grok) {
+ _grok->as_xml(root->add_child("Grok"));
+ }
+#endif
+
_export.write(root->add_child("Export"));
auto target = config_write_file();
@@ -1685,3 +1701,38 @@ Config::initial_path(string id) const
return iter->second;
}
+
+#ifdef DCPOMATIC_GROK
+
+Config::Grok::Grok(cxml::ConstNodePtr node)
+ : enable(node->bool_child("Enable"))
+ , binary_location(node->string_child("BinaryLocation"))
+ , selected(node->number_child<int>("Selected"))
+ , licence_server(node->string_child("LicenceServer"))
+ , licence_port(node->number_child<int>("LicencePort"))
+ , licence(node->string_child("Licence"))
+{
+
+}
+
+
+void
+Config::Grok::as_xml(xmlpp::Element* node) const
+{
+ node->add_child("BinaryLocation")->add_child_text(binary_location.string());
+ node->add_child("Enable")->add_child_text((enable ? "1" : "0"));
+ node->add_child("Selected")->add_child_text(raw_convert<string>(selected));
+ node->add_child("LicenceServer")->add_child_text(licence_server);
+ node->add_child("LicencePort")->add_child_text(raw_convert<string>(licence_port));
+ node->add_child("Licence")->add_child_text(licence);
+}
+
+
+void
+Config::set_grok(Grok const& grok)
+{
+ _grok = grok;
+ changed(GROK);
+}
+
+#endif
diff --git a/src/lib/config.h b/src/lib/config.h
index f3d080b0b..74b4316d2 100644
--- a/src/lib/config.h
+++ b/src/lib/config.h
@@ -97,6 +97,9 @@ public:
AUTO_CROP_THRESHOLD,
ALLOW_SMPTE_BV20,
ISDCF_NAME_PART_LENGTH,
+#ifdef DCPOMATIC_GROK
+ GROK,
+#endif
OTHER
};
@@ -621,6 +624,28 @@ public:
return _allow_smpte_bv20;
}
+#ifdef DCPOMATIC_GROK
+ class Grok
+ {
+ public:
+ Grok() = default;
+ Grok(cxml::ConstNodePtr node);
+
+ void as_xml(xmlpp::Element* node) const;
+
+ bool enable = false;
+ boost::filesystem::path binary_location;
+ int selected = 0;
+ std::string licence_server;
+ int licence_port = 5000;
+ std::string licence;
+ };
+
+ boost::optional<Grok> grok() const {
+ return _grok;
+ }
+#endif
+
int isdcf_name_part_length() const {
return _isdcf_name_part_length;
}
@@ -1202,10 +1227,15 @@ public:
maybe_set(_allow_smpte_bv20, allow, ALLOW_SMPTE_BV20);
}
+#ifdef DCPOMATIC_GROK
+ void set_grok(Grok const& grok);
+#endif
+
void set_isdcf_name_part_length(int length) {
maybe_set(_isdcf_name_part_length, length, ISDCF_NAME_PART_LENGTH);
}
+
void changed (Property p = OTHER);
boost::signals2::signal<void (Property)> Changed;
/** Emitted if read() failed on an existing Config file. There is nothing
@@ -1447,6 +1477,10 @@ private:
bool _allow_smpte_bv20;
int _isdcf_name_part_length;
+#ifdef DCPOMATIC_GROK
+ boost::optional<Grok> _grok;
+#endif
+
ExportConfig _export;
static int const _current_version;
diff --git a/src/lib/content_video.h b/src/lib/content_video.h
index 4fdab717a..1c145f602 100644
--- a/src/lib/content_video.h
+++ b/src/lib/content_video.h
@@ -23,6 +23,7 @@
#define DCPOMATIC_CONTENT_VIDEO_H
+#include "dcpomatic_time.h"
#include "types.h"
@@ -36,22 +37,22 @@ class ContentVideo
{
public:
ContentVideo ()
- : frame (0)
- , eyes (Eyes::LEFT)
+ : eyes (Eyes::LEFT)
, part (Part::WHOLE)
{}
- ContentVideo (std::shared_ptr<const ImageProxy> i, Frame f, Eyes e, Part p)
+ ContentVideo (std::shared_ptr<const ImageProxy> i, dcpomatic::ContentTime t, Eyes e, Part p)
: image (i)
- , frame (f)
+ , time (t)
, eyes (e)
, part (p)
{}
std::shared_ptr<const ImageProxy> image;
- Frame frame;
+ dcpomatic::ContentTime time;
Eyes eyes;
Part part;
};
+
#endif
diff --git a/src/lib/cpu_j2k_encoder_thread.cc b/src/lib/cpu_j2k_encoder_thread.cc
new file mode 100644
index 000000000..580facae9
--- /dev/null
+++ b/src/lib/cpu_j2k_encoder_thread.cc
@@ -0,0 +1,62 @@
+/*
+ Copyright (C) 2023 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 "cpu_j2k_encoder_thread.h"
+#include "cross.h"
+#include "dcpomatic_log.h"
+#include "dcp_video.h"
+#include "j2k_encoder.h"
+#include "util.h"
+
+#include "i18n.h"
+
+
+using std::make_shared;
+using std::shared_ptr;
+
+
+CPUJ2KEncoderThread::CPUJ2KEncoderThread(J2KEncoder& encoder)
+ : J2KSyncEncoderThread(encoder)
+{
+
+}
+
+
+void
+CPUJ2KEncoderThread::log_thread_start() const
+{
+ start_of_thread("CPUJ2KEncoder");
+ LOG_TIMING("start-encoder-thread thread=%1 server=localhost", thread_id());
+}
+
+
+shared_ptr<dcp::ArrayData>
+CPUJ2KEncoderThread::encode(DCPVideo const& frame)
+{
+ try {
+ return make_shared<dcp::ArrayData>(frame.encode_locally());
+ } catch (std::exception& e) {
+ LOG_ERROR(N_("Local encode failed (%1)"), e.what());
+ }
+
+ return {};
+}
+
diff --git a/src/lib/cpu_j2k_encoder_thread.h b/src/lib/cpu_j2k_encoder_thread.h
new file mode 100644
index 000000000..fb138f484
--- /dev/null
+++ b/src/lib/cpu_j2k_encoder_thread.h
@@ -0,0 +1,16 @@
+#include "j2k_sync_encoder_thread.h"
+#include <dcp/data.h>
+
+
+class DCPVideo;
+
+
+class CPUJ2KEncoderThread : public J2KSyncEncoderThread
+{
+public:
+ CPUJ2KEncoderThread(J2KEncoder& encoder);
+
+ void log_thread_start() const override;
+ std::shared_ptr<dcp::ArrayData> encode(DCPVideo const& frame) override;
+};
+
diff --git a/src/lib/cross.h b/src/lib/cross.h
index 6904811b7..150199561 100644
--- a/src/lib/cross.h
+++ b/src/lib/cross.h
@@ -138,29 +138,19 @@ private:
void disk_write_finished ();
-struct OSXMediaPath
-{
- bool real; ///< true for a "real" disk, false for a synthesized APFS one
- std::vector<std::string> parts; ///< parts of the media path after the :
-};
-
-
struct OSXDisk
{
std::string device;
boost::optional<std::string> vendor;
boost::optional<std::string> model;
- OSXMediaPath media_path;
- bool whole;
std::vector<boost::filesystem::path> mount_points;
unsigned long size;
+ bool system;
+ bool writeable;
+ bool partition;
};
-boost::optional<OSXMediaPath> analyse_osx_media_path (std::string path);
-std::vector<Drive> osx_disks_to_drives (std::vector<OSXDisk> disks);
-
-
class ArgFixer
{
public:
diff --git a/src/lib/cross_common.cc b/src/lib/cross_common.cc
index b4d322096..b8f1d48f1 100644
--- a/src/lib/cross_common.cc
+++ b/src/lib/cross_common.cc
@@ -40,9 +40,6 @@ using std::vector;
using boost::optional;
-auto constexpr MEDIA_PATH_REQUIRED_MATCHES = 3;
-
-
Drive::Drive (string xml)
{
cxml::Document doc;
@@ -122,97 +119,3 @@ Drive::log_summary () const
);
}
-
-
-/* This is in _common so we can use it in unit tests */
-optional<OSXMediaPath>
-analyse_osx_media_path (string path)
-{
- if (path.find("/IOHDIXController") != string::npos) {
- /* This is a disk image, so we completely ignore it */
- LOG_DISK_NC("Ignoring this as it seems to be a disk image");
- return {};
- }
-
- OSXMediaPath mp;
- vector<string> parts;
- split(parts, path, boost::is_any_of("/"));
- std::copy(parts.begin() + 1, parts.end(), back_inserter(mp.parts));
-
- if (!parts.empty() && parts[0] == "IODeviceTree:") {
- mp.real = true;
- if (mp.parts.size() < MEDIA_PATH_REQUIRED_MATCHES) {
- /* Later we expect at least MEDIA_PATH_REQUIRED_MATCHES parts in a IODeviceTree */
- LOG_DISK_NC("Ignoring this as it has a strange media path");
- return {};
- }
- } else if (!parts.empty() && parts[0] == "IOService:") {
- mp.real = false;
- } else {
- return {};
- }
-
- return mp;
-}
-
-
-/* Take some OSXDisk objects, representing disks that `DARegisterDiskAppearedCallback` told us about,
- * and find those drives that we could write a DCP to. The drives returned are "real" (not synthesized)
- * and are whole disks (not partitions). They may be mounted, or contain mounted partitions, in which
- * their mounted() method will return true.
- */
-vector<Drive>
-osx_disks_to_drives (vector<OSXDisk> disks)
-{
- using namespace boost::algorithm;
-
- /* Mark disks containing mounted partitions as themselves mounted */
- for (auto& i: disks) {
- if (!i.whole) {
- continue;
- }
- for (auto& j: disks) {
- if (&i != &j && !j.mount_points.empty() && starts_with(j.device, i.device)) {
- LOG_DISK("Marking %1 as mounted because %2 is", i.device, j.device);
- std::copy(j.mount_points.begin(), j.mount_points.end(), back_inserter(i.mount_points));
- }
- }
- }
-
- /* Mark containers of mounted synths as themselves mounted */
- for (auto& i: disks) {
- if (i.media_path.real) {
- for (auto& j: disks) {
- if (!j.media_path.real && !j.mount_points.empty()) {
- /* i is real, j is a mounted synth; if we see the first MEDIA_PATH_REQUIRED_MATCHES parts
- * of i anywhere in j we assume they are related and so i shares j's mount points.
- */
- bool one_missing = false;
- string all_parts;
- DCPOMATIC_ASSERT (i.media_path.parts.size() >= MEDIA_PATH_REQUIRED_MATCHES);
- for (auto k = 0; k < MEDIA_PATH_REQUIRED_MATCHES; ++k) {
- if (find(j.media_path.parts.begin(), j.media_path.parts.end(), i.media_path.parts[k]) == j.media_path.parts.end()) {
- one_missing = true;
- }
- all_parts += i.media_path.parts[k] + " ";
- }
-
- if (!one_missing) {
- LOG_DISK("Marking %1 as mounted because %2 is (found %3)", i.device, j.device, all_parts);
- std::copy(j.mount_points.begin(), j.mount_points.end(), back_inserter(i.mount_points));
- }
- }
- }
- }
- }
-
- vector<Drive> drives;
- for (auto const& i: disks) {
- if (i.whole && i.media_path.real) {
- drives.push_back(Drive(i.device, i.mount_points, i.size, i.vendor, i.model));
- LOG_DISK_NC(drives.back().log_summary());
- }
- }
-
- return drives;
-}
diff --git a/src/lib/cross_osx.cc b/src/lib/cross_osx.cc
index 913b19103..20fe9bce8 100644
--- a/src/lib/cross_osx.cc
+++ b/src/lib/cross_osx.cc
@@ -240,44 +240,6 @@ get_model (CFDictionaryRef& description)
}
-static optional<OSXMediaPath>
-analyse_media_path (CFDictionaryRef& description)
-{
- using namespace boost::algorithm;
-
- void const* str = CFDictionaryGetValue (description, kDADiskDescriptionMediaPathKey);
- if (!str) {
- LOG_DISK_NC("There is no MediaPathKey (no dictionary value)");
- return {};
- }
-
- auto path_key_cstr = CFStringGetCStringPtr((CFStringRef) str, kCFStringEncodingUTF8);
- if (!path_key_cstr) {
- LOG_DISK_NC("There is no MediaPathKey (no cstring)");
- return {};
- }
-
- string path(path_key_cstr);
- LOG_DISK("MediaPathKey is %1", path);
- return analyse_osx_media_path (path);
-}
-
-
-static bool
-is_whole_drive (DADiskRef& disk)
-{
- io_service_t service = DADiskCopyIOMedia (disk);
- CFTypeRef whole_media_ref = IORegistryEntryCreateCFProperty (service, CFSTR(kIOMediaWholeKey), kCFAllocatorDefault, 0);
- bool whole_media = false;
- if (whole_media_ref) {
- whole_media = CFBooleanGetValue((CFBooleanRef) whole_media_ref);
- CFRelease (whole_media_ref);
- }
- IOObjectRelease (service);
- return whole_media;
-}
-
-
static optional<boost::filesystem::path>
mount_point (CFDictionaryRef& description)
{
@@ -294,29 +256,17 @@ mount_point (CFDictionaryRef& description)
}
-/* Here follows some rather intricate and (probably) fragile code to find the list of available
- * "real" drives on macOS that we might want to write a DCP to.
- *
- * We use the Disk Arbitration framework to give us a series of mount_points (/dev/disk0, /dev/disk1,
- * /dev/disk1s1 and so on) and we use the API to gather useful information about these mount_points into
- * a vector of Disk structs.
- *
- * Then we read the Disks that we found and try to derive a list of drives that we should offer to the
- * user, with details of whether those drives are currently mounted or not.
- *
- * At the basic level we find the "disk"-level mount_points, looking at whether any of their partitions are mounted.
- *
- * This is complicated enormously by recent-ish macOS versions' habit of making `synthesized' volumes which
- * reflect data in `real' partitions. So, for example, we might have a real (physical) drive /dev/disk2 with
- * a partition /dev/disk2s2 whose content is made into a synthesized /dev/disk3, itself containing some partitions
- * which are mounted. /dev/disk2s2 is not considered to be mounted, in this case. So we need to know that
- * disk2s2 is related to disk3 so we can consider disk2s2 as mounted if any parts of disk3 are. In order to do
- * this I am taking the first two parts of the IODeviceTree and seeing if they exist anywhere in a
- * IOService identifier. If they do, I am assuming the IOService device is on the matching IODeviceTree device.
- *
- * Lots of this is guesswork and may be broken. In my defence the documentation that I have been able to
- * unearth is, to put it impolitely, crap.
- */
+static bool
+get_bool(CFDictionaryRef& description, void const* key)
+{
+ auto value = CFDictionaryGetValue(description, key);
+ if (!value) {
+ return false;
+ }
+
+ return CFBooleanGetValue(reinterpret_cast<CFBooleanRef>(value));
+}
+
static void
disk_appeared (DADiskRef disk, void* context)
@@ -339,32 +289,30 @@ disk_appeared (DADiskRef disk, void* context)
this_disk.model = get_model (description);
LOG_DISK("Vendor/model: %1 %2", this_disk.vendor.get_value_or("[none]"), this_disk.model.get_value_or("[none]"));
- auto media_path = analyse_media_path (description);
- if (!media_path) {
- LOG_DISK("Finding media path for %1 failed", bsd_name);
- return;
- }
-
- this_disk.media_path = *media_path;
- this_disk.whole = is_whole_drive (disk);
auto mp = mount_point (description);
if (mp) {
this_disk.mount_points.push_back (*mp);
}
- LOG_DISK(
- "%1 %2 mounted at %3",
- this_disk.media_path.real ? "Real" : "Synth",
- this_disk.whole ? "whole" : "part",
- mp ? mp->string() : "[nowhere]"
- );
-
auto media_size_cstr = CFDictionaryGetValue (description, kDADiskDescriptionMediaSizeKey);
if (!media_size_cstr) {
LOG_DISK_NC("Could not read media size");
return;
}
+ this_disk.system = get_bool(description, kDADiskDescriptionDeviceInternalKey) && !get_bool(description, kDADiskDescriptionMediaRemovableKey);
+ this_disk.writeable = get_bool(description, kDADiskDescriptionMediaWritableKey);
+ this_disk.partition = string(bsd_name).find("s", 5) != std::string::npos;
+
+ LOG_DISK(
+ "%1 %2 %3 %4 mounted at %5",
+ bsd_name,
+ this_disk.system ? "system" : "non-system",
+ this_disk.writeable ? "writeable" : "read-only",
+ this_disk.partition ? "partition" : "drive",
+ mp ? mp->string() : "[nowhere]"
+ );
+
CFNumberGetValue ((CFNumberRef) media_size_cstr, kCFNumberLongType, &this_disk.size);
CFRelease (description);
@@ -395,7 +343,12 @@ Drive::get ()
DAUnregisterCallback(session, (void *) disk_appeared, &disks);
CFRelease(session);
- auto drives = osx_disks_to_drives(disks);
+ vector<Drive> drives;
+ for (auto const& disk: disks) {
+ if (!disk.system && !disk.partition && disk.writeable) {
+ drives.push_back({disk.device, disk.mount_points, disk.size, disk.vendor, disk.model});
+ }
+ }
LOG_DISK("Drive::get() found %1 drives:", drives.size());
for (auto const& drive: drives) {
diff --git a/src/lib/dcp_content.cc b/src/lib/dcp_content.cc
index 6185b3a19..378ba1882 100644
--- a/src/lib/dcp_content.cc
+++ b/src/lib/dcp_content.cc
@@ -612,7 +612,7 @@ DCPContent::reel_split_points (shared_ptr<const Film> film) const
}
bool
-DCPContent::can_reference (shared_ptr<const Film> film, function<bool (shared_ptr<const Content>)> part, string overlapping, string& why_not) const
+DCPContent::can_reference_anything(shared_ptr<const Film> film, string& why_not) const
{
/* We must be using the same standard as the film */
if (_standard) {
@@ -658,15 +658,16 @@ DCPContent::can_reference (shared_ptr<const Film> film, function<bool (shared_pt
}
}
- auto a = overlaps (film, film->content(), part, position(), end(film));
- if (a.size() != 1 || a.front().get() != this) {
- why_not = overlapping;
- return false;
- }
-
return true;
}
+bool
+DCPContent::overlaps(shared_ptr<const Film> film, function<bool (shared_ptr<const Content>)> part) const
+{
+ auto const a = dcpomatic::overlaps(film, film->content(), part, position(), end(film));
+ return a.size() != 1 || a.front().get() != this;
+}
+
bool
DCPContent::can_reference_video (shared_ptr<const Film> film, string& why_not) const
@@ -691,15 +692,17 @@ DCPContent::can_reference_video (shared_ptr<const Film> film, string& why_not) c
return false;
}
- /// TRANSLATORS: this string will follow "Cannot reference this DCP: "
- return can_reference(
- film,
- [](shared_ptr<const Content> c) {
- return static_cast<bool>(c->video) && c->video->use();
- },
- _("it overlaps other video content; remove the other content."),
- why_not
- );
+ auto part = [](shared_ptr<const Content> c) {
+ return static_cast<bool>(c->video) && c->video->use();
+ };
+
+ if (overlaps(film, part)) {
+ /// TRANSLATORS: this string will follow "Cannot reference this DCP: "
+ why_not = _("it overlaps other video content.");
+ return false;
+ }
+
+ return can_reference_anything(film, why_not);
}
@@ -720,14 +723,17 @@ DCPContent::can_reference_audio (shared_ptr<const Film> film, string& why_not) c
return false;
}
- /// TRANSLATORS: this string will follow "Cannot reference this DCP: "
- return can_reference(
- film, [](shared_ptr<const Content> c) {
- return static_cast<bool>(c->audio) && !c->audio->mapping().mapped_output_channels().empty();
- },
- _("it overlaps other audio content; remove the other content."),
- why_not
- );
+ auto part = [](shared_ptr<const Content> c) {
+ return static_cast<bool>(c->audio) && !c->audio->mapping().mapped_output_channels().empty();
+ };
+
+ if (overlaps(film, part)) {
+ /// TRANSLATORS: this string will follow "Cannot reference this DCP: "
+ why_not = _("it overlaps other audio content.");
+ return false;
+ }
+
+ return can_reference_anything(film, why_not);
}
@@ -773,15 +779,17 @@ DCPContent::can_reference_text (shared_ptr<const Film> film, TextType type, stri
return false;
}
- /// TRANSLATORS: this string will follow "Cannot reference this DCP: "
- return can_reference(
- film,
- [type](shared_ptr<const Content> c) {
- return std::find_if(c->text.begin(), c->text.end(), [type](shared_ptr<const TextContent> t) { return t->type() == type; }) != c->text.end();
- },
- _("they overlap other text content; remove the other content."),
- why_not
- );
+ auto part = [type](shared_ptr<const Content> c) {
+ return std::find_if(c->text.begin(), c->text.end(), [type](shared_ptr<const TextContent> t) { return t->type() == type; }) != c->text.end();
+ };
+
+ if (overlaps(film, part)) {
+ /// TRANSLATORS: this string will follow "Cannot reference this DCP: "
+ why_not = _("it overlaps other text content.");
+ return false;
+ }
+
+ return can_reference_anything(film, why_not);
}
void
diff --git a/src/lib/dcp_content.h b/src/lib/dcp_content.h
index 3753740a2..d06d46523 100644
--- a/src/lib/dcp_content.h
+++ b/src/lib/dcp_content.h
@@ -100,6 +100,8 @@ public:
bool needs_kdm () const;
bool needs_assets () const;
+ bool can_reference_anything(std::shared_ptr<const Film> film, std::string& why_not) const;
+
void set_reference_video (bool r);
bool reference_video () const {
@@ -186,12 +188,7 @@ private:
void read_directory (boost::filesystem::path);
void read_sub_directory (boost::filesystem::path);
std::list<dcpomatic::DCPTimePeriod> reels (std::shared_ptr<const Film> film) const;
- bool can_reference (
- std::shared_ptr<const Film> film,
- std::function <bool (std::shared_ptr<const Content>)>,
- std::string overlapping,
- std::string& why_not
- ) const;
+ bool overlaps(std::shared_ptr<const Film> film, std::function<bool (std::shared_ptr<const Content>)> part) const;
std::string _name;
/** true if our DCP is encrypted */
diff --git a/src/lib/dcp_decoder.cc b/src/lib/dcp_decoder.cc
index 165b5bfb5..303126caa 100644
--- a/src/lib/dcp_decoder.cc
+++ b/src/lib/dcp_decoder.cc
@@ -180,7 +180,7 @@ DCPDecoder::pass ()
AV_PIX_FMT_XYZ12LE,
_forced_reduction
),
- _offset + frame
+ ContentTime::from_frames(_offset + frame, vfr)
);
} else {
video->emit (
@@ -192,7 +192,7 @@ DCPDecoder::pass ()
AV_PIX_FMT_XYZ12LE,
_forced_reduction
),
- _offset + frame
+ ContentTime::from_frames(_offset + frame, vfr)
);
video->emit (
@@ -204,7 +204,7 @@ DCPDecoder::pass ()
AV_PIX_FMT_XYZ12LE,
_forced_reduction
),
- _offset + frame
+ ContentTime::from_frames(_offset + frame, vfr)
);
}
}
diff --git a/src/lib/dcp_encoder.cc b/src/lib/dcp_encoder.cc
index 9a840c8ab..bd78312fa 100644
--- a/src/lib/dcp_encoder.cc
+++ b/src/lib/dcp_encoder.cc
@@ -18,6 +18,7 @@
*/
+
/** @file src/dcp_encoder.cc
* @brief A class which takes a Film and some Options, then uses those to encode the film into a DCP.
*
@@ -25,31 +26,33 @@
* as a parameter to the constructor.
*/
+
+#include "audio_decoder.h"
+#include "compose.hpp"
#include "dcp_encoder.h"
-#include "j2k_encoder.h"
#include "film.h"
-#include "video_decoder.h"
-#include "audio_decoder.h"
-#include "player.h"
+#include "j2k_encoder.h"
#include "job.h"
-#include "writer.h"
-#include "compose.hpp"
+#include "player.h"
+#include "player_video.h"
#include "referenced_reel_asset.h"
#include "text_content.h"
-#include "player_video.h"
+#include "video_decoder.h"
+#include "writer.h"
#include <boost/signals2.hpp>
#include <iostream>
#include "i18n.h"
-using std::string;
+
using std::cout;
+using std::dynamic_pointer_cast;
using std::list;
-using std::vector;
+using std::make_shared;
using std::shared_ptr;
+using std::string;
+using std::vector;
using std::weak_ptr;
-using std::dynamic_pointer_cast;
-using std::make_shared;
using boost::optional;
#if BOOST_VERSION >= 106100
using namespace boost::placeholders;
@@ -118,6 +121,20 @@ DCPEncoder::go ()
_writer.finish(_film->dir(_film->dcp_name()));
}
+
+void
+DCPEncoder::pause()
+{
+ _j2k_encoder.pause();
+}
+
+
+void
+DCPEncoder::resume()
+{
+ _j2k_encoder.resume();
+}
+
void
DCPEncoder::video (shared_ptr<PlayerVideo> data, DCPTime time)
{
diff --git a/src/lib/dcp_encoder.h b/src/lib/dcp_encoder.h
index ad77f6951..ce0b72204 100644
--- a/src/lib/dcp_encoder.h
+++ b/src/lib/dcp_encoder.h
@@ -35,6 +35,8 @@ class Job;
class Player;
class PlayerVideo;
+struct frames_not_lost_when_threads_disappear;
+
/** @class DCPEncoder */
class DCPEncoder : public Encoder
@@ -53,8 +55,13 @@ public:
return _finishing;
}
+ void pause() override;
+ void resume() override;
+
private:
+ friend struct ::frames_not_lost_when_threads_disappear;
+
void video (std::shared_ptr<PlayerVideo>, dcpomatic::DCPTime);
void audio (std::shared_ptr<AudioBuffers>, dcpomatic::DCPTime);
void text (PlayerText, TextType, boost::optional<DCPTextTrack>, dcpomatic::DCPTimePeriod);
diff --git a/src/lib/dcp_video.cc b/src/lib/dcp_video.cc
index 217b72183..3769a3285 100644
--- a/src/lib/dcp_video.cc
+++ b/src/lib/dcp_video.cc
@@ -117,6 +117,30 @@ DCPVideo::convert_to_xyz (shared_ptr<const PlayerVideo> frame)
return xyz;
}
+dcp::Size
+DCPVideo::get_size() const
+{
+ auto image = _frame->image(bind(&PlayerVideo::keep_xyz_or_rgb, _1), VideoRange::FULL, false);
+ return image->size();
+}
+
+
+void
+DCPVideo::convert_to_xyz(uint16_t* dst) const
+{
+ auto image = _frame->image(bind(&PlayerVideo::keep_xyz_or_rgb, _1), VideoRange::FULL, false);
+ if (_frame->colour_conversion()) {
+ dcp::rgb_to_xyz (
+ image->data()[0],
+ dst,
+ image->size(),
+ image->stride()[0],
+ _frame->colour_conversion().get()
+ );
+ }
+}
+
+
/** J2K-encode this frame on the local host.
* @return Encoded data.
*/
diff --git a/src/lib/dcp_video.h b/src/lib/dcp_video.h
index bf95ccfe6..d07c8322b 100644
--- a/src/lib/dcp_video.h
+++ b/src/lib/dcp_video.h
@@ -17,6 +17,8 @@
along with DCP-o-matic. If not, see <http://www.gnu.org/licenses/>.
*/
+#ifndef DCPOMATIC_DCP_VIDEO_H
+#define DCPOMATIC_DCP_VIDEO_H
#include "encode_server_description.h"
@@ -66,6 +68,9 @@ public:
static std::shared_ptr<dcp::OpenJPEGImage> convert_to_xyz(std::shared_ptr<const PlayerVideo> frame);
+ void convert_to_xyz(uint16_t* dst) const;
+ dcp::Size get_size() const;
+
private:
void add_metadata (xmlpp::Element *) const;
@@ -76,3 +81,5 @@ private:
int _j2k_bandwidth; ///< J2K bandwidth to use
Resolution _resolution; ///< Resolution (2K or 4K)
};
+
+#endif
diff --git a/src/lib/dcpomatic_time.h b/src/lib/dcpomatic_time.h
index 1b12ea901..9ebb334fe 100644
--- a/src/lib/dcpomatic_time.h
+++ b/src/lib/dcpomatic_time.h
@@ -152,6 +152,10 @@ public:
return *this;
}
+ Time<S, O> operator* (int o) const {
+ return Time<S, O> (_t * o);
+ }
+
Time<S, O> operator/ (int o) const {
return Time<S, O> (_t / o);
}
diff --git a/src/lib/dkdm_recipient.cc b/src/lib/dkdm_recipient.cc
index c73379bed..8f3bfea2e 100644
--- a/src/lib/dkdm_recipient.cc
+++ b/src/lib/dkdm_recipient.cc
@@ -24,7 +24,6 @@
#include "film.h"
#include "kdm_with_metadata.h"
#include <dcp/raw_convert.h>
-#include <dcp/utc_offset.h>
using std::make_shared;
@@ -40,9 +39,6 @@ DKDMRecipient::DKDMRecipient (cxml::ConstNodePtr node)
for (auto i: node->node_children("Email")) {
emails.push_back (i->content());
}
-
- utc_offset_hour = node->number_child<int>("UTCOffsetHour");
- utc_offset_minute = node->number_child<int>("UTCOffsetMinute");
}
@@ -54,9 +50,6 @@ DKDMRecipient::as_xml (xmlpp::Element* node) const
for (auto i: emails) {
node->add_child("Email")->add_child_text(i);
}
-
- node->add_child("UTCOffsetHour")->add_child_text(raw_convert<string>(utc_offset_hour));
- node->add_child("UTCOffsetMinute")->add_child_text(raw_convert<string>(utc_offset_minute));
}
@@ -65,29 +58,26 @@ kdm_for_dkdm_recipient (
shared_ptr<const Film> film,
boost::filesystem::path cpl,
shared_ptr<DKDMRecipient> recipient,
- boost::posix_time::ptime valid_from,
- boost::posix_time::ptime valid_to
+ dcp::LocalTime valid_from,
+ dcp::LocalTime valid_to
)
{
if (!recipient->recipient) {
return {};
}
- dcp::LocalTime const begin(valid_from, dcp::UTCOffset(recipient->utc_offset_hour, recipient->utc_offset_minute));
- dcp::LocalTime const end (valid_to, dcp::UTCOffset(recipient->utc_offset_hour, recipient->utc_offset_minute));
-
auto signer = Config::instance()->signer_chain();
if (!signer->valid()) {
throw InvalidSignerError();
}
- auto const decrypted_kdm = film->make_kdm(cpl, begin, end);
+ auto const decrypted_kdm = film->make_kdm(cpl, valid_from, valid_to);
auto const kdm = decrypted_kdm.encrypt(signer, recipient->recipient.get(), {}, dcp::Formulation::MODIFIED_TRANSITIONAL_1, true, 0);
dcp::NameFormat::Map name_values;
name_values['f'] = kdm.content_title_text();
- name_values['b'] = begin.date() + " " + begin.time_of_day(true, false);
- name_values['e'] = end.date() + " " + end.time_of_day(true, false);
+ name_values['b'] = valid_from.date() + " " + valid_from.time_of_day(true, false);
+ name_values['e'] = valid_to.date() + " " + valid_to.time_of_day(true, false);
name_values['i'] = kdm.cpl_id();
return make_shared<KDMWithMetadata>(name_values, nullptr, recipient->emails, kdm);
diff --git a/src/lib/dkdm_recipient.h b/src/lib/dkdm_recipient.h
index 7a0fa0185..3317ae6f9 100644
--- a/src/lib/dkdm_recipient.h
+++ b/src/lib/dkdm_recipient.h
@@ -33,14 +33,10 @@ public:
std::string const& name_,
std::string const& notes_,
boost::optional<dcp::Certificate> recipient_,
- std::vector<std::string> emails_,
- int utc_offset_hour_,
- int utc_offset_minute_
+ std::vector<std::string> emails_
)
: KDMRecipient (name_, notes_, recipient_, boost::none)
, emails (emails_)
- , utc_offset_hour (utc_offset_hour_)
- , utc_offset_minute (utc_offset_minute_)
{
}
@@ -50,8 +46,6 @@ public:
void as_xml (xmlpp::Element *) const override;
std::vector<std::string> emails;
- int utc_offset_hour;
- int utc_offset_minute;
};
@@ -60,7 +54,7 @@ kdm_for_dkdm_recipient (
std::shared_ptr<const Film> film,
boost::filesystem::path cpl,
std::shared_ptr<DKDMRecipient> recipient,
- boost::posix_time::ptime valid_from,
- boost::posix_time::ptime valid_to
+ dcp::LocalTime valid_from,
+ dcp::LocalTime valid_to
);
diff --git a/src/lib/dkdm_wrapper.cc b/src/lib/dkdm_wrapper.cc
index 016c77c3f..7beaae8f4 100644
--- a/src/lib/dkdm_wrapper.cc
+++ b/src/lib/dkdm_wrapper.cc
@@ -41,7 +41,11 @@ DKDMBase::read (cxml::ConstNodePtr node)
if (node->name() == "DKDM") {
return make_shared<DKDM>(dcp::EncryptedKDM(node->content()));
} else if (node->name() == "DKDMGroup") {
- auto group = make_shared<DKDMGroup>(node->string_attribute("Name"));
+ auto name = node->optional_string_attribute("Name");
+ if (!name) {
+ name = node->string_attribute("name");
+ }
+ auto group = make_shared<DKDMGroup>(*name);
for (auto i: node->node_children()) {
if (auto c = read(i)) {
group->add (c);
@@ -72,7 +76,7 @@ void
DKDMGroup::as_xml (xmlpp::Element* node) const
{
auto f = node->add_child("DKDMGroup");
- f->set_attribute ("Name", _name);
+ f->set_attribute("name", _name);
for (auto i: _children) {
i->as_xml (f);
}
diff --git a/src/lib/encode_server.cc b/src/lib/encode_server.cc
index 036ea58a5..7eae4375f 100644
--- a/src/lib/encode_server.cc
+++ b/src/lib/encode_server.cc
@@ -81,6 +81,7 @@ EncodeServer::EncodeServer (bool verbose, int num_threads)
#endif
, _verbose (verbose)
, _num_threads (num_threads)
+ , _frames_encoded(0)
{
}
@@ -169,6 +170,8 @@ EncodeServer::process (shared_ptr<Socket> socket, struct timeval& after_read, st
throw;
}
+ ++_frames_encoded;
+
return dcp_video_frame.index ();
}
diff --git a/src/lib/encode_server.h b/src/lib/encode_server.h
index f93d66746..8059abd0f 100644
--- a/src/lib/encode_server.h
+++ b/src/lib/encode_server.h
@@ -32,6 +32,7 @@
#include "exception_store.h"
#include "server.h"
#include <boost/asio.hpp>
+#include <boost/atomic.hpp>
#include <boost/thread.hpp>
#include <boost/thread/condition.hpp>
#include <string>
@@ -53,6 +54,10 @@ public:
void run () override;
+ int frames_encoded() const {
+ return _frames_encoded;
+ }
+
private:
void handle (std::shared_ptr<Socket>) override;
void worker_thread ();
@@ -67,6 +72,7 @@ private:
bool _verbose;
int _num_threads;
Waker _waker;
+ boost::atomic<int> _frames_encoded;
struct Broadcast {
diff --git a/src/lib/encode_server_finder.h b/src/lib/encode_server_finder.h
index f8a30af54..c478387f9 100644
--- a/src/lib/encode_server_finder.h
+++ b/src/lib/encode_server_finder.h
@@ -50,8 +50,6 @@ public:
static EncodeServerFinder* instance ();
static void drop ();
- void stop ();
-
std::list<EncodeServerDescription> servers () const;
/** Emitted whenever the list of servers changes */
@@ -62,6 +60,7 @@ private:
~EncodeServerFinder ();
void start ();
+ void stop ();
void search_thread ();
void listen_thread ();
diff --git a/src/lib/encoder.h b/src/lib/encoder.h
index 9b67720d3..aeaf7f620 100644
--- a/src/lib/encoder.h
+++ b/src/lib/encoder.h
@@ -58,6 +58,8 @@ public:
/** @return the number of frames that are done */
virtual Frame frames_done () const = 0;
virtual bool finishing () const = 0;
+ virtual void pause() {}
+ virtual void resume() {}
protected:
std::shared_ptr<const Film> _film;
diff --git a/src/lib/ffmpeg_decoder.cc b/src/lib/ffmpeg_decoder.cc
index 7f7a07863..09db1ff1c 100644
--- a/src/lib/ffmpeg_decoder.cc
+++ b/src/lib/ffmpeg_decoder.cc
@@ -178,9 +178,8 @@ FFmpegDecoder::flush_fill()
full_length = full_length.ceil (frc.source);
if (video && !video->ignore()) {
double const vfr = _ffmpeg_content->video_frame_rate().get();
- auto const f = full_length.frames_round (vfr);
- auto const v = video->position(film()).get_value_or(ContentTime()).frames_round(vfr) + 1;
- if (v < f) {
+ auto const v = video->position(film()).get_value_or(ContentTime()) + ContentTime::from_frames(1, vfr);
+ if (v < full_length) {
video->emit(film(), make_shared<const RawImageProxy>(_black_image), v);
did_something = true;
}
@@ -260,7 +259,7 @@ deinterleave_audio(AVFrame* frame)
/* XXX: can't we use swr_convert() to do the format conversion? */
- int const channels = frame->channels;
+ int const channels = frame->ch_layout.nb_channels;
int const frames = frame->nb_samples;
int const total_samples = frames * channels;
auto audio = make_shared<AudioBuffers>(channels, frames);
@@ -622,7 +621,7 @@ FFmpegDecoder::process_video_frame ()
video->emit (
film(),
make_shared<RawImageProxy>(image),
- llrint(pts * _ffmpeg_content->active_video_frame_rate(film()))
+ ContentTime::from_seconds(pts)
);
} else {
LOG_WARNING_NC ("Dropping frame without PTS");
diff --git a/src/lib/ffmpeg_examiner.cc b/src/lib/ffmpeg_examiner.cc
index 15cb14ad5..583ea1297 100644
--- a/src/lib/ffmpeg_examiner.cc
+++ b/src/lib/ffmpeg_examiner.cc
@@ -73,14 +73,6 @@ FFmpegExaminer::FFmpegExaminer (shared_ptr<const FFmpegContent> c, shared_ptr<Jo
auto codec = _codec_context[i] ? _codec_context[i]->codec : nullptr;
if (s->codecpar->codec_type == AVMEDIA_TYPE_AUDIO && codec) {
- /* This is a hack; sometimes it seems that _audio_codec_context->channel_layout isn't set up,
- so bodge it here. No idea why we should have to do this.
- */
-
- if (s->codecpar->channel_layout == 0) {
- s->codecpar->channel_layout = av_get_default_channel_layout (s->codecpar->channels);
- }
-
DCPOMATIC_ASSERT (_format_context->duration != AV_NOPTS_VALUE);
DCPOMATIC_ASSERT (codec->name);
@@ -91,7 +83,7 @@ FFmpegExaminer::FFmpegExaminer (shared_ptr<const FFmpegContent> c, shared_ptr<Jo
s->id,
s->codecpar->sample_rate,
llrint ((double(_format_context->duration) / AV_TIME_BASE) * s->codecpar->sample_rate),
- s->codecpar->channels,
+ s->codecpar->ch_layout.nb_channels,
s->codecpar->bits_per_raw_sample ? s->codecpar->bits_per_raw_sample : s->codecpar->bits_per_coded_sample
)
);
@@ -161,7 +153,7 @@ FFmpegExaminer::FFmpegExaminer (shared_ptr<const FFmpegContent> c, shared_ptr<Jo
av_packet_free (&packet);
- if (_first_video && got_all_audio && temporal_reference.size() >= (PULLDOWN_CHECK_FRAMES * 2)) {
+ if (got_all_audio && (!_video_stream || (_first_video && temporal_reference.size() >= (PULLDOWN_CHECK_FRAMES * 2)))) {
/* All done */
break;
}
@@ -181,7 +173,6 @@ FFmpegExaminer::FFmpegExaminer (shared_ptr<const FFmpegContent> c, shared_ptr<Jo
/* This code taken from get_rotation() in ffmpeg:cmdutils.c */
auto stream = _format_context->streams[*_video_stream];
auto rotate_tag = av_dict_get (stream->metadata, "rotate", 0, 0);
- uint8_t* displaymatrix = av_stream_get_side_data (stream, AV_PKT_DATA_DISPLAYMATRIX, 0);
_rotation = 0;
if (rotate_tag && *rotate_tag->value && strcmp(rotate_tag->value, "0")) {
@@ -192,8 +183,9 @@ FFmpegExaminer::FFmpegExaminer (shared_ptr<const FFmpegContent> c, shared_ptr<Jo
}
}
- if (displaymatrix && !_rotation) {
- _rotation = - av_display_rotation_get ((int32_t*) displaymatrix);
+ auto side_data = av_packet_side_data_get(stream->codecpar->coded_side_data, stream->codecpar->nb_coded_side_data, AV_PKT_DATA_DISPLAYMATRIX);
+ if (side_data && !_rotation) {
+ _rotation = - av_display_rotation_get(reinterpret_cast<int32_t*>(side_data->data));
}
_rotation = *_rotation - 360 * floor (*_rotation / 360 + 0.9 / 360);
@@ -252,7 +244,7 @@ FFmpegExaminer::video_packet (AVCodecContext* context, string& temporal_referenc
).get_value_or (ContentTime ()).frames_round (video_frame_rate().get ());
}
if (temporal_reference.size() < (PULLDOWN_CHECK_FRAMES * 2)) {
- temporal_reference += (_video_frame->top_field_first ? "T" : "B");
+ temporal_reference += ((_video_frame->flags & AV_FRAME_FLAG_TOP_FIELD_FIRST) ? "T" : "B");
temporal_reference += (_video_frame->repeat_pict ? "3" : "2");
}
diff --git a/src/lib/ffmpeg_file_encoder.cc b/src/lib/ffmpeg_file_encoder.cc
index 6d1ad68f7..d7833265d 100644
--- a/src/lib/ffmpeg_file_encoder.cc
+++ b/src/lib/ffmpeg_file_encoder.cc
@@ -73,8 +73,7 @@ public:
_codec_context->bit_rate = channels * 128 * 1024;
_codec_context->sample_fmt = sample_format;
_codec_context->sample_rate = frame_rate;
- _codec_context->channel_layout = av_get_default_channel_layout (channels);
- _codec_context->channels = channels;
+ av_channel_layout_default(&_codec_context->ch_layout, channels);
int r = avcodec_open2 (_codec_context, _codec, 0);
if (r < 0) {
@@ -143,7 +142,7 @@ public:
frame->nb_samples = size;
frame->format = _codec_context->sample_fmt;
- frame->channels = channels;
+ frame->ch_layout.nb_channels = channels;
int r = avcodec_fill_audio_frame (frame, channels, _codec_context->sample_fmt, (const uint8_t *) samples, buffer_size, 0);
DCPOMATIC_ASSERT (r >= 0);
diff --git a/src/lib/film.cc b/src/lib/film.cc
index d9ab6e2a3..d747efb0e 100644
--- a/src/lib/film.cc
+++ b/src/lib/film.cc
@@ -114,6 +114,7 @@ using namespace dcpomatic;
static constexpr char metadata_file[] = "metadata.xml";
+static constexpr char ui_state_file[] = "ui.xml";
/* 5 -> 6
@@ -416,7 +417,7 @@ Film::metadata (bool with_content_paths) const
root->add_child("UserExplicitVideoFrameRate")->add_child_text(_user_explicit_video_frame_rate ? "1" : "0");
for (auto const& marker: _markers) {
auto m = root->add_child("Marker");
- m->set_attribute("Type", dcp::marker_to_string(marker.first));
+ m->set_attribute("type", dcp::marker_to_string(marker.first));
m->add_child_text(raw_convert<string>(marker.second.get()));
}
for (auto i: _ratings) {
@@ -603,7 +604,11 @@ Film::read_metadata (optional<boost::filesystem::path> path)
_user_explicit_video_frame_rate = f.optional_bool_child("UserExplicitVideoFrameRate").get_value_or(false);
for (auto i: f.node_children("Marker")) {
- _markers[dcp::marker_from_string(i->string_attribute("Type"))] = DCPTime(dcp::raw_convert<DCPTime::Type>(i->content()));
+ auto type = i->optional_string_attribute("Type");
+ if (!type) {
+ type = i->string_attribute("type");
+ }
+ _markers[dcp::marker_from_string(*type)] = DCPTime(dcp::raw_convert<DCPTime::Type>(i->content()));
}
for (auto i: f.node_children("Rating")) {
@@ -2230,3 +2235,53 @@ Film::set_territory_type(TerritoryType type)
_territory_type = type;
}
+
+void
+Film::set_ui_state(string key, string value)
+{
+ _ui_state[key] = value;
+ write_ui_state();
+}
+
+
+boost::optional<std::string>
+Film::ui_state(string key) const
+{
+ auto iter = _ui_state.find(key);
+ if (iter == _ui_state.end()) {
+ return {};
+ }
+
+ return iter->second;
+}
+
+
+void
+Film::write_ui_state() const
+{
+ auto doc = make_shared<xmlpp::Document>();
+ auto root = doc->create_root_node("UI");
+
+ for (auto state: _ui_state) {
+ root->add_child(state.first)->add_child_text(state.second);
+ }
+
+ try {
+ doc->write_to_file_formatted(dcp::filesystem::fix_long_path(file(ui_state_file)).string());
+ } catch (...) {}
+}
+
+
+void
+Film::read_ui_state()
+{
+ try {
+ cxml::Document xml("UI");
+ xml.read_file(dcp::filesystem::fix_long_path(file(ui_state_file)));
+ for (auto node: xml.node_children()) {
+ if (!node->is_text()) {
+ _ui_state[node->name()] = node->content();
+ }
+ }
+ } catch (...) {}
+}
diff --git a/src/lib/film.h b/src/lib/film.h
index 43a41ad45..036bbed7e 100644
--- a/src/lib/film.h
+++ b/src/lib/film.h
@@ -434,6 +434,10 @@ public:
void add_ffoc_lfoc (Markers& markers) const;
+ void set_ui_state(std::string key, std::string value);
+ boost::optional<std::string> ui_state(std::string key) const;
+ void read_ui_state();
+
/** Emitted when some property has of the Film is about to change or has changed */
mutable boost::signals2::signal<void (ChangeType, FilmProperty)> Change;
@@ -477,6 +481,7 @@ private:
void check_settings_consistency ();
void maybe_set_container_and_resolution ();
void set_dirty (bool dirty);
+ void write_ui_state() const;
/** Log to write to */
std::shared_ptr<Log> _log;
@@ -562,6 +567,8 @@ private:
*/
bool _tolerant;
+ std::map<std::string, std::string> _ui_state;
+
mutable boost::mutex _info_file_mutex;
boost::signals2::scoped_connection _playlist_change_connection;
diff --git a/src/lib/grok/context.h b/src/lib/grok/context.h
new file mode 100644
index 000000000..521faae8d
--- /dev/null
+++ b/src/lib/grok/context.h
@@ -0,0 +1,291 @@
+/*
+ Copyright (C) 2023 Grok Image Compression Inc.
+
+ 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/>.
+
+*/
+
+#pragma once
+
+
+#include "../config.h"
+#include "../dcp_video.h"
+#include "../film.h"
+#include "../log.h"
+#include "../dcpomatic_log.h"
+#include "../writer.h"
+#include "messenger.h"
+#include <dcp/array_data.h>
+#include <boost/filesystem.hpp>
+
+
+static std::mutex launchMutex;
+
+namespace grk_plugin
+{
+
+struct GrokLogger : public MessengerLogger {
+ explicit GrokLogger(const std::string &preamble) : MessengerLogger(preamble)
+ {}
+ virtual ~GrokLogger() = default;
+ void info(const char* fmt, ...) override{
+ va_list arg;
+ va_start(arg, fmt);
+ dcpomatic_log->log(preamble_ + log_message(fmt, arg),LogEntry::TYPE_GENERAL);
+ va_end(arg);
+ }
+ void warn(const char* fmt, ...) override{
+ va_list arg;
+ va_start(arg, fmt);
+ dcpomatic_log->log(preamble_ + log_message(fmt, arg),LogEntry::TYPE_WARNING);
+ va_end(arg);
+ }
+ void error(const char* fmt, ...) override{
+ va_list arg;
+ va_start(arg, fmt);
+ dcpomatic_log->log(preamble_ + log_message(fmt, arg),LogEntry::TYPE_ERROR);
+ va_end(arg);
+ }
+};
+
+struct FrameProxy {
+ FrameProxy(int index, Eyes eyes, DCPVideo dcpv) : index_(index), eyes_(eyes), vf(dcpv)
+ {}
+ int index() const {
+ return index_;
+ }
+ Eyes eyes(void) const {
+ return eyes_;
+ }
+ int index_;
+ Eyes eyes_;
+ DCPVideo vf;
+};
+
+struct DcpomaticContext
+{
+ DcpomaticContext(
+ std::shared_ptr<const Film> film_,
+ Writer& writer_,
+ EventHistory& history_,
+ boost::filesystem::path const& location_
+ )
+ : film(film_)
+ , writer(writer_)
+ , history(history_)
+ , location(location_)
+ {
+
+ }
+
+ void set_dimensions(uint32_t w, uint32_t h)
+ {
+ width = w;
+ height = h;
+ }
+
+ std::shared_ptr<const Film> film;
+ Writer& writer;
+ EventHistory& history;
+ boost::filesystem::path location;
+ uint32_t width = 0;
+ uint32_t height = 0;
+};
+
+
+class GrokContext
+{
+public:
+ explicit GrokContext(DcpomaticContext* dcpomatic_context)
+ : _dcpomatic_context(dcpomatic_context)
+ {
+ auto grok = Config::instance()->grok().get_value_or({});
+ if (!grok.enable) {
+ return;
+ }
+
+ boost::filesystem::path folder(_dcpomatic_context->location);
+ boost::filesystem::path binary_path = folder / "grk_compress";
+ if (!boost::filesystem::exists(binary_path)) {
+ getMessengerLogger()->error(
+ "Invalid binary location %s", _dcpomatic_context->location.c_str()
+ );
+ return;
+ }
+
+ auto proc = [this](const std::string& str) {
+ try {
+ Msg msg(str);
+ auto tag = msg.next();
+ if (tag == GRK_MSGR_BATCH_SUBMIT_COMPRESSED) {
+ auto clientFrameId = msg.nextUint();
+ msg.nextUint(); // compressed frame ID
+ auto compressedFrameLength = msg.nextUint();
+ auto processor = [this](FrameProxy srcFrame, uint8_t* compressed, uint32_t compressedFrameLength) {
+ auto compressed_data = std::make_shared<dcp::ArrayData>(compressed, compressedFrameLength);
+ _dcpomatic_context->writer.write(compressed_data, srcFrame.index(), srcFrame.eyes());
+ frame_done ();
+ };
+
+ int const minimum_size = 16384;
+
+ bool needsRecompression = compressedFrameLength < minimum_size;
+ _messenger->processCompressed(str, processor, needsRecompression);
+
+ if (needsRecompression) {
+ auto fp = _messenger->retrieve(clientFrameId);
+ if (!fp) {
+ return;
+ }
+
+ auto encoded = std::make_shared<dcp::ArrayData>(fp->vf.encode_locally());
+ _dcpomatic_context->writer.write(encoded, fp->vf.index(), fp->vf.eyes());
+ frame_done ();
+ }
+ }
+ } catch (std::exception& ex) {
+ getMessengerLogger()->error("%s",ex.what());
+ }
+ };
+
+ auto clientInit = MessengerInit(
+ clientToGrokMessageBuf,
+ clientSentSynch,
+ grokReceiveReadySynch,
+ grokToClientMessageBuf,
+ grokSentSynch,
+ clientReceiveReadySynch,
+ proc,
+ std::thread::hardware_concurrency()
+ );
+
+ _messenger = new ScheduledMessenger<FrameProxy>(clientInit);
+ }
+
+ ~GrokContext()
+ {
+ shutdown();
+ }
+
+ bool launch(DCPVideo dcpv, int device)
+ {
+ namespace fs = boost::filesystem;
+
+ if (!_messenger) {
+ return false;
+ }
+ if (_launched) {
+ return true;
+ }
+ if (_launch_failed) {
+ return false;
+ }
+
+ std::unique_lock<std::mutex> lk_global(launchMutex);
+
+ if (!_messenger) {
+ return false;
+ }
+ if (_launched) {
+ return true;
+ }
+ if (_launch_failed) {
+ return false;
+ }
+
+ if (MessengerInit::firstLaunch(true)) {
+
+ if (!fs::exists(_dcpomatic_context->location) || !fs::is_directory(_dcpomatic_context->location)) {
+ getMessengerLogger()->error("Invalid directory %s", _dcpomatic_context->location.c_str());
+ return false;
+ }
+
+ auto s = dcpv.get_size();
+ _dcpomatic_context->set_dimensions(s.width, s.height);
+ auto grok = Config::instance()->grok().get_value_or({});
+ if (!_messenger->launchGrok(
+ _dcpomatic_context->location,
+ _dcpomatic_context->width,
+ _dcpomatic_context->width,
+ _dcpomatic_context->height,
+ 3,
+ 12,
+ device,
+ _dcpomatic_context->film->resolution() == Resolution::FOUR_K,
+ _dcpomatic_context->film->video_frame_rate(),
+ _dcpomatic_context->film->j2k_bandwidth(),
+ grok.licence_server,
+ grok.licence_port,
+ grok.licence)) {
+ _launch_failed = true;
+ return false;
+ }
+ }
+
+ _launched = _messenger->waitForClientInit();
+ _launch_failed = _launched;
+
+ return _launched;
+ }
+
+ bool scheduleCompress(DCPVideo const& vf)
+ {
+ if (!_messenger) {
+ return false;
+ }
+
+ auto fp = FrameProxy(vf.index(), vf.eyes(), vf);
+ auto cvt = [this, &fp](BufferSrc src) {
+ fp.vf.convert_to_xyz((uint16_t*)src.framePtr_);
+ };
+
+ return _messenger->scheduleCompress(fp, cvt);
+ }
+
+ void shutdown()
+ {
+ if (!_messenger) {
+ return;
+ }
+
+ std::unique_lock<std::mutex> lk_global(launchMutex);
+
+ if (!_messenger) {
+ return;
+ }
+
+ if (_launched) {
+ _messenger->shutdown();
+ }
+
+ delete _messenger;
+ _messenger = nullptr;
+ }
+
+ void frame_done()
+ {
+ _dcpomatic_context->history.event();
+ }
+
+private:
+ DcpomaticContext* _dcpomatic_context;
+ ScheduledMessenger<FrameProxy>* _messenger = nullptr;
+ bool _launched = false;
+ bool _launch_failed = false;
+};
+
+}
+
diff --git a/src/lib/grok/messenger.h b/src/lib/grok/messenger.h
new file mode 100644
index 000000000..eb2fe9560
--- /dev/null
+++ b/src/lib/grok/messenger.h
@@ -0,0 +1,906 @@
+/*
+ Copyright (C) 2023 Grok Image Compression Inc.
+
+ 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/>.
+
+*/
+#pragma once
+
+#include <iostream>
+#include <string>
+#include <cstring>
+#include <atomic>
+#include <functional>
+#include <sstream>
+#include <future>
+#include <map>
+#include <thread>
+#include <mutex>
+#include <condition_variable>
+#include <queue>
+#include <cassert>
+#include <cstdarg>
+
+#ifdef _WIN32
+#include <windows.h>
+#include <direct.h>
+#include <tlhelp32.h>
+#pragma warning(disable : 4100)
+#else
+#include <unistd.h>
+#include <fcntl.h>
+#include <sys/mman.h>
+#include <semaphore.h>
+#include <signal.h>
+#endif
+
+namespace grk_plugin
+{
+static std::string grokToClientMessageBuf = "Global\\grok_to_client_message";
+static std::string grokSentSynch = "Global\\grok_sent";
+static std::string clientReceiveReadySynch = "Global\\client_receive_ready";
+static std::string clientToGrokMessageBuf = "Global\\client_to_grok_message";
+static std::string clientSentSynch = "Global\\client_sent";
+static std::string grokReceiveReadySynch = "Global\\grok_receive_ready";
+static std::string grokUncompressedBuf = "Global\\grok_uncompressed_buf";
+static std::string grokCompressedBuf = "Global\\grok_compressed_buf";
+static const std::string GRK_MSGR_BATCH_IMAGE = "GRK_MSGR_BATCH_IMAGE";
+static const std::string GRK_MSGR_BATCH_COMPRESS_INIT = "GRK_MSGR_BATCH_COMPRESS_INIT";
+static const std::string GRK_MSGR_BATCH_SUBMIT_UNCOMPRESSED = "GRK_MSGR_BATCH_SUBMIT_UNCOMPRESSED";
+static const std::string GRK_MSGR_BATCH_PROCESSED_UNCOMPRESSED =
+ "GRK_MSGR_BATCH_PROCESSED_UNCOMPRESSED";
+static const std::string GRK_MSGR_BATCH_SUBMIT_COMPRESSED = "GRK_MSGR_BATCH_SUBMIT_COMPRESSED";
+static const std::string GRK_MSGR_BATCH_PROCESSSED_COMPRESSED =
+ "GRK_MSGR_BATCH_PROCESSSED_COMPRESSED";
+static const std::string GRK_MSGR_BATCH_SHUTDOWN = "GRK_MSGR_BATCH_SHUTDOWN";
+static const std::string GRK_MSGR_BATCH_FLUSH = "GRK_MSGR_BATCH_FLUSH";
+static const size_t messageBufferLen = 256;
+struct IMessengerLogger
+{
+ virtual ~IMessengerLogger(void) = default;
+ virtual void info(const char* fmt, ...) = 0;
+ virtual void warn(const char* fmt, ...) = 0;
+ virtual void error(const char* fmt, ...) = 0;
+
+ protected:
+ template<typename... Args>
+ std::string log_message(char const* const format, Args&... args) noexcept
+ {
+ constexpr size_t message_size = 512;
+ char message[message_size];
+
+ std::snprintf(message, message_size, format, args...);
+ return std::string(message);
+ }
+};
+struct MessengerLogger : public IMessengerLogger
+{
+ explicit MessengerLogger(const std::string &preamble) : preamble_(preamble) {}
+ virtual ~MessengerLogger() = default;
+ virtual void info(const char* fmt, ...) override
+ {
+ va_list args;
+ std::string new_fmt = preamble_ + fmt + "\n";
+ va_start(args, fmt);
+ vfprintf(stdout, new_fmt.c_str(), args);
+ va_end(args);
+ }
+ virtual void warn(const char* fmt, ...) override
+ {
+ va_list args;
+ std::string new_fmt = preamble_ + fmt + "\n";
+ va_start(args, fmt);
+ vfprintf(stdout, new_fmt.c_str(), args);
+ va_end(args);
+ }
+ virtual void error(const char* fmt, ...) override
+ {
+ va_list args;
+ std::string new_fmt = preamble_ + fmt + "\n";
+ va_start(args, fmt);
+ vfprintf(stderr, new_fmt.c_str(), args);
+ va_end(args);
+ }
+
+ protected:
+ std::string preamble_;
+};
+
+extern IMessengerLogger* sLogger;
+void setMessengerLogger(IMessengerLogger* logger);
+IMessengerLogger* getMessengerLogger(void);
+
+struct MessengerInit
+{
+ MessengerInit(const std::string &outBuf, const std::string &outSent,
+ const std::string &outReceiveReady, const std::string &inBuf,
+ const std::string &inSent,
+ const std::string &inReceiveReady,
+ std::function<void(std::string)> processor,
+ size_t numProcessingThreads)
+ : outboundMessageBuf(outBuf), outboundSentSynch(outSent),
+ outboundReceiveReadySynch(outReceiveReady), inboundMessageBuf(inBuf),
+ inboundSentSynch(inSent), inboundReceiveReadySynch(inReceiveReady), processor_(processor),
+ numProcessingThreads_(numProcessingThreads),
+ uncompressedFrameSize_(0), compressedFrameSize_(0),
+ numFrames_(0)
+ {
+ if(firstLaunch(true))
+ unlink();
+ }
+ void unlink(void)
+ {
+#ifndef _WIN32
+ shm_unlink(grokToClientMessageBuf.c_str());
+ shm_unlink(clientToGrokMessageBuf.c_str());
+#endif
+ }
+ static bool firstLaunch(bool isClient)
+ {
+ bool debugGrok = false;
+ return debugGrok != isClient;
+ }
+ std::string outboundMessageBuf;
+ std::string outboundSentSynch;
+ std::string outboundReceiveReadySynch;
+
+ std::string inboundMessageBuf;
+ std::string inboundSentSynch;
+ std::string inboundReceiveReadySynch;
+
+ std::function<void(std::string)> processor_;
+ size_t numProcessingThreads_;
+
+ size_t uncompressedFrameSize_;
+ size_t compressedFrameSize_;
+ size_t numFrames_;
+};
+
+/*************************** Synchronization *******************************/
+enum SynchDirection
+{
+ SYNCH_SENT,
+ SYNCH_RECEIVE_READY
+};
+
+typedef int grk_handle;
+struct Synch
+{
+ Synch(const std::string &sentSemName, const std::string &receiveReadySemName)
+ : sentSemName_(sentSemName), receiveReadySemName_(receiveReadySemName)
+ {
+ // unlink semaphores in case of previous crash
+ if(MessengerInit::firstLaunch(true))
+ unlink();
+ open();
+ }
+ ~Synch()
+ {
+ close();
+ if(MessengerInit::firstLaunch(true))
+ unlink();
+ }
+ void post(SynchDirection dir)
+ {
+ auto sem = (dir == SYNCH_SENT ? sentSem_ : receiveReadySem_);
+ int rc = sem_post(sem);
+ if(rc)
+ getMessengerLogger()->error("Error posting to semaphore: %s", strerror(errno));
+ }
+ void wait(SynchDirection dir)
+ {
+ auto sem = dir == SYNCH_SENT ? sentSem_ : receiveReadySem_;
+ int rc = sem_wait(sem);
+ if(rc)
+ getMessengerLogger()->error("Error waiting for semaphore: %s", strerror(errno));
+ }
+ void open(void)
+ {
+ sentSem_ = sem_open(sentSemName_.c_str(), O_CREAT, 0666, 0);
+ if(!sentSem_)
+ getMessengerLogger()->error("Error opening shared memory: %s", strerror(errno));
+ receiveReadySem_ = sem_open(receiveReadySemName_.c_str(), O_CREAT, 0666, 1);
+ if(!receiveReadySem_)
+ getMessengerLogger()->error("Error opening shared memory: %s", strerror(errno));
+ }
+ void close(void)
+ {
+ int rc = sem_close(sentSem_);
+ if(rc)
+ getMessengerLogger()->error("Error closing semaphore %s: %s", sentSemName_.c_str(),
+ strerror(errno));
+ rc = sem_close(receiveReadySem_);
+ if(rc)
+ getMessengerLogger()->error("Error closing semaphore %s: %s",
+ receiveReadySemName_.c_str(), strerror(errno));
+ }
+ void unlink(void)
+ {
+ int rc = sem_unlink(sentSemName_.c_str());
+ if(rc == -1 && errno != ENOENT)
+ getMessengerLogger()->error("Error unlinking semaphore %s: %s", sentSemName_.c_str(),
+ strerror(errno));
+ rc = sem_unlink(receiveReadySemName_.c_str());
+ if(rc == -1 && errno != ENOENT)
+ getMessengerLogger()->error("Error unlinking semaphore %s: %s",
+ receiveReadySemName_.c_str(), strerror(errno));
+ }
+ sem_t* sentSem_;
+ sem_t* receiveReadySem_;
+
+ private:
+ std::string sentSemName_;
+ std::string receiveReadySemName_;
+};
+struct SharedMemoryManager
+{
+ static bool initShm(const std::string &name, size_t len, grk_handle* shm_fd, char** buffer)
+ {
+ *shm_fd = shm_open(name.c_str(), O_CREAT | O_RDWR, 0666);
+ if(*shm_fd < 0)
+ {
+ getMessengerLogger()->error("Error opening shared memory: %s", strerror(errno));
+ return false;
+ }
+ int rc = ftruncate(*shm_fd, sizeof(char) * len);
+ if(rc)
+ {
+ getMessengerLogger()->error("Error truncating shared memory: %s", strerror(errno));
+ rc = close(*shm_fd);
+ if(rc)
+ getMessengerLogger()->error("Error closing shared memory: %s", strerror(errno));
+ rc = shm_unlink(name.c_str());
+ // 2 == No such file or directory
+ if(rc && errno != 2)
+ getMessengerLogger()->error("Error unlinking shared memory: %s", strerror(errno));
+ return false;
+ }
+ *buffer = static_cast<char*>(mmap(0, len, PROT_WRITE, MAP_SHARED, *shm_fd, 0));
+ if(!*buffer)
+ {
+ getMessengerLogger()->error("Error mapping shared memory: %s", strerror(errno));
+ rc = close(*shm_fd);
+ if(rc)
+ getMessengerLogger()->error("Error closing shared memory: %s", strerror(errno));
+ rc = shm_unlink(name.c_str());
+ // 2 == No such file or directory
+ if(rc && errno != 2)
+ getMessengerLogger()->error("Error unlinking shared memory: %s", strerror(errno));
+ }
+
+ return *buffer != nullptr;
+ }
+ static bool deinitShm(const std::string &name, size_t len, grk_handle &shm_fd, char** buffer)
+ {
+ if (!*buffer || !shm_fd)
+ return true;
+
+ int rc = munmap(*buffer, len);
+ *buffer = nullptr;
+ if(rc)
+ getMessengerLogger()->error("Error unmapping shared memory %s: %s", name.c_str(), strerror(errno));
+ rc = close(shm_fd);
+ shm_fd = 0;
+ if(rc)
+ getMessengerLogger()->error("Error closing shared memory %s: %s", name.c_str(), strerror(errno));
+ rc = shm_unlink(name.c_str());
+ // 2 == No such file or directory
+ if(rc && errno != 2)
+ fprintf(stderr,"Error unlinking shared memory %s : %s\n", name.c_str(), strerror(errno));
+
+ return true;
+ }
+};
+
+template<typename Data>
+class MessengerBlockingQueue
+{
+ public:
+ explicit MessengerBlockingQueue(size_t max) : active_(true), max_size_(max) {}
+ MessengerBlockingQueue() : MessengerBlockingQueue(UINT_MAX) {}
+ size_t size() const
+ {
+ return queue_.size();
+ }
+ // deactivate and clear queue
+ void deactivate()
+ {
+ {
+ std::lock_guard<std::mutex> lk(mutex_);
+ active_ = false;
+ while(!queue_.empty())
+ queue_.pop();
+ }
+
+ // release all waiting threads
+ can_pop_.notify_all();
+ can_push_.notify_all();
+ }
+ void activate()
+ {
+ std::lock_guard<std::mutex> lk(mutex_);
+ active_ = true;
+ }
+ bool push(Data const& value)
+ {
+ bool rc;
+ {
+ std::unique_lock<std::mutex> lk(mutex_);
+ rc = push_(value);
+ }
+ if(rc)
+ can_pop_.notify_one();
+
+ return rc;
+ }
+ bool waitAndPush(Data& value)
+ {
+ bool rc;
+ {
+ std::unique_lock<std::mutex> lk(mutex_);
+ if(!active_)
+ return false;
+ // in case of spurious wakeup, loop until predicate in lambda
+ // is satisfied.
+ can_push_.wait(lk, [this] { return queue_.size() < max_size_ || !active_; });
+ rc = push_(value);
+ }
+ if(rc)
+ can_pop_.notify_one();
+
+ return rc;
+ }
+ bool pop(Data& value)
+ {
+ bool rc;
+ {
+ std::unique_lock<std::mutex> lk(mutex_);
+ rc = pop_(value);
+ }
+ if(rc)
+ can_push_.notify_one();
+
+ return rc;
+ }
+ bool waitAndPop(Data& value)
+ {
+ bool rc;
+ {
+ std::unique_lock<std::mutex> lk(mutex_);
+ if(!active_)
+ return false;
+ // in case of spurious wakeup, loop until predicate in lambda
+ // is satisfied.
+ can_pop_.wait(lk, [this] { return !queue_.empty() || !active_; });
+ rc = pop_(value);
+ }
+ if(rc)
+ can_push_.notify_one();
+
+ return rc;
+ }
+
+ private:
+ bool push_(Data const& value)
+ {
+ if(queue_.size() == max_size_ || !active_)
+ return false;
+ queue_.push(value);
+
+ return true;
+ }
+ bool pop_(Data& value)
+ {
+ if(queue_.empty() || !active_)
+ return false;
+ value = queue_.front();
+ queue_.pop();
+
+ return true;
+ }
+ std::queue<Data> queue_;
+ mutable std::mutex mutex_;
+ std::condition_variable can_pop_;
+ std::condition_variable can_push_;
+ bool active_;
+ size_t max_size_;
+};
+struct BufferSrc
+{
+ BufferSrc(void) : BufferSrc("") {}
+ explicit BufferSrc(const std::string &file) : file_(file), clientFrameId_(0), frameId_(0), framePtr_(nullptr)
+ {}
+ BufferSrc(size_t clientFrameId, size_t frameId, uint8_t* framePtr)
+ : file_(""), clientFrameId_(clientFrameId), frameId_(frameId), framePtr_(framePtr)
+ {}
+ bool fromDisk(void)
+ {
+ return !file_.empty() && framePtr_ == nullptr;
+ }
+ size_t index() const
+ {
+ return clientFrameId_;
+ }
+ std::string file_;
+ size_t clientFrameId_;
+ size_t frameId_;
+ uint8_t* framePtr_;
+};
+
+struct Messenger;
+static void outboundThread(Messenger* messenger, const std::string &sendBuf, Synch* synch);
+static void inboundThread(Messenger* messenger, const std::string &receiveBuf, Synch* synch);
+static void processorThread(Messenger* messenger, std::function<void(std::string)> processor);
+
+struct Messenger
+{
+ explicit Messenger(MessengerInit init)
+ : running(true), _initialized(false), _shutdown(false), init_(init),
+ outboundSynch_(nullptr),
+ inboundSynch_(nullptr), uncompressed_buffer_(nullptr), compressed_buffer_(nullptr),
+ uncompressed_fd_(0), compressed_fd_(0)
+ {}
+ virtual ~Messenger(void)
+ {
+ running = false;
+ sendQueue.deactivate();
+ receiveQueue.deactivate();
+
+ if (outboundSynch_) {
+ outboundSynch_->post(SYNCH_RECEIVE_READY);
+ outbound.join();
+ }
+
+ if (inboundSynch_) {
+ inboundSynch_->post(SYNCH_SENT);
+ inbound.join();
+ }
+
+ for(auto& p : processors_)
+ p.join();
+
+ delete outboundSynch_;
+ delete inboundSynch_;
+
+ deinitShm();
+ }
+ void startThreads(void) {
+ outboundSynch_ =
+ new Synch(init_.outboundSentSynch, init_.outboundReceiveReadySynch);
+ outbound = std::thread(outboundThread, this, init_.outboundMessageBuf, outboundSynch_);
+
+ inboundSynch_ =
+ new Synch(init_.inboundSentSynch, init_.inboundReceiveReadySynch);
+ inbound = std::thread(inboundThread, this, init_.inboundMessageBuf, inboundSynch_);
+
+ for(size_t i = 0; i < init_.numProcessingThreads_; ++i)
+ processors_.push_back(std::thread(processorThread, this, init_.processor_));
+ }
+ bool initBuffers(void)
+ {
+ bool rc = true;
+ if(init_.uncompressedFrameSize_)
+ {
+ rc = rc && SharedMemoryManager::initShm(grokUncompressedBuf,
+ init_.uncompressedFrameSize_ * init_.numFrames_,
+ &uncompressed_fd_, &uncompressed_buffer_);
+ }
+ if(init_.compressedFrameSize_)
+ {
+ rc = rc && SharedMemoryManager::initShm(grokCompressedBuf,
+ init_.compressedFrameSize_ * init_.numFrames_,
+ &compressed_fd_, &compressed_buffer_);
+ }
+
+ return rc;
+ }
+
+ bool deinitShm(void)
+ {
+ bool rc = SharedMemoryManager::deinitShm(grokUncompressedBuf,
+ init_.uncompressedFrameSize_ * init_.numFrames_,
+ uncompressed_fd_, &uncompressed_buffer_);
+ rc = rc && SharedMemoryManager::deinitShm(grokCompressedBuf,
+ init_.compressedFrameSize_ * init_.numFrames_,
+ compressed_fd_, &compressed_buffer_);
+
+ return rc;
+ }
+ template<typename... Args>
+ void send(const std::string& str, Args... args)
+ {
+ std::ostringstream oss;
+ oss << str;
+ int dummy[] = {0, ((void)(oss << ',' << args), 0)...};
+ static_cast<void>(dummy);
+
+ sendQueue.push(oss.str());
+ }
+
+ bool launchGrok(
+ boost::filesystem::path const& dir,
+ uint32_t width,
+ uint32_t stride,
+ uint32_t height,
+ uint32_t samplesPerPixel,
+ uint32_t depth,
+ int device,
+ bool is4K,
+ uint32_t fps,
+ uint32_t bandwidth,
+ const std::string server,
+ uint32_t port,
+ const std::string license
+ )
+ {
+
+ std::unique_lock<std::mutex> lk(shutdownMutex_);
+ if (async_result_.valid())
+ return true;
+ if(MessengerInit::firstLaunch(true))
+ init_.unlink();
+ startThreads();
+ char _cmd[4096];
+ auto fullServer = server + ":" + std::to_string(port);
+ sprintf(_cmd,
+ "./grk_compress -batch_src %s,%d,%d,%d,%d,%d -out_fmt j2k -k 1 "
+ "-G %d -%s %d,%d -j %s -J %s -v",
+ GRK_MSGR_BATCH_IMAGE.c_str(), width, stride, height, samplesPerPixel, depth,
+ device, is4K ? "cinema4K" : "cinema2K", fps, bandwidth,
+ license.c_str(), fullServer.c_str());
+
+ return launch(_cmd, dir);
+ }
+ void initClient(size_t uncompressedFrameSize, size_t compressedFrameSize, size_t numFrames)
+ {
+ // client fills queue with pending uncompressed buffers
+ init_.uncompressedFrameSize_ = uncompressedFrameSize;
+ init_.compressedFrameSize_ = compressedFrameSize;
+ init_.numFrames_ = numFrames;
+ initBuffers();
+ auto ptr = uncompressed_buffer_;
+ for(size_t i = 0; i < init_.numFrames_; ++i)
+ {
+ availableBuffers_.push(BufferSrc(0, i, (uint8_t*)ptr));
+ ptr += init_.uncompressedFrameSize_;
+ }
+
+ std::unique_lock<std::mutex> lk(shutdownMutex_);
+ _initialized = true;
+ clientInitializedCondition_.notify_all();
+ }
+
+ bool waitForClientInit()
+ {
+ if (_initialized) {
+ return true;
+ } else if (_shutdown) {
+ return false;
+ }
+
+ std::unique_lock<std::mutex> lk(shutdownMutex_);
+
+ if (_initialized) {
+ return true;
+ } else if (_shutdown) {
+ return false;
+ }
+
+ while (true) {
+ if (clientInitializedCondition_.wait_for(lk, std::chrono::seconds(1), [this]{ return _initialized || _shutdown; })) {
+ break;
+ }
+ auto status = async_result_.wait_for(std::chrono::milliseconds(100));
+ if (status == std::future_status::ready) {
+ getMessengerLogger()->error("Grok exited unexpectedly during initialization");
+ return false;
+ }
+ }
+
+ return _initialized && !_shutdown;
+ }
+
+ static size_t uncompressedFrameSize(uint32_t w, uint32_t h, uint32_t samplesPerPixel)
+ {
+ return sizeof(uint16_t) * w * h * samplesPerPixel;
+ }
+ void reclaimCompressed(size_t frameId)
+ {
+ availableBuffers_.push(BufferSrc(0, frameId, getCompressedFrame(frameId)));
+ }
+ void reclaimUncompressed(size_t frameId)
+ {
+ availableBuffers_.push(BufferSrc(0, frameId, getUncompressedFrame(frameId)));
+ }
+ uint8_t* getUncompressedFrame(size_t frameId)
+ {
+ assert(frameId < init_.numFrames_);
+ if(frameId >= init_.numFrames_)
+ return nullptr;
+
+ return (uint8_t*)(uncompressed_buffer_ + frameId * init_.uncompressedFrameSize_);
+ }
+ uint8_t* getCompressedFrame(size_t frameId)
+ {
+ assert(frameId < init_.numFrames_);
+ if(frameId >= init_.numFrames_)
+ return nullptr;
+
+ return (uint8_t*)(compressed_buffer_ + frameId * init_.compressedFrameSize_);
+ }
+ std::atomic_bool running;
+ bool _initialized;
+ bool _shutdown;
+ MessengerBlockingQueue<std::string> sendQueue;
+ MessengerBlockingQueue<std::string> receiveQueue;
+ MessengerBlockingQueue<BufferSrc> availableBuffers_;
+ MessengerInit init_;
+ std::string cmd_;
+ std::future<int> async_result_;
+ std::mutex shutdownMutex_;
+ std::condition_variable shutdownCondition_;
+
+ protected:
+ std::condition_variable clientInitializedCondition_;
+ private:
+ bool launch(std::string const& cmd, boost::filesystem::path const& dir)
+ {
+ // Change the working directory
+ if(!dir.empty())
+ {
+ boost::system::error_code ec;
+ boost::filesystem::current_path(dir, ec);
+ if (ec) {
+ getMessengerLogger()->error("Error: failed to change the working directory");
+ return false;
+ }
+ }
+ // Execute the command using std::async and std::system
+ cmd_ = cmd;
+ getMessengerLogger()->info(cmd.c_str());
+ async_result_ = std::async(std::launch::async, [this]() { return std::system(cmd_.c_str()); });
+ bool success = async_result_.valid();
+ if (!success)
+ getMessengerLogger()->error("Grok launch failed");
+
+ return success;
+
+ }
+ std::thread outbound;
+ Synch* outboundSynch_;
+
+ std::thread inbound;
+ Synch* inboundSynch_;
+
+ std::vector<std::thread> processors_;
+ char* uncompressed_buffer_;
+ char* compressed_buffer_;
+
+ grk_handle uncompressed_fd_;
+ grk_handle compressed_fd_;
+};
+
+static void outboundThread(Messenger* messenger, const std::string &sendBuf, Synch* synch)
+{
+ grk_handle shm_fd = 0;
+ char* send_buffer = nullptr;
+
+ if(!SharedMemoryManager::initShm(sendBuf, messageBufferLen, &shm_fd, &send_buffer))
+ return;
+ while(messenger->running)
+ {
+ synch->wait(SYNCH_RECEIVE_READY);
+ if(!messenger->running)
+ break;
+ std::string message;
+ if(!messenger->sendQueue.waitAndPop(message))
+ break;
+ if(!messenger->running)
+ break;
+ memcpy(send_buffer, message.c_str(), message.size() + 1);
+ synch->post(SYNCH_SENT);
+ }
+ SharedMemoryManager::deinitShm(sendBuf, messageBufferLen, shm_fd, &send_buffer);
+}
+
+static void inboundThread(Messenger* messenger, const std::string &receiveBuf, Synch* synch)
+{
+ grk_handle shm_fd = 0;
+ char* receive_buffer = nullptr;
+
+ if(!SharedMemoryManager::initShm(receiveBuf, messageBufferLen, &shm_fd, &receive_buffer))
+ return;
+ while(messenger->running)
+ {
+ synch->wait(SYNCH_SENT);
+ if(!messenger->running)
+ break;
+ auto message = std::string(receive_buffer);
+ synch->post(SYNCH_RECEIVE_READY);
+ messenger->receiveQueue.push(message);
+ }
+ SharedMemoryManager::deinitShm(receiveBuf, messageBufferLen, shm_fd, &receive_buffer);
+}
+struct Msg
+{
+ explicit Msg(const std::string &msg) : ct_(0)
+ {
+ std::stringstream ss(msg);
+ while(ss.good())
+ {
+ std::string substr;
+ std::getline(ss, substr, ',');
+ cs_.push_back(substr);
+ }
+ }
+ std::string next()
+ {
+ if(ct_ == cs_.size())
+ {
+ getMessengerLogger()->error("Msg: comma separated list exhausted. returning empty.");
+ return "";
+ }
+ return cs_[ct_++];
+ }
+
+ uint32_t nextUint(void)
+ {
+ return (uint32_t)std::stoi(next());
+ }
+
+ std::vector<std::string> cs_;
+ size_t ct_;
+};
+
+static void processorThread(Messenger* messenger, std::function<void(std::string)> processor)
+{
+ while (messenger->running) {
+ std::string message;
+ if (!messenger->receiveQueue.waitAndPop(message)) {
+ break;
+ }
+
+ if (!messenger->running) {
+ break;
+ }
+
+ Msg msg(message);
+ auto tag = msg.next();
+ if (tag == GRK_MSGR_BATCH_COMPRESS_INIT) {
+ auto width = msg.nextUint();
+ msg.nextUint(); // stride
+ auto height = msg.nextUint();
+ auto samples_per_pixel = msg.nextUint();
+ msg.nextUint(); // depth
+ messenger->init_.uncompressedFrameSize_ = Messenger::uncompressedFrameSize(width, height, samples_per_pixel);
+ auto compressed_frame_size = msg.nextUint();
+ auto num_frames = msg.nextUint();
+ messenger->initClient(compressed_frame_size, compressed_frame_size, num_frames);
+ } else if (tag == GRK_MSGR_BATCH_PROCESSED_UNCOMPRESSED) {
+ messenger->reclaimUncompressed(msg.nextUint());
+ } else if (tag == GRK_MSGR_BATCH_PROCESSSED_COMPRESSED) {
+ messenger->reclaimCompressed(msg.nextUint());
+ }
+ processor(message);
+ }
+}
+
+template<typename F>
+struct ScheduledFrames
+{
+ void store(F const& val)
+ {
+ std::unique_lock<std::mutex> lk(mapMutex_);
+ auto it = map_.find(val.index());
+ if (it == map_.end())
+ map_.emplace(std::make_pair(val.index(), val));
+ }
+ boost::optional<F> retrieve(size_t index)
+ {
+ std::unique_lock<std::mutex> lk(mapMutex_);
+ auto it = map_.find(index);
+ if(it == map_.end())
+ return {};
+
+ F val = it->second;
+ map_.erase(index);
+
+ return val;
+ }
+
+ private:
+ std::mutex mapMutex_;
+ std::map<size_t, F> map_;
+};
+
+template<typename F>
+struct ScheduledMessenger : public Messenger
+{
+ explicit ScheduledMessenger(MessengerInit init) : Messenger(init),
+ framesScheduled_(0),
+ framesCompressed_(0)
+ {}
+ ~ScheduledMessenger(void) {
+ shutdown();
+ }
+ bool scheduleCompress(F const& proxy, std::function<void(BufferSrc const&)> converter){
+ size_t frameSize = init_.uncompressedFrameSize_;
+ assert(frameSize >= init_.uncompressedFrameSize_);
+ BufferSrc src;
+ if(!availableBuffers_.waitAndPop(src))
+ return false;
+ converter(src);
+ scheduledFrames_.store(proxy);
+ framesScheduled_++;
+ send(GRK_MSGR_BATCH_SUBMIT_UNCOMPRESSED, proxy.index(), src.frameId_);
+
+ return true;
+ }
+ void processCompressed(const std::string &message, std::function<void(F,uint8_t*,uint32_t)> processor, bool needsRecompression) {
+ Msg msg(message);
+ msg.next();
+ auto clientFrameId = msg.nextUint();
+ auto compressedFrameId = msg.nextUint();
+ auto compressedFrameLength = msg.nextUint();
+ if (!needsRecompression) {
+ auto src_frame = scheduledFrames_.retrieve(clientFrameId);
+ if (!src_frame) {
+ return;
+ }
+ processor(*src_frame, getCompressedFrame(compressedFrameId),compressedFrameLength);
+ }
+ ++framesCompressed_;
+ send(GRK_MSGR_BATCH_PROCESSSED_COMPRESSED, compressedFrameId);
+ if (_shutdown && framesCompressed_ == framesScheduled_)
+ shutdownCondition_.notify_all();
+ }
+ void shutdown(void){
+ try {
+ std::unique_lock<std::mutex> lk(shutdownMutex_);
+ if (!async_result_.valid())
+ return;
+ _shutdown = true;
+ if (framesScheduled_) {
+ uint32_t scheduled = framesScheduled_;
+ send(GRK_MSGR_BATCH_FLUSH, scheduled);
+ shutdownCondition_.wait(lk, [this] { return framesScheduled_ == framesCompressed_; });
+ }
+ availableBuffers_.deactivate();
+ send(GRK_MSGR_BATCH_SHUTDOWN);
+ int result = async_result_.get();
+ if(result != 0)
+ getMessengerLogger()->error("Accelerator failed with return code: %d\n",result);
+ } catch (std::exception &ex) {
+ getMessengerLogger()->error("%s",ex.what());
+ }
+
+ }
+
+ boost::optional<F> retrieve(size_t index) {
+ return scheduledFrames_.retrieve(index);
+ }
+
+ void store(F& val) {
+ scheduledFrames_.store(val);
+ }
+
+private:
+ ScheduledFrames<F> scheduledFrames_;
+ std::atomic<uint32_t> framesScheduled_;
+ std::atomic<uint32_t> framesCompressed_;
+};
+
+} // namespace grk_plugin
diff --git a/src/lib/grok_j2k_encoder_thread.cc b/src/lib/grok_j2k_encoder_thread.cc
new file mode 100644
index 000000000..c3cd6f8dd
--- /dev/null
+++ b/src/lib/grok_j2k_encoder_thread.cc
@@ -0,0 +1,72 @@
+/*
+ Copyright (C) 2023 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 "config.h"
+#include "cross.h"
+#include "dcpomatic_log.h"
+#include "dcp_video.h"
+#include "grok/context.h"
+#include "grok_j2k_encoder_thread.h"
+#include "j2k_encoder.h"
+#include "util.h"
+#include <dcp/scope_guard.h>
+
+#include "i18n.h"
+
+
+using std::make_shared;
+using std::shared_ptr;
+
+
+GrokJ2KEncoderThread::GrokJ2KEncoderThread(J2KEncoder& encoder, grk_plugin::GrokContext* context)
+ : J2KEncoderThread(encoder)
+ , _context(context)
+{
+
+}
+
+
+void
+GrokJ2KEncoderThread::run()
+try
+{
+ while (true)
+ {
+ LOG_TIMING("encoder-sleep thread=%1", thread_id());
+ auto frame = _encoder.pop();
+
+ dcp::ScopeGuard frame_guard([this, &frame]() {
+ LOG_ERROR("Failed to schedule encode of %1 using grok", frame.index());
+ _encoder.retry(frame);
+ });
+
+ LOG_TIMING("encoder-pop thread=%1 frame=%2 eyes=%3", thread_id(), frame.index(), static_cast<int>(frame.eyes()));
+
+ auto grok = Config::instance()->grok().get_value_or({});
+
+ if (_context->launch(frame, grok.selected) && _context->scheduleCompress(frame)) {
+ frame_guard.cancel();
+ }
+ }
+} catch (boost::thread_interrupted& e) {
+} catch (...) {
+ store_current();
+}
diff --git a/src/lib/grok_j2k_encoder_thread.h b/src/lib/grok_j2k_encoder_thread.h
new file mode 100644
index 000000000..5301e1670
--- /dev/null
+++ b/src/lib/grok_j2k_encoder_thread.h
@@ -0,0 +1,41 @@
+/*
+ Copyright (C) 2023 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 "exception_store.h"
+#include "j2k_encoder_thread.h"
+
+
+namespace grk_plugin {
+ class GrokContext;
+}
+
+
+class GrokJ2KEncoderThread : public J2KEncoderThread, public ExceptionStore
+{
+public:
+ GrokJ2KEncoderThread(J2KEncoder& encoder, grk_plugin::GrokContext* context);
+
+ void run() override;
+
+private:
+ grk_plugin::GrokContext* _context;
+};
+
diff --git a/src/lib/image_decoder.cc b/src/lib/image_decoder.cc
index ce5c8757f..527a98c7d 100644
--- a/src/lib/image_decoder.cc
+++ b/src/lib/image_decoder.cc
@@ -81,7 +81,7 @@ ImageDecoder::pass ()
}
}
- video->emit (film(), _image, _frame_video_position);
+ video->emit(film(), _image, dcpomatic::ContentTime::from_frames(_frame_video_position, _image_content->video_frame_rate().get_value_or(24)));
++_frame_video_position;
return false;
}
diff --git a/src/lib/j2k_encoder.cc b/src/lib/j2k_encoder.cc
index 7c9777c16..de229113b 100644
--- a/src/lib/j2k_encoder.cc
+++ b/src/lib/j2k_encoder.cc
@@ -32,6 +32,12 @@
#include "encode_server_description.h"
#include "encode_server_finder.h"
#include "film.h"
+#include "cpu_j2k_encoder_thread.h"
+#ifdef DCPOMATIC_GROK
+#include "grok/context.h"
+#include "grok_j2k_encoder_thread.h"
+#endif
+#include "remote_j2k_encoder_thread.h"
#include "j2k_encoder.h"
#include "log.h"
#include "player_video.h"
@@ -44,6 +50,7 @@
using std::cout;
+using std::dynamic_pointer_cast;
using std::exception;
using std::list;
using std::make_shared;
@@ -53,6 +60,33 @@ using boost::optional;
using dcp::Data;
using namespace dcpomatic;
+#ifdef DCPOMATIC_GROK
+
+namespace grk_plugin {
+
+IMessengerLogger* sLogger = nullptr;
+
+#if defined(__GNUC__) || defined(__clang__)
+#pragma GCC diagnostic push
+#pragma GCC diagnostic ignored "-Wunused-function"
+#endif
+void setMessengerLogger(grk_plugin::IMessengerLogger* logger)
+{
+ delete sLogger;
+ sLogger = logger;
+}
+#if defined(__GNUC__) || defined(__clang__)
+#pragma GCC diagnostic pop
+#endif
+grk_plugin::IMessengerLogger* getMessengerLogger(void)
+{
+ return sLogger;
+}
+
+}
+
+#endif
+
/** @param film Film that we are encoding.
* @param writer Writer that we are using.
@@ -62,6 +96,13 @@ J2KEncoder::J2KEncoder(shared_ptr<const Film> film, Writer& writer)
, _history (200)
, _writer (writer)
{
+#ifdef DCPOMATIC_GROK
+ auto grok = Config::instance()->grok().get_value_or({});
+ _dcpomatic_context = new grk_plugin::DcpomaticContext(film, writer, _history, grok.binary_location);
+ if (grok.enable) {
+ _context = new grk_plugin::GrokContext(_dcpomatic_context);
+ }
+#endif
}
@@ -69,8 +110,29 @@ J2KEncoder::~J2KEncoder ()
{
_server_found_connection.disconnect();
- boost::mutex::scoped_lock lm (_threads_mutex);
- terminate_threads ();
+ terminate_threads();
+
+#ifdef DCPOMATIC_GROK
+ delete _context;
+ delete _dcpomatic_context;
+#endif
+}
+
+
+void
+J2KEncoder::servers_list_changed()
+{
+ auto config = Config::instance();
+#ifdef DCPOMATIC_GROK
+ auto const grok_enable = config->grok().get_value_or({}).enable;
+#else
+ auto const grok_enable = false;
+#endif
+
+ auto const cpu = (grok_enable || config->only_servers_encode()) ? 0 : config->master_encoding_threads();
+ auto const gpu = grok_enable ? config->master_encoding_threads() : 0;
+
+ remake_threads(cpu, gpu, EncodeServerFinder::instance()->servers());
}
@@ -85,7 +147,40 @@ J2KEncoder::begin ()
void
-J2KEncoder::end ()
+J2KEncoder::pause()
+{
+#ifdef DCPOMATIC_GROK
+ if (!Config::instance()->grok().get_value_or({}).enable) {
+ return;
+ }
+ return;
+
+ terminate_threads ();
+
+ /* Something might have been thrown during terminate_threads */
+ rethrow ();
+
+ delete _context;
+ _context = nullptr;
+#endif
+}
+
+
+void J2KEncoder::resume()
+{
+#ifdef DCPOMATIC_GROK
+ if (!Config::instance()->grok().get_value_or({}).enable) {
+ return;
+ }
+
+ _context = new grk_plugin::GrokContext(_dcpomatic_context);
+ servers_list_changed();
+#endif
+}
+
+
+void
+J2KEncoder::end()
{
boost::mutex::scoped_lock lock (_queue_mutex);
@@ -94,18 +189,13 @@ J2KEncoder::end ()
/* Keep waking workers until the queue is empty */
while (!_queue.empty ()) {
rethrow ();
- _empty_condition.notify_all ();
_full_condition.wait (lock);
}
-
lock.unlock ();
LOG_GENERAL_NC (N_("Terminating encoder threads"));
- {
- boost::mutex::scoped_lock lm (_threads_mutex);
- terminate_threads ();
- }
+ terminate_threads ();
/* Something might have been thrown during terminate_threads */
rethrow ();
@@ -120,20 +210,35 @@ J2KEncoder::end ()
So just mop up anything left in the queue here.
*/
-
- for (auto const& i: _queue) {
- LOG_GENERAL(N_("Encode left-over frame %1"), i.index());
- try {
- _writer.write(
- make_shared<dcp::ArrayData>(i.encode_locally()),
- i.index(),
- i.eyes()
- );
- frame_done ();
- } catch (std::exception& e) {
- LOG_ERROR (N_("Local encode failed (%1)"), e.what ());
+ for (auto & i: _queue) {
+#ifdef DCPOMATIC_GROK
+ if (Config::instance()->grok().get_value_or({}).enable) {
+ if (!_context->scheduleCompress(i)){
+ LOG_GENERAL (N_("[%1] J2KEncoder thread pushes frame %2 back onto queue after failure"), thread_id(), i.index());
+ // handle error
+ }
+ } else {
+#else
+ {
+#endif
+ LOG_GENERAL(N_("Encode left-over frame %1"), i.index());
+ try {
+ _writer.write(
+ make_shared<dcp::ArrayData>(i.encode_locally()),
+ i.index(),
+ i.eyes()
+ );
+ frame_done ();
+ } catch (std::exception& e) {
+ LOG_ERROR (N_("Local encode failed (%1)"), e.what ());
+ }
}
}
+
+#ifdef DCPOMATIC_GROK
+ delete _context;
+ _context = nullptr;
+#endif
}
@@ -183,7 +288,7 @@ J2KEncoder::encode (shared_ptr<PlayerVideo> pv, DCPTime time)
size_t threads = 0;
{
boost::mutex::scoped_lock lm (_threads_mutex);
- threads = _threads->size();
+ threads = _threads.size();
}
boost::mutex::scoped_lock queue_lock (_queue_mutex);
@@ -223,13 +328,14 @@ J2KEncoder::encode (shared_ptr<PlayerVideo> pv, DCPTime time)
LOG_DEBUG_ENCODE("Frame @ %1 ENCODE", to_string(time));
/* Queue this new frame for encoding */
LOG_TIMING ("add-frame-to-queue queue=%1", _queue.size ());
- _queue.push_back (DCPVideo(
+ auto dcpv = DCPVideo(
pv,
position,
_film->video_frame_rate(),
_film->j2k_bandwidth(),
_film->resolution()
- ));
+ );
+ _queue.push_back (dcpv);
/* The queue might not be empty any more, so notify anything which is
waiting on that.
@@ -242,170 +348,141 @@ J2KEncoder::encode (shared_ptr<PlayerVideo> pv, DCPTime time)
}
-/** Caller must hold a lock on _threads_mutex */
void
J2KEncoder::terminate_threads ()
{
+ boost::mutex::scoped_lock lm(_threads_mutex);
boost::this_thread::disable_interruption dis;
- if (!_threads) {
- return;
- }
-
- _threads->interrupt_all ();
- try {
- _threads->join_all ();
- } catch (exception& e) {
- LOG_ERROR ("join() threw an exception: %1", e.what());
- } catch (...) {
- LOG_ERROR_NC ("join() threw an exception");
+ for (auto& thread: _threads) {
+ thread->stop();
}
- _threads.reset ();
+ _threads.clear();
+ _ending = true;
}
void
-J2KEncoder::encoder_thread (optional<EncodeServerDescription> server)
-try
+J2KEncoder::remake_threads(int cpu, int gpu, list<EncodeServerDescription> servers)
{
- start_of_thread ("J2KEncoder");
+ LOG_GENERAL("Making threads: CPU=%1, GPU=%2, Remote=%3", cpu, gpu, servers.size());
- if (server) {
- LOG_TIMING ("start-encoder-thread thread=%1 server=%2", thread_id (), server->host_name ());
- } else {
- LOG_TIMING ("start-encoder-thread thread=%1 server=localhost", thread_id ());
+ boost::mutex::scoped_lock lm (_threads_mutex);
+ if (_ending) {
+ return;
}
- /* Number of seconds that we currently wait between attempts
- to connect to the server; not relevant for localhost
- encodings.
- */
- int remote_backoff = 0;
+ auto remove_threads = [this](int wanted, int current, std::function<bool (shared_ptr<J2KEncoderThread>)> predicate) {
+ for (auto i = wanted; i < current; ++i) {
+ auto iter = std::find_if(_threads.begin(), _threads.end(), predicate);
+ if (iter != _threads.end()) {
+ (*iter)->stop();
+ _threads.erase(iter);
+ }
+ }
+ };
+
+
+ /* CPU */
+
+ auto const is_cpu_thread = [](shared_ptr<J2KEncoderThread> thread) {
+ return static_cast<bool>(dynamic_pointer_cast<CPUJ2KEncoderThread>(thread));
+ };
+
+ auto const current_cpu_threads = std::count_if(_threads.begin(), _threads.end(), is_cpu_thread);
+
+ for (auto i = current_cpu_threads; i < cpu; ++i) {
+ auto thread = make_shared<CPUJ2KEncoderThread>(*this);
+ thread->start();
+ _threads.push_back(thread);
+ }
+
+ remove_threads(cpu, current_cpu_threads, is_cpu_thread);
+
+#ifdef DCPOMATIC_GROK
+ /* GPU */
+
+ auto const is_grok_thread = [](shared_ptr<J2KEncoderThread> thread) {
+ return static_cast<bool>(dynamic_pointer_cast<GrokJ2KEncoderThread>(thread));
+ };
+
+ auto const current_gpu_threads = std::count_if(_threads.begin(), _threads.end(), is_grok_thread);
+
+ for (auto i = current_gpu_threads; i < gpu; ++i) {
+ auto thread = make_shared<GrokJ2KEncoderThread>(*this, _context);
+ thread->start();
+ _threads.push_back(thread);
+ }
+
+ remove_threads(gpu, current_gpu_threads, is_grok_thread);
+#endif
- while (true) {
+ /* Remote */
- LOG_TIMING ("encoder-sleep thread=%1", thread_id ());
- boost::mutex::scoped_lock lock (_queue_mutex);
- while (_queue.empty ()) {
- _empty_condition.wait (lock);
+ for (auto const& server: servers) {
+ if (!server.current_link_version()) {
+ continue;
}
- LOG_TIMING ("encoder-wake thread=%1 queue=%2", thread_id(), _queue.size());
- auto vf = _queue.front ();
+ auto is_remote_thread = [server](shared_ptr<J2KEncoderThread> thread) {
+ auto remote = dynamic_pointer_cast<RemoteJ2KEncoderThread>(thread);
+ return remote && remote->server().host_name() == server.host_name();
+ };
- /* We're about to commit to either encoding this frame or putting it back onto the queue,
- so we must not be interrupted until one or other of these things have happened. This
- block has thread interruption disabled.
- */
- {
- boost::this_thread::disable_interruption dis;
-
- LOG_TIMING ("encoder-pop thread=%1 frame=%2 eyes=%3", thread_id(), vf.index(), static_cast<int>(vf.eyes()));
- _queue.pop_front ();
-
- lock.unlock ();
-
- shared_ptr<Data> encoded;
-
- /* We need to encode this input */
- if (server) {
- try {
- encoded = make_shared<dcp::ArrayData>(vf.encode_remotely(server.get()));
-
- if (remote_backoff > 0) {
- LOG_GENERAL ("%1 was lost, but now she is found; removing backoff", server->host_name ());
- }
-
- /* This job succeeded, so remove any backoff */
- remote_backoff = 0;
-
- } catch (std::exception& e) {
- if (remote_backoff < 60) {
- /* back off more */
- remote_backoff += 10;
- }
- LOG_ERROR (
- N_("Remote encode of %1 on %2 failed (%3); thread sleeping for %4s"),
- vf.index(), server->host_name(), e.what(), remote_backoff
- );
- }
-
- } else {
- try {
- LOG_TIMING ("start-local-encode thread=%1 frame=%2", thread_id(), vf.index());
- encoded = make_shared<dcp::ArrayData>(vf.encode_locally());
- LOG_TIMING ("finish-local-encode thread=%1 frame=%2", thread_id(), vf.index());
- } catch (std::exception& e) {
- /* This is very bad, so don't cope with it, just pass it on */
- LOG_ERROR (N_("Local encode failed (%1)"), e.what ());
- throw;
- }
- }
+ auto const current_threads = std::count_if(_threads.begin(), _threads.end(), is_remote_thread);
- if (encoded) {
- _writer.write(encoded, vf.index(), vf.eyes());
- frame_done ();
- } else {
- lock.lock ();
- LOG_GENERAL (N_("[%1] J2KEncoder thread pushes frame %2 back onto queue after failure"), thread_id(), vf.index());
- _queue.push_front (vf);
- lock.unlock ();
- }
+ auto const wanted_threads = server.threads();
+
+ if (wanted_threads > current_threads) {
+ LOG_GENERAL(N_("Adding %1 worker threads for remote %2"), wanted_threads - current_threads, server.host_name());
+ } else if (wanted_threads < current_threads) {
+ LOG_GENERAL(N_("Removing %1 worker threads for remote %2"), current_threads - wanted_threads, server.host_name());
}
- if (remote_backoff > 0) {
- boost::this_thread::sleep (boost::posix_time::seconds (remote_backoff));
+ for (auto i = current_threads; i < wanted_threads; ++i) {
+ auto thread = make_shared<RemoteJ2KEncoderThread>(*this, server);
+ thread->start();
+ _threads.push_back(thread);
}
- /* The queue might not be full any more, so notify anything that is waiting on that */
- lock.lock ();
- _full_condition.notify_all ();
+ remove_threads(wanted_threads, current_threads, is_remote_thread);
}
-}
-catch (boost::thread_interrupted& e) {
- /* Ignore these and just stop the thread */
- _full_condition.notify_all ();
-}
-catch (...)
-{
- store_current ();
- /* Wake anything waiting on _full_condition so it can see the exception */
- _full_condition.notify_all ();
+
+ _writer.set_encoder_threads(_threads.size());
}
-void
-J2KEncoder::servers_list_changed ()
+DCPVideo
+J2KEncoder::pop()
{
- boost::mutex::scoped_lock lm (_threads_mutex);
+ boost::mutex::scoped_lock lock(_queue_mutex);
+ while (_queue.empty()) {
+ _empty_condition.wait (lock);
+ }
- terminate_threads ();
- _threads = make_shared<boost::thread_group>();
+ LOG_TIMING("encoder-wake thread=%1 queue=%2", thread_id(), _queue.size());
- /* XXX: could re-use threads */
+ auto vf = _queue.front();
+ _queue.pop_front();
- if (!Config::instance()->only_servers_encode ()) {
- for (int i = 0; i < Config::instance()->master_encoding_threads (); ++i) {
-#ifdef DCPOMATIC_LINUX
- auto t = _threads->create_thread(boost::bind(&J2KEncoder::encoder_thread, this, optional<EncodeServerDescription>()));
- pthread_setname_np (t->native_handle(), "encode-worker");
-#else
- _threads->create_thread(boost::bind(&J2KEncoder::encoder_thread, this, optional<EncodeServerDescription>()));
-#endif
- }
- }
+ _full_condition.notify_all();
+ return vf;
+}
- for (auto i: EncodeServerFinder::instance()->servers()) {
- if (!i.current_link_version()) {
- continue;
- }
- LOG_GENERAL (N_("Adding %1 worker threads for remote %2"), i.threads(), i.host_name ());
- for (int j = 0; j < i.threads(); ++j) {
- _threads->create_thread(boost::bind(&J2KEncoder::encoder_thread, this, i));
- }
- }
+void
+J2KEncoder::retry(DCPVideo video)
+{
+ boost::mutex::scoped_lock lock(_queue_mutex);
+ _queue.push_front(video);
+ _empty_condition.notify_all();
+}
- _writer.set_encoder_threads(_threads->size());
+
+void
+J2KEncoder::write(shared_ptr<const dcp::Data> data, int index, Eyes eyes)
+{
+ _writer.write(data, index, eyes);
+ frame_done();
}
diff --git a/src/lib/j2k_encoder.h b/src/lib/j2k_encoder.h
index 63228a6b8..6bfbaea49 100644
--- a/src/lib/j2k_encoder.h
+++ b/src/lib/j2k_encoder.h
@@ -32,6 +32,7 @@
#include "enum_indexed_vector.h"
#include "event_history.h"
#include "exception_store.h"
+#include "j2k_encoder_thread.h"
#include "writer.h"
#include <boost/optional.hpp>
#include <boost/signals2.hpp>
@@ -48,6 +49,15 @@ class Film;
class Job;
class PlayerVideo;
+namespace grk_plugin {
+ struct DcpomaticContext;
+ struct GrokContext;
+}
+
+struct local_threads_created_and_destroyed;
+struct remote_threads_created_and_destroyed;
+struct frames_not_lost_when_threads_disappear;
+
/** @class J2KEncoder
* @brief Class to manage encoding to J2K.
@@ -70,19 +80,27 @@ public:
/** Called to pass a bit of video to be encoded as the next DCP frame */
void encode (std::shared_ptr<PlayerVideo> pv, dcpomatic::DCPTime time);
+ void pause();
+ void resume();
+
/** Called when a processing run has finished */
- void end ();
+ void end();
boost::optional<float> current_encoding_rate () const;
int video_frames_enqueued () const;
- void servers_list_changed ();
+ DCPVideo pop();
+ void retry(DCPVideo frame);
+ void write(std::shared_ptr<const dcp::Data> data, int index, Eyes eyes);
private:
+ friend struct ::local_threads_created_and_destroyed;
+ friend struct ::remote_threads_created_and_destroyed;
+ friend struct ::frames_not_lost_when_threads_disappear;
void frame_done ();
-
- void encoder_thread (boost::optional<EncodeServerDescription>);
+ void servers_list_changed ();
+ void remake_threads(int cpu, int gpu, std::list<EncodeServerDescription> servers);
void terminate_threads ();
/** Film that we are encoding */
@@ -91,7 +109,7 @@ private:
EventHistory _history;
boost::mutex _threads_mutex;
- std::shared_ptr<boost::thread_group> _threads;
+ std::vector<std::shared_ptr<J2KEncoderThread>> _threads;
mutable boost::mutex _queue_mutex;
std::list<DCPVideo> _queue;
@@ -107,6 +125,13 @@ private:
boost::optional<dcpomatic::DCPTime> _last_player_video_time;
boost::signals2::scoped_connection _server_found_connection;
+
+#ifdef DCPOMATIC_GROK
+ grk_plugin::DcpomaticContext* _dcpomatic_context = nullptr;
+ grk_plugin::GrokContext *_context = nullptr;
+#endif
+
+ bool _ending = false;
};
diff --git a/src/lib/j2k_encoder_thread.cc b/src/lib/j2k_encoder_thread.cc
new file mode 100644
index 000000000..d0e8a439c
--- /dev/null
+++ b/src/lib/j2k_encoder_thread.cc
@@ -0,0 +1,58 @@
+/*
+ Copyright (C) 2023 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 "dcp_video.h"
+#include "dcpomatic_log.h"
+#include "j2k_encoder.h"
+#include "j2k_encoder_thread.h"
+
+
+J2KEncoderThread::J2KEncoderThread(J2KEncoder& encoder)
+ : _encoder(encoder)
+{
+
+}
+
+
+void
+J2KEncoderThread::start()
+{
+ _thread = boost::thread(boost::bind(&J2KEncoderThread::run, this));
+#ifdef DCPOMATIC_LINUX
+ pthread_setname_np(_thread.native_handle(), "encode-worker");
+#endif
+}
+
+
+void
+J2KEncoderThread::stop()
+{
+ _thread.interrupt();
+ try {
+ _thread.join();
+ } catch (std::exception& e) {
+ LOG_ERROR("join() threw an exception: %1", e.what());
+ } catch (...) {
+ LOG_ERROR_NC("join() threw an exception");
+ }
+}
+
+
diff --git a/src/lib/j2k_encoder_thread.h b/src/lib/j2k_encoder_thread.h
new file mode 100644
index 000000000..b03b6f356
--- /dev/null
+++ b/src/lib/j2k_encoder_thread.h
@@ -0,0 +1,53 @@
+/*
+ Copyright (C) 2023 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/>.
+
+*/
+
+
+#ifndef DCPOMATIC_J2K_ENCODER_THREAD
+#define DCPOMATIC_J2K_ENCODER_THREAD
+
+
+#include <boost/thread.hpp>
+
+
+class J2KEncoder;
+
+
+class J2KEncoderThread
+{
+public:
+ J2KEncoderThread(J2KEncoder& encoder);
+
+ J2KEncoderThread(J2KEncoderThread const&) = delete;
+ J2KEncoderThread& operator=(J2KEncoderThread const&) = delete;
+
+ void start();
+ void stop();
+
+ virtual void run() = 0;
+
+protected:
+ J2KEncoder& _encoder;
+
+private:
+ boost::thread _thread;
+};
+
+
+#endif
diff --git a/src/lib/j2k_sync_encoder_thread.cc b/src/lib/j2k_sync_encoder_thread.cc
new file mode 100644
index 000000000..ef6834f60
--- /dev/null
+++ b/src/lib/j2k_sync_encoder_thread.cc
@@ -0,0 +1,65 @@
+/*
+ Copyright (C) 2023 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 "dcp_video.h"
+#include "dcpomatic_log.h"
+#include "j2k_encoder.h"
+#include "j2k_sync_encoder_thread.h"
+#include <dcp/scope_guard.h>
+
+
+J2KSyncEncoderThread::J2KSyncEncoderThread(J2KEncoder& encoder)
+ : J2KEncoderThread(encoder)
+{
+
+}
+
+
+void
+J2KSyncEncoderThread::run()
+try
+{
+ log_thread_start();
+
+ while (true) {
+ LOG_TIMING("encoder-sleep thread=%1", thread_id());
+ auto frame = _encoder.pop();
+
+ dcp::ScopeGuard frame_guard([this, &frame]() {
+ boost::this_thread::disable_interruption dis;
+ _encoder.retry(frame);
+ });
+
+ LOG_TIMING("encoder-pop thread=%1 frame=%2 eyes=%3", thread_id(), frame.index(), static_cast<int>(frame.eyes()));
+
+ auto encoded = encode(frame);
+
+ if (encoded) {
+ boost::this_thread::disable_interruption dis;
+ frame_guard.cancel();
+ _encoder.write(encoded, frame.index(), frame.eyes());
+ }
+ }
+} catch (boost::thread_interrupted& e) {
+} catch (...) {
+ store_current();
+}
+
diff --git a/src/lib/j2k_sync_encoder_thread.h b/src/lib/j2k_sync_encoder_thread.h
new file mode 100644
index 000000000..45222279e
--- /dev/null
+++ b/src/lib/j2k_sync_encoder_thread.h
@@ -0,0 +1,32 @@
+#ifndef DCPOMATIC_J2K_SYNC_ENCODER_THREAD_H
+#define DCPOMATIC_J2K_SYNC_ENCODER_THREAD_H
+
+
+#include "exception_store.h"
+#include "j2k_encoder_thread.h"
+#include <dcp/array_data.h>
+#include <boost/thread.hpp>
+
+
+class DCPVideo;
+class J2KEncoder;
+
+
+class J2KSyncEncoderThread : public J2KEncoderThread, public ExceptionStore
+{
+public:
+ J2KSyncEncoderThread(J2KEncoder& encoder);
+
+ J2KSyncEncoderThread(J2KSyncEncoderThread const&) = delete;
+ J2KSyncEncoderThread& operator=(J2KSyncEncoderThread const&) = delete;
+
+ virtual ~J2KSyncEncoderThread() {}
+
+ void run() override;
+
+ virtual void log_thread_start() const = 0;
+ virtual std::shared_ptr<dcp::ArrayData> encode(DCPVideo const& frame) = 0;
+};
+
+
+#endif
diff --git a/src/lib/job.cc b/src/lib/job.cc
index 9c9530b7a..9e685ec11 100644
--- a/src/lib/job.cc
+++ b/src/lib/job.cc
@@ -662,7 +662,7 @@ void
Job::cancel ()
{
if (_thread.joinable()) {
- resume();
+ Job::resume();
_thread.interrupt ();
_thread.join ();
@@ -689,6 +689,7 @@ Job::pause_by_user ()
}
if (paused) {
+ pause();
_pause_changed.notify_all ();
}
@@ -701,6 +702,7 @@ Job::pause_by_priority ()
{
if (running ()) {
set_state (PAUSED_BY_PRIORITY);
+ pause();
_pause_changed.notify_all ();
}
}
diff --git a/src/lib/job.h b/src/lib/job.h
index d4d0f9510..9b5fdfa6e 100644
--- a/src/lib/job.h
+++ b/src/lib/job.h
@@ -62,9 +62,10 @@ public:
}
void start ();
+ virtual void pause() {}
bool pause_by_user ();
void pause_by_priority ();
- void resume ();
+ virtual void resume ();
void cancel ();
bool is_new () const;
diff --git a/src/lib/kdm_cli.cc b/src/lib/kdm_cli.cc
index ddc77e771..4e3f9ccb7 100644
--- a/src/lib/kdm_cli.cc
+++ b/src/lib/kdm_cli.cc
@@ -62,8 +62,8 @@ help (std::function<void (string)> out)
out (" -o, --output <path> output file or directory");
out (" -K, --filename-format <format> filename format for KDMs");
out (" -Z, --container-name-format <format> filename format for ZIP containers");
- out (" -f, --valid-from <time> valid from time (in local time zone of the cinema) (e.g. \"2013-09-28 01:41:51\") or \"now\"");
- out (" -t, --valid-to <time> valid to time (in local time zone of the cinema) (e.g. \"2014-09-28 01:41:51\")");
+ out (" -f, --valid-from <time> valid from time (e.g. \"2013-09-28T01:41:51+04:00\", \"2018-01-01T12:00:30\") or \"now\"");
+ out (" -t, --valid-to <time> valid to time (e.g. \"2014-09-28T01:41:51\")");
out (" -d, --valid-duration <duration> valid duration (e.g. \"1 day\", \"4 hours\", \"2 weeks\")");
out (" -F, --formulation <formulation> modified-transitional-1, multiple-modified-transitional-1, dci-any or dci-specific [default modified-transitional-1]");
out (" -p, --disable-forensic-marking-picture disable forensic marking of pictures essences");
@@ -99,17 +99,6 @@ public:
};
-static boost::posix_time::ptime
-time_from_string (string t)
-{
- if (t == "now") {
- return boost::posix_time::second_clock::local_time ();
- }
-
- return boost::posix_time::time_from_string (t);
-}
-
-
static boost::posix_time::time_duration
duration_from_string (string d)
{
@@ -211,8 +200,8 @@ from_film (
boost::filesystem::path output,
dcp::NameFormat container_name_format,
dcp::NameFormat filename_format,
- boost::posix_time::ptime valid_from,
- boost::posix_time::ptime valid_to,
+ dcp::LocalTime valid_from,
+ dcp::LocalTime valid_to,
dcp::Formulation formulation,
bool disable_forensic_marking_picture,
optional<int> disable_forensic_marking_audio,
@@ -362,8 +351,8 @@ from_dkdm (
boost::filesystem::path output,
dcp::NameFormat container_name_format,
dcp::NameFormat filename_format,
- boost::posix_time::ptime valid_from,
- boost::posix_time::ptime valid_to,
+ dcp::LocalTime valid_from,
+ dcp::LocalTime valid_to,
dcp::Formulation formulation,
bool disable_forensic_marking_picture,
optional<int> disable_forensic_marking_audio,
@@ -381,18 +370,12 @@ from_dkdm (
continue;
}
- int const offset_hour = i->cinema ? i->cinema->utc_offset_hour() : 0;
- int const offset_minute = i->cinema ? i->cinema->utc_offset_minute() : 0;
-
- dcp::LocalTime begin(valid_from, dcp::UTCOffset(offset_hour, offset_minute));
- dcp::LocalTime end(valid_to, dcp::UTCOffset(offset_hour, offset_minute));
-
auto const kdm = kdm_from_dkdm(
dkdm,
i->recipient.get(),
i->trusted_device_thumbprints(),
- begin,
- end,
+ valid_from,
+ valid_to,
formulation,
disable_forensic_marking_picture,
disable_forensic_marking_audio
@@ -402,8 +385,8 @@ from_dkdm (
name_values['c'] = i->cinema ? i->cinema->name : "";
name_values['s'] = i->name;
name_values['f'] = kdm.content_title_text();
- name_values['b'] = begin.date() + " " + begin.time_of_day(true, false);
- name_values['e'] = end.date() + " " + end.time_of_day(true, false);
+ name_values['b'] = valid_from.date() + " " + valid_from.time_of_day(true, false);
+ name_values['e'] = valid_to.date() + " " + valid_to.time_of_day(true, false);
name_values['i'] = kdm.cpl_id();
kdms.push_back(make_shared<KDMWithMetadata>(name_values, i->cinema.get(), i->cinema ? i->cinema->emails : vector<string>(), kdm));
@@ -440,6 +423,22 @@ dump_dkdm_group (shared_ptr<DKDMGroup> group, int indent, std::function<void (st
}
+static
+dcp::LocalTime
+time_from_string(string time)
+{
+ if (time == "now") {
+ return {};
+ }
+
+ if (time.length() > 10 && time[10] == ' ') {
+ time[10] = 'T';
+ }
+
+ return dcp::LocalTime(time);
+}
+
+
optional<string>
kdm_cli (int argc, char* argv[], std::function<void (string)> out)
try
@@ -453,8 +452,8 @@ try
optional<string> screen;
vector<shared_ptr<Screen>> screens;
optional<dcp::EncryptedKDM> dkdm;
- optional<boost::posix_time::ptime> valid_from;
- optional<boost::posix_time::ptime> valid_to;
+ optional<dcp::LocalTime> valid_from;
+ optional<dcp::LocalTime> valid_to;
bool zip = false;
bool list_cinemas = false;
bool list_dkdm_cpls = false;
@@ -517,10 +516,10 @@ try
container_name_format = dcp::NameFormat (optarg);
break;
case 'f':
- valid_from = time_from_string (optarg);
+ valid_from = time_from_string(optarg);
break;
case 't':
- valid_to = time_from_string (optarg);
+ valid_to = dcp::LocalTime(optarg);
break;
case 'd':
duration_string = optarg;
@@ -564,7 +563,7 @@ try
(for lookup) and by creating a Cinema which the next Screen will be added to.
*/
cinema_name = optarg;
- cinema = make_shared<Cinema>(optarg, vector<string>(), "", 0, 0);
+ cinema = make_shared<Cinema>(optarg, vector<string>(), "");
break;
case 'S':
/* Similarly, this could be the name of a new (temporary) screen or the name of a screen
@@ -644,11 +643,12 @@ try
}
if (duration_string) {
- valid_to = valid_from.get() + duration_from_string (*duration_string);
+ valid_to = valid_from.get();
+ valid_to->add(duration_from_string(*duration_string));
}
if (verbose) {
- out (String::compose("Making KDMs valid from %1 to %2", boost::posix_time::to_simple_string(valid_from.get()), boost::posix_time::to_simple_string(valid_to.get())));
+ out(String::compose("Making KDMs valid from %1 to %2", valid_from->as_string(), valid_to->as_string()));
}
string const thing = argv[optind];
diff --git a/src/lib/make_dcp.cc b/src/lib/make_dcp.cc
index 17d45be46..ddd231243 100644
--- a/src/lib/make_dcp.cc
+++ b/src/lib/make_dcp.cc
@@ -40,8 +40,8 @@ using std::shared_ptr;
using std::string;
-/** Add suitable Jobs to the JobManager to create a DCP for a Film */
-void
+/** Add suitable Job to the JobManager to create a DCP for a Film */
+shared_ptr<TranscodeJob>
make_dcp (shared_ptr<Film> film, TranscodeJob::ChangedBehaviour behaviour)
{
if (film->dcp_name().find("/") != string::npos) {
@@ -91,15 +91,12 @@ make_dcp (shared_ptr<Film> film, TranscodeJob::ChangedBehaviour behaviour)
LOG_GENERAL ("Content: %1", content->technical_summary());
}
LOG_GENERAL ("DCP video rate %1 fps", film->video_frame_rate());
- if (Config::instance()->only_servers_encode()) {
- LOG_GENERAL_NC ("0 threads: ONLY SERVERS SET TO ENCODE");
- } else {
- LOG_GENERAL ("%1 threads", Config::instance()->master_encoding_threads());
- }
LOG_GENERAL ("J2K bandwidth %1", film->j2k_bandwidth());
auto tj = make_shared<DCPTranscodeJob>(film, behaviour);
tj->set_encoder (make_shared<DCPEncoder>(film, tj));
JobManager::instance()->add (tj);
+
+ return tj;
}
diff --git a/src/lib/make_dcp.h b/src/lib/make_dcp.h
index 9f5072782..fe0bcd2f6 100644
--- a/src/lib/make_dcp.h
+++ b/src/lib/make_dcp.h
@@ -25,5 +25,5 @@
class Film;
-void make_dcp (std::shared_ptr<Film> film, TranscodeJob::ChangedBehaviour behaviour);
+std::shared_ptr<TranscodeJob> make_dcp(std::shared_ptr<Film> film, TranscodeJob::ChangedBehaviour behaviour);
diff --git a/src/lib/overlaps.cc b/src/lib/overlaps.cc
index fbc6b5cb1..bed75a822 100644
--- a/src/lib/overlaps.cc
+++ b/src/lib/overlaps.cc
@@ -28,7 +28,8 @@ using std::shared_ptr;
using namespace dcpomatic;
-ContentList overlaps (shared_ptr<const Film> film, ContentList cl, function<bool (shared_ptr<const Content>)> part, DCPTime from, DCPTime to)
+ContentList
+dcpomatic::overlaps(shared_ptr<const Film> film, ContentList cl, function<bool (shared_ptr<const Content>)> part, DCPTime from, DCPTime to)
{
ContentList overlaps;
DCPTimePeriod period (from, to);
diff --git a/src/lib/overlaps.h b/src/lib/overlaps.h
index bb8277eaa..f405c0fab 100644
--- a/src/lib/overlaps.h
+++ b/src/lib/overlaps.h
@@ -27,6 +27,9 @@ class ContentPart;
class Film;
+namespace dcpomatic {
+
+
/** @return Pieces of content with a given part (video, audio,
* subtitle) that overlap a specified time range in the given
* ContentList
@@ -34,3 +37,7 @@ class Film;
ContentList overlaps (
std::shared_ptr<const Film> film, ContentList cl, std::function<bool (std::shared_ptr<const Content>)> part, dcpomatic::DCPTime from, dcpomatic::DCPTime to
);
+
+
+}
+
diff --git a/src/lib/player.cc b/src/lib/player.cc
index c03cb97a5..dba02cfba 100644
--- a/src/lib/player.cc
+++ b/src/lib/player.cc
@@ -71,10 +71,8 @@ using std::dynamic_pointer_cast;
using std::list;
using std::make_pair;
using std::make_shared;
-using std::make_shared;
using std::max;
using std::min;
-using std::min;
using std::pair;
using std::shared_ptr;
using std::vector;
@@ -412,7 +410,6 @@ Player::setup_pieces ()
_silent = Empty(film, playlist(), bind(&have_audio, _1), _playback_length);
_next_video_time = boost::none;
- _next_video_eyes = Eyes::BOTH;
_next_audio_time = boost::none;
}
@@ -525,7 +522,7 @@ Player::black_player_video_frame (Eyes eyes) const
boost::mutex::scoped_lock lm(_black_image_mutex);
return std::make_shared<PlayerVideo> (
- std::make_shared<const RawImageProxy>(_black_image),
+ make_shared<const RawImageProxy>(_black_image),
Crop(),
optional<double>(),
_video_container_size,
@@ -535,7 +532,7 @@ Player::black_player_video_frame (Eyes eyes) const
PresetColourConversion::all().front().conversion,
VideoRange::FULL,
std::weak_ptr<Content>(),
- boost::optional<Frame>(),
+ boost::optional<dcpomatic::ContentTime>(),
false
);
}
@@ -703,7 +700,7 @@ Player::pass ()
if (_playback_length.load() == DCPTime() || !film) {
/* Special; just give one black frame */
- emit_video (black_player_video_frame(Eyes::BOTH), DCPTime());
+ use_video(black_player_video_frame(Eyes::BOTH), DCPTime(), one_video_frame());
return true;
}
@@ -761,22 +758,33 @@ Player::pass ()
LOG_DEBUG_PLAYER ("Calling pass() on %1", earliest_content->content->path(0));
earliest_content->done = earliest_content->decoder->pass ();
auto dcp = dynamic_pointer_cast<DCPContent>(earliest_content->content);
- if (dcp && !_play_referenced && dcp->reference_audio()) {
- /* We are skipping some referenced DCP audio content, so we need to update _next_audio_time
- to `hide' the fact that no audio was emitted during the referenced DCP (though
- we need to behave as though it was).
- */
- _next_audio_time = dcp->end(film);
+ if (dcp && !_play_referenced) {
+ if (dcp->reference_video()) {
+ _next_video_time = dcp->end(film);
+ }
+ if (dcp->reference_audio()) {
+ /* We are skipping some referenced DCP audio content, so we need to update _next_audio_time
+ to `hide' the fact that no audio was emitted during the referenced DCP (though
+ we need to behave as though it was).
+ */
+ _next_audio_time = dcp->end(film);
+ }
}
break;
}
case BLACK:
LOG_DEBUG_PLAYER ("Emit black for gap at %1", to_string(_black.position()));
+ if (!_next_video_time) {
+ /* Deciding to emit black has the same effect as getting some video from the content
+ * when we are inaccurately seeking.
+ */
+ _next_video_time = _black.position();
+ }
if (film->three_d()) {
- emit_video(black_player_video_frame(Eyes::LEFT), _black.position());
- emit_video(black_player_video_frame(Eyes::RIGHT), _black.position());
+ use_video(black_player_video_frame(Eyes::LEFT), _black.position(), _black.period_at_position().to);
+ use_video(black_player_video_frame(Eyes::RIGHT), _black.position(), _black.period_at_position().to);
} else {
- emit_video(black_player_video_frame(Eyes::BOTH), _black.position());
+ use_video(black_player_video_frame(Eyes::BOTH), _black.position(), _black.period_at_position().to);
}
_black.set_position (_black.position() + one_video_frame());
break;
@@ -879,24 +887,14 @@ Player::pass ()
}
if (done) {
+ LOG_DEBUG_PLAYER("Done: emit video until end of film at %1", to_string(film->length()));
+ emit_video_until(film->length());
+
if (_shuffler) {
_shuffler->flush ();
}
for (auto const& i: _delay) {
- do_emit_video(i.first, i.second);
- }
-
- /* Perhaps we should have Empty entries for both eyes in the 3D case (somehow).
- * However, if we have L and R video files, and one is shorter than the other,
- * the fill code in ::video mostly takes care of filling in the gaps.
- * However, since it fills at the point when it knows there is more video coming
- * at time t (so it should fill any gap up to t) it can't do anything right at the
- * end. This is particularly bad news if the last frame emitted is a LEFT
- * eye, as the MXF writer will complain about the 3D sequence being wrong.
- * Here's a hack to workaround that particular case.
- */
- if (_next_video_eyes && _next_video_time && *_next_video_eyes == Eyes::RIGHT) {
- do_emit_video (black_player_video_frame(Eyes::RIGHT), *_next_video_time);
+ emit_video(i.first, i.second);
}
}
@@ -959,15 +957,61 @@ Player::open_subtitles_for_frame (DCPTime time) const
}
-static
-Eyes
-increment_eyes (Eyes e)
+void
+Player::emit_video_until(DCPTime time)
{
- if (e == Eyes::LEFT) {
- return Eyes::RIGHT;
- }
+ LOG_DEBUG_PLAYER("emit_video_until %1; next video time is %2", to_string(time), to_string(_next_video_time.get_value_or({})));
+ auto frame = [this](shared_ptr<PlayerVideo> pv, DCPTime time) {
+ /* We need a delay to give a little wiggle room to ensure that relevant subtitles arrive at the
+ player before the video that requires them.
+ */
+ _delay.push_back(make_pair(pv, time));
+
+ if (pv->eyes() == Eyes::BOTH || pv->eyes() == Eyes::RIGHT) {
+ _next_video_time = time + one_video_frame();
+ }
+
+ if (_delay.size() < 3) {
+ return;
+ }
+
+ auto to_do = _delay.front();
+ _delay.pop_front();
+ emit_video(to_do.first, to_do.second);
+ };
- return Eyes::LEFT;
+ auto const age_threshold = one_video_frame() * 2;
+
+ while (_next_video_time.get_value_or({}) < time) {
+ auto left = _last_video[Eyes::LEFT];
+ auto right = _last_video[Eyes::RIGHT];
+ auto both = _last_video[Eyes::BOTH];
+
+ auto const next = _next_video_time.get_value_or({});
+
+ if (
+ left.first &&
+ right.first &&
+ (!both.first || (left.second >= both.second && right.second >= both.second)) &&
+ (left.second - next) < age_threshold &&
+ (right.second - next) < age_threshold
+ ) {
+ frame(left.first, next);
+ frame(right.first, next);
+ } else if (both.first && (both.second - next) < age_threshold) {
+ frame(both.first, next);
+ LOG_DEBUG_PLAYER("Content %1 selected for DCP %2 (age %3)", to_string(both.second), to_string(next), to_string(both.second - next));
+ } else {
+ auto film = _film.lock();
+ if (film && film->three_d()) {
+ frame(black_player_video_frame(Eyes::LEFT), next);
+ frame(black_player_video_frame(Eyes::RIGHT), next);
+ } else {
+ frame(black_player_video_frame(Eyes::BOTH), next);
+ }
+ LOG_DEBUG_PLAYER("Black selected for DCP %1", to_string(next));
+ }
+ }
}
@@ -992,11 +1036,6 @@ Player::video (weak_ptr<Piece> weak_piece, ContentVideo video)
return;
}
- FrameRateChange frc(film, piece->content);
- if (frc.skip && (video.frame % 2) == 1) {
- return;
- }
-
vector<Eyes> eyes_to_emit;
if (!film->three_d()) {
@@ -1019,15 +1058,11 @@ Player::video (weak_ptr<Piece> weak_piece, ContentVideo video)
eyes_to_emit = { video.eyes };
}
- /* Time of the first frame we will emit */
- DCPTime const time = content_video_to_dcp (piece, video.frame);
- LOG_DEBUG_PLAYER("Received video frame %1 at %2", video.frame, to_string(time));
+ /* Time of the frame we just received within the DCP */
+ auto const time = content_time_to_dcp(piece, video.time);
+ LOG_DEBUG_PLAYER("Received video frame %1 %2 eyes %3", to_string(video.time), to_string(time), static_cast<int>(video.eyes));
- /* Discard if it's before the content's period or the last accurate seek. We can't discard
- if it's after the content's period here as in that case we still need to fill any gap between
- `now' and the end of the content's period.
- */
- if (time < piece->content->position() || (_next_video_time && time < *_next_video_time)) {
+ if (time < piece->content->position()) {
return;
}
@@ -1040,56 +1075,8 @@ Player::video (weak_ptr<Piece> weak_piece, ContentVideo video)
return;
}
- /* Fill gaps that we discover now that we have some video which needs to be emitted.
- This is where we need to fill to.
- */
- DCPTime fill_to = min(time, piece->content->end(film));
-
- if (_next_video_time) {
- DCPTime fill_from = max (*_next_video_time, piece->content->position());
-
- /* Fill if we have more than half a frame to do */
- if ((fill_to - fill_from) > one_video_frame() / 2) {
- auto last = _last_video.find (weak_piece);
- if (film->three_d()) {
- auto fill_to_eyes = eyes_to_emit[0];
- if (fill_to_eyes == Eyes::BOTH) {
- fill_to_eyes = Eyes::LEFT;
- }
- if (fill_to == piece->content->end(film)) {
- /* Don't fill after the end of the content */
- fill_to_eyes = Eyes::LEFT;
- }
- auto j = fill_from;
- auto eyes = _next_video_eyes.get_value_or(Eyes::LEFT);
- if (eyes == Eyes::BOTH) {
- eyes = Eyes::LEFT;
- }
- while (j < fill_to || eyes != fill_to_eyes) {
- if (last != _last_video.end()) {
- LOG_DEBUG_PLAYER("Fill using last video at %1 in 3D mode", to_string(j));
- auto copy = last->second->shallow_copy();
- copy->set_eyes (eyes);
- emit_video (copy, j);
- } else {
- LOG_DEBUG_PLAYER("Fill using black at %1 in 3D mode", to_string(j));
- emit_video (black_player_video_frame(eyes), j);
- }
- if (eyes == Eyes::RIGHT) {
- j += one_video_frame();
- }
- eyes = increment_eyes (eyes);
- }
- } else {
- for (DCPTime j = fill_from; j < fill_to; j += one_video_frame()) {
- if (last != _last_video.end()) {
- emit_video (last->second, j);
- } else {
- emit_video (black_player_video_frame(Eyes::BOTH), j);
- }
- }
- }
- }
+ if (!_next_video_time) {
+ _next_video_time = time.round(film->video_frame_rate());
}
auto const content_video = piece->content->video;
@@ -1098,33 +1085,38 @@ Player::video (weak_ptr<Piece> weak_piece, ContentVideo video)
DCPOMATIC_ASSERT(scaled_size);
for (auto eyes: eyes_to_emit) {
- _last_video[weak_piece] = std::make_shared<PlayerVideo>(
- video.image,
- content_video->actual_crop(),
- content_video->fade(film, video.frame),
- scale_for_display(
- *scaled_size,
+ use_video(
+ std::make_shared<PlayerVideo>(
+ video.image,
+ content_video->actual_crop(),
+ content_video->fade(film, video.time),
+ scale_for_display(
+ *scaled_size,
+ _video_container_size,
+ film->frame_size(),
+ content_video->pixel_quanta()
+ ),
_video_container_size,
- film->frame_size(),
- content_video->pixel_quanta()
+ eyes,
+ video.part,
+ content_video->colour_conversion(),
+ content_video->range(),
+ piece->content,
+ video.time,
+ false
),
- _video_container_size,
- eyes,
- video.part,
- content_video->colour_conversion(),
- content_video->range(),
- piece->content,
- video.frame,
- false
+ time,
+ piece->content->end(film)
);
+ }
+}
- DCPTime t = time;
- for (int i = 0; i < frc.repeat; ++i) {
- if (t < piece->content->end(film)) {
- emit_video (_last_video[weak_piece], t);
- }
- t += one_video_frame ();
- }
+void
+Player::use_video(shared_ptr<PlayerVideo> pv, DCPTime time, DCPTime end)
+{
+ _last_video[pv->eyes()] = { pv, time };
+ if (pv->eyes() != Eyes::LEFT) {
+ emit_video_until(std::min(time + one_video_frame() / 2, end));
}
}
@@ -1406,18 +1398,18 @@ Player::seek (DCPTime time, bool accurate)
if (accurate) {
_next_video_time = time;
- _next_video_eyes = Eyes::LEFT;
_next_audio_time = time;
} else {
_next_video_time = boost::none;
- _next_video_eyes = boost::none;
_next_audio_time = boost::none;
}
_black.set_position (time);
_silent.set_position (time);
- _last_video.clear ();
+ _last_video[Eyes::LEFT] = {};
+ _last_video[Eyes::RIGHT] = {};
+ _last_video[Eyes::BOTH] = {};
for (auto& state: _stream_states) {
state.second.last_push_end = boost::none;
@@ -1426,33 +1418,7 @@ Player::seek (DCPTime time, bool accurate)
void
-Player::emit_video (shared_ptr<PlayerVideo> pv, DCPTime time)
-{
- auto film = _film.lock();
- DCPOMATIC_ASSERT(film);
-
- /* We need a delay to give a little wiggle room to ensure that relevant subtitles arrive at the
- player before the video that requires them.
- */
- _delay.push_back (make_pair (pv, time));
-
- if (pv->eyes() == Eyes::BOTH || pv->eyes() == Eyes::RIGHT) {
- _next_video_time = time + one_video_frame();
- }
- _next_video_eyes = increment_eyes (pv->eyes());
-
- if (_delay.size() < 3) {
- return;
- }
-
- auto to_do = _delay.front();
- _delay.pop_front();
- do_emit_video (to_do.first, to_do.second);
-}
-
-
-void
-Player::do_emit_video (shared_ptr<PlayerVideo> pv, DCPTime time)
+Player::emit_video(shared_ptr<PlayerVideo> pv, DCPTime time)
{
if (pv->eyes() == Eyes::BOTH || pv->eyes() == Eyes::RIGHT) {
std::for_each(_active_texts.begin(), _active_texts.end(), [time](ActiveText& a) { a.clear_before(time); });
diff --git a/src/lib/player.h b/src/lib/player.h
index 94e41bbca..2502ae536 100644
--- a/src/lib/player.h
+++ b/src/lib/player.h
@@ -155,6 +155,8 @@ private:
dcpomatic::ContentTime dcp_to_content_time (std::shared_ptr<const Piece> piece, dcpomatic::DCPTime t) const;
dcpomatic::DCPTime content_time_to_dcp (std::shared_ptr<const Piece> piece, dcpomatic::ContentTime t) const;
std::shared_ptr<PlayerVideo> black_player_video_frame (Eyes eyes) const;
+ void emit_video_until(dcpomatic::DCPTime time);
+ void insert_video(std::shared_ptr<PlayerVideo> pv, dcpomatic::DCPTime time, dcpomatic::DCPTime end);
void video (std::weak_ptr<Piece>, ContentVideo);
void audio (std::weak_ptr<Piece>, AudioStreamPtr, ContentAudio);
@@ -169,8 +171,8 @@ private:
std::shared_ptr<const AudioBuffers> audio, dcpomatic::DCPTime time, dcpomatic::DCPTime discard_to
) const;
boost::optional<PositionImage> open_subtitles_for_frame (dcpomatic::DCPTime time) const;
- void emit_video (std::shared_ptr<PlayerVideo> pv, dcpomatic::DCPTime time);
- void do_emit_video (std::shared_ptr<PlayerVideo> pv, dcpomatic::DCPTime time);
+ void emit_video(std::shared_ptr<PlayerVideo> pv, dcpomatic::DCPTime time);
+ void use_video(std::shared_ptr<PlayerVideo> pv, dcpomatic::DCPTime time, dcpomatic::DCPTime end);
void emit_audio (std::shared_ptr<AudioBuffers> data, dcpomatic::DCPTime time);
std::shared_ptr<const Playlist> playlist () const;
@@ -211,15 +213,12 @@ private:
/** Time of the next video that we will emit, or the time of the last accurate seek */
boost::optional<dcpomatic::DCPTime> _next_video_time;
- /** Eyes of the next video that we will emit */
- boost::optional<Eyes> _next_video_eyes;
/** Time of the next audio that we will emit, or the time of the last accurate seek */
boost::optional<dcpomatic::DCPTime> _next_audio_time;
boost::atomic<boost::optional<int>> _dcp_decode_reduction;
- typedef std::map<std::weak_ptr<Piece>, std::shared_ptr<PlayerVideo>, std::owner_less<std::weak_ptr<Piece>>> LastVideoMap;
- LastVideoMap _last_video;
+ EnumIndexedVector<std::pair<std::shared_ptr<PlayerVideo>, dcpomatic::DCPTime>, Eyes> _last_video;
AudioMerger _audio_merger;
std::unique_ptr<Shuffler> _shuffler;
diff --git a/src/lib/player_video.cc b/src/lib/player_video.cc
index 35c5d3daa..b39f83908 100644
--- a/src/lib/player_video.cc
+++ b/src/lib/player_video.cc
@@ -45,6 +45,7 @@ using std::weak_ptr;
using boost::optional;
using dcp::Data;
using dcp::raw_convert;
+using namespace dcpomatic;
PlayerVideo::PlayerVideo (
@@ -58,7 +59,7 @@ PlayerVideo::PlayerVideo (
optional<ColourConversion> colour_conversion,
VideoRange video_range,
weak_ptr<Content> content,
- optional<Frame> video_frame,
+ optional<ContentTime> video_time,
bool error
)
: _in (in)
@@ -71,7 +72,7 @@ PlayerVideo::PlayerVideo (
, _colour_conversion (colour_conversion)
, _video_range (video_range)
, _content (content)
- , _video_frame (video_frame)
+ , _video_time(video_time)
, _error (error)
{
@@ -343,7 +344,7 @@ PlayerVideo::shallow_copy () const
_colour_conversion,
_video_range,
_content,
- _video_frame,
+ _video_time,
_error
);
}
@@ -356,12 +357,12 @@ bool
PlayerVideo::reset_metadata (shared_ptr<const Film> film, dcp::Size player_video_container_size)
{
auto content = _content.lock();
- if (!content || !_video_frame) {
+ if (!content || !_video_time) {
return false;
}
_crop = content->video->actual_crop();
- _fade = content->video->fade(film, _video_frame.get());
+ _fade = content->video->fade(film, _video_time.get());
auto const size = content->video->scaled_size(film->frame_size());
if (!size) {
return false;
diff --git a/src/lib/player_video.h b/src/lib/player_video.h
index f2781c1a0..10b2078a0 100644
--- a/src/lib/player_video.h
+++ b/src/lib/player_video.h
@@ -59,7 +59,7 @@ public:
boost::optional<ColourConversion> colour_conversion,
VideoRange video_range,
std::weak_ptr<Content> content,
- boost::optional<Frame> video_frame,
+ boost::optional<dcpomatic::ContentTime> video_time,
bool error
);
@@ -141,8 +141,8 @@ private:
boost::optional<PositionImage> _text;
/** Content that we came from. This is so that reset_metadata() can work. */
std::weak_ptr<Content> _content;
- /** Video frame that we came from. Again, this is for reset_metadata() */
- boost::optional<Frame> _video_frame;
+ /** Video time that we came from. Again, this is for reset_metadata() */
+ boost::optional<dcpomatic::ContentTime> _video_time;
mutable boost::mutex _mutex;
mutable std::shared_ptr<Image> _image;
diff --git a/src/lib/remote_j2k_encoder_thread.cc b/src/lib/remote_j2k_encoder_thread.cc
new file mode 100644
index 000000000..49d80953d
--- /dev/null
+++ b/src/lib/remote_j2k_encoder_thread.cc
@@ -0,0 +1,84 @@
+/*
+ Copyright (C) 2023 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 "dcp_video.h"
+#include "dcpomatic_log.h"
+#include "j2k_encoder.h"
+#include "remote_j2k_encoder_thread.h"
+#include "util.h"
+
+#include "i18n.h"
+
+
+using std::make_shared;
+using std::shared_ptr;
+
+
+RemoteJ2KEncoderThread::RemoteJ2KEncoderThread(J2KEncoder& encoder, EncodeServerDescription server)
+ : J2KSyncEncoderThread(encoder)
+ , _server(server)
+{
+
+}
+
+
+void
+RemoteJ2KEncoderThread::log_thread_start() const
+{
+ start_of_thread("RemoteJ2KEncoder");
+ LOG_TIMING("start-encoder-thread thread=%1 server=%2", thread_id(), _server.host_name());
+}
+
+
+shared_ptr<dcp::ArrayData>
+RemoteJ2KEncoderThread::encode(DCPVideo const& frame)
+{
+ shared_ptr<dcp::ArrayData> encoded;
+
+ try {
+ encoded = make_shared<dcp::ArrayData>(frame.encode_remotely(_server));
+ if (_remote_backoff > 0) {
+ LOG_GENERAL("%1 was lost, but now she is found; removing backoff", _server.host_name());
+ _remote_backoff = 0;
+ }
+ } catch (std::exception& e) {
+ LOG_ERROR(
+ N_("Remote encode of %1 on %2 failed (%3); thread sleeping for %4s"),
+ frame.index(), _server.host_name(), e.what(), _remote_backoff
+ );
+ } catch (...) {
+ LOG_ERROR(
+ N_("Remote encode of %1 on %2 failed; thread sleeping for %4s"),
+ frame.index(), _server.host_name(), _remote_backoff
+ );
+ }
+
+ if (!encoded) {
+ if (_remote_backoff < 60) {
+ /* back off more */
+ _remote_backoff += 10;
+ }
+ boost::this_thread::sleep(boost::posix_time::seconds(_remote_backoff));
+ }
+
+ return encoded;
+}
+
diff --git a/src/lib/remote_j2k_encoder_thread.h b/src/lib/remote_j2k_encoder_thread.h
new file mode 100644
index 000000000..f3fe7f94a
--- /dev/null
+++ b/src/lib/remote_j2k_encoder_thread.h
@@ -0,0 +1,21 @@
+#include "encode_server_description.h"
+#include "j2k_sync_encoder_thread.h"
+
+
+class RemoteJ2KEncoderThread : public J2KSyncEncoderThread
+{
+public:
+ RemoteJ2KEncoderThread(J2KEncoder& encoder, EncodeServerDescription server);
+
+ void log_thread_start() const override;
+ std::shared_ptr<dcp::ArrayData> encode(DCPVideo const& frame) override;
+
+ EncodeServerDescription server() const {
+ return _server;
+ }
+
+private:
+ EncodeServerDescription _server;
+ /** Number of seconds that we currently wait between attempts to connect to the server */
+ int _remote_backoff = 0;
+};
diff --git a/src/lib/screen.cc b/src/lib/screen.cc
index febf9085c..f0c80cd79 100644
--- a/src/lib/screen.cc
+++ b/src/lib/screen.cc
@@ -77,8 +77,8 @@ KDMWithMetadataPtr
kdm_for_screen (
std::function<dcp::DecryptedKDM (dcp::LocalTime, dcp::LocalTime)> make_kdm,
shared_ptr<const dcpomatic::Screen> screen,
- boost::posix_time::ptime valid_from,
- boost::posix_time::ptime valid_to,
+ dcp::LocalTime valid_from,
+ dcp::LocalTime valid_to,
dcp::Formulation formulation,
bool disable_forensic_marking_picture,
optional<int> disable_forensic_marking_audio,
@@ -90,17 +90,15 @@ kdm_for_screen (
}
auto cinema = screen->cinema;
- dcp::LocalTime const begin(valid_from, dcp::UTCOffset(cinema ? cinema->utc_offset_hour() : 0, cinema ? cinema->utc_offset_minute() : 0));
- dcp::LocalTime const end (valid_to, dcp::UTCOffset(cinema ? cinema->utc_offset_hour() : 0, cinema ? cinema->utc_offset_minute() : 0));
- period_checks.push_back(check_kdm_and_certificate_validity_periods(cinema ? cinema->name : "", screen->name, screen->recipient.get(), begin, end));
+ period_checks.push_back(check_kdm_and_certificate_validity_periods(cinema ? cinema->name : "", screen->name, screen->recipient.get(), valid_from, valid_to));
auto signer = Config::instance()->signer_chain();
if (!signer->valid()) {
throw InvalidSignerError();
}
- auto kdm = make_kdm(begin, end).encrypt(
+ auto kdm = make_kdm(valid_from, valid_to).encrypt(
signer, screen->recipient.get(), screen->trusted_device_thumbprints(), formulation, disable_forensic_marking_picture, disable_forensic_marking_audio
);
@@ -112,8 +110,8 @@ kdm_for_screen (
}
name_values['s'] = screen->name;
name_values['f'] = kdm.content_title_text();
- name_values['b'] = begin.date() + " " + begin.time_of_day(true, false);
- name_values['e'] = end.date() + " " + end.time_of_day(true, false);
+ name_values['b'] = valid_from.date() + " " + valid_from.time_of_day(true, false);
+ name_values['e'] = valid_to.date() + " " + valid_to.time_of_day(true, false);
name_values['i'] = kdm.cpl_id();
return make_shared<KDMWithMetadata>(name_values, cinema.get(), cinema ? cinema->emails : vector<string>(), kdm);
diff --git a/src/lib/screen.h b/src/lib/screen.h
index 7f01cdf27..0a275aa34 100644
--- a/src/lib/screen.h
+++ b/src/lib/screen.h
@@ -23,12 +23,13 @@
#define DCPOMATIC_SCREEN_H
-#include "kdm_with_metadata.h"
#include "kdm_recipient.h"
#include "kdm_util.h"
+#include "kdm_with_metadata.h"
#include "trusted_device.h"
#include <dcp/certificate.h>
#include <dcp/decrypted_kdm.h>
+#include <dcp/utc_offset.h>
#include <libcxml/cxml.h>
#include <boost/optional.hpp>
#include <string>
@@ -78,8 +79,8 @@ KDMWithMetadataPtr
kdm_for_screen (
std::function<dcp::DecryptedKDM (dcp::LocalTime, dcp::LocalTime)> make_kdm,
std::shared_ptr<const dcpomatic::Screen> screen,
- boost::posix_time::ptime valid_from,
- boost::posix_time::ptime valid_to,
+ dcp::LocalTime valid_from,
+ dcp::LocalTime valid_to,
dcp::Formulation formulation,
bool disable_forensic_marking_picture,
boost::optional<int> disable_forensic_marking_audio,
diff --git a/src/lib/shuffler.cc b/src/lib/shuffler.cc
index 5a4faf4d1..a4ea0f5dc 100644
--- a/src/lib/shuffler.cc
+++ b/src/lib/shuffler.cc
@@ -40,8 +40,8 @@ int const Shuffler::_max_size = 64;
struct Comparator
{
bool operator()(Shuffler::Store const & a, Shuffler::Store const & b) {
- if (a.second.frame != b.second.frame) {
- return a.second.frame < b.second.frame;
+ if (a.second.time != b.second.time) {
+ return a.second.time < b.second.time;
}
return a.second.eyes < b.second.eyes;
}
@@ -51,7 +51,7 @@ struct Comparator
void
Shuffler::video (weak_ptr<Piece> weak_piece, ContentVideo video)
{
- LOG_DEBUG_THREE_D ("Shuffler::video frame=%1 eyes=%2 part=%3", video.frame, static_cast<int>(video.eyes), static_cast<int>(video.part));
+ LOG_DEBUG_THREE_D("Shuffler::video time=%1 eyes=%2 part=%3", to_string(video.time), static_cast<int>(video.eyes), static_cast<int>(video.part));
if (video.eyes != Eyes::LEFT && video.eyes != Eyes::RIGHT) {
/* Pass through anything that we don't care about */
@@ -79,13 +79,13 @@ Shuffler::video (weak_ptr<Piece> weak_piece, ContentVideo video)
!_store.empty() &&
_last &&
(
- (_store.front().second.frame == _last->frame && _store.front().second.eyes == Eyes::RIGHT && _last->eyes == Eyes::LEFT) ||
- (_store.front().second.frame >= (_last->frame + 1) && _store.front().second.eyes == Eyes::LEFT && _last->eyes == Eyes::RIGHT)
+ (_store.front().second.time == _last->time && _store.front().second.eyes == Eyes::RIGHT && _last->eyes == Eyes::LEFT) ||
+ (_store.front().second.time > _last->time && _store.front().second.eyes == Eyes::LEFT && _last->eyes == Eyes::RIGHT)
);
if (!store_front_in_sequence) {
- string const store = _store.empty() ? "store empty" : String::compose("store front frame=%1 eyes=%2", _store.front().second.frame, static_cast<int>(_store.front().second.eyes));
- string const last = _last ? String::compose("last frame=%1 eyes=%2", _last->frame, static_cast<int>(_last->eyes)) : "no last";
+ string const store = _store.empty() ? "store empty" : String::compose("store front time=%1 eyes=%2", to_string(_store.front().second.time), static_cast<int>(_store.front().second.eyes));
+ string const last = _last ? String::compose("last time=%1 eyes=%2", to_string(_last->time), static_cast<int>(_last->eyes)) : "no last";
LOG_DEBUG_THREE_D("Shuffler not in sequence: %1 %2", store, last);
}
@@ -98,10 +98,10 @@ Shuffler::video (weak_ptr<Piece> weak_piece, ContentVideo video)
}
if (_store.size() > _max_size) {
- LOG_WARNING ("Shuffler is full after receiving frame %1; 3D sync may be incorrect.", video.frame);
+ LOG_WARNING("Shuffler is full after receiving frame at %1; 3D sync may be incorrect.", to_string(video.time));
}
- LOG_DEBUG_THREE_D("Shuffler emits frame=%1 eyes=%2 store=%3", _store.front().second.frame, static_cast<int>(_store.front().second.eyes), _store.size());
+ LOG_DEBUG_THREE_D("Shuffler emits time=%1 eyes=%2 store=%3", to_string(_store.front().second.time), static_cast<int>(_store.front().second.eyes), _store.size());
Video (_store.front().first, _store.front().second);
_last = _store.front().second;
_store.pop_front ();
diff --git a/src/lib/state.cc b/src/lib/state.cc
index a8230385d..7a3232ee9 100644
--- a/src/lib/state.cc
+++ b/src/lib/state.cc
@@ -35,7 +35,7 @@ boost::optional<boost::filesystem::path> State::override_path;
/* List of config versions to look for in descending order of preference;
* i.e. look at the first one, and if that doesn't exist, try the second, etc.
*/
-static std::vector<std::string> config_versions = { "2.16" };
+static std::vector<std::string> config_versions = { "2.18", "2.16" };
static
diff --git a/src/lib/text_content.cc b/src/lib/text_content.cc
index 92a35b822..09f6e41b6 100644
--- a/src/lib/text_content.cc
+++ b/src/lib/text_content.cc
@@ -235,8 +235,11 @@ TextContent::TextContent (Content* parent, cxml::ConstNodePtr node, int version,
if (lang) {
try {
_language = dcp::LanguageTag(lang->content());
- auto add = lang->optional_bool_attribute("Additional");
- _language_is_additional = add && *add;
+ auto additional = lang->optional_bool_attribute("Additional");
+ if (!additional) {
+ additional = lang->optional_bool_attribute("additional");
+ }
+ _language_is_additional = additional.get_value_or(false);
} catch (dcp::LanguageTagError&) {
/* The language tag can be empty or invalid if it was loaded from a
* 2.14.x metadata file; we'll just ignore it in that case.
@@ -409,7 +412,7 @@ TextContent::as_xml (xmlpp::Node* root) const
if (_language) {
auto lang = text->add_child("Language");
lang->add_child_text (_language->to_string());
- lang->set_attribute ("Additional", _language_is_additional ? "1" : "0");
+ lang->set_attribute("additional", _language_is_additional ? "1" : "0");
}
}
diff --git a/src/lib/transcode_job.cc b/src/lib/transcode_job.cc
index 12b9a2aa3..b260bc44b 100644
--- a/src/lib/transcode_job.cc
+++ b/src/lib/transcode_job.cc
@@ -148,6 +148,20 @@ TranscodeJob::run ()
}
+void
+TranscodeJob::pause()
+{
+ _encoder->pause();
+}
+
+
+void TranscodeJob::resume()
+{
+ _encoder->resume();
+ Job::resume();
+}
+
+
string
TranscodeJob::status () const
{
diff --git a/src/lib/transcode_job.h b/src/lib/transcode_job.h
index b05b20a16..35870231d 100644
--- a/src/lib/transcode_job.h
+++ b/src/lib/transcode_job.h
@@ -37,6 +37,8 @@
class Encoder;
+struct frames_not_lost_when_threads_disappear;
+
/** @class TranscodeJob
* @brief A job which transcodes a Film to another format.
@@ -56,6 +58,8 @@ public:
std::string name () const override;
std::string json_name () const override;
void run () override;
+ void pause() override;
+ void resume() override;
std::string status () const override;
bool enable_notify () const override {
return true;
@@ -64,6 +68,8 @@ public:
void set_encoder (std::shared_ptr<Encoder> t);
private:
+ friend struct ::frames_not_lost_when_threads_disappear;
+
virtual void post_transcode () {}
float frames_per_second() const;
diff --git a/src/lib/util.cc b/src/lib/util.cc
index 7f6e9da5a..01a7f0248 100644
--- a/src/lib/util.cc
+++ b/src/lib/util.cc
@@ -243,6 +243,7 @@ addr2line (void const * const addr)
{
char addr2line_cmd[512] = { 0 };
sprintf (addr2line_cmd, "addr2line -f -p -e %.256s %p > %s", program_name.c_str(), addr, backtrace_file.string().c_str());
+ std::cout << addr2line_cmd << "\n";
return system(addr2line_cmd);
}
@@ -428,6 +429,11 @@ dcpomatic_setup ()
SetUnhandledExceptionFilter(exception_handler);
#endif
+#ifdef DCPOMATIC_GROK
+ /* This makes grok support work with CUDA 12.2 */
+ setenv("CUDA_MODULE_LOADING", "EAGER", 1);
+#endif
+
#ifdef DCPOMATIC_HAVE_AVREGISTER
LIBDCP_DISABLE_WARNINGS
av_register_all ();
@@ -877,16 +883,6 @@ remap (shared_ptr<const AudioBuffers> input, int output_channels, AudioMapping m
return mapped;
}
-Eyes
-increment_eyes (Eyes e)
-{
- if (e == Eyes::LEFT) {
- return Eyes::RIGHT;
- }
-
- return Eyes::LEFT;
-}
-
size_t
utf8_strlen (string s)
@@ -1123,3 +1119,31 @@ word_wrap(string input, int columns)
return output;
}
+
+
+#ifdef DCPOMATIC_GROK
+void
+setup_grok_library_path()
+{
+ static std::string old_path;
+ if (old_path.empty()) {
+ auto const old = getenv("LD_LIRARY_PATH");
+ if (old) {
+ old_path = old;
+ }
+ }
+ auto const grok = Config::instance()->grok();
+ if (!grok || grok->binary_location.empty()) {
+ setenv("LD_LIRARY_PATH", old_path.c_str(), 1);
+ return;
+ }
+
+ std::string new_path = old_path;
+ if (!new_path.empty()) {
+ new_path += ":";
+ }
+ new_path += grok->binary_location.string();
+
+ setenv("LD_LIBRARY_PATH", new_path.c_str(), 1);
+}
+#endif
diff --git a/src/lib/util.h b/src/lib/util.h
index b92869b25..b85cf0a33 100644
--- a/src/lib/util.h
+++ b/src/lib/util.h
@@ -32,6 +32,7 @@
#include "dcpomatic_time.h"
#include "pixel_quanta.h"
#include "types.h"
+#include <libcxml/cxml.h>
#include <dcp/atmos_asset.h>
#include <dcp/decrypted_kdm.h>
#include <dcp/util.h>
@@ -97,5 +98,20 @@ extern std::string error_details(boost::system::error_code ec);
extern bool contains_assetmap(boost::filesystem::path dir);
extern std::string word_wrap(std::string input, int columns);
extern void capture_ffmpeg_logs();
+#ifdef DCPOMATIC_GROK
+extern void setup_grok_library_path();
+#endif
+
+
+template <class T>
+T
+number_attribute(cxml::ConstNodePtr node, std::string name1, std::string name2)
+{
+ auto value = node->optional_number_attribute<T>(name1);
+ if (!value) {
+ value = node->number_attribute<T>(name2);
+ }
+ return *value;
+}
#endif
diff --git a/src/lib/video_content.cc b/src/lib/video_content.cc
index 91ed11855..c6046cb37 100644
--- a/src/lib/video_content.cc
+++ b/src/lib/video_content.cc
@@ -424,27 +424,29 @@ VideoContent::size_after_crop () const
}
-/** @param f Frame index within the whole (untrimmed) content.
+/** @param time Time within the whole (untrimmed) content.
* @return Fade factor (between 0 and 1) or unset if there is no fade.
*/
optional<double>
-VideoContent::fade (shared_ptr<const Film> film, Frame f) const
+VideoContent::fade(shared_ptr<const Film> film, ContentTime time) const
{
- DCPOMATIC_ASSERT (f >= 0);
+ DCPOMATIC_ASSERT(time.get() >= 0);
double const vfr = _parent->active_video_frame_rate(film);
- auto const ts = _parent->trim_start().frames_round(vfr);
- if ((f - ts) < fade_in()) {
- return double (f - ts) / fade_in();
+ auto const ts = _parent->trim_start();
+ auto const fade_in_time = ContentTime::from_frames(fade_in(), vfr);
+ if ((time - ts) < fade_in_time) {
+ return double(ContentTime(time - ts).get()) / fade_in_time.get();
}
- auto fade_out_start = length() - _parent->trim_end().frames_round(vfr) - fade_out();
- if (f >= fade_out_start) {
- return 1 - double (f - fade_out_start) / fade_out();
+ auto const fade_out_time = ContentTime::from_frames(fade_out(), vfr);
+ auto fade_out_start = ContentTime::from_frames(length(), vfr) - _parent->trim_end() - fade_out_time;
+ if (time >= fade_out_start) {
+ return 1 - double(ContentTime(time - fade_out_start).get()) / fade_out_time.get();
}
- return optional<double> ();
+ return {};
}
string
diff --git a/src/lib/video_content.h b/src/lib/video_content.h
index e7e8eb1b3..d31c25f13 100644
--- a/src/lib/video_content.h
+++ b/src/lib/video_content.h
@@ -208,7 +208,7 @@ public:
boost::optional<dcp::Size> size_after_crop() const;
boost::optional<dcp::Size> scaled_size(dcp::Size container_size);
- boost::optional<double> fade (std::shared_ptr<const Film> film, Frame) const;
+ boost::optional<double> fade(std::shared_ptr<const Film> film, dcpomatic::ContentTime time) const;
std::string processing_description (std::shared_ptr<const Film> film);
diff --git a/src/lib/video_decoder.cc b/src/lib/video_decoder.cc
index cf21f885a..c628fddd9 100644
--- a/src/lib/video_decoder.cc
+++ b/src/lib/video_decoder.cc
@@ -20,7 +20,6 @@
#include "compose.hpp"
-#include "film.h"
#include "frame_interval_checker.h"
#include "image.h"
#include "j2k_image_proxy.h"
@@ -47,17 +46,9 @@ VideoDecoder::VideoDecoder (Decoder* parent, shared_ptr<const Content> c)
}
-/** Called by decoder classes when they have a video frame ready.
- * @param frame Frame index within the content; this does not take into account 3D
- * so for 3D_ALTERNATE this value goes:
- * 0: frame 0 left
- * 1: frame 0 right
- * 2: frame 1 left
- * 3: frame 1 right
- * and so on.
- */
+/** Called by decoder classes when they have a video frame ready */
void
-VideoDecoder::emit (shared_ptr<const Film> film, shared_ptr<const ImageProxy> image, Frame decoder_frame)
+VideoDecoder::emit(shared_ptr<const Film> film, shared_ptr<const ImageProxy> image, ContentTime time)
{
if (ignore ()) {
return;
@@ -66,14 +57,12 @@ VideoDecoder::emit (shared_ptr<const Film> film, shared_ptr<const ImageProxy> im
auto const afr = _content->active_video_frame_rate(film);
auto const vft = _content->video->frame_type();
- auto frame_time = ContentTime::from_frames (decoder_frame, afr);
-
/* Do some heuristics to try and spot the case where the user sets content to 3D
* when it is not. We try to tell this by looking at the differences in time between
* the first few frames. Real 3D content should have two frames for each timestamp.
*/
if (_frame_interval_checker) {
- _frame_interval_checker->feed (frame_time, afr);
+ _frame_interval_checker->feed(time, afr);
if (_frame_interval_checker->guess() == FrameIntervalChecker::PROBABLY_NOT_3D && vft == VideoFrameType::THREE_D) {
boost::throw_exception (
DecodeError(
@@ -91,94 +80,54 @@ VideoDecoder::emit (shared_ptr<const Film> film, shared_ptr<const ImageProxy> im
}
}
- Frame frame;
- Eyes eyes = Eyes::BOTH;
- if (!_position) {
- /* This is the first data we have received since initialisation or seek. Set
- the position based on the frame that was given. After this first time
- we just count frames, since (as with audio) it seems that ContentTimes
- are unreliable from FFmpegDecoder. They are much better than audio times
- but still we get the occasional one which is duplicated. In this case
- ffmpeg seems to carry on regardless, processing the video frame as normal.
- If we drop the frame with the duplicated timestamp we obviously lose sync.
- */
-
- if (vft == VideoFrameType::THREE_D_ALTERNATE) {
- frame = decoder_frame / 2;
- eyes = (decoder_frame % 2) ? Eyes::RIGHT : Eyes::LEFT;
- } else {
- frame = decoder_frame;
- if (vft == VideoFrameType::THREE_D) {
- auto j2k = dynamic_pointer_cast<const J2KImageProxy>(image);
- /* At the moment only DCP decoders producers VideoFrameType::THREE_D, so only the J2KImageProxy
- * knows which eye it is.
- */
- if (j2k && j2k->eye()) {
- eyes = j2k->eye().get() == dcp::Eye::LEFT ? Eyes::LEFT : Eyes::RIGHT;
- }
- }
- }
-
- _position = ContentTime::from_frames (frame, afr);
- } else {
- if (vft == VideoFrameType::THREE_D) {
- auto j2k = dynamic_pointer_cast<const J2KImageProxy>(image);
- if (j2k && j2k->eye()) {
- if (j2k->eye() == dcp::Eye::LEFT) {
- frame = _position->frames_round(afr) + 1;
- eyes = Eyes::LEFT;
- } else {
- frame = _position->frames_round(afr);
- eyes = Eyes::RIGHT;
- }
- } else {
- /* This should not happen; see above */
- frame = _position->frames_round(afr) + 1;
- }
- } else if (vft == VideoFrameType::THREE_D_ALTERNATE) {
- DCPOMATIC_ASSERT (_last_emitted_eyes);
- if (_last_emitted_eyes.get() == Eyes::RIGHT) {
- frame = _position->frames_round(afr) + 1;
- eyes = Eyes::LEFT;
- } else {
- frame = _position->frames_round(afr);
- eyes = Eyes::RIGHT;
- }
- } else {
- frame = _position->frames_round(afr) + 1;
- }
- }
-
switch (vft) {
case VideoFrameType::TWO_D:
+ Data(ContentVideo(image, time, Eyes::BOTH, Part::WHOLE));
+ break;
case VideoFrameType::THREE_D:
- Data (ContentVideo (image, frame, eyes, Part::WHOLE));
+ {
+ auto eyes = Eyes::LEFT;
+ auto j2k = dynamic_pointer_cast<const J2KImageProxy>(image);
+ if (j2k && j2k->eye()) {
+ eyes = *j2k->eye() == dcp::Eye::LEFT ? Eyes::LEFT : Eyes::RIGHT;
+ }
+
+ Data(ContentVideo(image, time, eyes, Part::WHOLE));
break;
+ }
case VideoFrameType::THREE_D_ALTERNATE:
{
- Data (ContentVideo (image, frame, eyes, Part::WHOLE));
+ Eyes eyes;
+ if (_last_emitted_eyes) {
+ eyes = _last_emitted_eyes.get() == Eyes::LEFT ? Eyes::RIGHT : Eyes::LEFT;
+ } else {
+ /* We don't know what eye this frame is, so just guess */
+ auto frame = time.frames_round(_content->video_frame_rate().get_value_or(24));
+ eyes = (frame % 2) ? Eyes::RIGHT : Eyes::LEFT;
+ }
+ Data(ContentVideo(image, time, eyes, Part::WHOLE));
_last_emitted_eyes = eyes;
break;
}
case VideoFrameType::THREE_D_LEFT_RIGHT:
- Data (ContentVideo (image, frame, Eyes::LEFT, Part::LEFT_HALF));
- Data (ContentVideo (image, frame, Eyes::RIGHT, Part::RIGHT_HALF));
+ Data(ContentVideo(image, time, Eyes::LEFT, Part::LEFT_HALF));
+ Data(ContentVideo(image, time, Eyes::RIGHT, Part::RIGHT_HALF));
break;
case VideoFrameType::THREE_D_TOP_BOTTOM:
- Data (ContentVideo (image, frame, Eyes::LEFT, Part::TOP_HALF));
- Data (ContentVideo (image, frame, Eyes::RIGHT, Part::BOTTOM_HALF));
+ Data(ContentVideo(image, time, Eyes::LEFT, Part::TOP_HALF));
+ Data(ContentVideo(image, time, Eyes::RIGHT, Part::BOTTOM_HALF));
break;
case VideoFrameType::THREE_D_LEFT:
- Data (ContentVideo (image, frame, Eyes::LEFT, Part::WHOLE));
+ Data(ContentVideo(image, time, Eyes::LEFT, Part::WHOLE));
break;
case VideoFrameType::THREE_D_RIGHT:
- Data (ContentVideo (image, frame, Eyes::RIGHT, Part::WHOLE));
+ Data(ContentVideo(image, time, Eyes::RIGHT, Part::WHOLE));
break;
default:
DCPOMATIC_ASSERT (false);
}
- _position = ContentTime::from_frames (frame, afr);
+ _position = time;
}
diff --git a/src/lib/video_decoder.h b/src/lib/video_decoder.h
index f6ee17425..b609404c4 100644
--- a/src/lib/video_decoder.h
+++ b/src/lib/video_decoder.h
@@ -60,7 +60,7 @@ public:
}
void seek () override;
- void emit (std::shared_ptr<const Film> film, std::shared_ptr<const ImageProxy>, Frame frame);
+ void emit(std::shared_ptr<const Film> film, std::shared_ptr<const ImageProxy>, dcpomatic::ContentTime time);
boost::signals2::signal<void (ContentVideo)> Data;
diff --git a/src/lib/video_mxf_decoder.cc b/src/lib/video_mxf_decoder.cc
index 40d3a461a..9f451297b 100644
--- a/src/lib/video_mxf_decoder.cc
+++ b/src/lib/video_mxf_decoder.cc
@@ -76,18 +76,18 @@ VideoMXFDecoder::pass ()
video->emit (
film(),
std::make_shared<J2KImageProxy>(_mono_reader->get_frame(frame), _size, AV_PIX_FMT_XYZ12LE, optional<int>()),
- frame
+ _next
);
} else {
video->emit (
film(),
std::make_shared<J2KImageProxy>(_stereo_reader->get_frame(frame), _size, dcp::Eye::LEFT, AV_PIX_FMT_XYZ12LE, optional<int>()),
- frame
+ _next
);
video->emit (
film(),
std::make_shared<J2KImageProxy>(_stereo_reader->get_frame(frame), _size, dcp::Eye::RIGHT, AV_PIX_FMT_XYZ12LE, optional<int>()),
- frame
+ _next
);
}
diff --git a/src/lib/writer.h b/src/lib/writer.h
index efb6a17d8..1cd278221 100644
--- a/src/lib/writer.h
+++ b/src/lib/writer.h
@@ -34,6 +34,7 @@
#include "exception_store.h"
#include "font_id_map.h"
#include "player_text.h"
+#include "text_type.h"
#include "weak_film.h"
#include <dcp/atmos_frame.h>
#include <boost/thread.hpp>
diff --git a/src/lib/wscript b/src/lib/wscript
index 0d61d7a69..5bae3e0d1 100644
--- a/src/lib/wscript
+++ b/src/lib/wscript
@@ -59,6 +59,7 @@ sources = """
content_factory.cc
combine_dcp_job.cc
copy_dcp_details_to_film.cc
+ cpu_j2k_encoder_thread.cc
create_cli.cc
crop.cc
cross_common.cc
@@ -138,6 +139,8 @@ sources = """
job.cc
job_manager.cc
j2k_encoder.cc
+ j2k_encoder_thread.cc
+ j2k_sync_encoder_thread.cc
json_server.cc
kdm_cli.cc
kdm_recipient.cc
@@ -163,6 +166,7 @@ sources = """
referenced_reel_asset.cc
release_notes.cc
render_text.cc
+ remote_j2k_encoder_thread.cc
resampler.cc
resolution.cc
rgba.cc
@@ -244,6 +248,9 @@ def build(bld):
if bld.env.TARGET_LINUX:
obj.uselib += ' POLKIT'
+ if bld.env.ENABLE_GROK:
+ obj.source += ' grok_j2k_encoder_thread.cc'
+
if bld.env.TARGET_WINDOWS_64 or bld.env.TARGET_WINDOWS_32:
obj.uselib += ' WINSOCK2 DBGHELP SHLWAPI MSWSOCK BOOST_LOCALE SETUPAPI OLE32 UUID'
obj.source += ' cross_windows.cc'