diff options
Diffstat (limited to 'src')
218 files changed, 6710 insertions, 3196 deletions
diff --git a/src/lib/analytics.cc b/src/lib/analytics.cc index 836051fe5..3fce79749 100644 --- a/src/lib/analytics.cc +++ b/src/lib/analytics.cc @@ -93,8 +93,8 @@ Analytics::write () const xmlpp::Document doc; auto root = doc.create_root_node ("Analytics"); - root->add_child("Version")->add_child_text(raw_convert<string>(_current_version)); - root->add_child("SuccessfulDCPEncodes")->add_child_text(raw_convert<string>(_successful_dcp_encodes)); + cxml::add_text_child(root, "Version", raw_convert<string>(_current_version)); + cxml::add_text_child(root, "SuccessfulDCPEncodes", raw_convert<string>(_successful_dcp_encodes)); try { doc.write_to_file_formatted(write_path("analytics.xml").string()); diff --git a/src/lib/atmos_content.cc b/src/lib/atmos_content.cc index ada304dad..22bd431a2 100644 --- a/src/lib/atmos_content.cc +++ b/src/lib/atmos_content.cc @@ -63,10 +63,10 @@ AtmosContent::from_xml (Content* parent, cxml::ConstNodePtr node) void -AtmosContent::as_xml (xmlpp::Node* node) const +AtmosContent::as_xml(xmlpp::Element* element) const { - node->add_child("AtmosLength")->add_child_text(dcp::raw_convert<string>(_length)); - node->add_child("AtmosEditRate")->add_child_text(_edit_rate.as_string()); + cxml::add_text_child(element, "AtmosLength", dcp::raw_convert<string>(_length)); + cxml::add_text_child(element, "AtmosEditRate", _edit_rate.as_string()); } diff --git a/src/lib/atmos_content.h b/src/lib/atmos_content.h index afa3c75a2..ffa181497 100644 --- a/src/lib/atmos_content.h +++ b/src/lib/atmos_content.h @@ -42,7 +42,7 @@ public: static std::shared_ptr<AtmosContent> from_xml (Content* parent, cxml::ConstNodePtr node); - void as_xml (xmlpp::Node* node) const; + void as_xml(xmlpp::Element* element) const; void set_length (Frame len); diff --git a/src/lib/atmos_mxf_content.cc b/src/lib/atmos_mxf_content.cc index 82c20e88f..1283961ee 100644 --- a/src/lib/atmos_mxf_content.cc +++ b/src/lib/atmos_mxf_content.cc @@ -98,11 +98,11 @@ AtmosMXFContent::summary () const void -AtmosMXFContent::as_xml (xmlpp::Node* node, bool with_paths) const +AtmosMXFContent::as_xml(xmlpp::Element* element, bool with_paths) const { - node->add_child("Type")->add_child_text("AtmosMXF"); - Content::as_xml (node, with_paths); - atmos->as_xml (node); + cxml::add_text_child(element, "Type", "AtmosMXF"); + Content::as_xml(element, with_paths); + atmos->as_xml(element); } diff --git a/src/lib/atmos_mxf_content.h b/src/lib/atmos_mxf_content.h index 57f041774..e6225a355 100644 --- a/src/lib/atmos_mxf_content.h +++ b/src/lib/atmos_mxf_content.h @@ -39,7 +39,7 @@ public: void examine (std::shared_ptr<const Film> film, std::shared_ptr<Job> job) override; std::string summary () const override; - void as_xml (xmlpp::Node* node, bool with_path) const override; + void as_xml(xmlpp::Element* element, bool with_path) const override; dcpomatic::DCPTime full_length (std::shared_ptr<const Film> film) const override; dcpomatic::DCPTime approximate_length () const override; diff --git a/src/lib/audio_analysis.cc b/src/lib/audio_analysis.cc index b8c2e072d..ac990ee18 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")) { @@ -141,44 +138,44 @@ void AudioAnalysis::write (boost::filesystem::path filename) { auto doc = make_shared<xmlpp::Document>(); - xmlpp::Element* root = doc->create_root_node ("AudioAnalysis"); + auto root = doc->create_root_node("AudioAnalysis"); - root->add_child("Version")->add_child_text(raw_convert<string>(_current_state_version)); + cxml::add_text_child(root, "Version", raw_convert<string>(_current_state_version)); for (auto& i: _data) { - auto channel = root->add_child ("Channel"); + auto channel = cxml::add_child(root, "Channel"); for (auto& j: i) { - j.as_xml (channel->add_child ("Point")); + j.as_xml(cxml::add_child(channel, "Point")); } } for (size_t i = 0; i < _sample_peak.size(); ++i) { - auto n = root->add_child("SamplePeak"); + auto n = cxml::add_child(root, "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) { - root->add_child("TruePeak")->add_child_text (raw_convert<string> (i)); + cxml::add_text_child(root, "TruePeak", raw_convert<string>(i)); } if (_integrated_loudness) { - root->add_child("IntegratedLoudness")->add_child_text (raw_convert<string> (_integrated_loudness.get ())); + cxml::add_text_child(root, "IntegratedLoudness", raw_convert<string>(_integrated_loudness.get())); } if (_loudness_range) { - root->add_child("LoudnessRange")->add_child_text (raw_convert<string> (_loudness_range.get ())); + cxml::add_text_child(root, "LoudnessRange", raw_convert<string>(_loudness_range.get())); } if (_analysis_gain) { - root->add_child("AnalysisGain")->add_child_text (raw_convert<string> (_analysis_gain.get ())); + cxml::add_text_child(root, "AnalysisGain", raw_convert<string>(_analysis_gain.get())); } - root->add_child("SamplesPerPoint")->add_child_text (raw_convert<string> (_samples_per_point)); - root->add_child("SampleRate")->add_child_text (raw_convert<string> (_sample_rate)); + cxml::add_text_child(root, "SamplesPerPoint", raw_convert<string>(_samples_per_point)); + cxml::add_text_child(root, "SampleRate", raw_convert<string>(_sample_rate)); if (_leqm) { - root->add_child("Leqm")->add_child_text(raw_convert<string>(*_leqm)); + cxml::add_text_child(root, "Leqm", raw_convert<string>(*_leqm)); } doc->write_to_file_formatted (filename.string ()); diff --git a/src/lib/audio_content.cc b/src/lib/audio_content.cc index 16f2bd5f1..2569e1cf0 100644 --- a/src/lib/audio_content.cc +++ b/src/lib/audio_content.cc @@ -124,14 +124,14 @@ AudioContent::AudioContent (Content* parent, vector<shared_ptr<Content>> c) void -AudioContent::as_xml (xmlpp::Node* node) const +AudioContent::as_xml(xmlpp::Element* element) const { boost::mutex::scoped_lock lm (_mutex); - node->add_child("AudioGain")->add_child_text(raw_convert<string>(_gain)); - node->add_child("AudioDelay")->add_child_text(raw_convert<string>(_delay)); - node->add_child("AudioFadeIn")->add_child_text(raw_convert<string>(_fade_in.get())); - node->add_child("AudioFadeOut")->add_child_text(raw_convert<string>(_fade_out.get())); - node->add_child("AudioUseSameFadesAsVideo")->add_child_text(_use_same_fades_as_video ? "1" : "0"); + cxml::add_text_child(element, "AudioGain", raw_convert<string>(_gain)); + cxml::add_text_child(element, "AudioDelay", raw_convert<string>(_delay)); + cxml::add_text_child(element, "AudioFadeIn", raw_convert<string>(_fade_in.get())); + cxml::add_text_child(element, "AudioFadeOut", raw_convert<string>(_fade_out.get())); + cxml::add_text_child(element, "AudioUseSameFadesAsVideo", _use_same_fades_as_video ? "1" : "0"); } diff --git a/src/lib/audio_content.h b/src/lib/audio_content.h index 084871c8b..2a140b3e4 100644 --- a/src/lib/audio_content.h +++ b/src/lib/audio_content.h @@ -56,7 +56,7 @@ public: AudioContent (Content* parent, std::vector<std::shared_ptr<Content>>); AudioContent (Content* parent, cxml::ConstNodePtr); - void as_xml (xmlpp::Node *) const; + void as_xml(xmlpp::Element*) const; std::string technical_summary () const; void take_settings_from (std::shared_ptr<const AudioContent> c); 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..37929001d 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()) ); } @@ -219,19 +220,19 @@ AudioMapping::get (int input_channel, int output_channel) const void -AudioMapping::as_xml (xmlpp::Node* node) const +AudioMapping::as_xml(xmlpp::Element* element) const { auto const input = input_channels(); auto const output = output_channels(); - node->add_child("InputChannels")->add_child_text(raw_convert<string>(input)); - node->add_child("OutputChannels")->add_child_text(raw_convert<string>(output)); + cxml::add_text_child(element, "InputChannels", raw_convert<string>(input)); + cxml::add_text_child(element, "OutputChannels", raw_convert<string>(output)); 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)); + auto t = cxml::add_child(element, "Gain"); + 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/audio_mapping.h b/src/lib/audio_mapping.h index fe9a79789..68487d908 100644 --- a/src/lib/audio_mapping.h +++ b/src/lib/audio_mapping.h @@ -52,7 +52,7 @@ public: /* Default copy constructor is fine */ - void as_xml (xmlpp::Node *) const; + void as_xml(xmlpp::Element*) const; void make_zero (); void make_default (AudioProcessor const * processor, boost::optional<boost::filesystem::path> filename = boost::optional<boost::filesystem::path>()); diff --git a/src/lib/audio_point.cc b/src/lib/audio_point.cc index 1f32d25fc..036904520 100644 --- a/src/lib/audio_point.cc +++ b/src/lib/audio_point.cc @@ -72,6 +72,6 @@ AudioPoint::operator= (AudioPoint const & other) void AudioPoint::as_xml (xmlpp::Element* parent) const { - parent->add_child("Peak")->add_child_text(raw_convert<string>(_data[PEAK])); - parent->add_child("RMS")->add_child_text(raw_convert<string>(_data[RMS])); + cxml::add_text_child(parent, "Peak", raw_convert<string>(_data[PEAK])); + cxml::add_text_child(parent, "RMS", raw_convert<string>(_data[RMS])); } 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..6bd063733 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 @@ -65,19 +57,16 @@ Cinema::read_screens (cxml::ConstNodePtr node) void Cinema::as_xml (xmlpp::Element* parent) const { - parent->add_child("Name")->add_child_text (name); + cxml::add_text_child(parent, "Name", name); for (auto i: emails) { - parent->add_child("Email")->add_child_text (i); + cxml::add_text_child(parent, "Email", i); } - 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)); + cxml::add_text_child(parent, "Notes", notes); for (auto i: _screens) { - i->as_xml (parent->add_child ("Screen")); + i->as_xml(cxml::add_child(parent, "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/colour_conversion.cc b/src/lib/colour_conversion.cc index bd1b47cb1..f1e625812 100644 --- a/src/lib/colour_conversion.cc +++ b/src/lib/colour_conversion.cc @@ -151,41 +151,40 @@ ColourConversion::from_xml (cxml::NodePtr node, int version) } void -ColourConversion::as_xml (xmlpp::Node* node) const +ColourConversion::as_xml(xmlpp::Element* element) const { - auto in_node = node->add_child ("InputTransferFunction"); + auto in_node = cxml::add_child(element, "InputTransferFunction"); if (dynamic_pointer_cast<const dcp::GammaTransferFunction> (_in)) { auto tf = dynamic_pointer_cast<const dcp::GammaTransferFunction> (_in); - in_node->add_child("Type")->add_child_text ("Gamma"); - in_node->add_child("Gamma")->add_child_text (raw_convert<string> (tf->gamma ())); + cxml::add_text_child(in_node, "Type", "Gamma"); + cxml::add_text_child(in_node, "Gamma", raw_convert<string>(tf->gamma())); } else if (dynamic_pointer_cast<const dcp::ModifiedGammaTransferFunction> (_in)) { auto tf = dynamic_pointer_cast<const dcp::ModifiedGammaTransferFunction> (_in); - in_node->add_child("Type")->add_child_text ("ModifiedGamma"); - in_node->add_child("Power")->add_child_text (raw_convert<string> (tf->power ())); - in_node->add_child("Threshold")->add_child_text (raw_convert<string> (tf->threshold ())); - in_node->add_child("A")->add_child_text (raw_convert<string> (tf->A ())); - in_node->add_child("B")->add_child_text (raw_convert<string> (tf->B ())); - } else if (dynamic_pointer_cast<const dcp::SGamut3TransferFunction> (_in)) { - in_node->add_child("Type")->add_child_text ("SGamut3"); + cxml::add_text_child(in_node, "Type", "ModifiedGamma"); + cxml::add_text_child(in_node, "Power", raw_convert<string>(tf->power ())); + cxml::add_text_child(in_node, "Threshold", raw_convert<string>(tf->threshold ())); + cxml::add_text_child(in_node, "A", raw_convert<string>(tf->A())); + cxml::add_text_child(in_node, "B", raw_convert<string>(tf->B())); + } else if (dynamic_pointer_cast<const dcp::SGamut3TransferFunction>(_in)) { + cxml::add_text_child(in_node, "Type", "SGamut3"); } - node->add_child("YUVToRGB")->add_child_text (raw_convert<string> (static_cast<int> (_yuv_to_rgb))); - node->add_child("RedX")->add_child_text (raw_convert<string> (_red.x)); - node->add_child("RedY")->add_child_text (raw_convert<string> (_red.y)); - node->add_child("GreenX")->add_child_text (raw_convert<string> (_green.x)); - node->add_child("GreenY")->add_child_text (raw_convert<string> (_green.y)); - node->add_child("BlueX")->add_child_text (raw_convert<string> (_blue.x)); - node->add_child("BlueY")->add_child_text (raw_convert<string> (_blue.y)); - node->add_child("WhiteX")->add_child_text (raw_convert<string> (_white.x)); - node->add_child("WhiteY")->add_child_text (raw_convert<string> (_white.y)); + cxml::add_text_child(element, "YUVToRGB", raw_convert<string>(static_cast<int>(_yuv_to_rgb))); + cxml::add_text_child(element, "RedX", raw_convert<string>(_red.x)); + cxml::add_text_child(element, "RedY", raw_convert<string>(_red.y)); + cxml::add_text_child(element, "GreenX", raw_convert<string>(_green.x)); + cxml::add_text_child(element, "GreenY", raw_convert<string>(_green.y)); + cxml::add_text_child(element, "BlueX", raw_convert<string>(_blue.x)); + cxml::add_text_child(element, "BlueY", raw_convert<string>(_blue.y)); + cxml::add_text_child(element, "WhiteX", raw_convert<string>(_white.x)); + cxml::add_text_child(element, "WhiteY", raw_convert<string>(_white.y)); if (_adjusted_white) { - node->add_child("AdjustedWhiteX")->add_child_text (raw_convert<string> (_adjusted_white.get().x)); - node->add_child("AdjustedWhiteY")->add_child_text (raw_convert<string> (_adjusted_white.get().y)); + cxml::add_text_child(element, "AdjustedWhiteX", raw_convert<string>(_adjusted_white.get().x)); + cxml::add_text_child(element, "AdjustedWhiteY", raw_convert<string>(_adjusted_white.get().y)); } - if (dynamic_pointer_cast<const dcp::GammaTransferFunction> (_out)) { - shared_ptr<const dcp::GammaTransferFunction> gf = dynamic_pointer_cast<const dcp::GammaTransferFunction> (_out); - node->add_child("OutputGamma")->add_child_text (raw_convert<string> (gf->gamma ())); + if (auto gf = dynamic_pointer_cast<const dcp::GammaTransferFunction>(_out)) { + cxml::add_text_child(element, "OutputGamma", raw_convert<string>(gf->gamma())); } } diff --git a/src/lib/colour_conversion.h b/src/lib/colour_conversion.h index 73b6ad23c..0c07ddbac 100644 --- a/src/lib/colour_conversion.h +++ b/src/lib/colour_conversion.h @@ -41,7 +41,7 @@ public: ColourConversion (cxml::NodePtr, int version); virtual ~ColourConversion () {} - virtual void as_xml (xmlpp::Node *) const; + virtual void as_xml(xmlpp::Element*) const; std::string identifier () const; boost::optional<size_t> preset () const; diff --git a/src/lib/config.cc b/src/lib/config.cc index 384db5cde..c80ef224e 100644 --- a/src/lib/config.cc +++ b/src/lib/config.cc @@ -177,6 +177,8 @@ Config::set_defaults () _gdc_username = optional<string>(); _gdc_password = optional<string>(); _player_mode = PLAYER_MODE_WINDOW; + _player_restricted_menus = false; + _playlist_editor_restricted_menus = false; _image_display = 0; _video_view_type = VIDEO_VIEW_SIMPLE; _respect_kdm_validity_periods = true; @@ -220,6 +222,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 +500,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 +572,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()); } @@ -588,6 +594,9 @@ try _player_mode = PLAYER_MODE_DUAL; } + _player_restricted_menus = f.optional_bool_child("PlayerRestrictedMenus").get_value_or(false); + _playlist_editor_restricted_menus = f.optional_bool_child("PlaylistEditorRestrictedMenus").get_value_or(false); + _image_display = f.optional_number_child<int>("ImageDisplay").get_value_or(0); auto vc = f.optional_string_child("VideoViewType"); if (vc && *vc == "opengl") { @@ -642,6 +651,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 (...) { @@ -721,218 +736,218 @@ Config::write_config () const auto root = doc.create_root_node ("Config"); /* [XML] Version The version number of the configuration file format. */ - root->add_child("Version")->add_child_text (raw_convert<string>(_current_version)); + cxml::add_text_child(root, "Version", raw_convert<string>(_current_version)); /* [XML] MasterEncodingThreads Number of encoding threads to use when running as master. */ - root->add_child("MasterEncodingThreads")->add_child_text (raw_convert<string> (_master_encoding_threads)); + cxml::add_text_child(root, "MasterEncodingThreads", raw_convert<string>(_master_encoding_threads)); /* [XML] ServerEncodingThreads Number of encoding threads to use when running as server. */ - root->add_child("ServerEncodingThreads")->add_child_text (raw_convert<string> (_server_encoding_threads)); + cxml::add_text_child(root, "ServerEncodingThreads", raw_convert<string>(_server_encoding_threads)); if (_default_directory) { /* [XML:opt] DefaultDirectory Default directory when creating a new film in the GUI. */ - root->add_child("DefaultDirectory")->add_child_text (_default_directory->string ()); + cxml::add_text_child(root, "DefaultDirectory", _default_directory->string()); } /* [XML] ServerPortBase Port number to use for frame encoding requests. <code>ServerPortBase</code> + 1 and <code>ServerPortBase</code> + 2 are used for querying servers. <code>ServerPortBase</code> + 3 is used by the batch converter to listen for job requests. */ - root->add_child("ServerPortBase")->add_child_text (raw_convert<string> (_server_port_base)); + cxml::add_text_child(root, "ServerPortBase", raw_convert<string>(_server_port_base)); /* [XML] UseAnyServers 1 to broadcast to look for encoding servers to use, 0 to use only those configured. */ - root->add_child("UseAnyServers")->add_child_text (_use_any_servers ? "1" : "0"); + cxml::add_text_child(root, "UseAnyServers", _use_any_servers ? "1" : "0"); for (auto i: _servers) { /* [XML:opt] Server IP address or hostname of an encoding server to use; you can use as many of these tags as you like. */ - root->add_child("Server")->add_child_text (i); + cxml::add_text_child(root, "Server", i); } /* [XML] OnlyServersEncode 1 to set the master to do decoding of source content no JPEG2000 encoding; all encoding is done by the encoding servers. 0 to set the master to do some encoding as well as coordinating the job. */ - root->add_child("OnlyServersEncode")->add_child_text (_only_servers_encode ? "1" : "0"); + cxml::add_text_child(root, "OnlyServersEncode", _only_servers_encode ? "1" : "0"); /* [XML] TMSProtocol Protocol to use to copy files to a TMS; 0 to use SCP, 1 for FTP. */ - root->add_child("TMSProtocol")->add_child_text (raw_convert<string> (static_cast<int> (_tms_protocol))); + cxml::add_text_child(root, "TMSProtocol", raw_convert<string>(static_cast<int>(_tms_protocol))); /* [XML] TMSPassive True to use PASV mode with TMS FTP connections. */ - root->add_child("TMSPassive")->add_child_text(_tms_passive ? "1" : "0"); + cxml::add_text_child(root, "TMSPassive", _tms_passive ? "1" : "0"); /* [XML] TMSIP IP address of TMS. */ - root->add_child("TMSIP")->add_child_text (_tms_ip); + cxml::add_text_child(root, "TMSIP", _tms_ip); /* [XML] TMSPath Path on the TMS to copy files to. */ - root->add_child("TMSPath")->add_child_text (_tms_path); + cxml::add_text_child(root, "TMSPath", _tms_path); /* [XML] TMSUser Username to log into the TMS with. */ - root->add_child("TMSUser")->add_child_text (_tms_user); + cxml::add_text_child(root, "TMSUser", _tms_user); /* [XML] TMSPassword Password to log into the TMS with. */ - root->add_child("TMSPassword")->add_child_text (_tms_password); + cxml::add_text_child(root, "TMSPassword", _tms_password); if (_language) { /* [XML:opt] Language Language to use in the GUI e.g. <code>fr_FR</code>. */ - root->add_child("Language")->add_child_text (_language.get()); + cxml::add_text_child(root, "Language", _language.get()); } if (_default_dcp_content_type) { /* [XML:opt] DefaultDCPContentType Default content type to use when creating new films (<code>FTR</code>, <code>SHR</code>, <code>TLR</code>, <code>TST</code>, <code>XSN</code>, <code>RTG</code>, <code>TSR</code>, <code>POL</code>, <code>PSA</code> or <code>ADV</code>). */ - root->add_child("DefaultDCPContentType")->add_child_text (_default_dcp_content_type->isdcf_name ()); + cxml::add_text_child(root, "DefaultDCPContentType", _default_dcp_content_type->isdcf_name()); } /* [XML] DefaultDCPAudioChannels Default number of audio channels to use when creating new films. */ - root->add_child("DefaultDCPAudioChannels")->add_child_text (raw_convert<string> (_default_dcp_audio_channels)); + cxml::add_text_child(root, "DefaultDCPAudioChannels", raw_convert<string>(_default_dcp_audio_channels)); /* [XML] DCPIssuer Issuer text to write into CPL files. */ - root->add_child("DCPIssuer")->add_child_text (_dcp_issuer); + cxml::add_text_child(root, "DCPIssuer", _dcp_issuer); /* [XML] DCPCreator Creator text to write into CPL files. */ - root->add_child("DCPCreator")->add_child_text (_dcp_creator); + cxml::add_text_child(root, "DCPCreator", _dcp_creator); /* [XML] Company name to write into MXF files. */ - root->add_child("DCPCompanyName")->add_child_text (_dcp_company_name); + cxml::add_text_child(root, "DCPCompanyName", _dcp_company_name); /* [XML] Product name to write into MXF files. */ - root->add_child("DCPProductName")->add_child_text (_dcp_product_name); + cxml::add_text_child(root, "DCPProductName", _dcp_product_name); /* [XML] Product version to write into MXF files. */ - root->add_child("DCPProductVersion")->add_child_text (_dcp_product_version); + cxml::add_text_child(root, "DCPProductVersion", _dcp_product_version); /* [XML] Comment to write into JPEG2000 data. */ - root->add_child("DCPJ2KComment")->add_child_text (_dcp_j2k_comment); + cxml::add_text_child(root, "DCPJ2KComment", _dcp_j2k_comment); /* [XML] UploadAfterMakeDCP 1 to upload to a TMS after making a DCP, 0 for no upload. */ - root->add_child("UploadAfterMakeDCP")->add_child_text (_upload_after_make_dcp ? "1" : "0"); + cxml::add_text_child(root, "UploadAfterMakeDCP", _upload_after_make_dcp ? "1" : "0"); /* [XML] DefaultStillLength Default length (in seconds) for still images in new films. */ - root->add_child("DefaultStillLength")->add_child_text (raw_convert<string> (_default_still_length)); + cxml::add_text_child(root, "DefaultStillLength", raw_convert<string>(_default_still_length)); /* [XML] DefaultJ2KBandwidth Default bitrate (in bits per second) for JPEG2000 data in new films. */ - root->add_child("DefaultJ2KBandwidth")->add_child_text (raw_convert<string> (_default_j2k_bandwidth)); + cxml::add_text_child(root, "DefaultJ2KBandwidth", raw_convert<string>(_default_j2k_bandwidth)); /* [XML] DefaultAudioDelay Default delay to apply to audio (positive moves audio later) in milliseconds. */ - root->add_child("DefaultAudioDelay")->add_child_text (raw_convert<string> (_default_audio_delay)); + cxml::add_text_child(root, "DefaultAudioDelay", raw_convert<string>(_default_audio_delay)); /* [XML] DefaultInterop 1 to default new films to Interop, 0 for SMPTE. */ - root->add_child("DefaultInterop")->add_child_text (_default_interop ? "1" : "0"); + cxml::add_text_child(root, "DefaultInterop", _default_interop ? "1" : "0"); if (_default_audio_language) { /* [XML] DefaultAudioLanguage Default audio language to use for new films */ - root->add_child("DefaultAudioLanguage")->add_child_text(_default_audio_language->to_string()); + cxml::add_text_child(root, "DefaultAudioLanguage", _default_audio_language->to_string()); } if (_default_territory) { /* [XML] DefaultTerritory Default territory to use for new films */ - root->add_child("DefaultTerritory")->add_child_text(_default_territory->subtag()); + cxml::add_text_child(root, "DefaultTerritory", _default_territory->subtag()); } for (auto const& i: _default_metadata) { - auto c = root->add_child("DefaultMetadata"); + auto c = cxml::add_child(root, "DefaultMetadata"); c->set_attribute("key", i.first); c->add_child_text(i.second); } if (_default_kdm_directory) { /* [XML:opt] DefaultKDMDirectory Default directory to write KDMs to. */ - root->add_child("DefaultKDMDirectory")->add_child_text (_default_kdm_directory->string ()); + cxml::add_text_child(root, "DefaultKDMDirectory", _default_kdm_directory->string ()); } - _default_kdm_duration.as_xml(root->add_child("DefaultKDMDuration")); + _default_kdm_duration.as_xml(cxml::add_child(root, "DefaultKDMDuration")); /* [XML] MailServer Hostname of SMTP server to use. */ - root->add_child("MailServer")->add_child_text (_mail_server); + cxml::add_text_child(root, "MailServer", _mail_server); /* [XML] MailPort Port number to use on SMTP server. */ - root->add_child("MailPort")->add_child_text (raw_convert<string> (_mail_port)); + cxml::add_text_child(root, "MailPort", raw_convert<string>(_mail_port)); /* [XML] MailProtocol Protocol to use on SMTP server (Auto, Plain, STARTTLS or SSL) */ switch (_mail_protocol) { case EmailProtocol::AUTO: - root->add_child("MailProtocol")->add_child_text("Auto"); + cxml::add_text_child(root, "MailProtocol", "Auto"); break; case EmailProtocol::PLAIN: - root->add_child("MailProtocol")->add_child_text("Plain"); + cxml::add_text_child(root, "MailProtocol", "Plain"); break; case EmailProtocol::STARTTLS: - root->add_child("MailProtocol")->add_child_text("STARTTLS"); + cxml::add_text_child(root, "MailProtocol", "STARTTLS"); break; case EmailProtocol::SSL: - root->add_child("MailProtocol")->add_child_text("SSL"); + cxml::add_text_child(root, "MailProtocol", "SSL"); break; } /* [XML] MailUser Username to use on SMTP server. */ - root->add_child("MailUser")->add_child_text (_mail_user); + cxml::add_text_child(root, "MailUser", _mail_user); /* [XML] MailPassword Password to use on SMTP server. */ - root->add_child("MailPassword")->add_child_text (_mail_password); + cxml::add_text_child(root, "MailPassword", _mail_password); /* [XML] KDMSubject Subject to use for KDM emails. */ - root->add_child("KDMSubject")->add_child_text (_kdm_subject); + cxml::add_text_child(root, "KDMSubject", _kdm_subject); /* [XML] KDMFrom From address to use for KDM emails. */ - root->add_child("KDMFrom")->add_child_text (_kdm_from); + cxml::add_text_child(root, "KDMFrom", _kdm_from); for (auto i: _kdm_cc) { /* [XML] KDMCC CC address to use for KDM emails; you can use as many of these tags as you like. */ - root->add_child("KDMCC")->add_child_text (i); + cxml::add_text_child(root, "KDMCC", i); } /* [XML] KDMBCC BCC address to use for KDM emails. */ - root->add_child("KDMBCC")->add_child_text (_kdm_bcc); + cxml::add_text_child(root, "KDMBCC", _kdm_bcc); /* [XML] KDMEmail Text of KDM email. */ - root->add_child("KDMEmail")->add_child_text (_kdm_email); + cxml::add_text_child(root, "KDMEmail", _kdm_email); /* [XML] NotificationSubject Subject to use for notification emails. */ - root->add_child("NotificationSubject")->add_child_text (_notification_subject); + cxml::add_text_child(root, "NotificationSubject", _notification_subject); /* [XML] NotificationFrom From address to use for notification emails. */ - root->add_child("NotificationFrom")->add_child_text (_notification_from); + cxml::add_text_child(root, "NotificationFrom", _notification_from); /* [XML] NotificationFrom To address to use for notification emails. */ - root->add_child("NotificationTo")->add_child_text (_notification_to); + cxml::add_text_child(root, "NotificationTo", _notification_to); for (auto i: _notification_cc) { /* [XML] NotificationCC CC address to use for notification emails; you can use as many of these tags as you like. */ - root->add_child("NotificationCC")->add_child_text (i); + cxml::add_text_child(root, "NotificationCC", i); } /* [XML] NotificationBCC BCC address to use for notification emails. */ - root->add_child("NotificationBCC")->add_child_text (_notification_bcc); + cxml::add_text_child(root, "NotificationBCC", _notification_bcc); /* [XML] NotificationEmail Text of notification email. */ - root->add_child("NotificationEmail")->add_child_text (_notification_email); + cxml::add_text_child(root, "NotificationEmail", _notification_email); /* [XML] CheckForUpdates 1 to check dcpomatic.com for new versions, 0 to check only on request. */ - root->add_child("CheckForUpdates")->add_child_text (_check_for_updates ? "1" : "0"); + cxml::add_text_child(root, "CheckForUpdates", _check_for_updates ? "1" : "0"); /* [XML] CheckForUpdates 1 to check dcpomatic.com for new text versions, 0 to check only on request. */ - root->add_child("CheckForTestUpdates")->add_child_text (_check_for_test_updates ? "1" : "0"); + cxml::add_text_child(root, "CheckForTestUpdates", _check_for_test_updates ? "1" : "0"); /* [XML] MaximumJ2KBandwidth Maximum J2K bandwidth (in bits per second) that can be specified in the GUI. */ - root->add_child("MaximumJ2KBandwidth")->add_child_text (raw_convert<string> (_maximum_j2k_bandwidth)); + cxml::add_text_child(root, "MaximumJ2KBandwidth", raw_convert<string>(_maximum_j2k_bandwidth)); /* [XML] AllowAnyDCPFrameRate 1 to allow users to specify any frame rate when creating DCPs, 0 to limit the GUI to standard rates. */ - root->add_child("AllowAnyDCPFrameRate")->add_child_text (_allow_any_dcp_frame_rate ? "1" : "0"); + cxml::add_text_child(root, "AllowAnyDCPFrameRate", _allow_any_dcp_frame_rate ? "1" : "0"); /* [XML] AllowAnyContainer 1 to allow users to user any container ratio for their DCP, 0 to limit the GUI to DCI Flat/Scope */ - root->add_child("AllowAnyContainer")->add_child_text (_allow_any_container ? "1" : "0"); + cxml::add_text_child(root, "AllowAnyContainer", _allow_any_container ? "1" : "0"); /* [XML] Allow96kHzAudio 1 to allow users to make DCPs with 96kHz audio, 0 to always make 48kHz DCPs */ - root->add_child("Allow96kHzAudio")->add_child_text(_allow_96khz_audio ? "1" : "0"); + cxml::add_text_child(root, "Allow96kHzAudio", _allow_96khz_audio ? "1" : "0"); /* [XML] UseAllAudioChannels 1 to allow users to map audio to all 16 DCP channels, 0 to limit to the channels used in standard DCPs */ - root->add_child("UseAllAudioChannels")->add_child_text(_use_all_audio_channels ? "1" : "0"); + cxml::add_text_child(root, "UseAllAudioChannels", _use_all_audio_channels ? "1" : "0"); /* [XML] ShowExperimentalAudioProcessors 1 to offer users the (experimental) audio upmixer processors, 0 to hide them */ - root->add_child("ShowExperimentalAudioProcessors")->add_child_text (_show_experimental_audio_processors ? "1" : "0"); + cxml::add_text_child(root, "ShowExperimentalAudioProcessors", _show_experimental_audio_processors ? "1" : "0"); /* [XML] LogTypes Types of logging to write; a bitfield where 1 is general notes, 2 warnings, 4 errors, 8 debug information related to 3D, 16 debug information related to encoding, 32 debug information for timing purposes, 64 debug information related to sending email, 128 debug information related to the video view, 256 information about disk writing, 512 debug information related to the player, 1024 debug information related to audio analyses. */ - root->add_child("LogTypes")->add_child_text (raw_convert<string> (_log_types)); + cxml::add_text_child(root, "LogTypes", raw_convert<string> (_log_types)); /* [XML] AnalyseEBUR128 1 to do EBUR128 analyses when analysing audio, otherwise 0. */ - root->add_child("AnalyseEBUR128")->add_child_text (_analyse_ebur128 ? "1" : "0"); + cxml::add_text_child(root, "AnalyseEBUR128", _analyse_ebur128 ? "1" : "0"); /* [XML] AutomaticAudioAnalysis 1 to run audio analysis automatically when audio content is added to the film, otherwise 0. */ - root->add_child("AutomaticAudioAnalysis")->add_child_text (_automatic_audio_analysis ? "1" : "0"); + cxml::add_text_child(root, "AutomaticAudioAnalysis", _automatic_audio_analysis ? "1" : "0"); #ifdef DCPOMATIC_WINDOWS if (_win32_console) { /* [XML] Win32Console 1 to open a console when running on Windows, otherwise 0. * We only write this if it's true, which is a bit of a hack to allow unit tests to work * more easily on Windows (without a platform-specific reference in config_write_utf8_test) */ - root->add_child("Win32Console")->add_child_text ("1"); + cxml::add_text_child(root, "Win32Console", "1"); } #endif /* [XML] Signer Certificate chain and private key to use when signing DCPs and KDMs. Should contain <code><Certificate></code> tags in order and a <code><PrivateKey></code> tag all containing PEM-encoded certificates or private keys as appropriate. */ - auto signer = root->add_child ("Signer"); + auto signer = cxml::add_child(root, "Signer"); DCPOMATIC_ASSERT (_signer_chain); for (auto const& i: _signer_chain->unordered()) { - signer->add_child("Certificate")->add_child_text (i.certificate (true)); + cxml::add_text_child(signer, "Certificate", i.certificate (true)); } - signer->add_child("PrivateKey")->add_child_text (_signer_chain->key().get ()); + cxml::add_text_child(signer, "PrivateKey", _signer_chain->key().get ()); /* [XML] Decryption Certificate chain and private key to use when decrypting KDMs */ - auto decryption = root->add_child ("Decryption"); + auto decryption = cxml::add_child(root, "Decryption"); DCPOMATIC_ASSERT (_decryption_chain); for (auto const& i: _decryption_chain->unordered()) { - decryption->add_child("Certificate")->add_child_text (i.certificate (true)); + cxml::add_text_child(decryption, "Certificate", i.certificate (true)); } - decryption->add_child("PrivateKey")->add_child_text (_decryption_chain->key().get ()); + cxml::add_text_child(decryption, "PrivateKey", _decryption_chain->key().get()); /* [XML] History Filename of DCP to present in the <guilabel>File</guilabel> menu of the GUI; there can be more than one of these tags. */ for (auto i: _history) { - root->add_child("History")->add_child_text (i.string ()); + cxml::add_text_child(root, "History", i.string()); } /* [XML] History Filename of DCP to present in the <guilabel>File</guilabel> menu of the player; there can be more than one of these tags. */ for (auto i: _player_history) { - root->add_child("PlayerHistory")->add_child_text (i.string ()); + cxml::add_text_child(root, "PlayerHistory", i.string()); } /* [XML] DKDMGroup A group of DKDMs, each with a <code>Name</code> attribute, containing other <code><DKDMGroup></code> @@ -942,53 +957,53 @@ Config::write_config () const _dkdms->as_xml (root); /* [XML] CinemasFile Filename of cinemas list file. */ - root->add_child("CinemasFile")->add_child_text (_cinemas_file.string()); + cxml::add_text_child(root, "CinemasFile", _cinemas_file.string()); /* [XML] DKDMRecipientsFile Filename of DKDM recipients list file. */ - root->add_child("DKDMRecipientsFile")->add_child_text (_dkdm_recipients_file.string()); + cxml::add_text_child(root, "DKDMRecipientsFile", _dkdm_recipients_file.string()); /* [XML] ShowHintsBeforeMakeDCP 1 to show hints in the GUI before making a DCP, otherwise 0. */ - root->add_child("ShowHintsBeforeMakeDCP")->add_child_text (_show_hints_before_make_dcp ? "1" : "0"); + cxml::add_text_child(root, "ShowHintsBeforeMakeDCP", _show_hints_before_make_dcp ? "1" : "0"); /* [XML] ConfirmKDMEmail 1 to confirm before sending KDM emails in the GUI, otherwise 0. */ - root->add_child("ConfirmKDMEmail")->add_child_text (_confirm_kdm_email ? "1" : "0"); + cxml::add_text_child(root, "ConfirmKDMEmail", _confirm_kdm_email ? "1" : "0"); /* [XML] KDMFilenameFormat Format for KDM filenames. */ - root->add_child("KDMFilenameFormat")->add_child_text (_kdm_filename_format.specification ()); + cxml::add_text_child(root, "KDMFilenameFormat", _kdm_filename_format.specification()); /* [XML] KDMFilenameFormat Format for DKDM filenames. */ - root->add_child("DKDMFilenameFormat")->add_child_text(_dkdm_filename_format.specification()); + cxml::add_text_child(root, "DKDMFilenameFormat", _dkdm_filename_format.specification()); /* [XML] KDMContainerNameFormat Format for KDM containers (directories or ZIP files). */ - root->add_child("KDMContainerNameFormat")->add_child_text (_kdm_container_name_format.specification ()); + cxml::add_text_child(root, "KDMContainerNameFormat", _kdm_container_name_format.specification()); /* [XML] DCPMetadataFilenameFormat Format for DCP metadata filenames. */ - root->add_child("DCPMetadataFilenameFormat")->add_child_text (_dcp_metadata_filename_format.specification ()); + cxml::add_text_child(root, "DCPMetadataFilenameFormat", _dcp_metadata_filename_format.specification()); /* [XML] DCPAssetFilenameFormat Format for DCP asset filenames. */ - root->add_child("DCPAssetFilenameFormat")->add_child_text (_dcp_asset_filename_format.specification ()); + cxml::add_text_child(root, "DCPAssetFilenameFormat", _dcp_asset_filename_format.specification()); /* [XML] JumpToSelected 1 to make the GUI jump to the start of content when it is selected, otherwise 0. */ - root->add_child("JumpToSelected")->add_child_text (_jump_to_selected ? "1" : "0"); + cxml::add_text_child(root, "JumpToSelected", _jump_to_selected ? "1" : "0"); /* [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)); + auto e = cxml::add_child(root, "Nagged"); + 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. */ - root->add_child("PreviewSound")->add_child_text (_sound ? "1" : "0"); + cxml::add_text_child(root, "PreviewSound", _sound ? "1" : "0"); if (_sound_output) { /* [XML:opt] PreviewSoundOutput Name of the audio output to use. */ - root->add_child("PreviewSoundOutput")->add_child_text (_sound_output.get()); + cxml::add_text_child(root, "PreviewSoundOutput", _sound_output.get()); } /* [XML] CoverSheet Text of the cover sheet to write when making DCPs. */ - root->add_child("CoverSheet")->add_child_text (_cover_sheet); + cxml::add_text_child(root, "CoverSheet", _cover_sheet); if (_last_player_load_directory) { - root->add_child("LastPlayerLoadDirectory")->add_child_text(_last_player_load_directory->string()); + cxml::add_text_child(root, "LastPlayerLoadDirectory", _last_player_load_directory->string()); } /* [XML] LastKDMWriteType Last type of KDM-write: <code>flat</code> for a flat file, <code>folder</code> for a folder or <code>zip</code> for a ZIP file. */ if (_last_kdm_write_type) { switch (_last_kdm_write_type.get()) { case KDM_WRITE_FLAT: - root->add_child("LastKDMWriteType")->add_child_text("flat"); + cxml::add_text_child(root, "LastKDMWriteType", "flat"); break; case KDM_WRITE_FOLDER: - root->add_child("LastKDMWriteType")->add_child_text("folder"); + cxml::add_text_child(root, "LastKDMWriteType", "folder"); break; case KDM_WRITE_ZIP: - root->add_child("LastKDMWriteType")->add_child_text("zip"); + cxml::add_text_child(root, "LastKDMWriteType", "zip"); break; } } @@ -996,58 +1011,58 @@ Config::write_config () const if (_last_dkdm_write_type) { switch (_last_dkdm_write_type.get()) { case DKDM_WRITE_INTERNAL: - root->add_child("LastDKDMWriteType")->add_child_text("internal"); + cxml::add_text_child(root, "LastDKDMWriteType", "internal"); break; case DKDM_WRITE_FILE: - root->add_child("LastDKDMWriteType")->add_child_text("file"); + cxml::add_text_child(root, "LastDKDMWriteType", "file"); break; } } /* [XML] FramesInMemoryMultiplier value to multiply the encoding threads count by to get the maximum number of frames to be held in memory at once. */ - root->add_child("FramesInMemoryMultiplier")->add_child_text(raw_convert<string>(_frames_in_memory_multiplier)); + cxml::add_text_child(root, "FramesInMemoryMultiplier", raw_convert<string>(_frames_in_memory_multiplier)); /* [XML] DecodeReduction power of 2 to reduce DCP images by before decoding in the player. */ if (_decode_reduction) { - root->add_child("DecodeReduction")->add_child_text(raw_convert<string>(_decode_reduction.get())); + cxml::add_text_child(root, "DecodeReduction", raw_convert<string>(_decode_reduction.get())); } /* [XML] DefaultNotify 1 to default jobs to notify when complete, otherwise 0. */ - root->add_child("DefaultNotify")->add_child_text(_default_notify ? "1" : "0"); + cxml::add_text_child(root, "DefaultNotify", _default_notify ? "1" : "0"); /* [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)); + auto e = cxml::add_child(root, "Notification"); + e->set_attribute ("id", raw_convert<string>(i)); e->add_child_text (_notification[i] ? "1" : "0"); } if (_barco_username) { /* [XML] BarcoUsername Username for logging into Barco's servers when downloading server certificates. */ - root->add_child("BarcoUsername")->add_child_text(*_barco_username); + cxml::add_text_child(root, "BarcoUsername", *_barco_username); } if (_barco_password) { /* [XML] BarcoPassword Password for logging into Barco's servers when downloading server certificates. */ - root->add_child("BarcoPassword")->add_child_text(*_barco_password); + cxml::add_text_child(root, "BarcoPassword", *_barco_password); } if (_christie_username) { /* [XML] ChristieUsername Username for logging into Christie's servers when downloading server certificates. */ - root->add_child("ChristieUsername")->add_child_text(*_christie_username); + cxml::add_text_child(root, "ChristieUsername", *_christie_username); } if (_christie_password) { /* [XML] ChristiePassword Password for logging into Christie's servers when downloading server certificates. */ - root->add_child("ChristiePassword")->add_child_text(*_christie_password); + cxml::add_text_child(root, "ChristiePassword", *_christie_password); } if (_gdc_username) { /* [XML] GDCUsername Username for logging into GDC's servers when downloading server certificates. */ - root->add_child("GDCUsername")->add_child_text(*_gdc_username); + cxml::add_text_child(root, "GDCUsername", *_gdc_username); } if (_gdc_password) { /* [XML] GDCPassword Password for logging into GDC's servers when downloading server certificates. */ - root->add_child("GDCPassword")->add_child_text(*_gdc_password); + cxml::add_text_child(root, "GDCPassword", *_gdc_password); } /* [XML] PlayerMode <code>window</code> for a single window, <code>full</code> for full-screen and <code>dual</code> for full screen playback @@ -1055,80 +1070,94 @@ Config::write_config () const */ switch (_player_mode) { case PLAYER_MODE_WINDOW: - root->add_child("PlayerMode")->add_child_text("window"); + cxml::add_text_child(root, "PlayerMode", "window"); break; case PLAYER_MODE_FULL: - root->add_child("PlayerMode")->add_child_text("full"); + cxml::add_text_child(root, "PlayerMode", "full"); break; case PLAYER_MODE_DUAL: - root->add_child("PlayerMode")->add_child_text("dual"); + cxml::add_text_child(root, "PlayerMode", "dual"); break; } + if (_player_restricted_menus) { + cxml::add_text_child(root, "PlayerRestrictedMenus", "1"); + } + + if (_playlist_editor_restricted_menus) { + cxml::add_text_child(root, "PlaylistEditorRestrictedMenus", "1"); + } + /* [XML] ImageDisplay Screen number to put image on in dual-screen player mode. */ - root->add_child("ImageDisplay")->add_child_text(raw_convert<string>(_image_display)); + cxml::add_text_child(root, "ImageDisplay", raw_convert<string>(_image_display)); switch (_video_view_type) { case VIDEO_VIEW_SIMPLE: - root->add_child("VideoViewType")->add_child_text("simple"); + cxml::add_text_child(root, "VideoViewType", "simple"); break; case VIDEO_VIEW_OPENGL: - root->add_child("VideoViewType")->add_child_text("opengl"); + cxml::add_text_child(root, "VideoViewType", "opengl"); break; } /* [XML] RespectKDMValidityPeriods 1 to refuse to use KDMs that are out of date, 0 to ignore KDM dates. */ - root->add_child("RespectKDMValidityPeriods")->add_child_text(_respect_kdm_validity_periods ? "1" : "0"); + cxml::add_text_child(root, "RespectKDMValidityPeriods", _respect_kdm_validity_periods ? "1" : "0"); if (_player_debug_log_file) { /* [XML] PlayerLogFile Filename to use for player debug logs. */ - root->add_child("PlayerDebugLogFile")->add_child_text(_player_debug_log_file->string()); + cxml::add_text_child(root, "PlayerDebugLogFile", _player_debug_log_file->string()); } if (_player_content_directory) { /* [XML] PlayerContentDirectory Directory to use for player content in the dual-screen mode. */ - root->add_child("PlayerContentDirectory")->add_child_text(_player_content_directory->string()); + cxml::add_text_child(root, "PlayerContentDirectory", _player_content_directory->string()); } if (_player_playlist_directory) { /* [XML] PlayerPlaylistDirectory Directory to use for player playlists in the dual-screen mode. */ - root->add_child("PlayerPlaylistDirectory")->add_child_text(_player_playlist_directory->string()); + cxml::add_text_child(root, "PlayerPlaylistDirectory", _player_playlist_directory->string()); } if (_player_kdm_directory) { /* [XML] PlayerKDMDirectory Directory to use for player KDMs in the dual-screen mode. */ - root->add_child("PlayerKDMDirectory")->add_child_text(_player_kdm_directory->string()); + cxml::add_text_child(root, "PlayerKDMDirectory", _player_kdm_directory->string()); } if (_audio_mapping) { - _audio_mapping->as_xml (root->add_child("AudioMapping")); + _audio_mapping->as_xml(cxml::add_child(root, "AudioMapping")); } for (auto const& i: _custom_languages) { - root->add_child("CustomLanguage")->add_child_text(i.to_string()); + cxml::add_text_child(root, "CustomLanguage", i.to_string()); } for (auto const& initial: _initial_paths) { if (initial.second) { - root->add_child(initial.first)->add_child_text(initial.second->string()); + cxml::add_text_child(root, initial.first, initial.second->string()); } } - root->add_child("UseISDCFNameByDefault")->add_child_text(_use_isdcf_name_by_default ? "1" : "0"); - root->add_child("WriteKDMsToDisk")->add_child_text(_write_kdms_to_disk ? "1" : "0"); - root->add_child("EmailKDMs")->add_child_text(_email_kdms ? "1" : "0"); - root->add_child("DefaultKDMType")->add_child_text(dcp::formulation_to_string(_default_kdm_type)); - root->add_child("AutoCropThreshold")->add_child_text(raw_convert<string>(_auto_crop_threshold)); + cxml::add_text_child(root, "UseISDCFNameByDefault", _use_isdcf_name_by_default ? "1" : "0"); + cxml::add_text_child(root, "WriteKDMsToDisk", _write_kdms_to_disk ? "1" : "0"); + cxml::add_text_child(root, "EmailKDMs", _email_kdms ? "1" : "0"); + cxml::add_text_child(root, "DefaultKDMType", dcp::formulation_to_string(_default_kdm_type)); + cxml::add_text_child(root, "AutoCropThreshold", raw_convert<string>(_auto_crop_threshold)); if (_last_release_notes_version) { - root->add_child("LastReleaseNotesVersion")->add_child_text(*_last_release_notes_version); + cxml::add_text_child(root, "LastReleaseNotesVersion", *_last_release_notes_version); } if (_main_divider_sash_position) { - root->add_child("MainDividerSashPosition")->add_child_text(raw_convert<string>(*_main_divider_sash_position)); + cxml::add_text_child(root, "MainDividerSashPosition", raw_convert<string>(*_main_divider_sash_position)); } if (_main_content_divider_sash_position) { - root->add_child("MainContentDividerSashPosition")->add_child_text(raw_convert<string>(*_main_content_divider_sash_position)); + cxml::add_text_child(root, "MainContentDividerSashPosition", raw_convert<string>(*_main_content_divider_sash_position)); } - root->add_child("DefaultAddFileLocation")->add_child_text( + cxml::add_text_child(root, "DefaultAddFileLocation", _default_add_file_location == DefaultAddFileLocation::SAME_AS_LAST_TIME ? "last" : "project" ); /* [XML] AllowSMPTEBv20 1 to allow the user to choose SMPTE (Bv2.0 only) as a standard, otherwise 0 */ - root->add_child("AllowSMPTEBv20")->add_child_text(_allow_smpte_bv20 ? "1" : "0"); + cxml::add_text_child(root, "AllowSMPTEBv20", _allow_smpte_bv20 ? "1" : "0"); /* [XML] ISDCFNamePartLength Maximum length of the "name" part of an ISDCF name, which should be 14 according to the standard */ - root->add_child("ISDCFNamePartLength")->add_child_text(raw_convert<string>(_isdcf_name_part_length)); + cxml::add_text_child(root, "ISDCFNamePartLength", raw_convert<string>(_isdcf_name_part_length)); - _export.write(root->add_child("Export")); +#ifdef DCPOMATIC_GROK + if (_grok) { + _grok->as_xml(cxml::add_child(root, "Grok")); + } +#endif + + _export.write(cxml::add_child(root, "Export")); auto target = config_write_file(); @@ -1157,10 +1186,10 @@ write_file (string root_node, string node, string version, list<shared_ptr<T>> t { xmlpp::Document doc; auto root = doc.create_root_node (root_node); - root->add_child("Version")->add_child_text(version); + cxml::add_text_child(root, "Version", version); for (auto i: things) { - i->as_xml (root->add_child(node)); + i->as_xml(cxml::add_child(root, node)); } try { @@ -1500,7 +1529,7 @@ void Config::link (boost::filesystem::path new_file) const { xmlpp::Document doc; - doc.create_root_node("Config")->add_child("Link")->add_child_text(new_file.string()); + cxml::add_text_child(doc.create_root_node("Config"), "Link", new_file.string()); try { doc.write_to_file_formatted(write_path("config.xml").string()); } catch (xmlpp::exception& e) { @@ -1685,3 +1714,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..ec804c9bf 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 }; @@ -531,6 +534,14 @@ public: return _player_mode; } + bool player_restricted_menus() const { + return _player_restricted_menus; + } + + bool playlist_editor_restricted_menus() const { + return _playlist_editor_restricted_menus; + } + int image_display () const { return _image_display; } @@ -621,6 +632,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 +1235,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 @@ -1419,6 +1457,8 @@ private: boost::optional<std::string> _gdc_username; boost::optional<std::string> _gdc_password; PlayerMode _player_mode; + bool _player_restricted_menus = false; + bool _playlist_editor_restricted_menus = false; int _image_display; VideoViewType _video_view_type; bool _respect_kdm_validity_periods; @@ -1447,6 +1487,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/constants.h b/src/lib/constants.h index 3b1871554..bfe144420 100644 --- a/src/lib/constants.h +++ b/src/lib/constants.h @@ -47,6 +47,7 @@ #define MAX_CLOSED_CAPTION_XML_SIZE (256 * 1024) #define MAX_CLOSED_CAPTION_XML_SIZE_TEXT "256KB" #define CERTIFICATE_VALIDITY_PERIOD (10 * 365) +#define SNAP_SUBDIVISION 64 #endif diff --git a/src/lib/content.cc b/src/lib/content.cc index 6324050ec..6743705d9 100644 --- a/src/lib/content.cc +++ b/src/lib/content.cc @@ -140,23 +140,23 @@ Content::Content (vector<shared_ptr<Content>> c) void -Content::as_xml (xmlpp::Node* node, bool with_paths) const +Content::as_xml(xmlpp::Element* element, bool with_paths) const { boost::mutex::scoped_lock lm (_mutex); if (with_paths) { for (size_t i = 0; i < _paths.size(); ++i) { - auto p = node->add_child("Path"); + auto p = cxml::add_child(element, "Path"); p->add_child_text (_paths[i].string()); p->set_attribute ("mtime", raw_convert<string>(_last_write_times[i])); } } - node->add_child("Digest")->add_child_text(_digest); - node->add_child("Position")->add_child_text(raw_convert<string>(_position.get())); - node->add_child("TrimStart")->add_child_text(raw_convert<string>(_trim_start.get())); - node->add_child("TrimEnd")->add_child_text(raw_convert<string>(_trim_end.get())); + cxml::add_text_child(element, "Digest", _digest); + cxml::add_text_child(element, "Position", raw_convert<string>(_position.get())); + cxml::add_text_child(element, "TrimStart", raw_convert<string>(_trim_start.get())); + cxml::add_text_child(element, "TrimEnd", raw_convert<string>(_trim_end.get())); if (_video_frame_rate) { - node->add_child("VideoFrameRate")->add_child_text(raw_convert<string>(_video_frame_rate.get())); + cxml::add_text_child(element, "VideoFrameRate", raw_convert<string>(_video_frame_rate.get())); } } diff --git a/src/lib/content.h b/src/lib/content.h index f2fecddf0..3753c84d7 100644 --- a/src/lib/content.h +++ b/src/lib/content.h @@ -100,7 +100,7 @@ public: */ virtual std::string technical_summary () const; - virtual void as_xml (xmlpp::Node *, bool with_paths) const; + virtual void as_xml(xmlpp::Element* element, bool with_paths) const; virtual dcpomatic::DCPTime full_length (std::shared_ptr<const Film>) const = 0; virtual dcpomatic::DCPTime approximate_length () const = 0; virtual std::string identifier () const; 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/crop.cc b/src/lib/crop.cc index d2d020a9f..a6f7d4bac 100644 --- a/src/lib/crop.cc +++ b/src/lib/crop.cc @@ -42,12 +42,12 @@ Crop::Crop(shared_ptr<cxml::Node> node) void -Crop::as_xml(xmlpp::Node* node) const +Crop::as_xml(xmlpp::Element* element) const { - node->add_child("LeftCrop")->add_child_text(raw_convert<string>(left)); - node->add_child("RightCrop")->add_child_text(raw_convert<string>(right)); - node->add_child("TopCrop")->add_child_text(raw_convert<string>(top)); - node->add_child("BottomCrop")->add_child_text(raw_convert<string>(bottom)); + cxml::add_text_child(element, "LeftCrop", raw_convert<string>(left)); + cxml::add_text_child(element, "RightCrop", raw_convert<string>(right)); + cxml::add_text_child(element, "TopCrop", raw_convert<string>(top)); + cxml::add_text_child(element, "BottomCrop", raw_convert<string>(bottom)); } diff --git a/src/lib/crop.h b/src/lib/crop.h index 00734d5e9..24330731d 100644 --- a/src/lib/crop.h +++ b/src/lib/crop.h @@ -65,7 +65,7 @@ struct Crop return s; } - void as_xml (xmlpp::Node *) const; + void as_xml(xmlpp::Element*) const; }; 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..1e145c8fc 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; @@ -62,16 +59,16 @@ Drive::as_xml () const { xmlpp::Document doc; auto root = doc.create_root_node ("Drive"); - root->add_child("Device")->add_child_text(_device); + cxml::add_text_child(root, "Device", _device); for (auto i: _mount_points) { - root->add_child("MountPoint")->add_child_text(i.string()); + cxml::add_text_child(root, "MountPoint", i.string()); } - root->add_child("Size")->add_child_text(dcp::raw_convert<string>(_size)); + cxml::add_text_child(root, "Size", dcp::raw_convert<string>(_size)); if (_vendor) { - root->add_child("Vendor")->add_child_text(*_vendor); + cxml::add_text_child(root, "Vendor", *_vendor); } if (_model) { - root->add_child("Model")->add_child_text(*_model); + cxml::add_text_child(root, "Model", *_model); } return doc.write_to_string("UTF-8"); @@ -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 bdd5e0e09..e9afa04cc 100644 --- a/src/lib/dcp_content.cc +++ b/src/lib/dcp_content.cc @@ -163,6 +163,13 @@ DCPContent::DCPContent (cxml::ConstNodePtr node, int version) } _active_audio_channels = node->optional_number_child<int>("ActiveAudioChannels"); + + for (auto non_zero: node->node_children("HasNonZeroEntryPoint")) { + try { + auto type = string_to_text_type(non_zero->string_attribute("type")); + _has_non_zero_entry_point[type] = non_zero->content() == "1"; + } catch (MetadataError&) {} + } } void @@ -308,6 +315,7 @@ DCPContent::examine (shared_ptr<const Film> film, shared_ptr<Job> job) } _ratings = examiner->ratings (); _content_versions = examiner->content_versions (); + _has_non_zero_entry_point = examiner->has_non_zero_entry_point(); } if (needed_assets == needs_assets()) { @@ -347,88 +355,98 @@ DCPContent::technical_summary () const return s; } + void -DCPContent::as_xml (xmlpp::Node* node, bool with_paths) const +DCPContent::as_xml(xmlpp::Element* element, bool with_paths) const { - node->add_child("Type")->add_child_text ("DCP"); + cxml::add_text_child(element, "Type", "DCP"); - Content::as_xml (node, with_paths); + Content::as_xml(element, with_paths); if (video) { - video->as_xml (node); + video->as_xml(element); } if (audio) { - audio->as_xml (node); - node->add_child("AudioFrameRate")->add_child_text (raw_convert<string> (audio->stream()->frame_rate())); - node->add_child("AudioLength")->add_child_text (raw_convert<string> (audio->stream()->length())); - audio->stream()->mapping().as_xml (node->add_child("AudioMapping")); + audio->as_xml(element); + cxml::add_text_child(element, "AudioFrameRate", raw_convert<string>(audio->stream()->frame_rate())); + cxml::add_text_child(element, "AudioLength", raw_convert<string>(audio->stream()->length())); + audio->stream()->mapping().as_xml(cxml::add_child(element, "AudioMapping")); } for (auto i: text) { - i->as_xml (node); + i->as_xml(element); } if (atmos) { - atmos->as_xml (node); + atmos->as_xml(element); } boost::mutex::scoped_lock lm (_mutex); - node->add_child("Name")->add_child_text (_name); - node->add_child("Encrypted")->add_child_text (_encrypted ? "1" : "0"); - node->add_child("NeedsAssets")->add_child_text (_needs_assets ? "1" : "0"); + cxml::add_text_child(element, "Name", _name); + cxml::add_text_child(element, "Encrypted", _encrypted ? "1" : "0"); + cxml::add_text_child(element, "NeedsAssets", _needs_assets ? "1" : "0"); if (_kdm) { - node->add_child("KDM")->add_child_text (_kdm->as_xml ()); + cxml::add_text_child(element, "KDM", _kdm->as_xml()); } - node->add_child("KDMValid")->add_child_text (_kdm_valid ? "1" : "0"); - node->add_child("ReferenceVideo")->add_child_text (_reference_video ? "1" : "0"); - node->add_child("ReferenceAudio")->add_child_text (_reference_audio ? "1" : "0"); - node->add_child("ReferenceOpenSubtitle")->add_child_text(_reference_text[TextType::OPEN_SUBTITLE] ? "1" : "0"); - node->add_child("ReferenceClosedCaption")->add_child_text(_reference_text[TextType::CLOSED_CAPTION] ? "1" : "0"); + cxml::add_text_child(element, "KDMValid", _kdm_valid ? "1" : "0"); + cxml::add_text_child(element, "ReferenceVideo", _reference_video ? "1" : "0"); + cxml::add_text_child(element, "ReferenceAudio", _reference_audio ? "1" : "0"); + cxml::add_text_child(element, "ReferenceOpenSubtitle", _reference_text[TextType::OPEN_SUBTITLE] ? "1" : "0"); + cxml::add_text_child(element, "ReferenceClosedCaption", _reference_text[TextType::CLOSED_CAPTION] ? "1" : "0"); if (_standard) { switch (_standard.get ()) { case dcp::Standard::INTEROP: - node->add_child("Standard")->add_child_text ("Interop"); + cxml::add_text_child(element, "Standard", "Interop"); break; case dcp::Standard::SMPTE: - node->add_child("Standard")->add_child_text ("SMPTE"); + cxml::add_text_child(element, "Standard", "SMPTE"); break; default: DCPOMATIC_ASSERT (false); } } - node->add_child("ThreeD")->add_child_text (_three_d ? "1" : "0"); + cxml::add_text_child(element, "ThreeD", _three_d ? "1" : "0"); if (_content_kind) { - node->add_child("ContentKind")->add_child_text(_content_kind->name()); + cxml::add_text_child(element, "ContentKind", _content_kind->name()); } if (_cpl) { - node->add_child("CPL")->add_child_text (_cpl.get ()); + cxml::add_text_child(element, "CPL", _cpl.get()); } for (auto i: _reel_lengths) { - node->add_child("ReelLength")->add_child_text (raw_convert<string> (i)); + cxml::add_text_child(element, "ReelLength", raw_convert<string>(i)); } for (auto const& i: _markers) { - auto marker = node->add_child("Marker"); + auto marker = cxml::add_child(element, "Marker"); marker->set_attribute("type", dcp::marker_to_string(i.first)); marker->add_child_text(raw_convert<string>(i.second.get())); } for (auto i: _ratings) { - auto rating = node->add_child("Rating"); + auto rating = cxml::add_child(element, "Rating"); i.as_xml (rating); } for (auto i: _content_versions) { - node->add_child("ContentVersion")->add_child_text(i); + cxml::add_text_child(element, "ContentVersion", i); } if (_active_audio_channels) { - node->add_child("ActiveAudioChannels")->add_child_text(raw_convert<string>(*_active_audio_channels)); + cxml::add_text_child(element, "ActiveAudioChannels", raw_convert<string>(*_active_audio_channels)); + } + + for (auto i = 0; i < static_cast<int>(TextType::COUNT); ++i) { + if (_has_non_zero_entry_point[i]) { + auto has = cxml::add_child(element, "HasNonZeroEntryPoint"); + has->add_child_text("1"); + has->set_attribute("type", text_type_to_string(static_cast<TextType>(i))); + } } } + DCPTime DCPContent::full_length (shared_ptr<const Film> film) const { @@ -612,7 +630,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 +676,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,35 +710,23 @@ 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); } bool DCPContent::can_reference_audio (shared_ptr<const Film> film, string& why_not) const { - shared_ptr<DCPDecoder> decoder; - try { - decoder = make_shared<DCPDecoder>(film, shared_from_this(), false, film->tolerant(), shared_ptr<DCPDecoder>()); - } catch (dcp::ReadError &) { - /* We couldn't read the DCP, so it's probably missing */ - return false; - } catch (DCPError &) { - /* We couldn't read the DCP, so it's probably missing */ - return false; - } catch (dcp::KDMDecryptionError &) { - /* We have an incorrect KDM */ - return false; - } - if (audio && audio->stream()) { auto const channels = audio->stream()->channels(); if (channels != film->audio_channels()) { @@ -728,51 +735,33 @@ DCPContent::can_reference_audio (shared_ptr<const Film> film, string& why_not) c } } - /// 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); } bool DCPContent::can_reference_text (shared_ptr<const Film> film, TextType type, string& why_not) const { - shared_ptr<DCPDecoder> decoder; - try { - decoder = make_shared<DCPDecoder>(film, shared_from_this(), false, film->tolerant(), shared_ptr<DCPDecoder>()); - } catch (dcp::ReadError &) { - /* We couldn't read the DCP, so it's probably missing */ - return false; - } catch (DCPError &) { - /* We couldn't read the DCP, so it's probably missing */ - return false; - } catch (dcp::KDMDecryptionError &) { - /* We have an incorrect KDM */ + if (_has_non_zero_entry_point[TextType::OPEN_SUBTITLE]) { + /// TRANSLATORS: this string will follow "Cannot reference this DCP: " + why_not = _("one of its subtitle reels has a non-zero entry point so it must be re-written."); return false; } - for (auto i: decoder->reels()) { - if (type == TextType::OPEN_SUBTITLE) { - if (i->main_subtitle() && i->main_subtitle()->entry_point().get_value_or(0) != 0) { - /// TRANSLATORS: this string will follow "Cannot reference this DCP: " - why_not = _("one of its subtitle reels has a non-zero entry point so it must be re-written."); - return false; - } - } - if (type == TextType::CLOSED_CAPTION) { - for (auto j: i->closed_captions()) { - if (j->entry_point().get_value_or(0) != 0) { - /// TRANSLATORS: this string will follow "Cannot reference this DCP: " - why_not = _("one of its closed caption has a non-zero entry point so it must be re-written."); - return false; - } - } - } + if (_has_non_zero_entry_point[TextType::CLOSED_CAPTION]) { + /// TRANSLATORS: this string will follow "Cannot reference this DCP: " + why_not = _("one of its closed caption has a non-zero entry point so it must be re-written."); + return false; } if (trim_start() != dcpomatic::ContentTime()) { @@ -781,15 +770,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..995e0b681 100644 --- a/src/lib/dcp_content.h +++ b/src/lib/dcp_content.h @@ -75,7 +75,7 @@ public: void examine (std::shared_ptr<const Film> film, std::shared_ptr<Job>) override; std::string summary () const override; std::string technical_summary () const override; - void as_xml (xmlpp::Node *, bool with_paths) const override; + void as_xml(xmlpp::Element*, bool with_paths) const override; std::string identifier () const override; void take_settings_from (std::shared_ptr<const Content> c) override; @@ -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 */ @@ -227,6 +224,7 @@ private: std::map<dcp::Marker, dcpomatic::ContentTime> _markers; std::vector<dcp::Rating> _ratings; std::vector<std::string> _content_versions; + EnumIndexedVector<bool, TextType> _has_non_zero_entry_point; boost::optional<int> _active_audio_channels; }; diff --git a/src/lib/dcp_content_type.cc b/src/lib/dcp_content_type.cc index bc9bf0d67..d689f138c 100644 --- a/src/lib/dcp_content_type.cc +++ b/src/lib/dcp_content_type.cc @@ -28,8 +28,9 @@ #include "i18n.h" +using std::string; +using std::vector; using boost::optional; -using namespace std; vector<DCPContentType> DCPContentType::_dcp_content_types; 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_digest_file.cc b/src/lib/dcp_digest_file.cc index eb243b60d..4278caa98 100644 --- a/src/lib/dcp_digest_file.cc +++ b/src/lib/dcp_digest_file.cc @@ -42,14 +42,14 @@ template <class R, class A> void add_asset(string film_key, shared_ptr<R> reel_asset, shared_ptr<A> asset, xmlpp::Element* reel, string name) { if (asset) { - auto out = reel->add_child(name); - out->add_child("Id")->add_child_text("urn:uuid:" + asset->id()); + auto out = cxml::add_child(reel, name); + cxml::add_text_child(out, "Id", "urn:uuid:" + asset->id()); if (reel_asset->annotation_text()) { - out->add_child("AnnotationText")->add_child_text(reel_asset->annotation_text().get()); + cxml::add_text_child(out, "AnnotationText", reel_asset->annotation_text().get()); } if (asset->key_id()) { - out->add_child("KeyId")->add_child_text("urn:uuid:" + asset->key_id().get()); - out->add_child("Key")->add_child_text(asset->key() ? asset->key()->hex() : film_key); + cxml::add_text_child(out, "KeyId", "urn:uuid:" + asset->key_id().get()); + cxml::add_text_child(out, "Key", asset->key() ? asset->key()->hex() : film_key); } } }; @@ -64,16 +64,16 @@ write_dcp_digest_file ( { xmlpp::Document doc; auto root = doc.create_root_node("FHG_DCP_DIGEST", "http://www.fhg.de/2009/04/02/dcpdig"); - root->add_child("InteropMode")->add_child_text(cpl->standard() == dcp::Standard::INTEROP ? "true" : "false"); - auto composition = root->add_child("CompositionList")->add_child("Composition"); - composition->add_child("Id")->add_child_text("urn:uuid:" + cpl->id()); - composition->add_child("AnnotationText")->add_child_text(cpl->annotation_text().get_value_or("")); - composition->add_child("ContentTitleText")->add_child_text(cpl->content_title_text()); - auto reel_list = composition->add_child("ReelList"); + cxml::add_text_child(root, "InteropMode", cpl->standard() == dcp::Standard::INTEROP ? "true" : "false"); + auto composition = cxml::add_child(cxml::add_child(root, "CompositionList"), "Composition"); + cxml::add_text_child(composition, "Id", "urn:uuid:" + cpl->id()); + cxml::add_text_child(composition, "AnnotationText", cpl->annotation_text().get_value_or("")); + cxml::add_text_child(composition, "ContentTitleText", cpl->content_title_text()); + auto reel_list = cxml::add_child(composition, "ReelList"); for (auto in_reel: cpl->reels()) { - auto out_reel = reel_list->add_child("Reel"); - out_reel->add_child("Id")->add_child_text("urn:uuid:" + in_reel->id()); - out_reel->add_child("AnnotationText"); + auto out_reel = cxml::add_child(reel_list, "Reel"); + cxml::add_text_child(out_reel, "Id", "urn:uuid:" + in_reel->id()); + cxml::add_child(out_reel, "AnnotationText"); if (in_reel->main_picture()) { add_asset(film_key, in_reel->main_picture(), in_reel->main_picture()->asset(), out_reel, "MainPicture"); } 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_examiner.cc b/src/lib/dcp_examiner.cc index 88c9a5b70..3515f34c2 100644 --- a/src/lib/dcp_examiner.cc +++ b/src/lib/dcp_examiner.cc @@ -195,17 +195,20 @@ DCPExaminer::DCPExaminer (shared_ptr<const DCPContent> content, bool tolerant) } } - if (reel->main_subtitle()) { - if (!reel->main_subtitle()->asset_ref().resolved()) { - LOG_GENERAL("Main subtitle %1 of reel %2 is missing", reel->main_subtitle()->id(), reel->id()); + if (auto sub = reel->main_subtitle()) { + if (sub->entry_point().get_value_or(0) != 0) { + _has_non_zero_entry_point[TextType::OPEN_SUBTITLE] = true; + } + if (!sub->asset_ref().resolved()) { + LOG_GENERAL("Main subtitle %1 of reel %2 is missing", sub->id(), reel->id()); _needs_assets = true; } else { - LOG_GENERAL("Main subtitle %1 of reel %2 found", reel->main_subtitle()->id(), reel->id()); + LOG_GENERAL("Main subtitle %1 of reel %2 found", sub->id(), reel->id()); _text_count[TextType::OPEN_SUBTITLE] = 1; - _open_subtitle_language = try_to_parse_language(reel->main_subtitle()->language()); + _open_subtitle_language = try_to_parse_language(sub->language()); - auto asset = reel->main_subtitle()->asset(); + auto asset = sub->asset(); for (auto const& font: asset->font_data()) { _fonts.push_back({reel_index, asset->id(), make_shared<dcpomatic::Font>(font.first, font.second)}); } @@ -225,6 +228,9 @@ DCPExaminer::DCPExaminer (shared_ptr<const DCPContent> content, bool tolerant) } for (auto ccap: reel->closed_captions()) { + if (ccap->entry_point().get_value_or(0) != 0) { + _has_non_zero_entry_point[TextType::CLOSED_CAPTION] = true; + } if (!ccap->asset_ref().resolved()) { LOG_GENERAL("Closed caption %1 of reel %2 is missing", ccap->id(), reel->id()); _needs_assets = true; diff --git a/src/lib/dcp_examiner.h b/src/lib/dcp_examiner.h index 04fa31ea4..425552632 100644 --- a/src/lib/dcp_examiner.h +++ b/src/lib/dcp_examiner.h @@ -171,6 +171,10 @@ public: return _atmos_edit_rate; } + EnumIndexedVector<bool, TextType> has_non_zero_entry_point() const { + return _has_non_zero_entry_point; + } + void add_fonts(std::shared_ptr<TextContent> content); private: @@ -206,6 +210,7 @@ private: bool _has_atmos = false; Frame _atmos_length = 0; dcp::Fraction _atmos_edit_rate; + EnumIndexedVector<bool, TextType> _has_non_zero_entry_point; struct Font { diff --git a/src/lib/dcp_subtitle_content.cc b/src/lib/dcp_subtitle_content.cc index 8de5967ef..9ca3750d1 100644 --- a/src/lib/dcp_subtitle_content.cc +++ b/src/lib/dcp_subtitle_content.cc @@ -137,14 +137,14 @@ DCPSubtitleContent::technical_summary () const } void -DCPSubtitleContent::as_xml (xmlpp::Node* node, bool with_paths) const +DCPSubtitleContent::as_xml(xmlpp::Element* element, bool with_paths) const { - node->add_child("Type")->add_child_text ("DCPSubtitle"); - Content::as_xml (node, with_paths); + cxml::add_text_child(element, "Type", "DCPSubtitle"); + Content::as_xml(element, with_paths); if (only_text()) { - only_text()->as_xml (node); + only_text()->as_xml(element); } - node->add_child("Length")->add_child_text (raw_convert<string> (_length.get ())); + cxml::add_text_child(element, "Length", raw_convert<string>(_length.get())); } diff --git a/src/lib/dcp_subtitle_content.h b/src/lib/dcp_subtitle_content.h index 89a6f26a2..068b6dbac 100644 --- a/src/lib/dcp_subtitle_content.h +++ b/src/lib/dcp_subtitle_content.h @@ -30,7 +30,7 @@ public: void examine (std::shared_ptr<const Film> film, std::shared_ptr<Job>) override; std::string summary () const override; std::string technical_summary () const override; - void as_xml (xmlpp::Node *, bool with_paths) const override; + void as_xml(xmlpp::Element*, bool with_paths) const override; dcpomatic::DCPTime full_length (std::shared_ptr<const Film> film) const override; dcpomatic::DCPTime approximate_length () const override; diff --git a/src/lib/dcp_text_track.cc b/src/lib/dcp_text_track.cc index 0bd751275..292932396 100644 --- a/src/lib/dcp_text_track.cc +++ b/src/lib/dcp_text_track.cc @@ -56,9 +56,9 @@ DCPTextTrack::summary () const void DCPTextTrack::as_xml (xmlpp::Element* parent) const { - parent->add_child("Name")->add_child_text(name); + cxml::add_text_child(parent, "Name", name); if (language) { - parent->add_child("Language")->add_child_text(language->to_string()); + cxml::add_text_child(parent, "Language", language->to_string()); } } diff --git a/src/lib/dcp_video.cc b/src/lib/dcp_video.cc index 217b72183..6580ac448 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. */ @@ -215,7 +239,7 @@ DCPVideo::encode_remotely (EncodeServerDescription serv, int timeout) const /* Collect all XML metadata */ xmlpp::Document doc; auto root = doc.create_root_node ("EncodingRequest"); - root->add_child("Version")->add_child_text (raw_convert<string> (SERVER_LINK_VERSION)); + cxml::add_text_child(root, "Version", raw_convert<string>(SERVER_LINK_VERSION)); add_metadata (root); LOG_DEBUG_ENCODE (N_("Sending frame %1 to remote"), _index); @@ -254,10 +278,10 @@ DCPVideo::encode_remotely (EncodeServerDescription serv, int timeout) const void DCPVideo::add_metadata (xmlpp::Element* el) const { - el->add_child("Index")->add_child_text (raw_convert<string> (_index)); - el->add_child("FramesPerSecond")->add_child_text (raw_convert<string> (_frames_per_second)); - el->add_child("J2KBandwidth")->add_child_text (raw_convert<string> (_j2k_bandwidth)); - el->add_child("Resolution")->add_child_text (raw_convert<string> (int (_resolution))); + cxml::add_text_child(el, "Index", raw_convert<string>(_index)); + cxml::add_text_child(el, "FramesPerSecond", raw_convert<string>(_frames_per_second)); + cxml::add_text_child(el, "J2KBandwidth", raw_convert<string>(_j2k_bandwidth)); + cxml::add_text_child(el, "Resolution", raw_convert<string>(int(_resolution))); _frame->add_metadata (el); } 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.cc b/src/lib/dcpomatic_time.cc index ac797f8f4..60fc5342a 100644 --- a/src/lib/dcpomatic_time.cc +++ b/src/lib/dcpomatic_time.cc @@ -27,6 +27,25 @@ using std::string; using namespace dcpomatic; +bool +dcpomatic::operator<=(HMSF const& a, HMSF const& b) +{ + if (a.h != b.h) { + return a.h <= b.h; + } + + if (a.m != b.m) { + return a.m <= b.m; + } + + if (a.s != b.s) { + return a.s <= b.s; + } + + return a.f <= b.f; +} + + template <> Time<ContentTimeDifferentiator, DCPTimeDifferentiator>::Time (DCPTime d, FrameRateChange f) : _t (llrint(d.get() * f.speed_up)) diff --git a/src/lib/dcpomatic_time.h b/src/lib/dcpomatic_time.h index 1b12ea901..63bb86549 100644 --- a/src/lib/dcpomatic_time.h +++ b/src/lib/dcpomatic_time.h @@ -64,6 +64,9 @@ public: }; +bool operator<=(HMSF const& a, HMSF const& b); + + /** A time in seconds, expressed as a number scaled up by Time::HZ. We want two different * versions of this class, dcpomatic::ContentTime and dcpomatic::DCPTime, and we want it to be impossible to * convert implicitly between the two. Hence there's this template hack. I'm not @@ -152,6 +155,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..4e56a37c4 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"); } @@ -52,11 +48,8 @@ DKDMRecipient::as_xml (xmlpp::Element* node) const KDMRecipient::as_xml (node); for (auto i: emails) { - node->add_child("Email")->add_child_text(i); + cxml::add_text_child(node, "Email", 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..4c7838a8d 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); @@ -62,17 +66,17 @@ DKDM::name () const void -DKDM::as_xml (xmlpp::Element* node) const +DKDM::as_xml(xmlpp::Element* element) const { - node->add_child("DKDM")->add_child_text (_dkdm.as_xml ()); + cxml::add_text_child(element, "DKDM", _dkdm.as_xml()); } void -DKDMGroup::as_xml (xmlpp::Element* node) const +DKDMGroup::as_xml(xmlpp::Element* element) const { - auto f = node->add_child("DKDMGroup"); - f->set_attribute ("Name", _name); + auto f = cxml::add_child(element, "DKDMGroup"); + 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..b9c907e05 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 (); } @@ -298,8 +301,8 @@ EncodeServer::broadcast_received () /* Reply to the client saying what we can do */ xmlpp::Document doc; auto root = doc.create_root_node ("ServerAvailable"); - root->add_child("Threads")->add_child_text (raw_convert<string> (_worker_threads.size ())); - root->add_child("Version")->add_child_text (raw_convert<string> (SERVER_LINK_VERSION)); + cxml::add_text_child(root, "Threads", raw_convert<string>(_worker_threads.size())); + cxml::add_text_child(root, "Version", raw_convert<string>(SERVER_LINK_VERSION)); auto xml = doc.write_to_string ("UTF-8"); if (_verbose) { 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/export_config.cc b/src/lib/export_config.cc index e030b98e2..4dfbb9027 100644 --- a/src/lib/export_config.cc +++ b/src/lib/export_config.cc @@ -77,7 +77,7 @@ ExportConfig::read(cxml::ConstNodePtr node) void -ExportConfig::write(xmlpp::Element* node) const +ExportConfig::write(xmlpp::Element* element) const { string name; @@ -97,11 +97,11 @@ ExportConfig::write(xmlpp::Element* node) const break; } - node->add_child("Format")->add_child_text(name); - node->add_child("MixdownToStereo")->add_child_text(_mixdown_to_stereo ? "1" : "0"); - node->add_child("SplitReels")->add_child_text(_split_reels ? "1" : "0"); - node->add_child("SplitStreams")->add_child_text(_split_streams ? "1" : "0"); - node->add_child("X264CRF")->add_child_text(dcp::raw_convert<string>(_x264_crf)); + cxml::add_text_child(element, "Format", name); + cxml::add_text_child(element, "MixdownToStereo", _mixdown_to_stereo ? "1" : "0"); + cxml::add_text_child(element, "SplitReels", _split_reels ? "1" : "0"); + cxml::add_text_child(element, "SplitStreams", _split_streams ? "1" : "0"); + cxml::add_text_child(element, "X264CRF", dcp::raw_convert<string>(_x264_crf)); } diff --git a/src/lib/export_config.h b/src/lib/export_config.h index 47dddb6c3..52d7b601d 100644 --- a/src/lib/export_config.h +++ b/src/lib/export_config.h @@ -37,7 +37,7 @@ public: void set_defaults(); void read(cxml::ConstNodePtr node); - void write(xmlpp::Element* node) const; + void write(xmlpp::Element* element) const; ExportFormat format() const { return _format; diff --git a/src/lib/ffmpeg_audio_stream.cc b/src/lib/ffmpeg_audio_stream.cc index 9400eb60d..24f96b889 100644 --- a/src/lib/ffmpeg_audio_stream.cc +++ b/src/lib/ffmpeg_audio_stream.cc @@ -52,19 +52,19 @@ FFmpegAudioStream::FFmpegAudioStream (cxml::ConstNodePtr node, int version) void -FFmpegAudioStream::as_xml (xmlpp::Node* root) const +FFmpegAudioStream::as_xml(xmlpp::Element* root) const { FFmpegStream::as_xml (root); - root->add_child("FrameRate")->add_child_text(raw_convert<string>(frame_rate())); - root->add_child("Length")->add_child_text(raw_convert<string>(length())); - mapping().as_xml (root->add_child("Mapping")); + cxml::add_text_child(root, "FrameRate", raw_convert<string>(frame_rate())); + cxml::add_text_child(root, "Length", raw_convert<string>(length())); + mapping().as_xml(cxml::add_child(root, "Mapping")); if (first_audio) { - root->add_child("FirstAudio")->add_child_text(raw_convert<string>(first_audio.get().get())); + cxml::add_text_child(root, "FirstAudio", raw_convert<string>(first_audio.get().get())); } if (codec_name) { - root->add_child("CodecName")->add_child_text(codec_name.get()); + cxml::add_text_child(root, "CodecName", codec_name.get()); } if (bit_depth()) { - root->add_child("BitDepth")->add_child_text(raw_convert<string>(bit_depth().get())); + cxml::add_text_child(root, "BitDepth", raw_convert<string>(bit_depth().get())); } } diff --git a/src/lib/ffmpeg_audio_stream.h b/src/lib/ffmpeg_audio_stream.h index aae982f9e..afd30e905 100644 --- a/src/lib/ffmpeg_audio_stream.h +++ b/src/lib/ffmpeg_audio_stream.h @@ -48,7 +48,7 @@ public: FFmpegAudioStream (cxml::ConstNodePtr, int); - void as_xml (xmlpp::Node *) const; + void as_xml(xmlpp::Element*) const; /* XXX: should probably be locked */ diff --git a/src/lib/ffmpeg_content.cc b/src/lib/ffmpeg_content.cc index 4a7c87b34..c80bdec69 100644 --- a/src/lib/ffmpeg_content.cc +++ b/src/lib/ffmpeg_content.cc @@ -197,61 +197,61 @@ FFmpegContent::FFmpegContent (vector<shared_ptr<Content>> c) void -FFmpegContent::as_xml (xmlpp::Node* node, bool with_paths) const +FFmpegContent::as_xml(xmlpp::Element* element, bool with_paths) const { - node->add_child("Type")->add_child_text("FFmpeg"); - Content::as_xml (node, with_paths); + cxml::add_text_child(element, "Type", "FFmpeg"); + Content::as_xml(element, with_paths); if (video) { - video->as_xml (node); + video->as_xml(element); } if (audio) { - audio->as_xml (node); + audio->as_xml(element); for (auto i: audio->streams()) { auto f = dynamic_pointer_cast<FFmpegAudioStream> (i); DCPOMATIC_ASSERT (f); - f->as_xml (node->add_child("AudioStream")); + f->as_xml(cxml::add_child(element, "AudioStream")); } } if (only_text()) { - only_text()->as_xml (node); + only_text()->as_xml(element); } boost::mutex::scoped_lock lm (_mutex); for (auto i: _subtitle_streams) { - auto t = node->add_child("SubtitleStream"); + auto t = cxml::add_child(element, "SubtitleStream"); if (_subtitle_stream && i == _subtitle_stream) { - t->add_child("Selected")->add_child_text("1"); + cxml::add_text_child(t, "Selected", "1"); } i->as_xml (t); } for (auto i: _filters) { - node->add_child("Filter")->add_child_text(i.id()); + cxml::add_text_child(element, "Filter", i.id()); } if (_first_video) { - node->add_child("FirstVideo")->add_child_text(raw_convert<string>(_first_video.get().get())); + cxml::add_text_child(element, "FirstVideo", raw_convert<string>(_first_video.get().get())); } if (_color_range) { - node->add_child("ColorRange")->add_child_text(raw_convert<string>(static_cast<int>(*_color_range))); + cxml::add_text_child(element, "ColorRange", raw_convert<string>(static_cast<int>(*_color_range))); } if (_color_primaries) { - node->add_child("ColorPrimaries")->add_child_text(raw_convert<string>(static_cast<int>(*_color_primaries))); + cxml::add_text_child(element, "ColorPrimaries", raw_convert<string>(static_cast<int>(*_color_primaries))); } if (_color_trc) { - node->add_child("ColorTransferCharacteristic")->add_child_text(raw_convert<string>(static_cast<int>(*_color_trc))); + cxml::add_text_child(element, "ColorTransferCharacteristic", raw_convert<string>(static_cast<int>(*_color_trc))); } if (_colorspace) { - node->add_child("Colorspace")->add_child_text(raw_convert<string>(static_cast<int>(*_colorspace))); + cxml::add_text_child(element, "Colorspace", raw_convert<string>(static_cast<int>(*_colorspace))); } if (_bits_per_pixel) { - node->add_child("BitsPerPixel")->add_child_text(raw_convert<string>(*_bits_per_pixel)); + cxml::add_text_child(element, "BitsPerPixel", raw_convert<string>(*_bits_per_pixel)); } } diff --git a/src/lib/ffmpeg_content.h b/src/lib/ffmpeg_content.h index a86358b76..ce067f2d3 100644 --- a/src/lib/ffmpeg_content.h +++ b/src/lib/ffmpeg_content.h @@ -70,7 +70,7 @@ public: void take_settings_from (std::shared_ptr<const Content> c) override; std::string summary () const override; std::string technical_summary () const override; - void as_xml (xmlpp::Node *, bool with_paths) const override; + void as_xml(xmlpp::Element* element, bool with_paths) const override; dcpomatic::DCPTime full_length (std::shared_ptr<const Film> film) const override; dcpomatic::DCPTime approximate_length () const override; 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 51ade8e89..d173c6bb6 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,8 +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); - if (rotate_tag && *rotate_tag->value && strcmp(rotate_tag->value, "0")) { char *tail; _rotation = av_strtod (rotate_tag->value, &tail); @@ -191,8 +181,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)); } if (_rotation) { @@ -253,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/ffmpeg_image_proxy.cc b/src/lib/ffmpeg_image_proxy.cc index 2fcd486df..0ee41deab 100644 --- a/src/lib/ffmpeg_image_proxy.cc +++ b/src/lib/ffmpeg_image_proxy.cc @@ -234,9 +234,9 @@ FFmpegImageProxy::image (Image::Alignment alignment, optional<dcp::Size>) const void -FFmpegImageProxy::add_metadata (xmlpp::Node* node) const +FFmpegImageProxy::add_metadata(xmlpp::Element* element) const { - node->add_child("Type")->add_child_text (N_("FFmpeg")); + cxml::add_text_child(element, "Type", N_("FFmpeg")); } void diff --git a/src/lib/ffmpeg_image_proxy.h b/src/lib/ffmpeg_image_proxy.h index b567fadee..1b8f44a7a 100644 --- a/src/lib/ffmpeg_image_proxy.h +++ b/src/lib/ffmpeg_image_proxy.h @@ -35,7 +35,7 @@ public: boost::optional<dcp::Size> size = boost::optional<dcp::Size> () ) const override; - void add_metadata (xmlpp::Node *) const override; + void add_metadata(xmlpp::Element*) const override; void write_to_socket (std::shared_ptr<Socket>) const override; bool same (std::shared_ptr<const ImageProxy> other) const override; size_t memory_used () const override; diff --git a/src/lib/ffmpeg_stream.cc b/src/lib/ffmpeg_stream.cc index 47f71d119..800c45eb6 100644 --- a/src/lib/ffmpeg_stream.cc +++ b/src/lib/ffmpeg_stream.cc @@ -40,10 +40,10 @@ FFmpegStream::FFmpegStream (cxml::ConstNodePtr node) } void -FFmpegStream::as_xml (xmlpp::Node* root) const +FFmpegStream::as_xml(xmlpp::Element* root) const { - root->add_child("Name")->add_child_text (name); - root->add_child("Id")->add_child_text (raw_convert<string> (_id)); + cxml::add_text_child(root, "Name", name); + cxml::add_text_child(root, "Id", raw_convert<string>(_id)); } bool diff --git a/src/lib/ffmpeg_stream.h b/src/lib/ffmpeg_stream.h index 84b2a8853..221075eeb 100644 --- a/src/lib/ffmpeg_stream.h +++ b/src/lib/ffmpeg_stream.h @@ -37,7 +37,7 @@ public: explicit FFmpegStream (cxml::ConstNodePtr); - void as_xml (xmlpp::Node *) const; + void as_xml(xmlpp::Element*) const; /** @param c An AVFormatContext. * @param index A stream index within the AVFormatContext. diff --git a/src/lib/ffmpeg_subtitle_stream.cc b/src/lib/ffmpeg_subtitle_stream.cc index 1101901b7..3f43b6ec5 100644 --- a/src/lib/ffmpeg_subtitle_stream.cc +++ b/src/lib/ffmpeg_subtitle_stream.cc @@ -49,15 +49,15 @@ FFmpegSubtitleStream::FFmpegSubtitleStream (cxml::ConstNodePtr node, int version } void -FFmpegSubtitleStream::as_xml (xmlpp::Node* root) const +FFmpegSubtitleStream::as_xml(xmlpp::Element* root) const { - FFmpegStream::as_xml (root); + FFmpegStream::as_xml(root); boost::mutex::scoped_lock lm (_mutex); for (map<RGBA, RGBA>::const_iterator i = _colours.begin(); i != _colours.end(); ++i) { - xmlpp::Node* node = root->add_child("Colour"); - i->first.as_xml (node->add_child("From")); - i->second.as_xml (node->add_child("To")); + auto node = cxml::add_child(root, "Colour"); + i->first.as_xml(cxml::add_child(node, "From")); + i->second.as_xml(cxml::add_child(node, "To")); } } diff --git a/src/lib/ffmpeg_subtitle_stream.h b/src/lib/ffmpeg_subtitle_stream.h index 8f56d1975..6251350ce 100644 --- a/src/lib/ffmpeg_subtitle_stream.h +++ b/src/lib/ffmpeg_subtitle_stream.h @@ -33,7 +33,7 @@ public: FFmpegSubtitleStream (cxml::ConstNodePtr node, int version); - void as_xml (xmlpp::Node *) const; + void as_xml(xmlpp::Element*) const; void set_colour (RGBA from, RGBA to); std::map<RGBA, RGBA> colours () const; diff --git a/src/lib/film.cc b/src/lib/film.cc index d9ab6e2a3..a88e8a855 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 @@ -379,88 +380,91 @@ Film::metadata (bool with_content_paths) const auto doc = make_shared<xmlpp::Document>(); auto root = doc->create_root_node ("Metadata"); - root->add_child("Version")->add_child_text (raw_convert<string> (current_state_version)); - auto last_write = root->add_child("LastWrittenBy"); + cxml::add_text_child(root, "Version", raw_convert<string>(current_state_version)); + auto last_write = cxml::add_child(root, "LastWrittenBy"); last_write->add_child_text (dcpomatic_version); last_write->set_attribute("git", dcpomatic_git_commit); - root->add_child("Name")->add_child_text (_name); - root->add_child("UseISDCFName")->add_child_text (_use_isdcf_name ? "1" : "0"); + cxml::add_text_child(root, "Name", _name); + cxml::add_text_child(root, "UseISDCFName", _use_isdcf_name ? "1" : "0"); if (_dcp_content_type) { - root->add_child("DCPContentType")->add_child_text (_dcp_content_type->isdcf_name ()); + cxml::add_text_child(root, "DCPContentType", _dcp_content_type->isdcf_name()); } if (_container) { - root->add_child("Container")->add_child_text (_container->id ()); - } - - root->add_child("Resolution")->add_child_text (resolution_to_string (_resolution)); - root->add_child("J2KBandwidth")->add_child_text (raw_convert<string> (_j2k_bandwidth)); - root->add_child("VideoFrameRate")->add_child_text (raw_convert<string> (_video_frame_rate)); - root->add_child("AudioFrameRate")->add_child_text(raw_convert<string>(_audio_frame_rate)); - root->add_child("ISDCFDate")->add_child_text (boost::gregorian::to_iso_string (_isdcf_date)); - root->add_child("AudioChannels")->add_child_text (raw_convert<string> (_audio_channels)); - root->add_child("ThreeD")->add_child_text (_three_d ? "1" : "0"); - root->add_child("Sequence")->add_child_text (_sequence ? "1" : "0"); - root->add_child("Interop")->add_child_text (_interop ? "1" : "0"); - root->add_child("LimitToSMPTEBv20")->add_child_text(_limit_to_smpte_bv20 ? "1" : "0"); - root->add_child("Encrypted")->add_child_text (_encrypted ? "1" : "0"); - root->add_child("Key")->add_child_text (_key.hex ()); - root->add_child("ContextID")->add_child_text (_context_id); + cxml::add_text_child(root, "Container", _container->id()); + } + + cxml::add_text_child(root, "Resolution", resolution_to_string(_resolution)); + cxml::add_text_child(root, "J2KBandwidth", raw_convert<string>(_j2k_bandwidth)); + cxml::add_text_child(root, "VideoFrameRate", raw_convert<string>(_video_frame_rate)); + cxml::add_text_child(root, "AudioFrameRate", raw_convert<string>(_audio_frame_rate)); + cxml::add_text_child(root, "ISDCFDate", boost::gregorian::to_iso_string(_isdcf_date)); + cxml::add_text_child(root, "AudioChannels", raw_convert<string>(_audio_channels)); + cxml::add_text_child(root, "ThreeD", _three_d ? "1" : "0"); + cxml::add_text_child(root, "Sequence", _sequence ? "1" : "0"); + cxml::add_text_child(root, "Interop", _interop ? "1" : "0"); + cxml::add_text_child(root, "LimitToSMPTEBv20", _limit_to_smpte_bv20 ? "1" : "0"); + cxml::add_text_child(root, "Encrypted", _encrypted ? "1" : "0"); + cxml::add_text_child(root, "Key", _key.hex ()); + cxml::add_text_child(root, "ContextID", _context_id); if (_audio_processor) { - root->add_child("AudioProcessor")->add_child_text (_audio_processor->id ()); + cxml::add_text_child(root, "AudioProcessor", _audio_processor->id()); } - root->add_child("ReelType")->add_child_text (raw_convert<string> (static_cast<int> (_reel_type))); - root->add_child("ReelLength")->add_child_text (raw_convert<string> (_reel_length)); - root->add_child("ReencodeJ2K")->add_child_text (_reencode_j2k ? "1" : "0"); - root->add_child("UserExplicitVideoFrameRate")->add_child_text(_user_explicit_video_frame_rate ? "1" : "0"); + cxml::add_text_child(root, "ReelType", raw_convert<string>(static_cast<int> (_reel_type))); + cxml::add_text_child(root, "ReelLength", raw_convert<string>(_reel_length)); + for (auto boundary: _custom_reel_boundaries) { + cxml::add_text_child(root, "CustomReelBoundary", raw_convert<string>(boundary.get())); + } + cxml::add_text_child(root, "ReencodeJ2K", _reencode_j2k ? "1" : "0"); + cxml::add_text_child(root, "UserExplicitVideoFrameRate", _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)); + auto m = cxml::add_child(root, "Marker"); + m->set_attribute("type", dcp::marker_to_string(marker.first)); m->add_child_text(raw_convert<string>(marker.second.get())); } for (auto i: _ratings) { - i.as_xml (root->add_child("Rating")); + i.as_xml(cxml::add_child(root, "Rating")); } for (auto i: _content_versions) { - root->add_child("ContentVersion")->add_child_text(i); + cxml::add_text_child(root, "ContentVersion", i); } - root->add_child("NameLanguage")->add_child_text(_name_language.to_string()); - root->add_child("TerritoryType")->add_child_text(territory_type_to_string(_territory_type)); + cxml::add_text_child(root, "NameLanguage", _name_language.to_string()); + cxml::add_text_child(root, "TerritoryType", territory_type_to_string(_territory_type)); if (_release_territory) { - root->add_child("ReleaseTerritory")->add_child_text(_release_territory->subtag()); + cxml::add_text_child(root, "ReleaseTerritory", _release_territory->subtag()); } if (_sign_language_video_language) { - root->add_child("SignLanguageVideoLanguage")->add_child_text(_sign_language_video_language->to_string()); + cxml::add_text_child(root, "SignLanguageVideoLanguage", _sign_language_video_language->to_string()); } - root->add_child("VersionNumber")->add_child_text(raw_convert<string>(_version_number)); - root->add_child("Status")->add_child_text(dcp::status_to_string(_status)); + cxml::add_text_child(root, "VersionNumber", raw_convert<string>(_version_number)); + cxml::add_text_child(root, "Status", dcp::status_to_string(_status)); if (_chain) { - root->add_child("Chain")->add_child_text(*_chain); + cxml::add_text_child(root, "Chain", *_chain); } if (_distributor) { - root->add_child("Distributor")->add_child_text(*_distributor); + cxml::add_text_child(root, "Distributor", *_distributor); } if (_facility) { - root->add_child("Facility")->add_child_text(*_facility); + cxml::add_text_child(root, "Facility", *_facility); } if (_studio) { - root->add_child("Studio")->add_child_text(*_studio); + cxml::add_text_child(root, "Studio", *_studio); } - root->add_child("TempVersion")->add_child_text(_temp_version ? "1" : "0"); - root->add_child("PreRelease")->add_child_text(_pre_release ? "1" : "0"); - root->add_child("RedBand")->add_child_text(_red_band ? "1" : "0"); - root->add_child("TwoDVersionOfThreeD")->add_child_text(_two_d_version_of_three_d ? "1" : "0"); + cxml::add_text_child(root, "TempVersion", _temp_version ? "1" : "0"); + cxml::add_text_child(root, "PreRelease", _pre_release ? "1" : "0"); + cxml::add_text_child(root, "RedBand", _red_band ? "1" : "0"); + cxml::add_text_child(root, "TwoDVersionOfThreeD", _two_d_version_of_three_d ? "1" : "0"); if (_luminance) { - root->add_child("LuminanceValue")->add_child_text(raw_convert<string>(_luminance->value())); - root->add_child("LuminanceUnit")->add_child_text(dcp::Luminance::unit_to_string(_luminance->unit())); + cxml::add_text_child(root, "LuminanceValue", raw_convert<string>(_luminance->value())); + cxml::add_text_child(root, "LuminanceUnit", dcp::Luminance::unit_to_string(_luminance->unit())); } - root->add_child("UserExplicitContainer")->add_child_text(_user_explicit_container ? "1" : "0"); - root->add_child("UserExplicitResolution")->add_child_text(_user_explicit_resolution ? "1" : "0"); + cxml::add_text_child(root, "UserExplicitContainer", _user_explicit_container ? "1" : "0"); + cxml::add_text_child(root, "UserExplicitResolution", _user_explicit_resolution ? "1" : "0"); if (_audio_language) { - root->add_child("AudioLanguage")->add_child_text(_audio_language->to_string()); + cxml::add_text_child(root, "AudioLanguage", _audio_language->to_string()); } - _playlist->as_xml (root->add_child ("Playlist"), with_content_paths); + _playlist->as_xml(cxml::add_child(root, "Playlist"), with_content_paths); return doc; } @@ -599,11 +603,18 @@ Film::read_metadata (optional<boost::filesystem::path> path) _reel_type = static_cast<ReelType> (f.optional_number_child<int>("ReelType").get_value_or (static_cast<int>(ReelType::SINGLE))); _reel_length = f.optional_number_child<int64_t>("ReelLength").get_value_or (2000000000); + for (auto boundary: f.node_children("CustomReelBoundary")) { + _custom_reel_boundaries.push_back(DCPTime(raw_convert<int64_t>(boundary->content()))); + } _reencode_j2k = f.optional_bool_child("ReencodeJ2K").get_value_or(false); _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")) { @@ -1228,6 +1239,16 @@ Film::set_reel_length (int64_t r) _reel_length = r; } + +void +Film::set_custom_reel_boundaries(vector<DCPTime> boundaries) +{ + FilmChangeSignaller ch(this, FilmProperty::CUSTOM_REEL_BOUNDARIES); + std::sort(boundaries.begin(), boundaries.end()); + _custom_reel_boundaries = std::move(boundaries); +} + + void Film::set_reencode_j2k (bool r) { @@ -1595,6 +1616,23 @@ Film::check_settings_consistency () if (change_made) { Message (_("DCP-o-matic had to change your settings for referring to DCPs as OV. Please review those settings to make sure they are what you want.")); } + + if (reel_type() == ReelType::CUSTOM) { + auto boundaries = custom_reel_boundaries(); + auto too_late = std::find_if(boundaries.begin(), boundaries.end(), [this](dcpomatic::DCPTime const& time) { + return time >= length(); + }); + + if (too_late != boundaries.end()) { + if (std::distance(too_late, boundaries.end()) > 1) { + Message(_("DCP-o-matic had to remove some of your custom reel boundaries as they no longer lie within the film.")); + } else { + Message(_("DCP-o-matic had to remove one of your custom reel boundaries as it no longer lies within the film.")); + } + boundaries.erase(too_late, boundaries.end()); + set_custom_reel_boundaries(boundaries); + } + } } void @@ -1799,15 +1837,16 @@ Film::audio_analysis_finished () /* XXX */ } -list<DCPTimePeriod> + +vector<DCPTimePeriod> Film::reels () const { - list<DCPTimePeriod> p; + vector<DCPTimePeriod> periods; auto const len = length(); switch (reel_type ()) { case ReelType::SINGLE: - p.push_back (DCPTimePeriod (DCPTime (), len)); + periods.emplace_back(DCPTime(), len); break; case ReelType::BY_VIDEO_CONTENT: { @@ -1832,7 +1871,7 @@ Film::reels () const for (auto t: split_points) { if (last && (t - *last) >= DCPTime::from_seconds(1)) { /* Period from *last to t is long enough; use it and start a new one */ - p.push_back (DCPTimePeriod(*last, t)); + periods.emplace_back(*last, t); last = t; } else if (!last) { /* That was the first time, so start a new period */ @@ -1840,8 +1879,8 @@ Film::reels () const } } - if (!p.empty()) { - p.back().to = split_points.back(); + if (!periods.empty()) { + periods.back().to = split_points.back(); } break; } @@ -1854,16 +1893,29 @@ Film::reels () const Frame const reel_in_frames = max(_reel_length / ((j2k_bandwidth() / video_frame_rate()) / 8), static_cast<Frame>(video_frame_rate())); while (current < len) { DCPTime end = min (len, current + DCPTime::from_frames (reel_in_frames, video_frame_rate ())); - p.push_back (DCPTimePeriod (current, end)); + periods.emplace_back(current, end); current = end; } break; } + case ReelType::CUSTOM: + { + DCPTimePeriod current; + for (auto boundary: _custom_reel_boundaries) { + current.to = boundary; + periods.push_back(current); + current.from = boundary; + } + current.to = len; + periods.push_back(current); + break; + } } - return p; + return periods; } + /** @param period A period within the DCP * @return Name of the content which most contributes to the given period. */ @@ -2230,3 +2282,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) { + cxml::add_text_child(root, state.first, 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..0a0c5a4e1 100644 --- a/src/lib/film.h +++ b/src/lib/film.h @@ -186,7 +186,7 @@ public: return _playlist; } - std::list<dcpomatic::DCPTimePeriod> reels () const; + std::vector<dcpomatic::DCPTimePeriod> reels() const; std::list<int> mapped_audio_channels () const; boost::optional<dcp::LanguageTag> audio_language () const { @@ -291,6 +291,10 @@ public: return _reel_length; } + std::vector<dcpomatic::DCPTime> custom_reel_boundaries() const { + return _custom_reel_boundaries; + } + std::string context_id () const { return _context_id; } @@ -408,6 +412,7 @@ public: void set_audio_processor (AudioProcessor const * processor); void set_reel_type (ReelType); void set_reel_length (int64_t); + void set_custom_reel_boundaries(std::vector<dcpomatic::DCPTime> boundaries); void set_reencode_j2k (bool); void set_marker (dcp::Marker type, dcpomatic::DCPTime time); void unset_marker (dcp::Marker type); @@ -434,6 +439,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 +486,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; @@ -522,8 +532,10 @@ private: bool _limit_to_smpte_bv20; AudioProcessor const * _audio_processor; ReelType _reel_type; - /** Desired reel length in bytes, if _reel_type == REELTYPE_BY_LENGTH */ + /** Desired reel length in bytes, if _reel_type == BY_LENGTH */ int64_t _reel_length; + /** Reel boundaries (excluding those at the start and end, sorted in ascending order) if _reel_type == CUSTOM */ + std::vector<dcpomatic::DCPTime> _custom_reel_boundaries; bool _reencode_j2k; /** true if the user has ever explicitly set the video frame rate of this film */ bool _user_explicit_video_frame_rate; @@ -562,6 +574,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/film_property.h b/src/lib/film_property.h index c23297965..0841caa5c 100644 --- a/src/lib/film_property.h +++ b/src/lib/film_property.h @@ -51,6 +51,7 @@ enum class FilmProperty { AUDIO_PROCESSOR, REEL_TYPE, REEL_LENGTH, + CUSTOM_REEL_BOUNDARIES, REENCODE_J2K, MARKERS, RATINGS, diff --git a/src/lib/filter.cc b/src/lib/filter.cc index 9a14795ec..465b5991a 100644 --- a/src/lib/filter.cc +++ b/src/lib/filter.cc @@ -37,7 +37,8 @@ LIBDCP_ENABLE_WARNINGS #include "i18n.h" -using namespace std; +using std::string; +using std::vector; using boost::optional; diff --git a/src/lib/font.cc b/src/lib/font.cc index 955a2ad1c..246d3eeb8 100644 --- a/src/lib/font.cc +++ b/src/lib/font.cc @@ -63,11 +63,11 @@ Font& Font::operator=(Font const& other) void -Font::as_xml (xmlpp::Node* node) +Font::as_xml(xmlpp::Element* element) { - node->add_child("Id")->add_child_text(_id); + cxml::add_text_child(element, "Id", _id); if (_content.file) { - node->add_child("File")->add_child_text(_content.file->string()); + cxml::add_text_child(element, "File", _content.file->string()); } } diff --git a/src/lib/font.h b/src/lib/font.h index 12a14aba4..092b682ba 100644 --- a/src/lib/font.h +++ b/src/lib/font.h @@ -57,7 +57,7 @@ public: Font(Font const& other); Font& operator=(Font const& other); - void as_xml (xmlpp::Node* node); + void as_xml(xmlpp::Element* element); std::string id () const { return _id; 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_content.cc b/src/lib/image_content.cc index 1a92c944e..1ac6e085b 100644 --- a/src/lib/image_content.cc +++ b/src/lib/image_content.cc @@ -99,13 +99,13 @@ ImageContent::technical_summary () const void -ImageContent::as_xml (xmlpp::Node* node, bool with_paths) const +ImageContent::as_xml(xmlpp::Element* element, bool with_paths) const { - node->add_child("Type")->add_child_text ("Image"); - Content::as_xml (node, with_paths); + cxml::add_text_child(element, "Type", "Image"); + Content::as_xml(element, with_paths); if (video) { - video->as_xml (node); + video->as_xml(element); } } diff --git a/src/lib/image_content.h b/src/lib/image_content.h index d817eeee8..140159bfa 100644 --- a/src/lib/image_content.h +++ b/src/lib/image_content.h @@ -40,7 +40,7 @@ public: void examine (std::shared_ptr<const Film> film, std::shared_ptr<Job>) override; std::string summary () const override; std::string technical_summary () const override; - void as_xml (xmlpp::Node *, bool with_paths) const override; + void as_xml(xmlpp::Element*, bool with_paths) const override; dcpomatic::DCPTime full_length (std::shared_ptr<const Film> film) const override; dcpomatic::DCPTime approximate_length () const override; 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/image_proxy.h b/src/lib/image_proxy.h index a37be580f..cec13b773 100644 --- a/src/lib/image_proxy.h +++ b/src/lib/image_proxy.h @@ -41,7 +41,7 @@ class Image; class Socket; namespace xmlpp { - class Node; + class Element; } namespace cxml { @@ -101,7 +101,7 @@ public: boost::optional<dcp::Size> size = boost::optional<dcp::Size> () ) const = 0; - virtual void add_metadata (xmlpp::Node *) const = 0; + virtual void add_metadata(xmlpp::Element *) const = 0; virtual void write_to_socket (std::shared_ptr<Socket>) const = 0; /** @return true if our image is definitely the same as another, false if it is probably not */ virtual bool same (std::shared_ptr<const ImageProxy>) const = 0; 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_image_proxy.cc b/src/lib/j2k_image_proxy.cc index 269b01bca..869e96967 100644 --- a/src/lib/j2k_image_proxy.cc +++ b/src/lib/j2k_image_proxy.cc @@ -192,15 +192,15 @@ J2KImageProxy::image (Image::Alignment alignment, optional<dcp::Size> target_siz void -J2KImageProxy::add_metadata (xmlpp::Node* node) const +J2KImageProxy::add_metadata(xmlpp::Element* element) const { - node->add_child("Type")->add_child_text(N_("J2K")); - node->add_child("Width")->add_child_text(raw_convert<string>(_size.width)); - node->add_child("Height")->add_child_text(raw_convert<string>(_size.height)); + cxml::add_text_child(element, "Type", N_("J2K")); + cxml::add_text_child(element, "Width", raw_convert<string>(_size.width)); + cxml::add_text_child(element, "Height", raw_convert<string>(_size.height)); if (_eye) { - node->add_child("Eye")->add_child_text(raw_convert<string>(static_cast<int>(_eye.get()))); + cxml::add_text_child(element, "Eye", raw_convert<string>(static_cast<int>(_eye.get()))); } - node->add_child("Size")->add_child_text(raw_convert<string>(_data->size())); + cxml::add_text_child(element, "Size", raw_convert<string>(_data->size())); } diff --git a/src/lib/j2k_image_proxy.h b/src/lib/j2k_image_proxy.h index 9666ea406..3346fff08 100644 --- a/src/lib/j2k_image_proxy.h +++ b/src/lib/j2k_image_proxy.h @@ -61,7 +61,7 @@ public: boost::optional<dcp::Size> size = boost::optional<dcp::Size> () ) const override; - void add_metadata (xmlpp::Node *) const override; + void add_metadata(xmlpp::Element*) const override; void write_to_socket (std::shared_ptr<Socket> override) const override; /** @return true if our image is definitely the same as another, false if it is probably not */ bool same (std::shared_ptr<const ImageProxy>) const override; 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 2d3a021b5..e4fabe1a6 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/kdm_recipient.cc b/src/lib/kdm_recipient.cc index 671e9797c..c33eb1b9f 100644 --- a/src/lib/kdm_recipient.cc +++ b/src/lib/kdm_recipient.cc @@ -39,14 +39,14 @@ KDMRecipient::KDMRecipient (cxml::ConstNodePtr node) void KDMRecipient::as_xml (xmlpp::Element* parent) const { - parent->add_child("Name")->add_child_text(name); + cxml::add_text_child(parent, "Name", name); if (recipient) { - parent->add_child("Recipient")->add_child_text(recipient->certificate(true)); + cxml::add_text_child(parent, "Recipient", recipient->certificate(true)); } if (recipient_file) { - parent->add_child("RecipientFile")->add_child_text(*recipient_file); + cxml::add_text_child(parent, "RecipientFile", *recipient_file); } - parent->add_child("Notes")->add_child_text(notes); + cxml::add_text_child(parent, "Notes", notes); } 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/pixel_quanta.cc b/src/lib/pixel_quanta.cc index 7c1d285cf..07690f3ac 100644 --- a/src/lib/pixel_quanta.cc +++ b/src/lib/pixel_quanta.cc @@ -34,8 +34,8 @@ PixelQuanta::PixelQuanta (cxml::ConstNodePtr node) void PixelQuanta::as_xml (xmlpp::Element* node) const { - node->add_child("X")->add_child_text(dcp::raw_convert<std::string>(x)); - node->add_child("Y")->add_child_text(dcp::raw_convert<std::string>(y)); + cxml::add_text_child(node, "X", dcp::raw_convert<std::string>(x)); + cxml::add_text_child(node, "Y", dcp::raw_convert<std::string>(y)); } 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..247301d58 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) { @@ -199,29 +200,29 @@ PlayerVideo::make_image (function<AVPixelFormat (AVPixelFormat)> pixel_format, V void -PlayerVideo::add_metadata (xmlpp::Node* node) const +PlayerVideo::add_metadata(xmlpp::Element* element) const { - _crop.as_xml (node); + _crop.as_xml(element); if (_fade) { - node->add_child("Fade")->add_child_text (raw_convert<string> (_fade.get ())); + cxml::add_text_child(element, "Fade", raw_convert<string>(_fade.get())); } - _in->add_metadata (node->add_child ("In")); - node->add_child("InterWidth")->add_child_text (raw_convert<string> (_inter_size.width)); - node->add_child("InterHeight")->add_child_text (raw_convert<string> (_inter_size.height)); - node->add_child("OutWidth")->add_child_text (raw_convert<string> (_out_size.width)); - node->add_child("OutHeight")->add_child_text (raw_convert<string> (_out_size.height)); - node->add_child("Eyes")->add_child_text (raw_convert<string> (static_cast<int> (_eyes))); - node->add_child("Part")->add_child_text (raw_convert<string> (static_cast<int> (_part))); - node->add_child("VideoRange")->add_child_text(raw_convert<string>(static_cast<int>(_video_range))); - node->add_child("Error")->add_child_text(_error ? "1" : "0"); + _in->add_metadata(cxml::add_child(element, "In")); + cxml::add_text_child(element, "InterWidth", raw_convert<string>(_inter_size.width)); + cxml::add_text_child(element, "InterHeight", raw_convert<string>(_inter_size.height)); + cxml::add_text_child(element, "OutWidth", raw_convert<string>(_out_size.width)); + cxml::add_text_child(element, "OutHeight", raw_convert<string>(_out_size.height)); + cxml::add_text_child(element, "Eyes", raw_convert<string>(static_cast<int>(_eyes))); + cxml::add_text_child(element, "Part", raw_convert<string>(static_cast<int>(_part))); + cxml::add_text_child(element, "VideoRange", raw_convert<string>(static_cast<int>(_video_range))); + cxml::add_text_child(element, "Error", _error ? "1" : "0"); if (_colour_conversion) { - _colour_conversion.get().as_xml (node); + _colour_conversion.get().as_xml(element); } if (_text) { - node->add_child ("SubtitleWidth")->add_child_text (raw_convert<string> (_text->image->size().width)); - node->add_child ("SubtitleHeight")->add_child_text (raw_convert<string> (_text->image->size().height)); - node->add_child ("SubtitleX")->add_child_text (raw_convert<string> (_text->position.x)); - node->add_child ("SubtitleY")->add_child_text (raw_convert<string> (_text->position.y)); + cxml::add_text_child(element, "SubtitleWidth", raw_convert<string>(_text->image->size().width)); + cxml::add_text_child(element, "SubtitleHeight", raw_convert<string>(_text->image->size().height)); + cxml::add_text_child(element, "SubtitleX", raw_convert<string>(_text->position.x)); + cxml::add_text_child(element, "SubtitleY", raw_convert<string>(_text->position.y)); } } @@ -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..e2968749c 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 ); @@ -82,7 +82,7 @@ public: static AVPixelFormat force (AVPixelFormat); static AVPixelFormat keep_xyz_or_rgb (AVPixelFormat); - void add_metadata (xmlpp::Node* node) const; + void add_metadata(xmlpp::Element* element) const; void write_to_socket (std::shared_ptr<Socket> socket) const; bool reset_metadata (std::shared_ptr<const Film> film, dcp::Size player_video_container_size); @@ -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/playlist.cc b/src/lib/playlist.cc index 85957e106..7f83e9474 100644 --- a/src/lib/playlist.cc +++ b/src/lib/playlist.cc @@ -282,10 +282,10 @@ Playlist::set_from_xml (shared_ptr<const Film> film, cxml::ConstNodePtr node, in * @param with_content_paths true to include <Path> nodes in <Content> nodes, false to omit them. */ void -Playlist::as_xml (xmlpp::Node* node, bool with_content_paths) +Playlist::as_xml(xmlpp::Element* element, bool with_content_paths) { for (auto i: content()) { - i->as_xml (node->add_child ("Content"), with_content_paths); + i->as_xml(cxml::add_child(element, "Content"), with_content_paths); } } diff --git a/src/lib/playlist.h b/src/lib/playlist.h index e2662eb45..7b9bd19fa 100644 --- a/src/lib/playlist.h +++ b/src/lib/playlist.h @@ -50,7 +50,7 @@ public: Playlist (Playlist const&) = delete; Playlist& operator= (Playlist const&) = delete; - void as_xml (xmlpp::Node *, bool with_content_paths); + void as_xml(xmlpp::Element*, bool with_content_paths); void set_from_xml (std::shared_ptr<const Film> film, cxml::ConstNodePtr node, int version, std::list<std::string>& notes); void add (std::shared_ptr<const Film> film, std::shared_ptr<Content>); diff --git a/src/lib/raw_image_proxy.cc b/src/lib/raw_image_proxy.cc index c606ddd99..1ad78827c 100644 --- a/src/lib/raw_image_proxy.cc +++ b/src/lib/raw_image_proxy.cc @@ -73,12 +73,12 @@ RawImageProxy::image (Image::Alignment alignment, optional<dcp::Size>) const void -RawImageProxy::add_metadata (xmlpp::Node* node) const +RawImageProxy::add_metadata(xmlpp::Element* element) const { - node->add_child("Type")->add_child_text(N_("Raw")); - node->add_child("Width")->add_child_text(raw_convert<string>(_image->size().width)); - node->add_child("Height")->add_child_text(raw_convert<string>(_image->size().height)); - node->add_child("PixelFormat")->add_child_text(raw_convert<string>(static_cast<int>(_image->pixel_format()))); + cxml::add_text_child(element, "Type", N_("Raw")); + cxml::add_text_child(element, "Width", raw_convert<string>(_image->size().width)); + cxml::add_text_child(element, "Height", raw_convert<string>(_image->size().height)); + cxml::add_text_child(element, "PixelFormat", raw_convert<string>(static_cast<int>(_image->pixel_format()))); } diff --git a/src/lib/raw_image_proxy.h b/src/lib/raw_image_proxy.h index d5ae2457d..852f7c076 100644 --- a/src/lib/raw_image_proxy.h +++ b/src/lib/raw_image_proxy.h @@ -37,7 +37,7 @@ public: boost::optional<dcp::Size> size = boost::optional<dcp::Size> () ) const override; - void add_metadata (xmlpp::Node *) const override; + void add_metadata(xmlpp::Element*) const override; void write_to_socket (std::shared_ptr<Socket>) const override; bool same (std::shared_ptr<const ImageProxy>) const override; size_t memory_used () const override; 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/render_text.cc b/src/lib/render_text.cc index 870f3045d..52830dcf3 100644 --- a/src/lib/render_text.cc +++ b/src/lib/render_text.cc @@ -31,6 +31,7 @@ #include <cairomm/cairomm.h> LIBDCP_DISABLE_WARNINGS #include <pangomm.h> +#include <pangommconfig.h> LIBDCP_ENABLE_WARNINGS #include <pango/pangocairo.h> #include <boost/algorithm/string.hpp> @@ -51,6 +52,15 @@ using boost::optional; using namespace dcpomatic; +#if CAIROMM_MAJOR_VERSION == 1 && CAIROMM_MINOR_VERSION <= 14 +#define DCPOMATIC_OLD_CAIROMM_API +#endif + +#if PANGOMM_MAJOR_VERSION == 2 && PANGOMM_MINOR_VERSION <= 46 +#define DCPOMATIC_OLD_PANGOMM_API +#endif + + /** Create a Pango layout using a dummy context which we can use to calculate the size * of the text we will render. Then we can transfer the layout over to the real context * for the actual render. @@ -66,7 +76,11 @@ create_layout(string font_name, string markup) auto context = Glib::wrap (c_context); auto layout = Pango::Layout::create(context); - layout->set_alignment (Pango::ALIGN_LEFT); +#ifdef DCPOMATIC_OLD_PANGOMM_API + layout->set_alignment(Pango::ALIGN_LEFT); +#else + layout->set_alignment(Pango::Alignment::LEFT); +#endif Pango::FontDescription font (font_name); layout->set_font_description (font); layout->set_markup (markup); @@ -163,11 +177,22 @@ create_surface (shared_ptr<Image> image) DCPOMATIC_ASSERT (image->pixel_format() == AV_PIX_FMT_BGRA); return Cairo::ImageSurface::create ( image->data()[0], +#ifdef DCPOMATIC_OLD_CAIROMM_API Cairo::FORMAT_ARGB32, +#else + Cairo::ImageSurface::Format::ARGB32, +#endif image->size().width, image->size().height, /* Cairo ARGB32 means first byte blue, second byte green, third byte red, fourth byte alpha */ - Cairo::ImageSurface::format_stride_for_width (Cairo::FORMAT_ARGB32, image->size().width) + Cairo::ImageSurface::format_stride_for_width( +#ifdef DCPOMATIC_OLD_CAIROMM_API + Cairo::FORMAT_ARGB32, +#else + Cairo::ImageSurface::Format::ARGB32, +#endif + image->size().width + ) ); } @@ -394,7 +419,11 @@ render_line(vector<StringText> subtitles, dcp::Size target, DCPTime time, int fr /* Border effect */ set_source_rgba (context, first.effect_colour(), fade_factor); context->set_line_width (border_width); +#ifdef DCPOMATIC_OLD_CAIROMM_API context->set_line_join (Cairo::LINE_JOIN_ROUND); +#else + context->set_line_join (Cairo::Context::LineJoin::ROUND); +#endif context->move_to (x_offset, y_offset); layout.pango->add_to_cairo_context (context); context->stroke (); diff --git a/src/lib/rgba.cc b/src/lib/rgba.cc index 2e6a4fab6..2e0408d8a 100644 --- a/src/lib/rgba.cc +++ b/src/lib/rgba.cc @@ -41,12 +41,12 @@ RGBA::RGBA (cxml::ConstNodePtr node) void -RGBA::as_xml (xmlpp::Node* parent) const +RGBA::as_xml(xmlpp::Element* parent) const { - parent->add_child("R")->add_child_text(lexical_cast<string>(int(r))); - parent->add_child("G")->add_child_text(lexical_cast<string>(int(g))); - parent->add_child("B")->add_child_text(lexical_cast<string>(int(b))); - parent->add_child("A")->add_child_text(lexical_cast<string>(int(a))); + cxml::add_text_child(parent, "R", lexical_cast<string>(int(r))); + cxml::add_text_child(parent, "G", lexical_cast<string>(int(g))); + cxml::add_text_child(parent, "B", lexical_cast<string>(int(b))); + cxml::add_text_child(parent, "A", lexical_cast<string>(int(a))); } diff --git a/src/lib/rgba.h b/src/lib/rgba.h index 96fed710e..a582a4ca1 100644 --- a/src/lib/rgba.h +++ b/src/lib/rgba.h @@ -44,7 +44,7 @@ public: explicit RGBA (cxml::ConstNodePtr node); - void as_xml (xmlpp::Node* parent) const; + void as_xml(xmlpp::Element* parent) const; uint8_t r = 0; uint8_t g = 0; diff --git a/src/lib/screen.cc b/src/lib/screen.cc index febf9085c..38c474850 100644 --- a/src/lib/screen.cc +++ b/src/lib/screen.cc @@ -57,7 +57,7 @@ Screen::as_xml (xmlpp::Element* parent) const { KDMRecipient::as_xml (parent); for (auto i: trusted_devices) { - parent->add_child("TrustedDevice")->add_child_text(i.as_string()); + cxml::add_text_child(parent, "TrustedDevice", i.as_string()); } } @@ -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/spl.cc b/src/lib/spl.cc index f49f11a7d..e87465cdc 100644 --- a/src/lib/spl.cc +++ b/src/lib/spl.cc @@ -62,10 +62,10 @@ SPL::write (boost::filesystem::path path) const { xmlpp::Document doc; auto root = doc.create_root_node ("SPL"); - root->add_child("Id")->add_child_text (_id); - root->add_child("Name")->add_child_text (_name); + cxml::add_text_child(root, "Id", _id); + cxml::add_text_child(root, "Name", _name); for (auto i: _spl) { - i.as_xml (root->add_child("Entry")); + i.as_xml(cxml::add_child(root, "Entry")); } doc.write_to_file_formatted (path.string()); } diff --git a/src/lib/spl_entry.cc b/src/lib/spl_entry.cc index f0b377a56..a77b0b7d5 100644 --- a/src/lib/spl_entry.cc +++ b/src/lib/spl_entry.cc @@ -53,5 +53,5 @@ SPLEntry::SPLEntry (shared_ptr<Content> c) void SPLEntry::as_xml (xmlpp::Element* e) { - e->add_child("Digest")->add_child_text(digest); + cxml::add_text_child(e, "Digest", digest); } 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/string_text_file_content.cc b/src/lib/string_text_file_content.cc index d8c195be7..c6639936c 100644 --- a/src/lib/string_text_file_content.cc +++ b/src/lib/string_text_file_content.cc @@ -122,16 +122,16 @@ StringTextFileContent::technical_summary () const void -StringTextFileContent::as_xml (xmlpp::Node* node, bool with_paths) const +StringTextFileContent::as_xml(xmlpp::Element* element, bool with_paths) const { - node->add_child("Type")->add_child_text("TextSubtitle"); - Content::as_xml (node, with_paths); + cxml::add_text_child(element, "Type", "TextSubtitle"); + Content::as_xml(element, with_paths); if (only_text()) { - only_text()->as_xml(node); + only_text()->as_xml(element); } - node->add_child("Length")->add_child_text(raw_convert<string>(_length.get ())); + cxml::add_text_child(element, "Length", raw_convert<string>(_length.get())); } diff --git a/src/lib/string_text_file_content.h b/src/lib/string_text_file_content.h index 30f543381..8a5d9946a 100644 --- a/src/lib/string_text_file_content.h +++ b/src/lib/string_text_file_content.h @@ -45,7 +45,7 @@ public: void examine (std::shared_ptr<const Film> film, std::shared_ptr<Job>) override; std::string summary () const override; std::string technical_summary () const override; - void as_xml (xmlpp::Node *, bool with_paths) const override; + void as_xml(xmlpp::Element*, bool with_paths) const override; dcpomatic::DCPTime full_length (std::shared_ptr<const Film> film) const override; dcpomatic::DCPTime approximate_length () const override; std::string identifier () const override; diff --git a/src/lib/subtitle_analysis.cc b/src/lib/subtitle_analysis.cc index ff1969a3a..2968416c6 100644 --- a/src/lib/subtitle_analysis.cc +++ b/src/lib/subtitle_analysis.cc @@ -70,18 +70,18 @@ SubtitleAnalysis::write (boost::filesystem::path path) const auto doc = make_shared<xmlpp::Document>(); xmlpp::Element* root = doc->create_root_node ("SubtitleAnalysis"); - root->add_child("Version")->add_child_text (raw_convert<string>(_current_state_version)); + cxml::add_text_child(root, "Version", raw_convert<string>(_current_state_version)); if (_bounding_box) { - auto bounding_box = root->add_child("BoundingBox"); - bounding_box->add_child("X")->add_child_text(raw_convert<string>(_bounding_box->x)); - bounding_box->add_child("Y")->add_child_text(raw_convert<string>(_bounding_box->y)); - bounding_box->add_child("Width")->add_child_text(raw_convert<string>(_bounding_box->width)); - bounding_box->add_child("Height")->add_child_text(raw_convert<string>(_bounding_box->height)); + auto bounding_box = cxml::add_child(root, "BoundingBox"); + cxml::add_text_child(bounding_box, "X", raw_convert<string>(_bounding_box->x)); + cxml::add_text_child(bounding_box, "Y", raw_convert<string>(_bounding_box->y)); + cxml::add_text_child(bounding_box, "Width", raw_convert<string>(_bounding_box->width)); + cxml::add_text_child(bounding_box, "Height", raw_convert<string>(_bounding_box->height)); } - root->add_child("AnalysisXOffset")->add_child_text(raw_convert<string>(_analysis_x_offset)); - root->add_child("AnalysisYOffset")->add_child_text(raw_convert<string>(_analysis_y_offset)); + cxml::add_text_child(root, "AnalysisXOffset", raw_convert<string>(_analysis_x_offset)); + cxml::add_text_child(root, "AnalysisYOffset", raw_convert<string>(_analysis_y_offset)); doc->write_to_file_formatted (path.string()); } diff --git a/src/lib/text_content.cc b/src/lib/text_content.cc index 92a35b822..cfdaabcbc 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. @@ -353,63 +356,63 @@ TextContent::TextContent (Content* parent, vector<shared_ptr<Content>> c) /** _mutex must not be held on entry */ void -TextContent::as_xml (xmlpp::Node* root) const +TextContent::as_xml(xmlpp::Element* root) const { boost::mutex::scoped_lock lm (_mutex); - auto text = root->add_child ("Text"); + auto text = cxml::add_child(root, "Text"); - text->add_child("Use")->add_child_text (_use ? "1" : "0"); - text->add_child("Burn")->add_child_text (_burn ? "1" : "0"); - text->add_child("XOffset")->add_child_text (raw_convert<string> (_x_offset)); - text->add_child("YOffset")->add_child_text (raw_convert<string> (_y_offset)); - text->add_child("XScale")->add_child_text (raw_convert<string> (_x_scale)); - text->add_child("YScale")->add_child_text (raw_convert<string> (_y_scale)); + cxml::add_text_child(text, "Use", _use ? "1" : "0"); + cxml::add_text_child(text, "Burn", _burn ? "1" : "0"); + cxml::add_text_child(text, "XOffset", raw_convert<string>(_x_offset)); + cxml::add_text_child(text, "YOffset", raw_convert<string>(_y_offset)); + cxml::add_text_child(text, "XScale", raw_convert<string>(_x_scale)); + cxml::add_text_child(text, "YScale", raw_convert<string>(_y_scale)); if (_colour) { - text->add_child("Red")->add_child_text (raw_convert<string> (_colour->r)); - text->add_child("Green")->add_child_text (raw_convert<string> (_colour->g)); - text->add_child("Blue")->add_child_text (raw_convert<string> (_colour->b)); + cxml::add_text_child(text, "Red", raw_convert<string>(_colour->r)); + cxml::add_text_child(text, "Green", raw_convert<string>(_colour->g)); + cxml::add_text_child(text, "Blue", raw_convert<string>(_colour->b)); } if (_effect) { switch (*_effect) { case dcp::Effect::NONE: - text->add_child("Effect")->add_child_text("none"); + cxml::add_text_child(text, "Effect", "none"); break; case dcp::Effect::BORDER: - text->add_child("Effect")->add_child_text("outline"); + cxml::add_text_child(text, "Effect", "outline"); break; case dcp::Effect::SHADOW: - text->add_child("Effect")->add_child_text("shadow"); + cxml::add_text_child(text, "Effect", "shadow"); break; } } if (_effect_colour) { - text->add_child("EffectRed")->add_child_text (raw_convert<string> (_effect_colour->r)); - text->add_child("EffectGreen")->add_child_text (raw_convert<string> (_effect_colour->g)); - text->add_child("EffectBlue")->add_child_text (raw_convert<string> (_effect_colour->b)); + cxml::add_text_child(text, "EffectRed", raw_convert<string>(_effect_colour->r)); + cxml::add_text_child(text, "EffectGreen", raw_convert<string>(_effect_colour->g)); + cxml::add_text_child(text, "EffectBlue", raw_convert<string>(_effect_colour->b)); } - text->add_child("LineSpacing")->add_child_text (raw_convert<string> (_line_spacing)); + cxml::add_text_child(text, "LineSpacing", raw_convert<string>(_line_spacing)); if (_fade_in) { - text->add_child("FadeIn")->add_child_text (raw_convert<string> (_fade_in->get())); + cxml::add_text_child(text, "FadeIn", raw_convert<string>(_fade_in->get())); } if (_fade_out) { - text->add_child("FadeOut")->add_child_text (raw_convert<string> (_fade_out->get())); + cxml::add_text_child(text, "FadeOut", raw_convert<string>(_fade_out->get())); } - text->add_child("OutlineWidth")->add_child_text (raw_convert<string> (_outline_width)); + cxml::add_text_child(text, "OutlineWidth", raw_convert<string>(_outline_width)); for (auto i: _fonts) { - i->as_xml (text->add_child("Font")); + i->as_xml(cxml::add_child(text, "Font")); } - text->add_child("Type")->add_child_text (text_type_to_string(_type)); - text->add_child("OriginalType")->add_child_text (text_type_to_string(_original_type)); + cxml::add_text_child(text, "Type", text_type_to_string(_type)); + cxml::add_text_child(text, "OriginalType", text_type_to_string(_original_type)); if (_dcp_track) { - _dcp_track->as_xml(text->add_child("DCPTrack")); + _dcp_track->as_xml(cxml::add_child(text, "DCPTrack")); } if (_language) { - auto lang = text->add_child("Language"); + auto lang = cxml::add_child(text, "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/text_content.h b/src/lib/text_content.h index 4d4bdc507..312227120 100644 --- a/src/lib/text_content.h +++ b/src/lib/text_content.h @@ -72,7 +72,7 @@ public: TextContent (Content* parent, std::vector<std::shared_ptr<Content>>); TextContent (Content* parent, cxml::ConstNodePtr, int version, std::list<std::string>& notes); - void as_xml (xmlpp::Node *) const; + void as_xml(xmlpp::Element*) const; std::string identifier () const; void take_settings_from (std::shared_ptr<const TextContent> c); 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/types.h b/src/lib/types.h index 36059401e..c9c87bae5 100644 --- a/src/lib/types.h +++ b/src/lib/types.h @@ -107,7 +107,8 @@ enum class ReelType { SINGLE, BY_VIDEO_CONTENT, - BY_LENGTH + BY_LENGTH, + CUSTOM }; diff --git a/src/lib/util.cc b/src/lib/util.cc index fe6602de3..ef15b90e5 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) @@ -1124,6 +1120,33 @@ word_wrap(string input, int columns) } +#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 + string screen_names_to_string(vector<string> names) { diff --git a/src/lib/util.h b/src/lib/util.h index fd6fd6164..4f64369d3 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,6 +98,22 @@ 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; +} + extern std::string screen_names_to_string(std::vector<std::string> names); #endif diff --git a/src/lib/video_content.cc b/src/lib/video_content.cc index 6c027ff11..10dd5ff1e 100644 --- a/src/lib/video_content.cc +++ b/src/lib/video_content.cc @@ -273,37 +273,37 @@ VideoContent::VideoContent (Content* parent, vector<shared_ptr<Content> > c) void -VideoContent::as_xml (xmlpp::Node* node) const +VideoContent::as_xml(xmlpp::Element* element) const { boost::mutex::scoped_lock lm (_mutex); - node->add_child("Use")->add_child_text (_use ? "1" : "0"); - node->add_child("VideoLength")->add_child_text (raw_convert<string> (_length)); + cxml::add_text_child(element, "Use", _use ? "1" : "0"); + cxml::add_text_child(element, "VideoLength", raw_convert<string>(_length)); if (_size) { - node->add_child("VideoWidth")->add_child_text(raw_convert<string>(_size->width)); - node->add_child("VideoHeight")->add_child_text(raw_convert<string>(_size->height)); + cxml::add_text_child(element, "VideoWidth", raw_convert<string>(_size->width)); + cxml::add_text_child(element, "VideoHeight", raw_convert<string>(_size->height)); } - node->add_child("VideoFrameType")->add_child_text (video_frame_type_to_string (_frame_type)); + cxml::add_text_child(element, "VideoFrameType", video_frame_type_to_string(_frame_type)); if (_sample_aspect_ratio) { - node->add_child("SampleAspectRatio")->add_child_text (raw_convert<string> (_sample_aspect_ratio.get ())); + cxml::add_text_child(element, "SampleAspectRatio", raw_convert<string> (_sample_aspect_ratio.get ())); } - _crop.as_xml (node); + _crop.as_xml(element); if (_custom_ratio) { - node->add_child("CustomRatio")->add_child_text(raw_convert<string>(*_custom_ratio)); + cxml::add_text_child(element, "CustomRatio", raw_convert<string>(*_custom_ratio)); } if (_custom_size) { - node->add_child("CustomWidth")->add_child_text(raw_convert<string>(_custom_size->width)); - node->add_child("CustomHeight")->add_child_text(raw_convert<string>(_custom_size->height)); + cxml::add_text_child(element, "CustomWidth", raw_convert<string>(_custom_size->width)); + cxml::add_text_child(element, "CustomHeight", raw_convert<string>(_custom_size->height)); } if (_colour_conversion) { - _colour_conversion.get().as_xml (node->add_child("ColourConversion")); + _colour_conversion.get().as_xml(cxml::add_child(element, "ColourConversion")); } - node->add_child("YUV")->add_child_text (_yuv ? "1" : "0"); - node->add_child("FadeIn")->add_child_text (raw_convert<string> (_fade_in)); - node->add_child("FadeOut")->add_child_text (raw_convert<string> (_fade_out)); - node->add_child("Range")->add_child_text(_range == VideoRange::FULL ? "full" : "video"); - _pixel_quanta.as_xml(node->add_child("PixelQuanta")); + cxml::add_text_child(element, "YUV", _yuv ? "1" : "0"); + cxml::add_text_child(element, "FadeIn", raw_convert<string>(_fade_in)); + cxml::add_text_child(element, "FadeOut", raw_convert<string>(_fade_out)); + cxml::add_text_child(element, "Range", _range == VideoRange::FULL ? "full" : "video"); + _pixel_quanta.as_xml(cxml::add_child(element, "PixelQuanta")); if (_burnt_subtitle_language) { - node->add_child("BurntSubtitleLanguage")->add_child_text(_burnt_subtitle_language->to_string()); + cxml::add_text_child(element, "BurntSubtitleLanguage", _burnt_subtitle_language->to_string()); } } @@ -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 495d000e1..eb106cc75 100644 --- a/src/lib/video_content.h +++ b/src/lib/video_content.h @@ -66,7 +66,7 @@ public: VideoContent (Content* parent, cxml::ConstNodePtr node, int version, VideoRange video_range_hint); VideoContent (Content* parent, std::vector<std::shared_ptr<Content>>); - void as_xml (xmlpp::Node *) const; + void as_xml(xmlpp::Element*) const; std::string technical_summary () const; std::string identifier () const; void take_settings_from (std::shared_ptr<const VideoContent> c); @@ -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_content.cc b/src/lib/video_mxf_content.cc index 9adca5a2d..546e1445b 100644 --- a/src/lib/video_mxf_content.cc +++ b/src/lib/video_mxf_content.cc @@ -121,11 +121,11 @@ VideoMXFContent::identifier () const void -VideoMXFContent::as_xml (xmlpp::Node* node, bool with_paths) const +VideoMXFContent::as_xml(xmlpp::Element* element, bool with_paths) const { - node->add_child("Type")->add_child_text("VideoMXF"); - Content::as_xml (node, with_paths); - video->as_xml (node); + cxml::add_text_child(element, "Type", "VideoMXF"); + Content::as_xml(element, with_paths); + video->as_xml(element); } diff --git a/src/lib/video_mxf_content.h b/src/lib/video_mxf_content.h index 5a04c3da9..1731942a6 100644 --- a/src/lib/video_mxf_content.h +++ b/src/lib/video_mxf_content.h @@ -40,7 +40,7 @@ public: std::string summary () const override; std::string technical_summary () const override; std::string identifier () const override; - void as_xml (xmlpp::Node* node, bool with_paths) const override; + void as_xml(xmlpp::Element* element, bool with_paths) const override; dcpomatic::DCPTime full_length (std::shared_ptr<const Film> film) const override; dcpomatic::DCPTime approximate_length () const override; void add_properties (std::shared_ptr<const Film> film, std::list<UserProperty>& p) const override; 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 7dd3bfe35..878b503a8 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' diff --git a/src/tools/dcpomatic.cc b/src/tools/dcpomatic.cc index 1b1ef0629..d516c6f5b 100644 --- a/src/tools/dcpomatic.cc +++ b/src/tools/dcpomatic.cc @@ -26,6 +26,7 @@ #include "wx/about_dialog.h" #include "wx/content_panel.h" +#include "wx/dcp_referencing_dialog.h" #include "wx/dkdm_dialog.h" #include "wx/export_subtitles_dialog.h" #include "wx/export_video_file_dialog.h" @@ -77,6 +78,9 @@ #include "lib/ffmpeg_encoder.h" #include "lib/film.h" #include "lib/font_config.h" +#ifdef DCPOMATIC_GROK +#include "lib/grok/context.h" +#endif #include "lib/hints.h" #include "lib/job_manager.h" #include "lib/kdm_with_metadata.h" @@ -208,6 +212,7 @@ private: #define NEEDS_SELECTED_VIDEO_CONTENT 0x20 #define NEEDS_CLIPBOARD 0x40 #define NEEDS_ENCRYPTION 0x80 +#define NEEDS_DCP_CONTENT 0x100 map<wxMenuItem*, int> menu_items; @@ -237,6 +242,7 @@ enum { ID_jobs_open_dcp_in_player, ID_view_closed_captions, ID_view_video_waveform, + ID_tools_version_file, ID_tools_hints, ID_tools_encoding_servers, ID_tools_manage_templates, @@ -349,6 +355,7 @@ public: Bind (wxEVT_MENU, boost::bind (&DOMFrame::jobs_open_dcp_in_player, this), ID_jobs_open_dcp_in_player); Bind (wxEVT_MENU, boost::bind (&DOMFrame::view_closed_captions, this), ID_view_closed_captions); Bind (wxEVT_MENU, boost::bind (&DOMFrame::view_video_waveform, this), ID_view_video_waveform); + Bind (wxEVT_MENU, boost::bind (&DOMFrame::tools_version_file, this), ID_tools_version_file); Bind (wxEVT_MENU, boost::bind (&DOMFrame::tools_hints, this), ID_tools_hints); Bind (wxEVT_MENU, boost::bind (&DOMFrame::tools_encoding_servers, this), ID_tools_encoding_servers); Bind (wxEVT_MENU, boost::bind (&DOMFrame::tools_manage_templates, this), ID_tools_manage_templates); @@ -461,6 +468,7 @@ public: { auto film = make_shared<Film>(file); auto const notes = film->read_metadata (); + film->read_ui_state(); if (film->state_version() == 4) { error_dialog ( @@ -1074,6 +1082,17 @@ private: _system_information_dialog->Show (); } + void tools_version_file() + { + if (_dcp_referencing_dialog) { + _dcp_referencing_dialog->Destroy(); + _dcp_referencing_dialog = nullptr; + } + + _dcp_referencing_dialog = new DCPReferencingDialog(this, _film); + _dcp_referencing_dialog->Show(); + } + void tools_hints () { if (!_hints_dialog) { @@ -1202,6 +1221,7 @@ private: FontConfig::drop(); ev.Skip (); + JobManager::drop (); } void active_jobs_changed() @@ -1225,6 +1245,13 @@ private: bool const have_single_selected_content = _film_editor->content_panel()->selected().size() == 1; bool const have_selected_content = !_film_editor->content_panel()->selected().empty(); bool const have_selected_video_content = !_film_editor->content_panel()->selected_video().empty(); + vector<shared_ptr<Content>> content; + if (_film) { + content = _film->content(); + } + bool const have_dcp_content = std::find_if(content.begin(), content.end(), [](shared_ptr<const Content> content) { + return static_cast<bool>(dynamic_pointer_cast<const DCPContent>(content)); + }) != content.end(); for (auto j: menu_items) { @@ -1262,6 +1289,10 @@ private: enabled = false; } + if ((j.second & NEEDS_DCP_CONTENT) && !have_dcp_content) { + enabled = false; + } + j.first->Enable (enabled); } } @@ -1395,6 +1426,7 @@ private: add_item (view, _("Video waveform..."), ID_view_video_waveform, NEEDS_FILM); auto tools = new wxMenu; + add_item (tools, _("Version File (VF)..."), ID_tools_version_file, NEEDS_FILM | NEEDS_DCP_CONTENT); add_item (tools, _("Hints..."), ID_tools_hints, NEEDS_FILM); add_item (tools, _("Encoding servers..."), ID_tools_encoding_servers, 0); add_item (tools, _("Manage templates..."), ID_tools_manage_templates, 0); @@ -1500,6 +1532,12 @@ private: _history_items = history.size (); dcpomatic_log->set_types (Config::instance()->log_types()); + +#ifdef DCPOMATIC_GROK + if (what == Config::GROK) { + setup_grok_library_path(); + } +#endif } void update_checker_state_changed () @@ -1581,6 +1619,7 @@ private: StandardControls* _controls; wx_ptr<VideoWaveformDialog> _video_waveform_dialog; SystemInformationDialog* _system_information_dialog = nullptr; + DCPReferencingDialog* _dcp_referencing_dialog = nullptr; HintsDialog* _hints_dialog = nullptr; ServersListDialog* _servers_list_dialog = nullptr; wxPreferencesEditor* _config_dialog = nullptr; @@ -1748,6 +1787,11 @@ private: notes.Centre(); notes.ShowModal(); } + +#ifdef DCPOMATIC_GROK + grk_plugin::setMessengerLogger(new grk_plugin::GrokLogger("[GROK] ")); + setup_grok_library_path(); +#endif } catch (exception& e) { diff --git a/src/tools/dcpomatic_batch.cc b/src/tools/dcpomatic_batch.cc index e476c2163..1e383b39a 100644 --- a/src/tools/dcpomatic_batch.cc +++ b/src/tools/dcpomatic_batch.cc @@ -32,6 +32,9 @@ #include "lib/config.h" #include "lib/dcpomatic_socket.h" #include "lib/film.h" +#ifdef DCPOMATIC_GROK +#include "lib/grok/context.h" +#endif #include "lib/job.h" #include "lib/job_manager.h" #include "lib/make_dcp.h" @@ -289,6 +292,7 @@ private: } ev.Skip (); + JobManager::drop (); } void file_add_film () @@ -378,6 +382,12 @@ private: ); } } + +#ifdef DCPOMATIC_GROK + if (what == Config::GROK) { + setup_grok_library_path(); + } +#endif } boost::optional<boost::filesystem::path> _last_parent; @@ -498,6 +508,11 @@ class App : public wxApp } } +#ifdef DCPOMATIC_GROK + grk_plugin::setMessengerLogger(new grk_plugin::GrokLogger("[GROK] ")); + setup_grok_library_path(); +#endif + return true; } diff --git a/src/tools/dcpomatic_cli.cc b/src/tools/dcpomatic_cli.cc index 65252ff29..c2e7ee76c 100644 --- a/src/tools/dcpomatic_cli.cc +++ b/src/tools/dcpomatic_cli.cc @@ -28,6 +28,9 @@ #include "lib/ffmpeg_encoder.h" #include "lib/film.h" #include "lib/filter.h" +#ifdef DCPOMATIC_GROK +#include "lib/grok/context.h" +#endif #include "lib/hints.h" #include "lib/job_manager.h" #include "lib/json_server.h" @@ -414,7 +417,7 @@ main (int argc, char* argv[]) signal_manager = new SignalManager (); if (no_remote || export_format) { - EncodeServerFinder::instance()->stop (); + EncodeServerFinder::drop(); } if (json_port) { @@ -497,6 +500,11 @@ main (int argc, char* argv[]) } } +#ifdef DCPOMATIC_GROK + grk_plugin::setMessengerLogger(new grk_plugin::GrokLogger("[GROK] ")); + setup_grok_library_path(); +#endif + if (progress) { if (export_format) { cout << "\nExporting " << film->name() << "\n"; diff --git a/src/tools/dcpomatic_disk.cc b/src/tools/dcpomatic_disk.cc index 395b60a88..0cc7ccbcd 100644 --- a/src/tools/dcpomatic_disk.cc +++ b/src/tools/dcpomatic_disk.cc @@ -43,6 +43,7 @@ #include <dcp/filesystem.h> #include <dcp/warnings.h> #include <wx/cmdline.h> +#include <wx/progdlg.h> #include <wx/wx.h> LIBDCP_DISABLE_WARNINGS #include <boost/process.hpp> @@ -130,7 +131,7 @@ public: auto grid = new wxGridBagSizer (DCPOMATIC_SIZER_X_GAP, DCPOMATIC_SIZER_Y_GAP); int r = 0; - add_label_to_sizer (grid, overall_panel, _("DCP"), true, wxGBPosition(r, 0)); + add_label_to_sizer(grid, overall_panel, _("DCPs"), true, wxGBPosition(r, 0)); auto dcp_sizer = new wxBoxSizer (wxHORIZONTAL); auto dcps = new EditableList<boost::filesystem::path, DirDialogWrapper>( overall_panel, @@ -149,9 +150,9 @@ public: add_label_to_sizer (grid, overall_panel, _("Drive"), true, wxGBPosition(r, 0)); auto drive_sizer = new wxBoxSizer (wxHORIZONTAL); _drive = new wxChoice (overall_panel, wxID_ANY); - drive_sizer->Add (_drive, 1, wxALIGN_CENTER_VERTICAL | wxRIGHT, DCPOMATIC_SIZER_X_GAP); + drive_sizer->Add(_drive, 1, wxTOP, 2); _drive_refresh = new wxButton (overall_panel, wxID_ANY, _("Refresh")); - drive_sizer->Add (_drive_refresh, 0); + drive_sizer->Add(_drive_refresh, 0, wxLEFT, DCPOMATIC_SIZER_X_GAP); grid->Add (drive_sizer, wxGBPosition(r, 1), wxDefaultSpan, wxEXPAND); ++r; @@ -159,7 +160,7 @@ public: grid->Add (_jobs, wxGBPosition(r, 0), wxGBSpan(6, 2), wxEXPAND); r += 6; - _copy = new wxButton (overall_panel, wxID_ANY, _("Copy DCP")); + _copy = new wxButton(overall_panel, wxID_ANY, _("Copy DCPs")); grid->Add (_copy, wxGBPosition(r, 0), wxGBSpan(1, 2), wxEXPAND); ++r; @@ -172,7 +173,7 @@ public: _sizer->Add (grid, 1, wxALL | wxEXPAND, DCPOMATIC_DIALOG_BORDER); overall_panel->SetSizer (_sizer); Fit (); - SetSize (1024, GetSize().GetHeight() + 32); + SetSize(768, GetSize().GetHeight() + 32); /* XXX: this is a hack, but I expect we'll need logs and I'm not sure if there's * a better place to put them. @@ -181,7 +182,17 @@ public: dcpomatic_log->set_types (dcpomatic_log->types() | LogEntry::TYPE_DISK); LOG_DISK("dcpomatic_disk %1 started", dcpomatic_git_commit); - drive_refresh (); + { + int constexpr seconds_to_look = 3; + wxProgressDialog find_drives_progress(_("Disk Writer"), _("Finding disks"), seconds_to_look * 4, this); + for (auto i = 0; i < seconds_to_look * 4; ++i) { + if (!find_drives_progress.Update(i)) { + break; + } + drive_refresh(); + dcpomatic_sleep_milliseconds(250); + } + } Bind (wxEVT_SIZE, boost::bind(&DOMFrame::sized, this, _1)); Bind (wxEVT_CLOSE_WINDOW, boost::bind(&DOMFrame::close, this, _1)); @@ -270,6 +281,7 @@ private: } ev.Skip (); + JobManager::drop (); } void copy () diff --git a/src/tools/dcpomatic_player.cc b/src/tools/dcpomatic_player.cc index e65839521..a68cc074a 100644 --- a/src/tools/dcpomatic_player.cc +++ b/src/tools/dcpomatic_player.cc @@ -616,12 +616,14 @@ private: help->Append (ID_help_report_a_problem, _("Report a problem...")); m->Append (_file_menu, _("&File")); + if (!Config::instance()->player_restricted_menus()) { #ifndef __WXOSX__ - m->Append (edit, _("&Edit")); + m->Append (edit, _("&Edit")); #endif - m->Append (view, _("&View")); - m->Append (tools, _("&Tools")); - m->Append (help, _("&Help")); + m->Append (view, _("&View")); + m->Append (tools, _("&Tools")); + m->Append (help, _("&Help")); + } } void file_open () diff --git a/src/tools/dcpomatic_playlist.cc b/src/tools/dcpomatic_playlist.cc index a373d81e6..b16a2be68 100644 --- a/src/tools/dcpomatic_playlist.cc +++ b/src/tools/dcpomatic_playlist.cc @@ -567,34 +567,36 @@ private: void setup_menu (wxMenuBar* m) { - auto file = new wxMenu; -#ifdef __WXOSX__ - file->Append(wxID_PREFERENCES, _("&Preferences...\tCtrl-,")); - file->Append (wxID_EXIT, _("&Exit")); +#ifdef DCPOMATIC_OSX + auto help = new wxMenu; + /* These just need to be appended somewhere, it seems - they magically + * get moved to the right place. + */ + if (!Config::instance()->playlist_editor_restricted_menus()) { + help->Append(wxID_PREFERENCES, _("&Preferences...\tCtrl-,")); + } + help->Append(wxID_EXIT, _("&Exit")); + help->Append(wxID_ABOUT, _("About DCP-o-matic")); + + m->Append(help, _("&Help")); #else - file->Append (wxID_EXIT, _("&Quit")); -#endif + auto file = new wxMenu; + file->Append(wxID_EXIT, _("&Quit")); -#ifndef __WXOSX__ auto edit = new wxMenu; - edit->Append (wxID_PREFERENCES, _("&Preferences...\tCtrl-P")); -#endif + edit->Append(wxID_PREFERENCES, _("&Preferences...\tCtrl-P")); auto help = new wxMenu; -#ifdef __WXOSX__ - help->Append (wxID_ABOUT, _("About DCP-o-matic")); -#else - help->Append (wxID_ABOUT, _("About")); -#endif + help->Append(wxID_ABOUT, _("About")); - m->Append (file, _("&File")); -#ifndef __WXOSX__ - m->Append (edit, _("&Edit")); + m->Append(file, _("&File")); + if (!Config::instance()->playlist_editor_restricted_menus()) { + m->Append(edit, _("&Edit")); + m->Append(help, _("&Help")); + } #endif - m->Append (help, _("&Help")); } - void config_changed () { try { diff --git a/src/tools/dcpomatic_server.cc b/src/tools/dcpomatic_server.cc index 9bdc688c8..622704d21 100644 --- a/src/tools/dcpomatic_server.cc +++ b/src/tools/dcpomatic_server.cc @@ -23,6 +23,11 @@ #include "wx/wx_signal_manager.h" #include "wx/wx_util.h" #include "lib/config.h" +#ifdef DCPOMATIC_GROK +#include "lib/grok/context.h" +#endif +#include "lib/log.h" +#include "lib/signaller.h" #include "lib/cross.h" #include "lib/dcpomatic_log.h" #include "lib/encode_server.h" @@ -327,6 +332,11 @@ private: SetExitOnFrameDelete (false); +#ifdef DCPOMATIC_GROK + grk_plugin::setMessengerLogger(new grk_plugin::GrokLogger("[GROK] ")); + setup_grok_library_path(); +#endif + return true; } diff --git a/src/tools/dcpomatic_server_cli.cc b/src/tools/dcpomatic_server_cli.cc index 6d7f6aba7..8997bb92c 100644 --- a/src/tools/dcpomatic_server_cli.cc +++ b/src/tools/dcpomatic_server_cli.cc @@ -25,6 +25,9 @@ #include "lib/config.h" #include "lib/image.h" #include "lib/file_log.h" +#ifdef DCPOMATIC_GROK +#include "lib/grok/context.h" +#endif #include "lib/null_log.h" #include "lib/version.h" #include "lib/encode_server.h" @@ -109,6 +112,11 @@ main (int argc, char* argv[]) dcpomatic_log.reset (new FileLog("dcpomatic_server_cli.log")); } +#ifdef DCPOMATIC_GROK + setMessengerLogger(new grk_plugin::GrokLogger("[GROK] ")); + setup_grok_library_path(); +#endif + EncodeServer server (verbose, num_threads); try { diff --git a/src/wx/about_dialog.cc b/src/wx/about_dialog.cc index 2c742cce3..8b1f8a038 100644 --- a/src/wx/about_dialog.cc +++ b/src/wx/about_dialog.cc @@ -86,7 +86,7 @@ AboutDialog::AboutDialog (wxWindow* parent) t = new StaticText ( this, - _("(C) 2012-2024 Carl Hetherington, Terrence Meiczinger\n Ole Laursen"), + _("(C) 2012-2024 Carl Hetherington, Terrence Meiczinger\nOle Laursen, Aaron Boxer"), wxDefaultPosition, wxDefaultSize, wxALIGN_CENTER ); @@ -99,6 +99,7 @@ AboutDialog::AboutDialog (wxWindow* parent) written_by.Add (wxT ("Terrence Meiczinger")); written_by.Add (wxT ("Mart Jansink")); written_by.Add (wxT ("Ole Laursen")); + written_by.Add (wxT ("Aaron Boxer")); add_section (_("Written by"), written_by); wxArrayString with_help_from; diff --git a/src/wx/audio_panel.cc b/src/wx/audio_panel.cc index fcadd0c68..34923fb02 100644 --- a/src/wx/audio_panel.cc +++ b/src/wx/audio_panel.cc @@ -71,14 +71,6 @@ AudioPanel::AudioPanel (ContentPanel* p) void AudioPanel::create () { - _reference = new CheckBox (this, _("Use this DCP's audio as OV and make VF")); - _reference_note = new StaticText (this, wxT("")); - _reference_note->Wrap (200); - auto font = _reference_note->GetFont(); - font.SetStyle(wxFONTSTYLE_ITALIC); - font.SetPointSize(font.GetPointSize() - 1); - _reference_note->SetFont(font); - _show = new Button (this, _("Show graph of audio levels...")); _peak = new StaticText (this, wxT ("")); @@ -121,6 +113,9 @@ AudioPanel::create () _description = new StaticText (this, wxT(" \n"), wxDefaultPosition, wxDefaultSize); _sizer->Add (_description, 0, wxALL, 12); + auto font = _description->GetFont(); + font.SetStyle(wxFONTSTYLE_ITALIC); + font.SetPointSize(font.GetPointSize() - 1); _description->SetFont (font); _gain->wrapped()->SetRange (-60, 60); @@ -133,7 +128,6 @@ AudioPanel::create () film_changed(FilmProperty::VIDEO_FRAME_RATE); film_changed(FilmProperty::REEL_TYPE); - _reference->bind(&AudioPanel::reference_clicked, this); _show->Bind (wxEVT_BUTTON, boost::bind (&AudioPanel::show_clicked, this)); _gain_calculate_button->Bind (wxEVT_BUTTON, boost::bind (&AudioPanel::gain_calculate_button_clicked, this)); @@ -155,12 +149,6 @@ AudioPanel::add_to_grid () { int r = 0; - auto reference_sizer = new wxBoxSizer (wxVERTICAL); - reference_sizer->Add (_reference, 0); - reference_sizer->Add (_reference_note, 0); - _grid->Add (reference_sizer, wxGBPosition(r, 0), wxGBSpan(1, 4)); - ++r; - _grid->Add (_show, wxGBPosition (r, 0), wxGBSpan (1, 2)); _grid->Add (_peak, wxGBPosition (r, 2), wxGBSpan (1, 2), wxALIGN_CENTER_VERTICAL); ++r; @@ -258,15 +246,6 @@ AudioPanel::film_content_changed (int property) /* This is a bit aggressive but probably not so bad */ _peak_cache.clear(); setup_peak (); - } else if (property == DCPContentProperty::REFERENCE_AUDIO) { - if (ac.size() == 1) { - shared_ptr<DCPContent> dcp = dynamic_pointer_cast<DCPContent> (ac.front ()); - checked_set (_reference, dcp ? dcp->reference_audio () : false); - } else { - checked_set (_reference, false); - } - - setup_sensitivity (); } else if (property == ContentProperty::VIDEO_FRAME_RATE) { setup_description (); } else if (property == AudioContentProperty::FADE_IN) { @@ -384,19 +363,8 @@ AudioPanel::setup_sensitivity () dcp = dynamic_pointer_cast<DCPContent> (sel.front ()); } - string why_not; - bool const can_reference = dcp && dcp->can_reference_audio (_parent->film(), why_not); - wxString cannot; - if (why_not.empty()) { - cannot = _("Cannot reference this DCP's audio."); - } else { - cannot = _("Cannot reference this DCP's audio: ") + std_to_wx(why_not); - } - setup_refer_button (_reference, _reference_note, dcp, can_reference, cannot); - - auto const ref = _reference->GetValue(); + auto const ref = dcp && dcp->reference_audio(); auto const single = sel.size() == 1; - auto const all_have_video = std::all_of(sel.begin(), sel.end(), [](shared_ptr<const Content> c) { return static_cast<bool>(c->video); }); _gain->wrapped()->Enable (!ref); @@ -501,23 +469,6 @@ AudioPanel::active_jobs_changed (optional<string> old_active, optional<string> n void -AudioPanel::reference_clicked () -{ - auto c = _parent->selected (); - if (c.size() != 1) { - return; - } - - auto d = dynamic_pointer_cast<DCPContent>(c.front()); - if (!d) { - return; - } - - d->set_reference_audio (_reference->GetValue ()); -} - - -void AudioPanel::set_film (shared_ptr<Film>) { /* We are changing film, so destroy any audio dialog for the old one */ diff --git a/src/wx/audio_panel.h b/src/wx/audio_panel.h index 61dd2783a..d25f7e247 100644 --- a/src/wx/audio_panel.h +++ b/src/wx/audio_panel.h @@ -54,15 +54,12 @@ private: void setup_peak (); void active_jobs_changed (boost::optional<std::string>, boost::optional<std::string>); void setup_sensitivity (); - void reference_clicked (); void add_to_grid () override; boost::optional<float> peak () const; void fade_in_changed (); void fade_out_changed (); void use_same_fades_as_video_changed (); - CheckBox* _reference; - wxStaticText* _reference_note; wxButton* _show; wxStaticText* _gain_label; wxStaticText* _gain_db_label; diff --git a/src/wx/check_box.cc b/src/wx/check_box.cc index baec651c1..c18ceba47 100644 --- a/src/wx/check_box.cc +++ b/src/wx/check_box.cc @@ -53,3 +53,10 @@ CheckBox::get() const return GetValue(); } + +void +CheckBox::set(bool state) +{ + SetValue(state); +} + diff --git a/src/wx/check_box.h b/src/wx/check_box.h index 4b3fddb9f..d9feb00c9 100644 --- a/src/wx/check_box.h +++ b/src/wx/check_box.h @@ -39,6 +39,7 @@ public: void set_text (wxString text) override; wxString get_text () const override; bool get() const; + void set(bool state); template <typename... Args> void bind(Args... args) { diff --git a/src/wx/cinema_dialog.cc b/src/wx/cinema_dialog.cc index 84fde5f41..771a59892 100644 --- a/src/wx/cinema_dialog.cc +++ b/src/wx/cinema_dialog.cc @@ -36,7 +36,7 @@ using namespace boost::placeholders; #endif -CinemaDialog::CinemaDialog(wxWindow* parent, wxString title, string name, vector<string> emails, string notes, int utc_offset_hour, int utc_offset_minute) +CinemaDialog::CinemaDialog(wxWindow* parent, wxString title, string name, vector<string> emails, string notes) : wxDialog (parent, wxID_ANY, title) { auto overall_sizer = new wxBoxSizer (wxVERTICAL); @@ -50,11 +50,6 @@ CinemaDialog::CinemaDialog(wxWindow* parent, wxString title, string name, vector sizer->Add (_name, wxGBPosition(r, 1)); ++r; - add_label_to_sizer (sizer, this, _("UTC offset (time zone)"), true, wxGBPosition(r, 0)); - _utc_offset = new wxChoice (this, wxID_ANY); - sizer->Add (_utc_offset, wxGBPosition(r, 1)); - ++r; - add_label_to_sizer (sizer, this, _("Notes"), true, wxGBPosition(r, 0)); _notes = new wxTextCtrl (this, wxID_ANY, std_to_wx(notes), wxDefaultPosition, wxSize(500, -1)); sizer->Add (_notes, wxGBPosition(r, 1)); @@ -83,17 +78,6 @@ CinemaDialog::CinemaDialog(wxWindow* parent, wxString title, string name, vector overall_sizer->Add (buttons, wxSizerFlags().Expand().DoubleBorder()); } - /* Default to UTC */ - size_t sel = get_offsets (_offsets); - for (size_t i = 0; i < _offsets.size(); ++i) { - _utc_offset->Append (_offsets[i].name); - if (_offsets[i].hour == utc_offset_hour && _offsets[i].minute == utc_offset_minute) { - sel = i; - } - } - - _utc_offset->SetSelection (sel); - overall_sizer->Layout (); overall_sizer->SetSizeHints (this); @@ -122,30 +106,6 @@ CinemaDialog::emails() const } -int -CinemaDialog::utc_offset_hour () const -{ - int const sel = _utc_offset->GetSelection(); - if (sel < 0 || sel > int(_offsets.size())) { - return 0; - } - - return _offsets[sel].hour; -} - - -int -CinemaDialog::utc_offset_minute () const -{ - int const sel = _utc_offset->GetSelection(); - if (sel < 0 || sel > int(_offsets.size())) { - return 0; - } - - return _offsets[sel].minute; -} - - string CinemaDialog::notes () const { diff --git a/src/wx/cinema_dialog.h b/src/wx/cinema_dialog.h index 0b8362843..cd90f6dea 100644 --- a/src/wx/cinema_dialog.h +++ b/src/wx/cinema_dialog.h @@ -38,16 +38,12 @@ public: wxString, std::string name = "", std::vector<std::string> emails = std::vector<std::string>(), - std::string notes = "", - int utc_offset_hour = 0, - int utc_offset_minute = 0 + std::string notes = "" ); std::string name () const; std::string notes () const; std::vector<std::string> emails () const; - int utc_offset_hour () const; - int utc_offset_minute () const; private: void set_emails (std::vector<std::string>); @@ -56,6 +52,4 @@ private: wxTextCtrl* _notes; EditableList<std::string, EmailDialog>* _email_list; std::vector<std::string> _emails; - wxChoice* _utc_offset; - std::vector<Offset> _offsets; }; diff --git a/src/wx/colours.h b/src/wx/colours.h new file mode 100644 index 000000000..0d33208e0 --- /dev/null +++ b/src/wx/colours.h @@ -0,0 +1,25 @@ +/* + 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/>. + +*/ + + +#define VIDEO_CONTENT_COLOUR (wxColour(242, 92, 120, 255)) +#define AUDIO_CONTENT_COLOUR (wxColour(149, 121, 232, 255)) +#define TEXT_CONTENT_COLOUR (wxColour(163, 255, 154, 255)) +#define ATMOS_CONTENT_COLOUR (wxColour(149, 121, 232, 255)) diff --git a/src/wx/content_menu.cc b/src/wx/content_menu.cc index 642457d93..92a68e3b3 100644 --- a/src/wx/content_menu.cc +++ b/src/wx/content_menu.cc @@ -23,13 +23,13 @@ #include "content_advanced_dialog.h" #include "content_menu.h" #include "content_properties_dialog.h" +#include "content_timeline_audio_view.h" +#include "content_timeline_video_view.h" #include "dir_dialog.h" #include "file_dialog.h" #include "film_viewer.h" #include "id.h" #include "repeat_dialog.h" -#include "timeline_video_content_view.h" -#include "timeline_audio_content_view.h" #include "wx_util.h" #include "lib/audio_content.h" #include "lib/config.h" @@ -314,12 +314,12 @@ ContentMenu::remove () continue; } - shared_ptr<TimelineVideoContentView> video; - shared_ptr<TimelineAudioContentView> audio; + shared_ptr<ContentTimelineVideoView> video; + shared_ptr<ContentTimelineAudioView> audio; for (auto j: _views) { - auto v = dynamic_pointer_cast<TimelineVideoContentView>(j); - auto a = dynamic_pointer_cast<TimelineAudioContentView>(j); + auto v = dynamic_pointer_cast<ContentTimelineVideoView>(j); + auto a = dynamic_pointer_cast<ContentTimelineAudioView>(j); if (v && v->content() == fc) { video = v; } else if (a && a->content() == fc) { diff --git a/src/wx/content_panel.cc b/src/wx/content_panel.cc index 9e73900fc..ed9c15c83 100644 --- a/src/wx/content_panel.cc +++ b/src/wx/content_panel.cc @@ -21,13 +21,13 @@ #include "audio_panel.h" #include "content_panel.h" +#include "content_timeline_dialog.h" #include "dcpomatic_button.h" #include "dir_dialog.h" #include "file_dialog.h" #include "film_viewer.h" #include "image_sequence_dialog.h" #include "text_panel.h" -#include "timeline_dialog.h" #include "timing_panel.h" #include "video_panel.h" #include "wx_util.h" diff --git a/src/wx/content_panel.h b/src/wx/content_panel.h index ca0d49719..38b634a62 100644 --- a/src/wx/content_panel.h +++ b/src/wx/content_panel.h @@ -33,12 +33,12 @@ LIBDCP_ENABLE_WARNINGS class AudioPanel; class ContentListCtrl; class ContentSubPanel; +class ContentTimelineDialog; class Film; class FilmEditor; class FilmViewer; class LimitedContentPanelSplitter; class TextPanel; -class TimelineDialog; class TimingPanel; class VideoPanel; class wxListCtrl; @@ -132,7 +132,7 @@ private: EnumIndexedVector<TextPanel*, TextType> _text_panel; TimingPanel* _timing_panel; ContentMenu* _menu; - wx_ptr<TimelineDialog> _timeline_dialog; + wx_ptr<ContentTimelineDialog> _timeline_dialog; wxNotebook* _parent; wxWindow* _last_selected_tab = nullptr; diff --git a/src/wx/content_sub_panel.cc b/src/wx/content_sub_panel.cc index 6bbc9a51f..10fdfa232 100644 --- a/src/wx/content_sub_panel.cc +++ b/src/wx/content_sub_panel.cc @@ -53,29 +53,6 @@ ContentSubPanel::ContentSubPanel (ContentPanel* p, wxString name) } void -ContentSubPanel::setup_refer_button (wxCheckBox* button, wxStaticText* note, shared_ptr<DCPContent> dcp, bool can_reference, wxString cannot) -{ - button->Enable (can_reference); - - if (dcp && !can_reference) { - note->SetLabel (cannot); - } else { - note->SetLabel (wxT("")); - } - - note->Wrap (400); - - if (cannot.IsEmpty()) { - note->Hide (); - } else { - note->Show (); - } - - layout (); -} - - -void ContentSubPanel::layout () { int x; diff --git a/src/wx/content_sub_panel.h b/src/wx/content_sub_panel.h index a3916817e..48970e6a9 100644 --- a/src/wx/content_sub_panel.h +++ b/src/wx/content_sub_panel.h @@ -55,7 +55,6 @@ public: protected: - void setup_refer_button (wxCheckBox* button, wxStaticText* note, std::shared_ptr<DCPContent> dcp, bool can_reference, wxString cannot); void layout (); virtual void add_to_grid () = 0; diff --git a/src/wx/content_timeline.cc b/src/wx/content_timeline.cc new file mode 100644 index 000000000..663f93030 --- /dev/null +++ b/src/wx/content_timeline.cc @@ -0,0 +1,1021 @@ +/* + Copyright (C) 2013-2021 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 "content_panel.h" +#include "content_timeline.h" +#include "film_editor.h" +#include "film_viewer.h" +#include "content_timeline_atmos_view.h" +#include "content_timeline_audio_view.h" +#include "content_timeline_text_view.h" +#include "content_timeline_video_view.h" +#include "timeline_labels_view.h" +#include "timeline_reels_view.h" +#include "timeline_time_axis_view.h" +#include "wx_util.h" +#include "lib/atmos_mxf_content.h" +#include "lib/audio_content.h" +#include "lib/constants.h" +#include "lib/film.h" +#include "lib/image_content.h" +#include "lib/playlist.h" +#include "lib/text_content.h" +#include "lib/timer.h" +#include "lib/video_content.h" +#include <dcp/scope_guard.h> +#include <dcp/warnings.h> +LIBDCP_DISABLE_WARNINGS +#include <wx/graphics.h> +LIBDCP_ENABLE_WARNINGS +#include <iterator> +#include <list> + + +using std::abs; +using std::dynamic_pointer_cast; +using std::list; +using std::make_shared; +using std::max; +using std::min; +using std::shared_ptr; +using std::weak_ptr; +using boost::bind; +using boost::optional; +using namespace dcpomatic; +#if BOOST_VERSION >= 106100 +using namespace boost::placeholders; +#endif + + +int const ContentTimeline::_minimum_pixels_per_track = 16; + + +ContentTimeline::ContentTimeline(wxWindow* parent, ContentPanel* cp, shared_ptr<Film> film, FilmViewer& viewer) + : Timeline(parent) + , _labels_canvas (new wxScrolledCanvas (this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxFULL_REPAINT_ON_RESIZE)) + , _main_canvas (new wxScrolledCanvas (this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxFULL_REPAINT_ON_RESIZE)) + , _content_panel (cp) + , _film (film) + , _viewer (viewer) + , _time_axis_view (new TimelineTimeAxisView (*this, 64)) + , _reels_view (new TimelineReelsView (*this, 32)) + , _labels_view (new TimelineLabelsView (*this)) + , _tracks (0) + , _left_down (false) + , _down_view_position (0) + , _first_move (false) + , _menu (this, viewer) + , _snap (true) + , _tool (SELECT) + , _x_scroll_rate (16) + , _y_scroll_rate (16) + , _pixels_per_track (48) + , _first_resize (true) + , _timer (this) +{ +#ifndef __WXOSX__ + _labels_canvas->SetDoubleBuffered (true); + _main_canvas->SetDoubleBuffered (true); +#endif + + auto sizer = new wxBoxSizer (wxHORIZONTAL); + sizer->Add (_labels_canvas, 0, wxEXPAND); + _labels_canvas->SetMinSize (wxSize (_labels_view->bbox().width, -1)); + sizer->Add (_main_canvas, 1, wxEXPAND); + SetSizer (sizer); + + _labels_canvas->Bind(wxEVT_PAINT, boost::bind(&ContentTimeline::paint_labels, this)); + _main_canvas->Bind (wxEVT_PAINT, boost::bind(&ContentTimeline::paint_main, this)); + _main_canvas->Bind (wxEVT_LEFT_DOWN, boost::bind(&ContentTimeline::left_down, this, _1)); + _main_canvas->Bind (wxEVT_LEFT_UP, boost::bind(&ContentTimeline::left_up, this, _1)); + _main_canvas->Bind (wxEVT_RIGHT_DOWN, boost::bind(&ContentTimeline::right_down, this, _1)); + _main_canvas->Bind (wxEVT_MOTION, boost::bind(&ContentTimeline::mouse_moved, this, _1)); + _main_canvas->Bind (wxEVT_SIZE, boost::bind(&ContentTimeline::resized, this)); + _main_canvas->Bind (wxEVT_MOUSEWHEEL, boost::bind(&ContentTimeline::mouse_wheel_turned, this, _1)); + _main_canvas->Bind (wxEVT_SCROLLWIN_TOP, boost::bind(&ContentTimeline::scrolled, this, _1)); + _main_canvas->Bind (wxEVT_SCROLLWIN_BOTTOM, boost::bind(&ContentTimeline::scrolled, this, _1)); + _main_canvas->Bind (wxEVT_SCROLLWIN_LINEUP, boost::bind(&ContentTimeline::scrolled, this, _1)); + _main_canvas->Bind (wxEVT_SCROLLWIN_LINEDOWN, boost::bind(&ContentTimeline::scrolled, this, _1)); + _main_canvas->Bind (wxEVT_SCROLLWIN_PAGEUP, boost::bind(&ContentTimeline::scrolled, this, _1)); + _main_canvas->Bind (wxEVT_SCROLLWIN_PAGEDOWN, boost::bind(&ContentTimeline::scrolled, this, _1)); + _main_canvas->Bind (wxEVT_SCROLLWIN_THUMBTRACK, boost::bind(&ContentTimeline::scrolled, this, _1)); + + film_change(ChangeType::DONE, FilmProperty::CONTENT); + + SetMinSize (wxSize (640, 4 * pixels_per_track() + 96)); + + _film_changed_connection = film->Change.connect(bind(&ContentTimeline::film_change, this, _1, _2)); + _film_content_change_connection = film->ContentChange.connect(bind(&ContentTimeline::film_content_change, this, _1, _3, _4)); + + Bind(wxEVT_TIMER, boost::bind(&ContentTimeline::update_playhead, this)); + _timer.Start (200, wxTIMER_CONTINUOUS); + + setup_scrollbars (); + _labels_canvas->ShowScrollbars (wxSHOW_SB_NEVER, wxSHOW_SB_NEVER); +} + + +void +ContentTimeline::mouse_wheel_turned(wxMouseEvent& event) +{ + auto const rotation = event.GetWheelRotation(); + + if (event.ControlDown()) { + /* On my mouse one click of the scroll wheel is 120, and it's -ve when + * scrolling the wheel towards me. + */ + auto const scale = rotation > 0 ? + (1.0 / (rotation / 90.0)) : + (-rotation / 90.0); + + int before_start_x; + int before_start_y; + _main_canvas->GetViewStart(&before_start_x, &before_start_y); + + auto const before_pps = _pixels_per_second.get_value_or(1); + auto const before_pos = _last_mouse_wheel_x && *_last_mouse_wheel_x == event.GetX() ? + *_last_mouse_wheel_time : + (before_start_x * _x_scroll_rate + event.GetX()) / before_pps; + + set_pixels_per_second(before_pps * scale); + setup_scrollbars(); + + auto after_left = std::max(0.0, before_pos * _pixels_per_second.get_value_or(1) - event.GetX()); + _main_canvas->Scroll(after_left / _x_scroll_rate, before_start_y); + _labels_canvas->Scroll(0, before_start_y); + Refresh(); + + if (!_last_mouse_wheel_x || *_last_mouse_wheel_x != event.GetX()) { + _last_mouse_wheel_x = event.GetX(); + _last_mouse_wheel_time = before_pos; + } + } else if (event.ShiftDown()) { + int before_start_x; + int before_start_y; + _main_canvas->GetViewStart(&before_start_x, &before_start_y); + auto const width = _main_canvas->GetSize().GetWidth(); + _main_canvas->Scroll(std::max(0.0, before_start_x - rotation * 100.0 / width), before_start_y); + } +} + + +void +ContentTimeline::update_playhead() +{ + Refresh (); +} + + +void +ContentTimeline::paint_labels() +{ + wxPaintDC dc (_labels_canvas); + + auto film = _film.lock(); + if (film->content().empty()) { + return; + } + + auto gc = wxGraphicsContext::Create (dc); + if (!gc) { + return; + } + + dcp::ScopeGuard sg = [gc]() { delete gc; }; + + int vsx, vsy; + _labels_canvas->GetViewStart (&vsx, &vsy); + gc->Translate (-vsx * _x_scroll_rate, -vsy * _y_scroll_rate + tracks_y_offset()); + + _labels_view->paint (gc, {}); +} + + +void +ContentTimeline::paint_main() +{ + wxPaintDC dc (_main_canvas); + dc.Clear(); + + auto film = _film.lock(); + if (film->content().empty()) { + return; + } + + _main_canvas->DoPrepareDC (dc); + + auto gc = wxGraphicsContext::Create (dc); + if (!gc) { + return; + } + + dcp::ScopeGuard sg = [gc]() { delete gc; }; + + gc->SetAntialiasMode (wxANTIALIAS_DEFAULT); + + for (auto i: _views) { + + auto ic = dynamic_pointer_cast<TimelineContentView> (i); + + /* Find areas of overlap with other content views, so that we can plot them */ + list<dcpomatic::Rect<int>> overlaps; + for (auto j: _views) { + auto jc = dynamic_pointer_cast<TimelineContentView> (j); + /* No overlap with non-content views, views on different tracks, audio views or non-active views */ + if (!ic || !jc || i == j || ic->track() != jc->track() || ic->track().get_value_or(2) >= 2 || !ic->active() || !jc->active()) { + continue; + } + + auto r = j->bbox().intersection(i->bbox()); + if (r) { + overlaps.push_back (r.get ()); + } + } + + i->paint (gc, overlaps); + } + + if (_zoom_point) { + gc->SetPen(gui_is_dark() ? *wxWHITE_PEN : *wxBLACK_PEN); + gc->SetBrush (*wxTRANSPARENT_BRUSH); + gc->DrawRectangle ( + min (_down_point.x, _zoom_point->x), + min (_down_point.y, _zoom_point->y), + abs (_down_point.x - _zoom_point->x), + abs (_down_point.y - _zoom_point->y) + ); + } + + /* Playhead */ + + gc->SetPen (*wxRED_PEN); + auto path = gc->CreatePath (); + double const ph = _viewer.position().seconds() * pixels_per_second().get_value_or(0); + path.MoveToPoint (ph, 0); + path.AddLineToPoint (ph, pixels_per_track() * _tracks + 32); + gc->StrokePath (path); +} + + +void +ContentTimeline::film_change(ChangeType type, FilmProperty p) +{ + if (type != ChangeType::DONE) { + return; + } + + if (p == FilmProperty::CONTENT || p == FilmProperty::REEL_TYPE || p == FilmProperty::REEL_LENGTH) { + ensure_ui_thread (); + recreate_views (); + } else if (p == FilmProperty::CONTENT_ORDER) { + Refresh (); + } +} + + +void +ContentTimeline::recreate_views() +{ + auto film = _film.lock (); + if (!film) { + return; + } + + _views.clear (); + _views.push_back (_time_axis_view); + _views.push_back (_reels_view); + + for (auto i: film->content ()) { + if (i->video) { + _views.push_back(make_shared<ContentTimelineVideoView>(*this, i)); + } + + if (i->audio && !i->audio->mapping().mapped_output_channels().empty ()) { + _views.push_back(make_shared<ContentTimelineAudioView>(*this, i)); + } + + for (auto j: i->text) { + _views.push_back(make_shared<ContentTimelineTextView>(*this, i, j)); + } + + if (i->atmos) { + _views.push_back(make_shared<ContentTimelineAtmosView>(*this, i)); + } + } + + assign_tracks (); + setup_scrollbars (); + Refresh (); +} + + +void +ContentTimeline::film_content_change(ChangeType type, int property, bool frequent) +{ + if (type != ChangeType::DONE) { + return; + } + + ensure_ui_thread (); + + if (property == AudioContentProperty::STREAMS || property == VideoContentProperty::FRAME_TYPE) { + recreate_views (); + } else if (property == ContentProperty::POSITION || property == ContentProperty::LENGTH) { + _reels_view->force_redraw (); + } else if (!frequent) { + setup_scrollbars (); + Refresh (); + } +} + + +template <class T> +int +place(shared_ptr<const Film> film, ContentTimelineViewList& views, int& tracks) +{ + int const base = tracks; + + for (auto i: views) { + if (!dynamic_pointer_cast<T>(i)) { + continue; + } + + auto cv = dynamic_pointer_cast<TimelineContentView> (i); + DCPOMATIC_ASSERT(cv); + + int t = base; + + auto content = cv->content(); + DCPTimePeriod const content_period = content->period(film); + + while (true) { + auto j = views.begin(); + while (j != views.end()) { + auto test = dynamic_pointer_cast<T> (*j); + if (!test) { + ++j; + continue; + } + + auto test_content = test->content(); + if ( + test->track() && test->track().get() == t && + content_period.overlap(test_content->period(film)) + ) { + /* we have an overlap on track `t' */ + ++t; + break; + } + + ++j; + } + + if (j == views.end ()) { + /* no overlap on `t' */ + break; + } + } + + cv->set_track (t); + tracks = max (tracks, t + 1); + } + + return tracks - base; +} + + +/** Compare the mapped output channels of two TimelineViews, so we can into + * order of first mapped DCP channel. + */ +struct AudioMappingComparator { + bool operator()(shared_ptr<ContentTimelineView> a, shared_ptr<ContentTimelineView> b) { + int la = -1; + auto cva = dynamic_pointer_cast<ContentTimelineAudioView>(a); + if (cva) { + auto oc = cva->content()->audio->mapping().mapped_output_channels(); + la = *min_element(boost::begin(oc), boost::end(oc)); + } + int lb = -1; + auto cvb = dynamic_pointer_cast<ContentTimelineAudioView>(b); + if (cvb) { + auto oc = cvb->content()->audio->mapping().mapped_output_channels(); + lb = *min_element(boost::begin(oc), boost::end(oc)); + } + return la < lb; + } +}; + + +void +ContentTimeline::assign_tracks() +{ + /* Tracks are: + Video 1 + Video 2 + Video N + Text 1 + Text 2 + Text N + Atmos + Audio 1 + Audio 2 + Audio N + */ + + auto film = _film.lock (); + DCPOMATIC_ASSERT (film); + + _tracks = 0; + + for (auto i: _views) { + auto c = dynamic_pointer_cast<TimelineContentView>(i); + if (c) { + c->unset_track (); + } + } + + int const video_tracks = place<ContentTimelineVideoView>(film, _views, _tracks); + int const text_tracks = place<ContentTimelineTextView>(film, _views, _tracks); + + /* Atmos */ + + bool have_atmos = false; + for (auto i: _views) { + auto cv = dynamic_pointer_cast<ContentTimelineAtmosView>(i); + if (cv) { + cv->set_track (_tracks); + have_atmos = true; + } + } + + if (have_atmos) { + ++_tracks; + } + + /* Audio. We're sorting the views so that we get the audio views in order of increasing + DCP channel index. + */ + + auto views = _views; + sort(views.begin(), views.end(), AudioMappingComparator()); + int const audio_tracks = place<ContentTimelineAudioView>(film, views, _tracks); + + _labels_view->set_video_tracks (video_tracks); + _labels_view->set_audio_tracks (audio_tracks); + _labels_view->set_text_tracks (text_tracks); + _labels_view->set_atmos (have_atmos); + + _time_axis_view->set_y (tracks()); + _reels_view->set_y (8); +} + + +int +ContentTimeline::tracks() const +{ + return _tracks; +} + + +void +ContentTimeline::setup_scrollbars() +{ + auto film = _film.lock (); + if (!film || !_pixels_per_second) { + return; + } + + int const h = tracks() * pixels_per_track() + tracks_y_offset() + _time_axis_view->bbox().height; + + _labels_canvas->SetVirtualSize (_labels_view->bbox().width, h); + _labels_canvas->SetScrollRate (_x_scroll_rate, _y_scroll_rate); + _main_canvas->SetVirtualSize (*_pixels_per_second * film->length().seconds(), h); + _main_canvas->SetScrollRate (_x_scroll_rate, _y_scroll_rate); +} + + +shared_ptr<ContentTimelineView> +ContentTimeline::event_to_view(wxMouseEvent& ev) +{ + /* Search backwards through views so that we find the uppermost one first */ + auto i = _views.rbegin(); + + int vsx, vsy; + _main_canvas->GetViewStart (&vsx, &vsy); + Position<int> const p (ev.GetX() + vsx * _x_scroll_rate, ev.GetY() + vsy * _y_scroll_rate); + + while (i != _views.rend() && !(*i)->bbox().contains (p)) { + ++i; + } + + if (i == _views.rend ()) { + return {}; + } + + return *i; +} + + +void +ContentTimeline::left_down(wxMouseEvent& ev) +{ + _left_down = true; + _down_point = ev.GetPosition (); + + switch (_tool) { + case SELECT: + left_down_select (ev); + break; + case ZOOM: + case ZOOM_ALL: + case SNAP: + case SEQUENCE: + /* Nothing to do */ + break; + } +} + + +void +ContentTimeline::left_down_select(wxMouseEvent& ev) +{ + auto view = event_to_view (ev); + auto content_view = dynamic_pointer_cast<TimelineContentView>(view); + + _down_view.reset (); + + if (content_view) { + _down_view = content_view; + _down_view_position = content_view->content()->position (); + } + + if (dynamic_pointer_cast<TimelineTimeAxisView>(view)) { + int vsx, vsy; + _main_canvas->GetViewStart(&vsx, &vsy); + _viewer.seek(DCPTime::from_seconds((ev.GetPosition().x + vsx * _x_scroll_rate) / _pixels_per_second.get_value_or(1)), true); + } + + for (auto i: _views) { + auto cv = dynamic_pointer_cast<TimelineContentView>(i); + if (!cv) { + continue; + } + + if (!ev.ShiftDown ()) { + cv->set_selected (view == i); + } + } + + if (content_view && ev.ShiftDown ()) { + content_view->set_selected (!content_view->selected ()); + } + + _first_move = false; + + if (_down_view) { + /* Pre-compute the points that we might snap to */ + for (auto i: _views) { + auto cv = dynamic_pointer_cast<TimelineContentView>(i); + if (!cv || cv == _down_view || cv->content() == _down_view->content()) { + continue; + } + + auto film = _film.lock (); + DCPOMATIC_ASSERT (film); + + _start_snaps.push_back (cv->content()->position()); + _end_snaps.push_back (cv->content()->position()); + _start_snaps.push_back (cv->content()->end(film)); + _end_snaps.push_back (cv->content()->end(film)); + + for (auto i: cv->content()->reel_split_points(film)) { + _start_snaps.push_back (i); + } + } + + /* Tell everyone that things might change frequently during the drag */ + _down_view->content()->set_change_signals_frequent (true); + } +} + + +void +ContentTimeline::left_up(wxMouseEvent& ev) +{ + _left_down = false; + + switch (_tool) { + case SELECT: + left_up_select (ev); + break; + case ZOOM: + left_up_zoom (ev); + break; + case ZOOM_ALL: + case SNAP: + case SEQUENCE: + break; + } +} + + +void +ContentTimeline::left_up_select(wxMouseEvent& ev) +{ + if (_down_view) { + _down_view->content()->set_change_signals_frequent (false); + } + + _content_panel->set_selection (selected_content ()); + /* Since we may have just set change signals back to `not-frequent', we have to + make sure this position change is signalled, even if the position value has + not changed since the last time it was set (with frequent=true). This is + a bit of a hack. + */ + set_position_from_event (ev, true); + + /* Clear up up the stuff we don't do during drag */ + assign_tracks (); + setup_scrollbars (); + Refresh (); + + _start_snaps.clear (); + _end_snaps.clear (); +} + + +void +ContentTimeline::left_up_zoom(wxMouseEvent& ev) +{ + _zoom_point = ev.GetPosition (); + + int vsx, vsy; + _main_canvas->GetViewStart (&vsx, &vsy); + + wxPoint top_left(min(_down_point.x, _zoom_point->x), min(_down_point.y, _zoom_point->y)); + wxPoint bottom_right(max(_down_point.x, _zoom_point->x), max(_down_point.y, _zoom_point->y)); + + if ((bottom_right.x - top_left.x) < 8 || (bottom_right.y - top_left.y) < 8) { + /* Very small zoom rectangle: we assume it wasn't intentional */ + _zoom_point = optional<wxPoint> (); + Refresh (); + return; + } + + auto const time_left = DCPTime::from_seconds((top_left.x + vsx) / *_pixels_per_second); + auto const time_right = DCPTime::from_seconds((bottom_right.x + vsx) / *_pixels_per_second); + set_pixels_per_second (double(GetSize().GetWidth()) / (time_right.seconds() - time_left.seconds())); + + double const tracks_top = double(top_left.y - tracks_y_offset()) / _pixels_per_track; + double const tracks_bottom = double(bottom_right.y - tracks_y_offset()) / _pixels_per_track; + set_pixels_per_track (lrint(GetSize().GetHeight() / (tracks_bottom - tracks_top))); + + setup_scrollbars (); + int const y = (tracks_top * _pixels_per_track + tracks_y_offset()) / _y_scroll_rate; + _main_canvas->Scroll (time_left.seconds() * *_pixels_per_second / _x_scroll_rate, y); + _labels_canvas->Scroll (0, y); + + _zoom_point = optional<wxPoint> (); + Refresh (); +} + + +void +ContentTimeline::set_pixels_per_track(int h) +{ + _pixels_per_track = max(_minimum_pixels_per_track, h); +} + + +void +ContentTimeline::mouse_moved(wxMouseEvent& ev) +{ + switch (_tool) { + case SELECT: + mouse_moved_select (ev); + break; + case ZOOM: + mouse_moved_zoom (ev); + break; + case ZOOM_ALL: + case SNAP: + case SEQUENCE: + break; + } +} + + +void +ContentTimeline::mouse_moved_select(wxMouseEvent& ev) +{ + if (!_left_down) { + return; + } + + set_position_from_event (ev); +} + + +void +ContentTimeline::mouse_moved_zoom(wxMouseEvent& ev) +{ + if (!_left_down) { + return; + } + + _zoom_point = ev.GetPosition (); + setup_scrollbars(); + Refresh (); +} + + +void +ContentTimeline::right_down(wxMouseEvent& ev) +{ + switch (_tool) { + case SELECT: + right_down_select (ev); + break; + case ZOOM: + /* Zoom out */ + set_pixels_per_second (*_pixels_per_second / 2); + set_pixels_per_track (_pixels_per_track / 2); + setup_scrollbars (); + Refresh (); + break; + case ZOOM_ALL: + case SNAP: + case SEQUENCE: + break; + } +} + + +void +ContentTimeline::right_down_select(wxMouseEvent& ev) +{ + auto view = event_to_view (ev); + auto cv = dynamic_pointer_cast<TimelineContentView> (view); + if (!cv) { + return; + } + + if (!cv->selected ()) { + clear_selection (); + cv->set_selected (true); + } + + _menu.popup (_film, selected_content (), selected_views (), ev.GetPosition ()); +} + + +void +ContentTimeline::maybe_snap(DCPTime a, DCPTime b, optional<DCPTime>& nearest_distance) const +{ + auto const d = a - b; + if (!nearest_distance || d.abs() < nearest_distance.get().abs()) { + nearest_distance = d; + } +} + + +void +ContentTimeline::set_position_from_event(wxMouseEvent& ev, bool force_emit) +{ + if (!_pixels_per_second) { + return; + } + + double const pps = _pixels_per_second.get (); + + auto const p = ev.GetPosition(); + + if (!_first_move) { + /* We haven't moved yet; in that case, we must move the mouse some reasonable distance + before the drag is considered to have started. + */ + int const dist = sqrt (pow (p.x - _down_point.x, 2) + pow (p.y - _down_point.y, 2)); + if (dist < 8) { + return; + } + _first_move = true; + } + + if (!_down_view) { + return; + } + + auto new_position = _down_view_position + DCPTime::from_seconds ((p.x - _down_point.x) / pps); + + auto film = _film.lock (); + DCPOMATIC_ASSERT (film); + + if (_snap) { + auto const new_end = new_position + _down_view->content()->length_after_trim(film); + /* Signed `distance' to nearest thing (i.e. negative is left on the timeline, + positive is right). + */ + optional<DCPTime> nearest_distance; + + /* Find the nearest snap point */ + + for (auto i: _start_snaps) { + maybe_snap (i, new_position, nearest_distance); + } + + for (auto i: _end_snaps) { + maybe_snap (i, new_end, nearest_distance); + } + + if (nearest_distance) { + /* Snap if it's close; `close' means within a proportion of the time on the timeline */ + if (nearest_distance.get().abs() < DCPTime::from_seconds ((width() / pps) / SNAP_SUBDIVISION)) { + new_position += nearest_distance.get (); + } + } + } + + if (new_position < DCPTime ()) { + new_position = DCPTime (); + } + + _down_view->content()->set_position (film, new_position, force_emit); + + film->set_sequence (false); +} + + +void +ContentTimeline::force_redraw(dcpomatic::Rect<int> const & r) +{ + _main_canvas->RefreshRect (wxRect (r.x, r.y, r.width, r.height), false); +} + + +shared_ptr<const Film> +ContentTimeline::film() const +{ + return _film.lock (); +} + + +void +ContentTimeline::resized() +{ + if (_main_canvas->GetSize().GetWidth() > 0 && _first_resize) { + zoom_all (); + _first_resize = false; + } + setup_scrollbars (); +} + + +void +ContentTimeline::clear_selection() +{ + for (auto i: _views) { + shared_ptr<TimelineContentView> cv = dynamic_pointer_cast<TimelineContentView>(i); + if (cv) { + cv->set_selected (false); + } + } +} + + +TimelineContentViewList +ContentTimeline::selected_views() const +{ + TimelineContentViewList sel; + + for (auto i: _views) { + auto cv = dynamic_pointer_cast<TimelineContentView>(i); + if (cv && cv->selected()) { + sel.push_back (cv); + } + } + + return sel; +} + + +ContentList +ContentTimeline::selected_content() const +{ + ContentList sel; + + for (auto i: selected_views()) { + sel.push_back(i->content()); + } + + return sel; +} + + +void +ContentTimeline::set_selection(ContentList selection) +{ + for (auto i: _views) { + auto cv = dynamic_pointer_cast<TimelineContentView> (i); + if (cv) { + cv->set_selected (find (selection.begin(), selection.end(), cv->content ()) != selection.end ()); + } + } +} + + +int +ContentTimeline::tracks_y_offset() const +{ + return _reels_view->bbox().height + 4; +} + + +int +ContentTimeline::width() const +{ + return _main_canvas->GetVirtualSize().GetWidth(); +} + + +void +ContentTimeline::scrolled(wxScrollWinEvent& ev) +{ + if (ev.GetOrientation() == wxVERTICAL) { + int x, y; + _main_canvas->GetViewStart (&x, &y); + _labels_canvas->Scroll (0, y); + } + ev.Skip (); +} + + +void +ContentTimeline::tool_clicked(Tool t) +{ + switch (t) { + case ZOOM: + case SELECT: + _tool = t; + break; + case ZOOM_ALL: + zoom_all (); + break; + case SNAP: + case SEQUENCE: + break; + } +} + + +void +ContentTimeline::zoom_all() +{ + auto film = _film.lock (); + DCPOMATIC_ASSERT (film); + set_pixels_per_second((_main_canvas->GetSize().GetWidth() - 32) / std::max(1.0, film->length().seconds())); + set_pixels_per_track((_main_canvas->GetSize().GetHeight() - tracks_y_offset() - _time_axis_view->bbox().height - 32) / std::max(1, _tracks)); + setup_scrollbars (); + _main_canvas->Scroll (0, 0); + _labels_canvas->Scroll (0, 0); + Refresh (); +} + + +void +ContentTimeline::keypress(wxKeyEvent const& event) +{ + if (event.GetKeyCode() == WXK_DELETE) { + auto film = _film.lock(); + DCPOMATIC_ASSERT(film); + film->remove_content(selected_content()); + } else { + switch (event.GetRawKeyCode()) { + case '+': + set_pixels_per_second(_pixels_per_second.get_value_or(1) * 2); + setup_scrollbars(); + break; + case '-': + set_pixels_per_second(_pixels_per_second.get_value_or(1) / 2); + setup_scrollbars(); + break; + } + } +} + diff --git a/src/wx/content_timeline.h b/src/wx/content_timeline.h new file mode 100644 index 000000000..10f880191 --- /dev/null +++ b/src/wx/content_timeline.h @@ -0,0 +1,149 @@ +/* + Copyright (C) 2013-2019 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 "content_menu.h" +#include "timeline.h" +#include "timeline_content_view.h" +#include "lib/film_property.h" +#include "lib/rect.h" +#include <dcp/warnings.h> +LIBDCP_DISABLE_WARNINGS +#include <wx/wx.h> +LIBDCP_ENABLE_WARNINGS +#include <boost/signals2.hpp> + + +class ContentPanel; +class ContentTimelineView; +class Film; +class FilmViewer; +class TimelineLabelsView; +class TimelineReelsView; +class TimelineTimeAxisView; + + +class ContentTimeline : public Timeline +{ +public: + ContentTimeline(wxWindow *, ContentPanel *, std::shared_ptr<Film>, FilmViewer& viewer); + + std::shared_ptr<const Film> film () const; + + void force_redraw (dcpomatic::Rect<int> const &); + + int width () const; + + int pixels_per_track () const { + return _pixels_per_track; + } + + int tracks () const; + + void set_snap (bool s) { + _snap = s; + } + + bool snap () const { + return _snap; + } + + void set_selection (ContentList selection); + + enum Tool { + SELECT, + ZOOM, + ZOOM_ALL, + SNAP, + SEQUENCE + }; + + void tool_clicked (Tool t); + + int tracks_y_offset () const; + + void keypress(wxKeyEvent const &); + +private: + void paint_labels (); + void paint_main (); + void left_down (wxMouseEvent &); + void left_down_select (wxMouseEvent &); + void left_up (wxMouseEvent &); + void left_up_select (wxMouseEvent &); + void left_up_zoom (wxMouseEvent &); + void right_down (wxMouseEvent &); + void right_down_select (wxMouseEvent &); + void mouse_moved (wxMouseEvent &); + void mouse_moved_select (wxMouseEvent &); + void mouse_moved_zoom (wxMouseEvent &); + void film_change(ChangeType type, FilmProperty); + void film_content_change (ChangeType type, int, bool frequent); + void resized (); + void assign_tracks (); + void set_position_from_event (wxMouseEvent& ev, bool force_emit = false); + void clear_selection (); + void recreate_views (); + void setup_scrollbars (); + void scrolled (wxScrollWinEvent& ev); + void set_pixels_per_track (int h); + void zoom_all (); + void update_playhead (); + void mouse_wheel_turned(wxMouseEvent& event); + + std::shared_ptr<ContentTimelineView> event_to_view(wxMouseEvent &); + TimelineContentViewList selected_views () const; + ContentList selected_content () const; + void maybe_snap (dcpomatic::DCPTime a, dcpomatic::DCPTime b, boost::optional<dcpomatic::DCPTime>& nearest_distance) const; + + wxScrolledCanvas* _labels_canvas; + wxScrolledCanvas* _main_canvas; + ContentPanel* _content_panel; + std::weak_ptr<Film> _film; + FilmViewer& _viewer; + ContentTimelineViewList _views; + std::shared_ptr<TimelineTimeAxisView> _time_axis_view; + std::shared_ptr<TimelineReelsView> _reels_view; + std::shared_ptr<TimelineLabelsView> _labels_view; + int _tracks; + bool _left_down; + wxPoint _down_point; + boost::optional<wxPoint> _zoom_point; + std::shared_ptr<TimelineContentView> _down_view; + dcpomatic::DCPTime _down_view_position; + bool _first_move; + ContentMenu _menu; + bool _snap; + std::list<dcpomatic::DCPTime> _start_snaps; + std::list<dcpomatic::DCPTime> _end_snaps; + Tool _tool; + int _x_scroll_rate; + int _y_scroll_rate; + int _pixels_per_track; + bool _first_resize; + wxTimer _timer; + boost::optional<int> _last_mouse_wheel_x; + boost::optional<double> _last_mouse_wheel_time; + + static int const _minimum_pixels_per_track; + + boost::signals2::scoped_connection _film_changed_connection; + boost::signals2::scoped_connection _film_content_change_connection; +}; diff --git a/src/wx/timeline_atmos_content_view.cc b/src/wx/content_timeline_atmos_view.cc index ec0f06ec1..2352ba5dc 100644 --- a/src/wx/timeline_atmos_content_view.cc +++ b/src/wx/content_timeline_atmos_view.cc @@ -19,17 +19,18 @@ */ -#include "timeline_atmos_content_view.h" +#include "colours.h" +#include "content_timeline_atmos_view.h" using std::shared_ptr; -/** @class TimelineAtmosContentView - * @brief Timeline view for AtmosContent. +/** @class ContentTimelineContentView + * @brief Content timeline view for AtmosContent. */ -TimelineAtmosContentView::TimelineAtmosContentView (Timeline& tl, shared_ptr<Content> c) +ContentTimelineAtmosView::ContentTimelineAtmosView(ContentTimeline& tl, shared_ptr<Content> c) : TimelineContentView (tl, c) { @@ -37,14 +38,14 @@ TimelineAtmosContentView::TimelineAtmosContentView (Timeline& tl, shared_ptr<Con wxColour -TimelineAtmosContentView::background_colour () const +ContentTimelineAtmosView::background_colour() const { - return wxColour (149, 121, 232, 255); + return ATMOS_CONTENT_COLOUR; } wxColour -TimelineAtmosContentView::foreground_colour () const +ContentTimelineAtmosView::foreground_colour() const { return wxColour (0, 0, 0, 255); } diff --git a/src/wx/timeline_atmos_content_view.h b/src/wx/content_timeline_atmos_view.h index 15da14edc..d5fea9248 100644 --- a/src/wx/timeline_atmos_content_view.h +++ b/src/wx/content_timeline_atmos_view.h @@ -22,13 +22,13 @@ #include "timeline_content_view.h" -/** @class TimelineAtmosContentView - * @brief Timeline view for AtmosContent. +/** @class ContentTimelineAtmosContentView + * @brief Content timeline view for AtmosContent. */ -class TimelineAtmosContentView : public TimelineContentView +class ContentTimelineAtmosView : public TimelineContentView { public: - TimelineAtmosContentView (Timeline& tl, std::shared_ptr<Content> c); + ContentTimelineAtmosView(ContentTimeline& tl, std::shared_ptr<Content> c); private: bool active () const override { diff --git a/src/wx/timeline_audio_content_view.cc b/src/wx/content_timeline_audio_view.cc index 057ebb944..600e39057 100644 --- a/src/wx/timeline_audio_content_view.cc +++ b/src/wx/content_timeline_audio_view.cc @@ -18,39 +18,44 @@ */ -#include "timeline_audio_content_view.h" + +#include "colours.h" +#include "content_timeline_audio_view.h" #include "wx_util.h" #include "lib/audio_content.h" #include "lib/util.h" + +using std::dynamic_pointer_cast; using std::list; using std::shared_ptr; -using std::dynamic_pointer_cast; -/** @class TimelineAudioContentView - * @brief Timeline view for AudioContent. + +/** @class ContentTimelineAudioView + * @brief Content timeline view for AudioContent. */ -TimelineAudioContentView::TimelineAudioContentView (Timeline& tl, shared_ptr<Content> c) + +ContentTimelineAudioView::ContentTimelineAudioView(ContentTimeline& tl, shared_ptr<Content> c) : TimelineContentView (tl, c) { } wxColour -TimelineAudioContentView::background_colour () const +ContentTimelineAudioView::background_colour () const { - return wxColour (149, 121, 232, 255); + return AUDIO_CONTENT_COLOUR; } wxColour -TimelineAudioContentView::foreground_colour () const +ContentTimelineAudioView::foreground_colour () const { return wxColour (0, 0, 0, 255); } wxString -TimelineAudioContentView::label () const +ContentTimelineAudioView::label () const { wxString s = TimelineContentView::label (); shared_ptr<AudioContent> ac = content()->audio; diff --git a/src/wx/timeline_audio_content_view.h b/src/wx/content_timeline_audio_view.h index 5b8d6cdc2..719c2536d 100644 --- a/src/wx/timeline_audio_content_view.h +++ b/src/wx/content_timeline_audio_view.h @@ -22,13 +22,13 @@ #include "timeline_content_view.h" -/** @class TimelineAudioContentView - * @brief Timeline view for AudioContent. +/** @class ContentTimelineAudioView + * @brief Content timeline view for AudioContent. */ -class TimelineAudioContentView : public TimelineContentView +class ContentTimelineAudioView : public TimelineContentView { public: - TimelineAudioContentView (Timeline& tl, std::shared_ptr<Content> c); + ContentTimelineAudioView(ContentTimeline& tl, std::shared_ptr<Content> c); private: bool active () const override { diff --git a/src/wx/timeline_dialog.cc b/src/wx/content_timeline_dialog.cc index e0e1689a8..697e6561f 100644 --- a/src/wx/timeline_dialog.cc +++ b/src/wx/content_timeline_dialog.cc @@ -20,8 +20,8 @@ #include "content_panel.h" +#include "content_timeline_dialog.h" #include "film_editor.h" -#include "timeline_dialog.h" #include "wx_util.h" #include "lib/compose.hpp" #include "lib/cross.h" @@ -43,7 +43,7 @@ using namespace boost::placeholders; #endif -TimelineDialog::TimelineDialog(ContentPanel* cp, shared_ptr<Film> film, FilmViewer& viewer) +ContentTimelineDialog::ContentTimelineDialog(ContentPanel* cp, shared_ptr<Film> film, FilmViewer& viewer) : wxDialog ( cp->window(), wxID_ANY, @@ -73,14 +73,14 @@ TimelineDialog::TimelineDialog(ContentPanel* cp, shared_ptr<Film> film, FilmView _toolbar = new wxToolBar (this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxTB_HORIZONTAL); _toolbar->SetMargins (4, 4); _toolbar->SetToolBitmapSize (wxSize(32, 32)); - _toolbar->AddRadioTool ((int) Timeline::SELECT, _("Select"), select, wxNullBitmap, _("Select and move content")); - _toolbar->AddRadioTool ((int) Timeline::ZOOM, _("Zoom"), zoom, wxNullBitmap, _("Zoom in / out")); - _toolbar->AddTool ((int) Timeline::ZOOM_ALL, _("Zoom all"), zoom_all, _("Zoom out to whole film")); - _toolbar->AddCheckTool ((int) Timeline::SNAP, _("Snap"), snap, wxNullBitmap, _("Snap")); - _toolbar->AddCheckTool ((int) Timeline::SEQUENCE, _("Sequence"), sequence, wxNullBitmap, _("Keep video and subtitles in sequence")); + _toolbar->AddRadioTool(static_cast<int>(ContentTimeline::SELECT), _("Select"), select, wxNullBitmap, _("Select and move content")); + _toolbar->AddRadioTool(static_cast<int>(ContentTimeline::ZOOM), _("Zoom"), zoom, wxNullBitmap, _("Zoom in / out")); + _toolbar->AddTool(static_cast<int>(ContentTimeline::ZOOM_ALL), _("Zoom all"), zoom_all, _("Zoom out to whole film")); + _toolbar->AddCheckTool(static_cast<int>(ContentTimeline::SNAP), _("Snap"), snap, wxNullBitmap, _("Snap")); + _toolbar->AddCheckTool(static_cast<int>(ContentTimeline::SEQUENCE), _("Sequence"), sequence, wxNullBitmap, _("Keep video and subtitles in sequence")); _toolbar->Realize (); - _toolbar->Bind (wxEVT_TOOL, bind (&TimelineDialog::tool_clicked, this, _1)); + _toolbar->Bind(wxEVT_TOOL, bind(&ContentTimelineDialog::tool_clicked, this, _1)); sizer->Add (_toolbar, 0, wxALL, 12); sizer->Add (&_timeline, 1, wxEXPAND | wxALL, 12); @@ -96,17 +96,17 @@ TimelineDialog::TimelineDialog(ContentPanel* cp, shared_ptr<Film> film, FilmView sizer->Layout (); sizer->SetSizeHints (this); - Bind(wxEVT_CHAR_HOOK, boost::bind(&TimelineDialog::keypress, this, _1)); + Bind(wxEVT_CHAR_HOOK, boost::bind(&ContentTimelineDialog::keypress, this, _1)); - _toolbar->ToggleTool ((int) Timeline::SNAP, _timeline.snap ()); + _toolbar->ToggleTool(static_cast<int>(ContentTimeline::SNAP), _timeline.snap ()); film_change(ChangeType::DONE, FilmProperty::SEQUENCE); - _film_changed_connection = film->Change.connect (bind (&TimelineDialog::film_change, this, _1, _2)); + _film_changed_connection = film->Change.connect(bind(&ContentTimelineDialog::film_change, this, _1, _2)); } void -TimelineDialog::film_change(ChangeType type, FilmProperty p) +ContentTimelineDialog::film_change(ChangeType type, FilmProperty p) { if (type != ChangeType::DONE) { return; @@ -118,26 +118,26 @@ TimelineDialog::film_change(ChangeType type, FilmProperty p) } if (p == FilmProperty::SEQUENCE) { - _toolbar->ToggleTool ((int) Timeline::SEQUENCE, film->sequence ()); + _toolbar->ToggleTool(static_cast<int>(ContentTimeline::SEQUENCE), film->sequence()); } } void -TimelineDialog::set_selection (ContentList selection) +ContentTimelineDialog::set_selection(ContentList selection) { _timeline.set_selection (selection); } void -TimelineDialog::tool_clicked (wxCommandEvent& ev) +ContentTimelineDialog::tool_clicked(wxCommandEvent& ev) { - Timeline::Tool t = static_cast<Timeline::Tool>(ev.GetId()); + auto t = static_cast<ContentTimeline::Tool>(ev.GetId()); _timeline.tool_clicked (t); - if (t == Timeline::SNAP) { + if (t == ContentTimeline::SNAP) { _timeline.set_snap (_toolbar->GetToolState(static_cast<int>(t))); - } else if (t == Timeline::SEQUENCE) { + } else if (t == ContentTimeline::SEQUENCE) { auto film = _film.lock (); if (film) { film->set_sequence (_toolbar->GetToolState(static_cast<int>(t))); @@ -147,7 +147,7 @@ TimelineDialog::tool_clicked (wxCommandEvent& ev) void -TimelineDialog::keypress(wxKeyEvent const& event) +ContentTimelineDialog::keypress(wxKeyEvent const& event) { _timeline.keypress(event); } diff --git a/src/wx/timeline_dialog.h b/src/wx/content_timeline_dialog.h index 8134aa6db..2babf0437 100644 --- a/src/wx/timeline_dialog.h +++ b/src/wx/content_timeline_dialog.h @@ -19,7 +19,7 @@ */ -#include "timeline.h" +#include "content_timeline.h" #include <dcp/warnings.h> LIBDCP_DISABLE_WARNINGS #include <wx/wx.h> @@ -29,10 +29,10 @@ LIBDCP_ENABLE_WARNINGS class Playlist; -class TimelineDialog : public wxDialog +class ContentTimelineDialog : public wxDialog { public: - TimelineDialog(ContentPanel *, std::shared_ptr<Film>, FilmViewer& viewer); + ContentTimelineDialog(ContentPanel *, std::shared_ptr<Film>, FilmViewer& viewer); void set_selection (ContentList selection); @@ -42,7 +42,7 @@ private: void keypress(wxKeyEvent const& event); std::weak_ptr<Film> _film; - Timeline _timeline; + ContentTimeline _timeline; wxToolBar* _toolbar; boost::signals2::scoped_connection _film_changed_connection; }; diff --git a/src/wx/timeline_text_content_view.cc b/src/wx/content_timeline_text_view.cc index a57398599..f22f47850 100644 --- a/src/wx/timeline_text_content_view.cc +++ b/src/wx/content_timeline_text_view.cc @@ -19,7 +19,8 @@ */ -#include "timeline_text_content_view.h" +#include "colours.h" +#include "content_timeline_text_view.h" #include "lib/text_content.h" #include "lib/content.h" @@ -27,7 +28,7 @@ using std::shared_ptr; -TimelineTextContentView::TimelineTextContentView (Timeline& tl, shared_ptr<Content> c, shared_ptr<TextContent> caption) +ContentTimelineTextView::ContentTimelineTextView(ContentTimeline& tl, shared_ptr<Content> c, shared_ptr<TextContent> caption) : TimelineContentView (tl, c) , _caption (caption) { @@ -35,17 +36,17 @@ TimelineTextContentView::TimelineTextContentView (Timeline& tl, shared_ptr<Conte } wxColour -TimelineTextContentView::background_colour () const +ContentTimelineTextView::background_colour() const { if (!active ()) { return wxColour (210, 210, 210, 128); } - return wxColour (163, 255, 154, 255); + return TEXT_CONTENT_COLOUR; } wxColour -TimelineTextContentView::foreground_colour () const +ContentTimelineTextView::foreground_colour() const { if (!active ()) { return wxColour (180, 180, 180, 128); @@ -55,7 +56,7 @@ TimelineTextContentView::foreground_colour () const } bool -TimelineTextContentView::active () const +ContentTimelineTextView::active() const { return _caption->use(); } diff --git a/src/wx/timeline_text_content_view.h b/src/wx/content_timeline_text_view.h index 046f5b3e6..8d37ad489 100644 --- a/src/wx/timeline_text_content_view.h +++ b/src/wx/content_timeline_text_view.h @@ -18,18 +18,20 @@ */ + #include "timeline_content_view.h" + class TextContent; -class TextContent; -/** @class TimelineTextContentView - * @brief Timeline view for TextContent. + +/** @class ContentTimelineTextView + * @brief Content timeline view for TextContent. */ -class TimelineTextContentView : public TimelineContentView +class ContentTimelineTextView : public TimelineContentView { public: - TimelineTextContentView (Timeline& tl, std::shared_ptr<Content>, std::shared_ptr<TextContent>); + ContentTimelineTextView(ContentTimeline& tl, std::shared_ptr<Content>, std::shared_ptr<TextContent>); private: bool active () const override; diff --git a/src/wx/timeline_video_content_view.cc b/src/wx/content_timeline_video_view.cc index b0f4b4f5d..d907627cd 100644 --- a/src/wx/timeline_video_content_view.cc +++ b/src/wx/content_timeline_video_view.cc @@ -18,31 +18,35 @@ */ + +#include "colours.h" +#include "content_timeline_video_view.h" #include "lib/image_content.h" #include "lib/video_content.h" -#include "timeline_video_content_view.h" + using std::dynamic_pointer_cast; using std::shared_ptr; -TimelineVideoContentView::TimelineVideoContentView (Timeline& tl, shared_ptr<Content> c) + +ContentTimelineVideoView::ContentTimelineVideoView(ContentTimeline& tl, shared_ptr<Content> c) : TimelineContentView (tl, c) { } wxColour -TimelineVideoContentView::background_colour () const +ContentTimelineVideoView::background_colour() const { if (!active()) { return wxColour (210, 210, 210, 128); } - return wxColour (242, 92, 120, 255); + return VIDEO_CONTENT_COLOUR; } wxColour -TimelineVideoContentView::foreground_colour () const +ContentTimelineVideoView::foreground_colour() const { if (!active()) { return wxColour (180, 180, 180, 128); @@ -52,9 +56,9 @@ TimelineVideoContentView::foreground_colour () const } bool -TimelineVideoContentView::active () const +ContentTimelineVideoView::active() const { - shared_ptr<Content> c = _content.lock (); + auto c = _content.lock(); DCPOMATIC_ASSERT (c); return c->video && c->video->use(); } diff --git a/src/wx/timeline_video_content_view.h b/src/wx/content_timeline_video_view.h index fa8ddf54c..940a1a305 100644 --- a/src/wx/timeline_video_content_view.h +++ b/src/wx/content_timeline_video_view.h @@ -22,13 +22,13 @@ #include "timeline_content_view.h" -/** @class TimelineVideoContentView - * @brief Timeline view for VideoContent. +/** @class ContentTimelineVideoView + * @brief Content timeline view for VideoContent. */ -class TimelineVideoContentView : public TimelineContentView +class ContentTimelineVideoView : public TimelineContentView { public: - TimelineVideoContentView (Timeline& tl, std::shared_ptr<Content> c); + ContentTimelineVideoView(ContentTimeline& tl, std::shared_ptr<Content> c); private: bool active () const override; diff --git a/src/wx/content_timeline_view.cc b/src/wx/content_timeline_view.cc new file mode 100644 index 000000000..127e440fa --- /dev/null +++ b/src/wx/content_timeline_view.cc @@ -0,0 +1,42 @@ +/* + Copyright (C) 2013-2021 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 "content_timeline.h" +#include "content_timeline_view.h" + + +using std::list; +using namespace dcpomatic; + + +ContentTimelineView::ContentTimelineView(ContentTimeline& timeline) + : TimelineView(timeline) +{ + +} + +int +ContentTimelineView::y_pos(int t) const +{ + return t * _timeline.pixels_per_track() + _timeline.tracks_y_offset(); +} + + diff --git a/src/wx/content_timeline_view.h b/src/wx/content_timeline_view.h new file mode 100644 index 000000000..450d19df4 --- /dev/null +++ b/src/wx/content_timeline_view.h @@ -0,0 +1,60 @@ +/* + Copyright (C) 2013-2021 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_CONTENT_TIMELINE_VIEW_H +#define DCPOMATIC_CONTENT_TIMELINE_VIEW_H + + +#include "timeline_view.h" +#include "lib/rect.h" +#include "lib/dcpomatic_time.h" + + +class wxGraphicsContext; +class ContentTimeline; + + +/** @class ContentTimelineView + * @brief Parent class for components of the content timeline (e.g. a piece of content or an axis). + */ +class ContentTimelineView : public TimelineView<ContentTimeline> +{ +public: + explicit ContentTimelineView(ContentTimeline& t); + virtual ~ContentTimelineView () = default; + + void paint(wxGraphicsContext* gc, std::list<dcpomatic::Rect<int>> overlaps) + { + _last_paint_bbox = bbox(); + do_paint(gc, overlaps); + } + +protected: + virtual void do_paint(wxGraphicsContext *, std::list<dcpomatic::Rect<int>> overlaps) = 0; + + int y_pos(int t) const; +}; + + +typedef std::vector<std::shared_ptr<ContentTimelineView>> ContentTimelineViewList; + + +#endif diff --git a/src/wx/dcp_panel.cc b/src/wx/dcp_panel.cc index f4ba74cde..64b4935cc 100644 --- a/src/wx/dcp_panel.cc +++ b/src/wx/dcp_panel.cc @@ -23,6 +23,7 @@ #include "check_box.h" #include "check_box.h" #include "dcp_panel.h" +#include "dcp_timeline_dialog.h" #include "dcpomatic_button.h" #include "dcpomatic_choice.h" #include "dcpomatic_spin_ctrl.h" @@ -97,10 +98,6 @@ DCPPanel::DCPPanel(wxNotebook* n, shared_ptr<Film> film, FilmViewer& viewer) wxALIGN_CENTRE_HORIZONTAL | wxST_NO_AUTORESIZE | wxST_ELLIPSIZE_MIDDLE ); - _enable_audio_language = new CheckBox(_panel, _("Audio language")); - _audio_language = new wxStaticText (_panel, wxID_ANY, wxT("")); - _edit_audio_language = new Button (_panel, _("Edit...")); - _dcp_content_type_label = create_label (_panel, _("Content Type"), true); _dcp_content_type = new Choice(_panel); @@ -110,18 +107,12 @@ DCPPanel::DCPPanel(wxNotebook* n, shared_ptr<Film> film, FilmViewer& viewer) auto size = dc.GetTextExtent (wxT ("GGGGGGGG...")); size.SetHeight (-1); - _reels_label = create_label (_panel, _("Reels"), true); - _reel_type = new Choice(_panel); - - _reel_length_label = create_label (_panel, _("Reel length"), true); - _reel_length = new SpinCtrl (_panel, DCPOMATIC_SPIN_CTRL_WIDTH); - _reel_length_gb_label = create_label (_panel, _("GB"), false); - _standard_label = create_label (_panel, _("Standard"), true); _standard = new Choice(_panel); _markers = new Button (_panel, _("Markers...")); _metadata = new Button (_panel, _("Metadata...")); + _reels = new Button(_panel, _("Reels...")); _notebook = new wxNotebook (_panel, wxID_ANY); _sizer->Add (_notebook, 1, wxEXPAND | wxTOP, 6); @@ -134,28 +125,17 @@ DCPPanel::DCPPanel(wxNotebook* n, shared_ptr<Film> film, FilmViewer& viewer) _copy_isdcf_name_button->Bind(wxEVT_BUTTON, boost::bind(&DCPPanel::copy_isdcf_name_button_clicked, this)); _dcp_content_type->Bind (wxEVT_CHOICE, boost::bind(&DCPPanel::dcp_content_type_changed, this)); _encrypted->bind(&DCPPanel::encrypted_toggled, this); - _reel_type->Bind (wxEVT_CHOICE, boost::bind(&DCPPanel::reel_type_changed, this)); - _reel_length->Bind (wxEVT_SPINCTRL, boost::bind(&DCPPanel::reel_length_changed, this)); _standard->Bind (wxEVT_CHOICE, boost::bind(&DCPPanel::standard_changed, this)); _markers->Bind (wxEVT_BUTTON, boost::bind(&DCPPanel::markers_clicked, this)); _metadata->Bind (wxEVT_BUTTON, boost::bind(&DCPPanel::metadata_clicked, this)); - _enable_audio_language->bind(&DCPPanel::enable_audio_language_toggled, this); - _edit_audio_language->Bind (wxEVT_BUTTON, boost::bind(&DCPPanel::edit_audio_language_clicked, this)); + _reels->Bind(wxEVT_BUTTON, boost::bind(&DCPPanel::reels_clicked, this)); for (auto i: DCPContentType::all()) { _dcp_content_type->add(i->pretty_name()); } - _reel_type->add(_("Single reel")); - _reel_type->add(_("Split by video content")); - /// TRANSLATORS: translate the word "Custom" here; do not include the "Reel|" prefix - _reel_type->add(S_("Reel|Custom")); - _reel_type->SetToolTip(_("How the DCP should be split into parts internally. If in doubt, choose 'Single reel'")); - - _reel_length->SetRange (1, 64); - add_standards(); - _standard->SetToolTip(_("Which standard the DCP should use. Interop is older and SMPTE is the modern standard. If in doubt, choose 'SMPTE'")); + _standard->SetToolTip(_("The standard that the DCP should use. Interop is older, and SMPTE is the newer (current) standard. If in doubt, choose 'SMPTE'")); Config::instance()->Changed.connect (boost::bind(&DCPPanel::config_changed, this, _1)); @@ -241,15 +221,6 @@ DCPPanel::add_to_grid () _grid->Add (_dcp_name, wxGBPosition(r, 0), wxGBSpan(1, 2), wxALIGN_CENTER_VERTICAL | wxEXPAND); ++r; - { - auto s = new wxBoxSizer (wxHORIZONTAL); - s->Add (_enable_audio_language, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, DCPOMATIC_SIZER_GAP); - s->Add (_audio_language, 1, wxALIGN_CENTER_VERTICAL | wxBOTTOM, DCPOMATIC_CHECKBOX_BOTTOM_PAD); - s->Add (_edit_audio_language, 0, wxALIGN_CENTER_VERTICAL | wxBOTTOM, DCPOMATIC_CHECKBOX_BOTTOM_PAD); - _grid->Add (s, wxGBPosition(r, 0), wxGBSpan(1, 2), wxEXPAND | wxALIGN_CENTER_VERTICAL); - } - ++r; - add_label_to_sizer (_grid, _dcp_content_type_label, true, wxGBPosition(r, 0)); _grid->Add (_dcp_content_type, wxGBPosition(r, 1)); ++r; @@ -257,19 +228,6 @@ DCPPanel::add_to_grid () _grid->Add (_encrypted, wxGBPosition(r, 0), wxGBSpan(1, 2)); ++r; - add_label_to_sizer (_grid, _reels_label, true, wxGBPosition(r, 0)); - _grid->Add (_reel_type, wxGBPosition(r, 1), wxDefaultSpan, wxALIGN_CENTER_VERTICAL); - ++r; - - add_label_to_sizer (_grid, _reel_length_label, true, wxGBPosition(r, 0)); - { - auto s = new wxBoxSizer (wxHORIZONTAL); - s->Add (_reel_length); - add_label_to_sizer (s, _reel_length_gb_label, false, 0, wxLEFT | wxALIGN_CENTER_VERTICAL); - _grid->Add (s, wxGBPosition(r, 1)); - } - ++r; - add_label_to_sizer (_grid, _standard_label, true, wxGBPosition(r, 0)); _grid->Add (_standard, wxGBPosition(r, 1), wxDefaultSpan, wxALIGN_CENTER_VERTICAL); ++r; @@ -277,6 +235,7 @@ DCPPanel::add_to_grid () auto extra = new wxBoxSizer (wxHORIZONTAL); extra->Add (_markers, 1, wxRIGHT, DCPOMATIC_SIZER_X_GAP); extra->Add (_metadata, 1, wxRIGHT, DCPOMATIC_SIZER_X_GAP); + extra->Add(_reels, 1, wxRIGHT, DCPOMATIC_SIZER_X_GAP); _grid->Add (extra, wxGBPosition(r, 0), wxGBSpan(1, 2)); ++r; } @@ -390,6 +349,14 @@ DCPPanel::metadata_clicked () void +DCPPanel::reels_clicked() +{ + _dcp_timeline.reset(_panel, _film); + _dcp_timeline->Show(); +} + + +void DCPPanel::film_changed(FilmProperty p) { switch (p) { @@ -490,13 +457,6 @@ DCPPanel::film_changed(FilmProperty p) setup_audio_channels_choice (_audio_channels, minimum_allowed_audio_channels()); film_changed (FilmProperty::AUDIO_CHANNELS); break; - case FilmProperty::REEL_TYPE: - checked_set (_reel_type, static_cast<int>(_film->reel_type())); - _reel_length->Enable (_film->reel_type() == ReelType::BY_LENGTH); - break; - case FilmProperty::REEL_LENGTH: - checked_set (_reel_length, _film->reel_length() / 1000000000LL); - break; case FilmProperty::CONTENT: setup_dcp_name (); setup_sensitivity (); @@ -511,6 +471,7 @@ DCPPanel::film_changed(FilmProperty p) checked_set (_audio_language, al ? std_to_wx(al->to_string()) : wxT("")); setup_dcp_name (); setup_sensitivity (); + _audio_panel_sizer->Layout(); break; } case FilmProperty::AUDIO_FRAME_RATE: @@ -681,8 +642,6 @@ DCPPanel::setup_sensitivity () _audio_language->Enable (_enable_audio_language->GetValue()); _edit_audio_language->Enable (_enable_audio_language->GetValue()); _encrypted->Enable (_generally_sensitive); - _reel_type->Enable (_generally_sensitive && _film && !_film->references_dcp_video() && !_film->references_dcp_audio()); - _reel_length->Enable (_generally_sensitive && _film && _film->reel_type() == ReelType::BY_LENGTH); _markers->Enable (_generally_sensitive && _film && !_film->interop()); _metadata->Enable (_generally_sensitive); _frame_rate_choice->Enable (_generally_sensitive && _film && !_film->references_dcp_video() && !_film->contains_atmos_content()); @@ -953,6 +912,10 @@ DCPPanel::make_audio_panel () _audio_processor = new Choice(panel); add_audio_processors (); + _enable_audio_language = new CheckBox(panel, _("Language")); + _audio_language = new wxStaticText(panel, wxID_ANY, wxT("")); + _edit_audio_language = new Button(panel, _("Edit...")); + _show_audio = new Button (panel, _("Show graph of audio levels...")); _audio_channels->Bind (wxEVT_CHOICE, boost::bind (&DCPPanel::audio_channels_changed, this)); @@ -960,6 +923,10 @@ DCPPanel::make_audio_panel () _audio_sample_rate->Bind (wxEVT_CHOICE, boost::bind(&DCPPanel::audio_sample_rate_changed, this)); } _audio_processor->Bind (wxEVT_CHOICE, boost::bind (&DCPPanel::audio_processor_changed, this)); + + _enable_audio_language->bind(&DCPPanel::enable_audio_language_toggled, this); + _edit_audio_language->Bind(wxEVT_BUTTON, boost::bind(&DCPPanel::edit_audio_language_clicked, this)); + _show_audio->Bind (wxEVT_BUTTON, boost::bind (&DCPPanel::show_audio_clicked, this)); if (_audio_sample_rate) { @@ -992,6 +959,16 @@ DCPPanel::add_audio_panel_to_grid () _audio_grid->Add (_audio_processor, wxGBPosition (r, 1)); ++r; + { + auto s = new wxBoxSizer (wxHORIZONTAL); + s->Add(_enable_audio_language, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, DCPOMATIC_SIZER_GAP); + s->Add(_audio_language, 1, wxALIGN_CENTER_VERTICAL | wxBOTTOM, DCPOMATIC_CHECKBOX_BOTTOM_PAD); + s->Add(DCPOMATIC_SIZER_X_GAP, 0); + s->Add(_edit_audio_language, 0, wxALIGN_CENTER_VERTICAL | wxBOTTOM, DCPOMATIC_CHECKBOX_BOTTOM_PAD); + _audio_grid->Add(s, wxGBPosition(r, 0), wxGBSpan(1, 2), wxEXPAND | wxALIGN_CENTER_VERTICAL); + } + ++r; + _audio_grid->Add (_show_audio, wxGBPosition (r, 0), wxGBSpan (1, 2)); ++r; } @@ -1030,28 +1007,6 @@ DCPPanel::show_audio_clicked () void -DCPPanel::reel_type_changed () -{ - if (!_film || !_reel_type->get()) { - return; - } - - _film->set_reel_type(static_cast<ReelType>(*_reel_type->get())); -} - - -void -DCPPanel::reel_length_changed () -{ - if (!_film) { - return; - } - - _film->set_reel_length (_reel_length->GetValue() * 1000000000LL); -} - - -void DCPPanel::add_audio_processors () { _audio_processor->add(_("None"), new wxStringClientData(N_("none"))); diff --git a/src/wx/dcp_panel.h b/src/wx/dcp_panel.h index 849fe185c..6c97a41c3 100644 --- a/src/wx/dcp_panel.h +++ b/src/wx/dcp_panel.h @@ -39,6 +39,7 @@ class wxGridBagSizer; class AudioDialog; class Choice; +class DCPTimelineDialog; class Film; class FilmViewer; class InteropMetadataDialog; @@ -82,10 +83,9 @@ private: void encrypted_toggled (); void audio_processor_changed (); void show_audio_clicked (); - void reel_type_changed (); - void reel_length_changed (); void markers_clicked (); void metadata_clicked (); + void reels_clicked(); void reencode_j2k_changed (); void enable_audio_language_toggled (); void edit_audio_language_clicked (); @@ -153,19 +153,16 @@ private: wxStaticText* _standard_label; Choice* _standard; CheckBox* _encrypted; - wxStaticText* _reels_label; - Choice* _reel_type; - wxStaticText* _reel_length_label; - wxStaticText* _reel_length_gb_label; - wxSpinCtrl* _reel_length; wxButton* _markers; wxButton* _metadata; + Button* _reels; wxSizer* _audio_panel_sizer; wx_ptr<AudioDialog> _audio_dialog; wx_ptr<MarkersDialog> _markers_dialog; wx_ptr<InteropMetadataDialog> _interop_metadata_dialog; wx_ptr<SMPTEMetadataDialog> _smpte_metadata_dialog; + wx_ptr<DCPTimelineDialog> _dcp_timeline; std::shared_ptr<Film> _film; FilmViewer& _viewer; diff --git a/src/wx/dcp_referencing_dialog.cc b/src/wx/dcp_referencing_dialog.cc new file mode 100644 index 000000000..853391bd3 --- /dev/null +++ b/src/wx/dcp_referencing_dialog.cc @@ -0,0 +1,231 @@ +/* + 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 "check_box.h" +#include "dcp_referencing_dialog.h" +#include "static_text.h" +#include "wx_util.h" +#include "lib/dcp_content.h" +#include "lib/film.h" +#include "lib/types.h" +#include <wx/gbsizer.h> +#include <wx/wx.h> + + +using std::dynamic_pointer_cast; +using std::shared_ptr; +using std::string; +using std::vector; +using std::weak_ptr; +#if BOOST_VERSION >= 106100 +using namespace boost::placeholders; +#endif + + +DCPReferencingDialog::DCPReferencingDialog(wxWindow* parent, shared_ptr<const Film> film) + : wxDialog(parent, wxID_ANY, _("Version file (VF) setup")) + , _film(film) + , _dcp_grid(new wxGridBagSizer(DCPOMATIC_SIZER_X_GAP, DCPOMATIC_SIZER_Y_GAP)) + , _overall_sizer(new wxBoxSizer(wxVERTICAL)) +{ + _film_connection = film->Change.connect(boost::bind(&DCPReferencingDialog::film_changed, this, _1, _2)); + _film_content_connection = film->ContentChange.connect(boost::bind(&DCPReferencingDialog::film_content_changed, this, _1, _3)); + + _overall_sizer->Add(_dcp_grid, 1, wxALL, DCPOMATIC_DIALOG_BORDER); + SetSizer(_overall_sizer); + + auto buttons = CreateSeparatedButtonSizer(wxOK); + if (buttons) { + _overall_sizer->Add(buttons, wxSizerFlags().Expand().DoubleBorder()); + } + + setup(); +} + + +void +DCPReferencingDialog::film_changed(ChangeType type, FilmProperty property) +{ + if (type != ChangeType::DONE) { + return; + } + + if ( + property == FilmProperty::INTEROP || + property == FilmProperty::RESOLUTION || + property == FilmProperty::CONTAINER || + property == FilmProperty::REEL_TYPE || + property == FilmProperty::VIDEO_FRAME_RATE || + property == FilmProperty::CONTENT) { + setup(); + } +} + + +void +DCPReferencingDialog::film_content_changed(ChangeType type, int property) +{ + if (type != ChangeType::DONE) { + return; + } + + if ( + property != DCPContentProperty::REFERENCE_VIDEO && + property != DCPContentProperty::REFERENCE_AUDIO && + property != DCPContentProperty::REFERENCE_TEXT) { + setup(); + } +} + + +void +DCPReferencingDialog::setup() +{ + _dcps.clear(); + _dcp_grid->Clear(true); + + int column = 0; + for (auto const& heading: { _("DCP"), _("Picture"), _("Sound"), _("Subtitles"), _("Closed captions") }) { + auto text = new StaticText(this, heading); + wxFont font(*wxNORMAL_FONT); + font.SetWeight(wxFONTWEIGHT_BOLD); + text->SetFont(font); + _dcp_grid->Add(text, wxGBPosition(0, column), wxDefaultSpan, wxALL, DCPOMATIC_SIZER_GAP); + ++column; + }; + + auto const all_parts = { Part::VIDEO, Part::AUDIO, Part::SUBTITLES, Part::CLOSED_CAPTIONS }; + + int row = 1; + for (auto& content: _film->content()) { + auto dcp_content = dynamic_pointer_cast<DCPContent>(content); + if (!dcp_content) { + continue; + } + + DCP record; + record.content = dcp_content; + _dcp_grid->Add(new StaticText(this, std_to_wx(dcp_content->name())), wxGBPosition(row, 0)); + column = 1; + for (auto const& part: all_parts) { + record.check_box[part] = new CheckBox(this, wxT("")); + switch (part) { + case Part::VIDEO: + record.check_box[part]->set(dcp_content->reference_video()); + break; + case Part::AUDIO: + record.check_box[part]->set(dcp_content->reference_audio()); + break; + case Part::SUBTITLES: + record.check_box[part]->set(dcp_content->reference_text(TextType::OPEN_SUBTITLE)); + break; + case Part::CLOSED_CAPTIONS: + record.check_box[part]->set(dcp_content->reference_text(TextType::CLOSED_CAPTION)); + break; + default: + DCPOMATIC_ASSERT(false); + } + record.check_box[part]->bind(&DCPReferencingDialog::checkbox_changed, this, weak_ptr<DCPContent>(dcp_content), record.check_box[part], part); + _dcp_grid->Add(record.check_box[part], wxGBPosition(row, column++), wxDefaultSpan, wxALIGN_CENTER); + } + ++row; + + auto add_problem = [this, &row](wxString const& cannot, string why_not) { + auto reason = new StaticText(this, cannot + wxT(": ") + std_to_wx(why_not)); + wxFont font(*wxNORMAL_FONT); + font.SetStyle(wxFONTSTYLE_ITALIC); + reason->SetFont(font); + _dcp_grid->Add(reason, wxGBPosition(row, 0), wxGBSpan(1, 5), wxLEFT, DCPOMATIC_SIZER_X_GAP * 4); + ++row; + }; + + string why_not; + + if (!dcp_content->can_reference_anything(_film, why_not)) { + for (auto const& part: all_parts) { + record.check_box[part]->Enable(false); + } + add_problem(_("Cannot reference this DCP"), why_not); + } else { + if (!dcp_content->can_reference_video(_film, why_not)) { + record.check_box[Part::VIDEO]->Enable(false); + if (dcp_content->video) { + add_problem(_("Cannot reference this DCP's video"), why_not); + } + } + + if (!dcp_content->can_reference_audio(_film, why_not)) { + record.check_box[Part::AUDIO]->Enable(false); + if (dcp_content->audio) { + add_problem(_("Cannot reference this DCP's audio"), why_not); + } + } + + if (!dcp_content->can_reference_text(_film, TextType::OPEN_SUBTITLE, why_not)) { + record.check_box[Part::SUBTITLES]->Enable(false); + if (dcp_content->text_of_original_type(TextType::OPEN_SUBTITLE)) { + add_problem(_("Cannot reference this DCP's subtitles"), why_not); + } + } + + if (!dcp_content->can_reference_text(_film, TextType::CLOSED_CAPTION, why_not)) { + record.check_box[Part::CLOSED_CAPTIONS]->Enable(false); + if (dcp_content->text_of_original_type(TextType::CLOSED_CAPTION)) { + add_problem(_("Cannot reference this DCP's closed captions"), why_not); + } + } + } + + _dcps.push_back(record); + } + + _dcp_grid->Layout(); + _overall_sizer->Layout(); + _overall_sizer->SetSizeHints(this); +} + + +void +DCPReferencingDialog::checkbox_changed(weak_ptr<DCPContent> weak_content, CheckBox* check_box, Part part) +{ + auto content = weak_content.lock(); + if (!content) { + return; + } + + switch (part) { + case Part::VIDEO: + content->set_reference_video(check_box->get()); + break; + case Part::AUDIO: + content->set_reference_audio(check_box->get()); + break; + case Part::SUBTITLES: + content->set_reference_text(TextType::OPEN_SUBTITLE, check_box->get()); + break; + case Part::CLOSED_CAPTIONS: + content->set_reference_text(TextType::CLOSED_CAPTION, check_box->get()); + break; + default: + DCPOMATIC_ASSERT(false); + } +} + diff --git a/src/wx/dcp_referencing_dialog.h b/src/wx/dcp_referencing_dialog.h new file mode 100644 index 000000000..e49292c22 --- /dev/null +++ b/src/wx/dcp_referencing_dialog.h @@ -0,0 +1,70 @@ +/* + 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 "lib/enum_indexed_vector.h" +#include "lib/film.h" +#include <wx/wx.h> +#include <memory> +#include <vector> + + +class CheckBox; +class DCPContent; +class Film; +class wxBoxSizer; +class wxGridBagSizer; + + +class DCPReferencingDialog : public wxDialog +{ +public: + DCPReferencingDialog(wxWindow* parent, std::shared_ptr<const Film> film); + +private: + enum class Part { + VIDEO, + AUDIO, + SUBTITLES, + CLOSED_CAPTIONS, + COUNT + }; + + void setup(); + void checkbox_changed(std::weak_ptr<DCPContent> content, CheckBox* check_box, Part part); + void film_changed(ChangeType type, FilmProperty property); + void film_content_changed(ChangeType type, int property); + + std::shared_ptr<const Film> _film; + + wxGridBagSizer* _dcp_grid; + wxBoxSizer* _overall_sizer; + + struct DCP { + std::shared_ptr<DCPContent> content; + EnumIndexedVector<CheckBox*, Part> check_box; + }; + + std::vector<DCP> _dcps; + + boost::signals2::scoped_connection _film_connection; + boost::signals2::scoped_connection _film_content_connection; +}; + diff --git a/src/wx/dcp_timeline.cc b/src/wx/dcp_timeline.cc new file mode 100644 index 000000000..119d07b03 --- /dev/null +++ b/src/wx/dcp_timeline.cc @@ -0,0 +1,615 @@ +/* + 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 "check_box.h" +#include "colours.h" +#include "dcp_timeline.h" +#include "dcp_timeline_reel_marker_view.h" +#include "dcpomatic_choice.h" +#include "dcpomatic_spin_ctrl.h" +#include "timecode.h" +#include "wx_util.h" +#include "lib/atmos_content.h" +#include "lib/audio_content.h" +#include "lib/constants.h" +#include "lib/film.h" +#include "lib/text_content.h" +#include "lib/video_content.h" +#include <dcp/scope_guard.h> +LIBDCP_DISABLE_WARNINGS +#include <wx/graphics.h> +LIBDCP_ENABLE_WARNINGS + + +using std::dynamic_pointer_cast; +using std::make_shared; +using std::shared_ptr; +using std::vector; +using boost::optional; +#if BOOST_VERSION >= 106100 +using namespace boost::placeholders; +#endif +using namespace dcpomatic; + + +auto constexpr reel_marker_y_pos = 48; +auto constexpr content_y_pos = 112; +auto constexpr content_type_height = 12; + +enum { + ID_add_reel_boundary, +}; + + +class ReelBoundary +{ +public: + ReelBoundary(wxWindow* parent, wxGridBagSizer* sizer, int index, DCPTime maximum, int fps, DCPTimeline& timeline, bool editable) + : _label(new wxStaticText(parent, wxID_ANY, wxString::Format(_("Reel %d to reel %d"), index + 1, index + 2))) + , _timecode(new Timecode<DCPTime>(parent, true)) + , _index(index) + , _view(timeline, reel_marker_y_pos) + , _fps(fps) + { + sizer->Add(_label, wxGBPosition(index, 0), wxDefaultSpan, wxALIGN_CENTER_VERTICAL); + sizer->Add(_timecode, wxGBPosition(index, 1)); + + _timecode->set_maximum(maximum.split(fps)); + _timecode->set_editable(editable); + _timecode->Changed.connect(boost::bind(&ReelBoundary::timecode_changed, this)); + } + + ~ReelBoundary() + { + if (_label) { + _label->Destroy(); + } + + if (_timecode) { + _timecode->Destroy(); + } + } + + ReelBoundary(ReelBoundary const&) = delete; + ReelBoundary& operator=(ReelBoundary const&) = delete; + + ReelBoundary(ReelBoundary&& other) = delete; + ReelBoundary& operator=(ReelBoundary&& other) = delete; + + void set_time(DCPTime time) + { + if (_timecode) { + _timecode->set(time, _fps); + } + _view.set_time(time); + } + + dcpomatic::DCPTime time() const { + return _view.time(); + } + + int index() const { + return _index; + } + + DCPTimelineReelMarkerView& view() { + return _view; + } + + DCPTimelineReelMarkerView const& view() const { + return _view; + } + + boost::signals2::signal<void (int, dcpomatic::DCPTime)> Changed; + +private: + void timecode_changed() { + set_time(_timecode->get(_fps)); + Changed(_index, time()); + } + + wxStaticText* _label = nullptr; + Timecode<dcpomatic::DCPTime>* _timecode = nullptr; + int _index = 0; + DCPTimelineReelMarkerView _view; + int _fps; +}; + + +DCPTimeline::DCPTimeline(wxWindow* parent, shared_ptr<Film> film) + : Timeline(parent) + , _film(film) + , _canvas(new wxScrolledCanvas(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxFULL_REPAINT_ON_RESIZE)) + , _reel_settings(new wxPanel(this, wxID_ANY)) + , _reel_detail(new wxPanel(this, wxID_ANY)) + , _reel_detail_sizer(new wxGridBagSizer(DCPOMATIC_SIZER_X_GAP, DCPOMATIC_SIZER_Y_GAP)) +{ +#ifndef __WXOSX__ + _canvas->SetDoubleBuffered(true); +#endif + _reel_detail->SetSizer(_reel_detail_sizer); + + auto sizer = new wxBoxSizer(wxVERTICAL); + sizer->Add(_reel_settings, 0); + sizer->Add(_canvas, 0, wxEXPAND); + sizer->Add(_reel_detail, 1, wxEXPAND | wxALL, DCPOMATIC_DIALOG_BORDER); + SetSizer(sizer); + + SetMinSize(wxSize(640, 480)); + _canvas->SetMinSize({-1, content_y_pos + content_type_height * 4}); + + _canvas->Bind(wxEVT_PAINT, boost::bind(&DCPTimeline::paint, this)); + _canvas->Bind(wxEVT_SIZE, boost::bind(&DCPTimeline::setup_pixels_per_second, this)); + _canvas->Bind(wxEVT_LEFT_DOWN, boost::bind(&DCPTimeline::left_down, this, _1)); + _canvas->Bind(wxEVT_RIGHT_DOWN, boost::bind(&DCPTimeline::right_down, this, _1)); + _canvas->Bind(wxEVT_LEFT_UP, boost::bind(&DCPTimeline::left_up, this, _1)); + _canvas->Bind(wxEVT_MOTION, boost::bind(&DCPTimeline::mouse_moved, this, _1)); + + _film_connection = film->Change.connect(boost::bind(&DCPTimeline::film_changed, this, _1, _2)); + + _menu = new wxMenu; + _add_reel_boundary = _menu->Append(ID_add_reel_boundary, _("Add reel")); + _canvas->Bind(wxEVT_MENU, boost::bind(&DCPTimeline::add_reel_boundary, this)); + + setup_reel_settings(); + setup_reel_boundaries(); + + sizer->Layout(); + setup_pixels_per_second(); + setup_sensitivity(); +} + + +void +DCPTimeline::add_reel_boundary() +{ + auto boundaries = film()->custom_reel_boundaries(); + boundaries.push_back(DCPTime::from_seconds(_right_down_position.x / _pixels_per_second.get_value_or(1))); + film()->set_custom_reel_boundaries(boundaries); +} + + +void +DCPTimeline::film_changed(ChangeType type, FilmProperty property) +{ + if (type != ChangeType::DONE) { + return; + } + + switch (property) { + case FilmProperty::REEL_TYPE: + case FilmProperty::REEL_LENGTH: + case FilmProperty::CUSTOM_REEL_BOUNDARIES: + setup_sensitivity(); + setup_reel_boundaries(); + break; + case FilmProperty::CONTENT: + case FilmProperty::CONTENT_ORDER: + setup_pixels_per_second(); + Refresh(); + break; + default: + break; + } +} + + +void +DCPTimeline::setup_sensitivity() +{ + _snap->Enable(editable()); + _maximum_reel_size->Enable(film()->reel_type() == ReelType::BY_LENGTH); +} + + +void +DCPTimeline::setup_reel_settings() +{ + auto sizer = new wxGridBagSizer(DCPOMATIC_SIZER_X_GAP, DCPOMATIC_SIZER_Y_GAP); + _reel_settings->SetSizer(sizer); + + int r = 0; + add_label_to_sizer(sizer, _reel_settings, _("Reel mode"), true, wxGBPosition(r, 0)); + _reel_type = new Choice(_reel_settings); + _reel_type->add(_("Single reel")); + _reel_type->add(_("Split by video content")); + _reel_type->add(_("Split by maximum reel size")); + _reel_type->add(_("Custom")); + sizer->Add(_reel_type, wxGBPosition(r, 1)); + ++r; + + add_label_to_sizer(sizer, _reel_settings, _("Maximum reel size"), true, wxGBPosition(r, 0)); + _maximum_reel_size = new SpinCtrl(_reel_settings, DCPOMATIC_SPIN_CTRL_WIDTH); + _maximum_reel_size->SetRange(1, 1000); + { + auto s = new wxBoxSizer(wxHORIZONTAL); + s->Add(_maximum_reel_size, 0); + add_label_to_sizer(s, _reel_settings, _("GB"), false, 0, wxALIGN_CENTER_VERTICAL | wxLEFT); + sizer->Add(s, wxGBPosition(r, 1)); + } + ++r; + + _snap = new CheckBox(_reel_settings, _("Snap when dragging")); + sizer->Add(_snap, wxGBPosition(r, 1)); + ++r; + + _reel_type->set(static_cast<int>(film()->reel_type())); + _maximum_reel_size->SetValue(film()->reel_length() / 1000000000LL); + + _reel_type->bind(&DCPTimeline::reel_mode_changed, this); + _maximum_reel_size->Bind(wxEVT_SPINCTRL, boost::bind(&DCPTimeline::maximum_reel_size_changed, this)); +} + + +void +DCPTimeline::reel_mode_changed() +{ + film()->set_reel_type(static_cast<ReelType>(*_reel_type->get())); +} + + +void +DCPTimeline::maximum_reel_size_changed() +{ + film()->set_reel_length(_maximum_reel_size->GetValue() * 1000000000LL); +} + + +void +DCPTimeline::set_reel_boundary(int index, DCPTime time) +{ + auto boundaries = film()->custom_reel_boundaries(); + DCPOMATIC_ASSERT(index >= 0 && index < static_cast<int>(boundaries.size())); + boundaries[index] = time.round(film()->video_frame_rate()); + film()->set_custom_reel_boundaries(boundaries); +} + + +void +DCPTimeline::setup_reel_boundaries() +{ + auto const reels = film()->reels(); + if (reels.empty()) { + _reel_boundaries.clear(); + return; + } + + size_t const boundaries = reels.size() - 1; + auto const maximum = film()->length(); + for (size_t i = _reel_boundaries.size(); i < boundaries; ++i) { + auto boundary = std::make_shared<ReelBoundary>( + _reel_detail, _reel_detail_sizer, i, maximum, film()->video_frame_rate(), *this, editable() + ); + + boundary->Changed.connect(boost::bind(&DCPTimeline::set_reel_boundary, this, _1, _2)); + _reel_boundaries.push_back(boundary); + } + + _reel_boundaries.resize(boundaries); + + auto const active = editable(); + for (size_t i = 0; i < boundaries; ++i) { + _reel_boundaries[i]->set_time(reels[i].to); + _reel_boundaries[i]->view().set_active(active); + } + + _reel_detail_sizer->Layout(); + _canvas->Refresh(); +} + + +void +DCPTimeline::paint() +{ + wxPaintDC dc(_canvas); + dc.Clear(); + + if (film()->content().empty()) { + return; + } + + _canvas->DoPrepareDC(dc); + + auto gc = wxGraphicsContext::Create(dc); + if (!gc) { + return; + } + + dcp::ScopeGuard sg = [gc]() { delete gc; }; + + gc->SetAntialiasMode(wxANTIALIAS_DEFAULT); + + paint_reels(gc); + paint_content(gc); +} + + +void +DCPTimeline::paint_reels(wxGraphicsContext* gc) +{ + constexpr int x_offset = 2; + + for (auto const& boundary: _reel_boundaries) { + boundary->view().paint(gc); + } + + gc->SetFont(gc->CreateFont(*wxNORMAL_FONT, wxColour(0, 0, 0))); + gc->SetPen(*wxThePenList->FindOrCreatePen(wxColour(0, 0, 0), 2, wxPENSTYLE_SOLID)); + + auto const pps = pixels_per_second().get_value_or(1); + + auto start = gc->CreatePath(); + start.MoveToPoint(x_offset, reel_marker_y_pos); + start.AddLineToPoint(x_offset, reel_marker_y_pos + DCPTimelineReelMarkerView::HEIGHT); + gc->StrokePath(start); + + auto const length = film()->length().seconds() * pps; + auto end = gc->CreatePath(); + end.MoveToPoint(x_offset + length, reel_marker_y_pos); + end.AddLineToPoint(x_offset + length, reel_marker_y_pos + DCPTimelineReelMarkerView::HEIGHT); + gc->StrokePath(end); + + auto const y = reel_marker_y_pos + DCPTimelineReelMarkerView::HEIGHT * 3 / 4; + + auto paint_reel = [gc](double from, double to, int index) { + auto path = gc->CreatePath(); + path.MoveToPoint(from, y); + path.AddLineToPoint(to, y); + gc->StrokePath(path); + + auto str = wxString::Format(wxT("#%d"), index + 1); + wxDouble str_width; + wxDouble str_height; + wxDouble str_descent; + wxDouble str_leading; + gc->GetTextExtent(str, &str_width, &str_height, &str_descent, &str_leading); + + if (str_width < (to - from)) { + gc->DrawText(str, (from + to - str_width) / 2, y - str_height - 2); + } + }; + + gc->SetPen(*wxThePenList->FindOrCreatePen(wxColour(0, 0, 255), 2, wxPENSTYLE_DOT)); + int index = 0; + DCPTime last; + for (auto const& boundary: _reel_boundaries) { + paint_reel(last.seconds() * pps + 2, boundary->time().seconds() * pps, index++); + last = boundary->time(); + } + + paint_reel(last.seconds() * pps + 2, film()->length().seconds() * pps, index); +} + + +void +DCPTimeline::paint_content(wxGraphicsContext* gc) +{ + auto const pps = pixels_per_second().get_value_or(1); + auto const film = this->film(); + + auto const& solid_pen = *wxThePenList->FindOrCreatePen(wxColour(0, 0, 0), 1, wxPENSTYLE_SOLID); + auto const& dotted_pen = *wxThePenList->FindOrCreatePen(wxColour(0, 0, 0), 1, wxPENSTYLE_DOT); + + auto const& video_brush = *wxTheBrushList->FindOrCreateBrush(VIDEO_CONTENT_COLOUR, wxBRUSHSTYLE_SOLID); + auto const& audio_brush = *wxTheBrushList->FindOrCreateBrush(AUDIO_CONTENT_COLOUR, wxBRUSHSTYLE_SOLID); + auto const& text_brush = *wxTheBrushList->FindOrCreateBrush(TEXT_CONTENT_COLOUR, wxBRUSHSTYLE_SOLID); + auto const& atmos_brush = *wxTheBrushList->FindOrCreateBrush(ATMOS_CONTENT_COLOUR, wxBRUSHSTYLE_SOLID); + + auto maybe_draw = + [gc, film, pps, solid_pen, dotted_pen] + (shared_ptr<Content> content, shared_ptr<ContentPart> part, wxBrush brush, int offset) { + if (part) { + auto const y = content_y_pos + offset * content_type_height + 1; + gc->SetPen(solid_pen); + gc->SetBrush(brush); + gc->DrawRectangle( + content->position().seconds() * pps, + y, + content->length_after_trim(film).seconds() * pps, + content_type_height - 2 + ); + + gc->SetPen(dotted_pen); + for (auto split: content->reel_split_points(film)) { + if (split != content->position()) { + auto path = gc->CreatePath(); + path.MoveToPoint(split.seconds() * pps, y); + path.AddLineToPoint(split.seconds() * pps, y + content_type_height - 2); + gc->StrokePath(path); + } + } + } + }; + + for (auto content: film->content()) { + maybe_draw(content, dynamic_pointer_cast<ContentPart>(content->video), video_brush, 0); + maybe_draw(content, dynamic_pointer_cast<ContentPart>(content->audio), audio_brush, 1); + for (auto text: content->text) { + maybe_draw(content, dynamic_pointer_cast<ContentPart>(text), text_brush, 2); + } + maybe_draw(content, dynamic_pointer_cast<ContentPart>(content->atmos), atmos_brush, 3); + } +} + + +void +DCPTimeline::setup_pixels_per_second() +{ + set_pixels_per_second((_canvas->GetSize().GetWidth() - 4) / std::max(1.0, film()->length().seconds())); +} + + +shared_ptr<ReelBoundary> +DCPTimeline::event_to_reel_boundary(wxMouseEvent& ev) const +{ + Position<int> const position(ev.GetX(), ev.GetY()); + auto iter = std::find_if(_reel_boundaries.begin(), _reel_boundaries.end(), [position](shared_ptr<const ReelBoundary> boundary) { + return boundary->view().bbox().contains(position); + }); + + if (iter == _reel_boundaries.end()) { + return {}; + } + + return *iter; +} + + +void +DCPTimeline::left_down(wxMouseEvent& ev) +{ + if (!editable()) { + return; + } + + if (auto boundary = event_to_reel_boundary(ev)) { + auto const snap_distance = DCPTime::from_seconds((_canvas->GetSize().GetWidth() / _pixels_per_second.get_value_or(1)) / SNAP_SUBDIVISION); + _drag = DCPTimeline::Drag( + boundary, + _reel_boundaries, + film(), + static_cast<int>(ev.GetX() - boundary->time().seconds() * _pixels_per_second.get_value_or(0)), + _snap->get(), + snap_distance + ); + } else { + _drag = boost::none; + } +} + + +void +DCPTimeline::right_down(wxMouseEvent& ev) +{ + _right_down_position = ev.GetPosition(); + _canvas->PopupMenu(_menu, _right_down_position); +} + + +void +DCPTimeline::left_up(wxMouseEvent&) +{ + if (!_drag) { + return; + } + + set_reel_boundary(_drag->reel_boundary->index(), _drag->time()); + _drag = boost::none; +} + + +void +DCPTimeline::mouse_moved(wxMouseEvent& ev) +{ + if (!_drag) { + return; + } + + auto time = DCPTime::from_seconds((ev.GetPosition().x - _drag->offset) / _pixels_per_second.get_value_or(1)); + time = std::max(_drag->previous ? _drag->previous->time() : DCPTime(), time); + time = std::min(_drag->next ? _drag->next->time() : film()->length(), time); + _drag->set_time(time); + _canvas->RefreshRect({0, reel_marker_y_pos - 2, _canvas->GetSize().GetWidth(), DCPTimelineReelMarkerView::HEIGHT + 4}, true); +} + + +void +DCPTimeline::force_redraw(dcpomatic::Rect<int> const & r) +{ + _canvas->RefreshRect(wxRect(r.x, r.y, r.width, r.height), false); +} + + +shared_ptr<Film> +DCPTimeline::film() const +{ + auto film = _film.lock(); + DCPOMATIC_ASSERT(film); + return film; +} + + +bool +DCPTimeline::editable() const +{ + return film()->reel_type() == ReelType::CUSTOM; +} + + +DCPTimeline::Drag::Drag( + shared_ptr<ReelBoundary> reel_boundary_, + vector<shared_ptr<ReelBoundary>> const& reel_boundaries, + shared_ptr<const Film> film, + int offset_, + bool snap, + DCPTime snap_distance + ) + : reel_boundary(reel_boundary_) + , offset(offset_) + , _snap_distance(snap_distance) +{ + auto iter = std::find(reel_boundaries.begin(), reel_boundaries.end(), reel_boundary); + auto index = std::distance(reel_boundaries.begin(), iter); + + if (index > 0) { + previous = reel_boundaries[index - 1]; + } + if (index < static_cast<int>(reel_boundaries.size() - 1)) { + next = reel_boundaries[index + 1]; + } + + if (snap) { + for (auto content: film->content()) { + for (auto split: content->reel_split_points(film)) { + _snaps.push_back(split); + } + } + } +} + + +void +DCPTimeline::Drag::set_time(DCPTime time) +{ + optional<DCPTime> nearest_distance; + optional<DCPTime> nearest_time; + for (auto snap: _snaps) { + auto const distance = time - snap; + if (!nearest_distance || distance.abs() < nearest_distance->abs()) { + nearest_distance = distance.abs(); + nearest_time = snap; + } + } + + if (nearest_distance && *nearest_distance < _snap_distance) { + reel_boundary->set_time(*nearest_time); + } else { + reel_boundary->set_time(time); + } +} + + +DCPTime +DCPTimeline::Drag::time() const +{ + return reel_boundary->time(); +} + diff --git a/src/wx/dcp_timeline.h b/src/wx/dcp_timeline.h new file mode 100644 index 000000000..3413c2814 --- /dev/null +++ b/src/wx/dcp_timeline.h @@ -0,0 +1,123 @@ +/* + 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_DCP_TIMELINE_H +#define DCPOMATIC_DCP_TIMELINE_H + + +#include "timecode.h" +#include "timeline.h" +#include "lib/rect.h" +#include <dcp/warnings.h> +LIBDCP_DISABLE_WARNINGS +#include <wx/wx.h> +LIBDCP_ENABLE_WARNINGS +#include <memory> + + +class CheckBox; +class Choice; +class Film; +class ReelBoundary; +class SpinCtrl; +class wxGridBagSizer; + + +class DCPTimeline : public Timeline +{ +public: + DCPTimeline(wxWindow* parent, std::shared_ptr<Film> film); + + void force_redraw(dcpomatic::Rect<int> const &); + +private: + void paint(); + void paint_reels(wxGraphicsContext* gc); + void paint_content(wxGraphicsContext* gc); + void setup_pixels_per_second(); + void left_down(wxMouseEvent& ev); + void right_down(wxMouseEvent& ev); + void left_up(wxMouseEvent& ev); + void mouse_moved(wxMouseEvent& ev); + void reel_mode_changed(); + void maximum_reel_size_changed(); + void film_changed(ChangeType type, FilmProperty property); + std::shared_ptr<Film> film() const; + void setup_sensitivity(); + + void add_reel_boundary(); + void setup_reel_settings(); + void setup_reel_boundaries(); + std::shared_ptr<ReelBoundary> event_to_reel_boundary(wxMouseEvent& ev) const; + void set_reel_boundary(int index, dcpomatic::DCPTime time); + bool editable() const; + + std::weak_ptr<Film> _film; + + wxScrolledCanvas* _canvas; + + class Drag + { + public: + Drag( + std::shared_ptr<ReelBoundary> reel_boundary_, + std::vector<std::shared_ptr<ReelBoundary>> const& reel_boundaries, + std::shared_ptr<const Film> film, + int offset_, + bool snap, + dcpomatic::DCPTime snap_distance + ); + + std::shared_ptr<ReelBoundary> reel_boundary; + std::shared_ptr<ReelBoundary> previous; + std::shared_ptr<ReelBoundary> next; + int offset = 0; + + void set_time(dcpomatic::DCPTime time); + dcpomatic::DCPTime time() const; + + private: + std::vector<dcpomatic::DCPTime> _snaps; + dcpomatic::DCPTime _snap_distance; + }; + + boost::optional<Drag> _drag; + + wxPoint _right_down_position; + + wxPanel* _reel_settings; + Choice* _reel_type; + SpinCtrl* _maximum_reel_size; + CheckBox* _snap; + wxPanel* _reel_detail; + wxGridBagSizer* _reel_detail_sizer; + + wxMenu* _menu; + wxMenuItem* _add_reel_boundary; + + boost::signals2::scoped_connection _film_connection; + + std::vector<std::shared_ptr<ReelBoundary>> _reel_boundaries; +}; + + +#endif + diff --git a/src/wx/dcp_timeline_dialog.cc b/src/wx/dcp_timeline_dialog.cc new file mode 100644 index 000000000..2cf6a74f1 --- /dev/null +++ b/src/wx/dcp_timeline_dialog.cc @@ -0,0 +1,78 @@ +/* + Copyright (C) 2013-2021 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_panel.h" +#include "dcp_timeline_dialog.h" +#include "film_editor.h" +#include "wx_util.h" +#include "lib/compose.hpp" +#include "lib/cross.h" +#include "lib/film.h" +#include "lib/playlist.h" +#include <dcp/warnings.h> +LIBDCP_DISABLE_WARNINGS +#include <wx/graphics.h> +LIBDCP_ENABLE_WARNINGS +#include <list> + + +using std::list; +using std::shared_ptr; +using std::string; +using std::weak_ptr; +#if BOOST_VERSION >= 106100 +using namespace boost::placeholders; +#endif + + +DCPTimelineDialog::DCPTimelineDialog(wxWindow* parent, shared_ptr<Film> film) + : wxDialog( + parent, + wxID_ANY, + _("Reels"), + wxDefaultPosition, + wxSize(640, 512), +#ifdef DCPOMATIC_OSX + /* I can't get wxFRAME_FLOAT_ON_PARENT to work on OS X, and although wxSTAY_ON_TOP keeps + the window above all others (and not just our own) it's better than nothing for now. + */ + wxDEFAULT_DIALOG_STYLE | wxRESIZE_BORDER | wxFULL_REPAINT_ON_RESIZE | wxSTAY_ON_TOP +#else + wxDEFAULT_DIALOG_STYLE | wxRESIZE_BORDER | wxFULL_REPAINT_ON_RESIZE | wxFRAME_FLOAT_ON_PARENT +#endif + ) + , _timeline(this, film) +{ + auto sizer = new wxBoxSizer(wxVERTICAL); + sizer->Add (&_timeline, 1, wxEXPAND | wxALL, 12); + +#ifdef DCPOMATIC_LINUX + auto buttons = CreateSeparatedButtonSizer (wxCLOSE); + if (buttons) { + sizer->Add (buttons, wxSizerFlags().Expand().DoubleBorder()); + } +#endif + + SetSizer(sizer); + sizer->Layout(); + sizer->SetSizeHints(this); +} + diff --git a/src/wx/dcp_timeline_dialog.h b/src/wx/dcp_timeline_dialog.h new file mode 100644 index 000000000..d1293ca26 --- /dev/null +++ b/src/wx/dcp_timeline_dialog.h @@ -0,0 +1,39 @@ +/* + 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_timeline.h" +#include <dcp/warnings.h> +LIBDCP_DISABLE_WARNINGS +#include <wx/wx.h> +LIBDCP_ENABLE_WARNINGS +#include <memory> + + +class DCPTimelineDialog : public wxDialog +{ +public: + DCPTimelineDialog(wxWindow* parent, std::shared_ptr<Film> film); + +private: + std::weak_ptr<Film> _film; + DCPTimeline _timeline; +}; + diff --git a/src/wx/dcp_timeline_reel_marker_view.cc b/src/wx/dcp_timeline_reel_marker_view.cc new file mode 100644 index 000000000..1c97ca175 --- /dev/null +++ b/src/wx/dcp_timeline_reel_marker_view.cc @@ -0,0 +1,71 @@ +/* + 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_timeline_reel_marker_view.h" +LIBDCP_DISABLE_WARNINGS +#include <wx/graphics.h> +LIBDCP_ENABLE_WARNINGS + + +using namespace std; +using namespace dcpomatic; + + +DCPTimelineReelMarkerView::DCPTimelineReelMarkerView(DCPTimeline& timeline, int y_pos) + : DCPTimelineView(timeline) + , _y_pos(y_pos) +{ + +} + + +int +DCPTimelineReelMarkerView::x_pos() const +{ + /* Nudge it over slightly so that the full line width is drawn on the left hand side */ + return time_x(_time) + 2; +} + + +void +DCPTimelineReelMarkerView::do_paint(wxGraphicsContext* gc) +{ + wxColour const outline = _active ? wxColour(0, 0, 0) : wxColour(128, 128, 128); + wxColour const fill = _active ? wxColour(255, 0, 0) : wxColour(192, 192, 192); + gc->SetPen(*wxThePenList->FindOrCreatePen(outline, 2, wxPENSTYLE_SOLID)); + gc->SetBrush(*wxTheBrushList->FindOrCreateBrush(fill, wxBRUSHSTYLE_SOLID)); + + gc->DrawRectangle(x_pos(), _y_pos, HEAD_SIZE, HEAD_SIZE); + + auto path = gc->CreatePath(); + path.MoveToPoint(x_pos(), _y_pos + HEAD_SIZE + TAIL_LENGTH); + path.AddLineToPoint(x_pos(), _y_pos); + gc->StrokePath(path); + gc->FillPath(path); +} + + +dcpomatic::Rect<int> +DCPTimelineReelMarkerView::bbox() const +{ + return { x_pos(), _y_pos, HEAD_SIZE, HEAD_SIZE + TAIL_LENGTH }; +} + diff --git a/src/wx/dcp_timeline_reel_marker_view.h b/src/wx/dcp_timeline_reel_marker_view.h new file mode 100644 index 000000000..273d98259 --- /dev/null +++ b/src/wx/dcp_timeline_reel_marker_view.h @@ -0,0 +1,59 @@ +/* + 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_timeline_view.h" + + +class DCPTimeline; + + +class DCPTimelineReelMarkerView : public DCPTimelineView +{ +public: + DCPTimelineReelMarkerView(DCPTimeline& timeline, int y_pos); + + dcpomatic::Rect<int> bbox() const override; + + dcpomatic::DCPTime time() const { + return _time; + } + + void set_time(dcpomatic::DCPTime time) { + _time = time; + } + + void set_active(bool active) { + _active = active; + } + + static auto constexpr HEAD_SIZE = 16; + static auto constexpr TAIL_LENGTH = 28; + static auto constexpr HEIGHT = HEAD_SIZE + TAIL_LENGTH; + +private: + void do_paint(wxGraphicsContext* gc) override; + int x_pos() const; + + dcpomatic::DCPTime _time; + int _y_pos; + bool _active = false; +}; + diff --git a/src/wx/dcp_timeline_view.h b/src/wx/dcp_timeline_view.h new file mode 100644 index 000000000..24a75cad2 --- /dev/null +++ b/src/wx/dcp_timeline_view.h @@ -0,0 +1,44 @@ +/* + Copyright (C) 2013-2021 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_timeline.h" +#include "timeline_view.h" + + +class DCPTimelineView : public TimelineView<DCPTimeline> +{ +public: + explicit DCPTimelineView(DCPTimeline& timeline) + : TimelineView(timeline) + {} + + void paint(wxGraphicsContext* gc) + { + _last_paint_bbox = bbox(); + do_paint(gc); + } + +protected: + virtual void do_paint(wxGraphicsContext* context) = 0; +}; + + + diff --git a/src/wx/dkdm_dialog.cc b/src/wx/dkdm_dialog.cc index 34a8d6284..82c2e3598 100644 --- a/src/wx/dkdm_dialog.cc +++ b/src/wx/dkdm_dialog.cc @@ -161,7 +161,7 @@ DKDMDialog::make_clicked () list<KDMWithMetadataPtr> kdms; try { for (auto i: _recipients->recipients()) { - auto p = kdm_for_dkdm_recipient (film, _cpl->cpl(), i, _timing->from(), _timing->until()); + auto p = kdm_for_dkdm_recipient(film, _cpl->cpl(), i, _timing->from(), _timing->until()); if (p) { kdms.push_back (p); } diff --git a/src/wx/file_picker_ctrl.cc b/src/wx/file_picker_ctrl.cc index 7aa0bfb40..c6aba45b7 100644 --- a/src/wx/file_picker_ctrl.cc +++ b/src/wx/file_picker_ctrl.cc @@ -32,8 +32,8 @@ LIBDCP_ENABLE_WARNINGS #include <boost/filesystem.hpp> -using namespace std; -using namespace boost; +using std::string; +using boost::optional; FilePickerCtrl::FilePickerCtrl( @@ -42,9 +42,9 @@ FilePickerCtrl::FilePickerCtrl( wxString wildcard, bool open, bool warn_overwrite, - std::string initial_path_key, + string initial_path_key, optional<std::string> initial_filename, - optional<filesystem::path> override_path + optional<boost::filesystem::path> override_path ) : wxPanel (parent) , _prompt (prompt) diff --git a/src/wx/film_editor.cc b/src/wx/film_editor.cc index 9f54db58e..fae02787a 100644 --- a/src/wx/film_editor.cc +++ b/src/wx/film_editor.cc @@ -54,13 +54,15 @@ FilmEditor::FilmEditor(wxWindow* parent, FilmViewer& viewer) { auto s = new wxBoxSizer (wxVERTICAL); - auto notebook = new wxNotebook(this, wxID_ANY); - s->Add(notebook, 1, wxEXPAND); + _notebook = new wxNotebook(this, wxID_ANY); + s->Add(_notebook, 1, wxEXPAND); - _content_panel = new ContentPanel(notebook, _film, viewer); - notebook->AddPage(_content_panel->window(), _("Content"), true); - _dcp_panel = new DCPPanel(notebook, _film, viewer); - notebook->AddPage(_dcp_panel->panel (), _("DCP"), false); + _content_panel = new ContentPanel(_notebook, _film, viewer); + _notebook->AddPage(_content_panel->window(), _("Content"), true); + _dcp_panel = new DCPPanel(_notebook, _film, viewer); + _notebook->AddPage(_dcp_panel->panel (), _("DCP"), false); + + _notebook->Bind(wxEVT_NOTEBOOK_PAGE_CHANGED, boost::bind(&FilmEditor::page_changed, this, _1)); JobManager::instance()->ActiveJobsChanged.connect ( bind(&FilmEditor::active_jobs_changed, this, _2) @@ -71,6 +73,18 @@ FilmEditor::FilmEditor(wxWindow* parent, FilmViewer& viewer) } +void +FilmEditor::page_changed(wxBookCtrlEvent& ev) +{ + /* One of these events arrives early on with GetOldSelection() being a non-existent tab, + * and we want to ignore that. + */ + if (_film && ev.GetOldSelection() < 2) { + _film->set_ui_state("FilmEditorTab", ev.GetSelection() == 0 ? "content" : "dcp"); + } +} + + /** Called when the metadata stored in the Film object has changed; * so that we can update the GUI. * @param p Property of the Film that has changed. @@ -144,6 +158,13 @@ FilmEditor::set_film (shared_ptr<Film> film) if (!_film->content().empty()) { _content_panel->set_selection (_film->content().front()); } + + auto tab = _film->ui_state("FilmEditorTab").get_value_or("content"); + if (tab == "content") { + _notebook->SetSelection(0); + } else if (tab == "dcp") { + _notebook->SetSelection(1); + } } diff --git a/src/wx/film_editor.h b/src/wx/film_editor.h index a3df266ad..54d639ef5 100644 --- a/src/wx/film_editor.h +++ b/src/wx/film_editor.h @@ -32,11 +32,12 @@ LIBDCP_ENABLE_WARNINGS #include <boost/signals2.hpp> -class wxNotebook; -class Film; class ContentPanel; class DCPPanel; +class Film; class FilmViewer; +class wxBookCtrlEvent; +class wxNotebook; /** @class FilmEditor @@ -71,6 +72,9 @@ private: void set_general_sensitivity (bool); void active_jobs_changed (boost::optional<std::string>); + void page_changed(wxBookCtrlEvent& ev); + + wxNotebook* _notebook; ContentPanel* _content_panel; DCPPanel* _dcp_panel; diff --git a/src/wx/film_name_location_dialog.cc b/src/wx/film_name_location_dialog.cc index 05ffa7a68..9d309810e 100644 --- a/src/wx/film_name_location_dialog.cc +++ b/src/wx/film_name_location_dialog.cc @@ -35,11 +35,12 @@ LIBDCP_ENABLE_WARNINGS #include <boost/filesystem.hpp> -using namespace std; -using namespace boost; +using std::string; +using boost::bind; +using boost::optional; -optional<filesystem::path> FilmNameLocationDialog::_directory; +optional<boost::filesystem::path> FilmNameLocationDialog::_directory; FilmNameLocationDialog::FilmNameLocationDialog (wxWindow* parent, wxString title, bool offer_templates) @@ -112,10 +113,10 @@ FilmNameLocationDialog::folder_changed () } -filesystem::path +boost::filesystem::path FilmNameLocationDialog::path () const { - filesystem::path p; + boost::filesystem::path p; p /= wx_to_std (_folder->GetPath()); p /= wx_to_std (_name->GetValue()); return p; @@ -139,7 +140,7 @@ FilmNameLocationDialog::template_name () const bool FilmNameLocationDialog::check_path () { - if (filesystem::is_directory(path()) && !filesystem::is_empty(path())) { + if (boost::filesystem::is_directory(path()) && !boost::filesystem::is_empty(path())) { if (!confirm_dialog ( this, std_to_wx ( @@ -150,7 +151,7 @@ FilmNameLocationDialog::check_path () )) { return false; } - } else if (filesystem::is_regular_file(path())) { + } else if (boost::filesystem::is_regular_file(path())) { error_dialog ( this, String::compose (wx_to_std(_("%1 already exists as a file, so you cannot use it for a film.")), path().c_str()) diff --git a/src/wx/full_config_dialog.cc b/src/wx/full_config_dialog.cc index c1c36c4a4..e3ea91224 100644 --- a/src/wx/full_config_dialog.cc +++ b/src/wx/full_config_dialog.cc @@ -45,6 +45,9 @@ #include "send_test_email_dialog.h" #include "server_dialog.h" #include "static_text.h" +#ifdef DCPOMATIC_GROK +#include "grok/gpu_config_panel.h" +#endif #include "wx_util.h" #include "lib/config.h" #include "lib/cross.h" @@ -1946,6 +1949,9 @@ create_full_config_dialog () e->AddPage (new SoundPage (ps, border)); e->AddPage (new DefaultsPage (ps, border)); e->AddPage (new EncodingServersPage(ps, border)); +#ifdef DCPOMATIC_GROK + e->AddPage (new GPUPage (ps, border)); +#endif e->AddPage (new KeysPage (ps, border)); e->AddPage (new TMSPage (ps, border)); e->AddPage (new EmailPage (ps, border)); diff --git a/src/wx/grok/gpu_config_panel.h b/src/wx/grok/gpu_config_panel.h new file mode 100644 index 000000000..cbf037592 --- /dev/null +++ b/src/wx/grok/gpu_config_panel.h @@ -0,0 +1,227 @@ +/* + 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 + +static std::vector<std::string> get_gpu_names(boost::filesystem::path binary, boost::filesystem::path filename) +{ + // Execute the GPU listing program and redirect its output to a file + if (std::system((binary.string() + " > " + filename.string()).c_str()) < 0) { + return {}; + } + + std::vector<std::string> gpu_names; + std::ifstream file(filename.c_str()); + if (file.is_open()) + { + std::string line; + while (std::getline(file, line)) + gpu_names.push_back(line); + file.close(); + } + + return gpu_names; +} + + +class GpuList : public wxPanel +{ +public: + GpuList(wxPanel* parent) + : wxPanel(parent, wxID_ANY) + { + _combo_box = new wxComboBox(this, wxID_ANY, "", wxDefaultPosition, wxSize(400, -1)); + _combo_box->Bind(wxEVT_COMBOBOX, &GpuList::OnComboBox, this); + update(); + + auto sizer = new wxBoxSizer(wxHORIZONTAL); + sizer->Add(_combo_box, 0, wxALIGN_CENTER_VERTICAL); + SetSizerAndFit(sizer); + } + + void update() + { + auto grok = Config::instance()->grok().get_value_or({}); + auto lister_binary = grok.binary_location / "gpu_lister"; + auto lister_file = grok.binary_location / "gpus.txt"; + if (boost::filesystem::exists(lister_binary)) { + auto gpu_names = get_gpu_names(lister_binary, lister_file); + + _combo_box->Clear(); + for (auto const& name: gpu_names) { + _combo_box->Append(name); + } + } + } + + void set_selection(int sel) + { + if (sel < static_cast<int>(_combo_box->GetCount())) { + _combo_box->SetSelection(sel); + } + } + +private: + void OnComboBox(wxCommandEvent&) + { + auto selection = _combo_box->GetSelection(); + if (selection != wxNOT_FOUND) { + auto grok = Config::instance()->grok().get_value_or({}); + grok.selected = selection; + Config::instance()->set_grok(grok); + } + } + + wxComboBox* _combo_box; + int _selection = 0; +}; + + +class GPUPage : public Page +{ +public: + GPUPage(wxSize panel_size, int border) + : Page(panel_size, border) + {} + + wxString GetName() const override + { + return _("GPU"); + } + +#ifdef DCPOMATIC_OSX + /* XXX: this icon does not exist */ + wxBitmap GetLargeIcon() const override + { + return wxBitmap(icon_path("gpu"), wxBITMAP_TYPE_PNG); + } +#endif + +private: + void setup() override + { + _enable_gpu = new CheckBox(_panel, _("Enable GPU acceleration")); + _panel->GetSizer()->Add(_enable_gpu, 0, wxALL | wxEXPAND, _border); + + wxFlexGridSizer* table = new wxFlexGridSizer(2, DCPOMATIC_SIZER_X_GAP, DCPOMATIC_SIZER_Y_GAP); + table->AddGrowableCol(1, 1); + _panel->GetSizer()->Add(table, 1, wxALL | wxEXPAND, _border); + + add_label_to_sizer(table, _panel, _("Acceleration binary folder"), true, 0, wxLEFT | wxLEFT | wxALIGN_CENTRE_VERTICAL); + _binary_location = new wxDirPickerCtrl(_panel, wxDD_DIR_MUST_EXIST); + table->Add(_binary_location, 1, wxEXPAND); + + add_label_to_sizer(table, _panel, _("GPU selection"), true, 0, wxLEFT | wxRIGHT | wxALIGN_CENTRE_VERTICAL); + _gpu_list_control = new GpuList(_panel); + table->Add(_gpu_list_control, 1, wxEXPAND); + + add_label_to_sizer(table, _panel, _("License server"), true, 0, wxLEFT | wxRIGHT | wxALIGN_CENTRE_VERTICAL); + _server = new wxTextCtrl(_panel, wxID_ANY); + table->Add(_server, 1, wxEXPAND | wxALL); + + add_label_to_sizer(table, _panel, _("Port"), false, 0, wxLEFT | wxRIGHT | wxALIGN_CENTRE_VERTICAL); + _port = new wxSpinCtrl(_panel, wxID_ANY); + _port->SetRange(0, 65535); + table->Add(_port); + + add_label_to_sizer(table, _panel, _("License"), true, 0, wxLEFT | wxRIGHT | wxALIGN_CENTRE_VERTICAL); + _licence = new PasswordEntry(_panel); + table->Add(_licence->get_panel(), 1, wxEXPAND | wxALL); + + _enable_gpu->bind(&GPUPage::enable_gpu_changed, this); + _binary_location->Bind(wxEVT_DIRPICKER_CHANGED, boost::bind (&GPUPage::binary_location_changed, this)); + _server->Bind(wxEVT_TEXT, boost::bind(&GPUPage::server_changed, this)); + _port->Bind(wxEVT_SPINCTRL, boost::bind(&GPUPage::port_changed, this)); + _licence->Changed.connect(boost::bind(&GPUPage::licence_changed, this)); + + setup_sensitivity(); + } + + void setup_sensitivity() + { + auto grok = Config::instance()->grok().get_value_or({}); + + _binary_location->Enable(grok.enable); + _gpu_list_control->Enable(grok.enable); + _server->Enable(grok.enable); + _port->Enable(grok.enable); + _licence->get_panel()->Enable(grok.enable); + } + + void config_changed() override + { + auto grok = Config::instance()->grok().get_value_or({}); + + checked_set(_enable_gpu, grok.enable); + _binary_location->SetPath(std_to_wx(grok.binary_location.string())); + _gpu_list_control->update(); + _gpu_list_control->set_selection(grok.selected); + checked_set(_server, grok.licence_server); + checked_set(_port, grok.licence_port); + checked_set(_licence, grok.licence); + } + + void enable_gpu_changed() + { + auto grok = Config::instance()->grok().get_value_or({}); + grok.enable = _enable_gpu->GetValue(); + Config::instance()->set_grok(grok); + + setup_sensitivity(); + } + + void binary_location_changed() + { + auto grok = Config::instance()->grok().get_value_or({}); + grok.binary_location = wx_to_std(_binary_location->GetPath()); + Config::instance()->set_grok(grok); + + _gpu_list_control->update(); + } + + void server_changed() + { + auto grok = Config::instance()->grok().get_value_or({}); + grok.licence_server = wx_to_std(_server->GetValue()); + Config::instance()->set_grok(grok); + } + + void port_changed() + { + auto grok = Config::instance()->grok().get_value_or({}); + grok.licence_port = _port->GetValue(); + Config::instance()->set_grok(grok); + } + + void licence_changed() + { + auto grok = Config::instance()->grok().get_value_or({}); + grok.licence = wx_to_std(_licence->get()); + Config::instance()->set_grok(grok); + } + + CheckBox* _enable_gpu = nullptr; + wxDirPickerCtrl* _binary_location = nullptr; + GpuList* _gpu_list_control = nullptr; + wxTextCtrl* _server = nullptr; + wxSpinCtrl* _port = nullptr; + PasswordEntry* _licence = nullptr; +}; diff --git a/src/wx/kdm_cpl_panel.cc b/src/wx/kdm_cpl_panel.cc index 4699582aa..4e1eb8f34 100644 --- a/src/wx/kdm_cpl_panel.cc +++ b/src/wx/kdm_cpl_panel.cc @@ -41,11 +41,11 @@ KDMCPLPanel::KDMCPLPanel (wxWindow* parent, vector<CPLSummary> cpls) /* CPL choice */ auto s = new wxBoxSizer (wxHORIZONTAL); - add_label_to_sizer (s, this, _("CPL"), true, 0, wxLEFT | wxRIGHT | wxALIGN_CENTER_VERTICAL); + add_label_to_sizer (s, this, _("CPL"), true, 0, wxLEFT | wxRIGHT | wxALIGN_CENTRE_VERTICAL); _cpl = new wxChoice (this, wxID_ANY); - s->Add (_cpl, 1, wxEXPAND); + s->Add (_cpl, 1, wxTOP | wxEXPAND, DCPOMATIC_CHOICE_TOP_PAD); _cpl_browse = new Button (this, _("Browse...")); - s->Add (_cpl_browse, 0, wxLEFT | wxALIGN_CENTER_VERTICAL, DCPOMATIC_SIZER_X_GAP); + s->Add (_cpl_browse, 0, wxLEFT, DCPOMATIC_SIZER_X_GAP); vertical->Add (s, 0, wxEXPAND | wxTOP, DCPOMATIC_SIZER_GAP + 2); /* CPL details */ diff --git a/src/wx/kdm_timing_panel.cc b/src/wx/kdm_timing_panel.cc index 0fd00de93..f4112cb08 100644 --- a/src/wx/kdm_timing_panel.cc +++ b/src/wx/kdm_timing_panel.cc @@ -19,11 +19,13 @@ */ +#include "dcpomatic_choice.h" #include "kdm_timing_panel.h" #include "static_text.h" #include "time_picker.h" #include "wx_util.h" #include "lib/config.h" +#include <dcp/utc_offset.h> #include <dcp/warnings.h> LIBDCP_DISABLE_WARNINGS #include <wx/datectrl.h> @@ -105,6 +107,10 @@ KDMTimingPanel::KDMTimingPanel (wxWindow* parent) table->Add (_until_time, 0, wxALIGN_CENTER_VERTICAL); + add_label_to_sizer(table, this, _("UTC offset (time zone)"), true, 1, wxALIGN_CENTRE_VERTICAL); + _utc_offset = new Choice(this); + table->Add(_utc_offset, 0, wxALIGN_CENTRE_VERTICAL | wxLEFT, DCPOMATIC_SIZER_X_GAP); + overall_sizer->Add (table, 0, wxTOP, DCPOMATIC_SIZER_GAP); _warning = new StaticText (this, wxT("")); @@ -115,6 +121,17 @@ KDMTimingPanel::KDMTimingPanel (wxWindow* parent) _warning->SetForegroundColour (wxColour (255, 0, 0)); _warning->SetFont(font); + /* Default to UTC */ + size_t sel = get_offsets(_offsets); + for (size_t i = 0; i < _offsets.size(); ++i) { + _utc_offset->add(_offsets[i].name); + if (_offsets[i].hour == 0 && _offsets[i].minute == 0) { + sel = i; + } + } + + _utc_offset->set(sel); + /* I said I've been to the year 3000. Not much has changed but they live underwater. And your In-in-in-interop DCP is pretty fine. */ @@ -125,33 +142,38 @@ KDMTimingPanel::KDMTimingPanel (wxWindow* parent) _until_date->Bind (wxEVT_DATE_CHANGED, bind (&KDMTimingPanel::changed, this)); _from_time->Changed.connect (bind (&KDMTimingPanel::changed, this)); _until_time->Changed.connect (bind (&KDMTimingPanel::changed, this)); + _utc_offset->bind(&KDMTimingPanel::changed, this); SetSizer (overall_sizer); } -boost::posix_time::ptime +dcp::LocalTime KDMTimingPanel::from () const { - return posix_time (_from_date, _from_time); + return local_time(_from_date, _from_time, utc_offset()); } -boost::posix_time::ptime -KDMTimingPanel::posix_time (wxDatePickerCtrl* date_picker, TimePicker* time_picker) +dcp::LocalTime +KDMTimingPanel::local_time(wxDatePickerCtrl* date_picker, TimePicker* time_picker, dcp::UTCOffset offset) { auto const date = date_picker->GetValue (); - return boost::posix_time::ptime ( - boost::gregorian::date (date.GetYear(), date.GetMonth() + 1, date.GetDay()), - boost::posix_time::time_duration (time_picker->hours(), time_picker->minutes(), 0) + return dcp::LocalTime( + date.GetYear(), + date.GetMonth() + 1, + date.GetDay(), + time_picker->hours(), + time_picker->minutes(), + offset ); } -boost::posix_time::ptime +dcp::LocalTime KDMTimingPanel::until () const { - return posix_time (_until_date, _until_time); + return local_time(_until_date, _until_time, utc_offset()); } @@ -173,3 +195,20 @@ KDMTimingPanel::changed () const TimingChanged (); } + + +dcp::UTCOffset +KDMTimingPanel::utc_offset() const +{ + auto const sel = _utc_offset->get(); + if (!sel || *sel >= int(_offsets.size())) { + return {}; + } + + auto const& offset = _offsets[*sel]; + int const minute_scale = offset.hour < 0 ? -1 : 1; + + return { offset.hour, minute_scale * offset.minute }; +} + + diff --git a/src/wx/kdm_timing_panel.h b/src/wx/kdm_timing_panel.h index 7221ba722..f847992a7 100644 --- a/src/wx/kdm_timing_panel.h +++ b/src/wx/kdm_timing_panel.h @@ -18,6 +18,9 @@ */ + +#include "wx_util.h" +#include <dcp/utc_offset.h> #include <dcp/warnings.h> LIBDCP_DISABLE_WARNINGS #include <wx/wx.h> @@ -25,18 +28,19 @@ LIBDCP_ENABLE_WARNINGS #include <boost/date_time/posix_time/posix_time.hpp> #include <boost/signals2.hpp> -class wxDatePickerCtrl; + +class Choice; class TimePicker; +class wxDatePickerCtrl; + class KDMTimingPanel : public wxPanel { public: explicit KDMTimingPanel (wxWindow* parent); - /** @return KDM from time in local time */ - boost::posix_time::ptime from () const; - /** @return KDM until time in local time */ - boost::posix_time::ptime until () const; + dcp::LocalTime from() const; + dcp::LocalTime until() const; bool valid () const; @@ -44,11 +48,15 @@ public: private: void changed () const; - static boost::posix_time::ptime posix_time (wxDatePickerCtrl *, TimePicker *); + dcp::UTCOffset utc_offset() const; + + static dcp::LocalTime local_time(wxDatePickerCtrl *, TimePicker *, dcp::UTCOffset offset); wxDatePickerCtrl* _from_date; wxDatePickerCtrl* _until_date; TimePicker* _from_time; TimePicker* _until_time; + Choice* _utc_offset; wxStaticText* _warning; + std::vector<Offset> _offsets; }; diff --git a/src/wx/recipient_dialog.cc b/src/wx/recipient_dialog.cc index a69adb169..992c88362 100644 --- a/src/wx/recipient_dialog.cc +++ b/src/wx/recipient_dialog.cc @@ -55,7 +55,7 @@ column (string s) RecipientDialog::RecipientDialog ( - wxWindow* parent, wxString title, string name, string notes, vector<string> emails, int utc_offset_hour, int utc_offset_minute, optional<dcp::Certificate> recipient + wxWindow* parent, wxString title, string name, string notes, vector<string> emails, optional<dcp::Certificate> recipient ) : wxDialog (parent, wxID_ANY, title) , _recipient (recipient) @@ -76,11 +76,6 @@ RecipientDialog::RecipientDialog ( _sizer->Add (_notes, wxGBPosition (r, 1)); ++r; - add_label_to_sizer (_sizer, this, _("UTC offset (time zone)"), true, wxGBPosition (r, 0)); - _utc_offset = new wxChoice (this, wxID_ANY); - _sizer->Add (_utc_offset, wxGBPosition (r, 1)); - ++r; - add_label_to_sizer (_sizer, this, _("Email addresses for KDM delivery"), false, wxGBPosition (r, 0), wxGBSpan (1, 2)); ++r; @@ -125,17 +120,6 @@ RecipientDialog::RecipientDialog ( overall_sizer->Add (buttons, wxSizerFlags().Expand().DoubleBorder()); } - /* Default to UTC */ - size_t sel = get_offsets (_offsets); - for (size_t i = 0; i < _offsets.size(); ++i) { - _utc_offset->Append (_offsets[i].name); - if (_offsets[i].hour == utc_offset_hour && _offsets[i].minute == utc_offset_minute) { - sel = i; - } - } - - _utc_offset->SetSelection (sel); - overall_sizer->Layout (); overall_sizer->SetSizeHints (this); @@ -234,28 +218,3 @@ RecipientDialog::emails () const { return _emails; } - - -int -RecipientDialog::utc_offset_hour () const -{ - int const sel = _utc_offset->GetSelection(); - if (sel < 0 || sel > int (_offsets.size())) { - return 0; - } - - return _offsets[sel].hour; -} - -int -RecipientDialog::utc_offset_minute () const -{ - int const sel = _utc_offset->GetSelection(); - if (sel < 0 || sel > int (_offsets.size())) { - return 0; - } - - return _offsets[sel].minute; -} - - diff --git a/src/wx/recipient_dialog.h b/src/wx/recipient_dialog.h index a46e67af5..61f6b3031 100644 --- a/src/wx/recipient_dialog.h +++ b/src/wx/recipient_dialog.h @@ -44,8 +44,6 @@ public: std::string name = "", std::string notes = "", std::vector<std::string> emails = std::vector<std::string>(), - int utc_offset_hour = 0, - int utc_offset_minute = 0, boost::optional<dcp::Certificate> c = boost::optional<dcp::Certificate>() ); @@ -53,8 +51,6 @@ public: std::string notes () const; boost::optional<dcp::Certificate> recipient () const; std::vector<std::string> emails () const; - int utc_offset_hour () const; - int utc_offset_minute () const; private: void get_recipient_from_file (); @@ -71,8 +67,6 @@ private: wxButton* _get_recipient_from_file; EditableList<std::string, EmailDialog>* _email_list; std::vector<std::string> _emails; - wxChoice* _utc_offset; - std::vector<Offset> _offsets; boost::optional<dcp::Certificate> _recipient; }; diff --git a/src/wx/recipients_panel.cc b/src/wx/recipients_panel.cc index 485a0f94e..04ad0dd6e 100644 --- a/src/wx/recipients_panel.cc +++ b/src/wx/recipients_panel.cc @@ -122,7 +122,7 @@ RecipientsPanel::add_recipient_clicked () { RecipientDialog dialog(GetParent(), _("Add recipient")); if (dialog.ShowModal() == wxID_OK) { - auto r = std::make_shared<DKDMRecipient>(dialog.name(), dialog.notes(), dialog.recipient(), dialog.emails(), dialog.utc_offset_hour(), dialog.utc_offset_minute()); + auto r = std::make_shared<DKDMRecipient>(dialog.name(), dialog.notes(), dialog.recipient(), dialog.emails()); Config::instance()->add_dkdm_recipient (r); add_recipient (r); } @@ -139,15 +139,13 @@ RecipientsPanel::edit_recipient_clicked () auto c = *_selected.begin(); RecipientDialog dialog( - GetParent(), _("Edit recipient"), c.second->name, c.second->notes, c.second->emails, c.second->utc_offset_hour, c.second->utc_offset_minute, c.second->recipient + GetParent(), _("Edit recipient"), c.second->name, c.second->notes, c.second->emails, c.second->recipient ); if (dialog.ShowModal() == wxID_OK) { c.second->name = dialog.name(); c.second->emails = dialog.emails(); c.second->notes = dialog.notes(); - c.second->utc_offset_hour = dialog.utc_offset_hour(); - c.second->utc_offset_minute = dialog.utc_offset_minute(); _targets->SetItemText(c.first, std_to_wx(dialog.name())); Config::instance()->changed (Config::DKDM_RECIPIENTS); } diff --git a/src/wx/screens_panel.cc b/src/wx/screens_panel.cc index d3b1db77d..62f8688a9 100644 --- a/src/wx/screens_panel.cc +++ b/src/wx/screens_panel.cc @@ -252,7 +252,7 @@ ScreensPanel::add_cinema_clicked () CinemaDialog dialog(GetParent(), _("Add Cinema")); if (dialog.ShowModal() == wxID_OK) { - auto cinema = make_shared<Cinema>(dialog.name(), dialog.emails(), dialog.notes(), dialog.utc_offset_hour(), dialog.utc_offset_minute()); + auto cinema = make_shared<Cinema>(dialog.name(), dialog.emails(), dialog.notes()); auto cinemas = sorted_cinemas(); @@ -320,16 +320,12 @@ ScreensPanel::edit_cinema_clicked () void ScreensPanel::edit_cinema(shared_ptr<Cinema> cinema) { - CinemaDialog dialog( - GetParent(), _("Edit cinema"), cinema->name, cinema->emails, cinema->notes, cinema->utc_offset_hour(), cinema->utc_offset_minute() - ); + CinemaDialog dialog(GetParent(), _("Edit cinema"), cinema->name, cinema->emails, cinema->notes); if (dialog.ShowModal() == wxID_OK) { cinema->name = dialog.name(); cinema->emails = dialog.emails(); cinema->notes = dialog.notes(); - cinema->set_utc_offset_hour(dialog.utc_offset_hour()); - cinema->set_utc_offset_minute(dialog.utc_offset_minute()); notify_cinemas_changed(); auto item = cinema_to_item(cinema); DCPOMATIC_ASSERT(item); diff --git a/src/wx/text_panel.cc b/src/wx/text_panel.cc index 78c024565..c3a5706b0 100644 --- a/src/wx/text_panel.cc +++ b/src/wx/text_panel.cc @@ -84,14 +84,6 @@ TextPanel::create () refer = _("Use this DCP's closed caption as OV and make VF"); } - _reference = new CheckBox (this, refer); - _reference_note = new StaticText (this, wxT("")); - _reference_note->Wrap (200); - auto font = _reference_note->GetFont(); - font.SetStyle(wxFONTSTYLE_ITALIC); - font.SetPointSize(font.GetPointSize() - 1); - _reference_note->SetFont(font); - _use = new CheckBox (this, _("Use as")); _type = new wxChoice (this, wxID_ANY); _type->Append (_("open subtitles")); @@ -132,7 +124,6 @@ TextPanel::create () _y_scale->SetRange (0, 1000); _line_spacing->SetRange (0, 1000); - _reference->bind(&TextPanel::reference_clicked, this); _use->bind(&TextPanel::use_toggled, this); _type->Bind (wxEVT_CHOICE, boost::bind (&TextPanel::type_changed, this)); _burn->bind(&TextPanel::burn_toggled, this); @@ -232,12 +223,6 @@ TextPanel::add_to_grid () { int r = 0; - auto reference_sizer = new wxBoxSizer (wxVERTICAL); - reference_sizer->Add (_reference, 0); - reference_sizer->Add (_reference_note, 0); - _grid->Add (reference_sizer, wxGBPosition(r, 0), wxGBSpan(1, 4)); - ++r; - auto use = new wxBoxSizer (wxHORIZONTAL); use->Add (_use, 0, wxEXPAND | wxRIGHT, DCPOMATIC_SIZER_GAP); use->Add (_type, 1, wxEXPAND, 0); @@ -496,15 +481,6 @@ TextPanel::film_content_changed (int property) if (_language_type) { _language_type->SetSelection (text ? (text->language_is_additional() ? 1 : 0) : 0); } - } else if (property == DCPContentProperty::REFERENCE_TEXT) { - if (scs) { - auto dcp = dynamic_pointer_cast<DCPContent> (scs); - checked_set (_reference, dcp ? dcp->reference_text(_original_type) : false); - } else { - checked_set (_reference, false); - } - - setup_sensitivity (); } else if (property == DCPContentProperty::TEXTS) { setup_sensitivity (); } else if (property == ContentProperty::TRIM_START) { @@ -593,17 +569,7 @@ TextPanel::setup_sensitivity () dcp = dynamic_pointer_cast<DCPContent>(sel.front()); } - string why_not; - bool const can_reference = dcp && dcp->can_reference_text (_parent->film(), _original_type, why_not); - wxString cannot; - if (why_not.empty()) { - cannot = _("Cannot reference this DCP's subtitles or captions."); - } else { - cannot = _("Cannot reference this DCP's subtitles or captions: ") + std_to_wx(why_not); - } - setup_refer_button (_reference, _reference_note, dcp, can_reference, cannot); - - bool const reference = _reference->GetValue (); + auto const reference = dcp && dcp->reference_text(_original_type); auto const type = current_type (); @@ -763,23 +729,6 @@ TextPanel::fonts_dialog_clicked () void -TextPanel::reference_clicked () -{ - auto c = _parent->selected (); - if (c.size() != 1) { - return; - } - - auto d = dynamic_pointer_cast<DCPContent> (c.front ()); - if (!d) { - return; - } - - d->set_reference_text (_original_type, _reference->GetValue ()); -} - - -void TextPanel::appearance_dialog_clicked () { auto c = _parent->selected_text (); diff --git a/src/wx/text_panel.h b/src/wx/text_panel.h index 5adad5a3e..a2afba439 100644 --- a/src/wx/text_panel.h +++ b/src/wx/text_panel.h @@ -56,7 +56,6 @@ private: void stream_changed (); void text_view_clicked (); void fonts_dialog_clicked (); - void reference_clicked (); void appearance_dialog_clicked (); void outline_subtitles_changed (); TextType current_type () const; @@ -74,8 +73,6 @@ private: void update_outline_subtitles_in_viewer (); void clear_outline_subtitles (); - CheckBox* _reference; - wxStaticText* _reference_note; CheckBox* _outline_subtitles = nullptr; CheckBox* _use; wxChoice* _type; diff --git a/src/wx/timecode.cc b/src/wx/timecode.cc index 1e6a1930d..64fe87190 100644 --- a/src/wx/timecode.cc +++ b/src/wx/timecode.cc @@ -114,7 +114,7 @@ void TimecodeBase::changed () { if (_set_button && !_ignore_changed) { - _set_button->Enable (true); + _set_button->Enable(valid()); } } diff --git a/src/wx/timecode.h b/src/wx/timecode.h index 6c5d8ae23..22899ddc9 100644 --- a/src/wx/timecode.h +++ b/src/wx/timecode.h @@ -50,6 +50,7 @@ public: protected: void changed (); void set_clicked (); + virtual bool valid() const = 0; wxSizer* _sizer; wxPanel* _editable; @@ -96,6 +97,11 @@ public: _frames->SetHint (std_to_wx(dcp::raw_convert<std::string>(hmsf.f))); } + void set_maximum(dcpomatic::HMSF maximum) + { + _maximum = std::move(maximum); + } + dcpomatic::HMSF get () const { auto value_or_hint = [](wxTextCtrl const * t) { @@ -116,6 +122,13 @@ public: { return T(get(), fps); } + +private: + bool valid() const override { + return !_maximum || get() <= *_maximum; + } + + boost::optional<dcpomatic::HMSF> _maximum; }; #endif diff --git a/src/wx/timeline.cc b/src/wx/timeline.cc index 38e9de4ee..329f4ef00 100644 --- a/src/wx/timeline.cc +++ b/src/wx/timeline.cc @@ -1,5 +1,5 @@ /* - Copyright (C) 2013-2021 Carl Hetherington <cth@carlh.net> + Copyright (C) 2023 Carl Hetherington <cth@carlh.net> This file is part of DCP-o-matic. @@ -18,1012 +18,25 @@ */ -#include "content_panel.h" -#include "film_editor.h" -#include "film_viewer.h" -#include "timeline.h" -#include "timeline_atmos_content_view.h" -#include "timeline_audio_content_view.h" -#include "timeline_labels_view.h" -#include "timeline_reels_view.h" -#include "timeline_text_content_view.h" -#include "timeline_time_axis_view.h" -#include "timeline_video_content_view.h" -#include "wx_util.h" -#include "lib/atmos_mxf_content.h" -#include "lib/audio_content.h" -#include "lib/film.h" -#include "lib/image_content.h" -#include "lib/playlist.h" -#include "lib/text_content.h" -#include "lib/timer.h" -#include "lib/video_content.h" -#include <dcp/scope_guard.h> -#include <dcp/warnings.h> -LIBDCP_DISABLE_WARNINGS -#include <wx/graphics.h> -LIBDCP_ENABLE_WARNINGS -#include <iterator> -#include <list> - -using std::abs; -using std::dynamic_pointer_cast; -using std::list; -using std::make_shared; -using std::max; -using std::min; -using std::shared_ptr; -using std::weak_ptr; -using boost::bind; -using boost::optional; -using namespace dcpomatic; -#if BOOST_VERSION >= 106100 -using namespace boost::placeholders; -#endif +#include "timeline.h" /* 3 hours in 640 pixels */ -double const Timeline::_minimum_pixels_per_second = 640.0 / (60 * 60 * 3); -int const Timeline::_minimum_pixels_per_track = 16; - - -Timeline::Timeline(wxWindow* parent, ContentPanel* cp, shared_ptr<Film> film, FilmViewer& viewer) - : wxPanel (parent, wxID_ANY) - , _labels_canvas (new wxScrolledCanvas (this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxFULL_REPAINT_ON_RESIZE)) - , _main_canvas (new wxScrolledCanvas (this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxFULL_REPAINT_ON_RESIZE)) - , _content_panel (cp) - , _film (film) - , _viewer (viewer) - , _time_axis_view (new TimelineTimeAxisView (*this, 64)) - , _reels_view (new TimelineReelsView (*this, 32)) - , _labels_view (new TimelineLabelsView (*this)) - , _tracks (0) - , _left_down (false) - , _down_view_position (0) - , _first_move (false) - , _menu (this, viewer) - , _snap (true) - , _tool (SELECT) - , _x_scroll_rate (16) - , _y_scroll_rate (16) - , _pixels_per_track (48) - , _first_resize (true) - , _timer (this) -{ -#ifndef __WXOSX__ - _labels_canvas->SetDoubleBuffered (true); - _main_canvas->SetDoubleBuffered (true); -#endif - - auto sizer = new wxBoxSizer (wxHORIZONTAL); - sizer->Add (_labels_canvas, 0, wxEXPAND); - _labels_canvas->SetMinSize (wxSize (_labels_view->bbox().width, -1)); - sizer->Add (_main_canvas, 1, wxEXPAND); - SetSizer (sizer); - - _labels_canvas->Bind (wxEVT_PAINT, boost::bind (&Timeline::paint_labels, this)); - _main_canvas->Bind (wxEVT_PAINT, boost::bind (&Timeline::paint_main, this)); - _main_canvas->Bind (wxEVT_LEFT_DOWN, boost::bind (&Timeline::left_down, this, _1)); - _main_canvas->Bind (wxEVT_LEFT_UP, boost::bind (&Timeline::left_up, this, _1)); - _main_canvas->Bind (wxEVT_RIGHT_DOWN, boost::bind (&Timeline::right_down, this, _1)); - _main_canvas->Bind (wxEVT_MOTION, boost::bind (&Timeline::mouse_moved, this, _1)); - _main_canvas->Bind (wxEVT_SIZE, boost::bind (&Timeline::resized, this)); - _main_canvas->Bind (wxEVT_MOUSEWHEEL, boost::bind(&Timeline::mouse_wheel_turned, this, _1)); - _main_canvas->Bind (wxEVT_SCROLLWIN_TOP, boost::bind (&Timeline::scrolled, this, _1)); - _main_canvas->Bind (wxEVT_SCROLLWIN_BOTTOM, boost::bind (&Timeline::scrolled, this, _1)); - _main_canvas->Bind (wxEVT_SCROLLWIN_LINEUP, boost::bind (&Timeline::scrolled, this, _1)); - _main_canvas->Bind (wxEVT_SCROLLWIN_LINEDOWN, boost::bind (&Timeline::scrolled, this, _1)); - _main_canvas->Bind (wxEVT_SCROLLWIN_PAGEUP, boost::bind (&Timeline::scrolled, this, _1)); - _main_canvas->Bind (wxEVT_SCROLLWIN_PAGEDOWN, boost::bind (&Timeline::scrolled, this, _1)); - _main_canvas->Bind (wxEVT_SCROLLWIN_THUMBTRACK, boost::bind (&Timeline::scrolled, this, _1)); - - film_change(ChangeType::DONE, FilmProperty::CONTENT); - - SetMinSize (wxSize (640, 4 * pixels_per_track() + 96)); - - _film_changed_connection = film->Change.connect (bind (&Timeline::film_change, this, _1, _2)); - _film_content_change_connection = film->ContentChange.connect (bind (&Timeline::film_content_change, this, _1, _3, _4)); - - Bind (wxEVT_TIMER, boost::bind(&Timeline::update_playhead, this)); - _timer.Start (200, wxTIMER_CONTINUOUS); - - setup_scrollbars (); - _labels_canvas->ShowScrollbars (wxSHOW_SB_NEVER, wxSHOW_SB_NEVER); -} - - -void -Timeline::mouse_wheel_turned(wxMouseEvent& event) -{ - auto const rotation = event.GetWheelRotation(); - - if (event.ControlDown()) { - /* On my mouse one click of the scroll wheel is 120, and it's -ve when - * scrolling the wheel towards me. - */ - auto const scale = rotation > 0 ? - (1.0 / (rotation / 90.0)) : - (-rotation / 90.0); - - int before_start_x; - int before_start_y; - _main_canvas->GetViewStart(&before_start_x, &before_start_y); - - auto const before_pps = _pixels_per_second.get_value_or(1); - auto const before_pos = _last_mouse_wheel_x && *_last_mouse_wheel_x == event.GetX() ? - *_last_mouse_wheel_time : - (before_start_x * _x_scroll_rate + event.GetX()) / before_pps; - - set_pixels_per_second(before_pps * scale); - setup_scrollbars(); - - auto after_left = std::max(0.0, before_pos * _pixels_per_second.get_value_or(1) - event.GetX()); - _main_canvas->Scroll(after_left / _x_scroll_rate, before_start_y); - _labels_canvas->Scroll(0, before_start_y); - Refresh(); - - if (!_last_mouse_wheel_x || *_last_mouse_wheel_x != event.GetX()) { - _last_mouse_wheel_x = event.GetX(); - _last_mouse_wheel_time = before_pos; - } - } else if (event.ShiftDown()) { - int before_start_x; - int before_start_y; - _main_canvas->GetViewStart(&before_start_x, &before_start_y); - auto const width = _main_canvas->GetSize().GetWidth(); - _main_canvas->Scroll(std::max(0.0, before_start_x - rotation * 100.0 / width), before_start_y); - } -} - - -void -Timeline::update_playhead () -{ - Refresh (); -} - - -void -Timeline::set_pixels_per_second (double pps) -{ - _pixels_per_second = max (_minimum_pixels_per_second, pps); -} - - -void -Timeline::paint_labels () -{ - wxPaintDC dc (_labels_canvas); - - auto film = _film.lock(); - if (film->content().empty()) { - return; - } - - auto gc = wxGraphicsContext::Create (dc); - if (!gc) { - return; - } - - dcp::ScopeGuard sg = [gc]() { delete gc; }; - - int vsx, vsy; - _labels_canvas->GetViewStart (&vsx, &vsy); - gc->Translate (-vsx * _x_scroll_rate, -vsy * _y_scroll_rate + tracks_y_offset()); - - _labels_view->paint (gc, {}); -} - - -void -Timeline::paint_main () -{ - wxPaintDC dc (_main_canvas); - dc.Clear(); - - auto film = _film.lock(); - if (film->content().empty()) { - return; - } - - _main_canvas->DoPrepareDC (dc); - - auto gc = wxGraphicsContext::Create (dc); - if (!gc) { - return; - } - - dcp::ScopeGuard sg = [gc]() { delete gc; }; - - gc->SetAntialiasMode (wxANTIALIAS_DEFAULT); - - for (auto i: _views) { - - auto ic = dynamic_pointer_cast<TimelineContentView> (i); - - /* Find areas of overlap with other content views, so that we can plot them */ - list<dcpomatic::Rect<int>> overlaps; - for (auto j: _views) { - auto jc = dynamic_pointer_cast<TimelineContentView> (j); - /* No overlap with non-content views, views on different tracks, audio views or non-active views */ - if (!ic || !jc || i == j || ic->track() != jc->track() || ic->track().get_value_or(2) >= 2 || !ic->active() || !jc->active()) { - continue; - } - - auto r = j->bbox().intersection(i->bbox()); - if (r) { - overlaps.push_back (r.get ()); - } - } - - i->paint (gc, overlaps); - } - - if (_zoom_point) { - gc->SetPen(gui_is_dark() ? *wxWHITE_PEN : *wxBLACK_PEN); - gc->SetBrush (*wxTRANSPARENT_BRUSH); - gc->DrawRectangle ( - min (_down_point.x, _zoom_point->x), - min (_down_point.y, _zoom_point->y), - abs (_down_point.x - _zoom_point->x), - abs (_down_point.y - _zoom_point->y) - ); - } - - /* Playhead */ - - gc->SetPen (*wxRED_PEN); - auto path = gc->CreatePath (); - double const ph = _viewer.position().seconds() * pixels_per_second().get_value_or(0); - path.MoveToPoint (ph, 0); - path.AddLineToPoint (ph, pixels_per_track() * _tracks + 32); - gc->StrokePath (path); -} - - -void -Timeline::film_change(ChangeType type, FilmProperty p) -{ - if (type != ChangeType::DONE) { - return; - } - - if (p == FilmProperty::CONTENT || p == FilmProperty::REEL_TYPE || p == FilmProperty::REEL_LENGTH) { - ensure_ui_thread (); - recreate_views (); - } else if (p == FilmProperty::CONTENT_ORDER) { - Refresh (); - } -} - - -void -Timeline::recreate_views () -{ - auto film = _film.lock (); - if (!film) { - return; - } - - _views.clear (); - _views.push_back (_time_axis_view); - _views.push_back (_reels_view); - - for (auto i: film->content ()) { - if (i->video) { - _views.push_back (make_shared<TimelineVideoContentView>(*this, i)); - } - - if (i->audio && !i->audio->mapping().mapped_output_channels().empty ()) { - _views.push_back (make_shared<TimelineAudioContentView>(*this, i)); - } - - for (auto j: i->text) { - _views.push_back (make_shared<TimelineTextContentView>(*this, i, j)); - } - - if (i->atmos) { - _views.push_back (make_shared<TimelineAtmosContentView>(*this, i)); - } - } - - assign_tracks (); - setup_scrollbars (); - Refresh (); -} - - -void -Timeline::film_content_change (ChangeType type, int property, bool frequent) -{ - if (type != ChangeType::DONE) { - return; - } - - ensure_ui_thread (); - - if (property == AudioContentProperty::STREAMS || property == VideoContentProperty::FRAME_TYPE) { - recreate_views (); - } else if (property == ContentProperty::POSITION || property == ContentProperty::LENGTH) { - _reels_view->force_redraw (); - } else if (!frequent) { - setup_scrollbars (); - Refresh (); - } -} - - -template <class T> -int -place (shared_ptr<const Film> film, TimelineViewList& views, int& tracks) -{ - int const base = tracks; - - for (auto i: views) { - if (!dynamic_pointer_cast<T>(i)) { - continue; - } - - auto cv = dynamic_pointer_cast<TimelineContentView> (i); - DCPOMATIC_ASSERT(cv); - - int t = base; - - auto content = cv->content(); - DCPTimePeriod const content_period = content->period(film); - - while (true) { - auto j = views.begin(); - while (j != views.end()) { - auto test = dynamic_pointer_cast<T> (*j); - if (!test) { - ++j; - continue; - } - - auto test_content = test->content(); - if ( - test->track() && test->track().get() == t && - content_period.overlap(test_content->period(film)) - ) { - /* we have an overlap on track `t' */ - ++t; - break; - } - - ++j; - } - - if (j == views.end ()) { - /* no overlap on `t' */ - break; - } - } - - cv->set_track (t); - tracks = max (tracks, t + 1); - } - - return tracks - base; -} - - -/** Compare the mapped output channels of two TimelineViews, so we can into - * order of first mapped DCP channel. - */ -struct AudioMappingComparator { - bool operator()(shared_ptr<TimelineView> a, shared_ptr<TimelineView> b) { - int la = -1; - auto cva = dynamic_pointer_cast<TimelineAudioContentView>(a); - if (cva) { - auto oc = cva->content()->audio->mapping().mapped_output_channels(); - la = *min_element(boost::begin(oc), boost::end(oc)); - } - int lb = -1; - auto cvb = dynamic_pointer_cast<TimelineAudioContentView>(b); - if (cvb) { - auto oc = cvb->content()->audio->mapping().mapped_output_channels(); - lb = *min_element(boost::begin(oc), boost::end(oc)); - } - return la < lb; - } -}; - - -void -Timeline::assign_tracks () -{ - /* Tracks are: - Video 1 - Video 2 - Video N - Text 1 - Text 2 - Text N - Atmos - Audio 1 - Audio 2 - Audio N - */ - - auto film = _film.lock (); - DCPOMATIC_ASSERT (film); - - _tracks = 0; - - for (auto i: _views) { - auto c = dynamic_pointer_cast<TimelineContentView>(i); - if (c) { - c->unset_track (); - } - } - - int const video_tracks = place<TimelineVideoContentView> (film, _views, _tracks); - int const text_tracks = place<TimelineTextContentView> (film, _views, _tracks); - - /* Atmos */ - - bool have_atmos = false; - for (auto i: _views) { - auto cv = dynamic_pointer_cast<TimelineAtmosContentView>(i); - if (cv) { - cv->set_track (_tracks); - have_atmos = true; - } - } - - if (have_atmos) { - ++_tracks; - } - - /* Audio. We're sorting the views so that we get the audio views in order of increasing - DCP channel index. - */ - - auto views = _views; - sort(views.begin(), views.end(), AudioMappingComparator()); - int const audio_tracks = place<TimelineAudioContentView> (film, views, _tracks); - - _labels_view->set_video_tracks (video_tracks); - _labels_view->set_audio_tracks (audio_tracks); - _labels_view->set_text_tracks (text_tracks); - _labels_view->set_atmos (have_atmos); - - _time_axis_view->set_y (tracks()); - _reels_view->set_y (8); -} - - -int -Timeline::tracks () const -{ - return _tracks; -} - - -void -Timeline::setup_scrollbars () -{ - auto film = _film.lock (); - if (!film || !_pixels_per_second) { - return; - } - - int const h = tracks() * pixels_per_track() + tracks_y_offset() + _time_axis_view->bbox().height; - - _labels_canvas->SetVirtualSize (_labels_view->bbox().width, h); - _labels_canvas->SetScrollRate (_x_scroll_rate, _y_scroll_rate); - _main_canvas->SetVirtualSize (*_pixels_per_second * film->length().seconds(), h); - _main_canvas->SetScrollRate (_x_scroll_rate, _y_scroll_rate); -} - - -shared_ptr<TimelineView> -Timeline::event_to_view (wxMouseEvent& ev) -{ - /* Search backwards through views so that we find the uppermost one first */ - auto i = _views.rbegin(); - - int vsx, vsy; - _main_canvas->GetViewStart (&vsx, &vsy); - Position<int> const p (ev.GetX() + vsx * _x_scroll_rate, ev.GetY() + vsy * _y_scroll_rate); - - while (i != _views.rend() && !(*i)->bbox().contains (p)) { - ++i; - } - - if (i == _views.rend ()) { - return {}; - } - - return *i; -} - - -void -Timeline::left_down (wxMouseEvent& ev) -{ - _left_down = true; - _down_point = ev.GetPosition (); - - switch (_tool) { - case SELECT: - left_down_select (ev); - break; - case ZOOM: - case ZOOM_ALL: - case SNAP: - case SEQUENCE: - /* Nothing to do */ - break; - } -} - - -void -Timeline::left_down_select (wxMouseEvent& ev) -{ - auto view = event_to_view (ev); - auto content_view = dynamic_pointer_cast<TimelineContentView>(view); - - _down_view.reset (); +double constexpr minimum_pixels_per_second = 640.0 / (60 * 60 * 3); - if (content_view) { - _down_view = content_view; - _down_view_position = content_view->content()->position (); - } - if (dynamic_pointer_cast<TimelineTimeAxisView>(view)) { - int vsx, vsy; - _main_canvas->GetViewStart(&vsx, &vsy); - _viewer.seek(DCPTime::from_seconds((ev.GetPosition().x + vsx * _x_scroll_rate) / _pixels_per_second.get_value_or(1)), true); - } - - for (auto i: _views) { - auto cv = dynamic_pointer_cast<TimelineContentView>(i); - if (!cv) { - continue; - } - - if (!ev.ShiftDown ()) { - cv->set_selected (view == i); - } - } - - if (content_view && ev.ShiftDown ()) { - content_view->set_selected (!content_view->selected ()); - } - - _first_move = false; - - if (_down_view) { - /* Pre-compute the points that we might snap to */ - for (auto i: _views) { - auto cv = dynamic_pointer_cast<TimelineContentView>(i); - if (!cv || cv == _down_view || cv->content() == _down_view->content()) { - continue; - } - - auto film = _film.lock (); - DCPOMATIC_ASSERT (film); - - _start_snaps.push_back (cv->content()->position()); - _end_snaps.push_back (cv->content()->position()); - _start_snaps.push_back (cv->content()->end(film)); - _end_snaps.push_back (cv->content()->end(film)); - - for (auto i: cv->content()->reel_split_points(film)) { - _start_snaps.push_back (i); - } - } - - /* Tell everyone that things might change frequently during the drag */ - _down_view->content()->set_change_signals_frequent (true); - } -} - - -void -Timeline::left_up (wxMouseEvent& ev) -{ - _left_down = false; - - switch (_tool) { - case SELECT: - left_up_select (ev); - break; - case ZOOM: - left_up_zoom (ev); - break; - case ZOOM_ALL: - case SNAP: - case SEQUENCE: - break; - } -} - - -void -Timeline::left_up_select (wxMouseEvent& ev) +Timeline::Timeline(wxWindow* parent) + : wxPanel(parent, wxID_ANY) { - if (_down_view) { - _down_view->content()->set_change_signals_frequent (false); - } - _content_panel->set_selection (selected_content ()); - /* Since we may have just set change signals back to `not-frequent', we have to - make sure this position change is signalled, even if the position value has - not changed since the last time it was set (with frequent=true). This is - a bit of a hack. - */ - set_position_from_event (ev, true); - - /* Clear up up the stuff we don't do during drag */ - assign_tracks (); - setup_scrollbars (); - Refresh (); - - _start_snaps.clear (); - _end_snaps.clear (); -} - - -void -Timeline::left_up_zoom (wxMouseEvent& ev) -{ - _zoom_point = ev.GetPosition (); - - int vsx, vsy; - _main_canvas->GetViewStart (&vsx, &vsy); - - wxPoint top_left(min(_down_point.x, _zoom_point->x), min(_down_point.y, _zoom_point->y)); - wxPoint bottom_right(max(_down_point.x, _zoom_point->x), max(_down_point.y, _zoom_point->y)); - - if ((bottom_right.x - top_left.x) < 8 || (bottom_right.y - top_left.y) < 8) { - /* Very small zoom rectangle: we assume it wasn't intentional */ - _zoom_point = optional<wxPoint> (); - Refresh (); - return; - } - - auto const time_left = DCPTime::from_seconds((top_left.x + vsx) / *_pixels_per_second); - auto const time_right = DCPTime::from_seconds((bottom_right.x + vsx) / *_pixels_per_second); - set_pixels_per_second (double(GetSize().GetWidth()) / (time_right.seconds() - time_left.seconds())); - - double const tracks_top = double(top_left.y - tracks_y_offset()) / _pixels_per_track; - double const tracks_bottom = double(bottom_right.y - tracks_y_offset()) / _pixels_per_track; - set_pixels_per_track (lrint(GetSize().GetHeight() / (tracks_bottom - tracks_top))); - - setup_scrollbars (); - int const y = (tracks_top * _pixels_per_track + tracks_y_offset()) / _y_scroll_rate; - _main_canvas->Scroll (time_left.seconds() * *_pixels_per_second / _x_scroll_rate, y); - _labels_canvas->Scroll (0, y); - - _zoom_point = optional<wxPoint> (); - Refresh (); -} - - -void -Timeline::set_pixels_per_track (int h) -{ - _pixels_per_track = max(_minimum_pixels_per_track, h); } void -Timeline::mouse_moved (wxMouseEvent& ev) +Timeline::set_pixels_per_second(double pps) { - switch (_tool) { - case SELECT: - mouse_moved_select (ev); - break; - case ZOOM: - mouse_moved_zoom (ev); - break; - case ZOOM_ALL: - case SNAP: - case SEQUENCE: - break; - } + _pixels_per_second = std::max(minimum_pixels_per_second, pps); } -void -Timeline::mouse_moved_select (wxMouseEvent& ev) -{ - if (!_left_down) { - return; - } - - set_position_from_event (ev); -} - - -void -Timeline::mouse_moved_zoom (wxMouseEvent& ev) -{ - if (!_left_down) { - return; - } - - _zoom_point = ev.GetPosition (); - setup_scrollbars(); - Refresh (); -} - - -void -Timeline::right_down (wxMouseEvent& ev) -{ - switch (_tool) { - case SELECT: - right_down_select (ev); - break; - case ZOOM: - /* Zoom out */ - set_pixels_per_second (*_pixels_per_second / 2); - set_pixels_per_track (_pixels_per_track / 2); - setup_scrollbars (); - Refresh (); - break; - case ZOOM_ALL: - case SNAP: - case SEQUENCE: - break; - } -} - - -void -Timeline::right_down_select (wxMouseEvent& ev) -{ - auto view = event_to_view (ev); - auto cv = dynamic_pointer_cast<TimelineContentView> (view); - if (!cv) { - return; - } - - if (!cv->selected ()) { - clear_selection (); - cv->set_selected (true); - } - - _menu.popup (_film, selected_content (), selected_views (), ev.GetPosition ()); -} - - -void -Timeline::maybe_snap (DCPTime a, DCPTime b, optional<DCPTime>& nearest_distance) const -{ - auto const d = a - b; - if (!nearest_distance || d.abs() < nearest_distance.get().abs()) { - nearest_distance = d; - } -} - - -void -Timeline::set_position_from_event (wxMouseEvent& ev, bool force_emit) -{ - if (!_pixels_per_second) { - return; - } - - double const pps = _pixels_per_second.get (); - - auto const p = ev.GetPosition(); - - if (!_first_move) { - /* We haven't moved yet; in that case, we must move the mouse some reasonable distance - before the drag is considered to have started. - */ - int const dist = sqrt (pow (p.x - _down_point.x, 2) + pow (p.y - _down_point.y, 2)); - if (dist < 8) { - return; - } - _first_move = true; - } - - if (!_down_view) { - return; - } - - auto new_position = _down_view_position + DCPTime::from_seconds ((p.x - _down_point.x) / pps); - - auto film = _film.lock (); - DCPOMATIC_ASSERT (film); - - if (_snap) { - auto const new_end = new_position + _down_view->content()->length_after_trim(film); - /* Signed `distance' to nearest thing (i.e. negative is left on the timeline, - positive is right). - */ - optional<DCPTime> nearest_distance; - - /* Find the nearest snap point */ - - for (auto i: _start_snaps) { - maybe_snap (i, new_position, nearest_distance); - } - - for (auto i: _end_snaps) { - maybe_snap (i, new_end, nearest_distance); - } - - if (nearest_distance) { - /* Snap if it's close; `close' means within a proportion of the time on the timeline */ - if (nearest_distance.get().abs() < DCPTime::from_seconds ((width() / pps) / 64)) { - new_position += nearest_distance.get (); - } - } - } - - if (new_position < DCPTime ()) { - new_position = DCPTime (); - } - - _down_view->content()->set_position (film, new_position, force_emit); - - film->set_sequence (false); -} - - -void -Timeline::force_redraw (dcpomatic::Rect<int> const & r) -{ - _main_canvas->RefreshRect (wxRect (r.x, r.y, r.width, r.height), false); -} - - -shared_ptr<const Film> -Timeline::film () const -{ - return _film.lock (); -} - - -void -Timeline::resized () -{ - if (_main_canvas->GetSize().GetWidth() > 0 && _first_resize) { - zoom_all (); - _first_resize = false; - } - setup_scrollbars (); -} - - -void -Timeline::clear_selection () -{ - for (auto i: _views) { - shared_ptr<TimelineContentView> cv = dynamic_pointer_cast<TimelineContentView>(i); - if (cv) { - cv->set_selected (false); - } - } -} - - -TimelineContentViewList -Timeline::selected_views () const -{ - TimelineContentViewList sel; - - for (auto i: _views) { - auto cv = dynamic_pointer_cast<TimelineContentView>(i); - if (cv && cv->selected()) { - sel.push_back (cv); - } - } - - return sel; -} - - -ContentList -Timeline::selected_content () const -{ - ContentList sel; - - for (auto i: selected_views()) { - sel.push_back(i->content()); - } - - return sel; -} - - -void -Timeline::set_selection (ContentList selection) -{ - for (auto i: _views) { - auto cv = dynamic_pointer_cast<TimelineContentView> (i); - if (cv) { - cv->set_selected (find (selection.begin(), selection.end(), cv->content ()) != selection.end ()); - } - } -} - - -int -Timeline::tracks_y_offset () const -{ - return _reels_view->bbox().height + 4; -} - - -int -Timeline::width () const -{ - return _main_canvas->GetVirtualSize().GetWidth(); -} - - -void -Timeline::scrolled (wxScrollWinEvent& ev) -{ - if (ev.GetOrientation() == wxVERTICAL) { - int x, y; - _main_canvas->GetViewStart (&x, &y); - _labels_canvas->Scroll (0, y); - } - ev.Skip (); -} - - -void -Timeline::tool_clicked (Tool t) -{ - switch (t) { - case ZOOM: - case SELECT: - _tool = t; - break; - case ZOOM_ALL: - zoom_all (); - break; - case SNAP: - case SEQUENCE: - break; - } -} - - -void -Timeline::zoom_all () -{ - auto film = _film.lock (); - DCPOMATIC_ASSERT (film); - set_pixels_per_second((_main_canvas->GetSize().GetWidth() - 32) / std::max(1.0, film->length().seconds())); - set_pixels_per_track((_main_canvas->GetSize().GetHeight() - tracks_y_offset() - _time_axis_view->bbox().height - 32) / std::max(1, _tracks)); - setup_scrollbars (); - _main_canvas->Scroll (0, 0); - _labels_canvas->Scroll (0, 0); - Refresh (); -} - - -void -Timeline::keypress(wxKeyEvent const& event) -{ - if (event.GetKeyCode() == WXK_DELETE) { - auto film = _film.lock(); - DCPOMATIC_ASSERT(film); - film->remove_content(selected_content()); - } else { - switch (event.GetRawKeyCode()) { - case '+': - set_pixels_per_second(_pixels_per_second.get_value_or(1) * 2); - setup_scrollbars(); - break; - case '-': - set_pixels_per_second(_pixels_per_second.get_value_or(1) / 2); - setup_scrollbars(); - break; - } - } -} - diff --git a/src/wx/timeline.h b/src/wx/timeline.h index 621609fa7..cc35913b9 100644 --- a/src/wx/timeline.h +++ b/src/wx/timeline.h @@ -1,5 +1,5 @@ /* - Copyright (C) 2013-2019 Carl Hetherington <cth@carlh.net> + Copyright (C) 2023 Carl Hetherington <cth@carlh.net> This file is part of DCP-o-matic. @@ -19,137 +19,32 @@ */ -#include "content_menu.h" -#include "timeline_content_view.h" -#include "lib/film_property.h" -#include "lib/rect.h" +#ifndef DCPOMATIC_TIMELINE_H +#define DCPOMATIC_TIMELINE_H + + #include <dcp/warnings.h> LIBDCP_DISABLE_WARNINGS #include <wx/wx.h> LIBDCP_ENABLE_WARNINGS -#include <boost/signals2.hpp> - - -class ContentPanel; -class Film; -class FilmViewer; -class TimelineLabelsView; -class TimelineReelsView; -class TimelineTimeAxisView; -class TimelineView; +#include <boost/optional.hpp> class Timeline : public wxPanel { public: - Timeline (wxWindow *, ContentPanel *, std::shared_ptr<Film>, FilmViewer& viewer); - - std::shared_ptr<const Film> film () const; - - void force_redraw (dcpomatic::Rect<int> const &); - - int width () const; + explicit Timeline(wxWindow* parent); - int pixels_per_track () const { - return _pixels_per_track; - } - - boost::optional<double> pixels_per_second () const { + boost::optional<double> pixels_per_second() const { return _pixels_per_second; } - int tracks () const; - void set_snap (bool s) { - _snap = s; - } +protected: + void set_pixels_per_second(double pps); - bool snap () const { - return _snap; - } - - void set_selection (ContentList selection); - - enum Tool { - SELECT, - ZOOM, - ZOOM_ALL, - SNAP, - SEQUENCE - }; - - void tool_clicked (Tool t); - - int tracks_y_offset () const; - - void keypress(wxKeyEvent const &); - -private: - void paint_labels (); - void paint_main (); - void left_down (wxMouseEvent &); - void left_down_select (wxMouseEvent &); - void left_up (wxMouseEvent &); - void left_up_select (wxMouseEvent &); - void left_up_zoom (wxMouseEvent &); - void right_down (wxMouseEvent &); - void right_down_select (wxMouseEvent &); - void mouse_moved (wxMouseEvent &); - void mouse_moved_select (wxMouseEvent &); - void mouse_moved_zoom (wxMouseEvent &); - void film_change(ChangeType type, FilmProperty); - void film_content_change (ChangeType type, int, bool frequent); - void resized (); - void assign_tracks (); - void set_position_from_event (wxMouseEvent& ev, bool force_emit = false); - void clear_selection (); - void recreate_views (); - void setup_scrollbars (); - void scrolled (wxScrollWinEvent& ev); - void set_pixels_per_second (double pps); - void set_pixels_per_track (int h); - void zoom_all (); - void update_playhead (); - void mouse_wheel_turned(wxMouseEvent& event); - - std::shared_ptr<TimelineView> event_to_view (wxMouseEvent &); - TimelineContentViewList selected_views () const; - ContentList selected_content () const; - void maybe_snap (dcpomatic::DCPTime a, dcpomatic::DCPTime b, boost::optional<dcpomatic::DCPTime>& nearest_distance) const; - - wxScrolledCanvas* _labels_canvas; - wxScrolledCanvas* _main_canvas; - ContentPanel* _content_panel; - std::weak_ptr<Film> _film; - FilmViewer& _viewer; - TimelineViewList _views; - std::shared_ptr<TimelineTimeAxisView> _time_axis_view; - std::shared_ptr<TimelineReelsView> _reels_view; - std::shared_ptr<TimelineLabelsView> _labels_view; - int _tracks; boost::optional<double> _pixels_per_second; - bool _left_down; - wxPoint _down_point; - boost::optional<wxPoint> _zoom_point; - std::shared_ptr<TimelineContentView> _down_view; - dcpomatic::DCPTime _down_view_position; - bool _first_move; - ContentMenu _menu; - bool _snap; - std::list<dcpomatic::DCPTime> _start_snaps; - std::list<dcpomatic::DCPTime> _end_snaps; - Tool _tool; - int _x_scroll_rate; - int _y_scroll_rate; - int _pixels_per_track; - bool _first_resize; - wxTimer _timer; - boost::optional<int> _last_mouse_wheel_x; - boost::optional<double> _last_mouse_wheel_time; - - static double const _minimum_pixels_per_second; - static int const _minimum_pixels_per_track; - - boost::signals2::scoped_connection _film_changed_connection; - boost::signals2::scoped_connection _film_content_change_connection; }; + + +#endif diff --git a/src/wx/timeline_content_view.cc b/src/wx/timeline_content_view.cc index 5d039d0d3..cb0d10240 100644 --- a/src/wx/timeline_content_view.cc +++ b/src/wx/timeline_content_view.cc @@ -19,7 +19,7 @@ */ -#include "timeline.h" +#include "content_timeline.h" #include "timeline_content_view.h" #include "wx_util.h" #include "lib/content.h" @@ -37,8 +37,8 @@ using namespace boost::placeholders; #endif -TimelineContentView::TimelineContentView (Timeline& tl, shared_ptr<Content> c) - : TimelineView (tl) +TimelineContentView::TimelineContentView(ContentTimeline& tl, shared_ptr<Content> c) + : ContentTimelineView(tl) , _content (c) { _content_connection = c->Change.connect (bind (&TimelineContentView::content_change, this, _1, _3)); diff --git a/src/wx/timeline_content_view.h b/src/wx/timeline_content_view.h index 7794120cd..7b206d30f 100644 --- a/src/wx/timeline_content_view.h +++ b/src/wx/timeline_content_view.h @@ -23,7 +23,7 @@ #define DCPOMATIC_TIMELINE_CONTENT_VIEW_H -#include "timeline_view.h" +#include "content_timeline_view.h" #include "lib/change_signaller.h" #include <dcp/warnings.h> LIBDCP_DISABLE_WARNINGS @@ -37,10 +37,10 @@ class Content; /** @class TimelineContentView * @brief Parent class for views of pieces of content. */ -class TimelineContentView : public TimelineView +class TimelineContentView : public ContentTimelineView { public: - TimelineContentView (Timeline& tl, std::shared_ptr<Content> c); + TimelineContentView(ContentTimeline& tl, std::shared_ptr<Content> c); dcpomatic::Rect<int> bbox () const override; diff --git a/src/wx/timeline_labels_view.cc b/src/wx/timeline_labels_view.cc index 181adc5ca..c869d7ec5 100644 --- a/src/wx/timeline_labels_view.cc +++ b/src/wx/timeline_labels_view.cc @@ -19,7 +19,7 @@ */ -#include "timeline.h" +#include "content_timeline.h" #include "timeline_labels_view.h" #include "wx_util.h" #include <dcp/warnings.h> @@ -34,8 +34,8 @@ using std::max; using std::min; -TimelineLabelsView::TimelineLabelsView (Timeline& tl) - : TimelineView (tl) +TimelineLabelsView::TimelineLabelsView(ContentTimeline& tl) + : ContentTimelineView(tl) { wxString labels[] = { _("Video"), diff --git a/src/wx/timeline_labels_view.h b/src/wx/timeline_labels_view.h index fb80b1bf3..324531c39 100644 --- a/src/wx/timeline_labels_view.h +++ b/src/wx/timeline_labels_view.h @@ -19,16 +19,16 @@ */ -#include "timeline_view.h" +#include "content_timeline_view.h" class wxWindow; -class TimelineLabelsView : public TimelineView +class TimelineLabelsView : public ContentTimelineView { public: - explicit TimelineLabelsView (Timeline& tl); + explicit TimelineLabelsView(ContentTimeline& tl); dcpomatic::Rect<int> bbox () const override; diff --git a/src/wx/timeline_reels_view.cc b/src/wx/timeline_reels_view.cc index 0601a1196..5f2a78079 100644 --- a/src/wx/timeline_reels_view.cc +++ b/src/wx/timeline_reels_view.cc @@ -19,7 +19,7 @@ */ -#include "timeline.h" +#include "content_timeline.h" #include "timeline_reels_view.h" #include "wx_util.h" #include "lib/film.h" @@ -35,8 +35,8 @@ using std::list; using namespace dcpomatic; -TimelineReelsView::TimelineReelsView (Timeline& tl, int y) - : TimelineView (tl) +TimelineReelsView::TimelineReelsView(ContentTimeline& tl, int y) + : ContentTimelineView(tl) , _y (y) { diff --git a/src/wx/timeline_reels_view.h b/src/wx/timeline_reels_view.h index 357fe2ce4..7dbc34308 100644 --- a/src/wx/timeline_reels_view.h +++ b/src/wx/timeline_reels_view.h @@ -19,13 +19,13 @@ */ -#include "timeline_view.h" +#include "content_timeline_view.h" -class TimelineReelsView : public TimelineView +class TimelineReelsView : public ContentTimelineView { public: - TimelineReelsView (Timeline& tl, int y); + TimelineReelsView(ContentTimeline& tl, int y); dcpomatic::Rect<int> bbox () const override; void set_y (int y); diff --git a/src/wx/timeline_time_axis_view.cc b/src/wx/timeline_time_axis_view.cc index d055bda7d..d9b7710c6 100644 --- a/src/wx/timeline_time_axis_view.cc +++ b/src/wx/timeline_time_axis_view.cc @@ -19,7 +19,7 @@ */ -#include "timeline.h" +#include "content_timeline.h" #include "timeline_time_axis_view.h" #include "wx_util.h" #include <dcp/warnings.h> @@ -34,8 +34,8 @@ using std::list; using namespace dcpomatic; -TimelineTimeAxisView::TimelineTimeAxisView (Timeline& tl, int y) - : TimelineView (tl) +TimelineTimeAxisView::TimelineTimeAxisView(ContentTimeline& tl, int y) + : ContentTimelineView(tl) , _y (y) { diff --git a/src/wx/timeline_time_axis_view.h b/src/wx/timeline_time_axis_view.h index 4c8e090fe..f89121776 100644 --- a/src/wx/timeline_time_axis_view.h +++ b/src/wx/timeline_time_axis_view.h @@ -18,12 +18,14 @@ */ -#include "timeline_view.h" -class TimelineTimeAxisView : public TimelineView +#include "content_timeline_view.h" + + +class TimelineTimeAxisView : public ContentTimelineView { public: - TimelineTimeAxisView (Timeline& tl, int y); + TimelineTimeAxisView(ContentTimeline& tl, int y); dcpomatic::Rect<int> bbox () const override; void set_y (int y); diff --git a/src/wx/timeline_view.cc b/src/wx/timeline_view.cc deleted file mode 100644 index 2897c98e3..000000000 --- a/src/wx/timeline_view.cc +++ /dev/null @@ -1,69 +0,0 @@ -/* - Copyright (C) 2013-2021 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 "timeline_view.h" -#include "timeline.h" - - -using std::list; -using namespace dcpomatic; - - -/** @class TimelineView - * @brief Parent class for components of the timeline (e.g. a piece of content or an axis). - */ -TimelineView::TimelineView (Timeline& t) - : _timeline (t) -{ - -} - - -void -TimelineView::paint (wxGraphicsContext* g, list<dcpomatic::Rect<int>> overlaps) -{ - _last_paint_bbox = bbox (); - do_paint (g, overlaps); -} - - -void -TimelineView::force_redraw () -{ - _timeline.force_redraw (_last_paint_bbox.extended(4)); - _timeline.force_redraw (bbox().extended(4)); -} - - -int -TimelineView::time_x (DCPTime t) const -{ - return t.seconds() * _timeline.pixels_per_second().get_value_or(0); -} - - -int -TimelineView::y_pos(int t) const -{ - return t * _timeline.pixels_per_track() + _timeline.tracks_y_offset(); -} - - diff --git a/src/wx/timeline_view.h b/src/wx/timeline_view.h index 166a1121a..32eedde09 100644 --- a/src/wx/timeline_view.h +++ b/src/wx/timeline_view.h @@ -1,5 +1,5 @@ /* - Copyright (C) 2013-2021 Carl Hetherington <cth@carlh.net> + Copyright (C) 2023 Carl Hetherington <cth@carlh.net> This file is part of DCP-o-matic. @@ -28,40 +28,42 @@ class wxGraphicsContext; -class Timeline; -/** @class TimelineView - * @brief Parent class for components of the timeline (e.g. a piece of content or an axis). +/** @class ContentTimelineView + * @brief Parent class for components of the content timeline (e.g. a piece of content or an axis). */ +template <class Timeline> class TimelineView { public: - explicit TimelineView (Timeline& t); - virtual ~TimelineView () {} + explicit TimelineView(Timeline& timeline) + : _timeline(timeline) + {} - TimelineView (TimelineView const&) = delete; - TimelineView& operator= (TimelineView const&) = delete; + virtual ~TimelineView () = default; - void paint (wxGraphicsContext* g, std::list<dcpomatic::Rect<int>> overlaps); - void force_redraw (); + TimelineView(TimelineView const&) = delete; + TimelineView& operator=(TimelineView const&) = delete; - virtual dcpomatic::Rect<int> bbox () const = 0; + void force_redraw() + { + _timeline.force_redraw(_last_paint_bbox.extended(4)); + _timeline.force_redraw(bbox().extended(4)); + } -protected: - virtual void do_paint (wxGraphicsContext *, std::list<dcpomatic::Rect<int>> overlaps) = 0; + virtual dcpomatic::Rect<int> bbox() const = 0; - int time_x (dcpomatic::DCPTime t) const; - int y_pos(int t) const; +protected: + int time_x(dcpomatic::DCPTime t) const + { + return t.seconds() * _timeline.pixels_per_second().get_value_or(0); + } Timeline& _timeline; - -private: dcpomatic::Rect<int> _last_paint_bbox; }; -typedef std::vector<std::shared_ptr<TimelineView>> TimelineViewList; - - #endif + diff --git a/src/wx/video_panel.cc b/src/wx/video_panel.cc index 859c0a886..5a91df8fe 100644 --- a/src/wx/video_panel.cc +++ b/src/wx/video_panel.cc @@ -76,14 +76,6 @@ VideoPanel::VideoPanel (ContentPanel* p) void VideoPanel::create () { - _reference = new CheckBox (this, _("Use this DCP's video as OV and make VF")); - _reference_note = new StaticText (this, wxT("")); - _reference_note->Wrap (200); - auto font = _reference_note->GetFont(); - font.SetStyle(wxFONTSTYLE_ITALIC); - font.SetPointSize(font.GetPointSize() - 1); - _reference_note->SetFont(font); - _type_label = create_label (this, _("Type"), true); _frame_type = new ContentChoice<VideoContent, VideoFrameType> ( this, @@ -203,6 +195,9 @@ VideoPanel::create () _range->Append (_("Video (MPEG, 16-235)")); _description = new StaticText (this, wxT ("\n \n \n \n \n"), wxDefaultPosition, wxDefaultSize); + auto font = _description->GetFont(); + font.SetStyle(wxFONTSTYLE_ITALIC); + font.SetPointSize(font.GetPointSize() - 1); _description->SetFont(font); _left_crop->wrapped()->SetRange (0, 4096); @@ -223,7 +218,6 @@ VideoPanel::create () _fade_in->Changed.connect (boost::bind (&VideoPanel::fade_in_changed, this)); _fade_out->Changed.connect (boost::bind (&VideoPanel::fade_out_changed, this)); - _reference->bind(&VideoPanel::reference_clicked, this); _scale_fit->Bind (wxEVT_RADIOBUTTON, boost::bind (&VideoPanel::scale_fit_clicked, this)); _scale_custom->Bind (wxEVT_RADIOBUTTON, boost::bind (&VideoPanel::scale_custom_clicked, this)); _scale_custom_edit->Bind (wxEVT_BUTTON, boost::bind (&VideoPanel::scale_custom_edit_clicked, this)); @@ -244,12 +238,6 @@ VideoPanel::add_to_grid () { int r = 0; - auto reference_sizer = new wxBoxSizer (wxVERTICAL); - reference_sizer->Add (_reference, 0); - reference_sizer->Add (_reference_note, 0); - _grid->Add (reference_sizer, wxGBPosition(r, 0), wxGBSpan(1, 3)); - ++r; - add_label_to_sizer (_grid, _type_label, true, wxGBPosition(r, 0)); _frame_type->add (_grid, wxGBPosition(r, 1), wxGBSpan(1, 2)); ++r; @@ -458,15 +446,6 @@ VideoPanel::film_content_changed (int property) } else { _fade_out->clear (); } - } else if (property == DCPContentProperty::REFERENCE_VIDEO) { - if (vc.size() == 1) { - shared_ptr<DCPContent> dcp = dynamic_pointer_cast<DCPContent> (vc.front ()); - checked_set (_reference, dcp ? dcp->reference_video () : false); - } else { - checked_set (_reference, false); - } - - setup_sensitivity (); } else if (property == VideoContentProperty::RANGE) { if (vcs) { checked_set (_range, vcs->video->range() == VideoRange::FULL ? 0 : 1); @@ -594,15 +573,7 @@ VideoPanel::setup_sensitivity () dcp = dynamic_pointer_cast<DCPContent> (sel.front ()); } - string why_not; - bool const can_reference = dcp && dcp->can_reference_video (_parent->film(), why_not); - wxString cannot; - if (why_not.empty()) { - cannot = _("Cannot reference this DCP's video."); - } else { - cannot = _("Cannot reference this DCP's video: ") + std_to_wx(why_not); - } - setup_refer_button (_reference, _reference_note, dcp, can_reference, cannot); + bool const reference = dcp && dcp->reference_video(); bool any_use = false; for (auto i: _parent->selected_video()) { @@ -611,7 +582,7 @@ VideoPanel::setup_sensitivity () } } - bool const enable = !_reference->GetValue() && any_use; + bool const enable = !reference && any_use; if (!enable) { _frame_type->wrapped()->Enable (false); @@ -684,23 +655,6 @@ VideoPanel::fade_out_changed () void -VideoPanel::reference_clicked () -{ - auto c = _parent->selected (); - if (c.size() != 1) { - return; - } - - auto d = dynamic_pointer_cast<DCPContent> (c.front ()); - if (!d) { - return; - } - - d->set_reference_video (_reference->GetValue ()); -} - - -void VideoPanel::scale_fit_clicked () { for (auto i: _parent->selected_video()) { diff --git a/src/wx/video_panel.h b/src/wx/video_panel.h index 686d1b99b..e6b689b03 100644 --- a/src/wx/video_panel.h +++ b/src/wx/video_panel.h @@ -53,7 +53,6 @@ public: void content_selection_changed () override; private: - void reference_clicked (); void colour_conversion_changed (); void edit_colour_conversion_clicked (); void range_changed (); @@ -73,8 +72,6 @@ private: void setup_description (); void setup_sensitivity (); - CheckBox* _reference; - wxStaticText* _reference_note; wxStaticText* _type_label; ContentChoice<VideoContent, VideoFrameType>* _frame_type; wxStaticText* _crop_label; diff --git a/src/wx/wscript b/src/wx/wscript index 9c6ea6b84..cf05dfe7d 100644 --- a/src/wx/wscript +++ b/src/wx/wscript @@ -48,12 +48,23 @@ sources = """ content_panel.cc content_properties_dialog.cc content_sub_panel.cc + content_timeline.cc + content_timeline_atmos_view.cc + content_timeline_audio_view.cc + content_timeline_dialog.cc + content_timeline_text_view.cc + content_timeline_video_view.cc + content_timeline_view.cc content_version_dialog.cc content_view.cc controls.cc credentials_download_certificate_panel.cc custom_scale_dialog.cc + dcp_referencing_dialog.cc dcp_panel.cc + dcp_timeline.cc + dcp_timeline_dialog.cc + dcp_timeline_reel_marker_view.cc dcp_text_track_dialog.cc dcpomatic_button.cc dcpomatic_choice.cc @@ -160,16 +171,10 @@ sources = """ timer_display.cc timecode.cc timeline.cc - timeline_atmos_content_view.cc timeline_content_view.cc - timeline_dialog.cc - timeline_audio_content_view.cc timeline_labels_view.cc - timeline_text_content_view.cc timeline_reels_view.cc timeline_time_axis_view.cc - timeline_video_content_view.cc - timeline_view.cc timing_panel.cc try_unmount_dialog.cc update_dialog.cc diff --git a/src/wx/wx_util.h b/src/wx/wx_util.h index 66b01640c..d8935daa1 100644 --- a/src/wx/wx_util.h +++ b/src/wx/wx_util.h @@ -56,7 +56,7 @@ class PasswordEntry; #define DCPOMATIC_SIZER_GAP 8 #define DCPOMATIC_DIALOG_BORDER 12 #ifdef __WXGTK3__ -#define DCPOMATIC_SPIN_CTRL_WIDTH 118 +#define DCPOMATIC_SPIN_CTRL_WIDTH 124 #else #define DCPOMATIC_SPIN_CTRL_WIDTH 56 #endif @@ -74,7 +74,7 @@ class PasswordEntry; /** Amount by which you need to top-pad a choice to make it line up, in some cases */ #ifdef DCPOMATIC_OSX -#define DCPOMATIC_CHOICE_TOP_PAD 1 +#define DCPOMATIC_CHOICE_TOP_PAD 2 #else #define DCPOMATIC_CHOICE_TOP_PAD 0 #endif |
