Merge tag 'v2.16.78' into v2.17.x v2.17.12
authorCarl Hetherington <cth@carlh.net>
Thu, 22 Feb 2024 00:25:03 +0000 (01:25 +0100)
committerCarl Hetherington <cth@carlh.net>
Thu, 22 Feb 2024 00:25:03 +0000 (01:25 +0100)
133 files changed:
cscript
platform/osx/make_dmg.sh
platform/windows/wscript
run/tests.bat
src/lib/audio_analysis.cc
src/lib/audio_filter_graph.cc
src/lib/audio_filter_graph.h
src/lib/audio_mapping.cc
src/lib/butler.cc
src/lib/cinema.cc
src/lib/cinema.h
src/lib/config.cc
src/lib/config.h
src/lib/content_video.h
src/lib/cpu_j2k_encoder_thread.cc [new file with mode: 0644]
src/lib/cpu_j2k_encoder_thread.h [new file with mode: 0644]
src/lib/cross.h
src/lib/cross_common.cc
src/lib/cross_osx.cc
src/lib/dcp_content.cc
src/lib/dcp_content.h
src/lib/dcp_decoder.cc
src/lib/dcp_encoder.cc
src/lib/dcp_encoder.h
src/lib/dcp_video.cc
src/lib/dcp_video.h
src/lib/dcpomatic_time.h
src/lib/dkdm_recipient.cc
src/lib/dkdm_recipient.h
src/lib/dkdm_wrapper.cc
src/lib/encode_server.cc
src/lib/encode_server.h
src/lib/encode_server_finder.h
src/lib/encoder.h
src/lib/ffmpeg_decoder.cc
src/lib/ffmpeg_examiner.cc
src/lib/ffmpeg_file_encoder.cc
src/lib/film.cc
src/lib/film.h
src/lib/grok/context.h [new file with mode: 0644]
src/lib/grok/messenger.h [new file with mode: 0644]
src/lib/grok_j2k_encoder_thread.cc [new file with mode: 0644]
src/lib/grok_j2k_encoder_thread.h [new file with mode: 0644]
src/lib/image_decoder.cc
src/lib/j2k_encoder.cc
src/lib/j2k_encoder.h
src/lib/j2k_encoder_thread.cc [new file with mode: 0644]
src/lib/j2k_encoder_thread.h [new file with mode: 0644]
src/lib/j2k_sync_encoder_thread.cc [new file with mode: 0644]
src/lib/j2k_sync_encoder_thread.h [new file with mode: 0644]
src/lib/job.cc
src/lib/job.h
src/lib/kdm_cli.cc
src/lib/make_dcp.cc
src/lib/make_dcp.h
src/lib/overlaps.cc
src/lib/overlaps.h
src/lib/player.cc
src/lib/player.h
src/lib/player_video.cc
src/lib/player_video.h
src/lib/remote_j2k_encoder_thread.cc [new file with mode: 0644]
src/lib/remote_j2k_encoder_thread.h [new file with mode: 0644]
src/lib/screen.cc
src/lib/screen.h
src/lib/shuffler.cc
src/lib/state.cc
src/lib/text_content.cc
src/lib/transcode_job.cc
src/lib/transcode_job.h
src/lib/util.cc
src/lib/util.h
src/lib/video_content.cc
src/lib/video_content.h
src/lib/video_decoder.cc
src/lib/video_decoder.h
src/lib/video_mxf_decoder.cc
src/lib/writer.h
src/lib/wscript
src/tools/dcpomatic.cc
src/tools/dcpomatic_batch.cc
src/tools/dcpomatic_cli.cc
src/tools/dcpomatic_disk.cc
src/tools/dcpomatic_server.cc
src/tools/dcpomatic_server_cli.cc
src/wx/about_dialog.cc
src/wx/audio_panel.cc
src/wx/audio_panel.h
src/wx/check_box.cc
src/wx/check_box.h
src/wx/cinema_dialog.cc
src/wx/cinema_dialog.h
src/wx/content_sub_panel.cc
src/wx/content_sub_panel.h
src/wx/dcp_panel.cc
src/wx/dcp_referencing_dialog.cc [new file with mode: 0644]
src/wx/dcp_referencing_dialog.h [new file with mode: 0644]
src/wx/dkdm_dialog.cc
src/wx/film_editor.cc
src/wx/film_editor.h
src/wx/full_config_dialog.cc
src/wx/grok/gpu_config_panel.h [new file with mode: 0644]
src/wx/kdm_cpl_panel.cc
src/wx/kdm_timing_panel.cc
src/wx/kdm_timing_panel.h
src/wx/recipient_dialog.cc
src/wx/recipient_dialog.h
src/wx/recipients_panel.cc
src/wx/screens_panel.cc
src/wx/text_panel.cc
src/wx/text_panel.h
src/wx/video_panel.cc
src/wx/video_panel.h
src/wx/wscript
src/wx/wx_util.h
test/client_server_test.cc
test/config_test.cc
test/content_test.cc
test/data
test/disk_writer_test.cc
test/ffmpeg_decoder_seek_test.cc
test/ffmpeg_encoder_test.cc
test/ffmpeg_examiner_test.cc
test/j2k_encode_threading_test.cc [new file with mode: 0644]
test/kdm_cli_test.cc
test/kdm_naming_test.cc
test/low_bitrate_test.cc
test/map_cli_test.cc
test/player_test.cc
test/shuffler_test.cc
test/test.cc
test/wscript
wscript

diff --git a/cscript b/cscript
index 222a911b52b5a78eca527e4b8dab8707b5534af3..af1db69a66dd9a908dca48e6cba2a14e5d52fc4b 100644 (file)
--- a/cscript
+++ b/cscript
@@ -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):
index 4772d8b19cbaebb9b5377b15e75ddaca750f1d6f..161261faae0241ada24235adf5289d5c2daddfb2 100644 (file)
@@ -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
 }
 
index 8eb0fafa928f3f9f0e6fdcc6284c570615f914b1..5af8ca08a5238534b8e237c401a967350a76bdbe 100644 (file)
@@ -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"
index a9d9217887ca13d9fef12e46bd70f683b6be5d54..92030e61cb93dff7d2e469ad36894b1334340a89 100644 (file)
@@ -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\
index b8c2e072db8d6d94a364dd0a1054f0c126696703..c50a2bb9db525b61c6c1b2d1c90c785618204077 100644 (file)
@@ -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) {
index 4e3052d57c519eccc3f390e01d5f3e4c3ad6da5f..cb888c16206459f50be0e80680f9df6acdad31f4 100644 (file)
@@ -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);
 
index e5c55fa273b22009564b98fcce92f0f92bee3157..6d09f15be23d27d0bd7ed5bb37a88375867045e4 100644 (file)
@@ -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;
 };
 
index b8aa6249f5faf11697331bb15ff6236a1e0b61c9..6e8c4e30d3641f04cfa96c3961155d4f7168da2f 100644 (file)
@@ -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)));
                }
        }
index b2fbc6c60b630be0c0478dace3b922c735517878..dd98745878a1aadac6fb8699c6ee4ab8ceabb5bf 100644 (file)
@@ -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()));
index 3b4b9d7b615aa1a7116f4acb221abd3658fe41da..7388dbc2fb890390a15ca75f9f5beead2f956a20 100644 (file)
@@ -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;
-}
index 6c202a7bfc0e03515925d67162b90ee5c295995f..7008659d755f4da6938ad50e6d6fe9976ebd2e4c 100644 (file)
@@ -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;
 };
index 384db5cde137be48177afc3cb3ceca8df95f090f..8dce6237a0f2b9339abea394aa852fed2861a77f 100644 (file)
@@ -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
index f3d080b0bdc4552c00055418695a003d0f6d7814..74b4316d2740086a890fc378769104a43303d7d4 100644 (file)
@@ -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;
index 4fdab717a2965749bbd78f7a9939e1403eb15496..1c145f60294f4e7d1e1523cd8cf3abbe16eb12e5 100644 (file)
@@ -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 (file)
index 0000000..580faca
--- /dev/null
@@ -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 (file)
index 0000000..fb138f4
--- /dev/null
@@ -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;
+};
+
index 6904811b756bd7299c57c96bbdad45baac079761..150199561f31223ac2ecadbefdc123681329413a 100644 (file)
@@ -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:
index b4d322096a5645f3bf7f7fd5c09d40f94279b3ef..b8f1d48f13dbc983de6493d73d1c8689480603a7 100644 (file)
@@ -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;
-}
index 913b19103865ae2992e4bbfc802d3c4e0eb70d20..20fe9bce870b22bca53c3b6d104f46b9cee5d82c 100644 (file)
@@ -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) {
index 6185b3a19db2cfbd4e49474794b3b3057154022a..378ba18827e7ccd32b8bfe1946cfd8e8b77f2a26 100644 (file)
@@ -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
index 3753740a2728af929ff17572ffa5c4b2930ce3f7..d06d4652375cc7a2f38ad7ddd096c16f63b281df 100644 (file)
@@ -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 */
index 165b5bfb55a2b3ee70521f55fd3a15815722cc65..303126caa840292ab894bd6455e194d4c50193d2 100644 (file)
@@ -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)
                                );
                }
        }
index 9a840c8ab916b6a594f4e6881f223ad20346f4f5..bd78312fa0c02a7cab470cf71fd137ceb352c1a4 100644 (file)
@@ -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.
  *
  *  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)
 {
index ad77f6951312c03ace8fe91e7fe4d91608f2f743..ce0b7220439dfa0e4bf287648047ec3ba7864ec9 100644 (file)
@@ -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);
index 217b72183c2af0b2d61b945d0b60ba3551c34933..3769a3285e552b9e40400e2513e79e920a499eb1 100644 (file)
@@ -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.
  */
index bf95ccfe60d9d3336749a8a4008b9de8310b3839..d07c8322b56a456de6ddd1417eca3bdcd3e960ee 100644 (file)
@@ -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
index 1b12ea901653ead8afd3f935679eaea8ecf6e57a..9ebb334fe118e284e9cb609d53b75e3e29b4cf3a 100644 (file)
@@ -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);
        }
index c73379bed9cded1f3963fad8a62bf32bf67b4095..8f3bfea2e8b5ecd5c4e4f4e70aed8a049ba81ff3 100644 (file)
@@ -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);
index 7a0fa01854c271347fbc96f151367e8b453cb21b..3317ae6f9ed62f943aed7c0d74b165aeafdb51e0 100644 (file)
@@ -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
        );
 
index 016c77c3f9ec4a7671c3f2e8906ea0f0fae55883..7beaae8f478d4099fc6590db149a23e7ebd69bb9 100644 (file)
@@ -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);
        }
index 036ea58a5df92b94cb5cde24e6a7666be195f3a4..7eae4375f81f332c5f29bd906c35ff877ab9e85e 100644 (file)
@@ -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 ();
 }
 
index f93d667467a0a4fe439651fe540d7dd3107e68fe..8059abd0f6d5bfe1c3ab04763ffe62ce5fa97051 100644 (file)
@@ -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 {
 
index f8a30af54b295f9a3d6906bd5ab3b878ee6fe805..c478387f9bbaaaaaf799d4ca237382decb1b904e 100644 (file)
@@ -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 ();
index 9b67720d361cc56931fa39867b20e70003bd08ae..aeaf7f6204a9cb10c12a3c4ce3ee9e7a4ba2e7ee 100644 (file)
@@ -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;
index 7f7a078638b540ba81bbbe38c5ad260970232fa0..09db1ff1cd9df8e317738ff43a8779045fef2551 100644 (file)
@@ -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");
index 15cb14ad53301605df3e96ec4398f7ffdce55b8c..583ea129725eadee1e09e162bcacb3d00ffaf401 100644 (file)
@@ -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");
        }
 
index 6d1ad68f79bb79d939e4baf25991b792ef4bbaa9..d7833265d630d130437a5a3bedee49ac6f63f221 100644 (file)
@@ -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);
 
index d9ab6e2a3f8a2e4df368a6eece61ae2b37187b44..d747efb0e1706de0122f50fe42fa92f3828be7cc 100644 (file)
@@ -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 (...) {}
+}
index 43a41ad45d5a43fbeaf92accdea2595e9155e6db..036bbed7ef3ad8e0a13aa6c24ea5223bbbc52b1b 100644 (file)
@@ -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 (file)
index 0000000..521faae
--- /dev/null
@@ -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 (file)
index 0000000..eb2fe95
--- /dev/null
@@ -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 (file)
index 0000000..c3cd6f8
--- /dev/null
@@ -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 (file)
index 0000000..5301e16
--- /dev/null
@@ -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;
+};
+
index ce5c8757f7f636adc40cdf9f5dd23e5daf3d10dc..527a98c7dfd0b8cdc8c659a65577665e102f78e0 100644 (file)
@@ -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;
 }
index 7c9777c1694ed77df52f5af315b3c928e08a2252..de229113b3a93234f9af47d91c17c1133ae8575f 100644 (file)
 #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();
 }
index 63228a6b8fac8cb7635de681bbabf4d5b14b46d2..6bfbaea496b0743b14e02b922ef57a2be84f8511 100644 (file)
@@ -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 (file)
index 0000000..d0e8a43
--- /dev/null
@@ -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 (file)
index 0000000..b03b6f3
--- /dev/null
@@ -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 (file)
index 0000000..ef6834f
--- /dev/null
@@ -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 (file)
index 0000000..4522227
--- /dev/null
@@ -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
index 9c9530b7ad9bf16e89eaa08a110e1374ea20319d..9e685ec114cee9d01fe95e998e009b8f59d64390 100644 (file)
@@ -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 ();
        }
 }
index d4d0f95109b6e8936f68f1251d6e2e3c68806f78..9b5fdfa6e51fad7df7f51e62ba90d050c3e5294e 100644 (file)
@@ -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;
index ddc77e771323ab515525fa70e47ba8b12002a275..4e3f9ccb71324e7dd954705f826f6e8b4ed1bd4c 100644 (file)
@@ -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];
index 17d45be46e13416869977b6951d73a95573b2bdf..ddd231243b7d83f744ae2966111d15255c7a6989 100644 (file)
@@ -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;
 }
 
index 9f507278219730192793c6aef99301891e3d1303..fe0bcd2f630a3b6350ca62ccf8df8cd1006e7aef 100644 (file)
@@ -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);
 
index fbc6b5cb1813c7646950f554c25cc952f8e6a5d0..bed75a8221947fbb67de245195413d30145e66c1 100644 (file)
@@ -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);
index bb8277eaa91807966e2adf0f2779b1706211d283..f405c0fabffc80e79890a9d606f6212df4da73a9 100644 (file)
@@ -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
        );
+
+
+}
+
index c03cb97a59f50ad8549f8142279e48a236a801dc..dba02cfbafaf02ecf5f63a0e93719fc9df06a1d2 100644 (file)
@@ -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); });
index 94e41bbcace5aceab2ea56c05ef88c7f83166e9f..2502ae536b844a56d5e036cbe540477c64f1be27 100644 (file)
@@ -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;
index 35c5d3daa484f997d141417bf56efa10208753e8..b39f83908222bce2fd7313e944edc2d3199362d0 100644 (file)
@@ -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;
index f2781c1a0c51c3e099dd0009604e04a89ce2c3df..10b2078a00af83e73b1505b97aa2801145c9edde 100644 (file)
@@ -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 (file)
index 0000000..49d8095
--- /dev/null
@@ -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 (file)
index 0000000..f3fe7f9
--- /dev/null
@@ -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;
+};
index febf9085c3d344af6a77487c5aed1c04a60a7148..f0c80cd794f7a27e524859a76f7bdba85536e7ff 100644 (file)
@@ -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);
index 7f01cdf2792abb802f200b40fb1045f21b89a7c2..0a275aa34c9407e3403935e21c0c6dfb9639e91c 100644 (file)
 #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,
index 5a4faf4d15fa4c0f74617bd2bd03ecd57f1ba114..a4ea0f5dc05a923d00d8da3ea7a34983fc4522c8 100644 (file)
@@ -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 ();
index a8230385d42e957653f2b32ad2a622a7096ff3db..7a3232ee9c357e9edbdd21eb80c4266a3314de38 100644 (file)
@@ -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
index 92a35b8220cbca3f41c7302d9d8f0536a0b22ab9..09f6e41b656a476d0367690e9d67a6bd99cb5b0a 100644 (file)
@@ -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");
        }
 }
 
index 12b9a2aa30b1bce0c188a7aa442d5722a7feddcd..b260bc44b1d852c5c23ce0870a7a3c2594a2318f 100644 (file)
@@ -148,6 +148,20 @@ TranscodeJob::run ()
 }
 
 
+void
+TranscodeJob::pause()
+{
+       _encoder->pause();
+}
+
+
+void TranscodeJob::resume()
+{
+       _encoder->resume();
+       Job::resume();
+}
+
+
 string
 TranscodeJob::status () const
 {
index b05b20a1693373b33cc9b6416f37aef53fa1b5ae..35870231d5af1f592861c05158f57ea8b49debda 100644 (file)
@@ -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;
 
index 7f6e9da5aff95feaee14a7cac247952d2e5cbd5d..01a7f0248d603f4cb1d44c0661b57bca9a78828f 100644 (file)
@@ -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
index b92869b253a776167c72bbd1b20159a1a9015e4e..b85cf0a333fef898a4829e15917bbd7afdd61651 100644 (file)
@@ -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
index 91ed1185588d181056d76a8fd78ea11ffaefa8c5..c6046cb372b5a90241e0fce027c183db677eca7f 100644 (file)
@@ -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
index e7e8eb1b3d3d6f35c68169bc71677ffddf0632e7..d31c25f135e136f748204c5ae2479b09d8641db1 100644 (file)
@@ -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);
 
index cf21f885a3fbc6af45ba2efde1cfc18ad645eb9b..c628fddd983e2015a81b1bd7d13b52b0d9424a0a 100644 (file)
@@ -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;
 }
 
 
index f6ee17425b6d77b48d6b074c5d369f6cfbd888c3..b609404c49230a66b9788564cb9d60cad886e4b1 100644 (file)
@@ -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;
 
index 40d3a461afbd4ebb4fd57765d5bc80a5f42d8c39..9f451297b992d9a9fa0ecc7297aedfc340412b73 100644 (file)
@@ -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
                        );
        }
 
index efb6a17d8d54448b740e2b3fc238010c6abff01f..1cd278221738397523b1d6f1bcdc2e2c0aea8c3d 100644 (file)
@@ -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>
index 0d61d7a69987ef4c03ca9235d4b8d25a7da94c9a..5bae3e0d145cfaa0a27d0c6ab1476e60c553b393 100644 (file)
@@ -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'
index 459ce341c3d026254522d36afa3b47437dd36b17..f65037c2f2571a6045f72d715b4fbc853f9ee0c0 100644 (file)
@@ -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)
                {
index e476c2163a552a8842b05bc566c37fcba80b3262..1e383b39a5ecdafc516532308c85fc0638f4acf0 100644 (file)
@@ -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;
        }
 
index 65252ff29dfd1efb6d8804d8a336840446f9a122..c2e7ee76cf3c35860c7ab8982c19e0b5dcfbc0ae 100644 (file)
@@ -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";
index 395b60a8857f83866d7b29bb86c4327c6196d5f6..0cc7ccbcd0961b1f8b1b2db2564431ecffb8399e 100644 (file)
@@ -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 ()
index 9bdc688c8f0e4bda214e48b7f61c37703cd41815..622704d2167cd7952452ea861b7b08decd738c22 100644 (file)
 #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;
        }
 
index 6d7f6aba729ae06113de78f76ae9bbd5ab8dba68..8997bb92c4bf53ba079ade8e9349ef61818ab5c7 100644 (file)
@@ -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 {
index 2c742cce3a1549be4368297cbc0c59cd6f3e4496..8b1f8a03863b2796867559c10390e3220d6ecb3a 100644 (file)
@@ -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;
index f0863431a04b01bebad4feee14b08291c7b74a29..d7deeec41987053ce654dac7ac61797330140e4c 100644 (file)
@@ -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);
@@ -499,23 +467,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>)
 {
index 61dd2783ac8cd54d4ad5e20f8a884490ca7c3ffd..d25f7e247005bdf0a9dfcae06735417f679452c0 100644 (file)
@@ -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;
index baec651c1b8ca58e18ee8c07e10e79958c374571..c18ceba474bbe8615e5630e11ebf3e9d2a6399aa 100644 (file)
@@ -53,3 +53,10 @@ CheckBox::get() const
        return GetValue();
 }
 
+
+void
+CheckBox::set(bool state)
+{
+       SetValue(state);
+}
+
index 4b3fddb9fdf65fa4f55226c203f740eb2a9dd97b..d9feb00c9c9c7935cb8a0c4473c8e6fa236b0e71 100644 (file)
@@ -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) {
index 84fde5f41acb0985a4ed45d74a7612650e51d7d2..771a59892b491715ef805fb64317550be42c93b8 100644 (file)
@@ -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
 {
index 0b8362843de68f759c08a08c1b0aae8367b0d093..cd90f6dea9ee1e9bf2569f89af8e8b4eeadc557a 100644 (file)
@@ -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;
 };
index 6bbc9a51fcae43a89ad9e785ec99e2242fb266cb..10fdfa232e41e32027e7786bafc56e04f94dfb0d 100644 (file)
@@ -52,29 +52,6 @@ ContentSubPanel::ContentSubPanel (ContentPanel* p, wxString name)
        _sizer->Add (_grid, 0, wxALL, 8);
 }
 
-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 ()
 {
index a3916817e45f7057fdca651aef5d22097ab65bfb..48970e6a92651a97189a6ed236c7cf9c68b3536d 100644 (file)
@@ -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;
 
index f4ba74cdefbbcefd433379a26698af0bac33f9c5..d81e7bf9522cb9dd812789b7f7459230b3a2a29b 100644 (file)
@@ -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 (file)
index 0000000..853391b
--- /dev/null
@@ -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 (file)
index 0000000..e49292c
--- /dev/null
@@ -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;
+};
+
index 26f521573ddffafabdefdf2be1a623b19dd999f2..8da3090c5633ebdd2958378c43f5d8b5b9691baf 100644 (file)
@@ -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);
                        }
index 9f54db58e027e9b651f830d098d41045c0e91775..fae02787ab2c12d5cc8e9328138a4d4fea0933d7 100644 (file)
@@ -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);
+       }
 }
 
 
index a3df266adfd311933f44d37dd46f4d340dd2bff1..54d639ef595bd12102f66091a86d66d0ac4abbab 100644 (file)
@@ -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;
 
index 4c26d038c671d6611f322fdce2f52cc956640245..66dbae63e0f2640500e39cb0fbc41c50a1bd1489 100644 (file)
@@ -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 (file)
index 0000000..cbf0375
--- /dev/null
@@ -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;
+};
index 4699582aac538a4302a2472ae7307e336a0410da..4e1eb8f3404a62003f1c3a08a5dbffa1e08a7dae 100644 (file)
@@ -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 */
index 0fd00de93bb866f45a065e78a83b82ec842aa390..f4112cb087da5e0552d2bcfc1e9c0d1352838e28 100644 (file)
 */
 
 
+#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 };
+}
+
+
index 7221ba722f367eb40d47641af9d72c12fa63ab7b..f847992a7088155c3fd9d21e07185c9cb558efee 100644 (file)
@@ -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;
 };
index a69adb169ece6b323def2f00d623b64753821a5e..992c8836209b8529b7495e37ecddc2382a89d605 100644 (file)
@@ -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;
-}
-
-
index a46e67af5b907ebdfe3fa2b2fa441f091cff1355..61f6b3031bd3564e1511cf61763d0a37422627b4 100644 (file)
@@ -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;
 };
index 485a0f94e1ea65f8219f7bea1e9c690b6e157302..04ad0dd6e4911950fbfff26575d3dcd74126fd34 100644 (file)
@@ -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);
        }
index d3b1db77d4e36da0a04323244ac368922ec8789b..62f8688a9192150328c4fc7dac5724925f633dd9 100644 (file)
@@ -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);
index 78c024565eb95434805ae3a5836b4a03abafb16a..c3a5706b0d23ced98d2bd98b47ea57b81ea3b4cf 100644 (file)
@@ -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 ();
 
@@ -762,23 +728,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 ()
 {
index 5adad5a3ed14dd55fb05579d1586fff7c2840615..a2afba4399c359931c6ffebe69405d62fc5af991 100644 (file)
@@ -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;
index b27db2547baa72e5e2d1f7b9720184dd1997685d..0280f16a0525b585e79891c84b194e7feba03297 100644 (file)
@@ -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);
@@ -681,23 +652,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 ()
 {
index 686d1b99b32300e951013f947b08fa20ec1ceee5..e6b689b038ad71ec7b347983af46a897c5ab89eb 100644 (file)
@@ -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;
index 9c6ea6b8435c0ab71f00b1f08d402c04cccfd143..7dbd214c6ebc305833137fa91deed7f727717eff 100644 (file)
@@ -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
index 66b01640c5b1175583fee8898c5f7bc11335a88a..dcf5bbc1871d9cf1f16883821ab3a0dda7f88c34 100644 (file)
@@ -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
index 596668aaeb5333d970aa283f2f8325aba385a617..1bfa4c5a61825350e544cdb107831d3f458decfb 100644 (file)
 
 
 /** @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();
+}
+
index 103a6a58555ac0f181353afac8f6f257c14f495d..00adb7b09fc781e80106f8657aca126c3f55a5a9 100644 (file)
@@ -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");
 }
 
index 5ab714b47ad8e46a5363eef8fc939417853a4b32..dce7643dc9bd88bca1d3739c1720e174e612161a 100644 (file)
@@ -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 ();
 }
index 4cb08962ba07e99c442cb12091c0347d84d8fd89..ddf878730354cdec8a802a59543591f6f943f5c0 160000 (submodule)
--- a/test/data
+++ b/test/data
@@ -1 +1 @@
-Subproject commit 4cb08962ba07e99c442cb12091c0347d84d8fd89
+Subproject commit ddf878730354cdec8a802a59543591f6f943f5c0
index 553adcae7188693f8c330a0b7018e6c0f1894a43..ff39eaaeab6d373d6addfb5de3858e0717f31a39 100644 (file)
@@ -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");
-}
-
index 4dceae86ba6f6416e7add63be7c0093265fad6e8..f38ef3564a2004a0b49efb2fda7c929563c2fd39 100644 (file)
@@ -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)
+               }
+           );
 }
index dc57b473b48a3174ea080b8e2f3df545b680d260..f0133ff667a42e2e8cd56620ede320754ebdf00d 100644 (file)
@@ -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);
 
index c745335ae2b4dbba571d5b9381e2d538d502b4a5..231bd16e0aa51f242f013ac6dca314d12f674c76 100644 (file)
@@ -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 (file)
index 0000000..ff2e7b0
--- /dev/null
@@ -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);
+}
+
index e79e37b2d83434ef7d340ed1b7d181d19c775c6b..2e5e4760c47b506dfec6bdd16916e5dbfec548cc 100644 (file)
@@ -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));
+}
+
index 32500553e90e48065c6786064eaa94f5cf628e94..0f44fe2ea570cef51e83cf8b5258008cc3b210cf 100644 (file)
@@ -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>(),
index 7050dd771acc6dd74dd8a57f0e30b8d177010470..52b8d54be118c33559b9f1722d58141cbd34c116 100644 (file)
@@ -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
                );
 
index ed2a7e5ec5aad4ede1d3dba538abe39646bab74c..0600de31e650474aa7d1525fad73e82ed326d28f 100644 (file)
@@ -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;
index 5120c018018e02c172ef59d5b5375409acf73a90..530dfc770f2500196d5551b05f5a235649fe20f9 100644 (file)
@@ -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));
+}
+
index 2099a0923dfdd052439900e07f333718d212f78a..37d6749c38fd36bb2fb46e435385166bb3a2f039 100644 (file)
@@ -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();
 }
index fc5d9dc83b9d2263f47eccae4cb8e6d02a928d2f..bf008998abca27c36106ae16d991da552b0a9f43 100644 (file)
@@ -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 ()
index bb66d1a5f952ff07d115ac63c9d647e6346829a6..400d8ea74638f9161024efe07a977b803dd4dae3 100644 (file)
@@ -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
diff --git a/wscript b/wscript
index a62b5c2c3a2e9b27c5c67b484fb99617b7e08195..6dd7417cda41fefa7cd0b76389a0f829820f7523 100644 (file)
--- a/wscript
+++ b/wscript
@@ -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')