summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/lib/analytics.cc22
-rw-r--r--src/lib/atmos_content.cc6
-rw-r--r--src/lib/atmos_content.h2
-rw-r--r--src/lib/atmos_mxf_content.cc8
-rw-r--r--src/lib/atmos_mxf_content.h2
-rw-r--r--src/lib/audio_analysis.cc33
-rw-r--r--src/lib/audio_content.cc12
-rw-r--r--src/lib/audio_content.h2
-rw-r--r--src/lib/audio_filter_graph.cc19
-rw-r--r--src/lib/audio_filter_graph.h3
-rw-r--r--src/lib/audio_mapping.cc17
-rw-r--r--src/lib/audio_mapping.h2
-rw-r--r--src/lib/audio_point.cc4
-rw-r--r--src/lib/butler.cc2
-rw-r--r--src/lib/check_content_job.cc2
-rw-r--r--src/lib/cinema.cc112
-rw-r--r--src/lib/cinema.h59
-rw-r--r--src/lib/cinema_list.cc466
-rw-r--r--src/lib/cinema_list.h126
-rw-r--r--src/lib/colour_conversion.cc49
-rw-r--r--src/lib/colour_conversion.h2
-rw-r--r--src/lib/config.cc617
-rw-r--r--src/lib/config.h169
-rw-r--r--src/lib/constants.h2
-rw-r--r--src/lib/content.cc14
-rw-r--r--src/lib/content.h2
-rw-r--r--src/lib/content_video.h11
-rw-r--r--src/lib/cpu_j2k_encoder_thread.cc62
-rw-r--r--src/lib/cpu_j2k_encoder_thread.h16
-rw-r--r--src/lib/create_cli.cc21
-rw-r--r--src/lib/create_cli.h2
-rw-r--r--src/lib/crop.cc10
-rw-r--r--src/lib/crop.h2
-rw-r--r--src/lib/cross.h16
-rw-r--r--src/lib/cross_common.cc107
-rw-r--r--src/lib/cross_osx.cc120
-rw-r--r--src/lib/dcp_content.cc214
-rw-r--r--src/lib/dcp_content.h19
-rw-r--r--src/lib/dcp_content_type.cc3
-rw-r--r--src/lib/dcp_decoder.cc123
-rw-r--r--src/lib/dcp_decoder.h18
-rw-r--r--src/lib/dcp_digest_file.cc28
-rw-r--r--src/lib/dcp_examiner.cc59
-rw-r--r--src/lib/dcp_examiner.h13
-rw-r--r--src/lib/dcp_film_encoder.cc (renamed from src/lib/dcp_encoder.cc)99
-rw-r--r--src/lib/dcp_film_encoder.h (renamed from src/lib/dcp_encoder.h)19
-rw-r--r--src/lib/dcp_subtitle_content.cc10
-rw-r--r--src/lib/dcp_subtitle_content.h2
-rw-r--r--src/lib/dcp_text_track.cc4
-rw-r--r--src/lib/dcp_video.cc46
-rw-r--r--src/lib/dcp_video.h11
-rw-r--r--src/lib/dcpomatic_time.cc19
-rw-r--r--src/lib/dcpomatic_time.h7
-rw-r--r--src/lib/dkdm_recipient.cc49
-rw-r--r--src/lib/dkdm_recipient.h18
-rw-r--r--src/lib/dkdm_recipient_list.cc243
-rw-r--r--src/lib/dkdm_recipient_list.h90
-rw-r--r--src/lib/dkdm_wrapper.cc16
-rw-r--r--src/lib/email.cc6
-rw-r--r--src/lib/email.h1
-rw-r--r--src/lib/encode_server.cc18
-rw-r--r--src/lib/encode_server.h6
-rw-r--r--src/lib/encode_server_finder.cc4
-rw-r--r--src/lib/encode_server_finder.h3
-rw-r--r--src/lib/environment_info.cc7
-rw-r--r--src/lib/exceptions.h55
-rw-r--r--src/lib/export_config.cc12
-rw-r--r--src/lib/export_config.h2
-rw-r--r--src/lib/ffmpeg_audio_stream.cc14
-rw-r--r--src/lib/ffmpeg_audio_stream.h2
-rw-r--r--src/lib/ffmpeg_content.cc35
-rw-r--r--src/lib/ffmpeg_content.h2
-rw-r--r--src/lib/ffmpeg_decoder.cc9
-rw-r--r--src/lib/ffmpeg_examiner.cc21
-rw-r--r--src/lib/ffmpeg_file_encoder.cc7
-rw-r--r--src/lib/ffmpeg_file_encoder.h4
-rw-r--r--src/lib/ffmpeg_film_encoder.cc (renamed from src/lib/ffmpeg_encoder.cc)24
-rw-r--r--src/lib/ffmpeg_film_encoder.h (renamed from src/lib/ffmpeg_encoder.h)6
-rw-r--r--src/lib/ffmpeg_image_proxy.cc4
-rw-r--r--src/lib/ffmpeg_image_proxy.h4
-rw-r--r--src/lib/ffmpeg_stream.cc6
-rw-r--r--src/lib/ffmpeg_stream.h2
-rw-r--r--src/lib/ffmpeg_subtitle_stream.cc10
-rw-r--r--src/lib/ffmpeg_subtitle_stream.h2
-rw-r--r--src/lib/film.cc414
-rw-r--r--src/lib/film.h49
-rw-r--r--src/lib/film_encoder.cc (renamed from src/lib/encoder.cc)8
-rw-r--r--src/lib/film_encoder.h (renamed from src/lib/encoder.h)19
-rw-r--r--src/lib/film_property.h4
-rw-r--r--src/lib/filter.cc3
-rw-r--r--src/lib/font.cc6
-rw-r--r--src/lib/font.h2
-rw-r--r--src/lib/font_id_allocator.cc30
-rw-r--r--src/lib/font_id_allocator.h2
-rw-r--r--src/lib/frame_info.cc84
-rw-r--r--src/lib/frame_info.h45
-rw-r--r--src/lib/grok/context.h291
-rw-r--r--src/lib/grok/messenger.h906
-rw-r--r--src/lib/grok_j2k_encoder_thread.cc72
-rw-r--r--src/lib/grok_j2k_encoder_thread.h41
-rw-r--r--src/lib/hints.cc62
-rw-r--r--src/lib/hints.h12
-rw-r--r--src/lib/id.cc30
-rw-r--r--src/lib/id.h48
-rw-r--r--src/lib/image_content.cc8
-rw-r--r--src/lib/image_content.h2
-rw-r--r--src/lib/image_decoder.cc2
-rw-r--r--src/lib/image_proxy.h4
-rw-r--r--src/lib/j2k_encoder.cc416
-rw-r--r--src/lib/j2k_encoder.h52
-rw-r--r--src/lib/j2k_encoder_thread.cc58
-rw-r--r--src/lib/j2k_encoder_thread.h53
-rw-r--r--src/lib/j2k_image_proxy.cc20
-rw-r--r--src/lib/j2k_image_proxy.h10
-rw-r--r--src/lib/j2k_sync_encoder_thread.cc65
-rw-r--r--src/lib/j2k_sync_encoder_thread.h32
-rw-r--r--src/lib/job.cc38
-rw-r--r--src/lib/job.h3
-rw-r--r--src/lib/kdm_cli.cc297
-rw-r--r--src/lib/kdm_recipient.cc8
-rw-r--r--src/lib/kdm_with_metadata.h7
-rw-r--r--src/lib/make_dcp.cc17
-rw-r--r--src/lib/make_dcp.h2
-rw-r--r--src/lib/map_cli.cc4
-rw-r--r--src/lib/mpeg2_encoder.cc80
-rw-r--r--src/lib/mpeg2_encoder.h42
-rw-r--r--src/lib/overlaps.cc3
-rw-r--r--src/lib/overlaps.h7
-rw-r--r--src/lib/pixel_quanta.cc4
-rw-r--r--src/lib/player.cc272
-rw-r--r--src/lib/player.h11
-rw-r--r--src/lib/player_video.cc45
-rw-r--r--src/lib/player_video.h8
-rw-r--r--src/lib/playlist.cc10
-rw-r--r--src/lib/playlist.h4
-rw-r--r--src/lib/raw_image_proxy.cc10
-rw-r--r--src/lib/raw_image_proxy.h2
-rw-r--r--src/lib/reel_writer.cc261
-rw-r--r--src/lib/reel_writer.h40
-rw-r--r--src/lib/release_notes.cc3
-rw-r--r--src/lib/remembered_asset.cc97
-rw-r--r--src/lib/remembered_asset.h79
-rw-r--r--src/lib/remote_j2k_encoder_thread.cc84
-rw-r--r--src/lib/remote_j2k_encoder_thread.h21
-rw-r--r--src/lib/render_text.cc54
-rw-r--r--src/lib/rgba.cc10
-rw-r--r--src/lib/rgba.h2
-rw-r--r--src/lib/screen.cc58
-rw-r--r--src/lib/screen.h17
-rw-r--r--src/lib/send_problem_report_job.cc11
-rw-r--r--src/lib/shuffler.cc18
-rw-r--r--src/lib/spl.cc6
-rw-r--r--src/lib/spl_entry.cc2
-rw-r--r--src/lib/sqlite_statement.cc111
-rw-r--r--src/lib/sqlite_statement.h50
-rw-r--r--src/lib/sqlite_table.cc79
-rw-r--r--src/lib/sqlite_table.h54
-rw-r--r--src/lib/sqlite_transaction.cc50
-rw-r--r--src/lib/sqlite_transaction.h40
-rw-r--r--src/lib/state.cc2
-rw-r--r--src/lib/string_text_file_content.cc10
-rw-r--r--src/lib/string_text_file_content.h2
-rw-r--r--src/lib/subtitle_analysis.cc16
-rw-r--r--src/lib/subtitle_film_encoder.cc (renamed from src/lib/subtitle_encoder.cc)14
-rw-r--r--src/lib/subtitle_film_encoder.h (renamed from src/lib/subtitle_encoder.h)8
-rw-r--r--src/lib/text_content.cc80
-rw-r--r--src/lib/text_content.h6
-rw-r--r--src/lib/text_decoder.cc5
-rw-r--r--src/lib/transcode_job.cc28
-rw-r--r--src/lib/transcode_job.h12
-rw-r--r--src/lib/types.h6
-rw-r--r--src/lib/unzipper.cc14
-rw-r--r--src/lib/unzipper.h3
-rw-r--r--src/lib/util.cc84
-rw-r--r--src/lib/util.h19
-rw-r--r--src/lib/variant.cc172
-rw-r--r--src/lib/variant.h55
-rw-r--r--src/lib/verify_dcp_job.cc4
-rw-r--r--src/lib/verify_dcp_job.h6
-rw-r--r--src/lib/video_content.cc58
-rw-r--r--src/lib/video_content.h4
-rw-r--r--src/lib/video_decoder.cc111
-rw-r--r--src/lib/video_decoder.h2
-rw-r--r--src/lib/video_encoder.cc64
-rw-r--r--src/lib/video_encoder.h69
-rw-r--r--src/lib/video_encoding.cc58
-rw-r--r--src/lib/video_encoding.h42
-rw-r--r--src/lib/video_mxf_content.cc16
-rw-r--r--src/lib/video_mxf_content.h2
-rw-r--r--src/lib/video_mxf_decoder.cc18
-rw-r--r--src/lib/video_mxf_decoder.h8
-rw-r--r--src/lib/video_mxf_examiner.cc8
-rw-r--r--src/lib/writer.cc41
-rw-r--r--src/lib/writer.h14
-rw-r--r--src/lib/wscript30
-rw-r--r--src/tools/dcpomatic.cc272
-rw-r--r--src/tools/dcpomatic_batch.cc67
-rw-r--r--src/tools/dcpomatic_cli.cc21
-rw-r--r--src/tools/dcpomatic_combiner.cc25
-rw-r--r--src/tools/dcpomatic_disk.cc60
-rw-r--r--src/tools/dcpomatic_editor.cc27
-rw-r--r--src/tools/dcpomatic_kdm.cc57
-rw-r--r--src/tools/dcpomatic_player.cc75
-rw-r--r--src/tools/dcpomatic_playlist.cc67
-rw-r--r--src/tools/dcpomatic_server.cc15
-rw-r--r--src/tools/dcpomatic_server_cli.cc29
-rw-r--r--src/tools/dcpomatic_verifier.cc251
-rw-r--r--src/tools/wscript89
-rw-r--r--src/wx/about_dialog.cc43
-rw-r--r--src/wx/audio_dialog.cc5
-rw-r--r--src/wx/audio_panel.cc57
-rw-r--r--src/wx/audio_panel.h3
-rw-r--r--src/wx/check_box.cc7
-rw-r--r--src/wx/check_box.h1
-rw-r--r--src/wx/cinema_dialog.cc45
-rw-r--r--src/wx/cinema_dialog.h11
-rw-r--r--src/wx/colours.h25
-rw-r--r--src/wx/config_dialog.cc6
-rw-r--r--src/wx/content_menu.cc51
-rw-r--r--src/wx/content_menu.h2
-rw-r--r--src/wx/content_panel.cc15
-rw-r--r--src/wx/content_panel.h4
-rw-r--r--src/wx/content_sub_panel.cc23
-rw-r--r--src/wx/content_sub_panel.h1
-rw-r--r--src/wx/content_timeline.cc1021
-rw-r--r--src/wx/content_timeline.h149
-rw-r--r--src/wx/content_timeline_atmos_view.cc (renamed from src/wx/timeline_atmos_content_view.cc)15
-rw-r--r--src/wx/content_timeline_atmos_view.h (renamed from src/wx/timeline_atmos_content_view.h)8
-rw-r--r--src/wx/content_timeline_audio_view.cc (renamed from src/wx/timeline_audio_content_view.cc)23
-rw-r--r--src/wx/content_timeline_audio_view.h (renamed from src/wx/timeline_audio_content_view.h)8
-rw-r--r--src/wx/content_timeline_dialog.cc (renamed from src/wx/timeline_dialog.cc)38
-rw-r--r--src/wx/content_timeline_dialog.h (renamed from src/wx/timeline_dialog.h)8
-rw-r--r--src/wx/content_timeline_text_view.cc (renamed from src/wx/timeline_text_content_view.cc)13
-rw-r--r--src/wx/content_timeline_text_view.h (renamed from src/wx/timeline_text_content_view.h)12
-rw-r--r--src/wx/content_timeline_video_view.cc (renamed from src/wx/timeline_video_content_view.cc)18
-rw-r--r--src/wx/content_timeline_video_view.h (renamed from src/wx/timeline_video_content_view.h)8
-rw-r--r--src/wx/content_timeline_view.cc42
-rw-r--r--src/wx/content_timeline_view.h60
-rw-r--r--src/wx/content_view.cc3
-rw-r--r--src/wx/dcp_panel.cc233
-rw-r--r--src/wx/dcp_panel.h17
-rw-r--r--src/wx/dcp_referencing_dialog.cc231
-rw-r--r--src/wx/dcp_referencing_dialog.h70
-rw-r--r--src/wx/dcp_timeline.cc617
-rw-r--r--src/wx/dcp_timeline.h125
-rw-r--r--src/wx/dcp_timeline_dialog.cc78
-rw-r--r--src/wx/dcp_timeline_dialog.h39
-rw-r--r--src/wx/dcp_timeline_reel_marker_view.cc71
-rw-r--r--src/wx/dcp_timeline_reel_marker_view.h59
-rw-r--r--src/wx/dcp_timeline_view.h44
-rw-r--r--src/wx/dcpomatic_choice.cc22
-rw-r--r--src/wx/dcpomatic_choice.h10
-rw-r--r--src/wx/dir_picker_ctrl.cc11
-rw-r--r--src/wx/dir_picker_ctrl.h3
-rw-r--r--src/wx/disk_warning_dialog.cc7
-rw-r--r--src/wx/dkdm_dialog.cc11
-rw-r--r--src/wx/export_video_file_dialog.h2
-rw-r--r--src/wx/file_picker_ctrl.cc8
-rw-r--r--src/wx/film_editor.cc33
-rw-r--r--src/wx/film_editor.h8
-rw-r--r--src/wx/film_name_location_dialog.cc15
-rw-r--r--src/wx/film_viewer.cc17
-rw-r--r--src/wx/film_viewer.h10
-rw-r--r--src/wx/full_config_dialog.cc260
-rw-r--r--src/wx/gl_video_view.cc116
-rw-r--r--src/wx/gl_video_view.h7
-rw-r--r--src/wx/grok/gpu_config_panel.h227
-rw-r--r--src/wx/id.h1
-rw-r--r--src/wx/job_view.cc3
-rw-r--r--src/wx/kdm_cpl_panel.cc7
-rw-r--r--src/wx/kdm_dialog.cc36
-rw-r--r--src/wx/kdm_dialog.h1
-rw-r--r--src/wx/kdm_timing_panel.cc71
-rw-r--r--src/wx/kdm_timing_panel.h27
-rw-r--r--src/wx/load_config_from_zip_dialog.cc59
-rw-r--r--src/wx/load_config_from_zip_dialog.h41
-rw-r--r--src/wx/metadata_dialog.cc10
-rw-r--r--src/wx/optimisation.h37
-rw-r--r--src/wx/player_config_dialog.cc5
-rw-r--r--src/wx/playlist_controls.cc5
-rw-r--r--src/wx/playlist_editor_config_dialog.cc7
-rw-r--r--src/wx/recipient_dialog.cc43
-rw-r--r--src/wx/recipient_dialog.h6
-rw-r--r--src/wx/recipients_panel.cc67
-rw-r--r--src/wx/recipients_panel.h7
-rw-r--r--src/wx/save_template_dialog.cc56
-rw-r--r--src/wx/save_template_dialog.h12
-rw-r--r--src/wx/screens_panel.cc271
-rw-r--r--src/wx/screens_panel.h52
-rw-r--r--src/wx/system_information_dialog.cc5
-rw-r--r--src/wx/text_panel.cc55
-rw-r--r--src/wx/text_panel.h3
-rw-r--r--src/wx/text_view.cc34
-rw-r--r--src/wx/text_view.h10
-rw-r--r--src/wx/timecode.cc2
-rw-r--r--src/wx/timecode.h13
-rw-r--r--src/wx/timeline.cc1001
-rw-r--r--src/wx/timeline.h131
-rw-r--r--src/wx/timeline_content_view.cc7
-rw-r--r--src/wx/timeline_content_view.h6
-rw-r--r--src/wx/timeline_labels_view.cc6
-rw-r--r--src/wx/timeline_labels_view.h6
-rw-r--r--src/wx/timeline_reels_view.cc6
-rw-r--r--src/wx/timeline_reels_view.h6
-rw-r--r--src/wx/timeline_time_axis_view.cc6
-rw-r--r--src/wx/timeline_time_axis_view.h8
-rw-r--r--src/wx/timeline_view.cc69
-rw-r--r--src/wx/timeline_view.h42
-rw-r--r--src/wx/try_unmount_dialog.cc8
-rw-r--r--src/wx/update_dialog.cc5
-rw-r--r--src/wx/verify_dcp_progress_dialog.cc52
-rw-r--r--src/wx/verify_dcp_progress_dialog.h9
-rw-r--r--src/wx/verify_dcp_progress_panel.cc87
-rw-r--r--src/wx/verify_dcp_progress_panel.h44
-rw-r--r--src/wx/verify_dcp_result_dialog.cc47
-rw-r--r--src/wx/verify_dcp_result_dialog.h (renamed from src/wx/verify_dcp_dialog.h)4
-rw-r--r--src/wx/verify_dcp_result_panel.cc (renamed from src/wx/verify_dcp_dialog.cc)236
-rw-r--r--src/wx/verify_dcp_result_panel.h50
-rw-r--r--src/wx/video_panel.cc56
-rw-r--r--src/wx/video_panel.h3
-rw-r--r--src/wx/video_view.h7
-rw-r--r--src/wx/window_metrics.cc45
-rw-r--r--src/wx/window_metrics.h39
-rw-r--r--src/wx/wscript26
-rw-r--r--src/wx/wx_util.cc151
-rw-r--r--src/wx/wx_util.h20
-rw-r--r--src/wx/wx_variant.cc147
-rw-r--r--src/wx/wx_variant.h54
328 files changed, 12640 insertions, 5280 deletions
diff --git a/src/lib/analytics.cc b/src/lib/analytics.cc
index 836051fe5..638f59f71 100644
--- a/src/lib/analytics.cc
+++ b/src/lib/analytics.cc
@@ -22,6 +22,7 @@
#include "analytics.h"
#include "compose.hpp"
#include "exceptions.h"
+#include "variant.h"
#include <dcp/filesystem.h>
#include <dcp/raw_convert.h>
#include <dcp/warnings.h>
@@ -61,17 +62,17 @@ Analytics::successful_dcp_encode ()
boost::bind(
boost::ref(Message),
_("Congratulations!"),
- String::compose (_(
- "<h2>You have made %1 DCPs with DCP-o-matic!</h2>"
+ String::compose(_(
+ "<h2>You have made %1 DCPs with %2!</h2>"
"<img width=\"20%%\" src=\"memory:me.jpg\" align=\"center\">"
"<p>Hello. I'm Carl and I'm the "
- "developer of DCP-o-matic. I work on it in my spare time (with the help "
+ "developer of %3. I work on it in my spare time (with the help "
"of a fine volunteer team of testers and translators) and I release it "
"as free software."
- "<p>If you find DCP-o-matic useful, please consider a donation to the "
+ "<p>If you find %4 useful, please consider a donation to the "
"project. Financial support will help me to spend more "
- "time developing DCP-o-matic and making it better!"
+ "time developing %5 and making it better!"
"<p><ul>"
"<li><a href=\"https://dcpomatic.com/donate_amount?amount=40\">Go to Paypal to donate €40</a>"
@@ -79,7 +80,12 @@ Analytics::successful_dcp_encode ()
"<li><a href=\"https://dcpomatic.com/donate_amount?amount=10\">Go to Paypal to donate €10</a>"
"</ul>"
- "<p>Thank you!"), _successful_dcp_encodes
+ "<p>Thank you!"),
+ _successful_dcp_encodes,
+ variant::dcpomatic(),
+ variant::dcpomatic(),
+ variant::dcpomatic(),
+ variant::dcpomatic()
)
)
);
@@ -93,8 +99,8 @@ Analytics::write () const
xmlpp::Document doc;
auto root = doc.create_root_node ("Analytics");
- root->add_child("Version")->add_child_text(raw_convert<string>(_current_version));
- root->add_child("SuccessfulDCPEncodes")->add_child_text(raw_convert<string>(_successful_dcp_encodes));
+ cxml::add_text_child(root, "Version", raw_convert<string>(_current_version));
+ cxml::add_text_child(root, "SuccessfulDCPEncodes", raw_convert<string>(_successful_dcp_encodes));
try {
doc.write_to_file_formatted(write_path("analytics.xml").string());
diff --git a/src/lib/atmos_content.cc b/src/lib/atmos_content.cc
index ada304dad..22bd431a2 100644
--- a/src/lib/atmos_content.cc
+++ b/src/lib/atmos_content.cc
@@ -63,10 +63,10 @@ AtmosContent::from_xml (Content* parent, cxml::ConstNodePtr node)
void
-AtmosContent::as_xml (xmlpp::Node* node) const
+AtmosContent::as_xml(xmlpp::Element* element) const
{
- node->add_child("AtmosLength")->add_child_text(dcp::raw_convert<string>(_length));
- node->add_child("AtmosEditRate")->add_child_text(_edit_rate.as_string());
+ cxml::add_text_child(element, "AtmosLength", dcp::raw_convert<string>(_length));
+ cxml::add_text_child(element, "AtmosEditRate", _edit_rate.as_string());
}
diff --git a/src/lib/atmos_content.h b/src/lib/atmos_content.h
index afa3c75a2..ffa181497 100644
--- a/src/lib/atmos_content.h
+++ b/src/lib/atmos_content.h
@@ -42,7 +42,7 @@ public:
static std::shared_ptr<AtmosContent> from_xml (Content* parent, cxml::ConstNodePtr node);
- void as_xml (xmlpp::Node* node) const;
+ void as_xml(xmlpp::Element* element) const;
void set_length (Frame len);
diff --git a/src/lib/atmos_mxf_content.cc b/src/lib/atmos_mxf_content.cc
index 82c20e88f..1283961ee 100644
--- a/src/lib/atmos_mxf_content.cc
+++ b/src/lib/atmos_mxf_content.cc
@@ -98,11 +98,11 @@ AtmosMXFContent::summary () const
void
-AtmosMXFContent::as_xml (xmlpp::Node* node, bool with_paths) const
+AtmosMXFContent::as_xml(xmlpp::Element* element, bool with_paths) const
{
- node->add_child("Type")->add_child_text("AtmosMXF");
- Content::as_xml (node, with_paths);
- atmos->as_xml (node);
+ cxml::add_text_child(element, "Type", "AtmosMXF");
+ Content::as_xml(element, with_paths);
+ atmos->as_xml(element);
}
diff --git a/src/lib/atmos_mxf_content.h b/src/lib/atmos_mxf_content.h
index 57f041774..e6225a355 100644
--- a/src/lib/atmos_mxf_content.h
+++ b/src/lib/atmos_mxf_content.h
@@ -39,7 +39,7 @@ public:
void examine (std::shared_ptr<const Film> film, std::shared_ptr<Job> job) override;
std::string summary () const override;
- void as_xml (xmlpp::Node* node, bool with_path) const override;
+ void as_xml(xmlpp::Element* element, bool with_path) const override;
dcpomatic::DCPTime full_length (std::shared_ptr<const Film> film) const override;
dcpomatic::DCPTime approximate_length () const override;
diff --git a/src/lib/audio_analysis.cc b/src/lib/audio_analysis.cc
index b8c2e072d..ac990ee18 100644
--- a/src/lib/audio_analysis.cc
+++ b/src/lib/audio_analysis.cc
@@ -84,11 +84,8 @@ AudioAnalysis::AudioAnalysis (boost::filesystem::path filename)
}
for (auto i: f.node_children ("SamplePeak")) {
- _sample_peak.push_back (
- PeakTime(
- dcp::raw_convert<float>(i->content()), DCPTime(i->number_attribute<Frame>("Time"))
- )
- );
+ auto const time = number_attribute<Frame>(i, "Time", "time");
+ _sample_peak.push_back(PeakTime(dcp::raw_convert<float>(i->content()), DCPTime(time)));
}
for (auto i: f.node_children("TruePeak")) {
@@ -141,44 +138,44 @@ void
AudioAnalysis::write (boost::filesystem::path filename)
{
auto doc = make_shared<xmlpp::Document>();
- xmlpp::Element* root = doc->create_root_node ("AudioAnalysis");
+ auto root = doc->create_root_node("AudioAnalysis");
- root->add_child("Version")->add_child_text(raw_convert<string>(_current_state_version));
+ cxml::add_text_child(root, "Version", raw_convert<string>(_current_state_version));
for (auto& i: _data) {
- auto channel = root->add_child ("Channel");
+ auto channel = cxml::add_child(root, "Channel");
for (auto& j: i) {
- j.as_xml (channel->add_child ("Point"));
+ j.as_xml(cxml::add_child(channel, "Point"));
}
}
for (size_t i = 0; i < _sample_peak.size(); ++i) {
- auto n = root->add_child("SamplePeak");
+ auto n = cxml::add_child(root, "SamplePeak");
n->add_child_text (raw_convert<string> (_sample_peak[i].peak));
- n->set_attribute ("Time", raw_convert<string> (_sample_peak[i].time.get()));
+ n->set_attribute("time", raw_convert<string> (_sample_peak[i].time.get()));
}
for (auto i: _true_peak) {
- root->add_child("TruePeak")->add_child_text (raw_convert<string> (i));
+ cxml::add_text_child(root, "TruePeak", raw_convert<string>(i));
}
if (_integrated_loudness) {
- root->add_child("IntegratedLoudness")->add_child_text (raw_convert<string> (_integrated_loudness.get ()));
+ cxml::add_text_child(root, "IntegratedLoudness", raw_convert<string>(_integrated_loudness.get()));
}
if (_loudness_range) {
- root->add_child("LoudnessRange")->add_child_text (raw_convert<string> (_loudness_range.get ()));
+ cxml::add_text_child(root, "LoudnessRange", raw_convert<string>(_loudness_range.get()));
}
if (_analysis_gain) {
- root->add_child("AnalysisGain")->add_child_text (raw_convert<string> (_analysis_gain.get ()));
+ cxml::add_text_child(root, "AnalysisGain", raw_convert<string>(_analysis_gain.get()));
}
- root->add_child("SamplesPerPoint")->add_child_text (raw_convert<string> (_samples_per_point));
- root->add_child("SampleRate")->add_child_text (raw_convert<string> (_sample_rate));
+ cxml::add_text_child(root, "SamplesPerPoint", raw_convert<string>(_samples_per_point));
+ cxml::add_text_child(root, "SampleRate", raw_convert<string>(_sample_rate));
if (_leqm) {
- root->add_child("Leqm")->add_child_text(raw_convert<string>(*_leqm));
+ cxml::add_text_child(root, "Leqm", raw_convert<string>(*_leqm));
}
doc->write_to_file_formatted (filename.string ());
diff --git a/src/lib/audio_content.cc b/src/lib/audio_content.cc
index 16f2bd5f1..2569e1cf0 100644
--- a/src/lib/audio_content.cc
+++ b/src/lib/audio_content.cc
@@ -124,14 +124,14 @@ AudioContent::AudioContent (Content* parent, vector<shared_ptr<Content>> c)
void
-AudioContent::as_xml (xmlpp::Node* node) const
+AudioContent::as_xml(xmlpp::Element* element) const
{
boost::mutex::scoped_lock lm (_mutex);
- node->add_child("AudioGain")->add_child_text(raw_convert<string>(_gain));
- node->add_child("AudioDelay")->add_child_text(raw_convert<string>(_delay));
- node->add_child("AudioFadeIn")->add_child_text(raw_convert<string>(_fade_in.get()));
- node->add_child("AudioFadeOut")->add_child_text(raw_convert<string>(_fade_out.get()));
- node->add_child("AudioUseSameFadesAsVideo")->add_child_text(_use_same_fades_as_video ? "1" : "0");
+ cxml::add_text_child(element, "AudioGain", raw_convert<string>(_gain));
+ cxml::add_text_child(element, "AudioDelay", raw_convert<string>(_delay));
+ cxml::add_text_child(element, "AudioFadeIn", raw_convert<string>(_fade_in.get()));
+ cxml::add_text_child(element, "AudioFadeOut", raw_convert<string>(_fade_out.get()));
+ cxml::add_text_child(element, "AudioUseSameFadesAsVideo", _use_same_fades_as_video ? "1" : "0");
}
diff --git a/src/lib/audio_content.h b/src/lib/audio_content.h
index 084871c8b..2a140b3e4 100644
--- a/src/lib/audio_content.h
+++ b/src/lib/audio_content.h
@@ -56,7 +56,7 @@ public:
AudioContent (Content* parent, std::vector<std::shared_ptr<Content>>);
AudioContent (Content* parent, cxml::ConstNodePtr);
- void as_xml (xmlpp::Node *) const;
+ void as_xml(xmlpp::Element*) const;
std::string technical_summary () const;
void take_settings_from (std::shared_ptr<const AudioContent> c);
diff --git a/src/lib/audio_filter_graph.cc b/src/lib/audio_filter_graph.cc
index 4e3052d57..cb888c162 100644
--- a/src/lib/audio_filter_graph.cc
+++ b/src/lib/audio_filter_graph.cc
@@ -48,11 +48,7 @@ AudioFilterGraph::AudioFilterGraph (int sample_rate, int channels)
/* FFmpeg doesn't know any channel layouts for any counts between 8 and 16,
so we need to tell it we're using 16 channels if we are using more than 8.
*/
- if (_channels > 8) {
- _channel_layout = av_get_default_channel_layout (16);
- } else {
- _channel_layout = av_get_default_channel_layout (_channels);
- }
+ av_channel_layout_default(&_channel_layout, _channels > 8 ? 16 : _channels);
_in_frame = av_frame_alloc ();
if (_in_frame == nullptr) {
@@ -69,7 +65,7 @@ string
AudioFilterGraph::src_parameters () const
{
char layout[64];
- av_get_channel_layout_string (layout, sizeof(layout), 0, _channel_layout);
+ av_channel_layout_describe(&_channel_layout, layout, sizeof(layout));
char buffer[256];
snprintf (
@@ -88,8 +84,9 @@ AudioFilterGraph::set_parameters (AVFilterContext* context) const
int r = av_opt_set_int_list (context, "sample_fmts", sample_fmts, AV_SAMPLE_FMT_NONE, AV_OPT_SEARCH_CHILDREN);
DCPOMATIC_ASSERT (r >= 0);
- int64_t channel_layouts[] = { _channel_layout, -1 };
- r = av_opt_set_int_list (context, "channel_layouts", channel_layouts, -1, AV_OPT_SEARCH_CHILDREN);
+ char ch_layout[64];
+ av_channel_layout_describe(&_channel_layout, ch_layout, sizeof(ch_layout));
+ r = av_opt_set(context, "ch_layouts", ch_layout, AV_OPT_SEARCH_CHILDREN);
DCPOMATIC_ASSERT (r >= 0);
int sample_rates[] = { _sample_rate, -1 };
@@ -114,7 +111,7 @@ void
AudioFilterGraph::process (shared_ptr<AudioBuffers> buffers)
{
DCPOMATIC_ASSERT (buffers->frames() > 0);
- int const process_channels = av_get_channel_layout_nb_channels (_channel_layout);
+ int const process_channels = _channel_layout.nb_channels;
DCPOMATIC_ASSERT (process_channels >= buffers->channels());
if (buffers->channels() < process_channels) {
@@ -144,8 +141,10 @@ AudioFilterGraph::process (shared_ptr<AudioBuffers> buffers)
_in_frame->nb_samples = buffers->frames ();
_in_frame->format = AV_SAMPLE_FMT_FLTP;
_in_frame->sample_rate = _sample_rate;
- _in_frame->channel_layout = _channel_layout;
+ _in_frame->ch_layout = _channel_layout;
+LIBDCP_DISABLE_WARNINGS
_in_frame->channels = process_channels;
+LIBDCP_ENABLE_WARNINGS
int r = av_buffersrc_write_frame (_buffer_src_context, _in_frame);
diff --git a/src/lib/audio_filter_graph.h b/src/lib/audio_filter_graph.h
index e5c55fa27..6d09f15be 100644
--- a/src/lib/audio_filter_graph.h
+++ b/src/lib/audio_filter_graph.h
@@ -26,6 +26,7 @@
#include "filter_graph.h"
extern "C" {
#include <libavfilter/buffersink.h>
+#include <libavutil/channel_layout.h>
}
class AudioBuffers;
@@ -47,7 +48,7 @@ protected:
private:
int _sample_rate;
int _channels;
- int64_t _channel_layout;
+ AVChannelLayout _channel_layout;
AVFrame* _in_frame;
};
diff --git a/src/lib/audio_mapping.cc b/src/lib/audio_mapping.cc
index b8aa6249f..37929001d 100644
--- a/src/lib/audio_mapping.cc
+++ b/src/lib/audio_mapping.cc
@@ -24,6 +24,7 @@
#include "constants.h"
#include "dcpomatic_assert.h"
#include "digester.h"
+#include "util.h"
#include <dcp/raw_convert.h>
#include <dcp/warnings.h>
#include <libcxml/cxml.h>
@@ -169,8 +170,8 @@ AudioMapping::AudioMapping (cxml::ConstNodePtr node, int state_version)
);
} else {
set (
- i->number_attribute<int>("Input"),
- i->number_attribute<int>("Output"),
+ number_attribute<int>(i, "Input", "input"),
+ number_attribute<int>(i, "Output", "output"),
raw_convert<float>(i->content())
);
}
@@ -219,19 +220,19 @@ AudioMapping::get (int input_channel, int output_channel) const
void
-AudioMapping::as_xml (xmlpp::Node* node) const
+AudioMapping::as_xml(xmlpp::Element* element) const
{
auto const input = input_channels();
auto const output = output_channels();
- node->add_child("InputChannels")->add_child_text(raw_convert<string>(input));
- node->add_child("OutputChannels")->add_child_text(raw_convert<string>(output));
+ cxml::add_text_child(element, "InputChannels", raw_convert<string>(input));
+ cxml::add_text_child(element, "OutputChannels", raw_convert<string>(output));
for (int c = 0; c < input; ++c) {
for (int d = 0; d < output; ++d) {
- auto t = node->add_child ("Gain");
- t->set_attribute ("Input", raw_convert<string> (c));
- t->set_attribute ("Output", raw_convert<string> (d));
+ auto t = cxml::add_child(element, "Gain");
+ t->set_attribute("input", raw_convert<string>(c));
+ t->set_attribute("output", raw_convert<string>(d));
t->add_child_text (raw_convert<string> (get (c, d)));
}
}
diff --git a/src/lib/audio_mapping.h b/src/lib/audio_mapping.h
index fe9a79789..68487d908 100644
--- a/src/lib/audio_mapping.h
+++ b/src/lib/audio_mapping.h
@@ -52,7 +52,7 @@ public:
/* Default copy constructor is fine */
- void as_xml (xmlpp::Node *) const;
+ void as_xml(xmlpp::Element*) const;
void make_zero ();
void make_default (AudioProcessor const * processor, boost::optional<boost::filesystem::path> filename = boost::optional<boost::filesystem::path>());
diff --git a/src/lib/audio_point.cc b/src/lib/audio_point.cc
index 1f32d25fc..036904520 100644
--- a/src/lib/audio_point.cc
+++ b/src/lib/audio_point.cc
@@ -72,6 +72,6 @@ AudioPoint::operator= (AudioPoint const & other)
void
AudioPoint::as_xml (xmlpp::Element* parent) const
{
- parent->add_child("Peak")->add_child_text(raw_convert<string>(_data[PEAK]));
- parent->add_child("RMS")->add_child_text(raw_convert<string>(_data[RMS]));
+ cxml::add_text_child(parent, "Peak", raw_convert<string>(_data[PEAK]));
+ cxml::add_text_child(parent, "RMS", raw_convert<string>(_data[RMS]));
}
diff --git a/src/lib/butler.cc b/src/lib/butler.cc
index b2fbc6c60..dd9874587 100644
--- a/src/lib/butler.cc
+++ b/src/lib/butler.cc
@@ -138,7 +138,7 @@ Butler::should_run () const
{
if (_video.size() >= MAXIMUM_VIDEO_READAHEAD * 10) {
/* This is way too big */
- optional<DCPTime> pos = _audio.peek();
+ auto pos = _audio.peek();
if (pos) {
throw ProgrammingError
(__FILE__, __LINE__, String::compose ("Butler video buffers reached %1 frames (audio is %2 at %3)", _video.size(), _audio.size(), pos->get()));
diff --git a/src/lib/check_content_job.cc b/src/lib/check_content_job.cc
index f37890abf..2028b01ac 100644
--- a/src/lib/check_content_job.cc
+++ b/src/lib/check_content_job.cc
@@ -70,7 +70,7 @@ CheckContentJob::run ()
std::vector<shared_ptr<Content>> changed;
std::copy_if (content.begin(), content.end(), std::back_inserter(changed), [](shared_ptr<Content> c) { return c->changed(); });
- if (_film->last_written_by_earlier_than(2, 16, 15)) {
+ if (_film->last_written_by_earlier_than(2, 17, 17)) {
for (auto c: content) {
if (auto stf = dynamic_pointer_cast<StringTextFileContent>(c)) {
stf->check_font_ids();
diff --git a/src/lib/cinema.cc b/src/lib/cinema.cc
deleted file mode 100644
index 3b4b9d7b6..000000000
--- a/src/lib/cinema.cc
+++ /dev/null
@@ -1,112 +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 "cinema.h"
-#include "screen.h"
-#include "dcpomatic_assert.h"
-#include <libcxml/cxml.h>
-#include <dcp/raw_convert.h>
-#include <libxml++/libxml++.h>
-
-
-using std::make_shared;
-using std::shared_ptr;
-using std::string;
-using dcp::raw_convert;
-using dcpomatic::Screen;
-
-
-Cinema::Cinema (cxml::ConstNodePtr node)
- : name (node->string_child ("Name"))
- , notes (node->optional_string_child("Notes").get_value_or(""))
-{
- 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
- a constructor)
-*/
-void
-Cinema::read_screens (cxml::ConstNodePtr node)
-{
- for (auto i: node->node_children("Screen")) {
- add_screen (make_shared<Screen>(i));
- }
-}
-
-void
-Cinema::as_xml (xmlpp::Element* parent) const
-{
- parent->add_child("Name")->add_child_text (name);
-
- for (auto i: emails) {
- parent->add_child("Email")->add_child_text (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));
-
- for (auto i: _screens) {
- i->as_xml (parent->add_child ("Screen"));
- }
-}
-
-void
-Cinema::add_screen (shared_ptr<Screen> s)
-{
- s->cinema = shared_from_this ();
- _screens.push_back (s);
-}
-
-void
-Cinema::remove_screen (shared_ptr<Screen> s)
-{
- auto iter = std::find(_screens.begin(), _screens.end(), s);
- if (iter != _screens.end()) {
- _screens.erase(iter);
- }
-}
-
-void
-Cinema::set_utc_offset_hour (int h)
-{
- DCPOMATIC_ASSERT (h >= -11 && h <= 12);
- _utc_offset_hour = h;
-}
-
-void
-Cinema::set_utc_offset_minute (int m)
-{
- DCPOMATIC_ASSERT (m >= 0 && m <= 59);
- _utc_offset_minute = m;
-}
diff --git a/src/lib/cinema.h b/src/lib/cinema.h
index 6c202a7bf..44f232f91 100644
--- a/src/lib/cinema.h
+++ b/src/lib/cinema.h
@@ -18,76 +18,35 @@
*/
+
/** @file src/lib/cinema.h
* @brief Cinema class.
*/
-#include <libcxml/cxml.h>
+#include <dcp/utc_offset.h>
#include <memory>
+#include <string>
+#include <vector>
-namespace xmlpp {
- class Element;
-}
-
-namespace dcpomatic {
- class Screen;
-}
-
/** @class Cinema
* @brief A description of a Cinema for KDM generation.
*
- * This is a cinema name, some metadata and a list of
- * Screen objects.
+ * This is a cinema name and some metadata.
*/
-class Cinema : public std::enable_shared_from_this<Cinema>
+class 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_, dcp::UTCOffset utc_offset_)
: name (name_)
, emails (e)
, notes (notes_)
- , _utc_offset_hour (utc_offset_hour)
- , _utc_offset_minute (utc_offset_minute)
+ , utc_offset(std::move(utc_offset_))
{}
- explicit Cinema (cxml::ConstNodePtr);
-
- void read_screens (cxml::ConstNodePtr);
-
- void as_xml (xmlpp::Element *) const;
-
- 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;
+ dcp::UTCOffset utc_offset;
};
diff --git a/src/lib/cinema_list.cc b/src/lib/cinema_list.cc
new file mode 100644
index 000000000..41b9dbab3
--- /dev/null
+++ b/src/lib/cinema_list.cc
@@ -0,0 +1,466 @@
+/*
+ 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 "cinema.h"
+#include "cinema_list.h"
+#include "config.h"
+#include "dcpomatic_assert.h"
+#include "exceptions.h"
+#include "screen.h"
+#include "sqlite_statement.h"
+#include "sqlite_transaction.h"
+#include "util.h"
+#include <dcp/certificate.h>
+#include <sqlite3.h>
+#include <boost/algorithm/string.hpp>
+#include <iostream>
+#include <numeric>
+
+
+using std::pair;
+using std::make_pair;
+using std::string;
+using std::vector;
+using boost::optional;
+
+
+CinemaList::CinemaList()
+ : _cinemas("cinemas")
+ , _screens("screens")
+ , _trusted_devices("trusted_devices")
+{
+ setup_tables();
+ setup(Config::instance()->cinemas_file());
+}
+
+
+CinemaList::CinemaList(boost::filesystem::path db_file)
+ : _cinemas("cinemas")
+ , _screens("screens")
+ , _trusted_devices("trusted_devices")
+{
+ setup_tables();
+ setup(db_file);
+}
+
+
+void
+CinemaList::setup_tables()
+{
+ _cinemas.add_column("name", "TEXT");
+ _cinemas.add_column("emails", "TEXT");
+ _cinemas.add_column("notes", "TEXT");
+ _cinemas.add_column("utc_offset_hour", "INTEGER");
+ _cinemas.add_column("utc_offset_minute", "INTEGER");
+
+ _screens.add_column("cinema", "INTEGER");
+ _screens.add_column("name", "TEXT");
+ _screens.add_column("notes", "TEXT");
+ _screens.add_column("recipient", "TEXT");
+ _screens.add_column("recipient_file", "TEXT");
+
+ _trusted_devices.add_column("screen", "INTEGER");
+ _trusted_devices.add_column("certificate_or_thumbprint", "TEXT");
+}
+
+
+void
+CinemaList::read_legacy_file(boost::filesystem::path xml_file)
+{
+ cxml::Document doc("Cinemas");
+ doc.read_file(xml_file);
+ read_legacy_document(doc);
+}
+
+
+void
+CinemaList::read_legacy_string(std::string const& xml)
+{
+ cxml::Document doc("Cinemas");
+ doc.read_string(xml);
+ read_legacy_document(doc);
+}
+
+
+void
+CinemaList::read_legacy_document(cxml::Document const& doc)
+{
+ for (auto cinema_node: doc.node_children("Cinema")) {
+ vector<string> emails;
+ for (auto email_node: cinema_node->node_children("Email")) {
+ emails.push_back(email_node->content());
+ }
+
+ int hour = 0;
+ if (cinema_node->optional_number_child<int>("UTCOffset")) {
+ hour = cinema_node->number_child<int>("UTCOffset");
+ } else {
+ hour = cinema_node->optional_number_child<int>("UTCOffsetHour").get_value_or(0);
+ }
+
+ int minute = cinema_node->optional_number_child<int>("UTCOffsetMinute").get_value_or(0);
+
+ Cinema cinema(
+ cinema_node->string_child("Name"),
+ emails,
+ cinema_node->string_child("Notes"),
+ dcp::UTCOffset(hour, minute)
+ );
+
+ auto cinema_id = add_cinema(cinema);
+
+ for (auto screen_node: cinema_node->node_children("Screen")) {
+ optional<dcp::Certificate> recipient;
+ if (auto recipient_string = screen_node->optional_string_child("Recipient")) {
+ recipient = dcp::Certificate(*recipient_string);
+ }
+ vector<TrustedDevice> trusted_devices;
+ for (auto trusted_device_node: screen_node->node_children("TrustedDevice")) {
+ trusted_devices.push_back(TrustedDevice(trusted_device_node->content()));
+ }
+ dcpomatic::Screen screen(
+ screen_node->string_child("Name"),
+ screen_node->string_child("Notes"),
+ recipient,
+ screen_node->optional_string_child("RecipientFile"),
+ trusted_devices
+ );
+ add_screen(cinema_id, screen);
+ }
+ }
+}
+
+
+void
+CinemaList::clear()
+{
+ for (auto table: { "cinemas", "screens", "trusted_devices" }) {
+ SQLiteStatement sql(_db, String::compose("DELETE FROM %1", table));
+ sql.execute();
+ }
+}
+
+
+void
+CinemaList::setup(boost::filesystem::path db_file)
+{
+#ifdef DCPOMATIC_WINDOWS
+ auto rc = sqlite3_open16(db_file.c_str(), &_db);
+#else
+ auto rc = sqlite3_open(db_file.c_str(), &_db);
+#endif
+ if (rc != SQLITE_OK) {
+ throw FileError("Could not open SQLite database", db_file);
+ }
+
+ sqlite3_busy_timeout(_db, 500);
+
+ SQLiteStatement cinemas(_db, _cinemas.create());
+ cinemas.execute();
+
+ SQLiteStatement screens(_db, _screens.create());
+ screens.execute();
+
+ SQLiteStatement devices(_db, _trusted_devices.create());
+ devices.execute();
+}
+
+
+CinemaList::CinemaList(CinemaList&& other)
+ : _db(other._db)
+ , _cinemas(std::move(other._cinemas))
+ , _screens(std::move(other._screens))
+ , _trusted_devices(std::move(other._trusted_devices))
+{
+ other._db = nullptr;
+}
+
+
+CinemaList&
+CinemaList::operator=(CinemaList&& other)
+{
+ if (this != &other) {
+ _db = other._db;
+ other._db = nullptr;
+ }
+ return *this;
+}
+
+
+CinemaID
+CinemaList::add_cinema(Cinema const& cinema)
+{
+ SQLiteStatement statement(_db, _cinemas.insert());
+
+ statement.bind_text(1, cinema.name);
+ statement.bind_text(2, join_strings(cinema.emails));
+ statement.bind_text(3, cinema.notes);
+ statement.bind_int64(4, cinema.utc_offset.hour());
+ statement.bind_int64(5, cinema.utc_offset.minute());
+
+ statement.execute();
+
+ return sqlite3_last_insert_rowid(_db);
+}
+
+
+void
+CinemaList::update_cinema(CinemaID id, Cinema const& cinema)
+{
+ SQLiteStatement statement(_db, _cinemas.update("WHERE id=?"));
+
+ statement.bind_text(1, cinema.name);
+ statement.bind_text(2, join_strings(cinema.emails));
+ statement.bind_text(3, cinema.notes);
+ statement.bind_int64(4, cinema.utc_offset.hour());
+ statement.bind_int64(5, cinema.utc_offset.minute());
+ statement.bind_int64(6, id.get());
+
+ statement.execute();
+}
+
+
+void
+CinemaList::remove_cinema(CinemaID id)
+{
+ SQLiteStatement statement(_db, "DELETE FROM cinemas WHERE ID=?");
+ statement.bind_int64(1, id.get());
+ statement.execute();
+}
+
+
+CinemaList::~CinemaList()
+{
+ if (_db) {
+ sqlite3_close(_db);
+ }
+}
+
+
+static
+vector<pair<CinemaID, Cinema>>
+cinemas_from_result(SQLiteStatement& statement)
+{
+ vector<pair<CinemaID, Cinema>> output;
+
+ statement.execute([&output](SQLiteStatement& statement) {
+ DCPOMATIC_ASSERT(statement.data_count() == 6);
+ CinemaID const id = statement.column_int64(0);
+ auto const name = statement.column_text(1);
+ auto const join_strings = statement.column_text(2);
+ vector<string> emails;
+ boost::algorithm::split(emails, join_strings, boost::is_any_of(" "));
+ auto const notes = statement.column_text(3);
+ auto const utc_offset_hour = static_cast<int>(statement.column_int64(4));
+ auto const utc_offset_minute = static_cast<int>(statement.column_int64(5));
+ output.push_back(make_pair(id, Cinema(name, { emails }, notes, dcp::UTCOffset{utc_offset_hour, utc_offset_minute})));
+ });
+
+ return output;
+}
+
+
+vector<pair<CinemaID, Cinema>>
+CinemaList::cinemas() const
+{
+ SQLiteStatement statement(_db, _cinemas.select("ORDER BY name ASC"));
+ return cinemas_from_result(statement);
+}
+
+
+optional<Cinema>
+CinemaList::cinema(CinemaID id) const
+{
+ SQLiteStatement statement(_db, _cinemas.select("WHERE id=?"));
+ statement.bind_int64(1, id.get());
+ auto result = cinemas_from_result(statement);
+ if (result.empty()) {
+ return {};
+ }
+ return result[0].second;
+}
+
+ScreenID
+CinemaList::add_screen(CinemaID cinema_id, dcpomatic::Screen const& screen)
+{
+ SQLiteTransaction transaction(_db);
+
+ SQLiteStatement add_screen(_db, _screens.insert());
+
+ add_screen.bind_int64(1, cinema_id.get());
+ add_screen.bind_text(2, screen.name);
+ add_screen.bind_text(3, screen.notes);
+ add_screen.bind_text(4, screen.recipient->certificate(true));
+ add_screen.bind_text(5, screen.recipient_file.get_value_or(""));
+
+ add_screen.execute();
+
+ auto const screen_id = sqlite3_last_insert_rowid(_db);
+
+ for (auto device: screen.trusted_devices) {
+ SQLiteStatement add_device(_db, _trusted_devices.insert());
+ add_device.bind_int64(1, screen_id);
+ add_device.bind_text(2, device.as_string());
+ }
+
+ transaction.commit();
+
+ return screen_id;
+}
+
+
+dcpomatic::Screen
+CinemaList::screen_from_result(SQLiteStatement& statement, ScreenID screen_id) const
+{
+ auto certificate_string = statement.column_text(4);
+ optional<dcp::Certificate> certificate = certificate_string.empty() ? optional<dcp::Certificate>() : dcp::Certificate(certificate_string);
+ auto recipient_file_string = statement.column_text(5);
+ optional<string> recipient_file = recipient_file_string.empty() ? optional<string>() : recipient_file_string;
+
+ SQLiteStatement trusted_devices_statement(_db, _trusted_devices.select("WHERE screen=?"));
+ trusted_devices_statement.bind_int64(1, screen_id.get());
+ vector<TrustedDevice> trusted_devices;
+ trusted_devices_statement.execute([&trusted_devices](SQLiteStatement& statement) {
+ DCPOMATIC_ASSERT(statement.data_count() == 1);
+ auto description = statement.column_text(1);
+ if (boost::algorithm::starts_with(description, "-----BEGIN CERTIFICATE")) {
+ trusted_devices.push_back(TrustedDevice(dcp::Certificate(description)));
+ } else {
+ trusted_devices.push_back(TrustedDevice(description));
+ }
+ });
+
+ return dcpomatic::Screen(statement.column_text(2), statement.column_text(3), certificate, recipient_file, trusted_devices);
+}
+
+
+optional<dcpomatic::Screen>
+CinemaList::screen(ScreenID screen_id) const
+{
+ SQLiteStatement statement(_db, _screens.select("WHERE id=?"));
+ statement.bind_int64(1, screen_id.get());
+
+ optional<dcpomatic::Screen> output;
+
+ statement.execute([this, &output, screen_id](SQLiteStatement& statement) {
+ DCPOMATIC_ASSERT(statement.data_count() == 6);
+ output = screen_from_result(statement, screen_id);
+ });
+
+ return output;
+}
+
+
+
+vector<pair<ScreenID, dcpomatic::Screen>>
+CinemaList::screens_from_result(SQLiteStatement& statement) const
+{
+ vector<pair<ScreenID, dcpomatic::Screen>> output;
+
+ statement.execute([this, &output](SQLiteStatement& statement) {
+ DCPOMATIC_ASSERT(statement.data_count() == 6);
+ ScreenID const screen_id = statement.column_int64(0);
+ output.push_back({screen_id, screen_from_result(statement, screen_id)});
+ });
+
+ return output;
+}
+
+
+vector<pair<ScreenID, dcpomatic::Screen>>
+CinemaList::screens(CinemaID cinema_id) const
+{
+ SQLiteStatement statement(_db, _screens.select("WHERE cinema=?"));
+ statement.bind_int64(1, cinema_id.get());
+ return screens_from_result(statement);
+}
+
+
+vector<pair<ScreenID, dcpomatic::Screen>>
+CinemaList::screens_by_cinema_and_name(CinemaID id, std::string const& name) const
+{
+ SQLiteStatement statement(_db, _screens.select("WHERE cinema=? AND name=?"));
+ statement.bind_int64(1, id.get());
+ statement.bind_text(2, name);
+ return screens_from_result(statement);
+}
+
+
+optional<std::pair<CinemaID, Cinema>>
+CinemaList::cinema_by_name_or_email(std::string const& text) const
+{
+ SQLiteStatement statement(_db, _cinemas.select("WHERE name LIKE ? OR EMAILS LIKE ?"));
+ auto const wildcard = string("%") + text + "%";
+ statement.bind_text(1, wildcard);
+ statement.bind_text(2, wildcard);
+
+ auto all = cinemas_from_result(statement);
+ if (all.empty()) {
+ return {};
+ }
+ return all[0];
+}
+
+
+void
+CinemaList::update_screen(ScreenID id, dcpomatic::Screen const& screen)
+{
+ SQLiteStatement statement(_db, _screens.update("WHERE id=?"));
+
+ statement.bind_text(1, screen.name);
+ statement.bind_text(2, screen.notes);
+ statement.bind_text(3, screen.recipient->certificate(true));
+ statement.bind_text(4, screen.recipient_file.get_value_or(""));
+ statement.bind_int64(5, id.get());
+
+ statement.execute();
+}
+
+
+void
+CinemaList::remove_screen(ScreenID id)
+{
+ SQLiteStatement statement(_db, "DELETE FROM screens WHERE ID=?");
+ statement.bind_int64(1, id.get());
+ statement.execute();
+}
+
+
+optional<dcp::UTCOffset>
+CinemaList::unique_utc_offset(std::set<CinemaID> const& cinemas_to_check)
+{
+ optional<dcp::UTCOffset> offset;
+
+ for (auto const& cinema: cinemas()) {
+ if (cinemas_to_check.find(cinema.first) == cinemas_to_check.end()) {
+ continue;
+ }
+
+ if (!offset) {
+ offset = cinema.second.utc_offset;
+ } else if (cinema.second.utc_offset != *offset) {
+ return dcp::UTCOffset();
+ }
+ }
+
+ return offset;
+}
+
diff --git a/src/lib/cinema_list.h b/src/lib/cinema_list.h
new file mode 100644
index 000000000..c91f29476
--- /dev/null
+++ b/src/lib/cinema_list.h
@@ -0,0 +1,126 @@
+/*
+ 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_CINEMA_LIST_H
+#define DCPOMATIC_CINEMA_LIST_H
+
+
+#include "id.h"
+#include "sqlite_table.h"
+#include <libcxml/cxml.h>
+#include <dcp/utc_offset.h>
+#include <boost/filesystem.hpp>
+#include <boost/optional.hpp>
+#include <sqlite3.h>
+#include <set>
+
+
+class Cinema;
+namespace dcpomatic {
+ class Screen;
+}
+class SQLiteStatement;
+
+
+class CinemaID : public ID
+{
+public:
+ CinemaID(sqlite3_int64 id)
+ : ID(id) {}
+
+ bool operator<(CinemaID const& other) const {
+ return get() < other.get();
+ }
+};
+
+
+class ScreenID : public ID
+{
+public:
+ ScreenID(sqlite3_int64 id)
+ : ID(id) {}
+
+ bool operator==(ScreenID const& other) const {
+ return get() == other.get();
+ }
+
+ bool operator!=(ScreenID const& other) const {
+ return get() != other.get();
+ }
+
+ bool operator<(ScreenID const& other) const {
+ return get() < other.get();
+ }
+};
+
+
+class CinemaList
+{
+public:
+ CinemaList();
+ CinemaList(boost::filesystem::path db_file);
+ ~CinemaList();
+
+ CinemaList(CinemaList const&) = delete;
+ CinemaList& operator=(CinemaList const&) = delete;
+
+ CinemaList(CinemaList&& other);
+ CinemaList& operator=(CinemaList&& other);
+
+ void read_legacy_file(boost::filesystem::path xml_file);
+ void read_legacy_string(std::string const& xml);
+
+ void clear();
+
+ CinemaID add_cinema(Cinema const& cinema);
+ void update_cinema(CinemaID id, Cinema const& cinema);
+ void remove_cinema(CinemaID id);
+ std::vector<std::pair<CinemaID, Cinema>> cinemas() const;
+ boost::optional<Cinema> cinema(CinemaID id) const;
+ boost::optional<std::pair<CinemaID, Cinema>> cinema_by_partial_name(std::string const& text) const;
+ boost::optional<std::pair<CinemaID, Cinema>> cinema_by_name_or_email(std::string const& text) const;
+
+ ScreenID add_screen(CinemaID cinema_id, dcpomatic::Screen const& screen);
+ void update_screen(ScreenID id, dcpomatic::Screen const& screen);
+ void remove_screen(ScreenID id);
+ boost::optional<dcpomatic::Screen> screen(ScreenID screen_id) const;
+ std::vector<std::pair<ScreenID, dcpomatic::Screen>> screens(CinemaID cinema_id) const;
+ std::vector<std::pair<ScreenID, dcpomatic::Screen>> screens_by_cinema_and_name(CinemaID id, std::string const& name) const;
+
+ boost::optional<dcp::UTCOffset> unique_utc_offset(std::set<CinemaID> const& cinemas);
+
+private:
+ dcpomatic::Screen screen_from_result(SQLiteStatement& statement, ScreenID screen_id) const;
+ std::vector<std::pair<ScreenID, dcpomatic::Screen>> screens_from_result(SQLiteStatement& statement) const;
+ void setup_tables();
+ void setup(boost::filesystem::path db_file);
+ void read_legacy_document(cxml::Document const& doc);
+
+ sqlite3* _db = nullptr;
+ SQLiteTable _cinemas;
+ SQLiteTable _screens;
+ SQLiteTable _trusted_devices;
+};
+
+
+
+#endif
+
diff --git a/src/lib/colour_conversion.cc b/src/lib/colour_conversion.cc
index bd1b47cb1..f1e625812 100644
--- a/src/lib/colour_conversion.cc
+++ b/src/lib/colour_conversion.cc
@@ -151,41 +151,40 @@ ColourConversion::from_xml (cxml::NodePtr node, int version)
}
void
-ColourConversion::as_xml (xmlpp::Node* node) const
+ColourConversion::as_xml(xmlpp::Element* element) const
{
- auto in_node = node->add_child ("InputTransferFunction");
+ auto in_node = cxml::add_child(element, "InputTransferFunction");
if (dynamic_pointer_cast<const dcp::GammaTransferFunction> (_in)) {
auto tf = dynamic_pointer_cast<const dcp::GammaTransferFunction> (_in);
- in_node->add_child("Type")->add_child_text ("Gamma");
- in_node->add_child("Gamma")->add_child_text (raw_convert<string> (tf->gamma ()));
+ cxml::add_text_child(in_node, "Type", "Gamma");
+ cxml::add_text_child(in_node, "Gamma", raw_convert<string>(tf->gamma()));
} else if (dynamic_pointer_cast<const dcp::ModifiedGammaTransferFunction> (_in)) {
auto tf = dynamic_pointer_cast<const dcp::ModifiedGammaTransferFunction> (_in);
- in_node->add_child("Type")->add_child_text ("ModifiedGamma");
- in_node->add_child("Power")->add_child_text (raw_convert<string> (tf->power ()));
- in_node->add_child("Threshold")->add_child_text (raw_convert<string> (tf->threshold ()));
- in_node->add_child("A")->add_child_text (raw_convert<string> (tf->A ()));
- in_node->add_child("B")->add_child_text (raw_convert<string> (tf->B ()));
- } else if (dynamic_pointer_cast<const dcp::SGamut3TransferFunction> (_in)) {
- in_node->add_child("Type")->add_child_text ("SGamut3");
+ cxml::add_text_child(in_node, "Type", "ModifiedGamma");
+ cxml::add_text_child(in_node, "Power", raw_convert<string>(tf->power ()));
+ cxml::add_text_child(in_node, "Threshold", raw_convert<string>(tf->threshold ()));
+ cxml::add_text_child(in_node, "A", raw_convert<string>(tf->A()));
+ cxml::add_text_child(in_node, "B", raw_convert<string>(tf->B()));
+ } else if (dynamic_pointer_cast<const dcp::SGamut3TransferFunction>(_in)) {
+ cxml::add_text_child(in_node, "Type", "SGamut3");
}
- node->add_child("YUVToRGB")->add_child_text (raw_convert<string> (static_cast<int> (_yuv_to_rgb)));
- node->add_child("RedX")->add_child_text (raw_convert<string> (_red.x));
- node->add_child("RedY")->add_child_text (raw_convert<string> (_red.y));
- node->add_child("GreenX")->add_child_text (raw_convert<string> (_green.x));
- node->add_child("GreenY")->add_child_text (raw_convert<string> (_green.y));
- node->add_child("BlueX")->add_child_text (raw_convert<string> (_blue.x));
- node->add_child("BlueY")->add_child_text (raw_convert<string> (_blue.y));
- node->add_child("WhiteX")->add_child_text (raw_convert<string> (_white.x));
- node->add_child("WhiteY")->add_child_text (raw_convert<string> (_white.y));
+ cxml::add_text_child(element, "YUVToRGB", raw_convert<string>(static_cast<int>(_yuv_to_rgb)));
+ cxml::add_text_child(element, "RedX", raw_convert<string>(_red.x));
+ cxml::add_text_child(element, "RedY", raw_convert<string>(_red.y));
+ cxml::add_text_child(element, "GreenX", raw_convert<string>(_green.x));
+ cxml::add_text_child(element, "GreenY", raw_convert<string>(_green.y));
+ cxml::add_text_child(element, "BlueX", raw_convert<string>(_blue.x));
+ cxml::add_text_child(element, "BlueY", raw_convert<string>(_blue.y));
+ cxml::add_text_child(element, "WhiteX", raw_convert<string>(_white.x));
+ cxml::add_text_child(element, "WhiteY", raw_convert<string>(_white.y));
if (_adjusted_white) {
- node->add_child("AdjustedWhiteX")->add_child_text (raw_convert<string> (_adjusted_white.get().x));
- node->add_child("AdjustedWhiteY")->add_child_text (raw_convert<string> (_adjusted_white.get().y));
+ cxml::add_text_child(element, "AdjustedWhiteX", raw_convert<string>(_adjusted_white.get().x));
+ cxml::add_text_child(element, "AdjustedWhiteY", raw_convert<string>(_adjusted_white.get().y));
}
- if (dynamic_pointer_cast<const dcp::GammaTransferFunction> (_out)) {
- shared_ptr<const dcp::GammaTransferFunction> gf = dynamic_pointer_cast<const dcp::GammaTransferFunction> (_out);
- node->add_child("OutputGamma")->add_child_text (raw_convert<string> (gf->gamma ()));
+ if (auto gf = dynamic_pointer_cast<const dcp::GammaTransferFunction>(_out)) {
+ cxml::add_text_child(element, "OutputGamma", raw_convert<string>(gf->gamma()));
}
}
diff --git a/src/lib/colour_conversion.h b/src/lib/colour_conversion.h
index 73b6ad23c..0c07ddbac 100644
--- a/src/lib/colour_conversion.h
+++ b/src/lib/colour_conversion.h
@@ -41,7 +41,7 @@ public:
ColourConversion (cxml::NodePtr, int version);
virtual ~ColourConversion () {}
- virtual void as_xml (xmlpp::Node *) const;
+ virtual void as_xml(xmlpp::Element*) const;
std::string identifier () const;
boost::optional<size_t> preset () const;
diff --git a/src/lib/config.cc b/src/lib/config.cc
index 384db5cde..1ca73c400 100644
--- a/src/lib/config.cc
+++ b/src/lib/config.cc
@@ -19,20 +19,21 @@
*/
-#include "cinema.h"
+#include "cinema_list.h"
#include "colour_conversion.h"
#include "compose.hpp"
#include "config.h"
#include "constants.h"
#include "cross.h"
#include "dcp_content_type.h"
-#include "dkdm_recipient.h"
+#include "dkdm_recipient_list.h"
#include "dkdm_wrapper.h"
#include "film.h"
#include "filter.h"
#include "log.h"
#include "ratio.h"
#include "unzipper.h"
+#include "variant.h"
#include "zipper.h"
#include <dcp/certificate_chain.h>
#include <dcp/name_format.h>
@@ -107,7 +108,8 @@ Config::set_defaults ()
_default_still_length = 10;
_default_dcp_content_type = DCPContentType::from_isdcf_name ("FTR");
_default_dcp_audio_channels = 8;
- _default_j2k_bandwidth = 150000000;
+ _default_video_bit_rate[VideoEncoding::JPEG2000] = 150000000;
+ _default_video_bit_rate[VideoEncoding::MPEG2] = 5000000;
_default_audio_delay = 0;
_default_interop = false;
_default_metadata.clear ();
@@ -126,7 +128,8 @@ Config::set_defaults ()
_notification_bcc = "";
_check_for_updates = false;
_check_for_test_updates = false;
- _maximum_j2k_bandwidth = 250000000;
+ _maximum_video_bit_rate[VideoEncoding::JPEG2000] = 250000000;
+ _maximum_video_bit_rate[VideoEncoding::MPEG2] = 50000000;
_log_types = LogEntry::TYPE_GENERAL | LogEntry::TYPE_WARNING | LogEntry::TYPE_ERROR | LogEntry::TYPE_DISK;
_analyse_ebur128 = true;
_automatic_audio_analysis = false;
@@ -136,8 +139,8 @@ Config::set_defaults ()
/* At the moment we don't write these files anywhere new after a version change, so they will be read from
* ~/.config/dcpomatic2 (or equivalent) and written back there.
*/
- _cinemas_file = read_path ("cinemas.xml");
- _dkdm_recipients_file = read_path ("dkdm_recipients.xml");
+ _cinemas_file = read_path("cinemas.sqlite3");
+ _dkdm_recipients_file = read_path("dkdm_recipients.sqlite3");
_show_hints_before_make_dcp = true;
_confirm_kdm_email = true;
_kdm_container_name_format = dcp::NameFormat("KDM_%f_%c");
@@ -177,6 +180,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;
@@ -198,6 +203,8 @@ Config::set_defaults ()
_initial_paths["CinemaDatabasePath"] = boost::none;
_initial_paths["ConfigFilePath"] = boost::none;
_initial_paths["Preferences"] = boost::none;
+ _initial_paths["SaveVerificationReport"] = boost::none;
+ _initial_paths["CopySettingsPath"] = boost::none;
_use_isdcf_name_by_default = true;
_write_kdms_to_disk = true;
_email_kdms = false;
@@ -220,6 +227,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 = {};
@@ -266,7 +277,7 @@ Config::backup ()
copy_file(path_to_copy, add_number(path_to_copy, n), ec);
};
- /* Make a backup copy of any config.xml, cinemas.xml, dkdm_recipients.xml that we might be about
+ /* Make a backup copy of any config.xml, cinemas.sqlite3, dkdm_recipients.sqlite3 that we might be about
* to write over. This is more intended for the situation where we have a corrupted config.xml,
* and decide to overwrite it with a new one (possibly losing important details in the corrupted
* file). But we might as well back up the other files while we're about it.
@@ -286,15 +297,6 @@ Config::backup ()
void
Config::read ()
-{
- read_config();
- read_cinemas();
- read_dkdm_recipients();
-}
-
-
-void
-Config::read_config()
try
{
cxml::Document f ("Config");
@@ -367,7 +369,12 @@ try
_dcp_j2k_comment = f.optional_string_child("DCPJ2KComment").get_value_or("");
_default_still_length = f.optional_number_child<int>("DefaultStillLength").get_value_or (10);
- _default_j2k_bandwidth = f.optional_number_child<int>("DefaultJ2KBandwidth").get_value_or (200000000);
+ if (auto j2k = f.optional_number_child<int>("DefaultJ2KBandwidth")) {
+ _default_video_bit_rate[VideoEncoding::JPEG2000] = *j2k;
+ } else {
+ _default_video_bit_rate[VideoEncoding::JPEG2000] = f.optional_number_child<int64_t>("DefaultJ2KVideoBitRate").get_value_or(200000000);
+ }
+ _default_video_bit_rate[VideoEncoding::MPEG2] = f.optional_number_child<int64_t>("DefaultMPEG2VideoBitRate").get_value_or(5000000);
_default_audio_delay = f.optional_number_child<int>("DefaultAudioDelay").get_value_or (0);
_default_interop = f.optional_bool_child("DefaultInterop").get_value_or (false);
@@ -391,11 +398,6 @@ try
_default_kdm_directory = f.optional_string_child("DefaultKDMDirectory");
- /* Read any cinemas that are still lying around in the config file
- * from an old version.
- */
- read_cinemas (f);
-
_mail_server = f.string_child ("MailServer");
_mail_port = f.optional_number_child<int> ("MailPort").get_value_or (25);
@@ -426,7 +428,7 @@ try
_kdm_bcc = f.optional_string_child ("KDMBCC").get_value_or ("");
_kdm_email = f.string_child ("KDMEmail");
- _notification_subject = f.optional_string_child("NotificationSubject").get_value_or(_("DCP-o-matic notification"));
+ _notification_subject = f.optional_string_child("NotificationSubject").get_value_or(variant::insert_dcpomatic(_("%1 notification")));
_notification_from = f.optional_string_child("NotificationFrom").get_value_or("");
_notification_to = f.optional_string_child("NotificationTo").get_value_or("");
for (auto i: f.node_children("NotificationCC")) {
@@ -442,7 +444,12 @@ try
_check_for_updates = f.optional_bool_child("CheckForUpdates").get_value_or (false);
_check_for_test_updates = f.optional_bool_child("CheckForTestUpdates").get_value_or (false);
- _maximum_j2k_bandwidth = f.optional_number_child<int> ("MaximumJ2KBandwidth").get_value_or (250000000);
+ if (auto j2k = f.optional_number_child<int>("MaximumJ2KBandwidth")) {
+ _maximum_video_bit_rate[VideoEncoding::JPEG2000] = *j2k;
+ } else {
+ _maximum_video_bit_rate[VideoEncoding::JPEG2000] = f.optional_number_child<int64_t>("MaximumJ2KVideoBitRate").get_value_or(250000000);
+ }
+ _maximum_video_bit_rate[VideoEncoding::MPEG2] = f.optional_number_child<int64_t>("MaximumMPEG2VideoBitRate").get_value_or(50000000);
_allow_any_dcp_frame_rate = f.optional_bool_child ("AllowAnyDCPFrameRate").get_value_or (false);
_allow_any_container = f.optional_bool_child ("AllowAnyContainer").get_value_or (false);
_allow_96khz_audio = f.optional_bool_child("Allow96kHzAudio").get_value_or(false);
@@ -494,7 +501,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());
}
@@ -528,8 +535,8 @@ try
_dkdms->add (DKDMBase::read (i));
}
}
- _cinemas_file = f.optional_string_child("CinemasFile").get_value_or(read_path("cinemas.xml").string());
- _dkdm_recipients_file = f.optional_string_child("DKDMRecipientsFile").get_value_or(read_path("dkdm_recipients.xml").string());
+ _cinemas_file = f.optional_string_child("CinemasFile").get_value_or(read_path("cinemas.sqlite3").string());
+ _dkdm_recipients_file = f.optional_string_child("DKDMRecipientsFile").get_value_or(read_path("dkdm_recipients.sqlite3").string());
_show_hints_before_make_dcp = f.optional_bool_child("ShowHintsBeforeMakeDCP").get_value_or (true);
_confirm_kdm_email = f.optional_bool_child("ConfirmKDMEmail").get_value_or (true);
_kdm_container_name_format = dcp::NameFormat (f.optional_string_child("KDMContainerNameFormat").get_value_or ("KDM %f %c"));
@@ -566,7 +573,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 +595,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 +652,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 (...) {
@@ -659,40 +675,6 @@ catch (...) {
}
-void
-Config::read_cinemas()
-{
- if (dcp::filesystem::exists(_cinemas_file)) {
- try {
- cxml::Document f("Cinemas");
- f.read_file(dcp::filesystem::fix_long_path(_cinemas_file));
- read_cinemas(f);
- } catch (...) {
- backup();
- FailedToLoad(LoadFailure::CINEMAS);
- write_cinemas();
- }
- }
-}
-
-
-void
-Config::read_dkdm_recipients()
-{
- if (dcp::filesystem::exists(_dkdm_recipients_file)) {
- try {
- cxml::Document f("DKDMRecipients");
- f.read_file(dcp::filesystem::fix_long_path(_dkdm_recipients_file));
- read_dkdm_recipients(f);
- } catch (...) {
- backup();
- FailedToLoad(LoadFailure::DKDM_RECIPIENTS);
- write_dkdm_recipients();
- }
- }
-}
-
-
/** @return Singleton instance */
Config *
Config::instance ()
@@ -700,6 +682,32 @@ Config::instance ()
if (_instance == nullptr) {
_instance = new Config;
_instance->read ();
+
+ auto cinemas_file = _instance->cinemas_file();
+ if (cinemas_file.extension() == ".xml") {
+ auto sqlite = cinemas_file;
+ sqlite.replace_extension(".sqlite3");
+ bool const had_sqlite = dcp::filesystem::exists(sqlite);
+
+ _instance->set_cinemas_file(sqlite);
+
+ if (dcp::filesystem::exists(cinemas_file) && !had_sqlite) {
+ CinemaList cinemas;
+ cinemas.read_legacy_file(cinemas_file);
+ }
+ }
+
+ auto dkdm_recipients_file = _instance->dkdm_recipients_file();
+ if (dkdm_recipients_file.extension() == ".xml") {
+ auto sqlite = dkdm_recipients_file;
+ sqlite.replace_extension(".sqlite3");
+
+ if (dcp::filesystem::exists(dkdm_recipients_file) && !dcp::filesystem::exists(sqlite)) {
+ _instance->set_dkdm_recipients_file(sqlite);
+ DKDMRecipientList recipients;
+ recipients.read_legacy_file(dkdm_recipients_file);
+ }
+ }
}
return _instance;
@@ -710,8 +718,6 @@ void
Config::write () const
{
write_config ();
- write_cinemas ();
- write_dkdm_recipients ();
}
void
@@ -721,218 +727,199 @@ 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 ());
- }
- /* [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));
/* [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));
- /* [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, "DefaultStillLength", raw_convert<string>(_default_still_length));
/* [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));
- /* [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, "DefaultAudioDelay", raw_convert<string>(_default_audio_delay));
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());
- }
- if (_default_territory) {
- /* [XML] DefaultTerritory Default territory to use for new films */
- root->add_child("DefaultTerritory")->add_child_text(_default_territory->subtag());
- }
- for (auto const& i: _default_metadata) {
- auto c = root->add_child("DefaultMetadata");
- c->set_attribute("key", i.first);
- c->add_child_text(i.second);
+ cxml::add_text_child(root, "DefaultAudioLanguage", _default_audio_language->to_string());
}
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));
+ /* [XML] MaximumJ2KVideoBitRate Maximum video bit rate (in bits per second) that can be specified in the GUI for JPEG2000 encodes. */
+ cxml::add_text_child(root, "MaximumJ2KVideoBitRate", raw_convert<string>(_maximum_video_bit_rate[VideoEncoding::JPEG2000]));
+ /* [XML] MaximumMPEG2VideoBitRate Maximum video bit rate (in bits per second) that can be specified in the GUI for MPEG2 encodes. */
+ cxml::add_text_child(root, "MaximumMPEG2VideoBitRate", raw_convert<string>(_maximum_video_bit_rate[VideoEncoding::MPEG2]));
/* [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 +929,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 +983,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 +1042,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));
+
+#ifdef DCPOMATIC_GROK
+ if (_grok) {
+ _grok->as_xml(cxml::add_child(root, "Grok"));
+ }
+#endif
- _export.write(root->add_child("Export"));
+ _export.write(cxml::add_child(root, "Export"));
auto target = config_write_file();
@@ -1157,10 +1158,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 {
@@ -1175,20 +1176,6 @@ write_file (string root_node, string node, string version, list<shared_ptr<T>> t
}
-void
-Config::write_cinemas () const
-{
- write_file ("Cinemas", "Cinema", "1", _cinemas, _cinemas_file);
-}
-
-
-void
-Config::write_dkdm_recipients () const
-{
- write_file ("DKDMRecipients", "DKDMRecipient", "1", _dkdm_recipients, _dkdm_recipients_file);
-}
-
-
boost::filesystem::path
Config::default_directory_or (boost::filesystem::path a) const
{
@@ -1235,20 +1222,20 @@ Config::set_kdm_email_to_default ()
{
_kdm_subject = _("KDM delivery: $CPL_NAME");
- _kdm_email = _(
+ _kdm_email = variant::insert_dcpomatic(_(
"Dear Projectionist\n\n"
"Please find attached KDMs for $CPL_NAME.\n\n"
"Cinema: $CINEMA_NAME\n"
"Screen(s): $SCREENS\n\n"
"The KDMs are valid from $START_TIME until $END_TIME.\n\n"
- "Best regards,\nDCP-o-matic"
- );
+ "Best regards,\n%1"
+ ));
}
void
Config::set_notification_email_to_default ()
{
- _notification_subject = _("DCP-o-matic notification");
+ _notification_subject = variant::insert_dcpomatic(_("%1 notification"));
_notification_email = _(
"$JOB_NAME: $JOB_STATUS"
@@ -1319,7 +1306,7 @@ Config::add_to_history_internal (vector<boost::filesystem::path>& h, boost::file
h.insert (h.begin (), p);
if (h.size() > HISTORY_SIZE) {
- h.pop_back ();
+ h.resize(HISTORY_SIZE);
}
changed (HISTORY);
@@ -1350,20 +1337,6 @@ Config::have_existing (string file)
void
-Config::read_cinemas (cxml::Document const & f)
-{
- _cinemas.clear ();
- for (auto i: f.node_children("Cinema")) {
- /* Slightly grotty two-part construction of Cinema here so that we can use
- shared_from_this.
- */
- auto cinema = make_shared<Cinema>(i);
- cinema->read_screens (i);
- _cinemas.push_back (cinema);
- }
-}
-
-void
Config::set_cinemas_file (boost::filesystem::path file)
{
if (file == _cinemas_file) {
@@ -1372,25 +1345,27 @@ Config::set_cinemas_file (boost::filesystem::path file)
_cinemas_file = file;
- if (dcp::filesystem::exists(_cinemas_file)) {
- /* Existing file; read it in */
- cxml::Document f ("Cinemas");
- f.read_file(dcp::filesystem::fix_long_path(_cinemas_file));
- read_cinemas (f);
- }
-
- changed (CINEMAS);
changed (OTHER);
}
void
-Config::read_dkdm_recipients (cxml::Document const & f)
+Config::set_dkdm_recipients_file(boost::filesystem::path file)
{
- _dkdm_recipients.clear ();
- for (auto i: f.node_children("DKDMRecipient")) {
- _dkdm_recipients.push_back (make_shared<DKDMRecipient>(i));
+ if (file == _dkdm_recipients_file) {
+ return;
}
+
+ _dkdm_recipients_file = file;
+
+ changed(OTHER);
+}
+
+
+void
+Config::save_default_template(shared_ptr<const Film> film) const
+{
+ film->write_template(write_path("default.xml"));
}
@@ -1401,14 +1376,14 @@ Config::save_template (shared_ptr<const Film> film, string name) const
}
-list<string>
+vector<string>
Config::templates () const
{
if (!dcp::filesystem::exists(read_path("templates"))) {
return {};
}
- list<string> n;
+ vector<string> n;
for (auto const& i: dcp::filesystem::directory_iterator(read_path("templates"))) {
n.push_back (i.path().filename().string());
}
@@ -1430,6 +1405,18 @@ Config::template_read_path (string name) const
boost::filesystem::path
+Config::default_template_read_path() const
+{
+ if (!boost::filesystem::exists(read_path("default.xml"))) {
+ auto film = std::make_shared<const Film>(optional<boost::filesystem::path>());
+ save_default_template(film);
+ }
+
+ return read_path("default.xml");
+}
+
+
+boost::filesystem::path
Config::template_write_path (string name) const
{
return write_path("templates") / tidy_for_filename (name);
@@ -1500,7 +1487,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) {
@@ -1623,10 +1610,10 @@ save_all_config_as_zip (boost::filesystem::path zip_file)
auto config = Config::instance();
zipper.add ("config.xml", dcp::file_to_string(config->config_read_file()));
if (dcp::filesystem::exists(config->cinemas_file())) {
- zipper.add ("cinemas.xml", dcp::file_to_string(config->cinemas_file()));
+ zipper.add("cinemas.sqlite3", dcp::file_to_string(config->cinemas_file()));
}
if (dcp::filesystem::exists(config->dkdm_recipients_file())) {
- zipper.add ("dkdm_recipients.xml", dcp::file_to_string(config->dkdm_recipients_file()));
+ zipper.add("dkdm_recipients.sqlite3", dcp::file_to_string(config->dkdm_recipients_file()));
}
zipper.close ();
@@ -1634,22 +1621,58 @@ save_all_config_as_zip (boost::filesystem::path zip_file)
void
-Config::load_from_zip(boost::filesystem::path zip_file)
+Config::load_from_zip(boost::filesystem::path zip_file, CinemasAction action)
{
+ backup();
+
+ auto const current_cinemas = cinemas_file();
+ /* This is (unfortunately) a full path, and the user can't change it, so
+ * we always want to use that same path in the future no matter what is in the
+ * config.xml that we are about to load.
+ */
+ auto const current_dkdm_recipients = dkdm_recipients_file();
+
Unzipper unzipper(zip_file);
dcp::write_string_to_file(unzipper.get("config.xml"), config_write_file());
- try {
- dcp::write_string_to_file(unzipper.get("cinemas.xml"), cinemas_file());
- dcp::write_string_to_file(unzipper.get("dkdm_recipient.xml"), dkdm_recipients_file());
- } catch (std::runtime_error&) {}
+ if (action == CinemasAction::WRITE_TO_PATH_IN_ZIPPED_CONFIG) {
+ /* Read the zipped config, so that the cinemas file path is the new one and
+ * we write the cinemas to it.
+ */
+ read();
+ boost::filesystem::create_directories(cinemas_file().parent_path());
+ set_dkdm_recipients_file(current_dkdm_recipients);
+ }
- read();
+ if (unzipper.contains("cinemas.xml") && action != CinemasAction::IGNORE) {
+ CinemaList cinemas;
+ cinemas.clear();
+ cinemas.read_legacy_string(unzipper.get("cinemas.xml"));
+ }
+
+ if (unzipper.contains("dkdm_recipients.xml")) {
+ DKDMRecipientList recipients;
+ recipients.clear();
+ recipients.read_legacy_string(unzipper.get("dkdm_recipients.xml"));
+ }
+
+ if (unzipper.contains("cinemas.sqlite3") && action != CinemasAction::IGNORE) {
+ dcp::write_string_to_file(unzipper.get("cinemas.sqlite3"), cinemas_file());
+ }
+
+ if (unzipper.contains("dkdm_recipients.sqlite3")) {
+ dcp::write_string_to_file(unzipper.get("dkdm_recipients.sqlite3"), dkdm_recipients_file());
+ }
+
+ if (action != CinemasAction::WRITE_TO_PATH_IN_ZIPPED_CONFIG) {
+ /* Read the zipped config, then reset the cinemas file to be the old one */
+ read();
+ set_cinemas_file(current_cinemas);
+ set_dkdm_recipients_file(current_dkdm_recipients);
+ }
changed(Property::USE_ANY_SERVERS);
changed(Property::SERVERS);
- changed(Property::CINEMAS);
- changed(Property::DKDM_RECIPIENTS);
changed(Property::SOUND);
changed(Property::SOUND_OUTPUT);
changed(Property::PLAYER_CONTENT_DIRECTORY);
@@ -1685,3 +1708,57 @@ Config::initial_path(string id) const
return iter->second;
}
+
+bool
+Config::zip_contains_cinemas(boost::filesystem::path zip)
+{
+ Unzipper unzipper(zip);
+ return unzipper.contains("cinemas.sqlite3") || unzipper.contains("cinemas.xml");
+}
+
+
+boost::filesystem::path
+Config::cinemas_file_from_zip(boost::filesystem::path zip)
+{
+ Unzipper unzipper(zip);
+ DCPOMATIC_ASSERT(unzipper.contains("config.xml"));
+ cxml::Document document("Config");
+ document.read_string(unzipper.get("config.xml"));
+ return document.string_child("CinemasFile");
+}
+
+
+#ifdef DCPOMATIC_GROK
+
+Config::Grok::Grok(cxml::ConstNodePtr node)
+ : enable(node->bool_child("Enable"))
+ , binary_location(node->string_child("BinaryLocation"))
+ , selected(node->number_child<int>("Selected"))
+ , licence_server(node->string_child("LicenceServer"))
+ , licence_port(node->number_child<int>("LicencePort"))
+ , licence(node->string_child("Licence"))
+{
+
+}
+
+
+void
+Config::Grok::as_xml(xmlpp::Element* node) const
+{
+ node->add_child("BinaryLocation")->add_child_text(binary_location.string());
+ node->add_child("Enable")->add_child_text((enable ? "1" : "0"));
+ node->add_child("Selected")->add_child_text(raw_convert<string>(selected));
+ node->add_child("LicenceServer")->add_child_text(licence_server);
+ node->add_child("LicencePort")->add_child_text(raw_convert<string>(licence_port));
+ node->add_child("Licence")->add_child_text(licence);
+}
+
+
+void
+Config::set_grok(Grok const& grok)
+{
+ _grok = grok;
+ changed(GROK);
+}
+
+#endif
diff --git a/src/lib/config.h b/src/lib/config.h
index f3d080b0b..397af2222 100644
--- a/src/lib/config.h
+++ b/src/lib/config.h
@@ -28,9 +28,11 @@
#include "audio_mapping.h"
+#include "enum_indexed_vector.h"
#include "export_config.h"
#include "rough_duration.h"
#include "state.h"
+#include "video_encoding.h"
#include <dcp/name_format.h>
#include <dcp/certificate_chain.h>
#include <dcp/encrypted_kdm.h>
@@ -48,6 +50,8 @@ class DKDMRecipient;
class Film;
class Ratio;
+#undef IGNORE
+
extern void save_all_config_as_zip (boost::filesystem::path zip_file);
@@ -79,13 +83,28 @@ public:
boost::filesystem::path default_directory_or (boost::filesystem::path a) const;
boost::filesystem::path default_kdm_directory_or (boost::filesystem::path a) const;
- void load_from_zip(boost::filesystem::path zip_file);
+ enum class CinemasAction
+ {
+ /** Copy the cinemas.{xml,sqlite3} in the ZIP file to the path
+ * specified in the current config, overwriting whatever is there,
+ * and use that path.
+ */
+ WRITE_TO_CURRENT_PATH,
+ /** Copy the cinemas.{xml,sqlite3} in the ZIP file over the path
+ * specified in the config.xml from the ZIP, overwriting whatever
+ * is there and creating any required directories, and use
+ * that path.
+ */
+ WRITE_TO_PATH_IN_ZIPPED_CONFIG,
+ /** Do nothing with the cinemas.{xml,sqlite3} in the ZIP file */
+ IGNORE
+ };
+
+ void load_from_zip(boost::filesystem::path zip_file, CinemasAction action);
enum Property {
USE_ANY_SERVERS,
SERVERS,
- CINEMAS,
- DKDM_RECIPIENTS,
SOUND,
SOUND_OUTPUT,
PLAYER_CONTENT_DIRECTORY,
@@ -97,6 +116,10 @@ public:
AUTO_CROP_THRESHOLD,
ALLOW_SMPTE_BV20,
ISDCF_NAME_PART_LENGTH,
+ ALLOW_ANY_CONTAINER,
+#ifdef DCPOMATIC_GROK
+ GROK,
+#endif
OTHER
};
@@ -157,14 +180,6 @@ public:
return _tms_password;
}
- std::list<std::shared_ptr<Cinema>> cinemas () const {
- return _cinemas;
- }
-
- std::list<std::shared_ptr<DKDMRecipient>> dkdm_recipients () const {
- return _dkdm_recipients;
- }
-
std::list<int> allowed_dcp_frame_rates () const {
return _allowed_dcp_frame_rates;
}
@@ -229,8 +244,8 @@ public:
return _dcp_j2k_comment;
}
- int default_j2k_bandwidth () const {
- return _default_j2k_bandwidth;
+ int64_t default_video_bit_rate(VideoEncoding encoding) const {
+ return _default_video_bit_rate[encoding];
}
int default_audio_delay () const {
@@ -345,8 +360,8 @@ public:
return _check_for_test_updates;
}
- int maximum_j2k_bandwidth () const {
- return _maximum_j2k_bandwidth;
+ int64_t maximum_video_bit_rate(VideoEncoding encoding) const {
+ return _maximum_video_bit_rate[encoding];
}
int log_types () const {
@@ -531,6 +546,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 +644,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;
}
@@ -680,26 +725,6 @@ public:
maybe_set (_tms_password, p);
}
- void add_cinema (std::shared_ptr<Cinema> c) {
- _cinemas.push_back (c);
- changed (CINEMAS);
- }
-
- void remove_cinema (std::shared_ptr<Cinema> c) {
- _cinemas.remove (c);
- changed (CINEMAS);
- }
-
- void add_dkdm_recipient (std::shared_ptr<DKDMRecipient> c) {
- _dkdm_recipients.push_back (c);
- changed (DKDM_RECIPIENTS);
- }
-
- void remove_dkdm_recipient (std::shared_ptr<DKDMRecipient> c) {
- _dkdm_recipients.remove (c);
- changed (DKDM_RECIPIENTS);
- }
-
void set_allowed_dcp_frame_rates (std::list<int> const & r) {
maybe_set (_allowed_dcp_frame_rates, r);
}
@@ -709,7 +734,7 @@ public:
}
void set_allow_any_container (bool a) {
- maybe_set (_allow_any_container, a);
+ maybe_set(_allow_any_container, a, ALLOW_ANY_CONTAINER);
}
void set_allow_96hhz_audio (bool a) {
@@ -745,14 +770,6 @@ public:
maybe_set (_default_still_length, s);
}
- void set_default_dcp_content_type (DCPContentType const * t) {
- maybe_set (_default_dcp_content_type, t);
- }
-
- void set_default_dcp_audio_channels (int c) {
- maybe_set (_default_dcp_audio_channels, c);
- }
-
void set_dcp_issuer (std::string i) {
maybe_set (_dcp_issuer, i);
}
@@ -777,18 +794,10 @@ public:
maybe_set (_dcp_j2k_comment, c);
}
- void set_default_j2k_bandwidth (int b) {
- maybe_set (_default_j2k_bandwidth, b);
- }
-
void set_default_audio_delay (int d) {
maybe_set (_default_audio_delay, d);
}
- void set_default_interop (bool i) {
- maybe_set (_default_interop, i);
- }
-
void set_default_audio_language(dcp::LanguageTag tag) {
maybe_set(_default_audio_language, tag);
}
@@ -797,18 +806,6 @@ public:
maybe_set(_default_audio_language, boost::optional<dcp::LanguageTag>());
}
- void set_default_territory(dcp::LanguageTag::RegionSubtag tag) {
- maybe_set(_default_territory, tag);
- }
-
- void unset_default_territory() {
- maybe_set(_default_territory, boost::optional<dcp::LanguageTag::RegionSubtag>());
- }
-
- void set_default_metadata (std::map<std::string, std::string> const& metadata) {
- maybe_set (_default_metadata, metadata);
- }
-
void set_upload_after_make_dcp (bool u) {
maybe_set (_upload_after_make_dcp, u);
}
@@ -900,8 +897,8 @@ public:
maybe_set (_check_for_test_updates, c);
}
- void set_maximum_j2k_bandwidth (int b) {
- maybe_set (_maximum_j2k_bandwidth, b);
+ void set_maximum_video_bit_rate(VideoEncoding encoding, int64_t b) {
+ maybe_set(_maximum_video_bit_rate[encoding], b);
}
void set_log_types (int t) {
@@ -929,6 +926,8 @@ public:
void set_cinemas_file (boost::filesystem::path file);
+ void set_dkdm_recipients_file(boost::filesystem::path file);
+
void set_show_hints_before_make_dcp (bool s) {
maybe_set (_show_hints_before_make_dcp, s);
}
@@ -1202,10 +1201,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
@@ -1213,8 +1217,6 @@ public:
*/
enum class LoadFailure {
CONFIG,
- CINEMAS,
- DKDM_RECIPIENTS
};
static boost::signals2::signal<void (LoadFailure)> FailedToLoad;
/** Emitted if read() issued a warning which the user might want to know about */
@@ -1234,17 +1236,17 @@ public:
void write () const override;
void write_config () const;
- void write_cinemas () const;
- void write_dkdm_recipients () const;
void link (boost::filesystem::path new_file) const;
void copy_and_link (boost::filesystem::path new_file) const;
bool have_write_permission () const;
+ void save_default_template(std::shared_ptr<const Film> film) const;
void save_template (std::shared_ptr<const Film> film, std::string name) const;
bool existing_template (std::string name) const;
- std::list<std::string> templates () const;
+ /** @return Template names (not including the default) */
+ std::vector<std::string> templates() const;
boost::filesystem::path template_read_path (std::string name) const;
- boost::filesystem::path template_write_path (std::string name) const;
+ boost::filesystem::path default_template_read_path() const;
void rename_template (std::string old_name, std::string new_name) const;
void delete_template (std::string name) const;
@@ -1256,6 +1258,9 @@ public:
static bool have_existing (std::string);
static boost::filesystem::path config_read_file ();
static boost::filesystem::path config_write_file ();
+ static bool zip_contains_cinemas(boost::filesystem::path zip);
+ static boost::filesystem::path cinemas_file_from_zip(boost::filesystem::path zip);
+
template <class T>
void maybe_set (T& member, T new_value, Property prop = OTHER) {
@@ -1278,20 +1283,16 @@ public:
private:
Config ();
void read () override;
- void read_config();
- void read_cinemas();
- void read_dkdm_recipients();
void set_defaults ();
void set_kdm_email_to_default ();
void set_notification_email_to_default ();
void set_cover_sheet_to_default ();
- void read_cinemas (cxml::Document const & f);
- void read_dkdm_recipients (cxml::Document const & f);
std::shared_ptr<dcp::CertificateChain> create_certificate_chain ();
boost::filesystem::path directory_or (boost::optional<boost::filesystem::path> dir, boost::filesystem::path a) const;
void add_to_history_internal (std::vector<boost::filesystem::path>& h, boost::filesystem::path p);
void clean_history_internal (std::vector<boost::filesystem::path>& h);
void backup ();
+ boost::filesystem::path template_write_path(std::string name) const;
/** number of threads which a master DoM should use for J2K encoding on the local machine */
int _master_encoding_threads;
@@ -1342,7 +1343,7 @@ private:
std::string _dcp_product_name;
std::string _dcp_product_version;
std::string _dcp_j2k_comment;
- int _default_j2k_bandwidth;
+ EnumIndexedVector<int64_t, VideoEncoding> _default_video_bit_rate;
int _default_audio_delay;
bool _default_interop;
boost::optional<dcp::LanguageTag> _default_audio_language;
@@ -1353,8 +1354,6 @@ private:
*/
boost::optional<boost::filesystem::path> _default_kdm_directory;
bool _upload_after_make_dcp;
- std::list<std::shared_ptr<Cinema>> _cinemas;
- std::list<std::shared_ptr<DKDMRecipient>> _dkdm_recipients;
std::string _mail_server;
int _mail_port;
EmailProtocol _mail_protocol;
@@ -1379,8 +1378,8 @@ private:
/** true to check for updates on startup */
bool _check_for_updates;
bool _check_for_test_updates;
- /** maximum allowed J2K bandwidth in bits per second */
- int _maximum_j2k_bandwidth;
+ /** maximum allowed video bit rate in bits per second */
+ EnumIndexedVector<int64_t, VideoEncoding> _maximum_video_bit_rate;
int _log_types;
bool _analyse_ebur128;
bool _automatic_audio_analysis;
@@ -1419,6 +1418,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 +1448,10 @@ private:
bool _allow_smpte_bv20;
int _isdcf_name_part_length;
+#ifdef DCPOMATIC_GROK
+ boost::optional<Grok> _grok;
+#endif
+
ExportConfig _export;
static int const _current_version;
diff --git a/src/lib/constants.h b/src/lib/constants.h
index 3b1871554..cfa156778 100644
--- a/src/lib/constants.h
+++ b/src/lib/constants.h
@@ -29,7 +29,6 @@
#define DCPOMATIC_HELLO "I mean really, Ray, it's used."
/** Number of films to keep in history */
#define HISTORY_SIZE 10
-#define REPORT_PROBLEM _("Please report this problem by using Help -> Report a problem or via email to carl@dcpomatic.com")
#define TEXT_FONT_ID "font"
/** Largest KDM size (in bytes) that will be accepted */
#define MAX_KDM_SIZE (256 * 1024)
@@ -47,6 +46,7 @@
#define MAX_CLOSED_CAPTION_XML_SIZE (256 * 1024)
#define MAX_CLOSED_CAPTION_XML_SIZE_TEXT "256KB"
#define CERTIFICATE_VALIDITY_PERIOD (10 * 365)
+#define SNAP_SUBDIVISION 64
#endif
diff --git a/src/lib/content.cc b/src/lib/content.cc
index f13201b93..496a68a63 100644
--- a/src/lib/content.cc
+++ b/src/lib/content.cc
@@ -140,23 +140,23 @@ Content::Content (vector<shared_ptr<Content>> c)
void
-Content::as_xml (xmlpp::Node* node, bool with_paths) const
+Content::as_xml(xmlpp::Element* element, bool with_paths) const
{
boost::mutex::scoped_lock lm (_mutex);
if (with_paths) {
for (size_t i = 0; i < _paths.size(); ++i) {
- auto p = node->add_child("Path");
+ auto p = cxml::add_child(element, "Path");
p->add_child_text (_paths[i].string());
p->set_attribute ("mtime", raw_convert<string>(_last_write_times[i]));
}
}
- node->add_child("Digest")->add_child_text(_digest);
- node->add_child("Position")->add_child_text(raw_convert<string>(_position.get()));
- node->add_child("TrimStart")->add_child_text(raw_convert<string>(_trim_start.get()));
- node->add_child("TrimEnd")->add_child_text(raw_convert<string>(_trim_end.get()));
+ cxml::add_text_child(element, "Digest", _digest);
+ cxml::add_text_child(element, "Position", raw_convert<string>(_position.get()));
+ cxml::add_text_child(element, "TrimStart", raw_convert<string>(_trim_start.get()));
+ cxml::add_text_child(element, "TrimEnd", raw_convert<string>(_trim_end.get()));
if (_video_frame_rate) {
- node->add_child("VideoFrameRate")->add_child_text(raw_convert<string>(_video_frame_rate.get()));
+ cxml::add_text_child(element, "VideoFrameRate", raw_convert<string>(_video_frame_rate.get()));
}
}
diff --git a/src/lib/content.h b/src/lib/content.h
index 540abdd8a..d4b99d39e 100644
--- a/src/lib/content.h
+++ b/src/lib/content.h
@@ -100,7 +100,7 @@ public:
*/
virtual std::string technical_summary () const;
- virtual void as_xml (xmlpp::Node *, bool with_paths) const;
+ virtual void as_xml(xmlpp::Element* element, bool with_paths) const;
virtual dcpomatic::DCPTime full_length (std::shared_ptr<const Film>) const = 0;
virtual dcpomatic::DCPTime approximate_length () const = 0;
virtual std::string identifier () const;
diff --git a/src/lib/content_video.h b/src/lib/content_video.h
index 4fdab717a..1c145f602 100644
--- a/src/lib/content_video.h
+++ b/src/lib/content_video.h
@@ -23,6 +23,7 @@
#define DCPOMATIC_CONTENT_VIDEO_H
+#include "dcpomatic_time.h"
#include "types.h"
@@ -36,22 +37,22 @@ class ContentVideo
{
public:
ContentVideo ()
- : frame (0)
- , eyes (Eyes::LEFT)
+ : eyes (Eyes::LEFT)
, part (Part::WHOLE)
{}
- ContentVideo (std::shared_ptr<const ImageProxy> i, Frame f, Eyes e, Part p)
+ ContentVideo (std::shared_ptr<const ImageProxy> i, dcpomatic::ContentTime t, Eyes e, Part p)
: image (i)
- , frame (f)
+ , time (t)
, eyes (e)
, part (p)
{}
std::shared_ptr<const ImageProxy> image;
- Frame frame;
+ dcpomatic::ContentTime time;
Eyes eyes;
Part part;
};
+
#endif
diff --git a/src/lib/cpu_j2k_encoder_thread.cc b/src/lib/cpu_j2k_encoder_thread.cc
new file mode 100644
index 000000000..580facae9
--- /dev/null
+++ b/src/lib/cpu_j2k_encoder_thread.cc
@@ -0,0 +1,62 @@
+/*
+ Copyright (C) 2023 Carl Hetherington <cth@carlh.net>
+
+ This file is part of DCP-o-matic.
+
+ DCP-o-matic is free software; you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation; either version 2 of the License, or
+ (at your option) any later version.
+
+ DCP-o-matic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with DCP-o-matic. If not, see <http://www.gnu.org/licenses/>.
+
+*/
+
+
+#include "cpu_j2k_encoder_thread.h"
+#include "cross.h"
+#include "dcpomatic_log.h"
+#include "dcp_video.h"
+#include "j2k_encoder.h"
+#include "util.h"
+
+#include "i18n.h"
+
+
+using std::make_shared;
+using std::shared_ptr;
+
+
+CPUJ2KEncoderThread::CPUJ2KEncoderThread(J2KEncoder& encoder)
+ : J2KSyncEncoderThread(encoder)
+{
+
+}
+
+
+void
+CPUJ2KEncoderThread::log_thread_start() const
+{
+ start_of_thread("CPUJ2KEncoder");
+ LOG_TIMING("start-encoder-thread thread=%1 server=localhost", thread_id());
+}
+
+
+shared_ptr<dcp::ArrayData>
+CPUJ2KEncoderThread::encode(DCPVideo const& frame)
+{
+ try {
+ return make_shared<dcp::ArrayData>(frame.encode_locally());
+ } catch (std::exception& e) {
+ LOG_ERROR(N_("Local encode failed (%1)"), e.what());
+ }
+
+ return {};
+}
+
diff --git a/src/lib/cpu_j2k_encoder_thread.h b/src/lib/cpu_j2k_encoder_thread.h
new file mode 100644
index 000000000..fb138f484
--- /dev/null
+++ b/src/lib/cpu_j2k_encoder_thread.h
@@ -0,0 +1,16 @@
+#include "j2k_sync_encoder_thread.h"
+#include <dcp/data.h>
+
+
+class DCPVideo;
+
+
+class CPUJ2KEncoderThread : public J2KSyncEncoderThread
+{
+public:
+ CPUJ2KEncoderThread(J2KEncoder& encoder);
+
+ void log_thread_start() const override;
+ std::shared_ptr<dcp::ArrayData> encode(DCPVideo const& frame) override;
+};
+
diff --git a/src/lib/create_cli.cc b/src/lib/create_cli.cc
index 1c2f2c635..8fe5a2a14 100644
--- a/src/lib/create_cli.cc
+++ b/src/lib/create_cli.cc
@@ -26,6 +26,7 @@
#include "dcpomatic_log.h"
#include "film.h"
#include "ratio.h"
+#include "variant.h"
#include <dcp/raw_convert.h>
#include <iostream>
#include <string>
@@ -40,8 +41,8 @@ using boost::optional;
string CreateCLI::_help =
- "\nSyntax: %1 [OPTION] <CONTENT> [OPTION] [<CONTENT> ...]\n"
- " -v, --version show DCP-o-matic version\n"
+ string("\nSyntax: %1 [OPTION] <CONTENT> [OPTION] [<CONTENT> ...]\n") +
+ variant::insert_dcpomatic(" -v, --version show %1 version\n") +
" -h, --help show this help\n"
" -n, --name <name> film name\n"
" -t, --template <name> template name\n"
@@ -132,7 +133,7 @@ CreateCLI::CreateCLI (int argc, char* argv[])
optional<string> standard_string;
int dcp_frame_rate_int = 0;
string template_name_string;
- int j2k_bandwidth_int = 0;
+ int64_t video_bit_rate_int = 0;
auto next_frame_type = VideoFrameType::TWO_D;
optional<dcp::Channel> channel;
optional<float> gain;
@@ -205,7 +206,7 @@ CreateCLI::CreateCLI (int argc, char* argv[])
argument_option(i, argc, argv, "", "--standard", &claimed, &error, &standard_string, string_to_string);
argument_option(i, argc, argv, "", "--config", &claimed, &error, &config_dir, string_to_path);
argument_option(i, argc, argv, "-o", "--output", &claimed, &error, &output_dir, string_to_path);
- argument_option(i, argc, argv, "", "--j2k-bandwidth", &claimed, &error, &j2k_bandwidth_int);
+ argument_option(i, argc, argv, "", "--video-bit-rate", &claimed, &error, &video_bit_rate_int);
std::function<optional<dcp::Channel> (string)> convert_channel = [](string channel) -> optional<dcp::Channel>{
if (channel == "L") {
@@ -271,8 +272,8 @@ CreateCLI::CreateCLI (int argc, char* argv[])
dcp_frame_rate = dcp_frame_rate_int;
}
- if (j2k_bandwidth_int) {
- _j2k_bandwidth = j2k_bandwidth_int * 1000000;
+ if (video_bit_rate_int) {
+ _video_bit_rate = video_bit_rate_int * 1000000;
}
if (dcp_content_type_string) {
@@ -319,8 +320,8 @@ CreateCLI::CreateCLI (int argc, char* argv[])
_name = content[0].path.filename().string();
}
- if (_j2k_bandwidth && (*_j2k_bandwidth < 10000000 || *_j2k_bandwidth > Config::instance()->maximum_j2k_bandwidth())) {
- error = String::compose("%1: j2k-bandwidth must be between 10 and %2 Mbit/s", argv[0], (Config::instance()->maximum_j2k_bandwidth() / 1000000));
+ if (_video_bit_rate && (*_video_bit_rate < 10000000 || *_video_bit_rate > Config::instance()->maximum_video_bit_rate(VideoEncoding::JPEG2000))) {
+ error = String::compose("%1: video-bit-rate must be between 10 and %2 Mbit/s", argv[0], (Config::instance()->maximum_video_bit_rate(VideoEncoding::JPEG2000) / 1000000));
return;
}
}
@@ -371,8 +372,8 @@ CreateCLI::make_film() const
if (_fourk) {
film->set_resolution(Resolution::FOUR_K);
}
- if (_j2k_bandwidth) {
- film->set_j2k_bandwidth(*_j2k_bandwidth);
+ if (_video_bit_rate) {
+ film->set_video_bit_rate(VideoEncoding::JPEG2000, *_video_bit_rate);
}
int channels = 6;
diff --git a/src/lib/create_cli.h b/src/lib/create_cli.h
index 782aaf974..850cddea9 100644
--- a/src/lib/create_cli.h
+++ b/src/lib/create_cli.h
@@ -72,7 +72,7 @@ private:
bool _no_use_isdcf_name = false;
bool _twok = false;
bool _fourk = false;
- boost::optional<int> _j2k_bandwidth;
+ boost::optional<int64_t> _video_bit_rate;
static std::string _help;
};
diff --git a/src/lib/crop.cc b/src/lib/crop.cc
index d2d020a9f..a6f7d4bac 100644
--- a/src/lib/crop.cc
+++ b/src/lib/crop.cc
@@ -42,12 +42,12 @@ Crop::Crop(shared_ptr<cxml::Node> node)
void
-Crop::as_xml(xmlpp::Node* node) const
+Crop::as_xml(xmlpp::Element* element) const
{
- node->add_child("LeftCrop")->add_child_text(raw_convert<string>(left));
- node->add_child("RightCrop")->add_child_text(raw_convert<string>(right));
- node->add_child("TopCrop")->add_child_text(raw_convert<string>(top));
- node->add_child("BottomCrop")->add_child_text(raw_convert<string>(bottom));
+ cxml::add_text_child(element, "LeftCrop", raw_convert<string>(left));
+ cxml::add_text_child(element, "RightCrop", raw_convert<string>(right));
+ cxml::add_text_child(element, "TopCrop", raw_convert<string>(top));
+ cxml::add_text_child(element, "BottomCrop", raw_convert<string>(bottom));
}
diff --git a/src/lib/crop.h b/src/lib/crop.h
index 00734d5e9..24330731d 100644
--- a/src/lib/crop.h
+++ b/src/lib/crop.h
@@ -65,7 +65,7 @@ struct Crop
return s;
}
- void as_xml (xmlpp::Node *) const;
+ void as_xml(xmlpp::Element*) const;
};
diff --git a/src/lib/cross.h b/src/lib/cross.h
index 6904811b7..150199561 100644
--- a/src/lib/cross.h
+++ b/src/lib/cross.h
@@ -138,29 +138,19 @@ private:
void disk_write_finished ();
-struct OSXMediaPath
-{
- bool real; ///< true for a "real" disk, false for a synthesized APFS one
- std::vector<std::string> parts; ///< parts of the media path after the :
-};
-
-
struct OSXDisk
{
std::string device;
boost::optional<std::string> vendor;
boost::optional<std::string> model;
- OSXMediaPath media_path;
- bool whole;
std::vector<boost::filesystem::path> mount_points;
unsigned long size;
+ bool system;
+ bool writeable;
+ bool partition;
};
-boost::optional<OSXMediaPath> analyse_osx_media_path (std::string path);
-std::vector<Drive> osx_disks_to_drives (std::vector<OSXDisk> disks);
-
-
class ArgFixer
{
public:
diff --git a/src/lib/cross_common.cc b/src/lib/cross_common.cc
index b4d322096..1e145c8fc 100644
--- a/src/lib/cross_common.cc
+++ b/src/lib/cross_common.cc
@@ -40,9 +40,6 @@ using std::vector;
using boost::optional;
-auto constexpr MEDIA_PATH_REQUIRED_MATCHES = 3;
-
-
Drive::Drive (string xml)
{
cxml::Document doc;
@@ -62,16 +59,16 @@ Drive::as_xml () const
{
xmlpp::Document doc;
auto root = doc.create_root_node ("Drive");
- root->add_child("Device")->add_child_text(_device);
+ cxml::add_text_child(root, "Device", _device);
for (auto i: _mount_points) {
- root->add_child("MountPoint")->add_child_text(i.string());
+ cxml::add_text_child(root, "MountPoint", i.string());
}
- root->add_child("Size")->add_child_text(dcp::raw_convert<string>(_size));
+ cxml::add_text_child(root, "Size", dcp::raw_convert<string>(_size));
if (_vendor) {
- root->add_child("Vendor")->add_child_text(*_vendor);
+ cxml::add_text_child(root, "Vendor", *_vendor);
}
if (_model) {
- root->add_child("Model")->add_child_text(*_model);
+ cxml::add_text_child(root, "Model", *_model);
}
return doc.write_to_string("UTF-8");
@@ -122,97 +119,3 @@ Drive::log_summary () const
);
}
-
-
-/* This is in _common so we can use it in unit tests */
-optional<OSXMediaPath>
-analyse_osx_media_path (string path)
-{
- if (path.find("/IOHDIXController") != string::npos) {
- /* This is a disk image, so we completely ignore it */
- LOG_DISK_NC("Ignoring this as it seems to be a disk image");
- return {};
- }
-
- OSXMediaPath mp;
- vector<string> parts;
- split(parts, path, boost::is_any_of("/"));
- std::copy(parts.begin() + 1, parts.end(), back_inserter(mp.parts));
-
- if (!parts.empty() && parts[0] == "IODeviceTree:") {
- mp.real = true;
- if (mp.parts.size() < MEDIA_PATH_REQUIRED_MATCHES) {
- /* Later we expect at least MEDIA_PATH_REQUIRED_MATCHES parts in a IODeviceTree */
- LOG_DISK_NC("Ignoring this as it has a strange media path");
- return {};
- }
- } else if (!parts.empty() && parts[0] == "IOService:") {
- mp.real = false;
- } else {
- return {};
- }
-
- return mp;
-}
-
-
-/* Take some OSXDisk objects, representing disks that `DARegisterDiskAppearedCallback` told us about,
- * and find those drives that we could write a DCP to. The drives returned are "real" (not synthesized)
- * and are whole disks (not partitions). They may be mounted, or contain mounted partitions, in which
- * their mounted() method will return true.
- */
-vector<Drive>
-osx_disks_to_drives (vector<OSXDisk> disks)
-{
- using namespace boost::algorithm;
-
- /* Mark disks containing mounted partitions as themselves mounted */
- for (auto& i: disks) {
- if (!i.whole) {
- continue;
- }
- for (auto& j: disks) {
- if (&i != &j && !j.mount_points.empty() && starts_with(j.device, i.device)) {
- LOG_DISK("Marking %1 as mounted because %2 is", i.device, j.device);
- std::copy(j.mount_points.begin(), j.mount_points.end(), back_inserter(i.mount_points));
- }
- }
- }
-
- /* Mark containers of mounted synths as themselves mounted */
- for (auto& i: disks) {
- if (i.media_path.real) {
- for (auto& j: disks) {
- if (!j.media_path.real && !j.mount_points.empty()) {
- /* i is real, j is a mounted synth; if we see the first MEDIA_PATH_REQUIRED_MATCHES parts
- * of i anywhere in j we assume they are related and so i shares j's mount points.
- */
- bool one_missing = false;
- string all_parts;
- DCPOMATIC_ASSERT (i.media_path.parts.size() >= MEDIA_PATH_REQUIRED_MATCHES);
- for (auto k = 0; k < MEDIA_PATH_REQUIRED_MATCHES; ++k) {
- if (find(j.media_path.parts.begin(), j.media_path.parts.end(), i.media_path.parts[k]) == j.media_path.parts.end()) {
- one_missing = true;
- }
- all_parts += i.media_path.parts[k] + " ";
- }
-
- if (!one_missing) {
- LOG_DISK("Marking %1 as mounted because %2 is (found %3)", i.device, j.device, all_parts);
- std::copy(j.mount_points.begin(), j.mount_points.end(), back_inserter(i.mount_points));
- }
- }
- }
- }
- }
-
- vector<Drive> drives;
- for (auto const& i: disks) {
- if (i.whole && i.media_path.real) {
- drives.push_back(Drive(i.device, i.mount_points, i.size, i.vendor, i.model));
- LOG_DISK_NC(drives.back().log_summary());
- }
- }
-
- return drives;
-}
diff --git a/src/lib/cross_osx.cc b/src/lib/cross_osx.cc
index 913b19103..6df2f959e 100644
--- a/src/lib/cross_osx.cc
+++ b/src/lib/cross_osx.cc
@@ -19,12 +19,13 @@
*/
-#include "cross.h"
#include "compose.hpp"
-#include "log.h"
-#include "dcpomatic_log.h"
#include "config.h"
+#include "cross.h"
+#include "dcpomatic_log.h"
#include "exceptions.h"
+#include "log.h"
+#include "variant.h"
#include <dcp/filesystem.h>
#include <dcp/raw_convert.h>
#include <glib.h>
@@ -168,6 +169,8 @@ Waker::~Waker ()
void
start_tool (string executable, string app)
{
+ boost::algorithm::replace_all(app, " ", "\\ ");
+
auto exe_path = directory_containing_executable();
exe_path = exe_path.parent_path(); // Contents
exe_path = exe_path.parent_path(); // DCP-o-matic 2.app
@@ -191,14 +194,14 @@ start_tool (string executable, string app)
void
start_batch_converter ()
{
- start_tool ("dcpomatic2_batch", "DCP-o-matic\\ 2\\ Batch\\ Converter.app");
+ start_tool("dcpomatic2_batch", variant::dcpomatic_batch_converter_app());
}
void
start_player ()
{
- start_tool ("dcpomatic2_player", "DCP-o-matic\\ 2\\ Player.app");
+ start_tool("dcpomatic2_player", variant::dcpomatic_player_app());
}
@@ -240,44 +243,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 +259,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 +292,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 +346,12 @@ Drive::get ()
DAUnregisterCallback(session, (void *) disk_appeared, &disks);
CFRelease(session);
- auto drives = osx_disks_to_drives(disks);
+ vector<Drive> drives;
+ for (auto const& disk: disks) {
+ if (!disk.system && !disk.partition && disk.writeable) {
+ drives.push_back({disk.device, disk.mount_points, disk.size, disk.vendor, disk.model});
+ }
+ }
LOG_DISK("Drive::get() found %1 drives:", drives.size());
for (auto const& drive: drives) {
diff --git a/src/lib/dcp_content.cc b/src/lib/dcp_content.cc
index 6e573c639..b4e979481 100644
--- a/src/lib/dcp_content.cc
+++ b/src/lib/dcp_content.cc
@@ -139,6 +139,11 @@ DCPContent::DCPContent (cxml::ConstNodePtr node, int version)
DCPOMATIC_ASSERT (false);
}
}
+
+ if (auto encoding = node->optional_string_child("VideoEncoding")) {
+ _video_encoding = video_encoding_from_string(*encoding);
+ }
+
_three_d = node->optional_bool_child("ThreeD").get_value_or (false);
auto ck = node->optional_string_child("ContentKind");
@@ -163,6 +168,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
@@ -299,6 +311,7 @@ DCPContent::examine (shared_ptr<const Film> film, shared_ptr<Job> job)
_needs_assets = examiner->needs_assets ();
_kdm_valid = examiner->kdm_valid ();
_standard = examiner->standard ();
+ _video_encoding = examiner->video_encoding();
_three_d = examiner->three_d ();
_content_kind = examiner->content_kind ();
_cpl = examiner->cpl ();
@@ -308,6 +321,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 +361,99 @@ 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, "VideoEncoding", video_encoding_to_string(_video_encoding));
+ 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 +637,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 +683,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 +717,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 +742,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 c->has_mapped_audio();
- },
- _("it overlaps other audio content; remove the other content."),
- why_not
- );
+ auto part = [](shared_ptr<const Content> c) {
+ return c->has_mapped_audio();
+ };
+
+ 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 +777,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
@@ -853,8 +851,14 @@ DCPContent::check_font_ids()
return;
}
+ /* This might be called on a TextContent that already has some fonts
+ * (e.g. if run from a build with a LastWrittenBy containing only a git
+ * hash, or from a version between 2.16.15 and 2.17.17) so we'll get an
+ * error if we don't clear them out first.
+ */
+ text[0]->clear_fonts();
DCPExaminer examiner(shared_from_this(), true);
- examiner.add_fonts(text.front());
+ examiner.add_fonts(text[0]);
}
diff --git a/src/lib/dcp_content.h b/src/lib/dcp_content.h
index 3753740a2..be2c72002 100644
--- a/src/lib/dcp_content.h
+++ b/src/lib/dcp_content.h
@@ -32,6 +32,7 @@
#include "enum_indexed_vector.h"
#include "font.h"
#include "resolution.h"
+#include "video_encoding.h"
#include <libcxml/cxml.h>
#include <dcp/content_kind.h>
#include <dcp/encrypted_kdm.h>
@@ -75,7 +76,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 +101,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 {
@@ -158,6 +161,11 @@ public:
return _standard.get ();
}
+ VideoEncoding video_encoding() const {
+ boost::mutex::scoped_lock lm (_mutex);
+ return _video_encoding;
+ }
+
std::map<dcp::Marker, dcpomatic::ContentTime> markers () const {
return _markers;
}
@@ -186,12 +194,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 */
@@ -216,6 +219,7 @@ private:
EnumIndexedVector<bool, TextType> _reference_text;
boost::optional<dcp::Standard> _standard;
+ VideoEncoding _video_encoding;
boost::optional<dcp::ContentKind> _content_kind;
bool _three_d;
/** ID of the CPL to use; older metadata might not specify this: in that case
@@ -227,6 +231,7 @@ private:
std::map<dcp::Marker, dcpomatic::ContentTime> _markers;
std::vector<dcp::Rating> _ratings;
std::vector<std::string> _content_versions;
+ EnumIndexedVector<bool, TextType> _has_non_zero_entry_point;
boost::optional<int> _active_audio_channels;
};
diff --git a/src/lib/dcp_content_type.cc b/src/lib/dcp_content_type.cc
index bc9bf0d67..d689f138c 100644
--- a/src/lib/dcp_content_type.cc
+++ b/src/lib/dcp_content_type.cc
@@ -28,8 +28,9 @@
#include "i18n.h"
+using std::string;
+using std::vector;
using boost::optional;
-using namespace std;
vector<DCPContentType> DCPContentType::_dcp_content_types;
diff --git a/src/lib/dcp_decoder.cc b/src/lib/dcp_decoder.cc
index 727dcbf26..abbaaf15d 100644
--- a/src/lib/dcp_decoder.cc
+++ b/src/lib/dcp_decoder.cc
@@ -26,19 +26,23 @@
#include "constants.h"
#include "dcp_content.h"
#include "dcp_decoder.h"
+#include "dcpomatic_log.h"
#include "digester.h"
#include "ffmpeg_image_proxy.h"
#include "frame_interval_checker.h"
#include "image.h"
#include "j2k_image_proxy.h"
+#include "raw_image_proxy.h"
#include "text_decoder.h"
+#include "util.h"
#include "video_decoder.h"
#include <dcp/cpl.h>
#include <dcp/dcp.h>
#include <dcp/decrypted_kdm.h>
-#include <dcp/mono_picture_asset.h>
-#include <dcp/mono_picture_asset_reader.h>
-#include <dcp/mono_picture_frame.h>
+#include <dcp/mono_j2k_picture_asset.h>
+#include <dcp/mono_j2k_picture_asset_reader.h>
+#include <dcp/mono_j2k_picture_frame.h>
+#include <dcp/mono_mpeg2_picture_asset.h>
#include <dcp/reel.h>
#include <dcp/reel_atmos_asset.h>
#include <dcp/reel_closed_caption_asset.h>
@@ -48,9 +52,9 @@
#include <dcp/search.h>
#include <dcp/sound_asset_reader.h>
#include <dcp/sound_frame.h>
-#include <dcp/stereo_picture_asset.h>
-#include <dcp/stereo_picture_asset_reader.h>
-#include <dcp/stereo_picture_frame.h>
+#include <dcp/stereo_j2k_picture_asset.h>
+#include <dcp/stereo_j2k_picture_asset_reader.h>
+#include <dcp/stereo_j2k_picture_frame.h>
#include <dcp/subtitle_image.h>
#include <iostream>
@@ -169,43 +173,60 @@ DCPDecoder::pass ()
*/
pass_texts (_next, picture_asset->size());
- if ((_mono_reader || _stereo_reader) && (_decode_referenced || !_dcp_content->reference_video())) {
+ if ((_j2k_mono_reader || _j2k_stereo_reader || _mpeg2_mono_reader) && (_decode_referenced || !_dcp_content->reference_video())) {
auto const entry_point = (*_reel)->main_picture()->entry_point().get_value_or(0);
- if (_mono_reader) {
+ if (_j2k_mono_reader) {
video->emit (
film(),
std::make_shared<J2KImageProxy>(
- _mono_reader->get_frame (entry_point + frame),
+ _j2k_mono_reader->get_frame(entry_point + frame),
picture_asset->size(),
AV_PIX_FMT_XYZ12LE,
_forced_reduction
),
- _offset + frame
+ ContentTime::from_frames(_offset + frame, vfr)
);
- } else {
+ } else if (_j2k_stereo_reader) {
video->emit (
film(),
std::make_shared<J2KImageProxy>(
- _stereo_reader->get_frame (entry_point + frame),
+ _j2k_stereo_reader->get_frame (entry_point + frame),
picture_asset->size(),
dcp::Eye::LEFT,
AV_PIX_FMT_XYZ12LE,
_forced_reduction
),
- _offset + frame
+ ContentTime::from_frames(_offset + frame, vfr)
);
video->emit (
film(),
std::make_shared<J2KImageProxy>(
- _stereo_reader->get_frame (entry_point + frame),
+ _j2k_stereo_reader->get_frame (entry_point + frame),
picture_asset->size(),
dcp::Eye::RIGHT,
AV_PIX_FMT_XYZ12LE,
_forced_reduction
),
- _offset + frame
+ ContentTime::from_frames(_offset + frame, vfr)
);
+ } else if (_mpeg2_mono_reader) {
+ /* XXX: got to flush this at some point */
+ try {
+ for (auto const& image: _mpeg2_decompressor->decompress_frame(_mpeg2_mono_reader->get_frame(entry_point + frame))) {
+ video->emit(
+ film(),
+ /* XXX: should this be PADDED? */
+ std::make_shared<RawImageProxy>(std::make_shared<Image>(image.frame(), Image::Alignment::COMPACT)),
+ /* XXX: this will be wrong */
+ ContentTime::from_frames(_offset + frame, vfr)
+ );
+ }
+ } catch (dcp::MPEG2DecompressionError& e) {
+ LOG_ERROR("Failed to decompress MPEG video frame %1 (%2)", entry_point + frame, e.what());
+ } catch (dcp::ReadError& e) {
+ LOG_ERROR("Failed to read MPEG2 video frame %1 (%2)", entry_point + frame, e.what());
+ }
}
}
@@ -214,15 +235,32 @@ DCPDecoder::pass ()
auto sf = _sound_reader->get_frame (entry_point + frame);
auto from = sf->data ();
- int const channels = _dcp_content->audio->stream()->channels ();
- int const frames = sf->size() / (3 * channels);
+ int const channels = _dcp_content->audio->stream()->channels();
+ int const frames = sf->size() / (sf->bits() * channels / 8);
auto data = make_shared<AudioBuffers>(channels, frames);
auto data_data = data->data();
- for (int i = 0; i < frames; ++i) {
- for (int j = 0; j < channels; ++j) {
- data_data[j][i] = static_cast<int> ((from[0] << 8) | (from[1] << 16) | (from[2] << 24)) / static_cast<float> (INT_MAX - 256);
- from += 3;
+
+ switch (sf->bits()) {
+ case 24:
+ {
+ for (int i = 0; i < frames; ++i) {
+ for (int j = 0; j < channels; ++j) {
+ data_data[j][i] = static_cast<int>((from[0] << 8) | (from[1] << 16) | (from[2] << 24)) / static_cast<float> (INT_MAX - 256);
+ from += 3;
+ }
+ }
+ break;
+ }
+ case 16:
+ {
+ for (int i = 0; i < frames; ++i) {
+ for (int j = 0; j < channels; ++j) {
+ data_data[j][i] = static_cast<int>(from[0] << 16 | (from[1] << 24)) / static_cast<float> (INT_MAX - 256);
+ from += 2;
+ }
}
+ break;
+ }
}
audio->emit (film(), _dcp_content->audio->stream(), data, ContentTime::from_frames (_offset, vfr) + _next);
@@ -367,38 +405,40 @@ DCPDecoder::next_reel ()
void
DCPDecoder::get_readers ()
{
+ _j2k_mono_reader.reset();
+ _j2k_stereo_reader.reset();
+ _mpeg2_mono_reader.reset();
+ _sound_reader.reset();
+ _atmos_reader.reset();
+ _mpeg2_decompressor.reset();
+ _atmos_metadata = boost::none;
+
if (_reel == _reels.end() || !_dcp_content->can_be_played ()) {
- _mono_reader.reset ();
- _stereo_reader.reset ();
- _sound_reader.reset ();
- _atmos_reader.reset ();
return;
}
if (video && !video->ignore() && (*_reel)->main_picture()) {
auto asset = (*_reel)->main_picture()->asset ();
- auto mono = dynamic_pointer_cast<dcp::MonoPictureAsset> (asset);
- auto stereo = dynamic_pointer_cast<dcp::StereoPictureAsset> (asset);
- DCPOMATIC_ASSERT (mono || stereo);
- if (mono) {
- _mono_reader = mono->start_read ();
- _mono_reader->set_check_hmac (false);
- _stereo_reader.reset ();
+ auto j2k_mono = dynamic_pointer_cast<dcp::MonoJ2KPictureAsset>(asset);
+ auto j2k_stereo = dynamic_pointer_cast<dcp::StereoJ2KPictureAsset>(asset);
+ auto mpeg2_mono = dynamic_pointer_cast<dcp::MonoMPEG2PictureAsset>(asset);
+ DCPOMATIC_ASSERT(j2k_mono || j2k_stereo || mpeg2_mono)
+ if (j2k_mono) {
+ _j2k_mono_reader = j2k_mono->start_read();
+ _j2k_mono_reader->set_check_hmac(false);
+ } else if (j2k_stereo) {
+ _j2k_stereo_reader = j2k_stereo->start_read();
+ _j2k_stereo_reader->set_check_hmac(false);
} else {
- _stereo_reader = stereo->start_read ();
- _stereo_reader->set_check_hmac (false);
- _mono_reader.reset ();
+ _mpeg2_mono_reader = mpeg2_mono->start_read();
+ _mpeg2_mono_reader->set_check_hmac(false);
+ _mpeg2_decompressor = std::make_shared<dcp::MPEG2Decompressor>();
}
- } else {
- _mono_reader.reset ();
- _stereo_reader.reset ();
}
if (audio && !audio->ignore() && (*_reel)->main_sound()) {
_sound_reader = (*_reel)->main_sound()->asset()->start_read ();
_sound_reader->set_check_hmac (false);
- } else {
- _sound_reader.reset ();
}
if ((*_reel)->atmos()) {
@@ -406,9 +446,6 @@ DCPDecoder::get_readers ()
_atmos_reader = asset->start_read();
_atmos_reader->set_check_hmac (false);
_atmos_metadata = AtmosMetadata (asset);
- } else {
- _atmos_reader.reset ();
- _atmos_metadata = boost::none;
}
}
diff --git a/src/lib/dcp_decoder.h b/src/lib/dcp_decoder.h
index 2c0cd8f41..ee0f30694 100644
--- a/src/lib/dcp_decoder.h
+++ b/src/lib/dcp_decoder.h
@@ -27,8 +27,10 @@
#include "atmos_metadata.h"
#include "decoder.h"
#include "font_id_allocator.h"
-#include <dcp/mono_picture_asset_reader.h>
-#include <dcp/stereo_picture_asset_reader.h>
+#include <dcp/mono_j2k_picture_asset_reader.h>
+#include <dcp/stereo_j2k_picture_asset_reader.h>
+#include <dcp/mono_mpeg2_picture_asset_reader.h>
+#include <dcp/mpeg2_transcode.h>
#include <dcp/sound_asset_reader.h>
#include <dcp/subtitle_asset.h>
@@ -94,15 +96,19 @@ private:
std::vector<std::shared_ptr<dcp::Reel>>::iterator _reel;
/** Offset of _reel from the start of the content in frames */
int64_t _offset = 0;
- /** Reader for current mono picture asset, if applicable */
- std::shared_ptr<dcp::MonoPictureAssetReader> _mono_reader;
- /** Reader for current stereo picture asset, if applicable */
- std::shared_ptr<dcp::StereoPictureAssetReader> _stereo_reader;
+ /** Reader for current J2K mono picture asset, if applicable */
+ std::shared_ptr<dcp::MonoJ2KPictureAssetReader> _j2k_mono_reader;
+ /** Reader for current J2K stereo picture asset, if applicable */
+ std::shared_ptr<dcp::StereoJ2KPictureAssetReader> _j2k_stereo_reader;
+ /** Reader for current MPEG2 mono picture asset, if applicable */
+ std::shared_ptr<dcp::MonoMPEG2PictureAssetReader> _mpeg2_mono_reader;
/** Reader for current sound asset, if applicable */
std::shared_ptr<dcp::SoundAssetReader> _sound_reader;
std::shared_ptr<dcp::AtmosAssetReader> _atmos_reader;
boost::optional<AtmosMetadata> _atmos_metadata;
+ std::shared_ptr<dcp::MPEG2Decompressor> _mpeg2_decompressor;
+
bool _decode_referenced = false;
boost::optional<int> _forced_reduction;
diff --git a/src/lib/dcp_digest_file.cc b/src/lib/dcp_digest_file.cc
index eb243b60d..4278caa98 100644
--- a/src/lib/dcp_digest_file.cc
+++ b/src/lib/dcp_digest_file.cc
@@ -42,14 +42,14 @@ template <class R, class A>
void add_asset(string film_key, shared_ptr<R> reel_asset, shared_ptr<A> asset, xmlpp::Element* reel, string name)
{
if (asset) {
- auto out = reel->add_child(name);
- out->add_child("Id")->add_child_text("urn:uuid:" + asset->id());
+ auto out = cxml::add_child(reel, name);
+ cxml::add_text_child(out, "Id", "urn:uuid:" + asset->id());
if (reel_asset->annotation_text()) {
- out->add_child("AnnotationText")->add_child_text(reel_asset->annotation_text().get());
+ cxml::add_text_child(out, "AnnotationText", reel_asset->annotation_text().get());
}
if (asset->key_id()) {
- out->add_child("KeyId")->add_child_text("urn:uuid:" + asset->key_id().get());
- out->add_child("Key")->add_child_text(asset->key() ? asset->key()->hex() : film_key);
+ cxml::add_text_child(out, "KeyId", "urn:uuid:" + asset->key_id().get());
+ cxml::add_text_child(out, "Key", asset->key() ? asset->key()->hex() : film_key);
}
}
};
@@ -64,16 +64,16 @@ write_dcp_digest_file (
{
xmlpp::Document doc;
auto root = doc.create_root_node("FHG_DCP_DIGEST", "http://www.fhg.de/2009/04/02/dcpdig");
- root->add_child("InteropMode")->add_child_text(cpl->standard() == dcp::Standard::INTEROP ? "true" : "false");
- auto composition = root->add_child("CompositionList")->add_child("Composition");
- composition->add_child("Id")->add_child_text("urn:uuid:" + cpl->id());
- composition->add_child("AnnotationText")->add_child_text(cpl->annotation_text().get_value_or(""));
- composition->add_child("ContentTitleText")->add_child_text(cpl->content_title_text());
- auto reel_list = composition->add_child("ReelList");
+ cxml::add_text_child(root, "InteropMode", cpl->standard() == dcp::Standard::INTEROP ? "true" : "false");
+ auto composition = cxml::add_child(cxml::add_child(root, "CompositionList"), "Composition");
+ cxml::add_text_child(composition, "Id", "urn:uuid:" + cpl->id());
+ cxml::add_text_child(composition, "AnnotationText", cpl->annotation_text().get_value_or(""));
+ cxml::add_text_child(composition, "ContentTitleText", cpl->content_title_text());
+ auto reel_list = cxml::add_child(composition, "ReelList");
for (auto in_reel: cpl->reels()) {
- auto out_reel = reel_list->add_child("Reel");
- out_reel->add_child("Id")->add_child_text("urn:uuid:" + in_reel->id());
- out_reel->add_child("AnnotationText");
+ auto out_reel = cxml::add_child(reel_list, "Reel");
+ cxml::add_text_child(out_reel, "Id", "urn:uuid:" + in_reel->id());
+ cxml::add_child(out_reel, "AnnotationText");
if (in_reel->main_picture()) {
add_asset(film_key, in_reel->main_picture(), in_reel->main_picture()->asset(), out_reel, "MainPicture");
}
diff --git a/src/lib/dcp_examiner.cc b/src/lib/dcp_examiner.cc
index ae5f9e9c0..a7af9feca 100644
--- a/src/lib/dcp_examiner.cc
+++ b/src/lib/dcp_examiner.cc
@@ -32,9 +32,11 @@
#include <dcp/cpl.h>
#include <dcp/dcp.h>
#include <dcp/decrypted_kdm.h>
-#include <dcp/mono_picture_asset.h>
-#include <dcp/mono_picture_asset_reader.h>
-#include <dcp/mono_picture_frame.h>
+#include <dcp/mono_j2k_picture_asset.h>
+#include <dcp/mono_j2k_picture_asset_reader.h>
+#include <dcp/mono_j2k_picture_frame.h>
+#include <dcp/mono_mpeg2_picture_asset.h>
+#include <dcp/mpeg2_transcode.h>
#include <dcp/reel.h>
#include <dcp/reel_atmos_asset.h>
#include <dcp/reel_closed_caption_asset.h>
@@ -46,9 +48,9 @@
#include <dcp/sound_asset.h>
#include <dcp/sound_asset.h>
#include <dcp/sound_asset_reader.h>
-#include <dcp/stereo_picture_asset.h>
-#include <dcp/stereo_picture_asset_reader.h>
-#include <dcp/stereo_picture_frame.h>
+#include <dcp/stereo_j2k_picture_asset.h>
+#include <dcp/stereo_j2k_picture_asset_reader.h>
+#include <dcp/stereo_j2k_picture_frame.h>
#include <dcp/subtitle_asset.h>
#include <iostream>
@@ -162,6 +164,10 @@ DCPExaminer::DCPExaminer (shared_ptr<const DCPContent> content, bool tolerant)
} else if (_video_size.get() != asset->size ()) {
throw DCPError (_("Mismatched video sizes in DCP"));
}
+
+ if (dynamic_pointer_cast<dcp::MPEG2PictureAsset>(asset)) {
+ _video_range = VideoRange::VIDEO;
+ }
}
}
@@ -196,17 +202,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)});
}
@@ -226,6 +235,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;
@@ -288,17 +300,26 @@ DCPExaminer::DCPExaminer (shared_ptr<const DCPContent> content, bool tolerant)
LOG_GENERAL_NC ("Picture has no key");
break;
}
- auto mono = dynamic_pointer_cast<dcp::MonoPictureAsset>(pic);
- auto stereo = dynamic_pointer_cast<dcp::StereoPictureAsset>(pic);
+ auto j2k_mono = dynamic_pointer_cast<dcp::MonoJ2KPictureAsset>(pic);
+ auto j2k_stereo = dynamic_pointer_cast<dcp::StereoJ2KPictureAsset>(pic);
+ auto mpeg2_mono = dynamic_pointer_cast<dcp::MonoMPEG2PictureAsset>(pic);
- if (mono) {
- auto reader = mono->start_read();
+ if (j2k_mono) {
+ auto reader = j2k_mono->start_read();
reader->set_check_hmac (false);
reader->get_frame(0)->xyz_image();
- } else {
- auto reader = stereo->start_read();
+ _video_encoding = VideoEncoding::JPEG2000;
+ } else if (j2k_stereo) {
+ auto reader = j2k_stereo->start_read();
reader->set_check_hmac (false);
reader->get_frame(0)->xyz_image(dcp::Eye::LEFT);
+ _video_encoding = VideoEncoding::JPEG2000;
+ } else if (mpeg2_mono) {
+ auto reader = mpeg2_mono->start_read();
+ reader->set_check_hmac(false);
+ dcp::MPEG2Decompressor decompressor;
+ decompressor.decompress_frame(reader->get_frame(0));
+ _video_encoding = VideoEncoding::MPEG2;
}
}
@@ -349,7 +370,7 @@ DCPExaminer::DCPExaminer (shared_ptr<const DCPContent> content, bool tolerant)
_standard = selected_cpl->standard();
if (!selected_cpl->reels().empty()) {
auto first_reel = selected_cpl->reels()[0];
- _three_d = first_reel->main_picture() && first_reel->main_picture()->asset_ref().resolved() && dynamic_pointer_cast<dcp::StereoPictureAsset>(first_reel->main_picture()->asset());
+ _three_d = first_reel->main_picture() && first_reel->main_picture()->asset_ref().resolved() && dynamic_pointer_cast<dcp::StereoJ2KPictureAsset>(first_reel->main_picture()->asset());
}
_ratings = selected_cpl->ratings();
for (auto version: selected_cpl->content_versions()) {
diff --git a/src/lib/dcp_examiner.h b/src/lib/dcp_examiner.h
index 04fa31ea4..28b59ee2f 100644
--- a/src/lib/dcp_examiner.h
+++ b/src/lib/dcp_examiner.h
@@ -61,7 +61,7 @@ public:
}
VideoRange range () const override {
- return VideoRange::FULL;
+ return _video_range;
}
PixelQuanta pixel_quanta () const override {
@@ -130,6 +130,10 @@ public:
return _standard;
}
+ VideoEncoding video_encoding() const {
+ return _video_encoding;
+ }
+
bool three_d () const {
return _three_d;
}
@@ -171,6 +175,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:
@@ -196,6 +204,7 @@ private:
bool _needs_assets = false;
bool _kdm_valid = false;
boost::optional<dcp::Standard> _standard;
+ VideoEncoding _video_encoding = VideoEncoding::JPEG2000;
bool _three_d = false;
boost::optional<dcp::ContentKind> _content_kind;
std::string _cpl;
@@ -206,6 +215,8 @@ private:
bool _has_atmos = false;
Frame _atmos_length = 0;
dcp::Fraction _atmos_edit_rate;
+ EnumIndexedVector<bool, TextType> _has_non_zero_entry_point;
+ VideoRange _video_range = VideoRange::FULL;
struct Font
{
diff --git a/src/lib/dcp_encoder.cc b/src/lib/dcp_film_encoder.cc
index a4bc133f8..83da57756 100644
--- a/src/lib/dcp_encoder.cc
+++ b/src/lib/dcp_film_encoder.cc
@@ -18,38 +18,42 @@
*/
-/** @file src/dcp_encoder.cc
+
+/** @file src/dcp_film_encoder.cc
* @brief A class which takes a Film and some Options, then uses those to encode the film into a DCP.
*
* A decoder is selected according to the content type, and the encoder can be specified
* as a parameter to the constructor.
*/
-#include "dcp_encoder.h"
-#include "j2k_encoder.h"
-#include "film.h"
-#include "video_decoder.h"
+
#include "audio_decoder.h"
-#include "player.h"
-#include "job.h"
-#include "writer.h"
#include "compose.hpp"
+#include "dcp_film_encoder.h"
+#include "film.h"
+#include "j2k_encoder.h"
+#include "job.h"
+#include "mpeg2_encoder.h"
+#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;
@@ -61,17 +65,30 @@ using namespace dcpomatic;
* @param film Film that we are encoding.
* @param job Job that this encoder is being used in.
*/
-DCPEncoder::DCPEncoder (shared_ptr<const Film> film, weak_ptr<Job> job)
- : Encoder (film, job)
- , _writer(film, job)
- , _j2k_encoder(film, _writer)
+DCPFilmEncoder::DCPFilmEncoder(shared_ptr<const Film> film, weak_ptr<Job> job)
+ : FilmEncoder(film, job)
+ , _writer(film, job, film->dir(film->dcp_name()))
, _finishing (false)
, _non_burnt_subtitles (false)
{
- _player_video_connection = _player.Video.connect(bind(&DCPEncoder::video, this, _1, _2));
- _player_audio_connection = _player.Audio.connect(bind(&DCPEncoder::audio, this, _1, _2));
- _player_text_connection = _player.Text.connect(bind(&DCPEncoder::text, this, _1, _2, _3, _4));
- _player_atmos_connection = _player.Atmos.connect(bind(&DCPEncoder::atmos, this, _1, _2, _3));
+ switch (_film->video_encoding()) {
+ case VideoEncoding::JPEG2000:
+ _encoder.reset(new J2KEncoder(film, _writer));
+ break;
+ case VideoEncoding::MPEG2:
+ _encoder.reset(new MPEG2Encoder(film, _writer));
+ break;
+ case VideoEncoding::COUNT:
+ DCPOMATIC_ASSERT(false);
+ }
+
+ /* Now that we have a Writer we can clear out the assets directory */
+ clean_up_asset_directory(film->assets_path());
+
+ _player_video_connection = _player.Video.connect(bind(&DCPFilmEncoder::video, this, _1, _2));
+ _player_audio_connection = _player.Audio.connect(bind(&DCPFilmEncoder::audio, this, _1, _2));
+ _player_text_connection = _player.Text.connect(bind(&DCPFilmEncoder::text, this, _1, _2, _3, _4));
+ _player_atmos_connection = _player.Atmos.connect(bind(&DCPFilmEncoder::atmos, this, _1, _2, _3));
for (auto c: film->content ()) {
for (auto i: c->text) {
@@ -82,7 +99,7 @@ DCPEncoder::DCPEncoder (shared_ptr<const Film> film, weak_ptr<Job> job)
}
}
-DCPEncoder::~DCPEncoder ()
+DCPFilmEncoder::~DCPFilmEncoder()
{
/* We must stop receiving more video data before we die */
_player_video_connection.release ();
@@ -92,10 +109,10 @@ DCPEncoder::~DCPEncoder ()
}
void
-DCPEncoder::go ()
+DCPFilmEncoder::go()
{
_writer.start();
- _j2k_encoder.begin();
+ _encoder->begin();
{
auto job = _job.lock ();
@@ -121,24 +138,38 @@ DCPEncoder::go ()
}
_finishing = true;
- _j2k_encoder.end();
- _writer.finish(_film->dir(_film->dcp_name()));
+ _encoder->end();
+ _writer.finish();
+}
+
+
+void
+DCPFilmEncoder::pause()
+{
+ _encoder->pause();
+}
+
+
+void
+DCPFilmEncoder::resume()
+{
+ _encoder->resume();
}
void
-DCPEncoder::video (shared_ptr<PlayerVideo> data, DCPTime time)
+DCPFilmEncoder::video(shared_ptr<PlayerVideo> data, DCPTime time)
{
- _j2k_encoder.encode(data, time);
+ _encoder->encode(data, time);
}
void
-DCPEncoder::audio (shared_ptr<AudioBuffers> data, DCPTime time)
+DCPFilmEncoder::audio(shared_ptr<AudioBuffers> data, DCPTime time)
{
_writer.write(data, time);
}
void
-DCPEncoder::text (PlayerText data, TextType type, optional<DCPTextTrack> track, DCPTimePeriod period)
+DCPFilmEncoder::text(PlayerText data, TextType type, optional<DCPTextTrack> track, DCPTimePeriod period)
{
if (type == TextType::CLOSED_CAPTION || _non_burnt_subtitles) {
_writer.write(data, type, track, period);
@@ -147,20 +178,20 @@ DCPEncoder::text (PlayerText data, TextType type, optional<DCPTextTrack> track,
void
-DCPEncoder::atmos (shared_ptr<const dcp::AtmosFrame> data, DCPTime time, AtmosMetadata metadata)
+DCPFilmEncoder::atmos(shared_ptr<const dcp::AtmosFrame> data, DCPTime time, AtmosMetadata metadata)
{
_writer.write(data, time, metadata);
}
optional<float>
-DCPEncoder::current_rate () const
+DCPFilmEncoder::current_rate() const
{
- return _j2k_encoder.current_encoding_rate();
+ return _encoder->current_encoding_rate();
}
Frame
-DCPEncoder::frames_done () const
+DCPFilmEncoder::frames_done() const
{
return _player.frames_done();
}
diff --git a/src/lib/dcp_encoder.h b/src/lib/dcp_film_encoder.h
index ad77f6951..3c697a115 100644
--- a/src/lib/dcp_encoder.h
+++ b/src/lib/dcp_film_encoder.h
@@ -22,7 +22,7 @@
#include "atmos_metadata.h"
#include "dcp_text_track.h"
#include "dcpomatic_time.h"
-#include "encoder.h"
+#include "film_encoder.h"
#include "player_text.h"
#include "j2k_encoder.h"
#include "writer.h"
@@ -35,13 +35,15 @@ class Job;
class Player;
class PlayerVideo;
+struct frames_not_lost_when_threads_disappear;
-/** @class DCPEncoder */
-class DCPEncoder : public Encoder
+
+/** @class DCPFilmEncoder */
+class DCPFilmEncoder : public FilmEncoder
{
public:
- DCPEncoder (std::shared_ptr<const Film> film, std::weak_ptr<Job> job);
- ~DCPEncoder ();
+ DCPFilmEncoder(std::shared_ptr<const Film> film, std::weak_ptr<Job> job);
+ ~DCPFilmEncoder();
void go () override;
@@ -53,15 +55,20 @@ 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);
void atmos (std::shared_ptr<const dcp::AtmosFrame>, dcpomatic::DCPTime, AtmosMetadata metadata);
Writer _writer;
- J2KEncoder _j2k_encoder;
+ std::unique_ptr<VideoEncoder> _encoder;
bool _finishing;
bool _non_burnt_subtitles;
diff --git a/src/lib/dcp_subtitle_content.cc b/src/lib/dcp_subtitle_content.cc
index 8de5967ef..9ca3750d1 100644
--- a/src/lib/dcp_subtitle_content.cc
+++ b/src/lib/dcp_subtitle_content.cc
@@ -137,14 +137,14 @@ DCPSubtitleContent::technical_summary () const
}
void
-DCPSubtitleContent::as_xml (xmlpp::Node* node, bool with_paths) const
+DCPSubtitleContent::as_xml(xmlpp::Element* element, bool with_paths) const
{
- node->add_child("Type")->add_child_text ("DCPSubtitle");
- Content::as_xml (node, with_paths);
+ cxml::add_text_child(element, "Type", "DCPSubtitle");
+ Content::as_xml(element, with_paths);
if (only_text()) {
- only_text()->as_xml (node);
+ only_text()->as_xml(element);
}
- node->add_child("Length")->add_child_text (raw_convert<string> (_length.get ()));
+ cxml::add_text_child(element, "Length", raw_convert<string>(_length.get()));
}
diff --git a/src/lib/dcp_subtitle_content.h b/src/lib/dcp_subtitle_content.h
index 89a6f26a2..068b6dbac 100644
--- a/src/lib/dcp_subtitle_content.h
+++ b/src/lib/dcp_subtitle_content.h
@@ -30,7 +30,7 @@ public:
void examine (std::shared_ptr<const Film> film, std::shared_ptr<Job>) override;
std::string summary () const override;
std::string technical_summary () const override;
- void as_xml (xmlpp::Node *, bool with_paths) const override;
+ void as_xml(xmlpp::Element*, bool with_paths) const override;
dcpomatic::DCPTime full_length (std::shared_ptr<const Film> film) const override;
dcpomatic::DCPTime approximate_length () const override;
diff --git a/src/lib/dcp_text_track.cc b/src/lib/dcp_text_track.cc
index 0bd751275..292932396 100644
--- a/src/lib/dcp_text_track.cc
+++ b/src/lib/dcp_text_track.cc
@@ -56,9 +56,9 @@ DCPTextTrack::summary () const
void
DCPTextTrack::as_xml (xmlpp::Element* parent) const
{
- parent->add_child("Name")->add_child_text(name);
+ cxml::add_text_child(parent, "Name", name);
if (language) {
- parent->add_child("Language")->add_child_text(language->to_string());
+ cxml::add_text_child(parent, "Language", language->to_string());
}
}
diff --git a/src/lib/dcp_video.cc b/src/lib/dcp_video.cc
index 217b72183..ec88888dd 100644
--- a/src/lib/dcp_video.cc
+++ b/src/lib/dcp_video.cc
@@ -74,15 +74,15 @@ using namespace boost::placeholders;
/** Construct a DCP video frame.
* @param frame Input frame.
* @param index Index of the frame within the DCP.
- * @param bw J2K bandwidth to use (see Config::j2k_bandwidth ())
+ * @param bit_rate Video bit rate to use.
*/
DCPVideo::DCPVideo (
- shared_ptr<const PlayerVideo> frame, int index, int dcp_fps, int bw, Resolution r
+ shared_ptr<const PlayerVideo> frame, int index, int dcp_fps, int64_t bit_rate, Resolution r
)
: _frame (frame)
, _index (index)
, _frames_per_second (dcp_fps)
- , _j2k_bandwidth (bw)
+ , _video_bit_rate(bit_rate)
, _resolution (r)
{
@@ -93,7 +93,7 @@ DCPVideo::DCPVideo (shared_ptr<const PlayerVideo> frame, shared_ptr<const cxml::
{
_index = node->number_child<int> ("Index");
_frames_per_second = node->number_child<int> ("FramesPerSecond");
- _j2k_bandwidth = node->number_child<int> ("J2KBandwidth");
+ _video_bit_rate = node->number_child<int64_t>("VideoBitRate");
_resolution = Resolution (node->optional_number_child<int>("Resolution").get_value_or(static_cast<int>(Resolution::TWO_K)));
}
@@ -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.
*/
@@ -136,7 +160,7 @@ DCPVideo::encode_locally () const
while (true) {
enc = dcp::compress_j2k (
xyz,
- _j2k_bandwidth,
+ _video_bit_rate,
_frames_per_second,
_frame->eyes() == Eyes::LEFT || _frame->eyes() == Eyes::RIGHT,
_resolution == Resolution::FOUR_K,
@@ -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, "VideoBitRate", raw_convert<string>(_video_bit_rate));
+ cxml::add_text_child(el, "Resolution", raw_convert<string>(int(_resolution)));
_frame->add_metadata (el);
}
@@ -274,7 +298,7 @@ bool
DCPVideo::same (shared_ptr<const DCPVideo> other) const
{
if (_frames_per_second != other->_frames_per_second ||
- _j2k_bandwidth != other->_j2k_bandwidth ||
+ _video_bit_rate != other->_video_bit_rate ||
_resolution != other->_resolution) {
return false;
}
diff --git a/src/lib/dcp_video.h b/src/lib/dcp_video.h
index bf95ccfe6..92c155f0c 100644
--- a/src/lib/dcp_video.h
+++ b/src/lib/dcp_video.h
@@ -17,6 +17,8 @@
along with DCP-o-matic. If not, see <http://www.gnu.org/licenses/>.
*/
+#ifndef DCPOMATIC_DCP_VIDEO_H
+#define DCPOMATIC_DCP_VIDEO_H
#include "encode_server_description.h"
@@ -47,7 +49,7 @@ class PlayerVideo;
class DCPVideo
{
public:
- DCPVideo (std::shared_ptr<const PlayerVideo>, int index, int dcp_fps, int bandwidth, Resolution r);
+ DCPVideo(std::shared_ptr<const PlayerVideo>, int index, int dcp_fps, int64_t bit_rate, Resolution r);
DCPVideo (std::shared_ptr<const PlayerVideo>, cxml::ConstNodePtr);
DCPVideo (DCPVideo const&) = default;
@@ -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;
@@ -73,6 +78,8 @@ private:
std::shared_ptr<const PlayerVideo> _frame;
int _index; ///< frame index within the DCP's intrinsic duration
int _frames_per_second; ///< Frames per second that we will use for the DCP
- int _j2k_bandwidth; ///< J2K bandwidth to use
+ int64_t _video_bit_rate; ///< Video bit rate to use
Resolution _resolution; ///< Resolution (2K or 4K)
};
+
+#endif
diff --git a/src/lib/dcpomatic_time.cc b/src/lib/dcpomatic_time.cc
index ac797f8f4..60fc5342a 100644
--- a/src/lib/dcpomatic_time.cc
+++ b/src/lib/dcpomatic_time.cc
@@ -27,6 +27,25 @@ using std::string;
using namespace dcpomatic;
+bool
+dcpomatic::operator<=(HMSF const& a, HMSF const& b)
+{
+ if (a.h != b.h) {
+ return a.h <= b.h;
+ }
+
+ if (a.m != b.m) {
+ return a.m <= b.m;
+ }
+
+ if (a.s != b.s) {
+ return a.s <= b.s;
+ }
+
+ return a.f <= b.f;
+}
+
+
template <>
Time<ContentTimeDifferentiator, DCPTimeDifferentiator>::Time (DCPTime d, FrameRateChange f)
: _t (llrint(d.get() * f.speed_up))
diff --git a/src/lib/dcpomatic_time.h b/src/lib/dcpomatic_time.h
index 1b12ea901..63bb86549 100644
--- a/src/lib/dcpomatic_time.h
+++ b/src/lib/dcpomatic_time.h
@@ -64,6 +64,9 @@ public:
};
+bool operator<=(HMSF const& a, HMSF const& b);
+
+
/** A time in seconds, expressed as a number scaled up by Time::HZ. We want two different
* versions of this class, dcpomatic::ContentTime and dcpomatic::DCPTime, and we want it to be impossible to
* convert implicitly between the two. Hence there's this template hack. I'm not
@@ -152,6 +155,10 @@ public:
return *this;
}
+ Time<S, O> operator* (int o) const {
+ return Time<S, O> (_t * o);
+ }
+
Time<S, O> operator/ (int o) const {
return Time<S, O> (_t / o);
}
diff --git a/src/lib/dkdm_recipient.cc b/src/lib/dkdm_recipient.cc
index c73379bed..83ba96de6 100644
--- a/src/lib/dkdm_recipient.cc
+++ b/src/lib/dkdm_recipient.cc
@@ -19,12 +19,12 @@
*/
+#include "cinema_list.h"
#include "config.h"
#include "dkdm_recipient.h"
#include "film.h"
#include "kdm_with_metadata.h"
#include <dcp/raw_convert.h>
-#include <dcp/utc_offset.h>
using std::make_shared;
@@ -34,62 +34,33 @@ using std::vector;
using dcp::raw_convert;
-DKDMRecipient::DKDMRecipient (cxml::ConstNodePtr node)
- : KDMRecipient (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");
-}
-
-
-void
-DKDMRecipient::as_xml (xmlpp::Element* node) const
-{
- KDMRecipient::as_xml (node);
-
- for (auto i: emails) {
- node->add_child("Email")->add_child_text(i);
- }
-
- node->add_child("UTCOffsetHour")->add_child_text(raw_convert<string>(utc_offset_hour));
- node->add_child("UTCOffsetMinute")->add_child_text(raw_convert<string>(utc_offset_minute));
-}
-
-
KDMWithMetadataPtr
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
+ DKDMRecipient const& recipient,
+ dcp::LocalTime valid_from,
+ dcp::LocalTime valid_to
)
{
- if (!recipient->recipient) {
+ 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 kdm = decrypted_kdm.encrypt(signer, recipient->recipient.get(), {}, dcp::Formulation::MODIFIED_TRANSITIONAL_1, true, 0);
+ 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);
+ return make_shared<KDMWithMetadata>(name_values, CinemaID(0), recipient.emails, kdm);
}
diff --git a/src/lib/dkdm_recipient.h b/src/lib/dkdm_recipient.h
index 7a0fa0185..64da41cff 100644
--- a/src/lib/dkdm_recipient.h
+++ b/src/lib/dkdm_recipient.h
@@ -33,25 +33,15 @@ 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_)
{
}
- explicit DKDMRecipient (cxml::ConstNodePtr);
-
- void as_xml (xmlpp::Element *) const override;
-
std::vector<std::string> emails;
- int utc_offset_hour;
- int utc_offset_minute;
};
@@ -59,8 +49,8 @@ KDMWithMetadataPtr
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
+ DKDMRecipient const& recipient,
+ dcp::LocalTime valid_from,
+ dcp::LocalTime valid_to
);
diff --git a/src/lib/dkdm_recipient_list.cc b/src/lib/dkdm_recipient_list.cc
new file mode 100644
index 000000000..34179337e
--- /dev/null
+++ b/src/lib/dkdm_recipient_list.cc
@@ -0,0 +1,243 @@
+/*
+ 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 "config.h"
+#include "dkdm_recipient.h"
+#include "dkdm_recipient_list.h"
+#include "sqlite_statement.h"
+#include "sqlite_transaction.h"
+#include "util.h"
+#include <boost/algorithm/string.hpp>
+
+
+using std::make_pair;
+using std::pair;
+using std::string;
+using std::vector;
+using boost::optional;
+
+
+DKDMRecipientList::DKDMRecipientList()
+ : _dkdm_recipients("dkdm_recipients")
+{
+ setup(Config::instance()->dkdm_recipients_file());
+}
+
+
+DKDMRecipientList::DKDMRecipientList(boost::filesystem::path db_file)
+ : _dkdm_recipients("dkdm_recipients")
+{
+ setup(db_file);
+}
+
+
+
+DKDMRecipientList::~DKDMRecipientList()
+{
+ if (_db) {
+ sqlite3_close(_db);
+ }
+}
+
+
+void
+DKDMRecipientList::read_legacy_file(boost::filesystem::path xml_file)
+{
+ cxml::Document doc("DKDMRecipients");
+ doc.read_file(xml_file);
+
+ read_legacy_document(doc);
+}
+
+
+void
+DKDMRecipientList::read_legacy_string(string const& xml)
+{
+ cxml::Document doc("DKDMRecipients");
+ doc.read_file(xml);
+
+ read_legacy_document(doc);
+}
+
+
+void
+DKDMRecipientList::read_legacy_document(cxml::Document const& doc)
+{
+ for (auto recipient_node: doc.node_children("DKDMRecipient")) {
+ vector<string> emails;
+ for (auto email_node: recipient_node->node_children("Email")) {
+ emails.push_back(email_node->content());
+ }
+
+ optional<dcp::Certificate> certificate;
+ if (auto certificate_string = recipient_node->optional_string_child("Recipient")) {
+ certificate = dcp::Certificate(*certificate_string);
+ }
+
+ DKDMRecipient recipient(
+ recipient_node->string_child("Name"),
+ recipient_node->string_child("Notes"),
+ certificate,
+ emails
+ );
+
+ add_dkdm_recipient(recipient);
+ }
+}
+
+
+void
+DKDMRecipientList::setup(boost::filesystem::path db_file)
+{
+ _dkdm_recipients.add_column("name", "TEXT");
+ _dkdm_recipients.add_column("notes", "TEXT");
+ _dkdm_recipients.add_column("recipient", "TEXT");
+ _dkdm_recipients.add_column("emails", "TEXT");
+
+#ifdef DCPOMATIC_WINDOWS
+ auto rc = sqlite3_open16(db_file.c_str(), &_db);
+#else
+ auto rc = sqlite3_open(db_file.c_str(), &_db);
+#endif
+ if (rc != SQLITE_OK) {
+ throw FileError("Could not open SQLite database", db_file);
+ }
+
+ sqlite3_busy_timeout(_db, 500);
+
+ SQLiteStatement screens(_db, _dkdm_recipients.create());
+ screens.execute();
+}
+
+
+DKDMRecipientList::DKDMRecipientList(DKDMRecipientList&& other)
+ : _dkdm_recipients(std::move(other._dkdm_recipients))
+{
+ _db = other._db;
+ other._db = nullptr;
+}
+
+
+DKDMRecipientList&
+DKDMRecipientList::operator=(DKDMRecipientList&& other)
+{
+ if (this != &other) {
+ _db = other._db;
+ other._db = nullptr;
+ }
+ return *this;
+}
+
+
+DKDMRecipientID
+DKDMRecipientList::add_dkdm_recipient(DKDMRecipient const& dkdm_recipient)
+{
+ SQLiteStatement add_dkdm_recipient(_db, _dkdm_recipients.insert());
+
+ add_dkdm_recipient.bind_text(1, dkdm_recipient.name);
+ add_dkdm_recipient.bind_text(2, dkdm_recipient.notes);
+ add_dkdm_recipient.bind_text(3, dkdm_recipient.recipient ? dkdm_recipient.recipient->certificate(true) : "");
+ add_dkdm_recipient.bind_text(4, join_strings(dkdm_recipient.emails));
+
+ add_dkdm_recipient.execute();
+
+ return sqlite3_last_insert_rowid(_db);
+}
+
+
+void
+DKDMRecipientList::update_dkdm_recipient(DKDMRecipientID id, DKDMRecipient const& dkdm_recipient)
+{
+ SQLiteStatement add_dkdm_recipient(_db, _dkdm_recipients.update("WHERE id=?"));
+
+ add_dkdm_recipient.bind_text(1, dkdm_recipient.name);
+ add_dkdm_recipient.bind_text(2, dkdm_recipient.notes);
+ add_dkdm_recipient.bind_text(3, dkdm_recipient.recipient ? dkdm_recipient.recipient->certificate(true) : "");
+ add_dkdm_recipient.bind_text(4, join_strings(dkdm_recipient.emails));
+ add_dkdm_recipient.bind_int64(5, id.get());
+
+ add_dkdm_recipient.execute();
+}
+
+
+void
+DKDMRecipientList::remove_dkdm_recipient(DKDMRecipientID id)
+{
+ SQLiteStatement statement(_db, "DELETE FROM dkdm_recipients WHERE ID=?");
+ statement.bind_int64(1, id.get());
+ statement.execute();
+}
+
+
+static
+vector<pair<DKDMRecipientID, DKDMRecipient>>
+dkdm_recipients_from_result(SQLiteStatement& statement)
+{
+ vector<pair<DKDMRecipientID, DKDMRecipient>> output;
+
+ statement.execute([&output](SQLiteStatement& statement) {
+ DCPOMATIC_ASSERT(statement.data_count() == 5);
+ DKDMRecipientID const id = statement.column_int64(0);
+ auto const name = statement.column_text(1);
+ auto const notes = statement.column_text(2);
+ auto certificate_string = statement.column_text(3);
+ optional<dcp::Certificate> certificate = certificate_string.empty() ? optional<dcp::Certificate>() : dcp::Certificate(certificate_string);
+ auto const join_with_spaces = statement.column_text(4);
+ vector<string> emails;
+ boost::algorithm::split(emails, join_with_spaces, boost::is_any_of(" "));
+ output.push_back(make_pair(id, DKDMRecipient(name, notes, certificate, { emails })));
+ });
+
+ return output;
+}
+
+
+
+
+vector<std::pair<DKDMRecipientID, DKDMRecipient>>
+DKDMRecipientList::dkdm_recipients() const
+{
+ SQLiteStatement statement(_db, _dkdm_recipients.select("ORDER BY name ASC"));
+ return dkdm_recipients_from_result(statement);
+}
+
+
+boost::optional<DKDMRecipient>
+DKDMRecipientList::dkdm_recipient(DKDMRecipientID id) const
+{
+ SQLiteStatement statement(_db, _dkdm_recipients.select("WHERE id=?"));
+ statement.bind_int64(1, id.get());
+ auto result = dkdm_recipients_from_result(statement);
+ if (result.empty()) {
+ return {};
+ }
+ return result[0].second;
+}
+
+
+void
+DKDMRecipientList::clear()
+{
+ SQLiteStatement sql(_db, "DELETE FROM dkdm_recipients");
+ sql.execute();
+}
+
+
diff --git a/src/lib/dkdm_recipient_list.h b/src/lib/dkdm_recipient_list.h
new file mode 100644
index 000000000..fc4d84b60
--- /dev/null
+++ b/src/lib/dkdm_recipient_list.h
@@ -0,0 +1,90 @@
+/*
+ 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/>.
+
+*/
+
+
+#ifndef DCPOMATIC_DKDM_RECIPIENT_LIST_H
+#define DCPOMATIC_DKDM_RECIPIENT_LIST_H
+
+
+#include "id.h"
+#include "sqlite_table.h"
+#include <libcxml/cxml.h>
+#include <boost/filesystem.hpp>
+#include <boost/optional.hpp>
+
+
+class DKDMRecipient;
+
+
+class DKDMRecipientID : public ID
+{
+public:
+ DKDMRecipientID(sqlite3_int64 id)
+ : ID(id) {}
+
+ bool operator==(DKDMRecipientID const& other) const {
+ return get() == other.get();
+ }
+
+ bool operator!=(DKDMRecipientID const& other) const {
+ return get() != other.get();
+ }
+
+ bool operator<(DKDMRecipientID const& other) const {
+ return get() < other.get();
+ }
+};
+
+
+class DKDMRecipientList
+{
+public:
+ DKDMRecipientList();
+ DKDMRecipientList(boost::filesystem::path db_file);
+ ~DKDMRecipientList();
+
+ DKDMRecipientList(DKDMRecipientList const&) = delete;
+ DKDMRecipientList& operator=(DKDMRecipientList const&) = delete;
+
+ DKDMRecipientList(DKDMRecipientList&& other);
+ DKDMRecipientList& operator=(DKDMRecipientList&& other);
+
+ void read_legacy_file(boost::filesystem::path xml_file);
+ void read_legacy_string(std::string const& xml);
+
+ void clear();
+
+ DKDMRecipientID add_dkdm_recipient(DKDMRecipient const& dkdm_recipient);
+ void update_dkdm_recipient(DKDMRecipientID id, DKDMRecipient const& dkdm_recipient);
+ void remove_dkdm_recipient(DKDMRecipientID id);
+ std::vector<std::pair<DKDMRecipientID, DKDMRecipient>> dkdm_recipients() const;
+ boost::optional<DKDMRecipient> dkdm_recipient(DKDMRecipientID id) const;
+
+private:
+ void setup(boost::filesystem::path db_file);
+ void read_legacy_document(cxml::Document const& doc);
+
+ sqlite3* _db = nullptr;
+ SQLiteTable _dkdm_recipients;
+};
+
+
+#endif
+
diff --git a/src/lib/dkdm_wrapper.cc b/src/lib/dkdm_wrapper.cc
index 016c77c3f..4c7838a8d 100644
--- a/src/lib/dkdm_wrapper.cc
+++ b/src/lib/dkdm_wrapper.cc
@@ -41,7 +41,11 @@ DKDMBase::read (cxml::ConstNodePtr node)
if (node->name() == "DKDM") {
return make_shared<DKDM>(dcp::EncryptedKDM(node->content()));
} else if (node->name() == "DKDMGroup") {
- auto group = make_shared<DKDMGroup>(node->string_attribute("Name"));
+ auto name = node->optional_string_attribute("Name");
+ if (!name) {
+ name = node->string_attribute("name");
+ }
+ auto group = make_shared<DKDMGroup>(*name);
for (auto i: node->node_children()) {
if (auto c = read(i)) {
group->add (c);
@@ -62,17 +66,17 @@ DKDM::name () const
void
-DKDM::as_xml (xmlpp::Element* node) const
+DKDM::as_xml(xmlpp::Element* element) const
{
- node->add_child("DKDM")->add_child_text (_dkdm.as_xml ());
+ cxml::add_text_child(element, "DKDM", _dkdm.as_xml());
}
void
-DKDMGroup::as_xml (xmlpp::Element* node) const
+DKDMGroup::as_xml(xmlpp::Element* element) const
{
- auto f = node->add_child("DKDMGroup");
- f->set_attribute ("Name", _name);
+ auto f = cxml::add_child(element, "DKDMGroup");
+ f->set_attribute("name", _name);
for (auto i: _children) {
i->as_xml (f);
}
diff --git a/src/lib/email.cc b/src/lib/email.cc
index 8557b40e0..bd17bdf5b 100644
--- a/src/lib/email.cc
+++ b/src/lib/email.cc
@@ -23,6 +23,7 @@
#include "config.h"
#include "email.h"
#include "exceptions.h"
+#include "variant.h"
#include <curl/curl.h>
#include <boost/algorithm/string.hpp>
#include <boost/date_time/c_local_time_adjustor.hpp>
@@ -143,9 +144,8 @@ Email::send(string server, int port, EmailProtocol protocol, string user, string
"Content-Type: multipart/mixed; boundary=" + boundary + "\r\n";
}
- _email += "Subject: " + encode_rfc1342(_subject) + "\r\n"
- "User-Agent: DCP-o-matic\r\n"
- "\r\n";
+ _email += "Subject: " + encode_rfc1342(_subject) + "\r\n" +
+ variant::insert_dcpomatic("User-Agent: %1\r\n\r\n");
if (!_attachments.empty ()) {
_email += "--" + boundary + "\r\n"
diff --git a/src/lib/email.h b/src/lib/email.h
index 36398bfd8..ac4703453 100644
--- a/src/lib/email.h
+++ b/src/lib/email.h
@@ -19,6 +19,7 @@
*/
+#include "types.h"
#include <curl/curl.h>
#include <boost/scoped_array.hpp>
diff --git a/src/lib/encode_server.cc b/src/lib/encode_server.cc
index 036ea58a5..8d1759986 100644
--- a/src/lib/encode_server.cc
+++ b/src/lib/encode_server.cc
@@ -37,6 +37,8 @@
#include "image.h"
#include "log.h"
#include "player_video.h"
+#include "util.h"
+#include "variant.h"
#include "version.h"
#include <dcp/raw_convert.h>
#include <dcp/warnings.h>
@@ -81,6 +83,7 @@ EncodeServer::EncodeServer (bool verbose, int num_threads)
#endif
, _verbose (verbose)
, _num_threads (num_threads)
+ , _frames_encoded(0)
{
}
@@ -169,6 +172,8 @@ EncodeServer::process (shared_ptr<Socket> socket, struct timeval& after_read, st
throw;
}
+ ++_frames_encoded;
+
return dcp_video_frame.index ();
}
@@ -243,7 +248,7 @@ EncodeServer::run ()
{
LOG_GENERAL ("Server %1 (%2) starting with %3 threads", dcpomatic_version, dcpomatic_git_commit, _num_threads);
if (_verbose) {
- cout << "DCP-o-matic server starting with " << _num_threads << " threads.\n";
+ cout << variant::dcpomatic_encode_server() << " starting with " << _num_threads << " threads.\n";
}
for (int i = 0; i < _num_threads; ++i) {
@@ -268,12 +273,9 @@ void
EncodeServer::broadcast_thread ()
try
{
- auto address = boost::asio::ip::address_v4::any ();
- boost::asio::ip::udp::endpoint listen_endpoint (address, HELLO_PORT);
+ boost::asio::ip::udp::endpoint listen_endpoint(boost::asio::ip::udp::v4(), HELLO_PORT);
- _broadcast.socket = new boost::asio::ip::udp::socket (_broadcast.io_service);
- _broadcast.socket->open (listen_endpoint.protocol ());
- _broadcast.socket->bind (listen_endpoint);
+ _broadcast.socket = new boost::asio::ip::udp::socket(_broadcast.io_service, listen_endpoint);
_broadcast.socket->async_receive_from (
boost::asio::buffer (_broadcast.buffer, sizeof (_broadcast.buffer)),
@@ -298,8 +300,8 @@ EncodeServer::broadcast_received ()
/* Reply to the client saying what we can do */
xmlpp::Document doc;
auto root = doc.create_root_node ("ServerAvailable");
- root->add_child("Threads")->add_child_text (raw_convert<string> (_worker_threads.size ()));
- root->add_child("Version")->add_child_text (raw_convert<string> (SERVER_LINK_VERSION));
+ cxml::add_text_child(root, "Threads", raw_convert<string>(_worker_threads.size()));
+ cxml::add_text_child(root, "Version", raw_convert<string>(SERVER_LINK_VERSION));
auto xml = doc.write_to_string ("UTF-8");
if (_verbose) {
diff --git a/src/lib/encode_server.h b/src/lib/encode_server.h
index f93d66746..8059abd0f 100644
--- a/src/lib/encode_server.h
+++ b/src/lib/encode_server.h
@@ -32,6 +32,7 @@
#include "exception_store.h"
#include "server.h"
#include <boost/asio.hpp>
+#include <boost/atomic.hpp>
#include <boost/thread.hpp>
#include <boost/thread/condition.hpp>
#include <string>
@@ -53,6 +54,10 @@ public:
void run () override;
+ int frames_encoded() const {
+ return _frames_encoded;
+ }
+
private:
void handle (std::shared_ptr<Socket>) override;
void worker_thread ();
@@ -67,6 +72,7 @@ private:
bool _verbose;
int _num_threads;
Waker _waker;
+ boost::atomic<int> _frames_encoded;
struct Broadcast {
diff --git a/src/lib/encode_server_finder.cc b/src/lib/encode_server_finder.cc
index 1d4ced595..143569e16 100644
--- a/src/lib/encode_server_finder.cc
+++ b/src/lib/encode_server_finder.cc
@@ -26,6 +26,8 @@
#include "encode_server_description.h"
#include "encode_server_finder.h"
#include "exceptions.h"
+#include "util.h"
+#include "variant.h"
#include <dcp/raw_convert.h>
#include <libcxml/cxml.h>
#include <boost/bind/placeholders.hpp>
@@ -188,7 +190,7 @@ try {
new tcp::acceptor (_listen_io_service, tcp::endpoint(tcp::v4(), is_batch_converter ? BATCH_SERVER_PRESENCE_PORT : MAIN_SERVER_PRESENCE_PORT))
);
} catch (...) {
- boost::throw_exception (NetworkError (_("Could not listen for remote encode servers. Perhaps another instance of DCP-o-matic is running.")));
+ boost::throw_exception(NetworkError(variant::insert_dcpomatic(_("Could not listen for remote encode servers. Perhaps another instance of %1 is running."))));
}
start_accept ();
diff --git a/src/lib/encode_server_finder.h b/src/lib/encode_server_finder.h
index f8a30af54..c478387f9 100644
--- a/src/lib/encode_server_finder.h
+++ b/src/lib/encode_server_finder.h
@@ -50,8 +50,6 @@ public:
static EncodeServerFinder* instance ();
static void drop ();
- void stop ();
-
std::list<EncodeServerDescription> servers () const;
/** Emitted whenever the list of servers changes */
@@ -62,6 +60,7 @@ private:
~EncodeServerFinder ();
void start ();
+ void stop ();
void search_thread ();
void listen_thread ();
diff --git a/src/lib/environment_info.cc b/src/lib/environment_info.cc
index 2e0347c79..d6592d126 100644
--- a/src/lib/environment_info.cc
+++ b/src/lib/environment_info.cc
@@ -22,6 +22,7 @@
#include "compose.hpp"
#include "cross.h"
#include "log.h"
+#include "variant.h"
#include "version.h"
#include <dcp/version.h>
#include <dcp/warnings.h>
@@ -85,7 +86,7 @@ environment_info ()
{
list<string> info;
- info.push_back (String::compose ("DCP-o-matic %1 git %2 using %3", dcpomatic_version, dcpomatic_git_commit, dependency_version_summary()));
+ info.push_back(String::compose("%1 %2 git %3 using %4", variant::dcpomatic(), dcpomatic_version, dcpomatic_git_commit, dependency_version_summary()));
{
char buffer[128];
@@ -94,9 +95,9 @@ environment_info ()
}
#ifdef DCPOMATIC_DEBUG
- info.push_back ("DCP-o-matic built in debug mode.");
+ info.push_back(variant::insert_dcpomatic("%1 built in debug mode."));
#else
- info.push_back ("DCP-o-matic built in optimised mode.");
+ info.push_back(variant::insert_dcpomatic("%1 built in optimised mode."));
#endif
#ifdef LIBDCP_DEBUG
info.push_back ("libdcp built in debug mode.");
diff --git a/src/lib/exceptions.h b/src/lib/exceptions.h
index 10231f59a..e08392689 100644
--- a/src/lib/exceptions.h
+++ b/src/lib/exceptions.h
@@ -34,6 +34,7 @@ extern "C" {
}
#include <boost/filesystem.hpp>
#include <boost/optional.hpp>
+#include <sqlite3.h>
#include <cstring>
#include <stdexcept>
@@ -480,4 +481,58 @@ public:
};
+class SQLError : public std::runtime_error
+{
+public:
+ SQLError(sqlite3* db, char const* s)
+ : std::runtime_error(get_message(db, s))
+ {
+ _filename = get_filename(db);
+ }
+
+ SQLError(sqlite3* db, int rc)
+ : std::runtime_error(get_message(db, rc))
+ {
+ _filename = get_filename(db);
+ }
+
+ SQLError(sqlite3* db, int rc, std::string doing)
+ : std::runtime_error(get_message(db, rc, doing))
+ {
+ _filename = get_filename(db);
+ }
+
+ boost::filesystem::path filename() const {
+ return _filename;
+ }
+
+private:
+ boost::filesystem::path get_filename(sqlite3* db)
+ {
+ if (auto filename = sqlite3_db_filename(db, "main")) {
+ return filename;
+ }
+
+ return {};
+ }
+
+ std::string get_message(sqlite3* db, char const* s)
+ {
+ return String::compose("%1 (in %2)", s, get_filename(db));
+ }
+
+ std::string get_message(sqlite3* db, int rc)
+ {
+ return String::compose("%1 (in %2)", sqlite3_errstr(rc), get_filename(db));
+ }
+
+ std::string get_message(sqlite3* db, int rc, std::string doing)
+ {
+ return String::compose("%1 (while doing %2) (in %3)", sqlite3_errstr(rc), doing, get_filename(db));
+ }
+
+ boost::filesystem::path _filename;
+};
+
+
#endif
diff --git a/src/lib/export_config.cc b/src/lib/export_config.cc
index e030b98e2..4dfbb9027 100644
--- a/src/lib/export_config.cc
+++ b/src/lib/export_config.cc
@@ -77,7 +77,7 @@ ExportConfig::read(cxml::ConstNodePtr node)
void
-ExportConfig::write(xmlpp::Element* node) const
+ExportConfig::write(xmlpp::Element* element) const
{
string name;
@@ -97,11 +97,11 @@ ExportConfig::write(xmlpp::Element* node) const
break;
}
- node->add_child("Format")->add_child_text(name);
- node->add_child("MixdownToStereo")->add_child_text(_mixdown_to_stereo ? "1" : "0");
- node->add_child("SplitReels")->add_child_text(_split_reels ? "1" : "0");
- node->add_child("SplitStreams")->add_child_text(_split_streams ? "1" : "0");
- node->add_child("X264CRF")->add_child_text(dcp::raw_convert<string>(_x264_crf));
+ cxml::add_text_child(element, "Format", name);
+ cxml::add_text_child(element, "MixdownToStereo", _mixdown_to_stereo ? "1" : "0");
+ cxml::add_text_child(element, "SplitReels", _split_reels ? "1" : "0");
+ cxml::add_text_child(element, "SplitStreams", _split_streams ? "1" : "0");
+ cxml::add_text_child(element, "X264CRF", dcp::raw_convert<string>(_x264_crf));
}
diff --git a/src/lib/export_config.h b/src/lib/export_config.h
index 47dddb6c3..52d7b601d 100644
--- a/src/lib/export_config.h
+++ b/src/lib/export_config.h
@@ -37,7 +37,7 @@ public:
void set_defaults();
void read(cxml::ConstNodePtr node);
- void write(xmlpp::Element* node) const;
+ void write(xmlpp::Element* element) const;
ExportFormat format() const {
return _format;
diff --git a/src/lib/ffmpeg_audio_stream.cc b/src/lib/ffmpeg_audio_stream.cc
index 9400eb60d..24f96b889 100644
--- a/src/lib/ffmpeg_audio_stream.cc
+++ b/src/lib/ffmpeg_audio_stream.cc
@@ -52,19 +52,19 @@ FFmpegAudioStream::FFmpegAudioStream (cxml::ConstNodePtr node, int version)
void
-FFmpegAudioStream::as_xml (xmlpp::Node* root) const
+FFmpegAudioStream::as_xml(xmlpp::Element* root) const
{
FFmpegStream::as_xml (root);
- root->add_child("FrameRate")->add_child_text(raw_convert<string>(frame_rate()));
- root->add_child("Length")->add_child_text(raw_convert<string>(length()));
- mapping().as_xml (root->add_child("Mapping"));
+ cxml::add_text_child(root, "FrameRate", raw_convert<string>(frame_rate()));
+ cxml::add_text_child(root, "Length", raw_convert<string>(length()));
+ mapping().as_xml(cxml::add_child(root, "Mapping"));
if (first_audio) {
- root->add_child("FirstAudio")->add_child_text(raw_convert<string>(first_audio.get().get()));
+ cxml::add_text_child(root, "FirstAudio", raw_convert<string>(first_audio.get().get()));
}
if (codec_name) {
- root->add_child("CodecName")->add_child_text(codec_name.get());
+ cxml::add_text_child(root, "CodecName", codec_name.get());
}
if (bit_depth()) {
- root->add_child("BitDepth")->add_child_text(raw_convert<string>(bit_depth().get()));
+ cxml::add_text_child(root, "BitDepth", raw_convert<string>(bit_depth().get()));
}
}
diff --git a/src/lib/ffmpeg_audio_stream.h b/src/lib/ffmpeg_audio_stream.h
index aae982f9e..afd30e905 100644
--- a/src/lib/ffmpeg_audio_stream.h
+++ b/src/lib/ffmpeg_audio_stream.h
@@ -48,7 +48,7 @@ public:
FFmpegAudioStream (cxml::ConstNodePtr, int);
- void as_xml (xmlpp::Node *) const;
+ void as_xml(xmlpp::Element*) const;
/* XXX: should probably be locked */
diff --git a/src/lib/ffmpeg_content.cc b/src/lib/ffmpeg_content.cc
index 4a7c87b34..c2bb5ffe4 100644
--- a/src/lib/ffmpeg_content.cc
+++ b/src/lib/ffmpeg_content.cc
@@ -34,6 +34,7 @@
#include "job.h"
#include "log.h"
#include "text_content.h"
+#include "variant.h"
#include "video_content.h"
#include <dcp/raw_convert.h>
#include <libcxml/cxml.h>
@@ -118,7 +119,7 @@ FFmpegContent::FFmpegContent (cxml::ConstNodePtr node, int version, list<string>
if (auto filter = Filter::from_id(i->content())) {
_filters.push_back(*filter);
} else {
- notes.push_back (String::compose (_("DCP-o-matic no longer supports the `%1' filter, so it has been turned off."), i->content()));
+ notes.push_back(String::compose(_("%1 no longer supports the `%2' filter, so it has been turned off."), variant::dcpomatic(), i->content()));
}
}
@@ -197,61 +198,61 @@ FFmpegContent::FFmpegContent (vector<shared_ptr<Content>> c)
void
-FFmpegContent::as_xml (xmlpp::Node* node, bool with_paths) const
+FFmpegContent::as_xml(xmlpp::Element* element, bool with_paths) const
{
- node->add_child("Type")->add_child_text("FFmpeg");
- Content::as_xml (node, with_paths);
+ cxml::add_text_child(element, "Type", "FFmpeg");
+ Content::as_xml(element, with_paths);
if (video) {
- video->as_xml (node);
+ video->as_xml(element);
}
if (audio) {
- audio->as_xml (node);
+ audio->as_xml(element);
for (auto i: audio->streams()) {
auto f = dynamic_pointer_cast<FFmpegAudioStream> (i);
DCPOMATIC_ASSERT (f);
- f->as_xml (node->add_child("AudioStream"));
+ f->as_xml(cxml::add_child(element, "AudioStream"));
}
}
if (only_text()) {
- only_text()->as_xml (node);
+ only_text()->as_xml(element);
}
boost::mutex::scoped_lock lm (_mutex);
for (auto i: _subtitle_streams) {
- auto t = node->add_child("SubtitleStream");
+ auto t = cxml::add_child(element, "SubtitleStream");
if (_subtitle_stream && i == _subtitle_stream) {
- t->add_child("Selected")->add_child_text("1");
+ cxml::add_text_child(t, "Selected", "1");
}
i->as_xml (t);
}
for (auto i: _filters) {
- node->add_child("Filter")->add_child_text(i.id());
+ cxml::add_text_child(element, "Filter", i.id());
}
if (_first_video) {
- node->add_child("FirstVideo")->add_child_text(raw_convert<string>(_first_video.get().get()));
+ cxml::add_text_child(element, "FirstVideo", raw_convert<string>(_first_video.get().get()));
}
if (_color_range) {
- node->add_child("ColorRange")->add_child_text(raw_convert<string>(static_cast<int>(*_color_range)));
+ cxml::add_text_child(element, "ColorRange", raw_convert<string>(static_cast<int>(*_color_range)));
}
if (_color_primaries) {
- node->add_child("ColorPrimaries")->add_child_text(raw_convert<string>(static_cast<int>(*_color_primaries)));
+ cxml::add_text_child(element, "ColorPrimaries", raw_convert<string>(static_cast<int>(*_color_primaries)));
}
if (_color_trc) {
- node->add_child("ColorTransferCharacteristic")->add_child_text(raw_convert<string>(static_cast<int>(*_color_trc)));
+ cxml::add_text_child(element, "ColorTransferCharacteristic", raw_convert<string>(static_cast<int>(*_color_trc)));
}
if (_colorspace) {
- node->add_child("Colorspace")->add_child_text(raw_convert<string>(static_cast<int>(*_colorspace)));
+ cxml::add_text_child(element, "Colorspace", raw_convert<string>(static_cast<int>(*_colorspace)));
}
if (_bits_per_pixel) {
- node->add_child("BitsPerPixel")->add_child_text(raw_convert<string>(*_bits_per_pixel));
+ cxml::add_text_child(element, "BitsPerPixel", raw_convert<string>(*_bits_per_pixel));
}
}
diff --git a/src/lib/ffmpeg_content.h b/src/lib/ffmpeg_content.h
index a86358b76..ce067f2d3 100644
--- a/src/lib/ffmpeg_content.h
+++ b/src/lib/ffmpeg_content.h
@@ -70,7 +70,7 @@ public:
void take_settings_from (std::shared_ptr<const Content> c) override;
std::string summary () const override;
std::string technical_summary () const override;
- void as_xml (xmlpp::Node *, bool with_paths) const override;
+ void as_xml(xmlpp::Element* element, bool with_paths) const override;
dcpomatic::DCPTime full_length (std::shared_ptr<const Film> film) const override;
dcpomatic::DCPTime approximate_length () const override;
diff --git a/src/lib/ffmpeg_decoder.cc b/src/lib/ffmpeg_decoder.cc
index 45983795b..29ab5aaf8 100644
--- a/src/lib/ffmpeg_decoder.cc
+++ b/src/lib/ffmpeg_decoder.cc
@@ -178,9 +178,8 @@ FFmpegDecoder::flush_fill()
full_length = full_length.ceil (frc.source);
if (video && !video->ignore()) {
double const vfr = _ffmpeg_content->video_frame_rate().get();
- auto const f = full_length.frames_round (vfr);
- auto const v = video->position(film()).get_value_or(ContentTime()).frames_round(vfr) + 1;
- if (v < f) {
+ auto const v = video->position(film()).get_value_or(ContentTime()) + ContentTime::from_frames(1, vfr);
+ if (v < full_length) {
video->emit(film(), make_shared<const RawImageProxy>(_black_image), v);
did_something = true;
}
@@ -260,7 +259,7 @@ deinterleave_audio(AVFrame* frame)
/* XXX: can't we use swr_convert() to do the format conversion? */
- int const channels = frame->channels;
+ int const channels = frame->ch_layout.nb_channels;
int const frames = frame->nb_samples;
int const total_samples = frames * channels;
auto audio = make_shared<AudioBuffers>(channels, frames);
@@ -622,7 +621,7 @@ FFmpegDecoder::process_video_frame ()
video->emit (
film(),
make_shared<RawImageProxy>(image),
- llrint(pts * _ffmpeg_content->active_video_frame_rate(film()))
+ ContentTime::from_seconds(pts)
);
} else {
LOG_WARNING_NC ("Dropping frame without PTS");
diff --git a/src/lib/ffmpeg_examiner.cc b/src/lib/ffmpeg_examiner.cc
index 51ade8e89..d173c6bb6 100644
--- a/src/lib/ffmpeg_examiner.cc
+++ b/src/lib/ffmpeg_examiner.cc
@@ -73,14 +73,6 @@ FFmpegExaminer::FFmpegExaminer (shared_ptr<const FFmpegContent> c, shared_ptr<Jo
auto codec = _codec_context[i] ? _codec_context[i]->codec : nullptr;
if (s->codecpar->codec_type == AVMEDIA_TYPE_AUDIO && codec) {
- /* This is a hack; sometimes it seems that _audio_codec_context->channel_layout isn't set up,
- so bodge it here. No idea why we should have to do this.
- */
-
- if (s->codecpar->channel_layout == 0) {
- s->codecpar->channel_layout = av_get_default_channel_layout (s->codecpar->channels);
- }
-
DCPOMATIC_ASSERT (_format_context->duration != AV_NOPTS_VALUE);
DCPOMATIC_ASSERT (codec->name);
@@ -91,7 +83,7 @@ FFmpegExaminer::FFmpegExaminer (shared_ptr<const FFmpegContent> c, shared_ptr<Jo
s->id,
s->codecpar->sample_rate,
llrint ((double(_format_context->duration) / AV_TIME_BASE) * s->codecpar->sample_rate),
- s->codecpar->channels,
+ s->codecpar->ch_layout.nb_channels,
s->codecpar->bits_per_raw_sample ? s->codecpar->bits_per_raw_sample : s->codecpar->bits_per_coded_sample
)
);
@@ -161,7 +153,7 @@ FFmpegExaminer::FFmpegExaminer (shared_ptr<const FFmpegContent> c, shared_ptr<Jo
av_packet_free (&packet);
- if (_first_video && got_all_audio && temporal_reference.size() >= (PULLDOWN_CHECK_FRAMES * 2)) {
+ if (got_all_audio && (!_video_stream || (_first_video && temporal_reference.size() >= (PULLDOWN_CHECK_FRAMES * 2)))) {
/* All done */
break;
}
@@ -181,8 +173,6 @@ FFmpegExaminer::FFmpegExaminer (shared_ptr<const FFmpegContent> c, shared_ptr<Jo
/* This code taken from get_rotation() in ffmpeg:cmdutils.c */
auto stream = _format_context->streams[*_video_stream];
auto rotate_tag = av_dict_get (stream->metadata, "rotate", 0, 0);
- uint8_t* displaymatrix = av_stream_get_side_data (stream, AV_PKT_DATA_DISPLAYMATRIX, 0);
-
if (rotate_tag && *rotate_tag->value && strcmp(rotate_tag->value, "0")) {
char *tail;
_rotation = av_strtod (rotate_tag->value, &tail);
@@ -191,8 +181,9 @@ FFmpegExaminer::FFmpegExaminer (shared_ptr<const FFmpegContent> c, shared_ptr<Jo
}
}
- if (displaymatrix && !_rotation) {
- _rotation = - av_display_rotation_get ((int32_t*) displaymatrix);
+ auto side_data = av_packet_side_data_get(stream->codecpar->coded_side_data, stream->codecpar->nb_coded_side_data, AV_PKT_DATA_DISPLAYMATRIX);
+ if (side_data && !_rotation) {
+ _rotation = - av_display_rotation_get(reinterpret_cast<int32_t*>(side_data->data));
}
if (_rotation) {
@@ -253,7 +244,7 @@ FFmpegExaminer::video_packet (AVCodecContext* context, string& temporal_referenc
).get_value_or (ContentTime ()).frames_round (video_frame_rate().get ());
}
if (temporal_reference.size() < (PULLDOWN_CHECK_FRAMES * 2)) {
- temporal_reference += (_video_frame->top_field_first ? "T" : "B");
+ temporal_reference += ((_video_frame->flags & AV_FRAME_FLAG_TOP_FIELD_FIRST) ? "T" : "B");
temporal_reference += (_video_frame->repeat_pict ? "3" : "2");
}
diff --git a/src/lib/ffmpeg_file_encoder.cc b/src/lib/ffmpeg_file_encoder.cc
index 6d1ad68f7..4547a8e8e 100644
--- a/src/lib/ffmpeg_file_encoder.cc
+++ b/src/lib/ffmpeg_file_encoder.cc
@@ -21,7 +21,7 @@
#include "compose.hpp"
#include "cross.h"
-#include "ffmpeg_encoder.h"
+#include "ffmpeg_file_encoder.h"
#include "ffmpeg_wrapper.h"
#include "film.h"
#include "image.h"
@@ -73,8 +73,7 @@ public:
_codec_context->bit_rate = channels * 128 * 1024;
_codec_context->sample_fmt = sample_format;
_codec_context->sample_rate = frame_rate;
- _codec_context->channel_layout = av_get_default_channel_layout (channels);
- _codec_context->channels = channels;
+ av_channel_layout_default(&_codec_context->ch_layout, channels);
int r = avcodec_open2 (_codec_context, _codec, 0);
if (r < 0) {
@@ -143,7 +142,7 @@ public:
frame->nb_samples = size;
frame->format = _codec_context->sample_fmt;
- frame->channels = channels;
+ frame->ch_layout.nb_channels = channels;
int r = avcodec_fill_audio_frame (frame, channels, _codec_context->sample_fmt, (const uint8_t *) samples, buffer_size, 0);
DCPOMATIC_ASSERT (r >= 0);
diff --git a/src/lib/ffmpeg_file_encoder.h b/src/lib/ffmpeg_file_encoder.h
index 78840d6a8..907eca53d 100644
--- a/src/lib/ffmpeg_file_encoder.h
+++ b/src/lib/ffmpeg_file_encoder.h
@@ -23,12 +23,14 @@
#define DCPOMATIC_FFMPEG_FILE_ENCODER_H
+#include "audio_buffers.h"
#include "audio_mapping.h"
#include "dcpomatic_time.h"
-#include "encoder.h"
#include "event_history.h"
#include "image_store.h"
#include "log.h"
+#include "player_text.h"
+#include "player_video.h"
#include <dcp/key.h>
#include <dcp/warnings.h>
LIBDCP_DISABLE_WARNINGS
diff --git a/src/lib/ffmpeg_encoder.cc b/src/lib/ffmpeg_film_encoder.cc
index c1170f098..a2d26fd66 100644
--- a/src/lib/ffmpeg_encoder.cc
+++ b/src/lib/ffmpeg_film_encoder.cc
@@ -21,7 +21,7 @@
#include "butler.h"
#include "cross.h"
-#include "ffmpeg_encoder.h"
+#include "ffmpeg_film_encoder.h"
#include "film.h"
#include "image.h"
#include "job.h"
@@ -48,7 +48,7 @@ using namespace boost::placeholders;
#endif
-FFmpegEncoder::FFmpegEncoder (
+FFmpegFilmEncoder::FFmpegFilmEncoder(
shared_ptr<const Film> film,
weak_ptr<Job> job,
boost::filesystem::path output,
@@ -58,7 +58,7 @@ FFmpegEncoder::FFmpegEncoder (
bool audio_stream_per_channel,
int x264_crf
)
- : Encoder (film, job)
+ : FilmEncoder(film, job)
, _output_audio_channels(mixdown_to_stereo ? 2 : (_film->audio_channels() > 8 ? 16 : _film->audio_channels()))
, _history (200)
, _output (output)
@@ -85,7 +85,7 @@ FFmpegEncoder::FFmpegEncoder (
AudioMapping
-FFmpegEncoder::stereo_map() const
+FFmpegFilmEncoder::stereo_map() const
{
auto map = AudioMapping(_film->audio_channels(), 2);
float const overall_gain = 2 / (4 + sqrt(2));
@@ -116,7 +116,7 @@ FFmpegEncoder::stereo_map() const
AudioMapping
-FFmpegEncoder::many_channel_map() const
+FFmpegFilmEncoder::many_channel_map() const
{
auto map = AudioMapping(_film->audio_channels(), _output_audio_channels);
for (int i = 0; i < _film->audio_channels(); ++i) {
@@ -127,7 +127,7 @@ FFmpegEncoder::many_channel_map() const
void
-FFmpegEncoder::go ()
+FFmpegFilmEncoder::go()
{
{
auto job = _job.lock ();
@@ -234,19 +234,19 @@ FFmpegEncoder::go ()
}
optional<float>
-FFmpegEncoder::current_rate () const
+FFmpegFilmEncoder::current_rate() const
{
return _history.rate ();
}
Frame
-FFmpegEncoder::frames_done () const
+FFmpegFilmEncoder::frames_done() const
{
boost::mutex::scoped_lock lm (_mutex);
return _last_time.frames_round (_film->video_frame_rate ());
}
-FFmpegEncoder::FileEncoderSet::FileEncoderSet (
+FFmpegFilmEncoder::FileEncoderSet::FileEncoderSet(
dcp::Size video_frame_size,
int video_frame_rate,
int audio_frame_rate,
@@ -279,7 +279,7 @@ FFmpegEncoder::FileEncoderSet::FileEncoderSet (
}
shared_ptr<FFmpegFileEncoder>
-FFmpegEncoder::FileEncoderSet::get (Eyes eyes) const
+FFmpegFilmEncoder::FileEncoderSet::get(Eyes eyes) const
{
if (_encoders.size() == 1) {
/* We are doing a 2D export... */
@@ -298,7 +298,7 @@ FFmpegEncoder::FileEncoderSet::get (Eyes eyes) const
}
void
-FFmpegEncoder::FileEncoderSet::flush ()
+FFmpegFilmEncoder::FileEncoderSet::flush()
{
for (auto& i: _encoders) {
i.second->flush ();
@@ -306,7 +306,7 @@ FFmpegEncoder::FileEncoderSet::flush ()
}
void
-FFmpegEncoder::FileEncoderSet::audio (shared_ptr<AudioBuffers> a)
+FFmpegFilmEncoder::FileEncoderSet::audio(shared_ptr<AudioBuffers> a)
{
for (auto& i: _encoders) {
i.second->audio (a);
diff --git a/src/lib/ffmpeg_encoder.h b/src/lib/ffmpeg_film_encoder.h
index 2d5c6af87..ec6bb4594 100644
--- a/src/lib/ffmpeg_encoder.h
+++ b/src/lib/ffmpeg_film_encoder.h
@@ -23,15 +23,15 @@
#include "audio_mapping.h"
#include "butler.h"
-#include "encoder.h"
#include "event_history.h"
#include "ffmpeg_file_encoder.h"
+#include "film_encoder.h"
-class FFmpegEncoder : public Encoder
+class FFmpegFilmEncoder : public FilmEncoder
{
public:
- FFmpegEncoder (
+ FFmpegFilmEncoder(
std::shared_ptr<const Film> film,
std::weak_ptr<Job> job,
boost::filesystem::path output,
diff --git a/src/lib/ffmpeg_image_proxy.cc b/src/lib/ffmpeg_image_proxy.cc
index 2fcd486df..0ee41deab 100644
--- a/src/lib/ffmpeg_image_proxy.cc
+++ b/src/lib/ffmpeg_image_proxy.cc
@@ -234,9 +234,9 @@ FFmpegImageProxy::image (Image::Alignment alignment, optional<dcp::Size>) const
void
-FFmpegImageProxy::add_metadata (xmlpp::Node* node) const
+FFmpegImageProxy::add_metadata(xmlpp::Element* element) const
{
- node->add_child("Type")->add_child_text (N_("FFmpeg"));
+ cxml::add_text_child(element, "Type", N_("FFmpeg"));
}
void
diff --git a/src/lib/ffmpeg_image_proxy.h b/src/lib/ffmpeg_image_proxy.h
index b567fadee..3817d709f 100644
--- a/src/lib/ffmpeg_image_proxy.h
+++ b/src/lib/ffmpeg_image_proxy.h
@@ -28,14 +28,14 @@ class FFmpegImageProxy : public ImageProxy
public:
explicit FFmpegImageProxy (boost::filesystem::path);
explicit FFmpegImageProxy (dcp::ArrayData);
- FFmpegImageProxy (std::shared_ptr<Socket> socket);
+ explicit FFmpegImageProxy (std::shared_ptr<Socket> socket);
Result image (
Image::Alignment alignment,
boost::optional<dcp::Size> size = boost::optional<dcp::Size> ()
) const override;
- void add_metadata (xmlpp::Node *) const override;
+ void add_metadata(xmlpp::Element*) const override;
void write_to_socket (std::shared_ptr<Socket>) const override;
bool same (std::shared_ptr<const ImageProxy> other) const override;
size_t memory_used () const override;
diff --git a/src/lib/ffmpeg_stream.cc b/src/lib/ffmpeg_stream.cc
index 47f71d119..800c45eb6 100644
--- a/src/lib/ffmpeg_stream.cc
+++ b/src/lib/ffmpeg_stream.cc
@@ -40,10 +40,10 @@ FFmpegStream::FFmpegStream (cxml::ConstNodePtr node)
}
void
-FFmpegStream::as_xml (xmlpp::Node* root) const
+FFmpegStream::as_xml(xmlpp::Element* root) const
{
- root->add_child("Name")->add_child_text (name);
- root->add_child("Id")->add_child_text (raw_convert<string> (_id));
+ cxml::add_text_child(root, "Name", name);
+ cxml::add_text_child(root, "Id", raw_convert<string>(_id));
}
bool
diff --git a/src/lib/ffmpeg_stream.h b/src/lib/ffmpeg_stream.h
index 84b2a8853..221075eeb 100644
--- a/src/lib/ffmpeg_stream.h
+++ b/src/lib/ffmpeg_stream.h
@@ -37,7 +37,7 @@ public:
explicit FFmpegStream (cxml::ConstNodePtr);
- void as_xml (xmlpp::Node *) const;
+ void as_xml(xmlpp::Element*) const;
/** @param c An AVFormatContext.
* @param index A stream index within the AVFormatContext.
diff --git a/src/lib/ffmpeg_subtitle_stream.cc b/src/lib/ffmpeg_subtitle_stream.cc
index 1101901b7..3f43b6ec5 100644
--- a/src/lib/ffmpeg_subtitle_stream.cc
+++ b/src/lib/ffmpeg_subtitle_stream.cc
@@ -49,15 +49,15 @@ FFmpegSubtitleStream::FFmpegSubtitleStream (cxml::ConstNodePtr node, int version
}
void
-FFmpegSubtitleStream::as_xml (xmlpp::Node* root) const
+FFmpegSubtitleStream::as_xml(xmlpp::Element* root) const
{
- FFmpegStream::as_xml (root);
+ FFmpegStream::as_xml(root);
boost::mutex::scoped_lock lm (_mutex);
for (map<RGBA, RGBA>::const_iterator i = _colours.begin(); i != _colours.end(); ++i) {
- xmlpp::Node* node = root->add_child("Colour");
- i->first.as_xml (node->add_child("From"));
- i->second.as_xml (node->add_child("To"));
+ auto node = cxml::add_child(root, "Colour");
+ i->first.as_xml(cxml::add_child(node, "From"));
+ i->second.as_xml(cxml::add_child(node, "To"));
}
}
diff --git a/src/lib/ffmpeg_subtitle_stream.h b/src/lib/ffmpeg_subtitle_stream.h
index 8f56d1975..6251350ce 100644
--- a/src/lib/ffmpeg_subtitle_stream.h
+++ b/src/lib/ffmpeg_subtitle_stream.h
@@ -33,7 +33,7 @@ public:
FFmpegSubtitleStream (cxml::ConstNodePtr node, int version);
- void as_xml (xmlpp::Node *) const;
+ void as_xml(xmlpp::Element*) const;
void set_colour (RGBA from, RGBA to);
std::map<RGBA, RGBA> colours () const;
diff --git a/src/lib/film.cc b/src/lib/film.cc
index d9ab6e2a3..292004a8c 100644
--- a/src/lib/film.cc
+++ b/src/lib/film.cc
@@ -36,7 +36,7 @@
#include "cross.h"
#include "dcp_content.h"
#include "dcp_content_type.h"
-#include "dcp_encoder.h"
+#include "dcp_film_encoder.h"
#include "dcpomatic_log.h"
#include "digester.h"
#include "environment_info.h"
@@ -57,6 +57,7 @@
#include "text_content.h"
#include "transcode_job.h"
#include "upload_job.h"
+#include "variant.h"
#include "video_content.h"
#include "version.h"
#include <libcxml/cxml.h>
@@ -114,6 +115,8 @@ using namespace dcpomatic;
static constexpr char metadata_file[] = "metadata.xml";
+static constexpr char ui_state_file[] = "ui.xml";
+static constexpr char assets_file[] = "assets.xml";
/* 5 -> 6
@@ -150,6 +153,14 @@ int const Film::current_state_version = 38;
/** Construct a Film object in a given directory.
*
+ * Some initial values are taken from the Config object, and usually then overwritten
+ * by reading in the default template. Setting up things like _dcp_content_type in
+ * this constructor is useful so that if people configured such things in old versions
+ * they are used when the first default template is written by Config
+ *
+ * At some point these config entries can be removed, and this constructor could
+ * perhaps read the default template itself.
+ *
* @param dir Film directory.
*/
@@ -161,12 +172,12 @@ Film::Film (optional<boost::filesystem::path> dir)
, _resolution (Resolution::TWO_K)
, _encrypted (false)
, _context_id (dcp::make_uuid ())
- , _j2k_bandwidth (Config::instance()->default_j2k_bandwidth ())
, _video_frame_rate (24)
, _audio_channels (Config::instance()->default_dcp_audio_channels ())
, _three_d (false)
, _sequence (true)
, _interop (Config::instance()->default_interop ())
+ , _video_encoding(VideoEncoding::JPEG2000)
, _limit_to_smpte_bv20(false)
, _audio_processor (0)
, _reel_type (ReelType::SINGLE)
@@ -200,6 +211,10 @@ Film::Film (optional<boost::filesystem::path> dir)
_studio = metadata["studio"];
}
+ for (auto encoding: {VideoEncoding::JPEG2000, VideoEncoding::MPEG2}) {
+ _video_bit_rate[encoding] = Config::instance()->default_video_bit_rate(encoding);
+ }
+
_playlist_change_connection = _playlist->Change.connect (bind (&Film::playlist_change, this, _1));
_playlist_order_changed_connection = _playlist->OrderChange.connect (bind (&Film::playlist_order_changed, this));
_playlist_content_change_connection = _playlist->ContentChange.connect (bind (&Film::playlist_content_change, this, _1, _2, _3, _4));
@@ -238,7 +253,7 @@ Film::video_identifier () const
+ "_" + resolution_to_string (_resolution)
+ "_" + _playlist->video_identifier()
+ "_" + raw_convert<string>(_video_frame_rate)
- + "_" + raw_convert<string>(j2k_bandwidth());
+ + "_" + raw_convert<string>(video_bit_rate(video_encoding()));
if (encrypted ()) {
/* This is insecure but hey, the key is in plaintext in metadata.xml */
@@ -249,6 +264,9 @@ Film::video_identifier () const
if (_interop) {
s += "_I";
+ if (_video_encoding == VideoEncoding::MPEG2) {
+ s += "_M";
+ }
} else {
s += "_S";
if (_limit_to_smpte_bv20) {
@@ -279,17 +297,6 @@ Film::info_file (DCPTimePeriod period) const
return file (p);
}
-boost::filesystem::path
-Film::internal_video_asset_dir () const
-{
- return dir ("video");
-}
-
-boost::filesystem::path
-Film::internal_video_asset_filename (DCPTimePeriod p) const
-{
- return video_identifier() + "_" + raw_convert<string> (p.from.get()) + "_" + raw_convert<string> (p.to.get()) + ".mxf";
-}
boost::filesystem::path
Film::audio_analysis_path (shared_ptr<const Playlist> playlist) const
@@ -334,6 +341,13 @@ Film::audio_analysis_path (shared_ptr<const Playlist> playlist) const
boost::filesystem::path
+Film::assets_path() const
+{
+ return dir("assets");
+}
+
+
+boost::filesystem::path
Film::subtitle_analysis_path (shared_ptr<const Content> content) const
{
auto p = dir ("analysis");
@@ -379,88 +393,93 @@ 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, "J2KVideoBitRate", raw_convert<string>(_video_bit_rate[VideoEncoding::JPEG2000]));
+ cxml::add_text_child(root, "MPEG2VideoBitRate", raw_convert<string>(_video_bit_rate[VideoEncoding::MPEG2]));
+ 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, "VideoEncoding", video_encoding_to_string(_video_encoding));
+ 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());
+ }
+ 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()));
}
- 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, "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;
}
@@ -503,7 +522,13 @@ Film::read_metadata (optional<boost::filesystem::path> path)
{
if (!path) {
if (dcp::filesystem::exists(file("metadata")) && !dcp::filesystem::exists(file(metadata_file))) {
- throw runtime_error (_("This film was created with an older version of DCP-o-matic, and unfortunately it cannot be loaded into this version. You will need to create a new Film, re-add your content and set it up again. Sorry!"));
+ throw runtime_error(
+ variant::insert_dcpomatic(
+ _("This film was created with an older version of %1, and unfortunately it cannot "
+ "be loaded into this version. You will need to create a new Film, re-add your "
+ "content and set it up again. Sorry!")
+ )
+ );
}
path = file (metadata_file);
@@ -518,7 +543,7 @@ Film::read_metadata (optional<boost::filesystem::path> path)
_state_version = f.number_child<int> ("Version");
if (_state_version > current_state_version) {
- throw runtime_error (_("This film was created with a newer version of DCP-o-matic, and it cannot be loaded into this version. Sorry!"));
+ throw runtime_error(variant::insert_dcpomatic(_("This film was created with a newer version of %1, and it cannot be loaded into this version. Sorry!")));
} else if (_state_version < current_state_version) {
/* This is an older version; save a copy (if we haven't already) */
auto const older = path->parent_path() / String::compose("metadata.%1.xml", _state_version);
@@ -558,7 +583,12 @@ Film::read_metadata (optional<boost::filesystem::path> path)
}
_resolution = string_to_resolution (f.string_child ("Resolution"));
- _j2k_bandwidth = f.number_child<int> ("J2KBandwidth");
+ if (auto j2k = f.optional_number_child<int>("J2KBandwidth")) {
+ _video_bit_rate[VideoEncoding::JPEG2000] = *j2k;
+ } else {
+ _video_bit_rate[VideoEncoding::JPEG2000] = f.number_child<int64_t>("J2KVideoBitRate");
+ }
+ _video_bit_rate[VideoEncoding::MPEG2] = f.optional_number_child<int64_t>("MPEG2VideoBitRate").get_value_or(Config::instance()->default_video_bit_rate(VideoEncoding::MPEG2));
_video_frame_rate = f.number_child<int> ("VideoFrameRate");
_audio_frame_rate = f.optional_number_child<int>("AudioFrameRate").get_value_or(48000);
_encrypted = f.bool_child ("Encrypted");
@@ -580,6 +610,9 @@ Film::read_metadata (optional<boost::filesystem::path> path)
_three_d = f.bool_child ("ThreeD");
_interop = f.bool_child ("Interop");
+ if (auto encoding = f.optional_string_child("VideoEncoding")) {
+ _video_encoding = video_encoding_from_string(*encoding);
+ }
_limit_to_smpte_bv20 = f.optional_bool_child("LimitToSMPTEBv20").get_value_or(false);
_key = dcp::Key (f.string_child ("Key"));
_context_id = f.optional_string_child("ContextID").get_value_or (dcp::make_uuid ());
@@ -599,11 +632,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")) {
@@ -1151,10 +1191,10 @@ Film::set_resolution (Resolution r, bool explicit_user)
void
-Film::set_j2k_bandwidth (int b)
+Film::set_video_bit_rate(VideoEncoding encoding, int64_t bit_rate)
{
- FilmChangeSignaller ch(this, FilmProperty::J2K_BANDWIDTH);
- _j2k_bandwidth = b;
+ FilmChangeSignaller ch(this, FilmProperty::VIDEO_BIT_RATE);
+ _video_bit_rate[encoding] = bit_rate;
}
/** @param f New frame rate.
@@ -1198,6 +1238,15 @@ Film::set_interop (bool i)
void
+Film::set_video_encoding(VideoEncoding encoding)
+{
+ FilmChangeSignaller ch(this, FilmProperty::VIDEO_ENCODING);
+ _video_encoding = encoding;
+ check_settings_consistency();
+}
+
+
+void
Film::set_limit_to_smpte_bv20(bool limit)
{
FilmChangeSignaller ch(this, FilmProperty::LIMIT_TO_SMPTE_BV20);
@@ -1228,6 +1277,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)
{
@@ -1561,7 +1620,7 @@ Film::check_settings_consistency ()
} else if (!atmos_rate && rate != video_frame_rate()) {
atmos_rate = rate;
set_video_frame_rate (rate, false);
- Message (_("DCP-o-matic had to change your settings so that the film's frame rate is the same as that of your Atmos content."));
+ Message(variant::insert_dcpomatic(_("%1 had to change your settings so that the film's frame rate is the same as that of your Atmos content.")));
}
}
}
@@ -1593,7 +1652,41 @@ 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."));
+ Message(variant::insert_dcpomatic(_("%1 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(variant::insert_dcpomatic(_("%1 had to remove some of your custom reel boundaries as they no longer lie within the film.")));
+ } else {
+ Message(variant::insert_dcpomatic(_("%1 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);
+ }
+ }
+
+ auto const hd = Ratio::from_id("178");
+
+ if (video_encoding() == VideoEncoding::MPEG2) {
+ if (container() != hd || resolution() == Resolution::FOUR_K) {
+ set_container(hd);
+ set_resolution(Resolution::TWO_K);
+ Message(_("DCP-o-matic had to set your container to 1920x1080 as it's the only one that can be used with MPEG2 encoding."));
+ }
+ if (three_d()) {
+ set_three_d(false);
+ Message(_("DCP-o-matic had to set your film to 2D as 3D is not yet supported with MPEG2 encoding."));
+ }
+ } else if (container() == hd && !Config::instance()->allow_any_container()) {
+ set_container(Ratio::from_id("185"));
+ Message(_("DCP-o-matic set your container to DCI Flat as it was previously 1920x1080 and that is not a standard ratio with JPEG2000 encoding."));
}
}
@@ -1720,7 +1813,7 @@ Film::make_kdm(boost::filesystem::path cpl_file, dcp::LocalTime from, dcp::Local
uint64_t
Film::required_disk_space () const
{
- return _playlist->required_disk_space (shared_from_this(), j2k_bandwidth(), audio_channels(), audio_frame_rate());
+ return _playlist->required_disk_space (shared_from_this(), video_bit_rate(video_encoding()), audio_channels(), audio_frame_rate());
}
/** This method checks the disk that the Film is on and tries to decide whether or not
@@ -1731,30 +1824,11 @@ Film::required_disk_space () const
* Note: the decision made by this method isn't, of course, 100% reliable.
*/
bool
-Film::should_be_enough_disk_space (double& required, double& available, bool& can_hard_link) const
-{
- /* Create a test file and see if we can hard-link it */
- boost::filesystem::path test = internal_video_asset_dir() / "test";
- boost::filesystem::path test2 = internal_video_asset_dir() / "test2";
- can_hard_link = true;
- dcp::File f(test, "w");
- if (f) {
- f.close();
- boost::system::error_code ec;
- dcp::filesystem::create_hard_link(test, test2, ec);
- if (ec) {
- can_hard_link = false;
- }
- dcp::filesystem::remove(test);
- dcp::filesystem::remove(test2);
- }
-
- auto s = dcp::filesystem::space(internal_video_asset_dir());
- required = double (required_disk_space ()) / 1073741824.0f;
- if (!can_hard_link) {
- required *= 2;
- }
- available = double (s.available) / 1073741824.0f;
+Film::should_be_enough_disk_space(double& required, double& available) const
+{
+ DCPOMATIC_ASSERT(directory());
+ required = required_disk_space() / 1073741824.0f;
+ available = dcp::filesystem::space(*directory()).available / 1073741824.0f;
return (available - required) > 1;
}
@@ -1799,15 +1873,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 +1907,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 +1915,8 @@ Film::reels () const
}
}
- if (!p.empty()) {
- p.back().to = split_points.back();
+ if (!periods.empty()) {
+ periods.back().to = split_points.back();
}
break;
}
@@ -1851,19 +1926,32 @@ Film::reels () const
/* Integer-divide reel length by the size of one frame to give the number of frames per reel,
* making sure we don't go less than 1s long.
*/
- Frame const reel_in_frames = max(_reel_length / ((j2k_bandwidth() / video_frame_rate()) / 8), static_cast<Frame>(video_frame_rate()));
+ Frame const reel_in_frames = max(_reel_length / ((video_bit_rate(video_encoding()) / 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.
*/
@@ -1874,15 +1962,21 @@ Film::content_summary (DCPTimePeriod period) const
}
void
-Film::use_template (string name)
+Film::use_template(optional<string> name)
{
- _template_film.reset (new Film (optional<boost::filesystem::path>()));
- _template_film->read_metadata (Config::instance()->template_read_path(name));
+ _template_film = std::make_shared<Film>(optional<boost::filesystem::path>());
+ if (name) {
+ _template_film->read_metadata(Config::instance()->template_read_path(*name));
+ } else {
+ _template_film->read_metadata(Config::instance()->default_template_read_path());
+ }
_use_isdcf_name = _template_film->_use_isdcf_name;
_dcp_content_type = _template_film->_dcp_content_type;
_container = _template_film->_container;
_resolution = _template_film->_resolution;
- _j2k_bandwidth = _template_film->_j2k_bandwidth;
+ for (auto encoding: { VideoEncoding::JPEG2000, VideoEncoding::MPEG2 }) {
+ _video_bit_rate[encoding] = _template_film->_video_bit_rate[encoding];
+ }
_video_frame_rate = _template_film->_video_frame_rate;
_encrypted = _template_film->_encrypted;
_audio_channels = _template_film->_audio_channels;
@@ -1892,6 +1986,12 @@ Film::use_template (string name)
_audio_processor = _template_film->_audio_processor;
_reel_type = _template_film->_reel_type;
_reel_length = _template_film->_reel_length;
+ _chain = _template_film->_chain;
+ _distributor = _template_film->_distributor;
+ _facility = _template_film->_facility;
+ _studio = _template_film->_studio;
+ _territory_type = _template_film->_territory_type;
+ _release_territory = _template_film->_release_territory;
}
pair<double, double>
@@ -2230,3 +2330,95 @@ 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 (...) {}
+}
+
+
+vector<RememberedAsset>
+Film::read_remembered_assets() const
+{
+ vector<RememberedAsset> assets;
+
+ try {
+ cxml::Document xml("Assets");
+ xml.read_file(dcp::filesystem::fix_long_path(file(assets_file)));
+ for (auto node: xml.node_children("Asset")) {
+ assets.push_back(RememberedAsset(node));
+ }
+ } catch (std::exception& e) {
+ LOG_ERROR("Could not read assets file %1 (%2)", file(assets_file), e.what());
+ } catch (...) {
+ LOG_ERROR("Could not read assets file %1", file(assets_file));
+ }
+
+ return assets;
+}
+
+
+void
+Film::write_remembered_assets(vector<RememberedAsset> const& assets) const
+{
+ auto doc = make_shared<xmlpp::Document>();
+ auto root = doc->create_root_node("Assets");
+
+ for (auto asset: assets) {
+ asset.as_xml(cxml::add_child(root, "Asset"));
+ }
+
+ try {
+ doc->write_to_file_formatted(dcp::filesystem::fix_long_path(file(assets_file)).string());
+ } catch (std::exception& e) {
+ LOG_ERROR("Could not write assets file %1 (%2)", file(assets_file), e.what());
+ } catch (...) {
+ LOG_ERROR("Could not write assets file %1", file(assets_file));
+ }
+}
+
diff --git a/src/lib/film.h b/src/lib/film.h
index 43a41ad45..d71435566 100644
--- a/src/lib/film.h
+++ b/src/lib/film.h
@@ -32,15 +32,18 @@
#include "change_signaller.h"
#include "dcp_text_track.h"
#include "dcpomatic_time.h"
+#include "enum_indexed_vector.h"
#include "film_property.h"
#include "frame_rate_change.h"
#include "named_channel.h"
+#include "remembered_asset.h"
#include "resolution.h"
#include "signaller.h"
#include "territory_type.h"
#include "transcode_job.h"
#include "types.h"
#include "util.h"
+#include "video_encoding.h"
#include <dcp/encrypted_kdm.h>
#include <dcp/file.h>
#include <dcp/key.h>
@@ -116,11 +119,10 @@ public:
std::shared_ptr<InfoFileHandle> info_file_handle (dcpomatic::DCPTimePeriod period, bool read) const;
boost::filesystem::path j2c_path (int, Frame, Eyes, bool) const;
- boost::filesystem::path internal_video_asset_dir () const;
- boost::filesystem::path internal_video_asset_filename (dcpomatic::DCPTimePeriod p) const;
boost::filesystem::path audio_analysis_path (std::shared_ptr<const Playlist>) const;
boost::filesystem::path subtitle_analysis_path (std::shared_ptr<const Content>) const;
+ boost::filesystem::path assets_path() const;
void send_dcp_to_tms ();
@@ -134,7 +136,7 @@ public:
boost::filesystem::path file (boost::filesystem::path f) const;
boost::filesystem::path dir (boost::filesystem::path d, bool create = true) const;
- void use_template (std::string name);
+ void use_template(boost::optional<std::string> name);
std::list<std::string> read_metadata (boost::optional<boost::filesystem::path> path = boost::optional<boost::filesystem::path> ());
void write_metadata ();
void write_metadata (boost::filesystem::path path) const;
@@ -160,7 +162,7 @@ public:
std::list<DCPTextTrack> closed_caption_tracks () const;
uint64_t required_disk_space () const;
- bool should_be_enough_disk_space (double& required, double& available, bool& can_hard_link) const;
+ bool should_be_enough_disk_space(double& required, double& available) const;
bool has_sign_language_video_channel () const;
@@ -186,7 +188,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 {
@@ -250,8 +252,8 @@ public:
return _key;
}
- int j2k_bandwidth () const {
- return _j2k_bandwidth;
+ int video_bit_rate(VideoEncoding encoding) const {
+ return _video_bit_rate[encoding];
}
/** @return The frame rate of the DCP */
@@ -275,6 +277,10 @@ public:
return _interop;
}
+ VideoEncoding video_encoding() const {
+ return _video_encoding;
+ }
+
bool limit_to_smpte_bv20() const {
return _limit_to_smpte_bv20;
}
@@ -291,6 +297,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;
}
@@ -397,17 +407,19 @@ public:
void set_container (Ratio const *, bool user_explicit = true);
void set_resolution (Resolution, bool user_explicit = true);
void set_encrypted (bool);
- void set_j2k_bandwidth (int);
+ void set_video_bit_rate(VideoEncoding encoding, int64_t);
void set_video_frame_rate (int rate, bool user_explicit = false);
void set_audio_channels (int);
void set_three_d (bool);
void set_isdcf_date_today ();
void set_sequence (bool);
void set_interop (bool);
+ void set_video_encoding(VideoEncoding encoding);
void set_limit_to_smpte_bv20(bool);
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 +446,14 @@ 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();
+
+ std::vector<RememberedAsset> read_remembered_assets() const;
+ void write_remembered_assets(std::vector<RememberedAsset> const& assets) const;
+ std::string video_identifier() const;
+
/** Emitted when some property has of the Film is about to change or has changed */
mutable boost::signals2::signal<void (ChangeType, FilmProperty)> Change;
@@ -467,7 +487,6 @@ private:
void signal_change (ChangeType, FilmProperty);
void signal_change (ChangeType, int);
- std::string video_identifier () const;
void playlist_change (ChangeType);
void playlist_order_changed ();
void playlist_content_change (ChangeType type, std::weak_ptr<Content>, int, bool frequent);
@@ -477,6 +496,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;
@@ -505,8 +525,8 @@ private:
* re-start picture MXF encodes.
*/
std::string _context_id;
- /** bandwidth for J2K files in bits per second */
- int _j2k_bandwidth;
+ /** bit rate for encoding video using in bits per second */
+ EnumIndexedVector<int64_t, VideoEncoding> _video_bit_rate;
/** Frames per second to run our DCP at */
int _video_frame_rate;
/** The date that we should use in a ISDCF name */
@@ -519,11 +539,14 @@ private:
bool _three_d;
bool _sequence;
bool _interop;
+ VideoEncoding _video_encoding;
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 +585,8 @@ private:
*/
bool _tolerant;
+ std::map<std::string, std::string> _ui_state;
+
mutable boost::mutex _info_file_mutex;
boost::signals2::scoped_connection _playlist_change_connection;
diff --git a/src/lib/encoder.cc b/src/lib/film_encoder.cc
index 5268d8620..05b911daf 100644
--- a/src/lib/encoder.cc
+++ b/src/lib/film_encoder.cc
@@ -19,7 +19,7 @@
*/
-/** @file src/encoder.cc
+/** @file src/film_encoder.cc
* @brief A class which takes a Film and some Options, then uses those to encode the film
* into some output format.
*
@@ -28,17 +28,17 @@
*/
-#include "encoder.h"
+#include "film_encoder.h"
#include "player.h"
#include "i18n.h"
-/** Construct an encoder.
+/** Construct a FilmEncoder.
* @param film Film that we are encoding.
* @param job Job that this encoder is being used in.
*/
-Encoder::Encoder (std::shared_ptr<const Film> film, std::weak_ptr<Job> job)
+FilmEncoder::FilmEncoder(std::shared_ptr<const Film> film, std::weak_ptr<Job> job)
: _film (film)
, _job (job)
, _player(film, Image::Alignment::PADDED)
diff --git a/src/lib/encoder.h b/src/lib/film_encoder.h
index 9b67720d3..ed7626c68 100644
--- a/src/lib/encoder.h
+++ b/src/lib/film_encoder.h
@@ -19,8 +19,8 @@
*/
-#ifndef DCPOMATIC_ENCODER_H
-#define DCPOMATIC_ENCODER_H
+#ifndef DCPOMATIC_FILM_ENCODER_H
+#define DCPOMATIC_FILM_ENCODER_H
#include "player.h"
@@ -29,24 +29,23 @@
class Film;
-class Encoder;
class Player;
class Job;
class PlayerVideo;
class AudioBuffers;
-/** @class Encoder
+/** @class FilmEncoder
* @brief Parent class for something that can encode a film into some format
*/
-class Encoder
+class FilmEncoder
{
public:
- Encoder (std::shared_ptr<const Film> film, std::weak_ptr<Job> job);
- virtual ~Encoder () {}
+ FilmEncoder(std::shared_ptr<const Film> film, std::weak_ptr<Job> job);
+ virtual ~FilmEncoder() {}
- Encoder (Encoder const&) = delete;
- Encoder& operator= (Encoder const&) = delete;
+ FilmEncoder(FilmEncoder const&) = delete;
+ FilmEncoder& operator=(FilmEncoder const&) = delete;
virtual void go () = 0;
@@ -58,6 +57,8 @@ public:
/** @return the number of frames that are done */
virtual Frame frames_done () const = 0;
virtual bool finishing () const = 0;
+ virtual void pause() {}
+ virtual void resume() {}
protected:
std::shared_ptr<const Film> _film;
diff --git a/src/lib/film_property.h b/src/lib/film_property.h
index c23297965..ebda0e807 100644
--- a/src/lib/film_property.h
+++ b/src/lib/film_property.h
@@ -39,7 +39,7 @@ enum class FilmProperty {
CONTAINER,
RESOLUTION,
ENCRYPTED,
- J2K_BANDWIDTH,
+ VIDEO_BIT_RATE,
VIDEO_FRAME_RATE,
AUDIO_FRAME_RATE,
AUDIO_CHANNELS,
@@ -47,10 +47,12 @@ enum class FilmProperty {
THREE_D,
SEQUENCE,
INTEROP,
+ VIDEO_ENCODING,
LIMIT_TO_SMPTE_BV20,
AUDIO_PROCESSOR,
REEL_TYPE,
REEL_LENGTH,
+ CUSTOM_REEL_BOUNDARIES,
REENCODE_J2K,
MARKERS,
RATINGS,
diff --git a/src/lib/filter.cc b/src/lib/filter.cc
index 9a14795ec..465b5991a 100644
--- a/src/lib/filter.cc
+++ b/src/lib/filter.cc
@@ -37,7 +37,8 @@ LIBDCP_ENABLE_WARNINGS
#include "i18n.h"
-using namespace std;
+using std::string;
+using std::vector;
using boost::optional;
diff --git a/src/lib/font.cc b/src/lib/font.cc
index 955a2ad1c..246d3eeb8 100644
--- a/src/lib/font.cc
+++ b/src/lib/font.cc
@@ -63,11 +63,11 @@ Font& Font::operator=(Font const& other)
void
-Font::as_xml (xmlpp::Node* node)
+Font::as_xml(xmlpp::Element* element)
{
- node->add_child("Id")->add_child_text(_id);
+ cxml::add_text_child(element, "Id", _id);
if (_content.file) {
- node->add_child("File")->add_child_text(_content.file->string());
+ cxml::add_text_child(element, "File", _content.file->string());
}
}
diff --git a/src/lib/font.h b/src/lib/font.h
index 12a14aba4..092b682ba 100644
--- a/src/lib/font.h
+++ b/src/lib/font.h
@@ -57,7 +57,7 @@ public:
Font(Font const& other);
Font& operator=(Font const& other);
- void as_xml (xmlpp::Node* node);
+ void as_xml(xmlpp::Element* element);
std::string id () const {
return _id;
diff --git a/src/lib/font_id_allocator.cc b/src/lib/font_id_allocator.cc
index 112dd262b..76b52e730 100644
--- a/src/lib/font_id_allocator.cc
+++ b/src/lib/font_id_allocator.cc
@@ -76,37 +76,23 @@ FontIDAllocator::add_font(int reel_index, string asset_id, string font_id)
if (!_default_font) {
_default_font = font;
}
- _map[font] = 0;
+ _map[font] = {};
}
void
FontIDAllocator::allocate()
{
- /* We'll first try adding <reel>_ to the start of the font ID, but if a reel has multiple
- * identical font IDs we will need to use some number that is not a reel ID. Find the
- * first such number (1 higher than the highest reel index)
- */
- auto next_unused = std::max_element(
- _map.begin(),
- _map.end(),
- [] (std::pair<Font, int> const& a, std::pair<Font, int> const& b) {
- return a.first.reel_index < b.first.reel_index;
- })->first.reel_index + 1;
-
std::set<string> used_ids;
for (auto& font: _map) {
- auto const proposed = String::compose("%1_%2", font.first.reel_index, font.first.font_id);
- if (used_ids.find(proposed) != used_ids.end()) {
- /* This ID was already used; we need to disambiguate it. Do so by using
- * one of our unused prefixes.
- */
- font.second = next_unused++;
- } else {
- /* This ID was not yet used */
- font.second = font.first.reel_index;
+ auto proposed = font.first.font_id;
+ int prefix = 0;
+ while (used_ids.find(proposed) != used_ids.end()) {
+ proposed = String::compose("%1_%2", prefix++, font.first.font_id);
+ DCPOMATIC_ASSERT(prefix < 128);
}
+ font.second = proposed;
used_ids.insert(proposed);
}
}
@@ -117,7 +103,7 @@ FontIDAllocator::font_id(int reel_index, string asset_id, string font_id) const
{
auto iter = _map.find(Font(reel_index, asset_id, font_id));
DCPOMATIC_ASSERT(iter != _map.end());
- return String::compose("%1_%2", iter->second, font_id);
+ return iter->second;
}
diff --git a/src/lib/font_id_allocator.h b/src/lib/font_id_allocator.h
index fe4b9ef07..6737907c1 100644
--- a/src/lib/font_id_allocator.h
+++ b/src/lib/font_id_allocator.h
@@ -101,7 +101,7 @@ private:
std::string font_id;
};
- std::map<Font, int> _map;
+ std::map<Font, std::string> _map;
boost::optional<Font> _default_font;
};
diff --git a/src/lib/frame_info.cc b/src/lib/frame_info.cc
new file mode 100644
index 000000000..f348bca6a
--- /dev/null
+++ b/src/lib/frame_info.cc
@@ -0,0 +1,84 @@
+/*
+ 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 "frame_info.h"
+#include <string>
+
+
+using std::shared_ptr;
+using std::string;
+
+
+J2KFrameInfo::J2KFrameInfo(uint64_t offset_, uint64_t size_, string hash_)
+ : dcp::J2KFrameInfo(offset_, size_, hash_)
+{
+
+}
+
+
+J2KFrameInfo::J2KFrameInfo(dcp::J2KFrameInfo const& info)
+ : dcp::J2KFrameInfo(info)
+{
+
+}
+
+
+J2KFrameInfo::J2KFrameInfo(shared_ptr<InfoFileHandle> info_file, Frame frame, Eyes eyes)
+{
+ info_file->get().seek(position(frame, eyes), SEEK_SET);
+ info_file->get().checked_read(&offset, sizeof(offset));
+ info_file->get().checked_read(&size, sizeof(size));
+
+ char hash_buffer[33];
+ info_file->get().checked_read(hash_buffer, 32);
+ hash_buffer[32] = '\0';
+ hash = hash_buffer;
+}
+
+
+long
+J2KFrameInfo::position(Frame frame, Eyes eyes) const
+{
+ switch (eyes) {
+ case Eyes::BOTH:
+ return frame * _size_on_disk;
+ case Eyes::LEFT:
+ return frame * _size_on_disk * 2;
+ case Eyes::RIGHT:
+ return frame * _size_on_disk * 2 + _size_on_disk;
+ default:
+ DCPOMATIC_ASSERT(false);
+ }
+
+ DCPOMATIC_ASSERT(false);
+}
+
+
+/** @param frame reel-relative frame */
+void
+J2KFrameInfo::write(shared_ptr<InfoFileHandle> info_file, Frame frame, Eyes eyes) const
+{
+ info_file->get().seek(position(frame, eyes), SEEK_SET);
+ info_file->get().checked_write(&offset, sizeof(offset));
+ info_file->get().checked_write(&size, sizeof(size));
+ info_file->get().checked_write(hash.c_str(), hash.size());
+}
+
diff --git a/src/lib/frame_info.h b/src/lib/frame_info.h
new file mode 100644
index 000000000..a5a22bd9e
--- /dev/null
+++ b/src/lib/frame_info.h
@@ -0,0 +1,45 @@
+/*
+ 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 "film.h"
+#include <dcp/frame_info.h>
+#include <memory>
+
+
+class J2KFrameInfo : public dcp::J2KFrameInfo
+{
+public:
+ J2KFrameInfo(dcp::J2KFrameInfo const& info);
+ J2KFrameInfo(uint64_t offset_, uint64_t size_, std::string hash_);
+ J2KFrameInfo(std::shared_ptr<InfoFileHandle> info_file, Frame frame, Eyes eyes);
+
+ void write(std::shared_ptr<InfoFileHandle> info_file, Frame frame, Eyes eyes) const;
+
+ static int size_on_disk() {
+ return _size_on_disk;
+ }
+
+private:
+ long position(Frame frame, Eyes eyes) const;
+
+ static constexpr auto _size_on_disk = 32 + sizeof(dcp::J2KFrameInfo::offset) + sizeof(dcp::J2KFrameInfo::size);
+};
+
diff --git a/src/lib/grok/context.h b/src/lib/grok/context.h
new file mode 100644
index 000000000..81622ad9d
--- /dev/null
+++ b/src/lib/grok/context.h
@@ -0,0 +1,291 @@
+/*
+ Copyright (C) 2023 Grok Image Compression Inc.
+
+ This file is part of DCP-o-matic.
+
+ DCP-o-matic is free software; you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation; either version 2 of the License, or
+ (at your option) any later version.
+
+ DCP-o-matic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with DCP-o-matic. If not, see <http://www.gnu.org/licenses/>.
+
+*/
+
+#pragma once
+
+
+#include "../config.h"
+#include "../dcp_video.h"
+#include "../film.h"
+#include "../log.h"
+#include "../dcpomatic_log.h"
+#include "../writer.h"
+#include "messenger.h"
+#include <dcp/array_data.h>
+#include <boost/filesystem.hpp>
+
+
+static std::mutex launchMutex;
+
+namespace grk_plugin
+{
+
+struct GrokLogger : public MessengerLogger {
+ explicit GrokLogger(const std::string &preamble) : MessengerLogger(preamble)
+ {}
+ virtual ~GrokLogger() = default;
+ void info(const char* fmt, ...) override{
+ va_list arg;
+ va_start(arg, fmt);
+ dcpomatic_log->log(preamble_ + log_message(fmt, arg),LogEntry::TYPE_GENERAL);
+ va_end(arg);
+ }
+ void warn(const char* fmt, ...) override{
+ va_list arg;
+ va_start(arg, fmt);
+ dcpomatic_log->log(preamble_ + log_message(fmt, arg),LogEntry::TYPE_WARNING);
+ va_end(arg);
+ }
+ void error(const char* fmt, ...) override{
+ va_list arg;
+ va_start(arg, fmt);
+ dcpomatic_log->log(preamble_ + log_message(fmt, arg),LogEntry::TYPE_ERROR);
+ va_end(arg);
+ }
+};
+
+struct FrameProxy {
+ FrameProxy(int index, Eyes eyes, DCPVideo dcpv) : index_(index), eyes_(eyes), vf(dcpv)
+ {}
+ int index() const {
+ return index_;
+ }
+ Eyes eyes(void) const {
+ return eyes_;
+ }
+ int index_;
+ Eyes eyes_;
+ DCPVideo vf;
+};
+
+struct DcpomaticContext
+{
+ DcpomaticContext(
+ std::shared_ptr<const Film> film_,
+ Writer& writer_,
+ EventHistory& history_,
+ boost::filesystem::path const& location_
+ )
+ : film(film_)
+ , writer(writer_)
+ , history(history_)
+ , location(location_)
+ {
+
+ }
+
+ void set_dimensions(uint32_t w, uint32_t h)
+ {
+ width = w;
+ height = h;
+ }
+
+ std::shared_ptr<const Film> film;
+ Writer& writer;
+ EventHistory& history;
+ boost::filesystem::path location;
+ uint32_t width = 0;
+ uint32_t height = 0;
+};
+
+
+class GrokContext
+{
+public:
+ explicit GrokContext(DcpomaticContext* dcpomatic_context)
+ : _dcpomatic_context(dcpomatic_context)
+ {
+ auto grok = Config::instance()->grok().get_value_or({});
+ if (!grok.enable) {
+ return;
+ }
+
+ boost::filesystem::path folder(_dcpomatic_context->location);
+ boost::filesystem::path binary_path = folder / "grk_compress";
+ if (!boost::filesystem::exists(binary_path)) {
+ getMessengerLogger()->error(
+ "Invalid binary location %s", _dcpomatic_context->location.c_str()
+ );
+ return;
+ }
+
+ auto proc = [this](const std::string& str) {
+ try {
+ Msg msg(str);
+ auto tag = msg.next();
+ if (tag == GRK_MSGR_BATCH_SUBMIT_COMPRESSED) {
+ auto clientFrameId = msg.nextUint();
+ msg.nextUint(); // compressed frame ID
+ auto compressedFrameLength = msg.nextUint();
+ auto processor = [this](FrameProxy srcFrame, uint8_t* compressed, uint32_t compressedFrameLength) {
+ auto compressed_data = std::make_shared<dcp::ArrayData>(compressed, compressedFrameLength);
+ _dcpomatic_context->writer.write(compressed_data, srcFrame.index(), srcFrame.eyes());
+ frame_done ();
+ };
+
+ int const minimum_size = 16384;
+
+ bool needsRecompression = compressedFrameLength < minimum_size;
+ _messenger->processCompressed(str, processor, needsRecompression);
+
+ if (needsRecompression) {
+ auto fp = _messenger->retrieve(clientFrameId);
+ if (!fp) {
+ return;
+ }
+
+ auto encoded = std::make_shared<dcp::ArrayData>(fp->vf.encode_locally());
+ _dcpomatic_context->writer.write(encoded, fp->vf.index(), fp->vf.eyes());
+ frame_done ();
+ }
+ }
+ } catch (std::exception& ex) {
+ getMessengerLogger()->error("%s",ex.what());
+ }
+ };
+
+ auto clientInit = MessengerInit(
+ clientToGrokMessageBuf,
+ clientSentSynch,
+ grokReceiveReadySynch,
+ grokToClientMessageBuf,
+ grokSentSynch,
+ clientReceiveReadySynch,
+ proc,
+ std::thread::hardware_concurrency()
+ );
+
+ _messenger = new ScheduledMessenger<FrameProxy>(clientInit);
+ }
+
+ ~GrokContext()
+ {
+ shutdown();
+ }
+
+ bool launch(DCPVideo dcpv, int device)
+ {
+ namespace fs = boost::filesystem;
+
+ if (!_messenger) {
+ return false;
+ }
+ if (_launched) {
+ return true;
+ }
+ if (_launch_failed) {
+ return false;
+ }
+
+ std::unique_lock<std::mutex> lk_global(launchMutex);
+
+ if (!_messenger) {
+ return false;
+ }
+ if (_launched) {
+ return true;
+ }
+ if (_launch_failed) {
+ return false;
+ }
+
+ if (MessengerInit::firstLaunch(true)) {
+
+ if (!fs::exists(_dcpomatic_context->location) || !fs::is_directory(_dcpomatic_context->location)) {
+ getMessengerLogger()->error("Invalid directory %s", _dcpomatic_context->location.c_str());
+ return false;
+ }
+
+ auto s = dcpv.get_size();
+ _dcpomatic_context->set_dimensions(s.width, s.height);
+ auto grok = Config::instance()->grok().get_value_or({});
+ if (!_messenger->launchGrok(
+ _dcpomatic_context->location,
+ _dcpomatic_context->width,
+ _dcpomatic_context->width,
+ _dcpomatic_context->height,
+ 3,
+ 12,
+ device,
+ _dcpomatic_context->film->resolution() == Resolution::FOUR_K,
+ _dcpomatic_context->film->video_frame_rate(),
+ _dcpomatic_context->film->video_bit_rate(VideoEncoding::JPEG2000),
+ grok.licence_server,
+ grok.licence_port,
+ grok.licence)) {
+ _launch_failed = true;
+ return false;
+ }
+ }
+
+ _launched = _messenger->waitForClientInit();
+ _launch_failed = _launched;
+
+ return _launched;
+ }
+
+ bool scheduleCompress(DCPVideo const& vf)
+ {
+ if (!_messenger) {
+ return false;
+ }
+
+ auto fp = FrameProxy(vf.index(), vf.eyes(), vf);
+ auto cvt = [this, &fp](BufferSrc src) {
+ fp.vf.convert_to_xyz((uint16_t*)src.framePtr_);
+ };
+
+ return _messenger->scheduleCompress(fp, cvt);
+ }
+
+ void shutdown()
+ {
+ if (!_messenger) {
+ return;
+ }
+
+ std::unique_lock<std::mutex> lk_global(launchMutex);
+
+ if (!_messenger) {
+ return;
+ }
+
+ if (_launched) {
+ _messenger->shutdown();
+ }
+
+ delete _messenger;
+ _messenger = nullptr;
+ }
+
+ void frame_done()
+ {
+ _dcpomatic_context->history.event();
+ }
+
+private:
+ DcpomaticContext* _dcpomatic_context;
+ ScheduledMessenger<FrameProxy>* _messenger = nullptr;
+ bool _launched = false;
+ bool _launch_failed = false;
+};
+
+}
+
diff --git a/src/lib/grok/messenger.h b/src/lib/grok/messenger.h
new file mode 100644
index 000000000..eb2fe9560
--- /dev/null
+++ b/src/lib/grok/messenger.h
@@ -0,0 +1,906 @@
+/*
+ Copyright (C) 2023 Grok Image Compression Inc.
+
+ This file is part of DCP-o-matic.
+
+ DCP-o-matic is free software; you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation; either version 2 of the License, or
+ (at your option) any later version.
+
+ DCP-o-matic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with DCP-o-matic. If not, see <http://www.gnu.org/licenses/>.
+
+*/
+#pragma once
+
+#include <iostream>
+#include <string>
+#include <cstring>
+#include <atomic>
+#include <functional>
+#include <sstream>
+#include <future>
+#include <map>
+#include <thread>
+#include <mutex>
+#include <condition_variable>
+#include <queue>
+#include <cassert>
+#include <cstdarg>
+
+#ifdef _WIN32
+#include <windows.h>
+#include <direct.h>
+#include <tlhelp32.h>
+#pragma warning(disable : 4100)
+#else
+#include <unistd.h>
+#include <fcntl.h>
+#include <sys/mman.h>
+#include <semaphore.h>
+#include <signal.h>
+#endif
+
+namespace grk_plugin
+{
+static std::string grokToClientMessageBuf = "Global\\grok_to_client_message";
+static std::string grokSentSynch = "Global\\grok_sent";
+static std::string clientReceiveReadySynch = "Global\\client_receive_ready";
+static std::string clientToGrokMessageBuf = "Global\\client_to_grok_message";
+static std::string clientSentSynch = "Global\\client_sent";
+static std::string grokReceiveReadySynch = "Global\\grok_receive_ready";
+static std::string grokUncompressedBuf = "Global\\grok_uncompressed_buf";
+static std::string grokCompressedBuf = "Global\\grok_compressed_buf";
+static const std::string GRK_MSGR_BATCH_IMAGE = "GRK_MSGR_BATCH_IMAGE";
+static const std::string GRK_MSGR_BATCH_COMPRESS_INIT = "GRK_MSGR_BATCH_COMPRESS_INIT";
+static const std::string GRK_MSGR_BATCH_SUBMIT_UNCOMPRESSED = "GRK_MSGR_BATCH_SUBMIT_UNCOMPRESSED";
+static const std::string GRK_MSGR_BATCH_PROCESSED_UNCOMPRESSED =
+ "GRK_MSGR_BATCH_PROCESSED_UNCOMPRESSED";
+static const std::string GRK_MSGR_BATCH_SUBMIT_COMPRESSED = "GRK_MSGR_BATCH_SUBMIT_COMPRESSED";
+static const std::string GRK_MSGR_BATCH_PROCESSSED_COMPRESSED =
+ "GRK_MSGR_BATCH_PROCESSSED_COMPRESSED";
+static const std::string GRK_MSGR_BATCH_SHUTDOWN = "GRK_MSGR_BATCH_SHUTDOWN";
+static const std::string GRK_MSGR_BATCH_FLUSH = "GRK_MSGR_BATCH_FLUSH";
+static const size_t messageBufferLen = 256;
+struct IMessengerLogger
+{
+ virtual ~IMessengerLogger(void) = default;
+ virtual void info(const char* fmt, ...) = 0;
+ virtual void warn(const char* fmt, ...) = 0;
+ virtual void error(const char* fmt, ...) = 0;
+
+ protected:
+ template<typename... Args>
+ std::string log_message(char const* const format, Args&... args) noexcept
+ {
+ constexpr size_t message_size = 512;
+ char message[message_size];
+
+ std::snprintf(message, message_size, format, args...);
+ return std::string(message);
+ }
+};
+struct MessengerLogger : public IMessengerLogger
+{
+ explicit MessengerLogger(const std::string &preamble) : preamble_(preamble) {}
+ virtual ~MessengerLogger() = default;
+ virtual void info(const char* fmt, ...) override
+ {
+ va_list args;
+ std::string new_fmt = preamble_ + fmt + "\n";
+ va_start(args, fmt);
+ vfprintf(stdout, new_fmt.c_str(), args);
+ va_end(args);
+ }
+ virtual void warn(const char* fmt, ...) override
+ {
+ va_list args;
+ std::string new_fmt = preamble_ + fmt + "\n";
+ va_start(args, fmt);
+ vfprintf(stdout, new_fmt.c_str(), args);
+ va_end(args);
+ }
+ virtual void error(const char* fmt, ...) override
+ {
+ va_list args;
+ std::string new_fmt = preamble_ + fmt + "\n";
+ va_start(args, fmt);
+ vfprintf(stderr, new_fmt.c_str(), args);
+ va_end(args);
+ }
+
+ protected:
+ std::string preamble_;
+};
+
+extern IMessengerLogger* sLogger;
+void setMessengerLogger(IMessengerLogger* logger);
+IMessengerLogger* getMessengerLogger(void);
+
+struct MessengerInit
+{
+ MessengerInit(const std::string &outBuf, const std::string &outSent,
+ const std::string &outReceiveReady, const std::string &inBuf,
+ const std::string &inSent,
+ const std::string &inReceiveReady,
+ std::function<void(std::string)> processor,
+ size_t numProcessingThreads)
+ : outboundMessageBuf(outBuf), outboundSentSynch(outSent),
+ outboundReceiveReadySynch(outReceiveReady), inboundMessageBuf(inBuf),
+ inboundSentSynch(inSent), inboundReceiveReadySynch(inReceiveReady), processor_(processor),
+ numProcessingThreads_(numProcessingThreads),
+ uncompressedFrameSize_(0), compressedFrameSize_(0),
+ numFrames_(0)
+ {
+ if(firstLaunch(true))
+ unlink();
+ }
+ void unlink(void)
+ {
+#ifndef _WIN32
+ shm_unlink(grokToClientMessageBuf.c_str());
+ shm_unlink(clientToGrokMessageBuf.c_str());
+#endif
+ }
+ static bool firstLaunch(bool isClient)
+ {
+ bool debugGrok = false;
+ return debugGrok != isClient;
+ }
+ std::string outboundMessageBuf;
+ std::string outboundSentSynch;
+ std::string outboundReceiveReadySynch;
+
+ std::string inboundMessageBuf;
+ std::string inboundSentSynch;
+ std::string inboundReceiveReadySynch;
+
+ std::function<void(std::string)> processor_;
+ size_t numProcessingThreads_;
+
+ size_t uncompressedFrameSize_;
+ size_t compressedFrameSize_;
+ size_t numFrames_;
+};
+
+/*************************** Synchronization *******************************/
+enum SynchDirection
+{
+ SYNCH_SENT,
+ SYNCH_RECEIVE_READY
+};
+
+typedef int grk_handle;
+struct Synch
+{
+ Synch(const std::string &sentSemName, const std::string &receiveReadySemName)
+ : sentSemName_(sentSemName), receiveReadySemName_(receiveReadySemName)
+ {
+ // unlink semaphores in case of previous crash
+ if(MessengerInit::firstLaunch(true))
+ unlink();
+ open();
+ }
+ ~Synch()
+ {
+ close();
+ if(MessengerInit::firstLaunch(true))
+ unlink();
+ }
+ void post(SynchDirection dir)
+ {
+ auto sem = (dir == SYNCH_SENT ? sentSem_ : receiveReadySem_);
+ int rc = sem_post(sem);
+ if(rc)
+ getMessengerLogger()->error("Error posting to semaphore: %s", strerror(errno));
+ }
+ void wait(SynchDirection dir)
+ {
+ auto sem = dir == SYNCH_SENT ? sentSem_ : receiveReadySem_;
+ int rc = sem_wait(sem);
+ if(rc)
+ getMessengerLogger()->error("Error waiting for semaphore: %s", strerror(errno));
+ }
+ void open(void)
+ {
+ sentSem_ = sem_open(sentSemName_.c_str(), O_CREAT, 0666, 0);
+ if(!sentSem_)
+ getMessengerLogger()->error("Error opening shared memory: %s", strerror(errno));
+ receiveReadySem_ = sem_open(receiveReadySemName_.c_str(), O_CREAT, 0666, 1);
+ if(!receiveReadySem_)
+ getMessengerLogger()->error("Error opening shared memory: %s", strerror(errno));
+ }
+ void close(void)
+ {
+ int rc = sem_close(sentSem_);
+ if(rc)
+ getMessengerLogger()->error("Error closing semaphore %s: %s", sentSemName_.c_str(),
+ strerror(errno));
+ rc = sem_close(receiveReadySem_);
+ if(rc)
+ getMessengerLogger()->error("Error closing semaphore %s: %s",
+ receiveReadySemName_.c_str(), strerror(errno));
+ }
+ void unlink(void)
+ {
+ int rc = sem_unlink(sentSemName_.c_str());
+ if(rc == -1 && errno != ENOENT)
+ getMessengerLogger()->error("Error unlinking semaphore %s: %s", sentSemName_.c_str(),
+ strerror(errno));
+ rc = sem_unlink(receiveReadySemName_.c_str());
+ if(rc == -1 && errno != ENOENT)
+ getMessengerLogger()->error("Error unlinking semaphore %s: %s",
+ receiveReadySemName_.c_str(), strerror(errno));
+ }
+ sem_t* sentSem_;
+ sem_t* receiveReadySem_;
+
+ private:
+ std::string sentSemName_;
+ std::string receiveReadySemName_;
+};
+struct SharedMemoryManager
+{
+ static bool initShm(const std::string &name, size_t len, grk_handle* shm_fd, char** buffer)
+ {
+ *shm_fd = shm_open(name.c_str(), O_CREAT | O_RDWR, 0666);
+ if(*shm_fd < 0)
+ {
+ getMessengerLogger()->error("Error opening shared memory: %s", strerror(errno));
+ return false;
+ }
+ int rc = ftruncate(*shm_fd, sizeof(char) * len);
+ if(rc)
+ {
+ getMessengerLogger()->error("Error truncating shared memory: %s", strerror(errno));
+ rc = close(*shm_fd);
+ if(rc)
+ getMessengerLogger()->error("Error closing shared memory: %s", strerror(errno));
+ rc = shm_unlink(name.c_str());
+ // 2 == No such file or directory
+ if(rc && errno != 2)
+ getMessengerLogger()->error("Error unlinking shared memory: %s", strerror(errno));
+ return false;
+ }
+ *buffer = static_cast<char*>(mmap(0, len, PROT_WRITE, MAP_SHARED, *shm_fd, 0));
+ if(!*buffer)
+ {
+ getMessengerLogger()->error("Error mapping shared memory: %s", strerror(errno));
+ rc = close(*shm_fd);
+ if(rc)
+ getMessengerLogger()->error("Error closing shared memory: %s", strerror(errno));
+ rc = shm_unlink(name.c_str());
+ // 2 == No such file or directory
+ if(rc && errno != 2)
+ getMessengerLogger()->error("Error unlinking shared memory: %s", strerror(errno));
+ }
+
+ return *buffer != nullptr;
+ }
+ static bool deinitShm(const std::string &name, size_t len, grk_handle &shm_fd, char** buffer)
+ {
+ if (!*buffer || !shm_fd)
+ return true;
+
+ int rc = munmap(*buffer, len);
+ *buffer = nullptr;
+ if(rc)
+ getMessengerLogger()->error("Error unmapping shared memory %s: %s", name.c_str(), strerror(errno));
+ rc = close(shm_fd);
+ shm_fd = 0;
+ if(rc)
+ getMessengerLogger()->error("Error closing shared memory %s: %s", name.c_str(), strerror(errno));
+ rc = shm_unlink(name.c_str());
+ // 2 == No such file or directory
+ if(rc && errno != 2)
+ fprintf(stderr,"Error unlinking shared memory %s : %s\n", name.c_str(), strerror(errno));
+
+ return true;
+ }
+};
+
+template<typename Data>
+class MessengerBlockingQueue
+{
+ public:
+ explicit MessengerBlockingQueue(size_t max) : active_(true), max_size_(max) {}
+ MessengerBlockingQueue() : MessengerBlockingQueue(UINT_MAX) {}
+ size_t size() const
+ {
+ return queue_.size();
+ }
+ // deactivate and clear queue
+ void deactivate()
+ {
+ {
+ std::lock_guard<std::mutex> lk(mutex_);
+ active_ = false;
+ while(!queue_.empty())
+ queue_.pop();
+ }
+
+ // release all waiting threads
+ can_pop_.notify_all();
+ can_push_.notify_all();
+ }
+ void activate()
+ {
+ std::lock_guard<std::mutex> lk(mutex_);
+ active_ = true;
+ }
+ bool push(Data const& value)
+ {
+ bool rc;
+ {
+ std::unique_lock<std::mutex> lk(mutex_);
+ rc = push_(value);
+ }
+ if(rc)
+ can_pop_.notify_one();
+
+ return rc;
+ }
+ bool waitAndPush(Data& value)
+ {
+ bool rc;
+ {
+ std::unique_lock<std::mutex> lk(mutex_);
+ if(!active_)
+ return false;
+ // in case of spurious wakeup, loop until predicate in lambda
+ // is satisfied.
+ can_push_.wait(lk, [this] { return queue_.size() < max_size_ || !active_; });
+ rc = push_(value);
+ }
+ if(rc)
+ can_pop_.notify_one();
+
+ return rc;
+ }
+ bool pop(Data& value)
+ {
+ bool rc;
+ {
+ std::unique_lock<std::mutex> lk(mutex_);
+ rc = pop_(value);
+ }
+ if(rc)
+ can_push_.notify_one();
+
+ return rc;
+ }
+ bool waitAndPop(Data& value)
+ {
+ bool rc;
+ {
+ std::unique_lock<std::mutex> lk(mutex_);
+ if(!active_)
+ return false;
+ // in case of spurious wakeup, loop until predicate in lambda
+ // is satisfied.
+ can_pop_.wait(lk, [this] { return !queue_.empty() || !active_; });
+ rc = pop_(value);
+ }
+ if(rc)
+ can_push_.notify_one();
+
+ return rc;
+ }
+
+ private:
+ bool push_(Data const& value)
+ {
+ if(queue_.size() == max_size_ || !active_)
+ return false;
+ queue_.push(value);
+
+ return true;
+ }
+ bool pop_(Data& value)
+ {
+ if(queue_.empty() || !active_)
+ return false;
+ value = queue_.front();
+ queue_.pop();
+
+ return true;
+ }
+ std::queue<Data> queue_;
+ mutable std::mutex mutex_;
+ std::condition_variable can_pop_;
+ std::condition_variable can_push_;
+ bool active_;
+ size_t max_size_;
+};
+struct BufferSrc
+{
+ BufferSrc(void) : BufferSrc("") {}
+ explicit BufferSrc(const std::string &file) : file_(file), clientFrameId_(0), frameId_(0), framePtr_(nullptr)
+ {}
+ BufferSrc(size_t clientFrameId, size_t frameId, uint8_t* framePtr)
+ : file_(""), clientFrameId_(clientFrameId), frameId_(frameId), framePtr_(framePtr)
+ {}
+ bool fromDisk(void)
+ {
+ return !file_.empty() && framePtr_ == nullptr;
+ }
+ size_t index() const
+ {
+ return clientFrameId_;
+ }
+ std::string file_;
+ size_t clientFrameId_;
+ size_t frameId_;
+ uint8_t* framePtr_;
+};
+
+struct Messenger;
+static void outboundThread(Messenger* messenger, const std::string &sendBuf, Synch* synch);
+static void inboundThread(Messenger* messenger, const std::string &receiveBuf, Synch* synch);
+static void processorThread(Messenger* messenger, std::function<void(std::string)> processor);
+
+struct Messenger
+{
+ explicit Messenger(MessengerInit init)
+ : running(true), _initialized(false), _shutdown(false), init_(init),
+ outboundSynch_(nullptr),
+ inboundSynch_(nullptr), uncompressed_buffer_(nullptr), compressed_buffer_(nullptr),
+ uncompressed_fd_(0), compressed_fd_(0)
+ {}
+ virtual ~Messenger(void)
+ {
+ running = false;
+ sendQueue.deactivate();
+ receiveQueue.deactivate();
+
+ if (outboundSynch_) {
+ outboundSynch_->post(SYNCH_RECEIVE_READY);
+ outbound.join();
+ }
+
+ if (inboundSynch_) {
+ inboundSynch_->post(SYNCH_SENT);
+ inbound.join();
+ }
+
+ for(auto& p : processors_)
+ p.join();
+
+ delete outboundSynch_;
+ delete inboundSynch_;
+
+ deinitShm();
+ }
+ void startThreads(void) {
+ outboundSynch_ =
+ new Synch(init_.outboundSentSynch, init_.outboundReceiveReadySynch);
+ outbound = std::thread(outboundThread, this, init_.outboundMessageBuf, outboundSynch_);
+
+ inboundSynch_ =
+ new Synch(init_.inboundSentSynch, init_.inboundReceiveReadySynch);
+ inbound = std::thread(inboundThread, this, init_.inboundMessageBuf, inboundSynch_);
+
+ for(size_t i = 0; i < init_.numProcessingThreads_; ++i)
+ processors_.push_back(std::thread(processorThread, this, init_.processor_));
+ }
+ bool initBuffers(void)
+ {
+ bool rc = true;
+ if(init_.uncompressedFrameSize_)
+ {
+ rc = rc && SharedMemoryManager::initShm(grokUncompressedBuf,
+ init_.uncompressedFrameSize_ * init_.numFrames_,
+ &uncompressed_fd_, &uncompressed_buffer_);
+ }
+ if(init_.compressedFrameSize_)
+ {
+ rc = rc && SharedMemoryManager::initShm(grokCompressedBuf,
+ init_.compressedFrameSize_ * init_.numFrames_,
+ &compressed_fd_, &compressed_buffer_);
+ }
+
+ return rc;
+ }
+
+ bool deinitShm(void)
+ {
+ bool rc = SharedMemoryManager::deinitShm(grokUncompressedBuf,
+ init_.uncompressedFrameSize_ * init_.numFrames_,
+ uncompressed_fd_, &uncompressed_buffer_);
+ rc = rc && SharedMemoryManager::deinitShm(grokCompressedBuf,
+ init_.compressedFrameSize_ * init_.numFrames_,
+ compressed_fd_, &compressed_buffer_);
+
+ return rc;
+ }
+ template<typename... Args>
+ void send(const std::string& str, Args... args)
+ {
+ std::ostringstream oss;
+ oss << str;
+ int dummy[] = {0, ((void)(oss << ',' << args), 0)...};
+ static_cast<void>(dummy);
+
+ sendQueue.push(oss.str());
+ }
+
+ bool launchGrok(
+ boost::filesystem::path const& dir,
+ uint32_t width,
+ uint32_t stride,
+ uint32_t height,
+ uint32_t samplesPerPixel,
+ uint32_t depth,
+ int device,
+ bool is4K,
+ uint32_t fps,
+ uint32_t bandwidth,
+ const std::string server,
+ uint32_t port,
+ const std::string license
+ )
+ {
+
+ std::unique_lock<std::mutex> lk(shutdownMutex_);
+ if (async_result_.valid())
+ return true;
+ if(MessengerInit::firstLaunch(true))
+ init_.unlink();
+ startThreads();
+ char _cmd[4096];
+ auto fullServer = server + ":" + std::to_string(port);
+ sprintf(_cmd,
+ "./grk_compress -batch_src %s,%d,%d,%d,%d,%d -out_fmt j2k -k 1 "
+ "-G %d -%s %d,%d -j %s -J %s -v",
+ GRK_MSGR_BATCH_IMAGE.c_str(), width, stride, height, samplesPerPixel, depth,
+ device, is4K ? "cinema4K" : "cinema2K", fps, bandwidth,
+ license.c_str(), fullServer.c_str());
+
+ return launch(_cmd, dir);
+ }
+ void initClient(size_t uncompressedFrameSize, size_t compressedFrameSize, size_t numFrames)
+ {
+ // client fills queue with pending uncompressed buffers
+ init_.uncompressedFrameSize_ = uncompressedFrameSize;
+ init_.compressedFrameSize_ = compressedFrameSize;
+ init_.numFrames_ = numFrames;
+ initBuffers();
+ auto ptr = uncompressed_buffer_;
+ for(size_t i = 0; i < init_.numFrames_; ++i)
+ {
+ availableBuffers_.push(BufferSrc(0, i, (uint8_t*)ptr));
+ ptr += init_.uncompressedFrameSize_;
+ }
+
+ std::unique_lock<std::mutex> lk(shutdownMutex_);
+ _initialized = true;
+ clientInitializedCondition_.notify_all();
+ }
+
+ bool waitForClientInit()
+ {
+ if (_initialized) {
+ return true;
+ } else if (_shutdown) {
+ return false;
+ }
+
+ std::unique_lock<std::mutex> lk(shutdownMutex_);
+
+ if (_initialized) {
+ return true;
+ } else if (_shutdown) {
+ return false;
+ }
+
+ while (true) {
+ if (clientInitializedCondition_.wait_for(lk, std::chrono::seconds(1), [this]{ return _initialized || _shutdown; })) {
+ break;
+ }
+ auto status = async_result_.wait_for(std::chrono::milliseconds(100));
+ if (status == std::future_status::ready) {
+ getMessengerLogger()->error("Grok exited unexpectedly during initialization");
+ return false;
+ }
+ }
+
+ return _initialized && !_shutdown;
+ }
+
+ static size_t uncompressedFrameSize(uint32_t w, uint32_t h, uint32_t samplesPerPixel)
+ {
+ return sizeof(uint16_t) * w * h * samplesPerPixel;
+ }
+ void reclaimCompressed(size_t frameId)
+ {
+ availableBuffers_.push(BufferSrc(0, frameId, getCompressedFrame(frameId)));
+ }
+ void reclaimUncompressed(size_t frameId)
+ {
+ availableBuffers_.push(BufferSrc(0, frameId, getUncompressedFrame(frameId)));
+ }
+ uint8_t* getUncompressedFrame(size_t frameId)
+ {
+ assert(frameId < init_.numFrames_);
+ if(frameId >= init_.numFrames_)
+ return nullptr;
+
+ return (uint8_t*)(uncompressed_buffer_ + frameId * init_.uncompressedFrameSize_);
+ }
+ uint8_t* getCompressedFrame(size_t frameId)
+ {
+ assert(frameId < init_.numFrames_);
+ if(frameId >= init_.numFrames_)
+ return nullptr;
+
+ return (uint8_t*)(compressed_buffer_ + frameId * init_.compressedFrameSize_);
+ }
+ std::atomic_bool running;
+ bool _initialized;
+ bool _shutdown;
+ MessengerBlockingQueue<std::string> sendQueue;
+ MessengerBlockingQueue<std::string> receiveQueue;
+ MessengerBlockingQueue<BufferSrc> availableBuffers_;
+ MessengerInit init_;
+ std::string cmd_;
+ std::future<int> async_result_;
+ std::mutex shutdownMutex_;
+ std::condition_variable shutdownCondition_;
+
+ protected:
+ std::condition_variable clientInitializedCondition_;
+ private:
+ bool launch(std::string const& cmd, boost::filesystem::path const& dir)
+ {
+ // Change the working directory
+ if(!dir.empty())
+ {
+ boost::system::error_code ec;
+ boost::filesystem::current_path(dir, ec);
+ if (ec) {
+ getMessengerLogger()->error("Error: failed to change the working directory");
+ return false;
+ }
+ }
+ // Execute the command using std::async and std::system
+ cmd_ = cmd;
+ getMessengerLogger()->info(cmd.c_str());
+ async_result_ = std::async(std::launch::async, [this]() { return std::system(cmd_.c_str()); });
+ bool success = async_result_.valid();
+ if (!success)
+ getMessengerLogger()->error("Grok launch failed");
+
+ return success;
+
+ }
+ std::thread outbound;
+ Synch* outboundSynch_;
+
+ std::thread inbound;
+ Synch* inboundSynch_;
+
+ std::vector<std::thread> processors_;
+ char* uncompressed_buffer_;
+ char* compressed_buffer_;
+
+ grk_handle uncompressed_fd_;
+ grk_handle compressed_fd_;
+};
+
+static void outboundThread(Messenger* messenger, const std::string &sendBuf, Synch* synch)
+{
+ grk_handle shm_fd = 0;
+ char* send_buffer = nullptr;
+
+ if(!SharedMemoryManager::initShm(sendBuf, messageBufferLen, &shm_fd, &send_buffer))
+ return;
+ while(messenger->running)
+ {
+ synch->wait(SYNCH_RECEIVE_READY);
+ if(!messenger->running)
+ break;
+ std::string message;
+ if(!messenger->sendQueue.waitAndPop(message))
+ break;
+ if(!messenger->running)
+ break;
+ memcpy(send_buffer, message.c_str(), message.size() + 1);
+ synch->post(SYNCH_SENT);
+ }
+ SharedMemoryManager::deinitShm(sendBuf, messageBufferLen, shm_fd, &send_buffer);
+}
+
+static void inboundThread(Messenger* messenger, const std::string &receiveBuf, Synch* synch)
+{
+ grk_handle shm_fd = 0;
+ char* receive_buffer = nullptr;
+
+ if(!SharedMemoryManager::initShm(receiveBuf, messageBufferLen, &shm_fd, &receive_buffer))
+ return;
+ while(messenger->running)
+ {
+ synch->wait(SYNCH_SENT);
+ if(!messenger->running)
+ break;
+ auto message = std::string(receive_buffer);
+ synch->post(SYNCH_RECEIVE_READY);
+ messenger->receiveQueue.push(message);
+ }
+ SharedMemoryManager::deinitShm(receiveBuf, messageBufferLen, shm_fd, &receive_buffer);
+}
+struct Msg
+{
+ explicit Msg(const std::string &msg) : ct_(0)
+ {
+ std::stringstream ss(msg);
+ while(ss.good())
+ {
+ std::string substr;
+ std::getline(ss, substr, ',');
+ cs_.push_back(substr);
+ }
+ }
+ std::string next()
+ {
+ if(ct_ == cs_.size())
+ {
+ getMessengerLogger()->error("Msg: comma separated list exhausted. returning empty.");
+ return "";
+ }
+ return cs_[ct_++];
+ }
+
+ uint32_t nextUint(void)
+ {
+ return (uint32_t)std::stoi(next());
+ }
+
+ std::vector<std::string> cs_;
+ size_t ct_;
+};
+
+static void processorThread(Messenger* messenger, std::function<void(std::string)> processor)
+{
+ while (messenger->running) {
+ std::string message;
+ if (!messenger->receiveQueue.waitAndPop(message)) {
+ break;
+ }
+
+ if (!messenger->running) {
+ break;
+ }
+
+ Msg msg(message);
+ auto tag = msg.next();
+ if (tag == GRK_MSGR_BATCH_COMPRESS_INIT) {
+ auto width = msg.nextUint();
+ msg.nextUint(); // stride
+ auto height = msg.nextUint();
+ auto samples_per_pixel = msg.nextUint();
+ msg.nextUint(); // depth
+ messenger->init_.uncompressedFrameSize_ = Messenger::uncompressedFrameSize(width, height, samples_per_pixel);
+ auto compressed_frame_size = msg.nextUint();
+ auto num_frames = msg.nextUint();
+ messenger->initClient(compressed_frame_size, compressed_frame_size, num_frames);
+ } else if (tag == GRK_MSGR_BATCH_PROCESSED_UNCOMPRESSED) {
+ messenger->reclaimUncompressed(msg.nextUint());
+ } else if (tag == GRK_MSGR_BATCH_PROCESSSED_COMPRESSED) {
+ messenger->reclaimCompressed(msg.nextUint());
+ }
+ processor(message);
+ }
+}
+
+template<typename F>
+struct ScheduledFrames
+{
+ void store(F const& val)
+ {
+ std::unique_lock<std::mutex> lk(mapMutex_);
+ auto it = map_.find(val.index());
+ if (it == map_.end())
+ map_.emplace(std::make_pair(val.index(), val));
+ }
+ boost::optional<F> retrieve(size_t index)
+ {
+ std::unique_lock<std::mutex> lk(mapMutex_);
+ auto it = map_.find(index);
+ if(it == map_.end())
+ return {};
+
+ F val = it->second;
+ map_.erase(index);
+
+ return val;
+ }
+
+ private:
+ std::mutex mapMutex_;
+ std::map<size_t, F> map_;
+};
+
+template<typename F>
+struct ScheduledMessenger : public Messenger
+{
+ explicit ScheduledMessenger(MessengerInit init) : Messenger(init),
+ framesScheduled_(0),
+ framesCompressed_(0)
+ {}
+ ~ScheduledMessenger(void) {
+ shutdown();
+ }
+ bool scheduleCompress(F const& proxy, std::function<void(BufferSrc const&)> converter){
+ size_t frameSize = init_.uncompressedFrameSize_;
+ assert(frameSize >= init_.uncompressedFrameSize_);
+ BufferSrc src;
+ if(!availableBuffers_.waitAndPop(src))
+ return false;
+ converter(src);
+ scheduledFrames_.store(proxy);
+ framesScheduled_++;
+ send(GRK_MSGR_BATCH_SUBMIT_UNCOMPRESSED, proxy.index(), src.frameId_);
+
+ return true;
+ }
+ void processCompressed(const std::string &message, std::function<void(F,uint8_t*,uint32_t)> processor, bool needsRecompression) {
+ Msg msg(message);
+ msg.next();
+ auto clientFrameId = msg.nextUint();
+ auto compressedFrameId = msg.nextUint();
+ auto compressedFrameLength = msg.nextUint();
+ if (!needsRecompression) {
+ auto src_frame = scheduledFrames_.retrieve(clientFrameId);
+ if (!src_frame) {
+ return;
+ }
+ processor(*src_frame, getCompressedFrame(compressedFrameId),compressedFrameLength);
+ }
+ ++framesCompressed_;
+ send(GRK_MSGR_BATCH_PROCESSSED_COMPRESSED, compressedFrameId);
+ if (_shutdown && framesCompressed_ == framesScheduled_)
+ shutdownCondition_.notify_all();
+ }
+ void shutdown(void){
+ try {
+ std::unique_lock<std::mutex> lk(shutdownMutex_);
+ if (!async_result_.valid())
+ return;
+ _shutdown = true;
+ if (framesScheduled_) {
+ uint32_t scheduled = framesScheduled_;
+ send(GRK_MSGR_BATCH_FLUSH, scheduled);
+ shutdownCondition_.wait(lk, [this] { return framesScheduled_ == framesCompressed_; });
+ }
+ availableBuffers_.deactivate();
+ send(GRK_MSGR_BATCH_SHUTDOWN);
+ int result = async_result_.get();
+ if(result != 0)
+ getMessengerLogger()->error("Accelerator failed with return code: %d\n",result);
+ } catch (std::exception &ex) {
+ getMessengerLogger()->error("%s",ex.what());
+ }
+
+ }
+
+ boost::optional<F> retrieve(size_t index) {
+ return scheduledFrames_.retrieve(index);
+ }
+
+ void store(F& val) {
+ scheduledFrames_.store(val);
+ }
+
+private:
+ ScheduledFrames<F> scheduledFrames_;
+ std::atomic<uint32_t> framesScheduled_;
+ std::atomic<uint32_t> framesCompressed_;
+};
+
+} // namespace grk_plugin
diff --git a/src/lib/grok_j2k_encoder_thread.cc b/src/lib/grok_j2k_encoder_thread.cc
new file mode 100644
index 000000000..c3cd6f8dd
--- /dev/null
+++ b/src/lib/grok_j2k_encoder_thread.cc
@@ -0,0 +1,72 @@
+/*
+ Copyright (C) 2023 Carl Hetherington <cth@carlh.net>
+
+ This file is part of DCP-o-matic.
+
+ DCP-o-matic is free software; you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation; either version 2 of the License, or
+ (at your option) any later version.
+
+ DCP-o-matic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with DCP-o-matic. If not, see <http://www.gnu.org/licenses/>.
+
+*/
+
+
+#include "config.h"
+#include "cross.h"
+#include "dcpomatic_log.h"
+#include "dcp_video.h"
+#include "grok/context.h"
+#include "grok_j2k_encoder_thread.h"
+#include "j2k_encoder.h"
+#include "util.h"
+#include <dcp/scope_guard.h>
+
+#include "i18n.h"
+
+
+using std::make_shared;
+using std::shared_ptr;
+
+
+GrokJ2KEncoderThread::GrokJ2KEncoderThread(J2KEncoder& encoder, grk_plugin::GrokContext* context)
+ : J2KEncoderThread(encoder)
+ , _context(context)
+{
+
+}
+
+
+void
+GrokJ2KEncoderThread::run()
+try
+{
+ while (true)
+ {
+ LOG_TIMING("encoder-sleep thread=%1", thread_id());
+ auto frame = _encoder.pop();
+
+ dcp::ScopeGuard frame_guard([this, &frame]() {
+ LOG_ERROR("Failed to schedule encode of %1 using grok", frame.index());
+ _encoder.retry(frame);
+ });
+
+ LOG_TIMING("encoder-pop thread=%1 frame=%2 eyes=%3", thread_id(), frame.index(), static_cast<int>(frame.eyes()));
+
+ auto grok = Config::instance()->grok().get_value_or({});
+
+ if (_context->launch(frame, grok.selected) && _context->scheduleCompress(frame)) {
+ frame_guard.cancel();
+ }
+ }
+} catch (boost::thread_interrupted& e) {
+} catch (...) {
+ store_current();
+}
diff --git a/src/lib/grok_j2k_encoder_thread.h b/src/lib/grok_j2k_encoder_thread.h
new file mode 100644
index 000000000..5301e1670
--- /dev/null
+++ b/src/lib/grok_j2k_encoder_thread.h
@@ -0,0 +1,41 @@
+/*
+ Copyright (C) 2023 Carl Hetherington <cth@carlh.net>
+
+ This file is part of DCP-o-matic.
+
+ DCP-o-matic is free software; you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation; either version 2 of the License, or
+ (at your option) any later version.
+
+ DCP-o-matic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with DCP-o-matic. If not, see <http://www.gnu.org/licenses/>.
+
+*/
+
+
+#include "exception_store.h"
+#include "j2k_encoder_thread.h"
+
+
+namespace grk_plugin {
+ class GrokContext;
+}
+
+
+class GrokJ2KEncoderThread : public J2KEncoderThread, public ExceptionStore
+{
+public:
+ GrokJ2KEncoderThread(J2KEncoder& encoder, grk_plugin::GrokContext* context);
+
+ void run() override;
+
+private:
+ grk_plugin::GrokContext* _context;
+};
+
diff --git a/src/lib/hints.cc b/src/lib/hints.cc
index 2e2a8fd7b..1114d0acb 100644
--- a/src/lib/hints.cc
+++ b/src/lib/hints.cc
@@ -35,6 +35,7 @@
#include "player.h"
#include "ratio.h"
#include "text_content.h"
+#include "variant.h"
#include "video_content.h"
#include "writer.h"
#include <dcp/cpl.h>
@@ -76,7 +77,7 @@ using namespace boost::placeholders;
Hints::Hints (weak_ptr<const Film> weak_film)
: WeakConstFilm (weak_film)
- , _writer (new Writer(weak_film, weak_ptr<Job>(), true))
+ , _writer(new Writer(weak_film, weak_ptr<Job>(), film()->dir("hints") / dcpomatic::get_process_id(), true))
, _analyser (film(), film()->playlist(), true, [](float) {})
, _stop (false)
{
@@ -107,7 +108,13 @@ void
Hints::check_few_audio_channels ()
{
if (film()->audio_channels() < 6) {
- hint (_("Your DCP has fewer than 6 audio channels. This may cause problems on some projectors. You may want to set the DCP to have 6 channels. It does not matter if your content has fewer channels, as DCP-o-matic will fill the extras with silence."));
+ hint(
+ variant::insert_dcpomatic(
+ _("Your DCP has fewer than 6 audio channels. This may cause problems on some projectors. "
+ "You may want to set the DCP to have 6 channels. It does not matter if your content has "
+ "fewer channels, as %1 will fill the extras with silence.")
+ )
+ );
}
}
@@ -117,7 +124,12 @@ Hints::check_upmixers ()
{
auto ap = film()->audio_processor();
if (ap && (ap->id() == "stereo-5.1-upmix-a" || ap->id() == "stereo-5.1-upmix-b")) {
- hint (_("You are using DCP-o-matic's stereo-to-5.1 upmixer. This is experimental and may result in poor-quality audio. If you continue, you should listen to the resulting DCP in a cinema to make sure that it sounds good."));
+ hint(variant::insert_dcpomatic(
+ _("You are using %1's stereo-to-5.1 upmixer. This is experimental and "
+ "may result in poor-quality audio. If you continue, you should listen to the "
+ "resulting DCP in a cinema to make sure that it sounds good.")
+ )
+ );
}
}
@@ -154,17 +166,17 @@ void
Hints::check_unusual_container ()
{
auto const film_container = film()->container()->id();
- if (film_container != "185" && film_container != "239") {
+ if (film()->video_encoding() != VideoEncoding::MPEG2 && film_container != "185" && film_container != "239") {
hint (_("Your DCP uses an unusual container ratio. This may cause problems on some projectors. If possible, use Flat or Scope for the DCP container ratio."));
}
}
void
-Hints::check_high_j2k_bandwidth ()
+Hints::check_high_video_bit_rate()
{
- if (film()->j2k_bandwidth() >= 245000000) {
- hint (_("A few projectors have problems playing back very high bit-rate DCPs. It is a good idea to drop the JPEG2000 bandwidth down to about 200Mbit/s; this is unlikely to have any visible effect on the image."));
+ if (film()->video_encoding() == VideoEncoding::JPEG2000 && film()->video_bit_rate(VideoEncoding::JPEG2000) >= 245000000) {
+ hint (_("A few projectors have problems playing back very high bit-rate DCPs. It is a good idea to drop the video bit rate down to about 200Mbit/s; this is unlikely to have any visible effect on the image."));
}
}
@@ -252,6 +264,15 @@ Hints::check_interop ()
void
+Hints::check_video_encoding()
+{
+ if (film()->video_encoding() == VideoEncoding::MPEG2) {
+ hint(_("The vast majority of cinemas in Europe, Australasia and North America expect DCPs encoded with JPEG2000 rather than MPEG2. Make sure that your cinema really wants an old-style MPEG2 DCP."));
+ }
+}
+
+
+void
Hints::check_big_font_files ()
{
bool big_font_files = false;
@@ -451,12 +472,13 @@ try
check_certificates ();
check_interop ();
+ check_video_encoding();
check_big_font_files ();
check_few_audio_channels ();
check_upmixers ();
check_incorrect_container ();
check_unusual_container ();
- check_high_j2k_bandwidth ();
+ check_high_video_bit_rate();
check_frame_rate ();
check_4k_3d ();
check_speed_up ();
@@ -483,7 +505,7 @@ try
auto dcp_dir = film->dir("hints") / dcpomatic::get_process_id();
dcp::filesystem::remove_all(dcp_dir);
- _writer->finish (film->dir("hints") / dcpomatic::get_process_id());
+ _writer->finish();
dcp::DCP dcp (dcp_dir);
dcp.read ();
@@ -718,16 +740,20 @@ Hints::check_certificates ()
switch (*bad) {
case Config::BAD_SIGNER_UTF8_STRINGS:
- hint(_("The certificate chain that DCP-o-matic uses for signing DCPs and KDMs contains a small error "
- "which will prevent DCPs from being validated correctly on some systems. It is advisable to "
- "re-create the signing certificate chain by clicking the \"Re-make certificates and key...\" "
- "button in the Keys page of Preferences."));
+ hint(variant::insert_dcpomatic(
+ _("The certificate chain that %1 uses for signing DCPs and KDMs contains a small error "
+ "which will prevent DCPs from being validated correctly on some systems. It is advisable to "
+ "re-create the signing certificate chain by clicking the \"Re-make certificates and key...\" "
+ "button in the Keys page of Preferences.")
+ ));
break;
case Config::BAD_SIGNER_VALIDITY_TOO_LONG:
- hint(_("The certificate chain that DCP-o-matic uses for signing DCPs and KDMs has a validity period "
- "that is too long. This will cause problems playing back DCPs on some systems. "
- "It is advisable to re-create the signing certificate chain by clicking the "
- "\"Re-make certificates and key...\" button in the Keys page of Preferences."));
+ hint(variant::insert_dcpomatic(
+ _("The certificate chain that %1 uses for signing DCPs and KDMs has a validity period "
+ "that is too long. This will cause problems playing back DCPs on some systems. "
+ "It is advisable to re-create the signing certificate chain by clicking the "
+ "\"Re-make certificates and key...\" button in the Keys page of Preferences.")
+ ));
break;
default:
/* Some bad situations can't happen here as DCP-o-matic would have refused to start until they are fixed */
@@ -740,7 +766,7 @@ void
Hints::check_8_or_16_audio_channels()
{
auto const channels = film()->audio_channels();
- if (channels != 8 && channels != 16) {
+ if (film()->video_encoding() != VideoEncoding::MPEG2 && channels != 8 && channels != 16) {
hint(String::compose(_("Your DCP has %1 audio channels, rather than 8 or 16. This may cause some distributors to raise QC errors when they check your DCP. To avoid this, set the DCP audio channels to 8 or 16."), channels));
}
}
diff --git a/src/lib/hints.h b/src/lib/hints.h
index 0d65edc21..9e94f210f 100644
--- a/src/lib/hints.h
+++ b/src/lib/hints.h
@@ -20,15 +20,16 @@
#include "audio_analyser.h"
-#include "signaller.h"
-#include "player_text.h"
#include "dcp_text_track.h"
#include "dcpomatic_time.h"
+#include "player_text.h"
+#include "signaller.h"
+#include "text_type.h"
#include "weak_film.h"
-#include <boost/signals2.hpp>
#include <boost/atomic.hpp>
-#include <vector>
+#include <boost/signals2.hpp>
#include <string>
+#include <vector>
class Film;
@@ -68,12 +69,13 @@ private:
void check_certificates ();
void check_interop ();
+ void check_video_encoding();
void check_big_font_files ();
void check_few_audio_channels ();
void check_upmixers ();
void check_incorrect_container ();
void check_unusual_container ();
- void check_high_j2k_bandwidth ();
+ void check_high_video_bit_rate();
void check_frame_rate ();
void check_4k_3d ();
void check_speed_up ();
diff --git a/src/lib/id.cc b/src/lib/id.cc
new file mode 100644
index 000000000..2891fb4ab
--- /dev/null
+++ b/src/lib/id.cc
@@ -0,0 +1,30 @@
+/*
+ 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 "id.h"
+
+
+bool
+operator==(ID const& a, ID const& b)
+{
+ return a.get() == b.get();
+}
+
diff --git a/src/lib/id.h b/src/lib/id.h
new file mode 100644
index 000000000..ca4721039
--- /dev/null
+++ b/src/lib/id.h
@@ -0,0 +1,48 @@
+/*
+ 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_ID_H
+#define DCPOMATIC_ID_H
+
+
+#include <sqlite3.h>
+
+
+class ID
+{
+public:
+ sqlite3_int64 get() const {
+ return _id;
+ }
+
+protected:
+ ID(sqlite3_int64 id)
+ : _id(id) {}
+
+private:
+ sqlite3_int64 _id;
+};
+
+
+bool operator==(ID const& a, ID const& b);
+
+
+#endif
diff --git a/src/lib/image_content.cc b/src/lib/image_content.cc
index 1a92c944e..1ac6e085b 100644
--- a/src/lib/image_content.cc
+++ b/src/lib/image_content.cc
@@ -99,13 +99,13 @@ ImageContent::technical_summary () const
void
-ImageContent::as_xml (xmlpp::Node* node, bool with_paths) const
+ImageContent::as_xml(xmlpp::Element* element, bool with_paths) const
{
- node->add_child("Type")->add_child_text ("Image");
- Content::as_xml (node, with_paths);
+ cxml::add_text_child(element, "Type", "Image");
+ Content::as_xml(element, with_paths);
if (video) {
- video->as_xml (node);
+ video->as_xml(element);
}
}
diff --git a/src/lib/image_content.h b/src/lib/image_content.h
index d817eeee8..140159bfa 100644
--- a/src/lib/image_content.h
+++ b/src/lib/image_content.h
@@ -40,7 +40,7 @@ public:
void examine (std::shared_ptr<const Film> film, std::shared_ptr<Job>) override;
std::string summary () const override;
std::string technical_summary () const override;
- void as_xml (xmlpp::Node *, bool with_paths) const override;
+ void as_xml(xmlpp::Element*, bool with_paths) const override;
dcpomatic::DCPTime full_length (std::shared_ptr<const Film> film) const override;
dcpomatic::DCPTime approximate_length () const override;
diff --git a/src/lib/image_decoder.cc b/src/lib/image_decoder.cc
index ce5c8757f..527a98c7d 100644
--- a/src/lib/image_decoder.cc
+++ b/src/lib/image_decoder.cc
@@ -81,7 +81,7 @@ ImageDecoder::pass ()
}
}
- video->emit (film(), _image, _frame_video_position);
+ video->emit(film(), _image, dcpomatic::ContentTime::from_frames(_frame_video_position, _image_content->video_frame_rate().get_value_or(24)));
++_frame_video_position;
return false;
}
diff --git a/src/lib/image_proxy.h b/src/lib/image_proxy.h
index a37be580f..cec13b773 100644
--- a/src/lib/image_proxy.h
+++ b/src/lib/image_proxy.h
@@ -41,7 +41,7 @@ class Image;
class Socket;
namespace xmlpp {
- class Node;
+ class Element;
}
namespace cxml {
@@ -101,7 +101,7 @@ public:
boost::optional<dcp::Size> size = boost::optional<dcp::Size> ()
) const = 0;
- virtual void add_metadata (xmlpp::Node *) const = 0;
+ virtual void add_metadata(xmlpp::Element *) const = 0;
virtual void write_to_socket (std::shared_ptr<Socket>) const = 0;
/** @return true if our image is definitely the same as another, false if it is probably not */
virtual bool same (std::shared_ptr<const ImageProxy>) const = 0;
diff --git a/src/lib/j2k_encoder.cc b/src/lib/j2k_encoder.cc
index 32d2fefc2..0b50bcd5a 100644
--- a/src/lib/j2k_encoder.cc
+++ b/src/lib/j2k_encoder.cc
@@ -32,6 +32,12 @@
#include "encode_server_description.h"
#include "encode_server_finder.h"
#include "film.h"
+#include "cpu_j2k_encoder_thread.h"
+#ifdef DCPOMATIC_GROK
+#include "grok/context.h"
+#include "grok_j2k_encoder_thread.h"
+#endif
+#include "remote_j2k_encoder_thread.h"
#include "j2k_encoder.h"
#include "log.h"
#include "player_video.h"
@@ -44,6 +50,7 @@
using std::cout;
+using std::dynamic_pointer_cast;
using std::exception;
using std::list;
using std::make_shared;
@@ -53,15 +60,47 @@ 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.
*/
J2KEncoder::J2KEncoder(shared_ptr<const Film> film, Writer& writer)
- : _film (film)
- , _history (200)
- , _writer (writer)
+ : VideoEncoder(film, 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
}
@@ -79,8 +118,29 @@ J2KEncoder::~J2KEncoder ()
*/
_writer.zombify();
- 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());
}
@@ -95,7 +155,42 @@ J2KEncoder::begin ()
void
-J2KEncoder::end ()
+J2KEncoder::pause()
+{
+#ifdef DCPOMATIC_GROK
+ if (!Config::instance()->grok().get_value_or({}).enable) {
+ return;
+ }
+ return;
+
+ /* XXX; the same problem may occur here as in the destructor, perhaps? */
+
+ 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);
@@ -104,18 +199,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 ();
@@ -130,42 +220,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 ());
+ }
}
}
-}
-
-/** @return an estimate of the current number of frames we are encoding per second,
- * if known.
- */
-optional<float>
-J2KEncoder::current_encoding_rate () const
-{
- return _history.rate ();
-}
-
-
-/** @return Number of video frames that have been queued for encoding */
-int
-J2KEncoder::video_frames_enqueued () const
-{
- if (!_last_player_video_time) {
- return 0;
- }
-
- return _last_player_video_time->frames_floor (_film->video_frame_rate ());
+#ifdef DCPOMATIC_GROK
+ delete _context;
+ _context = nullptr;
+#endif
}
@@ -188,12 +271,14 @@ J2KEncoder::frame_done ()
void
J2KEncoder::encode (shared_ptr<PlayerVideo> pv, DCPTime time)
{
+ VideoEncoder::encode(pv, time);
+
_waker.nudge ();
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);
@@ -233,13 +318,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->video_bit_rate(VideoEncoding::JPEG2000),
_film->resolution()
- ));
+ );
+ _queue.push_back (dcpv);
/* The queue might not be empty any more, so notify anything which is
waiting on that.
@@ -248,174 +334,144 @@ J2KEncoder::encode (shared_ptr<PlayerVideo> pv, DCPTime time)
}
_last_player_video[pv->eyes()] = pv;
- _last_player_video_time = time;
}
-/** Caller must hold a lock on _threads_mutex */
void
J2KEncoder::terminate_threads ()
{
+ boost::mutex::scoped_lock lm(_threads_mutex);
boost::this_thread::disable_interruption dis;
- if (!_threads) {
- return;
- }
-
- _threads->interrupt_all ();
- try {
- _threads->join_all ();
- } catch (exception& e) {
- LOG_ERROR ("join() threw an exception: %1", e.what());
- } catch (...) {
- LOG_ERROR_NC ("join() threw an exception");
+ for (auto& thread: _threads) {
+ thread->stop();
}
- _threads.reset ();
+ _threads.clear();
+ _ending = true;
}
void
-J2KEncoder::encoder_thread (optional<EncodeServerDescription> server)
-try
+J2KEncoder::remake_threads(int cpu, int gpu, list<EncodeServerDescription> servers)
{
- start_of_thread ("J2KEncoder");
+ LOG_GENERAL("Making threads: CPU=%1, GPU=%2, Remote=%3", cpu, gpu, servers.size());
- if (server) {
- LOG_TIMING ("start-encoder-thread thread=%1 server=%2", thread_id (), server->host_name ());
- } else {
- LOG_TIMING ("start-encoder-thread thread=%1 server=localhost", thread_id ());
+ boost::mutex::scoped_lock lm (_threads_mutex);
+ if (_ending) {
+ return;
}
- /* Number of seconds that we currently wait between attempts
- to connect to the server; not relevant for localhost
- encodings.
- */
- int remote_backoff = 0;
+ auto remove_threads = [this](int wanted, int current, std::function<bool (shared_ptr<J2KEncoderThread>)> predicate) {
+ for (auto i = wanted; i < current; ++i) {
+ auto iter = std::find_if(_threads.begin(), _threads.end(), predicate);
+ if (iter != _threads.end()) {
+ (*iter)->stop();
+ _threads.erase(iter);
+ }
+ }
+ };
+
+
+ /* CPU */
+
+ auto const is_cpu_thread = [](shared_ptr<J2KEncoderThread> thread) {
+ return static_cast<bool>(dynamic_pointer_cast<CPUJ2KEncoderThread>(thread));
+ };
+
+ auto const current_cpu_threads = std::count_if(_threads.begin(), _threads.end(), is_cpu_thread);
+
+ for (auto i = current_cpu_threads; i < cpu; ++i) {
+ auto thread = make_shared<CPUJ2KEncoderThread>(*this);
+ thread->start();
+ _threads.push_back(thread);
+ }
+
+ remove_threads(cpu, current_cpu_threads, is_cpu_thread);
+
+#ifdef DCPOMATIC_GROK
+ /* GPU */
+
+ auto const is_grok_thread = [](shared_ptr<J2KEncoderThread> thread) {
+ return static_cast<bool>(dynamic_pointer_cast<GrokJ2KEncoderThread>(thread));
+ };
+
+ auto const current_gpu_threads = std::count_if(_threads.begin(), _threads.end(), is_grok_thread);
+
+ for (auto i = current_gpu_threads; i < gpu; ++i) {
+ auto thread = make_shared<GrokJ2KEncoderThread>(*this, _context);
+ thread->start();
+ _threads.push_back(thread);
+ }
+
+ remove_threads(gpu, current_gpu_threads, is_grok_thread);
+#endif
- while (true) {
+ /* Remote */
- LOG_TIMING ("encoder-sleep thread=%1", thread_id ());
- boost::mutex::scoped_lock lock (_queue_mutex);
- while (_queue.empty ()) {
- _empty_condition.wait (lock);
+ for (auto const& server: servers) {
+ if (!server.current_link_version()) {
+ continue;
}
- LOG_TIMING ("encoder-wake thread=%1 queue=%2", thread_id(), _queue.size());
- auto vf = _queue.front ();
+ auto is_remote_thread = [server](shared_ptr<J2KEncoderThread> thread) {
+ auto remote = dynamic_pointer_cast<RemoteJ2KEncoderThread>(thread);
+ return remote && remote->server().host_name() == server.host_name();
+ };
- /* We're about to commit to either encoding this frame or putting it back onto the queue,
- so we must not be interrupted until one or other of these things have happened. This
- block has thread interruption disabled.
- */
- {
- boost::this_thread::disable_interruption dis;
-
- LOG_TIMING ("encoder-pop thread=%1 frame=%2 eyes=%3", thread_id(), vf.index(), static_cast<int>(vf.eyes()));
- _queue.pop_front ();
-
- lock.unlock ();
-
- shared_ptr<Data> encoded;
-
- /* We need to encode this input */
- if (server) {
- try {
- encoded = make_shared<dcp::ArrayData>(vf.encode_remotely(server.get()));
-
- if (remote_backoff > 0) {
- LOG_GENERAL ("%1 was lost, but now she is found; removing backoff", server->host_name ());
- }
-
- /* This job succeeded, so remove any backoff */
- remote_backoff = 0;
-
- } catch (std::exception& e) {
- if (remote_backoff < 60) {
- /* back off more */
- remote_backoff += 10;
- }
- LOG_ERROR (
- N_("Remote encode of %1 on %2 failed (%3); thread sleeping for %4s"),
- vf.index(), server->host_name(), e.what(), remote_backoff
- );
- }
-
- } else {
- try {
- LOG_TIMING ("start-local-encode thread=%1 frame=%2", thread_id(), vf.index());
- encoded = make_shared<dcp::ArrayData>(vf.encode_locally());
- LOG_TIMING ("finish-local-encode thread=%1 frame=%2", thread_id(), vf.index());
- } catch (std::exception& e) {
- /* This is very bad, so don't cope with it, just pass it on */
- LOG_ERROR (N_("Local encode failed (%1)"), e.what ());
- throw;
- }
- }
+ auto const current_threads = std::count_if(_threads.begin(), _threads.end(), is_remote_thread);
- if (encoded) {
- _writer.write(encoded, vf.index(), vf.eyes());
- frame_done ();
- } else {
- lock.lock ();
- LOG_GENERAL (N_("[%1] J2KEncoder thread pushes frame %2 back onto queue after failure"), thread_id(), vf.index());
- _queue.push_front (vf);
- lock.unlock ();
- }
+ auto const wanted_threads = server.threads();
+
+ if (wanted_threads > current_threads) {
+ LOG_GENERAL(N_("Adding %1 worker threads for remote %2"), wanted_threads - current_threads, server.host_name());
+ } else if (wanted_threads < current_threads) {
+ LOG_GENERAL(N_("Removing %1 worker threads for remote %2"), current_threads - wanted_threads, server.host_name());
}
- if (remote_backoff > 0) {
- boost::this_thread::sleep (boost::posix_time::seconds (remote_backoff));
+ for (auto i = current_threads; i < wanted_threads; ++i) {
+ auto thread = make_shared<RemoteJ2KEncoderThread>(*this, server);
+ thread->start();
+ _threads.push_back(thread);
}
- /* The queue might not be full any more, so notify anything that is waiting on that */
- lock.lock ();
- _full_condition.notify_all ();
+ remove_threads(wanted_threads, current_threads, is_remote_thread);
}
-}
-catch (boost::thread_interrupted& e) {
- /* Ignore these and just stop the thread */
- _full_condition.notify_all ();
-}
-catch (...)
-{
- store_current ();
- /* Wake anything waiting on _full_condition so it can see the exception */
- _full_condition.notify_all ();
+
+ _writer.set_encoder_threads(_threads.size());
}
-void
-J2KEncoder::servers_list_changed ()
+DCPVideo
+J2KEncoder::pop()
{
- boost::mutex::scoped_lock lm (_threads_mutex);
+ boost::mutex::scoped_lock lock(_queue_mutex);
+ while (_queue.empty()) {
+ _empty_condition.wait (lock);
+ }
- terminate_threads ();
- _threads = make_shared<boost::thread_group>();
+ LOG_TIMING("encoder-wake thread=%1 queue=%2", thread_id(), _queue.size());
- /* XXX: could re-use threads */
+ auto vf = _queue.front();
+ _queue.pop_front();
- if (!Config::instance()->only_servers_encode ()) {
- for (int i = 0; i < Config::instance()->master_encoding_threads (); ++i) {
-#ifdef DCPOMATIC_LINUX
- auto t = _threads->create_thread(boost::bind(&J2KEncoder::encoder_thread, this, optional<EncodeServerDescription>()));
- pthread_setname_np (t->native_handle(), "encode-worker");
-#else
- _threads->create_thread(boost::bind(&J2KEncoder::encoder_thread, this, optional<EncodeServerDescription>()));
-#endif
- }
- }
+ _full_condition.notify_all();
+ return vf;
+}
- for (auto i: EncodeServerFinder::instance()->servers()) {
- if (!i.current_link_version()) {
- continue;
- }
- LOG_GENERAL (N_("Adding %1 worker threads for remote %2"), i.threads(), i.host_name ());
- for (int j = 0; j < i.threads(); ++j) {
- _threads->create_thread(boost::bind(&J2KEncoder::encoder_thread, this, i));
- }
- }
+void
+J2KEncoder::retry(DCPVideo video)
+{
+ boost::mutex::scoped_lock lock(_queue_mutex);
+ _queue.push_front(video);
+ _empty_condition.notify_all();
+}
+
- _writer.set_encoder_threads(_threads->size());
+void
+J2KEncoder::write(shared_ptr<const dcp::Data> data, int index, Eyes eyes)
+{
+ _writer.write(data, index, eyes);
+ frame_done();
}
diff --git a/src/lib/j2k_encoder.h b/src/lib/j2k_encoder.h
index 63228a6b8..c72a1debe 100644
--- a/src/lib/j2k_encoder.h
+++ b/src/lib/j2k_encoder.h
@@ -32,7 +32,9 @@
#include "enum_indexed_vector.h"
#include "event_history.h"
#include "exception_store.h"
+#include "j2k_encoder_thread.h"
#include "writer.h"
+#include "video_encoder.h"
#include <boost/optional.hpp>
#include <boost/signals2.hpp>
#include <boost/thread.hpp>
@@ -48,6 +50,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.
@@ -55,7 +66,7 @@ class PlayerVideo;
* This class keeps a queue of frames to be encoded and distributes
* the work around threads and encoding servers.
*/
-class J2KEncoder : public ExceptionStore
+class J2KEncoder : public VideoEncoder, public ExceptionStore
{
public:
J2KEncoder(std::shared_ptr<const Film> film, Writer& writer);
@@ -65,33 +76,33 @@ public:
J2KEncoder& operator= (J2KEncoder const&) = delete;
/** Called to indicate that a processing run is about to begin */
- void begin ();
+ void begin() override;
/** 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 encode (std::shared_ptr<PlayerVideo> pv, dcpomatic::DCPTime time) override;
- /** Called when a processing run has finished */
- void end ();
+ void pause() override;
+ void resume() override;
- boost::optional<float> current_encoding_rate () const;
- int video_frames_enqueued () const;
+ /** Called when a processing run has finished */
+ void end() override;
- 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 */
- std::shared_ptr<const Film> _film;
-
- 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;
@@ -100,13 +111,18 @@ private:
/** condition to manage thread wakeups when we have too much to do */
boost::condition _full_condition;
- Writer& _writer;
Waker _waker;
EnumIndexedVector<std::shared_ptr<PlayerVideo>, Eyes> _last_player_video;
- boost::optional<dcpomatic::DCPTime> _last_player_video_time;
boost::signals2::scoped_connection _server_found_connection;
+
+#ifdef DCPOMATIC_GROK
+ grk_plugin::DcpomaticContext* _dcpomatic_context = nullptr;
+ grk_plugin::GrokContext *_context = nullptr;
+#endif
+
+ bool _ending = false;
};
diff --git a/src/lib/j2k_encoder_thread.cc b/src/lib/j2k_encoder_thread.cc
new file mode 100644
index 000000000..d0e8a439c
--- /dev/null
+++ b/src/lib/j2k_encoder_thread.cc
@@ -0,0 +1,58 @@
+/*
+ Copyright (C) 2023 Carl Hetherington <cth@carlh.net>
+
+ This file is part of DCP-o-matic.
+
+ DCP-o-matic is free software; you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation; either version 2 of the License, or
+ (at your option) any later version.
+
+ DCP-o-matic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with DCP-o-matic. If not, see <http://www.gnu.org/licenses/>.
+
+*/
+
+
+#include "dcp_video.h"
+#include "dcpomatic_log.h"
+#include "j2k_encoder.h"
+#include "j2k_encoder_thread.h"
+
+
+J2KEncoderThread::J2KEncoderThread(J2KEncoder& encoder)
+ : _encoder(encoder)
+{
+
+}
+
+
+void
+J2KEncoderThread::start()
+{
+ _thread = boost::thread(boost::bind(&J2KEncoderThread::run, this));
+#ifdef DCPOMATIC_LINUX
+ pthread_setname_np(_thread.native_handle(), "encode-worker");
+#endif
+}
+
+
+void
+J2KEncoderThread::stop()
+{
+ _thread.interrupt();
+ try {
+ _thread.join();
+ } catch (std::exception& e) {
+ LOG_ERROR("join() threw an exception: %1", e.what());
+ } catch (...) {
+ LOG_ERROR_NC("join() threw an exception");
+ }
+}
+
+
diff --git a/src/lib/j2k_encoder_thread.h b/src/lib/j2k_encoder_thread.h
new file mode 100644
index 000000000..b03b6f356
--- /dev/null
+++ b/src/lib/j2k_encoder_thread.h
@@ -0,0 +1,53 @@
+/*
+ Copyright (C) 2023 Carl Hetherington <cth@carlh.net>
+
+ This file is part of DCP-o-matic.
+
+ DCP-o-matic is free software; you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation; either version 2 of the License, or
+ (at your option) any later version.
+
+ DCP-o-matic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with DCP-o-matic. If not, see <http://www.gnu.org/licenses/>.
+
+*/
+
+
+#ifndef DCPOMATIC_J2K_ENCODER_THREAD
+#define DCPOMATIC_J2K_ENCODER_THREAD
+
+
+#include <boost/thread.hpp>
+
+
+class J2KEncoder;
+
+
+class J2KEncoderThread
+{
+public:
+ J2KEncoderThread(J2KEncoder& encoder);
+
+ J2KEncoderThread(J2KEncoderThread const&) = delete;
+ J2KEncoderThread& operator=(J2KEncoderThread const&) = delete;
+
+ void start();
+ void stop();
+
+ virtual void run() = 0;
+
+protected:
+ J2KEncoder& _encoder;
+
+private:
+ boost::thread _thread;
+};
+
+
+#endif
diff --git a/src/lib/j2k_image_proxy.cc b/src/lib/j2k_image_proxy.cc
index 269b01bca..023723c8b 100644
--- a/src/lib/j2k_image_proxy.cc
+++ b/src/lib/j2k_image_proxy.cc
@@ -25,11 +25,11 @@
#include "j2k_image_proxy.h"
#include <dcp/colour_conversion.h>
#include <dcp/j2k_transcode.h>
-#include <dcp/mono_picture_frame.h>
+#include <dcp/mono_j2k_picture_frame.h>
#include <dcp/openjpeg_image.h>
#include <dcp/raw_convert.h>
#include <dcp/rgb_xyz.h>
-#include <dcp/stereo_picture_frame.h>
+#include <dcp/stereo_j2k_picture_frame.h>
#include <dcp/warnings.h>
#include <libcxml/cxml.h>
LIBDCP_DISABLE_WARNINGS
@@ -64,7 +64,7 @@ J2KImageProxy::J2KImageProxy (boost::filesystem::path path, dcp::Size size, AVPi
J2KImageProxy::J2KImageProxy (
- shared_ptr<const dcp::MonoPictureFrame> frame,
+ shared_ptr<const dcp::MonoJ2KPictureFrame> frame,
dcp::Size size,
AVPixelFormat pixel_format,
optional<int> forced_reduction
@@ -81,7 +81,7 @@ J2KImageProxy::J2KImageProxy (
J2KImageProxy::J2KImageProxy (
- shared_ptr<const dcp::StereoPictureFrame> frame,
+ shared_ptr<const dcp::StereoJ2KPictureFrame> frame,
dcp::Size size,
dcp::Eye eye,
AVPixelFormat pixel_format,
@@ -192,15 +192,15 @@ J2KImageProxy::image (Image::Alignment alignment, optional<dcp::Size> target_siz
void
-J2KImageProxy::add_metadata (xmlpp::Node* node) const
+J2KImageProxy::add_metadata(xmlpp::Element* element) const
{
- node->add_child("Type")->add_child_text(N_("J2K"));
- node->add_child("Width")->add_child_text(raw_convert<string>(_size.width));
- node->add_child("Height")->add_child_text(raw_convert<string>(_size.height));
+ cxml::add_text_child(element, "Type", N_("J2K"));
+ cxml::add_text_child(element, "Width", raw_convert<string>(_size.width));
+ cxml::add_text_child(element, "Height", raw_convert<string>(_size.height));
if (_eye) {
- node->add_child("Eye")->add_child_text(raw_convert<string>(static_cast<int>(_eye.get())));
+ cxml::add_text_child(element, "Eye", raw_convert<string>(static_cast<int>(_eye.get())));
}
- node->add_child("Size")->add_child_text(raw_convert<string>(_data->size()));
+ cxml::add_text_child(element, "Size", raw_convert<string>(_data->size()));
}
diff --git a/src/lib/j2k_image_proxy.h b/src/lib/j2k_image_proxy.h
index 9666ea406..1d2d5cc21 100644
--- a/src/lib/j2k_image_proxy.h
+++ b/src/lib/j2k_image_proxy.h
@@ -26,8 +26,8 @@
namespace dcp {
- class MonoPictureFrame;
- class StereoPictureFrame;
+ class MonoJ2KPictureFrame;
+ class StereoJ2KPictureFrame;
}
@@ -37,14 +37,14 @@ public:
J2KImageProxy (boost::filesystem::path path, dcp::Size, AVPixelFormat pixel_format);
J2KImageProxy (
- std::shared_ptr<const dcp::MonoPictureFrame> frame,
+ std::shared_ptr<const dcp::MonoJ2KPictureFrame> frame,
dcp::Size,
AVPixelFormat pixel_format,
boost::optional<int> forced_reduction
);
J2KImageProxy (
- std::shared_ptr<const dcp::StereoPictureFrame> frame,
+ std::shared_ptr<const dcp::StereoJ2KPictureFrame> frame,
dcp::Size,
dcp::Eye,
AVPixelFormat pixel_format,
@@ -61,7 +61,7 @@ public:
boost::optional<dcp::Size> size = boost::optional<dcp::Size> ()
) const override;
- void add_metadata (xmlpp::Node *) const override;
+ void add_metadata(xmlpp::Element*) const override;
void write_to_socket (std::shared_ptr<Socket> override) const override;
/** @return true if our image is definitely the same as another, false if it is probably not */
bool same (std::shared_ptr<const ImageProxy>) const override;
diff --git a/src/lib/j2k_sync_encoder_thread.cc b/src/lib/j2k_sync_encoder_thread.cc
new file mode 100644
index 000000000..ef6834f60
--- /dev/null
+++ b/src/lib/j2k_sync_encoder_thread.cc
@@ -0,0 +1,65 @@
+/*
+ Copyright (C) 2023 Carl Hetherington <cth@carlh.net>
+
+ This file is part of DCP-o-matic.
+
+ DCP-o-matic is free software; you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation; either version 2 of the License, or
+ (at your option) any later version.
+
+ DCP-o-matic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with DCP-o-matic. If not, see <http://www.gnu.org/licenses/>.
+
+*/
+
+
+#include "dcp_video.h"
+#include "dcpomatic_log.h"
+#include "j2k_encoder.h"
+#include "j2k_sync_encoder_thread.h"
+#include <dcp/scope_guard.h>
+
+
+J2KSyncEncoderThread::J2KSyncEncoderThread(J2KEncoder& encoder)
+ : J2KEncoderThread(encoder)
+{
+
+}
+
+
+void
+J2KSyncEncoderThread::run()
+try
+{
+ log_thread_start();
+
+ while (true) {
+ LOG_TIMING("encoder-sleep thread=%1", thread_id());
+ auto frame = _encoder.pop();
+
+ dcp::ScopeGuard frame_guard([this, &frame]() {
+ boost::this_thread::disable_interruption dis;
+ _encoder.retry(frame);
+ });
+
+ LOG_TIMING("encoder-pop thread=%1 frame=%2 eyes=%3", thread_id(), frame.index(), static_cast<int>(frame.eyes()));
+
+ auto encoded = encode(frame);
+
+ if (encoded) {
+ boost::this_thread::disable_interruption dis;
+ frame_guard.cancel();
+ _encoder.write(encoded, frame.index(), frame.eyes());
+ }
+ }
+} catch (boost::thread_interrupted& e) {
+} catch (...) {
+ store_current();
+}
+
diff --git a/src/lib/j2k_sync_encoder_thread.h b/src/lib/j2k_sync_encoder_thread.h
new file mode 100644
index 000000000..45222279e
--- /dev/null
+++ b/src/lib/j2k_sync_encoder_thread.h
@@ -0,0 +1,32 @@
+#ifndef DCPOMATIC_J2K_SYNC_ENCODER_THREAD_H
+#define DCPOMATIC_J2K_SYNC_ENCODER_THREAD_H
+
+
+#include "exception_store.h"
+#include "j2k_encoder_thread.h"
+#include <dcp/array_data.h>
+#include <boost/thread.hpp>
+
+
+class DCPVideo;
+class J2KEncoder;
+
+
+class J2KSyncEncoderThread : public J2KEncoderThread, public ExceptionStore
+{
+public:
+ J2KSyncEncoderThread(J2KEncoder& encoder);
+
+ J2KSyncEncoderThread(J2KSyncEncoderThread const&) = delete;
+ J2KSyncEncoderThread& operator=(J2KSyncEncoderThread const&) = delete;
+
+ virtual ~J2KSyncEncoderThread() {}
+
+ void run() override;
+
+ virtual void log_thread_start() const = 0;
+ virtual std::shared_ptr<dcp::ArrayData> encode(DCPVideo const& frame) = 0;
+};
+
+
+#endif
diff --git a/src/lib/job.cc b/src/lib/job.cc
index 9c9530b7a..ee6ad4e70 100644
--- a/src/lib/job.cc
+++ b/src/lib/job.cc
@@ -33,6 +33,7 @@
#include "job.h"
#include "log.h"
#include "util.h"
+#include "variant.h"
#include <dcp/exceptions.h>
#include <sub/exceptions.h>
#include <boost/date_time/posix_time/posix_time.hpp>
@@ -137,9 +138,13 @@ Job::run_wrapper ()
/* 32-bit */
set_error (
_("Failed to encode the DCP."),
- _("This error has probably occurred because you are running the 32-bit version of DCP-o-matic and "
- "trying to use too many encoding threads. Please reduce the 'number of threads DCP-o-matic should "
- "use' in the General tab of Preferences and try again.")
+ String::compose(
+ _("This error has probably occurred because you are running the 32-bit version of %1 and "
+ "trying to use too many encoding threads. Please reduce the 'number of threads %2 should "
+ "use' in the General tab of Preferences and try again."),
+ variant::dcpomatic(),
+ variant::dcpomatic()
+ )
);
done = true;
#else
@@ -147,7 +152,12 @@ Job::run_wrapper ()
if (running_32_on_64()) {
set_error (
_("Failed to encode the DCP."),
- _("This error has probably occurred because you are running the 32-bit version of DCP-o-matic. Please re-install DCP-o-matic with the 64-bit installer and try again.")
+ String::compose(
+ _("This error has probably occurred because you are running the 32-bit version of %1. "
+ "Please re-install %2 with the 64-bit installer and try again."),
+ variant::dcpomatic(),
+ variant::dcpomatic()
+ )
);
done = true;
}
@@ -157,7 +167,7 @@ Job::run_wrapper ()
if (!done) {
set_error (
e.what (),
- string (_("It is not known what caused this error.")) + " " + REPORT_PROBLEM
+ String::compose(_("It is not known what caused this error. %1"), report_problem())
);
}
@@ -168,8 +178,8 @@ Job::run_wrapper ()
set_error (
String::compose (_("Could not open %1"), e.file().string()),
- String::compose (
- _("DCP-o-matic could not open the file %1 (%2). Perhaps it does not exist or is in an unexpected format."),
+ String::compose(_("%1 could not open the file %2 (%3). Perhaps it does not exist or is in an unexpected format."),
+ variant::dcpomatic(),
dcp::filesystem::absolute(e.file()).string(),
e.what()
)
@@ -183,8 +193,8 @@ Job::run_wrapper ()
if (e.code() == boost::system::errc::no_such_file_or_directory) {
set_error (
String::compose (_("Could not open %1"), e.path1().string ()),
- String::compose (
- _("DCP-o-matic could not open the file %1 (%2). Perhaps it does not exist or is in an unexpected format."),
+ String::compose(_("%1 could not open the file %2 (%3). Perhaps it does not exist or is in an unexpected format."),
+ variant::dcpomatic(),
dcp::filesystem::absolute(e.path1()).string(),
e.what()
)
@@ -192,7 +202,7 @@ Job::run_wrapper ()
} else {
set_error (
e.what (),
- string (_("It is not known what caused this error.")) + " " + REPORT_PROBLEM
+ String::compose(_("It is not known what caused this error. %1"), report_problem())
);
}
@@ -252,7 +262,7 @@ Job::run_wrapper ()
set_error (
e.what (),
- string (_("It is not known what caused this error.")) + " " + REPORT_PROBLEM
+ String::compose(_("It is not known what caused this error. %1"), report_problem())
);
set_progress (1);
@@ -262,7 +272,7 @@ Job::run_wrapper ()
set_error (
_("Unknown error"),
- string (_("It is not known what caused this error.")) + " " + REPORT_PROBLEM
+ String::compose(_("It is not known what caused this error. %1"), report_problem())
);
set_progress (1);
@@ -662,7 +672,7 @@ void
Job::cancel ()
{
if (_thread.joinable()) {
- resume();
+ Job::resume();
_thread.interrupt ();
_thread.join ();
@@ -689,6 +699,7 @@ Job::pause_by_user ()
}
if (paused) {
+ pause();
_pause_changed.notify_all ();
}
@@ -701,6 +712,7 @@ Job::pause_by_priority ()
{
if (running ()) {
set_state (PAUSED_BY_PRIORITY);
+ pause();
_pause_changed.notify_all ();
}
}
diff --git a/src/lib/job.h b/src/lib/job.h
index d4d0f9510..9b5fdfa6e 100644
--- a/src/lib/job.h
+++ b/src/lib/job.h
@@ -62,9 +62,10 @@ public:
}
void start ();
+ virtual void pause() {}
bool pause_by_user ();
void pause_by_priority ();
- void resume ();
+ virtual void resume ();
void cancel ();
bool is_new () const;
diff --git a/src/lib/kdm_cli.cc b/src/lib/kdm_cli.cc
index 4199f8430..0b918266e 100644
--- a/src/lib/kdm_cli.cc
+++ b/src/lib/kdm_cli.cc
@@ -25,6 +25,7 @@
#include "cinema.h"
+#include "cinema_list.h"
#include "config.h"
#include "dkdm_wrapper.h"
#include "email.h"
@@ -32,6 +33,7 @@
#include "film.h"
#include "kdm_with_metadata.h"
#include "screen.h"
+#include "variant.h"
#include <dcp/certificate.h>
#include <dcp/decrypted_kdm.h>
#include <dcp/encrypted_kdm.h>
@@ -43,6 +45,7 @@
using std::dynamic_pointer_cast;
using std::list;
using std::make_shared;
+using std::pair;
using std::runtime_error;
using std::shared_ptr;
using std::string;
@@ -58,13 +61,19 @@ using namespace dcpomatic;
static void
help (std::function<void (string)> out)
{
- out (String::compose("Syntax: %1 [OPTION] <FILM|CPL-ID|DKDM>", program_name));
+ out (String::compose("Syntax: %1 [OPTION] [COMMAND] <FILM|CPL-ID|DKDM>", program_name));
+ out ("Commands:");
+ out ("create create KDMs; default if no other command is specified");
+ out (variant::insert_dcpomatic("list-cinemas list known cinemas from %1 settings"));
+ out (variant::insert_dcpomatic("list-dkdm-cpls list CPLs for which %1 has DKDMs"));
+ out (variant::insert_dcpomatic("add-dkdm add DKDM to %1's list"));
+ out (variant::insert_dcpomatic("dump-decryption-certificate write the %1 KDM decryption certificate to the console"));
out (" -h, --help show this help");
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");
@@ -74,19 +83,18 @@ help (std::function<void (string)> out)
out (" -v, --verbose be verbose");
out (" -c, --cinema <name|email> cinema name (when using -C) or name/email (to filter cinemas)");
out (" -S, --screen <name> screen name (when using -C) or screen name (to filter screens when using -c)");
- out (" -C, --certificate <file> file containing projector certificate");
- out (" -T, --trusted-device <file> file containing a trusted device's certificate");
+ out (" -C, --projector-certificate <file> file containing projector certificate");
+ out (" -T, --trusted-device-certificate <file> file containing a trusted device's certificate");
+ out (" --decryption-key <file> file containing the private key which can decrypt the given DKDM");
+ out (variant::insert_dcpomatic(" (%1's configured private key will be used otherwise)"));
out (" --cinemas-file <file> use the given file as a list of cinemas instead of the current configuration");
- out (" --dump-decryption-certificate write the DCP-o-matic KDM decryption certificate to the console");
- out (" --list-cinemas list known cinemas from the DCP-o-matic settings");
- out (" --list-dkdm-cpls list CPLs for which DCP-o-matic has DKDMs");
out ("");
- out ("CPL-ID must be the ID of a CPL that is mentioned in DCP-o-matic's DKDM list.");
+ out (variant::insert_dcpomatic("CPL-ID must be the ID of a CPL that is mentioned in %1's DKDM list."));
out ("");
out ("For example:");
out ("");
out ("Create KDMs for my_great_movie to play in all of Fred's Cinema's screens for the next two weeks and zip them up.");
- out ("(Fred's Cinema must have been set up in DCP-o-matic's KDM window)");
+ out (variant::insert_dcpomatic("(Fred's Cinema must have been set up in %1's KDM window)"));
out ("");
out (String::compose("\t%1 -c \"Fred's Cinema\" -f now -d \"2 weeks\" -z my_great_movie", program_name));
}
@@ -101,17 +109,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)
{
@@ -182,39 +179,32 @@ write_files (
}
-static
-shared_ptr<Cinema>
-find_cinema (string cinema_name)
+class ScreenDetails
{
- auto cinemas = Config::instance()->cinemas ();
- auto i = cinemas.begin();
- while (
- i != cinemas.end() &&
- (*i)->name != cinema_name &&
- find ((*i)->emails.begin(), (*i)->emails.end(), cinema_name) == (*i)->emails.end()) {
-
- ++i;
- }
-
- if (i == cinemas.end ()) {
- throw KDMCLIError (String::compose("could not find cinema \"%1\"", cinema_name));
- }
+public:
+ ScreenDetails(CinemaID const& cinema_id, Cinema const& cinema, Screen const& screen)
+ : cinema_id(cinema_id)
+ , cinema(cinema)
+ , screen(screen)
+ {}
- return *i;
-}
+ CinemaID cinema_id;
+ Cinema cinema;
+ Screen screen;
+};
static
void
from_film (
- vector<shared_ptr<Screen>> screens,
+ vector<ScreenDetails> const& screens,
boost::filesystem::path film_dir,
bool verbose,
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,
@@ -248,17 +238,27 @@ from_film (
try {
list<KDMWithMetadataPtr> kdms;
- for (auto i: screens) {
+ for (auto screen_details: screens) {
std::function<dcp::DecryptedKDM (dcp::LocalTime, dcp::LocalTime)> make_kdm = [film, cpl](dcp::LocalTime begin, dcp::LocalTime end) {
return film->make_kdm(cpl, begin, end);
};
- auto p = kdm_for_screen(make_kdm, i, valid_from, valid_to, formulation, disable_forensic_marking_picture, disable_forensic_marking_audio, period_checks);
+ auto p = kdm_for_screen(
+ make_kdm,
+ screen_details.cinema_id,
+ screen_details.cinema,
+ screen_details.screen,
+ valid_from,
+ valid_to,
+ formulation,
+ disable_forensic_marking_picture,
+ disable_forensic_marking_audio,
+ period_checks
+ );
if (p) {
kdms.push_back (p);
}
}
-
if (find_if(
period_checks.begin(),
period_checks.end(),
@@ -358,14 +358,14 @@ kdm_from_dkdm (
static
void
from_dkdm (
- vector<shared_ptr<Screen>> screens,
+ vector<ScreenDetails> const& screens,
dcp::DecryptedKDM dkdm,
bool verbose,
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,
@@ -378,37 +378,31 @@ from_dkdm (
try {
list<KDMWithMetadataPtr> kdms;
- for (auto i: screens) {
- if (!i->recipient) {
+ for (auto const& screen_details: screens) {
+ if (!screen_details.screen.recipient) {
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,
+ screen_details.screen.recipient.get(),
+ screen_details.screen.trusted_device_thumbprints(),
+ valid_from,
+ valid_to,
formulation,
disable_forensic_marking_picture,
disable_forensic_marking_audio
);
dcp::NameFormat::Map name_values;
- name_values['c'] = i->cinema ? i->cinema->name : "";
- name_values['s'] = i->name;
+ name_values['c'] = screen_details.cinema.name;
+ name_values['s'] = screen_details.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();
- kdms.push_back(make_shared<KDMWithMetadata>(name_values, i->cinema.get(), i->cinema ? i->cinema->emails : vector<string>(), kdm));
+ kdms.push_back(make_shared<KDMWithMetadata>(name_values, screen_details.cinema_id, screen_details.cinema.emails, kdm));
}
write_files (kdms, zip, output, container_name_format, filename_format, verbose, out);
if (email) {
@@ -442,6 +436,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);
+}
+
+
void
dump_decryption_certificate(std::function<void (string)> out)
{
@@ -461,17 +471,20 @@ try
boost::filesystem::path output = dcp::filesystem::current_path();
auto container_name_format = Config::instance()->kdm_container_name_format();
auto filename_format = Config::instance()->kdm_filename_format();
+ /* either a cinema name to search for, or the name of a cinema to associate with certificate */
optional<string> cinema_name;
- shared_ptr<Cinema> cinema;
- optional<boost::filesystem::path> certificate;
- optional<string> screen;
- vector<shared_ptr<Screen>> screens;
+ /* either a screen name to search for, or the name of a screen to associate with certificate */
+ optional<string> screen_name;
+ /* a certificate that we will use to make up a temporary cinema and screen */
+ optional<boost::filesystem::path> projector_certificate;
+ optional<boost::filesystem::path> decryption_key;
+ /* trusted devices that we will use to make up a temporary cinema and screen */
+ vector<TrustedDevice> trusted_devices;
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;
+ string command = "create";
optional<string> duration_string;
bool verbose = false;
dcp::Formulation formulation = dcp::Formulation::MODIFIED_TRANSITIONAL_1;
@@ -503,16 +516,14 @@ try
{ "verbose", no_argument, 0, 'v' },
{ "cinema", required_argument, 0, 'c' },
{ "screen", required_argument, 0, 'S' },
- { "certificate", required_argument, 0, 'C' },
- { "trusted-device", required_argument, 0, 'T' },
- { "list-cinemas", no_argument, 0, 'B' },
- { "list-dkdm-cpls", no_argument, 0, 'D' },
+ { "projector-certificate", required_argument, 0, 'C' },
+ { "trusted-device-certificate", required_argument, 0, 'T' },
+ { "decryption-key", required_argument, 0, 'G' },
{ "cinemas-file", required_argument, 0, 'E' },
- { "dump-decryption-certificate", no_argument, 0, 'G' },
{ 0, 0, 0, 0 }
};
- int c = getopt_long (argc, argv, "ho:K:Z:f:t:d:F:pae::zvc:S:C:T:BDE:G", long_options, &option_index);
+ int c = getopt_long (argc, argv, "ho:K:Z:f:t:d:F:pae::zvc:S:C:T:E:G:", long_options, &option_index);
if (c == -1) {
break;
@@ -532,10 +543,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;
@@ -574,70 +585,92 @@ try
verbose = true;
break;
case 'c':
- /* This could be a cinema to search for in the configured list or the name of a cinema being
- built up on-the-fly in the option. Cater for both possilibities here by storing the name
- (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);
break;
case 'S':
- /* Similarly, this could be the name of a new (temporary) screen or the name of a screen
- * to search for.
- */
- screen = optarg;
+ screen_name = optarg;
break;
case 'C':
- certificate = optarg;
+ projector_certificate = optarg;
break;
case 'T':
- /* A trusted device ends up in the last screen we made */
- if (!screens.empty ()) {
- screens.back()->trusted_devices.push_back(TrustedDevice(dcp::Certificate(dcp::file_to_string(optarg))));
- }
- break;
- case 'B':
- list_cinemas = true;
+ trusted_devices.push_back(TrustedDevice(dcp::Certificate(dcp::file_to_string(optarg))));
break;
- case 'D':
- list_dkdm_cpls = true;
+ case 'G':
+ decryption_key = optarg;
break;
case 'E':
cinemas_file = optarg;
break;
- case 'G':
- dump_decryption_certificate(out);
- return {};
}
}
+ vector<string> commands = {
+ "create",
+ "list-cinemas",
+ "list-dkdm-cpls",
+ "add-dkdm",
+ "dump-decryption-certificate"
+ };
+
+ if (optind < argc - 1) {
+ /* Command with some KDM / CPL / whever specified afterwards */
+ command = argv[optind++];
+ } else if (optind < argc) {
+ /* Look for a valid command, hoping that it's not the name of the KDM / CPL / whatever */
+ if (std::find(commands.begin(), commands.end(), argv[optind]) != commands.end()) {
+ command = argv[optind++];
+ }
+ }
+
+ if (std::find(commands.begin(), commands.end(), command) == commands.end()) {
+ throw KDMCLIError(String::compose("Unrecognised command %1", command));
+ }
+
if (cinemas_file) {
Config::instance()->set_cinemas_file(*cinemas_file);
}
- if (certificate) {
- /* Make a new screen and add it to the current cinema */
- dcp::CertificateChain chain(dcp::file_to_string(*certificate));
- auto screen_to_add = std::make_shared<Screen>(screen.get_value_or(""), "", chain.leaf(), boost::none, vector<TrustedDevice>());
- if (cinema) {
- cinema->add_screen(screen_to_add);
- }
- screens.push_back(screen_to_add);
+ /* If we've been given a certificate we can make up a temporary cinema and screen (not written to the
+ * database) to then use for making KDMs.
+ */
+ optional<Cinema> temp_cinema;
+ optional<Screen> temp_screen;
+ if (projector_certificate) {
+ temp_cinema = Cinema(cinema_name.get_value_or(""), {}, "", dcp::UTCOffset());
+ dcp::CertificateChain chain(dcp::file_to_string(*projector_certificate));
+ temp_screen = Screen(screen_name.get_value_or(""), "", chain.leaf(), boost::none, trusted_devices);
}
- if (list_cinemas) {
- auto cinemas = Config::instance()->cinemas ();
- for (auto i: cinemas) {
- out (String::compose("%1 (%2)", i->name, Email::address_list(i->emails)));
+ if (command == "list-cinemas") {
+ CinemaList cinemas;
+ for (auto const& cinema: cinemas.cinemas()) {
+ out(String::compose("%1 (%2)", cinema.second.name, Email::address_list(cinema.second.emails)));
}
return {};
}
- if (list_dkdm_cpls) {
+ if (command == "list-dkdm-cpls") {
dump_dkdm_group (Config::instance()->dkdms(), 0, out);
return {};
}
+ if (command == "dump-deccryption-certificate") {
+ dump_decryption_certificate(out);
+ return {};
+ }
+
+ if (optind >= argc) {
+ throw KDMCLIError("no film, CPL ID or DKDM specified");
+ }
+
+ if (command == "add-dkdm") {
+ auto dkdms = Config::instance()->dkdms();
+ dkdms->add(make_shared<DKDM>(dcp::EncryptedKDM(dcp::file_to_string(argv[optind]))));
+ Config::instance()->write_config();
+ return {};
+ }
+
if (!duration_string && !valid_to) {
throw KDMCLIError ("you must specify a --valid-duration or --valid-to");
}
@@ -650,23 +683,37 @@ try
throw KDMCLIError ("no film, CPL ID or DKDM specified");
}
- if (screens.empty()) {
+ vector<ScreenDetails> screens;
+
+ if (!temp_cinema) {
if (!cinema_name) {
- throw KDMCLIError ("you must specify either a cinema or one or more screens using certificate files");
+ throw KDMCLIError("you must specify either a cinema or one or more screens using certificate files");
}
- screens = find_cinema (*cinema_name)->screens ();
- if (screen) {
- screens.erase(std::remove_if(screens.begin(), screens.end(), [&screen](shared_ptr<Screen> s) { return s->name != *screen; }), screens.end());
+ CinemaList cinema_list;
+ if (auto cinema = cinema_list.cinema_by_name_or_email(*cinema_name)) {
+ if (screen_name) {
+ for (auto screen: cinema_list.screens_by_cinema_and_name(cinema->first, *screen_name)) {
+ screens.push_back({cinema->first, cinema->second, screen.second});
+ }
+ } else {
+ for (auto screen: cinema_list.screens(cinema->first)) {
+ screens.push_back({cinema->first, cinema->second, screen.second});
+ }
+ }
}
+ } else {
+ DCPOMATIC_ASSERT(temp_screen);
+ screens.push_back({CinemaID(0), *temp_cinema, *temp_screen});
}
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];
@@ -698,9 +745,11 @@ try
throw KDMCLIError ("could not find film or CPL ID corresponding to " + thing);
}
+ string const key = decryption_key ? dcp::file_to_string(*decryption_key) : Config::instance()->decryption_chain()->key().get();
+
from_dkdm (
screens,
- dcp::DecryptedKDM (*dkdm, Config::instance()->decryption_chain()->key().get()),
+ dcp::DecryptedKDM(*dkdm, key),
verbose,
output,
container_name_format,
diff --git a/src/lib/kdm_recipient.cc b/src/lib/kdm_recipient.cc
index 671e9797c..c33eb1b9f 100644
--- a/src/lib/kdm_recipient.cc
+++ b/src/lib/kdm_recipient.cc
@@ -39,14 +39,14 @@ KDMRecipient::KDMRecipient (cxml::ConstNodePtr node)
void
KDMRecipient::as_xml (xmlpp::Element* parent) const
{
- parent->add_child("Name")->add_child_text(name);
+ cxml::add_text_child(parent, "Name", name);
if (recipient) {
- parent->add_child("Recipient")->add_child_text(recipient->certificate(true));
+ cxml::add_text_child(parent, "Recipient", recipient->certificate(true));
}
if (recipient_file) {
- parent->add_child("RecipientFile")->add_child_text(*recipient_file);
+ cxml::add_text_child(parent, "RecipientFile", *recipient_file);
}
- parent->add_child("Notes")->add_child_text(notes);
+ cxml::add_text_child(parent, "Notes", notes);
}
diff --git a/src/lib/kdm_with_metadata.h b/src/lib/kdm_with_metadata.h
index fbeeffbc1..6198564b1 100644
--- a/src/lib/kdm_with_metadata.h
+++ b/src/lib/kdm_with_metadata.h
@@ -23,6 +23,7 @@
#define DCPOMATIC_KDM_WITH_METADATA_H
+#include "id.h"
#include <dcp/encrypted_kdm.h>
#include <dcp/name_format.h>
@@ -33,7 +34,7 @@ class Cinema;
class KDMWithMetadata
{
public:
- KDMWithMetadata(dcp::NameFormat::Map const& name_values, void const* group, std::vector<std::string> emails, dcp::EncryptedKDM kdm)
+ KDMWithMetadata(dcp::NameFormat::Map const& name_values, ID group, std::vector<std::string> emails, dcp::EncryptedKDM kdm)
: _name_values (name_values)
, _group (group)
, _emails (emails)
@@ -54,7 +55,7 @@ public:
boost::optional<std::string> get (char k) const;
- void const* group () const {
+ ID group() const {
return _group;
}
@@ -64,7 +65,7 @@ public:
private:
dcp::NameFormat::Map _name_values;
- void const* _group;
+ ID _group;
std::vector<std::string> _emails;
dcp::EncryptedKDM _kdm;
};
diff --git a/src/lib/make_dcp.cc b/src/lib/make_dcp.cc
index 17d45be46..b72756194 100644
--- a/src/lib/make_dcp.cc
+++ b/src/lib/make_dcp.cc
@@ -21,7 +21,7 @@
#include "config.h"
#include "dcp_content.h"
-#include "dcp_encoder.h"
+#include "dcp_film_encoder.h"
#include "dcp_transcode_job.h"
#include "dcpomatic_log.h"
#include "environment_info.h"
@@ -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());
+ LOG_GENERAL("Video bit rate %1", film->video_bit_rate(film->video_encoding()));
auto tj = make_shared<DCPTranscodeJob>(film, behaviour);
- tj->set_encoder (make_shared<DCPEncoder>(film, tj));
+ tj->set_encoder(make_shared<DCPFilmEncoder>(film, tj));
JobManager::instance()->add (tj);
+
+ return tj;
}
diff --git a/src/lib/make_dcp.h b/src/lib/make_dcp.h
index 9f5072782..fe0bcd2f6 100644
--- a/src/lib/make_dcp.h
+++ b/src/lib/make_dcp.h
@@ -25,5 +25,5 @@
class Film;
-void make_dcp (std::shared_ptr<Film> film, TranscodeJob::ChangedBehaviour behaviour);
+std::shared_ptr<TranscodeJob> make_dcp(std::shared_ptr<Film> film, TranscodeJob::ChangedBehaviour behaviour);
diff --git a/src/lib/map_cli.cc b/src/lib/map_cli.cc
index b158499a8..be3841deb 100644
--- a/src/lib/map_cli.cc
+++ b/src/lib/map_cli.cc
@@ -27,7 +27,7 @@
#include <dcp/interop_subtitle_asset.h>
#include <dcp/filesystem.h>
#include <dcp/font_asset.h>
-#include <dcp/mono_picture_asset.h>
+#include <dcp/mono_j2k_picture_asset.h>
#include <dcp/reel.h>
#include <dcp/reel_atmos_asset.h>
#include <dcp/reel_closed_caption_asset.h>
@@ -37,7 +37,7 @@
#include <dcp/reel_subtitle_asset.h>
#include <dcp/smpte_subtitle_asset.h>
#include <dcp/sound_asset.h>
-#include <dcp/stereo_picture_asset.h>
+#include <dcp/stereo_j2k_picture_asset.h>
#include <boost/optional.hpp>
#include <getopt.h>
#include <algorithm>
diff --git a/src/lib/mpeg2_encoder.cc b/src/lib/mpeg2_encoder.cc
new file mode 100644
index 000000000..9b9cdfd09
--- /dev/null
+++ b/src/lib/mpeg2_encoder.cc
@@ -0,0 +1,80 @@
+/*
+ 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 "mpeg2_encoder.h"
+#include "writer.h"
+#include <dcp/ffmpeg_image.h>
+extern "C" {
+#include <libavutil/pixfmt.h>
+}
+
+
+using std::shared_ptr;
+
+
+MPEG2Encoder::MPEG2Encoder(shared_ptr<const Film> film, Writer& writer)
+ : VideoEncoder(film, writer)
+ , _transcoder(film->frame_size(), film->video_frame_rate(), film->video_bit_rate(VideoEncoding::MPEG2))
+{
+
+}
+
+
+void
+MPEG2Encoder::encode(shared_ptr<PlayerVideo> pv, dcpomatic::DCPTime time)
+{
+ VideoEncoder::encode(pv, time);
+
+ auto image = pv->image(
+ [](AVPixelFormat) { return AV_PIX_FMT_YUV420P; },
+ VideoRange::VIDEO,
+ false
+ );
+
+ dcp::FFmpegImage ffmpeg_image(time.get() * _film->video_frame_rate() / dcpomatic::DCPTime::HZ);
+
+ DCPOMATIC_ASSERT(image->size() == ffmpeg_image.size());
+
+ auto height = image->size().height;
+
+ for (int y = 0; y < height; ++y) {
+ memcpy(ffmpeg_image.y() + ffmpeg_image.y_stride() * y, image->data()[0] + image->stride()[0] * y, ffmpeg_image.y_stride());
+ }
+
+ for (int y = 0; y < height / 2; ++y) {
+ memcpy(ffmpeg_image.u() + ffmpeg_image.u_stride() * y, image->data()[1] + image->stride()[1] * y, ffmpeg_image.u_stride());
+ memcpy(ffmpeg_image.v() + ffmpeg_image.v_stride() * y, image->data()[2] + image->stride()[2] * y, ffmpeg_image.v_stride());
+ }
+
+ if (auto compressed = _transcoder.compress_frame(std::move(ffmpeg_image))) {
+ _writer.write(compressed->first, compressed->second);
+ }
+}
+
+
+void
+MPEG2Encoder::end()
+{
+ if (auto compressed = _transcoder.flush()) {
+ _writer.write(compressed->first, compressed->second);
+ }
+}
+
diff --git a/src/lib/mpeg2_encoder.h b/src/lib/mpeg2_encoder.h
new file mode 100644
index 000000000..1b2259d26
--- /dev/null
+++ b/src/lib/mpeg2_encoder.h
@@ -0,0 +1,42 @@
+/*
+ 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 "video_encoder.h"
+#include <dcp/mpeg2_transcode.h>
+
+
+class MPEG2Encoder : public VideoEncoder
+{
+public:
+ MPEG2Encoder(std::shared_ptr<const Film> film, Writer& writer);
+
+ void encode(std::shared_ptr<PlayerVideo> pv, dcpomatic::DCPTime time) override;
+
+ void pause() override {}
+ void resume() override {}
+
+ /** Called when a processing run has finished */
+ void end() override;
+
+private:
+ dcp::MPEG2Compressor _transcoder;
+};
+
diff --git a/src/lib/overlaps.cc b/src/lib/overlaps.cc
index fbc6b5cb1..bed75a822 100644
--- a/src/lib/overlaps.cc
+++ b/src/lib/overlaps.cc
@@ -28,7 +28,8 @@ using std::shared_ptr;
using namespace dcpomatic;
-ContentList overlaps (shared_ptr<const Film> film, ContentList cl, function<bool (shared_ptr<const Content>)> part, DCPTime from, DCPTime to)
+ContentList
+dcpomatic::overlaps(shared_ptr<const Film> film, ContentList cl, function<bool (shared_ptr<const Content>)> part, DCPTime from, DCPTime to)
{
ContentList overlaps;
DCPTimePeriod period (from, to);
diff --git a/src/lib/overlaps.h b/src/lib/overlaps.h
index bb8277eaa..f405c0fab 100644
--- a/src/lib/overlaps.h
+++ b/src/lib/overlaps.h
@@ -27,6 +27,9 @@ class ContentPart;
class Film;
+namespace dcpomatic {
+
+
/** @return Pieces of content with a given part (video, audio,
* subtitle) that overlap a specified time range in the given
* ContentList
@@ -34,3 +37,7 @@ class Film;
ContentList overlaps (
std::shared_ptr<const Film> film, ContentList cl, std::function<bool (std::shared_ptr<const Content>)> part, dcpomatic::DCPTime from, dcpomatic::DCPTime to
);
+
+
+}
+
diff --git a/src/lib/pixel_quanta.cc b/src/lib/pixel_quanta.cc
index 7c1d285cf..07690f3ac 100644
--- a/src/lib/pixel_quanta.cc
+++ b/src/lib/pixel_quanta.cc
@@ -34,8 +34,8 @@ PixelQuanta::PixelQuanta (cxml::ConstNodePtr node)
void
PixelQuanta::as_xml (xmlpp::Element* node) const
{
- node->add_child("X")->add_child_text(dcp::raw_convert<std::string>(x));
- node->add_child("Y")->add_child_text(dcp::raw_convert<std::string>(y));
+ cxml::add_text_child(node, "X", dcp::raw_convert<std::string>(x));
+ cxml::add_text_child(node, "Y", dcp::raw_convert<std::string>(y));
}
diff --git a/src/lib/player.cc b/src/lib/player.cc
index 75f6b7919..ea236db39 100644
--- a/src/lib/player.cc
+++ b/src/lib/player.cc
@@ -71,10 +71,8 @@ using std::dynamic_pointer_cast;
using std::list;
using std::make_pair;
using std::make_shared;
-using std::make_shared;
using std::max;
using std::min;
-using std::min;
using std::pair;
using std::shared_ptr;
using std::vector;
@@ -412,7 +410,6 @@ Player::setup_pieces ()
_silent = Empty(film, playlist(), bind(&have_audio, _1), _playback_length);
_next_video_time = boost::none;
- _next_video_eyes = Eyes::BOTH;
_next_audio_time = boost::none;
}
@@ -525,7 +522,7 @@ Player::black_player_video_frame (Eyes eyes) const
boost::mutex::scoped_lock lm(_black_image_mutex);
return std::make_shared<PlayerVideo> (
- std::make_shared<const RawImageProxy>(_black_image),
+ make_shared<const RawImageProxy>(_black_image),
Crop(),
optional<double>(),
_video_container_size,
@@ -535,7 +532,7 @@ Player::black_player_video_frame (Eyes eyes) const
PresetColourConversion::all().front().conversion,
VideoRange::FULL,
std::weak_ptr<Content>(),
- boost::optional<Frame>(),
+ boost::optional<dcpomatic::ContentTime>(),
false
);
}
@@ -736,7 +733,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;
}
@@ -775,22 +772,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;
@@ -893,24 +901,16 @@ Player::pass ()
}
if (done) {
+ if (_next_video_time) {
+ 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);
}
}
@@ -973,15 +973,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));
+ }
+ }
}
@@ -1006,11 +1052,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()) {
@@ -1033,15 +1074,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;
}
@@ -1054,56 +1091,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;
@@ -1112,33 +1101,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));
}
}
@@ -1420,18 +1414,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;
@@ -1440,33 +1434,7 @@ Player::seek (DCPTime time, bool accurate)
void
-Player::emit_video (shared_ptr<PlayerVideo> pv, DCPTime time)
-{
- auto film = _film.lock();
- DCPOMATIC_ASSERT(film);
-
- /* We need a delay to give a little wiggle room to ensure that relevant subtitles arrive at the
- player before the video that requires them.
- */
- _delay.push_back (make_pair (pv, time));
-
- if (pv->eyes() == Eyes::BOTH || pv->eyes() == Eyes::RIGHT) {
- _next_video_time = time + one_video_frame();
- }
- _next_video_eyes = increment_eyes (pv->eyes());
-
- if (_delay.size() < 3) {
- return;
- }
-
- auto to_do = _delay.front();
- _delay.pop_front();
- do_emit_video (to_do.first, to_do.second);
-}
-
-
-void
-Player::do_emit_video (shared_ptr<PlayerVideo> pv, DCPTime time)
+Player::emit_video(shared_ptr<PlayerVideo> pv, DCPTime time)
{
if (pv->eyes() == Eyes::BOTH || pv->eyes() == Eyes::RIGHT) {
std::for_each(_active_texts.begin(), _active_texts.end(), [time](ActiveText& a) { a.clear_before(time); });
diff --git a/src/lib/player.h b/src/lib/player.h
index 48f6f97ca..314031698 100644
--- a/src/lib/player.h
+++ b/src/lib/player.h
@@ -157,6 +157,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);
std::pair<std::shared_ptr<Piece>, boost::optional<dcpomatic::DCPTime>> earliest_piece_and_time() const;
void video (std::weak_ptr<Piece>, ContentVideo);
@@ -172,8 +174,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;
@@ -214,15 +216,12 @@ private:
/** Time of the next video that we will emit, or the time of the last accurate seek */
boost::optional<dcpomatic::DCPTime> _next_video_time;
- /** Eyes of the next video that we will emit */
- boost::optional<Eyes> _next_video_eyes;
/** Time of the next audio that we will emit, or the time of the last accurate seek */
boost::optional<dcpomatic::DCPTime> _next_audio_time;
boost::atomic<boost::optional<int>> _dcp_decode_reduction;
- typedef std::map<std::weak_ptr<Piece>, std::shared_ptr<PlayerVideo>, std::owner_less<std::weak_ptr<Piece>>> LastVideoMap;
- LastVideoMap _last_video;
+ EnumIndexedVector<std::pair<std::shared_ptr<PlayerVideo>, dcpomatic::DCPTime>, Eyes> _last_video;
AudioMerger _audio_merger;
std::unique_ptr<Shuffler> _shuffler;
diff --git a/src/lib/player_video.cc b/src/lib/player_video.cc
index 35c5d3daa..247301d58 100644
--- a/src/lib/player_video.cc
+++ b/src/lib/player_video.cc
@@ -45,6 +45,7 @@ using std::weak_ptr;
using boost::optional;
using dcp::Data;
using dcp::raw_convert;
+using namespace dcpomatic;
PlayerVideo::PlayerVideo (
@@ -58,7 +59,7 @@ PlayerVideo::PlayerVideo (
optional<ColourConversion> colour_conversion,
VideoRange video_range,
weak_ptr<Content> content,
- optional<Frame> video_frame,
+ optional<ContentTime> video_time,
bool error
)
: _in (in)
@@ -71,7 +72,7 @@ PlayerVideo::PlayerVideo (
, _colour_conversion (colour_conversion)
, _video_range (video_range)
, _content (content)
- , _video_frame (video_frame)
+ , _video_time(video_time)
, _error (error)
{
@@ -199,29 +200,29 @@ PlayerVideo::make_image (function<AVPixelFormat (AVPixelFormat)> pixel_format, V
void
-PlayerVideo::add_metadata (xmlpp::Node* node) const
+PlayerVideo::add_metadata(xmlpp::Element* element) const
{
- _crop.as_xml (node);
+ _crop.as_xml(element);
if (_fade) {
- node->add_child("Fade")->add_child_text (raw_convert<string> (_fade.get ()));
+ cxml::add_text_child(element, "Fade", raw_convert<string>(_fade.get()));
}
- _in->add_metadata (node->add_child ("In"));
- node->add_child("InterWidth")->add_child_text (raw_convert<string> (_inter_size.width));
- node->add_child("InterHeight")->add_child_text (raw_convert<string> (_inter_size.height));
- node->add_child("OutWidth")->add_child_text (raw_convert<string> (_out_size.width));
- node->add_child("OutHeight")->add_child_text (raw_convert<string> (_out_size.height));
- node->add_child("Eyes")->add_child_text (raw_convert<string> (static_cast<int> (_eyes)));
- node->add_child("Part")->add_child_text (raw_convert<string> (static_cast<int> (_part)));
- node->add_child("VideoRange")->add_child_text(raw_convert<string>(static_cast<int>(_video_range)));
- node->add_child("Error")->add_child_text(_error ? "1" : "0");
+ _in->add_metadata(cxml::add_child(element, "In"));
+ cxml::add_text_child(element, "InterWidth", raw_convert<string>(_inter_size.width));
+ cxml::add_text_child(element, "InterHeight", raw_convert<string>(_inter_size.height));
+ cxml::add_text_child(element, "OutWidth", raw_convert<string>(_out_size.width));
+ cxml::add_text_child(element, "OutHeight", raw_convert<string>(_out_size.height));
+ cxml::add_text_child(element, "Eyes", raw_convert<string>(static_cast<int>(_eyes)));
+ cxml::add_text_child(element, "Part", raw_convert<string>(static_cast<int>(_part)));
+ cxml::add_text_child(element, "VideoRange", raw_convert<string>(static_cast<int>(_video_range)));
+ cxml::add_text_child(element, "Error", _error ? "1" : "0");
if (_colour_conversion) {
- _colour_conversion.get().as_xml (node);
+ _colour_conversion.get().as_xml(element);
}
if (_text) {
- node->add_child ("SubtitleWidth")->add_child_text (raw_convert<string> (_text->image->size().width));
- node->add_child ("SubtitleHeight")->add_child_text (raw_convert<string> (_text->image->size().height));
- node->add_child ("SubtitleX")->add_child_text (raw_convert<string> (_text->position.x));
- node->add_child ("SubtitleY")->add_child_text (raw_convert<string> (_text->position.y));
+ cxml::add_text_child(element, "SubtitleWidth", raw_convert<string>(_text->image->size().width));
+ cxml::add_text_child(element, "SubtitleHeight", raw_convert<string>(_text->image->size().height));
+ cxml::add_text_child(element, "SubtitleX", raw_convert<string>(_text->position.x));
+ cxml::add_text_child(element, "SubtitleY", raw_convert<string>(_text->position.y));
}
}
@@ -343,7 +344,7 @@ PlayerVideo::shallow_copy () const
_colour_conversion,
_video_range,
_content,
- _video_frame,
+ _video_time,
_error
);
}
@@ -356,12 +357,12 @@ bool
PlayerVideo::reset_metadata (shared_ptr<const Film> film, dcp::Size player_video_container_size)
{
auto content = _content.lock();
- if (!content || !_video_frame) {
+ if (!content || !_video_time) {
return false;
}
_crop = content->video->actual_crop();
- _fade = content->video->fade(film, _video_frame.get());
+ _fade = content->video->fade(film, _video_time.get());
auto const size = content->video->scaled_size(film->frame_size());
if (!size) {
return false;
diff --git a/src/lib/player_video.h b/src/lib/player_video.h
index f2781c1a0..e2968749c 100644
--- a/src/lib/player_video.h
+++ b/src/lib/player_video.h
@@ -59,7 +59,7 @@ public:
boost::optional<ColourConversion> colour_conversion,
VideoRange video_range,
std::weak_ptr<Content> content,
- boost::optional<Frame> video_frame,
+ boost::optional<dcpomatic::ContentTime> video_time,
bool error
);
@@ -82,7 +82,7 @@ public:
static AVPixelFormat force (AVPixelFormat);
static AVPixelFormat keep_xyz_or_rgb (AVPixelFormat);
- void add_metadata (xmlpp::Node* node) const;
+ void add_metadata(xmlpp::Element* element) const;
void write_to_socket (std::shared_ptr<Socket> socket) const;
bool reset_metadata (std::shared_ptr<const Film> film, dcp::Size player_video_container_size);
@@ -141,8 +141,8 @@ private:
boost::optional<PositionImage> _text;
/** Content that we came from. This is so that reset_metadata() can work. */
std::weak_ptr<Content> _content;
- /** Video frame that we came from. Again, this is for reset_metadata() */
- boost::optional<Frame> _video_frame;
+ /** Video time that we came from. Again, this is for reset_metadata() */
+ boost::optional<dcpomatic::ContentTime> _video_time;
mutable boost::mutex _mutex;
mutable std::shared_ptr<Image> _image;
diff --git a/src/lib/playlist.cc b/src/lib/playlist.cc
index 85957e106..d2af000e5 100644
--- a/src/lib/playlist.cc
+++ b/src/lib/playlist.cc
@@ -282,10 +282,10 @@ Playlist::set_from_xml (shared_ptr<const Film> film, cxml::ConstNodePtr node, in
* @param with_content_paths true to include &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);
}
}
@@ -645,16 +645,16 @@ Playlist::move_later (shared_ptr<const Film> film, shared_ptr<Content> c)
int64_t
-Playlist::required_disk_space (shared_ptr<const Film> film, int j2k_bandwidth, int audio_channels, int audio_frame_rate) const
+Playlist::required_disk_space(shared_ptr<const Film> film, int64_t video_bit_rate, int audio_channels, int audio_frame_rate) const
{
- int64_t video = uint64_t(j2k_bandwidth / 8) * length(film).seconds();
+ int64_t video = uint64_t(video_bit_rate / 8) * length(film).seconds();
int64_t audio = uint64_t(audio_channels) * audio_frame_rate * 3 * length(film).seconds();
for (auto i: content()) {
auto d = dynamic_pointer_cast<DCPContent> (i);
if (d) {
if (d->reference_video()) {
- video -= uint64_t (j2k_bandwidth / 8) * d->length_after_trim(film).seconds();
+ video -= uint64_t(video_bit_rate / 8) * d->length_after_trim(film).seconds();
}
if (d->reference_audio()) {
audio -= uint64_t(audio_channels) * audio_frame_rate * 3 * d->length_after_trim(film).seconds();
diff --git a/src/lib/playlist.h b/src/lib/playlist.h
index e2662eb45..3868c0b51 100644
--- a/src/lib/playlist.h
+++ b/src/lib/playlist.h
@@ -50,7 +50,7 @@ public:
Playlist (Playlist const&) = delete;
Playlist& operator= (Playlist const&) = delete;
- void as_xml (xmlpp::Node *, bool with_content_paths);
+ void as_xml(xmlpp::Element*, bool with_content_paths);
void set_from_xml (std::shared_ptr<const Film> film, cxml::ConstNodePtr node, int version, std::list<std::string>& notes);
void add (std::shared_ptr<const Film> film, std::shared_ptr<Content>);
@@ -65,7 +65,7 @@ public:
dcpomatic::DCPTime length (std::shared_ptr<const Film> film) const;
boost::optional<dcpomatic::DCPTime> start () const;
- int64_t required_disk_space (std::shared_ptr<const Film> film, int j2k_bandwidth, int audio_channels, int audio_frame_rate) const;
+ int64_t required_disk_space(std::shared_ptr<const Film> film, int64_t video_bit_rate, int audio_channels, int audio_frame_rate) const;
int best_video_frame_rate () const;
dcpomatic::DCPTime video_end (std::shared_ptr<const Film> film) const;
diff --git a/src/lib/raw_image_proxy.cc b/src/lib/raw_image_proxy.cc
index c606ddd99..1ad78827c 100644
--- a/src/lib/raw_image_proxy.cc
+++ b/src/lib/raw_image_proxy.cc
@@ -73,12 +73,12 @@ RawImageProxy::image (Image::Alignment alignment, optional<dcp::Size>) const
void
-RawImageProxy::add_metadata (xmlpp::Node* node) const
+RawImageProxy::add_metadata(xmlpp::Element* element) const
{
- node->add_child("Type")->add_child_text(N_("Raw"));
- node->add_child("Width")->add_child_text(raw_convert<string>(_image->size().width));
- node->add_child("Height")->add_child_text(raw_convert<string>(_image->size().height));
- node->add_child("PixelFormat")->add_child_text(raw_convert<string>(static_cast<int>(_image->pixel_format())));
+ cxml::add_text_child(element, "Type", N_("Raw"));
+ cxml::add_text_child(element, "Width", raw_convert<string>(_image->size().width));
+ cxml::add_text_child(element, "Height", raw_convert<string>(_image->size().height));
+ cxml::add_text_child(element, "PixelFormat", raw_convert<string>(static_cast<int>(_image->pixel_format())));
}
diff --git a/src/lib/raw_image_proxy.h b/src/lib/raw_image_proxy.h
index d5ae2457d..852f7c076 100644
--- a/src/lib/raw_image_proxy.h
+++ b/src/lib/raw_image_proxy.h
@@ -37,7 +37,7 @@ public:
boost::optional<dcp::Size> size = boost::optional<dcp::Size> ()
) const override;
- void add_metadata (xmlpp::Node *) const override;
+ void add_metadata(xmlpp::Element*) const override;
void write_to_socket (std::shared_ptr<Socket>) const override;
bool same (std::shared_ptr<const ImageProxy>) const override;
size_t memory_used () const override;
diff --git a/src/lib/reel_writer.cc b/src/lib/reel_writer.cc
index 1b33cae85..d8aff1162 100644
--- a/src/lib/reel_writer.cc
+++ b/src/lib/reel_writer.cc
@@ -28,11 +28,13 @@
#include "digester.h"
#include "film.h"
#include "film_util.h"
+#include "frame_info.h"
#include "image.h"
#include "image_png.h"
#include "job.h"
#include "log.h"
#include "reel_writer.h"
+#include "remembered_asset.h"
#include <dcp/atmos_asset.h>
#include <dcp/atmos_asset_writer.h>
#include <dcp/certificate_chain.h>
@@ -40,7 +42,7 @@
#include <dcp/dcp.h>
#include <dcp/filesystem.h>
#include <dcp/interop_subtitle_asset.h>
-#include <dcp/mono_picture_asset.h>
+#include <dcp/mono_j2k_picture_asset.h>
#include <dcp/raw_convert.h>
#include <dcp/reel.h>
#include <dcp/reel_atmos_asset.h>
@@ -55,7 +57,7 @@
#include <dcp/smpte_subtitle_asset.h>
#include <dcp/sound_asset.h>
#include <dcp/sound_asset_writer.h>
-#include <dcp/stereo_picture_asset.h>
+#include <dcp/stereo_j2k_picture_asset.h>
#include <dcp/subtitle_image.h>
#include "i18n.h"
@@ -81,9 +83,6 @@ using dcp::raw_convert;
using namespace dcpomatic;
-int const ReelWriter::_info_size = 48;
-
-
static dcp::MXFMetadata
mxf_metadata ()
{
@@ -108,9 +107,10 @@ mxf_metadata ()
* subtitle / closed caption files.
*/
ReelWriter::ReelWriter (
- weak_ptr<const Film> weak_film, DCPTimePeriod period, shared_ptr<Job> job, int reel_index, int reel_count, bool text_only
+ weak_ptr<const Film> weak_film, DCPTimePeriod period, shared_ptr<Job> job, int reel_index, int reel_count, bool text_only, boost::filesystem::path output_dir
)
- : WeakConstFilm (weak_film)
+ : WeakConstFilm(weak_film)
+ , _output_dir(std::move(output_dir))
, _period (period)
, _reel_index (reel_index)
, _reel_count (reel_count)
@@ -119,59 +119,87 @@ ReelWriter::ReelWriter (
, _text_only (text_only)
, _font_metrics(film()->frame_size().height)
{
- /* Create or find our picture asset in a subdirectory, named
- according to those film's parameters which affect the video
- output. We will hard-link it into the DCP later.
- */
+ _default_font = dcp::ArrayData(default_font_file());
+
+ if (text_only) {
+ return;
+ }
auto const standard = film()->interop() ? dcp::Standard::INTEROP : dcp::Standard::SMPTE;
- boost::filesystem::path const asset =
- film()->internal_video_asset_dir() / film()->internal_video_asset_filename(_period);
+ auto remembered_assets = film()->read_remembered_assets();
+ DCPOMATIC_ASSERT(film()->directory());
- _first_nonexistent_frame = check_existing_picture_asset (asset);
+ auto existing_asset_filename = find_asset(remembered_assets, *film()->directory(), period, film()->video_identifier());
+ if (existing_asset_filename) {
+ _first_nonexistent_frame = check_existing_picture_asset(*existing_asset_filename);
+ }
if (_first_nonexistent_frame < period.duration().frames_round(film()->video_frame_rate())) {
- /* We do not have a complete picture asset. If there is an
- existing asset, break any hard links to it as we are about
- to change its contents (if only by changing the IDs); see
- #1126.
- */
- if (dcp::filesystem::exists(asset) && dcp::filesystem::hard_link_count(asset) > 1) {
- if (job) {
- job->sub (_("Copying old video file"));
- copy_in_bits (asset, asset.string() + ".tmp", bind(&Job::set_progress, job.get(), _1, false));
- } else {
- dcp::filesystem::copy_file(asset, asset.string() + ".tmp");
+ /* No existing asset, or an incomplete one */
+
+ auto const rate = dcp::Fraction(film()->video_frame_rate(), 1);
+
+ auto setup = [this](shared_ptr<dcp::PictureAsset> asset) {
+ asset->set_size(film()->frame_size());
+ asset->set_metadata(mxf_metadata());
+
+ if (film()->encrypted()) {
+ asset->set_key(film()->key());
+ asset->set_context_id(film()->context_id());
}
- dcp::filesystem::remove(asset);
- dcp::filesystem::rename(asset.string() + ".tmp", asset);
- }
+ };
+ shared_ptr<dcp::PictureAsset> picture_asset;
- if (film()->three_d()) {
- _picture_asset.reset (new dcp::StereoPictureAsset(dcp::Fraction(film()->video_frame_rate(), 1), standard));
+ if (film()->video_encoding() == VideoEncoding::JPEG2000) {
+ if (film()->three_d()) {
+ _j2k_picture_asset = std::make_shared<dcp::StereoJ2KPictureAsset>(rate, standard);
+ } else {
+ _j2k_picture_asset = std::make_shared<dcp::MonoJ2KPictureAsset>(rate, standard);
+ }
+ setup(_j2k_picture_asset);
+ picture_asset = _j2k_picture_asset;
} else {
- _picture_asset.reset (new dcp::MonoPictureAsset(dcp::Fraction(film()->video_frame_rate(), 1), standard));
+ _mpeg2_picture_asset = std::make_shared<dcp::MonoMPEG2PictureAsset>(rate);
+ setup(_mpeg2_picture_asset);
+ picture_asset = _mpeg2_picture_asset;
}
- _picture_asset->set_size (film()->frame_size());
- _picture_asset->set_metadata (mxf_metadata());
-
- if (film()->encrypted()) {
- _picture_asset->set_key (film()->key());
- _picture_asset->set_context_id (film()->context_id());
+ auto new_asset_filename = _output_dir / video_asset_filename(picture_asset, _reel_index, _reel_count, _content_summary);
+ if (_first_nonexistent_frame > 0) {
+ LOG_GENERAL("Re-using partial asset %1: has frames up to %2", *existing_asset_filename, _first_nonexistent_frame);
+ dcp::filesystem::rename(*existing_asset_filename, new_asset_filename);
}
+ remembered_assets.push_back(RememberedAsset(new_asset_filename.filename(), period, film()->video_identifier()));
+ film()->write_remembered_assets(remembered_assets);
+ picture_asset->set_file(new_asset_filename);
- _picture_asset->set_file (asset);
- _picture_asset_writer = _picture_asset->start_write(asset, _first_nonexistent_frame > 0 ? dcp::PictureAsset::Behaviour::OVERWRITE_EXISTING : dcp::PictureAsset::Behaviour::MAKE_NEW);
- } else if (!text_only) {
+ dcp::Behaviour const behaviour = _first_nonexistent_frame > 0 ? dcp::Behaviour::OVERWRITE_EXISTING : dcp::Behaviour::MAKE_NEW;
+ if (_j2k_picture_asset) {
+ _j2k_picture_asset_writer = _j2k_picture_asset->start_write(new_asset_filename, behaviour);
+ } else {
+ _mpeg2_picture_asset_writer = _mpeg2_picture_asset->start_write(new_asset_filename, behaviour);
+ }
+ } else {
+ LOG_GENERAL("Re-using complete asset %1", *existing_asset_filename);
/* We already have a complete picture asset that we can just re-use */
/* XXX: what about if the encryption key changes? */
- if (film()->three_d()) {
- _picture_asset = make_shared<dcp::StereoPictureAsset>(asset);
+ auto new_asset_filename = _output_dir / existing_asset_filename->filename();
+ if (new_asset_filename != *existing_asset_filename) {
+ dcp::filesystem::copy(*existing_asset_filename, new_asset_filename);
+ remembered_assets.push_back(RememberedAsset(new_asset_filename, period, film()->video_identifier()));
+ }
+ film()->write_remembered_assets(remembered_assets);
+
+ if (film()->video_encoding() == VideoEncoding::JPEG2000) {
+ if (film()->three_d()) {
+ _j2k_picture_asset = make_shared<dcp::StereoJ2KPictureAsset>(new_asset_filename);
+ } else {
+ _j2k_picture_asset = make_shared<dcp::MonoJ2KPictureAsset>(new_asset_filename);
+ }
} else {
- _picture_asset = make_shared<dcp::MonoPictureAsset>(asset);
+ _mpeg2_picture_asset = make_shared<dcp::MonoMPEG2PictureAsset>(new_asset_filename);
}
}
@@ -210,55 +238,6 @@ ReelWriter::ReelWriter (
film()->limit_to_smpte_bv20() ? dcp::SoundAsset::MCASubDescriptors::DISABLED : dcp::SoundAsset::MCASubDescriptors::ENABLED
);
}
-
- _default_font = dcp::ArrayData(default_font_file());
-}
-
-
-/** @param frame reel-relative frame */
-void
-ReelWriter::write_frame_info (Frame frame, Eyes eyes, dcp::FrameInfo info) const
-{
- auto handle = film()->info_file_handle(_period, false);
- handle->get().seek(frame_info_position(frame, eyes), SEEK_SET);
- handle->get().checked_write(&info.offset, sizeof(info.offset));
- handle->get().checked_write(&info.size, sizeof(info.size));
- handle->get().checked_write(info.hash.c_str(), info.hash.size());
-}
-
-
-dcp::FrameInfo
-ReelWriter::read_frame_info (shared_ptr<InfoFileHandle> info, Frame frame, Eyes eyes) const
-{
- dcp::FrameInfo frame_info;
- info->get().seek(frame_info_position(frame, eyes), SEEK_SET);
- info->get().checked_read(&frame_info.offset, sizeof(frame_info.offset));
- info->get().checked_read(&frame_info.size, sizeof(frame_info.size));
-
- char hash_buffer[33];
- info->get().checked_read(hash_buffer, 32);
- hash_buffer[32] = '\0';
- frame_info.hash = hash_buffer;
-
- return frame_info;
-}
-
-
-long
-ReelWriter::frame_info_position (Frame frame, Eyes eyes) const
-{
- switch (eyes) {
- case Eyes::BOTH:
- return frame * _info_size;
- case Eyes::LEFT:
- return frame * _info_size * 2;
- case Eyes::RIGHT:
- return frame * _info_size * 2 + _info_size;
- default:
- DCPOMATIC_ASSERT (false);
- }
-
- DCPOMATIC_ASSERT (false);
}
@@ -290,8 +269,8 @@ ReelWriter::check_existing_picture_asset (boost::filesystem::path asset)
}
/* Offset of the last dcp::FrameInfo in the info file */
- int const n = (dcp::filesystem::file_size(info_file->get().path()) / _info_size) - 1;
- LOG_GENERAL ("The last FI is %1; info file is %2, info size %3", n, dcp::filesystem::file_size(info_file->get().path()), _info_size);
+ int const n = (dcp::filesystem::file_size(info_file->get().path()) / J2KFrameInfo::size_on_disk()) - 1;
+ LOG_GENERAL("The last FI is %1; info file is %2, info size %3", n, dcp::filesystem::file_size(info_file->get().path()), J2KFrameInfo::size_on_disk())
Frame first_nonexistent_frame;
if (film()->three_d()) {
@@ -321,13 +300,13 @@ ReelWriter::check_existing_picture_asset (boost::filesystem::path asset)
void
ReelWriter::write (shared_ptr<const Data> encoded, Frame frame, Eyes eyes)
{
- if (!_picture_asset_writer) {
+ if (!_j2k_picture_asset_writer) {
/* We're not writing any data */
return;
}
- auto fin = _picture_asset_writer->write (encoded->data(), encoded->size());
- write_frame_info (frame, eyes, fin);
+ auto fin = J2KFrameInfo(_j2k_picture_asset_writer->write(encoded->data(), encoded->size()));
+ fin.write(film()->info_file_handle(_period, false), frame, eyes);
_last_written[eyes] = encoded;
}
@@ -349,37 +328,50 @@ ReelWriter::write (shared_ptr<const dcp::AtmosFrame> atmos, AtmosMetadata metada
void
-ReelWriter::fake_write (int size)
+ReelWriter::write(shared_ptr<dcp::MonoMPEG2PictureFrame> image)
{
- if (!_picture_asset_writer) {
+ _mpeg2_picture_asset_writer->write(image->data(), image->size());
+}
+
+
+void
+ReelWriter::fake_write(dcp::J2KFrameInfo const& info)
+{
+ if (!_j2k_picture_asset_writer) {
/* We're not writing any data */
return;
}
- _picture_asset_writer->fake_write (size);
+ _j2k_picture_asset_writer->fake_write(info);
}
void
ReelWriter::repeat_write (Frame frame, Eyes eyes)
{
- if (!_picture_asset_writer) {
+ if (!_j2k_picture_asset_writer) {
/* We're not writing any data */
return;
}
- auto fin = _picture_asset_writer->write(_last_written[eyes]->data(), _last_written[eyes]->size());
- write_frame_info (frame, eyes, fin);
+ auto fin = J2KFrameInfo(_j2k_picture_asset_writer->write(_last_written[eyes]->data(), _last_written[eyes]->size()));
+ fin.write(film()->info_file_handle(_period, false), frame, eyes);
}
void
ReelWriter::finish (boost::filesystem::path output_dcp)
{
- if (_picture_asset_writer && !_picture_asset_writer->finalize ()) {
- /* Nothing was written to the picture asset */
- LOG_GENERAL ("Nothing was written to reel %1 of %2", _reel_index, _reel_count);
- _picture_asset.reset ();
+ if (_j2k_picture_asset_writer && !_j2k_picture_asset_writer->finalize()) {
+ /* Nothing was written to the J2K picture asset */
+ LOG_GENERAL("Nothing was written to J2K asset for reel %1 of %2", _reel_index, _reel_count);
+ _j2k_picture_asset.reset();
+ }
+
+ if (_mpeg2_picture_asset_writer && !_mpeg2_picture_asset_writer->finalize()) {
+ /* Nothing was written to the MPEG2 picture asset */
+ LOG_GENERAL("Nothing was written to MPEG2 asset for reel %1 of %2", _reel_index, _reel_count);
+ _mpeg2_picture_asset.reset();
}
if (_sound_asset_writer && !_sound_asset_writer->finalize ()) {
@@ -387,42 +379,6 @@ ReelWriter::finish (boost::filesystem::path output_dcp)
_sound_asset.reset ();
}
- /* Hard-link any video asset file into the DCP */
- if (_picture_asset) {
- DCPOMATIC_ASSERT (_picture_asset->file());
- boost::filesystem::path video_from = _picture_asset->file().get();
- boost::filesystem::path video_to = output_dcp;
- video_to /= video_asset_filename (_picture_asset, _reel_index, _reel_count, _content_summary);
- /* There may be an existing "to" file if we are recreating a DCP in the same place without
- changing any video.
- */
- boost::system::error_code ec;
- dcp::filesystem::remove(video_to, ec);
-
- dcp::filesystem::create_hard_link(video_from, video_to, ec);
- if (ec) {
- LOG_WARNING("Hard-link failed (%1); copying instead", error_details(ec));
- auto job = _job.lock ();
- if (job) {
- job->sub (_("Copying video file into DCP"));
- try {
- copy_in_bits (video_from, video_to, bind(&Job::set_progress, job.get(), _1, false));
- } catch (exception& e) {
- LOG_ERROR ("Failed to copy video file from %1 to %2 (%3)", video_from.string(), video_to.string(), e.what());
- throw FileError (e.what(), video_from);
- }
- } else {
- dcp::filesystem::copy_file(video_from, video_to, ec);
- if (ec) {
- LOG_ERROR("Failed to copy video file from %1 to %2 (%3)", video_from.string(), video_to.string(), error_details(ec));
- throw FileError (ec.message(), video_from);
- }
- }
- }
-
- _picture_asset->set_file (video_to);
- }
-
/* Move the audio asset into the DCP */
if (_sound_asset) {
boost::filesystem::path audio_to = output_dcp;
@@ -544,17 +500,17 @@ ReelWriter::create_reel_picture (shared_ptr<dcp::Reel> reel, list<ReferencedReel
{
shared_ptr<dcp::ReelPictureAsset> reel_asset;
- if (_picture_asset) {
+ if (_j2k_picture_asset) {
/* We have made a picture asset of our own. Put it into the reel */
- auto mono = dynamic_pointer_cast<dcp::MonoPictureAsset> (_picture_asset);
- if (mono) {
+ if (auto mono = dynamic_pointer_cast<dcp::MonoJ2KPictureAsset>(_j2k_picture_asset)) {
reel_asset = make_shared<dcp::ReelMonoPictureAsset>(mono, 0);
}
- auto stereo = dynamic_pointer_cast<dcp::StereoPictureAsset> (_picture_asset);
- if (stereo) {
+ if (auto stereo = dynamic_pointer_cast<dcp::StereoJ2KPictureAsset>(_j2k_picture_asset)) {
reel_asset = make_shared<dcp::ReelStereoPictureAsset>(stereo, 0);
}
+ } else if (_mpeg2_picture_asset) {
+ reel_asset = make_shared<dcp::ReelMonoPictureAsset>(_mpeg2_picture_asset, 0);
} else {
LOG_GENERAL ("no picture asset of our own; look through %1", refs.size());
/* We don't have a picture asset of our own; hopefully we have one to reference */
@@ -783,8 +739,11 @@ try
{
vector<shared_ptr<const dcp::Asset>> assets;
- if (_picture_asset) {
- assets.push_back(_picture_asset);
+ if (_j2k_picture_asset) {
+ assets.push_back(_j2k_picture_asset);
+ }
+ if (_mpeg2_picture_asset) {
+ assets.push_back(_mpeg2_picture_asset);
}
if (_sound_asset) {
assets.push_back(_sound_asset);
@@ -1012,7 +971,7 @@ ReelWriter::existing_picture_frame_ok (dcp::File& asset_file, shared_ptr<InfoFil
/* Read the data from the info file; for 3D we just check the left
frames until we find a good one.
*/
- auto const info = read_frame_info (info_file, frame, film()->three_d() ? Eyes::LEFT : Eyes::BOTH);
+ auto const info = J2KFrameInfo(info_file, frame, film()->three_d() ? Eyes::LEFT : Eyes::BOTH);
bool ok = true;
diff --git a/src/lib/reel_writer.h b/src/lib/reel_writer.h
index c9052c832..0b243b264 100644
--- a/src/lib/reel_writer.h
+++ b/src/lib/reel_writer.h
@@ -27,10 +27,14 @@
#include "player_text.h"
#include "referenced_reel_asset.h"
#include "render_text.h"
+#include "text_type.h"
#include "weak_film.h"
#include <dcp/atmos_asset_writer.h>
#include <dcp/file.h>
-#include <dcp/picture_asset_writer.h>
+#include <dcp/ffmpeg_image.h>
+#include <dcp/j2k_picture_asset_writer.h>
+#include <dcp/mono_mpeg2_picture_frame.h>
+#include <dcp/mpeg2_picture_asset_writer.h>
class AudioBuffers;
@@ -41,17 +45,18 @@ struct write_frame_info_test;
namespace dcp {
class AtmosAsset;
- class MonoPictureAsset;
- class MonoPictureAssetWriter;
- class PictureAsset;
- class PictureAssetWriter;
+ class MonoJ2KPictureAsset;
+ class MonoJ2KPictureAssetWriter;
+ class J2KPictureAsset;
+ class J2KPictureAssetWriter;
+ class MPEG2PictureAsset;
class Reel;
class ReelAsset;
class ReelPictureAsset;
class SoundAsset;
class SoundAssetWriter;
- class StereoPictureAsset;
- class StereoPictureAssetWriter;
+ class StereoJ2KPictureAsset;
+ class StereoJ2KPictureAssetWriter;
class SubtitleAsset;
}
@@ -65,15 +70,17 @@ public:
std::shared_ptr<Job> job,
int reel_index,
int reel_count,
- bool text_only
+ bool text_only,
+ boost::filesystem::path output_dir
);
void write (std::shared_ptr<const dcp::Data> encoded, Frame frame, Eyes eyes);
- void fake_write (int size);
+ void fake_write(dcp::J2KFrameInfo const& info);
void repeat_write (Frame frame, Eyes eyes);
void write (std::shared_ptr<const AudioBuffers> audio);
void write(PlayerText text, TextType type, boost::optional<DCPTextTrack> track, dcpomatic::DCPTimePeriod period, FontIdMap const& fonts, std::shared_ptr<dcpomatic::Font> chosen_interop_font);
void write (std::shared_ptr<const dcp::AtmosFrame> atmos, AtmosMetadata metadata);
+ void write(std::shared_ptr<dcp::MonoMPEG2PictureFrame> image);
void finish (boost::filesystem::path output_dcp);
std::shared_ptr<dcp::Reel> create_reel (
@@ -94,14 +101,10 @@ public:
return _first_nonexistent_frame;
}
- dcp::FrameInfo read_frame_info (std::shared_ptr<InfoFileHandle> info, Frame frame, Eyes eyes) const;
-
private:
friend struct ::write_frame_info_test;
- void write_frame_info (Frame frame, Eyes eyes, dcp::FrameInfo info) const;
- long frame_info_position (Frame frame, Eyes eyes) const;
Frame check_existing_picture_asset (boost::filesystem::path asset);
bool existing_picture_frame_ok (dcp::File& asset_file, std::shared_ptr<InfoFileHandle> info_file, Frame frame) const;
std::shared_ptr<dcp::SubtitleAsset> empty_text_asset (TextType type, boost::optional<DCPTextTrack> track, bool with_dummy) const;
@@ -119,9 +122,10 @@ private:
void create_reel_markers (std::shared_ptr<dcp::Reel> reel) const;
float convert_vertical_position(StringText const& subtitle, dcp::SubtitleStandard to) const;
+ boost::filesystem::path _output_dir;
dcpomatic::DCPTimePeriod _period;
/** the first picture frame index that does not already exist in our MXF */
- int _first_nonexistent_frame;
+ int _first_nonexistent_frame = 0;
/** the data of the last written frame, if there is one */
EnumIndexedVector<std::shared_ptr<const dcp::Data>, Eyes> _last_written;
/** index of this reel within the DCP (starting from 0) */
@@ -134,9 +138,11 @@ private:
dcp::ArrayData _default_font;
- std::shared_ptr<dcp::PictureAsset> _picture_asset;
+ std::shared_ptr<dcp::J2KPictureAsset> _j2k_picture_asset;
+ std::shared_ptr<dcp::MPEG2PictureAsset> _mpeg2_picture_asset;
/** picture asset writer, or 0 if we are not writing any picture because we already have one */
- std::shared_ptr<dcp::PictureAssetWriter> _picture_asset_writer;
+ std::shared_ptr<dcp::J2KPictureAssetWriter> _j2k_picture_asset_writer;
+ std::shared_ptr<dcp::MPEG2PictureAssetWriter> _mpeg2_picture_asset_writer;
std::shared_ptr<dcp::SoundAsset> _sound_asset;
std::shared_ptr<dcp::SoundAssetWriter> _sound_asset_writer;
std::shared_ptr<dcp::SubtitleAsset> _subtitle_asset;
@@ -145,6 +151,4 @@ private:
std::shared_ptr<dcp::AtmosAssetWriter> _atmos_asset_writer;
mutable FontMetrics _font_metrics;
-
- static int const _info_size;
};
diff --git a/src/lib/release_notes.cc b/src/lib/release_notes.cc
index d69738427..b12ebc030 100644
--- a/src/lib/release_notes.cc
+++ b/src/lib/release_notes.cc
@@ -21,6 +21,7 @@
#include "config.h"
#include "release_notes.h"
+#include "variant.h"
#include "version.h"
#include "i18n.h"
@@ -46,7 +47,7 @@ find_release_notes(bool dark, optional<string> current)
string const colour = dark ? "white" : "black";
auto const span = String::compose("<span style=\"color: %1\">", colour);
- const string header = String::compose("<h1>%1DCP-o-matic %2 release notes</span></h1>", span, *current);
+ const string header = String::compose("<h1>%1%2 %3 release notes</span></h1>", span, variant::dcpomatic(), *current);
if (!last) {
return header + span +
diff --git a/src/lib/remembered_asset.cc b/src/lib/remembered_asset.cc
new file mode 100644
index 000000000..20b65970c
--- /dev/null
+++ b/src/lib/remembered_asset.cc
@@ -0,0 +1,97 @@
+/*
+ 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 "dcpomatic_assert.h"
+#include "remembered_asset.h"
+#include <dcp/filesystem.h>
+#include <dcp/raw_convert.h>
+#include <libcxml/cxml.h>
+LIBDCP_DISABLE_WARNINGS
+#include <libxml++/libxml++.h>
+LIBDCP_ENABLE_WARNINGS
+
+
+using std::string;
+using std::vector;
+
+
+RememberedAsset::RememberedAsset(cxml::ConstNodePtr node)
+{
+ _filename = node->string_child("Filename");
+ auto period_node = node->node_child("Period");
+ DCPOMATIC_ASSERT(period_node);
+
+ _period = {
+ dcpomatic::DCPTime(period_node->number_child<int64_t>("From")),
+ dcpomatic::DCPTime(period_node->number_child<int64_t>("To"))
+ };
+
+ _identifier = node->string_child("Identifier");
+}
+
+
+void
+RememberedAsset::as_xml(xmlpp::Element* parent) const
+{
+ cxml::add_text_child(parent, "Filename", _filename.string());
+ auto period_node = cxml::add_child(parent, "Period");
+ cxml::add_text_child(period_node, "From", dcp::raw_convert<string>(_period.from.get()));
+ cxml::add_text_child(period_node, "To", dcp::raw_convert<string>(_period.to.get()));
+ cxml::add_text_child(parent, "Identifier", _identifier);
+}
+
+
+boost::optional<boost::filesystem::path>
+find_asset(vector<RememberedAsset> const& assets, boost::filesystem::path directory, dcpomatic::DCPTimePeriod period, string identifier)
+{
+ for (auto path: dcp::filesystem::recursive_directory_iterator(directory)) {
+ auto iter = std::find_if(assets.begin(), assets.end(), [period, identifier, path](RememberedAsset const& asset) {
+ return asset.filename() == path.path().filename() && asset.period() == period && asset.identifier() == identifier;
+ });
+ if (iter != assets.end()) {
+ return path.path();
+ }
+ }
+
+ return {};
+}
+
+
+void
+clean_up_asset_directory(boost::filesystem::path directory)
+{
+ /* We could do something more advanced here (e.g. keep the last N assets) but for now
+ * let's just clean the whole thing out.
+ */
+ boost::system::error_code ec;
+ dcp::filesystem::remove_all(directory, ec);
+}
+
+
+void
+preserve_assets(boost::filesystem::path search, boost::filesystem::path assets_path)
+{
+ for (auto const& path: boost::filesystem::directory_iterator(search)) {
+ if (path.path().extension() == ".mxf") {
+ dcp::filesystem::rename(path.path(), assets_path / path.path().filename());
+ }
+ }
+}
diff --git a/src/lib/remembered_asset.h b/src/lib/remembered_asset.h
new file mode 100644
index 000000000..ec9344e24
--- /dev/null
+++ b/src/lib/remembered_asset.h
@@ -0,0 +1,79 @@
+/*
+ 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/>.
+
+*/
+
+
+#ifndef DCPOMATIC_REMEMBERED_ASSET_H
+#define DCPOMATIC_REMEMBERED_ASSET_H
+
+
+#include "dcpomatic_time.h"
+#include <libcxml/cxml.h>
+#include <dcp/warnings.h>
+LIBDCP_DISABLE_WARNINGS
+#include <libxml++/libxml++.h>
+LIBDCP_ENABLE_WARNINGS
+#include <boost/filesystem.hpp>
+
+
+class RememberedAsset
+{
+public:
+ explicit RememberedAsset(cxml::ConstNodePtr node);
+
+ RememberedAsset(boost::filesystem::path filename, dcpomatic::DCPTimePeriod period, std::string identifier)
+ : _filename(filename)
+ , _period(period)
+ , _identifier(std::move(identifier))
+ {}
+
+ void as_xml(xmlpp::Element* parent) const;
+
+ boost::filesystem::path filename() const {
+ return _filename;
+ }
+
+ dcpomatic::DCPTimePeriod period() const {
+ return _period;
+ }
+
+ std::string identifier() const {
+ return _identifier;
+ }
+
+private:
+ boost::filesystem::path _filename;
+ dcpomatic::DCPTimePeriod _period;
+ std::string _identifier;
+};
+
+
+boost::optional<boost::filesystem::path> find_asset(
+ std::vector<RememberedAsset> const& assets, boost::filesystem::path directory, dcpomatic::DCPTimePeriod period, std::string identifier
+ );
+
+
+void clean_up_asset_directory(boost::filesystem::path directory);
+
+
+void preserve_assets(boost::filesystem::path search, boost::filesystem::path assets_path);
+
+
+#endif
+
diff --git a/src/lib/remote_j2k_encoder_thread.cc b/src/lib/remote_j2k_encoder_thread.cc
new file mode 100644
index 000000000..49d80953d
--- /dev/null
+++ b/src/lib/remote_j2k_encoder_thread.cc
@@ -0,0 +1,84 @@
+/*
+ Copyright (C) 2023 Carl Hetherington <cth@carlh.net>
+
+ This file is part of DCP-o-matic.
+
+ DCP-o-matic is free software; you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation; either version 2 of the License, or
+ (at your option) any later version.
+
+ DCP-o-matic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with DCP-o-matic. If not, see <http://www.gnu.org/licenses/>.
+
+*/
+
+
+#include "dcp_video.h"
+#include "dcpomatic_log.h"
+#include "j2k_encoder.h"
+#include "remote_j2k_encoder_thread.h"
+#include "util.h"
+
+#include "i18n.h"
+
+
+using std::make_shared;
+using std::shared_ptr;
+
+
+RemoteJ2KEncoderThread::RemoteJ2KEncoderThread(J2KEncoder& encoder, EncodeServerDescription server)
+ : J2KSyncEncoderThread(encoder)
+ , _server(server)
+{
+
+}
+
+
+void
+RemoteJ2KEncoderThread::log_thread_start() const
+{
+ start_of_thread("RemoteJ2KEncoder");
+ LOG_TIMING("start-encoder-thread thread=%1 server=%2", thread_id(), _server.host_name());
+}
+
+
+shared_ptr<dcp::ArrayData>
+RemoteJ2KEncoderThread::encode(DCPVideo const& frame)
+{
+ shared_ptr<dcp::ArrayData> encoded;
+
+ try {
+ encoded = make_shared<dcp::ArrayData>(frame.encode_remotely(_server));
+ if (_remote_backoff > 0) {
+ LOG_GENERAL("%1 was lost, but now she is found; removing backoff", _server.host_name());
+ _remote_backoff = 0;
+ }
+ } catch (std::exception& e) {
+ LOG_ERROR(
+ N_("Remote encode of %1 on %2 failed (%3); thread sleeping for %4s"),
+ frame.index(), _server.host_name(), e.what(), _remote_backoff
+ );
+ } catch (...) {
+ LOG_ERROR(
+ N_("Remote encode of %1 on %2 failed; thread sleeping for %4s"),
+ frame.index(), _server.host_name(), _remote_backoff
+ );
+ }
+
+ if (!encoded) {
+ if (_remote_backoff < 60) {
+ /* back off more */
+ _remote_backoff += 10;
+ }
+ boost::this_thread::sleep(boost::posix_time::seconds(_remote_backoff));
+ }
+
+ return encoded;
+}
+
diff --git a/src/lib/remote_j2k_encoder_thread.h b/src/lib/remote_j2k_encoder_thread.h
new file mode 100644
index 000000000..f3fe7f94a
--- /dev/null
+++ b/src/lib/remote_j2k_encoder_thread.h
@@ -0,0 +1,21 @@
+#include "encode_server_description.h"
+#include "j2k_sync_encoder_thread.h"
+
+
+class RemoteJ2KEncoderThread : public J2KSyncEncoderThread
+{
+public:
+ RemoteJ2KEncoderThread(J2KEncoder& encoder, EncodeServerDescription server);
+
+ void log_thread_start() const override;
+ std::shared_ptr<dcp::ArrayData> encode(DCPVideo const& frame) override;
+
+ EncodeServerDescription server() const {
+ return _server;
+ }
+
+private:
+ EncodeServerDescription _server;
+ /** Number of seconds that we currently wait between attempts to connect to the server */
+ int _remote_backoff = 0;
+};
diff --git a/src/lib/render_text.cc b/src/lib/render_text.cc
index 870f3045d..41652d32d 100644
--- a/src/lib/render_text.cc
+++ b/src/lib/render_text.cc
@@ -31,6 +31,7 @@
#include <cairomm/cairomm.h>
LIBDCP_DISABLE_WARNINGS
#include <pangomm.h>
+#include <pangommconfig.h>
LIBDCP_ENABLE_WARNINGS
#include <pango/pangocairo.h>
#include <boost/algorithm/string.hpp>
@@ -51,6 +52,15 @@ using boost::optional;
using namespace dcpomatic;
+#if CAIROMM_MAJOR_VERSION == 1 && CAIROMM_MINOR_VERSION <= 14
+#define DCPOMATIC_OLD_CAIROMM_API
+#endif
+
+#if PANGOMM_MAJOR_VERSION == 2 && PANGOMM_MINOR_VERSION <= 46
+#define DCPOMATIC_OLD_PANGOMM_API
+#endif
+
+
/** Create a Pango layout using a dummy context which we can use to calculate the size
* of the text we will render. Then we can transfer the layout over to the real context
* for the actual render.
@@ -62,11 +72,25 @@ create_layout(string font_name, string markup)
DCPOMATIC_ASSERT (c_font_map);
auto font_map = Glib::wrap (c_font_map);
auto c_context = pango_font_map_create_context (c_font_map);
+
+ cairo_font_options_t *options = cairo_font_options_create();
+ /* CAIRO_ANTIALIAS_BEST is totally broken here: see e.g.
+ * https://gitlab.freedesktop.org/cairo/cairo/-/issues/152
+ */
+ cairo_font_options_set_antialias(options, CAIRO_ANTIALIAS_GOOD);
+ cairo_font_options_set_hint_style(options, CAIRO_HINT_STYLE_FULL);
+ pango_cairo_context_set_font_options(c_context, options);
+ cairo_font_options_destroy(options);
+
DCPOMATIC_ASSERT (c_context);
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 +187,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,24 +429,19 @@ 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 ();
}
/* The actual subtitle */
-
- set_source_rgba (context, first.colour(), fade_factor);
-
- context->move_to (x_offset, y_offset);
- layout.pango->add_to_cairo_context (context);
- context->fill ();
-
- context->set_line_width (0.5);
context->move_to (x_offset, y_offset);
- layout.pango->add_to_cairo_context (context);
- context->stroke ();
+ layout.pango->show_in_cairo_context(context);
int const x = x_position(first.h_align(), first.h_position(), target.width, layout.size.width);
int const y = y_position(first.valign_standard, first.v_align(), first.v_position(), target.height, layout.baseline_to_bottom(border_width), layout.size.height);
diff --git a/src/lib/rgba.cc b/src/lib/rgba.cc
index 2e6a4fab6..2e0408d8a 100644
--- a/src/lib/rgba.cc
+++ b/src/lib/rgba.cc
@@ -41,12 +41,12 @@ RGBA::RGBA (cxml::ConstNodePtr node)
void
-RGBA::as_xml (xmlpp::Node* parent) const
+RGBA::as_xml(xmlpp::Element* parent) const
{
- parent->add_child("R")->add_child_text(lexical_cast<string>(int(r)));
- parent->add_child("G")->add_child_text(lexical_cast<string>(int(g)));
- parent->add_child("B")->add_child_text(lexical_cast<string>(int(b)));
- parent->add_child("A")->add_child_text(lexical_cast<string>(int(a)));
+ cxml::add_text_child(parent, "R", lexical_cast<string>(int(r)));
+ cxml::add_text_child(parent, "G", lexical_cast<string>(int(g)));
+ cxml::add_text_child(parent, "B", lexical_cast<string>(int(b)));
+ cxml::add_text_child(parent, "A", lexical_cast<string>(int(a)));
}
diff --git a/src/lib/rgba.h b/src/lib/rgba.h
index 96fed710e..a582a4ca1 100644
--- a/src/lib/rgba.h
+++ b/src/lib/rgba.h
@@ -44,7 +44,7 @@ public:
explicit RGBA (cxml::ConstNodePtr node);
- void as_xml (xmlpp::Node* parent) const;
+ void as_xml(xmlpp::Element* parent) const;
uint8_t r = 0;
uint8_t g = 0;
diff --git a/src/lib/screen.cc b/src/lib/screen.cc
index febf9085c..b77eb6b52 100644
--- a/src/lib/screen.cc
+++ b/src/lib/screen.cc
@@ -20,6 +20,7 @@
#include "cinema.h"
+#include "cinema_list.h"
#include "config.h"
#include "film.h"
#include "kdm_util.h"
@@ -39,29 +40,6 @@ using boost::optional;
using namespace dcpomatic;
-Screen::Screen (cxml::ConstNodePtr node)
- : KDMRecipient (node)
-{
- for (auto i: node->node_children ("TrustedDevice")) {
- if (boost::algorithm::starts_with(i->content(), "-----BEGIN CERTIFICATE-----")) {
- trusted_devices.push_back (TrustedDevice(dcp::Certificate(i->content())));
- } else {
- trusted_devices.push_back (TrustedDevice(i->content()));
- }
- }
-}
-
-
-void
-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());
- }
-}
-
-
vector<string>
Screen::trusted_device_thumbprints () const
{
@@ -76,46 +54,40 @@ Screen::trusted_device_thumbprints () const
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,
+ CinemaID cinema_id,
+ Cinema const& cinema,
+ Screen const& screen,
+ dcp::LocalTime valid_from,
+ dcp::LocalTime valid_to,
dcp::Formulation formulation,
bool disable_forensic_marking_picture,
optional<int> disable_forensic_marking_audio,
vector<KDMCertificatePeriod>& period_checks
)
{
- if (!screen->recipient) {
+ if (!screen.recipient) {
return {};
}
- 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.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(
- signer, screen->recipient.get(), screen->trusted_device_thumbprints(), formulation, disable_forensic_marking_picture, disable_forensic_marking_audio
+ 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
);
dcp::NameFormat::Map name_values;
- if (cinema) {
- name_values['c'] = cinema->name;
- } else {
- name_values['c'] = "";
- }
- name_values['s'] = screen->name;
+ name_values['c'] = cinema.name;
+ 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);
+ return make_shared<KDMWithMetadata>(name_values, cinema_id, cinema.emails, kdm);
}
diff --git a/src/lib/screen.h b/src/lib/screen.h
index 7f01cdf27..89ebc3ab4 100644
--- a/src/lib/screen.h
+++ b/src/lib/screen.h
@@ -23,12 +23,14 @@
#define DCPOMATIC_SCREEN_H
-#include "kdm_with_metadata.h"
+#include "cinema_list.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>
@@ -62,12 +64,7 @@ public:
, trusted_devices (trusted_devices_)
{}
- explicit Screen (cxml::ConstNodePtr);
-
- void as_xml (xmlpp::Element *) const override;
std::vector<std::string> trusted_device_thumbprints () const;
-
- std::shared_ptr<Cinema> cinema;
std::vector<TrustedDevice> trusted_devices;
};
@@ -77,9 +74,11 @@ public:
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,
+ CinemaID cinema_id,
+ Cinema const& cinema,
+ dcpomatic::Screen const& screen,
+ dcp::LocalTime valid_from,
+ dcp::LocalTime valid_to,
dcp::Formulation formulation,
bool disable_forensic_marking_picture,
boost::optional<int> disable_forensic_marking_audio,
diff --git a/src/lib/send_problem_report_job.cc b/src/lib/send_problem_report_job.cc
index 9569aca3b..ed78112e9 100644
--- a/src/lib/send_problem_report_job.cc
+++ b/src/lib/send_problem_report_job.cc
@@ -19,15 +19,16 @@
*/
-#include "send_problem_report_job.h"
#include "compose.hpp"
-#include "film.h"
#include "cross.h"
+#include "email.h"
+#include "environment_info.h"
+#include "film.h"
#include "film.h"
#include "log.h"
+#include "send_problem_report_job.h"
+#include "variant.h"
#include "version.h"
-#include "email.h"
-#include "environment_info.h"
#include <libxml++/libxml++.h>
#include "i18n.h"
@@ -108,7 +109,7 @@ SendProblemReportJob::run ()
body += "---<8----\n";
}
- Email email(_from, {"carl@dcpomatic.com"}, "DCP-o-matic problem report", body);
+ Email email(_from, {"carl@dcpomatic.com"}, variant::insert_dcpomatic("%1 problem report"), body);
email.send("main.carlh.net", 2525, EmailProtocol::STARTTLS);
set_progress (1);
diff --git a/src/lib/shuffler.cc b/src/lib/shuffler.cc
index 5a4faf4d1..a4ea0f5dc 100644
--- a/src/lib/shuffler.cc
+++ b/src/lib/shuffler.cc
@@ -40,8 +40,8 @@ int const Shuffler::_max_size = 64;
struct Comparator
{
bool operator()(Shuffler::Store const & a, Shuffler::Store const & b) {
- if (a.second.frame != b.second.frame) {
- return a.second.frame < b.second.frame;
+ if (a.second.time != b.second.time) {
+ return a.second.time < b.second.time;
}
return a.second.eyes < b.second.eyes;
}
@@ -51,7 +51,7 @@ struct Comparator
void
Shuffler::video (weak_ptr<Piece> weak_piece, ContentVideo video)
{
- LOG_DEBUG_THREE_D ("Shuffler::video frame=%1 eyes=%2 part=%3", video.frame, static_cast<int>(video.eyes), static_cast<int>(video.part));
+ LOG_DEBUG_THREE_D("Shuffler::video time=%1 eyes=%2 part=%3", to_string(video.time), static_cast<int>(video.eyes), static_cast<int>(video.part));
if (video.eyes != Eyes::LEFT && video.eyes != Eyes::RIGHT) {
/* Pass through anything that we don't care about */
@@ -79,13 +79,13 @@ Shuffler::video (weak_ptr<Piece> weak_piece, ContentVideo video)
!_store.empty() &&
_last &&
(
- (_store.front().second.frame == _last->frame && _store.front().second.eyes == Eyes::RIGHT && _last->eyes == Eyes::LEFT) ||
- (_store.front().second.frame >= (_last->frame + 1) && _store.front().second.eyes == Eyes::LEFT && _last->eyes == Eyes::RIGHT)
+ (_store.front().second.time == _last->time && _store.front().second.eyes == Eyes::RIGHT && _last->eyes == Eyes::LEFT) ||
+ (_store.front().second.time > _last->time && _store.front().second.eyes == Eyes::LEFT && _last->eyes == Eyes::RIGHT)
);
if (!store_front_in_sequence) {
- string const store = _store.empty() ? "store empty" : String::compose("store front frame=%1 eyes=%2", _store.front().second.frame, static_cast<int>(_store.front().second.eyes));
- string const last = _last ? String::compose("last frame=%1 eyes=%2", _last->frame, static_cast<int>(_last->eyes)) : "no last";
+ string const store = _store.empty() ? "store empty" : String::compose("store front time=%1 eyes=%2", to_string(_store.front().second.time), static_cast<int>(_store.front().second.eyes));
+ string const last = _last ? String::compose("last time=%1 eyes=%2", to_string(_last->time), static_cast<int>(_last->eyes)) : "no last";
LOG_DEBUG_THREE_D("Shuffler not in sequence: %1 %2", store, last);
}
@@ -98,10 +98,10 @@ Shuffler::video (weak_ptr<Piece> weak_piece, ContentVideo video)
}
if (_store.size() > _max_size) {
- LOG_WARNING ("Shuffler is full after receiving frame %1; 3D sync may be incorrect.", video.frame);
+ LOG_WARNING("Shuffler is full after receiving frame at %1; 3D sync may be incorrect.", to_string(video.time));
}
- LOG_DEBUG_THREE_D("Shuffler emits frame=%1 eyes=%2 store=%3", _store.front().second.frame, static_cast<int>(_store.front().second.eyes), _store.size());
+ LOG_DEBUG_THREE_D("Shuffler emits time=%1 eyes=%2 store=%3", to_string(_store.front().second.time), static_cast<int>(_store.front().second.eyes), _store.size());
Video (_store.front().first, _store.front().second);
_last = _store.front().second;
_store.pop_front ();
diff --git a/src/lib/spl.cc b/src/lib/spl.cc
index f49f11a7d..e87465cdc 100644
--- a/src/lib/spl.cc
+++ b/src/lib/spl.cc
@@ -62,10 +62,10 @@ SPL::write (boost::filesystem::path path) const
{
xmlpp::Document doc;
auto root = doc.create_root_node ("SPL");
- root->add_child("Id")->add_child_text (_id);
- root->add_child("Name")->add_child_text (_name);
+ cxml::add_text_child(root, "Id", _id);
+ cxml::add_text_child(root, "Name", _name);
for (auto i: _spl) {
- i.as_xml (root->add_child("Entry"));
+ i.as_xml(cxml::add_child(root, "Entry"));
}
doc.write_to_file_formatted (path.string());
}
diff --git a/src/lib/spl_entry.cc b/src/lib/spl_entry.cc
index f0b377a56..a77b0b7d5 100644
--- a/src/lib/spl_entry.cc
+++ b/src/lib/spl_entry.cc
@@ -53,5 +53,5 @@ SPLEntry::SPLEntry (shared_ptr<Content> c)
void
SPLEntry::as_xml (xmlpp::Element* e)
{
- e->add_child("Digest")->add_child_text(digest);
+ cxml::add_text_child(e, "Digest", digest);
}
diff --git a/src/lib/sqlite_statement.cc b/src/lib/sqlite_statement.cc
new file mode 100644
index 000000000..b3ec1fb81
--- /dev/null
+++ b/src/lib/sqlite_statement.cc
@@ -0,0 +1,111 @@
+/*
+ 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 "exceptions.h"
+#include "sqlite_statement.h"
+
+
+using std::function;
+using std::string;
+
+
+SQLiteStatement::SQLiteStatement(sqlite3* db, string const& statement)
+ : _db(db)
+{
+#ifdef DCPOMATIC_HAVE_SQLITE3_PREPARE_V3
+ auto rc = sqlite3_prepare_v3(_db, statement.c_str(), -1, 0, &_stmt, nullptr);
+#else
+ auto rc = sqlite3_prepare_v2(_db, statement.c_str(), -1, &_stmt, nullptr);
+#endif
+ if (rc != SQLITE_OK) {
+ throw SQLError(_db, rc, statement);
+ }
+}
+
+
+SQLiteStatement::~SQLiteStatement()
+{
+ sqlite3_finalize(_stmt);
+}
+
+
+void
+SQLiteStatement::bind_text(int index, string const& value)
+{
+ auto rc = sqlite3_bind_text(_stmt, index, value.c_str(), -1, SQLITE_TRANSIENT);
+ if (rc != SQLITE_OK) {
+ throw SQLError(_db, rc);
+ }
+}
+
+
+void
+SQLiteStatement::bind_int64(int index, int64_t value)
+{
+ auto rc = sqlite3_bind_int64(_stmt, index, value);
+ if (rc != SQLITE_OK) {
+ throw SQLError(_db, rc);
+ }
+}
+
+
+void
+SQLiteStatement::execute(function<void(SQLiteStatement&)> row, function<void()> busy)
+{
+ while (true) {
+ auto const rc = sqlite3_step(_stmt);
+ switch (rc) {
+ case SQLITE_BUSY:
+ busy();
+ break;
+ case SQLITE_DONE:
+ return;
+ case SQLITE_ROW:
+ row(*this);
+ break;
+ case SQLITE_ERROR:
+ case SQLITE_MISUSE:
+ throw SQLError(_db, sqlite3_errmsg(_db));
+ }
+ }
+}
+
+
+int
+SQLiteStatement::data_count()
+{
+ return sqlite3_data_count(_stmt);
+}
+
+
+int64_t
+SQLiteStatement::column_int64(int index)
+{
+ return sqlite3_column_int64(_stmt, index);
+}
+
+
+string
+SQLiteStatement::column_text(int index)
+{
+ return reinterpret_cast<const char*>(sqlite3_column_text(_stmt, index));
+}
+
diff --git a/src/lib/sqlite_statement.h b/src/lib/sqlite_statement.h
new file mode 100644
index 000000000..3c2246efb
--- /dev/null
+++ b/src/lib/sqlite_statement.h
@@ -0,0 +1,50 @@
+/*
+ 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 <sqlite3.h>
+#include <functional>
+#include <string>
+
+
+class SQLiteStatement
+{
+public:
+ SQLiteStatement(sqlite3* db, std::string const& statement);
+ ~SQLiteStatement();
+
+ SQLiteStatement(SQLiteStatement const&) = delete;
+ SQLiteStatement& operator=(SQLiteStatement const&) = delete;
+
+ void bind_text(int index, std::string const& value);
+ void bind_int64(int index, int64_t value);
+
+ int64_t column_int64(int index);
+ std::string column_text(int index);
+
+ void execute(std::function<void(SQLiteStatement&)> row = std::function<void(SQLiteStatement& statement)>(), std::function<void()> busy = std::function<void()>());
+
+ int data_count();
+
+private:
+ sqlite3* _db;
+ sqlite3_stmt* _stmt;
+};
+
diff --git a/src/lib/sqlite_table.cc b/src/lib/sqlite_table.cc
new file mode 100644
index 000000000..f00fa6b74
--- /dev/null
+++ b/src/lib/sqlite_table.cc
@@ -0,0 +1,79 @@
+/*
+ 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 "dcpomatic_assert.h"
+#include "compose.hpp"
+#include "sqlite_table.h"
+#include "util.h"
+
+
+using std::string;
+using std::vector;
+
+
+void
+SQLiteTable::add_column(string const& name, string const& type)
+{
+ _columns.push_back(name);
+ _types.push_back(type);
+}
+
+
+string
+SQLiteTable::create() const
+{
+ DCPOMATIC_ASSERT(!_columns.empty());
+ DCPOMATIC_ASSERT(_columns.size() == _types.size());
+ vector<string> columns(_columns.size());
+ for (size_t i = 0; i < _columns.size(); ++i) {
+ columns[i] = _columns[i] + " " + _types[i];
+ }
+ return String::compose("CREATE TABLE IF NOT EXISTS %1 (id INTEGER PRIMARY KEY, %2)", _name, join_strings(columns, ", "));
+}
+
+
+string
+SQLiteTable::insert() const
+{
+ DCPOMATIC_ASSERT(!_columns.empty());
+ vector<string> placeholders(_columns.size(), "?");
+ return String::compose("INSERT INTO %1 (%2) VALUES (%3)", _name, join_strings(_columns, ", "), join_strings(placeholders, ", "));
+}
+
+
+string
+SQLiteTable::update(string const& condition) const
+{
+ DCPOMATIC_ASSERT(!_columns.empty());
+ vector<string> placeholders(_columns.size());
+ for (size_t i = 0; i < _columns.size(); ++i) {
+ placeholders[i] = _columns[i] + "=?";
+ }
+
+ return String::compose("UPDATE %1 SET %2 %3", _name, join_strings(placeholders, ", "), condition);
+}
+
+
+string
+SQLiteTable::select(string const& condition) const
+{
+ return String::compose("SELECT id,%1 FROM %2 %3", join_strings(_columns, ","), _name, condition);
+}
diff --git a/src/lib/sqlite_table.h b/src/lib/sqlite_table.h
new file mode 100644
index 000000000..43c9491ed
--- /dev/null
+++ b/src/lib/sqlite_table.h
@@ -0,0 +1,54 @@
+/*
+ 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/>.
+
+*/
+
+
+#ifndef DCPOMATIC_SQLITE_TABLE_H
+#define DCPOMATIC_SQLITE_TABLE_H
+
+#include <string>
+#include <vector>
+
+
+class SQLiteTable
+{
+public:
+ SQLiteTable(std::string name)
+ : _name(std::move(name))
+ {}
+
+ SQLiteTable(SQLiteTable const&) = default;
+ SQLiteTable(SQLiteTable&&) = default;
+
+ void add_column(std::string const& name, std::string const& type);
+
+ std::string create() const;
+ std::string insert() const;
+ std::string update(std::string const& condition) const;
+ std::string select(std::string const& condition) const;
+
+private:
+ std::string _name;
+ std::vector<std::string> _columns;
+ std::vector<std::string> _types;
+};
+
+
+#endif
+
diff --git a/src/lib/sqlite_transaction.cc b/src/lib/sqlite_transaction.cc
new file mode 100644
index 000000000..239d85020
--- /dev/null
+++ b/src/lib/sqlite_transaction.cc
@@ -0,0 +1,50 @@
+/*
+ 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 "sqlite_statement.h"
+#include "sqlite_transaction.h"
+
+
+SQLiteTransaction::SQLiteTransaction(sqlite3* db)
+ : _db(db)
+{
+ SQLiteStatement statement(_db, "BEGIN TRANSACTION");
+ statement.execute();
+}
+
+
+SQLiteTransaction::~SQLiteTransaction()
+{
+ if (_rollback) {
+ SQLiteStatement rollback(_db, "ROLLBACK");
+ rollback.execute();
+ }
+}
+
+
+void
+SQLiteTransaction::commit()
+{
+ SQLiteStatement commit(_db, "COMMIT");
+ commit.execute();
+ _rollback = false;
+}
+
diff --git a/src/lib/sqlite_transaction.h b/src/lib/sqlite_transaction.h
new file mode 100644
index 000000000..0f6319243
--- /dev/null
+++ b/src/lib/sqlite_transaction.h
@@ -0,0 +1,40 @@
+/*
+ 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 <sqlite3.h>
+
+
+class SQLiteTransaction
+{
+public:
+ SQLiteTransaction(sqlite3* db);
+ ~SQLiteTransaction();
+
+ SQLiteTransaction(SQLiteTransaction const&) = delete;
+ SQLiteTransaction& operator=(SQLiteTransaction const&) = delete;
+
+ void commit();
+
+private:
+ sqlite3* _db;
+ bool _rollback = true;
+};
+
diff --git a/src/lib/state.cc b/src/lib/state.cc
index a8230385d..7a3232ee9 100644
--- a/src/lib/state.cc
+++ b/src/lib/state.cc
@@ -35,7 +35,7 @@ boost::optional<boost::filesystem::path> State::override_path;
/* List of config versions to look for in descending order of preference;
* i.e. look at the first one, and if that doesn't exist, try the second, etc.
*/
-static std::vector<std::string> config_versions = { "2.16" };
+static std::vector<std::string> config_versions = { "2.18", "2.16" };
static
diff --git a/src/lib/string_text_file_content.cc b/src/lib/string_text_file_content.cc
index d8c195be7..c6639936c 100644
--- a/src/lib/string_text_file_content.cc
+++ b/src/lib/string_text_file_content.cc
@@ -122,16 +122,16 @@ StringTextFileContent::technical_summary () const
void
-StringTextFileContent::as_xml (xmlpp::Node* node, bool with_paths) const
+StringTextFileContent::as_xml(xmlpp::Element* element, bool with_paths) const
{
- node->add_child("Type")->add_child_text("TextSubtitle");
- Content::as_xml (node, with_paths);
+ cxml::add_text_child(element, "Type", "TextSubtitle");
+ Content::as_xml(element, with_paths);
if (only_text()) {
- only_text()->as_xml(node);
+ only_text()->as_xml(element);
}
- node->add_child("Length")->add_child_text(raw_convert<string>(_length.get ()));
+ cxml::add_text_child(element, "Length", raw_convert<string>(_length.get()));
}
diff --git a/src/lib/string_text_file_content.h b/src/lib/string_text_file_content.h
index 30f543381..8a5d9946a 100644
--- a/src/lib/string_text_file_content.h
+++ b/src/lib/string_text_file_content.h
@@ -45,7 +45,7 @@ public:
void examine (std::shared_ptr<const Film> film, std::shared_ptr<Job>) override;
std::string summary () const override;
std::string technical_summary () const override;
- void as_xml (xmlpp::Node *, bool with_paths) const override;
+ void as_xml(xmlpp::Element*, bool with_paths) const override;
dcpomatic::DCPTime full_length (std::shared_ptr<const Film> film) const override;
dcpomatic::DCPTime approximate_length () const override;
std::string identifier () const override;
diff --git a/src/lib/subtitle_analysis.cc b/src/lib/subtitle_analysis.cc
index ff1969a3a..2968416c6 100644
--- a/src/lib/subtitle_analysis.cc
+++ b/src/lib/subtitle_analysis.cc
@@ -70,18 +70,18 @@ SubtitleAnalysis::write (boost::filesystem::path path) const
auto doc = make_shared<xmlpp::Document>();
xmlpp::Element* root = doc->create_root_node ("SubtitleAnalysis");
- root->add_child("Version")->add_child_text (raw_convert<string>(_current_state_version));
+ cxml::add_text_child(root, "Version", raw_convert<string>(_current_state_version));
if (_bounding_box) {
- auto bounding_box = root->add_child("BoundingBox");
- bounding_box->add_child("X")->add_child_text(raw_convert<string>(_bounding_box->x));
- bounding_box->add_child("Y")->add_child_text(raw_convert<string>(_bounding_box->y));
- bounding_box->add_child("Width")->add_child_text(raw_convert<string>(_bounding_box->width));
- bounding_box->add_child("Height")->add_child_text(raw_convert<string>(_bounding_box->height));
+ auto bounding_box = cxml::add_child(root, "BoundingBox");
+ cxml::add_text_child(bounding_box, "X", raw_convert<string>(_bounding_box->x));
+ cxml::add_text_child(bounding_box, "Y", raw_convert<string>(_bounding_box->y));
+ cxml::add_text_child(bounding_box, "Width", raw_convert<string>(_bounding_box->width));
+ cxml::add_text_child(bounding_box, "Height", raw_convert<string>(_bounding_box->height));
}
- root->add_child("AnalysisXOffset")->add_child_text(raw_convert<string>(_analysis_x_offset));
- root->add_child("AnalysisYOffset")->add_child_text(raw_convert<string>(_analysis_y_offset));
+ cxml::add_text_child(root, "AnalysisXOffset", raw_convert<string>(_analysis_x_offset));
+ cxml::add_text_child(root, "AnalysisYOffset", raw_convert<string>(_analysis_y_offset));
doc->write_to_file_formatted (path.string());
}
diff --git a/src/lib/subtitle_encoder.cc b/src/lib/subtitle_film_encoder.cc
index 8b1d9a15b..93ccc177b 100644
--- a/src/lib/subtitle_encoder.cc
+++ b/src/lib/subtitle_film_encoder.cc
@@ -23,7 +23,7 @@
#include "film.h"
#include "job.h"
#include "player.h"
-#include "subtitle_encoder.h"
+#include "subtitle_film_encoder.h"
#include <dcp/filesystem.h>
#include <dcp/interop_subtitle_asset.h>
#include <dcp/raw_convert.h>
@@ -51,8 +51,8 @@ using dcp::raw_convert;
* @param initial_name Hint that may be used to create filenames, if @ref output is a directory.
* @param include_font true to refer to and export any font file (for Interop; ignored for SMPTE).
*/
-SubtitleEncoder::SubtitleEncoder (shared_ptr<const Film> film, shared_ptr<Job> job, boost::filesystem::path output, string initial_name, bool split_reels, bool include_font)
- : Encoder (film, job)
+SubtitleFilmEncoder::SubtitleFilmEncoder(shared_ptr<const Film> film, shared_ptr<Job> job, boost::filesystem::path output, string initial_name, bool split_reels, bool include_font)
+ : FilmEncoder(film, job)
, _split_reels (split_reels)
, _include_font (include_font)
, _reel_index (0)
@@ -61,7 +61,7 @@ SubtitleEncoder::SubtitleEncoder (shared_ptr<const Film> film, shared_ptr<Job> j
_player.set_play_referenced();
_player.set_ignore_video();
_player.set_ignore_audio();
- _player.Text.connect(boost::bind(&SubtitleEncoder::text, this, _1, _2, _3, _4));
+ _player.Text.connect(boost::bind(&SubtitleFilmEncoder::text, this, _1, _2, _3, _4));
string const extension = film->interop() ? ".xml" : ".mxf";
@@ -91,7 +91,7 @@ SubtitleEncoder::SubtitleEncoder (shared_ptr<const Film> film, shared_ptr<Job> j
void
-SubtitleEncoder::go ()
+SubtitleFilmEncoder::go()
{
{
shared_ptr<Job> job = _job.lock ();
@@ -133,7 +133,7 @@ SubtitleEncoder::go ()
void
-SubtitleEncoder::text (PlayerText subs, TextType type, optional<DCPTextTrack> track, dcpomatic::DCPTimePeriod period)
+SubtitleFilmEncoder::text(PlayerText subs, TextType type, optional<DCPTextTrack> track, dcpomatic::DCPTimePeriod period)
{
if (type != TextType::OPEN_SUBTITLE) {
return;
@@ -193,7 +193,7 @@ SubtitleEncoder::text (PlayerText subs, TextType type, optional<DCPTextTrack> tr
Frame
-SubtitleEncoder::frames_done () const
+SubtitleFilmEncoder::frames_done() const
{
if (!_last) {
return 0;
diff --git a/src/lib/subtitle_encoder.h b/src/lib/subtitle_film_encoder.h
index 0815b1fff..54231794d 100644
--- a/src/lib/subtitle_encoder.h
+++ b/src/lib/subtitle_film_encoder.h
@@ -21,7 +21,7 @@
#include "dcp_text_track.h"
#include "dcpomatic_time.h"
-#include "encoder.h"
+#include "film_encoder.h"
#include "player_text.h"
@@ -33,13 +33,13 @@ namespace dcp {
class Film;
-/** @class SubtitleEncoder.
+/** @class SubtitleFilmEncoder.
* @brief An `encoder' which extracts a film's subtitles to DCP XML format.
*/
-class SubtitleEncoder : public Encoder
+class SubtitleFilmEncoder : public FilmEncoder
{
public:
- SubtitleEncoder (std::shared_ptr<const Film> film, std::shared_ptr<Job> job, boost::filesystem::path output, std::string initial_name, bool split_reels, bool include_font);
+ SubtitleFilmEncoder(std::shared_ptr<const Film> film, std::shared_ptr<Job> job, boost::filesystem::path output, std::string initial_name, bool split_reels, bool include_font);
void go () override;
diff --git a/src/lib/text_content.cc b/src/lib/text_content.cc
index 92a35b822..03336f15d 100644
--- a/src/lib/text_content.cc
+++ b/src/lib/text_content.cc
@@ -19,11 +19,12 @@
*/
-#include "text_content.h"
-#include "util.h"
+#include "content.h"
#include "exceptions.h"
#include "font.h"
-#include "content.h"
+#include "text_content.h"
+#include "util.h"
+#include "variant.h"
#include <dcp/raw_convert.h>
#include <libcxml/cxml.h>
#include <libxml++/libxml++.h>
@@ -32,13 +33,13 @@
#include "i18n.h"
-using std::string;
-using std::vector;
using std::cout;
+using std::dynamic_pointer_cast;
using std::list;
-using std::shared_ptr;
using std::make_shared;
-using std::dynamic_pointer_cast;
+using std::shared_ptr;
+using std::string;
+using std::vector;
using boost::optional;
using dcp::raw_convert;
using namespace dcpomatic;
@@ -235,17 +236,20 @@ 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.
*/
if (version <= 37) {
if (!lang->content().empty()) {
- notes.push_back (String::compose(
+ notes.push_back(String::compose(
_("A subtitle or closed caption file in this project is marked with the language '%1', "
- "which DCP-o-matic does not recognise. The file's language has been cleared."), lang->content()));
+ "which %2 does not recognise. The file's language has been cleared."), lang->content(), variant::dcpomatic()));
}
} else {
throw;
@@ -353,63 +357,63 @@ TextContent::TextContent (Content* parent, vector<shared_ptr<Content>> c)
/** _mutex must not be held on entry */
void
-TextContent::as_xml (xmlpp::Node* root) const
+TextContent::as_xml(xmlpp::Element* root) const
{
boost::mutex::scoped_lock lm (_mutex);
- auto text = root->add_child ("Text");
+ auto text = cxml::add_child(root, "Text");
- text->add_child("Use")->add_child_text (_use ? "1" : "0");
- text->add_child("Burn")->add_child_text (_burn ? "1" : "0");
- text->add_child("XOffset")->add_child_text (raw_convert<string> (_x_offset));
- text->add_child("YOffset")->add_child_text (raw_convert<string> (_y_offset));
- text->add_child("XScale")->add_child_text (raw_convert<string> (_x_scale));
- text->add_child("YScale")->add_child_text (raw_convert<string> (_y_scale));
+ cxml::add_text_child(text, "Use", _use ? "1" : "0");
+ cxml::add_text_child(text, "Burn", _burn ? "1" : "0");
+ cxml::add_text_child(text, "XOffset", raw_convert<string>(_x_offset));
+ cxml::add_text_child(text, "YOffset", raw_convert<string>(_y_offset));
+ cxml::add_text_child(text, "XScale", raw_convert<string>(_x_scale));
+ cxml::add_text_child(text, "YScale", raw_convert<string>(_y_scale));
if (_colour) {
- text->add_child("Red")->add_child_text (raw_convert<string> (_colour->r));
- text->add_child("Green")->add_child_text (raw_convert<string> (_colour->g));
- text->add_child("Blue")->add_child_text (raw_convert<string> (_colour->b));
+ cxml::add_text_child(text, "Red", raw_convert<string>(_colour->r));
+ cxml::add_text_child(text, "Green", raw_convert<string>(_colour->g));
+ cxml::add_text_child(text, "Blue", raw_convert<string>(_colour->b));
}
if (_effect) {
switch (*_effect) {
case dcp::Effect::NONE:
- text->add_child("Effect")->add_child_text("none");
+ cxml::add_text_child(text, "Effect", "none");
break;
case dcp::Effect::BORDER:
- text->add_child("Effect")->add_child_text("outline");
+ cxml::add_text_child(text, "Effect", "outline");
break;
case dcp::Effect::SHADOW:
- text->add_child("Effect")->add_child_text("shadow");
+ cxml::add_text_child(text, "Effect", "shadow");
break;
}
}
if (_effect_colour) {
- text->add_child("EffectRed")->add_child_text (raw_convert<string> (_effect_colour->r));
- text->add_child("EffectGreen")->add_child_text (raw_convert<string> (_effect_colour->g));
- text->add_child("EffectBlue")->add_child_text (raw_convert<string> (_effect_colour->b));
+ cxml::add_text_child(text, "EffectRed", raw_convert<string>(_effect_colour->r));
+ cxml::add_text_child(text, "EffectGreen", raw_convert<string>(_effect_colour->g));
+ cxml::add_text_child(text, "EffectBlue", raw_convert<string>(_effect_colour->b));
}
- text->add_child("LineSpacing")->add_child_text (raw_convert<string> (_line_spacing));
+ cxml::add_text_child(text, "LineSpacing", raw_convert<string>(_line_spacing));
if (_fade_in) {
- text->add_child("FadeIn")->add_child_text (raw_convert<string> (_fade_in->get()));
+ cxml::add_text_child(text, "FadeIn", raw_convert<string>(_fade_in->get()));
}
if (_fade_out) {
- text->add_child("FadeOut")->add_child_text (raw_convert<string> (_fade_out->get()));
+ cxml::add_text_child(text, "FadeOut", raw_convert<string>(_fade_out->get()));
}
- text->add_child("OutlineWidth")->add_child_text (raw_convert<string> (_outline_width));
+ cxml::add_text_child(text, "OutlineWidth", raw_convert<string>(_outline_width));
for (auto i: _fonts) {
- i->as_xml (text->add_child("Font"));
+ i->as_xml(cxml::add_child(text, "Font"));
}
- text->add_child("Type")->add_child_text (text_type_to_string(_type));
- text->add_child("OriginalType")->add_child_text (text_type_to_string(_original_type));
+ cxml::add_text_child(text, "Type", text_type_to_string(_type));
+ cxml::add_text_child(text, "OriginalType", text_type_to_string(_original_type));
if (_dcp_track) {
- _dcp_track->as_xml(text->add_child("DCPTrack"));
+ _dcp_track->as_xml(cxml::add_child(text, "DCPTrack"));
}
if (_language) {
- auto lang = text->add_child("Language");
+ auto lang = cxml::add_child(text, "Language");
lang->add_child_text (_language->to_string());
- lang->set_attribute ("Additional", _language_is_additional ? "1" : "0");
+ lang->set_attribute("additional", _language_is_additional ? "1" : "0");
}
}
diff --git a/src/lib/text_content.h b/src/lib/text_content.h
index 4d4bdc507..58014917b 100644
--- a/src/lib/text_content.h
+++ b/src/lib/text_content.h
@@ -72,7 +72,7 @@ public:
TextContent (Content* parent, std::vector<std::shared_ptr<Content>>);
TextContent (Content* parent, cxml::ConstNodePtr, int version, std::list<std::string>& notes);
- void as_xml (xmlpp::Node *) const;
+ void as_xml(xmlpp::Element*) const;
std::string identifier () const;
void take_settings_from (std::shared_ptr<const TextContent> c);
@@ -224,6 +224,10 @@ private:
double _x_scale;
/** y scale factor to apply to subtitles */
double _y_scale;
+ /** Fonts used by this content. They are added during content examination, then
+ * saved/loaded from metadata.xml. This is so the user can change the font in
+ * used by a piece of content.
+ */
std::list<std::shared_ptr<dcpomatic::Font>> _fonts;
boost::optional<dcp::Colour> _colour;
boost::optional<dcp::Effect> _effect;
diff --git a/src/lib/text_decoder.cc b/src/lib/text_decoder.cc
index 75fa33605..945ffaa03 100644
--- a/src/lib/text_decoder.cc
+++ b/src/lib/text_decoder.cc
@@ -20,6 +20,7 @@
#include "compose.hpp"
+#include "dcpomatic_log.h"
#include "log.h"
#include "text_content.h"
#include "text_decoder.h"
@@ -306,6 +307,10 @@ TextDecoder::emit_plain_start (ContentTime from, sub::Subtitle const & sub_subti
);
auto font = content()->get_font(block.font.get_value_or(""));
+ if (!font) {
+ LOG_WARNING("Could not find font '%1' in content; falling back to default", block.font.get_value_or(""));
+ font = std::make_shared<dcpomatic::Font>(block.font.get_value_or(""), default_font_file());
+ }
DCPOMATIC_ASSERT(font);
auto string_text = StringText(
diff --git a/src/lib/transcode_job.cc b/src/lib/transcode_job.cc
index 12b9a2aa3..f1373fb4a 100644
--- a/src/lib/transcode_job.cc
+++ b/src/lib/transcode_job.cc
@@ -28,15 +28,16 @@
#include "compose.hpp"
#include "content.h"
#include "config.h"
-#include "dcp_encoder.h"
+#include "dcp_film_encoder.h"
#include "dcpomatic_log.h"
-#include "encoder.h"
#include "examine_content_job.h"
#include "film.h"
+#include "film_encoder.h"
#include "job_manager.h"
#include "log.h"
#include "transcode_job.h"
#include "upload_job.h"
+#include "variant.h"
#include <iomanip>
#include <iostream>
@@ -83,7 +84,7 @@ TranscodeJob::json_name () const
void
-TranscodeJob::set_encoder (shared_ptr<Encoder> e)
+TranscodeJob::set_encoder(shared_ptr<FilmEncoder> e)
{
_encoder = e;
}
@@ -110,7 +111,10 @@ TranscodeJob::run ()
return;
case ChangedBehaviour::STOP:
set_progress (1);
- set_error (_("Files have changed since they were added to the project."), _("Open the project in DCP-o-matic, check the settings, then save it before trying again."));
+ set_error(
+ _("Files have changed since they were added to the project."),
+ variant::insert_dcpomatic(_("Open the project in %1, check the settings, then save it before trying again."))
+ );
set_state (FINISHED_ERROR);
return;
default:
@@ -129,7 +133,7 @@ TranscodeJob::run ()
LOG_GENERAL(N_("Transcode job completed successfully: %1 fps"), dcp::locale_convert<string>(frames_per_second(), 2, true));
- if (dynamic_pointer_cast<DCPEncoder>(_encoder)) {
+ if (variant::count_created_dcps() && dynamic_pointer_cast<DCPFilmEncoder>(_encoder)) {
try {
Analytics::instance()->successful_dcp_encode();
} catch (FileError& e) {
@@ -148,6 +152,20 @@ TranscodeJob::run ()
}
+void
+TranscodeJob::pause()
+{
+ _encoder->pause();
+}
+
+
+void TranscodeJob::resume()
+{
+ _encoder->resume();
+ Job::resume();
+}
+
+
string
TranscodeJob::status () const
{
diff --git a/src/lib/transcode_job.h b/src/lib/transcode_job.h
index b05b20a16..720d7f99b 100644
--- a/src/lib/transcode_job.h
+++ b/src/lib/transcode_job.h
@@ -35,7 +35,9 @@
#undef IGNORE
-class Encoder;
+class FilmEncoder;
+
+struct frames_not_lost_when_threads_disappear;
/** @class TranscodeJob
@@ -56,20 +58,24 @@ 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;
}
- void set_encoder (std::shared_ptr<Encoder> t);
+ void set_encoder(std::shared_ptr<FilmEncoder> encoder);
private:
+ friend struct ::frames_not_lost_when_threads_disappear;
+
virtual void post_transcode () {}
float frames_per_second() const;
int remaining_time () const override;
- std::shared_ptr<Encoder> _encoder;
+ std::shared_ptr<FilmEncoder> _encoder;
ChangedBehaviour _changed;
};
diff --git a/src/lib/types.h b/src/lib/types.h
index 36059401e..b4fcea959 100644
--- a/src/lib/types.h
+++ b/src/lib/types.h
@@ -43,8 +43,9 @@ class FFmpegContent;
*
* 64 - first version used
* 65 - v2.16.0 - checksums added to communication
+ * 66 - v2.17.x - J2KBandwidth -> VideoBitRate in metadata
*/
-#define SERVER_LINK_VERSION (64+1)
+#define SERVER_LINK_VERSION (64+2)
/** A film of F seconds at f FPS will be Ff frames;
Consider some delta FPS d, so if we run the same
@@ -107,7 +108,8 @@ enum class ReelType
{
SINGLE,
BY_VIDEO_CONTENT,
- BY_LENGTH
+ BY_LENGTH,
+ CUSTOM
};
diff --git a/src/lib/unzipper.cc b/src/lib/unzipper.cc
index f0170e7e0..8d468f24f 100644
--- a/src/lib/unzipper.cc
+++ b/src/lib/unzipper.cc
@@ -56,8 +56,20 @@ Unzipper::~Unzipper()
}
+bool
+Unzipper::contains(string const& filename) const
+{
+ auto file = zip_fopen(_zip, filename.c_str(), 0);
+ bool exists = file != nullptr;
+ if (file) {
+ zip_fclose(file);
+ }
+ return exists;
+}
+
+
string
-Unzipper::get(string const& filename)
+Unzipper::get(string const& filename) const
{
auto file = zip_fopen(_zip, filename.c_str(), 0);
if (!file) {
diff --git a/src/lib/unzipper.h b/src/lib/unzipper.h
index 7cab6e5f4..76b2fe45a 100644
--- a/src/lib/unzipper.h
+++ b/src/lib/unzipper.h
@@ -33,7 +33,8 @@ public:
Unzipper(Unzipper const&) = delete;
Unzipper& operator=(Unzipper const&) = delete;
- std::string get(std::string const& filename);
+ std::string get(std::string const& filename) const;
+ bool contains(std::string const& filename) const;
private:
struct zip* _zip;
diff --git a/src/lib/util.cc b/src/lib/util.cc
index fdc647fba..172b8d763 100644
--- a/src/lib/util.cc
+++ b/src/lib/util.cc
@@ -49,12 +49,14 @@
#include "string_text.h"
#include "text_decoder.h"
#include "util.h"
+#include "variant.h"
#include "video_content.h"
#include <dcp/atmos_asset.h>
#include <dcp/decrypted_kdm.h>
#include <dcp/file.h>
#include <dcp/filesystem.h>
#include <dcp/locale_convert.h>
+#include <dcp/mpeg2_picture_asset.h>
#include <dcp/picture_asset.h>
#include <dcp/raw_convert.h>
#include <dcp/scope_guard.h>
@@ -87,10 +89,11 @@ LIBDCP_ENABLE_WARNINGS
#include <dbghelp.h>
#endif
#include <signal.h>
+#include <climits>
#include <iomanip>
#include <iostream>
#include <fstream>
-#include <climits>
+#include <numeric>
#include <stdexcept>
#ifdef DCPOMATIC_POSIX
#include <execinfo.h>
@@ -102,6 +105,7 @@ LIBDCP_ENABLE_WARNINGS
using std::bad_alloc;
using std::cout;
+using std::dynamic_pointer_cast;
using std::endl;
using std::istream;
using std::list;
@@ -118,9 +122,6 @@ using std::vector;
using std::wstring;
using boost::thread;
using boost::optional;
-using boost::lexical_cast;
-using boost::bad_lexical_cast;
-using boost::scoped_array;
using dcp::Size;
using dcp::raw_convert;
using dcp::locale_convert;
@@ -243,6 +244,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 +430,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 ();
@@ -509,7 +516,7 @@ mo_path ()
boost::filesystem::path
mo_path ()
{
- return "DCP-o-matic 2.app/Contents/Resources";
+ return variant::dcpomatic_app() + "/Contents/Resources";
}
#endif
@@ -741,9 +748,10 @@ asset_filename (shared_ptr<dcp::Asset> asset, string type, int reel_index, int r
string
-video_asset_filename (shared_ptr<dcp::PictureAsset> asset, int reel_index, int reel_count, optional<string> summary)
+video_asset_filename(shared_ptr<dcp::PictureAsset> asset, int reel_index, int reel_count, optional<string> summary)
{
- return asset_filename(asset, "j2c", reel_index, reel_count, summary, ".mxf");
+ string type = dynamic_pointer_cast<dcp::MPEG2PictureAsset>(asset) ? "mpeg2" : "j2c";
+ return asset_filename(asset, type, reel_index, reel_count, summary, ".mxf");
}
@@ -879,16 +887,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)
@@ -1024,9 +1022,9 @@ decrypt_kdm_with_helpful_error (dcp::EncryptedKDM kdm)
}
}
if (!on_chain) {
- throw KDMError (_("This KDM was not made for DCP-o-matic's decryption certificate."), e.what());
+ throw KDMError(variant::insert_dcpomatic(_("This KDM was not made for %1's decryption certificate.")), e.what());
} else if (kdm_subject_name != dc->leaf().subject()) {
- throw KDMError (_("This KDM was made for DCP-o-matic but not for its leaf certificate."), e.what());
+ throw KDMError(variant::insert_dcpomatic(_("This KDM was made for %1 but not for its leaf certificate.")), e.what());
} else {
throw;
}
@@ -1126,6 +1124,34 @@ 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)
{
@@ -1153,3 +1179,23 @@ screen_names_to_string(vector<string> names)
return result.substr(0, result.length() - 2);
}
+
+string
+report_problem()
+{
+ return String::compose(_("Please report this problem by using Help -> Report a problem or via email to %1"), variant::report_problem_email());
+}
+
+
+string
+join_strings(vector<string> const& in, string const& separator)
+{
+ if (in.empty()) {
+ return {};
+ }
+
+ return std::accumulate(std::next(in.begin()), in.end(), in.front(), [separator](string a, string b) {
+ return a + separator + b;
+ });
+}
+
diff --git a/src/lib/util.h b/src/lib/util.h
index fd6fd6164..7c40c5ce8 100644
--- a/src/lib/util.h
+++ b/src/lib/util.h
@@ -32,6 +32,7 @@
#include "dcpomatic_time.h"
#include "pixel_quanta.h"
#include "types.h"
+#include <libcxml/cxml.h>
#include <dcp/atmos_asset.h>
#include <dcp/decrypted_kdm.h>
#include <dcp/util.h>
@@ -97,6 +98,24 @@ 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
+extern std::string join_strings(std::vector<std::string> const& in, std::string const& separator = " ");
+
+
+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);
+extern std::string report_problem();
#endif
diff --git a/src/lib/variant.cc b/src/lib/variant.cc
new file mode 100644
index 000000000..9b12d753d
--- /dev/null
+++ b/src/lib/variant.cc
@@ -0,0 +1,172 @@
+/*
+ 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 "variant.h"
+
+
+static char const* _dcpomatic = "DCP-o-matic";
+static char const* _dcpomatic_player = "DCP-o-matic Player";
+static char const* _dcpomatic_kdm_creator = "DCP-o-matic KDM Creator";
+static char const* _dcpomatic_verifier = "DCP-o-matic Verifier";
+static char const* _dcpomatic_app = "DCP-o-matic 2.app";
+static char const* _dcpomatic_player_app = "DCP-o-matic 2 Player.app";
+static char const* _dcpomatic_disk_writer = "DCP-o-matic Disk Writer";
+static char const* _dcpomatic_editor = "DCP-o-matic Editor";
+static char const* _dcpomatic_encode_server = "DCP-o-matic Encode Server";
+static char const* _dcpomatic_batch_converter_app = "DCP-o-matic 2 Batch Converter.app";
+static char const* _dcpomatic_playlist_editor = "DCP-o-matic Playlist Editor";
+static char const* _dcpomatic_combiner = "DCP-o-matic Combiner";
+static char const* _dcpomatic_batch_converter = "DCP-o-matic Batch Converter";
+
+static char const* _report_problem_email = "carl@dcpomatic.com";
+
+static bool const _show_tagline = true;
+static bool const _show_dcpomatic_website = true;
+static bool const _show_report_a_problem = true;
+static bool const _count_created_dcps = true;
+
+
+std::string
+variant::dcpomatic()
+{
+ return _dcpomatic;
+}
+
+std::string
+variant::dcpomatic_batch_converter()
+{
+ return _dcpomatic_batch_converter;
+}
+
+std::string
+variant::dcpomatic_combiner()
+{
+ return _dcpomatic_combiner;
+}
+
+std::string
+variant::dcpomatic_disk_writer()
+{
+ return _dcpomatic_disk_writer;
+}
+
+std::string
+variant::dcpomatic_editor()
+{
+ return _dcpomatic_editor;
+}
+
+std::string
+variant::dcpomatic_encode_server()
+{
+ return _dcpomatic_encode_server;
+}
+
+std::string
+variant::dcpomatic_kdm_creator()
+{
+ return _dcpomatic_kdm_creator;
+}
+
+std::string
+variant::dcpomatic_player()
+{
+ return _dcpomatic_player;
+}
+
+std::string
+variant::dcpomatic_playlist_editor()
+{
+ return _dcpomatic_playlist_editor;
+}
+
+std::string
+variant::dcpomatic_verifier()
+{
+ return _dcpomatic_verifier;
+}
+
+std::string
+variant::insert_dcpomatic(std::string const& s)
+{
+ return String::compose(s, _dcpomatic);
+}
+
+std::string
+variant::insert_dcpomatic_encode_server(std::string const& s)
+{
+ return String::compose(s, _dcpomatic_encode_server);
+}
+
+std::string
+variant::insert_dcpomatic_kdm_creator(std::string const& s)
+{
+ return String::compose(s, _dcpomatic_kdm_creator);
+}
+
+std::string
+variant::dcpomatic_app()
+{
+ return _dcpomatic_app;
+}
+
+std::string
+variant::dcpomatic_batch_converter_app()
+{
+ return _dcpomatic_batch_converter_app;
+}
+
+std::string
+variant::dcpomatic_player_app()
+{
+ return _dcpomatic_player_app;
+}
+
+bool
+variant::show_tagline()
+{
+ return _show_tagline;
+}
+
+bool
+variant::show_dcpomatic_website()
+{
+ return _show_dcpomatic_website;
+}
+
+bool
+variant::show_report_a_problem()
+{
+ return _show_report_a_problem;
+}
+
+bool
+variant::count_created_dcps()
+{
+ return _count_created_dcps;
+}
+
+std::string
+variant::report_problem_email()
+{
+ return _report_problem_email;
+}
+
diff --git a/src/lib/variant.h b/src/lib/variant.h
new file mode 100644
index 000000000..b69b5ba38
--- /dev/null
+++ b/src/lib/variant.h
@@ -0,0 +1,55 @@
+/*
+ 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 "compose.hpp"
+
+
+namespace variant
+{
+
+std::string dcpomatic();
+std::string dcpomatic_batch_converter();
+std::string dcpomatic_combiner();
+std::string dcpomatic_disk_writer();
+std::string dcpomatic_editor();
+std::string dcpomatic_encode_server();
+std::string dcpomatic_kdm_creator();
+std::string dcpomatic_player();
+std::string dcpomatic_playlist_editor();
+std::string dcpomatic_verifier();
+
+std::string insert_dcpomatic(std::string const& s);
+std::string insert_dcpomatic_encode_server(std::string const& s);
+std::string insert_dcpomatic_kdm_creator(std::string const& s);
+
+std::string dcpomatic_app();
+std::string dcpomatic_batch_converter_app();
+std::string dcpomatic_player_app();
+
+std::string report_problem_email();
+
+bool show_tagline();
+bool show_dcpomatic_website();
+bool show_report_a_problem();
+bool count_created_dcps();
+
+}
+
diff --git a/src/lib/verify_dcp_job.cc b/src/lib/verify_dcp_job.cc
index 5e50ec51d..668b1eab4 100644
--- a/src/lib/verify_dcp_job.cc
+++ b/src/lib/verify_dcp_job.cc
@@ -86,7 +86,7 @@ VerifyDCPJob::run ()
}
}
- _notes = dcp::verify(
+ _result = dcp::verify(
_directories,
decrypted_kdms,
bind(&VerifyDCPJob::update_stage, this, _1, _2),
@@ -96,7 +96,7 @@ VerifyDCPJob::run ()
);
bool failed = false;
- for (auto i: _notes) {
+ for (auto i: _result.notes) {
if (i.type() == dcp::VerificationNote::Type::ERROR) {
failed = true;
}
diff --git a/src/lib/verify_dcp_job.h b/src/lib/verify_dcp_job.h
index 61a347507..d7ac21d41 100644
--- a/src/lib/verify_dcp_job.h
+++ b/src/lib/verify_dcp_job.h
@@ -36,8 +36,8 @@ public:
std::string json_name () const override;
void run () override;
- std::vector<dcp::VerificationNote> notes () const {
- return _notes;
+ dcp::VerificationResult const& result() const {
+ return _result;
}
private:
@@ -45,5 +45,5 @@ private:
std::vector<boost::filesystem::path> _directories;
std::vector<boost::filesystem::path> _kdms;
- std::vector<dcp::VerificationNote> _notes;
+ dcp::VerificationResult _result;
};
diff --git a/src/lib/video_content.cc b/src/lib/video_content.cc
index 6c027ff11..10dd5ff1e 100644
--- a/src/lib/video_content.cc
+++ b/src/lib/video_content.cc
@@ -273,37 +273,37 @@ VideoContent::VideoContent (Content* parent, vector<shared_ptr<Content> > c)
void
-VideoContent::as_xml (xmlpp::Node* node) const
+VideoContent::as_xml(xmlpp::Element* element) const
{
boost::mutex::scoped_lock lm (_mutex);
- node->add_child("Use")->add_child_text (_use ? "1" : "0");
- node->add_child("VideoLength")->add_child_text (raw_convert<string> (_length));
+ cxml::add_text_child(element, "Use", _use ? "1" : "0");
+ cxml::add_text_child(element, "VideoLength", raw_convert<string>(_length));
if (_size) {
- node->add_child("VideoWidth")->add_child_text(raw_convert<string>(_size->width));
- node->add_child("VideoHeight")->add_child_text(raw_convert<string>(_size->height));
+ cxml::add_text_child(element, "VideoWidth", raw_convert<string>(_size->width));
+ cxml::add_text_child(element, "VideoHeight", raw_convert<string>(_size->height));
}
- node->add_child("VideoFrameType")->add_child_text (video_frame_type_to_string (_frame_type));
+ cxml::add_text_child(element, "VideoFrameType", video_frame_type_to_string(_frame_type));
if (_sample_aspect_ratio) {
- node->add_child("SampleAspectRatio")->add_child_text (raw_convert<string> (_sample_aspect_ratio.get ()));
+ cxml::add_text_child(element, "SampleAspectRatio", raw_convert<string> (_sample_aspect_ratio.get ()));
}
- _crop.as_xml (node);
+ _crop.as_xml(element);
if (_custom_ratio) {
- node->add_child("CustomRatio")->add_child_text(raw_convert<string>(*_custom_ratio));
+ cxml::add_text_child(element, "CustomRatio", raw_convert<string>(*_custom_ratio));
}
if (_custom_size) {
- node->add_child("CustomWidth")->add_child_text(raw_convert<string>(_custom_size->width));
- node->add_child("CustomHeight")->add_child_text(raw_convert<string>(_custom_size->height));
+ cxml::add_text_child(element, "CustomWidth", raw_convert<string>(_custom_size->width));
+ cxml::add_text_child(element, "CustomHeight", raw_convert<string>(_custom_size->height));
}
if (_colour_conversion) {
- _colour_conversion.get().as_xml (node->add_child("ColourConversion"));
+ _colour_conversion.get().as_xml(cxml::add_child(element, "ColourConversion"));
}
- node->add_child("YUV")->add_child_text (_yuv ? "1" : "0");
- node->add_child("FadeIn")->add_child_text (raw_convert<string> (_fade_in));
- node->add_child("FadeOut")->add_child_text (raw_convert<string> (_fade_out));
- node->add_child("Range")->add_child_text(_range == VideoRange::FULL ? "full" : "video");
- _pixel_quanta.as_xml(node->add_child("PixelQuanta"));
+ cxml::add_text_child(element, "YUV", _yuv ? "1" : "0");
+ cxml::add_text_child(element, "FadeIn", raw_convert<string>(_fade_in));
+ cxml::add_text_child(element, "FadeOut", raw_convert<string>(_fade_out));
+ cxml::add_text_child(element, "Range", _range == VideoRange::FULL ? "full" : "video");
+ _pixel_quanta.as_xml(cxml::add_child(element, "PixelQuanta"));
if (_burnt_subtitle_language) {
- node->add_child("BurntSubtitleLanguage")->add_child_text(_burnt_subtitle_language->to_string());
+ cxml::add_text_child(element, "BurntSubtitleLanguage", _burnt_subtitle_language->to_string());
}
}
@@ -424,27 +424,29 @@ VideoContent::size_after_crop () const
}
-/** @param f Frame index within the whole (untrimmed) content.
+/** @param time Time within the whole (untrimmed) content.
* @return Fade factor (between 0 and 1) or unset if there is no fade.
*/
optional<double>
-VideoContent::fade (shared_ptr<const Film> film, Frame f) const
+VideoContent::fade(shared_ptr<const Film> film, ContentTime time) const
{
- DCPOMATIC_ASSERT (f >= 0);
+ DCPOMATIC_ASSERT(time.get() >= 0);
double const vfr = _parent->active_video_frame_rate(film);
- auto const ts = _parent->trim_start().frames_round(vfr);
- if ((f - ts) < fade_in()) {
- return double (f - ts) / fade_in();
+ auto const ts = _parent->trim_start();
+ auto const fade_in_time = ContentTime::from_frames(fade_in(), vfr);
+ if ((time - ts) < fade_in_time) {
+ return double(ContentTime(time - ts).get()) / fade_in_time.get();
}
- auto fade_out_start = length() - _parent->trim_end().frames_round(vfr) - fade_out();
- if (f >= fade_out_start) {
- return 1 - double (f - fade_out_start) / fade_out();
+ auto const fade_out_time = ContentTime::from_frames(fade_out(), vfr);
+ auto fade_out_start = ContentTime::from_frames(length(), vfr) - _parent->trim_end() - fade_out_time;
+ if (time >= fade_out_start) {
+ return 1 - double(ContentTime(time - fade_out_start).get()) / fade_out_time.get();
}
- return optional<double> ();
+ return {};
}
string
diff --git a/src/lib/video_content.h b/src/lib/video_content.h
index 495d000e1..eb106cc75 100644
--- a/src/lib/video_content.h
+++ b/src/lib/video_content.h
@@ -66,7 +66,7 @@ public:
VideoContent (Content* parent, cxml::ConstNodePtr node, int version, VideoRange video_range_hint);
VideoContent (Content* parent, std::vector<std::shared_ptr<Content>>);
- void as_xml (xmlpp::Node *) const;
+ void as_xml(xmlpp::Element*) const;
std::string technical_summary () const;
std::string identifier () const;
void take_settings_from (std::shared_ptr<const VideoContent> c);
@@ -208,7 +208,7 @@ public:
boost::optional<dcp::Size> size_after_crop() const;
boost::optional<dcp::Size> scaled_size(dcp::Size container_size);
- boost::optional<double> fade (std::shared_ptr<const Film> film, Frame) const;
+ boost::optional<double> fade(std::shared_ptr<const Film> film, dcpomatic::ContentTime time) const;
std::string processing_description (std::shared_ptr<const Film> film);
diff --git a/src/lib/video_decoder.cc b/src/lib/video_decoder.cc
index cf21f885a..c628fddd9 100644
--- a/src/lib/video_decoder.cc
+++ b/src/lib/video_decoder.cc
@@ -20,7 +20,6 @@
#include "compose.hpp"
-#include "film.h"
#include "frame_interval_checker.h"
#include "image.h"
#include "j2k_image_proxy.h"
@@ -47,17 +46,9 @@ VideoDecoder::VideoDecoder (Decoder* parent, shared_ptr<const Content> c)
}
-/** Called by decoder classes when they have a video frame ready.
- * @param frame Frame index within the content; this does not take into account 3D
- * so for 3D_ALTERNATE this value goes:
- * 0: frame 0 left
- * 1: frame 0 right
- * 2: frame 1 left
- * 3: frame 1 right
- * and so on.
- */
+/** Called by decoder classes when they have a video frame ready */
void
-VideoDecoder::emit (shared_ptr<const Film> film, shared_ptr<const ImageProxy> image, Frame decoder_frame)
+VideoDecoder::emit(shared_ptr<const Film> film, shared_ptr<const ImageProxy> image, ContentTime time)
{
if (ignore ()) {
return;
@@ -66,14 +57,12 @@ VideoDecoder::emit (shared_ptr<const Film> film, shared_ptr<const ImageProxy> im
auto const afr = _content->active_video_frame_rate(film);
auto const vft = _content->video->frame_type();
- auto frame_time = ContentTime::from_frames (decoder_frame, afr);
-
/* Do some heuristics to try and spot the case where the user sets content to 3D
* when it is not. We try to tell this by looking at the differences in time between
* the first few frames. Real 3D content should have two frames for each timestamp.
*/
if (_frame_interval_checker) {
- _frame_interval_checker->feed (frame_time, afr);
+ _frame_interval_checker->feed(time, afr);
if (_frame_interval_checker->guess() == FrameIntervalChecker::PROBABLY_NOT_3D && vft == VideoFrameType::THREE_D) {
boost::throw_exception (
DecodeError(
@@ -91,94 +80,54 @@ VideoDecoder::emit (shared_ptr<const Film> film, shared_ptr<const ImageProxy> im
}
}
- Frame frame;
- Eyes eyes = Eyes::BOTH;
- if (!_position) {
- /* This is the first data we have received since initialisation or seek. Set
- the position based on the frame that was given. After this first time
- we just count frames, since (as with audio) it seems that ContentTimes
- are unreliable from FFmpegDecoder. They are much better than audio times
- but still we get the occasional one which is duplicated. In this case
- ffmpeg seems to carry on regardless, processing the video frame as normal.
- If we drop the frame with the duplicated timestamp we obviously lose sync.
- */
-
- if (vft == VideoFrameType::THREE_D_ALTERNATE) {
- frame = decoder_frame / 2;
- eyes = (decoder_frame % 2) ? Eyes::RIGHT : Eyes::LEFT;
- } else {
- frame = decoder_frame;
- if (vft == VideoFrameType::THREE_D) {
- auto j2k = dynamic_pointer_cast<const J2KImageProxy>(image);
- /* At the moment only DCP decoders producers VideoFrameType::THREE_D, so only the J2KImageProxy
- * knows which eye it is.
- */
- if (j2k && j2k->eye()) {
- eyes = j2k->eye().get() == dcp::Eye::LEFT ? Eyes::LEFT : Eyes::RIGHT;
- }
- }
- }
-
- _position = ContentTime::from_frames (frame, afr);
- } else {
- if (vft == VideoFrameType::THREE_D) {
- auto j2k = dynamic_pointer_cast<const J2KImageProxy>(image);
- if (j2k && j2k->eye()) {
- if (j2k->eye() == dcp::Eye::LEFT) {
- frame = _position->frames_round(afr) + 1;
- eyes = Eyes::LEFT;
- } else {
- frame = _position->frames_round(afr);
- eyes = Eyes::RIGHT;
- }
- } else {
- /* This should not happen; see above */
- frame = _position->frames_round(afr) + 1;
- }
- } else if (vft == VideoFrameType::THREE_D_ALTERNATE) {
- DCPOMATIC_ASSERT (_last_emitted_eyes);
- if (_last_emitted_eyes.get() == Eyes::RIGHT) {
- frame = _position->frames_round(afr) + 1;
- eyes = Eyes::LEFT;
- } else {
- frame = _position->frames_round(afr);
- eyes = Eyes::RIGHT;
- }
- } else {
- frame = _position->frames_round(afr) + 1;
- }
- }
-
switch (vft) {
case VideoFrameType::TWO_D:
+ Data(ContentVideo(image, time, Eyes::BOTH, Part::WHOLE));
+ break;
case VideoFrameType::THREE_D:
- Data (ContentVideo (image, frame, eyes, Part::WHOLE));
+ {
+ auto eyes = Eyes::LEFT;
+ auto j2k = dynamic_pointer_cast<const J2KImageProxy>(image);
+ if (j2k && j2k->eye()) {
+ eyes = *j2k->eye() == dcp::Eye::LEFT ? Eyes::LEFT : Eyes::RIGHT;
+ }
+
+ Data(ContentVideo(image, time, eyes, Part::WHOLE));
break;
+ }
case VideoFrameType::THREE_D_ALTERNATE:
{
- Data (ContentVideo (image, frame, eyes, Part::WHOLE));
+ Eyes eyes;
+ if (_last_emitted_eyes) {
+ eyes = _last_emitted_eyes.get() == Eyes::LEFT ? Eyes::RIGHT : Eyes::LEFT;
+ } else {
+ /* We don't know what eye this frame is, so just guess */
+ auto frame = time.frames_round(_content->video_frame_rate().get_value_or(24));
+ eyes = (frame % 2) ? Eyes::RIGHT : Eyes::LEFT;
+ }
+ Data(ContentVideo(image, time, eyes, Part::WHOLE));
_last_emitted_eyes = eyes;
break;
}
case VideoFrameType::THREE_D_LEFT_RIGHT:
- Data (ContentVideo (image, frame, Eyes::LEFT, Part::LEFT_HALF));
- Data (ContentVideo (image, frame, Eyes::RIGHT, Part::RIGHT_HALF));
+ Data(ContentVideo(image, time, Eyes::LEFT, Part::LEFT_HALF));
+ Data(ContentVideo(image, time, Eyes::RIGHT, Part::RIGHT_HALF));
break;
case VideoFrameType::THREE_D_TOP_BOTTOM:
- Data (ContentVideo (image, frame, Eyes::LEFT, Part::TOP_HALF));
- Data (ContentVideo (image, frame, Eyes::RIGHT, Part::BOTTOM_HALF));
+ Data(ContentVideo(image, time, Eyes::LEFT, Part::TOP_HALF));
+ Data(ContentVideo(image, time, Eyes::RIGHT, Part::BOTTOM_HALF));
break;
case VideoFrameType::THREE_D_LEFT:
- Data (ContentVideo (image, frame, Eyes::LEFT, Part::WHOLE));
+ Data(ContentVideo(image, time, Eyes::LEFT, Part::WHOLE));
break;
case VideoFrameType::THREE_D_RIGHT:
- Data (ContentVideo (image, frame, Eyes::RIGHT, Part::WHOLE));
+ Data(ContentVideo(image, time, Eyes::RIGHT, Part::WHOLE));
break;
default:
DCPOMATIC_ASSERT (false);
}
- _position = ContentTime::from_frames (frame, afr);
+ _position = time;
}
diff --git a/src/lib/video_decoder.h b/src/lib/video_decoder.h
index f6ee17425..b609404c4 100644
--- a/src/lib/video_decoder.h
+++ b/src/lib/video_decoder.h
@@ -60,7 +60,7 @@ public:
}
void seek () override;
- void emit (std::shared_ptr<const Film> film, std::shared_ptr<const ImageProxy>, Frame frame);
+ void emit(std::shared_ptr<const Film> film, std::shared_ptr<const ImageProxy>, dcpomatic::ContentTime time);
boost::signals2::signal<void (ContentVideo)> Data;
diff --git a/src/lib/video_encoder.cc b/src/lib/video_encoder.cc
new file mode 100644
index 000000000..590dc7471
--- /dev/null
+++ b/src/lib/video_encoder.cc
@@ -0,0 +1,64 @@
+/*
+ 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 "video_encoder.h"
+
+
+using std::shared_ptr;
+using boost::optional;
+
+
+VideoEncoder::VideoEncoder(shared_ptr<const Film> film, Writer& writer)
+ : _film(film)
+ , _writer(writer)
+ , _history(200)
+{
+
+}
+
+
+void
+VideoEncoder::encode(shared_ptr<PlayerVideo>, dcpomatic::DCPTime time)
+{
+ _last_player_video_time = time;
+}
+
+
+/** @return Number of video frames that have been queued for encoding */
+int
+VideoEncoder::video_frames_enqueued() const
+{
+ if (!_last_player_video_time) {
+ return 0;
+ }
+
+ return _last_player_video_time->frames_floor(_film->video_frame_rate());
+}
+
+
+/** @return an estimate of the current number of frames we are encoding per second,
+ * if known.
+ */
+optional<float>
+VideoEncoder::current_encoding_rate() const
+{
+ return _history.rate();
+}
diff --git a/src/lib/video_encoder.h b/src/lib/video_encoder.h
new file mode 100644
index 000000000..8cc33ef8a
--- /dev/null
+++ b/src/lib/video_encoder.h
@@ -0,0 +1,69 @@
+/*
+ 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/>.
+
+*/
+
+
+#ifndef DCPOMATIC_VIDEO_ENCODER_H
+#define DCPOMATIC_VIDEO_ENCODER_H
+
+
+#include "dcpomatic_time.h"
+#include "event_history.h"
+#include "film.h"
+#include "player_video.h"
+
+
+class Writer;
+
+
+class VideoEncoder
+{
+public:
+ VideoEncoder(std::shared_ptr<const Film> film, Writer& writer);
+ virtual ~VideoEncoder() {}
+
+ VideoEncoder(VideoEncoder const&) = delete;
+ VideoEncoder& operator=(VideoEncoder const&) = delete;
+
+ /** Called to indicate that a processing run is about to begin */
+ virtual void begin() {}
+
+ /** Called to pass a bit of video to be encoded as the next DCP frame */
+ virtual void encode(std::shared_ptr<PlayerVideo> pv, dcpomatic::DCPTime time);
+
+ virtual void pause() = 0;
+ virtual void resume() = 0;
+
+ /** Called when a processing run has finished */
+ virtual void end() = 0;
+
+ int video_frames_enqueued() const;
+ boost::optional<float> current_encoding_rate() const;
+
+protected:
+ /** Film that we are encoding */
+ std::shared_ptr<const Film> _film;
+ Writer& _writer;
+ EventHistory _history;
+ boost::optional<dcpomatic::DCPTime> _last_player_video_time;
+};
+
+
+#endif
+
diff --git a/src/lib/video_encoding.cc b/src/lib/video_encoding.cc
new file mode 100644
index 000000000..de68c6ae9
--- /dev/null
+++ b/src/lib/video_encoding.cc
@@ -0,0 +1,58 @@
+/*
+ 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 "dcpomatic_assert.h"
+#include "video_encoding.h"
+
+
+using std::string;
+
+
+string
+video_encoding_to_string(VideoEncoding encoding)
+{
+ switch (encoding) {
+ case VideoEncoding::JPEG2000:
+ return "jpeg2000";
+ case VideoEncoding::MPEG2:
+ return "mpeg2";
+ case VideoEncoding::COUNT:
+ DCPOMATIC_ASSERT(false);
+ }
+
+ DCPOMATIC_ASSERT(false);
+}
+
+
+VideoEncoding
+video_encoding_from_string(string const& encoding)
+{
+ if (encoding == "jpeg2000") {
+ return VideoEncoding::JPEG2000;
+ }
+
+ if (encoding == "mpeg2") {
+ return VideoEncoding::MPEG2;
+ }
+
+ DCPOMATIC_ASSERT(false);
+}
+
diff --git a/src/lib/video_encoding.h b/src/lib/video_encoding.h
new file mode 100644
index 000000000..7c240f06f
--- /dev/null
+++ b/src/lib/video_encoding.h
@@ -0,0 +1,42 @@
+/*
+ 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/>.
+
+*/
+
+
+#ifndef DCPOMATIC_VIDEO_ENCODING_H
+#define DCPOMATIC_VIDEO_ENCODING_H
+
+
+#include <string>
+
+
+enum class VideoEncoding
+{
+ JPEG2000,
+ MPEG2,
+ COUNT
+};
+
+
+std::string video_encoding_to_string(VideoEncoding encoding);
+VideoEncoding video_encoding_from_string(std::string const& encoding);
+
+
+#endif
+
diff --git a/src/lib/video_mxf_content.cc b/src/lib/video_mxf_content.cc
index 9adca5a2d..eeee6222c 100644
--- a/src/lib/video_mxf_content.cc
+++ b/src/lib/video_mxf_content.cc
@@ -26,8 +26,8 @@
#include "film.h"
#include "compose.hpp"
#include <asdcp/KM_log.h>
-#include <dcp/mono_picture_asset.h>
-#include <dcp/stereo_picture_asset.h>
+#include <dcp/mono_j2k_picture_asset.h>
+#include <dcp/stereo_j2k_picture_asset.h>
#include <dcp/exceptions.h>
#include <libxml++/libxml++.h>
@@ -61,7 +61,7 @@ VideoMXFContent::valid_mxf (boost::filesystem::path path)
Kumu::DefaultLogSink().UnsetFilterFlag(Kumu::LOG_ALLOW_ALL);
try {
- dcp::MonoPictureAsset mp (path);
+ dcp::MonoJ2KPictureAsset mp(path);
return true;
} catch (dcp::MXFFileError& e) {
@@ -71,7 +71,7 @@ VideoMXFContent::valid_mxf (boost::filesystem::path path)
try {
Kumu::DefaultLogSink().SetFilterFlag(0);
- dcp::StereoPictureAsset sp (path);
+ dcp::StereoJ2KPictureAsset sp (path);
return true;
} catch (dcp::MXFFileError& e) {
@@ -121,11 +121,11 @@ VideoMXFContent::identifier () const
void
-VideoMXFContent::as_xml (xmlpp::Node* node, bool with_paths) const
+VideoMXFContent::as_xml(xmlpp::Element* element, bool with_paths) const
{
- node->add_child("Type")->add_child_text("VideoMXF");
- Content::as_xml (node, with_paths);
- video->as_xml (node);
+ cxml::add_text_child(element, "Type", "VideoMXF");
+ Content::as_xml(element, with_paths);
+ video->as_xml(element);
}
diff --git a/src/lib/video_mxf_content.h b/src/lib/video_mxf_content.h
index 5a04c3da9..1731942a6 100644
--- a/src/lib/video_mxf_content.h
+++ b/src/lib/video_mxf_content.h
@@ -40,7 +40,7 @@ public:
std::string summary () const override;
std::string technical_summary () const override;
std::string identifier () const override;
- void as_xml (xmlpp::Node* node, bool with_paths) const override;
+ void as_xml(xmlpp::Element* element, bool with_paths) const override;
dcpomatic::DCPTime full_length (std::shared_ptr<const Film> film) const override;
dcpomatic::DCPTime approximate_length () const override;
void add_properties (std::shared_ptr<const Film> film, std::list<UserProperty>& p) const override;
diff --git a/src/lib/video_mxf_decoder.cc b/src/lib/video_mxf_decoder.cc
index 40d3a461a..3d1ed7f4e 100644
--- a/src/lib/video_mxf_decoder.cc
+++ b/src/lib/video_mxf_decoder.cc
@@ -24,10 +24,10 @@
#include "video_mxf_content.h"
#include "j2k_image_proxy.h"
#include "frame_interval_checker.h"
-#include <dcp/mono_picture_asset.h>
-#include <dcp/mono_picture_asset_reader.h>
-#include <dcp/stereo_picture_asset.h>
-#include <dcp/stereo_picture_asset_reader.h>
+#include <dcp/mono_j2k_picture_asset.h>
+#include <dcp/mono_j2k_picture_asset_reader.h>
+#include <dcp/stereo_j2k_picture_asset.h>
+#include <dcp/stereo_j2k_picture_asset_reader.h>
#include <dcp/exceptions.h>
@@ -44,7 +44,7 @@ VideoMXFDecoder::VideoMXFDecoder (shared_ptr<const Film> film, shared_ptr<const
video = make_shared<VideoDecoder>(this, content);
try {
- auto mono = make_shared<dcp::MonoPictureAsset>(_content->path(0));
+ auto mono = make_shared<dcp::MonoJ2KPictureAsset>(_content->path(0));
_mono_reader = mono->start_read ();
_mono_reader->set_check_hmac (false);
_size = mono->size ();
@@ -55,7 +55,7 @@ VideoMXFDecoder::VideoMXFDecoder (shared_ptr<const Film> film, shared_ptr<const
/* maybe it's stereo */
}
- auto stereo = make_shared<dcp::StereoPictureAsset>(_content->path(0));
+ auto stereo = make_shared<dcp::StereoJ2KPictureAsset>(_content->path(0));
_stereo_reader = stereo->start_read ();
_stereo_reader->set_check_hmac (false);
_size = stereo->size ();
@@ -76,18 +76,18 @@ VideoMXFDecoder::pass ()
video->emit (
film(),
std::make_shared<J2KImageProxy>(_mono_reader->get_frame(frame), _size, AV_PIX_FMT_XYZ12LE, optional<int>()),
- frame
+ _next
);
} else {
video->emit (
film(),
std::make_shared<J2KImageProxy>(_stereo_reader->get_frame(frame), _size, dcp::Eye::LEFT, AV_PIX_FMT_XYZ12LE, optional<int>()),
- frame
+ _next
);
video->emit (
film(),
std::make_shared<J2KImageProxy>(_stereo_reader->get_frame(frame), _size, dcp::Eye::RIGHT, AV_PIX_FMT_XYZ12LE, optional<int>()),
- frame
+ _next
);
}
diff --git a/src/lib/video_mxf_decoder.h b/src/lib/video_mxf_decoder.h
index 774e269c6..16d8b112e 100644
--- a/src/lib/video_mxf_decoder.h
+++ b/src/lib/video_mxf_decoder.h
@@ -20,8 +20,8 @@
#include "decoder.h"
-#include <dcp/mono_picture_asset_reader.h>
-#include <dcp/stereo_picture_asset_reader.h>
+#include <dcp/mono_j2k_picture_asset_reader.h>
+#include <dcp/stereo_j2k_picture_asset_reader.h>
class VideoMXFContent;
@@ -42,7 +42,7 @@ private:
/** Time of next thing to return from pass */
dcpomatic::ContentTime _next;
- std::shared_ptr<dcp::MonoPictureAssetReader> _mono_reader;
- std::shared_ptr<dcp::StereoPictureAssetReader> _stereo_reader;
+ std::shared_ptr<dcp::MonoJ2KPictureAssetReader> _mono_reader;
+ std::shared_ptr<dcp::StereoJ2KPictureAssetReader> _stereo_reader;
dcp::Size _size;
};
diff --git a/src/lib/video_mxf_examiner.cc b/src/lib/video_mxf_examiner.cc
index 7a05f3369..276664a14 100644
--- a/src/lib/video_mxf_examiner.cc
+++ b/src/lib/video_mxf_examiner.cc
@@ -21,8 +21,8 @@
#include "video_mxf_content.h"
#include "video_mxf_examiner.h"
#include <dcp/exceptions.h>
-#include <dcp/mono_picture_asset.h>
-#include <dcp/stereo_picture_asset.h>
+#include <dcp/mono_j2k_picture_asset.h>
+#include <dcp/stereo_j2k_picture_asset.h>
using std::shared_ptr;
using boost::optional;
@@ -30,7 +30,7 @@ using boost::optional;
VideoMXFExaminer::VideoMXFExaminer (shared_ptr<const VideoMXFContent> content)
{
try {
- _asset.reset (new dcp::MonoPictureAsset (content->path(0)));
+ _asset = std::make_shared<dcp::MonoJ2KPictureAsset>(content->path(0));
} catch (dcp::MXFFileError& e) {
/* maybe it's stereo */
} catch (dcp::ReadError& e) {
@@ -38,7 +38,7 @@ VideoMXFExaminer::VideoMXFExaminer (shared_ptr<const VideoMXFContent> content)
}
if (!_asset) {
- _asset.reset (new dcp::StereoPictureAsset (content->path(0)));
+ _asset = std::make_shared<dcp::StereoJ2KPictureAsset>(content->path(0));
}
}
diff --git a/src/lib/writer.cc b/src/lib/writer.cc
index 7b9defd73..f9293ed09 100644
--- a/src/lib/writer.cc
+++ b/src/lib/writer.cc
@@ -30,6 +30,7 @@
#include "dcpomatic_log.h"
#include "film.h"
#include "film_util.h"
+#include "frame_info.h"
#include "job.h"
#include "log.h"
#include "ratio.h"
@@ -39,6 +40,7 @@
#include "version.h"
#include "writer.h"
#include <dcp/cpl.h>
+#include <dcp/mono_mpeg2_picture_frame.h>
#include <dcp/locale_convert.h>
#include <dcp/raw_convert.h>
#include <dcp/reel_closed_caption_asset.h>
@@ -77,9 +79,10 @@ using namespace dcpomatic;
/** @param weak_job Job to report progress to, or 0.
* @param text_only true to enable only the text (subtitle/ccap) parts of the writer.
*/
-Writer::Writer(weak_ptr<const Film> weak_film, weak_ptr<Job> weak_job, bool text_only)
- : WeakConstFilm (weak_film)
+Writer::Writer(weak_ptr<const Film> weak_film, weak_ptr<Job> weak_job, boost::filesystem::path output_dir, bool text_only)
+ : WeakConstFilm(weak_film)
, _job(weak_job)
+ , _output_dir(output_dir)
/* These will be reset to sensible values when J2KEncoder is created */
, _maximum_frames_in_memory (8)
, _maximum_queue_size (8)
@@ -90,7 +93,7 @@ Writer::Writer(weak_ptr<const Film> weak_film, weak_ptr<Job> weak_job, bool text
int reel_index = 0;
auto const reels = film()->reels();
for (auto p: reels) {
- _reels.push_back (ReelWriter(weak_film, p, job, reel_index++, reels.size(), text_only));
+ _reels.push_back(ReelWriter(weak_film, p, job, reel_index++, reels.size(), text_only, _output_dir));
}
_last_written.resize (reels.size());
@@ -172,6 +175,13 @@ Writer::write (shared_ptr<const Data> encoded, Frame frame, Eyes eyes)
}
+void
+Writer::write(shared_ptr<dcp::MonoMPEG2PictureFrame> image, Frame frame)
+{
+ _reels[video_reel(frame)].write(image);
+}
+
+
bool
Writer::can_repeat (Frame frame) const
{
@@ -233,11 +243,7 @@ Writer::fake_write (Frame frame, Eyes eyes)
QueueItem qi;
qi.type = QueueItem::Type::FAKE;
-
- {
- shared_ptr<InfoFileHandle> info_file = film()->info_file_handle(_reels[reel].period(), true);
- qi.size = _reels[reel].read_frame_info(info_file, frame_in_reel, eyes).size;
- }
+ qi.info = J2KFrameInfo(film()->info_file_handle(_reels[reel].period(), true), frame_in_reel, eyes);
DCPOMATIC_ASSERT((film()->three_d() && eyes != Eyes::BOTH) || (!film()->three_d() && eyes == Eyes::BOTH));
@@ -411,7 +417,7 @@ try
if (i.type == QueueItem::Type::FULL) {
LOG_WARNING (N_("- type FULL, frame %1, eyes %2"), i.frame, (int) i.eyes);
} else {
- LOG_WARNING (N_("- type FAKE, size %1, frame %2, eyes %3"), i.size, i.frame, (int) i.eyes);
+ LOG_WARNING (N_("- type FAKE, size %1, frame %2, eyes %3"), i.info.size, i.frame, (int) i.eyes);
}
}
}
@@ -442,7 +448,7 @@ try
break;
case QueueItem::Type::FAKE:
LOG_DEBUG_ENCODE (N_("Writer FAKE-writes %1"), qi.frame);
- reel.fake_write (qi.size);
+ reel.fake_write(qi.info);
++_fake_written;
break;
case QueueItem::Type::REPEAT:
@@ -583,9 +589,8 @@ Writer::calculate_digests ()
}
-/** @param output_dcp Path to DCP folder to write */
void
-Writer::finish (boost::filesystem::path output_dcp)
+Writer::finish()
{
if (_thread.joinable()) {
LOG_GENERAL_NC ("Terminating writer thread");
@@ -596,12 +601,12 @@ Writer::finish (boost::filesystem::path output_dcp)
for (auto& reel: _reels) {
write_hanging_text(reel);
- reel.finish(output_dcp);
+ reel.finish(_output_dir);
}
LOG_GENERAL_NC ("Writing XML");
- dcp::DCP dcp (output_dcp);
+ dcp::DCP dcp(_output_dir);
auto cpl = make_shared<dcp::CPL>(
film()->dcp_name(),
@@ -616,7 +621,7 @@ Writer::finish (boost::filesystem::path output_dcp)
/* Add reels */
for (auto& i: _reels) {
- cpl->add(i.create_reel(_reel_assets, output_dcp, _have_subtitles, _have_closed_captions));
+ cpl->add(i.create_reel(_reel_assets, _output_dir, _have_subtitles, _have_closed_captions));
}
/* Add metadata */
@@ -718,12 +723,12 @@ Writer::finish (boost::filesystem::path output_dcp)
N_("Wrote %1 FULL, %2 FAKE, %3 REPEAT, %4 pushed to disk"), _full_written, _fake_written, _repeat_written, _pushed_to_disk
);
- write_cover_sheet (output_dcp);
+ write_cover_sheet();
}
void
-Writer::write_cover_sheet (boost::filesystem::path output_dcp)
+Writer::write_cover_sheet()
{
auto const cover = film()->file("COVER_SHEET.txt");
dcp::File file(cover, "w");
@@ -756,7 +761,7 @@ Writer::write_cover_sheet (boost::filesystem::path output_dcp)
boost::uintmax_t size = 0;
for (
- auto i = dcp::filesystem::recursive_directory_iterator(output_dcp);
+ auto i = dcp::filesystem::recursive_directory_iterator(_output_dir);
i != dcp::filesystem::recursive_directory_iterator();
++i) {
if (dcp::filesystem::is_regular_file(i->path())) {
diff --git a/src/lib/writer.h b/src/lib/writer.h
index f0f1fe69a..3e93c9b7b 100644
--- a/src/lib/writer.h
+++ b/src/lib/writer.h
@@ -38,6 +38,8 @@
#include "types.h"
#include "weak_film.h"
#include <dcp/atmos_frame.h>
+#include <dcp/frame_info.h>
+#include <dcp/mono_mpeg2_picture_frame.h>
#include <boost/thread.hpp>
#include <boost/thread/condition.hpp>
#include <list>
@@ -75,8 +77,8 @@ public:
/** encoded data for FULL */
std::shared_ptr<const dcp::Data> encoded;
- /** size of data for FAKE */
- int size = 0;
+ /** info for FAKE */
+ dcp::J2KFrameInfo info;
/** reel index */
size_t reel = 0;
/** frame index within the reel */
@@ -104,7 +106,7 @@ bool operator== (QueueItem const & a, QueueItem const & b);
class Writer : public ExceptionStore, public WeakConstFilm
{
public:
- Writer (std::weak_ptr<const Film>, std::weak_ptr<Job>, bool text_only = false);
+ Writer(std::weak_ptr<const Film>, std::weak_ptr<Job>, boost::filesystem::path output_dir, bool text_only = false);
~Writer ();
Writer (Writer const &) = delete;
@@ -123,7 +125,8 @@ public:
void write (std::vector<std::shared_ptr<dcpomatic::Font>> fonts);
void write (ReferencedReelAsset asset);
void write (std::shared_ptr<const dcp::AtmosFrame> atmos, dcpomatic::DCPTime time, AtmosMetadata metadata);
- void finish (boost::filesystem::path output_dcp);
+ void write (std::shared_ptr<dcp::MonoMPEG2PictureFrame> image, Frame frame);
+ void finish();
void set_encoder_threads (int threads);
@@ -139,7 +142,7 @@ private:
bool have_sequenced_image_at_queue_head ();
size_t video_reel (int frame) const;
void set_digest_progress(Job* job, int id, int64_t done, int64_t size);
- void write_cover_sheet (boost::filesystem::path output_dcp);
+ void write_cover_sheet();
void calculate_referenced_digests(std::function<void (int64_t, int64_t)> set_progress);
void write_hanging_text (ReelWriter& reel);
void calculate_digests ();
@@ -151,6 +154,7 @@ private:
std::map<DCPTextTrack, std::vector<ReelWriter>::iterator> _caption_reels;
std::vector<ReelWriter>::iterator _atmos_reel;
+ boost::filesystem::path _output_dir;
/** our thread */
boost::thread _thread;
/** true if our thread should finish */
diff --git a/src/lib/wscript b/src/lib/wscript
index 87a1ca787..33e68a108 100644
--- a/src/lib/wscript
+++ b/src/lib/wscript
@@ -49,7 +49,7 @@ sources = """
text_decoder.cc
case_insensitive_sorter.cc
check_content_job.cc
- cinema.cc
+ cinema_list.cc
cinema_sound_processor.cc
change_signaller.cc
collator.cc
@@ -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
@@ -67,9 +68,9 @@ sources = """
dcp_content.cc
dcp_content_type.cc
dcp_decoder.cc
- dcp_encoder.cc
dcp_examiner.cc
dcp_digest_file.cc
+ dcp_film_encoder.cc
dcp_subtitle.cc
dcp_subtitle_content.cc
dcp_subtitle_decoder.cc
@@ -84,11 +85,11 @@ sources = """
decoder_part.cc
digester.cc
dkdm_recipient.cc
+ dkdm_recipient_list.cc
dkdm_wrapper.cc
dolby_cp750.cc
email.cc
empty.cc
- encoder.cc
encode_server.cc
encode_server_finder.cc
encoded_log_entry.cc
@@ -98,6 +99,7 @@ sources = """
examine_ffmpeg_subtitles_job.cc
exceptions.cc
export_config.cc
+ frame_info.cc
file_group.cc
file_log.cc
filter_graph.cc
@@ -106,14 +108,15 @@ sources = """
ffmpeg_audio_stream.cc
ffmpeg_content.cc
ffmpeg_decoder.cc
- ffmpeg_encoder.cc
ffmpeg_examiner.cc
ffmpeg_file_encoder.cc
+ ffmpeg_film_encoder.cc
ffmpeg_image_proxy.cc
ffmpeg_stream.cc
ffmpeg_subtitle_stream.cc
ffmpeg_wrapper.cc
film.cc
+ film_encoder.cc
film_util.cc
filter.cc
font.cc
@@ -124,6 +127,7 @@ sources = """
frame_rate_change.cc
guess_crop.cc
hints.cc
+ id.cc
internet.cc
image.cc
image_content.cc
@@ -138,6 +142,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
@@ -150,6 +156,7 @@ sources = """
maths_util.cc
memory_util.cc
mid_side_decoder.cc
+ mpeg2_encoder.cc
named_channel.cc
overlaps.cc
pixel_quanta.cc
@@ -162,7 +169,9 @@ sources = """
reel_writer.cc
referenced_reel_asset.cc
release_notes.cc
+ remembered_asset.cc
render_text.cc
+ remote_j2k_encoder_thread.cc
resampler.cc
resolution.cc
rgba.cc
@@ -178,12 +187,15 @@ sources = """
state.cc
spl.cc
spl_entry.cc
+ sqlite_statement.cc
+ sqlite_table.cc
+ sqlite_transaction.cc
string_log_entry.cc
string_text_file.cc
string_text_file_content.cc
string_text_file_decoder.cc
subtitle_analysis.cc
- subtitle_encoder.cc
+ subtitle_film_encoder.cc
territory_type.cc
text_ring_buffers.cc
text_type.cc
@@ -202,9 +214,12 @@ sources = """
upmixer_b.cc
usl.cc
util.cc
+ variant.cc
verify_dcp_job.cc
video_content.cc
video_decoder.cc
+ video_encoder.cc
+ video_encoding.cc
video_filter_graph.cc
video_filter_graph_set.cc
video_frame_type.cc
@@ -230,7 +245,7 @@ def build(bld):
BOOST_FILESYSTEM BOOST_THREAD BOOST_DATETIME BOOST_SIGNALS2 BOOST_REGEX
SAMPLERATE POSTPROC TIFF SSH DCP CXML GLIB LZMA XML++
CURL ZIP BZ2 FONTCONFIG PANGOMM CAIROMM XMLSEC SUB ICU NETTLE PNG JPEG LEQM_NRT
- LIBZ
+ LIBZ SQLITE3
"""
if bld.env.TARGET_OSX:
@@ -244,6 +259,9 @@ def build(bld):
if bld.env.TARGET_LINUX:
obj.uselib += ' POLKIT'
+ if bld.env.ENABLE_GROK:
+ obj.source += ' grok_j2k_encoder_thread.cc'
+
if bld.env.TARGET_WINDOWS_64 or bld.env.TARGET_WINDOWS_32:
obj.uselib += ' WINSOCK2 DBGHELP SHLWAPI MSWSOCK BOOST_LOCALE SETUPAPI OLE32 UUID'
obj.source += ' cross_windows.cc'
diff --git a/src/tools/dcpomatic.cc b/src/tools/dcpomatic.cc
index 584ebd27d..0a6728731 100644
--- a/src/tools/dcpomatic.cc
+++ b/src/tools/dcpomatic.cc
@@ -26,6 +26,7 @@
#include "wx/about_dialog.h"
#include "wx/content_panel.h"
+#include "wx/dcp_referencing_dialog.h"
#include "wx/dkdm_dialog.h"
#include "wx/export_subtitles_dialog.h"
#include "wx/export_video_file_dialog.h"
@@ -41,6 +42,7 @@
#include "wx/id.h"
#include "wx/job_manager_view.h"
#include "wx/kdm_dialog.h"
+#include "wx/load_config_from_zip_dialog.h"
#include "wx/nag_dialog.h"
#include "wx/paste_dialog.h"
#include "wx/recreate_chain_dialog.h"
@@ -55,6 +57,7 @@
#include "wx/video_waveform_dialog.h"
#include "wx/wx_signal_manager.h"
#include "wx/wx_util.h"
+#include "wx/wx_variant.h"
#include "lib/analytics.h"
#include "lib/audio_content.h"
#include "lib/check_content_job.h"
@@ -73,9 +76,12 @@
#include "lib/email.h"
#include "lib/encode_server_finder.h"
#include "lib/exceptions.h"
-#include "lib/ffmpeg_encoder.h"
+#include "lib/ffmpeg_film_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"
@@ -85,15 +91,18 @@
#include "lib/screen.h"
#include "lib/send_kdm_email_job.h"
#include "lib/signal_manager.h"
-#include "lib/subtitle_encoder.h"
+#include "lib/subtitle_film_encoder.h"
#include "lib/text_content.h"
#include "lib/transcode_job.h"
+#include "lib/unzipper.h"
#include "lib/update_checker.h"
+#include "lib/variant.h"
#include "lib/version.h"
#include "lib/video_content.h"
#include <dcp/exceptions.h>
#include <dcp/filesystem.h>
#include <dcp/raw_convert.h>
+#include <dcp/scope_guard.h>
#include <dcp/warnings.h>
LIBDCP_DISABLE_WARNINGS
#include <wx/cmdline.h>
@@ -207,6 +216,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;
@@ -236,6 +246,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,
@@ -318,7 +329,7 @@ public:
#endif
_config_changed_connection = Config::instance()->Changed.connect(boost::bind(&DOMFrame::config_changed, this, _1));
- config_changed (Config::OTHER);
+ config_changed(Config::OTHER);
_analytics_message_connection = Analytics::instance()->Message.connect(boost::bind(&DOMFrame::analytics_message, this, _1, _2));
@@ -347,6 +358,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);
@@ -442,12 +454,13 @@ public:
}
}
+ /** Make a new film in the given path, using template_name as a template
+ * (or the default template if it's empty).
+ */
void new_film (boost::filesystem::path path, optional<string> template_name)
{
auto film = make_shared<Film>(path);
- if (template_name) {
- film->use_template (template_name.get());
- }
+ film->use_template(template_name);
film->set_name (path.filename().generic_string());
film->write_metadata ();
set_film (film);
@@ -458,6 +471,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 (
@@ -479,8 +493,10 @@ public:
auto const dir = e.file().parent_path();
if (dcp::filesystem::exists(dir / "ASSETMAP") || dcp::filesystem::exists(dir / "ASSETMAP.xml")) {
error_dialog (
- this, _("Could not open this folder as a DCP-o-matic project."),
- _("It looks like you are trying to open a DCP. File -> Open is for loading DCP-o-matic projects, not DCPs. To import a DCP, create a new project with File -> New and then click the \"Add DCP...\" button.")
+ this, variant::wx::insert_dcpomatic(_("Could not open this folder as a %s project.")),
+ variant::wx::insert_dcpomatic(
+ _("It looks like you are trying to open a DCP. File -> Open is for loading %s projects, not DCPs. "
+ "To import a DCP, create a new project with File -> New and then click the \"Add DCP...\" button."))
);
} else {
auto const p = std_to_wx(file.string ());
@@ -564,7 +580,7 @@ private:
if (!found_bad_chars.empty()) {
message += wxString::Format (_("Try removing the %s characters from your folder name."), std_to_wx(found_bad_chars).data());
} else {
- message += _("Please check that you do not have Windows controlled folder access enabled for DCP-o-matic.");
+ message += variant::wx::insert_dcpomatic(_("Please check that you do not have Windows controlled folder access enabled for %s."));
}
error_dialog (this, message, std_to_wx(e.what()));
#else
@@ -611,7 +627,11 @@ private:
SaveTemplateDialog dialog(this);
if (dialog.ShowModal() == wxID_OK) {
try {
- Config::instance()->save_template(_film, dialog.name());
+ if (dialog.name()) {
+ Config::instance()->save_template(_film, *dialog.name());
+ } else {
+ Config::instance()->save_default_template(_film);
+ }
} catch (exception& e) {
error_dialog(this, _("Could not save template."), std_to_wx(e.what()));
}
@@ -780,24 +800,31 @@ private:
{
FileDialog dialog(this, _("Specify ZIP file"), wxT("ZIP files (*.zip)|*.zip"), wxFD_OPEN, "Preferences");
- if (dialog.show()) {
- Config::instance()->load_from_zip(dialog.path());
+ if (!dialog.show()) {
+ return;
}
+
+ auto action = Config::CinemasAction::WRITE_TO_CURRENT_PATH;
+
+ if (Config::zip_contains_cinemas(dialog.path()) && Config::cinemas_file_from_zip(dialog.path()) != Config::instance()->cinemas_file()) {
+ LoadConfigFromZIPDialog how(this, dialog.path());
+ if (how.ShowModal() == wxID_CANCEL) {
+ return;
+ }
+
+ action = how.action();
+ }
+
+ Config::instance()->load_from_zip(dialog.path(), action);
}
void jobs_make_dcp ()
{
double required;
double available;
- bool can_hard_link;
- if (!_film->should_be_enough_disk_space (required, available, can_hard_link)) {
- wxString message;
- if (can_hard_link) {
- message = wxString::Format (_("The DCP for this film will take up about %.1f GB, and the disk that you are using only has %.1f GB available. Do you want to continue anyway?"), required, available);
- } else {
- message = wxString::Format (_("The DCP and intermediate files for this film will take up about %.1f GB, and the disk that you are using only has %.1f GB available. You would need half as much space if the filesystem supported hard links, but it does not. Do you want to continue anyway?"), required, available);
- }
+ if (!_film->should_be_enough_disk_space(required, available)) {
+ auto const message = wxString::Format(_("The DCP for this film will take up about %.1f GB, and the disk that you are using only has %.1f GB available. Do you want to continue anyway?"), required, available);
if (!confirm_dialog (this, message)) {
return;
}
@@ -827,6 +854,8 @@ private:
if (!confirm_dialog (this, wxString::Format (_("Do you want to overwrite the existing DCP %s?"), std_to_wx(dcp_dir.string()).data()))) {
return;
}
+
+ preserve_assets(dcp_dir, _film->assets_path());
dcp::filesystem::remove_all(dcp_dir);
}
@@ -1014,7 +1043,7 @@ private:
auto job = make_shared<TranscodeJob>(_film, TranscodeJob::ChangedBehaviour::EXAMINE_THEN_STOP);
job->set_encoder (
- make_shared<FFmpegEncoder> (
+ make_shared<FFmpegFilmEncoder>(
_film, job, dialog.path(), dialog.format(), dialog.mixdown_to_stereo(), dialog.split_reels(), dialog.split_streams(), dialog.x264_crf())
);
JobManager::instance()->add (job);
@@ -1029,7 +1058,7 @@ private:
}
auto job = make_shared<TranscodeJob>(_film, TranscodeJob::ChangedBehaviour::EXAMINE_THEN_STOP);
job->set_encoder(
- make_shared<SubtitleEncoder>(_film, job, dialog.path(), _film->isdcf_name(true), dialog.split_reels(), dialog.include_font())
+ make_shared<SubtitleFilmEncoder>(_film, job, dialog.path(), _film->isdcf_name(true), dialog.split_reels(), dialog.include_font())
);
JobManager::instance()->add(job);
}
@@ -1071,6 +1100,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) {
@@ -1170,6 +1210,7 @@ private:
FontConfig::drop();
ev.Skip ();
+ JobManager::drop ();
}
void active_jobs_changed()
@@ -1193,6 +1234,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) {
@@ -1230,6 +1278,10 @@ private:
enabled = false;
}
+ if ((j.second & NEEDS_DCP_CONTENT) && !have_dcp_content) {
+ enabled = false;
+ }
+
j.first->Enable (enabled);
}
}
@@ -1340,7 +1392,7 @@ private:
add_item (jobs_menu, _("Make &KDMs...\tCtrl-K"), ID_jobs_make_kdms, NEEDS_FILM);
/* [Shortcut] Ctrl+D:Make DKDMs */
add_item (jobs_menu, _("Make &DKDMs...\tCtrl-D"), ID_jobs_make_dkdms, NEEDS_FILM);
- add_item (jobs_menu, _("Make DKDM for DCP-o-matic..."), ID_jobs_make_self_dkdm, NEEDS_FILM | NEEDS_ENCRYPTION);
+ add_item(jobs_menu, variant::wx::insert_dcpomatic(_("Make DKDM for %s...")), ID_jobs_make_self_dkdm, NEEDS_FILM | NEEDS_ENCRYPTION);
jobs_menu->AppendSeparator ();
/* [Shortcut] Ctrl+E:Export video file */
add_item (jobs_menu, _("Export video file...\tCtrl-E"), ID_jobs_export_video_file, NEEDS_FILM);
@@ -1363,6 +1415,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);
@@ -1376,11 +1429,13 @@ private:
wxMenu* help = new wxMenu;
#ifdef __WXOSX__
- add_item (help, _("About DCP-o-matic"), wxID_ABOUT, ALWAYS);
+ add_item(help, variant::wx::insert_dcpomatic(_("About %s")), wxID_ABOUT, ALWAYS);
#else
add_item (help, _("About"), wxID_ABOUT, ALWAYS);
#endif
- add_item (help, _("Report a problem..."), ID_help_report_a_problem, NEEDS_FILM);
+ if (variant::show_report_a_problem()) {
+ add_item(help, _("Report a problem..."), ID_help_report_a_problem, NEEDS_FILM);
+ }
m->Append (_file_menu, _("&File"));
m->Append (edit, _("&Edit"));
@@ -1390,48 +1445,19 @@ private:
m->Append (help, _("&Help"));
}
- void config_changed (Config::Property what)
+ void config_changed(Config::Property what)
{
/* Instantly save any config changes when using the DCP-o-matic GUI */
- switch (what) {
- case Config::CINEMAS:
- try {
- Config::instance()->write_cinemas();
- } catch (exception& e) {
- error_dialog (
- this,
- wxString::Format (
- _("Could not write to cinemas file at %s. Your changes have not been saved."),
- std_to_wx (Config::instance()->cinemas_file().string()).data()
- )
- );
- }
- break;
- case Config::DKDM_RECIPIENTS:
- try {
- Config::instance()->write_dkdm_recipients();
- } catch (exception& e) {
- error_dialog (
- this,
- wxString::Format (
- _("Could not write to DKDM recipients file at %s. Your changes have not been saved."),
- std_to_wx(Config::instance()->dkdm_recipients_file().string()).data()
- )
- );
- }
- break;
- default:
- try {
- Config::instance()->write_config();
- } catch (exception& e) {
- error_dialog (
- this,
- wxString::Format (
- _("Could not write to config file at %s. Your changes have not been saved."),
- std_to_wx (Config::instance()->cinemas_file().string()).data()
- )
- );
- }
+ try {
+ Config::instance()->write_config();
+ } catch (exception& e) {
+ error_dialog (
+ this,
+ wxString::Format (
+ _("Could not write to config file at %s. Your changes have not been saved."),
+ std_to_wx (Config::instance()->cinemas_file().string()).data()
+ )
+ );
}
for (int i = 0; i < _history_items; ++i) {
@@ -1467,6 +1493,14 @@ private:
_history_items = history.size ();
dcpomatic_log->set_types (Config::instance()->log_types());
+
+#ifdef DCPOMATIC_GROK
+ if (what == Config::GROK) {
+ setup_grok_library_path();
+ }
+#else
+ LIBDCP_UNUSED(what);
+#endif
}
void update_checker_state_changed ()
@@ -1488,9 +1522,9 @@ private:
UpdateDialog dialog(this, uc->stable(), uc->test());
dialog.ShowModal();
} else if (uc->state() == UpdateChecker::State::FAILED) {
- error_dialog (this, _("The DCP-o-matic download server could not be contacted."));
+ error_dialog(this, variant::wx::insert_dcpomatic(_("The %s download server could not be contacted.")));
} else {
- error_dialog (this, _("There are no new versions of DCP-o-matic available."));
+ error_dialog(this, variant::wx::insert_dcpomatic(_("There are no new versions of %s available.")));
}
_update_news_requested = false;
@@ -1528,7 +1562,7 @@ private:
void set_title ()
{
- auto s = wx_to_std(_("DCP-o-matic"));
+ auto s = variant::dcpomatic();
if (_film) {
if (_film->directory()) {
s += " - " + _film->directory()->string();
@@ -1548,6 +1582,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;
@@ -1571,7 +1606,7 @@ static const wxCmdLineEntryDesc command_line_description[] = {
{ wxCMD_LINE_SWITCH, "n", "new", "create new film", wxCMD_LINE_VAL_NONE, wxCMD_LINE_PARAM_OPTIONAL },
{ wxCMD_LINE_OPTION, "c", "content", "add content file / directory", wxCMD_LINE_VAL_STRING, wxCMD_LINE_PARAM_OPTIONAL },
{ wxCMD_LINE_OPTION, "d", "dcp", "add content DCP", wxCMD_LINE_VAL_STRING, wxCMD_LINE_PARAM_OPTIONAL },
- { wxCMD_LINE_SWITCH, "v", "version", "show DCP-o-matic version", wxCMD_LINE_VAL_NONE, wxCMD_LINE_PARAM_OPTIONAL },
+ { wxCMD_LINE_SWITCH, "v", "version", "show version", wxCMD_LINE_VAL_NONE, wxCMD_LINE_PARAM_OPTIONAL },
{ wxCMD_LINE_OPTION, "", "config", "directory containing config.xml and cinemas.xml", wxCMD_LINE_VAL_STRING, wxCMD_LINE_PARAM_OPTIONAL },
{ wxCMD_LINE_PARAM, 0, 0, "film to load or create", wxCMD_LINE_VAL_STRING, wxCMD_LINE_PARAM_OPTIONAL },
{ wxCMD_LINE_NONE, "", "", "", wxCmdLineParamType (0), 0 }
@@ -1615,7 +1650,7 @@ private:
setvbuf(hf_in, NULL, _IONBF, 128);
*stdin = *hf_in;
- cout << "DCP-o-matic is starting." << "\n";
+ cout << variant::insert_dcpomatic("%1 is starting.") << "\n";
}
#endif
wxInitAllImageHandlers ();
@@ -1625,7 +1660,7 @@ private:
_splash = maybe_show_splash ();
- SetAppName (_("DCP-o-matic"));
+ SetAppName(variant::wx::dcpomatic());
if (!wxApp::OnInit()) {
return false;
@@ -1664,7 +1699,7 @@ private:
*/
Config::Bad.connect (boost::bind(&App::config_bad, this, _1));
- _frame = new DOMFrame (_("DCP-o-matic"));
+ _frame = new DOMFrame(variant::wx::dcpomatic());
SetTopWindow (_frame);
_frame->Maximize ();
close_splash ();
@@ -1672,7 +1707,14 @@ private:
if (running_32_on_64 ()) {
NagDialog::maybe_nag (
_frame, Config::NAG_32_ON_64,
- _("You are running the 32-bit version of DCP-o-matic on a 64-bit version of Windows. This will limit the memory available to DCP-o-matic and may cause errors. You are strongly advised to install the 64-bit version of DCP-o-matic."),
+ wxString::Format(
+ _("You are running the 32-bit version of %s on a 64-bit version of Windows. "
+ "This will limit the memory available to %s and may cause errors. You are "
+ "strongly advised to install the 64-bit version of %s."),
+ variant::wx::dcpomatic(),
+ variant::wx::dcpomatic(),
+ variant::wx::dcpomatic()
+ ),
false);
}
@@ -1715,11 +1757,16 @@ private:
notes.Centre();
notes.ShowModal();
}
+
+#ifdef DCPOMATIC_GROK
+ grk_plugin::setMessengerLogger(new grk_plugin::GrokLogger("[GROK] "));
+ setup_grok_library_path();
+#endif
}
catch (exception& e)
{
close_splash();
- error_dialog (nullptr, wxString::Format ("DCP-o-matic could not start."), std_to_wx(e.what()));
+ error_dialog(nullptr, variant::wx::insert_dcpomatic(_("%s could not start.")), std_to_wx(e.what()));
}
return true;
@@ -1772,31 +1819,34 @@ private:
error_dialog (
nullptr,
wxString::Format(
- _("An exception occurred: %s (%s)\n\n") + REPORT_PROBLEM,
- std_to_wx (e.what()),
- std_to_wx (e.file().string().c_str())
+ _("An exception occurred: %s (%s)\n\n%s"),
+ std_to_wx(e.what()),
+ std_to_wx(e.file().string().c_str()),
+ wx::report_problem()
)
);
} catch (boost::filesystem::filesystem_error& e) {
error_dialog (
nullptr,
wxString::Format(
- _("An exception occurred: %s (%s) (%s)\n\n") + REPORT_PROBLEM,
- std_to_wx (e.what()),
- std_to_wx (e.path1().string()),
- std_to_wx (e.path2().string())
+ _("An exception occurred: %s (%s) (%s)\n\n%s"),
+ std_to_wx(e.what()),
+ std_to_wx(e.path1().string()),
+ std_to_wx(e.path2().string()),
+ wx::report_problem()
)
);
} catch (exception& e) {
error_dialog (
nullptr,
wxString::Format(
- _("An exception occurred: %s.\n\n") + REPORT_PROBLEM,
- std_to_wx (e.what ())
+ _("An exception occurred: %s.\n\n%s"),
+ std_to_wx(e.what()),
+ wx::report_problem()
)
);
} catch (...) {
- error_dialog (0, _("An unknown exception occurred.") + " " + REPORT_PROBLEM);
+ error_dialog(nullptr, _("An unknown exception occurred.") + " " + wx::report_problem());
}
}
@@ -1862,9 +1912,11 @@ private:
}
RecreateChainDialog dialog(
_frame, _("Recreate signing certificates"),
- _("The certificate chain that DCP-o-matic uses for signing DCPs and KDMs contains a small error\n"
- "which will prevent DCPs from being validated correctly on some systems. Do you want to re-create\n"
- "the certificate chain for signing DCPs and KDMs?"),
+ variant::wx::insert_dcpomatic(
+ _("The certificate chain that %s uses for signing DCPs and KDMs contains a small error\n"
+ "which will prevent DCPs from being validated correctly on some systems. Do you want to re-create\n"
+ "the certificate chain for signing DCPs and KDMs?")
+ ),
_("Do nothing"),
Config::NAG_BAD_SIGNER_CHAIN_UTF8
);
@@ -1877,9 +1929,11 @@ private:
}
RecreateChainDialog dialog(
_frame, _("Recreate signing certificates"),
- _("The certificate chain that DCP-o-matic uses for signing DCPs and KDMs has a validity period\n"
- "that is too long. This will cause problems playing back DCPs on some systems.\n"
- "Do you want to re-create the certificate chain for signing DCPs and KDMs?"),
+ variant::wx::insert_dcpomatic(
+ _("The certificate chain that %s uses for signing DCPs and KDMs has a validity period\n"
+ "that is too long. This will cause problems playing back DCPs on some systems.\n"
+ "Do you want to re-create the certificate chain for signing DCPs and KDMs?")
+ ),
_("Do nothing"),
Config::NAG_BAD_SIGNER_CHAIN_VALIDITY
);
@@ -1889,10 +1943,14 @@ private:
{
RecreateChainDialog dialog(
_frame, _("Recreate signing certificates"),
- _("The certificate chain that DCP-o-matic uses for signing DCPs and KDMs is inconsistent and\n"
- "cannot be used. DCP-o-matic cannot start unless you re-create it. Do you want to re-create\n"
- "the certificate chain for signing DCPs and KDMs?"),
- _("Close DCP-o-matic")
+ wxString::Format(
+ _("The certificate chain that %s uses for signing DCPs and KDMs is inconsistent and\n"
+ "cannot be used. %s cannot start unless you re-create it. Do you want to re-create\n"
+ "the certificate chain for signing DCPs and KDMs?"),
+ variant::wx::dcpomatic(),
+ variant::wx::dcpomatic()
+ ),
+ variant::wx::insert_dcpomatic(_("Close %s"))
);
if (dialog.ShowModal() != wxID_OK) {
exit (EXIT_FAILURE);
@@ -1903,11 +1961,15 @@ private:
{
RecreateChainDialog dialog(
_frame, _("Recreate KDM decryption chain"),
- _("The certificate chain that DCP-o-matic uses for decrypting KDMs is inconsistent and\n"
- "cannot be used. DCP-o-matic cannot start unless you re-create it. Do you want to re-create\n"
- "the certificate chain for decrypting KDMs? You may want to say \"No\" here and back up your\n"
- "configuration before continuing."),
- _("Close DCP-o-matic")
+ wxString::Format(
+ _("The certificate chain that %s uses for decrypting KDMs is inconsistent and\n"
+ "cannot be used. %s cannot start unless you re-create it. Do you want to re-create\n"
+ "the certificate chain for decrypting KDMs? You may want to say \"No\" here and back up your\n"
+ "configuration before continuing."),
+ variant::wx::dcpomatic(),
+ variant::wx::dcpomatic()
+ ),
+ variant::wx::insert_dcpomatic(_("Close %s"))
);
if (dialog.ShowModal() != wxID_OK) {
exit (EXIT_FAILURE);
@@ -1918,10 +1980,14 @@ private:
{
RecreateChainDialog dialog(
_frame, _("Recreate signing certificates"),
- _("The certificate chain that DCP-o-matic uses for signing DCPs and KDMs contains a small error\n"
- "which will prevent DCPs from being validated correctly on some systems. This error was caused\n"
- "by a bug in DCP-o-matic which has now been fixed. Do you want to re-create the certificate chain\n"
- "for signing DCPs and KDMs?"),
+ wxString::Format(
+ _("The certificate chain that %s uses for signing DCPs and KDMs contains a small error\n"
+ "which will prevent DCPs from being validated correctly on some systems. This error was caused\n"
+ "by a bug in %s which has now been fixed. Do you want to re-create the certificate chain\n"
+ "for signing DCPs and KDMs?"),
+ variant::wx::dcpomatic(),
+ variant::wx::dcpomatic()
+ ),
_("Do nothing"),
Config::NAG_BAD_SIGNER_DN_QUALIFIER
);
diff --git a/src/tools/dcpomatic_batch.cc b/src/tools/dcpomatic_batch.cc
index e476c2163..51d700d20 100644
--- a/src/tools/dcpomatic_batch.cc
+++ b/src/tools/dcpomatic_batch.cc
@@ -28,10 +28,14 @@
#include "wx/wx_ptr.h"
#include "wx/wx_signal_manager.h"
#include "wx/wx_util.h"
+#include "wx/wx_variant.h"
#include "lib/compose.hpp"
#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"
@@ -216,9 +220,8 @@ public:
double total_required;
double available;
- bool can_hard_link;
- film->should_be_enough_disk_space (total_required, available, can_hard_link);
+ film->should_be_enough_disk_space(total_required, available);
set<shared_ptr<const Film>> films;
@@ -235,7 +238,7 @@ public:
}
double required;
- i->should_be_enough_disk_space (required, available, can_hard_link);
+ i->should_be_enough_disk_space(required, available);
total_required += (1 - progress) * required;
}
@@ -289,6 +292,7 @@ private:
}
ev.Skip ();
+ JobManager::drop ();
}
void file_add_film ()
@@ -353,31 +357,25 @@ private:
void config_changed (Config::Property what)
{
/* Instantly save any config changes when using the DCP-o-matic GUI */
- if (what == Config::CINEMAS) {
- try {
- Config::instance()->write_cinemas();
- } catch (exception& e) {
- error_dialog (
- this,
- wxString::Format(
- _("Could not write to cinemas file at %s. Your changes have not been saved."),
- std_to_wx (Config::instance()->cinemas_file().string()).data()
- )
- );
- }
- } else {
- try {
- Config::instance()->write_config();
- } catch (exception& e) {
- error_dialog (
- this,
- wxString::Format(
- _("Could not write to config file at %s. Your changes have not been saved."),
- std_to_wx (Config::instance()->cinemas_file().string()).data()
- )
- );
- }
+ try {
+ Config::instance()->write_config();
+ } catch (exception& e) {
+ error_dialog (
+ this,
+ wxString::Format(
+ _("Could not write to config file at %s. Your changes have not been saved."),
+ std_to_wx (Config::instance()->cinemas_file().string()).data()
+ )
+ );
+ }
+
+#ifdef DCPOMATIC_GROK
+ if (what == Config::GROK) {
+ setup_grok_library_path();
}
+#else
+ LIBDCP_UNUSED(what);
+#endif
}
boost::optional<boost::filesystem::path> _last_parent;
@@ -426,7 +424,7 @@ class App : public wxApp
{
wxInitAllImageHandlers ();
- SetAppName (_("DCP-o-matic Batch Converter"));
+ SetAppName(variant::wx::dcpomatic_batch_converter());
is_batch_converter = true;
Config::FailedToLoad.connect(boost::bind(&App::config_failed_to_load, this, _1));
@@ -462,7 +460,7 @@ class App : public wxApp
*/
Config::drop ();
- _frame = new DOMFrame (_("DCP-o-matic Batch Converter"));
+ _frame = new DOMFrame(variant::wx::dcpomatic_batch_converter());
SetTopWindow (_frame);
_frame->Maximize ();
if (splash) {
@@ -475,7 +473,11 @@ class App : public wxApp
server->StartJob.connect(bind(&DOMFrame::start_job, _frame, _1));
new thread (boost::bind (&JobServer::run, server));
} catch (boost::system::system_error& e) {
- error_dialog(_frame, _("Could not listen for new batch jobs. Perhaps another instance of the DCP-o-matic Batch Converter is running."));
+ error_dialog(
+ _frame,
+ variant::wx::insert_dcpomatic_batch_converter(
+ _("Could not listen for new batch jobs. Perhaps another instance of the %s is running.")
+ ));
}
signal_manager = new wxSignalManager (this);
@@ -498,6 +500,11 @@ class App : public wxApp
}
}
+#ifdef DCPOMATIC_GROK
+ grk_plugin::setMessengerLogger(new grk_plugin::GrokLogger("[GROK] "));
+ setup_grok_library_path();
+#endif
+
return true;
}
diff --git a/src/tools/dcpomatic_cli.cc b/src/tools/dcpomatic_cli.cc
index 65252ff29..49caf5cb2 100644
--- a/src/tools/dcpomatic_cli.cc
+++ b/src/tools/dcpomatic_cli.cc
@@ -25,9 +25,12 @@
#include "lib/cross.h"
#include "lib/dcpomatic_log.h"
#include "lib/encode_server_finder.h"
-#include "lib/ffmpeg_encoder.h"
+#include "lib/ffmpeg_film_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"
@@ -37,6 +40,7 @@
#include "lib/signal_manager.h"
#include "lib/transcode_job.h"
#include "lib/util.h"
+#include "lib/variant.h"
#include "lib/version.h"
#include "lib/video_content.h"
#include <dcp/filesystem.h>
@@ -63,7 +67,7 @@ static void
help (string n)
{
cerr << "Syntax: " << n << " [OPTION] [<FILM>]\n"
- << " -v, --version show DCP-o-matic version\n"
+ << variant::insert_dcpomatic(" -v, --version show %1 version\n")
<< " -h, --help show this help\n"
<< " -f, --flags show flags passed to C++ compiler on build\n"
<< " -n, --no-progress do not print progress to stdout\n"
@@ -72,7 +76,7 @@ help (string n)
<< " -j, --json <port> run a JSON server on the specified port\n"
<< " -k, --keep-going keep running even when the job is complete\n"
<< " -s, --servers <file> specify servers to use in a text file\n"
- << " -l, --list-servers just display a list of encoding servers that DCP-o-matic is configured to use; don't encode\n"
+ << variant::insert_dcpomatic(" -l, --list-servers just display a list of encoding servers that %1 is configured to use; don't encode\n")
<< " -d, --dcp-path echo DCP's path to stdout on successful completion (implies -n)\n"
<< " -c, --config <dir> directory containing config.xml and cinemas.xml\n"
<< " --dump just dump a summary of the film's settings; don't encode\n"
@@ -90,7 +94,7 @@ print_dump (shared_ptr<Film> film)
{
cout << film->dcp_name (true) << "\n"
<< film->container()->container_nickname() << " at " << ((film->resolution() == Resolution::TWO_K) ? "2K" : "4K") << "\n"
- << (film->j2k_bandwidth() / 1000000) << "Mbit/s" << "\n"
+ << (film->video_bit_rate(film->video_encoding()) / 1000000) << "Mbit/s" << "\n"
<< "Duration " << (film->length().timecode(film->video_frame_rate())) << "\n"
<< "Output " << film->video_frame_rate() << "fps " << (film->three_d() ? "3D" : "2D") << " " << (film->audio_frame_rate() / 1000) << "kHz\n"
<< (film->interop() ? "Inter-Op" : "SMPTE") << " " << (film->encrypted() ? "encrypted" : "unencrypted") << "\n";
@@ -414,7 +418,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 +501,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";
@@ -510,7 +519,7 @@ main (int argc, char* argv[])
if (export_format) {
auto job = std::make_shared<TranscodeJob>(film, behaviour);
job->set_encoder (
- std::make_shared<FFmpegEncoder> (
+ std::make_shared<FFmpegFilmEncoder>(
film, job, *export_filename, *export_format == "mp4" ? ExportFormat::H264_AAC : ExportFormat::PRORES_HQ, false, false, false, 23
)
);
diff --git a/src/tools/dcpomatic_combiner.cc b/src/tools/dcpomatic_combiner.cc
index c135184e6..26ca0022b 100644
--- a/src/tools/dcpomatic_combiner.cc
+++ b/src/tools/dcpomatic_combiner.cc
@@ -23,11 +23,14 @@
#include "wx/dir_picker_ctrl.h"
#include "wx/editable_list.h"
#include "wx/wx_signal_manager.h"
+#include "wx/wx_util.h"
+#include "wx/wx_variant.h"
#include "lib/combine_dcp_job.h"
#include "lib/config.h"
#include "lib/constants.h"
#include "lib/cross.h"
#include "lib/job_manager.h"
+#include "lib/util.h"
#include <dcp/combine.h>
LIBDCP_DISABLE_WARNINGS
#include <wx/filepicker.h>
@@ -175,7 +178,7 @@ private:
auto jm = JobManager::instance ();
jm->add (make_shared<CombineDCPJob>(_inputs, output, wx_to_std(_annotation_text->GetValue())));
- bool const ok = display_progress(_("DCP-o-matic Combiner"), _("Combining DCPs"));
+ bool const ok = display_progress(variant::wx::dcpomatic_combiner(), _("Combining DCPs"));
if (!ok) {
return;
}
@@ -219,7 +222,7 @@ public:
Config::FailedToLoad.connect(boost::bind(&App::config_failed_to_load, this, _1));
Config::Warning.connect (boost::bind (&App::config_warning, this, _1));
- SetAppName (_("DCP-o-matic Combiner"));
+ SetAppName(variant::wx::dcpomatic_combiner());
if (!wxApp::OnInit()) {
return false;
@@ -253,7 +256,7 @@ public:
*/
Config::drop ();
- _frame = new DOMFrame(_("DCP-o-matic Combiner"));
+ _frame = new DOMFrame(variant::wx::dcpomatic_combiner());
SetTopWindow (_frame);
_frame->Show ();
@@ -263,7 +266,7 @@ public:
}
catch (exception& e)
{
- error_dialog(nullptr, wxString::Format("DCP-o-matic Combiner could not start."), std_to_wx(e.what()));
+ error_dialog(nullptr, wxString::Format(_("%s could not start (%s)"), variant::wx::dcpomatic_combiner()), std_to_wx(e.what()));
return false;
}
@@ -294,21 +297,23 @@ public:
error_dialog (
0,
wxString::Format(
- _("An exception occurred: %s (%s)\n\n") + REPORT_PROBLEM,
- std_to_wx (e.what()),
- std_to_wx (e.file().string().c_str ())
+ _("An exception occurred: %s (%s)\n\n%s"),
+ std_to_wx(e.what()),
+ std_to_wx(e.file().string().c_str()),
+ wx::report_problem()
)
);
} catch (exception& e) {
error_dialog (
0,
wxString::Format(
- _("An exception occurred: %s.\n\n") + REPORT_PROBLEM,
- std_to_wx (e.what ())
+ _("An exception occurred: %s\n\n%s"),
+ std_to_wx(e.what()),
+ wx::report_problem()
)
);
} catch (...) {
- error_dialog (nullptr, _("An unknown exception occurred.") + " " + REPORT_PROBLEM);
+ error_dialog(nullptr, _("An unknown exception occurred.") + " " + wx::report_problem());
}
}
diff --git a/src/tools/dcpomatic_disk.cc b/src/tools/dcpomatic_disk.cc
index 395b60a88..a20a062e3 100644
--- a/src/tools/dcpomatic_disk.cc
+++ b/src/tools/dcpomatic_disk.cc
@@ -29,6 +29,7 @@
#include "wx/wx_util.h"
#include "wx/wx_signal_manager.h"
#include "wx/wx_util.h"
+#include "wx/wx_variant.h"
#include "lib/config.h"
#include "lib/constants.h"
#include "lib/copy_to_drive_job.h"
@@ -43,6 +44,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 +132,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 +151,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 +161,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 +174,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 +183,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 +282,7 @@ private:
}
ev.Skip ();
+ JobManager::drop ();
}
void copy ()
@@ -313,7 +326,7 @@ private:
#if defined(DCPOMATIC_WINDOWS)
auto m = make_wx<MessageDialog>(
this,
- _("DCP-o-matic Disk Writer"),
+ variant::wx::dcpomatic_disk_writer(),
_("Do you see a 'User Account Control' dialogue asking about dcpomatic2_disk_writer.exe? If so, click 'Yes', then try again.")
);
m->ShowModal ();
@@ -321,8 +334,8 @@ private:
#elif defined(DCPOMATIC_OSX)
auto m = make_wx<MessageDialog>(
this,
- _("DCP-o-matic Disk Writer"),
- _("Did you install the DCP-o-matic Disk Writer.pkg from the .dmg? Please check and try again.")
+ variant::wx::dcpomatic_disk_writer(),
+ variant::wx::insert_dcpomatic(_("Did you install the %s Disk Writer.pkg from the .dmg? Please check and try again."))
);
m->ShowModal ();
return;
@@ -354,7 +367,7 @@ private:
if (!reply || reply->type() != DiskWriterBackEndResponse::Type::OK) {
auto m = make_wx<MessageDialog>(
this,
- _("DCP-o-matic Disk Writer"),
+ variant::wx::dcpomatic_disk_writer(),
wxString::Format(
_("The drive %s could not be unmounted.\nClose any application that is using it, then try again. (%s)"),
std_to_wx(drive.description()),
@@ -442,7 +455,7 @@ public:
Config::FailedToLoad.connect (boost::bind (&App::config_failed_to_load, this));
Config::Warning.connect (boost::bind (&App::config_warning, this, _1));
- SetAppName (_("DCP-o-matic Disk Writer"));
+ SetAppName(variant::wx::dcpomatic_disk_writer());
if (!wxApp::OnInit()) {
return false;
@@ -483,12 +496,17 @@ public:
return false;
}
if (!warning->confirmed()) {
- message_dialog(nullptr, _("You did not correctly confirm that you read the warning that was just shown. DCP-o-matic Disk Writer will close now. Please try again."));
+ message_dialog(
+ nullptr,
+ variant::wx::insert_dcpomatic_disk_writer(
+ _("You did not correctly confirm that you read the warning that was just shown. %s will close now. Please try again.")
+ )
+ );
return false;
}
}
- _frame = new DOMFrame (_("DCP-o-matic Disk Writer"));
+ _frame = new DOMFrame(variant::wx::dcpomatic_disk_writer());
SetTopWindow (_frame);
_frame->Show ();
@@ -502,7 +520,7 @@ public:
}
catch (exception& e)
{
- error_dialog (0, wxString::Format ("DCP-o-matic could not start."), std_to_wx(e.what()));
+ error_dialog(nullptr, wxString::Format(_("%s could not start"), variant::wx::dcpomatic_disk_writer()), std_to_wx(e.what()));
return false;
}
@@ -551,21 +569,23 @@ public:
error_dialog (
0,
wxString::Format (
- _("An exception occurred: %s (%s)\n\n") + REPORT_PROBLEM,
- std_to_wx (e.what()),
- std_to_wx (e.file().string().c_str ())
+ _("An exception occurred: %s (%s)\n\n%s"),
+ std_to_wx(e.what()),
+ std_to_wx(e.file().string().c_str()),
+ wx::report_problem()
)
);
} catch (exception& e) {
error_dialog (
0,
wxString::Format (
- _("An exception occurred: %s.\n\n") + REPORT_PROBLEM,
- std_to_wx (e.what ())
+ _("An exception occurred: %s.\n\n%s"),
+ std_to_wx(e.what()),
+ wx::report_problem()
)
);
} catch (...) {
- error_dialog (0, _("An unknown exception occurred.") + " " + REPORT_PROBLEM);
+ error_dialog(nullptr, _("An unknown exception occurred.") + " " + wx::report_problem());
}
}
diff --git a/src/tools/dcpomatic_editor.cc b/src/tools/dcpomatic_editor.cc
index 14ff6da7f..47c7bac14 100644
--- a/src/tools/dcpomatic_editor.cc
+++ b/src/tools/dcpomatic_editor.cc
@@ -24,10 +24,13 @@
#include "wx/id.h"
#include "wx/wx_signal_manager.h"
#include "wx/wx_util.h"
+#include "wx/wx_variant.h"
#include "lib/constants.h"
#include "lib/cross.h"
#include "lib/dcpomatic_log.h"
#include "lib/null_log.h"
+#include "lib/util.h"
+#include "lib/variant.h"
#include <dcp/cpl.h>
#include <dcp/dcp.h>
#include <dcp/reel.h>
@@ -290,14 +293,14 @@ class DOMFrame : public wxFrame
{
public:
DOMFrame ()
- : wxFrame(nullptr, -1, _("DCP-o-matic Editor"))
+ : wxFrame(nullptr, -1, variant::wx::dcpomatic_editor())
, _main_sizer(new wxBoxSizer(wxVERTICAL))
{
dcpomatic_log = make_shared<NullLog>();
#if defined(DCPOMATIC_WINDOWS)
maybe_open_console();
- std::cout << "DCP-o-matic Editor is starting." << "\n";
+ std::cout << variant::dcpomatic_editor() << " is starting." << "\n";
#endif
auto bar = new wxMenuBar;
@@ -360,7 +363,7 @@ private:
auto help = new wxMenu;
#ifdef __WXOSX__
- help->Append (wxID_ABOUT, _("About DCP-o-matic"));
+ help->Append(wxID_ABOUT, variant::wx::insert_dcpomatic_editor(_("About %s")));
#else
help->Append (wxID_ABOUT, _("About"));
#endif
@@ -444,7 +447,7 @@ private:
splash = maybe_show_splash ();
- SetAppName (_("DCP-o-matic Editor"));
+ SetAppName(variant::wx::dcpomatic_editor());
if (!wxApp::OnInit()) {
return false;
@@ -495,7 +498,7 @@ private:
if (splash) {
splash->Destroy ();
}
- error_dialog (0, _("DCP-o-matic Editor could not start."), std_to_wx(e.what()));
+ error_dialog(nullptr, variant::wx::insert_dcpomatic_editor(_("%s could not start.")), std_to_wx(e.what()));
}
return true;
@@ -524,21 +527,23 @@ private:
error_dialog (
0,
wxString::Format (
- _("An exception occurred: %s (%s)\n\n") + REPORT_PROBLEM,
- std_to_wx (e.what()),
- std_to_wx (e.file().string().c_str ())
+ _("An exception occurred: %s (%s)\n\n%s"),
+ std_to_wx(e.what()),
+ std_to_wx(e.file().string().c_str()),
+ wx::report_problem()
)
);
} catch (exception& e) {
error_dialog (
0,
wxString::Format (
- _("An exception occurred: %s.\n\n") + REPORT_PROBLEM,
- std_to_wx (e.what ())
+ _("An exception occurred: %s\n\n%s"),
+ std_to_wx(e.what()),
+ wx::report_problem()
)
);
} catch (...) {
- error_dialog (0, _("An unknown exception occurred.") + " " + REPORT_PROBLEM);
+ error_dialog(nullptr, _("An unknown exception occurred.") + " " + wx::report_problem());
}
}
diff --git a/src/tools/dcpomatic_kdm.cc b/src/tools/dcpomatic_kdm.cc
index 9adef0ab0..af16f57aa 100644
--- a/src/tools/dcpomatic_kdm.cc
+++ b/src/tools/dcpomatic_kdm.cc
@@ -37,6 +37,7 @@
#include "wx/static_text.h"
#include "wx/wx_signal_manager.h"
#include "wx/wx_util.h"
+#include "wx/wx_variant.h"
#include "lib/cinema.h"
#include "lib/collator.h"
#include "lib/compose.hpp"
@@ -52,6 +53,8 @@
#include "lib/kdm_with_metadata.h"
#include "lib/screen.h"
#include "lib/send_kdm_email_job.h"
+#include "lib/util.h"
+#include "lib/variant.h"
#include <dcp/encrypted_kdm.h>
#include <dcp/decrypted_kdm.h>
#include <dcp/exceptions.h>
@@ -125,7 +128,7 @@ public:
setvbuf(hf_in, NULL, _IONBF, 128);
*stdin = *hf_in;
- std::cout << "DCP-o-matic KDM creator is starting." << "\n";
+ std::cout << variant::insert_dcpomatic_kdm_creator("%1 is starting.\n");
}
#endif
@@ -234,7 +237,7 @@ public:
/* Instantly save any config changes when using a DCP-o-matic GUI */
Config::instance()->Changed.connect (boost::bind (&Config::write, Config::instance ()));
- _screens->ScreensChanged.connect (boost::bind (&DOMFrame::setup_sensitivity, this));
+ _screens->ScreensChanged.connect(boost::bind(&DOMFrame::screens_changed, this));
_create->Bind (wxEVT_BUTTON, bind (&DOMFrame::create_kdms, this));
_dkdm->Bind(wxEVT_TREE_SEL_CHANGED, boost::bind(&DOMFrame::dkdm_selection_changed, this));
_dkdm->Bind (wxEVT_TREE_BEGIN_DRAG, boost::bind (&DOMFrame::dkdm_begin_drag, this, _1));
@@ -302,11 +305,13 @@ private:
wxMenu* help = new wxMenu;
#ifdef __WXOSX__
- help->Append (wxID_ABOUT, _("About DCP-o-matic"));
+ help->Append(wxID_ABOUT, variant::wx::insert_dcpomatic_kdm_creator(_("About %s")));
#else
help->Append (wxID_ABOUT, _("About"));
#endif
- help->Append (ID_help_report_a_problem, _("Report a problem..."));
+ if (variant::show_report_a_problem()) {
+ help->Append(ID_help_report_a_problem, _("Report a problem..."));
+ }
m->Append (file, _("&File"));
#ifndef __WXOSX__
@@ -400,11 +405,15 @@ private:
return kdm;
};
+ CinemaList cinemas;
+
for (auto i: _screens->screens()) {
auto kdm = kdm_for_screen(
make_kdm,
- i,
+ i.first,
+ *cinemas.cinema(i.first),
+ *cinemas.screen(i.second),
_timing->from(),
_timing->until(),
_output->formulation(),
@@ -460,7 +469,13 @@ private:
if (e.starts_too_early()) {
error_dialog(this, _("The KDM start period is before (or close to) the start of the signing certificate's validity period. Use a later start time for this KDM."));
} else {
- error_dialog(this, _("The KDM end period is after (or close to) the end of the signing certificates' validity period. Either use an earlier end time for this KDM or re-create your signing certificates in the DCP-o-matic preferences window."));
+ error_dialog(
+ this,
+ variant::wx::insert_dcpomatic_kdm_creator(
+ _("The KDM end period is after (or close to) the end of the signing certificates' validity period. "
+ "Either use an earlier end time for this KDM or re-create your signing certificates in the %s preferences window.")
+ )
+ );
}
return;
} catch (dcp::NotEncryptedError& e) {
@@ -788,6 +803,12 @@ private:
update_dkdm_view();
}
+ void screens_changed()
+ {
+ _timing->suggest_utc_offset(_screens->best_utc_offset());
+ setup_sensitivity();
+ }
+
wxPreferencesEditor* _config_dialog;
ScreensPanel* _screens;
KDMTimingPanel* _timing;
@@ -841,7 +862,7 @@ private:
splash = maybe_show_splash ();
- SetAppName (_("DCP-o-matic KDM Creator"));
+ SetAppName(variant::wx::dcpomatic_kdm_creator());
if (!wxApp::OnInit()) {
return false;
@@ -875,7 +896,7 @@ private:
*/
Config::drop ();
- _frame = new DOMFrame (_("DCP-o-matic KDM Creator"));
+ _frame = new DOMFrame(variant::wx::dcpomatic_kdm_creator());
SetTopWindow (_frame);
_frame->Maximize ();
if (splash) {
@@ -892,7 +913,7 @@ private:
if (splash) {
splash->Destroy ();
}
- error_dialog (0, _("DCP-o-matic could not start"), std_to_wx(e.what()));
+ error_dialog(nullptr, variant::wx::insert_dcpomatic_kdm_creator(_("%s could not start")), std_to_wx(e.what()));
}
return true;
@@ -905,23 +926,25 @@ private:
throw;
} catch (FileError& e) {
error_dialog (
- 0,
+ nullptr,
wxString::Format (
- _("An exception occurred: %s (%s)\n\n") + REPORT_PROBLEM,
- std_to_wx (e.what()),
- std_to_wx (e.file().string().c_str ())
+ _("An exception occurred: %s (%s)\n\n%s"),
+ std_to_wx(e.what()),
+ std_to_wx(e.file().string().c_str()),
+ wx::report_problem()
)
);
} catch (exception& e) {
error_dialog (
nullptr,
wxString::Format (
- _("An exception occurred: %s.\n\n") + " " + REPORT_PROBLEM,
- std_to_wx(e.what())
+ _("An exception occurred: %s.\n\n%s"),
+ std_to_wx(e.what()),
+ wx::report_problem()
)
);
} catch (...) {
- error_dialog (0, _("An unknown exception occurred.") + " " + REPORT_PROBLEM);
+ error_dialog(nullptr, _("An unknown exception occurred.") + " " + wx::report_problem());
}
/* This will terminate the program */
@@ -930,7 +953,7 @@ private:
void OnUnhandledException () override
{
- error_dialog (nullptr, _("An unknown exception occurred.") + " " + REPORT_PROBLEM);
+ error_dialog(nullptr, _("An unknown exception occurred.") + " " + wx::report_problem());
}
void idle ()
diff --git a/src/tools/dcpomatic_player.cc b/src/tools/dcpomatic_player.cc
index e65839521..d462c7271 100644
--- a/src/tools/dcpomatic_player.cc
+++ b/src/tools/dcpomatic_player.cc
@@ -32,11 +32,12 @@
#include "wx/system_information_dialog.h"
#include "wx/timer_display.h"
#include "wx/update_dialog.h"
-#include "wx/verify_dcp_dialog.h"
#include "wx/verify_dcp_progress_dialog.h"
+#include "wx/verify_dcp_result_dialog.h"
#include "wx/wx_ptr.h"
#include "wx/wx_signal_manager.h"
#include "wx/wx_util.h"
+#include "wx/wx_variant.h"
#include "lib/compose.hpp"
#include "lib/config.h"
#include "lib/constants.h"
@@ -64,6 +65,7 @@
#include "lib/server.h"
#include "lib/text_content.h"
#include "lib/update_checker.h"
+#include "lib/variant.h"
#include "lib/verify_dcp_job.h"
#include "lib/video_content.h"
#include <dcp/cpl.h>
@@ -198,7 +200,7 @@ public:
DOMFrame ()
- : wxFrame (nullptr, -1, _("DCP-o-matic Player"))
+ : wxFrame(nullptr, -1, variant::wx::dcpomatic_player())
, _mode (Config::instance()->player_mode())
/* Use a panel as the only child of the Frame so that we avoid
the dark-grey background on Windows.
@@ -211,7 +213,7 @@ public:
#if defined(DCPOMATIC_WINDOWS)
maybe_open_console ();
- cout << "DCP-o-matic Player is starting." << "\n";
+ cout << variant::dcpomatic_player() << " is starting." << "\n";
#endif
auto bar = new wxMenuBar;
@@ -259,7 +261,6 @@ public:
}
_controls->set_film(_viewer.film());
_viewer.set_dcp_decode_reduction(Config::instance()->decode_reduction());
- _viewer.set_optimise_for_j2k(true);
_viewer.PlaybackPermitted.connect(bind(&DOMFrame::playback_permitted, this));
_viewer.TooManyDropped.connect(bind(&DOMFrame::too_many_frames_dropped, this));
_info = new PlayerInformation (_overall_panel, _viewer);
@@ -398,7 +399,7 @@ public:
auto job = make_shared<ExamineContentJob>(_film, dcp);
_examine_job_connection = job->Finished.connect(bind(&DOMFrame::add_dcp_to_film, this, weak_ptr<Job>(job), weak_ptr<Content>(dcp)));
JobManager::instance()->add (job);
- bool const ok = display_progress (_("DCP-o-matic Player"), _("Loading content"));
+ bool const ok = display_progress(variant::wx::dcpomatic_player(), _("Loading content"));
if (!ok || !report_errors_from_last_job(this)) {
return;
}
@@ -406,14 +407,26 @@ public:
if (dcp->video_frame_rate()) {
_film->set_video_frame_rate(dcp->video_frame_rate().get(), true);
}
+ switch (dcp->video_encoding()) {
+ case VideoEncoding::JPEG2000:
+ _viewer.set_optimisation(Optimisation::JPEG2000);
+ break;
+ case VideoEncoding::MPEG2:
+ _viewer.set_optimisation(Optimisation::MPEG2);
+ break;
+ case VideoEncoding::COUNT:
+ DCPOMATIC_ASSERT(false);
+ }
} catch (ProjectFolderError &) {
error_dialog (
this,
wxString::Format(_("Could not load a DCP from %s"), std_to_wx(dir.string())),
- _(
- "This looks like a DCP-o-matic project folder, which cannot be loaded into the player. "
- "Choose the DCP folder inside the DCP-o-matic project folder if that's what you want to play."
- )
+ wxString::Format(
+ _("This looks like a %s project folder, which cannot be loaded into the player. "
+ "Choose the DCP folder inside the %s project folder if that's what you want to play."),
+ variant::wx::dcpomatic(),
+ variant::wx::dcpomatic()
+ )
);
} catch (dcp::ReadError& e) {
error_dialog (this, wxString::Format(_("Could not load a DCP from %s"), std_to_wx(dir.string())), std_to_wx(e.what()));
@@ -609,19 +622,23 @@ private:
auto help = new wxMenu;
#ifdef __WXOSX__
- help->Append (wxID_ABOUT, _("About DCP-o-matic"));
+ help->Append(wxID_ABOUT, variant::wx::insert_dcpomatic_player(_("About %s")));
#else
help->Append (wxID_ABOUT, _("About"));
#endif
- help->Append (ID_help_report_a_problem, _("Report a problem..."));
+ if (variant::show_report_a_problem()) {
+ 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 ()
@@ -680,7 +697,7 @@ private:
DCPOMATIC_ASSERT (dcp);
dcp->add_ov (wx_to_std(c->GetPath()));
JobManager::instance()->add(make_shared<ExamineContentJob>(_film, dcp));
- bool const ok = display_progress (_("DCP-o-matic Player"), _("Loading content"));
+ bool const ok = display_progress(variant::wx::dcpomatic_player(), _("Loading content"));
if (!ok || !report_errors_from_last_job(this)) {
return;
}
@@ -925,12 +942,12 @@ private:
DCPOMATIC_ASSERT (dcp);
auto job = make_shared<VerifyDCPJob>(dcp->directories(), _kdms);
- VerifyDCPProgressDialog progress(this, _("DCP-o-matic Player"));
+ VerifyDCPProgressDialog progress(this, variant::wx::dcpomatic_player());
bool const completed = progress.run(job);
progress.Close();
if (completed) {
- VerifyDCPDialog dialog(this, job);
+ VerifyDCPResultDialog dialog(this, job);
dialog.ShowModal();
}
}
@@ -989,9 +1006,9 @@ private:
auto dialog = make_wx<UpdateDialog>(this, uc->stable (), uc->test ());
dialog->ShowModal ();
} else if (uc->state() == UpdateChecker::State::FAILED) {
- error_dialog (this, _("The DCP-o-matic download server could not be contacted."));
+ error_dialog(this, variant::wx::insert_dcpomatic(_("The %s download server could not be contacted.")));
} else {
- error_dialog (this, _("There are no new versions of DCP-o-matic available."));
+ error_dialog(this, variant::wx::insert_dcpomatic(_("There are no new versions of %s available.")));
}
_update_news_requested = false;
@@ -1203,7 +1220,7 @@ private:
splash = maybe_show_splash ();
- SetAppName (_("DCP-o-matic Player"));
+ SetAppName(variant::wx::dcpomatic_player());
if (!wxApp::OnInit()) {
return false;
@@ -1285,7 +1302,7 @@ private:
if (splash) {
splash->Destroy ();
}
- error_dialog (0, _("DCP-o-matic Player could not start."), std_to_wx(e.what()));
+ error_dialog(nullptr, variant::wx::insert_dcpomatic_player(_("%s could not start")), std_to_wx(e.what()));
}
return true;
@@ -1323,21 +1340,23 @@ private:
error_dialog (
0,
wxString::Format (
- _("An exception occurred: %s (%s)\n\n") + REPORT_PROBLEM,
- std_to_wx (e.what()),
- std_to_wx (e.file().string().c_str ())
+ _("An exception occurred: %s (%s)\n\n%s"),
+ std_to_wx(e.what()),
+ std_to_wx(e.file().string().c_str()),
+ wx::report_problem()
)
);
} catch (exception& e) {
error_dialog (
0,
wxString::Format (
- _("An exception occurred: %s.\n\n") + REPORT_PROBLEM,
- std_to_wx (e.what ())
+ _("An exception occurred: %s\n\n%s"),
+ std_to_wx(e.what()),
+ wx::report_problem()
)
);
} catch (...) {
- error_dialog (0, _("An unknown exception occurred.") + " " + REPORT_PROBLEM);
+ error_dialog(nullptr, _("An unknown exception occurred.") + " " + wx::report_problem());
}
}
diff --git a/src/tools/dcpomatic_playlist.cc b/src/tools/dcpomatic_playlist.cc
index a373d81e6..a50fab37d 100644
--- a/src/tools/dcpomatic_playlist.cc
+++ b/src/tools/dcpomatic_playlist.cc
@@ -25,6 +25,7 @@
#include "wx/playlist_editor_config_dialog.h"
#include "wx/wx_signal_manager.h"
#include "wx/wx_util.h"
+#include "wx/wx_variant.h"
#include "lib/config.h"
#include "lib/constants.h"
#include "lib/cross.h"
@@ -567,34 +568,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, variant::wx::insert_dcpomatic_playlist_editor(_("About %s")));
+
+ 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 {
@@ -635,7 +638,7 @@ private:
try
{
wxInitAllImageHandlers ();
- SetAppName (_("DCP-o-matic Playlist Editor"));
+ SetAppName(variant::wx::dcpomatic_playlist_editor());
if (!wxApp::OnInit()) {
return false;
@@ -669,7 +672,7 @@ private:
*/
Config::drop ();
- _frame = new DOMFrame (_("DCP-o-matic Playlist Editor"));
+ _frame = new DOMFrame(variant::wx::dcpomatic_playlist_editor());
SetTopWindow (_frame);
_frame->Maximize ();
_frame->Show ();
@@ -681,7 +684,7 @@ private:
}
catch (exception& e)
{
- error_dialog (0, _("DCP-o-matic could not start"), std_to_wx(e.what()));
+ error_dialog(nullptr, variant::wx::insert_dcpomatic_playlist_editor(_("%s could not start %s")), std_to_wx(e.what()));
return true;
}
@@ -694,21 +697,23 @@ private:
error_dialog (
0,
wxString::Format (
- _("An exception occurred: %s (%s)\n\n") + REPORT_PROBLEM,
- std_to_wx (e.what()),
- std_to_wx (e.file().string().c_str ())
+ _("An exception occurred: %s (%s)\n\n%s") + wx::report_problem(),
+ std_to_wx(e.what()),
+ std_to_wx(e.file().string().c_str()),
+ wx::report_problem()
)
);
} catch (exception& e) {
error_dialog (
- 0,
- wxString::Format (
- _("An exception occurred: %s.\n\n") + " " + REPORT_PROBLEM,
- std_to_wx (e.what ())
+ nullptr,
+ wxString::Format(
+ _("An exception occurred: %s\n\n%s"),
+ std_to_wx(e.what()),
+ wx::report_problem()
)
);
} catch (...) {
- error_dialog (0, _("An unknown exception occurred.") + " " + REPORT_PROBLEM);
+ error_dialog(nullptr, _("An unknown exception occurred.") + " " + wx::report_problem());
}
/* This will terminate the program */
@@ -717,7 +722,7 @@ private:
void OnUnhandledException () override
{
- error_dialog (0, _("An unknown exception occurred.") + " " + REPORT_PROBLEM);
+ error_dialog(nullptr, _("An unknown exception occurred.") + " " + wx::report_problem());
}
void idle ()
diff --git a/src/tools/dcpomatic_server.cc b/src/tools/dcpomatic_server.cc
index 9bdc688c8..5aad282fd 100644
--- a/src/tools/dcpomatic_server.cc
+++ b/src/tools/dcpomatic_server.cc
@@ -22,7 +22,13 @@
#include "wx/static_text.h"
#include "wx/wx_signal_manager.h"
#include "wx/wx_util.h"
+#include "wx/wx_variant.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"
@@ -149,7 +155,7 @@ class StatusDialog : public wxDialog
public:
StatusDialog ()
: wxDialog (
- nullptr, wxID_ANY, _("DCP-o-matic Encode Server"),
+ nullptr, wxID_ANY, variant::wx::dcpomatic_encode_server(),
wxDefaultPosition, wxDefaultSize,
#ifdef DCPOMATIC_OSX
wxDEFAULT_DIALOG_STYLE | wxRESIZE_BORDER | wxSTAY_ON_TOP
@@ -327,6 +333,11 @@ private:
SetExitOnFrameDelete (false);
+#ifdef DCPOMATIC_GROK
+ grk_plugin::setMessengerLogger(new grk_plugin::GrokLogger("[GROK] "));
+ setup_grok_library_path();
+#endif
+
return true;
}
@@ -352,7 +363,7 @@ private:
error_dialog (nullptr, std_to_wx(e.what()));
wxTheApp->ExitMainLoop ();
} catch (...) {
- error_dialog (nullptr, _("An unknown error has occurred with the DCP-o-matic server."));
+ error_dialog(nullptr, variant::wx::insert_dcpomatic_encode_server(_("An unknown error has occurred with the %s.")));
wxTheApp->ExitMainLoop ();
}
}
diff --git a/src/tools/dcpomatic_server_cli.cc b/src/tools/dcpomatic_server_cli.cc
index 6d7f6aba7..25fe59fc7 100644
--- a/src/tools/dcpomatic_server_cli.cc
+++ b/src/tools/dcpomatic_server_cli.cc
@@ -19,22 +19,26 @@
*/
#include "lib/config.h"
+#include "lib/config.h"
#include "lib/dcp_video.h"
+#include "lib/dcpomatic_log.h"
+#include "lib/encode_server.h"
#include "lib/exceptions.h"
-#include "lib/util.h"
-#include "lib/config.h"
-#include "lib/image.h"
#include "lib/file_log.h"
+#ifdef DCPOMATIC_GROK
+#include "lib/grok/context.h"
+#endif
+#include "lib/image.h"
#include "lib/null_log.h"
+#include "lib/util.h"
+#include "lib/variant.h"
#include "lib/version.h"
-#include "lib/encode_server.h"
-#include "lib/dcpomatic_log.h"
+#include <boost/algorithm/string.hpp>
#include <boost/array.hpp>
#include <boost/asio.hpp>
-#include <boost/algorithm/string.hpp>
#include <boost/thread.hpp>
-#include <boost/thread/mutex.hpp>
#include <boost/thread/condition.hpp>
+#include <boost/thread/mutex.hpp>
#include <unistd.h>
#include <errno.h>
#include <getopt.h>
@@ -44,15 +48,15 @@
#include <vector>
using std::cerr;
-using std::string;
using std::cout;
using std::shared_ptr;
+using std::string;
static void
help (string n)
{
cerr << "Syntax: " << n << " [OPTION]\n"
- << " -v, --version show DCP-o-matic version\n"
+ << variant::insert_dcpomatic(" -v, --version show %1 version\n")
<< " -h, --help show this help\n"
<< " -t, --threads number of parallel encoding threads to use\n"
<< " --verbose be verbose to stdout\n"
@@ -109,13 +113,18 @@ 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 {
server.run ();
} catch (boost::system::system_error& e) {
if (e.code() == boost::system::errc::address_in_use) {
- cerr << argv[0] << ": address already in use. Is another DCP-o-matic server instance already running?\n";
+ cerr << argv[0] << variant::insert_dcpomatic(": address already in use. Is another %1 server instance already running?\n");
exit (EXIT_FAILURE);
}
cerr << argv[0] << ": " << e.what() << "\n";
diff --git a/src/tools/dcpomatic_verifier.cc b/src/tools/dcpomatic_verifier.cc
new file mode 100644
index 000000000..834ebefa1
--- /dev/null
+++ b/src/tools/dcpomatic_verifier.cc
@@ -0,0 +1,251 @@
+/*
+ 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/>.
+
+*/
+
+
+/** @file src/tools/dcpomatic_verify.cc
+ * @brief A DCP verify GUI.
+ */
+
+
+#include "wx/check_box.h"
+#include "wx/dcpomatic_button.h"
+#include "wx/dir_picker_ctrl.h"
+#include "wx/verify_dcp_progress_panel.h"
+#include "wx/verify_dcp_result_panel.h"
+#include "wx/wx_util.h"
+#include "wx/wx_variant.h"
+#include "lib/constants.h"
+#include "lib/cross.h"
+#include "lib/job_manager.h"
+#include "lib/verify_dcp_job.h"
+#include "lib/util.h"
+#include <dcp/verify_report.h>
+LIBDCP_DISABLE_WARNINGS
+#include <wx/evtloop.h>
+#include <wx/wx.h>
+LIBDCP_ENABLE_WARNINGS
+#ifdef __WXGTK__
+#include <X11/Xlib.h>
+#endif
+
+
+using std::exception;
+using std::make_shared;
+
+
+class DOMFrame : public wxFrame
+{
+public:
+ explicit DOMFrame(wxString const& title)
+ : wxFrame(nullptr, -1, title)
+ {
+#ifdef DCPOMATIC_WINDOWS
+ SetIcon(wxIcon(std_to_wx("id")));
+#endif
+ auto overall_sizer = new wxBoxSizer(wxVERTICAL);
+
+ auto dcp_sizer = new wxBoxSizer(wxHORIZONTAL);
+ add_label_to_sizer(dcp_sizer, this, _("DCP"), true, 0, wxALIGN_CENTER_VERTICAL);
+ _dcp = new DirPickerCtrl(this, true);
+ dcp_sizer->Add(_dcp, 1, wxEXPAND);
+ overall_sizer->Add(dcp_sizer, 0, wxEXPAND | wxALL, DCPOMATIC_DIALOG_BORDER);
+
+ auto options_sizer = new wxBoxSizer(wxVERTICAL);
+ _write_log = new CheckBox(this, _("Write log to DCP folder"));
+ options_sizer->Add(_write_log, 0, wxBOTTOM, DCPOMATIC_SIZER_GAP);
+ overall_sizer->Add(options_sizer, 0, wxLEFT, DCPOMATIC_DIALOG_BORDER);
+
+ _verify = new Button(this, _("Verify"));
+ overall_sizer->Add(_verify, 0, wxEXPAND | wxLEFT | wxRIGHT, DCPOMATIC_DIALOG_BORDER);
+
+ _progress_panel = new VerifyDCPProgressPanel(this);
+ overall_sizer->Add(_progress_panel, 0, wxEXPAND | wxALL, DCPOMATIC_DIALOG_BORDER);
+
+ _result_panel = new VerifyDCPResultPanel(this);
+ overall_sizer->Add(_result_panel, 0, wxEXPAND | wxALL, DCPOMATIC_DIALOG_BORDER);
+
+ SetSizerAndFit(overall_sizer);
+
+ _dcp->Changed.connect(boost::bind(&DOMFrame::setup_sensitivity, this));
+ _verify->bind(&DOMFrame::verify_clicked, this);
+
+ setup_sensitivity();
+ }
+
+private:
+ void setup_sensitivity()
+ {
+ _verify->Enable(!_dcp->GetPath().IsEmpty());
+ }
+
+ void verify_clicked()
+ {
+ auto dcp = boost::filesystem::path(wx_to_std(_dcp->GetPath()));
+ if (dcp.empty()) {
+ return;
+ }
+
+ auto job_manager = JobManager::instance();
+ auto job = make_shared<VerifyDCPJob>(std::vector<boost::filesystem::path>{dcp}, std::vector<boost::filesystem::path>());
+ job_manager->add(job);
+
+ while (job_manager->work_to_do()) {
+ wxEventLoopBase::GetActive()->YieldFor(wxEVT_CATEGORY_UI | wxEVT_CATEGORY_USER_INPUT);
+ dcpomatic_sleep_seconds(1);
+
+ _progress_panel->update(job);
+ }
+
+ _result_panel->fill(job);
+ if (_write_log->get()) {
+ dcp::TextFormatter formatter(dcp / "REPORT.txt");
+ dcp::verify_report(job->result(), formatter);
+ }
+ }
+
+ DirPickerCtrl* _dcp;
+ CheckBox* _write_log;
+ Button* _verify;
+ VerifyDCPProgressPanel* _progress_panel;
+ VerifyDCPResultPanel* _result_panel;
+};
+
+
+/** @class App
+ * @brief The magic App class for wxWidgets.
+ */
+class App : public wxApp
+{
+public:
+ App()
+ : wxApp()
+ {
+ dcpomatic_setup_path_encoding();
+#ifdef DCPOMATIC_LINUX
+ XInitThreads();
+#endif
+ }
+
+private:
+ bool OnInit() override
+ {
+ try {
+ SetAppName(variant::wx::dcpomatic_verifier());
+
+ if (!wxApp::OnInit()) {
+ return false;
+ }
+
+#ifdef DCPOMATIC_LINUX
+ unsetenv("UBUNTU_MENUPROXY");
+#endif
+
+#ifdef DCPOMATIC_OSX
+ dcpomatic_sleep_seconds(1);
+ make_foreground_application();
+#endif
+
+ /* Enable i18n; this will create a Config object
+ to look for a force-configured language. This Config
+ object will be wrong, however, because dcpomatic_setup
+ hasn't yet been called and there aren't any filters etc.
+ set up yet.
+ */
+ dcpomatic_setup_i18n();
+
+ /* Set things up, including filters etc.
+ which will now be internationalised correctly.
+ */
+ dcpomatic_setup();
+
+ /* Force the configuration to be re-loaded correctly next
+ time it is needed.
+ */
+ Config::drop();
+
+ _frame = new DOMFrame(variant::wx::dcpomatic_verifier());
+ SetTopWindow(_frame);
+ _frame->SetSize({480, 640});
+ _frame->Show();
+ }
+ catch (exception& e)
+ {
+ error_dialog(nullptr, variant::wx::insert_dcpomatic_verifier("%s could not start."), std_to_wx(e.what()));
+ }
+
+ return true;
+ }
+
+ void report_exception()
+ {
+ try {
+ throw;
+ } catch (FileError& e) {
+ error_dialog(
+ nullptr,
+ wxString::Format(
+ _("An exception occurred: %s (%s)\n\n%s"),
+ std_to_wx(e.what()),
+ std_to_wx(e.file().string().c_str()),
+ wx::report_problem()
+ )
+ );
+ } catch (boost::filesystem::filesystem_error& e) {
+ error_dialog(
+ nullptr,
+ wxString::Format(
+ _("An exception occurred: %s (%s) (%s)\n\n%s"),
+ std_to_wx(e.what()),
+ std_to_wx(e.path1().string()),
+ std_to_wx(e.path2().string()),
+ wx::report_problem()
+ )
+ );
+ } catch (exception& e) {
+ error_dialog(
+ nullptr,
+ wxString::Format(
+ _("An exception occurred: %s.\n\n%s"),
+ std_to_wx(e.what()),
+ wx::report_problem()
+ )
+ );
+ } catch (...) {
+ error_dialog(nullptr, _("An unknown exception occurred.") + " " + wx::report_problem());
+ }
+ }
+
+ /* An unhandled exception has occurred inside the main event loop */
+ bool OnExceptionInMainLoop() override
+ {
+ report_exception();
+ return false;
+ }
+
+ void OnUnhandledException() override
+ {
+ report_exception();
+ }
+
+ DOMFrame* _frame = nullptr;
+};
+
+
+IMPLEMENT_APP(App)
diff --git a/src/tools/wscript b/src/tools/wscript
index c3b2b5fe0..e6d7c4be1 100644
--- a/src/tools/wscript
+++ b/src/tools/wscript
@@ -17,11 +17,79 @@
# along with DCP-o-matic. If not, see <http://www.gnu.org/licenses/>.
#
+from __future__ import print_function
import os
import glob
from waflib import Logs
import i18n
+
+def description(tool, variant):
+ descriptions = {
+ 'dcpomatic': 'DCP-o-matic',
+ 'dcpomatic_batch': 'DCP-o-matic Batch Converter',
+ 'dcpomatic_server': 'DCP-o-matic Encode Server',
+ 'dcpomatic_kdm': 'DCP-o-matic KDM Creator',
+ 'dcpomatic_player': 'DCP-o-matic Player',
+ 'dcpomatic_playlist': 'DCP-o-matic Playlist Editor',
+ 'dcpomatic_combiner': 'DCP-o-matic Combiner',
+ 'dcpomatic_verifier': 'DCP-o-matic Verifier',
+ }
+ return descriptions[tool] if tool in descriptions else tool
+
+
+def make_rc(tool, icon, variant):
+ filename = 'build/src/tools/%s.rc' % tool
+ with open(filename, 'w') as rc:
+ if tool == 'dcpomatic_disk_writer':
+ print('#include "winuser.h"', file=rc)
+ print('1 RT_MANIFEST "dcpomatic2_disk_writer.exe.manifest"', file=rc)
+ with open("build/src/tools/dcpomatic2_disk_writer.exe.manifest", "w") as manifest:
+ print("""
+<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
+<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
+ <assemblyIdentity version="1.0.0.0" processorArchitecture="X86" name="dcpomatic2_disk_writer" type="win32"/>
+ <description>DCP-o-matic Disk Writer</description>
+ <trustInfo xmlns="urn:schemas-microsoft-com:asm.v2">
+ <security>
+ <requestedPrivileges>
+ <requestedExecutionLevel level="requireAdministrator" uiAccess="false"/>
+ </requestedPrivileges>
+ </security>
+ </trustInfo>
+</assembly>""", file=manifest)
+ else:
+ print("""
+id ICON "../../graphics/windows/%s"
+#include "wx-3.1/wx/msw/wx.rc"
+VS_VERSION_INFO VERSIONINFO
+ FILEVERSION 0,0,0,2
+ PRODUCTVERSION 0,0,0,2
+ FILEFLAGSMASK 0x3fL
+ #ifdef _DEBUG
+ FILEFLAGS 0x1L
+ #else
+ FILEFLAGS 0x0L
+ #endif
+ FILEOS 0x4L
+ FILETYPE 0x1L
+ FILESUBTYPE 0x0L
+ {
+ BLOCK "StringFileInfo"
+ {
+ BLOCK "040904b0"
+ {
+ VALUE "FileDescription", "%s\\0"
+ }
+ }
+ BLOCK "VarFileInfo"
+ {
+ VALUE "Translation", 0x409, 1200
+ }
+ }
+ """ % (icon, description(tool, variant)), file=rc)
+ return filename
+
def configure(conf):
if conf.env.TARGET_WINDOWS_64 or conf.env.TARGET_WINDOWS_32:
conf.env.append_value('CXXFLAGS', ['-mconsole'])
@@ -30,7 +98,7 @@ def configure(conf):
def build(bld):
uselib = 'BOOST_THREAD BOOST_DATETIME DCP XMLSEC CXML XMLPP AVFORMAT AVFILTER AVCODEC '
uselib += 'AVUTIL SWSCALE SWRESAMPLE POSTPROC CURL BOOST_FILESYSTEM SSH ZIP CAIROMM FONTCONFIG PANGOMM SUB '
- uselib += 'SNDFILE SAMPLERATE BOOST_REGEX ICU NETTLE RTAUDIO PNG JPEG LEQM_NRT '
+ uselib += 'SNDFILE SAMPLERATE BOOST_REGEX ICU NETTLE RTAUDIO PNG JPEG LEQM_NRT SQLITE3 '
if bld.env.ENABLE_DISK:
if bld.env.TARGET_LINUX:
@@ -46,6 +114,11 @@ def build(bld):
if bld.env.ENABLE_DISK and not bld.env.DISABLE_GUI:
cli_tools.append('dcpomatic_disk_writer')
+ try:
+ os.makedirs('build/src/tools')
+ except:
+ pass
+
for t in cli_tools:
obj = bld(features='cxx cxxprogram')
obj.uselib = uselib
@@ -53,7 +126,7 @@ def build(bld):
obj.use = ['libdcpomatic2']
obj.source = '%s.cc' % t
if (bld.env.TARGET_WINDOWS_64 or bld.env.TARGET_WINDOWS_32) and t == 'dcpomatic_disk_writer':
- obj.source += ' ../../platform/windows/%s.rc' % t
+ obj.source += ' ../../%s' % (make_rc(t, None, bld.env.VARIANT))
# Prevent a console window opening when we start dcpomatic2_disk_writer
obj.env.append_value('LINKFLAGS', '-Wl,-subsystem,windows')
obj.target = t.replace('dcpomatic', 'dcpomatic2')
@@ -62,7 +135,15 @@ def build(bld):
gui_tools = []
if not bld.env.DISABLE_GUI:
- gui_tools = ['dcpomatic', 'dcpomatic_batch', 'dcpomatic_server', 'dcpomatic_kdm', 'dcpomatic_player', 'dcpomatic_playlist', 'dcpomatic_combiner', 'dcpomatic_editor']
+ gui_tools = ['dcpomatic',
+ 'dcpomatic_batch',
+ 'dcpomatic_server',
+ 'dcpomatic_kdm',
+ 'dcpomatic_player',
+ 'dcpomatic_playlist',
+ 'dcpomatic_combiner',
+ 'dcpomatic_editor',
+ 'dcpomatic_verifier']
if bld.env.ENABLE_DISK:
gui_tools.append('dcpomatic_disk')
@@ -82,7 +163,7 @@ def build(bld):
obj.use = ['libdcpomatic2', 'libdcpomatic2-wx']
obj.source = '%s.cc' % t
if bld.env.TARGET_WINDOWS_64 or bld.env.TARGET_WINDOWS_32:
- obj.source += ' ../../platform/windows/%s.rc' % t
+ obj.source += ' ../../%s' % make_rc(t, t.replace("dcpomatic", "dcpomatic2") + ".ico", bld.env.VARIANT)
obj.target = t.replace('dcpomatic', 'dcpomatic2')
i18n.po_to_mo(os.path.join('src', 'tools'), 'dcpomatic2', bld)
diff --git a/src/wx/about_dialog.cc b/src/wx/about_dialog.cc
index 2c742cce3..220092da6 100644
--- a/src/wx/about_dialog.cc
+++ b/src/wx/about_dialog.cc
@@ -26,7 +26,9 @@
#include "about_dialog.h"
#include "static_text.h"
#include "wx_util.h"
+#include "wx_variant.h"
#include "lib/compose.hpp"
+#include "lib/variant.h"
#include "lib/version.h"
#include <dcp/warnings.h>
LIBDCP_DISABLE_WARNINGS
@@ -38,7 +40,7 @@ using std::vector;
AboutDialog::AboutDialog (wxWindow* parent)
- : wxDialog (parent, wxID_ANY, _("About DCP-o-matic"))
+ : wxDialog(parent, wxID_ANY, variant::wx::insert_dcpomatic(_("About %s")))
{
auto overall_sizer = new wxBoxSizer (wxVERTICAL);
auto sizer = new wxBoxSizer (wxVERTICAL);
@@ -53,7 +55,7 @@ AboutDialog::AboutDialog (wxWindow* parent)
wxFont version_font (*wxNORMAL_FONT);
version_font.SetWeight (wxFONTWEIGHT_BOLD);
- auto t = new StaticText (this, _("DCP-o-matic"));
+ auto t = new StaticText(this, variant::wx::dcpomatic());
t->SetFont (title_font);
sizer->Add (t, wxSizerFlags().Centre().Border(wxALL, 16));
@@ -67,26 +69,30 @@ AboutDialog::AboutDialog (wxWindow* parent)
sizer->Add (t, wxSizerFlags().Centre().Border(wxALL, 2));
sizer->AddSpacer (12);
- t = new StaticText (
- this,
- _("Free, open-source DCP creation from almost anything."),
- wxDefaultPosition, wxDefaultSize, wxALIGN_CENTER
- );
- t->SetFont (subtitle_font);
-
- sizer->Add (t, wxSizerFlags().Centre().Border(wxALL, 8));
-
- auto h = new wxHyperlinkCtrl (
- this, wxID_ANY,
- wxT ("dcpomatic.com"),
- wxT ("https://dcpomatic.com")
- );
+ if (variant::show_tagline())
+ {
+ t = new StaticText(
+ this,
+ _("Free, open-source DCP creation from almost anything."),
+ wxDefaultPosition, wxDefaultSize, wxALIGN_CENTER
+ );
+ t->SetFont(subtitle_font);
+ sizer->Add(t, wxSizerFlags().Centre().Border(wxALL, 8));
+ }
- sizer->Add (h, wxSizerFlags().Centre().Border(wxALL, 8));
+ if (variant::show_dcpomatic_website())
+ {
+ auto h = new wxHyperlinkCtrl(
+ this, wxID_ANY,
+ wxT("dcpomatic.com"),
+ wxT("https://dcpomatic.com")
+ );
+ sizer->Add(h, wxSizerFlags().Centre().Border(wxALL, 8));
+ }
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 +105,7 @@ AboutDialog::AboutDialog (wxWindow* parent)
written_by.Add (wxT ("Terrence Meiczinger"));
written_by.Add (wxT ("Mart Jansink"));
written_by.Add (wxT ("Ole Laursen"));
+ written_by.Add (wxT ("Aaron Boxer"));
add_section (_("Written by"), written_by);
wxArrayString with_help_from;
diff --git a/src/wx/audio_dialog.cc b/src/wx/audio_dialog.cc
index 714bf78e4..1f7f3ad03 100644
--- a/src/wx/audio_dialog.cc
+++ b/src/wx/audio_dialog.cc
@@ -24,6 +24,7 @@
#include "check_box.h"
#include "static_text.h"
#include "wx_util.h"
+#include "wx_variant.h"
#include "lib/analyse_audio_job.h"
#include "lib/audio_analysis.h"
#include "lib/audio_content.h"
@@ -166,9 +167,9 @@ AudioDialog::AudioDialog (wxWindow* parent, shared_ptr<Film> film, FilmViewer& v
_film_content_connection = film->ContentChange.connect(boost::bind(&AudioDialog::content_change, this, _1, _3));
DCPOMATIC_ASSERT (film->directory());
if (content) {
- SetTitle(wxString::Format(_("DCP-o-matic audio - %s"), std_to_wx(content->path(0).string())));
+ SetTitle(wxString::Format(_("%s audio - %s"), variant::wx::dcpomatic(), std_to_wx(content->path(0).string())));
} else {
- SetTitle(wxString::Format(_("DCP-o-matic audio - %s"), std_to_wx(film->directory().get().string())));
+ SetTitle(wxString::Format(_("%s audio - %s"), variant::wx::dcpomatic(), std_to_wx(film->directory().get().string())));
}
if (content) {
diff --git a/src/wx/audio_panel.cc b/src/wx/audio_panel.cc
index fcadd0c68..34923fb02 100644
--- a/src/wx/audio_panel.cc
+++ b/src/wx/audio_panel.cc
@@ -71,14 +71,6 @@ AudioPanel::AudioPanel (ContentPanel* p)
void
AudioPanel::create ()
{
- _reference = new CheckBox (this, _("Use this DCP's audio as OV and make VF"));
- _reference_note = new StaticText (this, wxT(""));
- _reference_note->Wrap (200);
- auto font = _reference_note->GetFont();
- font.SetStyle(wxFONTSTYLE_ITALIC);
- font.SetPointSize(font.GetPointSize() - 1);
- _reference_note->SetFont(font);
-
_show = new Button (this, _("Show graph of audio levels..."));
_peak = new StaticText (this, wxT (""));
@@ -121,6 +113,9 @@ AudioPanel::create ()
_description = new StaticText (this, wxT(" \n"), wxDefaultPosition, wxDefaultSize);
_sizer->Add (_description, 0, wxALL, 12);
+ auto font = _description->GetFont();
+ font.SetStyle(wxFONTSTYLE_ITALIC);
+ font.SetPointSize(font.GetPointSize() - 1);
_description->SetFont (font);
_gain->wrapped()->SetRange (-60, 60);
@@ -133,7 +128,6 @@ AudioPanel::create ()
film_changed(FilmProperty::VIDEO_FRAME_RATE);
film_changed(FilmProperty::REEL_TYPE);
- _reference->bind(&AudioPanel::reference_clicked, this);
_show->Bind (wxEVT_BUTTON, boost::bind (&AudioPanel::show_clicked, this));
_gain_calculate_button->Bind (wxEVT_BUTTON, boost::bind (&AudioPanel::gain_calculate_button_clicked, this));
@@ -155,12 +149,6 @@ AudioPanel::add_to_grid ()
{
int r = 0;
- auto reference_sizer = new wxBoxSizer (wxVERTICAL);
- reference_sizer->Add (_reference, 0);
- reference_sizer->Add (_reference_note, 0);
- _grid->Add (reference_sizer, wxGBPosition(r, 0), wxGBSpan(1, 4));
- ++r;
-
_grid->Add (_show, wxGBPosition (r, 0), wxGBSpan (1, 2));
_grid->Add (_peak, wxGBPosition (r, 2), wxGBSpan (1, 2), wxALIGN_CENTER_VERTICAL);
++r;
@@ -258,15 +246,6 @@ AudioPanel::film_content_changed (int property)
/* This is a bit aggressive but probably not so bad */
_peak_cache.clear();
setup_peak ();
- } else if (property == DCPContentProperty::REFERENCE_AUDIO) {
- if (ac.size() == 1) {
- shared_ptr<DCPContent> dcp = dynamic_pointer_cast<DCPContent> (ac.front ());
- checked_set (_reference, dcp ? dcp->reference_audio () : false);
- } else {
- checked_set (_reference, false);
- }
-
- setup_sensitivity ();
} else if (property == ContentProperty::VIDEO_FRAME_RATE) {
setup_description ();
} else if (property == AudioContentProperty::FADE_IN) {
@@ -384,19 +363,8 @@ AudioPanel::setup_sensitivity ()
dcp = dynamic_pointer_cast<DCPContent> (sel.front ());
}
- string why_not;
- bool const can_reference = dcp && dcp->can_reference_audio (_parent->film(), why_not);
- wxString cannot;
- if (why_not.empty()) {
- cannot = _("Cannot reference this DCP's audio.");
- } else {
- cannot = _("Cannot reference this DCP's audio: ") + std_to_wx(why_not);
- }
- setup_refer_button (_reference, _reference_note, dcp, can_reference, cannot);
-
- auto const ref = _reference->GetValue();
+ auto const ref = dcp && dcp->reference_audio();
auto const single = sel.size() == 1;
-
auto const all_have_video = std::all_of(sel.begin(), sel.end(), [](shared_ptr<const Content> c) { return static_cast<bool>(c->video); });
_gain->wrapped()->Enable (!ref);
@@ -501,23 +469,6 @@ AudioPanel::active_jobs_changed (optional<string> old_active, optional<string> n
void
-AudioPanel::reference_clicked ()
-{
- auto c = _parent->selected ();
- if (c.size() != 1) {
- return;
- }
-
- auto d = dynamic_pointer_cast<DCPContent>(c.front());
- if (!d) {
- return;
- }
-
- d->set_reference_audio (_reference->GetValue ());
-}
-
-
-void
AudioPanel::set_film (shared_ptr<Film>)
{
/* We are changing film, so destroy any audio dialog for the old one */
diff --git a/src/wx/audio_panel.h b/src/wx/audio_panel.h
index 61dd2783a..d25f7e247 100644
--- a/src/wx/audio_panel.h
+++ b/src/wx/audio_panel.h
@@ -54,15 +54,12 @@ private:
void setup_peak ();
void active_jobs_changed (boost::optional<std::string>, boost::optional<std::string>);
void setup_sensitivity ();
- void reference_clicked ();
void add_to_grid () override;
boost::optional<float> peak () const;
void fade_in_changed ();
void fade_out_changed ();
void use_same_fades_as_video_changed ();
- CheckBox* _reference;
- wxStaticText* _reference_note;
wxButton* _show;
wxStaticText* _gain_label;
wxStaticText* _gain_db_label;
diff --git a/src/wx/check_box.cc b/src/wx/check_box.cc
index baec651c1..c18ceba47 100644
--- a/src/wx/check_box.cc
+++ b/src/wx/check_box.cc
@@ -53,3 +53,10 @@ CheckBox::get() const
return GetValue();
}
+
+void
+CheckBox::set(bool state)
+{
+ SetValue(state);
+}
+
diff --git a/src/wx/check_box.h b/src/wx/check_box.h
index 4b3fddb9f..d9feb00c9 100644
--- a/src/wx/check_box.h
+++ b/src/wx/check_box.h
@@ -39,6 +39,7 @@ public:
void set_text (wxString text) override;
wxString get_text () const override;
bool get() const;
+ void set(bool state);
template <typename... Args>
void bind(Args... args) {
diff --git a/src/wx/cinema_dialog.cc b/src/wx/cinema_dialog.cc
index 84fde5f41..3b8265e6f 100644
--- a/src/wx/cinema_dialog.cc
+++ b/src/wx/cinema_dialog.cc
@@ -20,6 +20,7 @@
#include "cinema_dialog.h"
+#include "dcpomatic_choice.h"
#include "wx_util.h"
#include "lib/dcpomatic_assert.h"
#include "lib/util.h"
@@ -36,7 +37,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, dcp::UTCOffset utc_offset)
: wxDialog (parent, wxID_ANY, title)
{
auto overall_sizer = new wxBoxSizer (wxVERTICAL);
@@ -50,9 +51,9 @@ 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));
+ add_label_to_sizer(sizer, this, _("UTC offset (time zone)"), true, wxGBPosition(r, 0));
+ _utc_offset = new Choice(this);
+ sizer->Add(_utc_offset, wxGBPosition(r, 1));
++r;
add_label_to_sizer (sizer, this, _("Notes"), true, wxGBPosition(r, 0));
@@ -84,15 +85,15 @@ CinemaDialog::CinemaDialog(wxWindow* parent, wxString title, string name, vector
}
/* Default to UTC */
- size_t sel = get_offsets (_offsets);
+ 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) {
+ _utc_offset->add_entry(_offsets[i].name);
+ if (_offsets[i].offset == utc_offset) {
sel = i;
}
}
- _utc_offset->SetSelection (sel);
+ _utc_offset->set(sel);
overall_sizer->Layout ();
overall_sizer->SetSizeHints (this);
@@ -122,32 +123,20 @@ CinemaDialog::emails() const
}
-int
-CinemaDialog::utc_offset_hour () const
+string
+CinemaDialog::notes () const
{
- int const sel = _utc_offset->GetSelection();
- if (sel < 0 || sel > int(_offsets.size())) {
- return 0;
- }
-
- return _offsets[sel].hour;
+ return wx_to_std (_notes->GetValue());
}
-int
-CinemaDialog::utc_offset_minute () const
+dcp::UTCOffset
+CinemaDialog::utc_offset() const
{
- int const sel = _utc_offset->GetSelection();
+ auto const sel = _utc_offset->GetSelection();
if (sel < 0 || sel > int(_offsets.size())) {
- return 0;
+ return {};
}
- return _offsets[sel].minute;
-}
-
-
-string
-CinemaDialog::notes () const
-{
- return wx_to_std (_notes->GetValue());
+ return _offsets[sel].offset;
}
diff --git a/src/wx/cinema_dialog.h b/src/wx/cinema_dialog.h
index 0b8362843..873657772 100644
--- a/src/wx/cinema_dialog.h
+++ b/src/wx/cinema_dialog.h
@@ -30,6 +30,9 @@ LIBDCP_ENABLE_WARNINGS
#include <vector>
+class Choice;
+
+
class CinemaDialog : public wxDialog
{
public:
@@ -39,15 +42,13 @@ public:
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
+ dcp::UTCOffset = {}
);
std::string name () const;
std::string notes () const;
std::vector<std::string> emails () const;
- int utc_offset_hour () const;
- int utc_offset_minute () const;
+ dcp::UTCOffset utc_offset() const;
private:
void set_emails (std::vector<std::string>);
@@ -56,6 +57,6 @@ private:
wxTextCtrl* _notes;
EditableList<std::string, EmailDialog>* _email_list;
std::vector<std::string> _emails;
- wxChoice* _utc_offset;
+ Choice* _utc_offset;
std::vector<Offset> _offsets;
};
diff --git a/src/wx/colours.h b/src/wx/colours.h
new file mode 100644
index 000000000..0d33208e0
--- /dev/null
+++ b/src/wx/colours.h
@@ -0,0 +1,25 @@
+/*
+ Copyright (C) 2023 Carl Hetherington <cth@carlh.net>
+
+ This file is part of DCP-o-matic.
+
+ DCP-o-matic is free software; you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation; either version 2 of the License, or
+ (at your option) any later version.
+
+ DCP-o-matic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with DCP-o-matic. If not, see <http://www.gnu.org/licenses/>.
+
+*/
+
+
+#define VIDEO_CONTENT_COLOUR (wxColour(242, 92, 120, 255))
+#define AUDIO_CONTENT_COLOUR (wxColour(149, 121, 232, 255))
+#define TEXT_CONTENT_COLOUR (wxColour(163, 255, 154, 255))
+#define ATMOS_CONTENT_COLOUR (wxColour(149, 121, 232, 255))
diff --git a/src/wx/config_dialog.cc b/src/wx/config_dialog.cc
index b4adc855e..05c3f281c 100644
--- a/src/wx/config_dialog.cc
+++ b/src/wx/config_dialog.cc
@@ -25,7 +25,9 @@
#include "dcpomatic_button.h"
#include "nag_dialog.h"
#include "static_text.h"
+#include "wx_variant.h"
#include "lib/constants.h"
+#include "lib/util.h"
#include <dcp/file.h>
#include <dcp/filesystem.h>
#include <dcp/raw_convert.h>
@@ -148,7 +150,7 @@ GeneralPage::add_language_controls (wxGridBagSizer* table, int& r)
++r;
auto restart = add_label_to_sizer (
- table, _panel, _("(restart DCP-o-matic to see language changes)"), false, wxGBPosition (r, 0), wxGBSpan (1, 2)
+ table, _panel, variant::wx::insert_dcpomatic(_("(restart %s to see language changes)")), false, wxGBPosition (r, 0), wxGBSpan (1, 2)
);
wxFont font = restart->GetFont();
font.SetStyle (wxFONTSTYLE_ITALIC);
@@ -792,7 +794,7 @@ KeysPage::import_decryption_chain_and_key ()
if (new_chain->chain_valid() && new_chain->private_key_valid()) {
Config::instance()->set_decryption_chain(new_chain);
} else {
- error_dialog(_panel, _("Invalid DCP-o-matic export file"));
+ error_dialog(_panel, variant::wx::insert_dcpomatic(_("Invalid %s export file")));
}
}
diff --git a/src/wx/content_menu.cc b/src/wx/content_menu.cc
index 642457d93..fdf43ba7c 100644
--- a/src/wx/content_menu.cc
+++ b/src/wx/content_menu.cc
@@ -1,5 +1,5 @@
/*
- Copyright (C) 2013-2021 Carl Hetherington <cth@carlh.net>
+ Copyright (C) 2013-2024 Carl Hetherington <cth@carlh.net>
This file is part of DCP-o-matic.
@@ -23,13 +23,13 @@
#include "content_advanced_dialog.h"
#include "content_menu.h"
#include "content_properties_dialog.h"
+#include "content_timeline_audio_view.h"
+#include "content_timeline_video_view.h"
#include "dir_dialog.h"
#include "file_dialog.h"
#include "film_viewer.h"
#include "id.h"
#include "repeat_dialog.h"
-#include "timeline_video_content_view.h"
-#include "timeline_audio_content_view.h"
#include "wx_util.h"
#include "lib/audio_content.h"
#include "lib/config.h"
@@ -82,6 +82,7 @@ enum {
ID_advanced,
ID_re_examine,
ID_auto_crop,
+ ID_copy_settings,
ID_kdm,
ID_ov,
ID_choose_cpl,
@@ -102,6 +103,7 @@ ContentMenu::ContentMenu(wxWindow* p, FilmViewer& viewer)
_find_missing = _menu->Append (ID_find_missing, _("Find missing..."));
_re_examine = _menu->Append (ID_re_examine, _("Re-examine..."));
_auto_crop = _menu->Append (ID_auto_crop, _("Auto-crop..."));
+ _copy_settings = _menu->Append(ID_copy_settings, _("Copy settings from another project..."));
_properties = _menu->Append (ID_properties, _("Properties..."));
_advanced = _menu->Append (ID_advanced, _("Advanced settings..."));
_menu->AppendSeparator ();
@@ -121,6 +123,7 @@ ContentMenu::ContentMenu(wxWindow* p, FilmViewer& viewer)
_parent->Bind (wxEVT_MENU, boost::bind (&ContentMenu::advanced, this), ID_advanced);
_parent->Bind (wxEVT_MENU, boost::bind (&ContentMenu::re_examine, this), ID_re_examine);
_parent->Bind (wxEVT_MENU, boost::bind (&ContentMenu::auto_crop, this), ID_auto_crop);
+ _parent->Bind (wxEVT_MENU, boost::bind (&ContentMenu::copy_settings, this), ID_copy_settings);
_parent->Bind (wxEVT_MENU, boost::bind (&ContentMenu::kdm, this), ID_kdm);
_parent->Bind (wxEVT_MENU, boost::bind (&ContentMenu::ov, this), ID_ov);
_parent->Bind (wxEVT_MENU, boost::bind (&ContentMenu::set_dcp_settings, this), ID_set_dcp_settings);
@@ -157,6 +160,7 @@ ContentMenu::popup (weak_ptr<Film> film, ContentList c, TimelineContentViewList
_advanced->Enable (_content.size() == 1);
_re_examine->Enable (!_content.empty ());
_auto_crop->Enable (_content.size() == 1);
+ _copy_settings->Enable(_content.size() == 1);
if (_content.size() == 1) {
auto dcp = dynamic_pointer_cast<DCPContent> (_content.front());
@@ -314,12 +318,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) {
@@ -603,3 +607,36 @@ ContentMenu::auto_crop ()
update_viewer (crop);
});
}
+
+
+void
+ContentMenu::copy_settings()
+{
+ DCPOMATIC_ASSERT(_content.size() == 1);
+
+ auto film = _film.lock();
+ DCPOMATIC_ASSERT (film);
+
+ DirDialog dialog(_parent, _("Film to copy settings from"), wxDD_DIR_MUST_EXIST, "CopySettingsPath", add_files_override_path(film));
+
+ if (!dialog.show()) {
+ return;
+ }
+
+ try {
+ auto copy_film = std::make_shared<Film>(dialog.path());
+ copy_film->read_metadata();
+ auto copy_content = copy_film->content();
+ auto iter = std::find_if(copy_content.begin(), copy_content.end(), [this](shared_ptr<const Content> content) {
+ return content->paths() == _content[0]->paths();
+ });
+ if (iter == copy_content.end()) {
+ error_dialog(_parent, wxString::Format(_("Could not find this content in the project \"%s\"."), std_to_wx(dialog.path().filename().string())));
+ } else {
+ _content[0]->take_settings_from(*iter);
+ }
+ } catch (std::exception& e) {
+ error_dialog(_parent, std_to_wx(e.what()));
+ }
+}
+
diff --git a/src/wx/content_menu.h b/src/wx/content_menu.h
index 2f3250284..e7f095390 100644
--- a/src/wx/content_menu.h
+++ b/src/wx/content_menu.h
@@ -58,6 +58,7 @@ private:
void advanced ();
void re_examine ();
void auto_crop ();
+ void copy_settings();
void kdm ();
void ov ();
void set_dcp_settings ();
@@ -81,6 +82,7 @@ private:
wxMenuItem* _advanced;
wxMenuItem* _re_examine;
wxMenuItem* _auto_crop;
+ wxMenuItem* _copy_settings;
wxMenuItem* _kdm;
wxMenuItem* _ov;
wxMenuItem* _choose_cpl;
diff --git a/src/wx/content_panel.cc b/src/wx/content_panel.cc
index 9e73900fc..07a632b4a 100644
--- a/src/wx/content_panel.cc
+++ b/src/wx/content_panel.cc
@@ -21,16 +21,17 @@
#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"
+#include "wx_variant.h"
#include "lib/audio_content.h"
#include "lib/case_insensitive_sorter.h"
#include "lib/compose.hpp"
@@ -673,10 +674,14 @@ ContentPanel::add_dcp(boost::filesystem::path dcp)
} catch (ProjectFolderError &) {
error_dialog (
_parent,
- _(
- "This looks like a DCP-o-matic project folder, which cannot be added to a different project. "
- "Choose the DCP folder inside the DCP-o-matic project folder if that's what you want to import."
- )
+ wxString::Format(
+ _(
+ "This looks like a %s project folder, which cannot be added to a different project. "
+ "Choose the DCP folder inside the %s project folder if that's what you want to import."
+ ),
+ variant::wx::dcpomatic(),
+ variant::wx::dcpomatic()
+ )
);
} catch (exception& e) {
error_dialog(_parent, e.what());
diff --git a/src/wx/content_panel.h b/src/wx/content_panel.h
index ca0d49719..38b634a62 100644
--- a/src/wx/content_panel.h
+++ b/src/wx/content_panel.h
@@ -33,12 +33,12 @@ LIBDCP_ENABLE_WARNINGS
class AudioPanel;
class ContentListCtrl;
class ContentSubPanel;
+class ContentTimelineDialog;
class Film;
class FilmEditor;
class FilmViewer;
class LimitedContentPanelSplitter;
class TextPanel;
-class TimelineDialog;
class TimingPanel;
class VideoPanel;
class wxListCtrl;
@@ -132,7 +132,7 @@ private:
EnumIndexedVector<TextPanel*, TextType> _text_panel;
TimingPanel* _timing_panel;
ContentMenu* _menu;
- wx_ptr<TimelineDialog> _timeline_dialog;
+ wx_ptr<ContentTimelineDialog> _timeline_dialog;
wxNotebook* _parent;
wxWindow* _last_selected_tab = nullptr;
diff --git a/src/wx/content_sub_panel.cc b/src/wx/content_sub_panel.cc
index 6bbc9a51f..10fdfa232 100644
--- a/src/wx/content_sub_panel.cc
+++ b/src/wx/content_sub_panel.cc
@@ -53,29 +53,6 @@ ContentSubPanel::ContentSubPanel (ContentPanel* p, wxString name)
}
void
-ContentSubPanel::setup_refer_button (wxCheckBox* button, wxStaticText* note, shared_ptr<DCPContent> dcp, bool can_reference, wxString cannot)
-{
- button->Enable (can_reference);
-
- if (dcp && !can_reference) {
- note->SetLabel (cannot);
- } else {
- note->SetLabel (wxT(""));
- }
-
- note->Wrap (400);
-
- if (cannot.IsEmpty()) {
- note->Hide ();
- } else {
- note->Show ();
- }
-
- layout ();
-}
-
-
-void
ContentSubPanel::layout ()
{
int x;
diff --git a/src/wx/content_sub_panel.h b/src/wx/content_sub_panel.h
index a3916817e..48970e6a9 100644
--- a/src/wx/content_sub_panel.h
+++ b/src/wx/content_sub_panel.h
@@ -55,7 +55,6 @@ public:
protected:
- void setup_refer_button (wxCheckBox* button, wxStaticText* note, std::shared_ptr<DCPContent> dcp, bool can_reference, wxString cannot);
void layout ();
virtual void add_to_grid () = 0;
diff --git a/src/wx/content_timeline.cc b/src/wx/content_timeline.cc
new file mode 100644
index 000000000..7200bf076
--- /dev/null
+++ b/src/wx/content_timeline.cc
@@ -0,0 +1,1021 @@
+/*
+ Copyright (C) 2013-2021 Carl Hetherington <cth@carlh.net>
+
+ This file is part of DCP-o-matic.
+
+ DCP-o-matic is free software; you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation; either version 2 of the License, or
+ (at your option) any later version.
+
+ DCP-o-matic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with DCP-o-matic. If not, see <http://www.gnu.org/licenses/>.
+
+*/
+
+#include "content_panel.h"
+#include "content_timeline.h"
+#include "film_editor.h"
+#include "film_viewer.h"
+#include "content_timeline_atmos_view.h"
+#include "content_timeline_audio_view.h"
+#include "content_timeline_text_view.h"
+#include "content_timeline_video_view.h"
+#include "timeline_labels_view.h"
+#include "timeline_reels_view.h"
+#include "timeline_time_axis_view.h"
+#include "wx_util.h"
+#include "lib/atmos_mxf_content.h"
+#include "lib/audio_content.h"
+#include "lib/constants.h"
+#include "lib/film.h"
+#include "lib/image_content.h"
+#include "lib/playlist.h"
+#include "lib/text_content.h"
+#include "lib/timer.h"
+#include "lib/video_content.h"
+#include <dcp/scope_guard.h>
+#include <dcp/warnings.h>
+LIBDCP_DISABLE_WARNINGS
+#include <wx/graphics.h>
+LIBDCP_ENABLE_WARNINGS
+#include <iterator>
+#include <list>
+
+
+using std::abs;
+using std::dynamic_pointer_cast;
+using std::list;
+using std::make_shared;
+using std::max;
+using std::min;
+using std::shared_ptr;
+using std::weak_ptr;
+using boost::bind;
+using boost::optional;
+using namespace dcpomatic;
+#if BOOST_VERSION >= 106100
+using namespace boost::placeholders;
+#endif
+
+
+int const ContentTimeline::_minimum_pixels_per_track = 16;
+
+
+ContentTimeline::ContentTimeline(wxWindow* parent, ContentPanel* cp, shared_ptr<Film> film, FilmViewer& viewer)
+ : Timeline(parent)
+ , _labels_canvas (new wxScrolledCanvas (this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxFULL_REPAINT_ON_RESIZE))
+ , _main_canvas (new wxScrolledCanvas (this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxFULL_REPAINT_ON_RESIZE))
+ , _content_panel (cp)
+ , _film (film)
+ , _viewer (viewer)
+ , _time_axis_view (new TimelineTimeAxisView (*this, 64))
+ , _reels_view (new TimelineReelsView (*this, 32))
+ , _labels_view (new TimelineLabelsView (*this))
+ , _tracks (0)
+ , _left_down (false)
+ , _down_view_position (0)
+ , _first_move (false)
+ , _menu (this, viewer)
+ , _snap (true)
+ , _tool (SELECT)
+ , _x_scroll_rate (16)
+ , _y_scroll_rate (16)
+ , _pixels_per_track (48)
+ , _first_resize (true)
+ , _timer (this)
+{
+#ifndef __WXOSX__
+ _labels_canvas->SetDoubleBuffered (true);
+ _main_canvas->SetDoubleBuffered (true);
+#endif
+
+ auto sizer = new wxBoxSizer (wxHORIZONTAL);
+ sizer->Add (_labels_canvas, 0, wxEXPAND);
+ _labels_canvas->SetMinSize (wxSize (_labels_view->bbox().width, -1));
+ sizer->Add (_main_canvas, 1, wxEXPAND);
+ SetSizer (sizer);
+
+ _labels_canvas->Bind(wxEVT_PAINT, boost::bind(&ContentTimeline::paint_labels, this));
+ _main_canvas->Bind (wxEVT_PAINT, boost::bind(&ContentTimeline::paint_main, this));
+ _main_canvas->Bind (wxEVT_LEFT_DOWN, boost::bind(&ContentTimeline::left_down, this, _1));
+ _main_canvas->Bind (wxEVT_LEFT_UP, boost::bind(&ContentTimeline::left_up, this, _1));
+ _main_canvas->Bind (wxEVT_RIGHT_DOWN, boost::bind(&ContentTimeline::right_down, this, _1));
+ _main_canvas->Bind (wxEVT_MOTION, boost::bind(&ContentTimeline::mouse_moved, this, _1));
+ _main_canvas->Bind (wxEVT_SIZE, boost::bind(&ContentTimeline::resized, this));
+ _main_canvas->Bind (wxEVT_MOUSEWHEEL, boost::bind(&ContentTimeline::mouse_wheel_turned, this, _1));
+ _main_canvas->Bind (wxEVT_SCROLLWIN_TOP, boost::bind(&ContentTimeline::scrolled, this, _1));
+ _main_canvas->Bind (wxEVT_SCROLLWIN_BOTTOM, boost::bind(&ContentTimeline::scrolled, this, _1));
+ _main_canvas->Bind (wxEVT_SCROLLWIN_LINEUP, boost::bind(&ContentTimeline::scrolled, this, _1));
+ _main_canvas->Bind (wxEVT_SCROLLWIN_LINEDOWN, boost::bind(&ContentTimeline::scrolled, this, _1));
+ _main_canvas->Bind (wxEVT_SCROLLWIN_PAGEUP, boost::bind(&ContentTimeline::scrolled, this, _1));
+ _main_canvas->Bind (wxEVT_SCROLLWIN_PAGEDOWN, boost::bind(&ContentTimeline::scrolled, this, _1));
+ _main_canvas->Bind (wxEVT_SCROLLWIN_THUMBTRACK, boost::bind(&ContentTimeline::scrolled, this, _1));
+
+ film_change(ChangeType::DONE, FilmProperty::CONTENT);
+
+ SetMinSize (wxSize (640, 4 * pixels_per_track() + 96));
+
+ _film_changed_connection = film->Change.connect(bind(&ContentTimeline::film_change, this, _1, _2));
+ _film_content_change_connection = film->ContentChange.connect(bind(&ContentTimeline::film_content_change, this, _1, _3, _4));
+
+ Bind(wxEVT_TIMER, boost::bind(&ContentTimeline::update_playhead, this));
+ _timer.Start (200, wxTIMER_CONTINUOUS);
+
+ setup_scrollbars ();
+ _labels_canvas->ShowScrollbars (wxSHOW_SB_NEVER, wxSHOW_SB_NEVER);
+}
+
+
+void
+ContentTimeline::mouse_wheel_turned(wxMouseEvent& event)
+{
+ auto const rotation = event.GetWheelRotation();
+
+ if (event.ControlDown()) {
+ /* On my mouse one click of the scroll wheel is 120, and it's -ve when
+ * scrolling the wheel towards me.
+ */
+ auto const scale = rotation > 0 ?
+ (1.0 / (rotation / 90.0)) :
+ (-rotation / 90.0);
+
+ int before_start_x;
+ int before_start_y;
+ _main_canvas->GetViewStart(&before_start_x, &before_start_y);
+
+ auto const before_pps = _pixels_per_second.get_value_or(1);
+ auto const before_pos = _last_mouse_wheel_x && *_last_mouse_wheel_x == event.GetX() ?
+ *_last_mouse_wheel_time :
+ (before_start_x * _x_scroll_rate + event.GetX()) / before_pps;
+
+ set_pixels_per_second(before_pps * scale);
+ setup_scrollbars();
+
+ auto after_left = std::max(0.0, before_pos * _pixels_per_second.get_value_or(1) - event.GetX());
+ _main_canvas->Scroll(after_left / _x_scroll_rate, before_start_y);
+ _labels_canvas->Scroll(0, before_start_y);
+ Refresh();
+
+ if (!_last_mouse_wheel_x || *_last_mouse_wheel_x != event.GetX()) {
+ _last_mouse_wheel_x = event.GetX();
+ _last_mouse_wheel_time = before_pos;
+ }
+ } else if (event.ShiftDown()) {
+ int before_start_x;
+ int before_start_y;
+ _main_canvas->GetViewStart(&before_start_x, &before_start_y);
+ auto const width = _main_canvas->GetSize().GetWidth();
+ _main_canvas->Scroll(std::max(0.0, before_start_x - rotation * 100.0 / width), before_start_y);
+ }
+}
+
+
+void
+ContentTimeline::update_playhead()
+{
+ Refresh ();
+}
+
+
+void
+ContentTimeline::paint_labels()
+{
+ wxPaintDC dc (_labels_canvas);
+
+ auto film = _film.lock();
+ if (film->content().empty()) {
+ return;
+ }
+
+ auto gc = wxGraphicsContext::Create (dc);
+ if (!gc) {
+ return;
+ }
+
+ dcp::ScopeGuard sg = [gc]() { delete gc; };
+
+ int vsx, vsy;
+ _labels_canvas->GetViewStart (&vsx, &vsy);
+ gc->Translate (-vsx * _x_scroll_rate, -vsy * _y_scroll_rate + tracks_y_offset());
+
+ _labels_view->paint (gc, {});
+}
+
+
+void
+ContentTimeline::paint_main()
+{
+ wxPaintDC dc (_main_canvas);
+ dc.Clear();
+
+ auto film = _film.lock();
+ if (film->content().empty()) {
+ return;
+ }
+
+ _main_canvas->DoPrepareDC (dc);
+
+ auto gc = wxGraphicsContext::Create (dc);
+ if (!gc) {
+ return;
+ }
+
+ dcp::ScopeGuard sg = [gc]() { delete gc; };
+
+ gc->SetAntialiasMode (wxANTIALIAS_DEFAULT);
+
+ for (auto i: _views) {
+
+ auto ic = dynamic_pointer_cast<TimelineContentView> (i);
+
+ /* Find areas of overlap with other content views, so that we can plot them */
+ list<dcpomatic::Rect<int>> overlaps;
+ for (auto j: _views) {
+ auto jc = dynamic_pointer_cast<TimelineContentView> (j);
+ /* No overlap with non-content views, views on different tracks, audio views or non-active views */
+ if (!ic || !jc || i == j || ic->track() != jc->track() || ic->track().get_value_or(2) >= 2 || !ic->active() || !jc->active()) {
+ continue;
+ }
+
+ auto r = j->bbox().intersection(i->bbox());
+ if (r) {
+ overlaps.push_back (r.get ());
+ }
+ }
+
+ i->paint (gc, overlaps);
+ }
+
+ if (_zoom_point) {
+ gc->SetPen(gui_is_dark() ? *wxWHITE_PEN : *wxBLACK_PEN);
+ gc->SetBrush (*wxTRANSPARENT_BRUSH);
+ gc->DrawRectangle (
+ min (_down_point.x, _zoom_point->x),
+ min (_down_point.y, _zoom_point->y),
+ abs (_down_point.x - _zoom_point->x),
+ abs (_down_point.y - _zoom_point->y)
+ );
+ }
+
+ /* Playhead */
+
+ gc->SetPen (*wxRED_PEN);
+ auto path = gc->CreatePath ();
+ double const ph = _viewer.position().seconds() * pixels_per_second().get_value_or(0);
+ path.MoveToPoint (ph, 0);
+ path.AddLineToPoint (ph, pixels_per_track() * _tracks + 32);
+ gc->StrokePath (path);
+}
+
+
+void
+ContentTimeline::film_change(ChangeType type, FilmProperty p)
+{
+ if (type != ChangeType::DONE) {
+ return;
+ }
+
+ if (p == FilmProperty::CONTENT || p == FilmProperty::REEL_TYPE || p == FilmProperty::REEL_LENGTH) {
+ ensure_ui_thread ();
+ recreate_views ();
+ } else if (p == FilmProperty::CONTENT_ORDER) {
+ Refresh ();
+ }
+}
+
+
+void
+ContentTimeline::recreate_views()
+{
+ auto film = _film.lock ();
+ if (!film) {
+ return;
+ }
+
+ _views.clear ();
+ _views.push_back (_time_axis_view);
+ _views.push_back (_reels_view);
+
+ for (auto i: film->content ()) {
+ if (i->video) {
+ _views.push_back(make_shared<ContentTimelineVideoView>(*this, i));
+ }
+
+ if (i->has_mapped_audio()) {
+ _views.push_back(make_shared<ContentTimelineAudioView>(*this, i));
+ }
+
+ for (auto j: i->text) {
+ _views.push_back(make_shared<ContentTimelineTextView>(*this, i, j));
+ }
+
+ if (i->atmos) {
+ _views.push_back(make_shared<ContentTimelineAtmosView>(*this, i));
+ }
+ }
+
+ assign_tracks ();
+ setup_scrollbars ();
+ Refresh ();
+}
+
+
+void
+ContentTimeline::film_content_change(ChangeType type, int property, bool frequent)
+{
+ if (type != ChangeType::DONE) {
+ return;
+ }
+
+ ensure_ui_thread ();
+
+ if (property == AudioContentProperty::STREAMS || property == VideoContentProperty::FRAME_TYPE) {
+ recreate_views ();
+ } else if (property == ContentProperty::POSITION || property == ContentProperty::LENGTH) {
+ _reels_view->force_redraw ();
+ } else if (!frequent) {
+ setup_scrollbars ();
+ Refresh ();
+ }
+}
+
+
+template <class T>
+int
+place(shared_ptr<const Film> film, ContentTimelineViewList& views, int& tracks)
+{
+ int const base = tracks;
+
+ for (auto i: views) {
+ if (!dynamic_pointer_cast<T>(i)) {
+ continue;
+ }
+
+ auto cv = dynamic_pointer_cast<TimelineContentView> (i);
+ DCPOMATIC_ASSERT(cv);
+
+ int t = base;
+
+ auto content = cv->content();
+ DCPTimePeriod const content_period = content->period(film);
+
+ while (true) {
+ auto j = views.begin();
+ while (j != views.end()) {
+ auto test = dynamic_pointer_cast<T> (*j);
+ if (!test) {
+ ++j;
+ continue;
+ }
+
+ auto test_content = test->content();
+ if (
+ test->track() && test->track().get() == t &&
+ content_period.overlap(test_content->period(film))
+ ) {
+ /* we have an overlap on track `t' */
+ ++t;
+ break;
+ }
+
+ ++j;
+ }
+
+ if (j == views.end ()) {
+ /* no overlap on `t' */
+ break;
+ }
+ }
+
+ cv->set_track (t);
+ tracks = max (tracks, t + 1);
+ }
+
+ return tracks - base;
+}
+
+
+/** Compare the mapped output channels of two TimelineViews, so we can into
+ * order of first mapped DCP channel.
+ */
+struct AudioMappingComparator {
+ bool operator()(shared_ptr<ContentTimelineView> a, shared_ptr<ContentTimelineView> b) {
+ int la = -1;
+ auto cva = dynamic_pointer_cast<ContentTimelineAudioView>(a);
+ if (cva) {
+ auto oc = cva->content()->audio->mapping().mapped_output_channels();
+ la = *min_element(boost::begin(oc), boost::end(oc));
+ }
+ int lb = -1;
+ auto cvb = dynamic_pointer_cast<ContentTimelineAudioView>(b);
+ if (cvb) {
+ auto oc = cvb->content()->audio->mapping().mapped_output_channels();
+ lb = *min_element(boost::begin(oc), boost::end(oc));
+ }
+ return la < lb;
+ }
+};
+
+
+void
+ContentTimeline::assign_tracks()
+{
+ /* Tracks are:
+ Video 1
+ Video 2
+ Video N
+ Text 1
+ Text 2
+ Text N
+ Atmos
+ Audio 1
+ Audio 2
+ Audio N
+ */
+
+ auto film = _film.lock ();
+ DCPOMATIC_ASSERT (film);
+
+ _tracks = 0;
+
+ for (auto i: _views) {
+ auto c = dynamic_pointer_cast<TimelineContentView>(i);
+ if (c) {
+ c->unset_track ();
+ }
+ }
+
+ int const video_tracks = place<ContentTimelineVideoView>(film, _views, _tracks);
+ int const text_tracks = place<ContentTimelineTextView>(film, _views, _tracks);
+
+ /* Atmos */
+
+ bool have_atmos = false;
+ for (auto i: _views) {
+ auto cv = dynamic_pointer_cast<ContentTimelineAtmosView>(i);
+ if (cv) {
+ cv->set_track (_tracks);
+ have_atmos = true;
+ }
+ }
+
+ if (have_atmos) {
+ ++_tracks;
+ }
+
+ /* Audio. We're sorting the views so that we get the audio views in order of increasing
+ DCP channel index.
+ */
+
+ auto views = _views;
+ sort(views.begin(), views.end(), AudioMappingComparator());
+ int const audio_tracks = place<ContentTimelineAudioView>(film, views, _tracks);
+
+ _labels_view->set_video_tracks (video_tracks);
+ _labels_view->set_audio_tracks (audio_tracks);
+ _labels_view->set_text_tracks (text_tracks);
+ _labels_view->set_atmos (have_atmos);
+
+ _time_axis_view->set_y (tracks());
+ _reels_view->set_y (8);
+}
+
+
+int
+ContentTimeline::tracks() const
+{
+ return _tracks;
+}
+
+
+void
+ContentTimeline::setup_scrollbars()
+{
+ auto film = _film.lock ();
+ if (!film || !_pixels_per_second) {
+ return;
+ }
+
+ int const h = tracks() * pixels_per_track() + tracks_y_offset() + _time_axis_view->bbox().height;
+
+ _labels_canvas->SetVirtualSize (_labels_view->bbox().width, h);
+ _labels_canvas->SetScrollRate (_x_scroll_rate, _y_scroll_rate);
+ _main_canvas->SetVirtualSize (*_pixels_per_second * film->length().seconds(), h);
+ _main_canvas->SetScrollRate (_x_scroll_rate, _y_scroll_rate);
+}
+
+
+shared_ptr<ContentTimelineView>
+ContentTimeline::event_to_view(wxMouseEvent& ev)
+{
+ /* Search backwards through views so that we find the uppermost one first */
+ auto i = _views.rbegin();
+
+ int vsx, vsy;
+ _main_canvas->GetViewStart (&vsx, &vsy);
+ Position<int> const p (ev.GetX() + vsx * _x_scroll_rate, ev.GetY() + vsy * _y_scroll_rate);
+
+ while (i != _views.rend() && !(*i)->bbox().contains (p)) {
+ ++i;
+ }
+
+ if (i == _views.rend ()) {
+ return {};
+ }
+
+ return *i;
+}
+
+
+void
+ContentTimeline::left_down(wxMouseEvent& ev)
+{
+ _left_down = true;
+ _down_point = ev.GetPosition ();
+
+ switch (_tool) {
+ case SELECT:
+ left_down_select (ev);
+ break;
+ case ZOOM:
+ case ZOOM_ALL:
+ case SNAP:
+ case SEQUENCE:
+ /* Nothing to do */
+ break;
+ }
+}
+
+
+void
+ContentTimeline::left_down_select(wxMouseEvent& ev)
+{
+ auto view = event_to_view (ev);
+ auto content_view = dynamic_pointer_cast<TimelineContentView>(view);
+
+ _down_view.reset ();
+
+ if (content_view) {
+ _down_view = content_view;
+ _down_view_position = content_view->content()->position ();
+ }
+
+ if (dynamic_pointer_cast<TimelineTimeAxisView>(view)) {
+ int vsx, vsy;
+ _main_canvas->GetViewStart(&vsx, &vsy);
+ _viewer.seek(DCPTime::from_seconds((ev.GetPosition().x + vsx * _x_scroll_rate) / _pixels_per_second.get_value_or(1)), true);
+ }
+
+ for (auto i: _views) {
+ auto cv = dynamic_pointer_cast<TimelineContentView>(i);
+ if (!cv) {
+ continue;
+ }
+
+ if (!ev.ShiftDown ()) {
+ cv->set_selected (view == i);
+ }
+ }
+
+ if (content_view && ev.ShiftDown ()) {
+ content_view->set_selected (!content_view->selected ());
+ }
+
+ _first_move = false;
+
+ if (_down_view) {
+ /* Pre-compute the points that we might snap to */
+ for (auto i: _views) {
+ auto cv = dynamic_pointer_cast<TimelineContentView>(i);
+ if (!cv || cv == _down_view || cv->content() == _down_view->content()) {
+ continue;
+ }
+
+ auto film = _film.lock ();
+ DCPOMATIC_ASSERT (film);
+
+ _start_snaps.push_back (cv->content()->position());
+ _end_snaps.push_back (cv->content()->position());
+ _start_snaps.push_back (cv->content()->end(film));
+ _end_snaps.push_back (cv->content()->end(film));
+
+ for (auto i: cv->content()->reel_split_points(film)) {
+ _start_snaps.push_back (i);
+ }
+ }
+
+ /* Tell everyone that things might change frequently during the drag */
+ _down_view->content()->set_change_signals_frequent (true);
+ }
+}
+
+
+void
+ContentTimeline::left_up(wxMouseEvent& ev)
+{
+ _left_down = false;
+
+ switch (_tool) {
+ case SELECT:
+ left_up_select (ev);
+ break;
+ case ZOOM:
+ left_up_zoom (ev);
+ break;
+ case ZOOM_ALL:
+ case SNAP:
+ case SEQUENCE:
+ break;
+ }
+}
+
+
+void
+ContentTimeline::left_up_select(wxMouseEvent& ev)
+{
+ if (_down_view) {
+ _down_view->content()->set_change_signals_frequent (false);
+ }
+
+ _content_panel->set_selection (selected_content ());
+ /* Since we may have just set change signals back to `not-frequent', we have to
+ make sure this position change is signalled, even if the position value has
+ not changed since the last time it was set (with frequent=true). This is
+ a bit of a hack.
+ */
+ set_position_from_event (ev, true);
+
+ /* Clear up up the stuff we don't do during drag */
+ assign_tracks ();
+ setup_scrollbars ();
+ Refresh ();
+
+ _start_snaps.clear ();
+ _end_snaps.clear ();
+}
+
+
+void
+ContentTimeline::left_up_zoom(wxMouseEvent& ev)
+{
+ _zoom_point = ev.GetPosition ();
+
+ int vsx, vsy;
+ _main_canvas->GetViewStart (&vsx, &vsy);
+
+ wxPoint top_left(min(_down_point.x, _zoom_point->x), min(_down_point.y, _zoom_point->y));
+ wxPoint bottom_right(max(_down_point.x, _zoom_point->x), max(_down_point.y, _zoom_point->y));
+
+ if ((bottom_right.x - top_left.x) < 8 || (bottom_right.y - top_left.y) < 8) {
+ /* Very small zoom rectangle: we assume it wasn't intentional */
+ _zoom_point = optional<wxPoint> ();
+ Refresh ();
+ return;
+ }
+
+ auto const time_left = DCPTime::from_seconds((top_left.x + vsx) / *_pixels_per_second);
+ auto const time_right = DCPTime::from_seconds((bottom_right.x + vsx) / *_pixels_per_second);
+ set_pixels_per_second (double(GetSize().GetWidth()) / (time_right.seconds() - time_left.seconds()));
+
+ double const tracks_top = double(top_left.y - tracks_y_offset()) / _pixels_per_track;
+ double const tracks_bottom = double(bottom_right.y - tracks_y_offset()) / _pixels_per_track;
+ set_pixels_per_track (lrint(GetSize().GetHeight() / (tracks_bottom - tracks_top)));
+
+ setup_scrollbars ();
+ int const y = (tracks_top * _pixels_per_track + tracks_y_offset()) / _y_scroll_rate;
+ _main_canvas->Scroll (time_left.seconds() * *_pixels_per_second / _x_scroll_rate, y);
+ _labels_canvas->Scroll (0, y);
+
+ _zoom_point = optional<wxPoint> ();
+ Refresh ();
+}
+
+
+void
+ContentTimeline::set_pixels_per_track(int h)
+{
+ _pixels_per_track = max(_minimum_pixels_per_track, h);
+}
+
+
+void
+ContentTimeline::mouse_moved(wxMouseEvent& ev)
+{
+ switch (_tool) {
+ case SELECT:
+ mouse_moved_select (ev);
+ break;
+ case ZOOM:
+ mouse_moved_zoom (ev);
+ break;
+ case ZOOM_ALL:
+ case SNAP:
+ case SEQUENCE:
+ break;
+ }
+}
+
+
+void
+ContentTimeline::mouse_moved_select(wxMouseEvent& ev)
+{
+ if (!_left_down) {
+ return;
+ }
+
+ set_position_from_event (ev);
+}
+
+
+void
+ContentTimeline::mouse_moved_zoom(wxMouseEvent& ev)
+{
+ if (!_left_down) {
+ return;
+ }
+
+ _zoom_point = ev.GetPosition ();
+ setup_scrollbars();
+ Refresh ();
+}
+
+
+void
+ContentTimeline::right_down(wxMouseEvent& ev)
+{
+ switch (_tool) {
+ case SELECT:
+ right_down_select (ev);
+ break;
+ case ZOOM:
+ /* Zoom out */
+ set_pixels_per_second (*_pixels_per_second / 2);
+ set_pixels_per_track (_pixels_per_track / 2);
+ setup_scrollbars ();
+ Refresh ();
+ break;
+ case ZOOM_ALL:
+ case SNAP:
+ case SEQUENCE:
+ break;
+ }
+}
+
+
+void
+ContentTimeline::right_down_select(wxMouseEvent& ev)
+{
+ auto view = event_to_view (ev);
+ auto cv = dynamic_pointer_cast<TimelineContentView> (view);
+ if (!cv) {
+ return;
+ }
+
+ if (!cv->selected ()) {
+ clear_selection ();
+ cv->set_selected (true);
+ }
+
+ _menu.popup (_film, selected_content (), selected_views (), ev.GetPosition ());
+}
+
+
+void
+ContentTimeline::maybe_snap(DCPTime a, DCPTime b, optional<DCPTime>& nearest_distance) const
+{
+ auto const d = a - b;
+ if (!nearest_distance || d.abs() < nearest_distance.get().abs()) {
+ nearest_distance = d;
+ }
+}
+
+
+void
+ContentTimeline::set_position_from_event(wxMouseEvent& ev, bool force_emit)
+{
+ if (!_pixels_per_second) {
+ return;
+ }
+
+ double const pps = _pixels_per_second.get ();
+
+ auto const p = ev.GetPosition();
+
+ if (!_first_move) {
+ /* We haven't moved yet; in that case, we must move the mouse some reasonable distance
+ before the drag is considered to have started.
+ */
+ int const dist = sqrt (pow (p.x - _down_point.x, 2) + pow (p.y - _down_point.y, 2));
+ if (dist < 8) {
+ return;
+ }
+ _first_move = true;
+ }
+
+ if (!_down_view) {
+ return;
+ }
+
+ auto new_position = _down_view_position + DCPTime::from_seconds ((p.x - _down_point.x) / pps);
+
+ auto film = _film.lock ();
+ DCPOMATIC_ASSERT (film);
+
+ if (_snap) {
+ auto const new_end = new_position + _down_view->content()->length_after_trim(film);
+ /* Signed `distance' to nearest thing (i.e. negative is left on the timeline,
+ positive is right).
+ */
+ optional<DCPTime> nearest_distance;
+
+ /* Find the nearest snap point */
+
+ for (auto i: _start_snaps) {
+ maybe_snap (i, new_position, nearest_distance);
+ }
+
+ for (auto i: _end_snaps) {
+ maybe_snap (i, new_end, nearest_distance);
+ }
+
+ if (nearest_distance) {
+ /* Snap if it's close; `close' means within a proportion of the time on the timeline */
+ if (nearest_distance.get().abs() < DCPTime::from_seconds ((width() / pps) / SNAP_SUBDIVISION)) {
+ new_position += nearest_distance.get ();
+ }
+ }
+ }
+
+ if (new_position < DCPTime ()) {
+ new_position = DCPTime ();
+ }
+
+ _down_view->content()->set_position (film, new_position, force_emit);
+
+ film->set_sequence (false);
+}
+
+
+void
+ContentTimeline::force_redraw(dcpomatic::Rect<int> const & r)
+{
+ _main_canvas->RefreshRect (wxRect (r.x, r.y, r.width, r.height), false);
+}
+
+
+shared_ptr<const Film>
+ContentTimeline::film() const
+{
+ return _film.lock ();
+}
+
+
+void
+ContentTimeline::resized()
+{
+ if (_main_canvas->GetSize().GetWidth() > 0 && _first_resize) {
+ zoom_all ();
+ _first_resize = false;
+ }
+ setup_scrollbars ();
+}
+
+
+void
+ContentTimeline::clear_selection()
+{
+ for (auto i: _views) {
+ shared_ptr<TimelineContentView> cv = dynamic_pointer_cast<TimelineContentView>(i);
+ if (cv) {
+ cv->set_selected (false);
+ }
+ }
+}
+
+
+TimelineContentViewList
+ContentTimeline::selected_views() const
+{
+ TimelineContentViewList sel;
+
+ for (auto i: _views) {
+ auto cv = dynamic_pointer_cast<TimelineContentView>(i);
+ if (cv && cv->selected()) {
+ sel.push_back (cv);
+ }
+ }
+
+ return sel;
+}
+
+
+ContentList
+ContentTimeline::selected_content() const
+{
+ ContentList sel;
+
+ for (auto i: selected_views()) {
+ sel.push_back(i->content());
+ }
+
+ return sel;
+}
+
+
+void
+ContentTimeline::set_selection(ContentList selection)
+{
+ for (auto i: _views) {
+ auto cv = dynamic_pointer_cast<TimelineContentView> (i);
+ if (cv) {
+ cv->set_selected (find (selection.begin(), selection.end(), cv->content ()) != selection.end ());
+ }
+ }
+}
+
+
+int
+ContentTimeline::tracks_y_offset() const
+{
+ return _reels_view->bbox().height + 4;
+}
+
+
+int
+ContentTimeline::width() const
+{
+ return _main_canvas->GetVirtualSize().GetWidth();
+}
+
+
+void
+ContentTimeline::scrolled(wxScrollWinEvent& ev)
+{
+ if (ev.GetOrientation() == wxVERTICAL) {
+ int x, y;
+ _main_canvas->GetViewStart (&x, &y);
+ _labels_canvas->Scroll (0, y);
+ }
+ ev.Skip ();
+}
+
+
+void
+ContentTimeline::tool_clicked(Tool t)
+{
+ switch (t) {
+ case ZOOM:
+ case SELECT:
+ _tool = t;
+ break;
+ case ZOOM_ALL:
+ zoom_all ();
+ break;
+ case SNAP:
+ case SEQUENCE:
+ break;
+ }
+}
+
+
+void
+ContentTimeline::zoom_all()
+{
+ auto film = _film.lock ();
+ DCPOMATIC_ASSERT (film);
+ set_pixels_per_second((_main_canvas->GetSize().GetWidth() - 32) / std::max(1.0, film->length().seconds()));
+ set_pixels_per_track((_main_canvas->GetSize().GetHeight() - tracks_y_offset() - _time_axis_view->bbox().height - 32) / std::max(1, _tracks));
+ setup_scrollbars ();
+ _main_canvas->Scroll (0, 0);
+ _labels_canvas->Scroll (0, 0);
+ Refresh ();
+}
+
+
+void
+ContentTimeline::keypress(wxKeyEvent const& event)
+{
+ if (event.GetKeyCode() == WXK_DELETE) {
+ auto film = _film.lock();
+ DCPOMATIC_ASSERT(film);
+ film->remove_content(selected_content());
+ } else {
+ switch (event.GetRawKeyCode()) {
+ case '+':
+ set_pixels_per_second(_pixels_per_second.get_value_or(1) * 2);
+ setup_scrollbars();
+ break;
+ case '-':
+ set_pixels_per_second(_pixels_per_second.get_value_or(1) / 2);
+ setup_scrollbars();
+ break;
+ }
+ }
+}
+
diff --git a/src/wx/content_timeline.h b/src/wx/content_timeline.h
new file mode 100644
index 000000000..10f880191
--- /dev/null
+++ b/src/wx/content_timeline.h
@@ -0,0 +1,149 @@
+/*
+ Copyright (C) 2013-2019 Carl Hetherington <cth@carlh.net>
+
+ This file is part of DCP-o-matic.
+
+ DCP-o-matic is free software; you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation; either version 2 of the License, or
+ (at your option) any later version.
+
+ DCP-o-matic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with DCP-o-matic. If not, see <http://www.gnu.org/licenses/>.
+
+*/
+
+
+#include "content_menu.h"
+#include "timeline.h"
+#include "timeline_content_view.h"
+#include "lib/film_property.h"
+#include "lib/rect.h"
+#include <dcp/warnings.h>
+LIBDCP_DISABLE_WARNINGS
+#include <wx/wx.h>
+LIBDCP_ENABLE_WARNINGS
+#include <boost/signals2.hpp>
+
+
+class ContentPanel;
+class ContentTimelineView;
+class Film;
+class FilmViewer;
+class TimelineLabelsView;
+class TimelineReelsView;
+class TimelineTimeAxisView;
+
+
+class ContentTimeline : public Timeline
+{
+public:
+ ContentTimeline(wxWindow *, ContentPanel *, std::shared_ptr<Film>, FilmViewer& viewer);
+
+ std::shared_ptr<const Film> film () const;
+
+ void force_redraw (dcpomatic::Rect<int> const &);
+
+ int width () const;
+
+ int pixels_per_track () const {
+ return _pixels_per_track;
+ }
+
+ int tracks () const;
+
+ void set_snap (bool s) {
+ _snap = s;
+ }
+
+ bool snap () const {
+ return _snap;
+ }
+
+ void set_selection (ContentList selection);
+
+ enum Tool {
+ SELECT,
+ ZOOM,
+ ZOOM_ALL,
+ SNAP,
+ SEQUENCE
+ };
+
+ void tool_clicked (Tool t);
+
+ int tracks_y_offset () const;
+
+ void keypress(wxKeyEvent const &);
+
+private:
+ void paint_labels ();
+ void paint_main ();
+ void left_down (wxMouseEvent &);
+ void left_down_select (wxMouseEvent &);
+ void left_up (wxMouseEvent &);
+ void left_up_select (wxMouseEvent &);
+ void left_up_zoom (wxMouseEvent &);
+ void right_down (wxMouseEvent &);
+ void right_down_select (wxMouseEvent &);
+ void mouse_moved (wxMouseEvent &);
+ void mouse_moved_select (wxMouseEvent &);
+ void mouse_moved_zoom (wxMouseEvent &);
+ void film_change(ChangeType type, FilmProperty);
+ void film_content_change (ChangeType type, int, bool frequent);
+ void resized ();
+ void assign_tracks ();
+ void set_position_from_event (wxMouseEvent& ev, bool force_emit = false);
+ void clear_selection ();
+ void recreate_views ();
+ void setup_scrollbars ();
+ void scrolled (wxScrollWinEvent& ev);
+ void set_pixels_per_track (int h);
+ void zoom_all ();
+ void update_playhead ();
+ void mouse_wheel_turned(wxMouseEvent& event);
+
+ std::shared_ptr<ContentTimelineView> event_to_view(wxMouseEvent &);
+ TimelineContentViewList selected_views () const;
+ ContentList selected_content () const;
+ void maybe_snap (dcpomatic::DCPTime a, dcpomatic::DCPTime b, boost::optional<dcpomatic::DCPTime>& nearest_distance) const;
+
+ wxScrolledCanvas* _labels_canvas;
+ wxScrolledCanvas* _main_canvas;
+ ContentPanel* _content_panel;
+ std::weak_ptr<Film> _film;
+ FilmViewer& _viewer;
+ ContentTimelineViewList _views;
+ std::shared_ptr<TimelineTimeAxisView> _time_axis_view;
+ std::shared_ptr<TimelineReelsView> _reels_view;
+ std::shared_ptr<TimelineLabelsView> _labels_view;
+ int _tracks;
+ bool _left_down;
+ wxPoint _down_point;
+ boost::optional<wxPoint> _zoom_point;
+ std::shared_ptr<TimelineContentView> _down_view;
+ dcpomatic::DCPTime _down_view_position;
+ bool _first_move;
+ ContentMenu _menu;
+ bool _snap;
+ std::list<dcpomatic::DCPTime> _start_snaps;
+ std::list<dcpomatic::DCPTime> _end_snaps;
+ Tool _tool;
+ int _x_scroll_rate;
+ int _y_scroll_rate;
+ int _pixels_per_track;
+ bool _first_resize;
+ wxTimer _timer;
+ boost::optional<int> _last_mouse_wheel_x;
+ boost::optional<double> _last_mouse_wheel_time;
+
+ static int const _minimum_pixels_per_track;
+
+ boost::signals2::scoped_connection _film_changed_connection;
+ boost::signals2::scoped_connection _film_content_change_connection;
+};
diff --git a/src/wx/timeline_atmos_content_view.cc b/src/wx/content_timeline_atmos_view.cc
index ec0f06ec1..2352ba5dc 100644
--- a/src/wx/timeline_atmos_content_view.cc
+++ b/src/wx/content_timeline_atmos_view.cc
@@ -19,17 +19,18 @@
*/
-#include "timeline_atmos_content_view.h"
+#include "colours.h"
+#include "content_timeline_atmos_view.h"
using std::shared_ptr;
-/** @class TimelineAtmosContentView
- * @brief Timeline view for AtmosContent.
+/** @class ContentTimelineContentView
+ * @brief Content timeline view for AtmosContent.
*/
-TimelineAtmosContentView::TimelineAtmosContentView (Timeline& tl, shared_ptr<Content> c)
+ContentTimelineAtmosView::ContentTimelineAtmosView(ContentTimeline& tl, shared_ptr<Content> c)
: TimelineContentView (tl, c)
{
@@ -37,14 +38,14 @@ TimelineAtmosContentView::TimelineAtmosContentView (Timeline& tl, shared_ptr<Con
wxColour
-TimelineAtmosContentView::background_colour () const
+ContentTimelineAtmosView::background_colour() const
{
- return wxColour (149, 121, 232, 255);
+ return ATMOS_CONTENT_COLOUR;
}
wxColour
-TimelineAtmosContentView::foreground_colour () const
+ContentTimelineAtmosView::foreground_colour() const
{
return wxColour (0, 0, 0, 255);
}
diff --git a/src/wx/timeline_atmos_content_view.h b/src/wx/content_timeline_atmos_view.h
index 15da14edc..d5fea9248 100644
--- a/src/wx/timeline_atmos_content_view.h
+++ b/src/wx/content_timeline_atmos_view.h
@@ -22,13 +22,13 @@
#include "timeline_content_view.h"
-/** @class TimelineAtmosContentView
- * @brief Timeline view for AtmosContent.
+/** @class ContentTimelineAtmosContentView
+ * @brief Content timeline view for AtmosContent.
*/
-class TimelineAtmosContentView : public TimelineContentView
+class ContentTimelineAtmosView : public TimelineContentView
{
public:
- TimelineAtmosContentView (Timeline& tl, std::shared_ptr<Content> c);
+ ContentTimelineAtmosView(ContentTimeline& tl, std::shared_ptr<Content> c);
private:
bool active () const override {
diff --git a/src/wx/timeline_audio_content_view.cc b/src/wx/content_timeline_audio_view.cc
index 0e9701034..cff0b5ed5 100644
--- a/src/wx/timeline_audio_content_view.cc
+++ b/src/wx/content_timeline_audio_view.cc
@@ -18,39 +18,44 @@
*/
-#include "timeline_audio_content_view.h"
+
+#include "colours.h"
+#include "content_timeline_audio_view.h"
#include "wx_util.h"
#include "lib/audio_content.h"
#include "lib/util.h"
+
+using std::dynamic_pointer_cast;
using std::list;
using std::shared_ptr;
-using std::dynamic_pointer_cast;
-/** @class TimelineAudioContentView
- * @brief Timeline view for AudioContent.
+
+/** @class ContentTimelineAudioView
+ * @brief Content timeline view for AudioContent.
*/
-TimelineAudioContentView::TimelineAudioContentView (Timeline& tl, shared_ptr<Content> c)
+
+ContentTimelineAudioView::ContentTimelineAudioView(ContentTimeline& tl, shared_ptr<Content> c)
: TimelineContentView (tl, c)
{
}
wxColour
-TimelineAudioContentView::background_colour () const
+ContentTimelineAudioView::background_colour () const
{
- return wxColour (149, 121, 232, 255);
+ return AUDIO_CONTENT_COLOUR;
}
wxColour
-TimelineAudioContentView::foreground_colour () const
+ContentTimelineAudioView::foreground_colour () const
{
return wxColour (0, 0, 0, 255);
}
wxString
-TimelineAudioContentView::label () const
+ContentTimelineAudioView::label () const
{
wxString s = TimelineContentView::label ();
shared_ptr<AudioContent> ac = content()->audio;
diff --git a/src/wx/timeline_audio_content_view.h b/src/wx/content_timeline_audio_view.h
index 5b8d6cdc2..719c2536d 100644
--- a/src/wx/timeline_audio_content_view.h
+++ b/src/wx/content_timeline_audio_view.h
@@ -22,13 +22,13 @@
#include "timeline_content_view.h"
-/** @class TimelineAudioContentView
- * @brief Timeline view for AudioContent.
+/** @class ContentTimelineAudioView
+ * @brief Content timeline view for AudioContent.
*/
-class TimelineAudioContentView : public TimelineContentView
+class ContentTimelineAudioView : public TimelineContentView
{
public:
- TimelineAudioContentView (Timeline& tl, std::shared_ptr<Content> c);
+ ContentTimelineAudioView(ContentTimeline& tl, std::shared_ptr<Content> c);
private:
bool active () const override {
diff --git a/src/wx/timeline_dialog.cc b/src/wx/content_timeline_dialog.cc
index e0e1689a8..697e6561f 100644
--- a/src/wx/timeline_dialog.cc
+++ b/src/wx/content_timeline_dialog.cc
@@ -20,8 +20,8 @@
#include "content_panel.h"
+#include "content_timeline_dialog.h"
#include "film_editor.h"
-#include "timeline_dialog.h"
#include "wx_util.h"
#include "lib/compose.hpp"
#include "lib/cross.h"
@@ -43,7 +43,7 @@ using namespace boost::placeholders;
#endif
-TimelineDialog::TimelineDialog(ContentPanel* cp, shared_ptr<Film> film, FilmViewer& viewer)
+ContentTimelineDialog::ContentTimelineDialog(ContentPanel* cp, shared_ptr<Film> film, FilmViewer& viewer)
: wxDialog (
cp->window(),
wxID_ANY,
@@ -73,14 +73,14 @@ TimelineDialog::TimelineDialog(ContentPanel* cp, shared_ptr<Film> film, FilmView
_toolbar = new wxToolBar (this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxTB_HORIZONTAL);
_toolbar->SetMargins (4, 4);
_toolbar->SetToolBitmapSize (wxSize(32, 32));
- _toolbar->AddRadioTool ((int) Timeline::SELECT, _("Select"), select, wxNullBitmap, _("Select and move content"));
- _toolbar->AddRadioTool ((int) Timeline::ZOOM, _("Zoom"), zoom, wxNullBitmap, _("Zoom in / out"));
- _toolbar->AddTool ((int) Timeline::ZOOM_ALL, _("Zoom all"), zoom_all, _("Zoom out to whole film"));
- _toolbar->AddCheckTool ((int) Timeline::SNAP, _("Snap"), snap, wxNullBitmap, _("Snap"));
- _toolbar->AddCheckTool ((int) Timeline::SEQUENCE, _("Sequence"), sequence, wxNullBitmap, _("Keep video and subtitles in sequence"));
+ _toolbar->AddRadioTool(static_cast<int>(ContentTimeline::SELECT), _("Select"), select, wxNullBitmap, _("Select and move content"));
+ _toolbar->AddRadioTool(static_cast<int>(ContentTimeline::ZOOM), _("Zoom"), zoom, wxNullBitmap, _("Zoom in / out"));
+ _toolbar->AddTool(static_cast<int>(ContentTimeline::ZOOM_ALL), _("Zoom all"), zoom_all, _("Zoom out to whole film"));
+ _toolbar->AddCheckTool(static_cast<int>(ContentTimeline::SNAP), _("Snap"), snap, wxNullBitmap, _("Snap"));
+ _toolbar->AddCheckTool(static_cast<int>(ContentTimeline::SEQUENCE), _("Sequence"), sequence, wxNullBitmap, _("Keep video and subtitles in sequence"));
_toolbar->Realize ();
- _toolbar->Bind (wxEVT_TOOL, bind (&TimelineDialog::tool_clicked, this, _1));
+ _toolbar->Bind(wxEVT_TOOL, bind(&ContentTimelineDialog::tool_clicked, this, _1));
sizer->Add (_toolbar, 0, wxALL, 12);
sizer->Add (&_timeline, 1, wxEXPAND | wxALL, 12);
@@ -96,17 +96,17 @@ TimelineDialog::TimelineDialog(ContentPanel* cp, shared_ptr<Film> film, FilmView
sizer->Layout ();
sizer->SetSizeHints (this);
- Bind(wxEVT_CHAR_HOOK, boost::bind(&TimelineDialog::keypress, this, _1));
+ Bind(wxEVT_CHAR_HOOK, boost::bind(&ContentTimelineDialog::keypress, this, _1));
- _toolbar->ToggleTool ((int) Timeline::SNAP, _timeline.snap ());
+ _toolbar->ToggleTool(static_cast<int>(ContentTimeline::SNAP), _timeline.snap ());
film_change(ChangeType::DONE, FilmProperty::SEQUENCE);
- _film_changed_connection = film->Change.connect (bind (&TimelineDialog::film_change, this, _1, _2));
+ _film_changed_connection = film->Change.connect(bind(&ContentTimelineDialog::film_change, this, _1, _2));
}
void
-TimelineDialog::film_change(ChangeType type, FilmProperty p)
+ContentTimelineDialog::film_change(ChangeType type, FilmProperty p)
{
if (type != ChangeType::DONE) {
return;
@@ -118,26 +118,26 @@ TimelineDialog::film_change(ChangeType type, FilmProperty p)
}
if (p == FilmProperty::SEQUENCE) {
- _toolbar->ToggleTool ((int) Timeline::SEQUENCE, film->sequence ());
+ _toolbar->ToggleTool(static_cast<int>(ContentTimeline::SEQUENCE), film->sequence());
}
}
void
-TimelineDialog::set_selection (ContentList selection)
+ContentTimelineDialog::set_selection(ContentList selection)
{
_timeline.set_selection (selection);
}
void
-TimelineDialog::tool_clicked (wxCommandEvent& ev)
+ContentTimelineDialog::tool_clicked(wxCommandEvent& ev)
{
- Timeline::Tool t = static_cast<Timeline::Tool>(ev.GetId());
+ auto t = static_cast<ContentTimeline::Tool>(ev.GetId());
_timeline.tool_clicked (t);
- if (t == Timeline::SNAP) {
+ if (t == ContentTimeline::SNAP) {
_timeline.set_snap (_toolbar->GetToolState(static_cast<int>(t)));
- } else if (t == Timeline::SEQUENCE) {
+ } else if (t == ContentTimeline::SEQUENCE) {
auto film = _film.lock ();
if (film) {
film->set_sequence (_toolbar->GetToolState(static_cast<int>(t)));
@@ -147,7 +147,7 @@ TimelineDialog::tool_clicked (wxCommandEvent& ev)
void
-TimelineDialog::keypress(wxKeyEvent const& event)
+ContentTimelineDialog::keypress(wxKeyEvent const& event)
{
_timeline.keypress(event);
}
diff --git a/src/wx/timeline_dialog.h b/src/wx/content_timeline_dialog.h
index 8134aa6db..2babf0437 100644
--- a/src/wx/timeline_dialog.h
+++ b/src/wx/content_timeline_dialog.h
@@ -19,7 +19,7 @@
*/
-#include "timeline.h"
+#include "content_timeline.h"
#include <dcp/warnings.h>
LIBDCP_DISABLE_WARNINGS
#include <wx/wx.h>
@@ -29,10 +29,10 @@ LIBDCP_ENABLE_WARNINGS
class Playlist;
-class TimelineDialog : public wxDialog
+class ContentTimelineDialog : public wxDialog
{
public:
- TimelineDialog(ContentPanel *, std::shared_ptr<Film>, FilmViewer& viewer);
+ ContentTimelineDialog(ContentPanel *, std::shared_ptr<Film>, FilmViewer& viewer);
void set_selection (ContentList selection);
@@ -42,7 +42,7 @@ private:
void keypress(wxKeyEvent const& event);
std::weak_ptr<Film> _film;
- Timeline _timeline;
+ ContentTimeline _timeline;
wxToolBar* _toolbar;
boost::signals2::scoped_connection _film_changed_connection;
};
diff --git a/src/wx/timeline_text_content_view.cc b/src/wx/content_timeline_text_view.cc
index a57398599..f22f47850 100644
--- a/src/wx/timeline_text_content_view.cc
+++ b/src/wx/content_timeline_text_view.cc
@@ -19,7 +19,8 @@
*/
-#include "timeline_text_content_view.h"
+#include "colours.h"
+#include "content_timeline_text_view.h"
#include "lib/text_content.h"
#include "lib/content.h"
@@ -27,7 +28,7 @@
using std::shared_ptr;
-TimelineTextContentView::TimelineTextContentView (Timeline& tl, shared_ptr<Content> c, shared_ptr<TextContent> caption)
+ContentTimelineTextView::ContentTimelineTextView(ContentTimeline& tl, shared_ptr<Content> c, shared_ptr<TextContent> caption)
: TimelineContentView (tl, c)
, _caption (caption)
{
@@ -35,17 +36,17 @@ TimelineTextContentView::TimelineTextContentView (Timeline& tl, shared_ptr<Conte
}
wxColour
-TimelineTextContentView::background_colour () const
+ContentTimelineTextView::background_colour() const
{
if (!active ()) {
return wxColour (210, 210, 210, 128);
}
- return wxColour (163, 255, 154, 255);
+ return TEXT_CONTENT_COLOUR;
}
wxColour
-TimelineTextContentView::foreground_colour () const
+ContentTimelineTextView::foreground_colour() const
{
if (!active ()) {
return wxColour (180, 180, 180, 128);
@@ -55,7 +56,7 @@ TimelineTextContentView::foreground_colour () const
}
bool
-TimelineTextContentView::active () const
+ContentTimelineTextView::active() const
{
return _caption->use();
}
diff --git a/src/wx/timeline_text_content_view.h b/src/wx/content_timeline_text_view.h
index 046f5b3e6..8d37ad489 100644
--- a/src/wx/timeline_text_content_view.h
+++ b/src/wx/content_timeline_text_view.h
@@ -18,18 +18,20 @@
*/
+
#include "timeline_content_view.h"
+
class TextContent;
-class TextContent;
-/** @class TimelineTextContentView
- * @brief Timeline view for TextContent.
+
+/** @class ContentTimelineTextView
+ * @brief Content timeline view for TextContent.
*/
-class TimelineTextContentView : public TimelineContentView
+class ContentTimelineTextView : public TimelineContentView
{
public:
- TimelineTextContentView (Timeline& tl, std::shared_ptr<Content>, std::shared_ptr<TextContent>);
+ ContentTimelineTextView(ContentTimeline& tl, std::shared_ptr<Content>, std::shared_ptr<TextContent>);
private:
bool active () const override;
diff --git a/src/wx/timeline_video_content_view.cc b/src/wx/content_timeline_video_view.cc
index b0f4b4f5d..d907627cd 100644
--- a/src/wx/timeline_video_content_view.cc
+++ b/src/wx/content_timeline_video_view.cc
@@ -18,31 +18,35 @@
*/
+
+#include "colours.h"
+#include "content_timeline_video_view.h"
#include "lib/image_content.h"
#include "lib/video_content.h"
-#include "timeline_video_content_view.h"
+
using std::dynamic_pointer_cast;
using std::shared_ptr;
-TimelineVideoContentView::TimelineVideoContentView (Timeline& tl, shared_ptr<Content> c)
+
+ContentTimelineVideoView::ContentTimelineVideoView(ContentTimeline& tl, shared_ptr<Content> c)
: TimelineContentView (tl, c)
{
}
wxColour
-TimelineVideoContentView::background_colour () const
+ContentTimelineVideoView::background_colour() const
{
if (!active()) {
return wxColour (210, 210, 210, 128);
}
- return wxColour (242, 92, 120, 255);
+ return VIDEO_CONTENT_COLOUR;
}
wxColour
-TimelineVideoContentView::foreground_colour () const
+ContentTimelineVideoView::foreground_colour() const
{
if (!active()) {
return wxColour (180, 180, 180, 128);
@@ -52,9 +56,9 @@ TimelineVideoContentView::foreground_colour () const
}
bool
-TimelineVideoContentView::active () const
+ContentTimelineVideoView::active() const
{
- shared_ptr<Content> c = _content.lock ();
+ auto c = _content.lock();
DCPOMATIC_ASSERT (c);
return c->video && c->video->use();
}
diff --git a/src/wx/timeline_video_content_view.h b/src/wx/content_timeline_video_view.h
index fa8ddf54c..940a1a305 100644
--- a/src/wx/timeline_video_content_view.h
+++ b/src/wx/content_timeline_video_view.h
@@ -22,13 +22,13 @@
#include "timeline_content_view.h"
-/** @class TimelineVideoContentView
- * @brief Timeline view for VideoContent.
+/** @class ContentTimelineVideoView
+ * @brief Content timeline view for VideoContent.
*/
-class TimelineVideoContentView : public TimelineContentView
+class ContentTimelineVideoView : public TimelineContentView
{
public:
- TimelineVideoContentView (Timeline& tl, std::shared_ptr<Content> c);
+ ContentTimelineVideoView(ContentTimeline& tl, std::shared_ptr<Content> c);
private:
bool active () const override;
diff --git a/src/wx/content_timeline_view.cc b/src/wx/content_timeline_view.cc
new file mode 100644
index 000000000..127e440fa
--- /dev/null
+++ b/src/wx/content_timeline_view.cc
@@ -0,0 +1,42 @@
+/*
+ Copyright (C) 2013-2021 Carl Hetherington <cth@carlh.net>
+
+ This file is part of DCP-o-matic.
+
+ DCP-o-matic is free software; you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation; either version 2 of the License, or
+ (at your option) any later version.
+
+ DCP-o-matic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with DCP-o-matic. If not, see <http://www.gnu.org/licenses/>.
+
+*/
+
+
+#include "content_timeline.h"
+#include "content_timeline_view.h"
+
+
+using std::list;
+using namespace dcpomatic;
+
+
+ContentTimelineView::ContentTimelineView(ContentTimeline& timeline)
+ : TimelineView(timeline)
+{
+
+}
+
+int
+ContentTimelineView::y_pos(int t) const
+{
+ return t * _timeline.pixels_per_track() + _timeline.tracks_y_offset();
+}
+
+
diff --git a/src/wx/content_timeline_view.h b/src/wx/content_timeline_view.h
new file mode 100644
index 000000000..450d19df4
--- /dev/null
+++ b/src/wx/content_timeline_view.h
@@ -0,0 +1,60 @@
+/*
+ Copyright (C) 2013-2021 Carl Hetherington <cth@carlh.net>
+
+ This file is part of DCP-o-matic.
+
+ DCP-o-matic is free software; you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation; either version 2 of the License, or
+ (at your option) any later version.
+
+ DCP-o-matic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with DCP-o-matic. If not, see <http://www.gnu.org/licenses/>.
+
+*/
+
+
+#ifndef DCPOMATIC_CONTENT_TIMELINE_VIEW_H
+#define DCPOMATIC_CONTENT_TIMELINE_VIEW_H
+
+
+#include "timeline_view.h"
+#include "lib/rect.h"
+#include "lib/dcpomatic_time.h"
+
+
+class wxGraphicsContext;
+class ContentTimeline;
+
+
+/** @class ContentTimelineView
+ * @brief Parent class for components of the content timeline (e.g. a piece of content or an axis).
+ */
+class ContentTimelineView : public TimelineView<ContentTimeline>
+{
+public:
+ explicit ContentTimelineView(ContentTimeline& t);
+ virtual ~ContentTimelineView () = default;
+
+ void paint(wxGraphicsContext* gc, std::list<dcpomatic::Rect<int>> overlaps)
+ {
+ _last_paint_bbox = bbox();
+ do_paint(gc, overlaps);
+ }
+
+protected:
+ virtual void do_paint(wxGraphicsContext *, std::list<dcpomatic::Rect<int>> overlaps) = 0;
+
+ int y_pos(int t) const;
+};
+
+
+typedef std::vector<std::shared_ptr<ContentTimelineView>> ContentTimelineViewList;
+
+
+#endif
diff --git a/src/wx/content_view.cc b/src/wx/content_view.cc
index b9fe7ce00..b2d6f86d1 100644
--- a/src/wx/content_view.cc
+++ b/src/wx/content_view.cc
@@ -21,6 +21,7 @@
#include "content_view.h"
#include "wx_util.h"
+#include "wx_variant.h"
#include "lib/config.h"
#include "lib/content_factory.h"
#include "lib/cross.h"
@@ -86,7 +87,7 @@ ContentView::update ()
dir = home_directory ();
}
- wxProgressDialog progress (_("DCP-o-matic"), _("Reading content directory"));
+ wxProgressDialog progress(variant::wx::dcpomatic(), _("Reading content directory"));
auto jm = JobManager::instance ();
list<shared_ptr<ExamineContentJob>> jobs;
diff --git a/src/wx/dcp_panel.cc b/src/wx/dcp_panel.cc
index f4ba74cde..4d2ee12b9 100644
--- a/src/wx/dcp_panel.cc
+++ b/src/wx/dcp_panel.cc
@@ -23,6 +23,7 @@
#include "check_box.h"
#include "check_box.h"
#include "dcp_panel.h"
+#include "dcp_timeline_dialog.h"
#include "dcpomatic_button.h"
#include "dcpomatic_choice.h"
#include "dcpomatic_spin_ctrl.h"
@@ -97,10 +98,6 @@ DCPPanel::DCPPanel(wxNotebook* n, shared_ptr<Film> film, FilmViewer& viewer)
wxALIGN_CENTRE_HORIZONTAL | wxST_NO_AUTORESIZE | wxST_ELLIPSIZE_MIDDLE
);
- _enable_audio_language = new CheckBox(_panel, _("Audio language"));
- _audio_language = new wxStaticText (_panel, wxID_ANY, wxT(""));
- _edit_audio_language = new Button (_panel, _("Edit..."));
-
_dcp_content_type_label = create_label (_panel, _("Content Type"), true);
_dcp_content_type = new Choice(_panel);
@@ -110,18 +107,12 @@ DCPPanel::DCPPanel(wxNotebook* n, shared_ptr<Film> film, FilmViewer& viewer)
auto size = dc.GetTextExtent (wxT ("GGGGGGGG..."));
size.SetHeight (-1);
- _reels_label = create_label (_panel, _("Reels"), true);
- _reel_type = new Choice(_panel);
-
- _reel_length_label = create_label (_panel, _("Reel length"), true);
- _reel_length = new SpinCtrl (_panel, DCPOMATIC_SPIN_CTRL_WIDTH);
- _reel_length_gb_label = create_label (_panel, _("GB"), false);
-
_standard_label = create_label (_panel, _("Standard"), true);
_standard = new Choice(_panel);
_markers = new Button (_panel, _("Markers..."));
_metadata = new Button (_panel, _("Metadata..."));
+ _reels = new Button(_panel, _("Reels..."));
_notebook = new wxNotebook (_panel, wxID_ANY);
_sizer->Add (_notebook, 1, wxEXPAND | wxTOP, 6);
@@ -134,28 +125,17 @@ DCPPanel::DCPPanel(wxNotebook* n, shared_ptr<Film> film, FilmViewer& viewer)
_copy_isdcf_name_button->Bind(wxEVT_BUTTON, boost::bind(&DCPPanel::copy_isdcf_name_button_clicked, this));
_dcp_content_type->Bind (wxEVT_CHOICE, boost::bind(&DCPPanel::dcp_content_type_changed, this));
_encrypted->bind(&DCPPanel::encrypted_toggled, this);
- _reel_type->Bind (wxEVT_CHOICE, boost::bind(&DCPPanel::reel_type_changed, this));
- _reel_length->Bind (wxEVT_SPINCTRL, boost::bind(&DCPPanel::reel_length_changed, this));
_standard->Bind (wxEVT_CHOICE, boost::bind(&DCPPanel::standard_changed, this));
_markers->Bind (wxEVT_BUTTON, boost::bind(&DCPPanel::markers_clicked, this));
_metadata->Bind (wxEVT_BUTTON, boost::bind(&DCPPanel::metadata_clicked, this));
- _enable_audio_language->bind(&DCPPanel::enable_audio_language_toggled, this);
- _edit_audio_language->Bind (wxEVT_BUTTON, boost::bind(&DCPPanel::edit_audio_language_clicked, this));
+ _reels->Bind(wxEVT_BUTTON, boost::bind(&DCPPanel::reels_clicked, this));
for (auto i: DCPContentType::all()) {
- _dcp_content_type->add(i->pretty_name());
+ _dcp_content_type->add_entry(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));
@@ -166,11 +146,12 @@ DCPPanel::DCPPanel(wxNotebook* n, shared_ptr<Film> film, FilmViewer& viewer)
void
DCPPanel::add_standards()
{
- _standard->add(_("SMPTE"), N_("smpte"));
+ _standard->add_entry(_("SMPTE"), N_("smpte"));
if (Config::instance()->allow_smpte_bv20() || (_film && _film->limit_to_smpte_bv20())) {
- _standard->add(_("SMPTE (Bv2.0 only)"), N_("smpte-bv20"));
+ _standard->add_entry(_("SMPTE (Bv2.0 only)"), N_("smpte-bv20"));
}
- _standard->add(_("Interop"), N_("interop"));
+ _standard->add_entry(_("Interop"), N_("interop"));
+ _standard->add_entry(_("MPEG2 Interop"), N_("mpeg2-interop"));
_sizer->Layout();
}
@@ -182,7 +163,11 @@ DCPPanel::set_standard()
DCPOMATIC_ASSERT(!_film->limit_to_smpte_bv20() || _standard->GetCount() == 3);
if (_film->interop()) {
- checked_set(_standard, "interop");
+ if (_film->video_encoding() == VideoEncoding::JPEG2000) {
+ checked_set(_standard, "interop");
+ } else {
+ checked_set(_standard, "mpeg2-interop");
+ }
} else {
checked_set(_standard, _film->limit_to_smpte_bv20() ? "smpte-bv20" : "smpte");
}
@@ -204,12 +189,18 @@ DCPPanel::standard_changed ()
if (*data == N_("interop")) {
_film->set_interop(true);
_film->set_limit_to_smpte_bv20(false);
+ _film->set_video_encoding(VideoEncoding::JPEG2000);
} else if (*data == N_("smpte")) {
_film->set_interop(false);
_film->set_limit_to_smpte_bv20(false);
+ _film->set_video_encoding(VideoEncoding::JPEG2000);
} else if (*data == N_("smpte-bv20")) {
_film->set_interop(false);
_film->set_limit_to_smpte_bv20(true);
+ _film->set_video_encoding(VideoEncoding::JPEG2000);
+ } else if (*data == N_("mpeg2-interop")) {
+ _film->set_interop(true);
+ _film->set_video_encoding(VideoEncoding::MPEG2);
}
}
@@ -241,15 +232,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 +239,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 +246,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;
}
@@ -294,13 +264,13 @@ DCPPanel::name_changed ()
void
-DCPPanel::j2k_bandwidth_changed ()
+DCPPanel::video_bit_rate_changed()
{
if (!_film) {
return;
}
- _film->set_j2k_bandwidth (_j2k_bandwidth->GetValue() * 1000000);
+ _film->set_video_bit_rate(_film->video_encoding(), _video_bit_rate->GetValue() * 1000000);
}
@@ -390,6 +360,14 @@ DCPPanel::metadata_clicked ()
void
+DCPPanel::reels_clicked()
+{
+ _dcp_timeline.reset(_panel, _film);
+ _dcp_timeline->Show();
+}
+
+
+void
DCPPanel::film_changed(FilmProperty p)
{
switch (p) {
@@ -418,8 +396,8 @@ DCPPanel::film_changed(FilmProperty p)
setup_container ();
setup_dcp_name ();
break;
- case FilmProperty::J2K_BANDWIDTH:
- checked_set (_j2k_bandwidth, _film->j2k_bandwidth() / 1000000);
+ case FilmProperty::VIDEO_BIT_RATE:
+ checked_set(_video_bit_rate, _film->video_bit_rate(_film->video_encoding()) / 1000000);
break;
case FilmProperty::USE_ISDCF_NAME:
{
@@ -429,10 +407,7 @@ DCPPanel::film_changed(FilmProperty p)
in case the user has clicked 'Copy as name' then re-ticked 'Use ISDCF name' (#1513).
*/
string const name = _film->name ();
- string::size_type const u = name.find("_");
- if (u != string::npos) {
- _film->set_name (name.substr(0, u));
- }
+ _film->set_name(name.substr(0, name.find("_")));
}
setup_dcp_name ();
break;
@@ -478,6 +453,12 @@ DCPPanel::film_changed(FilmProperty p)
setup_dcp_name ();
_markers->Enable (!_film->interop());
break;
+ case FilmProperty::VIDEO_ENCODING:
+ set_standard();
+ setup_container();
+ setup_sensitivity();
+ film_changed(FilmProperty::VIDEO_BIT_RATE);
+ break;
case FilmProperty::LIMIT_TO_SMPTE_BV20:
set_standard();
break;
@@ -490,13 +471,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 +485,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:
@@ -565,24 +540,28 @@ DCPPanel::film_content_changed (int property)
void
DCPPanel::setup_container ()
{
- int n = 0;
- auto ratios = Ratio::containers ();
- auto i = ratios.begin ();
- while (i != ratios.end() && *i != _film->container()) {
- ++i;
- ++n;
+ auto ratios = Ratio::containers();
+ if (std::find(ratios.begin(), ratios.end(), _film->container()) == ratios.end()) {
+ ratios.push_back(_film->container());
}
- if (i == ratios.end()) {
- checked_set (_container, -1);
- checked_set (_container_size, wxT(""));
- } else {
- checked_set (_container, n);
- auto const size = fit_ratio_within (_film->container()->ratio(), _film->full_frame ());
- checked_set (_container_size, wxString::Format("%dx%d", size.width, size.height));
+ wxArrayString new_ratios;
+ for (auto ratio: ratios) {
+ new_ratios.Add(std_to_wx(ratio->container_nickname()));
}
+ _container->set_entries(new_ratios);
+
+ auto iter = std::find_if(ratios.begin(), ratios.end(), [this](Ratio const* ratio) { return ratio == _film->container(); });
+ DCPOMATIC_ASSERT(iter != ratios.end());
+
+ checked_set(_container, iter - ratios.begin());
+ auto const size = fit_ratio_within(_film->container()->ratio(), _film->full_frame ());
+ checked_set(_container_size, wxString::Format("%dx%d", size.width, size.height));
+
setup_dcp_name ();
+
+ _video_grid->Layout();
}
@@ -644,7 +623,7 @@ DCPPanel::set_film (shared_ptr<Film> film)
film_changed(FilmProperty::CONTAINER);
film_changed(FilmProperty::RESOLUTION);
film_changed(FilmProperty::ENCRYPTED);
- film_changed(FilmProperty::J2K_BANDWIDTH);
+ film_changed(FilmProperty::VIDEO_BIT_RATE);
film_changed(FilmProperty::VIDEO_FRAME_RATE);
film_changed(FilmProperty::AUDIO_CHANNELS);
film_changed(FilmProperty::SEQUENCE);
@@ -673,6 +652,8 @@ DCPPanel::set_general_sensitivity (bool s)
void
DCPPanel::setup_sensitivity ()
{
+ auto const mpeg2 = _film && _film->video_encoding() == VideoEncoding::MPEG2;
+
_name->Enable (_generally_sensitive);
_use_isdcf_name->Enable (_generally_sensitive);
_dcp_content_type->Enable (_generally_sensitive);
@@ -681,16 +662,15 @@ 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);
+ _reels->Enable (_generally_sensitive && _film);
_frame_rate_choice->Enable (_generally_sensitive && _film && !_film->references_dcp_video() && !_film->contains_atmos_content());
_frame_rate_spin->Enable (_generally_sensitive && _film && !_film->references_dcp_video() && !_film->contains_atmos_content());
_audio_channels->Enable (_generally_sensitive && _film && !_film->references_dcp_audio());
_audio_processor->Enable (_generally_sensitive && _film && !_film->references_dcp_audio());
- _j2k_bandwidth->Enable (_generally_sensitive && _film && !_film->references_dcp_video());
- _container->Enable (_generally_sensitive && _film && !_film->references_dcp_video());
+ _video_bit_rate->Enable (_generally_sensitive && _film && !_film->references_dcp_video());
+ _container->Enable (_generally_sensitive && _film && !_film->references_dcp_video() && !mpeg2);
_best_frame_rate->Enable (
_generally_sensitive &&
_film &&
@@ -698,8 +678,8 @@ DCPPanel::setup_sensitivity ()
!_film->references_dcp_video() &&
!_film->contains_atmos_content()
);
- _resolution->Enable (_generally_sensitive && _film && !_film->references_dcp_video());
- _three_d->Enable (_generally_sensitive && _film && !_film->references_dcp_video());
+ _resolution->Enable (_generally_sensitive && _film && !_film->references_dcp_video() && !mpeg2);
+ _three_d->Enable (_generally_sensitive && _film && !_film->references_dcp_video() && !mpeg2);
_standard->Enable (
_generally_sensitive &&
@@ -772,7 +752,8 @@ DCPPanel::reencode_j2k_changed ()
void
DCPPanel::config_changed (Config::Property p)
{
- _j2k_bandwidth->SetRange (1, Config::instance()->maximum_j2k_bandwidth() / 1000000);
+ VideoEncoding const encoding = _film ? _film->video_encoding() : VideoEncoding::JPEG2000;
+ _video_bit_rate->SetRange(1, Config::instance()->maximum_video_bit_rate(encoding) / 1000000);
setup_frame_rate_widget ();
if (p == Config::SHOW_EXPERIMENTAL_AUDIO_PROCESSORS) {
@@ -790,6 +771,8 @@ DCPPanel::config_changed (Config::Property p)
}
} else if (p == Config::ISDCF_NAME_PART_LENGTH) {
setup_dcp_name();
+ } else if (p == Config::ALLOW_ANY_CONTAINER) {
+ setup_container();
}
}
@@ -831,8 +814,8 @@ DCPPanel::make_video_panel ()
_three_d = new CheckBox (panel, _("3D"));
- _j2k_bandwidth_label = create_label (panel, _("JPEG2000 bandwidth\nfor newly-encoded data"), true);
- _j2k_bandwidth = new SpinCtrl (panel, DCPOMATIC_SPIN_CTRL_WIDTH);
+ _video_bit_rate_label = create_label(panel, _("Video bit rate\nfor newly-encoded data"), true);
+ _video_bit_rate = new SpinCtrl(panel, DCPOMATIC_SPIN_CTRL_WIDTH);
_mbits_label = create_label (panel, _("Mbit/s"), false);
_reencode_j2k = new CheckBox (panel, _("Re-encode JPEG2000 data from input"));
@@ -841,26 +824,23 @@ DCPPanel::make_video_panel ()
_frame_rate_choice->Bind (wxEVT_CHOICE, boost::bind(&DCPPanel::frame_rate_choice_changed, this));
_frame_rate_spin->Bind (wxEVT_SPINCTRL, boost::bind(&DCPPanel::frame_rate_spin_changed, this));
_best_frame_rate->Bind (wxEVT_BUTTON, boost::bind(&DCPPanel::best_frame_rate_clicked, this));
- _j2k_bandwidth->Bind (wxEVT_SPINCTRL, boost::bind(&DCPPanel::j2k_bandwidth_changed, this));
+ _video_bit_rate->Bind (wxEVT_SPINCTRL, boost::bind(&DCPPanel::video_bit_rate_changed, this));
/* Also listen to wxEVT_TEXT so that typing numbers directly in is always noticed */
- _j2k_bandwidth->Bind (wxEVT_TEXT, boost::bind(&DCPPanel::j2k_bandwidth_changed, this));
+ _video_bit_rate->Bind (wxEVT_TEXT, boost::bind(&DCPPanel::video_bit_rate_changed, this));
_resolution->Bind (wxEVT_CHOICE, boost::bind(&DCPPanel::resolution_changed, this));
_three_d->bind(&DCPPanel::three_d_changed, this);
_reencode_j2k->bind(&DCPPanel::reencode_j2k_changed, this);
- for (auto i: Ratio::containers()) {
- _container->add(i->container_nickname());
- }
-
for (auto i: Config::instance()->allowed_dcp_frame_rates()) {
- _frame_rate_choice->add(boost::lexical_cast<string>(i));
+ _frame_rate_choice->add_entry(boost::lexical_cast<string>(i));
}
- _j2k_bandwidth->SetRange (1, Config::instance()->maximum_j2k_bandwidth() / 1000000);
+ VideoEncoding const encoding = _film ? _film->video_encoding() : VideoEncoding::JPEG2000;
+ _video_bit_rate->SetRange(1, Config::instance()->maximum_video_bit_rate(encoding) / 1000000);
_frame_rate_spin->SetRange (1, 480);
- _resolution->add(_("2K"));
- _resolution->add(_("4K"));
+ _resolution->add_entry(_("2K"));
+ _resolution->add_entry(_("4K"));
add_video_panel_to_grid ();
setup_frame_rate_widget();
@@ -900,9 +880,9 @@ DCPPanel::add_video_panel_to_grid ()
_video_grid->Add (_three_d, wxGBPosition (r, 0), wxGBSpan (1, 2));
++r;
- add_label_to_sizer (_video_grid, _j2k_bandwidth_label, true, wxGBPosition (r, 0));
+ add_label_to_sizer(_video_grid, _video_bit_rate_label, true, wxGBPosition (r, 0));
auto s = new wxBoxSizer (wxHORIZONTAL);
- s->Add (_j2k_bandwidth, 0, wxALIGN_CENTER_VERTICAL);
+ s->Add(_video_bit_rate, 0, wxALIGN_CENTER_VERTICAL);
add_label_to_sizer (s, _mbits_label, false, 0, wxLEFT | wxALIGN_CENTER_VERTICAL);
_video_grid->Add (s, wxGBPosition(r, 1), wxDefaultSpan);
++r;
@@ -953,6 +933,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 +944,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 +980,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;
}
@@ -1000,6 +998,13 @@ DCPPanel::add_audio_panel_to_grid ()
void
DCPPanel::copy_isdcf_name_button_clicked ()
{
+ auto name = _film->name();
+ if (name.length() > 20 && std::count(name.begin(), name.end(), '_') > 6) {
+ /* At a guess, the existing film name is itself an ISDCF name, so chop
+ * off the actual name part first.
+ */
+ _film->set_name(name.substr(0, name.find("_")));
+ }
_film->set_name (_film->isdcf_name (true));
_film->set_use_isdcf_name (false);
}
@@ -1030,33 +1035,11 @@ DCPPanel::show_audio_clicked ()
void
-DCPPanel::reel_type_changed ()
-{
- if (!_film || !_reel_type->get()) {
- return;
- }
-
- _film->set_reel_type(static_cast<ReelType>(*_reel_type->get()));
-}
-
-
-void
-DCPPanel::reel_length_changed ()
-{
- if (!_film) {
- return;
- }
-
- _film->set_reel_length (_reel_length->GetValue() * 1000000000LL);
-}
-
-
-void
DCPPanel::add_audio_processors ()
{
- _audio_processor->add(_("None"), new wxStringClientData(N_("none")));
+ _audio_processor->add_entry(_("None"), new wxStringClientData(N_("none")));
for (auto ap: AudioProcessor::visible()) {
- _audio_processor->add(std_to_wx(ap->name()), new wxStringClientData(std_to_wx(ap->id())));
+ _audio_processor->add_entry(std_to_wx(ap->name()), new wxStringClientData(std_to_wx(ap->id())));
}
_audio_panel_sizer->Layout();
}
diff --git a/src/wx/dcp_panel.h b/src/wx/dcp_panel.h
index 849fe185c..c686a9c55 100644
--- a/src/wx/dcp_panel.h
+++ b/src/wx/dcp_panel.h
@@ -39,6 +39,7 @@ class wxGridBagSizer;
class AudioDialog;
class Choice;
+class DCPTimelineDialog;
class Film;
class FilmViewer;
class InteropMetadataDialog;
@@ -70,7 +71,7 @@ private:
void copy_isdcf_name_button_clicked ();
void container_changed ();
void dcp_content_type_changed ();
- void j2k_bandwidth_changed ();
+ void video_bit_rate_changed();
void frame_rate_choice_changed ();
void frame_rate_spin_changed ();
void best_frame_rate_clicked ();
@@ -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 ();
@@ -129,9 +129,9 @@ private:
Choice* _container;
wxStaticText* _container_size;
wxButton* _copy_isdcf_name_button;
- wxStaticText* _j2k_bandwidth_label;
+ wxStaticText* _video_bit_rate_label;
wxStaticText* _mbits_label;
- wxSpinCtrl* _j2k_bandwidth;
+ wxSpinCtrl* _video_bit_rate;
wxStaticText* _dcp_content_type_label;
Choice* _dcp_content_type;
wxStaticText* _frame_rate_label;
@@ -153,19 +153,16 @@ private:
wxStaticText* _standard_label;
Choice* _standard;
CheckBox* _encrypted;
- wxStaticText* _reels_label;
- Choice* _reel_type;
- wxStaticText* _reel_length_label;
- wxStaticText* _reel_length_gb_label;
- wxSpinCtrl* _reel_length;
wxButton* _markers;
wxButton* _metadata;
+ Button* _reels;
wxSizer* _audio_panel_sizer;
wx_ptr<AudioDialog> _audio_dialog;
wx_ptr<MarkersDialog> _markers_dialog;
wx_ptr<InteropMetadataDialog> _interop_metadata_dialog;
wx_ptr<SMPTEMetadataDialog> _smpte_metadata_dialog;
+ wx_ptr<DCPTimelineDialog> _dcp_timeline;
std::shared_ptr<Film> _film;
FilmViewer& _viewer;
diff --git a/src/wx/dcp_referencing_dialog.cc b/src/wx/dcp_referencing_dialog.cc
new file mode 100644
index 000000000..853391bd3
--- /dev/null
+++ b/src/wx/dcp_referencing_dialog.cc
@@ -0,0 +1,231 @@
+/*
+ Copyright (C) 2023 Carl Hetherington <cth@carlh.net>
+
+ This file is part of DCP-o-matic.
+
+ DCP-o-matic is free software; you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation; either version 2 of the License, or
+ (at your option) any later version.
+
+ DCP-o-matic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with DCP-o-matic. If not, see <http://www.gnu.org/licenses/>.
+
+*/
+
+
+#include "check_box.h"
+#include "dcp_referencing_dialog.h"
+#include "static_text.h"
+#include "wx_util.h"
+#include "lib/dcp_content.h"
+#include "lib/film.h"
+#include "lib/types.h"
+#include <wx/gbsizer.h>
+#include <wx/wx.h>
+
+
+using std::dynamic_pointer_cast;
+using std::shared_ptr;
+using std::string;
+using std::vector;
+using std::weak_ptr;
+#if BOOST_VERSION >= 106100
+using namespace boost::placeholders;
+#endif
+
+
+DCPReferencingDialog::DCPReferencingDialog(wxWindow* parent, shared_ptr<const Film> film)
+ : wxDialog(parent, wxID_ANY, _("Version file (VF) setup"))
+ , _film(film)
+ , _dcp_grid(new wxGridBagSizer(DCPOMATIC_SIZER_X_GAP, DCPOMATIC_SIZER_Y_GAP))
+ , _overall_sizer(new wxBoxSizer(wxVERTICAL))
+{
+ _film_connection = film->Change.connect(boost::bind(&DCPReferencingDialog::film_changed, this, _1, _2));
+ _film_content_connection = film->ContentChange.connect(boost::bind(&DCPReferencingDialog::film_content_changed, this, _1, _3));
+
+ _overall_sizer->Add(_dcp_grid, 1, wxALL, DCPOMATIC_DIALOG_BORDER);
+ SetSizer(_overall_sizer);
+
+ auto buttons = CreateSeparatedButtonSizer(wxOK);
+ if (buttons) {
+ _overall_sizer->Add(buttons, wxSizerFlags().Expand().DoubleBorder());
+ }
+
+ setup();
+}
+
+
+void
+DCPReferencingDialog::film_changed(ChangeType type, FilmProperty property)
+{
+ if (type != ChangeType::DONE) {
+ return;
+ }
+
+ if (
+ property == FilmProperty::INTEROP ||
+ property == FilmProperty::RESOLUTION ||
+ property == FilmProperty::CONTAINER ||
+ property == FilmProperty::REEL_TYPE ||
+ property == FilmProperty::VIDEO_FRAME_RATE ||
+ property == FilmProperty::CONTENT) {
+ setup();
+ }
+}
+
+
+void
+DCPReferencingDialog::film_content_changed(ChangeType type, int property)
+{
+ if (type != ChangeType::DONE) {
+ return;
+ }
+
+ if (
+ property != DCPContentProperty::REFERENCE_VIDEO &&
+ property != DCPContentProperty::REFERENCE_AUDIO &&
+ property != DCPContentProperty::REFERENCE_TEXT) {
+ setup();
+ }
+}
+
+
+void
+DCPReferencingDialog::setup()
+{
+ _dcps.clear();
+ _dcp_grid->Clear(true);
+
+ int column = 0;
+ for (auto const& heading: { _("DCP"), _("Picture"), _("Sound"), _("Subtitles"), _("Closed captions") }) {
+ auto text = new StaticText(this, heading);
+ wxFont font(*wxNORMAL_FONT);
+ font.SetWeight(wxFONTWEIGHT_BOLD);
+ text->SetFont(font);
+ _dcp_grid->Add(text, wxGBPosition(0, column), wxDefaultSpan, wxALL, DCPOMATIC_SIZER_GAP);
+ ++column;
+ };
+
+ auto const all_parts = { Part::VIDEO, Part::AUDIO, Part::SUBTITLES, Part::CLOSED_CAPTIONS };
+
+ int row = 1;
+ for (auto& content: _film->content()) {
+ auto dcp_content = dynamic_pointer_cast<DCPContent>(content);
+ if (!dcp_content) {
+ continue;
+ }
+
+ DCP record;
+ record.content = dcp_content;
+ _dcp_grid->Add(new StaticText(this, std_to_wx(dcp_content->name())), wxGBPosition(row, 0));
+ column = 1;
+ for (auto const& part: all_parts) {
+ record.check_box[part] = new CheckBox(this, wxT(""));
+ switch (part) {
+ case Part::VIDEO:
+ record.check_box[part]->set(dcp_content->reference_video());
+ break;
+ case Part::AUDIO:
+ record.check_box[part]->set(dcp_content->reference_audio());
+ break;
+ case Part::SUBTITLES:
+ record.check_box[part]->set(dcp_content->reference_text(TextType::OPEN_SUBTITLE));
+ break;
+ case Part::CLOSED_CAPTIONS:
+ record.check_box[part]->set(dcp_content->reference_text(TextType::CLOSED_CAPTION));
+ break;
+ default:
+ DCPOMATIC_ASSERT(false);
+ }
+ record.check_box[part]->bind(&DCPReferencingDialog::checkbox_changed, this, weak_ptr<DCPContent>(dcp_content), record.check_box[part], part);
+ _dcp_grid->Add(record.check_box[part], wxGBPosition(row, column++), wxDefaultSpan, wxALIGN_CENTER);
+ }
+ ++row;
+
+ auto add_problem = [this, &row](wxString const& cannot, string why_not) {
+ auto reason = new StaticText(this, cannot + wxT(": ") + std_to_wx(why_not));
+ wxFont font(*wxNORMAL_FONT);
+ font.SetStyle(wxFONTSTYLE_ITALIC);
+ reason->SetFont(font);
+ _dcp_grid->Add(reason, wxGBPosition(row, 0), wxGBSpan(1, 5), wxLEFT, DCPOMATIC_SIZER_X_GAP * 4);
+ ++row;
+ };
+
+ string why_not;
+
+ if (!dcp_content->can_reference_anything(_film, why_not)) {
+ for (auto const& part: all_parts) {
+ record.check_box[part]->Enable(false);
+ }
+ add_problem(_("Cannot reference this DCP"), why_not);
+ } else {
+ if (!dcp_content->can_reference_video(_film, why_not)) {
+ record.check_box[Part::VIDEO]->Enable(false);
+ if (dcp_content->video) {
+ add_problem(_("Cannot reference this DCP's video"), why_not);
+ }
+ }
+
+ if (!dcp_content->can_reference_audio(_film, why_not)) {
+ record.check_box[Part::AUDIO]->Enable(false);
+ if (dcp_content->audio) {
+ add_problem(_("Cannot reference this DCP's audio"), why_not);
+ }
+ }
+
+ if (!dcp_content->can_reference_text(_film, TextType::OPEN_SUBTITLE, why_not)) {
+ record.check_box[Part::SUBTITLES]->Enable(false);
+ if (dcp_content->text_of_original_type(TextType::OPEN_SUBTITLE)) {
+ add_problem(_("Cannot reference this DCP's subtitles"), why_not);
+ }
+ }
+
+ if (!dcp_content->can_reference_text(_film, TextType::CLOSED_CAPTION, why_not)) {
+ record.check_box[Part::CLOSED_CAPTIONS]->Enable(false);
+ if (dcp_content->text_of_original_type(TextType::CLOSED_CAPTION)) {
+ add_problem(_("Cannot reference this DCP's closed captions"), why_not);
+ }
+ }
+ }
+
+ _dcps.push_back(record);
+ }
+
+ _dcp_grid->Layout();
+ _overall_sizer->Layout();
+ _overall_sizer->SetSizeHints(this);
+}
+
+
+void
+DCPReferencingDialog::checkbox_changed(weak_ptr<DCPContent> weak_content, CheckBox* check_box, Part part)
+{
+ auto content = weak_content.lock();
+ if (!content) {
+ return;
+ }
+
+ switch (part) {
+ case Part::VIDEO:
+ content->set_reference_video(check_box->get());
+ break;
+ case Part::AUDIO:
+ content->set_reference_audio(check_box->get());
+ break;
+ case Part::SUBTITLES:
+ content->set_reference_text(TextType::OPEN_SUBTITLE, check_box->get());
+ break;
+ case Part::CLOSED_CAPTIONS:
+ content->set_reference_text(TextType::CLOSED_CAPTION, check_box->get());
+ break;
+ default:
+ DCPOMATIC_ASSERT(false);
+ }
+}
+
diff --git a/src/wx/dcp_referencing_dialog.h b/src/wx/dcp_referencing_dialog.h
new file mode 100644
index 000000000..e49292c22
--- /dev/null
+++ b/src/wx/dcp_referencing_dialog.h
@@ -0,0 +1,70 @@
+/*
+ Copyright (C) 2023 Carl Hetherington <cth@carlh.net>
+
+ This file is part of DCP-o-matic.
+
+ DCP-o-matic is free software; you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation; either version 2 of the License, or
+ (at your option) any later version.
+
+ DCP-o-matic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with DCP-o-matic. If not, see <http://www.gnu.org/licenses/>.
+
+*/
+
+
+#include "lib/enum_indexed_vector.h"
+#include "lib/film.h"
+#include <wx/wx.h>
+#include <memory>
+#include <vector>
+
+
+class CheckBox;
+class DCPContent;
+class Film;
+class wxBoxSizer;
+class wxGridBagSizer;
+
+
+class DCPReferencingDialog : public wxDialog
+{
+public:
+ DCPReferencingDialog(wxWindow* parent, std::shared_ptr<const Film> film);
+
+private:
+ enum class Part {
+ VIDEO,
+ AUDIO,
+ SUBTITLES,
+ CLOSED_CAPTIONS,
+ COUNT
+ };
+
+ void setup();
+ void checkbox_changed(std::weak_ptr<DCPContent> content, CheckBox* check_box, Part part);
+ void film_changed(ChangeType type, FilmProperty property);
+ void film_content_changed(ChangeType type, int property);
+
+ std::shared_ptr<const Film> _film;
+
+ wxGridBagSizer* _dcp_grid;
+ wxBoxSizer* _overall_sizer;
+
+ struct DCP {
+ std::shared_ptr<DCPContent> content;
+ EnumIndexedVector<CheckBox*, Part> check_box;
+ };
+
+ std::vector<DCP> _dcps;
+
+ boost::signals2::scoped_connection _film_connection;
+ boost::signals2::scoped_connection _film_content_connection;
+};
+
diff --git a/src/wx/dcp_timeline.cc b/src/wx/dcp_timeline.cc
new file mode 100644
index 000000000..93e15f6cc
--- /dev/null
+++ b/src/wx/dcp_timeline.cc
@@ -0,0 +1,617 @@
+/*
+ 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 "id.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 = DCPOMATIC_DCP_TIMELINE_MENU
+};
+
+
+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);
+ _add_reel_boundary->Enable(film()->reel_type() == ReelType::CUSTOM);
+}
+
+
+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_entry(_("Single reel"));
+ _reel_type->add_entry(_("Split by video content"));
+ _reel_type->add_entry(_("Split by maximum reel size"));
+ _reel_type->add_entry(_("Custom"));
+ sizer->Add(_reel_type, wxGBPosition(r, 1));
+ ++r;
+
+ add_label_to_sizer(sizer, _reel_settings, _("Maximum reel size"), true, wxGBPosition(r, 0));
+ _maximum_reel_size = new SpinCtrl(_reel_settings, DCPOMATIC_SPIN_CTRL_WIDTH);
+ _maximum_reel_size->SetRange(1, 1000);
+ {
+ auto s = new wxBoxSizer(wxHORIZONTAL);
+ s->Add(_maximum_reel_size, 0);
+ add_label_to_sizer(s, _reel_settings, _("GB"), false, 0, wxALIGN_CENTER_VERTICAL | wxLEFT);
+ sizer->Add(s, wxGBPosition(r, 1));
+ }
+ ++r;
+
+ _snap = new CheckBox(_reel_settings, _("Snap when dragging"));
+ sizer->Add(_snap, wxGBPosition(r, 1));
+ ++r;
+
+ _reel_type->set(static_cast<int>(film()->reel_type()));
+ _maximum_reel_size->SetValue(film()->reel_length() / 1000000000LL);
+
+ _reel_type->bind(&DCPTimeline::reel_mode_changed, this);
+ _maximum_reel_size->Bind(wxEVT_SPINCTRL, boost::bind(&DCPTimeline::maximum_reel_size_changed, this));
+}
+
+
+void
+DCPTimeline::reel_mode_changed()
+{
+ film()->set_reel_type(static_cast<ReelType>(*_reel_type->get()));
+}
+
+
+void
+DCPTimeline::maximum_reel_size_changed()
+{
+ film()->set_reel_length(_maximum_reel_size->GetValue() * 1000000000LL);
+}
+
+
+void
+DCPTimeline::set_reel_boundary(int index, DCPTime time)
+{
+ auto boundaries = film()->custom_reel_boundaries();
+ DCPOMATIC_ASSERT(index >= 0 && index < static_cast<int>(boundaries.size()));
+ boundaries[index] = time.round(film()->video_frame_rate());
+ film()->set_custom_reel_boundaries(boundaries);
+}
+
+
+void
+DCPTimeline::setup_reel_boundaries()
+{
+ auto const reels = film()->reels();
+ if (reels.empty()) {
+ _reel_boundaries.clear();
+ return;
+ }
+
+ size_t const boundaries = reels.size() - 1;
+ auto const maximum = film()->length();
+ for (size_t i = _reel_boundaries.size(); i < boundaries; ++i) {
+ auto boundary = std::make_shared<ReelBoundary>(
+ _reel_detail, _reel_detail_sizer, i, maximum, film()->video_frame_rate(), *this, editable()
+ );
+
+ boundary->Changed.connect(boost::bind(&DCPTimeline::set_reel_boundary, this, _1, _2));
+ _reel_boundaries.push_back(boundary);
+ }
+
+ _reel_boundaries.resize(boundaries);
+
+ auto const active = editable();
+ for (size_t i = 0; i < boundaries; ++i) {
+ _reel_boundaries[i]->set_time(reels[i].to);
+ _reel_boundaries[i]->view().set_active(active);
+ }
+
+ _reel_detail_sizer->Layout();
+ _canvas->Refresh();
+}
+
+
+void
+DCPTimeline::paint()
+{
+ wxPaintDC dc(_canvas);
+ dc.Clear();
+
+ if (film()->content().empty()) {
+ return;
+ }
+
+ _canvas->DoPrepareDC(dc);
+
+ auto gc = wxGraphicsContext::Create(dc);
+ if (!gc) {
+ return;
+ }
+
+ dcp::ScopeGuard sg = [gc]() { delete gc; };
+
+ gc->SetAntialiasMode(wxANTIALIAS_DEFAULT);
+
+ paint_reels(gc);
+ paint_content(gc);
+}
+
+
+void
+DCPTimeline::paint_reels(wxGraphicsContext* gc)
+{
+ constexpr int x_offset = 2;
+
+ for (auto const& boundary: _reel_boundaries) {
+ boundary->view().paint(gc);
+ }
+
+ gc->SetFont(gc->CreateFont(*wxNORMAL_FONT, wxColour(0, 0, 0)));
+ gc->SetPen(*wxThePenList->FindOrCreatePen(wxColour(0, 0, 0), 2, wxPENSTYLE_SOLID));
+
+ auto const pps = pixels_per_second().get_value_or(1);
+
+ auto start = gc->CreatePath();
+ start.MoveToPoint(x_offset, reel_marker_y_pos);
+ start.AddLineToPoint(x_offset, reel_marker_y_pos + DCPTimelineReelMarkerView::HEIGHT);
+ gc->StrokePath(start);
+
+ auto const length = film()->length().seconds() * pps;
+ auto end = gc->CreatePath();
+ end.MoveToPoint(x_offset + length, reel_marker_y_pos);
+ end.AddLineToPoint(x_offset + length, reel_marker_y_pos + DCPTimelineReelMarkerView::HEIGHT);
+ gc->StrokePath(end);
+
+ auto const y = reel_marker_y_pos + DCPTimelineReelMarkerView::HEIGHT * 3 / 4;
+
+ auto paint_reel = [gc](double from, double to, int index) {
+ auto path = gc->CreatePath();
+ path.MoveToPoint(from, y);
+ path.AddLineToPoint(to, y);
+ gc->StrokePath(path);
+
+ auto str = wxString::Format(wxT("#%d"), index + 1);
+ wxDouble str_width;
+ wxDouble str_height;
+ wxDouble str_descent;
+ wxDouble str_leading;
+ gc->GetTextExtent(str, &str_width, &str_height, &str_descent, &str_leading);
+
+ if (str_width < (to - from)) {
+ gc->DrawText(str, (from + to - str_width) / 2, y - str_height - 2);
+ }
+ };
+
+ gc->SetPen(*wxThePenList->FindOrCreatePen(wxColour(0, 0, 255), 2, wxPENSTYLE_DOT));
+ int index = 0;
+ DCPTime last;
+ for (auto const& boundary: _reel_boundaries) {
+ paint_reel(last.seconds() * pps + 2, boundary->time().seconds() * pps, index++);
+ last = boundary->time();
+ }
+
+ paint_reel(last.seconds() * pps + 2, film()->length().seconds() * pps, index);
+}
+
+
+void
+DCPTimeline::paint_content(wxGraphicsContext* gc)
+{
+ auto const pps = pixels_per_second().get_value_or(1);
+ auto const film = this->film();
+
+ auto const& solid_pen = *wxThePenList->FindOrCreatePen(wxColour(0, 0, 0), 1, wxPENSTYLE_SOLID);
+ auto const& dotted_pen = *wxThePenList->FindOrCreatePen(wxColour(0, 0, 0), 1, wxPENSTYLE_DOT);
+
+ auto const& video_brush = *wxTheBrushList->FindOrCreateBrush(VIDEO_CONTENT_COLOUR, wxBRUSHSTYLE_SOLID);
+ auto const& audio_brush = *wxTheBrushList->FindOrCreateBrush(AUDIO_CONTENT_COLOUR, wxBRUSHSTYLE_SOLID);
+ auto const& text_brush = *wxTheBrushList->FindOrCreateBrush(TEXT_CONTENT_COLOUR, wxBRUSHSTYLE_SOLID);
+ auto const& atmos_brush = *wxTheBrushList->FindOrCreateBrush(ATMOS_CONTENT_COLOUR, wxBRUSHSTYLE_SOLID);
+
+ auto maybe_draw =
+ [gc, film, pps, solid_pen, dotted_pen]
+ (shared_ptr<Content> content, shared_ptr<ContentPart> part, wxBrush brush, int offset) {
+ if (part) {
+ auto const y = content_y_pos + offset * content_type_height + 1;
+ gc->SetPen(solid_pen);
+ gc->SetBrush(brush);
+ gc->DrawRectangle(
+ content->position().seconds() * pps,
+ y,
+ content->length_after_trim(film).seconds() * pps,
+ content_type_height - 2
+ );
+
+ gc->SetPen(dotted_pen);
+ for (auto split: content->reel_split_points(film)) {
+ if (split != content->position()) {
+ auto path = gc->CreatePath();
+ path.MoveToPoint(split.seconds() * pps, y);
+ path.AddLineToPoint(split.seconds() * pps, y + content_type_height - 2);
+ gc->StrokePath(path);
+ }
+ }
+ }
+ };
+
+ for (auto content: film->content()) {
+ maybe_draw(content, dynamic_pointer_cast<ContentPart>(content->video), video_brush, 0);
+ maybe_draw(content, dynamic_pointer_cast<ContentPart>(content->audio), audio_brush, 1);
+ for (auto text: content->text) {
+ maybe_draw(content, dynamic_pointer_cast<ContentPart>(text), text_brush, 2);
+ }
+ maybe_draw(content, dynamic_pointer_cast<ContentPart>(content->atmos), atmos_brush, 3);
+ }
+}
+
+
+void
+DCPTimeline::setup_pixels_per_second()
+{
+ set_pixels_per_second((_canvas->GetSize().GetWidth() - 4) / std::max(1.0, film()->length().seconds()));
+}
+
+
+shared_ptr<ReelBoundary>
+DCPTimeline::event_to_reel_boundary(wxMouseEvent& ev) const
+{
+ Position<int> const position(ev.GetX(), ev.GetY());
+ auto iter = std::find_if(_reel_boundaries.begin(), _reel_boundaries.end(), [position](shared_ptr<const ReelBoundary> boundary) {
+ return boundary->view().bbox().contains(position);
+ });
+
+ if (iter == _reel_boundaries.end()) {
+ return {};
+ }
+
+ return *iter;
+}
+
+
+void
+DCPTimeline::left_down(wxMouseEvent& ev)
+{
+ if (!editable()) {
+ return;
+ }
+
+ if (auto boundary = event_to_reel_boundary(ev)) {
+ auto const snap_distance = DCPTime::from_seconds((_canvas->GetSize().GetWidth() / _pixels_per_second.get_value_or(1)) / SNAP_SUBDIVISION);
+ _drag = DCPTimeline::Drag(
+ boundary,
+ _reel_boundaries,
+ film(),
+ static_cast<int>(ev.GetX() - boundary->time().seconds() * _pixels_per_second.get_value_or(0)),
+ _snap->get(),
+ snap_distance
+ );
+ } else {
+ _drag = boost::none;
+ }
+}
+
+
+void
+DCPTimeline::right_down(wxMouseEvent& ev)
+{
+ _right_down_position = ev.GetPosition();
+ _canvas->PopupMenu(_menu, _right_down_position);
+}
+
+
+void
+DCPTimeline::left_up(wxMouseEvent&)
+{
+ if (!_drag) {
+ return;
+ }
+
+ set_reel_boundary(_drag->reel_boundary->index(), _drag->time());
+ _drag = boost::none;
+}
+
+
+void
+DCPTimeline::mouse_moved(wxMouseEvent& ev)
+{
+ if (!_drag) {
+ return;
+ }
+
+ auto time = DCPTime::from_seconds((ev.GetPosition().x - _drag->offset) / _pixels_per_second.get_value_or(1));
+ time = std::max(_drag->previous ? _drag->previous->time() : DCPTime(), time);
+ time = std::min(_drag->next ? _drag->next->time() : film()->length(), time);
+ _drag->set_time(time);
+ _canvas->RefreshRect({0, reel_marker_y_pos - 2, _canvas->GetSize().GetWidth(), DCPTimelineReelMarkerView::HEIGHT + 4}, true);
+}
+
+
+void
+DCPTimeline::force_redraw(dcpomatic::Rect<int> const & r)
+{
+ _canvas->RefreshRect(wxRect(r.x, r.y, r.width, r.height), false);
+}
+
+
+shared_ptr<Film>
+DCPTimeline::film() const
+{
+ auto film = _film.lock();
+ DCPOMATIC_ASSERT(film);
+ return film;
+}
+
+
+bool
+DCPTimeline::editable() const
+{
+ return film()->reel_type() == ReelType::CUSTOM;
+}
+
+
+DCPTimeline::Drag::Drag(
+ shared_ptr<ReelBoundary> reel_boundary_,
+ vector<shared_ptr<ReelBoundary>> const& reel_boundaries,
+ shared_ptr<const Film> film,
+ int offset_,
+ bool snap,
+ DCPTime snap_distance
+ )
+ : reel_boundary(reel_boundary_)
+ , offset(offset_)
+ , _snap_distance(snap_distance)
+{
+ auto iter = std::find(reel_boundaries.begin(), reel_boundaries.end(), reel_boundary);
+ auto index = std::distance(reel_boundaries.begin(), iter);
+
+ if (index > 0) {
+ previous = reel_boundaries[index - 1];
+ }
+ if (index < static_cast<int>(reel_boundaries.size() - 1)) {
+ next = reel_boundaries[index + 1];
+ }
+
+ if (snap) {
+ for (auto content: film->content()) {
+ for (auto split: content->reel_split_points(film)) {
+ _snaps.push_back(split);
+ }
+ }
+ }
+}
+
+
+void
+DCPTimeline::Drag::set_time(DCPTime time)
+{
+ optional<DCPTime> nearest_distance;
+ optional<DCPTime> nearest_time;
+ for (auto snap: _snaps) {
+ auto const distance = time - snap;
+ if (!nearest_distance || distance.abs() < nearest_distance->abs()) {
+ nearest_distance = distance.abs();
+ nearest_time = snap;
+ }
+ }
+
+ if (nearest_distance && *nearest_distance < _snap_distance) {
+ reel_boundary->set_time(*nearest_time);
+ } else {
+ reel_boundary->set_time(time);
+ }
+}
+
+
+DCPTime
+DCPTimeline::Drag::time() const
+{
+ return reel_boundary->time();
+}
+
diff --git a/src/wx/dcp_timeline.h b/src/wx/dcp_timeline.h
new file mode 100644
index 000000000..23644c03f
--- /dev/null
+++ b/src/wx/dcp_timeline.h
@@ -0,0 +1,125 @@
+/*
+ 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/change_signaller.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 <memory>
+
+
+class CheckBox;
+class Choice;
+class Film;
+class ReelBoundary;
+class SpinCtrl;
+class wxGridBagSizer;
+
+
+class DCPTimeline : public Timeline
+{
+public:
+ DCPTimeline(wxWindow* parent, std::shared_ptr<Film> film);
+
+ void force_redraw(dcpomatic::Rect<int> const &);
+
+private:
+ void paint();
+ void paint_reels(wxGraphicsContext* gc);
+ void paint_content(wxGraphicsContext* gc);
+ void setup_pixels_per_second();
+ void left_down(wxMouseEvent& ev);
+ void right_down(wxMouseEvent& ev);
+ void left_up(wxMouseEvent& ev);
+ void mouse_moved(wxMouseEvent& ev);
+ void reel_mode_changed();
+ void maximum_reel_size_changed();
+ void film_changed(ChangeType type, FilmProperty property);
+ std::shared_ptr<Film> film() const;
+ void setup_sensitivity();
+
+ void add_reel_boundary();
+ void setup_reel_settings();
+ void setup_reel_boundaries();
+ std::shared_ptr<ReelBoundary> event_to_reel_boundary(wxMouseEvent& ev) const;
+ void set_reel_boundary(int index, dcpomatic::DCPTime time);
+ bool editable() const;
+
+ std::weak_ptr<Film> _film;
+
+ wxScrolledCanvas* _canvas;
+
+ class Drag
+ {
+ public:
+ Drag(
+ std::shared_ptr<ReelBoundary> reel_boundary_,
+ std::vector<std::shared_ptr<ReelBoundary>> const& reel_boundaries,
+ std::shared_ptr<const Film> film,
+ int offset_,
+ bool snap,
+ dcpomatic::DCPTime snap_distance
+ );
+
+ std::shared_ptr<ReelBoundary> reel_boundary;
+ std::shared_ptr<ReelBoundary> previous;
+ std::shared_ptr<ReelBoundary> next;
+ int offset = 0;
+
+ void set_time(dcpomatic::DCPTime time);
+ dcpomatic::DCPTime time() const;
+
+ private:
+ std::vector<dcpomatic::DCPTime> _snaps;
+ dcpomatic::DCPTime _snap_distance;
+ };
+
+ boost::optional<Drag> _drag;
+
+ wxPoint _right_down_position;
+
+ wxPanel* _reel_settings;
+ Choice* _reel_type;
+ SpinCtrl* _maximum_reel_size;
+ CheckBox* _snap;
+ wxPanel* _reel_detail;
+ wxGridBagSizer* _reel_detail_sizer;
+
+ wxMenu* _menu;
+ wxMenuItem* _add_reel_boundary;
+
+ boost::signals2::scoped_connection _film_connection;
+
+ std::vector<std::shared_ptr<ReelBoundary>> _reel_boundaries;
+};
+
+
+#endif
+
diff --git a/src/wx/dcp_timeline_dialog.cc b/src/wx/dcp_timeline_dialog.cc
new file mode 100644
index 000000000..2cf6a74f1
--- /dev/null
+++ b/src/wx/dcp_timeline_dialog.cc
@@ -0,0 +1,78 @@
+/*
+ Copyright (C) 2013-2021 Carl Hetherington <cth@carlh.net>
+
+ This file is part of DCP-o-matic.
+
+ DCP-o-matic is free software; you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation; either version 2 of the License, or
+ (at your option) any later version.
+
+ DCP-o-matic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with DCP-o-matic. If not, see <http://www.gnu.org/licenses/>.
+
+*/
+
+
+#include "dcp_panel.h"
+#include "dcp_timeline_dialog.h"
+#include "film_editor.h"
+#include "wx_util.h"
+#include "lib/compose.hpp"
+#include "lib/cross.h"
+#include "lib/film.h"
+#include "lib/playlist.h"
+#include <dcp/warnings.h>
+LIBDCP_DISABLE_WARNINGS
+#include <wx/graphics.h>
+LIBDCP_ENABLE_WARNINGS
+#include <list>
+
+
+using std::list;
+using std::shared_ptr;
+using std::string;
+using std::weak_ptr;
+#if BOOST_VERSION >= 106100
+using namespace boost::placeholders;
+#endif
+
+
+DCPTimelineDialog::DCPTimelineDialog(wxWindow* parent, shared_ptr<Film> film)
+ : wxDialog(
+ parent,
+ wxID_ANY,
+ _("Reels"),
+ wxDefaultPosition,
+ wxSize(640, 512),
+#ifdef DCPOMATIC_OSX
+ /* I can't get wxFRAME_FLOAT_ON_PARENT to work on OS X, and although wxSTAY_ON_TOP keeps
+ the window above all others (and not just our own) it's better than nothing for now.
+ */
+ wxDEFAULT_DIALOG_STYLE | wxRESIZE_BORDER | wxFULL_REPAINT_ON_RESIZE | wxSTAY_ON_TOP
+#else
+ wxDEFAULT_DIALOG_STYLE | wxRESIZE_BORDER | wxFULL_REPAINT_ON_RESIZE | wxFRAME_FLOAT_ON_PARENT
+#endif
+ )
+ , _timeline(this, film)
+{
+ auto sizer = new wxBoxSizer(wxVERTICAL);
+ sizer->Add (&_timeline, 1, wxEXPAND | wxALL, 12);
+
+#ifdef DCPOMATIC_LINUX
+ auto buttons = CreateSeparatedButtonSizer (wxCLOSE);
+ if (buttons) {
+ sizer->Add (buttons, wxSizerFlags().Expand().DoubleBorder());
+ }
+#endif
+
+ SetSizer(sizer);
+ sizer->Layout();
+ sizer->SetSizeHints(this);
+}
+
diff --git a/src/wx/dcp_timeline_dialog.h b/src/wx/dcp_timeline_dialog.h
new file mode 100644
index 000000000..d1293ca26
--- /dev/null
+++ b/src/wx/dcp_timeline_dialog.h
@@ -0,0 +1,39 @@
+/*
+ Copyright (C) 2023 Carl Hetherington <cth@carlh.net>
+
+ This file is part of DCP-o-matic.
+
+ DCP-o-matic is free software; you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation; either version 2 of the License, or
+ (at your option) any later version.
+
+ DCP-o-matic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with DCP-o-matic. If not, see <http://www.gnu.org/licenses/>.
+
+*/
+
+
+#include "dcp_timeline.h"
+#include <dcp/warnings.h>
+LIBDCP_DISABLE_WARNINGS
+#include <wx/wx.h>
+LIBDCP_ENABLE_WARNINGS
+#include <memory>
+
+
+class DCPTimelineDialog : public wxDialog
+{
+public:
+ DCPTimelineDialog(wxWindow* parent, std::shared_ptr<Film> film);
+
+private:
+ std::weak_ptr<Film> _film;
+ DCPTimeline _timeline;
+};
+
diff --git a/src/wx/dcp_timeline_reel_marker_view.cc b/src/wx/dcp_timeline_reel_marker_view.cc
new file mode 100644
index 000000000..1c97ca175
--- /dev/null
+++ b/src/wx/dcp_timeline_reel_marker_view.cc
@@ -0,0 +1,71 @@
+/*
+ Copyright (C) 2023 Carl Hetherington <cth@carlh.net>
+
+ This file is part of DCP-o-matic.
+
+ DCP-o-matic is free software; you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation; either version 2 of the License, or
+ (at your option) any later version.
+
+ DCP-o-matic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with DCP-o-matic. If not, see <http://www.gnu.org/licenses/>.
+
+*/
+
+
+#include "dcp_timeline_reel_marker_view.h"
+LIBDCP_DISABLE_WARNINGS
+#include <wx/graphics.h>
+LIBDCP_ENABLE_WARNINGS
+
+
+using namespace std;
+using namespace dcpomatic;
+
+
+DCPTimelineReelMarkerView::DCPTimelineReelMarkerView(DCPTimeline& timeline, int y_pos)
+ : DCPTimelineView(timeline)
+ , _y_pos(y_pos)
+{
+
+}
+
+
+int
+DCPTimelineReelMarkerView::x_pos() const
+{
+ /* Nudge it over slightly so that the full line width is drawn on the left hand side */
+ return time_x(_time) + 2;
+}
+
+
+void
+DCPTimelineReelMarkerView::do_paint(wxGraphicsContext* gc)
+{
+ wxColour const outline = _active ? wxColour(0, 0, 0) : wxColour(128, 128, 128);
+ wxColour const fill = _active ? wxColour(255, 0, 0) : wxColour(192, 192, 192);
+ gc->SetPen(*wxThePenList->FindOrCreatePen(outline, 2, wxPENSTYLE_SOLID));
+ gc->SetBrush(*wxTheBrushList->FindOrCreateBrush(fill, wxBRUSHSTYLE_SOLID));
+
+ gc->DrawRectangle(x_pos(), _y_pos, HEAD_SIZE, HEAD_SIZE);
+
+ auto path = gc->CreatePath();
+ path.MoveToPoint(x_pos(), _y_pos + HEAD_SIZE + TAIL_LENGTH);
+ path.AddLineToPoint(x_pos(), _y_pos);
+ gc->StrokePath(path);
+ gc->FillPath(path);
+}
+
+
+dcpomatic::Rect<int>
+DCPTimelineReelMarkerView::bbox() const
+{
+ return { x_pos(), _y_pos, HEAD_SIZE, HEAD_SIZE + TAIL_LENGTH };
+}
+
diff --git a/src/wx/dcp_timeline_reel_marker_view.h b/src/wx/dcp_timeline_reel_marker_view.h
new file mode 100644
index 000000000..273d98259
--- /dev/null
+++ b/src/wx/dcp_timeline_reel_marker_view.h
@@ -0,0 +1,59 @@
+/*
+ Copyright (C) 2023 Carl Hetherington <cth@carlh.net>
+
+ This file is part of DCP-o-matic.
+
+ DCP-o-matic is free software; you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation; either version 2 of the License, or
+ (at your option) any later version.
+
+ DCP-o-matic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with DCP-o-matic. If not, see <http://www.gnu.org/licenses/>.
+
+*/
+
+
+#include "dcp_timeline_view.h"
+
+
+class DCPTimeline;
+
+
+class DCPTimelineReelMarkerView : public DCPTimelineView
+{
+public:
+ DCPTimelineReelMarkerView(DCPTimeline& timeline, int y_pos);
+
+ dcpomatic::Rect<int> bbox() const override;
+
+ dcpomatic::DCPTime time() const {
+ return _time;
+ }
+
+ void set_time(dcpomatic::DCPTime time) {
+ _time = time;
+ }
+
+ void set_active(bool active) {
+ _active = active;
+ }
+
+ static auto constexpr HEAD_SIZE = 16;
+ static auto constexpr TAIL_LENGTH = 28;
+ static auto constexpr HEIGHT = HEAD_SIZE + TAIL_LENGTH;
+
+private:
+ void do_paint(wxGraphicsContext* gc) override;
+ int x_pos() const;
+
+ dcpomatic::DCPTime _time;
+ int _y_pos;
+ bool _active = false;
+};
+
diff --git a/src/wx/dcp_timeline_view.h b/src/wx/dcp_timeline_view.h
new file mode 100644
index 000000000..24a75cad2
--- /dev/null
+++ b/src/wx/dcp_timeline_view.h
@@ -0,0 +1,44 @@
+/*
+ Copyright (C) 2013-2021 Carl Hetherington <cth@carlh.net>
+
+ This file is part of DCP-o-matic.
+
+ DCP-o-matic is free software; you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation; either version 2 of the License, or
+ (at your option) any later version.
+
+ DCP-o-matic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with DCP-o-matic. If not, see <http://www.gnu.org/licenses/>.
+
+*/
+
+
+#include "dcp_timeline.h"
+#include "timeline_view.h"
+
+
+class DCPTimelineView : public TimelineView<DCPTimeline>
+{
+public:
+ explicit DCPTimelineView(DCPTimeline& timeline)
+ : TimelineView(timeline)
+ {}
+
+ void paint(wxGraphicsContext* gc)
+ {
+ _last_paint_bbox = bbox();
+ do_paint(gc);
+ }
+
+protected:
+ virtual void do_paint(wxGraphicsContext* context) = 0;
+};
+
+
+
diff --git a/src/wx/dcpomatic_choice.cc b/src/wx/dcpomatic_choice.cc
index 2f1a6f0e9..f2e215439 100644
--- a/src/wx/dcpomatic_choice.cc
+++ b/src/wx/dcpomatic_choice.cc
@@ -40,14 +40,14 @@ Choice::Choice(wxWindow* parent)
void
-Choice::add(string const& entry)
+Choice::add_entry(string const& entry)
{
- add(std_to_wx(entry));
+ add_entry(std_to_wx(entry));
}
void
-Choice::add(wxString const& entry)
+Choice::add_entry(wxString const& entry)
{
if (_needs_clearing) {
Clear();
@@ -59,7 +59,7 @@ Choice::add(wxString const& entry)
void
-Choice::add(wxString const& entry, wxClientData* data)
+Choice::add_entry(wxString const& entry, wxClientData* data)
{
if (_needs_clearing) {
Clear();
@@ -71,7 +71,7 @@ Choice::add(wxString const& entry, wxClientData* data)
void
-Choice::add(wxString const& entry, wxString const& data)
+Choice::add_entry(wxString const& entry, wxString const& data)
{
if (_needs_clearing) {
Clear();
@@ -83,6 +83,18 @@ Choice::add(wxString const& entry, wxString const& data)
void
+Choice::set_entries(wxArrayString const& entries)
+{
+ if (GetStrings() == entries) {
+ return;
+ }
+
+ Clear();
+ Set(entries);
+}
+
+
+void
Choice::set(int index)
{
SetSelection(index);
diff --git a/src/wx/dcpomatic_choice.h b/src/wx/dcpomatic_choice.h
index dec0a3701..cc8115d20 100644
--- a/src/wx/dcpomatic_choice.h
+++ b/src/wx/dcpomatic_choice.h
@@ -32,10 +32,12 @@ class Choice : public wxChoice
public:
Choice(wxWindow* parent);
- void add(wxString const& entry);
- void add(wxString const& entry, wxClientData* data);
- void add(wxString const& entry, wxString const& data);
- void add(std::string const& entry);
+ void add_entry(wxString const& entry);
+ void add_entry(wxString const& entry, wxClientData* data);
+ void add_entry(wxString const& entry, wxString const& data);
+ void add_entry(std::string const& entry);
+ void set_entries(wxArrayString const& entries);
+
void set(int index);
void set_by_data(wxString const& data);
boost::optional<int> get() const;
diff --git a/src/wx/dir_picker_ctrl.cc b/src/wx/dir_picker_ctrl.cc
index 4c19da42a..a8c09e334 100644
--- a/src/wx/dir_picker_ctrl.cc
+++ b/src/wx/dir_picker_ctrl.cc
@@ -36,12 +36,13 @@ using namespace std;
using namespace boost;
-DirPickerCtrl::DirPickerCtrl (wxWindow* parent)
+DirPickerCtrl::DirPickerCtrl(wxWindow* parent, bool leaf)
: wxPanel (parent)
+ , _leaf(leaf)
{
_sizer = new wxBoxSizer (wxHORIZONTAL);
- _folder = new StaticText (this, wxT(""));
+ _folder = new StaticText(this, wxT(""), wxDefaultPosition, wxDefaultSize, wxST_ELLIPSIZE_END);
wxFont font = _folder->GetFont ();
font.SetStyle (wxFONTSTYLE_ITALIC);
_folder->SetFont (font);
@@ -62,7 +63,11 @@ DirPickerCtrl::SetPath (wxString p)
if (_path == wxStandardPaths::Get().GetDocumentsDir()) {
_folder->SetLabel (_("My Documents"));
} else {
- _folder->SetLabel (_path);
+ if (_leaf) {
+ _folder->SetLabel(std_to_wx(boost::filesystem::path(wx_to_std(_path)).filename().string()));
+ } else {
+ _folder->SetLabel(_path);
+ }
}
wxCommandEvent ev (wxEVT_DIRPICKER_CHANGED, wxID_ANY);
diff --git a/src/wx/dir_picker_ctrl.h b/src/wx/dir_picker_ctrl.h
index f70168db4..31df9518c 100644
--- a/src/wx/dir_picker_ctrl.h
+++ b/src/wx/dir_picker_ctrl.h
@@ -33,7 +33,7 @@ LIBDCP_ENABLE_WARNINGS
class DirPickerCtrl : public wxPanel
{
public:
- explicit DirPickerCtrl (wxWindow *);
+ DirPickerCtrl(wxWindow *, bool leaf = false);
wxString GetPath () const;
void SetPath (wxString);
@@ -47,6 +47,7 @@ private:
wxButton* _browse;
wxString _path;
wxSizer* _sizer;
+ bool _leaf = false;
};
#endif
diff --git a/src/wx/disk_warning_dialog.cc b/src/wx/disk_warning_dialog.cc
index 531b7f4f9..9af4a2aca 100644
--- a/src/wx/disk_warning_dialog.cc
+++ b/src/wx/disk_warning_dialog.cc
@@ -18,9 +18,12 @@
*/
+
#include "disk_warning_dialog.h"
#include "static_text.h"
#include "wx_util.h"
+#include "wx_variant.h"
+
DiskWarningDialog::DiskWarningDialog ()
: wxDialog(nullptr, wxID_ANY, _("Important notice"))
@@ -45,10 +48,10 @@ DiskWarningDialog::DiskWarningDialog ()
auto const confirmation = _("I am sure");
text->SetLabelMarkup(wxString::Format(
- _("The <b>DCP-o-matic Disk Writer</b> is\n\n<span weight=\"bold\" size=\"20480\" foreground=\"red\">BETA-GRADE TEST SOFTWARE</span>\n\n"
+ _("The <b>%s</b> is\n\n<span weight=\"bold\" size=\"20480\" foreground=\"red\">BETA-GRADE TEST SOFTWARE</span>\n\n"
"and may\n\n<span weight=\"bold\" size=\"20480\" foreground=\"red\">DESTROY DATA!</span>\n\n"
"If you are sure you want to continue please type\n\n<tt>%s</tt>\n\ninto the box below, then click OK."),
- confirmation));
+ variant::wx::dcpomatic_disk_writer(), confirmation));
}
bool
diff --git a/src/wx/dkdm_dialog.cc b/src/wx/dkdm_dialog.cc
index 34a8d6284..a36a9ceae 100644
--- a/src/wx/dkdm_dialog.cc
+++ b/src/wx/dkdm_dialog.cc
@@ -28,6 +28,7 @@
#include "recipients_panel.h"
#include "static_text.h"
#include "wx_util.h"
+#include "wx_variant.h"
#include "lib/config.h"
#include "lib/film.h"
#include "lib/job_manager.h"
@@ -161,7 +162,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);
}
@@ -170,7 +171,13 @@ DKDMDialog::make_clicked ()
if (e.starts_too_early()) {
error_dialog (this, _("The KDM start period is before (or close to) the start of the signing certificate's validity period. Use a later start time for this KDM."));
} else {
- error_dialog (this, _("The KDM end period is after (or close to) the end of the signing certificates' validity period. Either use an earlier end time for this KDM or re-create your signing certificates in the DCP-o-matic preferences window."));
+ error_dialog(
+ this,
+ variant::wx::insert_dcpomatic(
+ _("The KDM end period is after (or close to) the end of the signing certificates' validity period. "
+ "Either use an earlier end time for this KDM or re-create your signing certificates in the %s preferences window.")
+ )
+ );
}
return;
} catch (runtime_error& e) {
diff --git a/src/wx/export_video_file_dialog.h b/src/wx/export_video_file_dialog.h
index beb33610b..4e626be6b 100644
--- a/src/wx/export_video_file_dialog.h
+++ b/src/wx/export_video_file_dialog.h
@@ -20,7 +20,7 @@
#include "table_dialog.h"
-#include "lib/ffmpeg_encoder.h"
+#include "lib/ffmpeg_file_encoder.h"
#include <dcp/warnings.h>
LIBDCP_DISABLE_WARNINGS
#include <wx/wx.h>
diff --git a/src/wx/file_picker_ctrl.cc b/src/wx/file_picker_ctrl.cc
index 7aa0bfb40..c6aba45b7 100644
--- a/src/wx/file_picker_ctrl.cc
+++ b/src/wx/file_picker_ctrl.cc
@@ -32,8 +32,8 @@ LIBDCP_ENABLE_WARNINGS
#include <boost/filesystem.hpp>
-using namespace std;
-using namespace boost;
+using std::string;
+using boost::optional;
FilePickerCtrl::FilePickerCtrl(
@@ -42,9 +42,9 @@ FilePickerCtrl::FilePickerCtrl(
wxString wildcard,
bool open,
bool warn_overwrite,
- std::string initial_path_key,
+ string initial_path_key,
optional<std::string> initial_filename,
- optional<filesystem::path> override_path
+ optional<boost::filesystem::path> override_path
)
: wxPanel (parent)
, _prompt (prompt)
diff --git a/src/wx/film_editor.cc b/src/wx/film_editor.cc
index 9f54db58e..fae02787a 100644
--- a/src/wx/film_editor.cc
+++ b/src/wx/film_editor.cc
@@ -54,13 +54,15 @@ FilmEditor::FilmEditor(wxWindow* parent, FilmViewer& viewer)
{
auto s = new wxBoxSizer (wxVERTICAL);
- auto notebook = new wxNotebook(this, wxID_ANY);
- s->Add(notebook, 1, wxEXPAND);
+ _notebook = new wxNotebook(this, wxID_ANY);
+ s->Add(_notebook, 1, wxEXPAND);
- _content_panel = new ContentPanel(notebook, _film, viewer);
- notebook->AddPage(_content_panel->window(), _("Content"), true);
- _dcp_panel = new DCPPanel(notebook, _film, viewer);
- notebook->AddPage(_dcp_panel->panel (), _("DCP"), false);
+ _content_panel = new ContentPanel(_notebook, _film, viewer);
+ _notebook->AddPage(_content_panel->window(), _("Content"), true);
+ _dcp_panel = new DCPPanel(_notebook, _film, viewer);
+ _notebook->AddPage(_dcp_panel->panel (), _("DCP"), false);
+
+ _notebook->Bind(wxEVT_NOTEBOOK_PAGE_CHANGED, boost::bind(&FilmEditor::page_changed, this, _1));
JobManager::instance()->ActiveJobsChanged.connect (
bind(&FilmEditor::active_jobs_changed, this, _2)
@@ -71,6 +73,18 @@ FilmEditor::FilmEditor(wxWindow* parent, FilmViewer& viewer)
}
+void
+FilmEditor::page_changed(wxBookCtrlEvent& ev)
+{
+ /* One of these events arrives early on with GetOldSelection() being a non-existent tab,
+ * and we want to ignore that.
+ */
+ if (_film && ev.GetOldSelection() < 2) {
+ _film->set_ui_state("FilmEditorTab", ev.GetSelection() == 0 ? "content" : "dcp");
+ }
+}
+
+
/** Called when the metadata stored in the Film object has changed;
* so that we can update the GUI.
* @param p Property of the Film that has changed.
@@ -144,6 +158,13 @@ FilmEditor::set_film (shared_ptr<Film> film)
if (!_film->content().empty()) {
_content_panel->set_selection (_film->content().front());
}
+
+ auto tab = _film->ui_state("FilmEditorTab").get_value_or("content");
+ if (tab == "content") {
+ _notebook->SetSelection(0);
+ } else if (tab == "dcp") {
+ _notebook->SetSelection(1);
+ }
}
diff --git a/src/wx/film_editor.h b/src/wx/film_editor.h
index a3df266ad..54d639ef5 100644
--- a/src/wx/film_editor.h
+++ b/src/wx/film_editor.h
@@ -32,11 +32,12 @@ LIBDCP_ENABLE_WARNINGS
#include <boost/signals2.hpp>
-class wxNotebook;
-class Film;
class ContentPanel;
class DCPPanel;
+class Film;
class FilmViewer;
+class wxBookCtrlEvent;
+class wxNotebook;
/** @class FilmEditor
@@ -71,6 +72,9 @@ private:
void set_general_sensitivity (bool);
void active_jobs_changed (boost::optional<std::string>);
+ void page_changed(wxBookCtrlEvent& ev);
+
+ wxNotebook* _notebook;
ContentPanel* _content_panel;
DCPPanel* _dcp_panel;
diff --git a/src/wx/film_name_location_dialog.cc b/src/wx/film_name_location_dialog.cc
index 05ffa7a68..9d309810e 100644
--- a/src/wx/film_name_location_dialog.cc
+++ b/src/wx/film_name_location_dialog.cc
@@ -35,11 +35,12 @@ LIBDCP_ENABLE_WARNINGS
#include <boost/filesystem.hpp>
-using namespace std;
-using namespace boost;
+using std::string;
+using boost::bind;
+using boost::optional;
-optional<filesystem::path> FilmNameLocationDialog::_directory;
+optional<boost::filesystem::path> FilmNameLocationDialog::_directory;
FilmNameLocationDialog::FilmNameLocationDialog (wxWindow* parent, wxString title, bool offer_templates)
@@ -112,10 +113,10 @@ FilmNameLocationDialog::folder_changed ()
}
-filesystem::path
+boost::filesystem::path
FilmNameLocationDialog::path () const
{
- filesystem::path p;
+ boost::filesystem::path p;
p /= wx_to_std (_folder->GetPath());
p /= wx_to_std (_name->GetValue());
return p;
@@ -139,7 +140,7 @@ FilmNameLocationDialog::template_name () const
bool
FilmNameLocationDialog::check_path ()
{
- if (filesystem::is_directory(path()) && !filesystem::is_empty(path())) {
+ if (boost::filesystem::is_directory(path()) && !boost::filesystem::is_empty(path())) {
if (!confirm_dialog (
this,
std_to_wx (
@@ -150,7 +151,7 @@ FilmNameLocationDialog::check_path ()
)) {
return false;
}
- } else if (filesystem::is_regular_file(path())) {
+ } else if (boost::filesystem::is_regular_file(path())) {
error_dialog (
this,
String::compose (wx_to_std(_("%1 already exists as a file, so you cannot use it for a film.")), path().c_str())
diff --git a/src/wx/film_viewer.cc b/src/wx/film_viewer.cc
index b6f8096e8..ad240b957 100644
--- a/src/wx/film_viewer.cc
+++ b/src/wx/film_viewer.cc
@@ -174,7 +174,7 @@ FilmViewer::set_film (shared_ptr<Film> film)
}
try {
- _player.emplace(_film, _optimise_for_j2k ? Image::Alignment::COMPACT : Image::Alignment::PADDED);
+ _player.emplace(_film, _optimisation == Optimisation::NONE ? Image::Alignment::PADDED : Image::Alignment::COMPACT);
_player->set_fast ();
if (_dcp_decode_reduction) {
_player->set_dcp_decode_reduction (_dcp_decode_reduction);
@@ -235,9 +235,9 @@ void
FilmViewer::create_butler()
{
#if wxCHECK_VERSION(3, 1, 0)
- auto const j2k_gl_optimised = dynamic_pointer_cast<GLVideoView>(_video_view) && _optimise_for_j2k;
+ auto const opengl = dynamic_pointer_cast<GLVideoView>(_video_view);
#else
- auto const j2k_gl_optimised = false;
+ auto const opengl = false;
#endif
DCPOMATIC_ASSERT(_player);
@@ -249,9 +249,9 @@ FilmViewer::create_butler()
_audio_channels,
boost::bind(&PlayerVideo::force, AV_PIX_FMT_RGB24),
VideoRange::FULL,
- j2k_gl_optimised ? Image::Alignment::COMPACT : Image::Alignment::PADDED,
+ (opengl && _optimisation != Optimisation::NONE) ? Image::Alignment::COMPACT : Image::Alignment::PADDED,
true,
- j2k_gl_optimised,
+ opengl && _optimisation == Optimisation::JPEG2000,
(Config::instance()->sound() && _audio.isStreamOpen()) ? Butler::Audio::ENABLED : Butler::Audio::DISABLED
);
@@ -874,10 +874,11 @@ FilmViewer::image_changed (shared_ptr<PlayerVideo> pv)
void
-FilmViewer::set_optimise_for_j2k (bool o)
+FilmViewer::set_optimisation(Optimisation o)
{
- _optimise_for_j2k = o;
- _video_view->set_optimise_for_j2k (o);
+ _optimisation = o;
+ _video_view->set_optimisation(o);
+ destroy_and_maybe_create_butler();
}
diff --git a/src/wx/film_viewer.h b/src/wx/film_viewer.h
index 5824f8baa..392cf85a3 100644
--- a/src/wx/film_viewer.h
+++ b/src/wx/film_viewer.h
@@ -24,9 +24,12 @@
*/
+#include "optimisation.h"
#include "video_view.h"
+#include "lib/change_signaller.h"
#include "lib/config.h"
#include "lib/film_property.h"
+#include "lib/player.h"
#include "lib/player_text.h"
#include "lib/signaller.h"
#include "lib/timer.h"
@@ -99,7 +102,7 @@ public:
void set_outline_subtitles (boost::optional<dcpomatic::Rect<double>>);
void set_eyes (Eyes e);
void set_pad_black (bool p);
- void set_optimise_for_j2k (bool o);
+ void set_optimisation(Optimisation o);
void set_crop_guess (dcpomatic::Rect<float> crop);
void unset_crop_guess ();
@@ -204,10 +207,7 @@ private:
boost::optional<int> _dcp_decode_reduction;
- /** true to assume that this viewer is only being used for JPEG2000 sources
- * so it can optimise accordingly.
- */
- bool _optimise_for_j2k = false;
+ Optimisation _optimisation = Optimisation::NONE;
ClosedCaptionsDialog* _closed_captions_dialog = nullptr;
diff --git a/src/wx/full_config_dialog.cc b/src/wx/full_config_dialog.cc
index c1c36c4a4..955ea6493 100644
--- a/src/wx/full_config_dialog.cc
+++ b/src/wx/full_config_dialog.cc
@@ -45,10 +45,13 @@
#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 "wx_variant.h"
#include "lib/config.h"
#include "lib/cross.h"
-#include "lib/dcp_content_type.h"
#include "lib/email.h"
#include "lib/exceptions.h"
#include "lib/filter.h"
@@ -104,12 +107,12 @@ private:
int r = 0;
add_language_controls (table, r);
- add_label_to_sizer (table, _panel, _("Number of threads DCP-o-matic should use"), true, wxGBPosition (r, 0));
+ add_label_to_sizer(table, _panel, variant::wx::insert_dcpomatic(_("Number of threads %s should use")), true, wxGBPosition(r, 0));
_master_encoding_threads = new wxSpinCtrl (_panel);
table->Add (_master_encoding_threads, wxGBPosition (r, 1));
++r;
- add_label_to_sizer (table, _panel, _("Number of threads DCP-o-matic encode server should use"), true, wxGBPosition (r, 0));
+ add_label_to_sizer(table, _panel, variant::wx::insert_dcpomatic_encode_server(_("Number of threads %s should use")), true, wxGBPosition(r, 0));
_server_encoding_threads = new wxSpinCtrl (_panel);
table->Add (_server_encoding_threads, wxGBPosition (r, 1));
++r;
@@ -143,8 +146,8 @@ private:
add_update_controls (table, r);
- _default_add_file_location->add(_("Same place as last time"));
- _default_add_file_location->add(_("Same place as project"));
+ _default_add_file_location->add_entry(_("Same place as last time"));
+ _default_add_file_location->add_entry(_("Same place as project"));
_default_add_file_location->bind(&FullGeneralPage::default_add_file_location_changed, this);
_config_file->Bind (wxEVT_FILEPICKER_CHANGED, boost::bind(&FullGeneralPage::config_file_changed, this));
@@ -307,23 +310,6 @@ private:
#endif
table->Add (_directory, 1, wxEXPAND);
- add_label_to_sizer (table, _panel, _("Default content type"), true, 0, wxLEFT | wxRIGHT | wxALIGN_CENTRE_VERTICAL);
- _dcp_content_type = new wxChoice (_panel, wxID_ANY);
- table->Add (_dcp_content_type);
-
- add_label_to_sizer (table, _panel, _("Default DCP audio channels"), true, 0, wxLEFT | wxRIGHT | wxALIGN_CENTRE_VERTICAL);
- _dcp_audio_channels = new wxChoice (_panel, wxID_ANY);
- table->Add (_dcp_audio_channels);
-
- {
- add_label_to_sizer (table, _panel, _("Default JPEG2000 bandwidth"), true, 0, wxLEFT | wxRIGHT | wxALIGN_CENTRE_VERTICAL);
- auto s = new wxBoxSizer (wxHORIZONTAL);
- _j2k_bandwidth = new wxSpinCtrl (_panel);
- s->Add (_j2k_bandwidth);
- add_label_to_sizer (s, _panel, _("Mbit/s"), false, 0, wxLEFT | wxRIGHT | wxALIGN_CENTRE_VERTICAL);
- table->Add (s, 1);
- }
-
{
add_label_to_sizer (table, _panel, _("Default audio delay"), true, 0, wxLEFT | wxRIGHT | wxALIGN_CENTRE_VERTICAL);
auto s = new wxBoxSizer (wxHORIZONTAL);
@@ -333,32 +319,11 @@ private:
table->Add (s, 1);
}
- add_label_to_sizer (table, _panel, _("Default standard"), true, 0, wxLEFT | wxRIGHT | wxALIGN_CENTRE_VERTICAL);
- _standard = new wxChoice (_panel, wxID_ANY);
- table->Add (_standard);
-
_enable_audio_language = new CheckBox(_panel, _("Default audio language"));
table->Add(_enable_audio_language, 1, wxEXPAND | wxALIGN_CENTRE_VERTICAL);
_audio_language = new LanguageTagWidget(_panel, _("Default audio language to use for new DCPs"), Config::instance()->default_audio_language(), wxString("cmnr-Hant-"));
table->Add(_audio_language->sizer());
- _enable_territory = new CheckBox(_panel, _("Default territory"));
- table->Add(_enable_territory, 1, wxEXPAND | wxALIGN_CENTRE_VERTICAL);
- _territory = new RegionSubtagWidget(_panel, _("Default territory to use for new DCPs"), Config::instance()->default_territory(), wxString("cmnr-Hant-"));
- table->Add(_territory->sizer());
-
- table->Add (_enable_metadata["facility"] = new CheckBox (_panel, _("Default facility")), 0, wxALIGN_CENTRE_VERTICAL);
- table->Add (_metadata["facility"] = new wxTextCtrl (_panel, wxID_ANY, wxT("")), 0, wxEXPAND);
-
- table->Add (_enable_metadata["studio"] = new CheckBox (_panel, _("Default studio")), 0, wxALIGN_CENTRE_VERTICAL);
- table->Add (_metadata["studio"] = new wxTextCtrl (_panel, wxID_ANY, wxT("")), 0, wxEXPAND);
-
- table->Add (_enable_metadata["chain"] = new CheckBox (_panel, _("Default chain")), 0, wxALIGN_CENTRE_VERTICAL);
- table->Add (_metadata["chain"] = new wxTextCtrl (_panel, wxID_ANY, wxT("")), 0, wxEXPAND);
-
- table->Add (_enable_metadata["distributor"] = new CheckBox (_panel, _("Default distributor")), 0, wxALIGN_CENTRE_VERTICAL);
- table->Add (_metadata["distributor"] = new wxTextCtrl (_panel, wxID_ANY, wxT("")), 0, wxEXPAND);
-
add_label_to_sizer (table, _panel, _("Default KDM directory"), true, 0, wxLEFT | wxRIGHT | wxALIGN_CENTRE_VERTICAL);
#ifdef DCPOMATIC_USE_OWN_PICKER
_kdm_directory = new DirPickerCtrl (_panel);
@@ -397,86 +362,25 @@ private:
_use_isdcf_name_by_default->bind(&DefaultsPage::use_isdcf_name_by_default_changed, this);
- for (auto i: DCPContentType::all()) {
- _dcp_content_type->Append (std_to_wx (i->pretty_name ()));
- }
-
- setup_audio_channels_choice (_dcp_audio_channels, 2);
-
- _dcp_content_type->Bind (wxEVT_CHOICE, boost::bind (&DefaultsPage::dcp_content_type_changed, this));
- _dcp_audio_channels->Bind (wxEVT_CHOICE, boost::bind (&DefaultsPage::dcp_audio_channels_changed, this));
-
- _j2k_bandwidth->SetRange (50, 250);
- _j2k_bandwidth->Bind (wxEVT_SPINCTRL, boost::bind (&DefaultsPage::j2k_bandwidth_changed, this));
-
_audio_delay->SetRange (-1000, 1000);
_audio_delay->Bind (wxEVT_SPINCTRL, boost::bind (&DefaultsPage::audio_delay_changed, this));
- _standard->Append (_("SMPTE"));
- _standard->Append (_("Interop"));
- _standard->Bind (wxEVT_CHOICE, boost::bind (&DefaultsPage::standard_changed, this));
-
- for (auto const& i: _enable_metadata) {
- i.second->bind(&DefaultsPage::metadata_changed, this);
- }
-
- for (auto const& i: _metadata) {
- i.second->Bind (wxEVT_TEXT, boost::bind(&DefaultsPage::metadata_changed, this));
- }
-
_enable_audio_language->bind(&DefaultsPage::enable_audio_language_toggled, this);
_audio_language->Changed.connect(boost::bind(&DefaultsPage::audio_language_changed, this));
-
- _enable_territory->bind(&DefaultsPage::enable_territory_toggled, this);
- _territory->Changed.connect(boost::bind(&DefaultsPage::territory_changed, this));
}
void config_changed () override
{
auto config = Config::instance ();
-
- auto const ct = DCPContentType::all ();
- for (size_t i = 0; i < ct.size(); ++i) {
- if (ct[i] == config->default_dcp_content_type()) {
- _dcp_content_type->SetSelection (i);
- }
- }
-
checked_set (_still_length, config->default_still_length ());
_directory->SetPath (std_to_wx (config->default_directory_or (wx_to_std (wxStandardPaths::Get().GetDocumentsDir())).string ()));
_kdm_directory->SetPath (std_to_wx (config->default_kdm_directory_or (wx_to_std (wxStandardPaths::Get().GetDocumentsDir())).string ()));
_kdm_type->set (config->default_kdm_type());
checked_set (_use_isdcf_name_by_default, config->use_isdcf_name_by_default());
- checked_set (_j2k_bandwidth, config->default_j2k_bandwidth() / 1000000);
- _j2k_bandwidth->SetRange (50, config->maximum_j2k_bandwidth() / 1000000);
- checked_set (_dcp_audio_channels, locale_convert<string> (config->default_dcp_audio_channels()));
checked_set (_audio_delay, config->default_audio_delay ());
- checked_set (_standard, config->default_interop() ? 1 : 0);
auto dal = config->default_audio_language();
checked_set(_enable_audio_language, static_cast<bool>(dal));
checked_set(_audio_language, dal ? dal : boost::none);
- auto dt = config->default_territory();
- checked_set(_enable_territory, static_cast<bool>(dt));
- checked_set(_territory, dt ? dt : boost::none);
-
- auto metadata = config->default_metadata();
-
- for (auto const& i: metadata) {
- _enable_metadata[i.first]->SetValue(true);
- checked_set (_metadata[i.first], i.second);
- }
-
- for (auto const& i: _enable_metadata) {
- if (metadata.find(i.first) == metadata.end()) {
- checked_set (i.second, false);
- }
- }
-
- for (auto const& i: _metadata) {
- if (metadata.find(i.first) == metadata.end()) {
- checked_set (i.second, wxT(""));
- }
- }
checked_set (_kdm_duration, config->default_kdm_duration().duration);
switch (config->default_kdm_duration().unit) {
@@ -523,26 +427,11 @@ private:
config->set_default_kdm_duration (RoughDuration(duration, unit));
}
- void j2k_bandwidth_changed ()
- {
- Config::instance()->set_default_j2k_bandwidth (_j2k_bandwidth->GetValue() * 1000000);
- }
-
void audio_delay_changed ()
{
Config::instance()->set_default_audio_delay (_audio_delay->GetValue());
}
- void dcp_audio_channels_changed ()
- {
- int const s = _dcp_audio_channels->GetSelection ();
- if (s != wxNOT_FOUND) {
- Config::instance()->set_default_dcp_audio_channels (
- locale_convert<int>(string_client_data(_dcp_audio_channels->GetClientObject(s)))
- );
- }
- }
-
void directory_changed ()
{
Config::instance()->set_default_directory (wx_to_std (_directory->GetPath ()));
@@ -568,29 +457,6 @@ private:
Config::instance()->set_default_still_length (_still_length->GetValue ());
}
- void dcp_content_type_changed ()
- {
- auto ct = DCPContentType::all ();
- Config::instance()->set_default_dcp_content_type (ct[_dcp_content_type->GetSelection()]);
- }
-
- void standard_changed ()
- {
- Config::instance()->set_default_interop (_standard->GetSelection() == 1);
- }
-
- void metadata_changed ()
- {
- map<string, string> metadata;
- for (auto const& i: _enable_metadata) {
- if (i.second->GetValue()) {
- metadata[i.first] = wx_to_std(_metadata[i.first]->GetValue());
- }
- }
- Config::instance()->set_default_metadata (metadata);
- setup_sensitivity ();
- }
-
void enable_audio_language_toggled()
{
setup_sensitivity();
@@ -606,31 +472,11 @@ private:
}
}
- void enable_territory_toggled()
- {
- setup_sensitivity();
- territory_changed();
- }
-
- void territory_changed()
- {
- if (_enable_territory->get()) {
- Config::instance()->set_default_territory(_territory->get().get_value_or(dcp::LanguageTag::RegionSubtag("US")));
- } else {
- Config::instance()->unset_default_territory();
- }
- }
-
void setup_sensitivity ()
{
_audio_language->enable(_enable_audio_language->get());
- _territory->enable(_enable_territory->get());
- for (auto const& i: _enable_metadata) {
- _metadata[i.first]->Enable(i.second->GetValue());
- }
}
- wxSpinCtrl* _j2k_bandwidth;
wxSpinCtrl* _audio_delay;
wxSpinCtrl* _still_length;
#ifdef DCPOMATIC_USE_OWN_PICKER
@@ -644,15 +490,8 @@ private:
wxSpinCtrl* _kdm_duration;
wxChoice* _kdm_duration_unit;
CheckBox* _use_isdcf_name_by_default;
- wxChoice* _dcp_content_type;
- wxChoice* _dcp_audio_channels;
- wxChoice* _standard;
CheckBox* _enable_audio_language;
LanguageTagWidget* _audio_language;
- CheckBox* _enable_territory;
- RegionSubtagWidget* _territory;
- map<string, CheckBox*> _enable_metadata;
- map<string, wxTextCtrl*> _metadata;
};
@@ -983,8 +822,8 @@ private:
Email email(
wx_to_std(dialog.from()),
{ wx_to_std(dialog.to()) },
- wx_to_std(_("DCP-o-matic test email")),
- wx_to_std(_("This is a test email from DCP-o-matic."))
+ wx_to_std(variant::wx::insert_dcpomatic(_("%s test email"))),
+ wx_to_std(variant::wx::insert_dcpomatic(_("This is a test email from %s.")))
);
auto config = Config::instance();
try {
@@ -1404,32 +1243,50 @@ private:
add_label_to_sizer (table, _panel, _("Issuer"), true, 0, wxLEFT | wxRIGHT | wxALIGN_CENTRE_VERTICAL);
_issuer = new wxTextCtrl (_panel, wxID_ANY);
- _issuer->SetToolTip (_("This will be written to the DCP's XML files as the <Issuer>. If it is blank, a default value mentioning DCP-o-matic will be used."));
+ _issuer->SetToolTip(
+ variant::wx::insert_dcpomatic(
+ _("This will be written to the DCP's XML files as the <Issuer>. If it is blank, a default value mentioning %s will be used.")
+ ));
table->Add (_issuer, 1, wxALL | wxEXPAND);
add_label_to_sizer (table, _panel, _("Creator"), true, 0, wxLEFT | wxRIGHT | wxALIGN_CENTRE_VERTICAL);
_creator = new wxTextCtrl (_panel, wxID_ANY);
- _creator->SetToolTip (_("This will be written to the DCP's XML files as the <Creator>. If it is blank, a default value mentioning DCP-o-matic will be used."));
+ _creator->SetToolTip(
+ variant::wx::insert_dcpomatic(
+ _("This will be written to the DCP's XML files as the <Creator>. If it is blank, a default value mentioning %s will be used.")
+ ));
table->Add (_creator, 1, wxALL | wxEXPAND);
add_label_to_sizer (table, _panel, _("Company name"), true, 0, wxLEFT | wxRIGHT | wxALIGN_CENTRE_VERTICAL);
_company_name = new wxTextCtrl (_panel, wxID_ANY);
- _company_name->SetToolTip (_("This will be written to the DCP's MXF files as the 'company name'. If it is blank, a default value mentioning libdcp (an internal DCP-o-matic library) will be used."));
+ _company_name->SetToolTip(
+ variant::wx::insert_dcpomatic(
+ _("This will be written to the DCP's MXF files as the 'company name'. If it is blank, a default value mentioning libdcp (an internal %s library) will be used.")
+ ));
table->Add (_company_name, 1, wxALL | wxEXPAND);
add_label_to_sizer (table, _panel, _("Product name"), true, 0, wxLEFT | wxRIGHT | wxALIGN_CENTRE_VERTICAL);
_product_name = new wxTextCtrl (_panel, wxID_ANY);
- _product_name->SetToolTip (_("This will be written to the DCP's MXF files as the 'product name'. If it is blank, a default value mentioning libdcp (an internal DCP-o-matic library) will be used."));
+ _product_name->SetToolTip(
+ variant::wx::insert_dcpomatic(
+ _("This will be written to the DCP's MXF files as the 'product name'. If it is blank, a default value mentioning libdcp (an internal %s library) will be used.")
+ ));
table->Add (_product_name, 1, wxALL | wxEXPAND);
add_label_to_sizer (table, _panel, _("Product version"), true, 0, wxLEFT | wxRIGHT | wxALIGN_CENTRE_VERTICAL);
_product_version = new wxTextCtrl (_panel, wxID_ANY);
- _product_version->SetToolTip (_("This will be written to the DCP's MXF files as the 'product version'. If it is blank, a default value mentioning libdcp (an internal DCP-o-matic library) will be used."));
+ _product_version->SetToolTip(
+ variant::wx::insert_dcpomatic(
+ _("This will be written to the DCP's MXF files as the 'product version'. If it is blank, a default value mentioning libdcp (an internal %s library) will be used.")
+ ));
table->Add (_product_version, 1, wxALL | wxEXPAND);
add_label_to_sizer (table, _panel, _("JPEG2000 comment"), true, 0, wxLEFT | wxRIGHT | wxALIGN_CENTRE_VERTICAL);
_j2k_comment = new wxTextCtrl (_panel, wxID_ANY);
- _j2k_comment->SetToolTip (_("This will be written to the DCP's JPEG2000 data as a comment. If it is blank, a default value mentioning libdcp (an internal DCP-o-matic library) will be used."));
+ _j2k_comment->SetToolTip(
+ variant::wx::insert_dcpomatic(
+ _("This will be written to the DCP's JPEG2000 data as a comment. If it is blank, a default value mentioning libdcp (an internal %s library) will be used.")
+ ));
table->Add (_j2k_comment, 1, wxALL | wxEXPAND);
_panel->GetSizer()->Add (table, 0, wxEXPAND | wxALL, _border);
@@ -1519,10 +1376,19 @@ private:
_panel->GetSizer()->Add(table, 1, wxALL | wxEXPAND, _border);
{
- add_label_to_sizer(table, _panel, _("Maximum JPEG2000 bandwidth"), true, 0, wxLEFT | wxRIGHT | wxALIGN_CENTRE_VERTICAL);
+ add_label_to_sizer(table, _panel, _("Maximum JPEG2000 bit rate"), true, 0, wxLEFT | wxRIGHT | wxALIGN_CENTRE_VERTICAL);
+ auto s = new wxBoxSizer(wxHORIZONTAL);
+ _maximum_j2k_video_bit_rate = new wxSpinCtrl(_panel);
+ s->Add(_maximum_j2k_video_bit_rate, 1);
+ add_label_to_sizer(s, _panel, _("Mbit/s"), false, 0, wxLEFT | wxALIGN_CENTRE_VERTICAL);
+ table->Add(s, 1);
+ }
+
+ {
+ add_label_to_sizer(table, _panel, _("Maximum MPEG2 bit rate"), true, 0, wxLEFT | wxRIGHT | wxALIGN_CENTRE_VERTICAL);
auto s = new wxBoxSizer(wxHORIZONTAL);
- _maximum_j2k_bandwidth = new wxSpinCtrl(_panel);
- s->Add(_maximum_j2k_bandwidth, 1);
+ _maximum_mpeg2_video_bit_rate = new wxSpinCtrl(_panel);
+ s->Add(_maximum_mpeg2_video_bit_rate, 1);
add_label_to_sizer(s, _panel, _("Mbit/s"), false, 0, wxLEFT | wxALIGN_CENTRE_VERTICAL);
table->Add(s, 1);
}
@@ -1537,13 +1403,7 @@ private:
_allow_any_container = new CheckBox(_panel, _("Allow full-frame and non-standard container ratios"));
table->Add(_allow_any_container, 1, wxEXPAND | wxLEFT, DCPOMATIC_SIZER_GAP);
- auto restart = new StaticText(_panel, _("(restart DCP-o-matic to see all ratios)"));
- auto font = restart->GetFont();
- font.SetStyle(wxFONTSTYLE_ITALIC);
- font.SetPointSize(font.GetPointSize() - 1);
- restart->SetFont(font);
- table->Add(restart, 1, wxALIGN_CENTRE_VERTICAL | wxBOTTOM, DCPOMATIC_CHECKBOX_BOTTOM_PAD);
- restart->SetFont(font);
+ table->AddSpacer(0);
checkbox(_("Allow creation of DCPs with 96kHz audio"), _allow_96khz_audio);
checkbox(_("Allow mapping to all audio channels"), _use_all_audio_channels);
@@ -1557,8 +1417,10 @@ private:
table->Add(s, 1);
}
- _maximum_j2k_bandwidth->SetRange(1, 1000);
- _maximum_j2k_bandwidth->Bind(wxEVT_SPINCTRL, boost::bind(&NonStandardPage::maximum_j2k_bandwidth_changed, this));
+ _maximum_j2k_video_bit_rate->SetRange(1, 1000);
+ _maximum_j2k_video_bit_rate->Bind(wxEVT_SPINCTRL, boost::bind(&NonStandardPage::maximum_j2k_video_bit_rate_changed, this));
+ _maximum_mpeg2_video_bit_rate->SetRange(1, 100);
+ _maximum_mpeg2_video_bit_rate->Bind(wxEVT_SPINCTRL, boost::bind(&NonStandardPage::maximum_mpeg2_video_bit_rate_changed, this));
_allow_any_dcp_frame_rate->bind(&NonStandardPage::allow_any_dcp_frame_rate_changed, this);
_allow_any_container->bind(&NonStandardPage::allow_any_container_changed, this);
_allow_96khz_audio->bind(&NonStandardPage::allow_96khz_audio_changed, this);
@@ -1572,7 +1434,8 @@ private:
{
auto config = Config::instance();
- checked_set(_maximum_j2k_bandwidth, config->maximum_j2k_bandwidth() / 1000000);
+ checked_set(_maximum_j2k_video_bit_rate, config->maximum_video_bit_rate(VideoEncoding::JPEG2000) / 1000000);
+ checked_set(_maximum_mpeg2_video_bit_rate, config->maximum_video_bit_rate(VideoEncoding::MPEG2) / 1000000);
checked_set(_allow_any_dcp_frame_rate, config->allow_any_dcp_frame_rate());
checked_set(_allow_any_container, config->allow_any_container());
checked_set(_allow_96khz_audio, config->allow_96khz_audio());
@@ -1581,9 +1444,14 @@ private:
checked_set(_isdcf_name_part_length, config->isdcf_name_part_length());
}
- void maximum_j2k_bandwidth_changed()
+ void maximum_j2k_video_bit_rate_changed()
{
- Config::instance()->set_maximum_j2k_bandwidth(_maximum_j2k_bandwidth->GetValue() * 1000000);
+ Config::instance()->set_maximum_video_bit_rate(VideoEncoding::JPEG2000, _maximum_j2k_video_bit_rate->GetValue() * 1000000);
+ }
+
+ void maximum_mpeg2_video_bit_rate_changed()
+ {
+ Config::instance()->set_maximum_video_bit_rate(VideoEncoding::MPEG2, _maximum_mpeg2_video_bit_rate->GetValue() * 1000000);
}
void allow_any_dcp_frame_rate_changed()
@@ -1616,7 +1484,8 @@ private:
Config::instance()->set_isdcf_name_part_length(_isdcf_name_part_length->GetValue());
}
- wxSpinCtrl* _maximum_j2k_bandwidth = nullptr;
+ wxSpinCtrl* _maximum_j2k_video_bit_rate = nullptr;
+ wxSpinCtrl* _maximum_mpeg2_video_bit_rate = nullptr;
CheckBox* _allow_any_dcp_frame_rate = nullptr;
CheckBox* _allow_any_container = nullptr;
CheckBox* _allow_96khz_audio = nullptr;
@@ -1671,7 +1540,7 @@ private:
_video_display_mode = new wxChoice (_panel, wxID_ANY);
table->Add (_video_display_mode);
- auto restart = add_label_to_sizer (table, _panel, _("(restart DCP-o-matic to change display mode)"), false);
+ auto restart = add_label_to_sizer(table, _panel, variant::wx::insert_dcpomatic(_("(restart %s to change display mode)")), false);
auto font = restart->GetFont();
font.SetStyle (wxFONTSTYLE_ITALIC);
font.SetPointSize (font.GetPointSize() - 1);
@@ -1946,6 +1815,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/gl_video_view.cc b/src/wx/gl_video_view.cc
index 06c9f268b..c96fd02a0 100644
--- a/src/wx/gl_video_view.cc
+++ b/src/wx/gl_video_view.cc
@@ -194,12 +194,15 @@ static constexpr char fragment_source[] =
"\n"
"in vec2 TexCoord;\n"
"\n"
-"uniform sampler2D texture_sampler;\n"
+"uniform sampler2D texture_sampler_0;\n"
+"uniform sampler2D texture_sampler_1;\n"
+"uniform sampler2D texture_sampler_2;\n"
/* type = 0: draw outline content rectangle
* type = 1: draw crop guess rectangle
* type = 2: draw XYZ image
* type = 3: draw RGB image (with sRGB/Rec709 primaries)
* type = 4: draw RGB image (converting from Rec2020 primaries)
+ * type = 5; draw YUV image (Y in texture_sampler_0, U in texture_sampler_1, V in texture_sampler_2)
* See FragmentType enum below.
*/
"uniform int type = 0;\n"
@@ -270,7 +273,7 @@ static constexpr char fragment_source[] =
" FragColor = crop_guess_colour;\n"
" break;\n"
" case 2:\n"
-" FragColor = texture_bicubic(texture_sampler, TexCoord);\n"
+" FragColor = texture_bicubic(texture_sampler_0, TexCoord);\n"
" FragColor.x = pow(FragColor.x, IN_GAMMA) / DCI_COEFFICIENT;\n"
" FragColor.y = pow(FragColor.y, IN_GAMMA) / DCI_COEFFICIENT;\n"
" FragColor.z = pow(FragColor.z, IN_GAMMA) / DCI_COEFFICIENT;\n"
@@ -280,12 +283,22 @@ static constexpr char fragment_source[] =
" FragColor.z = pow(FragColor.z, OUT_GAMMA);\n"
" break;\n"
" case 3:\n"
-" FragColor = texture_bicubic(texture_sampler, TexCoord);\n"
+" FragColor = texture_bicubic(texture_sampler_0, TexCoord);\n"
" break;\n"
" case 4:\n"
-" FragColor = texture_bicubic(texture_sampler, TexCoord);\n"
+" FragColor = texture_bicubic(texture_sampler_0, TexCoord);\n"
" FragColor = rec2020_rec709_colour_conversion * FragColor;\n"
" break;\n"
+" case 5:\n"
+" float y = texture_bicubic(texture_sampler_0, TexCoord).x;\n"
+" float u = texture_bicubic(texture_sampler_1, TexCoord).x - 0.5;\n"
+" float v = texture_bicubic(texture_sampler_2, TexCoord).x - 0.5;\n"
+ // From https://en.wikipedia.org/wiki/YCbCr#ITU-R_BT.709_conversion
+" FragColor.x = y + 1.5748 * v;\n"
+" FragColor.y = y - 0.1873 * u - 0.4681 * v;\n"
+" FragColor.z = y + 1.8556 * u;\n"
+" FragColor.a = 1;\n"
+" break;\n"
" }\n"
"}\n";
@@ -297,6 +310,7 @@ enum class FragmentType
XYZ_IMAGE = 2,
REC709_IMAGE = 3,
REC2020_IMAGE = 4,
+ YUV420P_IMAGE = 5,
};
@@ -455,6 +469,18 @@ GLVideoView::setup_shaders ()
glDeleteShader (fragment_shader);
glUseProgram (program);
+ auto texture_0 = glGetUniformLocation(program, "texture_sampler_0");
+ check_gl_error("glGetUniformLocation");
+ glUniform1i(texture_0, 0);
+ check_gl_error("glUniform1i");
+ auto texture_1 = glGetUniformLocation(program, "texture_sampler_1");
+ check_gl_error("glGetUniformLocation");
+ glUniform1i(texture_1, 1);
+ check_gl_error("glUniform1i");
+ auto texture_2 = glGetUniformLocation(program, "texture_sampler_2");
+ check_gl_error("glGetUniformLocation");
+ glUniform1i(texture_2, 2);
+ check_gl_error("glUniform1i");
_fragment_type = glGetUniformLocation (program, "type");
check_gl_error ("glGetUniformLocation");
@@ -560,14 +586,18 @@ GLVideoView::draw ()
glBindVertexArray(_vao);
check_gl_error ("glBindVertexArray");
- if (_optimise_for_j2k) {
+ if (_optimisation == Optimisation::MPEG2) {
+ glUniform1i(_fragment_type, static_cast<GLint>(FragmentType::YUV420P_IMAGE));
+ } else if (_optimisation == Optimisation::JPEG2000) {
glUniform1i(_fragment_type, static_cast<GLint>(FragmentType::XYZ_IMAGE));
} else if (_rec2020) {
glUniform1i(_fragment_type, static_cast<GLint>(FragmentType::REC2020_IMAGE));
} else {
glUniform1i(_fragment_type, static_cast<GLint>(FragmentType::REC709_IMAGE));
}
- _video_texture->bind();
+ for (auto& texture: _video_textures) {
+ texture->bind();
+ }
glDrawElements (GL_TRIANGLES, indices_video_texture_number, GL_UNSIGNED_INT, reinterpret_cast<void*>(indices_video_texture_offset * sizeof(int)));
if (_have_subtitle_to_render) {
glUniform1i(_fragment_type, static_cast<GLint>(FragmentType::REC709_IMAGE));
@@ -595,22 +625,39 @@ GLVideoView::draw ()
void
GLVideoView::set_image (shared_ptr<const PlayerVideo> pv)
{
- shared_ptr<const Image> video = _optimise_for_j2k ? pv->raw_image() : pv->image(boost::bind(&PlayerVideo::force, AV_PIX_FMT_RGB24), VideoRange::FULL, true);
+ shared_ptr<const Image> video;
+
+ switch (_optimisation) {
+ case Optimisation::JPEG2000:
+ case Optimisation::MPEG2:
+ video = pv->raw_image();
+ break;
+ case Optimisation::NONE:
+ video = pv->image(boost::bind(&PlayerVideo::force, AV_PIX_FMT_RGB24), VideoRange::FULL, true);
+ break;
+ }
/* Only the player's black frames should be aligned at this stage, so this should
* almost always have no work to do.
*/
video = Image::ensure_alignment (video, Image::Alignment::COMPACT);
- /** If _optimise_for_j2k is true we render a XYZ image, doing the colourspace
+ /** If _optimisation is J2K we render a XYZ image, doing the colourspace
* conversion, scaling and video range conversion in the GL shader.
+ * Similarly for MPEG2 we do YUV -> RGB and scaling in the shader.
* Otherwise we render a RGB image without any shader-side processing.
*/
- _video_texture->set (video);
+ if (_optimisation == Optimisation::MPEG2) {
+ for (int i = 0; i < 3; ++i) {
+ _video_textures[i]->set(video, i);
+ }
+ } else {
+ _video_textures[0]->set(video, 0);
+ }
auto const text = pv->text();
- _have_subtitle_to_render = static_cast<bool>(text) && _optimise_for_j2k;
+ _have_subtitle_to_render = static_cast<bool>(text) && _optimisation != Optimisation::NONE;
if (_have_subtitle_to_render) {
/* opt: only do this if it's a new subtitle? */
DCPOMATIC_ASSERT (text->image->alignment() == Image::Alignment::COMPACT);
@@ -707,9 +754,9 @@ GLVideoView::set_image (shared_ptr<const PlayerVideo> pv)
auto const sizing_changed = _last_canvas_size.changed() || _last_inter_position.changed() || _last_inter_size.changed() || _last_out_size.changed();
if (sizing_changed) {
- const auto video = _optimise_for_j2k ?
- Rectangle(canvas_size, inter_position.x + x_offset, inter_position.y + y_offset, inter_size)
- : Rectangle(canvas_size, x_offset, y_offset, out_size);
+ const auto video = _optimisation == Optimisation::NONE
+ ? Rectangle(canvas_size, x_offset, y_offset, out_size)
+ : Rectangle(canvas_size, inter_position.x + x_offset, inter_position.y + y_offset, inter_size);
glBufferSubData (GL_ARRAY_BUFFER, array_buffer_video_offset, video.size(), video.vertices());
check_gl_error ("glBufferSubData (video)");
@@ -739,14 +786,16 @@ GLVideoView::set_image (shared_ptr<const PlayerVideo> pv)
_rec2020 = pv->colour_conversion() && pv->colour_conversion()->about_equal(dcp::ColourConversion::rec2020_to_xyz(), 1e-6);
/* opt: where should these go? */
-
- glTexParameteri (GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
- glTexParameteri (GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
- check_gl_error ("glTexParameteri");
-
- glTexParameterf (GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
- glTexParameterf (GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
- check_gl_error ("glTexParameterf");
+ for (auto i = 0; i < 3; ++i) {
+ _video_textures[i]->bind();
+ glTexParameteri (GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
+ glTexParameteri (GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
+ check_gl_error ("glTexParameteri");
+
+ glTexParameterf (GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
+ glTexParameterf (GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
+ check_gl_error ("glTexParameterf");
+ }
}
@@ -856,8 +905,11 @@ try
_vsync_enabled = true;
#endif
- _video_texture.reset(new Texture(_optimise_for_j2k ? 2 : 1));
- _subtitle_texture.reset(new Texture(1));
+ for (int i = 0; i < 3; ++i) {
+ std::unique_ptr<Texture> texture(new Texture(_optimisation == Optimisation::JPEG2000 ? 2 : 1, i));
+ _video_textures.push_back(std::move(texture));
+ }
+ _subtitle_texture.reset(new Texture(1, 4));
while (true) {
boost::mutex::scoped_lock lm (_playing_mutex);
@@ -905,8 +957,9 @@ GLVideoView::request_one_shot ()
}
-Texture::Texture (GLint unpack_alignment)
+Texture::Texture(GLint unpack_alignment, int unit)
: _unpack_alignment (unpack_alignment)
+ , _unit(unit)
{
glGenTextures (1, &_name);
check_gl_error ("glGenTextures");
@@ -922,13 +975,15 @@ Texture::~Texture ()
void
Texture::bind ()
{
+ glActiveTexture(GL_TEXTURE0 + _unit);
+ check_gl_error("glActiveTexture");
glBindTexture(GL_TEXTURE_2D, _name);
check_gl_error ("glBindTexture");
}
void
-Texture::set (shared_ptr<const Image> image)
+Texture::set(shared_ptr<const Image> image, int component)
{
auto const create = !_size || image->size() != _size;
_size = image->size();
@@ -941,8 +996,15 @@ Texture::set (shared_ptr<const Image> image)
GLint internal_format;
GLenum format;
GLenum type;
+ int subsample = 1;
switch (image->pixel_format()) {
+ case AV_PIX_FMT_YUV420P:
+ internal_format = GL_R8;
+ format = GL_RED;
+ type = GL_UNSIGNED_BYTE;
+ subsample = component > 0 ? 2 : 1;
+ break;
case AV_PIX_FMT_BGRA:
internal_format = GL_RGBA8;
format = GL_BGRA;
@@ -970,10 +1032,10 @@ Texture::set (shared_ptr<const Image> image)
bind ();
if (create) {
- glTexImage2D (GL_TEXTURE_2D, 0, internal_format, _size->width, _size->height, 0, format, type, image->data()[0]);
+ glTexImage2D (GL_TEXTURE_2D, 0, internal_format, _size->width / subsample, _size->height / subsample, 0, format, type, image->data()[component]);
check_gl_error ("glTexImage2D");
} else {
- glTexSubImage2D (GL_TEXTURE_2D, 0, 0, 0, _size->width, _size->height, format, type, image->data()[0]);
+ glTexSubImage2D (GL_TEXTURE_2D, 0, 0, 0, _size->width / subsample, _size->height / subsample, format, type, image->data()[component]);
check_gl_error ("glTexSubImage2D");
}
}
diff --git a/src/wx/gl_video_view.h b/src/wx/gl_video_view.h
index 69de7b76f..25750b64b 100644
--- a/src/wx/gl_video_view.h
+++ b/src/wx/gl_video_view.h
@@ -51,18 +51,19 @@ LIBDCP_ENABLE_WARNINGS
class Texture
{
public:
- Texture (GLint unpack_alignment);
+ Texture(GLint unpack_alignment, int unit = 0);
~Texture ();
Texture (Texture const&) = delete;
Texture& operator= (Texture const&) = delete;
void bind ();
- void set (std::shared_ptr<const Image> image);
+ void set(std::shared_ptr<const Image> image, int component = 0);
private:
GLuint _name;
GLint _unpack_alignment;
+ int _unit;
boost::optional<dcp::Size> _size;
};
@@ -137,7 +138,7 @@ private:
boost::atomic<wxSize> _canvas_size;
boost::atomic<bool> _rec2020;
- std::unique_ptr<Texture> _video_texture;
+ std::vector<std::unique_ptr<Texture>> _video_textures;
std::unique_ptr<Texture> _subtitle_texture;
bool _have_subtitle_to_render = false;
bool _vsync_enabled;
diff --git a/src/wx/grok/gpu_config_panel.h b/src/wx/grok/gpu_config_panel.h
new file mode 100644
index 000000000..cbf037592
--- /dev/null
+++ b/src/wx/grok/gpu_config_panel.h
@@ -0,0 +1,227 @@
+/*
+ Copyright (C) 2023 Grok Image Compression Inc.
+
+ This file is part of DCP-o-matic.
+
+ DCP-o-matic is free software; you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation; either version 2 of the License, or
+ (at your option) any later version.
+
+ DCP-o-matic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with DCP-o-matic. If not, see <http://www.gnu.org/licenses/>.
+
+*/
+
+
+#pragma once
+
+static std::vector<std::string> get_gpu_names(boost::filesystem::path binary, boost::filesystem::path filename)
+{
+ // Execute the GPU listing program and redirect its output to a file
+ if (std::system((binary.string() + " > " + filename.string()).c_str()) < 0) {
+ return {};
+ }
+
+ std::vector<std::string> gpu_names;
+ std::ifstream file(filename.c_str());
+ if (file.is_open())
+ {
+ std::string line;
+ while (std::getline(file, line))
+ gpu_names.push_back(line);
+ file.close();
+ }
+
+ return gpu_names;
+}
+
+
+class GpuList : public wxPanel
+{
+public:
+ GpuList(wxPanel* parent)
+ : wxPanel(parent, wxID_ANY)
+ {
+ _combo_box = new wxComboBox(this, wxID_ANY, "", wxDefaultPosition, wxSize(400, -1));
+ _combo_box->Bind(wxEVT_COMBOBOX, &GpuList::OnComboBox, this);
+ update();
+
+ auto sizer = new wxBoxSizer(wxHORIZONTAL);
+ sizer->Add(_combo_box, 0, wxALIGN_CENTER_VERTICAL);
+ SetSizerAndFit(sizer);
+ }
+
+ void update()
+ {
+ auto grok = Config::instance()->grok().get_value_or({});
+ auto lister_binary = grok.binary_location / "gpu_lister";
+ auto lister_file = grok.binary_location / "gpus.txt";
+ if (boost::filesystem::exists(lister_binary)) {
+ auto gpu_names = get_gpu_names(lister_binary, lister_file);
+
+ _combo_box->Clear();
+ for (auto const& name: gpu_names) {
+ _combo_box->Append(name);
+ }
+ }
+ }
+
+ void set_selection(int sel)
+ {
+ if (sel < static_cast<int>(_combo_box->GetCount())) {
+ _combo_box->SetSelection(sel);
+ }
+ }
+
+private:
+ void OnComboBox(wxCommandEvent&)
+ {
+ auto selection = _combo_box->GetSelection();
+ if (selection != wxNOT_FOUND) {
+ auto grok = Config::instance()->grok().get_value_or({});
+ grok.selected = selection;
+ Config::instance()->set_grok(grok);
+ }
+ }
+
+ wxComboBox* _combo_box;
+ int _selection = 0;
+};
+
+
+class GPUPage : public Page
+{
+public:
+ GPUPage(wxSize panel_size, int border)
+ : Page(panel_size, border)
+ {}
+
+ wxString GetName() const override
+ {
+ return _("GPU");
+ }
+
+#ifdef DCPOMATIC_OSX
+ /* XXX: this icon does not exist */
+ wxBitmap GetLargeIcon() const override
+ {
+ return wxBitmap(icon_path("gpu"), wxBITMAP_TYPE_PNG);
+ }
+#endif
+
+private:
+ void setup() override
+ {
+ _enable_gpu = new CheckBox(_panel, _("Enable GPU acceleration"));
+ _panel->GetSizer()->Add(_enable_gpu, 0, wxALL | wxEXPAND, _border);
+
+ wxFlexGridSizer* table = new wxFlexGridSizer(2, DCPOMATIC_SIZER_X_GAP, DCPOMATIC_SIZER_Y_GAP);
+ table->AddGrowableCol(1, 1);
+ _panel->GetSizer()->Add(table, 1, wxALL | wxEXPAND, _border);
+
+ add_label_to_sizer(table, _panel, _("Acceleration binary folder"), true, 0, wxLEFT | wxLEFT | wxALIGN_CENTRE_VERTICAL);
+ _binary_location = new wxDirPickerCtrl(_panel, wxDD_DIR_MUST_EXIST);
+ table->Add(_binary_location, 1, wxEXPAND);
+
+ add_label_to_sizer(table, _panel, _("GPU selection"), true, 0, wxLEFT | wxRIGHT | wxALIGN_CENTRE_VERTICAL);
+ _gpu_list_control = new GpuList(_panel);
+ table->Add(_gpu_list_control, 1, wxEXPAND);
+
+ add_label_to_sizer(table, _panel, _("License server"), true, 0, wxLEFT | wxRIGHT | wxALIGN_CENTRE_VERTICAL);
+ _server = new wxTextCtrl(_panel, wxID_ANY);
+ table->Add(_server, 1, wxEXPAND | wxALL);
+
+ add_label_to_sizer(table, _panel, _("Port"), false, 0, wxLEFT | wxRIGHT | wxALIGN_CENTRE_VERTICAL);
+ _port = new wxSpinCtrl(_panel, wxID_ANY);
+ _port->SetRange(0, 65535);
+ table->Add(_port);
+
+ add_label_to_sizer(table, _panel, _("License"), true, 0, wxLEFT | wxRIGHT | wxALIGN_CENTRE_VERTICAL);
+ _licence = new PasswordEntry(_panel);
+ table->Add(_licence->get_panel(), 1, wxEXPAND | wxALL);
+
+ _enable_gpu->bind(&GPUPage::enable_gpu_changed, this);
+ _binary_location->Bind(wxEVT_DIRPICKER_CHANGED, boost::bind (&GPUPage::binary_location_changed, this));
+ _server->Bind(wxEVT_TEXT, boost::bind(&GPUPage::server_changed, this));
+ _port->Bind(wxEVT_SPINCTRL, boost::bind(&GPUPage::port_changed, this));
+ _licence->Changed.connect(boost::bind(&GPUPage::licence_changed, this));
+
+ setup_sensitivity();
+ }
+
+ void setup_sensitivity()
+ {
+ auto grok = Config::instance()->grok().get_value_or({});
+
+ _binary_location->Enable(grok.enable);
+ _gpu_list_control->Enable(grok.enable);
+ _server->Enable(grok.enable);
+ _port->Enable(grok.enable);
+ _licence->get_panel()->Enable(grok.enable);
+ }
+
+ void config_changed() override
+ {
+ auto grok = Config::instance()->grok().get_value_or({});
+
+ checked_set(_enable_gpu, grok.enable);
+ _binary_location->SetPath(std_to_wx(grok.binary_location.string()));
+ _gpu_list_control->update();
+ _gpu_list_control->set_selection(grok.selected);
+ checked_set(_server, grok.licence_server);
+ checked_set(_port, grok.licence_port);
+ checked_set(_licence, grok.licence);
+ }
+
+ void enable_gpu_changed()
+ {
+ auto grok = Config::instance()->grok().get_value_or({});
+ grok.enable = _enable_gpu->GetValue();
+ Config::instance()->set_grok(grok);
+
+ setup_sensitivity();
+ }
+
+ void binary_location_changed()
+ {
+ auto grok = Config::instance()->grok().get_value_or({});
+ grok.binary_location = wx_to_std(_binary_location->GetPath());
+ Config::instance()->set_grok(grok);
+
+ _gpu_list_control->update();
+ }
+
+ void server_changed()
+ {
+ auto grok = Config::instance()->grok().get_value_or({});
+ grok.licence_server = wx_to_std(_server->GetValue());
+ Config::instance()->set_grok(grok);
+ }
+
+ void port_changed()
+ {
+ auto grok = Config::instance()->grok().get_value_or({});
+ grok.licence_port = _port->GetValue();
+ Config::instance()->set_grok(grok);
+ }
+
+ void licence_changed()
+ {
+ auto grok = Config::instance()->grok().get_value_or({});
+ grok.licence = wx_to_std(_licence->get());
+ Config::instance()->set_grok(grok);
+ }
+
+ CheckBox* _enable_gpu = nullptr;
+ wxDirPickerCtrl* _binary_location = nullptr;
+ GpuList* _gpu_list_control = nullptr;
+ wxTextCtrl* _server = nullptr;
+ wxSpinCtrl* _port = nullptr;
+ PasswordEntry* _licence = nullptr;
+};
diff --git a/src/wx/id.h b/src/wx/id.h
index 99aa25855..4839a0868 100644
--- a/src/wx/id.h
+++ b/src/wx/id.h
@@ -24,4 +24,5 @@
#define DCPOMATIC_CPL_MENU (wxID_HIGHEST + 1000)
#define DCPOMATIC_CONTENT_MENU (wxID_HIGHEST + 1500)
#define DCPOMATIC_AUDIO_GAIN_MENU (wxID_HIGHEST + 2000)
+#define DCPOMATIC_DCP_TIMELINE_MENU (wxID_HIGHEST + 2500)
diff --git a/src/wx/job_view.cc b/src/wx/job_view.cc
index efe17f4de..d5b6c4632 100644
--- a/src/wx/job_view.cc
+++ b/src/wx/job_view.cc
@@ -25,6 +25,7 @@
#include "message_dialog.h"
#include "static_text.h"
#include "wx_util.h"
+#include "wx_variant.h"
#include "lib/analyse_audio_job.h"
#include "lib/compose.hpp"
#include "lib/config.h"
@@ -165,7 +166,7 @@ JobView::finished ()
if (_job->enable_notify() && _notify->GetValue()) {
if (Config::instance()->notification(Config::MESSAGE_BOX)) {
- wxMessageBox (std_to_wx(_job->name() + ": " + _job->status()), _("DCP-o-matic"), wxICON_INFORMATION);
+ wxMessageBox(std_to_wx(_job->name() + ": " + _job->status()), variant::wx::dcpomatic(), wxICON_INFORMATION);
}
if (Config::instance()->notification(Config::EMAIL)) {
string body = Config::instance()->notification_email();
diff --git a/src/wx/kdm_cpl_panel.cc b/src/wx/kdm_cpl_panel.cc
index 4699582aa..523d0c369 100644
--- a/src/wx/kdm_cpl_panel.cc
+++ b/src/wx/kdm_cpl_panel.cc
@@ -23,6 +23,7 @@
#include "kdm_cpl_panel.h"
#include "static_text.h"
#include "wx_util.h"
+#include <dcp/filesystem.h>
#include <dcp/warnings.h>
LIBDCP_DISABLE_WARNINGS
#include <libxml++/libxml++.h>
@@ -41,11 +42,11 @@ KDMCPLPanel::KDMCPLPanel (wxWindow* parent, vector<CPLSummary> cpls)
/* CPL choice */
auto s = new wxBoxSizer (wxHORIZONTAL);
- add_label_to_sizer (s, this, _("CPL"), true, 0, wxLEFT | wxRIGHT | wxALIGN_CENTER_VERTICAL);
+ add_label_to_sizer (s, this, _("CPL"), true, 0, wxLEFT | wxRIGHT | wxALIGN_CENTRE_VERTICAL);
_cpl = new wxChoice (this, wxID_ANY);
- s->Add (_cpl, 1, wxEXPAND);
+ s->Add (_cpl, 1, wxTOP | wxEXPAND, DCPOMATIC_CHOICE_TOP_PAD);
_cpl_browse = new Button (this, _("Browse..."));
- s->Add (_cpl_browse, 0, wxLEFT | wxALIGN_CENTER_VERTICAL, DCPOMATIC_SIZER_X_GAP);
+ s->Add (_cpl_browse, 0, wxLEFT, DCPOMATIC_SIZER_X_GAP);
vertical->Add (s, 0, wxEXPAND | wxTOP, DCPOMATIC_SIZER_GAP + 2);
/* CPL details */
diff --git a/src/wx/kdm_dialog.cc b/src/wx/kdm_dialog.cc
index 5ab13b4ce..1a9c564d4 100644
--- a/src/wx/kdm_dialog.cc
+++ b/src/wx/kdm_dialog.cc
@@ -29,6 +29,7 @@
#include "screens_panel.h"
#include "static_text.h"
#include "wx_util.h"
+#include "wx_variant.h"
#include "lib/cinema.h"
#include "lib/config.h"
#include "lib/film.h"
@@ -125,7 +126,7 @@ KDMDialog::KDMDialog (wxWindow* parent, shared_ptr<const Film> film)
/* Bind */
- _screens->ScreensChanged.connect (boost::bind (&KDMDialog::setup_sensitivity, this));
+ _screens->ScreensChanged.connect(boost::bind(&KDMDialog::screens_changed, this));
_timing->TimingChanged.connect (boost::bind (&KDMDialog::setup_sensitivity, this));
_make->Bind (wxEVT_BUTTON, boost::bind (&KDMDialog::make_clicked, this));
_cpl->Changed.connect(boost::bind(&KDMDialog::cpl_changed, this));
@@ -140,6 +141,14 @@ KDMDialog::KDMDialog (wxWindow* parent, shared_ptr<const Film> film)
void
+KDMDialog::screens_changed()
+{
+ _timing->suggest_utc_offset(_screens->best_utc_offset());
+ setup_sensitivity();
+}
+
+
+void
KDMDialog::cpl_changed()
{
try {
@@ -196,8 +205,21 @@ KDMDialog::make_clicked ()
return film->make_kdm(_cpl->cpl(), begin, end);
};
- for (auto i: _screens->screens()) {
- auto p = kdm_for_screen(make_kdm, i, _timing->from(), _timing->until(), _output->formulation(), !_output->forensic_mark_video(), for_audio, period_checks);
+ CinemaList cinemas;
+
+ for (auto screen: _screens->screens()) {
+ auto p = kdm_for_screen(
+ make_kdm,
+ screen.first,
+ *cinemas.cinema(screen.first),
+ *cinemas.screen(screen.second),
+ _timing->from(),
+ _timing->until(),
+ _output->formulation(),
+ !_output->forensic_mark_video(),
+ for_audio,
+ period_checks
+ );
if (p) {
kdms.push_back (p);
}
@@ -219,7 +241,13 @@ KDMDialog::make_clicked ()
if (e.starts_too_early()) {
error_dialog (this, _("The KDM start period is before (or close to) the start of the signing certificate's validity period. Use a later start time for this KDM."));
} else {
- error_dialog (this, _("The KDM end period is after (or close to) the end of the signing certificates' validity period. Either use an earlier end time for this KDM or re-create your signing certificates in the DCP-o-matic preferences window."));
+ error_dialog(
+ this,
+ variant::wx::insert_dcpomatic(
+ _("The KDM end period is after (or close to) the end of the signing certificates' validity "
+ "period. Either use an earlier end time for this KDM or re-create your signing certificates "
+ "in the %s preferences window."))
+ );
}
return;
} catch (runtime_error& e) {
diff --git a/src/wx/kdm_dialog.h b/src/wx/kdm_dialog.h
index c1e9588fe..ec92db616 100644
--- a/src/wx/kdm_dialog.h
+++ b/src/wx/kdm_dialog.h
@@ -52,6 +52,7 @@ private:
void make_clicked ();
bool confirm_overwrite (boost::filesystem::path path);
void cpl_changed();
+ void screens_changed();
std::weak_ptr<const Film> _film;
ScreensPanel* _screens;
diff --git a/src/wx/kdm_timing_panel.cc b/src/wx/kdm_timing_panel.cc
index 0fd00de93..c892c9d48 100644
--- a/src/wx/kdm_timing_panel.cc
+++ b/src/wx/kdm_timing_panel.cc
@@ -19,11 +19,13 @@
*/
+#include "dcpomatic_choice.h"
#include "kdm_timing_panel.h"
#include "static_text.h"
#include "time_picker.h"
#include "wx_util.h"
#include "lib/config.h"
+#include <dcp/utc_offset.h>
#include <dcp/warnings.h>
LIBDCP_DISABLE_WARNINGS
#include <wx/datectrl.h>
@@ -105,6 +107,10 @@ KDMTimingPanel::KDMTimingPanel (wxWindow* parent)
table->Add (_until_time, 0, wxALIGN_CENTER_VERTICAL);
+ add_label_to_sizer(table, this, _("UTC offset (time zone)"), true, 1, wxALIGN_CENTRE_VERTICAL);
+ _utc_offset = new Choice(this);
+ table->Add(_utc_offset, 0, wxALIGN_CENTRE_VERTICAL | wxLEFT, DCPOMATIC_SIZER_X_GAP);
+
overall_sizer->Add (table, 0, wxTOP, DCPOMATIC_SIZER_GAP);
_warning = new StaticText (this, wxT(""));
@@ -115,6 +121,13 @@ KDMTimingPanel::KDMTimingPanel (wxWindow* parent)
_warning->SetForegroundColour (wxColour (255, 0, 0));
_warning->SetFont(font);
+ /* Default to UTC */
+ auto const sel = get_offsets(_offsets);
+ for (auto const& offset: _offsets) {
+ _utc_offset->add_entry(offset.name);
+ }
+ _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 +138,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::utc_offset_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 +191,38 @@ KDMTimingPanel::changed () const
TimingChanged ();
}
+
+
+dcp::UTCOffset
+KDMTimingPanel::utc_offset() const
+{
+ auto const sel = _utc_offset->get();
+ if (!sel || *sel >= int(_offsets.size())) {
+ return {};
+ }
+
+ return _offsets[*sel].offset;
+}
+
+
+void
+KDMTimingPanel::utc_offset_changed()
+{
+ _utc_offset_changed_once = true;
+ changed();
+}
+
+
+void
+KDMTimingPanel::suggest_utc_offset(dcp::UTCOffset offset)
+{
+ if (!_utc_offset_changed_once) {
+ for (size_t i = 0; i < _offsets.size(); ++i) {
+ if (_offsets[i].offset == offset) {
+ _utc_offset->set(i);
+ break;
+ }
+ }
+ }
+}
+
diff --git a/src/wx/kdm_timing_panel.h b/src/wx/kdm_timing_panel.h
index 7221ba722..a6199534a 100644
--- a/src/wx/kdm_timing_panel.h
+++ b/src/wx/kdm_timing_panel.h
@@ -18,6 +18,9 @@
*/
+
+#include "wx_util.h"
+#include <dcp/utc_offset.h>
#include <dcp/warnings.h>
LIBDCP_DISABLE_WARNINGS
#include <wx/wx.h>
@@ -25,30 +28,42 @@ 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;
+ /** Give a UTC offset from a cinema that the user just selected. If the user
+ * never changed the UTC offset in the panel, the suggested UTC will be set.
+ */
+ void suggest_utc_offset(dcp::UTCOffset offset);
+
boost::signals2::signal<void ()> TimingChanged;
private:
void changed () const;
- static boost::posix_time::ptime posix_time (wxDatePickerCtrl *, TimePicker *);
+ void utc_offset_changed();
+ 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;
+ bool _utc_offset_changed_once = false;
wxStaticText* _warning;
+ std::vector<Offset> _offsets;
};
diff --git a/src/wx/load_config_from_zip_dialog.cc b/src/wx/load_config_from_zip_dialog.cc
new file mode 100644
index 000000000..a7d573ded
--- /dev/null
+++ b/src/wx/load_config_from_zip_dialog.cc
@@ -0,0 +1,59 @@
+/*
+ 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 "load_config_from_zip_dialog.h"
+#include "wx_util.h"
+#include "lib/config.h"
+#include "lib/unzipper.h"
+
+
+LoadConfigFromZIPDialog::LoadConfigFromZIPDialog(wxWindow* parent, boost::filesystem::path zip_file)
+ : TableDialog(parent, _("Load configuration from ZIP file"), 1, 0, true)
+{
+ _use_current = add(new wxRadioButton(this, wxID_ANY, _("Copy the cinemas in the ZIP file over the current list at")));
+ auto current_path = add(new wxStaticText(this, wxID_ANY, std_to_wx(Config::instance()->cinemas_file().string())));
+ auto current_path_font = current_path->GetFont();
+ current_path_font.SetFamily(wxFONTFAMILY_TELETYPE);
+ current_path->SetFont(current_path_font);
+
+ _use_zip = add(new wxRadioButton(this, wxID_ANY, _("Copy the cinemas in the ZIP file to the original location at")));
+ auto zip_path = add(new wxStaticText(this, wxID_ANY, std_to_wx(Config::cinemas_file_from_zip(zip_file).string())));
+ auto zip_path_font = zip_path->GetFont();
+ zip_path_font.SetFamily(wxFONTFAMILY_TELETYPE);
+ zip_path->SetFont(zip_path_font);
+
+ _ignore = add(new wxRadioButton(this, wxID_ANY, _("Do not use the cinemas in the ZIP file")));
+
+ layout();
+}
+
+
+Config::CinemasAction
+LoadConfigFromZIPDialog::action() const
+{
+ if (_use_current->GetValue()) {
+ return Config::CinemasAction::WRITE_TO_CURRENT_PATH;
+ } else if (_use_zip->GetValue()) {
+ return Config::CinemasAction::WRITE_TO_PATH_IN_ZIPPED_CONFIG;
+ }
+
+ return Config::CinemasAction::IGNORE;
+}
diff --git a/src/wx/load_config_from_zip_dialog.h b/src/wx/load_config_from_zip_dialog.h
new file mode 100644
index 000000000..f5f4ec6ea
--- /dev/null
+++ b/src/wx/load_config_from_zip_dialog.h
@@ -0,0 +1,41 @@
+/*
+ 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 "table_dialog.h"
+#include "lib/config.h"
+#include <boost/filesystem.hpp>
+
+
+class LoadConfigFromZIPDialog : public TableDialog
+{
+public:
+ LoadConfigFromZIPDialog(wxWindow* parent, boost::filesystem::path zip_file);
+
+ Config::CinemasAction action() const;
+
+private:
+ wxRadioButton* _use_current;
+ wxRadioButton* _use_zip;
+ wxRadioButton* _ignore;
+};
+
+
+
diff --git a/src/wx/metadata_dialog.cc b/src/wx/metadata_dialog.cc
index 347f2fffd..1d0758175 100644
--- a/src/wx/metadata_dialog.cc
+++ b/src/wx/metadata_dialog.cc
@@ -204,9 +204,9 @@ MetadataDialog::setup_standard (wxPanel* panel, wxSizer* sizer)
{
add_label_to_sizer(sizer, panel, _("Territory type"), true, 0, wxALIGN_CENTER_VERTICAL);
_territory_type = new Choice(panel);
- _territory_type->add(_("Specific"), wx_to_std(territory_type_to_string(TerritoryType::SPECIFIC)));
- _territory_type->add(_("International texted"), wx_to_std(territory_type_to_string(TerritoryType::INTERNATIONAL_TEXTED)));
- _territory_type->add(_("International textless"), wx_to_std(territory_type_to_string(TerritoryType::INTERNATIONAL_TEXTLESS)));
+ _territory_type->add_entry(_("Specific"), wx_to_std(territory_type_to_string(TerritoryType::SPECIFIC)));
+ _territory_type->add_entry(_("International texted"), wx_to_std(territory_type_to_string(TerritoryType::INTERNATIONAL_TEXTED)));
+ _territory_type->add_entry(_("International textless"), wx_to_std(territory_type_to_string(TerritoryType::INTERNATIONAL_TEXTLESS)));
sizer->Add(_territory_type);
_enable_release_territory = new CheckBox(panel, _("Release territory"));
@@ -330,8 +330,8 @@ MetadataDialog::setup_advanced (wxPanel* panel, wxSizer* sizer)
sizer->Add (s, 1, wxEXPAND);
}
- _luminance_unit->add(_("candela per m²"));
- _luminance_unit->add(_("foot lambert"));
+ _luminance_unit->add_entry(_("candela per m²"));
+ _luminance_unit->add_entry(_("foot lambert"));
}
diff --git a/src/wx/optimisation.h b/src/wx/optimisation.h
new file mode 100644
index 000000000..5d5a0197f
--- /dev/null
+++ b/src/wx/optimisation.h
@@ -0,0 +1,37 @@
+/*
+ Copyright (C) 2019-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_OPTIMISATION_H
+#define DCPOMATIC_OPTIMISATION_H
+
+
+/** An optimisation that can be requested of the FilmViewer if it is known that
+ * all content will be the same type.
+ */
+enum class Optimisation
+{
+ JPEG2000, ///< all content is JPEG2000
+ MPEG2, ///< all content is Interop MPEG2
+ NONE, ///< viewer must be prepared for anything
+};
+
+
+#endif
diff --git a/src/wx/player_config_dialog.cc b/src/wx/player_config_dialog.cc
index 58874e50b..d7bba4dcc 100644
--- a/src/wx/player_config_dialog.cc
+++ b/src/wx/player_config_dialog.cc
@@ -37,6 +37,7 @@
#include "server_dialog.h"
#include "static_text.h"
#include "wx_util.h"
+#include "wx_variant.h"
#include "lib/config.h"
#include "lib/cross.h"
#include "lib/dcp_content_type.h"
@@ -114,7 +115,7 @@ private:
table->Add (_video_display_mode, wxGBPosition(r, 1));
++r;
- wxStaticText* restart = add_label_to_sizer (table, _panel, _("(restart DCP-o-matic to change display mode)"), false, wxGBPosition(r, 0));
+ wxStaticText* restart = add_label_to_sizer(table, _panel, variant::wx::insert_dcpomatic_player(_("(restart %s to change display mode)")), false, wxGBPosition(r, 0));
wxFont font = restart->GetFont();
font.SetStyle (wxFONTSTYLE_ITALIC);
font.SetPointSize (font.GetPointSize() - 1);
@@ -353,7 +354,7 @@ private:
wxPreferencesEditor*
create_player_config_dialog ()
{
- auto e = new wxPreferencesEditor (_("DCP-o-matic Player Preferences"));
+ auto e = new wxPreferencesEditor(variant::wx::insert_dcpomatic_player(_("%s Preferences")));
#ifdef DCPOMATIC_OSX
/* Width that we force some of the config panels to be on OSX so that
diff --git a/src/wx/playlist_controls.cc b/src/wx/playlist_controls.cc
index 3a1bba362..f51bcc4e2 100644
--- a/src/wx/playlist_controls.cc
+++ b/src/wx/playlist_controls.cc
@@ -25,6 +25,7 @@
#include "playlist_controls.h"
#include "static_text.h"
#include "wx_util.h"
+#include "wx_variant.h"
#include "lib/compose.hpp"
#include "lib/constants.h"
#include "lib/cross.h"
@@ -331,7 +332,7 @@ PlaylistControls::spl_selection_changed ()
void
PlaylistControls::select_playlist (int selected, int position)
{
- wxProgressDialog dialog (_("DCP-o-matic"), "Loading playlist and KDMs");
+ wxProgressDialog dialog(variant::wx::dcpomatic(), "Loading playlist and KDMs");
for (auto const& i: _playlists[selected].get()) {
dialog.Pulse ();
@@ -403,7 +404,7 @@ PlaylistControls::update_current_content ()
{
DCPOMATIC_ASSERT (_selected_playlist);
- wxProgressDialog dialog (_("DCP-o-matic"), "Loading content");
+ wxProgressDialog dialog(variant::wx::dcpomatic(), "Loading content");
setup_sensitivity ();
dialog.Pulse ();
diff --git a/src/wx/playlist_editor_config_dialog.cc b/src/wx/playlist_editor_config_dialog.cc
index d5c4106ea..67bedc05e 100644
--- a/src/wx/playlist_editor_config_dialog.cc
+++ b/src/wx/playlist_editor_config_dialog.cc
@@ -18,8 +18,11 @@
*/
-#include "playlist_editor_config_dialog.h"
+
#include "config_dialog.h"
+#include "playlist_editor_config_dialog.h"
+#include "wx_variant.h"
+
/** @file src/playlist_editor_config_dialog.cc
* @brief A dialogue to edit DCP-o-matic Playlist Editor configuration.
@@ -28,7 +31,7 @@
wxPreferencesEditor*
create_playlist_editor_config_dialog ()
{
- auto e = new wxPreferencesEditor (_("DCP-o-matic Playlist Editor Preferences"));
+ auto e = new wxPreferencesEditor(variant::wx::insert_dcpomatic_playlist_editor(_("%s Preferences")));
#ifdef DCPOMATIC_OSX
/* Width that we force some of the config panels to be on OSX so that
diff --git a/src/wx/recipient_dialog.cc b/src/wx/recipient_dialog.cc
index a69adb169..992c88362 100644
--- a/src/wx/recipient_dialog.cc
+++ b/src/wx/recipient_dialog.cc
@@ -55,7 +55,7 @@ column (string s)
RecipientDialog::RecipientDialog (
- wxWindow* parent, wxString title, string name, string notes, vector<string> emails, int utc_offset_hour, int utc_offset_minute, optional<dcp::Certificate> recipient
+ wxWindow* parent, wxString title, string name, string notes, vector<string> emails, optional<dcp::Certificate> recipient
)
: wxDialog (parent, wxID_ANY, title)
, _recipient (recipient)
@@ -76,11 +76,6 @@ RecipientDialog::RecipientDialog (
_sizer->Add (_notes, wxGBPosition (r, 1));
++r;
- add_label_to_sizer (_sizer, this, _("UTC offset (time zone)"), true, wxGBPosition (r, 0));
- _utc_offset = new wxChoice (this, wxID_ANY);
- _sizer->Add (_utc_offset, wxGBPosition (r, 1));
- ++r;
-
add_label_to_sizer (_sizer, this, _("Email addresses for KDM delivery"), false, wxGBPosition (r, 0), wxGBSpan (1, 2));
++r;
@@ -125,17 +120,6 @@ RecipientDialog::RecipientDialog (
overall_sizer->Add (buttons, wxSizerFlags().Expand().DoubleBorder());
}
- /* Default to UTC */
- size_t sel = get_offsets (_offsets);
- for (size_t i = 0; i < _offsets.size(); ++i) {
- _utc_offset->Append (_offsets[i].name);
- if (_offsets[i].hour == utc_offset_hour && _offsets[i].minute == utc_offset_minute) {
- sel = i;
- }
- }
-
- _utc_offset->SetSelection (sel);
-
overall_sizer->Layout ();
overall_sizer->SetSizeHints (this);
@@ -234,28 +218,3 @@ RecipientDialog::emails () const
{
return _emails;
}
-
-
-int
-RecipientDialog::utc_offset_hour () const
-{
- int const sel = _utc_offset->GetSelection();
- if (sel < 0 || sel > int (_offsets.size())) {
- return 0;
- }
-
- return _offsets[sel].hour;
-}
-
-int
-RecipientDialog::utc_offset_minute () const
-{
- int const sel = _utc_offset->GetSelection();
- if (sel < 0 || sel > int (_offsets.size())) {
- return 0;
- }
-
- return _offsets[sel].minute;
-}
-
-
diff --git a/src/wx/recipient_dialog.h b/src/wx/recipient_dialog.h
index a46e67af5..61f6b3031 100644
--- a/src/wx/recipient_dialog.h
+++ b/src/wx/recipient_dialog.h
@@ -44,8 +44,6 @@ public:
std::string name = "",
std::string notes = "",
std::vector<std::string> emails = std::vector<std::string>(),
- int utc_offset_hour = 0,
- int utc_offset_minute = 0,
boost::optional<dcp::Certificate> c = boost::optional<dcp::Certificate>()
);
@@ -53,8 +51,6 @@ public:
std::string notes () const;
boost::optional<dcp::Certificate> recipient () const;
std::vector<std::string> emails () const;
- int utc_offset_hour () const;
- int utc_offset_minute () const;
private:
void get_recipient_from_file ();
@@ -71,8 +67,6 @@ private:
wxButton* _get_recipient_from_file;
EditableList<std::string, EmailDialog>* _email_list;
std::vector<std::string> _emails;
- wxChoice* _utc_offset;
- std::vector<Offset> _offsets;
boost::optional<dcp::Certificate> _recipient;
};
diff --git a/src/wx/recipients_panel.cc b/src/wx/recipients_panel.cc
index 485a0f94e..9596c3cfd 100644
--- a/src/wx/recipients_panel.cc
+++ b/src/wx/recipients_panel.cc
@@ -23,7 +23,7 @@
#include "wx_util.h"
#include "recipient_dialog.h"
#include "dcpomatic_button.h"
-#include "lib/config.h"
+#include "lib/dkdm_recipient_list.h"
#include <list>
#include <iostream>
@@ -103,15 +103,15 @@ RecipientsPanel::setup_sensitivity ()
void
-RecipientsPanel::add_recipient (shared_ptr<DKDMRecipient> r)
+RecipientsPanel::add_recipient(DKDMRecipientID id, DKDMRecipient const& recipient)
{
string const search = wx_to_std(_search->GetValue());
- if (!search.empty() && !_collator.find(search, r->name)) {
+ if (!search.empty() && !_collator.find(search, recipient.name)) {
return;
}
- _recipients[_targets->AppendItem(_root, std_to_wx(r->name))] = r;
+ _recipients.emplace(make_pair(_targets->AppendItem(_root, std_to_wx(recipient.name)), id));
_targets->SortChildren (_root);
}
@@ -122,9 +122,10 @@ 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());
- Config::instance()->add_dkdm_recipient (r);
- add_recipient (r);
+ auto recipient = DKDMRecipient(dialog.name(), dialog.notes(), dialog.recipient(), dialog.emails());
+ DKDMRecipientList recipient_list;
+ auto const id = recipient_list.add_dkdm_recipient(recipient);
+ add_recipient(id, recipient);
}
}
@@ -136,20 +137,28 @@ RecipientsPanel::edit_recipient_clicked ()
return;
}
- auto c = *_selected.begin();
+ DKDMRecipientList recipients;
+ auto selection = *_selected.begin();
+ auto const recipient_id = selection.second;
+ auto recipient = recipients.dkdm_recipient(recipient_id);
+ DCPOMATIC_ASSERT(recipient);
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"),
+ recipient->name,
+ recipient->notes,
+ recipient->emails,
+ recipient->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);
+ recipient->name = dialog.name();
+ recipient->emails = dialog.emails();
+ recipient->notes = dialog.notes();
+ recipient->recipient = dialog.recipient();
+ recipients.update_dkdm_recipient(recipient_id, *recipient);
+ _targets->SetItemText(selection.first, std_to_wx(dialog.name()));
}
}
@@ -158,7 +167,8 @@ void
RecipientsPanel::remove_recipient_clicked ()
{
for (auto const& i: _selected) {
- Config::instance()->remove_dkdm_recipient (i.second);
+ DKDMRecipientList recipient_list;
+ recipient_list.remove_dkdm_recipient(i.second);
_targets->Delete (i.first);
}
@@ -166,19 +176,15 @@ RecipientsPanel::remove_recipient_clicked ()
}
-list<shared_ptr<DKDMRecipient>>
+list<DKDMRecipient>
RecipientsPanel::recipients () const
{
- list<shared_ptr<DKDMRecipient>> r;
-
- for (auto const& i: _selected) {
- r.push_back (i.second);
+ list<DKDMRecipient> all;
+ DKDMRecipientList recipients;
+ for (auto const& recipient: recipients.dkdm_recipients()) {
+ all.push_back(recipient.second);
}
-
- r.sort ();
- r.unique ();
-
- return r;
+ return all;
}
@@ -204,7 +210,7 @@ RecipientsPanel::selection_changed ()
for (size_t i = 0; i < s.GetCount(); ++i) {
RecipientMap::const_iterator j = _recipients.find (s[i]);
if (j != _recipients.end ()) {
- _selected[j->first] = j->second;
+ _selected.emplace(*j);
}
}
@@ -218,8 +224,9 @@ RecipientsPanel::add_recipients ()
{
_root = _targets->AddRoot ("Foo");
- for (auto i: Config::instance()->dkdm_recipients()) {
- add_recipient (i);
+ DKDMRecipientList recipients;
+ for (auto const& recipient: recipients.dkdm_recipients()) {
+ add_recipient(recipient.first, recipient.second);
}
}
diff --git a/src/wx/recipients_panel.h b/src/wx/recipients_panel.h
index 6e1f1408f..d252b8d06 100644
--- a/src/wx/recipients_panel.h
+++ b/src/wx/recipients_panel.h
@@ -21,6 +21,7 @@
#include "lib/collator.h"
#include "lib/dkdm_recipient.h"
+#include "lib/dkdm_recipient_list.h"
#include <dcp/warnings.h>
LIBDCP_DISABLE_WARNINGS
#include <wx/srchctrl.h>
@@ -43,12 +44,12 @@ public:
void setup_sensitivity ();
- std::list<std::shared_ptr<DKDMRecipient>> recipients () const;
+ std::list<DKDMRecipient> recipients() const;
boost::signals2::signal<void ()> RecipientsChanged;
private:
void add_recipients ();
- void add_recipient (std::shared_ptr<DKDMRecipient>);
+ void add_recipient(DKDMRecipientID id, DKDMRecipient const& recipient);
void add_recipient_clicked ();
void edit_recipient_clicked ();
void remove_recipient_clicked ();
@@ -63,7 +64,7 @@ private:
wxButton* _remove_recipient;
wxTreeItemId _root;
- typedef std::map<wxTreeItemId, std::shared_ptr<DKDMRecipient>> RecipientMap;
+ typedef std::map<wxTreeItemId, DKDMRecipientID> RecipientMap;
RecipientMap _recipients;
RecipientMap _selected;
diff --git a/src/wx/save_template_dialog.cc b/src/wx/save_template_dialog.cc
index 6a6644efe..0917387c9 100644
--- a/src/wx/save_template_dialog.cc
+++ b/src/wx/save_template_dialog.cc
@@ -18,54 +18,90 @@
*/
+
+#include "dcpomatic_choice.h"
#include "save_template_dialog.h"
#include "wx_util.h"
#include "lib/config.h"
+#include "lib/constants.h"
+
using std::string;
#if BOOST_VERSION >= 106100
using namespace boost::placeholders;
#endif
+
+using boost::optional;
+
+
SaveTemplateDialog::SaveTemplateDialog (wxWindow* parent)
: TableDialog (parent, _("Save template"), 2, 1, true)
{
- add (_("Template name"), true);
- _name = add (new wxTextCtrl (this, wxID_ANY, wxEmptyString, wxDefaultPosition, wxSize (300, -1)));
- _name->SetFocus ();
+ _default = add(new wxRadioButton(this, wxID_ANY, _("Save as default")));
+ add_spacer();
+ _existing = add(new wxRadioButton(this, wxID_ANY, _("Save over existing template")));
+ _existing_name = add(new Choice(this));
+ _new = add(new wxRadioButton(this, wxID_ANY, _("Save as new with name")));
+ _new_name = add(new wxTextCtrl(this, wxID_ANY, wxEmptyString, wxDefaultPosition, wxSize(300, -1)));
+
+ _default->SetFocus ();
layout ();
- wxButton* ok = dynamic_cast<wxButton *> (FindWindowById (wxID_OK, this));
+ auto ok = dynamic_cast<wxButton *>(FindWindowById(wxID_OK, this));
ok->Bind (wxEVT_BUTTON, boost::bind(&SaveTemplateDialog::check, this, _1));
- _name->Bind (wxEVT_TEXT, boost::bind(&SaveTemplateDialog::setup_sensitivity, this));
+ _default->Bind(wxEVT_RADIOBUTTON, boost::bind(&SaveTemplateDialog::setup_sensitivity, this));
+ _existing->Bind(wxEVT_RADIOBUTTON, boost::bind(&SaveTemplateDialog::setup_sensitivity, this));
+ _new->Bind(wxEVT_RADIOBUTTON, boost::bind(&SaveTemplateDialog::setup_sensitivity, this));
+ _existing_name->Bind(wxEVT_TEXT, boost::bind(&SaveTemplateDialog::setup_sensitivity, this));
setup_sensitivity ();
+
+ for (auto name: Config::instance()->templates()) {
+ _existing_name->add_entry(name);
+ }
}
void
SaveTemplateDialog::setup_sensitivity ()
{
- wxButton* ok = dynamic_cast<wxButton *>(FindWindowById(wxID_OK, this));
+ auto const have_templates = !Config::instance()->templates().empty();
+ _existing->Enable(have_templates);
+ _existing_name->Enable(have_templates && _existing->GetValue());
+ _new_name->Enable(_new->GetValue());
+
+ auto ok = dynamic_cast<wxButton *>(FindWindowById(wxID_OK, this));
if (ok) {
- ok->Enable (!_name->GetValue().IsEmpty());
+ ok->Enable(_default->GetValue() || (_existing->GetValue() && have_templates) || (_new->GetValue() && !_new_name->GetValue().IsEmpty()));
}
}
-string
+optional<string>
SaveTemplateDialog::name () const
{
- return wx_to_std (_name->GetValue ());
+ if (_default->GetValue()) {
+ return {};
+ } else if (_existing->GetValue()) {
+ DCPOMATIC_ASSERT(_existing_name->get());
+ auto index = *_existing_name->get();
+ auto templates = Config::instance()->templates();
+ DCPOMATIC_ASSERT(index < int(templates.size()));
+ return templates[index];
+ } else {
+ return wx_to_std(_new_name->GetValue());
+ }
}
+
void
SaveTemplateDialog::check (wxCommandEvent& ev)
{
bool ok = true;
- if (Config::instance()->existing_template (wx_to_std (_name->GetValue ()))) {
+ if (_new->GetValue() && Config::instance()->existing_template(wx_to_std(_new_name->GetValue()))) {
ok = confirm_dialog (this, _("There is already a template with this name. Do you want to overwrite it?"));
}
diff --git a/src/wx/save_template_dialog.h b/src/wx/save_template_dialog.h
index 7e4808ca2..101d35ddf 100644
--- a/src/wx/save_template_dialog.h
+++ b/src/wx/save_template_dialog.h
@@ -18,18 +18,26 @@
*/
+
#include "table_dialog.h"
+class Choice;
+
+
class SaveTemplateDialog : public TableDialog
{
public:
explicit SaveTemplateDialog (wxWindow* parent);
- std::string name () const;
+ boost::optional<std::string> name() const;
private:
void setup_sensitivity ();
void check (wxCommandEvent& ev);
- wxTextCtrl* _name;
+ wxRadioButton* _default;
+ wxRadioButton* _existing;
+ Choice* _existing_name;
+ wxRadioButton* _new;
+ wxTextCtrl* _new_name;
};
diff --git a/src/wx/screens_panel.cc b/src/wx/screens_panel.cc
index d3b1db77d..fdc4dacc3 100644
--- a/src/wx/screens_panel.cc
+++ b/src/wx/screens_panel.cc
@@ -25,6 +25,7 @@
#include "screen_dialog.h"
#include "screens_panel.h"
#include "wx_util.h"
+#include "wx_variant.h"
#include "lib/cinema.h"
#include "lib/config.h"
#include "lib/screen.h"
@@ -125,8 +126,6 @@ ScreensPanel::ScreensPanel (wxWindow* parent)
_uncheck_all->Bind (wxEVT_BUTTON, boost::bind(&ScreensPanel::uncheck_all, this));
SetSizer(_overall_sizer);
-
- _config_connection = Config::instance()->Changed.connect(boost::bind(&ScreensPanel::config_changed, this, _1));
}
@@ -188,28 +187,37 @@ ScreensPanel::convert_to_lower(string& s)
bool
-ScreensPanel::matches_search(shared_ptr<const Cinema> cinema, string search)
+ScreensPanel::matches_search(Cinema const& cinema, string search)
{
if (search.empty()) {
return true;
}
- return _collator.find(search, cinema->name);
+ return _collator.find(search, cinema.name);
}
+/** Add an existing cinema to the GUI */
optional<wxTreeListItem>
-ScreensPanel::add_cinema (shared_ptr<Cinema> cinema, wxTreeListItem previous)
+ScreensPanel::add_cinema(CinemaID cinema_id, wxTreeListItem previous)
{
+ CinemaList cinemas;
+ auto cinema = cinemas.cinema(cinema_id);
+ DCPOMATIC_ASSERT(cinema);
+
auto const search = wx_to_std(_search->GetValue());
- if (!matches_search(cinema, search)) {
+ if (!matches_search(*cinema, search)) {
return {};
}
+ auto screens = cinemas.screens(cinema_id);
+
if (_show_only_checked->get()) {
- auto screens = cinema->screens();
- auto iter = std::find_if(screens.begin(), screens.end(), [this](shared_ptr<dcpomatic::Screen> screen) {
- return _checked_screens.find(screen) != _checked_screens.end();
+ auto iter = std::find_if(screens.begin(), screens.end(), [this](pair<ScreenID, dcpomatic::Screen> const& screen) {
+ auto iter2 = std::find_if(_checked_screens.begin(), _checked_screens.end(), [screen](pair<CinemaID, ScreenID> const& checked) {
+ return checked.second == screen.first;
+ });
+ return iter2 != _checked_screens.end();
});
if (iter == screens.end()) {
return {};
@@ -218,29 +226,34 @@ ScreensPanel::add_cinema (shared_ptr<Cinema> cinema, wxTreeListItem previous)
auto id = _targets->InsertItem(_targets->GetRootItem(), previous, std_to_wx(cinema->name));
- _item_to_cinema[id] = cinema;
- _cinema_to_item[cinema] = id;
+ _item_to_cinema.emplace(make_pair(id, cinema_id));
+ _cinema_to_item[cinema_id] = id;
- for (auto screen: cinema->screens()) {
- add_screen (cinema, screen);
+ for (auto screen: screens) {
+ add_screen(cinema_id, screen.first);
}
return id;
}
+/** Add an existing screen to the GUI */
optional<wxTreeListItem>
-ScreensPanel::add_screen (shared_ptr<Cinema> cinema, shared_ptr<Screen> screen)
+ScreensPanel::add_screen(CinemaID cinema_id, ScreenID screen_id)
{
- auto item = cinema_to_item(cinema);
+ auto item = cinema_to_item(cinema_id);
if (!item) {
return {};
}
+ CinemaList cinemas;
+ auto screen = cinemas.screen(screen_id);
+ DCPOMATIC_ASSERT(screen);
+
auto id = _targets->AppendItem(*item, std_to_wx(screen->name));
- _item_to_screen[id] = screen;
- _screen_to_item[screen] = id;
+ _item_to_screen.emplace(make_pair(id, make_pair(cinema_id, screen_id)));
+ _screen_to_item[screen_id] = id;
return item;
}
@@ -252,37 +265,31 @@ 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 = Cinema(dialog.name(), dialog.emails(), dialog.notes(), dialog.utc_offset());
- auto cinemas = sorted_cinemas();
+ CinemaList cinemas;
+ auto existing_cinemas = cinemas.cinemas();
- try {
- _ignore_cinemas_changed = true;
- dcp::ScopeGuard sg = [this]() { _ignore_cinemas_changed = false; };
- Config::instance()->add_cinema(cinema);
- } catch (FileError& e) {
- error_dialog(GetParent(), _("Could not write cinema details to the cinemas.xml file. Check that the location of cinemas.xml is valid in DCP-o-matic's preferences."), std_to_wx(e.what()));
- return;
- }
+ auto const cinema_id = cinemas.add_cinema(cinema);
wxTreeListItem previous = wxTLI_FIRST;
bool found = false;
auto const search = wx_to_std(_search->GetValue());
- for (auto existing_cinema: cinemas) {
- if (!matches_search(existing_cinema, search)) {
+ for (auto existing_cinema: existing_cinemas) {
+ if (!matches_search(existing_cinema.second, search)) {
continue;
}
- if (_collator.compare(dialog.name(), existing_cinema->name) < 0) {
+ if (_collator.compare(dialog.name(), existing_cinema.second.name) < 0) {
/* existing_cinema should be after the one we're inserting */
found = true;
break;
}
- auto item = cinema_to_item(existing_cinema);
+ auto item = cinema_to_item(existing_cinema.first);
DCPOMATIC_ASSERT(item);
previous = *item;
}
- auto item = add_cinema(cinema, found ? previous : wxTLI_LAST);
+ auto item = add_cinema(cinema_id, found ? previous : wxTLI_LAST);
if (item) {
_targets->UnselectAll ();
@@ -294,13 +301,13 @@ ScreensPanel::add_cinema_clicked ()
}
-shared_ptr<Cinema>
+optional<CinemaID>
ScreensPanel::cinema_for_operation () const
{
if (_selected_cinemas.size() == 1) {
return _selected_cinemas[0];
} else if (_selected_screens.size() == 1) {
- return _selected_screens[0]->cinema;
+ return _selected_screens[0].first;
}
return {};
@@ -310,28 +317,29 @@ ScreensPanel::cinema_for_operation () const
void
ScreensPanel::edit_cinema_clicked ()
{
- auto cinema = cinema_for_operation ();
- if (cinema) {
- edit_cinema(cinema);
+ auto cinema_id = cinema_for_operation();
+ if (cinema_id) {
+ edit_cinema(*cinema_id);
}
}
void
-ScreensPanel::edit_cinema(shared_ptr<Cinema> cinema)
+ScreensPanel::edit_cinema(CinemaID cinema_id)
{
- CinemaDialog dialog(
- GetParent(), _("Edit cinema"), cinema->name, cinema->emails, cinema->notes, cinema->utc_offset_hour(), cinema->utc_offset_minute()
- );
+ CinemaList cinemas;
+ auto cinema = cinemas.cinema(cinema_id);
+ DCPOMATIC_ASSERT(cinema);
+
+ CinemaDialog dialog(GetParent(), _("Edit cinema"), cinema->name, cinema->emails, cinema->notes, cinema->utc_offset);
if (dialog.ShowModal() == wxID_OK) {
- cinema->name = dialog.name();
cinema->emails = dialog.emails();
+ cinema->name = dialog.name();
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);
+ cinema->utc_offset = dialog.utc_offset();
+ cinemas.update_cinema(cinema_id, *cinema);
+ auto item = cinema_to_item(cinema_id);
DCPOMATIC_ASSERT(item);
_targets->SetItemText (*item, std_to_wx(dialog.name()));
}
@@ -341,8 +349,11 @@ ScreensPanel::edit_cinema(shared_ptr<Cinema> cinema)
void
ScreensPanel::remove_cinema_clicked ()
{
+ CinemaList cinemas;
+
if (_selected_cinemas.size() == 1) {
- if (!confirm_dialog(this, wxString::Format(_("Are you sure you want to remove the cinema '%s'?"), std_to_wx(_selected_cinemas[0]->name)))) {
+ auto cinema = cinemas.cinema(_selected_cinemas[0]);
+ if (!confirm_dialog(this, wxString::Format(_("Are you sure you want to remove the cinema '%s'?"), std_to_wx(cinema->name)))) {
return;
}
} else {
@@ -353,14 +364,12 @@ ScreensPanel::remove_cinema_clicked ()
auto cinemas_to_remove = _selected_cinemas;
- for (auto const& cinema: cinemas_to_remove) {
- _ignore_cinemas_changed = true;
- dcp::ScopeGuard sg = [this]() { _ignore_cinemas_changed = false; };
- for (auto screen: cinema->screens()) {
- _checked_screens.erase(screen);
+ for (auto const& cinema_id: cinemas_to_remove) {
+ for (auto screen: cinemas.screens(cinema_id)) {
+ _checked_screens.erase({cinema_id, screen.first});
}
- Config::instance()->remove_cinema(cinema);
- auto item = cinema_to_item(cinema);
+ cinemas.remove_cinema(cinema_id);
+ auto item = cinema_to_item(cinema_id);
DCPOMATIC_ASSERT(item);
_targets->DeleteItem(*item);
}
@@ -373,8 +382,8 @@ ScreensPanel::remove_cinema_clicked ()
void
ScreensPanel::add_screen_clicked ()
{
- auto cinema = cinema_for_operation ();
- if (!cinema) {
+ auto cinema_id = cinema_for_operation();
+ if (!cinema_id) {
return;
}
@@ -384,8 +393,10 @@ ScreensPanel::add_screen_clicked ()
return;
}
- for (auto screen: cinema->screens()) {
- if (screen->name == dialog.name()) {
+ CinemaList cinemas;
+
+ for (auto screen: cinemas.screens(*cinema_id)) {
+ if (screen.second.name == dialog.name()) {
error_dialog (
GetParent(),
wxString::Format (
@@ -397,11 +408,10 @@ ScreensPanel::add_screen_clicked ()
}
}
- auto screen = std::make_shared<Screen>(dialog.name(), dialog.notes(), dialog.recipient(), dialog.recipient_file(), dialog.trusted_devices());
- cinema->add_screen (screen);
- notify_cinemas_changed();
+ auto screen = Screen(dialog.name(), dialog.notes(), dialog.recipient(), dialog.recipient_file(), dialog.trusted_devices());
+ auto const screen_id = cinemas.add_screen(*cinema_id, screen);
- auto id = add_screen (cinema, screen);
+ auto id = add_screen(*cinema_id, screen_id);
if (id) {
_targets->Expand (id.get ());
}
@@ -412,30 +422,33 @@ void
ScreensPanel::edit_screen_clicked ()
{
if (_selected_screens.size() == 1) {
- edit_screen(_selected_screens[0]);
+ edit_screen(_selected_screens[0].first, _selected_screens[0].second);
}
}
void
-ScreensPanel::edit_screen(shared_ptr<Screen> edit_screen)
+ScreensPanel::edit_screen(CinemaID cinema_id, ScreenID screen_id)
{
+ CinemaList cinemas;
+ auto screen = cinemas.screen(screen_id);
+ DCPOMATIC_ASSERT(screen);
+
ScreenDialog dialog(
GetParent(), _("Edit screen"),
- edit_screen->name,
- edit_screen->notes,
- edit_screen->recipient,
- edit_screen->recipient_file,
- edit_screen->trusted_devices
+ screen->name,
+ screen->notes,
+ screen->recipient,
+ screen->recipient_file,
+ screen->trusted_devices
);
if (dialog.ShowModal() != wxID_OK) {
return;
}
- auto cinema = edit_screen->cinema;
- for (auto screen: cinema->screens()) {
- if (screen != edit_screen && screen->name == dialog.name()) {
+ for (auto screen: cinemas.screens(cinema_id)) {
+ if (screen.first != screen_id && screen.second.name == dialog.name()) {
error_dialog (
GetParent(),
wxString::Format (
@@ -447,14 +460,14 @@ ScreensPanel::edit_screen(shared_ptr<Screen> edit_screen)
}
}
- edit_screen->name = dialog.name();
- edit_screen->notes = dialog.notes();
- edit_screen->recipient = dialog.recipient();
- edit_screen->recipient_file = dialog.recipient_file();
- edit_screen->trusted_devices = dialog.trusted_devices();
- notify_cinemas_changed();
+ screen->name = dialog.name();
+ screen->notes = dialog.notes();
+ screen->recipient = dialog.recipient();
+ screen->recipient_file = dialog.recipient_file();
+ screen->trusted_devices = dialog.trusted_devices();
+ cinemas.update_screen(screen_id, *screen);
- auto item = screen_to_item(edit_screen);
+ auto item = screen_to_item(screen_id);
DCPOMATIC_ASSERT (item);
_targets->SetItemText(*item, std_to_wx(dialog.name()));
}
@@ -463,8 +476,12 @@ ScreensPanel::edit_screen(shared_ptr<Screen> edit_screen)
void
ScreensPanel::remove_screen_clicked ()
{
+ CinemaList cinemas;
+
if (_selected_screens.size() == 1) {
- if (!confirm_dialog(this, wxString::Format(_("Are you sure you want to remove the screen '%s'?"), std_to_wx(_selected_screens[0]->name)))) {
+ auto screen = cinemas.screen(_selected_screens[0].second);
+ DCPOMATIC_ASSERT(screen);
+ if (!confirm_dialog(this, wxString::Format(_("Are you sure you want to remove the screen '%s'?"), std_to_wx(screen->name)))) {
return;
}
} else {
@@ -473,10 +490,10 @@ ScreensPanel::remove_screen_clicked ()
}
}
- for (auto screen: _selected_screens) {
- _checked_screens.erase(screen);
- screen->cinema->remove_screen(screen);
- auto item = screen_to_item(screen);
+ for (auto screen_id: _selected_screens) {
+ _checked_screens.erase(screen_id);
+ cinemas.remove_screen(screen_id.second);
+ auto item = screen_to_item(screen_id.second);
DCPOMATIC_ASSERT(item);
_targets->DeleteItem(*item);
}
@@ -485,17 +502,14 @@ ScreensPanel::remove_screen_clicked ()
* as well.
*/
selection_changed();
- notify_cinemas_changed();
setup_show_only_checked();
}
-vector<shared_ptr<Screen>>
+std::set<pair<CinemaID, ScreenID>>
ScreensPanel::screens () const
{
- vector<shared_ptr<Screen>> output;
- std::copy (_checked_screens.begin(), _checked_screens.end(), std::back_inserter(output));
- return output;
+ return _checked_screens;
}
@@ -521,10 +535,10 @@ ScreensPanel::selection_changed ()
for (size_t i = 0; i < selection.size(); ++i) {
if (auto cinema = item_to_cinema(selection[i])) {
- _selected_cinemas.push_back(cinema);
+ _selected_cinemas.push_back(*cinema);
}
if (auto screen = item_to_screen(selection[i])) {
- _selected_screens.push_back(screen);
+ _selected_screens.push_back(*screen);
}
}
@@ -532,24 +546,12 @@ ScreensPanel::selection_changed ()
}
-list<shared_ptr<Cinema>>
-ScreensPanel::sorted_cinemas() const
-{
- auto cinemas = Config::instance()->cinemas();
-
- cinemas.sort(
- [this](shared_ptr<Cinema> a, shared_ptr<Cinema> b) { return _collator.compare(a->name, b->name) < 0; }
- );
-
- return cinemas;
-}
-
-
void
ScreensPanel::add_cinemas ()
{
- for (auto cinema: sorted_cinemas()) {
- add_cinema (cinema, wxTLI_LAST);
+ CinemaList cinemas;
+ for (auto cinema: cinemas.cinemas()) {
+ add_cinema (cinema.first, wxTLI_LAST);
}
}
@@ -583,7 +585,7 @@ ScreensPanel::display_filter_changed()
}
for (auto const& selection: _selected_screens) {
- if (auto item = screen_to_item(selection)) {
+ if (auto item = screen_to_item(selection.second)) {
_targets->Select (*item);
}
}
@@ -593,7 +595,7 @@ ScreensPanel::display_filter_changed()
_ignore_check_change = true;
for (auto const& checked: _checked_screens) {
- if (auto item = screen_to_item(checked)) {
+ if (auto item = screen_to_item(checked.second)) {
_targets->CheckItem(*item, wxCHK_CHECKED);
setup_cinema_checked_state(*item);
}
@@ -609,9 +611,9 @@ ScreensPanel::set_screen_checked (wxTreeListItem item, bool checked)
auto screen = item_to_screen(item);
DCPOMATIC_ASSERT(screen);
if (checked) {
- _checked_screens.insert(screen);
+ _checked_screens.insert({screen->first, screen->second});
} else {
- _checked_screens.erase(screen);
+ _checked_screens.erase({screen->first, screen->second});
}
setup_show_only_checked();
@@ -665,7 +667,7 @@ ScreensPanel::checkbox_changed (wxTreeListEvent& ev)
}
-shared_ptr<Cinema>
+optional<CinemaID>
ScreensPanel::item_to_cinema (wxTreeListItem item) const
{
auto iter = _item_to_cinema.find (item);
@@ -677,7 +679,7 @@ ScreensPanel::item_to_cinema (wxTreeListItem item) const
}
-shared_ptr<Screen>
+optional<pair<CinemaID, ScreenID>>
ScreensPanel::item_to_screen (wxTreeListItem item) const
{
auto iter = _item_to_screen.find (item);
@@ -690,7 +692,7 @@ ScreensPanel::item_to_screen (wxTreeListItem item) const
optional<wxTreeListItem>
-ScreensPanel::cinema_to_item (shared_ptr<Cinema> cinema) const
+ScreensPanel::cinema_to_item(CinemaID cinema) const
{
auto iter = _cinema_to_item.find (cinema);
if (iter == _cinema_to_item.end()) {
@@ -702,7 +704,7 @@ ScreensPanel::cinema_to_item (shared_ptr<Cinema> cinema) const
optional<wxTreeListItem>
-ScreensPanel::screen_to_item (shared_ptr<Screen> screen) const
+ScreensPanel::screen_to_item(ScreenID screen) const
{
auto iter = _screen_to_item.find (screen);
if (iter == _screen_to_item.end()) {
@@ -713,32 +715,6 @@ ScreensPanel::screen_to_item (shared_ptr<Screen> screen) const
}
-bool
-ScreensPanel::notify_cinemas_changed()
-{
- _ignore_cinemas_changed = true;
- dcp::ScopeGuard sg = [this]() { _ignore_cinemas_changed = false; };
-
- try {
- Config::instance()->changed(Config::CINEMAS);
- } catch (FileError& e) {
- error_dialog(GetParent(), _("Could not write cinema details to the cinemas.xml file. Check that the location of cinemas.xml is valid in DCP-o-matic's preferences."), std_to_wx(e.what()));
- return false;
- }
-
- return true;
-}
-
-
-void
-ScreensPanel::config_changed(Config::Property property)
-{
- if (property == Config::Property::CINEMAS && !_ignore_cinemas_changed) {
- clear_and_re_add();
- }
-}
-
-
void
ScreensPanel::item_activated(wxTreeListEvent& ev)
{
@@ -748,7 +724,7 @@ ScreensPanel::item_activated(wxTreeListEvent& ev)
} else {
auto iter = _item_to_screen.find(ev.GetItem());
if (iter != _item_to_screen.end()) {
- edit_screen(iter->second);
+ edit_screen(iter->second.first, iter->second.second);
}
}
}
@@ -767,3 +743,16 @@ ScreensPanel::setup_show_only_checked()
setup_sensitivity();
}
+
+dcp::UTCOffset
+ScreensPanel::best_utc_offset() const
+{
+ std::set<CinemaID> unique_cinema_ids;
+ for (auto const& screen: screens()) {
+ unique_cinema_ids.insert(screen.first);
+ }
+
+ CinemaList cinema_list;
+ return cinema_list.unique_utc_offset(unique_cinema_ids).get_value_or(dcp::UTCOffset());
+}
+
diff --git a/src/wx/screens_panel.h b/src/wx/screens_panel.h
index 80a7b3843..98ec2c631 100644
--- a/src/wx/screens_panel.h
+++ b/src/wx/screens_panel.h
@@ -19,6 +19,7 @@
*/
+#include "lib/cinema_list.h"
#include "lib/collator.h"
#include "lib/config.h"
#include <dcp/warnings.h>
@@ -38,7 +39,6 @@ namespace dcpomatic {
}
-class Cinema;
class CheckBox;
@@ -48,45 +48,44 @@ public:
explicit ScreensPanel (wxWindow* parent);
~ScreensPanel ();
- std::vector<std::shared_ptr<dcpomatic::Screen>> screens () const;
+ std::set<std::pair<CinemaID, ScreenID>> screens() const;
void setup_sensitivity ();
+ dcp::UTCOffset best_utc_offset() const;
+
boost::signals2::signal<void ()> ScreensChanged;
private:
void add_cinemas ();
- boost::optional<wxTreeListItem> add_cinema (std::shared_ptr<Cinema>, wxTreeListItem previous);
- boost::optional<wxTreeListItem> add_screen (std::shared_ptr<Cinema>, std::shared_ptr<dcpomatic::Screen>);
+ boost::optional<wxTreeListItem> add_cinema(CinemaID cinema, wxTreeListItem previous);
+ boost::optional<wxTreeListItem> add_screen(CinemaID cinema, ScreenID screen);
void add_cinema_clicked ();
void edit_cinema_clicked ();
- void edit_cinema(std::shared_ptr<Cinema> cinema);
+ void edit_cinema(CinemaID cinema_id);
void remove_cinema_clicked ();
void add_screen_clicked ();
void edit_screen_clicked ();
- void edit_screen(std::shared_ptr<dcpomatic::Screen> screen);
+ void edit_screen(CinemaID cinema_id, ScreenID screen_id);
void remove_screen_clicked ();
void selection_changed_shim (wxTreeListEvent &);
void selection_changed ();
void display_filter_changed();
void checkbox_changed (wxTreeListEvent& ev);
void item_activated(wxTreeListEvent& ev);
- std::shared_ptr<Cinema> cinema_for_operation () const;
+ boost::optional<CinemaID> cinema_for_operation() const;
void set_screen_checked (wxTreeListItem item, bool checked);
void setup_cinema_checked_state (wxTreeListItem screen);
void check_all ();
void uncheck_all ();
- bool notify_cinemas_changed();
void clear_and_re_add();
- void config_changed(Config::Property);
void convert_to_lower(std::string& s);
- bool matches_search(std::shared_ptr<const Cinema> cinema, std::string search);
- std::list<std::shared_ptr<Cinema>> sorted_cinemas() const;
+ bool matches_search(Cinema const& cinema, std::string search);
void setup_show_only_checked();
- std::shared_ptr<Cinema> item_to_cinema (wxTreeListItem item) const;
- std::shared_ptr<dcpomatic::Screen> item_to_screen (wxTreeListItem item) const;
- boost::optional<wxTreeListItem> cinema_to_item (std::shared_ptr<Cinema> cinema) const;
- boost::optional<wxTreeListItem> screen_to_item (std::shared_ptr<dcpomatic::Screen> screen) const;
+ boost::optional<CinemaID> item_to_cinema(wxTreeListItem item) const;
+ boost::optional<std::pair<CinemaID, ScreenID>> item_to_screen(wxTreeListItem item) const;
+ boost::optional<wxTreeListItem> cinema_to_item(CinemaID cinema) const;
+ boost::optional<wxTreeListItem> screen_to_item(ScreenID screen) const;
wxBoxSizer* _overall_sizer;
wxSearchCtrl* _search;
@@ -104,24 +103,19 @@ private:
/* We want to be able to search (and so remove selected things from the view)
* but not deselect them, so we maintain lists of selected cinemas and screens.
*/
- std::vector<std::shared_ptr<Cinema>> _selected_cinemas;
- std::vector<std::shared_ptr<dcpomatic::Screen>> _selected_screens;
- /* Likewise with checked screens, except that we can work out which cinemas
- * are checked from which screens are checked, so we don't need to store the
- * cinemas.
- */
- std::set<std::shared_ptr<dcpomatic::Screen>> _checked_screens;
+ std::vector<CinemaID> _selected_cinemas;
+ /* List of cinema_id, screen_id */
+ std::vector<std::pair<CinemaID, ScreenID>> _selected_screens;
+ /* Likewise with checked screens */
+ std::set<std::pair<CinemaID, ScreenID>> _checked_screens;
- std::map<wxTreeListItem, std::shared_ptr<Cinema>> _item_to_cinema;
- std::map<wxTreeListItem, std::shared_ptr<dcpomatic::Screen>> _item_to_screen;
- std::map<std::shared_ptr<Cinema>, wxTreeListItem> _cinema_to_item;
- std::map<std::shared_ptr<dcpomatic::Screen>, wxTreeListItem> _screen_to_item;
+ std::map<wxTreeListItem, CinemaID> _item_to_cinema;
+ std::map<wxTreeListItem, std::pair<CinemaID, ScreenID>> _item_to_screen;
+ std::map<CinemaID, wxTreeListItem> _cinema_to_item;
+ std::map<ScreenID, wxTreeListItem> _screen_to_item;
bool _ignore_selection_change = false;
bool _ignore_check_change = false;
Collator _collator;
-
- boost::signals2::scoped_connection _config_connection;
- bool _ignore_cinemas_changed = false;
};
diff --git a/src/wx/system_information_dialog.cc b/src/wx/system_information_dialog.cc
index fbae2e010..26138900a 100644
--- a/src/wx/system_information_dialog.cc
+++ b/src/wx/system_information_dialog.cc
@@ -23,6 +23,7 @@
#include "gl_video_view.h"
#include "system_information_dialog.h"
#include "wx_util.h"
+#include "wx_variant.h"
#ifdef DCPOMATIC_OSX
@@ -48,7 +49,7 @@ SystemInformationDialog::SystemInformationDialog(wxWindow* parent, FilmViewer co
if (!gl) {
add (_("OpenGL version"), true);
- add (_("unknown (OpenGL not enabled in DCP-o-matic)"), false);
+ add(variant::wx::insert_dcpomatic(_("unknown (OpenGL not enabled in %s)")), false);
} else {
auto information = gl->information();
auto add_string = [this, &information](GLenum name, wxString label) {
@@ -79,7 +80,7 @@ SystemInformationDialog::SystemInformationDialog(wxWindow* parent, FilmViewer co
: TableDialog (parent, _("System information"), 2, 1, false)
{
add (_("OpenGL version"), true);
- add (_("OpenGL renderer not supported by this DCP-o-matic version"), false);
+ add(variant::wx::insert_dcpomatic(_("OpenGL renderer not supported by this %s version")), false);
}
#endif
diff --git a/src/wx/text_panel.cc b/src/wx/text_panel.cc
index 78c024565..4b6e900db 100644
--- a/src/wx/text_panel.cc
+++ b/src/wx/text_panel.cc
@@ -84,14 +84,6 @@ TextPanel::create ()
refer = _("Use this DCP's closed caption as OV and make VF");
}
- _reference = new CheckBox (this, refer);
- _reference_note = new StaticText (this, wxT(""));
- _reference_note->Wrap (200);
- auto font = _reference_note->GetFont();
- font.SetStyle(wxFONTSTYLE_ITALIC);
- font.SetPointSize(font.GetPointSize() - 1);
- _reference_note->SetFont(font);
-
_use = new CheckBox (this, _("Use as"));
_type = new wxChoice (this, wxID_ANY);
_type->Append (_("open subtitles"));
@@ -132,7 +124,6 @@ TextPanel::create ()
_y_scale->SetRange (0, 1000);
_line_spacing->SetRange (0, 1000);
- _reference->bind(&TextPanel::reference_clicked, this);
_use->bind(&TextPanel::use_toggled, this);
_type->Bind (wxEVT_CHOICE, boost::bind (&TextPanel::type_changed, this));
_burn->bind(&TextPanel::burn_toggled, this);
@@ -232,12 +223,6 @@ TextPanel::add_to_grid ()
{
int r = 0;
- auto reference_sizer = new wxBoxSizer (wxVERTICAL);
- reference_sizer->Add (_reference, 0);
- reference_sizer->Add (_reference_note, 0);
- _grid->Add (reference_sizer, wxGBPosition(r, 0), wxGBSpan(1, 4));
- ++r;
-
auto use = new wxBoxSizer (wxHORIZONTAL);
use->Add (_use, 0, wxEXPAND | wxRIGHT, DCPOMATIC_SIZER_GAP);
use->Add (_type, 1, wxEXPAND, 0);
@@ -496,15 +481,6 @@ TextPanel::film_content_changed (int property)
if (_language_type) {
_language_type->SetSelection (text ? (text->language_is_additional() ? 1 : 0) : 0);
}
- } else if (property == DCPContentProperty::REFERENCE_TEXT) {
- if (scs) {
- auto dcp = dynamic_pointer_cast<DCPContent> (scs);
- checked_set (_reference, dcp ? dcp->reference_text(_original_type) : false);
- } else {
- checked_set (_reference, false);
- }
-
- setup_sensitivity ();
} else if (property == DCPContentProperty::TEXTS) {
setup_sensitivity ();
} else if (property == ContentProperty::TRIM_START) {
@@ -593,17 +569,7 @@ TextPanel::setup_sensitivity ()
dcp = dynamic_pointer_cast<DCPContent>(sel.front());
}
- string why_not;
- bool const can_reference = dcp && dcp->can_reference_text (_parent->film(), _original_type, why_not);
- wxString cannot;
- if (why_not.empty()) {
- cannot = _("Cannot reference this DCP's subtitles or captions.");
- } else {
- cannot = _("Cannot reference this DCP's subtitles or captions: ") + std_to_wx(why_not);
- }
- setup_refer_button (_reference, _reference_note, dcp, can_reference, cannot);
-
- bool const reference = _reference->GetValue ();
+ auto const reference = dcp && dcp->reference_text(_original_type);
auto const type = current_type ();
@@ -746,7 +712,7 @@ TextPanel::text_view_clicked ()
if (decoder) {
_text_view.reset(this, _parent->film(), c.front(), c.front()->text_of_original_type(_original_type), decoder, _parent->film_viewer());
- _text_view->Show ();
+ _text_view->show();
}
}
@@ -763,23 +729,6 @@ TextPanel::fonts_dialog_clicked ()
void
-TextPanel::reference_clicked ()
-{
- auto c = _parent->selected ();
- if (c.size() != 1) {
- return;
- }
-
- auto d = dynamic_pointer_cast<DCPContent> (c.front ());
- if (!d) {
- return;
- }
-
- d->set_reference_text (_original_type, _reference->GetValue ());
-}
-
-
-void
TextPanel::appearance_dialog_clicked ()
{
auto c = _parent->selected_text ();
diff --git a/src/wx/text_panel.h b/src/wx/text_panel.h
index 5adad5a3e..a2afba439 100644
--- a/src/wx/text_panel.h
+++ b/src/wx/text_panel.h
@@ -56,7 +56,6 @@ private:
void stream_changed ();
void text_view_clicked ();
void fonts_dialog_clicked ();
- void reference_clicked ();
void appearance_dialog_clicked ();
void outline_subtitles_changed ();
TextType current_type () const;
@@ -74,8 +73,6 @@ private:
void update_outline_subtitles_in_viewer ();
void clear_outline_subtitles ();
- CheckBox* _reference;
- wxStaticText* _reference_note;
CheckBox* _outline_subtitles = nullptr;
CheckBox* _use;
wxChoice* _type;
diff --git a/src/wx/text_view.cc b/src/wx/text_view.cc
index 7e5267886..bde7b09e9 100644
--- a/src/wx/text_view.cc
+++ b/src/wx/text_view.cc
@@ -42,14 +42,17 @@ using namespace boost::placeholders;
#endif
+WindowMetrics TextView::_metrics;
+
+
TextView::TextView (
wxWindow* parent, shared_ptr<Film> film, shared_ptr<Content> content, shared_ptr<TextContent> text, shared_ptr<Decoder> decoder, FilmViewer& viewer
)
- : wxDialog (parent, wxID_ANY, _("Captions"), wxDefaultPosition, wxDefaultSize, wxDEFAULT_DIALOG_STYLE | wxRESIZE_BORDER)
+ : wxDialog(parent, wxID_ANY, _("Captions"), _metrics.position, wxDefaultSize, wxDEFAULT_DIALOG_STYLE | wxRESIZE_BORDER)
, _content (content)
, _film_viewer (viewer)
{
- _list = new wxListCtrl (this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxLC_REPORT | wxLC_SINGLE_SEL);
+ _list = new wxListCtrl(this, wxID_ANY, wxDefaultPosition, _metrics.size, wxLC_REPORT | wxLC_SINGLE_SEL);
{
wxListItem ip;
@@ -104,10 +107,35 @@ TextView::TextView (
}
while (!decoder->pass ()) {}
SetSizerAndFit (sizer);
+
+ _list->Bind(wxEVT_SIZE, boost::bind(&TextView::list_sized, this, _1));
+ Bind(wxEVT_MOVE, boost::bind(&TextView::moved, this, _1));
}
void
+TextView::list_sized(wxSizeEvent& ev)
+{
+ _metrics.size = ev.GetSize();
+ ev.Skip();
+}
+
+
+void
+TextView::moved(wxMoveEvent& ev)
+{
+ _metrics.position = ClientToScreen({0, 0});
+ ev.Skip();
+}
+
+
+void
+TextView::show()
+{
+ _metrics.show(this);
+}
+
+void
TextView::data_start (ContentStringText cts)
{
for (auto const& i: cts.subs) {
@@ -147,6 +175,6 @@ TextView::subtitle_selected (wxListEvent& ev)
DCPOMATIC_ASSERT (ev.GetIndex() < int(_start_times.size()));
auto lc = _content.lock ();
DCPOMATIC_ASSERT (lc);
- /* Add on a frame here to work around any rounding errors and make sure land in the subtitle */
+ /* Add on a frame here to work around any rounding errors and make sure we land in the subtitle */
_film_viewer.seek(lc, _start_times[ev.GetIndex()] + ContentTime::from_frames(1, _frc->source), true);
}
diff --git a/src/wx/text_view.h b/src/wx/text_view.h
index 8cf3c78bb..d1271ef26 100644
--- a/src/wx/text_view.h
+++ b/src/wx/text_view.h
@@ -18,6 +18,8 @@
*/
+
+#include "window_metrics.h"
#include "lib/content_text.h"
#include <dcp/warnings.h>
LIBDCP_DISABLE_WARNINGS
@@ -25,10 +27,12 @@ LIBDCP_DISABLE_WARNINGS
#include <wx/wx.h>
LIBDCP_ENABLE_WARNINGS
+
class Decoder;
class FilmViewer;
class Film;
+
class TextView : public wxDialog
{
public:
@@ -41,10 +45,14 @@ public:
FilmViewer& viewer
);
+ void show();
+
private:
void data_start (ContentStringText cts);
void data_stop (dcpomatic::ContentTime time);
void subtitle_selected (wxListEvent &);
+ void moved(wxMoveEvent& ev);
+ void list_sized(wxSizeEvent& ev);
wxListCtrl* _list;
int _subs;
@@ -53,4 +61,6 @@ private:
std::vector<dcpomatic::ContentTime> _start_times;
std::weak_ptr<Content> _content;
FilmViewer& _film_viewer;
+
+ static WindowMetrics _metrics;
};
diff --git a/src/wx/timecode.cc b/src/wx/timecode.cc
index 1e6a1930d..64fe87190 100644
--- a/src/wx/timecode.cc
+++ b/src/wx/timecode.cc
@@ -114,7 +114,7 @@ void
TimecodeBase::changed ()
{
if (_set_button && !_ignore_changed) {
- _set_button->Enable (true);
+ _set_button->Enable(valid());
}
}
diff --git a/src/wx/timecode.h b/src/wx/timecode.h
index 6c5d8ae23..22899ddc9 100644
--- a/src/wx/timecode.h
+++ b/src/wx/timecode.h
@@ -50,6 +50,7 @@ public:
protected:
void changed ();
void set_clicked ();
+ virtual bool valid() const = 0;
wxSizer* _sizer;
wxPanel* _editable;
@@ -96,6 +97,11 @@ public:
_frames->SetHint (std_to_wx(dcp::raw_convert<std::string>(hmsf.f)));
}
+ void set_maximum(dcpomatic::HMSF maximum)
+ {
+ _maximum = std::move(maximum);
+ }
+
dcpomatic::HMSF get () const
{
auto value_or_hint = [](wxTextCtrl const * t) {
@@ -116,6 +122,13 @@ public:
{
return T(get(), fps);
}
+
+private:
+ bool valid() const override {
+ return !_maximum || get() <= *_maximum;
+ }
+
+ boost::optional<dcpomatic::HMSF> _maximum;
};
#endif
diff --git a/src/wx/timeline.cc b/src/wx/timeline.cc
index 2789d2a54..329f4ef00 100644
--- a/src/wx/timeline.cc
+++ b/src/wx/timeline.cc
@@ -1,5 +1,5 @@
/*
- Copyright (C) 2013-2021 Carl Hetherington <cth@carlh.net>
+ Copyright (C) 2023 Carl Hetherington <cth@carlh.net>
This file is part of DCP-o-matic.
@@ -18,1012 +18,25 @@
*/
-#include "content_panel.h"
-#include "film_editor.h"
-#include "film_viewer.h"
-#include "timeline.h"
-#include "timeline_atmos_content_view.h"
-#include "timeline_audio_content_view.h"
-#include "timeline_labels_view.h"
-#include "timeline_reels_view.h"
-#include "timeline_text_content_view.h"
-#include "timeline_time_axis_view.h"
-#include "timeline_video_content_view.h"
-#include "wx_util.h"
-#include "lib/atmos_mxf_content.h"
-#include "lib/audio_content.h"
-#include "lib/film.h"
-#include "lib/image_content.h"
-#include "lib/playlist.h"
-#include "lib/text_content.h"
-#include "lib/timer.h"
-#include "lib/video_content.h"
-#include <dcp/scope_guard.h>
-#include <dcp/warnings.h>
-LIBDCP_DISABLE_WARNINGS
-#include <wx/graphics.h>
-LIBDCP_ENABLE_WARNINGS
-#include <iterator>
-#include <list>
-
-using std::abs;
-using std::dynamic_pointer_cast;
-using std::list;
-using std::make_shared;
-using std::max;
-using std::min;
-using std::shared_ptr;
-using std::weak_ptr;
-using boost::bind;
-using boost::optional;
-using namespace dcpomatic;
-#if BOOST_VERSION >= 106100
-using namespace boost::placeholders;
-#endif
+#include "timeline.h"
/* 3 hours in 640 pixels */
-double const Timeline::_minimum_pixels_per_second = 640.0 / (60 * 60 * 3);
-int const Timeline::_minimum_pixels_per_track = 16;
-
-
-Timeline::Timeline(wxWindow* parent, ContentPanel* cp, shared_ptr<Film> film, FilmViewer& viewer)
- : wxPanel (parent, wxID_ANY)
- , _labels_canvas (new wxScrolledCanvas (this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxFULL_REPAINT_ON_RESIZE))
- , _main_canvas (new wxScrolledCanvas (this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxFULL_REPAINT_ON_RESIZE))
- , _content_panel (cp)
- , _film (film)
- , _viewer (viewer)
- , _time_axis_view (new TimelineTimeAxisView (*this, 64))
- , _reels_view (new TimelineReelsView (*this, 32))
- , _labels_view (new TimelineLabelsView (*this))
- , _tracks (0)
- , _left_down (false)
- , _down_view_position (0)
- , _first_move (false)
- , _menu (this, viewer)
- , _snap (true)
- , _tool (SELECT)
- , _x_scroll_rate (16)
- , _y_scroll_rate (16)
- , _pixels_per_track (48)
- , _first_resize (true)
- , _timer (this)
-{
-#ifndef __WXOSX__
- _labels_canvas->SetDoubleBuffered (true);
- _main_canvas->SetDoubleBuffered (true);
-#endif
-
- auto sizer = new wxBoxSizer (wxHORIZONTAL);
- sizer->Add (_labels_canvas, 0, wxEXPAND);
- _labels_canvas->SetMinSize (wxSize (_labels_view->bbox().width, -1));
- sizer->Add (_main_canvas, 1, wxEXPAND);
- SetSizer (sizer);
-
- _labels_canvas->Bind (wxEVT_PAINT, boost::bind (&Timeline::paint_labels, this));
- _main_canvas->Bind (wxEVT_PAINT, boost::bind (&Timeline::paint_main, this));
- _main_canvas->Bind (wxEVT_LEFT_DOWN, boost::bind (&Timeline::left_down, this, _1));
- _main_canvas->Bind (wxEVT_LEFT_UP, boost::bind (&Timeline::left_up, this, _1));
- _main_canvas->Bind (wxEVT_RIGHT_DOWN, boost::bind (&Timeline::right_down, this, _1));
- _main_canvas->Bind (wxEVT_MOTION, boost::bind (&Timeline::mouse_moved, this, _1));
- _main_canvas->Bind (wxEVT_SIZE, boost::bind (&Timeline::resized, this));
- _main_canvas->Bind (wxEVT_MOUSEWHEEL, boost::bind(&Timeline::mouse_wheel_turned, this, _1));
- _main_canvas->Bind (wxEVT_SCROLLWIN_TOP, boost::bind (&Timeline::scrolled, this, _1));
- _main_canvas->Bind (wxEVT_SCROLLWIN_BOTTOM, boost::bind (&Timeline::scrolled, this, _1));
- _main_canvas->Bind (wxEVT_SCROLLWIN_LINEUP, boost::bind (&Timeline::scrolled, this, _1));
- _main_canvas->Bind (wxEVT_SCROLLWIN_LINEDOWN, boost::bind (&Timeline::scrolled, this, _1));
- _main_canvas->Bind (wxEVT_SCROLLWIN_PAGEUP, boost::bind (&Timeline::scrolled, this, _1));
- _main_canvas->Bind (wxEVT_SCROLLWIN_PAGEDOWN, boost::bind (&Timeline::scrolled, this, _1));
- _main_canvas->Bind (wxEVT_SCROLLWIN_THUMBTRACK, boost::bind (&Timeline::scrolled, this, _1));
-
- film_change(ChangeType::DONE, FilmProperty::CONTENT);
-
- SetMinSize (wxSize (640, 4 * pixels_per_track() + 96));
-
- _film_changed_connection = film->Change.connect (bind (&Timeline::film_change, this, _1, _2));
- _film_content_change_connection = film->ContentChange.connect (bind (&Timeline::film_content_change, this, _1, _3, _4));
-
- Bind (wxEVT_TIMER, boost::bind(&Timeline::update_playhead, this));
- _timer.Start (200, wxTIMER_CONTINUOUS);
-
- setup_scrollbars ();
- _labels_canvas->ShowScrollbars (wxSHOW_SB_NEVER, wxSHOW_SB_NEVER);
-}
-
-
-void
-Timeline::mouse_wheel_turned(wxMouseEvent& event)
-{
- auto const rotation = event.GetWheelRotation();
-
- if (event.ControlDown()) {
- /* On my mouse one click of the scroll wheel is 120, and it's -ve when
- * scrolling the wheel towards me.
- */
- auto const scale = rotation > 0 ?
- (1.0 / (rotation / 90.0)) :
- (-rotation / 90.0);
-
- int before_start_x;
- int before_start_y;
- _main_canvas->GetViewStart(&before_start_x, &before_start_y);
-
- auto const before_pps = _pixels_per_second.get_value_or(1);
- auto const before_pos = _last_mouse_wheel_x && *_last_mouse_wheel_x == event.GetX() ?
- *_last_mouse_wheel_time :
- (before_start_x * _x_scroll_rate + event.GetX()) / before_pps;
-
- set_pixels_per_second(before_pps * scale);
- setup_scrollbars();
-
- auto after_left = std::max(0.0, before_pos * _pixels_per_second.get_value_or(1) - event.GetX());
- _main_canvas->Scroll(after_left / _x_scroll_rate, before_start_y);
- _labels_canvas->Scroll(0, before_start_y);
- Refresh();
-
- if (!_last_mouse_wheel_x || *_last_mouse_wheel_x != event.GetX()) {
- _last_mouse_wheel_x = event.GetX();
- _last_mouse_wheel_time = before_pos;
- }
- } else if (event.ShiftDown()) {
- int before_start_x;
- int before_start_y;
- _main_canvas->GetViewStart(&before_start_x, &before_start_y);
- auto const width = _main_canvas->GetSize().GetWidth();
- _main_canvas->Scroll(std::max(0.0, before_start_x - rotation * 100.0 / width), before_start_y);
- }
-}
-
-
-void
-Timeline::update_playhead ()
-{
- Refresh ();
-}
-
-
-void
-Timeline::set_pixels_per_second (double pps)
-{
- _pixels_per_second = max (_minimum_pixels_per_second, pps);
-}
-
-
-void
-Timeline::paint_labels ()
-{
- wxPaintDC dc (_labels_canvas);
-
- auto film = _film.lock();
- if (film->content().empty()) {
- return;
- }
-
- auto gc = wxGraphicsContext::Create (dc);
- if (!gc) {
- return;
- }
-
- dcp::ScopeGuard sg = [gc]() { delete gc; };
-
- int vsx, vsy;
- _labels_canvas->GetViewStart (&vsx, &vsy);
- gc->Translate (-vsx * _x_scroll_rate, -vsy * _y_scroll_rate + tracks_y_offset());
-
- _labels_view->paint (gc, {});
-}
-
-
-void
-Timeline::paint_main ()
-{
- wxPaintDC dc (_main_canvas);
- dc.Clear();
-
- auto film = _film.lock();
- if (film->content().empty()) {
- return;
- }
-
- _main_canvas->DoPrepareDC (dc);
-
- auto gc = wxGraphicsContext::Create (dc);
- if (!gc) {
- return;
- }
-
- dcp::ScopeGuard sg = [gc]() { delete gc; };
-
- gc->SetAntialiasMode (wxANTIALIAS_DEFAULT);
-
- for (auto i: _views) {
-
- auto ic = dynamic_pointer_cast<TimelineContentView> (i);
-
- /* Find areas of overlap with other content views, so that we can plot them */
- list<dcpomatic::Rect<int>> overlaps;
- for (auto j: _views) {
- auto jc = dynamic_pointer_cast<TimelineContentView> (j);
- /* No overlap with non-content views, views on different tracks, audio views or non-active views */
- if (!ic || !jc || i == j || ic->track() != jc->track() || ic->track().get_value_or(2) >= 2 || !ic->active() || !jc->active()) {
- continue;
- }
-
- auto r = j->bbox().intersection(i->bbox());
- if (r) {
- overlaps.push_back (r.get ());
- }
- }
-
- i->paint (gc, overlaps);
- }
-
- if (_zoom_point) {
- gc->SetPen(gui_is_dark() ? *wxWHITE_PEN : *wxBLACK_PEN);
- gc->SetBrush (*wxTRANSPARENT_BRUSH);
- gc->DrawRectangle (
- min (_down_point.x, _zoom_point->x),
- min (_down_point.y, _zoom_point->y),
- abs (_down_point.x - _zoom_point->x),
- abs (_down_point.y - _zoom_point->y)
- );
- }
-
- /* Playhead */
-
- gc->SetPen (*wxRED_PEN);
- auto path = gc->CreatePath ();
- double const ph = _viewer.position().seconds() * pixels_per_second().get_value_or(0);
- path.MoveToPoint (ph, 0);
- path.AddLineToPoint (ph, pixels_per_track() * _tracks + 32);
- gc->StrokePath (path);
-}
-
-
-void
-Timeline::film_change(ChangeType type, FilmProperty p)
-{
- if (type != ChangeType::DONE) {
- return;
- }
-
- if (p == FilmProperty::CONTENT || p == FilmProperty::REEL_TYPE || p == FilmProperty::REEL_LENGTH) {
- ensure_ui_thread ();
- recreate_views ();
- } else if (p == FilmProperty::CONTENT_ORDER) {
- Refresh ();
- }
-}
-
-
-void
-Timeline::recreate_views ()
-{
- auto film = _film.lock ();
- if (!film) {
- return;
- }
-
- _views.clear ();
- _views.push_back (_time_axis_view);
- _views.push_back (_reels_view);
-
- for (auto i: film->content ()) {
- if (i->video) {
- _views.push_back (make_shared<TimelineVideoContentView>(*this, i));
- }
-
- if (i->has_mapped_audio()) {
- _views.push_back (make_shared<TimelineAudioContentView>(*this, i));
- }
-
- for (auto j: i->text) {
- _views.push_back (make_shared<TimelineTextContentView>(*this, i, j));
- }
-
- if (i->atmos) {
- _views.push_back (make_shared<TimelineAtmosContentView>(*this, i));
- }
- }
-
- assign_tracks ();
- setup_scrollbars ();
- Refresh ();
-}
-
-
-void
-Timeline::film_content_change (ChangeType type, int property, bool frequent)
-{
- if (type != ChangeType::DONE) {
- return;
- }
-
- ensure_ui_thread ();
-
- if (property == AudioContentProperty::STREAMS || property == VideoContentProperty::FRAME_TYPE) {
- recreate_views ();
- } else if (property == ContentProperty::POSITION || property == ContentProperty::LENGTH) {
- _reels_view->force_redraw ();
- } else if (!frequent) {
- setup_scrollbars ();
- Refresh ();
- }
-}
-
-
-template <class T>
-int
-place (shared_ptr<const Film> film, TimelineViewList& views, int& tracks)
-{
- int const base = tracks;
-
- for (auto i: views) {
- if (!dynamic_pointer_cast<T>(i)) {
- continue;
- }
-
- auto cv = dynamic_pointer_cast<TimelineContentView> (i);
- DCPOMATIC_ASSERT(cv);
-
- int t = base;
-
- auto content = cv->content();
- DCPTimePeriod const content_period = content->period(film);
-
- while (true) {
- auto j = views.begin();
- while (j != views.end()) {
- auto test = dynamic_pointer_cast<T> (*j);
- if (!test) {
- ++j;
- continue;
- }
-
- auto test_content = test->content();
- if (
- test->track() && test->track().get() == t &&
- content_period.overlap(test_content->period(film))
- ) {
- /* we have an overlap on track `t' */
- ++t;
- break;
- }
-
- ++j;
- }
-
- if (j == views.end ()) {
- /* no overlap on `t' */
- break;
- }
- }
-
- cv->set_track (t);
- tracks = max (tracks, t + 1);
- }
-
- return tracks - base;
-}
-
-
-/** Compare the mapped output channels of two TimelineViews, so we can into
- * order of first mapped DCP channel.
- */
-struct AudioMappingComparator {
- bool operator()(shared_ptr<TimelineView> a, shared_ptr<TimelineView> b) {
- int la = -1;
- auto cva = dynamic_pointer_cast<TimelineAudioContentView>(a);
- if (cva) {
- auto oc = cva->content()->audio->mapping().mapped_output_channels();
- la = *min_element(boost::begin(oc), boost::end(oc));
- }
- int lb = -1;
- auto cvb = dynamic_pointer_cast<TimelineAudioContentView>(b);
- if (cvb) {
- auto oc = cvb->content()->audio->mapping().mapped_output_channels();
- lb = *min_element(boost::begin(oc), boost::end(oc));
- }
- return la < lb;
- }
-};
-
-
-void
-Timeline::assign_tracks ()
-{
- /* Tracks are:
- Video 1
- Video 2
- Video N
- Text 1
- Text 2
- Text N
- Atmos
- Audio 1
- Audio 2
- Audio N
- */
-
- auto film = _film.lock ();
- DCPOMATIC_ASSERT (film);
-
- _tracks = 0;
-
- for (auto i: _views) {
- auto c = dynamic_pointer_cast<TimelineContentView>(i);
- if (c) {
- c->unset_track ();
- }
- }
-
- int const video_tracks = place<TimelineVideoContentView> (film, _views, _tracks);
- int const text_tracks = place<TimelineTextContentView> (film, _views, _tracks);
-
- /* Atmos */
-
- bool have_atmos = false;
- for (auto i: _views) {
- auto cv = dynamic_pointer_cast<TimelineAtmosContentView>(i);
- if (cv) {
- cv->set_track (_tracks);
- have_atmos = true;
- }
- }
-
- if (have_atmos) {
- ++_tracks;
- }
-
- /* Audio. We're sorting the views so that we get the audio views in order of increasing
- DCP channel index.
- */
-
- auto views = _views;
- sort(views.begin(), views.end(), AudioMappingComparator());
- int const audio_tracks = place<TimelineAudioContentView> (film, views, _tracks);
-
- _labels_view->set_video_tracks (video_tracks);
- _labels_view->set_audio_tracks (audio_tracks);
- _labels_view->set_text_tracks (text_tracks);
- _labels_view->set_atmos (have_atmos);
-
- _time_axis_view->set_y (tracks());
- _reels_view->set_y (8);
-}
-
-
-int
-Timeline::tracks () const
-{
- return _tracks;
-}
-
-
-void
-Timeline::setup_scrollbars ()
-{
- auto film = _film.lock ();
- if (!film || !_pixels_per_second) {
- return;
- }
-
- int const h = tracks() * pixels_per_track() + tracks_y_offset() + _time_axis_view->bbox().height;
-
- _labels_canvas->SetVirtualSize (_labels_view->bbox().width, h);
- _labels_canvas->SetScrollRate (_x_scroll_rate, _y_scroll_rate);
- _main_canvas->SetVirtualSize (*_pixels_per_second * film->length().seconds(), h);
- _main_canvas->SetScrollRate (_x_scroll_rate, _y_scroll_rate);
-}
-
-
-shared_ptr<TimelineView>
-Timeline::event_to_view (wxMouseEvent& ev)
-{
- /* Search backwards through views so that we find the uppermost one first */
- auto i = _views.rbegin();
-
- int vsx, vsy;
- _main_canvas->GetViewStart (&vsx, &vsy);
- Position<int> const p (ev.GetX() + vsx * _x_scroll_rate, ev.GetY() + vsy * _y_scroll_rate);
-
- while (i != _views.rend() && !(*i)->bbox().contains (p)) {
- ++i;
- }
-
- if (i == _views.rend ()) {
- return {};
- }
-
- return *i;
-}
-
-
-void
-Timeline::left_down (wxMouseEvent& ev)
-{
- _left_down = true;
- _down_point = ev.GetPosition ();
-
- switch (_tool) {
- case SELECT:
- left_down_select (ev);
- break;
- case ZOOM:
- case ZOOM_ALL:
- case SNAP:
- case SEQUENCE:
- /* Nothing to do */
- break;
- }
-}
-
-
-void
-Timeline::left_down_select (wxMouseEvent& ev)
-{
- auto view = event_to_view (ev);
- auto content_view = dynamic_pointer_cast<TimelineContentView>(view);
-
- _down_view.reset ();
+double constexpr minimum_pixels_per_second = 640.0 / (60 * 60 * 3);
- if (content_view) {
- _down_view = content_view;
- _down_view_position = content_view->content()->position ();
- }
- if (dynamic_pointer_cast<TimelineTimeAxisView>(view)) {
- int vsx, vsy;
- _main_canvas->GetViewStart(&vsx, &vsy);
- _viewer.seek(DCPTime::from_seconds((ev.GetPosition().x + vsx * _x_scroll_rate) / _pixels_per_second.get_value_or(1)), true);
- }
-
- for (auto i: _views) {
- auto cv = dynamic_pointer_cast<TimelineContentView>(i);
- if (!cv) {
- continue;
- }
-
- if (!ev.ShiftDown ()) {
- cv->set_selected (view == i);
- }
- }
-
- if (content_view && ev.ShiftDown ()) {
- content_view->set_selected (!content_view->selected ());
- }
-
- _first_move = false;
-
- if (_down_view) {
- /* Pre-compute the points that we might snap to */
- for (auto i: _views) {
- auto cv = dynamic_pointer_cast<TimelineContentView>(i);
- if (!cv || cv == _down_view || cv->content() == _down_view->content()) {
- continue;
- }
-
- auto film = _film.lock ();
- DCPOMATIC_ASSERT (film);
-
- _start_snaps.push_back (cv->content()->position());
- _end_snaps.push_back (cv->content()->position());
- _start_snaps.push_back (cv->content()->end(film));
- _end_snaps.push_back (cv->content()->end(film));
-
- for (auto i: cv->content()->reel_split_points(film)) {
- _start_snaps.push_back (i);
- }
- }
-
- /* Tell everyone that things might change frequently during the drag */
- _down_view->content()->set_change_signals_frequent (true);
- }
-}
-
-
-void
-Timeline::left_up (wxMouseEvent& ev)
-{
- _left_down = false;
-
- switch (_tool) {
- case SELECT:
- left_up_select (ev);
- break;
- case ZOOM:
- left_up_zoom (ev);
- break;
- case ZOOM_ALL:
- case SNAP:
- case SEQUENCE:
- break;
- }
-}
-
-
-void
-Timeline::left_up_select (wxMouseEvent& ev)
+Timeline::Timeline(wxWindow* parent)
+ : wxPanel(parent, wxID_ANY)
{
- if (_down_view) {
- _down_view->content()->set_change_signals_frequent (false);
- }
- _content_panel->set_selection (selected_content ());
- /* Since we may have just set change signals back to `not-frequent', we have to
- make sure this position change is signalled, even if the position value has
- not changed since the last time it was set (with frequent=true). This is
- a bit of a hack.
- */
- set_position_from_event (ev, true);
-
- /* Clear up up the stuff we don't do during drag */
- assign_tracks ();
- setup_scrollbars ();
- Refresh ();
-
- _start_snaps.clear ();
- _end_snaps.clear ();
-}
-
-
-void
-Timeline::left_up_zoom (wxMouseEvent& ev)
-{
- _zoom_point = ev.GetPosition ();
-
- int vsx, vsy;
- _main_canvas->GetViewStart (&vsx, &vsy);
-
- wxPoint top_left(min(_down_point.x, _zoom_point->x), min(_down_point.y, _zoom_point->y));
- wxPoint bottom_right(max(_down_point.x, _zoom_point->x), max(_down_point.y, _zoom_point->y));
-
- if ((bottom_right.x - top_left.x) < 8 || (bottom_right.y - top_left.y) < 8) {
- /* Very small zoom rectangle: we assume it wasn't intentional */
- _zoom_point = optional<wxPoint> ();
- Refresh ();
- return;
- }
-
- auto const time_left = DCPTime::from_seconds((top_left.x + vsx) / *_pixels_per_second);
- auto const time_right = DCPTime::from_seconds((bottom_right.x + vsx) / *_pixels_per_second);
- set_pixels_per_second (double(GetSize().GetWidth()) / (time_right.seconds() - time_left.seconds()));
-
- double const tracks_top = double(top_left.y - tracks_y_offset()) / _pixels_per_track;
- double const tracks_bottom = double(bottom_right.y - tracks_y_offset()) / _pixels_per_track;
- set_pixels_per_track (lrint(GetSize().GetHeight() / (tracks_bottom - tracks_top)));
-
- setup_scrollbars ();
- int const y = (tracks_top * _pixels_per_track + tracks_y_offset()) / _y_scroll_rate;
- _main_canvas->Scroll (time_left.seconds() * *_pixels_per_second / _x_scroll_rate, y);
- _labels_canvas->Scroll (0, y);
-
- _zoom_point = optional<wxPoint> ();
- Refresh ();
-}
-
-
-void
-Timeline::set_pixels_per_track (int h)
-{
- _pixels_per_track = max(_minimum_pixels_per_track, h);
}
void
-Timeline::mouse_moved (wxMouseEvent& ev)
+Timeline::set_pixels_per_second(double pps)
{
- switch (_tool) {
- case SELECT:
- mouse_moved_select (ev);
- break;
- case ZOOM:
- mouse_moved_zoom (ev);
- break;
- case ZOOM_ALL:
- case SNAP:
- case SEQUENCE:
- break;
- }
+ _pixels_per_second = std::max(minimum_pixels_per_second, pps);
}
-void
-Timeline::mouse_moved_select (wxMouseEvent& ev)
-{
- if (!_left_down) {
- return;
- }
-
- set_position_from_event (ev);
-}
-
-
-void
-Timeline::mouse_moved_zoom (wxMouseEvent& ev)
-{
- if (!_left_down) {
- return;
- }
-
- _zoom_point = ev.GetPosition ();
- setup_scrollbars();
- Refresh ();
-}
-
-
-void
-Timeline::right_down (wxMouseEvent& ev)
-{
- switch (_tool) {
- case SELECT:
- right_down_select (ev);
- break;
- case ZOOM:
- /* Zoom out */
- set_pixels_per_second (*_pixels_per_second / 2);
- set_pixels_per_track (_pixels_per_track / 2);
- setup_scrollbars ();
- Refresh ();
- break;
- case ZOOM_ALL:
- case SNAP:
- case SEQUENCE:
- break;
- }
-}
-
-
-void
-Timeline::right_down_select (wxMouseEvent& ev)
-{
- auto view = event_to_view (ev);
- auto cv = dynamic_pointer_cast<TimelineContentView> (view);
- if (!cv) {
- return;
- }
-
- if (!cv->selected ()) {
- clear_selection ();
- cv->set_selected (true);
- }
-
- _menu.popup (_film, selected_content (), selected_views (), ev.GetPosition ());
-}
-
-
-void
-Timeline::maybe_snap (DCPTime a, DCPTime b, optional<DCPTime>& nearest_distance) const
-{
- auto const d = a - b;
- if (!nearest_distance || d.abs() < nearest_distance.get().abs()) {
- nearest_distance = d;
- }
-}
-
-
-void
-Timeline::set_position_from_event (wxMouseEvent& ev, bool force_emit)
-{
- if (!_pixels_per_second) {
- return;
- }
-
- double const pps = _pixels_per_second.get ();
-
- auto const p = ev.GetPosition();
-
- if (!_first_move) {
- /* We haven't moved yet; in that case, we must move the mouse some reasonable distance
- before the drag is considered to have started.
- */
- int const dist = sqrt (pow (p.x - _down_point.x, 2) + pow (p.y - _down_point.y, 2));
- if (dist < 8) {
- return;
- }
- _first_move = true;
- }
-
- if (!_down_view) {
- return;
- }
-
- auto new_position = _down_view_position + DCPTime::from_seconds ((p.x - _down_point.x) / pps);
-
- auto film = _film.lock ();
- DCPOMATIC_ASSERT (film);
-
- if (_snap) {
- auto const new_end = new_position + _down_view->content()->length_after_trim(film);
- /* Signed `distance' to nearest thing (i.e. negative is left on the timeline,
- positive is right).
- */
- optional<DCPTime> nearest_distance;
-
- /* Find the nearest snap point */
-
- for (auto i: _start_snaps) {
- maybe_snap (i, new_position, nearest_distance);
- }
-
- for (auto i: _end_snaps) {
- maybe_snap (i, new_end, nearest_distance);
- }
-
- if (nearest_distance) {
- /* Snap if it's close; `close' means within a proportion of the time on the timeline */
- if (nearest_distance.get().abs() < DCPTime::from_seconds ((width() / pps) / 64)) {
- new_position += nearest_distance.get ();
- }
- }
- }
-
- if (new_position < DCPTime ()) {
- new_position = DCPTime ();
- }
-
- _down_view->content()->set_position (film, new_position, force_emit);
-
- film->set_sequence (false);
-}
-
-
-void
-Timeline::force_redraw (dcpomatic::Rect<int> const & r)
-{
- _main_canvas->RefreshRect (wxRect (r.x, r.y, r.width, r.height), false);
-}
-
-
-shared_ptr<const Film>
-Timeline::film () const
-{
- return _film.lock ();
-}
-
-
-void
-Timeline::resized ()
-{
- if (_main_canvas->GetSize().GetWidth() > 0 && _first_resize) {
- zoom_all ();
- _first_resize = false;
- }
- setup_scrollbars ();
-}
-
-
-void
-Timeline::clear_selection ()
-{
- for (auto i: _views) {
- shared_ptr<TimelineContentView> cv = dynamic_pointer_cast<TimelineContentView>(i);
- if (cv) {
- cv->set_selected (false);
- }
- }
-}
-
-
-TimelineContentViewList
-Timeline::selected_views () const
-{
- TimelineContentViewList sel;
-
- for (auto i: _views) {
- auto cv = dynamic_pointer_cast<TimelineContentView>(i);
- if (cv && cv->selected()) {
- sel.push_back (cv);
- }
- }
-
- return sel;
-}
-
-
-ContentList
-Timeline::selected_content () const
-{
- ContentList sel;
-
- for (auto i: selected_views()) {
- sel.push_back(i->content());
- }
-
- return sel;
-}
-
-
-void
-Timeline::set_selection (ContentList selection)
-{
- for (auto i: _views) {
- auto cv = dynamic_pointer_cast<TimelineContentView> (i);
- if (cv) {
- cv->set_selected (find (selection.begin(), selection.end(), cv->content ()) != selection.end ());
- }
- }
-}
-
-
-int
-Timeline::tracks_y_offset () const
-{
- return _reels_view->bbox().height + 4;
-}
-
-
-int
-Timeline::width () const
-{
- return _main_canvas->GetVirtualSize().GetWidth();
-}
-
-
-void
-Timeline::scrolled (wxScrollWinEvent& ev)
-{
- if (ev.GetOrientation() == wxVERTICAL) {
- int x, y;
- _main_canvas->GetViewStart (&x, &y);
- _labels_canvas->Scroll (0, y);
- }
- ev.Skip ();
-}
-
-
-void
-Timeline::tool_clicked (Tool t)
-{
- switch (t) {
- case ZOOM:
- case SELECT:
- _tool = t;
- break;
- case ZOOM_ALL:
- zoom_all ();
- break;
- case SNAP:
- case SEQUENCE:
- break;
- }
-}
-
-
-void
-Timeline::zoom_all ()
-{
- auto film = _film.lock ();
- DCPOMATIC_ASSERT (film);
- set_pixels_per_second((_main_canvas->GetSize().GetWidth() - 32) / std::max(1.0, film->length().seconds()));
- set_pixels_per_track((_main_canvas->GetSize().GetHeight() - tracks_y_offset() - _time_axis_view->bbox().height - 32) / std::max(1, _tracks));
- setup_scrollbars ();
- _main_canvas->Scroll (0, 0);
- _labels_canvas->Scroll (0, 0);
- Refresh ();
-}
-
-
-void
-Timeline::keypress(wxKeyEvent const& event)
-{
- if (event.GetKeyCode() == WXK_DELETE) {
- auto film = _film.lock();
- DCPOMATIC_ASSERT(film);
- film->remove_content(selected_content());
- } else {
- switch (event.GetRawKeyCode()) {
- case '+':
- set_pixels_per_second(_pixels_per_second.get_value_or(1) * 2);
- setup_scrollbars();
- break;
- case '-':
- set_pixels_per_second(_pixels_per_second.get_value_or(1) / 2);
- setup_scrollbars();
- break;
- }
- }
-}
-
diff --git a/src/wx/timeline.h b/src/wx/timeline.h
index 621609fa7..cc35913b9 100644
--- a/src/wx/timeline.h
+++ b/src/wx/timeline.h
@@ -1,5 +1,5 @@
/*
- Copyright (C) 2013-2019 Carl Hetherington <cth@carlh.net>
+ Copyright (C) 2023 Carl Hetherington <cth@carlh.net>
This file is part of DCP-o-matic.
@@ -19,137 +19,32 @@
*/
-#include "content_menu.h"
-#include "timeline_content_view.h"
-#include "lib/film_property.h"
-#include "lib/rect.h"
+#ifndef DCPOMATIC_TIMELINE_H
+#define DCPOMATIC_TIMELINE_H
+
+
#include <dcp/warnings.h>
LIBDCP_DISABLE_WARNINGS
#include <wx/wx.h>
LIBDCP_ENABLE_WARNINGS
-#include <boost/signals2.hpp>
-
-
-class ContentPanel;
-class Film;
-class FilmViewer;
-class TimelineLabelsView;
-class TimelineReelsView;
-class TimelineTimeAxisView;
-class TimelineView;
+#include <boost/optional.hpp>
class Timeline : public wxPanel
{
public:
- Timeline (wxWindow *, ContentPanel *, std::shared_ptr<Film>, FilmViewer& viewer);
-
- std::shared_ptr<const Film> film () const;
-
- void force_redraw (dcpomatic::Rect<int> const &);
-
- int width () const;
+ explicit Timeline(wxWindow* parent);
- int pixels_per_track () const {
- return _pixels_per_track;
- }
-
- boost::optional<double> pixels_per_second () const {
+ boost::optional<double> pixels_per_second() const {
return _pixels_per_second;
}
- int tracks () const;
- void set_snap (bool s) {
- _snap = s;
- }
+protected:
+ void set_pixels_per_second(double pps);
- bool snap () const {
- return _snap;
- }
-
- void set_selection (ContentList selection);
-
- enum Tool {
- SELECT,
- ZOOM,
- ZOOM_ALL,
- SNAP,
- SEQUENCE
- };
-
- void tool_clicked (Tool t);
-
- int tracks_y_offset () const;
-
- void keypress(wxKeyEvent const &);
-
-private:
- void paint_labels ();
- void paint_main ();
- void left_down (wxMouseEvent &);
- void left_down_select (wxMouseEvent &);
- void left_up (wxMouseEvent &);
- void left_up_select (wxMouseEvent &);
- void left_up_zoom (wxMouseEvent &);
- void right_down (wxMouseEvent &);
- void right_down_select (wxMouseEvent &);
- void mouse_moved (wxMouseEvent &);
- void mouse_moved_select (wxMouseEvent &);
- void mouse_moved_zoom (wxMouseEvent &);
- void film_change(ChangeType type, FilmProperty);
- void film_content_change (ChangeType type, int, bool frequent);
- void resized ();
- void assign_tracks ();
- void set_position_from_event (wxMouseEvent& ev, bool force_emit = false);
- void clear_selection ();
- void recreate_views ();
- void setup_scrollbars ();
- void scrolled (wxScrollWinEvent& ev);
- void set_pixels_per_second (double pps);
- void set_pixels_per_track (int h);
- void zoom_all ();
- void update_playhead ();
- void mouse_wheel_turned(wxMouseEvent& event);
-
- std::shared_ptr<TimelineView> event_to_view (wxMouseEvent &);
- TimelineContentViewList selected_views () const;
- ContentList selected_content () const;
- void maybe_snap (dcpomatic::DCPTime a, dcpomatic::DCPTime b, boost::optional<dcpomatic::DCPTime>& nearest_distance) const;
-
- wxScrolledCanvas* _labels_canvas;
- wxScrolledCanvas* _main_canvas;
- ContentPanel* _content_panel;
- std::weak_ptr<Film> _film;
- FilmViewer& _viewer;
- TimelineViewList _views;
- std::shared_ptr<TimelineTimeAxisView> _time_axis_view;
- std::shared_ptr<TimelineReelsView> _reels_view;
- std::shared_ptr<TimelineLabelsView> _labels_view;
- int _tracks;
boost::optional<double> _pixels_per_second;
- bool _left_down;
- wxPoint _down_point;
- boost::optional<wxPoint> _zoom_point;
- std::shared_ptr<TimelineContentView> _down_view;
- dcpomatic::DCPTime _down_view_position;
- bool _first_move;
- ContentMenu _menu;
- bool _snap;
- std::list<dcpomatic::DCPTime> _start_snaps;
- std::list<dcpomatic::DCPTime> _end_snaps;
- Tool _tool;
- int _x_scroll_rate;
- int _y_scroll_rate;
- int _pixels_per_track;
- bool _first_resize;
- wxTimer _timer;
- boost::optional<int> _last_mouse_wheel_x;
- boost::optional<double> _last_mouse_wheel_time;
-
- static double const _minimum_pixels_per_second;
- static int const _minimum_pixels_per_track;
-
- boost::signals2::scoped_connection _film_changed_connection;
- boost::signals2::scoped_connection _film_content_change_connection;
};
+
+
+#endif
diff --git a/src/wx/timeline_content_view.cc b/src/wx/timeline_content_view.cc
index 5d039d0d3..69a675c42 100644
--- a/src/wx/timeline_content_view.cc
+++ b/src/wx/timeline_content_view.cc
@@ -19,10 +19,11 @@
*/
-#include "timeline.h"
+#include "content_timeline.h"
#include "timeline_content_view.h"
#include "wx_util.h"
#include "lib/content.h"
+#include "lib/util.h"
#include <dcp/warnings.h>
LIBDCP_DISABLE_WARNINGS
#include <wx/graphics.h>
@@ -37,8 +38,8 @@ using namespace boost::placeholders;
#endif
-TimelineContentView::TimelineContentView (Timeline& tl, shared_ptr<Content> c)
- : TimelineView (tl)
+TimelineContentView::TimelineContentView(ContentTimeline& tl, shared_ptr<Content> c)
+ : ContentTimelineView(tl)
, _content (c)
{
_content_connection = c->Change.connect (bind (&TimelineContentView::content_change, this, _1, _3));
diff --git a/src/wx/timeline_content_view.h b/src/wx/timeline_content_view.h
index 7794120cd..7b206d30f 100644
--- a/src/wx/timeline_content_view.h
+++ b/src/wx/timeline_content_view.h
@@ -23,7 +23,7 @@
#define DCPOMATIC_TIMELINE_CONTENT_VIEW_H
-#include "timeline_view.h"
+#include "content_timeline_view.h"
#include "lib/change_signaller.h"
#include <dcp/warnings.h>
LIBDCP_DISABLE_WARNINGS
@@ -37,10 +37,10 @@ class Content;
/** @class TimelineContentView
* @brief Parent class for views of pieces of content.
*/
-class TimelineContentView : public TimelineView
+class TimelineContentView : public ContentTimelineView
{
public:
- TimelineContentView (Timeline& tl, std::shared_ptr<Content> c);
+ TimelineContentView(ContentTimeline& tl, std::shared_ptr<Content> c);
dcpomatic::Rect<int> bbox () const override;
diff --git a/src/wx/timeline_labels_view.cc b/src/wx/timeline_labels_view.cc
index 181adc5ca..c869d7ec5 100644
--- a/src/wx/timeline_labels_view.cc
+++ b/src/wx/timeline_labels_view.cc
@@ -19,7 +19,7 @@
*/
-#include "timeline.h"
+#include "content_timeline.h"
#include "timeline_labels_view.h"
#include "wx_util.h"
#include <dcp/warnings.h>
@@ -34,8 +34,8 @@ using std::max;
using std::min;
-TimelineLabelsView::TimelineLabelsView (Timeline& tl)
- : TimelineView (tl)
+TimelineLabelsView::TimelineLabelsView(ContentTimeline& tl)
+ : ContentTimelineView(tl)
{
wxString labels[] = {
_("Video"),
diff --git a/src/wx/timeline_labels_view.h b/src/wx/timeline_labels_view.h
index fb80b1bf3..324531c39 100644
--- a/src/wx/timeline_labels_view.h
+++ b/src/wx/timeline_labels_view.h
@@ -19,16 +19,16 @@
*/
-#include "timeline_view.h"
+#include "content_timeline_view.h"
class wxWindow;
-class TimelineLabelsView : public TimelineView
+class TimelineLabelsView : public ContentTimelineView
{
public:
- explicit TimelineLabelsView (Timeline& tl);
+ explicit TimelineLabelsView(ContentTimeline& tl);
dcpomatic::Rect<int> bbox () const override;
diff --git a/src/wx/timeline_reels_view.cc b/src/wx/timeline_reels_view.cc
index 0601a1196..5f2a78079 100644
--- a/src/wx/timeline_reels_view.cc
+++ b/src/wx/timeline_reels_view.cc
@@ -19,7 +19,7 @@
*/
-#include "timeline.h"
+#include "content_timeline.h"
#include "timeline_reels_view.h"
#include "wx_util.h"
#include "lib/film.h"
@@ -35,8 +35,8 @@ using std::list;
using namespace dcpomatic;
-TimelineReelsView::TimelineReelsView (Timeline& tl, int y)
- : TimelineView (tl)
+TimelineReelsView::TimelineReelsView(ContentTimeline& tl, int y)
+ : ContentTimelineView(tl)
, _y (y)
{
diff --git a/src/wx/timeline_reels_view.h b/src/wx/timeline_reels_view.h
index 357fe2ce4..7dbc34308 100644
--- a/src/wx/timeline_reels_view.h
+++ b/src/wx/timeline_reels_view.h
@@ -19,13 +19,13 @@
*/
-#include "timeline_view.h"
+#include "content_timeline_view.h"
-class TimelineReelsView : public TimelineView
+class TimelineReelsView : public ContentTimelineView
{
public:
- TimelineReelsView (Timeline& tl, int y);
+ TimelineReelsView(ContentTimeline& tl, int y);
dcpomatic::Rect<int> bbox () const override;
void set_y (int y);
diff --git a/src/wx/timeline_time_axis_view.cc b/src/wx/timeline_time_axis_view.cc
index d055bda7d..d9b7710c6 100644
--- a/src/wx/timeline_time_axis_view.cc
+++ b/src/wx/timeline_time_axis_view.cc
@@ -19,7 +19,7 @@
*/
-#include "timeline.h"
+#include "content_timeline.h"
#include "timeline_time_axis_view.h"
#include "wx_util.h"
#include <dcp/warnings.h>
@@ -34,8 +34,8 @@ using std::list;
using namespace dcpomatic;
-TimelineTimeAxisView::TimelineTimeAxisView (Timeline& tl, int y)
- : TimelineView (tl)
+TimelineTimeAxisView::TimelineTimeAxisView(ContentTimeline& tl, int y)
+ : ContentTimelineView(tl)
, _y (y)
{
diff --git a/src/wx/timeline_time_axis_view.h b/src/wx/timeline_time_axis_view.h
index 4c8e090fe..f89121776 100644
--- a/src/wx/timeline_time_axis_view.h
+++ b/src/wx/timeline_time_axis_view.h
@@ -18,12 +18,14 @@
*/
-#include "timeline_view.h"
-class TimelineTimeAxisView : public TimelineView
+#include "content_timeline_view.h"
+
+
+class TimelineTimeAxisView : public ContentTimelineView
{
public:
- TimelineTimeAxisView (Timeline& tl, int y);
+ TimelineTimeAxisView(ContentTimeline& tl, int y);
dcpomatic::Rect<int> bbox () const override;
void set_y (int y);
diff --git a/src/wx/timeline_view.cc b/src/wx/timeline_view.cc
deleted file mode 100644
index 2897c98e3..000000000
--- a/src/wx/timeline_view.cc
+++ /dev/null
@@ -1,69 +0,0 @@
-/*
- Copyright (C) 2013-2021 Carl Hetherington <cth@carlh.net>
-
- This file is part of DCP-o-matic.
-
- DCP-o-matic is free software; you can redistribute it and/or modify
- it under the terms of the GNU General Public License as published by
- the Free Software Foundation; either version 2 of the License, or
- (at your option) any later version.
-
- DCP-o-matic is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU General Public License for more details.
-
- You should have received a copy of the GNU General Public License
- along with DCP-o-matic. If not, see <http://www.gnu.org/licenses/>.
-
-*/
-
-
-#include "timeline_view.h"
-#include "timeline.h"
-
-
-using std::list;
-using namespace dcpomatic;
-
-
-/** @class TimelineView
- * @brief Parent class for components of the timeline (e.g. a piece of content or an axis).
- */
-TimelineView::TimelineView (Timeline& t)
- : _timeline (t)
-{
-
-}
-
-
-void
-TimelineView::paint (wxGraphicsContext* g, list<dcpomatic::Rect<int>> overlaps)
-{
- _last_paint_bbox = bbox ();
- do_paint (g, overlaps);
-}
-
-
-void
-TimelineView::force_redraw ()
-{
- _timeline.force_redraw (_last_paint_bbox.extended(4));
- _timeline.force_redraw (bbox().extended(4));
-}
-
-
-int
-TimelineView::time_x (DCPTime t) const
-{
- return t.seconds() * _timeline.pixels_per_second().get_value_or(0);
-}
-
-
-int
-TimelineView::y_pos(int t) const
-{
- return t * _timeline.pixels_per_track() + _timeline.tracks_y_offset();
-}
-
-
diff --git a/src/wx/timeline_view.h b/src/wx/timeline_view.h
index 166a1121a..32eedde09 100644
--- a/src/wx/timeline_view.h
+++ b/src/wx/timeline_view.h
@@ -1,5 +1,5 @@
/*
- Copyright (C) 2013-2021 Carl Hetherington <cth@carlh.net>
+ Copyright (C) 2023 Carl Hetherington <cth@carlh.net>
This file is part of DCP-o-matic.
@@ -28,40 +28,42 @@
class wxGraphicsContext;
-class Timeline;
-/** @class TimelineView
- * @brief Parent class for components of the timeline (e.g. a piece of content or an axis).
+/** @class ContentTimelineView
+ * @brief Parent class for components of the content timeline (e.g. a piece of content or an axis).
*/
+template <class Timeline>
class TimelineView
{
public:
- explicit TimelineView (Timeline& t);
- virtual ~TimelineView () {}
+ explicit TimelineView(Timeline& timeline)
+ : _timeline(timeline)
+ {}
- TimelineView (TimelineView const&) = delete;
- TimelineView& operator= (TimelineView const&) = delete;
+ virtual ~TimelineView () = default;
- void paint (wxGraphicsContext* g, std::list<dcpomatic::Rect<int>> overlaps);
- void force_redraw ();
+ TimelineView(TimelineView const&) = delete;
+ TimelineView& operator=(TimelineView const&) = delete;
- virtual dcpomatic::Rect<int> bbox () const = 0;
+ void force_redraw()
+ {
+ _timeline.force_redraw(_last_paint_bbox.extended(4));
+ _timeline.force_redraw(bbox().extended(4));
+ }
-protected:
- virtual void do_paint (wxGraphicsContext *, std::list<dcpomatic::Rect<int>> overlaps) = 0;
+ virtual dcpomatic::Rect<int> bbox() const = 0;
- int time_x (dcpomatic::DCPTime t) const;
- int y_pos(int t) const;
+protected:
+ int time_x(dcpomatic::DCPTime t) const
+ {
+ return t.seconds() * _timeline.pixels_per_second().get_value_or(0);
+ }
Timeline& _timeline;
-
-private:
dcpomatic::Rect<int> _last_paint_bbox;
};
-typedef std::vector<std::shared_ptr<TimelineView>> TimelineViewList;
-
-
#endif
+
diff --git a/src/wx/try_unmount_dialog.cc b/src/wx/try_unmount_dialog.cc
index ddfaec5c0..37d6e75fa 100644
--- a/src/wx/try_unmount_dialog.cc
+++ b/src/wx/try_unmount_dialog.cc
@@ -22,6 +22,7 @@
#include "static_text.h"
#include "try_unmount_dialog.h"
#include "wx_util.h"
+#include "wx_variant.h"
#include <dcp/warnings.h>
LIBDCP_DISABLE_WARNINGS
#include <wx/wx.h>
@@ -32,14 +33,17 @@ static int constexpr width = 300;
TryUnmountDialog::TryUnmountDialog (wxWindow* parent, wxString description)
- : wxDialog (parent, wxID_ANY, _("DCP-o-matic Disk Writer"))
+ : wxDialog(parent, wxID_ANY, variant::wx::dcpomatic_disk_writer())
{
auto sizer = new wxBoxSizer (wxVERTICAL);
auto text = new StaticText (this, wxEmptyString, wxDefaultPosition, wxSize(width, -1));
sizer->Add (text, 1, wxEXPAND | wxALL, DCPOMATIC_DIALOG_BORDER);
text->SetLabelMarkup (
- wxString::Format(_("The drive <b>%s</b> is mounted.\n\nIt must be unmounted before DCP-o-matic can write to it.\n\nShould DCP-o-matic try to unmount it now?"), description)
+ wxString::Format(
+ _("The drive <b>%s</b> is mounted.\n\nIt must be unmounted before %s can write to it.\n\nShould DCP-o-matic try to unmount it now?"),
+ description, variant::wx::dcpomatic()
+ )
);
text->Wrap(width);
diff --git a/src/wx/update_dialog.cc b/src/wx/update_dialog.cc
index 0a7319704..7c687aa60 100644
--- a/src/wx/update_dialog.cc
+++ b/src/wx/update_dialog.cc
@@ -22,6 +22,7 @@
#include "static_text.h"
#include "update_dialog.h"
#include "wx_util.h"
+#include "wx_variant.h"
#include <dcp/warnings.h>
LIBDCP_DISABLE_WARNINGS
#include <wx/hyperlink.h>
@@ -40,9 +41,9 @@ UpdateDialog::UpdateDialog (wxWindow* parent, optional<string> stable, optional<
wxStaticText* message;
if ((stable || test) && !(stable && test)) {
- message = new StaticText (this, _("A new version of DCP-o-matic is available."));
+ message = new StaticText(this, variant::wx::insert_dcpomatic(_("A new version of %s is available.")));
} else {
- message = new StaticText (this, _("New versions of DCP-o-matic are available."));
+ message = new StaticText(this, variant::wx::insert_dcpomatic(_("New versions of %s are available.")));
}
overall_sizer->Add (message, 0, wxTOP | wxLEFT | wxRIGHT, DCPOMATIC_DIALOG_BORDER);
diff --git a/src/wx/verify_dcp_progress_dialog.cc b/src/wx/verify_dcp_progress_dialog.cc
index 6c941c98d..5e3a7be62 100644
--- a/src/wx/verify_dcp_progress_dialog.cc
+++ b/src/wx/verify_dcp_progress_dialog.cc
@@ -20,6 +20,7 @@
#include "verify_dcp_progress_dialog.h"
+#include "verify_dcp_progress_panel.h"
#include "wx_util.h"
#include "lib/cross.h"
#include "lib/job.h"
@@ -36,36 +37,17 @@ using std::shared_ptr;
using boost::optional;
-static int const max_file_name_length = 80;
-
-
VerifyDCPProgressDialog::VerifyDCPProgressDialog (wxWindow* parent, wxString title)
: wxDialog (parent, wxID_ANY, title)
+ , _panel(new VerifyDCPProgressPanel(this))
, _cancel (false)
{
- wxBoxSizer* overall_sizer = new wxBoxSizer (wxVERTICAL);
-
- _job_name = new wxStaticText (this, wxID_ANY, wxT(""));
- overall_sizer->Add (_job_name, 0, wxEXPAND | wxTOP | wxLEFT | wxRIGHT, DCPOMATIC_SIZER_GAP);
-
- _file_name = new wxStaticText (this, wxID_ANY, wxT(""));
- wxFont file_name_font (*wxNORMAL_FONT);
- file_name_font.SetFamily (wxFONTFAMILY_MODERN);
- file_name_font.SetPointSize (file_name_font.GetPointSize() - 2);
- _file_name->SetFont (file_name_font);
-
- int w;
- int h;
- _file_name->GetTextExtent (std_to_wx(string(max_file_name_length, 'X')), &w, &h);
- _file_name->SetMinSize (wxSize(w, -1));
+ auto overall_sizer = new wxBoxSizer (wxVERTICAL);
- overall_sizer->Add (_file_name, 0, wxEXPAND | wxLEFT | wxRIGHT | wxTOP, DCPOMATIC_SIZER_GAP);
+ overall_sizer->Add(_panel, 0, wxEXPAND | wxALL, DCPOMATIC_SIZER_GAP);
- _progress = new wxGauge (this, wxID_ANY, 100);
- overall_sizer->Add (_progress, 0, wxEXPAND | wxALL, DCPOMATIC_SIZER_GAP);
-
- wxButton* cancel = new wxButton (this, wxID_ANY, _("Cancel"));
- wxSizer* buttons = new wxBoxSizer (wxHORIZONTAL);
+ auto cancel = new wxButton(this, wxID_ANY, _("Cancel"));
+ auto buttons = new wxBoxSizer(wxHORIZONTAL);
buttons->AddStretchSpacer ();
buttons->Add (cancel, 0);
overall_sizer->Add (buttons, 0, wxEXPAND | wxALL, DCPOMATIC_SIZER_GAP);
@@ -94,26 +76,8 @@ VerifyDCPProgressDialog::run (shared_ptr<Job> job)
while (jm->work_to_do()) {
wxEventLoopBase::GetActive()->YieldFor(wxEVT_CATEGORY_UI | wxEVT_CATEGORY_USER_INPUT);
dcpomatic_sleep_seconds (1);
- optional<float> const progress = job->progress ();
- if (progress) {
- _progress->SetValue (*progress * 100);
- } else {
- _progress->Pulse ();
- }
- string const sub = job->sub_name ();
- size_t colon = sub.find (":");
- if (colon != string::npos) {
- _job_name->SetLabel (std_to_wx(sub.substr(0, colon)));
- string file_name;
- if ((sub.length() - colon - 1) > max_file_name_length) {
- file_name = "..." + sub.substr(sub.length() - max_file_name_length + 3);
- } else {
- file_name = sub.substr(colon + 1);
- }
- _file_name->SetLabel (std_to_wx(file_name));
- } else {
- _job_name->SetLabel (std_to_wx(sub));
- }
+
+ _panel->update(job);
if (_cancel) {
break;
diff --git a/src/wx/verify_dcp_progress_dialog.h b/src/wx/verify_dcp_progress_dialog.h
index 190c302ea..1ae3336e8 100644
--- a/src/wx/verify_dcp_progress_dialog.h
+++ b/src/wx/verify_dcp_progress_dialog.h
@@ -25,10 +25,9 @@ LIBDCP_DISABLE_WARNINGS
LIBDCP_ENABLE_WARNINGS
#include <memory>
-class Job;
-class wxGauge;
-class wxStaticText;
+class Job;
+class VerifyDCPProgressPanel;
class VerifyDCPProgressDialog : public wxDialog
@@ -41,9 +40,7 @@ public:
private:
void cancel ();
- wxStaticText* _job_name;
- wxStaticText* _file_name;
- wxGauge* _progress;
+ VerifyDCPProgressPanel* _panel;
bool _cancel;
};
diff --git a/src/wx/verify_dcp_progress_panel.cc b/src/wx/verify_dcp_progress_panel.cc
new file mode 100644
index 000000000..4c4ce6883
--- /dev/null
+++ b/src/wx/verify_dcp_progress_panel.cc
@@ -0,0 +1,87 @@
+/*
+ Copyright (C) 2020 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/job.h"
+#include "verify_dcp_progress_panel.h"
+#include "wx_util.h"
+
+
+using std::shared_ptr;
+using std::string;
+
+
+auto constexpr max_file_name_length = 80;
+
+
+
+VerifyDCPProgressPanel::VerifyDCPProgressPanel(wxWindow* parent)
+ : wxPanel(parent, wxID_ANY)
+{
+ auto overall_sizer = new wxBoxSizer(wxVERTICAL);
+
+ _job_name = new wxStaticText(this, wxID_ANY, wxT(""));
+ overall_sizer->Add(_job_name, 0, wxEXPAND | wxTOP | wxLEFT | wxRIGHT, DCPOMATIC_SIZER_GAP);
+
+ _file_name = new wxStaticText(this, wxID_ANY, wxT(""));
+ wxFont file_name_font(*wxNORMAL_FONT);
+ file_name_font.SetFamily(wxFONTFAMILY_MODERN);
+ file_name_font.SetPointSize(file_name_font.GetPointSize() - 2);
+ _file_name->SetFont(file_name_font);
+
+ int w;
+ int h;
+ _file_name->GetTextExtent(std_to_wx(string(max_file_name_length, 'X')), &w, &h);
+ _file_name->SetMinSize(wxSize(w, -1));
+
+ overall_sizer->Add(_file_name, 0, wxEXPAND | wxLEFT | wxRIGHT | wxTOP, DCPOMATIC_SIZER_GAP);
+
+ _progress = new wxGauge(this, wxID_ANY, 100);
+ overall_sizer->Add(_progress, 0, wxEXPAND | wxALL, DCPOMATIC_SIZER_GAP);
+
+ SetSizerAndFit (overall_sizer);
+}
+
+
+void
+VerifyDCPProgressPanel::update(shared_ptr<Job> job)
+{
+ auto const progress = job->progress();
+ if (progress) {
+ _progress->SetValue(*progress * 100);
+ } else {
+ _progress->Pulse();
+ }
+ string const sub = job->sub_name();
+ size_t colon = sub.find(":");
+ if (colon != string::npos) {
+ _job_name->SetLabel(std_to_wx(sub.substr(0, colon)));
+ string file_name;
+ if ((sub.length() - colon - 1) > max_file_name_length) {
+ file_name = "..." + sub.substr(sub.length() - max_file_name_length + 3);
+ } else {
+ file_name = sub.substr(colon + 1);
+ }
+ _file_name->SetLabel(std_to_wx(file_name));
+ } else {
+ _job_name->SetLabel(std_to_wx(sub));
+ }
+}
+
diff --git a/src/wx/verify_dcp_progress_panel.h b/src/wx/verify_dcp_progress_panel.h
new file mode 100644
index 000000000..3fde50827
--- /dev/null
+++ b/src/wx/verify_dcp_progress_panel.h
@@ -0,0 +1,44 @@
+/*
+ Copyright (C) 2020 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/warnings.h>
+LIBDCP_DISABLE_WARNINGS
+#include <wx/wx.h>
+LIBDCP_ENABLE_WARNINGS
+#include <memory>
+
+
+class Job;
+
+
+class VerifyDCPProgressPanel : public wxPanel
+{
+public:
+ VerifyDCPProgressPanel(wxWindow* parent);
+
+ void update(std::shared_ptr<Job> job);
+
+private:
+ wxStaticText* _job_name;
+ wxStaticText* _file_name;
+ wxGauge* _progress;
+};
+
diff --git a/src/wx/verify_dcp_result_dialog.cc b/src/wx/verify_dcp_result_dialog.cc
new file mode 100644
index 000000000..806eac85a
--- /dev/null
+++ b/src/wx/verify_dcp_result_dialog.cc
@@ -0,0 +1,47 @@
+/*
+ Copyright (C) 2018-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 "verify_dcp_result_dialog.h"
+#include "verify_dcp_result_panel.h"
+#include "wx_util.h"
+
+
+using std::shared_ptr;
+
+
+VerifyDCPResultDialog::VerifyDCPResultDialog(wxWindow* parent, shared_ptr<VerifyDCPJob> job)
+ : wxDialog (parent, wxID_ANY, _("DCP verification"), wxDefaultPosition, {600, 400})
+{
+ auto sizer = new wxBoxSizer (wxVERTICAL);
+
+ auto panel = new VerifyDCPResultPanel(this);
+ panel->fill(job);
+ sizer->Add(panel, 1, wxEXPAND);
+
+ auto buttons = CreateStdDialogButtonSizer(0);
+ sizer->Add (CreateSeparatedSizer(buttons), wxSizerFlags().Expand().DoubleBorder());
+ buttons->SetAffirmativeButton (new wxButton (this, wxID_OK));
+ buttons->Realize ();
+
+ SetSizer (sizer);
+ sizer->Layout ();
+ sizer->SetSizeHints (this);
+}
diff --git a/src/wx/verify_dcp_dialog.h b/src/wx/verify_dcp_result_dialog.h
index 076217b1f..8953ff8c5 100644
--- a/src/wx/verify_dcp_dialog.h
+++ b/src/wx/verify_dcp_result_dialog.h
@@ -30,8 +30,8 @@ class wxRichTextCtrl;
class VerifyDCPJob;
-class VerifyDCPDialog : public wxDialog
+class VerifyDCPResultDialog : public wxDialog
{
public:
- VerifyDCPDialog (wxWindow* parent, std::shared_ptr<VerifyDCPJob> job);
+ VerifyDCPResultDialog(wxWindow* parent, std::shared_ptr<VerifyDCPJob> job);
};
diff --git a/src/wx/verify_dcp_dialog.cc b/src/wx/verify_dcp_result_panel.cc
index c7a32e5dd..d10c94350 100644
--- a/src/wx/verify_dcp_dialog.cc
+++ b/src/wx/verify_dcp_result_panel.cc
@@ -1,5 +1,5 @@
/*
- Copyright (C) 2018-2021 Carl Hetherington <cth@carlh.net>
+ Copyright (C) 2018 Carl Hetherington <cth@carlh.net>
This file is part of DCP-o-matic.
@@ -19,11 +19,14 @@
*/
-#include "verify_dcp_dialog.h"
+#include "dcpomatic_button.h"
+#include "file_dialog.h"
+#include "verify_dcp_result_panel.h"
#include "wx_util.h"
#include "lib/verify_dcp_job.h"
#include <dcp/raw_convert.h>
#include <dcp/verify.h>
+#include <dcp/verify_report.h>
#include <dcp/warnings.h>
LIBDCP_DISABLE_WARNINGS
#include <wx/richtext/richtextctrl.h>
@@ -39,55 +42,66 @@ using std::string;
using std::vector;
-VerifyDCPDialog::VerifyDCPDialog (wxWindow* parent, shared_ptr<VerifyDCPJob> job)
- : wxDialog (parent, wxID_ANY, _("DCP verification"), wxDefaultPosition, {600, 400})
+VerifyDCPResultPanel::VerifyDCPResultPanel(wxWindow* parent)
+ : wxPanel(parent, wxID_ANY)
{
- auto sizer = new wxBoxSizer (wxVERTICAL);
- auto notebook = new wxNotebook (this, wxID_ANY);
- sizer->Add (notebook, 1, wxEXPAND | wxALL, DCPOMATIC_DIALOG_BORDER);
-
- map<dcp::VerificationNote::Type, wxRichTextCtrl*> pages;
- pages[dcp::VerificationNote::Type::ERROR] = new wxRichTextCtrl (notebook, wxID_ANY, wxEmptyString, wxDefaultPosition, {400, 300}, wxRE_READONLY);
- notebook->AddPage (pages[dcp::VerificationNote::Type::ERROR], _("Errors"));
- pages[dcp::VerificationNote::Type::BV21_ERROR] = new wxRichTextCtrl (notebook, wxID_ANY, wxEmptyString, wxDefaultPosition, {400, 300}, wxRE_READONLY);
- notebook->AddPage (pages[dcp::VerificationNote::Type::BV21_ERROR], _("SMPTE Bv2.1 errors"));
- pages[dcp::VerificationNote::Type::WARNING] = new wxRichTextCtrl (notebook, wxID_ANY, wxEmptyString, wxDefaultPosition, {400, 300}, wxRE_READONLY);
- notebook->AddPage (pages[dcp::VerificationNote::Type::WARNING], _("Warnings"));
-
- auto summary = new wxStaticText (this, wxID_ANY, wxT(""));
- sizer->Add (summary, 0, wxALL, DCPOMATIC_DIALOG_BORDER);
-
- auto buttons = CreateStdDialogButtonSizer (0);
- sizer->Add (CreateSeparatedSizer(buttons), wxSizerFlags().Expand().DoubleBorder());
- buttons->SetAffirmativeButton (new wxButton (this, wxID_OK));
- buttons->Realize ();
-
- SetSizer (sizer);
- sizer->Layout ();
- sizer->SetSizeHints (this);
-
- for (auto const& i: pages) {
+ auto sizer = new wxBoxSizer(wxVERTICAL);
+ auto notebook = new wxNotebook(this, wxID_ANY);
+ sizer->Add(notebook, 1, wxEXPAND | wxALL, DCPOMATIC_DIALOG_BORDER);
+
+ _pages[dcp::VerificationNote::Type::ERROR] = new wxRichTextCtrl(notebook, wxID_ANY, wxEmptyString, wxDefaultPosition, {400, 300}, wxRE_READONLY);
+ notebook->AddPage(_pages[dcp::VerificationNote::Type::ERROR], _("Errors"));
+ _pages[dcp::VerificationNote::Type::BV21_ERROR] = new wxRichTextCtrl(notebook, wxID_ANY, wxEmptyString, wxDefaultPosition, {400, 300}, wxRE_READONLY);
+ notebook->AddPage(_pages[dcp::VerificationNote::Type::BV21_ERROR], _("SMPTE Bv2.1 errors"));
+ _pages[dcp::VerificationNote::Type::WARNING] = new wxRichTextCtrl(notebook, wxID_ANY, wxEmptyString, wxDefaultPosition, {400, 300}, wxRE_READONLY);
+ notebook->AddPage(_pages[dcp::VerificationNote::Type::WARNING], _("Warnings"));
+
+ _summary = new wxStaticText(this, wxID_ANY, wxT(""));
+ sizer->Add(_summary, 0, wxALL, DCPOMATIC_DIALOG_BORDER);
+
+ auto save_sizer = new wxBoxSizer(wxHORIZONTAL);
+ _save_text_report = new Button(this, _("Save report as text..."));
+ save_sizer->Add(_save_text_report, 0, wxALL, DCPOMATIC_SIZER_GAP);
+ _save_html_report = new Button(this, _("Save report as HTML..."));
+ save_sizer->Add(_save_html_report, 0, wxALL, DCPOMATIC_SIZER_GAP);
+ sizer->Add(save_sizer);
+
+ SetSizer(sizer);
+ sizer->Layout();
+ sizer->SetSizeHints(this);
+
+ for (auto const& i: _pages) {
i.second->GetCaret()->Hide();
}
- if (job->finished_ok() && job->notes().empty()) {
- summary->SetLabel (_("DCP validates OK."));
+ _save_text_report->bind(&VerifyDCPResultPanel::save_text_report, this);
+ _save_html_report->bind(&VerifyDCPResultPanel::save_html_report, this);
+
+ _save_text_report->Enable(false);
+ _save_html_report->Enable(false);
+}
+
+
+void
+VerifyDCPResultPanel::fill(shared_ptr<VerifyDCPJob> job)
+{
+ if (job->finished_ok() && job->result().notes.empty()) {
+ _summary->SetLabel(_("DCP validates OK."));
return;
}
- map<dcp::VerificationNote::Type, int> counts;
- counts[dcp::VerificationNote::Type::WARNING] = 0;
- counts[dcp::VerificationNote::Type::BV21_ERROR] = 0;
- counts[dcp::VerificationNote::Type::ERROR] = 0;
-
- auto add_bullet = [&pages](dcp::VerificationNote::Type type, wxString message) {
- pages[type]->BeginStandardBullet(N_("standard/diamond"), 1, 50);
- pages[type]->WriteText (message);
- pages[type]->Newline ();
- pages[type]->EndStandardBullet ();
+ vector<dcp::VerificationNote::Type> types = {
+ dcp::VerificationNote::Type::WARNING,
+ dcp::VerificationNote::Type::BV21_ERROR,
+ dcp::VerificationNote::Type::ERROR
};
- auto add = [&counts, &add_bullet](dcp::VerificationNote note, wxString message) {
+ map<dcp::VerificationNote::Type, std::vector<wxString>> notes;
+ for (auto type: types) {
+ notes[type] = {};
+ }
+
+ auto add = [&notes](dcp::VerificationNote note, wxString message) {
if (note.reference_hash()) {
message.Replace("%reference_hash", std_to_wx(note.reference_hash().get()));
}
@@ -123,23 +137,25 @@ VerifyDCPDialog::VerifyDCPDialog (wxWindow* parent, shared_ptr<VerifyDCPJob> job
if (note.other_id()) {
message.Replace("%other_id", std_to_wx(note.other_id().get()));
}
- add_bullet (note.type(), message);
- counts[note.type()]++;
+ if (note.cpl_id()) {
+ message.Replace("%cpl", std_to_wx(note.cpl_id().get()));
+ }
+
+ notes[note.type()].push_back(message);
};
if (job->finished_in_error() && job->error_summary() != "") {
/* We have an error that did not come from dcp::verify */
- add_bullet (dcp::VerificationNote::Type::ERROR, std_to_wx(job->error_summary()));
- ++counts[dcp::VerificationNote::Type::ERROR];
+ notes[dcp::VerificationNote::Type::ERROR].push_back(std_to_wx(job->error_summary()));
}
- for (auto i: job->notes()) {
+ for (auto i: job->result().notes) {
switch (i.code()) {
case dcp::VerificationNote::Code::FAILED_READ:
- add (i, _("Could not read DCP (%n)"));
+ add(i, _("Could not read DCP (%n)"));
break;
case dcp::VerificationNote::Code::MISMATCHED_CPL_HASHES:
- add(i, _("The hash (%reference_hash) of the CPL %n in the PKL does not agree with the CPL file (%calculated_hash). This probably means that the CPL file is corrupt."));
+ add(i, _("The hash (%reference_hash) of the CPL %cpl in the PKL does not agree with the CPL file (%calculated_hash). This probably means that the CPL file is corrupt."));
break;
case dcp::VerificationNote::Code::INVALID_PICTURE_FRAME_RATE:
add(i, _("The picture in a reel has a frame rate of %n, which is not valid."));
@@ -259,11 +275,14 @@ VerifyDCPDialog::VerifyDCPDialog (wxWindow* parent, shared_ptr<VerifyDCPJob> job
case dcp::VerificationNote::Code::INVALID_SOUND_FRAME_RATE:
add(i, _("The sound asset %f has an invalid frame rate of %n."));
break;
+ case dcp::VerificationNote::Code::INVALID_SOUND_BIT_DEPTH:
+ add(i, _("The sound asset %f has an invalid bit depth of %n."));
+ break;
case dcp::VerificationNote::Code::MISSING_CPL_ANNOTATION_TEXT:
- add(i, _("The CPL %n has no <AnnotationText> tag."));
+ add(i, _("The CPL %cpl has no <AnnotationText> tag."));
break;
case dcp::VerificationNote::Code::MISMATCHED_CPL_ANNOTATION_TEXT:
- add(i, _("The CPL %n has an <AnnotationText> which is not the same as its <ContentTitleText>."));
+ add(i, _("The CPL %cpcpl has an <AnnotationText> which is not the same as its <ContentTitleText>."));
break;
case dcp::VerificationNote::Code::MISMATCHED_ASSET_DURATION:
add(i, _("At least one asset in a reel does not have the same duration as the others."));
@@ -308,19 +327,19 @@ VerifyDCPDialog::VerifyDCPDialog (wxWindow* parent, shared_ptr<VerifyDCPJob> job
add(i, _("The DCP has a LFOC of %n instead of the reel duration minus one."));
break;
case dcp::VerificationNote::Code::MISSING_CPL_METADATA:
- add(i, _("The CPL %n has no CPL metadata tag."));
+ add(i, _("The CPL %cpl has no CPL metadata tag."));
break;
case dcp::VerificationNote::Code::MISSING_CPL_METADATA_VERSION_NUMBER:
- add(i, _("The CPL %n has no CPL metadata version number tag."));
+ add(i, _("The CPL %cpl has no CPL metadata version number tag."));
break;
case dcp::VerificationNote::Code::MISSING_EXTENSION_METADATA:
- add(i, _("The CPL %n has no CPL extension metadata tag."));
+ add(i, _("The CPL %cpl has no CPL extension metadata tag."));
break;
case dcp::VerificationNote::Code::INVALID_EXTENSION_METADATA:
add(i, _("The CPL %f has an invalid CPL extension metadata tag (%n)"));
break;
case dcp::VerificationNote::Code::UNSIGNED_CPL_WITH_ENCRYPTED_CONTENT:
- add(i, _("The CPL %n has encrypted content but is not signed."));
+ add(i, _("The CPL %cpl has encrypted content but is not signed."));
break;
case dcp::VerificationNote::Code::UNSIGNED_PKL_WITH_ENCRYPTED_CONTENT:
add(i, _("The PKL %n has encrypted content but is not signed."));
@@ -382,7 +401,7 @@ VerifyDCPDialog::VerifyDCPDialog (wxWindow* parent, shared_ptr<VerifyDCPJob> job
case dcp::VerificationNote::Code::MISMATCHED_TIMED_TEXT_DURATION:
{
vector<string> parts;
- boost::split (parts, i.note().get(), boost::is_any_of(" "));
+ boost::split(parts, i.note().get(), boost::is_any_of(" "));
add(i, wxString::Format(_("The reel duration (%s) of some timed text is not the same as the ContainerDuration (%s) of its MXF."), std_to_wx(parts[0]), std_to_wx(parts[1])));
break;
}
@@ -447,54 +466,127 @@ VerifyDCPDialog::VerifyDCPDialog (wxWindow* parent, shared_ptr<VerifyDCPJob> job
add(i, _("The asset with ID %id in the asset map actually has an id of %other_id"));
break;
case dcp::VerificationNote::Code::EMPTY_CONTENT_VERSION_LABEL_TEXT:
- add(i, _("The <LabelText> in a <ContentVersion> in CPL %id is empty"));
+ add(i, _("The <LabelText> in a <ContentVersion> in CPL %cpl is empty"));
break;
case dcp::VerificationNote::Code::INVALID_CPL_NAMESPACE:
- add(i, _("The CPL %f has an invalid namespace %n"));
+ add(i, _("The CPL %cpl has an invalid namespace %n"));
break;
case dcp::VerificationNote::Code::MISSING_CPL_CONTENT_VERSION:
- add(i, _("The CPL %n has no <ContentVersion> tag"));
+ add(i, _("The CPL %cpl has no <ContentVersion> tag"));
+ break;
+ case dcp::VerificationNote::Code::MATCHING_CPL_HASHES:
+ case dcp::VerificationNote::Code::CORRECT_PICTURE_HASH:
+ case dcp::VerificationNote::Code::VALID_PICTURE_FRAME_SIZES_IN_BYTES:
+ case dcp::VerificationNote::Code::VALID_RELEASE_TERRITORY:
+ case dcp::VerificationNote::Code::VALID_CPL_ANNOTATION_TEXT:
+ case dcp::VerificationNote::Code::MATCHING_PKL_ANNOTATION_TEXT_WITH_CPL:
+ case dcp::VerificationNote::Code::ALL_ENCRYPTED:
+ case dcp::VerificationNote::Code::NONE_ENCRYPTED:
+ case dcp::VerificationNote::Code::VALID_CONTENT_KIND:
+ case dcp::VerificationNote::Code::VALID_MAIN_PICTURE_ACTIVE_AREA:
+ case dcp::VerificationNote::Code::VALID_CONTENT_VERSION_LABEL_TEXT:
+ /* These are all "OK" messages which we don't report here */
break;
}
}
wxString summary_text;
- if (counts[dcp::VerificationNote::Type::ERROR] == 1) {
+ if (notes[dcp::VerificationNote::Type::ERROR].size() == 1) {
/// TRANSLATORS: this will be used at the start of a string like "1 error, 2 Bv2.1 errors and 3 warnings."
summary_text = _("1 error, ");
} else {
/// TRANSLATORS: this will be used at the start of a string like "1 error, 2 Bv2.1 errors and 3 warnings."
- summary_text = wxString::Format("%d errors, ", counts[dcp::VerificationNote::Type::ERROR]);
+ summary_text = wxString::Format("%d errors, ", static_cast<int>(notes[dcp::VerificationNote::Type::ERROR].size()));
}
- if (counts[dcp::VerificationNote::Type::BV21_ERROR] == 1) {
+ if (notes[dcp::VerificationNote::Type::BV21_ERROR].size() == 1) {
/// TRANSLATORS: this will be used in the middle of a string like "1 error, 2 Bv2.1 errors and 3 warnings."
summary_text += _("1 Bv2.1 error, ");
} else {
/// TRANSLATORS: this will be used in the middle of a string like "1 error, 2 Bv2.1 errors and 3 warnings."
- summary_text += wxString::Format("%d Bv2.1 errors, ", counts[dcp::VerificationNote::Type::BV21_ERROR]);
+ summary_text += wxString::Format("%d Bv2.1 errors, ", static_cast<int>(notes[dcp::VerificationNote::Type::BV21_ERROR].size()));
}
- if (counts[dcp::VerificationNote::Type::WARNING] == 1) {
+ if (notes[dcp::VerificationNote::Type::WARNING].size() == 1) {
/// TRANSLATORS: this will be used at the end of a string like "1 error, 2 Bv2.1 errors and 3 warnings."
summary_text += _("and 1 warning.");
} else {
/// TRANSLATORS: this will be used at the end of a string like "1 error, 2 Bv2.1 errors and 3 warnings."
- summary_text += wxString::Format("and %d warnings.", counts[dcp::VerificationNote::Type::WARNING]);
+ summary_text += wxString::Format("and %d warnings.", static_cast<int>(notes[dcp::VerificationNote::Type::WARNING].size()));
+ }
+
+ _summary->SetLabel(summary_text);
+
+ auto add_bullet = [this](dcp::VerificationNote::Type type, wxString message) {
+ _pages[type]->BeginStandardBullet(N_("standard/diamond"), 1, 50);
+ _pages[type]->WriteText(message);
+ _pages[type]->Newline();
+ _pages[type]->EndStandardBullet();
+ };
+
+ if (notes[dcp::VerificationNote::Type::ERROR].empty()) {
+ add_bullet(dcp::VerificationNote::Type::ERROR, _("No errors found."));
}
- summary->SetLabel(summary_text);
+ if (notes[dcp::VerificationNote::Type::BV21_ERROR].empty()) {
+ add_bullet(dcp::VerificationNote::Type::BV21_ERROR, _("No SMPTE Bv2.1 errors found."));
+ }
- if (counts[dcp::VerificationNote::Type::ERROR] == 0) {
- add_bullet (dcp::VerificationNote::Type::ERROR, _("No errors found."));
+ if (notes[dcp::VerificationNote::Type::WARNING].empty()) {
+ add_bullet(dcp::VerificationNote::Type::WARNING, _("No warnings found."));
}
- if (counts[dcp::VerificationNote::Type::BV21_ERROR] == 0) {
- add_bullet (dcp::VerificationNote::Type::BV21_ERROR, _("No SMPTE Bv2.1 errors found."));
+ for (auto type: types) {
+ std::sort(notes[type].begin(), notes[type].end());
+ for (auto i = notes[type].begin(); i != notes[type].end(); ++i) {
+ int extra = 0;
+ while (std::next(i) != notes[type].end() && *std::next(i) == *i) {
+ ++i;
+ ++extra;
+ }
+ if (extra == 1) {
+ add_bullet(type, wxString::Format(_("%s (repeated twice)."), i->SubString(0, i->Length() - 2)));
+ } else if (extra > 1) {
+ add_bullet(type, wxString::Format(_("%s (repeated %d times)."), i->SubString(0, i->Length() - 2), extra + 1));
+ } else {
+ add_bullet(type, *i);
+ }
+ }
}
- if (counts[dcp::VerificationNote::Type::WARNING] == 0) {
- add_bullet (dcp::VerificationNote::Type::WARNING, _("No warnings found."));
+ _job = job;
+ _save_text_report->Enable(true);
+ _save_html_report->Enable(true);
+}
+
+
+template <class T>
+void save(wxWindow* parent, wxString filter, dcp::VerificationResult const& result)
+{
+ FileDialog dialog(parent, _("Verification report"), filter, wxFD_SAVE | wxFD_OVERWRITE_PROMPT, "SaveVerificationReport");
+ if (!dialog.show()) {
+ return;
+ }
+
+ T formatter(dialog.path());
+ dcp::verify_report(result, formatter);
+}
+
+
+void
+VerifyDCPResultPanel::save_text_report()
+{
+ if (_job) {
+ save<dcp::TextFormatter>(this, wxT("Text files (*.txt)|*.txt"), _job->result());
+ }
+}
+
+
+void
+VerifyDCPResultPanel::save_html_report()
+{
+ if (_job) {
+ save<dcp::HTMLFormatter>(this, wxT("HTML files (*.htm;*html)|*.htm;*.html"), _job->result());
}
}
diff --git a/src/wx/verify_dcp_result_panel.h b/src/wx/verify_dcp_result_panel.h
new file mode 100644
index 000000000..8cf92118b
--- /dev/null
+++ b/src/wx/verify_dcp_result_panel.h
@@ -0,0 +1,50 @@
+/*
+ Copyright (C) 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 <dcp/verify.h>
+#include <wx/wx.h>
+#include <map>
+#include <memory>
+
+
+class Button;
+class VerifyDCPJob;
+class wxRichTextCtrl;
+
+
+class VerifyDCPResultPanel : public wxPanel
+{
+public:
+ VerifyDCPResultPanel(wxWindow* parent);
+
+ void fill(std::shared_ptr<VerifyDCPJob> job);
+
+private:
+ void save_text_report();
+ void save_html_report();
+
+ wxStaticText* _summary;
+ std::map<dcp::VerificationNote::Type, wxRichTextCtrl*> _pages;
+ Button* _save_text_report;
+ Button* _save_html_report;
+
+ std::shared_ptr<VerifyDCPJob> _job;
+};
diff --git a/src/wx/video_panel.cc b/src/wx/video_panel.cc
index e065f9bb4..65f381fe3 100644
--- a/src/wx/video_panel.cc
+++ b/src/wx/video_panel.cc
@@ -76,14 +76,6 @@ VideoPanel::VideoPanel (ContentPanel* p)
void
VideoPanel::create ()
{
- _reference = new CheckBox (this, _("Use this DCP's video as OV and make VF"));
- _reference_note = new StaticText (this, wxT(""));
- _reference_note->Wrap (200);
- auto font = _reference_note->GetFont();
- font.SetStyle(wxFONTSTYLE_ITALIC);
- font.SetPointSize(font.GetPointSize() - 1);
- _reference_note->SetFont(font);
-
_type_label = create_label (this, _("Type"), true);
_frame_type = new ContentChoice<VideoContent, VideoFrameType> (
this,
@@ -207,6 +199,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);
@@ -227,7 +222,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));
@@ -248,12 +242,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;
@@ -462,15 +450,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);
@@ -598,15 +577,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()) {
@@ -615,7 +586,7 @@ VideoPanel::setup_sensitivity ()
}
}
- bool const enable = !_reference->GetValue() && any_use;
+ bool const enable = !reference && any_use;
if (!enable) {
_frame_type->wrapped()->Enable (false);
@@ -688,23 +659,6 @@ VideoPanel::fade_out_changed ()
void
-VideoPanel::reference_clicked ()
-{
- auto c = _parent->selected ();
- if (c.size() != 1) {
- return;
- }
-
- auto d = dynamic_pointer_cast<DCPContent> (c.front ());
- if (!d) {
- return;
- }
-
- d->set_reference_video (_reference->GetValue ());
-}
-
-
-void
VideoPanel::scale_fit_clicked ()
{
for (auto i: _parent->selected_video()) {
diff --git a/src/wx/video_panel.h b/src/wx/video_panel.h
index 686d1b99b..e6b689b03 100644
--- a/src/wx/video_panel.h
+++ b/src/wx/video_panel.h
@@ -53,7 +53,6 @@ public:
void content_selection_changed () override;
private:
- void reference_clicked ();
void colour_conversion_changed ();
void edit_colour_conversion_clicked ();
void range_changed ();
@@ -73,8 +72,6 @@ private:
void setup_description ();
void setup_sensitivity ();
- CheckBox* _reference;
- wxStaticText* _reference_note;
wxStaticText* _type_label;
ContentChoice<VideoContent, VideoFrameType>* _frame_type;
wxStaticText* _crop_label;
diff --git a/src/wx/video_view.h b/src/wx/video_view.h
index 387cca9f6..3ea03a5fd 100644
--- a/src/wx/video_view.h
+++ b/src/wx/video_view.h
@@ -23,6 +23,7 @@
#define DCPOMATIC_VIDEO_VIEW_H
+#include "optimisation.h"
#include "lib/dcpomatic_time.h"
#include "lib/exception_store.h"
#include "lib/signaller.h"
@@ -131,8 +132,8 @@ public:
_three_d = t;
}
- void set_optimise_for_j2k (bool o) {
- _optimise_for_j2k = o;
+ void set_optimisation(Optimisation o) {
+ _optimisation = o;
}
protected:
@@ -180,7 +181,7 @@ protected:
StateTimer _state_timer;
- bool _optimise_for_j2k = false;
+ Optimisation _optimisation = Optimisation::NONE;
private:
/** Mutex protecting all the state in this class */
diff --git a/src/wx/window_metrics.cc b/src/wx/window_metrics.cc
new file mode 100644
index 000000000..4b0c6ce66
--- /dev/null
+++ b/src/wx/window_metrics.cc
@@ -0,0 +1,45 @@
+/*
+ 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 "window_metrics.h"
+#include <dcp/warnings.h>
+LIBDCP_DISABLE_WARNINGS
+#include <wx/app.h>
+#include <wx/wx.h>
+LIBDCP_ENABLE_WARNINGS
+
+
+void
+WindowMetrics::show(wxWindow* window) const
+{
+#ifdef DCPOMATIC_LINUX
+ auto const position_before = position;
+#endif
+
+ window->Show();
+
+#ifdef DCPOMATIC_LINUX
+ wxTheApp->CallAfter([window, position_before] {
+ window->SetPosition(position_before);
+ });
+#endif
+}
+
diff --git a/src/wx/window_metrics.h b/src/wx/window_metrics.h
new file mode 100644
index 000000000..30a921134
--- /dev/null
+++ b/src/wx/window_metrics.h
@@ -0,0 +1,39 @@
+/*
+ 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 <dcp/warnings.h>
+LIBDCP_DISABLE_WARNINGS
+#include <wx/gdicmn.h>
+LIBDCP_ENABLE_WARNINGS
+
+
+class wxWindow;
+
+
+class WindowMetrics
+{
+public:
+ void show(wxWindow* window) const;
+
+ wxPoint position = wxDefaultPosition;
+ wxSize size = wxDefaultSize;
+};
+
diff --git a/src/wx/wscript b/src/wx/wscript
index a6eefa69f..0255149ed 100644
--- a/src/wx/wscript
+++ b/src/wx/wscript
@@ -48,12 +48,23 @@ sources = """
content_panel.cc
content_properties_dialog.cc
content_sub_panel.cc
+ content_timeline.cc
+ content_timeline_atmos_view.cc
+ content_timeline_audio_view.cc
+ content_timeline_dialog.cc
+ content_timeline_text_view.cc
+ content_timeline_video_view.cc
+ content_timeline_view.cc
content_version_dialog.cc
content_view.cc
controls.cc
credentials_download_certificate_panel.cc
custom_scale_dialog.cc
+ dcp_referencing_dialog.cc
dcp_panel.cc
+ dcp_timeline.cc
+ dcp_timeline_dialog.cc
+ dcp_timeline_reel_marker_view.cc
dcp_text_track_dialog.cc
dcpomatic_button.cc
dcpomatic_choice.cc
@@ -102,6 +113,7 @@ sources = """
language_subtag_panel.cc
language_tag_dialog.cc
language_tag_widget.cc
+ load_config_from_zip_dialog.cc
kdm_choice.cc
make_chain_dialog.cc
markers.cc
@@ -159,27 +171,25 @@ 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
- verify_dcp_dialog.cc
+ verify_dcp_result_dialog.cc
+ verify_dcp_result_panel.cc
verify_dcp_progress_dialog.cc
+ verify_dcp_progress_panel.cc
video_panel.cc
video_view.cc
video_waveform_dialog.cc
video_waveform_plot.cc
- wx_util.cc
+ window_metrics.cc
wx_signal_manager.cc
+ wx_util.cc
+ wx_variant.cc
"""
def configure(conf):
diff --git a/src/wx/wx_util.cc b/src/wx/wx_util.cc
index 28ac6ab5c..1c04a4755 100644
--- a/src/wx/wx_util.cc
+++ b/src/wx/wx_util.cc
@@ -31,20 +31,22 @@
#include "static_text.h"
#include "wx_ptr.h"
#include "wx_util.h"
+#include "wx_variant.h"
#include "lib/config.h"
#include "lib/cross.h"
#include "lib/job.h"
#include "lib/job_manager.h"
#include "lib/util.h"
+#include "lib/variant.h"
#include "lib/version.h"
#include <dcp/locale_convert.h>
#include <dcp/warnings.h>
LIBDCP_DISABLE_WARNINGS
-#include <wx/spinctrl.h>
-#include <wx/splash.h>
-#include <wx/progdlg.h>
#include <wx/filepicker.h>
+#include <wx/progdlg.h>
#include <wx/sizer.h>
+#include <wx/spinctrl.h>
+#include <wx/splash.h>
LIBDCP_ENABLE_WARNINGS
#include <boost/thread.hpp>
@@ -161,7 +163,7 @@ add_label_to_sizer (wxGridBagSizer* s, wxStaticText* t, bool, wxGBPosition pos,
void
error_dialog (wxWindow* parent, wxString m, optional<wxString> e)
{
- auto d = make_wx<wxMessageDialog>(parent, m, _("DCP-o-matic"), wxOK | wxICON_ERROR);
+ auto d = make_wx<wxMessageDialog>(parent, m, variant::wx::dcpomatic(), wxOK | wxICON_ERROR);
if (e) {
wxString em = *e;
em[0] = wxToupper (em[0]);
@@ -178,7 +180,7 @@ error_dialog (wxWindow* parent, wxString m, optional<wxString> e)
void
message_dialog (wxWindow* parent, wxString m)
{
- auto d = make_wx<wxMessageDialog>(parent, m, _("DCP-o-matic"), wxOK | wxICON_INFORMATION);
+ auto d = make_wx<wxMessageDialog>(parent, m, variant::wx::dcpomatic(), wxOK | wxICON_INFORMATION);
d->ShowModal ();
}
@@ -187,7 +189,7 @@ message_dialog (wxWindow* parent, wxString m)
bool
confirm_dialog (wxWindow* parent, wxString m)
{
- auto d = make_wx<wxMessageDialog>(parent, m, _("DCP-o-matic"), wxYES_NO | wxICON_QUESTION);
+ auto d = make_wx<wxMessageDialog>(parent, m, variant::wx::dcpomatic(), wxYES_NO | wxICON_QUESTION);
return d->ShowModal() == wxID_YES;
}
@@ -409,6 +411,65 @@ checked_set(RegionSubtagWidget* widget, optional<dcp::LanguageTag::RegionSubtag>
}
+#ifdef DCPOMATIC_OSX
+
+void
+dcpomatic_setup_i18n()
+{
+ wxLog::EnableLogging();
+
+ auto get_locale_value = [](CFLocaleKey key) {
+ CFLocaleRef cflocale = CFLocaleCopyCurrent();
+ auto value = (CFStringRef) CFLocaleGetValue(cflocale, key);
+ char buffer[64];
+ CFStringGetCString(value, buffer, sizeof(buffer), kCFStringEncodingUTF8);
+ CFRelease(cflocale);
+ return string(buffer);
+ };
+
+ auto translations = new wxTranslations();
+
+ auto config_lang = Config::instance()->language();
+ if (config_lang && !config_lang->empty()) {
+ translations->SetLanguage(std_to_wx(*config_lang));
+ } else {
+ /* We want to use the user's preferred language. It seems that if we use the wxWidgets default we will get the
+ * language for the locale, which may not be what we want (e.g. for a machine in Germany, configured for DE locale,
+ * but with the preferred language set to English).
+ *
+ * Instead, the the language code from macOS then get the corresponding canonical language string with region,
+ * which wxTranslations::SetLanguage will accept.
+ */
+ auto const language_code = get_locale_value(kCFLocaleLanguageCode);
+ /* Ideally this would be wxUILocale (as wxLocale is deprecated) but we want to keep this building
+ * with the old wxWidgets we use for the older macOS builds.
+ */
+ auto const info = wxLocale::FindLanguageInfo(std_to_wx(language_code));
+ if (info) {
+#if wxCHECK_VERSION(3, 1, 6)
+ translations->SetLanguage(info->GetCanonicalWithRegion());
+#else
+ translations->SetLanguage(info->CanonicalName);
+#endif
+ }
+ }
+
+#ifdef DCPOMATIC_DEBUG
+ wxFileTranslationsLoader::AddCatalogLookupPathPrefix(wxT("build/src/wx/mo"));
+ wxFileTranslationsLoader::AddCatalogLookupPathPrefix(wxT("build/src/tools/mo"));
+#endif
+
+ translations->AddStdCatalog();
+ translations->AddCatalog(wxT("libdcpomatic2-wx"));
+ translations->AddCatalog(wxT("dcpomatic2"));
+
+ wxTranslations::Set(translations);
+
+ dcpomatic_setup_gettext_i18n(config_lang.get_value_or(""));
+}
+
+#else
+
void
dcpomatic_setup_i18n ()
{
@@ -458,6 +519,8 @@ dcpomatic_setup_i18n ()
}
}
+#endif
+
int
wx_get (wxSpinCtrl* w)
@@ -623,35 +686,35 @@ display_progress (wxString title, wxString task)
int
get_offsets (vector<Offset>& offsets)
{
- offsets.push_back (Offset(_("UTC-11"), -11, 0));
- offsets.push_back (Offset(_("UTC-10"), -10, 0));
- offsets.push_back (Offset(_("UTC-9"), -9, 0));
- offsets.push_back (Offset(_("UTC-8"), -8, 0));
- offsets.push_back (Offset(_("UTC-7"), -7, 0));
- offsets.push_back (Offset(_("UTC-6"), -6, 0));
- offsets.push_back (Offset(_("UTC-5"), -5, 0));
- offsets.push_back (Offset(_("UTC-4:30"), -4, 30));
- offsets.push_back (Offset(_("UTC-4"), -4, 0));
- offsets.push_back (Offset(_("UTC-3:30"), -3, 30));
- offsets.push_back (Offset(_("UTC-3"), -3, 0));
- offsets.push_back (Offset(_("UTC-2"), -2, 0));
- offsets.push_back (Offset(_("UTC-1"), -1, 0));
+ offsets.push_back({_("UTC-11"), dcp::UTCOffset(-11, 0)});
+ offsets.push_back({_("UTC-10"), dcp::UTCOffset(-10, 0)});
+ offsets.push_back({_("UTC-9"), dcp::UTCOffset( -9, 0)});
+ offsets.push_back({_("UTC-8"), dcp::UTCOffset( -8, 0)});
+ offsets.push_back({_("UTC-7"), dcp::UTCOffset( -7, 0)});
+ offsets.push_back({_("UTC-6"), dcp::UTCOffset( -6, 0)});
+ offsets.push_back({_("UTC-5"), dcp::UTCOffset( -5, 0)});
+ offsets.push_back({_("UTC-4:30"),dcp::UTCOffset( -4, -30)});
+ offsets.push_back({_("UTC-4"), dcp::UTCOffset( -4, 0)});
+ offsets.push_back({_("UTC-3:30"),dcp::UTCOffset( -3, -30)});
+ offsets.push_back({_("UTC-3"), dcp::UTCOffset( -3, 0)});
+ offsets.push_back({_("UTC-2"), dcp::UTCOffset( -2, 0)});
+ offsets.push_back({_("UTC-1"), dcp::UTCOffset( -1, 0)});
int utc = offsets.size();
- offsets.push_back (Offset(_("UTC") , 0, 0));
- offsets.push_back (Offset(_("UTC+1"), 1, 0));
- offsets.push_back (Offset(_("UTC+2"), 2, 0));
- offsets.push_back (Offset(_("UTC+3"), 3, 0));
- offsets.push_back (Offset(_("UTC+4"), 4, 0));
- offsets.push_back (Offset(_("UTC+5"), 5, 0));
- offsets.push_back (Offset(_("UTC+5:30"), 5, 30));
- offsets.push_back (Offset(_("UTC+6"), 6, 0));
- offsets.push_back (Offset(_("UTC+7"), 7, 0));
- offsets.push_back (Offset(_("UTC+8"), 8, 0));
- offsets.push_back (Offset(_("UTC+9"), 9, 0));
- offsets.push_back (Offset(_("UTC+9:30"), 9, 30));
- offsets.push_back (Offset(_("UTC+10"), 10, 0));
- offsets.push_back (Offset(_("UTC+11"), 11, 0));
- offsets.push_back (Offset(_("UTC+12"), 12, 0));
+ offsets.push_back({_("UTC") , dcp::UTCOffset( 0, 0)});
+ offsets.push_back({_("UTC+1"), dcp::UTCOffset( 1, 0)});
+ offsets.push_back({_("UTC+2"), dcp::UTCOffset( 2, 0)});
+ offsets.push_back({_("UTC+3"), dcp::UTCOffset( 3, 0)});
+ offsets.push_back({_("UTC+4"), dcp::UTCOffset( 4, 0)});
+ offsets.push_back({_("UTC+5"), dcp::UTCOffset( 5, 0)});
+ offsets.push_back({_("UTC+5:30"),dcp::UTCOffset( 5, 30)});
+ offsets.push_back({_("UTC+6"), dcp::UTCOffset( 6, 0)});
+ offsets.push_back({_("UTC+7"), dcp::UTCOffset( 7, 0)});
+ offsets.push_back({_("UTC+8"), dcp::UTCOffset( 8, 0)});
+ offsets.push_back({_("UTC+9"), dcp::UTCOffset( 9, 0)});
+ offsets.push_back({_("UTC+9:30"),dcp::UTCOffset( 9, 30)});
+ offsets.push_back({_("UTC+10"), dcp::UTCOffset( 10, 0)});
+ offsets.push_back({_("UTC+11"), dcp::UTCOffset( 11, 0)});
+ offsets.push_back({_("UTC+12"), dcp::UTCOffset( 12, 0)});
return utc;
}
@@ -747,20 +810,12 @@ report_config_load_failure(wxWindow* parent, Config::LoadFailure what)
case Config::LoadFailure::CONFIG:
message_dialog(parent, _("The existing configuration failed to load. Default values will be used instead. These may take a short time to create."));
break;
- case Config::LoadFailure::CINEMAS:
- message_dialog(
- parent,
- _(wxString::Format("The cinemas list for creating KDMs (cinemas.xml) failed to load. Please check the numbered backup files in %s",
- std_to_wx(Config::instance()->cinemas_file().parent_path().string())))
- );
- break;
- case Config::LoadFailure::DKDM_RECIPIENTS:
- message_dialog(
- parent,
- _(wxString::Format("The recipients list for creating DKDMs (dkdm_recipients.xml) failed to load. Please check the numbered backup files in %s",
- std_to_wx(Config::instance()->dkdm_recipients_file().parent_path().string())))
- );
- break;
}
}
+
+wxString
+wx::report_problem()
+{
+ return std_to_wx(::report_problem());
+}
diff --git a/src/wx/wx_util.h b/src/wx/wx_util.h
index 66b01640c..db1b3ae0c 100644
--- a/src/wx/wx_util.h
+++ b/src/wx/wx_util.h
@@ -31,6 +31,7 @@
#include "wx_ptr.h"
#include "lib/config.h"
#include "lib/dcpomatic_time.h"
+#include <dcp/utc_offset.h>
#include <dcp/warnings.h>
LIBDCP_DISABLE_WARNINGS
#include <wx/gbsizer.h>
@@ -56,7 +57,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 +75,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
@@ -131,21 +132,24 @@ extern double dpi_scale_factor (wxWindow* window);
extern int search_ctrl_height ();
extern void report_config_load_failure(wxWindow* parent, Config::LoadFailure what);
+
struct Offset
{
- Offset (wxString n, int h, int m)
- : name (n)
- , hour (h)
- , minute (m)
+ Offset(wxString name_, dcp::UTCOffset offset_)
+ : name(name_)
+ , offset(offset_)
{}
wxString name;
- int hour;
- int minute;
+ dcp::UTCOffset offset;
};
extern int get_offsets (std::vector<Offset>& offsets);
+namespace wx {
+ extern wxString report_problem();
+}
+
extern void checked_set (FilePickerCtrl* widget, boost::filesystem::path value);
extern void checked_set (wxDirPickerCtrl* widget, boost::filesystem::path value);
diff --git a/src/wx/wx_variant.cc b/src/wx/wx_variant.cc
new file mode 100644
index 000000000..96b00bba4
--- /dev/null
+++ b/src/wx/wx_variant.cc
@@ -0,0 +1,147 @@
+/*
+ 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 "wx_util.h"
+#include "wx_variant.h"
+#include "lib/variant.h"
+
+
+
+wxString
+variant::wx::dcpomatic()
+{
+ return std_to_wx(variant::dcpomatic());
+}
+
+wxString
+variant::wx::dcpomatic_batch_converter()
+{
+ return std_to_wx(variant::dcpomatic_batch_converter());
+}
+
+wxString
+variant::wx::dcpomatic_combiner()
+{
+ return std_to_wx(variant::dcpomatic_combiner());
+}
+
+wxString
+variant::wx::dcpomatic_disk_writer()
+{
+ return std_to_wx(variant::dcpomatic_disk_writer());
+}
+
+wxString
+variant::wx::dcpomatic_editor()
+{
+ return std_to_wx(variant::dcpomatic_editor());
+}
+
+wxString
+variant::wx::dcpomatic_encode_server()
+{
+ return std_to_wx(variant::dcpomatic_encode_server());
+}
+
+wxString
+variant::wx::dcpomatic_kdm_creator()
+{
+ return std_to_wx(variant::dcpomatic_kdm_creator());
+}
+
+wxString
+variant::wx::dcpomatic_player()
+{
+ return std_to_wx(variant::dcpomatic_player());
+}
+
+wxString
+variant::wx::dcpomatic_playlist_editor()
+{
+ return std_to_wx(variant::dcpomatic_playlist_editor());
+}
+
+wxString
+variant::wx::dcpomatic_verifier()
+{
+ return std_to_wx(variant::dcpomatic_verifier());
+}
+
+wxString
+variant::wx::insert_dcpomatic(wxString const& s)
+{
+ return wxString::Format(s, dcpomatic());
+}
+
+wxString
+variant::wx::insert_dcpomatic_batch_converter(wxString const& s)
+{
+ return wxString::Format(s, dcpomatic_batch_converter());
+}
+
+wxString
+variant::wx::insert_dcpomatic_disk_writer(wxString const& s)
+{
+ return wxString::Format(s, dcpomatic_disk_writer());
+}
+
+wxString
+variant::wx::insert_dcpomatic_editor(wxString const& s)
+{
+ return wxString::Format(s, dcpomatic_editor());
+}
+
+wxString
+variant::wx::insert_dcpomatic_encode_server(wxString const& s)
+{
+ return wxString::Format(s, dcpomatic_encode_server());
+}
+
+wxString
+variant::wx::insert_dcpomatic_kdm_creator(wxString const& s)
+{
+ return wxString::Format(s, dcpomatic_kdm_creator());
+}
+
+wxString
+variant::wx::insert_dcpomatic_player(wxString const& s)
+{
+ return wxString::Format(s, dcpomatic_player());
+}
+
+wxString
+variant::wx::insert_dcpomatic_playlist_editor(wxString const& s)
+{
+ return wxString::Format(s, dcpomatic_playlist_editor());
+}
+
+wxString
+variant::wx::insert_dcpomatic_verifier(wxString const& s)
+{
+ return wxString::Format(s, dcpomatic_verifier());
+}
+
+wxString
+variant::wx::report_problem_email()
+{
+ return std_to_wx(variant::report_problem_email());
+}
+
diff --git a/src/wx/wx_variant.h b/src/wx/wx_variant.h
new file mode 100644
index 000000000..bf2bcaee0
--- /dev/null
+++ b/src/wx/wx_variant.h
@@ -0,0 +1,54 @@
+/*
+ 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 <wx/wx.h>
+
+
+namespace variant {
+namespace wx {
+
+
+wxString dcpomatic();
+wxString dcpomatic_batch_converter();
+wxString dcpomatic_combiner();
+wxString dcpomatic_disk_writer();
+wxString dcpomatic_editor();
+wxString dcpomatic_encode_server();
+wxString dcpomatic_kdm_creator();
+wxString dcpomatic_player();
+wxString dcpomatic_playlist_editor();
+wxString dcpomatic_verifier();
+
+wxString insert_dcpomatic(wxString const& s);
+wxString insert_dcpomatic_batch_converter(wxString const& s);
+wxString insert_dcpomatic_disk_writer(wxString const& s);
+wxString insert_dcpomatic_editor(wxString const& s);
+wxString insert_dcpomatic_encode_server(wxString const& s);
+wxString insert_dcpomatic_kdm_creator(wxString const& s);
+wxString insert_dcpomatic_player(wxString const& s);
+wxString insert_dcpomatic_playlist_editor(wxString const& s);
+wxString insert_dcpomatic_verifier(wxString const& s);
+
+wxString report_problem_email();
+
+
+}
+}