Merge remote-tracking branch 'origin/main' into v2.17.x
authorCarl Hetherington <cth@carlh.net>
Fri, 29 Mar 2024 23:00:44 +0000 (00:00 +0100)
committerCarl Hetherington <cth@carlh.net>
Fri, 29 Mar 2024 23:00:44 +0000 (00:00 +0100)
261 files changed:
cscript
hacks/text.cc
platform/osx/make_dmg.sh
platform/windows/wscript
run/dcpomatic_playlist
run/tests.bat
src/lib/analytics.cc
src/lib/atmos_content.cc
src/lib/atmos_content.h
src/lib/atmos_mxf_content.cc
src/lib/atmos_mxf_content.h
src/lib/audio_analysis.cc
src/lib/audio_content.cc
src/lib/audio_content.h
src/lib/audio_filter_graph.cc
src/lib/audio_filter_graph.h
src/lib/audio_mapping.cc
src/lib/audio_mapping.h
src/lib/audio_point.cc
src/lib/butler.cc
src/lib/cinema.cc
src/lib/cinema.h
src/lib/colour_conversion.cc
src/lib/colour_conversion.h
src/lib/config.cc
src/lib/config.h
src/lib/constants.h
src/lib/content.cc
src/lib/content.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/crop.cc
src/lib/crop.h
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_content_type.cc
src/lib/dcp_decoder.cc
src/lib/dcp_digest_file.cc
src/lib/dcp_encoder.cc
src/lib/dcp_encoder.h
src/lib/dcp_examiner.cc
src/lib/dcp_examiner.h
src/lib/dcp_subtitle_content.cc
src/lib/dcp_subtitle_content.h
src/lib/dcp_text_track.cc
src/lib/dcp_video.cc
src/lib/dcp_video.h
src/lib/dcpomatic_time.cc
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/export_config.cc
src/lib/export_config.h
src/lib/ffmpeg_audio_stream.cc
src/lib/ffmpeg_audio_stream.h
src/lib/ffmpeg_content.cc
src/lib/ffmpeg_content.h
src/lib/ffmpeg_decoder.cc
src/lib/ffmpeg_examiner.cc
src/lib/ffmpeg_file_encoder.cc
src/lib/ffmpeg_image_proxy.cc
src/lib/ffmpeg_image_proxy.h
src/lib/ffmpeg_stream.cc
src/lib/ffmpeg_stream.h
src/lib/ffmpeg_subtitle_stream.cc
src/lib/ffmpeg_subtitle_stream.h
src/lib/film.cc
src/lib/film.h
src/lib/film_property.h
src/lib/filter.cc
src/lib/font.cc
src/lib/font.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_content.cc
src/lib/image_content.h
src/lib/image_decoder.cc
src/lib/image_proxy.h
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_image_proxy.cc
src/lib/j2k_image_proxy.h
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/kdm_recipient.cc
src/lib/make_dcp.cc
src/lib/make_dcp.h
src/lib/overlaps.cc
src/lib/overlaps.h
src/lib/pixel_quanta.cc
src/lib/player.cc
src/lib/player.h
src/lib/player_video.cc
src/lib/player_video.h
src/lib/playlist.cc
src/lib/playlist.h
src/lib/raw_image_proxy.cc
src/lib/raw_image_proxy.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/render_text.cc
src/lib/rgba.cc
src/lib/rgba.h
src/lib/screen.cc
src/lib/screen.h
src/lib/shuffler.cc
src/lib/spl.cc
src/lib/spl_entry.cc
src/lib/state.cc
src/lib/string_text_file_content.cc
src/lib/string_text_file_content.h
src/lib/subtitle_analysis.cc
src/lib/text_content.cc
src/lib/text_content.h
src/lib/transcode_job.cc
src/lib/transcode_job.h
src/lib/types.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_content.cc
src/lib/video_mxf_content.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_player.cc
src/tools/dcpomatic_playlist.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/colours.h [new file with mode: 0644]
src/wx/content_menu.cc
src/wx/content_panel.cc
src/wx/content_panel.h
src/wx/content_sub_panel.cc
src/wx/content_sub_panel.h
src/wx/content_timeline.cc [new file with mode: 0644]
src/wx/content_timeline.h [new file with mode: 0644]
src/wx/content_timeline_atmos_view.cc [new file with mode: 0644]
src/wx/content_timeline_atmos_view.h [new file with mode: 0644]
src/wx/content_timeline_audio_view.cc [new file with mode: 0644]
src/wx/content_timeline_audio_view.h [new file with mode: 0644]
src/wx/content_timeline_dialog.cc [new file with mode: 0644]
src/wx/content_timeline_dialog.h [new file with mode: 0644]
src/wx/content_timeline_text_view.cc [new file with mode: 0644]
src/wx/content_timeline_text_view.h [new file with mode: 0644]
src/wx/content_timeline_video_view.cc [new file with mode: 0644]
src/wx/content_timeline_video_view.h [new file with mode: 0644]
src/wx/content_timeline_view.cc [new file with mode: 0644]
src/wx/content_timeline_view.h [new file with mode: 0644]
src/wx/dcp_panel.cc
src/wx/dcp_panel.h
src/wx/dcp_referencing_dialog.cc [new file with mode: 0644]
src/wx/dcp_referencing_dialog.h [new file with mode: 0644]
src/wx/dcp_timeline.cc [new file with mode: 0644]
src/wx/dcp_timeline.h [new file with mode: 0644]
src/wx/dcp_timeline_dialog.cc [new file with mode: 0644]
src/wx/dcp_timeline_dialog.h [new file with mode: 0644]
src/wx/dcp_timeline_reel_marker_view.cc [new file with mode: 0644]
src/wx/dcp_timeline_reel_marker_view.h [new file with mode: 0644]
src/wx/dcp_timeline_view.h [new file with mode: 0644]
src/wx/dkdm_dialog.cc
src/wx/file_picker_ctrl.cc
src/wx/film_editor.cc
src/wx/film_editor.h
src/wx/film_name_location_dialog.cc
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/timecode.cc
src/wx/timecode.h
src/wx/timeline.cc
src/wx/timeline.h
src/wx/timeline_atmos_content_view.cc [deleted file]
src/wx/timeline_atmos_content_view.h [deleted file]
src/wx/timeline_audio_content_view.cc [deleted file]
src/wx/timeline_audio_content_view.h [deleted file]
src/wx/timeline_content_view.cc
src/wx/timeline_content_view.h
src/wx/timeline_dialog.cc [deleted file]
src/wx/timeline_dialog.h [deleted file]
src/wx/timeline_labels_view.cc
src/wx/timeline_labels_view.h
src/wx/timeline_reels_view.cc
src/wx/timeline_reels_view.h
src/wx/timeline_text_content_view.cc [deleted file]
src/wx/timeline_text_content_view.h [deleted file]
src/wx/timeline_time_axis_view.cc
src/wx/timeline_time_axis_view.h
src/wx/timeline_video_content_view.cc [deleted file]
src/wx/timeline_video_content_view.h [deleted file]
src/wx/timeline_view.cc [deleted file]
src/wx/timeline_view.h
src/wx/video_panel.cc
src/wx/video_panel.h
src/wx/wscript
src/wx/wx_util.h
test/bv20_test.cc
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/film_metadata_test.cc
test/image_filename_sorter_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/remake_with_subtitle_test.cc
test/shuffler_test.cc
test/stream_test.cc
test/test.cc
test/test.h
test/text_entry_point_test.cc [new file with mode: 0644]
test/torture_test.cc
test/video_level_test.cc
test/wscript
wscript

diff --git a/cscript b/cscript
index 53d2beb7b40bc1901102b270b55cb641b6f50b6e..f70e7ca36df11813ff3b7f6507a9b78541ad984b 100644 (file)
--- a/cscript
+++ b/cscript
@@ -530,12 +530,12 @@ 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 = []
 
-    deps.append(('libdcp', 'v1.8.98'))
+    deps.append(('libdcp', '56b3586247d76b2d07911018ccba63d4b05f4995', {'c++17': target.platform == 'osx'}))
     deps.append(('libsub', 'v1.6.47'))
     deps.append(('leqm-nrt', '30dcaea1373ac62fba050e02ce5b0c1085797a23'))
     deps.append(('rtaudio', 'f619b76'))
@@ -546,7 +546,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
 
@@ -588,8 +588,13 @@ def configure_options(target, options, for_package=False):
     if can_build_disk(target):
         opt += ' --enable-disk'
 
-    if target.platform == 'osx' and target.arch == 'arm64':
-        opt += ' --target-macos-arm64 --wx-config=%s/wx-config' % target.bin
+    if target.platform == 'osx':
+        opt += ' --c++17'
+        if 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
 
index bf391a32548b1b76c4d5da38e493a090275c4895..ff5d6f8b8ff5f581b3cfa445bfcffa8ec6c6c2c3 100644 (file)
@@ -19,10 +19,10 @@ int main ()
 
        Cairo::RefPtr<Cairo::ImageSurface> surface = Cairo::ImageSurface::create (
                data,
-               Cairo::FORMAT_ARGB32,
+               Cairo::ImageSurface::Format::ARGB32,
                width, height,
                /* Cairo ARGB32 means first byte blue, second byte green, third byte red, fourth byte alpha */
-               Cairo::ImageSurface::format_stride_for_width (Cairo::FORMAT_ARGB32, width)
+               Cairo::ImageSurface::format_stride_for_width(Cairo::ImageSurface::Format::ARGB32, width)
                );
 
        Cairo::RefPtr<Cairo::Context> context = Cairo::Context::create (surface);
@@ -33,7 +33,7 @@ int main ()
        context->rectangle (0, 0, width, height);
        context->fill ();
 
-       layout->set_alignment (Pango::ALIGN_LEFT);
+       layout->set_alignment (Pango::Alignment::LEFT);
 
        context->set_line_width (1);
        // Cairo::FontOptions fo;
index bebd089bd80b425953721671eaf94b62b38a5572..f53203264fe286b2ce9e5af49a14c753f4b022d7 100644 (file)
@@ -141,8 +141,8 @@ function copy_libs {
     local dest="$1"
     copy_lib_root libcxml "$dest"
     copy_lib_root libdcp-1.0 "$dest"
-    copy_lib_root libasdcp-carl "$dest"
-    copy_lib_root libkumu-carl "$dest"
+    copy_lib_root libasdcp-dcpomatic "$dest"
+    copy_lib_root libkumu-dcpomatic "$dest"
     copy_lib_root libsub "$dest"
     copy_lib_root libopenjp2 "$dest"
     copy_lib_root libavdevice "$dest"
@@ -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"
@@ -167,7 +168,7 @@ function copy_libs {
     copy_lib_env libxml++ "$dest"
     copy_lib_env libxslt "$dest"
     copy_lib_env libxml2 "$dest"
-    copy_lib_env libglibmm-2.4 "$dest"
+    copy_lib_env libglibmm "$dest"
     copy_lib_env libgobject "$dest"
     copy_lib_env libgthread "$dest"
     copy_lib_env libgmodule "$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 17965d54ce92c4680482f0f60e36ad2d7f0edece..b208f94dd7294ff5987f5ba57b0bc7c00f906e88 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:
@@ -184,8 +188,8 @@ File "%static_deps%/bin/libdl.dll"
 File /oname=dcpomatic2_verify.exe "%cdist_deps%/bin/dcpverify.exe"
 File /oname=dcpomatic2_kdm_inspect.exe "%cdist_deps%/bin/dcpkdm.exe"
 File "%cdist_deps%/bin/leqm_nrt.dll"
-File "%cdist_deps%/bin/asdcp-carl.dll"
-File "%cdist_deps%/bin/kumu-carl.dll"
+File "%cdist_deps%/bin/asdcp-dcpomatic.dll"
+File "%cdist_deps%/bin/kumu-dcpomatic.dll"
     """, file=f)
 
     if disk:
@@ -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 a23c45766b58c0659789c7f3c7e4f33d50b171ce..134a1047ee8dc459e1f2279aafc72b7bab5eb812 100755 (executable)
@@ -4,6 +4,14 @@ DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )"
 source $DIR/environment
 binary=build/src/tools/dcpomatic2_playlist
 
+if [[ "$(uname -m)" == arm64 ]]; then
+    env=arm64/11.0
+else
+    env=x86_64/10.10
+fi
+
+export DYLD_LIBRARY_PATH=/Users/cah/osx-environment/$env/lib:/usr/local/lib
+
 if [ "$1" == "--debug" ]; then
     shift
     gdb --args $binary $*
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 836051fe59616756482acf6003ba4539226e90a9..3fce7974923d639be1890d678848d8442cc5ab72 100644 (file)
@@ -93,8 +93,8 @@ Analytics::write () const
        xmlpp::Document doc;
        auto root = doc.create_root_node ("Analytics");
 
-       root->add_child("Version")->add_child_text(raw_convert<string>(_current_version));
-       root->add_child("SuccessfulDCPEncodes")->add_child_text(raw_convert<string>(_successful_dcp_encodes));
+       cxml::add_text_child(root, "Version", raw_convert<string>(_current_version));
+       cxml::add_text_child(root, "SuccessfulDCPEncodes", raw_convert<string>(_successful_dcp_encodes));
 
        try {
                doc.write_to_file_formatted(write_path("analytics.xml").string());
index ada304dad1c698d5d167230b1f7b42ccbb6dc0ee..22bd431a22fe9e7102de2b67cadab94516d24dbe 100644 (file)
@@ -63,10 +63,10 @@ AtmosContent::from_xml (Content* parent, cxml::ConstNodePtr node)
 
 
 void
-AtmosContent::as_xml (xmlpp::Node* node) const
+AtmosContent::as_xml(xmlpp::Element* element) const
 {
-       node->add_child("AtmosLength")->add_child_text(dcp::raw_convert<string>(_length));
-       node->add_child("AtmosEditRate")->add_child_text(_edit_rate.as_string());
+       cxml::add_text_child(element, "AtmosLength", dcp::raw_convert<string>(_length));
+       cxml::add_text_child(element, "AtmosEditRate", _edit_rate.as_string());
 }
 
 
index afa3c75a2770cd160250bb7fca673b718483ad05..ffa1814972240b17a69079e081033f0a03fe3a99 100644 (file)
@@ -42,7 +42,7 @@ public:
 
        static std::shared_ptr<AtmosContent> from_xml (Content* parent, cxml::ConstNodePtr node);
 
-       void as_xml (xmlpp::Node* node) const;
+       void as_xml(xmlpp::Element* element) const;
 
        void set_length (Frame len);
 
index 82c20e88f7bd8e7c723e49216b1d4aeeabe3bc4d..1283961ee121ad5ad4e5466500c69106475e7ae7 100644 (file)
@@ -98,11 +98,11 @@ AtmosMXFContent::summary () const
 
 
 void
-AtmosMXFContent::as_xml (xmlpp::Node* node, bool with_paths) const
+AtmosMXFContent::as_xml(xmlpp::Element* element, bool with_paths) const
 {
-       node->add_child("Type")->add_child_text("AtmosMXF");
-       Content::as_xml (node, with_paths);
-       atmos->as_xml (node);
+       cxml::add_text_child(element, "Type", "AtmosMXF");
+       Content::as_xml(element, with_paths);
+       atmos->as_xml(element);
 }
 
 
index 57f041774c1b3776926545b7b77c025c2c872c33..e6225a3556fec632c6bf919b9ecf3454348d4ceb 100644 (file)
@@ -39,7 +39,7 @@ public:
 
        void examine (std::shared_ptr<const Film> film, std::shared_ptr<Job> job) override;
        std::string summary () const override;
-       void as_xml (xmlpp::Node* node, bool with_path) const override;
+       void as_xml(xmlpp::Element* element, bool with_path) const override;
        dcpomatic::DCPTime full_length (std::shared_ptr<const Film> film) const override;
        dcpomatic::DCPTime approximate_length () const override;
 
index b8c2e072db8d6d94a364dd0a1054f0c126696703..ac990ee18f13e3488a4cbef06456458e31371416 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")) {
@@ -141,44 +138,44 @@ void
 AudioAnalysis::write (boost::filesystem::path filename)
 {
        auto doc = make_shared<xmlpp::Document>();
-       xmlpp::Element* root = doc->create_root_node ("AudioAnalysis");
+       auto root = doc->create_root_node("AudioAnalysis");
 
-       root->add_child("Version")->add_child_text(raw_convert<string>(_current_state_version));
+       cxml::add_text_child(root, "Version", raw_convert<string>(_current_state_version));
 
        for (auto& i: _data) {
-               auto channel = root->add_child ("Channel");
+               auto channel = cxml::add_child(root, "Channel");
                for (auto& j: i) {
-                       j.as_xml (channel->add_child ("Point"));
+                       j.as_xml(cxml::add_child(channel, "Point"));
                }
        }
 
        for (size_t i = 0; i < _sample_peak.size(); ++i) {
-               auto n = root->add_child("SamplePeak");
+               auto n = cxml::add_child(root, "SamplePeak");
                n->add_child_text (raw_convert<string> (_sample_peak[i].peak));
-               n->set_attribute ("Time", raw_convert<string> (_sample_peak[i].time.get()));
+               n->set_attribute("time", raw_convert<string> (_sample_peak[i].time.get()));
        }
 
        for (auto i: _true_peak) {
-               root->add_child("TruePeak")->add_child_text (raw_convert<string> (i));
+               cxml::add_text_child(root, "TruePeak", raw_convert<string>(i));
        }
 
        if (_integrated_loudness) {
-               root->add_child("IntegratedLoudness")->add_child_text (raw_convert<string> (_integrated_loudness.get ()));
+               cxml::add_text_child(root, "IntegratedLoudness", raw_convert<string>(_integrated_loudness.get()));
        }
 
        if (_loudness_range) {
-               root->add_child("LoudnessRange")->add_child_text (raw_convert<string> (_loudness_range.get ()));
+               cxml::add_text_child(root, "LoudnessRange", raw_convert<string>(_loudness_range.get()));
        }
 
        if (_analysis_gain) {
-               root->add_child("AnalysisGain")->add_child_text (raw_convert<string> (_analysis_gain.get ()));
+               cxml::add_text_child(root, "AnalysisGain", raw_convert<string>(_analysis_gain.get()));
        }
 
-       root->add_child("SamplesPerPoint")->add_child_text (raw_convert<string> (_samples_per_point));
-       root->add_child("SampleRate")->add_child_text (raw_convert<string> (_sample_rate));
+       cxml::add_text_child(root, "SamplesPerPoint", raw_convert<string>(_samples_per_point));
+       cxml::add_text_child(root, "SampleRate", raw_convert<string>(_sample_rate));
 
        if (_leqm) {
-               root->add_child("Leqm")->add_child_text(raw_convert<string>(*_leqm));
+               cxml::add_text_child(root, "Leqm", raw_convert<string>(*_leqm));
        }
 
        doc->write_to_file_formatted (filename.string ());
index 16f2bd5f1a250d7188cb1a73740a415d530b2b08..2569e1cf049fe683646a17811d20642dfb6f54a9 100644 (file)
@@ -124,14 +124,14 @@ AudioContent::AudioContent (Content* parent, vector<shared_ptr<Content>> c)
 
 
 void
-AudioContent::as_xml (xmlpp::Node* node) const
+AudioContent::as_xml(xmlpp::Element* element) const
 {
        boost::mutex::scoped_lock lm (_mutex);
-       node->add_child("AudioGain")->add_child_text(raw_convert<string>(_gain));
-       node->add_child("AudioDelay")->add_child_text(raw_convert<string>(_delay));
-       node->add_child("AudioFadeIn")->add_child_text(raw_convert<string>(_fade_in.get()));
-       node->add_child("AudioFadeOut")->add_child_text(raw_convert<string>(_fade_out.get()));
-       node->add_child("AudioUseSameFadesAsVideo")->add_child_text(_use_same_fades_as_video ? "1" : "0");
+       cxml::add_text_child(element, "AudioGain", raw_convert<string>(_gain));
+       cxml::add_text_child(element, "AudioDelay", raw_convert<string>(_delay));
+       cxml::add_text_child(element, "AudioFadeIn", raw_convert<string>(_fade_in.get()));
+       cxml::add_text_child(element, "AudioFadeOut", raw_convert<string>(_fade_out.get()));
+       cxml::add_text_child(element, "AudioUseSameFadesAsVideo", _use_same_fades_as_video ? "1" : "0");
 }
 
 
index 084871c8ba8bb6e6da951e0faa885fa6f9532596..2a140b3e4f772bef453dde1d0899b0237d55159d 100644 (file)
@@ -56,7 +56,7 @@ public:
        AudioContent (Content* parent, std::vector<std::shared_ptr<Content>>);
        AudioContent (Content* parent, cxml::ConstNodePtr);
 
-       void as_xml (xmlpp::Node *) const;
+       void as_xml(xmlpp::Element*) const;
        std::string technical_summary () const;
        void take_settings_from (std::shared_ptr<const AudioContent> c);
 
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..37929001dc3735490f6f36ae43153754e5777001 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())
                                        );
                        }
@@ -219,19 +220,19 @@ AudioMapping::get (int input_channel, int output_channel) const
 
 
 void
-AudioMapping::as_xml (xmlpp::Node* node) const
+AudioMapping::as_xml(xmlpp::Element* element) const
 {
        auto const input = input_channels();
        auto const output = output_channels();
 
-       node->add_child("InputChannels")->add_child_text(raw_convert<string>(input));
-       node->add_child("OutputChannels")->add_child_text(raw_convert<string>(output));
+       cxml::add_text_child(element, "InputChannels", raw_convert<string>(input));
+       cxml::add_text_child(element, "OutputChannels", raw_convert<string>(output));
 
        for (int c = 0; c < input; ++c) {
                for (int d = 0; d < output; ++d) {
-                       auto t = node->add_child ("Gain");
-                       t->set_attribute ("Input", raw_convert<string> (c));
-                       t->set_attribute ("Output", raw_convert<string> (d));
+                       auto t = cxml::add_child(element, "Gain");
+                       t->set_attribute("input", raw_convert<string>(c));
+                       t->set_attribute("output", raw_convert<string>(d));
                        t->add_child_text (raw_convert<string> (get (c, d)));
                }
        }
index fe9a7978939d938db88f9b98db1b11f40d804595..68487d908f2cb1613bb93cf4f2015816ece95aff 100644 (file)
@@ -52,7 +52,7 @@ public:
 
        /* Default copy constructor is fine */
 
-       void as_xml (xmlpp::Node *) const;
+       void as_xml(xmlpp::Element*) const;
 
        void make_zero ();
        void make_default (AudioProcessor const * processor, boost::optional<boost::filesystem::path> filename = boost::optional<boost::filesystem::path>());
index 1f32d25fce3e2e35bbb0ff193a37617ad1284613..036904520ad1bf81c4dcf507a52c6ff097e5785f 100644 (file)
@@ -72,6 +72,6 @@ AudioPoint::operator= (AudioPoint const & other)
 void
 AudioPoint::as_xml (xmlpp::Element* parent) const
 {
-       parent->add_child("Peak")->add_child_text(raw_convert<string>(_data[PEAK]));
-       parent->add_child("RMS")->add_child_text(raw_convert<string>(_data[RMS]));
+       cxml::add_text_child(parent, "Peak", raw_convert<string>(_data[PEAK]));
+       cxml::add_text_child(parent, "RMS", raw_convert<string>(_data[RMS]));
 }
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..6bd06373344232cdfdde749d9a6d771fbe36c165 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
@@ -65,19 +57,16 @@ Cinema::read_screens (cxml::ConstNodePtr node)
 void
 Cinema::as_xml (xmlpp::Element* parent) const
 {
-       parent->add_child("Name")->add_child_text (name);
+       cxml::add_text_child(parent, "Name", name);
 
        for (auto i: emails) {
-               parent->add_child("Email")->add_child_text (i);
+               cxml::add_text_child(parent, "Email", i);
        }
 
-       parent->add_child("Notes")->add_child_text (notes);
-
-       parent->add_child("UTCOffsetHour")->add_child_text (raw_convert<string> (_utc_offset_hour));
-       parent->add_child("UTCOffsetMinute")->add_child_text (raw_convert<string> (_utc_offset_minute));
+       cxml::add_text_child(parent, "Notes", notes);
 
        for (auto i: _screens) {
-               i->as_xml (parent->add_child ("Screen"));
+               i->as_xml(cxml::add_child(parent, "Screen"));
        }
 }
 
@@ -97,16 +86,3 @@ Cinema::remove_screen (shared_ptr<Screen> s)
        }
 }
 
-void
-Cinema::set_utc_offset_hour (int h)
-{
-       DCPOMATIC_ASSERT (h >= -11 && h <= 12);
-       _utc_offset_hour = h;
-}
-
-void
-Cinema::set_utc_offset_minute (int m)
-{
-       DCPOMATIC_ASSERT (m >= 0 && m <= 59);
-       _utc_offset_minute = m;
-}
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 bd1b47cb1ad43a067bf912c929d144c003eb171f..f1e6258122fbf3cebb7638be85e38ecedb4f8705 100644 (file)
@@ -151,41 +151,40 @@ ColourConversion::from_xml (cxml::NodePtr node, int version)
 }
 
 void
-ColourConversion::as_xml (xmlpp::Node* node) const
+ColourConversion::as_xml(xmlpp::Element* element) const
 {
-       auto in_node = node->add_child ("InputTransferFunction");
+       auto in_node = cxml::add_child(element, "InputTransferFunction");
        if (dynamic_pointer_cast<const dcp::GammaTransferFunction> (_in)) {
                auto tf = dynamic_pointer_cast<const dcp::GammaTransferFunction> (_in);
-               in_node->add_child("Type")->add_child_text ("Gamma");
-               in_node->add_child("Gamma")->add_child_text (raw_convert<string> (tf->gamma ()));
+               cxml::add_text_child(in_node, "Type", "Gamma");
+               cxml::add_text_child(in_node, "Gamma", raw_convert<string>(tf->gamma()));
        } else if (dynamic_pointer_cast<const dcp::ModifiedGammaTransferFunction> (_in)) {
                auto tf = dynamic_pointer_cast<const dcp::ModifiedGammaTransferFunction> (_in);
-               in_node->add_child("Type")->add_child_text ("ModifiedGamma");
-               in_node->add_child("Power")->add_child_text (raw_convert<string> (tf->power ()));
-               in_node->add_child("Threshold")->add_child_text (raw_convert<string> (tf->threshold ()));
-               in_node->add_child("A")->add_child_text (raw_convert<string> (tf->A ()));
-               in_node->add_child("B")->add_child_text (raw_convert<string> (tf->B ()));
-       } else if (dynamic_pointer_cast<const dcp::SGamut3TransferFunction> (_in)) {
-               in_node->add_child("Type")->add_child_text ("SGamut3");
+               cxml::add_text_child(in_node, "Type", "ModifiedGamma");
+               cxml::add_text_child(in_node, "Power", raw_convert<string>(tf->power ()));
+               cxml::add_text_child(in_node, "Threshold", raw_convert<string>(tf->threshold ()));
+               cxml::add_text_child(in_node, "A", raw_convert<string>(tf->A()));
+               cxml::add_text_child(in_node, "B", raw_convert<string>(tf->B()));
+       } else if (dynamic_pointer_cast<const dcp::SGamut3TransferFunction>(_in)) {
+               cxml::add_text_child(in_node, "Type", "SGamut3");
        }
 
-       node->add_child("YUVToRGB")->add_child_text (raw_convert<string> (static_cast<int> (_yuv_to_rgb)));
-       node->add_child("RedX")->add_child_text (raw_convert<string> (_red.x));
-       node->add_child("RedY")->add_child_text (raw_convert<string> (_red.y));
-       node->add_child("GreenX")->add_child_text (raw_convert<string> (_green.x));
-       node->add_child("GreenY")->add_child_text (raw_convert<string> (_green.y));
-       node->add_child("BlueX")->add_child_text (raw_convert<string> (_blue.x));
-       node->add_child("BlueY")->add_child_text (raw_convert<string> (_blue.y));
-       node->add_child("WhiteX")->add_child_text (raw_convert<string> (_white.x));
-       node->add_child("WhiteY")->add_child_text (raw_convert<string> (_white.y));
+       cxml::add_text_child(element, "YUVToRGB", raw_convert<string>(static_cast<int>(_yuv_to_rgb)));
+       cxml::add_text_child(element, "RedX", raw_convert<string>(_red.x));
+       cxml::add_text_child(element, "RedY", raw_convert<string>(_red.y));
+       cxml::add_text_child(element, "GreenX", raw_convert<string>(_green.x));
+       cxml::add_text_child(element, "GreenY", raw_convert<string>(_green.y));
+       cxml::add_text_child(element, "BlueX", raw_convert<string>(_blue.x));
+       cxml::add_text_child(element, "BlueY", raw_convert<string>(_blue.y));
+       cxml::add_text_child(element, "WhiteX", raw_convert<string>(_white.x));
+       cxml::add_text_child(element, "WhiteY", raw_convert<string>(_white.y));
        if (_adjusted_white) {
-               node->add_child("AdjustedWhiteX")->add_child_text (raw_convert<string> (_adjusted_white.get().x));
-               node->add_child("AdjustedWhiteY")->add_child_text (raw_convert<string> (_adjusted_white.get().y));
+               cxml::add_text_child(element, "AdjustedWhiteX", raw_convert<string>(_adjusted_white.get().x));
+               cxml::add_text_child(element, "AdjustedWhiteY", raw_convert<string>(_adjusted_white.get().y));
        }
 
-       if (dynamic_pointer_cast<const dcp::GammaTransferFunction> (_out)) {
-               shared_ptr<const dcp::GammaTransferFunction> gf = dynamic_pointer_cast<const dcp::GammaTransferFunction> (_out);
-               node->add_child("OutputGamma")->add_child_text (raw_convert<string> (gf->gamma ()));
+       if (auto gf = dynamic_pointer_cast<const dcp::GammaTransferFunction>(_out)) {
+               cxml::add_text_child(element, "OutputGamma", raw_convert<string>(gf->gamma()));
        }
 }
 
index 73b6ad23ccf558f469b2e29a5369d562301ba505..0c07ddbacd87c03ac30aea4f2a230e7606723ea0 100644 (file)
@@ -41,7 +41,7 @@ public:
        ColourConversion (cxml::NodePtr, int version);
        virtual ~ColourConversion () {}
 
-       virtual void as_xml (xmlpp::Node *) const;
+       virtual void as_xml(xmlpp::Element*) const;
        std::string identifier () const;
 
        boost::optional<size_t> preset () const;
index 384db5cde137be48177afc3cb3ceca8df95f090f..c80ef224e922391c142014fe909f3e5b08732030 100644 (file)
@@ -177,6 +177,8 @@ Config::set_defaults ()
        _gdc_username = optional<string>();
        _gdc_password = optional<string>();
        _player_mode = PLAYER_MODE_WINDOW;
+       _player_restricted_menus = false;
+       _playlist_editor_restricted_menus = false;
        _image_display = 0;
        _video_view_type = VIDEO_VIEW_SIMPLE;
        _respect_kdm_validity_periods = true;
@@ -220,6 +222,10 @@ Config::set_defaults ()
        set_notification_email_to_default ();
        set_cover_sheet_to_default ();
 
+#ifdef DCPOMATIC_GROK
+       _grok = boost::none;
+#endif
+
        _main_divider_sash_position = {};
        _main_content_divider_sash_position = {};
 
@@ -494,7 +500,7 @@ try
           of the nags.
        */
        for (auto i: f.node_children("Nagged")) {
-               auto const id = i->number_attribute<int>("Id");
+               auto const id = number_attribute<int>(i, "Id", "id");
                if (id >= 0 && id < NAG_COUNT) {
                        _nagged[id] = raw_convert<int>(i->content());
                }
@@ -566,7 +572,7 @@ try
        _default_notify = f.optional_bool_child("DefaultNotify").get_value_or(false);
 
        for (auto i: f.node_children("Notification")) {
-               int const id = i->number_attribute<int>("Id");
+               int const id = number_attribute<int>(i, "Id", "id");
                if (id >= 0 && id < NOTIFICATION_COUNT) {
                        _notification[id] = raw_convert<int>(i->content());
                }
@@ -588,6 +594,9 @@ try
                _player_mode = PLAYER_MODE_DUAL;
        }
 
+       _player_restricted_menus = f.optional_bool_child("PlayerRestrictedMenus").get_value_or(false);
+       _playlist_editor_restricted_menus = f.optional_bool_child("PlaylistEditorRestrictedMenus").get_value_or(false);
+
        _image_display = f.optional_number_child<int>("ImageDisplay").get_value_or(0);
        auto vc = f.optional_string_child("VideoViewType");
        if (vc && *vc == "opengl") {
@@ -642,6 +651,12 @@ try
        _allow_smpte_bv20 = f.optional_bool_child("AllowSMPTEBv20").get_value_or(false);
        _isdcf_name_part_length = f.optional_number_child<int>("ISDCFNamePartLength").get_value_or(14);
 
+#ifdef DCPOMATIC_GROK
+       if (auto grok = f.optional_node_child("Grok")) {
+               _grok = Grok(grok);
+       }
+#endif
+
        _export.read(f.optional_node_child("Export"));
 }
 catch (...) {
@@ -721,218 +736,218 @@ Config::write_config () const
        auto root = doc.create_root_node ("Config");
 
        /* [XML] Version The version number of the configuration file format. */
-       root->add_child("Version")->add_child_text (raw_convert<string>(_current_version));
+       cxml::add_text_child(root, "Version", raw_convert<string>(_current_version));
        /* [XML] MasterEncodingThreads Number of encoding threads to use when running as master. */
-       root->add_child("MasterEncodingThreads")->add_child_text (raw_convert<string> (_master_encoding_threads));
+       cxml::add_text_child(root, "MasterEncodingThreads", raw_convert<string>(_master_encoding_threads));
        /* [XML] ServerEncodingThreads Number of encoding threads to use when running as server. */
-       root->add_child("ServerEncodingThreads")->add_child_text (raw_convert<string> (_server_encoding_threads));
+       cxml::add_text_child(root, "ServerEncodingThreads", raw_convert<string>(_server_encoding_threads));
        if (_default_directory) {
                /* [XML:opt] DefaultDirectory Default directory when creating a new film in the GUI. */
-               root->add_child("DefaultDirectory")->add_child_text (_default_directory->string ());
+               cxml::add_text_child(root, "DefaultDirectory", _default_directory->string());
        }
        /* [XML] ServerPortBase Port number to use for frame encoding requests.  <code>ServerPortBase</code> + 1 and
           <code>ServerPortBase</code> + 2 are used for querying servers.  <code>ServerPortBase</code> + 3 is used
           by the batch converter to listen for job requests.
        */
-       root->add_child("ServerPortBase")->add_child_text (raw_convert<string> (_server_port_base));
+       cxml::add_text_child(root, "ServerPortBase", raw_convert<string>(_server_port_base));
        /* [XML] UseAnyServers 1 to broadcast to look for encoding servers to use, 0 to use only those configured. */
-       root->add_child("UseAnyServers")->add_child_text (_use_any_servers ? "1" : "0");
+       cxml::add_text_child(root, "UseAnyServers", _use_any_servers ? "1" : "0");
 
        for (auto i: _servers) {
                /* [XML:opt] Server IP address or hostname of an encoding server to use; you can use as many of these tags
                   as you like.
                */
-               root->add_child("Server")->add_child_text (i);
+               cxml::add_text_child(root, "Server", i);
        }
 
        /* [XML] OnlyServersEncode 1 to set the master to do decoding of source content no JPEG2000 encoding; all encoding
           is done by the encoding servers.  0 to set the master to do some encoding as well as coordinating the job.
        */
-       root->add_child("OnlyServersEncode")->add_child_text (_only_servers_encode ? "1" : "0");
+       cxml::add_text_child(root, "OnlyServersEncode", _only_servers_encode ? "1" : "0");
        /* [XML] TMSProtocol Protocol to use to copy files to a TMS; 0 to use SCP, 1 for FTP. */
-       root->add_child("TMSProtocol")->add_child_text (raw_convert<string> (static_cast<int> (_tms_protocol)));
+       cxml::add_text_child(root, "TMSProtocol", raw_convert<string>(static_cast<int>(_tms_protocol)));
        /* [XML] TMSPassive True to use PASV mode with TMS FTP connections. */
-       root->add_child("TMSPassive")->add_child_text(_tms_passive ? "1" : "0");
+       cxml::add_text_child(root, "TMSPassive", _tms_passive ? "1" : "0");
        /* [XML] TMSIP IP address of TMS. */
-       root->add_child("TMSIP")->add_child_text (_tms_ip);
+       cxml::add_text_child(root, "TMSIP", _tms_ip);
        /* [XML] TMSPath Path on the TMS to copy files to. */
-       root->add_child("TMSPath")->add_child_text (_tms_path);
+       cxml::add_text_child(root, "TMSPath", _tms_path);
        /* [XML] TMSUser Username to log into the TMS with. */
-       root->add_child("TMSUser")->add_child_text (_tms_user);
+       cxml::add_text_child(root, "TMSUser", _tms_user);
        /* [XML] TMSPassword Password to log into the TMS with. */
-       root->add_child("TMSPassword")->add_child_text (_tms_password);
+       cxml::add_text_child(root, "TMSPassword", _tms_password);
        if (_language) {
                /* [XML:opt] Language Language to use in the GUI e.g. <code>fr_FR</code>. */
-               root->add_child("Language")->add_child_text (_language.get());
+               cxml::add_text_child(root, "Language", _language.get());
        }
        if (_default_dcp_content_type) {
                /* [XML:opt] DefaultDCPContentType Default content type to use when creating new films (<code>FTR</code>, <code>SHR</code>,
                   <code>TLR</code>, <code>TST</code>, <code>XSN</code>, <code>RTG</code>, <code>TSR</code>, <code>POL</code>,
                   <code>PSA</code> or <code>ADV</code>). */
-               root->add_child("DefaultDCPContentType")->add_child_text (_default_dcp_content_type->isdcf_name ());
+               cxml::add_text_child(root, "DefaultDCPContentType", _default_dcp_content_type->isdcf_name());
        }
        /* [XML] DefaultDCPAudioChannels Default number of audio channels to use when creating new films. */
-       root->add_child("DefaultDCPAudioChannels")->add_child_text (raw_convert<string> (_default_dcp_audio_channels));
+       cxml::add_text_child(root, "DefaultDCPAudioChannels", raw_convert<string>(_default_dcp_audio_channels));
        /* [XML] DCPIssuer Issuer text to write into CPL files. */
-       root->add_child("DCPIssuer")->add_child_text (_dcp_issuer);
+       cxml::add_text_child(root, "DCPIssuer", _dcp_issuer);
        /* [XML] DCPCreator Creator text to write into CPL files. */
-       root->add_child("DCPCreator")->add_child_text (_dcp_creator);
+       cxml::add_text_child(root, "DCPCreator", _dcp_creator);
        /* [XML] Company name to write into MXF files. */
-       root->add_child("DCPCompanyName")->add_child_text (_dcp_company_name);
+       cxml::add_text_child(root, "DCPCompanyName", _dcp_company_name);
        /* [XML] Product name to write into MXF files. */
-       root->add_child("DCPProductName")->add_child_text (_dcp_product_name);
+       cxml::add_text_child(root, "DCPProductName", _dcp_product_name);
        /* [XML] Product version to write into MXF files. */
-       root->add_child("DCPProductVersion")->add_child_text (_dcp_product_version);
+       cxml::add_text_child(root, "DCPProductVersion", _dcp_product_version);
        /* [XML] Comment to write into JPEG2000 data. */
-       root->add_child("DCPJ2KComment")->add_child_text (_dcp_j2k_comment);
+       cxml::add_text_child(root, "DCPJ2KComment", _dcp_j2k_comment);
        /* [XML] UploadAfterMakeDCP 1 to upload to a TMS after making a DCP, 0 for no upload. */
-       root->add_child("UploadAfterMakeDCP")->add_child_text (_upload_after_make_dcp ? "1" : "0");
+       cxml::add_text_child(root, "UploadAfterMakeDCP", _upload_after_make_dcp ? "1" : "0");
 
        /* [XML] DefaultStillLength Default length (in seconds) for still images in new films. */
-       root->add_child("DefaultStillLength")->add_child_text (raw_convert<string> (_default_still_length));
+       cxml::add_text_child(root, "DefaultStillLength", raw_convert<string>(_default_still_length));
        /* [XML] DefaultJ2KBandwidth Default bitrate (in bits per second) for JPEG2000 data in new films. */
-       root->add_child("DefaultJ2KBandwidth")->add_child_text (raw_convert<string> (_default_j2k_bandwidth));
+       cxml::add_text_child(root, "DefaultJ2KBandwidth", raw_convert<string>(_default_j2k_bandwidth));
        /* [XML] DefaultAudioDelay Default delay to apply to audio (positive moves audio later) in milliseconds. */
-       root->add_child("DefaultAudioDelay")->add_child_text (raw_convert<string> (_default_audio_delay));
+       cxml::add_text_child(root, "DefaultAudioDelay", raw_convert<string>(_default_audio_delay));
        /* [XML] DefaultInterop 1 to default new films to Interop, 0 for SMPTE. */
-       root->add_child("DefaultInterop")->add_child_text (_default_interop ? "1" : "0");
+       cxml::add_text_child(root, "DefaultInterop", _default_interop ? "1" : "0");
        if (_default_audio_language) {
                /* [XML] DefaultAudioLanguage Default audio language to use for new films */
-               root->add_child("DefaultAudioLanguage")->add_child_text(_default_audio_language->to_string());
+               cxml::add_text_child(root, "DefaultAudioLanguage", _default_audio_language->to_string());
        }
        if (_default_territory) {
                /* [XML] DefaultTerritory Default territory to use for new films */
-               root->add_child("DefaultTerritory")->add_child_text(_default_territory->subtag());
+               cxml::add_text_child(root, "DefaultTerritory", _default_territory->subtag());
        }
        for (auto const& i: _default_metadata) {
-               auto c = root->add_child("DefaultMetadata");
+               auto c = cxml::add_child(root, "DefaultMetadata");
                c->set_attribute("key", i.first);
                c->add_child_text(i.second);
        }
        if (_default_kdm_directory) {
                /* [XML:opt] DefaultKDMDirectory Default directory to write KDMs to. */
-               root->add_child("DefaultKDMDirectory")->add_child_text (_default_kdm_directory->string ());
+               cxml::add_text_child(root, "DefaultKDMDirectory", _default_kdm_directory->string ());
        }
-       _default_kdm_duration.as_xml(root->add_child("DefaultKDMDuration"));
+       _default_kdm_duration.as_xml(cxml::add_child(root, "DefaultKDMDuration"));
        /* [XML] MailServer Hostname of SMTP server to use. */
-       root->add_child("MailServer")->add_child_text (_mail_server);
+       cxml::add_text_child(root, "MailServer", _mail_server);
        /* [XML] MailPort Port number to use on SMTP server. */
-       root->add_child("MailPort")->add_child_text (raw_convert<string> (_mail_port));
+       cxml::add_text_child(root, "MailPort", raw_convert<string>(_mail_port));
        /* [XML] MailProtocol Protocol to use on SMTP server (Auto, Plain, STARTTLS or SSL) */
        switch (_mail_protocol) {
        case EmailProtocol::AUTO:
-               root->add_child("MailProtocol")->add_child_text("Auto");
+               cxml::add_text_child(root, "MailProtocol", "Auto");
                break;
        case EmailProtocol::PLAIN:
-               root->add_child("MailProtocol")->add_child_text("Plain");
+               cxml::add_text_child(root, "MailProtocol", "Plain");
                break;
        case EmailProtocol::STARTTLS:
-               root->add_child("MailProtocol")->add_child_text("STARTTLS");
+               cxml::add_text_child(root, "MailProtocol", "STARTTLS");
                break;
        case EmailProtocol::SSL:
-               root->add_child("MailProtocol")->add_child_text("SSL");
+               cxml::add_text_child(root, "MailProtocol", "SSL");
                break;
        }
        /* [XML] MailUser Username to use on SMTP server. */
-       root->add_child("MailUser")->add_child_text (_mail_user);
+       cxml::add_text_child(root, "MailUser", _mail_user);
        /* [XML] MailPassword Password to use on SMTP server. */
-       root->add_child("MailPassword")->add_child_text (_mail_password);
+       cxml::add_text_child(root, "MailPassword", _mail_password);
 
        /* [XML] KDMSubject Subject to use for KDM emails. */
-       root->add_child("KDMSubject")->add_child_text (_kdm_subject);
+       cxml::add_text_child(root, "KDMSubject", _kdm_subject);
        /* [XML] KDMFrom From address to use for KDM emails. */
-       root->add_child("KDMFrom")->add_child_text (_kdm_from);
+       cxml::add_text_child(root, "KDMFrom", _kdm_from);
        for (auto i: _kdm_cc) {
                /* [XML] KDMCC CC address to use for KDM emails; you can use as many of these tags as you like. */
-               root->add_child("KDMCC")->add_child_text (i);
+               cxml::add_text_child(root, "KDMCC", i);
        }
        /* [XML] KDMBCC BCC address to use for KDM emails. */
-       root->add_child("KDMBCC")->add_child_text (_kdm_bcc);
+       cxml::add_text_child(root, "KDMBCC", _kdm_bcc);
        /* [XML] KDMEmail Text of KDM email. */
-       root->add_child("KDMEmail")->add_child_text (_kdm_email);
+       cxml::add_text_child(root, "KDMEmail", _kdm_email);
 
        /* [XML] NotificationSubject Subject to use for notification emails. */
-       root->add_child("NotificationSubject")->add_child_text (_notification_subject);
+       cxml::add_text_child(root, "NotificationSubject", _notification_subject);
        /* [XML] NotificationFrom From address to use for notification emails. */
-       root->add_child("NotificationFrom")->add_child_text (_notification_from);
+       cxml::add_text_child(root, "NotificationFrom", _notification_from);
        /* [XML] NotificationFrom To address to use for notification emails. */
-       root->add_child("NotificationTo")->add_child_text (_notification_to);
+       cxml::add_text_child(root, "NotificationTo", _notification_to);
        for (auto i: _notification_cc) {
                /* [XML] NotificationCC CC address to use for notification emails; you can use as many of these tags as you like. */
-               root->add_child("NotificationCC")->add_child_text (i);
+               cxml::add_text_child(root, "NotificationCC", i);
        }
        /* [XML] NotificationBCC BCC address to use for notification emails. */
-       root->add_child("NotificationBCC")->add_child_text (_notification_bcc);
+       cxml::add_text_child(root, "NotificationBCC", _notification_bcc);
        /* [XML] NotificationEmail Text of notification email. */
-       root->add_child("NotificationEmail")->add_child_text (_notification_email);
+       cxml::add_text_child(root, "NotificationEmail", _notification_email);
 
        /* [XML] CheckForUpdates 1 to check dcpomatic.com for new versions, 0 to check only on request. */
-       root->add_child("CheckForUpdates")->add_child_text (_check_for_updates ? "1" : "0");
+       cxml::add_text_child(root, "CheckForUpdates", _check_for_updates ? "1" : "0");
        /* [XML] CheckForUpdates 1 to check dcpomatic.com for new text versions, 0 to check only on request. */
-       root->add_child("CheckForTestUpdates")->add_child_text (_check_for_test_updates ? "1" : "0");
+       cxml::add_text_child(root, "CheckForTestUpdates", _check_for_test_updates ? "1" : "0");
 
        /* [XML] MaximumJ2KBandwidth Maximum J2K bandwidth (in bits per second) that can be specified in the GUI. */
-       root->add_child("MaximumJ2KBandwidth")->add_child_text (raw_convert<string> (_maximum_j2k_bandwidth));
+       cxml::add_text_child(root, "MaximumJ2KBandwidth", raw_convert<string>(_maximum_j2k_bandwidth));
        /* [XML] AllowAnyDCPFrameRate 1 to allow users to specify any frame rate when creating DCPs, 0 to limit the GUI to standard rates. */
-       root->add_child("AllowAnyDCPFrameRate")->add_child_text (_allow_any_dcp_frame_rate ? "1" : "0");
+       cxml::add_text_child(root, "AllowAnyDCPFrameRate", _allow_any_dcp_frame_rate ? "1" : "0");
        /* [XML] AllowAnyContainer 1 to allow users to user any container ratio for their DCP, 0 to limit the GUI to DCI Flat/Scope */
-       root->add_child("AllowAnyContainer")->add_child_text (_allow_any_container ? "1" : "0");
+       cxml::add_text_child(root, "AllowAnyContainer", _allow_any_container ? "1" : "0");
        /* [XML] Allow96kHzAudio 1 to allow users to make DCPs with 96kHz audio, 0 to always make 48kHz DCPs */
-       root->add_child("Allow96kHzAudio")->add_child_text(_allow_96khz_audio ? "1" : "0");
+       cxml::add_text_child(root, "Allow96kHzAudio", _allow_96khz_audio ? "1" : "0");
        /* [XML] UseAllAudioChannels 1 to allow users to map audio to all 16 DCP channels, 0 to limit to the channels used in standard DCPs */
-       root->add_child("UseAllAudioChannels")->add_child_text(_use_all_audio_channels ? "1" : "0");
+       cxml::add_text_child(root, "UseAllAudioChannels", _use_all_audio_channels ? "1" : "0");
        /* [XML] ShowExperimentalAudioProcessors 1 to offer users the (experimental) audio upmixer processors, 0 to hide them */
-       root->add_child("ShowExperimentalAudioProcessors")->add_child_text (_show_experimental_audio_processors ? "1" : "0");
+       cxml::add_text_child(root, "ShowExperimentalAudioProcessors", _show_experimental_audio_processors ? "1" : "0");
        /* [XML] LogTypes Types of logging to write; a bitfield where 1 is general notes, 2 warnings, 4 errors, 8 debug information related
           to 3D, 16 debug information related to encoding, 32 debug information for timing purposes, 64 debug information related
           to sending email, 128 debug information related to the video view, 256 information about disk writing, 512 debug information
           related to the player, 1024 debug information related to audio analyses.
        */
-       root->add_child("LogTypes")->add_child_text (raw_convert<string> (_log_types));
+       cxml::add_text_child(root, "LogTypes", raw_convert<string> (_log_types));
        /* [XML] AnalyseEBUR128 1 to do EBUR128 analyses when analysing audio, otherwise 0. */
-       root->add_child("AnalyseEBUR128")->add_child_text (_analyse_ebur128 ? "1" : "0");
+       cxml::add_text_child(root, "AnalyseEBUR128", _analyse_ebur128 ? "1" : "0");
        /* [XML] AutomaticAudioAnalysis 1 to run audio analysis automatically when audio content is added to the film, otherwise 0. */
-       root->add_child("AutomaticAudioAnalysis")->add_child_text (_automatic_audio_analysis ? "1" : "0");
+       cxml::add_text_child(root, "AutomaticAudioAnalysis", _automatic_audio_analysis ? "1" : "0");
 #ifdef DCPOMATIC_WINDOWS
        if (_win32_console) {
                /* [XML] Win32Console 1 to open a console when running on Windows, otherwise 0.
                 * We only write this if it's true, which is a bit of a hack to allow unit tests to work
                 * more easily on Windows (without a platform-specific reference in config_write_utf8_test)
                 */
-               root->add_child("Win32Console")->add_child_text ("1");
+               cxml::add_text_child(root, "Win32Console", "1");
        }
 #endif
 
        /* [XML] Signer Certificate chain and private key to use when signing DCPs and KDMs.  Should contain <code>&lt;Certificate&gt;</code>
           tags in order and a <code>&lt;PrivateKey&gt;</code> tag all containing PEM-encoded certificates or private keys as appropriate.
        */
-       auto signer = root->add_child ("Signer");
+       auto signer = cxml::add_child(root, "Signer");
        DCPOMATIC_ASSERT (_signer_chain);
        for (auto const& i: _signer_chain->unordered()) {
-               signer->add_child("Certificate")->add_child_text (i.certificate (true));
+               cxml::add_text_child(signer, "Certificate", i.certificate (true));
        }
-       signer->add_child("PrivateKey")->add_child_text (_signer_chain->key().get ());
+       cxml::add_text_child(signer, "PrivateKey", _signer_chain->key().get ());
 
        /* [XML] Decryption Certificate chain and private key to use when decrypting KDMs */
-       auto decryption = root->add_child ("Decryption");
+       auto decryption = cxml::add_child(root, "Decryption");
        DCPOMATIC_ASSERT (_decryption_chain);
        for (auto const& i: _decryption_chain->unordered()) {
-               decryption->add_child("Certificate")->add_child_text (i.certificate (true));
+               cxml::add_text_child(decryption, "Certificate", i.certificate (true));
        }
-       decryption->add_child("PrivateKey")->add_child_text (_decryption_chain->key().get ());
+       cxml::add_text_child(decryption, "PrivateKey", _decryption_chain->key().get());
 
        /* [XML] History Filename of DCP to present in the <guilabel>File</guilabel> menu of the GUI; there can be more than one
           of these tags.
        */
        for (auto i: _history) {
-               root->add_child("History")->add_child_text (i.string ());
+               cxml::add_text_child(root, "History", i.string());
        }
 
        /* [XML] History Filename of DCP to present in the <guilabel>File</guilabel> menu of the player; there can be more than one
           of these tags.
        */
        for (auto i: _player_history) {
-               root->add_child("PlayerHistory")->add_child_text (i.string ());
+               cxml::add_text_child(root, "PlayerHistory", i.string());
        }
 
        /* [XML] DKDMGroup A group of DKDMs, each with a <code>Name</code> attribute, containing other <code>&lt;DKDMGroup&gt;</code>
@@ -942,53 +957,53 @@ Config::write_config () const
        _dkdms->as_xml (root);
 
        /* [XML] CinemasFile Filename of cinemas list file. */
-       root->add_child("CinemasFile")->add_child_text (_cinemas_file.string());
+       cxml::add_text_child(root, "CinemasFile", _cinemas_file.string());
        /* [XML] DKDMRecipientsFile Filename of DKDM recipients list file. */
-       root->add_child("DKDMRecipientsFile")->add_child_text (_dkdm_recipients_file.string());
+       cxml::add_text_child(root, "DKDMRecipientsFile", _dkdm_recipients_file.string());
        /* [XML] ShowHintsBeforeMakeDCP 1 to show hints in the GUI before making a DCP, otherwise 0. */
-       root->add_child("ShowHintsBeforeMakeDCP")->add_child_text (_show_hints_before_make_dcp ? "1" : "0");
+       cxml::add_text_child(root, "ShowHintsBeforeMakeDCP", _show_hints_before_make_dcp ? "1" : "0");
        /* [XML] ConfirmKDMEmail 1 to confirm before sending KDM emails in the GUI, otherwise 0. */
-       root->add_child("ConfirmKDMEmail")->add_child_text (_confirm_kdm_email ? "1" : "0");
+       cxml::add_text_child(root, "ConfirmKDMEmail", _confirm_kdm_email ? "1" : "0");
        /* [XML] KDMFilenameFormat Format for KDM filenames. */
-       root->add_child("KDMFilenameFormat")->add_child_text (_kdm_filename_format.specification ());
+       cxml::add_text_child(root, "KDMFilenameFormat", _kdm_filename_format.specification());
        /* [XML] KDMFilenameFormat Format for DKDM filenames. */
-       root->add_child("DKDMFilenameFormat")->add_child_text(_dkdm_filename_format.specification());
+       cxml::add_text_child(root, "DKDMFilenameFormat", _dkdm_filename_format.specification());
        /* [XML] KDMContainerNameFormat Format for KDM containers (directories or ZIP files). */
-       root->add_child("KDMContainerNameFormat")->add_child_text (_kdm_container_name_format.specification ());
+       cxml::add_text_child(root, "KDMContainerNameFormat", _kdm_container_name_format.specification());
        /* [XML] DCPMetadataFilenameFormat Format for DCP metadata filenames. */
-       root->add_child("DCPMetadataFilenameFormat")->add_child_text (_dcp_metadata_filename_format.specification ());
+       cxml::add_text_child(root, "DCPMetadataFilenameFormat", _dcp_metadata_filename_format.specification());
        /* [XML] DCPAssetFilenameFormat Format for DCP asset filenames. */
-       root->add_child("DCPAssetFilenameFormat")->add_child_text (_dcp_asset_filename_format.specification ());
+       cxml::add_text_child(root, "DCPAssetFilenameFormat", _dcp_asset_filename_format.specification());
        /* [XML] JumpToSelected 1 to make the GUI jump to the start of content when it is selected, otherwise 0. */
-       root->add_child("JumpToSelected")->add_child_text (_jump_to_selected ? "1" : "0");
+       cxml::add_text_child(root, "JumpToSelected", _jump_to_selected ? "1" : "0");
        /* [XML] Nagged 1 if a particular nag screen has been shown and should not be shown again, otherwise 0. */
        for (int i = 0; i < NAG_COUNT; ++i) {
-               xmlpp::Element* e = root->add_child ("Nagged");
-               e->set_attribute ("Id", raw_convert<string>(i));
+               auto e = cxml::add_child(root, "Nagged");
+               e->set_attribute("id", raw_convert<string>(i));
                e->add_child_text (_nagged[i] ? "1" : "0");
        }
        /* [XML] PreviewSound 1 to use sound in the GUI preview and player, otherwise 0. */
-       root->add_child("PreviewSound")->add_child_text (_sound ? "1" : "0");
+       cxml::add_text_child(root, "PreviewSound", _sound ? "1" : "0");
        if (_sound_output) {
                /* [XML:opt] PreviewSoundOutput Name of the audio output to use. */
-               root->add_child("PreviewSoundOutput")->add_child_text (_sound_output.get());
+               cxml::add_text_child(root, "PreviewSoundOutput", _sound_output.get());
        }
        /* [XML] CoverSheet Text of the cover sheet to write when making DCPs. */
-       root->add_child("CoverSheet")->add_child_text (_cover_sheet);
+       cxml::add_text_child(root, "CoverSheet", _cover_sheet);
        if (_last_player_load_directory) {
-               root->add_child("LastPlayerLoadDirectory")->add_child_text(_last_player_load_directory->string());
+               cxml::add_text_child(root, "LastPlayerLoadDirectory", _last_player_load_directory->string());
        }
        /* [XML] LastKDMWriteType Last type of KDM-write: <code>flat</code> for a flat file, <code>folder</code> for a folder or <code>zip</code> for a ZIP file. */
        if (_last_kdm_write_type) {
                switch (_last_kdm_write_type.get()) {
                case KDM_WRITE_FLAT:
-                       root->add_child("LastKDMWriteType")->add_child_text("flat");
+                       cxml::add_text_child(root, "LastKDMWriteType", "flat");
                        break;
                case KDM_WRITE_FOLDER:
-                       root->add_child("LastKDMWriteType")->add_child_text("folder");
+                       cxml::add_text_child(root, "LastKDMWriteType", "folder");
                        break;
                case KDM_WRITE_ZIP:
-                       root->add_child("LastKDMWriteType")->add_child_text("zip");
+                       cxml::add_text_child(root, "LastKDMWriteType", "zip");
                        break;
                }
        }
@@ -996,58 +1011,58 @@ Config::write_config () const
        if (_last_dkdm_write_type) {
                switch (_last_dkdm_write_type.get()) {
                case DKDM_WRITE_INTERNAL:
-                       root->add_child("LastDKDMWriteType")->add_child_text("internal");
+                       cxml::add_text_child(root, "LastDKDMWriteType", "internal");
                        break;
                case DKDM_WRITE_FILE:
-                       root->add_child("LastDKDMWriteType")->add_child_text("file");
+                       cxml::add_text_child(root, "LastDKDMWriteType", "file");
                        break;
                }
        }
        /* [XML] FramesInMemoryMultiplier value to multiply the encoding threads count by to get the maximum number of
           frames to be held in memory at once.
        */
-       root->add_child("FramesInMemoryMultiplier")->add_child_text(raw_convert<string>(_frames_in_memory_multiplier));
+       cxml::add_text_child(root, "FramesInMemoryMultiplier", raw_convert<string>(_frames_in_memory_multiplier));
 
        /* [XML] DecodeReduction power of 2 to reduce DCP images by before decoding in the player. */
        if (_decode_reduction) {
-               root->add_child("DecodeReduction")->add_child_text(raw_convert<string>(_decode_reduction.get()));
+               cxml::add_text_child(root, "DecodeReduction", raw_convert<string>(_decode_reduction.get()));
        }
 
        /* [XML] DefaultNotify 1 to default jobs to notify when complete, otherwise 0. */
-       root->add_child("DefaultNotify")->add_child_text(_default_notify ? "1" : "0");
+       cxml::add_text_child(root, "DefaultNotify", _default_notify ? "1" : "0");
 
        /* [XML] Notification 1 if a notification type is enabled, otherwise 0. */
        for (int i = 0; i < NOTIFICATION_COUNT; ++i) {
-               xmlpp::Element* e = root->add_child ("Notification");
-               e->set_attribute ("Id", raw_convert<string>(i));
+               auto e = cxml::add_child(root, "Notification");
+               e->set_attribute ("id", raw_convert<string>(i));
                e->add_child_text (_notification[i] ? "1" : "0");
        }
 
        if (_barco_username) {
                /* [XML] BarcoUsername Username for logging into Barco's servers when downloading server certificates. */
-               root->add_child("BarcoUsername")->add_child_text(*_barco_username);
+               cxml::add_text_child(root, "BarcoUsername", *_barco_username);
        }
        if (_barco_password) {
                /* [XML] BarcoPassword Password for logging into Barco's servers when downloading server certificates. */
-               root->add_child("BarcoPassword")->add_child_text(*_barco_password);
+               cxml::add_text_child(root, "BarcoPassword", *_barco_password);
        }
 
        if (_christie_username) {
                /* [XML] ChristieUsername Username for logging into Christie's servers when downloading server certificates. */
-               root->add_child("ChristieUsername")->add_child_text(*_christie_username);
+               cxml::add_text_child(root, "ChristieUsername", *_christie_username);
        }
        if (_christie_password) {
                /* [XML] ChristiePassword Password for logging into Christie's servers when downloading server certificates. */
-               root->add_child("ChristiePassword")->add_child_text(*_christie_password);
+               cxml::add_text_child(root, "ChristiePassword", *_christie_password);
        }
 
        if (_gdc_username) {
                /* [XML] GDCUsername Username for logging into GDC's servers when downloading server certificates. */
-               root->add_child("GDCUsername")->add_child_text(*_gdc_username);
+               cxml::add_text_child(root, "GDCUsername", *_gdc_username);
        }
        if (_gdc_password) {
                /* [XML] GDCPassword Password for logging into GDC's servers when downloading server certificates. */
-               root->add_child("GDCPassword")->add_child_text(*_gdc_password);
+               cxml::add_text_child(root, "GDCPassword", *_gdc_password);
        }
 
        /* [XML] PlayerMode <code>window</code> for a single window, <code>full</code> for full-screen and <code>dual</code> for full screen playback
@@ -1055,80 +1070,94 @@ Config::write_config () const
        */
        switch (_player_mode) {
        case PLAYER_MODE_WINDOW:
-               root->add_child("PlayerMode")->add_child_text("window");
+               cxml::add_text_child(root, "PlayerMode", "window");
                break;
        case PLAYER_MODE_FULL:
-               root->add_child("PlayerMode")->add_child_text("full");
+               cxml::add_text_child(root, "PlayerMode", "full");
                break;
        case PLAYER_MODE_DUAL:
-               root->add_child("PlayerMode")->add_child_text("dual");
+               cxml::add_text_child(root, "PlayerMode", "dual");
                break;
        }
 
+       if (_player_restricted_menus) {
+               cxml::add_text_child(root, "PlayerRestrictedMenus", "1");
+       }
+
+       if (_playlist_editor_restricted_menus) {
+               cxml::add_text_child(root, "PlaylistEditorRestrictedMenus", "1");
+       }
+
        /* [XML] ImageDisplay Screen number to put image on in dual-screen player mode. */
-       root->add_child("ImageDisplay")->add_child_text(raw_convert<string>(_image_display));
+       cxml::add_text_child(root, "ImageDisplay", raw_convert<string>(_image_display));
        switch (_video_view_type) {
        case VIDEO_VIEW_SIMPLE:
-               root->add_child("VideoViewType")->add_child_text("simple");
+               cxml::add_text_child(root, "VideoViewType", "simple");
                break;
        case VIDEO_VIEW_OPENGL:
-               root->add_child("VideoViewType")->add_child_text("opengl");
+               cxml::add_text_child(root, "VideoViewType", "opengl");
                break;
        }
        /* [XML] RespectKDMValidityPeriods 1 to refuse to use KDMs that are out of date, 0 to ignore KDM dates. */
-       root->add_child("RespectKDMValidityPeriods")->add_child_text(_respect_kdm_validity_periods ? "1" : "0");
+       cxml::add_text_child(root, "RespectKDMValidityPeriods", _respect_kdm_validity_periods ? "1" : "0");
        if (_player_debug_log_file) {
                /* [XML] PlayerLogFile Filename to use for player debug logs. */
-               root->add_child("PlayerDebugLogFile")->add_child_text(_player_debug_log_file->string());
+               cxml::add_text_child(root, "PlayerDebugLogFile", _player_debug_log_file->string());
        }
        if (_player_content_directory) {
                /* [XML] PlayerContentDirectory Directory to use for player content in the dual-screen mode. */
-               root->add_child("PlayerContentDirectory")->add_child_text(_player_content_directory->string());
+               cxml::add_text_child(root, "PlayerContentDirectory", _player_content_directory->string());
        }
        if (_player_playlist_directory) {
                /* [XML] PlayerPlaylistDirectory Directory to use for player playlists in the dual-screen mode. */
-               root->add_child("PlayerPlaylistDirectory")->add_child_text(_player_playlist_directory->string());
+               cxml::add_text_child(root, "PlayerPlaylistDirectory", _player_playlist_directory->string());
        }
        if (_player_kdm_directory) {
                /* [XML] PlayerKDMDirectory Directory to use for player KDMs in the dual-screen mode. */
-               root->add_child("PlayerKDMDirectory")->add_child_text(_player_kdm_directory->string());
+               cxml::add_text_child(root, "PlayerKDMDirectory", _player_kdm_directory->string());
        }
        if (_audio_mapping) {
-               _audio_mapping->as_xml (root->add_child("AudioMapping"));
+               _audio_mapping->as_xml(cxml::add_child(root, "AudioMapping"));
        }
        for (auto const& i: _custom_languages) {
-               root->add_child("CustomLanguage")->add_child_text(i.to_string());
+               cxml::add_text_child(root, "CustomLanguage", i.to_string());
        }
        for (auto const& initial: _initial_paths) {
                if (initial.second) {
-                       root->add_child(initial.first)->add_child_text(initial.second->string());
+                       cxml::add_text_child(root, initial.first, initial.second->string());
                }
        }
-       root->add_child("UseISDCFNameByDefault")->add_child_text(_use_isdcf_name_by_default ? "1" : "0");
-       root->add_child("WriteKDMsToDisk")->add_child_text(_write_kdms_to_disk ? "1" : "0");
-       root->add_child("EmailKDMs")->add_child_text(_email_kdms ? "1" : "0");
-       root->add_child("DefaultKDMType")->add_child_text(dcp::formulation_to_string(_default_kdm_type));
-       root->add_child("AutoCropThreshold")->add_child_text(raw_convert<string>(_auto_crop_threshold));
+       cxml::add_text_child(root, "UseISDCFNameByDefault", _use_isdcf_name_by_default ? "1" : "0");
+       cxml::add_text_child(root, "WriteKDMsToDisk", _write_kdms_to_disk ? "1" : "0");
+       cxml::add_text_child(root, "EmailKDMs", _email_kdms ? "1" : "0");
+       cxml::add_text_child(root, "DefaultKDMType", dcp::formulation_to_string(_default_kdm_type));
+       cxml::add_text_child(root, "AutoCropThreshold", raw_convert<string>(_auto_crop_threshold));
        if (_last_release_notes_version) {
-               root->add_child("LastReleaseNotesVersion")->add_child_text(*_last_release_notes_version);
+               cxml::add_text_child(root, "LastReleaseNotesVersion", *_last_release_notes_version);
        }
        if (_main_divider_sash_position) {
-               root->add_child("MainDividerSashPosition")->add_child_text(raw_convert<string>(*_main_divider_sash_position));
+               cxml::add_text_child(root, "MainDividerSashPosition", raw_convert<string>(*_main_divider_sash_position));
        }
        if (_main_content_divider_sash_position) {
-               root->add_child("MainContentDividerSashPosition")->add_child_text(raw_convert<string>(*_main_content_divider_sash_position));
+               cxml::add_text_child(root, "MainContentDividerSashPosition", raw_convert<string>(*_main_content_divider_sash_position));
        }
 
-       root->add_child("DefaultAddFileLocation")->add_child_text(
+       cxml::add_text_child(root, "DefaultAddFileLocation",
                _default_add_file_location == DefaultAddFileLocation::SAME_AS_LAST_TIME ? "last" : "project"
                );
 
        /* [XML] AllowSMPTEBv20 1 to allow the user to choose SMPTE (Bv2.0 only) as a standard, otherwise 0 */
-       root->add_child("AllowSMPTEBv20")->add_child_text(_allow_smpte_bv20 ? "1" : "0");
+       cxml::add_text_child(root, "AllowSMPTEBv20", _allow_smpte_bv20 ? "1" : "0");
        /* [XML] ISDCFNamePartLength Maximum length of the "name" part of an ISDCF name, which should be 14 according to the standard */
-       root->add_child("ISDCFNamePartLength")->add_child_text(raw_convert<string>(_isdcf_name_part_length));
+       cxml::add_text_child(root, "ISDCFNamePartLength", raw_convert<string>(_isdcf_name_part_length));
 
-       _export.write(root->add_child("Export"));
+#ifdef DCPOMATIC_GROK
+       if (_grok) {
+               _grok->as_xml(cxml::add_child(root, "Grok"));
+       }
+#endif
+
+       _export.write(cxml::add_child(root, "Export"));
 
        auto target = config_write_file();
 
@@ -1157,10 +1186,10 @@ write_file (string root_node, string node, string version, list<shared_ptr<T>> t
 {
        xmlpp::Document doc;
        auto root = doc.create_root_node (root_node);
-       root->add_child("Version")->add_child_text(version);
+       cxml::add_text_child(root, "Version", version);
 
        for (auto i: things) {
-               i->as_xml (root->add_child(node));
+               i->as_xml(cxml::add_child(root, node));
        }
 
        try {
@@ -1500,7 +1529,7 @@ void
 Config::link (boost::filesystem::path new_file) const
 {
        xmlpp::Document doc;
-       doc.create_root_node("Config")->add_child("Link")->add_child_text(new_file.string());
+       cxml::add_text_child(doc.create_root_node("Config"), "Link", new_file.string());
        try {
                doc.write_to_file_formatted(write_path("config.xml").string());
        } catch (xmlpp::exception& e) {
@@ -1685,3 +1714,38 @@ Config::initial_path(string id) const
        return iter->second;
 }
 
+
+#ifdef DCPOMATIC_GROK
+
+Config::Grok::Grok(cxml::ConstNodePtr node)
+       : enable(node->bool_child("Enable"))
+       , binary_location(node->string_child("BinaryLocation"))
+       , selected(node->number_child<int>("Selected"))
+       , licence_server(node->string_child("LicenceServer"))
+       , licence_port(node->number_child<int>("LicencePort"))
+       , licence(node->string_child("Licence"))
+{
+
+}
+
+
+void
+Config::Grok::as_xml(xmlpp::Element* node) const
+{
+       node->add_child("BinaryLocation")->add_child_text(binary_location.string());
+       node->add_child("Enable")->add_child_text((enable ? "1" : "0"));
+       node->add_child("Selected")->add_child_text(raw_convert<string>(selected));
+       node->add_child("LicenceServer")->add_child_text(licence_server);
+       node->add_child("LicencePort")->add_child_text(raw_convert<string>(licence_port));
+       node->add_child("Licence")->add_child_text(licence);
+}
+
+
+void
+Config::set_grok(Grok const& grok)
+{
+       _grok = grok;
+       changed(GROK);
+}
+
+#endif
index f3d080b0bdc4552c00055418695a003d0f6d7814..ec804c9bf58cbdad822028f2da3f7f135b972706 100644 (file)
@@ -97,6 +97,9 @@ public:
                AUTO_CROP_THRESHOLD,
                ALLOW_SMPTE_BV20,
                ISDCF_NAME_PART_LENGTH,
+#ifdef DCPOMATIC_GROK
+               GROK,
+#endif
                OTHER
        };
 
@@ -531,6 +534,14 @@ public:
                return _player_mode;
        }
 
+       bool player_restricted_menus() const {
+               return _player_restricted_menus;
+       }
+
+       bool playlist_editor_restricted_menus() const {
+               return _playlist_editor_restricted_menus;
+       }
+
        int image_display () const {
                return _image_display;
        }
@@ -621,6 +632,28 @@ public:
                return _allow_smpte_bv20;
        }
 
+#ifdef DCPOMATIC_GROK
+       class Grok
+       {
+       public:
+               Grok() = default;
+               Grok(cxml::ConstNodePtr node);
+
+               void as_xml(xmlpp::Element* node) const;
+
+               bool enable = false;
+               boost::filesystem::path binary_location;
+               int selected = 0;
+               std::string licence_server;
+               int licence_port = 5000;
+               std::string licence;
+       };
+
+       boost::optional<Grok> grok() const {
+               return _grok;
+       }
+#endif
+
        int isdcf_name_part_length() const {
                return _isdcf_name_part_length;
        }
@@ -1202,10 +1235,15 @@ public:
                maybe_set(_allow_smpte_bv20, allow, ALLOW_SMPTE_BV20);
        }
 
+#ifdef DCPOMATIC_GROK
+       void set_grok(Grok const& grok);
+#endif
+
        void set_isdcf_name_part_length(int length) {
                maybe_set(_isdcf_name_part_length, length, ISDCF_NAME_PART_LENGTH);
        }
 
+
        void changed (Property p = OTHER);
        boost::signals2::signal<void (Property)> Changed;
        /** Emitted if read() failed on an existing Config file.  There is nothing
@@ -1419,6 +1457,8 @@ private:
        boost::optional<std::string> _gdc_username;
        boost::optional<std::string> _gdc_password;
        PlayerMode _player_mode;
+       bool _player_restricted_menus = false;
+       bool _playlist_editor_restricted_menus = false;
        int _image_display;
        VideoViewType _video_view_type;
        bool _respect_kdm_validity_periods;
@@ -1447,6 +1487,10 @@ private:
        bool _allow_smpte_bv20;
        int _isdcf_name_part_length;
 
+#ifdef DCPOMATIC_GROK
+       boost::optional<Grok> _grok;
+#endif
+
        ExportConfig _export;
 
        static int const _current_version;
index 3b187155453d32b0a6f822e799fbbe08f918432a..bfe144420281ae7805408884bbebaf68eed01f59 100644 (file)
@@ -47,6 +47,7 @@
 #define MAX_CLOSED_CAPTION_XML_SIZE (256 * 1024)
 #define MAX_CLOSED_CAPTION_XML_SIZE_TEXT "256KB"
 #define CERTIFICATE_VALIDITY_PERIOD (10 * 365)
+#define SNAP_SUBDIVISION 64
 
 
 #endif
index 6324050ec69a22f056cc3bedb4c1c7ae2ff002f0..6743705d9b94659fd019c7b6286778d0f7d75919 100644 (file)
@@ -140,23 +140,23 @@ Content::Content (vector<shared_ptr<Content>> c)
 
 
 void
-Content::as_xml (xmlpp::Node* node, bool with_paths) const
+Content::as_xml(xmlpp::Element* element, bool with_paths) const
 {
        boost::mutex::scoped_lock lm (_mutex);
 
        if (with_paths) {
                for (size_t i = 0; i < _paths.size(); ++i) {
-                       auto p = node->add_child("Path");
+                       auto p = cxml::add_child(element, "Path");
                        p->add_child_text (_paths[i].string());
                        p->set_attribute ("mtime", raw_convert<string>(_last_write_times[i]));
                }
        }
-       node->add_child("Digest")->add_child_text(_digest);
-       node->add_child("Position")->add_child_text(raw_convert<string>(_position.get()));
-       node->add_child("TrimStart")->add_child_text(raw_convert<string>(_trim_start.get()));
-       node->add_child("TrimEnd")->add_child_text(raw_convert<string>(_trim_end.get()));
+       cxml::add_text_child(element, "Digest", _digest);
+       cxml::add_text_child(element, "Position", raw_convert<string>(_position.get()));
+       cxml::add_text_child(element, "TrimStart", raw_convert<string>(_trim_start.get()));
+       cxml::add_text_child(element, "TrimEnd", raw_convert<string>(_trim_end.get()));
        if (_video_frame_rate) {
-               node->add_child("VideoFrameRate")->add_child_text(raw_convert<string>(_video_frame_rate.get()));
+               cxml::add_text_child(element, "VideoFrameRate", raw_convert<string>(_video_frame_rate.get()));
        }
 }
 
index f2fecddf063df96a2546fffb66cd82722922aa13..3753c84d701fdda7409b07f44289d8eebcad13da 100644 (file)
@@ -100,7 +100,7 @@ public:
         */
        virtual std::string technical_summary () const;
 
-       virtual void as_xml (xmlpp::Node *, bool with_paths) const;
+       virtual void as_xml(xmlpp::Element* element, bool with_paths) const;
        virtual dcpomatic::DCPTime full_length (std::shared_ptr<const Film>) const = 0;
        virtual dcpomatic::DCPTime approximate_length () const = 0;
        virtual std::string identifier () const;
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 d2d020a9fb36cb96d0f4bca83507432ba41deedf..a6f7d4bacd6948272a7db73e9a6d3ea8c7b2f241 100644 (file)
@@ -42,12 +42,12 @@ Crop::Crop(shared_ptr<cxml::Node> node)
 
 
 void
-Crop::as_xml(xmlpp::Node* node) const
+Crop::as_xml(xmlpp::Element* element) const
 {
-       node->add_child("LeftCrop")->add_child_text(raw_convert<string>(left));
-       node->add_child("RightCrop")->add_child_text(raw_convert<string>(right));
-       node->add_child("TopCrop")->add_child_text(raw_convert<string>(top));
-       node->add_child("BottomCrop")->add_child_text(raw_convert<string>(bottom));
+       cxml::add_text_child(element, "LeftCrop", raw_convert<string>(left));
+       cxml::add_text_child(element, "RightCrop", raw_convert<string>(right));
+       cxml::add_text_child(element, "TopCrop", raw_convert<string>(top));
+       cxml::add_text_child(element, "BottomCrop", raw_convert<string>(bottom));
 }
 
 
index 00734d5e9c63e4d732b341b2e4983487ee3a3455..24330731dec59ac9429489ee5e30c2f5d9990daa 100644 (file)
@@ -65,7 +65,7 @@ struct Crop
                return s;
        }
 
-       void as_xml (xmlpp::Node *) const;
+       void as_xml(xmlpp::Element*) const;
 };
 
 
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..1e145c8fc5b19285519e93c8064e7751b8efe2cc 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;
@@ -62,16 +59,16 @@ Drive::as_xml () const
 {
        xmlpp::Document doc;
        auto root = doc.create_root_node ("Drive");
-       root->add_child("Device")->add_child_text(_device);
+       cxml::add_text_child(root, "Device", _device);
        for (auto i: _mount_points) {
-               root->add_child("MountPoint")->add_child_text(i.string());
+               cxml::add_text_child(root, "MountPoint", i.string());
        }
-       root->add_child("Size")->add_child_text(dcp::raw_convert<string>(_size));
+       cxml::add_text_child(root, "Size", dcp::raw_convert<string>(_size));
        if (_vendor) {
-               root->add_child("Vendor")->add_child_text(*_vendor);
+               cxml::add_text_child(root, "Vendor", *_vendor);
        }
        if (_model) {
-               root->add_child("Model")->add_child_text(*_model);
+               cxml::add_text_child(root, "Model", *_model);
        }
 
        return doc.write_to_string("UTF-8");
@@ -122,97 +119,3 @@ Drive::log_summary () const
                        );
 }
 
-
-
-/* This is in _common so we can use it in unit tests */
-optional<OSXMediaPath>
-analyse_osx_media_path (string path)
-{
-       if (path.find("/IOHDIXController") != string::npos) {
-               /* This is a disk image, so we completely ignore it */
-               LOG_DISK_NC("Ignoring this as it seems to be a disk image");
-               return {};
-       }
-
-       OSXMediaPath mp;
-       vector<string> parts;
-       split(parts, path, boost::is_any_of("/"));
-       std::copy(parts.begin() + 1, parts.end(), back_inserter(mp.parts));
-
-       if (!parts.empty() && parts[0] == "IODeviceTree:") {
-               mp.real = true;
-               if (mp.parts.size() < MEDIA_PATH_REQUIRED_MATCHES) {
-                       /* Later we expect at least MEDIA_PATH_REQUIRED_MATCHES parts in a IODeviceTree */
-                       LOG_DISK_NC("Ignoring this as it has a strange media path");
-                       return {};
-               }
-       } else if (!parts.empty() && parts[0] == "IOService:") {
-               mp.real = false;
-       } else {
-               return {};
-       }
-
-       return mp;
-}
-
-
-/* Take some OSXDisk objects, representing disks that `DARegisterDiskAppearedCallback` told us about,
- * and find those drives that we could write a DCP to.  The drives returned are "real" (not synthesized)
- * and are whole disks (not partitions).  They may be mounted, or contain mounted partitions, in which
- * their mounted() method will return true.
- */
-vector<Drive>
-osx_disks_to_drives (vector<OSXDisk> disks)
-{
-       using namespace boost::algorithm;
-
-       /* Mark disks containing mounted partitions as themselves mounted */
-       for (auto& i: disks) {
-               if (!i.whole) {
-                       continue;
-               }
-               for (auto& j: disks) {
-                       if (&i != &j && !j.mount_points.empty() && starts_with(j.device, i.device)) {
-                               LOG_DISK("Marking %1 as mounted because %2 is", i.device, j.device);
-                               std::copy(j.mount_points.begin(), j.mount_points.end(), back_inserter(i.mount_points));
-                       }
-               }
-       }
-
-       /* Mark containers of mounted synths as themselves mounted */
-       for (auto& i: disks) {
-               if (i.media_path.real) {
-                       for (auto& j: disks) {
-                               if (!j.media_path.real && !j.mount_points.empty()) {
-                                       /* i is real, j is a mounted synth; if we see the first MEDIA_PATH_REQUIRED_MATCHES parts
-                                        * of i anywhere in j we assume they are related and so i shares j's mount points.
-                                        */
-                                       bool one_missing = false;
-                                       string all_parts;
-                                       DCPOMATIC_ASSERT (i.media_path.parts.size() >= MEDIA_PATH_REQUIRED_MATCHES);
-                                       for (auto k = 0; k < MEDIA_PATH_REQUIRED_MATCHES; ++k) {
-                                               if (find(j.media_path.parts.begin(), j.media_path.parts.end(), i.media_path.parts[k]) == j.media_path.parts.end()) {
-                                                       one_missing = true;
-                                               }
-                                               all_parts += i.media_path.parts[k] + " ";
-                                       }
-
-                                       if (!one_missing) {
-                                               LOG_DISK("Marking %1 as mounted because %2 is (found %3)", i.device, j.device, all_parts);
-                                               std::copy(j.mount_points.begin(), j.mount_points.end(), back_inserter(i.mount_points));
-                                       }
-                               }
-                       }
-               }
-       }
-
-       vector<Drive> drives;
-       for (auto const& i: disks) {
-               if (i.whole && i.media_path.real) {
-                       drives.push_back(Drive(i.device, i.mount_points, i.size, i.vendor, i.model));
-                       LOG_DISK_NC(drives.back().log_summary());
-               }
-       }
-
-       return drives;
-}
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 bdd5e0e091c0aa6b523ec7c7400ec9449beef3a0..e9afa04cc1cdf9136d87d5c66455422d20652438 100644 (file)
@@ -163,6 +163,13 @@ DCPContent::DCPContent (cxml::ConstNodePtr node, int version)
        }
 
        _active_audio_channels = node->optional_number_child<int>("ActiveAudioChannels");
+
+       for (auto non_zero: node->node_children("HasNonZeroEntryPoint")) {
+               try {
+                       auto type = string_to_text_type(non_zero->string_attribute("type"));
+                       _has_non_zero_entry_point[type] = non_zero->content() == "1";
+               } catch (MetadataError&) {}
+       }
 }
 
 void
@@ -308,6 +315,7 @@ DCPContent::examine (shared_ptr<const Film> film, shared_ptr<Job> job)
                }
                _ratings = examiner->ratings ();
                _content_versions = examiner->content_versions ();
+               _has_non_zero_entry_point = examiner->has_non_zero_entry_point();
        }
 
        if (needed_assets == needs_assets()) {
@@ -347,88 +355,98 @@ DCPContent::technical_summary () const
        return s;
 }
 
+
 void
-DCPContent::as_xml (xmlpp::Node* node, bool with_paths) const
+DCPContent::as_xml(xmlpp::Element* element, bool with_paths) const
 {
-       node->add_child("Type")->add_child_text ("DCP");
+       cxml::add_text_child(element, "Type", "DCP");
 
-       Content::as_xml (node, with_paths);
+       Content::as_xml(element, with_paths);
 
        if (video) {
-               video->as_xml (node);
+               video->as_xml(element);
        }
 
        if (audio) {
-               audio->as_xml (node);
-               node->add_child("AudioFrameRate")->add_child_text (raw_convert<string> (audio->stream()->frame_rate()));
-               node->add_child("AudioLength")->add_child_text (raw_convert<string> (audio->stream()->length()));
-               audio->stream()->mapping().as_xml (node->add_child("AudioMapping"));
+               audio->as_xml(element);
+               cxml::add_text_child(element, "AudioFrameRate", raw_convert<string>(audio->stream()->frame_rate()));
+               cxml::add_text_child(element, "AudioLength", raw_convert<string>(audio->stream()->length()));
+               audio->stream()->mapping().as_xml(cxml::add_child(element, "AudioMapping"));
        }
 
        for (auto i: text) {
-               i->as_xml (node);
+               i->as_xml(element);
        }
 
        if (atmos) {
-               atmos->as_xml (node);
+               atmos->as_xml(element);
        }
 
        boost::mutex::scoped_lock lm (_mutex);
 
-       node->add_child("Name")->add_child_text (_name);
-       node->add_child("Encrypted")->add_child_text (_encrypted ? "1" : "0");
-       node->add_child("NeedsAssets")->add_child_text (_needs_assets ? "1" : "0");
+       cxml::add_text_child(element, "Name", _name);
+       cxml::add_text_child(element, "Encrypted", _encrypted ? "1" : "0");
+       cxml::add_text_child(element, "NeedsAssets", _needs_assets ? "1" : "0");
        if (_kdm) {
-               node->add_child("KDM")->add_child_text (_kdm->as_xml ());
+               cxml::add_text_child(element, "KDM", _kdm->as_xml());
        }
-       node->add_child("KDMValid")->add_child_text (_kdm_valid ? "1" : "0");
-       node->add_child("ReferenceVideo")->add_child_text (_reference_video ? "1" : "0");
-       node->add_child("ReferenceAudio")->add_child_text (_reference_audio ? "1" : "0");
-       node->add_child("ReferenceOpenSubtitle")->add_child_text(_reference_text[TextType::OPEN_SUBTITLE] ? "1" : "0");
-       node->add_child("ReferenceClosedCaption")->add_child_text(_reference_text[TextType::CLOSED_CAPTION] ? "1" : "0");
+       cxml::add_text_child(element, "KDMValid", _kdm_valid ? "1" : "0");
+       cxml::add_text_child(element, "ReferenceVideo", _reference_video ? "1" : "0");
+       cxml::add_text_child(element, "ReferenceAudio", _reference_audio ? "1" : "0");
+       cxml::add_text_child(element, "ReferenceOpenSubtitle", _reference_text[TextType::OPEN_SUBTITLE] ? "1" : "0");
+       cxml::add_text_child(element, "ReferenceClosedCaption", _reference_text[TextType::CLOSED_CAPTION] ? "1" : "0");
        if (_standard) {
                switch (_standard.get ()) {
                case dcp::Standard::INTEROP:
-                       node->add_child("Standard")->add_child_text ("Interop");
+                       cxml::add_text_child(element, "Standard", "Interop");
                        break;
                case dcp::Standard::SMPTE:
-                       node->add_child("Standard")->add_child_text ("SMPTE");
+                       cxml::add_text_child(element, "Standard", "SMPTE");
                        break;
                default:
                        DCPOMATIC_ASSERT (false);
                }
        }
-       node->add_child("ThreeD")->add_child_text (_three_d ? "1" : "0");
+       cxml::add_text_child(element, "ThreeD", _three_d ? "1" : "0");
        if (_content_kind) {
-               node->add_child("ContentKind")->add_child_text(_content_kind->name());
+               cxml::add_text_child(element, "ContentKind", _content_kind->name());
        }
        if (_cpl) {
-               node->add_child("CPL")->add_child_text (_cpl.get ());
+               cxml::add_text_child(element, "CPL", _cpl.get());
        }
        for (auto i: _reel_lengths) {
-               node->add_child("ReelLength")->add_child_text (raw_convert<string> (i));
+               cxml::add_text_child(element, "ReelLength", raw_convert<string>(i));
        }
 
        for (auto const& i: _markers) {
-               auto marker = node->add_child("Marker");
+               auto marker = cxml::add_child(element, "Marker");
                marker->set_attribute("type", dcp::marker_to_string(i.first));
                marker->add_child_text(raw_convert<string>(i.second.get()));
        }
 
        for (auto i: _ratings) {
-               auto rating = node->add_child("Rating");
+               auto rating = cxml::add_child(element, "Rating");
                i.as_xml (rating);
        }
 
        for (auto i: _content_versions) {
-               node->add_child("ContentVersion")->add_child_text(i);
+               cxml::add_text_child(element, "ContentVersion", i);
        }
 
        if (_active_audio_channels) {
-               node->add_child("ActiveAudioChannels")->add_child_text(raw_convert<string>(*_active_audio_channels));
+               cxml::add_text_child(element, "ActiveAudioChannels", raw_convert<string>(*_active_audio_channels));
+       }
+
+       for (auto i = 0; i < static_cast<int>(TextType::COUNT); ++i) {
+               if (_has_non_zero_entry_point[i]) {
+                       auto has = cxml::add_child(element, "HasNonZeroEntryPoint");
+                       has->add_child_text("1");
+                       has->set_attribute("type", text_type_to_string(static_cast<TextType>(i)));
+               }
        }
 }
 
+
 DCPTime
 DCPContent::full_length (shared_ptr<const Film> film) const
 {
@@ -612,7 +630,7 @@ DCPContent::reel_split_points (shared_ptr<const Film> film) const
 }
 
 bool
-DCPContent::can_reference (shared_ptr<const Film> film, function<bool (shared_ptr<const Content>)> part, string overlapping, string& why_not) const
+DCPContent::can_reference_anything(shared_ptr<const Film> film, string& why_not) const
 {
        /* We must be using the same standard as the film */
        if (_standard) {
@@ -658,15 +676,16 @@ DCPContent::can_reference (shared_ptr<const Film> film, function<bool (shared_pt
                }
        }
 
-       auto a = overlaps (film, film->content(), part, position(), end(film));
-       if (a.size() != 1 || a.front().get() != this) {
-               why_not = overlapping;
-               return false;
-       }
-
        return true;
 }
 
+bool
+DCPContent::overlaps(shared_ptr<const Film> film, function<bool (shared_ptr<const Content>)> part) const
+{
+       auto const a = dcpomatic::overlaps(film, film->content(), part, position(), end(film));
+       return a.size() != 1 || a.front().get() != this;
+}
+
 
 bool
 DCPContent::can_reference_video (shared_ptr<const Film> film, string& why_not) const
@@ -691,35 +710,23 @@ DCPContent::can_reference_video (shared_ptr<const Film> film, string& why_not) c
                return false;
        }
 
-       /// TRANSLATORS: this string will follow "Cannot reference this DCP: "
-       return can_reference(
-               film,
-               [](shared_ptr<const Content> c) {
-                       return static_cast<bool>(c->video) && c->video->use();
-               },
-               _("it overlaps other video content; remove the other content."),
-               why_not
-       );
+       auto part = [](shared_ptr<const Content> c) {
+               return static_cast<bool>(c->video) && c->video->use();
+       };
+
+       if (overlaps(film, part)) {
+               /// TRANSLATORS: this string will follow "Cannot reference this DCP: "
+               why_not = _("it overlaps other video content.");
+               return false;
+       }
+
+       return can_reference_anything(film, why_not);
 }
 
 
 bool
 DCPContent::can_reference_audio (shared_ptr<const Film> film, string& why_not) const
 {
-       shared_ptr<DCPDecoder> decoder;
-       try {
-               decoder = make_shared<DCPDecoder>(film, shared_from_this(), false, film->tolerant(), shared_ptr<DCPDecoder>());
-       } catch (dcp::ReadError &) {
-               /* We couldn't read the DCP, so it's probably missing */
-               return false;
-       } catch (DCPError &) {
-               /* We couldn't read the DCP, so it's probably missing */
-               return false;
-       } catch (dcp::KDMDecryptionError &) {
-               /* We have an incorrect KDM */
-               return false;
-       }
-
        if (audio && audio->stream()) {
                auto const channels = audio->stream()->channels();
                if (channels != film->audio_channels()) {
@@ -728,51 +735,33 @@ DCPContent::can_reference_audio (shared_ptr<const Film> film, string& why_not) c
                }
        }
 
-       /// TRANSLATORS: this string will follow "Cannot reference this DCP: "
-       return can_reference(
-               film, [](shared_ptr<const Content> c) {
-                       return static_cast<bool>(c->audio) && !c->audio->mapping().mapped_output_channels().empty();
-               },
-               _("it overlaps other audio content; remove the other content."),
-               why_not
-       );
+       auto part = [](shared_ptr<const Content> c) {
+               return static_cast<bool>(c->audio) && !c->audio->mapping().mapped_output_channels().empty();
+       };
+
+       if (overlaps(film, part)) {
+               /// TRANSLATORS: this string will follow "Cannot reference this DCP: "
+               why_not = _("it overlaps other audio content.");
+               return false;
+       }
+
+       return can_reference_anything(film, why_not);
 }
 
 
 bool
 DCPContent::can_reference_text (shared_ptr<const Film> film, TextType type, string& why_not) const
 {
-       shared_ptr<DCPDecoder> decoder;
-       try {
-               decoder = make_shared<DCPDecoder>(film, shared_from_this(), false, film->tolerant(), shared_ptr<DCPDecoder>());
-       } catch (dcp::ReadError &) {
-               /* We couldn't read the DCP, so it's probably missing */
-               return false;
-       } catch (DCPError &) {
-               /* We couldn't read the DCP, so it's probably missing */
-               return false;
-       } catch (dcp::KDMDecryptionError &) {
-               /* We have an incorrect KDM */
+       if (_has_non_zero_entry_point[TextType::OPEN_SUBTITLE]) {
+               /// TRANSLATORS: this string will follow "Cannot reference this DCP: "
+               why_not = _("one of its subtitle reels has a non-zero entry point so it must be re-written.");
                return false;
        }
 
-        for (auto i: decoder->reels()) {
-                if (type == TextType::OPEN_SUBTITLE) {
-                       if (i->main_subtitle() && i->main_subtitle()->entry_point().get_value_or(0) != 0) {
-                               /// TRANSLATORS: this string will follow "Cannot reference this DCP: "
-                               why_not = _("one of its subtitle reels has a non-zero entry point so it must be re-written.");
-                               return false;
-                       }
-                }
-               if (type == TextType::CLOSED_CAPTION) {
-                       for (auto j: i->closed_captions()) {
-                               if (j->entry_point().get_value_or(0) != 0) {
-                                       /// TRANSLATORS: this string will follow "Cannot reference this DCP: "
-                                       why_not = _("one of its closed caption has a non-zero entry point so it must be re-written.");
-                                       return false;
-                               }
-                       }
-               }
+       if (_has_non_zero_entry_point[TextType::CLOSED_CAPTION]) {
+               /// TRANSLATORS: this string will follow "Cannot reference this DCP: "
+               why_not = _("one of its closed caption has a non-zero entry point so it must be re-written.");
+               return false;
         }
 
        if (trim_start() != dcpomatic::ContentTime()) {
@@ -781,15 +770,17 @@ DCPContent::can_reference_text (shared_ptr<const Film> film, TextType type, stri
                return false;
        }
 
-       /// TRANSLATORS: this string will follow "Cannot reference this DCP: "
-       return can_reference(
-               film,
-               [type](shared_ptr<const Content> c) {
-                       return std::find_if(c->text.begin(), c->text.end(), [type](shared_ptr<const TextContent> t) { return t->type() == type; }) != c->text.end();
-               },
-               _("they overlap other text content; remove the other content."),
-               why_not
-       );
+       auto part = [type](shared_ptr<const Content> c) {
+               return std::find_if(c->text.begin(), c->text.end(), [type](shared_ptr<const TextContent> t) { return t->type() == type; }) != c->text.end();
+       };
+
+       if (overlaps(film, part)) {
+               /// TRANSLATORS: this string will follow "Cannot reference this DCP: "
+               why_not = _("it overlaps other text content.");
+               return false;
+       }
+
+       return can_reference_anything(film, why_not);
 }
 
 void
index 3753740a2728af929ff17572ffa5c4b2930ce3f7..995e0b68134aa55c44075e399a32dc989d346b8a 100644 (file)
@@ -75,7 +75,7 @@ public:
        void examine (std::shared_ptr<const Film> film, std::shared_ptr<Job>) override;
        std::string summary () const override;
        std::string technical_summary () const override;
-       void as_xml (xmlpp::Node *, bool with_paths) const override;
+       void as_xml(xmlpp::Element*, bool with_paths) const override;
        std::string identifier () const override;
        void take_settings_from (std::shared_ptr<const Content> c) override;
 
@@ -100,6 +100,8 @@ public:
        bool needs_kdm () const;
        bool needs_assets () const;
 
+       bool can_reference_anything(std::shared_ptr<const Film> film, std::string& why_not) const;
+
        void set_reference_video (bool r);
 
        bool reference_video () const {
@@ -186,12 +188,7 @@ private:
        void read_directory (boost::filesystem::path);
        void read_sub_directory (boost::filesystem::path);
        std::list<dcpomatic::DCPTimePeriod> reels (std::shared_ptr<const Film> film) const;
-       bool can_reference (
-               std::shared_ptr<const Film> film,
-               std::function <bool (std::shared_ptr<const Content>)>,
-               std::string overlapping,
-               std::string& why_not
-               ) const;
+       bool overlaps(std::shared_ptr<const Film> film, std::function<bool (std::shared_ptr<const Content>)> part) const;
 
        std::string _name;
        /** true if our DCP is encrypted */
@@ -227,6 +224,7 @@ private:
        std::map<dcp::Marker, dcpomatic::ContentTime> _markers;
        std::vector<dcp::Rating> _ratings;
        std::vector<std::string> _content_versions;
+       EnumIndexedVector<bool, TextType> _has_non_zero_entry_point;
 
        boost::optional<int> _active_audio_channels;
 };
index bc9bf0d6775d47939c365a66d9040e94a557a2a2..d689f138cafd2aaa1599df2918d24fb718284e52 100644 (file)
@@ -28,8 +28,9 @@
 #include "i18n.h"
 
 
+using std::string;
+using std::vector;
 using boost::optional;
-using namespace std;
 
 
 vector<DCPContentType> DCPContentType::_dcp_content_types;
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 eb243b60dbc41e652e624b5ad461dd523e6e8ecf..4278caa986a6762604fef23e471ed2a3166ea0e9 100644 (file)
@@ -42,14 +42,14 @@ template <class R, class A>
 void add_asset(string film_key, shared_ptr<R> reel_asset, shared_ptr<A> asset, xmlpp::Element* reel, string name)
 {
        if (asset) {
-               auto out = reel->add_child(name);
-               out->add_child("Id")->add_child_text("urn:uuid:" + asset->id());
+               auto out = cxml::add_child(reel, name);
+               cxml::add_text_child(out, "Id", "urn:uuid:" + asset->id());
                if (reel_asset->annotation_text()) {
-                       out->add_child("AnnotationText")->add_child_text(reel_asset->annotation_text().get());
+                       cxml::add_text_child(out, "AnnotationText", reel_asset->annotation_text().get());
                }
                if (asset->key_id()) {
-                       out->add_child("KeyId")->add_child_text("urn:uuid:" + asset->key_id().get());
-                       out->add_child("Key")->add_child_text(asset->key() ? asset->key()->hex() : film_key);
+                       cxml::add_text_child(out, "KeyId", "urn:uuid:" + asset->key_id().get());
+                       cxml::add_text_child(out, "Key", asset->key() ? asset->key()->hex() : film_key);
                }
        }
 };
@@ -64,16 +64,16 @@ write_dcp_digest_file (
 {
        xmlpp::Document doc;
        auto root = doc.create_root_node("FHG_DCP_DIGEST", "http://www.fhg.de/2009/04/02/dcpdig");
-       root->add_child("InteropMode")->add_child_text(cpl->standard() == dcp::Standard::INTEROP ? "true" : "false");
-       auto composition = root->add_child("CompositionList")->add_child("Composition");
-       composition->add_child("Id")->add_child_text("urn:uuid:" + cpl->id());
-       composition->add_child("AnnotationText")->add_child_text(cpl->annotation_text().get_value_or(""));
-       composition->add_child("ContentTitleText")->add_child_text(cpl->content_title_text());
-       auto reel_list = composition->add_child("ReelList");
+       cxml::add_text_child(root, "InteropMode", cpl->standard() == dcp::Standard::INTEROP ? "true" : "false");
+       auto composition = cxml::add_child(cxml::add_child(root, "CompositionList"), "Composition");
+       cxml::add_text_child(composition, "Id", "urn:uuid:" + cpl->id());
+       cxml::add_text_child(composition, "AnnotationText", cpl->annotation_text().get_value_or(""));
+       cxml::add_text_child(composition, "ContentTitleText", cpl->content_title_text());
+       auto reel_list = cxml::add_child(composition, "ReelList");
        for (auto in_reel: cpl->reels()) {
-               auto out_reel = reel_list->add_child("Reel");
-               out_reel->add_child("Id")->add_child_text("urn:uuid:" + in_reel->id());
-               out_reel->add_child("AnnotationText");
+               auto out_reel = cxml::add_child(reel_list, "Reel");
+               cxml::add_text_child(out_reel, "Id", "urn:uuid:" + in_reel->id());
+               cxml::add_child(out_reel, "AnnotationText");
                if (in_reel->main_picture()) {
                        add_asset(film_key, in_reel->main_picture(), in_reel->main_picture()->asset(), out_reel, "MainPicture");
                }
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 88c9a5b700a64e279c5b459610c29f70955ec1d3..3515f34c287e749970809cbd3f750d2f8d8da40c 100644 (file)
@@ -195,17 +195,20 @@ DCPExaminer::DCPExaminer (shared_ptr<const DCPContent> content, bool tolerant)
                        }
                }
 
-               if (reel->main_subtitle()) {
-                       if (!reel->main_subtitle()->asset_ref().resolved()) {
-                               LOG_GENERAL("Main subtitle %1 of reel %2 is missing", reel->main_subtitle()->id(), reel->id());
+               if (auto sub = reel->main_subtitle()) {
+                       if (sub->entry_point().get_value_or(0) != 0) {
+                               _has_non_zero_entry_point[TextType::OPEN_SUBTITLE] = true;
+                       }
+                       if (!sub->asset_ref().resolved()) {
+                               LOG_GENERAL("Main subtitle %1 of reel %2 is missing", sub->id(), reel->id());
                                _needs_assets = true;
                        } else {
-                               LOG_GENERAL("Main subtitle %1 of reel %2 found", reel->main_subtitle()->id(), reel->id());
+                               LOG_GENERAL("Main subtitle %1 of reel %2 found", sub->id(), reel->id());
 
                                _text_count[TextType::OPEN_SUBTITLE] = 1;
-                               _open_subtitle_language = try_to_parse_language(reel->main_subtitle()->language());
+                               _open_subtitle_language = try_to_parse_language(sub->language());
 
-                               auto asset = reel->main_subtitle()->asset();
+                               auto asset = sub->asset();
                                for (auto const& font: asset->font_data()) {
                                        _fonts.push_back({reel_index, asset->id(), make_shared<dcpomatic::Font>(font.first, font.second)});
                                }
@@ -225,6 +228,9 @@ DCPExaminer::DCPExaminer (shared_ptr<const DCPContent> content, bool tolerant)
                }
 
                for (auto ccap: reel->closed_captions()) {
+                       if (ccap->entry_point().get_value_or(0) != 0) {
+                               _has_non_zero_entry_point[TextType::CLOSED_CAPTION] = true;
+                       }
                        if (!ccap->asset_ref().resolved()) {
                                LOG_GENERAL("Closed caption %1 of reel %2 is missing", ccap->id(), reel->id());
                                _needs_assets = true;
index 04fa31ea4281c611e16ed237616aff749d76e328..425552632ced7c86eca03829b82ec0557f9797db 100644 (file)
@@ -171,6 +171,10 @@ public:
                return _atmos_edit_rate;
        }
 
+       EnumIndexedVector<bool, TextType> has_non_zero_entry_point() const {
+               return _has_non_zero_entry_point;
+       }
+
        void add_fonts(std::shared_ptr<TextContent> content);
 
 private:
@@ -206,6 +210,7 @@ private:
        bool _has_atmos = false;
        Frame _atmos_length = 0;
        dcp::Fraction _atmos_edit_rate;
+       EnumIndexedVector<bool, TextType> _has_non_zero_entry_point;
 
        struct Font
        {
index 8de5967efb51421d64240fec484a43da786cbbc8..9ca3750d193122534dd63560a603fa5232c672be 100644 (file)
@@ -137,14 +137,14 @@ DCPSubtitleContent::technical_summary () const
 }
 
 void
-DCPSubtitleContent::as_xml (xmlpp::Node* node, bool with_paths) const
+DCPSubtitleContent::as_xml(xmlpp::Element* element, bool with_paths) const
 {
-       node->add_child("Type")->add_child_text ("DCPSubtitle");
-       Content::as_xml (node, with_paths);
+       cxml::add_text_child(element, "Type", "DCPSubtitle");
+       Content::as_xml(element, with_paths);
 
        if (only_text()) {
-               only_text()->as_xml (node);
+               only_text()->as_xml(element);
        }
 
-       node->add_child("Length")->add_child_text (raw_convert<string> (_length.get ()));
+       cxml::add_text_child(element, "Length", raw_convert<string>(_length.get()));
 }
index 89a6f26a2b28dd34e7df1cc58e42669b3db44687..068b6dbac0b943f4c72c1be9de44169de6297810 100644 (file)
@@ -30,7 +30,7 @@ public:
        void examine (std::shared_ptr<const Film> film, std::shared_ptr<Job>) override;
        std::string summary () const override;
        std::string technical_summary () const override;
-       void as_xml (xmlpp::Node *, bool with_paths) const override;
+       void as_xml(xmlpp::Element*, bool with_paths) const override;
        dcpomatic::DCPTime full_length (std::shared_ptr<const Film> film) const override;
        dcpomatic::DCPTime approximate_length () const override;
 
index 0bd75127541ac7be96abf6854426aaf46df06446..2929323963d76c4c4561292a657144cd85c43743 100644 (file)
@@ -56,9 +56,9 @@ DCPTextTrack::summary () const
 void
 DCPTextTrack::as_xml (xmlpp::Element* parent) const
 {
-       parent->add_child("Name")->add_child_text(name);
+       cxml::add_text_child(parent, "Name", name);
        if (language) {
-               parent->add_child("Language")->add_child_text(language->to_string());
+               cxml::add_text_child(parent, "Language", language->to_string());
        }
 }
 
index 217b72183c2af0b2d61b945d0b60ba3551c34933..6580ac448bc9d80c0c7e83983e1bd5e88fa5fc3d 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.
  */
@@ -215,7 +239,7 @@ DCPVideo::encode_remotely (EncodeServerDescription serv, int timeout) const
        /* Collect all XML metadata */
        xmlpp::Document doc;
        auto root = doc.create_root_node ("EncodingRequest");
-       root->add_child("Version")->add_child_text (raw_convert<string> (SERVER_LINK_VERSION));
+       cxml::add_text_child(root, "Version", raw_convert<string>(SERVER_LINK_VERSION));
        add_metadata (root);
 
        LOG_DEBUG_ENCODE (N_("Sending frame %1 to remote"), _index);
@@ -254,10 +278,10 @@ DCPVideo::encode_remotely (EncodeServerDescription serv, int timeout) const
 void
 DCPVideo::add_metadata (xmlpp::Element* el) const
 {
-       el->add_child("Index")->add_child_text (raw_convert<string> (_index));
-       el->add_child("FramesPerSecond")->add_child_text (raw_convert<string> (_frames_per_second));
-       el->add_child("J2KBandwidth")->add_child_text (raw_convert<string> (_j2k_bandwidth));
-       el->add_child("Resolution")->add_child_text (raw_convert<string> (int (_resolution)));
+       cxml::add_text_child(el, "Index", raw_convert<string>(_index));
+       cxml::add_text_child(el, "FramesPerSecond", raw_convert<string>(_frames_per_second));
+       cxml::add_text_child(el, "J2KBandwidth", raw_convert<string>(_j2k_bandwidth));
+       cxml::add_text_child(el, "Resolution", raw_convert<string>(int(_resolution)));
        _frame->add_metadata (el);
 }
 
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 ac797f8f4205589c5ad605dc768deb83bbfed54e..60fc5342a33437414fec6f1b76ad38086320324c 100644 (file)
@@ -27,6 +27,25 @@ using std::string;
 using namespace dcpomatic;
 
 
+bool
+dcpomatic::operator<=(HMSF const& a, HMSF const& b)
+{
+       if (a.h != b.h) {
+               return a.h <= b.h;
+       }
+
+       if (a.m != b.m) {
+               return a.m <= b.m;
+       }
+
+       if (a.s != b.s) {
+               return a.s <= b.s;
+       }
+
+       return a.f <= b.f;
+}
+
+
 template <>
 Time<ContentTimeDifferentiator, DCPTimeDifferentiator>::Time (DCPTime d, FrameRateChange f)
        : _t (llrint(d.get() * f.speed_up))
index 1b12ea901653ead8afd3f935679eaea8ecf6e57a..63bb865491f32cecc957c2181e550492fbdd8b29 100644 (file)
@@ -64,6 +64,9 @@ public:
 };
 
 
+bool operator<=(HMSF const& a, HMSF const& b);
+
+
 /** A time in seconds, expressed as a number scaled up by Time::HZ.  We want two different
  *  versions of this class, dcpomatic::ContentTime and dcpomatic::DCPTime, and we want it to be impossible to
  *  convert implicitly between the two.  Hence there's this template hack.  I'm not
@@ -152,6 +155,10 @@ public:
                return *this;
        }
 
+       Time<S, O> operator* (int o) const {
+               return Time<S, O> (_t * o);
+       }
+
        Time<S, O> operator/ (int o) const {
                return Time<S, O> (_t / o);
        }
index c73379bed9cded1f3963fad8a62bf32bf67b4095..4e56a37c4627423a9d8fecd0d1daeaca51b7c3a0 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");
 }
 
 
@@ -52,11 +48,8 @@ DKDMRecipient::as_xml (xmlpp::Element* node) const
        KDMRecipient::as_xml (node);
 
        for (auto i: emails) {
-               node->add_child("Email")->add_child_text(i);
+               cxml::add_text_child(node, "Email", i);
        }
-
-       node->add_child("UTCOffsetHour")->add_child_text(raw_convert<string>(utc_offset_hour));
-       node->add_child("UTCOffsetMinute")->add_child_text(raw_convert<string>(utc_offset_minute));
 }
 
 
@@ -65,29 +58,26 @@ kdm_for_dkdm_recipient (
        shared_ptr<const Film> film,
        boost::filesystem::path cpl,
        shared_ptr<DKDMRecipient> recipient,
-       boost::posix_time::ptime valid_from,
-       boost::posix_time::ptime valid_to
+       dcp::LocalTime valid_from,
+       dcp::LocalTime valid_to
        )
 {
        if (!recipient->recipient) {
                return {};
        }
 
-       dcp::LocalTime const begin(valid_from, dcp::UTCOffset(recipient->utc_offset_hour, recipient->utc_offset_minute));
-       dcp::LocalTime const end  (valid_to,   dcp::UTCOffset(recipient->utc_offset_hour, recipient->utc_offset_minute));
-
        auto signer = Config::instance()->signer_chain();
        if (!signer->valid()) {
                throw InvalidSignerError();
        }
 
-       auto const decrypted_kdm = film->make_kdm(cpl, begin, end);
+       auto const decrypted_kdm = film->make_kdm(cpl, valid_from, valid_to);
        auto const kdm = decrypted_kdm.encrypt(signer, recipient->recipient.get(), {}, dcp::Formulation::MODIFIED_TRANSITIONAL_1, true, 0);
 
        dcp::NameFormat::Map name_values;
        name_values['f'] = kdm.content_title_text();
-       name_values['b'] = begin.date() + " " + begin.time_of_day(true, false);
-       name_values['e'] = end.date() + " " + end.time_of_day(true, false);
+       name_values['b'] = valid_from.date() + " " + valid_from.time_of_day(true, false);
+       name_values['e'] = valid_to.date() + " " + valid_to.time_of_day(true, false);
        name_values['i'] = kdm.cpl_id();
 
        return make_shared<KDMWithMetadata>(name_values, nullptr, recipient->emails, kdm);
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..4c7838a8d0ceba65cb5768c017ef7944e6c1edac 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);
@@ -62,17 +66,17 @@ DKDM::name () const
 
 
 void
-DKDM::as_xml (xmlpp::Element* node) const
+DKDM::as_xml(xmlpp::Element* element) const
 {
-       node->add_child("DKDM")->add_child_text (_dkdm.as_xml ());
+       cxml::add_text_child(element, "DKDM", _dkdm.as_xml());
 }
 
 
 void
-DKDMGroup::as_xml (xmlpp::Element* node) const
+DKDMGroup::as_xml(xmlpp::Element* element) const
 {
-       auto f = node->add_child("DKDMGroup");
-       f->set_attribute ("Name", _name);
+       auto f = cxml::add_child(element, "DKDMGroup");
+       f->set_attribute("name", _name);
        for (auto i: _children) {
                i->as_xml (f);
        }
index 036ea58a5df92b94cb5cde24e6a7666be195f3a4..b9c907e050a6af4cb4221eb105db3717f2299e54 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 ();
 }
 
@@ -298,8 +301,8 @@ EncodeServer::broadcast_received ()
                /* Reply to the client saying what we can do */
                xmlpp::Document doc;
                auto root = doc.create_root_node ("ServerAvailable");
-               root->add_child("Threads")->add_child_text (raw_convert<string> (_worker_threads.size ()));
-               root->add_child("Version")->add_child_text (raw_convert<string> (SERVER_LINK_VERSION));
+               cxml::add_text_child(root, "Threads", raw_convert<string>(_worker_threads.size()));
+               cxml::add_text_child(root, "Version", raw_convert<string>(SERVER_LINK_VERSION));
                auto xml = doc.write_to_string ("UTF-8");
 
                if (_verbose) {
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 e030b98e26ceeed3c8d6c2fc6fd7c02e57048cb7..4dfbb902796565df2d49337d62eec8c2ca98fc00 100644 (file)
@@ -77,7 +77,7 @@ ExportConfig::read(cxml::ConstNodePtr node)
 
 
 void
-ExportConfig::write(xmlpp::Element* node) const
+ExportConfig::write(xmlpp::Element* element) const
 {
        string name;
 
@@ -97,11 +97,11 @@ ExportConfig::write(xmlpp::Element* node) const
                        break;
        }
 
-       node->add_child("Format")->add_child_text(name);
-       node->add_child("MixdownToStereo")->add_child_text(_mixdown_to_stereo ? "1" : "0");
-       node->add_child("SplitReels")->add_child_text(_split_reels ? "1" : "0");
-       node->add_child("SplitStreams")->add_child_text(_split_streams ? "1" : "0");
-       node->add_child("X264CRF")->add_child_text(dcp::raw_convert<string>(_x264_crf));
+       cxml::add_text_child(element, "Format", name);
+       cxml::add_text_child(element, "MixdownToStereo", _mixdown_to_stereo ? "1" : "0");
+       cxml::add_text_child(element, "SplitReels", _split_reels ? "1" : "0");
+       cxml::add_text_child(element, "SplitStreams", _split_streams ? "1" : "0");
+       cxml::add_text_child(element, "X264CRF", dcp::raw_convert<string>(_x264_crf));
 }
 
 
index 47dddb6c3783c8ce6a5e7542256d2b996df35b13..52d7b601d8ea2dca91ffcbd75e2f98c5fa0b2b25 100644 (file)
@@ -37,7 +37,7 @@ public:
 
        void set_defaults();
        void read(cxml::ConstNodePtr node);
-       void write(xmlpp::Element* node) const;
+       void write(xmlpp::Element* element) const;
 
        ExportFormat format() const {
                return _format;
index 9400eb60dddd6c46fc2d90853b5339e098ca40b5..24f96b889217edf6581bcbbcfd42778dc0e8b7fb 100644 (file)
@@ -52,19 +52,19 @@ FFmpegAudioStream::FFmpegAudioStream (cxml::ConstNodePtr node, int version)
 
 
 void
-FFmpegAudioStream::as_xml (xmlpp::Node* root) const
+FFmpegAudioStream::as_xml(xmlpp::Element* root) const
 {
        FFmpegStream::as_xml (root);
-       root->add_child("FrameRate")->add_child_text(raw_convert<string>(frame_rate()));
-       root->add_child("Length")->add_child_text(raw_convert<string>(length()));
-       mapping().as_xml (root->add_child("Mapping"));
+       cxml::add_text_child(root, "FrameRate", raw_convert<string>(frame_rate()));
+       cxml::add_text_child(root, "Length", raw_convert<string>(length()));
+       mapping().as_xml(cxml::add_child(root, "Mapping"));
        if (first_audio) {
-               root->add_child("FirstAudio")->add_child_text(raw_convert<string>(first_audio.get().get()));
+               cxml::add_text_child(root, "FirstAudio", raw_convert<string>(first_audio.get().get()));
        }
        if (codec_name) {
-               root->add_child("CodecName")->add_child_text(codec_name.get());
+               cxml::add_text_child(root, "CodecName", codec_name.get());
        }
        if (bit_depth()) {
-               root->add_child("BitDepth")->add_child_text(raw_convert<string>(bit_depth().get()));
+               cxml::add_text_child(root, "BitDepth", raw_convert<string>(bit_depth().get()));
        }
 }
index aae982f9e81ff6ecaff40f62b384dc893cf067dc..afd30e905a9445c766e6eef429393b593f56c815 100644 (file)
@@ -48,7 +48,7 @@ public:
 
        FFmpegAudioStream (cxml::ConstNodePtr, int);
 
-       void as_xml (xmlpp::Node *) const;
+       void as_xml(xmlpp::Element*) const;
 
        /* XXX: should probably be locked */
 
index 4a7c87b341e1046e0efbc4cabe965626d68ce01e..c80bdec69fef072cc2aa07f48e88a48aec73004b 100644 (file)
@@ -197,61 +197,61 @@ FFmpegContent::FFmpegContent (vector<shared_ptr<Content>> c)
 
 
 void
-FFmpegContent::as_xml (xmlpp::Node* node, bool with_paths) const
+FFmpegContent::as_xml(xmlpp::Element* element, bool with_paths) const
 {
-       node->add_child("Type")->add_child_text("FFmpeg");
-       Content::as_xml (node, with_paths);
+       cxml::add_text_child(element, "Type", "FFmpeg");
+       Content::as_xml(element, with_paths);
 
        if (video) {
-               video->as_xml (node);
+               video->as_xml(element);
        }
 
        if (audio) {
-               audio->as_xml (node);
+               audio->as_xml(element);
 
                for (auto i: audio->streams()) {
                        auto f = dynamic_pointer_cast<FFmpegAudioStream> (i);
                        DCPOMATIC_ASSERT (f);
-                       f->as_xml (node->add_child("AudioStream"));
+                       f->as_xml(cxml::add_child(element, "AudioStream"));
                }
        }
 
        if (only_text()) {
-               only_text()->as_xml (node);
+               only_text()->as_xml(element);
        }
 
        boost::mutex::scoped_lock lm (_mutex);
 
        for (auto i: _subtitle_streams) {
-               auto t = node->add_child("SubtitleStream");
+               auto t = cxml::add_child(element, "SubtitleStream");
                if (_subtitle_stream && i == _subtitle_stream) {
-                       t->add_child("Selected")->add_child_text("1");
+                       cxml::add_text_child(t, "Selected", "1");
                }
                i->as_xml (t);
        }
 
        for (auto i: _filters) {
-               node->add_child("Filter")->add_child_text(i.id());
+               cxml::add_text_child(element, "Filter", i.id());
        }
 
        if (_first_video) {
-               node->add_child("FirstVideo")->add_child_text(raw_convert<string>(_first_video.get().get()));
+               cxml::add_text_child(element, "FirstVideo", raw_convert<string>(_first_video.get().get()));
        }
 
        if (_color_range) {
-               node->add_child("ColorRange")->add_child_text(raw_convert<string>(static_cast<int>(*_color_range)));
+               cxml::add_text_child(element, "ColorRange", raw_convert<string>(static_cast<int>(*_color_range)));
        }
        if (_color_primaries) {
-               node->add_child("ColorPrimaries")->add_child_text(raw_convert<string>(static_cast<int>(*_color_primaries)));
+               cxml::add_text_child(element, "ColorPrimaries", raw_convert<string>(static_cast<int>(*_color_primaries)));
        }
        if (_color_trc) {
-               node->add_child("ColorTransferCharacteristic")->add_child_text(raw_convert<string>(static_cast<int>(*_color_trc)));
+               cxml::add_text_child(element, "ColorTransferCharacteristic", raw_convert<string>(static_cast<int>(*_color_trc)));
        }
        if (_colorspace) {
-               node->add_child("Colorspace")->add_child_text(raw_convert<string>(static_cast<int>(*_colorspace)));
+               cxml::add_text_child(element, "Colorspace", raw_convert<string>(static_cast<int>(*_colorspace)));
        }
        if (_bits_per_pixel) {
-               node->add_child("BitsPerPixel")->add_child_text(raw_convert<string>(*_bits_per_pixel));
+               cxml::add_text_child(element, "BitsPerPixel", raw_convert<string>(*_bits_per_pixel));
        }
 }
 
index a86358b7677ec46eb07f143585c1cfccf2788647..ce067f2d35b1f310595c51b11db4fc1baa1afdd9 100644 (file)
@@ -70,7 +70,7 @@ public:
        void take_settings_from (std::shared_ptr<const Content> c) override;
        std::string summary () const override;
        std::string technical_summary () const override;
-       void as_xml (xmlpp::Node *, bool with_paths) const override;
+       void as_xml(xmlpp::Element* element, bool with_paths) const override;
        dcpomatic::DCPTime full_length (std::shared_ptr<const Film> film) const override;
        dcpomatic::DCPTime approximate_length () const override;
 
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 51ade8e89cb844336baa4dfac51ef56be3f4fbea..d173c6bb660cd12415285721bad4f068e68a982f 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,8 +173,6 @@ FFmpegExaminer::FFmpegExaminer (shared_ptr<const FFmpegContent> c, shared_ptr<Jo
                /* This code taken from get_rotation() in ffmpeg:cmdutils.c */
                auto stream = _format_context->streams[*_video_stream];
                auto rotate_tag = av_dict_get (stream->metadata, "rotate", 0, 0);
-               uint8_t* displaymatrix = av_stream_get_side_data (stream, AV_PKT_DATA_DISPLAYMATRIX, 0);
-
                if (rotate_tag && *rotate_tag->value && strcmp(rotate_tag->value, "0")) {
                        char *tail;
                        _rotation = av_strtod (rotate_tag->value, &tail);
@@ -191,8 +181,9 @@ FFmpegExaminer::FFmpegExaminer (shared_ptr<const FFmpegContent> c, shared_ptr<Jo
                        }
                }
 
-               if (displaymatrix && !_rotation) {
-                       _rotation = - av_display_rotation_get ((int32_t*) displaymatrix);
+               auto side_data = av_packet_side_data_get(stream->codecpar->coded_side_data, stream->codecpar->nb_coded_side_data, AV_PKT_DATA_DISPLAYMATRIX);
+               if (side_data && !_rotation) {
+                       _rotation = - av_display_rotation_get(reinterpret_cast<int32_t*>(side_data->data));
                }
 
                if (_rotation) {
@@ -253,7 +244,7 @@ FFmpegExaminer::video_packet (AVCodecContext* context, string& temporal_referenc
                        ).get_value_or (ContentTime ()).frames_round (video_frame_rate().get ());
        }
        if (temporal_reference.size() < (PULLDOWN_CHECK_FRAMES * 2)) {
-               temporal_reference += (_video_frame->top_field_first ? "T" : "B");
+               temporal_reference += ((_video_frame->flags & AV_FRAME_FLAG_TOP_FIELD_FIRST) ? "T" : "B");
                temporal_reference += (_video_frame->repeat_pict ? "3" : "2");
        }
 
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 2fcd486dfafc72daa641f0a7c50e6c891c8c9d01..0ee41deabd1dd6af427a9a2c6c787fae35eec2b7 100644 (file)
@@ -234,9 +234,9 @@ FFmpegImageProxy::image (Image::Alignment alignment, optional<dcp::Size>) const
 
 
 void
-FFmpegImageProxy::add_metadata (xmlpp::Node* node) const
+FFmpegImageProxy::add_metadata(xmlpp::Element* element) const
 {
-       node->add_child("Type")->add_child_text (N_("FFmpeg"));
+       cxml::add_text_child(element, "Type", N_("FFmpeg"));
 }
 
 void
index b567fadee0825e2c6506746c6d44739c999132ea..1b8f44a7a87bd0b0cfdb4772c8121ce3727483d0 100644 (file)
@@ -35,7 +35,7 @@ public:
                boost::optional<dcp::Size> size = boost::optional<dcp::Size> ()
                ) const override;
 
-       void add_metadata (xmlpp::Node *) const override;
+       void add_metadata(xmlpp::Element*) const override;
        void write_to_socket (std::shared_ptr<Socket>) const override;
        bool same (std::shared_ptr<const ImageProxy> other) const override;
        size_t memory_used () const override;
index 47f71d1195bcbb2a169b5661c31171a8bd165c11..800c45eb6b45685d5258169074e77bace1a0eca6 100644 (file)
@@ -40,10 +40,10 @@ FFmpegStream::FFmpegStream (cxml::ConstNodePtr node)
 }
 
 void
-FFmpegStream::as_xml (xmlpp::Node* root) const
+FFmpegStream::as_xml(xmlpp::Element* root) const
 {
-       root->add_child("Name")->add_child_text (name);
-       root->add_child("Id")->add_child_text (raw_convert<string> (_id));
+       cxml::add_text_child(root, "Name", name);
+       cxml::add_text_child(root, "Id", raw_convert<string>(_id));
 }
 
 bool
index 84b2a8853317082b676f651bee47f11de3bc940b..221075eebec32a95c60a63de6b39302ffef87c6a 100644 (file)
@@ -37,7 +37,7 @@ public:
 
        explicit FFmpegStream (cxml::ConstNodePtr);
 
-       void as_xml (xmlpp::Node *) const;
+       void as_xml(xmlpp::Element*) const;
 
        /** @param c An AVFormatContext.
         *  @param index A stream index within the AVFormatContext.
index 1101901b7f1b20a83a192129b8361f5ce4d9cba5..3f43b6ec5773fe130873930d84c87f323c3e733e 100644 (file)
@@ -49,15 +49,15 @@ FFmpegSubtitleStream::FFmpegSubtitleStream (cxml::ConstNodePtr node, int version
 }
 
 void
-FFmpegSubtitleStream::as_xml (xmlpp::Node* root) const
+FFmpegSubtitleStream::as_xml(xmlpp::Element* root) const
 {
-       FFmpegStream::as_xml (root);
+       FFmpegStream::as_xml(root);
 
        boost::mutex::scoped_lock lm (_mutex);
        for (map<RGBA, RGBA>::const_iterator i = _colours.begin(); i != _colours.end(); ++i) {
-               xmlpp::Node* node = root->add_child("Colour");
-               i->first.as_xml (node->add_child("From"));
-               i->second.as_xml (node->add_child("To"));
+               auto node = cxml::add_child(root, "Colour");
+               i->first.as_xml(cxml::add_child(node, "From"));
+               i->second.as_xml(cxml::add_child(node, "To"));
        }
 }
 
index 8f56d1975e5e7eb5defa7b529af64af4909fef06..6251350cecee9045f1881077cd69f94529a4f8f9 100644 (file)
@@ -33,7 +33,7 @@ public:
 
        FFmpegSubtitleStream (cxml::ConstNodePtr node, int version);
 
-       void as_xml (xmlpp::Node *) const;
+       void as_xml(xmlpp::Element*) const;
 
        void set_colour (RGBA from, RGBA to);
        std::map<RGBA, RGBA> colours () const;
index d9ab6e2a3f8a2e4df368a6eece61ae2b37187b44..a88e8a855573d62a270ccf9a2eb35a428b9df0aa 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
@@ -379,88 +380,91 @@ Film::metadata (bool with_content_paths) const
        auto doc = make_shared<xmlpp::Document>();
        auto root = doc->create_root_node ("Metadata");
 
-       root->add_child("Version")->add_child_text (raw_convert<string> (current_state_version));
-       auto last_write = root->add_child("LastWrittenBy");
+       cxml::add_text_child(root, "Version", raw_convert<string>(current_state_version));
+       auto last_write = cxml::add_child(root, "LastWrittenBy");
        last_write->add_child_text (dcpomatic_version);
        last_write->set_attribute("git", dcpomatic_git_commit);
-       root->add_child("Name")->add_child_text (_name);
-       root->add_child("UseISDCFName")->add_child_text (_use_isdcf_name ? "1" : "0");
+       cxml::add_text_child(root, "Name", _name);
+       cxml::add_text_child(root, "UseISDCFName", _use_isdcf_name ? "1" : "0");
 
        if (_dcp_content_type) {
-               root->add_child("DCPContentType")->add_child_text (_dcp_content_type->isdcf_name ());
+               cxml::add_text_child(root, "DCPContentType", _dcp_content_type->isdcf_name());
        }
 
        if (_container) {
-               root->add_child("Container")->add_child_text (_container->id ());
-       }
-
-       root->add_child("Resolution")->add_child_text (resolution_to_string (_resolution));
-       root->add_child("J2KBandwidth")->add_child_text (raw_convert<string> (_j2k_bandwidth));
-       root->add_child("VideoFrameRate")->add_child_text (raw_convert<string> (_video_frame_rate));
-       root->add_child("AudioFrameRate")->add_child_text(raw_convert<string>(_audio_frame_rate));
-       root->add_child("ISDCFDate")->add_child_text (boost::gregorian::to_iso_string (_isdcf_date));
-       root->add_child("AudioChannels")->add_child_text (raw_convert<string> (_audio_channels));
-       root->add_child("ThreeD")->add_child_text (_three_d ? "1" : "0");
-       root->add_child("Sequence")->add_child_text (_sequence ? "1" : "0");
-       root->add_child("Interop")->add_child_text (_interop ? "1" : "0");
-       root->add_child("LimitToSMPTEBv20")->add_child_text(_limit_to_smpte_bv20 ? "1" : "0");
-       root->add_child("Encrypted")->add_child_text (_encrypted ? "1" : "0");
-       root->add_child("Key")->add_child_text (_key.hex ());
-       root->add_child("ContextID")->add_child_text (_context_id);
+               cxml::add_text_child(root, "Container", _container->id());
+       }
+
+       cxml::add_text_child(root, "Resolution", resolution_to_string(_resolution));
+       cxml::add_text_child(root, "J2KBandwidth", raw_convert<string>(_j2k_bandwidth));
+       cxml::add_text_child(root, "VideoFrameRate", raw_convert<string>(_video_frame_rate));
+       cxml::add_text_child(root, "AudioFrameRate", raw_convert<string>(_audio_frame_rate));
+       cxml::add_text_child(root, "ISDCFDate", boost::gregorian::to_iso_string(_isdcf_date));
+       cxml::add_text_child(root, "AudioChannels", raw_convert<string>(_audio_channels));
+       cxml::add_text_child(root, "ThreeD", _three_d ? "1" : "0");
+       cxml::add_text_child(root, "Sequence", _sequence ? "1" : "0");
+       cxml::add_text_child(root, "Interop", _interop ? "1" : "0");
+       cxml::add_text_child(root, "LimitToSMPTEBv20", _limit_to_smpte_bv20 ? "1" : "0");
+       cxml::add_text_child(root, "Encrypted", _encrypted ? "1" : "0");
+       cxml::add_text_child(root, "Key", _key.hex ());
+       cxml::add_text_child(root, "ContextID", _context_id);
        if (_audio_processor) {
-               root->add_child("AudioProcessor")->add_child_text (_audio_processor->id ());
+               cxml::add_text_child(root, "AudioProcessor", _audio_processor->id());
        }
-       root->add_child("ReelType")->add_child_text (raw_convert<string> (static_cast<int> (_reel_type)));
-       root->add_child("ReelLength")->add_child_text (raw_convert<string> (_reel_length));
-       root->add_child("ReencodeJ2K")->add_child_text (_reencode_j2k ? "1" : "0");
-       root->add_child("UserExplicitVideoFrameRate")->add_child_text(_user_explicit_video_frame_rate ? "1" : "0");
+       cxml::add_text_child(root, "ReelType", raw_convert<string>(static_cast<int> (_reel_type)));
+       cxml::add_text_child(root, "ReelLength", raw_convert<string>(_reel_length));
+       for (auto boundary: _custom_reel_boundaries) {
+               cxml::add_text_child(root, "CustomReelBoundary", raw_convert<string>(boundary.get()));
+       }
+       cxml::add_text_child(root, "ReencodeJ2K", _reencode_j2k ? "1" : "0");
+       cxml::add_text_child(root, "UserExplicitVideoFrameRate", _user_explicit_video_frame_rate ? "1" : "0");
        for (auto const& marker: _markers) {
-               auto m = root->add_child("Marker");
-               m->set_attribute("Type", dcp::marker_to_string(marker.first));
+               auto m = cxml::add_child(root, "Marker");
+               m->set_attribute("type", dcp::marker_to_string(marker.first));
                m->add_child_text(raw_convert<string>(marker.second.get()));
        }
        for (auto i: _ratings) {
-               i.as_xml (root->add_child("Rating"));
+               i.as_xml(cxml::add_child(root, "Rating"));
        }
        for (auto i: _content_versions) {
-               root->add_child("ContentVersion")->add_child_text(i);
+               cxml::add_text_child(root, "ContentVersion", i);
        }
-       root->add_child("NameLanguage")->add_child_text(_name_language.to_string());
-       root->add_child("TerritoryType")->add_child_text(territory_type_to_string(_territory_type));
+       cxml::add_text_child(root, "NameLanguage", _name_language.to_string());
+       cxml::add_text_child(root, "TerritoryType", territory_type_to_string(_territory_type));
        if (_release_territory) {
-               root->add_child("ReleaseTerritory")->add_child_text(_release_territory->subtag());
+               cxml::add_text_child(root, "ReleaseTerritory", _release_territory->subtag());
        }
        if (_sign_language_video_language) {
-               root->add_child("SignLanguageVideoLanguage")->add_child_text(_sign_language_video_language->to_string());
+               cxml::add_text_child(root, "SignLanguageVideoLanguage", _sign_language_video_language->to_string());
        }
-       root->add_child("VersionNumber")->add_child_text(raw_convert<string>(_version_number));
-       root->add_child("Status")->add_child_text(dcp::status_to_string(_status));
+       cxml::add_text_child(root, "VersionNumber", raw_convert<string>(_version_number));
+       cxml::add_text_child(root, "Status", dcp::status_to_string(_status));
        if (_chain) {
-               root->add_child("Chain")->add_child_text(*_chain);
+               cxml::add_text_child(root, "Chain", *_chain);
        }
        if (_distributor) {
-               root->add_child("Distributor")->add_child_text(*_distributor);
+               cxml::add_text_child(root, "Distributor", *_distributor);
        }
        if (_facility) {
-               root->add_child("Facility")->add_child_text(*_facility);
+               cxml::add_text_child(root, "Facility", *_facility);
        }
        if (_studio) {
-               root->add_child("Studio")->add_child_text(*_studio);
+               cxml::add_text_child(root, "Studio", *_studio);
        }
-       root->add_child("TempVersion")->add_child_text(_temp_version ? "1" : "0");
-       root->add_child("PreRelease")->add_child_text(_pre_release ? "1" : "0");
-       root->add_child("RedBand")->add_child_text(_red_band ? "1" : "0");
-       root->add_child("TwoDVersionOfThreeD")->add_child_text(_two_d_version_of_three_d ? "1" : "0");
+       cxml::add_text_child(root, "TempVersion", _temp_version ? "1" : "0");
+       cxml::add_text_child(root, "PreRelease", _pre_release ? "1" : "0");
+       cxml::add_text_child(root, "RedBand", _red_band ? "1" : "0");
+       cxml::add_text_child(root, "TwoDVersionOfThreeD", _two_d_version_of_three_d ? "1" : "0");
        if (_luminance) {
-               root->add_child("LuminanceValue")->add_child_text(raw_convert<string>(_luminance->value()));
-               root->add_child("LuminanceUnit")->add_child_text(dcp::Luminance::unit_to_string(_luminance->unit()));
+               cxml::add_text_child(root, "LuminanceValue", raw_convert<string>(_luminance->value()));
+               cxml::add_text_child(root, "LuminanceUnit", dcp::Luminance::unit_to_string(_luminance->unit()));
        }
-       root->add_child("UserExplicitContainer")->add_child_text(_user_explicit_container ? "1" : "0");
-       root->add_child("UserExplicitResolution")->add_child_text(_user_explicit_resolution ? "1" : "0");
+       cxml::add_text_child(root, "UserExplicitContainer", _user_explicit_container ? "1" : "0");
+       cxml::add_text_child(root, "UserExplicitResolution", _user_explicit_resolution ? "1" : "0");
        if (_audio_language) {
-               root->add_child("AudioLanguage")->add_child_text(_audio_language->to_string());
+               cxml::add_text_child(root, "AudioLanguage", _audio_language->to_string());
        }
-       _playlist->as_xml (root->add_child ("Playlist"), with_content_paths);
+       _playlist->as_xml(cxml::add_child(root, "Playlist"), with_content_paths);
 
        return doc;
 }
@@ -599,11 +603,18 @@ Film::read_metadata (optional<boost::filesystem::path> path)
 
        _reel_type = static_cast<ReelType> (f.optional_number_child<int>("ReelType").get_value_or (static_cast<int>(ReelType::SINGLE)));
        _reel_length = f.optional_number_child<int64_t>("ReelLength").get_value_or (2000000000);
+       for (auto boundary: f.node_children("CustomReelBoundary")) {
+               _custom_reel_boundaries.push_back(DCPTime(raw_convert<int64_t>(boundary->content())));
+       }
        _reencode_j2k = f.optional_bool_child("ReencodeJ2K").get_value_or(false);
        _user_explicit_video_frame_rate = f.optional_bool_child("UserExplicitVideoFrameRate").get_value_or(false);
 
        for (auto i: f.node_children("Marker")) {
-               _markers[dcp::marker_from_string(i->string_attribute("Type"))] = DCPTime(dcp::raw_convert<DCPTime::Type>(i->content()));
+               auto type = i->optional_string_attribute("Type");
+               if (!type) {
+                       type = i->string_attribute("type");
+               }
+               _markers[dcp::marker_from_string(*type)] = DCPTime(dcp::raw_convert<DCPTime::Type>(i->content()));
        }
 
        for (auto i: f.node_children("Rating")) {
@@ -1228,6 +1239,16 @@ Film::set_reel_length (int64_t r)
        _reel_length = r;
 }
 
+
+void
+Film::set_custom_reel_boundaries(vector<DCPTime> boundaries)
+{
+       FilmChangeSignaller ch(this, FilmProperty::CUSTOM_REEL_BOUNDARIES);
+       std::sort(boundaries.begin(), boundaries.end());
+       _custom_reel_boundaries = std::move(boundaries);
+}
+
+
 void
 Film::set_reencode_j2k (bool r)
 {
@@ -1595,6 +1616,23 @@ Film::check_settings_consistency ()
        if (change_made) {
                Message (_("DCP-o-matic had to change your settings for referring to DCPs as OV.  Please review those settings to make sure they are what you want."));
        }
+
+       if (reel_type() == ReelType::CUSTOM) {
+               auto boundaries = custom_reel_boundaries();
+               auto too_late = std::find_if(boundaries.begin(), boundaries.end(), [this](dcpomatic::DCPTime const& time) {
+                       return time >= length();
+               });
+
+               if (too_late != boundaries.end()) {
+                       if (std::distance(too_late, boundaries.end()) > 1) {
+                               Message(_("DCP-o-matic had to remove some of your custom reel boundaries as they no longer lie within the film."));
+                       } else {
+                               Message(_("DCP-o-matic had to remove one of your custom reel boundaries as it no longer lies within the film."));
+                       }
+                       boundaries.erase(too_late, boundaries.end());
+                       set_custom_reel_boundaries(boundaries);
+               }
+       }
 }
 
 void
@@ -1799,15 +1837,16 @@ Film::audio_analysis_finished ()
        /* XXX */
 }
 
-list<DCPTimePeriod>
+
+vector<DCPTimePeriod>
 Film::reels () const
 {
-       list<DCPTimePeriod> p;
+       vector<DCPTimePeriod> periods;
        auto const len = length();
 
        switch (reel_type ()) {
        case ReelType::SINGLE:
-               p.push_back (DCPTimePeriod (DCPTime (), len));
+               periods.emplace_back(DCPTime(), len);
                break;
        case ReelType::BY_VIDEO_CONTENT:
        {
@@ -1832,7 +1871,7 @@ Film::reels () const
                for (auto t: split_points) {
                        if (last && (t - *last) >= DCPTime::from_seconds(1)) {
                                /* Period from *last to t is long enough; use it and start a new one */
-                               p.push_back (DCPTimePeriod(*last, t));
+                               periods.emplace_back(*last, t);
                                last = t;
                        } else if (!last) {
                                /* That was the first time, so start a new period */
@@ -1840,8 +1879,8 @@ Film::reels () const
                        }
                }
 
-               if (!p.empty()) {
-                       p.back().to = split_points.back();
+               if (!periods.empty()) {
+                       periods.back().to = split_points.back();
                }
                break;
        }
@@ -1854,16 +1893,29 @@ Film::reels () const
                Frame const reel_in_frames = max(_reel_length / ((j2k_bandwidth() / video_frame_rate()) / 8), static_cast<Frame>(video_frame_rate()));
                while (current < len) {
                        DCPTime end = min (len, current + DCPTime::from_frames (reel_in_frames, video_frame_rate ()));
-                       p.push_back (DCPTimePeriod (current, end));
+                       periods.emplace_back(current, end);
                        current = end;
                }
                break;
        }
+       case ReelType::CUSTOM:
+       {
+               DCPTimePeriod current;
+               for (auto boundary: _custom_reel_boundaries) {
+                       current.to = boundary;
+                       periods.push_back(current);
+                       current.from = boundary;
+               }
+               current.to = len;
+               periods.push_back(current);
+               break;
+       }
        }
 
-       return p;
+       return periods;
 }
 
+
 /** @param period A period within the DCP
  *  @return Name of the content which most contributes to the given period.
  */
@@ -2230,3 +2282,53 @@ Film::set_territory_type(TerritoryType type)
        _territory_type = type;
 }
 
+
+void
+Film::set_ui_state(string key, string value)
+{
+       _ui_state[key] = value;
+       write_ui_state();
+}
+
+
+boost::optional<std::string>
+Film::ui_state(string key) const
+{
+       auto iter = _ui_state.find(key);
+       if (iter == _ui_state.end()) {
+               return {};
+       }
+
+       return iter->second;
+}
+
+
+void
+Film::write_ui_state() const
+{
+       auto doc = make_shared<xmlpp::Document>();
+       auto root = doc->create_root_node("UI");
+
+       for (auto state: _ui_state) {
+               cxml::add_text_child(root, state.first, state.second);
+       }
+
+       try {
+               doc->write_to_file_formatted(dcp::filesystem::fix_long_path(file(ui_state_file)).string());
+       } catch (...) {}
+}
+
+
+void
+Film::read_ui_state()
+{
+       try {
+               cxml::Document xml("UI");
+               xml.read_file(dcp::filesystem::fix_long_path(file(ui_state_file)));
+               for (auto node: xml.node_children()) {
+                       if (!node->is_text()) {
+                               _ui_state[node->name()] = node->content();
+                       }
+               }
+       } catch (...) {}
+}
index 43a41ad45d5a43fbeaf92accdea2595e9155e6db..0a0c5a4e1dcadd716af7197d3335eeb185ca0821 100644 (file)
@@ -186,7 +186,7 @@ public:
                return _playlist;
        }
 
-       std::list<dcpomatic::DCPTimePeriod> reels () const;
+       std::vector<dcpomatic::DCPTimePeriod> reels() const;
        std::list<int> mapped_audio_channels () const;
 
        boost::optional<dcp::LanguageTag> audio_language () const {
@@ -291,6 +291,10 @@ public:
                return _reel_length;
        }
 
+       std::vector<dcpomatic::DCPTime> custom_reel_boundaries() const {
+               return _custom_reel_boundaries;
+       }
+
        std::string context_id () const {
                return _context_id;
        }
@@ -408,6 +412,7 @@ public:
        void set_audio_processor (AudioProcessor const * processor);
        void set_reel_type (ReelType);
        void set_reel_length (int64_t);
+       void set_custom_reel_boundaries(std::vector<dcpomatic::DCPTime> boundaries);
        void set_reencode_j2k (bool);
        void set_marker (dcp::Marker type, dcpomatic::DCPTime time);
        void unset_marker (dcp::Marker type);
@@ -434,6 +439,10 @@ public:
 
        void add_ffoc_lfoc (Markers& markers) const;
 
+       void set_ui_state(std::string key, std::string value);
+       boost::optional<std::string> ui_state(std::string key) const;
+       void read_ui_state();
+
        /** Emitted when some property has of the Film is about to change or has changed */
        mutable boost::signals2::signal<void (ChangeType, FilmProperty)> Change;
 
@@ -477,6 +486,7 @@ private:
        void check_settings_consistency ();
        void maybe_set_container_and_resolution ();
        void set_dirty (bool dirty);
+       void write_ui_state() const;
 
        /** Log to write to */
        std::shared_ptr<Log> _log;
@@ -522,8 +532,10 @@ private:
        bool _limit_to_smpte_bv20;
        AudioProcessor const * _audio_processor;
        ReelType _reel_type;
-       /** Desired reel length in bytes, if _reel_type == REELTYPE_BY_LENGTH */
+       /** Desired reel length in bytes, if _reel_type == BY_LENGTH */
        int64_t _reel_length;
+       /** Reel boundaries (excluding those at the start and end, sorted in ascending order) if _reel_type == CUSTOM */
+       std::vector<dcpomatic::DCPTime> _custom_reel_boundaries;
        bool _reencode_j2k;
        /** true if the user has ever explicitly set the video frame rate of this film */
        bool _user_explicit_video_frame_rate;
@@ -562,6 +574,8 @@ private:
        */
        bool _tolerant;
 
+       std::map<std::string, std::string> _ui_state;
+
        mutable boost::mutex _info_file_mutex;
 
        boost::signals2::scoped_connection _playlist_change_connection;
index c232979657620e09a51a366415afb207d0b9d2e3..0841caa5ca19ea56ab1b3778e36c6cd18b687db3 100644 (file)
@@ -51,6 +51,7 @@ enum class FilmProperty {
        AUDIO_PROCESSOR,
        REEL_TYPE,
        REEL_LENGTH,
+       CUSTOM_REEL_BOUNDARIES,
        REENCODE_J2K,
        MARKERS,
        RATINGS,
index 9a14795eccf70c9ebe26427ff35c058da262c4d2..465b5991a41754bec81b4fbe21e1d9ce6353a8e6 100644 (file)
@@ -37,7 +37,8 @@ LIBDCP_ENABLE_WARNINGS
 #include "i18n.h"
 
 
-using namespace std;
+using std::string;
+using std::vector;
 using boost::optional;
 
 
index 955a2ad1ca2821b94ad43df82829d12ba2449b5d..246d3eeb8a67998af65ab4a24a11718ea867913f 100644 (file)
@@ -63,11 +63,11 @@ Font& Font::operator=(Font const& other)
 
 
 void
-Font::as_xml (xmlpp::Node* node)
+Font::as_xml(xmlpp::Element* element)
 {
-       node->add_child("Id")->add_child_text(_id);
+       cxml::add_text_child(element, "Id", _id);
        if (_content.file) {
-               node->add_child("File")->add_child_text(_content.file->string());
+               cxml::add_text_child(element, "File", _content.file->string());
        }
 }
 
index 12a14aba4e74086dee82c93d3a6464dd6159dee7..092b682bac1cd496d17f2d2ad5399166112eab2b 100644 (file)
@@ -57,7 +57,7 @@ public:
        Font(Font const& other);
        Font& operator=(Font const& other);
 
-       void as_xml (xmlpp::Node* node);
+       void as_xml(xmlpp::Element* element);
 
        std::string id () const {
                return _id;
diff --git a/src/lib/grok/context.h b/src/lib/grok/context.h
new file mode 100644 (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 1a92c944ed8047cdceb50f7f43b46e894923a323..1ac6e085bdc6cebbefdfad195b4fb5a4628b96b1 100644 (file)
@@ -99,13 +99,13 @@ ImageContent::technical_summary () const
 
 
 void
-ImageContent::as_xml (xmlpp::Node* node, bool with_paths) const
+ImageContent::as_xml(xmlpp::Element* element, bool with_paths) const
 {
-       node->add_child("Type")->add_child_text ("Image");
-       Content::as_xml (node, with_paths);
+       cxml::add_text_child(element, "Type", "Image");
+       Content::as_xml(element, with_paths);
 
        if (video) {
-               video->as_xml (node);
+               video->as_xml(element);
        }
 }
 
index d817eeee84e4cc0b6a2685666db207eae79a0025..140159bfaa77e5f0e890ce46954628459e099361 100644 (file)
@@ -40,7 +40,7 @@ public:
        void examine (std::shared_ptr<const Film> film, std::shared_ptr<Job>) override;
        std::string summary () const override;
        std::string technical_summary () const override;
-       void as_xml (xmlpp::Node *, bool with_paths) const override;
+       void as_xml(xmlpp::Element*, bool with_paths) const override;
        dcpomatic::DCPTime full_length (std::shared_ptr<const Film> film) const override;
        dcpomatic::DCPTime approximate_length () const override;
 
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 a37be580fcf062fbb247e1d02285e532d49279da..cec13b7732e162b8c57fb033f2f3a7b6eed20d55 100644 (file)
@@ -41,7 +41,7 @@ class Image;
 class Socket;
 
 namespace xmlpp {
-       class Node;
+       class Element;
 }
 
 namespace cxml {
@@ -101,7 +101,7 @@ public:
                boost::optional<dcp::Size> size = boost::optional<dcp::Size> ()
                ) const = 0;
 
-       virtual void add_metadata (xmlpp::Node *) const = 0;
+       virtual void add_metadata(xmlpp::Element *) const = 0;
        virtual void write_to_socket (std::shared_ptr<Socket>) const = 0;
        /** @return true if our image is definitely the same as another, false if it is probably not */
        virtual bool same (std::shared_ptr<const ImageProxy>) const = 0;
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
index 269b01bca7457c65dfe0377240bb4b7fbbdc7348..869e96967723d279142a5249de0f1e09b0534739 100644 (file)
@@ -192,15 +192,15 @@ J2KImageProxy::image (Image::Alignment alignment, optional<dcp::Size> target_siz
 
 
 void
-J2KImageProxy::add_metadata (xmlpp::Node* node) const
+J2KImageProxy::add_metadata(xmlpp::Element* element) const
 {
-       node->add_child("Type")->add_child_text(N_("J2K"));
-       node->add_child("Width")->add_child_text(raw_convert<string>(_size.width));
-       node->add_child("Height")->add_child_text(raw_convert<string>(_size.height));
+       cxml::add_text_child(element, "Type", N_("J2K"));
+       cxml::add_text_child(element, "Width", raw_convert<string>(_size.width));
+       cxml::add_text_child(element, "Height", raw_convert<string>(_size.height));
        if (_eye) {
-               node->add_child("Eye")->add_child_text(raw_convert<string>(static_cast<int>(_eye.get())));
+               cxml::add_text_child(element, "Eye", raw_convert<string>(static_cast<int>(_eye.get())));
        }
-       node->add_child("Size")->add_child_text(raw_convert<string>(_data->size()));
+       cxml::add_text_child(element, "Size", raw_convert<string>(_data->size()));
 }
 
 
index 9666ea406784f9981780661e60ba39c7dcfb2c31..3346fff08c4dc8d8cc82d5270468c52d0d17dd11 100644 (file)
@@ -61,7 +61,7 @@ public:
                boost::optional<dcp::Size> size = boost::optional<dcp::Size> ()
                ) const override;
 
-       void add_metadata (xmlpp::Node *) const override;
+       void add_metadata(xmlpp::Element*) const override;
        void write_to_socket (std::shared_ptr<Socket> override) const override;
        /** @return true if our image is definitely the same as another, false if it is probably not */
        bool same (std::shared_ptr<const ImageProxy>) const override;
diff --git a/src/lib/j2k_sync_encoder_thread.cc b/src/lib/j2k_sync_encoder_thread.cc
new file mode 100644 (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 2d3a021b5f5709368c2c46854ed561dbd24c8579..e4fabe1a6d4015cf6c5502546b714fbe333c1240 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 671e9797c24cbfb40e2b9bdf509538d2cf1dc391..c33eb1b9fa962fcd0e308af21d4770aad10c08dc 100644 (file)
@@ -39,14 +39,14 @@ KDMRecipient::KDMRecipient (cxml::ConstNodePtr node)
 void
 KDMRecipient::as_xml (xmlpp::Element* parent) const
 {
-       parent->add_child("Name")->add_child_text(name);
+       cxml::add_text_child(parent, "Name", name);
        if (recipient) {
-               parent->add_child("Recipient")->add_child_text(recipient->certificate(true));
+               cxml::add_text_child(parent, "Recipient", recipient->certificate(true));
        }
        if (recipient_file) {
-               parent->add_child("RecipientFile")->add_child_text(*recipient_file);
+               cxml::add_text_child(parent, "RecipientFile", *recipient_file);
        }
 
-       parent->add_child("Notes")->add_child_text(notes);
+       cxml::add_text_child(parent, "Notes", notes);
 }
 
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 7c1d285cffd3d0c1b55b0384f73cc582b10cd135..07690f3ac3537cd2e5b2528fef7c6278db4775d1 100644 (file)
@@ -34,8 +34,8 @@ PixelQuanta::PixelQuanta (cxml::ConstNodePtr node)
 void
 PixelQuanta::as_xml (xmlpp::Element* node) const
 {
-       node->add_child("X")->add_child_text(dcp::raw_convert<std::string>(x));
-       node->add_child("Y")->add_child_text(dcp::raw_convert<std::string>(y));
+       cxml::add_text_child(node, "X", dcp::raw_convert<std::string>(x));
+       cxml::add_text_child(node, "Y", dcp::raw_convert<std::string>(y));
 }
 
 
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..247301d582312f83656bce2d270a2bcc1319d9b1 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)
 {
 
@@ -199,29 +200,29 @@ PlayerVideo::make_image (function<AVPixelFormat (AVPixelFormat)> pixel_format, V
 
 
 void
-PlayerVideo::add_metadata (xmlpp::Node* node) const
+PlayerVideo::add_metadata(xmlpp::Element* element) const
 {
-       _crop.as_xml (node);
+       _crop.as_xml(element);
        if (_fade) {
-               node->add_child("Fade")->add_child_text (raw_convert<string> (_fade.get ()));
+               cxml::add_text_child(element, "Fade", raw_convert<string>(_fade.get()));
        }
-       _in->add_metadata (node->add_child ("In"));
-       node->add_child("InterWidth")->add_child_text (raw_convert<string> (_inter_size.width));
-       node->add_child("InterHeight")->add_child_text (raw_convert<string> (_inter_size.height));
-       node->add_child("OutWidth")->add_child_text (raw_convert<string> (_out_size.width));
-       node->add_child("OutHeight")->add_child_text (raw_convert<string> (_out_size.height));
-       node->add_child("Eyes")->add_child_text (raw_convert<string> (static_cast<int> (_eyes)));
-       node->add_child("Part")->add_child_text (raw_convert<string> (static_cast<int> (_part)));
-       node->add_child("VideoRange")->add_child_text(raw_convert<string>(static_cast<int>(_video_range)));
-       node->add_child("Error")->add_child_text(_error ? "1" : "0");
+       _in->add_metadata(cxml::add_child(element, "In"));
+       cxml::add_text_child(element, "InterWidth", raw_convert<string>(_inter_size.width));
+       cxml::add_text_child(element, "InterHeight", raw_convert<string>(_inter_size.height));
+       cxml::add_text_child(element, "OutWidth", raw_convert<string>(_out_size.width));
+       cxml::add_text_child(element, "OutHeight", raw_convert<string>(_out_size.height));
+       cxml::add_text_child(element, "Eyes", raw_convert<string>(static_cast<int>(_eyes)));
+       cxml::add_text_child(element, "Part", raw_convert<string>(static_cast<int>(_part)));
+       cxml::add_text_child(element, "VideoRange", raw_convert<string>(static_cast<int>(_video_range)));
+       cxml::add_text_child(element, "Error", _error ? "1" : "0");
        if (_colour_conversion) {
-               _colour_conversion.get().as_xml (node);
+               _colour_conversion.get().as_xml(element);
        }
        if (_text) {
-               node->add_child ("SubtitleWidth")->add_child_text (raw_convert<string> (_text->image->size().width));
-               node->add_child ("SubtitleHeight")->add_child_text (raw_convert<string> (_text->image->size().height));
-               node->add_child ("SubtitleX")->add_child_text (raw_convert<string> (_text->position.x));
-               node->add_child ("SubtitleY")->add_child_text (raw_convert<string> (_text->position.y));
+               cxml::add_text_child(element, "SubtitleWidth", raw_convert<string>(_text->image->size().width));
+               cxml::add_text_child(element, "SubtitleHeight", raw_convert<string>(_text->image->size().height));
+               cxml::add_text_child(element, "SubtitleX", raw_convert<string>(_text->position.x));
+               cxml::add_text_child(element, "SubtitleY", raw_convert<string>(_text->position.y));
        }
 }
 
@@ -343,7 +344,7 @@ PlayerVideo::shallow_copy () const
                _colour_conversion,
                _video_range,
                _content,
-               _video_frame,
+               _video_time,
                _error
                );
 }
@@ -356,12 +357,12 @@ bool
 PlayerVideo::reset_metadata (shared_ptr<const Film> film, dcp::Size player_video_container_size)
 {
        auto content = _content.lock();
-       if (!content || !_video_frame) {
+       if (!content || !_video_time) {
                return false;
        }
 
        _crop = content->video->actual_crop();
-       _fade = content->video->fade(film, _video_frame.get());
+       _fade = content->video->fade(film, _video_time.get());
        auto const size = content->video->scaled_size(film->frame_size());
        if (!size) {
                return false;
index f2781c1a0c51c3e099dd0009604e04a89ce2c3df..e2968749c48dc3ebf2e693e05550aee9b4a51192 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
                );
 
@@ -82,7 +82,7 @@ public:
        static AVPixelFormat force (AVPixelFormat);
        static AVPixelFormat keep_xyz_or_rgb (AVPixelFormat);
 
-       void add_metadata (xmlpp::Node* node) const;
+       void add_metadata(xmlpp::Element* element) const;
        void write_to_socket (std::shared_ptr<Socket> socket) const;
 
        bool reset_metadata (std::shared_ptr<const Film> film, dcp::Size player_video_container_size);
@@ -141,8 +141,8 @@ private:
        boost::optional<PositionImage> _text;
        /** Content that we came from.  This is so that reset_metadata() can work. */
        std::weak_ptr<Content> _content;
-       /** Video frame that we came from.  Again, this is for reset_metadata() */
-       boost::optional<Frame> _video_frame;
+       /** Video time that we came from.  Again, this is for reset_metadata() */
+       boost::optional<dcpomatic::ContentTime> _video_time;
 
        mutable boost::mutex _mutex;
        mutable std::shared_ptr<Image> _image;
index 85957e106803be886e4f5a85016f15cc21282282..7f83e94740bc4b6dc3b4837090aab81bb1e4f692 100644 (file)
@@ -282,10 +282,10 @@ Playlist::set_from_xml (shared_ptr<const Film> film, cxml::ConstNodePtr node, in
  *  @param with_content_paths true to include &lt;Path&gt; nodes in &lt;Content&gt; nodes, false to omit them.
  */
 void
-Playlist::as_xml (xmlpp::Node* node, bool with_content_paths)
+Playlist::as_xml(xmlpp::Element* element, bool with_content_paths)
 {
        for (auto i: content()) {
-               i->as_xml (node->add_child ("Content"), with_content_paths);
+               i->as_xml(cxml::add_child(element, "Content"), with_content_paths);
        }
 }
 
index e2662eb4581d78293f7d344c6e35619eaae9c400..7b9bd19fac914ed644e2251a29ccadf7f6e37539 100644 (file)
@@ -50,7 +50,7 @@ public:
        Playlist (Playlist const&) = delete;
        Playlist& operator= (Playlist const&) = delete;
 
-       void as_xml (xmlpp::Node *, bool with_content_paths);
+       void as_xml(xmlpp::Element*, bool with_content_paths);
        void set_from_xml (std::shared_ptr<const Film> film, cxml::ConstNodePtr node, int version, std::list<std::string>& notes);
 
        void add (std::shared_ptr<const Film> film, std::shared_ptr<Content>);
index c606ddd9906520b128c1009517d2c706dd180659..1ad78827c51d362ac237e489a9b38bc44b2f6ade 100644 (file)
@@ -73,12 +73,12 @@ RawImageProxy::image (Image::Alignment alignment, optional<dcp::Size>) const
 
 
 void
-RawImageProxy::add_metadata (xmlpp::Node* node) const
+RawImageProxy::add_metadata(xmlpp::Element* element) const
 {
-       node->add_child("Type")->add_child_text(N_("Raw"));
-       node->add_child("Width")->add_child_text(raw_convert<string>(_image->size().width));
-       node->add_child("Height")->add_child_text(raw_convert<string>(_image->size().height));
-       node->add_child("PixelFormat")->add_child_text(raw_convert<string>(static_cast<int>(_image->pixel_format())));
+       cxml::add_text_child(element, "Type", N_("Raw"));
+       cxml::add_text_child(element, "Width", raw_convert<string>(_image->size().width));
+       cxml::add_text_child(element, "Height", raw_convert<string>(_image->size().height));
+       cxml::add_text_child(element, "PixelFormat", raw_convert<string>(static_cast<int>(_image->pixel_format())));
 }
 
 
index d5ae2457d265ed156da08da1b9a269d10408e437..852f7c07677c9b66c5fa34df0bc98f805db285c9 100644 (file)
@@ -37,7 +37,7 @@ public:
                boost::optional<dcp::Size> size = boost::optional<dcp::Size> ()
                ) const override;
 
-       void add_metadata (xmlpp::Node *) const override;
+       void add_metadata(xmlpp::Element*) const override;
        void write_to_socket (std::shared_ptr<Socket>) const override;
        bool same (std::shared_ptr<const ImageProxy>) const override;
        size_t memory_used () const override;
diff --git a/src/lib/remote_j2k_encoder_thread.cc b/src/lib/remote_j2k_encoder_thread.cc
new file mode 100644 (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 870f3045d31340fb692c98cb65d8c858d6c4b38f..52830dcf39b6e721b7b4364cb349857d541c7334 100644 (file)
@@ -31,6 +31,7 @@
 #include <cairomm/cairomm.h>
 LIBDCP_DISABLE_WARNINGS
 #include <pangomm.h>
+#include <pangommconfig.h>
 LIBDCP_ENABLE_WARNINGS
 #include <pango/pangocairo.h>
 #include <boost/algorithm/string.hpp>
@@ -51,6 +52,15 @@ using boost::optional;
 using namespace dcpomatic;
 
 
+#if CAIROMM_MAJOR_VERSION == 1 && CAIROMM_MINOR_VERSION <= 14
+#define DCPOMATIC_OLD_CAIROMM_API
+#endif
+
+#if PANGOMM_MAJOR_VERSION == 2 && PANGOMM_MINOR_VERSION <= 46
+#define DCPOMATIC_OLD_PANGOMM_API
+#endif
+
+
 /** Create a Pango layout using a dummy context which we can use to calculate the size
  *  of the text we will render.  Then we can transfer the layout over to the real context
  *  for the actual render.
@@ -66,7 +76,11 @@ create_layout(string font_name, string markup)
        auto context = Glib::wrap (c_context);
        auto layout = Pango::Layout::create(context);
 
-       layout->set_alignment (Pango::ALIGN_LEFT);
+#ifdef DCPOMATIC_OLD_PANGOMM_API
+       layout->set_alignment(Pango::ALIGN_LEFT);
+#else
+       layout->set_alignment(Pango::Alignment::LEFT);
+#endif
        Pango::FontDescription font (font_name);
        layout->set_font_description (font);
        layout->set_markup (markup);
@@ -163,11 +177,22 @@ create_surface (shared_ptr<Image> image)
        DCPOMATIC_ASSERT (image->pixel_format() == AV_PIX_FMT_BGRA);
        return Cairo::ImageSurface::create (
                image->data()[0],
+#ifdef DCPOMATIC_OLD_CAIROMM_API
                Cairo::FORMAT_ARGB32,
+#else
+               Cairo::ImageSurface::Format::ARGB32,
+#endif
                image->size().width,
                image->size().height,
                /* Cairo ARGB32 means first byte blue, second byte green, third byte red, fourth byte alpha */
-               Cairo::ImageSurface::format_stride_for_width (Cairo::FORMAT_ARGB32, image->size().width)
+               Cairo::ImageSurface::format_stride_for_width(
+#ifdef DCPOMATIC_OLD_CAIROMM_API
+                       Cairo::FORMAT_ARGB32,
+#else
+                       Cairo::ImageSurface::Format::ARGB32,
+#endif
+                       image->size().width
+                       )
                );
 }
 
@@ -394,7 +419,11 @@ render_line(vector<StringText> subtitles, dcp::Size target, DCPTime time, int fr
                /* Border effect */
                set_source_rgba (context, first.effect_colour(), fade_factor);
                context->set_line_width (border_width);
+#ifdef DCPOMATIC_OLD_CAIROMM_API
                context->set_line_join (Cairo::LINE_JOIN_ROUND);
+#else
+               context->set_line_join (Cairo::Context::LineJoin::ROUND);
+#endif
                context->move_to (x_offset, y_offset);
                layout.pango->add_to_cairo_context (context);
                context->stroke ();
index 2e6a4fab632aa6e64c251fba52ac51f899d44dc0..2e0408d8ab5ddd730e32c1ad02b1a73ed0ce2558 100644 (file)
@@ -41,12 +41,12 @@ RGBA::RGBA (cxml::ConstNodePtr node)
 
 
 void
-RGBA::as_xml (xmlpp::Node* parent) const
+RGBA::as_xml(xmlpp::Element* parent) const
 {
-       parent->add_child("R")->add_child_text(lexical_cast<string>(int(r)));
-       parent->add_child("G")->add_child_text(lexical_cast<string>(int(g)));
-       parent->add_child("B")->add_child_text(lexical_cast<string>(int(b)));
-       parent->add_child("A")->add_child_text(lexical_cast<string>(int(a)));
+       cxml::add_text_child(parent, "R", lexical_cast<string>(int(r)));
+       cxml::add_text_child(parent, "G", lexical_cast<string>(int(g)));
+       cxml::add_text_child(parent, "B", lexical_cast<string>(int(b)));
+       cxml::add_text_child(parent, "A", lexical_cast<string>(int(a)));
 }
 
 
index 96fed710ef4582e3cc5676bc77d0aa0f5429e147..a582a4ca103e9f2fe4c0707dfea621bb0ea77e3c 100644 (file)
@@ -44,7 +44,7 @@ public:
 
        explicit RGBA (cxml::ConstNodePtr node);
 
-       void as_xml (xmlpp::Node* parent) const;
+       void as_xml(xmlpp::Element* parent) const;
 
        uint8_t r = 0;
        uint8_t g = 0;
index febf9085c3d344af6a77487c5aed1c04a60a7148..38c474850d64c2efe10d7b5322a95728a7e66ba4 100644 (file)
@@ -57,7 +57,7 @@ Screen::as_xml (xmlpp::Element* parent) const
 {
        KDMRecipient::as_xml (parent);
        for (auto i: trusted_devices) {
-               parent->add_child("TrustedDevice")->add_child_text(i.as_string());
+               cxml::add_text_child(parent, "TrustedDevice", i.as_string());
        }
 }
 
@@ -77,8 +77,8 @@ KDMWithMetadataPtr
 kdm_for_screen (
        std::function<dcp::DecryptedKDM (dcp::LocalTime, dcp::LocalTime)> make_kdm,
        shared_ptr<const dcpomatic::Screen> screen,
-       boost::posix_time::ptime valid_from,
-       boost::posix_time::ptime valid_to,
+       dcp::LocalTime valid_from,
+       dcp::LocalTime valid_to,
        dcp::Formulation formulation,
        bool disable_forensic_marking_picture,
        optional<int> disable_forensic_marking_audio,
@@ -90,17 +90,15 @@ kdm_for_screen (
        }
 
        auto cinema = screen->cinema;
-       dcp::LocalTime const begin(valid_from, dcp::UTCOffset(cinema ? cinema->utc_offset_hour() : 0, cinema ? cinema->utc_offset_minute() : 0));
-       dcp::LocalTime const end  (valid_to,   dcp::UTCOffset(cinema ? cinema->utc_offset_hour() : 0, cinema ? cinema->utc_offset_minute() : 0));
 
-       period_checks.push_back(check_kdm_and_certificate_validity_periods(cinema ? cinema->name : "", screen->name, screen->recipient.get(), begin, end));
+       period_checks.push_back(check_kdm_and_certificate_validity_periods(cinema ? cinema->name : "", screen->name, screen->recipient.get(), valid_from, valid_to));
 
        auto signer = Config::instance()->signer_chain();
        if (!signer->valid()) {
                throw InvalidSignerError();
        }
 
-       auto kdm = make_kdm(begin, end).encrypt(
+       auto kdm = make_kdm(valid_from, valid_to).encrypt(
                signer, screen->recipient.get(), screen->trusted_device_thumbprints(), formulation, disable_forensic_marking_picture, disable_forensic_marking_audio
                );
 
@@ -112,8 +110,8 @@ kdm_for_screen (
        }
        name_values['s'] = screen->name;
        name_values['f'] = kdm.content_title_text();
-       name_values['b'] = begin.date() + " " + begin.time_of_day(true, false);
-       name_values['e'] = end.date() + " " + end.time_of_day(true, false);
+       name_values['b'] = valid_from.date() + " " + valid_from.time_of_day(true, false);
+       name_values['e'] = valid_to.date() + " " + valid_to.time_of_day(true, false);
        name_values['i'] = kdm.cpl_id();
 
        return make_shared<KDMWithMetadata>(name_values, cinema.get(), cinema ? cinema->emails : vector<string>(), kdm);
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 f49f11a7d26ac15cb8b1429b6bd64b36ee3618ac..e87465cdc5dd6909bf71ef05fbce63fe86ce6e06 100644 (file)
@@ -62,10 +62,10 @@ SPL::write (boost::filesystem::path path) const
 {
        xmlpp::Document doc;
        auto root = doc.create_root_node ("SPL");
-       root->add_child("Id")->add_child_text (_id);
-       root->add_child("Name")->add_child_text (_name);
+       cxml::add_text_child(root, "Id", _id);
+       cxml::add_text_child(root, "Name", _name);
        for (auto i: _spl) {
-               i.as_xml (root->add_child("Entry"));
+               i.as_xml(cxml::add_child(root, "Entry"));
        }
        doc.write_to_file_formatted (path.string());
 }
index f0b377a56bfe5b6d0b983f22aa293a4b39702f2a..a77b0b7d5a4e350ed2253da79b5686aef25b67f7 100644 (file)
@@ -53,5 +53,5 @@ SPLEntry::SPLEntry (shared_ptr<Content> c)
 void
 SPLEntry::as_xml (xmlpp::Element* e)
 {
-       e->add_child("Digest")->add_child_text(digest);
+       cxml::add_text_child(e, "Digest", digest);
 }
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 d8c195be77af1fe53ada9e66830d776ad4810826..c6639936c9bbd70cae7867221ba51f9485ab150c 100644 (file)
@@ -122,16 +122,16 @@ StringTextFileContent::technical_summary () const
 
 
 void
-StringTextFileContent::as_xml (xmlpp::Node* node, bool with_paths) const
+StringTextFileContent::as_xml(xmlpp::Element* element, bool with_paths) const
 {
-       node->add_child("Type")->add_child_text("TextSubtitle");
-       Content::as_xml (node, with_paths);
+       cxml::add_text_child(element, "Type", "TextSubtitle");
+       Content::as_xml(element, with_paths);
 
        if (only_text()) {
-               only_text()->as_xml(node);
+               only_text()->as_xml(element);
        }
 
-       node->add_child("Length")->add_child_text(raw_convert<string>(_length.get ()));
+       cxml::add_text_child(element, "Length", raw_convert<string>(_length.get()));
 }
 
 
index 30f543381bf68fd0390b025fbac30a888210b881..8a5d9946a6cfbebae29860fb80714a1db91ad924 100644 (file)
@@ -45,7 +45,7 @@ public:
        void examine (std::shared_ptr<const Film> film, std::shared_ptr<Job>) override;
        std::string summary () const override;
        std::string technical_summary () const override;
-       void as_xml (xmlpp::Node *, bool with_paths) const override;
+       void as_xml(xmlpp::Element*, bool with_paths) const override;
        dcpomatic::DCPTime full_length (std::shared_ptr<const Film> film) const override;
        dcpomatic::DCPTime approximate_length () const override;
        std::string identifier () const override;
index ff1969a3a37c227cd32fde4a2dc793760648908a..2968416c671c4e62b8797efe4480361f98e38604 100644 (file)
@@ -70,18 +70,18 @@ SubtitleAnalysis::write (boost::filesystem::path path) const
        auto doc = make_shared<xmlpp::Document>();
        xmlpp::Element* root = doc->create_root_node ("SubtitleAnalysis");
 
-       root->add_child("Version")->add_child_text (raw_convert<string>(_current_state_version));
+       cxml::add_text_child(root, "Version", raw_convert<string>(_current_state_version));
 
        if (_bounding_box) {
-               auto bounding_box = root->add_child("BoundingBox");
-               bounding_box->add_child("X")->add_child_text(raw_convert<string>(_bounding_box->x));
-               bounding_box->add_child("Y")->add_child_text(raw_convert<string>(_bounding_box->y));
-               bounding_box->add_child("Width")->add_child_text(raw_convert<string>(_bounding_box->width));
-               bounding_box->add_child("Height")->add_child_text(raw_convert<string>(_bounding_box->height));
+               auto bounding_box = cxml::add_child(root, "BoundingBox");
+               cxml::add_text_child(bounding_box, "X", raw_convert<string>(_bounding_box->x));
+               cxml::add_text_child(bounding_box, "Y", raw_convert<string>(_bounding_box->y));
+               cxml::add_text_child(bounding_box, "Width", raw_convert<string>(_bounding_box->width));
+               cxml::add_text_child(bounding_box, "Height", raw_convert<string>(_bounding_box->height));
        }
 
-       root->add_child("AnalysisXOffset")->add_child_text(raw_convert<string>(_analysis_x_offset));
-       root->add_child("AnalysisYOffset")->add_child_text(raw_convert<string>(_analysis_y_offset));
+       cxml::add_text_child(root, "AnalysisXOffset", raw_convert<string>(_analysis_x_offset));
+       cxml::add_text_child(root, "AnalysisYOffset", raw_convert<string>(_analysis_y_offset));
 
        doc->write_to_file_formatted (path.string());
 }
index 92a35b8220cbca3f41c7302d9d8f0536a0b22ab9..cfdaabcbc0bdf24e3bf133052d842563c4837e40 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.
@@ -353,63 +356,63 @@ TextContent::TextContent (Content* parent, vector<shared_ptr<Content>> c)
 
 /** _mutex must not be held on entry */
 void
-TextContent::as_xml (xmlpp::Node* root) const
+TextContent::as_xml(xmlpp::Element* root) const
 {
        boost::mutex::scoped_lock lm (_mutex);
 
-       auto text = root->add_child ("Text");
+       auto text = cxml::add_child(root, "Text");
 
-       text->add_child("Use")->add_child_text (_use ? "1" : "0");
-       text->add_child("Burn")->add_child_text (_burn ? "1" : "0");
-       text->add_child("XOffset")->add_child_text (raw_convert<string> (_x_offset));
-       text->add_child("YOffset")->add_child_text (raw_convert<string> (_y_offset));
-       text->add_child("XScale")->add_child_text (raw_convert<string> (_x_scale));
-       text->add_child("YScale")->add_child_text (raw_convert<string> (_y_scale));
+       cxml::add_text_child(text, "Use", _use ? "1" : "0");
+       cxml::add_text_child(text, "Burn", _burn ? "1" : "0");
+       cxml::add_text_child(text, "XOffset", raw_convert<string>(_x_offset));
+       cxml::add_text_child(text, "YOffset", raw_convert<string>(_y_offset));
+       cxml::add_text_child(text, "XScale", raw_convert<string>(_x_scale));
+       cxml::add_text_child(text, "YScale", raw_convert<string>(_y_scale));
        if (_colour) {
-               text->add_child("Red")->add_child_text (raw_convert<string> (_colour->r));
-               text->add_child("Green")->add_child_text (raw_convert<string> (_colour->g));
-               text->add_child("Blue")->add_child_text (raw_convert<string> (_colour->b));
+               cxml::add_text_child(text, "Red", raw_convert<string>(_colour->r));
+               cxml::add_text_child(text, "Green", raw_convert<string>(_colour->g));
+               cxml::add_text_child(text, "Blue", raw_convert<string>(_colour->b));
        }
        if (_effect) {
                switch (*_effect) {
                case dcp::Effect::NONE:
-                       text->add_child("Effect")->add_child_text("none");
+                       cxml::add_text_child(text, "Effect", "none");
                        break;
                case dcp::Effect::BORDER:
-                       text->add_child("Effect")->add_child_text("outline");
+                       cxml::add_text_child(text, "Effect", "outline");
                        break;
                case dcp::Effect::SHADOW:
-                       text->add_child("Effect")->add_child_text("shadow");
+                       cxml::add_text_child(text, "Effect", "shadow");
                        break;
                }
        }
        if (_effect_colour) {
-               text->add_child("EffectRed")->add_child_text (raw_convert<string> (_effect_colour->r));
-               text->add_child("EffectGreen")->add_child_text (raw_convert<string> (_effect_colour->g));
-               text->add_child("EffectBlue")->add_child_text (raw_convert<string> (_effect_colour->b));
+               cxml::add_text_child(text, "EffectRed", raw_convert<string>(_effect_colour->r));
+               cxml::add_text_child(text, "EffectGreen", raw_convert<string>(_effect_colour->g));
+               cxml::add_text_child(text, "EffectBlue", raw_convert<string>(_effect_colour->b));
        }
-       text->add_child("LineSpacing")->add_child_text (raw_convert<string> (_line_spacing));
+       cxml::add_text_child(text, "LineSpacing", raw_convert<string>(_line_spacing));
        if (_fade_in) {
-               text->add_child("FadeIn")->add_child_text (raw_convert<string> (_fade_in->get()));
+               cxml::add_text_child(text, "FadeIn", raw_convert<string>(_fade_in->get()));
        }
        if (_fade_out) {
-               text->add_child("FadeOut")->add_child_text (raw_convert<string> (_fade_out->get()));
+               cxml::add_text_child(text, "FadeOut", raw_convert<string>(_fade_out->get()));
        }
-       text->add_child("OutlineWidth")->add_child_text (raw_convert<string> (_outline_width));
+       cxml::add_text_child(text, "OutlineWidth", raw_convert<string>(_outline_width));
 
        for (auto i: _fonts) {
-               i->as_xml (text->add_child("Font"));
+               i->as_xml(cxml::add_child(text, "Font"));
        }
 
-       text->add_child("Type")->add_child_text (text_type_to_string(_type));
-       text->add_child("OriginalType")->add_child_text (text_type_to_string(_original_type));
+       cxml::add_text_child(text, "Type", text_type_to_string(_type));
+       cxml::add_text_child(text, "OriginalType", text_type_to_string(_original_type));
        if (_dcp_track) {
-               _dcp_track->as_xml(text->add_child("DCPTrack"));
+               _dcp_track->as_xml(cxml::add_child(text, "DCPTrack"));
        }
        if (_language) {
-               auto lang = text->add_child("Language");
+               auto lang = cxml::add_child(text, "Language");
                lang->add_child_text (_language->to_string());
-               lang->set_attribute ("Additional", _language_is_additional ? "1" : "0");
+               lang->set_attribute("additional", _language_is_additional ? "1" : "0");
        }
 }
 
index 4d4bdc507e478043ca2111c2fa0507d812aaec8d..3122271209deb67e33aea54d6b9e1d3c77e14ce7 100644 (file)
@@ -72,7 +72,7 @@ public:
        TextContent (Content* parent, std::vector<std::shared_ptr<Content>>);
        TextContent (Content* parent, cxml::ConstNodePtr, int version, std::list<std::string>& notes);
 
-       void as_xml (xmlpp::Node *) const;
+       void as_xml(xmlpp::Element*) const;
        std::string identifier () const;
        void take_settings_from (std::shared_ptr<const TextContent> c);
 
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 36059401ec8eb96d73a6921d4715443ab028d9fc..c9c87bae5200b48bdf9bfdaf48d99216400bc110 100644 (file)
@@ -107,7 +107,8 @@ enum class ReelType
 {
        SINGLE,
        BY_VIDEO_CONTENT,
-       BY_LENGTH
+       BY_LENGTH,
+       CUSTOM
 };
 
 
index fe6602de380cf60c2b3f0cf9fc197471eeef011b..ef15b90e5b1c4fea79837a814ccce69df84d2017 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)
@@ -1124,6 +1120,33 @@ word_wrap(string input, int columns)
 }
 
 
+#ifdef DCPOMATIC_GROK
+void
+setup_grok_library_path()
+{
+       static std::string old_path;
+       if (old_path.empty()) {
+               auto const old = getenv("LD_LIRARY_PATH");
+               if (old) {
+                       old_path = old;
+               }
+       }
+       auto const grok = Config::instance()->grok();
+       if (!grok || grok->binary_location.empty()) {
+               setenv("LD_LIRARY_PATH", old_path.c_str(), 1);
+               return;
+       }
+
+       std::string new_path = old_path;
+       if (!new_path.empty()) {
+               new_path += ":";
+       }
+       new_path += grok->binary_location.string();
+
+       setenv("LD_LIBRARY_PATH", new_path.c_str(), 1);
+}
+#endif
+
 string
 screen_names_to_string(vector<string> names)
 {
index fd6fd61649ac4a896f9f04d40571e3686d429913..4f64369d33ac6d5cdb34e73b60be7a6e1f09b069 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,6 +98,22 @@ extern std::string error_details(boost::system::error_code ec);
 extern bool contains_assetmap(boost::filesystem::path dir);
 extern std::string word_wrap(std::string input, int columns);
 extern void capture_ffmpeg_logs();
+#ifdef DCPOMATIC_GROK
+extern void setup_grok_library_path();
+#endif
+
+
+template <class T>
+T
+number_attribute(cxml::ConstNodePtr node, std::string name1, std::string name2)
+{
+       auto value = node->optional_number_attribute<T>(name1);
+       if (!value) {
+               value = node->number_attribute<T>(name2);
+       }
+       return *value;
+}
+
 extern std::string screen_names_to_string(std::vector<std::string> names);
 
 #endif
index 6c027ff114ceaae500adbb3d2c340f94753a2d37..10dd5ff1e27e1a5782d44f7540d5395e18795034 100644 (file)
@@ -273,37 +273,37 @@ VideoContent::VideoContent (Content* parent, vector<shared_ptr<Content> > c)
 
 
 void
-VideoContent::as_xml (xmlpp::Node* node) const
+VideoContent::as_xml(xmlpp::Element* element) const
 {
        boost::mutex::scoped_lock lm (_mutex);
-       node->add_child("Use")->add_child_text (_use ? "1" : "0");
-       node->add_child("VideoLength")->add_child_text (raw_convert<string> (_length));
+       cxml::add_text_child(element, "Use", _use ? "1" : "0");
+       cxml::add_text_child(element, "VideoLength", raw_convert<string>(_length));
        if (_size) {
-               node->add_child("VideoWidth")->add_child_text(raw_convert<string>(_size->width));
-               node->add_child("VideoHeight")->add_child_text(raw_convert<string>(_size->height));
+               cxml::add_text_child(element, "VideoWidth", raw_convert<string>(_size->width));
+               cxml::add_text_child(element, "VideoHeight", raw_convert<string>(_size->height));
        }
-       node->add_child("VideoFrameType")->add_child_text (video_frame_type_to_string (_frame_type));
+       cxml::add_text_child(element, "VideoFrameType", video_frame_type_to_string(_frame_type));
        if (_sample_aspect_ratio) {
-               node->add_child("SampleAspectRatio")->add_child_text (raw_convert<string> (_sample_aspect_ratio.get ()));
+               cxml::add_text_child(element, "SampleAspectRatio", raw_convert<string> (_sample_aspect_ratio.get ()));
        }
-       _crop.as_xml (node);
+       _crop.as_xml(element);
        if (_custom_ratio) {
-               node->add_child("CustomRatio")->add_child_text(raw_convert<string>(*_custom_ratio));
+               cxml::add_text_child(element, "CustomRatio", raw_convert<string>(*_custom_ratio));
        }
        if (_custom_size) {
-               node->add_child("CustomWidth")->add_child_text(raw_convert<string>(_custom_size->width));
-               node->add_child("CustomHeight")->add_child_text(raw_convert<string>(_custom_size->height));
+               cxml::add_text_child(element, "CustomWidth", raw_convert<string>(_custom_size->width));
+               cxml::add_text_child(element, "CustomHeight", raw_convert<string>(_custom_size->height));
        }
        if (_colour_conversion) {
-               _colour_conversion.get().as_xml (node->add_child("ColourConversion"));
+               _colour_conversion.get().as_xml(cxml::add_child(element, "ColourConversion"));
        }
-       node->add_child("YUV")->add_child_text (_yuv ? "1" : "0");
-       node->add_child("FadeIn")->add_child_text (raw_convert<string> (_fade_in));
-       node->add_child("FadeOut")->add_child_text (raw_convert<string> (_fade_out));
-       node->add_child("Range")->add_child_text(_range == VideoRange::FULL ? "full" : "video");
-       _pixel_quanta.as_xml(node->add_child("PixelQuanta"));
+       cxml::add_text_child(element, "YUV", _yuv ? "1" : "0");
+       cxml::add_text_child(element, "FadeIn", raw_convert<string>(_fade_in));
+       cxml::add_text_child(element, "FadeOut", raw_convert<string>(_fade_out));
+       cxml::add_text_child(element, "Range", _range == VideoRange::FULL ? "full" : "video");
+       _pixel_quanta.as_xml(cxml::add_child(element, "PixelQuanta"));
        if (_burnt_subtitle_language) {
-               node->add_child("BurntSubtitleLanguage")->add_child_text(_burnt_subtitle_language->to_string());
+               cxml::add_text_child(element, "BurntSubtitleLanguage", _burnt_subtitle_language->to_string());
        }
 }
 
@@ -424,27 +424,29 @@ VideoContent::size_after_crop () const
 }
 
 
-/** @param f Frame index within the whole (untrimmed) content.
+/** @param time Time within the whole (untrimmed) content.
  *  @return Fade factor (between 0 and 1) or unset if there is no fade.
  */
 optional<double>
-VideoContent::fade (shared_ptr<const Film> film, Frame f) const
+VideoContent::fade(shared_ptr<const Film> film, ContentTime time) const
 {
-       DCPOMATIC_ASSERT (f >= 0);
+       DCPOMATIC_ASSERT(time.get() >= 0);
 
        double const vfr = _parent->active_video_frame_rate(film);
 
-       auto const ts = _parent->trim_start().frames_round(vfr);
-       if ((f - ts) < fade_in()) {
-               return double (f - ts) / fade_in();
+       auto const ts = _parent->trim_start();
+       auto const fade_in_time = ContentTime::from_frames(fade_in(), vfr);
+       if ((time - ts) < fade_in_time) {
+               return double(ContentTime(time - ts).get()) / fade_in_time.get();
        }
 
-       auto fade_out_start = length() - _parent->trim_end().frames_round(vfr) - fade_out();
-       if (f >= fade_out_start) {
-               return 1 - double (f - fade_out_start) / fade_out();
+       auto const fade_out_time = ContentTime::from_frames(fade_out(), vfr);
+       auto fade_out_start = ContentTime::from_frames(length(), vfr) - _parent->trim_end() - fade_out_time;
+       if (time >= fade_out_start) {
+               return 1 - double(ContentTime(time - fade_out_start).get()) / fade_out_time.get();
        }
 
-       return optional<double> ();
+       return {};
 }
 
 string
index 495d000e160401a24059a4f355a92df3acc90f04..eb106cc75877cb7186da74f545dc210e265c8afb 100644 (file)
@@ -66,7 +66,7 @@ public:
        VideoContent (Content* parent, cxml::ConstNodePtr node, int version, VideoRange video_range_hint);
        VideoContent (Content* parent, std::vector<std::shared_ptr<Content>>);
 
-       void as_xml (xmlpp::Node *) const;
+       void as_xml(xmlpp::Element*) const;
        std::string technical_summary () const;
        std::string identifier () const;
        void take_settings_from (std::shared_ptr<const VideoContent> c);
@@ -208,7 +208,7 @@ public:
        boost::optional<dcp::Size> size_after_crop() const;
        boost::optional<dcp::Size> scaled_size(dcp::Size container_size);
 
-       boost::optional<double> fade (std::shared_ptr<const Film> film, Frame) const;
+       boost::optional<double> fade(std::shared_ptr<const Film> film, dcpomatic::ContentTime time) const;
 
        std::string processing_description (std::shared_ptr<const Film> film);
 
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 9adca5a2da33d40d91f5312f5ccb9918df006206..546e1445b2110a1191f436a09a25f69e2b03a698 100644 (file)
@@ -121,11 +121,11 @@ VideoMXFContent::identifier () const
 
 
 void
-VideoMXFContent::as_xml (xmlpp::Node* node, bool with_paths) const
+VideoMXFContent::as_xml(xmlpp::Element* element, bool with_paths) const
 {
-       node->add_child("Type")->add_child_text("VideoMXF");
-       Content::as_xml (node, with_paths);
-       video->as_xml (node);
+       cxml::add_text_child(element, "Type", "VideoMXF");
+       Content::as_xml(element, with_paths);
+       video->as_xml(element);
 }
 
 
index 5a04c3da9c292405b83f7e3f0c4e20fe73995077..1731942a6958c0bcc533381dbcbe8f9a9effbc75 100644 (file)
@@ -40,7 +40,7 @@ public:
        std::string summary () const override;
        std::string technical_summary () const override;
        std::string identifier () const override;
-       void as_xml (xmlpp::Node* node, bool with_paths) const override;
+       void as_xml(xmlpp::Element* element, bool with_paths) const override;
        dcpomatic::DCPTime full_length (std::shared_ptr<const Film> film) const override;
        dcpomatic::DCPTime approximate_length () const override;
        void add_properties (std::shared_ptr<const Film> film, std::list<UserProperty>& p) const override;
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 7dd3bfe358877f7359f88c7c6e3bee0a06310de2..878b503a8b8aa0504226d88b19d6f07022dcbeca 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 1b1ef06294373adaa68443e9909b43981d4cb7a6..d516c6f5bea3c41ca8728139f0cbfb9a74314fd7 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 e658395211c5b040cce4b191518b641ea477ab9d..a68cc074ae61a8d7baf83cf35ec5b008b4429025 100644 (file)
@@ -616,12 +616,14 @@ private:
                help->Append (ID_help_report_a_problem, _("Report a problem..."));
 
                m->Append (_file_menu, _("&File"));
+               if (!Config::instance()->player_restricted_menus()) {
 #ifndef __WXOSX__
-               m->Append (edit, _("&Edit"));
+                       m->Append (edit, _("&Edit"));
 #endif
-               m->Append (view, _("&View"));
-               m->Append (tools, _("&Tools"));
-               m->Append (help, _("&Help"));
+                       m->Append (view, _("&View"));
+                       m->Append (tools, _("&Tools"));
+                       m->Append (help, _("&Help"));
+               }
        }
 
        void file_open ()
index a373d81e69e518f329bbd69479f78c4b86534544..b16a2be68338ed85a0195e8d174f49e4d3d120c0 100644 (file)
@@ -567,34 +567,36 @@ private:
 
        void setup_menu (wxMenuBar* m)
        {
-               auto file = new wxMenu;
-#ifdef __WXOSX__
-               file->Append(wxID_PREFERENCES, _("&Preferences...\tCtrl-,"));
-               file->Append (wxID_EXIT, _("&Exit"));
+#ifdef DCPOMATIC_OSX
+               auto help = new wxMenu;
+               /* These just need to be appended somewhere, it seems - they magically
+                * get moved to the right place.
+                */
+               if (!Config::instance()->playlist_editor_restricted_menus()) {
+                       help->Append(wxID_PREFERENCES, _("&Preferences...\tCtrl-,"));
+               }
+               help->Append(wxID_EXIT, _("&Exit"));
+               help->Append(wxID_ABOUT, _("About DCP-o-matic"));
+
+               m->Append(help, _("&Help"));
 #else
-               file->Append (wxID_EXIT, _("&Quit"));
-#endif
+               auto file = new wxMenu;
+               file->Append(wxID_EXIT, _("&Quit"));
 
-#ifndef __WXOSX__
                auto edit = new wxMenu;
-               edit->Append (wxID_PREFERENCES, _("&Preferences...\tCtrl-P"));
-#endif
+               edit->Append(wxID_PREFERENCES, _("&Preferences...\tCtrl-P"));
 
                auto help = new wxMenu;
-#ifdef __WXOSX__
-               help->Append (wxID_ABOUT, _("About DCP-o-matic"));
-#else
-               help->Append (wxID_ABOUT, _("About"));
-#endif
+               help->Append(wxID_ABOUT, _("About"));
 
-               m->Append (file, _("&File"));
-#ifndef __WXOSX__
-               m->Append (edit, _("&Edit"));
+               m->Append(file, _("&File"));
+               if (!Config::instance()->playlist_editor_restricted_menus()) {
+                       m->Append(edit, _("&Edit"));
+                       m->Append(help, _("&Help"));
+               }
 #endif
-               m->Append (help, _("&Help"));
        }
 
-
        void config_changed ()
        {
                try {
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 fcadd0c68fa6deccce4c48f9fda17ed0a9a50275..34923fb022cc5050598ba7b03efe8ccffe467d69 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;
@@ -258,15 +246,6 @@ AudioPanel::film_content_changed (int property)
                /* This is a bit aggressive but probably not so bad */
                _peak_cache.clear();
                setup_peak ();
-       } else if (property == DCPContentProperty::REFERENCE_AUDIO) {
-               if (ac.size() == 1) {
-                       shared_ptr<DCPContent> dcp = dynamic_pointer_cast<DCPContent> (ac.front ());
-                       checked_set (_reference, dcp ? dcp->reference_audio () : false);
-               } else {
-                       checked_set (_reference, false);
-               }
-
-               setup_sensitivity ();
        } else if (property == ContentProperty::VIDEO_FRAME_RATE) {
                setup_description ();
        } else if (property == AudioContentProperty::FADE_IN) {
@@ -384,19 +363,8 @@ AudioPanel::setup_sensitivity ()
                dcp = dynamic_pointer_cast<DCPContent> (sel.front ());
        }
 
-       string why_not;
-       bool const can_reference = dcp && dcp->can_reference_audio (_parent->film(), why_not);
-       wxString cannot;
-       if (why_not.empty()) {
-               cannot = _("Cannot reference this DCP's audio.");
-       } else {
-               cannot = _("Cannot reference this DCP's audio: ") + std_to_wx(why_not);
-       }
-       setup_refer_button (_reference, _reference_note, dcp, can_reference, cannot);
-
-       auto const ref = _reference->GetValue();
+       auto const ref = dcp && dcp->reference_audio();
        auto const single = sel.size() == 1;
-
        auto const all_have_video = std::all_of(sel.begin(), sel.end(), [](shared_ptr<const Content> c) { return static_cast<bool>(c->video); });
 
        _gain->wrapped()->Enable (!ref);
@@ -500,23 +468,6 @@ AudioPanel::active_jobs_changed (optional<string> old_active, optional<string> n
 }
 
 
-void
-AudioPanel::reference_clicked ()
-{
-       auto c = _parent->selected ();
-       if (c.size() != 1) {
-               return;
-       }
-
-       auto d = dynamic_pointer_cast<DCPContent>(c.front());
-       if (!d) {
-               return;
-       }
-
-       d->set_reference_audio (_reference->GetValue ());
-}
-
-
 void
 AudioPanel::set_film (shared_ptr<Film>)
 {
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;
 };
diff --git a/src/wx/colours.h b/src/wx/colours.h
new file mode 100644 (file)
index 0000000..0d33208
--- /dev/null
@@ -0,0 +1,25 @@
+/*
+    Copyright (C) 2023 Carl Hetherington <cth@carlh.net>
+
+    This file is part of DCP-o-matic.
+
+    DCP-o-matic is free software; you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or
+    (at your option) any later version.
+
+    DCP-o-matic is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with DCP-o-matic.  If not, see <http://www.gnu.org/licenses/>.
+
+*/
+
+
+#define VIDEO_CONTENT_COLOUR (wxColour(242, 92, 120, 255))
+#define AUDIO_CONTENT_COLOUR (wxColour(149, 121, 232, 255))
+#define TEXT_CONTENT_COLOUR (wxColour(163, 255, 154, 255))
+#define ATMOS_CONTENT_COLOUR (wxColour(149, 121, 232, 255))
index 642457d933e0f64aa99b8cc05388b48478ca4806..92a68e3b3e8df435a644118e2947ac254f10e453 100644 (file)
 #include "content_advanced_dialog.h"
 #include "content_menu.h"
 #include "content_properties_dialog.h"
+#include "content_timeline_audio_view.h"
+#include "content_timeline_video_view.h"
 #include "dir_dialog.h"
 #include "file_dialog.h"
 #include "film_viewer.h"
 #include "id.h"
 #include "repeat_dialog.h"
-#include "timeline_video_content_view.h"
-#include "timeline_audio_content_view.h"
 #include "wx_util.h"
 #include "lib/audio_content.h"
 #include "lib/config.h"
@@ -314,12 +314,12 @@ ContentMenu::remove ()
                                continue;
                        }
 
-                       shared_ptr<TimelineVideoContentView> video;
-                       shared_ptr<TimelineAudioContentView> audio;
+                       shared_ptr<ContentTimelineVideoView> video;
+                       shared_ptr<ContentTimelineAudioView> audio;
 
                        for (auto j: _views) {
-                               auto v = dynamic_pointer_cast<TimelineVideoContentView>(j);
-                               auto a = dynamic_pointer_cast<TimelineAudioContentView>(j);
+                               auto v = dynamic_pointer_cast<ContentTimelineVideoView>(j);
+                               auto a = dynamic_pointer_cast<ContentTimelineAudioView>(j);
                                if (v && v->content() == fc) {
                                        video = v;
                                } else if (a && a->content() == fc) {
index 9e73900fcf012efd41e30502e27509553aae74ae..ed9c15c834368c47b7a808315ab5f6b48a597e9c 100644 (file)
 
 #include "audio_panel.h"
 #include "content_panel.h"
+#include "content_timeline_dialog.h"
 #include "dcpomatic_button.h"
 #include "dir_dialog.h"
 #include "file_dialog.h"
 #include "film_viewer.h"
 #include "image_sequence_dialog.h"
 #include "text_panel.h"
-#include "timeline_dialog.h"
 #include "timing_panel.h"
 #include "video_panel.h"
 #include "wx_util.h"
index ca0d4971946f779336d28da953da681ddfd2b222..38b634a62566895bc12488322aabf115bf89c4ea 100644 (file)
@@ -33,12 +33,12 @@ LIBDCP_ENABLE_WARNINGS
 class AudioPanel;
 class ContentListCtrl;
 class ContentSubPanel;
+class ContentTimelineDialog;
 class Film;
 class FilmEditor;
 class FilmViewer;
 class LimitedContentPanelSplitter;
 class TextPanel;
-class TimelineDialog;
 class TimingPanel;
 class VideoPanel;
 class wxListCtrl;
@@ -132,7 +132,7 @@ private:
        EnumIndexedVector<TextPanel*, TextType> _text_panel;
        TimingPanel* _timing_panel;
        ContentMenu* _menu;
-       wx_ptr<TimelineDialog> _timeline_dialog;
+       wx_ptr<ContentTimelineDialog> _timeline_dialog;
        wxNotebook* _parent;
        wxWindow* _last_selected_tab = nullptr;
 
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;
 
diff --git a/src/wx/content_timeline.cc b/src/wx/content_timeline.cc
new file mode 100644 (file)
index 0000000..663f930
--- /dev/null
@@ -0,0 +1,1021 @@
+/*
+    Copyright (C) 2013-2021 Carl Hetherington <cth@carlh.net>
+
+    This file is part of DCP-o-matic.
+
+    DCP-o-matic is free software; you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or
+    (at your option) any later version.
+
+    DCP-o-matic is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with DCP-o-matic.  If not, see <http://www.gnu.org/licenses/>.
+
+*/
+
+#include "content_panel.h"
+#include "content_timeline.h"
+#include "film_editor.h"
+#include "film_viewer.h"
+#include "content_timeline_atmos_view.h"
+#include "content_timeline_audio_view.h"
+#include "content_timeline_text_view.h"
+#include "content_timeline_video_view.h"
+#include "timeline_labels_view.h"
+#include "timeline_reels_view.h"
+#include "timeline_time_axis_view.h"
+#include "wx_util.h"
+#include "lib/atmos_mxf_content.h"
+#include "lib/audio_content.h"
+#include "lib/constants.h"
+#include "lib/film.h"
+#include "lib/image_content.h"
+#include "lib/playlist.h"
+#include "lib/text_content.h"
+#include "lib/timer.h"
+#include "lib/video_content.h"
+#include <dcp/scope_guard.h>
+#include <dcp/warnings.h>
+LIBDCP_DISABLE_WARNINGS
+#include <wx/graphics.h>
+LIBDCP_ENABLE_WARNINGS
+#include <iterator>
+#include <list>
+
+
+using std::abs;
+using std::dynamic_pointer_cast;
+using std::list;
+using std::make_shared;
+using std::max;
+using std::min;
+using std::shared_ptr;
+using std::weak_ptr;
+using boost::bind;
+using boost::optional;
+using namespace dcpomatic;
+#if BOOST_VERSION >= 106100
+using namespace boost::placeholders;
+#endif
+
+
+int const ContentTimeline::_minimum_pixels_per_track = 16;
+
+
+ContentTimeline::ContentTimeline(wxWindow* parent, ContentPanel* cp, shared_ptr<Film> film, FilmViewer& viewer)
+       : Timeline(parent)
+       , _labels_canvas (new wxScrolledCanvas (this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxFULL_REPAINT_ON_RESIZE))
+       , _main_canvas (new wxScrolledCanvas (this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxFULL_REPAINT_ON_RESIZE))
+       , _content_panel (cp)
+       , _film (film)
+       , _viewer (viewer)
+       , _time_axis_view (new TimelineTimeAxisView (*this, 64))
+       , _reels_view (new TimelineReelsView (*this, 32))
+       , _labels_view (new TimelineLabelsView (*this))
+       , _tracks (0)
+       , _left_down (false)
+       , _down_view_position (0)
+       , _first_move (false)
+       , _menu (this, viewer)
+       , _snap (true)
+       , _tool (SELECT)
+       , _x_scroll_rate (16)
+       , _y_scroll_rate (16)
+       , _pixels_per_track (48)
+       , _first_resize (true)
+       , _timer (this)
+{
+#ifndef __WXOSX__
+       _labels_canvas->SetDoubleBuffered (true);
+       _main_canvas->SetDoubleBuffered (true);
+#endif
+
+       auto sizer = new wxBoxSizer (wxHORIZONTAL);
+       sizer->Add (_labels_canvas, 0, wxEXPAND);
+       _labels_canvas->SetMinSize (wxSize (_labels_view->bbox().width, -1));
+       sizer->Add (_main_canvas, 1, wxEXPAND);
+       SetSizer (sizer);
+
+       _labels_canvas->Bind(wxEVT_PAINT,      boost::bind(&ContentTimeline::paint_labels, this));
+       _main_canvas->Bind  (wxEVT_PAINT,      boost::bind(&ContentTimeline::paint_main,   this));
+       _main_canvas->Bind  (wxEVT_LEFT_DOWN,  boost::bind(&ContentTimeline::left_down,    this, _1));
+       _main_canvas->Bind  (wxEVT_LEFT_UP,    boost::bind(&ContentTimeline::left_up,      this, _1));
+       _main_canvas->Bind  (wxEVT_RIGHT_DOWN, boost::bind(&ContentTimeline::right_down,   this, _1));
+       _main_canvas->Bind  (wxEVT_MOTION,     boost::bind(&ContentTimeline::mouse_moved,  this, _1));
+       _main_canvas->Bind  (wxEVT_SIZE,       boost::bind(&ContentTimeline::resized,      this));
+       _main_canvas->Bind  (wxEVT_MOUSEWHEEL, boost::bind(&ContentTimeline::mouse_wheel_turned, this, _1));
+       _main_canvas->Bind  (wxEVT_SCROLLWIN_TOP,        boost::bind(&ContentTimeline::scrolled,     this, _1));
+       _main_canvas->Bind  (wxEVT_SCROLLWIN_BOTTOM,     boost::bind(&ContentTimeline::scrolled,     this, _1));
+       _main_canvas->Bind  (wxEVT_SCROLLWIN_LINEUP,     boost::bind(&ContentTimeline::scrolled,     this, _1));
+       _main_canvas->Bind  (wxEVT_SCROLLWIN_LINEDOWN,   boost::bind(&ContentTimeline::scrolled,     this, _1));
+       _main_canvas->Bind  (wxEVT_SCROLLWIN_PAGEUP,     boost::bind(&ContentTimeline::scrolled,     this, _1));
+       _main_canvas->Bind  (wxEVT_SCROLLWIN_PAGEDOWN,   boost::bind(&ContentTimeline::scrolled,     this, _1));
+       _main_canvas->Bind  (wxEVT_SCROLLWIN_THUMBTRACK, boost::bind(&ContentTimeline::scrolled,     this, _1));
+
+       film_change(ChangeType::DONE, FilmProperty::CONTENT);
+
+       SetMinSize (wxSize (640, 4 * pixels_per_track() + 96));
+
+       _film_changed_connection = film->Change.connect(bind(&ContentTimeline::film_change, this, _1, _2));
+       _film_content_change_connection = film->ContentChange.connect(bind(&ContentTimeline::film_content_change, this, _1, _3, _4));
+
+       Bind(wxEVT_TIMER, boost::bind(&ContentTimeline::update_playhead, this));
+       _timer.Start (200, wxTIMER_CONTINUOUS);
+
+       setup_scrollbars ();
+       _labels_canvas->ShowScrollbars (wxSHOW_SB_NEVER, wxSHOW_SB_NEVER);
+}
+
+
+void
+ContentTimeline::mouse_wheel_turned(wxMouseEvent& event)
+{
+       auto const rotation = event.GetWheelRotation();
+
+       if (event.ControlDown()) {
+               /* On my mouse one click of the scroll wheel is 120, and it's -ve when
+                * scrolling the wheel towards me.
+                */
+               auto const scale = rotation > 0 ?
+                       (1.0 / (rotation / 90.0)) :
+                       (-rotation / 90.0);
+
+               int before_start_x;
+               int before_start_y;
+               _main_canvas->GetViewStart(&before_start_x, &before_start_y);
+
+               auto const before_pps = _pixels_per_second.get_value_or(1);
+               auto const before_pos = _last_mouse_wheel_x && *_last_mouse_wheel_x == event.GetX() ?
+                       *_last_mouse_wheel_time :
+                       (before_start_x * _x_scroll_rate + event.GetX()) / before_pps;
+
+               set_pixels_per_second(before_pps * scale);
+               setup_scrollbars();
+
+               auto after_left = std::max(0.0, before_pos * _pixels_per_second.get_value_or(1) - event.GetX());
+               _main_canvas->Scroll(after_left / _x_scroll_rate, before_start_y);
+               _labels_canvas->Scroll(0, before_start_y);
+               Refresh();
+
+               if (!_last_mouse_wheel_x || *_last_mouse_wheel_x != event.GetX()) {
+                       _last_mouse_wheel_x = event.GetX();
+                       _last_mouse_wheel_time = before_pos;
+               }
+       } else if (event.ShiftDown()) {
+               int before_start_x;
+               int before_start_y;
+               _main_canvas->GetViewStart(&before_start_x, &before_start_y);
+               auto const width = _main_canvas->GetSize().GetWidth();
+               _main_canvas->Scroll(std::max(0.0, before_start_x - rotation * 100.0 / width), before_start_y);
+       }
+}
+
+
+void
+ContentTimeline::update_playhead()
+{
+       Refresh ();
+}
+
+
+void
+ContentTimeline::paint_labels()
+{
+       wxPaintDC dc (_labels_canvas);
+
+       auto film = _film.lock();
+       if (film->content().empty()) {
+               return;
+       }
+
+       auto gc = wxGraphicsContext::Create (dc);
+       if (!gc) {
+               return;
+       }
+
+       dcp::ScopeGuard sg = [gc]() { delete gc; };
+
+       int vsx, vsy;
+       _labels_canvas->GetViewStart (&vsx, &vsy);
+       gc->Translate (-vsx * _x_scroll_rate, -vsy * _y_scroll_rate + tracks_y_offset());
+
+       _labels_view->paint (gc, {});
+}
+
+
+void
+ContentTimeline::paint_main()
+{
+       wxPaintDC dc (_main_canvas);
+       dc.Clear();
+
+       auto film = _film.lock();
+       if (film->content().empty()) {
+               return;
+       }
+
+       _main_canvas->DoPrepareDC (dc);
+
+       auto gc = wxGraphicsContext::Create (dc);
+       if (!gc) {
+               return;
+       }
+
+       dcp::ScopeGuard sg = [gc]() { delete gc; };
+
+       gc->SetAntialiasMode (wxANTIALIAS_DEFAULT);
+
+       for (auto i: _views) {
+
+               auto ic = dynamic_pointer_cast<TimelineContentView> (i);
+
+               /* Find areas of overlap with other content views, so that we can plot them */
+               list<dcpomatic::Rect<int>> overlaps;
+               for (auto j: _views) {
+                       auto jc = dynamic_pointer_cast<TimelineContentView> (j);
+                       /* No overlap with non-content views, views on different tracks, audio views or non-active views */
+                       if (!ic || !jc || i == j || ic->track() != jc->track() || ic->track().get_value_or(2) >= 2 || !ic->active() || !jc->active()) {
+                               continue;
+                       }
+
+                       auto r = j->bbox().intersection(i->bbox());
+                       if (r) {
+                               overlaps.push_back (r.get ());
+                       }
+               }
+
+               i->paint (gc, overlaps);
+       }
+
+       if (_zoom_point) {
+               gc->SetPen(gui_is_dark() ? *wxWHITE_PEN : *wxBLACK_PEN);
+               gc->SetBrush (*wxTRANSPARENT_BRUSH);
+               gc->DrawRectangle (
+                       min (_down_point.x, _zoom_point->x),
+                       min (_down_point.y, _zoom_point->y),
+                       abs (_down_point.x - _zoom_point->x),
+                       abs (_down_point.y - _zoom_point->y)
+                       );
+       }
+
+       /* Playhead */
+
+       gc->SetPen (*wxRED_PEN);
+       auto path = gc->CreatePath ();
+       double const ph = _viewer.position().seconds() * pixels_per_second().get_value_or(0);
+       path.MoveToPoint (ph, 0);
+       path.AddLineToPoint (ph, pixels_per_track() * _tracks + 32);
+       gc->StrokePath (path);
+}
+
+
+void
+ContentTimeline::film_change(ChangeType type, FilmProperty p)
+{
+       if (type != ChangeType::DONE) {
+               return;
+       }
+
+       if (p == FilmProperty::CONTENT || p == FilmProperty::REEL_TYPE || p == FilmProperty::REEL_LENGTH) {
+               ensure_ui_thread ();
+               recreate_views ();
+       } else if (p == FilmProperty::CONTENT_ORDER) {
+               Refresh ();
+       }
+}
+
+
+void
+ContentTimeline::recreate_views()
+{
+       auto film = _film.lock ();
+       if (!film) {
+               return;
+       }
+
+       _views.clear ();
+       _views.push_back (_time_axis_view);
+       _views.push_back (_reels_view);
+
+       for (auto i: film->content ()) {
+               if (i->video) {
+                       _views.push_back(make_shared<ContentTimelineVideoView>(*this, i));
+               }
+
+               if (i->audio && !i->audio->mapping().mapped_output_channels().empty ()) {
+                       _views.push_back(make_shared<ContentTimelineAudioView>(*this, i));
+               }
+
+               for (auto j: i->text) {
+                       _views.push_back(make_shared<ContentTimelineTextView>(*this, i, j));
+               }
+
+               if (i->atmos) {
+                       _views.push_back(make_shared<ContentTimelineAtmosView>(*this, i));
+               }
+       }
+
+       assign_tracks ();
+       setup_scrollbars ();
+       Refresh ();
+}
+
+
+void
+ContentTimeline::film_content_change(ChangeType type, int property, bool frequent)
+{
+       if (type != ChangeType::DONE) {
+               return;
+       }
+
+       ensure_ui_thread ();
+
+       if (property == AudioContentProperty::STREAMS || property == VideoContentProperty::FRAME_TYPE) {
+               recreate_views ();
+       } else if (property == ContentProperty::POSITION || property == ContentProperty::LENGTH) {
+               _reels_view->force_redraw ();
+       } else if (!frequent) {
+               setup_scrollbars ();
+               Refresh ();
+       }
+}
+
+
+template <class T>
+int
+place(shared_ptr<const Film> film, ContentTimelineViewList& views, int& tracks)
+{
+       int const base = tracks;
+
+       for (auto i: views) {
+               if (!dynamic_pointer_cast<T>(i)) {
+                       continue;
+               }
+
+               auto cv = dynamic_pointer_cast<TimelineContentView> (i);
+               DCPOMATIC_ASSERT(cv);
+
+               int t = base;
+
+               auto content = cv->content();
+               DCPTimePeriod const content_period = content->period(film);
+
+               while (true) {
+                       auto j = views.begin();
+                       while (j != views.end()) {
+                               auto test = dynamic_pointer_cast<T> (*j);
+                               if (!test) {
+                                       ++j;
+                                       continue;
+                               }
+
+                               auto test_content = test->content();
+                               if (
+                                       test->track() && test->track().get() == t &&
+                                       content_period.overlap(test_content->period(film))
+                                  ) {
+                                       /* we have an overlap on track `t' */
+                                       ++t;
+                                       break;
+                               }
+
+                               ++j;
+                       }
+
+                       if (j == views.end ()) {
+                               /* no overlap on `t' */
+                               break;
+                       }
+               }
+
+               cv->set_track (t);
+               tracks = max (tracks, t + 1);
+       }
+
+       return tracks - base;
+}
+
+
+/** Compare the mapped output channels of two TimelineViews, so we can into
+ *  order of first mapped DCP channel.
+ */
+struct AudioMappingComparator {
+       bool operator()(shared_ptr<ContentTimelineView> a, shared_ptr<ContentTimelineView> b) {
+               int la = -1;
+               auto cva = dynamic_pointer_cast<ContentTimelineAudioView>(a);
+               if (cva) {
+                       auto oc = cva->content()->audio->mapping().mapped_output_channels();
+                       la = *min_element(boost::begin(oc), boost::end(oc));
+               }
+               int lb = -1;
+               auto cvb = dynamic_pointer_cast<ContentTimelineAudioView>(b);
+               if (cvb) {
+                       auto oc = cvb->content()->audio->mapping().mapped_output_channels();
+                       lb = *min_element(boost::begin(oc), boost::end(oc));
+               }
+               return la < lb;
+       }
+};
+
+
+void
+ContentTimeline::assign_tracks()
+{
+       /* Tracks are:
+          Video 1
+          Video 2
+          Video N
+          Text 1
+          Text 2
+          Text N
+          Atmos
+          Audio 1
+          Audio 2
+          Audio N
+       */
+
+       auto film = _film.lock ();
+       DCPOMATIC_ASSERT (film);
+
+       _tracks = 0;
+
+       for (auto i: _views) {
+               auto c = dynamic_pointer_cast<TimelineContentView>(i);
+               if (c) {
+                       c->unset_track ();
+               }
+       }
+
+       int const video_tracks = place<ContentTimelineVideoView>(film, _views, _tracks);
+       int const text_tracks = place<ContentTimelineTextView>(film, _views, _tracks);
+
+       /* Atmos */
+
+       bool have_atmos = false;
+       for (auto i: _views) {
+               auto cv = dynamic_pointer_cast<ContentTimelineAtmosView>(i);
+               if (cv) {
+                       cv->set_track (_tracks);
+                       have_atmos = true;
+               }
+       }
+
+       if (have_atmos) {
+               ++_tracks;
+       }
+
+       /* Audio.  We're sorting the views so that we get the audio views in order of increasing
+          DCP channel index.
+       */
+
+       auto views = _views;
+       sort(views.begin(), views.end(), AudioMappingComparator());
+       int const audio_tracks = place<ContentTimelineAudioView>(film, views, _tracks);
+
+       _labels_view->set_video_tracks (video_tracks);
+       _labels_view->set_audio_tracks (audio_tracks);
+       _labels_view->set_text_tracks (text_tracks);
+       _labels_view->set_atmos (have_atmos);
+
+       _time_axis_view->set_y (tracks());
+       _reels_view->set_y (8);
+}
+
+
+int
+ContentTimeline::tracks() const
+{
+       return _tracks;
+}
+
+
+void
+ContentTimeline::setup_scrollbars()
+{
+       auto film = _film.lock ();
+       if (!film || !_pixels_per_second) {
+               return;
+       }
+
+       int const h = tracks() * pixels_per_track() + tracks_y_offset() + _time_axis_view->bbox().height;
+
+       _labels_canvas->SetVirtualSize (_labels_view->bbox().width, h);
+       _labels_canvas->SetScrollRate (_x_scroll_rate, _y_scroll_rate);
+       _main_canvas->SetVirtualSize (*_pixels_per_second * film->length().seconds(), h);
+       _main_canvas->SetScrollRate (_x_scroll_rate, _y_scroll_rate);
+}
+
+
+shared_ptr<ContentTimelineView>
+ContentTimeline::event_to_view(wxMouseEvent& ev)
+{
+       /* Search backwards through views so that we find the uppermost one first */
+       auto i = _views.rbegin();
+
+       int vsx, vsy;
+       _main_canvas->GetViewStart (&vsx, &vsy);
+       Position<int> const p (ev.GetX() + vsx * _x_scroll_rate, ev.GetY() + vsy * _y_scroll_rate);
+
+       while (i != _views.rend() && !(*i)->bbox().contains (p)) {
+               ++i;
+       }
+
+       if (i == _views.rend ()) {
+               return {};
+       }
+
+       return *i;
+}
+
+
+void
+ContentTimeline::left_down(wxMouseEvent& ev)
+{
+       _left_down = true;
+       _down_point = ev.GetPosition ();
+
+       switch (_tool) {
+       case SELECT:
+               left_down_select (ev);
+               break;
+       case ZOOM:
+       case ZOOM_ALL:
+       case SNAP:
+       case SEQUENCE:
+               /* Nothing to do */
+               break;
+       }
+}
+
+
+void
+ContentTimeline::left_down_select(wxMouseEvent& ev)
+{
+       auto view = event_to_view (ev);
+       auto content_view = dynamic_pointer_cast<TimelineContentView>(view);
+
+       _down_view.reset ();
+
+       if (content_view) {
+               _down_view = content_view;
+               _down_view_position = content_view->content()->position ();
+       }
+
+       if (dynamic_pointer_cast<TimelineTimeAxisView>(view)) {
+               int vsx, vsy;
+               _main_canvas->GetViewStart(&vsx, &vsy);
+               _viewer.seek(DCPTime::from_seconds((ev.GetPosition().x + vsx * _x_scroll_rate) / _pixels_per_second.get_value_or(1)), true);
+       }
+
+       for (auto i: _views) {
+               auto cv = dynamic_pointer_cast<TimelineContentView>(i);
+               if (!cv) {
+                       continue;
+               }
+
+               if (!ev.ShiftDown ()) {
+                       cv->set_selected (view == i);
+               }
+       }
+
+       if (content_view && ev.ShiftDown ()) {
+               content_view->set_selected (!content_view->selected ());
+       }
+
+       _first_move = false;
+
+       if (_down_view) {
+               /* Pre-compute the points that we might snap to */
+               for (auto i: _views) {
+                       auto cv = dynamic_pointer_cast<TimelineContentView>(i);
+                       if (!cv || cv == _down_view || cv->content() == _down_view->content()) {
+                               continue;
+                       }
+
+                       auto film = _film.lock ();
+                       DCPOMATIC_ASSERT (film);
+
+                       _start_snaps.push_back (cv->content()->position());
+                       _end_snaps.push_back (cv->content()->position());
+                       _start_snaps.push_back (cv->content()->end(film));
+                       _end_snaps.push_back (cv->content()->end(film));
+
+                       for (auto i: cv->content()->reel_split_points(film)) {
+                               _start_snaps.push_back (i);
+                       }
+               }
+
+               /* Tell everyone that things might change frequently during the drag */
+               _down_view->content()->set_change_signals_frequent (true);
+       }
+}
+
+
+void
+ContentTimeline::left_up(wxMouseEvent& ev)
+{
+       _left_down = false;
+
+       switch (_tool) {
+       case SELECT:
+               left_up_select (ev);
+               break;
+       case ZOOM:
+               left_up_zoom (ev);
+               break;
+       case ZOOM_ALL:
+       case SNAP:
+       case SEQUENCE:
+               break;
+       }
+}
+
+
+void
+ContentTimeline::left_up_select(wxMouseEvent& ev)
+{
+       if (_down_view) {
+               _down_view->content()->set_change_signals_frequent (false);
+       }
+
+       _content_panel->set_selection (selected_content ());
+       /* Since we may have just set change signals back to `not-frequent', we have to
+          make sure this position change is signalled, even if the position value has
+          not changed since the last time it was set (with frequent=true).  This is
+          a bit of a hack.
+       */
+       set_position_from_event (ev, true);
+
+       /* Clear up up the stuff we don't do during drag */
+       assign_tracks ();
+       setup_scrollbars ();
+       Refresh ();
+
+       _start_snaps.clear ();
+       _end_snaps.clear ();
+}
+
+
+void
+ContentTimeline::left_up_zoom(wxMouseEvent& ev)
+{
+       _zoom_point = ev.GetPosition ();
+
+       int vsx, vsy;
+       _main_canvas->GetViewStart (&vsx, &vsy);
+
+       wxPoint top_left(min(_down_point.x, _zoom_point->x), min(_down_point.y, _zoom_point->y));
+       wxPoint bottom_right(max(_down_point.x, _zoom_point->x), max(_down_point.y, _zoom_point->y));
+
+       if ((bottom_right.x - top_left.x) < 8 || (bottom_right.y - top_left.y) < 8) {
+               /* Very small zoom rectangle: we assume it wasn't intentional */
+               _zoom_point = optional<wxPoint> ();
+               Refresh ();
+               return;
+       }
+
+       auto const time_left = DCPTime::from_seconds((top_left.x + vsx) / *_pixels_per_second);
+       auto const time_right = DCPTime::from_seconds((bottom_right.x + vsx) / *_pixels_per_second);
+       set_pixels_per_second (double(GetSize().GetWidth()) / (time_right.seconds() - time_left.seconds()));
+
+       double const tracks_top = double(top_left.y - tracks_y_offset()) / _pixels_per_track;
+       double const tracks_bottom = double(bottom_right.y - tracks_y_offset()) / _pixels_per_track;
+       set_pixels_per_track (lrint(GetSize().GetHeight() / (tracks_bottom - tracks_top)));
+
+       setup_scrollbars ();
+       int const y = (tracks_top * _pixels_per_track + tracks_y_offset()) / _y_scroll_rate;
+       _main_canvas->Scroll (time_left.seconds() * *_pixels_per_second / _x_scroll_rate, y);
+       _labels_canvas->Scroll (0, y);
+
+       _zoom_point = optional<wxPoint> ();
+       Refresh ();
+}
+
+
+void
+ContentTimeline::set_pixels_per_track(int h)
+{
+       _pixels_per_track = max(_minimum_pixels_per_track, h);
+}
+
+
+void
+ContentTimeline::mouse_moved(wxMouseEvent& ev)
+{
+       switch (_tool) {
+       case SELECT:
+               mouse_moved_select (ev);
+               break;
+       case ZOOM:
+               mouse_moved_zoom (ev);
+               break;
+       case ZOOM_ALL:
+       case SNAP:
+       case SEQUENCE:
+               break;
+       }
+}
+
+
+void
+ContentTimeline::mouse_moved_select(wxMouseEvent& ev)
+{
+       if (!_left_down) {
+               return;
+       }
+
+       set_position_from_event (ev);
+}
+
+
+void
+ContentTimeline::mouse_moved_zoom(wxMouseEvent& ev)
+{
+       if (!_left_down) {
+               return;
+       }
+
+       _zoom_point = ev.GetPosition ();
+       setup_scrollbars();
+       Refresh ();
+}
+
+
+void
+ContentTimeline::right_down(wxMouseEvent& ev)
+{
+       switch (_tool) {
+       case SELECT:
+               right_down_select (ev);
+               break;
+       case ZOOM:
+               /* Zoom out */
+               set_pixels_per_second (*_pixels_per_second / 2);
+               set_pixels_per_track (_pixels_per_track / 2);
+               setup_scrollbars ();
+               Refresh ();
+               break;
+       case ZOOM_ALL:
+       case SNAP:
+       case SEQUENCE:
+               break;
+       }
+}
+
+
+void
+ContentTimeline::right_down_select(wxMouseEvent& ev)
+{
+       auto view = event_to_view (ev);
+       auto cv = dynamic_pointer_cast<TimelineContentView> (view);
+       if (!cv) {
+               return;
+       }
+
+       if (!cv->selected ()) {
+               clear_selection ();
+               cv->set_selected (true);
+       }
+
+       _menu.popup (_film, selected_content (), selected_views (), ev.GetPosition ());
+}
+
+
+void
+ContentTimeline::maybe_snap(DCPTime a, DCPTime b, optional<DCPTime>& nearest_distance) const
+{
+       auto const d = a - b;
+       if (!nearest_distance || d.abs() < nearest_distance.get().abs()) {
+               nearest_distance = d;
+       }
+}
+
+
+void
+ContentTimeline::set_position_from_event(wxMouseEvent& ev, bool force_emit)
+{
+       if (!_pixels_per_second) {
+               return;
+       }
+
+       double const pps = _pixels_per_second.get ();
+
+       auto const p = ev.GetPosition();
+
+       if (!_first_move) {
+               /* We haven't moved yet; in that case, we must move the mouse some reasonable distance
+                  before the drag is considered to have started.
+               */
+               int const dist = sqrt (pow (p.x - _down_point.x, 2) + pow (p.y - _down_point.y, 2));
+               if (dist < 8) {
+                       return;
+               }
+               _first_move = true;
+       }
+
+       if (!_down_view) {
+               return;
+       }
+
+       auto new_position = _down_view_position + DCPTime::from_seconds ((p.x - _down_point.x) / pps);
+
+       auto film = _film.lock ();
+       DCPOMATIC_ASSERT (film);
+
+       if (_snap) {
+               auto const new_end = new_position + _down_view->content()->length_after_trim(film);
+               /* Signed `distance' to nearest thing (i.e. negative is left on the timeline,
+                  positive is right).
+               */
+               optional<DCPTime> nearest_distance;
+
+               /* Find the nearest snap point */
+
+               for (auto i: _start_snaps) {
+                       maybe_snap (i, new_position, nearest_distance);
+               }
+
+               for (auto i: _end_snaps) {
+                       maybe_snap (i, new_end, nearest_distance);
+               }
+
+               if (nearest_distance) {
+                       /* Snap if it's close; `close' means within a proportion of the time on the timeline */
+                       if (nearest_distance.get().abs() < DCPTime::from_seconds ((width() / pps) / SNAP_SUBDIVISION)) {
+                               new_position += nearest_distance.get ();
+                       }
+               }
+       }
+
+       if (new_position < DCPTime ()) {
+               new_position = DCPTime ();
+       }
+
+       _down_view->content()->set_position (film, new_position, force_emit);
+
+       film->set_sequence (false);
+}
+
+
+void
+ContentTimeline::force_redraw(dcpomatic::Rect<int> const & r)
+{
+       _main_canvas->RefreshRect (wxRect (r.x, r.y, r.width, r.height), false);
+}
+
+
+shared_ptr<const Film>
+ContentTimeline::film() const
+{
+       return _film.lock ();
+}
+
+
+void
+ContentTimeline::resized()
+{
+       if (_main_canvas->GetSize().GetWidth() > 0 && _first_resize) {
+               zoom_all ();
+               _first_resize = false;
+       }
+       setup_scrollbars ();
+}
+
+
+void
+ContentTimeline::clear_selection()
+{
+       for (auto i: _views) {
+               shared_ptr<TimelineContentView> cv = dynamic_pointer_cast<TimelineContentView>(i);
+               if (cv) {
+                       cv->set_selected (false);
+               }
+       }
+}
+
+
+TimelineContentViewList
+ContentTimeline::selected_views() const
+{
+       TimelineContentViewList sel;
+
+       for (auto i: _views) {
+               auto cv = dynamic_pointer_cast<TimelineContentView>(i);
+               if (cv && cv->selected()) {
+                       sel.push_back (cv);
+               }
+       }
+
+       return sel;
+}
+
+
+ContentList
+ContentTimeline::selected_content() const
+{
+       ContentList sel;
+
+       for (auto i: selected_views()) {
+               sel.push_back(i->content());
+       }
+
+       return sel;
+}
+
+
+void
+ContentTimeline::set_selection(ContentList selection)
+{
+       for (auto i: _views) {
+               auto cv = dynamic_pointer_cast<TimelineContentView> (i);
+               if (cv) {
+                       cv->set_selected (find (selection.begin(), selection.end(), cv->content ()) != selection.end ());
+               }
+       }
+}
+
+
+int
+ContentTimeline::tracks_y_offset() const
+{
+       return _reels_view->bbox().height + 4;
+}
+
+
+int
+ContentTimeline::width() const
+{
+       return _main_canvas->GetVirtualSize().GetWidth();
+}
+
+
+void
+ContentTimeline::scrolled(wxScrollWinEvent& ev)
+{
+       if (ev.GetOrientation() == wxVERTICAL) {
+               int x, y;
+               _main_canvas->GetViewStart (&x, &y);
+               _labels_canvas->Scroll (0, y);
+       }
+       ev.Skip ();
+}
+
+
+void
+ContentTimeline::tool_clicked(Tool t)
+{
+       switch (t) {
+       case ZOOM:
+       case SELECT:
+               _tool = t;
+               break;
+       case ZOOM_ALL:
+               zoom_all ();
+               break;
+       case SNAP:
+       case SEQUENCE:
+               break;
+       }
+}
+
+
+void
+ContentTimeline::zoom_all()
+{
+       auto film = _film.lock ();
+       DCPOMATIC_ASSERT (film);
+       set_pixels_per_second((_main_canvas->GetSize().GetWidth() - 32) / std::max(1.0, film->length().seconds()));
+       set_pixels_per_track((_main_canvas->GetSize().GetHeight() - tracks_y_offset() - _time_axis_view->bbox().height - 32) / std::max(1, _tracks));
+       setup_scrollbars ();
+       _main_canvas->Scroll (0, 0);
+       _labels_canvas->Scroll (0, 0);
+       Refresh ();
+}
+
+
+void
+ContentTimeline::keypress(wxKeyEvent const& event)
+{
+       if (event.GetKeyCode() == WXK_DELETE) {
+               auto film = _film.lock();
+               DCPOMATIC_ASSERT(film);
+               film->remove_content(selected_content());
+       } else {
+               switch (event.GetRawKeyCode()) {
+               case '+':
+                       set_pixels_per_second(_pixels_per_second.get_value_or(1) * 2);
+                       setup_scrollbars();
+                       break;
+               case '-':
+                       set_pixels_per_second(_pixels_per_second.get_value_or(1) / 2);
+                       setup_scrollbars();
+                       break;
+               }
+       }
+}
+
diff --git a/src/wx/content_timeline.h b/src/wx/content_timeline.h
new file mode 100644 (file)
index 0000000..10f8801
--- /dev/null
@@ -0,0 +1,149 @@
+/*
+    Copyright (C) 2013-2019 Carl Hetherington <cth@carlh.net>
+
+    This file is part of DCP-o-matic.
+
+    DCP-o-matic is free software; you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or
+    (at your option) any later version.
+
+    DCP-o-matic is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with DCP-o-matic.  If not, see <http://www.gnu.org/licenses/>.
+
+*/
+
+
+#include "content_menu.h"
+#include "timeline.h"
+#include "timeline_content_view.h"
+#include "lib/film_property.h"
+#include "lib/rect.h"
+#include <dcp/warnings.h>
+LIBDCP_DISABLE_WARNINGS
+#include <wx/wx.h>
+LIBDCP_ENABLE_WARNINGS
+#include <boost/signals2.hpp>
+
+
+class ContentPanel;
+class ContentTimelineView;
+class Film;
+class FilmViewer;
+class TimelineLabelsView;
+class TimelineReelsView;
+class TimelineTimeAxisView;
+
+
+class ContentTimeline : public Timeline
+{
+public:
+       ContentTimeline(wxWindow *, ContentPanel *, std::shared_ptr<Film>, FilmViewer& viewer);
+
+       std::shared_ptr<const Film> film () const;
+
+       void force_redraw (dcpomatic::Rect<int> const &);
+
+       int width () const;
+
+       int pixels_per_track () const {
+               return _pixels_per_track;
+       }
+
+       int tracks () const;
+
+       void set_snap (bool s) {
+               _snap = s;
+       }
+
+       bool snap () const {
+               return _snap;
+       }
+
+       void set_selection (ContentList selection);
+
+       enum Tool {
+               SELECT,
+               ZOOM,
+               ZOOM_ALL,
+               SNAP,
+               SEQUENCE
+       };
+
+       void tool_clicked (Tool t);
+
+       int tracks_y_offset () const;
+
+       void keypress(wxKeyEvent const &);
+
+private:
+       void paint_labels ();
+       void paint_main ();
+       void left_down (wxMouseEvent &);
+       void left_down_select (wxMouseEvent &);
+       void left_up (wxMouseEvent &);
+       void left_up_select (wxMouseEvent &);
+       void left_up_zoom (wxMouseEvent &);
+       void right_down (wxMouseEvent &);
+       void right_down_select (wxMouseEvent &);
+       void mouse_moved (wxMouseEvent &);
+       void mouse_moved_select (wxMouseEvent &);
+       void mouse_moved_zoom (wxMouseEvent &);
+       void film_change(ChangeType type, FilmProperty);
+       void film_content_change (ChangeType type, int, bool frequent);
+       void resized ();
+       void assign_tracks ();
+       void set_position_from_event (wxMouseEvent& ev, bool force_emit = false);
+       void clear_selection ();
+       void recreate_views ();
+       void setup_scrollbars ();
+       void scrolled (wxScrollWinEvent& ev);
+       void set_pixels_per_track (int h);
+       void zoom_all ();
+       void update_playhead ();
+       void mouse_wheel_turned(wxMouseEvent& event);
+
+       std::shared_ptr<ContentTimelineView> event_to_view(wxMouseEvent &);
+       TimelineContentViewList selected_views () const;
+       ContentList selected_content () const;
+       void maybe_snap (dcpomatic::DCPTime a, dcpomatic::DCPTime b, boost::optional<dcpomatic::DCPTime>& nearest_distance) const;
+
+       wxScrolledCanvas* _labels_canvas;
+       wxScrolledCanvas* _main_canvas;
+       ContentPanel* _content_panel;
+       std::weak_ptr<Film> _film;
+       FilmViewer& _viewer;
+       ContentTimelineViewList _views;
+       std::shared_ptr<TimelineTimeAxisView> _time_axis_view;
+       std::shared_ptr<TimelineReelsView> _reels_view;
+       std::shared_ptr<TimelineLabelsView> _labels_view;
+       int _tracks;
+       bool _left_down;
+       wxPoint _down_point;
+       boost::optional<wxPoint> _zoom_point;
+       std::shared_ptr<TimelineContentView> _down_view;
+       dcpomatic::DCPTime _down_view_position;
+       bool _first_move;
+       ContentMenu _menu;
+       bool _snap;
+       std::list<dcpomatic::DCPTime> _start_snaps;
+       std::list<dcpomatic::DCPTime> _end_snaps;
+       Tool _tool;
+       int _x_scroll_rate;
+       int _y_scroll_rate;
+       int _pixels_per_track;
+       bool _first_resize;
+       wxTimer _timer;
+       boost::optional<int> _last_mouse_wheel_x;
+       boost::optional<double> _last_mouse_wheel_time;
+
+       static int const _minimum_pixels_per_track;
+
+       boost::signals2::scoped_connection _film_changed_connection;
+       boost::signals2::scoped_connection _film_content_change_connection;
+};
diff --git a/src/wx/content_timeline_atmos_view.cc b/src/wx/content_timeline_atmos_view.cc
new file mode 100644 (file)
index 0000000..2352ba5
--- /dev/null
@@ -0,0 +1,51 @@
+/*
+    Copyright (C) 2016-2021 Carl Hetherington <cth@carlh.net>
+
+    This file is part of DCP-o-matic.
+
+    DCP-o-matic is free software; you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or
+    (at your option) any later version.
+
+    DCP-o-matic is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with DCP-o-matic.  If not, see <http://www.gnu.org/licenses/>.
+
+*/
+
+
+#include "colours.h"
+#include "content_timeline_atmos_view.h"
+
+
+using std::shared_ptr;
+
+
+/** @class ContentTimelineContentView
+ *  @brief Content timeline view for AtmosContent.
+ */
+
+ContentTimelineAtmosView::ContentTimelineAtmosView(ContentTimeline& tl, shared_ptr<Content> c)
+       : TimelineContentView (tl, c)
+{
+
+}
+
+
+wxColour
+ContentTimelineAtmosView::background_colour() const
+{
+       return ATMOS_CONTENT_COLOUR;
+}
+
+
+wxColour
+ContentTimelineAtmosView::foreground_colour() const
+{
+       return wxColour (0, 0, 0, 255);
+}
diff --git a/src/wx/content_timeline_atmos_view.h b/src/wx/content_timeline_atmos_view.h
new file mode 100644 (file)
index 0000000..d5fea92
--- /dev/null
@@ -0,0 +1,40 @@
+/*
+    Copyright (C) 2016-2021 Carl Hetherington <cth@carlh.net>
+
+    This file is part of DCP-o-matic.
+
+    DCP-o-matic is free software; you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or
+    (at your option) any later version.
+
+    DCP-o-matic is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with DCP-o-matic.  If not, see <http://www.gnu.org/licenses/>.
+
+*/
+
+
+#include "timeline_content_view.h"
+
+
+/** @class ContentTimelineAtmosContentView
+ *  @brief Content timeline view for AtmosContent.
+ */
+class ContentTimelineAtmosView : public TimelineContentView
+{
+public:
+       ContentTimelineAtmosView(ContentTimeline& tl, std::shared_ptr<Content> c);
+
+private:
+       bool active () const override {
+               return true;
+       }
+
+       wxColour background_colour () const override;
+       wxColour foreground_colour () const override;
+};
diff --git a/src/wx/content_timeline_audio_view.cc b/src/wx/content_timeline_audio_view.cc
new file mode 100644 (file)
index 0000000..600e390
--- /dev/null
@@ -0,0 +1,86 @@
+/*
+    Copyright (C) 2013-2018 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 "colours.h"
+#include "content_timeline_audio_view.h"
+#include "wx_util.h"
+#include "lib/audio_content.h"
+#include "lib/util.h"
+
+
+using std::dynamic_pointer_cast;
+using std::list;
+using std::shared_ptr;
+
+
+/** @class ContentTimelineAudioView
+ *  @brief Content timeline view for AudioContent.
+ */
+
+
+ContentTimelineAudioView::ContentTimelineAudioView(ContentTimeline& tl, shared_ptr<Content> c)
+       : TimelineContentView (tl, c)
+{
+
+}
+
+wxColour
+ContentTimelineAudioView::background_colour () const
+{
+       return AUDIO_CONTENT_COLOUR;
+}
+
+wxColour
+ContentTimelineAudioView::foreground_colour () const
+{
+       return wxColour (0, 0, 0, 255);
+}
+
+wxString
+ContentTimelineAudioView::label () const
+{
+       wxString s = TimelineContentView::label ();
+       shared_ptr<AudioContent> ac = content()->audio;
+       DCPOMATIC_ASSERT (ac);
+
+       if (ac->gain() > 0.01) {
+               s += wxString::Format (" +%.1fdB", ac->gain());
+       } else if (ac->gain() < -0.01) {
+               s += wxString::Format (" %.1fdB", ac->gain());
+       }
+
+       if (ac->delay() > 0) {
+               s += wxString::Format (_(" delayed by %dms"), ac->delay());
+       } else if (ac->delay() < 0) {
+               s += wxString::Format (_(" advanced by %dms"), -ac->delay());
+       }
+
+       list<int> mapped = ac->mapping().mapped_output_channels();
+       if (!mapped.empty ()) {
+               s += wxString::FromUTF8(" â†’ ");
+               for (auto i: mapped) {
+                       s += std_to_wx(short_audio_channel_name(i)) + ", ";
+               }
+               s = s.Left(s.Length() - 2);
+       }
+
+       return s;
+}
diff --git a/src/wx/content_timeline_audio_view.h b/src/wx/content_timeline_audio_view.h
new file mode 100644 (file)
index 0000000..719c253
--- /dev/null
@@ -0,0 +1,40 @@
+/*
+    Copyright (C) 2013-2015 Carl Hetherington <cth@carlh.net>
+
+    This file is part of DCP-o-matic.
+
+    DCP-o-matic is free software; you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or
+    (at your option) any later version.
+
+    DCP-o-matic is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with DCP-o-matic.  If not, see <http://www.gnu.org/licenses/>.
+
+*/
+
+
+#include "timeline_content_view.h"
+
+
+/** @class ContentTimelineAudioView
+ *  @brief Content timeline view for AudioContent.
+ */
+class ContentTimelineAudioView : public TimelineContentView
+{
+public:
+       ContentTimelineAudioView(ContentTimeline& tl, std::shared_ptr<Content> c);
+
+private:
+       bool active () const override {
+               return true;
+       }
+       wxColour background_colour () const override;
+       wxColour foreground_colour () const override;
+       wxString label () const override;
+};
diff --git a/src/wx/content_timeline_dialog.cc b/src/wx/content_timeline_dialog.cc
new file mode 100644 (file)
index 0000000..697e656
--- /dev/null
@@ -0,0 +1,153 @@
+/*
+    Copyright (C) 2013-2021 Carl Hetherington <cth@carlh.net>
+
+    This file is part of DCP-o-matic.
+
+    DCP-o-matic is free software; you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or
+    (at your option) any later version.
+
+    DCP-o-matic is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with DCP-o-matic.  If not, see <http://www.gnu.org/licenses/>.
+
+*/
+
+
+#include "content_panel.h"
+#include "content_timeline_dialog.h"
+#include "film_editor.h"
+#include "wx_util.h"
+#include "lib/compose.hpp"
+#include "lib/cross.h"
+#include "lib/film.h"
+#include "lib/playlist.h"
+#include <dcp/warnings.h>
+LIBDCP_DISABLE_WARNINGS
+#include <wx/graphics.h>
+LIBDCP_ENABLE_WARNINGS
+#include <list>
+
+
+using std::list;
+using std::shared_ptr;
+using std::string;
+using std::weak_ptr;
+#if BOOST_VERSION >= 106100
+using namespace boost::placeholders;
+#endif
+
+
+ContentTimelineDialog::ContentTimelineDialog(ContentPanel* cp, shared_ptr<Film> film, FilmViewer& viewer)
+       : wxDialog (
+               cp->window(),
+               wxID_ANY,
+               _("Timeline"),
+               wxDefaultPosition,
+               wxSize (640, 512),
+#ifdef DCPOMATIC_OSX
+               /* I can't get wxFRAME_FLOAT_ON_PARENT to work on OS X, and although wxSTAY_ON_TOP keeps
+                  the window above all others (and not just our own) it's better than nothing for now.
+               */
+               wxDEFAULT_DIALOG_STYLE | wxRESIZE_BORDER | wxFULL_REPAINT_ON_RESIZE | wxSTAY_ON_TOP
+#else
+               wxDEFAULT_DIALOG_STYLE | wxRESIZE_BORDER | wxFULL_REPAINT_ON_RESIZE | wxFRAME_FLOAT_ON_PARENT
+#endif
+               )
+       , _film (film)
+       , _timeline (this, cp, film, viewer)
+{
+       auto sizer = new wxBoxSizer (wxVERTICAL);
+
+       wxBitmap select(icon_path("select"), wxBITMAP_TYPE_PNG);
+       wxBitmap zoom(icon_path("zoom"), wxBITMAP_TYPE_PNG);
+       wxBitmap zoom_all(icon_path("zoom_all"), wxBITMAP_TYPE_PNG);
+       wxBitmap snap(icon_path("snap"), wxBITMAP_TYPE_PNG);
+       wxBitmap sequence(icon_path("sequence"), wxBITMAP_TYPE_PNG);
+
+       _toolbar = new wxToolBar (this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxTB_HORIZONTAL);
+       _toolbar->SetMargins (4, 4);
+       _toolbar->SetToolBitmapSize (wxSize(32, 32));
+       _toolbar->AddRadioTool(static_cast<int>(ContentTimeline::SELECT), _("Select"), select, wxNullBitmap, _("Select and move content"));
+       _toolbar->AddRadioTool(static_cast<int>(ContentTimeline::ZOOM), _("Zoom"), zoom, wxNullBitmap, _("Zoom in / out"));
+       _toolbar->AddTool(static_cast<int>(ContentTimeline::ZOOM_ALL), _("Zoom all"), zoom_all, _("Zoom out to whole film"));
+       _toolbar->AddCheckTool(static_cast<int>(ContentTimeline::SNAP), _("Snap"), snap, wxNullBitmap, _("Snap"));
+       _toolbar->AddCheckTool(static_cast<int>(ContentTimeline::SEQUENCE), _("Sequence"), sequence, wxNullBitmap, _("Keep video and subtitles in sequence"));
+       _toolbar->Realize ();
+
+       _toolbar->Bind(wxEVT_TOOL, bind(&ContentTimelineDialog::tool_clicked, this, _1));
+
+       sizer->Add (_toolbar, 0, wxALL, 12);
+       sizer->Add (&_timeline, 1, wxEXPAND | wxALL, 12);
+
+#ifdef DCPOMATIC_LINUX
+       auto buttons = CreateSeparatedButtonSizer (wxCLOSE);
+       if (buttons) {
+               sizer->Add (buttons, wxSizerFlags().Expand().DoubleBorder());
+       }
+#endif
+
+       SetSizer (sizer);
+       sizer->Layout ();
+       sizer->SetSizeHints (this);
+
+       Bind(wxEVT_CHAR_HOOK, boost::bind(&ContentTimelineDialog::keypress, this, _1));
+
+        _toolbar->ToggleTool(static_cast<int>(ContentTimeline::SNAP), _timeline.snap ());
+       film_change(ChangeType::DONE, FilmProperty::SEQUENCE);
+
+       _film_changed_connection = film->Change.connect(bind(&ContentTimelineDialog::film_change, this, _1, _2));
+}
+
+
+void
+ContentTimelineDialog::film_change(ChangeType type, FilmProperty p)
+{
+       if (type != ChangeType::DONE) {
+               return;
+       }
+
+       auto film = _film.lock ();
+       if (!film) {
+               return;
+       }
+
+       if (p == FilmProperty::SEQUENCE) {
+               _toolbar->ToggleTool(static_cast<int>(ContentTimeline::SEQUENCE), film->sequence());
+       }
+}
+
+
+void
+ContentTimelineDialog::set_selection(ContentList selection)
+{
+       _timeline.set_selection (selection);
+}
+
+
+void
+ContentTimelineDialog::tool_clicked(wxCommandEvent& ev)
+{
+       auto t = static_cast<ContentTimeline::Tool>(ev.GetId());
+       _timeline.tool_clicked (t);
+       if (t == ContentTimeline::SNAP) {
+               _timeline.set_snap (_toolbar->GetToolState(static_cast<int>(t)));
+       } else if (t == ContentTimeline::SEQUENCE) {
+               auto film = _film.lock ();
+               if (film) {
+                       film->set_sequence (_toolbar->GetToolState(static_cast<int>(t)));
+               }
+       }
+}
+
+
+void
+ContentTimelineDialog::keypress(wxKeyEvent const& event)
+{
+       _timeline.keypress(event);
+}
diff --git a/src/wx/content_timeline_dialog.h b/src/wx/content_timeline_dialog.h
new file mode 100644 (file)
index 0000000..2babf04
--- /dev/null
@@ -0,0 +1,48 @@
+/*
+    Copyright (C) 2013-2021 Carl Hetherington <cth@carlh.net>
+
+    This file is part of DCP-o-matic.
+
+    DCP-o-matic is free software; you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or
+    (at your option) any later version.
+
+    DCP-o-matic is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with DCP-o-matic.  If not, see <http://www.gnu.org/licenses/>.
+
+*/
+
+
+#include "content_timeline.h"
+#include <dcp/warnings.h>
+LIBDCP_DISABLE_WARNINGS
+#include <wx/wx.h>
+LIBDCP_ENABLE_WARNINGS
+
+
+class Playlist;
+
+
+class ContentTimelineDialog : public wxDialog
+{
+public:
+       ContentTimelineDialog(ContentPanel *, std::shared_ptr<Film>, FilmViewer& viewer);
+
+       void set_selection (ContentList selection);
+
+private:
+       void film_change(ChangeType type, FilmProperty);
+       void tool_clicked (wxCommandEvent& id);
+       void keypress(wxKeyEvent const& event);
+
+       std::weak_ptr<Film> _film;
+       ContentTimeline _timeline;
+       wxToolBar* _toolbar;
+       boost::signals2::scoped_connection _film_changed_connection;
+};
diff --git a/src/wx/content_timeline_text_view.cc b/src/wx/content_timeline_text_view.cc
new file mode 100644 (file)
index 0000000..f22f478
--- /dev/null
@@ -0,0 +1,62 @@
+/*
+    Copyright (C) 2013-2018 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 "colours.h"
+#include "content_timeline_text_view.h"
+#include "lib/text_content.h"
+#include "lib/content.h"
+
+
+using std::shared_ptr;
+
+
+ContentTimelineTextView::ContentTimelineTextView(ContentTimeline& tl, shared_ptr<Content> c, shared_ptr<TextContent> caption)
+       : TimelineContentView (tl, c)
+       , _caption (caption)
+{
+
+}
+
+wxColour
+ContentTimelineTextView::background_colour() const
+{
+       if (!active ()) {
+               return wxColour (210, 210, 210, 128);
+       }
+
+       return TEXT_CONTENT_COLOUR;
+}
+
+wxColour
+ContentTimelineTextView::foreground_colour() const
+{
+       if (!active ()) {
+               return wxColour (180, 180, 180, 128);
+       }
+
+       return wxColour (0, 0, 0, 255);
+}
+
+bool
+ContentTimelineTextView::active() const
+{
+       return _caption->use();
+}
diff --git a/src/wx/content_timeline_text_view.h b/src/wx/content_timeline_text_view.h
new file mode 100644 (file)
index 0000000..8d37ad4
--- /dev/null
@@ -0,0 +1,42 @@
+/*
+    Copyright (C) 2013-2016 Carl Hetherington <cth@carlh.net>
+
+    This file is part of DCP-o-matic.
+
+    DCP-o-matic is free software; you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or
+    (at your option) any later version.
+
+    DCP-o-matic is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with DCP-o-matic.  If not, see <http://www.gnu.org/licenses/>.
+
+*/
+
+
+#include "timeline_content_view.h"
+
+
+class TextContent;
+
+
+/** @class ContentTimelineTextView
+ *  @brief Content timeline view for TextContent.
+ */
+class ContentTimelineTextView : public TimelineContentView
+{
+public:
+       ContentTimelineTextView(ContentTimeline& tl, std::shared_ptr<Content>, std::shared_ptr<TextContent>);
+
+private:
+       bool active () const override;
+       wxColour background_colour () const override;
+       wxColour foreground_colour () const override;
+
+       std::shared_ptr<TextContent> _caption;
+};
diff --git a/src/wx/content_timeline_video_view.cc b/src/wx/content_timeline_video_view.cc
new file mode 100644 (file)
index 0000000..d907627
--- /dev/null
@@ -0,0 +1,64 @@
+/*
+    Copyright (C) 2013-2019 Carl Hetherington <cth@carlh.net>
+
+    This file is part of DCP-o-matic.
+
+    DCP-o-matic is free software; you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or
+    (at your option) any later version.
+
+    DCP-o-matic is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with DCP-o-matic.  If not, see <http://www.gnu.org/licenses/>.
+
+*/
+
+
+#include "colours.h"
+#include "content_timeline_video_view.h"
+#include "lib/image_content.h"
+#include "lib/video_content.h"
+
+
+using std::dynamic_pointer_cast;
+using std::shared_ptr;
+
+
+ContentTimelineVideoView::ContentTimelineVideoView(ContentTimeline& tl, shared_ptr<Content> c)
+       : TimelineContentView (tl, c)
+{
+
+}
+
+wxColour
+ContentTimelineVideoView::background_colour() const
+{
+       if (!active()) {
+               return wxColour (210, 210, 210, 128);
+       }
+
+       return VIDEO_CONTENT_COLOUR;
+}
+
+wxColour
+ContentTimelineVideoView::foreground_colour() const
+{
+       if (!active()) {
+               return wxColour (180, 180, 180, 128);
+       }
+
+       return wxColour (0, 0, 0, 255);
+}
+
+bool
+ContentTimelineVideoView::active() const
+{
+       auto c = _content.lock();
+       DCPOMATIC_ASSERT (c);
+       return c->video && c->video->use();
+}
diff --git a/src/wx/content_timeline_video_view.h b/src/wx/content_timeline_video_view.h
new file mode 100644 (file)
index 0000000..940a1a3
--- /dev/null
@@ -0,0 +1,37 @@
+/*
+    Copyright (C) 2013-2015 Carl Hetherington <cth@carlh.net>
+
+    This file is part of DCP-o-matic.
+
+    DCP-o-matic is free software; you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or
+    (at your option) any later version.
+
+    DCP-o-matic is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with DCP-o-matic.  If not, see <http://www.gnu.org/licenses/>.
+
+*/
+
+
+#include "timeline_content_view.h"
+
+
+/** @class ContentTimelineVideoView
+ *  @brief Content timeline view for VideoContent.
+ */
+class ContentTimelineVideoView : public TimelineContentView
+{
+public:
+       ContentTimelineVideoView(ContentTimeline& tl, std::shared_ptr<Content> c);
+
+private:
+       bool active () const override;
+       wxColour background_colour () const override;
+       wxColour foreground_colour () const override;
+};
diff --git a/src/wx/content_timeline_view.cc b/src/wx/content_timeline_view.cc
new file mode 100644 (file)
index 0000000..127e440
--- /dev/null
@@ -0,0 +1,42 @@
+/*
+    Copyright (C) 2013-2021 Carl Hetherington <cth@carlh.net>
+
+    This file is part of DCP-o-matic.
+
+    DCP-o-matic is free software; you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or
+    (at your option) any later version.
+
+    DCP-o-matic is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with DCP-o-matic.  If not, see <http://www.gnu.org/licenses/>.
+
+*/
+
+
+#include "content_timeline.h"
+#include "content_timeline_view.h"
+
+
+using std::list;
+using namespace dcpomatic;
+
+
+ContentTimelineView::ContentTimelineView(ContentTimeline& timeline)
+       : TimelineView(timeline)
+{
+
+}
+
+int
+ContentTimelineView::y_pos(int t) const
+{
+       return t * _timeline.pixels_per_track() + _timeline.tracks_y_offset();
+}
+
+
diff --git a/src/wx/content_timeline_view.h b/src/wx/content_timeline_view.h
new file mode 100644 (file)
index 0000000..450d19d
--- /dev/null
@@ -0,0 +1,60 @@
+/*
+    Copyright (C) 2013-2021 Carl Hetherington <cth@carlh.net>
+
+    This file is part of DCP-o-matic.
+
+    DCP-o-matic is free software; you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or
+    (at your option) any later version.
+
+    DCP-o-matic is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with DCP-o-matic.  If not, see <http://www.gnu.org/licenses/>.
+
+*/
+
+
+#ifndef DCPOMATIC_CONTENT_TIMELINE_VIEW_H
+#define DCPOMATIC_CONTENT_TIMELINE_VIEW_H
+
+
+#include "timeline_view.h"
+#include "lib/rect.h"
+#include "lib/dcpomatic_time.h"
+
+
+class wxGraphicsContext;
+class ContentTimeline;
+
+
+/** @class ContentTimelineView
+ *  @brief Parent class for components of the content timeline (e.g. a piece of content or an axis).
+ */
+class ContentTimelineView : public TimelineView<ContentTimeline>
+{
+public:
+       explicit ContentTimelineView(ContentTimeline& t);
+       virtual ~ContentTimelineView () = default;
+
+       void paint(wxGraphicsContext* gc, std::list<dcpomatic::Rect<int>> overlaps)
+       {
+               _last_paint_bbox = bbox();
+               do_paint(gc, overlaps);
+       }
+
+protected:
+       virtual void do_paint(wxGraphicsContext *, std::list<dcpomatic::Rect<int>> overlaps) = 0;
+
+       int y_pos(int t) const;
+};
+
+
+typedef std::vector<std::shared_ptr<ContentTimelineView>> ContentTimelineViewList;
+
+
+#endif
index f4ba74cdefbbcefd433379a26698af0bac33f9c5..64b4935cc784459e732a04d4f73d214d033077f2 100644 (file)
@@ -23,6 +23,7 @@
 #include "check_box.h"
 #include "check_box.h"
 #include "dcp_panel.h"
+#include "dcp_timeline_dialog.h"
 #include "dcpomatic_button.h"
 #include "dcpomatic_choice.h"
 #include "dcpomatic_spin_ctrl.h"
@@ -97,10 +98,6 @@ DCPPanel::DCPPanel(wxNotebook* n, shared_ptr<Film> film, FilmViewer& viewer)
                wxALIGN_CENTRE_HORIZONTAL | wxST_NO_AUTORESIZE | wxST_ELLIPSIZE_MIDDLE
                );
 
-       _enable_audio_language = new CheckBox(_panel, _("Audio language"));
-       _audio_language = new wxStaticText (_panel, wxID_ANY, wxT(""));
-       _edit_audio_language = new Button (_panel, _("Edit..."));
-
        _dcp_content_type_label = create_label (_panel, _("Content Type"), true);
        _dcp_content_type = new Choice(_panel);
 
@@ -110,18 +107,12 @@ DCPPanel::DCPPanel(wxNotebook* n, shared_ptr<Film> film, FilmViewer& viewer)
         auto size = dc.GetTextExtent (wxT ("GGGGGGGG..."));
         size.SetHeight (-1);
 
-       _reels_label = create_label (_panel, _("Reels"), true);
-       _reel_type = new Choice(_panel);
-
-       _reel_length_label = create_label (_panel, _("Reel length"), true);
-       _reel_length = new SpinCtrl (_panel, DCPOMATIC_SPIN_CTRL_WIDTH);
-       _reel_length_gb_label = create_label (_panel, _("GB"), false);
-
        _standard_label = create_label (_panel, _("Standard"), true);
        _standard = new Choice(_panel);
 
        _markers = new Button (_panel, _("Markers..."));
        _metadata = new Button (_panel, _("Metadata..."));
+       _reels = new Button(_panel, _("Reels..."));
 
        _notebook = new wxNotebook (_panel, wxID_ANY);
        _sizer->Add (_notebook, 1, wxEXPAND | wxTOP, 6);
@@ -134,28 +125,17 @@ DCPPanel::DCPPanel(wxNotebook* n, shared_ptr<Film> film, FilmViewer& viewer)
        _copy_isdcf_name_button->Bind(wxEVT_BUTTON,   boost::bind(&DCPPanel::copy_isdcf_name_button_clicked, this));
        _dcp_content_type->Bind      (wxEVT_CHOICE,   boost::bind(&DCPPanel::dcp_content_type_changed, this));
        _encrypted->bind(&DCPPanel::encrypted_toggled, this);
-       _reel_type->Bind             (wxEVT_CHOICE,   boost::bind(&DCPPanel::reel_type_changed, this));
-       _reel_length->Bind           (wxEVT_SPINCTRL, boost::bind(&DCPPanel::reel_length_changed, this));
        _standard->Bind              (wxEVT_CHOICE,   boost::bind(&DCPPanel::standard_changed, this));
        _markers->Bind               (wxEVT_BUTTON,   boost::bind(&DCPPanel::markers_clicked, this));
        _metadata->Bind              (wxEVT_BUTTON,   boost::bind(&DCPPanel::metadata_clicked, this));
-       _enable_audio_language->bind(&DCPPanel::enable_audio_language_toggled, this);
-       _edit_audio_language->Bind   (wxEVT_BUTTON,   boost::bind(&DCPPanel::edit_audio_language_clicked, this));
+       _reels->Bind(wxEVT_BUTTON, boost::bind(&DCPPanel::reels_clicked, this));
 
        for (auto i: DCPContentType::all()) {
                _dcp_content_type->add(i->pretty_name());
        }
 
-       _reel_type->add(_("Single reel"));
-       _reel_type->add(_("Split by video content"));
-       /// TRANSLATORS: translate the word "Custom" here; do not include the "Reel|" prefix
-       _reel_type->add(S_("Reel|Custom"));
-       _reel_type->SetToolTip(_("How the DCP should be split into parts internally.  If in doubt, choose 'Single reel'"));
-
-       _reel_length->SetRange (1, 64);
-
        add_standards();
-       _standard->SetToolTip(_("Which standard the DCP should use.  Interop is older and SMPTE is the modern standard.  If in doubt, choose 'SMPTE'"));
+       _standard->SetToolTip(_("The standard that the DCP should use.  Interop is older, and SMPTE is the newer (current) standard.  If in doubt, choose 'SMPTE'"));
 
        Config::instance()->Changed.connect (boost::bind(&DCPPanel::config_changed, this, _1));
 
@@ -241,15 +221,6 @@ DCPPanel::add_to_grid ()
        _grid->Add (_dcp_name, wxGBPosition(r, 0), wxGBSpan(1, 2), wxALIGN_CENTER_VERTICAL | wxEXPAND);
        ++r;
 
-       {
-               auto s = new wxBoxSizer (wxHORIZONTAL);
-               s->Add (_enable_audio_language, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, DCPOMATIC_SIZER_GAP);
-               s->Add (_audio_language, 1, wxALIGN_CENTER_VERTICAL | wxBOTTOM, DCPOMATIC_CHECKBOX_BOTTOM_PAD);
-               s->Add (_edit_audio_language, 0, wxALIGN_CENTER_VERTICAL | wxBOTTOM, DCPOMATIC_CHECKBOX_BOTTOM_PAD);
-               _grid->Add (s, wxGBPosition(r, 0), wxGBSpan(1, 2), wxEXPAND | wxALIGN_CENTER_VERTICAL);
-       }
-       ++r;
-
        add_label_to_sizer (_grid, _dcp_content_type_label, true, wxGBPosition(r, 0));
        _grid->Add (_dcp_content_type, wxGBPosition(r, 1));
        ++r;
@@ -257,19 +228,6 @@ DCPPanel::add_to_grid ()
        _grid->Add (_encrypted, wxGBPosition(r, 0), wxGBSpan(1, 2));
        ++r;
 
-       add_label_to_sizer (_grid, _reels_label, true, wxGBPosition(r, 0));
-       _grid->Add (_reel_type, wxGBPosition(r, 1), wxDefaultSpan, wxALIGN_CENTER_VERTICAL);
-       ++r;
-
-       add_label_to_sizer (_grid, _reel_length_label, true, wxGBPosition(r, 0));
-       {
-               auto s = new wxBoxSizer (wxHORIZONTAL);
-               s->Add (_reel_length);
-               add_label_to_sizer (s, _reel_length_gb_label, false, 0, wxLEFT | wxALIGN_CENTER_VERTICAL);
-               _grid->Add (s, wxGBPosition(r, 1));
-       }
-       ++r;
-
        add_label_to_sizer (_grid, _standard_label, true, wxGBPosition(r, 0));
        _grid->Add (_standard, wxGBPosition(r, 1), wxDefaultSpan, wxALIGN_CENTER_VERTICAL);
        ++r;
@@ -277,6 +235,7 @@ DCPPanel::add_to_grid ()
        auto extra = new wxBoxSizer (wxHORIZONTAL);
        extra->Add (_markers, 1, wxRIGHT, DCPOMATIC_SIZER_X_GAP);
        extra->Add (_metadata, 1, wxRIGHT, DCPOMATIC_SIZER_X_GAP);
+       extra->Add(_reels, 1, wxRIGHT, DCPOMATIC_SIZER_X_GAP);
        _grid->Add (extra, wxGBPosition(r, 0), wxGBSpan(1, 2));
        ++r;
 }
@@ -389,6 +348,14 @@ DCPPanel::metadata_clicked ()
 }
 
 
+void
+DCPPanel::reels_clicked()
+{
+       _dcp_timeline.reset(_panel, _film);
+       _dcp_timeline->Show();
+}
+
+
 void
 DCPPanel::film_changed(FilmProperty p)
 {
@@ -490,13 +457,6 @@ DCPPanel::film_changed(FilmProperty p)
                setup_audio_channels_choice (_audio_channels, minimum_allowed_audio_channels());
                film_changed (FilmProperty::AUDIO_CHANNELS);
                break;
-       case FilmProperty::REEL_TYPE:
-               checked_set (_reel_type, static_cast<int>(_film->reel_type()));
-               _reel_length->Enable (_film->reel_type() == ReelType::BY_LENGTH);
-               break;
-       case FilmProperty::REEL_LENGTH:
-               checked_set (_reel_length, _film->reel_length() / 1000000000LL);
-               break;
        case FilmProperty::CONTENT:
                setup_dcp_name ();
                setup_sensitivity ();
@@ -511,6 +471,7 @@ DCPPanel::film_changed(FilmProperty p)
                checked_set (_audio_language, al ? std_to_wx(al->to_string()) : wxT(""));
                setup_dcp_name ();
                setup_sensitivity ();
+               _audio_panel_sizer->Layout();
                break;
        }
        case FilmProperty::AUDIO_FRAME_RATE:
@@ -681,8 +642,6 @@ DCPPanel::setup_sensitivity ()
        _audio_language->Enable         (_enable_audio_language->GetValue());
        _edit_audio_language->Enable    (_enable_audio_language->GetValue());
        _encrypted->Enable              (_generally_sensitive);
-       _reel_type->Enable              (_generally_sensitive && _film && !_film->references_dcp_video() && !_film->references_dcp_audio());
-       _reel_length->Enable            (_generally_sensitive && _film && _film->reel_type() == ReelType::BY_LENGTH);
        _markers->Enable                (_generally_sensitive && _film && !_film->interop());
        _metadata->Enable               (_generally_sensitive);
        _frame_rate_choice->Enable      (_generally_sensitive && _film && !_film->references_dcp_video() && !_film->contains_atmos_content());
@@ -953,6 +912,10 @@ DCPPanel::make_audio_panel ()
        _audio_processor = new Choice(panel);
        add_audio_processors ();
 
+       _enable_audio_language = new CheckBox(panel, _("Language"));
+       _audio_language = new wxStaticText(panel, wxID_ANY, wxT(""));
+       _edit_audio_language = new Button(panel, _("Edit..."));
+
        _show_audio = new Button (panel, _("Show graph of audio levels..."));
 
        _audio_channels->Bind (wxEVT_CHOICE, boost::bind (&DCPPanel::audio_channels_changed, this));
@@ -960,6 +923,10 @@ DCPPanel::make_audio_panel ()
                _audio_sample_rate->Bind (wxEVT_CHOICE, boost::bind(&DCPPanel::audio_sample_rate_changed, this));
        }
        _audio_processor->Bind (wxEVT_CHOICE, boost::bind (&DCPPanel::audio_processor_changed, this));
+
+       _enable_audio_language->bind(&DCPPanel::enable_audio_language_toggled, this);
+       _edit_audio_language->Bind(wxEVT_BUTTON, boost::bind(&DCPPanel::edit_audio_language_clicked, this));
+
        _show_audio->Bind (wxEVT_BUTTON, boost::bind (&DCPPanel::show_audio_clicked, this));
 
        if (_audio_sample_rate) {
@@ -992,6 +959,16 @@ DCPPanel::add_audio_panel_to_grid ()
        _audio_grid->Add (_audio_processor, wxGBPosition (r, 1));
        ++r;
 
+       {
+               auto s = new wxBoxSizer (wxHORIZONTAL);
+               s->Add(_enable_audio_language, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, DCPOMATIC_SIZER_GAP);
+               s->Add(_audio_language, 1, wxALIGN_CENTER_VERTICAL | wxBOTTOM, DCPOMATIC_CHECKBOX_BOTTOM_PAD);
+               s->Add(DCPOMATIC_SIZER_X_GAP, 0);
+               s->Add(_edit_audio_language, 0, wxALIGN_CENTER_VERTICAL | wxBOTTOM, DCPOMATIC_CHECKBOX_BOTTOM_PAD);
+               _audio_grid->Add(s, wxGBPosition(r, 0), wxGBSpan(1, 2), wxEXPAND | wxALIGN_CENTER_VERTICAL);
+       }
+       ++r;
+
        _audio_grid->Add (_show_audio, wxGBPosition (r, 0), wxGBSpan (1, 2));
        ++r;
 }
@@ -1029,28 +1006,6 @@ DCPPanel::show_audio_clicked ()
 }
 
 
-void
-DCPPanel::reel_type_changed ()
-{
-       if (!_film || !_reel_type->get()) {
-               return;
-       }
-
-       _film->set_reel_type(static_cast<ReelType>(*_reel_type->get()));
-}
-
-
-void
-DCPPanel::reel_length_changed ()
-{
-       if (!_film) {
-               return;
-       }
-
-       _film->set_reel_length (_reel_length->GetValue() * 1000000000LL);
-}
-
-
 void
 DCPPanel::add_audio_processors ()
 {
index 849fe185cf836bc588428c9308dd928db014226b..6c97a41c31c5bea31783616e163e0c6532a1ed75 100644 (file)
@@ -39,6 +39,7 @@ class wxGridBagSizer;
 
 class AudioDialog;
 class Choice;
+class DCPTimelineDialog;
 class Film;
 class FilmViewer;
 class InteropMetadataDialog;
@@ -82,10 +83,9 @@ private:
        void encrypted_toggled ();
        void audio_processor_changed ();
        void show_audio_clicked ();
-       void reel_type_changed ();
-       void reel_length_changed ();
        void markers_clicked ();
        void metadata_clicked ();
+       void reels_clicked();
        void reencode_j2k_changed ();
        void enable_audio_language_toggled ();
        void edit_audio_language_clicked ();
@@ -153,19 +153,16 @@ private:
        wxStaticText* _standard_label;
        Choice* _standard;
        CheckBox* _encrypted;
-       wxStaticText* _reels_label;
-       Choice* _reel_type;
-       wxStaticText* _reel_length_label;
-       wxStaticText* _reel_length_gb_label;
-       wxSpinCtrl* _reel_length;
        wxButton* _markers;
        wxButton* _metadata;
+       Button* _reels;
        wxSizer* _audio_panel_sizer;
 
        wx_ptr<AudioDialog> _audio_dialog;
        wx_ptr<MarkersDialog> _markers_dialog;
        wx_ptr<InteropMetadataDialog> _interop_metadata_dialog;
        wx_ptr<SMPTEMetadataDialog> _smpte_metadata_dialog;
+       wx_ptr<DCPTimelineDialog> _dcp_timeline;
 
        std::shared_ptr<Film> _film;
        FilmViewer& _viewer;
diff --git a/src/wx/dcp_referencing_dialog.cc b/src/wx/dcp_referencing_dialog.cc
new file mode 100644 (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;
+};
+
diff --git a/src/wx/dcp_timeline.cc b/src/wx/dcp_timeline.cc
new file mode 100644 (file)
index 0000000..119d07b
--- /dev/null
@@ -0,0 +1,615 @@
+/*
+    Copyright (C) 2023 Carl Hetherington <cth@carlh.net>
+
+    This file is part of DCP-o-matic.
+
+    DCP-o-matic is free software; you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or
+    (at your option) any later version.
+
+    DCP-o-matic is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with DCP-o-matic.  If not, see <http://www.gnu.org/licenses/>.
+
+*/
+
+
+#include "check_box.h"
+#include "colours.h"
+#include "dcp_timeline.h"
+#include "dcp_timeline_reel_marker_view.h"
+#include "dcpomatic_choice.h"
+#include "dcpomatic_spin_ctrl.h"
+#include "timecode.h"
+#include "wx_util.h"
+#include "lib/atmos_content.h"
+#include "lib/audio_content.h"
+#include "lib/constants.h"
+#include "lib/film.h"
+#include "lib/text_content.h"
+#include "lib/video_content.h"
+#include <dcp/scope_guard.h>
+LIBDCP_DISABLE_WARNINGS
+#include <wx/graphics.h>
+LIBDCP_ENABLE_WARNINGS
+
+
+using std::dynamic_pointer_cast;
+using std::make_shared;
+using std::shared_ptr;
+using std::vector;
+using boost::optional;
+#if BOOST_VERSION >= 106100
+using namespace boost::placeholders;
+#endif
+using namespace dcpomatic;
+
+
+auto constexpr reel_marker_y_pos = 48;
+auto constexpr content_y_pos = 112;
+auto constexpr content_type_height = 12;
+
+enum {
+       ID_add_reel_boundary,
+};
+
+
+class ReelBoundary
+{
+public:
+       ReelBoundary(wxWindow* parent, wxGridBagSizer* sizer, int index, DCPTime maximum, int fps, DCPTimeline& timeline, bool editable)
+               : _label(new wxStaticText(parent, wxID_ANY, wxString::Format(_("Reel %d to reel %d"), index + 1, index + 2)))
+               , _timecode(new Timecode<DCPTime>(parent, true))
+               , _index(index)
+               , _view(timeline, reel_marker_y_pos)
+               , _fps(fps)
+       {
+               sizer->Add(_label, wxGBPosition(index, 0), wxDefaultSpan, wxALIGN_CENTER_VERTICAL);
+               sizer->Add(_timecode, wxGBPosition(index, 1));
+
+               _timecode->set_maximum(maximum.split(fps));
+               _timecode->set_editable(editable);
+               _timecode->Changed.connect(boost::bind(&ReelBoundary::timecode_changed, this));
+       }
+
+       ~ReelBoundary()
+       {
+               if (_label) {
+                       _label->Destroy();
+               }
+
+               if (_timecode) {
+                       _timecode->Destroy();
+               }
+       }
+
+       ReelBoundary(ReelBoundary const&) = delete;
+       ReelBoundary& operator=(ReelBoundary const&) = delete;
+
+       ReelBoundary(ReelBoundary&& other) = delete;
+       ReelBoundary& operator=(ReelBoundary&& other) = delete;
+
+       void set_time(DCPTime time)
+       {
+               if (_timecode) {
+                       _timecode->set(time, _fps);
+               }
+               _view.set_time(time);
+       }
+
+       dcpomatic::DCPTime time() const {
+               return _view.time();
+       }
+
+       int index() const {
+               return _index;
+       }
+
+       DCPTimelineReelMarkerView& view() {
+               return _view;
+       }
+
+       DCPTimelineReelMarkerView const& view() const {
+               return _view;
+       }
+
+       boost::signals2::signal<void (int, dcpomatic::DCPTime)> Changed;
+
+private:
+       void timecode_changed() {
+               set_time(_timecode->get(_fps));
+               Changed(_index, time());
+       }
+
+       wxStaticText* _label = nullptr;
+       Timecode<dcpomatic::DCPTime>* _timecode = nullptr;
+       int _index = 0;
+       DCPTimelineReelMarkerView _view;
+       int _fps;
+};
+
+
+DCPTimeline::DCPTimeline(wxWindow* parent, shared_ptr<Film> film)
+       : Timeline(parent)
+       , _film(film)
+       , _canvas(new wxScrolledCanvas(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxFULL_REPAINT_ON_RESIZE))
+       , _reel_settings(new wxPanel(this, wxID_ANY))
+       , _reel_detail(new wxPanel(this, wxID_ANY))
+       , _reel_detail_sizer(new wxGridBagSizer(DCPOMATIC_SIZER_X_GAP, DCPOMATIC_SIZER_Y_GAP))
+{
+#ifndef __WXOSX__
+       _canvas->SetDoubleBuffered(true);
+#endif
+       _reel_detail->SetSizer(_reel_detail_sizer);
+
+       auto sizer = new wxBoxSizer(wxVERTICAL);
+       sizer->Add(_reel_settings, 0);
+       sizer->Add(_canvas, 0, wxEXPAND);
+       sizer->Add(_reel_detail, 1, wxEXPAND | wxALL, DCPOMATIC_DIALOG_BORDER);
+       SetSizer(sizer);
+
+       SetMinSize(wxSize(640, 480));
+       _canvas->SetMinSize({-1, content_y_pos + content_type_height * 4});
+
+       _canvas->Bind(wxEVT_PAINT, boost::bind(&DCPTimeline::paint, this));
+       _canvas->Bind(wxEVT_SIZE, boost::bind(&DCPTimeline::setup_pixels_per_second, this));
+       _canvas->Bind(wxEVT_LEFT_DOWN, boost::bind(&DCPTimeline::left_down, this, _1));
+       _canvas->Bind(wxEVT_RIGHT_DOWN, boost::bind(&DCPTimeline::right_down, this, _1));
+       _canvas->Bind(wxEVT_LEFT_UP, boost::bind(&DCPTimeline::left_up, this, _1));
+       _canvas->Bind(wxEVT_MOTION, boost::bind(&DCPTimeline::mouse_moved, this, _1));
+
+       _film_connection = film->Change.connect(boost::bind(&DCPTimeline::film_changed, this, _1, _2));
+
+       _menu = new wxMenu;
+       _add_reel_boundary = _menu->Append(ID_add_reel_boundary, _("Add reel"));
+       _canvas->Bind(wxEVT_MENU, boost::bind(&DCPTimeline::add_reel_boundary, this));
+
+       setup_reel_settings();
+       setup_reel_boundaries();
+
+       sizer->Layout();
+       setup_pixels_per_second();
+       setup_sensitivity();
+}
+
+
+void
+DCPTimeline::add_reel_boundary()
+{
+       auto boundaries = film()->custom_reel_boundaries();
+       boundaries.push_back(DCPTime::from_seconds(_right_down_position.x / _pixels_per_second.get_value_or(1)));
+       film()->set_custom_reel_boundaries(boundaries);
+}
+
+
+void
+DCPTimeline::film_changed(ChangeType type, FilmProperty property)
+{
+       if (type != ChangeType::DONE) {
+               return;
+       }
+
+       switch (property) {
+       case FilmProperty::REEL_TYPE:
+       case FilmProperty::REEL_LENGTH:
+       case FilmProperty::CUSTOM_REEL_BOUNDARIES:
+               setup_sensitivity();
+               setup_reel_boundaries();
+               break;
+       case FilmProperty::CONTENT:
+       case FilmProperty::CONTENT_ORDER:
+               setup_pixels_per_second();
+               Refresh();
+               break;
+       default:
+               break;
+       }
+}
+
+
+void
+DCPTimeline::setup_sensitivity()
+{
+       _snap->Enable(editable());
+       _maximum_reel_size->Enable(film()->reel_type() == ReelType::BY_LENGTH);
+}
+
+
+void
+DCPTimeline::setup_reel_settings()
+{
+       auto sizer = new wxGridBagSizer(DCPOMATIC_SIZER_X_GAP, DCPOMATIC_SIZER_Y_GAP);
+       _reel_settings->SetSizer(sizer);
+
+       int r = 0;
+       add_label_to_sizer(sizer, _reel_settings, _("Reel mode"), true, wxGBPosition(r, 0));
+       _reel_type = new Choice(_reel_settings);
+       _reel_type->add(_("Single reel"));
+       _reel_type->add(_("Split by video content"));
+       _reel_type->add(_("Split by maximum reel size"));
+       _reel_type->add(_("Custom"));
+       sizer->Add(_reel_type, wxGBPosition(r, 1));
+       ++r;
+
+       add_label_to_sizer(sizer, _reel_settings, _("Maximum reel size"), true, wxGBPosition(r, 0));
+       _maximum_reel_size = new SpinCtrl(_reel_settings, DCPOMATIC_SPIN_CTRL_WIDTH);
+       _maximum_reel_size->SetRange(1, 1000);
+       {
+               auto s = new wxBoxSizer(wxHORIZONTAL);
+               s->Add(_maximum_reel_size, 0);
+               add_label_to_sizer(s, _reel_settings, _("GB"), false, 0, wxALIGN_CENTER_VERTICAL | wxLEFT);
+               sizer->Add(s, wxGBPosition(r, 1));
+       }
+       ++r;
+
+       _snap = new CheckBox(_reel_settings, _("Snap when dragging"));
+       sizer->Add(_snap, wxGBPosition(r, 1));
+       ++r;
+
+       _reel_type->set(static_cast<int>(film()->reel_type()));
+       _maximum_reel_size->SetValue(film()->reel_length() / 1000000000LL);
+
+       _reel_type->bind(&DCPTimeline::reel_mode_changed, this);
+       _maximum_reel_size->Bind(wxEVT_SPINCTRL, boost::bind(&DCPTimeline::maximum_reel_size_changed, this));
+}
+
+
+void
+DCPTimeline::reel_mode_changed()
+{
+       film()->set_reel_type(static_cast<ReelType>(*_reel_type->get()));
+}
+
+
+void
+DCPTimeline::maximum_reel_size_changed()
+{
+       film()->set_reel_length(_maximum_reel_size->GetValue() * 1000000000LL);
+}
+
+
+void
+DCPTimeline::set_reel_boundary(int index, DCPTime time)
+{
+       auto boundaries = film()->custom_reel_boundaries();
+       DCPOMATIC_ASSERT(index >= 0 && index < static_cast<int>(boundaries.size()));
+       boundaries[index] = time.round(film()->video_frame_rate());
+       film()->set_custom_reel_boundaries(boundaries);
+}
+
+
+void
+DCPTimeline::setup_reel_boundaries()
+{
+       auto const reels = film()->reels();
+       if (reels.empty()) {
+               _reel_boundaries.clear();
+               return;
+       }
+
+       size_t const boundaries = reels.size() - 1;
+       auto const maximum = film()->length();
+       for (size_t i = _reel_boundaries.size(); i < boundaries; ++i) {
+               auto boundary = std::make_shared<ReelBoundary>(
+                               _reel_detail, _reel_detail_sizer, i, maximum, film()->video_frame_rate(), *this, editable()
+                               );
+
+               boundary->Changed.connect(boost::bind(&DCPTimeline::set_reel_boundary, this, _1, _2));
+               _reel_boundaries.push_back(boundary);
+       }
+
+       _reel_boundaries.resize(boundaries);
+
+       auto const active = editable();
+       for (size_t i = 0; i < boundaries; ++i) {
+               _reel_boundaries[i]->set_time(reels[i].to);
+               _reel_boundaries[i]->view().set_active(active);
+       }
+
+       _reel_detail_sizer->Layout();
+       _canvas->Refresh();
+}
+
+
+void
+DCPTimeline::paint()
+{
+       wxPaintDC dc(_canvas);
+       dc.Clear();
+
+       if (film()->content().empty()) {
+               return;
+       }
+
+       _canvas->DoPrepareDC(dc);
+
+       auto gc = wxGraphicsContext::Create(dc);
+       if (!gc) {
+               return;
+       }
+
+       dcp::ScopeGuard sg = [gc]() { delete gc; };
+
+       gc->SetAntialiasMode(wxANTIALIAS_DEFAULT);
+
+       paint_reels(gc);
+       paint_content(gc);
+}
+
+
+void
+DCPTimeline::paint_reels(wxGraphicsContext* gc)
+{
+       constexpr int x_offset = 2;
+
+       for (auto const& boundary: _reel_boundaries) {
+               boundary->view().paint(gc);
+       }
+
+       gc->SetFont(gc->CreateFont(*wxNORMAL_FONT, wxColour(0, 0, 0)));
+       gc->SetPen(*wxThePenList->FindOrCreatePen(wxColour(0, 0, 0), 2, wxPENSTYLE_SOLID));
+
+       auto const pps = pixels_per_second().get_value_or(1);
+
+       auto start = gc->CreatePath();
+       start.MoveToPoint(x_offset, reel_marker_y_pos);
+       start.AddLineToPoint(x_offset, reel_marker_y_pos + DCPTimelineReelMarkerView::HEIGHT);
+       gc->StrokePath(start);
+
+       auto const length = film()->length().seconds() * pps;
+       auto end = gc->CreatePath();
+       end.MoveToPoint(x_offset + length, reel_marker_y_pos);
+       end.AddLineToPoint(x_offset + length, reel_marker_y_pos + DCPTimelineReelMarkerView::HEIGHT);
+       gc->StrokePath(end);
+
+       auto const y = reel_marker_y_pos + DCPTimelineReelMarkerView::HEIGHT * 3 / 4;
+
+       auto paint_reel = [gc](double from, double to, int index) {
+               auto path = gc->CreatePath();
+               path.MoveToPoint(from, y);
+               path.AddLineToPoint(to, y);
+               gc->StrokePath(path);
+
+               auto str = wxString::Format(wxT("#%d"), index + 1);
+               wxDouble str_width;
+               wxDouble str_height;
+               wxDouble str_descent;
+               wxDouble str_leading;
+               gc->GetTextExtent(str, &str_width, &str_height, &str_descent, &str_leading);
+
+               if (str_width < (to - from)) {
+                       gc->DrawText(str, (from + to - str_width) / 2, y - str_height - 2);
+               }
+       };
+
+       gc->SetPen(*wxThePenList->FindOrCreatePen(wxColour(0, 0, 255), 2, wxPENSTYLE_DOT));
+       int index = 0;
+       DCPTime last;
+       for (auto const& boundary: _reel_boundaries) {
+               paint_reel(last.seconds() * pps + 2, boundary->time().seconds() * pps, index++);
+               last = boundary->time();
+       }
+
+       paint_reel(last.seconds() * pps + 2, film()->length().seconds() * pps, index);
+}
+
+
+void
+DCPTimeline::paint_content(wxGraphicsContext* gc)
+{
+       auto const pps = pixels_per_second().get_value_or(1);
+       auto const film = this->film();
+
+       auto const& solid_pen = *wxThePenList->FindOrCreatePen(wxColour(0, 0, 0), 1, wxPENSTYLE_SOLID);
+       auto const& dotted_pen = *wxThePenList->FindOrCreatePen(wxColour(0, 0, 0), 1, wxPENSTYLE_DOT);
+
+       auto const& video_brush = *wxTheBrushList->FindOrCreateBrush(VIDEO_CONTENT_COLOUR, wxBRUSHSTYLE_SOLID);
+       auto const& audio_brush = *wxTheBrushList->FindOrCreateBrush(AUDIO_CONTENT_COLOUR, wxBRUSHSTYLE_SOLID);
+       auto const& text_brush = *wxTheBrushList->FindOrCreateBrush(TEXT_CONTENT_COLOUR, wxBRUSHSTYLE_SOLID);
+       auto const& atmos_brush = *wxTheBrushList->FindOrCreateBrush(ATMOS_CONTENT_COLOUR, wxBRUSHSTYLE_SOLID);
+
+       auto maybe_draw =
+               [gc, film, pps, solid_pen, dotted_pen]
+               (shared_ptr<Content> content, shared_ptr<ContentPart> part, wxBrush brush, int offset) {
+               if (part) {
+                       auto const y = content_y_pos + offset * content_type_height + 1;
+                       gc->SetPen(solid_pen);
+                       gc->SetBrush(brush);
+                       gc->DrawRectangle(
+                               content->position().seconds() * pps,
+                               y,
+                               content->length_after_trim(film).seconds() * pps,
+                               content_type_height - 2
+                               );
+
+                       gc->SetPen(dotted_pen);
+                       for (auto split: content->reel_split_points(film)) {
+                               if (split != content->position()) {
+                                       auto path = gc->CreatePath();
+                                       path.MoveToPoint(split.seconds() * pps, y);
+                                       path.AddLineToPoint(split.seconds() * pps, y + content_type_height - 2);
+                                       gc->StrokePath(path);
+                               }
+                       }
+               }
+       };
+
+       for (auto content: film->content()) {
+               maybe_draw(content, dynamic_pointer_cast<ContentPart>(content->video), video_brush, 0);
+               maybe_draw(content, dynamic_pointer_cast<ContentPart>(content->audio), audio_brush, 1);
+               for (auto text: content->text) {
+                       maybe_draw(content, dynamic_pointer_cast<ContentPart>(text), text_brush, 2);
+               }
+               maybe_draw(content, dynamic_pointer_cast<ContentPart>(content->atmos), atmos_brush, 3);
+       }
+}
+
+
+void
+DCPTimeline::setup_pixels_per_second()
+{
+       set_pixels_per_second((_canvas->GetSize().GetWidth() - 4) / std::max(1.0, film()->length().seconds()));
+}
+
+
+shared_ptr<ReelBoundary>
+DCPTimeline::event_to_reel_boundary(wxMouseEvent& ev) const
+{
+       Position<int> const position(ev.GetX(), ev.GetY());
+       auto iter = std::find_if(_reel_boundaries.begin(), _reel_boundaries.end(), [position](shared_ptr<const ReelBoundary> boundary) {
+               return boundary->view().bbox().contains(position);
+       });
+
+       if (iter == _reel_boundaries.end()) {
+               return {};
+       }
+
+       return *iter;
+}
+
+
+void
+DCPTimeline::left_down(wxMouseEvent& ev)
+{
+       if (!editable()) {
+               return;
+       }
+
+       if (auto boundary = event_to_reel_boundary(ev)) {
+               auto const snap_distance = DCPTime::from_seconds((_canvas->GetSize().GetWidth() / _pixels_per_second.get_value_or(1)) / SNAP_SUBDIVISION);
+               _drag = DCPTimeline::Drag(
+                       boundary,
+                       _reel_boundaries,
+                       film(),
+                       static_cast<int>(ev.GetX() - boundary->time().seconds() * _pixels_per_second.get_value_or(0)),
+                       _snap->get(),
+                       snap_distance
+                       );
+       } else {
+               _drag = boost::none;
+       }
+}
+
+
+void
+DCPTimeline::right_down(wxMouseEvent& ev)
+{
+       _right_down_position = ev.GetPosition();
+       _canvas->PopupMenu(_menu, _right_down_position);
+}
+
+
+void
+DCPTimeline::left_up(wxMouseEvent&)
+{
+       if (!_drag) {
+               return;
+       }
+
+       set_reel_boundary(_drag->reel_boundary->index(), _drag->time());
+       _drag = boost::none;
+}
+
+
+void
+DCPTimeline::mouse_moved(wxMouseEvent& ev)
+{
+       if (!_drag) {
+               return;
+       }
+
+       auto time = DCPTime::from_seconds((ev.GetPosition().x - _drag->offset) / _pixels_per_second.get_value_or(1));
+       time = std::max(_drag->previous ? _drag->previous->time() : DCPTime(), time);
+       time = std::min(_drag->next ? _drag->next->time() : film()->length(), time);
+       _drag->set_time(time);
+       _canvas->RefreshRect({0, reel_marker_y_pos - 2, _canvas->GetSize().GetWidth(), DCPTimelineReelMarkerView::HEIGHT + 4}, true);
+}
+
+
+void
+DCPTimeline::force_redraw(dcpomatic::Rect<int> const & r)
+{
+       _canvas->RefreshRect(wxRect(r.x, r.y, r.width, r.height), false);
+}
+
+
+shared_ptr<Film>
+DCPTimeline::film() const
+{
+       auto film = _film.lock();
+       DCPOMATIC_ASSERT(film);
+       return film;
+}
+
+
+bool
+DCPTimeline::editable() const
+{
+       return film()->reel_type() == ReelType::CUSTOM;
+}
+
+
+DCPTimeline::Drag::Drag(
+       shared_ptr<ReelBoundary> reel_boundary_,
+       vector<shared_ptr<ReelBoundary>> const& reel_boundaries,
+       shared_ptr<const Film> film,
+       int offset_,
+       bool snap,
+       DCPTime snap_distance
+       )
+       : reel_boundary(reel_boundary_)
+       , offset(offset_)
+       , _snap_distance(snap_distance)
+{
+       auto iter = std::find(reel_boundaries.begin(), reel_boundaries.end(), reel_boundary);
+       auto index = std::distance(reel_boundaries.begin(), iter);
+
+       if (index > 0) {
+               previous = reel_boundaries[index - 1];
+       }
+       if (index < static_cast<int>(reel_boundaries.size() - 1)) {
+               next = reel_boundaries[index + 1];
+       }
+
+       if (snap) {
+               for (auto content: film->content()) {
+                       for (auto split: content->reel_split_points(film)) {
+                               _snaps.push_back(split);
+                       }
+               }
+       }
+}
+
+
+void
+DCPTimeline::Drag::set_time(DCPTime time)
+{
+       optional<DCPTime> nearest_distance;
+       optional<DCPTime> nearest_time;
+       for (auto snap: _snaps) {
+               auto const distance = time - snap;
+               if (!nearest_distance || distance.abs() < nearest_distance->abs()) {
+                       nearest_distance = distance.abs();
+                       nearest_time = snap;
+               }
+       }
+
+       if (nearest_distance && *nearest_distance < _snap_distance) {
+               reel_boundary->set_time(*nearest_time);
+       } else {
+               reel_boundary->set_time(time);
+       }
+}
+
+
+DCPTime
+DCPTimeline::Drag::time() const
+{
+       return reel_boundary->time();
+}
+
diff --git a/src/wx/dcp_timeline.h b/src/wx/dcp_timeline.h
new file mode 100644 (file)
index 0000000..3413c28
--- /dev/null
@@ -0,0 +1,123 @@
+/*
+    Copyright (C) 2023 Carl Hetherington <cth@carlh.net>
+
+    This file is part of DCP-o-matic.
+
+    DCP-o-matic is free software; you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or
+    (at your option) any later version.
+
+    DCP-o-matic is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with DCP-o-matic.  If not, see <http://www.gnu.org/licenses/>.
+
+*/
+
+
+#ifndef DCPOMATIC_DCP_TIMELINE_H
+#define DCPOMATIC_DCP_TIMELINE_H
+
+
+#include "timecode.h"
+#include "timeline.h"
+#include "lib/rect.h"
+#include <dcp/warnings.h>
+LIBDCP_DISABLE_WARNINGS
+#include <wx/wx.h>
+LIBDCP_ENABLE_WARNINGS
+#include <memory>
+
+
+class CheckBox;
+class Choice;
+class Film;
+class ReelBoundary;
+class SpinCtrl;
+class wxGridBagSizer;
+
+
+class DCPTimeline : public Timeline
+{
+public:
+       DCPTimeline(wxWindow* parent, std::shared_ptr<Film> film);
+
+       void force_redraw(dcpomatic::Rect<int> const &);
+
+private:
+       void paint();
+       void paint_reels(wxGraphicsContext* gc);
+       void paint_content(wxGraphicsContext* gc);
+       void setup_pixels_per_second();
+       void left_down(wxMouseEvent& ev);
+       void right_down(wxMouseEvent& ev);
+       void left_up(wxMouseEvent& ev);
+       void mouse_moved(wxMouseEvent& ev);
+       void reel_mode_changed();
+       void maximum_reel_size_changed();
+       void film_changed(ChangeType type, FilmProperty property);
+       std::shared_ptr<Film> film() const;
+       void setup_sensitivity();
+
+       void add_reel_boundary();
+       void setup_reel_settings();
+       void setup_reel_boundaries();
+       std::shared_ptr<ReelBoundary> event_to_reel_boundary(wxMouseEvent& ev) const;
+       void set_reel_boundary(int index, dcpomatic::DCPTime time);
+       bool editable() const;
+
+       std::weak_ptr<Film> _film;
+
+       wxScrolledCanvas* _canvas;
+
+       class Drag
+       {
+       public:
+               Drag(
+                       std::shared_ptr<ReelBoundary> reel_boundary_,
+                       std::vector<std::shared_ptr<ReelBoundary>> const& reel_boundaries,
+                       std::shared_ptr<const Film> film,
+                       int offset_,
+                       bool snap,
+                       dcpomatic::DCPTime snap_distance
+                   );
+
+               std::shared_ptr<ReelBoundary> reel_boundary;
+               std::shared_ptr<ReelBoundary> previous;
+               std::shared_ptr<ReelBoundary> next;
+               int offset = 0;
+
+               void set_time(dcpomatic::DCPTime time);
+               dcpomatic::DCPTime time() const;
+
+       private:
+               std::vector<dcpomatic::DCPTime> _snaps;
+               dcpomatic::DCPTime _snap_distance;
+       };
+
+       boost::optional<Drag> _drag;
+
+       wxPoint _right_down_position;
+
+       wxPanel* _reel_settings;
+       Choice* _reel_type;
+       SpinCtrl* _maximum_reel_size;
+       CheckBox* _snap;
+       wxPanel* _reel_detail;
+       wxGridBagSizer* _reel_detail_sizer;
+
+       wxMenu* _menu;
+       wxMenuItem* _add_reel_boundary;
+
+       boost::signals2::scoped_connection _film_connection;
+
+       std::vector<std::shared_ptr<ReelBoundary>> _reel_boundaries;
+};
+
+
+#endif
+
diff --git a/src/wx/dcp_timeline_dialog.cc b/src/wx/dcp_timeline_dialog.cc
new file mode 100644 (file)
index 0000000..2cf6a74
--- /dev/null
@@ -0,0 +1,78 @@
+/*
+    Copyright (C) 2013-2021 Carl Hetherington <cth@carlh.net>
+
+    This file is part of DCP-o-matic.
+
+    DCP-o-matic is free software; you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or
+    (at your option) any later version.
+
+    DCP-o-matic is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with DCP-o-matic.  If not, see <http://www.gnu.org/licenses/>.
+
+*/
+
+
+#include "dcp_panel.h"
+#include "dcp_timeline_dialog.h"
+#include "film_editor.h"
+#include "wx_util.h"
+#include "lib/compose.hpp"
+#include "lib/cross.h"
+#include "lib/film.h"
+#include "lib/playlist.h"
+#include <dcp/warnings.h>
+LIBDCP_DISABLE_WARNINGS
+#include <wx/graphics.h>
+LIBDCP_ENABLE_WARNINGS
+#include <list>
+
+
+using std::list;
+using std::shared_ptr;
+using std::string;
+using std::weak_ptr;
+#if BOOST_VERSION >= 106100
+using namespace boost::placeholders;
+#endif
+
+
+DCPTimelineDialog::DCPTimelineDialog(wxWindow* parent, shared_ptr<Film> film)
+       : wxDialog(
+               parent,
+               wxID_ANY,
+               _("Reels"),
+               wxDefaultPosition,
+               wxSize(640, 512),
+#ifdef DCPOMATIC_OSX
+               /* I can't get wxFRAME_FLOAT_ON_PARENT to work on OS X, and although wxSTAY_ON_TOP keeps
+                  the window above all others (and not just our own) it's better than nothing for now.
+               */
+               wxDEFAULT_DIALOG_STYLE | wxRESIZE_BORDER | wxFULL_REPAINT_ON_RESIZE | wxSTAY_ON_TOP
+#else
+               wxDEFAULT_DIALOG_STYLE | wxRESIZE_BORDER | wxFULL_REPAINT_ON_RESIZE | wxFRAME_FLOAT_ON_PARENT
+#endif
+               )
+       , _timeline(this, film)
+{
+       auto sizer = new wxBoxSizer(wxVERTICAL);
+       sizer->Add (&_timeline, 1, wxEXPAND | wxALL, 12);
+
+#ifdef DCPOMATIC_LINUX
+       auto buttons = CreateSeparatedButtonSizer (wxCLOSE);
+       if (buttons) {
+               sizer->Add (buttons, wxSizerFlags().Expand().DoubleBorder());
+       }
+#endif
+
+       SetSizer(sizer);
+       sizer->Layout();
+       sizer->SetSizeHints(this);
+}
+
diff --git a/src/wx/dcp_timeline_dialog.h b/src/wx/dcp_timeline_dialog.h
new file mode 100644 (file)
index 0000000..d1293ca
--- /dev/null
@@ -0,0 +1,39 @@
+/*
+    Copyright (C) 2023 Carl Hetherington <cth@carlh.net>
+
+    This file is part of DCP-o-matic.
+
+    DCP-o-matic is free software; you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or
+    (at your option) any later version.
+
+    DCP-o-matic is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with DCP-o-matic.  If not, see <http://www.gnu.org/licenses/>.
+
+*/
+
+
+#include "dcp_timeline.h"
+#include <dcp/warnings.h>
+LIBDCP_DISABLE_WARNINGS
+#include <wx/wx.h>
+LIBDCP_ENABLE_WARNINGS
+#include <memory>
+
+
+class DCPTimelineDialog : public wxDialog
+{
+public:
+       DCPTimelineDialog(wxWindow* parent, std::shared_ptr<Film> film);
+
+private:
+       std::weak_ptr<Film> _film;
+       DCPTimeline _timeline;
+};
+
diff --git a/src/wx/dcp_timeline_reel_marker_view.cc b/src/wx/dcp_timeline_reel_marker_view.cc
new file mode 100644 (file)
index 0000000..1c97ca1
--- /dev/null
@@ -0,0 +1,71 @@
+/*
+    Copyright (C) 2023 Carl Hetherington <cth@carlh.net>
+
+    This file is part of DCP-o-matic.
+
+    DCP-o-matic is free software; you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or
+    (at your option) any later version.
+
+    DCP-o-matic is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with DCP-o-matic.  If not, see <http://www.gnu.org/licenses/>.
+
+*/
+
+
+#include "dcp_timeline_reel_marker_view.h"
+LIBDCP_DISABLE_WARNINGS
+#include <wx/graphics.h>
+LIBDCP_ENABLE_WARNINGS
+
+
+using namespace std;
+using namespace dcpomatic;
+
+
+DCPTimelineReelMarkerView::DCPTimelineReelMarkerView(DCPTimeline& timeline, int y_pos)
+       : DCPTimelineView(timeline)
+       , _y_pos(y_pos)
+{
+
+}
+
+
+int
+DCPTimelineReelMarkerView::x_pos() const
+{
+       /* Nudge it over slightly so that the full line width is drawn on the left hand side */
+       return time_x(_time) + 2;
+}
+
+
+void
+DCPTimelineReelMarkerView::do_paint(wxGraphicsContext* gc)
+{
+       wxColour const outline = _active ? wxColour(0, 0, 0) : wxColour(128, 128, 128);
+       wxColour const fill = _active ? wxColour(255, 0, 0) : wxColour(192, 192, 192);
+       gc->SetPen(*wxThePenList->FindOrCreatePen(outline, 2, wxPENSTYLE_SOLID));
+       gc->SetBrush(*wxTheBrushList->FindOrCreateBrush(fill, wxBRUSHSTYLE_SOLID));
+
+       gc->DrawRectangle(x_pos(), _y_pos, HEAD_SIZE, HEAD_SIZE);
+
+       auto path = gc->CreatePath();
+       path.MoveToPoint(x_pos(), _y_pos + HEAD_SIZE + TAIL_LENGTH);
+       path.AddLineToPoint(x_pos(), _y_pos);
+       gc->StrokePath(path);
+       gc->FillPath(path);
+}
+
+
+dcpomatic::Rect<int>
+DCPTimelineReelMarkerView::bbox() const
+{
+       return { x_pos(), _y_pos, HEAD_SIZE, HEAD_SIZE + TAIL_LENGTH };
+}
+
diff --git a/src/wx/dcp_timeline_reel_marker_view.h b/src/wx/dcp_timeline_reel_marker_view.h
new file mode 100644 (file)
index 0000000..273d982
--- /dev/null
@@ -0,0 +1,59 @@
+/*
+    Copyright (C) 2023 Carl Hetherington <cth@carlh.net>
+
+    This file is part of DCP-o-matic.
+
+    DCP-o-matic is free software; you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or
+    (at your option) any later version.
+
+    DCP-o-matic is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with DCP-o-matic.  If not, see <http://www.gnu.org/licenses/>.
+
+*/
+
+
+#include "dcp_timeline_view.h"
+
+
+class DCPTimeline;
+
+
+class DCPTimelineReelMarkerView : public DCPTimelineView
+{
+public:
+       DCPTimelineReelMarkerView(DCPTimeline& timeline, int y_pos);
+
+       dcpomatic::Rect<int> bbox() const override;
+
+       dcpomatic::DCPTime time() const {
+               return _time;
+       }
+
+       void set_time(dcpomatic::DCPTime time) {
+               _time = time;
+       }
+
+       void set_active(bool active) {
+               _active = active;
+       }
+
+       static auto constexpr HEAD_SIZE = 16;
+       static auto constexpr TAIL_LENGTH = 28;
+       static auto constexpr HEIGHT = HEAD_SIZE + TAIL_LENGTH;
+
+private:
+       void do_paint(wxGraphicsContext* gc) override;
+       int x_pos() const;
+
+       dcpomatic::DCPTime _time;
+       int _y_pos;
+       bool _active = false;
+};
+
diff --git a/src/wx/dcp_timeline_view.h b/src/wx/dcp_timeline_view.h
new file mode 100644 (file)
index 0000000..24a75ca
--- /dev/null
@@ -0,0 +1,44 @@
+/*
+    Copyright (C) 2013-2021 Carl Hetherington <cth@carlh.net>
+
+    This file is part of DCP-o-matic.
+
+    DCP-o-matic is free software; you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or
+    (at your option) any later version.
+
+    DCP-o-matic is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with DCP-o-matic.  If not, see <http://www.gnu.org/licenses/>.
+
+*/
+
+
+#include "dcp_timeline.h"
+#include "timeline_view.h"
+
+
+class DCPTimelineView : public TimelineView<DCPTimeline>
+{
+public:
+       explicit DCPTimelineView(DCPTimeline& timeline)
+               : TimelineView(timeline)
+       {}
+
+       void paint(wxGraphicsContext* gc)
+       {
+               _last_paint_bbox = bbox();
+               do_paint(gc);
+       }
+
+protected:
+       virtual void do_paint(wxGraphicsContext* context) = 0;
+};
+
+
+
index 34a8d6284f0419757963b4138d4eb13df05c10b9..82c2e3598268788ecce0e401f5cd8c3a9589088c 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 7aa0bfb40cc9d9031a238c7e3f39f2980773a34d..c6aba45b74121e9ee84f229c0884552e46f97dbd 100644 (file)
@@ -32,8 +32,8 @@ LIBDCP_ENABLE_WARNINGS
 #include <boost/filesystem.hpp>
 
 
-using namespace std;
-using namespace boost;
+using std::string;
+using boost::optional;
 
 
 FilePickerCtrl::FilePickerCtrl(
@@ -42,9 +42,9 @@ FilePickerCtrl::FilePickerCtrl(
        wxString wildcard,
        bool open,
        bool warn_overwrite,
-       std::string initial_path_key,
+       string initial_path_key,
        optional<std::string> initial_filename,
-       optional<filesystem::path> override_path
+       optional<boost::filesystem::path> override_path
        )
        : wxPanel (parent)
        , _prompt (prompt)
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 05ffa7a685264cfbd46a3e683f21015b3be2bd16..9d309810e5f93392831a6ce568c9fce0a910b96b 100644 (file)
@@ -35,11 +35,12 @@ LIBDCP_ENABLE_WARNINGS
 #include <boost/filesystem.hpp>
 
 
-using namespace std;
-using namespace boost;
+using std::string;
+using boost::bind;
+using boost::optional;
 
 
-optional<filesystem::path> FilmNameLocationDialog::_directory;
+optional<boost::filesystem::path> FilmNameLocationDialog::_directory;
 
 
 FilmNameLocationDialog::FilmNameLocationDialog (wxWindow* parent, wxString title, bool offer_templates)
@@ -112,10 +113,10 @@ FilmNameLocationDialog::folder_changed ()
 }
 
 
-filesystem::path
+boost::filesystem::path
 FilmNameLocationDialog::path () const
 {
-       filesystem::path p;
+       boost::filesystem::path p;
        p /= wx_to_std (_folder->GetPath());
        p /= wx_to_std (_name->GetValue());
        return p;
@@ -139,7 +140,7 @@ FilmNameLocationDialog::template_name () const
 bool
 FilmNameLocationDialog::check_path ()
 {
-       if (filesystem::is_directory(path()) && !filesystem::is_empty(path())) {
+       if (boost::filesystem::is_directory(path()) && !boost::filesystem::is_empty(path())) {
                if (!confirm_dialog (
                            this,
                            std_to_wx (
@@ -150,7 +151,7 @@ FilmNameLocationDialog::check_path ()
                            )) {
                        return false;
                }
-       } else if (filesystem::is_regular_file(path())) {
+       } else if (boost::filesystem::is_regular_file(path())) {
                error_dialog (
                        this,
                        String::compose (wx_to_std(_("%1 already exists as a file, so you cannot use it for a film.")), path().c_str())
index c1c36c4a4e370dbc3d74edd305fdf30c6270f167..e3ea9122494f478c80e979fcc91b31cda7127a01 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 1e6a1930d95f6c358015eb643ffa37b471a6d7c8..64fe8719038a62e5415c017572fa0c0e9644c4d6 100644 (file)
@@ -114,7 +114,7 @@ void
 TimecodeBase::changed ()
 {
        if (_set_button && !_ignore_changed) {
-               _set_button->Enable (true);
+               _set_button->Enable(valid());
        }
 }
 
index 6c5d8ae231cd0d6679e1b56cd2e98b0da34b89d6..22899ddc9ba49bd07f40f8d1acb12229b7d47ea1 100644 (file)
@@ -50,6 +50,7 @@ public:
 protected:
        void changed ();
        void set_clicked ();
+       virtual bool valid() const = 0;
 
        wxSizer* _sizer;
        wxPanel* _editable;
@@ -96,6 +97,11 @@ public:
                _frames->SetHint (std_to_wx(dcp::raw_convert<std::string>(hmsf.f)));
        }
 
+       void set_maximum(dcpomatic::HMSF maximum)
+       {
+               _maximum = std::move(maximum);
+       }
+
        dcpomatic::HMSF get () const
        {
                auto value_or_hint = [](wxTextCtrl const * t) {
@@ -116,6 +122,13 @@ public:
        {
                return T(get(), fps);
        }
+
+private:
+       bool valid() const override {
+               return !_maximum || get() <= *_maximum;
+       }
+
+       boost::optional<dcpomatic::HMSF> _maximum;
 };
 
 #endif
index 38e9de4ee46318ca73bfff98a29023c64bac5047..329f4ef005ce7d3936c448bf459b6f44c5c1c51a 100644 (file)
@@ -1,5 +1,5 @@
 /*
-    Copyright (C) 2013-2021 Carl Hetherington <cth@carlh.net>
+    Copyright (C) 2023 Carl Hetherington <cth@carlh.net>
 
     This file is part of DCP-o-matic.
 
 
 */
 
-#include "content_panel.h"
-#include "film_editor.h"
-#include "film_viewer.h"
-#include "timeline.h"
-#include "timeline_atmos_content_view.h"
-#include "timeline_audio_content_view.h"
-#include "timeline_labels_view.h"
-#include "timeline_reels_view.h"
-#include "timeline_text_content_view.h"
-#include "timeline_time_axis_view.h"
-#include "timeline_video_content_view.h"
-#include "wx_util.h"
-#include "lib/atmos_mxf_content.h"
-#include "lib/audio_content.h"
-#include "lib/film.h"
-#include "lib/image_content.h"
-#include "lib/playlist.h"
-#include "lib/text_content.h"
-#include "lib/timer.h"
-#include "lib/video_content.h"
-#include <dcp/scope_guard.h>
-#include <dcp/warnings.h>
-LIBDCP_DISABLE_WARNINGS
-#include <wx/graphics.h>
-LIBDCP_ENABLE_WARNINGS
-#include <iterator>
-#include <list>
-
 
-using std::abs;
-using std::dynamic_pointer_cast;
-using std::list;
-using std::make_shared;
-using std::max;
-using std::min;
-using std::shared_ptr;
-using std::weak_ptr;
-using boost::bind;
-using boost::optional;
-using namespace dcpomatic;
-#if BOOST_VERSION >= 106100
-using namespace boost::placeholders;
-#endif
+#include "timeline.h"
 
 
 /* 3 hours in 640 pixels */
-double const Timeline::_minimum_pixels_per_second = 640.0 / (60 * 60 * 3);
-int const Timeline::_minimum_pixels_per_track = 16;
-
-
-Timeline::Timeline(wxWindow* parent, ContentPanel* cp, shared_ptr<Film> film, FilmViewer& viewer)
-       : wxPanel (parent, wxID_ANY)
-       , _labels_canvas (new wxScrolledCanvas (this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxFULL_REPAINT_ON_RESIZE))
-       , _main_canvas (new wxScrolledCanvas (this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxFULL_REPAINT_ON_RESIZE))
-       , _content_panel (cp)
-       , _film (film)
-       , _viewer (viewer)
-       , _time_axis_view (new TimelineTimeAxisView (*this, 64))
-       , _reels_view (new TimelineReelsView (*this, 32))
-       , _labels_view (new TimelineLabelsView (*this))
-       , _tracks (0)
-       , _left_down (false)
-       , _down_view_position (0)
-       , _first_move (false)
-       , _menu (this, viewer)
-       , _snap (true)
-       , _tool (SELECT)
-       , _x_scroll_rate (16)
-       , _y_scroll_rate (16)
-       , _pixels_per_track (48)
-       , _first_resize (true)
-       , _timer (this)
-{
-#ifndef __WXOSX__
-       _labels_canvas->SetDoubleBuffered (true);
-       _main_canvas->SetDoubleBuffered (true);
-#endif
-
-       auto sizer = new wxBoxSizer (wxHORIZONTAL);
-       sizer->Add (_labels_canvas, 0, wxEXPAND);
-       _labels_canvas->SetMinSize (wxSize (_labels_view->bbox().width, -1));
-       sizer->Add (_main_canvas, 1, wxEXPAND);
-       SetSizer (sizer);
-
-       _labels_canvas->Bind (wxEVT_PAINT,      boost::bind (&Timeline::paint_labels, this));
-       _main_canvas->Bind   (wxEVT_PAINT,      boost::bind (&Timeline::paint_main,   this));
-       _main_canvas->Bind   (wxEVT_LEFT_DOWN,  boost::bind (&Timeline::left_down,    this, _1));
-       _main_canvas->Bind   (wxEVT_LEFT_UP,    boost::bind (&Timeline::left_up,      this, _1));
-       _main_canvas->Bind   (wxEVT_RIGHT_DOWN, boost::bind (&Timeline::right_down,   this, _1));
-       _main_canvas->Bind   (wxEVT_MOTION,     boost::bind (&Timeline::mouse_moved,  this, _1));
-       _main_canvas->Bind   (wxEVT_SIZE,       boost::bind (&Timeline::resized,      this));
-       _main_canvas->Bind   (wxEVT_MOUSEWHEEL, boost::bind(&Timeline::mouse_wheel_turned, this, _1));
-       _main_canvas->Bind   (wxEVT_SCROLLWIN_TOP,        boost::bind (&Timeline::scrolled,     this, _1));
-       _main_canvas->Bind   (wxEVT_SCROLLWIN_BOTTOM,     boost::bind (&Timeline::scrolled,     this, _1));
-       _main_canvas->Bind   (wxEVT_SCROLLWIN_LINEUP,     boost::bind (&Timeline::scrolled,     this, _1));
-       _main_canvas->Bind   (wxEVT_SCROLLWIN_LINEDOWN,   boost::bind (&Timeline::scrolled,     this, _1));
-       _main_canvas->Bind   (wxEVT_SCROLLWIN_PAGEUP,     boost::bind (&Timeline::scrolled,     this, _1));
-       _main_canvas->Bind   (wxEVT_SCROLLWIN_PAGEDOWN,   boost::bind (&Timeline::scrolled,     this, _1));
-       _main_canvas->Bind   (wxEVT_SCROLLWIN_THUMBTRACK, boost::bind (&Timeline::scrolled,     this, _1));
-
-       film_change(ChangeType::DONE, FilmProperty::CONTENT);
-
-       SetMinSize (wxSize (640, 4 * pixels_per_track() + 96));
-
-       _film_changed_connection = film->Change.connect (bind (&Timeline::film_change, this, _1, _2));
-       _film_content_change_connection = film->ContentChange.connect (bind (&Timeline::film_content_change, this, _1, _3, _4));
-
-       Bind (wxEVT_TIMER, boost::bind(&Timeline::update_playhead, this));
-       _timer.Start (200, wxTIMER_CONTINUOUS);
-
-       setup_scrollbars ();
-       _labels_canvas->ShowScrollbars (wxSHOW_SB_NEVER, wxSHOW_SB_NEVER);
-}
-
-
-void
-Timeline::mouse_wheel_turned(wxMouseEvent& event)
-{
-       auto const rotation = event.GetWheelRotation();
-
-       if (event.ControlDown()) {
-               /* On my mouse one click of the scroll wheel is 120, and it's -ve when
-                * scrolling the wheel towards me.
-                */
-               auto const scale = rotation > 0 ?
-                       (1.0 / (rotation / 90.0)) :
-                       (-rotation / 90.0);
-
-               int before_start_x;
-               int before_start_y;
-               _main_canvas->GetViewStart(&before_start_x, &before_start_y);
-
-               auto const before_pps = _pixels_per_second.get_value_or(1);
-               auto const before_pos = _last_mouse_wheel_x && *_last_mouse_wheel_x == event.GetX() ?
-                       *_last_mouse_wheel_time :
-                       (before_start_x * _x_scroll_rate + event.GetX()) / before_pps;
-
-               set_pixels_per_second(before_pps * scale);
-               setup_scrollbars();
-
-               auto after_left = std::max(0.0, before_pos * _pixels_per_second.get_value_or(1) - event.GetX());
-               _main_canvas->Scroll(after_left / _x_scroll_rate, before_start_y);
-               _labels_canvas->Scroll(0, before_start_y);
-               Refresh();
-
-               if (!_last_mouse_wheel_x || *_last_mouse_wheel_x != event.GetX()) {
-                       _last_mouse_wheel_x = event.GetX();
-                       _last_mouse_wheel_time = before_pos;
-               }
-       } else if (event.ShiftDown()) {
-               int before_start_x;
-               int before_start_y;
-               _main_canvas->GetViewStart(&before_start_x, &before_start_y);
-               auto const width = _main_canvas->GetSize().GetWidth();
-               _main_canvas->Scroll(std::max(0.0, before_start_x - rotation * 100.0 / width), before_start_y);
-       }
-}
-
-
-void
-Timeline::update_playhead ()
-{
-       Refresh ();
-}
-
-
-void
-Timeline::set_pixels_per_second (double pps)
-{
-       _pixels_per_second = max (_minimum_pixels_per_second, pps);
-}
-
-
-void
-Timeline::paint_labels ()
-{
-       wxPaintDC dc (_labels_canvas);
-
-       auto film = _film.lock();
-       if (film->content().empty()) {
-               return;
-       }
-
-       auto gc = wxGraphicsContext::Create (dc);
-       if (!gc) {
-               return;
-       }
-
-       dcp::ScopeGuard sg = [gc]() { delete gc; };
-
-       int vsx, vsy;
-       _labels_canvas->GetViewStart (&vsx, &vsy);
-       gc->Translate (-vsx * _x_scroll_rate, -vsy * _y_scroll_rate + tracks_y_offset());
-
-       _labels_view->paint (gc, {});
-}
-
-
-void
-Timeline::paint_main ()
-{
-       wxPaintDC dc (_main_canvas);
-       dc.Clear();
-
-       auto film = _film.lock();
-       if (film->content().empty()) {
-               return;
-       }
-
-       _main_canvas->DoPrepareDC (dc);
-
-       auto gc = wxGraphicsContext::Create (dc);
-       if (!gc) {
-               return;
-       }
-
-       dcp::ScopeGuard sg = [gc]() { delete gc; };
-
-       gc->SetAntialiasMode (wxANTIALIAS_DEFAULT);
-
-       for (auto i: _views) {
-
-               auto ic = dynamic_pointer_cast<TimelineContentView> (i);
-
-               /* Find areas of overlap with other content views, so that we can plot them */
-               list<dcpomatic::Rect<int>> overlaps;
-               for (auto j: _views) {
-                       auto jc = dynamic_pointer_cast<TimelineContentView> (j);
-                       /* No overlap with non-content views, views on different tracks, audio views or non-active views */
-                       if (!ic || !jc || i == j || ic->track() != jc->track() || ic->track().get_value_or(2) >= 2 || !ic->active() || !jc->active()) {
-                               continue;
-                       }
-
-                       auto r = j->bbox().intersection(i->bbox());
-                       if (r) {
-                               overlaps.push_back (r.get ());
-                       }
-               }
-
-               i->paint (gc, overlaps);
-       }
-
-       if (_zoom_point) {
-               gc->SetPen(gui_is_dark() ? *wxWHITE_PEN : *wxBLACK_PEN);
-               gc->SetBrush (*wxTRANSPARENT_BRUSH);
-               gc->DrawRectangle (
-                       min (_down_point.x, _zoom_point->x),
-                       min (_down_point.y, _zoom_point->y),
-                       abs (_down_point.x - _zoom_point->x),
-                       abs (_down_point.y - _zoom_point->y)
-                       );
-       }
-
-       /* Playhead */
-
-       gc->SetPen (*wxRED_PEN);
-       auto path = gc->CreatePath ();
-       double const ph = _viewer.position().seconds() * pixels_per_second().get_value_or(0);
-       path.MoveToPoint (ph, 0);
-       path.AddLineToPoint (ph, pixels_per_track() * _tracks + 32);
-       gc->StrokePath (path);
-}
-
-
-void
-Timeline::film_change(ChangeType type, FilmProperty p)
-{
-       if (type != ChangeType::DONE) {
-               return;
-       }
-
-       if (p == FilmProperty::CONTENT || p == FilmProperty::REEL_TYPE || p == FilmProperty::REEL_LENGTH) {
-               ensure_ui_thread ();
-               recreate_views ();
-       } else if (p == FilmProperty::CONTENT_ORDER) {
-               Refresh ();
-       }
-}
-
-
-void
-Timeline::recreate_views ()
-{
-       auto film = _film.lock ();
-       if (!film) {
-               return;
-       }
-
-       _views.clear ();
-       _views.push_back (_time_axis_view);
-       _views.push_back (_reels_view);
-
-       for (auto i: film->content ()) {
-               if (i->video) {
-                       _views.push_back (make_shared<TimelineVideoContentView>(*this, i));
-               }
-
-               if (i->audio && !i->audio->mapping().mapped_output_channels().empty ()) {
-                       _views.push_back (make_shared<TimelineAudioContentView>(*this, i));
-               }
-
-               for (auto j: i->text) {
-                       _views.push_back (make_shared<TimelineTextContentView>(*this, i, j));
-               }
-
-               if (i->atmos) {
-                       _views.push_back (make_shared<TimelineAtmosContentView>(*this, i));
-               }
-       }
-
-       assign_tracks ();
-       setup_scrollbars ();
-       Refresh ();
-}
-
-
-void
-Timeline::film_content_change (ChangeType type, int property, bool frequent)
-{
-       if (type != ChangeType::DONE) {
-               return;
-       }
-
-       ensure_ui_thread ();
-
-       if (property == AudioContentProperty::STREAMS || property == VideoContentProperty::FRAME_TYPE) {
-               recreate_views ();
-       } else if (property == ContentProperty::POSITION || property == ContentProperty::LENGTH) {
-               _reels_view->force_redraw ();
-       } else if (!frequent) {
-               setup_scrollbars ();
-               Refresh ();
-       }
-}
-
-
-template <class T>
-int
-place (shared_ptr<const Film> film, TimelineViewList& views, int& tracks)
-{
-       int const base = tracks;
-
-       for (auto i: views) {
-               if (!dynamic_pointer_cast<T>(i)) {
-                       continue;
-               }
-
-               auto cv = dynamic_pointer_cast<TimelineContentView> (i);
-               DCPOMATIC_ASSERT(cv);
-
-               int t = base;
-
-               auto content = cv->content();
-               DCPTimePeriod const content_period = content->period(film);
-
-               while (true) {
-                       auto j = views.begin();
-                       while (j != views.end()) {
-                               auto test = dynamic_pointer_cast<T> (*j);
-                               if (!test) {
-                                       ++j;
-                                       continue;
-                               }
-
-                               auto test_content = test->content();
-                               if (
-                                       test->track() && test->track().get() == t &&
-                                       content_period.overlap(test_content->period(film))
-                                  ) {
-                                       /* we have an overlap on track `t' */
-                                       ++t;
-                                       break;
-                               }
-
-                               ++j;
-                       }
-
-                       if (j == views.end ()) {
-                               /* no overlap on `t' */
-                               break;
-                       }
-               }
-
-               cv->set_track (t);
-               tracks = max (tracks, t + 1);
-       }
-
-       return tracks - base;
-}
-
-
-/** Compare the mapped output channels of two TimelineViews, so we can into
- *  order of first mapped DCP channel.
- */
-struct AudioMappingComparator {
-       bool operator()(shared_ptr<TimelineView> a, shared_ptr<TimelineView> b) {
-               int la = -1;
-               auto cva = dynamic_pointer_cast<TimelineAudioContentView>(a);
-               if (cva) {
-                       auto oc = cva->content()->audio->mapping().mapped_output_channels();
-                       la = *min_element(boost::begin(oc), boost::end(oc));
-               }
-               int lb = -1;
-               auto cvb = dynamic_pointer_cast<TimelineAudioContentView>(b);
-               if (cvb) {
-                       auto oc = cvb->content()->audio->mapping().mapped_output_channels();
-                       lb = *min_element(boost::begin(oc), boost::end(oc));
-               }
-               return la < lb;
-       }
-};
-
-
-void
-Timeline::assign_tracks ()
-{
-       /* Tracks are:
-          Video 1
-          Video 2
-          Video N
-          Text 1
-          Text 2
-          Text N
-          Atmos
-          Audio 1
-          Audio 2
-          Audio N
-       */
-
-       auto film = _film.lock ();
-       DCPOMATIC_ASSERT (film);
-
-       _tracks = 0;
-
-       for (auto i: _views) {
-               auto c = dynamic_pointer_cast<TimelineContentView>(i);
-               if (c) {
-                       c->unset_track ();
-               }
-       }
-
-       int const video_tracks = place<TimelineVideoContentView> (film, _views, _tracks);
-       int const text_tracks = place<TimelineTextContentView> (film, _views, _tracks);
-
-       /* Atmos */
-
-       bool have_atmos = false;
-       for (auto i: _views) {
-               auto cv = dynamic_pointer_cast<TimelineAtmosContentView>(i);
-               if (cv) {
-                       cv->set_track (_tracks);
-                       have_atmos = true;
-               }
-       }
-
-       if (have_atmos) {
-               ++_tracks;
-       }
-
-       /* Audio.  We're sorting the views so that we get the audio views in order of increasing
-          DCP channel index.
-       */
-
-       auto views = _views;
-       sort(views.begin(), views.end(), AudioMappingComparator());
-       int const audio_tracks = place<TimelineAudioContentView> (film, views, _tracks);
-
-       _labels_view->set_video_tracks (video_tracks);
-       _labels_view->set_audio_tracks (audio_tracks);
-       _labels_view->set_text_tracks (text_tracks);
-       _labels_view->set_atmos (have_atmos);
-
-       _time_axis_view->set_y (tracks());
-       _reels_view->set_y (8);
-}
-
-
-int
-Timeline::tracks () const
-{
-       return _tracks;
-}
-
-
-void
-Timeline::setup_scrollbars ()
-{
-       auto film = _film.lock ();
-       if (!film || !_pixels_per_second) {
-               return;
-       }
-
-       int const h = tracks() * pixels_per_track() + tracks_y_offset() + _time_axis_view->bbox().height;
-
-       _labels_canvas->SetVirtualSize (_labels_view->bbox().width, h);
-       _labels_canvas->SetScrollRate (_x_scroll_rate, _y_scroll_rate);
-       _main_canvas->SetVirtualSize (*_pixels_per_second * film->length().seconds(), h);
-       _main_canvas->SetScrollRate (_x_scroll_rate, _y_scroll_rate);
-}
-
-
-shared_ptr<TimelineView>
-Timeline::event_to_view (wxMouseEvent& ev)
-{
-       /* Search backwards through views so that we find the uppermost one first */
-       auto i = _views.rbegin();
-
-       int vsx, vsy;
-       _main_canvas->GetViewStart (&vsx, &vsy);
-       Position<int> const p (ev.GetX() + vsx * _x_scroll_rate, ev.GetY() + vsy * _y_scroll_rate);
-
-       while (i != _views.rend() && !(*i)->bbox().contains (p)) {
-               ++i;
-       }
-
-       if (i == _views.rend ()) {
-               return {};
-       }
-
-       return *i;
-}
-
-
-void
-Timeline::left_down (wxMouseEvent& ev)
-{
-       _left_down = true;
-       _down_point = ev.GetPosition ();
-
-       switch (_tool) {
-       case SELECT:
-               left_down_select (ev);
-               break;
-       case ZOOM:
-       case ZOOM_ALL:
-       case SNAP:
-       case SEQUENCE:
-               /* Nothing to do */
-               break;
-       }
-}
-
-
-void
-Timeline::left_down_select (wxMouseEvent& ev)
-{
-       auto view = event_to_view (ev);
-       auto content_view = dynamic_pointer_cast<TimelineContentView>(view);
-
-       _down_view.reset ();
+double constexpr minimum_pixels_per_second = 640.0 / (60 * 60 * 3);
 
-       if (content_view) {
-               _down_view = content_view;
-               _down_view_position = content_view->content()->position ();
-       }
 
-       if (dynamic_pointer_cast<TimelineTimeAxisView>(view)) {
-               int vsx, vsy;
-               _main_canvas->GetViewStart(&vsx, &vsy);
-               _viewer.seek(DCPTime::from_seconds((ev.GetPosition().x + vsx * _x_scroll_rate) / _pixels_per_second.get_value_or(1)), true);
-       }
-
-       for (auto i: _views) {
-               auto cv = dynamic_pointer_cast<TimelineContentView>(i);
-               if (!cv) {
-                       continue;
-               }
-
-               if (!ev.ShiftDown ()) {
-                       cv->set_selected (view == i);
-               }
-       }
-
-       if (content_view && ev.ShiftDown ()) {
-               content_view->set_selected (!content_view->selected ());
-       }
-
-       _first_move = false;
-
-       if (_down_view) {
-               /* Pre-compute the points that we might snap to */
-               for (auto i: _views) {
-                       auto cv = dynamic_pointer_cast<TimelineContentView>(i);
-                       if (!cv || cv == _down_view || cv->content() == _down_view->content()) {
-                               continue;
-                       }
-
-                       auto film = _film.lock ();
-                       DCPOMATIC_ASSERT (film);
-
-                       _start_snaps.push_back (cv->content()->position());
-                       _end_snaps.push_back (cv->content()->position());
-                       _start_snaps.push_back (cv->content()->end(film));
-                       _end_snaps.push_back (cv->content()->end(film));
-
-                       for (auto i: cv->content()->reel_split_points(film)) {
-                               _start_snaps.push_back (i);
-                       }
-               }
-
-               /* Tell everyone that things might change frequently during the drag */
-               _down_view->content()->set_change_signals_frequent (true);
-       }
-}
-
-
-void
-Timeline::left_up (wxMouseEvent& ev)
-{
-       _left_down = false;
-
-       switch (_tool) {
-       case SELECT:
-               left_up_select (ev);
-               break;
-       case ZOOM:
-               left_up_zoom (ev);
-               break;
-       case ZOOM_ALL:
-       case SNAP:
-       case SEQUENCE:
-               break;
-       }
-}
-
-
-void
-Timeline::left_up_select (wxMouseEvent& ev)
+Timeline::Timeline(wxWindow* parent)
+       : wxPanel(parent, wxID_ANY)
 {
-       if (_down_view) {
-               _down_view->content()->set_change_signals_frequent (false);
-       }
 
-       _content_panel->set_selection (selected_content ());
-       /* Since we may have just set change signals back to `not-frequent', we have to
-          make sure this position change is signalled, even if the position value has
-          not changed since the last time it was set (with frequent=true).  This is
-          a bit of a hack.
-       */
-       set_position_from_event (ev, true);
-
-       /* Clear up up the stuff we don't do during drag */
-       assign_tracks ();
-       setup_scrollbars ();
-       Refresh ();
-
-       _start_snaps.clear ();
-       _end_snaps.clear ();
-}
-
-
-void
-Timeline::left_up_zoom (wxMouseEvent& ev)
-{
-       _zoom_point = ev.GetPosition ();
-
-       int vsx, vsy;
-       _main_canvas->GetViewStart (&vsx, &vsy);
-
-       wxPoint top_left(min(_down_point.x, _zoom_point->x), min(_down_point.y, _zoom_point->y));
-       wxPoint bottom_right(max(_down_point.x, _zoom_point->x), max(_down_point.y, _zoom_point->y));
-
-       if ((bottom_right.x - top_left.x) < 8 || (bottom_right.y - top_left.y) < 8) {
-               /* Very small zoom rectangle: we assume it wasn't intentional */
-               _zoom_point = optional<wxPoint> ();
-               Refresh ();
-               return;
-       }
-
-       auto const time_left = DCPTime::from_seconds((top_left.x + vsx) / *_pixels_per_second);
-       auto const time_right = DCPTime::from_seconds((bottom_right.x + vsx) / *_pixels_per_second);
-       set_pixels_per_second (double(GetSize().GetWidth()) / (time_right.seconds() - time_left.seconds()));
-
-       double const tracks_top = double(top_left.y - tracks_y_offset()) / _pixels_per_track;
-       double const tracks_bottom = double(bottom_right.y - tracks_y_offset()) / _pixels_per_track;
-       set_pixels_per_track (lrint(GetSize().GetHeight() / (tracks_bottom - tracks_top)));
-
-       setup_scrollbars ();
-       int const y = (tracks_top * _pixels_per_track + tracks_y_offset()) / _y_scroll_rate;
-       _main_canvas->Scroll (time_left.seconds() * *_pixels_per_second / _x_scroll_rate, y);
-       _labels_canvas->Scroll (0, y);
-
-       _zoom_point = optional<wxPoint> ();
-       Refresh ();
-}
-
-
-void
-Timeline::set_pixels_per_track (int h)
-{
-       _pixels_per_track = max(_minimum_pixels_per_track, h);
 }
 
 
 void
-Timeline::mouse_moved (wxMouseEvent& ev)
+Timeline::set_pixels_per_second(double pps)
 {
-       switch (_tool) {
-       case SELECT:
-               mouse_moved_select (ev);
-               break;
-       case ZOOM:
-               mouse_moved_zoom (ev);
-               break;
-       case ZOOM_ALL:
-       case SNAP:
-       case SEQUENCE:
-               break;
-       }
+       _pixels_per_second = std::max(minimum_pixels_per_second, pps);
 }
 
 
-void
-Timeline::mouse_moved_select (wxMouseEvent& ev)
-{
-       if (!_left_down) {
-               return;
-       }
-
-       set_position_from_event (ev);
-}
-
-
-void
-Timeline::mouse_moved_zoom (wxMouseEvent& ev)
-{
-       if (!_left_down) {
-               return;
-       }
-
-       _zoom_point = ev.GetPosition ();
-       setup_scrollbars();
-       Refresh ();
-}
-
-
-void
-Timeline::right_down (wxMouseEvent& ev)
-{
-       switch (_tool) {
-       case SELECT:
-               right_down_select (ev);
-               break;
-       case ZOOM:
-               /* Zoom out */
-               set_pixels_per_second (*_pixels_per_second / 2);
-               set_pixels_per_track (_pixels_per_track / 2);
-               setup_scrollbars ();
-               Refresh ();
-               break;
-       case ZOOM_ALL:
-       case SNAP:
-       case SEQUENCE:
-               break;
-       }
-}
-
-
-void
-Timeline::right_down_select (wxMouseEvent& ev)
-{
-       auto view = event_to_view (ev);
-       auto cv = dynamic_pointer_cast<TimelineContentView> (view);
-       if (!cv) {
-               return;
-       }
-
-       if (!cv->selected ()) {
-               clear_selection ();
-               cv->set_selected (true);
-       }
-
-       _menu.popup (_film, selected_content (), selected_views (), ev.GetPosition ());
-}
-
-
-void
-Timeline::maybe_snap (DCPTime a, DCPTime b, optional<DCPTime>& nearest_distance) const
-{
-       auto const d = a - b;
-       if (!nearest_distance || d.abs() < nearest_distance.get().abs()) {
-               nearest_distance = d;
-       }
-}
-
-
-void
-Timeline::set_position_from_event (wxMouseEvent& ev, bool force_emit)
-{
-       if (!_pixels_per_second) {
-               return;
-       }
-
-       double const pps = _pixels_per_second.get ();
-
-       auto const p = ev.GetPosition();
-
-       if (!_first_move) {
-               /* We haven't moved yet; in that case, we must move the mouse some reasonable distance
-                  before the drag is considered to have started.
-               */
-               int const dist = sqrt (pow (p.x - _down_point.x, 2) + pow (p.y - _down_point.y, 2));
-               if (dist < 8) {
-                       return;
-               }
-               _first_move = true;
-       }
-
-       if (!_down_view) {
-               return;
-       }
-
-       auto new_position = _down_view_position + DCPTime::from_seconds ((p.x - _down_point.x) / pps);
-
-       auto film = _film.lock ();
-       DCPOMATIC_ASSERT (film);
-
-       if (_snap) {
-               auto const new_end = new_position + _down_view->content()->length_after_trim(film);
-               /* Signed `distance' to nearest thing (i.e. negative is left on the timeline,
-                  positive is right).
-               */
-               optional<DCPTime> nearest_distance;
-
-               /* Find the nearest snap point */
-
-               for (auto i: _start_snaps) {
-                       maybe_snap (i, new_position, nearest_distance);
-               }
-
-               for (auto i: _end_snaps) {
-                       maybe_snap (i, new_end, nearest_distance);
-               }
-
-               if (nearest_distance) {
-                       /* Snap if it's close; `close' means within a proportion of the time on the timeline */
-                       if (nearest_distance.get().abs() < DCPTime::from_seconds ((width() / pps) / 64)) {
-                               new_position += nearest_distance.get ();
-                       }
-               }
-       }
-
-       if (new_position < DCPTime ()) {
-               new_position = DCPTime ();
-       }
-
-       _down_view->content()->set_position (film, new_position, force_emit);
-
-       film->set_sequence (false);
-}
-
-
-void
-Timeline::force_redraw (dcpomatic::Rect<int> const & r)
-{
-       _main_canvas->RefreshRect (wxRect (r.x, r.y, r.width, r.height), false);
-}
-
-
-shared_ptr<const Film>
-Timeline::film () const
-{
-       return _film.lock ();
-}
-
-
-void
-Timeline::resized ()
-{
-       if (_main_canvas->GetSize().GetWidth() > 0 && _first_resize) {
-               zoom_all ();
-               _first_resize = false;
-       }
-       setup_scrollbars ();
-}
-
-
-void
-Timeline::clear_selection ()
-{
-       for (auto i: _views) {
-               shared_ptr<TimelineContentView> cv = dynamic_pointer_cast<TimelineContentView>(i);
-               if (cv) {
-                       cv->set_selected (false);
-               }
-       }
-}
-
-
-TimelineContentViewList
-Timeline::selected_views () const
-{
-       TimelineContentViewList sel;
-
-       for (auto i: _views) {
-               auto cv = dynamic_pointer_cast<TimelineContentView>(i);
-               if (cv && cv->selected()) {
-                       sel.push_back (cv);
-               }
-       }
-
-       return sel;
-}
-
-
-ContentList
-Timeline::selected_content () const
-{
-       ContentList sel;
-
-       for (auto i: selected_views()) {
-               sel.push_back(i->content());
-       }
-
-       return sel;
-}
-
-
-void
-Timeline::set_selection (ContentList selection)
-{
-       for (auto i: _views) {
-               auto cv = dynamic_pointer_cast<TimelineContentView> (i);
-               if (cv) {
-                       cv->set_selected (find (selection.begin(), selection.end(), cv->content ()) != selection.end ());
-               }
-       }
-}
-
-
-int
-Timeline::tracks_y_offset () const
-{
-       return _reels_view->bbox().height + 4;
-}
-
-
-int
-Timeline::width () const
-{
-       return _main_canvas->GetVirtualSize().GetWidth();
-}
-
-
-void
-Timeline::scrolled (wxScrollWinEvent& ev)
-{
-       if (ev.GetOrientation() == wxVERTICAL) {
-               int x, y;
-               _main_canvas->GetViewStart (&x, &y);
-               _labels_canvas->Scroll (0, y);
-       }
-       ev.Skip ();
-}
-
-
-void
-Timeline::tool_clicked (Tool t)
-{
-       switch (t) {
-       case ZOOM:
-       case SELECT:
-               _tool = t;
-               break;
-       case ZOOM_ALL:
-               zoom_all ();
-               break;
-       case SNAP:
-       case SEQUENCE:
-               break;
-       }
-}
-
-
-void
-Timeline::zoom_all ()
-{
-       auto film = _film.lock ();
-       DCPOMATIC_ASSERT (film);
-       set_pixels_per_second((_main_canvas->GetSize().GetWidth() - 32) / std::max(1.0, film->length().seconds()));
-       set_pixels_per_track((_main_canvas->GetSize().GetHeight() - tracks_y_offset() - _time_axis_view->bbox().height - 32) / std::max(1, _tracks));
-       setup_scrollbars ();
-       _main_canvas->Scroll (0, 0);
-       _labels_canvas->Scroll (0, 0);
-       Refresh ();
-}
-
-
-void
-Timeline::keypress(wxKeyEvent const& event)
-{
-       if (event.GetKeyCode() == WXK_DELETE) {
-               auto film = _film.lock();
-               DCPOMATIC_ASSERT(film);
-               film->remove_content(selected_content());
-       } else {
-               switch (event.GetRawKeyCode()) {
-               case '+':
-                       set_pixels_per_second(_pixels_per_second.get_value_or(1) * 2);
-                       setup_scrollbars();
-                       break;
-               case '-':
-                       set_pixels_per_second(_pixels_per_second.get_value_or(1) / 2);
-                       setup_scrollbars();
-                       break;
-               }
-       }
-}
-
index 621609fa7e66b94a2ae1f8cd73f2d548ce609368..cc35913b98052cf4462f73f8735e45cf02f25545 100644 (file)
@@ -1,5 +1,5 @@
 /*
-    Copyright (C) 2013-2019 Carl Hetherington <cth@carlh.net>
+    Copyright (C) 2023 Carl Hetherington <cth@carlh.net>
 
     This file is part of DCP-o-matic.
 
 */
 
 
-#include "content_menu.h"
-#include "timeline_content_view.h"
-#include "lib/film_property.h"
-#include "lib/rect.h"
+#ifndef DCPOMATIC_TIMELINE_H
+#define DCPOMATIC_TIMELINE_H
+
+
 #include <dcp/warnings.h>
 LIBDCP_DISABLE_WARNINGS
 #include <wx/wx.h>
 LIBDCP_ENABLE_WARNINGS
-#include <boost/signals2.hpp>
-
-
-class ContentPanel;
-class Film;
-class FilmViewer;
-class TimelineLabelsView;
-class TimelineReelsView;
-class TimelineTimeAxisView;
-class TimelineView;
+#include <boost/optional.hpp>
 
 
 class Timeline : public wxPanel
 {
 public:
-       Timeline (wxWindow *, ContentPanel *, std::shared_ptr<Film>, FilmViewer& viewer);
-
-       std::shared_ptr<const Film> film () const;
-
-       void force_redraw (dcpomatic::Rect<int> const &);
-
-       int width () const;
+       explicit Timeline(wxWindow* parent);
 
-       int pixels_per_track () const {
-               return _pixels_per_track;
-       }
-
-       boost::optional<double> pixels_per_second () const {
+       boost::optional<double> pixels_per_second() const {
                return _pixels_per_second;
        }
 
-       int tracks () const;
 
-       void set_snap (bool s) {
-               _snap = s;
-       }
+protected:
+       void set_pixels_per_second(double pps);
 
-       bool snap () const {
-               return _snap;
-       }
-
-       void set_selection (ContentList selection);
-
-       enum Tool {
-               SELECT,
-               ZOOM,
-               ZOOM_ALL,
-               SNAP,
-               SEQUENCE
-       };
-
-       void tool_clicked (Tool t);
-
-       int tracks_y_offset () const;
-
-       void keypress(wxKeyEvent const &);
-
-private:
-       void paint_labels ();
-       void paint_main ();
-       void left_down (wxMouseEvent &);
-       void left_down_select (wxMouseEvent &);
-       void left_up (wxMouseEvent &);
-       void left_up_select (wxMouseEvent &);
-       void left_up_zoom (wxMouseEvent &);
-       void right_down (wxMouseEvent &);
-       void right_down_select (wxMouseEvent &);
-       void mouse_moved (wxMouseEvent &);
-       void mouse_moved_select (wxMouseEvent &);
-       void mouse_moved_zoom (wxMouseEvent &);
-       void film_change(ChangeType type, FilmProperty);
-       void film_content_change (ChangeType type, int, bool frequent);
-       void resized ();
-       void assign_tracks ();
-       void set_position_from_event (wxMouseEvent& ev, bool force_emit = false);
-       void clear_selection ();
-       void recreate_views ();
-       void setup_scrollbars ();
-       void scrolled (wxScrollWinEvent& ev);
-       void set_pixels_per_second (double pps);
-       void set_pixels_per_track (int h);
-       void zoom_all ();
-       void update_playhead ();
-       void mouse_wheel_turned(wxMouseEvent& event);
-
-       std::shared_ptr<TimelineView> event_to_view (wxMouseEvent &);
-       TimelineContentViewList selected_views () const;
-       ContentList selected_content () const;
-       void maybe_snap (dcpomatic::DCPTime a, dcpomatic::DCPTime b, boost::optional<dcpomatic::DCPTime>& nearest_distance) const;
-
-       wxScrolledCanvas* _labels_canvas;
-       wxScrolledCanvas* _main_canvas;
-       ContentPanel* _content_panel;
-       std::weak_ptr<Film> _film;
-       FilmViewer& _viewer;
-       TimelineViewList _views;
-       std::shared_ptr<TimelineTimeAxisView> _time_axis_view;
-       std::shared_ptr<TimelineReelsView> _reels_view;
-       std::shared_ptr<TimelineLabelsView> _labels_view;
-       int _tracks;
        boost::optional<double> _pixels_per_second;
-       bool _left_down;
-       wxPoint _down_point;
-       boost::optional<wxPoint> _zoom_point;
-       std::shared_ptr<TimelineContentView> _down_view;
-       dcpomatic::DCPTime _down_view_position;
-       bool _first_move;
-       ContentMenu _menu;
-       bool _snap;
-       std::list<dcpomatic::DCPTime> _start_snaps;
-       std::list<dcpomatic::DCPTime> _end_snaps;
-       Tool _tool;
-       int _x_scroll_rate;
-       int _y_scroll_rate;
-       int _pixels_per_track;
-       bool _first_resize;
-       wxTimer _timer;
-       boost::optional<int> _last_mouse_wheel_x;
-       boost::optional<double> _last_mouse_wheel_time;
-
-       static double const _minimum_pixels_per_second;
-       static int const _minimum_pixels_per_track;
-
-       boost::signals2::scoped_connection _film_changed_connection;
-       boost::signals2::scoped_connection _film_content_change_connection;
 };
+
+
+#endif
diff --git a/src/wx/timeline_atmos_content_view.cc b/src/wx/timeline_atmos_content_view.cc
deleted file mode 100644 (file)
index ec0f06e..0000000
+++ /dev/null
@@ -1,50 +0,0 @@
-/*
-    Copyright (C) 2016-2021 Carl Hetherington <cth@carlh.net>
-
-    This file is part of DCP-o-matic.
-
-    DCP-o-matic is free software; you can redistribute it and/or modify
-    it under the terms of the GNU General Public License as published by
-    the Free Software Foundation; either version 2 of the License, or
-    (at your option) any later version.
-
-    DCP-o-matic is distributed in the hope that it will be useful,
-    but WITHOUT ANY WARRANTY; without even the implied warranty of
-    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-    GNU General Public License for more details.
-
-    You should have received a copy of the GNU General Public License
-    along with DCP-o-matic.  If not, see <http://www.gnu.org/licenses/>.
-
-*/
-
-
-#include "timeline_atmos_content_view.h"
-
-
-using std::shared_ptr;
-
-
-/** @class TimelineAtmosContentView
- *  @brief Timeline view for AtmosContent.
- */
-
-TimelineAtmosContentView::TimelineAtmosContentView (Timeline& tl, shared_ptr<Content> c)
-       : TimelineContentView (tl, c)
-{
-
-}
-
-
-wxColour
-TimelineAtmosContentView::background_colour () const
-{
-       return wxColour (149, 121, 232, 255);
-}
-
-
-wxColour
-TimelineAtmosContentView::foreground_colour () const
-{
-       return wxColour (0, 0, 0, 255);
-}
diff --git a/src/wx/timeline_atmos_content_view.h b/src/wx/timeline_atmos_content_view.h
deleted file mode 100644 (file)
index 15da14e..0000000
+++ /dev/null
@@ -1,40 +0,0 @@
-/*
-    Copyright (C) 2016-2021 Carl Hetherington <cth@carlh.net>
-
-    This file is part of DCP-o-matic.
-
-    DCP-o-matic is free software; you can redistribute it and/or modify
-    it under the terms of the GNU General Public License as published by
-    the Free Software Foundation; either version 2 of the License, or
-    (at your option) any later version.
-
-    DCP-o-matic is distributed in the hope that it will be useful,
-    but WITHOUT ANY WARRANTY; without even the implied warranty of
-    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-    GNU General Public License for more details.
-
-    You should have received a copy of the GNU General Public License
-    along with DCP-o-matic.  If not, see <http://www.gnu.org/licenses/>.
-
-*/
-
-
-#include "timeline_content_view.h"
-
-
-/** @class TimelineAtmosContentView
- *  @brief Timeline view for AtmosContent.
- */
-class TimelineAtmosContentView : public TimelineContentView
-{
-public:
-       TimelineAtmosContentView (Timeline& tl, std::shared_ptr<Content> c);
-
-private:
-       bool active () const override {
-               return true;
-       }
-
-       wxColour background_colour () const override;
-       wxColour foreground_colour () const override;
-};
diff --git a/src/wx/timeline_audio_content_view.cc b/src/wx/timeline_audio_content_view.cc
deleted file mode 100644 (file)
index 057ebb9..0000000
+++ /dev/null
@@ -1,81 +0,0 @@
-/*
-    Copyright (C) 2013-2018 Carl Hetherington <cth@carlh.net>
-
-    This file is part of DCP-o-matic.
-
-    DCP-o-matic is free software; you can redistribute it and/or modify
-    it under the terms of the GNU General Public License as published by
-    the Free Software Foundation; either version 2 of the License, or
-    (at your option) any later version.
-
-    DCP-o-matic is distributed in the hope that it will be useful,
-    but WITHOUT ANY WARRANTY; without even the implied warranty of
-    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-    GNU General Public License for more details.
-
-    You should have received a copy of the GNU General Public License
-    along with DCP-o-matic.  If not, see <http://www.gnu.org/licenses/>.
-
-*/
-
-#include "timeline_audio_content_view.h"
-#include "wx_util.h"
-#include "lib/audio_content.h"
-#include "lib/util.h"
-
-using std::list;
-using std::shared_ptr;
-using std::dynamic_pointer_cast;
-
-/** @class TimelineAudioContentView
- *  @brief Timeline view for AudioContent.
- */
-
-TimelineAudioContentView::TimelineAudioContentView (Timeline& tl, shared_ptr<Content> c)
-       : TimelineContentView (tl, c)
-{
-
-}
-
-wxColour
-TimelineAudioContentView::background_colour () const
-{
-       return wxColour (149, 121, 232, 255);
-}
-
-wxColour
-TimelineAudioContentView::foreground_colour () const
-{
-       return wxColour (0, 0, 0, 255);
-}
-
-wxString
-TimelineAudioContentView::label () const
-{
-       wxString s = TimelineContentView::label ();
-       shared_ptr<AudioContent> ac = content()->audio;
-       DCPOMATIC_ASSERT (ac);
-
-       if (ac->gain() > 0.01) {
-               s += wxString::Format (" +%.1fdB", ac->gain());
-       } else if (ac->gain() < -0.01) {
-               s += wxString::Format (" %.1fdB", ac->gain());
-       }
-
-       if (ac->delay() > 0) {
-               s += wxString::Format (_(" delayed by %dms"), ac->delay());
-       } else if (ac->delay() < 0) {
-               s += wxString::Format (_(" advanced by %dms"), -ac->delay());
-       }
-
-       list<int> mapped = ac->mapping().mapped_output_channels();
-       if (!mapped.empty ()) {
-               s += wxString::FromUTF8(" â†’ ");
-               for (auto i: mapped) {
-                       s += std_to_wx(short_audio_channel_name(i)) + ", ";
-               }
-               s = s.Left(s.Length() - 2);
-       }
-
-       return s;
-}
diff --git a/src/wx/timeline_audio_content_view.h b/src/wx/timeline_audio_content_view.h
deleted file mode 100644 (file)
index 5b8d6cd..0000000
+++ /dev/null
@@ -1,40 +0,0 @@
-/*
-    Copyright (C) 2013-2015 Carl Hetherington <cth@carlh.net>
-
-    This file is part of DCP-o-matic.
-
-    DCP-o-matic is free software; you can redistribute it and/or modify
-    it under the terms of the GNU General Public License as published by
-    the Free Software Foundation; either version 2 of the License, or
-    (at your option) any later version.
-
-    DCP-o-matic is distributed in the hope that it will be useful,
-    but WITHOUT ANY WARRANTY; without even the implied warranty of
-    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-    GNU General Public License for more details.
-
-    You should have received a copy of the GNU General Public License
-    along with DCP-o-matic.  If not, see <http://www.gnu.org/licenses/>.
-
-*/
-
-
-#include "timeline_content_view.h"
-
-
-/** @class TimelineAudioContentView
- *  @brief Timeline view for AudioContent.
- */
-class TimelineAudioContentView : public TimelineContentView
-{
-public:
-       TimelineAudioContentView (Timeline& tl, std::shared_ptr<Content> c);
-
-private:
-       bool active () const override {
-               return true;
-       }
-       wxColour background_colour () const override;
-       wxColour foreground_colour () const override;
-       wxString label () const override;
-};
index 5d039d0d3f0a2e82d252da234f63519b8eb4864e..cb0d102405ea34cad4e6b8bc65a4c56820b75557 100644 (file)
@@ -19,7 +19,7 @@
 */
 
 
-#include "timeline.h"
+#include "content_timeline.h"
 #include "timeline_content_view.h"
 #include "wx_util.h"
 #include "lib/content.h"
@@ -37,8 +37,8 @@ using namespace boost::placeholders;
 #endif
 
 
-TimelineContentView::TimelineContentView (Timeline& tl, shared_ptr<Content> c)
-       : TimelineView (tl)
+TimelineContentView::TimelineContentView(ContentTimeline& tl, shared_ptr<Content> c)
+       : ContentTimelineView(tl)
        , _content (c)
 {
        _content_connection = c->Change.connect (bind (&TimelineContentView::content_change, this, _1, _3));
index 7794120cdc96762880e944bb14baf1b58b12503e..7b206d30f324f8fefb0cc5580997e12ab29cab9c 100644 (file)
@@ -23,7 +23,7 @@
 #define DCPOMATIC_TIMELINE_CONTENT_VIEW_H
 
 
-#include "timeline_view.h"
+#include "content_timeline_view.h"
 #include "lib/change_signaller.h"
 #include <dcp/warnings.h>
 LIBDCP_DISABLE_WARNINGS
@@ -37,10 +37,10 @@ class Content;
 /** @class TimelineContentView
  *  @brief Parent class for views of pieces of content.
  */
-class TimelineContentView : public TimelineView
+class TimelineContentView : public ContentTimelineView
 {
 public:
-       TimelineContentView (Timeline& tl, std::shared_ptr<Content> c);
+       TimelineContentView(ContentTimeline& tl, std::shared_ptr<Content> c);
 
        dcpomatic::Rect<int> bbox () const override;
 
diff --git a/src/wx/timeline_dialog.cc b/src/wx/timeline_dialog.cc
deleted file mode 100644 (file)
index e0e1689..0000000
+++ /dev/null
@@ -1,153 +0,0 @@
-/*
-    Copyright (C) 2013-2021 Carl Hetherington <cth@carlh.net>
-
-    This file is part of DCP-o-matic.
-
-    DCP-o-matic is free software; you can redistribute it and/or modify
-    it under the terms of the GNU General Public License as published by
-    the Free Software Foundation; either version 2 of the License, or
-    (at your option) any later version.
-
-    DCP-o-matic is distributed in the hope that it will be useful,
-    but WITHOUT ANY WARRANTY; without even the implied warranty of
-    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-    GNU General Public License for more details.
-
-    You should have received a copy of the GNU General Public License
-    along with DCP-o-matic.  If not, see <http://www.gnu.org/licenses/>.
-
-*/
-
-
-#include "content_panel.h"
-#include "film_editor.h"
-#include "timeline_dialog.h"
-#include "wx_util.h"
-#include "lib/compose.hpp"
-#include "lib/cross.h"
-#include "lib/film.h"
-#include "lib/playlist.h"
-#include <dcp/warnings.h>
-LIBDCP_DISABLE_WARNINGS
-#include <wx/graphics.h>
-LIBDCP_ENABLE_WARNINGS
-#include <list>
-
-
-using std::list;
-using std::shared_ptr;
-using std::string;
-using std::weak_ptr;
-#if BOOST_VERSION >= 106100
-using namespace boost::placeholders;
-#endif
-
-
-TimelineDialog::TimelineDialog(ContentPanel* cp, shared_ptr<Film> film, FilmViewer& viewer)
-       : wxDialog (
-               cp->window(),
-               wxID_ANY,
-               _("Timeline"),
-               wxDefaultPosition,
-               wxSize (640, 512),
-#ifdef DCPOMATIC_OSX
-               /* I can't get wxFRAME_FLOAT_ON_PARENT to work on OS X, and although wxSTAY_ON_TOP keeps
-                  the window above all others (and not just our own) it's better than nothing for now.
-               */
-               wxDEFAULT_DIALOG_STYLE | wxRESIZE_BORDER | wxFULL_REPAINT_ON_RESIZE | wxSTAY_ON_TOP
-#else
-               wxDEFAULT_DIALOG_STYLE | wxRESIZE_BORDER | wxFULL_REPAINT_ON_RESIZE | wxFRAME_FLOAT_ON_PARENT
-#endif
-               )
-       , _film (film)
-       , _timeline (this, cp, film, viewer)
-{
-       auto sizer = new wxBoxSizer (wxVERTICAL);
-
-       wxBitmap select(icon_path("select"), wxBITMAP_TYPE_PNG);
-       wxBitmap zoom(icon_path("zoom"), wxBITMAP_TYPE_PNG);
-       wxBitmap zoom_all(icon_path("zoom_all"), wxBITMAP_TYPE_PNG);
-       wxBitmap snap(icon_path("snap"), wxBITMAP_TYPE_PNG);
-       wxBitmap sequence(icon_path("sequence"), wxBITMAP_TYPE_PNG);
-
-       _toolbar = new wxToolBar (this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxTB_HORIZONTAL);
-       _toolbar->SetMargins (4, 4);
-       _toolbar->SetToolBitmapSize (wxSize(32, 32));
-       _toolbar->AddRadioTool ((int) Timeline::SELECT, _("Select"), select, wxNullBitmap, _("Select and move content"));
-       _toolbar->AddRadioTool ((int) Timeline::ZOOM, _("Zoom"), zoom, wxNullBitmap, _("Zoom in / out"));
-       _toolbar->AddTool ((int) Timeline::ZOOM_ALL, _("Zoom all"), zoom_all, _("Zoom out to whole film"));
-       _toolbar->AddCheckTool ((int) Timeline::SNAP, _("Snap"), snap, wxNullBitmap, _("Snap"));
-       _toolbar->AddCheckTool ((int) Timeline::SEQUENCE, _("Sequence"), sequence, wxNullBitmap, _("Keep video and subtitles in sequence"));
-       _toolbar->Realize ();
-
-       _toolbar->Bind (wxEVT_TOOL, bind (&TimelineDialog::tool_clicked, this, _1));
-
-       sizer->Add (_toolbar, 0, wxALL, 12);
-       sizer->Add (&_timeline, 1, wxEXPAND | wxALL, 12);
-
-#ifdef DCPOMATIC_LINUX
-       auto buttons = CreateSeparatedButtonSizer (wxCLOSE);
-       if (buttons) {
-               sizer->Add (buttons, wxSizerFlags().Expand().DoubleBorder());
-       }
-#endif
-
-       SetSizer (sizer);
-       sizer->Layout ();
-       sizer->SetSizeHints (this);
-
-       Bind(wxEVT_CHAR_HOOK, boost::bind(&TimelineDialog::keypress, this, _1));
-
-        _toolbar->ToggleTool ((int) Timeline::SNAP, _timeline.snap ());
-       film_change(ChangeType::DONE, FilmProperty::SEQUENCE);
-
-       _film_changed_connection = film->Change.connect (bind (&TimelineDialog::film_change, this, _1, _2));
-}
-
-
-void
-TimelineDialog::film_change(ChangeType type, FilmProperty p)
-{
-       if (type != ChangeType::DONE) {
-               return;
-       }
-
-       auto film = _film.lock ();
-       if (!film) {
-               return;
-       }
-
-       if (p == FilmProperty::SEQUENCE) {
-               _toolbar->ToggleTool ((int) Timeline::SEQUENCE, film->sequence ());
-       }
-}
-
-
-void
-TimelineDialog::set_selection (ContentList selection)
-{
-       _timeline.set_selection (selection);
-}
-
-
-void
-TimelineDialog::tool_clicked (wxCommandEvent& ev)
-{
-       Timeline::Tool t = static_cast<Timeline::Tool>(ev.GetId());
-       _timeline.tool_clicked (t);
-       if (t == Timeline::SNAP) {
-               _timeline.set_snap (_toolbar->GetToolState(static_cast<int>(t)));
-       } else if (t == Timeline::SEQUENCE) {
-               auto film = _film.lock ();
-               if (film) {
-                       film->set_sequence (_toolbar->GetToolState(static_cast<int>(t)));
-               }
-       }
-}
-
-
-void
-TimelineDialog::keypress(wxKeyEvent const& event)
-{
-       _timeline.keypress(event);
-}
diff --git a/src/wx/timeline_dialog.h b/src/wx/timeline_dialog.h
deleted file mode 100644 (file)
index 8134aa6..0000000
+++ /dev/null
@@ -1,48 +0,0 @@
-/*
-    Copyright (C) 2013-2021 Carl Hetherington <cth@carlh.net>
-
-    This file is part of DCP-o-matic.
-
-    DCP-o-matic is free software; you can redistribute it and/or modify
-    it under the terms of the GNU General Public License as published by
-    the Free Software Foundation; either version 2 of the License, or
-    (at your option) any later version.
-
-    DCP-o-matic is distributed in the hope that it will be useful,
-    but WITHOUT ANY WARRANTY; without even the implied warranty of
-    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-    GNU General Public License for more details.
-
-    You should have received a copy of the GNU General Public License
-    along with DCP-o-matic.  If not, see <http://www.gnu.org/licenses/>.
-
-*/
-
-
-#include "timeline.h"
-#include <dcp/warnings.h>
-LIBDCP_DISABLE_WARNINGS
-#include <wx/wx.h>
-LIBDCP_ENABLE_WARNINGS
-
-
-class Playlist;
-
-
-class TimelineDialog : public wxDialog
-{
-public:
-       TimelineDialog(ContentPanel *, std::shared_ptr<Film>, FilmViewer& viewer);
-
-       void set_selection (ContentList selection);
-
-private:
-       void film_change(ChangeType type, FilmProperty);
-       void tool_clicked (wxCommandEvent& id);
-       void keypress(wxKeyEvent const& event);
-
-       std::weak_ptr<Film> _film;
-       Timeline _timeline;
-       wxToolBar* _toolbar;
-       boost::signals2::scoped_connection _film_changed_connection;
-};
index 181adc5ca411b07a8ada58617309e62782768010..c869d7ec524a63d85287e42dfe765a0674aa8dd6 100644 (file)
@@ -19,7 +19,7 @@
 */
 
 
-#include "timeline.h"
+#include "content_timeline.h"
 #include "timeline_labels_view.h"
 #include "wx_util.h"
 #include <dcp/warnings.h>
@@ -34,8 +34,8 @@ using std::max;
 using std::min;
 
 
-TimelineLabelsView::TimelineLabelsView (Timeline& tl)
-       : TimelineView (tl)
+TimelineLabelsView::TimelineLabelsView(ContentTimeline& tl)
+       : ContentTimelineView(tl)
 {
        wxString labels[] = {
                _("Video"),
index fb80b1bf3311ec9533fdd6e280620750516c188d..324531c397c715b810b0ba431ea017d0c70e21fe 100644 (file)
 */
 
 
-#include "timeline_view.h"
+#include "content_timeline_view.h"
 
 
 class wxWindow;
 
 
-class TimelineLabelsView : public TimelineView
+class TimelineLabelsView : public ContentTimelineView
 {
 public:
-       explicit TimelineLabelsView (Timeline& tl);
+       explicit TimelineLabelsView(ContentTimeline& tl);
 
        dcpomatic::Rect<int> bbox () const override;
 
index 0601a1196a1ee77a7aa1d52281182200d11af25b..5f2a7807954dd047a0019966e444518196a0711c 100644 (file)
@@ -19,7 +19,7 @@
 */
 
 
-#include "timeline.h"
+#include "content_timeline.h"
 #include "timeline_reels_view.h"
 #include "wx_util.h"
 #include "lib/film.h"
@@ -35,8 +35,8 @@ using std::list;
 using namespace dcpomatic;
 
 
-TimelineReelsView::TimelineReelsView (Timeline& tl, int y)
-       : TimelineView (tl)
+TimelineReelsView::TimelineReelsView(ContentTimeline& tl, int y)
+       : ContentTimelineView(tl)
        , _y (y)
 {
 
index 357fe2ce47f80daa69ad3c6892f15f5796f8a9f0..7dbc34308289721e32424831df359540d2b31da3 100644 (file)
 */
 
 
-#include "timeline_view.h"
+#include "content_timeline_view.h"
 
 
-class TimelineReelsView : public TimelineView
+class TimelineReelsView : public ContentTimelineView
 {
 public:
-       TimelineReelsView (Timeline& tl, int y);
+       TimelineReelsView(ContentTimeline& tl, int y);
 
        dcpomatic::Rect<int> bbox () const override;
        void set_y (int y);
diff --git a/src/wx/timeline_text_content_view.cc b/src/wx/timeline_text_content_view.cc
deleted file mode 100644 (file)
index a573985..0000000
+++ /dev/null
@@ -1,61 +0,0 @@
-/*
-    Copyright (C) 2013-2018 Carl Hetherington <cth@carlh.net>
-
-    This file is part of DCP-o-matic.
-
-    DCP-o-matic is free software; you can redistribute it and/or modify
-    it under the terms of the GNU General Public License as published by
-    the Free Software Foundation; either version 2 of the License, or
-    (at your option) any later version.
-
-    DCP-o-matic is distributed in the hope that it will be useful,
-    but WITHOUT ANY WARRANTY; without even the implied warranty of
-    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-    GNU General Public License for more details.
-
-    You should have received a copy of the GNU General Public License
-    along with DCP-o-matic.  If not, see <http://www.gnu.org/licenses/>.
-
-*/
-
-
-#include "timeline_text_content_view.h"
-#include "lib/text_content.h"
-#include "lib/content.h"
-
-
-using std::shared_ptr;
-
-
-TimelineTextContentView::TimelineTextContentView (Timeline& tl, shared_ptr<Content> c, shared_ptr<TextContent> caption)
-       : TimelineContentView (tl, c)
-       , _caption (caption)
-{
-
-}
-
-wxColour
-TimelineTextContentView::background_colour () const
-{
-       if (!active ()) {
-               return wxColour (210, 210, 210, 128);
-       }
-
-       return wxColour (163, 255, 154, 255);
-}
-
-wxColour
-TimelineTextContentView::foreground_colour () const
-{
-       if (!active ()) {
-               return wxColour (180, 180, 180, 128);
-       }
-
-       return wxColour (0, 0, 0, 255);
-}
-
-bool
-TimelineTextContentView::active () const
-{
-       return _caption->use();
-}
diff --git a/src/wx/timeline_text_content_view.h b/src/wx/timeline_text_content_view.h
deleted file mode 100644 (file)
index 046f5b3..0000000
+++ /dev/null
@@ -1,40 +0,0 @@
-/*
-    Copyright (C) 2013-2016 Carl Hetherington <cth@carlh.net>
-
-    This file is part of DCP-o-matic.
-
-    DCP-o-matic is free software; you can redistribute it and/or modify
-    it under the terms of the GNU General Public License as published by
-    the Free Software Foundation; either version 2 of the License, or
-    (at your option) any later version.
-
-    DCP-o-matic is distributed in the hope that it will be useful,
-    but WITHOUT ANY WARRANTY; without even the implied warranty of
-    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-    GNU General Public License for more details.
-
-    You should have received a copy of the GNU General Public License
-    along with DCP-o-matic.  If not, see <http://www.gnu.org/licenses/>.
-
-*/
-
-#include "timeline_content_view.h"
-
-class TextContent;
-class TextContent;
-
-/** @class TimelineTextContentView
- *  @brief Timeline view for TextContent.
- */
-class TimelineTextContentView : public TimelineContentView
-{
-public:
-       TimelineTextContentView (Timeline& tl, std::shared_ptr<Content>, std::shared_ptr<TextContent>);
-
-private:
-       bool active () const override;
-       wxColour background_colour () const override;
-       wxColour foreground_colour () const override;
-
-       std::shared_ptr<TextContent> _caption;
-};
index d055bda7d54bfb310e338f1fcd2b2112cc64ad51..d9b7710c62589ff61f4ceae5ac37d6a13ef14f4c 100644 (file)
@@ -19,7 +19,7 @@
 */
 
 
-#include "timeline.h"
+#include "content_timeline.h"
 #include "timeline_time_axis_view.h"
 #include "wx_util.h"
 #include <dcp/warnings.h>
@@ -34,8 +34,8 @@ using std::list;
 using namespace dcpomatic;
 
 
-TimelineTimeAxisView::TimelineTimeAxisView (Timeline& tl, int y)
-       : TimelineView (tl)
+TimelineTimeAxisView::TimelineTimeAxisView(ContentTimeline& tl, int y)
+       : ContentTimelineView(tl)
        , _y (y)
 {
 
index 4c8e090fe7f5850a376e3eabb9646f61053f661d..f89121776dcb774fa853cf358e9ec44313d0df60 100644 (file)
 
 */
 
-#include "timeline_view.h"
 
-class TimelineTimeAxisView : public TimelineView
+#include "content_timeline_view.h"
+
+
+class TimelineTimeAxisView : public ContentTimelineView
 {
 public:
-       TimelineTimeAxisView (Timeline& tl, int y);
+       TimelineTimeAxisView(ContentTimeline& tl, int y);
 
        dcpomatic::Rect<int> bbox () const override;
        void set_y (int y);
diff --git a/src/wx/timeline_video_content_view.cc b/src/wx/timeline_video_content_view.cc
deleted file mode 100644 (file)
index b0f4b4f..0000000
+++ /dev/null
@@ -1,60 +0,0 @@
-/*
-    Copyright (C) 2013-2019 Carl Hetherington <cth@carlh.net>
-
-    This file is part of DCP-o-matic.
-
-    DCP-o-matic is free software; you can redistribute it and/or modify
-    it under the terms of the GNU General Public License as published by
-    the Free Software Foundation; either version 2 of the License, or
-    (at your option) any later version.
-
-    DCP-o-matic is distributed in the hope that it will be useful,
-    but WITHOUT ANY WARRANTY; without even the implied warranty of
-    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-    GNU General Public License for more details.
-
-    You should have received a copy of the GNU General Public License
-    along with DCP-o-matic.  If not, see <http://www.gnu.org/licenses/>.
-
-*/
-
-#include "lib/image_content.h"
-#include "lib/video_content.h"
-#include "timeline_video_content_view.h"
-
-using std::dynamic_pointer_cast;
-using std::shared_ptr;
-
-TimelineVideoContentView::TimelineVideoContentView (Timeline& tl, shared_ptr<Content> c)
-       : TimelineContentView (tl, c)
-{
-
-}
-
-wxColour
-TimelineVideoContentView::background_colour () const
-{
-       if (!active()) {
-               return wxColour (210, 210, 210, 128);
-       }
-
-       return wxColour (242, 92, 120, 255);
-}
-
-wxColour
-TimelineVideoContentView::foreground_colour () const
-{
-       if (!active()) {
-               return wxColour (180, 180, 180, 128);
-       }
-
-       return wxColour (0, 0, 0, 255);
-}
-
-bool
-TimelineVideoContentView::active () const
-{
-       shared_ptr<Content> c = _content.lock ();
-       DCPOMATIC_ASSERT (c);
-       return c->video && c->video->use();
-}
diff --git a/src/wx/timeline_video_content_view.h b/src/wx/timeline_video_content_view.h
deleted file mode 100644 (file)
index fa8ddf5..0000000
+++ /dev/null
@@ -1,37 +0,0 @@
-/*
-    Copyright (C) 2013-2015 Carl Hetherington <cth@carlh.net>
-
-    This file is part of DCP-o-matic.
-
-    DCP-o-matic is free software; you can redistribute it and/or modify
-    it under the terms of the GNU General Public License as published by
-    the Free Software Foundation; either version 2 of the License, or
-    (at your option) any later version.
-
-    DCP-o-matic is distributed in the hope that it will be useful,
-    but WITHOUT ANY WARRANTY; without even the implied warranty of
-    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-    GNU General Public License for more details.
-
-    You should have received a copy of the GNU General Public License
-    along with DCP-o-matic.  If not, see <http://www.gnu.org/licenses/>.
-
-*/
-
-
-#include "timeline_content_view.h"
-
-
-/** @class TimelineVideoContentView
- *  @brief Timeline view for VideoContent.
- */
-class TimelineVideoContentView : public TimelineContentView
-{
-public:
-       TimelineVideoContentView (Timeline& tl, std::shared_ptr<Content> c);
-
-private:
-       bool active () const override;
-       wxColour background_colour () const override;
-       wxColour foreground_colour () const override;
-};
diff --git a/src/wx/timeline_view.cc b/src/wx/timeline_view.cc
deleted file mode 100644 (file)
index 2897c98..0000000
+++ /dev/null
@@ -1,69 +0,0 @@
-/*
-    Copyright (C) 2013-2021 Carl Hetherington <cth@carlh.net>
-
-    This file is part of DCP-o-matic.
-
-    DCP-o-matic is free software; you can redistribute it and/or modify
-    it under the terms of the GNU General Public License as published by
-    the Free Software Foundation; either version 2 of the License, or
-    (at your option) any later version.
-
-    DCP-o-matic is distributed in the hope that it will be useful,
-    but WITHOUT ANY WARRANTY; without even the implied warranty of
-    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-    GNU General Public License for more details.
-
-    You should have received a copy of the GNU General Public License
-    along with DCP-o-matic.  If not, see <http://www.gnu.org/licenses/>.
-
-*/
-
-
-#include "timeline_view.h"
-#include "timeline.h"
-
-
-using std::list;
-using namespace dcpomatic;
-
-
-/** @class TimelineView
- *  @brief Parent class for components of the timeline (e.g. a piece of content or an axis).
- */
-TimelineView::TimelineView (Timeline& t)
-       : _timeline (t)
-{
-
-}
-
-
-void
-TimelineView::paint (wxGraphicsContext* g, list<dcpomatic::Rect<int>> overlaps)
-{
-       _last_paint_bbox = bbox ();
-       do_paint (g, overlaps);
-}
-
-
-void
-TimelineView::force_redraw ()
-{
-       _timeline.force_redraw (_last_paint_bbox.extended(4));
-       _timeline.force_redraw (bbox().extended(4));
-}
-
-
-int
-TimelineView::time_x (DCPTime t) const
-{
-       return t.seconds() * _timeline.pixels_per_second().get_value_or(0);
-}
-
-
-int
-TimelineView::y_pos(int t) const
-{
-       return t * _timeline.pixels_per_track() + _timeline.tracks_y_offset();
-}
-
-
index 166a1121a80df30432a08979dc11c07be043e647..32eedde09e47765d45bb186c18e6e9f390b0f16b 100644 (file)
@@ -1,5 +1,5 @@
 /*
-    Copyright (C) 2013-2021 Carl Hetherington <cth@carlh.net>
+    Copyright (C) 2023 Carl Hetherington <cth@carlh.net>
 
     This file is part of DCP-o-matic.
 
 
 
 class wxGraphicsContext;
-class Timeline;
 
 
-/** @class TimelineView
- *  @brief Parent class for components of the timeline (e.g. a piece of content or an axis).
+/** @class ContentTimelineView
+ *  @brief Parent class for components of the content timeline (e.g. a piece of content or an axis).
  */
+template <class Timeline>
 class TimelineView
 {
 public:
-       explicit TimelineView (Timeline& t);
-       virtual ~TimelineView () {}
+       explicit TimelineView(Timeline& timeline)
+               : _timeline(timeline)
+       {}
 
-       TimelineView (TimelineView const&) = delete;
-       TimelineView& operator= (TimelineView const&) = delete;
+       virtual ~TimelineView () = default;
 
-       void paint (wxGraphicsContext* g, std::list<dcpomatic::Rect<int>> overlaps);
-       void force_redraw ();
+       TimelineView(TimelineView const&) = delete;
+       TimelineView& operator=(TimelineView const&) = delete;
 
-       virtual dcpomatic::Rect<int> bbox () const = 0;
+       void force_redraw()
+       {
+               _timeline.force_redraw(_last_paint_bbox.extended(4));
+               _timeline.force_redraw(bbox().extended(4));
+       }
 
-protected:
-       virtual void do_paint (wxGraphicsContext *, std::list<dcpomatic::Rect<int>> overlaps) = 0;
+       virtual dcpomatic::Rect<int> bbox() const = 0;
 
-       int time_x (dcpomatic::DCPTime t) const;
-       int y_pos(int t) const;
+protected:
+       int time_x(dcpomatic::DCPTime t) const
+       {
+               return t.seconds() * _timeline.pixels_per_second().get_value_or(0);
+       }
 
        Timeline& _timeline;
-
-private:
        dcpomatic::Rect<int> _last_paint_bbox;
 };
 
 
-typedef std::vector<std::shared_ptr<TimelineView>> TimelineViewList;
-
-
 #endif
+
index 859c0a886c8be59a76445c01a0640dd3b5c3eb38..5a91df8feb4a8fa9ad3e849a530f01e5c22f9687 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,
@@ -203,6 +195,9 @@ VideoPanel::create ()
        _range->Append (_("Video (MPEG, 16-235)"));
 
        _description = new StaticText (this, wxT ("\n \n \n \n \n"), wxDefaultPosition, wxDefaultSize);
+       auto font = _description->GetFont();
+       font.SetStyle(wxFONTSTYLE_ITALIC);
+       font.SetPointSize(font.GetPointSize() - 1);
        _description->SetFont(font);
 
        _left_crop->wrapped()->SetRange (0, 4096);
@@ -223,7 +218,6 @@ VideoPanel::create ()
        _fade_in->Changed.connect (boost::bind (&VideoPanel::fade_in_changed, this));
        _fade_out->Changed.connect (boost::bind (&VideoPanel::fade_out_changed, this));
 
-       _reference->bind(&VideoPanel::reference_clicked, this);
        _scale_fit->Bind                     (wxEVT_RADIOBUTTON, boost::bind (&VideoPanel::scale_fit_clicked, this));
        _scale_custom->Bind                  (wxEVT_RADIOBUTTON, boost::bind (&VideoPanel::scale_custom_clicked, this));
        _scale_custom_edit->Bind             (wxEVT_BUTTON,   boost::bind (&VideoPanel::scale_custom_edit_clicked, this));
@@ -244,12 +238,6 @@ VideoPanel::add_to_grid ()
 {
        int r = 0;
 
-       auto reference_sizer = new wxBoxSizer (wxVERTICAL);
-       reference_sizer->Add (_reference, 0);
-       reference_sizer->Add (_reference_note, 0);
-       _grid->Add (reference_sizer, wxGBPosition(r, 0), wxGBSpan(1, 3));
-       ++r;
-
        add_label_to_sizer (_grid, _type_label, true, wxGBPosition(r, 0));
        _frame_type->add (_grid, wxGBPosition(r, 1), wxGBSpan(1, 2));
        ++r;
@@ -458,15 +446,6 @@ VideoPanel::film_content_changed (int property)
                } else {
                        _fade_out->clear ();
                }
-       } else if (property == DCPContentProperty::REFERENCE_VIDEO) {
-               if (vc.size() == 1) {
-                       shared_ptr<DCPContent> dcp = dynamic_pointer_cast<DCPContent> (vc.front ());
-                       checked_set (_reference, dcp ? dcp->reference_video () : false);
-               } else {
-                       checked_set (_reference, false);
-               }
-
-               setup_sensitivity ();
        } else if (property == VideoContentProperty::RANGE) {
                if (vcs) {
                        checked_set (_range, vcs->video->range() == VideoRange::FULL ? 0 : 1);
@@ -594,15 +573,7 @@ VideoPanel::setup_sensitivity ()
                dcp = dynamic_pointer_cast<DCPContent> (sel.front ());
        }
 
-       string why_not;
-       bool const can_reference = dcp && dcp->can_reference_video (_parent->film(), why_not);
-       wxString cannot;
-       if (why_not.empty()) {
-               cannot = _("Cannot reference this DCP's video.");
-       } else {
-               cannot = _("Cannot reference this DCP's video: ") + std_to_wx(why_not);
-       }
-       setup_refer_button (_reference, _reference_note, dcp, can_reference, cannot);
+       bool const reference = dcp && dcp->reference_video();
 
        bool any_use = false;
        for (auto i: _parent->selected_video()) {
@@ -611,7 +582,7 @@ VideoPanel::setup_sensitivity ()
                }
        }
 
-       bool const enable = !_reference->GetValue() && any_use;
+       bool const enable = !reference && any_use;
 
        if (!enable) {
                _frame_type->wrapped()->Enable (false);
@@ -683,23 +654,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..cf05dfe7dbab731083361435c9e2d64e4f0251be 100644 (file)
@@ -48,12 +48,23 @@ sources = """
           content_panel.cc
           content_properties_dialog.cc
           content_sub_panel.cc
+          content_timeline.cc
+          content_timeline_atmos_view.cc
+          content_timeline_audio_view.cc
+          content_timeline_dialog.cc
+          content_timeline_text_view.cc
+          content_timeline_video_view.cc
+          content_timeline_view.cc
           content_version_dialog.cc
           content_view.cc
           controls.cc
           credentials_download_certificate_panel.cc
           custom_scale_dialog.cc
+          dcp_referencing_dialog.cc
           dcp_panel.cc
+          dcp_timeline.cc
+          dcp_timeline_dialog.cc
+          dcp_timeline_reel_marker_view.cc
           dcp_text_track_dialog.cc
           dcpomatic_button.cc
           dcpomatic_choice.cc
@@ -160,16 +171,10 @@ sources = """
           timer_display.cc
           timecode.cc
           timeline.cc
-          timeline_atmos_content_view.cc
           timeline_content_view.cc
-          timeline_dialog.cc
-          timeline_audio_content_view.cc
           timeline_labels_view.cc
-          timeline_text_content_view.cc
           timeline_reels_view.cc
           timeline_time_axis_view.cc
-          timeline_video_content_view.cc
-          timeline_view.cc
           timing_panel.cc
           try_unmount_dialog.cc
           update_dialog.cc
index 66b01640c5b1175583fee8898c5f7bc11335a88a..d8935daa195d1523f70a3f98ae6c3b49d6dd9fa7 100644 (file)
@@ -56,7 +56,7 @@ class PasswordEntry;
 #define DCPOMATIC_SIZER_GAP 8
 #define DCPOMATIC_DIALOG_BORDER 12
 #ifdef __WXGTK3__
-#define DCPOMATIC_SPIN_CTRL_WIDTH 118
+#define DCPOMATIC_SPIN_CTRL_WIDTH 124
 #else
 #define DCPOMATIC_SPIN_CTRL_WIDTH 56
 #endif
@@ -74,7 +74,7 @@ class PasswordEntry;
 
 /** Amount by which you need to top-pad a choice to make it line up, in some cases */
 #ifdef DCPOMATIC_OSX
-#define DCPOMATIC_CHOICE_TOP_PAD 1
+#define DCPOMATIC_CHOICE_TOP_PAD 2
 #else
 #define DCPOMATIC_CHOICE_TOP_PAD 0
 #endif
index 5530a05d05f3b8d3a0869b57d0eed3946974e819..7f4e98aa736d11111ae75f9cca3730887fbc0c03 100644 (file)
@@ -49,7 +49,8 @@ has_mxf_mca_subdescriptors(shared_ptr<const Film> film)
         * whether they exist.
         */
 
-       ASDCP::PCM::MXFReader reader;
+       Kumu::FileReaderFactory factory;
+       ASDCP::PCM::MXFReader reader(factory);
        auto r = reader.OpenRead(find_file(film->dir(film->dcp_name()), "pcm_").string());
        BOOST_REQUIRE(!ASDCP_FAILURE(r));
 
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);
 }
 
 
index 878e6025422208c851e530953c41dc090d7b1925..2bd60dc81071e0b2180d66060c0f0cab40b8be45 100644 (file)
@@ -66,7 +66,7 @@ BOOST_AUTO_TEST_CASE (film_metadata_test)
        film->set_audio_channels(6);
        film->write_metadata ();
 
-       list<string> ignore = { "Key", "ContextID", "LastWrittenBy" };
+       list<Glib::ustring> ignore = { "Key", "ContextID", "LastWrittenBy" };
        check_xml ("test/data/metadata.xml.ref", dir.string() + "/metadata.xml", ignore);
 
        auto g = make_shared<Film>(dir);
index eb662a120e14da3c86f26b8330b0f3e57d44af92..29b550753e45c32c950ee0a336137391ab054107 100644 (file)
 #include "lib/image_filename_sorter.h"
 #include "lib/compose.hpp"
 #include <boost/test/unit_test.hpp>
+#include <algorithm>
+#include <random>
 
 
-using std::random_shuffle;
 using std::sort;
 using std::vector;
 
@@ -69,7 +70,11 @@ BOOST_AUTO_TEST_CASE (image_filename_sorter_test2)
        for (int i = 0; i < 100000; ++i) {
                paths.push_back(String::compose("some.filename.with.%1.number.tiff", i));
        }
-       random_shuffle (paths.begin(), paths.end());
+
+       std::random_device rd;
+       std::mt19937 generator(rd());
+       std::shuffle(paths.begin(), paths.end(), generator);
+
        sort (paths.begin(), paths.end(), ImageFilenameSorter());
        for (int i = 0; i < 100000; ++i) {
                BOOST_CHECK_EQUAL(paths[i].string(), String::compose("some.filename.with.%1.number.tiff", i));
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 57322d3ac3379e4d1dbb0434c21998892c368f9d..70d38c1ebcef63dab0cc8d9989112cd2c7b29eba 100644 (file)
@@ -48,5 +48,9 @@ BOOST_AUTO_TEST_CASE (remake_with_subtitle_test)
        content->only_text()->set_use (false);
        make_and_verify_dcp (film);
 
-       check_one_frame (film->dir(film->dcp_name()), 325, TestPaths::private_data() / "prophet_frame_325_no_subs.j2c");
+#ifdef DCPOMATIC_OSX
+       check_one_frame(film->dir(film->dcp_name()), 325, TestPaths::private_data() / "v2.18.x" / "prophet_frame_325_no_subs_mac.j2c");
+#else
+       check_one_frame(film->dir(film->dcp_name()), 325, TestPaths::private_data() / "v2.18.x" / "prophet_frame_325_no_subs.j2c");
+#endif
 }
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 a180739ab5f8e84fe9cd1223731824345d74263b..ecd2deac8fc3c4275544ad5e917444e170143033 100644 (file)
@@ -43,38 +43,38 @@ BOOST_AUTO_TEST_CASE (stream_test)
 {
        xmlpp::Document doc;
        auto root = doc.create_root_node("FFmpegAudioStream");
-       root->add_child("Name")->add_child_text ("hello there world");
-       root->add_child("Id")->add_child_text ("4");
-       root->add_child("FrameRate")->add_child_text ("44100");
-       root->add_child("Channels")->add_child_text ("2");
+       cxml::add_text_child(root, "Name", "hello there world");
+       cxml::add_text_child(root, "Id", "4");
+       cxml::add_text_child(root, "FrameRate", "44100");
+       cxml::add_text_child(root, "Channels", "2");
 
        /* This is the state file version 5 description of the mapping */
 
-       auto mapping = root->add_child("Mapping");
-       mapping->add_child("ContentChannels")->add_child_text ("2");
+       auto mapping = cxml::add_child(root, "Mapping");
+       cxml::add_text_child(mapping, "ContentChannels", "2");
        {
                /* L -> L */
-               auto map = mapping->add_child("Map");
-               map->add_child("ContentIndex")->add_child_text ("0");
-               map->add_child("DCP")->add_child_text ("0");
+               auto map = cxml::add_child(mapping, "Map");
+               cxml::add_text_child(map, "ContentIndex", "0");
+               cxml::add_text_child(map, "DCP", "0");
        }
        {
                /* L -> C */
-               auto map = mapping->add_child("Map");
-               map->add_child("ContentIndex")->add_child_text ("0");
-               map->add_child("DCP")->add_child_text ("2");
+               auto map = cxml::add_child(mapping, "Map");
+               cxml::add_text_child(map, "ContentIndex", "0");
+               cxml::add_text_child(map, "DCP", "2");
        }
        {
                /* R -> R */
-               auto map = mapping->add_child("Map");
-               map->add_child("ContentIndex")->add_child_text ("1");
-               map->add_child("DCP")->add_child_text ("1");
+               auto map = cxml::add_child(mapping, "Map");
+               cxml::add_text_child(map, "ContentIndex", "1");
+               cxml::add_text_child(map, "DCP", "1");
        }
        {
                /* R -> C */
-               auto map = mapping->add_child("Map");
-               map->add_child("ContentIndex")->add_child_text ("1");
-               map->add_child("DCP")->add_child_text ("2");
+               auto map = cxml::add_child(mapping, "Map");
+               cxml::add_text_child(map, "ContentIndex", "1");
+               cxml::add_text_child(map, "DCP", "2");
        }
 
        FFmpegAudioStream a (cxml::NodePtr (new cxml::Node (root)), 5);
index fc5d9dc83b9d2263f47eccae4cb8e6d02a928d2f..ff4a3c9c0cafd8f223796c35bd5d36df0ddd05d4 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 ()
@@ -277,13 +287,14 @@ check_wav_file (boost::filesystem::path ref, boost::filesystem::path check)
 void
 check_mxf_audio_file (boost::filesystem::path ref, boost::filesystem::path check)
 {
-       ASDCP::PCM::MXFReader ref_reader;
+       Kumu::FileReaderFactory factory;
+       ASDCP::PCM::MXFReader ref_reader(factory);
        BOOST_REQUIRE (!ASDCP_FAILURE (ref_reader.OpenRead (ref.string().c_str())));
 
        ASDCP::PCM::AudioDescriptor ref_desc;
        BOOST_REQUIRE (!ASDCP_FAILURE (ref_reader.FillAudioDescriptor (ref_desc)));
 
-       ASDCP::PCM::MXFReader check_reader;
+       ASDCP::PCM::MXFReader check_reader(factory);
        BOOST_REQUIRE (!ASDCP_FAILURE (check_reader.OpenRead (check.string().c_str())));
 
        ASDCP::PCM::AudioDescriptor check_desc;
@@ -306,13 +317,14 @@ check_mxf_audio_file (boost::filesystem::path ref, boost::filesystem::path check
 bool
 mxf_atmos_files_same (boost::filesystem::path ref, boost::filesystem::path check, bool verbose)
 {
-       ASDCP::ATMOS::MXFReader ref_reader;
+       Kumu::FileReaderFactory factory;
+       ASDCP::ATMOS::MXFReader ref_reader(factory);
        BOOST_REQUIRE (!ASDCP_FAILURE(ref_reader.OpenRead(ref.string().c_str())));
 
        ASDCP::ATMOS::AtmosDescriptor ref_desc;
        BOOST_REQUIRE (!ASDCP_FAILURE(ref_reader.FillAtmosDescriptor(ref_desc)));
 
-       ASDCP::ATMOS::MXFReader check_reader;
+       ASDCP::ATMOS::MXFReader check_reader(factory);
        BOOST_REQUIRE (!ASDCP_FAILURE(check_reader.OpenRead(check.string().c_str())));
 
        ASDCP::ATMOS::AtmosDescriptor check_desc;
@@ -574,7 +586,7 @@ check_dcp(boost::filesystem::path ref, boost::filesystem::path check, bool sound
 }
 
 void
-check_xml (xmlpp::Element* ref, xmlpp::Element* test, list<string> ignore)
+check_xml(xmlpp::Element* ref, xmlpp::Element* test, list<Glib::ustring> ignore)
 {
        BOOST_CHECK_EQUAL (ref->get_name (), test->get_name ());
        BOOST_CHECK_EQUAL (ref->get_namespace_prefix (), test->get_namespace_prefix ());
@@ -630,7 +642,7 @@ check_xml (xmlpp::Element* ref, xmlpp::Element* test, list<string> ignore)
 }
 
 void
-check_xml (boost::filesystem::path ref, boost::filesystem::path test, list<string> ignore)
+check_xml(boost::filesystem::path ref, boost::filesystem::path test, list<Glib::ustring> ignore)
 {
        auto ref_parser = new xmlpp::DomParser(ref.string());
        auto ref_root = ref_parser->get_document()->get_root_node();
@@ -820,19 +832,22 @@ check_one_frame (boost::filesystem::path dcp_dir, int64_t index, boost::filesyst
        auto asset = dynamic_pointer_cast<dcp::MonoPictureAsset> (dcp.cpls().front()->reels().front()->main_picture()->asset());
        BOOST_REQUIRE (asset);
        auto frame = asset->start_read()->get_frame(index);
-       auto ref_frame (new dcp::MonoPictureFrame (ref));
+       dcp::MonoPictureFrame ref_frame(ref);
 
        auto image = frame->xyz_image ();
-       auto ref_image = ref_frame->xyz_image ();
+       auto ref_image = ref_frame.xyz_image();
 
        BOOST_REQUIRE (image->size() == ref_image->size());
 
        int off = 0;
        for (int y = 0; y < ref_image->size().height; ++y) {
                for (int x = 0; x < ref_image->size().width; ++x) {
-                       BOOST_REQUIRE_EQUAL (ref_image->data(0)[off], image->data(0)[off]);
-                       BOOST_REQUIRE_EQUAL (ref_image->data(1)[off], image->data(1)[off]);
-                       BOOST_REQUIRE_EQUAL (ref_image->data(2)[off], image->data(2)[off]);
+                       auto x_error = std::abs(ref_image->data(0)[off] - image->data(0)[off]);
+                       BOOST_REQUIRE_MESSAGE(x_error == 0, "x component at " << x << "," << y << " differs by " << x_error);
+                       auto y_error = std::abs(ref_image->data(1)[off] - image->data(1)[off]);
+                       BOOST_REQUIRE_MESSAGE(y_error == 0, "y component at " << x << "," << y << " differs by " << y_error);
+                       auto z_error = std::abs(ref_image->data(2)[off] - image->data(2)[off]);
+                       BOOST_REQUIRE_MESSAGE(z_error == 0, "z component at " << x << "," << y << " differs by " << z_error);
                        ++off;
                }
        }
index 6687affea6b41c1624db524e555589c58dc95f23..0776d6e5ae963436b6a577b2b979f11590b11606 100644 (file)
@@ -22,6 +22,7 @@
 #include "lib/video_frame_type.h"
 #include <dcp/types.h>
 #include <dcp/verify.h>
+#include <glibmm.h>
 #include <boost/filesystem.hpp>
 #include <vector>
 
@@ -66,7 +67,7 @@ extern void check_text_file (boost::filesystem::path ref, boost::filesystem::pat
 extern void check_wav_file (boost::filesystem::path ref, boost::filesystem::path check);
 extern void check_mxf_audio_file (boost::filesystem::path ref, boost::filesystem::path check);
 extern bool mxf_atmos_files_same (boost::filesystem::path ref, boost::filesystem::path check, bool verbose = false);
-extern void check_xml (boost::filesystem::path, boost::filesystem::path, std::list<std::string>);
+extern void check_xml(boost::filesystem::path, boost::filesystem::path, std::list<Glib::ustring>);
 extern void check_ffmpeg (boost::filesystem::path, boost::filesystem::path, int audio_tolerance);
 extern void check_image (boost::filesystem::path, boost::filesystem::path, double threshold = 4);
 extern boost::filesystem::path test_film_dir (std::string);
diff --git a/test/text_entry_point_test.cc b/test/text_entry_point_test.cc
new file mode 100644 (file)
index 0000000..ce4f614
--- /dev/null
@@ -0,0 +1,70 @@
+/*
+    Copyright (C) 2024 Carl Hetherington <cth@carlh.net>
+
+    This file is part of DCP-o-matic.
+
+    DCP-o-matic is free software; you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or
+    (at your option) any later version.
+
+    DCP-o-matic is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with DCP-o-matic.  If not, see <http://www.gnu.org/licenses/>.
+
+*/
+
+
+#include "test.h"
+#include "lib/dcp_content.h"
+#include "lib/film.h"
+#include <dcp/cpl.h>
+#include <dcp/dcp.h>
+#include <dcp/reel.h>
+#include <dcp/reel_smpte_subtitle_asset.h>
+#include <dcp/smpte_subtitle_asset.h>
+#include <boost/test/unit_test.hpp>
+
+
+using std::make_shared;
+using std::string;
+
+
+BOOST_AUTO_TEST_CASE(test_text_entry_point)
+{
+       auto const path = boost::filesystem::path("build/test/test_text_entry_point");
+       boost::filesystem::remove_all(path);
+       boost::filesystem::create_directories(path);
+
+       /* Make a "bad" DCP with a non-zero text entry point */
+       dcp::DCP bad_dcp(path / "dcp");
+       auto sub = make_shared<dcp::SMPTESubtitleAsset>();
+       sub->write(path / "dcp" / "subs.mxf");
+       auto reel_sub = make_shared<dcp::ReelSMPTESubtitleAsset>(sub, dcp::Fraction{24, 1}, 42, 6);
+       auto reel = make_shared<dcp::Reel>();
+       reel->add(reel_sub);
+
+       auto cpl = make_shared<dcp::CPL>("foo", dcp::ContentKind::FEATURE, dcp::Standard::SMPTE);
+       bad_dcp.add(cpl);
+       cpl->add(reel);
+
+       bad_dcp.write_xml();
+
+       /* Make a film and add the bad DCP, so that the examiner spots the problem */
+       auto dcp_content = make_shared<DCPContent>(path / "dcp");
+       auto film = new_test_film2("test_text_entry_point/film", { dcp_content });
+       film->write_metadata();
+
+       /* Reload the film to check that the examiner's output is saved and recovered */
+       auto film2 = make_shared<Film>(path / "film");
+       film2->read_metadata();
+
+       string why_not;
+       BOOST_CHECK(!dcp_content->can_reference_text(film2, TextType::OPEN_SUBTITLE, why_not));
+       BOOST_CHECK_EQUAL(why_not, "one of its subtitle reels has a non-zero entry point so it must be re-written.");
+}
+
index 0c781fdb1218faa4e6c5d0d0f72747a6a59cb94d..8e6a516bd196cb463522fbeeb77245d82058493f 100644 (file)
@@ -248,7 +248,7 @@ BOOST_AUTO_TEST_CASE (torture_test1)
                        for (int c = 0; c < 3; ++c) {
                                for (int y = 0; y < size.height; ++y) {
                                        for (int x = 0; x < size.width; ++x) {
-                                               BOOST_REQUIRE (image->data(c)[y * size.height + x] <= 3);
+                                               BOOST_REQUIRE (image->data(c)[y * size.height + x] <= 5);
                                        }
                                }
                        }
index e2419d8e734c96f0ef92add497afb2062a0d5f75..0ff2c47321231e0f27ff0ed154893fe561ae69ef 100644 (file)
@@ -480,7 +480,7 @@ BOOST_AUTO_TEST_CASE (movie_V_to_dcp)
 {
        auto range = dcp_range (movie_V("movie_V_to_dcp"));
        /* Video range has been correctly expanded to full for the DCP */
-       check_int_close (range, {0, 4083}, 2);
+       check_int_close(range, {0, 4081}, 2);
 }
 
 
@@ -498,7 +498,7 @@ BOOST_AUTO_TEST_CASE (movie_F_to_dcp)
 {
        auto range = dcp_range (movie_F("movie_F_to_dcp"));
        /* The nearly-full-range of the input has been preserved */
-       check_int_close (range, {0, 4083}, 2);
+       check_int_close(range, {0, 4080}, 2);
 }
 
 
@@ -506,14 +506,14 @@ BOOST_AUTO_TEST_CASE (video_FoV_to_dcp)
 {
        auto range = dcp_range (movie_FoV("video_FoV_to_dcp"));
        /* The nearly-full-range of the input has become even more full, and clipped */
-       check_int_close (range, {0, 4095}, 2);
+       check_int_close(range, {0, 4093}, 2);
 }
 
 
 BOOST_AUTO_TEST_CASE (image_F_to_dcp)
 {
        auto range = dcp_range (image_F("image_F_to_dcp"));
-       check_int_close (range, {0, 4083}, 3);
+       check_int_close(range, {0, 4080}, 3);
 }
 
 
index bb66d1a5f952ff07d115ac63c9d647e6346829a6..eb89013383a539832661088cdb1c4665e37f79a7 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
@@ -161,6 +162,7 @@ def build(bld):
                  template_test.cc
                  test.cc
                  text_decoder_test.cc
+                 text_entry_point_test.cc
                  threed_test.cc
                  time_calculation_test.cc
                  torture_test.cc
diff --git a/wscript b/wscript
index f6f73a52a132701aa667dfe169270935e817681e..19fd4260c2c6c9a6a7598d8ff2c54e64500c82e6 100644 (file)
--- a/wscript
+++ b/wscript
@@ -75,10 +75,12 @@ 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')
     opt.add_option('--disable-more-warnings', action='store_true', default=False, help='disable some warnings raised by Xcode 15 with the 2.16 branch')
+    opt.add_option('--c++17', action='store_true', default=False, help='build with C++17 and libxml++-4.0')
 
 def configure(conf):
     conf.load('compiler_cxx')
@@ -86,6 +88,17 @@ def configure(conf):
     if conf.options.target_windows_64 or conf.options.target_windows_32:
         conf.load('winres')
 
+    if vars(conf.options)['c++17']:
+        cpp_std = '17'
+        conf.env.XMLPP_API = '4.0'
+        conf.env.PANGOMM_API = '2.48'
+        conf.env.CAIROMM_API = '1.16'
+    else:
+        cpp_std = '11'
+        conf.env.XMLPP_API = '2.6'
+        conf.env.PANGOMM_API = '1.4'
+        conf.env.CAIROMM_API = '1.0'
+
     # Save conf.options that we need elsewhere in conf.env
     conf.env.DISABLE_GUI = conf.options.disable_gui
     conf.env.DISABLE_TESTS = conf.options.disable_tests
@@ -97,6 +110,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:
@@ -112,11 +126,13 @@ 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',
                                        '-D_FILE_OFFSET_BITS=64',
-                                       '-std=c++11'])
+                                       '-std=c++' + cpp_std])
 
     if conf.options.disable_more_warnings:
         # These are for Xcode 15.0.1 with the v2.16.x-era
@@ -144,8 +160,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:
@@ -156,6 +174,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')
@@ -318,10 +339,10 @@ def configure(conf):
     conf.check_cfg(package='fontconfig', args='--cflags --libs', uselib_store='FONTCONFIG', mandatory=True)
 
     # pangomm
-    conf.check_cfg(package='pangomm-1.4', args='--cflags --libs', uselib_store='PANGOMM', mandatory=True)
+    conf.check_cfg(package='pangomm-' + conf.env.PANGOMM_API, args='--cflags --libs', uselib_store='PANGOMM', mandatory=True)
 
     # cairomm
-    conf.check_cfg(package='cairomm-1.0', args='--cflags --libs', uselib_store='CAIROMM', mandatory=True)
+    conf.check_cfg(package='cairomm-' + conf.env.CAIROMM_API, args='--cflags --libs', uselib_store='CAIROMM', mandatory=True)
 
     # leqm_nrt
     conf.check_cfg(package='leqm_nrt', args='--cflags --libs', uselib_store='LEQM_NRT', mandatory=True)
@@ -355,7 +376,7 @@ def configure(conf):
     if conf.options.static_dcp:
         conf.check_cfg(package='libdcp-1.0', args='libdcp-1.0 >= %s --cflags' % libdcp_version, uselib_store='DCP', mandatory=True)
         conf.env.DEFINES_DCP = [f.replace('\\', '') for f in conf.env.DEFINES_DCP]
-        conf.env.STLIB_DCP = ['dcp-1.0', 'asdcp-carl', 'kumu-carl', 'openjp2']
+        conf.env.STLIB_DCP = ['dcp-1.0', 'asdcp-dcpomatic', 'kumu-dcpomatic', 'openjp2']
         conf.env.LIB_DCP = ['glibmm-2.4', 'ssl', 'crypto', 'bz2', 'xslt', 'xerces-c']
     else:
         conf.check_cfg(package='libdcp-1.0', args='libdcp-1.0 >= %s --cflags --libs' % libdcp_version, uselib_store='DCP', mandatory=True)
@@ -372,10 +393,10 @@ def configure(conf):
 
     # libxml++
     if conf.options.static_xmlpp:
-        conf.env.STLIB_XMLPP = ['xml++-2.6']
+        conf.env.STLIB_XMLPP = ['xml++-' + conf.env.XMLPP_API]
         conf.env.LIB_XMLPP = ['xml2']
     else:
-        conf.check_cfg(package='libxml++-2.6', args='--cflags --libs', uselib_store='XMLPP', mandatory=True)
+        conf.check_cfg(package='libxml++-' + conf.env.XMLPP_API, args='--cflags --libs', uselib_store='XMLPP', mandatory=True)
 
     # libxmlsec
     if conf.options.static_xmlsec:
@@ -467,7 +488,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)
 
@@ -651,6 +672,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')