diff options
133 files changed, 3887 insertions, 1444 deletions
@@ -502,7 +502,7 @@ def dependencies(target, options): ffmpeg_options = {} if target.platform != 'linux' or target.distro != 'arch': - deps = [('ffmpeg', '7276e269a93c2ae30e302c34708e8095ac5475e8', ffmpeg_options)] + deps = [('ffmpeg', '0b73d2f5e70a04a67aa902902c42e3025ef3bb77', ffmpeg_options)] else: # Use distro-provided FFmpeg on Arch deps = [] @@ -518,7 +518,7 @@ def dependencies(target, options): deps.append(('openssl', '54298369cacfe0ae01c5aa42ace8a463fd2e7a2e')) if can_build_disk(target): deps.append(('lwext4', 'ab082923a791b58478d1d9939d65a0583566ac1f')) - deps.append(('ffcmp', '53c853d2935de3f2b0d53777529e48c102afd237')) + deps.append(('ffcmp', '5ab6ed3b75d8ca7cf1f66bb9fb08792b92f4b419')) return deps @@ -563,6 +563,9 @@ def configure_options(target, options, for_package=False): if target.platform == 'osx' and target.arch == 'arm64': opt += ' --target-macos-arm64 --wx-config=%s/wx-config' % target.bin + if target.platform == 'linux' and target.distro == 'ubuntu' and target.version in ['22.04']: + opt += ' --enable-grok' + return opt def build(target, options, for_package): diff --git a/platform/osx/make_dmg.sh b/platform/osx/make_dmg.sh index 4772d8b19..161261faa 100644 --- a/platform/osx/make_dmg.sh +++ b/platform/osx/make_dmg.sh @@ -158,6 +158,7 @@ function copy_libs { copy_lib_root libleqm_nrt "$dest" copy $ROOT src/dcpomatic/build/src/lib/libdcpomatic2.dylib "$dest" copy $ROOT src/dcpomatic/build/src/wx/libdcpomatic2-wx.dylib "$dest" + copy_lib_env libboost_atomic "$dest" copy_lib_env libboost_system "$dest" copy_lib_env libboost_filesystem "$dest" copy_lib_env libboost_thread "$dest" @@ -269,7 +270,7 @@ function copy_resources { # i18n: wxWidgets .mo files for lang in de es fr it sv nl ru pl da cs sl; do mkdir "$dest/$lang" - cp $ENV/$ARCH1/share/locale/$lang/LC_MESSAGES/wxstd.mo "$dest/$lang" + cp $ENV/$ARCH1/share/locale/$lang/LC_MESSAGES/wxstd*.mo "$dest/$lang" done } diff --git a/platform/windows/wscript b/platform/windows/wscript index 8eb0fafa9..5af8ca08a 100644 --- a/platform/windows/wscript +++ b/platform/windows/wscript @@ -116,14 +116,14 @@ File "%static_deps%/bin/libssh.dll" File "%static_deps%/bin/libstdc++-6.dll" File "%static_deps%/bin/zlib1.dll" File "%static_deps%/bin/libjpeg-9.dll" -File "%static_deps%/bin/wxbase314u_gcc_custom.dll" -File "%static_deps%/bin/wxmsw314u_core_gcc_custom.dll" -File "%static_deps%/bin/wxmsw314u_adv_gcc_custom.dll" -File "%static_deps%/bin/wxmsw314u_richtext_gcc_custom.dll" -File "%static_deps%/bin/wxmsw314u_html_gcc_custom.dll" -File "%static_deps%/bin/wxmsw314u_gl_gcc_custom.dll" -File "%static_deps%/bin/wxmsw314u_propgrid_gcc_custom.dll" -File "%static_deps%/bin/wxbase314u_xml_gcc_custom.dll" +File "%static_deps%/bin/wxbase317u_gcc_custom.dll" +File "%static_deps%/bin/wxmsw317u_core_gcc_custom.dll" +File "%static_deps%/bin/wxmsw317u_adv_gcc_custom.dll" +File "%static_deps%/bin/wxmsw317u_richtext_gcc_custom.dll" +File "%static_deps%/bin/wxmsw317u_html_gcc_custom.dll" +File "%static_deps%/bin/wxmsw317u_gl_gcc_custom.dll" +File "%static_deps%/bin/wxmsw317u_propgrid_gcc_custom.dll" +File "%static_deps%/bin/wxbase317u_xml_gcc_custom.dll" File "%static_deps%/bin/libcairo-2.dll" File "%static_deps%/bin/libfreetype-6.dll" File "%static_deps%/bin/libgthread-2.0-0.dll" @@ -131,7 +131,8 @@ File "%static_deps%/bin/libpango-1.0-0.dll" File "%static_deps%/bin/libgmodule-2.0-0.dll" File "%static_deps%/bin/libpangocairo-1.0-0.dll" File "%static_deps%/bin/libpangowin32-1.0-0.dll" -File "%static_deps%/bin/libtiff-5.dll" +File "%static_deps%/bin/libpangoft2-1.0-0.dll" +File "%static_deps%/bin/libtiff-6.dll" File "%static_deps%/bin/libglibmm-2.4-1.dll" File "%static_deps%/bin/libxml++-2.6-2.dll" File "%static_deps%/bin/libxml2-2.dll" @@ -143,7 +144,7 @@ File "%static_deps%/bin/libxmlsec1.dll" File "%static_deps%/bin/libxmlsec1-openssl.dll" File "%static_deps%/bin/libexslt-0.dll" File "%static_deps%/bin/libxslt-1.dll" -File "%static_deps%/bin/libffi-6.dll" +File "%static_deps%/bin/libffi-7.dll" File "%static_deps%/bin/openssl.exe" File "%static_deps%/bin/libcurl-4.dll" File "%static_deps%/bin/libzip.dll" @@ -166,11 +167,14 @@ File "%static_deps%/bin/libunistring-2.dll" File "%static_deps%/bin/libssh2-1.dll" File "%static_deps%/bin/libgcrypt-20.dll" File "%static_deps%/bin/libgpg-error-0.dll" -File "%static_deps%/bin/libpangoft2-1.0-0.dll" File "%static_deps%/bin/libx264-155.dll" File "%static_deps%/bin/libwebp-7.dll" File "%static_deps%/bin/GLEW.dll" File "%static_deps%/bin/libdav1d.dll" +File "%static_deps%/bin/libbrotlidec.dll" +File "%static_deps%/bin/libbrotlicommon.dll" +File "%static_deps%/bin/libfribidi-0.dll" +File "%static_deps%/bin/libsharpyuv-0.dll" """, file=f) if bits == 32: @@ -196,14 +200,14 @@ File "%cdist_deps%/lib/liblwext4.dll" """, file=f) print(""" -File "%cdist_deps%/bin/avcodec-58.dll" -File "%cdist_deps%/bin/avfilter-7.dll" -File "%cdist_deps%/bin/avformat-58.dll" -File "%cdist_deps%/bin/avutil-56.dll" -File "%cdist_deps%/bin/avdevice-58.dll" -File "%cdist_deps%/bin/postproc-55.dll" -File "%cdist_deps%/bin/swresample-3.dll" -File "%cdist_deps%/bin/swscale-5.dll" +File "%cdist_deps%/bin/avcodec-60.dll" +File "%cdist_deps%/bin/avfilter-9.dll" +File "%cdist_deps%/bin/avformat-60.dll" +File "%cdist_deps%/bin/avutil-58.dll" +File "%cdist_deps%/bin/avdevice-60.dll" +File "%cdist_deps%/bin/postproc-57.dll" +File "%cdist_deps%/bin/swresample-4.dll" +File "%cdist_deps%/bin/swscale-7.dll" File "%cdist_deps%/bin/dcp-1.0.dll" File "%cdist_deps%/bin/cxml-0.dll" File "%cdist_deps%/bin/sub-1.0.dll" @@ -232,47 +236,47 @@ SetOutPath "$INSTDIR\\locale\\fr\\LC_MESSAGES" File "%binaries%/src/lib/mo/fr_FR/libdcpomatic2.mo" File "%binaries%/src/wx/mo/fr_FR/libdcpomatic2-wx.mo" File "%binaries%/src/tools/mo/fr_FR/dcpomatic2.mo" -File "%static_deps%/share/locale/fr/LC_MESSAGES/wxstd.mo" +File "%static_deps%/share/locale/fr/LC_MESSAGES/wxstd-3.1.mo" SetOutPath "$INSTDIR\\locale\\it\\LC_MESSAGES" File "%binaries%/src/lib/mo/it_IT/libdcpomatic2.mo" File "%binaries%/src/wx/mo/it_IT/libdcpomatic2-wx.mo" File "%binaries%/src/tools/mo/it_IT/dcpomatic2.mo" -File "%static_deps%/share/locale/it/LC_MESSAGES/wxstd.mo" +File "%static_deps%/share/locale/it/LC_MESSAGES/wxstd-3.1.mo" SetOutPath "$INSTDIR\\locale\\es\\LC_MESSAGES" File "%binaries%/src/lib/mo/es_ES/libdcpomatic2.mo" File "%binaries%/src/wx/mo/es_ES/libdcpomatic2-wx.mo" File "%binaries%/src/tools/mo/es_ES/dcpomatic2.mo" -File "%static_deps%/share/locale/es/LC_MESSAGES/wxstd.mo" +File "%static_deps%/share/locale/es/LC_MESSAGES/wxstd-3.1.mo" SetOutPath "$INSTDIR\\locale\\sv\\LC_MESSAGES" File "%binaries%/src/lib/mo/sv_SE/libdcpomatic2.mo" File "%binaries%/src/wx/mo/sv_SE/libdcpomatic2-wx.mo" File "%binaries%/src/tools/mo/sv_SE/dcpomatic2.mo" -File "%static_deps%/share/locale/sv/LC_MESSAGES/wxstd.mo" +File "%static_deps%/share/locale/sv/LC_MESSAGES/wxstd-3.1.mo" SetOutPath "$INSTDIR\\locale\\de\\LC_MESSAGES" File "%binaries%/src/lib/mo/de_DE/libdcpomatic2.mo" File "%binaries%/src/wx/mo/de_DE/libdcpomatic2-wx.mo" File "%binaries%/src/tools/mo/de_DE/dcpomatic2.mo" -File "%static_deps%/share/locale/de/LC_MESSAGES/wxstd.mo" +File "%static_deps%/share/locale/de/LC_MESSAGES/wxstd-3.1.mo" SetOutPath "$INSTDIR\\locale\\nl\\LC_MESSAGES" File "%binaries%/src/lib/mo/nl_NL/libdcpomatic2.mo" File "%binaries%/src/wx/mo/nl_NL/libdcpomatic2-wx.mo" File "%binaries%/src/tools/mo/nl_NL/dcpomatic2.mo" -File "%static_deps%/share/locale/nl/LC_MESSAGES/wxstd.mo" +File "%static_deps%/share/locale/nl/LC_MESSAGES/wxstd-3.1.mo" SetOutPath "$INSTDIR\\locale\\ru\\LC_MESSAGES" File "%binaries%/src/lib/mo/ru_RU/libdcpomatic2.mo" File "%binaries%/src/wx/mo/ru_RU/libdcpomatic2-wx.mo" File "%binaries%/src/tools/mo/ru_RU/dcpomatic2.mo" -File "%static_deps%/share/locale/ru/LC_MESSAGES/wxstd.mo" +File "%static_deps%/share/locale/ru/LC_MESSAGES/wxstd-3.1.mo" SetOutPath "$INSTDIR\\locale\\pl\\LC_MESSAGES" File "%binaries%/src/lib/mo/pl_PL/libdcpomatic2.mo" File "%binaries%/src/wx/mo/pl_PL/libdcpomatic2-wx.mo" File "%binaries%/src/tools/mo/pl_PL/dcpomatic2.mo" -File "%static_deps%/share/locale/pl/LC_MESSAGES/wxstd.mo" +File "%static_deps%/share/locale/pl/LC_MESSAGES/wxstd-3.1.mo" SetOutPath "$INSTDIR\\locale\\da\\LC_MESSAGES" File "%binaries%/src/lib/mo/da_DK/libdcpomatic2.mo" File "%binaries%/src/wx/mo/da_DK/libdcpomatic2-wx.mo" File "%binaries%/src/tools/mo/da_DK/dcpomatic2.mo" -File "%static_deps%/share/locale/da/LC_MESSAGES/wxstd.mo" +File "%static_deps%/share/locale/da/LC_MESSAGES/wxstd-3.1.mo" SetOutPath "$INSTDIR\\locale\\pt_PT\\LC_MESSAGES" File "%binaries%/src/lib/mo/pt_PT/libdcpomatic2.mo" File "%binaries%/src/wx/mo/pt_PT/libdcpomatic2-wx.mo" @@ -289,7 +293,7 @@ SetOutPath "$INSTDIR\\locale\\cs\\LC_MESSAGES" File "%binaries%/src/lib/mo/cs_CZ/libdcpomatic2.mo" File "%binaries%/src/wx/mo/cs_CZ/libdcpomatic2-wx.mo" File "%binaries%/src/tools/mo/cs_CZ/dcpomatic2.mo" -File "%static_deps%/share/locale/cs/LC_MESSAGES/wxstd.mo" +File "%static_deps%/share/locale/cs/LC_MESSAGES/wxstd-3.1.mo" SetOutPath "$INSTDIR\\locale\\uk\\LC_MESSAGES" File "%binaries%/src/lib/mo/uk_UA/libdcpomatic2.mo" File "%binaries%/src/wx/mo/uk_UA/libdcpomatic2-wx.mo" diff --git a/run/tests.bat b/run/tests.bat index a9d921788..92030e61c 100644 --- a/run/tests.bat +++ b/run/tests.bat @@ -1,4 +1,4 @@ -set PATH=%PATH%;c:\users\ci\bin;c:\users\ci\workspace\dcpomatic\bin;c:\users\ci\workspace\dcpomatic\lib +set PATH=%PATH%;c:\users\ci\bin_v2.17.x;c:\users\ci\workspace\dcpomatic\bin;c:\users\ci\workspace\dcpomatic\lib set DCPOMATIC_TEST_PRIVATE=c:\users\ci\dcpomatic-test-private xcopy ..\libdcp\tags build\tags\ copy ..\libdcp\ratings build\ diff --git a/src/lib/audio_analysis.cc b/src/lib/audio_analysis.cc index b8c2e072d..c50a2bb9d 100644 --- a/src/lib/audio_analysis.cc +++ b/src/lib/audio_analysis.cc @@ -84,11 +84,8 @@ AudioAnalysis::AudioAnalysis (boost::filesystem::path filename) } for (auto i: f.node_children ("SamplePeak")) { - _sample_peak.push_back ( - PeakTime( - dcp::raw_convert<float>(i->content()), DCPTime(i->number_attribute<Frame>("Time")) - ) - ); + auto const time = number_attribute<Frame>(i, "Time", "time"); + _sample_peak.push_back(PeakTime(dcp::raw_convert<float>(i->content()), DCPTime(time))); } for (auto i: f.node_children("TruePeak")) { @@ -155,7 +152,7 @@ AudioAnalysis::write (boost::filesystem::path filename) for (size_t i = 0; i < _sample_peak.size(); ++i) { auto n = root->add_child("SamplePeak"); n->add_child_text (raw_convert<string> (_sample_peak[i].peak)); - n->set_attribute ("Time", raw_convert<string> (_sample_peak[i].time.get())); + n->set_attribute("time", raw_convert<string> (_sample_peak[i].time.get())); } for (auto i: _true_peak) { diff --git a/src/lib/audio_filter_graph.cc b/src/lib/audio_filter_graph.cc index 4e3052d57..cb888c162 100644 --- a/src/lib/audio_filter_graph.cc +++ b/src/lib/audio_filter_graph.cc @@ -48,11 +48,7 @@ AudioFilterGraph::AudioFilterGraph (int sample_rate, int channels) /* FFmpeg doesn't know any channel layouts for any counts between 8 and 16, so we need to tell it we're using 16 channels if we are using more than 8. */ - if (_channels > 8) { - _channel_layout = av_get_default_channel_layout (16); - } else { - _channel_layout = av_get_default_channel_layout (_channels); - } + av_channel_layout_default(&_channel_layout, _channels > 8 ? 16 : _channels); _in_frame = av_frame_alloc (); if (_in_frame == nullptr) { @@ -69,7 +65,7 @@ string AudioFilterGraph::src_parameters () const { char layout[64]; - av_get_channel_layout_string (layout, sizeof(layout), 0, _channel_layout); + av_channel_layout_describe(&_channel_layout, layout, sizeof(layout)); char buffer[256]; snprintf ( @@ -88,8 +84,9 @@ AudioFilterGraph::set_parameters (AVFilterContext* context) const int r = av_opt_set_int_list (context, "sample_fmts", sample_fmts, AV_SAMPLE_FMT_NONE, AV_OPT_SEARCH_CHILDREN); DCPOMATIC_ASSERT (r >= 0); - int64_t channel_layouts[] = { _channel_layout, -1 }; - r = av_opt_set_int_list (context, "channel_layouts", channel_layouts, -1, AV_OPT_SEARCH_CHILDREN); + char ch_layout[64]; + av_channel_layout_describe(&_channel_layout, ch_layout, sizeof(ch_layout)); + r = av_opt_set(context, "ch_layouts", ch_layout, AV_OPT_SEARCH_CHILDREN); DCPOMATIC_ASSERT (r >= 0); int sample_rates[] = { _sample_rate, -1 }; @@ -114,7 +111,7 @@ void AudioFilterGraph::process (shared_ptr<AudioBuffers> buffers) { DCPOMATIC_ASSERT (buffers->frames() > 0); - int const process_channels = av_get_channel_layout_nb_channels (_channel_layout); + int const process_channels = _channel_layout.nb_channels; DCPOMATIC_ASSERT (process_channels >= buffers->channels()); if (buffers->channels() < process_channels) { @@ -144,8 +141,10 @@ AudioFilterGraph::process (shared_ptr<AudioBuffers> buffers) _in_frame->nb_samples = buffers->frames (); _in_frame->format = AV_SAMPLE_FMT_FLTP; _in_frame->sample_rate = _sample_rate; - _in_frame->channel_layout = _channel_layout; + _in_frame->ch_layout = _channel_layout; +LIBDCP_DISABLE_WARNINGS _in_frame->channels = process_channels; +LIBDCP_ENABLE_WARNINGS int r = av_buffersrc_write_frame (_buffer_src_context, _in_frame); diff --git a/src/lib/audio_filter_graph.h b/src/lib/audio_filter_graph.h index e5c55fa27..6d09f15be 100644 --- a/src/lib/audio_filter_graph.h +++ b/src/lib/audio_filter_graph.h @@ -26,6 +26,7 @@ #include "filter_graph.h" extern "C" { #include <libavfilter/buffersink.h> +#include <libavutil/channel_layout.h> } class AudioBuffers; @@ -47,7 +48,7 @@ protected: private: int _sample_rate; int _channels; - int64_t _channel_layout; + AVChannelLayout _channel_layout; AVFrame* _in_frame; }; diff --git a/src/lib/audio_mapping.cc b/src/lib/audio_mapping.cc index b8aa6249f..6e8c4e30d 100644 --- a/src/lib/audio_mapping.cc +++ b/src/lib/audio_mapping.cc @@ -24,6 +24,7 @@ #include "constants.h" #include "dcpomatic_assert.h" #include "digester.h" +#include "util.h" #include <dcp/raw_convert.h> #include <dcp/warnings.h> #include <libcxml/cxml.h> @@ -169,8 +170,8 @@ AudioMapping::AudioMapping (cxml::ConstNodePtr node, int state_version) ); } else { set ( - i->number_attribute<int>("Input"), - i->number_attribute<int>("Output"), + number_attribute<int>(i, "Input", "input"), + number_attribute<int>(i, "Output", "output"), raw_convert<float>(i->content()) ); } @@ -230,8 +231,8 @@ AudioMapping::as_xml (xmlpp::Node* node) const for (int c = 0; c < input; ++c) { for (int d = 0; d < output; ++d) { auto t = node->add_child ("Gain"); - t->set_attribute ("Input", raw_convert<string> (c)); - t->set_attribute ("Output", raw_convert<string> (d)); + t->set_attribute("input", raw_convert<string>(c)); + t->set_attribute("output", raw_convert<string>(d)); t->add_child_text (raw_convert<string> (get (c, d))); } } diff --git a/src/lib/butler.cc b/src/lib/butler.cc index b2fbc6c60..dd9874587 100644 --- a/src/lib/butler.cc +++ b/src/lib/butler.cc @@ -138,7 +138,7 @@ Butler::should_run () const { if (_video.size() >= MAXIMUM_VIDEO_READAHEAD * 10) { /* This is way too big */ - optional<DCPTime> pos = _audio.peek(); + auto pos = _audio.peek(); if (pos) { throw ProgrammingError (__FILE__, __LINE__, String::compose ("Butler video buffers reached %1 frames (audio is %2 at %3)", _video.size(), _audio.size(), pos->get())); diff --git a/src/lib/cinema.cc b/src/lib/cinema.cc index 3b4b9d7b6..7388dbc2f 100644 --- a/src/lib/cinema.cc +++ b/src/lib/cinema.cc @@ -41,14 +41,6 @@ Cinema::Cinema (cxml::ConstNodePtr node) for (auto i: node->node_children("Email")) { emails.push_back (i->content ()); } - - if (node->optional_number_child<int>("UTCOffset")) { - _utc_offset_hour = node->number_child<int>("UTCOffset"); - } else { - _utc_offset_hour = node->optional_number_child<int>("UTCOffsetHour").get_value_or (0); - } - - _utc_offset_minute = node->optional_number_child<int>("UTCOffsetMinute").get_value_or (0); } /* This is necessary so that we can use shared_from_this in add_screen (which cannot be done from @@ -73,9 +65,6 @@ Cinema::as_xml (xmlpp::Element* parent) const parent->add_child("Notes")->add_child_text (notes); - parent->add_child("UTCOffsetHour")->add_child_text (raw_convert<string> (_utc_offset_hour)); - parent->add_child("UTCOffsetMinute")->add_child_text (raw_convert<string> (_utc_offset_minute)); - for (auto i: _screens) { i->as_xml (parent->add_child ("Screen")); } @@ -97,16 +86,3 @@ Cinema::remove_screen (shared_ptr<Screen> s) } } -void -Cinema::set_utc_offset_hour (int h) -{ - DCPOMATIC_ASSERT (h >= -11 && h <= 12); - _utc_offset_hour = h; -} - -void -Cinema::set_utc_offset_minute (int m) -{ - DCPOMATIC_ASSERT (m >= 0 && m <= 59); - _utc_offset_minute = m; -} diff --git a/src/lib/cinema.h b/src/lib/cinema.h index 6c202a7bf..7008659d7 100644 --- a/src/lib/cinema.h +++ b/src/lib/cinema.h @@ -44,12 +44,10 @@ namespace dcpomatic { class Cinema : public std::enable_shared_from_this<Cinema> { public: - Cinema(std::string const & name_, std::vector<std::string> const & e, std::string notes_, int utc_offset_hour, int utc_offset_minute) + Cinema(std::string const & name_, std::vector<std::string> const & e, std::string notes_) : name (name_) , emails (e) , notes (notes_) - , _utc_offset_hour (utc_offset_hour) - , _utc_offset_minute (utc_offset_minute) {} explicit Cinema (cxml::ConstNodePtr); @@ -61,33 +59,14 @@ public: void add_screen (std::shared_ptr<dcpomatic::Screen>); void remove_screen (std::shared_ptr<dcpomatic::Screen>); - void set_utc_offset_hour (int h); - void set_utc_offset_minute (int m); - std::string name; std::vector<std::string> emails; std::string notes; - int utc_offset_hour () const { - return _utc_offset_hour; - } - - int utc_offset_minute () const { - return _utc_offset_minute; - } - std::vector<std::shared_ptr<dcpomatic::Screen>> screens() const { return _screens; } private: std::vector<std::shared_ptr<dcpomatic::Screen>> _screens; - /** Offset such that the equivalent time in UTC can be determined - by subtracting the offset from the local time. - */ - int _utc_offset_hour; - /** Additional minutes to add to _utc_offset_hour if _utc_offset_hour is - positive, or to subtract if _utc_offset_hour is negative. - */ - int _utc_offset_minute; }; diff --git a/src/lib/config.cc b/src/lib/config.cc index 384db5cde..8dce6237a 100644 --- a/src/lib/config.cc +++ b/src/lib/config.cc @@ -220,6 +220,10 @@ Config::set_defaults () set_notification_email_to_default (); set_cover_sheet_to_default (); +#ifdef DCPOMATIC_GROK + _grok = boost::none; +#endif + _main_divider_sash_position = {}; _main_content_divider_sash_position = {}; @@ -494,7 +498,7 @@ try of the nags. */ for (auto i: f.node_children("Nagged")) { - auto const id = i->number_attribute<int>("Id"); + auto const id = number_attribute<int>(i, "Id", "id"); if (id >= 0 && id < NAG_COUNT) { _nagged[id] = raw_convert<int>(i->content()); } @@ -566,7 +570,7 @@ try _default_notify = f.optional_bool_child("DefaultNotify").get_value_or(false); for (auto i: f.node_children("Notification")) { - int const id = i->number_attribute<int>("Id"); + int const id = number_attribute<int>(i, "Id", "id"); if (id >= 0 && id < NOTIFICATION_COUNT) { _notification[id] = raw_convert<int>(i->content()); } @@ -642,6 +646,12 @@ try _allow_smpte_bv20 = f.optional_bool_child("AllowSMPTEBv20").get_value_or(false); _isdcf_name_part_length = f.optional_number_child<int>("ISDCFNamePartLength").get_value_or(14); +#ifdef DCPOMATIC_GROK + if (auto grok = f.optional_node_child("Grok")) { + _grok = Grok(grok); + } +#endif + _export.read(f.optional_node_child("Export")); } catch (...) { @@ -964,7 +974,7 @@ Config::write_config () const /* [XML] Nagged 1 if a particular nag screen has been shown and should not be shown again, otherwise 0. */ for (int i = 0; i < NAG_COUNT; ++i) { xmlpp::Element* e = root->add_child ("Nagged"); - e->set_attribute ("Id", raw_convert<string>(i)); + e->set_attribute("id", raw_convert<string>(i)); e->add_child_text (_nagged[i] ? "1" : "0"); } /* [XML] PreviewSound 1 to use sound in the GUI preview and player, otherwise 0. */ @@ -1019,7 +1029,7 @@ Config::write_config () const /* [XML] Notification 1 if a notification type is enabled, otherwise 0. */ for (int i = 0; i < NOTIFICATION_COUNT; ++i) { xmlpp::Element* e = root->add_child ("Notification"); - e->set_attribute ("Id", raw_convert<string>(i)); + e->set_attribute("id", raw_convert<string>(i)); e->add_child_text (_notification[i] ? "1" : "0"); } @@ -1128,6 +1138,12 @@ Config::write_config () const /* [XML] ISDCFNamePartLength Maximum length of the "name" part of an ISDCF name, which should be 14 according to the standard */ root->add_child("ISDCFNamePartLength")->add_child_text(raw_convert<string>(_isdcf_name_part_length)); +#ifdef DCPOMATIC_GROK + if (_grok) { + _grok->as_xml(root->add_child("Grok")); + } +#endif + _export.write(root->add_child("Export")); auto target = config_write_file(); @@ -1685,3 +1701,38 @@ Config::initial_path(string id) const return iter->second; } + +#ifdef DCPOMATIC_GROK + +Config::Grok::Grok(cxml::ConstNodePtr node) + : enable(node->bool_child("Enable")) + , binary_location(node->string_child("BinaryLocation")) + , selected(node->number_child<int>("Selected")) + , licence_server(node->string_child("LicenceServer")) + , licence_port(node->number_child<int>("LicencePort")) + , licence(node->string_child("Licence")) +{ + +} + + +void +Config::Grok::as_xml(xmlpp::Element* node) const +{ + node->add_child("BinaryLocation")->add_child_text(binary_location.string()); + node->add_child("Enable")->add_child_text((enable ? "1" : "0")); + node->add_child("Selected")->add_child_text(raw_convert<string>(selected)); + node->add_child("LicenceServer")->add_child_text(licence_server); + node->add_child("LicencePort")->add_child_text(raw_convert<string>(licence_port)); + node->add_child("Licence")->add_child_text(licence); +} + + +void +Config::set_grok(Grok const& grok) +{ + _grok = grok; + changed(GROK); +} + +#endif diff --git a/src/lib/config.h b/src/lib/config.h index f3d080b0b..74b4316d2 100644 --- a/src/lib/config.h +++ b/src/lib/config.h @@ -97,6 +97,9 @@ public: AUTO_CROP_THRESHOLD, ALLOW_SMPTE_BV20, ISDCF_NAME_PART_LENGTH, +#ifdef DCPOMATIC_GROK + GROK, +#endif OTHER }; @@ -621,6 +624,28 @@ public: return _allow_smpte_bv20; } +#ifdef DCPOMATIC_GROK + class Grok + { + public: + Grok() = default; + Grok(cxml::ConstNodePtr node); + + void as_xml(xmlpp::Element* node) const; + + bool enable = false; + boost::filesystem::path binary_location; + int selected = 0; + std::string licence_server; + int licence_port = 5000; + std::string licence; + }; + + boost::optional<Grok> grok() const { + return _grok; + } +#endif + int isdcf_name_part_length() const { return _isdcf_name_part_length; } @@ -1202,10 +1227,15 @@ public: maybe_set(_allow_smpte_bv20, allow, ALLOW_SMPTE_BV20); } +#ifdef DCPOMATIC_GROK + void set_grok(Grok const& grok); +#endif + void set_isdcf_name_part_length(int length) { maybe_set(_isdcf_name_part_length, length, ISDCF_NAME_PART_LENGTH); } + void changed (Property p = OTHER); boost::signals2::signal<void (Property)> Changed; /** Emitted if read() failed on an existing Config file. There is nothing @@ -1447,6 +1477,10 @@ private: bool _allow_smpte_bv20; int _isdcf_name_part_length; +#ifdef DCPOMATIC_GROK + boost::optional<Grok> _grok; +#endif + ExportConfig _export; static int const _current_version; diff --git a/src/lib/content_video.h b/src/lib/content_video.h index 4fdab717a..1c145f602 100644 --- a/src/lib/content_video.h +++ b/src/lib/content_video.h @@ -23,6 +23,7 @@ #define DCPOMATIC_CONTENT_VIDEO_H +#include "dcpomatic_time.h" #include "types.h" @@ -36,22 +37,22 @@ class ContentVideo { public: ContentVideo () - : frame (0) - , eyes (Eyes::LEFT) + : eyes (Eyes::LEFT) , part (Part::WHOLE) {} - ContentVideo (std::shared_ptr<const ImageProxy> i, Frame f, Eyes e, Part p) + ContentVideo (std::shared_ptr<const ImageProxy> i, dcpomatic::ContentTime t, Eyes e, Part p) : image (i) - , frame (f) + , time (t) , eyes (e) , part (p) {} std::shared_ptr<const ImageProxy> image; - Frame frame; + dcpomatic::ContentTime time; Eyes eyes; Part part; }; + #endif diff --git a/src/lib/cpu_j2k_encoder_thread.cc b/src/lib/cpu_j2k_encoder_thread.cc new file mode 100644 index 000000000..580facae9 --- /dev/null +++ b/src/lib/cpu_j2k_encoder_thread.cc @@ -0,0 +1,62 @@ +/* + Copyright (C) 2023 Carl Hetherington <cth@carlh.net> + + This file is part of DCP-o-matic. + + DCP-o-matic is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + DCP-o-matic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with DCP-o-matic. If not, see <http://www.gnu.org/licenses/>. + +*/ + + +#include "cpu_j2k_encoder_thread.h" +#include "cross.h" +#include "dcpomatic_log.h" +#include "dcp_video.h" +#include "j2k_encoder.h" +#include "util.h" + +#include "i18n.h" + + +using std::make_shared; +using std::shared_ptr; + + +CPUJ2KEncoderThread::CPUJ2KEncoderThread(J2KEncoder& encoder) + : J2KSyncEncoderThread(encoder) +{ + +} + + +void +CPUJ2KEncoderThread::log_thread_start() const +{ + start_of_thread("CPUJ2KEncoder"); + LOG_TIMING("start-encoder-thread thread=%1 server=localhost", thread_id()); +} + + +shared_ptr<dcp::ArrayData> +CPUJ2KEncoderThread::encode(DCPVideo const& frame) +{ + try { + return make_shared<dcp::ArrayData>(frame.encode_locally()); + } catch (std::exception& e) { + LOG_ERROR(N_("Local encode failed (%1)"), e.what()); + } + + return {}; +} + diff --git a/src/lib/cpu_j2k_encoder_thread.h b/src/lib/cpu_j2k_encoder_thread.h new file mode 100644 index 000000000..fb138f484 --- /dev/null +++ b/src/lib/cpu_j2k_encoder_thread.h @@ -0,0 +1,16 @@ +#include "j2k_sync_encoder_thread.h" +#include <dcp/data.h> + + +class DCPVideo; + + +class CPUJ2KEncoderThread : public J2KSyncEncoderThread +{ +public: + CPUJ2KEncoderThread(J2KEncoder& encoder); + + void log_thread_start() const override; + std::shared_ptr<dcp::ArrayData> encode(DCPVideo const& frame) override; +}; + diff --git a/src/lib/cross.h b/src/lib/cross.h index 6904811b7..150199561 100644 --- a/src/lib/cross.h +++ b/src/lib/cross.h @@ -138,29 +138,19 @@ private: void disk_write_finished (); -struct OSXMediaPath -{ - bool real; ///< true for a "real" disk, false for a synthesized APFS one - std::vector<std::string> parts; ///< parts of the media path after the : -}; - - struct OSXDisk { std::string device; boost::optional<std::string> vendor; boost::optional<std::string> model; - OSXMediaPath media_path; - bool whole; std::vector<boost::filesystem::path> mount_points; unsigned long size; + bool system; + bool writeable; + bool partition; }; -boost::optional<OSXMediaPath> analyse_osx_media_path (std::string path); -std::vector<Drive> osx_disks_to_drives (std::vector<OSXDisk> disks); - - class ArgFixer { public: diff --git a/src/lib/cross_common.cc b/src/lib/cross_common.cc index b4d322096..b8f1d48f1 100644 --- a/src/lib/cross_common.cc +++ b/src/lib/cross_common.cc @@ -40,9 +40,6 @@ using std::vector; using boost::optional; -auto constexpr MEDIA_PATH_REQUIRED_MATCHES = 3; - - Drive::Drive (string xml) { cxml::Document doc; @@ -122,97 +119,3 @@ Drive::log_summary () const ); } - - -/* This is in _common so we can use it in unit tests */ -optional<OSXMediaPath> -analyse_osx_media_path (string path) -{ - if (path.find("/IOHDIXController") != string::npos) { - /* This is a disk image, so we completely ignore it */ - LOG_DISK_NC("Ignoring this as it seems to be a disk image"); - return {}; - } - - OSXMediaPath mp; - vector<string> parts; - split(parts, path, boost::is_any_of("/")); - std::copy(parts.begin() + 1, parts.end(), back_inserter(mp.parts)); - - if (!parts.empty() && parts[0] == "IODeviceTree:") { - mp.real = true; - if (mp.parts.size() < MEDIA_PATH_REQUIRED_MATCHES) { - /* Later we expect at least MEDIA_PATH_REQUIRED_MATCHES parts in a IODeviceTree */ - LOG_DISK_NC("Ignoring this as it has a strange media path"); - return {}; - } - } else if (!parts.empty() && parts[0] == "IOService:") { - mp.real = false; - } else { - return {}; - } - - return mp; -} - - -/* Take some OSXDisk objects, representing disks that `DARegisterDiskAppearedCallback` told us about, - * and find those drives that we could write a DCP to. The drives returned are "real" (not synthesized) - * and are whole disks (not partitions). They may be mounted, or contain mounted partitions, in which - * their mounted() method will return true. - */ -vector<Drive> -osx_disks_to_drives (vector<OSXDisk> disks) -{ - using namespace boost::algorithm; - - /* Mark disks containing mounted partitions as themselves mounted */ - for (auto& i: disks) { - if (!i.whole) { - continue; - } - for (auto& j: disks) { - if (&i != &j && !j.mount_points.empty() && starts_with(j.device, i.device)) { - LOG_DISK("Marking %1 as mounted because %2 is", i.device, j.device); - std::copy(j.mount_points.begin(), j.mount_points.end(), back_inserter(i.mount_points)); - } - } - } - - /* Mark containers of mounted synths as themselves mounted */ - for (auto& i: disks) { - if (i.media_path.real) { - for (auto& j: disks) { - if (!j.media_path.real && !j.mount_points.empty()) { - /* i is real, j is a mounted synth; if we see the first MEDIA_PATH_REQUIRED_MATCHES parts - * of i anywhere in j we assume they are related and so i shares j's mount points. - */ - bool one_missing = false; - string all_parts; - DCPOMATIC_ASSERT (i.media_path.parts.size() >= MEDIA_PATH_REQUIRED_MATCHES); - for (auto k = 0; k < MEDIA_PATH_REQUIRED_MATCHES; ++k) { - if (find(j.media_path.parts.begin(), j.media_path.parts.end(), i.media_path.parts[k]) == j.media_path.parts.end()) { - one_missing = true; - } - all_parts += i.media_path.parts[k] + " "; - } - - if (!one_missing) { - LOG_DISK("Marking %1 as mounted because %2 is (found %3)", i.device, j.device, all_parts); - std::copy(j.mount_points.begin(), j.mount_points.end(), back_inserter(i.mount_points)); - } - } - } - } - } - - vector<Drive> drives; - for (auto const& i: disks) { - if (i.whole && i.media_path.real) { - drives.push_back(Drive(i.device, i.mount_points, i.size, i.vendor, i.model)); - LOG_DISK_NC(drives.back().log_summary()); - } - } - - return drives; -} diff --git a/src/lib/cross_osx.cc b/src/lib/cross_osx.cc index 913b19103..20fe9bce8 100644 --- a/src/lib/cross_osx.cc +++ b/src/lib/cross_osx.cc @@ -240,44 +240,6 @@ get_model (CFDictionaryRef& description) } -static optional<OSXMediaPath> -analyse_media_path (CFDictionaryRef& description) -{ - using namespace boost::algorithm; - - void const* str = CFDictionaryGetValue (description, kDADiskDescriptionMediaPathKey); - if (!str) { - LOG_DISK_NC("There is no MediaPathKey (no dictionary value)"); - return {}; - } - - auto path_key_cstr = CFStringGetCStringPtr((CFStringRef) str, kCFStringEncodingUTF8); - if (!path_key_cstr) { - LOG_DISK_NC("There is no MediaPathKey (no cstring)"); - return {}; - } - - string path(path_key_cstr); - LOG_DISK("MediaPathKey is %1", path); - return analyse_osx_media_path (path); -} - - -static bool -is_whole_drive (DADiskRef& disk) -{ - io_service_t service = DADiskCopyIOMedia (disk); - CFTypeRef whole_media_ref = IORegistryEntryCreateCFProperty (service, CFSTR(kIOMediaWholeKey), kCFAllocatorDefault, 0); - bool whole_media = false; - if (whole_media_ref) { - whole_media = CFBooleanGetValue((CFBooleanRef) whole_media_ref); - CFRelease (whole_media_ref); - } - IOObjectRelease (service); - return whole_media; -} - - static optional<boost::filesystem::path> mount_point (CFDictionaryRef& description) { @@ -294,29 +256,17 @@ mount_point (CFDictionaryRef& description) } -/* Here follows some rather intricate and (probably) fragile code to find the list of available - * "real" drives on macOS that we might want to write a DCP to. - * - * We use the Disk Arbitration framework to give us a series of mount_points (/dev/disk0, /dev/disk1, - * /dev/disk1s1 and so on) and we use the API to gather useful information about these mount_points into - * a vector of Disk structs. - * - * Then we read the Disks that we found and try to derive a list of drives that we should offer to the - * user, with details of whether those drives are currently mounted or not. - * - * At the basic level we find the "disk"-level mount_points, looking at whether any of their partitions are mounted. - * - * This is complicated enormously by recent-ish macOS versions' habit of making `synthesized' volumes which - * reflect data in `real' partitions. So, for example, we might have a real (physical) drive /dev/disk2 with - * a partition /dev/disk2s2 whose content is made into a synthesized /dev/disk3, itself containing some partitions - * which are mounted. /dev/disk2s2 is not considered to be mounted, in this case. So we need to know that - * disk2s2 is related to disk3 so we can consider disk2s2 as mounted if any parts of disk3 are. In order to do - * this I am taking the first two parts of the IODeviceTree and seeing if they exist anywhere in a - * IOService identifier. If they do, I am assuming the IOService device is on the matching IODeviceTree device. - * - * Lots of this is guesswork and may be broken. In my defence the documentation that I have been able to - * unearth is, to put it impolitely, crap. - */ +static bool +get_bool(CFDictionaryRef& description, void const* key) +{ + auto value = CFDictionaryGetValue(description, key); + if (!value) { + return false; + } + + return CFBooleanGetValue(reinterpret_cast<CFBooleanRef>(value)); +} + static void disk_appeared (DADiskRef disk, void* context) @@ -339,32 +289,30 @@ disk_appeared (DADiskRef disk, void* context) this_disk.model = get_model (description); LOG_DISK("Vendor/model: %1 %2", this_disk.vendor.get_value_or("[none]"), this_disk.model.get_value_or("[none]")); - auto media_path = analyse_media_path (description); - if (!media_path) { - LOG_DISK("Finding media path for %1 failed", bsd_name); - return; - } - - this_disk.media_path = *media_path; - this_disk.whole = is_whole_drive (disk); auto mp = mount_point (description); if (mp) { this_disk.mount_points.push_back (*mp); } - LOG_DISK( - "%1 %2 mounted at %3", - this_disk.media_path.real ? "Real" : "Synth", - this_disk.whole ? "whole" : "part", - mp ? mp->string() : "[nowhere]" - ); - auto media_size_cstr = CFDictionaryGetValue (description, kDADiskDescriptionMediaSizeKey); if (!media_size_cstr) { LOG_DISK_NC("Could not read media size"); return; } + this_disk.system = get_bool(description, kDADiskDescriptionDeviceInternalKey) && !get_bool(description, kDADiskDescriptionMediaRemovableKey); + this_disk.writeable = get_bool(description, kDADiskDescriptionMediaWritableKey); + this_disk.partition = string(bsd_name).find("s", 5) != std::string::npos; + + LOG_DISK( + "%1 %2 %3 %4 mounted at %5", + bsd_name, + this_disk.system ? "system" : "non-system", + this_disk.writeable ? "writeable" : "read-only", + this_disk.partition ? "partition" : "drive", + mp ? mp->string() : "[nowhere]" + ); + CFNumberGetValue ((CFNumberRef) media_size_cstr, kCFNumberLongType, &this_disk.size); CFRelease (description); @@ -395,7 +343,12 @@ Drive::get () DAUnregisterCallback(session, (void *) disk_appeared, &disks); CFRelease(session); - auto drives = osx_disks_to_drives(disks); + vector<Drive> drives; + for (auto const& disk: disks) { + if (!disk.system && !disk.partition && disk.writeable) { + drives.push_back({disk.device, disk.mount_points, disk.size, disk.vendor, disk.model}); + } + } LOG_DISK("Drive::get() found %1 drives:", drives.size()); for (auto const& drive: drives) { diff --git a/src/lib/dcp_content.cc b/src/lib/dcp_content.cc index 6185b3a19..378ba1882 100644 --- a/src/lib/dcp_content.cc +++ b/src/lib/dcp_content.cc @@ -612,7 +612,7 @@ DCPContent::reel_split_points (shared_ptr<const Film> film) const } bool -DCPContent::can_reference (shared_ptr<const Film> film, function<bool (shared_ptr<const Content>)> part, string overlapping, string& why_not) const +DCPContent::can_reference_anything(shared_ptr<const Film> film, string& why_not) const { /* We must be using the same standard as the film */ if (_standard) { @@ -658,15 +658,16 @@ DCPContent::can_reference (shared_ptr<const Film> film, function<bool (shared_pt } } - auto a = overlaps (film, film->content(), part, position(), end(film)); - if (a.size() != 1 || a.front().get() != this) { - why_not = overlapping; - return false; - } - return true; } +bool +DCPContent::overlaps(shared_ptr<const Film> film, function<bool (shared_ptr<const Content>)> part) const +{ + auto const a = dcpomatic::overlaps(film, film->content(), part, position(), end(film)); + return a.size() != 1 || a.front().get() != this; +} + bool DCPContent::can_reference_video (shared_ptr<const Film> film, string& why_not) const @@ -691,15 +692,17 @@ DCPContent::can_reference_video (shared_ptr<const Film> film, string& why_not) c return false; } - /// TRANSLATORS: this string will follow "Cannot reference this DCP: " - return can_reference( - film, - [](shared_ptr<const Content> c) { - return static_cast<bool>(c->video) && c->video->use(); - }, - _("it overlaps other video content; remove the other content."), - why_not - ); + auto part = [](shared_ptr<const Content> c) { + return static_cast<bool>(c->video) && c->video->use(); + }; + + if (overlaps(film, part)) { + /// TRANSLATORS: this string will follow "Cannot reference this DCP: " + why_not = _("it overlaps other video content."); + return false; + } + + return can_reference_anything(film, why_not); } @@ -720,14 +723,17 @@ DCPContent::can_reference_audio (shared_ptr<const Film> film, string& why_not) c return false; } - /// TRANSLATORS: this string will follow "Cannot reference this DCP: " - return can_reference( - film, [](shared_ptr<const Content> c) { - return static_cast<bool>(c->audio) && !c->audio->mapping().mapped_output_channels().empty(); - }, - _("it overlaps other audio content; remove the other content."), - why_not - ); + auto part = [](shared_ptr<const Content> c) { + return static_cast<bool>(c->audio) && !c->audio->mapping().mapped_output_channels().empty(); + }; + + if (overlaps(film, part)) { + /// TRANSLATORS: this string will follow "Cannot reference this DCP: " + why_not = _("it overlaps other audio content."); + return false; + } + + return can_reference_anything(film, why_not); } @@ -773,15 +779,17 @@ DCPContent::can_reference_text (shared_ptr<const Film> film, TextType type, stri return false; } - /// TRANSLATORS: this string will follow "Cannot reference this DCP: " - return can_reference( - film, - [type](shared_ptr<const Content> c) { - return std::find_if(c->text.begin(), c->text.end(), [type](shared_ptr<const TextContent> t) { return t->type() == type; }) != c->text.end(); - }, - _("they overlap other text content; remove the other content."), - why_not - ); + auto part = [type](shared_ptr<const Content> c) { + return std::find_if(c->text.begin(), c->text.end(), [type](shared_ptr<const TextContent> t) { return t->type() == type; }) != c->text.end(); + }; + + if (overlaps(film, part)) { + /// TRANSLATORS: this string will follow "Cannot reference this DCP: " + why_not = _("it overlaps other text content."); + return false; + } + + return can_reference_anything(film, why_not); } void diff --git a/src/lib/dcp_content.h b/src/lib/dcp_content.h index 3753740a2..d06d46523 100644 --- a/src/lib/dcp_content.h +++ b/src/lib/dcp_content.h @@ -100,6 +100,8 @@ public: bool needs_kdm () const; bool needs_assets () const; + bool can_reference_anything(std::shared_ptr<const Film> film, std::string& why_not) const; + void set_reference_video (bool r); bool reference_video () const { @@ -186,12 +188,7 @@ private: void read_directory (boost::filesystem::path); void read_sub_directory (boost::filesystem::path); std::list<dcpomatic::DCPTimePeriod> reels (std::shared_ptr<const Film> film) const; - bool can_reference ( - std::shared_ptr<const Film> film, - std::function <bool (std::shared_ptr<const Content>)>, - std::string overlapping, - std::string& why_not - ) const; + bool overlaps(std::shared_ptr<const Film> film, std::function<bool (std::shared_ptr<const Content>)> part) const; std::string _name; /** true if our DCP is encrypted */ diff --git a/src/lib/dcp_decoder.cc b/src/lib/dcp_decoder.cc index 165b5bfb5..303126caa 100644 --- a/src/lib/dcp_decoder.cc +++ b/src/lib/dcp_decoder.cc @@ -180,7 +180,7 @@ DCPDecoder::pass () AV_PIX_FMT_XYZ12LE, _forced_reduction ), - _offset + frame + ContentTime::from_frames(_offset + frame, vfr) ); } else { video->emit ( @@ -192,7 +192,7 @@ DCPDecoder::pass () AV_PIX_FMT_XYZ12LE, _forced_reduction ), - _offset + frame + ContentTime::from_frames(_offset + frame, vfr) ); video->emit ( @@ -204,7 +204,7 @@ DCPDecoder::pass () AV_PIX_FMT_XYZ12LE, _forced_reduction ), - _offset + frame + ContentTime::from_frames(_offset + frame, vfr) ); } } diff --git a/src/lib/dcp_encoder.cc b/src/lib/dcp_encoder.cc index 9a840c8ab..bd78312fa 100644 --- a/src/lib/dcp_encoder.cc +++ b/src/lib/dcp_encoder.cc @@ -18,6 +18,7 @@ */ + /** @file src/dcp_encoder.cc * @brief A class which takes a Film and some Options, then uses those to encode the film into a DCP. * @@ -25,31 +26,33 @@ * as a parameter to the constructor. */ + +#include "audio_decoder.h" +#include "compose.hpp" #include "dcp_encoder.h" -#include "j2k_encoder.h" #include "film.h" -#include "video_decoder.h" -#include "audio_decoder.h" -#include "player.h" +#include "j2k_encoder.h" #include "job.h" -#include "writer.h" -#include "compose.hpp" +#include "player.h" +#include "player_video.h" #include "referenced_reel_asset.h" #include "text_content.h" -#include "player_video.h" +#include "video_decoder.h" +#include "writer.h" #include <boost/signals2.hpp> #include <iostream> #include "i18n.h" -using std::string; + using std::cout; +using std::dynamic_pointer_cast; using std::list; -using std::vector; +using std::make_shared; using std::shared_ptr; +using std::string; +using std::vector; using std::weak_ptr; -using std::dynamic_pointer_cast; -using std::make_shared; using boost::optional; #if BOOST_VERSION >= 106100 using namespace boost::placeholders; @@ -118,6 +121,20 @@ DCPEncoder::go () _writer.finish(_film->dir(_film->dcp_name())); } + +void +DCPEncoder::pause() +{ + _j2k_encoder.pause(); +} + + +void +DCPEncoder::resume() +{ + _j2k_encoder.resume(); +} + void DCPEncoder::video (shared_ptr<PlayerVideo> data, DCPTime time) { diff --git a/src/lib/dcp_encoder.h b/src/lib/dcp_encoder.h index ad77f6951..ce0b72204 100644 --- a/src/lib/dcp_encoder.h +++ b/src/lib/dcp_encoder.h @@ -35,6 +35,8 @@ class Job; class Player; class PlayerVideo; +struct frames_not_lost_when_threads_disappear; + /** @class DCPEncoder */ class DCPEncoder : public Encoder @@ -53,8 +55,13 @@ public: return _finishing; } + void pause() override; + void resume() override; + private: + friend struct ::frames_not_lost_when_threads_disappear; + void video (std::shared_ptr<PlayerVideo>, dcpomatic::DCPTime); void audio (std::shared_ptr<AudioBuffers>, dcpomatic::DCPTime); void text (PlayerText, TextType, boost::optional<DCPTextTrack>, dcpomatic::DCPTimePeriod); diff --git a/src/lib/dcp_video.cc b/src/lib/dcp_video.cc index 217b72183..3769a3285 100644 --- a/src/lib/dcp_video.cc +++ b/src/lib/dcp_video.cc @@ -117,6 +117,30 @@ DCPVideo::convert_to_xyz (shared_ptr<const PlayerVideo> frame) return xyz; } +dcp::Size +DCPVideo::get_size() const +{ + auto image = _frame->image(bind(&PlayerVideo::keep_xyz_or_rgb, _1), VideoRange::FULL, false); + return image->size(); +} + + +void +DCPVideo::convert_to_xyz(uint16_t* dst) const +{ + auto image = _frame->image(bind(&PlayerVideo::keep_xyz_or_rgb, _1), VideoRange::FULL, false); + if (_frame->colour_conversion()) { + dcp::rgb_to_xyz ( + image->data()[0], + dst, + image->size(), + image->stride()[0], + _frame->colour_conversion().get() + ); + } +} + + /** J2K-encode this frame on the local host. * @return Encoded data. */ diff --git a/src/lib/dcp_video.h b/src/lib/dcp_video.h index bf95ccfe6..d07c8322b 100644 --- a/src/lib/dcp_video.h +++ b/src/lib/dcp_video.h @@ -17,6 +17,8 @@ along with DCP-o-matic. If not, see <http://www.gnu.org/licenses/>. */ +#ifndef DCPOMATIC_DCP_VIDEO_H +#define DCPOMATIC_DCP_VIDEO_H #include "encode_server_description.h" @@ -66,6 +68,9 @@ public: static std::shared_ptr<dcp::OpenJPEGImage> convert_to_xyz(std::shared_ptr<const PlayerVideo> frame); + void convert_to_xyz(uint16_t* dst) const; + dcp::Size get_size() const; + private: void add_metadata (xmlpp::Element *) const; @@ -76,3 +81,5 @@ private: int _j2k_bandwidth; ///< J2K bandwidth to use Resolution _resolution; ///< Resolution (2K or 4K) }; + +#endif diff --git a/src/lib/dcpomatic_time.h b/src/lib/dcpomatic_time.h index 1b12ea901..9ebb334fe 100644 --- a/src/lib/dcpomatic_time.h +++ b/src/lib/dcpomatic_time.h @@ -152,6 +152,10 @@ public: return *this; } + Time<S, O> operator* (int o) const { + return Time<S, O> (_t * o); + } + Time<S, O> operator/ (int o) const { return Time<S, O> (_t / o); } diff --git a/src/lib/dkdm_recipient.cc b/src/lib/dkdm_recipient.cc index c73379bed..8f3bfea2e 100644 --- a/src/lib/dkdm_recipient.cc +++ b/src/lib/dkdm_recipient.cc @@ -24,7 +24,6 @@ #include "film.h" #include "kdm_with_metadata.h" #include <dcp/raw_convert.h> -#include <dcp/utc_offset.h> using std::make_shared; @@ -40,9 +39,6 @@ DKDMRecipient::DKDMRecipient (cxml::ConstNodePtr node) for (auto i: node->node_children("Email")) { emails.push_back (i->content()); } - - utc_offset_hour = node->number_child<int>("UTCOffsetHour"); - utc_offset_minute = node->number_child<int>("UTCOffsetMinute"); } @@ -54,9 +50,6 @@ DKDMRecipient::as_xml (xmlpp::Element* node) const for (auto i: emails) { node->add_child("Email")->add_child_text(i); } - - node->add_child("UTCOffsetHour")->add_child_text(raw_convert<string>(utc_offset_hour)); - node->add_child("UTCOffsetMinute")->add_child_text(raw_convert<string>(utc_offset_minute)); } @@ -65,29 +58,26 @@ kdm_for_dkdm_recipient ( shared_ptr<const Film> film, boost::filesystem::path cpl, shared_ptr<DKDMRecipient> recipient, - boost::posix_time::ptime valid_from, - boost::posix_time::ptime valid_to + dcp::LocalTime valid_from, + dcp::LocalTime valid_to ) { if (!recipient->recipient) { return {}; } - dcp::LocalTime const begin(valid_from, dcp::UTCOffset(recipient->utc_offset_hour, recipient->utc_offset_minute)); - dcp::LocalTime const end (valid_to, dcp::UTCOffset(recipient->utc_offset_hour, recipient->utc_offset_minute)); - auto signer = Config::instance()->signer_chain(); if (!signer->valid()) { throw InvalidSignerError(); } - auto const decrypted_kdm = film->make_kdm(cpl, begin, end); + auto const decrypted_kdm = film->make_kdm(cpl, valid_from, valid_to); auto const kdm = decrypted_kdm.encrypt(signer, recipient->recipient.get(), {}, dcp::Formulation::MODIFIED_TRANSITIONAL_1, true, 0); dcp::NameFormat::Map name_values; name_values['f'] = kdm.content_title_text(); - name_values['b'] = begin.date() + " " + begin.time_of_day(true, false); - name_values['e'] = end.date() + " " + end.time_of_day(true, false); + name_values['b'] = valid_from.date() + " " + valid_from.time_of_day(true, false); + name_values['e'] = valid_to.date() + " " + valid_to.time_of_day(true, false); name_values['i'] = kdm.cpl_id(); return make_shared<KDMWithMetadata>(name_values, nullptr, recipient->emails, kdm); diff --git a/src/lib/dkdm_recipient.h b/src/lib/dkdm_recipient.h index 7a0fa0185..3317ae6f9 100644 --- a/src/lib/dkdm_recipient.h +++ b/src/lib/dkdm_recipient.h @@ -33,14 +33,10 @@ public: std::string const& name_, std::string const& notes_, boost::optional<dcp::Certificate> recipient_, - std::vector<std::string> emails_, - int utc_offset_hour_, - int utc_offset_minute_ + std::vector<std::string> emails_ ) : KDMRecipient (name_, notes_, recipient_, boost::none) , emails (emails_) - , utc_offset_hour (utc_offset_hour_) - , utc_offset_minute (utc_offset_minute_) { } @@ -50,8 +46,6 @@ public: void as_xml (xmlpp::Element *) const override; std::vector<std::string> emails; - int utc_offset_hour; - int utc_offset_minute; }; @@ -60,7 +54,7 @@ kdm_for_dkdm_recipient ( std::shared_ptr<const Film> film, boost::filesystem::path cpl, std::shared_ptr<DKDMRecipient> recipient, - boost::posix_time::ptime valid_from, - boost::posix_time::ptime valid_to + dcp::LocalTime valid_from, + dcp::LocalTime valid_to ); diff --git a/src/lib/dkdm_wrapper.cc b/src/lib/dkdm_wrapper.cc index 016c77c3f..7beaae8f4 100644 --- a/src/lib/dkdm_wrapper.cc +++ b/src/lib/dkdm_wrapper.cc @@ -41,7 +41,11 @@ DKDMBase::read (cxml::ConstNodePtr node) if (node->name() == "DKDM") { return make_shared<DKDM>(dcp::EncryptedKDM(node->content())); } else if (node->name() == "DKDMGroup") { - auto group = make_shared<DKDMGroup>(node->string_attribute("Name")); + auto name = node->optional_string_attribute("Name"); + if (!name) { + name = node->string_attribute("name"); + } + auto group = make_shared<DKDMGroup>(*name); for (auto i: node->node_children()) { if (auto c = read(i)) { group->add (c); @@ -72,7 +76,7 @@ void DKDMGroup::as_xml (xmlpp::Element* node) const { auto f = node->add_child("DKDMGroup"); - f->set_attribute ("Name", _name); + f->set_attribute("name", _name); for (auto i: _children) { i->as_xml (f); } diff --git a/src/lib/encode_server.cc b/src/lib/encode_server.cc index 036ea58a5..7eae4375f 100644 --- a/src/lib/encode_server.cc +++ b/src/lib/encode_server.cc @@ -81,6 +81,7 @@ EncodeServer::EncodeServer (bool verbose, int num_threads) #endif , _verbose (verbose) , _num_threads (num_threads) + , _frames_encoded(0) { } @@ -169,6 +170,8 @@ EncodeServer::process (shared_ptr<Socket> socket, struct timeval& after_read, st throw; } + ++_frames_encoded; + return dcp_video_frame.index (); } diff --git a/src/lib/encode_server.h b/src/lib/encode_server.h index f93d66746..8059abd0f 100644 --- a/src/lib/encode_server.h +++ b/src/lib/encode_server.h @@ -32,6 +32,7 @@ #include "exception_store.h" #include "server.h" #include <boost/asio.hpp> +#include <boost/atomic.hpp> #include <boost/thread.hpp> #include <boost/thread/condition.hpp> #include <string> @@ -53,6 +54,10 @@ public: void run () override; + int frames_encoded() const { + return _frames_encoded; + } + private: void handle (std::shared_ptr<Socket>) override; void worker_thread (); @@ -67,6 +72,7 @@ private: bool _verbose; int _num_threads; Waker _waker; + boost::atomic<int> _frames_encoded; struct Broadcast { diff --git a/src/lib/encode_server_finder.h b/src/lib/encode_server_finder.h index f8a30af54..c478387f9 100644 --- a/src/lib/encode_server_finder.h +++ b/src/lib/encode_server_finder.h @@ -50,8 +50,6 @@ public: static EncodeServerFinder* instance (); static void drop (); - void stop (); - std::list<EncodeServerDescription> servers () const; /** Emitted whenever the list of servers changes */ @@ -62,6 +60,7 @@ private: ~EncodeServerFinder (); void start (); + void stop (); void search_thread (); void listen_thread (); diff --git a/src/lib/encoder.h b/src/lib/encoder.h index 9b67720d3..aeaf7f620 100644 --- a/src/lib/encoder.h +++ b/src/lib/encoder.h @@ -58,6 +58,8 @@ public: /** @return the number of frames that are done */ virtual Frame frames_done () const = 0; virtual bool finishing () const = 0; + virtual void pause() {} + virtual void resume() {} protected: std::shared_ptr<const Film> _film; diff --git a/src/lib/ffmpeg_decoder.cc b/src/lib/ffmpeg_decoder.cc index 7f7a07863..09db1ff1c 100644 --- a/src/lib/ffmpeg_decoder.cc +++ b/src/lib/ffmpeg_decoder.cc @@ -178,9 +178,8 @@ FFmpegDecoder::flush_fill() full_length = full_length.ceil (frc.source); if (video && !video->ignore()) { double const vfr = _ffmpeg_content->video_frame_rate().get(); - auto const f = full_length.frames_round (vfr); - auto const v = video->position(film()).get_value_or(ContentTime()).frames_round(vfr) + 1; - if (v < f) { + auto const v = video->position(film()).get_value_or(ContentTime()) + ContentTime::from_frames(1, vfr); + if (v < full_length) { video->emit(film(), make_shared<const RawImageProxy>(_black_image), v); did_something = true; } @@ -260,7 +259,7 @@ deinterleave_audio(AVFrame* frame) /* XXX: can't we use swr_convert() to do the format conversion? */ - int const channels = frame->channels; + int const channels = frame->ch_layout.nb_channels; int const frames = frame->nb_samples; int const total_samples = frames * channels; auto audio = make_shared<AudioBuffers>(channels, frames); @@ -622,7 +621,7 @@ FFmpegDecoder::process_video_frame () video->emit ( film(), make_shared<RawImageProxy>(image), - llrint(pts * _ffmpeg_content->active_video_frame_rate(film())) + ContentTime::from_seconds(pts) ); } else { LOG_WARNING_NC ("Dropping frame without PTS"); diff --git a/src/lib/ffmpeg_examiner.cc b/src/lib/ffmpeg_examiner.cc index 15cb14ad5..583ea1297 100644 --- a/src/lib/ffmpeg_examiner.cc +++ b/src/lib/ffmpeg_examiner.cc @@ -73,14 +73,6 @@ FFmpegExaminer::FFmpegExaminer (shared_ptr<const FFmpegContent> c, shared_ptr<Jo auto codec = _codec_context[i] ? _codec_context[i]->codec : nullptr; if (s->codecpar->codec_type == AVMEDIA_TYPE_AUDIO && codec) { - /* This is a hack; sometimes it seems that _audio_codec_context->channel_layout isn't set up, - so bodge it here. No idea why we should have to do this. - */ - - if (s->codecpar->channel_layout == 0) { - s->codecpar->channel_layout = av_get_default_channel_layout (s->codecpar->channels); - } - DCPOMATIC_ASSERT (_format_context->duration != AV_NOPTS_VALUE); DCPOMATIC_ASSERT (codec->name); @@ -91,7 +83,7 @@ FFmpegExaminer::FFmpegExaminer (shared_ptr<const FFmpegContent> c, shared_ptr<Jo s->id, s->codecpar->sample_rate, llrint ((double(_format_context->duration) / AV_TIME_BASE) * s->codecpar->sample_rate), - s->codecpar->channels, + s->codecpar->ch_layout.nb_channels, s->codecpar->bits_per_raw_sample ? s->codecpar->bits_per_raw_sample : s->codecpar->bits_per_coded_sample ) ); @@ -161,7 +153,7 @@ FFmpegExaminer::FFmpegExaminer (shared_ptr<const FFmpegContent> c, shared_ptr<Jo av_packet_free (&packet); - if (_first_video && got_all_audio && temporal_reference.size() >= (PULLDOWN_CHECK_FRAMES * 2)) { + if (got_all_audio && (!_video_stream || (_first_video && temporal_reference.size() >= (PULLDOWN_CHECK_FRAMES * 2)))) { /* All done */ break; } @@ -181,7 +173,6 @@ FFmpegExaminer::FFmpegExaminer (shared_ptr<const FFmpegContent> c, shared_ptr<Jo /* This code taken from get_rotation() in ffmpeg:cmdutils.c */ auto stream = _format_context->streams[*_video_stream]; auto rotate_tag = av_dict_get (stream->metadata, "rotate", 0, 0); - uint8_t* displaymatrix = av_stream_get_side_data (stream, AV_PKT_DATA_DISPLAYMATRIX, 0); _rotation = 0; if (rotate_tag && *rotate_tag->value && strcmp(rotate_tag->value, "0")) { @@ -192,8 +183,9 @@ FFmpegExaminer::FFmpegExaminer (shared_ptr<const FFmpegContent> c, shared_ptr<Jo } } - if (displaymatrix && !_rotation) { - _rotation = - av_display_rotation_get ((int32_t*) displaymatrix); + auto side_data = av_packet_side_data_get(stream->codecpar->coded_side_data, stream->codecpar->nb_coded_side_data, AV_PKT_DATA_DISPLAYMATRIX); + if (side_data && !_rotation) { + _rotation = - av_display_rotation_get(reinterpret_cast<int32_t*>(side_data->data)); } _rotation = *_rotation - 360 * floor (*_rotation / 360 + 0.9 / 360); @@ -252,7 +244,7 @@ FFmpegExaminer::video_packet (AVCodecContext* context, string& temporal_referenc ).get_value_or (ContentTime ()).frames_round (video_frame_rate().get ()); } if (temporal_reference.size() < (PULLDOWN_CHECK_FRAMES * 2)) { - temporal_reference += (_video_frame->top_field_first ? "T" : "B"); + temporal_reference += ((_video_frame->flags & AV_FRAME_FLAG_TOP_FIELD_FIRST) ? "T" : "B"); temporal_reference += (_video_frame->repeat_pict ? "3" : "2"); } diff --git a/src/lib/ffmpeg_file_encoder.cc b/src/lib/ffmpeg_file_encoder.cc index 6d1ad68f7..d7833265d 100644 --- a/src/lib/ffmpeg_file_encoder.cc +++ b/src/lib/ffmpeg_file_encoder.cc @@ -73,8 +73,7 @@ public: _codec_context->bit_rate = channels * 128 * 1024; _codec_context->sample_fmt = sample_format; _codec_context->sample_rate = frame_rate; - _codec_context->channel_layout = av_get_default_channel_layout (channels); - _codec_context->channels = channels; + av_channel_layout_default(&_codec_context->ch_layout, channels); int r = avcodec_open2 (_codec_context, _codec, 0); if (r < 0) { @@ -143,7 +142,7 @@ public: frame->nb_samples = size; frame->format = _codec_context->sample_fmt; - frame->channels = channels; + frame->ch_layout.nb_channels = channels; int r = avcodec_fill_audio_frame (frame, channels, _codec_context->sample_fmt, (const uint8_t *) samples, buffer_size, 0); DCPOMATIC_ASSERT (r >= 0); diff --git a/src/lib/film.cc b/src/lib/film.cc index d9ab6e2a3..d747efb0e 100644 --- a/src/lib/film.cc +++ b/src/lib/film.cc @@ -114,6 +114,7 @@ using namespace dcpomatic; static constexpr char metadata_file[] = "metadata.xml"; +static constexpr char ui_state_file[] = "ui.xml"; /* 5 -> 6 @@ -416,7 +417,7 @@ Film::metadata (bool with_content_paths) const root->add_child("UserExplicitVideoFrameRate")->add_child_text(_user_explicit_video_frame_rate ? "1" : "0"); for (auto const& marker: _markers) { auto m = root->add_child("Marker"); - m->set_attribute("Type", dcp::marker_to_string(marker.first)); + m->set_attribute("type", dcp::marker_to_string(marker.first)); m->add_child_text(raw_convert<string>(marker.second.get())); } for (auto i: _ratings) { @@ -603,7 +604,11 @@ Film::read_metadata (optional<boost::filesystem::path> path) _user_explicit_video_frame_rate = f.optional_bool_child("UserExplicitVideoFrameRate").get_value_or(false); for (auto i: f.node_children("Marker")) { - _markers[dcp::marker_from_string(i->string_attribute("Type"))] = DCPTime(dcp::raw_convert<DCPTime::Type>(i->content())); + auto type = i->optional_string_attribute("Type"); + if (!type) { + type = i->string_attribute("type"); + } + _markers[dcp::marker_from_string(*type)] = DCPTime(dcp::raw_convert<DCPTime::Type>(i->content())); } for (auto i: f.node_children("Rating")) { @@ -2230,3 +2235,53 @@ Film::set_territory_type(TerritoryType type) _territory_type = type; } + +void +Film::set_ui_state(string key, string value) +{ + _ui_state[key] = value; + write_ui_state(); +} + + +boost::optional<std::string> +Film::ui_state(string key) const +{ + auto iter = _ui_state.find(key); + if (iter == _ui_state.end()) { + return {}; + } + + return iter->second; +} + + +void +Film::write_ui_state() const +{ + auto doc = make_shared<xmlpp::Document>(); + auto root = doc->create_root_node("UI"); + + for (auto state: _ui_state) { + root->add_child(state.first)->add_child_text(state.second); + } + + try { + doc->write_to_file_formatted(dcp::filesystem::fix_long_path(file(ui_state_file)).string()); + } catch (...) {} +} + + +void +Film::read_ui_state() +{ + try { + cxml::Document xml("UI"); + xml.read_file(dcp::filesystem::fix_long_path(file(ui_state_file))); + for (auto node: xml.node_children()) { + if (!node->is_text()) { + _ui_state[node->name()] = node->content(); + } + } + } catch (...) {} +} diff --git a/src/lib/film.h b/src/lib/film.h index 43a41ad45..036bbed7e 100644 --- a/src/lib/film.h +++ b/src/lib/film.h @@ -434,6 +434,10 @@ public: void add_ffoc_lfoc (Markers& markers) const; + void set_ui_state(std::string key, std::string value); + boost::optional<std::string> ui_state(std::string key) const; + void read_ui_state(); + /** Emitted when some property has of the Film is about to change or has changed */ mutable boost::signals2::signal<void (ChangeType, FilmProperty)> Change; @@ -477,6 +481,7 @@ private: void check_settings_consistency (); void maybe_set_container_and_resolution (); void set_dirty (bool dirty); + void write_ui_state() const; /** Log to write to */ std::shared_ptr<Log> _log; @@ -562,6 +567,8 @@ private: */ bool _tolerant; + std::map<std::string, std::string> _ui_state; + mutable boost::mutex _info_file_mutex; boost::signals2::scoped_connection _playlist_change_connection; diff --git a/src/lib/grok/context.h b/src/lib/grok/context.h new file mode 100644 index 000000000..521faae8d --- /dev/null +++ b/src/lib/grok/context.h @@ -0,0 +1,291 @@ +/* + Copyright (C) 2023 Grok Image Compression Inc. + + This file is part of DCP-o-matic. + + DCP-o-matic is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + DCP-o-matic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with DCP-o-matic. If not, see <http://www.gnu.org/licenses/>. + +*/ + +#pragma once + + +#include "../config.h" +#include "../dcp_video.h" +#include "../film.h" +#include "../log.h" +#include "../dcpomatic_log.h" +#include "../writer.h" +#include "messenger.h" +#include <dcp/array_data.h> +#include <boost/filesystem.hpp> + + +static std::mutex launchMutex; + +namespace grk_plugin +{ + +struct GrokLogger : public MessengerLogger { + explicit GrokLogger(const std::string &preamble) : MessengerLogger(preamble) + {} + virtual ~GrokLogger() = default; + void info(const char* fmt, ...) override{ + va_list arg; + va_start(arg, fmt); + dcpomatic_log->log(preamble_ + log_message(fmt, arg),LogEntry::TYPE_GENERAL); + va_end(arg); + } + void warn(const char* fmt, ...) override{ + va_list arg; + va_start(arg, fmt); + dcpomatic_log->log(preamble_ + log_message(fmt, arg),LogEntry::TYPE_WARNING); + va_end(arg); + } + void error(const char* fmt, ...) override{ + va_list arg; + va_start(arg, fmt); + dcpomatic_log->log(preamble_ + log_message(fmt, arg),LogEntry::TYPE_ERROR); + va_end(arg); + } +}; + +struct FrameProxy { + FrameProxy(int index, Eyes eyes, DCPVideo dcpv) : index_(index), eyes_(eyes), vf(dcpv) + {} + int index() const { + return index_; + } + Eyes eyes(void) const { + return eyes_; + } + int index_; + Eyes eyes_; + DCPVideo vf; +}; + +struct DcpomaticContext +{ + DcpomaticContext( + std::shared_ptr<const Film> film_, + Writer& writer_, + EventHistory& history_, + boost::filesystem::path const& location_ + ) + : film(film_) + , writer(writer_) + , history(history_) + , location(location_) + { + + } + + void set_dimensions(uint32_t w, uint32_t h) + { + width = w; + height = h; + } + + std::shared_ptr<const Film> film; + Writer& writer; + EventHistory& history; + boost::filesystem::path location; + uint32_t width = 0; + uint32_t height = 0; +}; + + +class GrokContext +{ +public: + explicit GrokContext(DcpomaticContext* dcpomatic_context) + : _dcpomatic_context(dcpomatic_context) + { + auto grok = Config::instance()->grok().get_value_or({}); + if (!grok.enable) { + return; + } + + boost::filesystem::path folder(_dcpomatic_context->location); + boost::filesystem::path binary_path = folder / "grk_compress"; + if (!boost::filesystem::exists(binary_path)) { + getMessengerLogger()->error( + "Invalid binary location %s", _dcpomatic_context->location.c_str() + ); + return; + } + + auto proc = [this](const std::string& str) { + try { + Msg msg(str); + auto tag = msg.next(); + if (tag == GRK_MSGR_BATCH_SUBMIT_COMPRESSED) { + auto clientFrameId = msg.nextUint(); + msg.nextUint(); // compressed frame ID + auto compressedFrameLength = msg.nextUint(); + auto processor = [this](FrameProxy srcFrame, uint8_t* compressed, uint32_t compressedFrameLength) { + auto compressed_data = std::make_shared<dcp::ArrayData>(compressed, compressedFrameLength); + _dcpomatic_context->writer.write(compressed_data, srcFrame.index(), srcFrame.eyes()); + frame_done (); + }; + + int const minimum_size = 16384; + + bool needsRecompression = compressedFrameLength < minimum_size; + _messenger->processCompressed(str, processor, needsRecompression); + + if (needsRecompression) { + auto fp = _messenger->retrieve(clientFrameId); + if (!fp) { + return; + } + + auto encoded = std::make_shared<dcp::ArrayData>(fp->vf.encode_locally()); + _dcpomatic_context->writer.write(encoded, fp->vf.index(), fp->vf.eyes()); + frame_done (); + } + } + } catch (std::exception& ex) { + getMessengerLogger()->error("%s",ex.what()); + } + }; + + auto clientInit = MessengerInit( + clientToGrokMessageBuf, + clientSentSynch, + grokReceiveReadySynch, + grokToClientMessageBuf, + grokSentSynch, + clientReceiveReadySynch, + proc, + std::thread::hardware_concurrency() + ); + + _messenger = new ScheduledMessenger<FrameProxy>(clientInit); + } + + ~GrokContext() + { + shutdown(); + } + + bool launch(DCPVideo dcpv, int device) + { + namespace fs = boost::filesystem; + + if (!_messenger) { + return false; + } + if (_launched) { + return true; + } + if (_launch_failed) { + return false; + } + + std::unique_lock<std::mutex> lk_global(launchMutex); + + if (!_messenger) { + return false; + } + if (_launched) { + return true; + } + if (_launch_failed) { + return false; + } + + if (MessengerInit::firstLaunch(true)) { + + if (!fs::exists(_dcpomatic_context->location) || !fs::is_directory(_dcpomatic_context->location)) { + getMessengerLogger()->error("Invalid directory %s", _dcpomatic_context->location.c_str()); + return false; + } + + auto s = dcpv.get_size(); + _dcpomatic_context->set_dimensions(s.width, s.height); + auto grok = Config::instance()->grok().get_value_or({}); + if (!_messenger->launchGrok( + _dcpomatic_context->location, + _dcpomatic_context->width, + _dcpomatic_context->width, + _dcpomatic_context->height, + 3, + 12, + device, + _dcpomatic_context->film->resolution() == Resolution::FOUR_K, + _dcpomatic_context->film->video_frame_rate(), + _dcpomatic_context->film->j2k_bandwidth(), + grok.licence_server, + grok.licence_port, + grok.licence)) { + _launch_failed = true; + return false; + } + } + + _launched = _messenger->waitForClientInit(); + _launch_failed = _launched; + + return _launched; + } + + bool scheduleCompress(DCPVideo const& vf) + { + if (!_messenger) { + return false; + } + + auto fp = FrameProxy(vf.index(), vf.eyes(), vf); + auto cvt = [this, &fp](BufferSrc src) { + fp.vf.convert_to_xyz((uint16_t*)src.framePtr_); + }; + + return _messenger->scheduleCompress(fp, cvt); + } + + void shutdown() + { + if (!_messenger) { + return; + } + + std::unique_lock<std::mutex> lk_global(launchMutex); + + if (!_messenger) { + return; + } + + if (_launched) { + _messenger->shutdown(); + } + + delete _messenger; + _messenger = nullptr; + } + + void frame_done() + { + _dcpomatic_context->history.event(); + } + +private: + DcpomaticContext* _dcpomatic_context; + ScheduledMessenger<FrameProxy>* _messenger = nullptr; + bool _launched = false; + bool _launch_failed = false; +}; + +} + diff --git a/src/lib/grok/messenger.h b/src/lib/grok/messenger.h new file mode 100644 index 000000000..eb2fe9560 --- /dev/null +++ b/src/lib/grok/messenger.h @@ -0,0 +1,906 @@ +/* + Copyright (C) 2023 Grok Image Compression Inc. + + This file is part of DCP-o-matic. + + DCP-o-matic is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + DCP-o-matic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with DCP-o-matic. If not, see <http://www.gnu.org/licenses/>. + +*/ +#pragma once + +#include <iostream> +#include <string> +#include <cstring> +#include <atomic> +#include <functional> +#include <sstream> +#include <future> +#include <map> +#include <thread> +#include <mutex> +#include <condition_variable> +#include <queue> +#include <cassert> +#include <cstdarg> + +#ifdef _WIN32 +#include <windows.h> +#include <direct.h> +#include <tlhelp32.h> +#pragma warning(disable : 4100) +#else +#include <unistd.h> +#include <fcntl.h> +#include <sys/mman.h> +#include <semaphore.h> +#include <signal.h> +#endif + +namespace grk_plugin +{ +static std::string grokToClientMessageBuf = "Global\\grok_to_client_message"; +static std::string grokSentSynch = "Global\\grok_sent"; +static std::string clientReceiveReadySynch = "Global\\client_receive_ready"; +static std::string clientToGrokMessageBuf = "Global\\client_to_grok_message"; +static std::string clientSentSynch = "Global\\client_sent"; +static std::string grokReceiveReadySynch = "Global\\grok_receive_ready"; +static std::string grokUncompressedBuf = "Global\\grok_uncompressed_buf"; +static std::string grokCompressedBuf = "Global\\grok_compressed_buf"; +static const std::string GRK_MSGR_BATCH_IMAGE = "GRK_MSGR_BATCH_IMAGE"; +static const std::string GRK_MSGR_BATCH_COMPRESS_INIT = "GRK_MSGR_BATCH_COMPRESS_INIT"; +static const std::string GRK_MSGR_BATCH_SUBMIT_UNCOMPRESSED = "GRK_MSGR_BATCH_SUBMIT_UNCOMPRESSED"; +static const std::string GRK_MSGR_BATCH_PROCESSED_UNCOMPRESSED = + "GRK_MSGR_BATCH_PROCESSED_UNCOMPRESSED"; +static const std::string GRK_MSGR_BATCH_SUBMIT_COMPRESSED = "GRK_MSGR_BATCH_SUBMIT_COMPRESSED"; +static const std::string GRK_MSGR_BATCH_PROCESSSED_COMPRESSED = + "GRK_MSGR_BATCH_PROCESSSED_COMPRESSED"; +static const std::string GRK_MSGR_BATCH_SHUTDOWN = "GRK_MSGR_BATCH_SHUTDOWN"; +static const std::string GRK_MSGR_BATCH_FLUSH = "GRK_MSGR_BATCH_FLUSH"; +static const size_t messageBufferLen = 256; +struct IMessengerLogger +{ + virtual ~IMessengerLogger(void) = default; + virtual void info(const char* fmt, ...) = 0; + virtual void warn(const char* fmt, ...) = 0; + virtual void error(const char* fmt, ...) = 0; + + protected: + template<typename... Args> + std::string log_message(char const* const format, Args&... args) noexcept + { + constexpr size_t message_size = 512; + char message[message_size]; + + std::snprintf(message, message_size, format, args...); + return std::string(message); + } +}; +struct MessengerLogger : public IMessengerLogger +{ + explicit MessengerLogger(const std::string &preamble) : preamble_(preamble) {} + virtual ~MessengerLogger() = default; + virtual void info(const char* fmt, ...) override + { + va_list args; + std::string new_fmt = preamble_ + fmt + "\n"; + va_start(args, fmt); + vfprintf(stdout, new_fmt.c_str(), args); + va_end(args); + } + virtual void warn(const char* fmt, ...) override + { + va_list args; + std::string new_fmt = preamble_ + fmt + "\n"; + va_start(args, fmt); + vfprintf(stdout, new_fmt.c_str(), args); + va_end(args); + } + virtual void error(const char* fmt, ...) override + { + va_list args; + std::string new_fmt = preamble_ + fmt + "\n"; + va_start(args, fmt); + vfprintf(stderr, new_fmt.c_str(), args); + va_end(args); + } + + protected: + std::string preamble_; +}; + +extern IMessengerLogger* sLogger; +void setMessengerLogger(IMessengerLogger* logger); +IMessengerLogger* getMessengerLogger(void); + +struct MessengerInit +{ + MessengerInit(const std::string &outBuf, const std::string &outSent, + const std::string &outReceiveReady, const std::string &inBuf, + const std::string &inSent, + const std::string &inReceiveReady, + std::function<void(std::string)> processor, + size_t numProcessingThreads) + : outboundMessageBuf(outBuf), outboundSentSynch(outSent), + outboundReceiveReadySynch(outReceiveReady), inboundMessageBuf(inBuf), + inboundSentSynch(inSent), inboundReceiveReadySynch(inReceiveReady), processor_(processor), + numProcessingThreads_(numProcessingThreads), + uncompressedFrameSize_(0), compressedFrameSize_(0), + numFrames_(0) + { + if(firstLaunch(true)) + unlink(); + } + void unlink(void) + { +#ifndef _WIN32 + shm_unlink(grokToClientMessageBuf.c_str()); + shm_unlink(clientToGrokMessageBuf.c_str()); +#endif + } + static bool firstLaunch(bool isClient) + { + bool debugGrok = false; + return debugGrok != isClient; + } + std::string outboundMessageBuf; + std::string outboundSentSynch; + std::string outboundReceiveReadySynch; + + std::string inboundMessageBuf; + std::string inboundSentSynch; + std::string inboundReceiveReadySynch; + + std::function<void(std::string)> processor_; + size_t numProcessingThreads_; + + size_t uncompressedFrameSize_; + size_t compressedFrameSize_; + size_t numFrames_; +}; + +/*************************** Synchronization *******************************/ +enum SynchDirection +{ + SYNCH_SENT, + SYNCH_RECEIVE_READY +}; + +typedef int grk_handle; +struct Synch +{ + Synch(const std::string &sentSemName, const std::string &receiveReadySemName) + : sentSemName_(sentSemName), receiveReadySemName_(receiveReadySemName) + { + // unlink semaphores in case of previous crash + if(MessengerInit::firstLaunch(true)) + unlink(); + open(); + } + ~Synch() + { + close(); + if(MessengerInit::firstLaunch(true)) + unlink(); + } + void post(SynchDirection dir) + { + auto sem = (dir == SYNCH_SENT ? sentSem_ : receiveReadySem_); + int rc = sem_post(sem); + if(rc) + getMessengerLogger()->error("Error posting to semaphore: %s", strerror(errno)); + } + void wait(SynchDirection dir) + { + auto sem = dir == SYNCH_SENT ? sentSem_ : receiveReadySem_; + int rc = sem_wait(sem); + if(rc) + getMessengerLogger()->error("Error waiting for semaphore: %s", strerror(errno)); + } + void open(void) + { + sentSem_ = sem_open(sentSemName_.c_str(), O_CREAT, 0666, 0); + if(!sentSem_) + getMessengerLogger()->error("Error opening shared memory: %s", strerror(errno)); + receiveReadySem_ = sem_open(receiveReadySemName_.c_str(), O_CREAT, 0666, 1); + if(!receiveReadySem_) + getMessengerLogger()->error("Error opening shared memory: %s", strerror(errno)); + } + void close(void) + { + int rc = sem_close(sentSem_); + if(rc) + getMessengerLogger()->error("Error closing semaphore %s: %s", sentSemName_.c_str(), + strerror(errno)); + rc = sem_close(receiveReadySem_); + if(rc) + getMessengerLogger()->error("Error closing semaphore %s: %s", + receiveReadySemName_.c_str(), strerror(errno)); + } + void unlink(void) + { + int rc = sem_unlink(sentSemName_.c_str()); + if(rc == -1 && errno != ENOENT) + getMessengerLogger()->error("Error unlinking semaphore %s: %s", sentSemName_.c_str(), + strerror(errno)); + rc = sem_unlink(receiveReadySemName_.c_str()); + if(rc == -1 && errno != ENOENT) + getMessengerLogger()->error("Error unlinking semaphore %s: %s", + receiveReadySemName_.c_str(), strerror(errno)); + } + sem_t* sentSem_; + sem_t* receiveReadySem_; + + private: + std::string sentSemName_; + std::string receiveReadySemName_; +}; +struct SharedMemoryManager +{ + static bool initShm(const std::string &name, size_t len, grk_handle* shm_fd, char** buffer) + { + *shm_fd = shm_open(name.c_str(), O_CREAT | O_RDWR, 0666); + if(*shm_fd < 0) + { + getMessengerLogger()->error("Error opening shared memory: %s", strerror(errno)); + return false; + } + int rc = ftruncate(*shm_fd, sizeof(char) * len); + if(rc) + { + getMessengerLogger()->error("Error truncating shared memory: %s", strerror(errno)); + rc = close(*shm_fd); + if(rc) + getMessengerLogger()->error("Error closing shared memory: %s", strerror(errno)); + rc = shm_unlink(name.c_str()); + // 2 == No such file or directory + if(rc && errno != 2) + getMessengerLogger()->error("Error unlinking shared memory: %s", strerror(errno)); + return false; + } + *buffer = static_cast<char*>(mmap(0, len, PROT_WRITE, MAP_SHARED, *shm_fd, 0)); + if(!*buffer) + { + getMessengerLogger()->error("Error mapping shared memory: %s", strerror(errno)); + rc = close(*shm_fd); + if(rc) + getMessengerLogger()->error("Error closing shared memory: %s", strerror(errno)); + rc = shm_unlink(name.c_str()); + // 2 == No such file or directory + if(rc && errno != 2) + getMessengerLogger()->error("Error unlinking shared memory: %s", strerror(errno)); + } + + return *buffer != nullptr; + } + static bool deinitShm(const std::string &name, size_t len, grk_handle &shm_fd, char** buffer) + { + if (!*buffer || !shm_fd) + return true; + + int rc = munmap(*buffer, len); + *buffer = nullptr; + if(rc) + getMessengerLogger()->error("Error unmapping shared memory %s: %s", name.c_str(), strerror(errno)); + rc = close(shm_fd); + shm_fd = 0; + if(rc) + getMessengerLogger()->error("Error closing shared memory %s: %s", name.c_str(), strerror(errno)); + rc = shm_unlink(name.c_str()); + // 2 == No such file or directory + if(rc && errno != 2) + fprintf(stderr,"Error unlinking shared memory %s : %s\n", name.c_str(), strerror(errno)); + + return true; + } +}; + +template<typename Data> +class MessengerBlockingQueue +{ + public: + explicit MessengerBlockingQueue(size_t max) : active_(true), max_size_(max) {} + MessengerBlockingQueue() : MessengerBlockingQueue(UINT_MAX) {} + size_t size() const + { + return queue_.size(); + } + // deactivate and clear queue + void deactivate() + { + { + std::lock_guard<std::mutex> lk(mutex_); + active_ = false; + while(!queue_.empty()) + queue_.pop(); + } + + // release all waiting threads + can_pop_.notify_all(); + can_push_.notify_all(); + } + void activate() + { + std::lock_guard<std::mutex> lk(mutex_); + active_ = true; + } + bool push(Data const& value) + { + bool rc; + { + std::unique_lock<std::mutex> lk(mutex_); + rc = push_(value); + } + if(rc) + can_pop_.notify_one(); + + return rc; + } + bool waitAndPush(Data& value) + { + bool rc; + { + std::unique_lock<std::mutex> lk(mutex_); + if(!active_) + return false; + // in case of spurious wakeup, loop until predicate in lambda + // is satisfied. + can_push_.wait(lk, [this] { return queue_.size() < max_size_ || !active_; }); + rc = push_(value); + } + if(rc) + can_pop_.notify_one(); + + return rc; + } + bool pop(Data& value) + { + bool rc; + { + std::unique_lock<std::mutex> lk(mutex_); + rc = pop_(value); + } + if(rc) + can_push_.notify_one(); + + return rc; + } + bool waitAndPop(Data& value) + { + bool rc; + { + std::unique_lock<std::mutex> lk(mutex_); + if(!active_) + return false; + // in case of spurious wakeup, loop until predicate in lambda + // is satisfied. + can_pop_.wait(lk, [this] { return !queue_.empty() || !active_; }); + rc = pop_(value); + } + if(rc) + can_push_.notify_one(); + + return rc; + } + + private: + bool push_(Data const& value) + { + if(queue_.size() == max_size_ || !active_) + return false; + queue_.push(value); + + return true; + } + bool pop_(Data& value) + { + if(queue_.empty() || !active_) + return false; + value = queue_.front(); + queue_.pop(); + + return true; + } + std::queue<Data> queue_; + mutable std::mutex mutex_; + std::condition_variable can_pop_; + std::condition_variable can_push_; + bool active_; + size_t max_size_; +}; +struct BufferSrc +{ + BufferSrc(void) : BufferSrc("") {} + explicit BufferSrc(const std::string &file) : file_(file), clientFrameId_(0), frameId_(0), framePtr_(nullptr) + {} + BufferSrc(size_t clientFrameId, size_t frameId, uint8_t* framePtr) + : file_(""), clientFrameId_(clientFrameId), frameId_(frameId), framePtr_(framePtr) + {} + bool fromDisk(void) + { + return !file_.empty() && framePtr_ == nullptr; + } + size_t index() const + { + return clientFrameId_; + } + std::string file_; + size_t clientFrameId_; + size_t frameId_; + uint8_t* framePtr_; +}; + +struct Messenger; +static void outboundThread(Messenger* messenger, const std::string &sendBuf, Synch* synch); +static void inboundThread(Messenger* messenger, const std::string &receiveBuf, Synch* synch); +static void processorThread(Messenger* messenger, std::function<void(std::string)> processor); + +struct Messenger +{ + explicit Messenger(MessengerInit init) + : running(true), _initialized(false), _shutdown(false), init_(init), + outboundSynch_(nullptr), + inboundSynch_(nullptr), uncompressed_buffer_(nullptr), compressed_buffer_(nullptr), + uncompressed_fd_(0), compressed_fd_(0) + {} + virtual ~Messenger(void) + { + running = false; + sendQueue.deactivate(); + receiveQueue.deactivate(); + + if (outboundSynch_) { + outboundSynch_->post(SYNCH_RECEIVE_READY); + outbound.join(); + } + + if (inboundSynch_) { + inboundSynch_->post(SYNCH_SENT); + inbound.join(); + } + + for(auto& p : processors_) + p.join(); + + delete outboundSynch_; + delete inboundSynch_; + + deinitShm(); + } + void startThreads(void) { + outboundSynch_ = + new Synch(init_.outboundSentSynch, init_.outboundReceiveReadySynch); + outbound = std::thread(outboundThread, this, init_.outboundMessageBuf, outboundSynch_); + + inboundSynch_ = + new Synch(init_.inboundSentSynch, init_.inboundReceiveReadySynch); + inbound = std::thread(inboundThread, this, init_.inboundMessageBuf, inboundSynch_); + + for(size_t i = 0; i < init_.numProcessingThreads_; ++i) + processors_.push_back(std::thread(processorThread, this, init_.processor_)); + } + bool initBuffers(void) + { + bool rc = true; + if(init_.uncompressedFrameSize_) + { + rc = rc && SharedMemoryManager::initShm(grokUncompressedBuf, + init_.uncompressedFrameSize_ * init_.numFrames_, + &uncompressed_fd_, &uncompressed_buffer_); + } + if(init_.compressedFrameSize_) + { + rc = rc && SharedMemoryManager::initShm(grokCompressedBuf, + init_.compressedFrameSize_ * init_.numFrames_, + &compressed_fd_, &compressed_buffer_); + } + + return rc; + } + + bool deinitShm(void) + { + bool rc = SharedMemoryManager::deinitShm(grokUncompressedBuf, + init_.uncompressedFrameSize_ * init_.numFrames_, + uncompressed_fd_, &uncompressed_buffer_); + rc = rc && SharedMemoryManager::deinitShm(grokCompressedBuf, + init_.compressedFrameSize_ * init_.numFrames_, + compressed_fd_, &compressed_buffer_); + + return rc; + } + template<typename... Args> + void send(const std::string& str, Args... args) + { + std::ostringstream oss; + oss << str; + int dummy[] = {0, ((void)(oss << ',' << args), 0)...}; + static_cast<void>(dummy); + + sendQueue.push(oss.str()); + } + + bool launchGrok( + boost::filesystem::path const& dir, + uint32_t width, + uint32_t stride, + uint32_t height, + uint32_t samplesPerPixel, + uint32_t depth, + int device, + bool is4K, + uint32_t fps, + uint32_t bandwidth, + const std::string server, + uint32_t port, + const std::string license + ) + { + + std::unique_lock<std::mutex> lk(shutdownMutex_); + if (async_result_.valid()) + return true; + if(MessengerInit::firstLaunch(true)) + init_.unlink(); + startThreads(); + char _cmd[4096]; + auto fullServer = server + ":" + std::to_string(port); + sprintf(_cmd, + "./grk_compress -batch_src %s,%d,%d,%d,%d,%d -out_fmt j2k -k 1 " + "-G %d -%s %d,%d -j %s -J %s -v", + GRK_MSGR_BATCH_IMAGE.c_str(), width, stride, height, samplesPerPixel, depth, + device, is4K ? "cinema4K" : "cinema2K", fps, bandwidth, + license.c_str(), fullServer.c_str()); + + return launch(_cmd, dir); + } + void initClient(size_t uncompressedFrameSize, size_t compressedFrameSize, size_t numFrames) + { + // client fills queue with pending uncompressed buffers + init_.uncompressedFrameSize_ = uncompressedFrameSize; + init_.compressedFrameSize_ = compressedFrameSize; + init_.numFrames_ = numFrames; + initBuffers(); + auto ptr = uncompressed_buffer_; + for(size_t i = 0; i < init_.numFrames_; ++i) + { + availableBuffers_.push(BufferSrc(0, i, (uint8_t*)ptr)); + ptr += init_.uncompressedFrameSize_; + } + + std::unique_lock<std::mutex> lk(shutdownMutex_); + _initialized = true; + clientInitializedCondition_.notify_all(); + } + + bool waitForClientInit() + { + if (_initialized) { + return true; + } else if (_shutdown) { + return false; + } + + std::unique_lock<std::mutex> lk(shutdownMutex_); + + if (_initialized) { + return true; + } else if (_shutdown) { + return false; + } + + while (true) { + if (clientInitializedCondition_.wait_for(lk, std::chrono::seconds(1), [this]{ return _initialized || _shutdown; })) { + break; + } + auto status = async_result_.wait_for(std::chrono::milliseconds(100)); + if (status == std::future_status::ready) { + getMessengerLogger()->error("Grok exited unexpectedly during initialization"); + return false; + } + } + + return _initialized && !_shutdown; + } + + static size_t uncompressedFrameSize(uint32_t w, uint32_t h, uint32_t samplesPerPixel) + { + return sizeof(uint16_t) * w * h * samplesPerPixel; + } + void reclaimCompressed(size_t frameId) + { + availableBuffers_.push(BufferSrc(0, frameId, getCompressedFrame(frameId))); + } + void reclaimUncompressed(size_t frameId) + { + availableBuffers_.push(BufferSrc(0, frameId, getUncompressedFrame(frameId))); + } + uint8_t* getUncompressedFrame(size_t frameId) + { + assert(frameId < init_.numFrames_); + if(frameId >= init_.numFrames_) + return nullptr; + + return (uint8_t*)(uncompressed_buffer_ + frameId * init_.uncompressedFrameSize_); + } + uint8_t* getCompressedFrame(size_t frameId) + { + assert(frameId < init_.numFrames_); + if(frameId >= init_.numFrames_) + return nullptr; + + return (uint8_t*)(compressed_buffer_ + frameId * init_.compressedFrameSize_); + } + std::atomic_bool running; + bool _initialized; + bool _shutdown; + MessengerBlockingQueue<std::string> sendQueue; + MessengerBlockingQueue<std::string> receiveQueue; + MessengerBlockingQueue<BufferSrc> availableBuffers_; + MessengerInit init_; + std::string cmd_; + std::future<int> async_result_; + std::mutex shutdownMutex_; + std::condition_variable shutdownCondition_; + + protected: + std::condition_variable clientInitializedCondition_; + private: + bool launch(std::string const& cmd, boost::filesystem::path const& dir) + { + // Change the working directory + if(!dir.empty()) + { + boost::system::error_code ec; + boost::filesystem::current_path(dir, ec); + if (ec) { + getMessengerLogger()->error("Error: failed to change the working directory"); + return false; + } + } + // Execute the command using std::async and std::system + cmd_ = cmd; + getMessengerLogger()->info(cmd.c_str()); + async_result_ = std::async(std::launch::async, [this]() { return std::system(cmd_.c_str()); }); + bool success = async_result_.valid(); + if (!success) + getMessengerLogger()->error("Grok launch failed"); + + return success; + + } + std::thread outbound; + Synch* outboundSynch_; + + std::thread inbound; + Synch* inboundSynch_; + + std::vector<std::thread> processors_; + char* uncompressed_buffer_; + char* compressed_buffer_; + + grk_handle uncompressed_fd_; + grk_handle compressed_fd_; +}; + +static void outboundThread(Messenger* messenger, const std::string &sendBuf, Synch* synch) +{ + grk_handle shm_fd = 0; + char* send_buffer = nullptr; + + if(!SharedMemoryManager::initShm(sendBuf, messageBufferLen, &shm_fd, &send_buffer)) + return; + while(messenger->running) + { + synch->wait(SYNCH_RECEIVE_READY); + if(!messenger->running) + break; + std::string message; + if(!messenger->sendQueue.waitAndPop(message)) + break; + if(!messenger->running) + break; + memcpy(send_buffer, message.c_str(), message.size() + 1); + synch->post(SYNCH_SENT); + } + SharedMemoryManager::deinitShm(sendBuf, messageBufferLen, shm_fd, &send_buffer); +} + +static void inboundThread(Messenger* messenger, const std::string &receiveBuf, Synch* synch) +{ + grk_handle shm_fd = 0; + char* receive_buffer = nullptr; + + if(!SharedMemoryManager::initShm(receiveBuf, messageBufferLen, &shm_fd, &receive_buffer)) + return; + while(messenger->running) + { + synch->wait(SYNCH_SENT); + if(!messenger->running) + break; + auto message = std::string(receive_buffer); + synch->post(SYNCH_RECEIVE_READY); + messenger->receiveQueue.push(message); + } + SharedMemoryManager::deinitShm(receiveBuf, messageBufferLen, shm_fd, &receive_buffer); +} +struct Msg +{ + explicit Msg(const std::string &msg) : ct_(0) + { + std::stringstream ss(msg); + while(ss.good()) + { + std::string substr; + std::getline(ss, substr, ','); + cs_.push_back(substr); + } + } + std::string next() + { + if(ct_ == cs_.size()) + { + getMessengerLogger()->error("Msg: comma separated list exhausted. returning empty."); + return ""; + } + return cs_[ct_++]; + } + + uint32_t nextUint(void) + { + return (uint32_t)std::stoi(next()); + } + + std::vector<std::string> cs_; + size_t ct_; +}; + +static void processorThread(Messenger* messenger, std::function<void(std::string)> processor) +{ + while (messenger->running) { + std::string message; + if (!messenger->receiveQueue.waitAndPop(message)) { + break; + } + + if (!messenger->running) { + break; + } + + Msg msg(message); + auto tag = msg.next(); + if (tag == GRK_MSGR_BATCH_COMPRESS_INIT) { + auto width = msg.nextUint(); + msg.nextUint(); // stride + auto height = msg.nextUint(); + auto samples_per_pixel = msg.nextUint(); + msg.nextUint(); // depth + messenger->init_.uncompressedFrameSize_ = Messenger::uncompressedFrameSize(width, height, samples_per_pixel); + auto compressed_frame_size = msg.nextUint(); + auto num_frames = msg.nextUint(); + messenger->initClient(compressed_frame_size, compressed_frame_size, num_frames); + } else if (tag == GRK_MSGR_BATCH_PROCESSED_UNCOMPRESSED) { + messenger->reclaimUncompressed(msg.nextUint()); + } else if (tag == GRK_MSGR_BATCH_PROCESSSED_COMPRESSED) { + messenger->reclaimCompressed(msg.nextUint()); + } + processor(message); + } +} + +template<typename F> +struct ScheduledFrames +{ + void store(F const& val) + { + std::unique_lock<std::mutex> lk(mapMutex_); + auto it = map_.find(val.index()); + if (it == map_.end()) + map_.emplace(std::make_pair(val.index(), val)); + } + boost::optional<F> retrieve(size_t index) + { + std::unique_lock<std::mutex> lk(mapMutex_); + auto it = map_.find(index); + if(it == map_.end()) + return {}; + + F val = it->second; + map_.erase(index); + + return val; + } + + private: + std::mutex mapMutex_; + std::map<size_t, F> map_; +}; + +template<typename F> +struct ScheduledMessenger : public Messenger +{ + explicit ScheduledMessenger(MessengerInit init) : Messenger(init), + framesScheduled_(0), + framesCompressed_(0) + {} + ~ScheduledMessenger(void) { + shutdown(); + } + bool scheduleCompress(F const& proxy, std::function<void(BufferSrc const&)> converter){ + size_t frameSize = init_.uncompressedFrameSize_; + assert(frameSize >= init_.uncompressedFrameSize_); + BufferSrc src; + if(!availableBuffers_.waitAndPop(src)) + return false; + converter(src); + scheduledFrames_.store(proxy); + framesScheduled_++; + send(GRK_MSGR_BATCH_SUBMIT_UNCOMPRESSED, proxy.index(), src.frameId_); + + return true; + } + void processCompressed(const std::string &message, std::function<void(F,uint8_t*,uint32_t)> processor, bool needsRecompression) { + Msg msg(message); + msg.next(); + auto clientFrameId = msg.nextUint(); + auto compressedFrameId = msg.nextUint(); + auto compressedFrameLength = msg.nextUint(); + if (!needsRecompression) { + auto src_frame = scheduledFrames_.retrieve(clientFrameId); + if (!src_frame) { + return; + } + processor(*src_frame, getCompressedFrame(compressedFrameId),compressedFrameLength); + } + ++framesCompressed_; + send(GRK_MSGR_BATCH_PROCESSSED_COMPRESSED, compressedFrameId); + if (_shutdown && framesCompressed_ == framesScheduled_) + shutdownCondition_.notify_all(); + } + void shutdown(void){ + try { + std::unique_lock<std::mutex> lk(shutdownMutex_); + if (!async_result_.valid()) + return; + _shutdown = true; + if (framesScheduled_) { + uint32_t scheduled = framesScheduled_; + send(GRK_MSGR_BATCH_FLUSH, scheduled); + shutdownCondition_.wait(lk, [this] { return framesScheduled_ == framesCompressed_; }); + } + availableBuffers_.deactivate(); + send(GRK_MSGR_BATCH_SHUTDOWN); + int result = async_result_.get(); + if(result != 0) + getMessengerLogger()->error("Accelerator failed with return code: %d\n",result); + } catch (std::exception &ex) { + getMessengerLogger()->error("%s",ex.what()); + } + + } + + boost::optional<F> retrieve(size_t index) { + return scheduledFrames_.retrieve(index); + } + + void store(F& val) { + scheduledFrames_.store(val); + } + +private: + ScheduledFrames<F> scheduledFrames_; + std::atomic<uint32_t> framesScheduled_; + std::atomic<uint32_t> framesCompressed_; +}; + +} // namespace grk_plugin diff --git a/src/lib/grok_j2k_encoder_thread.cc b/src/lib/grok_j2k_encoder_thread.cc new file mode 100644 index 000000000..c3cd6f8dd --- /dev/null +++ b/src/lib/grok_j2k_encoder_thread.cc @@ -0,0 +1,72 @@ +/* + Copyright (C) 2023 Carl Hetherington <cth@carlh.net> + + This file is part of DCP-o-matic. + + DCP-o-matic is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + DCP-o-matic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with DCP-o-matic. If not, see <http://www.gnu.org/licenses/>. + +*/ + + +#include "config.h" +#include "cross.h" +#include "dcpomatic_log.h" +#include "dcp_video.h" +#include "grok/context.h" +#include "grok_j2k_encoder_thread.h" +#include "j2k_encoder.h" +#include "util.h" +#include <dcp/scope_guard.h> + +#include "i18n.h" + + +using std::make_shared; +using std::shared_ptr; + + +GrokJ2KEncoderThread::GrokJ2KEncoderThread(J2KEncoder& encoder, grk_plugin::GrokContext* context) + : J2KEncoderThread(encoder) + , _context(context) +{ + +} + + +void +GrokJ2KEncoderThread::run() +try +{ + while (true) + { + LOG_TIMING("encoder-sleep thread=%1", thread_id()); + auto frame = _encoder.pop(); + + dcp::ScopeGuard frame_guard([this, &frame]() { + LOG_ERROR("Failed to schedule encode of %1 using grok", frame.index()); + _encoder.retry(frame); + }); + + LOG_TIMING("encoder-pop thread=%1 frame=%2 eyes=%3", thread_id(), frame.index(), static_cast<int>(frame.eyes())); + + auto grok = Config::instance()->grok().get_value_or({}); + + if (_context->launch(frame, grok.selected) && _context->scheduleCompress(frame)) { + frame_guard.cancel(); + } + } +} catch (boost::thread_interrupted& e) { +} catch (...) { + store_current(); +} diff --git a/src/lib/grok_j2k_encoder_thread.h b/src/lib/grok_j2k_encoder_thread.h new file mode 100644 index 000000000..5301e1670 --- /dev/null +++ b/src/lib/grok_j2k_encoder_thread.h @@ -0,0 +1,41 @@ +/* + Copyright (C) 2023 Carl Hetherington <cth@carlh.net> + + This file is part of DCP-o-matic. + + DCP-o-matic is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + DCP-o-matic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with DCP-o-matic. If not, see <http://www.gnu.org/licenses/>. + +*/ + + +#include "exception_store.h" +#include "j2k_encoder_thread.h" + + +namespace grk_plugin { + class GrokContext; +} + + +class GrokJ2KEncoderThread : public J2KEncoderThread, public ExceptionStore +{ +public: + GrokJ2KEncoderThread(J2KEncoder& encoder, grk_plugin::GrokContext* context); + + void run() override; + +private: + grk_plugin::GrokContext* _context; +}; + diff --git a/src/lib/image_decoder.cc b/src/lib/image_decoder.cc index ce5c8757f..527a98c7d 100644 --- a/src/lib/image_decoder.cc +++ b/src/lib/image_decoder.cc @@ -81,7 +81,7 @@ ImageDecoder::pass () } } - video->emit (film(), _image, _frame_video_position); + video->emit(film(), _image, dcpomatic::ContentTime::from_frames(_frame_video_position, _image_content->video_frame_rate().get_value_or(24))); ++_frame_video_position; return false; } diff --git a/src/lib/j2k_encoder.cc b/src/lib/j2k_encoder.cc index 7c9777c16..de229113b 100644 --- a/src/lib/j2k_encoder.cc +++ b/src/lib/j2k_encoder.cc @@ -32,6 +32,12 @@ #include "encode_server_description.h" #include "encode_server_finder.h" #include "film.h" +#include "cpu_j2k_encoder_thread.h" +#ifdef DCPOMATIC_GROK +#include "grok/context.h" +#include "grok_j2k_encoder_thread.h" +#endif +#include "remote_j2k_encoder_thread.h" #include "j2k_encoder.h" #include "log.h" #include "player_video.h" @@ -44,6 +50,7 @@ using std::cout; +using std::dynamic_pointer_cast; using std::exception; using std::list; using std::make_shared; @@ -53,6 +60,33 @@ using boost::optional; using dcp::Data; using namespace dcpomatic; +#ifdef DCPOMATIC_GROK + +namespace grk_plugin { + +IMessengerLogger* sLogger = nullptr; + +#if defined(__GNUC__) || defined(__clang__) +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wunused-function" +#endif +void setMessengerLogger(grk_plugin::IMessengerLogger* logger) +{ + delete sLogger; + sLogger = logger; +} +#if defined(__GNUC__) || defined(__clang__) +#pragma GCC diagnostic pop +#endif +grk_plugin::IMessengerLogger* getMessengerLogger(void) +{ + return sLogger; +} + +} + +#endif + /** @param film Film that we are encoding. * @param writer Writer that we are using. @@ -62,6 +96,13 @@ J2KEncoder::J2KEncoder(shared_ptr<const Film> film, Writer& writer) , _history (200) , _writer (writer) { +#ifdef DCPOMATIC_GROK + auto grok = Config::instance()->grok().get_value_or({}); + _dcpomatic_context = new grk_plugin::DcpomaticContext(film, writer, _history, grok.binary_location); + if (grok.enable) { + _context = new grk_plugin::GrokContext(_dcpomatic_context); + } +#endif } @@ -69,8 +110,29 @@ J2KEncoder::~J2KEncoder () { _server_found_connection.disconnect(); - boost::mutex::scoped_lock lm (_threads_mutex); - terminate_threads (); + terminate_threads(); + +#ifdef DCPOMATIC_GROK + delete _context; + delete _dcpomatic_context; +#endif +} + + +void +J2KEncoder::servers_list_changed() +{ + auto config = Config::instance(); +#ifdef DCPOMATIC_GROK + auto const grok_enable = config->grok().get_value_or({}).enable; +#else + auto const grok_enable = false; +#endif + + auto const cpu = (grok_enable || config->only_servers_encode()) ? 0 : config->master_encoding_threads(); + auto const gpu = grok_enable ? config->master_encoding_threads() : 0; + + remake_threads(cpu, gpu, EncodeServerFinder::instance()->servers()); } @@ -85,7 +147,40 @@ J2KEncoder::begin () void -J2KEncoder::end () +J2KEncoder::pause() +{ +#ifdef DCPOMATIC_GROK + if (!Config::instance()->grok().get_value_or({}).enable) { + return; + } + return; + + terminate_threads (); + + /* Something might have been thrown during terminate_threads */ + rethrow (); + + delete _context; + _context = nullptr; +#endif +} + + +void J2KEncoder::resume() +{ +#ifdef DCPOMATIC_GROK + if (!Config::instance()->grok().get_value_or({}).enable) { + return; + } + + _context = new grk_plugin::GrokContext(_dcpomatic_context); + servers_list_changed(); +#endif +} + + +void +J2KEncoder::end() { boost::mutex::scoped_lock lock (_queue_mutex); @@ -94,18 +189,13 @@ J2KEncoder::end () /* Keep waking workers until the queue is empty */ while (!_queue.empty ()) { rethrow (); - _empty_condition.notify_all (); _full_condition.wait (lock); } - lock.unlock (); LOG_GENERAL_NC (N_("Terminating encoder threads")); - { - boost::mutex::scoped_lock lm (_threads_mutex); - terminate_threads (); - } + terminate_threads (); /* Something might have been thrown during terminate_threads */ rethrow (); @@ -120,20 +210,35 @@ J2KEncoder::end () So just mop up anything left in the queue here. */ - - for (auto const& i: _queue) { - LOG_GENERAL(N_("Encode left-over frame %1"), i.index()); - try { - _writer.write( - make_shared<dcp::ArrayData>(i.encode_locally()), - i.index(), - i.eyes() - ); - frame_done (); - } catch (std::exception& e) { - LOG_ERROR (N_("Local encode failed (%1)"), e.what ()); + for (auto & i: _queue) { +#ifdef DCPOMATIC_GROK + if (Config::instance()->grok().get_value_or({}).enable) { + if (!_context->scheduleCompress(i)){ + LOG_GENERAL (N_("[%1] J2KEncoder thread pushes frame %2 back onto queue after failure"), thread_id(), i.index()); + // handle error + } + } else { +#else + { +#endif + LOG_GENERAL(N_("Encode left-over frame %1"), i.index()); + try { + _writer.write( + make_shared<dcp::ArrayData>(i.encode_locally()), + i.index(), + i.eyes() + ); + frame_done (); + } catch (std::exception& e) { + LOG_ERROR (N_("Local encode failed (%1)"), e.what ()); + } } } + +#ifdef DCPOMATIC_GROK + delete _context; + _context = nullptr; +#endif } @@ -183,7 +288,7 @@ J2KEncoder::encode (shared_ptr<PlayerVideo> pv, DCPTime time) size_t threads = 0; { boost::mutex::scoped_lock lm (_threads_mutex); - threads = _threads->size(); + threads = _threads.size(); } boost::mutex::scoped_lock queue_lock (_queue_mutex); @@ -223,13 +328,14 @@ J2KEncoder::encode (shared_ptr<PlayerVideo> pv, DCPTime time) LOG_DEBUG_ENCODE("Frame @ %1 ENCODE", to_string(time)); /* Queue this new frame for encoding */ LOG_TIMING ("add-frame-to-queue queue=%1", _queue.size ()); - _queue.push_back (DCPVideo( + auto dcpv = DCPVideo( pv, position, _film->video_frame_rate(), _film->j2k_bandwidth(), _film->resolution() - )); + ); + _queue.push_back (dcpv); /* The queue might not be empty any more, so notify anything which is waiting on that. @@ -242,170 +348,141 @@ J2KEncoder::encode (shared_ptr<PlayerVideo> pv, DCPTime time) } -/** Caller must hold a lock on _threads_mutex */ void J2KEncoder::terminate_threads () { + boost::mutex::scoped_lock lm(_threads_mutex); boost::this_thread::disable_interruption dis; - if (!_threads) { - return; - } - - _threads->interrupt_all (); - try { - _threads->join_all (); - } catch (exception& e) { - LOG_ERROR ("join() threw an exception: %1", e.what()); - } catch (...) { - LOG_ERROR_NC ("join() threw an exception"); + for (auto& thread: _threads) { + thread->stop(); } - _threads.reset (); + _threads.clear(); + _ending = true; } void -J2KEncoder::encoder_thread (optional<EncodeServerDescription> server) -try +J2KEncoder::remake_threads(int cpu, int gpu, list<EncodeServerDescription> servers) { - start_of_thread ("J2KEncoder"); + LOG_GENERAL("Making threads: CPU=%1, GPU=%2, Remote=%3", cpu, gpu, servers.size()); - if (server) { - LOG_TIMING ("start-encoder-thread thread=%1 server=%2", thread_id (), server->host_name ()); - } else { - LOG_TIMING ("start-encoder-thread thread=%1 server=localhost", thread_id ()); + boost::mutex::scoped_lock lm (_threads_mutex); + if (_ending) { + return; } - /* Number of seconds that we currently wait between attempts - to connect to the server; not relevant for localhost - encodings. - */ - int remote_backoff = 0; + auto remove_threads = [this](int wanted, int current, std::function<bool (shared_ptr<J2KEncoderThread>)> predicate) { + for (auto i = wanted; i < current; ++i) { + auto iter = std::find_if(_threads.begin(), _threads.end(), predicate); + if (iter != _threads.end()) { + (*iter)->stop(); + _threads.erase(iter); + } + } + }; + + + /* CPU */ + + auto const is_cpu_thread = [](shared_ptr<J2KEncoderThread> thread) { + return static_cast<bool>(dynamic_pointer_cast<CPUJ2KEncoderThread>(thread)); + }; + + auto const current_cpu_threads = std::count_if(_threads.begin(), _threads.end(), is_cpu_thread); + + for (auto i = current_cpu_threads; i < cpu; ++i) { + auto thread = make_shared<CPUJ2KEncoderThread>(*this); + thread->start(); + _threads.push_back(thread); + } + + remove_threads(cpu, current_cpu_threads, is_cpu_thread); + +#ifdef DCPOMATIC_GROK + /* GPU */ + + auto const is_grok_thread = [](shared_ptr<J2KEncoderThread> thread) { + return static_cast<bool>(dynamic_pointer_cast<GrokJ2KEncoderThread>(thread)); + }; + + auto const current_gpu_threads = std::count_if(_threads.begin(), _threads.end(), is_grok_thread); + + for (auto i = current_gpu_threads; i < gpu; ++i) { + auto thread = make_shared<GrokJ2KEncoderThread>(*this, _context); + thread->start(); + _threads.push_back(thread); + } + + remove_threads(gpu, current_gpu_threads, is_grok_thread); +#endif - while (true) { + /* Remote */ - LOG_TIMING ("encoder-sleep thread=%1", thread_id ()); - boost::mutex::scoped_lock lock (_queue_mutex); - while (_queue.empty ()) { - _empty_condition.wait (lock); + for (auto const& server: servers) { + if (!server.current_link_version()) { + continue; } - LOG_TIMING ("encoder-wake thread=%1 queue=%2", thread_id(), _queue.size()); - auto vf = _queue.front (); + auto is_remote_thread = [server](shared_ptr<J2KEncoderThread> thread) { + auto remote = dynamic_pointer_cast<RemoteJ2KEncoderThread>(thread); + return remote && remote->server().host_name() == server.host_name(); + }; - /* We're about to commit to either encoding this frame or putting it back onto the queue, - so we must not be interrupted until one or other of these things have happened. This - block has thread interruption disabled. - */ - { - boost::this_thread::disable_interruption dis; - - LOG_TIMING ("encoder-pop thread=%1 frame=%2 eyes=%3", thread_id(), vf.index(), static_cast<int>(vf.eyes())); - _queue.pop_front (); - - lock.unlock (); - - shared_ptr<Data> encoded; - - /* We need to encode this input */ - if (server) { - try { - encoded = make_shared<dcp::ArrayData>(vf.encode_remotely(server.get())); - - if (remote_backoff > 0) { - LOG_GENERAL ("%1 was lost, but now she is found; removing backoff", server->host_name ()); - } - - /* This job succeeded, so remove any backoff */ - remote_backoff = 0; - - } catch (std::exception& e) { - if (remote_backoff < 60) { - /* back off more */ - remote_backoff += 10; - } - LOG_ERROR ( - N_("Remote encode of %1 on %2 failed (%3); thread sleeping for %4s"), - vf.index(), server->host_name(), e.what(), remote_backoff - ); - } - - } else { - try { - LOG_TIMING ("start-local-encode thread=%1 frame=%2", thread_id(), vf.index()); - encoded = make_shared<dcp::ArrayData>(vf.encode_locally()); - LOG_TIMING ("finish-local-encode thread=%1 frame=%2", thread_id(), vf.index()); - } catch (std::exception& e) { - /* This is very bad, so don't cope with it, just pass it on */ - LOG_ERROR (N_("Local encode failed (%1)"), e.what ()); - throw; - } - } + auto const current_threads = std::count_if(_threads.begin(), _threads.end(), is_remote_thread); - if (encoded) { - _writer.write(encoded, vf.index(), vf.eyes()); - frame_done (); - } else { - lock.lock (); - LOG_GENERAL (N_("[%1] J2KEncoder thread pushes frame %2 back onto queue after failure"), thread_id(), vf.index()); - _queue.push_front (vf); - lock.unlock (); - } + auto const wanted_threads = server.threads(); + + if (wanted_threads > current_threads) { + LOG_GENERAL(N_("Adding %1 worker threads for remote %2"), wanted_threads - current_threads, server.host_name()); + } else if (wanted_threads < current_threads) { + LOG_GENERAL(N_("Removing %1 worker threads for remote %2"), current_threads - wanted_threads, server.host_name()); } - if (remote_backoff > 0) { - boost::this_thread::sleep (boost::posix_time::seconds (remote_backoff)); + for (auto i = current_threads; i < wanted_threads; ++i) { + auto thread = make_shared<RemoteJ2KEncoderThread>(*this, server); + thread->start(); + _threads.push_back(thread); } - /* The queue might not be full any more, so notify anything that is waiting on that */ - lock.lock (); - _full_condition.notify_all (); + remove_threads(wanted_threads, current_threads, is_remote_thread); } -} -catch (boost::thread_interrupted& e) { - /* Ignore these and just stop the thread */ - _full_condition.notify_all (); -} -catch (...) -{ - store_current (); - /* Wake anything waiting on _full_condition so it can see the exception */ - _full_condition.notify_all (); + + _writer.set_encoder_threads(_threads.size()); } -void -J2KEncoder::servers_list_changed () +DCPVideo +J2KEncoder::pop() { - boost::mutex::scoped_lock lm (_threads_mutex); + boost::mutex::scoped_lock lock(_queue_mutex); + while (_queue.empty()) { + _empty_condition.wait (lock); + } - terminate_threads (); - _threads = make_shared<boost::thread_group>(); + LOG_TIMING("encoder-wake thread=%1 queue=%2", thread_id(), _queue.size()); - /* XXX: could re-use threads */ + auto vf = _queue.front(); + _queue.pop_front(); - if (!Config::instance()->only_servers_encode ()) { - for (int i = 0; i < Config::instance()->master_encoding_threads (); ++i) { -#ifdef DCPOMATIC_LINUX - auto t = _threads->create_thread(boost::bind(&J2KEncoder::encoder_thread, this, optional<EncodeServerDescription>())); - pthread_setname_np (t->native_handle(), "encode-worker"); -#else - _threads->create_thread(boost::bind(&J2KEncoder::encoder_thread, this, optional<EncodeServerDescription>())); -#endif - } - } + _full_condition.notify_all(); + return vf; +} - for (auto i: EncodeServerFinder::instance()->servers()) { - if (!i.current_link_version()) { - continue; - } - LOG_GENERAL (N_("Adding %1 worker threads for remote %2"), i.threads(), i.host_name ()); - for (int j = 0; j < i.threads(); ++j) { - _threads->create_thread(boost::bind(&J2KEncoder::encoder_thread, this, i)); - } - } +void +J2KEncoder::retry(DCPVideo video) +{ + boost::mutex::scoped_lock lock(_queue_mutex); + _queue.push_front(video); + _empty_condition.notify_all(); +} - _writer.set_encoder_threads(_threads->size()); + +void +J2KEncoder::write(shared_ptr<const dcp::Data> data, int index, Eyes eyes) +{ + _writer.write(data, index, eyes); + frame_done(); } diff --git a/src/lib/j2k_encoder.h b/src/lib/j2k_encoder.h index 63228a6b8..6bfbaea49 100644 --- a/src/lib/j2k_encoder.h +++ b/src/lib/j2k_encoder.h @@ -32,6 +32,7 @@ #include "enum_indexed_vector.h" #include "event_history.h" #include "exception_store.h" +#include "j2k_encoder_thread.h" #include "writer.h" #include <boost/optional.hpp> #include <boost/signals2.hpp> @@ -48,6 +49,15 @@ class Film; class Job; class PlayerVideo; +namespace grk_plugin { + struct DcpomaticContext; + struct GrokContext; +} + +struct local_threads_created_and_destroyed; +struct remote_threads_created_and_destroyed; +struct frames_not_lost_when_threads_disappear; + /** @class J2KEncoder * @brief Class to manage encoding to J2K. @@ -70,19 +80,27 @@ public: /** Called to pass a bit of video to be encoded as the next DCP frame */ void encode (std::shared_ptr<PlayerVideo> pv, dcpomatic::DCPTime time); + void pause(); + void resume(); + /** Called when a processing run has finished */ - void end (); + void end(); boost::optional<float> current_encoding_rate () const; int video_frames_enqueued () const; - void servers_list_changed (); + DCPVideo pop(); + void retry(DCPVideo frame); + void write(std::shared_ptr<const dcp::Data> data, int index, Eyes eyes); private: + friend struct ::local_threads_created_and_destroyed; + friend struct ::remote_threads_created_and_destroyed; + friend struct ::frames_not_lost_when_threads_disappear; void frame_done (); - - void encoder_thread (boost::optional<EncodeServerDescription>); + void servers_list_changed (); + void remake_threads(int cpu, int gpu, std::list<EncodeServerDescription> servers); void terminate_threads (); /** Film that we are encoding */ @@ -91,7 +109,7 @@ private: EventHistory _history; boost::mutex _threads_mutex; - std::shared_ptr<boost::thread_group> _threads; + std::vector<std::shared_ptr<J2KEncoderThread>> _threads; mutable boost::mutex _queue_mutex; std::list<DCPVideo> _queue; @@ -107,6 +125,13 @@ private: boost::optional<dcpomatic::DCPTime> _last_player_video_time; boost::signals2::scoped_connection _server_found_connection; + +#ifdef DCPOMATIC_GROK + grk_plugin::DcpomaticContext* _dcpomatic_context = nullptr; + grk_plugin::GrokContext *_context = nullptr; +#endif + + bool _ending = false; }; diff --git a/src/lib/j2k_encoder_thread.cc b/src/lib/j2k_encoder_thread.cc new file mode 100644 index 000000000..d0e8a439c --- /dev/null +++ b/src/lib/j2k_encoder_thread.cc @@ -0,0 +1,58 @@ +/* + Copyright (C) 2023 Carl Hetherington <cth@carlh.net> + + This file is part of DCP-o-matic. + + DCP-o-matic is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + DCP-o-matic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with DCP-o-matic. If not, see <http://www.gnu.org/licenses/>. + +*/ + + +#include "dcp_video.h" +#include "dcpomatic_log.h" +#include "j2k_encoder.h" +#include "j2k_encoder_thread.h" + + +J2KEncoderThread::J2KEncoderThread(J2KEncoder& encoder) + : _encoder(encoder) +{ + +} + + +void +J2KEncoderThread::start() +{ + _thread = boost::thread(boost::bind(&J2KEncoderThread::run, this)); +#ifdef DCPOMATIC_LINUX + pthread_setname_np(_thread.native_handle(), "encode-worker"); +#endif +} + + +void +J2KEncoderThread::stop() +{ + _thread.interrupt(); + try { + _thread.join(); + } catch (std::exception& e) { + LOG_ERROR("join() threw an exception: %1", e.what()); + } catch (...) { + LOG_ERROR_NC("join() threw an exception"); + } +} + + diff --git a/src/lib/j2k_encoder_thread.h b/src/lib/j2k_encoder_thread.h new file mode 100644 index 000000000..b03b6f356 --- /dev/null +++ b/src/lib/j2k_encoder_thread.h @@ -0,0 +1,53 @@ +/* + Copyright (C) 2023 Carl Hetherington <cth@carlh.net> + + This file is part of DCP-o-matic. + + DCP-o-matic is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + DCP-o-matic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with DCP-o-matic. If not, see <http://www.gnu.org/licenses/>. + +*/ + + +#ifndef DCPOMATIC_J2K_ENCODER_THREAD +#define DCPOMATIC_J2K_ENCODER_THREAD + + +#include <boost/thread.hpp> + + +class J2KEncoder; + + +class J2KEncoderThread +{ +public: + J2KEncoderThread(J2KEncoder& encoder); + + J2KEncoderThread(J2KEncoderThread const&) = delete; + J2KEncoderThread& operator=(J2KEncoderThread const&) = delete; + + void start(); + void stop(); + + virtual void run() = 0; + +protected: + J2KEncoder& _encoder; + +private: + boost::thread _thread; +}; + + +#endif diff --git a/src/lib/j2k_sync_encoder_thread.cc b/src/lib/j2k_sync_encoder_thread.cc new file mode 100644 index 000000000..ef6834f60 --- /dev/null +++ b/src/lib/j2k_sync_encoder_thread.cc @@ -0,0 +1,65 @@ +/* + Copyright (C) 2023 Carl Hetherington <cth@carlh.net> + + This file is part of DCP-o-matic. + + DCP-o-matic is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + DCP-o-matic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with DCP-o-matic. If not, see <http://www.gnu.org/licenses/>. + +*/ + + +#include "dcp_video.h" +#include "dcpomatic_log.h" +#include "j2k_encoder.h" +#include "j2k_sync_encoder_thread.h" +#include <dcp/scope_guard.h> + + +J2KSyncEncoderThread::J2KSyncEncoderThread(J2KEncoder& encoder) + : J2KEncoderThread(encoder) +{ + +} + + +void +J2KSyncEncoderThread::run() +try +{ + log_thread_start(); + + while (true) { + LOG_TIMING("encoder-sleep thread=%1", thread_id()); + auto frame = _encoder.pop(); + + dcp::ScopeGuard frame_guard([this, &frame]() { + boost::this_thread::disable_interruption dis; + _encoder.retry(frame); + }); + + LOG_TIMING("encoder-pop thread=%1 frame=%2 eyes=%3", thread_id(), frame.index(), static_cast<int>(frame.eyes())); + + auto encoded = encode(frame); + + if (encoded) { + boost::this_thread::disable_interruption dis; + frame_guard.cancel(); + _encoder.write(encoded, frame.index(), frame.eyes()); + } + } +} catch (boost::thread_interrupted& e) { +} catch (...) { + store_current(); +} + diff --git a/src/lib/j2k_sync_encoder_thread.h b/src/lib/j2k_sync_encoder_thread.h new file mode 100644 index 000000000..45222279e --- /dev/null +++ b/src/lib/j2k_sync_encoder_thread.h @@ -0,0 +1,32 @@ +#ifndef DCPOMATIC_J2K_SYNC_ENCODER_THREAD_H +#define DCPOMATIC_J2K_SYNC_ENCODER_THREAD_H + + +#include "exception_store.h" +#include "j2k_encoder_thread.h" +#include <dcp/array_data.h> +#include <boost/thread.hpp> + + +class DCPVideo; +class J2KEncoder; + + +class J2KSyncEncoderThread : public J2KEncoderThread, public ExceptionStore +{ +public: + J2KSyncEncoderThread(J2KEncoder& encoder); + + J2KSyncEncoderThread(J2KSyncEncoderThread const&) = delete; + J2KSyncEncoderThread& operator=(J2KSyncEncoderThread const&) = delete; + + virtual ~J2KSyncEncoderThread() {} + + void run() override; + + virtual void log_thread_start() const = 0; + virtual std::shared_ptr<dcp::ArrayData> encode(DCPVideo const& frame) = 0; +}; + + +#endif diff --git a/src/lib/job.cc b/src/lib/job.cc index 9c9530b7a..9e685ec11 100644 --- a/src/lib/job.cc +++ b/src/lib/job.cc @@ -662,7 +662,7 @@ void Job::cancel () { if (_thread.joinable()) { - resume(); + Job::resume(); _thread.interrupt (); _thread.join (); @@ -689,6 +689,7 @@ Job::pause_by_user () } if (paused) { + pause(); _pause_changed.notify_all (); } @@ -701,6 +702,7 @@ Job::pause_by_priority () { if (running ()) { set_state (PAUSED_BY_PRIORITY); + pause(); _pause_changed.notify_all (); } } diff --git a/src/lib/job.h b/src/lib/job.h index d4d0f9510..9b5fdfa6e 100644 --- a/src/lib/job.h +++ b/src/lib/job.h @@ -62,9 +62,10 @@ public: } void start (); + virtual void pause() {} bool pause_by_user (); void pause_by_priority (); - void resume (); + virtual void resume (); void cancel (); bool is_new () const; diff --git a/src/lib/kdm_cli.cc b/src/lib/kdm_cli.cc index ddc77e771..4e3f9ccb7 100644 --- a/src/lib/kdm_cli.cc +++ b/src/lib/kdm_cli.cc @@ -62,8 +62,8 @@ help (std::function<void (string)> out) out (" -o, --output <path> output file or directory"); out (" -K, --filename-format <format> filename format for KDMs"); out (" -Z, --container-name-format <format> filename format for ZIP containers"); - out (" -f, --valid-from <time> valid from time (in local time zone of the cinema) (e.g. \"2013-09-28 01:41:51\") or \"now\""); - out (" -t, --valid-to <time> valid to time (in local time zone of the cinema) (e.g. \"2014-09-28 01:41:51\")"); + out (" -f, --valid-from <time> valid from time (e.g. \"2013-09-28T01:41:51+04:00\", \"2018-01-01T12:00:30\") or \"now\""); + out (" -t, --valid-to <time> valid to time (e.g. \"2014-09-28T01:41:51\")"); out (" -d, --valid-duration <duration> valid duration (e.g. \"1 day\", \"4 hours\", \"2 weeks\")"); out (" -F, --formulation <formulation> modified-transitional-1, multiple-modified-transitional-1, dci-any or dci-specific [default modified-transitional-1]"); out (" -p, --disable-forensic-marking-picture disable forensic marking of pictures essences"); @@ -99,17 +99,6 @@ public: }; -static boost::posix_time::ptime -time_from_string (string t) -{ - if (t == "now") { - return boost::posix_time::second_clock::local_time (); - } - - return boost::posix_time::time_from_string (t); -} - - static boost::posix_time::time_duration duration_from_string (string d) { @@ -211,8 +200,8 @@ from_film ( boost::filesystem::path output, dcp::NameFormat container_name_format, dcp::NameFormat filename_format, - boost::posix_time::ptime valid_from, - boost::posix_time::ptime valid_to, + dcp::LocalTime valid_from, + dcp::LocalTime valid_to, dcp::Formulation formulation, bool disable_forensic_marking_picture, optional<int> disable_forensic_marking_audio, @@ -362,8 +351,8 @@ from_dkdm ( boost::filesystem::path output, dcp::NameFormat container_name_format, dcp::NameFormat filename_format, - boost::posix_time::ptime valid_from, - boost::posix_time::ptime valid_to, + dcp::LocalTime valid_from, + dcp::LocalTime valid_to, dcp::Formulation formulation, bool disable_forensic_marking_picture, optional<int> disable_forensic_marking_audio, @@ -381,18 +370,12 @@ from_dkdm ( continue; } - int const offset_hour = i->cinema ? i->cinema->utc_offset_hour() : 0; - int const offset_minute = i->cinema ? i->cinema->utc_offset_minute() : 0; - - dcp::LocalTime begin(valid_from, dcp::UTCOffset(offset_hour, offset_minute)); - dcp::LocalTime end(valid_to, dcp::UTCOffset(offset_hour, offset_minute)); - auto const kdm = kdm_from_dkdm( dkdm, i->recipient.get(), i->trusted_device_thumbprints(), - begin, - end, + valid_from, + valid_to, formulation, disable_forensic_marking_picture, disable_forensic_marking_audio @@ -402,8 +385,8 @@ from_dkdm ( name_values['c'] = i->cinema ? i->cinema->name : ""; name_values['s'] = i->name; name_values['f'] = kdm.content_title_text(); - name_values['b'] = begin.date() + " " + begin.time_of_day(true, false); - name_values['e'] = end.date() + " " + end.time_of_day(true, false); + name_values['b'] = valid_from.date() + " " + valid_from.time_of_day(true, false); + name_values['e'] = valid_to.date() + " " + valid_to.time_of_day(true, false); name_values['i'] = kdm.cpl_id(); kdms.push_back(make_shared<KDMWithMetadata>(name_values, i->cinema.get(), i->cinema ? i->cinema->emails : vector<string>(), kdm)); @@ -440,6 +423,22 @@ dump_dkdm_group (shared_ptr<DKDMGroup> group, int indent, std::function<void (st } +static +dcp::LocalTime +time_from_string(string time) +{ + if (time == "now") { + return {}; + } + + if (time.length() > 10 && time[10] == ' ') { + time[10] = 'T'; + } + + return dcp::LocalTime(time); +} + + optional<string> kdm_cli (int argc, char* argv[], std::function<void (string)> out) try @@ -453,8 +452,8 @@ try optional<string> screen; vector<shared_ptr<Screen>> screens; optional<dcp::EncryptedKDM> dkdm; - optional<boost::posix_time::ptime> valid_from; - optional<boost::posix_time::ptime> valid_to; + optional<dcp::LocalTime> valid_from; + optional<dcp::LocalTime> valid_to; bool zip = false; bool list_cinemas = false; bool list_dkdm_cpls = false; @@ -517,10 +516,10 @@ try container_name_format = dcp::NameFormat (optarg); break; case 'f': - valid_from = time_from_string (optarg); + valid_from = time_from_string(optarg); break; case 't': - valid_to = time_from_string (optarg); + valid_to = dcp::LocalTime(optarg); break; case 'd': duration_string = optarg; @@ -564,7 +563,7 @@ try (for lookup) and by creating a Cinema which the next Screen will be added to. */ cinema_name = optarg; - cinema = make_shared<Cinema>(optarg, vector<string>(), "", 0, 0); + cinema = make_shared<Cinema>(optarg, vector<string>(), ""); break; case 'S': /* Similarly, this could be the name of a new (temporary) screen or the name of a screen @@ -644,11 +643,12 @@ try } if (duration_string) { - valid_to = valid_from.get() + duration_from_string (*duration_string); + valid_to = valid_from.get(); + valid_to->add(duration_from_string(*duration_string)); } if (verbose) { - out (String::compose("Making KDMs valid from %1 to %2", boost::posix_time::to_simple_string(valid_from.get()), boost::posix_time::to_simple_string(valid_to.get()))); + out(String::compose("Making KDMs valid from %1 to %2", valid_from->as_string(), valid_to->as_string())); } string const thing = argv[optind]; diff --git a/src/lib/make_dcp.cc b/src/lib/make_dcp.cc index 17d45be46..ddd231243 100644 --- a/src/lib/make_dcp.cc +++ b/src/lib/make_dcp.cc @@ -40,8 +40,8 @@ using std::shared_ptr; using std::string; -/** Add suitable Jobs to the JobManager to create a DCP for a Film */ -void +/** Add suitable Job to the JobManager to create a DCP for a Film */ +shared_ptr<TranscodeJob> make_dcp (shared_ptr<Film> film, TranscodeJob::ChangedBehaviour behaviour) { if (film->dcp_name().find("/") != string::npos) { @@ -91,15 +91,12 @@ make_dcp (shared_ptr<Film> film, TranscodeJob::ChangedBehaviour behaviour) LOG_GENERAL ("Content: %1", content->technical_summary()); } LOG_GENERAL ("DCP video rate %1 fps", film->video_frame_rate()); - if (Config::instance()->only_servers_encode()) { - LOG_GENERAL_NC ("0 threads: ONLY SERVERS SET TO ENCODE"); - } else { - LOG_GENERAL ("%1 threads", Config::instance()->master_encoding_threads()); - } LOG_GENERAL ("J2K bandwidth %1", film->j2k_bandwidth()); auto tj = make_shared<DCPTranscodeJob>(film, behaviour); tj->set_encoder (make_shared<DCPEncoder>(film, tj)); JobManager::instance()->add (tj); + + return tj; } diff --git a/src/lib/make_dcp.h b/src/lib/make_dcp.h index 9f5072782..fe0bcd2f6 100644 --- a/src/lib/make_dcp.h +++ b/src/lib/make_dcp.h @@ -25,5 +25,5 @@ class Film; -void make_dcp (std::shared_ptr<Film> film, TranscodeJob::ChangedBehaviour behaviour); +std::shared_ptr<TranscodeJob> make_dcp(std::shared_ptr<Film> film, TranscodeJob::ChangedBehaviour behaviour); diff --git a/src/lib/overlaps.cc b/src/lib/overlaps.cc index fbc6b5cb1..bed75a822 100644 --- a/src/lib/overlaps.cc +++ b/src/lib/overlaps.cc @@ -28,7 +28,8 @@ using std::shared_ptr; using namespace dcpomatic; -ContentList overlaps (shared_ptr<const Film> film, ContentList cl, function<bool (shared_ptr<const Content>)> part, DCPTime from, DCPTime to) +ContentList +dcpomatic::overlaps(shared_ptr<const Film> film, ContentList cl, function<bool (shared_ptr<const Content>)> part, DCPTime from, DCPTime to) { ContentList overlaps; DCPTimePeriod period (from, to); diff --git a/src/lib/overlaps.h b/src/lib/overlaps.h index bb8277eaa..f405c0fab 100644 --- a/src/lib/overlaps.h +++ b/src/lib/overlaps.h @@ -27,6 +27,9 @@ class ContentPart; class Film; +namespace dcpomatic { + + /** @return Pieces of content with a given part (video, audio, * subtitle) that overlap a specified time range in the given * ContentList @@ -34,3 +37,7 @@ class Film; ContentList overlaps ( std::shared_ptr<const Film> film, ContentList cl, std::function<bool (std::shared_ptr<const Content>)> part, dcpomatic::DCPTime from, dcpomatic::DCPTime to ); + + +} + diff --git a/src/lib/player.cc b/src/lib/player.cc index c03cb97a5..dba02cfba 100644 --- a/src/lib/player.cc +++ b/src/lib/player.cc @@ -71,10 +71,8 @@ using std::dynamic_pointer_cast; using std::list; using std::make_pair; using std::make_shared; -using std::make_shared; using std::max; using std::min; -using std::min; using std::pair; using std::shared_ptr; using std::vector; @@ -412,7 +410,6 @@ Player::setup_pieces () _silent = Empty(film, playlist(), bind(&have_audio, _1), _playback_length); _next_video_time = boost::none; - _next_video_eyes = Eyes::BOTH; _next_audio_time = boost::none; } @@ -525,7 +522,7 @@ Player::black_player_video_frame (Eyes eyes) const boost::mutex::scoped_lock lm(_black_image_mutex); return std::make_shared<PlayerVideo> ( - std::make_shared<const RawImageProxy>(_black_image), + make_shared<const RawImageProxy>(_black_image), Crop(), optional<double>(), _video_container_size, @@ -535,7 +532,7 @@ Player::black_player_video_frame (Eyes eyes) const PresetColourConversion::all().front().conversion, VideoRange::FULL, std::weak_ptr<Content>(), - boost::optional<Frame>(), + boost::optional<dcpomatic::ContentTime>(), false ); } @@ -703,7 +700,7 @@ Player::pass () if (_playback_length.load() == DCPTime() || !film) { /* Special; just give one black frame */ - emit_video (black_player_video_frame(Eyes::BOTH), DCPTime()); + use_video(black_player_video_frame(Eyes::BOTH), DCPTime(), one_video_frame()); return true; } @@ -761,22 +758,33 @@ Player::pass () LOG_DEBUG_PLAYER ("Calling pass() on %1", earliest_content->content->path(0)); earliest_content->done = earliest_content->decoder->pass (); auto dcp = dynamic_pointer_cast<DCPContent>(earliest_content->content); - if (dcp && !_play_referenced && dcp->reference_audio()) { - /* We are skipping some referenced DCP audio content, so we need to update _next_audio_time - to `hide' the fact that no audio was emitted during the referenced DCP (though - we need to behave as though it was). - */ - _next_audio_time = dcp->end(film); + if (dcp && !_play_referenced) { + if (dcp->reference_video()) { + _next_video_time = dcp->end(film); + } + if (dcp->reference_audio()) { + /* We are skipping some referenced DCP audio content, so we need to update _next_audio_time + to `hide' the fact that no audio was emitted during the referenced DCP (though + we need to behave as though it was). + */ + _next_audio_time = dcp->end(film); + } } break; } case BLACK: LOG_DEBUG_PLAYER ("Emit black for gap at %1", to_string(_black.position())); + if (!_next_video_time) { + /* Deciding to emit black has the same effect as getting some video from the content + * when we are inaccurately seeking. + */ + _next_video_time = _black.position(); + } if (film->three_d()) { - emit_video(black_player_video_frame(Eyes::LEFT), _black.position()); - emit_video(black_player_video_frame(Eyes::RIGHT), _black.position()); + use_video(black_player_video_frame(Eyes::LEFT), _black.position(), _black.period_at_position().to); + use_video(black_player_video_frame(Eyes::RIGHT), _black.position(), _black.period_at_position().to); } else { - emit_video(black_player_video_frame(Eyes::BOTH), _black.position()); + use_video(black_player_video_frame(Eyes::BOTH), _black.position(), _black.period_at_position().to); } _black.set_position (_black.position() + one_video_frame()); break; @@ -879,24 +887,14 @@ Player::pass () } if (done) { + LOG_DEBUG_PLAYER("Done: emit video until end of film at %1", to_string(film->length())); + emit_video_until(film->length()); + if (_shuffler) { _shuffler->flush (); } for (auto const& i: _delay) { - do_emit_video(i.first, i.second); - } - - /* Perhaps we should have Empty entries for both eyes in the 3D case (somehow). - * However, if we have L and R video files, and one is shorter than the other, - * the fill code in ::video mostly takes care of filling in the gaps. - * However, since it fills at the point when it knows there is more video coming - * at time t (so it should fill any gap up to t) it can't do anything right at the - * end. This is particularly bad news if the last frame emitted is a LEFT - * eye, as the MXF writer will complain about the 3D sequence being wrong. - * Here's a hack to workaround that particular case. - */ - if (_next_video_eyes && _next_video_time && *_next_video_eyes == Eyes::RIGHT) { - do_emit_video (black_player_video_frame(Eyes::RIGHT), *_next_video_time); + emit_video(i.first, i.second); } } @@ -959,15 +957,61 @@ Player::open_subtitles_for_frame (DCPTime time) const } -static -Eyes -increment_eyes (Eyes e) +void +Player::emit_video_until(DCPTime time) { - if (e == Eyes::LEFT) { - return Eyes::RIGHT; - } + LOG_DEBUG_PLAYER("emit_video_until %1; next video time is %2", to_string(time), to_string(_next_video_time.get_value_or({}))); + auto frame = [this](shared_ptr<PlayerVideo> pv, DCPTime time) { + /* We need a delay to give a little wiggle room to ensure that relevant subtitles arrive at the + player before the video that requires them. + */ + _delay.push_back(make_pair(pv, time)); + + if (pv->eyes() == Eyes::BOTH || pv->eyes() == Eyes::RIGHT) { + _next_video_time = time + one_video_frame(); + } + + if (_delay.size() < 3) { + return; + } + + auto to_do = _delay.front(); + _delay.pop_front(); + emit_video(to_do.first, to_do.second); + }; - return Eyes::LEFT; + auto const age_threshold = one_video_frame() * 2; + + while (_next_video_time.get_value_or({}) < time) { + auto left = _last_video[Eyes::LEFT]; + auto right = _last_video[Eyes::RIGHT]; + auto both = _last_video[Eyes::BOTH]; + + auto const next = _next_video_time.get_value_or({}); + + if ( + left.first && + right.first && + (!both.first || (left.second >= both.second && right.second >= both.second)) && + (left.second - next) < age_threshold && + (right.second - next) < age_threshold + ) { + frame(left.first, next); + frame(right.first, next); + } else if (both.first && (both.second - next) < age_threshold) { + frame(both.first, next); + LOG_DEBUG_PLAYER("Content %1 selected for DCP %2 (age %3)", to_string(both.second), to_string(next), to_string(both.second - next)); + } else { + auto film = _film.lock(); + if (film && film->three_d()) { + frame(black_player_video_frame(Eyes::LEFT), next); + frame(black_player_video_frame(Eyes::RIGHT), next); + } else { + frame(black_player_video_frame(Eyes::BOTH), next); + } + LOG_DEBUG_PLAYER("Black selected for DCP %1", to_string(next)); + } + } } @@ -992,11 +1036,6 @@ Player::video (weak_ptr<Piece> weak_piece, ContentVideo video) return; } - FrameRateChange frc(film, piece->content); - if (frc.skip && (video.frame % 2) == 1) { - return; - } - vector<Eyes> eyes_to_emit; if (!film->three_d()) { @@ -1019,15 +1058,11 @@ Player::video (weak_ptr<Piece> weak_piece, ContentVideo video) eyes_to_emit = { video.eyes }; } - /* Time of the first frame we will emit */ - DCPTime const time = content_video_to_dcp (piece, video.frame); - LOG_DEBUG_PLAYER("Received video frame %1 at %2", video.frame, to_string(time)); + /* Time of the frame we just received within the DCP */ + auto const time = content_time_to_dcp(piece, video.time); + LOG_DEBUG_PLAYER("Received video frame %1 %2 eyes %3", to_string(video.time), to_string(time), static_cast<int>(video.eyes)); - /* Discard if it's before the content's period or the last accurate seek. We can't discard - if it's after the content's period here as in that case we still need to fill any gap between - `now' and the end of the content's period. - */ - if (time < piece->content->position() || (_next_video_time && time < *_next_video_time)) { + if (time < piece->content->position()) { return; } @@ -1040,56 +1075,8 @@ Player::video (weak_ptr<Piece> weak_piece, ContentVideo video) return; } - /* Fill gaps that we discover now that we have some video which needs to be emitted. - This is where we need to fill to. - */ - DCPTime fill_to = min(time, piece->content->end(film)); - - if (_next_video_time) { - DCPTime fill_from = max (*_next_video_time, piece->content->position()); - - /* Fill if we have more than half a frame to do */ - if ((fill_to - fill_from) > one_video_frame() / 2) { - auto last = _last_video.find (weak_piece); - if (film->three_d()) { - auto fill_to_eyes = eyes_to_emit[0]; - if (fill_to_eyes == Eyes::BOTH) { - fill_to_eyes = Eyes::LEFT; - } - if (fill_to == piece->content->end(film)) { - /* Don't fill after the end of the content */ - fill_to_eyes = Eyes::LEFT; - } - auto j = fill_from; - auto eyes = _next_video_eyes.get_value_or(Eyes::LEFT); - if (eyes == Eyes::BOTH) { - eyes = Eyes::LEFT; - } - while (j < fill_to || eyes != fill_to_eyes) { - if (last != _last_video.end()) { - LOG_DEBUG_PLAYER("Fill using last video at %1 in 3D mode", to_string(j)); - auto copy = last->second->shallow_copy(); - copy->set_eyes (eyes); - emit_video (copy, j); - } else { - LOG_DEBUG_PLAYER("Fill using black at %1 in 3D mode", to_string(j)); - emit_video (black_player_video_frame(eyes), j); - } - if (eyes == Eyes::RIGHT) { - j += one_video_frame(); - } - eyes = increment_eyes (eyes); - } - } else { - for (DCPTime j = fill_from; j < fill_to; j += one_video_frame()) { - if (last != _last_video.end()) { - emit_video (last->second, j); - } else { - emit_video (black_player_video_frame(Eyes::BOTH), j); - } - } - } - } + if (!_next_video_time) { + _next_video_time = time.round(film->video_frame_rate()); } auto const content_video = piece->content->video; @@ -1098,33 +1085,38 @@ Player::video (weak_ptr<Piece> weak_piece, ContentVideo video) DCPOMATIC_ASSERT(scaled_size); for (auto eyes: eyes_to_emit) { - _last_video[weak_piece] = std::make_shared<PlayerVideo>( - video.image, - content_video->actual_crop(), - content_video->fade(film, video.frame), - scale_for_display( - *scaled_size, + use_video( + std::make_shared<PlayerVideo>( + video.image, + content_video->actual_crop(), + content_video->fade(film, video.time), + scale_for_display( + *scaled_size, + _video_container_size, + film->frame_size(), + content_video->pixel_quanta() + ), _video_container_size, - film->frame_size(), - content_video->pixel_quanta() + eyes, + video.part, + content_video->colour_conversion(), + content_video->range(), + piece->content, + video.time, + false ), - _video_container_size, - eyes, - video.part, - content_video->colour_conversion(), - content_video->range(), - piece->content, - video.frame, - false + time, + piece->content->end(film) ); + } +} - DCPTime t = time; - for (int i = 0; i < frc.repeat; ++i) { - if (t < piece->content->end(film)) { - emit_video (_last_video[weak_piece], t); - } - t += one_video_frame (); - } +void +Player::use_video(shared_ptr<PlayerVideo> pv, DCPTime time, DCPTime end) +{ + _last_video[pv->eyes()] = { pv, time }; + if (pv->eyes() != Eyes::LEFT) { + emit_video_until(std::min(time + one_video_frame() / 2, end)); } } @@ -1406,18 +1398,18 @@ Player::seek (DCPTime time, bool accurate) if (accurate) { _next_video_time = time; - _next_video_eyes = Eyes::LEFT; _next_audio_time = time; } else { _next_video_time = boost::none; - _next_video_eyes = boost::none; _next_audio_time = boost::none; } _black.set_position (time); _silent.set_position (time); - _last_video.clear (); + _last_video[Eyes::LEFT] = {}; + _last_video[Eyes::RIGHT] = {}; + _last_video[Eyes::BOTH] = {}; for (auto& state: _stream_states) { state.second.last_push_end = boost::none; @@ -1426,33 +1418,7 @@ Player::seek (DCPTime time, bool accurate) void -Player::emit_video (shared_ptr<PlayerVideo> pv, DCPTime time) -{ - auto film = _film.lock(); - DCPOMATIC_ASSERT(film); - - /* We need a delay to give a little wiggle room to ensure that relevant subtitles arrive at the - player before the video that requires them. - */ - _delay.push_back (make_pair (pv, time)); - - if (pv->eyes() == Eyes::BOTH || pv->eyes() == Eyes::RIGHT) { - _next_video_time = time + one_video_frame(); - } - _next_video_eyes = increment_eyes (pv->eyes()); - - if (_delay.size() < 3) { - return; - } - - auto to_do = _delay.front(); - _delay.pop_front(); - do_emit_video (to_do.first, to_do.second); -} - - -void -Player::do_emit_video (shared_ptr<PlayerVideo> pv, DCPTime time) +Player::emit_video(shared_ptr<PlayerVideo> pv, DCPTime time) { if (pv->eyes() == Eyes::BOTH || pv->eyes() == Eyes::RIGHT) { std::for_each(_active_texts.begin(), _active_texts.end(), [time](ActiveText& a) { a.clear_before(time); }); diff --git a/src/lib/player.h b/src/lib/player.h index 94e41bbca..2502ae536 100644 --- a/src/lib/player.h +++ b/src/lib/player.h @@ -155,6 +155,8 @@ private: dcpomatic::ContentTime dcp_to_content_time (std::shared_ptr<const Piece> piece, dcpomatic::DCPTime t) const; dcpomatic::DCPTime content_time_to_dcp (std::shared_ptr<const Piece> piece, dcpomatic::ContentTime t) const; std::shared_ptr<PlayerVideo> black_player_video_frame (Eyes eyes) const; + void emit_video_until(dcpomatic::DCPTime time); + void insert_video(std::shared_ptr<PlayerVideo> pv, dcpomatic::DCPTime time, dcpomatic::DCPTime end); void video (std::weak_ptr<Piece>, ContentVideo); void audio (std::weak_ptr<Piece>, AudioStreamPtr, ContentAudio); @@ -169,8 +171,8 @@ private: std::shared_ptr<const AudioBuffers> audio, dcpomatic::DCPTime time, dcpomatic::DCPTime discard_to ) const; boost::optional<PositionImage> open_subtitles_for_frame (dcpomatic::DCPTime time) const; - void emit_video (std::shared_ptr<PlayerVideo> pv, dcpomatic::DCPTime time); - void do_emit_video (std::shared_ptr<PlayerVideo> pv, dcpomatic::DCPTime time); + void emit_video(std::shared_ptr<PlayerVideo> pv, dcpomatic::DCPTime time); + void use_video(std::shared_ptr<PlayerVideo> pv, dcpomatic::DCPTime time, dcpomatic::DCPTime end); void emit_audio (std::shared_ptr<AudioBuffers> data, dcpomatic::DCPTime time); std::shared_ptr<const Playlist> playlist () const; @@ -211,15 +213,12 @@ private: /** Time of the next video that we will emit, or the time of the last accurate seek */ boost::optional<dcpomatic::DCPTime> _next_video_time; - /** Eyes of the next video that we will emit */ - boost::optional<Eyes> _next_video_eyes; /** Time of the next audio that we will emit, or the time of the last accurate seek */ boost::optional<dcpomatic::DCPTime> _next_audio_time; boost::atomic<boost::optional<int>> _dcp_decode_reduction; - typedef std::map<std::weak_ptr<Piece>, std::shared_ptr<PlayerVideo>, std::owner_less<std::weak_ptr<Piece>>> LastVideoMap; - LastVideoMap _last_video; + EnumIndexedVector<std::pair<std::shared_ptr<PlayerVideo>, dcpomatic::DCPTime>, Eyes> _last_video; AudioMerger _audio_merger; std::unique_ptr<Shuffler> _shuffler; diff --git a/src/lib/player_video.cc b/src/lib/player_video.cc index 35c5d3daa..b39f83908 100644 --- a/src/lib/player_video.cc +++ b/src/lib/player_video.cc @@ -45,6 +45,7 @@ using std::weak_ptr; using boost::optional; using dcp::Data; using dcp::raw_convert; +using namespace dcpomatic; PlayerVideo::PlayerVideo ( @@ -58,7 +59,7 @@ PlayerVideo::PlayerVideo ( optional<ColourConversion> colour_conversion, VideoRange video_range, weak_ptr<Content> content, - optional<Frame> video_frame, + optional<ContentTime> video_time, bool error ) : _in (in) @@ -71,7 +72,7 @@ PlayerVideo::PlayerVideo ( , _colour_conversion (colour_conversion) , _video_range (video_range) , _content (content) - , _video_frame (video_frame) + , _video_time(video_time) , _error (error) { @@ -343,7 +344,7 @@ PlayerVideo::shallow_copy () const _colour_conversion, _video_range, _content, - _video_frame, + _video_time, _error ); } @@ -356,12 +357,12 @@ bool PlayerVideo::reset_metadata (shared_ptr<const Film> film, dcp::Size player_video_container_size) { auto content = _content.lock(); - if (!content || !_video_frame) { + if (!content || !_video_time) { return false; } _crop = content->video->actual_crop(); - _fade = content->video->fade(film, _video_frame.get()); + _fade = content->video->fade(film, _video_time.get()); auto const size = content->video->scaled_size(film->frame_size()); if (!size) { return false; diff --git a/src/lib/player_video.h b/src/lib/player_video.h index f2781c1a0..10b2078a0 100644 --- a/src/lib/player_video.h +++ b/src/lib/player_video.h @@ -59,7 +59,7 @@ public: boost::optional<ColourConversion> colour_conversion, VideoRange video_range, std::weak_ptr<Content> content, - boost::optional<Frame> video_frame, + boost::optional<dcpomatic::ContentTime> video_time, bool error ); @@ -141,8 +141,8 @@ private: boost::optional<PositionImage> _text; /** Content that we came from. This is so that reset_metadata() can work. */ std::weak_ptr<Content> _content; - /** Video frame that we came from. Again, this is for reset_metadata() */ - boost::optional<Frame> _video_frame; + /** Video time that we came from. Again, this is for reset_metadata() */ + boost::optional<dcpomatic::ContentTime> _video_time; mutable boost::mutex _mutex; mutable std::shared_ptr<Image> _image; diff --git a/src/lib/remote_j2k_encoder_thread.cc b/src/lib/remote_j2k_encoder_thread.cc new file mode 100644 index 000000000..49d80953d --- /dev/null +++ b/src/lib/remote_j2k_encoder_thread.cc @@ -0,0 +1,84 @@ +/* + Copyright (C) 2023 Carl Hetherington <cth@carlh.net> + + This file is part of DCP-o-matic. + + DCP-o-matic is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + DCP-o-matic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with DCP-o-matic. If not, see <http://www.gnu.org/licenses/>. + +*/ + + +#include "dcp_video.h" +#include "dcpomatic_log.h" +#include "j2k_encoder.h" +#include "remote_j2k_encoder_thread.h" +#include "util.h" + +#include "i18n.h" + + +using std::make_shared; +using std::shared_ptr; + + +RemoteJ2KEncoderThread::RemoteJ2KEncoderThread(J2KEncoder& encoder, EncodeServerDescription server) + : J2KSyncEncoderThread(encoder) + , _server(server) +{ + +} + + +void +RemoteJ2KEncoderThread::log_thread_start() const +{ + start_of_thread("RemoteJ2KEncoder"); + LOG_TIMING("start-encoder-thread thread=%1 server=%2", thread_id(), _server.host_name()); +} + + +shared_ptr<dcp::ArrayData> +RemoteJ2KEncoderThread::encode(DCPVideo const& frame) +{ + shared_ptr<dcp::ArrayData> encoded; + + try { + encoded = make_shared<dcp::ArrayData>(frame.encode_remotely(_server)); + if (_remote_backoff > 0) { + LOG_GENERAL("%1 was lost, but now she is found; removing backoff", _server.host_name()); + _remote_backoff = 0; + } + } catch (std::exception& e) { + LOG_ERROR( + N_("Remote encode of %1 on %2 failed (%3); thread sleeping for %4s"), + frame.index(), _server.host_name(), e.what(), _remote_backoff + ); + } catch (...) { + LOG_ERROR( + N_("Remote encode of %1 on %2 failed; thread sleeping for %4s"), + frame.index(), _server.host_name(), _remote_backoff + ); + } + + if (!encoded) { + if (_remote_backoff < 60) { + /* back off more */ + _remote_backoff += 10; + } + boost::this_thread::sleep(boost::posix_time::seconds(_remote_backoff)); + } + + return encoded; +} + diff --git a/src/lib/remote_j2k_encoder_thread.h b/src/lib/remote_j2k_encoder_thread.h new file mode 100644 index 000000000..f3fe7f94a --- /dev/null +++ b/src/lib/remote_j2k_encoder_thread.h @@ -0,0 +1,21 @@ +#include "encode_server_description.h" +#include "j2k_sync_encoder_thread.h" + + +class RemoteJ2KEncoderThread : public J2KSyncEncoderThread +{ +public: + RemoteJ2KEncoderThread(J2KEncoder& encoder, EncodeServerDescription server); + + void log_thread_start() const override; + std::shared_ptr<dcp::ArrayData> encode(DCPVideo const& frame) override; + + EncodeServerDescription server() const { + return _server; + } + +private: + EncodeServerDescription _server; + /** Number of seconds that we currently wait between attempts to connect to the server */ + int _remote_backoff = 0; +}; diff --git a/src/lib/screen.cc b/src/lib/screen.cc index febf9085c..f0c80cd79 100644 --- a/src/lib/screen.cc +++ b/src/lib/screen.cc @@ -77,8 +77,8 @@ KDMWithMetadataPtr kdm_for_screen ( std::function<dcp::DecryptedKDM (dcp::LocalTime, dcp::LocalTime)> make_kdm, shared_ptr<const dcpomatic::Screen> screen, - boost::posix_time::ptime valid_from, - boost::posix_time::ptime valid_to, + dcp::LocalTime valid_from, + dcp::LocalTime valid_to, dcp::Formulation formulation, bool disable_forensic_marking_picture, optional<int> disable_forensic_marking_audio, @@ -90,17 +90,15 @@ kdm_for_screen ( } auto cinema = screen->cinema; - dcp::LocalTime const begin(valid_from, dcp::UTCOffset(cinema ? cinema->utc_offset_hour() : 0, cinema ? cinema->utc_offset_minute() : 0)); - dcp::LocalTime const end (valid_to, dcp::UTCOffset(cinema ? cinema->utc_offset_hour() : 0, cinema ? cinema->utc_offset_minute() : 0)); - period_checks.push_back(check_kdm_and_certificate_validity_periods(cinema ? cinema->name : "", screen->name, screen->recipient.get(), begin, end)); + period_checks.push_back(check_kdm_and_certificate_validity_periods(cinema ? cinema->name : "", screen->name, screen->recipient.get(), valid_from, valid_to)); auto signer = Config::instance()->signer_chain(); if (!signer->valid()) { throw InvalidSignerError(); } - auto kdm = make_kdm(begin, end).encrypt( + auto kdm = make_kdm(valid_from, valid_to).encrypt( signer, screen->recipient.get(), screen->trusted_device_thumbprints(), formulation, disable_forensic_marking_picture, disable_forensic_marking_audio ); @@ -112,8 +110,8 @@ kdm_for_screen ( } name_values['s'] = screen->name; name_values['f'] = kdm.content_title_text(); - name_values['b'] = begin.date() + " " + begin.time_of_day(true, false); - name_values['e'] = end.date() + " " + end.time_of_day(true, false); + name_values['b'] = valid_from.date() + " " + valid_from.time_of_day(true, false); + name_values['e'] = valid_to.date() + " " + valid_to.time_of_day(true, false); name_values['i'] = kdm.cpl_id(); return make_shared<KDMWithMetadata>(name_values, cinema.get(), cinema ? cinema->emails : vector<string>(), kdm); diff --git a/src/lib/screen.h b/src/lib/screen.h index 7f01cdf27..0a275aa34 100644 --- a/src/lib/screen.h +++ b/src/lib/screen.h @@ -23,12 +23,13 @@ #define DCPOMATIC_SCREEN_H -#include "kdm_with_metadata.h" #include "kdm_recipient.h" #include "kdm_util.h" +#include "kdm_with_metadata.h" #include "trusted_device.h" #include <dcp/certificate.h> #include <dcp/decrypted_kdm.h> +#include <dcp/utc_offset.h> #include <libcxml/cxml.h> #include <boost/optional.hpp> #include <string> @@ -78,8 +79,8 @@ KDMWithMetadataPtr kdm_for_screen ( std::function<dcp::DecryptedKDM (dcp::LocalTime, dcp::LocalTime)> make_kdm, std::shared_ptr<const dcpomatic::Screen> screen, - boost::posix_time::ptime valid_from, - boost::posix_time::ptime valid_to, + dcp::LocalTime valid_from, + dcp::LocalTime valid_to, dcp::Formulation formulation, bool disable_forensic_marking_picture, boost::optional<int> disable_forensic_marking_audio, diff --git a/src/lib/shuffler.cc b/src/lib/shuffler.cc index 5a4faf4d1..a4ea0f5dc 100644 --- a/src/lib/shuffler.cc +++ b/src/lib/shuffler.cc @@ -40,8 +40,8 @@ int const Shuffler::_max_size = 64; struct Comparator { bool operator()(Shuffler::Store const & a, Shuffler::Store const & b) { - if (a.second.frame != b.second.frame) { - return a.second.frame < b.second.frame; + if (a.second.time != b.second.time) { + return a.second.time < b.second.time; } return a.second.eyes < b.second.eyes; } @@ -51,7 +51,7 @@ struct Comparator void Shuffler::video (weak_ptr<Piece> weak_piece, ContentVideo video) { - LOG_DEBUG_THREE_D ("Shuffler::video frame=%1 eyes=%2 part=%3", video.frame, static_cast<int>(video.eyes), static_cast<int>(video.part)); + LOG_DEBUG_THREE_D("Shuffler::video time=%1 eyes=%2 part=%3", to_string(video.time), static_cast<int>(video.eyes), static_cast<int>(video.part)); if (video.eyes != Eyes::LEFT && video.eyes != Eyes::RIGHT) { /* Pass through anything that we don't care about */ @@ -79,13 +79,13 @@ Shuffler::video (weak_ptr<Piece> weak_piece, ContentVideo video) !_store.empty() && _last && ( - (_store.front().second.frame == _last->frame && _store.front().second.eyes == Eyes::RIGHT && _last->eyes == Eyes::LEFT) || - (_store.front().second.frame >= (_last->frame + 1) && _store.front().second.eyes == Eyes::LEFT && _last->eyes == Eyes::RIGHT) + (_store.front().second.time == _last->time && _store.front().second.eyes == Eyes::RIGHT && _last->eyes == Eyes::LEFT) || + (_store.front().second.time > _last->time && _store.front().second.eyes == Eyes::LEFT && _last->eyes == Eyes::RIGHT) ); if (!store_front_in_sequence) { - string const store = _store.empty() ? "store empty" : String::compose("store front frame=%1 eyes=%2", _store.front().second.frame, static_cast<int>(_store.front().second.eyes)); - string const last = _last ? String::compose("last frame=%1 eyes=%2", _last->frame, static_cast<int>(_last->eyes)) : "no last"; + string const store = _store.empty() ? "store empty" : String::compose("store front time=%1 eyes=%2", to_string(_store.front().second.time), static_cast<int>(_store.front().second.eyes)); + string const last = _last ? String::compose("last time=%1 eyes=%2", to_string(_last->time), static_cast<int>(_last->eyes)) : "no last"; LOG_DEBUG_THREE_D("Shuffler not in sequence: %1 %2", store, last); } @@ -98,10 +98,10 @@ Shuffler::video (weak_ptr<Piece> weak_piece, ContentVideo video) } if (_store.size() > _max_size) { - LOG_WARNING ("Shuffler is full after receiving frame %1; 3D sync may be incorrect.", video.frame); + LOG_WARNING("Shuffler is full after receiving frame at %1; 3D sync may be incorrect.", to_string(video.time)); } - LOG_DEBUG_THREE_D("Shuffler emits frame=%1 eyes=%2 store=%3", _store.front().second.frame, static_cast<int>(_store.front().second.eyes), _store.size()); + LOG_DEBUG_THREE_D("Shuffler emits time=%1 eyes=%2 store=%3", to_string(_store.front().second.time), static_cast<int>(_store.front().second.eyes), _store.size()); Video (_store.front().first, _store.front().second); _last = _store.front().second; _store.pop_front (); diff --git a/src/lib/state.cc b/src/lib/state.cc index a8230385d..7a3232ee9 100644 --- a/src/lib/state.cc +++ b/src/lib/state.cc @@ -35,7 +35,7 @@ boost::optional<boost::filesystem::path> State::override_path; /* List of config versions to look for in descending order of preference; * i.e. look at the first one, and if that doesn't exist, try the second, etc. */ -static std::vector<std::string> config_versions = { "2.16" }; +static std::vector<std::string> config_versions = { "2.18", "2.16" }; static diff --git a/src/lib/text_content.cc b/src/lib/text_content.cc index 92a35b822..09f6e41b6 100644 --- a/src/lib/text_content.cc +++ b/src/lib/text_content.cc @@ -235,8 +235,11 @@ TextContent::TextContent (Content* parent, cxml::ConstNodePtr node, int version, if (lang) { try { _language = dcp::LanguageTag(lang->content()); - auto add = lang->optional_bool_attribute("Additional"); - _language_is_additional = add && *add; + auto additional = lang->optional_bool_attribute("Additional"); + if (!additional) { + additional = lang->optional_bool_attribute("additional"); + } + _language_is_additional = additional.get_value_or(false); } catch (dcp::LanguageTagError&) { /* The language tag can be empty or invalid if it was loaded from a * 2.14.x metadata file; we'll just ignore it in that case. @@ -409,7 +412,7 @@ TextContent::as_xml (xmlpp::Node* root) const if (_language) { auto lang = text->add_child("Language"); lang->add_child_text (_language->to_string()); - lang->set_attribute ("Additional", _language_is_additional ? "1" : "0"); + lang->set_attribute("additional", _language_is_additional ? "1" : "0"); } } diff --git a/src/lib/transcode_job.cc b/src/lib/transcode_job.cc index 12b9a2aa3..b260bc44b 100644 --- a/src/lib/transcode_job.cc +++ b/src/lib/transcode_job.cc @@ -148,6 +148,20 @@ TranscodeJob::run () } +void +TranscodeJob::pause() +{ + _encoder->pause(); +} + + +void TranscodeJob::resume() +{ + _encoder->resume(); + Job::resume(); +} + + string TranscodeJob::status () const { diff --git a/src/lib/transcode_job.h b/src/lib/transcode_job.h index b05b20a16..35870231d 100644 --- a/src/lib/transcode_job.h +++ b/src/lib/transcode_job.h @@ -37,6 +37,8 @@ class Encoder; +struct frames_not_lost_when_threads_disappear; + /** @class TranscodeJob * @brief A job which transcodes a Film to another format. @@ -56,6 +58,8 @@ public: std::string name () const override; std::string json_name () const override; void run () override; + void pause() override; + void resume() override; std::string status () const override; bool enable_notify () const override { return true; @@ -64,6 +68,8 @@ public: void set_encoder (std::shared_ptr<Encoder> t); private: + friend struct ::frames_not_lost_when_threads_disappear; + virtual void post_transcode () {} float frames_per_second() const; diff --git a/src/lib/util.cc b/src/lib/util.cc index 7f6e9da5a..01a7f0248 100644 --- a/src/lib/util.cc +++ b/src/lib/util.cc @@ -243,6 +243,7 @@ addr2line (void const * const addr) { char addr2line_cmd[512] = { 0 }; sprintf (addr2line_cmd, "addr2line -f -p -e %.256s %p > %s", program_name.c_str(), addr, backtrace_file.string().c_str()); + std::cout << addr2line_cmd << "\n"; return system(addr2line_cmd); } @@ -428,6 +429,11 @@ dcpomatic_setup () SetUnhandledExceptionFilter(exception_handler); #endif +#ifdef DCPOMATIC_GROK + /* This makes grok support work with CUDA 12.2 */ + setenv("CUDA_MODULE_LOADING", "EAGER", 1); +#endif + #ifdef DCPOMATIC_HAVE_AVREGISTER LIBDCP_DISABLE_WARNINGS av_register_all (); @@ -877,16 +883,6 @@ remap (shared_ptr<const AudioBuffers> input, int output_channels, AudioMapping m return mapped; } -Eyes -increment_eyes (Eyes e) -{ - if (e == Eyes::LEFT) { - return Eyes::RIGHT; - } - - return Eyes::LEFT; -} - size_t utf8_strlen (string s) @@ -1123,3 +1119,31 @@ word_wrap(string input, int columns) return output; } + + +#ifdef DCPOMATIC_GROK +void +setup_grok_library_path() +{ + static std::string old_path; + if (old_path.empty()) { + auto const old = getenv("LD_LIRARY_PATH"); + if (old) { + old_path = old; + } + } + auto const grok = Config::instance()->grok(); + if (!grok || grok->binary_location.empty()) { + setenv("LD_LIRARY_PATH", old_path.c_str(), 1); + return; + } + + std::string new_path = old_path; + if (!new_path.empty()) { + new_path += ":"; + } + new_path += grok->binary_location.string(); + + setenv("LD_LIBRARY_PATH", new_path.c_str(), 1); +} +#endif diff --git a/src/lib/util.h b/src/lib/util.h index b92869b25..b85cf0a33 100644 --- a/src/lib/util.h +++ b/src/lib/util.h @@ -32,6 +32,7 @@ #include "dcpomatic_time.h" #include "pixel_quanta.h" #include "types.h" +#include <libcxml/cxml.h> #include <dcp/atmos_asset.h> #include <dcp/decrypted_kdm.h> #include <dcp/util.h> @@ -97,5 +98,20 @@ extern std::string error_details(boost::system::error_code ec); extern bool contains_assetmap(boost::filesystem::path dir); extern std::string word_wrap(std::string input, int columns); extern void capture_ffmpeg_logs(); +#ifdef DCPOMATIC_GROK +extern void setup_grok_library_path(); +#endif + + +template <class T> +T +number_attribute(cxml::ConstNodePtr node, std::string name1, std::string name2) +{ + auto value = node->optional_number_attribute<T>(name1); + if (!value) { + value = node->number_attribute<T>(name2); + } + return *value; +} #endif diff --git a/src/lib/video_content.cc b/src/lib/video_content.cc index 91ed11855..c6046cb37 100644 --- a/src/lib/video_content.cc +++ b/src/lib/video_content.cc @@ -424,27 +424,29 @@ VideoContent::size_after_crop () const } -/** @param f Frame index within the whole (untrimmed) content. +/** @param time Time within the whole (untrimmed) content. * @return Fade factor (between 0 and 1) or unset if there is no fade. */ optional<double> -VideoContent::fade (shared_ptr<const Film> film, Frame f) const +VideoContent::fade(shared_ptr<const Film> film, ContentTime time) const { - DCPOMATIC_ASSERT (f >= 0); + DCPOMATIC_ASSERT(time.get() >= 0); double const vfr = _parent->active_video_frame_rate(film); - auto const ts = _parent->trim_start().frames_round(vfr); - if ((f - ts) < fade_in()) { - return double (f - ts) / fade_in(); + auto const ts = _parent->trim_start(); + auto const fade_in_time = ContentTime::from_frames(fade_in(), vfr); + if ((time - ts) < fade_in_time) { + return double(ContentTime(time - ts).get()) / fade_in_time.get(); } - auto fade_out_start = length() - _parent->trim_end().frames_round(vfr) - fade_out(); - if (f >= fade_out_start) { - return 1 - double (f - fade_out_start) / fade_out(); + auto const fade_out_time = ContentTime::from_frames(fade_out(), vfr); + auto fade_out_start = ContentTime::from_frames(length(), vfr) - _parent->trim_end() - fade_out_time; + if (time >= fade_out_start) { + return 1 - double(ContentTime(time - fade_out_start).get()) / fade_out_time.get(); } - return optional<double> (); + return {}; } string diff --git a/src/lib/video_content.h b/src/lib/video_content.h index e7e8eb1b3..d31c25f13 100644 --- a/src/lib/video_content.h +++ b/src/lib/video_content.h @@ -208,7 +208,7 @@ public: boost::optional<dcp::Size> size_after_crop() const; boost::optional<dcp::Size> scaled_size(dcp::Size container_size); - boost::optional<double> fade (std::shared_ptr<const Film> film, Frame) const; + boost::optional<double> fade(std::shared_ptr<const Film> film, dcpomatic::ContentTime time) const; std::string processing_description (std::shared_ptr<const Film> film); diff --git a/src/lib/video_decoder.cc b/src/lib/video_decoder.cc index cf21f885a..c628fddd9 100644 --- a/src/lib/video_decoder.cc +++ b/src/lib/video_decoder.cc @@ -20,7 +20,6 @@ #include "compose.hpp" -#include "film.h" #include "frame_interval_checker.h" #include "image.h" #include "j2k_image_proxy.h" @@ -47,17 +46,9 @@ VideoDecoder::VideoDecoder (Decoder* parent, shared_ptr<const Content> c) } -/** Called by decoder classes when they have a video frame ready. - * @param frame Frame index within the content; this does not take into account 3D - * so for 3D_ALTERNATE this value goes: - * 0: frame 0 left - * 1: frame 0 right - * 2: frame 1 left - * 3: frame 1 right - * and so on. - */ +/** Called by decoder classes when they have a video frame ready */ void -VideoDecoder::emit (shared_ptr<const Film> film, shared_ptr<const ImageProxy> image, Frame decoder_frame) +VideoDecoder::emit(shared_ptr<const Film> film, shared_ptr<const ImageProxy> image, ContentTime time) { if (ignore ()) { return; @@ -66,14 +57,12 @@ VideoDecoder::emit (shared_ptr<const Film> film, shared_ptr<const ImageProxy> im auto const afr = _content->active_video_frame_rate(film); auto const vft = _content->video->frame_type(); - auto frame_time = ContentTime::from_frames (decoder_frame, afr); - /* Do some heuristics to try and spot the case where the user sets content to 3D * when it is not. We try to tell this by looking at the differences in time between * the first few frames. Real 3D content should have two frames for each timestamp. */ if (_frame_interval_checker) { - _frame_interval_checker->feed (frame_time, afr); + _frame_interval_checker->feed(time, afr); if (_frame_interval_checker->guess() == FrameIntervalChecker::PROBABLY_NOT_3D && vft == VideoFrameType::THREE_D) { boost::throw_exception ( DecodeError( @@ -91,94 +80,54 @@ VideoDecoder::emit (shared_ptr<const Film> film, shared_ptr<const ImageProxy> im } } - Frame frame; - Eyes eyes = Eyes::BOTH; - if (!_position) { - /* This is the first data we have received since initialisation or seek. Set - the position based on the frame that was given. After this first time - we just count frames, since (as with audio) it seems that ContentTimes - are unreliable from FFmpegDecoder. They are much better than audio times - but still we get the occasional one which is duplicated. In this case - ffmpeg seems to carry on regardless, processing the video frame as normal. - If we drop the frame with the duplicated timestamp we obviously lose sync. - */ - - if (vft == VideoFrameType::THREE_D_ALTERNATE) { - frame = decoder_frame / 2; - eyes = (decoder_frame % 2) ? Eyes::RIGHT : Eyes::LEFT; - } else { - frame = decoder_frame; - if (vft == VideoFrameType::THREE_D) { - auto j2k = dynamic_pointer_cast<const J2KImageProxy>(image); - /* At the moment only DCP decoders producers VideoFrameType::THREE_D, so only the J2KImageProxy - * knows which eye it is. - */ - if (j2k && j2k->eye()) { - eyes = j2k->eye().get() == dcp::Eye::LEFT ? Eyes::LEFT : Eyes::RIGHT; - } - } - } - - _position = ContentTime::from_frames (frame, afr); - } else { - if (vft == VideoFrameType::THREE_D) { - auto j2k = dynamic_pointer_cast<const J2KImageProxy>(image); - if (j2k && j2k->eye()) { - if (j2k->eye() == dcp::Eye::LEFT) { - frame = _position->frames_round(afr) + 1; - eyes = Eyes::LEFT; - } else { - frame = _position->frames_round(afr); - eyes = Eyes::RIGHT; - } - } else { - /* This should not happen; see above */ - frame = _position->frames_round(afr) + 1; - } - } else if (vft == VideoFrameType::THREE_D_ALTERNATE) { - DCPOMATIC_ASSERT (_last_emitted_eyes); - if (_last_emitted_eyes.get() == Eyes::RIGHT) { - frame = _position->frames_round(afr) + 1; - eyes = Eyes::LEFT; - } else { - frame = _position->frames_round(afr); - eyes = Eyes::RIGHT; - } - } else { - frame = _position->frames_round(afr) + 1; - } - } - switch (vft) { case VideoFrameType::TWO_D: + Data(ContentVideo(image, time, Eyes::BOTH, Part::WHOLE)); + break; case VideoFrameType::THREE_D: - Data (ContentVideo (image, frame, eyes, Part::WHOLE)); + { + auto eyes = Eyes::LEFT; + auto j2k = dynamic_pointer_cast<const J2KImageProxy>(image); + if (j2k && j2k->eye()) { + eyes = *j2k->eye() == dcp::Eye::LEFT ? Eyes::LEFT : Eyes::RIGHT; + } + + Data(ContentVideo(image, time, eyes, Part::WHOLE)); break; + } case VideoFrameType::THREE_D_ALTERNATE: { - Data (ContentVideo (image, frame, eyes, Part::WHOLE)); + Eyes eyes; + if (_last_emitted_eyes) { + eyes = _last_emitted_eyes.get() == Eyes::LEFT ? Eyes::RIGHT : Eyes::LEFT; + } else { + /* We don't know what eye this frame is, so just guess */ + auto frame = time.frames_round(_content->video_frame_rate().get_value_or(24)); + eyes = (frame % 2) ? Eyes::RIGHT : Eyes::LEFT; + } + Data(ContentVideo(image, time, eyes, Part::WHOLE)); _last_emitted_eyes = eyes; break; } case VideoFrameType::THREE_D_LEFT_RIGHT: - Data (ContentVideo (image, frame, Eyes::LEFT, Part::LEFT_HALF)); - Data (ContentVideo (image, frame, Eyes::RIGHT, Part::RIGHT_HALF)); + Data(ContentVideo(image, time, Eyes::LEFT, Part::LEFT_HALF)); + Data(ContentVideo(image, time, Eyes::RIGHT, Part::RIGHT_HALF)); break; case VideoFrameType::THREE_D_TOP_BOTTOM: - Data (ContentVideo (image, frame, Eyes::LEFT, Part::TOP_HALF)); - Data (ContentVideo (image, frame, Eyes::RIGHT, Part::BOTTOM_HALF)); + Data(ContentVideo(image, time, Eyes::LEFT, Part::TOP_HALF)); + Data(ContentVideo(image, time, Eyes::RIGHT, Part::BOTTOM_HALF)); break; case VideoFrameType::THREE_D_LEFT: - Data (ContentVideo (image, frame, Eyes::LEFT, Part::WHOLE)); + Data(ContentVideo(image, time, Eyes::LEFT, Part::WHOLE)); break; case VideoFrameType::THREE_D_RIGHT: - Data (ContentVideo (image, frame, Eyes::RIGHT, Part::WHOLE)); + Data(ContentVideo(image, time, Eyes::RIGHT, Part::WHOLE)); break; default: DCPOMATIC_ASSERT (false); } - _position = ContentTime::from_frames (frame, afr); + _position = time; } diff --git a/src/lib/video_decoder.h b/src/lib/video_decoder.h index f6ee17425..b609404c4 100644 --- a/src/lib/video_decoder.h +++ b/src/lib/video_decoder.h @@ -60,7 +60,7 @@ public: } void seek () override; - void emit (std::shared_ptr<const Film> film, std::shared_ptr<const ImageProxy>, Frame frame); + void emit(std::shared_ptr<const Film> film, std::shared_ptr<const ImageProxy>, dcpomatic::ContentTime time); boost::signals2::signal<void (ContentVideo)> Data; diff --git a/src/lib/video_mxf_decoder.cc b/src/lib/video_mxf_decoder.cc index 40d3a461a..9f451297b 100644 --- a/src/lib/video_mxf_decoder.cc +++ b/src/lib/video_mxf_decoder.cc @@ -76,18 +76,18 @@ VideoMXFDecoder::pass () video->emit ( film(), std::make_shared<J2KImageProxy>(_mono_reader->get_frame(frame), _size, AV_PIX_FMT_XYZ12LE, optional<int>()), - frame + _next ); } else { video->emit ( film(), std::make_shared<J2KImageProxy>(_stereo_reader->get_frame(frame), _size, dcp::Eye::LEFT, AV_PIX_FMT_XYZ12LE, optional<int>()), - frame + _next ); video->emit ( film(), std::make_shared<J2KImageProxy>(_stereo_reader->get_frame(frame), _size, dcp::Eye::RIGHT, AV_PIX_FMT_XYZ12LE, optional<int>()), - frame + _next ); } diff --git a/src/lib/writer.h b/src/lib/writer.h index efb6a17d8..1cd278221 100644 --- a/src/lib/writer.h +++ b/src/lib/writer.h @@ -34,6 +34,7 @@ #include "exception_store.h" #include "font_id_map.h" #include "player_text.h" +#include "text_type.h" #include "weak_film.h" #include <dcp/atmos_frame.h> #include <boost/thread.hpp> diff --git a/src/lib/wscript b/src/lib/wscript index 0d61d7a69..5bae3e0d1 100644 --- a/src/lib/wscript +++ b/src/lib/wscript @@ -59,6 +59,7 @@ sources = """ content_factory.cc combine_dcp_job.cc copy_dcp_details_to_film.cc + cpu_j2k_encoder_thread.cc create_cli.cc crop.cc cross_common.cc @@ -138,6 +139,8 @@ sources = """ job.cc job_manager.cc j2k_encoder.cc + j2k_encoder_thread.cc + j2k_sync_encoder_thread.cc json_server.cc kdm_cli.cc kdm_recipient.cc @@ -163,6 +166,7 @@ sources = """ referenced_reel_asset.cc release_notes.cc render_text.cc + remote_j2k_encoder_thread.cc resampler.cc resolution.cc rgba.cc @@ -244,6 +248,9 @@ def build(bld): if bld.env.TARGET_LINUX: obj.uselib += ' POLKIT' + if bld.env.ENABLE_GROK: + obj.source += ' grok_j2k_encoder_thread.cc' + if bld.env.TARGET_WINDOWS_64 or bld.env.TARGET_WINDOWS_32: obj.uselib += ' WINSOCK2 DBGHELP SHLWAPI MSWSOCK BOOST_LOCALE SETUPAPI OLE32 UUID' obj.source += ' cross_windows.cc' diff --git a/src/tools/dcpomatic.cc b/src/tools/dcpomatic.cc index 459ce341c..f65037c2f 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_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 f0863431a..d7deeec41 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; @@ -257,15 +245,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) { @@ -383,19 +362,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); @@ -500,23 +468,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/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/dcp_panel.cc b/src/wx/dcp_panel.cc index f4ba74cde..d81e7bf95 100644 --- a/src/wx/dcp_panel.cc +++ b/src/wx/dcp_panel.cc @@ -97,10 +97,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); @@ -139,9 +135,6 @@ DCPPanel::DCPPanel(wxNotebook* n, shared_ptr<Film> film, FilmViewer& viewer) _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)); - for (auto i: DCPContentType::all()) { _dcp_content_type->add(i->pretty_name()); } @@ -155,7 +148,7 @@ DCPPanel::DCPPanel(wxNotebook* n, shared_ptr<Film> film, FilmViewer& viewer) _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 +234,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; @@ -511,6 +495,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: @@ -953,6 +938,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 +949,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 +985,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; } 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/dkdm_dialog.cc b/src/wx/dkdm_dialog.cc index 26f521573..8da3090c5 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/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/full_config_dialog.cc b/src/wx/full_config_dialog.cc index 4c26d038c..66dbae63e 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/video_panel.cc b/src/wx/video_panel.cc index b27db2547..0280f16a0 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, @@ -201,6 +193,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); @@ -221,7 +216,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)); @@ -242,12 +236,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; @@ -456,15 +444,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); @@ -592,15 +571,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()) { @@ -609,7 +580,7 @@ VideoPanel::setup_sensitivity () } } - bool const enable = !_reference->GetValue() && any_use; + bool const enable = !reference && any_use; if (!enable) { _frame_type->wrapped()->Enable (false); @@ -682,23 +653,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..7dbd214c6 100644 --- a/src/wx/wscript +++ b/src/wx/wscript @@ -53,6 +53,7 @@ sources = """ controls.cc credentials_download_certificate_panel.cc custom_scale_dialog.cc + dcp_referencing_dialog.cc dcp_panel.cc dcp_text_track_dialog.cc dcpomatic_button.cc diff --git a/src/wx/wx_util.h b/src/wx/wx_util.h index 66b01640c..dcf5bbc18 100644 --- a/src/wx/wx_util.h +++ b/src/wx/wx_util.h @@ -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 diff --git a/test/client_server_test.cc b/test/client_server_test.cc index 596668aae..1bfa4c5a6 100644 --- a/test/client_server_test.cc +++ b/test/client_server_test.cc @@ -20,20 +20,18 @@ /** @file test/client_server_test.cc - * @brief Test the server class. + * @brief Test the remote encoding code. * @ingroup feature - * - * Create a test image and then encode it using the standard mechanism - * and also using a EncodeServer object running on localhost. Compare the resulting - * encoded data to check that they are the same. */ +#include "lib/content_factory.h" #include "lib/cross.h" #include "lib/dcp_video.h" #include "lib/dcpomatic_log.h" #include "lib/encode_server.h" #include "lib/encode_server_description.h" +#include "lib/encode_server_finder.h" #include "lib/file_log.h" #include "lib/image.h" #include "lib/j2k_image_proxy.h" @@ -51,6 +49,7 @@ using std::weak_ptr; using boost::thread; using boost::optional; using dcp::ArrayData; +using namespace dcpomatic; void @@ -105,7 +104,7 @@ BOOST_AUTO_TEST_CASE (client_server_test_rgb) ColourConversion(), VideoRange::FULL, weak_ptr<Content>(), - optional<Frame>(), + optional<ContentTime>(), false ); @@ -184,7 +183,7 @@ BOOST_AUTO_TEST_CASE (client_server_test_yuv) ColourConversion(), VideoRange::FULL, weak_ptr<Content>(), - optional<Frame>(), + optional<ContentTime>(), false ); @@ -250,7 +249,7 @@ BOOST_AUTO_TEST_CASE (client_server_test_j2k) ColourConversion(), VideoRange::FULL, weak_ptr<Content>(), - optional<Frame>(), + optional<ContentTime>(), false ); @@ -275,7 +274,7 @@ BOOST_AUTO_TEST_CASE (client_server_test_j2k) PresetColourConversion::all().front().conversion, VideoRange::FULL, weak_ptr<Content>(), - optional<Frame>(), + optional<ContentTime>(), false ); @@ -315,3 +314,22 @@ BOOST_AUTO_TEST_CASE (client_server_test_j2k) } +BOOST_AUTO_TEST_CASE(real_encode_with_server) +{ + auto content = content_factory(TestPaths::private_data() / "dolby_aurora.vob"); + auto film = new_test_film2("real_encode_with_server", content); + + EncodeServerFinder::instance(); + + EncodeServer server(true, 4); + thread server_thread(boost::bind(&EncodeServer::run, &server)); + + make_and_verify_dcp(film); + + server.stop(); + server_thread.join(); + + BOOST_CHECK(server.frames_encoded() > 0); + EncodeServerFinder::drop(); +} + diff --git a/test/config_test.cc b/test/config_test.cc index 103a6a585..00adb7b09 100644 --- a/test/config_test.cc +++ b/test/config_test.cc @@ -38,7 +38,7 @@ rewrite_bad_config (string filename, string extra_line) { using namespace boost::filesystem; - auto base = path("build/test/bad_config/2.16"); + auto base = path("build/test/bad_config/2.18"); auto file = base / filename; boost::system::error_code ec; @@ -73,47 +73,49 @@ BOOST_AUTO_TEST_CASE (config_backup_test) */ Config::instance(); - BOOST_CHECK ( boost::filesystem::exists("build/test/bad_config/2.16/config.xml.1")); - BOOST_CHECK (dcp::file_to_string("build/test/bad_config/2.16/config.xml.1") == first_write_xml); - BOOST_CHECK (!boost::filesystem::exists("build/test/bad_config/2.16/config.xml.2")); - BOOST_CHECK (!boost::filesystem::exists("build/test/bad_config/2.16/config.xml.3")); - BOOST_CHECK (!boost::filesystem::exists("build/test/bad_config/2.16/config.xml.4")); + boost::filesystem::path const prefix = "build/test/bad_config/2.18"; + + BOOST_CHECK(boost::filesystem::exists(prefix / "config.xml.1")); + BOOST_CHECK(dcp::file_to_string(prefix / "config.xml.1") == first_write_xml); + BOOST_CHECK(!boost::filesystem::exists(prefix / "config.xml.2")); + BOOST_CHECK(!boost::filesystem::exists(prefix / "config.xml.3")); + BOOST_CHECK(!boost::filesystem::exists(prefix / "config.xml.4")); Config::drop(); auto const second_write_xml = rewrite_bad_config("config.xml", "second write"); Config::instance(); - BOOST_CHECK ( boost::filesystem::exists("build/test/bad_config/2.16/config.xml.1")); - BOOST_CHECK (dcp::file_to_string("build/test/bad_config/2.16/config.xml.1") == first_write_xml); - BOOST_CHECK ( boost::filesystem::exists("build/test/bad_config/2.16/config.xml.2")); - BOOST_CHECK (dcp::file_to_string("build/test/bad_config/2.16/config.xml.2") == second_write_xml); - BOOST_CHECK (!boost::filesystem::exists("build/test/bad_config/2.16/config.xml.3")); - BOOST_CHECK (!boost::filesystem::exists("build/test/bad_config/2.16/config.xml.4")); + BOOST_CHECK(boost::filesystem::exists(prefix / "config.xml.1")); + BOOST_CHECK(dcp::file_to_string(prefix / "config.xml.1") == first_write_xml); + BOOST_CHECK(boost::filesystem::exists(prefix / "config.xml.2")); + BOOST_CHECK(dcp::file_to_string(prefix / "config.xml.2") == second_write_xml); + BOOST_CHECK(!boost::filesystem::exists(prefix / "config.xml.3")); + BOOST_CHECK(!boost::filesystem::exists(prefix / "config.xml.4")); Config::drop(); auto const third_write_xml = rewrite_bad_config("config.xml", "third write"); Config::instance(); - BOOST_CHECK ( boost::filesystem::exists("build/test/bad_config/2.16/config.xml.1")); - BOOST_CHECK (dcp::file_to_string("build/test/bad_config/2.16/config.xml.1") == first_write_xml); - BOOST_CHECK ( boost::filesystem::exists("build/test/bad_config/2.16/config.xml.2")); - BOOST_CHECK (dcp::file_to_string("build/test/bad_config/2.16/config.xml.2") == second_write_xml); - BOOST_CHECK ( boost::filesystem::exists("build/test/bad_config/2.16/config.xml.3")); - BOOST_CHECK (dcp::file_to_string("build/test/bad_config/2.16/config.xml.3") == third_write_xml); - BOOST_CHECK (!boost::filesystem::exists("build/test/bad_config/2.16/config.xml.4")); + BOOST_CHECK(boost::filesystem::exists(prefix / "config.xml.1")); + BOOST_CHECK(dcp::file_to_string(prefix / "config.xml.1") == first_write_xml); + BOOST_CHECK(boost::filesystem::exists(prefix / "config.xml.2")); + BOOST_CHECK(dcp::file_to_string(prefix / "config.xml.2") == second_write_xml); + BOOST_CHECK(boost::filesystem::exists(prefix / "config.xml.3")); + BOOST_CHECK(dcp::file_to_string(prefix / "config.xml.3") == third_write_xml); + BOOST_CHECK(!boost::filesystem::exists(prefix / "config.xml.4")); Config::drop(); auto const fourth_write_xml = rewrite_bad_config("config.xml", "fourth write"); Config::instance(); - BOOST_CHECK (boost::filesystem::exists("build/test/bad_config/2.16/config.xml.1")); - BOOST_CHECK (dcp::file_to_string("build/test/bad_config/2.16/config.xml.1") == first_write_xml); - BOOST_CHECK (boost::filesystem::exists("build/test/bad_config/2.16/config.xml.2")); - BOOST_CHECK (dcp::file_to_string("build/test/bad_config/2.16/config.xml.2") == second_write_xml); - BOOST_CHECK (boost::filesystem::exists("build/test/bad_config/2.16/config.xml.3")); - BOOST_CHECK (dcp::file_to_string("build/test/bad_config/2.16/config.xml.3") == third_write_xml); - BOOST_CHECK (boost::filesystem::exists("build/test/bad_config/2.16/config.xml.4")); - BOOST_CHECK (dcp::file_to_string("build/test/bad_config/2.16/config.xml.4") == fourth_write_xml); + BOOST_CHECK(boost::filesystem::exists(prefix / "config.xml.1")); + BOOST_CHECK(dcp::file_to_string(prefix / "config.xml.1") == first_write_xml); + BOOST_CHECK(boost::filesystem::exists(prefix / "config.xml.2")); + BOOST_CHECK(dcp::file_to_string(prefix / "config.xml.2") == second_write_xml); + BOOST_CHECK(boost::filesystem::exists(prefix / "config.xml.3")); + BOOST_CHECK(dcp::file_to_string(prefix / "config.xml.3") == third_write_xml); + BOOST_CHECK(boost::filesystem::exists(prefix / "config.xml.4")); + BOOST_CHECK(dcp::file_to_string(prefix / "config.xml.4") == fourth_write_xml); } @@ -124,7 +126,7 @@ BOOST_AUTO_TEST_CASE (config_backup_with_link_test) ConfigRestorer cr; auto base = path("build/test/bad_config"); - auto version = base / "2.16"; + auto version = base / "2.18"; Config::override_path = base; Config::drop(); @@ -163,7 +165,8 @@ BOOST_AUTO_TEST_CASE (config_write_utf8_test) } -BOOST_AUTO_TEST_CASE (config_upgrade_test) +/* 2.14 -> 2.18 */ +BOOST_AUTO_TEST_CASE (config_upgrade_test1) { ConfigRestorer cr; @@ -185,12 +188,49 @@ BOOST_AUTO_TEST_CASE (config_upgrade_test) check_xml (dir / "cinemas.xml", "test/data/2.14.cinemas.xml", {}); #ifdef DCPOMATIC_WINDOWS /* This file has the windows path for dkdm_recipients.xml (with backslashes) */ - check_xml (dir / "2.16" / "config.xml", "test/data/2.16.config.windows.xml", {}); + check_xml(dir / "2.18" / "config.xml", "test/data/2.18.config.windows.xml", {}); +#else + check_xml(dir / "2.18" / "config.xml", "test/data/2.18.config.xml", {}); +#endif + /* cinemas.xml is not copied into 2.18 as its format has not changed */ + BOOST_REQUIRE (!boost::filesystem::exists(dir / "2.18" / "cinemas.xml")); +} + + +/* 2.16 -> 2.18 */ +BOOST_AUTO_TEST_CASE (config_upgrade_test2) +{ + ConfigRestorer cr; + + boost::filesystem::path dir = "build/test/config_upgrade_test"; + Config::override_path = dir; + Config::drop (); + boost::filesystem::remove_all (dir); + boost::filesystem::create_directories (dir); + +#ifdef DCPOMATIC_WINDOWS + boost::filesystem::copy_file("test/data/2.16.config.windows.xml", dir / "config.xml"); +#else + boost::filesystem::copy_file("test/data/2.16.config.xml", dir / "config.xml"); +#endif + boost::filesystem::copy_file("test/data/2.14.cinemas.xml", dir / "cinemas.xml"); + Config::instance(); + try { + /* This will fail to write cinemas.xml since the link is to a non-existent directory */ + Config::instance()->write(); + } catch (...) {} + + check_xml(dir / "cinemas.xml", "test/data/2.14.cinemas.xml", {}); +#ifdef DCPOMATIC_WINDOWS + /* This file has the windows path for dkdm_recipients.xml (with backslashes) */ + check_xml(dir / "2.18" / "config.xml", "test/data/2.18.config.windows.xml", {}); + check_xml(dir / "config.xml", "test/data/2.16.config.windows.xml", {}); #else - check_xml (dir / "2.16" / "config.xml", "test/data/2.16.config.xml", {}); + check_xml(dir / "2.18" / "config.xml", "test/data/2.18.config.xml", {}); + check_xml(dir / "config.xml", "test/data/2.16.config.xml", {}); #endif - /* cinemas.xml is not copied into 2.16 as its format has not changed */ - BOOST_REQUIRE (!boost::filesystem::exists(dir / "2.16" / "cinemas.xml")); + /* cinemas.xml is not copied into 2.18 as its format has not changed */ + BOOST_REQUIRE (!boost::filesystem::exists(dir / "2.18" / "cinemas.xml")); } @@ -206,13 +246,13 @@ BOOST_AUTO_TEST_CASE (config_keep_cinemas_if_making_new_config) Config::instance()->write(); - Config::instance()->add_cinema(make_shared<Cinema>("My Great Cinema", vector<string>(), "", 0, 0)); + Config::instance()->add_cinema(make_shared<Cinema>("My Great Cinema", vector<string>(), "")); Config::instance()->write(); boost::filesystem::copy_file (dir / "cinemas.xml", dir / "backup_for_test.xml"); Config::drop (); - boost::filesystem::remove (dir / "2.16" / "config.xml"); + boost::filesystem::remove (dir / "2.18" / "config.xml"); Config::instance(); check_text_file(dir / "backup_for_test.xml", dir / "cinemas.xml"); @@ -234,7 +274,7 @@ BOOST_AUTO_TEST_CASE(keep_config_if_cinemas_fail_to_load) auto const cinemas = dir / "cinemas.xml"; /* Back things up */ - boost::filesystem::copy_file(dir / "2.16" / "config.xml", dir / "config_backup_for_test.xml"); + boost::filesystem::copy_file(dir / "2.18" / "config.xml", dir / "config_backup_for_test.xml"); boost::filesystem::copy_file(cinemas, dir / "cinemas_backup_for_test.xml"); /* Corrupt the cinemas */ @@ -245,7 +285,7 @@ BOOST_AUTO_TEST_CASE(keep_config_if_cinemas_fail_to_load) Config::instance(); /* We should have a new cinemas.xml and the old config.xml */ - check_text_file(dir / "2.16" / "config.xml", dir / "config_backup_for_test.xml"); + check_text_file(dir / "2.18" / "config.xml", dir / "config_backup_for_test.xml"); check_text_file(cinemas, dir / "cinemas_backup_for_test.xml"); } diff --git a/test/content_test.cc b/test/content_test.cc index 5ab714b47..dce7643dc 100644 --- a/test/content_test.cc +++ b/test/content_test.cc @@ -154,7 +154,7 @@ BOOST_AUTO_TEST_CASE (content_test6) film->set_audio_channels(16); make_and_verify_dcp (film); - check_dcp (TestPaths::private_data() / "fha", film); + check_dcp (TestPaths::private_data() / "v2.18.x" / "fha", film); cl.run (); } diff --git a/test/data b/test/data -Subproject 4cb08962ba07e99c442cb12091c0347d84d8fd8 +Subproject ddf878730354cdec8a802a59543591f6f943f5c diff --git a/test/disk_writer_test.cc b/test/disk_writer_test.cc index 553adcae7..ff39eaaea 100644 --- a/test/disk_writer_test.cc +++ b/test/disk_writer_test.cc @@ -228,110 +228,3 @@ BOOST_AUTO_TEST_CASE (disk_writer_test3) cl.run(); } - - -BOOST_AUTO_TEST_CASE (osx_drive_identification_test) -{ - vector<OSXDisk> disks; - - auto disk = [&disks](string mount_point, string media_path, bool whole, std::vector<boost::filesystem::path> mount_points) - { - auto mp = analyse_osx_media_path (media_path); - if (mp) { - disks.push_back({mount_point, {}, {}, *mp, whole, mount_points, 0}); - } - }; - - auto find_unmounted = [](vector<OSXDisk> disks) { - auto drives = osx_disks_to_drives (disks); - vector<Drive> unmounted; - std::copy_if (drives.begin(), drives.end(), std::back_inserter(unmounted), [](Drive const& drive) { return !drive.mounted(); }); - return unmounted; - }; - - disk("/dev/disk4s1", "IODeviceTree:/arm-io@10F00000/apcie@90000000/pci-bridge1@1/pcie-xhci@0/@7:1", false, {}); - disk("/dev/disk4", "IODeviceTree:/arm-io@10F00000/apcie@90000000/pci-bridge1@1/pcie-xhci@0/@7:0", true, {}); - disk("/dev/disk0", "IODeviceTree:/arm-io@10F00000/ans@77400000/iop-ans-nub/AppleANS3NVMeController/@1:0", true, {}); - disk("/dev/disk0s1", "IODeviceTree:/arm-io@10F00000/ans@77400000/iop-ans-nub/AppleANS3NVMeController/@1:1", false, {}); - disk("/dev/disk0s2", "IODeviceTree:/arm-io@10F00000/ans@77400000/iop-ans-nub/AppleANS3NVMeController/@1:2", false, {}); - disk("/dev/disk0s3", "IODeviceTree:/arm-io@10F00000/ans@77400000/iop-ans-nub/AppleANS3NVMeController/@1:3", false, {}); - disk("/dev/disk1", "IOService:/AppleARMPE/arm-io@10F00000/AppleT810xIO/ans@77400000/AppleASCWrapV4/iop-ans-nub/RTBuddyV2/RTBuddyService/AppleANS3NVMeController/NS_01@1/IOBlockStorageDriver/APPLE SSD AP0512Q Media/IOGUIDPartitionScheme/iBootSystemContainer@1/AppleAPFSContainerScheme/AppleAPFSMedia", true, {}); - disk("/dev/disk2", "IOService:/AppleARMPE/arm-io@10F00000/AppleT810xIO/ans@77400000/AppleASCWrapV4/iop-ans-nub/RTBuddyV2/RTBuddyService/AppleANS3NVMeController/NS_01@1/IOBlockStorageDriver/APPLE SSD AP0512Q Media/IOGUIDPartitionScheme/RecoveryOSContainer@3/AppleAPFSContainerScheme/AppleAPFSMedia", true, {}); - disk("/dev/disk3", "IOService:/AppleARMPE/arm-io@10F00000/AppleT810xIO/ans@77400000/AppleASCWrapV4/iop-ans-nub/RTBuddyV2/RTBuddyService/AppleANS3NVMeController/NS_01@1/IOBlockStorageDriver/APPLE SSD AP0512Q Media/IOGUIDPartitionScheme/Container@2/AppleAPFSContainerScheme/AppleAPFSMedia", false, {}); - disk("/dev/disk1s1", "IOService:/AppleARMPE/arm-io@10F00000/AppleT810xIO/ans@77400000/AppleASCWrapV4/iop-ans-nub/RTBuddyV2/RTBuddyService/AppleANS3NVMeController/NS_01@1/IOBlockStorageDriver/APPLE SSD AP0512Q Media/IOGUIDPartitionScheme/iBootSystemContainer@1/AppleAPFSContainerScheme/AppleAPFSMedia/AppleAPFSContainer/iSCPreboot@1", false, {}); - disk("/dev/disk1s2", "IOService:/AppleARMPE/arm-io@10F00000/AppleT810xIO/ans@77400000/AppleASCWrapV4/iop-ans-nub/RTBuddyV2/RTBuddyService/AppleANS3NVMeController/NS_01@1/IOBlockStorageDriver/APPLE SSD AP0512Q Media/IOGUIDPartitionScheme/iBootSystemContainer@1/AppleAPFSContainerScheme/AppleAPFSMedia/AppleAPFSContainer/xART@2", false, {}); - disk("/dev/disk1s3", "IOService:/AppleARMPE/arm-io@10F00000/AppleT810xIO/ans@77400000/AppleASCWrapV4/iop-ans-nub/RTBuddyV2/RTBuddyService/AppleANS3NVMeController/NS_01@1/IOBlockStorageDriver/APPLE SSD AP0512Q Media/IOGUIDPartitionScheme/iBootSystemContainer@1/AppleAPFSContainerScheme/AppleAPFSMedia/AppleAPFSContainer/Hardware@3", false, {}); - disk("/dev/disk1s4", "IOService:/AppleARMPE/arm-io@10F00000/AppleT810xIO/ans@77400000/AppleASCWrapV4/iop-ans-nub/RTBuddyV2/RTBuddyService/AppleANS3NVMeController/NS_01@1/IOBlockStorageDriver/APPLE SSD AP0512Q Media/IOGUIDPartitionScheme/iBootSystemContainer@1/AppleAPFSContainerScheme/AppleAPFSMedia/AppleAPFSContainer/Recovery@4", false, {}); - disk("/dev/disk2s1", "IOService:/AppleARMPE/arm-io@10F00000/AppleT810xIO/ans@77400000/AppleASCWrapV4/iop-ans-nub/RTBuddyV2/RTBuddyService/AppleANS3NVMeController/NS_01@1/IOBlockStorageDriver/APPLE SSD AP0512Q Media/IOGUIDPartitionScheme/RecoveryOSContainer@3/AppleAPFSContainerScheme/AppleAPFSMedia/AppleAPFSContainer/Recovery@1", false, {}); - disk("/dev/disk2s2", "IOService:/AppleARMPE/arm-io@10F00000/AppleT810xIO/ans@77400000/AppleASCWrapV4/iop-ans-nub/RTBuddyV2/RTBuddyService/AppleANS3NVMeController/NS_01@1/IOBlockStorageDriver/APPLE SSD AP0512Q Media/IOGUIDPartitionScheme/RecoveryOSContainer@3/AppleAPFSContainerScheme/AppleAPFSMedia/AppleAPFSContainer/Update@2", false, {}); - disk("/dev/disk3s1", "IOService:/AppleARMPE/arm-io@10F00000/AppleT810xIO/ans@77400000/AppleASCWrapV4/iop-ans-nub/RTBuddyV2/RTBuddyService/AppleANS3NVMeController/NS_01@1/IOBlockStorageDriver/APPLE SSD AP0512Q Media/IOGUIDPartitionScheme/Container@2/AppleAPFSContainerScheme/AppleAPFSMedia/AppleAPFSContainer/Macintosh HD@1", false, {}); - disk("/dev/disk3s4", "IOService:/AppleARMPE/arm-io@10F00000/AppleT810xIO/ans@77400000/AppleASCWrapV4/iop-ans-nub/RTBuddyV2/RTBuddyService/AppleANS3NVMeController/NS_01@1/IOBlockStorageDriver/APPLE SSD AP0512Q Media/IOGUIDPartitionScheme/Container@2/AppleAPFSContainerScheme/AppleAPFSMedia/AppleAPFSContainer/Update@4", false, {"/System/Volumes/Update"}); - disk("/dev/disk3s5", "IOService:/AppleARMPE/arm-io@10F00000/AppleT810xIO/ans@77400000/AppleASCWrapV4/iop-ans-nub/RTBuddyV2/RTBuddyService/AppleANS3NVMeController/NS_01@1/IOBlockStorageDriver/APPLE SSD AP0512Q Media/IOGUIDPartitionScheme/Container@2/AppleAPFSContainerScheme/AppleAPFSMedia/AppleAPFSContainer/Data@5", false, {"/System/Volumes/Data"}); - disk("/dev/disk3s2", "IOService:/AppleARMPE/arm-io@10F00000/AppleT810xIO/ans@77400000/AppleASCWrapV4/iop-ans-nub/RTBuddyV2/RTBuddyService/AppleANS3NVMeController/NS_01@1/IOBlockStorageDriver/APPLE SSD AP0512Q Media/IOGUIDPartitionScheme/Container@2/AppleAPFSContainerScheme/AppleAPFSMedia/AppleAPFSContainer/Preboot@2", false, {"/System/Volumes/Preboot"}); - disk("/dev/disk3s3", "IOService:/AppleARMPE/arm-io@10F00000/AppleT810xIO/ans@77400000/AppleASCWrapV4/iop-ans-nub/RTBuddyV2/RTBuddyService/AppleANS3NVMeController/NS_01@1/IOBlockStorageDriver/APPLE SSD AP0512Q Media/IOGUIDPartitionScheme/Container@2/AppleAPFSContainerScheme/AppleAPFSMedia/AppleAPFSContainer/Recovery@3", false, {}); - disk("/dev/disk3s6", "IOService:/AppleARMPE/arm-io@10F00000/AppleT810xIO/ans@77400000/AppleASCWrapV4/iop-ans-nub/RTBuddyV2/RTBuddyService/AppleANS3NVMeController/NS_01@1/IOBlockStorageDriver/APPLE SSD AP0512Q Media/IOGUIDPartitionScheme/Container@2/AppleAPFSContainerScheme/AppleAPFSMedia/AppleAPFSContainer/VM@6", false, {"/System/Volumes/VM"}); - disk("/dev/disk3s1s1", "IOService:/AppleARMPE/arm-io@10F00000/AppleT810xIO/ans@77400000/AppleASCWrapV4/iop-ans-nub/RTBuddyV2/RTBuddyService/AppleANS3NVMeController/NS_01@1/IOBlockStorageDriver/APPLE SSD AP0512Q Media/IOGUIDPartitionScheme/Container@2/AppleAPFSContainerScheme/AppleAPFSMedia/AppleAPFSContainer/Macintosh HD@1/com.apple.os.update-EA882DCA7A28EBA0A6E94689836BB10D77D84D1AEE2468E17775A447AA815278@1", false, {"/"}); - - vector<Drive> writeable = find_unmounted (disks); - BOOST_REQUIRE_EQUAL (writeable.size(), 1U); - BOOST_CHECK_EQUAL (writeable[0].device(), "/dev/disk4"); - - disks.clear (); - disk("/dev/disk4s1", "IOService:/IOResources/IOHDIXController/IOHDIXHDDriveOutKernel@0/IODiskImageBlockStorageDeviceOutKernel/IOBlockStorageDriver/Apple UDIF read-only compressed (zlib) Media/IOGUIDPartitionScheme/disk image@1", false, {}); - disk("/dev/disk4", "IOService:/IOResources/IOHDIXController/IOHDIXHDDriveOutKernel@0/IODiskImageBlockStorageDeviceOutKernel/IOBlockStorageDriver/Apple UDIF read-only compressed (zlib) Media", true, {}); - disk("/dev/disk3s1", "IODeviceTree:/PCI0@0/XHC1@14/@2:1", false, {}); - disk("/dev/disk3", "IODeviceTree:/PCI0@0/XHC1@14/@2:0", true, {}); - disk("/dev/disk0", "IODeviceTree:/PCI0@0/SATA@1F,2/PRT1@1/PMP@0/@0:0", true, {}); - disk("/dev/disk0s1", "IODeviceTree:/PCI0@0/SATA@1F,2/PRT1@1/PMP@0/@0:1", false, {}); - disk("/dev/disk0s2", "IODeviceTree:/PCI0@0/SATA@1F,2/PRT1@1/PMP@0/@0:2", false, {"/Volumes/Macintosh HD"}); - disk("/dev/disk0s3", "IODeviceTree:/PCI0@0/SATA@1F,2/PRT1@1/PMP@0/@0:3", false, {}); - disk("/dev/disk0s4", "IODeviceTree:/PCI0@0/SATA@1F,2/PRT1@1/PMP@0/@0:4", false, {}); - disk("/dev/disk0s5", "IODeviceTree:/PCI0@0/SATA@1F,2/PRT1@1/PMP@0/@0:5", false, {"/Volumes/High Sierra"}); - disk("/dev/disk0s6", "IODeviceTree:/PCI0@0/SATA@1F,2/PRT1@1/PMP@0/@0:6", false, {}); - disk("/dev/disk0s7", "IODeviceTree:/PCI0@0/SATA@1F,2/PRT1@1/PMP@0/@0:7", false, {"/Volumes/Recovery HD"}); - disk("/dev/disk1", "IOService:/AppleACPIPlatformExpert/PCI0@0/AppleACPIPCI/SATA@1F,2/AppleIntelPchSeriesAHCI/PRT1@1/IOAHCIDevice@0/AppleAHCIDiskDriver/IOAHCIBlockStorageDevice/IOBlockStorageDriver/APPLE HDD ST500LM012 Media/IOGUIDPartitionScheme/Untitled 3@3/AppleAPFSContainerScheme/AppleAPFSMedia", true, {}); - disk("/dev/disk", "IOService:/AppleACPIPlatformExpert/PCI0@0/AppleACPIPCI/SATA@1F,2/AppleIntelPchSeriesAHCI/PRT1@1/IOAHCIDevice@0/AppleAHCIDiskDriver/IOAHCIBlockStorageDevice/IOBlockStorageDriver/APPLE HDD ST500LM012 Media/IOGUIDPartitionScheme/Untitled 4@4/AppleAPFSContainerScheme/AppleAPFSMedia", true, {}); - disk("/dev/disk1s1", "IOService:/AppleACPIPlatformExpert/PCI0@0/AppleACPIPCI/SATA@1F,2/AppleIntelPchSeriesAHCI/PRT1@1/IOAHCIDevice@0/AppleAHCIDiskDriver/IOAHCIBlockStorageDevice/IOBlockStorageDriver/APPLE HDD ST500LM012 Media/IOGUIDPartitionScheme/Untitled 3@3/AppleAPFSContainerScheme/AppleAPFSMedia/AppleAPFSContainer/Untitled - Data@1", false, {"/Volumes/Untitled - Data"}); - disk("/dev/disk1s2", "IOService:/AppleACPIPlatformExpert/PCI0@0/AppleACPIPCI/SATA@1F,2/AppleIntelPchSeriesAHCI/PRT1@1/IOAHCIDevice@0/AppleAHCIDiskDriver/IOAHCIBlockStorageDevice/IOBlockStorageDriver/APPLE HDD ST500LM012 Media/IOGUIDPartitionScheme/Untitled 3@3/AppleAPFSContainerScheme/AppleAPFSMedia/AppleAPFSContainer/Preboot@2", false, {}); - disk("/dev/disk1s3", "IOService:/AppleACPIPlatformExpert/PCI0@0/AppleACPIPCI/SATA@1F,2/AppleIntelPchSeriesAHCI/PRT1@1/IOAHCIDevice@0/AppleAHCIDiskDriver/IOAHCIBlockStorageDevice/IOBlockStorageDriver/APPLE HDD ST500LM012 Media/IOGUIDPartitionScheme/Untitled 3@3/AppleAPFSContainerScheme/AppleAPFSMedia/AppleAPFSContainer/Recovery@3", false, {}); - disk("/dev/disk1s4", "IOService:/AppleACPIPlatformExpert/PCI0@0/AppleACPIPCI/SATA@1F,2/AppleIntelPchSeriesAHCI/PRT1@1/IOAHCIDevice@0/AppleAHCIDiskDriver/IOAHCIBlockStorageDevice/IOBlockStorageDriver/APPLE HDD ST500LM012 Media/IOGUIDPartitionScheme/Untitled 3@3/AppleAPFSContainerScheme/AppleAPFSMedia/AppleAPFSContainer/VM@4", false, {}); - disk("/dev/disk1s5", "IOService:/AppleACPIPlatformExpert/PCI0@0/AppleACPIPCI/SATA@1F,2/AppleIntelPchSeriesAHCI/PRT1@1/IOAHCIDevice@0/AppleAHCIDiskDriver/IOAHCIBlockStorageDevice/IOBlockStorageDriver/APPLE HDD ST500LM012 Media/IOGUIDPartitionScheme/Untitled 3@3/AppleAPFSContainerScheme/AppleAPFSMedia/AppleAPFSContainer/Untitled@5", false, {"/Volumes/Untitled"}); - disk("/dev/disk2s1", "IOService:/AppleACPIPlatformExpert/PCI0@0/AppleACPIPCI/SATA@1F,2/AppleIntelPchSeriesAHCI/PRT1@1/IOAHCIDevice@0/AppleAHCIDiskDriver/IOAHCIBlockStorageDevice/IOBlockStorageDriver/APPLE HDD ST500LM012 Media/IOGUIDPartitionScheme/Untitled 4@4/AppleAPFSContainerScheme/AppleAPFSMedia/AppleAPFSContainer/Catalina - Data@1", false, {}); - disk("/dev/disk2s2", "IOService:/AppleACPIPlatformExpert/PCI0@0/AppleACPIPCI/SATA@1F,2/AppleIntelPchSeriesAHCI/PRT1@1/IOAHCIDevice@0/AppleAHCIDiskDriver/IOAHCIBlockStorageDevice/IOBlockStorageDriver/APPLE HDD ST500LM012 Media/IOGUIDPartitionScheme/Untitled 4@4/AppleAPFSContainerScheme/AppleAPFSMedia/AppleAPFSContainer/Preboot@2", false, {}); - disk("/dev/disk2s3", "IOService:/AppleACPIPlatformExpert/PCI0@0/AppleACPIPCI/SATA@1F,2/AppleIntelPchSeriesAHCI/PRT1@1/IOAHCIDevice@0/AppleAHCIDiskDriver/IOAHCIBlockStorageDevice/IOBlockStorageDriver/APPLE HDD ST500LM012 Media/IOGUIDPartitionScheme/Untitled 4@4/AppleAPFSContainerScheme/AppleAPFSMedia/AppleAPFSContainer/Recovery@3", false, {}); - disk("/dev/disk2s4", "IOService:/AppleACPIPlatformExpert/PCI0@0/AppleACPIPCI/SATA@1F,2/AppleIntelPchSeriesAHCI/PRT1@1/IOAHCIDevice@0/AppleAHCIDiskDriver/IOAHCIBlockStorageDevice/IOBlockStorageDriver/APPLE HDD ST500LM012 Media/IOGUIDPartitionScheme/Untitled 4@4/AppleAPFSContainerScheme/AppleAPFSMedia/AppleAPFSContainer/VM@4", false, {"/private/var/vm"}); - disk("/dev/disk2s5", "IOService:/AppleACPIPlatformExpert/PCI0@0/AppleACPIPCI/SATA@1F,2/AppleIntelPchSeriesAHCI/PRT1@1/IOAHCIDevice@0/AppleAHCIDiskDriver/IOAHCIBlockStorageDevice/IOBlockStorageDriver/APPLE HDD ST500LM012 Media/IOGUIDPartitionScheme/Untitled 4@4/AppleAPFSContainerScheme/AppleAPFSMedia/AppleAPFSContainer/Catalina@5", false, {"/"}); - - writeable = find_unmounted (disks); - BOOST_REQUIRE_EQUAL (writeable.size(), 1U); - BOOST_CHECK_EQUAL (writeable[0].device(), "/dev/disk3"); - - disks.clear (); - disk("/dev/disk7", "IOService:/IOResources/IOHDIXController/IOHDIXHDDriveOutKernel@3/IODiskImageBlockStorageDeviceOutKernel/IOBlockStorageDriver/Apple UDIF read-only compressed (zlib) Media", true, {}); - disk("/dev/disk7s1", "IOService:/IOResources/IOHDIXController/IOHDIXHDDriveOutKernel@3/IODiskImageBlockStorageDeviceOutKernel/IOBlockStorageDriver/Apple UDIF read-only compressed (zlib) Media/IOGUIDPartitionScheme/disk image@1", false, {}); - disk("/dev/disk6s1", "MediaPathKey is IOService:/IOResources/IOHDIXController/IOHDIXHDDriveOutKernel@2/IODiskImageBlockStorageDeviceOutKernel/IOBlockStorageDriver/Apple UDIF read-only compressed (zlib) Media/IOGUIDPartitionScheme/disk image@1", false, {}); - disk("/dev/disk6", "IOService:/IOResources/IOHDIXController/IOHDIXHDDriveOutKernel@2/IODiskImageBlockStorageDeviceOutKernel/IOBlockStorageDriver/Apple UDIF read-only compressed (zlib) Media", true, {}); - disk("/dev/disk5s1", "IOService:/IOResources/IOHDIXController/IOHDIXHDDriveOutKernel@1/IODiskImageBlockStorageDeviceOutKernel/IOBlockStorageDriver/Apple UDIF read-only compressed (zlib) Media/IOGUIDPartitionScheme/disk image@1", false, {}); - disk("/dev/disk5", "IOService:/IOResources/IOHDIXController/IOHDIXHDDriveOutKernel@1/IODiskImageBlockStorageDeviceOutKernel/IOBlockStorageDriver/Apple UDIF read-only compressed (zlib) Media", true, {}); - disk("/dev/disk4s1", "IOService:/IOResources/IOHDIXController/IOHDIXHDDriveOutKernel@0/IODiskImageBlockStorageDeviceOutKernel/IOBlockStorageDriver/Apple UDIF read-only compressed (zlib) Media/IOGUIDPartitionScheme/disk image@1", false, {}); - disk("/dev/disk4", "IOService:/IOResources/IOHDIXController/IOHDIXHDDriveOutKernel@0/IODiskImageBlockStorageDeviceOutKernel/IOBlockStorageDriver/Apple UDIF read-only compressed (zlib) Media", true, {}); - disk("/dev/disk0", "IODeviceTree:/PCI0@1e0000/pci8086,2829@1F,2/PRT3@3/PMP@0/@0:0", true, {}); - disk("/dev/disk2", "IODeviceTree:/PCI0@1e0000/pci8086,2829@1F,2/PRT1@1/PMP@0/@0:0", true, {}); - disk("/dev/disk1", "IODeviceTree:/PCI0@1e0000/pci8086,2829@1F,2/PRT0@0/PMP@0/@0:0", true, {}); - disk("/dev/disk1s1", "IODeviceTree:/PCI0@1e0000/pci8086,2829@1F,2/PRT0@0/PMP@0/@0:1", false, {"/Volumes/EFI"}); - disk("/dev/disk2s1", "IODeviceTree:/PCI0@1e0000/pci8086,2829@1F,2/PRT1@1/PMP@0/@0:1", false, {}); - disk("/dev/disk2s2", "IODeviceTree:/PCI0@1e0000/pci8086,2829@1F,2/PRT1@1/PMP@0/@0:2", false, {}); - disk("/dev/disk3", "IOService:/AppleACPIPlatformExpert/PCI0@1e0000/AppleACPIPCI/pci8086,2829@1F,2/AppleAHCI/PRT1@1/IOAHCIDevice@0/AppleAHCIDiskDriver/IOAHCIBlockStorageDevice/IOBlockStorageDriver/VBOX HARDDISK Media/IOGUIDPartitionScheme/disk image@2/AppleAPFSContainerScheme/AppleAPFSMedia", false, {}); - disk("/dev/disk3s1", "IOService:/AppleACPIPlatformExpert/PCI0@1e0000/AppleACPIPCI/pci8086,2829@1F,2/AppleAHCI/PRT1@1/IOAHCIDevice@0/AppleAHCIDiskDriver/IOAHCIBlockStorageDevice/IOBlockStorageDriver/VBOX HARDDISK Media/IOGUIDPartitionScheme/disk image@2/AppleAPFSContainerScheme/AppleAPFSMedia/AppleAPFSContainer/macOS - Data@1", false, {"/System/Volumes/Data"}); - disk("/dev/disk3s2", "IOService:/AppleACPIPlatformExpert/PCI0@1e0000/AppleACPIPCI/pci8086,2829@1F,2/AppleAHCI/PRT1@1/IOAHCIDevice@0/AppleAHCIDiskDriver/IOAHCIBlockStorageDevice/IOBlockStorageDriver/VBOX HARDDISK Media/IOGUIDPartitionScheme/disk image@2/AppleAPFSContainerScheme/AppleAPFSMedia/AppleAPFSContainer/Preboot@2", false, {"/System/Volumes/Preboot"}); - disk("/dev/disk3s3", "IOService:/AppleACPIPlatformExpert/PCI0@1e0000/AppleACPIPCI/pci8086,2829@1F,2/AppleAHCI/PRT1@1/IOAHCIDevice@0/AppleAHCIDiskDriver/IOAHCIBlockStorageDevice/IOBlockStorageDriver/VBOX HARDDISK Media/IOGUIDPartitionScheme/disk image@2/AppleAPFSContainerScheme/AppleAPFSMedia/AppleAPFSContainer/Recovery@3", false, {}); - disk("/dev/disk3s4", "IOService:/AppleACPIPlatformExpert/PCI0@1e0000/AppleACPIPCI/pci8086,2829@1F,2/AppleAHCI/PRT1@1/IOAHCIDevice@0/AppleAHCIDiskDriver/IOAHCIBlockStorageDevice/IOBlockStorageDriver/VBOX HARDDISK Media/IOGUIDPartitionScheme/disk image@2/AppleAPFSContainerScheme/AppleAPFSMedia/AppleAPFSContainer/VM@4", false, {"/System/Volumes/VM"}); - disk("/dev/disk3s5", "IOService:/AppleACPIPlatformExpert/PCI0@1e0000/AppleACPIPCI/pci8086,2829@1F,2/AppleAHCI/PRT1@1/IOAHCIDevice@0/AppleAHCIDiskDriver/IOAHCIBlockStorageDevice/IOBlockStorageDriver/VBOX HARDDISK Media/IOGUIDPartitionScheme/disk image@2/AppleAPFSContainerScheme/AppleAPFSMedia/AppleAPFSContainer/macOS@5", false, {}); - disk("/dev/disk3s6", "IOService:/AppleACPIPlatformExpert/PCI0@1e0000/AppleACPIPCI/pci8086,2829@1F,2/AppleAHCI/PRT1@1/IOAHCIDevice@0/AppleAHCIDiskDriver/IOAHCIBlockStorageDevice/IOBlockStorageDriver/VBOX HARDDISK Media/IOGUIDPartitionScheme/disk image@2/AppleAPFSContainerScheme/AppleAPFSMedia/AppleAPFSContainer/Update@6", false, {"/System/Volumes/Update"}); - disk("/dev/disk3s5s1", "IOService:/AppleACPIPlatformExpert/PCI0@1e0000/AppleACPIPCI/pci8086,2829@1F,2/AppleAHCI/PRT1@1/IOAHCIDevice@0/AppleAHCIDiskDriver/IOAHCIBlockStorageDevice/IOBlockStorageDriver/VBOX HARDDISK Media/IOGUIDPartitionScheme/disk image@2/AppleAPFSContainerScheme/AppleAPFSMedia/AppleAPFSContainer/macOS@5/com.apple.os.update-5523D8E63431315F9F949CCDD0274BF797F5CEE4EAF616D4C66A01B8D6A83C7B@1", false, {"/"}); - - writeable = find_unmounted (disks); - BOOST_REQUIRE_EQUAL (writeable.size(), 1U); - BOOST_CHECK_EQUAL (writeable[0].device(), "/dev/disk0"); -} - diff --git a/test/ffmpeg_decoder_seek_test.cc b/test/ffmpeg_decoder_seek_test.cc index 4dceae86b..f38ef3564 100644 --- a/test/ffmpeg_decoder_seek_test.cc +++ b/test/ffmpeg_decoder_seek_test.cc @@ -64,18 +64,18 @@ store (ContentVideo v) static void -check (shared_ptr<FFmpegDecoder> decoder, int frame) +check (shared_ptr<FFmpegDecoder> decoder, ContentTime time) { BOOST_REQUIRE (decoder->ffmpeg_content()->video_frame_rate ()); - decoder->seek (ContentTime::from_frames (frame, decoder->ffmpeg_content()->video_frame_rate().get()), true); + decoder->seek(time, true); stored = optional<ContentVideo> (); while (!decoder->pass() && !stored) {} - BOOST_CHECK (stored->frame <= frame); + BOOST_CHECK(stored->time <= time); } static void -test (boost::filesystem::path file, vector<int> frames) +test (boost::filesystem::path file, vector<ContentTime> times) { auto path = TestPaths::private_data() / file; BOOST_REQUIRE (boost::filesystem::exists (path)); @@ -87,7 +87,7 @@ test (boost::filesystem::path file, vector<int> frames) auto decoder = make_shared<FFmpegDecoder>(film, content, false); decoder->video->Data.connect (bind (&store, _1)); - for (auto i: frames) { + for (auto i: times) { check (decoder, i); } } @@ -95,10 +95,43 @@ test (boost::filesystem::path file, vector<int> frames) BOOST_AUTO_TEST_CASE (ffmpeg_decoder_seek_test) { - vector<int> frames = { 0, 42, 999, 0 }; - - test ("boon_telly.mkv", frames); - test ("Sintel_Trailer1.480p.DivX_Plus_HD.mkv", frames); - test ("prophet_long_clip.mkv", { 15, 42, 999, 15 }); - test ("dolby_aurora.vob", { 0, 125, 250, 41 }); + test( + "boon_telly.mkv", + { + ContentTime::from_frames(0, 29.97), + ContentTime::from_frames(42, 29.97), + ContentTime::from_frames(999, 29.97), + ContentTime::from_frames(0, 29.97), + } + ); + + test( + "Sintel_Trailer1.480p.DivX_Plus_HD.mkv", + { + ContentTime::from_frames(0, 24), + ContentTime::from_frames(42, 24), + ContentTime::from_frames(999, 24), + ContentTime::from_frames(0, 24), + } + ); + + test( + "prophet_long_clip.mkv", + { + ContentTime::from_frames(15, 23.976), + ContentTime::from_frames(42, 23.976), + ContentTime::from_frames(999, 23.976), + ContentTime::from_frames(15, 23.976) + } + ); + + test( + "dolby_aurora.vob", + { + ContentTime::from_frames(0, 25), + ContentTime::from_frames(125, 25), + ContentTime::from_frames(250, 25), + ContentTime::from_frames(41, 25) + } + ); } diff --git a/test/ffmpeg_encoder_test.cc b/test/ffmpeg_encoder_test.cc index dc57b473b..f0133ff66 100644 --- a/test/ffmpeg_encoder_test.cc +++ b/test/ffmpeg_encoder_test.cc @@ -259,8 +259,8 @@ BOOST_AUTO_TEST_CASE (ffmpeg_encoder_h264_test4) /** Test mixdown from 5.1 to stereo */ BOOST_AUTO_TEST_CASE (ffmpeg_encoder_h264_test5) { - auto film = new_test_film ("ffmpeg_transcoder_h264_test5"); - film->set_name ("ffmpeg_transcoder_h264_test5"); + auto film = new_test_film("ffmpeg_encoder_h264_test5"); + film->set_name("ffmpeg_encoder_h264_test5"); film->set_container (Ratio::from_id ("185")); film->set_audio_channels (6); diff --git a/test/ffmpeg_examiner_test.cc b/test/ffmpeg_examiner_test.cc index c745335ae..231bd16e0 100644 --- a/test/ffmpeg_examiner_test.cc +++ b/test/ffmpeg_examiner_test.cc @@ -64,7 +64,7 @@ BOOST_AUTO_TEST_CASE (ffmpeg_examiner_probesize_test) BOOST_CHECK_EQUAL (examiner->audio_streams()[0]->frame_rate(), 48000); BOOST_CHECK_EQUAL (examiner->audio_streams()[0]->channels(), 2); BOOST_CHECK_EQUAL (examiner->audio_streams()[1]->frame_rate(), 48000); - BOOST_CHECK_EQUAL (examiner->audio_streams()[1]->channels(), 5); + BOOST_CHECK_EQUAL (examiner->audio_streams()[1]->channels(), 6); } diff --git a/test/j2k_encode_threading_test.cc b/test/j2k_encode_threading_test.cc new file mode 100644 index 000000000..ff2e7b0dc --- /dev/null +++ b/test/j2k_encode_threading_test.cc @@ -0,0 +1,117 @@ +/* + 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/config.h" +#include "lib/content_factory.h" +#include "lib/dcp_encoder.h" +#include "lib/dcp_transcode_job.h" +#include "lib/encode_server_description.h" +#include "lib/film.h" +#include "lib/j2k_encoder.h" +#include "lib/job_manager.h" +#include "lib/make_dcp.h" +#include "lib/transcode_job.h" +#include "test.h" +#include <dcp/cpl.h> +#include <dcp/dcp.h> +#include <dcp/reel.h> +#include <dcp/reel_picture_asset.h> +#include <boost/test/unit_test.hpp> + + +using std::dynamic_pointer_cast; +using std::list; + + +BOOST_AUTO_TEST_CASE(local_threads_created_and_destroyed) +{ + auto film = new_test_film2("local_threads_created_and_destroyed", {}); + Writer writer(film, {}); + J2KEncoder encoder(film, writer); + + encoder.remake_threads(32, 0, {}); + BOOST_CHECK_EQUAL(encoder._threads.size(), 32U); + + encoder.remake_threads(9, 0, {}); + BOOST_CHECK_EQUAL(encoder._threads.size(), 9U); + + encoder.end(); + BOOST_CHECK_EQUAL(encoder._threads.size(), 0U); +} + + +BOOST_AUTO_TEST_CASE(remote_threads_created_and_destroyed) +{ + auto film = new_test_film2("remote_threads_created_and_destroyed", {}); + Writer writer(film, {}); + J2KEncoder encoder(film, writer); + + list<EncodeServerDescription> servers = { + { "fred", 7, SERVER_LINK_VERSION }, + { "jim", 2, SERVER_LINK_VERSION }, + { "sheila", 14, SERVER_LINK_VERSION }, + }; + + encoder.remake_threads(0, 0, servers); + BOOST_CHECK_EQUAL(encoder._threads.size(), 7U + 2U + 14U); + + servers = { + { "fred", 7, SERVER_LINK_VERSION }, + { "jim", 5, SERVER_LINK_VERSION }, + { "sheila", 14, SERVER_LINK_VERSION }, + }; + + encoder.remake_threads(0, 0, servers); + BOOST_CHECK_EQUAL(encoder._threads.size(), 7U + 5U + 14U); + + servers = { + { "fred", 0, SERVER_LINK_VERSION }, + { "jim", 0, SERVER_LINK_VERSION }, + { "sheila", 11, SERVER_LINK_VERSION }, + }; + + encoder.remake_threads(0, 0, servers); + BOOST_CHECK_EQUAL(encoder._threads.size(), 11U); +} + + +BOOST_AUTO_TEST_CASE(frames_not_lost_when_threads_disappear) +{ + auto content = content_factory(TestPaths::private_data() / "clapperboard.mp4"); + auto film = new_test_film2("frames_not_lost", content); + film->write_metadata(); + auto job = make_dcp(film, TranscodeJob::ChangedBehaviour::IGNORE); + auto& encoder = dynamic_pointer_cast<DCPEncoder>(job->_encoder)->_j2k_encoder; + + while (JobManager::instance()->work_to_do()) { + encoder.remake_threads(rand() % 8, 0, {}); + dcpomatic_sleep_seconds(1); + } + + BOOST_CHECK(!JobManager::instance()->errors()); + + dcp::DCP dcp(film->dir(film->dcp_name())); + dcp.read(); + BOOST_REQUIRE_EQUAL(dcp.cpls().size(), 1U); + BOOST_REQUIRE_EQUAL(dcp.cpls()[0]->reels().size(), 1U); + BOOST_REQUIRE_EQUAL(dcp.cpls()[0]->reels()[0]->main_picture()->intrinsic_duration(), 423U); +} + diff --git a/test/kdm_cli_test.cc b/test/kdm_cli_test.cc index e79e37b2d..2e5e4760c 100644 --- a/test/kdm_cli_test.cc +++ b/test/kdm_cli_test.cc @@ -87,13 +87,13 @@ setup_test_config() auto config = Config::instance(); auto const cert = dcp::Certificate(dcp::file_to_string("test/data/cert.pem")); - auto cinema_a = std::make_shared<Cinema>("Dean's Screens", vector<string>(), "", 0, 0); + auto cinema_a = std::make_shared<Cinema>("Dean's Screens", vector<string>(), ""); cinema_a->add_screen(std::make_shared<dcpomatic::Screen>("Screen 1", "", cert, boost::none, std::vector<TrustedDevice>())); cinema_a->add_screen(std::make_shared<dcpomatic::Screen>("Screen 2", "", cert, boost::none, std::vector<TrustedDevice>())); cinema_a->add_screen(std::make_shared<dcpomatic::Screen>("Screen 3", "", cert, boost::none, std::vector<TrustedDevice>())); config->add_cinema(cinema_a); - auto cinema_b = std::make_shared<Cinema>("Floyd's Celluloid", vector<string>(), "", 0, 0); + auto cinema_b = std::make_shared<Cinema>("Floyd's Celluloid", vector<string>(), ""); cinema_b->add_screen(std::make_shared<dcpomatic::Screen>("Foo", "", cert, boost::none, std::vector<TrustedDevice>())); cinema_b->add_screen(std::make_shared<dcpomatic::Screen>("Bar", "", cert, boost::none, std::vector<TrustedDevice>())); config->add_cinema(cinema_b); @@ -228,3 +228,40 @@ BOOST_AUTO_TEST_CASE(kdm_cli_specify_cert) BOOST_CHECK(boost::filesystem::exists(kdm_filename)); } + +BOOST_AUTO_TEST_CASE(kdm_cli_time) +{ + ConfigRestorer cr; + + setup_test_config(); + + boost::filesystem::path kdm_filename = "build/test/KDM_Test_FTR-1_F-133_XX-XX_MOS_2K_20220109_SMPTE_OV_Deans_Screens_Screen_2.xml"; + + boost::system::error_code ec; + boost::filesystem::remove(kdm_filename, ec); + + dcp::LocalTime now; + now.add_days(2); + + vector<string> args = { + "kdm_cli", + "--verbose", + "--valid-from", now.as_string(), + "--valid-duration", "2 weeks", + "-c", "Dean's Screens", + "-S", "Screen 2", + "-o", "build/test", + "test/data/dkdm.xml" + }; + + vector<string> output; + auto error = run(args, output); + BOOST_CHECK(!error); + + BOOST_REQUIRE_EQUAL(output.size(), 2U); + BOOST_CHECK(boost::algorithm::starts_with(output[0], "Making KDMs valid from")); + BOOST_CHECK_EQUAL(output[1], "Wrote 1 KDM files to build/test"); + + BOOST_CHECK(boost::filesystem::exists(kdm_filename)); +} + diff --git a/test/kdm_naming_test.cc b/test/kdm_naming_test.cc index 32500553e..0f44fe2ea 100644 --- a/test/kdm_naming_test.cc +++ b/test/kdm_naming_test.cc @@ -59,16 +59,14 @@ BOOST_AUTO_TEST_CASE (single_kdm_naming_test) auto crypt_cert = c->decryption_chain()->leaf(); - /* Cinema A: UTC +4:30 */ - auto cinema_a = make_shared<Cinema>("Cinema A", vector<string>(), "", 4, 30); + auto cinema_a = make_shared<Cinema>("Cinema A", vector<string>(), ""); cinema_a_screen_1 = std::make_shared<dcpomatic::Screen>("Screen 1", "", crypt_cert, boost::none, vector<TrustedDevice>()); cinema_a->add_screen (cinema_a_screen_1); cinema_a_screen_2 = std::make_shared<dcpomatic::Screen>("Screen 2", "", crypt_cert, boost::none, vector<TrustedDevice>()); cinema_a->add_screen (cinema_a_screen_2); c->add_cinema (cinema_a); - /* Cinema B: UTC -1:00 */ - auto cinema_b = make_shared<Cinema>("Cinema B", vector<string>(), "", -1, 0); + auto cinema_b = make_shared<Cinema>("Cinema B", vector<string>(), ""); cinema_b_screen_x = std::make_shared<dcpomatic::Screen>("Screen X", "", crypt_cert, boost::none, vector<TrustedDevice>()); cinema_b->add_screen (cinema_b_screen_x); cinema_b_screen_y = std::make_shared<dcpomatic::Screen>("Screen Y", "", crypt_cert, boost::none, vector<TrustedDevice>()); @@ -90,14 +88,13 @@ BOOST_AUTO_TEST_CASE (single_kdm_naming_test) auto sign_cert = c->signer_chain()->leaf(); - dcp::LocalTime from (sign_cert.not_before()); + dcp::LocalTime from = sign_cert.not_before(); + from.set_offset({ 4, 30 }); from.add_months (2); - dcp::LocalTime until (sign_cert.not_after()); + dcp::LocalTime until = sign_cert.not_after(); + until.set_offset({ 4, 30 }); until.add_months (-2); - auto const from_string = from.date() + " " + from.time_of_day(true, false); - auto const until_string = until.date() + " " + until.time_of_day(true, false); - std::vector<KDMCertificatePeriod> period_checks; auto cpl = cpls.front().cpl_file; @@ -107,8 +104,8 @@ BOOST_AUTO_TEST_CASE (single_kdm_naming_test) auto kdm = kdm_for_screen ( make_kdm, cinema_a_screen_1, - boost::posix_time::time_from_string(from_string), - boost::posix_time::time_from_string(until_string), + from, + until, dcp::Formulation::MODIFIED_TRANSITIONAL_1, false, optional<int>(), @@ -157,9 +154,6 @@ BOOST_AUTO_TEST_CASE (directory_kdm_naming_test, * boost::unit_test::depends_on( dcp::LocalTime until (sign_cert.not_after()); until.add_months (-2); - string const from_string = from.date() + " " + from.time_of_day(true, false); - string const until_string = until.date() + " " + until.time_of_day(true, false); - vector<shared_ptr<dcpomatic::Screen>> screens = { cinema_a_screen_2, cinema_b_screen_x, cinema_a_screen_1, (cinema_b_screen_z) }; @@ -178,8 +172,8 @@ BOOST_AUTO_TEST_CASE (directory_kdm_naming_test, * boost::unit_test::depends_on( auto kdm = kdm_for_screen ( make_kdm, i, - boost::posix_time::time_from_string(from_string), - boost::posix_time::time_from_string(until_string), + from, + until, dcp::Formulation::MODIFIED_TRANSITIONAL_1, false, optional<int>(), diff --git a/test/low_bitrate_test.cc b/test/low_bitrate_test.cc index 7050dd771..52b8d54be 100644 --- a/test/low_bitrate_test.cc +++ b/test/low_bitrate_test.cc @@ -31,6 +31,7 @@ extern "C" { using std::make_shared; +using namespace dcpomatic; BOOST_AUTO_TEST_CASE (low_bitrate_test) @@ -51,7 +52,7 @@ BOOST_AUTO_TEST_CASE (low_bitrate_test) boost::optional<ColourConversion>(), VideoRange::FULL, std::weak_ptr<Content>(), - boost::optional<Frame>(), + boost::optional<ContentTime>(), false ); diff --git a/test/map_cli_test.cc b/test/map_cli_test.cc index ed2a7e5ec..0600de31e 100644 --- a/test/map_cli_test.cc +++ b/test/map_cli_test.cc @@ -466,6 +466,7 @@ BOOST_AUTO_TEST_CASE(map_with_given_config) }; boost::filesystem::remove_all(out); + boost::filesystem::remove_all("test/data/map_with_given_config/2.18"); Config::instance()->drop(); vector<string> output_messages; diff --git a/test/player_test.cc b/test/player_test.cc index 5120c0180..530dfc770 100644 --- a/test/player_test.cc +++ b/test/player_test.cc @@ -54,6 +54,7 @@ using std::cout; using std::list; using std::shared_ptr; using std::make_shared; +using std::vector; using boost::bind; using boost::optional; #if BOOST_VERSION >= 106100 @@ -710,3 +711,31 @@ BOOST_AUTO_TEST_CASE(three_d_in_two_d_chooses_left) while (!player->pass()) {} } + +BOOST_AUTO_TEST_CASE(check_seek_with_no_video) +{ + auto content = content_factory(TestPaths::private_data() / "Fight.Club.1999.720p.BRRip.x264-x0r.srt")[0]; + auto film = new_test_film2("check_seek_with_no_video", { content }); + auto player = std::make_shared<Player>(film, film->playlist()); + + boost::signals2::signal<void (std::shared_ptr<PlayerVideo>, dcpomatic::DCPTime)> Video; + + optional<dcpomatic::DCPTime> earliest; + + player->Video.connect( + [&earliest](shared_ptr<PlayerVideo>, dcpomatic::DCPTime time) { + if (!earliest || time < *earliest) { + earliest = time; + } + }); + + player->seek(dcpomatic::DCPTime::from_seconds(60 * 60), false); + + for (int i = 0; i < 10; ++i) { + player->pass(); + } + + BOOST_REQUIRE(earliest); + BOOST_CHECK(*earliest >= dcpomatic::DCPTime(60 * 60)); +} + diff --git a/test/shuffler_test.cc b/test/shuffler_test.cc index 2099a0923..37d6749c3 100644 --- a/test/shuffler_test.cc +++ b/test/shuffler_test.cc @@ -33,14 +33,15 @@ using boost::optional; #if BOOST_VERSION >= 106100 using namespace boost::placeholders; #endif +using namespace dcpomatic; static void -push (Shuffler& s, int frame, Eyes eyes) +push(Shuffler& s, int frame, Eyes eyes) { auto piece = make_shared<Piece>(shared_ptr<Content>(), shared_ptr<Decoder>(), FrameRateChange(24, 24)); ContentVideo cv; - cv.frame = frame; + cv.time = ContentTime::from_frames(frame, 24); cv.eyes = eyes; s.video (piece, cv); } @@ -56,8 +57,9 @@ receive (weak_ptr<Piece>, ContentVideo cv) static void check (int frame, Eyes eyes, int line) { + auto const time = ContentTime::from_frames(frame, 24); BOOST_REQUIRE_MESSAGE (!pending_cv.empty(), "Check at " << line << " failed."); - BOOST_CHECK_MESSAGE (pending_cv.front().frame == frame, "Check at " << line << " failed."); + BOOST_CHECK_MESSAGE (pending_cv.front().time == time, "Check at " << line << " failed."); BOOST_CHECK_MESSAGE (pending_cv.front().eyes == eyes, "Check at " << line << " failed."); pending_cv.pop_front(); } diff --git a/test/test.cc b/test/test.cc index fc5d9dc83..bf008998a 100644 --- a/test/test.cc +++ b/test/test.cc @@ -163,11 +163,21 @@ struct TestConfig setup_test_config (); capture_ffmpeg_logs(); - EncodeServerFinder::instance()->stop (); + EncodeServerFinder::drop(); signal_manager = new TestSignalManager (); dcpomatic_log.reset (new FileLog("build/test/log")); + + auto const& suite = boost::unit_test::framework::master_test_suite(); + int types = LogEntry::TYPE_GENERAL | LogEntry::TYPE_WARNING | LogEntry::TYPE_ERROR; + for (int i = 1; i < suite.argc; ++i) { + if (string(suite.argv[i]) == "--log=debug-player") { + types |= LogEntry::TYPE_DEBUG_PLAYER; + } + } + + dcpomatic_log->set_types(types); } ~TestConfig () diff --git a/test/wscript b/test/wscript index bb66d1a5f..400d8ea74 100644 --- a/test/wscript +++ b/test/wscript @@ -112,6 +112,7 @@ def build(bld): interrupt_encoder_test.cc isdcf_name_test.cc j2k_bandwidth_test.cc + j2k_encode_threading_test.cc job_manager_test.cc kdm_cli_test.cc kdm_naming_test.cc @@ -76,6 +76,7 @@ def options(opt): opt.add_option('--workaround-gssapi', action='store_true', default=False, help='link to gssapi_krb5') opt.add_option('--use-lld', action='store_true', default=False, help='use lld linker') opt.add_option('--enable-disk', action='store_true', default=False, help='build dcpomatic2_disk tool; requires Boost process, lwext4 and nanomsg libraries') + opt.add_option('--enable-grok', action='store_true', default=False, help='build with support for grok J2K encoder') opt.add_option('--warnings-are-errors', action='store_true', default=False, help='build with -Werror') opt.add_option('--wx-config', help='path to wx-config') opt.add_option('--enable-asan', action='store_true', help='build with asan') @@ -98,6 +99,7 @@ def configure(conf): conf.env.DEBUG = conf.options.enable_debug conf.env.STATIC_DCPOMATIC = conf.options.static_dcpomatic conf.env.ENABLE_DISK = conf.options.enable_disk + conf.env.ENABLE_GROK = conf.options.enable_grok if conf.options.destdir == '': conf.env.INSTALL_PREFIX = conf.options.prefix else: @@ -111,6 +113,8 @@ def configure(conf): '-Wall', '-Wextra', '-Wwrite-strings', + # getMessengerLogger() in the grok code triggers these warnings + '-Wno-nonnull', '-Wno-error=deprecated', # I tried and failed to ignore these with _Pragma '-Wno-ignored-qualifiers', @@ -143,8 +147,10 @@ def configure(conf): conf.env.append_value('CXXFLAGS', ['-Wno-cast-function-type']) # Most gccs still give these warnings from boost::optional conf.env.append_value('CXXFLAGS', ['-Wno-maybe-uninitialized']) - if int(gcc[0]) > 4: + if int(gcc[0]) > 8: # gcc 4.8.5 on Centos 7 does not have this warning + # gcc 7.5.0 on Ubuntu 18.04 and gcc 8.3.0 on Debian 10 do, but + # I didn't manage to turn it back off again with a pragma conf.env.append_value('CXXFLAGS', ['-Wsuggest-override']) if conf.options.enable_debug: @@ -155,6 +161,9 @@ def configure(conf): if conf.options.enable_disk: conf.env.append_value('CXXFLAGS', '-DDCPOMATIC_DISK') + if conf.options.enable_grok: + conf.env.append_value('CXXFLAGS', '-DDCPOMATIC_GROK') + if conf.options.use_lld: try: conf.find_program('ld.lld') @@ -468,7 +477,7 @@ def configure(conf): int main () { av_ebur128_get_true_peaks (0); }\n """, msg='Checking for EBUR128-patched FFmpeg', - uselib='AVCODEC AVFILTER', + uselib='AVCODEC AVFILTER AVUTIL SWRESAMPLE', define_name='DCPOMATIC_HAVE_EBUR128_PATCHED_FFMPEG', mandatory=False) @@ -652,6 +661,21 @@ def configure(conf): def build(bld): create_version_cc(VERSION, bld.env.CXXFLAGS) + # waf can't find these dependencies by itself because they are only included if DCPOMATIC_GROK is defined, + # and I can't find a way to pass that to waf's dependency scanner + if bld.env.ENABLE_GROK: + for dep in ( + 'src/lib/j2k_encoder.cc', + 'src/tools/dcpomatic.cc', + 'src/tools/dcpomatic_server.cc', + 'src/tools/dcpomatic_server_cli.cc', + 'src/tools/dcpomatic_batch.cc' + ): + bld.add_manual_dependency(bld.path.find_node(dep), bld.path.find_node('src/lib/grok/context.h')) + bld.add_manual_dependency(bld.path.find_node(dep), bld.path.find_node('src/lib/grok/messenger.h')) + + bld.add_manual_dependency(bld.path.find_node('src/wx/full_config_dialog.cc'), bld.path.find_node('src/wx/grok/gpu_config_panel.h')) + bld.recurse('src') bld.recurse('graphics') |
