summaryrefslogtreecommitdiff
path: root/test/lib
diff options
context:
space:
mode:
authorCarl Hetherington <cth@carlh.net>2025-05-12 17:05:44 +0200
committerCarl Hetherington <cth@carlh.net>2025-05-21 23:50:03 +0200
commit6cab56ab466e821d336998cdb6769c864214e1aa (patch)
treee28b21ed644f40545eb8de78cabf8ee63faee0fd /test/lib
parentd84cfe7de28070ea31c9a1c0bd7872ac4be4b773 (diff)
Move tests that only need src/lib into test/lib.
Diffstat (limited to 'test/lib')
-rw-r--r--test/lib/2536_regression_test.cc76
-rw-r--r--test/lib/2986_regression_test.cc65
-rw-r--r--test/lib/4k_test.cc68
-rw-r--r--test/lib/analytics_test.cc77
-rw-r--r--test/lib/atmos_test.cc148
-rw-r--r--test/lib/audio_analysis_test.cc314
-rw-r--r--test/lib/audio_buffers_test.cc387
-rw-r--r--test/lib/audio_content_test.cc289
-rw-r--r--test/lib/audio_delay_test.cc108
-rw-r--r--test/lib/audio_filter_test.cc112
-rw-r--r--test/lib/audio_mapping_test.cc150
-rw-r--r--test/lib/audio_merger_test.cc192
-rw-r--r--test/lib/audio_processor_delay_test.cc137
-rw-r--r--test/lib/audio_processor_test.cc59
-rw-r--r--test/lib/audio_ring_buffers_test.cc194
-rw-r--r--test/lib/burnt_subtitle_test.cc205
-rw-r--r--test/lib/butler_test.cc132
-rw-r--r--test/lib/bv20_test.cc155
-rw-r--r--test/lib/cinema_list_test.cc276
-rw-r--r--test/lib/cinema_sound_processor_test.cc88
-rw-r--r--test/lib/client_server_test.cc343
-rw-r--r--test/lib/closed_caption_test.cc123
-rw-r--r--test/lib/collator_test.cc59
-rw-r--r--test/lib/colour_conversion_test.cc122
-rw-r--r--test/lib/config_test.cc568
-rw-r--r--test/lib/content_test.cc182
-rw-r--r--test/lib/copy_dcp_details_to_film_test.cc52
-rw-r--r--test/lib/cpl_hash_test.cc96
-rw-r--r--test/lib/cpl_metadata_test.cc121
-rw-r--r--test/lib/create_cli_test.cc386
-rw-r--r--test/lib/dcp_decoder_test.cc133
-rw-r--r--test/lib/dcp_digest_file_test.cc101
-rw-r--r--test/lib/dcp_examiner_test.cc52
-rw-r--r--test/lib/dcp_metadata_test.cc71
-rw-r--r--test/lib/dcp_playback_test.cc69
-rw-r--r--test/lib/dcp_subtitle_test.cc354
-rw-r--r--test/lib/dcpomatic_time_test.cc352
-rw-r--r--test/lib/digest_test.cc99
-rw-r--r--test/lib/disk_writer_test.cc231
-rw-r--r--test/lib/dkdm_recipient_list_test.cc57
-rw-r--r--test/lib/email_test.cc43
-rw-r--r--test/lib/empty_caption_test.cc46
-rw-r--r--test/lib/empty_test.cc183
-rw-r--r--test/lib/encode_cli_test.cc163
-rw-r--r--test/lib/encryption_test.cc69
-rw-r--r--test/lib/ffmpeg_audio_only_test.cc201
-rw-r--r--test/lib/ffmpeg_audio_test.cc146
-rw-r--r--test/lib/ffmpeg_dcp_test.cc72
-rw-r--r--test/lib/ffmpeg_decoder_error_test.cc60
-rw-r--r--test/lib/ffmpeg_decoder_seek_test.cc135
-rw-r--r--test/lib/ffmpeg_decoder_sequential_test.cc94
-rw-r--r--test/lib/ffmpeg_encoder_test.cc541
-rw-r--r--test/lib/ffmpeg_examiner_test.cc95
-rw-r--r--test/lib/ffmpeg_properties_test.cc65
-rw-r--r--test/lib/ffmpeg_pts_offset_test.cc92
-rw-r--r--test/lib/ffmpeg_subtitles_test.cc60
-rw-r--r--test/lib/file_extension_test.cc81
-rw-r--r--test/lib/file_group_test.cc131
-rw-r--r--test/lib/file_log_test.cc37
-rw-r--r--test/lib/file_naming_test.cc200
-rw-r--r--test/lib/filename_charset_test.cc42
-rw-r--r--test/lib/film_metadata_test.cc238
-rw-r--r--test/lib/film_test.cc88
-rw-r--r--test/lib/find_missing_test.cc222
-rw-r--r--test/lib/font_comparator_test.cc27
-rw-r--r--test/lib/font_id_allocator_test.cc100
-rw-r--r--test/lib/frame_interval_checker_test.cc140
-rw-r--r--test/lib/frame_rate_test.cc296
-rw-r--r--test/lib/grok_util_test.cc43
-rw-r--r--test/lib/guess_crop_test.cc70
-rw-r--r--test/lib/hints_test.cc311
-rw-r--r--test/lib/image_content_fade_test.cc49
-rw-r--r--test/lib/image_filename_sorter_test.cc82
-rw-r--r--test/lib/image_proxy_test.cc67
-rw-r--r--test/lib/image_test.cc753
-rw-r--r--test/lib/import_dcp_test.cc179
-rw-r--r--test/lib/interrupt_encoder_test.cc56
-rw-r--r--test/lib/isdcf_name_test.cc293
-rw-r--r--test/lib/j2k_encode_threading_test.cc148
-rw-r--r--test/lib/j2k_encoder_test.cc89
-rw-r--r--test/lib/j2k_video_bit_rate_test.cc89
-rw-r--r--test/lib/job_manager_test.cc163
-rw-r--r--test/lib/kdm_cli_test.cc362
-rw-r--r--test/lib/kdm_naming_test.cc259
-rw-r--r--test/lib/kdm_util_test.cc95
-rw-r--r--test/lib/low_bitrate_test.cc64
-rw-r--r--test/lib/map_cli_test.cc613
-rw-r--r--test/lib/markers_test.cc154
-rw-r--r--test/lib/mca_subdescriptors_test.cc174
-rw-r--r--test/lib/mpeg2_dcp_test.cc116
-rw-r--r--test/lib/no_use_video_test.cc159
-rw-r--r--test/lib/open_caption_test.cc46
-rw-r--r--test/lib/optimise_stills_test.cc97
-rw-r--r--test/lib/overlap_video_test.cc123
-rw-r--r--test/lib/pixel_formats_test.cc100
-rw-r--r--test/lib/player_test.cc759
-rw-r--r--test/lib/playlist_test.cc98
-rw-r--r--test/lib/pulldown_detect_test.cc37
-rw-r--r--test/lib/ratio_test.cc75
-rw-r--r--test/lib/recover_test.cc179
-rw-r--r--test/lib/rect_test.cc53
-rw-r--r--test/lib/reel_writer_test.cc146
-rw-r--r--test/lib/reels_test.cc680
-rw-r--r--test/lib/relative_paths_test.cc44
-rw-r--r--test/lib/release_notes_test.cc38
-rw-r--r--test/lib/remake_id_test.cc101
-rw-r--r--test/lib/remake_video_test.cc80
-rw-r--r--test/lib/remake_with_subtitle_test.cc57
-rw-r--r--test/lib/render_subtitles_test.cc210
-rw-r--r--test/lib/repeat_frame_test.cc57
-rw-r--r--test/lib/required_disk_space_test.cc83
-rw-r--r--test/lib/resampler_test.cc62
-rw-r--r--test/lib/scaling_test.cc112
-rw-r--r--test/lib/scoped_temporary_test.cc59
-rw-r--r--test/lib/shuffler_test.cc198
-rw-r--r--test/lib/silence_padding_test.cc145
-rw-r--r--test/lib/skip_frame_test.cc55
-rw-r--r--test/lib/smtp_server.cc79
-rw-r--r--test/lib/smtp_server.h23
-rw-r--r--test/lib/socket_test.cc182
-rw-r--r--test/lib/srt_subtitle_test.cc298
-rw-r--r--test/lib/ssa_subtitle_test.cc78
-rw-r--r--test/lib/stream_test.cc95
-rw-r--r--test/lib/subtitle_charset_test.cc51
-rw-r--r--test/lib/subtitle_font_id_change_test.cc164
-rw-r--r--test/lib/subtitle_font_id_test.cc349
-rw-r--r--test/lib/subtitle_language_test.cc121
-rw-r--r--test/lib/subtitle_metadata_test.cc55
-rw-r--r--test/lib/subtitle_position_test.cc186
-rw-r--r--test/lib/subtitle_reel_number_test.cc75
-rw-r--r--test/lib/subtitle_reel_test.cc261
-rw-r--r--test/lib/subtitle_timing_test.cc146
-rw-r--r--test/lib/subtitle_trim_test.cc47
-rw-r--r--test/lib/template_test.cc48
-rw-r--r--test/lib/text_decoder_test.cc32
-rw-r--r--test/lib/text_entry_point_test.cc70
-rw-r--r--test/lib/threed_test.cc355
-rw-r--r--test/lib/time_calculation_test.cc818
-rw-r--r--test/lib/torture_test.cc327
-rw-r--r--test/lib/unzipper_test.cc59
-rw-r--r--test/lib/update_checker_test.cc42
-rw-r--r--test/lib/upmixer_a_test.cc103
-rw-r--r--test/lib/util_test.cc226
-rw-r--r--test/lib/vf_kdm_test.cc107
-rw-r--r--test/lib/vf_test.cc540
-rw-r--r--test/lib/video_content_scale_test.cc146
-rw-r--r--test/lib/video_content_test.cc53
-rw-r--r--test/lib/video_level_test.cc590
-rw-r--r--test/lib/video_mxf_content_test.cc70
-rw-r--r--test/lib/video_trim_test.cc57
-rw-r--r--test/lib/writer_test.cc155
-rw-r--r--test/lib/wscript207
-rw-r--r--test/lib/zipper_test.cc75
153 files changed, 24842 insertions, 0 deletions
diff --git a/test/lib/2536_regression_test.cc b/test/lib/2536_regression_test.cc
new file mode 100644
index 000000000..b5a956208
--- /dev/null
+++ b/test/lib/2536_regression_test.cc
@@ -0,0 +1,76 @@
+/*
+ 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/content_factory.h"
+#include "lib/dcp_content.h"
+#include "lib/film.h"
+#include "lib/image.h"
+#include "lib/player.h"
+#include "lib/text_content.h"
+#include "../test.h"
+#include <boost/test/unit_test.hpp>
+
+
+using std::make_shared;
+
+
+BOOST_AUTO_TEST_CASE(crash_rendering_vf_interop_subs_test)
+{
+ auto prefix = std::string("crash_rendering_vf_interop_subs_test");
+
+ auto video = content_factory("test/data/flat_red.png");
+ auto ov = new_test_film(prefix + "_ov", video);
+ ov->set_interop(true);
+
+ make_and_verify_dcp(
+ ov,
+ {
+ dcp::VerificationNote::Code::INVALID_STANDARD,
+ });
+
+ auto ov_dcp = make_shared<DCPContent>(ov->dir(ov->dcp_name()));
+ auto subtitles = content_factory("test/data/short.srt");
+ auto vf = new_test_film(prefix + "_vf", { ov_dcp, subtitles.front() });
+ vf->set_interop(true);
+ vf->set_reel_type(ReelType::BY_VIDEO_CONTENT);
+ ov_dcp->set_reference_video(true);
+ ov_dcp->set_reference_audio(true);
+ subtitles[0]->text[0]->set_language(dcp::LanguageTag("de"));
+
+ make_and_verify_dcp(
+ vf,
+ {
+ dcp::VerificationNote::Code::INVALID_STANDARD,
+ dcp::VerificationNote::Code::MISSING_SUBTITLE_LANGUAGE,
+ dcp::VerificationNote::Code::INVALID_SUBTITLE_FIRST_TEXT_TIME,
+ dcp::VerificationNote::Code::EXTERNAL_ASSET,
+ });
+
+ auto vf_dcp = make_shared<DCPContent>(vf->dir(vf->dcp_name()));
+ vf_dcp->add_ov(ov->dir(ov->dcp_name()));
+ auto test = new_test_film(prefix + "_test", { vf_dcp });
+ vf_dcp->text[0]->set_use(true);
+
+ auto player = make_shared<Player>(test, Image::Alignment::COMPACT, false);
+ player->set_always_burn_open_subtitles();
+ while (!player->pass()) {}
+}
+
diff --git a/test/lib/2986_regression_test.cc b/test/lib/2986_regression_test.cc
new file mode 100644
index 000000000..0d66b9d94
--- /dev/null
+++ b/test/lib/2986_regression_test.cc
@@ -0,0 +1,65 @@
+/*
+ Copyright (C) 2025 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/content_factory.h"
+#include "lib/film.h"
+#include "lib/find_missing.h"
+#include "lib/font.h"
+#include "lib/player.h"
+#include "lib/text_content.h"
+#include "../test.h"
+#include <boost/filesystem.hpp>
+#include <boost/test/unit_test.hpp>
+
+
+using std::make_shared;
+
+
+BOOST_AUTO_TEST_CASE(font_error_after_find_missing_test)
+{
+ boost::filesystem::path dir("build/test/font_error_after_find_missing_test_assets");
+ boost::filesystem::remove_all(dir);
+ boost::filesystem::create_directories(dir);
+
+ boost::filesystem::copy_file("test/data/15s.srt", dir / "15s.srt");
+ boost::filesystem::copy_file("test/data/Inconsolata-VF.ttf", dir / "Inconsolata-VF.ttf");
+
+ {
+ auto content = content_factory(dir / "15s.srt");
+ auto film = new_test_film("font_error_after_find_missing_test", content);
+ auto fonts = content[0]->text[0]->fonts();
+ fonts.front()->set_file(dir / "Inconsolata-VF.ttf");
+ film->write_metadata();
+ }
+
+ boost::filesystem::remove_all(dir);
+
+ auto film2 = make_shared<Film>(boost::filesystem::path("build/test/font_error_after_find_missing_test"));
+ film2->read_metadata();
+
+ dcpomatic::find_missing(film2->content(), "test/data/15s.srt");
+
+ Player player(film2, Image::Alignment::PADDED, false);
+ player.set_always_burn_open_subtitles();
+ for (int i = 0; i < 48; ++i) {
+ player.pass();
+ }
+}
diff --git a/test/lib/4k_test.cc b/test/lib/4k_test.cc
new file mode 100644
index 000000000..fd37d76da
--- /dev/null
+++ b/test/lib/4k_test.cc
@@ -0,0 +1,68 @@
+/*
+ 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/>.
+
+*/
+
+
+/** @defgroup completedcp Complete builds of DCPs */
+
+/** @file test/4k_test.cc
+ * @brief Run a 4K encode from a simple input.
+ * @ingroup completedcp
+ *
+ * The output is checked against test/data/4k_test.
+ */
+
+
+#include "lib/dcp_content_type.h"
+#include "lib/dcpomatic_log.h"
+#include "lib/ffmpeg_content.h"
+#include "lib/film.h"
+#include "lib/ratio.h"
+#include "lib/video_content.h"
+#include "../test.h"
+#include <boost/test/unit_test.hpp>
+
+
+using std::make_shared;
+
+
+BOOST_AUTO_TEST_CASE (fourk_test)
+{
+ auto c = make_shared<FFmpegContent>("test/data/test.mp4");
+ auto film = new_test_film("4k_test", { c });
+ LogSwitcher ls (film->log());
+ film->set_resolution (Resolution::FOUR_K);
+ film->set_dcp_content_type(DCPContentType::from_isdcf_name("FTR"));
+
+ make_and_verify_dcp (
+ film,
+ {
+ dcp::VerificationNote::Code::MISSING_FFMC_IN_FEATURE,
+ dcp::VerificationNote::Code::MISSING_FFEC_IN_FEATURE
+ });
+
+ boost::filesystem::path p (test_film_dir("4k_test"));
+ p /= film->dcp_name ();
+
+ /* This test is concerned with the image and its metadata, so we'll
+ * ignore any differences in sound between the DCP and the reference to
+ * avoid test failures for unrelated reasons.
+ */
+ check_dcp("test/data/4k_test", p.string(), true);
+}
diff --git a/test/lib/analytics_test.cc b/test/lib/analytics_test.cc
new file mode 100644
index 000000000..4a3bdf066
--- /dev/null
+++ b/test/lib/analytics_test.cc
@@ -0,0 +1,77 @@
+/*
+ Copyright (C) 2025 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/analytics.h"
+#include "../test.h"
+#include <dcp/filesystem.h>
+#include <boost/test/unit_test.hpp>
+
+
+using std::string;
+
+
+BOOST_AUTO_TEST_CASE(many_successful_encodes_test)
+{
+ boost::filesystem::path const path = "build/test/many_successful_encodes_test";
+ dcp::filesystem::remove_all(path);
+ ConfigRestorer cr(path);
+
+ Analytics analytics;
+
+ string last_title;
+ string last_body;
+ analytics.Message.connect([&](string title, string body) {
+ last_title = title;
+ last_body = body;
+ });
+
+ for (int i = 0; i < 19; ++i) {
+ analytics.successful_dcp_encode();
+ BOOST_CHECK(last_title.empty());
+ BOOST_CHECK(last_body.empty());
+ }
+
+ analytics.successful_dcp_encode();
+ BOOST_CHECK_EQUAL(last_title, "Congratulations!");
+ BOOST_CHECK_EQUAL(last_body,
+ "<h2>You have made 20 DCPs with DCP-o-matic!</h2>"
+ "<img width=\"20%%\" src=\"memory:me.jpg\" align=\"center\">"
+ "<font size=\"+1\">"
+ "<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 "
+ "of a 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 "
+ "project. Financial support will help me to spend more "
+ "time developing DCP-o-matic and making it better!"
+
+ "<p><ul>"
+ "<li><a href=\"https://dcpomatic.com/donate_amount?amount=40\">Go to Paypal to donate €40</a>"
+ "<li><a href=\"https://dcpomatic.com/donate_amount?amount=20\">Go to Paypal to donate €20</a>"
+ "<li><a href=\"https://dcpomatic.com/donate_amount?amount=10\">Go to Paypal to donate €10</a>"
+ "</ul>"
+
+ "<p>Thank you!"
+ "</font>"
+ );
+}
+
diff --git a/test/lib/atmos_test.cc b/test/lib/atmos_test.cc
new file mode 100644
index 000000000..5bd57ee7d
--- /dev/null
+++ b/test/lib/atmos_test.cc
@@ -0,0 +1,148 @@
+/*
+ Copyright (C) 2020-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 "lib/config.h"
+#include "lib/content.h"
+#include "lib/content_factory.h"
+#include "lib/dcp_content.h"
+#include "lib/film.h"
+#include "../test.h"
+#include <dcp/atmos_asset.h>
+#include <dcp/cpl.h>
+#include <dcp/dcp.h>
+#include <dcp/reel.h>
+#include <dcp/reel_atmos_asset.h>
+#include <boost/test/unit_test.hpp>
+
+
+using std::make_shared;
+using std::shared_ptr;
+using std::shared_ptr;
+using std::string;
+using std::vector;
+using boost::optional;
+
+
+BOOST_AUTO_TEST_CASE(atmos_passthrough_test)
+{
+ Cleanup cl;
+
+ auto film = new_test_film(
+ "atmos_passthrough_test",
+ content_factory(TestPaths::private_data() / "atmos_asset.mxf"),
+ &cl
+ );
+
+ make_and_verify_dcp(film, {dcp::VerificationNote::Code::MISSING_CPL_METADATA});
+
+ auto ref = TestPaths::private_data() / "atmos_asset.mxf";
+ BOOST_REQUIRE(mxf_atmos_files_same(ref, dcp_file(film, "atmos"), true));
+
+ cl.run();
+}
+
+
+BOOST_AUTO_TEST_CASE(atmos_encrypted_passthrough_test)
+{
+ Cleanup cl;
+
+ auto ref = TestPaths::private_data() / "atmos_asset.mxf";
+ auto content = content_factory(TestPaths::private_data() / "atmos_asset.mxf");
+ auto film = new_test_film("atmos_encrypted_passthrough_test", content, &cl);
+
+ film->set_encrypted(true);
+ film->_key = dcp::Key("4fac12927eb122af1c2781aa91f3a4cc");
+ make_and_verify_dcp(film, { dcp::VerificationNote::Code::MISSING_CPL_METADATA });
+
+ BOOST_REQUIRE(!mxf_atmos_files_same(ref, dcp_file(film, "atmos")));
+
+ auto signer = Config::instance()->signer_chain();
+ BOOST_REQUIRE(signer->valid());
+
+ auto const decrypted_kdm = film->make_kdm(dcp_file(film, "cpl"), dcp::LocalTime(), dcp::LocalTime());
+ auto const kdm = decrypted_kdm.encrypt(signer, Config::instance()->decryption_chain()->leaf(), {}, dcp::Formulation::MODIFIED_TRANSITIONAL_1, false, {});
+
+ auto content2 = make_shared<DCPContent>(film->dir(film->dcp_name()));
+ content2->add_kdm(kdm);
+ auto film2 = new_test_film("atmos_encrypted_passthrough_test2", {content2}, &cl);
+ make_and_verify_dcp(film2, { dcp::VerificationNote::Code::MISSING_CPL_METADATA });
+
+ BOOST_CHECK(mxf_atmos_files_same(ref, dcp_file(film2, "atmos"), true));
+
+ cl.run();
+}
+
+
+BOOST_AUTO_TEST_CASE(atmos_trim_test)
+{
+ Cleanup cl;
+
+ auto ref = TestPaths::private_data() / "atmos_asset.mxf";
+ auto content = content_factory(TestPaths::private_data() / "atmos_asset.mxf");
+ auto film = new_test_film("atmos_trim_test", content, &cl);
+
+ content[0]->set_trim_start(film, dcpomatic::ContentTime::from_seconds(1));
+
+ /* Just check that the encode runs; I'm not sure how to test the MXF */
+ make_and_verify_dcp(film, { dcp::VerificationNote::Code::MISSING_CPL_METADATA });
+
+ cl.run();
+}
+
+
+BOOST_AUTO_TEST_CASE(atmos_replace_test)
+{
+ auto const frames = 240;
+
+ auto check = [](shared_ptr<const Film> film, int data) {
+ dcp::DCP dcp(film->dir(film->dcp_name()));
+ dcp.read();
+ BOOST_REQUIRE_EQUAL(dcp.cpls().size(), 1U);
+ BOOST_REQUIRE_EQUAL(dcp.cpls()[0]->reels().size(), 1U);
+ BOOST_REQUIRE(dcp.cpls()[0]->reels()[0]->atmos());
+ auto asset = dcp.cpls()[0]->reels()[0]->atmos()->asset();
+ BOOST_REQUIRE(asset);
+ auto reader = asset->start_read();
+ for (int i = 0; i < frames; ++i) {
+ auto frame = reader->get_frame(i);
+ BOOST_REQUIRE(frame);
+ for (int j = 0; j < frame->size(); ++j) {
+ BOOST_REQUIRE_EQUAL(frame->data()[j], data);
+ }
+ }
+ };
+
+ auto atmos_0 = content_factory("test/data/atmos_0.mxf");
+ auto ov = new_test_film("atmos_merge_test_ov", atmos_0);
+ make_and_verify_dcp(ov, { dcp::VerificationNote::Code::MISSING_CPL_METADATA });
+ // atmos_0.mxf should contain all zeros for its data
+ check(ov, 0);
+
+ auto atmos_1 = content_factory("test/data/atmos_1.mxf");
+ auto ov_content = std::make_shared<DCPContent>(boost::filesystem::path("build/test/atmos_merge_test_ov") / ov->dcp_name());
+ auto vf = new_test_film("atmos_merge_test_vf", { ov_content, atmos_1.front() });
+ ov_content->set_reference_video(true);
+ atmos_1.front()->set_position(vf, dcpomatic::DCPTime());
+ make_and_verify_dcp(vf, { dcp::VerificationNote::Code::MISSING_CPL_METADATA, dcp::VerificationNote::Code::EXTERNAL_ASSET }, false);
+ // atmos_1.mxf should contain all ones for its data, and it should have replaced atmos_0 in this DCP
+ check(vf, 1);
+}
+
diff --git a/test/lib/audio_analysis_test.cc b/test/lib/audio_analysis_test.cc
new file mode 100644
index 000000000..201f3464e
--- /dev/null
+++ b/test/lib/audio_analysis_test.cc
@@ -0,0 +1,314 @@
+/*
+ Copyright (C) 2012-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/>.
+
+*/
+
+
+/** @defgroup selfcontained Self-contained tests of single classes / method sets */
+
+/** @file test/audio_analysis_test.cc
+ * @brief Test AudioAnalysis class.
+ * @ingroup selfcontained
+ */
+
+
+#include "lib/analyse_audio_job.h"
+#include "lib/audio_analysis.h"
+#include "lib/audio_content.h"
+#include "lib/content_factory.h"
+#include "lib/dcp_content.h"
+#include "lib/dcp_content_type.h"
+#include "lib/ffmpeg_content.h"
+#include "lib/ffmpeg_content.h"
+#include "lib/film.h"
+#include "lib/job_manager.h"
+#include "lib/playlist.h"
+#include "lib/ratio.h"
+#include "../test.h"
+#include <boost/test/unit_test.hpp>
+#include <numeric>
+
+
+using std::make_shared;
+using std::vector;
+using namespace dcpomatic;
+
+
+BOOST_AUTO_TEST_CASE (audio_analysis_serialisation_test)
+{
+ int const channels = 3;
+ int const points = 4096;
+
+ auto random_float = []() {
+ return (float (rand ()) / RAND_MAX) * 2 - 1;
+ };
+
+ AudioAnalysis a (3);
+ for (int i = 0; i < channels; ++i) {
+ for (int j = 0; j < points; ++j) {
+ AudioPoint p;
+ p[AudioPoint::PEAK] = random_float ();
+ p[AudioPoint::RMS] = random_float ();
+ a.add_point (i, p);
+ }
+ }
+
+ vector<AudioAnalysis::PeakTime> peak;
+ for (int i = 0; i < channels; ++i) {
+ peak.push_back (AudioAnalysis::PeakTime(random_float(), DCPTime(rand())));
+ }
+ a.set_sample_peak (peak);
+
+ a.set_samples_per_point (100);
+ a.set_sample_rate (48000);
+ a.write ("build/test/audio_analysis_serialisation_test");
+
+ AudioAnalysis b ("build/test/audio_analysis_serialisation_test");
+ for (int i = 0; i < channels; ++i) {
+ BOOST_CHECK_EQUAL (b.points(i), points);
+ for (int j = 0; j < points; ++j) {
+ AudioPoint p = a.get_point (i, j);
+ AudioPoint q = b.get_point (i, j);
+ BOOST_CHECK_CLOSE (p[AudioPoint::PEAK], q[AudioPoint::PEAK], 1);
+ BOOST_CHECK_CLOSE (p[AudioPoint::RMS], q[AudioPoint::RMS], 1);
+ }
+ }
+
+ BOOST_REQUIRE_EQUAL (b.sample_peak().size(), 3U);
+ for (int i = 0; i < channels; ++i) {
+ BOOST_CHECK_CLOSE (b.sample_peak()[i].peak, peak[i].peak, 1);
+ BOOST_CHECK_EQUAL (b.sample_peak()[i].time.get(), peak[i].time.get());
+ }
+
+ BOOST_CHECK_EQUAL (a.samples_per_point(), 100);
+ BOOST_CHECK_EQUAL (a.sample_rate(), 48000);
+}
+
+
+BOOST_AUTO_TEST_CASE (audio_analysis_test)
+{
+ auto c = make_shared<FFmpegContent>(TestPaths::private_data() / "betty_L.wav");
+ auto film = new_test_film("audio_analysis_test", { c });
+
+ auto job = make_shared<AnalyseAudioJob>(film, film->playlist(), false);
+ JobManager::instance()->add (job);
+ BOOST_REQUIRE (!wait_for_jobs());
+}
+
+
+/** Check that audio analysis works (i.e. runs without error) with a -ve delay */
+BOOST_AUTO_TEST_CASE (audio_analysis_negative_delay_test)
+{
+ auto c = make_shared<FFmpegContent>(TestPaths::private_data() / "boon_telly.mkv");
+ auto film = new_test_film("audio_analysis_negative_delay_test", { c });
+ c->audio->set_delay (-250);
+
+ auto job = make_shared<AnalyseAudioJob>(film, film->playlist(), false);
+ JobManager::instance()->add (job);
+ BOOST_REQUIRE (!wait_for_jobs());
+}
+
+
+/** Check audio analysis that is incorrect in 2e98263 */
+BOOST_AUTO_TEST_CASE (audio_analysis_test2)
+{
+ auto c = make_shared<FFmpegContent>(TestPaths::private_data() / "3d_thx_broadway_2010_lossless.m2ts");
+ auto film = new_test_film("audio_analysis_test2", { c });
+
+ auto job = make_shared<AnalyseAudioJob>(film, film->playlist(), false);
+ JobManager::instance()->add (job);
+ BOOST_REQUIRE (!wait_for_jobs());
+}
+
+
+/* Test a case which was reported to throw an exception; analysing
+ * a 12-channel DCP's audio.
+ */
+BOOST_AUTO_TEST_CASE (audio_analysis_test3)
+{
+ auto content = make_shared<FFmpegContent>("test/data/white.wav");
+ auto film = new_test_film("analyse_audio_test", { content });
+ film->set_audio_channels (12);
+
+ boost::signals2::connection connection;
+ bool done = false;
+ JobManager::instance()->analyse_audio(film, film->playlist(), false, connection, [&done](Job::Result) { done = true; });
+ BOOST_REQUIRE (!wait_for_jobs());
+ BOOST_CHECK (done);
+}
+
+
+/** Run an audio analysis that triggered an exception in the audio decoder at one point */
+BOOST_AUTO_TEST_CASE (analyse_audio_test4)
+{
+ auto content = content_factory(TestPaths::private_data() / "20 The Wedding Convoy Song.m4a")[0];
+ auto film = new_test_film("analyse_audio_test", { content });
+
+ auto playlist = make_shared<Playlist>();
+ playlist->add (film, content);
+ boost::signals2::connection c;
+ JobManager::instance()->analyse_audio(film, playlist, false, c, [](Job::Result) {});
+ BOOST_CHECK (!wait_for_jobs ());
+}
+
+
+BOOST_AUTO_TEST_CASE (analyse_audio_leqm_test)
+{
+ auto film = new_test_film("analyse_audio_leqm_test");
+ film->set_audio_channels (2);
+ auto content = content_factory(TestPaths::private_data() / "betty_stereo_48k.wav")[0];
+ film->examine_and_add_content (content);
+ BOOST_REQUIRE (!wait_for_jobs());
+
+ auto playlist = make_shared<Playlist>();
+ playlist->add (film, content);
+ boost::signals2::connection c;
+ JobManager::instance()->analyse_audio(film, playlist, false, c, [](Job::Result) {});
+ BOOST_CHECK (!wait_for_jobs());
+
+ AudioAnalysis analysis(film->audio_analysis_path(playlist));
+
+ /* The CLI tool of leqm_nrt gives this value for betty_stereo_48k.wav */
+ BOOST_CHECK_CLOSE (analysis.leqm().get_value_or(0), 88.276, 0.001);
+}
+
+
+BOOST_AUTO_TEST_CASE(analyse_audio_leqm_same_with_empty_channels)
+{
+ auto dcp = make_shared<DCPContent>(TestPaths::private_data() / "JourneyToJah_TLR-1_F_EN-DE-FR_CH_51_2K_LOK_20140225_DGL_SMPTE_OV");
+ auto film = new_test_film("analyse_audio_leqm_test2", { dcp });
+ film->set_audio_channels(8);
+
+ auto analyse = [film, dcp](int channels) {
+ film->set_audio_channels(channels);
+ auto playlist = make_shared<Playlist>();
+ playlist->add(film, dcp);
+ boost::signals2::connection c;
+ JobManager::instance()->analyse_audio(film, playlist, false, c, [](Job::Result) {});
+ BOOST_CHECK(!wait_for_jobs());
+ AudioAnalysis analysis(film->audio_analysis_path(playlist));
+ return analysis.leqm().get_value_or(0);
+ };
+
+ BOOST_CHECK_CLOSE(analyse( 6), 84.51411, 0.001);
+ BOOST_CHECK_CLOSE(analyse( 8), 84.51411, 0.001);
+ BOOST_CHECK_CLOSE(analyse(16), 84.51411, 0.001);
+}
+
+
+/** Bug #2364; a file with a lot of silent video at the end (about 50s worth)
+ * crashed the audio analysis with an OOM on Windows.
+ */
+BOOST_AUTO_TEST_CASE(analyse_audio_with_long_silent_end)
+{
+ auto content = content_factory(TestPaths::private_data() / "2364.mkv")[0];
+ auto film = new_test_film("analyse_audio_with_long_silent_end", { content });
+
+ auto playlist = make_shared<Playlist>();
+ playlist->add(film, content);
+ boost::signals2::connection c;
+ JobManager::instance()->analyse_audio(film, playlist, false, c, [](Job::Result) {});
+ BOOST_CHECK(!wait_for_jobs());
+}
+
+
+BOOST_AUTO_TEST_CASE(analyse_audio_with_strange_channel_count)
+{
+ auto content = content_factory(TestPaths::private_data() / "mali.mkv")[0];
+ auto film = new_test_film("analyse_audio_with_strange_channel_count", { content });
+
+ auto playlist = make_shared<Playlist>();
+ playlist->add(film, content);
+ boost::signals2::connection c;
+ JobManager::instance()->analyse_audio(film, playlist, false, c, [](Job::Result) {});
+ BOOST_CHECK(!wait_for_jobs());
+}
+
+
+BOOST_AUTO_TEST_CASE(analyse_audio_with_more_channels_than_film)
+{
+ auto picture = content_factory("test/data/flat_red.png");
+ auto film_16ch = new_test_film("analyse_audio_with_more_channels_than_film_16ch", picture);
+ film_16ch->set_audio_channels(16);
+ make_and_verify_dcp(film_16ch);
+
+ auto pcm_16ch = find_file(film_16ch->dir(film_16ch->dcp_name()), "pcm_");
+ auto sound = content_factory(pcm_16ch)[0];
+
+ auto film_6ch = new_test_film("analyse_audio_with_more_channels_than_film_6ch", { sound });
+
+ auto playlist = make_shared<Playlist>();
+ playlist->add(film_6ch, sound);
+ boost::signals2::connection c;
+ JobManager::instance()->analyse_audio(film_6ch, playlist, false, c, [](Job::Result) {});
+ BOOST_CHECK(!wait_for_jobs());
+}
+
+
+BOOST_AUTO_TEST_CASE(analyse_audio_uses_processor_when_analysing_whole_film)
+{
+ auto sound = content_factory(TestPaths::private_data() / "betty_stereo.wav")[0];
+ auto film = new_test_film("analyse_audio_uses_processor_when_analysing_whole_film", { sound });
+
+ auto job = make_shared<AnalyseAudioJob>(film, film->playlist(), true);
+ JobManager::instance()->add(job);
+ BOOST_REQUIRE(!wait_for_jobs());
+
+ AudioAnalysis analysis(job->path());
+
+ BOOST_REQUIRE(analysis.channels() > 2);
+ bool centre_non_zero = false;
+ /* Make sure there's something from the mid-side decoder on the centre channel */
+ for (auto point = 0; point < analysis.points(2); ++point) {
+ if (std::abs(analysis.get_point(2, point)[AudioPoint::Type::PEAK]) > 0) {
+ centre_non_zero = true;
+ }
+ }
+
+ BOOST_CHECK(centre_non_zero);
+}
+
+
+BOOST_AUTO_TEST_CASE(ebur128_test)
+{
+ auto dcp = make_shared<DCPContent>(TestPaths::private_data() / "JourneyToJah_TLR-1_F_EN-DE-FR_CH_51_2K_LOK_20140225_DGL_SMPTE_OV");
+ auto film = new_test_film("ebur128_test", { dcp });
+ film->set_audio_channels(8);
+
+ auto analyse = [film, dcp](int channels) {
+ film->set_audio_channels(channels);
+ auto playlist = make_shared<Playlist>();
+ playlist->add(film, dcp);
+ boost::signals2::connection c;
+ JobManager::instance()->analyse_audio(film, playlist, false, c, [](Job::Result) {});
+ BOOST_CHECK(!wait_for_jobs());
+ return AudioAnalysis(film->audio_analysis_path(playlist));
+ };
+
+ auto six = analyse(6);
+ BOOST_CHECK_CLOSE(six.true_peak()[0], 0.520668, 1);
+ BOOST_CHECK_CLOSE(six.true_peak()[1], 0.519579, 1);
+ BOOST_CHECK_CLOSE(six.true_peak()[2], 0.533980, 1);
+ BOOST_CHECK_CLOSE(six.true_peak()[3], 0.326270, 1);
+ BOOST_CHECK_CLOSE(six.true_peak()[4], 0.363581, 1);
+ BOOST_CHECK_CLOSE(six.true_peak()[5], 0.317751, 1);
+ BOOST_CHECK_CLOSE(six.overall_true_peak().get(), 0.53398, 1);
+ BOOST_CHECK_CLOSE(six.overall_true_peak().get(), 0.53398, 1);
+ BOOST_CHECK_CLOSE(six.integrated_loudness().get(), -18.1432, 1);
+ BOOST_CHECK_CLOSE(six.loudness_range().get(), 6.92, 1);
+}
diff --git a/test/lib/audio_buffers_test.cc b/test/lib/audio_buffers_test.cc
new file mode 100644
index 000000000..49df09646
--- /dev/null
+++ b/test/lib/audio_buffers_test.cc
@@ -0,0 +1,387 @@
+/*
+ Copyright (C) 2014 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 test/audio_buffers_test.cc
+ * @brief Test AudioBuffers class.
+ * @ingroup selfcontained
+ */
+
+#include <cmath>
+#include <boost/test/unit_test.hpp>
+#include "lib/audio_buffers.h"
+
+using std::pow;
+
+static float tolerance = 1e-3;
+
+static float
+random_float ()
+{
+ return float (rand ()) / RAND_MAX;
+}
+
+static void
+random_fill (AudioBuffers& buffers)
+{
+ for (int i = 0; i < buffers.frames(); ++i) {
+ for (int j = 0; j < buffers.channels(); ++j) {
+ buffers.data(j)[i] = random_float ();
+ }
+ }
+}
+
+static void
+random_check (AudioBuffers& buffers, int from, int frames)
+{
+ for (int i = from; i < (from + frames); ++i) {
+ for (int j = 0; j < buffers.channels(); ++j) {
+ BOOST_CHECK_CLOSE (buffers.data(j)[i], random_float (), tolerance);
+ }
+ }
+}
+
+/** Basic setup */
+BOOST_AUTO_TEST_CASE (audio_buffers_setup_test)
+{
+ AudioBuffers buffers (4, 9155);
+
+ BOOST_CHECK (buffers.data ());
+ for (int i = 0; i < 4; ++i) {
+ BOOST_CHECK (buffers.data (i));
+ }
+
+ BOOST_CHECK_EQUAL (buffers.channels(), 4);
+ BOOST_CHECK_EQUAL (buffers.frames(), 9155);
+}
+
+/** Extending some buffers */
+BOOST_AUTO_TEST_CASE (audio_buffers_extend_test)
+{
+ AudioBuffers buffers (3, 150);
+ srand (1);
+ random_fill (buffers);
+
+ /* Extend */
+ buffers.set_frames (299);
+
+ srand (1);
+ random_check (buffers, 0, 150);
+
+ /* New space should be silent */
+ for (int i = 150; i < 299; ++i) {
+ for (int c = 0; c < 3; ++c) {
+ BOOST_CHECK_EQUAL (buffers.data(c)[i], 0);
+ }
+ }
+}
+
+/** make_silent() */
+BOOST_AUTO_TEST_CASE (audio_buffers_make_silent_test)
+{
+ AudioBuffers buffers (9, 9933);
+ srand (2);
+ random_fill (buffers);
+
+ buffers.make_silent ();
+
+ for (int i = 0; i < 9933; ++i) {
+ for (int c = 0; c < 9; ++c) {
+ BOOST_CHECK_EQUAL (buffers.data(c)[i], 0);
+ }
+ }
+}
+
+/** make_silent (int c) */
+BOOST_AUTO_TEST_CASE (audio_buffers_make_silent_channel_test)
+{
+ AudioBuffers buffers (9, 9933);
+ srand (3);
+ random_fill (buffers);
+
+ buffers.make_silent (4);
+
+ srand (3);
+ for (int i = 0; i < 9933; ++i) {
+ for (int c = 0; c < 9; ++c) {
+ if (c == 4) {
+ random_float ();
+ BOOST_CHECK_EQUAL (buffers.data(c)[i], 0);
+ } else {
+ BOOST_CHECK_CLOSE (buffers.data(c)[i], random_float (), tolerance);
+ }
+ }
+ }
+}
+
+/** make_silent (int from, int frames) */
+BOOST_AUTO_TEST_CASE (audio_buffers_make_silent_part_test)
+{
+ AudioBuffers buffers (9, 9933);
+ srand (4);
+ random_fill (buffers);
+
+ buffers.make_silent (145, 833);
+
+ srand (4);
+ for (int i = 0; i < 145; ++i) {
+ for (int c = 0; c < 9; ++c) {
+ BOOST_CHECK_EQUAL (buffers.data(c)[i], random_float ());
+ }
+ }
+
+ for (int i = 145; i < (145 + 833); ++i) {
+ for (int c = 0; c < 9; ++c) {
+ random_float ();
+ BOOST_CHECK_EQUAL (buffers.data(c)[i], 0);
+ }
+ }
+
+ for (int i = (145 + 833); i < 9933; ++i) {
+ for (int c = 0; c < 9; ++c) {
+ BOOST_CHECK_EQUAL (buffers.data(c)[i], random_float ());
+ }
+ }
+}
+
+/* apply_gain */
+BOOST_AUTO_TEST_CASE (audio_buffers_apply_gain)
+{
+ AudioBuffers buffers (2, 417315);
+ srand (9);
+ random_fill (buffers);
+
+ buffers.apply_gain (5.4);
+
+ srand (9);
+ for (int i = 0; i < 417315; ++i) {
+ for (int c = 0; c < 2; ++c) {
+ BOOST_CHECK_CLOSE (buffers.data(c)[i], random_float() * pow (10, 5.4 / 20), tolerance);
+ }
+ }
+}
+
+/* copy_from */
+BOOST_AUTO_TEST_CASE (audio_buffers_copy_from)
+{
+ AudioBuffers a (5, 63711);
+ AudioBuffers b (5, 12345);
+
+ srand (42);
+ random_fill (a);
+
+ srand (99);
+ random_fill (b);
+
+ a.copy_from (&b, 517, 233, 194);
+
+ /* Re-seed a's generator and check the numbers that came from it */
+
+ /* First part; not copied-over */
+ srand (42);
+ random_check (a, 0, 194);
+
+ /* Second part; copied-over (just burn generator a's numbers) */
+ for (int i = 0; i < (517 * 5); ++i) {
+ random_float ();
+ }
+
+ /* Third part; not copied-over */
+ random_check (a, 194 + 517, a.frames() - 194 - 517);
+
+ /* Re-seed b's generator and check the numbers that came from it */
+ srand (99);
+
+ /* First part; burn */
+ for (int i = 0; i < 194 * 5; ++i) {
+ random_float ();
+ }
+
+ /* Second part; copied */
+ random_check (b, 194, 517);
+}
+
+/* move */
+BOOST_AUTO_TEST_CASE (audio_buffers_move)
+{
+ AudioBuffers buffers (7, 65536);
+
+ srand (84);
+ random_fill (buffers);
+
+ int const from = 888;
+ int const to = 666;
+ int const frames = 444;
+
+ buffers.move (frames, from, to);
+
+ /* Re-seed and check the un-moved parts */
+ srand (84);
+
+ random_check (buffers, 0, to);
+
+ /* Burn a few */
+ for (int i = 0; i < (from - to + frames) * 7; ++i) {
+ random_float ();
+ }
+
+ random_check (buffers, from + frames, 65536 - frames - from);
+
+ /* Re-seed and check the moved part */
+ srand (84);
+
+ /* Burn a few */
+ for (int i = 0; i < from * 7; ++i) {
+ random_float ();
+ }
+
+ random_check (buffers, to, frames);
+}
+
+/** accumulate_channel */
+BOOST_AUTO_TEST_CASE (audio_buffers_accumulate_channel)
+{
+ AudioBuffers a (3, 256);
+ srand (38);
+ random_fill (a);
+
+ AudioBuffers b (3, 256);
+ random_fill (b);
+
+ a.accumulate_channel (&b, 2, 1, 1.2);
+
+ srand (38);
+ for (int i = 0; i < 256; ++i) {
+ for (int c = 0; c < 3; ++c) {
+ float const A = random_float ();
+ if (c == 1) {
+ BOOST_CHECK_CLOSE (a.data(c)[i], A + b.data(2)[i] * 1.2, tolerance);
+ } else {
+ BOOST_CHECK_CLOSE (a.data(c)[i], A, tolerance);
+ }
+ }
+ }
+}
+
+/** accumulate_frames */
+BOOST_AUTO_TEST_CASE (audio_buffers_accumulate_frames)
+{
+ AudioBuffers a (3, 256);
+ srand (38);
+ random_fill (a);
+
+ AudioBuffers b (3, 256);
+ random_fill (b);
+
+ a.accumulate_frames (&b, 129, 91, 44);
+
+ srand (38);
+ for (int i = 0; i < 256; ++i) {
+ for (int c = 0; c < 3; ++c) {
+ float const A = random_float ();
+ if (i < 44 || i >= (44 + 129)) {
+ BOOST_CHECK_CLOSE (a.data(c)[i], A, tolerance);
+ } else {
+ BOOST_CHECK_CLOSE (a.data(c)[i], A + b.data(c)[i + 91 - 44], tolerance);
+ }
+ }
+ }
+}
+
+
+BOOST_AUTO_TEST_CASE (audio_buffers_data)
+{
+ AudioBuffers a (94, 512);
+
+ for (int i = 0; i < 94; ++i) {
+ BOOST_CHECK_EQUAL (a.data()[i], a.data(i));
+ }
+
+ a.set_frames (2048);
+
+ for (int i = 0; i < 94; ++i) {
+ BOOST_CHECK_EQUAL (a.data()[i], a.data(i));
+ }
+}
+
+
+BOOST_AUTO_TEST_CASE (audio_buffers_trim_start)
+{
+ AudioBuffers a (13, 999);
+
+ srand (55);
+ random_fill (a);
+
+ a.trim_start (101);
+
+ srand (55);
+
+ /* Burn the first 101 numbers in the sequence */
+ for (int i = 0; i < 101 * 13; ++i) {
+ random_float ();
+ }
+
+ for (int i = 0; i < (999 - 101); ++i) {
+ for (int j = 0; j < 13; ++j) {
+ BOOST_CHECK_CLOSE (a.data(j)[i], random_float(), tolerance);
+ }
+ }
+}
+
+
+BOOST_AUTO_TEST_CASE(audio_buffers_set_channels_lower)
+{
+ AudioBuffers buffers(9, 9933);
+ srand(4);
+ random_fill(buffers);
+
+ buffers.set_channels(4);
+ BOOST_REQUIRE_EQUAL(buffers.channels(), 4);
+
+ srand(4);
+ for (int i = 0; i < 9933; ++i) {
+ for (int c = 0; c < 4; ++c) {
+ BOOST_CHECK_EQUAL(buffers.data(c)[i], random_float());
+ }
+ for (int c = 4; c < 9; ++c) {
+ random_float();
+ }
+ }
+}
+
+
+BOOST_AUTO_TEST_CASE(audio_buffers_set_channels_higher)
+{
+ AudioBuffers buffers(9, 9933);
+ srand(4);
+ random_fill(buffers);
+
+ buffers.set_channels(13);
+ BOOST_REQUIRE_EQUAL(buffers.channels(), 13);
+
+ srand(4);
+ for (int i = 0; i < 9933; ++i) {
+ for (int c = 0; c < 9; ++c) {
+ BOOST_CHECK_EQUAL(buffers.data(c)[i], random_float());
+ }
+ for (int c = 9; c < 13; ++c) {
+ BOOST_CHECK_EQUAL(buffers.data(c)[i], 0);
+ }
+ }
+}
diff --git a/test/lib/audio_content_test.cc b/test/lib/audio_content_test.cc
new file mode 100644
index 000000000..8c4dafd39
--- /dev/null
+++ b/test/lib/audio_content_test.cc
@@ -0,0 +1,289 @@
+/*
+ Copyright (C) 2022 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/audio_content.h"
+#include "lib/dcp_content.h"
+#include "lib/content_factory.h"
+#include "lib/film.h"
+#include "lib/maths_util.h"
+#include "lib/video_content.h"
+#include "../test.h"
+#include <dcp/sound_asset.h>
+#include <dcp/sound_asset_reader.h>
+#include <boost/test/unit_test.hpp>
+
+
+BOOST_AUTO_TEST_CASE (audio_content_fade_empty_region)
+{
+ auto content = content_factory("test/data/impulse_train.wav");
+ auto film = new_test_film("audio_content_fade_empty_region", content);
+
+ BOOST_CHECK(content[0]->audio->fade(content[0]->audio->stream(), 0, 0, 48000).empty());
+}
+
+
+BOOST_AUTO_TEST_CASE (audio_content_fade_no_fade)
+{
+ auto content = content_factory("test/data/impulse_train.wav");
+ auto film = new_test_film("audio_content_fade_no_fade", content);
+
+ auto const stream = content[0]->audio->stream();
+
+ BOOST_CHECK(content[0]->audio->fade(stream, 0, 2000, 48000).empty());
+ BOOST_CHECK(content[0]->audio->fade(stream, 9999, 451, 48000).empty());
+ BOOST_CHECK(content[0]->audio->fade(stream, stream->length() + 100, 8000, 48000).empty());
+}
+
+
+BOOST_AUTO_TEST_CASE (audio_content_fade_unfaded_part)
+{
+ auto content = content_factory("test/data/impulse_train.wav")[0];
+ auto film = new_test_film("audio_content_fade_unfaded_part", { content });
+
+ auto const stream = content->audio->stream();
+
+ content->audio->set_fade_in(dcpomatic::ContentTime::from_frames(2000, 48000));
+ content->audio->set_fade_out(dcpomatic::ContentTime::from_frames(2000, 48000));
+
+ BOOST_CHECK (content->audio->fade(stream, 2000, 50, 48000).empty());
+ BOOST_CHECK (content->audio->fade(stream, 12000, 99, 48000).empty());
+ BOOST_CHECK (content->audio->fade(stream, stream->length() - 2051, 50, 48000).empty());
+}
+
+
+BOOST_AUTO_TEST_CASE (audio_content_within_the_fade_in)
+{
+ auto content = content_factory("test/data/impulse_train.wav")[0];
+ auto film = new_test_film("audio_content_within_the_fade_in", { content });
+
+ content->audio->set_fade_in(dcpomatic::ContentTime::from_frames(2000, 48000));
+
+ auto const f1 = content->audio->fade(content->audio->stream(), 0, 2000, 48000);
+ BOOST_REQUIRE_EQUAL (f1.size(), 2000U);
+ for (auto i = 0; i < 2000; ++i) {
+ BOOST_REQUIRE_CLOSE (f1[i], logarithmic_fade_in_curve(static_cast<float>(i) / 2000), 0.01);
+ }
+}
+
+
+BOOST_AUTO_TEST_CASE (audio_content_within_the_fade_out)
+{
+ auto content = content_factory("test/data/impulse_train.wav")[0];
+ auto film = new_test_film("audio_content_within_the_fade_out", { content });
+
+ auto const stream = content->audio->stream();
+
+ content->audio->set_fade_in(dcpomatic::ContentTime::from_frames(2000, 48000));
+ content->audio->set_fade_out(dcpomatic::ContentTime::from_frames(2000, 48000));
+
+ auto const f1 = content->audio->fade(stream, stream->length() - 2000, 2000, 48000);
+ BOOST_REQUIRE_EQUAL (f1.size(), 2000U);
+ for (auto i = 0; i < 2000; ++i) {
+ BOOST_REQUIRE_CLOSE (f1[i], logarithmic_fade_out_curve(static_cast<float>(i) / 2000), 0.01);
+ }
+}
+
+
+BOOST_AUTO_TEST_CASE (audio_content_overlapping_the_fade_in)
+{
+ auto content = content_factory("test/data/impulse_train.wav")[0];
+ auto film = new_test_film("audio_content_overlapping_the_fade_in", { content });
+
+ content->audio->set_fade_in(dcpomatic::ContentTime::from_frames(2000, 48000));
+ content->audio->set_fade_out(dcpomatic::ContentTime::from_frames(2000, 48000));
+
+ auto const f1 = content->audio->fade(content->audio->stream(), 1500, 2000, 48000);
+ BOOST_REQUIRE_EQUAL (f1.size(), 2000U);
+ for (auto i = 0; i < 500; ++i) {
+ BOOST_REQUIRE_CLOSE (f1[i], logarithmic_fade_in_curve(static_cast<float>(i + 1500) / 2000), 0.01);
+ }
+ for (auto i = 500; i < 2000; ++i) {
+ BOOST_REQUIRE_CLOSE (f1[i], 1.0f, 0.01);
+ }
+}
+
+
+BOOST_AUTO_TEST_CASE (audio_content_overlapping_the_fade_out)
+{
+ auto content = content_factory("test/data/impulse_train.wav")[0];
+ auto film = new_test_film("audio_content_overlapping_the_fade_out", { content });
+
+ auto const stream = content->audio->stream();
+
+ content->audio->set_fade_in(dcpomatic::ContentTime::from_frames(2000, 48000));
+ content->audio->set_fade_out(dcpomatic::ContentTime::from_frames(4000, 48000));
+
+ auto const f1 = content->audio->fade(stream, stream->length() - 4100, 2000, 48000);
+ BOOST_REQUIRE_EQUAL (f1.size(), 2000U);
+ for (auto i = 0; i < 100; ++i) {
+ BOOST_REQUIRE_CLOSE (f1[i], 1.0f, 0.01);
+ }
+ for (auto i = 100; i < 2000; ++i) {
+ BOOST_REQUIRE_CLOSE (f1[i], logarithmic_fade_out_curve(static_cast<float>(i - 100) / 4000), 0.01);
+ }
+}
+
+
+BOOST_AUTO_TEST_CASE (audio_content_fade_in_and_out)
+{
+ auto content = content_factory("test/data/impulse_train.wav")[0];
+ auto film = new_test_film("audio_content_fade_in_and_out", { content });
+
+ auto const stream = content->audio->stream();
+ auto const length = stream->length();
+
+ content->audio->set_fade_in(dcpomatic::ContentTime::from_frames(length, 48000));
+ content->audio->set_fade_out(dcpomatic::ContentTime::from_frames(length, 48000));
+
+ auto const f1 = content->audio->fade(stream, 0, 10000, 48000);
+ BOOST_REQUIRE_EQUAL (f1.size(), 10000U);
+ for (auto i = 0; i < 10000; ++i) {
+ BOOST_REQUIRE_CLOSE (f1[i], logarithmic_fade_in_curve(static_cast<float>(i) / length) * logarithmic_fade_out_curve(static_cast<float>(i) / length), 0.01);
+ }
+}
+
+
+BOOST_AUTO_TEST_CASE (audio_content_fade_in_with_trim)
+{
+ auto content = content_factory("test/data/impulse_train.wav")[0];
+ auto film = new_test_film("audio_content_fade_in_with_trim", { content });
+
+ auto const stream = content->audio->stream();
+
+ content->audio->set_fade_in(dcpomatic::ContentTime::from_frames(2000, 48000));
+ content->audio->set_fade_out(dcpomatic::ContentTime::from_frames(1000, 48000));
+ content->set_trim_start(film, dcpomatic::ContentTime::from_frames(5200, 48000));
+
+ /* In the trim */
+ auto const f1 = content->audio->fade(stream, 0, 2000, 48000);
+ BOOST_REQUIRE_EQUAL (f1.size(), 2000U);
+ for (auto i = 0; i < 2000; ++i) {
+ BOOST_REQUIRE_CLOSE (f1[i], 0.0f, 0.01);
+ }
+
+ /* In the fade */
+ auto const f2 = content->audio->fade(stream, 5200, 2000, 48000);
+ BOOST_REQUIRE_EQUAL (f2.size(), 2000U);
+ for (auto i = 0; i < 2000; ++i) {
+ BOOST_REQUIRE_CLOSE (f2[i], logarithmic_fade_in_curve(static_cast<float>(i) / 2000), 0.01);
+ }
+}
+
+
+BOOST_AUTO_TEST_CASE (audio_content_fade_out_with_trim)
+{
+ auto content = content_factory("test/data/impulse_train.wav")[0];
+ auto film = new_test_film("audio_content_fade_out_with_trim", { content });
+
+ auto const stream = content->audio->stream();
+ auto const length = stream->length();
+
+ content->audio->set_fade_in(dcpomatic::ContentTime::from_frames(2000, 48000));
+ content->audio->set_fade_out(dcpomatic::ContentTime::from_frames(1000, 48000));
+ content->set_trim_start(film, dcpomatic::ContentTime::from_frames(5200, 48000));
+ content->set_trim_end(dcpomatic::ContentTime::from_frames(9000, 48000));
+
+ /* In the trim */
+ auto const f1 = content->audio->fade(stream, length - 6000, 2000, 48000);
+ BOOST_REQUIRE_EQUAL (f1.size(), 2000U);
+ for (auto i = 0; i < 2000; ++i) {
+ BOOST_REQUIRE_CLOSE (f1[i], 0.0f, 0.01);
+ }
+
+ /* In the fade */
+ auto const f2 = content->audio->fade(stream, length - 9000 - 1000, 1000, 48000);
+ BOOST_REQUIRE_EQUAL (f2.size(), 1000U);
+ for (auto i = 0; i < 1000; ++i) {
+ BOOST_REQUIRE_CLOSE (f2[i], logarithmic_fade_out_curve(static_cast<float>(i) / 1000), 0.01);
+ }
+}
+
+
+BOOST_AUTO_TEST_CASE (audio_content_fade_out_with_trim_at_44k1)
+{
+ /* 5s at 44.1kHz */
+ auto content = content_factory("test/data/white.wav")[0];
+ auto film = new_test_film("audio_content_fade_out_with_trim_at_44k1", { content });
+
+ auto const stream = content->audio->stream();
+
+ /* /----- 3.5s ------|-Fade-|-Trim-\
+ * | | 1s | 0.5s |
+ * \-----------------|------|------/
+ */
+
+ content->audio->set_fade_out(dcpomatic::ContentTime::from_seconds(1));
+ content->set_trim_end(dcpomatic::ContentTime::from_seconds(0.5));
+
+ /* In the trim */
+ auto const f1 = content->audio->fade(stream, std::round(48000 * 4.75), 200, 48000);
+ BOOST_REQUIRE_EQUAL (f1.size(), 200U);
+ for (auto i = 0; i < 200; ++i) {
+ BOOST_REQUIRE_CLOSE (f1[i], 0.0f, 0.01);
+ }
+
+ /* In the fade */
+ auto const f2 = content->audio->fade(stream, std::round(48000 * 3.5 + 200), 7000, 48000);
+ BOOST_REQUIRE_EQUAL (f2.size(), 7000U);
+ for (auto i = 0; i < 7000; ++i) {
+ BOOST_REQUIRE_CLOSE (f2[i], logarithmic_fade_out_curve(static_cast<float>(i + 200) / 48000), 0.01);
+ }
+
+}
+
+
+BOOST_AUTO_TEST_CASE (audio_content_fades_same_as_video)
+{
+ auto content = content_factory("test/data/staircase.mov")[0];
+ auto film = new_test_film("audio_content_fades_same_as_video", { content });
+
+ content->audio->set_use_same_fades_as_video(true);
+ content->video->set_fade_in(9);
+ content->video->set_fade_out(81);
+
+ BOOST_CHECK(content->audio->fade_in() == dcpomatic::ContentTime::from_frames(9 * 48000 / 24, 48000));
+ BOOST_CHECK(content->audio->fade_out() == dcpomatic::ContentTime::from_frames(81 * 48000 / 24, 48000));
+}
+
+
+
+BOOST_AUTO_TEST_CASE(fade_out_works_with_dcp_content)
+{
+ auto dcp = std::make_shared<DCPContent>(TestPaths::private_data() / "JourneyToJah_TLR-1_F_EN-DE-FR_CH_51_2K_LOK_20140225_DGL_SMPTE_OV");
+ auto film = new_test_film("fade_out_works_with_dcp_content", { dcp });
+ dcp->audio->set_fade_out(dcpomatic::ContentTime::from_seconds(15));
+ make_and_verify_dcp(film);
+
+ int32_t max = 0;
+ dcp::SoundAsset sound(find_file(film->dir(film->dcp_name()), "pcm_"));
+ auto reader = sound.start_read();
+ for (auto i = 0; i < sound.intrinsic_duration(); ++i) {
+ auto frame = reader->get_frame(i);
+ for (auto j = 0; j < frame->channels(); ++j) {
+ for (auto k = 0; k < frame->samples(); ++k) {
+ max = std::max(max, frame->get(j, k));
+ }
+ }
+ }
+
+ BOOST_CHECK(max > 2000);
+}
+
diff --git a/test/lib/audio_delay_test.cc b/test/lib/audio_delay_test.cc
new file mode 100644
index 000000000..96ea86f8f
--- /dev/null
+++ b/test/lib/audio_delay_test.cc
@@ -0,0 +1,108 @@
+/*
+ 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/>.
+
+*/
+
+
+/** @defgroup feature Tests of features */
+
+/** @file test/audio_delay_test.cc
+ * @brief Test encode using some FFmpegContents which have audio delays.
+ * @ingroup feature
+ *
+ * The output is checked algorithmically using knowledge of the input.
+ */
+
+
+#include "lib/audio_content.h"
+#include "lib/dcp_content_type.h"
+#include "lib/ffmpeg_content.h"
+#include "lib/film.h"
+#include "lib/ratio.h"
+#include "../test.h"
+#include <dcp/cpl.h>
+#include <dcp/reel.h>
+#include <dcp/reel_sound_asset.h>
+#include <dcp/sound_asset.h>
+#include <dcp/sound_asset_reader.h>
+#include <dcp/sound_frame.h>
+#include <boost/test/unit_test.hpp>
+#include <iostream>
+
+
+using std::cout;
+using std::make_shared;
+using std::string;
+using boost::lexical_cast;
+
+
+static
+void test_audio_delay (int delay_in_ms)
+{
+ string const film_name = "audio_delay_test_" + lexical_cast<string> (delay_in_ms);
+ auto content = make_shared<FFmpegContent>("test/data/staircase.wav");
+ auto film = new_test_film(film_name, { content });
+
+ content->audio->set_delay (delay_in_ms);
+
+ make_and_verify_dcp (film, { dcp::VerificationNote::Code::MISSING_CPL_METADATA });
+
+ boost::filesystem::path path = "build/test";
+ path /= film_name;
+ path /= film->dcp_name ();
+ dcp::DCP check (path.string ());
+ check.read ();
+
+ auto sound_asset = check.cpls().front()->reels().front()->main_sound ();
+ BOOST_CHECK (sound_asset);
+
+ /* Sample index in the DCP */
+ int n = 0;
+ /* DCP sound asset frame */
+ int frame = 0;
+ /* Delay in frames */
+ int const delay_in_frames = delay_in_ms * 48000 / 1000;
+
+ while (n < sound_asset->asset()->intrinsic_duration()) {
+ auto sound_frame = sound_asset->asset()->start_read()->get_frame (frame++);
+ uint8_t const * d = sound_frame->data ();
+
+ for (int i = 0; i < sound_frame->size(); i += (3 * sound_asset->asset()->channels())) {
+
+ /* Mono input so it will appear on centre */
+ int const sample = d[i + 7] | (d[i + 8] << 8);
+
+ int delayed = n - delay_in_frames;
+ if (delayed < 0 || delayed >= 4800) {
+ delayed = 0;
+ }
+
+ BOOST_REQUIRE_EQUAL (sample, delayed);
+ ++n;
+ }
+ }
+}
+
+
+/* Test audio delay when specified in a piece of audio content */
+BOOST_AUTO_TEST_CASE (audio_delay_test)
+{
+ test_audio_delay (0);
+ test_audio_delay (42);
+ test_audio_delay (-66);
+}
diff --git a/test/lib/audio_filter_test.cc b/test/lib/audio_filter_test.cc
new file mode 100644
index 000000000..b5ac25af7
--- /dev/null
+++ b/test/lib/audio_filter_test.cc
@@ -0,0 +1,112 @@
+/*
+ Copyright (C) 2014-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/>.
+
+*/
+
+
+/** @file test/audio_filter_test.cc
+ * @brief Test AudioFilter, LowPassAudioFilter, HighPassAudioFilter classes.
+ * @ingroup selfcontained
+ */
+
+
+#include <boost/test/unit_test.hpp>
+#include "lib/audio_filter.h"
+#include "lib/audio_buffers.h"
+
+
+using std::make_shared;
+
+
+static void
+audio_filter_impulse_test_one (AudioFilter& f, int block_size, int num_blocks)
+{
+ int c = 0;
+
+ for (int i = 0; i < num_blocks; ++i) {
+
+ auto in = make_shared<AudioBuffers>(1, block_size);
+ for (int j = 0; j < block_size; ++j) {
+ in->data()[0][j] = c + j;
+ }
+
+ auto out = f.run (in);
+
+ for (int j = 0; j < out->frames(); ++j) {
+ BOOST_CHECK_EQUAL (out->data()[0][j], c + j);
+ }
+
+ c += block_size;
+ }
+}
+
+
+/** Create a filter with an impulse as a kernel and check that it
+ * passes data through unaltered.
+ */
+BOOST_AUTO_TEST_CASE (audio_filter_impulse_kernel_test)
+{
+ AudioFilter f (0.02);
+
+ f._ir.resize(f._M + 1);
+ f._ir[0] = 1;
+ for (int i = 1; i <= f._M; ++i) {
+ f._ir[i] = 0;
+ }
+
+ audio_filter_impulse_test_one (f, 32, 1);
+ audio_filter_impulse_test_one (f, 256, 1);
+ audio_filter_impulse_test_one (f, 2048, 1);
+}
+
+
+/** Create filters and pass them impulses as input and check that
+ * the filter kernels comes back.
+ */
+BOOST_AUTO_TEST_CASE (audio_filter_impulse_input_test)
+{
+ LowPassAudioFilter lpf (0.02, 0.3);
+
+ auto in = make_shared<AudioBuffers>(1, 1751);
+ in->make_silent ();
+ in->data(0)[0] = 1;
+
+ auto out = lpf.run (in);
+ for (int j = 0; j < out->frames(); ++j) {
+ if (j <= lpf._M) {
+ BOOST_CHECK_EQUAL (out->data(0)[j], lpf._ir[j]);
+ } else {
+ BOOST_CHECK_EQUAL (out->data(0)[j], 0);
+ }
+ }
+
+ HighPassAudioFilter hpf (0.02, 0.3);
+
+ in = make_shared<AudioBuffers>(1, 9133);
+ in->make_silent ();
+ in->data(0)[0] = 1;
+
+ out = hpf.run (in);
+ for (int j = 0; j < out->frames(); ++j) {
+ if (j <= hpf._M) {
+ BOOST_CHECK_EQUAL (out->data(0)[j], hpf._ir[j]);
+ } else {
+ BOOST_CHECK_EQUAL (out->data(0)[j], 0);
+ }
+ }
+}
diff --git a/test/lib/audio_mapping_test.cc b/test/lib/audio_mapping_test.cc
new file mode 100644
index 000000000..22412b260
--- /dev/null
+++ b/test/lib/audio_mapping_test.cc
@@ -0,0 +1,150 @@
+/*
+ Copyright (C) 2014-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/>.
+
+*/
+
+
+/** @file test/audio_mapping_test.cc
+ * @brief Test AudioMapping class.
+ * @ingroup selfcontained
+ */
+
+
+#include <boost/test/unit_test.hpp>
+#include "lib/audio_mapping.h"
+#include "lib/constants.h"
+#include "lib/compose.hpp"
+
+
+using std::list;
+using std::string;
+using boost::optional;
+
+
+BOOST_AUTO_TEST_CASE (audio_mapping_test)
+{
+ AudioMapping none;
+ BOOST_CHECK_EQUAL (none.input_channels(), 0);
+
+ AudioMapping four (4, MAX_DCP_AUDIO_CHANNELS);
+ BOOST_CHECK_EQUAL (four.input_channels(), 4);
+
+ four.set (0, 1, 1);
+
+ for (int i = 0; i < 4; ++i) {
+ for (int j = 0; j < MAX_DCP_AUDIO_CHANNELS; ++j) {
+ BOOST_CHECK_EQUAL (four.get(i, j), (i == 0 && j == 1) ? 1 : 0);
+ }
+ }
+
+ auto mapped = four.mapped_output_channels ();
+ BOOST_CHECK_EQUAL (mapped.size(), 1U);
+ BOOST_CHECK_EQUAL (mapped.front(), 1);
+
+ four.make_zero ();
+
+ for (int i = 0; i < 4; ++i) {
+ for (int j = 0; j < MAX_DCP_AUDIO_CHANNELS; ++j) {
+ BOOST_CHECK_EQUAL (four.get (i, j), 0);
+ }
+ }
+}
+
+
+static void
+guess_check (boost::filesystem::path filename, int output_channel)
+{
+ AudioMapping m (1, 8);
+ m.make_default (0, filename);
+ for (int i = 0; i < 8; ++i) {
+ BOOST_TEST_INFO (String::compose("%1 channel %2", filename, i));
+ BOOST_CHECK_CLOSE (m.get(0, i), i == output_channel ? 1 : 0, 0.01);
+ }
+}
+
+
+BOOST_AUTO_TEST_CASE (audio_mapping_guess_test)
+{
+ guess_check ("stuff_L_nonsense.wav", 0);
+ guess_check ("stuff_nonsense.wav", 2);
+ guess_check ("fred_R.wav", 1);
+ guess_check ("jim_C_sheila.aiff", 2);
+ guess_check ("things_Lfe_and.wav", 3);
+ guess_check ("weeee_Ls.aiff", 4);
+ guess_check ("try_Rs-it.wav", 5);
+
+ /* PT-style */
+ guess_check ("things_LFE.wav", 3);
+ guess_check ("ptish_Lsr_abc.wav", 6);
+ guess_check ("ptish_Rsr_abc.wav", 7);
+ guess_check ("more_Lss_s.wav", 4);
+ guess_check ("other_Rss.aiff", 5);
+
+ /* Only the filename should be taken into account */
+ guess_check ("-Lfe-/foo_L.wav", 0);
+
+ /* Dolby-style */
+ guess_check ("jake-Lrs-good.wav", 6);
+ guess_check ("elwood-Rrs-good.wav", 7);
+}
+
+
+BOOST_AUTO_TEST_CASE(audio_mapping_take_from_larger)
+{
+ AudioMapping A(4, 9);
+ AudioMapping B(2, 3);
+
+ A.set(0, 0, 4);
+ A.set(1, 0, 8);
+ A.set(0, 1, 3);
+ A.set(1, 1, 6);
+ A.set(0, 2, 1);
+ A.set(1, 2, 9);
+
+ B.take_from(A);
+
+ BOOST_CHECK_CLOSE(B.get(0, 0), 4, 0.01);
+ BOOST_CHECK_CLOSE(B.get(1, 0), 8, 0.01);
+ BOOST_CHECK_CLOSE(B.get(0, 1), 3, 0.01);
+ BOOST_CHECK_CLOSE(B.get(1, 1), 6, 0.01);
+ BOOST_CHECK_CLOSE(B.get(0, 2), 1, 0.01);
+ BOOST_CHECK_CLOSE(B.get(1, 2), 9, 0.01);
+}
+
+
+BOOST_AUTO_TEST_CASE(audio_mapping_take_from_smaller)
+{
+ AudioMapping A(4, 9);
+ AudioMapping B(2, 3);
+
+ B.set(0, 0, 4);
+ B.set(1, 0, 8);
+ B.set(0, 1, 3);
+ B.set(1, 1, 6);
+ B.set(0, 2, 1);
+ B.set(1, 2, 9);
+
+ A.take_from(B);
+
+ BOOST_CHECK_CLOSE(A.get(0, 0), 4, 0.01);
+ BOOST_CHECK_CLOSE(A.get(1, 0), 8, 0.01);
+ BOOST_CHECK_CLOSE(A.get(0, 1), 3, 0.01);
+ BOOST_CHECK_CLOSE(A.get(1, 1), 6, 0.01);
+ BOOST_CHECK_CLOSE(A.get(0, 2), 1, 0.01);
+ BOOST_CHECK_CLOSE(A.get(1, 2), 9, 0.01);
+}
diff --git a/test/lib/audio_merger_test.cc b/test/lib/audio_merger_test.cc
new file mode 100644
index 000000000..bda141ef9
--- /dev/null
+++ b/test/lib/audio_merger_test.cc
@@ -0,0 +1,192 @@
+/*
+ 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/>.
+
+*/
+
+
+/** @file test/audio_merger_test.cc
+ * @brief Test AudioMerger class.
+ * @ingroup selfcontained
+ */
+
+
+#include "lib/cross.h"
+#include "lib/audio_merger.h"
+#include "lib/audio_buffers.h"
+#include "lib/dcpomatic_time.h"
+#include "../test.h"
+#include <dcp/file.h>
+#include <dcp/raw_convert.h>
+#include <boost/test/unit_test.hpp>
+#include <boost/bind/bind.hpp>
+#include <boost/signals2.hpp>
+#include <iostream>
+
+
+using std::pair;
+using std::make_shared;
+using std::list;
+using std::cout;
+using std::string;
+using std::shared_ptr;
+using boost::bind;
+using namespace dcpomatic;
+
+
+static shared_ptr<const AudioBuffers> last_audio;
+
+
+int const sampling_rate = 48000;
+
+
+static void
+push(AudioMerger& merger, int from, int to, int at)
+{
+ auto buffers = make_shared<AudioBuffers>(1, to - from);
+ for (int i = 0; i < (to - from); ++i) {
+ buffers->data()[0][i] = from + i;
+ }
+ merger.push(buffers, DCPTime(at, sampling_rate));
+}
+
+
+/* Basic mixing, 2 overlapping pushes */
+BOOST_AUTO_TEST_CASE(audio_merger_test1)
+{
+ AudioMerger merger(sampling_rate);
+
+ push(merger, 0, 64, 0);
+ push(merger, 0, 64, 22);
+
+ auto tb = merger.pull(DCPTime::from_frames(22, sampling_rate));
+ BOOST_REQUIRE(tb.size() == 1U);
+ BOOST_CHECK(tb.front().first != shared_ptr<const AudioBuffers>());
+ BOOST_CHECK_EQUAL(tb.front().first->frames(), 22);
+ BOOST_CHECK_EQUAL(tb.front().second.get(), 0);
+
+ /* And they should be a staircase */
+ for (int i = 0; i < 22; ++i) {
+ BOOST_CHECK_EQUAL(tb.front().first->data()[0][i], i);
+ }
+
+ tb = merger.pull(DCPTime::from_frames(22 + 64, sampling_rate));
+ BOOST_REQUIRE(tb.size() == 1U);
+ BOOST_CHECK_EQUAL(tb.front().first->frames(), 64);
+ BOOST_CHECK_EQUAL(tb.front().second.get(), DCPTime::from_frames(22, sampling_rate).get());
+
+ /* Check the sample values */
+ for (int i = 0; i < 64; ++i) {
+ int correct = i;
+ if (i < (64 - 22)) {
+ correct += i + 22;
+ }
+ BOOST_CHECK_EQUAL(tb.front().first->data()[0][i], correct);
+ }
+}
+
+
+/* Push at non-zero time */
+BOOST_AUTO_TEST_CASE(audio_merger_test2)
+{
+ AudioMerger merger(sampling_rate);
+
+ push(merger, 0, 64, 9);
+
+ /* There's nothing from 0 to 9 */
+ auto tb = merger.pull(DCPTime::from_frames(9, sampling_rate));
+ BOOST_CHECK_EQUAL(tb.size(), 0U);
+
+ /* Then there's our data at 9 */
+ tb = merger.pull(DCPTime::from_frames(9 + 64, sampling_rate));
+
+ BOOST_CHECK_EQUAL(tb.front().first->frames(), 64);
+ BOOST_CHECK_EQUAL(tb.front().second.get(), DCPTime::from_frames(9, sampling_rate).get());
+
+ /* Check the sample values */
+ for (int i = 0; i < 64; ++i) {
+ BOOST_CHECK_EQUAL(tb.front().first->data()[0][i], i);
+ }
+}
+
+
+/* Push two non contiguous blocks */
+BOOST_AUTO_TEST_CASE(audio_merger_test3)
+{
+ AudioMerger merger(sampling_rate);
+
+ push(merger, 0, 64, 17);
+ push(merger, 0, 64, 114);
+
+ /* Get them back */
+
+ auto tb = merger.pull(DCPTime::from_frames(100, sampling_rate));
+ BOOST_REQUIRE(tb.size() == 1U);
+ BOOST_CHECK_EQUAL(tb.front().first->frames(), 64);
+ BOOST_CHECK_EQUAL(tb.front().second.get(), DCPTime::from_frames(17, sampling_rate).get());
+ for (int i = 0; i < 64; ++i) {
+ BOOST_CHECK_EQUAL(tb.front().first->data()[0][i], i);
+ }
+
+ tb = merger.pull(DCPTime::from_frames(200, sampling_rate));
+ BOOST_REQUIRE(tb.size() == 1U);
+ BOOST_CHECK_EQUAL(tb.front().first->frames(), 64);
+ BOOST_CHECK_EQUAL(tb.front().second.get(), DCPTime::from_frames(114, sampling_rate).get());
+ for (int i = 0; i < 64; ++i) {
+ BOOST_CHECK_EQUAL(tb.front().first->data()[0][i], i);
+ }
+}
+
+
+/* Reply a sequence of calls to AudioMerger that resulted in a crash */
+BOOST_AUTO_TEST_CASE(audio_merger_test4)
+{
+ dcp::File f("test/data/audio_merger_bug1.log", "r");
+ BOOST_REQUIRE(f);
+ list<string> tokens;
+ char buf[64];
+ while (fscanf(f.get(), "%63s", buf) == 1) {
+ tokens.push_back(buf);
+ }
+
+ shared_ptr<AudioMerger> merger;
+ auto i = tokens.begin();
+ while (i != tokens.end()) {
+ BOOST_CHECK(*i++ == "I/AM");
+ string const cmd = *i++;
+ if (cmd == "frame_rate") {
+ BOOST_REQUIRE(i != tokens.end());
+ merger.reset(new AudioMerger(dcp::raw_convert<int>(*i++)));
+ } else if (cmd == "clear") {
+ BOOST_REQUIRE(merger);
+ merger->clear();
+ } else if (cmd == "push") {
+ BOOST_REQUIRE(i != tokens.end());
+ DCPTime time(dcp::raw_convert<DCPTime::Type>(*i++));
+ BOOST_REQUIRE(i != tokens.end());
+ int const frames = dcp::raw_convert<int>(*i++);
+ auto buffers = make_shared<AudioBuffers>(1, frames);
+ BOOST_REQUIRE(merger);
+ merger->push(buffers, time);
+ } else if (cmd == "pull") {
+ BOOST_REQUIRE(i != tokens.end());
+ DCPTime time(dcp::raw_convert<DCPTime::Type>(*i++));
+ merger->pull(time);
+ }
+ }
+}
+
diff --git a/test/lib/audio_processor_delay_test.cc b/test/lib/audio_processor_delay_test.cc
new file mode 100644
index 000000000..90c8c1897
--- /dev/null
+++ b/test/lib/audio_processor_delay_test.cc
@@ -0,0 +1,137 @@
+/*
+ Copyright (C) 2015-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/>.
+
+*/
+
+
+/** @file test/audio_processor_delay_test.cc
+ * @brief Test the AudioDelay class.
+ * @ingroup selfcontained
+ */
+
+
+#include "lib/audio_buffers.h"
+#include "lib/audio_delay.h"
+#include <boost/test/unit_test.hpp>
+#include <cmath>
+#include <iostream>
+
+
+using std::cerr;
+using std::cout;
+using std::make_shared;
+
+
+#define CHECK_SAMPLE(c,f,r) \
+ if (fabs(out->data(c)[f] - (r)) > 0.1) { \
+ cerr << "Sample " << f << " at line " << __LINE__ << " is " << out->data(c)[f] << " not " << r << "; difference is " << fabs(out->data(c)[f] - (r)) << "\n"; \
+ BOOST_REQUIRE (fabs(out->data(c)[f] - (r)) <= 0.1); \
+ }
+
+
+/** Block size greater than delay */
+BOOST_AUTO_TEST_CASE (audio_processor_delay_test1)
+{
+ AudioDelay delay (64);
+
+ int const C = 2;
+
+ auto in = make_shared<AudioBuffers>(C, 256);
+ for (int i = 0; i < C; ++i) {
+ for (int j = 0; j < 256; ++j) {
+ in->data(i)[j] = j;
+ }
+ }
+
+ auto out = delay.run (in);
+ BOOST_REQUIRE_EQUAL (out->frames(), in->frames());
+
+ /* Silence at the start */
+ for (int i = 0; i < C; ++i) {
+ for (int j = 0; j < 64; ++j) {
+ CHECK_SAMPLE (i, j, 0);
+ }
+ }
+
+ /* Then the delayed data */
+ for (int i = 0; i < C; ++i) {
+ for (int j = 64; j < 256; ++j) {
+ CHECK_SAMPLE (i, j, j - 64);
+ }
+ }
+
+ /* Feed some more in */
+ for (int i = 0; i < C; ++i) {
+ for (int j = 0; j < 256; ++j) {
+ in->data(i)[j] = j + 256;
+ }
+ }
+ out = delay.run (in);
+
+ /* Check again */
+ for (int i = 0; i < C; ++i) {
+ for (int j = 256; j < 512; ++j) {
+ CHECK_SAMPLE (i, j - 256, j - 64);
+ }
+ }
+}
+
+
+/** Block size less than delay */
+BOOST_AUTO_TEST_CASE (audio_processor_delay_test2)
+{
+ AudioDelay delay (256);
+
+ int const C = 2;
+
+ /* Feeding 4 blocks of 64 should give silence each time */
+
+ for (int i = 0; i < 4; ++i) {
+ auto in = make_shared<AudioBuffers>(C, 64);
+ for (int j = 0; j < C; ++j) {
+ for (int k = 0; k < 64; ++k) {
+ in->data(j)[k] = k + i * 64;
+ }
+ }
+
+ auto out = delay.run (in);
+ BOOST_REQUIRE_EQUAL (out->frames(), in->frames());
+
+ /* Check for silence */
+ for (int j = 0; j < C; ++j) {
+ for (int k = 0; k < 64; ++k) {
+ CHECK_SAMPLE (j, k, 0);
+ }
+ }
+ }
+
+ /* Now feed 4 blocks of silence and we should see the data */
+ for (int i = 0; i < 4; ++i) {
+ /* Feed some silence */
+ auto in = make_shared<AudioBuffers>(C, 64);
+ in->make_silent ();
+ auto out = delay.run (in);
+
+ /* Should now see the delayed data */
+ for (int j = 0; j < C; ++j) {
+ for (int k = 0; k < 64; ++k) {
+ CHECK_SAMPLE (j, k, k + i * 64);
+ }
+ }
+ }
+}
diff --git a/test/lib/audio_processor_test.cc b/test/lib/audio_processor_test.cc
new file mode 100644
index 000000000..79b7c9e62
--- /dev/null
+++ b/test/lib/audio_processor_test.cc
@@ -0,0 +1,59 @@
+/*
+ Copyright (C) 2015-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/>.
+
+*/
+
+
+/** @file test/audio_processor_test.cc
+ * @brief Test audio processors.
+ * @ingroup feature
+ */
+
+
+#include "lib/audio_processor.h"
+#include "lib/analyse_audio_job.h"
+#include "lib/dcp_content_type.h"
+#include "lib/job_manager.h"
+#include "lib/ffmpeg_content.h"
+#include "lib/film.h"
+#include "../test.h"
+#include <boost/test/unit_test.hpp>
+
+
+using std::make_shared;
+
+
+/** Test the mid-side decoder for analysis and DCP-making */
+BOOST_AUTO_TEST_CASE (audio_processor_test)
+{
+ auto c = make_shared<FFmpegContent>("test/data/white.wav");
+ auto film = new_test_film("audio_processor_test", { c });
+
+ film->set_audio_channels(16);
+ film->set_dcp_content_type (DCPContentType::from_isdcf_name ("TST"));
+ film->set_audio_processor (AudioProcessor::from_id ("mid-side-decoder"));
+
+ /* Analyse the audio and check it doesn't crash */
+ auto job = make_shared<AnalyseAudioJob> (film, film->playlist(), false);
+ JobManager::instance()->add (job);
+ BOOST_REQUIRE (!wait_for_jobs());
+
+ /* Make a DCP and check it */
+ make_and_verify_dcp (film, {dcp::VerificationNote::Code::MISSING_CPL_METADATA});
+ check_dcp ("test/data/audio_processor_test", film->dir (film->dcp_name ()));
+}
diff --git a/test/lib/audio_ring_buffers_test.cc b/test/lib/audio_ring_buffers_test.cc
new file mode 100644
index 000000000..265142d53
--- /dev/null
+++ b/test/lib/audio_ring_buffers_test.cc
@@ -0,0 +1,194 @@
+/*
+ Copyright (C) 2016-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 "lib/audio_ring_buffers.h"
+#include <boost/test/unit_test.hpp>
+
+
+using std::make_shared;
+using namespace dcpomatic;
+
+
+#define CANARY 9999
+
+
+/** Basic tests fetching the same number of channels as went in */
+BOOST_AUTO_TEST_CASE (audio_ring_buffers_test1)
+{
+ AudioRingBuffers rb;
+
+ /* Should start off empty */
+ BOOST_CHECK_EQUAL (rb.size(), 0);
+
+ /* Getting some data should give an underrun and write zeros */
+ float buffer[256 * 6];
+ buffer[240 * 6] = CANARY;
+ BOOST_CHECK (!rb.get(buffer, 6, 240));
+ for (int i = 0; i < 240 * 6; ++i) {
+ BOOST_REQUIRE_EQUAL (buffer[i], 0);
+ }
+ BOOST_CHECK_EQUAL (buffer[240 * 6], CANARY);
+
+ /* clear() should give the same result */
+ rb.clear ();
+ BOOST_CHECK_EQUAL (rb.size(), 0);
+ buffer[240 * 6] = CANARY;
+ BOOST_CHECK (rb.get(buffer, 6, 240) == boost::optional<DCPTime>());
+ for (int i = 0; i < 240 * 6; ++i) {
+ BOOST_REQUIRE_EQUAL (buffer[i], 0);
+ }
+ BOOST_CHECK_EQUAL (buffer[240 * 6], CANARY);
+
+ /* Put some data in */
+ auto data = make_shared<AudioBuffers>(6, 91);
+ int value = 0;
+ for (int i = 0; i < 91; ++i) {
+ for (int j = 0; j < 6; ++j) {
+ data->data(j)[i] = value++;
+ }
+ }
+ rb.put (data, DCPTime(), 48000);
+ BOOST_CHECK_EQUAL (rb.size(), 91);
+
+ /* Get part of it out */
+ buffer[40 * 6] = CANARY;
+ BOOST_CHECK (*rb.get(buffer, 6, 40) == DCPTime());
+ int check = 0;
+ for (int i = 0; i < 40 * 6; ++i) {
+ BOOST_REQUIRE_EQUAL (buffer[i], check++);
+ }
+ BOOST_CHECK_EQUAL (buffer[40 * 6], CANARY);
+ BOOST_CHECK_EQUAL (rb.size(), 51);
+
+ /* Get the rest */
+ buffer[51 * 6] = CANARY;
+ BOOST_CHECK (*rb.get(buffer, 6, 51) == DCPTime::from_frames(40, 48000));
+ for (int i = 0; i < 51 * 6; ++i) {
+ BOOST_REQUIRE_EQUAL (buffer[i], check++);
+ }
+ BOOST_CHECK_EQUAL (buffer[51 * 6], CANARY);
+ BOOST_CHECK_EQUAL (rb.size(), 0);
+
+ /* Now there should be an underrun */
+ buffer[240 * 6] = CANARY;
+ BOOST_CHECK (!rb.get(buffer, 6, 240));
+ BOOST_CHECK_EQUAL (buffer[240 * 6], CANARY);
+}
+
+/** Similar tests but fetching more channels than were put in */
+BOOST_AUTO_TEST_CASE (audio_ring_buffers_test2)
+{
+ AudioRingBuffers rb;
+
+ /* Put some data in */
+ auto data = make_shared<AudioBuffers>(2, 91);
+ int value = 0;
+ for (int i = 0; i < 91; ++i) {
+ for (int j = 0; j < 2; ++j) {
+ data->data(j)[i] = value++;
+ }
+ }
+ rb.put (data, DCPTime(), 48000);
+ BOOST_CHECK_EQUAL (rb.size(), 91);
+
+ /* Get part of it out */
+ float buffer[256 * 6];
+ buffer[40 * 6] = CANARY;
+ BOOST_CHECK (*rb.get(buffer, 6, 40) == DCPTime());
+ int check = 0;
+ for (int i = 0; i < 40; ++i) {
+ for (int j = 0; j < 2; ++j) {
+ BOOST_REQUIRE_EQUAL (buffer[i * 6 + j], check++);
+ }
+ for (int j = 2; j < 6; ++j) {
+ BOOST_REQUIRE_EQUAL (buffer[i * 6 + j], 0);
+ }
+ }
+ BOOST_CHECK_EQUAL (buffer[40 * 6], CANARY);
+ BOOST_CHECK_EQUAL (rb.size(), 51);
+
+ /* Get the rest */
+ buffer[51 * 6] = CANARY;
+ BOOST_CHECK (*rb.get(buffer, 6, 51) == DCPTime::from_frames(40, 48000));
+ for (int i = 0; i < 51; ++i) {
+ for (int j = 0; j < 2; ++j) {
+ BOOST_REQUIRE_EQUAL (buffer[i * 6 + j], check++);
+ }
+ for (int j = 2; j < 6; ++j) {
+ BOOST_REQUIRE_EQUAL (buffer[i * 6 + j], 0);
+ }
+ }
+ BOOST_CHECK_EQUAL (buffer[51 * 6], CANARY);
+ BOOST_CHECK_EQUAL (rb.size(), 0);
+
+ /* Now there should be an underrun */
+ buffer[240 * 6] = CANARY;
+ BOOST_CHECK (!rb.get(buffer, 6, 240));
+ BOOST_CHECK_EQUAL (buffer[240 * 6], CANARY);
+}
+
+/** Similar tests but fetching fewer channels than were put in */
+BOOST_AUTO_TEST_CASE (audio_ring_buffers_test3)
+{
+ AudioRingBuffers rb;
+
+ /* Put some data in */
+ auto data = make_shared<AudioBuffers>(6, 91);
+ int value = 0;
+ for (int i = 0; i < 91; ++i) {
+ for (int j = 0; j < 6; ++j) {
+ data->data(j)[i] = value++;
+ }
+ }
+ rb.put (data, DCPTime(), 48000);
+ BOOST_CHECK_EQUAL (rb.size(), 91);
+
+ /* Get part of it out */
+ float buffer[256 * 6];
+ buffer[40 * 2] = CANARY;
+ BOOST_CHECK (*rb.get(buffer, 2, 40) == DCPTime());
+ int check = 0;
+ for (int i = 0; i < 40; ++i) {
+ for (int j = 0; j < 2; ++j) {
+ BOOST_REQUIRE_EQUAL (buffer[i * 2 + j], check++);
+ }
+ check += 4;
+ }
+ BOOST_CHECK_EQUAL (buffer[40 * 2], CANARY);
+ BOOST_CHECK_EQUAL (rb.size(), 51);
+
+ /* Get the rest */
+ buffer[51 * 2] = CANARY;
+ BOOST_CHECK (*rb.get(buffer, 2, 51) == DCPTime::from_frames(40, 48000));
+ for (int i = 0; i < 51; ++i) {
+ for (int j = 0; j < 2; ++j) {
+ BOOST_REQUIRE_EQUAL (buffer[i * 2 + j], check++);
+ }
+ check += 4;
+ }
+ BOOST_CHECK_EQUAL (buffer[51 * 2], CANARY);
+ BOOST_CHECK_EQUAL (rb.size(), 0);
+
+ /* Now there should be an underrun */
+ buffer[240 * 2] = CANARY;
+ BOOST_CHECK (!rb.get(buffer, 2, 240));
+ BOOST_CHECK_EQUAL (buffer[240 * 2], CANARY);
+}
diff --git a/test/lib/burnt_subtitle_test.cc b/test/lib/burnt_subtitle_test.cc
new file mode 100644
index 000000000..356c3cd98
--- /dev/null
+++ b/test/lib/burnt_subtitle_test.cc
@@ -0,0 +1,205 @@
+/*
+ Copyright (C) 2014-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/>.
+
+*/
+
+
+/** @file test/burnt_subtitle_test.cc
+ * @brief Test the burning of subtitles into the DCP.
+ * @ingroup feature
+ */
+
+
+#include "lib/config.h"
+#include "lib/content_factory.h"
+#include "lib/dcp_content.h"
+#include "lib/dcp_content_type.h"
+#include "lib/film.h"
+#include "lib/ffmpeg_film_encoder.h"
+#include "lib/log_entry.h"
+#include "lib/ratio.h"
+#include "lib/text_content.h"
+#include "../test.h"
+#include <dcp/dcp.h>
+#include <dcp/cpl.h>
+#include <dcp/reel.h>
+#include <dcp/j2k_transcode.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/openjpeg_image.h>
+#include <dcp/reel_picture_asset.h>
+#include <dcp/reel_mono_picture_asset.h>
+#include <pango/pango-utils.h>
+#include <boost/test/unit_test.hpp>
+
+
+using std::dynamic_pointer_cast;
+using std::make_shared;
+using std::map;
+using std::string;
+using namespace dcpomatic;
+
+
+/* Some of these tests produce slightly different outputs on different platforms / OS versions.
+ * I don't know why...
+ */
+
+
+/** Build a small DCP with no picture and a single subtitle overlaid onto it from a SubRip file */
+BOOST_AUTO_TEST_CASE (burnt_subtitle_test_subrip)
+{
+ auto content = content_factory("test/data/subrip2.srt")[0];
+ auto film = new_test_film("burnt_subtitle_test_subrip", { content });
+ film->set_dcp_content_type(DCPContentType::from_isdcf_name("TLR"));
+ content->text[0]->set_use(true);
+ content->text[0]->set_burn(true);
+ make_and_verify_dcp(
+ film,
+ { dcp::VerificationNote::Code::MISSING_CPL_METADATA }
+ );
+
+#if defined(DCPOMATIC_WINDOWS)
+ check_dcp("test/data/windows/burnt_subtitle_test_subrip", film);
+#elif defined(DCPOMATIC_OSX)
+ check_dcp("test/data/mac/burnt_subtitle_test_subrip", film);
+#else
+ check_dcp("test/data/burnt_subtitle_test_subrip", film);
+#endif
+}
+
+/** Build a small DCP with no picture and a single subtitle overlaid onto it from a DCP XML file */
+BOOST_AUTO_TEST_CASE (burnt_subtitle_test_dcp)
+{
+ auto content = content_factory("test/data/dcp_sub.xml")[0];
+ auto film = new_test_film("burnt_subtitle_test_dcp", { content });
+ film->set_dcp_content_type(DCPContentType::from_isdcf_name("TLR"));
+ film->set_name("frobozz");
+ content->text[0]->set_use(true);
+ make_and_verify_dcp(
+ film,
+ {
+ dcp::VerificationNote::Code::MISSING_SUBTITLE_LANGUAGE,
+ dcp::VerificationNote::Code::INVALID_SUBTITLE_FIRST_TEXT_TIME,
+ dcp::VerificationNote::Code::MISSING_CPL_METADATA
+ });
+
+ check_dcp("test/data/burnt_subtitle_test_dcp", film);
+}
+
+/** Burn some subtitles into an existing DCP to check the colour conversion */
+BOOST_AUTO_TEST_CASE (burnt_subtitle_test_onto_dcp)
+{
+ auto film = new_test_film("burnt_subtitle_test_onto_dcp", { content_factory("test/data/flat_black.png")[0] });
+ film->set_dcp_content_type(DCPContentType::from_isdcf_name("TLR"));
+ make_and_verify_dcp (film);
+
+ Config::instance()->set_log_types (Config::instance()->log_types() | LogEntry::TYPE_DEBUG_ENCODE);
+ auto background_dcp = make_shared<DCPContent>(film->dir(film->dcp_name()));
+ auto sub = content_factory("test/data/subrip2.srt")[0];
+ auto film2 = new_test_film("burnt_subtitle_test_onto_dcp2", { background_dcp, sub });
+ film2->set_dcp_content_type(DCPContentType::from_isdcf_name("TLR"));
+ film2->set_name("frobozz");
+ sub->text[0]->set_burn(true);
+ sub->text[0]->set_effect(dcp::Effect::BORDER);
+ make_and_verify_dcp (film2);
+
+ BOOST_CHECK (background_dcp->position() == DCPTime());
+ BOOST_CHECK (sub->position() == DCPTime());
+
+ dcp::DCP dcp (film2->dir (film2->dcp_name ()));
+ dcp.read ();
+ BOOST_REQUIRE_EQUAL(dcp.cpls().size(), 1U);
+ BOOST_REQUIRE_EQUAL(dcp.cpls().front()->reels().size(), 1U);
+ BOOST_REQUIRE (dcp.cpls().front()->reels().front()->main_picture());
+ BOOST_REQUIRE (dcp.cpls().front()->reels().front()->main_picture()->asset());
+ auto pic = dynamic_pointer_cast<dcp::ReelMonoPictureAsset> (
+ dcp.cpls().front()->reels().front()->main_picture()
+ )->mono_j2k_asset();
+ BOOST_REQUIRE (pic);
+ auto frame = pic->start_read()->get_frame(12);
+ auto xyz = frame->xyz_image ();
+ BOOST_CHECK_EQUAL (xyz->size().width, 1998);
+ BOOST_CHECK_EQUAL (xyz->size().height, 1080);
+
+#if defined(DCPOMATIC_WINDOWS)
+ check_dcp("test/data/windows/burnt_subtitle_test_onto_dcp2", film2);
+#elif defined(DCPOMATIC_OSX)
+ check_dcp("test/data/mac/burnt_subtitle_test_onto_dcp2", film2);
+#else
+ check_dcp("test/data/burnt_subtitle_test_onto_dcp2", film2);
+#endif
+}
+
+
+
+/** Check positioning of some burnt subtitles from XML files */
+BOOST_AUTO_TEST_CASE(burnt_subtitle_test_position)
+{
+ auto check = [](string alignment)
+ {
+ auto const name = String::compose("burnt_subtitle_test_position_%1", alignment);
+ auto subs = content_factory(String::compose("test/data/burn_%1.xml", alignment));
+ auto film = new_test_film(name, subs);
+ subs[0]->text[0]->set_use(true);
+ subs[0]->text[0]->set_burn(true);
+ make_and_verify_dcp(
+ film,
+ {
+ dcp::VerificationNote::Code::MISSING_SUBTITLE_LANGUAGE,
+ dcp::VerificationNote::Code::INVALID_SUBTITLE_FIRST_TEXT_TIME,
+ dcp::VerificationNote::Code::MISSING_CPL_METADATA
+ });
+
+#if defined(DCPOMATIC_WINDOWS)
+ check_dcp(String::compose("test/data/windows/%1", name), film);
+#elif defined(DCPOMATIC_OSX)
+ check_dcp(String::compose("test/data/mac/%1", name), film);
+#else
+ check_dcp(String::compose("test/data/%1", name), film);
+#endif
+ };
+
+ /* Should have a baseline 216 pixels from the top (0.2 * 1080) */
+ check("top");
+ /* Should have a baseline 756 pixels from the top ((0.5 + 0.2) * 1080) */
+ check("center");
+ /* Should have a baseline 864 pixels from the top ((1 - 0.2) * 1080) */
+ check("bottom");
+}
+
+
+/* Bug #2743 */
+BOOST_AUTO_TEST_CASE(burn_empty_subtitle_test)
+{
+ Cleanup cl;
+
+ auto content = content_factory("test/data/empty_sub.xml")[0];
+ auto film = new_test_film("burnt_empty_subtitle_test", { content });
+ content->text[0]->set_use(true);
+
+ auto job = make_shared<TranscodeJob>(film, TranscodeJob::ChangedBehaviour::IGNORE);
+ auto file = boost::filesystem::path("build") / "test" / "burnt_empty_subtitle_test.mov";
+ cl.add(file);
+ FFmpegFilmEncoder encoder(film, job, file, ExportFormat::PRORES_4444, false, false, false, 23);
+ encoder.go();
+
+ cl.run();
+}
+
+
diff --git a/test/lib/butler_test.cc b/test/lib/butler_test.cc
new file mode 100644
index 000000000..af18a723f
--- /dev/null
+++ b/test/lib/butler_test.cc
@@ -0,0 +1,132 @@
+/*
+ Copyright (C) 2017-2022 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/audio_content.h"
+#include "lib/audio_mapping.h"
+#include "lib/butler.h"
+#include "lib/content_factory.h"
+#include "lib/dcp_content_type.h"
+#include "lib/film.h"
+#include "lib/player.h"
+#include "lib/ratio.h"
+#include "../test.h"
+#include <boost/test/unit_test.hpp>
+
+
+using std::make_shared;
+#if BOOST_VERSION >= 106100
+using namespace boost::placeholders;
+#endif
+using namespace dcpomatic;
+
+
+BOOST_AUTO_TEST_CASE (butler_test1)
+{
+ auto video = content_factory("test/data/flat_red.png")[0];
+ auto audio = content_factory("test/data/staircase.wav")[0];
+ auto film = new_test_film("butler_test1", { video, audio });
+ film->set_audio_channels (6);
+
+ /* This is the map of the player output (5.1) to the butler output (also 5.1) */
+ auto map = AudioMapping (6, 6);
+ for (int i = 0; i < 6; ++i) {
+ map.set (i, i, 1);
+ }
+
+ Player player(film, Image::Alignment::COMPACT, false);
+
+ Butler butler (
+ film,
+ player,
+ map,
+ 6,
+ boost::bind(&PlayerVideo::force, AV_PIX_FMT_RGB24),
+ VideoRange::FULL,
+ Image::Alignment::COMPACT,
+ false,
+ false,
+ Butler::Audio::ENABLED
+ );
+
+ BOOST_CHECK (butler.get_video(Butler::Behaviour::BLOCKING, 0).second == DCPTime());
+ BOOST_CHECK (butler.get_video(Butler::Behaviour::BLOCKING, 0).second == DCPTime::from_frames(1, 24));
+ BOOST_CHECK (butler.get_video(Butler::Behaviour::BLOCKING, 0).second == DCPTime::from_frames(2, 24));
+ /* XXX: check the frame contents */
+
+ float buffer[256 * 6];
+ BOOST_REQUIRE (butler.get_audio(Butler::Behaviour::BLOCKING, buffer, 256) == DCPTime());
+ for (int i = 0; i < 256; ++i) {
+ BOOST_REQUIRE_EQUAL (buffer[i * 6 + 0], 0);
+ BOOST_REQUIRE_EQUAL (buffer[i * 6 + 1], 0);
+ BOOST_REQUIRE_CLOSE (buffer[i * 6 + 2], i / 32768.0f, 0.1);
+ BOOST_REQUIRE_EQUAL (buffer[i * 6 + 3], 0);
+ BOOST_REQUIRE_EQUAL (buffer[i * 6 + 4], 0);
+ BOOST_REQUIRE_EQUAL (buffer[i * 6 + 5], 0);
+ }
+}
+
+
+BOOST_AUTO_TEST_CASE (butler_test2)
+{
+ auto content = content_factory(TestPaths::private_data() / "arrietty_JP-EN.mkv");
+ BOOST_REQUIRE (!content.empty());
+ auto film = new_test_film("butler_test2", { content.front() });
+ BOOST_REQUIRE (content.front()->audio);
+ content.front()->audio->set_delay(100);
+
+ /* This is the map of the player output (5.1) to the butler output (also 5.1) */
+ auto map = AudioMapping (6, 6);
+ for (int i = 0; i < 6; ++i) {
+ map.set (i, i, 1);
+ }
+
+ Player player(film, Image::Alignment::COMPACT, false);
+
+ Butler butler (
+ film,
+ player,
+ map,
+ 6,
+ boost::bind(&PlayerVideo::force, AV_PIX_FMT_RGB24),
+ VideoRange::FULL,
+ Image::Alignment::COMPACT,
+ false,
+ false,
+ Butler::Audio::ENABLED
+ );
+
+ int const audio_frames_per_video_frame = 48000 / 25;
+ float audio_buffer[audio_frames_per_video_frame * 6];
+ for (int i = 0; i < 16; ++i) {
+ butler.get_video(Butler::Behaviour::BLOCKING, 0);
+ butler.get_audio(Butler::Behaviour::BLOCKING, audio_buffer, audio_frames_per_video_frame);
+ }
+
+ butler.seek (DCPTime::from_seconds(60), false);
+
+ for (int i = 0; i < 240; ++i) {
+ butler.get_video(Butler::Behaviour::BLOCKING, 0);
+ butler.get_audio(Butler::Behaviour::BLOCKING, audio_buffer, audio_frames_per_video_frame);
+ }
+
+ butler.rethrow();
+}
+
diff --git a/test/lib/bv20_test.cc b/test/lib/bv20_test.cc
new file mode 100644
index 000000000..8f9164664
--- /dev/null
+++ b/test/lib/bv20_test.cc
@@ -0,0 +1,155 @@
+/*
+ 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/content_factory.h"
+#include "lib/film.h"
+#include "../test.h"
+#include <dcp/warnings.h>
+LIBDCP_DISABLE_WARNINGS
+#include <asdcp/Metadata.h>
+LIBDCP_ENABLE_WARNINGS
+#include <dcp/sound_asset.h>
+#include <dcp/util.h>
+#include <boost/test/unit_test.hpp>
+
+
+using std::shared_ptr;
+using std::string;
+
+
+bool
+has_cpl_mca_subdescriptors(shared_ptr<const Film> film)
+{
+ auto cpl = dcp::file_to_string(find_file(film->dir(film->dcp_name()), "cpl_"));
+ return cpl.find("MCASubDescriptors") != std::string::npos;
+}
+
+
+bool
+has_mxf_mca_subdescriptors(shared_ptr<const Film> film)
+{
+ /* One day hopefully libdcp will read these descriptors and we can find out from the SoundAsset
+ * whether they exist.
+ */
+
+ Kumu::FileReaderFactory factory;
+ ASDCP::PCM::MXFReader reader(factory);
+ auto r = reader.OpenRead(find_file(film->dir(film->dcp_name()), "pcm_").string());
+ BOOST_REQUIRE(!ASDCP_FAILURE(r));
+
+ ASDCP::MXF::WaveAudioDescriptor* essence_descriptor = nullptr;
+ auto const rr = reader.OP1aHeader().GetMDObjectByType(
+ dcp::asdcp_smpte_dict->ul(ASDCP::MDD_WaveAudioDescriptor),
+ reinterpret_cast<ASDCP::MXF::InterchangeObject**>(&essence_descriptor)
+ );
+
+ if (!KM_SUCCESS(rr)) {
+ return false;
+ }
+
+ return essence_descriptor->SubDescriptors.size() > 0;
+}
+
+
+string
+constraints_profile(shared_ptr<const Film> film)
+{
+ auto cpl = dcp::file_to_string(find_file(film->dir(film->dcp_name()), "cpl_"));
+ cxml::Document xml("CompositionPlaylist");
+ xml.read_string(cpl);
+
+ auto reel_list = xml.node_child("ReelList");
+ if (!reel_list) {
+ return {};
+ }
+
+ auto reel = reel_list->node_child("Reel");
+ if (!reel) {
+ return {};
+ }
+
+ auto asset_list = reel->node_child("AssetList");
+ if (!asset_list) {
+ return {};
+ }
+
+ auto meta_asset = asset_list->node_child("CompositionMetadataAsset");
+ if (!meta_asset) {
+ return {};
+ }
+
+ auto extension = meta_asset->node_child("ExtensionMetadataList");
+ if (!extension) {
+ return {};
+ }
+
+ auto metadata = extension->node_child("ExtensionMetadata");
+ if (!metadata) {
+ return {};
+ }
+
+ auto property_list = metadata->node_child("PropertyList");
+ if (!property_list) {
+ return {};
+ }
+
+ auto property = property_list->node_child("Property");
+ if (!property) {
+ return {};
+ }
+
+ if (auto value = property->optional_string_child("Value")) {
+ return *value;
+ }
+
+ return {};
+}
+
+
+BOOST_AUTO_TEST_CASE(bv21_extensions_used_when_not_limited)
+{
+ auto picture = content_factory("test/data/flat_red.png");
+ auto sound = content_factory("test/data/sine_440.wav");
+ auto film = new_test_film("bv21_extensions_used_when_not_limited", { picture.front(), sound.front() });
+
+ make_and_verify_dcp(film);
+
+ BOOST_CHECK(has_cpl_mca_subdescriptors(film));
+ BOOST_CHECK(has_mxf_mca_subdescriptors(film));
+ BOOST_CHECK(constraints_profile(film) == "SMPTE-RDD-52:2020-Bv2.1");
+
+}
+
+
+BOOST_AUTO_TEST_CASE(bv21_extensions_not_used_when_limited)
+{
+ auto picture = content_factory("test/data/flat_red.png");
+ auto sound = content_factory("test/data/sine_440.wav");
+ auto film = new_test_film("bv21_extensions_not_used_when_limited", { picture.front(), sound.front () });
+ film->set_limit_to_smpte_bv20(true);
+
+ make_and_verify_dcp(film);
+
+ BOOST_CHECK(!has_cpl_mca_subdescriptors(film));
+ BOOST_CHECK(!has_mxf_mca_subdescriptors(film));
+ BOOST_CHECK(constraints_profile(film) == "SMPTE-RDD-52:2020-Bv2.0");
+}
+
diff --git a/test/lib/cinema_list_test.cc b/test/lib/cinema_list_test.cc
new file mode 100644
index 000000000..519997dc5
--- /dev/null
+++ b/test/lib/cinema_list_test.cc
@@ -0,0 +1,276 @@
+/*
+ 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/cinema.h"
+#include "lib/cinema_list.h"
+#include "lib/config.h"
+#include "lib/screen.h"
+#include "../test.h"
+#include <dcp/certificate.h>
+#include <dcp/filesystem.h>
+#include <dcp/util.h>
+#include <boost/filesystem.hpp>
+#include <boost/test/unit_test.hpp>
+#include <list>
+#include <string>
+
+
+using std::pair;
+using std::string;
+using std::vector;
+
+
+static
+boost::filesystem::path
+setup(string name)
+{
+ boost::filesystem::path db = boost::filesystem::path("build") / "test" / (name + ".db");
+ boost::system::error_code ec;
+ boost::filesystem::remove(db, ec);
+ return db;
+}
+
+
+BOOST_AUTO_TEST_CASE(add_cinema_test)
+{
+ auto const db = setup("add_cinema_test");
+
+ auto const name = "Bob's Zero-G Cinema";
+ auto const emails = vector<string>{"zerogbob@hotmail.com"};
+ auto const notes = "Nice enough place but the popcorn keeps floating away";
+ auto const utc_offset = dcp::UTCOffset{5, 0};
+
+ CinemaList cinemas(db);
+ cinemas.add_cinema({name, emails, notes, utc_offset});
+
+ CinemaList cinemas2(db);
+ auto const check = cinemas2.cinemas();
+ BOOST_REQUIRE_EQUAL(check.size(), 1U);
+ BOOST_CHECK(check[0].second.name == name);
+ BOOST_CHECK(check[0].second.emails == emails);
+ BOOST_CHECK_EQUAL(check[0].second.notes, notes);
+ BOOST_CHECK(check[0].second.utc_offset == utc_offset);
+}
+
+
+BOOST_AUTO_TEST_CASE(remove_cinema_test)
+{
+ auto const db = setup("remove_cinema_test");
+
+ auto const name1 = "Bob's Zero-G Cinema";
+ auto const emails1 = vector<string>{"zerogbob@hotmail.com"};
+ auto const notes1 = "Nice enough place but the popcorn keeps floating away";
+ auto const utc_offset1 = dcp::UTCOffset{-4, -30};
+
+ auto const name2 = "Angie's Infinite-Screen Cinema";
+ auto const emails2 = vector<string>{"angie@infinitium.com", "projection-screen912341235@infinitium.com"};
+ auto const notes2 = "Nice enough place but it's very hard to find the right screen";
+ auto const utc_offset2 = dcp::UTCOffset{9, 0};
+
+ CinemaList cinemas(db);
+ auto const id1 = cinemas.add_cinema({name1, emails1, notes1, utc_offset1});
+ cinemas.add_cinema({name2, emails2, notes2, utc_offset2});
+
+ auto const check = cinemas.cinemas();
+ BOOST_REQUIRE_EQUAL(check.size(), 2U);
+ BOOST_CHECK(check[0].second.name == name2);
+ BOOST_CHECK(check[0].second.emails == emails2);
+ BOOST_CHECK_EQUAL(check[0].second.notes, notes2);
+ BOOST_CHECK(check[0].second.utc_offset == utc_offset2);
+ BOOST_CHECK(check[1].second.name == name1);
+ BOOST_CHECK(check[1].second.emails == emails1);
+ BOOST_CHECK_EQUAL(check[1].second.notes, notes1);
+ BOOST_CHECK(check[1].second.utc_offset == utc_offset1);
+
+ cinemas.remove_cinema(id1);
+
+ auto const check2 = cinemas.cinemas();
+ BOOST_REQUIRE_EQUAL(check2.size(), 1U);
+ BOOST_CHECK(check2[0].second.name == name2);
+ BOOST_CHECK(check2[0].second.emails == emails2);
+ BOOST_CHECK_EQUAL(check2[0].second.notes, notes2);
+}
+
+
+BOOST_AUTO_TEST_CASE(update_cinema_test)
+{
+ auto const db = setup("update_cinema_test");
+
+ auto const name1 = "Bob's Zero-G Cinema";
+ auto const emails1 = vector<string>{"zerogbob@hotmail.com"};
+ auto const notes1 = "Nice enough place but the popcorn keeps floating away";
+ auto const utc_offset1 = dcp::UTCOffset{-4, -30};
+
+ auto const name2 = "Angie's Infinite-Screen Cinema";
+ auto const emails2 = vector<string>{"angie@infinitium.com", "projection-screen912341235@infinitium.com"};
+ auto const notes2 = "Nice enough place but it's very hard to find the right screen";
+ auto const utc_offset2 = dcp::UTCOffset{9, 0};
+
+ CinemaList cinemas(db);
+ auto const id = cinemas.add_cinema({name1, emails1, notes1, utc_offset1});
+ cinemas.add_cinema({name2, emails2, notes2, utc_offset2});
+
+ auto check = cinemas.cinemas();
+ BOOST_REQUIRE_EQUAL(check.size(), 2U);
+ /* Alphabetically ordered so first is 2 */
+ BOOST_CHECK_EQUAL(check[0].second.name, name2);
+ BOOST_CHECK(check[0].second.emails == emails2);
+ BOOST_CHECK_EQUAL(check[0].second.notes, notes2);
+ BOOST_CHECK(check[0].second.utc_offset == utc_offset2);
+ /* Then 1 */
+ BOOST_CHECK_EQUAL(check[1].second.name, name1);
+ BOOST_CHECK(check[1].second.emails == emails1);
+ BOOST_CHECK_EQUAL(check[1].second.notes, notes1);
+ BOOST_CHECK(check[1].second.utc_offset == utc_offset1);
+
+ cinemas.update_cinema(id, Cinema{name1, vector<string>{"bob@zerogkino.com"}, notes1, utc_offset1});
+
+ check = cinemas.cinemas();
+ BOOST_REQUIRE_EQUAL(check.size(), 2U);
+ BOOST_CHECK_EQUAL(check[0].second.name, name2);
+ BOOST_CHECK(check[0].second.emails == emails2);
+ BOOST_CHECK_EQUAL(check[0].second.notes, notes2);
+ BOOST_CHECK(check[0].second.utc_offset == utc_offset2);
+ BOOST_CHECK_EQUAL(check[1].second.name, name1);
+ BOOST_CHECK(check[1].second.emails == vector<string>{"bob@zerogkino.com"});
+ BOOST_CHECK_EQUAL(check[1].second.notes, notes1);
+ BOOST_CHECK(check[1].second.utc_offset == utc_offset1);
+}
+
+
+BOOST_AUTO_TEST_CASE(add_screen_test)
+{
+ auto const db = setup("add_screen_test");
+
+ CinemaList cinemas(db);
+ auto const cinema_id = cinemas.add_cinema({"Name", { "foo@bar.com" }, "", dcp::UTCOffset()});
+ auto const screen_id = cinemas.add_screen(
+ cinema_id,
+ dcpomatic::Screen(
+ "Screen 1",
+ "Smells of popcorn",
+ dcp::Certificate(dcp::file_to_string("test/data/cert.pem")),
+ string("test/data/cert.pem"),
+ vector<TrustedDevice>{}
+ ));
+
+ auto check = cinemas.screens(cinema_id);
+ BOOST_REQUIRE_EQUAL(check.size(), 1U);
+ BOOST_CHECK(check[0].first == screen_id);
+ BOOST_CHECK_EQUAL(check[0].second.name, "Screen 1");
+ BOOST_CHECK_EQUAL(check[0].second.notes, "Smells of popcorn");
+ BOOST_CHECK(check[0].second.recipient() == dcp::Certificate(dcp::file_to_string("test/data/cert.pem")));
+ BOOST_CHECK(check[0].second.recipient_file == string("test/data/cert.pem"));
+}
+
+
+BOOST_AUTO_TEST_CASE(update_screen_test)
+{
+ auto const db = setup("update_screen_test");
+
+ CinemaList cinemas(db);
+ auto const cinema_id = cinemas.add_cinema({"Name", { "foo@bar.com" }, "", dcp::UTCOffset()});
+
+ auto screen = dcpomatic::Screen(
+ "Screen 1",
+ "Smells of popcorn",
+ dcp::Certificate(dcp::file_to_string("test/data/cert.pem")),
+ string("test/data/cert.pem"),
+ vector<TrustedDevice>{}
+ );
+
+ auto const screen_id = cinemas.add_screen(cinema_id, screen);
+
+ screen.name = "Screen 1 updated";
+ screen.notes = "Smells of popcorn and hope";
+ cinemas.update_screen(cinema_id, screen_id, screen);
+
+ auto check = cinemas.screens(cinema_id);
+ BOOST_REQUIRE_EQUAL(check.size(), 1U);
+ BOOST_CHECK(check[0].first == screen_id);
+ BOOST_CHECK_EQUAL(check[0].second.name, "Screen 1 updated");
+ BOOST_CHECK_EQUAL(check[0].second.notes, "Smells of popcorn and hope");
+ BOOST_CHECK(check[0].second.recipient() == dcp::Certificate(dcp::file_to_string("test/data/cert.pem")));
+ BOOST_CHECK(check[0].second.recipient_file == string("test/data/cert.pem"));
+}
+
+
+BOOST_AUTO_TEST_CASE(cinemas_list_copy_from_xml_test)
+{
+ ConfigRestorer cr("build/test/cinemas_list_copy_config");
+
+ dcp::filesystem::remove_all(*Config::override_path);
+ dcp::filesystem::create_directories(*Config::override_path);
+ dcp::filesystem::copy_file("test/data/cinemas2.xml", *Config::override_path / "cinemas2.xml");
+
+ CinemaList cinema_list;
+ cinema_list.read_legacy_file(Config::instance()->read_path("cinemas2.xml"));
+ auto cinemas = cinema_list.cinemas();
+ BOOST_REQUIRE_EQUAL(cinemas.size(), 3U);
+
+ auto cinema_iter = cinemas.begin();
+ BOOST_CHECK_EQUAL(cinema_iter->second.name, "classy joint");
+ BOOST_CHECK_EQUAL(cinema_iter->second.notes, "Can't stand this place");
+ ++cinema_iter;
+
+ BOOST_CHECK_EQUAL(cinema_iter->second.name, "Great");
+ BOOST_CHECK_EQUAL(cinema_iter->second.emails.size(), 1U);
+ BOOST_CHECK_EQUAL(cinema_iter->second.emails[0], "julie@tinyscreen.com");
+ BOOST_CHECK(cinema_iter->second.utc_offset == dcp::UTCOffset(1, 0));
+ ++cinema_iter;
+
+ BOOST_CHECK_EQUAL(cinema_iter->second.name, "stinking dump");
+ BOOST_CHECK_EQUAL(cinema_iter->second.emails.size(), 2U);
+ BOOST_CHECK_EQUAL(cinema_iter->second.emails[0], "bob@odourscreen.com");
+ BOOST_CHECK_EQUAL(cinema_iter->second.emails[1], "alice@whiff.com");
+ BOOST_CHECK_EQUAL(cinema_iter->second.notes, "Great cinema, smells of roses");
+ BOOST_CHECK(cinema_iter->second.utc_offset == dcp::UTCOffset(-7, 0));
+ auto screens = cinema_list.screens(cinema_iter->first);
+ BOOST_CHECK_EQUAL(screens.size(), 2U);
+ auto screen_iter = screens.begin();
+ BOOST_CHECK_EQUAL(screen_iter->second.name, "1");
+ BOOST_CHECK(screen_iter->second.recipient());
+ BOOST_CHECK_EQUAL(screen_iter->second.recipient()->subject_dn_qualifier(), "CVsuuv9eYsQZSl8U4fDpvOmzZhI=");
+ ++screen_iter;
+ BOOST_CHECK_EQUAL(screen_iter->second.name, "2");
+ BOOST_CHECK(screen_iter->second.recipient());
+ BOOST_CHECK_EQUAL(screen_iter->second.recipient()->subject_dn_qualifier(), "CVsuuv9eYsQZSl8U4fDpvOmzZhI=");
+}
+
+
+BOOST_AUTO_TEST_CASE(cinemas_list_sort_test)
+{
+ auto const db = setup("cinemas_list_sort_test");
+
+ CinemaList cinemas(db);
+ cinemas.add_cinema({"Ŝpecial", { "foo@bar.com" }, "", dcp::UTCOffset()});
+ cinemas.add_cinema({"Ţest", { "foo@bar.com" }, "", dcp::UTCOffset()});
+ cinemas.add_cinema({"Name", { "foo@bar.com" }, "", dcp::UTCOffset()});
+ cinemas.add_cinema({"ÄBC", { "foo@bar.com" }, "", dcp::UTCOffset()});
+
+ auto sorted = cinemas.cinemas();
+ BOOST_REQUIRE_EQUAL(sorted.size(), 4U);
+ BOOST_CHECK_EQUAL(sorted[0].second.name, "ÄBC");
+ BOOST_CHECK_EQUAL(sorted[1].second.name, "Name");
+ BOOST_CHECK_EQUAL(sorted[2].second.name, "Ŝpecial");
+ BOOST_CHECK_EQUAL(sorted[3].second.name, "Ţest");
+}
+
diff --git a/test/lib/cinema_sound_processor_test.cc b/test/lib/cinema_sound_processor_test.cc
new file mode 100644
index 000000000..b53917641
--- /dev/null
+++ b/test/lib/cinema_sound_processor_test.cc
@@ -0,0 +1,88 @@
+/*
+ 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/>.
+
+*/
+
+
+#include "lib/datasat_ap2x.h"
+#include "lib/dolby_cp750.h"
+#include "lib/usl.h"
+#include <boost/test/unit_test.hpp>
+
+
+BOOST_AUTO_TEST_CASE (dolby_cp750_test)
+{
+ DolbyCP750 ap;
+
+ /* No change */
+ BOOST_CHECK_CLOSE (ap.db_for_fader_change(7, 7), 0, 0.1);
+ /* Within 0->4 range, up */
+ BOOST_CHECK_CLOSE (ap.db_for_fader_change(1, 3), 40, 0.1);
+ /* Within 0->4 range, down */
+ BOOST_CHECK_CLOSE (ap.db_for_fader_change(3, 1), -40, 0.1);
+ /* Within 4->10 range, up */
+ BOOST_CHECK_CLOSE (ap.db_for_fader_change(5, 8), 10, 0.1);
+ /* Within 4->10 range, down */
+ BOOST_CHECK_CLOSE (ap.db_for_fader_change(8, 5), -10, 0.1);
+ /* Crossing knee, up */
+ BOOST_CHECK_CLOSE (ap.db_for_fader_change(3, 6), 20 + 6.66666666666666666, 0.1);
+ /* Crossing knee, down */
+ BOOST_CHECK_CLOSE (ap.db_for_fader_change(6, 3), -(20 + 6.66666666666666666), 0.1);
+}
+
+
+BOOST_AUTO_TEST_CASE (usl_test)
+{
+ USL ap;
+
+ /* No change */
+ BOOST_CHECK_CLOSE (ap.db_for_fader_change(7, 7), 0, 0.1);
+ /* Within 0->5.5 range, up */
+ BOOST_CHECK_CLOSE (ap.db_for_fader_change(1, 3), 20, 0.1);
+ /* Within 0->5.5 range, down */
+ BOOST_CHECK_CLOSE (ap.db_for_fader_change(3, 1), -20, 0.1);
+ /* Within 5.5->10 range, up */
+ BOOST_CHECK_CLOSE (ap.db_for_fader_change(6, 9), 10, 0.1);
+ /* Within 5.5->10 range, down */
+ BOOST_CHECK_CLOSE (ap.db_for_fader_change(9, 6), -10, 0.1);
+ /* Crossing knee, up */
+ BOOST_CHECK_CLOSE (ap.db_for_fader_change(3, 6), (2.5 * 10 + 0.5 * 3.333333333333333333), 0.1);
+ /* Crossing knee, down */
+ BOOST_CHECK_CLOSE (ap.db_for_fader_change(6, 3), -(2.5 * 10 + 0.5 * 3.333333333333333333), 0.1);
+}
+
+
+BOOST_AUTO_TEST_CASE (datasat_ap2x_test)
+{
+ DatasatAP2x ap;
+
+ /* No change */
+ BOOST_CHECK_CLOSE (ap.db_for_fader_change(7, 7), 0, 0.1);
+ /* Within 0->3.2 range, up */
+ BOOST_CHECK_CLOSE (ap.db_for_fader_change(0, 2), 40, 0.1);
+ /* Within 0->3.2 range, down */
+ BOOST_CHECK_CLOSE (ap.db_for_fader_change(2, 0), -40, 0.1);
+ /* Within 3.2->10 range, up */
+ BOOST_CHECK_CLOSE (ap.db_for_fader_change(6, 9), 15, 0.1);
+ /* Within 3.2->10 range, down */
+ BOOST_CHECK_CLOSE (ap.db_for_fader_change(9, 6), -15, 0.1);
+ /* Crossing knee, up */
+ BOOST_CHECK_CLOSE (ap.db_for_fader_change(3, 6), (0.2 * 20 + 2.8 * 5), 0.1);
+ /* Crossing knee, down */
+ BOOST_CHECK_CLOSE (ap.db_for_fader_change(6, 3), -(0.2 * 20 + 2.8 * 5), 0.1);
+}
diff --git a/test/lib/client_server_test.cc b/test/lib/client_server_test.cc
new file mode 100644
index 000000000..d09f32f47
--- /dev/null
+++ b/test/lib/client_server_test.cc
@@ -0,0 +1,343 @@
+/*
+ Copyright (C) 2012-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/>.
+
+*/
+
+
+/** @file test/client_server_test.cc
+ * @brief Test the remote encoding code.
+ * @ingroup feature
+ */
+
+
+#include "lib/content_factory.h"
+#include "lib/cross.h"
+#include "lib/dcp_video.h"
+#include "lib/dcpomatic_log.h"
+#include "lib/encode_server.h"
+#include "lib/encode_server_description.h"
+#include "lib/encode_server_finder.h"
+#include "lib/file_log.h"
+#include "lib/film.h"
+#include "lib/image.h"
+#include "lib/j2k_image_proxy.h"
+#include "lib/player_video.h"
+#include "lib/raw_image_proxy.h"
+#include "../test.h"
+#include <boost/test/unit_test.hpp>
+#include <boost/thread.hpp>
+
+
+using std::list;
+using std::make_shared;
+using std::shared_ptr;
+using std::weak_ptr;
+using boost::thread;
+using boost::optional;
+using dcp::ArrayData;
+using namespace dcpomatic;
+
+
+void
+do_remote_encode (shared_ptr<DCPVideo> frame, EncodeServerDescription description, ArrayData locally_encoded)
+{
+ ArrayData remotely_encoded;
+ BOOST_REQUIRE_NO_THROW (remotely_encoded = frame->encode_remotely (description, 1200));
+
+ BOOST_REQUIRE_EQUAL (locally_encoded.size(), remotely_encoded.size());
+ BOOST_CHECK_EQUAL (memcmp (locally_encoded.data(), remotely_encoded.data(), locally_encoded.size()), 0);
+}
+
+
+BOOST_AUTO_TEST_CASE (client_server_test_rgb)
+{
+ auto image = make_shared<Image>(AV_PIX_FMT_RGB24, dcp::Size (1998, 1080), Image::Alignment::PADDED);
+ uint8_t* p = image->data()[0];
+
+ for (int y = 0; y < 1080; ++y) {
+ uint8_t* q = p;
+ for (int x = 0; x < 1998; ++x) {
+ *q++ = x % 256;
+ *q++ = y % 256;
+ *q++ = (x + y) % 256;
+ }
+ p += image->stride()[0];
+ }
+
+ auto sub_image = make_shared<Image>(AV_PIX_FMT_BGRA, dcp::Size (100, 200), Image::Alignment::PADDED);
+ p = sub_image->data()[0];
+ for (int y = 0; y < 200; ++y) {
+ uint8_t* q = p;
+ for (int x = 0; x < 100; ++x) {
+ *q++ = y % 256;
+ *q++ = x % 256;
+ *q++ = (x + y) % 256;
+ *q++ = 1;
+ }
+ p += sub_image->stride()[0];
+ }
+
+ LogSwitcher ls (make_shared<FileLog>("build/test/client_server_test_rgb.log"));
+
+ auto pvf = std::make_shared<PlayerVideo>(
+ make_shared<RawImageProxy>(image),
+ Crop (),
+ optional<double> (),
+ dcp::Size (1998, 1080),
+ dcp::Size (1998, 1080),
+ Eyes::BOTH,
+ Part::WHOLE,
+ ColourConversion(),
+ VideoRange::FULL,
+ weak_ptr<Content>(),
+ optional<ContentTime>(),
+ false
+ );
+
+ pvf->set_text (PositionImage(sub_image, Position<int>(50, 60)));
+
+ auto frame = make_shared<DCPVideo> (
+ pvf,
+ 0,
+ 24,
+ 200000000,
+ Resolution::TWO_K
+ );
+
+ auto locally_encoded = frame->encode_locally ();
+
+ auto server = make_shared<EncodeServer>(true, 2);
+
+ thread server_thread(boost::bind(&EncodeServer::run, server));
+
+ /* Let the server get itself ready */
+ dcpomatic_sleep_seconds (1);
+
+ /* "localhost" rather than "127.0.0.1" here fails on docker; go figure */
+ EncodeServerDescription description ("127.0.0.1", 1, SERVER_LINK_VERSION);
+
+ list<thread> threads;
+ for (int i = 0; i < 8; ++i) {
+ threads.push_back(thread(boost::bind(do_remote_encode, frame, description, locally_encoded)));
+ }
+
+ for (auto& i: threads) {
+ i.join();
+ }
+
+ threads.clear();
+
+ server->stop ();
+ server_thread.join();
+}
+
+
+BOOST_AUTO_TEST_CASE (client_server_test_yuv)
+{
+ auto image = make_shared<Image>(AV_PIX_FMT_YUV420P, dcp::Size (1998, 1080), Image::Alignment::PADDED);
+
+ for (int i = 0; i < image->planes(); ++i) {
+ uint8_t* p = image->data()[i];
+ for (int j = 0; j < image->line_size()[i]; ++j) {
+ *p++ = j % 256;
+ }
+ }
+
+ auto sub_image = make_shared<Image>(AV_PIX_FMT_BGRA, dcp::Size (100, 200), Image::Alignment::PADDED);
+ uint8_t* p = sub_image->data()[0];
+ for (int y = 0; y < 200; ++y) {
+ uint8_t* q = p;
+ for (int x = 0; x < 100; ++x) {
+ *q++ = y % 256;
+ *q++ = x % 256;
+ *q++ = (x + y) % 256;
+ *q++ = 1;
+ }
+ p += sub_image->stride()[0];
+ }
+
+ LogSwitcher ls (make_shared<FileLog>("build/test/client_server_test_yuv.log"));
+
+ auto pvf = std::make_shared<PlayerVideo>(
+ std::make_shared<RawImageProxy>(image),
+ Crop(),
+ optional<double>(),
+ dcp::Size(1998, 1080),
+ dcp::Size(1998, 1080),
+ Eyes::BOTH,
+ Part::WHOLE,
+ ColourConversion(),
+ VideoRange::FULL,
+ weak_ptr<Content>(),
+ optional<ContentTime>(),
+ false
+ );
+
+ pvf->set_text (PositionImage(sub_image, Position<int>(50, 60)));
+
+ auto frame = make_shared<DCPVideo>(
+ pvf,
+ 0,
+ 24,
+ 200000000,
+ Resolution::TWO_K
+ );
+
+ auto locally_encoded = frame->encode_locally ();
+
+ auto server = make_shared<EncodeServer>(true, 2);
+
+ thread server_thread(boost::bind(&EncodeServer::run, server));
+
+ /* Let the server get itself ready */
+ dcpomatic_sleep_seconds (1);
+
+ /* "localhost" rather than "127.0.0.1" here fails on docker; go figure */
+ EncodeServerDescription description ("127.0.0.1", 2, SERVER_LINK_VERSION);
+
+ list<thread> threads;
+ for (int i = 0; i < 8; ++i) {
+ threads.push_back(thread(boost::bind(do_remote_encode, frame, description, locally_encoded)));
+ }
+
+ for (auto& i: threads) {
+ i.join();
+ }
+
+ threads.clear();
+
+ server->stop ();
+ server_thread.join();
+}
+
+
+BOOST_AUTO_TEST_CASE (client_server_test_j2k)
+{
+ auto image = make_shared<Image>(AV_PIX_FMT_YUV420P, dcp::Size (1998, 1080), Image::Alignment::PADDED);
+
+ for (int i = 0; i < image->planes(); ++i) {
+ uint8_t* p = image->data()[i];
+ for (int j = 0; j < image->line_size()[i]; ++j) {
+ *p++ = j % 256;
+ }
+ }
+
+ LogSwitcher ls (make_shared<FileLog>("build/test/client_server_test_j2k.log"));
+
+ auto raw_pvf = std::make_shared<PlayerVideo> (
+ std::make_shared<RawImageProxy>(image),
+ Crop(),
+ optional<double>(),
+ dcp::Size(1998, 1080),
+ dcp::Size(1998, 1080),
+ Eyes::BOTH,
+ Part::WHOLE,
+ ColourConversion(),
+ VideoRange::FULL,
+ weak_ptr<Content>(),
+ optional<ContentTime>(),
+ false
+ );
+
+ auto raw_frame = make_shared<DCPVideo> (
+ raw_pvf,
+ 0,
+ 24,
+ 200000000,
+ Resolution::TWO_K
+ );
+
+ auto raw_locally_encoded = raw_frame->encode_locally ();
+
+ auto j2k_pvf = std::make_shared<PlayerVideo> (
+ std::make_shared<J2KImageProxy>(raw_locally_encoded, dcp::Size(1998, 1080), AV_PIX_FMT_XYZ12LE),
+ Crop(),
+ optional<double>(),
+ dcp::Size(1998, 1080),
+ dcp::Size(1998, 1080),
+ Eyes::BOTH,
+ Part::WHOLE,
+ PresetColourConversion::all().front().conversion,
+ VideoRange::FULL,
+ weak_ptr<Content>(),
+ optional<ContentTime>(),
+ false
+ );
+
+ auto j2k_frame = make_shared<DCPVideo> (
+ j2k_pvf,
+ 0,
+ 24,
+ 200000000,
+ Resolution::TWO_K
+ );
+
+ auto j2k_locally_encoded = j2k_frame->encode_locally ();
+
+ auto server = make_shared<EncodeServer>(true, 2);
+
+ thread server_thread(boost::bind (&EncodeServer::run, server));
+
+ /* Let the server get itself ready */
+ dcpomatic_sleep_seconds (1);
+
+ /* "localhost" rather than "127.0.0.1" here fails on docker; go figure */
+ EncodeServerDescription description ("127.0.0.1", 2, SERVER_LINK_VERSION);
+
+ list<thread> threads;
+ for (int i = 0; i < 8; ++i) {
+ threads.push_back(thread(boost::bind(do_remote_encode, j2k_frame, description, j2k_locally_encoded)));
+ }
+
+ for (auto& i: threads) {
+ i.join();
+ }
+
+ threads.clear();
+
+ server->stop ();
+ server_thread.join();
+
+ EncodeServerFinder::drop();
+}
+
+
+BOOST_AUTO_TEST_CASE(real_encode_with_server)
+{
+ Cleanup cl;
+
+ auto content = content_factory(TestPaths::private_data() / "dolby_aurora.vob");
+ auto film = new_test_film("real_encode_with_server", content, &cl);
+ film->set_interop(false);
+
+ EncodeServerFinder::instance();
+
+ EncodeServer server(true, 4);
+ thread server_thread(boost::bind(&EncodeServer::run, &server));
+
+ make_and_verify_dcp(film);
+
+ server.stop();
+ server_thread.join();
+
+ BOOST_CHECK(server.frames_encoded() > 0);
+ EncodeServerFinder::drop();
+
+ cl.run();
+}
+
diff --git a/test/lib/closed_caption_test.cc b/test/lib/closed_caption_test.cc
new file mode 100644
index 000000000..615aad16a
--- /dev/null
+++ b/test/lib/closed_caption_test.cc
@@ -0,0 +1,123 @@
+/*
+ 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 "lib/film.h"
+#include "lib/string_text_file_content.h"
+#include "lib/text_content.h"
+#include "../test.h"
+#include <dcp/cpl.h>
+#include <dcp/dcp.h>
+#include <dcp/reel.h>
+#include <dcp/reel_text_asset.h>
+#include <boost/test/unit_test.hpp>
+
+
+using std::list;
+using std::make_shared;
+
+
+/** Basic test that Interop closed captions are written */
+BOOST_AUTO_TEST_CASE (closed_caption_test1)
+{
+ Cleanup cl;
+
+ auto content = make_shared<StringTextFileContent>("test/data/subrip.srt");
+ auto film = new_test_film("closed_caption_test1", { content }, &cl);
+
+ content->only_text()->set_type (TextType::CLOSED_CAPTION);
+
+ make_and_verify_dcp (
+ film,
+ {
+ dcp::VerificationNote::Code::MISSING_SUBTITLE_LANGUAGE,
+ dcp::VerificationNote::Code::INVALID_SUBTITLE_FIRST_TEXT_TIME,
+ dcp::VerificationNote::Code::INVALID_CLOSED_CAPTION_LINE_LENGTH,
+ dcp::VerificationNote::Code::MISSING_CPL_METADATA
+ });
+
+ /* Just check to see that there's a CCAP in the CPL: this
+ check could be better!
+ */
+
+ dcp::DCP check (film->dir(film->dcp_name()));
+ check.read ();
+
+ BOOST_REQUIRE_EQUAL (check.cpls().size(), 1U);
+ BOOST_REQUIRE_EQUAL (check.cpls().front()->reels().size(), 1U);
+ BOOST_REQUIRE (!check.cpls().front()->reels().front()->closed_captions().empty());
+
+ cl.run ();
+}
+
+/** Test multiple closed captions */
+BOOST_AUTO_TEST_CASE (closed_caption_test2)
+{
+ Cleanup cl;
+ auto content1 = make_shared<StringTextFileContent>("test/data/subrip.srt");
+ auto content2 = make_shared<StringTextFileContent>("test/data/subrip2.srt");
+ auto content3 = make_shared<StringTextFileContent>("test/data/subrip3.srt");
+ auto film = new_test_film("closed_caption_test2", { content1, content2, content3 }, &cl);
+
+ content1->only_text()->set_type (TextType::CLOSED_CAPTION);
+ content1->only_text()->set_dcp_track (DCPTextTrack("First track", dcp::LanguageTag("fr-FR")));
+ content2->only_text()->set_type (TextType::CLOSED_CAPTION);
+ content2->only_text()->set_dcp_track (DCPTextTrack("Second track", dcp::LanguageTag("de-DE")));
+ content3->only_text()->set_type (TextType::CLOSED_CAPTION);
+ content3->only_text()->set_dcp_track (DCPTextTrack("Third track", dcp::LanguageTag("it-IT")));
+
+ make_and_verify_dcp (
+ film,
+ {
+ dcp::VerificationNote::Code::INVALID_SUBTITLE_DURATION_BV21,
+ dcp::VerificationNote::Code::INVALID_CLOSED_CAPTION_LINE_LENGTH,
+ dcp::VerificationNote::Code::MISSING_CPL_METADATA,
+ dcp::VerificationNote::Code::INVALID_SUBTITLE_FIRST_TEXT_TIME,
+ },
+ true,
+ /* Clairmeta gives an error about having duplicate ClosedCaption entries,
+ * which seems wrong.
+ */
+ false
+ );
+
+ dcp::DCP check (film->dir(film->dcp_name()));
+ check.read ();
+
+ BOOST_REQUIRE_EQUAL (check.cpls().size(), 1U);
+ BOOST_REQUIRE_EQUAL (check.cpls().front()->reels().size(), 1U);
+ auto ccaps = check.cpls().front()->reels().front()->closed_captions();
+ BOOST_REQUIRE_EQUAL (ccaps.size(), 3U);
+
+ auto i = ccaps.begin ();
+ BOOST_CHECK_EQUAL ((*i)->annotation_text().get_value_or(""), "First track");
+ BOOST_REQUIRE (static_cast<bool>((*i)->language()));
+ BOOST_CHECK_EQUAL ((*i)->language().get(), "fr-FR");
+ ++i;
+ BOOST_CHECK_EQUAL ((*i)->annotation_text().get_value_or(""), "Second track");
+ BOOST_REQUIRE (static_cast<bool>((*i)->language()));
+ BOOST_CHECK_EQUAL ((*i)->language().get(), "de-DE");
+ ++i;
+ BOOST_CHECK_EQUAL ((*i)->annotation_text().get_value_or(""), "Third track");
+ BOOST_REQUIRE (static_cast<bool>((*i)->language()));
+ BOOST_CHECK_EQUAL ((*i)->language().get(), "it-IT");
+
+ cl.run ();
+}
diff --git a/test/lib/collator_test.cc b/test/lib/collator_test.cc
new file mode 100644
index 000000000..792a11182
--- /dev/null
+++ b/test/lib/collator_test.cc
@@ -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 "lib/collator.h"
+#include <unicode/uenum.h>
+#include <unicode/coll.h>
+#include <boost/test/unit_test.hpp>
+
+
+BOOST_AUTO_TEST_CASE(collator_compare_works_and_ignores_case)
+{
+ Collator collator;
+
+#if 0
+ // Print out available locales
+ // UErrorCode status;
+ // auto available = ucol_openAvailableLocales(&status);
+ // int32_t length = 0;
+ // while (true) {
+ // auto next = uenum_next(available, &length, &status);
+ // if (!next) {
+ // break;
+ // }
+ // std::cout << next << "\n";
+ // }
+#endif
+
+ BOOST_CHECK_EQUAL(collator.compare("So often YOU won't even notice", "SO OFTEN you won't even NOTiCE"), 0);
+ BOOST_CHECK_EQUAL(collator.compare("So often YOU won't even notice", "SO OFTEN you won't even see"), -1);
+}
+
+
+
+BOOST_AUTO_TEST_CASE(collator_search_works_and_ignores_case)
+{
+ Collator collator;
+
+ BOOST_CHECK(collator.find("outh", "With filthy mouths, and bad attitudes"));
+ BOOST_CHECK(collator.find("with", "With filthy mouths, and bad attitudes"));
+ BOOST_CHECK(!collator.find("ostrabagalous", "With filthy mouths, and bad attitudes"));
+}
diff --git a/test/lib/colour_conversion_test.cc b/test/lib/colour_conversion_test.cc
new file mode 100644
index 000000000..7b4d1eebd
--- /dev/null
+++ b/test/lib/colour_conversion_test.cc
@@ -0,0 +1,122 @@
+/*
+ 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/>.
+
+*/
+
+
+/** @file test/colour_conversion_test.cc
+ * @brief Test ColourConversion class.
+ * @ingroup selfcontained
+ */
+
+
+#include "lib/colour_conversion.h"
+#include "lib/film.h"
+#include <dcp/gamma_transfer_function.h>
+#include <libxml++/libxml++.h>
+#include <boost/test/unit_test.hpp>
+#include <iostream>
+
+
+using std::cout;
+using std::make_shared;
+
+
+BOOST_AUTO_TEST_CASE (colour_conversion_test1)
+{
+ ColourConversion A (dcp::ColourConversion::srgb_to_xyz());
+ ColourConversion B (dcp::ColourConversion::rec709_to_xyz());
+
+ BOOST_CHECK_EQUAL (A.identifier(), "ef8af1b1fda1dfe9656dc99b7a9532c7");
+ BOOST_CHECK_EQUAL (B.identifier(), "e6bd82fd7ebeabe75742fbece0cbf652");
+}
+
+
+BOOST_AUTO_TEST_CASE (colour_conversion_test2)
+{
+ ColourConversion A (dcp::ColourConversion::srgb_to_xyz ());
+ xmlpp::Document doc;
+ auto root = doc.create_root_node ("Test");
+ A.as_xml (root);
+ BOOST_CHECK_EQUAL (
+ doc.write_to_string_formatted ("UTF-8"),
+ "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
+ "<Test>\n"
+ " <InputTransferFunction>\n"
+ " <Type>ModifiedGamma</Type>\n"
+ " <Power>2.4</Power>\n"
+ " <Threshold>0.04045</Threshold>\n"
+ " <A>0.055</A>\n"
+ " <B>12.92</B>\n"
+ " </InputTransferFunction>\n"
+ " <YUVToRGB>0</YUVToRGB>\n"
+ " <RedX>0.64</RedX>\n"
+ " <RedY>0.33</RedY>\n"
+ " <GreenX>0.3</GreenX>\n"
+ " <GreenY>0.6</GreenY>\n"
+ " <BlueX>0.15</BlueX>\n"
+ " <BlueY>0.06</BlueY>\n"
+ " <WhiteX>0.3127</WhiteX>\n"
+ " <WhiteY>0.329</WhiteY>\n"
+ " <OutputGamma>2.6</OutputGamma>\n"
+ "</Test>\n"
+ );
+}
+
+
+BOOST_AUTO_TEST_CASE (colour_conversion_test3)
+{
+ ColourConversion A (dcp::ColourConversion::rec709_to_xyz());
+ xmlpp::Document doc;
+ auto root = doc.create_root_node ("Test");
+ A.as_xml (root);
+ BOOST_CHECK_EQUAL (
+ doc.write_to_string_formatted ("UTF-8"),
+ "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
+ "<Test>\n"
+ " <InputTransferFunction>\n"
+ " <Type>Gamma</Type>\n"
+ " <Gamma>2.2</Gamma>\n"
+ " </InputTransferFunction>\n"
+ " <YUVToRGB>1</YUVToRGB>\n"
+ " <RedX>0.64</RedX>\n"
+ " <RedY>0.33</RedY>\n"
+ " <GreenX>0.3</GreenX>\n"
+ " <GreenY>0.6</GreenY>\n"
+ " <BlueX>0.15</BlueX>\n"
+ " <BlueY>0.06</BlueY>\n"
+ " <WhiteX>0.3127</WhiteX>\n"
+ " <WhiteY>0.329</WhiteY>\n"
+ " <OutputGamma>2.6</OutputGamma>\n"
+ "</Test>\n"
+ );
+}
+
+
+/** Test a round trip via the XML representation */
+BOOST_AUTO_TEST_CASE (colour_conversion_test4)
+{
+ for (auto const& i: PresetColourConversion::all()) {
+ xmlpp::Document out;
+ auto out_root = out.create_root_node("Test");
+ i.conversion.as_xml (out_root);
+ auto in = make_shared<cxml::Document> ("Test");
+ in->read_string (out.write_to_string("UTF-8"));
+ BOOST_CHECK (ColourConversion::from_xml(in, Film::current_state_version).get() == i.conversion);
+ }
+}
diff --git a/test/lib/config_test.cc b/test/lib/config_test.cc
new file mode 100644
index 000000000..b7acb66e4
--- /dev/null
+++ b/test/lib/config_test.cc
@@ -0,0 +1,568 @@
+/*
+ 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 "lib/cinema.h"
+#include "lib/cinema_list.h"
+#include "lib/config.h"
+#include "lib/dkdm_recipient.h"
+#include "lib/dkdm_recipient_list.h"
+#include "lib/unzipper.h"
+#include "lib/zipper.h"
+#include "../test.h"
+#include <boost/test/unit_test.hpp>
+#include <fstream>
+
+
+using std::make_shared;
+using std::ofstream;
+using std::string;
+using std::vector;
+using boost::optional;
+
+
+static string
+rewrite_bad_config (string filename, string extra_line)
+{
+ using namespace boost::filesystem;
+
+ auto base = path("build/test/bad_config/2.18");
+ auto file = base / filename;
+
+ boost::system::error_code ec;
+ remove (file, ec);
+
+ boost::filesystem::create_directories (base);
+ std::ofstream f (file.string().c_str());
+ f << "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
+ << "<Config>\n"
+ << "<Foo></Foo>\n"
+ << extra_line << "\n"
+ << "</Config>\n";
+ f.close ();
+
+ return dcp::file_to_string (file);
+}
+
+
+BOOST_AUTO_TEST_CASE (config_backup_test)
+{
+ ConfigRestorer cr("build/test/bad_config");
+ boost::filesystem::remove_all ("build/test/bad_config");
+
+ /* Write an invalid config file to config.xml */
+ auto const first_write_xml = rewrite_bad_config("config.xml", "first write");
+
+ /* Load the config; this should fail, causing the bad config to be copied to config.xml.1
+ * and a new config.xml created in its place.
+ */
+ Config::instance();
+
+ boost::filesystem::path const prefix = "build/test/bad_config/2.18";
+
+ BOOST_CHECK(boost::filesystem::exists(prefix / "config.xml.1"));
+ BOOST_CHECK(dcp::file_to_string(prefix / "config.xml.1") == first_write_xml);
+ BOOST_CHECK(!boost::filesystem::exists(prefix / "config.xml.2"));
+ BOOST_CHECK(!boost::filesystem::exists(prefix / "config.xml.3"));
+ BOOST_CHECK(!boost::filesystem::exists(prefix / "config.xml.4"));
+
+ Config::drop();
+ auto const second_write_xml = rewrite_bad_config("config.xml", "second write");
+ Config::instance();
+
+ BOOST_CHECK(boost::filesystem::exists(prefix / "config.xml.1"));
+ BOOST_CHECK(dcp::file_to_string(prefix / "config.xml.1") == first_write_xml);
+ BOOST_CHECK(boost::filesystem::exists(prefix / "config.xml.2"));
+ BOOST_CHECK(dcp::file_to_string(prefix / "config.xml.2") == second_write_xml);
+ BOOST_CHECK(!boost::filesystem::exists(prefix / "config.xml.3"));
+ BOOST_CHECK(!boost::filesystem::exists(prefix / "config.xml.4"));
+
+ Config::drop();
+ auto const third_write_xml = rewrite_bad_config("config.xml", "third write");
+ Config::instance();
+
+ BOOST_CHECK(boost::filesystem::exists(prefix / "config.xml.1"));
+ BOOST_CHECK(dcp::file_to_string(prefix / "config.xml.1") == first_write_xml);
+ BOOST_CHECK(boost::filesystem::exists(prefix / "config.xml.2"));
+ BOOST_CHECK(dcp::file_to_string(prefix / "config.xml.2") == second_write_xml);
+ BOOST_CHECK(boost::filesystem::exists(prefix / "config.xml.3"));
+ BOOST_CHECK(dcp::file_to_string(prefix / "config.xml.3") == third_write_xml);
+ BOOST_CHECK(!boost::filesystem::exists(prefix / "config.xml.4"));
+
+ Config::drop();
+ auto const fourth_write_xml = rewrite_bad_config("config.xml", "fourth write");
+ Config::instance();
+
+ BOOST_CHECK(boost::filesystem::exists(prefix / "config.xml.1"));
+ BOOST_CHECK(dcp::file_to_string(prefix / "config.xml.1") == first_write_xml);
+ BOOST_CHECK(boost::filesystem::exists(prefix / "config.xml.2"));
+ BOOST_CHECK(dcp::file_to_string(prefix / "config.xml.2") == second_write_xml);
+ BOOST_CHECK(boost::filesystem::exists(prefix / "config.xml.3"));
+ BOOST_CHECK(dcp::file_to_string(prefix / "config.xml.3") == third_write_xml);
+ BOOST_CHECK(boost::filesystem::exists(prefix / "config.xml.4"));
+ BOOST_CHECK(dcp::file_to_string(prefix / "config.xml.4") == fourth_write_xml);
+}
+
+
+BOOST_AUTO_TEST_CASE (config_backup_with_link_test)
+{
+ using namespace boost::filesystem;
+
+ auto base = path("build/test/bad_config");
+ auto version = base / "2.18";
+
+ ConfigRestorer cr(base);
+
+ boost::filesystem::remove_all (base);
+
+ boost::filesystem::create_directories (version);
+ std::ofstream f (path(version / "config.xml").string().c_str());
+ f << "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
+ << "<Config>\n"
+ << "<Link>" << path(version / "actual.xml").string() << "</Link>\n"
+ << "</Config>\n";
+ f.close ();
+
+ Config::drop ();
+ /* Cause actual.xml to be backed up */
+ rewrite_bad_config ("actual.xml", "first write");
+ Config::instance ();
+
+ /* Make sure actual.xml was backed up to the right place */
+ BOOST_CHECK (boost::filesystem::exists(version / "actual.xml.1"));
+}
+
+
+BOOST_AUTO_TEST_CASE (config_write_utf8_test)
+{
+ ConfigRestorer cr("build/test");
+
+ boost::filesystem::remove_all ("build/test/config.xml");
+ boost::filesystem::copy_file ("test/data/utf8_config.xml", "build/test/config.xml");
+ Config::instance()->write();
+
+ check_text_file ("test/data/utf8_config.xml", "build/test/config.xml");
+}
+
+
+/* 2.14 -> 2.18 */
+BOOST_AUTO_TEST_CASE (config_upgrade_test1)
+{
+ boost::filesystem::path dir = "build/test/config_upgrade_test1";
+ ConfigRestorer cr(dir);
+
+ boost::filesystem::remove_all (dir);
+ boost::filesystem::create_directories (dir);
+
+ boost::filesystem::copy_file ("test/data/2.14.config.xml", dir / "config.xml");
+ boost::filesystem::copy_file ("test/data/2.14.cinemas.xml", dir / "cinemas.xml");
+ try {
+ /* This will fail to read cinemas.xml since the link is to a non-existent directory */
+ Config::instance();
+ } catch (...) {}
+
+ Config::instance()->write();
+
+ check_xml (dir / "config.xml", "test/data/2.14.config.xml", {});
+ check_xml (dir / "cinemas.xml", "test/data/2.14.cinemas.xml", {});
+#if defined(DCPOMATIC_WINDOWS)
+ /* This file has the windows path for dkdm_recipients.xml (with backslashes) */
+ check_xml(dir / "2.18" / "config.xml", "test/data/2.18.config.windows.sqlite.xml", {});
+#elif defined(DCPOMATIC_GROK)
+ check_xml(dir / "2.18" / "config.xml", "test/data/2.18.config.sqlite.grok.xml", {});
+#else
+ check_xml(dir / "2.18" / "config.xml", "test/data/2.18.config.sqlite.xml", {});
+#endif
+ /* cinemas.xml is not copied into 2.18 as its format has not changed */
+ BOOST_REQUIRE (!boost::filesystem::exists(dir / "2.18" / "cinemas.xml"));
+}
+
+
+/* 2.16 -> 2.18 */
+BOOST_AUTO_TEST_CASE (config_upgrade_test2)
+{
+ boost::filesystem::path dir = "build/test/config_upgrade_test2";
+ ConfigRestorer cr(dir);
+ boost::filesystem::remove_all (dir);
+ boost::filesystem::create_directories (dir);
+
+#ifdef DCPOMATIC_WINDOWS
+ boost::filesystem::copy_file("test/data/2.16.config.windows.xml", dir / "config.xml");
+#else
+ boost::filesystem::copy_file("test/data/2.16.config.xml", dir / "config.xml");
+#endif
+ boost::filesystem::copy_file("test/data/2.14.cinemas.xml", dir / "cinemas.xml");
+ try {
+ /* This will fail to read cinemas.xml since the link is to a non-existent directory */
+ Config::instance();
+ } catch (...) {}
+
+ Config::instance()->write();
+
+ check_xml(dir / "cinemas.xml", "test/data/2.14.cinemas.xml", {});
+#if defined(DCPOMATIC_WINDOWS)
+ /* This file has the windows path for dkdm_recipients.xml (with backslashes) */
+ check_xml(dir / "2.18" / "config.xml", "test/data/2.18.config.windows.xml", {});
+ check_xml(dir / "config.xml", "test/data/2.16.config.windows.xml", {});
+#elif defined(DCPOMATIC_GROK)
+ check_xml(dir / "2.18" / "config.xml", "test/data/2.18.config.grok.xml", {});
+ check_xml(dir / "config.xml", "test/data/2.16.config.xml", {});
+#else
+ check_xml(dir / "2.18" / "config.xml", "test/data/2.18.config.xml", {});
+ check_xml(dir / "config.xml", "test/data/2.16.config.xml", {});
+#endif
+ /* cinemas.xml is not copied into 2.18 as its format has not changed */
+ BOOST_REQUIRE (!boost::filesystem::exists(dir / "2.18" / "cinemas.xml"));
+}
+
+
+BOOST_AUTO_TEST_CASE (config_keep_cinemas_if_making_new_config)
+{
+ boost::filesystem::path dir = "build/test/config_keep_cinemas_if_making_new_config";
+ ConfigRestorer cr(dir);
+ boost::filesystem::remove_all (dir);
+ boost::filesystem::create_directories (dir);
+
+ Config::instance()->write();
+
+ CinemaList cinemas;
+ cinemas.add_cinema({"My Great Cinema", {}, "", dcp::UTCOffset()});
+
+ boost::filesystem::copy_file(dir / "cinemas.sqlite3", dir / "backup_for_test.sqlite3");
+
+ Config::drop ();
+ boost::filesystem::remove (dir / "2.18" / "config.xml");
+ Config::instance();
+
+ check_file(dir / "backup_for_test.sqlite3", dir / "cinemas.sqlite3");
+}
+
+
+BOOST_AUTO_TEST_CASE(keep_config_if_cinemas_fail_to_load)
+{
+ /* Make a new config */
+ boost::filesystem::path dir = "build/test/keep_config_if_cinemas_fail_to_load";
+ ConfigRestorer cr(dir);
+ boost::filesystem::remove_all(dir);
+ boost::filesystem::create_directories(dir);
+ Config::instance()->write();
+
+ CinemaList cinema_list;
+ cinema_list.add_cinema(Cinema("Foo", {}, "Bar", dcp::UTCOffset()));
+
+ auto const cinemas = dir / "cinemas.sqlite3";
+
+ /* Back things up */
+ boost::filesystem::copy_file(dir / "2.18" / "config.xml", dir / "config_backup_for_test.xml");
+ boost::filesystem::copy_file(cinemas, dir / "cinemas_backup_for_test.sqlite3");
+
+ /* Corrupt the cinemas */
+ Config::drop();
+ std::ofstream corrupt(cinemas.string().c_str());
+ corrupt << "foo\n";
+ corrupt.close();
+ Config::instance();
+
+ /* We should have the old config.xml */
+ check_text_file(dir / "2.18" / "config.xml", dir / "config_backup_for_test.xml");
+}
+
+
+BOOST_AUTO_TEST_CASE(read_cinemas_xml_and_write_sqlite)
+{
+ /* Set up a config with an XML cinemas file */
+ boost::filesystem::path dir = "build/test/read_cinemas_xml_and_write_sqlite";
+ boost::filesystem::remove_all(dir);
+ boost::filesystem::create_directories(dir);
+ boost::filesystem::create_directories(dir / "2.18");
+
+ boost::filesystem::copy_file("test/data/cinemas.xml", dir / "cinemas.xml");
+ boost::filesystem::copy_file("test/data/2.18.config.xml", dir / "2.18" / "config.xml");
+ {
+ Editor editor(dir / "2.18" / "config.xml");
+ editor.replace(
+ "/home/realldoesnt/exist/this/path/is/nonsense.sqlite3",
+ boost::filesystem::canonical(dir / "cinemas.xml").string()
+ );
+ }
+
+ ConfigRestorer cr(dir);
+
+ /* This should make a sqlite3 file containing the recipients from cinemas.xml */
+ Config::instance();
+
+ {
+ CinemaList test(dir / "cinemas.sqlite3");
+
+ /* The detailed creation of sqlite3 from XML is tested in cinema_list_test.cc */
+ auto cinemas = test.cinemas();
+ BOOST_REQUIRE_EQUAL(cinemas.size(), 3U);
+ BOOST_CHECK_EQUAL(cinemas[0].second.name, "classy joint");
+ BOOST_CHECK_EQUAL(cinemas[1].second.name, "Great");
+ BOOST_CHECK_EQUAL(cinemas[2].second.name, "stinking dump");
+
+ /* Add another recipient to the sqlite */
+ test.add_cinema({"The ol' 1-seater", {}, "Quiet but lonely", dcp::UTCOffset()});
+ }
+
+ /* Reload the config; the old XML should not clobber the new sqlite3 */
+ Config::drop();
+ Config::instance();
+
+ {
+ CinemaList test(dir / "cinemas.sqlite3");
+
+ auto cinemas = test.cinemas();
+ BOOST_REQUIRE_EQUAL(cinemas.size(), 4U);
+ BOOST_CHECK_EQUAL(cinemas[0].second.name, "classy joint");
+ BOOST_CHECK_EQUAL(cinemas[1].second.name, "Great");
+ BOOST_CHECK_EQUAL(cinemas[2].second.name, "stinking dump");
+ BOOST_CHECK_EQUAL(cinemas[3].second.name, "The ol' 1-seater");
+ }
+}
+
+
+BOOST_AUTO_TEST_CASE(read_dkdm_recipients_xml_and_write_sqlite)
+{
+ /* Set up a config with an XML cinemas file */
+ boost::filesystem::path dir = "build/test/read_dkdm_recipients_xml_and_write_sqlite";
+ boost::filesystem::remove_all(dir);
+ boost::filesystem::create_directories(dir);
+ boost::filesystem::create_directories(dir / "2.18");
+
+ boost::filesystem::copy_file("test/data/dkdm_recipients.xml", dir / "dkdm_recipients.xml");
+ boost::filesystem::copy_file("test/data/2.18.config.xml", dir / "2.18" / "config.xml");
+ {
+ Editor editor(dir / "2.18" / "config.xml");
+ editor.replace(
+ "<DKDMRecipientsFile>/home/realldoesnt/exist/this/path/is/nonsense.sqlite3",
+ string{"<DKDMRecipientsFile>"} + boost::filesystem::canonical(dir / "dkdm_recipients.xml").string()
+ );
+ }
+
+ ConfigRestorer cr(dir);
+
+ /* This should make a sqlite3 file containing the recipients from dkdm_recipients.xml */
+ Config::instance();
+
+ {
+ DKDMRecipientList test(dir / "dkdm_recipients.sqlite3");
+
+ /* The detailed creation of sqlite3 from XML is tested in dkdm_recipient_list_test.cc */
+ auto recipients = test.dkdm_recipients();
+ BOOST_REQUIRE_EQUAL(recipients.size(), 2U);
+ BOOST_CHECK_EQUAL(recipients[0].second.name, "Bob's Epics");
+ BOOST_CHECK_EQUAL(recipients[1].second.name, "Sharon's Shorts");
+
+ /* Add another recipient to the sqlite */
+ test.add_dkdm_recipient({"Carl's Classics", "Oldies but goodies", {}, {}});
+ }
+
+ /* Reload the config; the old XML should not clobber the new sqlite3 */
+ Config::drop();
+ Config::instance();
+
+ {
+ DKDMRecipientList test(dir / "dkdm_recipients.sqlite3");
+
+ auto recipients = test.dkdm_recipients();
+ BOOST_REQUIRE_EQUAL(recipients.size(), 3U);
+ BOOST_CHECK_EQUAL(recipients[0].second.name, "Bob's Epics");
+ BOOST_CHECK_EQUAL(recipients[1].second.name, "Carl's Classics");
+ BOOST_CHECK_EQUAL(recipients[2].second.name, "Sharon's Shorts");
+ }
+}
+
+
+BOOST_AUTO_TEST_CASE(save_config_as_zip_test)
+{
+ boost::filesystem::path const dir = "build/test/save_config_as_zip_test";
+ ConfigRestorer cr(dir);
+ boost::system::error_code ec;
+ boost::filesystem::remove_all(dir, ec);
+ boost::filesystem::create_directories(dir);
+ boost::filesystem::copy_file("test/data/2.18.config.xml", dir / "config.xml");
+
+ Config::instance()->set_cinemas_file(dir / "cinemas.sqlite3");
+ Config::instance()->set_dkdm_recipients_file(dir / "dkdm_recipients.sqlite3");
+
+ CinemaList cinemas;
+ cinemas.add_cinema({"My Great Cinema", {}, "", dcp::UTCOffset()});
+ DKDMRecipientList recipients;
+ recipients.add_dkdm_recipient({"Carl's Classics", "Oldies but goodies", {}, {}});
+
+ boost::filesystem::path const zip = "build/test/save.zip";
+ boost::filesystem::remove(zip, ec);
+ save_all_config_as_zip(zip);
+ Unzipper unzipper(zip);
+
+ BOOST_CHECK(unzipper.contains("config.xml"));
+ BOOST_CHECK(unzipper.contains("cinemas.sqlite3"));
+ BOOST_CHECK(unzipper.contains("dkdm_recipients.sqlite3"));
+}
+
+
+/** Load a config ZIP file, which contains an XML cinemas file, and ask to overwrite
+ * the existing cinemas file that we had.
+ */
+BOOST_AUTO_TEST_CASE(load_config_from_zip_with_only_xml_current)
+{
+ ConfigRestorer cr;
+
+ auto cinemas_file = Config::instance()->cinemas_file();
+
+ boost::filesystem::path const zip = "build/test/load.zip";
+ boost::system::error_code ec;
+ boost::filesystem::remove(zip, ec);
+
+ Zipper zipper(zip);
+ zipper.add(
+ "config.xml",
+ boost::algorithm::replace_all_copy(
+ dcp::file_to_string("test/data/2.18.config.xml"),
+ "/home/realldoesnt/exist/this/path/is/nonsense.xml",
+ ""
+ )
+ );
+
+ zipper.add("cinemas.xml", dcp::file_to_string("test/data/cinemas.xml"));
+ zipper.close();
+
+ Config::instance()->load_from_zip(zip, Config::CinemasAction::WRITE_TO_CURRENT_PATH);
+
+ CinemaList cinema_list(cinemas_file);
+ auto cinemas = cinema_list.cinemas();
+ BOOST_REQUIRE_EQUAL(cinemas.size(), 3U);
+ BOOST_CHECK_EQUAL(cinemas[0].second.name, "classy joint");
+ BOOST_CHECK_EQUAL(cinemas[1].second.name, "Great");
+ BOOST_CHECK_EQUAL(cinemas[2].second.name, "stinking dump");
+}
+
+
+/** Load a config ZIP file, which contains an XML cinemas file, and ask to write it to
+ * the location specified by the zipped config.xml.
+ */
+BOOST_AUTO_TEST_CASE(load_config_from_zip_with_only_xml_zip)
+{
+ ConfigRestorer cr;
+
+ boost::filesystem::path const zip = "build/test/load.zip";
+ boost::system::error_code ec;
+ boost::filesystem::remove(zip, ec);
+
+ Zipper zipper(zip);
+ zipper.add(
+ "config.xml",
+ boost::algorithm::replace_all_copy(
+ dcp::file_to_string("test/data/2.18.config.xml"),
+ "/home/realldoesnt/exist/this/path/is/nonsense.sqlite3",
+ "build/test/hide/it/here/cinemas.sqlite3"
+ )
+ );
+
+ zipper.add("cinemas.xml", dcp::file_to_string("test/data/cinemas.xml"));
+ zipper.close();
+
+ Config::instance()->load_from_zip(zip, Config::CinemasAction::WRITE_TO_PATH_IN_ZIPPED_CONFIG);
+
+ CinemaList cinema_list("build/test/hide/it/here/cinemas.sqlite3");
+ auto cinemas = cinema_list.cinemas();
+ BOOST_REQUIRE_EQUAL(cinemas.size(), 3U);
+ BOOST_CHECK_EQUAL(cinemas[0].second.name, "classy joint");
+ BOOST_CHECK_EQUAL(cinemas[1].second.name, "Great");
+ BOOST_CHECK_EQUAL(cinemas[2].second.name, "stinking dump");
+}
+
+
+/** Load a config ZIP file, which contains an XML cinemas file, and ask to ignore it */
+BOOST_AUTO_TEST_CASE(load_config_from_zip_with_only_xml_ignore)
+{
+ ConfigRestorer cr;
+
+ CinemaList cinema_list("build/test/hide/it/here/cinemas.sqlite3");
+ cinema_list.add_cinema(Cinema("Foo", {}, "Bar", dcp::UTCOffset()));
+
+ boost::filesystem::path const zip = "build/test/load.zip";
+ boost::system::error_code ec;
+ boost::filesystem::remove(zip, ec);
+
+ Zipper zipper(zip);
+ zipper.add(
+ "config.xml",
+ boost::algorithm::replace_all_copy(
+ dcp::file_to_string("test/data/2.18.config.xml"),
+ "/home/realldoesnt/exist/this/path/is/nonsense.xml",
+ "build/test/hide/it/here/cinemas.sqlite3"
+ )
+ );
+
+ zipper.add("cinemas.xml", "<?xml version=\"1.0\" encoding=\"UTF-8\"?><Cinemas/>");
+ zipper.close();
+
+ Config::instance()->load_from_zip(zip, Config::CinemasAction::IGNORE);
+
+ auto cinemas = cinema_list.cinemas();
+ BOOST_CHECK(!cinemas.empty());
+}
+
+
+BOOST_AUTO_TEST_CASE(use_sqlite_if_present)
+{
+ for (auto file: { string("cinemas"), string("dkdm_recipients") }) {
+
+ /* Set up a config with an XML file */
+ boost::filesystem::path dir = "build/test/use_sqlite_if_present";
+ boost::filesystem::remove_all(dir);
+ boost::filesystem::create_directories(dir);
+ boost::filesystem::create_directories(dir / "2.18");
+
+ boost::filesystem::copy_file("test/data/cinemas.xml", dir / (file + ".xml"));
+ boost::filesystem::copy_file("test/data/2.18.config.xml", dir / "2.18" / "config.xml");
+ {
+ Editor editor(dir / "2.18" / "config.xml");
+ editor.replace(
+ "/home/realldoesnt/exist/this/path/is/nonsense.sqlite3",
+ boost::filesystem::canonical(dir / (file + ".xml")).string()
+ );
+ }
+
+ ConfigRestorer cr(dir);
+
+ /* This should make a sqlite3 file containing the recipients from cinemas.xml.
+ * But it won't write config.xml, so config.xml will still point to cinemas.xml.
+ * This also happens in real life - but I'm not sure how (perhaps just when DoM is
+ * loaded but doesn't save the config, and then another tool is loaded).
+ */
+ Config::instance();
+
+ BOOST_CHECK(boost::filesystem::exists(dir / (file + ".sqlite3")));
+
+ Config::drop();
+
+ if (file == "cinemas") {
+ BOOST_CHECK(Config::instance()->cinemas_file() == boost::filesystem::canonical(dir / (file + ".sqlite3")));
+ } else {
+ BOOST_CHECK(Config::instance()->dkdm_recipients_file() == boost::filesystem::canonical(dir / (file + ".sqlite3")));
+ }
+ }
+}
+
+
+
diff --git a/test/lib/content_test.cc b/test/lib/content_test.cc
new file mode 100644
index 000000000..0a276f122
--- /dev/null
+++ b/test/lib/content_test.cc
@@ -0,0 +1,182 @@
+/*
+ Copyright (C) 2017-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/>.
+
+*/
+
+
+/** @file test/content_test.cc
+ * @brief Tests which expose problems with certain pieces of content.
+ * @ingroup completedcp
+ */
+
+
+#include "lib/audio_content.h"
+#include "lib/film.h"
+#include "lib/dcp_content_type.h"
+#include "lib/content_factory.h"
+#include "lib/content.h"
+#include "lib/ratio.h"
+#include "../test.h"
+#include <boost/test/unit_test.hpp>
+
+
+using namespace dcpomatic;
+
+
+/** There has been garbled audio with this piece of content */
+BOOST_AUTO_TEST_CASE (content_test1)
+{
+ auto content = content_factory(TestPaths::private_data() / "demo_sound_bug.mkv")[0];
+ auto film = new_test_film("content_test1", { content });
+ film->set_audio_channels(16);
+ make_and_verify_dcp (
+ film,
+ { dcp::VerificationNote::Code::MISSING_FFEC_IN_FEATURE, dcp::VerificationNote::Code::MISSING_FFMC_IN_FEATURE }
+ );
+
+ check_mxf_audio_file(TestPaths::private_data() / "content_test1.mxf", dcp_file(film, "pcm_"));
+}
+
+
+/** Taking some 23.976fps content and trimming 0.5s (in content time) from the start
+ * has failed in the past; ensure that this is fixed.
+ */
+BOOST_AUTO_TEST_CASE (content_test2)
+{
+ auto content = content_factory("test/data/red_23976.mp4")[0];
+ auto film = new_test_film("content_test2", {content});
+ content->set_trim_start(film, ContentTime::from_seconds(0.5));
+ make_and_verify_dcp (film);
+}
+
+
+/** Check that position and start trim of video content is forced to a frame boundary */
+BOOST_AUTO_TEST_CASE (content_test3)
+{
+ auto content = content_factory("test/data/red_24.mp4")[0];
+ auto film = new_test_film("content_test3", {content});
+ film->set_sequence (false);
+
+ /* Trim */
+
+ /* 12 frames */
+ content->set_trim_start(film, ContentTime::from_seconds (12.0 / 24.0));
+ BOOST_CHECK (content->trim_start() == ContentTime::from_seconds (12.0 / 24.0));
+
+ /* 11.2 frames */
+ content->set_trim_start(film, ContentTime::from_seconds (11.2 / 24.0));
+ BOOST_CHECK (content->trim_start() == ContentTime::from_seconds (11.0 / 24.0));
+
+ /* 13.9 frames */
+ content->set_trim_start(film, ContentTime::from_seconds (13.9 / 24.0));
+ BOOST_CHECK (content->trim_start() == ContentTime::from_seconds (14.0 / 24.0));
+
+ /* Position */
+
+ /* 12 frames */
+ content->set_position (film, DCPTime::from_seconds(12.0 / 24.0));
+ BOOST_CHECK (content->position() == DCPTime::from_seconds (12.0 / 24.0));
+
+ /* 11.2 frames */
+ content->set_position (film, DCPTime::from_seconds(11.2 / 24.0));
+ BOOST_CHECK (content->position() == DCPTime::from_seconds (11.0 / 24.0));
+
+ /* 13.9 frames */
+ content->set_position (film, DCPTime::from_seconds(13.9 / 24.0));
+ BOOST_CHECK (content->position() == DCPTime::from_seconds (14.0 / 24.0));
+
+ content->set_video_frame_rate(film, 25);
+
+ /* Check that trim is fixed when the content's video frame rate is `forced' */
+
+ BOOST_CHECK (content->trim_start() == ContentTime::from_seconds (15.0 / 25.0));
+}
+
+
+/** Content containing video will have its length rounded to the nearest video frame */
+BOOST_AUTO_TEST_CASE (content_test4)
+{
+ auto film = new_test_film("content_test4");
+
+ auto video = content_factory("test/data/count300bd24.m2ts")[0];
+ film->examine_and_add_content (video);
+ BOOST_REQUIRE (!wait_for_jobs());
+
+ video->set_trim_end (dcpomatic::ContentTime(3000));
+ BOOST_CHECK (video->length_after_trim(film) == DCPTime::from_frames(299, 24));
+}
+
+
+/** Content containing no video will not have its length rounded to the nearest video frame */
+BOOST_AUTO_TEST_CASE (content_test5)
+{
+ auto audio = content_factory("test/data/sine_16_48_220_10.wav");
+ auto film = new_test_film("content_test5", audio);
+
+ audio[0]->set_trim_end(dcpomatic::ContentTime(3000));
+
+ BOOST_CHECK(audio[0]->length_after_trim(film) == DCPTime(957000));
+}
+
+
+/** Sync error #1833 */
+BOOST_AUTO_TEST_CASE (content_test6)
+{
+ Cleanup cl;
+
+ auto film = new_test_film(
+ "content_test6",
+ content_factory(TestPaths::private_data() / "fha.mkv"),
+ &cl
+ );
+
+ film->set_audio_channels(16);
+ film->set_video_bit_rate(VideoEncoding::JPEG2000, 100000000);
+
+ make_and_verify_dcp (film);
+ check_dcp (TestPaths::private_data() / "v2.18.x" / "fha", film);
+
+ cl.run ();
+}
+
+
+/** Reel length error when making the test for #1833 */
+BOOST_AUTO_TEST_CASE (content_test7)
+{
+ Cleanup cl;
+
+ auto content = content_factory(TestPaths::private_data() / "clapperboard.mp4");
+ auto film = new_test_film("content_test7", content, &cl);
+ content[0]->audio->set_delay(-1000);
+ make_and_verify_dcp (film, { dcp::VerificationNote::Code::INVALID_PICTURE_FRAME_RATE_FOR_2K });
+
+ cl.run();
+}
+
+
+/** WAVs with markers (I think) can end up making audio packets with no channels and no frames (#2617) */
+BOOST_AUTO_TEST_CASE(wav_with_markers_zero_channels_test)
+{
+ Cleanup cl;
+
+ auto content = content_factory(TestPaths::private_data() / "wav_with_markers.wav");
+ auto film = new_test_film("wav_with_markers_zero_channels_test", content, &cl);
+ make_and_verify_dcp(film, { dcp::VerificationNote::Code::MISSING_CPL_METADATA });
+
+ cl.run();
+}
diff --git a/test/lib/copy_dcp_details_to_film_test.cc b/test/lib/copy_dcp_details_to_film_test.cc
new file mode 100644
index 000000000..ffa96e3c5
--- /dev/null
+++ b/test/lib/copy_dcp_details_to_film_test.cc
@@ -0,0 +1,52 @@
+/*
+ Copyright (C) 2025 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/content_factory.h"
+#include "lib/copy_dcp_details_to_film.h"
+#include "lib/dcp_content.h"
+#include "lib/film.h"
+#include "../test.h"
+#include <dcp/language_tag.h>
+#include <boost/test/unit_test.hpp>
+
+
+using std::make_shared;
+
+
+BOOST_AUTO_TEST_CASE(copy_audio_language_to_film)
+{
+ auto content = content_factory("test/data/sine_440.wav")[0];
+ auto film1 = new_test_film("copy_audio_language_to_film1", { content });
+ film1->set_audio_language(dcp::LanguageTag("de-DE"));
+ make_and_verify_dcp(
+ film1,
+ {
+ dcp::VerificationNote::Code::MISSING_CPL_METADATA
+ });
+
+ auto dcp = make_shared<DCPContent>(film1->dir(film1->dcp_name()));
+ auto film2 = new_test_film("copy_audio_language_to_film2", { dcp });
+ copy_dcp_settings_to_film(dcp, film2);
+
+ BOOST_REQUIRE(film2->audio_language());
+ BOOST_CHECK_EQUAL(film2->audio_language()->as_string(), "de-DE");
+}
+
diff --git a/test/lib/cpl_hash_test.cc b/test/lib/cpl_hash_test.cc
new file mode 100644
index 000000000..c3f2b119a
--- /dev/null
+++ b/test/lib/cpl_hash_test.cc
@@ -0,0 +1,96 @@
+/*
+ Copyright (C) 2020-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/>.
+
+*/
+
+
+/** @file test/cpl_hash_test.cc
+ * @brief Make sure that <Hash> tags are always written to CPLs where required.
+ * @ingroup feature
+ */
+
+
+#include "lib/content_factory.h"
+#include "lib/cross.h"
+#include "lib/dcp_content.h"
+#include "lib/film.h"
+#include "../test.h"
+#include <boost/algorithm/string.hpp>
+#include <boost/test/unit_test.hpp>
+
+
+using std::make_shared;
+using std::string;
+
+
+BOOST_AUTO_TEST_CASE (hash_added_to_imported_dcp_test)
+{
+ using namespace boost::filesystem;
+
+ string const ov_name = "hash_added_to_imported_dcp_test_ov";
+ auto ov = new_test_film(
+ ov_name,
+ content_factory("test/data/flat_red.png")
+ );
+ make_and_verify_dcp (ov);
+
+ /* Remove <Hash> tags from the CPL */
+ for (auto i: directory_iterator(String::compose("build/test/%1/%2", ov_name, ov->dcp_name()))) {
+ if (boost::algorithm::starts_with(i.path().filename().string(), "cpl_")) {
+ dcp::File in(i.path(), "r");
+ BOOST_REQUIRE (in);
+ dcp::File out(i.path().string() + ".tmp", "w");
+ BOOST_REQUIRE (out);
+ char buffer[256];
+ while (in.gets(buffer, sizeof(buffer))) {
+ if (string(buffer).find("Hash") == string::npos) {
+ out.puts(buffer);
+ }
+ }
+ in.close();
+ out.close();
+ rename (i.path().string() + ".tmp", i.path());
+ }
+ }
+
+ string const vf_name = "hash_added_to_imported_dcp_test_vf";
+ auto ov_content = make_shared<DCPContent>(String::compose("build/test/%1/%2", ov_name, ov->dcp_name()));
+ auto vf = new_test_film(
+ vf_name, { ov_content }
+ );
+
+ ov_content->set_reference_video (true);
+ make_and_verify_dcp(vf, {dcp::VerificationNote::Code::EXTERNAL_ASSET}, false);
+
+ /* Check for Hash tags in the VF DCP */
+ int hashes = 0;
+ for (auto i: directory_iterator(String::compose("build/test/%1/%2", vf_name, vf->dcp_name()))) {
+ if (boost::algorithm::starts_with(i.path().filename().string(), "cpl_")) {
+ dcp::File in(i.path(), "r");
+ BOOST_REQUIRE (in);
+ char buffer[256];
+ while (in.gets(buffer, sizeof(buffer))) {
+ if (string(buffer).find("Hash") != string::npos) {
+ ++hashes;
+ }
+ }
+ }
+ }
+ BOOST_CHECK_EQUAL (hashes, 2);
+}
+
diff --git a/test/lib/cpl_metadata_test.cc b/test/lib/cpl_metadata_test.cc
new file mode 100644
index 000000000..9ce14c70e
--- /dev/null
+++ b/test/lib/cpl_metadata_test.cc
@@ -0,0 +1,121 @@
+/*
+ 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/audio_content.h"
+#include "lib/content.h"
+#include "lib/content_factory.h"
+#include "lib/film.h"
+#include "../test.h"
+#include <dcp/cpl.h>
+#include <dcp/dcp.h>
+#include <boost/test/unit_test.hpp>
+
+
+using std::shared_ptr;
+
+
+BOOST_AUTO_TEST_CASE(main_sound_configuration_test_51_vi)
+{
+ auto picture = content_factory("test/data/flat_red.png")[0];
+ auto L = content_factory("test/data/L.wav")[0];
+ auto R = content_factory("test/data/R.wav")[0];
+ auto C = content_factory("test/data/C.wav")[0];
+ auto Lfe = content_factory("test/data/Lfe.wav")[0];
+ auto Ls = content_factory("test/data/Ls.wav")[0];
+ auto Rs = content_factory("test/data/Rs.wav")[0];
+ auto VI = content_factory("test/data/sine_440.wav")[0];
+
+ auto film = new_test_film("main_sound_configuration_test_51_vi", { picture, L, R, C, Lfe, Ls, Rs, VI });
+ film->set_audio_channels(8);
+
+ auto set_map = [](shared_ptr<Content> content, dcp::Channel channel) {
+ auto map = content->audio->mapping();
+ map.set(0, channel, 1.0f);
+ content->audio->set_mapping(map);
+ };
+
+ set_map(L, dcp::Channel::LEFT);
+ set_map(R, dcp::Channel::RIGHT);
+ set_map(C, dcp::Channel::CENTRE);
+ set_map(Lfe, dcp::Channel::LFE);
+ set_map(Ls, dcp::Channel::LS);
+ set_map(Rs, dcp::Channel::RS);
+ set_map(VI, dcp::Channel::VI);
+
+ make_and_verify_dcp(film);
+
+ dcp::DCP dcp(film->dir(film->dcp_name()));
+ dcp.read();
+ BOOST_REQUIRE_EQUAL(dcp.cpls().size(), 1U);
+ auto cpl = dcp.cpls()[0];
+
+ auto msc = cpl->main_sound_configuration();
+ BOOST_REQUIRE(msc);
+
+ /* We think this should say 51 not 71 at the start (#2580) */
+ BOOST_CHECK_EQUAL(msc->as_string(), "51/L,R,C,LFE,Ls,Rs,-,VIN");
+}
+
+
+BOOST_AUTO_TEST_CASE(main_sound_configuration_test_71)
+{
+ auto picture = content_factory("test/data/flat_red.png")[0];
+ auto L = content_factory("test/data/L.wav")[0];
+ auto R = content_factory("test/data/R.wav")[0];
+ auto C = content_factory("test/data/C.wav")[0];
+ auto Lfe = content_factory("test/data/Lfe.wav")[0];
+ auto Ls = content_factory("test/data/Ls.wav")[0];
+ auto Rs = content_factory("test/data/Rs.wav")[0];
+ auto BsL = content_factory("test/data/Ls.wav")[0];
+ auto BsR = content_factory("test/data/Rs.wav")[0];
+ auto VI = content_factory("test/data/sine_440.wav")[0];
+
+ auto film = new_test_film("main_sound_configuration_test_51_vi", { picture, L, R, C, Lfe, Ls, Rs, BsL, BsR, VI });
+ film->set_audio_channels(12);
+
+ auto set_map = [](shared_ptr<Content> content, dcp::Channel channel) {
+ auto map = content->audio->mapping();
+ map.set(0, channel, 1.0f);
+ content->audio->set_mapping(map);
+ };
+
+ set_map(L, dcp::Channel::LEFT);
+ set_map(R, dcp::Channel::RIGHT);
+ set_map(C, dcp::Channel::CENTRE);
+ set_map(Lfe, dcp::Channel::LFE);
+ set_map(Ls, dcp::Channel::LS);
+ set_map(Rs, dcp::Channel::RS);
+ set_map(BsL, dcp::Channel::BSL);
+ set_map(BsR, dcp::Channel::BSR);
+ set_map(VI, dcp::Channel::VI);
+
+ make_and_verify_dcp(film);
+
+ dcp::DCP dcp(film->dir(film->dcp_name()));
+ dcp.read();
+ BOOST_REQUIRE_EQUAL(dcp.cpls().size(), 1U);
+ auto cpl = dcp.cpls()[0];
+
+ auto msc = cpl->main_sound_configuration();
+ BOOST_REQUIRE(msc);
+
+ BOOST_CHECK_EQUAL(msc->as_string(), "71/L,R,C,LFE,Lss,Rss,-,VIN,-,-,Lrs,Rrs");
+}
diff --git a/test/lib/create_cli_test.cc b/test/lib/create_cli_test.cc
new file mode 100644
index 000000000..2213c2215
--- /dev/null
+++ b/test/lib/create_cli_test.cc
@@ -0,0 +1,386 @@
+/*
+ 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/>.
+
+*/
+
+
+#include "lib/config.h"
+#include "lib/content.h"
+#include "lib/create_cli.h"
+#include "lib/dcp_content_type.h"
+#include "lib/film.h"
+#include "lib/ratio.h"
+#include "lib/video_content.h"
+#include "../test.h"
+#include <fmt/format.h>
+#include <boost/test/unit_test.hpp>
+#include <boost/tokenizer.hpp>
+#include <boost/algorithm/string/predicate.hpp>
+#include <iostream>
+
+
+using std::string;
+
+
+static CreateCLI
+run (string cmd)
+{
+ /* This approximates the logic which splits command lines up into argc/argv */
+
+ boost::escaped_list_separator<char> els ("", " ", "\"\'");
+ boost::tokenizer<boost::escaped_list_separator<char> > tok (cmd, els);
+
+ std::vector<char*> argv(256);
+ int argc = 0;
+
+ for (boost::tokenizer<boost::escaped_list_separator<char> >::iterator i = tok.begin(); i != tok.end(); ++i) {
+ argv[argc++] = strdup (i->c_str());
+ }
+
+ CreateCLI cc (argc, argv.data());
+
+ for (int i = 0; i < argc; ++i) {
+ free (argv[i]);
+ }
+
+ return cc;
+}
+
+BOOST_AUTO_TEST_CASE (create_cli_test)
+{
+ string collected_error;
+ auto error = [&collected_error](string s) {
+ collected_error += s;
+ };
+
+ CreateCLI cc = run ("dcpomatic2_create --version");
+ BOOST_CHECK (!cc.error);
+ BOOST_CHECK (cc.version);
+
+ cc = run ("dcpomatic2_create --versionX");
+ BOOST_REQUIRE (cc.error);
+ BOOST_CHECK (boost::algorithm::starts_with(*cc.error, "dcpomatic2_create: unrecognised option '--versionX'"));
+
+ cc = run ("dcpomatic2_create --help");
+ BOOST_REQUIRE (cc.error);
+
+ cc = run ("dcpomatic2_create -h");
+ BOOST_REQUIRE (cc.error);
+ BOOST_CHECK(collected_error.empty());
+
+ cc = run ("dcpomatic2_create x --name frobozz --template bar");
+ BOOST_CHECK (!cc.error);
+ BOOST_CHECK_EQUAL(cc._name, "frobozz");
+ BOOST_REQUIRE(cc._template_name);
+ BOOST_CHECK_EQUAL(*cc._template_name, "bar");
+ BOOST_CHECK(collected_error.empty());
+
+ cc = run ("dcpomatic2_create x --dcp-content-type FTR");
+ BOOST_CHECK (!cc.error);
+ BOOST_CHECK_EQUAL(cc._dcp_content_type, DCPContentType::from_isdcf_name("FTR"));
+
+ cc = run ("dcpomatic2_create x --dcp-frame-rate 30");
+ BOOST_CHECK (!cc.error);
+ BOOST_REQUIRE (cc.dcp_frame_rate);
+ BOOST_CHECK_EQUAL (*cc.dcp_frame_rate, 30);
+
+ cc = run ("dcpomatic2_create x --container-ratio 185");
+ BOOST_CHECK (!cc.error);
+ BOOST_CHECK_EQUAL(cc._container_ratio, Ratio::from_id("185"));
+
+ cc = run ("dcpomatic2_create x --container-ratio XXX");
+ BOOST_CHECK (cc.error);
+
+ cc = run ("dcpomatic2_create x --still-length 42");
+ BOOST_CHECK (!cc.error);
+ BOOST_CHECK_EQUAL(cc.still_length.get_value_or(0), 42);
+
+ cc = run ("dcpomatic2_create x --standard SMPTE");
+ BOOST_CHECK (!cc.error);
+ BOOST_REQUIRE(cc._standard);
+ BOOST_CHECK_EQUAL(*cc._standard, dcp::Standard::SMPTE);
+
+ cc = run ("dcpomatic2_create x --standard interop");
+ BOOST_CHECK (!cc.error);
+ BOOST_REQUIRE(cc._standard);
+ BOOST_CHECK_EQUAL(*cc._standard, dcp::Standard::INTEROP);
+
+ cc = run ("dcpomatic2_create x --standard SMPTEX");
+ BOOST_CHECK (cc.error);
+
+ cc = run("dcpomatic2_create x --no-encrypt");
+ BOOST_CHECK(cc._no_encrypt);
+
+ cc = run("dcpomatic2_create x --encrypt");
+ BOOST_CHECK(cc._encrypt);
+
+ cc = run("dcpomatic2_create x --no-encrypt --encrypt");
+ BOOST_CHECK(cc.error);
+
+ cc = run("dcpomatic2_create x --twod");
+ BOOST_CHECK(cc._twod);
+
+ cc = run("dcpomatic2_create x --threed");
+ BOOST_CHECK(cc._threed);
+
+ cc = run("dcpomatic2_create x --twod --threed");
+ BOOST_CHECK(cc.error);
+
+ cc = run ("dcpomatic2_create x --config foo/bar");
+ BOOST_CHECK (!cc.error);
+ BOOST_REQUIRE (cc.config_dir);
+ BOOST_CHECK_EQUAL (*cc.config_dir, "foo/bar");
+
+ cc = run ("dcpomatic2_create x --output fred/jim");
+ BOOST_CHECK (!cc.error);
+ BOOST_REQUIRE (cc.output_dir);
+ BOOST_CHECK_EQUAL (*cc.output_dir, "fred/jim");
+
+ cc = run ("dcpomatic2_create x --outputX fred/jim");
+ BOOST_CHECK (cc.error);
+
+ cc = run ("dcpomatic2_create --config foo/bar --still-length 42 --output flaps fred jim sheila");
+ BOOST_CHECK (!cc.error);
+ BOOST_REQUIRE (cc.config_dir);
+ BOOST_CHECK_EQUAL (*cc.config_dir, "foo/bar");
+ BOOST_CHECK_EQUAL(cc.still_length.get_value_or(0), 42);
+ BOOST_REQUIRE (cc.output_dir);
+ BOOST_CHECK_EQUAL (*cc.output_dir, "flaps");
+ BOOST_REQUIRE_EQUAL (cc.content.size(), 3U);
+ BOOST_CHECK_EQUAL (cc.content[0].path, "fred");
+ BOOST_CHECK_EQUAL (cc.content[0].frame_type, VideoFrameType::TWO_D);
+ BOOST_CHECK_EQUAL (cc.content[1].path, "jim");
+ BOOST_CHECK_EQUAL (cc.content[1].frame_type, VideoFrameType::TWO_D);
+ BOOST_CHECK_EQUAL (cc.content[2].path, "sheila");
+ BOOST_CHECK_EQUAL (cc.content[2].frame_type, VideoFrameType::TWO_D);
+
+ cc = run ("dcpomatic2_create --left-eye left.mp4 --right-eye right.mp4");
+ BOOST_REQUIRE_EQUAL (cc.content.size(), 2U);
+ BOOST_CHECK_EQUAL (cc.content[0].path, "left.mp4");
+ BOOST_CHECK_EQUAL (cc.content[0].frame_type, VideoFrameType::THREE_D_LEFT);
+ BOOST_CHECK_EQUAL (cc.content[1].path, "right.mp4");
+ BOOST_CHECK_EQUAL (cc.content[1].frame_type, VideoFrameType::THREE_D_RIGHT);
+ BOOST_CHECK_EQUAL(cc._fourk, false);
+
+ cc = run ("dcpomatic2_create --colourspace rec1886 test/data/flat_red.png");
+ BOOST_REQUIRE_EQUAL(cc.content.size(), 1U);
+ BOOST_CHECK_EQUAL(cc.content[0].colour_conversion.get_value_or(""), "rec1886");
+ BOOST_CHECK(!cc.error);
+ auto film = cc.make_film(error);
+ BOOST_REQUIRE_EQUAL(film->content().size(), 1U);
+ BOOST_REQUIRE(static_cast<bool>(film->content()[0]->video->colour_conversion()));
+ BOOST_REQUIRE(film->content()[0]->video->colour_conversion() == PresetColourConversion::from_id("rec1886").conversion);
+
+ cc = run ("dcpomatic2_create --colourspace ostrobogulous foo.mp4");
+ BOOST_CHECK_EQUAL(cc.error.get_value_or(""), "dcpomatic2_create: ostrobogulous is not a recognised colourspace");
+
+ cc = run ("dcpomatic2_create --twok foo.mp4");
+ BOOST_REQUIRE_EQUAL (cc.content.size(), 1U);
+ BOOST_CHECK_EQUAL (cc.content[0].path, "foo.mp4");
+ BOOST_CHECK_EQUAL(cc._twok, true);
+ BOOST_CHECK (!cc.error);
+
+ cc = run ("dcpomatic2_create --fourk foo.mp4");
+ BOOST_REQUIRE_EQUAL (cc.content.size(), 1U);
+ BOOST_CHECK_EQUAL (cc.content[0].path, "foo.mp4");
+ BOOST_CHECK_EQUAL(cc._fourk, true);
+ BOOST_CHECK (!cc.error);
+
+ cc = run("dcpomatic2_create --auto-crop foo.mp4 bar.mp4 --auto-crop baz.mp4");
+ BOOST_REQUIRE_EQUAL(cc.content.size(), 3U);
+ BOOST_CHECK(cc.content[0].auto_crop);
+ BOOST_CHECK(!cc.content[1].auto_crop);
+ BOOST_CHECK(cc.content[2].auto_crop);
+
+ cc = run("dcpomatic2_create --auto-crop foo.mp4 bar.mp4 --auto-crop baz.mp4");
+ BOOST_REQUIRE_EQUAL(cc.content.size(), 3U);
+ BOOST_CHECK(cc.content[0].auto_crop);
+ BOOST_CHECK(!cc.content[1].auto_crop);
+ BOOST_CHECK(cc.content[2].auto_crop);
+
+ cc = run("dcpomatic2_create --auto-crop-threshold 42 --auto-crop foo.mp4 bar.mp4 --auto-crop baz.mp4");
+ BOOST_REQUIRE_EQUAL(cc.content.size(), 3U);
+ BOOST_CHECK(cc.content[0].auto_crop);
+ BOOST_CHECK(!cc.content[1].auto_crop);
+ BOOST_CHECK(cc.content[2].auto_crop);
+ BOOST_CHECK_EQUAL(cc.auto_crop_threshold.get_value_or(0), 42);
+
+ auto pillarbox = TestPaths::private_data() / "pillarbox.png";
+ cc = run("dcpomatic2_create --auto-crop " + pillarbox.string());
+ film = cc.make_film(error);
+ BOOST_CHECK_EQUAL(film->content().size(), 1U);
+ BOOST_CHECK(film->content()[0]->video->actual_crop() == Crop(113, 262, 0, 0));
+ BOOST_CHECK_EQUAL(collected_error, fmt::format("Cropped {} to 113 left, 262 right, 0 top and 0 bottom", pillarbox.string()));
+ collected_error = "";
+
+ cc = run ("dcpomatic2_create --video-bit-rate 120 foo.mp4");
+ BOOST_REQUIRE_EQUAL (cc.content.size(), 1U);
+ BOOST_CHECK_EQUAL (cc.content[0].path, "foo.mp4");
+ BOOST_REQUIRE(cc._video_bit_rate);
+ BOOST_CHECK_EQUAL(*cc._video_bit_rate, 120000000);
+ BOOST_CHECK (!cc.error);
+
+ cc = run ("dcpomatic2_create --channel L test/data/L.wav --channel R test/data/R.wav test/data/Lfe.wav");
+ BOOST_REQUIRE_EQUAL (cc.content.size(), 3U);
+ BOOST_CHECK_EQUAL (cc.content[0].path, "test/data/L.wav");
+ BOOST_CHECK (cc.content[0].channel);
+ BOOST_CHECK (*cc.content[0].channel == dcp::Channel::LEFT);
+ BOOST_CHECK_EQUAL (cc.content[1].path, "test/data/R.wav");
+ BOOST_CHECK (cc.content[1].channel);
+ BOOST_CHECK (*cc.content[1].channel == dcp::Channel::RIGHT);
+ BOOST_CHECK_EQUAL (cc.content[2].path, "test/data/Lfe.wav");
+ BOOST_CHECK (!cc.content[2].channel);
+ film = cc.make_film(error);
+ BOOST_CHECK_EQUAL(film->audio_channels(), 6);
+ BOOST_CHECK(collected_error.empty());
+
+ cc = run ("dcpomatic2_create --channel foo fred.wav");
+ BOOST_REQUIRE (cc.error);
+ BOOST_CHECK (boost::algorithm::starts_with(*cc.error, "dcpomatic2_create: foo is not valid for --channel"));
+
+ cc = run ("dcpomatic2_create fred.wav --gain -6 jim.wav --gain 2 sheila.wav");
+ BOOST_REQUIRE_EQUAL (cc.content.size(), 3U);
+ BOOST_CHECK_EQUAL (cc.content[0].path, "fred.wav");
+ BOOST_CHECK (!cc.content[0].gain);
+ BOOST_CHECK_EQUAL (cc.content[1].path, "jim.wav");
+ BOOST_CHECK_CLOSE (*cc.content[1].gain, -6, 0.001);
+ BOOST_CHECK_EQUAL (cc.content[2].path, "sheila.wav");
+ BOOST_CHECK_CLOSE (*cc.content[2].gain, 2, 0.001);
+
+ cc = run("dcpomatic2_create --cpl 123456-789-0 dcp");
+ BOOST_REQUIRE_EQUAL(cc.content.size(), 1U);
+ BOOST_CHECK_EQUAL(cc.content[0].path, "dcp");
+ BOOST_REQUIRE(static_cast<bool>(cc.content[0].cpl));
+ BOOST_CHECK_EQUAL(*cc.content[0].cpl, "123456-789-0");
+
+ cc = run("dcpomatic2_create -s SMPTE sheila.wav");
+ BOOST_CHECK(!cc.still_length);
+ BOOST_CHECK(cc.error);
+
+ cc = run("dcpomatic2_create --channel L fred.wav --channel R jim.wav --channel C sheila.wav --audio-channels 2");
+ BOOST_REQUIRE(cc.error);
+ BOOST_CHECK_EQUAL(*cc.error, "dcpomatic2_create: cannot map audio as requested with only 2 channels");
+
+ cc = run("dcpomatic2_create --channel L fred.wav --channel R jim.wav --channel C sheila.wav --audio-channels 3");
+ BOOST_REQUIRE(cc.error);
+ BOOST_CHECK_EQUAL(*cc.error, "dcpomatic2_create: audio channel count must be even");
+
+ cc = run("dcpomatic2_create --channel L test/data/L.wav --channel R test/data/R.wav --channel C test/data/C.wav");
+ BOOST_CHECK(!cc.error);
+ film = cc.make_film(error);
+ BOOST_CHECK_EQUAL(film->audio_channels(), 6);
+ BOOST_CHECK(collected_error.empty());
+
+ cc = run("dcpomatic2_create --channel L test/data/L.wav --channel R test/data/R.wav --channel HI test/data/sine_440.wav");
+ BOOST_CHECK(!cc.error);
+ film = cc.make_film(error);
+ BOOST_CHECK_EQUAL(film->audio_channels(), 8);
+ BOOST_CHECK(collected_error.empty());
+
+ cc = run("dcpomatic2_create --channel L test/data/L.wav --channel R test/data/R.wav --channel C test/data/C.wav --audio-channels 16");
+ BOOST_CHECK(!cc.error);
+ film = cc.make_film(error);
+ BOOST_CHECK_EQUAL(film->audio_channels(), 16);
+ BOOST_CHECK(collected_error.empty());
+}
+
+
+BOOST_AUTO_TEST_CASE(create_cli_template_test)
+{
+ ConfigRestorer cr("test/data");
+
+ string collected_error;
+ auto error = [&collected_error](string s) {
+ collected_error += s;
+ };
+
+ auto cc = run("dcpomatic2_create test/data/flat_red.png");
+ auto film = cc.make_film(error);
+ BOOST_CHECK(!film->three_d());
+ BOOST_CHECK(collected_error.empty());
+
+ cc = run("dcpomatic2_create test/data/flat_red.png --template 2d");
+ film = cc.make_film(error);
+ BOOST_CHECK(!film->three_d());
+ BOOST_CHECK(collected_error.empty());
+
+ cc = run("dcpomatic2_create test/data/flat_red.png --template 2d --threed");
+ film = cc.make_film(error);
+ BOOST_CHECK(film->three_d());
+ BOOST_CHECK(collected_error.empty());
+
+ cc = run("dcpomatic2_create test/data/flat_red.png --template 3d");
+ film = cc.make_film(error);
+ BOOST_CHECK(film->three_d());
+ BOOST_CHECK(collected_error.empty());
+
+ cc = run("dcpomatic2_create test/data/flat_red.png --template 3d --twod");
+ film = cc.make_film(error);
+ BOOST_CHECK(!film->three_d());
+ BOOST_CHECK(collected_error.empty());
+
+ cc = run("dcpomatic2_create test/data/flat_red.png");
+ film = cc.make_film(error);
+ BOOST_CHECK(!film->encrypted());
+ BOOST_CHECK(collected_error.empty());
+
+ cc = run("dcpomatic2_create test/data/flat_red.png --template unencrypted");
+ film = cc.make_film(error);
+ BOOST_CHECK(!film->encrypted());
+ BOOST_CHECK(collected_error.empty());
+
+ cc = run("dcpomatic2_create test/data/flat_red.png --template unencrypted --encrypt");
+ film = cc.make_film(error);
+ BOOST_CHECK(film->encrypted());
+ BOOST_CHECK(collected_error.empty());
+
+ cc = run("dcpomatic2_create test/data/flat_red.png --template encrypted");
+ film = cc.make_film(error);
+ BOOST_CHECK(film->encrypted());
+ BOOST_CHECK(collected_error.empty());
+
+ cc = run("dcpomatic2_create test/data/flat_red.png --template encrypted --no-encrypt");
+ film = cc.make_film(error);
+ BOOST_CHECK(!film->encrypted());
+ BOOST_CHECK(collected_error.empty());
+
+ cc = run("dcpomatic2_create test/data/flat_red.png");
+ film = cc.make_film(error);
+ BOOST_CHECK(!film->interop());
+ BOOST_CHECK(collected_error.empty());
+
+ cc = run("dcpomatic2_create test/data/flat_red.png --template interop");
+ film = cc.make_film(error);
+ BOOST_CHECK(film->interop());
+ BOOST_CHECK(collected_error.empty());
+
+ cc = run("dcpomatic2_create test/data/flat_red.png --template interop --standard SMPTE");
+ film = cc.make_film(error);
+ BOOST_CHECK(!film->interop());
+ BOOST_CHECK(collected_error.empty());
+
+ cc = run("dcpomatic2_create test/data/flat_red.png --template smpte");
+ film = cc.make_film(error);
+ BOOST_CHECK(!film->interop());
+ BOOST_CHECK(collected_error.empty());
+
+ cc = run("dcpomatic2_create test/data/flat_red.png --template smpte --standard interop");
+ film = cc.make_film(error);
+ BOOST_CHECK(film->interop());
+ BOOST_CHECK(collected_error.empty());
+}
diff --git a/test/lib/dcp_decoder_test.cc b/test/lib/dcp_decoder_test.cc
new file mode 100644
index 000000000..e772f4706
--- /dev/null
+++ b/test/lib/dcp_decoder_test.cc
@@ -0,0 +1,133 @@
+/*
+ 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/>.
+
+*/
+
+
+/** @file test/dcp_decoder_test.cc
+ * @brief Test DCPDecoder class.
+ * @ingroup selfcontained
+ */
+
+
+#include "lib/config.h"
+#include "lib/content_factory.h"
+#include "lib/dcp_content.h"
+#include "lib/dcp_decoder.h"
+#include "lib/examine_content_job.h"
+#include "lib/film.h"
+#include "lib/job_manager.h"
+#include "lib/piece.h"
+#include "lib/player.h"
+#include "../test.h"
+#include <dcp/cpl.h>
+#include <dcp/dcp.h>
+#include <boost/test/unit_test.hpp>
+#include <iostream>
+
+
+using std::list;
+using std::make_shared;
+using std::string;
+using std::vector;
+
+
+/* Check that DCPDecoder reuses old data when it should */
+BOOST_AUTO_TEST_CASE (check_reuse_old_data_test)
+{
+ /* Make some DCPs */
+
+ auto ov = new_test_film("check_reuse_old_data_ov", content_factory("test/data/flat_red.png"));
+ make_and_verify_dcp (ov);
+
+ auto ov_content = make_shared<DCPContent>(ov->dir(ov->dcp_name(false)));
+ auto vf = new_test_film("check_reuse_old_data_vf", {ov_content, content_factory("test/data/L.wav")[0]});
+ ov_content->set_reference_video (true);
+ make_and_verify_dcp(vf, {dcp::VerificationNote::Code::EXTERNAL_ASSET}, false);
+
+ auto encrypted = new_test_film("check_reuse_old_data_decrypted");
+ encrypted->examine_and_add_content (content_factory("test/data/flat_red.png")[0]);
+ BOOST_REQUIRE (!wait_for_jobs());
+ encrypted->set_encrypted (true);
+ make_and_verify_dcp (encrypted);
+
+ dcp::DCP encrypted_dcp (encrypted->dir(encrypted->dcp_name()));
+ encrypted_dcp.read ();
+
+ auto signer = Config::instance()->signer_chain();
+ BOOST_REQUIRE(signer->valid());
+
+ auto const decrypted_kdm = encrypted->make_kdm(encrypted_dcp.cpls().front()->file().get(), dcp::LocalTime ("2030-07-21T00:00:00+00:00"), dcp::LocalTime ("2031-07-21T00:00:00+00:00"));
+ auto const kdm = decrypted_kdm.encrypt(signer, Config::instance()->decryption_chain()->leaf(), {}, dcp::Formulation::MODIFIED_TRANSITIONAL_1, true, 0);
+
+ /* Add just the OV to a new project, move it around a bit and check that
+ the _reels get reused.
+ */
+ auto test = new_test_film("check_reuse_old_data_test1");
+ ov_content = make_shared<DCPContent>(ov->dir(ov->dcp_name(false)));
+ test->examine_and_add_content (ov_content);
+ BOOST_REQUIRE (!wait_for_jobs());
+ auto player = make_shared<Player>(test, Image::Alignment::COMPACT, false);
+
+ auto decoder = std::dynamic_pointer_cast<DCPDecoder>(player->_pieces.front()->decoder);
+ BOOST_REQUIRE (decoder);
+ auto reels = decoder->reels();
+
+ ov_content->set_position (test, dcpomatic::DCPTime(96000));
+ decoder = std::dynamic_pointer_cast<DCPDecoder>(player->_pieces.front()->decoder);
+ BOOST_REQUIRE (decoder);
+ BOOST_REQUIRE (reels == decoder->reels());
+
+ /* Add the VF to a new project, then add the OV and check that the
+ _reels did not get reused.
+ */
+ test = new_test_film("check_reuse_old_data_test2");
+ auto vf_content = make_shared<DCPContent>(vf->dir(vf->dcp_name(false)));
+ test->examine_and_add_content (vf_content);
+ BOOST_REQUIRE (!wait_for_jobs());
+ player = make_shared<Player>(test, Image::Alignment::COMPACT, false);
+
+ decoder = std::dynamic_pointer_cast<DCPDecoder>(player->_pieces.front()->decoder);
+ BOOST_REQUIRE (decoder);
+ reels = decoder->reels();
+
+ vf_content->add_ov (ov->dir(ov->dcp_name(false)));
+ JobManager::instance()->add(make_shared<ExamineContentJob>(test, vf_content, false));
+ BOOST_REQUIRE (!wait_for_jobs());
+ decoder = std::dynamic_pointer_cast<DCPDecoder>(player->_pieces.front()->decoder);
+ BOOST_REQUIRE (decoder);
+ BOOST_REQUIRE (reels != decoder->reels());
+
+ /* Add a KDM to an encrypted DCP and check that the _reels did not get reused */
+ test = new_test_film("check_reuse_old_data_test3");
+ auto encrypted_content = make_shared<DCPContent>(encrypted->dir(encrypted->dcp_name(false)));
+ test->examine_and_add_content (encrypted_content);
+ BOOST_REQUIRE (!wait_for_jobs());
+ player = make_shared<Player>(test, Image::Alignment::COMPACT, false);
+
+ decoder = std::dynamic_pointer_cast<DCPDecoder>(player->_pieces.front()->decoder);
+ BOOST_REQUIRE (decoder);
+ reels = decoder->reels();
+
+ encrypted_content->add_kdm (kdm);
+ JobManager::instance()->add(make_shared<ExamineContentJob>(test, encrypted_content, false));
+ BOOST_REQUIRE (!wait_for_jobs());
+ decoder = std::dynamic_pointer_cast<DCPDecoder>(player->_pieces.front()->decoder);
+ BOOST_REQUIRE (decoder);
+ BOOST_REQUIRE (reels != decoder->reels());
+}
diff --git a/test/lib/dcp_digest_file_test.cc b/test/lib/dcp_digest_file_test.cc
new file mode 100644
index 000000000..770d198c1
--- /dev/null
+++ b/test/lib/dcp_digest_file_test.cc
@@ -0,0 +1,101 @@
+/*
+ Copyright (C) 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 "lib/config.h"
+#include "lib/content_factory.h"
+#include "lib/dcp_content.h"
+#include "lib/dcp_digest_file.h"
+#include "lib/film.h"
+#include "../test.h"
+#include <dcp/cpl.h>
+#include <dcp/dcp.h>
+#include <boost/algorithm/string.hpp>
+#include <boost/test/unit_test.hpp>
+
+
+using std::ifstream;
+using std::make_shared;
+using std::string;
+using boost::optional;
+
+
+BOOST_AUTO_TEST_CASE (dcp_digest_file_test)
+{
+ dcp::DCP dcp("test/data/dcp_digest_test_dcp");
+ dcp.read ();
+ BOOST_REQUIRE_EQUAL (dcp.cpls().size(), 1U);
+
+ write_dcp_digest_file ("build/test/digest.xml", dcp.cpls()[0], "e684e49e89182e907dabe5d9b3bd81ba");
+
+ check_xml ("test/data/digest.xml", "build/test/digest.xml", {});
+}
+
+
+BOOST_AUTO_TEST_CASE (dcp_digest_file_test2)
+{
+ auto get_key_from_digest = [](boost::filesystem::path filename) -> optional<string> {
+ ifstream digest(filename.string().c_str());
+ while (digest.good()) {
+ string line;
+ getline (digest, line);
+ boost::algorithm::trim (line);
+ if (boost::starts_with(line, "<Key>") && line.length() > 37) {
+ return line.substr(5, 32);
+ }
+ }
+
+ return {};
+ };
+
+ auto red = content_factory("test/data/flat_red.png");
+ auto ov = new_test_film("dcp_digest_file_test2_ov", red);
+ ov->set_encrypted (true);
+ make_and_verify_dcp (ov);
+
+ auto ov_key_check = get_key_from_digest ("build/test/dcp_digest_file_test2_ov/" + ov->dcp_name() + ".dcpdig");
+ BOOST_REQUIRE (static_cast<bool>(ov_key_check));
+ BOOST_CHECK_EQUAL (*ov_key_check, ov->key().hex());
+
+ dcp::DCP find_cpl (ov->dir(ov->dcp_name()));
+ find_cpl.read ();
+ BOOST_REQUIRE (!find_cpl.cpls().empty());
+ auto ov_cpl = find_cpl.cpls()[0]->file();
+ BOOST_REQUIRE (static_cast<bool>(ov_cpl));
+
+ auto signer = Config::instance()->signer_chain();
+ BOOST_REQUIRE(signer->valid());
+
+ auto decrypted_kdm = ov->make_kdm(ov_cpl.get(), dcp::LocalTime(), dcp::LocalTime());
+ auto kdm = decrypted_kdm.encrypt(signer, Config::instance()->decryption_chain()->leaf(), {}, dcp::Formulation::MODIFIED_TRANSITIONAL_1, true, 0);
+
+ auto ov_dcp = make_shared<DCPContent>(ov->dir(ov->dcp_name()));
+ ov_dcp->add_kdm (kdm);
+ ov_dcp->set_reference_video (true);
+ ov_dcp->set_reference_audio (true);
+ auto vf = new_test_film("dcp_digest_file_test2_vf", { ov_dcp });
+ vf->set_encrypted (true);
+ make_and_verify_dcp(vf, {dcp::VerificationNote::Code::EXTERNAL_ASSET}, false);
+
+ auto vf_key_check = get_key_from_digest ("build/test/dcp_digest_file_test2_vf/" + vf->dcp_name() + ".dcpdig");
+ BOOST_REQUIRE (static_cast<bool>(vf_key_check));
+ BOOST_CHECK_EQUAL (*vf_key_check, ov->key().hex());
+}
+
diff --git a/test/lib/dcp_examiner_test.cc b/test/lib/dcp_examiner_test.cc
new file mode 100644
index 000000000..d8fc81d5e
--- /dev/null
+++ b/test/lib/dcp_examiner_test.cc
@@ -0,0 +1,52 @@
+/*
+ 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 "lib/content_factory.h"
+#include "lib/dcp_content.h"
+#include "lib/dcp_examiner.h"
+#include "lib/film.h"
+#include "lib/ratio.h"
+#include "../test.h"
+#include <boost/test/unit_test.hpp>
+
+
+using std::make_shared;
+
+
+BOOST_AUTO_TEST_CASE(check_examine_vfs)
+{
+ auto image = content_factory("test/data/scope_red.png")[0];
+ auto ov = new_test_film("check_examine_vfs_ov", { image });
+ ov->set_container(Ratio::from_id("239"));
+ make_and_verify_dcp(ov);
+
+ auto ov_dcp = make_shared<DCPContent>(ov->dir(ov->dcp_name()));
+ auto second_reel = content_factory("test/data/scope_red.png")[0];
+ auto vf = new_test_film("check_examine_vfs_vf", { ov_dcp, second_reel });
+ vf->set_container(Ratio::from_id("239"));
+ vf->set_reel_type(ReelType::BY_VIDEO_CONTENT);
+ ov_dcp->set_reference_video(true);
+ make_and_verify_dcp(vf, { dcp::VerificationNote::Code::EXTERNAL_ASSET }, false);
+
+ auto vf_dcp = make_shared<DCPContent>(vf->dir(vf->dcp_name()));
+ DCPExaminer examiner(vf_dcp, false);
+}
+
diff --git a/test/lib/dcp_metadata_test.cc b/test/lib/dcp_metadata_test.cc
new file mode 100644
index 000000000..05b648bc1
--- /dev/null
+++ b/test/lib/dcp_metadata_test.cc
@@ -0,0 +1,71 @@
+/*
+ Copyright (C) 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 "lib/config.h"
+#include "lib/content_factory.h"
+#include "lib/film.h"
+#include "../test.h"
+#include <dcp/cpl.h>
+#include <dcp/dcp.h>
+#include <boost/test/unit_test.hpp>
+
+
+BOOST_AUTO_TEST_CASE (dcp_metadata_test)
+{
+ auto content = content_factory("test/data/flat_red.png");
+ auto film = new_test_film("dcp_metadata_test", content);
+
+ Config::instance()->set_dcp_creator ("this is the creator");
+ Config::instance()->set_dcp_issuer ("this is the issuer");
+
+ make_and_verify_dcp (
+ film,
+ { dcp::VerificationNote::Code::MISSING_CPL_METADATA }
+ );
+
+ dcp::DCP dcp (film->dir(film->dcp_name()));
+ dcp.read ();
+ auto cpls = dcp.cpls();
+ BOOST_REQUIRE_EQUAL (cpls.size(), 1U);
+
+ BOOST_CHECK_EQUAL (cpls[0]->creator(), "this is the creator");
+ BOOST_CHECK_EQUAL (cpls[0]->issuer(), "this is the issuer");
+}
+
+
+BOOST_AUTO_TEST_CASE(main_picture_active_area_test)
+{
+ auto content = content_factory(TestPaths::private_data() / "bbc405.png");
+ auto film = new_test_film("main_picture_active_area_test", content);
+ film->set_resolution(Resolution::FOUR_K);
+ film->set_interop(false);
+
+ make_and_verify_dcp(film, { dcp::VerificationNote::Code::MISSING_CPL_METADATA });
+
+ dcp::DCP dcp(film->dir(film->dcp_name()));
+ dcp.read();
+ auto cpls = dcp.cpls();
+ BOOST_REQUIRE_EQUAL(cpls.size(), 1U);
+
+ BOOST_REQUIRE(static_cast<bool>(cpls[0]->main_picture_active_area()));
+ BOOST_REQUIRE(cpls[0]->main_picture_active_area() == dcp::Size(2866, 2160));
+}
+
diff --git a/test/lib/dcp_playback_test.cc b/test/lib/dcp_playback_test.cc
new file mode 100644
index 000000000..2ce763911
--- /dev/null
+++ b/test/lib/dcp_playback_test.cc
@@ -0,0 +1,69 @@
+/*
+ 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 "lib/butler.h"
+#include "lib/dcp_content.h"
+#include "lib/film.h"
+#include "lib/player.h"
+#include "../test.h"
+#include <boost/test/unit_test.hpp>
+
+
+using std::make_shared;
+using std::make_shared;
+#if BOOST_VERSION >= 106100
+using namespace boost::placeholders;
+#endif
+using namespace dcpomatic;
+
+
+/** Simulate the work that the player does, for profiling */
+BOOST_AUTO_TEST_CASE (dcp_playback_test)
+{
+ auto content = make_shared<DCPContent>(TestPaths::private_data() / "JourneyToJah_TLR-1_F_EN-DE-FR_CH_51_2K_LOK_20140225_DGL_SMPTE_OV");
+ auto film = new_test_film("dcp_playback_test", { content });
+
+ Player player(film, Image::Alignment::PADDED, false);
+
+ auto butler = std::make_shared<Butler>(
+ film,
+ player,
+ AudioMapping(6, 6),
+ 6,
+ boost::bind(&PlayerVideo::force, AV_PIX_FMT_RGB24),
+ VideoRange::FULL,
+ Image::Alignment::PADDED,
+ true,
+ false,
+ Butler::Audio::ENABLED
+ );
+
+ std::vector<float> audio_buffer(2000 * 6);
+ while (true) {
+ auto p = butler->get_video (Butler::Behaviour::BLOCKING, 0);
+ if (!p.first) {
+ break;
+ }
+ /* assuming DCP is 24fps/48kHz */
+ butler->get_audio (Butler::Behaviour::BLOCKING, audio_buffer.data(), 2000);
+ p.first->image(boost::bind(&PlayerVideo::force, AV_PIX_FMT_RGB24), VideoRange::FULL, true);
+ }
+}
diff --git a/test/lib/dcp_subtitle_test.cc b/test/lib/dcp_subtitle_test.cc
new file mode 100644
index 000000000..7e65b5047
--- /dev/null
+++ b/test/lib/dcp_subtitle_test.cc
@@ -0,0 +1,354 @@
+/*
+ Copyright (C) 2014-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/>.
+
+*/
+
+
+/** @file test/dcp_subtitle_test.cc
+ * @brief Test DCP subtitle content in various ways.
+ * @ingroup feature
+ */
+
+
+#include "lib/content_text.h"
+#include "lib/dcp_content.h"
+#include "lib/dcp_content_type.h"
+#include "lib/dcp_decoder.h"
+#include "lib/dcp_subtitle_content.h"
+#include "lib/dcp_subtitle_decoder.h"
+#include "lib/film.h"
+#include "lib/font.h"
+#include "lib/ratio.h"
+#include "lib/text_content.h"
+#include "lib/text_decoder.h"
+#include "../test.h"
+#include <dcp/mono_j2k_picture_asset.h>
+#include <dcp/openjpeg_image.h>
+#include <dcp/smpte_text_asset.h>
+#include <boost/test/unit_test.hpp>
+#include <iostream>
+
+
+using std::cout;
+using std::list;
+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;
+
+
+optional<ContentStringText> stored;
+
+
+static void
+store (ContentStringText sub)
+{
+ if (!stored) {
+ stored = sub;
+ } else {
+ for (auto i: sub.subs) {
+ stored->subs.push_back (i);
+ }
+ }
+}
+
+
+/** Test pass-through of a very simple DCP subtitle file */
+BOOST_AUTO_TEST_CASE (dcp_subtitle_test)
+{
+ auto content = make_shared<DCPSubtitleContent>("test/data/dcp_sub.xml");
+ auto film = new_test_film("dcp_subtitle_test", { content });
+ film->set_dcp_content_type(DCPContentType::from_isdcf_name("TLR"));
+ film->set_name("frobozz");
+
+ BOOST_CHECK_EQUAL (content->full_length(film).get(), DCPTime::from_seconds(2).get());
+
+ content->only_text()->set_use (true);
+ content->only_text()->set_burn (false);
+ make_and_verify_dcp (
+ film,
+ {
+ dcp::VerificationNote::Code::MISSING_SUBTITLE_LANGUAGE,
+ dcp::VerificationNote::Code::INVALID_SUBTITLE_FIRST_TEXT_TIME,
+ dcp::VerificationNote::Code::MISSING_CPL_METADATA
+ });
+
+ /* This test is concerned with the subtitles, so we'll ignore any
+ * differences in sound between the DCP and the reference to avoid test
+ * failures for unrelated reasons.
+ */
+ check_dcp("test/data/dcp_subtitle_test", film->dir(film->dcp_name()), true);
+}
+
+
+/** Test parsing of a subtitle within an existing DCP */
+BOOST_AUTO_TEST_CASE (dcp_subtitle_within_dcp_test)
+{
+ auto content = make_shared<DCPContent>(TestPaths::private_data() / "JourneyToJah_TLR-1_F_EN-DE-FR_CH_51_2K_LOK_20140225_DGL_SMPTE_OV");
+ auto film = new_test_film("dcp_subtitle_within_dcp_test", { content });
+
+ auto decoder = make_shared<DCPDecoder>(film, content, false, false, shared_ptr<DCPDecoder>());
+ decoder->only_text()->PlainStart.connect (bind (store, _1));
+
+ stored = optional<ContentStringText> ();
+ while (!decoder->pass() && !stored) {}
+
+ BOOST_REQUIRE (stored);
+ BOOST_REQUIRE_EQUAL (stored->subs.size(), 2U);
+ BOOST_CHECK_EQUAL (stored->subs.front().text(), "Noch mal.");
+ BOOST_CHECK_EQUAL (stored->subs.back().text(), "Encore une fois.");
+}
+
+/** Test subtitles whose text includes things like &lt;b&gt; */
+BOOST_AUTO_TEST_CASE (dcp_subtitle_test2)
+{
+ auto content = make_shared<DCPSubtitleContent>("test/data/dcp_sub2.xml");
+ auto film = new_test_film("dcp_subtitle_test2", { content });
+
+ auto decoder = make_shared<DCPSubtitleDecoder>(film, content);
+ decoder->only_text()->PlainStart.connect (bind (store, _1));
+
+ stored = optional<ContentStringText> ();
+ while (!decoder->pass()) {
+ if (stored && stored->from() == ContentTime(0)) {
+ /* Text passed around by the player should be unescaped */
+ BOOST_CHECK_EQUAL(stored->subs.front().text(), "<b>Hello world!</b>");
+ }
+ }
+}
+
+
+/** Test a failure case */
+BOOST_AUTO_TEST_CASE (dcp_subtitle_test3)
+{
+ auto content = make_shared<DCPSubtitleContent>("test/data/dcp_sub3.xml");
+ auto film = new_test_film("dcp_subtitle_test3", { content });
+ film->set_interop (true);
+ content->only_text()->set_language(dcp::LanguageTag("de"));
+
+ make_and_verify_dcp (film, { dcp::VerificationNote::Code::INVALID_STANDARD });
+
+ auto decoder = make_shared<DCPSubtitleDecoder>(film, content);
+ stored = optional<ContentStringText> ();
+ while (!decoder->pass ()) {
+ decoder->only_text()->PlainStart.connect (bind (store, _1));
+ if (stored && stored->from() == ContentTime::from_seconds(0.08)) {
+ auto s = stored->subs;
+ auto i = s.begin ();
+ BOOST_CHECK_EQUAL (i->text(), "This");
+ ++i;
+ BOOST_REQUIRE (i != s.end ());
+ BOOST_CHECK_EQUAL (i->text(), " is ");
+ ++i;
+ BOOST_REQUIRE (i != s.end ());
+ BOOST_CHECK_EQUAL (i->text(), "wrong.");
+ ++i;
+ BOOST_REQUIRE (i == s.end ());
+ }
+ }
+}
+
+
+/** Check that Interop DCPs aren't made with more than one <LoadFont> (#1273) */
+BOOST_AUTO_TEST_CASE (dcp_subtitle_test4)
+{
+ auto content = make_shared<DCPSubtitleContent>("test/data/dcp_sub3.xml");
+ auto content2 = make_shared<DCPSubtitleContent>("test/data/dcp_sub3.xml");
+ auto film = new_test_film("dcp_subtitle_test4", {content, content2});
+ film->set_interop (true);
+
+ content->only_text()->add_font(make_shared<Font>("font1"));
+ content2->only_text()->add_font(make_shared<Font>("font2"));
+ content->only_text()->set_language(dcp::LanguageTag("de"));
+ content2->only_text()->set_language(dcp::LanguageTag("de"));
+
+ make_and_verify_dcp (film, { dcp::VerificationNote::Code::INVALID_STANDARD });
+
+ cxml::Document doc ("DCSubtitle");
+ doc.read_file (subtitle_file (film));
+ BOOST_REQUIRE_EQUAL (doc.node_children("LoadFont").size(), 1U);
+}
+
+
+static
+void
+check_font_tags (vector<cxml::NodePtr> nodes)
+{
+ for (auto i: nodes) {
+ if (i->name() == "Font") {
+ BOOST_CHECK (!i->optional_string_attribute("Id") || i->string_attribute("Id") != "");
+ }
+ check_font_tags (i->node_children());
+ }
+}
+
+
+/** Check that imported <LoadFont> tags with empty IDs (or corresponding Font tags with empty IDs)
+ * are not passed through into the DCP.
+ */
+BOOST_AUTO_TEST_CASE (dcp_subtitle_test5)
+{
+ auto content = make_shared<DCPSubtitleContent>("test/data/dcp_sub6.xml");
+ auto film = new_test_film("dcp_subtitle_test5", {content});
+ film->set_interop (true);
+ content->only_text()->set_language(dcp::LanguageTag("de"));
+
+ make_and_verify_dcp (film, { dcp::VerificationNote::Code::INVALID_STANDARD });
+
+ cxml::Document doc ("DCSubtitle");
+ doc.read_file (subtitle_file(film));
+ BOOST_REQUIRE_EQUAL (doc.node_children("LoadFont").size(), 1U);
+ BOOST_CHECK (doc.node_children("LoadFont").front()->string_attribute("Id") != "");
+
+ check_font_tags (doc.node_children());
+}
+
+
+/** Check that fonts specified in the DoM content are used in the output and not ignored (#2074) */
+BOOST_AUTO_TEST_CASE (test_font_override)
+{
+ auto content = make_shared<DCPSubtitleContent>("test/data/dcp_sub4.xml");
+ auto film = new_test_film("test_font_override", {content});
+ film->set_interop(true);
+ content->only_text()->set_language(dcp::LanguageTag("de"));
+
+ BOOST_REQUIRE_EQUAL(content->text.size(), 1U);
+ auto font = content->text.front()->get_font("theFontId");
+ BOOST_REQUIRE(font);
+ font->set_file("test/data/Inconsolata-VF.ttf");
+
+ make_and_verify_dcp (film, { dcp::VerificationNote::Code::INVALID_STANDARD });
+ check_file (subtitle_file(film).parent_path() / "font_0.ttf", "test/data/Inconsolata-VF.ttf");
+}
+
+
+BOOST_AUTO_TEST_CASE(entity_from_dcp_source)
+{
+ std::ofstream source_xml("build/test/entity_from_dcp_source.xml");
+ source_xml
+ << "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
+ << "<SubtitleReel xmlns=\"http://www.smpte-ra.org/schemas/428-7/2010/DCST\" xmlns:xs=\"http://www.w3.org/2001/XMLSchema\">\n"
+ << "<Id>urn:uuid:9c0a0a67-ffd8-4c65-8b5a-c6be3ef182c5</Id>\n"
+ << "<ContentTitleText>DCP</ContentTitleText>\n"
+ << "<IssueDate>2022-11-30T18:13:56.000+01:00</IssueDate>\n"
+ << "<ReelNumber>1</ReelNumber>\n"
+ << "<EditRate>24 1</EditRate>\n"
+ << "<TimeCodeRate>24</TimeCodeRate>\n"
+ << "<StartTime>00:00:00:00</StartTime>\n"
+ << "<LoadFont ID=\"font\">urn:uuid:899e5c59-50f6-467b-985b-8282c020e1ee</LoadFont>\n"
+ << "<SubtitleList>\n"
+ << "<Font AspectAdjust=\"1.0\" Color=\"FFFFFFFF\" Effect=\"none\" EffectColor=\"FF000000\" ID=\"font\" Italic=\"no\" Script=\"normal\" Size=\"48\" Underline=\"no\" Weight=\"normal\">\n"
+ << "<Subtitle SpotNumber=\"1\" TimeIn=\"00:00:00:00\" TimeOut=\"00:00:10:00\" FadeUpTime=\"00:00:00:00\" FadeDownTime=\"00:00:00:00\">\n"
+ << "<Text Valign=\"top\" Vposition=\"82.7273\">Hello &amp; world</Text>\n"
+ << "</Subtitle>\n"
+ << "</Font>\n"
+ << "</SubtitleList>\n"
+ << "</SubtitleReel>\n";
+ source_xml.close();
+
+ auto content = make_shared<DCPSubtitleContent>("build/test/entity_from_dcp_source.xml");
+ auto film = new_test_film("entity_from_dcp_source", { content });
+ film->set_interop(false);
+ content->only_text()->set_use(true);
+ content->only_text()->set_burn(false);
+ make_and_verify_dcp (
+ film,
+ {
+ dcp::VerificationNote::Code::MISSING_SUBTITLE_LANGUAGE,
+ dcp::VerificationNote::Code::INVALID_SUBTITLE_FIRST_TEXT_TIME,
+ dcp::VerificationNote::Code::MISSING_CPL_METADATA,
+ dcp::VerificationNote::Code::INVALID_SUBTITLE_DURATION_BV21,
+ dcp::VerificationNote::Code::INVALID_SUBTITLE_SPACING,
+ });
+
+ dcp::SMPTETextAsset check(dcp_file(film, "sub_"));
+ auto subs = check.texts();
+ BOOST_REQUIRE_EQUAL(subs.size(), 1U);
+ auto sub = std::dynamic_pointer_cast<const dcp::TextString>(subs[0]);
+ BOOST_REQUIRE(sub);
+ /* dcp::TextAsset gets the text from the XML with get_content(), which
+ * resolves the 5 predefined entities & " < > ' so we shouldn't see any
+ * entity here.
+ */
+ BOOST_CHECK_EQUAL(sub->text(), "Hello & world");
+
+ /* It should be escaped in the raw XML though */
+ BOOST_REQUIRE(static_cast<bool>(check.raw_xml()));
+ BOOST_CHECK(check.raw_xml()->find("Hello &amp; world") != std::string::npos);
+
+ /* Remake with burn */
+ content->only_text()->set_burn(true);
+ boost::filesystem::remove_all(film->dir(film->dcp_name()));
+ make_and_verify_dcp (
+ film,
+ {
+ dcp::VerificationNote::Code::MISSING_SUBTITLE_LANGUAGE,
+ dcp::VerificationNote::Code::INVALID_SUBTITLE_FIRST_TEXT_TIME,
+ dcp::VerificationNote::Code::MISSING_CPL_METADATA,
+ dcp::VerificationNote::Code::INVALID_SUBTITLE_DURATION_BV21,
+ dcp::VerificationNote::Code::INVALID_SUBTITLE_SPACING,
+ });
+
+ dcp::MonoJ2KPictureAsset burnt(dcp_file(film, "j2c_"));
+ auto frame = burnt.start_read()->get_frame(12)->xyz_image();
+ auto const size = frame->size();
+ int max_X = 0;
+ for (auto y = 0; y < size.height; ++y) {
+ for (auto x = 0; x < size.width; ++x) {
+ max_X = std::max(frame->data(0)[x + y * size.width], max_X);
+ }
+ }
+
+ /* Check that the subtitle got rendered to the image; if the escaping of the & is wrong Pango
+ * will throw errors and nothing will be rendered.
+ */
+ BOOST_CHECK(max_X > 100);
+}
+
+
+BOOST_AUTO_TEST_CASE(dcp_subtitle_trim_test)
+{
+ auto content = make_shared<DCPSubtitleContent>("test/data/dcp_sub7.xml");
+ auto film = new_test_film("dcp_subtitle_trim_start_test", { content });
+ content->set_trim_start(film, dcpomatic::ContentTime::from_seconds(10.5));
+ content->set_trim_end(dcpomatic::ContentTime::from_seconds(2.5));
+ content->text[0]->set_language(dcp::LanguageTag("en"));
+
+ make_and_verify_dcp(
+ film,
+ {
+ dcp::VerificationNote::Code::INVALID_SUBTITLE_FIRST_TEXT_TIME,
+ dcp::VerificationNote::Code::MISSING_CPL_METADATA,
+ dcp::VerificationNote::Code::INVALID_SUBTITLE_DURATION_BV21,
+ });
+
+ dcp::SMPTETextAsset asset(find_file(film->dir(film->dcp_name()), "sub_"));
+ auto texts = asset.texts();
+ BOOST_REQUIRE_EQUAL(texts.size(), 9U);
+ BOOST_CHECK(texts[0]->in() == dcp::Time(0, 0, 0, 0, 24));
+ BOOST_CHECK(texts[0]->out() == dcp::Time(0, 0, 0, 12, 24));
+ BOOST_CHECK(texts[8]->in() == dcp::Time(0, 0, 15, 12, 24));
+ BOOST_CHECK(texts[8]->out() == dcp::Time(0, 0, 16, 0, 24));
+}
+
+
+
diff --git a/test/lib/dcpomatic_time_test.cc b/test/lib/dcpomatic_time_test.cc
new file mode 100644
index 000000000..5d23f2478
--- /dev/null
+++ b/test/lib/dcpomatic_time_test.cc
@@ -0,0 +1,352 @@
+/*
+ Copyright (C) 2015-2017 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 test/dcpomatic_time_test.cc
+ * @brief Test dcpomatic::Time and dcpomatic::TimePeriod classes.
+ * @ingroup selfcontained
+ */
+
+#include "lib/dcpomatic_time.h"
+#include "lib/dcpomatic_time_coalesce.h"
+#include <boost/test/unit_test.hpp>
+#include <list>
+#include <iostream>
+
+using std::list;
+using std::cout;
+using namespace dcpomatic;
+
+BOOST_AUTO_TEST_CASE (dcpomatic_time_test)
+{
+ FrameRateChange frc (24, 48);
+ int j = 0;
+ int k = 0;
+ for (int64_t i = 0; i < 62000; i += 2000) {
+ DCPTime d (i);
+ ContentTime c (d, frc);
+ BOOST_CHECK_EQUAL (c.frames_floor (24.0), j);
+ ++k;
+ if (k == 2) {
+ ++j;
+ k = 0;
+ }
+ }
+}
+
+BOOST_AUTO_TEST_CASE (dcpomatic_time_period_overlaps_test)
+{
+ /* Taking times as the start of a sampling interval
+
+ |--|--|--|--|--|--|--|--|--|--|
+ 0 1 2 3 4 5 6 7 8 9 |
+ |--|--|--|--|--|--|--|--|--|--|
+
+ <------a----><----b----->
+
+ and saying `from' is the start of the first sampling
+ interval and `to' is the start of the interval after
+ the period... a and b do not overlap.
+ */
+
+ TimePeriod<DCPTime> a (DCPTime (0), DCPTime (4));
+ TimePeriod<DCPTime> b (DCPTime (4), DCPTime (8));
+ BOOST_CHECK (!a.overlap (b));
+
+ /* Some more obvious non-overlaps */
+ a = TimePeriod<DCPTime> (DCPTime (0), DCPTime (4));
+ b = TimePeriod<DCPTime> (DCPTime (5), DCPTime (8));
+ BOOST_CHECK (!a.overlap (b));
+
+ /* Some overlaps */
+ a = TimePeriod<DCPTime> (DCPTime (0), DCPTime (4));
+ b = TimePeriod<DCPTime> (DCPTime (3), DCPTime (8));
+ BOOST_CHECK (a.overlap(b));
+ BOOST_CHECK (a.overlap(b).get() == DCPTimePeriod(DCPTime(3), DCPTime(4)));
+ a = TimePeriod<DCPTime> (DCPTime (1), DCPTime (9));
+ b = TimePeriod<DCPTime> (DCPTime (0), DCPTime (10));
+ BOOST_CHECK (a.overlap(b));
+ BOOST_CHECK (a.overlap(b).get() == DCPTimePeriod(DCPTime(1), DCPTime(9)));
+}
+
+BOOST_AUTO_TEST_CASE (dcpomatic_time_period_subtract_test1)
+{
+ DCPTimePeriod A (DCPTime (0), DCPTime (106));
+ list<DCPTimePeriod> B;
+ B.push_back (DCPTimePeriod (DCPTime (0), DCPTime (42)));
+ B.push_back (DCPTimePeriod (DCPTime (52), DCPTime (91)));
+ B.push_back (DCPTimePeriod (DCPTime (94), DCPTime (106)));
+ list<DCPTimePeriod> r = subtract (A, B);
+ list<DCPTimePeriod>::const_iterator i = r.begin ();
+ BOOST_REQUIRE (i != r.end ());
+ BOOST_CHECK (i->from == DCPTime (42));
+ BOOST_CHECK (i->to == DCPTime (52));
+ ++i;
+ BOOST_REQUIRE (i != r.end ());
+ BOOST_CHECK (i->from == DCPTime (91));
+ BOOST_CHECK (i->to == DCPTime (94));
+ ++i;
+ BOOST_REQUIRE (i == r.end ());
+}
+
+BOOST_AUTO_TEST_CASE (dcpomatic_time_period_subtract_test2)
+{
+ DCPTimePeriod A (DCPTime (0), DCPTime (106));
+ list<DCPTimePeriod> B;
+ B.push_back (DCPTimePeriod (DCPTime (14), DCPTime (42)));
+ B.push_back (DCPTimePeriod (DCPTime (52), DCPTime (91)));
+ B.push_back (DCPTimePeriod (DCPTime (94), DCPTime (106)));
+ list<DCPTimePeriod> r = subtract (A, B);
+ list<DCPTimePeriod>::const_iterator i = r.begin ();
+ BOOST_REQUIRE (i != r.end ());
+ BOOST_CHECK (i->from == DCPTime (0));
+ BOOST_CHECK (i->to == DCPTime (14));
+ ++i;
+ BOOST_REQUIRE (i != r.end ());
+ BOOST_CHECK (i->from == DCPTime (42));
+ BOOST_CHECK (i->to == DCPTime (52));
+ ++i;
+ BOOST_REQUIRE (i != r.end ());
+ BOOST_CHECK (i->from == DCPTime (91));
+ BOOST_CHECK (i->to == DCPTime (94));
+ ++i;
+ BOOST_REQUIRE (i == r.end ());
+}
+
+BOOST_AUTO_TEST_CASE (dcpomatic_time_period_subtract_test3)
+{
+ DCPTimePeriod A (DCPTime (0), DCPTime (106));
+ list<DCPTimePeriod> B;
+ B.push_back (DCPTimePeriod (DCPTime (14), DCPTime (42)));
+ B.push_back (DCPTimePeriod (DCPTime (52), DCPTime (91)));
+ B.push_back (DCPTimePeriod (DCPTime (94), DCPTime (99)));
+ list<DCPTimePeriod> r = subtract (A, B);
+ list<DCPTimePeriod>::const_iterator i = r.begin ();
+ BOOST_REQUIRE (i != r.end ());
+ BOOST_CHECK (i->from == DCPTime (0));
+ BOOST_CHECK (i->to == DCPTime (14));
+ ++i;
+ BOOST_REQUIRE (i != r.end ());
+ BOOST_CHECK (i->from == DCPTime (42));
+ BOOST_CHECK (i->to == DCPTime (52));
+ ++i;
+ BOOST_REQUIRE (i != r.end ());
+ BOOST_CHECK (i->from == DCPTime (91));
+ BOOST_CHECK (i->to == DCPTime (94));
+ ++i;
+ BOOST_REQUIRE (i != r.end ());
+ BOOST_CHECK (i->from == DCPTime (99));
+ BOOST_CHECK (i->to == DCPTime (106));
+ ++i;
+ BOOST_REQUIRE (i == r.end ());
+}
+
+BOOST_AUTO_TEST_CASE (dcpomatic_time_period_subtract_test4)
+{
+ DCPTimePeriod A (DCPTime (0), DCPTime (106));
+ list<DCPTimePeriod> B;
+ list<DCPTimePeriod> r = subtract (A, B);
+ list<DCPTimePeriod>::const_iterator i = r.begin ();
+ BOOST_REQUIRE (i != r.end ());
+ BOOST_CHECK (i->from == DCPTime (0));
+ BOOST_CHECK (i->to == DCPTime (106));
+ ++i;
+ BOOST_REQUIRE (i == r.end ());
+}
+
+BOOST_AUTO_TEST_CASE (dcpomatic_time_period_subtract_test5)
+{
+ DCPTimePeriod A (DCPTime (0), DCPTime (106));
+ list<DCPTimePeriod> B;
+ B.push_back (DCPTimePeriod (DCPTime (14), DCPTime (42)));
+ B.push_back (DCPTimePeriod (DCPTime (42), DCPTime (91)));
+ B.push_back (DCPTimePeriod (DCPTime (94), DCPTime (99)));
+ list<DCPTimePeriod> r = subtract (A, B);
+ list<DCPTimePeriod>::const_iterator i = r.begin ();
+ BOOST_REQUIRE (i != r.end ());
+ BOOST_CHECK (i->from == DCPTime (0));
+ BOOST_CHECK (i->to == DCPTime (14));
+ ++i;
+ BOOST_REQUIRE (i != r.end ());
+ BOOST_CHECK (i->from ==DCPTime (91));
+ BOOST_CHECK (i->to == DCPTime (94));
+ ++i;
+ BOOST_REQUIRE (i != r.end ());
+ BOOST_CHECK (i->from == DCPTime (99));
+ BOOST_CHECK (i->to == DCPTime (106));
+ ++i;
+ BOOST_REQUIRE (i == r.end ());
+}
+
+BOOST_AUTO_TEST_CASE (dcpomatic_time_period_subtract_test6)
+{
+ DCPTimePeriod A (DCPTime (0), DCPTime (106));
+ list<DCPTimePeriod> B;
+ B.push_back (DCPTimePeriod (DCPTime (0), DCPTime (42)));
+ B.push_back (DCPTimePeriod (DCPTime (42), DCPTime (91)));
+ B.push_back (DCPTimePeriod (DCPTime (91), DCPTime (106)));
+ list<DCPTimePeriod> r = subtract (A, B);
+ BOOST_CHECK (r.empty());
+}
+
+BOOST_AUTO_TEST_CASE (dcpomatic_time_period_subtract_test7)
+{
+ DCPTimePeriod A (DCPTime (228), DCPTime (356));
+ list<DCPTimePeriod> B;
+ B.push_back (DCPTimePeriod (DCPTime (34), DCPTime (162)));
+ list<DCPTimePeriod> r = subtract (A, B);
+ list<DCPTimePeriod>::const_iterator i = r.begin ();
+ BOOST_REQUIRE (i != r.end ());
+ BOOST_CHECK (i->from == DCPTime (228));
+ BOOST_CHECK (i->to == DCPTime (356));
+ ++i;
+ BOOST_REQUIRE (i == r.end ());
+}
+
+BOOST_AUTO_TEST_CASE (dcpomatic_time_period_subtract_test8)
+{
+ DCPTimePeriod A (DCPTime(0), DCPTime(32000));
+ list<DCPTimePeriod> B;
+ B.push_back (DCPTimePeriod (DCPTime(8000), DCPTime(20000)));
+ B.push_back (DCPTimePeriod (DCPTime(28000), DCPTime(32000)));
+ list<DCPTimePeriod> r = subtract (A, B);
+ list<DCPTimePeriod>::const_iterator i = r.begin ();
+ BOOST_REQUIRE (i != r.end ());
+ BOOST_CHECK (*i == DCPTimePeriod(DCPTime(0), DCPTime(8000)));
+ ++i;
+ BOOST_REQUIRE (i != r.end ());
+ BOOST_CHECK (*i == DCPTimePeriod(DCPTime(20000), DCPTime(28000)));
+ ++i;
+ BOOST_REQUIRE (i == r.end ());
+}
+
+BOOST_AUTO_TEST_CASE (dcpomatic_time_period_coalesce_test1)
+{
+ DCPTimePeriod A (DCPTime(14), DCPTime(29));
+ DCPTimePeriod B (DCPTime(45), DCPTime(91));
+ list<DCPTimePeriod> p;
+ p.push_back (A);
+ p.push_back (B);
+ list<DCPTimePeriod> q = coalesce (p);
+ BOOST_REQUIRE_EQUAL (q.size(), 2U);
+ BOOST_CHECK (q.front() == DCPTimePeriod(DCPTime(14), DCPTime(29)));
+ BOOST_CHECK (q.back () == DCPTimePeriod(DCPTime(45), DCPTime(91)));
+}
+
+BOOST_AUTO_TEST_CASE (dcpomatic_time_period_coalesce_test2)
+{
+ DCPTimePeriod A (DCPTime(14), DCPTime(29));
+ DCPTimePeriod B (DCPTime(26), DCPTime(91));
+ list<DCPTimePeriod> p;
+ p.push_back (A);
+ p.push_back (B);
+ list<DCPTimePeriod> q = coalesce (p);
+ BOOST_REQUIRE_EQUAL (q.size(), 1U);
+ BOOST_CHECK (q.front() == DCPTimePeriod(DCPTime(14), DCPTime(91)));
+}
+
+BOOST_AUTO_TEST_CASE (dcpomatic_time_period_coalesce_test3)
+{
+ DCPTimePeriod A (DCPTime(14), DCPTime(29));
+ DCPTimePeriod B (DCPTime(29), DCPTime(91));
+ list<DCPTimePeriod> p;
+ p.push_back (A);
+ p.push_back (B);
+ list<DCPTimePeriod> q = coalesce (p);
+ BOOST_REQUIRE_EQUAL (q.size(), 1U);
+ BOOST_CHECK (q.front() == DCPTimePeriod(DCPTime(14), DCPTime(91)));
+}
+
+BOOST_AUTO_TEST_CASE (dcpomatic_time_period_coalesce_test4)
+{
+ DCPTimePeriod A (DCPTime(14), DCPTime(29));
+ DCPTimePeriod B (DCPTime(20), DCPTime(91));
+ DCPTimePeriod C (DCPTime(35), DCPTime(106));
+ list<DCPTimePeriod> p;
+ p.push_back (A);
+ p.push_back (B);
+ p.push_back (C);
+ list<DCPTimePeriod> q = coalesce (p);
+ BOOST_REQUIRE_EQUAL (q.size(), 1U);
+ BOOST_CHECK (q.front() == DCPTimePeriod(DCPTime(14), DCPTime(106)));
+}
+
+BOOST_AUTO_TEST_CASE (dcpomatic_time_period_coalesce_test5)
+{
+ DCPTimePeriod A (DCPTime(14), DCPTime(29));
+ DCPTimePeriod B (DCPTime(20), DCPTime(91));
+ DCPTimePeriod C (DCPTime(100), DCPTime(106));
+ list<DCPTimePeriod> p;
+ p.push_back (A);
+ p.push_back (B);
+ p.push_back (C);
+ list<DCPTimePeriod> q = coalesce (p);
+ BOOST_REQUIRE_EQUAL (q.size(), 2U);
+ BOOST_CHECK (q.front() == DCPTimePeriod(DCPTime(14), DCPTime(91)));
+ BOOST_CHECK (q.back() == DCPTimePeriod(DCPTime(100), DCPTime(106)));
+}
+
+BOOST_AUTO_TEST_CASE (test_coalesce_with_overlapping_periods)
+{
+ DCPTimePeriod A (DCPTime(0), DCPTime(10));
+ DCPTimePeriod B (DCPTime(2), DCPTime(8));
+ list<DCPTimePeriod> p;
+ p.push_back (A);
+ p.push_back (B);
+ auto q = coalesce(p);
+ BOOST_REQUIRE_EQUAL (q.size(), 1U);
+ BOOST_CHECK (q.front() == DCPTimePeriod(DCPTime(0), DCPTime(10)));
+}
+
+/* Straightforward test of DCPTime::ceil */
+BOOST_AUTO_TEST_CASE (dcpomatic_time_ceil_test)
+{
+ BOOST_CHECK_EQUAL (DCPTime(0).ceil(DCPTime::HZ / 2).get(), 0);
+ BOOST_CHECK_EQUAL (DCPTime(1).ceil(DCPTime::HZ / 2).get(), 2);
+ BOOST_CHECK_EQUAL (DCPTime(2).ceil(DCPTime::HZ / 2).get(), 2);
+ BOOST_CHECK_EQUAL (DCPTime(3).ceil(DCPTime::HZ / 2).get(), 4);
+
+ BOOST_CHECK_EQUAL (DCPTime(0).ceil(DCPTime::HZ / 42).get(), 0);
+ BOOST_CHECK_EQUAL (DCPTime(1).ceil(DCPTime::HZ / 42).get(), 42);
+ BOOST_CHECK_EQUAL (DCPTime(42).ceil(DCPTime::HZ / 42).get(), 42);
+ BOOST_CHECK_EQUAL (DCPTime(43).ceil(DCPTime::HZ / 42).get(), 84);
+
+ /* Check that rounding up to non-integer frame rates works */
+ BOOST_CHECK_EQUAL (DCPTime(45312).ceil(29.976).get(), 48038);
+
+ /* Check another tricky case that used to fail */
+ BOOST_CHECK_EQUAL (DCPTime(212256039).ceil(23.976).get(), 212256256);
+}
+
+/* Straightforward test of DCPTime::floor */
+BOOST_AUTO_TEST_CASE (dcpomatic_time_floor_test)
+{
+ BOOST_CHECK_EQUAL (DCPTime(0).floor(DCPTime::HZ / 2).get(), 0);
+ BOOST_CHECK_EQUAL (DCPTime(1).floor(DCPTime::HZ / 2).get(), 0);
+ BOOST_CHECK_EQUAL (DCPTime(2).floor(DCPTime::HZ / 2).get(), 2);
+ BOOST_CHECK_EQUAL (DCPTime(3).floor(DCPTime::HZ / 2).get(), 2);
+
+ BOOST_CHECK_EQUAL (DCPTime(0).floor(DCPTime::HZ / 42).get(), 0);
+ BOOST_CHECK_EQUAL (DCPTime(1).floor(DCPTime::HZ / 42).get(), 0);
+ BOOST_CHECK_EQUAL (DCPTime(42).floor(DCPTime::HZ / 42.0).get(), 42);
+ BOOST_CHECK_EQUAL (DCPTime(43).floor(DCPTime::HZ / 42.0).get(), 42);
+
+ /* Check that rounding down to non-integer frame rates works */
+ BOOST_CHECK_EQUAL (DCPTime(45312).floor(29.976).get(), 44836);
+}
diff --git a/test/lib/digest_test.cc b/test/lib/digest_test.cc
new file mode 100644
index 000000000..6ee829000
--- /dev/null
+++ b/test/lib/digest_test.cc
@@ -0,0 +1,99 @@
+/*
+ Copyright (C) 2016-2021 Carl Hetherington <cth@carlh.net>
+
+ This file is part of DCP-o-matic.
+
+ DCP-o-matic is free software; you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation; either version 2 of the License, or
+ (at your option) any later version.
+
+ DCP-o-matic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with DCP-o-matic. If not, see <http://www.gnu.org/licenses/>.
+
+*/
+
+
+/** @file test/digest_test.cc
+ * @brief Check computed DCP digests against references calculated by the `openssl` binary.
+ * @ingroup feature
+ */
+
+
+#include "lib/compose.hpp"
+#include "lib/config.h"
+#include "lib/dcp_content_type.h"
+#include "lib/film.h"
+#include "lib/image_content.h"
+#include "../test.h"
+#include <dcp/cpl.h>
+#include <dcp/reel.h>
+#include <dcp/reel_picture_asset.h>
+#include <boost/test/unit_test.hpp>
+
+
+using std::list;
+using std::make_shared;
+using std::string;
+
+
+static string
+openssl_hash (boost::filesystem::path file)
+{
+ auto pipe = popen (String::compose ("openssl sha1 -binary %1 | openssl base64 -e", file.string()).c_str (), "r");
+ BOOST_REQUIRE (pipe);
+ char buffer[128];
+ string output;
+ while (!feof (pipe)) {
+ if (fgets (buffer, sizeof(buffer), pipe)) {
+ output += buffer;
+ }
+ }
+ pclose (pipe);
+ if (!output.empty ()) {
+ output = output.substr (0, output.length() - 1);
+ }
+ return output;
+}
+
+
+/** Test the digests made by the DCP writing code on a multi-reel DCP */
+BOOST_AUTO_TEST_CASE (digest_test)
+{
+ auto r = make_shared<ImageContent>("test/data/flat_red.png");
+ auto g = make_shared<ImageContent>("test/data/flat_green.png");
+ auto b = make_shared<ImageContent>("test/data/flat_blue.png");
+ auto film = new_test_film("digest_test", { r, g, b });
+ film->set_reel_type (ReelType::BY_VIDEO_CONTENT);
+
+ BOOST_CHECK (Config::instance()->master_encoding_threads() > 1);
+ make_and_verify_dcp (film);
+
+ dcp::DCP dcp (film->dir (film->dcp_name ()));
+ dcp.read ();
+ BOOST_CHECK_EQUAL (dcp.cpls().size(), 1U);
+ auto reels = dcp.cpls()[0]->reels();
+
+ auto i = reels.begin ();
+ BOOST_REQUIRE (i != reels.end ());
+ BOOST_REQUIRE ((*i)->main_picture()->hash());
+ BOOST_REQUIRE ((*i)->main_picture()->asset()->file());
+ BOOST_CHECK_EQUAL ((*i)->main_picture()->hash().get(), openssl_hash ((*i)->main_picture()->asset()->file().get()));
+ ++i;
+ BOOST_REQUIRE (i != reels.end ());
+ BOOST_REQUIRE ((*i)->main_picture()->hash());
+ BOOST_REQUIRE ((*i)->main_picture()->asset()->file());
+ BOOST_CHECK_EQUAL ((*i)->main_picture()->hash().get(), openssl_hash ((*i)->main_picture()->asset()->file().get()));
+ ++i;
+ BOOST_REQUIRE (i != reels.end ());
+ BOOST_REQUIRE ((*i)->main_picture()->hash());
+ BOOST_REQUIRE ((*i)->main_picture()->asset()->file());
+ BOOST_CHECK_EQUAL ((*i)->main_picture()->hash().get(), openssl_hash ((*i)->main_picture()->asset()->file().get()));
+ ++i;
+ BOOST_REQUIRE (i == reels.end ());
+}
diff --git a/test/lib/disk_writer_test.cc b/test/lib/disk_writer_test.cc
new file mode 100644
index 000000000..e28dc1c56
--- /dev/null
+++ b/test/lib/disk_writer_test.cc
@@ -0,0 +1,231 @@
+/*
+ Copyright (C) 2020-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 "lib/cross.h"
+#include "lib/ext.h"
+#include "lib/io_context.h"
+#include "../test.h"
+#include <boost/algorithm/string.hpp>
+#include <boost/asio.hpp>
+#include <boost/filesystem.hpp>
+#include <boost/process.hpp>
+#include <boost/test/unit_test.hpp>
+#include <sys/stat.h>
+#include <sys/types.h>
+#include <fcntl.h>
+#include <iostream>
+#include <future>
+#include <regex>
+
+
+using std::future;
+using std::string;
+using std::vector;
+
+
+vector<string>
+ext2_ls (vector<string> arguments)
+{
+ using namespace boost::process;
+
+ dcpomatic::io_context ios;
+ future<string> data;
+ child ch (search_path("e2ls"), arguments, std_in.close(), std_out > data, ios);
+ ios.run();
+
+ auto output = data.get();
+ boost::trim (output);
+ vector<string> parts;
+ boost::split (parts, output, boost::is_any_of("\t "), boost::token_compress_on);
+ return parts;
+}
+
+
+static
+void
+make_empty_file(boost::filesystem::path file, off_t size)
+{
+ auto fd = open (file.string().c_str(), O_RDWR | O_CREAT, S_IRUSR | S_IWUSR);
+ BOOST_REQUIRE (fd != -1);
+ auto const r = posix_fallocate (fd, 0, size);
+ BOOST_REQUIRE_EQUAL (r, 0);
+ close (fd);
+}
+
+
+/** Use the writer code to make a disk and partition and copy a file (in a directory)
+ * to it, then check that:
+ * - the partition has inode size 128
+ * - the file and directory have reasonable timestamps
+ * - the file can be copied back off the disk
+ */
+BOOST_AUTO_TEST_CASE (disk_writer_test1)
+{
+ using namespace boost::filesystem;
+ using namespace boost::process;
+
+ Cleanup cl;
+
+ path disk = "build/test/disk_writer_test1.disk";
+ path partition = "build/test/disk_writer_test1.partition";
+
+ cl.add(disk);
+ cl.add(partition);
+
+ /* lwext4 has a lower limit of correct ext2 partition sizes it can make; 32Mb
+ * does not work here: fsck gives errors about an incorrect free blocks count.
+ */
+ make_random_file(disk, 256 * 1024 * 1024);
+ make_random_file(partition, 256 * 1024 * 1024);
+
+ path dcp = "build/test/disk_writer_test1";
+ create_directory (dcp);
+ /* Some arbitrary file size here */
+ make_random_file (dcp / "foo", 1024 * 1024 * 32 - 6128);
+
+ dcpomatic::write ({dcp}, disk.string(), partition.string(), nullptr);
+
+ BOOST_CHECK_EQUAL (system("/sbin/e2fsck -fn build/test/disk_writer_test1.partition"), 0);
+
+ {
+ dcpomatic::io_context ios;
+ future<string> data;
+ child ch ("/sbin/tune2fs", args({"-l", partition.string()}), std_in.close(), std_out > data, ios);
+ ios.run();
+
+ string output = data.get();
+ std::smatch matches;
+ std::regex reg("Inode size:\\s*(.*)");
+ BOOST_REQUIRE (std::regex_search(output, matches, reg));
+ BOOST_REQUIRE (matches.size() == 2);
+ BOOST_CHECK_EQUAL (matches[1].str(), "128");
+ }
+
+ BOOST_CHECK (ext2_ls({partition.string()}) == vector<string>({"disk_writer_test1", "lost+found"}));
+
+ string const unset_date = "1-Jan-1970";
+
+ /* Check timestamp of the directory has been set */
+ auto details = ext2_ls({"-l", partition.string()});
+ BOOST_REQUIRE (details.size() >= 6);
+ BOOST_CHECK (details[5] != unset_date);
+
+ auto const dir = partition.string() + ":disk_writer_test1";
+ BOOST_CHECK (ext2_ls({dir}) == vector<string>({"foo"}));
+
+ /* Check timestamp of foo */
+ details = ext2_ls({"-l", dir});
+ BOOST_REQUIRE (details.size() >= 6);
+ BOOST_CHECK (details[5] != unset_date);
+
+ system ("e2cp " + partition.string() + ":disk_writer_test1/foo build/test/disk_writer_test1_foo_back");
+ check_file ("build/test/disk_writer_test1/foo", "build/test/disk_writer_test1_foo_back");
+
+ cl.run();
+}
+
+
+BOOST_AUTO_TEST_CASE (disk_writer_test2)
+{
+ using namespace boost::filesystem;
+ using namespace boost::process;
+
+ remove_all("build/test/disk_writer_test2.disk");
+ remove_all("build/test/disk_writer_test2.partition");
+ remove_all("build/test/disk_writer_test2");
+
+ Cleanup cl;
+
+ path const disk = "build/test/disk_writer_test2.disk";
+ path const partition = "build/test/disk_writer_test2.partition";
+
+ cl.add(disk);
+ cl.add(partition);
+
+ /* Using empty files here still triggers the bug and is much quicker than using random data */
+ make_empty_file(disk, 31043616768LL);
+ make_empty_file(partition, 31043571712LL);
+
+ auto const dcp = TestPaths::private_data() / "xm";
+ dcpomatic::write({dcp}, disk.string(), partition.string(), nullptr);
+
+ BOOST_CHECK_EQUAL(system("/sbin/e2fsck -fn build/test/disk_writer_test2.partition"), 0);
+
+ path const check = "build/test/disk_writer_test2";
+ create_directory(check);
+ cl.add(check);
+
+ for (auto original: directory_iterator(dcp)) {
+ auto path_in_copy = path("xm") / original.path().filename();
+ auto path_in_check = check / original.path().filename();
+ system("e2cp " + partition.string() + ":" + path_in_copy.string() + " " + path_in_check.string());
+ check_file(original.path(), path_in_check);
+ }
+
+ cl.run();
+}
+
+
+
+BOOST_AUTO_TEST_CASE (disk_writer_test3)
+{
+ using namespace boost::filesystem;
+ using namespace boost::process;
+
+ remove_all("build/test/disk_writer_test3.disk");
+ remove_all("build/test/disk_writer_test3.partition");
+ remove_all("build/test/disk_writer_test3");
+
+ Cleanup cl;
+
+ path const disk = "build/test/disk_writer_test3.disk";
+ path const partition = "build/test/disk_writer_test3.partition";
+
+ cl.add(disk);
+ cl.add(partition);
+
+ /* Using empty files here still triggers the bug and is much quicker than using random data */
+ make_empty_file(disk, 31043616768LL);
+ make_empty_file(partition, 31043571712LL);
+
+ vector<boost::filesystem::path> const dcps = {
+ TestPaths::private_data() / "xm",
+ TestPaths::private_data() / "JourneyToJah_TLR-1_F_EN-DE-FR_CH_51_2K_LOK_20140225_DGL_SMPTE_OV"
+ };
+ dcpomatic::write(dcps, disk.string(), partition.string(), nullptr);
+
+ BOOST_CHECK_EQUAL(system("/sbin/e2fsck -fn build/test/disk_writer_test3.partition"), 0);
+
+ path const check = "build/test/disk_writer_test3";
+ create_directory(check);
+ cl.add(check);
+
+ for (auto dcp: dcps) {
+ for (auto original: directory_iterator(dcp)) {
+ auto path_in_copy = dcp.filename() / original.path().filename();
+ auto path_in_check = check / original.path().filename();
+ system("e2cp " + partition.string() + ":" + path_in_copy.string() + " " + path_in_check.string());
+ check_file(original.path(), path_in_check);
+ }
+ }
+
+ cl.run();
+}
diff --git a/test/lib/dkdm_recipient_list_test.cc b/test/lib/dkdm_recipient_list_test.cc
new file mode 100644
index 000000000..4996de6d2
--- /dev/null
+++ b/test/lib/dkdm_recipient_list_test.cc
@@ -0,0 +1,57 @@
+/*
+ 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 "lib/config.h"
+#include "lib/dkdm_recipient.h"
+#include "lib/dkdm_recipient_list.h"
+#include "../test.h"
+#include <dcp/filesystem.h>
+#include <boost/test/unit_test.hpp>
+
+
+BOOST_AUTO_TEST_CASE(dkdm_receipient_list_copy_from_xml_test)
+{
+ ConfigRestorer cr("build/test/dkdm_recipient_list_copy_config");
+
+ dcp::filesystem::remove_all(*Config::override_path);
+ dcp::filesystem::create_directories(*Config::override_path);
+ dcp::filesystem::copy_file("test/data/dkdm_recipients.xml", *Config::override_path / "dkdm_recipients.xml");
+
+ DKDMRecipientList dkdm_recipient_list;
+ dkdm_recipient_list.read_legacy_file(Config::instance()->read_path("dkdm_recipients.xml"));
+ auto dkdm_recipients = dkdm_recipient_list.dkdm_recipients();
+ BOOST_REQUIRE_EQUAL(dkdm_recipients.size(), 2U);
+
+ auto dkdm_recipient_iter = dkdm_recipients.begin();
+ BOOST_CHECK_EQUAL(dkdm_recipient_iter->second.name, "Bob's Epics");
+ BOOST_CHECK_EQUAL(dkdm_recipient_iter->second.emails.size(), 2U);
+ BOOST_CHECK_EQUAL(dkdm_recipient_iter->second.emails[0], "epicbob@gmail.com");
+ BOOST_CHECK_EQUAL(dkdm_recipient_iter->second.emails[1], "boblikesemlong@cinema-bob.com");
+ BOOST_CHECK_EQUAL(dkdm_recipient_iter->second.recipient()->subject_dn_qualifier(), "r5/Q5f3UTm7qzoF5QzNZP6aEuvI=");
+ ++dkdm_recipient_iter;
+
+ BOOST_CHECK_EQUAL(dkdm_recipient_iter->second.name, "Sharon's Shorts");
+ BOOST_CHECK_EQUAL(dkdm_recipient_iter->second.notes, "Even if it sucks, at least it's over quickly");
+ BOOST_CHECK_EQUAL(dkdm_recipient_iter->second.recipient()->subject_dn_qualifier(), "FHerM3Us/DWuqD1MnztStSlFJO0=");
+ ++dkdm_recipient_iter;
+}
+
+
diff --git a/test/lib/email_test.cc b/test/lib/email_test.cc
new file mode 100644
index 000000000..56599313f
--- /dev/null
+++ b/test/lib/email_test.cc
@@ -0,0 +1,43 @@
+/*
+ Copyright (C) 2025 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/email.h"
+#include "smtp_server.h"
+#include "../test.h"
+#include <boost/test/unit_test.hpp>
+#include <boost/thread.hpp>
+
+
+auto constexpr port = 31925;
+
+
+BOOST_AUTO_TEST_CASE(email_retry_test)
+{
+ boost::thread thread([]() {
+ run_smtp_server(port, true);
+ run_smtp_server(port, true);
+ run_smtp_server(port, false);
+ });
+
+ Email email("carl@crunchcinema.com", { "bob@snacks.com" }, "Louder crisps - possible?", "These crisps just aren't loud enough. People can still hear the film.");
+ email.send_with_retry("localhost", port, EmailProtocol::PLAIN, 3);
+}
+
diff --git a/test/lib/empty_caption_test.cc b/test/lib/empty_caption_test.cc
new file mode 100644
index 000000000..f64ca6578
--- /dev/null
+++ b/test/lib/empty_caption_test.cc
@@ -0,0 +1,46 @@
+/*
+ Copyright (C) 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 "lib/content.h"
+#include "lib/content_factory.h"
+#include "lib/film.h"
+#include "lib/text_content.h"
+#include "../test.h"
+#include <boost/test/unit_test.hpp>
+
+
+BOOST_AUTO_TEST_CASE (check_for_no_empty_text_nodes_in_failure_case)
+{
+ Cleanup cl;
+
+ auto content = content_factory("test/data/empty.srt");
+ auto film = new_test_film("check_for_no_empty_text_nodes_in_failure_case", content, &cl);
+ auto text = content[0]->text.front();
+ text->set_type (TextType::CLOSED_CAPTION);
+ text->set_dcp_track({"English", dcp::LanguageTag("en")});
+
+ make_and_verify_dcp (film, {
+ dcp::VerificationNote::Code::MISSING_CPL_METADATA
+ });
+
+ cl.run();
+}
+
diff --git a/test/lib/empty_test.cc b/test/lib/empty_test.cc
new file mode 100644
index 000000000..d3e41068f
--- /dev/null
+++ b/test/lib/empty_test.cc
@@ -0,0 +1,183 @@
+/*
+ Copyright (C) 2017-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/>.
+
+*/
+
+
+/** @file test/empty_test.cc
+ * @brief Test the creation of Empty objects.
+ * @ingroup feature
+ */
+
+
+#include "lib/dcp_content_type.h"
+#include "lib/decoder.h"
+#include "lib/empty.h"
+#include "lib/film.h"
+#include "lib/image_content.h"
+#include "lib/player.h"
+#include "lib/ratio.h"
+#include "lib/video_content.h"
+#include "../test.h"
+#include <boost/test/unit_test.hpp>
+
+
+using std::list;
+using std::make_shared;
+using std::shared_ptr;
+#if BOOST_VERSION >= 106100
+using namespace boost::placeholders;
+#endif
+using namespace dcpomatic;
+
+
+bool
+has_video (shared_ptr<const Content> content)
+{
+ return static_cast<bool>(content->video);
+}
+
+
+BOOST_AUTO_TEST_CASE (empty_test1)
+{
+ auto film = new_test_film("empty_test1");
+ film->set_sequence (false);
+ auto contentA = make_shared<ImageContent>("test/data/simple_testcard_640x480.png");
+ auto contentB = make_shared<ImageContent>("test/data/simple_testcard_640x480.png");
+
+ film->examine_and_add_content (contentA);
+ film->examine_and_add_content (contentB);
+ BOOST_REQUIRE (!wait_for_jobs());
+
+ int const vfr = film->video_frame_rate ();
+
+ /* 0 1 2 3 4 5 6 7
+ * A A A B
+ */
+ contentA->video->set_length (3);
+ contentA->set_position (film, DCPTime::from_frames(2, vfr));
+ contentB->video->set_length (1);
+ contentB->set_position (film, DCPTime::from_frames(7, vfr));
+
+ Empty black (film, film->playlist(), bind(&has_video, _1), film->playlist()->length(film));
+ BOOST_REQUIRE_EQUAL (black._periods.size(), 2U);
+ auto i = black._periods.begin();
+ BOOST_CHECK (i->from == DCPTime::from_frames(0, vfr));
+ BOOST_CHECK (i->to == DCPTime::from_frames(2, vfr));
+ ++i;
+ BOOST_CHECK (i->from == DCPTime::from_frames(5, vfr));
+ BOOST_CHECK (i->to == DCPTime::from_frames(7, vfr));
+}
+
+
+/** Some tests where the first empty period is not at time 0 */
+BOOST_AUTO_TEST_CASE (empty_test2)
+{
+ auto film = new_test_film("empty_test2");
+ film->set_sequence (false);
+ auto contentA = make_shared<ImageContent>("test/data/simple_testcard_640x480.png");
+ auto contentB = make_shared<ImageContent>("test/data/simple_testcard_640x480.png");
+
+ film->examine_and_add_content (contentA);
+ film->examine_and_add_content (contentB);
+ BOOST_REQUIRE (!wait_for_jobs());
+
+ int const vfr = film->video_frame_rate ();
+
+ /* 0 1 2 3 4 5 6 7
+ * A A A B
+ */
+ contentA->video->set_length (3);
+ contentA->set_position (film, DCPTime(0));
+ contentB->video->set_length (1);
+ contentB->set_position (film, DCPTime::from_frames(7, vfr));
+
+ Empty black (film, film->playlist(), bind(&has_video, _1), film->playlist()->length(film));
+ BOOST_REQUIRE_EQUAL (black._periods.size(), 1U);
+ BOOST_CHECK (black._periods.front().from == DCPTime::from_frames(3, vfr));
+ BOOST_CHECK (black._periods.front().to == DCPTime::from_frames(7, vfr));
+
+ /* position should initially be the start of the first empty period */
+ BOOST_CHECK (black.position() == DCPTime::from_frames(3, vfr));
+
+ /* check that done() works */
+ BOOST_CHECK (!black.done ());
+ black.set_position (DCPTime::from_frames (4, vfr));
+ BOOST_CHECK (!black.done ());
+ black.set_position (DCPTime::from_frames (7, vfr));
+ BOOST_CHECK (black.done ());
+}
+
+
+/** Test for when the film's playlist is not the same as the one passed into Empty */
+BOOST_AUTO_TEST_CASE (empty_test3)
+{
+ auto film = new_test_film("empty_test3");
+ film->set_sequence (false);
+ auto contentA = make_shared<ImageContent>("test/data/simple_testcard_640x480.png");
+ auto contentB = make_shared<ImageContent>("test/data/simple_testcard_640x480.png");
+
+ film->examine_and_add_content (contentA);
+ film->examine_and_add_content (contentB);
+ BOOST_REQUIRE (!wait_for_jobs());
+
+ int const vfr = film->video_frame_rate ();
+
+ /* 0 1 2 3 4 5 6 7
+ * A A A B
+ */
+ contentA->video->set_length (3);
+ contentA->set_position (film, DCPTime(0));
+ contentB->video->set_length (1);
+ contentB->set_position (film, DCPTime::from_frames(7, vfr));
+
+ auto playlist = make_shared<Playlist>();
+ playlist->add (film, contentB);
+ Empty black (film, playlist, bind(&has_video, _1), playlist->length(film));
+ BOOST_REQUIRE_EQUAL (black._periods.size(), 1U);
+ BOOST_CHECK (black._periods.front().from == DCPTime::from_frames(0, vfr));
+ BOOST_CHECK (black._periods.front().to == DCPTime::from_frames(7, vfr));
+
+ /* position should initially be the start of the first empty period */
+ BOOST_CHECK (black.position() == DCPTime::from_frames(0, vfr));
+}
+
+
+BOOST_AUTO_TEST_CASE (empty_test_with_overlapping_content)
+{
+ auto film = new_test_film("empty_test_with_overlapping_content");
+ film->set_sequence (false);
+ auto contentA = make_shared<ImageContent>("test/data/simple_testcard_640x480.png");
+ auto contentB = make_shared<ImageContent>("test/data/simple_testcard_640x480.png");
+
+ film->examine_and_add_content (contentA);
+ film->examine_and_add_content (contentB);
+ BOOST_REQUIRE (!wait_for_jobs());
+
+ int const vfr = film->video_frame_rate ();
+
+ contentA->video->set_length (vfr * 3);
+ contentA->set_position (film, DCPTime());
+ contentB->video->set_length (vfr * 1);
+ contentB->set_position (film, DCPTime::from_seconds(1));
+
+ Empty black(film, film->playlist(), bind(&has_video, _1), film->playlist()->length(film));
+
+ BOOST_REQUIRE (black._periods.empty());
+}
+
diff --git a/test/lib/encode_cli_test.cc b/test/lib/encode_cli_test.cc
new file mode 100644
index 000000000..f607861b2
--- /dev/null
+++ b/test/lib/encode_cli_test.cc
@@ -0,0 +1,163 @@
+/*
+ Copyright (C) 2025 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/content_factory.h"
+#include "lib/encode_cli.h"
+#include "lib/film.h"
+#include "../test.h"
+#include <boost/optional.hpp>
+#include <boost/test/unit_test.hpp>
+#include <iostream>
+#include <string>
+#include <vector>
+
+
+using std::cout;
+using std::string;
+using std::vector;
+using boost::optional;
+
+
+static
+optional<string>
+run(vector<string> const& args, vector<string>& output)
+{
+ vector<char*> argv(args.size() + 1);
+ for (auto i = 0U; i < args.size(); ++i) {
+ argv[i] = const_cast<char*>(args[i].c_str());
+ }
+ argv[args.size()] = nullptr;
+
+ auto error = encode_cli(args.size(), argv.data(), [&output](string s) { output.push_back(s); }, []() { });
+ for (auto i: output) {
+ std::cout << "O:" << i;
+ }
+ if (error) {
+ std::cout << "E:" << *error << "\n";
+ }
+
+ return error;
+}
+
+
+static
+bool
+find_in_order(vector<string> const& output, vector<string> const& check)
+{
+ BOOST_REQUIRE(!check.empty());
+
+ auto next = check.begin();
+ for (auto line: output) {
+ if (line.find(*next) != string::npos) {
+ ++next;
+ if (next == check.end()) {
+ return true;
+ }
+ }
+ }
+
+ return false;
+}
+
+
+BOOST_AUTO_TEST_CASE(basic_encode_cli_test)
+{
+ auto content = content_factory("test/data/flat_red.png");
+ auto film = new_test_film("basic_encode_cli_test", content);
+ film->write_metadata();
+
+ vector<string> output;
+ run({ "cli", "build/test/basic_encode_cli_test" }, output);
+
+ BOOST_CHECK(find_in_order(output, { "Making DCP for", "Examining content", "OK", "Transcoding DCP", "OK" }));
+}
+
+
+BOOST_AUTO_TEST_CASE(encode_cli_with_explicit_encode_command_test)
+{
+ auto content = content_factory("test/data/flat_red.png");
+ auto film = new_test_film("basic_encode_cli_test", content);
+ film->write_metadata();
+
+ vector<string> output;
+ run({ "cli", "make-dcp", "build/test/basic_encode_cli_test" }, output);
+
+ BOOST_CHECK(find_in_order(output, { "Making DCP for", "Examining content", "OK", "Transcoding DCP", "OK" }));
+}
+
+
+#ifdef DCPOMATIC_GROK
+BOOST_AUTO_TEST_CASE(encode_cli_set_grok_licence)
+{
+ boost::filesystem::path config = "build/encode_cli_set_grok_licence";
+ boost::filesystem::remove_all(config);
+ boost::filesystem::create_directories(config);
+ ConfigRestorer cr(config);
+
+ vector<string> output;
+ auto error = run({ "cli", "config", "grok-licence", "12345678ABC" }, output);
+ BOOST_CHECK(output.empty());
+ BOOST_CHECK(!error);
+
+ cxml::Document check("Config");
+ check.read_file(config / "2.18" / "config.xml");
+ BOOST_CHECK_EQUAL(check.node_child("Grok")->string_child("Licence"), "12345678ABC");
+}
+
+
+BOOST_AUTO_TEST_CASE(encode_cli_enable_grok)
+{
+ boost::filesystem::path config = "build/encode_cli_enable_grok";
+ boost::filesystem::remove_all(config);
+ boost::filesystem::create_directories(config);
+ ConfigRestorer cr(config);
+
+ for (auto value: vector<string>{ "1", "0"}) {
+ vector<string> output;
+ auto error = run({ "cli", "config", "grok-enable", value }, output);
+ BOOST_CHECK(output.empty());
+ BOOST_CHECK(!error);
+
+ cxml::Document check("Config");
+ check.read_file(config / "2.18" / "config.xml");
+ BOOST_CHECK_EQUAL(check.node_child("Grok")->string_child("Enable"), value);
+ }
+}
+
+
+BOOST_AUTO_TEST_CASE(encode_cli_set_grok_binary_location)
+{
+ boost::filesystem::path config = "build/encode_cli_set_grok_binary_location";
+ boost::filesystem::remove_all(config);
+ boost::filesystem::create_directories(config);
+ ConfigRestorer cr(config);
+
+ vector<string> output;
+ auto error = run({ "cli", "config", "grok-binary-location", "foo/bar/baz" }, output);
+ BOOST_CHECK(output.empty());
+ BOOST_CHECK(!error);
+
+ cxml::Document check("Config");
+ check.read_file(config / "2.18" / "config.xml");
+ BOOST_CHECK_EQUAL(check.node_child("Grok")->string_child("BinaryLocation"), "foo/bar/baz");
+}
+#endif
+
diff --git a/test/lib/encryption_test.cc b/test/lib/encryption_test.cc
new file mode 100644
index 000000000..29cc39a06
--- /dev/null
+++ b/test/lib/encryption_test.cc
@@ -0,0 +1,69 @@
+/*
+ Copyright (C) 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 "lib/config.h"
+#include "lib/content_factory.h"
+#include "lib/dcp_content.h"
+#include "lib/dcp_examiner.h"
+#include "lib/film.h"
+#include "../test.h"
+#include <dcp/cpl.h>
+#include <dcp/dcp.h>
+#include <boost/test/unit_test.hpp>
+
+
+using std::make_shared;
+
+
+BOOST_AUTO_TEST_CASE (smpte_dcp_with_subtitles_can_be_decrypted)
+{
+ auto content = content_factory("test/data/15s.srt");
+ auto film = new_test_film("smpte_dcp_with_subtitles_can_be_decrypted", content);
+ film->set_interop (false);
+ film->set_encrypted (true);
+ make_and_verify_dcp (
+ film,
+ {
+ dcp::VerificationNote::Code::MISSING_CPL_METADATA,
+ dcp::VerificationNote::Code::MISSED_CHECK_OF_ENCRYPTED,
+ dcp::VerificationNote::Code::MISSED_CHECK_OF_ENCRYPTED,
+ dcp::VerificationNote::Code::MISSING_SUBTITLE_LANGUAGE,
+ dcp::VerificationNote::Code::MISSING_SUBTITLE_START_TIME,
+ });
+
+ dcp::DCP dcp (film->dir(film->dcp_name()));
+ dcp.read ();
+ BOOST_REQUIRE_EQUAL (dcp.cpls().size(), 1U);
+ auto cpl = dcp.cpls()[0];
+ BOOST_REQUIRE (cpl->file());
+
+ auto signer = Config::instance()->signer_chain();
+ BOOST_REQUIRE(signer->valid());
+
+ auto const decrypted_kdm = film->make_kdm(*cpl->file(), dcp::LocalTime(), dcp::LocalTime());
+ auto const kdm = decrypted_kdm.encrypt(signer, Config::instance()->decryption_chain()->leaf(), {}, dcp::Formulation::MODIFIED_TRANSITIONAL_1, true, 0);
+
+ auto dcp_content = make_shared<DCPContent>(film->dir(film->dcp_name()));
+ dcp_content->add_kdm (kdm);
+ DCPExaminer examiner (dcp_content, false);
+ BOOST_CHECK (examiner.kdm_valid());
+}
+
diff --git a/test/lib/ffmpeg_audio_only_test.cc b/test/lib/ffmpeg_audio_only_test.cc
new file mode 100644
index 000000000..db62a3a3d
--- /dev/null
+++ b/test/lib/ffmpeg_audio_only_test.cc
@@ -0,0 +1,201 @@
+/*
+ Copyright (C) 2016-2021 Carl Hetherington <cth@carlh.net>
+
+ This file is part of DCP-o-matic.
+
+ DCP-o-matic is free software; you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation; either version 2 of the License, or
+ (at your option) any later version.
+
+ DCP-o-matic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with DCP-o-matic. If not, see <http://www.gnu.org/licenses/>.
+
+*/
+
+
+/** @file test/ffmpeg_audio_only_test.cc
+ * @brief Test FFmpeg content with audio but no video.
+ * @ingroup feature
+ */
+
+
+#include "lib/film.h"
+#include "lib/ffmpeg_content.h"
+#include "lib/dcp_content_type.h"
+#include "lib/player.h"
+#include "lib/job_manager.h"
+#include "lib/audio_buffers.h"
+#include "../test.h"
+#include <dcp/sound_asset.h>
+#include <dcp/sound_asset_reader.h>
+#include <sndfile.h>
+#include <boost/test/unit_test.hpp>
+#include <iostream>
+
+
+using std::min;
+using std::shared_ptr;
+using std::make_shared;
+#if BOOST_VERSION >= 106100
+using namespace boost::placeholders;
+#endif
+
+
+static SNDFILE* ref = nullptr;
+static std::vector<float> ref_buffer;
+
+
+static void
+audio (std::shared_ptr<AudioBuffers> audio, int channels)
+{
+ /* Check that we have a big enough buffer */
+ BOOST_CHECK (audio->frames() * audio->channels() < static_cast<int>(ref_buffer.size()));
+
+ int const N = sf_readf_float (ref, ref_buffer.data(), audio->frames());
+ for (int i = 0; i < N; ++i) {
+ switch (channels) {
+ case 1:
+ BOOST_CHECK_EQUAL (ref_buffer[i], audio->data(2)[i]);
+ break;
+ case 2:
+ BOOST_CHECK_EQUAL (ref_buffer[i*2 + 0], audio->data(0)[i]);
+ BOOST_CHECK_EQUAL (ref_buffer[i*2 + 1], audio->data(1)[i]);
+ break;
+ default:
+ BOOST_REQUIRE (false);
+ }
+ }
+}
+
+
+/** Test the FFmpeg code with audio-only content */
+static shared_ptr<Film>
+test (boost::filesystem::path file)
+{
+ auto c = make_shared<FFmpegContent>(file);
+ auto film = new_test_film("ffmpeg_audio_only_test", { c });
+ film->write_metadata ();
+
+ /* See if can make a DCP without any errors */
+ make_and_verify_dcp (film, {dcp::VerificationNote::Code::MISSING_CPL_METADATA});
+ BOOST_CHECK (!JobManager::instance()->errors());
+
+ /* Compare the audio data player reads with what libsndfile reads */
+
+ SF_INFO info;
+ info.format = 0;
+ ref = sf_open (file.string().c_str(), SFM_READ, &info);
+ /* We don't want to test anything that requires resampling */
+ BOOST_REQUIRE_EQUAL (info.samplerate, 48000);
+ ref_buffer.resize(info.samplerate * info.channels);
+
+ auto player = make_shared<Player>(film, Image::Alignment::COMPACT, false);
+
+ player->Audio.connect (bind (&audio, _1, info.channels));
+ while (!player->pass ()) {}
+
+ sf_close (ref);
+
+ return film;
+}
+
+
+BOOST_AUTO_TEST_CASE (ffmpeg_audio_only_test1)
+{
+ /* S16 */
+ auto film = test ("test/data/staircase.wav");
+
+ /* Compare the audio data in the DCP with what libsndfile reads */
+
+ SF_INFO info;
+ info.format = 0;
+ ref = sf_open ("test/data/staircase.wav", SFM_READ, &info);
+ /* We don't want to test anything that requires resampling */
+ BOOST_REQUIRE_EQUAL (info.samplerate, 48000);
+
+ std::vector<int16_t> buffer(info.channels * 2000);
+
+ dcp::SoundAsset asset (dcp_file(film, "pcm"));
+ auto reader = asset.start_read ();
+ for (int i = 0; i < asset.intrinsic_duration(); ++i) {
+ auto frame = reader->get_frame(i);
+ sf_count_t this_time = min (info.frames, sf_count_t(2000));
+ sf_readf_short (ref, buffer.data(), this_time);
+ for (int j = 0; j < this_time; ++j) {
+ BOOST_REQUIRE_EQUAL (frame->get(2, j) >> 8, buffer[j]);
+ }
+ info.frames -= this_time;
+ }
+}
+
+
+BOOST_AUTO_TEST_CASE (ffmpeg_audio_only_test2)
+{
+ /* S32 1 channel */
+ auto film = test ("test/data/sine_440.wav");
+
+ /* Compare the audio data in the DCP with what libsndfile reads */
+
+ SF_INFO info;
+ info.format = 0;
+ ref = sf_open ("test/data/sine_440.wav", SFM_READ, &info);
+ /* We don't want to test anything that requires resampling */
+ BOOST_REQUIRE_EQUAL (info.samplerate, 48000);
+
+ std::vector<int32_t> buffer(info.channels * 2000);
+
+ dcp::SoundAsset asset (dcp_file(film, "pcm"));
+ auto reader = asset.start_read ();
+ for (int i = 0; i < asset.intrinsic_duration(); ++i) {
+ auto frame = reader->get_frame(i);
+ sf_count_t this_time = min (info.frames, sf_count_t(2000));
+ sf_readf_int (ref, buffer.data(), this_time);
+ for (int j = 0; j < this_time; ++j) {
+ int32_t s = frame->get(2, j);
+ if (s > (1 << 23)) {
+ s -= (1 << 24);
+ }
+ BOOST_REQUIRE_MESSAGE (abs(s - (buffer[j] / 256)) <= 1, "failed on asset frame " << i << " sample " << j);
+ }
+ info.frames -= this_time;
+ }
+}
+
+
+BOOST_AUTO_TEST_CASE (ffmpeg_audio_only_test3)
+{
+ /* S24 1 channel */
+ auto film = test ("test/data/sine_24_48_440.wav");
+
+ /* Compare the audio data in the DCP with what libsndfile reads */
+
+ SF_INFO info;
+ info.format = 0;
+ ref = sf_open ("test/data/sine_24_48_440.wav", SFM_READ, &info);
+ /* We don't want to test anything that requires resampling */
+ BOOST_REQUIRE_EQUAL (info.samplerate, 48000);
+
+ std::vector<int32_t> buffer(info.channels * 2000);
+
+ dcp::SoundAsset asset (dcp_file(film, "pcm"));
+ auto reader = asset.start_read ();
+ for (int i = 0; i < asset.intrinsic_duration(); ++i) {
+ auto frame = reader->get_frame(i);
+ sf_count_t this_time = min (info.frames, sf_count_t(2000));
+ sf_readf_int (ref, buffer.data(), this_time);
+ for (int j = 0; j < this_time; ++j) {
+ int32_t s = frame->get(2, j);
+ if (s > (1 << 23)) {
+ s -= (1 << 24);
+ }
+ BOOST_REQUIRE_MESSAGE (abs(s - buffer[j] /256) <= 1, "failed on asset frame " << i << " sample " << j);
+ }
+ info.frames -= this_time;
+ }
+}
diff --git a/test/lib/ffmpeg_audio_test.cc b/test/lib/ffmpeg_audio_test.cc
new file mode 100644
index 000000000..de377defc
--- /dev/null
+++ b/test/lib/ffmpeg_audio_test.cc
@@ -0,0 +1,146 @@
+/*
+ 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/>.
+
+*/
+
+
+/** @file test/ffmpeg_audio_test.cc
+ * @brief Test reading audio from an FFmpeg file.
+ * @ingroup feature
+ */
+
+
+#include "lib/constants.h"
+#include "lib/content_factory.h"
+#include "lib/dcp_content_type.h"
+#include "lib/ffmpeg_content.h"
+#include "lib/ffmpeg_content.h"
+#include "lib/film.h"
+#include "lib/player.h"
+#include "lib/ratio.h"
+#include "lib/video_content.h"
+#include "../test.h"
+#include <dcp/cpl.h>
+#include <dcp/dcp.h>
+#include <dcp/sound_asset.h>
+#include <dcp/sound_frame.h>
+#include <dcp/reel_sound_asset.h>
+#include <dcp/sound_asset_reader.h>
+#include <dcp/reel.h>
+#include <boost/test/unit_test.hpp>
+
+
+using std::make_shared;
+using std::string;
+
+
+BOOST_AUTO_TEST_CASE (ffmpeg_audio_test)
+{
+ auto c = make_shared<FFmpegContent> ("test/data/staircase.mov");
+ auto film = new_test_film("ffmpeg_audio_test", { c });
+
+ int constexpr audio_channels = 6;
+
+ film->set_container (Ratio::from_id ("185"));
+ film->set_audio_channels(audio_channels);
+ film->set_dcp_content_type (DCPContentType::from_isdcf_name ("TST"));
+ make_and_verify_dcp (film);
+
+ boost::filesystem::path path = "build/test";
+ path /= "ffmpeg_audio_test";
+ path /= film->dcp_name ();
+ dcp::DCP check (path.string ());
+ check.read ();
+
+ auto sound_asset = check.cpls().front()->reels().front()->main_sound ();
+ BOOST_CHECK (sound_asset);
+ BOOST_REQUIRE_EQUAL(sound_asset->asset()->channels (), audio_channels);
+
+ /* Sample index in the DCP */
+ int n = 0;
+ /* DCP sound asset frame */
+ int frame = 0;
+
+ while (n < sound_asset->asset()->intrinsic_duration()) {
+ auto sound_frame = sound_asset->asset()->start_read()->get_frame (frame++);
+ uint8_t const * d = sound_frame->data ();
+ for (int offset = 0; offset < sound_frame->size(); offset += (3 * sound_asset->asset()->channels())) {
+ for (auto channel = 0; channel < audio_channels; ++channel) {
+ auto const sample = d[offset + channel * 3 + 1] | (d[offset + channel * 3 + 2] << 8);
+ if (channel == 2) {
+ /* Input should be on centre */
+ BOOST_CHECK_EQUAL(sample, n);
+ } else {
+ /* Everything else should be silent */
+ BOOST_CHECK_EQUAL(sample, 0);
+ }
+ }
+ ++n;
+ }
+ }
+}
+
+
+/** Decode a file containing truehd so we can profile it; this is with the player set to normal */
+BOOST_AUTO_TEST_CASE (ffmpeg_audio_test2)
+{
+ auto film = new_test_film("ffmpeg_audio_test2");
+ auto content = content_factory(TestPaths::private_data() / "wayne.mkv")[0];
+ film->examine_and_add_content (content);
+ BOOST_REQUIRE (!wait_for_jobs ());
+
+ auto player = make_shared<Player>(film, Image::Alignment::COMPACT, false);
+ while (!player->pass ()) {}
+}
+
+
+/** Decode a file containing truehd so we can profile it; this is with the player set to fast */
+BOOST_AUTO_TEST_CASE (ffmpeg_audio_test3)
+{
+ auto film = new_test_film("ffmpeg_audio_test3");
+ auto content = content_factory(TestPaths::private_data() / "wayne.mkv")[0];
+ film->examine_and_add_content (content);
+ BOOST_REQUIRE (!wait_for_jobs ());
+
+ auto player = make_shared<Player>(film, Image::Alignment::COMPACT, false);
+ player->set_fast ();
+ while (!player->pass ()) {}
+}
+
+
+/** Decode a file whose audio previously crashed DCP-o-matic (#1857) */
+BOOST_AUTO_TEST_CASE (ffmpeg_audio_test4)
+{
+ auto film = new_test_film("ffmpeg_audio_test4");
+ auto content = content_factory(TestPaths::private_data() / "Actuellement aout 2020.wmv")[0];
+ film->examine_and_add_content (content);
+ BOOST_REQUIRE (!wait_for_jobs ());
+
+ auto player = make_shared<Player>(film, Image::Alignment::COMPACT, false);
+ player->set_fast ();
+ BOOST_CHECK_NO_THROW (while (!player->pass()) {});
+}
+
+
+
+BOOST_AUTO_TEST_CASE(no_audio_length_in_header)
+{
+ auto content = content_factory(TestPaths::private_data() / "10-seconds.thd");
+ auto film = new_test_film("no_audio_length_in_header", content);
+ BOOST_CHECK(content[0]->full_length(film) == dcpomatic::DCPTime::from_seconds(10));
+}
diff --git a/test/lib/ffmpeg_dcp_test.cc b/test/lib/ffmpeg_dcp_test.cc
new file mode 100644
index 000000000..62d65f32e
--- /dev/null
+++ b/test/lib/ffmpeg_dcp_test.cc
@@ -0,0 +1,72 @@
+/*
+ Copyright (C) 2012-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/>.
+
+*/
+
+
+/** @file test/ffmpeg_dcp_test.cc
+ * @brief Test creation of a very simple DCP from some FFmpegContent (data/test.mp4).
+ * @ingroup feature
+ *
+ * Also a quick test of Film::have_dcp ().
+ */
+
+
+#include "lib/dcp_content_type.h"
+#include "lib/ffmpeg_content.h"
+#include "lib/film.h"
+#include "lib/ratio.h"
+#include "lib/video_content.h"
+#include "../test.h"
+#include <boost/algorithm/string.hpp>
+#include <boost/filesystem.hpp>
+#include <boost/test/unit_test.hpp>
+
+
+using std::make_shared;
+
+
+BOOST_AUTO_TEST_CASE (ffmpeg_dcp_test)
+{
+ auto c = make_shared<FFmpegContent>("test/data/test.mp4");
+ auto film = new_test_film("ffmpeg_dcp_test", { c });
+ film->set_name("test_film2");
+ make_and_verify_dcp (film);
+}
+
+
+/** Briefly test Film::cpls() */
+BOOST_AUTO_TEST_CASE (ffmpeg_have_dcp_test, * boost::unit_test::depends_on("ffmpeg_dcp_test"))
+{
+ auto p = test_film_dir ("ffmpeg_dcp_test");
+ auto film = make_shared<Film>(p);
+ film->read_metadata ();
+ BOOST_CHECK (!film->cpls().empty());
+
+ p /= film->dcp_name();
+ auto i = boost::filesystem::directory_iterator (p);
+ while (i != boost::filesystem::directory_iterator() && !boost::algorithm::starts_with(i->path().filename().string(), "j2c")) {
+ ++i;
+ }
+
+ if (i != boost::filesystem::directory_iterator ()) {
+ boost::filesystem::remove (i->path ());
+ }
+
+ BOOST_CHECK (film->cpls().empty());
+}
diff --git a/test/lib/ffmpeg_decoder_error_test.cc b/test/lib/ffmpeg_decoder_error_test.cc
new file mode 100644
index 000000000..d5293d924
--- /dev/null
+++ b/test/lib/ffmpeg_decoder_error_test.cc
@@ -0,0 +1,60 @@
+/*
+ Copyright (C) 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 "lib/content.h"
+#include "lib/content_factory.h"
+#include "lib/dcpomatic_time.h"
+#include "lib/film.h"
+#include "lib/player.h"
+#include "../test.h"
+#include <boost/test/unit_test.hpp>
+
+
+/** @defgroup regression Regression tests */
+
+/** @file test/ffmpeg_decoder_error_test.cc
+ * @brief Check some bugs in the FFmpegDecoder
+ * @ingroup regression
+ */
+
+
+BOOST_AUTO_TEST_CASE (check_exception_during_flush)
+{
+ auto content = content_factory(TestPaths::private_data() / "3d_thx_broadway_2010_lossless.m2ts");
+ auto film = new_test_film("check_exception_during_flush", content);
+
+ content[0]->set_trim_start(film, dcpomatic::ContentTime(2310308));
+ content[0]->set_trim_end(dcpomatic::ContentTime(116020));
+
+ make_and_verify_dcp (film);
+}
+
+
+
+BOOST_AUTO_TEST_CASE (check_exception_with_multiple_video_frames_per_packet)
+{
+ auto content = content_factory(TestPaths::private_data() / "chk.mkv")[0];
+ auto film = new_test_film("check_exception_with_multiple_video_frames_per_packet", { content });
+ auto player = std::make_shared<Player>(film, film->playlist(), false);
+
+ while (!player->pass()) {}
+}
+
diff --git a/test/lib/ffmpeg_decoder_seek_test.cc b/test/lib/ffmpeg_decoder_seek_test.cc
new file mode 100644
index 000000000..18ed19914
--- /dev/null
+++ b/test/lib/ffmpeg_decoder_seek_test.cc
@@ -0,0 +1,135 @@
+/*
+ Copyright (C) 2014-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/>.
+
+*/
+
+
+/** @file test/ffmpeg_decoder_seek_test.cc
+ * @brief Check seek() with FFmpegDecoder.
+ * @ingroup selfcontained
+ *
+ * This doesn't check that the contents of those frames are right, which
+ * it probably should.
+ */
+
+
+#include "lib/content_video.h"
+#include "lib/ffmpeg_content.h"
+#include "lib/ffmpeg_decoder.h"
+#include "lib/film.h"
+#include "lib/null_log.h"
+#include "lib/video_decoder.h"
+#include "../test.h"
+#include <boost/filesystem.hpp>
+#include <boost/test/unit_test.hpp>
+#include <iostream>
+#include <vector>
+
+
+using std::cerr;
+using std::cout;
+using std::list;
+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;
+
+
+static optional<ContentVideo> stored;
+static bool
+store (ContentVideo v)
+{
+ stored = v;
+ return true;
+}
+
+
+static void
+check (shared_ptr<FFmpegDecoder> decoder, ContentTime time)
+{
+ BOOST_REQUIRE (decoder->ffmpeg_content()->video_frame_rate ());
+ decoder->seek(time, true);
+ stored = optional<ContentVideo> ();
+ while (!decoder->pass() && !stored) {}
+ BOOST_CHECK(stored->time <= time);
+}
+
+
+static void
+test (boost::filesystem::path file, vector<ContentTime> times)
+{
+ auto path = TestPaths::private_data() / file;
+ BOOST_REQUIRE (boost::filesystem::exists (path));
+
+ auto content = make_shared<FFmpegContent>(path);
+ auto film = new_test_film("ffmpeg_decoder_seek_test_" + file.string(), { content });
+ auto decoder = make_shared<FFmpegDecoder>(film, content, false);
+ decoder->video->Data.connect (bind (&store, _1));
+
+ for (auto i: times) {
+ check (decoder, i);
+ }
+}
+
+
+BOOST_AUTO_TEST_CASE (ffmpeg_decoder_seek_test)
+{
+ test(
+ "boon_telly.mkv",
+ {
+ ContentTime::from_frames(0, 29.97),
+ ContentTime::from_frames(42, 29.97),
+ ContentTime::from_frames(999, 29.97),
+ ContentTime::from_frames(0, 29.97),
+ }
+ );
+
+ test(
+ "Sintel_Trailer1.480p.DivX_Plus_HD.mkv",
+ {
+ ContentTime::from_frames(0, 24),
+ ContentTime::from_frames(42, 24),
+ ContentTime::from_frames(999, 24),
+ ContentTime::from_frames(0, 24),
+ }
+ );
+
+ test(
+ "prophet_long_clip.mkv",
+ {
+ ContentTime::from_frames(15, 23.976),
+ ContentTime::from_frames(42, 23.976),
+ ContentTime::from_frames(999, 23.976),
+ ContentTime::from_frames(15, 23.976)
+ }
+ );
+
+ test(
+ "dolby_aurora.vob",
+ {
+ ContentTime::from_frames(0, 25),
+ ContentTime::from_frames(125, 25),
+ ContentTime::from_frames(250, 25),
+ ContentTime::from_frames(41, 25)
+ }
+ );
+}
diff --git a/test/lib/ffmpeg_decoder_sequential_test.cc b/test/lib/ffmpeg_decoder_sequential_test.cc
new file mode 100644
index 000000000..479e15c59
--- /dev/null
+++ b/test/lib/ffmpeg_decoder_sequential_test.cc
@@ -0,0 +1,94 @@
+/*
+ Copyright (C) 2014-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/>.
+
+*/
+
+
+/** @file test/ffmpeg_decoder_sequential_test.cc
+ * @brief Check that the FFmpeg decoder and Player produce sequential frames without gaps or dropped frames;
+ * Also that the decoder picks up frame rates correctly.
+ * @ingroup feature
+ */
+
+
+#include "lib/ffmpeg_content.h"
+#include "lib/ffmpeg_decoder.h"
+#include "lib/content_video.h"
+#include "lib/video_decoder.h"
+#include "lib/film.h"
+#include "lib/player_video.h"
+#include "lib/player.h"
+#include "../test.h"
+#include <boost/filesystem.hpp>
+#include <boost/test/unit_test.hpp>
+#include <iostream>
+
+
+using std::cerr;
+using std::cout;
+using std::list;
+using std::make_shared;
+using std::shared_ptr;
+using boost::optional;
+using boost::bind;
+#if BOOST_VERSION >= 106100
+using namespace boost::placeholders;
+#endif
+using namespace dcpomatic;
+
+
+static DCPTime next;
+static DCPTime frame;
+
+
+static void
+check (shared_ptr<PlayerVideo>, DCPTime time)
+{
+ BOOST_REQUIRE (time == next);
+ next += frame;
+}
+
+
+void
+ffmpeg_decoder_sequential_test_one (boost::filesystem::path file, float fps, int video_length)
+{
+ auto path = TestPaths::private_data() / file;
+ BOOST_REQUIRE (boost::filesystem::exists (path));
+
+ auto content = make_shared<FFmpegContent>(path);
+ auto film = new_test_film("ffmpeg_decoder_sequential_test_" + file.string(), { content });
+ auto player = make_shared<Player>(film, Image::Alignment::COMPACT, false);
+
+ BOOST_REQUIRE (content->video_frame_rate());
+ BOOST_CHECK_CLOSE (content->video_frame_rate().get(), fps, 0.01);
+
+ player->Video.connect (bind (&check, _1, _2));
+
+ next = DCPTime ();
+ frame = DCPTime::from_frames (1, film->video_frame_rate ());
+ while (!player->pass()) {}
+ BOOST_REQUIRE (next == DCPTime::from_frames (video_length, film->video_frame_rate()));
+}
+
+
+BOOST_AUTO_TEST_CASE (ffmpeg_decoder_sequential_test)
+{
+ ffmpeg_decoder_sequential_test_one ("boon_telly.mkv", 29.97, 6912);
+ ffmpeg_decoder_sequential_test_one ("Sintel_Trailer1.480p.DivX_Plus_HD.mkv", 24, 1253);
+ ffmpeg_decoder_sequential_test_one ("prophet_long_clip.mkv", 23.976, 2879);
+}
diff --git a/test/lib/ffmpeg_encoder_test.cc b/test/lib/ffmpeg_encoder_test.cc
new file mode 100644
index 000000000..a73436f3a
--- /dev/null
+++ b/test/lib/ffmpeg_encoder_test.cc
@@ -0,0 +1,541 @@
+/*
+ Copyright (C) 2017-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 "lib/audio_content.h"
+#include "lib/compose.hpp"
+#include "lib/config.h"
+#include "lib/constants.h"
+#include "lib/content_factory.h"
+#include "lib/cross.h"
+#include "lib/dcp_content.h"
+#include "lib/dcpomatic_log.h"
+#include "lib/ffmpeg_content.h"
+#include "lib/ffmpeg_examiner.h"
+#include "lib/ffmpeg_film_encoder.h"
+#include "lib/film.h"
+#include "lib/image_content.h"
+#include "lib/ratio.h"
+#include "lib/string_text_file_content.h"
+#include "lib/text_content.h"
+#include "lib/transcode_job.h"
+#include "lib/video_content.h"
+#include "../test.h"
+#include <dcp/file.h>
+#include <dcp/raw_convert.h>
+#include <boost/test/unit_test.hpp>
+
+
+using std::make_shared;
+using std::string;
+using std::vector;
+using namespace dcpomatic;
+
+
+static void
+ffmpeg_content_test (int number, boost::filesystem::path content, ExportFormat format)
+{
+ Cleanup cl;
+
+ string name = "ffmpeg_encoder_";
+ string extension;
+ switch (format) {
+ case ExportFormat::H264_AAC:
+ name += "h264";
+ extension = "mp4";
+ break;
+ case ExportFormat::PRORES_4444:
+ name += "prores-444";
+ extension = "mov";
+ break;
+ case ExportFormat::PRORES_HQ:
+ name += "prores-hq";
+ extension = "mov";
+ break;
+ case ExportFormat::PRORES_LT:
+ name += "prores-lt";
+ extension = "mov";
+ break;
+ case ExportFormat::SUBTITLES_DCP:
+ BOOST_REQUIRE (false);
+ }
+
+ name = String::compose("%1_test%2", name, number);
+
+ auto c = make_shared<FFmpegContent>(content);
+ auto film = new_test_film(name, {c}, &cl);
+ film->set_name (name);
+ film->set_audio_channels (6);
+
+ film->write_metadata ();
+ auto job = make_shared<TranscodeJob>(film, TranscodeJob::ChangedBehaviour::IGNORE);
+ auto file = boost::filesystem::path("build") / "test" / String::compose("%1.%2", name, extension);
+ cl.add (file);
+ FFmpegFilmEncoder encoder(film, job, file, format, false, false, false, 23);
+ encoder.go ();
+
+ cl.run ();
+}
+
+
+/** Red / green / blue MP4 -> Prores */
+BOOST_AUTO_TEST_CASE (ffmpeg_encoder_prores_test1)
+{
+ ffmpeg_content_test (1, "test/data/test.mp4", ExportFormat::PRORES_HQ);
+}
+
+
+/** Dolby Aurora trailer VOB -> Prores */
+BOOST_AUTO_TEST_CASE (ffmpeg_encoder_prores_test2)
+{
+ ffmpeg_content_test (2, TestPaths::private_data() / "dolby_aurora.vob", ExportFormat::PRORES_HQ);
+}
+
+
+/** Sintel trailer -> Prores */
+BOOST_AUTO_TEST_CASE (ffmpeg_encoder_prores_test3)
+{
+ ffmpeg_content_test (3, TestPaths::private_data() / "Sintel_Trailer1.480p.DivX_Plus_HD.mkv", ExportFormat::PRORES_HQ);
+}
+
+
+/** Big Buck Bunny trailer -> Prores */
+BOOST_AUTO_TEST_CASE (ffmpeg_encoder_prores_test4)
+{
+ ffmpeg_content_test (4, TestPaths::private_data() / "big_buck_bunny_trailer_480p.mov", ExportFormat::PRORES_HQ);
+}
+
+
+/** Still image -> Prores */
+BOOST_AUTO_TEST_CASE (ffmpeg_encoder_prores_test5)
+{
+ boost::filesystem::path const output = "build/test/ffmpeg_encoder_prores_test5.mov";
+ Cleanup cl;
+ cl.add(output);
+
+ auto c = make_shared<ImageContent>(TestPaths::private_data() / "bbc405.png");
+ auto film = new_test_film("ffmpeg_encoder_prores_test5", { c });
+ film->set_audio_channels (6);
+
+ c->video->set_length (240);
+
+ film->write_metadata ();
+ auto job = make_shared<TranscodeJob>(film, TranscodeJob::ChangedBehaviour::IGNORE);
+ FFmpegFilmEncoder encoder(film, job, output, ExportFormat::PRORES_HQ, false, false, false, 23);
+ encoder.go ();
+
+ cl.run();
+}
+
+
+/** Subs -> Prores */
+BOOST_AUTO_TEST_CASE (ffmpeg_encoder_prores_test6)
+{
+ auto s = make_shared<StringTextFileContent>("test/data/subrip2.srt");
+ auto film = new_test_film("ffmpeg_encoder_prores_test6", { s });
+ film->set_audio_channels (6);
+
+ s->only_text()->set_colour (dcp::Colour (255, 255, 0));
+ s->only_text()->set_effect (dcp::Effect::SHADOW);
+ s->only_text()->set_effect_colour (dcp::Colour (0, 255, 255));
+ film->write_metadata();
+
+ auto job = make_shared<TranscodeJob> (film, TranscodeJob::ChangedBehaviour::IGNORE);
+ FFmpegFilmEncoder encoder(film, job, "build/test/ffmpeg_encoder_prores_test6.mov", ExportFormat::PRORES_HQ, false, false, false, 23);
+ encoder.go ();
+}
+
+
+/** Video + subs -> Prores */
+BOOST_AUTO_TEST_CASE (ffmpeg_encoder_prores_test7)
+{
+ boost::filesystem::path const output = "build/test/ffmpeg_encoder_prores_test7.mov";
+ Cleanup cl;
+ cl.add(output);
+
+ auto c = make_shared<FFmpegContent>("test/data/test.mp4");
+ auto s = make_shared<StringTextFileContent>("test/data/subrip.srt");
+ auto film = new_test_film("ffmpeg_encoder_prores_test7", { c, s });
+ film->set_audio_channels (6);
+
+ s->only_text()->set_colour (dcp::Colour (255, 255, 0));
+ s->only_text()->set_effect (dcp::Effect::SHADOW);
+ s->only_text()->set_effect_colour (dcp::Colour (0, 255, 255));
+
+ auto job = make_shared<TranscodeJob>(film, TranscodeJob::ChangedBehaviour::IGNORE);
+ FFmpegFilmEncoder encoder(film, job, output, ExportFormat::PRORES_HQ, false, false, false, 23);
+ encoder.go ();
+
+ cl.run();
+}
+
+
+/** Red / green / blue MP4 -> H264 */
+BOOST_AUTO_TEST_CASE (ffmpeg_encoder_h264_test1)
+{
+ ffmpeg_content_test(1, "test/data/test.mp4", ExportFormat::H264_AAC);
+}
+
+
+/** Just subtitles -> H264 */
+BOOST_AUTO_TEST_CASE (ffmpeg_encoder_h264_test2)
+{
+ auto s = make_shared<StringTextFileContent>("test/data/subrip2.srt");
+ auto film = new_test_film("ffmpeg_encoder_h264_test2", { s });
+ film->set_audio_channels (6);
+
+ s->only_text()->set_colour (dcp::Colour (255, 255, 0));
+ s->only_text()->set_effect (dcp::Effect::SHADOW);
+ s->only_text()->set_effect_colour (dcp::Colour (0, 255, 255));
+ film->write_metadata();
+
+ auto job = make_shared<TranscodeJob>(film, TranscodeJob::ChangedBehaviour::IGNORE);
+ FFmpegFilmEncoder encoder(film, job, "build/test/ffmpeg_encoder_h264_test2.mp4", ExportFormat::H264_AAC, false, false, false, 23);
+ encoder.go ();
+}
+
+
+/** Video + subs -> H264 */
+BOOST_AUTO_TEST_CASE (ffmpeg_encoder_h264_test3)
+{
+ auto c = make_shared<FFmpegContent>("test/data/test.mp4");
+ auto s = make_shared<StringTextFileContent>("test/data/subrip.srt");
+ auto film = new_test_film("ffmpeg_encoder_h264_test3", { c, s });
+ film->set_audio_channels (6);
+
+ s->only_text()->set_colour (dcp::Colour (255, 255, 0));
+ s->only_text()->set_effect (dcp::Effect::SHADOW);
+ s->only_text()->set_effect_colour (dcp::Colour (0, 255, 255));
+ film->write_metadata();
+
+ auto job = make_shared<TranscodeJob> (film, TranscodeJob::ChangedBehaviour::IGNORE);
+ FFmpegFilmEncoder encoder(film, job, "build/test/ffmpeg_encoder_h264_test3.mp4", ExportFormat::H264_AAC, false, false, false, 23);
+ encoder.go ();
+}
+
+
+/** Scope-in-flat DCP -> H264 */
+BOOST_AUTO_TEST_CASE (ffmpeg_encoder_h264_test4)
+{
+ auto film = new_test_film("ffmpeg_encoder_h264_test4", {make_shared<DCPContent>("test/data/scope_dcp")});
+ BOOST_REQUIRE(!wait_for_jobs());
+
+ film->set_container(Ratio::from_id("185"));
+
+ auto job = make_shared<TranscodeJob>(film, TranscodeJob::ChangedBehaviour::IGNORE);
+ FFmpegFilmEncoder encoder(film, job, "build/test/ffmpeg_encoder_h264_test4.mp4", ExportFormat::H264_AAC, false, false, false, 23);
+ encoder.go();
+}
+
+
+/** Test mixdown from 5.1 to stereo */
+BOOST_AUTO_TEST_CASE (ffmpeg_encoder_h264_test5)
+{
+ auto L = make_shared<FFmpegContent>("test/data/L.wav");
+ auto R = make_shared<FFmpegContent>("test/data/R.wav");
+ auto C = make_shared<FFmpegContent>("test/data/C.wav");
+ auto Ls = make_shared<FFmpegContent>("test/data/Ls.wav");
+ auto Rs = make_shared<FFmpegContent>("test/data/Rs.wav");
+ auto Lfe = make_shared<FFmpegContent>("test/data/Lfe.wav");
+
+ auto film = new_test_film("ffmpeg_encoder_h264_test5", { L, R, C, Ls, Rs, Lfe });
+ film->set_audio_channels (6);
+
+ AudioMapping map (1, MAX_DCP_AUDIO_CHANNELS);
+
+ L->set_position (film, DCPTime::from_seconds(0));
+ map.make_zero ();
+ map.set (0, 0, 1);
+ L->audio->set_mapping (map);
+ R->set_position (film, DCPTime::from_seconds(1));
+ map.make_zero ();
+ map.set (0, 1, 1);
+ R->audio->set_mapping (map);
+ C->set_position (film, DCPTime::from_seconds(2));
+ map.make_zero ();
+ map.set (0, 2, 1);
+ C->audio->set_mapping (map);
+ Lfe->set_position (film, DCPTime::from_seconds(3));
+ map.make_zero ();
+ map.set (0, 3, 1);
+ Lfe->audio->set_mapping (map);
+ Ls->set_position (film, DCPTime::from_seconds(4));
+ map.make_zero ();
+ map.set (0, 4, 1);
+ Ls->audio->set_mapping (map);
+ Rs->set_position (film, DCPTime::from_seconds(5));
+ map.make_zero ();
+ map.set (0, 5, 1);
+ Rs->audio->set_mapping (map);
+
+ auto job = make_shared<TranscodeJob>(film, TranscodeJob::ChangedBehaviour::IGNORE);
+ FFmpegFilmEncoder encoder(film, job, "build/test/ffmpeg_encoder_h264_test5.mp4", ExportFormat::H264_AAC, true, false, false, 23);
+ encoder.go ();
+
+ check_ffmpeg ("build/test/ffmpeg_encoder_h264_test5.mp4", "test/data/ffmpeg_encoder_h264_test5.mp4", 1);
+}
+
+
+/** Test export of a VF */
+BOOST_AUTO_TEST_CASE (ffmpeg_encoder_h264_test6)
+{
+ auto film = new_test_film("ffmpeg_encoder_h264_test6_ov");
+ film->examine_and_add_content (make_shared<ImageContent>(TestPaths::private_data() / "bbc405.png"));
+ BOOST_REQUIRE (!wait_for_jobs());
+ make_and_verify_dcp (film);
+
+ auto film2 = new_test_film("ffmpeg_encoder_h264_test6_vf");
+ auto ov = make_shared<DCPContent>("build/test/ffmpeg_encoder_h264_test6_ov/" + film->dcp_name(false));
+ film2->examine_and_add_content (ov);
+ BOOST_REQUIRE (!wait_for_jobs());
+ ov->set_reference_video (true);
+ auto subs = content_factory("test/data/subrip.srt")[0];
+ film2->examine_and_add_content (subs);
+ BOOST_REQUIRE (!wait_for_jobs());
+ for (auto i: subs->text) {
+ i->set_use (true);
+ }
+
+ auto job = make_shared<TranscodeJob>(film2, TranscodeJob::ChangedBehaviour::IGNORE);
+ FFmpegFilmEncoder encoder(film2, job, "build/test/ffmpeg_encoder_h264_test6_vf.mp4", ExportFormat::H264_AAC, true, false, false, 23);
+ encoder.go ();
+}
+
+
+/** Test export of a 3D DCP in a 2D project */
+BOOST_AUTO_TEST_CASE (ffmpeg_encoder_3d_dcp_to_h264)
+{
+ boost::filesystem::path const output = "build/test/ffmpeg_encoder_3d_dcp_to_h264.mp4";
+ Cleanup cl;
+ cl.add(output);
+
+ auto dcp = make_shared<DCPContent>(TestPaths::private_data() / "xm");
+ auto film2 = new_test_film("ffmpeg_encoder_3d_dcp_to_h264_export", {dcp});
+
+ auto job = make_shared<TranscodeJob>(film2, TranscodeJob::ChangedBehaviour::IGNORE);
+ FFmpegFilmEncoder encoder(film2, job, output, ExportFormat::H264_AAC, true, false, false, 23);
+ encoder.go ();
+
+ cl.run();
+}
+
+
+/** Test export of a 3D DCP in a 2D project */
+BOOST_AUTO_TEST_CASE (ffmpeg_encoder_h264_test7)
+{
+ auto L = make_shared<ImageContent>(TestPaths::private_data() / "bbc405.png");
+ auto R = make_shared<ImageContent>(TestPaths::private_data() / "bbc405.png");
+ auto film = new_test_film("ffmpeg_encoder_h264_test7_data", {L, R});
+ L->video->set_frame_type (VideoFrameType::THREE_D_LEFT);
+ L->set_position (film, DCPTime());
+ R->video->set_frame_type (VideoFrameType::THREE_D_RIGHT);
+ R->set_position (film, DCPTime());
+ film->set_three_d (true);
+ make_and_verify_dcp (film);
+
+ auto dcp = make_shared<DCPContent>(film->dir(film->dcp_name()));
+ auto film2 = new_test_film("ffmpeg_encoder_h264_test7_export", {dcp});
+
+ auto job = make_shared<TranscodeJob> (film2, TranscodeJob::ChangedBehaviour::IGNORE);
+ FFmpegFilmEncoder encoder(film2, job, "build/test/ffmpeg_encoder_h264_test7.mp4", ExportFormat::H264_AAC, true, false, false, 23);
+ encoder.go ();
+}
+
+
+BOOST_AUTO_TEST_CASE(ffmpeg_encoder_2d_content_in_3d_project)
+{
+ auto content = make_shared<ImageContent>(TestPaths::private_data() / "bbc405.png");
+ auto film = new_test_film("ffmpeg_encoder_2d_content_in_3d_project", { content });
+ film->set_three_d(true);
+
+ auto job = make_shared<TranscodeJob>(film, TranscodeJob::ChangedBehaviour::IGNORE);
+ FFmpegFilmEncoder encoder(film, job, "build/test/ffmpeg_encoder_2d_content_in_3d_project.mp4", ExportFormat::H264_AAC, true, false, false, 23);
+ encoder.go();
+}
+
+
+/** Stereo project with mixdown-to-stereo set */
+BOOST_AUTO_TEST_CASE (ffmpeg_encoder_h264_test8)
+{
+ auto film = new_test_film("ffmpeg_encoder_h264_test4");
+ film->examine_and_add_content(make_shared<DCPContent>("test/data/scope_dcp"));
+ BOOST_REQUIRE(!wait_for_jobs());
+ film->set_audio_channels (2);
+
+ auto job = make_shared<TranscodeJob>(film, TranscodeJob::ChangedBehaviour::IGNORE);
+ FFmpegFilmEncoder encoder(film, job, "build/test/ffmpeg_encoder_h264_test8.mp4", ExportFormat::H264_AAC, true, false, false, 23);
+ encoder.go();
+}
+
+
+/** 7.1/HI/VI (i.e. 12-channel) project */
+BOOST_AUTO_TEST_CASE (ffmpeg_encoder_h264_test9)
+{
+ auto c = make_shared<ImageContent>(TestPaths::private_data() / "bbc405.png");
+ auto film = new_test_film("ffmpeg_encoder_prores_test9", { c });
+ film->set_name ("ffmpeg_encoder_prores_test9");
+ film->set_container (Ratio::from_id ("185"));
+ film->set_audio_channels (12);
+
+ film->examine_and_add_content (c);
+ BOOST_REQUIRE (!wait_for_jobs ());
+
+ c->video->set_length (240);
+
+ film->write_metadata ();
+ auto job = make_shared<TranscodeJob>(film, TranscodeJob::ChangedBehaviour::IGNORE);
+ FFmpegFilmEncoder encoder (film, job, "build/test/ffmpeg_encoder_prores_test9.mov", ExportFormat::H264_AAC, false, false, false, 23);
+ encoder.go ();
+}
+
+
+/** DCP -> Prores with crop */
+BOOST_AUTO_TEST_CASE (ffmpeg_encoder_prores_from_dcp_with_crop)
+{
+ auto dcp = make_shared<DCPContent>("test/data/import_dcp_test2");
+ auto film = new_test_film("ffmpeg_encoder_prores_from_dcp_with_crop", { dcp });
+ dcp->video->set_left_crop (32);
+ dcp->video->set_right_crop (32);
+ film->write_metadata ();
+
+ auto job = make_shared<TranscodeJob>(film, TranscodeJob::ChangedBehaviour::IGNORE);
+ FFmpegFilmEncoder encoder (film, job, "build/test/ffmpeg_encoder_prores_from_dcp_with_crop.mov", ExportFormat::PRORES_HQ, false, false, false, 23);
+ encoder.go ();
+}
+
+
+/** DCP -> H264 with crop */
+BOOST_AUTO_TEST_CASE (ffmpeg_encoder_h264_from_dcp_with_crop)
+{
+ auto dcp = make_shared<DCPContent>("test/data/import_dcp_test2");
+ auto film = new_test_film("ffmpeg_encoder_h264_from_dcp_with_crop", { dcp });
+ dcp->video->set_left_crop (32);
+ dcp->video->set_right_crop (32);
+ film->write_metadata ();
+
+ auto job = make_shared<TranscodeJob>(film, TranscodeJob::ChangedBehaviour::IGNORE);
+ FFmpegFilmEncoder encoder (film, job, "build/test/ffmpeg_encoder_prores_from_dcp_with_crop.mov", ExportFormat::H264_AAC, false, false, false, 23);
+ encoder.go ();
+}
+
+
+/** Export to H264 with reels */
+BOOST_AUTO_TEST_CASE (ffmpeg_encoder_h264_with_reels)
+{
+ auto content1 = content_factory("test/data/flat_red.png")[0];
+ auto content2 = content_factory("test/data/flat_red.png")[0];
+ auto film = new_test_film("ffmpeg_encoder_h264_with_reels", { content1, content2 });
+ film->set_reel_type (ReelType::BY_VIDEO_CONTENT);
+ content1->video->set_length (240);
+ content2->video->set_length (240);
+
+ auto job = make_shared<TranscodeJob>(film, TranscodeJob::ChangedBehaviour::IGNORE);
+ FFmpegFilmEncoder encoder (film, job, "build/test/ffmpeg_encoder_h264_with_reels.mov", ExportFormat::H264_AAC, false, true, false, 23);
+ encoder.go ();
+
+ auto check = [](boost::filesystem::path path) {
+ auto reel = std::dynamic_pointer_cast<FFmpegContent>(content_factory(path)[0]);
+ BOOST_REQUIRE (reel);
+ FFmpegExaminer examiner(reel);
+ BOOST_CHECK_EQUAL (examiner.video_length(), 240U);
+ };
+
+ check ("build/test/ffmpeg_encoder_h264_with_reels_reel1.mov");
+ check ("build/test/ffmpeg_encoder_h264_with_reels_reel2.mov");
+}
+
+
+/** Regression test for "Error during decoding: Butler finished" (#2097) */
+BOOST_AUTO_TEST_CASE (ffmpeg_encoder_prores_regression_1)
+{
+ Cleanup cl;
+
+ auto content = content_factory(TestPaths::private_data() / "arrietty_JP-EN.mkv")[0];
+ auto film = new_test_film("ffmpeg_encoder_prores_regression_1", { content });
+
+ auto job = make_shared<TranscodeJob>(film, TranscodeJob::ChangedBehaviour::IGNORE);
+ FFmpegFilmEncoder encoder (film, job, "build/test/ffmpeg_encoder_prores_regression_1.mov", ExportFormat::PRORES_HQ, false, true, false, 23);
+ encoder.go ();
+
+ cl.add("build/test/ffmpeg_encoder_prores_regression_1.mov");
+ cl.run();
+}
+
+
+/** Regression test for Butler video buffers reached 480 frames (audio is 0) (#2101) */
+BOOST_AUTO_TEST_CASE (ffmpeg_encoder_prores_regression_2)
+{
+ Cleanup cl;
+
+ auto logs = dcpomatic_log->types();
+ dcpomatic_log->set_types(logs | LogEntry::TYPE_DEBUG_PLAYER);
+
+ auto content = content_factory(TestPaths::private_data() / "tge_clip.mkv")[0];
+ auto film = new_test_film("ffmpeg_encoder_prores_regression_2", { content });
+
+ auto job = make_shared<TranscodeJob>(film, TranscodeJob::ChangedBehaviour::IGNORE);
+ FFmpegFilmEncoder encoder (film, job, "build/test/ffmpeg_encoder_prores_regression_2.mov", ExportFormat::PRORES_HQ, false, true, false, 23);
+ encoder.go ();
+
+ dcpomatic_log->set_types(logs);
+
+ cl.add("build/test/ffmpeg_encoder_prores_regression_2.mov");
+ cl.run();
+}
+
+
+BOOST_AUTO_TEST_CASE(ffmpeg_encoder_missing_frame_at_end)
+{
+ auto content = content_factory(TestPaths::private_data() / "1s1f.mov");
+ auto film = new_test_film("ffmpeg_encoder_missing_frame_at_end", content);
+
+ boost::filesystem::path output("build/test/ffmpeg_encoder_missing_frame_at_end.mov");
+ boost::filesystem::path log("build/test/ffmpeg_encoder_missing_frame_at_end.log");
+
+ auto job = make_shared<TranscodeJob>(film, TranscodeJob::ChangedBehaviour::IGNORE);
+ FFmpegFilmEncoder encoder(film, job, output, ExportFormat::PRORES_HQ, false, true, false, 23);
+ encoder.go();
+
+ run_ffprobe(output, log, false, "-show_frames -show_format -show_streams -select_streams v:0");
+
+ dcp::File read_log(log, "r");
+ BOOST_REQUIRE(read_log);
+
+ int nb_read_frames = 0;
+ int nb_frames = 0;
+ char buffer[256];
+
+ while (!read_log.eof()) {
+ read_log.gets(buffer, sizeof(buffer));
+ vector<string> parts;
+ boost::algorithm::split(parts, buffer, boost::is_any_of("="));
+ if (parts.size() == 2) {
+ if (parts[0] == "nb_read_frames") {
+ nb_read_frames = dcp::raw_convert<int>(parts[1]);
+ } else if (parts[0] == "nb_frames") {
+ nb_frames = dcp::raw_convert<int>(parts[1]);
+ }
+ }
+ }
+
+ BOOST_CHECK_EQUAL(nb_frames, 26);
+ BOOST_CHECK_EQUAL(nb_read_frames, 26);
+}
+
diff --git a/test/lib/ffmpeg_examiner_test.cc b/test/lib/ffmpeg_examiner_test.cc
new file mode 100644
index 000000000..e9ca32e3a
--- /dev/null
+++ b/test/lib/ffmpeg_examiner_test.cc
@@ -0,0 +1,95 @@
+/*
+ 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/>.
+
+*/
+
+
+/** @file test/ffmpeg_examiner_test.cc
+ * @brief FFmpegExaminer tests
+ * @ingroup selfcontained
+ */
+
+
+#include "lib/ffmpeg_examiner.h"
+#include "lib/ffmpeg_content.h"
+#include "lib/ffmpeg_audio_stream.h"
+#include "../test.h"
+#include <boost/test/unit_test.hpp>
+
+
+using std::make_shared;
+using namespace dcpomatic;
+
+
+/** Check that the FFmpegExaminer can extract the first video and audio time
+ * correctly from data/count300bd24.m2ts.
+ */
+BOOST_AUTO_TEST_CASE (ffmpeg_examiner_test)
+{
+ auto content = make_shared<FFmpegContent>("test/data/count300bd24.m2ts");
+ auto film = new_test_film("ffmpeg_examiner_test", { content });
+ auto examiner = make_shared<FFmpegExaminer>(content);
+
+ BOOST_CHECK_EQUAL (examiner->first_video().get().get(), ContentTime::from_seconds(600).get());
+ BOOST_CHECK_EQUAL (examiner->audio_streams().size(), 1U);
+ BOOST_CHECK_EQUAL (examiner->audio_streams()[0]->first_audio.get().get(), ContentTime::from_seconds(600).get());
+}
+
+
+/** Check that audio sampling rate and channel counts are correctly picked up from
+ * a problematic file. When we used to specify analyzeduration and probesize
+ * this file's details were picked up incorrectly.
+ */
+BOOST_AUTO_TEST_CASE (ffmpeg_examiner_probesize_test)
+{
+ auto content = make_shared<FFmpegContent>(TestPaths::private_data() / "RockyTop10 Playlist Flat.m4v");
+ auto examiner = make_shared<FFmpegExaminer>(content);
+
+ BOOST_CHECK_EQUAL (examiner->audio_streams().size(), 2U);
+ BOOST_CHECK_EQUAL (examiner->audio_streams()[0]->frame_rate(), 48000);
+ BOOST_CHECK_EQUAL (examiner->audio_streams()[0]->channels(), 2);
+ BOOST_CHECK_EQUAL (examiner->audio_streams()[1]->frame_rate(), 48000);
+ BOOST_CHECK_EQUAL (examiner->audio_streams()[1]->channels(), 6);
+}
+
+
+/** Check that a file can be examined without error */
+BOOST_AUTO_TEST_CASE (ffmpeg_examiner_vob_test)
+{
+ auto content = make_shared<FFmpegContent>(TestPaths::private_data() / "bad.vob");
+ auto examiner = make_shared<FFmpegExaminer>(content);
+}
+
+
+/** Check that another file can be examined without error */
+BOOST_AUTO_TEST_CASE (ffmpeg_examiner_mkv_test)
+{
+ auto content = make_shared<FFmpegContent>(TestPaths::private_data() / "sample.mkv");
+ auto examiner = make_shared<FFmpegExaminer>(content);
+}
+
+
+/** Check that the video stream is correctly picked from a difficult file (#2238) */
+BOOST_AUTO_TEST_CASE (ffmpeg_examiner_video_stream_selection_test)
+{
+ auto content = make_shared<FFmpegContent>(TestPaths::private_data() / "isy.mp4");
+ auto examiner = make_shared<FFmpegExaminer>(content);
+
+ BOOST_REQUIRE (examiner->video_frame_rate());
+ BOOST_CHECK_EQUAL (examiner->video_frame_rate().get(), 25);
+}
diff --git a/test/lib/ffmpeg_properties_test.cc b/test/lib/ffmpeg_properties_test.cc
new file mode 100644
index 000000000..b3cbb0609
--- /dev/null
+++ b/test/lib/ffmpeg_properties_test.cc
@@ -0,0 +1,65 @@
+/*
+ Copyright (C) 2022 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/compose.hpp"
+#include "lib/content.h"
+#include "lib/content_factory.h"
+#include "lib/user_property.h"
+#include "../test.h"
+#include <boost/test/unit_test.hpp>
+
+
+using std::list;
+using std::string;
+
+
+static
+void
+colour_range_test(string name, boost::filesystem::path file, string ref)
+{
+ auto content = content_factory(file);
+ BOOST_REQUIRE(!content.empty());
+ auto film = new_test_film(String::compose("ffmpeg_properties_test_%1", name), { content.front() });
+
+ auto properties = content.front()->user_properties(film);
+ auto iter = std::find_if(properties.begin(), properties.end(), [](UserProperty const& property) { return property.key == "Colour range"; });
+ BOOST_REQUIRE(iter != properties.end());
+ BOOST_CHECK_EQUAL(iter->value, ref);
+}
+
+
+BOOST_AUTO_TEST_CASE(ffmpeg_properties_test)
+{
+ colour_range_test("1", "test/data/test.mp4", "Unspecified");
+ colour_range_test("2", TestPaths::private_data() / "arrietty_JP-EN.mkv", "Limited / video (16-235)");
+ colour_range_test("3", "test/data/8bit_full_420.mp4", "Full (0-255)");
+ colour_range_test("4", "test/data/8bit_full_422.mp4", "Full (0-255)");
+ colour_range_test("5", "test/data/8bit_full_444.mp4", "Full (0-255)");
+ colour_range_test("6", "test/data/8bit_video_420.mp4", "Limited / video (16-235)");
+ colour_range_test("7", "test/data/8bit_video_422.mp4", "Limited / video (16-235)");
+ colour_range_test("8", "test/data/8bit_video_444.mp4", "Limited / video (16-235)");
+ colour_range_test("9", "test/data/10bit_full_420.mp4", "Full (0-1023)");
+ colour_range_test("10", "test/data/10bit_full_422.mp4", "Full (0-1023)");
+ colour_range_test("11", "test/data/10bit_full_444.mp4", "Full (0-1023)");
+ colour_range_test("12", "test/data/10bit_video_420.mp4", "Limited / video (64-940)");
+ colour_range_test("13", "test/data/10bit_video_422.mp4", "Limited / video (64-940)");
+ colour_range_test("14", "test/data/10bit_video_444.mp4", "Limited / video (64-940)");
+}
diff --git a/test/lib/ffmpeg_pts_offset_test.cc b/test/lib/ffmpeg_pts_offset_test.cc
new file mode 100644
index 000000000..4f447dc3d
--- /dev/null
+++ b/test/lib/ffmpeg_pts_offset_test.cc
@@ -0,0 +1,92 @@
+/*
+ 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/>.
+
+*/
+
+
+/** @file test/ffmpeg_pts_offset_test.cc
+ * @brief Check the computation of _pts_offset in FFmpegDecoder.
+ * @ingroup selfcontained
+ */
+
+
+#include "lib/audio_content.h"
+#include "lib/ffmpeg_audio_stream.h"
+#include "lib/ffmpeg_content.h"
+#include "lib/ffmpeg_decoder.h"
+#include "lib/film.h"
+#include "../test.h"
+#include <boost/test/unit_test.hpp>
+
+
+using std::make_shared;
+using std::shared_ptr;
+using namespace dcpomatic;
+
+
+BOOST_AUTO_TEST_CASE (ffmpeg_pts_offset_test)
+{
+ auto content = make_shared<FFmpegContent>("test/data/test.mp4");
+ auto film = new_test_film("ffmpeg_pts_offset_test", { content });
+
+ content->audio = make_shared<AudioContent>(content.get());
+ content->audio->add_stream (shared_ptr<FFmpegAudioStream> (new FFmpegAudioStream));
+ content->_video_frame_rate = 24;
+
+ {
+ /* Sound == video so no offset required */
+ content->_first_video = ContentTime ();
+ content->ffmpeg_audio_streams().front()->first_audio = ContentTime ();
+ FFmpegDecoder decoder (film, content, false);
+ BOOST_CHECK_EQUAL (decoder._pts_offset.get(), 0);
+ }
+
+ {
+ /* Common offset should be removed */
+ content->_first_video = ContentTime::from_seconds (600);
+ content->ffmpeg_audio_streams().front()->first_audio = ContentTime::from_seconds (600);
+ FFmpegDecoder decoder (film, content, false);
+ BOOST_CHECK_EQUAL (decoder._pts_offset.get(), ContentTime::from_seconds(-600).get());
+ }
+
+ {
+ /* Video is on a frame boundary */
+ content->_first_video = ContentTime::from_frames (1, 24);
+ content->ffmpeg_audio_streams().front()->first_audio = ContentTime ();
+ FFmpegDecoder decoder (film, content, false);
+ BOOST_CHECK_EQUAL (decoder._pts_offset.get(), 0);
+ }
+
+ {
+ /* Video is off a frame boundary */
+ double const frame = 1.0 / 24.0;
+ content->_first_video = ContentTime::from_seconds (frame + 0.0215);
+ content->ffmpeg_audio_streams().front()->first_audio = ContentTime ();
+ FFmpegDecoder decoder (film, content, false);
+ BOOST_CHECK_CLOSE (decoder._pts_offset.seconds(), (frame - 0.0215), 0.00001);
+ }
+
+ {
+ /* Video is off a frame boundary and both have a common offset */
+ double const frame = 1.0 / 24.0;
+ content->_first_video = ContentTime::from_seconds (frame + 0.0215 + 4.1);
+ content->ffmpeg_audio_streams().front()->first_audio = ContentTime::from_seconds (4.1);
+ FFmpegDecoder decoder (film, content, false);
+ BOOST_CHECK_CLOSE (decoder._pts_offset.seconds(), (frame - 0.0215) - 4.1, 0.1);
+ }
+}
diff --git a/test/lib/ffmpeg_subtitles_test.cc b/test/lib/ffmpeg_subtitles_test.cc
new file mode 100644
index 000000000..21ac3a10b
--- /dev/null
+++ b/test/lib/ffmpeg_subtitles_test.cc
@@ -0,0 +1,60 @@
+/*
+ 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 "lib/content_factory.h"
+#include "lib/film.h"
+#include "lib/player.h"
+#include "lib/text_content.h"
+#include "../test.h"
+#include <boost/optional.hpp>
+#include <boost/test/unit_test.hpp>
+
+
+using std::make_shared;
+using std::shared_ptr;
+using std::string;
+using std::vector;
+using boost::optional;
+
+
+BOOST_AUTO_TEST_CASE(decoding_ssa_subs_from_mkv)
+{
+ auto subs = content_factory(TestPaths::private_data() / "ssa_subs.mkv")[0];
+ auto film = new_test_film("decoding_ssa_subs_from_mkv", { subs });
+ subs->text[0]->set_use(true);
+
+ vector<string> lines;
+
+ auto player = make_shared<Player>(film, film->playlist(), false);
+ player->Text.connect([&lines](PlayerText text, TextType, optional<DCPTextTrack>, dcpomatic::DCPTimePeriod) {
+ for (auto i: text.string) {
+ lines.push_back(i.text());
+ }
+ });
+
+ while (lines.size() <= 2 && !player->pass()) {}
+
+ BOOST_REQUIRE_EQUAL(lines.size(), 3U);
+ BOOST_CHECK_EQUAL(lines[0], "-You're hungry.");
+ BOOST_CHECK_EQUAL(lines[1], "-Unit 14, nothing's happening");
+ BOOST_CHECK_EQUAL(lines[2], "here, we're gonna go to the beach.");
+}
+
diff --git a/test/lib/file_extension_test.cc b/test/lib/file_extension_test.cc
new file mode 100644
index 000000000..40179121c
--- /dev/null
+++ b/test/lib/file_extension_test.cc
@@ -0,0 +1,81 @@
+/*
+ Copyright (C) 2022 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/content.h"
+#include "lib/content_factory.h"
+#include "lib/film.h"
+#include "lib/text_content.h"
+#include "../test.h"
+#include <boost/test/unit_test.hpp>
+
+
+/* Sanity check to make sure that files in a DCP have the right extensions / names.
+ * This is mostly to catch a crazy mistake where Interop subtitle files suddenly got
+ * a MXF extension but no tests caught it (#2270).
+ */
+BOOST_AUTO_TEST_CASE (interop_file_extension_test)
+{
+ auto video = content_factory("test/data/flat_red.png")[0];
+ auto audio = content_factory("test/data/sine_440.wav")[0];
+ auto sub = content_factory("test/data/15s.srt")[0];
+ auto film = new_test_film("interop_file_extension_test", { video, audio, sub });
+ film->set_interop(true);
+ sub->only_text()->set_language(dcp::LanguageTag("de"));
+
+ make_and_verify_dcp(
+ film, {
+ dcp::VerificationNote::Code::INVALID_SUBTITLE_FIRST_TEXT_TIME,
+ dcp::VerificationNote::Code::MISSING_SUBTITLE_LANGUAGE,
+ dcp::VerificationNote::Code::INVALID_STANDARD
+ });
+
+ BOOST_REQUIRE(dcp_file(film, "ASSETMAP").extension() == "");
+ BOOST_REQUIRE(dcp_file(film, "VOLINDEX").extension() == "");
+ BOOST_REQUIRE(dcp_file(film, "cpl").extension() == ".xml");
+ BOOST_REQUIRE(dcp_file(film, "pkl").extension() == ".xml");
+ BOOST_REQUIRE(dcp_file(film, "j2c").extension() == ".mxf");
+ BOOST_REQUIRE(dcp_file(film, "pcm").extension() == ".mxf");
+ BOOST_REQUIRE(dcp_file(film, "sub").extension() == ".xml");
+}
+
+
+BOOST_AUTO_TEST_CASE (smpte_file_extension_test)
+{
+ auto video = content_factory("test/data/flat_red.png")[0];
+ auto audio = content_factory("test/data/sine_440.wav")[0];
+ auto sub = content_factory("test/data/15s.srt")[0];
+ auto film = new_test_film("smpte_file_extension_test", { video, audio, sub });
+ film->set_interop(false);
+
+ make_and_verify_dcp(
+ film, {
+ dcp::VerificationNote::Code::INVALID_SUBTITLE_FIRST_TEXT_TIME,
+ dcp::VerificationNote::Code::MISSING_SUBTITLE_LANGUAGE
+ });
+
+ BOOST_REQUIRE(dcp_file(film, "ASSETMAP").extension() == ".xml");
+ BOOST_REQUIRE(dcp_file(film, "VOLINDEX").extension() == ".xml");
+ BOOST_REQUIRE(dcp_file(film, "cpl").extension() == ".xml");
+ BOOST_REQUIRE(dcp_file(film, "pkl").extension() == ".xml");
+ BOOST_REQUIRE(dcp_file(film, "j2c").extension() == ".mxf");
+ BOOST_REQUIRE(dcp_file(film, "pcm").extension() == ".mxf");
+ BOOST_REQUIRE(dcp_file(film, "sub").extension() == ".mxf");
+}
diff --git a/test/lib/file_group_test.cc b/test/lib/file_group_test.cc
new file mode 100644
index 000000000..3fb31317c
--- /dev/null
+++ b/test/lib/file_group_test.cc
@@ -0,0 +1,131 @@
+/*
+ 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/>.
+
+*/
+
+
+/** @file test/file_group_test.cc
+ * @brief Test FileGroup class.
+ * @ingroup selfcontained
+ */
+
+
+#include "lib/file_group.h"
+#include <boost/test/unit_test.hpp>
+#include <boost/filesystem.hpp>
+#include <stdint.h>
+#include <cstdio>
+
+
+using std::vector;
+
+
+BOOST_AUTO_TEST_CASE (file_group_test)
+{
+ /* Random data; must be big enough for all the files */
+ uint8_t data[65536];
+ for (int i = 0; i < 65536; ++i) {
+ data[i] = rand() & 0xff;
+ }
+
+ int const num_files = 4;
+
+ int length[] = {
+ 99,
+ 18941,
+ 33110,
+ 42
+ };
+
+ int total_length = 0;
+ for (int i = 0; i < num_files; ++i) {
+ total_length += length[i];
+ }
+
+ boost::filesystem::create_directories ("build/test/file_group_test");
+ vector<boost::filesystem::path> name = {
+ "build/test/file_group_test/A",
+ "build/test/file_group_test/B",
+ "build/test/file_group_test/C",
+ "build/test/file_group_test/D"
+ };
+
+ int base = 0;
+ for (int i = 0; i < num_files; ++i) {
+ auto f = fopen (name[i].string().c_str(), "wb");
+ fwrite (data + base, 1, length[i], f);
+ fclose (f);
+ base += length[i];
+ }
+
+ FileGroup fg (name);
+ uint8_t test[65536];
+
+ int pos = 0;
+
+ /* Basic read from 0 */
+ BOOST_CHECK_EQUAL(fg.read(test, 64).bytes_read, 64);
+ BOOST_CHECK_EQUAL (memcmp(data, test, 64), 0);
+ pos += 64;
+
+ /* Another read following the previous */
+ BOOST_CHECK_EQUAL(fg.read(test, 4).bytes_read, 4);
+ BOOST_CHECK_EQUAL (memcmp(data + pos, test, 4), 0);
+ pos += 4;
+
+ /* Read overlapping A and B */
+ BOOST_CHECK_EQUAL(fg.read(test, 128).bytes_read, 128);
+ BOOST_CHECK_EQUAL (memcmp(data + pos, test, 128), 0);
+ pos += 128;
+
+ /* Read overlapping B/C/D and over-reading by a lot */
+ BOOST_CHECK_EQUAL(fg.read(test, total_length * 3).bytes_read, total_length - pos);
+ BOOST_CHECK_EQUAL (memcmp(data + pos, test, total_length - pos), 0);
+
+ /* Over-read by a little */
+ BOOST_CHECK_EQUAL (fg.seek(0, SEEK_SET), 0);
+ BOOST_CHECK_EQUAL(fg.read(test, total_length).bytes_read, total_length);
+ BOOST_CHECK_EQUAL(fg.read(test, 1).bytes_read, 0);
+
+ /* Seeking off the end of the file should not give an error */
+ BOOST_CHECK_EQUAL (fg.seek(total_length * 2, SEEK_SET), total_length * 2);
+ /* and attempting to read should return nothing and EOF */
+ auto result = fg.read(test, 64);
+ BOOST_CHECK_EQUAL(result.bytes_read, 0);
+ BOOST_CHECK(result.eof);
+ /* but the requested seek should be remembered, so if we now go back (relatively) */
+ BOOST_CHECK_EQUAL (fg.seek(-total_length * 2, SEEK_CUR), 0);
+ /* we should be at the start again */
+ BOOST_CHECK_EQUAL(fg.read(test, 64).bytes_read, 64);
+ BOOST_CHECK_EQUAL (memcmp(data, test, 64), 0);
+
+ /* SEEK_SET */
+ BOOST_CHECK_EQUAL (fg.seek(999, SEEK_SET), 999);
+ BOOST_CHECK_EQUAL(fg.read(test, 64).bytes_read, 64);
+ BOOST_CHECK_EQUAL (memcmp(data + 999, test, 64), 0);
+
+ /* SEEK_CUR */
+ BOOST_CHECK_EQUAL (fg.seek(42, SEEK_CUR), 999 + 64 + 42);
+ BOOST_CHECK_EQUAL(fg.read(test, 64).bytes_read, 64);
+ BOOST_CHECK_EQUAL (memcmp(data + 999 + 64 + 42, test, 64), 0);
+
+ /* SEEK_END */
+ BOOST_CHECK_EQUAL (fg.seek(1077, SEEK_END), total_length - 1077);
+ BOOST_CHECK_EQUAL(fg.read(test, 256).bytes_read, 256);
+ BOOST_CHECK_EQUAL (memcmp(data + total_length - 1077, test, 256), 0);
+}
diff --git a/test/lib/file_log_test.cc b/test/lib/file_log_test.cc
new file mode 100644
index 000000000..dca1d4386
--- /dev/null
+++ b/test/lib/file_log_test.cc
@@ -0,0 +1,37 @@
+/*
+ Copyright (C) 2014-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/>.
+
+*/
+
+
+/** @file test/file_log_test.cc
+ * @brief Test FileLog.
+ * @ingroup selfcontained
+ */
+
+
+#include "lib/file_log.h"
+#include <boost/test/unit_test.hpp>
+
+
+BOOST_AUTO_TEST_CASE (file_log_test)
+{
+ FileLog log ("test/data/short.log");
+ BOOST_CHECK_EQUAL (log.head_and_tail(1024), "This is a short log.\nWith only two lines.\n");
+ BOOST_CHECK_EQUAL (log.head_and_tail(8), "This is \n .\n .\n .\no lines.\n");
+}
diff --git a/test/lib/file_naming_test.cc b/test/lib/file_naming_test.cc
new file mode 100644
index 000000000..32374e8ce
--- /dev/null
+++ b/test/lib/file_naming_test.cc
@@ -0,0 +1,200 @@
+/*
+ Copyright (C) 2016-2021 Carl Hetherington <cth@carlh.net>
+
+ This file is part of DCP-o-matic.
+
+ DCP-o-matic is free software; you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation; either version 2 of the License, or
+ (at your option) any later version.
+
+ DCP-o-matic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with DCP-o-matic. If not, see <http://www.gnu.org/licenses/>.
+
+*/
+
+
+/** @file test/file_naming_test.cc
+ * @brief Test how files in DCPs are named.
+ * @ingroup feature
+ */
+
+
+#include "lib/config.h"
+#include "lib/content_factory.h"
+#include "lib/dcp_content_type.h"
+#include "lib/ffmpeg_content.h"
+#include "lib/film.h"
+#include "lib/video_content.h"
+#include "../test.h"
+#ifdef DCPOMATIC_WINDOWS
+#include <boost/locale.hpp>
+#endif
+#include <boost/test/unit_test.hpp>
+#include <boost/regex.hpp>
+
+
+using std::make_shared;
+using std::string;
+
+
+static
+string
+mxf_regex(string part) {
+#ifdef DCPOMATIC_WINDOWS
+ /* Windows replaces . in filenames with _ */
+ return String::compose(".*flat_%1_png_.*\\.mxf", part);
+#else
+ return String::compose(".*flat_%1\\.png_.*\\.mxf", part);
+#endif
+};
+
+
+
+BOOST_AUTO_TEST_CASE (file_naming_test)
+{
+ ConfigRestorer cr;
+ Config::instance()->set_dcp_asset_filename_format (dcp::NameFormat("%c"));
+
+ auto r = make_shared<FFmpegContent>("test/data/flat_red.png");
+ auto g = make_shared<FFmpegContent>("test/data/flat_green.png");
+ auto b = make_shared<FFmpegContent>("test/data/flat_blue.png");
+ auto film = new_test_film("file_naming_test", { r, g, b });
+ film->set_video_frame_rate (24);
+
+ r->set_position (film, dcpomatic::DCPTime::from_seconds(0));
+ r->set_video_frame_rate(film, 24);
+ r->video->set_length (24);
+ g->set_position (film, dcpomatic::DCPTime::from_seconds(1));
+ g->set_video_frame_rate(film, 24);
+ g->video->set_length (24);
+ b->set_position (film, dcpomatic::DCPTime::from_seconds(2));
+ b->set_video_frame_rate(film, 24);
+ b->video->set_length (24);
+
+ film->set_reel_type (ReelType::BY_VIDEO_CONTENT);
+ film->write_metadata ();
+ make_and_verify_dcp (
+ film,
+ {
+ dcp::VerificationNote::Code::MISSING_FFMC_IN_FEATURE,
+ dcp::VerificationNote::Code::MISSING_FFEC_IN_FEATURE
+ });
+
+ int got[3] = { 0, 0, 0 };
+ for (auto i: boost::filesystem::directory_iterator(film->file(film->dcp_name()))) {
+ if (boost::regex_match(i.path().string(), boost::regex(mxf_regex("red")))) {
+ ++got[0];
+ } else if (boost::regex_match(i.path().string(), boost::regex(mxf_regex("green")))) {
+ ++got[1];
+ } else if (boost::regex_match(i.path().string(), boost::regex(mxf_regex("blue")))) {
+ ++got[2];
+ }
+ }
+
+ for (int i = 0; i < 3; ++i) {
+ BOOST_CHECK (got[i] == 2);
+ }
+}
+
+
+BOOST_AUTO_TEST_CASE (file_naming_test2)
+{
+ ConfigRestorer cr;
+
+ Config::instance()->set_dcp_asset_filename_format (dcp::NameFormat ("%c"));
+
+ auto r = make_shared<FFmpegContent>("test/data/flät_red.png");
+ auto g = make_shared<FFmpegContent>("test/data/flat_green.png");
+ auto b = make_shared<FFmpegContent>("test/data/flat_blue.png");
+ auto film = new_test_film("file_naming_test2", { r, g, b });
+
+ r->set_position (film, dcpomatic::DCPTime::from_seconds(0));
+ r->set_video_frame_rate(film, 24);
+ r->video->set_length (24);
+ g->set_position (film, dcpomatic::DCPTime::from_seconds(1));
+ g->set_video_frame_rate(film, 24);
+ g->video->set_length (24);
+ b->set_position (film, dcpomatic::DCPTime::from_seconds(2));
+ b->set_video_frame_rate(film, 24);
+ b->video->set_length (24);
+
+ film->set_reel_type (ReelType::BY_VIDEO_CONTENT);
+ make_and_verify_dcp (
+ film,
+ {
+ dcp::VerificationNote::Code::MISSING_FFMC_IN_FEATURE,
+ dcp::VerificationNote::Code::MISSING_FFEC_IN_FEATURE
+ });
+
+ int got[3] = { 0, 0, 0 };
+ for (auto i: boost::filesystem::directory_iterator (film->file(film->dcp_name()))) {
+ if (boost::regex_match(i.path().string(), boost::regex(mxf_regex("red")))) {
+ ++got[0];
+ } else if (boost::regex_match(i.path().string(), boost::regex(mxf_regex("green")))) {
+ ++got[1];
+ } else if (boost::regex_match(i.path().string(), boost::regex(mxf_regex("blue")))) {
+ ++got[2];
+ }
+ }
+
+ for (int i = 0; i < 3; ++i) {
+ BOOST_CHECK (got[i] == 2);
+ }
+}
+
+
+BOOST_AUTO_TEST_CASE (subtitle_file_naming)
+{
+ ConfigRestorer cr;
+
+ Config::instance()->set_dcp_asset_filename_format(dcp::NameFormat("%t ostrabagalous %c"));
+
+ auto content = content_factory("test/data/15s.srt");
+ auto film = new_test_film("subtitle_file_naming", content);
+ film->set_interop(false);
+
+ make_and_verify_dcp (
+ film,
+ {
+ dcp::VerificationNote::Code::MISSING_CPL_METADATA,
+ dcp::VerificationNote::Code::MISSING_SUBTITLE_LANGUAGE,
+ dcp::VerificationNote::Code::INVALID_SUBTITLE_FIRST_TEXT_TIME,
+ });
+
+ int got = 0;
+
+ for (auto i: boost::filesystem::directory_iterator(film->file(film->dcp_name()))) {
+ if (boost::regex_match(i.path().filename().string(), boost::regex("sub_ostrabagalous_15s.*\\.mxf"))) {
+ ++got;
+ }
+ }
+
+ BOOST_CHECK_EQUAL(got, 1);
+}
+
+
+BOOST_AUTO_TEST_CASE(remove_bad_characters_from_template)
+{
+ ConfigRestorer cr;
+
+ /* %z is not recognised, so the % should be discarded so it won't trip
+ * an invalid URI check in make_and_verify_dcp
+ */
+ Config::instance()->set_dcp_asset_filename_format(dcp::NameFormat("%c%z"));
+
+ auto content = content_factory("test/data/flat_red.png");
+ auto film = new_test_film("remove_bad_characters_from_template", content);
+ make_and_verify_dcp(
+ film,
+ {
+ dcp::VerificationNote::Code::MISSING_FFMC_IN_FEATURE,
+ dcp::VerificationNote::Code::MISSING_FFEC_IN_FEATURE
+ });
+}
+
diff --git a/test/lib/filename_charset_test.cc b/test/lib/filename_charset_test.cc
new file mode 100644
index 000000000..d69c530ec
--- /dev/null
+++ b/test/lib/filename_charset_test.cc
@@ -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 "lib/dcp_content.h"
+#include "../test.h"
+#include <boost/filesystem.hpp>
+#include <boost/test/unit_test.hpp>
+
+
+/* Check that a DCP can be imported from a non-ASCII path */
+BOOST_AUTO_TEST_CASE(utf8_filename_handling_test)
+{
+ auto const dir = boost::filesystem::path("build/test/utf8_filename_handling_test_input/ᴟᶒḃↈ");
+ boost::filesystem::remove_all(dir);
+ boost::filesystem::create_directories(dir);
+
+ for (auto i: boost::filesystem::directory_iterator("test/data/dcp_digest_test_dcp")) {
+ boost::filesystem::copy_file(i, dir / i.path().filename());
+ }
+
+ auto content = std::make_shared<DCPContent>(dir);
+ auto film = new_test_film("utf8_filename_handling_test", { content });
+}
+
diff --git a/test/lib/film_metadata_test.cc b/test/lib/film_metadata_test.cc
new file mode 100644
index 000000000..9ec0a8399
--- /dev/null
+++ b/test/lib/film_metadata_test.cc
@@ -0,0 +1,238 @@
+/*
+ Copyright (C) 2013-2014 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 test/film_metadata_test.cc
+ * @brief Test some basic reading/writing of film metadata.
+ * @ingroup feature
+ */
+
+
+#include "lib/content.h"
+#include "lib/content_factory.h"
+#include "lib/dcp_content.h"
+#include "lib/dcp_content_type.h"
+#include "lib/film.h"
+#include "lib/ratio.h"
+#include "lib/text_content.h"
+#include "lib/video_content.h"
+#include "../test.h"
+#include <boost/date_time.hpp>
+#include <boost/filesystem.hpp>
+#include <boost/test/unit_test.hpp>
+
+
+using std::string;
+using std::list;
+using std::make_shared;
+using std::vector;
+
+
+BOOST_AUTO_TEST_CASE (film_metadata_test)
+{
+ auto film = new_test_film("film_metadata_test");
+ auto dir = test_film_dir ("film_metadata_test");
+
+ film->_isdcf_date = boost::gregorian::from_undelimited_string ("20130211");
+ BOOST_CHECK (film->container() == Ratio::from_id ("185"));
+ BOOST_CHECK (film->dcp_content_type() == DCPContentType::from_isdcf_name("TST"));
+
+ film->set_name ("fred");
+ film->set_dcp_content_type (DCPContentType::from_isdcf_name ("SHR"));
+ film->set_container (Ratio::from_id ("185"));
+ film->set_video_bit_rate(VideoEncoding::JPEG2000, 200000000);
+ film->set_interop (false);
+ film->set_chain (string(""));
+ film->set_distributor (string(""));
+ film->set_facility (string(""));
+ film->set_release_territory (dcp::LanguageTag::RegionSubtag("US"));
+ film->set_audio_channels(6);
+ film->write_metadata ();
+
+ list<Glib::ustring> ignore = { "Key", "ContextID", "LastWrittenBy" };
+ check_xml ("test/data/metadata.xml.ref", dir.string() + "/metadata.xml", ignore);
+
+ auto g = make_shared<Film>(dir);
+ g->read_metadata ();
+
+ BOOST_CHECK_EQUAL (g->name(), "fred");
+ BOOST_CHECK_EQUAL (g->dcp_content_type(), DCPContentType::from_isdcf_name ("SHR"));
+ BOOST_CHECK_EQUAL (g->container(), Ratio::from_id ("185"));
+
+ g->write_metadata ();
+ check_xml ("test/data/metadata.xml.ref", dir.string() + "/metadata.xml", ignore);
+}
+
+
+/** Check a bug where <Content> tags with multiple <Text>s would fail to load */
+BOOST_AUTO_TEST_CASE (multiple_text_nodes_are_allowed)
+{
+ Cleanup cl;
+
+ auto subs = content_factory("test/data/15s.srt")[0];
+ auto caps = content_factory("test/data/15s.srt")[0];
+ auto film = new_test_film("multiple_text_nodes_are_allowed1", { subs, caps }, &cl);
+ caps->only_text()->set_type(TextType::CLOSED_CAPTION);
+ make_and_verify_dcp (
+ film,
+ {
+ dcp::VerificationNote::Code::MISSING_CPL_METADATA,
+ dcp::VerificationNote::Code::MISSING_SUBTITLE_LANGUAGE,
+ dcp::VerificationNote::Code::INVALID_SUBTITLE_FIRST_TEXT_TIME
+ });
+
+ auto reload = make_shared<DCPContent>(film->dir(film->dcp_name()));
+ auto film2 = new_test_film("multiple_text_nodes_are_allowed2", { reload });
+ film2->write_metadata ();
+
+ auto test = make_shared<Film>(boost::filesystem::path("build/test/multiple_text_nodes_are_allowed2"));
+ test->read_metadata();
+
+ cl.run();
+}
+
+
+/** Read some metadata from v2.14.x that fails to open on 2.15.x */
+BOOST_AUTO_TEST_CASE (metadata_loads_from_2_14_x_1)
+{
+ namespace fs = boost::filesystem;
+ auto dir = fs::path("build/test/metadata_loads_from_2_14_x_1");
+ fs::remove_all(dir);
+ auto film = make_shared<Film>(dir);
+ fs::copy_file("test/data/2.14.x.metadata.1.xml", dir / "metadata.xml");
+ auto notes = film->read_metadata(dir / "metadata.xml");
+ BOOST_REQUIRE_EQUAL (notes.size(), 0U);
+}
+
+
+/** Read some more metadata from v2.14.x that fails to open on 2.15.x */
+BOOST_AUTO_TEST_CASE (metadata_loads_from_2_14_x_2)
+{
+ namespace fs = boost::filesystem;
+ auto dir = fs::path("build/test/metadata_loads_from_2_14_x_2");
+ fs::remove_all(dir);
+ auto film = make_shared<Film>(dir);
+ fs::copy_file("test/data/2.14.x.metadata.2.xml", dir / "metadata.xml");
+ auto notes = film->read_metadata(dir / "metadata.xml");
+ BOOST_REQUIRE_EQUAL (notes.size(), 1U);
+ BOOST_REQUIRE_EQUAL (notes.front(),
+ "A subtitle or closed caption file in this project is marked with the language 'eng', "
+ "which DCP-o-matic does not recognise. The file's language has been cleared."
+ );
+}
+
+
+BOOST_AUTO_TEST_CASE (metadata_loads_from_2_14_x_3)
+{
+ namespace fs = boost::filesystem;
+ auto dir = fs::path("build/test/metadata_loads_from_2_14_x_3");
+ fs::remove_all(dir);
+ auto film = make_shared<Film>(dir);
+ fs::copy_file("test/data/2.14.x.metadata.3.xml", dir / "metadata.xml");
+ auto notes = film->read_metadata(dir / "metadata.xml");
+
+ BOOST_REQUIRE (film->release_territory());
+ BOOST_REQUIRE (film->release_territory()->subtag() == dcp::LanguageTag::RegionSubtag("de").subtag());
+
+ BOOST_REQUIRE (film->audio_language());
+ BOOST_REQUIRE (*film->audio_language() == dcp::LanguageTag("sv-SE"));
+
+ BOOST_REQUIRE (film->content_versions() == vector<string>{"3"});
+ BOOST_REQUIRE (film->ratings() == vector<dcp::Rating>{ dcp::Rating("", "214rating") });
+ BOOST_REQUIRE_EQUAL (film->studio().get_value_or(""), "214studio");
+ BOOST_REQUIRE_EQUAL (film->facility().get_value_or(""), "214facility");
+ BOOST_REQUIRE_EQUAL (film->temp_version(), true);
+ BOOST_REQUIRE_EQUAL (film->pre_release(), true);
+ BOOST_REQUIRE_EQUAL (film->red_band(), true);
+ BOOST_REQUIRE_EQUAL (film->two_d_version_of_three_d(), true);
+ BOOST_REQUIRE_EQUAL (film->chain().get_value_or(""), "214chain");
+ BOOST_REQUIRE (film->luminance() == dcp::Luminance(14, dcp::Luminance::Unit::FOOT_LAMBERT));
+}
+
+
+/** Check that an empty <MasteredLuminance> tag results in the film's luminance being unset */
+BOOST_AUTO_TEST_CASE (metadata_loads_from_2_14_x_4)
+{
+ namespace fs = boost::filesystem;
+ auto dir = fs::path("build/test/metadata_loads_from_2_14_x_4");
+ fs::remove_all(dir);
+ auto film = make_shared<Film>(dir);
+ fs::copy_file("test/data/2.14.x.metadata.4.xml", dir / "metadata.xml");
+ auto notes = film->read_metadata(dir / "metadata.xml");
+
+ BOOST_REQUIRE (!film->luminance());
+}
+
+
+BOOST_AUTO_TEST_CASE (metadata_video_range_guessed_for_dcp)
+{
+ namespace fs = boost::filesystem;
+ auto film = make_shared<Film>(fs::path("test/data/214x_dcp"));
+ film->read_metadata();
+
+ BOOST_REQUIRE_EQUAL(film->content().size(), 1U);
+ BOOST_REQUIRE(film->content()[0]->video);
+ BOOST_CHECK(film->content()[0]->video->range() == VideoRange::FULL);
+}
+
+
+BOOST_AUTO_TEST_CASE (metadata_video_range_guessed_for_mp4_with_unknown_range)
+{
+ namespace fs = boost::filesystem;
+ auto film = make_shared<Film>(fs::path("test/data/214x_mp4"));
+ film->read_metadata();
+
+ BOOST_REQUIRE_EQUAL(film->content().size(), 1U);
+ BOOST_REQUIRE(film->content()[0]->video);
+ BOOST_CHECK(film->content()[0]->video->range() == VideoRange::VIDEO);
+}
+
+
+BOOST_AUTO_TEST_CASE (metadata_video_range_guessed_for_png)
+{
+ namespace fs = boost::filesystem;
+ auto film = make_shared<Film>(fs::path("test/data/214x_png"));
+ film->read_metadata();
+
+ BOOST_REQUIRE_EQUAL(film->content().size(), 1U);
+ BOOST_REQUIRE(film->content()[0]->video);
+ BOOST_CHECK(film->content()[0]->video->range() == VideoRange::FULL);
+}
+
+
+/* Bug #2581 */
+BOOST_AUTO_TEST_CASE(effect_node_not_inserted_incorrectly)
+{
+ auto sub = content_factory("test/data/15s.srt");
+ auto film = new_test_film("effect_node_not_inserted_incorrectly", sub);
+ film->write_metadata();
+
+ namespace fs = boost::filesystem;
+ auto film2 = make_shared<Film>(fs::path("build/test/effect_node_not_inserted_incorrectly"));
+ film2->read_metadata();
+ film2->write_metadata();
+
+ cxml::Document doc("Metadata");
+ doc.read_file("build/test/effect_node_not_inserted_incorrectly/metadata.xml");
+
+ /* There should be no <Effect> node in the text, since we don't want to force the effect to "none" */
+ BOOST_CHECK(!doc.node_child("Playlist")->node_child("Content")->node_child("Text")->optional_node_child("Effect"));
+}
+
diff --git a/test/lib/film_test.cc b/test/lib/film_test.cc
new file mode 100644
index 000000000..6abce5c77
--- /dev/null
+++ b/test/lib/film_test.cc
@@ -0,0 +1,88 @@
+/*
+ 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/content_factory.h"
+#include "lib/dcp_content.h"
+#include "lib/film.h"
+#include "lib/job_manager.h"
+#include "../test.h"
+#include <boost/test/unit_test.hpp>
+
+
+using std::make_shared;
+
+
+BOOST_AUTO_TEST_CASE(film_contains_atmos_content_test)
+{
+ auto atmos = content_factory("test/data/atmos_0.mxf")[0];
+ auto image = content_factory("test/data/flat_red.png")[0];
+ auto sound = content_factory("test/data/white.wav")[0];
+
+ auto film1 = new_test_film("film_contains_atmos_content_test1", { atmos, image, sound });
+ BOOST_CHECK(film1->contains_atmos_content());
+
+ auto film2 = new_test_film("film_contains_atmos_content_test2", { sound, atmos, image });
+ BOOST_CHECK(film2->contains_atmos_content());
+
+ auto film3 = new_test_film("film_contains_atmos_content_test3", { image, sound, atmos });
+ BOOST_CHECK(film3->contains_atmos_content());
+
+ auto film4 = new_test_film("film_contains_atmos_content_test4", { image, sound });
+ BOOST_CHECK(!film4->contains_atmos_content());
+}
+
+
+BOOST_AUTO_TEST_CASE(film_possible_reel_types_test1)
+{
+ auto film = new_test_film("film_possible_reel_types_test1");
+ BOOST_CHECK_EQUAL(film->possible_reel_types().size(), 4U);
+
+ film->examine_and_add_content(content_factory("test/data/flat_red.png")[0]);
+ BOOST_REQUIRE(!wait_for_jobs());
+ BOOST_CHECK_EQUAL(film->possible_reel_types().size(), 4U);
+
+ auto dcp = make_shared<DCPContent>("test/data/reels_test2");
+ film->examine_and_add_content(dcp);
+ BOOST_REQUIRE(!wait_for_jobs());
+ BOOST_CHECK_EQUAL(film->possible_reel_types().size(), 4U);
+
+ /* If we don't do this the set_reference_video will be overridden by the Film's
+ * check_settings_consistency() stuff.
+ */
+ film->set_reel_type(ReelType::BY_VIDEO_CONTENT);
+ dcp->set_reference_video(true);
+ BOOST_CHECK_EQUAL(film->possible_reel_types().size(), 1U);
+}
+
+
+BOOST_AUTO_TEST_CASE(film_possible_reel_types_test2)
+{
+ auto film = new_test_film("film_possible_reel_types_test2");
+
+ auto dcp = make_shared<DCPContent>("test/data/dcp_digest_test_dcp");
+ film->examine_and_add_content(dcp);
+ BOOST_REQUIRE(!wait_for_jobs());
+ BOOST_CHECK_EQUAL(film->possible_reel_types().size(), 4U);
+
+ dcp->set_reference_video(true);
+ BOOST_CHECK_EQUAL(film->possible_reel_types().size(), 2U);
+}
+
diff --git a/test/lib/find_missing_test.cc b/test/lib/find_missing_test.cc
new file mode 100644
index 000000000..b9bef8055
--- /dev/null
+++ b/test/lib/find_missing_test.cc
@@ -0,0 +1,222 @@
+/*
+ Copyright (C) 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 "lib/content.h"
+#include "lib/content_factory.h"
+#include "lib/dcp_content.h"
+#include "lib/film.h"
+#include "lib/find_missing.h"
+#include "../test.h"
+#include <boost/filesystem.hpp>
+#include <boost/test/unit_test.hpp>
+
+
+using std::make_shared;
+using std::shared_ptr;
+using std::string;
+
+
+BOOST_AUTO_TEST_CASE (find_missing_test_with_single_files)
+{
+ using namespace boost::filesystem;
+
+ auto name = string{"find_missing_test_with_single_files"};
+
+ /* Make a directory with some content */
+ auto content_dir = path("build/test") / path(name + "_content");
+ remove_all (content_dir);
+ create_directories (content_dir);
+ copy_file ("test/data/flat_red.png", content_dir / "A.png");
+ copy_file ("test/data/flat_red.png", content_dir / "B.png");
+ copy_file ("test/data/flat_red.png", content_dir / "C.png");
+
+ /* Make a film with that content */
+ auto film = new_test_film(name + "_film", {
+ content_factory(content_dir / "A.png")[0],
+ content_factory(content_dir / "B.png")[0],
+ content_factory(content_dir / "C.png")[0]
+ });
+ film->write_metadata ();
+
+ /* Move the content somewhere else */
+ auto moved = path("build/test") / path(name + "_moved");
+ remove_all (moved);
+ rename (content_dir, moved);
+
+ /* That should make the content paths invalid */
+ for (auto content: film->content()) {
+ BOOST_CHECK(!paths_exist(content->paths()));
+ }
+
+ /* Fix the missing files and check the result */
+ dcpomatic::find_missing (film->content(), moved / "A.png");
+
+ for (auto content: film->content()) {
+ BOOST_CHECK(paths_exist(content->paths()));
+ }
+}
+
+
+BOOST_AUTO_TEST_CASE (find_missing_test_with_multiple_files)
+{
+ using namespace boost::filesystem;
+
+ auto name = string{"find_missing_test_with_multiple_files"};
+
+ /* Copy an arbitrary DCP into a test directory */
+ auto content_dir = path("build/test") / path(name + "_content");
+ remove_all (content_dir);
+ create_directories (content_dir);
+ for (auto ref: directory_iterator("test/data/scaling_test_133_185")) {
+ copy (ref, content_dir / ref.path().filename());
+ }
+
+ /* Make a film containing that DCP */
+ auto film = new_test_film(name + "_film", { make_shared<DCPContent>(content_dir) });
+ film->write_metadata ();
+
+ /* Move the DCP's content elsewhere */
+ auto moved = path("build/test") / path(name + "_moved");
+ remove_all (moved);
+ rename (content_dir, moved);
+
+ /* That should make the content paths invalid */
+ for (auto content: film->content()) {
+ BOOST_CHECK(!paths_exist(content->paths()));
+ }
+
+ /* Fix the missing files and check the result */
+ dcpomatic::find_missing (film->content(), moved / "foo");
+
+ for (auto content: film->content()) {
+ BOOST_CHECK(paths_exist(content->paths()));
+ }
+}
+
+
+BOOST_AUTO_TEST_CASE (find_missing_test_with_multiple_files_one_incorrect)
+{
+ using namespace boost::filesystem;
+
+ auto name = string{"find_missing_test_with_multiple_files_one_incorrect"};
+
+ /* Copy an arbitrary DCP into a test directory */
+ auto content_dir = path("build/test") / path(name + "_content");
+ remove_all (content_dir);
+ create_directories (content_dir);
+ for (auto ref: directory_iterator("test/data/scaling_test_133_185")) {
+ copy (ref, content_dir / ref.path().filename());
+ }
+
+ /* Make a film containing that DCP */
+ auto film = new_test_film(name + "_film", { make_shared<DCPContent>(content_dir) });
+ film->write_metadata ();
+
+ /* Move the DCP's content elsewhere */
+ auto moved = path("build/test") / path(name + "_moved");
+ remove_all (moved);
+ rename (content_dir, moved);
+
+ /* Corrupt one of the files in the moved content, so that it should not be found in the find_missing
+ * step
+ */
+ auto cpl = find_file(moved, "cpl_");
+ remove (cpl);
+ copy ("test/data/scaling_test_133_185/ASSETMAP.xml", cpl);
+
+ /* The film's contents should be invalid */
+ for (auto content: film->content()) {
+ BOOST_CHECK(!paths_exist(content->paths()));
+ }
+
+ dcpomatic::find_missing (film->content(), moved / "foo");
+
+ /* And even after find_missing there should still be missing content */
+ for (auto content: film->content()) {
+ BOOST_CHECK(!paths_exist(content->paths()));
+ }
+}
+
+
+BOOST_AUTO_TEST_CASE(find_missing_test_with_rename)
+{
+ using namespace boost::filesystem;
+
+ auto name = string{"find_missing_test_with_rename"};
+
+ /* Make a directory with some content */
+ auto content_dir = path("build/test") / path(name + "_content");
+ remove_all(content_dir);
+ create_directories(content_dir);
+ copy_file("test/data/flat_red.png", content_dir / "A.png");
+ copy_file("test/data/flat_red.png", content_dir / "B.png");
+ copy_file("test/data/flat_red.png", content_dir / "C.png");
+
+ /* Make a film with that content */
+ auto film = new_test_film(name + "_film", {
+ content_factory(content_dir / "A.png")[0],
+ content_factory(content_dir / "B.png")[0],
+ content_factory(content_dir / "C.png")[0]
+ });
+ film->write_metadata();
+
+ /* Rename one of the files */
+ rename(content_dir / "C.png", content_dir / "bogus.png");
+
+ /* That should make one of the content paths invalid */
+ auto content_list = film->content();
+ int const valid = std::count_if(content_list.begin(), content_list.end(), [](shared_ptr<const Content> content) {
+ return paths_exist(content->paths());
+ });
+ BOOST_CHECK_EQUAL(valid, 2);
+
+ /* Fix the missing files and check the result */
+ dcpomatic::find_missing(content_list, content_dir / "bogus.png");
+
+ for (auto content: content_list) {
+ BOOST_CHECK(paths_exist(content->paths()));
+ }
+
+}
+
+
+BOOST_AUTO_TEST_CASE(test_film_saved_on_windows)
+{
+ auto film = make_shared<Film>(boost::filesystem::path("test/data/windows_film"));
+ film->read_metadata();
+ dcpomatic::find_missing(film->content(), TestPaths::private_data());
+
+ for (auto content: film->content()) {
+ BOOST_CHECK(paths_exist(content->paths()));
+ }
+}
+
+
+BOOST_AUTO_TEST_CASE(test_film_saved_on_posix)
+{
+ auto film = make_shared<Film>(boost::filesystem::path("test/data/posix_film"));
+ film->read_metadata();
+ dcpomatic::find_missing(film->content(), TestPaths::private_data());
+
+ for (auto content: film->content()) {
+ BOOST_CHECK(paths_exist(content->paths()));
+ }
+}
diff --git a/test/lib/font_comparator_test.cc b/test/lib/font_comparator_test.cc
new file mode 100644
index 000000000..dad058a74
--- /dev/null
+++ b/test/lib/font_comparator_test.cc
@@ -0,0 +1,27 @@
+#include "lib/font.h"
+#include "lib/font_comparator.h"
+#include <boost/test/unit_test.hpp>
+#include <iostream>
+
+
+using std::make_shared;
+using std::map;
+using std::shared_ptr;
+using std::string;
+
+
+BOOST_AUTO_TEST_CASE(font_comparator_test)
+{
+ map<dcpomatic::Font::Content, string, FontComparator> cache;
+
+ auto font = make_shared<dcpomatic::Font>("foo");
+
+ BOOST_CHECK(cache.find(font->content()) == cache.end());
+ cache[font->content()] = "foo";
+ BOOST_CHECK(cache.find(font->content()) != cache.end());
+
+ font->set_file("test/data/Inconsolata-VF.ttf");
+ BOOST_CHECK(cache.find(font->content()) == cache.end());
+}
+
+
diff --git a/test/lib/font_id_allocator_test.cc b/test/lib/font_id_allocator_test.cc
new file mode 100644
index 000000000..19c4a2154
--- /dev/null
+++ b/test/lib/font_id_allocator_test.cc
@@ -0,0 +1,100 @@
+/*
+ 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/font_id_allocator.h"
+#include <boost/test/unit_test.hpp>
+
+
+using std::string;
+using std::vector;
+
+
+BOOST_AUTO_TEST_CASE(font_id_allocator_test_without_disambiguation)
+{
+ FontIDAllocator allocator;
+
+ /* Reel 0 has just one asset with two fonts */
+ allocator.add_font(0, "asset1", "font");
+ allocator.add_font(0, "asset1", "font2");
+
+ /* Reel 1 has two assets each with two more fonts */
+ allocator.add_font(1, "asset2", "font");
+ allocator.add_font(1, "asset2", "font2");
+ allocator.add_font(1, "asset3", "font3");
+ allocator.add_font(1, "asset3", "font4");
+
+ allocator.allocate();
+
+ BOOST_CHECK_EQUAL(allocator.font_id(0, "asset1", "font"), "font");
+ BOOST_CHECK_EQUAL(allocator.font_id(0, "asset1", "font2"), "font2");
+ BOOST_CHECK_EQUAL(allocator.font_id(1, "asset2", "font"), "0_font");
+ BOOST_CHECK_EQUAL(allocator.font_id(1, "asset2", "font2"), "0_font2");
+ BOOST_CHECK_EQUAL(allocator.font_id(1, "asset3", "font3"), "font3");
+ BOOST_CHECK_EQUAL(allocator.font_id(1, "asset3", "font4"), "font4");
+}
+
+
+BOOST_AUTO_TEST_CASE(font_id_allocator_test_with_disambiguation)
+{
+ FontIDAllocator allocator;
+
+ /* Reel 0 has two assets each with a font with the same ID (perhaps a subtitle and a ccap).
+ * This would have crashed DCP-o-matic before the FontIDAllocator change (bug #2600)
+ * so it's OK if the second font gets a new index that we didn't see before.
+ */
+ allocator.add_font(0, "asset1", "font");
+ allocator.add_font(0, "asset2", "font");
+
+ /* Reel 1 has one asset with another font */
+ allocator.add_font(1, "asset3", "font1");
+
+ allocator.allocate();
+
+ BOOST_CHECK(allocator.font_id(0, "asset1", "font") == "font");
+ BOOST_CHECK(allocator.font_id(0, "asset2", "font") == "0_font");
+ BOOST_CHECK(allocator.font_id(1, "asset3", "font1") == "font1");
+}
+
+
+/* Bug #2822: multiple reels, each with subs + closed captions, and each using the same
+ * basic font ID.
+ */
+BOOST_AUTO_TEST_CASE(font_id_allocator_test_with_disambiguation2)
+{
+ FontIDAllocator allocator;
+
+ allocator.add_font(0, "asset1", "font");
+ allocator.add_font(0, "asset2", "font");
+
+ allocator.add_font(1, "asset1", "font");
+ allocator.add_font(1, "asset2", "font");
+
+ allocator.allocate();
+ vector<string> ids = {
+ allocator.font_id(0, "asset1", "font"),
+ allocator.font_id(0, "asset2", "font"),
+ allocator.font_id(1, "asset1", "font"),
+ allocator.font_id(1, "asset2", "font")
+ };
+
+ std::sort(ids.begin(), ids.end());
+ BOOST_CHECK(std::adjacent_find(ids.begin(), ids.end()) == ids.end());
+}
diff --git a/test/lib/frame_interval_checker_test.cc b/test/lib/frame_interval_checker_test.cc
new file mode 100644
index 000000000..4b92d33d6
--- /dev/null
+++ b/test/lib/frame_interval_checker_test.cc
@@ -0,0 +1,140 @@
+/*
+ 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/frame_interval_checker.h"
+#include <boost/test/unit_test.hpp>
+
+using namespace dcpomatic;
+
+/** Test of 2D-ish frame timings */
+BOOST_AUTO_TEST_CASE (frame_interval_checker_test1)
+{
+ FrameIntervalChecker checker;
+ ContentTime t(3888);
+ checker.feed (t, 24);
+ BOOST_CHECK (checker.guess() == FrameIntervalChecker::AGAIN);
+ t += ContentTime(4012);
+ checker.feed (t, 24);
+ BOOST_CHECK (checker.guess() == FrameIntervalChecker::AGAIN);
+ t += ContentTime(4000);
+ checker.feed (t, 24);
+ BOOST_CHECK (checker.guess() == FrameIntervalChecker::AGAIN);
+ t += ContentTime(4000);
+ checker.feed (t, 24);
+ BOOST_CHECK (checker.guess() == FrameIntervalChecker::AGAIN);
+ t += ContentTime(3776);
+ checker.feed (t, 24);
+ BOOST_CHECK (checker.guess() == FrameIntervalChecker::AGAIN);
+ t += ContentTime(3779);
+ checker.feed (t, 24);
+ BOOST_CHECK (checker.guess() == FrameIntervalChecker::AGAIN);
+ t += ContentTime(4010);
+ checker.feed (t, 24);
+ BOOST_CHECK (checker.guess() == FrameIntervalChecker::AGAIN);
+ t += ContentTime(4085);
+ checker.feed (t, 24);
+ BOOST_CHECK (checker.guess() == FrameIntervalChecker::AGAIN);
+ t += ContentTime(4085);
+ checker.feed (t, 24);
+ BOOST_CHECK (checker.guess() == FrameIntervalChecker::AGAIN);
+ t += ContentTime(4012);
+ checker.feed (t, 24);
+ BOOST_CHECK (checker.guess() == FrameIntervalChecker::AGAIN);
+ t += ContentTime(4000);
+ checker.feed (t, 24);
+ BOOST_CHECK (checker.guess() == FrameIntervalChecker::AGAIN);
+ t += ContentTime(4000);
+ checker.feed (t, 24);
+ BOOST_CHECK (checker.guess() == FrameIntervalChecker::AGAIN);
+ t += ContentTime(3776);
+ checker.feed (t, 24);
+ BOOST_CHECK (checker.guess() == FrameIntervalChecker::AGAIN);
+ t += ContentTime(3779);
+ checker.feed (t, 24);
+ BOOST_CHECK (checker.guess() == FrameIntervalChecker::AGAIN);
+ t += ContentTime(4010);
+ checker.feed (t, 24);
+ BOOST_CHECK (checker.guess() == FrameIntervalChecker::AGAIN);
+ t += ContentTime(4085);
+ checker.feed (t, 24);
+ BOOST_CHECK (checker.guess() == FrameIntervalChecker::AGAIN);
+ t += ContentTime(4085);
+ checker.feed (t, 24);
+ BOOST_CHECK (checker.guess() == FrameIntervalChecker::PROBABLY_NOT_3D);
+}
+
+/** Test of 3D-ish frame timings */
+BOOST_AUTO_TEST_CASE (frame_interval_checker_test2)
+{
+ FrameIntervalChecker checker;
+ ContentTime t(3888);
+ checker.feed (t, 24);
+ BOOST_CHECK (checker.guess() == FrameIntervalChecker::AGAIN);
+ t += ContentTime(0);
+ checker.feed (t, 24);
+ BOOST_CHECK (checker.guess() == FrameIntervalChecker::AGAIN);
+ t += ContentTime(4000);
+ checker.feed (t, 24);
+ BOOST_CHECK (checker.guess() == FrameIntervalChecker::AGAIN);
+ t += ContentTime(0);
+ checker.feed (t, 24);
+ BOOST_CHECK (checker.guess() == FrameIntervalChecker::AGAIN);
+ t += ContentTime(3776);
+ checker.feed (t, 24);
+ BOOST_CHECK (checker.guess() == FrameIntervalChecker::AGAIN);
+ t += ContentTime(50);
+ checker.feed (t, 24);
+ BOOST_CHECK (checker.guess() == FrameIntervalChecker::AGAIN);
+ t += ContentTime(4010);
+ checker.feed (t, 24);
+ BOOST_CHECK (checker.guess() == FrameIntervalChecker::AGAIN);
+ t += ContentTime(2);
+ checker.feed (t, 24);
+ BOOST_CHECK (checker.guess() == FrameIntervalChecker::AGAIN);
+ t += ContentTime(4011);
+ checker.feed (t, 24);
+ BOOST_CHECK (checker.guess() == FrameIntervalChecker::AGAIN);
+ t += ContentTime(0);
+ checker.feed (t, 24);
+ BOOST_CHECK (checker.guess() == FrameIntervalChecker::AGAIN);
+ t += ContentTime(4000);
+ checker.feed (t, 24);
+ BOOST_CHECK (checker.guess() == FrameIntervalChecker::AGAIN);
+ t += ContentTime(0);
+ checker.feed (t, 24);
+ BOOST_CHECK (checker.guess() == FrameIntervalChecker::AGAIN);
+ t += ContentTime(3776);
+ checker.feed (t, 24);
+ BOOST_CHECK (checker.guess() == FrameIntervalChecker::AGAIN);
+ t += ContentTime(50);
+ checker.feed (t, 24);
+ BOOST_CHECK (checker.guess() == FrameIntervalChecker::AGAIN);
+ t += ContentTime(4010);
+ checker.feed (t, 24);
+ BOOST_CHECK (checker.guess() == FrameIntervalChecker::AGAIN);
+ t += ContentTime(2);
+ checker.feed (t, 24);
+ BOOST_CHECK (checker.guess() == FrameIntervalChecker::AGAIN);
+ t += ContentTime(4011);
+ checker.feed (t, 24);
+ BOOST_CHECK (checker.guess() == FrameIntervalChecker::PROBABLY_3D);
+}
+
+
diff --git a/test/lib/frame_rate_test.cc b/test/lib/frame_rate_test.cc
new file mode 100644
index 000000000..99db2be68
--- /dev/null
+++ b/test/lib/frame_rate_test.cc
@@ -0,0 +1,296 @@
+/*
+ Copyright (C) 2012-2016 Carl Hetherington <cth@carlh.net>
+
+ This file is part of DCP-o-matic.
+
+ DCP-o-matic is free software; you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation; either version 2 of the License, or
+ (at your option) any later version.
+
+ DCP-o-matic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with DCP-o-matic. If not, see <http://www.gnu.org/licenses/>.
+
+*/
+
+
+/** @file test/frame_rate_test.cc
+ * @brief Tests for FrameRateChange and the computation of the best
+ * frame rate for the DCP.
+ * @ingroup feature
+ */
+
+
+#include "lib/audio_content.h"
+#include "lib/config.h"
+#include "lib/ffmpeg_audio_stream.h"
+#include "lib/ffmpeg_content.h"
+#include "lib/film.h"
+#include "lib/frame_rate_change.h"
+#include "lib/playlist.h"
+#include "lib/video_content.h"
+#include "../test.h"
+#include <boost/test/unit_test.hpp>
+
+
+
+/* Test Playlist::best_dcp_frame_rate and FrameRateChange
+ with a single piece of content.
+*/
+BOOST_AUTO_TEST_CASE (best_dcp_frame_rate_test_single)
+{
+ auto content = std::make_shared<FFmpegContent>("test/data/test.mp4");
+ auto film = new_test_film("best_dcp_frame_rate_test_single", { content });
+
+ /* Run some tests with a limited range of allowed rates */
+
+ std::list<int> afr;
+ afr.push_back (24);
+ afr.push_back (25);
+ afr.push_back (30);
+ Config::instance()->set_allowed_dcp_frame_rates (afr);
+
+ content->_video_frame_rate = 60;
+ int best = film->best_video_frame_rate ();
+ auto frc = FrameRateChange(60, best);
+ BOOST_CHECK_EQUAL (best, 30);
+ BOOST_CHECK_EQUAL (frc.skip, true);
+ BOOST_CHECK_EQUAL (frc.repeat, 1);
+ BOOST_CHECK_EQUAL (frc.change_speed, false);
+ BOOST_CHECK_CLOSE (frc.speed_up, 1, 0.1);
+
+ content->_video_frame_rate = 50;
+ best = film->best_video_frame_rate ();
+ frc = FrameRateChange (50, best);
+ BOOST_CHECK_EQUAL (best, 25);
+ BOOST_CHECK_EQUAL (frc.skip, true);
+ BOOST_CHECK_EQUAL (frc.repeat, 1);
+ BOOST_CHECK_EQUAL (frc.change_speed, false);
+ BOOST_CHECK_CLOSE (frc.speed_up, 1, 0.1);
+
+ content->_video_frame_rate = 48;
+ best = film->best_video_frame_rate ();
+ frc = FrameRateChange (48, best);
+ BOOST_CHECK_EQUAL (best, 24);
+ BOOST_CHECK_EQUAL (frc.skip, true);
+ BOOST_CHECK_EQUAL (frc.repeat, 1);
+ BOOST_CHECK_EQUAL (frc.change_speed, false);
+ BOOST_CHECK_CLOSE (frc.speed_up, 1, 0.1);
+
+ content->_video_frame_rate = 30;
+ best = film->best_video_frame_rate ();
+ frc = FrameRateChange (30, best);
+ BOOST_CHECK_EQUAL (best, 30);
+ BOOST_CHECK_EQUAL (frc.skip, false);
+ BOOST_CHECK_EQUAL (frc.repeat, 1);
+ BOOST_CHECK_EQUAL (frc.change_speed, false);
+ BOOST_CHECK_CLOSE (frc.speed_up, 1, 0.1);
+
+ content->_video_frame_rate = 29.97;
+ best = film->best_video_frame_rate ();
+ frc = FrameRateChange (29.97, best);
+ BOOST_CHECK_EQUAL (best, 30);
+ BOOST_CHECK_EQUAL (frc.skip, false);
+ BOOST_CHECK_EQUAL (frc.repeat, 1);
+ BOOST_CHECK_EQUAL (frc.change_speed, true);
+ BOOST_CHECK_CLOSE (frc.speed_up, 30 / 29.97, 0.1);
+
+ content->_video_frame_rate = 25;
+ best = film->best_video_frame_rate ();
+ frc = FrameRateChange (25, best);
+ BOOST_CHECK_EQUAL (best, 25);
+ BOOST_CHECK_EQUAL (frc.skip, false);
+ BOOST_CHECK_EQUAL (frc.repeat, 1);
+ BOOST_CHECK_EQUAL (frc.change_speed, false);
+ BOOST_CHECK_CLOSE (frc.speed_up, 1, 0.1);
+
+ content->_video_frame_rate = 24;
+ best = film->best_video_frame_rate ();
+ frc = FrameRateChange (24, best);
+ BOOST_CHECK_EQUAL (best, 24);
+ BOOST_CHECK_EQUAL (frc.skip, false);
+ BOOST_CHECK_EQUAL (frc.repeat, 1);
+ BOOST_CHECK_EQUAL (frc.change_speed, false);
+ BOOST_CHECK_CLOSE (frc.speed_up, 1, 0.1);
+
+ content->_video_frame_rate = 14.5;
+ best = film->best_video_frame_rate ();
+ frc = FrameRateChange (14.5, best);
+ BOOST_CHECK_EQUAL (best, 30);
+ BOOST_CHECK_EQUAL (frc.skip, false);
+ BOOST_CHECK_EQUAL (frc.repeat, 2);
+ BOOST_CHECK_EQUAL (frc.change_speed, true);
+ BOOST_CHECK_CLOSE (frc.speed_up, 15 / 14.5, 0.1);
+
+ content->_video_frame_rate = 12.6;
+ best = film->best_video_frame_rate ();
+ frc = FrameRateChange (12.6, best);
+ BOOST_CHECK_EQUAL (best, 25);
+ BOOST_CHECK_EQUAL (frc.skip, false);
+ BOOST_CHECK_EQUAL (frc.repeat, 2);
+ BOOST_CHECK_EQUAL (frc.change_speed, true);
+ BOOST_CHECK_CLOSE (frc.speed_up, 25 / 25.2, 0.1);
+
+ content->_video_frame_rate = 12.4;
+ best = film->best_video_frame_rate ();
+ frc = FrameRateChange (12.4, best);
+ BOOST_CHECK_EQUAL (best, 25);
+ BOOST_CHECK_EQUAL (frc.skip, false);
+ BOOST_CHECK_EQUAL (frc.repeat, 2);
+ BOOST_CHECK_EQUAL (frc.change_speed, true);
+ BOOST_CHECK_CLOSE (frc.speed_up, 25 / 24.8, 0.1);
+
+ content->_video_frame_rate = 12;
+ best = film->best_video_frame_rate ();
+ frc = FrameRateChange (12, best);
+ BOOST_CHECK_EQUAL (best, 24);
+ BOOST_CHECK_EQUAL (frc.skip, false);
+ BOOST_CHECK_EQUAL (frc.repeat, 2);
+ BOOST_CHECK_EQUAL (frc.change_speed, false);
+ BOOST_CHECK_CLOSE (frc.speed_up, 1, 0.1);
+
+ /* Now add some more rates and see if it will use them
+ in preference to skip/repeat.
+ */
+
+ afr.push_back (48);
+ afr.push_back (50);
+ afr.push_back (60);
+ Config::instance()->set_allowed_dcp_frame_rates (afr);
+
+ content->_video_frame_rate = 60;
+ best = film->playlist()->best_video_frame_rate ();
+ frc = FrameRateChange (60, best);
+ BOOST_CHECK_EQUAL (best, 60);
+ BOOST_CHECK_EQUAL (frc.skip, false);
+ BOOST_CHECK_EQUAL (frc.repeat, 1);
+ BOOST_CHECK_EQUAL (frc.change_speed, false);
+ BOOST_CHECK_CLOSE (frc.speed_up, 1, 0.1);
+
+ content->_video_frame_rate = 50;
+ best = film->playlist()->best_video_frame_rate ();
+ frc = FrameRateChange (50, best);
+ BOOST_CHECK_EQUAL (best, 50);
+ BOOST_CHECK_EQUAL (frc.skip, false);
+ BOOST_CHECK_EQUAL (frc.repeat, 1);
+ BOOST_CHECK_EQUAL (frc.change_speed, false);
+ BOOST_CHECK_CLOSE (frc.speed_up, 1, 0.1);
+
+ content->_video_frame_rate = 48;
+ best = film->playlist()->best_video_frame_rate ();
+ frc = FrameRateChange (48, best);
+ BOOST_CHECK_EQUAL (best, 48);
+ BOOST_CHECK_EQUAL (frc.skip, false);
+ BOOST_CHECK_EQUAL (frc.repeat, 1);
+ BOOST_CHECK_EQUAL (frc.change_speed, false);
+ BOOST_CHECK_CLOSE (frc.speed_up, 1, 0.1);
+
+ /* Check some out-there conversions (not the best) */
+
+ frc = FrameRateChange (14.99, 24);
+ BOOST_CHECK_EQUAL (frc.skip, false);
+ BOOST_CHECK_EQUAL (frc.repeat, 2);
+ BOOST_CHECK_EQUAL (frc.change_speed, true);
+ BOOST_CHECK_CLOSE (frc.speed_up, 24 / (2 * 14.99), 0.1);
+
+ /* Check some conversions with limited DCP targets */
+
+ afr.clear ();
+ afr.push_back (24);
+ Config::instance()->set_allowed_dcp_frame_rates (afr);
+
+ content->_video_frame_rate = 25;
+ best = film->best_video_frame_rate ();
+ frc = FrameRateChange (25, best);
+ BOOST_CHECK_EQUAL (best, 24);
+ BOOST_CHECK_EQUAL (frc.skip, false);
+ BOOST_CHECK_EQUAL (frc.repeat, 1);
+ BOOST_CHECK_EQUAL (frc.change_speed, true);
+ BOOST_CHECK_CLOSE (frc.speed_up, 24.0 / 25, 0.1);
+}
+
+/* Test Playlist::best_dcp_frame_rate and FrameRateChange
+ with two pieces of content.
+*/
+BOOST_AUTO_TEST_CASE (best_dcp_frame_rate_test_double)
+{
+ auto A = std::make_shared<FFmpegContent>("test/data/test.mp4");
+ auto B = std::make_shared<FFmpegContent>("test/data/test.mp4");
+ auto film = new_test_film("best_dcp_frame_rate_test_double", { A, B });
+
+ /* Run some tests with a limited range of allowed rates */
+
+ std::list<int> afr = { 24, 25, 30 };
+ Config::instance()->set_allowed_dcp_frame_rates (afr);
+
+ A->_video_frame_rate = 30;
+ B->_video_frame_rate = 24;
+ BOOST_CHECK_EQUAL (film->best_video_frame_rate(), 25);
+
+ A->_video_frame_rate = 24;
+ B->_video_frame_rate = 24;
+ BOOST_CHECK_EQUAL (film->best_video_frame_rate(), 24);
+
+ A->_video_frame_rate = 24;
+ B->_video_frame_rate = 48;
+ BOOST_CHECK_EQUAL (film->best_video_frame_rate(), 24);
+}
+
+BOOST_AUTO_TEST_CASE (audio_sampling_rate_test)
+{
+ auto content = std::make_shared<FFmpegContent>("test/data/test.mp4");
+ auto film = new_test_film("audio_sampling_rate_test", { content });
+
+ std::list<int> afr = { 24, 25, 30 };
+ Config::instance()->set_allowed_dcp_frame_rates (afr);
+
+ auto stream = std::make_shared<FFmpegAudioStream>("foo", 0, 0, 0, 0, 0);
+ content->audio.reset (new AudioContent (content.get()));
+ content->audio->add_stream (stream);
+ content->_video_frame_rate = 24;
+ film->set_video_frame_rate (24);
+ stream->_frame_rate = 48000;
+ BOOST_CHECK_EQUAL (content->audio->resampled_frame_rate(film), 48000);
+
+ stream->_frame_rate = 44100;
+ BOOST_CHECK_EQUAL (content->audio->resampled_frame_rate(film), 48000);
+
+ stream->_frame_rate = 80000;
+ BOOST_CHECK_EQUAL (content->audio->resampled_frame_rate(film), 48000);
+
+ content->_video_frame_rate = 23.976;
+ film->set_video_frame_rate (24);
+ stream->_frame_rate = 48000;
+ BOOST_CHECK_EQUAL (content->audio->resampled_frame_rate(film), 47952);
+
+ content->_video_frame_rate = 29.97;
+ film->set_video_frame_rate (30);
+ BOOST_CHECK_EQUAL (film->video_frame_rate (), 30);
+ stream->_frame_rate = 48000;
+ BOOST_CHECK_EQUAL (content->audio->resampled_frame_rate(film), 47952);
+
+ content->_video_frame_rate = 25;
+ film->set_video_frame_rate (24);
+ stream->_frame_rate = 48000;
+ BOOST_CHECK_EQUAL (content->audio->resampled_frame_rate(film), 50000);
+
+ content->_video_frame_rate = 25;
+ film->set_video_frame_rate (24);
+ stream->_frame_rate = 44100;
+ BOOST_CHECK_EQUAL (content->audio->resampled_frame_rate(film), 50000);
+
+ /* Check some out-there conversions (not the best) */
+
+ content->_video_frame_rate = 14.99;
+ film->set_video_frame_rate (25);
+ stream->_frame_rate = 16000;
+ /* The FrameRateChange within resampled_frame_rate should choose to double-up
+ the 14.99 fps video to 30 and then run it slow at 25.
+ */
+ BOOST_CHECK_EQUAL (content->audio->resampled_frame_rate(film), lrint (48000 * 2 * 14.99 / 25));
+}
diff --git a/test/lib/grok_util_test.cc b/test/lib/grok_util_test.cc
new file mode 100644
index 000000000..435f3f449
--- /dev/null
+++ b/test/lib/grok_util_test.cc
@@ -0,0 +1,43 @@
+/*
+ Copyright (C) 2025 Carl Hetherington <cth@carlh.net>
+
+ This file is part of DCP-o-matic.
+
+ DCP-o-matic is free software; you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation; either version 2 of the License, or
+ (at your option) any later version.
+
+ DCP-o-matic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with DCP-o-matic. If not, see <http://www.gnu.org/licenses/>.
+
+*/
+
+
+#include "lib/config.h"
+#include "lib/grok/util.h"
+#include "../test.h"
+#include <boost/test/unit_test.hpp>
+
+
+#ifdef DCPOMATIC_GROK
+BOOST_AUTO_TEST_CASE(get_gpu_names_test)
+{
+ ConfigRestorer cr;
+
+ Config::Grok grok;
+ grok.binary_location = "test";
+ Config::instance()->set_grok(grok);
+
+ auto names = get_gpu_names();
+ BOOST_REQUIRE_EQUAL(names.size(), 3U);
+ BOOST_CHECK_EQUAL(names[0], "Foo bar baz");
+ BOOST_CHECK_EQUAL(names[1], "Spondoolix Mega Kompute 2000");
+ BOOST_CHECK_EQUAL(names[2], "Energy Sink-o-matic");
+}
+#endif
diff --git a/test/lib/guess_crop_test.cc b/test/lib/guess_crop_test.cc
new file mode 100644
index 000000000..bf199dd64
--- /dev/null
+++ b/test/lib/guess_crop_test.cc
@@ -0,0 +1,70 @@
+/*
+ Copyright (C) 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 "lib/content.h"
+#include "lib/content_factory.h"
+#include "lib/dcp_content.h"
+#include "lib/guess_crop.h"
+#include "lib/video_content.h"
+#include "../test.h"
+#include <boost/test/unit_test.hpp>
+#include <vector>
+
+
+using std::make_shared;
+using namespace dcpomatic;
+
+
+BOOST_AUTO_TEST_CASE (guess_crop_image_test1)
+{
+ auto content = content_factory(TestPaths::private_data() / "arrietty_724.tiff");
+ auto film = new_test_film("guess_crop_image_test1", content);
+
+ BOOST_CHECK(guess_crop_by_brightness(film, content[0], 0.1, {}) == Crop(0, 0, 11, 11));
+}
+
+
+BOOST_AUTO_TEST_CASE (guess_crop_image_test2)
+{
+ auto content = content_factory(TestPaths::private_data() / "prophet_frame.tiff");
+ auto film = new_test_film("guess_crop_image_test2", content);
+
+ BOOST_CHECK(guess_crop_by_brightness(film, content[0], 0.1, {}) == Crop(0, 0, 22, 22));
+}
+
+
+BOOST_AUTO_TEST_CASE (guess_crop_image_test3)
+{
+ auto content = content_factory(TestPaths::private_data() / "pillarbox.png");
+ auto film = new_test_film("guess_crop_image_test3", content);
+
+ BOOST_CHECK(guess_crop_by_brightness(film, content[0], 0.1, {}) == Crop(113, 262, 0, 0));
+}
+
+
+BOOST_AUTO_TEST_CASE(guess_crop_image_dcp_test)
+{
+ auto content = make_shared<DCPContent>("test/data/scaling_test_133_185");
+ auto film = new_test_film("guess_crop_image_dcp_test", { content });
+
+ BOOST_CHECK(guess_crop_by_brightness(film, content, 0.1, {}) == Crop(279, 279, 0, 0));
+}
+
diff --git a/test/lib/hints_test.cc b/test/lib/hints_test.cc
new file mode 100644
index 000000000..14d79c1df
--- /dev/null
+++ b/test/lib/hints_test.cc
@@ -0,0 +1,311 @@
+/*
+ Copyright (C) 2020-2022 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/audio_content.h"
+#include "lib/config.h"
+#include "lib/constants.h"
+#include "lib/content.h"
+#include "lib/content_factory.h"
+#include "lib/cross.h"
+#include "lib/film.h"
+#include "lib/font.h"
+#include "lib/hints.h"
+#include "lib/text_content.h"
+#include "../test.h"
+#include <boost/test/unit_test.hpp>
+
+
+using std::make_shared;
+using std::shared_ptr;
+using std::string;
+using std::vector;
+using boost::optional;
+
+
+vector<string> current_hints;
+
+
+static
+void
+collect_hint (string hint)
+{
+ current_hints.push_back (hint);
+}
+
+
+static
+vector<string>
+get_hints (shared_ptr<Film> film)
+{
+ current_hints.clear ();
+ Hints hints (film);
+ /* None of our tests need the audio analysis, and it is quite time-consuming */
+ hints.disable_audio_analysis ();
+ hints.Hint.connect (collect_hint);
+ hints.start ();
+ hints.join ();
+ while (signal_manager->ui_idle()) {}
+ hints.rethrow();
+ return current_hints;
+}
+
+
+static
+void
+check (TextType type, string name, optional<string> expected_hint = optional<string>())
+{
+ auto film = new_test_film(name);
+ auto content = content_factory("test/data/" + name + ".srt")[0];
+ content->text.front()->set_type (type);
+ content->text.front()->set_language (dcp::LanguageTag("en-US"));
+ film->examine_and_add_content (content);
+ BOOST_REQUIRE (!wait_for_jobs());
+ auto hints = get_hints (film);
+
+ if (expected_hint) {
+ BOOST_REQUIRE_EQUAL (hints.size(), 1U);
+ BOOST_CHECK_EQUAL (hints[0], *expected_hint);
+ } else {
+ string message;
+ for (auto hint: hints) {
+ message += hint + "\n";
+ }
+ BOOST_CHECK_MESSAGE(hints.empty(), "Found: " << message);
+ }
+}
+
+
+BOOST_AUTO_TEST_CASE (hint_closed_caption_too_long)
+{
+ check (
+ TextType::CLOSED_CAPTION,
+ "hint_closed_caption_too_long",
+ String::compose("At least one of your closed caption lines has more than %1 characters. It is advisable to make each line %1 characters at most in length.", MAX_CLOSED_CAPTION_LENGTH, MAX_CLOSED_CAPTION_LENGTH)
+ );
+}
+
+
+BOOST_AUTO_TEST_CASE (hint_many_closed_caption_lines)
+{
+ check (
+ TextType::CLOSED_CAPTION,
+ "hint_many_closed_caption_lines",
+ String::compose("Some of your closed captions span more than %1 lines, so they will be truncated.", MAX_CLOSED_CAPTION_LINES)
+ );
+}
+
+
+BOOST_AUTO_TEST_CASE (hint_subtitle_too_early)
+{
+ check (
+ TextType::OPEN_SUBTITLE,
+ "hint_subtitle_too_early",
+ string("It is advisable to put your first subtitle at least 4 seconds after the start of the DCP to make sure it is seen.")
+ );
+}
+
+
+BOOST_AUTO_TEST_CASE (hint_short_subtitles)
+{
+ check (
+ TextType::OPEN_SUBTITLE,
+ "hint_short_subtitles",
+ string("At least one of your subtitles lasts less than 15 frames. It is advisable to make each subtitle at least 15 frames long.")
+ );
+}
+
+
+BOOST_AUTO_TEST_CASE (hint_subtitles_too_close)
+{
+ check (
+ TextType::OPEN_SUBTITLE,
+ "hint_subtitles_too_close",
+ string("At least one of your subtitles starts less than 2 frames after the previous one. It is advisable to make the gap between subtitles at least 2 frames.")
+ );
+}
+
+
+BOOST_AUTO_TEST_CASE (hint_many_subtitle_lines)
+{
+ check (
+ TextType::OPEN_SUBTITLE,
+ "hint_many_subtitle_lines",
+ string("At least one of your subtitles has more than 3 lines. It is advisable to use no more than 3 lines.")
+ );
+}
+
+
+BOOST_AUTO_TEST_CASE(hint_many_subtitle_lines2)
+{
+ check(TextType::OPEN_SUBTITLE, "hint_many_subtitle_lines2");
+}
+
+
+BOOST_AUTO_TEST_CASE (hint_subtitle_too_long)
+{
+ check (
+ TextType::OPEN_SUBTITLE,
+ "hint_subtitle_too_long",
+ string("At least one of your subtitle lines has more than 52 characters. It is recommended to make each line 52 characters at most in length.")
+ );
+}
+
+
+BOOST_AUTO_TEST_CASE (hint_subtitle_much_too_long)
+{
+ check (
+ TextType::OPEN_SUBTITLE,
+ "hint_subtitle_much_too_long",
+ string("At least one of your subtitle lines has more than 79 characters. You should make each line 79 characters at most in length.")
+ );
+}
+
+
+BOOST_AUTO_TEST_CASE (hint_subtitle_mxf_too_big)
+{
+ string const name = "hint_subtitle_mxf_too_big";
+
+ auto film = new_test_film(name);
+
+ for (int i = 0; i < 4; ++i) {
+ dcp::File fake_font("build/test/hint_subtitle_mxf_too_big.ttf", "w");
+ for (int i = 0; i < 512; ++i) {
+ std::vector<uint8_t> rubbish(65536);
+ fake_font.write(rubbish.data(), 1, rubbish.size());
+ }
+ fake_font.close();
+
+ auto content = content_factory(String::compose("test/data/%1%2.xml", name, i))[0];
+ content->text[0]->set_type(TextType::OPEN_SUBTITLE);
+ content->text[0]->set_language(dcp::LanguageTag("en-US"));
+ film->examine_and_add_content(content);
+ BOOST_REQUIRE (!wait_for_jobs());
+ auto const font = content->text[0]->get_font(String::compose("font_%1", i));
+ BOOST_REQUIRE(font);
+ font->set_file("build/test/hint_subtitle_mxf_too_big.ttf");
+ }
+
+ auto hints = get_hints (film);
+
+ BOOST_REQUIRE_EQUAL (hints.size(), 1U);
+ BOOST_CHECK_EQUAL (
+ hints[0],
+ "At least one of your subtitle files is larger than " MAX_TEXT_MXF_SIZE_TEXT " in total. "
+ "You should divide the DCP into shorter reels."
+ );
+}
+
+
+BOOST_AUTO_TEST_CASE (hint_closed_caption_xml_too_big)
+{
+ string const name = "hint_closed_caption_xml_too_big";
+
+ auto film = new_test_film(name);
+
+ dcp::File ccap(String::compose("build/test/%1.srt", name), "w");
+ BOOST_REQUIRE (ccap);
+ for (int i = 0; i < 2048; ++i) {
+ fprintf(ccap.get(), "%d\n", i + 1);
+ int second = i * 2;
+ int minute = second % 60;
+ fprintf(ccap.get(), "00:%02d:%02d,000 --> 00:%02d:%02d,000\n", minute, second, minute, second + 1);
+ fprintf(ccap.get(), "Here are some closed captions.\n\n");
+ }
+ ccap.close();
+
+ auto content = content_factory("build/test/" + name + ".srt")[0];
+ content->text.front()->set_type (TextType::CLOSED_CAPTION);
+ content->text.front()->set_language (dcp::LanguageTag("en-US"));
+ film->examine_and_add_content (content);
+ BOOST_REQUIRE (!wait_for_jobs());
+ auto hints = get_hints (film);
+
+ BOOST_REQUIRE_EQUAL (hints.size(), 1U);
+ BOOST_CHECK_EQUAL (
+ hints[0],
+ "At least one of your closed caption files' XML part is larger than " MAX_CLOSED_CAPTION_XML_SIZE_TEXT ". "
+ "You should divide the DCP into shorter reels."
+ );
+}
+
+
+BOOST_AUTO_TEST_CASE (hints_destroyed_while_running)
+{
+ auto film = new_test_film("hints_destroyed_while_running");
+ auto content = content_factory(TestPaths::private_data() / "boon_telly.mkv")[0];
+ film->examine_and_add_content (content);
+ BOOST_REQUIRE (!wait_for_jobs());
+
+ auto hints = make_shared<Hints>(film);
+ hints->start ();
+ dcpomatic_sleep_seconds (1);
+ hints.reset ();
+ dcpomatic_sleep_seconds (1);
+}
+
+
+BOOST_AUTO_TEST_CASE (hints_audio_with_no_language)
+{
+ auto content = content_factory("test/data/sine_440.wav")[0];
+ auto film = new_test_film("hints_audio_with_no_language", { content });
+ content->audio->set_gain (-6);
+
+ auto hints = get_hints (film);
+ BOOST_REQUIRE_EQUAL (hints.size(), 1U);
+ BOOST_CHECK_EQUAL (
+ hints[0],
+ "Some of your content has audio but you have not set the audio language. It is advisable to set the audio language "
+ "in the \"DCP\" tab unless your audio has no spoken parts."
+ );
+}
+
+
+BOOST_AUTO_TEST_CASE (hints_certificate_validity)
+{
+ ConfigRestorer cr;
+
+ Config::instance()->set_signer_chain(make_shared<dcp::CertificateChain>(openssl_path(), 40 * 365));
+
+ auto film = new_test_film("hints_certificate_validity");
+ auto hints = get_hints (film);
+ BOOST_REQUIRE_EQUAL (hints.size(), 1U);
+ BOOST_CHECK_EQUAL (
+ hints[0],
+ "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."
+ );
+}
+
+
+BOOST_AUTO_TEST_CASE(hints_mpeg2)
+{
+ auto film = new_test_film("hints_certificate_validity");
+ film->set_video_encoding(VideoEncoding::MPEG2);
+ auto hints = get_hints(film);
+ BOOST_REQUIRE_EQUAL(hints.size(), 1U);
+ BOOST_CHECK_EQUAL(
+ hints[0],
+ "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."
+ );
+}
diff --git a/test/lib/image_content_fade_test.cc b/test/lib/image_content_fade_test.cc
new file mode 100644
index 000000000..d4de0f7b9
--- /dev/null
+++ b/test/lib/image_content_fade_test.cc
@@ -0,0 +1,49 @@
+/*
+ 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/>.
+
+*/
+
+
+#include "lib/content.h"
+#include "lib/content_factory.h"
+#include "lib/film.h"
+#include "lib/video_content.h"
+#include "../test.h"
+#include <boost/test/unit_test.hpp>
+
+
+using std::string;
+using std::list;
+
+
+BOOST_AUTO_TEST_CASE (image_content_fade_test)
+{
+ auto film = new_test_film("image_content_fade_test");
+ auto content = content_factory("test/data/flat_red.png")[0];
+ film->examine_and_add_content (content);
+ BOOST_REQUIRE (!wait_for_jobs());
+
+ content->video->set_fade_in (1);
+ make_and_verify_dcp (film);
+
+ /* This test is concerned with the image, so we'll ignore any
+ * differences in sound between the DCP and the reference to avoid test
+ * failures for unrelated reasons.
+ */
+ check_dcp("test/data/image_content_fade_test", film->dir(film->dcp_name()), true);
+}
diff --git a/test/lib/image_filename_sorter_test.cc b/test/lib/image_filename_sorter_test.cc
new file mode 100644
index 000000000..29b550753
--- /dev/null
+++ b/test/lib/image_filename_sorter_test.cc
@@ -0,0 +1,82 @@
+/*
+ Copyright (C) 2015-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/>.
+
+*/
+
+
+/** @file test/image_filename_sorter_test.cc
+ * @brief Test ImageFilenameSorter
+ * @ingroup selfcontained
+ */
+
+
+#include "lib/image_filename_sorter.h"
+#include "lib/compose.hpp"
+#include <boost/test/unit_test.hpp>
+#include <algorithm>
+#include <random>
+
+
+using std::sort;
+using std::vector;
+
+
+BOOST_AUTO_TEST_CASE (image_filename_sorter_test1)
+{
+ ImageFilenameSorter x;
+ BOOST_CHECK (x("abc0000000001", "abc0000000002"));
+ BOOST_CHECK (x("1", "2"));
+ BOOST_CHECK (x("1", "0002"));
+ BOOST_CHECK (x("0001", "2"));
+ BOOST_CHECK (x("1", "999"));
+ BOOST_CHECK (x("00057.tif", "00166.tif"));
+ BOOST_CHECK (x("/my/numeric999/path/00057.tif", "/my/numeric999/path/00166.tif"));
+ BOOST_CHECK (x("1_01.tif", "1_02.tif"));
+ BOOST_CHECK (x("EWS_DCP_092815_000000.j2c", "EWS_DCP_092815_000001.j2c"));
+ BOOST_CHECK (x("ap_trlr_178_uhd_bt1886_txt_e5c1_033115.86352.dpx", "ap_trlr_178_uhd_bt1886_txt_e5c1_033115.86353.dpx"));
+
+ BOOST_CHECK (!x("abc0000000002", "abc0000000001"));
+ BOOST_CHECK (!x("2", "1"));
+ BOOST_CHECK (!x("0002", "1"));
+ BOOST_CHECK (!x("2", "0001"));
+ BOOST_CHECK (!x("999", "1"));
+ BOOST_CHECK (!x("/my/numeric999/path/00166.tif", "/my/numeric999/path/00057.tif"));
+ BOOST_CHECK (!x("1_02.tif", "1_01.tif"));
+ BOOST_CHECK (!x("EWS_DCP_092815_000000.j2c", "EWS_DCP_092815_000000.j2c"));
+ BOOST_CHECK (!x("EWS_DCP_092815_000100.j2c", "EWS_DCP_092815_000000.j2c"));
+ BOOST_CHECK (!x("ap_trlr_178_uhd_bt1886_txt_e5c1_033115.86353.dpx", "ap_trlr_178_uhd_bt1886_txt_e5c1_033115.86352.dpx"));
+}
+
+
+/** Test a sort of a lot of paths. Mostly useful for profiling. */
+BOOST_AUTO_TEST_CASE (image_filename_sorter_test2)
+{
+ vector<boost::filesystem::path> paths;
+ for (int i = 0; i < 100000; ++i) {
+ paths.push_back(String::compose("some.filename.with.%1.number.tiff", i));
+ }
+
+ std::random_device rd;
+ std::mt19937 generator(rd());
+ std::shuffle(paths.begin(), paths.end(), generator);
+
+ sort (paths.begin(), paths.end(), ImageFilenameSorter());
+ for (int i = 0; i < 100000; ++i) {
+ BOOST_CHECK_EQUAL(paths[i].string(), String::compose("some.filename.with.%1.number.tiff", i));
+ }
+}
diff --git a/test/lib/image_proxy_test.cc b/test/lib/image_proxy_test.cc
new file mode 100644
index 000000000..18b8bb366
--- /dev/null
+++ b/test/lib/image_proxy_test.cc
@@ -0,0 +1,67 @@
+/*
+ Copyright (C) 2020-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 "lib/ffmpeg_image_proxy.h"
+#include "lib/j2k_image_proxy.h"
+#include "../test.h"
+#include <boost/test/unit_test.hpp>
+
+
+using std::make_shared;
+
+
+static const boost::filesystem::path data_file0 = TestPaths::private_data() / "player_seek_test_0.png";
+static const boost::filesystem::path data_file1 = TestPaths::private_data() / "player_seek_test_1.png";
+
+
+BOOST_AUTO_TEST_CASE (j2k_image_proxy_same_test)
+{
+ /* The files don't matter here, we just need some data to compare */
+
+ {
+ auto proxy1 = make_shared<J2KImageProxy>(data_file0, dcp::Size(1998, 1080), AV_PIX_FMT_RGB48);
+ auto proxy2 = make_shared<J2KImageProxy>(data_file0, dcp::Size(1998, 1080), AV_PIX_FMT_RGB48);
+ BOOST_CHECK (proxy1->same(proxy2));
+ }
+
+ {
+ auto proxy1 = make_shared<J2KImageProxy>(data_file0, dcp::Size(1998, 1080), AV_PIX_FMT_RGB48);
+ auto proxy2 = make_shared<J2KImageProxy>(data_file1, dcp::Size(1998, 1080), AV_PIX_FMT_RGB48);
+ BOOST_CHECK (!proxy1->same(proxy2));
+ }
+}
+
+
+BOOST_AUTO_TEST_CASE (ffmpeg_image_proxy_same_test)
+{
+ {
+ auto proxy1 = make_shared<FFmpegImageProxy>(data_file0);
+ auto proxy2 = make_shared<FFmpegImageProxy>(data_file0);
+ BOOST_CHECK (proxy1->same(proxy2));
+ }
+
+ {
+ auto proxy1 = make_shared<FFmpegImageProxy>(data_file0);
+ auto proxy2 = make_shared<FFmpegImageProxy>(data_file1);
+ BOOST_CHECK (!proxy1->same(proxy2));
+ }
+}
+
diff --git a/test/lib/image_test.cc b/test/lib/image_test.cc
new file mode 100644
index 000000000..cd18d479d
--- /dev/null
+++ b/test/lib/image_test.cc
@@ -0,0 +1,753 @@
+/*
+ Copyright (C) 2012-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/>.
+
+*/
+
+
+/** @file test/image_test.cc
+ * @brief Test Image class.
+ * @ingroup selfcontained
+ * @see test/pixel_formats_test.cc
+ */
+
+
+#include "lib/compose.hpp"
+#include "lib/image.h"
+#include "lib/image_content.h"
+#include "lib/image_decoder.h"
+#include "lib/image_jpeg.h"
+#include "lib/image_png.h"
+#include "lib/ffmpeg_image_proxy.h"
+#include "../test.h"
+#include <boost/test/unit_test.hpp>
+#include <iostream>
+
+
+using std::cout;
+using std::list;
+using std::make_shared;
+using std::string;
+
+
+BOOST_AUTO_TEST_CASE (aligned_image_test)
+{
+ auto s = new Image (AV_PIX_FMT_RGB24, dcp::Size (50, 50), Image::Alignment::PADDED);
+ BOOST_CHECK_EQUAL (s->planes(), 1);
+ /* 192 is 150 aligned to the nearest 64 bytes */
+ BOOST_CHECK_EQUAL (s->stride()[0], 192);
+ BOOST_CHECK_EQUAL (s->line_size()[0], 150);
+ BOOST_CHECK (s->data()[0]);
+ BOOST_CHECK (!s->data()[1]);
+ BOOST_CHECK (!s->data()[2]);
+ BOOST_CHECK (!s->data()[3]);
+
+ /* copy constructor */
+ auto t = new Image (*s);
+ BOOST_CHECK_EQUAL (t->planes(), 1);
+ BOOST_CHECK_EQUAL (t->stride()[0], 192);
+ BOOST_CHECK_EQUAL (t->line_size()[0], 150);
+ BOOST_CHECK (t->data()[0]);
+ BOOST_CHECK (!t->data()[1]);
+ BOOST_CHECK (!t->data()[2]);
+ BOOST_CHECK (!t->data()[3]);
+ BOOST_CHECK (t->data() != s->data());
+ BOOST_CHECK (t->data()[0] != s->data()[0]);
+ BOOST_CHECK (t->line_size() != s->line_size());
+ BOOST_CHECK_EQUAL (t->line_size()[0], s->line_size()[0]);
+ BOOST_CHECK (t->stride() != s->stride());
+ BOOST_CHECK_EQUAL (t->stride()[0], s->stride()[0]);
+
+ /* assignment operator */
+ auto u = new Image (AV_PIX_FMT_YUV422P, dcp::Size (150, 150), Image::Alignment::COMPACT);
+ *u = *s;
+ BOOST_CHECK_EQUAL (u->planes(), 1);
+ BOOST_CHECK_EQUAL (u->stride()[0], 192);
+ BOOST_CHECK_EQUAL (u->line_size()[0], 150);
+ BOOST_CHECK (u->data()[0]);
+ BOOST_CHECK (!u->data()[1]);
+ BOOST_CHECK (!u->data()[2]);
+ BOOST_CHECK (!u->data()[3]);
+ BOOST_CHECK (u->data() != s->data());
+ BOOST_CHECK (u->data()[0] != s->data()[0]);
+ BOOST_CHECK (u->line_size() != s->line_size());
+ BOOST_CHECK_EQUAL (u->line_size()[0], s->line_size()[0]);
+ BOOST_CHECK (u->stride() != s->stride());
+ BOOST_CHECK_EQUAL (u->stride()[0], s->stride()[0]);
+
+ delete s;
+ delete t;
+ delete u;
+}
+
+
+BOOST_AUTO_TEST_CASE (compact_image_test)
+{
+ auto s = new Image (AV_PIX_FMT_RGB24, dcp::Size (50, 50), Image::Alignment::COMPACT);
+ BOOST_CHECK_EQUAL (s->planes(), 1);
+ BOOST_CHECK_EQUAL (s->stride()[0], 50 * 3);
+ BOOST_CHECK_EQUAL (s->line_size()[0], 50 * 3);
+ BOOST_CHECK (s->data()[0]);
+ BOOST_CHECK (!s->data()[1]);
+ BOOST_CHECK (!s->data()[2]);
+ BOOST_CHECK (!s->data()[3]);
+
+ /* copy constructor */
+ auto t = new Image (*s);
+ BOOST_CHECK_EQUAL (t->planes(), 1);
+ BOOST_CHECK_EQUAL (t->stride()[0], 50 * 3);
+ BOOST_CHECK_EQUAL (t->line_size()[0], 50 * 3);
+ BOOST_CHECK (t->data()[0]);
+ BOOST_CHECK (!t->data()[1]);
+ BOOST_CHECK (!t->data()[2]);
+ BOOST_CHECK (!t->data()[3]);
+ BOOST_CHECK (t->data() != s->data());
+ BOOST_CHECK (t->data()[0] != s->data()[0]);
+ BOOST_CHECK (t->line_size() != s->line_size());
+ BOOST_CHECK_EQUAL (t->line_size()[0], s->line_size()[0]);
+ BOOST_CHECK (t->stride() != s->stride());
+ BOOST_CHECK_EQUAL (t->stride()[0], s->stride()[0]);
+
+ /* assignment operator */
+ auto u = new Image (AV_PIX_FMT_YUV422P, dcp::Size (150, 150), Image::Alignment::PADDED);
+ *u = *s;
+ BOOST_CHECK_EQUAL (u->planes(), 1);
+ BOOST_CHECK_EQUAL (u->stride()[0], 50 * 3);
+ BOOST_CHECK_EQUAL (u->line_size()[0], 50 * 3);
+ BOOST_CHECK (u->data()[0]);
+ BOOST_CHECK (!u->data()[1]);
+ BOOST_CHECK (!u->data()[2]);
+ BOOST_CHECK (!u->data()[3]);
+ BOOST_CHECK (u->data() != s->data());
+ BOOST_CHECK (u->data()[0] != s->data()[0]);
+ BOOST_CHECK (u->line_size() != s->line_size());
+ BOOST_CHECK_EQUAL (u->line_size()[0], s->line_size()[0]);
+ BOOST_CHECK (u->stride() != s->stride());
+ BOOST_CHECK_EQUAL (u->stride()[0], s->stride()[0]);
+
+ delete s;
+ delete t;
+ delete u;
+}
+
+
+static
+void
+alpha_blend_test_bgra_onto(AVPixelFormat format, string suffix)
+{
+ auto proxy = make_shared<FFmpegImageProxy>(TestPaths::private_data() / "prophet_frame.tiff");
+ auto raw = proxy->image(Image::Alignment::PADDED).image;
+ auto background = raw->convert_pixel_format (dcp::YUVToRGB::REC709, format, Image::Alignment::PADDED, false);
+
+ auto overlay = make_shared<Image>(AV_PIX_FMT_BGRA, dcp::Size(431, 891), Image::Alignment::PADDED);
+ overlay->make_transparent ();
+
+ for (int y = 0; y < 128; ++y) {
+ auto p = overlay->data()[0] + y * overlay->stride()[0];
+ for (int x = 0; x < 128; ++x) {
+ p[x * 4 + 2] = 255;
+ p[x * 4 + 3] = 255;
+ }
+ }
+
+ for (int y = 128; y < 256; ++y) {
+ auto p = overlay->data()[0] + y * overlay->stride()[0];
+ for (int x = 0; x < 128; ++x) {
+ p[x * 4 + 1] = 255;
+ p[x * 4 + 3] = 255;
+ }
+ }
+
+ for (int y = 256; y < 384; ++y) {
+ auto p = overlay->data()[0] + y * overlay->stride()[0];
+ for (int x = 0; x < 128; ++x) {
+ p[x * 4] = 255;
+ p[x * 4 + 3] = 255;
+ }
+ }
+
+ background->alpha_blend (overlay, Position<int> (13, 17));
+
+ auto save = background->convert_pixel_format (dcp::YUVToRGB::REC709, AV_PIX_FMT_RGB24, Image::Alignment::COMPACT, false);
+
+ write_image(save, "build/test/image_test_bgra_" + suffix + ".png");
+ check_image("build/test/image_test_bgra_" + suffix + ".png", TestPaths::private_data() / ("image_test_bgra_" + suffix + ".png"));
+}
+
+
+static
+void
+alpha_blend_test_rgba64be_onto(AVPixelFormat format, string suffix)
+{
+ auto proxy = make_shared<FFmpegImageProxy>(TestPaths::private_data() / "prophet_frame.tiff");
+ auto raw = proxy->image(Image::Alignment::PADDED).image;
+ auto background = raw->convert_pixel_format (dcp::YUVToRGB::REC709, format, Image::Alignment::PADDED, false);
+
+ auto overlay = make_shared<Image>(AV_PIX_FMT_RGBA64BE, dcp::Size(431, 891), Image::Alignment::PADDED);
+ overlay->make_transparent();
+
+ for (int y = 0; y < 128; ++y) {
+ auto p = reinterpret_cast<uint16_t*>(overlay->data()[0] + y * overlay->stride()[0]);
+ for (int x = 0; x < 128; ++x) {
+ p[x * 4 + 0] = 65535;
+ p[x * 4 + 3] = 65535;
+ }
+ }
+
+ for (int y = 128; y < 256; ++y) {
+ auto p = reinterpret_cast<uint16_t*>(overlay->data()[0] + y * overlay->stride()[0]);
+ for (int x = 0; x < 128; ++x) {
+ p[x * 4 + 1] = 65535;
+ p[x * 4 + 3] = 65535;
+ }
+ }
+
+ for (int y = 256; y < 384; ++y) {
+ auto p = reinterpret_cast<uint16_t*>(overlay->data()[0] + y * overlay->stride()[0]);
+ for (int x = 0; x < 128; ++x) {
+ p[x * 4 + 2] = 65535;
+ p[x * 4 + 3] = 65535;
+ }
+ }
+
+ background->alpha_blend(overlay, Position<int>(13, 17));
+
+ auto save = background->convert_pixel_format(dcp::YUVToRGB::REC709, AV_PIX_FMT_RGB24, Image::Alignment::COMPACT, false);
+
+ write_image(save, "build/test/image_test_rgba64_" + suffix + ".png");
+ check_image("build/test/image_test_rgba64_" + suffix + ".png", TestPaths::private_data() / ("image_test_rgba64_" + suffix + ".png"));
+}
+
+
+/** Test Image::alpha_blend */
+BOOST_AUTO_TEST_CASE (alpha_blend_test)
+{
+ alpha_blend_test_bgra_onto(AV_PIX_FMT_RGB24, "rgb24");
+ alpha_blend_test_bgra_onto(AV_PIX_FMT_BGRA, "bgra");
+ alpha_blend_test_bgra_onto(AV_PIX_FMT_RGBA, "rgba");
+ alpha_blend_test_bgra_onto(AV_PIX_FMT_RGB48LE, "rgb48le");
+ alpha_blend_test_bgra_onto(AV_PIX_FMT_YUV420P, "yuv420p");
+ alpha_blend_test_bgra_onto(AV_PIX_FMT_YUV420P10, "yuv420p10");
+ alpha_blend_test_bgra_onto(AV_PIX_FMT_YUV422P9LE, "yuv422p9le");
+ alpha_blend_test_bgra_onto(AV_PIX_FMT_YUV422P10LE, "yuv422p10le");
+ alpha_blend_test_bgra_onto(AV_PIX_FMT_YUV444P9LE, "yuv444p9le");
+ alpha_blend_test_bgra_onto(AV_PIX_FMT_YUV444P10LE, "yuv444p10le");
+
+ alpha_blend_test_rgba64be_onto(AV_PIX_FMT_RGB24, "rgb24");
+ alpha_blend_test_rgba64be_onto(AV_PIX_FMT_BGRA, "bgra");
+ alpha_blend_test_rgba64be_onto(AV_PIX_FMT_RGBA, "rgba");
+ alpha_blend_test_rgba64be_onto(AV_PIX_FMT_RGB48LE, "rgb48le");
+ alpha_blend_test_rgba64be_onto(AV_PIX_FMT_YUV420P, "yuv420p");
+ alpha_blend_test_rgba64be_onto(AV_PIX_FMT_YUV420P10, "yuv420p10");
+ alpha_blend_test_rgba64be_onto(AV_PIX_FMT_YUV422P9LE, "yuv422p9le");
+ alpha_blend_test_rgba64be_onto(AV_PIX_FMT_YUV422P10LE, "yuv422p10le");
+ alpha_blend_test_rgba64be_onto(AV_PIX_FMT_YUV444P9LE, "yuv444p9le");
+ alpha_blend_test_rgba64be_onto(AV_PIX_FMT_YUV444P10LE, "yuv444p10le");
+}
+
+
+/** Test Image::alpha_blend when blending RGBA onto XYZ12LE */
+BOOST_AUTO_TEST_CASE(alpha_blend_test_rgba_onto_xyz)
+{
+ Image xyz(AV_PIX_FMT_XYZ12LE, dcp::Size(50, 50), Image::Alignment::PADDED);
+ xyz.make_black();
+
+ auto overlay = make_shared<Image>(AV_PIX_FMT_RGBA, dcp::Size(8, 8), Image::Alignment::PADDED);
+ for (int y = 0; y < 8; ++y) {
+ uint8_t* p = overlay->data()[0] + (y * overlay->stride()[0]);
+ for (int x = 0; x < 8; ++x) {
+ *p++ = 255;
+ *p++ = 0;
+ *p++ = 0;
+ *p++ = 255;
+ }
+ }
+
+ xyz.alpha_blend(overlay, Position<int>(4, 4));
+
+ for (int y = 0; y < 50; ++y) {
+ uint16_t* p = reinterpret_cast<uint16_t*>(xyz.data()[0]) + (y * xyz.stride()[0] / 2);
+ for (int x = 0; x < 50; ++x) {
+ if (4 <= x && x < 12 && 4 <= y && y < 12) {
+ BOOST_REQUIRE_EQUAL(p[0], 45078U);
+ BOOST_REQUIRE_EQUAL(p[1], 34939U);
+ BOOST_REQUIRE_EQUAL(p[2], 13892U);
+ } else {
+ BOOST_REQUIRE_EQUAL(p[0], 0U);
+ BOOST_REQUIRE_EQUAL(p[1], 0U);
+ BOOST_REQUIRE_EQUAL(p[2], 0U);
+ }
+ p += 3;
+ }
+ }
+}
+
+
+/** Test Image::alpha_blend when blending RGBA64BE onto XYZ12LE */
+BOOST_AUTO_TEST_CASE(alpha_blend_test_rgba64be_onto_xyz)
+{
+ Image xyz(AV_PIX_FMT_XYZ12LE, dcp::Size(50, 50), Image::Alignment::PADDED);
+ xyz.make_black();
+
+ auto overlay = make_shared<Image>(AV_PIX_FMT_RGBA64BE, dcp::Size(8, 8), Image::Alignment::PADDED);
+ for (int y = 0; y < 8; ++y) {
+ auto p = reinterpret_cast<uint16_t*>(overlay->data()[0] + (y * overlay->stride()[0]));
+ for (int x = 0; x < 8; ++x) {
+ *p++ = 65535;
+ *p++ = 0;
+ *p++ = 0;
+ *p++ = 65535;
+ }
+ }
+
+ xyz.alpha_blend(overlay, Position<int>(4, 4));
+
+ for (int y = 0; y < 50; ++y) {
+ auto p = reinterpret_cast<uint16_t*>(xyz.data()[0]) + (y * xyz.stride()[0] / 2);
+ for (int x = 0; x < 50; ++x) {
+ if (4 <= x && x < 12 && 4 <= y && y < 12) {
+ BOOST_REQUIRE_EQUAL(p[0], 45078U);
+ BOOST_REQUIRE_EQUAL(p[1], 34939U);
+ BOOST_REQUIRE_EQUAL(p[2], 13892U);
+ } else {
+ BOOST_REQUIRE_EQUAL(p[0], 0U);
+ BOOST_REQUIRE_EQUAL(p[1], 0U);
+ BOOST_REQUIRE_EQUAL(p[2], 0U);
+ }
+ p += 3;
+ }
+ }
+}
+
+
+BOOST_AUTO_TEST_CASE(alpha_blend_text)
+{
+ Image target(AV_PIX_FMT_RGB24, dcp::Size(1998, 1080), Image::Alignment::PADDED);
+ target.make_black();
+
+ FFmpegImageProxy subtitle_proxy(TestPaths::private_data() / "16-bit-sub.png");
+ auto subtitle = subtitle_proxy.image(Image::Alignment::COMPACT);
+
+ target.alpha_blend(subtitle.image, Position<int>(0, 0));
+ write_image(make_shared<Image>(target), "build/test/alpha_blend_text.png");
+ check_image("build/test/alpha_blend_text.png", TestPaths::private_data() / "16-bit-sub-blended.png");
+}
+
+
+/** Test merge (list<PositionImage>) with a single image */
+BOOST_AUTO_TEST_CASE (merge_test1)
+{
+ int const stride = 48 * 4;
+
+ auto A = make_shared<Image>(AV_PIX_FMT_BGRA, dcp::Size (48, 48), Image::Alignment::COMPACT);
+ A->make_transparent ();
+ auto a = A->data()[0];
+
+ for (int y = 0; y < 48; ++y) {
+ auto p = a + y * stride;
+ for (int x = 0; x < 16; ++x) {
+ /* blue */
+ p[x * 4] = 255;
+ /* opaque */
+ p[x * 4 + 3] = 255;
+ }
+ }
+
+ list<PositionImage> all;
+ all.push_back (PositionImage (A, Position<int>(0, 0)));
+ auto merged = merge (all, Image::Alignment::COMPACT);
+
+ BOOST_CHECK (merged.position == Position<int>(0, 0));
+ BOOST_CHECK_EQUAL (memcmp (merged.image->data()[0], A->data()[0], stride * 48), 0);
+}
+
+
+/** Test merge (list<PositionImage>) with two images */
+BOOST_AUTO_TEST_CASE (merge_test2)
+{
+ auto A = make_shared<Image>(AV_PIX_FMT_BGRA, dcp::Size (48, 1), Image::Alignment::COMPACT);
+ A->make_transparent ();
+ auto a = A->data()[0];
+ for (int x = 0; x < 16; ++x) {
+ /* blue */
+ a[x * 4] = 255;
+ /* opaque */
+ a[x * 4 + 3] = 255;
+ }
+
+ auto B = make_shared<Image>(AV_PIX_FMT_BGRA, dcp::Size (48, 1), Image::Alignment::COMPACT);
+ B->make_transparent ();
+ auto b = B->data()[0];
+ for (int x = 0; x < 16; ++x) {
+ /* red */
+ b[(x + 32) * 4 + 2] = 255;
+ /* opaque */
+ b[(x + 32) * 4 + 3] = 255;
+ }
+
+ list<PositionImage> all;
+ all.push_back (PositionImage(A, Position<int>(0, 0)));
+ all.push_back (PositionImage(B, Position<int>(0, 0)));
+ auto merged = merge (all, Image::Alignment::COMPACT);
+
+ BOOST_CHECK (merged.position == Position<int>(0, 0));
+
+ auto m = merged.image->data()[0];
+
+ for (int x = 0; x < 16; ++x) {
+ BOOST_CHECK_EQUAL (m[x * 4], 255);
+ BOOST_CHECK_EQUAL (m[x * 4 + 3], 255);
+ BOOST_CHECK_EQUAL (m[(x + 16) * 4 + 3], 0);
+ BOOST_CHECK_EQUAL (m[(x + 32) * 4 + 2], 255);
+ BOOST_CHECK_EQUAL (m[(x + 32) * 4 + 3], 255);
+ }
+}
+
+
+/** Test Image::crop_scale_window with YUV420P and some windowing */
+BOOST_AUTO_TEST_CASE (crop_scale_window_test)
+{
+ auto proxy = make_shared<FFmpegImageProxy>("test/data/flat_red.png");
+ auto raw = proxy->image(Image::Alignment::PADDED).image;
+ auto out = raw->crop_scale_window(
+ Crop(), dcp::Size(1998, 836), dcp::Size(1998, 1080), dcp::YUVToRGB::REC709, VideoRange::FULL, AV_PIX_FMT_YUV420P, VideoRange::FULL, Image::Alignment::PADDED, false
+ );
+ auto save = out->scale(dcp::Size(1998, 1080), dcp::YUVToRGB::REC709, AV_PIX_FMT_RGB24, Image::Alignment::COMPACT, false);
+ write_image(save, "build/test/crop_scale_window_test.png");
+ check_image("test/data/crop_scale_window_test.png", "build/test/crop_scale_window_test.png");
+}
+
+
+/** Special cases of Image::crop_scale_window which triggered some valgrind warnings */
+BOOST_AUTO_TEST_CASE (crop_scale_window_test2)
+{
+ auto image = make_shared<Image>(AV_PIX_FMT_XYZ12LE, dcp::Size(2048, 858), Image::Alignment::PADDED);
+ image->crop_scale_window (
+ Crop(279, 0, 0, 0), dcp::Size(1069, 448), dcp::Size(1069, 578), dcp::YUVToRGB::REC709, VideoRange::FULL, AV_PIX_FMT_RGB24, VideoRange::FULL, Image::Alignment::COMPACT, false
+ );
+ image->crop_scale_window (
+ Crop(2048, 0, 0, 0), dcp::Size(1069, 448), dcp::Size(1069, 578), dcp::YUVToRGB::REC709, VideoRange::FULL, AV_PIX_FMT_RGB24, VideoRange::FULL, Image::Alignment::COMPACT, false
+ );
+}
+
+
+BOOST_AUTO_TEST_CASE (crop_scale_window_test3)
+{
+ auto proxy = make_shared<FFmpegImageProxy>(TestPaths::private_data() / "player_seek_test_0.png");
+ auto xyz = proxy->image(Image::Alignment::PADDED).image->convert_pixel_format(dcp::YUVToRGB::REC709, AV_PIX_FMT_RGB24, Image::Alignment::PADDED, false);
+ auto cropped = xyz->crop_scale_window(
+ Crop(512, 0, 0, 0), dcp::Size(1486, 1080), dcp::Size(1998, 1080), dcp::YUVToRGB::REC709, VideoRange::FULL, AV_PIX_FMT_RGB24, VideoRange::FULL, Image::Alignment::COMPACT, false
+ );
+ write_image(cropped, "build/test/crop_scale_window_test3.png");
+ check_image("test/data/crop_scale_window_test3.png", "build/test/crop_scale_window_test3.png");
+}
+
+
+BOOST_AUTO_TEST_CASE (crop_scale_window_test4)
+{
+ auto proxy = make_shared<FFmpegImageProxy>(TestPaths::private_data() / "player_seek_test_0.png");
+ auto xyz = proxy->image(Image::Alignment::PADDED).image->convert_pixel_format(dcp::YUVToRGB::REC709, AV_PIX_FMT_RGB24, Image::Alignment::PADDED, false);
+ auto cropped = xyz->crop_scale_window(
+ Crop(512, 0, 0, 0), dcp::Size(1486, 1080), dcp::Size(1998, 1080), dcp::YUVToRGB::REC709, VideoRange::FULL, AV_PIX_FMT_XYZ12LE, VideoRange::FULL, Image::Alignment::COMPACT, false
+ );
+ write_image(cropped, "build/test/crop_scale_window_test4.png");
+ check_image("test/data/crop_scale_window_test4.png", "build/test/crop_scale_window_test4.png");
+}
+
+
+BOOST_AUTO_TEST_CASE (crop_scale_window_test5)
+{
+ auto proxy = make_shared<FFmpegImageProxy>(TestPaths::private_data() / "player_seek_test_0.png");
+ auto xyz = proxy->image(Image::Alignment::PADDED).image->convert_pixel_format(dcp::YUVToRGB::REC709, AV_PIX_FMT_XYZ12LE, Image::Alignment::PADDED, false);
+ auto cropped = xyz->crop_scale_window(
+ Crop(512, 0, 0, 0), dcp::Size(1486, 1080), dcp::Size(1998, 1080), dcp::YUVToRGB::REC709, VideoRange::FULL, AV_PIX_FMT_RGB24, VideoRange::FULL, Image::Alignment::COMPACT, false
+ );
+ write_image(cropped, "build/test/crop_scale_window_test5.png");
+ check_image("test/data/crop_scale_window_test5.png", "build/test/crop_scale_window_test5.png");
+}
+
+
+BOOST_AUTO_TEST_CASE (crop_scale_window_test6)
+{
+ auto proxy = make_shared<FFmpegImageProxy>(TestPaths::private_data() / "player_seek_test_0.png");
+ auto xyz = proxy->image(Image::Alignment::PADDED).image->convert_pixel_format(dcp::YUVToRGB::REC709, AV_PIX_FMT_XYZ12LE, Image::Alignment::PADDED, false);
+ auto cropped = xyz->crop_scale_window(
+ Crop(512, 0, 0, 0), dcp::Size(1486, 1080), dcp::Size(1998, 1080), dcp::YUVToRGB::REC709, VideoRange::FULL, AV_PIX_FMT_XYZ12LE, VideoRange::FULL, Image::Alignment::COMPACT, false
+ );
+ write_image(cropped, "build/test/crop_scale_window_test6.png");
+ check_image("test/data/crop_scale_window_test6.png", "build/test/crop_scale_window_test6.png");
+}
+
+
+/** Test some small crops with an image that shows up errors in registration of the YUV planes (#1872) */
+BOOST_AUTO_TEST_CASE (crop_scale_window_test7)
+{
+ using namespace boost::filesystem;
+ for (int left_crop = 0; left_crop < 8; ++left_crop) {
+ auto proxy = make_shared<FFmpegImageProxy>("test/data/rgb_grey_testcard.png");
+ auto yuv = proxy->image(Image::Alignment::PADDED).image->convert_pixel_format(dcp::YUVToRGB::REC709, AV_PIX_FMT_YUV420P, Image::Alignment::PADDED, false);
+ int rounded = left_crop - (left_crop % 2);
+ auto cropped = yuv->crop_scale_window(
+ Crop(left_crop, 0, 0, 0),
+ dcp::Size(1998 - rounded, 1080),
+ dcp::Size(1998 - rounded, 1080),
+ dcp::YUVToRGB::REC709,
+ VideoRange::VIDEO,
+ AV_PIX_FMT_RGB24,
+ VideoRange::VIDEO,
+ Image::Alignment::PADDED,
+ false
+ );
+ path file = String::compose("crop_scale_window_test7-%1.png", left_crop);
+ write_image(cropped, path("build") / "test" / file);
+ check_image(path("test") / "data" / file, path("build") / "test" / file, 10);
+ }
+}
+
+
+BOOST_AUTO_TEST_CASE (crop_scale_window_test8)
+{
+ using namespace boost::filesystem;
+
+ auto image = make_shared<Image>(AV_PIX_FMT_YUV420P, dcp::Size(800, 600), Image::Alignment::PADDED);
+ memset(image->data()[0], 41, image->stride()[0] * 600);
+ memset(image->data()[1], 240, image->stride()[1] * 300);
+ memset(image->data()[2], 41, image->stride()[2] * 300);
+ auto scaled = image->crop_scale_window(
+ Crop(), dcp::Size(1435, 1080), dcp::Size(1998, 1080), dcp::YUVToRGB::REC709, VideoRange::FULL, AV_PIX_FMT_YUV420P, VideoRange::FULL, Image::Alignment::PADDED, false
+ );
+ auto file = "crop_scale_window_test8.png";
+ write_image(scaled->convert_pixel_format(dcp::YUVToRGB::REC709, AV_PIX_FMT_RGB24, Image::Alignment::COMPACT, false), path("build") / "test" / file);
+ check_image(path("test") / "data" / file, path("build") / "test" / file, 10);
+}
+
+
+BOOST_AUTO_TEST_CASE (as_png_test)
+{
+ auto proxy = make_shared<FFmpegImageProxy>("test/data/3d_test/000001.png");
+ auto image_rgb = proxy->image(Image::Alignment::PADDED).image;
+ auto image_bgr = image_rgb->convert_pixel_format(dcp::YUVToRGB::REC709, AV_PIX_FMT_BGRA, Image::Alignment::PADDED, false);
+ image_as_png(image_rgb).write ("build/test/as_png_rgb.png");
+ image_as_png(image_bgr).write ("build/test/as_png_bgr.png");
+
+ check_image ("test/data/3d_test/000001.png", "build/test/as_png_rgb.png");
+ check_image ("test/data/3d_test/000001.png", "build/test/as_png_bgr.png");
+}
+
+
+BOOST_AUTO_TEST_CASE (as_jpeg_test)
+{
+ auto proxy = make_shared<FFmpegImageProxy>("test/data/3d_test/000001.png");
+ auto image_rgb = proxy->image(Image::Alignment::PADDED).image;
+ auto image_bgr = image_rgb->convert_pixel_format(dcp::YUVToRGB::REC709, AV_PIX_FMT_BGRA, Image::Alignment::PADDED, false);
+ image_as_jpeg(image_rgb, 60).write("build/test/as_jpeg_rgb.jpeg");
+ image_as_jpeg(image_bgr, 60).write("build/test/as_jpeg_bgr.jpeg");
+
+ check_image ("test/data/as_jpeg_rgb.jpeg", "build/test/as_jpeg_rgb.jpeg");
+ check_image ("test/data/as_jpeg_bgr.jpeg", "build/test/as_jpeg_bgr.jpeg");
+}
+
+
+/* Very dumb test to fade black to make sure it stays black */
+static void
+fade_test_format_black (AVPixelFormat f, string name)
+{
+ Image yuv (f, dcp::Size(640, 480), Image::Alignment::PADDED);
+ yuv.make_black ();
+ yuv.fade (0);
+ string const filename = "fade_test_black_" + name + ".png";
+ image_as_png(yuv.convert_pixel_format(dcp::YUVToRGB::REC709, AV_PIX_FMT_RGBA, Image::Alignment::PADDED, false)).write("build/test/" + filename);
+ check_image ("test/data/" + filename, "build/test/" + filename);
+}
+
+
+/* Fade red to make sure it stays red */
+static void
+fade_test_format_red (AVPixelFormat f, float amount, string name)
+{
+ auto proxy = make_shared<FFmpegImageProxy>("test/data/flat_red.png");
+ auto red = proxy->image(Image::Alignment::PADDED).image->convert_pixel_format(dcp::YUVToRGB::REC709, f, Image::Alignment::PADDED, false);
+ red->fade (amount);
+ string const filename = "fade_test_red_" + name + ".png";
+ image_as_png(red->convert_pixel_format(dcp::YUVToRGB::REC709, AV_PIX_FMT_RGBA, Image::Alignment::PADDED, false)).write("build/test/" + filename);
+ check_image ("test/data/" + filename, "build/test/" + filename);
+}
+
+
+BOOST_AUTO_TEST_CASE (fade_test)
+{
+ fade_test_format_black (AV_PIX_FMT_YUV420P, "yuv420p");
+ fade_test_format_black (AV_PIX_FMT_YUV422P10, "yuv422p10");
+ fade_test_format_black (AV_PIX_FMT_RGB24, "rgb24");
+ fade_test_format_black (AV_PIX_FMT_XYZ12LE, "xyz12le");
+ fade_test_format_black (AV_PIX_FMT_RGB48LE, "rgb48le");
+
+ fade_test_format_red (AV_PIX_FMT_YUV420P, 0, "yuv420p_0");
+ fade_test_format_red (AV_PIX_FMT_YUV420P, 0.5, "yuv420p_50");
+ fade_test_format_red (AV_PIX_FMT_YUV420P, 1, "yuv420p_100");
+ fade_test_format_red (AV_PIX_FMT_YUV422P10, 0, "yuv422p10_0");
+ fade_test_format_red (AV_PIX_FMT_YUV422P10, 0.5, "yuv422p10_50");
+ fade_test_format_red (AV_PIX_FMT_YUV422P10, 1, "yuv422p10_100");
+ fade_test_format_red (AV_PIX_FMT_RGB24, 0, "rgb24_0");
+ fade_test_format_red (AV_PIX_FMT_RGB24, 0.5, "rgb24_50");
+ fade_test_format_red (AV_PIX_FMT_RGB24, 1, "rgb24_100");
+ fade_test_format_red (AV_PIX_FMT_XYZ12LE, 0, "xyz12le_0");
+ fade_test_format_red (AV_PIX_FMT_XYZ12LE, 0.5, "xyz12le_50");
+ fade_test_format_red (AV_PIX_FMT_XYZ12LE, 1, "xyz12le_100");
+ fade_test_format_red (AV_PIX_FMT_RGB48LE, 0, "rgb48le_0");
+ fade_test_format_red (AV_PIX_FMT_RGB48LE, 0.5, "rgb48le_50");
+ fade_test_format_red (AV_PIX_FMT_RGB48LE, 1, "rgb48le_100");
+}
+
+
+BOOST_AUTO_TEST_CASE (make_black_test)
+{
+ dcp::Size in_size (512, 512);
+ dcp::Size out_size (1024, 1024);
+
+ list<AVPixelFormat> pix_fmts = {
+ AV_PIX_FMT_RGB24, // 2
+ AV_PIX_FMT_ARGB,
+ AV_PIX_FMT_RGBA,
+ AV_PIX_FMT_ABGR,
+ AV_PIX_FMT_BGRA,
+ AV_PIX_FMT_YUV420P, // 0
+ AV_PIX_FMT_YUV411P,
+ AV_PIX_FMT_YUV422P10LE,
+ AV_PIX_FMT_YUV422P16LE,
+ AV_PIX_FMT_YUV444P9LE,
+ AV_PIX_FMT_YUV444P9BE,
+ AV_PIX_FMT_YUV444P10LE,
+ AV_PIX_FMT_YUV444P10BE,
+ AV_PIX_FMT_UYVY422,
+ AV_PIX_FMT_YUVJ420P,
+ AV_PIX_FMT_YUVJ422P,
+ AV_PIX_FMT_YUVJ444P,
+ AV_PIX_FMT_YUVA420P9BE,
+ AV_PIX_FMT_YUVA422P9BE,
+ AV_PIX_FMT_YUVA444P9BE,
+ AV_PIX_FMT_YUVA420P9LE,
+ AV_PIX_FMT_YUVA422P9LE,
+ AV_PIX_FMT_YUVA444P9LE,
+ AV_PIX_FMT_YUVA420P10BE,
+ AV_PIX_FMT_YUVA422P10BE,
+ AV_PIX_FMT_YUVA444P10BE,
+ AV_PIX_FMT_YUVA420P10LE,
+ AV_PIX_FMT_YUVA422P10LE,
+ AV_PIX_FMT_YUVA444P10LE,
+ AV_PIX_FMT_YUVA420P16BE,
+ AV_PIX_FMT_YUVA422P16BE,
+ AV_PIX_FMT_YUVA444P16BE,
+ AV_PIX_FMT_YUVA420P16LE,
+ AV_PIX_FMT_YUVA422P16LE,
+ AV_PIX_FMT_YUVA444P16LE,
+ AV_PIX_FMT_RGB555LE, // 46
+ };
+
+ for (auto i: pix_fmts) {
+ auto foo = make_shared<Image>(i, in_size, Image::Alignment::PADDED);
+ foo->make_black ();
+ auto bar = foo->scale (out_size, dcp::YUVToRGB::REC601, AV_PIX_FMT_RGB24, Image::Alignment::PADDED, false);
+
+ uint8_t* p = bar->data()[0];
+ for (int y = 0; y < bar->size().height; ++y) {
+ uint8_t* q = p;
+ for (int x = 0; x < bar->line_size()[0]; ++x) {
+ if (*q != 0) {
+ std::cerr << "x=" << x << ", (x%3)=" << (x%3) << "\n";
+ }
+ BOOST_CHECK_EQUAL (*q++, 0);
+ }
+ p += bar->stride()[0];
+ }
+ }
+}
+
+
+BOOST_AUTO_TEST_CASE (make_part_black_test)
+{
+ auto proxy = make_shared<FFmpegImageProxy>("test/data/flat_red.png");
+ auto original = proxy->image(Image::Alignment::PADDED).image;
+
+ list<AVPixelFormat> pix_fmts = {
+ AV_PIX_FMT_RGB24,
+ AV_PIX_FMT_ARGB,
+ AV_PIX_FMT_RGBA,
+ AV_PIX_FMT_ABGR,
+ AV_PIX_FMT_BGRA,
+ AV_PIX_FMT_YUV420P,
+ AV_PIX_FMT_YUV422P10LE,
+ AV_PIX_FMT_YUV444P10LE
+ };
+
+ list<std::pair<int, int>> positions = {
+ { 0, 256 },
+ { 128, 64 },
+ };
+
+ for (auto i: pix_fmts) {
+ for (auto j: positions) {
+ auto foo = original->convert_pixel_format(dcp::YUVToRGB::REC601, i, Image::Alignment::PADDED, false);
+ foo->make_part_black (j.first, j.second);
+ auto bar = foo->convert_pixel_format (dcp::YUVToRGB::REC601, AV_PIX_FMT_RGB24, Image::Alignment::PADDED, false);
+
+ auto p = bar->data()[0];
+ for (int y = 0; y < bar->size().height; ++y) {
+ auto q = p;
+ for (int x = 0; x < bar->size().width; ++x) {
+ int r = *q++;
+ int g = *q++;
+ int b = *q++;
+ if (x >= j.first && x < (j.first + j.second)) {
+ BOOST_CHECK_MESSAGE (
+ r < 3, "red=" << static_cast<int>(r) << " at (" << x << "," << y << ") format " << i << " from " << j.first << " width " << j.second
+ );
+ } else {
+ BOOST_CHECK_MESSAGE (
+ r >= 252, "red=" << static_cast<int>(r) << " at (" << x << "," << y << ") format " << i << " from " << j.first << " width " << j.second
+ );
+
+ }
+ BOOST_CHECK_MESSAGE (
+ g == 0, "green=" << static_cast<int>(g) << " at (" << x << "," << y << ") format " << i << " from " << j.first << " width " << j.second
+ );
+ BOOST_CHECK_MESSAGE (
+ b == 0, "blue=" << static_cast<int>(b) << " at (" << x << "," << y << ") format " << i << " from " << j.first << " width " << j.second
+ );
+ }
+ p += bar->stride()[0];
+ }
+ }
+ }
+}
+
+
+/** Make sure the image isn't corrupted if it is cropped too much. This can happen when a
+ * filler 128x128 black frame is emitted from the FFmpegDecoder and the overall crop in either direction
+ * is greater than 128 pixels.
+ */
+BOOST_AUTO_TEST_CASE (over_crop_test)
+{
+ auto image = make_shared<Image>(AV_PIX_FMT_RGB24, dcp::Size(128, 128), Image::Alignment::PADDED);
+ image->make_black ();
+ auto scaled = image->crop_scale_window (
+ Crop(0, 0, 128, 128), dcp::Size(1323, 565), dcp::Size(1349, 565), dcp::YUVToRGB::REC709, VideoRange::FULL, AV_PIX_FMT_RGB24, VideoRange::FULL, Image::Alignment::PADDED, true
+ );
+ string const filename = "over_crop_test.png";
+ write_image (scaled, "build/test/" + filename);
+ check_image ("test/data/" + filename, "build/test/" + filename);
+}
diff --git a/test/lib/import_dcp_test.cc b/test/lib/import_dcp_test.cc
new file mode 100644
index 000000000..6a1448660
--- /dev/null
+++ b/test/lib/import_dcp_test.cc
@@ -0,0 +1,179 @@
+/*
+ Copyright (C) 2014-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/>.
+
+*/
+
+
+/** @file test/import_dcp_test.cc
+ * @brief Test import of encrypted DCPs.
+ * @ingroup feature
+ */
+
+
+#include "lib/config.h"
+#include "lib/constants.h"
+#include "lib/content_factory.h"
+#include "lib/cross.h"
+#include "lib/dcp_content.h"
+#include "lib/dcp_content_type.h"
+#include "lib/dcp_subtitle_content.h"
+#include "lib/examine_content_job.h"
+#include "lib/ffmpeg_content.h"
+#include "lib/film.h"
+#include "lib/job_manager.h"
+#include "lib/ratio.h"
+#include "lib/screen.h"
+#include "lib/video_content.h"
+#include "../test.h"
+#include <dcp/cpl.h>
+#include <boost/test/unit_test.hpp>
+
+
+using std::dynamic_pointer_cast;
+using std::make_shared;
+using std::map;
+using std::string;
+using std::vector;
+
+
+/** Make an encrypted DCP, import it and make a new unencrypted DCP */
+BOOST_AUTO_TEST_CASE (import_dcp_test)
+{
+ ConfigRestorer cr;
+
+ auto c = make_shared<FFmpegContent>("test/data/test.mp4");
+ auto A = new_test_film("import_dcp_test", { c });
+ A->set_encrypted (true);
+ A->set_dcp_content_type(DCPContentType::from_isdcf_name("TLR"));
+ make_and_verify_dcp (A);
+
+ dcp::DCP A_dcp ("build/test/import_dcp_test/" + A->dcp_name());
+ A_dcp.read ();
+
+ Config::instance()->set_decryption_chain (make_shared<dcp::CertificateChain>(openssl_path(), CERTIFICATE_VALIDITY_PERIOD));
+
+ auto signer = Config::instance()->signer_chain();
+ BOOST_REQUIRE(signer->valid());
+
+ auto const decrypted_kdm = A->make_kdm(A_dcp.cpls().front()->file().get(), dcp::LocalTime ("2030-07-21T00:00:00+00:00"), dcp::LocalTime ("2031-07-21T00:00:00+00:00"));
+ auto const kdm = decrypted_kdm.encrypt(signer, Config::instance()->decryption_chain()->leaf(), {}, dcp::Formulation::MODIFIED_TRANSITIONAL_1, true, 0);
+
+ auto d = make_shared<DCPContent>("build/test/import_dcp_test/" + A->dcp_name());
+ d->add_kdm (kdm);
+ auto B = new_test_film("import_dcp_test2", { d });
+ B->set_dcp_content_type(DCPContentType::from_isdcf_name("TLR"));
+ B->set_audio_channels(16);
+ make_and_verify_dcp (B);
+
+ /* Should be 1s red, 1s green, 1s blue */
+ check_dcp ("test/data/import_dcp_test2", "build/test/import_dcp_test2/" + B->dcp_name());
+}
+
+
+/** Check that DCP markers are imported correctly */
+BOOST_AUTO_TEST_CASE (import_dcp_markers_test)
+{
+ Cleanup cl;
+
+ /* Make a DCP with some markers */
+ auto content = content_factory("test/data/flat_red.png");
+ auto film = new_test_film("import_dcp_markers_test", content, &cl);
+
+ content[0]->video->set_length (24 * 60 * 10);
+
+ film->set_marker(dcp::Marker::FFOC, dcpomatic::DCPTime::from_frames(1, 24));
+ film->set_marker(dcp::Marker::FFMC, dcpomatic::DCPTime::from_seconds(9.4));
+ film->set_marker(dcp::Marker::LFMC, dcpomatic::DCPTime::from_seconds(9.99));
+
+ make_and_verify_dcp (film);
+
+ /* Import the DCP to a new film and check the markers */
+ auto imported = make_shared<DCPContent>(film->dir(film->dcp_name()));
+ auto film2 = new_test_film("import_dcp_markers_test2", {imported}, &cl);
+ film2->write_metadata ();
+
+ /* When import_dcp_markers_test was made a LFOC marker will automatically
+ * have been added.
+ */
+ BOOST_CHECK_EQUAL (imported->markers().size(), 4U);
+
+ auto markers = imported->markers();
+ BOOST_REQUIRE(markers.find(dcp::Marker::FFMC) != markers.end());
+ BOOST_CHECK(markers[dcp::Marker::FFMC] == dcpomatic::ContentTime(904000));
+ BOOST_REQUIRE(markers.find(dcp::Marker::LFMC) != markers.end());
+ BOOST_CHECK(markers[dcp::Marker::LFMC] == dcpomatic::ContentTime(960000));
+
+ /* Load that film and check that the markers have been loaded */
+ auto film3 = make_shared<Film>(boost::filesystem::path("build/test/import_dcp_markers_test2"));
+ film3->read_metadata ();
+ BOOST_REQUIRE_EQUAL (film3->content().size(), 1U);
+ auto reloaded = dynamic_pointer_cast<DCPContent>(film3->content().front());
+ BOOST_REQUIRE (reloaded);
+
+ BOOST_CHECK_EQUAL (reloaded->markers().size(), 4U);
+
+ markers = reloaded->markers();
+ BOOST_REQUIRE(markers.find(dcp::Marker::FFMC) != markers.end());
+ BOOST_CHECK(markers[dcp::Marker::FFMC] == dcpomatic::ContentTime(904000));
+ BOOST_REQUIRE(markers.find(dcp::Marker::LFMC) != markers.end());
+ BOOST_CHECK(markers[dcp::Marker::LFMC] == dcpomatic::ContentTime(960000));
+
+ cl.run ();
+}
+
+
+/** Check that DCP metadata (ratings and content version) are imported correctly */
+BOOST_AUTO_TEST_CASE (import_dcp_metadata_test)
+{
+ /* Make a DCP with some ratings and a content version */
+ auto film = new_test_film("import_dcp_metadata_test");
+ auto content = content_factory("test/data/flat_red.png")[0];
+ film->examine_and_add_content (content);
+ BOOST_REQUIRE (!wait_for_jobs());
+
+ content->video->set_length (10);
+
+ vector<dcp::Rating> ratings = { {"BBFC", "15"}, {"MPAA", "NC-17"} };
+ film->set_ratings (ratings);
+
+ vector<string> cv = { "Fred "};
+ film->set_content_versions (cv);
+
+ make_and_verify_dcp (film);
+
+ /* Import the DCP to a new film and check the metadata */
+ auto film2 = new_test_film("import_dcp_metadata_test2");
+ auto imported = make_shared<DCPContent>(film->dir(film->dcp_name()));
+ film2->examine_and_add_content (imported);
+ BOOST_REQUIRE (!wait_for_jobs());
+ film2->write_metadata ();
+
+ BOOST_CHECK (imported->ratings() == ratings);
+ BOOST_CHECK (imported->content_versions() == cv);
+
+ /* Load that film and check that the metadata has been loaded */
+ auto film3 = make_shared<Film>(boost::filesystem::path("build/test/import_dcp_metadata_test2"));
+ film3->read_metadata ();
+ BOOST_REQUIRE_EQUAL (film3->content().size(), 1U);
+ auto reloaded = dynamic_pointer_cast<DCPContent>(film3->content().front());
+ BOOST_REQUIRE (reloaded);
+
+ BOOST_CHECK (reloaded->ratings() == ratings);
+ BOOST_CHECK (reloaded->content_versions() == cv);
+}
+
diff --git a/test/lib/interrupt_encoder_test.cc b/test/lib/interrupt_encoder_test.cc
new file mode 100644
index 000000000..071669a2a
--- /dev/null
+++ b/test/lib/interrupt_encoder_test.cc
@@ -0,0 +1,56 @@
+/*
+ Copyright (C) 2016-2021 Carl Hetherington <cth@carlh.net>
+
+ This file is part of DCP-o-matic.
+
+ DCP-o-matic is free software; you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation; either version 2 of the License, or
+ (at your option) any later version.
+
+ DCP-o-matic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with DCP-o-matic. If not, see <http://www.gnu.org/licenses/>.
+
+*/
+
+
+/** @file test/interrupt_encoder_test.cc
+ * @brief Test clean shutdown of threads if a DCP encode is interrupted.
+ * @ingroup feature
+ */
+
+
+#include "lib/audio_content.h"
+#include "lib/cross.h"
+#include "lib/dcp_content_type.h"
+#include "lib/ffmpeg_content.h"
+#include "lib/film.h"
+#include "lib/job_manager.h"
+#include "lib/make_dcp.h"
+#include "lib/ratio.h"
+#include "../test.h"
+#include <boost/test/unit_test.hpp>
+
+
+using std::make_shared;
+
+
+/** Interrupt a DCP encode when it is in progress, as this used to (still does?)
+ * sometimes give an error related to pthreads.
+ */
+BOOST_AUTO_TEST_CASE (interrupt_encoder_test)
+{
+ auto content = make_shared<FFmpegContent>(TestPaths::private_data() / "prophet_long_clip.mkv");
+ auto film = new_test_film("interrupt_encoder_test", { content });
+
+ make_dcp (film, TranscodeJob::ChangedBehaviour::IGNORE);
+
+ dcpomatic_sleep_seconds (10);
+
+ JobManager::drop ();
+}
diff --git a/test/lib/isdcf_name_test.cc b/test/lib/isdcf_name_test.cc
new file mode 100644
index 000000000..c4bbdaf4a
--- /dev/null
+++ b/test/lib/isdcf_name_test.cc
@@ -0,0 +1,293 @@
+/*
+ Copyright (C) 2014-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/>.
+
+*/
+
+
+/** @file test/isdcf_name_test.cc
+ * @brief Test creation of ISDCF names.
+ * @ingroup feature
+ */
+
+
+#include "lib/audio_content.h"
+#include "lib/audio_mapping.h"
+#include "lib/content_factory.h"
+#include "lib/dcp_content_type.h"
+#include "lib/ffmpeg_content.h"
+#include "lib/film.h"
+#include "lib/image_content.h"
+#include "lib/ratio.h"
+#include "lib/text_content.h"
+#include "lib/video_content.h"
+#include "../test.h"
+#include <boost/test/unit_test.hpp>
+
+
+using std::make_shared;
+using std::shared_ptr;
+using std::string;
+
+
+BOOST_AUTO_TEST_CASE (isdcf_name_test)
+{
+ auto audio = content_factory("test/data/sine_440.wav")[0];
+ auto film = new_test_film("isdcf_name_test", { audio });
+
+ /* A basic test */
+
+ film->set_name ("My Nice Film");
+ film->set_dcp_content_type (DCPContentType::from_isdcf_name ("FTR"));
+ film->set_container (Ratio::from_id ("185"));
+ film->_isdcf_date = boost::gregorian::date (2014, boost::gregorian::Jul, 4);
+ film->set_audio_language(dcp::LanguageTag("en-US"));
+ film->set_content_versions({"1"});
+ film->set_release_territory(dcp::LanguageTag::RegionSubtag("GB"));
+ film->set_ratings({dcp::Rating("BBFC", "PG")});
+ film->set_studio (string("ST"));
+ film->set_facility (string("FAC"));
+ film->set_interop (true);
+ BOOST_CHECK_EQUAL (film->isdcf_name(false), "MyNiceFilm_FTR-1_F_EN-XX_GB-PG_10_2K_ST_20140704_FAC_IOP_OV");
+
+ /* Check that specifying no audio language writes XX */
+ film->set_audio_language (boost::none);
+ BOOST_CHECK_EQUAL (film->isdcf_name(false), "MyNiceFilm_FTR-1_F_XX-XX_GB-PG_10_2K_ST_20140704_FAC_IOP_OV");
+
+ /* Test a long name and some different data */
+
+ film->set_name ("My Nice Film With A Very Long Name");
+ film->set_dcp_content_type (DCPContentType::from_isdcf_name ("TLR"));
+ film->set_container (Ratio::from_id ("239"));
+ film->_isdcf_date = boost::gregorian::date (2014, boost::gregorian::Jul, 4);
+ film->set_audio_channels (1);
+ film->set_resolution (Resolution::FOUR_K);
+ auto text = content_factory("test/data/subrip.srt")[0];
+ BOOST_REQUIRE_EQUAL (text->text.size(), 1U);
+ text->text[0]->set_burn(true);
+ text->text[0]->set_language(dcp::LanguageTag("fr-FR"));
+ film->examine_and_add_content (text);
+ film->set_version_number(2);
+ film->set_release_territory(dcp::LanguageTag::RegionSubtag("US"));
+ film->set_ratings({dcp::Rating("MPA", "R")});
+ film->set_studio (string("di"));
+ film->set_facility (string("ppfacility"));
+ BOOST_REQUIRE (!wait_for_jobs());
+ audio = content_factory("test/data/sine_440.wav")[0];
+ film->examine_and_add_content (audio);
+ BOOST_REQUIRE (!wait_for_jobs());
+ film->set_audio_language (dcp::LanguageTag("de-DE"));
+ film->set_interop (false);
+ BOOST_CHECK_EQUAL (film->isdcf_name(false), "MyNiceFilmWith_TLR-2_S_DE-fr_US-R_MOS_4K_DI_20140704_PPF_SMPTE_OV");
+
+ /* Test the subs being marked as open captions */
+ text->text[0]->set_type(TextType::OPEN_CAPTION);
+ BOOST_CHECK_EQUAL(film->isdcf_name(false), "MyNiceFilmWith_TLR-2_S_DE-fr-OCAP_US-R_MOS_4K_DI_20140704_PPF_SMPTE_OV");
+ text->text[0]->set_type(TextType::OPEN_SUBTITLE);
+
+ /* Test to see that RU ratings like 6+ are stripped of their + */
+ film->set_ratings({dcp::Rating("RARS", "6+")});
+ BOOST_CHECK_EQUAL (film->dcp_name(false), "MyNiceFilmWith_TLR-2_S_DE-fr_US-6_MOS_4K_DI_20140704_PPF_SMPTE_OV");
+ film->set_ratings({dcp::Rating("MPA", "R")});
+
+ /* Test interior aspect ratio: shouldn't be shown with trailers */
+
+ auto content = std::make_shared<ImageContent>("test/data/simple_testcard_640x480.png");
+ film->examine_and_add_content (content);
+ BOOST_REQUIRE (!wait_for_jobs());
+ content->video->set_custom_ratio (1.33);
+ film->set_container (Ratio::from_id ("185"));
+ BOOST_CHECK_EQUAL (film->isdcf_name(false), "MyNiceFilmWith_TLR-2_F_DE-fr_US-R_MOS_4K_DI_20140704_PPF_SMPTE_OV");
+
+ /* But should be shown for anything else */
+
+ film->set_dcp_content_type (DCPContentType::from_isdcf_name ("XSN"));
+ BOOST_CHECK_EQUAL (film->isdcf_name(false), "MyNiceFilmWith_XSN-2_F-133_DE-fr_US-R_MOS_4K_DI_20140704_PPF_SMPTE_OV");
+
+ /* And it should always be numeric */
+
+ content->video->set_custom_ratio (2.39);
+ BOOST_CHECK_EQUAL (film->isdcf_name(false), "MyNiceFilmWith_XSN-2_F-239_DE-fr_US-R_MOS_4K_DI_20140704_PPF_SMPTE_OV");
+
+ content->video->set_custom_ratio (1.9);
+ BOOST_CHECK_EQUAL (film->isdcf_name(false), "MyNiceFilmWith_XSN-2_F-190_DE-fr_US-R_MOS_4K_DI_20140704_PPF_SMPTE_OV");
+
+ /* And it should be possible to set any 'strange' ratio, not just the ones we know about */
+ content->video->set_custom_ratio (2.2);
+ BOOST_CHECK_EQUAL (film->isdcf_name(false), "MyNiceFilmWith_XSN-2_F-220_DE-fr_US-R_MOS_4K_DI_20140704_PPF_SMPTE_OV");
+ content->video->set_custom_ratio (1.95);
+ BOOST_CHECK_EQUAL (film->isdcf_name(false), "MyNiceFilmWith_XSN-2_F-195_DE-fr_US-R_MOS_4K_DI_20140704_PPF_SMPTE_OV");
+
+ content->video->set_custom_ratio (1.33);
+
+ /* Test 3D */
+
+ film->set_three_d (true);
+ BOOST_CHECK_EQUAL (film->isdcf_name(false), "MyNiceFilmWith_XSN-2-3D_F-133_DE-fr_US-R_MOS_4K_DI_20140704_PPF_SMPTE-3D_OV");
+
+ /* Test content type modifiers */
+
+ film->set_three_d (false);
+ film->set_temp_version (true);
+ film->set_pre_release (true);
+ film->set_red_band (true);
+ film->set_two_d_version_of_three_d (true);
+ film->set_chain (string("MyChain"));
+ film->set_luminance (dcp::Luminance(4.5, dcp::Luminance::Unit::FOOT_LAMBERT));
+ film->set_video_frame_rate (48);
+ BOOST_CHECK_EQUAL (film->isdcf_name(false), "MyNiceFilmWith_XSN-2-Temp-Pre-RedBand-MyChain-2D-45fl-48_F-133_DE-fr_US-R_MOS_4K_DI_20140704_PPF_SMPTE_OV");
+
+ /* Test a name which is already in camelCase */
+
+ film->set_three_d (false);
+ film->set_temp_version (false);
+ film->set_pre_release (false);
+ film->set_red_band (false);
+ film->set_two_d_version_of_three_d (false);
+ film->set_chain (string(""));
+ film->set_luminance (boost::none);
+ film->set_video_frame_rate (24);
+ film->set_name ("IKnowCamels");
+ BOOST_CHECK_EQUAL (film->isdcf_name(false), "IKnowCamels_XSN-2_F-133_DE-fr_US-R_MOS_4K_DI_20140704_PPF_SMPTE_OV");
+
+ /* And one in capitals */
+
+ film->set_name ("LIKE SHOUTING");
+ BOOST_CHECK_EQUAL (film->isdcf_name(false), "LikeShouting_XSN-2_F-133_DE-fr_US-R_MOS_4K_DI_20140704_PPF_SMPTE_OV");
+
+ /* And one with underscores */
+
+ film->set_name("LIKE_SHOUTING");
+ BOOST_CHECK_EQUAL(film->isdcf_name(false), "LikeShouting_XSN-2_F-133_DE-fr_US-R_MOS_4K_DI_20140704_PPF_SMPTE_OV");
+
+ /* And one with hyphens */
+
+ film->set_name("LIKE-SHOUTING");
+ BOOST_CHECK_EQUAL(film->isdcf_name(false), "LIKE-SHOUTING_XSN-2_F-133_DE-fr_US-R_MOS_4K_DI_20140704_PPF_SMPTE_OV");
+
+ film->set_name("LIKE_SHOUTING");
+
+ /* Test audio channel markup */
+
+ film->set_audio_channels (6);
+ auto sound = make_shared<FFmpegContent>("test/data/sine_440.wav");
+ film->examine_and_add_content (sound);
+ BOOST_REQUIRE (!wait_for_jobs());
+ BOOST_CHECK_EQUAL (film->isdcf_name(false), "LikeShouting_XSN-2_F-133_DE-fr_US-R_10_4K_DI_20140704_PPF_SMPTE_OV");
+
+ AudioMapping mapping = sound->audio->mapping ();
+
+ mapping.set (0, dcp::Channel::LEFT, 1.0);
+ sound->audio->set_mapping (mapping);
+ BOOST_CHECK_EQUAL (film->isdcf_name(false), "LikeShouting_XSN-2_F-133_DE-fr_US-R_20_4K_DI_20140704_PPF_SMPTE_OV");
+ mapping.set (0, dcp::Channel::RIGHT, 1.0);
+ sound->audio->set_mapping (mapping);
+ BOOST_CHECK_EQUAL (film->isdcf_name(false), "LikeShouting_XSN-2_F-133_DE-fr_US-R_30_4K_DI_20140704_PPF_SMPTE_OV");
+ mapping.set (0, dcp::Channel::LFE, 1.0);
+ sound->audio->set_mapping (mapping);
+ BOOST_CHECK_EQUAL (film->isdcf_name(false), "LikeShouting_XSN-2_F-133_DE-fr_US-R_31_4K_DI_20140704_PPF_SMPTE_OV");
+ mapping.set (0, dcp::Channel::LS, 1.0);
+ sound->audio->set_mapping (mapping);
+ BOOST_CHECK_EQUAL (film->isdcf_name(false), "LikeShouting_XSN-2_F-133_DE-fr_US-R_41_4K_DI_20140704_PPF_SMPTE_OV");
+ mapping.set (0, dcp::Channel::RS, 1.0);
+ sound->audio->set_mapping (mapping);
+ BOOST_CHECK_EQUAL (film->isdcf_name(false), "LikeShouting_XSN-2_F-133_DE-fr_US-R_51_4K_DI_20140704_PPF_SMPTE_OV");
+ mapping.set (0, dcp::Channel::HI, 1.0);
+ sound->audio->set_mapping (mapping);
+ BOOST_CHECK_EQUAL (film->isdcf_name(false), "LikeShouting_XSN-2_F-133_DE-fr_US-R_51_4K_DI_20140704_PPF_SMPTE_OV");
+
+ film->set_audio_channels (8);
+ mapping.set (0, dcp::Channel::HI, 1.0);
+ sound->audio->set_mapping (mapping);
+ BOOST_CHECK_EQUAL (film->isdcf_name(false), "LikeShouting_XSN-2_F-133_DE-fr_US-R_51-HI_4K_DI_20140704_PPF_SMPTE_OV");
+ mapping.set (0, dcp::Channel::VI, 1.0);
+ sound->audio->set_mapping (mapping);
+ BOOST_CHECK_EQUAL (film->isdcf_name(false), "LikeShouting_XSN-2_F-133_DE-fr_US-R_51-HI-VI_4K_DI_20140704_PPF_SMPTE_OV");
+
+ film->set_audio_channels(10);
+ mapping.set (0, dcp::Channel::HI, 0.0);
+ mapping.set (0, dcp::Channel::VI, 0.0);
+ sound->audio->set_mapping (mapping);
+ BOOST_CHECK_EQUAL (film->isdcf_name(false), "LikeShouting_XSN-2_F-133_DE-fr_US-R_51_4K_DI_20140704_PPF_SMPTE_OV");
+ mapping.set (0, dcp::Channel::HI, 1.0);
+ sound->audio->set_mapping (mapping);
+ BOOST_CHECK_EQUAL (film->isdcf_name(false), "LikeShouting_XSN-2_F-133_DE-fr_US-R_51-HI_4K_DI_20140704_PPF_SMPTE_OV");
+ mapping.set (0, dcp::Channel::VI, 1.0);
+ sound->audio->set_mapping (mapping);
+ BOOST_CHECK_EQUAL (film->isdcf_name(false), "LikeShouting_XSN-2_F-133_DE-fr_US-R_51-HI-VI_4K_DI_20140704_PPF_SMPTE_OV");
+
+ film->set_audio_channels(12);
+ mapping.set (0, dcp::Channel::BSL, 1.0);
+ mapping.set (0, dcp::Channel::BSR, 1.0);
+ mapping.set (0, dcp::Channel::HI, 0.0);
+ mapping.set (0, dcp::Channel::VI, 0.0);
+ sound->audio->set_mapping (mapping);
+ BOOST_CHECK_EQUAL (film->isdcf_name(false), "LikeShouting_XSN-2_F-133_DE-fr_US-R_71_4K_DI_20140704_PPF_SMPTE_OV");
+ mapping.set (0, dcp::Channel::HI, 1.0);
+ sound->audio->set_mapping (mapping);
+ BOOST_CHECK_EQUAL (film->isdcf_name(false), "LikeShouting_XSN-2_F-133_DE-fr_US-R_71-HI_4K_DI_20140704_PPF_SMPTE_OV");
+ mapping.set (0, dcp::Channel::VI, 1.0);
+ sound->audio->set_mapping (mapping);
+ BOOST_CHECK_EQUAL (film->isdcf_name(false), "LikeShouting_XSN-2_F-133_DE-fr_US-R_71-HI-VI_4K_DI_20140704_PPF_SMPTE_OV");
+
+ /* Check that the proper codes are used, not just part of the language code; in this case, QBP instead of PT (#2235) */
+ film->set_audio_language(dcp::LanguageTag("pt-BR"));
+ BOOST_CHECK_EQUAL (film->isdcf_name(false), "LikeShouting_XSN-2_F-133_QBP-fr_US-R_71-HI-VI_4K_DI_20140704_PPF_SMPTE_OV");
+
+ /* Check that nothing is added for non-existent ratings */
+ film->set_ratings({});
+ BOOST_CHECK_EQUAL (film->isdcf_name(false), "LikeShouting_XSN-2_F-133_QBP-fr_US_71-HI-VI_4K_DI_20140704_PPF_SMPTE_OV");
+}
+
+
+BOOST_AUTO_TEST_CASE(isdcf_name_with_atmos)
+{
+ auto content = content_factory(TestPaths::private_data() / "atmos_asset.mxf");
+ auto film = new_test_film("isdcf_name_with_atmos", content);
+ film->_isdcf_date = boost::gregorian::date(2023, boost::gregorian::Jan, 18);
+ film->set_name("Hello");
+
+ BOOST_CHECK_EQUAL(film->isdcf_name(false), "Hello_TST-1_F_XX-XX_MOS-IAB_2K_20230118_SMPTE_OV");
+}
+
+
+BOOST_AUTO_TEST_CASE(isdcf_name_with_ccap)
+{
+ auto content = content_factory("test/data/short.srt")[0];
+ auto film = new_test_film("isdcf_name_with_ccap", { content });
+ content->text[0]->set_use(true);
+ content->text[0]->set_type(TextType::CLOSED_CAPTION);
+ content->text[0]->set_dcp_track(DCPTextTrack("Foo", dcp::LanguageTag("de-DE")));
+ film->_isdcf_date = boost::gregorian::date(2023, boost::gregorian::Jan, 18);
+ film->set_name("Hello");
+
+ BOOST_CHECK_EQUAL(film->isdcf_name(false), "Hello_TST-1_F_XX-DE-CCAP_MOS_2K_20230118_SMPTE_OV");
+}
+
+
+BOOST_AUTO_TEST_CASE(isdcf_name_with_closed_subtitles)
+{
+ auto content = content_factory("test/data/short.srt")[0];
+ auto film = new_test_film("isdcf_name_with_closed_subtitles", { content });
+ content->text[0]->set_use(true);
+ content->text[0]->set_type(TextType::CLOSED_SUBTITLE);
+ content->text[0]->set_dcp_track(DCPTextTrack("Foo", dcp::LanguageTag("de-DE")));
+ film->_isdcf_date = boost::gregorian::date(2023, boost::gregorian::Jan, 18);
+ film->set_name("Hello");
+
+ BOOST_CHECK_EQUAL(film->isdcf_name(false), "Hello_TST-1_F_XX-DE_MOS_2K_20230118_SMPTE_OV");
+}
diff --git a/test/lib/j2k_encode_threading_test.cc b/test/lib/j2k_encode_threading_test.cc
new file mode 100644
index 000000000..5f1178175
--- /dev/null
+++ b/test/lib/j2k_encode_threading_test.cc
@@ -0,0 +1,148 @@
+/*
+ Copyright (C) 2023 Carl Hetherington <cth@carlh.net>
+
+ This file is part of DCP-o-matic.
+
+ DCP-o-matic is free software; you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation; either version 2 of the License, or
+ (at your option) any later version.
+
+ DCP-o-matic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with DCP-o-matic. If not, see <http://www.gnu.org/licenses/>.
+
+*/
+
+
+#include "lib/config.h"
+#include "lib/content_factory.h"
+#include "lib/dcp_film_encoder.h"
+#include "lib/dcp_transcode_job.h"
+#include "lib/encode_server_description.h"
+#include "lib/film.h"
+#ifdef DCPOMATIC_GROK
+#include "lib/grok/context.h"
+#endif
+#include "lib/j2k_encoder.h"
+#include "lib/job_manager.h"
+#include "lib/make_dcp.h"
+#include "lib/transcode_job.h"
+#include "../test.h"
+#include <dcp/cpl.h>
+#include <dcp/dcp.h>
+#include <dcp/reel.h>
+#include <dcp/reel_picture_asset.h>
+#include <boost/test/unit_test.hpp>
+
+
+using std::dynamic_pointer_cast;
+using std::list;
+
+
+BOOST_AUTO_TEST_CASE(local_threads_created_and_destroyed)
+{
+ auto film = new_test_film("local_threads_created_and_destroyed", {});
+ Writer writer(film, {}, "foo");
+ J2KEncoder encoder(film, writer);
+
+ encoder.remake_threads(32, 0, {});
+ BOOST_CHECK_EQUAL(encoder._threads.size(), 32U);
+
+ encoder.remake_threads(9, 0, {});
+ BOOST_CHECK_EQUAL(encoder._threads.size(), 9U);
+
+ encoder.end();
+ BOOST_CHECK_EQUAL(encoder._threads.size(), 0U);
+}
+
+
+BOOST_AUTO_TEST_CASE(remote_threads_created_and_destroyed)
+{
+ auto film = new_test_film("remote_threads_created_and_destroyed", {});
+ Writer writer(film, {}, "foo");
+ J2KEncoder encoder(film, writer);
+
+ list<EncodeServerDescription> servers = {
+ { "fred", 7, SERVER_LINK_VERSION },
+ { "jim", 2, SERVER_LINK_VERSION },
+ { "sheila", 14, SERVER_LINK_VERSION },
+ };
+
+ encoder.remake_threads(0, 0, servers);
+ BOOST_CHECK_EQUAL(encoder._threads.size(), 7U + 2U + 14U);
+
+ servers = {
+ { "fred", 7, SERVER_LINK_VERSION },
+ { "jim", 5, SERVER_LINK_VERSION },
+ { "sheila", 14, SERVER_LINK_VERSION },
+ };
+
+ encoder.remake_threads(0, 0, servers);
+ BOOST_CHECK_EQUAL(encoder._threads.size(), 7U + 5U + 14U);
+
+ servers = {
+ { "fred", 0, SERVER_LINK_VERSION },
+ { "jim", 0, SERVER_LINK_VERSION },
+ { "sheila", 11, SERVER_LINK_VERSION },
+ };
+
+ encoder.remake_threads(0, 0, servers);
+ BOOST_CHECK_EQUAL(encoder._threads.size(), 11U);
+}
+
+
+BOOST_AUTO_TEST_CASE(frames_not_lost_when_threads_disappear)
+{
+ auto content = content_factory(TestPaths::private_data() / "clapperboard.mp4");
+ auto film = new_test_film("frames_not_lost", content);
+ film->write_metadata();
+ auto job = make_dcp(film, TranscodeJob::ChangedBehaviour::IGNORE);
+ auto encoder = dynamic_cast<J2KEncoder*>(dynamic_pointer_cast<DCPFilmEncoder>(job->_encoder)->_encoder.get());
+
+ while (JobManager::instance()->work_to_do()) {
+ encoder->remake_threads((rand() % 7) + 1, 0, {});
+ dcpomatic_sleep_seconds(1);
+ }
+
+ BOOST_CHECK(!JobManager::instance()->errors());
+
+ dcp::DCP dcp(film->dir(film->dcp_name()));
+ dcp.read();
+ BOOST_REQUIRE_EQUAL(dcp.cpls().size(), 1U);
+ BOOST_REQUIRE_EQUAL(dcp.cpls()[0]->reels().size(), 1U);
+ BOOST_REQUIRE_EQUAL(dcp.cpls()[0]->reels()[0]->main_picture()->intrinsic_duration(), 423U);
+}
+
+
+#ifdef DCPOMATIC_GROK
+BOOST_AUTO_TEST_CASE(transcode_stops_when_gpu_enabled_with_no_gpu)
+{
+ ConfigRestorer cr;
+
+ grk_plugin::setMessengerLogger(new grk_plugin::GrokLogger("[GROK] "));
+
+ Config::Grok grok;
+ grok.enable = true;
+ Config::instance()->set_grok(grok);
+
+ auto content = content_factory(TestPaths::private_data() / "clapperboard.mp4");
+ auto film = new_test_film("transcode_stops_when_gpu_enabled_with_no_gpu", content);
+ film->write_metadata();
+ auto job = make_dcp(film, TranscodeJob::ChangedBehaviour::IGNORE);
+
+ int slept = 0;
+ while (JobManager::instance()->work_to_do() && slept < 10) {
+ dcpomatic_sleep_seconds(1);
+ ++slept;
+ }
+
+ BOOST_REQUIRE(slept < 10);
+
+ JobManager::drop();
+}
+#endif
diff --git a/test/lib/j2k_encoder_test.cc b/test/lib/j2k_encoder_test.cc
new file mode 100644
index 000000000..31b055f1d
--- /dev/null
+++ b/test/lib/j2k_encoder_test.cc
@@ -0,0 +1,89 @@
+/*
+ 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 "lib/config.h"
+#include "lib/cross.h"
+#include "lib/image.h"
+#include "lib/j2k_encoder.h"
+#include "lib/player_video.h"
+#include "lib/raw_image_proxy.h"
+#include "lib/writer.h"
+#include "../test.h"
+extern "C" {
+#include <libavutil/pixfmt.h>
+}
+#include <boost/test/unit_test.hpp>
+
+
+using std::make_shared;
+using std::weak_ptr;
+using boost::optional;
+
+
+BOOST_AUTO_TEST_CASE(j2k_encoder_deadlock_test)
+{
+ ConfigRestorer cr;
+
+ auto film = new_test_film("j2k_encoder_deadlock_test");
+
+ auto constexpr threads = 4;
+
+ /* Don't call ::start() on this Writer, so it can never write anything */
+ Writer writer(film, {}, {});
+ writer.set_encoder_threads(threads);
+
+ /* We want to test the case where the writer queue fills, and this can't happen unless there
+ * are enough encoding threads (each of which will end up waiting for the writer to empty,
+ * which will never happen).
+ */
+ Config::instance()->set_master_encoding_threads(threads);
+ J2KEncoder encoder(film, writer);
+ encoder.begin();
+
+ /* The queue will be full when we write another frame when there are already
+ * more than (threads * frames_in_memory_multiplier [i.e. 3])
+ * in the queue, so to fill the queue we must add threads * 3 + 2.
+ */
+ for (int i = 0; i < (threads * 3) + 2; ++i) {
+ auto image = make_shared<Image>(AV_PIX_FMT_RGB24, dcp::Size(1998, 1080), Image::Alignment::PADDED);
+ auto image_proxy = make_shared<RawImageProxy>(image);
+ encoder.encode(
+ std::make_shared<PlayerVideo>(
+ image_proxy,
+ Crop(),
+ optional<double>(),
+ dcp::Size(1998, 1080),
+ dcp::Size(1998, 1080),
+ Eyes::BOTH,
+ Part::WHOLE,
+ optional<ColourConversion>(),
+ VideoRange::VIDEO,
+ weak_ptr<Content>(),
+ optional<dcpomatic::ContentTime>(),
+ false
+ ),
+ {}
+ );
+ }
+
+ dcpomatic_sleep_seconds(10);
+}
+
diff --git a/test/lib/j2k_video_bit_rate_test.cc b/test/lib/j2k_video_bit_rate_test.cc
new file mode 100644
index 000000000..83944f58a
--- /dev/null
+++ b/test/lib/j2k_video_bit_rate_test.cc
@@ -0,0 +1,89 @@
+/*
+ Copyright (C) 2016-2021 Carl Hetherington <cth@carlh.net>
+
+ This file is part of DCP-o-matic.
+
+ DCP-o-matic is free software; you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation; either version 2 of the License, or
+ (at your option) any later version.
+
+ DCP-o-matic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with DCP-o-matic. If not, see <http://www.gnu.org/licenses/>.
+
+*/
+
+
+/** @file test/j2k_video_bit_rate_test.cc
+ * @brief Test whether we output whatever J2K bandwidth is requested.
+ * @ingroup feature
+ */
+
+
+#include "lib/dcp_content_type.h"
+#include "lib/film.h"
+#include "lib/image_content.h"
+#include "lib/video_content.h"
+#include "../test.h"
+#include <boost/test/unit_test.hpp>
+#include <fmt/format.h>
+
+
+using std::make_shared;
+using std::string;
+
+
+static void
+check (int target_bits_per_second)
+{
+ Cleanup cl;
+
+ int const duration = 10;
+
+ string const name = "bandwidth_test_" + fmt::to_string(target_bits_per_second);
+ auto content = make_shared<ImageContent>(TestPaths::private_data() / "prophet_frame.tiff");
+ auto film = new_test_film(name, { content }, &cl);
+ film->set_video_bit_rate(VideoEncoding::JPEG2000, target_bits_per_second);
+ content->video->set_length (24 * duration);
+ make_and_verify_dcp (
+ film,
+ {
+ dcp::VerificationNote::Code::MISSING_FFMC_IN_FEATURE,
+ dcp::VerificationNote::Code::MISSING_FFEC_IN_FEATURE,
+ dcp::VerificationNote::Code::NEARLY_INVALID_PICTURE_FRAME_SIZE_IN_BYTES,
+ dcp::VerificationNote::Code::INVALID_PICTURE_FRAME_SIZE_IN_BYTES,
+ dcp::VerificationNote::Code::INVALID_JPEG2000_TILE_PART_SIZE,
+ },
+ target_bits_per_second <= 250000000,
+ target_bits_per_second <= 250000000
+ );
+
+ auto test = find_file(film->dir(film->dcp_name()), "j2c_");
+ double actual_bits_per_second = boost::filesystem::file_size(test) * 8.0 / duration;
+
+ /* Check that we're within 85% to 115% of target on average */
+ BOOST_CHECK ((actual_bits_per_second / target_bits_per_second) > 0.85);
+ BOOST_CHECK ((actual_bits_per_second / target_bits_per_second) < 1.15);
+
+ cl.run();
+}
+
+
+BOOST_AUTO_TEST_CASE (bandwidth_test)
+{
+ check (50000000);
+ check (100000000);
+ check (150000000);
+ check (200000000);
+ check (250000000);
+ check (300000000);
+ check (350000000);
+ check (400000000);
+ check (450000000);
+ check (500000000);
+}
diff --git a/test/lib/job_manager_test.cc b/test/lib/job_manager_test.cc
new file mode 100644
index 000000000..abbbf5c4b
--- /dev/null
+++ b/test/lib/job_manager_test.cc
@@ -0,0 +1,163 @@
+/*
+ Copyright (C) 2012-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/>.
+
+*/
+
+
+/** @file test/job_manager_test.cc
+ * @brief Test JobManager.
+ * @ingroup selfcontained
+ */
+
+
+#include "lib/cross.h"
+#include "lib/job.h"
+#include "lib/job_manager.h"
+#include <boost/test/unit_test.hpp>
+
+
+using std::make_shared;
+using std::shared_ptr;
+using std::string;
+using std::vector;
+
+
+class TestJob : public Job
+{
+public:
+ explicit TestJob (shared_ptr<Film> film)
+ : Job (film)
+ {
+
+ }
+
+ ~TestJob ()
+ {
+ stop_thread ();
+ }
+
+ void set_finished_ok () {
+ set_state (FINISHED_OK);
+ }
+
+ void set_finished_error () {
+ set_state (FINISHED_ERROR);
+ }
+
+ void run () override
+ {
+ while (true) {
+ if (finished ()) {
+ return;
+ }
+ boost::this_thread::interruption_point();
+ }
+ }
+
+ string name () const override {
+ return "";
+ }
+
+ string json_name () const override {
+ return "";
+ }
+};
+
+
+BOOST_AUTO_TEST_CASE (job_manager_test1)
+{
+ shared_ptr<Film> film;
+
+ /* Single job */
+ auto a = make_shared<TestJob>(film);
+
+ JobManager::instance()->add (a);
+ dcpomatic_sleep_seconds (1);
+ BOOST_CHECK (a->running());
+ a->set_finished_ok ();
+ dcpomatic_sleep_seconds (2);
+ BOOST_CHECK (a->finished_ok());
+}
+
+
+BOOST_AUTO_TEST_CASE (job_manager_test2)
+{
+ shared_ptr<Film> film;
+
+ vector<shared_ptr<TestJob>> jobs;
+ for (int i = 0; i < 16; ++i) {
+ auto job = make_shared<TestJob>(film);
+ jobs.push_back (job);
+ JobManager::instance()->add (job);
+ }
+
+ dcpomatic_sleep_seconds (1);
+ BOOST_CHECK (jobs[0]->running());
+ jobs[0]->set_finished_ok();
+
+ dcpomatic_sleep_seconds (1);
+ BOOST_CHECK (!jobs[0]->running());
+ BOOST_CHECK (jobs[1]->running());
+
+ /* Push our jobs[5] to the top of the list */
+ for (int i = 0; i < 5; ++i) {
+ JobManager::instance()->increase_priority(jobs[5]);
+ }
+
+ dcpomatic_sleep_seconds (1);
+ for (int i = 0; i < 16; ++i) {
+ BOOST_CHECK (i == 5 ? jobs[i]->running() : !jobs[i]->running());
+ }
+
+ /* Set any jobs that are started to be finished, until they're all finished */
+ while (true) {
+ if (std::find_if(jobs.begin(), jobs.end(), [](shared_ptr<Job> job) { return !job->finished_ok(); }) == jobs.end()) {
+ break;
+ }
+
+ for (auto job: jobs) {
+ if (job->running()) {
+ job->set_finished_ok();
+ }
+ }
+ }
+
+ BOOST_REQUIRE (!wait_for_jobs());
+}
+
+
+BOOST_AUTO_TEST_CASE(cancel_job_test)
+{
+ shared_ptr<Film> film;
+
+ vector<shared_ptr<TestJob>> jobs;
+ for (int i = 0; i < 2; ++i) {
+ auto job = make_shared<TestJob>(film);
+ jobs.push_back(job);
+ JobManager::instance()->add(job);
+ }
+
+ jobs[1]->cancel();
+ jobs[0]->cancel();
+
+ dcpomatic_sleep_seconds(5);
+
+ BOOST_CHECK(jobs[0]->finished_cancelled());
+ BOOST_CHECK(jobs[1]->finished_cancelled());
+}
+
diff --git a/test/lib/kdm_cli_test.cc b/test/lib/kdm_cli_test.cc
new file mode 100644
index 000000000..fe2244732
--- /dev/null
+++ b/test/lib/kdm_cli_test.cc
@@ -0,0 +1,362 @@
+/*
+ Copyright (C) 2022 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/cinema.h"
+#include "lib/cinema_list.h"
+#include "lib/config.h"
+#include "lib/content_factory.h"
+#include "lib/cross.h"
+#include "lib/dkdm_wrapper.h"
+#include "lib/film.h"
+#include "lib/kdm_cli.h"
+#include "lib/screen.h"
+#include "lib/trusted_device.h"
+#include "../test.h"
+#include <boost/algorithm/string/predicate.hpp>
+#include <boost/filesystem.hpp>
+#include <boost/test/unit_test.hpp>
+#include <iostream>
+
+
+using std::dynamic_pointer_cast;
+using std::string;
+using std::vector;
+using boost::optional;
+
+
+optional<string>
+run(vector<string> const& args, vector<string>& output, bool dump_errors = true)
+{
+ std::vector<char*> argv(args.size());
+ for (auto i = 0U; i < args.size(); ++i) {
+ argv[i] = const_cast<char*>(args[i].c_str());
+ }
+
+ auto error = kdm_cli(args.size(), argv.data(), [&output](string s) { output.push_back(s); });
+ if (error && dump_errors) {
+ std::cout << *error << "\n";
+ }
+
+ return error;
+}
+
+
+BOOST_AUTO_TEST_CASE (kdm_cli_test_certificate)
+{
+ vector<string> args = {
+ "kdm_cli",
+ "--verbose",
+ "--valid-from", "now",
+ "--valid-duration", "2 weeks",
+ "--projector-certificate", "test/data/cert.pem",
+ "-S", "my great screen",
+ "-o", "build/test",
+ "test/data/dkdm.xml"
+ };
+
+ boost::filesystem::path const kdm_filename = "build/test/KDM_Test_FTR-1_F-133_XX-XX_MOS_2K_20220109_SMPTE_OV__my_great_screen.xml";
+ boost::system::error_code ec;
+ boost::filesystem::remove(kdm_filename, ec);
+
+ vector<string> output;
+ auto error = run(args, output);
+ BOOST_CHECK (!error);
+
+ BOOST_CHECK(boost::filesystem::exists(kdm_filename));
+}
+
+
+BOOST_AUTO_TEST_CASE(kdm_cli_specify_decryption_key_test)
+{
+ using boost::filesystem::path;
+
+ ConfigRestorer cr;
+
+ path const dir = "build/test/kdm_cli_specify_decryption_key_test";
+
+ boost::system::error_code ec;
+ boost::filesystem::remove_all(dir, ec);
+ boost::filesystem::create_directories(dir);
+
+ dcp::CertificateChain chain(openssl_path(), 365);
+ dcp::write_string_to_file(chain.leaf().certificate(true), dir / "cert.pem");
+ dcp::write_string_to_file(*chain.key(), dir / "key.pem");
+
+ vector<string> make_args = {
+ "kdm_cli",
+ "--valid-from", "now",
+ "--valid-duration", "2 weeks",
+ "--projector-certificate", path(dir / "cert.pem").string(),
+ "-S", "base",
+ "-o", dir.string(),
+ "test/data/dkdm.xml"
+ };
+
+ vector<string> output;
+ auto error = run(make_args, output);
+ BOOST_CHECK(!error);
+
+ vector<string> bad_args = {
+ "kdm_cli",
+ "--valid-from", "now",
+ "--valid-duration", "2 weeks",
+ "--projector-certificate", path(dir / "cert.pem").string(),
+ "-S", "bad",
+ "-o", dir.string(),
+ path(dir / "KDM_Test_FTR-1_F-133_XX-XX_MOS_2K_20220109_SMPTE_OV__base.xml").string()
+ };
+
+ /* This should fail because we're using the wrong decryption certificate */
+ output.clear();
+ error = run(bad_args, output, false);
+ BOOST_REQUIRE(error);
+ BOOST_CHECK_MESSAGE(error->find("Could not decrypt KDM") != string::npos, "Error was " << *error);
+
+ vector<string> good_args = {
+ "kdm_cli",
+ "--valid-from", "now",
+ "--valid-duration", "2 weeks",
+ "--projector-certificate", path(dir / "cert.pem").string(),
+ "--decryption-key", path(dir / "key.pem").string(),
+ "-S", "good",
+ "-o", dir.string(),
+ path(dir / "KDM_Test_FTR-1_F-133_XX-XX_MOS_2K_20220109_SMPTE_OV__base.xml").string()
+ };
+
+ /* This should succeed */
+ output.clear();
+ error = run(good_args, output);
+ BOOST_CHECK(!error);
+}
+
+
+static
+void
+setup_test_config()
+{
+ auto config = Config::instance();
+ auto const cert = dcp::Certificate(dcp::file_to_string("test/data/cert.pem"));
+
+ CinemaList cinemas(config->cinemas_file());
+
+ auto cinema_a = cinemas.add_cinema({"Dean's Screens", {}, "", dcp::UTCOffset()});
+ cinemas.add_screen(cinema_a, {"Screen 1", "", cert, boost::none, {}});
+ cinemas.add_screen(cinema_a, {"Screen 2", "", cert, boost::none, {}});
+ cinemas.add_screen(cinema_a, {"Screen 3", "", cert, boost::none, {}});
+
+ auto cinema_b = cinemas.add_cinema({"Floyd's Celluloid", {}, "", dcp::UTCOffset()});
+ cinemas.add_screen(cinema_b, {"Foo", "", cert, boost::none, std::vector<TrustedDevice>()});
+ cinemas.add_screen(cinema_b, {"Bar", "", cert, boost::none, std::vector<TrustedDevice>()});
+}
+
+
+BOOST_AUTO_TEST_CASE(kdm_cli_select_cinema)
+{
+ ConfigRestorer cr;
+
+ setup_test_config();
+
+ vector<boost::filesystem::path> kdm_filenames = {
+ "build/test/KDM_Test_FTR-1_F-133_XX-XX_MOS_2K_20220109_SMPTE_OV_Floyds_Celluloid_Foo.xml",
+ "build/test/KDM_Test_FTR-1_F-133_XX-XX_MOS_2K_20220109_SMPTE_OV_Floyds_Celluloid_Bar.xml"
+ };
+
+ for (auto path: kdm_filenames) {
+ boost::system::error_code ec;
+ boost::filesystem::remove(path, ec);
+ }
+
+ vector<string> args = {
+ "kdm_cli",
+ "--verbose",
+ "--valid-from", "now",
+ "--valid-duration", "2 weeks",
+ "-c", "Floyd's Celluloid",
+ "-o", "build/test",
+ "test/data/dkdm.xml"
+ };
+
+ vector<string> output;
+ auto error = run(args, output);
+ BOOST_CHECK(!error);
+
+ BOOST_REQUIRE_EQUAL(output.size(), 2U);
+ BOOST_CHECK(boost::algorithm::starts_with(output[0], "Making KDMs valid from"));
+ BOOST_CHECK_EQUAL(output[1], "Wrote 2 KDM files to build/test");
+
+ for (auto path: kdm_filenames) {
+ BOOST_CHECK(boost::filesystem::exists(path));
+ }
+}
+
+
+BOOST_AUTO_TEST_CASE(kdm_cli_select_screen)
+{
+ ConfigRestorer cr;
+
+ setup_test_config();
+
+ boost::filesystem::path kdm_filename = "build/test/KDM_Test_FTR-1_F-133_XX-XX_MOS_2K_20220109_SMPTE_OV_Deans_Screens_Screen_2.xml";
+
+ boost::system::error_code ec;
+ boost::filesystem::remove(kdm_filename, ec);
+
+ vector<string> args = {
+ "kdm_cli",
+ "--verbose",
+ "--valid-from", "now",
+ "--valid-duration", "2 weeks",
+ "-c", "Dean's Screens",
+ "-S", "Screen 2",
+ "-o", "build/test",
+ "test/data/dkdm.xml"
+ };
+
+ vector<string> output;
+ auto error = run(args, output);
+ BOOST_CHECK(!error);
+
+ BOOST_REQUIRE_EQUAL(output.size(), 2U);
+ BOOST_CHECK(boost::algorithm::starts_with(output[0], "Making KDMs valid from"));
+ BOOST_CHECK_EQUAL(output[1], "Wrote 1 KDM files to build/test");
+
+ BOOST_CHECK(boost::filesystem::exists(kdm_filename));
+}
+
+
+BOOST_AUTO_TEST_CASE(kdm_cli_specify_cinemas_file)
+{
+ ConfigRestorer cr;
+
+ setup_test_config();
+
+ vector<string> args = {
+ "kdm_cli",
+ "--cinemas-file",
+ "test/data/cinemas.sqlite3",
+ "list-cinemas"
+ };
+
+ vector<string> output;
+ auto const error = run(args, output);
+ BOOST_CHECK(!error);
+
+ BOOST_REQUIRE_EQUAL(output.size(), 3U);
+ BOOST_CHECK_EQUAL(output[0], "classy joint ()");
+ BOOST_CHECK_EQUAL(output[1], "Great (julie@tinyscreen.com)");
+ BOOST_CHECK_EQUAL(output[2], "stinking dump (bob@odourscreen.com, alice@whiff.com)");
+}
+
+
+BOOST_AUTO_TEST_CASE(kdm_cli_specify_cert)
+{
+ boost::filesystem::path kdm_filename = "build/test/KDM_KDMCLI__.xml";
+
+ boost::system::error_code ec;
+ boost::filesystem::remove(kdm_filename, ec);
+
+ auto film = new_test_film("kdm_cli_specify_cert", content_factory("test/data/flat_red.png"));
+ film->set_encrypted(true);
+ film->set_name("KDMCLI");
+ film->set_use_isdcf_name(false);
+ make_and_verify_dcp(film);
+
+ vector<string> args = {
+ "kdm_cli",
+ "--valid-from", "2024-01-01 10:10:10",
+ "--valid-duration", "2 weeks",
+ "-C", "test/data/cert.pem",
+ "-o", "build/test",
+ "create",
+ "build/test/kdm_cli_specify_cert"
+ };
+
+ vector<string> output;
+ auto error = run(args, output);
+ BOOST_CHECK(!error);
+
+ BOOST_CHECK(output.empty());
+ BOOST_CHECK(boost::filesystem::exists(kdm_filename));
+}
+
+
+BOOST_AUTO_TEST_CASE(kdm_cli_time)
+{
+ ConfigRestorer cr;
+
+ setup_test_config();
+
+ boost::filesystem::path kdm_filename = "build/test/KDM_Test_FTR-1_F-133_XX-XX_MOS_2K_20220109_SMPTE_OV_Deans_Screens_Screen_2.xml";
+
+ boost::system::error_code ec;
+ boost::filesystem::remove(kdm_filename, ec);
+
+ dcp::LocalTime now;
+ now.add_days(2);
+
+ vector<string> args = {
+ "kdm_cli",
+ "--verbose",
+ "--valid-from", now.as_string(),
+ "--valid-duration", "2 weeks",
+ "-c", "Dean's Screens",
+ "-S", "Screen 2",
+ "-o", "build/test",
+ "test/data/dkdm.xml"
+ };
+
+ vector<string> output;
+ auto error = run(args, output);
+ BOOST_CHECK(!error);
+
+ BOOST_REQUIRE_EQUAL(output.size(), 2U);
+ BOOST_CHECK(boost::algorithm::starts_with(output[0], "Making KDMs valid from"));
+ BOOST_CHECK_EQUAL(output[1], "Wrote 1 KDM files to build/test");
+
+ BOOST_CHECK(boost::filesystem::exists(kdm_filename));
+}
+
+
+BOOST_AUTO_TEST_CASE(kdm_cli_add_dkdm)
+{
+ ConfigRestorer cr;
+
+ setup_test_config();
+
+ BOOST_CHECK_EQUAL(Config::instance()->dkdms()->children().size(), 0U);
+
+ vector<string> args = {
+ "kdm_cli",
+ "add-dkdm",
+ "test/data/dkdm.xml"
+ };
+
+ vector<string> output;
+ auto error = run(args, output);
+ BOOST_CHECK(!error);
+
+ auto dkdms = Config::instance()->dkdms()->children();
+ BOOST_CHECK_EQUAL(dkdms.size(), 1U);
+ auto dkdm = dynamic_pointer_cast<DKDM>(dkdms.front());
+ BOOST_CHECK(dkdm);
+ BOOST_CHECK_EQUAL(dkdm->dkdm().as_xml(), dcp::file_to_string("test/data/dkdm.xml"));
+}
+
diff --git a/test/lib/kdm_naming_test.cc b/test/lib/kdm_naming_test.cc
new file mode 100644
index 000000000..cbf151d90
--- /dev/null
+++ b/test/lib/kdm_naming_test.cc
@@ -0,0 +1,259 @@
+/*
+ Copyright (C) 2020-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 "lib/cinema.h"
+#include "lib/cinema_list.h"
+#include "lib/config.h"
+#include "lib/content_factory.h"
+#include "lib/film.h"
+#include "lib/kdm_with_metadata.h"
+#include "lib/screen.h"
+#include "../test.h"
+#include <boost/test/unit_test.hpp>
+
+
+using std::dynamic_pointer_cast;
+using std::list;
+using std::make_shared;
+using std::pair;
+using std::shared_ptr;
+using std::string;
+using std::vector;
+using boost::optional;
+
+
+static
+bool
+confirm_overwrite (boost::filesystem::path)
+{
+ return true;
+}
+
+
+struct Context
+{
+ Context()
+ {
+ CinemaList cinemas;
+
+ auto crypt_cert = Config::instance()->decryption_chain()->leaf();
+
+ cinema_a = cinemas.add_cinema({"Cinema A", {}, "", dcp::UTCOffset(4, 30)});
+ cinema_a_screen_1 = cinemas.add_screen(cinema_a, {"Screen 1", "", crypt_cert, boost::none, {}});
+ cinema_a_screen_2 = cinemas.add_screen(cinema_a, {"Screen 2", "", crypt_cert, boost::none, {}});
+
+ cinema_b = cinemas.add_cinema({"Cinema B", {}, "", dcp::UTCOffset(-1, 0)});
+ cinema_b_screen_x = cinemas.add_screen(cinema_b, {"Screen X", "", crypt_cert, boost::none, {}});
+ cinema_b_screen_y = cinemas.add_screen(cinema_b, {"Screen Y", "", crypt_cert, boost::none, {}});
+ cinema_b_screen_z = cinemas.add_screen(cinema_b, {"Screen Z", "", crypt_cert, boost::none, {}});
+ }
+
+ CinemaID cinema_a = 0;
+ CinemaID cinema_b = 0;
+ ScreenID cinema_a_screen_1 = 0;
+ ScreenID cinema_a_screen_2 = 0;
+ ScreenID cinema_b_screen_x = 0;
+ ScreenID cinema_b_screen_y = 0;
+ ScreenID cinema_b_screen_z = 0;
+};
+
+
+BOOST_AUTO_TEST_CASE (single_kdm_naming_test)
+{
+ auto c = Config::instance();
+
+ Context context;
+ CinemaList cinemas;
+
+ /* Film */
+ boost::filesystem::remove_all ("build/test/single_kdm_naming_test");
+ auto film = new_test_film("single_kdm_naming_test");
+ film->set_name ("my_great_film");
+ film->examine_and_add_content (content_factory("test/data/flat_black.png")[0]);
+ BOOST_REQUIRE (!wait_for_jobs());
+ film->set_encrypted (true);
+ make_and_verify_dcp (film);
+ auto cpls = film->cpls ();
+ BOOST_REQUIRE(cpls.size() == 1);
+
+ auto sign_cert = c->signer_chain()->leaf();
+
+ dcp::LocalTime from = sign_cert.not_before();
+ from.add_months (2);
+ dcp::LocalTime until = sign_cert.not_after();
+ until.add_months (-2);
+
+ std::vector<KDMCertificatePeriod> period_checks;
+
+ auto cpl = cpls.front().cpl_file;
+ 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 kdm = kdm_for_screen (
+ make_kdm,
+ context.cinema_a,
+ *cinemas.cinema(context.cinema_a),
+ *cinemas.screen(context.cinema_a_screen_1),
+ from,
+ until,
+ dcp::Formulation::MODIFIED_TRANSITIONAL_1,
+ false,
+ optional<int>(),
+ period_checks
+ );
+
+ write_files (
+ { kdm },
+ boost::filesystem::path("build/test/single_kdm_naming_test"),
+ dcp::NameFormat("KDM %c - %s - %f - %b - %e"),
+ &confirm_overwrite
+ );
+
+ auto from_time = from.time_of_day (true, false);
+ boost::algorithm::replace_all (from_time, ":", "-");
+ auto until_time = until.time_of_day (true, false);
+ boost::algorithm::replace_all (until_time, ":", "-");
+
+ auto const dcp_date = boost::gregorian::to_iso_string(film->isdcf_date());
+ auto const ref = String::compose("KDM_Cinema_A_-_Screen_1_-_MyGreatFilm_TST-1_F_XX-XX_MOS_2K_%1_SMPTE_OV_-_%2_%3_-_%4_%5.xml", dcp_date, from.date(), from_time, until.date(), until_time);
+ BOOST_CHECK_MESSAGE (boost::filesystem::exists("build/test/single_kdm_naming_test/" + ref), "File " << ref << " not found");
+}
+
+
+BOOST_AUTO_TEST_CASE(directory_kdm_naming_test)
+{
+ using boost::filesystem::path;
+
+ Context context;
+ CinemaList cinemas;
+
+ /* Film */
+ boost::filesystem::remove_all ("build/test/directory_kdm_naming_test");
+ auto film = new_test_film(
+ "directory_kdm_naming_test",
+ { content_factory("test/data/flat_black.png")[0] }
+ );
+
+ film->set_name ("my_great_film");
+ film->set_encrypted (true);
+ make_and_verify_dcp (film);
+ auto cpls = film->cpls ();
+ BOOST_REQUIRE(cpls.size() == 1);
+
+ auto sign_cert = Config::instance()->signer_chain()->leaf();
+
+ dcp::LocalTime from (sign_cert.not_before());
+ from.add_months (2);
+ dcp::LocalTime until (sign_cert.not_after());
+ until.add_months (-2);
+
+ vector<pair<CinemaID, ScreenID>> screens = {
+ { context.cinema_a, context.cinema_a_screen_2 },
+ { context.cinema_b, context.cinema_b_screen_x },
+ { context.cinema_a, context.cinema_a_screen_1 },
+ { context.cinema_b, context.cinema_b_screen_z }
+ };
+
+ auto const cpl = cpls.front().cpl_file;
+ auto const cpl_id = cpls.front().cpl_id;
+
+ std::vector<KDMCertificatePeriod> period_checks;
+ list<KDMWithMetadataPtr> kdms;
+
+ std::function<dcp::DecryptedKDM (dcp::LocalTime, dcp::LocalTime)> make_kdm = [film, cpls](dcp::LocalTime begin, dcp::LocalTime end) {
+ return film->make_kdm(cpls.front().cpl_file, begin, end);
+ };
+
+ for (auto screen: screens) {
+ auto kdm = kdm_for_screen (
+ make_kdm,
+ screen.first,
+ *cinemas.cinema(screen.first),
+ *cinemas.screen(screen.second),
+ from,
+ until,
+ dcp::Formulation::MODIFIED_TRANSITIONAL_1,
+ false,
+ optional<int>(),
+ period_checks
+ );
+
+ kdms.push_back (kdm);
+ }
+
+ write_directories (
+ collect(kdms),
+ path("build/test/directory_kdm_naming_test"),
+ dcp::NameFormat("%c - %s - %f - %b - %e"),
+#ifdef DCPOMATIC_WINDOWS
+ /* Use a shorter name on Windows so that the paths aren't too long */
+ dcp::NameFormat("KDM %f"),
+#else
+ dcp::NameFormat("KDM %c - %s - %f - %b - %e - %i"),
+#endif
+ &confirm_overwrite
+ );
+
+ auto from_time = from.time_of_day (true, false);
+ boost::algorithm::replace_all (from_time, ":", "-");
+ auto until_time = until.time_of_day (true, false);
+ boost::algorithm::replace_all (until_time, ":", "-");
+
+ auto const dcp_date = boost::gregorian::to_iso_string(film->isdcf_date());
+ auto const dcp_name = String::compose("MyGreatFilm_TST-1_F_XX-XX_MOS_2K_%1_SMPTE_OV", dcp_date);
+ auto const common = String::compose("%1_-_%2_%3_-_%4_%5", dcp_name, from.date(), from_time, until.date(), until_time);
+
+ path const base = "build/test/directory_kdm_naming_test";
+
+ path dir_a = String::compose("Cinema_A_-_%s_-_%1", common);
+ BOOST_CHECK_MESSAGE (boost::filesystem::exists(base / dir_a), "Directory " << dir_a << " not found");
+ path dir_b = String::compose("Cinema_B_-_%s_-_%1", common);
+ BOOST_CHECK_MESSAGE (boost::filesystem::exists(base / dir_b), "Directory " << dir_b << " not found");
+
+#ifdef DCPOMATIC_WINDOWS
+ path ref = String::compose("KDM_%1.xml", dcp_name);
+#else
+ path ref = String::compose("KDM_Cinema_A_-_Screen_2_-_%1_-_%2.xml", common, cpl_id);
+#endif
+ BOOST_CHECK_MESSAGE (boost::filesystem::exists(base / dir_a / ref), "File " << ref << " not found");
+
+#ifdef DCPOMATIC_WINDOWS
+ ref = String::compose("KDM_%1.xml", dcp_name);
+#else
+ ref = String::compose("KDM_Cinema_B_-_Screen_X_-_%1_-_%2.xml", common, cpl_id);
+#endif
+ BOOST_CHECK_MESSAGE (boost::filesystem::exists(base / dir_b / ref), "File " << ref << " not found");
+
+#ifdef DCPOMATIC_WINDOWS
+ ref = String::compose("KDM_%1.xml", dcp_name);
+#else
+ ref = String::compose("KDM_Cinema_A_-_Screen_1_-_%1_-_%2.xml", common, cpl_id);
+#endif
+ BOOST_CHECK_MESSAGE (boost::filesystem::exists(base / dir_a / ref), "File " << ref << " not found");
+
+#ifdef DCPOMATIC_WINDOWS
+ ref = String::compose("KDM_%1.xml", dcp_name);
+#else
+ ref = String::compose("KDM_Cinema_B_-_Screen_Z_-_%1_-_%2.xml", common, cpl_id);
+#endif
+ BOOST_CHECK_MESSAGE (boost::filesystem::exists(base / dir_b / ref), "File " << ref << " not found");
+}
+
diff --git a/test/lib/kdm_util_test.cc b/test/lib/kdm_util_test.cc
new file mode 100644
index 000000000..8426f247a
--- /dev/null
+++ b/test/lib/kdm_util_test.cc
@@ -0,0 +1,95 @@
+/*
+ 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/kdm_util.h"
+#include <dcp/certificate.h>
+#include <dcp/util.h>
+#include <boost/test/unit_test.hpp>
+
+
+BOOST_AUTO_TEST_CASE(check_kdm_and_certificate_validity_periods_good)
+{
+ auto const result = check_kdm_and_certificate_validity_periods(
+ "Bob's Place",
+ "Country",
+ dcp::Certificate(dcp::file_to_string("test/data/cert.pem")),
+ dcp::LocalTime("2023-01-03T10:30:00"),
+ dcp::LocalTime("2050-10-20T14:00:00")
+ );
+
+ BOOST_CHECK(result.overlap == KDMCertificateOverlap::KDM_WITHIN_CERTIFICATE);
+}
+
+
+BOOST_AUTO_TEST_CASE(check_kdm_and_certificate_validity_periods_overlap_start)
+{
+ auto const result = check_kdm_and_certificate_validity_periods(
+ "Bob's Place",
+ "Western",
+ dcp::Certificate(dcp::file_to_string("test/data/cert.pem")),
+ dcp::LocalTime("2011-01-03T10:30:00"),
+ dcp::LocalTime("2050-10-20T14:00:00")
+ );
+
+ BOOST_CHECK(result.overlap == KDMCertificateOverlap::KDM_OVERLAPS_CERTIFICATE);
+}
+
+
+BOOST_AUTO_TEST_CASE(check_kdm_and_certificate_validity_periods_overlap_end)
+{
+ auto const result = check_kdm_and_certificate_validity_periods(
+ "Palace Hotel Ballroom",
+ "Lobby",
+ dcp::Certificate(dcp::file_to_string("test/data/cert.pem")),
+ dcp::LocalTime("2033-01-03T10:30:00"),
+ dcp::LocalTime("2095-10-20T14:00:00")
+ );
+
+ BOOST_CHECK(result.overlap == KDMCertificateOverlap::KDM_OVERLAPS_CERTIFICATE);
+}
+
+
+BOOST_AUTO_TEST_CASE(check_kdm_and_certificate_validity_periods_overlap_start_and_end)
+{
+ auto const result = check_kdm_and_certificate_validity_periods(
+ "Palace Hotel Ballroom",
+ "Stage",
+ dcp::Certificate(dcp::file_to_string("test/data/cert.pem")),
+ dcp::LocalTime("2011-01-03T10:30:00"),
+ dcp::LocalTime("2095-10-20T14:00:00")
+ );
+
+ BOOST_CHECK(result.overlap == KDMCertificateOverlap::KDM_OVERLAPS_CERTIFICATE);
+}
+
+
+BOOST_AUTO_TEST_CASE(check_kdm_and_certificate_validity_periods_outside)
+{
+ auto const result = check_kdm_and_certificate_validity_periods(
+ "Palace Hotel Ballroom",
+ "Drum Riser",
+ dcp::Certificate(dcp::file_to_string("test/data/cert.pem")),
+ dcp::LocalTime("2011-01-03T10:30:00"),
+ dcp::LocalTime("2012-10-20T14:00:00")
+ );
+
+ BOOST_CHECK(result.overlap == KDMCertificateOverlap::KDM_OUTSIDE_CERTIFICATE);
+}
diff --git a/test/lib/low_bitrate_test.cc b/test/lib/low_bitrate_test.cc
new file mode 100644
index 000000000..52b8d54be
--- /dev/null
+++ b/test/lib/low_bitrate_test.cc
@@ -0,0 +1,64 @@
+/*
+ Copyright (C) 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 "lib/dcp_video.h"
+#include "lib/image.h"
+#include "lib/player_video.h"
+#include "lib/raw_image_proxy.h"
+extern "C" {
+#include <libavutil/pixfmt.h>
+}
+#include <boost/test/unit_test.hpp>
+#include <iostream>
+
+
+using std::make_shared;
+using namespace dcpomatic;
+
+
+BOOST_AUTO_TEST_CASE (low_bitrate_test)
+{
+ auto image = make_shared<Image>(AV_PIX_FMT_RGB24, dcp::Size(1998, 1080), Image::Alignment::PADDED);
+ image->make_black ();
+
+ auto proxy = make_shared<RawImageProxy>(image);
+
+ auto frame = make_shared<PlayerVideo>(
+ proxy,
+ Crop(),
+ boost::optional<double>(),
+ dcp::Size(1998, 1080),
+ dcp::Size(1998, 1080),
+ Eyes::BOTH,
+ Part::WHOLE,
+ boost::optional<ColourConversion>(),
+ VideoRange::FULL,
+ std::weak_ptr<Content>(),
+ boost::optional<ContentTime>(),
+ false
+ );
+
+ auto dcp_video = make_shared<DCPVideo>(frame, 0, 24, 100000000, Resolution::TWO_K);
+ auto j2k = dcp_video->encode_locally();
+ BOOST_REQUIRE (j2k.size() >= 16536);
+}
+
+
diff --git a/test/lib/map_cli_test.cc b/test/lib/map_cli_test.cc
new file mode 100644
index 000000000..b299c7e72
--- /dev/null
+++ b/test/lib/map_cli_test.cc
@@ -0,0 +1,613 @@
+/*
+ Copyright (C) 2023 Carl Hetherington <cth@carlh.net>
+
+ This file is part of DCP-o-matic.
+
+ DCP-o-matic is free software; you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation; either version 2 of the License, or
+ (at your option) any later version.
+
+ DCP-o-matic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with DCP-o-matic. If not, see <http://www.gnu.org/licenses/>.
+
+*/
+
+
+#include "lib/config.h"
+#include "lib/content.h"
+#include "lib/dcp_content.h"
+#include "lib/content_factory.h"
+#include "lib/film.h"
+#include "lib/map_cli.h"
+#include "lib/text_content.h"
+#include "../test.h"
+#include <dcp/cpl.h>
+#include <dcp/dcp.h>
+#include <dcp/reel.h>
+#include <dcp/reel_picture_asset.h>
+#include <dcp/reel_sound_asset.h>
+#include <boost/algorithm/string.hpp>
+#include <boost/filesystem.hpp>
+#include <boost/optional.hpp>
+#include <boost/test/unit_test.hpp>
+
+
+using std::dynamic_pointer_cast;
+using std::make_shared;
+using std::shared_ptr;
+using std::string;
+using std::vector;
+using boost::optional;
+
+
+static
+optional<string>
+run(vector<string> const& args, vector<string>& output)
+{
+ vector<char*> argv(args.size() + 1);
+ for (auto i = 0U; i < args.size(); ++i) {
+ argv[i] = const_cast<char*>(args[i].c_str());
+ }
+ argv[args.size()] = nullptr;
+
+ auto error = map_cli(args.size(), argv.data(), [&output](string s) { output.push_back(s); });
+ if (error) {
+ std::cout << *error << "\n";
+ }
+
+ return error;
+}
+
+
+static
+boost::filesystem::path
+find_prefix(boost::filesystem::path dir, string prefix)
+{
+ auto iter = std::find_if(boost::filesystem::directory_iterator(dir), boost::filesystem::directory_iterator(), [prefix](boost::filesystem::path const& p) {
+ return boost::starts_with(p.filename().string(), prefix);
+ });
+
+ BOOST_REQUIRE(iter != boost::filesystem::directory_iterator());
+ return iter->path();
+}
+
+
+static
+boost::filesystem::path
+find_cpl(boost::filesystem::path dir)
+{
+ return find_prefix(dir, "cpl_");
+}
+
+
+/** Map a single DCP into a new DCP */
+BOOST_AUTO_TEST_CASE(map_simple_dcp_copy)
+{
+ string const name = "map_simple_dcp_copy";
+ string const out = String::compose("build/test/%1_out", name);
+
+ auto content = content_factory("test/data/flat_red.png");
+ auto film = new_test_film(name + "_in", content);
+ make_and_verify_dcp(film);
+
+ vector<string> const args = {
+ "map_cli",
+ "-o", out,
+ "-d", film->dir(film->dcp_name()).string(),
+ find_cpl(film->dir(film->dcp_name())).string()
+ };
+
+ boost::filesystem::remove_all(out);
+
+ vector<string> output_messages;
+ auto error = run(args, output_messages);
+ BOOST_CHECK(!error);
+
+ verify_dcp(out, {});
+
+ BOOST_CHECK(boost::filesystem::is_regular_file(find_prefix(out, "j2c_")));
+ BOOST_CHECK(boost::filesystem::is_regular_file(find_prefix(out, "pcm_")));
+}
+
+
+/** Map a single DCP into a new DCP, referring to the CPL by ID */
+BOOST_AUTO_TEST_CASE(map_simple_dcp_copy_by_id)
+{
+ string const name = "map_simple_dcp_copy_by_id";
+ string const out = String::compose("build/test/%1_out", name);
+
+ auto content = content_factory("test/data/flat_red.png");
+ auto film = new_test_film(name + "_in", content);
+ make_and_verify_dcp(film);
+
+ dcp::CPL cpl(find_cpl(film->dir(film->dcp_name())));
+
+ vector<string> const args = {
+ "map_cli",
+ "-o", out,
+ "-d", film->dir(film->dcp_name()).string(),
+ cpl.id()
+ };
+
+ boost::filesystem::remove_all(out);
+
+ vector<string> output_messages;
+ auto error = run(args, output_messages);
+ BOOST_CHECK(!error);
+
+ verify_dcp(out, {});
+
+ BOOST_CHECK(boost::filesystem::is_regular_file(find_prefix(out, "j2c_")));
+ BOOST_CHECK(boost::filesystem::is_regular_file(find_prefix(out, "pcm_")));
+}
+
+
+/** Map a single DCP into a new DCP using the symlink option */
+BOOST_AUTO_TEST_CASE(map_simple_dcp_copy_with_symlinks)
+{
+ string const name = "map_simple_dcp_copy_with_symlinks";
+ string const out = String::compose("build/test/%1_out", name);
+
+ auto content = content_factory("test/data/flat_red.png");
+ auto film = new_test_film(name + "_in", content);
+ make_and_verify_dcp(film);
+
+ vector<string> const args = {
+ "map_cli",
+ "-o", out,
+ "-d", film->dir(film->dcp_name()).string(),
+ "-s",
+ find_cpl(film->dir(film->dcp_name())).string()
+ };
+
+ boost::filesystem::remove_all(out);
+
+ vector<string> output_messages;
+ auto error = run(args, output_messages);
+ BOOST_CHECK(!error);
+
+ /* We can't verify this DCP because the symlinks will make it fail
+ * (as it should be, I think).
+ */
+
+ BOOST_CHECK(boost::filesystem::is_symlink(find_prefix(out, "j2c_")));
+ BOOST_CHECK(boost::filesystem::is_symlink(find_prefix(out, "pcm_")));
+}
+
+
+/** Map a single DCP into a new DCP using the hardlink option */
+BOOST_AUTO_TEST_CASE(map_simple_dcp_copy_with_hardlinks)
+{
+ string const name = "map_simple_dcp_copy_with_hardlinks";
+ string const out = String::compose("build/test/%1_out", name);
+
+ auto content = content_factory("test/data/flat_red.png");
+ auto film = new_test_film(name + "_in", content);
+ make_and_verify_dcp(film);
+
+ vector<string> const args = {
+ "map_cli",
+ "-o", out,
+ "-d", film->dir(film->dcp_name()).string(),
+ "-l",
+ find_cpl(film->dir(film->dcp_name())).string()
+ };
+
+ boost::filesystem::remove_all(out);
+
+ vector<string> output_messages;
+ auto error = run(args, output_messages);
+ BOOST_CHECK(!error);
+
+ verify_dcp(out, {});
+
+ BOOST_CHECK_EQUAL(boost::filesystem::hard_link_count(find_prefix(out, "j2c_")), 2U);
+ BOOST_CHECK_EQUAL(boost::filesystem::hard_link_count(find_prefix(out, "pcm_")), 2U);
+}
+
+
+/** Map a single Interop DCP with subs into a new DCP */
+BOOST_AUTO_TEST_CASE(map_simple_interop_dcp_with_subs)
+{
+ string const name = "map_simple_interop_dcp_with_subs";
+ string const out = String::compose("build/test/%1_out", name);
+
+ auto picture = content_factory("test/data/flat_red.png").front();
+ auto subs = content_factory("test/data/15s.srt").front();
+ auto film = new_test_film(name + "_in", { picture, subs });
+ film->set_interop(true);
+ subs->only_text()->set_language(dcp::LanguageTag("de"));
+ make_and_verify_dcp(film, {dcp::VerificationNote::Code::INVALID_STANDARD});
+
+ vector<string> const args = {
+ "map_cli",
+ "-o", out,
+ "-d", film->dir(film->dcp_name()).string(),
+ find_cpl(film->dir(film->dcp_name())).string()
+ };
+
+ boost::filesystem::remove_all(out);
+
+ vector<string> output_messages;
+ auto error = run(args, output_messages);
+ BOOST_CHECK(!error);
+
+ verify_dcp(out, {dcp::VerificationNote::Code::INVALID_STANDARD});
+}
+
+
+static
+void
+test_map_ov_vf_copy(vector<string> extra_args = {})
+{
+ string const name = "map_ov_vf_copy";
+ string const out = String::compose("build/test/%1_out", name);
+
+ auto ov_content = content_factory("test/data/flat_red.png");
+ auto ov_film = new_test_film(name + "_ov", ov_content);
+ make_and_verify_dcp(ov_film);
+
+ auto const ov_dir = ov_film->dir(ov_film->dcp_name());
+ auto vf_ov = make_shared<DCPContent>(ov_dir);
+ auto vf_sound = content_factory("test/data/sine_440.wav").front();
+ auto vf_film = new_test_film(name + "_vf", { vf_ov, vf_sound });
+ vf_ov->set_reference_video(true);
+ make_and_verify_dcp(vf_film, {dcp::VerificationNote::Code::EXTERNAL_ASSET}, false);
+
+ auto const vf_dir = vf_film->dir(vf_film->dcp_name());
+
+ vector<string> args = {
+ "map_cli",
+ "-o", out,
+ "-d", ov_dir.string(),
+ "-d", vf_dir.string(),
+ find_cpl(vf_dir).string()
+ };
+
+ args.insert(std::end(args), std::begin(extra_args), std::end(extra_args));
+
+ boost::filesystem::remove_all(out);
+
+ vector<string> output_messages;
+ auto error = run(args, output_messages);
+ BOOST_CHECK(!error);
+
+ verify_dcp(out, {});
+
+ check_file(find_file(out, "cpl_"), find_file(vf_dir, "cpl_"));
+ check_file(find_file(out, "j2c_"), find_file(ov_dir, "j2c_"));
+ check_file(find_file(out, "pcm_"), find_file(vf_dir, "pcm_"));
+}
+
+
+/** Map an OV and a VF into a single DCP */
+BOOST_AUTO_TEST_CASE(map_ov_vf_copy)
+{
+ test_map_ov_vf_copy();
+ test_map_ov_vf_copy({"-l"});
+}
+
+
+/** Map an OV and VF into a single DCP, where the VF refers to the OV's assets multiple times */
+BOOST_AUTO_TEST_CASE(map_ov_vf_copy_multiple_reference)
+{
+ string const name = "map_ov_vf_copy_multiple_reference";
+ string const out = String::compose("build/test/%1_out", name);
+
+ auto ov_content = content_factory("test/data/flat_red.png");
+ auto ov_film = new_test_film(name + "_ov", ov_content);
+ make_and_verify_dcp(ov_film);
+
+ auto const ov_dir = ov_film->dir(ov_film->dcp_name());
+
+ auto vf_ov1 = make_shared<DCPContent>(ov_dir);
+ auto vf_ov2 = make_shared<DCPContent>(ov_dir);
+ auto vf_sound = content_factory("test/data/sine_440.wav").front();
+ auto vf_film = new_test_film(name + "_vf", { vf_ov1, vf_ov2, vf_sound });
+ vf_film->set_reel_type(ReelType::BY_VIDEO_CONTENT);
+ vf_ov2->set_position(vf_film, vf_ov1->end(vf_film));
+ vf_ov1->set_reference_video(true);
+ vf_ov2->set_reference_video(true);
+ make_and_verify_dcp(vf_film, {dcp::VerificationNote::Code::EXTERNAL_ASSET}, false);
+
+ auto const vf_dir = vf_film->dir(vf_film->dcp_name());
+
+ vector<string> const args = {
+ "map_cli",
+ "-o", out,
+ "-d", ov_dir.string(),
+ "-d", vf_dir.string(),
+ "-l",
+ find_cpl(vf_dir).string()
+ };
+
+ boost::filesystem::remove_all(out);
+
+ vector<string> output_messages;
+ auto error = run(args, output_messages);
+ BOOST_CHECK(!error);
+
+ verify_dcp(out, {});
+
+ check_file(find_file(out, "cpl_"), find_file(vf_dir, "cpl_"));
+ check_file(find_file(out, "j2c_"), find_file(ov_dir, "j2c_"));
+}
+
+
+/** Map a single DCP into a new DCP using the rename option */
+BOOST_AUTO_TEST_CASE(map_simple_dcp_copy_with_rename)
+{
+ ConfigRestorer cr;
+ Config::instance()->set_dcp_asset_filename_format(dcp::NameFormat("hello%c"));
+ string const name = "map_simple_dcp_copy_with_rename";
+ string const out = String::compose("build/test/%1_out", name);
+
+ auto content = content_factory("test/data/flat_red.png");
+ auto film = new_test_film(name + "_in", content);
+ make_and_verify_dcp(film);
+
+ vector<string> const args = {
+ "map_cli",
+ "-o", out,
+ "-d", film->dir(film->dcp_name()).string(),
+ "-r",
+ find_cpl(film->dir(film->dcp_name())).string()
+ };
+
+ boost::filesystem::remove_all(out);
+
+ vector<string> output_messages;
+ auto error = run(args, output_messages);
+ BOOST_CHECK(!error);
+
+ verify_dcp(out, {});
+
+ dcp::DCP out_dcp(out);
+ out_dcp.read();
+
+ BOOST_REQUIRE_EQUAL(out_dcp.cpls().size(), 1U);
+ auto const cpl = out_dcp.cpls()[0];
+ BOOST_REQUIRE_EQUAL(cpl->reels().size(), 1U);
+ auto const reel = cpl->reels()[0];
+ BOOST_REQUIRE(reel->main_picture());
+ BOOST_REQUIRE(reel->main_sound());
+ auto const picture = reel->main_picture()->asset();
+ BOOST_REQUIRE(picture);
+ auto const sound = reel->main_sound()->asset();
+ BOOST_REQUIRE(sound);
+
+ BOOST_REQUIRE(picture->file());
+ BOOST_CHECK(picture->file().get().filename() == picture->id() + ".mxf");
+
+ BOOST_REQUIRE(sound->file());
+ BOOST_CHECK(sound->file().get().filename() == sound->id() + ".mxf");
+}
+
+
+static
+void
+test_two_cpls_each_with_subs(string name, bool interop)
+{
+ string const out = String::compose("build/test/%1_out", name);
+
+ vector<dcp::VerificationNote::Code> acceptable_errors;
+ if (interop) {
+ acceptable_errors.push_back(dcp::VerificationNote::Code::INVALID_STANDARD);
+ } else {
+ acceptable_errors.push_back(dcp::VerificationNote::Code::MISSING_SUBTITLE_LANGUAGE);
+ acceptable_errors.push_back(dcp::VerificationNote::Code::INVALID_SUBTITLE_FIRST_TEXT_TIME);
+ }
+
+ shared_ptr<Film> films[2];
+ for (auto i = 0; i < 2; ++i) {
+ auto picture = content_factory("test/data/flat_red.png").front();
+ auto subs = content_factory("test/data/15s.srt").front();
+ films[i] = new_test_film(String::compose("%1_%2_in", name, i), { picture, subs });
+ films[i]->set_interop(interop);
+ subs->only_text()->set_language(dcp::LanguageTag("de"));
+ make_and_verify_dcp(films[i], acceptable_errors);
+ }
+
+ vector<string> const args = {
+ "map_cli",
+ "-o", out,
+ "-d", films[0]->dir(films[0]->dcp_name()).string(),
+ "-d", films[1]->dir(films[1]->dcp_name()).string(),
+ find_cpl(films[0]->dir(films[0]->dcp_name())).string(),
+ find_cpl(films[1]->dir(films[1]->dcp_name())).string()
+ };
+
+ boost::filesystem::remove_all(out);
+
+ vector<string> output_messages;
+ auto error = run(args, output_messages);
+ BOOST_CHECK(!error);
+
+ verify_dcp(out, acceptable_errors);
+}
+
+
+BOOST_AUTO_TEST_CASE(map_two_interop_cpls_each_with_subs)
+{
+ test_two_cpls_each_with_subs("map_two_interop_cpls_each_with_subs", true);
+}
+
+
+BOOST_AUTO_TEST_CASE(map_two_smpte_cpls_each_with_subs)
+{
+ test_two_cpls_each_with_subs("map_two_smpte_cpls_each_with_subs", false);
+}
+
+
+BOOST_AUTO_TEST_CASE(map_with_given_config)
+{
+ ConfigRestorer cr;
+
+ string const name = "map_with_given_config";
+ string const out = String::compose("build/test/%1_out", name);
+
+ auto content = content_factory("test/data/flat_red.png");
+ auto film = new_test_film(name + "_in", content);
+ make_and_verify_dcp(film);
+
+ vector<string> const args = {
+ "map_cli",
+ "-o", out,
+ "-d", film->dir(film->dcp_name()).string(),
+ "--config", "test/data/map_with_given_config",
+ find_cpl(film->dir(film->dcp_name())).string()
+ };
+
+ boost::filesystem::remove_all(out);
+ boost::filesystem::remove_all("test/data/map_with_given_config/2.18");
+
+ Config::instance()->drop();
+ vector<string> output_messages;
+ auto error = run(args, output_messages);
+ BOOST_CHECK(!error);
+
+ /* It should be signed by the key in test/data/map_with_given_config, not the one in test/data/signer_key */
+ BOOST_CHECK(dcp::file_to_string(find_file(out, "cpl_")).find("dnQualifier=\\+uOcNN2lPuxpxgd/5vNkkBER0GE=,CN=CS.dcpomatic.smpte-430-2.LEAF,OU=dcpomatic.com,O=dcpomatic.com") != std::string::npos);
+}
+
+
+BOOST_AUTO_TEST_CASE(map_multireel_interop_ov_and_vf_adding_ccaps)
+{
+ string const name = "map_multireel_interop_ov_and_vf_adding_ccaps";
+ string const out = String::compose("build/test/%1_out", name);
+
+ vector<shared_ptr<Content>> video = {
+ content_factory("test/data/flat_red.png")[0],
+ content_factory("test/data/flat_red.png")[0],
+ content_factory("test/data/flat_red.png")[0]
+ };
+
+ auto ov = new_test_film(name + "_ov", { video[0], video[1], video[2] });
+ ov->set_reel_type(ReelType::BY_VIDEO_CONTENT);
+ ov->set_interop(true);
+ make_and_verify_dcp(ov, { dcp::VerificationNote::Code::INVALID_STANDARD });
+
+ auto ov_dcp = make_shared<DCPContent>(ov->dir(ov->dcp_name()));
+
+ vector<shared_ptr<Content>> ccap = {
+ content_factory("test/data/short.srt")[0],
+ content_factory("test/data/short.srt")[0],
+ content_factory("test/data/short.srt")[0]
+ };
+
+ auto vf = new_test_film(name + "_vf", { ov_dcp, ccap[0], ccap[1], ccap[2] });
+ vf->set_interop(true);
+ vf->set_reel_type(ReelType::BY_VIDEO_CONTENT);
+ ov_dcp->set_reference_video(true);
+ ov_dcp->set_reference_audio(true);
+ for (auto i = 0; i < 3; ++i) {
+ ccap[i]->text[0]->set_use(true);
+ ccap[i]->text[0]->set_type(TextType::CLOSED_CAPTION);
+ }
+ make_and_verify_dcp(
+ vf,
+ {
+ dcp::VerificationNote::Code::INVALID_STANDARD,
+ dcp::VerificationNote::Code::INVALID_SUBTITLE_FIRST_TEXT_TIME,
+ dcp::VerificationNote::Code::MISSING_SUBTITLE_LANGUAGE,
+ dcp::VerificationNote::Code::EXTERNAL_ASSET
+ });
+
+ vector<string> const args = {
+ "map_cli",
+ "-o", out,
+ "-d", ov->dir(ov->dcp_name()).string(),
+ "-d", vf->dir(vf->dcp_name()).string(),
+ find_cpl(vf->dir(vf->dcp_name())).string()
+ };
+
+ boost::filesystem::remove_all(out);
+
+ vector<string> output_messages;
+ auto error = run(args, output_messages);
+ BOOST_CHECK(!error);
+
+ verify_dcp(out, { dcp::VerificationNote::Code::INVALID_STANDARD });
+}
+
+
+BOOST_AUTO_TEST_CASE(map_uses_config_for_issuer_and_creator)
+{
+ ConfigRestorer cr;
+
+ Config::instance()->set_dcp_issuer("ostrabagalous");
+ Config::instance()->set_dcp_creator("Fred");
+
+ string const name = "map_uses_config_for_issuer_and_creator";
+ string const out = String::compose("build/test/%1_out", name);
+
+ auto content = content_factory("test/data/flat_red.png");
+ auto film = new_test_film(name + "_in", content);
+ make_and_verify_dcp(film);
+
+ vector<string> const args = {
+ "map_cli",
+ "-o", out,
+ "-d", film->dir(film->dcp_name()).string(),
+ find_cpl(film->dir(film->dcp_name())).string()
+ };
+
+ boost::filesystem::remove_all(out);
+
+ vector<string> output_messages;
+ auto error = run(args, output_messages);
+ BOOST_CHECK(!error);
+
+ cxml::Document assetmap("AssetMap");
+ assetmap.read_file(film->dir(film->dcp_name()) / "ASSETMAP.xml");
+ BOOST_CHECK(assetmap.string_child("Issuer") == "ostrabagalous");
+ BOOST_CHECK(assetmap.string_child("Creator") == "Fred");
+
+ cxml::Document pkl("PackingList");
+ pkl.read_file(find_prefix(out, "pkl_"));
+ BOOST_CHECK(pkl.string_child("Issuer") == "ostrabagalous");
+ BOOST_CHECK(pkl.string_child("Creator") == "Fred");
+}
+
+
+BOOST_AUTO_TEST_CASE(map_handles_interop_png_subs)
+{
+ string const name = "map_handles_interop_png_subs";
+ auto arrietty = content_factory(TestPaths::private_data() / "arrietty_JP-EN.mkv")[0];
+ auto film = new_test_film(name + "_input", { arrietty });
+ film->set_interop(true);
+ arrietty->set_trim_end(dcpomatic::ContentTime::from_seconds(110));
+ arrietty->text[0]->set_use(true);
+ arrietty->text[0]->set_language(dcp::LanguageTag("de"));
+ make_and_verify_dcp(
+ film,
+ {
+ dcp::VerificationNote::Code::INVALID_SUBTITLE_FIRST_TEXT_TIME,
+ dcp::VerificationNote::Code::MISSING_SUBTITLE_LANGUAGE,
+ dcp::VerificationNote::Code::INVALID_STANDARD
+ });
+
+ auto const out = boost::filesystem::path("build") / "test" / (name + "_output");
+
+ vector<string> const args = {
+ "map_cli",
+ "-o", out.string(),
+ "-d", film->dir(film->dcp_name()).string(),
+ find_cpl(film->dir(film->dcp_name())).string()
+ };
+
+ boost::filesystem::remove_all(out);
+
+ vector<string> output_messages;
+ auto error = run(args, output_messages);
+ BOOST_CHECK(!error);
+
+ verify_dcp(out, { dcp::VerificationNote::Code::INVALID_STANDARD });
+}
+
diff --git a/test/lib/markers_test.cc b/test/lib/markers_test.cc
new file mode 100644
index 000000000..62021242b
--- /dev/null
+++ b/test/lib/markers_test.cc
@@ -0,0 +1,154 @@
+/*
+ Copyright (C) 2020-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/>.
+
+*/
+
+
+/** @file test/markers_test
+ * @brief Test SMPTE markers.
+ * @ingroup feature
+ */
+
+
+#include "lib/content_factory.h"
+#include "lib/film.h"
+#include "../test.h"
+#include <dcp/cpl.h>
+#include <dcp/dcp.h>
+#include <dcp/reel_markers_asset.h>
+#include <dcp/reel.h>
+#include <boost/test/unit_test.hpp>
+
+
+using std::string;
+using boost::optional;
+
+
+/** Check that FFOC and LFOC are automatically added if not specified */
+BOOST_AUTO_TEST_CASE (automatic_ffoc_lfoc_markers_test1)
+{
+ string const name = "automatic_ffoc_lfoc_markers_test1";
+ auto film = new_test_film(name);
+ film->examine_and_add_content (content_factory("test/data/flat_red.png")[0]);
+ BOOST_REQUIRE (!wait_for_jobs());
+
+ film->set_interop (false);
+ make_and_verify_dcp (film);
+
+ dcp::DCP dcp (String::compose("build/test/%1/%2", name, film->dcp_name()));
+ dcp.read ();
+ BOOST_REQUIRE_EQUAL (dcp.cpls().size(), 1U);
+ auto cpl = dcp.cpls().front();
+ BOOST_REQUIRE_EQUAL (cpl->reels().size(), 1U);
+ auto reel = cpl->reels()[0];
+ auto markers = reel->main_markers();
+ BOOST_REQUIRE (markers);
+
+ auto ffoc = markers->get (dcp::Marker::FFOC);
+ BOOST_REQUIRE (ffoc);
+ BOOST_CHECK (*ffoc == dcp::Time(0, 0, 0, 1, 24));
+ auto lfoc = markers->get (dcp::Marker::LFOC);
+ BOOST_REQUIRE (lfoc);
+ BOOST_CHECK (*lfoc == dcp::Time(0, 0, 9, 23, 24));
+}
+
+
+/** Check that FFOC and LFOC are not overridden if they are specified */
+BOOST_AUTO_TEST_CASE (automatic_ffoc_lfoc_markers_test2)
+{
+ string const name = "automatic_ffoc_lfoc_markers_test2";
+ auto film = new_test_film(name);
+ film->examine_and_add_content (content_factory("test/data/flat_red.png")[0]);
+ BOOST_REQUIRE (!wait_for_jobs());
+
+ film->set_interop (false);
+ film->set_marker (dcp::Marker::FFOC, dcpomatic::DCPTime::from_seconds(1));
+ film->set_marker (dcp::Marker::LFOC, dcpomatic::DCPTime::from_seconds(9));
+ make_and_verify_dcp (
+ film,
+ {
+ dcp::VerificationNote::Code::INCORRECT_FFOC,
+ dcp::VerificationNote::Code::INCORRECT_LFOC
+ });
+
+ dcp::DCP dcp (String::compose("build/test/%1/%2", name, film->dcp_name()));
+ dcp.read ();
+ BOOST_REQUIRE_EQUAL (dcp.cpls().size(), 1U);
+ auto cpl = dcp.cpls().front();
+ BOOST_REQUIRE_EQUAL (cpl->reels().size(), 1U);
+ auto reel = cpl->reels()[0];
+ auto markers = reel->main_markers();
+ BOOST_REQUIRE (markers);
+
+ auto ffoc = markers->get (dcp::Marker::FFOC);
+ BOOST_REQUIRE (ffoc);
+ BOOST_CHECK (*ffoc == dcp::Time (0, 0, 1, 0, 24));
+ auto lfoc = markers->get (dcp::Marker::LFOC);
+ BOOST_REQUIRE (lfoc);
+ BOOST_CHECK (*lfoc == dcp::Time(0, 0, 9, 0, 24));
+}
+
+
+
+BOOST_AUTO_TEST_CASE(markers_correct_with_reels)
+{
+ string const name = "markers_correct_with_reels";
+ auto content1 = content_factory("test/data/flat_red.png")[0];
+ auto content2 = content_factory("test/data/flat_red.png")[0];
+ auto film = new_test_film(name, { content1, content2});
+
+ film->set_interop(false);
+ film->set_reel_type(ReelType::BY_VIDEO_CONTENT);
+ make_and_verify_dcp(film);
+
+ dcp::DCP dcp(String::compose("build/test/%1/%2", name, film->dcp_name()));
+ dcp.read ();
+ BOOST_REQUIRE_EQUAL(dcp.cpls().size(), 1U);
+ auto cpl = dcp.cpls()[0];
+ BOOST_REQUIRE_EQUAL(cpl->reels().size(), 2U);
+
+ auto markers1 = cpl->reels()[0]->main_markers();
+ BOOST_REQUIRE(markers1);
+ auto ffoc = markers1->get(dcp::Marker::FFOC);
+ BOOST_REQUIRE(ffoc);
+ BOOST_CHECK(*ffoc == dcp::Time(0, 0, 0, 1, 24));
+ auto no_lfoc = markers1->get(dcp::Marker::LFOC);
+ BOOST_CHECK(!no_lfoc);
+
+ auto markers2 = cpl->reels()[1]->main_markers();
+ BOOST_REQUIRE(markers2);
+ auto no_ffoc = markers2->get(dcp::Marker::FFOC);
+ BOOST_REQUIRE(!no_ffoc);
+ auto lfoc = markers2->get(dcp::Marker::LFOC);
+ BOOST_REQUIRE(lfoc);
+ BOOST_CHECK(*lfoc == dcp::Time(0, 0, 9, 23, 24));
+}
+
+
+BOOST_AUTO_TEST_CASE(no_markers_with_interop)
+{
+ string const name = "no_markers_with_interop";
+ auto film = new_test_film(name, content_factory("test/data/flat_red.png"));
+
+ film->set_interop(true);
+ make_and_verify_dcp(film, { dcp::VerificationNote::Code::INVALID_STANDARD });
+
+ auto cpl = find_file(film->dir(film->dcp_name()), "cpl_");
+ BOOST_CHECK(dcp::file_to_string(cpl).find("MainMarkers") == std::string::npos);
+}
+
diff --git a/test/lib/mca_subdescriptors_test.cc b/test/lib/mca_subdescriptors_test.cc
new file mode 100644
index 000000000..de035e3c1
--- /dev/null
+++ b/test/lib/mca_subdescriptors_test.cc
@@ -0,0 +1,174 @@
+/*
+ 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/audio_content.h"
+#include "lib/constants.h"
+#include "lib/content.h"
+#include "lib/content_factory.h"
+#include "lib/film.h"
+#include "../test.h"
+#include <libcxml/cxml.h>
+#include <boost/test/unit_test.hpp>
+
+
+using std::shared_ptr;
+using std::string;
+using std::vector;
+
+
+static
+void
+test_descriptors(int mxf_channels, vector<dcp::Channel> active_channels, vector<string> mca_tag_symbols, string group_name)
+{
+ auto content = content_factory("test/data/flat_red.png");
+ for (auto i = 0; i < mxf_channels; ++i) {
+ content.push_back(content_factory("test/data/C.wav").front());
+ }
+ auto film = new_test_film("mca_subdescriptors_written_correctly", content);
+ film->set_interop(false);
+ film->set_audio_channels(mxf_channels);
+
+ int N = 1;
+ for (auto channel: active_channels) {
+ auto mapping = AudioMapping(1, MAX_DCP_AUDIO_CHANNELS);
+ mapping.set(0, channel, 1);
+ content[N]->audio->set_mapping(mapping);
+ ++N;
+ }
+
+ make_and_verify_dcp(film);
+
+ cxml::Document check("CompositionPlaylist", find_file(film->dir(film->dcp_name()), "cpl_"));
+ vector<string> cpl_mca_tag_symbols;
+
+ auto mca_sub_descriptors = check.node_child("ReelList")->node_child("Reel")->node_child("AssetList")->node_child("CompositionMetadataAsset")->node_child("MCASubDescriptors");
+
+ for (auto node: mca_sub_descriptors->node_children("AudioChannelLabelSubDescriptor")) {
+ cpl_mca_tag_symbols.push_back(node->string_child("MCATagSymbol"));
+ }
+
+ auto const cpl_group_name = mca_sub_descriptors->node_child("SoundfieldGroupLabelSubDescriptor")->string_child("MCATagSymbol");
+
+ BOOST_CHECK(cpl_mca_tag_symbols == mca_tag_symbols);
+ BOOST_CHECK(cpl_group_name == group_name);
+}
+
+
+/* This seems like an impossible case but let's check it anyway */
+BOOST_AUTO_TEST_CASE(mca_subdescriptors_written_correctly_mono_in_2_channel)
+{
+ test_descriptors(2, { dcp::Channel::CENTRE }, { "chL", "chR" }, "sg51");
+}
+
+
+BOOST_AUTO_TEST_CASE(mca_subdescriptors_written_correctly_mono_in_6_channel)
+{
+ test_descriptors(6, { dcp::Channel::CENTRE }, { "chL", "chR", "chC", "chLFE", "chLs", "chRs" }, "sg51");
+}
+
+
+/* If we only have two channels in the MXF we shouldn't see any extra descriptors */
+BOOST_AUTO_TEST_CASE(mca_subdescriptors_written_correctly_stereo_in_2_channel)
+{
+ test_descriptors(2, { dcp::Channel::LEFT, dcp::Channel::RIGHT }, { "chL", "chR" }, "sg51");
+}
+
+
+BOOST_AUTO_TEST_CASE(mca_subdescriptors_written_correctly_stereo_in_6_channel)
+{
+ test_descriptors(6, { dcp::Channel::LEFT, dcp::Channel::RIGHT }, { "chL", "chR", "chC", "chLFE", "chLs", "chRs" }, "sg51");
+}
+
+
+BOOST_AUTO_TEST_CASE(mca_subdescriptors_written_correctly_51)
+{
+ test_descriptors(6,
+ {
+ dcp::Channel::LEFT,
+ dcp::Channel::RIGHT,
+ dcp::Channel::CENTRE,
+ dcp::Channel::LFE,
+ dcp::Channel::LS,
+ dcp::Channel::RS,
+ },
+ { "chL", "chR", "chC", "chLFE", "chLs", "chRs" },
+ "sg51"
+ );
+}
+
+
+BOOST_AUTO_TEST_CASE(mca_subdescriptors_written_correctly_51_with_hi_vi)
+{
+ test_descriptors(8,
+ {
+ dcp::Channel::LEFT,
+ dcp::Channel::RIGHT,
+ dcp::Channel::CENTRE,
+ dcp::Channel::LFE,
+ dcp::Channel::LS,
+ dcp::Channel::RS,
+ dcp::Channel::HI,
+ dcp::Channel::VI,
+ },
+ { "chL", "chR", "chC", "chLFE", "chLs", "chRs", "chHI", "chVIN" },
+ "sg51"
+ );
+}
+
+
+BOOST_AUTO_TEST_CASE(mca_subdescriptors_written_correctly_71)
+{
+ test_descriptors(16,
+ {
+ dcp::Channel::LEFT,
+ dcp::Channel::RIGHT,
+ dcp::Channel::CENTRE,
+ dcp::Channel::LFE,
+ dcp::Channel::LS,
+ dcp::Channel::RS,
+ dcp::Channel::BSL,
+ dcp::Channel::BSR,
+ },
+ { "chL", "chR", "chC", "chLFE", "chLss", "chRss", "chLrs", "chRrs" },
+ "sg71"
+ );
+}
+
+
+BOOST_AUTO_TEST_CASE(mca_subdescriptors_written_correctly_71_with_hi_vi)
+{
+ test_descriptors(16,
+ {
+ dcp::Channel::LEFT,
+ dcp::Channel::RIGHT,
+ dcp::Channel::CENTRE,
+ dcp::Channel::LFE,
+ dcp::Channel::LS,
+ dcp::Channel::RS,
+ dcp::Channel::HI,
+ dcp::Channel::VI,
+ dcp::Channel::BSL,
+ dcp::Channel::BSR,
+ },
+ { "chL", "chR", "chC", "chLFE", "chLss", "chRss", "chHI", "chVIN", "chLrs", "chRrs" },
+ "sg71"
+ );
+}
diff --git a/test/lib/mpeg2_dcp_test.cc b/test/lib/mpeg2_dcp_test.cc
new file mode 100644
index 000000000..d28d7f2df
--- /dev/null
+++ b/test/lib/mpeg2_dcp_test.cc
@@ -0,0 +1,116 @@
+/*
+ Copyright (C) 2025 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/content_factory.h"
+#include "lib/dcp_content.h"
+#include "lib/film.h"
+#include "lib/video_encoding.h"
+#include "../test.h"
+#ifdef DCPOMATIC_LINUX
+#include <boost/process.hpp>
+#endif
+#include <boost/test/unit_test.hpp>
+
+
+using std::make_shared;
+using std::shared_ptr;
+using std::string;
+
+
+static
+float
+mbits_per_second(shared_ptr<const Film> film)
+{
+ auto const mxf = find_file(film->dir(film->dcp_name()), "mpeg2");
+ auto const size = boost::filesystem::file_size(mxf);
+ auto const seconds = film->length().seconds();
+ return ((size * 8) / seconds) / 1e6;
+}
+
+
+#ifdef DCPOMATIC_LINUX
+static
+string
+bitrate_in_header(shared_ptr<const Film> film)
+{
+ namespace bp = boost::process;
+
+ bp::ipstream stream;
+ bp::child child(boost::process::search_path("mediainfo"), find_file(film->dir(film->dcp_name()), "mpeg2"), bp::std_out > stream);
+
+ string line;
+ string rate;
+ while (child.running() && std::getline(stream, line)) {
+ if (line.substr(0, 10) == "Bit rate ") {
+ auto colon = line.find(":");
+ if (colon != string::npos) {
+ rate = line.substr(colon + 2);
+ }
+ }
+ }
+
+ child.wait();
+
+ return rate;
+}
+#endif
+
+
+BOOST_AUTO_TEST_CASE(mpeg2_video_bitrate1)
+{
+ auto content = content_factory(TestPaths::private_data() / "boon_telly.mkv");
+ auto film = new_test_film("mpeg2_video_bitrate", content);
+ film->set_video_bit_rate(VideoEncoding::MPEG2, 25000000);
+ film->set_video_encoding(VideoEncoding::MPEG2);
+ film->set_interop(true);
+
+ make_and_verify_dcp(
+ film,
+ { dcp::VerificationNote::Code::INVALID_STANDARD },
+ false, false
+ );
+
+ BOOST_CHECK_CLOSE(mbits_per_second(film), 25.0047, 1e-3);
+#ifdef DCPOMATIC_LINUX
+ BOOST_CHECK_EQUAL(bitrate_in_header(film), "25.0 Mb/s");
+#endif
+}
+
+
+BOOST_AUTO_TEST_CASE(mpeg2_video_bitrate2)
+{
+ auto content = make_shared<DCPContent>(TestPaths::private_data() / "JourneyToJah_TLR-1_F_EN-DE-FR_CH_51_2K_LOK_20140225_DGL_SMPTE_OV");
+ auto film = new_test_film("mpeg2_video_bitrate", { content });
+ film->set_video_bit_rate(VideoEncoding::MPEG2, 5000000);
+ film->set_video_encoding(VideoEncoding::MPEG2);
+ film->set_interop(true);
+
+ make_and_verify_dcp(
+ film,
+ { dcp::VerificationNote::Code::INVALID_STANDARD },
+ false, false
+ );
+
+ BOOST_CHECK_CLOSE(mbits_per_second(film), 5.02106571, 1e-3);
+#ifdef DCPOMATIC_LINUX
+ BOOST_CHECK_EQUAL(bitrate_in_header(film), "5 000 kb/s");
+#endif
+}
diff --git a/test/lib/no_use_video_test.cc b/test/lib/no_use_video_test.cc
new file mode 100644
index 000000000..ba06cbe10
--- /dev/null
+++ b/test/lib/no_use_video_test.cc
@@ -0,0 +1,159 @@
+/*
+ Copyright (C) 2020-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/>.
+
+*/
+
+
+/** @file test/test_no_use_video.cc
+ * @brief Test some cases where the video parts of inputs are ignored, to
+ * check that the right DCPs are made.
+ * @ingroup completedcp
+ */
+
+
+#include "lib/audio_content.h"
+#include "lib/content.h"
+#include "lib/content_factory.h"
+#include "lib/film.h"
+#include "lib/dcp_content.h"
+#include "lib/video_content.h"
+#include "../test.h"
+#include <dcp/cpl.h>
+#include <dcp/dcp.h>
+#include <dcp/reel.h>
+#include <dcp/reel_sound_asset.h>
+#include <dcp/reel_picture_asset.h>
+#include <boost/test/unit_test.hpp>
+
+
+using std::dynamic_pointer_cast;
+using std::make_shared;
+
+
+/** Overlay two video-only bits of content, don't use the video on one and
+ * make sure the other one is in the DCP.
+ */
+BOOST_AUTO_TEST_CASE (no_use_video_test1)
+{
+ auto film = new_test_film("no_use_video_test1");
+ auto A = content_factory("test/data/flat_red.png")[0];
+ auto B = content_factory("test/data/flat_green.png")[0];
+ film->examine_and_add_content (A);
+ film->examine_and_add_content (B);
+ BOOST_REQUIRE (!wait_for_jobs());
+
+ A->set_position (film, dcpomatic::DCPTime());
+ B->set_position (film, dcpomatic::DCPTime());
+ A->video->set_use (false);
+
+ film->set_audio_channels(16);
+
+ make_and_verify_dcp (film);
+
+ check_dcp ("test/data/no_use_video_test1", film);
+}
+
+
+/** Overlay two muxed sources and disable the video on one */
+BOOST_AUTO_TEST_CASE (no_use_video_test2)
+{
+ Cleanup cl;
+
+ auto A = content_factory(TestPaths::private_data() / "dolby_aurora.vob")[0];
+ auto B = content_factory(TestPaths::private_data() / "big_buck_bunny_trailer_480p.mov")[0];
+ auto film = new_test_film("no_use_video_test2", { A, B }, &cl);
+ film->set_video_bit_rate(VideoEncoding::JPEG2000, 100000000);
+ A->set_position (film, dcpomatic::DCPTime());
+ B->set_position (film, dcpomatic::DCPTime());
+ A->video->set_use (false);
+
+ film->set_audio_channels(16);
+
+ make_and_verify_dcp (film);
+
+ check_dcp (TestPaths::private_data() / "no_use_video_test2", film);
+
+ cl.run();
+}
+
+
+/** Make two DCPs and make a VF with the audio from one and the video from another */
+BOOST_AUTO_TEST_CASE (no_use_video_test3)
+{
+ auto ov_a = new_test_film("no_use_video_test3_ov_a");
+ auto ov_a_pic = content_factory("test/data/flat_red.png")[0];
+ BOOST_REQUIRE (ov_a_pic);
+ auto ov_a_snd = content_factory("test/data/sine_16_48_220_10.wav")[0];
+ BOOST_REQUIRE (ov_a_snd);
+ ov_a->examine_and_add_content (ov_a_pic);
+ ov_a->examine_and_add_content (ov_a_snd);
+ BOOST_REQUIRE (!wait_for_jobs());
+ make_and_verify_dcp (ov_a);
+
+ auto ov_b = new_test_film("no_use_video_test3_ov_b");
+ auto ov_b_pic = content_factory("test/data/flat_green.png")[0];
+ BOOST_REQUIRE (ov_b_pic);
+ auto ov_b_snd = content_factory("test/data/sine_16_48_880_10.wav")[0];
+ BOOST_REQUIRE (ov_b_snd);
+ ov_b->examine_and_add_content (ov_b_pic);
+ ov_b->examine_and_add_content (ov_b_snd);
+ BOOST_REQUIRE (!wait_for_jobs());
+ make_and_verify_dcp (ov_b);
+
+ auto vf = new_test_film("no_use_video_test3_vf");
+ auto A = make_shared<DCPContent>(ov_a->dir(ov_a->dcp_name()));
+ auto B = make_shared<DCPContent>(ov_b->dir(ov_b->dcp_name()));
+ vf->examine_and_add_content (A);
+ vf->examine_and_add_content (B);
+ BOOST_REQUIRE (!wait_for_jobs());
+
+ A->set_position (vf, dcpomatic::DCPTime());
+ A->video->set_use (false);
+ B->set_position (vf, dcpomatic::DCPTime());
+ AudioMapping mapping (16, 16);
+ mapping.make_zero ();
+ B->audio->set_mapping(mapping);
+
+ A->set_reference_audio (true);
+ B->set_reference_video (true);
+
+ make_and_verify_dcp(vf, {dcp::VerificationNote::Code::EXTERNAL_ASSET}, false);
+
+ dcp::DCP ov_a_check (ov_a->dir(ov_a->dcp_name()));
+ ov_a_check.read ();
+ BOOST_REQUIRE_EQUAL (ov_a_check.cpls().size(), 1U);
+ BOOST_REQUIRE_EQUAL (ov_a_check.cpls().front()->reels().size(), 1U);
+ auto ov_a_reel (ov_a_check.cpls().front()->reels().front());
+
+ dcp::DCP ov_b_check (ov_b->dir(ov_b->dcp_name()));
+ ov_b_check.read ();
+ BOOST_REQUIRE_EQUAL (ov_b_check.cpls().size(), 1U);
+ BOOST_REQUIRE_EQUAL (ov_b_check.cpls().front()->reels().size(), 1U);
+ auto ov_b_reel (ov_b_check.cpls().front()->reels().front());
+
+ dcp::DCP vf_check (vf->dir(vf->dcp_name()));
+ vf_check.read ();
+ BOOST_REQUIRE_EQUAL (vf_check.cpls().size(), 1U);
+ BOOST_REQUIRE_EQUAL (vf_check.cpls().front()->reels().size(), 1U);
+ auto vf_reel (vf_check.cpls().front()->reels().front());
+
+ BOOST_CHECK_EQUAL (vf_reel->main_picture()->asset_ref().id(), ov_b_reel->main_picture()->asset_ref().id());
+ BOOST_CHECK_EQUAL (vf_reel->main_sound()->asset_ref().id(), ov_a_reel->main_sound()->asset_ref().id());
+}
+
+
diff --git a/test/lib/open_caption_test.cc b/test/lib/open_caption_test.cc
new file mode 100644
index 000000000..62ce89d94
--- /dev/null
+++ b/test/lib/open_caption_test.cc
@@ -0,0 +1,46 @@
+/*
+ Copyright (C) 2025 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/content_factory.h"
+#include "lib/text_content.h"
+#include "../test.h"
+#include <boost/test/unit_test.hpp>
+
+
+BOOST_AUTO_TEST_CASE(basic_open_caption_test)
+{
+ auto content = content_factory("test/data/15s.srt")[0];
+ auto film = new_test_film("basic_open_caption_test", { content });
+
+ content->text[0]->set_type(TextType::OPEN_CAPTION);
+ content->text[0]->set_language(dcp::LanguageTag("de"));
+
+ make_and_verify_dcp(
+ film,
+ {
+ dcp::VerificationNote::Code::MISSING_CPL_METADATA,
+ dcp::VerificationNote::Code::INVALID_SUBTITLE_FIRST_TEXT_TIME
+ }
+ );
+
+}
+
diff --git a/test/lib/optimise_stills_test.cc b/test/lib/optimise_stills_test.cc
new file mode 100644
index 000000000..e3df7b36c
--- /dev/null
+++ b/test/lib/optimise_stills_test.cc
@@ -0,0 +1,97 @@
+/*
+ Copyright (C) 2017-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 "lib/content.h"
+#include "lib/content_factory.h"
+#include "lib/dcp_content_type.h"
+#include "lib/dcpomatic_log.h"
+#include "lib/film.h"
+#include "lib/job_manager.h"
+#include "lib/ratio.h"
+#include "lib/transcode_job.h"
+#include "lib/video_content.h"
+#include "lib/writer.h"
+#include "../test.h"
+#include <boost/algorithm/string.hpp>
+#include <boost/test/unit_test.hpp>
+
+
+using std::dynamic_pointer_cast;
+using std::getline;
+using std::ifstream;
+using std::string;
+using std::vector;
+using boost::starts_with;
+using boost::split;
+
+
+static
+void
+check (string name, int check_full, int check_repeat)
+{
+ /* The encoder will have been destroyed so parse the logs */
+ string log_file = "build/test/" + name + "/log";
+ ifstream log (log_file.c_str());
+ string line;
+ int repeat = 0;
+ int full = 0;
+ while (getline (log, line)) {
+ vector<string> bits;
+ split (bits, line, boost::is_any_of (":"));
+ if (bits.size() >= 4 && starts_with (bits[3], " Wrote")) {
+ split (bits, bits[3], boost::is_any_of (" "));
+ if (bits.size() >= 7) {
+ full = atoi (bits[2].c_str());
+ repeat = atoi (bits[6].c_str());
+ }
+ }
+
+ }
+
+ BOOST_CHECK_EQUAL (full, check_full);
+ BOOST_CHECK_EQUAL (repeat, check_repeat);
+}
+
+
+/** Make a 2D DCP out of a 2D still and check that the J2K encoding is only done once for each frame */
+BOOST_AUTO_TEST_CASE (optimise_stills_test1)
+{
+ auto content = content_factory("test/data/flat_red.png")[0];
+ auto film = new_test_film("optimise_stills_test1", { content });
+ LogSwitcher ls (film->log());
+ make_and_verify_dcp (film);
+
+ check ("optimise_stills_test1", 1, 10 * 24 - 1);
+}
+
+
+/** Make a 3D DCP out of a 3D L/R still and check that the J2K encoding is only done once for L and R */
+BOOST_AUTO_TEST_CASE (optimise_stills_test2)
+{
+ auto content = content_factory("test/data/flat_red.png")[0];
+ auto film = new_test_film("optimise_stills_test2", { content });
+ LogSwitcher ls (film->log());
+ content->video->set_frame_type (VideoFrameType::THREE_D_LEFT_RIGHT);
+ film->set_three_d (true);
+ make_and_verify_dcp (film);
+
+ check ("optimise_stills_test2", 2, 10 * 48 - 2);
+}
diff --git a/test/lib/overlap_video_test.cc b/test/lib/overlap_video_test.cc
new file mode 100644
index 000000000..4b195862c
--- /dev/null
+++ b/test/lib/overlap_video_test.cc
@@ -0,0 +1,123 @@
+/*
+ Copyright (C) 2020-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 "lib/content.h"
+#include "lib/content_factory.h"
+#include "lib/dcpomatic_time.h"
+#include "lib/film.h"
+#include "lib/piece.h"
+#include "lib/player.h"
+#include "lib/video_content.h"
+#include "../test.h"
+#include <dcp/cpl.h>
+#include <dcp/dcp.h>
+#include <dcp/j2k_transcode.h>
+#include <dcp/mono_j2k_picture_asset.h>
+#include <dcp/mono_j2k_picture_frame.h>
+#include <dcp/openjpeg_image.h>
+#include <dcp/reel.h>
+#include <dcp/reel_mono_picture_asset.h>
+#include <boost/test/unit_test.hpp>
+
+
+using std::dynamic_pointer_cast;
+using std::make_shared;
+using std::shared_ptr;
+using std::vector;
+
+
+BOOST_AUTO_TEST_CASE (overlap_video_test1)
+{
+ auto A = content_factory("test/data/flat_red.png")[0];
+ auto B = content_factory("test/data/flat_green.png")[0];
+ auto C = content_factory("test/data/flat_blue.png")[0];
+ auto film = new_test_film("overlap_video_test1", { A, B, C });
+ film->set_sequence (false);
+
+ auto const fps = 24;
+
+ // 01234
+ // AAAAA
+ // B
+ // C
+
+ A->video->set_length(5 * fps);
+ B->video->set_length(1 * fps);
+ C->video->set_length(1 * fps);
+
+ B->set_position(film, dcpomatic::DCPTime::from_seconds(1));
+ C->set_position(film, dcpomatic::DCPTime::from_seconds(3));
+
+ auto player = make_shared<Player>(film, Image::Alignment::COMPACT, false);
+ auto pieces = player->_pieces;
+ BOOST_REQUIRE_EQUAL (pieces.size(), 3U);
+ BOOST_CHECK_EQUAL(pieces[0]->content, A);
+ BOOST_CHECK_EQUAL(pieces[1]->content, B);
+ BOOST_CHECK_EQUAL(pieces[2]->content, C);
+ BOOST_CHECK_EQUAL(pieces[0]->ignore_video.size(), 2U);
+ BOOST_CHECK(pieces[0]->ignore_video[0] == dcpomatic::DCPTimePeriod(dcpomatic::DCPTime::from_seconds(1), dcpomatic::DCPTime::from_seconds(1) + B->length_after_trim(film)));
+ BOOST_CHECK(pieces[0]->ignore_video[1] == dcpomatic::DCPTimePeriod(dcpomatic::DCPTime::from_seconds(3), dcpomatic::DCPTime::from_seconds(3) + C->length_after_trim(film)));
+
+ BOOST_CHECK (player->_black.done());
+
+ make_and_verify_dcp (film);
+
+ dcp::DCP back (film->dir(film->dcp_name()));
+ back.read ();
+ BOOST_REQUIRE_EQUAL (back.cpls().size(), 1U);
+ auto cpl = back.cpls()[0];
+ BOOST_REQUIRE_EQUAL (cpl->reels().size(), 1U);
+ auto reel = cpl->reels()[0];
+ BOOST_REQUIRE (reel->main_picture());
+ auto mono_picture = dynamic_pointer_cast<dcp::ReelMonoPictureAsset>(reel->main_picture());
+ BOOST_REQUIRE (mono_picture);
+ auto asset = mono_picture->mono_j2k_asset();
+ BOOST_REQUIRE (asset);
+ BOOST_CHECK_EQUAL (asset->intrinsic_duration(), fps * 5);
+ auto reader = asset->start_read ();
+
+ auto close = [](shared_ptr<const dcp::OpenJPEGImage> image, vector<int> rgb) {
+ for (int component = 0; component < 3; ++component) {
+ BOOST_REQUIRE(std::abs(image->data(component)[0] - rgb[component]) < 2);
+ }
+ };
+
+ vector<int> const red = { 2808, 2176, 865 };
+ vector<int> const blue = { 2657, 3470, 1742 };
+ vector<int> const green = { 2044, 1437, 3871 };
+
+ for (int i = 0; i < 5 * fps; ++i) {
+ auto frame = reader->get_frame (i);
+ auto image = dcp::decompress_j2k(*frame.get(), 0);
+ if (i < fps) {
+ close(image, red);
+ } else if (i < 2 * fps) {
+ close(image, blue);
+ } else if (i < 3 * fps) {
+ close(image, red);
+ } else if (i < 4 * fps) {
+ close(image, green);
+ } else if (i < 5 * fps) {
+ close(image, red);
+ }
+ }
+}
+
diff --git a/test/lib/pixel_formats_test.cc b/test/lib/pixel_formats_test.cc
new file mode 100644
index 000000000..12a95bd69
--- /dev/null
+++ b/test/lib/pixel_formats_test.cc
@@ -0,0 +1,100 @@
+/*
+ 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/>.
+
+*/
+
+
+/** @file src/pixel_formats_test.cc
+ * @brief Make sure that Image::sample_size() and Image::bytes_per_pixel() return the right
+ * things for various pixel formats.
+ * @ingroup selfcontained
+ * @see test/image_test.cc
+ */
+
+
+#include <boost/test/unit_test.hpp>
+#include <list>
+extern "C" {
+#include <libavutil/pixfmt.h>
+#include <libavcodec/avcodec.h>
+}
+#include "lib/image.h"
+#include <iostream>
+
+
+using std::list;
+using std::cout;
+
+
+/** @struct Case
+ * @brief A test case for pixel_formats_test.
+ */
+struct Case
+{
+ Case (AVPixelFormat f, int c, int l0, int l1, int l2, float b0, float b1, float b2)
+ : format(f)
+ , planes(c)
+ {
+ lines[0] = l0;
+ lines[1] = l1;
+ lines[2] = l2;
+ bpp[0] = b0;
+ bpp[1] = b1;
+ bpp[2] = b2;
+ }
+
+ AVPixelFormat format;
+ int planes;
+ int lines[3];
+ float bpp[3];
+};
+
+
+BOOST_AUTO_TEST_CASE (pixel_formats_test)
+{
+ list<Case> cases = {
+ { AV_PIX_FMT_RGB24, 1, 480, 480, 480, 3, 0, 0 },
+ { AV_PIX_FMT_RGBA, 1, 480, 480, 480, 4, 0, 0 },
+ { AV_PIX_FMT_YUV420P, 3, 480, 240, 240, 1, 0.5, 0.5},
+ { AV_PIX_FMT_YUV422P, 3, 480, 480, 480, 1, 0.5, 0.5},
+ { AV_PIX_FMT_YUV422P10LE, 3, 480, 480, 480, 2, 1, 1 },
+ { AV_PIX_FMT_YUV422P16LE, 3, 480, 480, 480, 2, 1, 1 },
+ { AV_PIX_FMT_UYVY422, 1, 480, 480, 480, 2, 0, 0 },
+ { AV_PIX_FMT_YUV444P, 3, 480, 480, 480, 1, 1, 1 },
+ { AV_PIX_FMT_YUV444P9BE, 3, 480, 480, 480, 2, 2, 2 },
+ { AV_PIX_FMT_YUV444P9LE, 3, 480, 480, 480, 2, 2, 2 },
+ { AV_PIX_FMT_YUV444P10BE, 3, 480, 480, 480, 2, 2, 2 },
+ { AV_PIX_FMT_YUV444P10LE, 3, 480, 480, 480, 2, 2, 2 }
+ };
+
+ for (auto const& i: cases) {
+ auto f = av_frame_alloc ();
+ f->width = 640;
+ f->height = 480;
+ f->format = static_cast<int> (i.format);
+ av_frame_get_buffer (f, true);
+ Image t (f, Image::Alignment::COMPACT);
+ BOOST_CHECK_EQUAL(t.planes(), i.planes);
+ BOOST_CHECK_EQUAL(t.sample_size(0).height, i.lines[0]);
+ BOOST_CHECK_EQUAL(t.sample_size(1).height, i.lines[1]);
+ BOOST_CHECK_EQUAL(t.sample_size(2).height, i.lines[2]);
+ BOOST_CHECK_EQUAL(t.bytes_per_pixel(0), i.bpp[0]);
+ BOOST_CHECK_EQUAL(t.bytes_per_pixel(1), i.bpp[1]);
+ BOOST_CHECK_EQUAL(t.bytes_per_pixel(2), i.bpp[2]);
+ }
+}
diff --git a/test/lib/player_test.cc b/test/lib/player_test.cc
new file mode 100644
index 000000000..a61015a81
--- /dev/null
+++ b/test/lib/player_test.cc
@@ -0,0 +1,759 @@
+/*
+ Copyright (C) 2014-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/>.
+
+*/
+
+
+/** @file test/player_test.cc
+ * @brief Test Player class.
+ * @ingroup selfcontained
+ */
+
+
+#include "lib/audio_buffers.h"
+#include "lib/audio_content.h"
+#include "lib/butler.h"
+#include "lib/compose.hpp"
+#include "lib/config.h"
+#include "lib/constants.h"
+#include "lib/content_factory.h"
+#include "lib/cross.h"
+#include "lib/dcp_content.h"
+#include "lib/dcp_content_type.h"
+#include "lib/dcpomatic_log.h"
+#include "lib/ffmpeg_content.h"
+#include "lib/film.h"
+#include "lib/image_content.h"
+#include "lib/image_png.h"
+#include "lib/player.h"
+#include "lib/ratio.h"
+#include "lib/string_text_file_content.h"
+#include "lib/text_content.h"
+#include "lib/video_content.h"
+#include "../test.h"
+#include <boost/test/unit_test.hpp>
+#include <boost/algorithm/string.hpp>
+#include <iostream>
+
+
+using std::cout;
+using std::list;
+using std::shared_ptr;
+using std::make_shared;
+using std::vector;
+using boost::bind;
+using boost::optional;
+#if BOOST_VERSION >= 106100
+using namespace boost::placeholders;
+#endif
+using namespace dcpomatic;
+
+
+static shared_ptr<AudioBuffers> accumulated;
+
+
+static void
+accumulate (shared_ptr<AudioBuffers> audio, DCPTime)
+{
+ BOOST_REQUIRE (accumulated);
+ accumulated->append (audio);
+}
+
+
+/** Check that the Player correctly generates silence when used with a silent FFmpegContent */
+BOOST_AUTO_TEST_CASE (player_silence_padding_test)
+{
+ auto c = std::make_shared<FFmpegContent>("test/data/test.mp4");
+ auto film = new_test_film("player_silence_padding_test", { c });
+ film->set_audio_channels (6);
+
+ accumulated = std::make_shared<AudioBuffers>(film->audio_channels(), 0);
+
+ Player player(film, Image::Alignment::COMPACT, false);
+ player.Audio.connect(bind(&accumulate, _1, _2));
+ while (!player.pass()) {}
+ BOOST_REQUIRE (accumulated->frames() >= 48000);
+ BOOST_CHECK_EQUAL (accumulated->channels(), film->audio_channels ());
+
+ for (int i = 0; i < 48000; ++i) {
+ for (int c = 0; c < accumulated->channels(); ++c) {
+ BOOST_CHECK_EQUAL (accumulated->data()[c][i], 0);
+ }
+ }
+}
+
+
+/* Test insertion of black frames between separate bits of video content */
+BOOST_AUTO_TEST_CASE (player_black_fill_test)
+{
+ auto contentA = std::make_shared<ImageContent>("test/data/simple_testcard_640x480.png");
+ auto contentB = std::make_shared<ImageContent>("test/data/simple_testcard_640x480.png");
+ auto film = new_test_film("black_fill_test", { contentA, contentB });
+ film->set_dcp_content_type(DCPContentType::from_isdcf_name("FTR"));
+ film->set_sequence (false);
+
+ contentA->video->set_length (3);
+ contentA->set_position (film, DCPTime::from_frames(2, film->video_frame_rate()));
+ contentA->video->set_custom_ratio (1.85);
+ contentB->video->set_length (1);
+ contentB->set_position (film, DCPTime::from_frames(7, film->video_frame_rate()));
+ contentB->video->set_custom_ratio (1.85);
+
+ make_and_verify_dcp (
+ film,
+ {
+ dcp::VerificationNote::Code::MISSING_FFMC_IN_FEATURE,
+ dcp::VerificationNote::Code::MISSING_FFEC_IN_FEATURE
+ });
+
+ boost::filesystem::path ref;
+ ref = "test";
+ ref /= "data";
+ ref /= "black_fill_test";
+
+ boost::filesystem::path check;
+ check = "build";
+ check /= "test";
+ check /= "black_fill_test";
+ check /= film->dcp_name();
+
+ /* This test is concerned with the image, so we'll ignore any
+ * differences in sound between the DCP and the reference to avoid test
+ * failures for unrelated reasons.
+ */
+ check_dcp(ref.string(), check.string(), true);
+}
+
+
+/** Check behaviour with an awkward playlist whose data does not end on a video frame start */
+BOOST_AUTO_TEST_CASE (player_subframe_test)
+{
+ auto A = content_factory("test/data/flat_red.png")[0];
+ auto B = content_factory("test/data/awkward_length.wav")[0];
+ auto film = new_test_film("reels_test7", { A, B });
+ film->set_video_frame_rate (24);
+ A->video->set_length (3 * 24);
+
+ BOOST_CHECK (A->full_length(film) == DCPTime::from_frames(3 * 24, 24));
+ BOOST_CHECK (B->full_length(film) == DCPTime(289920));
+ /* Length should be rounded up from B's length to the next video frame */
+ BOOST_CHECK (film->length() == DCPTime::from_frames(3 * 24 + 1, 24));
+
+ Player player(film, Image::Alignment::COMPACT, false);
+ player.setup_pieces();
+ BOOST_REQUIRE_EQUAL(player._black._periods.size(), 1U);
+ BOOST_CHECK(player._black._periods.front() == DCPTimePeriod(DCPTime::from_frames(3 * 24, 24), DCPTime::from_frames(3 * 24 + 1, 24)));
+ BOOST_REQUIRE_EQUAL(player._silent._periods.size(), 1U);
+ BOOST_CHECK(player._silent._periods.front() == DCPTimePeriod(DCPTime(289920), DCPTime::from_frames(3 * 24 + 1, 24)));
+}
+
+
+static Frame video_frames;
+static Frame audio_frames;
+
+
+static void
+video (shared_ptr<PlayerVideo>, DCPTime)
+{
+ ++video_frames;
+}
+
+static void
+audio (shared_ptr<AudioBuffers> audio, DCPTime)
+{
+ audio_frames += audio->frames();
+}
+
+
+/** Check with a video-only file that the video and audio emissions happen more-or-less together */
+BOOST_AUTO_TEST_CASE (player_interleave_test)
+{
+ auto c = std::make_shared<FFmpegContent>("test/data/test.mp4");
+ auto s = std::make_shared<StringTextFileContent>("test/data/subrip.srt");
+ auto film = new_test_film("ffmpeg_transcoder_basic_test_subs", { c, s });
+ film->set_audio_channels (6);
+
+ Player player(film, Image::Alignment::COMPACT, false);
+ player.Video.connect(bind(&video, _1, _2));
+ player.Audio.connect(bind(&audio, _1, _2));
+ video_frames = audio_frames = 0;
+ while (!player.pass()) {
+ BOOST_CHECK (abs(video_frames - (audio_frames / 2000)) <= 8);
+ }
+}
+
+
+/** Test some seeks towards the start of a DCP with awkward subtitles; see mantis #1085
+ * and a number of others. I thought this was a player seek bug but in fact it was
+ * caused by the subtitle starting just after the start of the video frame and hence
+ * being faded out.
+ */
+BOOST_AUTO_TEST_CASE (player_seek_test)
+{
+ auto film = std::make_shared<Film>(optional<boost::filesystem::path>());
+ auto dcp = std::make_shared<DCPContent>(TestPaths::private_data() / "awkward_subs");
+ film->examine_and_add_content (dcp, true);
+ BOOST_REQUIRE (!wait_for_jobs ());
+ dcp->only_text()->set_use (true);
+
+ Player player(film, Image::Alignment::COMPACT, false);
+ player.set_fast();
+ player.set_always_burn_open_subtitles();
+ player.set_play_referenced();
+
+ auto butler = std::make_shared<Butler>(
+ film, player, AudioMapping(), 2, bind(PlayerVideo::force, AV_PIX_FMT_RGB24), VideoRange::FULL, Image::Alignment::PADDED, true, false, Butler::Audio::DISABLED
+ );
+
+ for (int i = 0; i < 10; ++i) {
+ auto t = DCPTime::from_frames (i, 24);
+ butler->seek (t, true);
+ auto video = butler->get_video(Butler::Behaviour::BLOCKING, 0);
+ BOOST_CHECK_EQUAL(video.second.get(), t.get());
+ write_image(video.first->image(bind(PlayerVideo::force, AV_PIX_FMT_RGB24), VideoRange::FULL, true), String::compose("build/test/player_seek_test_%1.png", i));
+ /* This 14.08 is empirically chosen (hopefully) to accept changes in rendering between the reference and a test machine
+ (17.10 and 16.04 seem to anti-alias a little differently) but to reject gross errors e.g. missing fonts or missing
+ text altogether.
+ */
+ check_image(TestPaths::private_data() / String::compose("player_seek_test_%1.png", i), String::compose("build/test/player_seek_test_%1.png", i), 14.08);
+ }
+}
+
+
+/** Test some more seeks towards the start of a DCP with awkward subtitles */
+BOOST_AUTO_TEST_CASE (player_seek_test2)
+{
+ auto film = std::make_shared<Film>(optional<boost::filesystem::path>());
+ auto dcp = std::make_shared<DCPContent>(TestPaths::private_data() / "awkward_subs2");
+ film->examine_and_add_content (dcp, true);
+ BOOST_REQUIRE (!wait_for_jobs ());
+ dcp->only_text()->set_use (true);
+
+ Player player(film, Image::Alignment::COMPACT, false);
+ player.set_fast();
+ player.set_always_burn_open_subtitles();
+ player.set_play_referenced();
+
+ auto butler = std::make_shared<Butler>
+ (film, player, AudioMapping(), 2, bind(PlayerVideo::force, AV_PIX_FMT_RGB24), VideoRange::FULL, Image::Alignment::PADDED, true, false, Butler::Audio::DISABLED
+ );
+
+ butler->seek(DCPTime::from_seconds(5), true);
+
+ for (int i = 0; i < 10; ++i) {
+ auto t = DCPTime::from_seconds(5) + DCPTime::from_frames (i, 24);
+ butler->seek (t, true);
+ auto video = butler->get_video(Butler::Behaviour::BLOCKING, 0);
+ BOOST_CHECK_EQUAL(video.second.get(), t.get());
+ write_image(
+ video.first->image(bind(PlayerVideo::force, AV_PIX_FMT_RGB24), VideoRange::FULL, true), String::compose("build/test/player_seek_test2_%1.png", i)
+ );
+ check_image(TestPaths::private_data() / String::compose("player_seek_test2_%1.png", i), String::compose("build/test/player_seek_test2_%1.png", i), 14.08);
+ }
+}
+
+
+/** Test a bug when trimmed content follows other content */
+BOOST_AUTO_TEST_CASE (player_trim_test)
+{
+ auto film = new_test_film("player_trim_test");
+ auto A = content_factory("test/data/flat_red.png")[0];
+ film->examine_and_add_content (A);
+ BOOST_REQUIRE (!wait_for_jobs ());
+ A->video->set_length (10 * 24);
+ auto B = content_factory("test/data/flat_red.png")[0];
+ film->examine_and_add_content (B);
+ BOOST_REQUIRE (!wait_for_jobs ());
+ B->video->set_length (10 * 24);
+ B->set_position (film, DCPTime::from_seconds(10));
+ B->set_trim_start(film, ContentTime::from_seconds(2));
+
+ make_and_verify_dcp (film);
+}
+
+
+struct Sub {
+ PlayerText text;
+ TextType type;
+ optional<DCPTextTrack> track;
+ DCPTimePeriod period;
+};
+
+
+static void
+store (list<Sub>* out, PlayerText text, TextType type, optional<DCPTextTrack> track, DCPTimePeriod period)
+{
+ Sub s;
+ s.text = text;
+ s.type = type;
+ s.track = track;
+ s.period = period;
+ out->push_back (s);
+}
+
+
+/** Test ignoring both video and audio */
+BOOST_AUTO_TEST_CASE (player_ignore_video_and_audio_test)
+{
+ auto film = new_test_film("player_ignore_video_and_audio_test");
+ auto ff = content_factory(TestPaths::private_data() / "boon_telly.mkv")[0];
+ film->examine_and_add_content (ff);
+ auto text = content_factory("test/data/subrip.srt")[0];
+ film->examine_and_add_content (text);
+ BOOST_REQUIRE (!wait_for_jobs());
+ text->only_text()->set_type (TextType::CLOSED_CAPTION);
+ text->only_text()->set_use (true);
+
+ Player player(film, Image::Alignment::COMPACT, false);
+ player.set_ignore_video();
+ player.set_ignore_audio();
+
+ list<Sub> out;
+ player.Text.connect(bind (&store, &out, _1, _2, _3, _4));
+ while (!player.pass()) {}
+
+ BOOST_CHECK_EQUAL (out.size(), 6U);
+}
+
+
+/** Trigger a crash due to the assertion failure in Player::emit_audio */
+BOOST_AUTO_TEST_CASE (player_trim_crash)
+{
+ auto film = new_test_film("player_trim_crash");
+ auto boon = content_factory(TestPaths::private_data() / "boon_telly.mkv")[0];
+ film->examine_and_add_content (boon);
+ BOOST_REQUIRE (!wait_for_jobs());
+
+ Player player(film, Image::Alignment::COMPACT, false);
+ player.set_fast();
+ auto butler = std::make_shared<Butler>(
+ film, player, AudioMapping(), 6, bind(&PlayerVideo::force, AV_PIX_FMT_RGB24), VideoRange::FULL, Image::Alignment::COMPACT, true, false, Butler::Audio::ENABLED
+ );
+
+ /* Wait for the butler to fill */
+ dcpomatic_sleep_seconds (5);
+
+ boon->set_trim_start(film, ContentTime::from_seconds(5));
+
+ butler->seek (DCPTime(), true);
+
+ /* Wait for the butler to refill */
+ dcpomatic_sleep_seconds (5);
+
+ butler->rethrow ();
+}
+
+
+/** Test a crash when the gap between the last audio and the start of a silent period is more than 1 sample */
+BOOST_AUTO_TEST_CASE (player_silence_crash)
+{
+ Cleanup cl;
+
+ auto sine = content_factory("test/data/impulse_train.wav")[0];
+ auto film = new_test_film("player_silence_crash", { sine }, &cl);
+ sine->set_video_frame_rate(film, 23.976);
+ make_and_verify_dcp (film, {dcp::VerificationNote::Code::MISSING_CPL_METADATA});
+
+ cl.run();
+}
+
+
+/** Test a crash when processing a 3D DCP */
+BOOST_AUTO_TEST_CASE (player_3d_test_1)
+{
+ auto film = new_test_film("player_3d_test_1a");
+ auto left = content_factory("test/data/flat_red.png")[0];
+ film->examine_and_add_content (left);
+ auto right = content_factory("test/data/flat_blue.png")[0];
+ film->examine_and_add_content (right);
+ BOOST_REQUIRE (!wait_for_jobs());
+
+ left->video->set_frame_type (VideoFrameType::THREE_D_LEFT);
+ left->set_position (film, DCPTime());
+ right->video->set_frame_type (VideoFrameType::THREE_D_RIGHT);
+ right->set_position (film, DCPTime());
+ film->set_three_d (true);
+
+ make_and_verify_dcp (film);
+
+ auto dcp = std::make_shared<DCPContent>(film->dir(film->dcp_name()));
+ auto film2 = new_test_film("player_3d_test_1b", {dcp});
+
+ film2->set_three_d (true);
+ make_and_verify_dcp (film2);
+}
+
+
+/** Test a crash when processing a 3D DCP as content in a 2D project */
+BOOST_AUTO_TEST_CASE (player_3d_test_2)
+{
+ auto left = content_factory("test/data/flat_red.png")[0];
+ auto right = content_factory("test/data/flat_blue.png")[0];
+ auto film = new_test_film("player_3d_test_2a", {left, right});
+
+ left->video->set_frame_type (VideoFrameType::THREE_D_LEFT);
+ left->set_position (film, DCPTime());
+ right->video->set_frame_type (VideoFrameType::THREE_D_RIGHT);
+ right->set_position (film, DCPTime());
+ film->set_three_d (true);
+
+ make_and_verify_dcp (film);
+
+ auto dcp = std::make_shared<DCPContent>(film->dir(film->dcp_name()));
+ auto film2 = new_test_film("player_3d_test_2b", {dcp});
+
+ make_and_verify_dcp (film2);
+}
+
+
+/** Test a crash when there is video-only content at the end of the DCP and a frame-rate conversion is happening;
+ * #1691.
+ */
+BOOST_AUTO_TEST_CASE (player_silence_at_end_crash)
+{
+ /* 25fps DCP with some audio */
+ auto content1 = content_factory("test/data/flat_red.png")[0];
+ auto film1 = new_test_film("player_silence_at_end_crash_1", {content1});
+ content1->video->set_length (25);
+ film1->set_video_frame_rate (25);
+ make_and_verify_dcp (film1);
+
+ /* Make another project importing this DCP */
+ auto content2 = std::make_shared<DCPContent>(film1->dir(film1->dcp_name()));
+ auto film2 = new_test_film("player_silence_at_end_crash_2", {content2});
+
+ /* and importing just the video MXF on its own at the end */
+ optional<boost::filesystem::path> video;
+ for (auto i: boost::filesystem::directory_iterator(film1->dir(film1->dcp_name()))) {
+ if (boost::starts_with(i.path().filename().string(), "j2c_")) {
+ video = i.path();
+ }
+ }
+
+ BOOST_REQUIRE (video);
+ auto content3 = content_factory(*video)[0];
+ film2->examine_and_add_content (content3);
+ BOOST_REQUIRE (!wait_for_jobs());
+ content3->set_position (film2, DCPTime::from_seconds(1.5));
+ film2->set_video_frame_rate (24);
+ make_and_verify_dcp (film2);
+}
+
+
+/** #2257 */
+BOOST_AUTO_TEST_CASE (encrypted_dcp_with_no_kdm_gives_no_butler_error)
+{
+ auto content = content_factory("test/data/flat_red.png")[0];
+ auto film = new_test_film("encrypted_dcp_with_no_kdm_gives_no_butler_error", { content });
+ int constexpr length = 24 * 25;
+ content->video->set_length(length);
+ film->set_encrypted (true);
+ make_and_verify_dcp (
+ film,
+ {
+ dcp::VerificationNote::Code::MISSING_CPL_METADATA,
+ });
+
+ auto content2 = std::make_shared<DCPContent>(film->dir(film->dcp_name()));
+ auto film2 = new_test_film("encrypted_dcp_with_no_kdm_gives_no_butler_error2", { content2 });
+
+ Player player(film, Image::Alignment::COMPACT, false);
+ Butler butler(film2, player, AudioMapping(), 2, bind(PlayerVideo::force, AV_PIX_FMT_RGB24), VideoRange::FULL, Image::Alignment::PADDED, true, false, Butler::Audio::ENABLED);
+
+ float buffer[2000 * 6];
+ for (int i = 0; i < length; ++i) {
+ butler.get_video(Butler::Behaviour::BLOCKING, 0);
+ butler.get_audio(Butler::Behaviour::BLOCKING, buffer, 2000);
+ }
+
+ BOOST_CHECK_NO_THROW(butler.rethrow());
+}
+
+
+BOOST_AUTO_TEST_CASE (interleaved_subtitle_are_emitted_correctly)
+{
+ boost::filesystem::path paths[2] = {
+ "build/test/interleaved_subtitle_are_emitted_correctly1.srt",
+ "build/test/interleaved_subtitle_are_emitted_correctly2.srt"
+ };
+
+ dcp::File subs_file[2] = { dcp::File(paths[0], "w"), dcp::File(paths[1], "w") };
+
+ fprintf(subs_file[0].get(), "1\n00:00:01,000 -> 00:00:02,000\nSub 1/1\n\n");
+ fprintf(subs_file[0].get(), "2\n00:00:05,000 -> 00:00:06,000\nSub 1/2\n\n");
+
+ fprintf(subs_file[1].get(), "1\n00:00:00,500 -> 00:00:01,500\nSub 2/1\n\n");
+ fprintf(subs_file[1].get(), "2\n00:00:02,000 -> 00:00:03,000\nSub 2/2\n\n");
+
+ subs_file[0].close();
+ subs_file[1].close();
+
+ auto subs1 = content_factory(paths[0])[0];
+ auto subs2 = content_factory(paths[1])[0];
+ auto film = new_test_film("interleaved_subtitle_are_emitted_correctly", { subs1, subs2 });
+ film->set_sequence(false);
+ subs1->set_position(film, DCPTime());
+ subs2->set_position(film, DCPTime());
+
+ Player player(film, Image::Alignment::COMPACT, false);
+ dcp::Time last;
+ player.Text.connect([&last](PlayerText text, TextType, optional<DCPTextTrack>, dcpomatic::DCPTimePeriod) {
+ for (auto sub: text.string) {
+ BOOST_CHECK(sub.in() >= last);
+ last = sub.in();
+ }
+ });
+ while (!player.pass()) {}
+}
+
+
+BOOST_AUTO_TEST_CASE(multiple_sound_files_bug)
+{
+ Cleanup cl;
+
+ Config::instance()->set_log_types(Config::instance()->log_types() | LogEntry::TYPE_DEBUG_PLAYER);
+
+ auto A = content_factory(TestPaths::private_data() / "kook" / "1.wav").front();
+ auto B = content_factory(TestPaths::private_data() / "kook" / "2.wav").front();
+ auto C = content_factory(TestPaths::private_data() / "kook" / "3.wav").front();
+
+ auto film = new_test_film("multiple_sound_files_bug", { A, B, C }, &cl);
+ film->set_audio_channels(16);
+ C->set_position(film, DCPTime(3840000));
+
+ make_and_verify_dcp(film, { dcp::VerificationNote::Code::MISSING_CPL_METADATA });
+
+ check_mxf_audio_file(TestPaths::private_data() / "kook" / "reference.mxf", dcp_file(film, "pcm_"));
+
+ cl.run();
+}
+
+
+BOOST_AUTO_TEST_CASE(trimmed_sound_mix_bug_13)
+{
+ auto A = content_factory("test/data/sine_16_48_440_10.wav").front();
+ auto B = content_factory("test/data/sine_16_44.1_440_10.wav").front();
+ auto film = new_test_film("trimmed_sound_mix_bug_13", { A, B });
+ film->set_audio_channels(16);
+
+ A->set_position(film, DCPTime());
+ A->audio->set_gain(-12);
+ B->set_position(film, DCPTime());
+ B->audio->set_gain(-12);
+ B->set_trim_start(film, ContentTime(13));
+
+ make_and_verify_dcp(film, { dcp::VerificationNote::Code::MISSING_CPL_METADATA });
+ check_mxf_audio_file("test/data/trimmed_sound_mix_bug_13.mxf", dcp_file(film, "pcm_"));
+}
+
+
+BOOST_AUTO_TEST_CASE(trimmed_sound_mix_bug_13_frame_rate_change)
+{
+ auto A = content_factory("test/data/sine_16_48_440_10.wav").front();
+ auto B = content_factory("test/data/sine_16_44.1_440_10.wav").front();
+ auto film = new_test_film("trimmed_sound_mix_bug_13_frame_rate_change", { A, B });
+
+ A->set_position(film, DCPTime());
+ A->audio->set_gain(-12);
+ B->set_position(film, DCPTime());
+ B->audio->set_gain(-12);
+ B->set_trim_start(film, ContentTime(13));
+
+ A->set_video_frame_rate(film, 24);
+ B->set_video_frame_rate(film, 24);
+ film->set_video_frame_rate(25);
+ film->set_audio_channels(16);
+
+ make_and_verify_dcp(film, { dcp::VerificationNote::Code::MISSING_CPL_METADATA });
+ check_mxf_audio_file("test/data/trimmed_sound_mix_bug_13_frame_rate_change.mxf", dcp_file(film, "pcm_"));
+}
+
+
+BOOST_AUTO_TEST_CASE(two_d_in_three_d_duplicates)
+{
+ auto A = content_factory("test/data/flat_red.png").front();
+ auto B = content_factory("test/data/flat_green.png").front();
+ auto film = new_test_film("two_d_in_three_d_duplicates", { A, B });
+
+ film->set_three_d(true);
+ B->video->set_frame_type(VideoFrameType::THREE_D_LEFT_RIGHT);
+ B->set_position(film, DCPTime::from_seconds(10));
+ B->video->set_custom_size(dcp::Size(1998, 1080));
+
+ auto player = std::make_shared<Player>(film, film->playlist(), false);
+
+ std::vector<uint8_t> red_line(1998 * 3);
+ for (int i = 0; i < 1998; ++i) {
+ red_line[i * 3] = 255;
+ };
+
+ std::vector<uint8_t> green_line(1998 * 3);
+ for (int i = 0; i < 1998; ++i) {
+ green_line[i * 3 + 1] = 255;
+ };
+
+ Eyes last_eyes = Eyes::RIGHT;
+ optional<DCPTime> last_time;
+ player->Video.connect([&last_eyes, &last_time, &red_line, &green_line](shared_ptr<PlayerVideo> video, dcpomatic::DCPTime time) {
+ BOOST_CHECK(last_eyes != video->eyes());
+ last_eyes = video->eyes();
+ if (video->eyes() == Eyes::LEFT) {
+ BOOST_CHECK(!last_time || time == *last_time + DCPTime::from_frames(1, 24));
+ } else {
+ BOOST_CHECK(time == *last_time);
+ }
+ last_time = time;
+
+ auto image = video->image([](AVPixelFormat) { return AV_PIX_FMT_RGB24; }, VideoRange::FULL, false);
+ auto const size = image->size();
+ for (int y = 0; y < size.height; ++y) {
+ uint8_t* line = image->data()[0] + y * image->stride()[0];
+ if (time < DCPTime::from_seconds(10)) {
+ BOOST_REQUIRE_EQUAL(memcmp(line, red_line.data(), 1998 * 3), 0);
+ } else {
+ BOOST_REQUIRE_EQUAL(memcmp(line, green_line.data(), 1998 * 3), 0);
+ }
+ }
+ });
+
+ BOOST_CHECK(film->length() == DCPTime::from_seconds(20));
+ while (!player->pass()) {}
+}
+
+
+BOOST_AUTO_TEST_CASE(three_d_in_two_d_chooses_left)
+{
+ auto left = content_factory("test/data/flat_red.png").front();
+ auto right = content_factory("test/data/flat_green.png").front();
+ auto mono = content_factory("test/data/flat_blue.png").front();
+
+ auto film = new_test_film("three_d_in_two_d_chooses_left", { left, right, mono });
+
+ left->video->set_frame_type(VideoFrameType::THREE_D_LEFT);
+ left->set_position(film, dcpomatic::DCPTime());
+ right->video->set_frame_type(VideoFrameType::THREE_D_RIGHT);
+ right->set_position(film, dcpomatic::DCPTime());
+
+ mono->set_position(film, dcpomatic::DCPTime::from_seconds(10));
+
+ auto player = std::make_shared<Player>(film, film->playlist(), false);
+
+ std::vector<uint8_t> red_line(1998 * 3);
+ for (int i = 0; i < 1998; ++i) {
+ red_line[i * 3] = 255;
+ };
+
+ std::vector<uint8_t> blue_line(1998 * 3);
+ for (int i = 0; i < 1998; ++i) {
+ blue_line[i * 3 + 2] = 255;
+ };
+
+ optional<DCPTime> last_time;
+ player->Video.connect([&last_time, &red_line, &blue_line](shared_ptr<PlayerVideo> video, dcpomatic::DCPTime time) {
+ BOOST_CHECK(video->eyes() == Eyes::BOTH);
+ BOOST_CHECK(!last_time || time == *last_time + DCPTime::from_frames(1, 24));
+ last_time = time;
+
+ auto image = video->image([](AVPixelFormat) { return AV_PIX_FMT_RGB24; }, VideoRange::FULL, false);
+ auto const size = image->size();
+ for (int y = 0; y < size.height; ++y) {
+ uint8_t* line = image->data()[0] + y * image->stride()[0];
+ if (time < DCPTime::from_seconds(10)) {
+ BOOST_REQUIRE_EQUAL(memcmp(line, red_line.data(), 1998 * 3), 0);
+ } else {
+ BOOST_REQUIRE_EQUAL(memcmp(line, blue_line.data(), 1998 * 3), 0);
+ }
+ }
+ });
+
+ BOOST_CHECK(film->length() == DCPTime::from_seconds(20));
+ while (!player->pass()) {}
+}
+
+
+BOOST_AUTO_TEST_CASE(check_seek_with_no_video)
+{
+ auto content = content_factory(TestPaths::private_data() / "Fight.Club.1999.720p.BRRip.x264-x0r.srt")[0];
+ auto film = new_test_film("check_seek_with_no_video", { content });
+ auto player = std::make_shared<Player>(film, film->playlist(), false);
+
+ boost::signals2::signal<void (std::shared_ptr<PlayerVideo>, dcpomatic::DCPTime)> Video;
+
+ optional<dcpomatic::DCPTime> earliest;
+
+ player->Video.connect(
+ [&earliest](shared_ptr<PlayerVideo>, dcpomatic::DCPTime time) {
+ if (!earliest || time < *earliest) {
+ earliest = time;
+ }
+ });
+
+ player->seek(dcpomatic::DCPTime::from_seconds(60 * 60), false);
+
+ for (int i = 0; i < 10; ++i) {
+ player->pass();
+ }
+
+ BOOST_REQUIRE(earliest);
+ BOOST_CHECK(*earliest >= dcpomatic::DCPTime(60 * 60));
+}
+
+
+BOOST_AUTO_TEST_CASE(unmapped_audio_does_not_raise_buffer_error)
+{
+ auto content = content_factory(TestPaths::private_data() / "arrietty_JP-EN.mkv")[0];
+ auto film = new_test_film("unmapped_audio_does_not_raise_buffer_error", { content });
+
+ content->audio->set_mapping(AudioMapping(6 * 2, MAX_DCP_AUDIO_CHANNELS));
+
+ Player player(film, Image::Alignment::COMPACT, false);
+ Butler butler(film, player, AudioMapping(), 2, bind(PlayerVideo::force, AV_PIX_FMT_RGB24), VideoRange::FULL, Image::Alignment::PADDED, true, false, Butler::Audio::ENABLED);
+
+ /* Wait for the butler thread to run for a while; in the case under test it will throw an exception because
+ * the video buffers are filled but no audio comes.
+ */
+ dcpomatic_sleep_seconds(10);
+ butler.rethrow();
+}
+
+
+BOOST_AUTO_TEST_CASE(frames_are_copied_correctly_for_low_frame_rates)
+{
+ auto content = content_factory("test/data/flat_red.png");
+ auto film = new_test_film("frames_are_copied_correctly_for_low_frame_rates", content);
+
+ content[0]->set_video_frame_rate(film, 10);
+ film->set_video_frame_rate(30);
+
+ Player player(film, Image::Alignment::COMPACT, false);
+ Butler butler(film, player, AudioMapping(), 2, bind(PlayerVideo::force, AV_PIX_FMT_RGB24), VideoRange::FULL, Image::Alignment::PADDED, true, false, Butler::Audio::ENABLED);
+
+ /* Check that only red frames come out - previously there would be some black ones mixed in */
+ for (auto i = 0; i < 24; ++i) {
+ auto frame = butler.get_video(Butler::Behaviour::BLOCKING);
+ auto image = frame.first->image([](AVPixelFormat) { return AV_PIX_FMT_RGB24; }, VideoRange::FULL, false);
+ for (int y = 0; y < image->size().height; ++y) {
+ uint8_t const* p = image->data()[0] + image->stride()[0] * y;
+ for (int x = 0; x < image->size().width; ++x) {
+ BOOST_REQUIRE_EQUAL(p[0], 255);
+ BOOST_REQUIRE_EQUAL(p[1], 0);
+ BOOST_REQUIRE_EQUAL(p[2], 0);
+ }
+ }
+ }
+}
diff --git a/test/lib/playlist_test.cc b/test/lib/playlist_test.cc
new file mode 100644
index 000000000..949d7e043
--- /dev/null
+++ b/test/lib/playlist_test.cc
@@ -0,0 +1,98 @@
+/*
+ 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/content.h"
+#include "lib/content_factory.h"
+#include "lib/film.h"
+#include "lib/playlist.h"
+#include "../test.h"
+#include <boost/test/unit_test.hpp>
+
+
+using std::shared_ptr;
+using std::vector;
+
+
+static
+shared_ptr<Film>
+setup(vector<shared_ptr<Content>>& content, vector<dcpomatic::DCPTime>& positions, vector<dcpomatic::DCPTime>& lengths)
+{
+ for (auto i = 0; i < 3; ++i) {
+ content.push_back(content_factory("test/data/flat_red.png")[0]);
+ }
+
+ auto film = new_test_film("playlist_move_later_test", content);
+
+ for (auto i: content) {
+ positions.push_back(i->position());
+ }
+
+ for (auto i: content) {
+ lengths.push_back(i->length_after_trim(film));
+ }
+
+ return film;
+}
+
+
+BOOST_AUTO_TEST_CASE(playlist_move_later_test1)
+{
+ vector<shared_ptr<Content>> content;
+ vector<dcpomatic::DCPTime> positions;
+ vector<dcpomatic::DCPTime> lengths;
+ auto film = setup(content, positions, lengths);
+
+ film->move_content_later(content[1]);
+
+ auto moved_content = film->content();
+ BOOST_REQUIRE_EQUAL(moved_content.size(), 3U);
+
+ BOOST_CHECK_EQUAL(moved_content[0], content[0]);
+ BOOST_CHECK_EQUAL(moved_content[1], content[2]);
+ BOOST_CHECK_EQUAL(moved_content[2], content[1]);
+
+ BOOST_CHECK(content[0]->position() == positions[0]);
+ BOOST_CHECK(content[1]->position() == positions[1] + lengths[2]);
+ BOOST_CHECK(content[2]->position() == positions[1]);
+}
+
+
+BOOST_AUTO_TEST_CASE(playlist_move_later_test2)
+{
+ vector<shared_ptr<Content>> content;
+ vector<dcpomatic::DCPTime> positions;
+ vector<dcpomatic::DCPTime> lengths;
+ auto film = setup(content, positions, lengths);
+
+ film->move_content_later(content[0]);
+
+ auto moved_content = film->content();
+ BOOST_REQUIRE_EQUAL(moved_content.size(), 3U);
+
+ BOOST_CHECK_EQUAL(moved_content[0], content[1]);
+ BOOST_CHECK_EQUAL(moved_content[1], content[0]);
+ BOOST_CHECK_EQUAL(moved_content[2], content[2]);
+
+ BOOST_CHECK(content[0]->position() == positions[0] + lengths[1]);
+ BOOST_CHECK(content[1]->position() == positions[0]);
+ BOOST_CHECK(content[2]->position() == positions[2]);
+}
+
diff --git a/test/lib/pulldown_detect_test.cc b/test/lib/pulldown_detect_test.cc
new file mode 100644
index 000000000..d3a7e5ed7
--- /dev/null
+++ b/test/lib/pulldown_detect_test.cc
@@ -0,0 +1,37 @@
+/*
+ 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/content.h"
+#include "lib/content_factory.h"
+#include "lib/film.h"
+#include "../test.h"
+#include <boost/test/unit_test.hpp>
+
+
+BOOST_AUTO_TEST_CASE (pulldown_detect_test1)
+{
+ auto content = content_factory(TestPaths::private_data() / "greatbrain.mkv");
+ auto film = new_test_film("pulldown_detect_test1", content);
+
+ BOOST_REQUIRE(static_cast<bool>(content[0]->video_frame_rate()));
+ BOOST_CHECK_CLOSE(content[0]->video_frame_rate().get(), 23.976, 0.1);
+}
+
diff --git a/test/lib/ratio_test.cc b/test/lib/ratio_test.cc
new file mode 100644
index 000000000..c1450e0e9
--- /dev/null
+++ b/test/lib/ratio_test.cc
@@ -0,0 +1,75 @@
+/*
+ Copyright (C) 2012-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/>.
+
+*/
+
+/** @file test/ratio_test.cc
+ * @brief Test Ratio class and fit_ratio_within().
+ * @ingroup selfcontained
+ */
+
+#include <iostream>
+#include <boost/test/unit_test.hpp>
+#include <dcp/util.h>
+#include "lib/ratio.h"
+#include "lib/util.h"
+#include "../test.h"
+
+using std::ostream;
+
+BOOST_AUTO_TEST_CASE (ratio_test)
+{
+ Ratio const * r = Ratio::from_id ("119");
+ BOOST_CHECK (r);
+ BOOST_CHECK_EQUAL (fit_ratio_within (r->ratio(), dcp::Size (2048, 1080)), dcp::Size (1290, 1080));
+
+ r = Ratio::from_id ("133");
+ BOOST_CHECK (r);
+ BOOST_CHECK_EQUAL (fit_ratio_within (r->ratio(), dcp::Size (2048, 1080)), dcp::Size (1440, 1080));
+
+ r = Ratio::from_id ("138");
+ BOOST_CHECK (r);
+ BOOST_CHECK_EQUAL (fit_ratio_within (r->ratio(), dcp::Size (2048, 1080)), dcp::Size (1485, 1080));
+
+ r = Ratio::from_id ("166");
+ BOOST_CHECK (r);
+ BOOST_CHECK_EQUAL (fit_ratio_within (r->ratio(), dcp::Size (2048, 1080)), dcp::Size (1800, 1080));
+
+ r = Ratio::from_id ("178");
+ BOOST_CHECK (r);
+ BOOST_CHECK_EQUAL (fit_ratio_within (r->ratio(), dcp::Size (2048, 1080)), dcp::Size (1920, 1080));
+
+ r = Ratio::from_id ("185");
+ BOOST_CHECK (r);
+ BOOST_CHECK_EQUAL (fit_ratio_within (r->ratio(), dcp::Size (2048, 1080)), dcp::Size (1998, 1080));
+
+ r = Ratio::from_id ("239");
+ BOOST_CHECK (r);
+ BOOST_CHECK_EQUAL (fit_ratio_within (r->ratio(), dcp::Size (2048, 1080)), dcp::Size (2048, 858));
+
+ r = Ratio::from_id ("190");
+ BOOST_CHECK (r);
+ BOOST_CHECK_EQUAL (fit_ratio_within (r->ratio(), dcp::Size (2048, 1080)), dcp::Size (2048, 1080));
+}
+
+
+BOOST_AUTO_TEST_CASE (ratios_use_same_pointers_test)
+{
+ auto const test = Ratio::from_id ("119");
+ BOOST_CHECK_EQUAL (test, Ratio::from_id("119"));
+}
diff --git a/test/lib/recover_test.cc b/test/lib/recover_test.cc
new file mode 100644
index 000000000..d2d23bbbf
--- /dev/null
+++ b/test/lib/recover_test.cc
@@ -0,0 +1,179 @@
+/*
+ Copyright (C) 2014-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/>.
+
+*/
+
+
+/** @file test/recover_test.cc
+ * @brief Test recovery of a DCP transcode after a crash.
+ * @ingroup feature
+ */
+
+
+#include "lib/dcp_content_type.h"
+#include "lib/ffmpeg_content.h"
+#include "lib/film.h"
+#include "lib/image_content.h"
+#include "lib/ratio.h"
+#include "lib/video_content.h"
+#include "../test.h"
+#include <dcp/equality_options.h>
+#include <dcp/mono_j2k_picture_asset.h>
+#include <dcp/stereo_j2k_picture_asset.h>
+#include <boost/test/unit_test.hpp>
+#include <iostream>
+
+
+using std::cout;
+using std::make_shared;
+using std::string;
+#if BOOST_VERSION >= 106100
+using namespace boost::placeholders;
+#endif
+
+
+static void
+note (dcp::NoteType t, string n)
+{
+ if (t == dcp::NoteType::ERROR) {
+ cout << n << "\n";
+ }
+}
+
+
+BOOST_AUTO_TEST_CASE (recover_test_2d)
+{
+ auto content = make_shared<FFmpegContent>("test/data/count300bd24.m2ts");
+ auto film = new_test_film("recover_test_2d", { content });
+ film->set_video_bit_rate(VideoEncoding::JPEG2000, 100000000);
+
+ make_and_verify_dcp (
+ film,
+ {
+ dcp::VerificationNote::Code::MISSING_FFMC_IN_FEATURE,
+ dcp::VerificationNote::Code::MISSING_FFEC_IN_FEATURE
+ });
+
+ auto video = [film]() {
+ return find_file(boost::filesystem::path("build/test/recover_test_2d") / film->dcp_name(false), "j2c_");
+ };
+
+ boost::filesystem::copy_file (
+ video(),
+ "build/test/recover_test_2d/original.mxf"
+ );
+
+ boost::filesystem::resize_file(video(), 2 * 1024 * 1024);
+
+ make_and_verify_dcp(
+ film,
+ {
+ dcp::VerificationNote::Code::MISSING_FFEC_IN_FEATURE,
+ dcp::VerificationNote::Code::MISSING_FFMC_IN_FEATURE
+ },
+ true,
+ /* We end up with two CPLs in this directory, which Clairmeta gives an error for */
+ false
+ );
+
+ auto A = make_shared<dcp::MonoJ2KPictureAsset>("build/test/recover_test_2d/original.mxf");
+ auto B = make_shared<dcp::MonoJ2KPictureAsset>(video());
+
+ dcp::EqualityOptions eq;
+ BOOST_CHECK (A->equals (B, eq, boost::bind (&note, _1, _2)));
+}
+
+
+BOOST_AUTO_TEST_CASE (recover_test_3d, * boost::unit_test::depends_on("recover_test_2d"))
+{
+ auto content = make_shared<ImageContent>("test/data/3d_test");
+ content->video->set_frame_type (VideoFrameType::THREE_D_LEFT_RIGHT);
+ auto film = new_test_film("recover_test_3d", { content });
+ film->set_three_d (true);
+ film->set_video_bit_rate(VideoEncoding::JPEG2000, 100000000);
+
+ make_and_verify_dcp (film, { dcp::VerificationNote::Code::MISSING_FFEC_IN_FEATURE, dcp::VerificationNote::Code::MISSING_FFMC_IN_FEATURE });
+
+ auto video = [film]() {
+ return find_file(boost::filesystem::path("build/test/recover_test_3d") / film->dcp_name(false), "j2c_");
+ };
+
+ boost::filesystem::copy_file (
+ video(),
+ "build/test/recover_test_3d/original.mxf"
+ );
+
+ boost::filesystem::resize_file(video(), 2 * 1024 * 1024);
+
+ make_and_verify_dcp(
+ film,
+ {
+ dcp::VerificationNote::Code::MISSING_FFEC_IN_FEATURE, dcp::VerificationNote::Code::MISSING_FFMC_IN_FEATURE
+ },
+ true,
+ /* We end up with two CPLs in this directory, which Clairmeta gives an error for */
+ false
+ );
+
+ auto A = make_shared<dcp::StereoJ2KPictureAsset>("build/test/recover_test_3d/original.mxf");
+ auto B = make_shared<dcp::StereoJ2KPictureAsset>(video());
+
+ dcp::EqualityOptions eq;
+ BOOST_CHECK (A->equals (B, eq, boost::bind (&note, _1, _2)));
+}
+
+
+BOOST_AUTO_TEST_CASE (recover_test_2d_encrypted, * boost::unit_test::depends_on("recover_test_3d"))
+{
+ auto content = make_shared<FFmpegContent>("test/data/count300bd24.m2ts");
+ auto film = new_test_film("recover_test_2d_encrypted", { content });
+ film->set_encrypted (true);
+ film->_key = dcp::Key("eafcb91c9f5472edf01f3a2404c57258");
+ film->set_video_bit_rate(VideoEncoding::JPEG2000, 100000000);
+
+ make_and_verify_dcp (film, { dcp::VerificationNote::Code::MISSING_FFEC_IN_FEATURE, dcp::VerificationNote::Code::MISSING_FFMC_IN_FEATURE });
+
+ auto video = [film]() {
+ return find_file(boost::filesystem::path("build/test/recover_test_2d_encrypted") / film->dcp_name(false), "j2c_");
+ };
+
+ boost::filesystem::copy_file (
+ video(),
+ "build/test/recover_test_2d_encrypted/original.mxf"
+ );
+
+ boost::filesystem::resize_file(video(), 2 * 1024 * 1024);
+
+ make_and_verify_dcp(
+ film,
+ {
+ dcp::VerificationNote::Code::MISSING_FFEC_IN_FEATURE, dcp::VerificationNote::Code::MISSING_FFMC_IN_FEATURE
+ },
+ true,
+ /* We end up with two CPLs in this directory, which Clairmeta gives an error for */
+ false
+ );
+
+ auto A = make_shared<dcp::MonoJ2KPictureAsset>("build/test/recover_test_2d_encrypted/original.mxf");
+ A->set_key (film->key ());
+ auto B = make_shared<dcp::MonoJ2KPictureAsset>(video());
+ B->set_key (film->key ());
+
+ dcp::EqualityOptions eq;
+ BOOST_CHECK (A->equals (B, eq, boost::bind (&note, _1, _2)));
+}
diff --git a/test/lib/rect_test.cc b/test/lib/rect_test.cc
new file mode 100644
index 000000000..24df33316
--- /dev/null
+++ b/test/lib/rect_test.cc
@@ -0,0 +1,53 @@
+/*
+ Copyright (C) 2016-2021 Carl Hetherington <cth@carlh.net>
+
+ This file is part of DCP-o-matic.
+
+ DCP-o-matic is free software; you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation; either version 2 of the License, or
+ (at your option) any later version.
+
+ DCP-o-matic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with DCP-o-matic. If not, see <http://www.gnu.org/licenses/>.
+
+*/
+
+
+/** @file test/rect_test.cc
+ * @brief Test dcpomatic::Rect class.
+ * @ingroup selfcontained
+ */
+
+
+#include "lib/rect.h"
+#include <boost/test/unit_test.hpp>
+#include <iostream>
+
+
+using boost::optional;
+
+
+BOOST_AUTO_TEST_CASE (rect_test1)
+{
+ dcpomatic::Rect<int> a (0, 0, 100, 100);
+ dcpomatic::Rect<int> b (200, 200, 100, 100);
+ auto c = a.intersection (b);
+ BOOST_CHECK (!c);
+}
+
+
+BOOST_AUTO_TEST_CASE (rect_test2)
+{
+ dcpomatic::Rect<int> a (0, 330, 100, 85);
+ a.extend (dcpomatic::Rect<int>(50, 235, 100, 85));
+ BOOST_CHECK_EQUAL (a.x, 0);
+ BOOST_CHECK_EQUAL (a.y, 235);
+ BOOST_CHECK_EQUAL (a.width, 150);
+ BOOST_CHECK_EQUAL (a.height, 180);
+}
diff --git a/test/lib/reel_writer_test.cc b/test/lib/reel_writer_test.cc
new file mode 100644
index 000000000..876d3ab8d
--- /dev/null
+++ b/test/lib/reel_writer_test.cc
@@ -0,0 +1,146 @@
+/*
+ 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/>.
+
+*/
+
+
+/** @file test/reel_writer_test.cc
+ * @brief Test ReelWriter class.
+ * @ingroup selfcontained
+ */
+
+
+#include "lib/audio_content.h"
+#include "lib/content.h"
+#include "lib/content_factory.h"
+#include "lib/cross.h"
+#include "lib/film.h"
+#include "lib/frame_info.h"
+#include "lib/reel_writer.h"
+#include "lib/video_content.h"
+#include "../test.h"
+#include <dcp/dcp.h>
+#include <dcp/cpl.h>
+#include <dcp/reel.h>
+#include <dcp/reel_picture_asset.h>
+#include <dcp/reel_sound_asset.h>
+#include <boost/test/unit_test.hpp>
+
+
+using std::shared_ptr;
+using std::string;
+using boost::optional;
+
+
+static bool equal(J2KFrameInfo a, dcp::File& file, Frame frame, Eyes eyes)
+{
+ auto b = J2KFrameInfo(file, frame, eyes);
+ return a.offset == b.offset && a.size == b.size && a.hash == b.hash;
+}
+
+
+BOOST_AUTO_TEST_CASE (write_frame_info_test)
+{
+ auto film = new_test_film("write_frame_info_test");
+ dcpomatic::DCPTimePeriod const period (dcpomatic::DCPTime(0), dcpomatic::DCPTime(96000));
+
+ J2KFrameInfo info1(0, 123, "12345678901234567890123456789012");
+ J2KFrameInfo info2(596, 14921, "123acb789f1234ae782012n456339522");
+ J2KFrameInfo info3(12494, 99157123, "xxxxyyyyabc12356ffsfdsf456339522");
+ J2KFrameInfo info4(55512494, 123599157123, "ABCDEFGyabc12356ffsfdsf4563395ZZ");
+
+ {
+ ReelWriter writer(film, period, shared_ptr<Job>(), 0, 1, false, "foo");
+ info1.write(writer._info_file, 0, Eyes::LEFT);
+ info2.write(writer._info_file, 5, Eyes::RIGHT);
+ info3.write(writer._info_file, 10, Eyes::LEFT);
+ }
+
+ auto file1 = dcp::File(film->info_file(period), "rb");
+ BOOST_CHECK(equal(info1, file1, 0, Eyes::LEFT));
+ BOOST_CHECK(equal(info1, file1, 0, Eyes::LEFT));
+ BOOST_CHECK(equal(info2, file1, 5, Eyes::RIGHT));
+ BOOST_CHECK(equal(info1, file1, 0, Eyes::LEFT));
+ BOOST_CHECK(equal(info2, file1, 5, Eyes::RIGHT));
+ BOOST_CHECK(equal(info3, file1, 10, Eyes::LEFT));
+
+ {
+ ReelWriter writer(film, period, shared_ptr<Job>(), 0, 1, false, "foo");
+
+ /* Overwrite one */
+ info4.write(writer._info_file, 5, Eyes::RIGHT);
+ }
+
+ auto file2 = dcp::File(film->info_file(period), "rb");
+ BOOST_CHECK(equal(info1, file2, 0, Eyes::LEFT));
+ BOOST_CHECK(equal(info4, file2, 5, Eyes::RIGHT));
+ BOOST_CHECK(equal(info3, file2, 10, Eyes::LEFT));
+}
+
+
+/** Check that the reel writer correctly re-uses a video asset changed if we remake
+ * a DCP with no video changes.
+ */
+BOOST_AUTO_TEST_CASE (reel_reuse_video_test)
+{
+ /* Make a DCP */
+ auto video = content_factory("test/data/flat_red.png")[0];
+ auto audio = content_factory("test/data/white.wav")[0];
+ auto film = new_test_film("reel_reuse_video_test", { video, audio });
+ make_and_verify_dcp (film);
+
+ /* Find main picture and sound asset IDs */
+ dcp::DCP dcp1 (film->dir(film->dcp_name()));
+ dcp1.read ();
+ BOOST_REQUIRE_EQUAL (dcp1.cpls().size(), 1U);
+ BOOST_REQUIRE_EQUAL (dcp1.cpls()[0]->reels().size(), 1U);
+ BOOST_REQUIRE (dcp1.cpls()[0]->reels()[0]->main_picture());
+ BOOST_REQUIRE (dcp1.cpls()[0]->reels()[0]->main_sound());
+ auto const picture_id = dcp1.cpls()[0]->reels()[0]->main_picture()->asset()->id();
+ auto const sound_id = dcp1.cpls()[0]->reels()[0]->main_sound()->asset()->id();
+
+ /* Change the audio and re-make */
+ audio->audio->set_gain (-3);
+ /* >1 CPLs in the DCP raises an error in ClairMeta */
+ make_and_verify_dcp(film, {}, true, false);
+
+ /* Video ID should be the same, sound different */
+ dcp::DCP dcp2 (film->dir(film->dcp_name()));
+ dcp2.read ();
+ BOOST_REQUIRE_EQUAL (dcp2.cpls().size(), 1U);
+ BOOST_REQUIRE_EQUAL (dcp2.cpls()[0]->reels().size(), 1U);
+ BOOST_REQUIRE (dcp2.cpls()[0]->reels()[0]->main_picture());
+ BOOST_REQUIRE (dcp2.cpls()[0]->reels()[0]->main_sound());
+ BOOST_CHECK_EQUAL (picture_id, dcp2.cpls()[0]->reels()[0]->main_picture()->asset()->id());
+ BOOST_CHECK (sound_id != dcp2.cpls()[0]->reels().front()->main_sound()->asset()->id());
+
+ /* Crop video and re-make */
+ video->video->set_left_crop (5);
+ /* >1 CPLs in the DCP raises an error in ClairMeta */
+ make_and_verify_dcp(film, {}, true, false);
+
+ /* Video and sound IDs should be different */
+ dcp::DCP dcp3 (film->dir(film->dcp_name()));
+ dcp3.read ();
+ BOOST_REQUIRE_EQUAL (dcp3.cpls().size(), 1U);
+ BOOST_REQUIRE_EQUAL (dcp3.cpls()[0]->reels().size(), 1U);
+ BOOST_REQUIRE (dcp3.cpls()[0]->reels()[0]->main_picture());
+ BOOST_REQUIRE (dcp3.cpls()[0]->reels()[0]->main_sound());
+ BOOST_CHECK (picture_id != dcp3.cpls()[0]->reels()[0]->main_picture()->asset()->id());
+ BOOST_CHECK (sound_id != dcp3.cpls()[0]->reels().front()->main_sound()->asset()->id());
+}
diff --git a/test/lib/reels_test.cc b/test/lib/reels_test.cc
new file mode 100644
index 000000000..bb2215244
--- /dev/null
+++ b/test/lib/reels_test.cc
@@ -0,0 +1,680 @@
+/*
+ Copyright (C) 2015-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/>.
+
+*/
+
+
+/** @file test/reels_test.cc
+ * @brief Check manipulation of reels in various ways.
+ * @ingroup feature
+ */
+
+
+#include "lib/content_factory.h"
+#include "lib/dcp_content.h"
+#include "lib/dcp_content_type.h"
+#include "lib/ffmpeg_content.h"
+#include "lib/film.h"
+#include "lib/image_content.h"
+#include "lib/make_dcp.h"
+#include "lib/ratio.h"
+#include "lib/string_text_file_content.h"
+#include "lib/variant.h"
+#include "lib/video_content.h"
+#include "../test.h"
+#include <dcp/cpl.h>
+#include <dcp/reel.h>
+#include <dcp/reel_atmos_asset.h>
+#include <dcp/reel_picture_asset.h>
+#include <dcp/reel_sound_asset.h>
+#include <boost/test/unit_test.hpp>
+#include <iostream>
+
+
+using std::cout;
+using std::function;
+using std::list;
+using std::make_shared;
+using std::shared_ptr;
+using std::string;
+using std::vector;
+using namespace dcpomatic;
+
+
+static
+void
+filter_ok(std::vector<dcp::VerificationNote>& notes)
+{
+ notes.erase(std::remove_if(notes.begin(), notes.end(), [](dcp::VerificationNote const& note) { return note.type() == dcp::VerificationNote::Type::OK; }), notes.end());
+}
+
+
+/** Test Film::reels() */
+BOOST_AUTO_TEST_CASE (reels_test1)
+{
+ auto A = make_shared<FFmpegContent>("test/data/test.mp4");
+ auto B = make_shared<FFmpegContent>("test/data/test.mp4");
+ auto film = new_test_film("reels_test1", { A, B });
+ BOOST_CHECK_EQUAL (A->full_length(film).get(), 288000);
+
+ film->set_reel_type (ReelType::SINGLE);
+ auto r = film->reels ();
+ BOOST_CHECK_EQUAL (r.size(), 1U);
+ BOOST_CHECK_EQUAL (r.front().from.get(), 0);
+ BOOST_CHECK_EQUAL (r.front().to.get(), 288000 * 2);
+
+ film->set_reel_type (ReelType::BY_VIDEO_CONTENT);
+ r = film->reels ();
+ BOOST_CHECK_EQUAL (r.size(), 2U);
+ BOOST_CHECK_EQUAL (r.front().from.get(), 0);
+ BOOST_CHECK_EQUAL (r.front().to.get(), 288000);
+ BOOST_CHECK_EQUAL (r.back().from.get(), 288000);
+ BOOST_CHECK_EQUAL (r.back().to.get(), 288000 * 2);
+
+ film->set_video_bit_rate(VideoEncoding::JPEG2000, 100000000);
+ film->set_reel_type (ReelType::BY_LENGTH);
+ /* This is just over 2.5s at 100Mbit/s; should correspond to 60 frames */
+ film->set_reel_length (31253154);
+ r = film->reels ();
+ BOOST_CHECK_EQUAL (r.size(), 3U);
+ auto i = r.begin ();
+ BOOST_CHECK_EQUAL (i->from.get(), 0);
+ BOOST_CHECK_EQUAL (i->to.get(), DCPTime::from_frames(60, 24).get());
+ ++i;
+ BOOST_CHECK_EQUAL (i->from.get(), DCPTime::from_frames(60, 24).get());
+ BOOST_CHECK_EQUAL (i->to.get(), DCPTime::from_frames(120, 24).get());
+ ++i;
+ BOOST_CHECK_EQUAL (i->from.get(), DCPTime::from_frames(120, 24).get());
+ BOOST_CHECK_EQUAL (i->to.get(), DCPTime::from_frames(144, 24).get());
+}
+
+
+/** Make a short DCP with multi reels split by video content, then import
+ * this into a new project and make a new DCP referencing it.
+ */
+BOOST_AUTO_TEST_CASE (reels_test2)
+{
+ auto r = make_shared<ImageContent>("test/data/flat_red.png");
+ auto g = make_shared<ImageContent>("test/data/flat_green.png");
+ auto b = make_shared<ImageContent>("test/data/flat_blue.png");
+ auto film = new_test_film("reels_test2", { r, g, b });
+ r->video->set_length(24);
+ g->video->set_length(24);
+ b->video->set_length(24);
+
+ film->set_reel_type (ReelType::BY_VIDEO_CONTENT);
+ BOOST_CHECK_EQUAL (film->reels().size(), 3U);
+ BOOST_REQUIRE (!wait_for_jobs());
+
+ film->set_audio_channels(16);
+
+ make_and_verify_dcp (film);
+
+ check_dcp ("test/data/reels_test2", film->dir (film->dcp_name()));
+
+ auto c = make_shared<DCPContent>(film->dir(film->dcp_name()));
+ auto film2 = new_test_film("reels_test2b", {c});
+ film2->set_reel_type (ReelType::BY_VIDEO_CONTENT);
+ film2->set_audio_channels(16);
+
+ auto reels = film2->reels ();
+ BOOST_CHECK_EQUAL(reels.size(), 3U);
+ auto i = reels.begin();
+ BOOST_CHECK_EQUAL (i->from.get(), 0);
+ BOOST_CHECK_EQUAL (i->to.get(), 96000);
+ ++i;
+ BOOST_CHECK_EQUAL (i->from.get(), 96000);
+ BOOST_CHECK_EQUAL (i->to.get(), 96000 * 2);
+ ++i;
+ BOOST_CHECK_EQUAL (i->from.get(), 96000 * 2);
+ BOOST_CHECK_EQUAL (i->to.get(), 96000 * 3);
+
+ c->set_reference_video (true);
+ c->set_reference_audio (true);
+
+ make_and_verify_dcp(film2, {dcp::VerificationNote::Code::EXTERNAL_ASSET}, false);
+}
+
+
+/** Check that ReelType::BY_VIDEO_CONTENT adds an extra reel, if necessary, at the end
+ * of all the video content to mop up anything afterward.
+ */
+BOOST_AUTO_TEST_CASE (reels_test3)
+{
+ auto dcp = make_shared<DCPContent>("test/data/reels_test2");
+ auto sub = make_shared<StringTextFileContent>("test/data/subrip.srt");
+ auto film = new_test_film("reels_test3", {dcp, sub});
+ film->set_reel_type (ReelType::BY_VIDEO_CONTENT);
+
+ auto reels = film->reels();
+ BOOST_REQUIRE_EQUAL (reels.size(), 4U);
+ auto i = reels.begin ();
+ BOOST_CHECK_EQUAL (i->from.get(), 0);
+ BOOST_CHECK_EQUAL (i->to.get(), 96000);
+ ++i;
+ BOOST_CHECK_EQUAL (i->from.get(), 96000);
+ BOOST_CHECK_EQUAL (i->to.get(), 96000 * 2);
+ ++i;
+ BOOST_CHECK_EQUAL (i->from.get(), 96000 * 2);
+ BOOST_CHECK_EQUAL (i->to.get(), 96000 * 3);
+ ++i;
+ BOOST_CHECK_EQUAL (i->from.get(), 96000 * 3);
+ BOOST_CHECK_EQUAL (i->to.get(), sub->full_length(film).ceil(film->video_frame_rate()).get());
+}
+
+
+/** Check creation of a multi-reel DCP with a single .srt subtitle file;
+ * make sure that the reel subtitle timing is done right.
+ */
+BOOST_AUTO_TEST_CASE (reels_test4)
+{
+ auto film = new_test_film("reels_test4");
+ film->set_reel_type (ReelType::BY_VIDEO_CONTENT);
+ film->set_interop (false);
+
+ /* 4 piece of 1s-long content */
+ shared_ptr<ImageContent> content[4];
+ for (int i = 0; i < 4; ++i) {
+ content[i] = make_shared<ImageContent>("test/data/flat_green.png");
+ film->examine_and_add_content (content[i]);
+ BOOST_REQUIRE (!wait_for_jobs());
+ content[i]->video->set_length (24);
+ }
+
+ auto subs = make_shared<StringTextFileContent>("test/data/subrip3.srt");
+ film->examine_and_add_content (subs);
+ BOOST_REQUIRE (!wait_for_jobs());
+
+ film->set_audio_channels(16);
+
+ auto reels = film->reels();
+ BOOST_REQUIRE_EQUAL (reels.size(), 4U);
+ auto i = reels.begin ();
+ BOOST_CHECK_EQUAL (i->from.get(), 0);
+ BOOST_CHECK_EQUAL (i->to.get(), 96000);
+ ++i;
+ BOOST_CHECK_EQUAL (i->from.get(), 96000);
+ BOOST_CHECK_EQUAL (i->to.get(), 96000 * 2);
+ ++i;
+ BOOST_CHECK_EQUAL (i->from.get(), 96000 * 2);
+ BOOST_CHECK_EQUAL (i->to.get(), 96000 * 3);
+ ++i;
+ BOOST_CHECK_EQUAL (i->from.get(), 96000 * 3);
+ BOOST_CHECK_EQUAL (i->to.get(), 96000 * 4);
+
+ make_and_verify_dcp (
+ film,
+ {
+ dcp::VerificationNote::Code::MISSING_SUBTITLE_LANGUAGE,
+ dcp::VerificationNote::Code::INVALID_SUBTITLE_FIRST_TEXT_TIME,
+ dcp::VerificationNote::Code::INVALID_SUBTITLE_DURATION_BV21
+ });
+
+ check_dcp ("test/data/reels_test4", film->dir (film->dcp_name()));
+}
+
+
+BOOST_AUTO_TEST_CASE (reels_test5)
+{
+ auto dcp = make_shared<DCPContent>("test/data/reels_test4");
+ dcp->check_font_ids();
+ auto film = new_test_film("reels_test5", {dcp});
+ film->set_sequence (false);
+
+ /* Set to 2123 but it will be rounded up to the next frame (4000) */
+ dcp->set_position(film, DCPTime(2123));
+
+ {
+ auto p = dcp->reels (film);
+ BOOST_REQUIRE_EQUAL (p.size(), 4U);
+ auto i = p.begin();
+ BOOST_CHECK (*i++ == DCPTimePeriod (DCPTime(4000 + 0), DCPTime(4000 + 96000)));
+ BOOST_CHECK (*i++ == DCPTimePeriod (DCPTime(4000 + 96000), DCPTime(4000 + 192000)));
+ BOOST_CHECK (*i++ == DCPTimePeriod (DCPTime(4000 + 192000), DCPTime(4000 + 288000)));
+ BOOST_CHECK (*i++ == DCPTimePeriod (DCPTime(4000 + 288000), DCPTime(4000 + 384000)));
+ }
+
+ {
+ dcp->set_trim_start(film, ContentTime::from_seconds(0.5));
+ auto p = dcp->reels (film);
+ BOOST_REQUIRE_EQUAL (p.size(), 4U);
+ auto i = p.begin();
+ BOOST_CHECK (*i++ == DCPTimePeriod (DCPTime(4000 + 0), DCPTime(4000 + 48000)));
+ BOOST_CHECK (*i++ == DCPTimePeriod (DCPTime(4000 + 48000), DCPTime(4000 + 144000)));
+ BOOST_CHECK (*i++ == DCPTimePeriod (DCPTime(4000 + 144000), DCPTime(4000 + 240000)));
+ BOOST_CHECK (*i++ == DCPTimePeriod (DCPTime(4000 + 240000), DCPTime(4000 + 336000)));
+ }
+
+ {
+ dcp->set_trim_end (ContentTime::from_seconds (0.5));
+ auto p = dcp->reels (film);
+ BOOST_REQUIRE_EQUAL (p.size(), 4U);
+ auto i = p.begin();
+ BOOST_CHECK (*i++ == DCPTimePeriod (DCPTime(4000 + 0), DCPTime(4000 + 48000)));
+ BOOST_CHECK (*i++ == DCPTimePeriod (DCPTime(4000 + 48000), DCPTime(4000 + 144000)));
+ BOOST_CHECK (*i++ == DCPTimePeriod (DCPTime(4000 + 144000), DCPTime(4000 + 240000)));
+ BOOST_CHECK (*i++ == DCPTimePeriod (DCPTime(4000 + 240000), DCPTime(4000 + 288000)));
+ }
+
+ {
+ dcp->set_trim_start(film, ContentTime::from_seconds(1.5));
+ auto p = dcp->reels (film);
+ BOOST_REQUIRE_EQUAL (p.size(), 3U);
+ auto i = p.begin();
+ BOOST_CHECK (*i++ == DCPTimePeriod (DCPTime(4000 + 0), DCPTime(4000 + 48000)));
+ BOOST_CHECK (*i++ == DCPTimePeriod (DCPTime(4000 + 48000), DCPTime(4000 + 144000)));
+ BOOST_CHECK (*i++ == DCPTimePeriod (DCPTime(4000 + 144000), DCPTime(4000 + 192000)));
+ }
+}
+
+
+/** Check reel split with a muxed video/audio source */
+BOOST_AUTO_TEST_CASE (reels_test6)
+{
+ auto A = make_shared<FFmpegContent>("test/data/test2.mp4");
+ auto film = new_test_film("reels_test6", {A});
+
+ film->set_video_bit_rate(VideoEncoding::JPEG2000, 100000000);
+ film->set_reel_type (ReelType::BY_LENGTH);
+ /* This is just over 2.5s at 100Mbit/s; should correspond to 60 frames */
+ film->set_reel_length (31253154);
+ /* dcp_inspect and clairmeta both give errors about reel <1s in length */
+ make_and_verify_dcp (
+ film,
+ {
+ dcp::VerificationNote::Code::INVALID_INTRINSIC_DURATION,
+ dcp::VerificationNote::Code::INVALID_DURATION,
+ },
+ false,
+ false
+ );
+}
+
+
+/** Check the case where the last bit of audio hangs over the end of the video
+ * and we are using ReelType::BY_VIDEO_CONTENT.
+ */
+BOOST_AUTO_TEST_CASE (reels_test7)
+{
+ auto A = content_factory("test/data/flat_red.png")[0];
+ auto B = content_factory("test/data/awkward_length.wav")[0];
+ auto film = new_test_film("reels_test7", { A, B });
+ film->set_video_frame_rate (24);
+ A->video->set_length (2 * 24);
+
+ film->set_reel_type (ReelType::BY_VIDEO_CONTENT);
+ BOOST_REQUIRE_EQUAL (film->reels().size(), 2U);
+ BOOST_CHECK (film->reels().front() == DCPTimePeriod(DCPTime(0), DCPTime::from_frames(2 * 24, 24)));
+ BOOST_CHECK (film->reels().back() == DCPTimePeriod(DCPTime::from_frames(2 * 24, 24), DCPTime::from_frames(3 * 24 + 1, 24)));
+
+ make_and_verify_dcp (film);
+}
+
+
+/** Check a reels-related error; make_dcp() would raise a ProgrammingError */
+BOOST_AUTO_TEST_CASE (reels_test8)
+{
+ auto A = make_shared<FFmpegContent>("test/data/test2.mp4");
+ auto film = new_test_film("reels_test8", {A});
+
+ A->set_trim_end (ContentTime::from_seconds (1));
+ make_and_verify_dcp (film);
+}
+
+
+/** Check another reels-related error; make_dcp() would raise a ProgrammingError */
+BOOST_AUTO_TEST_CASE (reels_test9)
+{
+ auto A = make_shared<FFmpegContent>("test/data/flat_red.png");
+ auto film = new_test_film("reels_test9a", {A});
+ A->video->set_length(5 * 24);
+ film->set_video_frame_rate(24);
+ make_and_verify_dcp (film);
+
+ auto B = make_shared<DCPContent>(film->dir(film->dcp_name()));
+ auto film2 = new_test_film("reels_test9b", {B, content_factory("test/data/dcp_sub4.xml")[0]});
+ B->set_reference_video(true);
+ B->set_reference_audio(true);
+ film2->set_reel_type(ReelType::BY_VIDEO_CONTENT);
+ film2->write_metadata();
+ make_and_verify_dcp (
+ film2,
+ {
+ dcp::VerificationNote::Code::MISSING_SUBTITLE_LANGUAGE,
+ dcp::VerificationNote::Code::INVALID_SUBTITLE_FIRST_TEXT_TIME
+ });
+}
+
+
+/** Another reels-related error; make_dcp() would raise a ProgrammingError
+ * in AudioBuffers::allocate due to an attempt to allocate a negatively-sized buffer.
+ * This was triggered by a VF where there are referenced audio reels followed by
+ * VF audio. When the VF audio arrives the Writer did not correctly skip over the
+ * referenced reels.
+ */
+BOOST_AUTO_TEST_CASE (reels_test10)
+{
+ /* Make the OV */
+ auto A = make_shared<FFmpegContent>("test/data/flat_red.png");
+ auto B = make_shared<FFmpegContent>("test/data/flat_red.png");
+ auto ov = new_test_film("reels_test10_ov", {A, B});
+ A->video->set_length (5 * 24);
+ B->video->set_length (5 * 24);
+
+ ov->set_reel_type (ReelType::BY_VIDEO_CONTENT);
+ make_and_verify_dcp (ov);
+ ov->write_metadata ();
+
+ /* Now try to make the VF; this used to fail */
+ auto ov_dcp = make_shared<DCPContent>(ov->dir(ov->dcp_name()));
+ auto vf = new_test_film("reels_test10_vf", {ov_dcp, content_factory("test/data/15s.srt")[0]});
+ vf->set_reel_type (ReelType::BY_VIDEO_CONTENT);
+ ov_dcp->set_reference_video (true);
+ ov_dcp->set_reference_audio (true);
+
+ make_and_verify_dcp (
+ vf,
+ {
+ dcp::VerificationNote::Code::EXTERNAL_ASSET,
+ dcp::VerificationNote::Code::MISSING_SUBTITLE_LANGUAGE,
+ dcp::VerificationNote::Code::INVALID_SUBTITLE_FIRST_TEXT_TIME,
+ dcp::VerificationNote::Code::INVALID_SUBTITLE_DURATION_BV21,
+ },
+ false);
+}
+
+
+/** Another reels error; ReelType::BY_VIDEO_CONTENT when the first content is not
+ * at time 0.
+ */
+BOOST_AUTO_TEST_CASE (reels_test11)
+{
+ auto A = make_shared<FFmpegContent>("test/data/flat_red.png");
+ auto film = new_test_film("reels_test11", {A});
+ film->set_video_frame_rate (24);
+ A->video->set_length (240);
+ A->set_video_frame_rate(film, 24);
+ A->set_position (film, DCPTime::from_seconds(1));
+ film->set_reel_type (ReelType::BY_VIDEO_CONTENT);
+ make_and_verify_dcp (film);
+ BOOST_CHECK_EQUAL (A->position().get(), DCPTime::from_seconds(1).get());
+ BOOST_CHECK_EQUAL (A->end(film).get(), DCPTime::from_seconds(1 + 10).get());
+
+ auto r = film->reels ();
+ BOOST_CHECK_EQUAL (r.size(), 2U);
+ BOOST_CHECK_EQUAL (r.front().from.get(), 0);
+ BOOST_CHECK_EQUAL (r.front().to.get(), DCPTime::from_seconds(1).get());
+ BOOST_CHECK_EQUAL (r.back().from.get(), DCPTime::from_seconds(1).get());
+ BOOST_CHECK_EQUAL (r.back().to.get(), DCPTime::from_seconds(1 + 10).get());
+}
+
+
+/** For VFs to work right we have to make separate reels for empty bits between
+ * video content.
+ */
+BOOST_AUTO_TEST_CASE (reels_test12)
+{
+ auto A = make_shared<FFmpegContent>("test/data/flat_red.png");
+ auto B = make_shared<FFmpegContent>("test/data/flat_red.png");
+ auto film = new_test_film("reels_test12", {A, B});
+ film->set_video_frame_rate (24);
+ film->set_reel_type (ReelType::BY_VIDEO_CONTENT);
+ film->set_sequence (false);
+
+ A->video->set_length (240);
+ A->set_video_frame_rate(film, 24);
+ A->set_position (film, DCPTime::from_seconds(1));
+
+ B->video->set_length (120);
+ B->set_video_frame_rate(film, 24);
+ B->set_position (film, DCPTime::from_seconds(14));
+
+ auto r = film->reels ();
+ BOOST_REQUIRE_EQUAL (r.size(), 4U);
+ auto i = r.begin ();
+
+ BOOST_CHECK_EQUAL (i->from.get(), 0);
+ BOOST_CHECK_EQUAL (i->to.get(), DCPTime::from_seconds(1).get());
+ ++i;
+ BOOST_CHECK_EQUAL (i->from.get(), DCPTime::from_seconds(1).get());
+ BOOST_CHECK_EQUAL (i->to.get(), DCPTime::from_seconds(11).get());
+ ++i;
+ BOOST_CHECK_EQUAL (i->from.get(), DCPTime::from_seconds(11).get());
+ BOOST_CHECK_EQUAL (i->to.get(), DCPTime::from_seconds(14).get());
+ ++i;
+ BOOST_CHECK_EQUAL (i->from.get(), DCPTime::from_seconds(14).get());
+ BOOST_CHECK_EQUAL (i->to.get(), DCPTime::from_seconds(19).get());
+}
+
+
+static void
+no_op ()
+{
+
+}
+
+static void
+dump_notes (vector<dcp::VerificationNote> const & notes)
+{
+ for (auto i: notes) {
+ std::cout << dcp::note_to_string(i) << "\n";
+ }
+}
+
+
+/** Using less than 1 second's worth of content should not result in a reel
+ * of less than 1 second's duration.
+ */
+BOOST_AUTO_TEST_CASE (reels_should_not_be_short1)
+{
+ auto A = make_shared<FFmpegContent>("test/data/flat_red.png");
+ auto B = make_shared<FFmpegContent>("test/data/flat_red.png");
+ auto film = new_test_film("reels_should_not_be_short1", {A, B});
+ film->set_video_frame_rate (24);
+
+ A->video->set_length (23);
+
+ B->video->set_length (23);
+ B->set_position (film, DCPTime::from_frames(23, 24));
+
+ make_and_verify_dcp (film);
+
+ vector<boost::filesystem::path> dirs = { film->dir(film->dcp_name(false)) };
+ auto result = dcp::verify(dirs, {}, boost::bind(&no_op), boost::bind(&no_op), {}, TestPaths::xsd());
+ filter_ok(result.notes);
+ dump_notes(result.notes);
+ BOOST_REQUIRE(result.notes.empty());
+}
+
+
+/** Leaving less than 1 second's gap between two pieces of content with
+ * ReelType::BY_VIDEO_CONTENT should not make a <1s reel.
+ */
+BOOST_AUTO_TEST_CASE (reels_should_not_be_short2)
+{
+ auto A = make_shared<FFmpegContent>("test/data/flat_red.png");
+ auto B = make_shared<FFmpegContent>("test/data/flat_red.png");
+ auto film = new_test_film("reels_should_not_be_short2", {A, B});
+ film->set_video_frame_rate (24);
+ film->set_reel_type (ReelType::BY_VIDEO_CONTENT);
+
+ A->video->set_length (240);
+
+ B->video->set_length (240);
+ B->set_position (film, DCPTime::from_seconds(10.2));
+
+ make_and_verify_dcp (film);
+
+ vector<boost::filesystem::path> dirs = { film->dir(film->dcp_name(false)) };
+ auto result = dcp::verify(dirs, {}, boost::bind(&no_op), boost::bind(&no_op), {}, TestPaths::xsd());
+ filter_ok(result.notes);
+ dump_notes(result.notes);
+ BOOST_REQUIRE(result.notes.empty());
+}
+
+
+/** Setting ReelType::BY_LENGTH and using a small length value should not make
+ * <1s reels.
+ */
+BOOST_AUTO_TEST_CASE (reels_should_not_be_short3)
+{
+ auto A = make_shared<FFmpegContent>("test/data/flat_red.png");
+ auto film = new_test_film("reels_should_not_be_short3", {A});
+ film->set_video_frame_rate (24);
+ film->set_reel_type (ReelType::BY_LENGTH);
+ film->set_reel_length (1024 * 1024 * 10);
+
+ A->video->set_length (240);
+
+ make_and_verify_dcp (film);
+
+ auto result = dcp::verify({}, {}, boost::bind(&no_op), boost::bind(&no_op), {}, TestPaths::xsd());
+ filter_ok(result.notes);
+ dump_notes(result.notes);
+ BOOST_REQUIRE(result.notes.empty());
+}
+
+
+/** Having one piece of content less than 1s long in ReelType::BY_VIDEO_CONTENT
+ * should not make a reel less than 1s long.
+ */
+BOOST_AUTO_TEST_CASE (reels_should_not_be_short4)
+{
+ auto A = make_shared<FFmpegContent>("test/data/flat_red.png");
+ auto B = make_shared<FFmpegContent>("test/data/flat_red.png");
+ auto film = new_test_film("reels_should_not_be_short4", {A, B});
+ film->set_video_frame_rate (24);
+ film->set_reel_type (ReelType::BY_VIDEO_CONTENT);
+
+ A->video->set_length (240);
+
+ B->video->set_length (23);
+ B->set_position (film, DCPTime::from_frames(240, 24));
+
+ BOOST_CHECK_EQUAL (film->reels().size(), 1U);
+ BOOST_CHECK (film->reels().front() == dcpomatic::DCPTimePeriod(dcpomatic::DCPTime(), dcpomatic::DCPTime::from_frames(263, 24)));
+
+ film->write_metadata ();
+ make_dcp (film, TranscodeJob::ChangedBehaviour::IGNORE);
+ BOOST_REQUIRE (!wait_for_jobs());
+
+ vector<boost::filesystem::path> dirs = { film->dir(film->dcp_name(false)) };
+ auto result = dcp::verify(dirs, {}, boost::bind(&no_op), boost::bind(&no_op), {}, TestPaths::xsd());
+ filter_ok(result.notes);
+ dump_notes(result.notes);
+ BOOST_REQUIRE(result.notes.empty());
+}
+
+
+/** Create a long DCP A then insert it repeatedly into a new project, trimming it differently each time.
+ * Make a DCP B from that project which refers to A and splits into reels. This was found to go wrong
+ * when looking at #2268.
+ */
+BOOST_AUTO_TEST_CASE (repeated_dcp_into_reels)
+{
+ /* Make a 20s DCP */
+ auto A = make_shared<FFmpegContent>("test/data/flat_red.png");
+ auto film1 = new_test_film("repeated_dcp_into_reels1", { A });
+ auto constexpr frame_rate = 24;
+ auto constexpr length_in_seconds = 20;
+ auto constexpr total_frames = frame_rate * length_in_seconds;
+ film1->set_video_frame_rate(frame_rate);
+ A->video->set_length(total_frames);
+ make_and_verify_dcp(film1);
+
+ /* Make a new project that includes this long DCP 4 times, each
+ * trimmed to a quarter of the original, i.e.
+ * /----------------------|----------------------|----------------------|----------------------\
+ * | 1st quarter of film1 | 2nd quarter of film1 | 3rd quarter of film1 | 4th quarter of film1 |
+ * \----------------------|----------------------|----------------------|_---------------------/
+ */
+
+ shared_ptr<DCPContent> original_dcp[4] = {
+ make_shared<DCPContent>(film1->dir(film1->dcp_name(false))),
+ make_shared<DCPContent>(film1->dir(film1->dcp_name(false))),
+ make_shared<DCPContent>(film1->dir(film1->dcp_name(false))),
+ make_shared<DCPContent>(film1->dir(film1->dcp_name(false)))
+ };
+
+ auto film2 = new_test_film("repeated_dcp_into_reels2", { original_dcp[0], original_dcp[1], original_dcp[2], original_dcp[3] });
+ film2->set_reel_type(ReelType::BY_VIDEO_CONTENT);
+ film2->set_video_frame_rate(frame_rate);
+ film2->set_sequence(false);
+
+ for (int i = 0; i < 4; ++i) {
+ original_dcp[i]->set_position(film2, DCPTime::from_frames(total_frames * i / 4, frame_rate));
+ original_dcp[i]->set_trim_start(film2, ContentTime::from_frames(total_frames * i / 4, frame_rate));
+ original_dcp[i]->set_trim_end (ContentTime::from_frames(total_frames * (4 - i - 1) / 4, frame_rate));
+ original_dcp[i]->set_reference_video(true);
+ original_dcp[i]->set_reference_audio(true);
+ }
+
+ make_and_verify_dcp(film2, { dcp::VerificationNote::Code::EXTERNAL_ASSET }, false);
+
+ dcp::DCP check1(film1->dir(film1->dcp_name()));
+ check1.read();
+ BOOST_REQUIRE(!check1.cpls().empty());
+ BOOST_REQUIRE(!check1.cpls()[0]->reels().empty());
+ auto picture = check1.cpls()[0]->reels()[0]->main_picture()->asset();
+ BOOST_REQUIRE(picture);
+ auto sound = check1.cpls()[0]->reels()[0]->main_sound()->asset();
+ BOOST_REQUIRE(sound);
+
+ dcp::DCP check2(film2->dir(film2->dcp_name()));
+ check2.read();
+ BOOST_REQUIRE(!check2.cpls().empty());
+ auto cpl = check2.cpls()[0];
+ BOOST_REQUIRE_EQUAL(cpl->reels().size(), 4U);
+ for (int i = 0; i < 4; ++i) {
+ BOOST_REQUIRE_EQUAL(cpl->reels()[i]->main_picture()->entry_point().get_value_or(0), total_frames * i / 4);
+ BOOST_REQUIRE_EQUAL(cpl->reels()[i]->main_picture()->duration().get_value_or(0), total_frames / 4);
+ BOOST_REQUIRE_EQUAL(cpl->reels()[i]->main_picture()->id(), picture->id());
+ BOOST_REQUIRE_EQUAL(cpl->reels()[i]->main_sound()->entry_point().get_value_or(0), total_frames * i / 4);
+ BOOST_REQUIRE_EQUAL(cpl->reels()[i]->main_sound()->duration().get_value_or(0), total_frames / 4);
+ BOOST_REQUIRE_EQUAL(cpl->reels()[i]->main_sound()->id(), sound->id());
+ }
+}
+
+
+/** Check that reel lengths are adapted to cope with Atmos content, as I don't know how to fill gaps with Atmos silence */
+BOOST_AUTO_TEST_CASE(reel_assets_same_length_with_atmos)
+{
+ auto atmos = content_factory("test/data/atmos_0.mxf")[0];
+ auto film = new_test_film("reel_assets_same_length_with_atmos", { atmos });
+
+ vector<string> messages;
+ film->Message.connect([&messages](string message) {
+ messages.push_back(message);
+ });
+
+ auto picture = content_factory("test/data/flat_red.png")[0];
+ film->examine_and_add_content(picture);
+ wait_for_jobs();
+ picture->video->set_length(480);
+
+ BOOST_REQUIRE_EQUAL(messages.size(), 1U);
+ BOOST_CHECK_EQUAL(messages[0], variant::insert_dcpomatic("%1 had to change your reel settings to accommodate the Atmos content"));
+
+ auto const reels = film->reels();
+ BOOST_CHECK_EQUAL(reels.size(), 2U);
+ BOOST_CHECK(reels[0] == dcpomatic::DCPTimePeriod({}, dcpomatic::DCPTime::from_frames(240, 24)));
+ BOOST_CHECK(reels[1] == dcpomatic::DCPTimePeriod(dcpomatic::DCPTime::from_frames(240, 24), dcpomatic::DCPTime::from_frames(480, 24)));
+
+ make_and_verify_dcp(film, { dcp::VerificationNote::Code::MISSING_CPL_METADATA });
+}
+
diff --git a/test/lib/relative_paths_test.cc b/test/lib/relative_paths_test.cc
new file mode 100644
index 000000000..9aaf965a8
--- /dev/null
+++ b/test/lib/relative_paths_test.cc
@@ -0,0 +1,44 @@
+/*
+ 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 "lib/content.h"
+#include "lib/content_factory.h"
+#include "lib/config.h"
+#include "lib/film.h"
+#include "../test.h"
+#include <boost/test/unit_test.hpp>
+
+
+BOOST_AUTO_TEST_CASE(relative_paths_test)
+{
+ ConfigRestorer cr;
+ Config::instance()->set_relative_paths(true);
+
+ auto picture = content_factory("test/data/flat_red.png")[0];
+ auto film = new_test_film("relative_paths_test", { picture });
+ film->write_metadata();
+
+ auto film2 = std::make_shared<Film>(boost::filesystem::path("build/test/relative_paths_test"));
+ film2->read_metadata();
+ BOOST_REQUIRE_EQUAL(film2->content().size(), 1U);
+ BOOST_REQUIRE(paths_exist(film2->content()[0]->paths()));
+}
+
diff --git a/test/lib/release_notes_test.cc b/test/lib/release_notes_test.cc
new file mode 100644
index 000000000..addcc9f85
--- /dev/null
+++ b/test/lib/release_notes_test.cc
@@ -0,0 +1,38 @@
+/*
+ Copyright (C) 2022 Carl Hetherington <cth@carlh.net>
+
+ This file is part of DCP-o-matic.
+
+ DCP-o-matic is free software; you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation; either version 2 of the License, or
+ (at your option) any later version.
+
+ DCP-o-matic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with DCP-o-matic. If not, see <http://www.gnu.org/licenses/>.
+
+*/
+
+
+#include "lib/config.h"
+#include "lib/release_notes.h"
+#include <boost/test/unit_test.hpp>
+
+
+using std::string;
+
+
+// Once we're running 2.17.19 we have no more release notes (for now, at least)
+BOOST_AUTO_TEST_CASE(release_notes_test2)
+{
+ for (auto version: { "2.17.19", "2.17.20", "2.18.0", "2.18.1devel6" }) {
+ Config::instance()->set_last_release_notes_version("2.17.19");
+ auto notes = find_release_notes(false, string(version));
+ BOOST_CHECK(!static_cast<bool>(notes));
+ }
+}
diff --git a/test/lib/remake_id_test.cc b/test/lib/remake_id_test.cc
new file mode 100644
index 000000000..a3bc04eea
--- /dev/null
+++ b/test/lib/remake_id_test.cc
@@ -0,0 +1,101 @@
+/*
+ Copyright (C) 2017-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 "lib/config.h"
+#include "lib/content_factory.h"
+#include "lib/dcp_content.h"
+#include "lib/examine_content_job.h"
+#include "lib/ffmpeg_content.h"
+#include "lib/film.h"
+#include "lib/job_manager.h"
+#include "lib/text_content.h"
+#include "../test.h"
+#include <boost/test/unit_test.hpp>
+#include <iostream>
+
+
+using std::dynamic_pointer_cast;
+using std::make_shared;
+using std::string;
+using std::vector;
+using boost::optional;
+
+
+/** Check for bug #1126 whereby making a new DCP using the same video asset as an old one
+ * corrupts the old one.
+ */
+BOOST_AUTO_TEST_CASE (remake_id_test1)
+{
+ /* Make a DCP */
+ auto content = content_factory("test/data/flat_red.png");
+ auto film = new_test_film("remake_id_test1_1", content);
+ make_and_verify_dcp (film);
+
+ /* Copy the video file */
+ auto first_video = dcp_file(film, "j2c");
+ boost::filesystem::copy_file (first_video, first_video.string() + ".copy");
+
+ /* Make a new DCP with the same video file */
+ film->set_name ("remake_id_test1_2");
+ make_and_verify_dcp (film);
+
+ /* Check that the video in the first DCP hasn't changed */
+ check_file (first_video, first_video.string() + ".copy");
+}
+
+
+/** Check for bug #1232 where remaking an encrypted DCP causes problems with HMAC IDs (?) */
+BOOST_AUTO_TEST_CASE (remake_id_test2)
+{
+ /* Make a DCP */
+ auto content = content_factory("test/data/flat_red.png");
+ auto film = new_test_film("remake_id_test2_1", content);
+ film->set_encrypted (true);
+ make_and_verify_dcp (film);
+
+ /* Remove and remake it */
+ boost::filesystem::remove_all(film->dir(film->dcp_name()));
+ make_and_verify_dcp (film);
+
+ /* Find the CPL */
+ optional<boost::filesystem::path> cpl;
+ for (auto i: boost::filesystem::directory_iterator(film->dir(film->dcp_name()))) {
+ if (i.path().filename().string().substr(0, 4) == "cpl_") {
+ cpl = i.path();
+ }
+ }
+ BOOST_REQUIRE(cpl);
+
+ auto signer = Config::instance()->signer_chain();
+ BOOST_REQUIRE(signer->valid());
+
+ /* Make a DKDM */
+ auto const decrypted_kdm = film->make_kdm(*cpl, dcp::LocalTime ("2030-01-01T01:00:00+00:00"), dcp::LocalTime ("2031-01-01T01:00:00+00:00"));
+ auto const kdm = decrypted_kdm.encrypt(signer, Config::instance()->decryption_chain()->leaf(), {}, dcp::Formulation::MODIFIED_TRANSITIONAL_1, true, 0);
+
+ /* Import the DCP into a new film */
+ auto dcp_content = make_shared<DCPContent>(film->dir(film->dcp_name()));
+ auto film2 = new_test_film("remake_id_test2_2", { dcp_content });
+ dcp_content->add_kdm(kdm);
+ JobManager::instance()->add(make_shared<ExamineContentJob>(film2, dcp_content, false));
+ BOOST_REQUIRE(!wait_for_jobs());
+ make_and_verify_dcp (film2);
+}
diff --git a/test/lib/remake_video_test.cc b/test/lib/remake_video_test.cc
new file mode 100644
index 000000000..c91d0a310
--- /dev/null
+++ b/test/lib/remake_video_test.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 "lib/content_factory.h"
+#include "lib/film.h"
+#include "lib/video_content.h"
+#include "../test.h"
+#include <dcp/colour_conversion.h>
+#include <dcp/cpl.h>
+#include <dcp/dcp.h>
+#include <dcp/mono_j2k_picture_asset_reader.h>
+#include <dcp/reel.h>
+#include <dcp/reel_mono_picture_asset.h>
+#include <boost/test/unit_test.hpp>
+
+
+using std::dynamic_pointer_cast;
+using std::shared_ptr;
+using std::string;
+using std::vector;
+
+
+BOOST_AUTO_TEST_CASE(remake_video_after_yub_rgb_matrix_changed)
+{
+ auto content = content_factory("test/data/rgb_grey_testcard.mp4")[0];
+ auto film = new_test_film("remake_video_after_yub_rgb_matrix_changed", { content });
+
+ auto conversion = content->video->colour_conversion();
+ BOOST_REQUIRE(static_cast<bool>(conversion));
+ conversion->set_yuv_to_rgb(dcp::YUVToRGB::REC709);
+ content->video->set_colour_conversion(*conversion);
+
+ auto calculate_picture_hashes = [](shared_ptr<Film> film) {
+ /* >1 CPLs in the DCP raises an error in ClairMeta */
+ make_and_verify_dcp(film, {}, true, false);
+ dcp::DCP dcp(film->dir(film->dcp_name()));
+ dcp.read();
+ BOOST_REQUIRE(!dcp.cpls().empty());
+ auto cpl = dcp.cpls()[0];
+ BOOST_REQUIRE(!cpl->reels().empty());
+ auto reel = cpl->reels()[0];
+ BOOST_REQUIRE(reel->main_picture());
+ auto mono = dynamic_pointer_cast<dcp::MonoJ2KPictureAsset>(reel->main_picture()->asset());
+ BOOST_REQUIRE(mono);
+ auto reader = mono->start_read();
+
+ vector<string> hashes;
+ for (auto i = 0; i < reel->main_picture()->intrinsic_duration(); ++i) {
+ auto frame = reader->get_frame(i);
+ hashes.push_back(dcp::make_digest(dcp::ArrayData(frame->data(), frame->size())));
+ }
+ return hashes;
+ };
+
+ auto before = calculate_picture_hashes(film);
+ conversion->set_yuv_to_rgb(dcp::YUVToRGB::REC601);
+ content->video->set_colour_conversion(*conversion);
+ auto after = calculate_picture_hashes(film);
+
+ BOOST_CHECK(before != after);
+}
+
diff --git a/test/lib/remake_with_subtitle_test.cc b/test/lib/remake_with_subtitle_test.cc
new file mode 100644
index 000000000..237003b59
--- /dev/null
+++ b/test/lib/remake_with_subtitle_test.cc
@@ -0,0 +1,57 @@
+/*
+ Copyright (C) 2017-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 "lib/ffmpeg_content.h"
+#include "lib/content_factory.h"
+#include "lib/text_content.h"
+#include "lib/film.h"
+#include "../test.h"
+#include <boost/test/unit_test.hpp>
+
+
+using std::dynamic_pointer_cast;
+
+
+/** Check that if we remake a DCP having turned off subtitles the code notices
+ * and doesn't re-use the old video data.
+ */
+BOOST_AUTO_TEST_CASE (remake_with_subtitle_test)
+{
+ auto film = new_test_film("remake_with_subtitle_test");
+ film->set_video_bit_rate(VideoEncoding::JPEG2000, 100000000);
+ auto content = dynamic_pointer_cast<FFmpegContent>(content_factory(TestPaths::private_data() / "prophet_short_clip.mkv")[0]);
+ film->examine_and_add_content (content);
+ BOOST_REQUIRE (!wait_for_jobs ());
+ content->only_text()->set_burn (true);
+ content->only_text()->set_use (true);
+ make_and_verify_dcp (film);
+
+ boost::filesystem::remove_all (film->dir (film->dcp_name(), false));
+
+ content->only_text()->set_use (false);
+ make_and_verify_dcp (film);
+
+#ifdef DCPOMATIC_OSX
+ check_one_frame(film->dir(film->dcp_name()), 325, TestPaths::private_data() / "v2.18.x" / "prophet_frame_325_no_subs_mac.j2c");
+#else
+ check_one_frame(film->dir(film->dcp_name()), 325, TestPaths::private_data() / "v2.18.x" / "prophet_frame_325_no_subs.j2c");
+#endif
+}
diff --git a/test/lib/render_subtitles_test.cc b/test/lib/render_subtitles_test.cc
new file mode 100644
index 000000000..93251e29d
--- /dev/null
+++ b/test/lib/render_subtitles_test.cc
@@ -0,0 +1,210 @@
+/*
+ Copyright (C) 2016 Carl Hetherington <cth@carlh.net>
+
+ This file is part of DCP-o-matic.
+
+ DCP-o-matic is free software; you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation; either version 2 of the License, or
+ (at your option) any later version.
+
+ DCP-o-matic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with DCP-o-matic. If not, see <http://www.gnu.org/licenses/>.
+
+*/
+
+
+/** @file test/render_text_test.cc
+ * @brief Check markup of subtitles for rendering.
+ * @ingroup feature
+ */
+
+
+#include "lib/image.h"
+#include "lib/image_png.h"
+#include "lib/render_text.h"
+#include "lib/string_text.h"
+#include "../test.h"
+#include <dcp/text_string.h>
+#include <pango/pango-utils.h>
+#include <boost/test/unit_test.hpp>
+
+
+using std::make_shared;
+using std::shared_ptr;
+using std::string;
+using std::vector;
+using boost::optional;
+
+
+static void
+add(std::vector<StringText>& s, std::string text, bool italic, bool bold, bool underline)
+{
+ s.push_back (
+ StringText (
+ dcp::TextString(
+ boost::optional<std::string> (),
+ italic,
+ bold,
+ underline,
+ dcp::Colour (255, 255, 255),
+ 42,
+ 1,
+ dcp::Time (),
+ dcp::Time (),
+ 1,
+ dcp::HAlign::LEFT,
+ 1,
+ dcp::VAlign::TOP,
+ 0,
+ vector<dcp::Text::VariableZPosition>(),
+ dcp::Direction::LTR,
+ text,
+ dcp::Effect::NONE,
+ dcp::Colour (0, 0, 0),
+ dcp::Time (),
+ dcp::Time (),
+ 0,
+ std::vector<dcp::Ruby>()
+ ),
+ 2,
+ std::shared_ptr<dcpomatic::Font>(),
+ dcp::SubtitleStandard::SMPTE_2014
+ )
+ );
+}
+
+
+BOOST_AUTO_TEST_CASE (marked_up_test1)
+{
+ std::vector<StringText> s;
+ add (s, "Hello", false, false, false);
+ BOOST_CHECK_EQUAL(marked_up(s, 1024, 1, ""), "<span size=\"41705\" alpha=\"65535\" color=\"#FFFFFF\">Hello</span>");
+}
+
+
+BOOST_AUTO_TEST_CASE (marked_up_test2)
+{
+ std::vector<StringText> s;
+ add (s, "Hello", false, true, false);
+ BOOST_CHECK_EQUAL(marked_up(s, 1024, 1, ""), "<span weight=\"bold\" size=\"41705\" alpha=\"65535\" color=\"#FFFFFF\">Hello</span>");
+}
+
+
+BOOST_AUTO_TEST_CASE (marked_up_test3)
+{
+ std::vector<StringText> s;
+ add (s, "Hello", true, true, false);
+ BOOST_CHECK_EQUAL(marked_up(s, 1024, 1, ""), "<span style=\"italic\" weight=\"bold\" size=\"41705\" alpha=\"65535\" color=\"#FFFFFF\">Hello</span>");
+}
+
+BOOST_AUTO_TEST_CASE (marked_up_test4)
+{
+ std::vector<StringText> s;
+ add (s, "Hello", true, true, true);
+ BOOST_CHECK_EQUAL(marked_up(s, 1024, 1, ""), "<span style=\"italic\" weight=\"bold\" underline=\"single\" size=\"41705\" alpha=\"65535\" color=\"#FFFFFF\">Hello</span>");
+}
+
+BOOST_AUTO_TEST_CASE (marked_up_test5)
+{
+ std::vector<StringText> s;
+ add (s, "Hello", false, true, false);
+ add (s, " world.", false, false, false);
+ BOOST_CHECK_EQUAL (marked_up(s, 1024, 1, ""), "<span weight=\"bold\" size=\"41705\" alpha=\"65535\" color=\"#FFFFFF\">Hello</span><span size=\"41705\" alpha=\"65535\" color=\"#FFFFFF\"> world.</span>");
+}
+
+BOOST_AUTO_TEST_CASE (marked_up_test6)
+{
+ std::vector<StringText> s;
+ add (s, "Hello", true, false, false);
+ add (s, " world ", false, false, false);
+ add (s, "we are bold.", false, true, false);
+ BOOST_CHECK_EQUAL (marked_up(s, 1024, 1, ""), "<span style=\"italic\" size=\"41705\" alpha=\"65535\" color=\"#FFFFFF\">Hello</span><span size=\"41705\" alpha=\"65535\" color=\"#FFFFFF\"> world </span><span weight=\"bold\" size=\"41705\" alpha=\"65535\" color=\"#FFFFFF\">we are bold.</span>");
+}
+
+
+BOOST_AUTO_TEST_CASE(render_text_with_newline_test)
+{
+ std::list<dcp::TextString> ss = {
+ {
+ {}, true, false, false, dcp::Colour(255, 255, 255), 42, 1.0,
+ dcp::Time(0, 0, 0, 0, 24), dcp::Time(0, 0, 1, 0, 24),
+ 0.5, dcp::HAlign::CENTER,
+ 0.5, dcp::VAlign::CENTER,
+ 0.0,
+ vector<dcp::Text::VariableZPosition>(),
+ dcp::Direction::LTR,
+ "Hello world",
+ dcp::Effect::NONE, dcp::Colour(0, 0, 0),
+ {}, {},
+ 0,
+ std::vector<dcp::Ruby>()
+ },
+ {
+ {}, true, false, false, dcp::Colour(255, 255, 255), 42, 1.0,
+ dcp::Time(0, 0, 0, 0, 24), dcp::Time(0, 0, 1, 0, 24),
+ 0.5, dcp::HAlign::CENTER,
+ 0.5, dcp::VAlign::CENTER,
+ 0.0,
+ vector<dcp::Text::VariableZPosition>(),
+ dcp::Direction::LTR,
+ "\n",
+ dcp::Effect::NONE, dcp::Colour(0, 0, 0),
+ {}, {},
+ 0,
+ std::vector<dcp::Ruby>()
+ }
+ };
+
+ std::vector<StringText> st;
+ for (auto i: ss) {
+ st.push_back({i, 0, make_shared<dcpomatic::Font>("foo"), dcp::SubtitleStandard::SMPTE_2014});
+ }
+
+ auto images = render_text(st, dcp::Size(1998, 1080), {}, 24);
+
+ BOOST_CHECK_EQUAL(images.size(), 1U);
+ image_as_png(Image::ensure_alignment(images.front().image, Image::Alignment::PADDED)).write("build/test/render_text_with_newline_test.png");
+#if defined(DCPOMATIC_OSX)
+ check_image("test/data/mac/render_text_with_newline_test.png", "build/test/render_text_with_newline_test.png");
+#elif defined(DCPOMATIC_WINDOWS)
+ check_image("test/data/windows/render_text_with_newline_test.png", "build/test/render_text_with_newline_test.png");
+#elif PANGO_VERSION_CHECK(1, 52, 1)
+ /* This pango version is the one on Ubuntu 24.04, which renders slightly differently */
+ check_image("test/data/ubuntu-24.04/render_text_with_newline_test.png", "build/test/render_text_with_newline_test.png");
+#else
+ check_image("test/data/render_text_with_newline_test.png", "build/test/render_text_with_newline_test.png");
+#endif
+}
+
+
+#if 0
+
+BOOST_AUTO_TEST_CASE (render_text_test)
+{
+ auto dcp_string = dcp::TextString(
+ {}, false, false, false, dcp::Colour(255, 255, 255), 42, 1.0,
+ dcp::Time(0, 0, 0, 0, 24), dcp::Time(0, 0, 1, 0, 24),
+ 0.5, dcp::HAlign::CENTER,
+ 0.5, dcp::VAlign::CENTER,
+ dcp::Direction::LTR,
+ "HÄllo jokers",
+ dcp::Effect::NONE, dcp::Colour(0, 0, 0),
+ {}, {},
+ 0
+ );
+
+ auto string_text = StringText(dcp_string, 0, shared_ptr<dcpomatic::Font>());
+
+ auto images = render_text({ string_text }, dcp::Size(1998, 1080), {}, 24);
+
+ BOOST_CHECK_EQUAL(images.size(), 1U);
+ image_as_png(Image::ensure_alignment(images.front().image, Image::Alignment::PADDED)).write("build/test/render_text_test.png");
+}
+
+#endif
diff --git a/test/lib/repeat_frame_test.cc b/test/lib/repeat_frame_test.cc
new file mode 100644
index 000000000..731b1aadb
--- /dev/null
+++ b/test/lib/repeat_frame_test.cc
@@ -0,0 +1,57 @@
+/*
+ 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/>.
+
+*/
+
+
+/** @file test/repeat_frame_test.cc
+ * @brief Test the repeat of frames by the player when putting a 24fps
+ * source into a 48fps DCP.
+ * @ingroup feature
+ *
+ * @see test/skip_frame_test.cc
+ */
+
+
+#include "lib/dcp_content_type.h"
+#include "lib/ffmpeg_content.h"
+#include "lib/film.h"
+#include "lib/ratio.h"
+#include "lib/video_content.h"
+#include "../test.h"
+#include <boost/test/unit_test.hpp>
+
+
+using std::make_shared;
+
+
+BOOST_AUTO_TEST_CASE (repeat_frame_test)
+{
+ auto c = make_shared<FFmpegContent>("test/data/red_24.mp4");
+ auto film = new_test_film("repeat_frame_test", {c});
+ film->set_interop (false);
+ c->video->set_custom_ratio (1.85);
+
+ film->set_video_frame_rate (48);
+ make_and_verify_dcp (film);
+
+ /* Should be 32 frames of red followed by 16 frames of black to fill the DCP up to 1 second;
+ * no need to check sound.
+ */
+ check_dcp("test/data/repeat_frame_test", film->dir(film->dcp_name()), true);
+}
diff --git a/test/lib/required_disk_space_test.cc b/test/lib/required_disk_space_test.cc
new file mode 100644
index 000000000..b7ee84e1a
--- /dev/null
+++ b/test/lib/required_disk_space_test.cc
@@ -0,0 +1,83 @@
+/*
+ Copyright (C) 2016-2021 Carl Hetherington <cth@carlh.net>
+
+ This file is part of DCP-o-matic.
+
+ DCP-o-matic is free software; you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation; either version 2 of the License, or
+ (at your option) any later version.
+
+ DCP-o-matic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with DCP-o-matic. If not, see <http://www.gnu.org/licenses/>.
+
+*/
+
+
+/** @file test/required_disk_space_test.cc
+ * @brief Check Film::required_disk_space
+ * @ingroup selfcontained
+ */
+
+
+#include "lib/content_factory.h"
+#include "lib/dcp_content.h"
+#include "lib/film.h"
+#include "../test.h"
+#include <boost/test/unit_test.hpp>
+
+
+using std::dynamic_pointer_cast;
+using std::make_shared;
+
+
+void check_within_n (int64_t a, int64_t b, int64_t n)
+{
+ BOOST_CHECK_MESSAGE (abs(a - b) <= n, "Estimated " << a << " differs from reference " << b << " by more than " << n);
+}
+
+
+BOOST_AUTO_TEST_CASE (required_disk_space_test)
+{
+ auto content_a = content_factory("test/data/flat_blue.png")[0];
+ auto content_b = make_shared<DCPContent>("test/data/burnt_subtitle_test_dcp");
+ auto film = new_test_film("required_disk_space_test", { content_a, content_b });
+ film->set_video_bit_rate(VideoEncoding::JPEG2000, 100000000);
+ film->set_audio_channels(8);
+ film->set_reel_type (ReelType::BY_VIDEO_CONTENT);
+
+ check_within_n (
+ film->required_disk_space(),
+ 288LL * (100000000 / 8) / 24 + // video
+ 288LL * 48000 * 8 * 3 / 24 + // audio
+ 65536, // extra
+ 16
+ );
+
+ content_b->set_reference_video (true);
+
+ check_within_n (
+ film->required_disk_space(),
+ 240LL * (100000000 / 8) / 24 + // video
+ 288LL * 48000 * 8 * 3 / 24 + // audio
+ 65536, // extra
+ 16
+ );
+
+ std::string why_not;
+ BOOST_CHECK(content_b->can_reference_audio(film, why_not));
+ content_b->set_reference_audio (true);
+
+ check_within_n (
+ film->required_disk_space(),
+ 240LL * (100000000 / 8) / 24 + // video
+ 240LL * 48000 * 8 * 3 / 24 + // audio
+ 65536, // extra
+ 16
+ );
+}
diff --git a/test/lib/resampler_test.cc b/test/lib/resampler_test.cc
new file mode 100644
index 000000000..3bab146da
--- /dev/null
+++ b/test/lib/resampler_test.cc
@@ -0,0 +1,62 @@
+/*
+ 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/>.
+
+*/
+
+
+/** @file test/resampler_test.cc
+ * @brief Check that the timings that come back from Resampler correspond
+ * to the number of samples it generates.
+ * @ingroup selfcontained
+ */
+
+
+#include "lib/audio_buffers.h"
+#include "lib/resampler.h"
+#include <boost/test/unit_test.hpp>
+#include <iostream>
+
+
+using std::cout;
+using std::make_shared;
+using std::pair;
+
+
+static void
+resampler_test_one (int from, int to)
+{
+ Resampler resamp (from, to, 1);
+
+ /* 3 hours */
+ int64_t const N = int64_t (from) * 60 * 60 * 3;
+
+ /* XXX: no longer checks anything */
+ for (int64_t i = 0; i < N; i += 1000) {
+ auto a = make_shared<AudioBuffers> (1, 1000);
+ a->make_silent ();
+ auto r = resamp.run (a, 0);
+ }
+}
+
+
+BOOST_AUTO_TEST_CASE (resampler_test)
+{
+ resampler_test_one (44100, 48000);
+ resampler_test_one (44100, 46080);
+ resampler_test_one (44100, 50000);
+}
diff --git a/test/lib/scaling_test.cc b/test/lib/scaling_test.cc
new file mode 100644
index 000000000..04b666680
--- /dev/null
+++ b/test/lib/scaling_test.cc
@@ -0,0 +1,112 @@
+/*
+ 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/>.
+
+*/
+
+
+/** @file test/scaling_test.cc
+ * @brief Test scaling and black-padding of images from a still-image source.
+ * @ingroup feature
+ */
+
+
+#include "lib/content_factory.h"
+#include "lib/dcp_content_type.h"
+#include "lib/film.h"
+#include "lib/image_content.h"
+#include "lib/ratio.h"
+#include "lib/video_content.h"
+#include "../test.h"
+#include <boost/test/unit_test.hpp>
+
+
+using std::string;
+using std::shared_ptr;
+using std::make_shared;
+
+
+static void scaling_test_for (shared_ptr<Film> film, shared_ptr<Content> content, float ratio, std::string image, string container)
+{
+ content->video->set_custom_ratio (ratio);
+ film->set_container (Ratio::from_id (container));
+ film->set_interop (false);
+ make_and_verify_dcp (
+ film,
+ {
+ dcp::VerificationNote::Code::MISSING_FFMC_IN_FEATURE,
+ dcp::VerificationNote::Code::MISSING_FFEC_IN_FEATURE
+ });
+
+ boost::filesystem::path ref;
+ ref = "test";
+ ref /= "data";
+ ref /= "scaling_test_" + image + "_" + container;
+
+ boost::filesystem::path check;
+ check = "build";
+ check /= "test";
+ check /= "scaling_test";
+ check /= film->dcp_name();
+
+ /* This test is concerned with the image, so we'll ignore any
+ * differences in sound between the DCP and the reference to avoid test
+ * failures for unrelated reasons.
+ */
+ check_dcp(ref.string(), check.string(), true);
+}
+
+
+BOOST_AUTO_TEST_CASE (scaling_test)
+{
+ auto imc = make_shared<ImageContent>("test/data/simple_testcard_640x480.png");
+ auto film = new_test_film("scaling_test", { imc });
+ film->set_dcp_content_type(DCPContentType::from_isdcf_name("FTR"));
+ imc->video->set_length (1);
+
+ /* F-133: 133 image in a flat container */
+ scaling_test_for (film, imc, 4.0 / 3, "133", "185");
+ /* F: flat image in a flat container */
+ scaling_test_for (film, imc, 1.85, "185", "185");
+ /* F-S: scope image in a flat container */
+ scaling_test_for (film, imc, 2.38695, "239", "185");
+
+ /* S-133: 133 image in a scope container */
+ scaling_test_for (film, imc, 4.0 / 3, "133", "239");
+ /* S-F: flat image in a scope container */
+ scaling_test_for (film, imc, 1.85, "185", "239");
+ /* S: scope image in a scope container */
+ scaling_test_for (film, imc, 2.38695, "239", "239");
+}
+
+
+BOOST_AUTO_TEST_CASE(assertion_failure_when_scaling)
+{
+ auto content = content_factory("test/data/flat_red.png");
+ auto film = new_test_film("assertion_failure_when_scaling", content);
+
+ content[0]->video->set_custom_size(dcp::Size{3996, 2180});
+ film->set_resolution(Resolution::FOUR_K);
+
+ make_and_verify_dcp (
+ film,
+ {
+ dcp::VerificationNote::Code::MISSING_FFMC_IN_FEATURE,
+ dcp::VerificationNote::Code::MISSING_FFEC_IN_FEATURE
+ });
+}
+
diff --git a/test/lib/scoped_temporary_test.cc b/test/lib/scoped_temporary_test.cc
new file mode 100644
index 000000000..714dab3fc
--- /dev/null
+++ b/test/lib/scoped_temporary_test.cc
@@ -0,0 +1,59 @@
+/*
+ Copyright (C) 2022 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/scoped_temporary.h"
+#include <boost/test/unit_test.hpp>
+
+
+using std::string;
+
+
+BOOST_AUTO_TEST_CASE(scoped_temporary_path_allocated_on_construction)
+{
+ ScopedTemporary st;
+
+ BOOST_REQUIRE(!st.path().empty());
+}
+
+
+BOOST_AUTO_TEST_CASE(scoped_temporary_write_read)
+{
+ ScopedTemporary st;
+
+ auto& write = st.open("w");
+ write.puts("hello world");
+ auto& read = st.open("r");
+
+ char buffer[64];
+ read.gets(buffer, 64);
+ BOOST_CHECK(string(buffer) == "hello world");
+}
+
+
+BOOST_AUTO_TEST_CASE(scoped_temporary_take)
+{
+ ScopedTemporary st;
+
+ auto& got = st.open("w");
+ auto taken = got.take();
+
+ fclose(taken);
+}
diff --git a/test/lib/shuffler_test.cc b/test/lib/shuffler_test.cc
new file mode 100644
index 000000000..37d6749c3
--- /dev/null
+++ b/test/lib/shuffler_test.cc
@@ -0,0 +1,198 @@
+/*
+ Copyright (C) 2020-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 "lib/content_video.h"
+#include "lib/piece.h"
+#include "lib/shuffler.h"
+#include <boost/test/unit_test.hpp>
+
+
+using std::list;
+using std::make_shared;
+using std::shared_ptr;
+using std::weak_ptr;
+using boost::optional;
+#if BOOST_VERSION >= 106100
+using namespace boost::placeholders;
+#endif
+using namespace dcpomatic;
+
+
+static void
+push(Shuffler& s, int frame, Eyes eyes)
+{
+ auto piece = make_shared<Piece>(shared_ptr<Content>(), shared_ptr<Decoder>(), FrameRateChange(24, 24));
+ ContentVideo cv;
+ cv.time = ContentTime::from_frames(frame, 24);
+ cv.eyes = eyes;
+ s.video (piece, cv);
+}
+
+list<ContentVideo> pending_cv;
+
+static void
+receive (weak_ptr<Piece>, ContentVideo cv)
+{
+ pending_cv.push_back (cv);
+}
+
+static void
+check (int frame, Eyes eyes, int line)
+{
+ auto const time = ContentTime::from_frames(frame, 24);
+ BOOST_REQUIRE_MESSAGE (!pending_cv.empty(), "Check at " << line << " failed.");
+ BOOST_CHECK_MESSAGE (pending_cv.front().time == time, "Check at " << line << " failed.");
+ BOOST_CHECK_MESSAGE (pending_cv.front().eyes == eyes, "Check at " << line << " failed.");
+ pending_cv.pop_front();
+}
+
+/** A perfect sequence */
+BOOST_AUTO_TEST_CASE (shuffler_test1)
+{
+ Shuffler s;
+ s.Video.connect (boost::bind (&receive, _1, _2));
+
+ for (int i = 0; i < 10; ++i) {
+ push (s, i, Eyes::LEFT);
+ push (s, i, Eyes::RIGHT);
+ check (i, Eyes::LEFT, __LINE__);
+ check (i, Eyes::RIGHT, __LINE__);
+ }
+}
+
+/** Everything present but some simple shuffling needed */
+BOOST_AUTO_TEST_CASE (shuffler_test2)
+{
+ Shuffler s;
+ s.Video.connect (boost::bind (&receive, _1, _2));
+
+ for (int i = 0; i < 10; i += 2) {
+ push (s, i, Eyes::LEFT);
+ push (s, i + 1, Eyes::LEFT);
+ push (s, i, Eyes::RIGHT);
+ push (s, i + 1, Eyes::RIGHT);
+ check (i, Eyes::LEFT, __LINE__);
+ check (i, Eyes::RIGHT, __LINE__);
+ check (i + 1, Eyes::LEFT, __LINE__);
+ check (i + 1, Eyes::RIGHT, __LINE__);
+ }
+}
+
+/** One missing left eye image */
+BOOST_AUTO_TEST_CASE (shuffler_test3)
+{
+ Shuffler s;
+ s.Video.connect (boost::bind (&receive, _1, _2));
+
+ push (s, 0, Eyes::LEFT);
+ check (0, Eyes::LEFT, __LINE__);
+ push (s, 0, Eyes::RIGHT);
+ check (0, Eyes::RIGHT, __LINE__);
+ push (s, 1, Eyes::LEFT);
+ check (1, Eyes::LEFT, __LINE__);
+ push (s, 1, Eyes::RIGHT);
+ check (1, Eyes::RIGHT, __LINE__);
+ push (s, 2, Eyes::RIGHT);
+ push (s, 3, Eyes::LEFT);
+ push (s, 3, Eyes::RIGHT);
+ push (s, 4, Eyes::LEFT);
+ push (s, 4, Eyes::RIGHT);
+ s.flush ();
+ check (2, Eyes::RIGHT, __LINE__);
+ check (3, Eyes::LEFT, __LINE__);
+ check (3, Eyes::RIGHT, __LINE__);
+ check (4, Eyes::LEFT, __LINE__);
+ check (4, Eyes::RIGHT, __LINE__);
+}
+
+/** One missing right eye image */
+BOOST_AUTO_TEST_CASE (shuffler_test4)
+{
+ Shuffler s;
+ s.Video.connect (boost::bind (&receive, _1, _2));
+
+ push (s, 0, Eyes::LEFT);
+ check (0, Eyes::LEFT, __LINE__);
+ push (s, 0, Eyes::RIGHT);
+ check (0, Eyes::RIGHT, __LINE__);
+ push (s, 1, Eyes::LEFT);
+ check (1, Eyes::LEFT, __LINE__);
+ push (s, 1, Eyes::RIGHT);
+ check (1, Eyes::RIGHT, __LINE__);
+ push (s, 2, Eyes::LEFT);
+ push (s, 3, Eyes::LEFT);
+ push (s, 3, Eyes::RIGHT);
+ push (s, 4, Eyes::LEFT);
+ push (s, 4, Eyes::RIGHT);
+ s.flush ();
+ check (2, Eyes::LEFT, __LINE__);
+ check (3, Eyes::LEFT, __LINE__);
+ check (3, Eyes::RIGHT, __LINE__);
+ check (4, Eyes::LEFT, __LINE__);
+ check (4, Eyes::RIGHT, __LINE__);
+}
+
+/** Only one eye */
+BOOST_AUTO_TEST_CASE (shuffler_test5)
+{
+ Shuffler s;
+ s.Video.connect (boost::bind (&receive, _1, _2));
+
+ /* One left should come out straight away */
+ push (s, 0, Eyes::LEFT);
+ check (0, Eyes::LEFT, __LINE__);
+
+ /* More lefts should be kept in the shuffler in the hope that some rights arrive */
+ for (int i = 0; i < s._max_size; ++i) {
+ push (s, i + 1, Eyes::LEFT);
+ }
+ BOOST_CHECK (pending_cv.empty ());
+
+ /* If enough lefts come the shuffler should conclude that there's no rights and start
+ giving out the lefts.
+ */
+ push (s, s._max_size + 1, Eyes::LEFT);
+ check (1, Eyes::LEFT, __LINE__);
+}
+
+/** One complete frame (L+R) missing.
+ Shuffler should carry on, skipping this frame, as the player will cope with it.
+*/
+BOOST_AUTO_TEST_CASE (shuffler_test6)
+{
+ Shuffler s;
+ s.Video.connect (boost::bind (&receive, _1, _2));
+
+ push (s, 0, Eyes::LEFT);
+ check (0, Eyes::LEFT, __LINE__);
+ push (s, 0, Eyes::RIGHT);
+ check (0, Eyes::RIGHT, __LINE__);
+
+ push (s, 2, Eyes::LEFT);
+ push (s, 2, Eyes::RIGHT);
+ check (2, Eyes::LEFT, __LINE__);
+ check (2, Eyes::RIGHT, __LINE__);
+
+ push (s, 3, Eyes::LEFT);
+ check (3, Eyes::LEFT, __LINE__);
+ push (s, 3, Eyes::RIGHT);
+ check (3, Eyes::RIGHT, __LINE__);
+}
diff --git a/test/lib/silence_padding_test.cc b/test/lib/silence_padding_test.cc
new file mode 100644
index 000000000..4155ff5c3
--- /dev/null
+++ b/test/lib/silence_padding_test.cc
@@ -0,0 +1,145 @@
+/*
+ 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/>.
+
+*/
+
+
+/** @file test/silence_padding_test.cc
+ * @brief Test the padding (with silence) of a mono source to a 6-channel DCP.
+ * @ingroup feature
+ */
+
+
+#include "lib/constants.h"
+#include "lib/ffmpeg_content.h"
+#include "lib/film.h"
+#include "lib/dcp_content_type.h"
+#include "lib/ratio.h"
+#include "../test.h"
+#include <dcp/cpl.h>
+#include <dcp/dcp.h>
+#include <dcp/sound_asset.h>
+#include <dcp/sound_frame.h>
+#include <dcp/reel.h>
+#include <dcp/reel_sound_asset.h>
+#include <dcp/sound_asset_reader.h>
+#include <boost/test/unit_test.hpp>
+
+
+using std::make_shared;
+using std::string;
+using boost::lexical_cast;
+
+
+static void
+test_silence_padding(int channels, dcp::Standard standard)
+{
+ string const film_name = "silence_padding_test_" + lexical_cast<string> (channels);
+ auto film = new_test_film(
+ film_name,
+ {
+ make_shared<FFmpegContent>("test/data/flat_red.png"),
+ make_shared<FFmpegContent>("test/data/staircase.wav")
+ });
+
+ if (standard == dcp::Standard::INTEROP) {
+ film->set_interop(true);
+ }
+ film->set_audio_channels (channels);
+
+ std::vector<dcp::VerificationNote::Code> codes;
+ if (standard == dcp::Standard::INTEROP) {
+ codes.push_back(dcp::VerificationNote::Code::INVALID_STANDARD);
+ }
+ auto const dcp_inspect = channels == 2 || channels == 6 || channels >= 8;
+ auto const clairmeta = (channels % 2) == 0;
+ make_and_verify_dcp(film, codes, dcp_inspect, clairmeta);
+
+ boost::filesystem::path path = "build/test";
+ path /= film_name;
+ path /= film->dcp_name ();
+ dcp::DCP check (path.string ());
+ check.read ();
+
+ auto sound_asset = check.cpls()[0]->reels()[0]->main_sound();
+ BOOST_CHECK (sound_asset);
+ BOOST_CHECK_EQUAL (sound_asset->asset()->channels (), channels);
+
+ /* Sample index in the DCP */
+ int n = 0;
+ /* DCP sound asset frame */
+ int frame = 0;
+
+ auto const asset_channels = sound_asset->asset()->channels();
+ if (standard == dcp::Standard::SMPTE) {
+ DCP_ASSERT(asset_channels == MAX_DCP_AUDIO_CHANNELS)
+ } else {
+ DCP_ASSERT(asset_channels == channels);
+ }
+
+ while (n < sound_asset->asset()->intrinsic_duration()) {
+ auto sound_frame = sound_asset->asset()->start_read()->get_frame(frame++);
+ uint8_t const * d = sound_frame->data ();
+
+ for (int offset = 0; offset < sound_frame->size(); offset += (3 * asset_channels)) {
+
+ for (auto channel = 0; channel < asset_channels; ++channel) {
+ auto const sample = d[offset + channel * 3 + 1] | (d[offset + channel * 3 + 2] << 8);
+ if (channel == 2) {
+ /* Input should be on centre */
+ BOOST_CHECK_EQUAL(sample, n);
+ } else {
+ /* Everything else should be silent */
+ BOOST_CHECK_EQUAL(sample, 0);
+ }
+ }
+
+ ++n;
+ }
+ }
+}
+
+
+BOOST_AUTO_TEST_CASE (silence_padding_test)
+{
+ for (int i = 1; i < MAX_DCP_AUDIO_CHANNELS; ++i) {
+ test_silence_padding(i, dcp::Standard::INTEROP);
+ }
+
+ test_silence_padding(MAX_DCP_AUDIO_CHANNELS, dcp::Standard::SMPTE);
+}
+
+
+/** Test a situation that used to crash because of a sub-sample rounding confusion
+ * caused by a trim.
+ */
+
+BOOST_AUTO_TEST_CASE (silence_padding_test2)
+{
+ Cleanup cl;
+
+ auto content = make_shared<FFmpegContent>(TestPaths::private_data() / "cars.mov");
+ auto film = new_test_film("silence_padding_test2", { content }, &cl);
+
+ film->set_video_frame_rate (24);
+ content->set_trim_start(film, dcpomatic::ContentTime(4003));
+
+ make_and_verify_dcp (film);
+
+ cl.run ();
+}
diff --git a/test/lib/skip_frame_test.cc b/test/lib/skip_frame_test.cc
new file mode 100644
index 000000000..7eef2f1b5
--- /dev/null
+++ b/test/lib/skip_frame_test.cc
@@ -0,0 +1,55 @@
+/*
+ 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/>.
+
+*/
+
+
+/** @file test/skip_frame_test.cc
+ * @brief Test the skip of frames by the player when putting a 48fps
+ * source into a 24fps DCP.
+ * @ingroup feature
+ *
+ * @see test/repeat_frame_test.cc
+ */
+
+
+#include <boost/test/unit_test.hpp>
+#include "../test.h"
+#include "lib/film.h"
+#include "lib/ratio.h"
+#include "lib/ffmpeg_content.h"
+#include "lib/dcp_content_type.h"
+#include "lib/video_content.h"
+
+
+using std::make_shared;
+
+
+BOOST_AUTO_TEST_CASE (skip_frame_test)
+{
+ auto c = make_shared<FFmpegContent>("test/data/count300bd48.m2ts");
+ auto film = new_test_film("skip_frame_test", { c });
+
+ film->set_video_frame_rate (24);
+ make_and_verify_dcp (film);
+
+ /* Should be white numbers on a black background counting up from 2 in steps of 2
+ * up to 300. The sound is irrelevant here.
+ */
+ check_dcp("test/data/skip_frame_test", film->dir(film->dcp_name()), true);
+}
diff --git a/test/lib/smtp_server.cc b/test/lib/smtp_server.cc
new file mode 100644
index 000000000..c6802bded
--- /dev/null
+++ b/test/lib/smtp_server.cc
@@ -0,0 +1,79 @@
+/*
+ Copyright (C) 2025 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 "smtp_server.h"
+#include <boost/asio.hpp>
+#include <iostream>
+#include <string>
+
+
+using std::string;
+using boost::asio::ip::tcp;
+
+
+void
+run_smtp_server(int port, bool fail)
+{
+ boost::asio::io_context context;
+ tcp::acceptor acceptor(context, tcp::endpoint(tcp::v4(), port));
+ tcp::socket socket(context);
+ acceptor.accept(socket);
+
+ auto send = [&socket](string message) {
+ boost::system::error_code error;
+ boost::asio::write(socket, boost::asio::buffer(message + "\r\n"), boost::asio::transfer_all(), error);
+ };
+
+ auto receive = [&socket]() {
+ boost::asio::streambuf buffer;
+ boost::asio::read_until(socket, buffer, "\n");
+ return string{
+ std::istreambuf_iterator<char>(&buffer),
+ std::istreambuf_iterator<char>()
+ };
+ };
+
+ send("220 smtp.example.com ESMTP Postfix");
+ /* EHLO */
+ receive();
+ send("250-smtp.example.com Hello mate [127.0.0.1]");
+ send("250-SIZE 14680064");
+ send("250-PIPELINING");
+ send("250 HELP");
+ /* MAIL FROM */
+ receive();
+ send("250 Ok");
+ /* RCPT TO */
+ if (fail) {
+ return;
+ }
+ receive();
+ send("250 Ok");
+ /* DATA */
+ receive();
+ send("354 End data with <CR><LF>.<CR><LF>");
+ /* Email body */
+ receive();
+ send("250 Ok");
+ /* QUIT */
+ receive();
+}
+
diff --git a/test/lib/smtp_server.h b/test/lib/smtp_server.h
new file mode 100644
index 000000000..f3cc1d0a9
--- /dev/null
+++ b/test/lib/smtp_server.h
@@ -0,0 +1,23 @@
+/*
+ Copyright (C) 2025 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/>.
+
+*/
+
+
+extern void run_smtp_server(int port, bool fail);
+
diff --git a/test/lib/socket_test.cc b/test/lib/socket_test.cc
new file mode 100644
index 000000000..b85f7b0cb
--- /dev/null
+++ b/test/lib/socket_test.cc
@@ -0,0 +1,182 @@
+/*
+ Copyright (C) 2020-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 "lib/dcpomatic_socket.h"
+#include "lib/io_context.h"
+#include "lib/server.h"
+#include <fmt/format.h>
+#include <boost/test/unit_test.hpp>
+#include <boost/thread.hpp>
+#include <cstring>
+#include <iostream>
+
+
+using std::make_shared;
+using std::shared_ptr;
+using std::string;
+using boost::bind;
+
+
+#define TEST_SERVER_PORT 9142
+#define TEST_SERVER_BUFFER_LENGTH 1024
+
+
+class TestServer : public Server
+{
+public:
+ TestServer (bool digest)
+ : Server (TEST_SERVER_PORT, 30)
+ , _buffer (TEST_SERVER_BUFFER_LENGTH)
+ , _size (0)
+ , _result (false)
+ , _digest (digest)
+ {
+ _thread = boost::thread(bind(&TestServer::run, this));
+ }
+
+ ~TestServer ()
+ {
+ boost::this_thread::disable_interruption dis;
+ stop ();
+ try {
+ _thread.join ();
+ } catch (...) {}
+ }
+
+ void expect (int size)
+ {
+ boost::mutex::scoped_lock lm (_mutex);
+ _size = size;
+ }
+
+ uint8_t const * buffer() const {
+ return _buffer.data();
+ }
+
+ void await ()
+ {
+ boost::mutex::scoped_lock lm (_mutex);
+ if (_size) {
+ _condition.wait (lm);
+ }
+ }
+
+ bool result () const {
+ return _result;
+ }
+
+private:
+ void handle (std::shared_ptr<Socket> socket) override
+ {
+ boost::mutex::scoped_lock lm (_mutex);
+ BOOST_REQUIRE (_size);
+ if (_digest) {
+ Socket::ReadDigestScope ds (socket);
+ socket->read (_buffer.data(), _size);
+ _size = 0;
+ _condition.notify_one ();
+ _result = ds.check();
+ } else {
+ socket->read (_buffer.data(), _size);
+ _size = 0;
+ _condition.notify_one ();
+ }
+ }
+
+ boost::thread _thread;
+ boost::mutex _mutex;
+ boost::condition _condition;
+ std::vector<uint8_t> _buffer;
+ int _size;
+ bool _result;
+ bool _digest;
+};
+
+
+void
+send (shared_ptr<Socket> socket, char const* message)
+{
+ socket->write (reinterpret_cast<uint8_t const *>(message), strlen(message) + 1);
+}
+
+
+/** Basic test to see if Socket can send and receive data */
+BOOST_AUTO_TEST_CASE (socket_basic_test)
+{
+ using boost::asio::ip::tcp;
+
+ TestServer server(false);
+ server.expect (13);
+
+ auto socket = make_shared<Socket>();
+ socket->connect("127.0.0.1", TEST_SERVER_PORT);
+ send (socket, "Hello world!");
+
+ server.await ();
+ BOOST_CHECK_EQUAL(strcmp(reinterpret_cast<char const *>(server.buffer()), "Hello world!"), 0);
+}
+
+
+/** Check that the socket "auto-digest" creation works */
+BOOST_AUTO_TEST_CASE (socket_digest_test1)
+{
+ using boost::asio::ip::tcp;
+
+ TestServer server(false);
+ server.expect (13 + 16);
+
+ shared_ptr<Socket> socket(new Socket);
+ socket->connect("127.0.0.1", TEST_SERVER_PORT);
+ {
+ Socket::WriteDigestScope ds(socket);
+ send (socket, "Hello world!");
+ }
+
+ server.await ();
+ BOOST_CHECK_EQUAL(strcmp(reinterpret_cast<char const *>(server.buffer()), "Hello world!"), 0);
+
+ /* printf "%s\0" "Hello world!" | md5sum" in bash */
+ char ref[] = "\x59\x86\x88\xed\x18\xc8\x71\xdd\x57\xb9\xb7\x9f\x4b\x03\x14\xcf";
+ BOOST_CHECK_EQUAL (memcmp(server.buffer() + 13, ref, 16), 0);
+}
+
+
+/** Check that the socket "auto-digest" round-trip works */
+BOOST_AUTO_TEST_CASE (socket_digest_test2)
+{
+ using boost::asio::ip::tcp;
+
+ TestServer server(true);
+ server.expect (13);
+
+ shared_ptr<Socket> socket(new Socket);
+ socket->connect("127.0.0.1", TEST_SERVER_PORT);
+ {
+ Socket::WriteDigestScope ds(socket);
+ send (socket, "Hello world!");
+ }
+
+ server.await ();
+ BOOST_CHECK_EQUAL(strcmp(reinterpret_cast<char const *>(server.buffer()), "Hello world!"), 0);
+
+ BOOST_CHECK (server.result());
+}
+
diff --git a/test/lib/srt_subtitle_test.cc b/test/lib/srt_subtitle_test.cc
new file mode 100644
index 000000000..bb74d77f3
--- /dev/null
+++ b/test/lib/srt_subtitle_test.cc
@@ -0,0 +1,298 @@
+/*
+ Copyright (C) 2015-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/>.
+
+*/
+
+
+/** @file test/srt_subtitle_test.cc
+ * @brief Test writing DCPs with subtitles from .srt.
+ * @ingroup feature
+ */
+
+
+#include "lib/dcp_content_type.h"
+#include "lib/film.h"
+#include "lib/font.h"
+#include "lib/ratio.h"
+#include "lib/string_text_file_content.h"
+#include "lib/text_content.h"
+#include "../test.h"
+#include <dcp/smpte_text_asset.h>
+#include <dcp/text_string.h>
+#include <boost/algorithm/string.hpp>
+#include <boost/test/unit_test.hpp>
+#include <list>
+
+
+using std::list;
+using std::make_shared;
+using std::shared_ptr;
+using std::string;
+using namespace dcpomatic;
+
+
+/** Make a very short DCP with a single subtitle from .srt with no specified fonts */
+BOOST_AUTO_TEST_CASE (srt_subtitle_test)
+{
+ auto content = make_shared<StringTextFileContent>("test/data/subrip2.srt");
+ auto film = new_test_film("srt_subtitle_test", { content });
+ film->set_dcp_content_type(DCPContentType::from_isdcf_name("TLR"));
+ film->set_name("frobozz");
+ film->set_audio_channels(16);
+
+ content->only_text()->set_use (true);
+ content->only_text()->set_burn (false);
+ make_and_verify_dcp (
+ film,
+ {
+ dcp::VerificationNote::Code::MISSING_SUBTITLE_LANGUAGE,
+ dcp::VerificationNote::Code::INVALID_SUBTITLE_FIRST_TEXT_TIME,
+ dcp::VerificationNote::Code::MISSING_CPL_METADATA
+ });
+
+
+ /* Should be blank video with a subtitle MXF */
+ check_dcp ("test/data/srt_subtitle_test", film->dir (film->dcp_name ()));
+}
+
+
+/** Same again but with a `font' specified */
+BOOST_AUTO_TEST_CASE (srt_subtitle_test2)
+{
+ auto content = make_shared<StringTextFileContent> ("test/data/subrip2.srt");
+ auto film = new_test_film("srt_subtitle_test2", { content });
+ film->set_dcp_content_type(DCPContentType::from_isdcf_name("TLR"));
+ film->set_name("frobozz");
+ film->set_audio_channels (6);
+
+ content->only_text()->set_use (true);
+ content->only_text()->set_burn (false);
+ /* Use test/data/subrip2.srt as if it were a font file */
+ content->only_text()->fonts().front()->set_file("test/data/subrip2.srt");
+
+ make_and_verify_dcp (
+ film,
+ {
+ dcp::VerificationNote::Code::MISSING_SUBTITLE_LANGUAGE,
+ dcp::VerificationNote::Code::INVALID_SUBTITLE_FIRST_TEXT_TIME,
+ dcp::VerificationNote::Code::MISSING_CPL_METADATA
+ },
+ true,
+ /* ClairMeta tries to inspect the font file and fails because it isn't one */
+ false
+ );
+
+ /* Should be blank video with a subtitle MXF; sound is irrelevant */
+ check_dcp("test/data/srt_subtitle_test2", film->dir(film->dcp_name()), true);
+}
+
+
+static void
+check_subtitle_file (shared_ptr<Film> film, boost::filesystem::path ref)
+{
+ /* Find the subtitle file and check it */
+ check_xml (subtitle_file(film), ref, {"SubtitleID"});
+}
+
+
+/** Make another DCP with a longer .srt file */
+BOOST_AUTO_TEST_CASE (srt_subtitle_test3)
+{
+ Cleanup cl;
+
+ auto content = make_shared<StringTextFileContent>(TestPaths::private_data() / "Ankoemmling_short.srt");
+ auto film = new_test_film("srt_subtitle_test3", { content }, &cl);
+
+ film->set_name ("frobozz");
+ film->set_interop (true);
+ film->set_audio_channels (6);
+
+ content->only_text()->set_use (true);
+ content->only_text()->set_burn (false);
+ content->only_text()->set_language(dcp::LanguageTag("de"));
+
+ make_and_verify_dcp (film, {dcp::VerificationNote::Code::INVALID_STANDARD});
+
+ check_subtitle_file (film, TestPaths::private_data() / "Ankoemmling_short.xml");
+
+ cl.run ();
+}
+
+
+/** Build a small DCP with no picture and a single subtitle overlaid onto it */
+BOOST_AUTO_TEST_CASE (srt_subtitle_test4)
+{
+ auto content = make_shared<StringTextFileContent>("test/data/subrip2.srt");
+ auto film = new_test_film("srt_subtitle_test4", { content });
+ film->set_dcp_content_type(DCPContentType::from_isdcf_name("TLR"));
+ film->set_name("frobozz");
+ content->only_text()->set_use (true);
+ content->only_text()->set_burn (false);
+ make_and_verify_dcp (
+ film,
+ {
+ dcp::VerificationNote::Code::MISSING_SUBTITLE_LANGUAGE,
+ dcp::VerificationNote::Code::INVALID_SUBTITLE_FIRST_TEXT_TIME,
+ dcp::VerificationNote::Code::MISSING_CPL_METADATA
+ });
+
+ /* Should be blank video with MXF subtitles; sound is irrelevant */
+ check_dcp("test/data/xml_subtitle_test", film->dir(film->dcp_name()), true);
+}
+
+
+/** Check the subtitle XML when there are two subtitle files in the project */
+BOOST_AUTO_TEST_CASE (srt_subtitle_test5)
+{
+ auto film = new_test_film("srt_subtitle_test5");
+ film->set_dcp_content_type(DCPContentType::from_isdcf_name("TLR"));
+ film->set_name("frobozz");
+ film->set_interop (true);
+ film->set_sequence (false);
+ film->set_audio_channels(6);
+ for (auto i = 0; i < 2; ++i) {
+ auto content = make_shared<StringTextFileContent>("test/data/subrip2.srt");
+ content->only_text()->set_use (true);
+ content->only_text()->set_burn (false);
+ content->only_text()->set_language(dcp::LanguageTag("de"));
+ film->examine_and_add_content (content);
+ BOOST_REQUIRE (!wait_for_jobs());
+ content->set_position (film, DCPTime());
+ }
+ make_and_verify_dcp (film, {dcp::VerificationNote::Code::INVALID_STANDARD});
+
+ check_dcp ("test/data/xml_subtitle_test2", film->dir (film->dcp_name ()));
+}
+
+
+BOOST_AUTO_TEST_CASE (srt_subtitle_test6)
+{
+ auto content = make_shared<StringTextFileContent>("test/data/frames.srt");
+ auto film = new_test_film("srt_subtitle_test6", {content});
+ film->set_interop (false);
+ content->only_text()->set_use (true);
+ content->only_text()->set_burn (false);
+ make_and_verify_dcp (
+ film,
+ {
+ dcp::VerificationNote::Code::MISSING_SUBTITLE_LANGUAGE,
+ dcp::VerificationNote::Code::INVALID_SUBTITLE_FIRST_TEXT_TIME,
+ dcp::VerificationNote::Code::MISSING_CPL_METADATA,
+ dcp::VerificationNote::Code::INVALID_SUBTITLE_DURATION_BV21,
+ dcp::VerificationNote::Code::INVALID_SUBTITLE_SPACING,
+ });
+
+ /* This test is concerned with the subtitles, so we'll ignore any
+ * differences in sound between the DCP and the reference to avoid test
+ * failures for unrelated reasons.
+ */
+ check_dcp("test/data/srt_subtitle_test6", film->dir(film->dcp_name()), true);
+}
+
+
+/** Test a case where a & in srt ended up in the SMPTE subtitle as &amp;amp */
+BOOST_AUTO_TEST_CASE(srt_subtitle_entity)
+{
+ std::ofstream srt("build/test/srt_subtitle_entity.srt");
+ srt << "1\n";
+ srt << "00:00:01,000 -> 00:00:10,000\n";
+ srt << "Hello & world\n";
+ srt.close();
+
+ auto content = make_shared<StringTextFileContent>("build/test/srt_subtitle_entity.srt");
+ auto film = new_test_film("srt_subtitle_entity", { content });
+ film->set_interop(false);
+ content->only_text()->set_use(true);
+ content->only_text()->set_burn(false);
+ make_and_verify_dcp (
+ film,
+ {
+ dcp::VerificationNote::Code::MISSING_SUBTITLE_LANGUAGE,
+ dcp::VerificationNote::Code::INVALID_SUBTITLE_FIRST_TEXT_TIME,
+ dcp::VerificationNote::Code::MISSING_CPL_METADATA,
+ dcp::VerificationNote::Code::INVALID_SUBTITLE_DURATION_BV21,
+ dcp::VerificationNote::Code::INVALID_SUBTITLE_SPACING,
+ });
+
+ dcp::SMPTETextAsset check(dcp_file(film, "sub_"));
+ auto subs = check.texts();
+ BOOST_REQUIRE_EQUAL(subs.size(), 1U);
+ auto sub = std::dynamic_pointer_cast<const dcp::TextString>(subs[0]);
+ BOOST_REQUIRE(sub);
+ /* dcp::TextAsset gets the text from the XML with get_content(), which
+ * resolves the 5 predefined entities & " < > ' so we shouldn't see any
+ * entity here.
+ */
+ BOOST_CHECK_EQUAL(sub->text(), "Hello & world");
+
+ /* It should be escaped in the raw XML though */
+ BOOST_REQUIRE(static_cast<bool>(check.raw_xml()));
+ BOOST_CHECK(check.raw_xml()->find("Hello &amp; world") != string::npos);
+}
+
+
+/** A control code in a .srt file should not make it into the XML */
+BOOST_AUTO_TEST_CASE(srt_subtitle_control_code)
+{
+ std::ofstream srt("build/test/srt_subtitle_control_code.srt");
+ srt << "1\n";
+ srt << "00:00:01,000 -> 00:00:10,000\n";
+ srt << "Hello \x0c world\n";
+ srt.close();
+
+ auto content = make_shared<StringTextFileContent>("build/test/srt_subtitle_control_code.srt");
+ auto film = new_test_film("srt_subtitle_control_code", { content });
+ film->set_interop(false);
+ content->only_text()->set_use(true);
+ content->only_text()->set_burn(false);
+ make_and_verify_dcp (
+ film,
+ {
+ dcp::VerificationNote::Code::MISSING_SUBTITLE_LANGUAGE,
+ dcp::VerificationNote::Code::INVALID_SUBTITLE_FIRST_TEXT_TIME,
+ dcp::VerificationNote::Code::MISSING_CPL_METADATA,
+ });
+}
+
+
+#if 0
+/* XXX: this is disabled; there is some difference in font rendering
+ between the test machine and others.
+*/
+
+/** Test rendering of a SubRip subtitle */
+BOOST_AUTO_TEST_CASE (srt_subtitle_test4)
+{
+ shared_ptr<StringTextFile> content (new StringTextFile("test/data/subrip.srt"));
+ shared_ptr<Film> film = new_test_film("subrip_render_test", { content });
+ content->examine (shared_ptr<Job> (), true);
+ BOOST_CHECK_EQUAL (content->full_length(), DCPTime::from_seconds ((3 * 60) + 56.471));
+
+ shared_ptr<SubRipDecoder> decoder (new SubRipDecoder (content));
+ list<ContentStringText> cts = decoder->get_plain_texts (
+ ContentTimePeriod (
+ ContentTime::from_seconds (109), ContentTime::from_seconds (110)
+ ), false
+ );
+ BOOST_CHECK_EQUAL (cts.size(), 1);
+
+ PositionImage image = render_text (cts.front().subs, dcp::Size (1998, 1080));
+ write_image (image.image, "build/test/subrip_render_test.png");
+ check_file ("build/test/subrip_render_test.png", "test/data/subrip_render_test.png");
+}
+#endif
diff --git a/test/lib/ssa_subtitle_test.cc b/test/lib/ssa_subtitle_test.cc
new file mode 100644
index 000000000..db0759f36
--- /dev/null
+++ b/test/lib/ssa_subtitle_test.cc
@@ -0,0 +1,78 @@
+/*
+ Copyright (C) 2016-2021 Carl Hetherington <cth@carlh.net>
+
+ This file is part of DCP-o-matic.
+
+ DCP-o-matic is free software; you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation; either version 2 of the License, or
+ (at your option) any later version.
+
+ DCP-o-matic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with DCP-o-matic. If not, see <http://www.gnu.org/licenses/>.
+
+*/
+
+
+/** @file test/ssa_subtitle_test.cc
+ * @brief Test use of SSA subtitle files.
+ * @ingroup feature
+ */
+
+
+#include "lib/dcp_content_type.h"
+#include "lib/film.h"
+#include "lib/font.h"
+#include "lib/ratio.h"
+#include "lib/string_text_file_content.h"
+#include "lib/text_content.h"
+#include "../test.h"
+#include <dcp/equality_options.h>
+#include <dcp/interop_text_asset.h>
+#include <boost/test/unit_test.hpp>
+#include <boost/algorithm/string.hpp>
+
+
+using std::list;
+using std::make_shared;
+using std::string;
+
+
+/** Make a DCP with subs from a .ssa file */
+BOOST_AUTO_TEST_CASE (ssa_subtitle_test1)
+{
+ Cleanup cl;
+
+ auto film = new_test_film("ssa_subtitle_test1", {}, &cl);
+
+ film->set_container (Ratio::from_id ("185"));
+ film->set_name ("frobozz");
+ film->set_interop (true);
+ auto content = make_shared<StringTextFileContent>(TestPaths::private_data() / "DKH_UT_EN20160601def.ssa");
+ film->examine_and_add_content (content);
+ BOOST_REQUIRE (!wait_for_jobs());
+
+ content->only_text()->set_use (true);
+ content->only_text()->set_burn (false);
+ content->only_text()->set_language(dcp::LanguageTag("de"));
+
+ make_and_verify_dcp (film, { dcp::VerificationNote::Code::INVALID_STANDARD });
+
+ auto ref = make_shared<dcp::InteropTextAsset>(TestPaths::private_data() / "DKH_UT_EN20160601def.xml");
+ auto check = make_shared<dcp::InteropTextAsset>(subtitle_file(film));
+
+ dcp::EqualityOptions options;
+ options.max_text_vertical_position_error = 0.1;
+ BOOST_CHECK(ref->equals(check, options, [](dcp::NoteType t, string n) {
+ if (t == dcp::NoteType::ERROR) {
+ std::cerr << n << "\n";
+ }
+ }));
+
+ cl.run ();
+}
diff --git a/test/lib/stream_test.cc b/test/lib/stream_test.cc
new file mode 100644
index 000000000..ecd2deac8
--- /dev/null
+++ b/test/lib/stream_test.cc
@@ -0,0 +1,95 @@
+/*
+ Copyright (C) 2013-2014 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/>.
+
+*/
+
+
+/** @test test/stream_test.cc
+ * @brief Some simple tests of FFmpegAudioStream.
+ */
+
+
+#include "lib/ffmpeg_audio_stream.h"
+#include "lib/ffmpeg_content.h"
+#include "lib/film.h"
+#include <libcxml/cxml.h>
+#include <dcp/warnings.h>
+LIBDCP_DISABLE_WARNINGS
+#include <libxml++/libxml++.h>
+LIBDCP_ENABLE_WARNINGS
+#include <boost/test/unit_test.hpp>
+
+
+using std::list;
+using std::pair;
+
+
+BOOST_AUTO_TEST_CASE (stream_test)
+{
+ xmlpp::Document doc;
+ auto root = doc.create_root_node("FFmpegAudioStream");
+ cxml::add_text_child(root, "Name", "hello there world");
+ cxml::add_text_child(root, "Id", "4");
+ cxml::add_text_child(root, "FrameRate", "44100");
+ cxml::add_text_child(root, "Channels", "2");
+
+ /* This is the state file version 5 description of the mapping */
+
+ auto mapping = cxml::add_child(root, "Mapping");
+ cxml::add_text_child(mapping, "ContentChannels", "2");
+ {
+ /* L -> L */
+ auto map = cxml::add_child(mapping, "Map");
+ cxml::add_text_child(map, "ContentIndex", "0");
+ cxml::add_text_child(map, "DCP", "0");
+ }
+ {
+ /* L -> C */
+ auto map = cxml::add_child(mapping, "Map");
+ cxml::add_text_child(map, "ContentIndex", "0");
+ cxml::add_text_child(map, "DCP", "2");
+ }
+ {
+ /* R -> R */
+ auto map = cxml::add_child(mapping, "Map");
+ cxml::add_text_child(map, "ContentIndex", "1");
+ cxml::add_text_child(map, "DCP", "1");
+ }
+ {
+ /* R -> C */
+ auto map = cxml::add_child(mapping, "Map");
+ cxml::add_text_child(map, "ContentIndex", "1");
+ cxml::add_text_child(map, "DCP", "2");
+ }
+
+ FFmpegAudioStream a (cxml::NodePtr (new cxml::Node (root)), 5);
+
+ BOOST_CHECK_EQUAL (a.identifier(), "4");
+ BOOST_CHECK_EQUAL (a.frame_rate(), 44100);
+ BOOST_CHECK_EQUAL (a.channels(), 2);
+ BOOST_CHECK_EQUAL (a.name, "hello there world");
+ BOOST_CHECK_EQUAL (a.mapping().input_channels(), 2);
+
+ BOOST_CHECK_EQUAL (a.mapping().get(0, dcp::Channel::LEFT), 1);
+ BOOST_CHECK_EQUAL (a.mapping().get(0, dcp::Channel::RIGHT), 0);
+ BOOST_CHECK_EQUAL (a.mapping().get(0, dcp::Channel::CENTRE), 1);
+ BOOST_CHECK_EQUAL (a.mapping().get(1, dcp::Channel::LEFT), 0);
+ BOOST_CHECK_EQUAL (a.mapping().get(1, dcp::Channel::RIGHT), 1);
+ BOOST_CHECK_EQUAL (a.mapping().get(1, dcp::Channel::CENTRE), 1);
+}
+
diff --git a/test/lib/subtitle_charset_test.cc b/test/lib/subtitle_charset_test.cc
new file mode 100644
index 000000000..9800e86f2
--- /dev/null
+++ b/test/lib/subtitle_charset_test.cc
@@ -0,0 +1,51 @@
+/*
+ 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 "lib/content.h"
+#include "lib/content_factory.h"
+#include "lib/film.h"
+#include "lib/string_text_file.h"
+#include "lib/string_text_file_content.h"
+#include "../test.h"
+#include <boost/test/unit_test.hpp>
+
+
+using std::dynamic_pointer_cast;
+
+
+/** Test parsing of UTF16 CR/LF input */
+BOOST_AUTO_TEST_CASE (subtitle_charset_test1)
+{
+ auto content = content_factory(TestPaths::private_data() / "PADDINGTON soustitresVFdef.srt");
+ auto film = new_test_film("subtitle_charset_test1", content);
+}
+
+
+/** Test parsing of OSX input */
+BOOST_AUTO_TEST_CASE (subtitle_charset_test2)
+{
+ auto content = content_factory("test/data/osx.srt");
+ auto film = new_test_film("subtitle_charset_test2", content);
+ auto ts = dynamic_pointer_cast<StringTextFileContent>(content[0]);
+ BOOST_REQUIRE (ts);
+ /* Make sure we got the subtitle data from the file */
+ BOOST_REQUIRE_EQUAL(content[0]->full_length(film).get(), 6052032);
+}
diff --git a/test/lib/subtitle_font_id_change_test.cc b/test/lib/subtitle_font_id_change_test.cc
new file mode 100644
index 000000000..b8e6f6ac5
--- /dev/null
+++ b/test/lib/subtitle_font_id_change_test.cc
@@ -0,0 +1,164 @@
+/*
+ Copyright (C) 2022 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 test/subtitle_font_id_change_test.cc
+ * @brief Check that old projects can still be used after the changes in 5a820bb8fae34591be5ac6d19a73461b9dab532a
+ */
+
+
+#include "lib/check_content_job.h"
+#include "lib/content.h"
+#include "lib/film.h"
+#include "lib/font.h"
+#include "lib/text_content.h"
+#include "../test.h"
+#include <dcp/verify.h>
+#include <boost/filesystem.hpp>
+#include <boost/test/unit_test.hpp>
+
+
+using std::string;
+
+
+BOOST_AUTO_TEST_CASE(subtitle_font_id_change_test1)
+{
+ auto film = new_test_film("subtitle_font_id_change_test1");
+ boost::filesystem::remove(film->file("metadata.xml"));
+ boost::filesystem::copy_file("test/data/subtitle_font_id_change_test1.xml", film->file("metadata.xml"));
+ film->read_metadata();
+
+ auto content = film->content();
+ BOOST_REQUIRE_EQUAL(content.size(), 1U);
+ BOOST_REQUIRE_EQUAL(content[0]->text.size(), 1U);
+
+ content[0]->set_paths({"test/data/short.srt"});
+ content[0]->only_text()->set_language(dcp::LanguageTag("de"));
+
+ CheckContentJob check(film);
+ check.run();
+ BOOST_REQUIRE (!wait_for_jobs());
+
+ make_and_verify_dcp(film, { dcp::VerificationNote::Code::INVALID_STANDARD });
+}
+
+
+BOOST_AUTO_TEST_CASE(subtitle_font_id_change_test2)
+{
+ auto film = new_test_film("subtitle_font_id_change_test2");
+ boost::filesystem::remove(film->file("metadata.xml"));
+ boost::filesystem::copy_file("test/data/subtitle_font_id_change_test2.xml", film->file("metadata.xml"));
+ {
+ Editor editor(film->file("metadata.xml"));
+ editor.replace("/usr/share/fonts/truetype/inconsolata/Inconsolata.otf", "test/data/Inconsolata-VF.ttf");
+ }
+ film->read_metadata();
+
+ auto content = film->content();
+ BOOST_REQUIRE_EQUAL(content.size(), 1U);
+ BOOST_REQUIRE_EQUAL(content[0]->text.size(), 1U);
+
+ content[0]->set_paths({"test/data/short.srt"});
+ /* Make sure the content doesn't look like it's changed, otherwise it will be re-examined
+ * which obscures the point of this test.
+ */
+ content[0]->_last_write_times[0] = boost::filesystem::last_write_time("test/data/short.srt");
+ content[0]->only_text()->set_language(dcp::LanguageTag("de"));
+
+ CheckContentJob check(film);
+ check.run();
+ BOOST_REQUIRE (!wait_for_jobs());
+
+ auto font = content[0]->text.front()->get_font("");
+ BOOST_REQUIRE(font->file());
+ BOOST_CHECK_EQUAL(*font->file(), "test/data/Inconsolata-VF.ttf");
+
+ make_and_verify_dcp(film, { dcp::VerificationNote::Code::INVALID_STANDARD });
+}
+
+
+BOOST_AUTO_TEST_CASE(subtitle_font_id_change_test3)
+{
+ Cleanup cl;
+
+ auto film = new_test_film("subtitle_font_id_change_test3", {}, &cl);
+ boost::filesystem::remove(film->file("metadata.xml"));
+ boost::filesystem::copy_file("test/data/subtitle_font_id_change_test3.xml", film->file("metadata.xml"));
+ {
+ Editor editor(film->file("metadata.xml"));
+ editor.replace("/usr/share/fonts/truetype/inconsolata/Inconsolata.otf", "test/data/Inconsolata-VF.ttf");
+ }
+ film->read_metadata();
+
+ auto content = film->content();
+ BOOST_REQUIRE_EQUAL(content.size(), 1U);
+ BOOST_REQUIRE_EQUAL(content[0]->text.size(), 1U);
+
+ content[0]->set_paths({"test/data/fonts.ass"});
+ content[0]->only_text()->set_language(dcp::LanguageTag("de"));
+
+ CheckContentJob check(film);
+ check.run();
+ BOOST_REQUIRE (!wait_for_jobs());
+
+ auto font = content[0]->text.front()->get_font("Arial Black");
+ BOOST_REQUIRE(font);
+ BOOST_REQUIRE(font->file());
+ BOOST_CHECK_EQUAL(*font->file(), "test/data/Inconsolata-VF.ttf");
+
+ font = content[0]->text.front()->get_font("Helvetica Neue");
+ BOOST_REQUIRE(font);
+ BOOST_REQUIRE(font->file());
+ BOOST_CHECK_EQUAL(*font->file(), "test/data/Inconsolata-VF.ttf");
+
+ make_and_verify_dcp(film, { dcp::VerificationNote::Code::INVALID_STANDARD });
+
+ cl.run();
+}
+
+
+BOOST_AUTO_TEST_CASE(subtitle_font_id_change_test4)
+{
+ Cleanup cl;
+
+ auto film = new_test_film("subtitle_font_id_change_test4", {}, &cl);
+ boost::filesystem::remove(film->file("metadata.xml"));
+ boost::filesystem::copy_file("test/data/subtitle_font_id_change_test4.xml", film->file("metadata.xml"));
+
+ {
+ Editor editor(film->file("metadata.xml"));
+ editor.replace("dcpomatic-test-private", TestPaths::private_data().string());
+ }
+
+ film->read_metadata();
+
+ auto content = film->content();
+ BOOST_REQUIRE_EQUAL(content.size(), 1U);
+ BOOST_REQUIRE_EQUAL(content[0]->text.size(), 1U);
+
+ CheckContentJob check(film);
+ check.run();
+ BOOST_REQUIRE(!wait_for_jobs());
+
+ make_and_verify_dcp(film, { dcp::VerificationNote::Code::INVALID_STANDARD });
+
+ cl.run();
+}
+
diff --git a/test/lib/subtitle_font_id_test.cc b/test/lib/subtitle_font_id_test.cc
new file mode 100644
index 000000000..e90429fbe
--- /dev/null
+++ b/test/lib/subtitle_font_id_test.cc
@@ -0,0 +1,349 @@
+/*
+ Copyright (C) 2022 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/check_content_job.h"
+#include "lib/content_factory.h"
+#include "lib/dcp_content.h"
+#include "lib/film.h"
+#include "lib/font.h"
+#include "lib/player.h"
+#include "lib/text_content.h"
+#include "lib/util.h"
+#include <dcp/cpl.h>
+#include <dcp/dcp.h>
+#include <dcp/reel.h>
+#include <dcp/reel_text_asset.h>
+#include <dcp/smpte_text_asset.h>
+#include "../test.h"
+#include <boost/test/unit_test.hpp>
+
+
+using std::make_shared;
+
+
+BOOST_AUTO_TEST_CASE(full_dcp_subtitle_font_id_test)
+{
+ auto dcp = make_shared<DCPContent>(TestPaths::private_data() / "JourneyToJah_TLR-1_F_EN-DE-FR_CH_51_2K_LOK_20140225_DGL_SMPTE_OV");
+ auto film = new_test_film("full_dcp_subtitle_font_id_test", { dcp });
+
+ auto content = film->content();
+ BOOST_REQUIRE_EQUAL(content.size(), 1U);
+ auto text = content[0]->only_text();
+ BOOST_REQUIRE(text);
+
+ BOOST_REQUIRE_EQUAL(text->fonts().size(), 1U);
+ auto font = text->fonts().front();
+ BOOST_CHECK_EQUAL(font->id(), "theFontId");
+ BOOST_REQUIRE(font->data());
+ BOOST_CHECK_EQUAL(font->data()->size(), 367112);
+}
+
+
+BOOST_AUTO_TEST_CASE(dcp_subtitle_font_id_test)
+{
+ auto subs = content_factory(TestPaths::private_data() / "JourneyToJah_TLR-1_F_EN-DE-FR_CH_51_2K_LOK_20140225_DGL_SMPTE_OV" / "8b48f6ae-c74b-4b80-b994-a8236bbbad74_sub.mxf");
+ auto film = new_test_film("dcp_subtitle_font_id_test", subs);
+
+ auto content = film->content();
+ BOOST_REQUIRE_EQUAL(content.size(), 1U);
+ auto text = content[0]->only_text();
+ BOOST_REQUIRE(text);
+
+ BOOST_REQUIRE_EQUAL(text->fonts().size(), 1U);
+ auto font = text->fonts().front();
+ BOOST_CHECK_EQUAL(font->id(), "theFontId");
+ BOOST_REQUIRE(font->data());
+ BOOST_CHECK_EQUAL(font->data()->size(), 367112);
+}
+
+
+BOOST_AUTO_TEST_CASE(make_dcp_with_subs_from_interop_dcp)
+{
+ auto dcp = make_shared<DCPContent>("test/data/Iopsubs_FTR-1_F_XX-XX_MOS_2K_20220710_IOP_OV");
+ auto film = new_test_film("make_dcp_with_subs_from_interop_dcp", { dcp });
+ dcp->text.front()->set_use(true);
+ make_and_verify_dcp(
+ film,
+ {
+ dcp::VerificationNote::Code::MISSING_SUBTITLE_LANGUAGE,
+ dcp::VerificationNote::Code::INVALID_SUBTITLE_FIRST_TEXT_TIME
+ }
+ );
+}
+
+
+BOOST_AUTO_TEST_CASE(make_dcp_with_subs_from_smpte_dcp)
+{
+ Cleanup cl;
+
+ auto dcp = make_shared<DCPContent>(TestPaths::private_data() / "JourneyToJah_TLR-1_F_EN-DE-FR_CH_51_2K_LOK_20140225_DGL_SMPTE_OV");
+ auto film = new_test_film("make_dcp_with_subs_from_smpte_dcp", { dcp }, &cl);
+ dcp->text.front()->set_use(true);
+ make_and_verify_dcp(film);
+
+ cl.run();
+}
+
+
+BOOST_AUTO_TEST_CASE(make_dcp_with_subs_from_mkv)
+{
+ auto subs = content_factory(TestPaths::private_data() / "clapperboard_with_subs.mkv");
+ auto film = new_test_film("make_dcp_with_subs_from_mkv", subs);
+ subs[0]->text.front()->set_use(true);
+ subs[0]->text.front()->set_language(dcp::LanguageTag("en"));
+ make_and_verify_dcp(film, { dcp::VerificationNote::Code::INVALID_PICTURE_FRAME_RATE_FOR_2K });
+}
+
+
+BOOST_AUTO_TEST_CASE(make_dcp_with_subs_without_font_tag)
+{
+ auto subs = content_factory("test/data/no_font.xml");
+ auto film = new_test_film("make_dcp_with_subs_without_font_tag", { subs });
+ subs[0]->text.front()->set_use(true);
+ subs[0]->text.front()->set_language(dcp::LanguageTag("de"));
+ make_and_verify_dcp(
+ film,
+ {
+ dcp::VerificationNote::Code::MISSING_SUBTITLE_LANGUAGE,
+ dcp::VerificationNote::Code::INVALID_SUBTITLE_FIRST_TEXT_TIME,
+ dcp::VerificationNote::Code::MISSING_CPL_METADATA
+ });
+
+ auto check_file = subtitle_file(film);
+ dcp::SMPTETextAsset check_asset(check_file);
+ BOOST_CHECK_EQUAL(check_asset.load_font_nodes().size(), 1U);
+ auto check_font_data = check_asset.font_data();
+ BOOST_CHECK_EQUAL(check_font_data.size(), 1U);
+ BOOST_CHECK(check_font_data.begin()->second == dcp::ArrayData(default_font_file()));
+}
+
+
+BOOST_AUTO_TEST_CASE(make_dcp_with_subs_in_dcp_without_font_tag)
+{
+ /* Make a DCP with some subs in */
+ auto source_subs = content_factory("test/data/short.srt");
+ auto source = new_test_film("make_dcp_with_subs_in_dcp_without_font_tag_source", { source_subs });
+ source->set_interop(true);
+ source_subs[0]->only_text()->set_language(dcp::LanguageTag("de"));
+ make_and_verify_dcp(
+ source,
+ {
+ dcp::VerificationNote::Code::MISSING_SUBTITLE_LANGUAGE,
+ dcp::VerificationNote::Code::INVALID_SUBTITLE_FIRST_TEXT_TIME,
+ dcp::VerificationNote::Code::MISSING_CPL_METADATA,
+ dcp::VerificationNote::Code::INVALID_STANDARD
+ });
+
+ /* Find the ID of the subs */
+ dcp::DCP source_dcp(source->dir(source->dcp_name()));
+ source_dcp.read();
+ BOOST_REQUIRE(!source_dcp.cpls().empty());
+ BOOST_REQUIRE(!source_dcp.cpls()[0]->reels().empty());
+ BOOST_REQUIRE(source_dcp.cpls()[0]->reels()[0]->main_subtitle());
+ auto const id = source_dcp.cpls()[0]->reels()[0]->main_subtitle()->asset()->id();
+
+ /* Graft in some bad subs with no <Font> tag */
+ auto source_subtitle_file = subtitle_file(source);
+#if BOOST_VERSION >= 107400
+ boost::filesystem::copy_file("test/data/no_font.xml", source_subtitle_file, boost::filesystem::copy_options::overwrite_existing);
+#else
+ boost::filesystem::copy_file("test/data/no_font.xml", source_subtitle_file, boost::filesystem::copy_option::overwrite_if_exists);
+#endif
+
+ /* Fix the <Id> tag */
+ {
+ Editor editor(source_subtitle_file);
+ editor.replace("4dd8ee05-5986-4c67-a6f8-bbeac62e21db", id);
+ }
+
+ /* Now make a project which imports that DCP and makes another DCP from it */
+ auto dcp_content = make_shared<DCPContent>(source->dir(source->dcp_name()));
+ auto film = new_test_film("make_dcp_with_subs_without_font_tag", { dcp_content });
+ BOOST_REQUIRE(!dcp_content->text.empty());
+ dcp_content->text.front()->set_use(true);
+ make_and_verify_dcp(
+ film,
+ {
+ dcp::VerificationNote::Code::MISSING_SUBTITLE_LANGUAGE,
+ dcp::VerificationNote::Code::INVALID_SUBTITLE_FIRST_TEXT_TIME,
+ dcp::VerificationNote::Code::MISSING_CPL_METADATA
+ });
+
+ auto check_file = subtitle_file(film);
+ dcp::SMPTETextAsset check_asset(check_file);
+ BOOST_CHECK_EQUAL(check_asset.load_font_nodes().size(), 1U);
+ auto check_font_data = check_asset.font_data();
+ BOOST_CHECK_EQUAL(check_font_data.size(), 1U);
+ BOOST_CHECK(check_font_data.begin()->second == dcp::ArrayData(default_font_file()));
+}
+
+
+BOOST_AUTO_TEST_CASE(filler_subtitle_reels_have_load_font_tags)
+{
+ auto const name = boost::unit_test::framework::current_test_case().full_name();
+
+ auto subs = content_factory("test/data/short.srt")[0];
+ auto video1 = content_factory("test/data/flat_red.png")[0];
+ auto video2 = content_factory("test/data/flat_red.png")[0];
+
+ auto film = new_test_film(name, { video1, video2, subs });
+ film->set_reel_type(ReelType::BY_VIDEO_CONTENT);
+
+ make_and_verify_dcp(
+ film,
+ {
+ dcp::VerificationNote::Code::MISSING_SUBTITLE_LANGUAGE,
+ dcp::VerificationNote::Code::INVALID_SUBTITLE_FIRST_TEXT_TIME,
+ dcp::VerificationNote::Code::INVALID_SUBTITLE_SPACING,
+ dcp::VerificationNote::Code::MISSING_CPL_METADATA
+ });
+}
+
+
+BOOST_AUTO_TEST_CASE(subtitle_with_no_font_test)
+{
+ auto const name_base = boost::unit_test::framework::current_test_case().full_name();
+
+ auto video1 = content_factory("test/data/flat_red.png")[0];
+ auto video2 = content_factory("test/data/flat_red.png")[0];
+ auto subs = content_factory("test/data/short.srt")[0];
+
+ auto bad_film = new_test_film(name_base + "_bad", { video1, video2, subs });
+ bad_film->set_reel_type(ReelType::BY_VIDEO_CONTENT);
+ video2->set_position(bad_film, video1->end(bad_film));
+ subs->set_position(bad_film, video1->end(bad_film));
+ subs->text[0]->add_font(make_shared<dcpomatic::Font>("foo", "test/data/LiberationSans-Regular.ttf"));
+
+ make_and_verify_dcp(
+ bad_film,
+ {
+ dcp::VerificationNote::Code::MISSING_SUBTITLE_LANGUAGE,
+ dcp::VerificationNote::Code::INVALID_SUBTITLE_FIRST_TEXT_TIME
+ });
+
+ /* When this test was written, this DCP would have one reel whose subtitles had <LoadFont>s
+ * but the subtitles specified no particular font. This triggers bug #2649, which this test
+ * is intended to trigger. First, make sure that the DCP has the required characteristics,
+ * to guard against a case where for some reason the DCP here is different enough that it
+ * doesn't trigger the bug.
+ */
+ dcp::DCP check(bad_film->dir(bad_film->dcp_name()));
+ check.read();
+ BOOST_REQUIRE_EQUAL(check.cpls().size(), 1U);
+ auto cpl = check.cpls()[0];
+ BOOST_REQUIRE_EQUAL(cpl->reels().size(), 2U);
+ auto check_subs_reel = cpl->reels()[0]->main_subtitle();
+ BOOST_REQUIRE(check_subs_reel);
+ auto check_subs = check_subs_reel->asset();
+ BOOST_REQUIRE(check_subs);
+
+ BOOST_CHECK_EQUAL(check_subs->font_data().size(), 1U);
+ BOOST_REQUIRE_EQUAL(check_subs->texts().size(), 1U);
+ BOOST_CHECK(!std::dynamic_pointer_cast<const dcp::TextString>(check_subs->texts()[0])->font().has_value());
+
+ auto check_film = new_test_film(name_base + "_check", { make_shared<DCPContent>(bad_film->dir(bad_film->dcp_name())) });
+ make_and_verify_dcp(check_film);
+}
+
+
+BOOST_AUTO_TEST_CASE(load_dcp_with_empty_font_id_test)
+{
+ auto dcp = std::make_shared<DCPContent>(TestPaths::private_data() / "kr_vf");
+ auto film = new_test_film("load_dcp_with_empty_font_id_test", { dcp });
+}
+
+
+BOOST_AUTO_TEST_CASE(use_first_loadfont_as_default)
+{
+ auto dcp = std::make_shared<DCPContent>("test/data/use_default_font");
+ auto film = new_test_film("use_first_loadfont_as_default", { dcp });
+ dcp->only_text()->set_use(true);
+ dcp->only_text()->set_language(dcp::LanguageTag("de"));
+ make_and_verify_dcp(
+ film,
+ { dcp::VerificationNote::Code::INVALID_SUBTITLE_FIRST_TEXT_TIME }
+ );
+
+ dcp::DCP test(film->dir(film->dcp_name()));
+ test.read();
+ BOOST_REQUIRE(!test.cpls().empty());
+ auto cpl = test.cpls()[0];
+ BOOST_REQUIRE(!cpl->reels().empty());
+ auto reel = cpl->reels()[0];
+ BOOST_REQUIRE(reel->main_subtitle()->asset());
+ auto subtitle = std::dynamic_pointer_cast<dcp::SMPTETextAsset>(reel->main_subtitle()->asset());
+ BOOST_REQUIRE_EQUAL(subtitle->font_data().size(), 1U);
+ BOOST_CHECK(subtitle->font_data().begin()->second == dcp::ArrayData("test/data/Inconsolata-VF.ttf"));
+}
+
+
+BOOST_AUTO_TEST_CASE(no_error_with_ccap_that_mentions_no_font)
+{
+ auto dcp = make_shared<DCPContent>("test/data/ccap_only");
+ auto film = new_test_film("no_error_with_ccap_that_mentions_no_font", { dcp });
+ auto player = Player(film, film->playlist(), false);
+ while (!player.pass()) {}
+}
+
+
+BOOST_AUTO_TEST_CASE(subtitle_font_ids_survive_project_save)
+{
+ std::string const name = "subtitle_font_ids_survive_project_save";
+
+ auto subs = content_factory("test/data/short.srt")[0];
+ auto film = new_test_film(name + "_film", { subs });
+ film->set_interop(false);
+ make_and_verify_dcp(
+ film,
+ {
+ dcp::VerificationNote::Code::MISSING_SUBTITLE_LANGUAGE,
+ dcp::VerificationNote::Code::INVALID_SUBTITLE_FIRST_TEXT_TIME,
+ dcp::VerificationNote::Code::MISSING_CPL_METADATA
+ });
+
+ auto dcp = std::make_shared<DCPContent>(film->dir(film->dcp_name()));
+ auto film2 = new_test_film(name + "_film2", { dcp });
+ film2->write_metadata();
+
+ auto film3 = std::make_shared<Film>(film2->dir("."));
+ film3->read_metadata();
+ BOOST_REQUIRE(!film3->content().empty());
+ auto check_dcp = std::dynamic_pointer_cast<DCPContent>(film3->content()[0]);
+ BOOST_REQUIRE(check_dcp);
+
+ check_dcp->check_font_ids();
+}
+
+
+BOOST_AUTO_TEST_CASE(cope_with_unloaded_font_id)
+{
+ /* This file has a <Font> with an ID that corresponds to no <LoadFont> */
+ auto subs = content_factory("test/data/unloaded_font.xml")[0];
+ auto film = new_test_film("cope_with_unloaded_font_id", { subs });
+
+ make_and_verify_dcp(
+ film,
+ {
+ dcp::VerificationNote::Code::MISSING_SUBTITLE_LANGUAGE,
+ dcp::VerificationNote::Code::MISSING_CPL_METADATA
+ });
+}
+
diff --git a/test/lib/subtitle_language_test.cc b/test/lib/subtitle_language_test.cc
new file mode 100644
index 000000000..612ec4af8
--- /dev/null
+++ b/test/lib/subtitle_language_test.cc
@@ -0,0 +1,121 @@
+/*
+ Copyright (C) 2020-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/>.
+
+*/
+
+
+/** @file test/subtitle_language_test.cc
+ * @brief Test that subtitle language information is correctly written to DCPs.
+ */
+
+
+#include "lib/content.h"
+#include "lib/content_factory.h"
+#include "lib/film.h"
+#include "lib/text_content.h"
+#include "../test.h"
+#include <dcp/language_tag.h>
+#include <boost/test/unit_test.hpp>
+#include <vector>
+
+
+using std::string;
+using std::vector;
+
+
+BOOST_AUTO_TEST_CASE (subtitle_language_interop_test)
+{
+ string const name = "subtitle_language_interop_test";
+ auto fr = content_factory("test/data/frames.srt");
+ auto film = new_test_film(name, fr);
+
+ fr[0]->only_text()->set_language(dcp::LanguageTag("fr"));
+ film->set_interop (true);
+ film->set_audio_channels(6);
+
+ make_and_verify_dcp (
+ film,
+ {
+ dcp::VerificationNote::Code::INVALID_STANDARD,
+ dcp::VerificationNote::Code::INVALID_SUBTITLE_SPACING,
+ dcp::VerificationNote::Code::INVALID_SUBTITLE_DURATION_BV21,
+ },
+ false,
+ /* clairmeta raises errors about subtitle spacing/duration */
+ false
+ );
+
+ check_dcp(String::compose("test/data/%1", name), String::compose("build/test/%1/%2", name, film->dcp_name()));
+}
+
+
+BOOST_AUTO_TEST_CASE (subtitle_language_smpte_test)
+{
+ string const name = "subtitle_language_smpte_test";
+ auto fr = content_factory("test/data/frames.srt");
+ auto film = new_test_film(name, fr);
+
+ fr[0]->only_text()->set_language(dcp::LanguageTag("fr"));
+ film->set_interop (false);
+
+ make_and_verify_dcp (
+ film,
+ {
+ dcp::VerificationNote::Code::MISSING_SUBTITLE_LANGUAGE,
+ dcp::VerificationNote::Code::INVALID_SUBTITLE_FIRST_TEXT_TIME,
+ dcp::VerificationNote::Code::INVALID_SUBTITLE_DURATION_BV21,
+ dcp::VerificationNote::Code::INVALID_SUBTITLE_SPACING,
+ dcp::VerificationNote::Code::MISSING_CPL_METADATA
+ });
+
+ /* This test is concerned with the subtitles, so we'll ignore any
+ * differences in sound between the DCP and the reference to avoid test
+ * failures for unrelated reasons.
+ */
+ check_dcp(String::compose("test/data/%1", name), String::compose("build/test/%1/%2", name, film->dcp_name()), true);
+}
+
+
+BOOST_AUTO_TEST_CASE(subtitle_language_in_cpl_test)
+{
+ auto subs = content_factory("test/data/frames.srt")[0];
+ auto video1 = content_factory("test/data/flat_red.png")[0];
+ auto video2 = content_factory("test/data/flat_red.png")[0];
+ auto film = new_test_film(boost::unit_test::framework::current_test_unit().full_name(), { subs, video1, video2 });
+ video2->set_position(film, dcpomatic::DCPTime::from_seconds(5));
+ film->set_reel_type(ReelType::BY_VIDEO_CONTENT);
+ subs->only_text()->set_language(dcp::LanguageTag("fr"));
+
+ make_and_verify_dcp(
+ film,
+ {
+ dcp::VerificationNote::Code::INVALID_SUBTITLE_FIRST_TEXT_TIME,
+ dcp::VerificationNote::Code::INVALID_SUBTITLE_DURATION_BV21,
+ dcp::VerificationNote::Code::INVALID_SUBTITLE_SPACING
+ });
+
+ cxml::Document cpl("CompositionPlaylist");
+ cpl.read_file(find_file(film->dir(film->dcp_name()), "cpl_"));
+
+ for (auto reel: cpl.node_child("ReelList")->node_children("Reel")) {
+ auto subtitle = reel->node_child("AssetList")->node_child("MainSubtitle");
+ BOOST_REQUIRE(subtitle);
+ BOOST_CHECK(subtitle->optional_node_child("Language"));
+ }
+}
+
diff --git a/test/lib/subtitle_metadata_test.cc b/test/lib/subtitle_metadata_test.cc
new file mode 100644
index 000000000..412320122
--- /dev/null
+++ b/test/lib/subtitle_metadata_test.cc
@@ -0,0 +1,55 @@
+/*
+ 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/>.
+
+*/
+
+/** @file test/subtitle_metadata_test.cc
+ * @brief Test that subtitle language metadata is recovered from metadata files
+ * written by versions before the subtitle language was only stored in Film.
+ */
+
+
+#include "lib/film.h"
+#include "../test.h"
+#include <boost/filesystem.hpp>
+#include <boost/test/unit_test.hpp>
+
+
+using std::make_shared;
+using std::vector;
+
+
+BOOST_AUTO_TEST_CASE (subtitle_metadata_test1)
+{
+ using namespace boost::filesystem;
+
+ auto p = test_film_dir ("subtitle_metadata_test1");
+ if (exists (p)) {
+ remove_all (p);
+ }
+ create_directory (p);
+
+ copy_file ("test/data/subtitle_metadata1.xml", p / "metadata.xml");
+ auto film = make_shared<Film>(p);
+ film->read_metadata();
+
+ auto langs = film->open_text_languages();
+ BOOST_REQUIRE (langs.first);
+ BOOST_CHECK_EQUAL(langs.first->as_string(), "de-DE");
+}
+
diff --git a/test/lib/subtitle_position_test.cc b/test/lib/subtitle_position_test.cc
new file mode 100644
index 000000000..2dda73945
--- /dev/null
+++ b/test/lib/subtitle_position_test.cc
@@ -0,0 +1,186 @@
+/*
+ Copyright (C) 2022 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/content.h"
+#include "lib/content_factory.h"
+#include "lib/film.h"
+#include "lib/make_dcp.h"
+#include "lib/text_content.h"
+#include "../test.h"
+#include <dcp/interop_text_asset.h>
+#include <dcp/language_tag.h>
+#include <dcp/smpte_text_asset.h>
+#include <boost/test/unit_test.hpp>
+#include <vector>
+
+
+using std::shared_ptr;
+using std::string;
+using std::vector;
+
+
+BOOST_AUTO_TEST_CASE(srt_correctly_placed_in_interop)
+{
+ string const name = "srt_in_interop_position_test";
+ auto fr = content_factory("test/data/short.srt");
+ auto film = new_test_film(name, fr);
+ fr[0]->only_text()->set_language(dcp::LanguageTag("de"));
+
+ film->set_interop(true);
+
+ make_and_verify_dcp (
+ film,
+ {
+ dcp::VerificationNote::Code::INVALID_STANDARD,
+ dcp::VerificationNote::Code::INVALID_SUBTITLE_SPACING,
+ dcp::VerificationNote::Code::INVALID_SUBTITLE_FIRST_TEXT_TIME
+ });
+
+ auto output = subtitle_file(film);
+
+ dcp::InteropTextAsset asset(output);
+ auto output_subs = asset.texts();
+ BOOST_REQUIRE_EQUAL(output_subs.size(), 1U);
+
+ BOOST_CHECK(output_subs[0]->v_align() == dcp::VAlign::BOTTOM);
+ BOOST_CHECK_CLOSE(output_subs[0]->v_position(), 0.172726989, 1e-3);
+}
+
+
+BOOST_AUTO_TEST_CASE(srt_correctly_placed_in_smpte)
+{
+ string const name = "srt_in_smpte_position_test";
+ auto fr = content_factory("test/data/short.srt");
+ auto film = new_test_film(name, fr);
+
+ fr[0]->text[0]->set_language(dcp::LanguageTag("en"));
+ film->set_interop(false);
+
+ make_and_verify_dcp (
+ film,
+ {
+ dcp::VerificationNote::Code::MISSING_CPL_METADATA,
+ dcp::VerificationNote::Code::INVALID_SUBTITLE_SPACING,
+ dcp::VerificationNote::Code::INVALID_SUBTITLE_FIRST_TEXT_TIME
+ });
+
+ auto output = subtitle_file(film);
+
+ dcp::SMPTETextAsset asset(output);
+ auto output_subs = asset.texts();
+ BOOST_REQUIRE_EQUAL(output_subs.size(), 1U);
+
+ BOOST_CHECK(output_subs[0]->v_align() == dcp::VAlign::BOTTOM);
+ BOOST_CHECK_CLOSE(output_subs[0]->v_position(), 0.172726989, 1e-3);
+}
+
+
+/** Make a DCP from some DCP subtitles and check the vertical alignment */
+static
+void
+vpos_test(dcp::VAlign reference, float position, dcp::SubtitleStandard from, dcp::Standard to)
+{
+ string standard;
+ switch (from) {
+ case dcp::SubtitleStandard::SMPTE_2007:
+ case dcp::SubtitleStandard::SMPTE_2010:
+ standard = "smpte_2010";
+ break;
+ case dcp::SubtitleStandard::INTEROP:
+ standard = "interop";
+ break;
+ case dcp::SubtitleStandard::SMPTE_2014:
+ standard = "smpte_2014";
+ break;
+ }
+
+ auto name = String::compose("vpos_test_%1_%2", standard, valign_to_string(reference));
+ auto in = content_factory(String::compose("test/data/%1.xml", name));
+ auto film = new_test_film(name, in);
+
+ film->set_interop(to == dcp::Standard::INTEROP);
+
+ film->write_metadata();
+ make_dcp(film, TranscodeJob::ChangedBehaviour::IGNORE);
+ BOOST_REQUIRE(!wait_for_jobs());
+
+ auto out = subtitle_file(film);
+ vector<shared_ptr<const dcp::Text>> subtitles;
+ if (to == dcp::Standard::INTEROP) {
+ dcp::InteropTextAsset asset(out);
+ subtitles = asset.texts();
+ } else {
+ dcp::SMPTETextAsset asset(out);
+ subtitles = asset.texts();
+ }
+
+ BOOST_REQUIRE_EQUAL(subtitles.size(), 1U);
+
+ BOOST_CHECK(subtitles[0]->v_align() == reference);
+ BOOST_CHECK_CLOSE(subtitles[0]->v_position(), position, 2);
+}
+
+
+BOOST_AUTO_TEST_CASE(subtitles_correctly_placed_with_all_references)
+{
+ constexpr auto baseline_to_bottom = 0.00925926;
+ constexpr auto height = 0.0462963;
+
+ /* Interop source */
+ auto from = dcp::SubtitleStandard::INTEROP;
+
+ // -> Interop
+ vpos_test(dcp::VAlign::TOP, 0.2, from, dcp::Standard::INTEROP);
+ vpos_test(dcp::VAlign::CENTER, 0.11, from, dcp::Standard::INTEROP);
+ vpos_test(dcp::VAlign::BOTTOM, 0.08, from, dcp::Standard::INTEROP);
+
+ // -> SMPTE (2014)
+ vpos_test(dcp::VAlign::TOP, 0.2, from, dcp::Standard::SMPTE);
+ vpos_test(dcp::VAlign::CENTER, 0.11, from, dcp::Standard::SMPTE);
+ vpos_test(dcp::VAlign::BOTTOM, 0.08, from, dcp::Standard::SMPTE);
+
+ /* SMPTE 2010 source */
+ from = dcp::SubtitleStandard::SMPTE_2010;
+
+ // -> Interop
+ vpos_test(dcp::VAlign::TOP, 0.1 + height - baseline_to_bottom, from, dcp::Standard::INTEROP);
+ vpos_test(dcp::VAlign::CENTER, 0.15 + (height / 2) - baseline_to_bottom, from, dcp::Standard::INTEROP);
+ vpos_test(dcp::VAlign::BOTTOM, 0.10 + baseline_to_bottom, from, dcp::Standard::INTEROP);
+
+ // -> SMPTE (2014)
+ vpos_test(dcp::VAlign::TOP, 0.1 + height - baseline_to_bottom, from, dcp::Standard::SMPTE);
+ vpos_test(dcp::VAlign::CENTER, 0.15 + (height / 2) - baseline_to_bottom, from, dcp::Standard::SMPTE);
+ vpos_test(dcp::VAlign::BOTTOM, 0.10 + baseline_to_bottom, from, dcp::Standard::SMPTE);
+
+ /* SMPTE 2014 source */
+ from = dcp::SubtitleStandard::SMPTE_2014;
+
+ // -> Interop
+ vpos_test(dcp::VAlign::TOP, 0.2, from, dcp::Standard::INTEROP);
+ vpos_test(dcp::VAlign::CENTER, 0.11, from, dcp::Standard::INTEROP);
+ vpos_test(dcp::VAlign::BOTTOM, 0.08, from, dcp::Standard::INTEROP);
+
+ // -> SMPTE (2014)
+ vpos_test(dcp::VAlign::TOP, 0.2, from, dcp::Standard::SMPTE);
+ vpos_test(dcp::VAlign::CENTER, 0.11, from, dcp::Standard::SMPTE);
+ vpos_test(dcp::VAlign::BOTTOM, 0.08, from, dcp::Standard::SMPTE);
+}
+
diff --git a/test/lib/subtitle_reel_number_test.cc b/test/lib/subtitle_reel_number_test.cc
new file mode 100644
index 000000000..7badb217e
--- /dev/null
+++ b/test/lib/subtitle_reel_number_test.cc
@@ -0,0 +1,75 @@
+/*
+ Copyright (C) 2017-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 "lib/string_text_file_content.h"
+#include "lib/film.h"
+#include "lib/ratio.h"
+#include "lib/text_content.h"
+#include "lib/dcp_content_type.h"
+#include "../test.h"
+#include <dcp/cpl.h>
+#include <dcp/dcp.h>
+#include <dcp/reel.h>
+#include <dcp/interop_text_asset.h>
+#include <dcp/reel_text_asset.h>
+#include <fmt/format.h>
+#include <boost/test/unit_test.hpp>
+
+
+using std::dynamic_pointer_cast;
+using std::make_shared;
+using std::string;
+
+
+/* Check that ReelNumber is setup correctly when making multi-reel subtitled DCPs */
+BOOST_AUTO_TEST_CASE (subtitle_reel_number_test)
+{
+ Cleanup cl;
+
+ auto content = make_shared<StringTextFileContent>("test/data/subrip5.srt");
+ auto film = new_test_film("subtitle_reel_number_test", { content }, &cl);
+ content->only_text()->set_use (true);
+ content->only_text()->set_burn (false);
+ content->only_text()->set_language(dcp::LanguageTag("de"));
+ film->set_reel_type (ReelType::BY_LENGTH);
+ film->set_interop (true);
+ film->set_reel_length (1024 * 1024 * 512);
+ film->set_video_bit_rate(VideoEncoding::JPEG2000, 100000000);
+ make_and_verify_dcp (film, {dcp::VerificationNote::Code::INVALID_STANDARD});
+
+ dcp::DCP dcp ("build/test/subtitle_reel_number_test/" + film->dcp_name());
+ dcp.read ();
+ BOOST_REQUIRE_EQUAL (dcp.cpls().size(), 1U);
+ auto cpl = dcp.cpls()[0];
+ BOOST_REQUIRE_EQUAL (cpl->reels().size(), 6U);
+
+ int n = 1;
+ for (auto i: cpl->reels()) {
+ if (i->main_subtitle()) {
+ auto ass = dynamic_pointer_cast<dcp::InteropTextAsset>(i->main_subtitle()->asset());
+ BOOST_REQUIRE (ass);
+ BOOST_CHECK_EQUAL (ass->reel_number(), fmt::to_string(n));
+ ++n;
+ }
+ }
+
+ cl.run();
+}
diff --git a/test/lib/subtitle_reel_test.cc b/test/lib/subtitle_reel_test.cc
new file mode 100644
index 000000000..b0b185392
--- /dev/null
+++ b/test/lib/subtitle_reel_test.cc
@@ -0,0 +1,261 @@
+/*
+ Copyright (C) 2019-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/content_factory.h"
+#include "lib/dcp_subtitle_content.h"
+#include "lib/film.h"
+#include "lib/image_content.h"
+#include "lib/text_content.h"
+#include "lib/video_content.h"
+#include "../test.h"
+#include <dcp/dcp.h>
+#include <dcp/cpl.h>
+#include <dcp/interop_text_asset.h>
+#include <dcp/reel.h>
+#include <dcp/reel_text_asset.h>
+#include <boost/test/unit_test.hpp>
+
+
+using std::list;
+using std::make_shared;
+using std::string;
+using boost::optional;
+
+
+/* Check that timings are done correctly for multi-reel DCPs with PNG subs */
+BOOST_AUTO_TEST_CASE (subtitle_reel_test)
+{
+ auto film = new_test_film("subtitle_reel_test");
+ film->set_interop (true);
+ auto red_a = make_shared<ImageContent>("test/data/flat_red.png");
+ auto red_b = make_shared<ImageContent>("test/data/flat_red.png");
+ auto sub_a = make_shared<DCPSubtitleContent>("test/data/png_subs/subs.xml");
+ auto sub_b = make_shared<DCPSubtitleContent>("test/data/png_subs/subs.xml");
+
+ film->examine_and_add_content (red_a);
+ film->examine_and_add_content (red_b);
+ film->examine_and_add_content (sub_a);
+ film->examine_and_add_content (sub_b);
+
+ BOOST_REQUIRE (!wait_for_jobs());
+
+ red_a->set_position (film, dcpomatic::DCPTime());
+ red_a->video->set_length (240);
+ sub_a->set_position (film, dcpomatic::DCPTime());
+ sub_a->only_text()->set_language(dcp::LanguageTag("de"));
+ red_b->set_position (film, dcpomatic::DCPTime::from_seconds(10));
+ red_b->video->set_length (240);
+ sub_b->set_position (film, dcpomatic::DCPTime::from_seconds(10));
+ sub_b->only_text()->set_language(dcp::LanguageTag("de"));
+
+ film->set_reel_type (ReelType::BY_VIDEO_CONTENT);
+
+ make_and_verify_dcp (film, {dcp::VerificationNote::Code::INVALID_STANDARD});
+
+ dcp::DCP dcp ("build/test/subtitle_reel_test/" + film->dcp_name());
+ dcp.read ();
+ BOOST_REQUIRE_EQUAL (dcp.cpls().size(), 1U);
+ auto cpl = dcp.cpls().front();
+
+ auto reels = cpl->reels ();
+ BOOST_REQUIRE_EQUAL (reels.size(), 2U);
+ auto i = reels.begin ();
+ BOOST_REQUIRE ((*i)->main_subtitle());
+ BOOST_REQUIRE ((*i)->main_subtitle()->asset());
+ auto A = std::dynamic_pointer_cast<dcp::InteropTextAsset>((*i)->main_subtitle()->asset());
+ BOOST_REQUIRE (A);
+ ++i;
+ BOOST_REQUIRE ((*i)->main_subtitle());
+ BOOST_REQUIRE ((*i)->main_subtitle()->asset());
+ auto B = std::dynamic_pointer_cast<dcp::InteropTextAsset>((*i)->main_subtitle()->asset());
+ BOOST_REQUIRE (B);
+
+ BOOST_REQUIRE_EQUAL(A->texts().size(), 1U);
+ BOOST_REQUIRE_EQUAL(B->texts().size(), 1U);
+
+ /* These times should be the same as they are should be offset from the start of the reel */
+ BOOST_CHECK(A->texts().front()->in() == B->texts().front()->in());
+}
+
+
+
+/** Check that with a SMPTE DCP if we have subtitles in one reel, all reels have a
+ * SubtitleAsset (even if it's empty); SMPTE Bv2.1 section 8.3.1.
+ */
+BOOST_AUTO_TEST_CASE (subtitle_in_all_reels_test)
+{
+ auto film = new_test_film("subtitle_in_all_reels_test");
+ film->set_interop (false);
+ film->set_sequence (false);
+ film->set_reel_type (ReelType::BY_VIDEO_CONTENT);
+ for (int i = 0; i < 3; ++i) {
+ auto video = content_factory("test/data/flat_red.png")[0];
+ film->examine_and_add_content (video);
+ BOOST_REQUIRE (!wait_for_jobs());
+ video->video->set_length (15 * 24);
+ video->set_position (film, dcpomatic::DCPTime::from_seconds(15 * i));
+ }
+ auto subs = content_factory("test/data/15s.srt")[0];
+ film->examine_and_add_content (subs);
+ BOOST_REQUIRE (!wait_for_jobs());
+ make_and_verify_dcp (
+ film,
+ {
+ dcp::VerificationNote::Code::MISSING_SUBTITLE_LANGUAGE,
+ dcp::VerificationNote::Code::INVALID_SUBTITLE_FIRST_TEXT_TIME,
+ dcp::VerificationNote::Code::INVALID_SUBTITLE_SPACING
+ });
+
+ dcp::DCP dcp ("build/test/subtitle_in_all_reels_test/" + film->dcp_name());
+ dcp.read ();
+ BOOST_REQUIRE_EQUAL (dcp.cpls().size(), 1U);
+ auto cpl = dcp.cpls()[0];
+ BOOST_REQUIRE_EQUAL (cpl->reels().size(), 3U);
+
+ for (auto i: cpl->reels()) {
+ BOOST_CHECK (i->main_subtitle());
+ }
+}
+
+
+/** Check that with a SMPTE DCP if we have closed captions in one reel, all reels have a
+ * ClosedCaptionAssets for the same set of tracks (even if they are empty); SMPTE Bv2.1 section 8.3.1.
+ */
+BOOST_AUTO_TEST_CASE (closed_captions_in_all_reels_test)
+{
+ auto film = new_test_film("closed_captions_in_all_reels_test");
+ film->set_interop (false);
+ film->set_sequence (false);
+ film->set_reel_type (ReelType::BY_VIDEO_CONTENT);
+
+ for (int i = 0; i < 3; ++i) {
+ auto video = content_factory("test/data/flat_red.png")[0];
+ film->examine_and_add_content (video);
+ BOOST_REQUIRE (!wait_for_jobs());
+ video->video->set_length (15 * 24);
+ video->set_position (film, dcpomatic::DCPTime::from_seconds(15 * i));
+ }
+
+ auto ccap1 = content_factory("test/data/15s.srt")[0];
+ film->examine_and_add_content (ccap1);
+ BOOST_REQUIRE (!wait_for_jobs());
+ ccap1->text.front()->set_type (TextType::CLOSED_CAPTION);
+ ccap1->text.front()->set_dcp_track (DCPTextTrack("Test", dcp::LanguageTag("de-DE")));
+
+ auto ccap2 = content_factory("test/data/15s.srt")[0];
+ film->examine_and_add_content (ccap2);
+ BOOST_REQUIRE (!wait_for_jobs());
+ ccap2->text.front()->set_type (TextType::CLOSED_CAPTION);
+ ccap2->text.front()->set_dcp_track (DCPTextTrack("Other", dcp::LanguageTag("en-GB")));
+
+ make_and_verify_dcp (
+ film,
+ {
+ dcp::VerificationNote::Code::INVALID_SUBTITLE_FIRST_TEXT_TIME,
+ dcp::VerificationNote::Code::INVALID_SUBTITLE_SPACING
+ },
+ true,
+ /* ClairMeta gives an error with multiple ClosedCaption assets */
+ false
+ );
+
+ dcp::DCP dcp ("build/test/closed_captions_in_all_reels_test/" + film->dcp_name());
+ dcp.read ();
+ BOOST_REQUIRE_EQUAL (dcp.cpls().size(), 1U);
+ auto cpl = dcp.cpls().front();
+ BOOST_REQUIRE_EQUAL (cpl->reels().size(), 3U);
+
+ for (auto i: cpl->reels()) {
+ BOOST_REQUIRE_EQUAL (i->closed_captions().size(), 2U);
+ auto first = i->closed_captions().front()->language();
+ auto second = i->closed_captions().back()->language();
+ BOOST_REQUIRE (first);
+ BOOST_REQUIRE (second);
+ BOOST_CHECK (
+ (*first == "en-GB" && *second == "de-DE") ||
+ (*first == "de-DE" && *second == "en-GB")
+ );
+ }
+}
+
+
+BOOST_AUTO_TEST_CASE (subtitles_split_at_reel_boundaries)
+{
+ auto film = new_test_film("subtitles_split_at_reel_boundaries");
+ film->set_interop (true);
+
+ film->set_sequence (false);
+ film->set_reel_type (ReelType::BY_VIDEO_CONTENT);
+
+ for (int i = 0; i < 3; ++i) {
+ auto video = content_factory("test/data/flat_red.png")[0];
+ film->examine_and_add_content (video);
+ BOOST_REQUIRE (!wait_for_jobs());
+ video->video->set_length (15 * 24);
+ video->set_position (film, dcpomatic::DCPTime::from_seconds(15 * i));
+ }
+
+ auto subtitle = content_factory("test/data/45s.srt")[0];
+ film->examine_and_add_content (subtitle);
+ BOOST_REQUIRE (!wait_for_jobs());
+ subtitle->only_text()->set_language(dcp::LanguageTag("de"));
+
+ make_and_verify_dcp (film, { dcp::VerificationNote::Code::INVALID_STANDARD });
+
+ dcp::DCP dcp (film->dir(film->dcp_name()));
+ dcp.read();
+ BOOST_REQUIRE_EQUAL (dcp.cpls().size(), 1U);
+ auto cpl = dcp.cpls()[0];
+ BOOST_REQUIRE_EQUAL (cpl->reels().size(), 3U);
+
+ for (auto i: cpl->reels()) {
+ auto reel_sub = i->main_subtitle();
+ BOOST_REQUIRE (reel_sub);
+ auto sub = reel_sub->asset();
+ BOOST_REQUIRE (sub);
+ BOOST_CHECK_EQUAL(sub->texts().size(), 1U);
+ }
+}
+
+
+BOOST_AUTO_TEST_CASE(bad_subtitle_not_created_at_reel_boundaries)
+{
+ boost::filesystem::path const srt = "build/test/bad_subtitle_not_created_at_reel_boundaries.srt";
+ dcp::write_string_to_file("1\n00:00:10,000 -> 00:00:20,000\nHello world", srt);
+ auto content = content_factory(srt)[0];
+
+ auto film = new_test_film("bad_subtitle_not_created_at_reel_boundaries", { content });
+ film->set_reel_type(ReelType::CUSTOM);
+ content->text[0]->set_language(dcp::LanguageTag("de"));
+ /* This is 1 frame after the start of the subtitle */
+ film->set_custom_reel_boundaries({dcpomatic::DCPTime::from_frames(241, 24)});
+
+ /* This is a tricky situation and the way DoM deals with it gives two Bv2.1
+ * warnings, but these are "should" not "shall" so I think it's OK.
+ */
+ make_and_verify_dcp(
+ film,
+ {
+ dcp::VerificationNote::Code::MISSING_CPL_METADATA,
+ dcp::VerificationNote::Code::INVALID_SUBTITLE_DURATION_BV21,
+ dcp::VerificationNote::Code::INVALID_SUBTITLE_SPACING,
+ });
+}
+
diff --git a/test/lib/subtitle_timing_test.cc b/test/lib/subtitle_timing_test.cc
new file mode 100644
index 000000000..bd2a878bc
--- /dev/null
+++ b/test/lib/subtitle_timing_test.cc
@@ -0,0 +1,146 @@
+/*
+ Copyright (C) 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 "lib/content.h"
+#include "lib/content_factory.h"
+#include "lib/content_text.h"
+#include "lib/dcpomatic_time.h"
+#include "lib/film.h"
+#include "lib/ffmpeg_content.h"
+#include "lib/ffmpeg_decoder.h"
+#include "lib/text_content.h"
+#include "lib/text_decoder.h"
+#include "lib/video_content.h"
+#include "../test.h"
+#include <dcp/cpl.h>
+#include <dcp/dcp.h>
+#include <dcp/reel.h>
+#include <dcp/reel_text_asset.h>
+#include <boost/test/unit_test.hpp>
+#include <iostream>
+
+
+using std::dynamic_pointer_cast;
+
+
+BOOST_AUTO_TEST_CASE (test_subtitle_timing_with_frame_rate_change)
+{
+ Cleanup cl;
+
+ using boost::filesystem::path;
+
+ constexpr auto content_frame_rate = 29.976f;
+ const std::string name = "test_subtitle_timing_with_frame_rate_change";
+
+ auto picture = content_factory("test/data/flat_red.png")[0];
+ auto sub = content_factory("test/data/hour.srt")[0];
+ sub->text.front()->set_language(dcp::LanguageTag("en"));
+
+ auto film = new_test_film(name, { picture, sub }, &cl);
+ film->set_video_bit_rate(VideoEncoding::JPEG2000, 10000000);
+ picture->set_video_frame_rate(film, content_frame_rate);
+ auto const dcp_frame_rate = film->video_frame_rate();
+
+ make_and_verify_dcp (film, {dcp::VerificationNote::Code::INVALID_SUBTITLE_FIRST_TEXT_TIME, dcp::VerificationNote::Code::INVALID_PICTURE_FRAME_RATE_FOR_2K });
+
+ dcp::DCP dcp(path("build/test") / name / film->dcp_name());
+ dcp.read();
+ BOOST_REQUIRE_EQUAL(dcp.cpls().size(), 1U);
+ auto cpl = dcp.cpls()[0];
+ BOOST_REQUIRE_EQUAL(cpl->reels().size(), 1U);
+ auto reel = cpl->reels()[0];
+ BOOST_REQUIRE(reel->main_subtitle());
+ BOOST_REQUIRE(reel->main_subtitle()->asset());
+
+ auto subs = reel->main_subtitle()->asset()->texts();
+ int index = 0;
+ for (auto i: subs) {
+ auto error = std::abs(i->in().as_seconds() - (index * content_frame_rate / dcp_frame_rate));
+ BOOST_CHECK (error < (1.0f / dcp_frame_rate));
+ ++index;
+ }
+
+ cl.run();
+}
+
+
+BOOST_AUTO_TEST_CASE(dvb_subtitles_replace_the_last)
+{
+ /* roh.mkv contains subtitles that come out of FFmpeg with incorrect stop times (30s
+ * after the start, which seems to be some kind of DVB "standard" timeout).
+ * Between actual subtitles it contains blanks that are apparently supposed to clear
+ * the previous subtitle. Make sure that happens.
+ */
+ auto content = content_factory(TestPaths::private_data() / "roh.mkv");
+ BOOST_REQUIRE(!content.empty());
+ auto film = new_test_film("dvb_subtitles_replace_the_last", { content[0] });
+
+ FFmpegDecoder decoder(film, dynamic_pointer_cast<FFmpegContent>(content[0]), false);
+ BOOST_REQUIRE(!decoder.text.empty());
+
+ struct Event {
+ std::string type;
+ dcpomatic::ContentTime time;
+
+ bool operator==(Event const& other) const {
+ return type == other.type && time == other.time;
+ }
+ };
+
+ std::vector<Event> events;
+
+ auto start = [&events](ContentBitmapText text) {
+ events.push_back({"start", text.from()});
+ };
+
+ auto stop = [&events](dcpomatic::ContentTime time) {
+ if (!events.empty() && events.back().type == "stop") {
+ /* We'll get a bad (too-late) stop time, then the correct one
+ * when the "clearing" subtitle arrives.
+ */
+ events.pop_back();
+ }
+ events.push_back({"stop", time});
+ };
+
+ decoder.text.front()->BitmapStart.connect(start);
+ decoder.text.front()->Stop.connect(stop);
+
+ while (!decoder.pass()) {}
+
+ using dcpomatic::ContentTime;
+
+ std::vector<Event> correct = {
+ { "start", ContentTime(439872) }, // 4.582000s actual subtitle #1
+ { "stop", ContentTime(998400) }, // 10.400000s stop caused by incoming blank
+ { "start", ContentTime(998400) }, // 10.400000s blank
+ { "stop", ContentTime(1141248) }, // 11.888000s stop caused by incoming subtitle #2
+ { "start", ContentTime(1141248) }, // 11.888000s subtitle #2
+ { "stop", ContentTime(1455936) }, // 15.166000s ...
+ { "start", ContentTime(1455936) }, // 15.166000s
+ { "stop", ContentTime(1626816) }, // 16.946000s
+ { "start", ContentTime(1626816) }, // 16.946000s
+ };
+
+ BOOST_REQUIRE(events.size() > correct.size());
+ BOOST_CHECK(std::vector<Event>(events.begin(), events.begin() + correct.size()) == correct);
+}
+
diff --git a/test/lib/subtitle_trim_test.cc b/test/lib/subtitle_trim_test.cc
new file mode 100644
index 000000000..587300426
--- /dev/null
+++ b/test/lib/subtitle_trim_test.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 "lib/film.h"
+#include "lib/dcp_subtitle_content.h"
+#include "../test.h"
+#include <boost/test/unit_test.hpp>
+
+
+using std::make_shared;
+
+
+/** Check for no crash when trimming DCP subtitles (#1275) */
+BOOST_AUTO_TEST_CASE (subtitle_trim_test1)
+{
+ auto content = make_shared<DCPSubtitleContent>("test/data/dcp_sub5.xml");
+ auto film = new_test_film("subtitle_trim_test1", {content});
+
+ content->set_trim_end (dcpomatic::ContentTime::from_seconds(2));
+ film->write_metadata ();
+
+ make_and_verify_dcp (
+ film,
+ {
+ dcp::VerificationNote::Code::MISSING_SUBTITLE_LANGUAGE,
+ dcp::VerificationNote::Code::INVALID_SUBTITLE_FIRST_TEXT_TIME,
+ dcp::VerificationNote::Code::MISSING_CPL_METADATA
+ });
+}
diff --git a/test/lib/template_test.cc b/test/lib/template_test.cc
new file mode 100644
index 000000000..3d0dbff15
--- /dev/null
+++ b/test/lib/template_test.cc
@@ -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/>.
+
+*/
+
+
+#include "lib/audio_content.h"
+#include "lib/config.h"
+#include "lib/content_factory.h"
+#include "lib/film.h"
+#include "../test.h"
+#include <boost/test/unit_test.hpp>
+
+
+using std::string;
+
+
+/* Bug #2491 */
+BOOST_AUTO_TEST_CASE(template_wrong_channel_counts)
+{
+ ConfigRestorer cr("test/data");
+
+ auto film = new_test_film("template_wrong_channel_counts", {});
+ film->use_template(string("Bug"));
+
+ auto mono = content_factory("test/data/C.wav").front();
+ film->examine_and_add_content(mono);
+ BOOST_REQUIRE(!wait_for_jobs());
+
+ BOOST_REQUIRE_EQUAL(mono->audio->streams().size(), 1U);
+ BOOST_CHECK_EQUAL(mono->audio->streams()[0]->channels(), 1);
+}
+
diff --git a/test/lib/text_decoder_test.cc b/test/lib/text_decoder_test.cc
new file mode 100644
index 000000000..d6cbd4ce1
--- /dev/null
+++ b/test/lib/text_decoder_test.cc
@@ -0,0 +1,32 @@
+/*
+ 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/text_decoder.h"
+#include <boost/test/unit_test.hpp>
+
+
+BOOST_AUTO_TEST_CASE(strip_invalid_characters_for_xml_test)
+{
+ BOOST_CHECK_EQUAL(TextDecoder::remove_invalid_characters_for_xml("hello world"), "hello world");
+ BOOST_CHECK_EQUAL(TextDecoder::remove_invalid_characters_for_xml("hello\x0cworld"), "helloworld");
+ BOOST_CHECK_EQUAL(TextDecoder::remove_invalid_characters_for_xml("𒀖hello\x02worl𒁝d"), "𒀖helloworl𒁝d");
+ BOOST_CHECK_EQUAL(TextDecoder::remove_invalid_characters_for_xml("😀œ´®†¥¨ˆø\x09π¬˚∆\x1a˙©ƒ∂ßåΩ≈ç√∫\x02˜µ≤ユーザーコードa"), "😀œ´®†¥¨ˆø\x09π¬˚∆˙©ƒ∂ßåΩ≈ç√∫˜µ≤ユーザーコードa");
+}
diff --git a/test/lib/text_entry_point_test.cc b/test/lib/text_entry_point_test.cc
new file mode 100644
index 000000000..9da6cb36e
--- /dev/null
+++ b/test/lib/text_entry_point_test.cc
@@ -0,0 +1,70 @@
+/*
+ Copyright (C) 2024 Carl Hetherington <cth@carlh.net>
+
+ This file is part of DCP-o-matic.
+
+ DCP-o-matic is free software; you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation; either version 2 of the License, or
+ (at your option) any later version.
+
+ DCP-o-matic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with DCP-o-matic. If not, see <http://www.gnu.org/licenses/>.
+
+*/
+
+
+#include "lib/dcp_content.h"
+#include "lib/film.h"
+#include "../test.h"
+#include <dcp/cpl.h>
+#include <dcp/dcp.h>
+#include <dcp/reel.h>
+#include <dcp/reel_smpte_text_asset.h>
+#include <dcp/smpte_text_asset.h>
+#include <boost/test/unit_test.hpp>
+
+
+using std::make_shared;
+using std::string;
+
+
+BOOST_AUTO_TEST_CASE(test_text_entry_point)
+{
+ auto const path = boost::filesystem::path("build/test/test_text_entry_point");
+ boost::filesystem::remove_all(path);
+ boost::filesystem::create_directories(path);
+
+ /* Make a "bad" DCP with a non-zero text entry point */
+ dcp::DCP bad_dcp(path / "dcp");
+ auto sub = make_shared<dcp::SMPTETextAsset>();
+ sub->write(path / "dcp" / "subs.mxf");
+ auto reel_sub = make_shared<dcp::ReelSMPTETextAsset>(dcp::TextType::OPEN_SUBTITLE, sub, dcp::Fraction{24, 1}, 42, 6);
+ auto reel = make_shared<dcp::Reel>();
+ reel->add(reel_sub);
+
+ auto cpl = make_shared<dcp::CPL>("foo", dcp::ContentKind::FEATURE, dcp::Standard::SMPTE);
+ bad_dcp.add(cpl);
+ cpl->add(reel);
+
+ bad_dcp.write_xml();
+
+ /* Make a film and add the bad DCP, so that the examiner spots the problem */
+ auto dcp_content = make_shared<DCPContent>(path / "dcp");
+ auto film = new_test_film("test_text_entry_point/film", { dcp_content });
+ film->write_metadata();
+
+ /* Reload the film to check that the examiner's output is saved and recovered */
+ auto film2 = make_shared<Film>(path / "film");
+ film2->read_metadata();
+
+ string why_not;
+ BOOST_CHECK(!dcp_content->can_reference_text(film2, TextType::OPEN_SUBTITLE, why_not));
+ BOOST_CHECK_EQUAL(why_not, "one of its subtitle reels has a non-zero entry point so it must be re-written.");
+}
+
diff --git a/test/lib/threed_test.cc b/test/lib/threed_test.cc
new file mode 100644
index 000000000..83bfc46b9
--- /dev/null
+++ b/test/lib/threed_test.cc
@@ -0,0 +1,355 @@
+/*
+ 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/>.
+
+*/
+
+
+/** @file test/threed_test.cc
+ * @brief Create some 3D DCPs (without comparing the results to anything).
+ * @ingroup completedcp
+ */
+
+
+#include "lib/butler.h"
+#include "lib/config.h"
+#include "lib/content_factory.h"
+#include "lib/cross.h"
+#include "lib/dcp_content.h"
+#include "lib/dcp_content_type.h"
+#include "lib/ffmpeg_content.h"
+#include "lib/film.h"
+#include "lib/image.h"
+#include "lib/job.h"
+#include "lib/job_manager.h"
+#include "lib/make_dcp.h"
+#include "lib/player.h"
+#include "lib/ratio.h"
+#include "lib/util.h"
+#include "lib/video_content.h"
+#include "../test.h"
+#include <dcp/mono_j2k_picture_asset.h>
+#include <dcp/stereo_j2k_picture_asset.h>
+#include <boost/test/unit_test.hpp>
+#include <iostream>
+
+
+using std::cout;
+using std::make_shared;
+using std::shared_ptr;
+
+
+/** Basic sanity check of THREE_D_LEFT_RIGHT */
+BOOST_AUTO_TEST_CASE (threed_test1)
+{
+ auto c = make_shared<FFmpegContent>("test/data/test.mp4");
+ auto film = new_test_film("threed_test1", { c });
+
+ c->video->set_frame_type (VideoFrameType::THREE_D_LEFT_RIGHT);
+
+ film->set_container (Ratio::from_id ("185"));
+ film->set_dcp_content_type (DCPContentType::from_isdcf_name ("TST"));
+ film->set_three_d (true);
+ make_and_verify_dcp (film);
+}
+
+
+/** Basic sanity check of THREE_D_ALTERNATE; at the moment this is just to make sure
+ * that such a transcode completes without error.
+ */
+BOOST_AUTO_TEST_CASE (threed_test2)
+{
+ auto c = make_shared<FFmpegContent>("test/data/test.mp4");
+ auto film = new_test_film("threed_test2", { c });
+ c->video->set_frame_type (VideoFrameType::THREE_D_ALTERNATE);
+
+ film->set_three_d (true);
+ make_and_verify_dcp (film);
+}
+
+
+/** Basic sanity check of THREE_D_LEFT and THREE_D_RIGHT; at the moment this is just to make sure
+ * that such a transcode completes without error.
+ */
+BOOST_AUTO_TEST_CASE (threed_test3)
+{
+ auto film = new_test_film("threed_test3");
+ auto L = make_shared<FFmpegContent>("test/data/test.mp4");
+ film->examine_and_add_content (L);
+ auto R = make_shared<FFmpegContent>("test/data/test.mp4");
+ film->examine_and_add_content (R);
+ BOOST_REQUIRE (!wait_for_jobs());
+
+ L->video->set_frame_type (VideoFrameType::THREE_D_LEFT);
+ R->video->set_frame_type (VideoFrameType::THREE_D_RIGHT);
+
+ film->set_three_d (true);
+ make_and_verify_dcp (film);
+}
+
+
+BOOST_AUTO_TEST_CASE (threed_test4)
+{
+ ConfigRestorer cr;
+
+ auto film = new_test_film("threed_test4");
+ auto L = make_shared<FFmpegContent>(TestPaths::private_data() / "LEFT_TEST_DCP3D4K.mov");
+ film->examine_and_add_content (L);
+ auto R = make_shared<FFmpegContent>(TestPaths::private_data() / "RIGHT_TEST_DCP3D4K.mov");
+ film->examine_and_add_content (R);
+ BOOST_REQUIRE (!wait_for_jobs());
+
+ L->video->set_frame_type (VideoFrameType::THREE_D_LEFT);
+ R->video->set_frame_type (VideoFrameType::THREE_D_RIGHT);
+ /* There doesn't seem much point in encoding the whole input, especially as we're only
+ * checking for errors during the encode and not the result. Also decoding these files
+ * (4K HQ Prores) is very slow.
+ */
+ L->set_trim_end (dcpomatic::ContentTime::from_seconds(22));
+ R->set_trim_end (dcpomatic::ContentTime::from_seconds(22));
+
+ film->set_three_d (true);
+ make_and_verify_dcp(
+ film,
+ {dcp::VerificationNote::Code::INVALID_PICTURE_ASSET_RESOLUTION_FOR_3D},
+ true,
+ /* XXX: Clairmeta error about invalid edit rate 24 for 4K 3D */
+ false
+ );
+}
+
+
+BOOST_AUTO_TEST_CASE (threed_test5)
+{
+ auto film = new_test_film("threed_test5");
+ auto L = make_shared<FFmpegContent>(TestPaths::private_data() / "boon_telly.mkv");
+ film->examine_and_add_content (L);
+ auto R = make_shared<FFmpegContent>(TestPaths::private_data() / "boon_telly.mkv");
+ film->examine_and_add_content (R);
+ BOOST_REQUIRE (!wait_for_jobs());
+
+ L->video->set_frame_type (VideoFrameType::THREE_D_LEFT);
+ R->video->set_frame_type (VideoFrameType::THREE_D_RIGHT);
+ /* There doesn't seem much point in encoding the whole input, especially as we're only
+ * checking for errors during the encode and not the result.
+ */
+ L->set_trim_end (dcpomatic::ContentTime::from_seconds(3 * 60 + 20));
+ R->set_trim_end (dcpomatic::ContentTime::from_seconds(3 * 60 + 20));
+
+ film->set_three_d (true);
+ make_and_verify_dcp (film, {dcp::VerificationNote::Code::INVALID_PICTURE_FRAME_RATE_FOR_2K});
+}
+
+
+BOOST_AUTO_TEST_CASE (threed_test6)
+{
+ auto film = new_test_film("threed_test6");
+ auto L = make_shared<FFmpegContent>("test/data/3dL.mp4");
+ film->examine_and_add_content (L);
+ auto R = make_shared<FFmpegContent>("test/data/3dR.mp4");
+ film->examine_and_add_content (R);
+ film->set_audio_channels(16);
+ BOOST_REQUIRE (!wait_for_jobs());
+
+ L->video->set_frame_type (VideoFrameType::THREE_D_LEFT);
+ R->video->set_frame_type (VideoFrameType::THREE_D_RIGHT);
+
+ film->set_three_d (true);
+ make_and_verify_dcp (film);
+ check_dcp ("test/data/threed_test6", film->dir(film->dcp_name()));
+}
+
+
+/** Check 2D content set as being 3D; this should give an informative error */
+BOOST_AUTO_TEST_CASE (threed_test7)
+{
+ using boost::filesystem::path;
+
+ auto film = new_test_film("threed_test7");
+ path const content_path = "test/data/flat_red.png";
+ auto c = content_factory(content_path)[0];
+ film->examine_and_add_content (c);
+ BOOST_REQUIRE (!wait_for_jobs());
+
+ c->video->set_frame_type (VideoFrameType::THREE_D);
+ c->video->set_length (24);
+
+ film->set_three_d (true);
+ make_dcp (film, TranscodeJob::ChangedBehaviour::IGNORE);
+ film->write_metadata ();
+
+ auto jm = JobManager::instance ();
+ while (jm->work_to_do ()) {
+ while (signal_manager->ui_idle()) {}
+ dcpomatic_sleep_seconds (1);
+ }
+
+ while (signal_manager->ui_idle ()) {}
+
+ BOOST_REQUIRE (jm->errors());
+ shared_ptr<Job> failed;
+ for (auto i: jm->_jobs) {
+ if (i->finished_in_error()) {
+ BOOST_REQUIRE (!failed);
+ failed = i;
+ }
+ }
+ BOOST_REQUIRE (failed);
+ BOOST_CHECK_EQUAL (failed->error_summary(), String::compose("The content file %1 is set as 3D but does not appear to contain 3D images. Please set it to 2D. You can still make a 3D DCP from this content by ticking the 3D option in the DCP video tab.", boost::filesystem::canonical(content_path).string()));
+
+ while (signal_manager->ui_idle ()) {}
+
+ JobManager::drop ();
+}
+
+
+/** Trigger a -114 error by trying to make a 3D DCP out of two files with slightly
+ * different lengths.
+ */
+BOOST_AUTO_TEST_CASE (threed_test_separate_files_slightly_different_lengths)
+{
+ auto film = new_test_film("threed_test3");
+ auto L = make_shared<FFmpegContent>("test/data/test.mp4");
+ film->examine_and_add_content (L);
+ auto R = make_shared<FFmpegContent>("test/data/test.mp4");
+ film->examine_and_add_content (R);
+ BOOST_REQUIRE (!wait_for_jobs());
+
+ L->video->set_frame_type (VideoFrameType::THREE_D_LEFT);
+ R->video->set_frame_type (VideoFrameType::THREE_D_RIGHT);
+ R->set_trim_end (dcpomatic::ContentTime::from_frames(1, 24));
+
+ film->set_three_d (true);
+ make_and_verify_dcp (film);
+}
+
+
+/** Trigger a -114 error by trying to make a 3D DCP out of two files with very
+ * different lengths.
+ */
+BOOST_AUTO_TEST_CASE (threed_test_separate_files_very_different_lengths)
+{
+ auto film = new_test_film("threed_test3");
+ auto L = make_shared<FFmpegContent>("test/data/test.mp4");
+ film->examine_and_add_content (L);
+ auto R = make_shared<FFmpegContent>("test/data/test.mp4");
+ film->examine_and_add_content (R);
+ BOOST_REQUIRE (!wait_for_jobs());
+
+ L->video->set_frame_type (VideoFrameType::THREE_D_LEFT);
+ R->video->set_frame_type (VideoFrameType::THREE_D_RIGHT);
+ R->set_trim_end (dcpomatic::ContentTime::from_seconds(1.5));
+
+ film->set_three_d (true);
+ make_and_verify_dcp (film);
+}
+
+
+BOOST_AUTO_TEST_CASE (threed_test_butler_overfill)
+{
+ auto film = new_test_film("threed_test_butler_overfill");
+ auto A = make_shared<FFmpegContent>(TestPaths::private_data() / "arrietty_JP-EN.mkv");
+ film->examine_and_add_content(A);
+ auto B = make_shared<FFmpegContent>(TestPaths::private_data() / "arrietty_JP-EN.mkv");
+ film->examine_and_add_content(B);
+ BOOST_REQUIRE (!wait_for_jobs());
+
+ Player player(film, Image::Alignment::COMPACT, false);
+ int const audio_channels = 2;
+ auto butler = std::make_shared<Butler>(
+ film, player, AudioMapping(), audio_channels, boost::bind(PlayerVideo::force, AV_PIX_FMT_RGB24), VideoRange::FULL, Image::Alignment::PADDED, true, false, Butler::Audio::ENABLED
+ );
+
+ int const audio_frames = 1920;
+ std::vector<float> audio(audio_frames * audio_channels);
+
+ B->video->set_frame_type(VideoFrameType::THREE_D_RIGHT);
+ B->set_position(film, dcpomatic::DCPTime());
+
+ butler->seek(dcpomatic::DCPTime(), true);
+ Butler::Error error;
+ for (auto i = 0; i < 960; ++i) {
+ butler->get_video(Butler::Behaviour::BLOCKING, &error);
+ butler->get_audio(Butler::Behaviour::BLOCKING, audio.data(), audio_frames);
+ }
+ BOOST_REQUIRE (error.code == Butler::Error::Code::NONE);
+}
+
+
+/** Check that creating a 2D DCP from a 3D DCP passes the J2K data unaltered */
+BOOST_AUTO_TEST_CASE(threed_passthrough_test, * boost::unit_test::depends_on("threed_test6"))
+{
+ using namespace boost::filesystem;
+
+ /* Find the DCP in threed_test6 */
+ boost::optional<path> input_dcp;
+ for (auto i: directory_iterator("build/test/threed_test6")) {
+ if (is_directory(i.path()) && boost::algorithm::starts_with(i.path().filename().string(), "Dcp")) {
+ input_dcp = i.path();
+ }
+ }
+
+ BOOST_REQUIRE(input_dcp);
+
+ auto content = make_shared<DCPContent>(*input_dcp);
+ auto film = new_test_film("threed_passthrough_test", { content });
+ film->set_three_d(false);
+
+ make_and_verify_dcp(film);
+
+ std::vector<directory_entry> matches;
+ std::copy_if(recursive_directory_iterator(*input_dcp), recursive_directory_iterator(), std::back_inserter(matches), [](directory_entry const& entry) {
+ return boost::algorithm::starts_with(entry.path().filename().string(), "j2c");
+ });
+
+ BOOST_REQUIRE_EQUAL(matches.size(), 1U);
+
+ auto stereo = dcp::StereoJ2KPictureAsset(matches[0]);
+ auto stereo_reader = stereo.start_read();
+
+ auto mono = dcp::MonoJ2KPictureAsset(dcp_file(film, "j2c"));
+ auto mono_reader = mono.start_read();
+
+ BOOST_REQUIRE_EQUAL(stereo.intrinsic_duration(), mono.intrinsic_duration());
+
+ for (auto i = 0; i < stereo.intrinsic_duration(); ++i) {
+ auto stereo_frame = stereo_reader->get_frame(i);
+ auto mono_frame = mono_reader->get_frame(i);
+ BOOST_REQUIRE(stereo_frame->left()->size() == mono_frame->size());
+ BOOST_REQUIRE_EQUAL(memcmp(stereo_frame->left()->data(), mono_frame->data(), mono_frame->size()), 0);
+ }
+}
+
+/* #2476 was a writer error when 3D picture padding is needed */
+BOOST_AUTO_TEST_CASE(threed_test_when_padding_needed)
+{
+ auto left = content_factory("test/data/flat_red.png").front();
+ auto right = content_factory("test/data/flat_red.png").front();
+ auto sound = content_factory("test/data/sine_440.wav").front();
+ auto film = new_test_film("threed_test_when_padding_needed", { left, right, sound });
+
+ left->video->set_frame_type(VideoFrameType::THREE_D_LEFT);
+ left->set_position(film, dcpomatic::DCPTime());
+ left->video->set_length(23);
+ right->video->set_frame_type(VideoFrameType::THREE_D_RIGHT);
+ right->set_position(film, dcpomatic::DCPTime());
+ right->video->set_frame_type(VideoFrameType::THREE_D_RIGHT);
+ right->video->set_length(23);
+ film->set_three_d(true);
+
+ make_and_verify_dcp(film);
+}
diff --git a/test/lib/time_calculation_test.cc b/test/lib/time_calculation_test.cc
new file mode 100644
index 000000000..a136fe848
--- /dev/null
+++ b/test/lib/time_calculation_test.cc
@@ -0,0 +1,818 @@
+/*
+ Copyright (C) 2015-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/>.
+
+*/
+
+
+/** @file test/time_calculation_test.cc
+ * @brief Test calculation of timings when frame rates change.
+ * @ingroup feature
+ */
+
+
+#include "lib/film.h"
+#include "lib/ffmpeg_content.h"
+#include "lib/video_content.h"
+#include "lib/player.h"
+#include "lib/audio_content.h"
+#include "../test.h"
+#include <boost/test/unit_test.hpp>
+
+
+using std::list;
+using std::make_shared;
+using std::string;
+using namespace dcpomatic;
+
+
+static string const xml = "<Content>"
+ "<Type>FFmpeg</Type>"
+ "<BurnSubtitles>0</BurnSubtitles>"
+ "<BitsPerPixel>8</BitsPerPixel>"
+ "<Path>test/data/red_24.mp4</Path>"
+ "<Digest>2760e03c7251480f7f02c01a907792673784335</Digest>"
+ "<Position>0</Position>"
+ "<TrimStart>0</TrimStart>"
+ "<TrimEnd>0</TrimEnd>"
+ "<VideoLength>1353600</VideoLength>"
+ "<VideoWidth>1280</VideoWidth>"
+ "<VideoHeight>720</VideoHeight>"
+ "<VideoFrameRate>25</VideoFrameRate>"
+ "<VideoFrameType>2d</VideoFrameType>"
+ "<LeftCrop>0</LeftCrop>"
+ "<RightCrop>0</RightCrop>"
+ "<TopCrop>0</TopCrop>"
+ "<BottomCrop>0</BottomCrop>"
+ "<Scale>"
+ "<Ratio>178</Ratio>"
+ "</Scale>"
+ "<ColourConversion>"
+ "<InputTransferFunction>"
+ "<Type>ModifiedGamma</Type>"
+ "<Power>2.222222222222222</Power>"
+ "<Threshold>0.081</Threshold>"
+ "<A>0.099</A>"
+ "<B>4.5</B>"
+ "</InputTransferFunction>"
+ "<RedX>0.64</RedX>"
+ "<RedY>0.33</RedY>"
+ "<GreenX>0.3</GreenX>"
+ "<GreenY>0.6</GreenY>"
+ "<BlueX>0.15</BlueX>"
+ "<BlueY>0.06</BlueY>"
+ "<WhiteX>0.3127</WhiteX>"
+ "<WhiteY>0.329</WhiteY>"
+ "<OutputGamma>2.6</OutputGamma>"
+ "</ColourConversion>"
+ "<FadeIn>0</FadeIn>"
+ "<FadeOut>0</FadeOut>"
+ "<AudioGain>0</AudioGain>"
+ "<AudioDelay>0</AudioDelay>"
+ "<UseSubtitles>0</UseSubtitles>"
+ "<SubtitleXOffset>0</SubtitleXOffset>"
+ "<SubtitleYOffset>0</SubtitleYOffset>"
+ "<SubtitleXScale>1</SubtitleXScale>"
+ "<SubtitleYScale>1</SubtitleYScale>"
+ "<SubtitleLanguage></SubtitleLanguage>"
+ "<AudioStream>"
+ "<Selected>1</Selected>"
+ "<Name>und; 2 channels</Name>"
+ "<Id>1</Id>"
+ "<FrameRate>44100</FrameRate>"
+ "<Length>44100</Length>"
+ "<Channels>2</Channels>"
+ "<FirstAudio>0</FirstAudio>"
+ "<Mapping>"
+ "<InputChannels>2</InputChannels>"
+ "<OutputChannels>12</OutputChannels>"
+ "<Gain Input=\"0\" Output=\"0\">1</Gain>"
+ "<Gain Input=\"0\" Output=\"1\">0</Gain>"
+ "<Gain Input=\"0\" Output=\"2\">0</Gain>"
+ "<Gain Input=\"0\" Output=\"3\">0</Gain>"
+ "<Gain Input=\"0\" Output=\"4\">0</Gain>"
+ "<Gain Input=\"0\" Output=\"5\">0</Gain>"
+ "<Gain Input=\"0\" Output=\"6\">0</Gain>"
+ "<Gain Input=\"0\" Output=\"7\">0</Gain>"
+ "<Gain Input=\"0\" Output=\"8\">0</Gain>"
+ "<Gain Input=\"0\" Output=\"9\">0</Gain>"
+ "<Gain Input=\"0\" Output=\"10\">0</Gain>"
+ "<Gain Input=\"0\" Output=\"11\">0</Gain>"
+ "<Gain Input=\"1\" Output=\"0\">0</Gain>"
+ "<Gain Input=\"1\" Output=\"1\">1</Gain>"
+ "<Gain Input=\"1\" Output=\"2\">0</Gain>"
+ "<Gain Input=\"1\" Output=\"3\">0</Gain>"
+ "<Gain Input=\"1\" Output=\"4\">0</Gain>"
+ "<Gain Input=\"1\" Output=\"5\">0</Gain>"
+ "<Gain Input=\"1\" Output=\"6\">0</Gain>"
+ "<Gain Input=\"1\" Output=\"7\">0</Gain>"
+ "<Gain Input=\"1\" Output=\"8\">0</Gain>"
+ "<Gain Input=\"1\" Output=\"9\">0</Gain>"
+ "<Gain Input=\"1\" Output=\"10\">0</Gain>"
+ "<Gain Input=\"1\" Output=\"11\">0</Gain>"
+ "</Mapping>"
+ "</AudioStream>"
+ "<FirstVideo>0</FirstVideo>"
+ "</Content>";
+
+
+BOOST_AUTO_TEST_CASE (ffmpeg_time_calculation_test)
+{
+ auto film = new_test_film("ffmpeg_time_calculation_test");
+
+ auto doc = make_shared<cxml::Document>();
+ doc->read_string (xml);
+
+ list<string> notes;
+ auto content = std::make_shared<FFmpegContent>(doc, boost::none, 38, notes);
+
+ /* 25fps content, 25fps DCP */
+ film->set_video_frame_rate (25);
+ BOOST_CHECK_EQUAL (content->full_length(film).get(), DCPTime::from_seconds(content->video->length() / 25.0).get());
+ /* 25fps content, 24fps DCP; length should be increased */
+ film->set_video_frame_rate (24);
+ BOOST_CHECK_EQUAL (content->full_length(film).get(), DCPTime::from_seconds(content->video->length() / 24.0).get());
+ /* 25fps content, 30fps DCP; length should be decreased */
+ film->set_video_frame_rate (30);
+ BOOST_CHECK_EQUAL (content->full_length(film).get(), DCPTime::from_seconds(content->video->length() / 30.0).get());
+ /* 25fps content, 50fps DCP; length should be the same */
+ film->set_video_frame_rate (50);
+ BOOST_CHECK_EQUAL (content->full_length(film).get(), DCPTime::from_seconds(content->video->length() / 25.0).get());
+ /* 25fps content, 60fps DCP; length should be decreased */
+ film->set_video_frame_rate (60);
+ BOOST_CHECK_EQUAL (content->full_length(film).get(), DCPTime::from_seconds(content->video->length() * (50.0 / 60) / 25.0).get());
+
+ /* Make the content audio-only */
+ content->video.reset ();
+
+ /* 24fps content, 24fps DCP */
+ film->set_video_frame_rate (24);
+ content->set_video_frame_rate(film, 24);
+ BOOST_CHECK_EQUAL (content->full_length(film).get(), DCPTime::from_seconds(1).get());
+ /* 25fps content, 25fps DCP */
+ film->set_video_frame_rate (25);
+ content->set_video_frame_rate(film, 25);
+ BOOST_CHECK_EQUAL (content->full_length(film).get(), DCPTime::from_seconds(1).get());
+ /* 25fps content, 24fps DCP; length should be increased */
+ film->set_video_frame_rate (24);
+ BOOST_CHECK_SMALL (labs (content->full_length(film).get() - DCPTime::from_seconds(25.0 / 24).get()), 2L);
+ /* 25fps content, 30fps DCP; length should be decreased */
+ film->set_video_frame_rate (30);
+ BOOST_CHECK_EQUAL (content->full_length(film).get(), DCPTime::from_seconds(25.0 / 30).get());
+ /* 25fps content, 50fps DCP; length should be the same */
+ film->set_video_frame_rate (50);
+ BOOST_CHECK_EQUAL (content->full_length(film).get(), DCPTime::from_seconds(1).get());
+ /* 25fps content, 60fps DCP; length should be decreased */
+ film->set_video_frame_rate (60);
+ BOOST_CHECK_EQUAL (content->full_length(film).get(), DCPTime::from_seconds(50.0 / 60).get());
+
+}
+
+
+/** Test Player::dcp_to_content_video */
+BOOST_AUTO_TEST_CASE (player_time_calculation_test1)
+{
+ auto film = new_test_film("player_time_calculation_test1");
+
+ auto doc = make_shared<cxml::Document>();
+ doc->read_string (xml);
+
+ list<string> notes;
+ auto content = std::make_shared<FFmpegContent>(doc, boost::none, 38, notes);
+ film->set_sequence (false);
+ film->add_content (content);
+
+ auto player = make_shared<Player>(film, Image::Alignment::COMPACT, false);
+
+ /* Position 0, no trim, content rate = DCP rate */
+ content->set_position (film, DCPTime());
+ content->set_trim_start(film, ContentTime());
+ content->set_video_frame_rate(film, 24);
+ film->set_video_frame_rate (24);
+ player->setup_pieces ();
+ BOOST_REQUIRE_EQUAL (player->_pieces.size(), 1U);
+ auto piece = player->_pieces.front();
+ BOOST_CHECK_EQUAL (player->dcp_to_content_video (piece, DCPTime ()), 0);
+ BOOST_CHECK_EQUAL (player->dcp_to_content_video (piece, DCPTime::from_seconds (0.5)), 12);
+ BOOST_CHECK_EQUAL (player->dcp_to_content_video (piece, DCPTime::from_seconds (3.0)), 72);
+
+ /* Position 3s, no trim, content rate = DCP rate */
+ content->set_position (film, DCPTime::from_seconds(3));
+ content->set_trim_start(film, ContentTime());
+ content->set_video_frame_rate(film, 24);
+ film->set_video_frame_rate (24);
+ player->setup_pieces ();
+ BOOST_REQUIRE_EQUAL (player->_pieces.size(), 1U);
+ piece = player->_pieces.front ();
+ BOOST_CHECK_EQUAL (player->dcp_to_content_video (piece, DCPTime ()), 0);
+ BOOST_CHECK_EQUAL (player->dcp_to_content_video (piece, DCPTime::from_seconds (0.50)), 0);
+ BOOST_CHECK_EQUAL (player->dcp_to_content_video (piece, DCPTime::from_seconds (3.00)), 0);
+ BOOST_CHECK_EQUAL (player->dcp_to_content_video (piece, DCPTime::from_seconds (4.50)), 36);
+ BOOST_CHECK_EQUAL (player->dcp_to_content_video (piece, DCPTime::from_seconds (9.75)), 162);
+
+ /* Position 3s, 1.5s trim, content rate = DCP rate */
+ content->set_position (film, DCPTime::from_seconds(3));
+ content->set_trim_start(film, ContentTime::from_seconds(1.5));
+ content->set_video_frame_rate(film, 24);
+ film->set_video_frame_rate (24);
+ player->setup_pieces ();
+ BOOST_REQUIRE_EQUAL (player->_pieces.size(), 1U);
+ piece = player->_pieces.front ();
+ BOOST_CHECK_EQUAL (player->dcp_to_content_video (piece, DCPTime ()), 0);
+ BOOST_CHECK_EQUAL (player->dcp_to_content_video (piece, DCPTime::from_seconds (0.50)), 0);
+ BOOST_CHECK_EQUAL (player->dcp_to_content_video (piece, DCPTime::from_seconds (3.00)), 36);
+ BOOST_CHECK_EQUAL (player->dcp_to_content_video (piece, DCPTime::from_seconds (4.50)), 72);
+ BOOST_CHECK_EQUAL (player->dcp_to_content_video (piece, DCPTime::from_seconds (9.75)), 198);
+
+ /* Position 0, no trim, content rate 24, DCP rate 25.
+ Now, for example, a DCPTime position of 3s means 3s at 25fps. Since we run the video
+ fast (at 25fps) in this case, this means 75 frames of content video will be used.
+ */
+ content->set_position (film, DCPTime());
+ content->set_trim_start(film, ContentTime());
+ content->set_video_frame_rate(film, 24);
+ film->set_video_frame_rate (25);
+ player->setup_pieces ();
+ BOOST_REQUIRE_EQUAL (player->_pieces.size(), 1U);
+ piece = player->_pieces.front ();
+ BOOST_CHECK_EQUAL (player->dcp_to_content_video (piece, DCPTime ()), 0);
+ BOOST_CHECK_EQUAL (player->dcp_to_content_video (piece, DCPTime::from_seconds (0.6)), 15);
+ BOOST_CHECK_EQUAL (player->dcp_to_content_video (piece, DCPTime::from_seconds (3.0)), 75);
+
+ /* Position 3s, no trim, content rate 24, DCP rate 25 */
+ content->set_position (film, DCPTime::from_seconds(3));
+ content->set_trim_start(film, ContentTime());
+ content->set_video_frame_rate(film, 24);
+ film->set_video_frame_rate (25);
+ player->setup_pieces ();
+ BOOST_REQUIRE_EQUAL (player->_pieces.size(), 1U);
+ piece = player->_pieces.front ();
+ BOOST_CHECK_EQUAL (player->dcp_to_content_video (piece, DCPTime ()), 0);
+ BOOST_CHECK_EQUAL (player->dcp_to_content_video (piece, DCPTime::from_seconds (0.60)), 0);
+ BOOST_CHECK_EQUAL (player->dcp_to_content_video (piece, DCPTime::from_seconds (3.00)), 0);
+ BOOST_CHECK_EQUAL (player->dcp_to_content_video (piece, DCPTime::from_seconds (4.60)), 40);
+ BOOST_CHECK_EQUAL (player->dcp_to_content_video (piece, DCPTime::from_seconds (9.75)), 168);
+
+ /* Position 3s, 1.6s trim, content rate 24, DCP rate 25. Here the trim is in ContentTime,
+ so it's 1.6s at 24fps. Note that trims are rounded to the nearest video frame, so
+ some of these results are not quite what you'd perhaps expect.
+ */
+ content->set_position (film, DCPTime::from_seconds(3));
+ content->set_trim_start(film, ContentTime::from_seconds(1.6));
+ content->set_video_frame_rate(film, 24);
+ film->set_video_frame_rate (25);
+ player->setup_pieces ();
+ BOOST_REQUIRE_EQUAL (player->_pieces.size(), 1U);
+ piece = player->_pieces.front ();
+ BOOST_CHECK_EQUAL (player->dcp_to_content_video (piece, DCPTime ()), 0);
+ BOOST_CHECK_EQUAL (player->dcp_to_content_video (piece, DCPTime::from_seconds (0.60)), 0);
+ BOOST_CHECK_EQUAL (player->dcp_to_content_video (piece, DCPTime::from_seconds (3.00)), 38);
+ BOOST_CHECK_EQUAL (player->dcp_to_content_video (piece, DCPTime::from_seconds (4.60)), 78);
+ BOOST_CHECK_EQUAL (player->dcp_to_content_video (piece, DCPTime::from_seconds (9.75)), 206);
+
+ /* Position 0, no trim, content rate 24, DCP rate 48
+ Now, for example, a DCPTime position of 3s means 3s at 48fps. Since we run the video
+ with repeated frames in this case, 3 * 24 frames of content video will
+ be used to make 3 * 48 frames of DCP video. The results should be the same as the
+ content rate = DCP rate case.
+ */
+ content->set_position (film, DCPTime());
+ content->set_trim_start(film, ContentTime());
+ content->set_video_frame_rate(film, 24);
+ film->set_video_frame_rate (48);
+ player->setup_pieces ();
+ BOOST_REQUIRE_EQUAL (player->_pieces.size(), 1U);
+ piece = player->_pieces.front ();
+ BOOST_CHECK_EQUAL (player->dcp_to_content_video (piece, DCPTime ()), 0);
+ BOOST_CHECK_EQUAL (player->dcp_to_content_video (piece, DCPTime::from_seconds (0.5)), 12);
+ BOOST_CHECK_EQUAL (player->dcp_to_content_video (piece, DCPTime::from_seconds (3.0)), 72);
+
+ /* Position 3s, no trim, content rate 24, DCP rate 48 */
+ content->set_position (film, DCPTime::from_seconds(3));
+ content->set_trim_start(film, ContentTime());
+ content->set_video_frame_rate(film, 24);
+ film->set_video_frame_rate (48);
+ player->setup_pieces ();
+ BOOST_REQUIRE_EQUAL (player->_pieces.size(), 1U);
+ piece = player->_pieces.front ();
+ BOOST_CHECK_EQUAL (player->dcp_to_content_video (piece, DCPTime ()), 0);
+ BOOST_CHECK_EQUAL (player->dcp_to_content_video (piece, DCPTime::from_seconds (0.50)), 0);
+ BOOST_CHECK_EQUAL (player->dcp_to_content_video (piece, DCPTime::from_seconds (3.00)), 0);
+ BOOST_CHECK_EQUAL (player->dcp_to_content_video (piece, DCPTime::from_seconds (4.50)), 36);
+ BOOST_CHECK_EQUAL (player->dcp_to_content_video (piece, DCPTime::from_seconds (9.75)), 162);
+
+ /* Position 3s, 1.5s trim, content rate 24, DCP rate 48 */
+ content->set_position (film, DCPTime::from_seconds(3));
+ content->set_trim_start(film, ContentTime::from_seconds(1.5));
+ content->set_video_frame_rate(film, 24);
+ film->set_video_frame_rate (48);
+ player->setup_pieces ();
+ BOOST_REQUIRE_EQUAL (player->_pieces.size(), 1U);
+ piece = player->_pieces.front ();
+ BOOST_CHECK_EQUAL (player->dcp_to_content_video (piece, DCPTime ()), 0);
+ BOOST_CHECK_EQUAL (player->dcp_to_content_video (piece, DCPTime::from_seconds (0.50)), 0);
+ BOOST_CHECK_EQUAL (player->dcp_to_content_video (piece, DCPTime::from_seconds (3.00)), 36);
+ BOOST_CHECK_EQUAL (player->dcp_to_content_video (piece, DCPTime::from_seconds (4.50)), 72);
+ BOOST_CHECK_EQUAL (player->dcp_to_content_video (piece, DCPTime::from_seconds (9.75)), 198);
+
+ /* Position 0, no trim, content rate 48, DCP rate 24
+ Now, for example, a DCPTime position of 3s means 3s at 24fps. Since we run the video
+ with skipped frames in this case, 3 * 48 frames of content video will
+ be used to make 3 * 24 frames of DCP video.
+ */
+ content->set_position (film, DCPTime());
+ content->set_trim_start(film, ContentTime());
+ content->set_video_frame_rate(film, 48);
+ film->set_video_frame_rate (24);
+ player->setup_pieces ();
+ BOOST_REQUIRE_EQUAL (player->_pieces.size(), 1U);
+ piece = player->_pieces.front ();
+ BOOST_CHECK_EQUAL (player->dcp_to_content_video (piece, DCPTime ()), 0);
+ BOOST_CHECK_EQUAL (player->dcp_to_content_video (piece, DCPTime::from_seconds (0.5)), 24);
+ BOOST_CHECK_EQUAL (player->dcp_to_content_video (piece, DCPTime::from_seconds (3.0)), 144);
+
+ /* Position 3s, no trim, content rate 24, DCP rate 48 */
+ content->set_position (film, DCPTime::from_seconds(3));
+ content->set_trim_start(film, ContentTime());
+ content->set_video_frame_rate(film, 48);
+ film->set_video_frame_rate (24);
+ player->setup_pieces ();
+ BOOST_REQUIRE_EQUAL (player->_pieces.size(), 1U);
+ piece = player->_pieces.front ();
+ BOOST_CHECK_EQUAL (player->dcp_to_content_video (piece, DCPTime ()), 0);
+ BOOST_CHECK_EQUAL (player->dcp_to_content_video (piece, DCPTime::from_seconds (0.50)), 0);
+ BOOST_CHECK_EQUAL (player->dcp_to_content_video (piece, DCPTime::from_seconds (3.00)), 0);
+ BOOST_CHECK_EQUAL (player->dcp_to_content_video (piece, DCPTime::from_seconds (4.50)), 72);
+ BOOST_CHECK_EQUAL (player->dcp_to_content_video (piece, DCPTime::from_seconds (9.75)), 324);
+
+ /* Position 3s, 1.5s trim, content rate 24, DCP rate 48 */
+ content->set_position (film, DCPTime::from_seconds(3));
+ content->set_trim_start(film, ContentTime::from_seconds(1.5));
+ content->set_video_frame_rate(film, 48);
+ film->set_video_frame_rate (24);
+ player->setup_pieces ();
+ BOOST_REQUIRE_EQUAL (player->_pieces.size(), 1U);
+ piece = player->_pieces.front ();
+ BOOST_CHECK_EQUAL (player->dcp_to_content_video (piece, DCPTime ()), 0);
+ BOOST_CHECK_EQUAL (player->dcp_to_content_video (piece, DCPTime::from_seconds (0.50)), 0);
+ BOOST_CHECK_EQUAL (player->dcp_to_content_video (piece, DCPTime::from_seconds (3.00)), 72);
+ BOOST_CHECK_EQUAL (player->dcp_to_content_video (piece, DCPTime::from_seconds (4.50)), 144);
+ BOOST_CHECK_EQUAL (player->dcp_to_content_video (piece, DCPTime::from_seconds (9.75)), 396);
+
+ /* Position 0s, no trim, content rate 29.9978733, DCP rate 30 */
+ content->set_position (film, DCPTime::from_seconds(0));
+ content->set_trim_start(film, ContentTime::from_seconds (0));
+ content->set_video_frame_rate(film, 29.9978733);
+ film->set_video_frame_rate (30);
+ player->setup_pieces ();
+ BOOST_REQUIRE_EQUAL (player->_pieces.size(), 1U);
+ piece = player->_pieces.front ();
+ BOOST_CHECK_EQUAL (player->dcp_to_content_video (piece, DCPTime ()), 0);
+ BOOST_CHECK_EQUAL (player->dcp_to_content_video (piece, DCPTime (3200)), 1);
+ BOOST_CHECK_EQUAL (player->dcp_to_content_video (piece, DCPTime (6400)), 2);
+ BOOST_CHECK_EQUAL (player->dcp_to_content_video (piece, DCPTime (9600)), 3);
+ BOOST_CHECK_EQUAL (player->dcp_to_content_video (piece, DCPTime (12800)), 4);
+
+}
+
+/** Test Player::content_video_to_dcp */
+BOOST_AUTO_TEST_CASE (player_time_calculation_test2)
+{
+ auto film = new_test_film("player_time_calculation_test2");
+
+ auto doc = make_shared<cxml::Document>();
+ doc->read_string (xml);
+
+ list<string> notes;
+ auto content = std::make_shared<FFmpegContent>(doc, boost::none, 38, notes);
+ film->set_sequence (false);
+ film->add_content (content);
+
+ auto player = make_shared<Player>(film, Image::Alignment::COMPACT, false);
+
+ /* Position 0, no trim, content rate = DCP rate */
+ content->set_position (film, DCPTime());
+ content->set_trim_start(film, ContentTime());
+ content->set_video_frame_rate(film, 24);
+ film->set_video_frame_rate (24);
+ player->setup_pieces ();
+ BOOST_REQUIRE_EQUAL (player->_pieces.size(), 1U);
+ auto piece = player->_pieces.front ();
+ BOOST_CHECK_EQUAL (player->content_video_to_dcp (piece, 0).get(), 0);
+ BOOST_CHECK_EQUAL (player->content_video_to_dcp (piece, 12).get(), DCPTime::from_seconds(0.5).get());
+ BOOST_CHECK_EQUAL (player->content_video_to_dcp (piece, 72).get(), DCPTime::from_seconds(3.0).get());
+
+ /* Position 3s, no trim, content rate = DCP rate */
+ content->set_position (film, DCPTime::from_seconds(3));
+ content->set_trim_start(film, ContentTime());
+ content->set_video_frame_rate(film, 24);
+ film->set_video_frame_rate (24);
+ player->setup_pieces ();
+ BOOST_REQUIRE_EQUAL (player->_pieces.size(), 1U);
+ piece = player->_pieces.front ();
+ BOOST_CHECK_EQUAL (player->content_video_to_dcp(piece, 0).get(), DCPTime::from_seconds(3.00).get());
+ BOOST_CHECK_EQUAL (player->content_video_to_dcp(piece, 36).get(), DCPTime::from_seconds(4.50).get());
+ BOOST_CHECK_EQUAL (player->content_video_to_dcp(piece, 162).get(), DCPTime::from_seconds(9.75).get());
+
+ /* Position 3s, 1.5s trim, content rate = DCP rate */
+ content->set_position (film, DCPTime::from_seconds(3));
+ content->set_trim_start(film, ContentTime::from_seconds(1.5));
+ content->set_video_frame_rate(film, 24);
+ film->set_video_frame_rate (24);
+ player->setup_pieces ();
+ BOOST_REQUIRE_EQUAL (player->_pieces.size(), 1U);
+ piece = player->_pieces.front ();
+ BOOST_CHECK_EQUAL (player->content_video_to_dcp(piece, 0).get(), DCPTime::from_seconds(1.50).get());
+ BOOST_CHECK_EQUAL (player->content_video_to_dcp(piece, 36).get(), DCPTime::from_seconds(3.00).get());
+ BOOST_CHECK_EQUAL (player->content_video_to_dcp(piece, 72).get(), DCPTime::from_seconds(4.50).get());
+ BOOST_CHECK_EQUAL (player->content_video_to_dcp(piece, 198).get(), DCPTime::from_seconds(9.75).get());
+
+ /* Position 0, no trim, content rate 24, DCP rate 25.
+ Now, for example, a DCPTime position of 3s means 3s at 25fps. Since we run the video
+ fast (at 25fps) in this case, this means 75 frames of content video will be used.
+ */
+ content->set_position (film, DCPTime());
+ content->set_trim_start(film, ContentTime());
+ content->set_video_frame_rate(film, 24);
+ film->set_video_frame_rate (25);
+ player->setup_pieces ();
+ BOOST_REQUIRE_EQUAL (player->_pieces.size(), 1U);
+ piece = player->_pieces.front ();
+ BOOST_CHECK_EQUAL (player->content_video_to_dcp(piece, 0).get(), 0);
+ BOOST_CHECK_EQUAL (player->content_video_to_dcp(piece, 15).get(), DCPTime::from_seconds(0.6).get());
+ BOOST_CHECK_EQUAL (player->content_video_to_dcp(piece, 75).get(), DCPTime::from_seconds(3.0).get());
+
+ /* Position 3s, no trim, content rate 24, DCP rate 25 */
+ content->set_position (film, DCPTime::from_seconds(3));
+ content->set_trim_start(film, ContentTime());
+ content->set_video_frame_rate(film, 24);
+ film->set_video_frame_rate (25);
+ player->setup_pieces ();
+ BOOST_REQUIRE_EQUAL (player->_pieces.size(), 1U);
+ piece = player->_pieces.front ();
+ BOOST_CHECK_EQUAL (player->content_video_to_dcp(piece, 0).get(), DCPTime::from_seconds(3.00).get());
+ BOOST_CHECK_EQUAL (player->content_video_to_dcp(piece, 40).get(), DCPTime::from_seconds(4.60).get());
+ BOOST_CHECK_EQUAL (player->content_video_to_dcp(piece, 169).get(), DCPTime::from_seconds(9.76).get());
+
+ /* Position 3s, 1.6s trim, content rate 24, DCP rate 25, so the 1.6s trim is at 24fps */
+ content->set_position (film, DCPTime::from_seconds(3));
+ content->set_trim_start(film, ContentTime::from_seconds(1.6));
+ content->set_video_frame_rate(film, 24);
+ film->set_video_frame_rate (25);
+ player->setup_pieces ();
+ BOOST_REQUIRE_EQUAL (player->_pieces.size(), 1U);
+ piece = player->_pieces.front ();
+ BOOST_CHECK_EQUAL (player->content_video_to_dcp(piece, 0).get(), 142080);
+ BOOST_CHECK_EQUAL (player->content_video_to_dcp(piece, 40).get(), 295680);
+ BOOST_CHECK_EQUAL (player->content_video_to_dcp(piece, 80).get(), 449280);
+ BOOST_CHECK_EQUAL (player->content_video_to_dcp(piece, 209).get(), 944640);
+
+ /* Position 0, no trim, content rate 24, DCP rate 48
+ Now, for example, a DCPTime position of 3s means 3s at 48fps. Since we run the video
+ with repeated frames in this case, 3 * 24 frames of content video will
+ be used to make 3 * 48 frames of DCP video. The results should be the same as the
+ content rate = DCP rate case.
+ */
+ content->set_position (film, DCPTime());
+ content->set_trim_start(film, ContentTime());
+ content->set_video_frame_rate(film, 24);
+ film->set_video_frame_rate (48);
+ player->setup_pieces ();
+ BOOST_REQUIRE_EQUAL (player->_pieces.size(), 1U);
+ piece = player->_pieces.front ();
+ BOOST_CHECK_EQUAL (player->content_video_to_dcp(piece, 0).get(), 0);
+ BOOST_CHECK_EQUAL (player->content_video_to_dcp(piece, 12).get(), DCPTime::from_seconds(0.5).get());
+ BOOST_CHECK_EQUAL (player->content_video_to_dcp(piece, 72).get(), DCPTime::from_seconds(3.0).get());
+
+ /* Position 3s, no trim, content rate 24, DCP rate 48 */
+ content->set_position (film, DCPTime::from_seconds(3));
+ content->set_trim_start(film, ContentTime());
+ content->set_video_frame_rate(film, 24);
+ film->set_video_frame_rate (48);
+ player->setup_pieces ();
+ BOOST_REQUIRE_EQUAL (player->_pieces.size(), 1U);
+ piece = player->_pieces.front ();
+ BOOST_CHECK_EQUAL (player->content_video_to_dcp(piece, 0).get(), DCPTime::from_seconds(3.00).get());
+ BOOST_CHECK_EQUAL (player->content_video_to_dcp(piece, 36).get(), DCPTime::from_seconds(4.50).get());
+ BOOST_CHECK_EQUAL (player->content_video_to_dcp(piece, 162).get(), DCPTime::from_seconds(9.75).get());
+
+ /* Position 3s, 1.5s trim, content rate 24, DCP rate 48 */
+ content->set_position (film, DCPTime::from_seconds(3));
+ content->set_trim_start(film, ContentTime::from_seconds(1.5));
+ content->set_video_frame_rate(film, 24);
+ film->set_video_frame_rate (48);
+ player->setup_pieces ();
+ BOOST_REQUIRE_EQUAL (player->_pieces.size(), 1U);
+ piece = player->_pieces.front ();
+ BOOST_CHECK_EQUAL (player->content_video_to_dcp(piece, 0).get(), DCPTime::from_seconds(1.50).get());
+ BOOST_CHECK_EQUAL (player->content_video_to_dcp(piece, 36).get(), DCPTime::from_seconds(3.00).get());
+ BOOST_CHECK_EQUAL (player->content_video_to_dcp(piece, 72).get(), DCPTime::from_seconds(4.50).get());
+ BOOST_CHECK_EQUAL (player->content_video_to_dcp(piece, 198).get(), DCPTime::from_seconds(9.75).get());
+
+ /* Position 0, no trim, content rate 48, DCP rate 24
+ Now, for example, a DCPTime position of 3s means 3s at 24fps. Since we run the video
+ with skipped frames in this case, 3 * 48 frames of content video will
+ be used to make 3 * 24 frames of DCP video.
+ */
+ content->set_position (film, DCPTime());
+ content->set_trim_start(film, ContentTime());
+ content->set_video_frame_rate(film, 48);
+ film->set_video_frame_rate (24);
+ player->setup_pieces ();
+ BOOST_REQUIRE_EQUAL (player->_pieces.size(), 1U);
+ piece = player->_pieces.front ();
+ BOOST_CHECK_EQUAL (player->content_video_to_dcp(piece, 0).get(), 0);
+ BOOST_CHECK_EQUAL (player->content_video_to_dcp(piece, 24).get(), DCPTime::from_seconds(0.5).get());
+ BOOST_CHECK_EQUAL (player->content_video_to_dcp(piece, 144).get(), DCPTime::from_seconds(3.0).get());
+
+ /* Position 3s, no trim, content rate 24, DCP rate 48 */
+ content->set_position (film, DCPTime::from_seconds(3));
+ content->set_trim_start(film, ContentTime());
+ content->set_video_frame_rate(film, 48);
+ film->set_video_frame_rate (24);
+ player->setup_pieces ();
+ BOOST_REQUIRE_EQUAL (player->_pieces.size(), 1U);
+ piece = player->_pieces.front ();
+ BOOST_CHECK_EQUAL (player->content_video_to_dcp(piece, 0).get(), DCPTime::from_seconds(3.00).get());
+ BOOST_CHECK_EQUAL (player->content_video_to_dcp(piece, 72).get(), DCPTime::from_seconds(4.50).get());
+ BOOST_CHECK_EQUAL (player->content_video_to_dcp(piece, 324).get(), DCPTime::from_seconds(9.75).get());
+
+ /* Position 3s, 1.5s trim, content rate 24, DCP rate 48 */
+ content->set_position (film, DCPTime::from_seconds(3));
+ content->set_trim_start(film, ContentTime::from_seconds(1.5));
+ content->set_video_frame_rate(film, 48);
+ film->set_video_frame_rate (24);
+ player->setup_pieces ();
+ BOOST_REQUIRE_EQUAL (player->_pieces.size(), 1U);
+ piece = player->_pieces.front ();
+ BOOST_CHECK_EQUAL (player->content_video_to_dcp(piece, 0).get(), DCPTime::from_seconds(1.50).get());
+ BOOST_CHECK_EQUAL (player->content_video_to_dcp(piece, 72).get(), DCPTime::from_seconds(3.00).get());
+ BOOST_CHECK_EQUAL (player->content_video_to_dcp(piece, 144).get(), DCPTime::from_seconds(4.50).get());
+ BOOST_CHECK_EQUAL (player->content_video_to_dcp(piece, 396).get(), DCPTime::from_seconds(9.75).get());
+}
+
+/** Test Player::dcp_to_content_audio */
+BOOST_AUTO_TEST_CASE (player_time_calculation_test3)
+{
+ auto film = new_test_film("player_time_calculation_test3");
+
+ auto doc = make_shared<cxml::Document>();
+ doc->read_string (xml);
+
+ list<string> notes;
+ auto content = std::make_shared<FFmpegContent>(doc, boost::none, 38, notes);
+ auto stream = content->audio->streams().front();
+ film->set_sequence (false);
+ film->add_content (content);
+
+ auto player = make_shared<Player>(film, Image::Alignment::COMPACT, false);
+
+ /* Position 0, no trim, video/audio content rate = video/audio DCP rate */
+ content->set_position (film, DCPTime());
+ content->set_trim_start(film, ContentTime());
+ content->set_video_frame_rate(film, 24);
+ film->set_video_frame_rate (24);
+ stream->_frame_rate = 48000;
+ player->setup_pieces ();
+ BOOST_REQUIRE_EQUAL (player->_pieces.size(), 1U);
+ auto piece = player->_pieces.front ();
+ BOOST_CHECK_EQUAL (player->dcp_to_resampled_audio (piece, DCPTime ()), 0);
+ BOOST_CHECK_EQUAL (player->dcp_to_resampled_audio (piece, DCPTime::from_seconds (0.5)), 24000);
+ BOOST_CHECK_EQUAL (player->dcp_to_resampled_audio (piece, DCPTime::from_seconds (3.0)), 144000);
+
+ /* Position 3s, no trim, video/audio content rate = video/audio DCP rate */
+ content->set_position (film, DCPTime::from_seconds (3));
+ content->set_trim_start(film, ContentTime());
+ content->set_video_frame_rate(film, 24);
+ film->set_video_frame_rate (24);
+ stream->_frame_rate = 48000;
+ player->setup_pieces ();
+ BOOST_REQUIRE_EQUAL (player->_pieces.size(), 1U);
+ piece = player->_pieces.front ();
+ BOOST_CHECK_EQUAL (player->dcp_to_resampled_audio (piece, DCPTime ()), 0);
+ BOOST_CHECK_EQUAL (player->dcp_to_resampled_audio (piece, DCPTime::from_seconds (0.50)), 0);
+ BOOST_CHECK_EQUAL (player->dcp_to_resampled_audio (piece, DCPTime::from_seconds (3.00)), 0);
+ BOOST_CHECK_EQUAL (player->dcp_to_resampled_audio (piece, DCPTime::from_seconds (4.50)), 72000);
+ BOOST_CHECK_EQUAL (player->dcp_to_resampled_audio (piece, DCPTime::from_seconds (9.75)), 324000);
+
+ /* Position 3s, 1.5s trim, video/audio content rate = video/audio DCP rate */
+ content->set_position (film, DCPTime::from_seconds (3));
+ content->set_trim_start(film, ContentTime::from_seconds(1.5));
+ content->set_video_frame_rate(film, 24);
+ film->set_video_frame_rate (24);
+ stream->_frame_rate = 48000;
+ player->setup_pieces ();
+ BOOST_REQUIRE_EQUAL (player->_pieces.size(), 1U);
+ piece = player->_pieces.front ();
+ BOOST_CHECK_EQUAL (player->dcp_to_resampled_audio (piece, DCPTime ()), 0);
+ BOOST_CHECK_EQUAL (player->dcp_to_resampled_audio (piece, DCPTime::from_seconds (0.50)), 0);
+ BOOST_CHECK_EQUAL (player->dcp_to_resampled_audio (piece, DCPTime::from_seconds (3.00)), 72000);
+ BOOST_CHECK_EQUAL (player->dcp_to_resampled_audio (piece, DCPTime::from_seconds (4.50)), 144000);
+ BOOST_CHECK_EQUAL (player->dcp_to_resampled_audio (piece, DCPTime::from_seconds (9.75)), 396000);
+
+ /* Position 0, no trim, content video rate 24, DCP video rate 25, both audio rates still 48k */
+ content->set_position (film, DCPTime());
+ content->set_trim_start(film, ContentTime());
+ content->set_video_frame_rate(film, 24);
+ film->set_video_frame_rate (25);
+ stream->_frame_rate = 48000;
+ player->setup_pieces ();
+ BOOST_REQUIRE_EQUAL (player->_pieces.size(), 1U);
+ piece = player->_pieces.front ();
+ BOOST_CHECK_EQUAL (player->dcp_to_resampled_audio (piece, DCPTime ()), 0);
+ BOOST_CHECK_EQUAL (player->dcp_to_resampled_audio (piece, DCPTime::from_seconds (0.6)), 28800);
+ BOOST_CHECK_EQUAL (player->dcp_to_resampled_audio (piece, DCPTime::from_seconds (3.0)), 144000);
+
+ /* Position 3s, no trim, content video rate 24, DCP rate 25, both audio rates still 48k. */
+ content->set_position (film, DCPTime::from_seconds(3));
+ content->set_trim_start(film, ContentTime());
+ content->set_video_frame_rate(film, 24);
+ film->set_video_frame_rate (25);
+ stream->_frame_rate = 48000;
+ player->setup_pieces ();
+ BOOST_REQUIRE_EQUAL (player->_pieces.size(), 1U);
+ piece = player->_pieces.front ();
+ BOOST_CHECK_EQUAL (player->dcp_to_resampled_audio (piece, DCPTime ()), 0);
+ BOOST_CHECK_EQUAL (player->dcp_to_resampled_audio (piece, DCPTime::from_seconds (0.60)), 0);
+ BOOST_CHECK_EQUAL (player->dcp_to_resampled_audio (piece, DCPTime::from_seconds (3.00)), 0);
+ BOOST_CHECK_EQUAL (player->dcp_to_resampled_audio (piece, DCPTime::from_seconds (4.60)), 76800);
+ BOOST_CHECK_EQUAL (player->dcp_to_resampled_audio (piece, DCPTime::from_seconds (9.75)), 324000);
+
+ /* Position 3s, 1.6s trim, content rate 24, DCP rate 25, both audio rates still 48k.
+ 1s of content is 46080 samples after resampling.
+ */
+ content->set_position (film, DCPTime::from_seconds(3));
+ content->set_trim_start(film, ContentTime::from_seconds(1.6));
+ content->set_video_frame_rate(film, 24);
+ film->set_video_frame_rate (25);
+ stream->_frame_rate = 48000;
+ player->setup_pieces ();
+ BOOST_REQUIRE_EQUAL (player->_pieces.size(), 1U);
+ piece = player->_pieces.front ();
+ BOOST_CHECK_EQUAL (player->dcp_to_resampled_audio (piece, DCPTime ()), 0);
+ BOOST_CHECK_EQUAL (player->dcp_to_resampled_audio (piece, DCPTime::from_seconds (0.60)), 0);
+ BOOST_CHECK_EQUAL (player->dcp_to_resampled_audio (piece, DCPTime::from_seconds (3.00)), 72960);
+ BOOST_CHECK_EQUAL (player->dcp_to_resampled_audio (piece, DCPTime::from_seconds (4.60)), 149760);
+ BOOST_CHECK_EQUAL (player->dcp_to_resampled_audio (piece, DCPTime::from_seconds (9.75)), 396960);
+
+ /* Position 0, no trim, content rate 24, DCP rate 48, both audio rates still 48k.
+ Now, for example, a DCPTime position of 3s means 3s at 48fps. Since we run the video
+ with repeated frames in this case, audio samples will map straight through.
+ The results should be the same as the content rate = DCP rate case.
+ */
+ content->set_position (film, DCPTime());
+ content->set_trim_start(film, ContentTime());
+ content->set_video_frame_rate(film, 24);
+ film->set_video_frame_rate (48);
+ stream->_frame_rate = 48000;
+ player->setup_pieces ();
+ BOOST_REQUIRE_EQUAL (player->_pieces.size(), 1U);
+ piece = player->_pieces.front ();
+ BOOST_CHECK_EQUAL (player->dcp_to_resampled_audio (piece, DCPTime ()), 0);
+ BOOST_CHECK_EQUAL (player->dcp_to_resampled_audio (piece, DCPTime::from_seconds (0.5)), 24000);
+ BOOST_CHECK_EQUAL (player->dcp_to_resampled_audio (piece, DCPTime::from_seconds (3.0)), 144000);
+
+ /* Position 3s, no trim, content rate 24, DCP rate 48 */
+ content->set_position (film, DCPTime::from_seconds(3));
+ content->set_trim_start(film, ContentTime());
+ content->set_video_frame_rate(film, 24);
+ film->set_video_frame_rate (24);
+ stream->_frame_rate = 48000;
+ player->setup_pieces ();
+ BOOST_REQUIRE_EQUAL (player->_pieces.size(), 1U);
+ piece = player->_pieces.front ();
+ BOOST_CHECK_EQUAL (player->dcp_to_resampled_audio (piece, DCPTime ()), 0);
+ BOOST_CHECK_EQUAL (player->dcp_to_resampled_audio (piece, DCPTime::from_seconds (0.50)), 0);
+ BOOST_CHECK_EQUAL (player->dcp_to_resampled_audio (piece, DCPTime::from_seconds (3.00)), 0);
+ BOOST_CHECK_EQUAL (player->dcp_to_resampled_audio (piece, DCPTime::from_seconds (4.50)), 72000);
+ BOOST_CHECK_EQUAL (player->dcp_to_resampled_audio (piece, DCPTime::from_seconds (9.75)), 324000);
+
+ /* Position 3s, 1.5s trim, content rate 24, DCP rate 48 */
+ content->set_position (film, DCPTime::from_seconds(3));
+ content->set_trim_start(film, ContentTime::from_seconds(1.5));
+ content->set_video_frame_rate(film, 24);
+ film->set_video_frame_rate (24);
+ stream->_frame_rate = 48000;
+ player->setup_pieces ();
+ BOOST_REQUIRE_EQUAL (player->_pieces.size(), 1U);
+ piece = player->_pieces.front ();
+ BOOST_CHECK_EQUAL (player->dcp_to_resampled_audio (piece, DCPTime ()), 0);
+ BOOST_CHECK_EQUAL (player->dcp_to_resampled_audio (piece, DCPTime::from_seconds (0.50)), 0);
+ BOOST_CHECK_EQUAL (player->dcp_to_resampled_audio (piece, DCPTime::from_seconds (3.00)), 72000);
+ BOOST_CHECK_EQUAL (player->dcp_to_resampled_audio (piece, DCPTime::from_seconds (4.50)), 144000);
+ BOOST_CHECK_EQUAL (player->dcp_to_resampled_audio (piece, DCPTime::from_seconds (9.75)), 396000);
+
+ /* Position 0, no trim, content rate 48, DCP rate 24
+ Now, for example, a DCPTime position of 3s means 3s at 24fps. Since we run the video
+ with skipped frames in this case, audio samples should map straight through.
+ */
+ content->set_position (film, DCPTime());
+ content->set_trim_start(film, ContentTime());
+ content->set_video_frame_rate(film, 24);
+ film->set_video_frame_rate (48);
+ stream->_frame_rate = 48000;
+ player->setup_pieces ();
+ BOOST_REQUIRE_EQUAL (player->_pieces.size(), 1U);
+ piece = player->_pieces.front ();
+ BOOST_CHECK_EQUAL (player->dcp_to_resampled_audio (piece, DCPTime ()), 0);
+ BOOST_CHECK_EQUAL (player->dcp_to_resampled_audio (piece, DCPTime::from_seconds (0.5)), 24000);
+ BOOST_CHECK_EQUAL (player->dcp_to_resampled_audio (piece, DCPTime::from_seconds (3.0)), 144000);
+
+ /* Position 3s, no trim, content rate 24, DCP rate 48 */
+ content->set_position (film, DCPTime::from_seconds(3));
+ content->set_trim_start(film, ContentTime());
+ content->set_video_frame_rate(film, 24);
+ film->set_video_frame_rate (24);
+ stream->_frame_rate = 48000;
+ player->setup_pieces ();
+ BOOST_REQUIRE_EQUAL (player->_pieces.size(), 1U);
+ piece = player->_pieces.front ();
+ BOOST_CHECK_EQUAL (player->dcp_to_resampled_audio (piece, DCPTime ()), 0);
+ BOOST_CHECK_EQUAL (player->dcp_to_resampled_audio (piece, DCPTime::from_seconds (0.50)), 0);
+ BOOST_CHECK_EQUAL (player->dcp_to_resampled_audio (piece, DCPTime::from_seconds (3.00)), 0);
+ BOOST_CHECK_EQUAL (player->dcp_to_resampled_audio (piece, DCPTime::from_seconds (4.50)), 72000);
+ BOOST_CHECK_EQUAL (player->dcp_to_resampled_audio (piece, DCPTime::from_seconds (9.75)), 324000);
+
+ /* Position 3s, 1.5s trim, content rate 24, DCP rate 48 */
+ content->set_position (film, DCPTime::from_seconds(3));
+ content->set_trim_start(film, ContentTime::from_seconds(1.5));
+ content->set_video_frame_rate(film, 24);
+ film->set_video_frame_rate (24);
+ stream->_frame_rate = 48000;
+ player->setup_pieces ();
+ BOOST_REQUIRE_EQUAL (player->_pieces.size(), 1U);
+ piece = player->_pieces.front ();
+ BOOST_CHECK_EQUAL (player->dcp_to_resampled_audio (piece, DCPTime ()), 0);
+ BOOST_CHECK_EQUAL (player->dcp_to_resampled_audio (piece, DCPTime::from_seconds (0.50)), 0);
+ BOOST_CHECK_EQUAL (player->dcp_to_resampled_audio (piece, DCPTime::from_seconds (3.00)), 72000);
+ BOOST_CHECK_EQUAL (player->dcp_to_resampled_audio (piece, DCPTime::from_seconds (4.50)), 144000);
+ BOOST_CHECK_EQUAL (player->dcp_to_resampled_audio (piece, DCPTime::from_seconds (9.75)), 396000);
+
+ /* Position 0, no trim, video content rate = video DCP rate, content audio rate = 44.1k */
+ content->set_position (film, DCPTime());
+ content->set_trim_start(film, ContentTime());
+ content->set_video_frame_rate(film, 24);
+ film->set_video_frame_rate (24);
+ stream->_frame_rate = 44100;
+ player->setup_pieces ();
+ BOOST_REQUIRE_EQUAL (player->_pieces.size(), 1U);
+ piece = player->_pieces.front ();
+ BOOST_CHECK_EQUAL (player->dcp_to_resampled_audio (piece, DCPTime ()), 0);
+ BOOST_CHECK_EQUAL (player->dcp_to_resampled_audio (piece, DCPTime::from_seconds (0.5)), 24000);
+ BOOST_CHECK_EQUAL (player->dcp_to_resampled_audio (piece, DCPTime::from_seconds (3.0)), 144000);
+
+ /* Position 3s, no trim, video content rate = video DCP rate, content audio rate = 44.1k */
+ content->set_position (film, DCPTime::from_seconds(3));
+ content->set_trim_start(film, ContentTime());
+ content->set_video_frame_rate(film, 24);
+ film->set_video_frame_rate (24);
+ stream->_frame_rate = 44100;
+ player->setup_pieces ();
+ BOOST_REQUIRE_EQUAL (player->_pieces.size(), 1U);
+ piece = player->_pieces.front ();
+ BOOST_CHECK_EQUAL (player->dcp_to_resampled_audio (piece, DCPTime ()), 0);
+ BOOST_CHECK_EQUAL (player->dcp_to_resampled_audio (piece, DCPTime::from_seconds (0.50)), 0);
+ BOOST_CHECK_EQUAL (player->dcp_to_resampled_audio (piece, DCPTime::from_seconds (3.00)), 0);
+ BOOST_CHECK_EQUAL (player->dcp_to_resampled_audio (piece, DCPTime::from_seconds (4.50)), 72000);
+ BOOST_CHECK_EQUAL (player->dcp_to_resampled_audio (piece, DCPTime::from_seconds (9.75)), 324000);
+
+ /* Position 3s, 1.5s trim, video content rate = video DCP rate, content audio rate = 44.1k */
+ content->set_position (film, DCPTime::from_seconds(3));
+ content->set_trim_start(film, ContentTime::from_seconds(1.5));
+ content->set_video_frame_rate(film, 24);
+ film->set_video_frame_rate (24);
+ stream->_frame_rate = 44100;
+ player->setup_pieces ();
+ BOOST_REQUIRE_EQUAL (player->_pieces.size(), 1U);
+ piece = player->_pieces.front ();
+ BOOST_CHECK_EQUAL (player->dcp_to_resampled_audio (piece, DCPTime ()), 0);
+ BOOST_CHECK_EQUAL (player->dcp_to_resampled_audio (piece, DCPTime::from_seconds (0.50)), 0);
+ BOOST_CHECK_EQUAL (player->dcp_to_resampled_audio (piece, DCPTime::from_seconds (3.00)), 72000);
+ BOOST_CHECK_EQUAL (player->dcp_to_resampled_audio (piece, DCPTime::from_seconds (4.50)), 144000);
+ BOOST_CHECK_EQUAL (player->dcp_to_resampled_audio (piece, DCPTime::from_seconds (9.75)), 396000);
+
+ /* Check with a large start trim */
+ content->set_position (film, DCPTime::from_seconds(0));
+ content->set_trim_start(film, ContentTime::from_seconds(54143));
+ content->set_video_frame_rate(film, 24);
+ film->set_video_frame_rate (24);
+ stream->_frame_rate = 48000;
+ player->setup_pieces ();
+ BOOST_REQUIRE_EQUAL (player->_pieces.size(), 1U);
+ piece = player->_pieces.front ();
+ BOOST_CHECK_EQUAL (player->dcp_to_resampled_audio (piece, DCPTime ()), 54143LL * 48000);
+}
diff --git a/test/lib/torture_test.cc b/test/lib/torture_test.cc
new file mode 100644
index 000000000..7abbaa04b
--- /dev/null
+++ b/test/lib/torture_test.cc
@@ -0,0 +1,327 @@
+/*
+ Copyright (C) 2017-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/>.
+
+*/
+
+
+/** @file test/torture_test.cc
+ * @brief Tricky arrangements of content whose resulting DCPs are checked programmatically.
+ * @ingroup completedcp
+ */
+
+
+#include "lib/audio_content.h"
+#include "lib/content_factory.h"
+#include "lib/dcp_content.h"
+#include "lib/dcp_content_type.h"
+#include "lib/film.h"
+#include "lib/ratio.h"
+#include "lib/text_content.h"
+#include "lib/video_content.h"
+#include "../test.h"
+#include <dcp/cpl.h>
+#include <dcp/mono_j2k_picture_asset.h>
+#include <dcp/mono_j2k_picture_frame.h>
+#include <dcp/openjpeg_image.h>
+#include <dcp/reel.h>
+#include <dcp/reel_picture_asset.h>
+#include <dcp/reel_sound_asset.h>
+#include <dcp/sound_asset.h>
+#include <boost/test/unit_test.hpp>
+#include <iostream>
+
+
+using std::cout;
+using std::dynamic_pointer_cast;
+using std::list;
+using std::make_shared;
+using std::shared_ptr;
+using namespace dcpomatic;
+
+
+/** Test start/end trim and positioning of some audio content */
+BOOST_AUTO_TEST_CASE (torture_test1)
+{
+ auto film = new_test_film("torture_test1");
+ film->set_sequence (false);
+
+ /* Staircase at an offset of 2000 samples, trimmed both start and end, with a gain of exactly 2 (linear) */
+ auto staircase = content_factory("test/data/staircase.wav")[0];
+ film->examine_and_add_content (staircase);
+ BOOST_REQUIRE (!wait_for_jobs());
+ staircase->set_position (film, DCPTime::from_frames(2000, film->audio_frame_rate()));
+ staircase->set_trim_start(film, ContentTime::from_frames(12, 48000));
+ staircase->set_trim_end (ContentTime::from_frames (35, 48000));
+ staircase->audio->set_gain (20 * log10(2));
+
+ /* And again at an offset of 50000 samples, trimmed both start and end, with a gain of exactly 2 (linear) */
+ staircase = content_factory("test/data/staircase.wav")[0];
+ film->examine_and_add_content (staircase);
+ BOOST_REQUIRE (!wait_for_jobs());
+ staircase->set_position (film, DCPTime::from_frames(50000, film->audio_frame_rate()));
+ staircase->set_trim_start(film, ContentTime::from_frames(12, 48000));
+ staircase->set_trim_end (ContentTime::from_frames(35, 48000));
+ staircase->audio->set_gain (20 * log10(2));
+
+ /* 1s of red at 5s in */
+ auto red = content_factory("test/data/flat_red.png")[0];
+ film->examine_and_add_content (red);
+ BOOST_REQUIRE (!wait_for_jobs());
+ red->set_position (film, DCPTime::from_seconds(5));
+ red->video->set_length (24);
+
+ film->set_video_frame_rate (24);
+ make_and_verify_dcp (film);
+
+ dcp::DCP dcp ("build/test/torture_test1/" + film->dcp_name(false));
+ dcp.read ();
+
+ auto cpls = dcp.cpls ();
+ BOOST_REQUIRE_EQUAL (cpls.size(), 1U);
+ auto reels = cpls.front()->reels ();
+ BOOST_REQUIRE_EQUAL (reels.size(), 1U);
+
+ /* Check sound */
+
+ auto reel_sound = reels.front()->main_sound();
+ BOOST_REQUIRE (reel_sound);
+ auto sound = reel_sound->asset();
+ BOOST_REQUIRE (sound);
+ BOOST_CHECK_EQUAL (sound->intrinsic_duration(), 144);
+
+ auto sound_reader = sound->start_read ();
+
+ /* First frame silent */
+ auto fr = sound_reader->get_frame (0);
+ for (int i = 0; i < fr->samples(); ++i) {
+ for (int j = 0; j < 6; ++j) {
+ BOOST_CHECK_EQUAL (fr->get(j, i), 0);
+ }
+ }
+
+ /* The first staircase is 4800 - 12 - 35 = 4753 samples. One frame is 2000 samples, so we span 3 frames */
+
+ BOOST_REQUIRE_EQUAL (fr->samples(), 2000);
+
+ int stair = 12;
+
+ BOOST_TEST_CONTEXT("First staircase, frame #1") {
+ fr = sound_reader->get_frame (1);
+ for (int i = 0; i < fr->samples(); ++i) {
+ for (int j = 0; j < 6; ++j) {
+ if (j == 2) {
+ BOOST_CHECK_EQUAL ((fr->get(j, i) + 128) >> 8, stair * 2);
+ ++stair;
+ } else {
+ BOOST_CHECK_EQUAL (fr->get(j, i), 0);
+ }
+ }
+ }
+ }
+
+ BOOST_TEST_CONTEXT("First staircase, frame #2") {
+ fr = sound_reader->get_frame (2);
+ for (int i = 0; i < fr->samples(); ++i) {
+ for (int j = 0; j < 6; ++j) {
+ if (j == 2) {
+ BOOST_CHECK_EQUAL ((fr->get(j, i) + 128) >> 8, stair * 2);
+ ++stair;
+ } else {
+ BOOST_CHECK_EQUAL (fr->get(j, i), 0);
+ }
+ }
+ }
+ }
+
+ BOOST_TEST_CONTEXT("First staircase, frame #3") {
+ fr = sound_reader->get_frame (3);
+ for (int i = 0; i < fr->samples(); ++i) {
+ for (int j = 0; j < 6; ++j) {
+ if (j == 2 && i < (4753 - (2000 * 2))) {
+ BOOST_CHECK_EQUAL ((fr->get(j, i) + 128) >> 8, stair * 2);
+ ++stair;
+ } else {
+ BOOST_CHECK_EQUAL (fr->get(j, i), 0);
+ }
+ }
+ }
+ }
+
+ /* Then some silence */
+
+ BOOST_TEST_CONTEXT("Silence") {
+ for (int i = 4; i < 24; ++i) {
+ fr = sound_reader->get_frame (i);
+ for (int j = 0; j < fr->samples(); ++j) {
+ for (int k = 0; k < 6; ++k) {
+ BOOST_CHECK_EQUAL (fr->get(k, j), 0);
+ }
+ }
+ }
+ }
+
+ /* Then the same thing again */
+
+ stair = 12;
+
+ fr = sound_reader->get_frame (25);
+ for (int i = 0; i < fr->samples(); ++i) {
+ for (int j = 0; j < 6; ++j) {
+ if (j == 2) {
+ BOOST_CHECK_EQUAL ((fr->get(j, i) + 128) >> 8, stair * 2);
+ ++stair;
+ } else {
+ BOOST_CHECK_EQUAL (fr->get(j, i), 0);
+ }
+ }
+ }
+
+ fr = sound_reader->get_frame (26);
+ for (int i = 0; i < fr->samples(); ++i) {
+ for (int j = 0; j < 6; ++j) {
+ if (j == 2) {
+ BOOST_CHECK_EQUAL ((fr->get(j, i) + 128) >> 8, stair * 2);
+ ++stair;
+ } else {
+ BOOST_CHECK_EQUAL (fr->get(j, i), 0);
+ }
+ }
+ }
+
+ fr = sound_reader->get_frame (27);
+ for (int i = 0; i < fr->samples(); ++i) {
+ for (int j = 0; j < 6; ++j) {
+ if (j == 2 && i < (4753 - (2000 * 2))) {
+ BOOST_CHECK_EQUAL ((fr->get(j, i) + 128) >> 8, stair * 2);
+ ++stair;
+ } else {
+ BOOST_CHECK_EQUAL (fr->get(j, i), 0);
+ }
+ }
+ }
+
+ /* Then some silence */
+
+ for (int i = 28; i < 144; ++i) {
+ fr = sound_reader->get_frame (i);
+ for (int j = 0; j < fr->samples(); ++j) {
+ for (int k = 0; k < 6; ++k) {
+ BOOST_CHECK_EQUAL (fr->get(k, j), 0);
+ }
+ }
+ }
+
+ /* Check picture */
+
+ auto reel_picture = reels.front()->main_picture();
+ BOOST_REQUIRE (reel_picture);
+ auto picture = dynamic_pointer_cast<dcp::MonoJ2KPictureAsset>(reel_picture->asset());
+ BOOST_REQUIRE (picture);
+ BOOST_CHECK_EQUAL (picture->intrinsic_duration(), 144);
+
+ auto picture_reader = picture->start_read ();
+
+ /* First 5 * 24 = 120 frames should be black, possibly with a little noise to raise the bitrate */
+
+ shared_ptr<dcp::OpenJPEGImage> ref;
+ for (int i = 0; i < 120; ++i) {
+ auto fr = picture_reader->get_frame (i);
+ auto image = fr->xyz_image ();
+ auto const size = image->size ();
+ if (i == 0) {
+ /* Check the first frame pixel by pixel... */
+ for (int c = 0; c < 3; ++c) {
+ for (int y = 0; y < size.height; ++y) {
+ for (int x = 0; x < size.width; ++x) {
+ BOOST_REQUIRE (image->data(c)[y * size.height + x] <= 5);
+ }
+ }
+ }
+ ref = image;
+ } else {
+ /* ... then all the others should be the same */
+ for (int c = 0; c < 3; ++c) {
+ BOOST_REQUIRE_MESSAGE (
+ memcmp (image->data(c), ref->data(c), size.width * size.height * sizeof(int)) == 0,
+ "failed on frame " << i << " component " << c
+ );
+ }
+ }
+ }
+
+ /* Then 24 red, perhaps also with some noise */
+
+ for (int i = 120; i < 144; ++i) {
+ auto fr = picture_reader->get_frame (i);
+ auto image = fr->xyz_image ();
+ auto const size = image->size ();
+ if (i == 120) {
+ for (int y = 0; y < size.height; ++y) {
+ for (int x = 0; x < size.width; ++x) {
+ BOOST_REQUIRE_MESSAGE (
+ abs(image->data(0)[y * size.height + x] - 2808) <= 5,
+ "failed on frame " << i << " with image data " << image->data(0)[y * size.height + x]
+ );
+ BOOST_REQUIRE_MESSAGE (
+ abs(image->data(1)[y * size.height + x] - 2176) <= 5,
+ "failed on frame " << i << " with image data " << image->data(1)[y * size.height + x]
+ );
+ BOOST_REQUIRE_MESSAGE (
+ abs(image->data(2)[y * size.height + x] - 865) <= 5,
+ "failed on frame " << i << " with image data " << image->data(2)[y * size.height + x]
+ );
+ }
+ }
+ ref = image;
+ } else {
+ for (int c = 0; c < 3; ++c) {
+ BOOST_REQUIRE_MESSAGE (
+ memcmp (image->data(c), ref->data(c), size.width * size.height * sizeof(int)) == 0,
+ "failed on frame " << i << " component " << c
+ );
+ }
+ }
+ }
+
+}
+
+
+BOOST_AUTO_TEST_CASE(multi_reel_interop_ccap_test)
+{
+ auto pic1 = content_factory("test/data/flat_red.png").front();
+ auto ccap1 = content_factory("test/data/15s.srt").front();
+ auto pic2 = content_factory("test/data/flat_red.png").front();
+ auto ccap2 = content_factory("test/data/15s.srt").front();
+ auto film1 = new_test_film("multi_reel_interop_ccap_test1", { pic1, ccap1, pic2, ccap2 });
+ film1->set_interop(true);
+ film1->set_reel_type(ReelType::BY_VIDEO_CONTENT);
+ ccap1->text[0]->set_type(TextType::CLOSED_CAPTION);
+ pic1->video->set_length(15 * 24);
+ ccap2->text[0]->set_type(TextType::CLOSED_CAPTION);
+ pic2->video->set_length(15 * 24);
+ make_and_verify_dcp(film1, { dcp::VerificationNote::Code::INVALID_STANDARD, dcp::VerificationNote::Code::INVALID_SUBTITLE_SPACING });
+
+ auto reload = make_shared<DCPContent>(film1->dir(film1->dcp_name()));
+ auto film2 = new_test_film("multi_reel_interop_ccap_test2", { reload });
+ for (auto i: reload->text) {
+ i->set_use(true);
+ }
+ film2->set_interop(true);
+ make_and_verify_dcp(film2, { dcp::VerificationNote::Code::INVALID_STANDARD, dcp::VerificationNote::Code::INVALID_SUBTITLE_SPACING });
+}
+
diff --git a/test/lib/unzipper_test.cc b/test/lib/unzipper_test.cc
new file mode 100644
index 000000000..88aec7baf
--- /dev/null
+++ b/test/lib/unzipper_test.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/>.
+
+*/
+
+
+/** @file test/unzipper_test.cc
+ * @brief Test Unzipper class.
+ * @ingroup selfcontained
+ */
+
+
+#include "lib/exceptions.h"
+#include "lib/unzipper.h"
+#include "lib/zipper.h"
+#include "../test.h"
+#include <dcp/filesystem.h>
+#include <dcp/util.h>
+#include <boost/test/unit_test.hpp>
+#include <boost/filesystem.hpp>
+
+
+using std::string;
+
+
+/** Basic test of Unzipper working normally */
+BOOST_AUTO_TEST_CASE(unzipper_test1)
+{
+ boost::system::error_code ec;
+ boost::filesystem::remove("build/test/zipped.zip", ec);
+
+ Zipper zipper("build/test/zipped.zip");
+ zipper.add("foo.txt", "1234567890");
+ zipper.add("bar.txt", "xxxxxxCCCCbbbbbbb1");
+ zipper.add("its_bigger_than_that_chris_its_large.txt", string(128 * 1024, 'X'));
+ zipper.close();
+
+ Unzipper unzipper("build/test/zipped.zip");
+ BOOST_CHECK_EQUAL(unzipper.get("foo.txt"), "1234567890");
+ BOOST_CHECK_EQUAL(unzipper.get("bar.txt"), "xxxxxxCCCCbbbbbbb1");
+ BOOST_CHECK_THROW(unzipper.get("hatstand"), std::runtime_error);
+ BOOST_CHECK_THROW(unzipper.get("its_bigger_than_that_chris_its_large.txt"), std::runtime_error);
+}
+
diff --git a/test/lib/update_checker_test.cc b/test/lib/update_checker_test.cc
new file mode 100644
index 000000000..27754f0dd
--- /dev/null
+++ b/test/lib/update_checker_test.cc
@@ -0,0 +1,42 @@
+/*
+ Copyright (C) 2014 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 test/update_checker_test.cc
+ * @brief Check UpdateChecker::version_less_than
+ * @ingroup selfcontained
+ */
+
+#include <boost/test/unit_test.hpp>
+#include "lib/update_checker.h"
+
+BOOST_AUTO_TEST_CASE (update_checker_test)
+{
+ BOOST_CHECK (UpdateChecker::version_less_than ("0.0.1", "0.0.2"));
+ BOOST_CHECK (UpdateChecker::version_less_than ("1.0.1", "2.0.2"));
+ BOOST_CHECK (UpdateChecker::version_less_than ("0.1.1", "1.5.2"));
+ BOOST_CHECK (UpdateChecker::version_less_than ("1.9.45", "1.9.46"));
+
+ BOOST_CHECK (!UpdateChecker::version_less_than ("0.0.1", "0.0.1"));
+ BOOST_CHECK (!UpdateChecker::version_less_than ("2.0.2", "1.0.1"));
+ BOOST_CHECK (!UpdateChecker::version_less_than ("1.5.2", "0.1.1"));
+ BOOST_CHECK (!UpdateChecker::version_less_than ("1.9.46", "1.9.45"));
+
+ BOOST_CHECK (!UpdateChecker::version_less_than ("1.9.46devel", "1.9.46"));
+}
diff --git a/test/lib/upmixer_a_test.cc b/test/lib/upmixer_a_test.cc
new file mode 100644
index 000000000..ab127a0ac
--- /dev/null
+++ b/test/lib/upmixer_a_test.cc
@@ -0,0 +1,103 @@
+/*
+ Copyright (C) 2014-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/>.
+
+*/
+
+
+/** @file test/upmixer_a_test.cc
+ * @brief Check the Upmixer A against some reference sound files.
+ * @ingroup selfcontained
+ */
+
+
+#include "lib/audio_buffers.h"
+#include "lib/dcp_content_type.h"
+#include "lib/ffmpeg_content.h"
+#include "lib/film.h"
+#include "lib/player.h"
+#include "lib/ratio.h"
+#include "lib/upmixer_a.h"
+#include "../test.h"
+#include <sndfile.h>
+#include <boost/test/unit_test.hpp>
+
+
+using std::make_shared;
+using std::shared_ptr;
+#if BOOST_VERSION >= 106100
+using namespace boost::placeholders;
+#endif
+using namespace dcpomatic;
+
+
+static SNDFILE* L;
+static SNDFILE* R;
+static SNDFILE* C;
+static SNDFILE* Lfe;
+static SNDFILE* Ls;
+static SNDFILE* Rs;
+
+
+static void
+write (shared_ptr<AudioBuffers> b, DCPTime)
+{
+ sf_write_float (L, b->data(0), b->frames());
+ sf_write_float (R, b->data(1), b->frames());
+ sf_write_float (C, b->data(2), b->frames());
+ sf_write_float (Lfe, b->data(3), b->frames());
+ sf_write_float (Ls, b->data(4), b->frames());
+ sf_write_float (Rs, b->data(5), b->frames());
+
+}
+
+
+BOOST_AUTO_TEST_CASE (upmixer_a_test)
+{
+ auto content = make_shared<FFmpegContent>("test/data/white.wav");
+ auto film = new_test_film("upmixer_a_test", { content });
+ film->set_audio_processor (AudioProcessor::from_id("stereo-5.1-upmix-a"));
+
+ SF_INFO info;
+ info.samplerate = 48000;
+ info.channels = 1;
+ info.format = SF_FORMAT_WAV | SF_FORMAT_PCM_16;
+ L = sf_open ("build/test/upmixer_a_test/L.wav", SFM_WRITE, &info);
+ R = sf_open ("build/test/upmixer_a_test/R.wav", SFM_WRITE, &info);
+ C = sf_open ("build/test/upmixer_a_test/C.wav", SFM_WRITE, &info);
+ Lfe = sf_open ("build/test/upmixer_a_test/Lfe.wav", SFM_WRITE, &info);
+ Ls = sf_open ("build/test/upmixer_a_test/Ls.wav", SFM_WRITE, &info);
+ Rs = sf_open ("build/test/upmixer_a_test/Rs.wav", SFM_WRITE, &info);
+
+ auto player = make_shared<Player>(film, Image::Alignment::COMPACT, false);
+ player->Audio.connect (bind (&write, _1, _2));
+ while (!player->pass()) {}
+
+ sf_close (L);
+ sf_close (R);
+ sf_close (C);
+ sf_close (Lfe);
+ sf_close (Ls);
+ sf_close (Rs);
+
+ check_wav_file ("test/data/upmixer_a_test/L.wav", "build/test/upmixer_a_test/L.wav");
+ check_wav_file ("test/data/upmixer_a_test/R.wav", "build/test/upmixer_a_test/R.wav");
+ check_wav_file ("test/data/upmixer_a_test/C.wav", "build/test/upmixer_a_test/C.wav");
+ check_wav_file ("test/data/upmixer_a_test/Lfe.wav", "build/test/upmixer_a_test/Lfe.wav");
+ check_wav_file ("test/data/upmixer_a_test/Ls.wav", "build/test/upmixer_a_test/Ls.wav");
+ check_wav_file ("test/data/upmixer_a_test/Rs.wav", "build/test/upmixer_a_test/Rs.wav");
+}
diff --git a/test/lib/util_test.cc b/test/lib/util_test.cc
new file mode 100644
index 000000000..6d8f1b5c0
--- /dev/null
+++ b/test/lib/util_test.cc
@@ -0,0 +1,226 @@
+/*
+ Copyright (C) 2012-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/>.
+
+*/
+
+
+/** @file test/util_test.cc
+ * @brief Test various utility methods.
+ * @ingroup selfcontained
+ */
+
+
+#include "lib/util.h"
+#include "lib/cross.h"
+#include "lib/exceptions.h"
+#include "../test.h"
+#include <dcp/certificate_chain.h>
+#include <fmt/format.h>
+#include <boost/bind/bind.hpp>
+#include <boost/date_time/c_local_time_adjustor.hpp>
+#include <boost/test/unit_test.hpp>
+
+
+
+using std::list;
+using std::string;
+using std::vector;
+#if BOOST_VERSION >= 106100
+using namespace boost::placeholders;
+#endif
+using namespace dcpomatic;
+
+
+BOOST_AUTO_TEST_CASE (digest_head_tail_test)
+{
+ vector<boost::filesystem::path> p;
+ p.push_back ("test/data/digest.test");
+ BOOST_CHECK_EQUAL (digest_head_tail (p, 1024), "57497ef84a0487f2bb0939a1f5703912");
+
+ p.push_back ("test/data/digest.test2");
+ BOOST_CHECK_EQUAL (digest_head_tail (p, 1024), "5a3a89857b931755ae728a518224a05c");
+
+ p.clear ();
+ p.push_back ("test/data/digest.test3");
+ p.push_back ("test/data/digest.test");
+ p.push_back ("test/data/digest.test2");
+ p.push_back ("test/data/digest.test4");
+ BOOST_CHECK_EQUAL (digest_head_tail (p, 1024), "52ccf111e4e72b58bb7b2aaa6bd45ea5");
+
+ p.clear ();
+ p.push_back ("foobar");
+ BOOST_CHECK_THROW (digest_head_tail (p, 1024), OpenFileError);
+}
+
+
+BOOST_AUTO_TEST_CASE (timecode_test)
+{
+ auto t = DCPTime::from_seconds (2 * 60 * 60 + 4 * 60 + 31) + DCPTime::from_frames (19, 24);
+ BOOST_CHECK_EQUAL (t.timecode (24), "02:04:31:19");
+}
+
+
+BOOST_AUTO_TEST_CASE (seconds_to_approximate_hms_test)
+{
+ BOOST_CHECK_EQUAL (seconds_to_approximate_hms(1), "1s");
+ BOOST_CHECK_EQUAL (seconds_to_approximate_hms(2), "2s");
+ BOOST_CHECK_EQUAL (seconds_to_approximate_hms(60), "1m");
+ BOOST_CHECK_EQUAL (seconds_to_approximate_hms(1.5 * 60), "1m 30s");
+ BOOST_CHECK_EQUAL (seconds_to_approximate_hms(2 * 60), "2m");
+ BOOST_CHECK_EQUAL (seconds_to_approximate_hms(17 * 60 + 20), "17m");
+ BOOST_CHECK_EQUAL (seconds_to_approximate_hms(1 * 3600), "1h");
+ BOOST_CHECK_EQUAL (seconds_to_approximate_hms(3600 + 40 * 60), "1h 40m");
+ BOOST_CHECK_EQUAL (seconds_to_approximate_hms(2 * 3600), "2h");
+ BOOST_CHECK_EQUAL (seconds_to_approximate_hms(2 * 3600 - 1), "2h");
+ BOOST_CHECK_EQUAL (seconds_to_approximate_hms(13 * 3600 + 40 * 60), "14h");
+}
+
+
+BOOST_AUTO_TEST_CASE (time_to_hmsf_test)
+{
+ BOOST_CHECK_EQUAL (time_to_hmsf(DCPTime::from_frames(12, 24), 24), "0:00:00.12");
+ BOOST_CHECK_EQUAL (time_to_hmsf(DCPTime::from_frames(24, 24), 24), "0:00:01.0");
+ BOOST_CHECK_EQUAL (time_to_hmsf(DCPTime::from_frames(32, 24), 24), "0:00:01.8");
+ BOOST_CHECK_EQUAL (time_to_hmsf(DCPTime::from_seconds(92), 24), "0:01:32.0");
+ BOOST_CHECK_EQUAL (time_to_hmsf(DCPTime::from_seconds(2 * 60 * 60 + 92), 24), "2:01:32.0");
+}
+
+
+BOOST_AUTO_TEST_CASE (tidy_for_filename_test)
+{
+ BOOST_CHECK_EQUAL (tidy_for_filename ("fish\\chips"), "fish_chips");
+ BOOST_CHECK_EQUAL (tidy_for_filename ("fish:chips\\"), "fish_chips_");
+ BOOST_CHECK_EQUAL (tidy_for_filename ("fish/chips\\"), "fish_chips_");
+ BOOST_CHECK_EQUAL (tidy_for_filename ("abcdefghï"), "abcdefghï");
+}
+
+
+BOOST_AUTO_TEST_CASE (utf8_strlen_test)
+{
+ BOOST_CHECK_EQUAL (utf8_strlen("hello world"), 11U);
+ BOOST_CHECK_EQUAL (utf8_strlen("hëllo world"), 11U);
+ BOOST_CHECK_EQUAL (utf8_strlen("hëłlo wørld"), 11U);
+}
+
+
+BOOST_AUTO_TEST_CASE (careful_string_filter_test)
+{
+ BOOST_CHECK_EQUAL ("hello_world", careful_string_filter("hello_world"));
+ BOOST_CHECK_EQUAL ("hello_world", careful_string_filter("héllo_world"));
+ BOOST_CHECK_EQUAL ("hello_world", careful_string_filter("héllo_wörld"));
+ BOOST_CHECK_EQUAL ("hello_world", careful_string_filter("héllo_wörld"));
+ BOOST_CHECK_EQUAL ("hello_world_a", careful_string_filter("héllo_wörld_à"));
+ BOOST_CHECK_EQUAL ("hello_world_CcGgIOoSsUuLl", careful_string_filter("hello_world_ÇçĞğİÖöŞşÜüŁł"));
+}
+
+
+static list<float> progress_values;
+
+static void
+progress (float p)
+{
+ progress_values.push_back (p);
+}
+
+
+BOOST_AUTO_TEST_CASE (copy_in_bits_test)
+{
+ for (int i = 0; i < 32; ++i) {
+ make_random_file ("build/test/random.dat", std::max(1, rand() % (256 * 1024 * 1024)));
+
+ progress_values.clear ();
+ copy_in_bits ("build/test/random.dat", "build/test/random.dat2", boost::bind(&progress, _1));
+ BOOST_CHECK (!progress_values.empty());
+
+ check_file ("build/test/random.dat", "build/test/random.dat2");
+ }
+}
+
+
+BOOST_AUTO_TEST_CASE(word_wrap_test)
+{
+ BOOST_CHECK_EQUAL(word_wrap("hello world", 8), "hello \nworld\n");
+ BOOST_CHECK(word_wrap("hello this is a longer bit of text and it should be word-wrapped", 31) == string{"hello this is a longer bit of \ntext and it should be word-\nwrapped\n"});
+ BOOST_CHECK_EQUAL(word_wrap("hellocan'twrapthissadly", 5), "hello\ncan't\nwrapt\nhissa\ndly\n");
+}
+
+
+BOOST_AUTO_TEST_CASE(screen_names_to_string_test)
+{
+ BOOST_CHECK_EQUAL(screen_names_to_string({"1", "2", "3"}), "1, 2, 3");
+ BOOST_CHECK_EQUAL(screen_names_to_string({"3", "2", "1"}), "1, 2, 3");
+ BOOST_CHECK_EQUAL(screen_names_to_string({"39", "3", "10", "1", "2"}), "1, 2, 3, 10, 39");
+ BOOST_CHECK_EQUAL(screen_names_to_string({"Sheila", "Fred", "Jim"}), "Fred, Jim, Sheila");
+ BOOST_CHECK_EQUAL(screen_names_to_string({"Sheila", "Fred", "Jim", "1"}), "1, Fred, Jim, Sheila");
+}
+
+
+BOOST_AUTO_TEST_CASE(rfc_2822_date_test)
+{
+#ifdef DCPOMATIC_WINDOWS
+ auto result = setlocale(LC_TIME, "German");
+#endif
+#ifdef DCPOMATIC_OSX
+ auto result = setlocale(LC_TIME, "de_DE");
+#endif
+#ifdef DCPOMATIC_LINUX
+ auto result = setlocale(LC_TIME, "de_DE.UTF8");
+#endif
+ BOOST_REQUIRE(result);
+
+ auto const utc_now = boost::posix_time::second_clock::universal_time ();
+ auto const local_now = boost::date_time::c_local_adjustor<boost::posix_time::ptime>::utc_to_local (utc_now);
+ auto const offset = local_now - utc_now;
+
+ auto const hours = int(abs(offset.hours()));
+ auto const tz = fmt::format("{}{:02d}{:02d}", offset.hours() >= 0 ? "+" : "-", hours, int(offset.minutes()));
+
+ int constexpr day = 24 * 60 * 60;
+
+ /* This won't pass when running in all time zones, but it's really the overall format (and in particular
+ * the use of English for day and month names) that we want to check.
+ */
+
+ auto check_allowing_dst = [hours, tz](int day_index, string format) {
+ auto test = rfc_2822_date(day_index * day);
+ BOOST_CHECK(
+ test == fmt::format(format, hours, tz) ||
+ test == fmt::format(format, hours - 1, tz)
+ );
+ };
+
+ check_allowing_dst(0, "Thu, 01 Jan 1970 {:02d}:00:00 {}");
+ check_allowing_dst(1, "Fri, 02 Jan 1970 {:02d}:00:00 {}");
+ check_allowing_dst(2, "Sat, 03 Jan 1970 {:02d}:00:00 {}");
+ check_allowing_dst(3, "Sun, 04 Jan 1970 {:02d}:00:00 {}");
+ check_allowing_dst(4, "Mon, 05 Jan 1970 {:02d}:00:00 {}");
+ check_allowing_dst(5, "Tue, 06 Jan 1970 {:02d}:00:00 {}");
+ check_allowing_dst(6, "Wed, 07 Jan 1970 {:02d}:00:00 {}");
+ check_allowing_dst(39, "Mon, 09 Feb 1970 {:02d}:00:00 {}");
+ check_allowing_dst(89, "Tue, 31 Mar 1970 {:02d}:00:00 {}");
+ check_allowing_dst(109, "Mon, 20 Apr 1970 {:02d}:00:00 {}");
+ check_allowing_dst(134, "Fri, 15 May 1970 {:02d}:00:00 {}");
+ check_allowing_dst(158, "Mon, 08 Jun 1970 {:02d}:00:00 {}");
+ check_allowing_dst(182, "Thu, 02 Jul 1970 {:02d}:00:00 {}");
+ check_allowing_dst(221, "Mon, 10 Aug 1970 {:02d}:00:00 {}");
+ check_allowing_dst(247, "Sat, 05 Sep 1970 {:02d}:00:00 {}");
+ check_allowing_dst(300, "Wed, 28 Oct 1970 {:02d}:00:00 {}");
+ check_allowing_dst(314, "Wed, 11 Nov 1970 {:02d}:00:00 {}");
+ check_allowing_dst(363, "Wed, 30 Dec 1970 {:02d}:00:00 {}");
+}
+
diff --git a/test/lib/vf_kdm_test.cc b/test/lib/vf_kdm_test.cc
new file mode 100644
index 000000000..b9357cd26
--- /dev/null
+++ b/test/lib/vf_kdm_test.cc
@@ -0,0 +1,107 @@
+/*
+ Copyright (C) 2017-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/>.
+
+*/
+
+
+/** @file test/vf_kdm_test.cc
+ * @brief Test encrypted VF creation and import
+ * @ingroup feature
+ */
+
+
+#include "lib/config.h"
+#include "lib/constants.h"
+#include "lib/cross.h"
+#include "lib/dcp_content.h"
+#include "lib/dcp_content_type.h"
+#include "lib/dcp_subtitle_content.h"
+#include "lib/ffmpeg_content.h"
+#include "lib/film.h"
+#include "lib/ratio.h"
+#include "lib/screen.h"
+#include "../test.h"
+#include <dcp/cpl.h>
+#include <boost/test/unit_test.hpp>
+
+
+using std::make_shared;
+using std::string;
+using std::vector;
+
+
+BOOST_AUTO_TEST_CASE (vf_kdm_test)
+{
+ ConfigRestorer cr;
+
+ /* Make an encrypted DCP from test.mp4 */
+
+ auto c = make_shared<FFmpegContent>("test/data/test.mp4");
+ auto A = new_test_film("vf_kdm_test_ov", { c });
+ A->set_interop (true);
+ A->set_dcp_content_type(DCPContentType::from_isdcf_name("TLR"));
+ A->set_encrypted (true);
+ make_and_verify_dcp (A, {dcp::VerificationNote::Code::INVALID_STANDARD});
+
+ dcp::DCP A_dcp ("build/test/vf_kdm_test_ov/" + A->dcp_name());
+ A_dcp.read ();
+
+ Config::instance()->set_decryption_chain (make_shared<dcp::CertificateChain>(openssl_path(), CERTIFICATE_VALIDITY_PERIOD));
+
+ auto signer = Config::instance()->signer_chain();
+ BOOST_REQUIRE(signer->valid());
+
+ auto const A_decrypted_kdm = A->make_kdm(A_dcp.cpls().front()->file().get(), dcp::LocalTime("2030-07-21T00:00:00+00:00"), dcp::LocalTime("2031-07-21T00:00:00+00:00"));
+ auto const A_kdm = A_decrypted_kdm.encrypt(signer, Config::instance()->decryption_chain()->leaf(), {}, dcp::Formulation::MODIFIED_TRANSITIONAL_1, true, 0);
+
+ /* Import A into a new project, with the required KDM, and make a VF that refers to it */
+
+ auto d = make_shared<DCPContent>("build/test/vf_kdm_test_ov/" + A->dcp_name());
+ d->add_kdm(A_kdm);
+
+ auto B = new_test_film("vf_kdm_test_vf", { d });
+ B->set_dcp_content_type(DCPContentType::from_isdcf_name("TLR"));
+ B->set_interop(true);
+
+ d->set_reference_video (true);
+ B->set_encrypted (true);
+ make_and_verify_dcp (B, {dcp::VerificationNote::Code::INVALID_STANDARD, dcp::VerificationNote::Code::EXTERNAL_ASSET});
+
+ dcp::DCP B_dcp ("build/test/vf_kdm_test_vf/" + B->dcp_name());
+ B_dcp.read ();
+
+ auto const B_decrypted_kdm = B->make_kdm(B_dcp.cpls().front()->file().get(), dcp::LocalTime ("2030-07-21T00:00:00+00:00"), dcp::LocalTime ("2031-07-21T00:00:00+00:00"));
+ auto const B_kdm = B_decrypted_kdm.encrypt(signer, Config::instance()->decryption_chain()->leaf(), {}, dcp::Formulation::MODIFIED_TRANSITIONAL_1, true, 0);
+
+ /* Import the OV and VF into a new project with the KDM that was created for the VF.
+ This KDM should decrypt assets from the OV too.
+ */
+
+ auto e = make_shared<DCPContent>("build/test/vf_kdm_test_vf/" + B->dcp_name());
+ e->add_ov ("build/test/vf_kdm_test_ov/" + A->dcp_name());
+ e->add_kdm(B_kdm);
+ auto C = new_test_film("vf_kdm_test_check", { e });
+ C->set_interop (true);
+ C->set_audio_channels(6);
+ C->set_dcp_content_type(DCPContentType::from_isdcf_name("TLR"));
+
+ make_and_verify_dcp (C, {dcp::VerificationNote::Code::INVALID_STANDARD});
+
+ /* Should be 1s red, 1s green, 1s blue */
+ check_dcp ("test/data/vf_kdm_test_check", "build/test/vf_kdm_test_check/" + C->dcp_name());
+}
diff --git a/test/lib/vf_test.cc b/test/lib/vf_test.cc
new file mode 100644
index 000000000..793bdc413
--- /dev/null
+++ b/test/lib/vf_test.cc
@@ -0,0 +1,540 @@
+/*
+ Copyright (C) 2015-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/>.
+
+*/
+
+
+/** @file test/vf_Test.cc
+ * @brief Various VF-related tests.
+ * @ingroup feature
+ */
+
+
+#include "lib/content_factory.h"
+#include "lib/dcp_content.h"
+#include "lib/dcp_content_type.h"
+#include "lib/examine_content_job.h"
+#include "lib/ffmpeg_content.h"
+#include "lib/film.h"
+#include "lib/job_manager.h"
+#include "lib/make_dcp.h"
+#include "lib/player.h"
+#include "lib/ratio.h"
+#include "lib/text_content.h"
+#include "lib/referenced_reel_asset.h"
+#include "lib/video_content.h"
+#include "../test.h"
+#include <dcp/cpl.h>
+#include <dcp/mono_j2k_picture_asset.h>
+#include <dcp/j2k_picture_asset_writer.h>
+#include <dcp/reel.h>
+#include <dcp/reel_mono_picture_asset.h>
+#include <dcp/reel_sound_asset.h>
+#include <dcp/reel_smpte_text_asset.h>
+#include <dcp/smpte_text_asset.h>
+#include <dcp/text_string.h>
+#include <boost/test/unit_test.hpp>
+#include <iostream>
+
+
+using std::cout;
+using std::dynamic_pointer_cast;
+using std::list;
+using std::make_shared;
+using std::shared_ptr;
+using std::string;
+using std::vector;
+using boost::optional;
+using namespace dcpomatic;
+
+
+/** Test the logic which decides whether a DCP can be referenced or not */
+BOOST_AUTO_TEST_CASE (vf_test1)
+{
+ auto dcp = make_shared<DCPContent>("test/data/reels_test2");
+ auto film = new_test_film("vf_test1", { dcp });
+
+ /* Multi-reel DCP can't be referenced if we are using a single reel for the project */
+ film->set_reel_type (ReelType::SINGLE);
+ film->set_audio_channels(16);
+ string why_not;
+ BOOST_CHECK (!dcp->can_reference_video(film, why_not));
+ BOOST_CHECK (!dcp->can_reference_audio(film, why_not));
+ BOOST_CHECK (!dcp->can_reference_text(film, TextType::OPEN_SUBTITLE, why_not));
+ BOOST_CHECK (!dcp->can_reference_text(film, TextType::CLOSED_CAPTION, why_not));
+
+ /* Multi-reel DCP can be referenced if we are using by-video-content */
+ film->set_reel_type (ReelType::BY_VIDEO_CONTENT);
+ BOOST_CHECK (dcp->can_reference_video(film, why_not));
+ BOOST_CHECK (dcp->can_reference_audio(film, why_not));
+ /* (but reels_test2 has no texts to reference) */
+ BOOST_CHECK (!dcp->can_reference_text(film, TextType::OPEN_SUBTITLE, why_not));
+ BOOST_CHECK (!dcp->can_reference_text(film, TextType::CLOSED_CAPTION, why_not));
+
+ auto other = make_shared<FFmpegContent>("test/data/test.mp4");
+ film->examine_and_add_content (other);
+ BOOST_REQUIRE (!wait_for_jobs());
+ BOOST_CHECK (!other->audio);
+
+ /* Not possible if there is overlap; we only check video here as that's all test.mp4 has */
+ other->set_position (film, DCPTime());
+ BOOST_CHECK (!dcp->can_reference_video(film, why_not));
+
+ /* This should not be considered an overlap */
+ other->set_position (film, dcp->end(film));
+ BOOST_CHECK (dcp->can_reference_video(film, why_not));
+ BOOST_CHECK (dcp->can_reference_audio(film, why_not));
+ /* (reels_test2 has no texts to reference) */
+ BOOST_CHECK (!dcp->can_reference_text(film, TextType::OPEN_SUBTITLE, why_not));
+ BOOST_CHECK (!dcp->can_reference_text(film, TextType::CLOSED_CAPTION, why_not));
+}
+
+
+/** Make a OV with video and audio and a VF referencing the OV and adding subs */
+BOOST_AUTO_TEST_CASE (vf_test2)
+{
+ /* Make the OV */
+ auto video = content_factory("test/data/flat_red.png")[0];
+ auto audio = content_factory("test/data/white.wav")[0];
+ auto ov = new_test_film("vf_test2_ov", { video, audio });
+ video->video->set_length (24 * 5);
+ make_and_verify_dcp (ov);
+
+ /* Make the VF */
+ auto dcp = make_shared<DCPContent>(ov->dir(ov->dcp_name()));
+ auto sub = content_factory("test/data/subrip4.srt")[0];
+ auto vf = new_test_film("vf_test2_vf", { dcp, sub });
+ vf->set_reel_type (ReelType::BY_VIDEO_CONTENT);
+ dcp->set_reference_video (true);
+ dcp->set_reference_audio (true);
+ make_and_verify_dcp (
+ vf,
+ {
+ dcp::VerificationNote::Code::EXTERNAL_ASSET,
+ dcp::VerificationNote::Code::MISSING_SUBTITLE_LANGUAGE,
+ dcp::VerificationNote::Code::INVALID_SUBTITLE_FIRST_TEXT_TIME,
+ dcp::VerificationNote::Code::INVALID_SUBTITLE_DURATION_BV21,
+ },
+ false
+ );
+
+ dcp::DCP ov_c (ov->dir(ov->dcp_name()));
+ ov_c.read ();
+ BOOST_REQUIRE_EQUAL (ov_c.cpls().size(), 1U);
+ BOOST_REQUIRE_EQUAL (ov_c.cpls()[0]->reels().size(), 1U);
+ BOOST_REQUIRE (ov_c.cpls()[0]->reels()[0]->main_picture());
+ string const pic_id = ov_c.cpls()[0]->reels()[0]->main_picture()->id();
+ BOOST_REQUIRE (ov_c.cpls()[0]->reels()[0]->main_sound());
+ string const sound_id = ov_c.cpls()[0]->reels()[0]->main_sound()->id();
+ BOOST_REQUIRE (!ov_c.cpls()[0]->reels()[0]->main_subtitle());
+ BOOST_REQUIRE(!ov_c.pkls().empty());
+ BOOST_CHECK(!static_cast<bool>(ov_c.pkls()[0]->group_id()));
+
+ dcp::DCP vf_c (vf->dir(vf->dcp_name()));
+ vf_c.read ();
+ BOOST_REQUIRE_EQUAL (vf_c.cpls().size(), 1U);
+ BOOST_REQUIRE_EQUAL (vf_c.cpls()[0]->reels().size(), 1U);
+ BOOST_REQUIRE (vf_c.cpls()[0]->reels()[0]->main_picture());
+ BOOST_CHECK_EQUAL (vf_c.cpls()[0]->reels()[0]->main_picture()->id(), pic_id);
+ BOOST_REQUIRE (vf_c.cpls()[0]->reels()[0]->main_sound());
+ BOOST_CHECK_EQUAL (vf_c.cpls()[0]->reels()[0]->main_sound()->id(), sound_id);
+ BOOST_REQUIRE (vf_c.cpls()[0]->reels()[0]->main_subtitle());
+ BOOST_REQUIRE(!vf_c.pkls().empty());
+ BOOST_CHECK(static_cast<bool>(vf_c.pkls()[0]->group_id()));
+}
+
+
+/** Test creation of a VF using a trimmed OV; the output should have entry point /
+ * duration altered to effect the trimming.
+ */
+BOOST_AUTO_TEST_CASE (vf_test3)
+{
+ /* Make the OV */
+ auto video = content_factory("test/data/flat_red.png")[0];
+ auto audio = content_factory("test/data/white.wav")[0];
+ auto ov = new_test_film("vf_test3_ov", { video, audio });
+ video->video->set_length (24 * 5);
+ make_and_verify_dcp (ov);
+
+ /* Make the VF */
+ auto dcp = make_shared<DCPContent>(ov->dir(ov->dcp_name()));
+ auto vf = new_test_film("vf_test3_vf", { dcp });
+ vf->set_reel_type (ReelType::BY_VIDEO_CONTENT);
+ dcp->set_trim_start(vf, ContentTime::from_seconds (1));
+ dcp->set_trim_end (ContentTime::from_seconds (1));
+ dcp->set_reference_video (true);
+ dcp->set_reference_audio (true);
+ make_and_verify_dcp(vf, {dcp::VerificationNote::Code::EXTERNAL_ASSET}, false);
+
+ dcp::DCP vf_c (vf->dir(vf->dcp_name()));
+ vf_c.read ();
+ BOOST_REQUIRE_EQUAL (vf_c.cpls().size(), 1U);
+ BOOST_REQUIRE_EQUAL (vf_c.cpls()[0]->reels().size(), 1U);
+ BOOST_REQUIRE (vf_c.cpls()[0]->reels()[0]->main_picture());
+ BOOST_CHECK_EQUAL (vf_c.cpls()[0]->reels()[0]->main_picture()->entry_point().get_value_or(0), 24);
+ BOOST_CHECK_EQUAL (vf_c.cpls()[0]->reels()[0]->main_picture()->actual_duration(), 72);
+ BOOST_REQUIRE (vf_c.cpls()[0]->reels()[0]->main_sound());
+ BOOST_CHECK_EQUAL (vf_c.cpls()[0]->reels()[0]->main_sound()->entry_point().get_value_or(0), 24);
+ BOOST_CHECK_EQUAL (vf_c.cpls()[0]->reels()[0]->main_sound()->actual_duration(), 72);
+}
+
+
+/** Make a OV with video and audio and a VF referencing the OV and adding some more video */
+BOOST_AUTO_TEST_CASE (vf_test4)
+{
+ /* Make the OV */
+ auto video = content_factory("test/data/flat_red.png")[0];
+ auto audio = content_factory("test/data/white.wav")[0];
+ auto ov = new_test_film("vf_test4_ov", { video, audio });
+ video->video->set_length (24 * 5);
+ make_and_verify_dcp (ov);
+
+ /* Make the VF */
+ auto dcp = make_shared<DCPContent>(ov->dir(ov->dcp_name()));
+ auto vf = new_test_film("vf_test4_vf", { dcp });
+ vf->set_reel_type (ReelType::BY_VIDEO_CONTENT);
+ vf->set_sequence (false);
+ dcp->set_position(vf, DCPTime::from_seconds(10));
+ dcp->set_reference_video (true);
+ dcp->set_reference_audio (true);
+ auto more_video = content_factory("test/data/flat_red.png")[0];
+ vf->examine_and_add_content (more_video);
+ BOOST_REQUIRE (!wait_for_jobs());
+ more_video->set_position (vf, DCPTime());
+ make_and_verify_dcp(vf, {dcp::VerificationNote::Code::EXTERNAL_ASSET}, false);
+
+ dcp::DCP ov_c (ov->dir(ov->dcp_name()));
+ ov_c.read ();
+ BOOST_REQUIRE_EQUAL (ov_c.cpls().size(), 1U);
+ BOOST_REQUIRE_EQUAL (ov_c.cpls()[0]->reels().size(), 1U);
+ BOOST_REQUIRE (ov_c.cpls()[0]->reels()[0]->main_picture());
+ string const pic_id = ov_c.cpls()[0]->reels()[0]->main_picture()->id();
+ BOOST_REQUIRE (ov_c.cpls()[0]->reels()[0]->main_sound());
+ string const sound_id = ov_c.cpls()[0]->reels()[0]->main_sound()->id();
+ BOOST_REQUIRE (!ov_c.cpls()[0]->reels()[0]->main_subtitle());
+
+ dcp::DCP vf_c (vf->dir (vf->dcp_name ()));
+ vf_c.read ();
+ BOOST_REQUIRE_EQUAL (vf_c.cpls().size(), 1U);
+ BOOST_REQUIRE_EQUAL (vf_c.cpls()[0]->reels().size(), 2U);
+ BOOST_REQUIRE (vf_c.cpls()[0]->reels().back()->main_picture());
+ BOOST_CHECK_EQUAL (vf_c.cpls()[0]->reels().back()->main_picture()->id(), pic_id);
+ BOOST_REQUIRE (vf_c.cpls()[0]->reels().back()->main_sound());
+ BOOST_CHECK_EQUAL (vf_c.cpls()[0]->reels().back()->main_sound()->id(), sound_id);
+}
+
+
+/** Test bug #1495 */
+BOOST_AUTO_TEST_CASE (vf_test5)
+{
+ /* Make the OV */
+ auto ov = new_test_film("vf_test5_ov");
+ ov->set_reel_type (ReelType::BY_VIDEO_CONTENT);
+ for (int i = 0; i < 3; ++i) {
+ auto video = content_factory("test/data/flat_red.png")[0];
+ ov->examine_and_add_content (video);
+ BOOST_REQUIRE (!wait_for_jobs());
+ video->video->set_length (24 * 10);
+ }
+
+ BOOST_REQUIRE (!wait_for_jobs());
+ make_and_verify_dcp (ov);
+
+ /* Make the VF */
+ auto dcp = make_shared<DCPContent>(ov->dir(ov->dcp_name()));
+ auto vf = new_test_film("vf_test5_vf", { dcp });
+ vf->set_reel_type (ReelType::BY_VIDEO_CONTENT);
+ vf->set_sequence (false);
+ dcp->set_reference_video (true);
+ dcp->set_reference_audio (true);
+ dcp->set_trim_end (ContentTime::from_seconds(15));
+ make_and_verify_dcp(vf, {dcp::VerificationNote::Code::EXTERNAL_ASSET}, false);
+
+ /* Check that the selected reel assets are right */
+ auto a = get_referenced_reel_assets(vf, vf->playlist());
+ BOOST_REQUIRE_EQUAL (a.size(), 4U);
+ auto i = a.begin();
+ BOOST_CHECK (i->period == DCPTimePeriod(DCPTime(0), DCPTime(960000)));
+ ++i;
+ BOOST_CHECK (i->period == DCPTimePeriod(DCPTime(0), DCPTime(960000)));
+ ++i;
+ BOOST_CHECK (i->period == DCPTimePeriod(DCPTime(960000), DCPTime(1440000)));
+ ++i;
+ BOOST_CHECK (i->period == DCPTimePeriod(DCPTime(960000), DCPTime(1440000)));
+ ++i;
+}
+
+
+/** Test bug #1528 */
+BOOST_AUTO_TEST_CASE (vf_test6)
+{
+ /* Make the OV */
+ auto video = content_factory("test/data/flat_red.png")[0];
+ auto ov = new_test_film("vf_test6_ov", { video });
+ ov->set_reel_type (ReelType::BY_VIDEO_CONTENT);
+ video->video->set_length (24 * 10);
+ make_and_verify_dcp (ov);
+
+ /* Make the VF */
+ auto dcp = make_shared<DCPContent>(ov->dir(ov->dcp_name()));
+ auto vf = new_test_film("vf_test6_vf", { dcp });
+ vf->set_reel_type (ReelType::BY_VIDEO_CONTENT);
+ vf->set_sequence (false);
+ dcp->set_reference_video (true);
+ dcp->set_reference_audio (true);
+
+ auto sub = content_factory("test/data/15s.srt")[0];
+ vf->examine_and_add_content (sub);
+ BOOST_REQUIRE (!wait_for_jobs());
+
+ make_and_verify_dcp (
+ vf,
+ {
+ dcp::VerificationNote::Code::EXTERNAL_ASSET,
+ dcp::VerificationNote::Code::MISSING_SUBTITLE_LANGUAGE,
+ dcp::VerificationNote::Code::INVALID_SUBTITLE_FIRST_TEXT_TIME
+ },
+ false
+ );
+}
+
+
+/** Test bug #1643 (the second part; referring fails if there are gaps) */
+BOOST_AUTO_TEST_CASE (vf_test7)
+{
+ /* First OV */
+ auto ov1 = new_test_film("vf_test7_ov1", {content_factory("test/data/flat_red.png")[0]});
+ ov1->set_video_frame_rate (24);
+ make_and_verify_dcp (ov1);
+
+ /* Second OV */
+ auto ov2 = new_test_film("vf_test7_ov2", {content_factory("test/data/flat_red.png")[0]});
+ ov2->set_video_frame_rate (24);
+ make_and_verify_dcp (ov2);
+
+ /* VF */
+ auto ov1_dcp = make_shared<DCPContent>(ov1->dir(ov1->dcp_name()));
+ auto ov2_dcp = make_shared<DCPContent>(ov2->dir(ov2->dcp_name()));
+ auto vf = new_test_film("vf_test7_vf", {ov1_dcp, ov2_dcp});
+ vf->set_reel_type (ReelType::BY_VIDEO_CONTENT);
+ ov1_dcp->set_reference_video (true);
+ ov2_dcp->set_reference_video (true);
+ ov1_dcp->set_position (vf, DCPTime::from_seconds(1));
+ ov2_dcp->set_position (vf, DCPTime::from_seconds(20));
+ vf->write_metadata ();
+ make_and_verify_dcp (vf);
+}
+
+
+/** Test bug #2116 */
+BOOST_AUTO_TEST_CASE (test_vf_with_trimmed_multi_reel_dcp)
+{
+ /* Make an OV with 3 reels */
+ std::vector<std::shared_ptr<Content>> ov_content;
+ for (int i = 0; i < 3; ++i) {
+ auto c = content_factory("test/data/flat_red.png")[0];
+ c->video->set_length(240);
+ ov_content.push_back(c);
+ }
+ auto ov = new_test_film("test_vf_with_trimmed_multi_reel_dcp_ov", ov_content);
+ ov->set_reel_type(ReelType::BY_VIDEO_CONTENT);
+ make_and_verify_dcp (ov);
+
+ /* Make a VF with a specific arrangement */
+ auto vf_image = content_factory("test/data/flat_red.png")[0];
+ auto vf_dcp = make_shared<DCPContent>(ov->dir(ov->dcp_name()));
+ auto vf = new_test_film("test_vf_with_trimmed_multi_reel_dcp_vf", { vf_image, vf_dcp });
+ vf->set_reel_type(ReelType::BY_VIDEO_CONTENT);
+ vf_dcp->set_reference_video(true);
+ vf_dcp->set_reference_audio(true);
+ vf_dcp->set_trim_start(vf, ContentTime::from_seconds(10));
+ vf_dcp->set_position(vf, DCPTime::from_seconds(10));
+ make_and_verify_dcp(vf, { dcp::VerificationNote::Code::EXTERNAL_ASSET }, false);
+}
+
+
+/** Test bug #2599: unable to reference open subtitles in an OV when creating a VF that adds closed captions */
+BOOST_AUTO_TEST_CASE(test_referencing_ov_with_subs_when_adding_ccaps)
+{
+ string const name("test_referencing_ov_with_subs_when_adding_ccaps");
+ auto subs = content_factory("test/data/15s.srt");
+ auto ov = new_test_film(name + "_ov", subs);
+ make_and_verify_dcp(
+ ov,
+ {
+ dcp::VerificationNote::Code::MISSING_SUBTITLE_LANGUAGE,
+ dcp::VerificationNote::Code::INVALID_SUBTITLE_FIRST_TEXT_TIME,
+ dcp::VerificationNote::Code::MISSING_CPL_METADATA
+ });
+
+ auto ccaps = content_factory("test/data/15s.srt")[0];
+ auto ov_dcp = make_shared<DCPContent>(ov->dir(ov->dcp_name(false)));
+ auto vf = new_test_film(name + "_vf", { ov_dcp, ccaps });
+ ccaps->text[0]->set_type(TextType::CLOSED_CAPTION);
+
+ string why_not;
+ BOOST_CHECK(ov_dcp->can_reference_text(vf, TextType::OPEN_SUBTITLE, why_not));
+ std::cout << why_not << "\n";
+}
+
+
+BOOST_AUTO_TEST_CASE(test_duplicate_font_id_in_vf)
+{
+ string const name("test_duplicate_font_id_in_vf");
+ auto subs = content_factory("test/data/15s.srt");
+ auto ov = new_test_film(name + "_ov", subs);
+ make_and_verify_dcp(
+ ov,
+ {
+ dcp::VerificationNote::Code::MISSING_SUBTITLE_LANGUAGE,
+ dcp::VerificationNote::Code::INVALID_SUBTITLE_FIRST_TEXT_TIME,
+ dcp::VerificationNote::Code::MISSING_CPL_METADATA
+ });
+
+ auto ccaps = content_factory("test/data/15s.srt")[0];
+ auto ov_dcp = make_shared<DCPContent>(ov->dir(ov->dcp_name(false)));
+ auto vf = new_test_film(name + "_vf", { ov_dcp, ccaps });
+ ov_dcp->set_reference_audio(true);
+ ov_dcp->set_reference_video(true);
+ ov_dcp->text[0]->set_use(true);
+ ccaps->text[0]->set_type(TextType::CLOSED_CAPTION);
+ string why_not;
+ BOOST_CHECK_MESSAGE(ov_dcp->can_reference_text(vf, TextType::OPEN_SUBTITLE, why_not), why_not);
+ ov_dcp->set_reference_text(TextType::OPEN_SUBTITLE, true);
+ vf->write_metadata();
+ make_dcp(vf, TranscodeJob::ChangedBehaviour::IGNORE);
+ BOOST_REQUIRE(!wait_for_jobs());
+
+ auto vf_dcp = make_shared<DCPContent>(vf->dir(vf->dcp_name(false)));
+
+ auto test = new_test_film(name + "_test", { vf_dcp });
+ vf_dcp->add_ov(ov->dir(ov->dcp_name(false)));
+ JobManager::instance()->add(make_shared<ExamineContentJob>(test, vf_dcp, false));
+ BOOST_CHECK(!wait_for_jobs());
+
+ make_and_verify_dcp(
+ test,
+ {
+ dcp::VerificationNote::Code::MISSING_SUBTITLE_LANGUAGE,
+ dcp::VerificationNote::Code::INVALID_SUBTITLE_FIRST_TEXT_TIME,
+ dcp::VerificationNote::Code::MISSING_CPL_METADATA,
+ });
+}
+
+
+BOOST_AUTO_TEST_CASE(test_referencing_ov_with_missing_subtitle_in_some_reels)
+{
+ auto const path = boost::filesystem::path("build/test/test_referencing_ov_with_missing_subtitle_in_some_reels");
+ boost::filesystem::remove_all(path);
+
+ boost::filesystem::create_directories(path / "ov");
+ dcp::DCP ov(path / "ov");
+
+ auto make_picture = [path](string filename) {
+ auto pic = make_shared<dcp::MonoJ2KPictureAsset>(dcp::Fraction(24, 1), dcp::Standard::SMPTE);
+ auto writer = pic->start_write(path / "ov" / filename, dcp::Behaviour::MAKE_NEW);
+ auto frame = dcp::ArrayData("test/data/picture.j2c");
+ for (int i = 0; i < 240; ++i) {
+ writer->write(frame);
+ }
+ writer->finalize();
+ return pic;
+ };
+
+ auto pic1 = make_picture("pic1.mxf");
+ auto pic2 = make_picture("pic2.mxf");
+
+ auto sub1 = make_shared<dcp::SMPTETextAsset>();
+
+ sub1->add(std::make_shared<dcp::TextString>(
+ boost::optional<string>(), false, false, false, dcp::Colour(255, 255, 255),
+ 42, 1, dcp::Time(0, 0, 5, 0, 24), dcp::Time(0, 0, 9, 0, 24),
+ 0, dcp::HAlign::CENTER,
+ 0, dcp::VAlign::CENTER,
+ 0, vector<dcp::Text::VariableZPosition>(), dcp::Direction::LTR,
+ "Hello",
+ dcp::Effect::NONE, dcp::Colour(0, 0, 0),
+ dcp::Time{}, dcp::Time{},
+ 0, vector<dcp::Ruby>{}
+ ));
+ sub1->write(path / "ov" / "sub.mxf");
+
+ auto reel1_pic = make_shared<dcp::ReelMonoPictureAsset>(pic1, 0);
+ auto reel1_sub = make_shared<dcp::ReelSMPTETextAsset>(dcp::TextType::OPEN_SUBTITLE, sub1, dcp::Fraction(24, 1), 240, 0);
+
+ auto reel2_pic = make_shared<dcp::ReelMonoPictureAsset>(pic1, 0);
+
+ auto reel1 = make_shared<dcp::Reel>(reel1_pic, shared_ptr<dcp::ReelSoundAsset>(), reel1_sub);
+ auto reel2 = make_shared<dcp::Reel>(reel2_pic);
+
+ auto cpl = make_shared<dcp::CPL>("Test CPL", dcp::ContentKind::FEATURE, dcp::Standard::SMPTE);
+ cpl->add(reel1);
+ cpl->add(reel2);
+
+ ov.add(cpl);
+ ov.write_xml();
+
+ auto dcp_ov = make_shared<DCPContent>(path / "ov");
+ auto vf = make_shared<Film>(path / "vf");
+ vf->set_dcp_content_type(DCPContentType::from_isdcf_name("TST"));
+ vf->set_container(Ratio::from_id("185"));
+ vf->write_metadata();
+ vf->examine_and_add_content(dcp_ov);
+ BOOST_REQUIRE(!wait_for_jobs());
+ vf->set_reel_type(ReelType::BY_VIDEO_CONTENT);
+ dcp_ov->set_reference_video(true);
+ dcp_ov->set_reference_text(TextType::OPEN_SUBTITLE, true);
+
+ vf->write_metadata();
+ make_dcp(vf, TranscodeJob::ChangedBehaviour::IGNORE);
+ BOOST_REQUIRE(!wait_for_jobs());
+
+ vector<dcp::VerificationNote::Code> ignore = {
+ dcp::VerificationNote::Code::MISSING_SUBTITLE_LANGUAGE,
+ dcp::VerificationNote::Code::INVALID_SUBTITLE_FIRST_TEXT_TIME,
+ dcp::VerificationNote::Code::INVALID_SUBTITLE_SPACING,
+ dcp::VerificationNote::Code::EXTERNAL_ASSET,
+ };
+
+ verify_dcp(vf->dir(vf->dcp_name()), ignore);
+}
+
+
+/* Test bug #2703: a VF that refers to some OV subs does not get the correct subtitle language in the ISDCF name */
+BOOST_AUTO_TEST_CASE(ov_subs_in_vf_name)
+{
+ auto subs = content_factory("test/data/short.srt")[0];
+ auto ov = new_test_film("ov_subs_in_vf_name_ov", { subs });
+ ov->set_audio_channels(8);
+ subs->only_text()->set_language(dcp::LanguageTag("de"));
+ make_and_verify_dcp(
+ ov,
+ {
+ dcp::VerificationNote::Code::INVALID_SUBTITLE_FIRST_TEXT_TIME,
+ dcp::VerificationNote::Code::MISSING_CPL_METADATA
+ });
+
+ auto ov_dcp = make_shared<DCPContent>(ov->dir(ov->dcp_name()));
+ auto vf = new_test_film("ov_subs_in_vf_name_vf", { ov_dcp });
+ vf->set_name("foo");
+ vf->set_audio_channels(8);
+ ov_dcp->set_reference_text(TextType::OPEN_SUBTITLE, true);
+ vf->_isdcf_date = boost::gregorian::date(2023, boost::gregorian::Jan, 18);
+
+ BOOST_CHECK_EQUAL(vf->isdcf_name(false), "Foo_TST-1_F_XX-DE_51-HI-VI_2K_20230118_SMPTE_VF");
+}
diff --git a/test/lib/video_content_scale_test.cc b/test/lib/video_content_scale_test.cc
new file mode 100644
index 000000000..ed47e81bb
--- /dev/null
+++ b/test/lib/video_content_scale_test.cc
@@ -0,0 +1,146 @@
+/*
+ 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/ratio.h"
+#include "lib/video_content.h"
+#include "../test.h"
+#include <boost/test/unit_test.hpp>
+
+
+static dcp::Size const FOUR_TO_THREE(1436, 1080);
+static dcp::Size const FLAT(1998, 1080);
+static dcp::Size const SCOPE(2048, 858);
+
+
+/* Test VideoContent::scaled_size() without any legacy stuff */
+BOOST_AUTO_TEST_CASE (scaled_size_test1)
+{
+ VideoContent vc (0);
+
+ /* Images at full size and in DCP-approved sizes that will not be scaled */
+ // Flat/scope content into flat/scope container
+ vc._size = FLAT;
+ BOOST_CHECK_EQUAL(*vc.scaled_size(FLAT), FLAT);
+ vc._size = SCOPE;
+ BOOST_CHECK_EQUAL(*vc.scaled_size(SCOPE), SCOPE);
+ // 1.33:1 into flat container
+ vc._size = FOUR_TO_THREE;
+ BOOST_CHECK_EQUAL(*vc.scaled_size(FLAT), dcp::Size(FOUR_TO_THREE));
+ // Scope into flat container
+ vc._size = SCOPE;
+ BOOST_CHECK_EQUAL(*vc.scaled_size(FLAT), dcp::Size(1998, 837));
+
+ /* Smaller images but in the same ratios */
+ vc._size = dcp::Size(185, 100);
+ BOOST_CHECK_EQUAL(*vc.scaled_size(FLAT), FLAT);
+ vc._size = dcp::Size(955, 400);
+ BOOST_CHECK_EQUAL(*vc.scaled_size(SCOPE), SCOPE);
+ // 1.33:1 into flat container
+ vc._size = dcp::Size(133, 100);
+ BOOST_CHECK_EQUAL(*vc.scaled_size(FLAT), dcp::Size(FOUR_TO_THREE));
+ // Scope into flat container
+ vc._size = dcp::Size(239, 100);
+ BOOST_CHECK_EQUAL(*vc.scaled_size(FLAT), dcp::Size(1998, 836));
+
+ /* Images at full size that are not DCP-approved but will still remain unscaled */
+ vc._size = dcp::Size(600, 1080);
+ BOOST_CHECK_EQUAL(*vc.scaled_size(FLAT), dcp::Size(600, 1080));
+ vc._size = dcp::Size(1700, 1080);
+ BOOST_CHECK_EQUAL(*vc.scaled_size(FLAT), dcp::Size(1700, 1080));
+
+ /* Image at full size that is too big for the container and will be shrunk */
+ vc._size = dcp::Size(3000, 1080);
+ BOOST_CHECK_EQUAL(*vc.scaled_size(FLAT), dcp::Size(1998, 719));
+}
+
+
+/* Same as scaled_size_test1 but with a non-unity sample aspect ratio */
+BOOST_AUTO_TEST_CASE (scaled_size_test2)
+{
+ VideoContent vc (0);
+
+ vc._sample_aspect_ratio = 2;
+
+ /* Images at full size and in DCP-approved sizes that will not be scaled */
+ // Flat/scope content into flat/scope container
+ vc._size = dcp::Size (1998 / 2, 1080);
+ BOOST_CHECK_EQUAL(*vc.scaled_size(FLAT), FLAT);
+ vc._size = dcp::Size (2048 / 2, 858);
+ BOOST_CHECK_EQUAL(*vc.scaled_size(SCOPE), SCOPE);
+ // 1.33:1 into flat container
+ vc._size = dcp::Size (1436 / 2, 1080);
+ BOOST_CHECK_EQUAL(*vc.scaled_size(FLAT), dcp::Size(FOUR_TO_THREE));
+ // Scope into flat container
+ vc._size = dcp::Size (2048 / 2, 858);
+ BOOST_CHECK_EQUAL(*vc.scaled_size(FLAT), dcp::Size(1998, 837));
+
+ /* Smaller images but in the same ratios */
+ vc._size = dcp::Size(185, 200);
+ BOOST_CHECK_EQUAL(*vc.scaled_size(FLAT), FLAT);
+ vc._size = dcp::Size(955, 800);
+ BOOST_CHECK_EQUAL(*vc.scaled_size(SCOPE), SCOPE);
+ // 4:3 into flat container
+ vc._size = dcp::Size(133, 200);
+ BOOST_CHECK_EQUAL(*vc.scaled_size(FLAT), dcp::Size(FOUR_TO_THREE));
+ // Scope into flat container
+ vc._size = dcp::Size(239, 200);
+ BOOST_CHECK_EQUAL(*vc.scaled_size(FLAT), dcp::Size(1998, 836));
+
+ /* Images at full size that are not DCP-approved but will still remain unscaled */
+ vc._size = dcp::Size(600 / 2, 1080);
+ BOOST_CHECK_EQUAL(*vc.scaled_size(FLAT), dcp::Size(600, 1080));
+ vc._size = dcp::Size(1700 / 2, 1080);
+ BOOST_CHECK_EQUAL(*vc.scaled_size(FLAT), dcp::Size(1700, 1080));
+
+ /* Image at full size that is too big for the container and will be shrunk */
+ vc._size = dcp::Size(3000 / 2, 1080);
+ BOOST_CHECK_EQUAL(*vc.scaled_size(FLAT), dcp::Size(1998, 719));
+}
+
+
+/* Test VideoContent::scaled_size() with some legacy stuff */
+BOOST_AUTO_TEST_CASE (scaled_size_legacy_test)
+{
+ {
+ /* 640x480 content that the user had asked to be stretched to 1.85:1 */
+ VideoContent vc (0);
+ vc._size = dcp::Size(640, 480);
+ vc._legacy_ratio = Ratio::from_id("185")->ratio();
+ BOOST_CHECK_EQUAL(*vc.scaled_size(FLAT), FLAT);
+ }
+
+ {
+ /* 640x480 content that the user had asked to be scaled to fit the container, without stretch */
+ VideoContent vc (0);
+ vc._size = dcp::Size(640, 480);
+ vc._legacy_ratio = 1.33;
+ BOOST_CHECK_EQUAL(*vc.scaled_size(FLAT), FOUR_TO_THREE);
+ }
+
+ {
+ /* 640x480 content that the user had asked to be kept the same size */
+ VideoContent vc (0);
+ vc._size = dcp::Size(640, 480);
+ vc._custom_size = dcp::Size(640, 480);
+ BOOST_CHECK_EQUAL(*vc.scaled_size(FLAT), dcp::Size(640, 480));
+ }
+}
+
diff --git a/test/lib/video_content_test.cc b/test/lib/video_content_test.cc
new file mode 100644
index 000000000..5458bea3f
--- /dev/null
+++ b/test/lib/video_content_test.cc
@@ -0,0 +1,53 @@
+/*
+ Copyright (C) 2025 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/content_factory.h"
+#include "lib/dcpomatic_time.h"
+#include "lib/video_content.h"
+#include "../test.h"
+#include <boost/test/unit_test.hpp>
+
+
+BOOST_AUTO_TEST_CASE(video_content_fade_test)
+{
+ auto content = content_factory("test/data/flat_red.png")[0];
+ auto film = new_test_film("video_content_fade_test", { content });
+
+ content->video->set_length(240);
+ content->set_trim_start(film, dcpomatic::ContentTime::from_frames(24, 24));
+ content->video->set_fade_in(15);
+ content->video->set_fade_out(4);
+
+ /* Before fade-in */
+ BOOST_CHECK(content->video->fade(film, dcpomatic::ContentTime::from_frames(24 - 12, 24)).get_value_or(-99) == 0);
+ /* Start of fade-in */
+ BOOST_CHECK(content->video->fade(film, dcpomatic::ContentTime::from_frames(24, 24)).get_value_or(-99) == 0);
+ /* During fade-in */
+ BOOST_CHECK(content->video->fade(film, dcpomatic::ContentTime::from_frames(24 + 13, 24)).get_value_or(-99) > 0);
+ BOOST_CHECK(content->video->fade(film, dcpomatic::ContentTime::from_frames(24 + 13, 24)).get_value_or(-99) < 1);
+ /* After fade-in */
+ BOOST_CHECK(!static_cast<bool>(content->video->fade(film, dcpomatic::ContentTime::from_frames(24 + 55, 24))));
+ /* During fade-out */
+ BOOST_CHECK(content->video->fade(film, dcpomatic::ContentTime::from_frames(240 - 16, 24)).get_value_or(-90) <= 1);
+ /* After fade-out */
+ BOOST_CHECK(content->video->fade(film, dcpomatic::ContentTime::from_frames(240 + 20, 24)).get_value_or(-90) >= 0);
+}
+
diff --git a/test/lib/video_level_test.cc b/test/lib/video_level_test.cc
new file mode 100644
index 000000000..dc6c815c9
--- /dev/null
+++ b/test/lib/video_level_test.cc
@@ -0,0 +1,590 @@
+/*
+ Copyright (C) 2020-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/>.
+
+*/
+
+
+/** @file test/video_level_test.cc
+ * @brief Test that video level ranges are handled correctly.
+ * @ingroup feature
+ */
+
+
+#include "lib/content_factory.h"
+#include "lib/content_video.h"
+#include "lib/dcp_content.h"
+#include "lib/decoder_factory.h"
+#include "lib/film.h"
+#include "lib/ffmpeg_content.h"
+#include "lib/ffmpeg_decoder.h"
+#include "lib/ffmpeg_image_proxy.h"
+#include "lib/image.h"
+#include "lib/image_content.h"
+#include "lib/image_decoder.h"
+#include "lib/ffmpeg_film_encoder.h"
+#include "lib/job_manager.h"
+#include "lib/player.h"
+#include "lib/player_video.h"
+#include "lib/transcode_job.h"
+#include "lib/video_decoder.h"
+#include "../test.h"
+#include <dcp/cpl.h>
+#include <dcp/dcp.h>
+#include <dcp/mono_j2k_picture_asset.h>
+#include <dcp/mono_j2k_picture_frame.h>
+#include <dcp/openjpeg_image.h>
+#include <dcp/reel.h>
+#include <dcp/reel_picture_asset.h>
+#include <boost/test/unit_test.hpp>
+
+
+using std::min;
+using std::max;
+using std::pair;
+using std::string;
+using std::dynamic_pointer_cast;
+using std::make_shared;
+using boost::optional;
+#if BOOST_VERSION >= 106100
+using namespace boost::placeholders;
+#endif
+using std::shared_ptr;
+
+
+static
+shared_ptr<Image>
+grey_image (dcp::Size size, uint8_t pixel)
+{
+ auto grey = make_shared<Image>(AV_PIX_FMT_RGB24, size, Image::Alignment::PADDED);
+ for (int y = 0; y < size.height; ++y) {
+ uint8_t* p = grey->data()[0] + y * grey->stride()[0];
+ for (int x = 0; x < size.width; ++x) {
+ *p++ = pixel;
+ *p++ = pixel;
+ *p++ = pixel;
+ }
+ }
+
+ return grey;
+}
+
+
+BOOST_AUTO_TEST_CASE (ffmpeg_image_full_range_not_changed)
+{
+ dcp::Size size(640, 480);
+ uint8_t const grey_pixel = 128;
+ boost::filesystem::path const file = "build/test/ffmpeg_image_full_range_not_changed.png";
+
+ write_image (grey_image(size, grey_pixel), file);
+
+ FFmpegImageProxy proxy (file);
+ ImageProxy::Result result = proxy.image (Image::Alignment::COMPACT);
+ BOOST_REQUIRE (!result.error);
+
+ for (int y = 0; y < size.height; ++y) {
+ uint8_t* p = result.image->data()[0] + y * result.image->stride()[0];
+ for (int x = 0; x < size.width; ++x) {
+ BOOST_REQUIRE (*p++ == grey_pixel);
+ }
+ }
+}
+
+
+BOOST_AUTO_TEST_CASE (ffmpeg_image_video_range_expanded)
+{
+ dcp::Size size(1998, 1080);
+ uint8_t const grey_pixel = 128;
+ uint8_t const expanded_grey_pixel = static_cast<uint8_t>(lrintf((grey_pixel - 16) * 256.0 / 219));
+ boost::filesystem::path const file = "build/test/ffmpeg_image_video_range_expanded.png";
+
+ write_image(grey_image(size, grey_pixel), file);
+
+ auto content = content_factory(file);
+ auto film = new_test_film("ffmpeg_image_video_range_expanded", content);
+ content[0]->video->set_range (VideoRange::VIDEO);
+ auto player = make_shared<Player>(film, film->playlist(), false);
+
+ shared_ptr<PlayerVideo> player_video;
+ player->Video.connect([&player_video](shared_ptr<PlayerVideo> pv, dcpomatic::DCPTime) {
+ player_video = pv;
+ });
+ while (!player_video) {
+ BOOST_REQUIRE (!player->pass());
+ }
+
+ auto image = player_video->image ([](AVPixelFormat f) { return f; }, VideoRange::FULL, false);
+
+ for (int y = 0; y < size.height; ++y) {
+ uint8_t* p = image->data()[0] + y * image->stride()[0];
+ for (int x = 0; x < size.width; ++x) {
+ BOOST_REQUIRE_EQUAL (*p++, expanded_grey_pixel);
+ }
+ }
+}
+
+
+BOOST_AUTO_TEST_CASE(yuv_expanded_into_full_rgb)
+{
+ auto convert = [](int y_val, int u_val, int v_val, AVPixelFormat pix_fmt) {
+ auto const size = dcp::Size(640, 480);
+ auto yuv = make_shared<Image>(AV_PIX_FMT_YUVA444P12LE, size, Image::Alignment::PADDED);
+ BOOST_REQUIRE_EQUAL(yuv->planes(), 4);
+ for (int y = 0; y < size.height; ++y) {
+ uint16_t* Y = reinterpret_cast<uint16_t*>(yuv->data()[0] + y * yuv->stride()[0]);
+ uint16_t* U = reinterpret_cast<uint16_t*>(yuv->data()[1] + y * yuv->stride()[1]);
+ uint16_t* V = reinterpret_cast<uint16_t*>(yuv->data()[2] + y * yuv->stride()[2]);
+ uint16_t* A = reinterpret_cast<uint16_t*>(yuv->data()[3] + y * yuv->stride()[3]);
+ for (int x = 0; x < size.width; ++x) {
+ *Y++ = y_val;
+ *U++ = u_val;
+ *V++ = v_val;
+ *A++ = 4096;
+ }
+ }
+
+ return yuv->crop_scale_window(
+ Crop(), size, size, dcp::YUVToRGB::REC709,
+ VideoRange::VIDEO,
+ pix_fmt,
+ VideoRange::FULL,
+ Image::Alignment::COMPACT,
+ false
+ );
+ };
+
+ auto white24 = convert(3760, 2048, 2048, AV_PIX_FMT_RGB24);
+ BOOST_CHECK_EQUAL(white24->data()[0][0], 255);
+ BOOST_CHECK_EQUAL(white24->data()[0][1], 255);
+ BOOST_CHECK_EQUAL(white24->data()[0][2], 255);
+
+ auto black24 = convert(256, 2048, 2048, AV_PIX_FMT_RGB24);
+ BOOST_CHECK_EQUAL(black24->data()[0][0], 0);
+ BOOST_CHECK_EQUAL(black24->data()[0][1], 0);
+ BOOST_CHECK_EQUAL(black24->data()[0][2], 0);
+
+ auto white48 = convert(3760, 2048, 2048, AV_PIX_FMT_RGB48LE);
+ BOOST_CHECK_EQUAL(reinterpret_cast<uint16_t*>(white48->data()[0])[0], 65283);
+ BOOST_CHECK_EQUAL(reinterpret_cast<uint16_t*>(white48->data()[0])[1], 65283);
+ BOOST_CHECK_EQUAL(reinterpret_cast<uint16_t*>(white48->data()[0])[2], 65283);
+
+ auto black48 = convert(256, 2048, 2048, AV_PIX_FMT_RGB48LE);
+ BOOST_CHECK_EQUAL(reinterpret_cast<uint16_t*>(black48->data()[0])[0], 0);
+ BOOST_CHECK_EQUAL(reinterpret_cast<uint16_t*>(black48->data()[0])[1], 0);
+ BOOST_CHECK_EQUAL(reinterpret_cast<uint16_t*>(black48->data()[0])[2], 0);
+}
+
+
+static
+pair<int, int>
+pixel_range (shared_ptr<const Image> image)
+{
+ pair<int, int> range(INT_MAX, 0);
+ switch (image->pixel_format()) {
+ case AV_PIX_FMT_RGB24:
+ {
+ dcp::Size const size = image->sample_size(0);
+ for (int y = 0; y < size.height; ++y) {
+ uint8_t* p = image->data()[0] + y * image->stride()[0];
+ for (int x = 0; x < size.width * 3; ++x) {
+ range.first = min(range.first, static_cast<int>(*p));
+ range.second = max(range.second, static_cast<int>(*p));
+ ++p;
+ }
+ }
+ break;
+ }
+ case AV_PIX_FMT_YUV444P:
+ {
+ for (int c = 0; c < 3; ++c) {
+ dcp::Size const size = image->sample_size(c);
+ for (int y = 0; y < size.height; ++y) {
+ uint8_t* p = image->data()[c] + y * image->stride()[c];
+ for (int x = 0; x < size.width; ++x) {
+ range.first = min(range.first, static_cast<int>(*p));
+ range.second = max(range.second, static_cast<int>(*p));
+ ++p;
+ }
+ }
+ }
+ break;
+ }
+ case AV_PIX_FMT_YUV422P10LE:
+ case AV_PIX_FMT_YUV444P10LE:
+ case AV_PIX_FMT_YUV444P12LE:
+ {
+ for (int c = 0; c < 3; ++c) {
+ dcp::Size const size = image->sample_size(c);
+ for (int y = 0; y < size.height; ++y) {
+ uint16_t* p = reinterpret_cast<uint16_t*>(image->data()[c]) + y * image->stride()[c] / 2;
+ for (int x = 0; x < size.width; ++x) {
+ range.first = min(range.first, static_cast<int>(*p));
+ range.second = max(range.second, static_cast<int>(*p));
+ ++p;
+ }
+ }
+ }
+ break;
+ }
+ default:
+ BOOST_REQUIRE_MESSAGE (false, "No support for pixel format " << image->pixel_format());
+ }
+
+ return range;
+}
+
+
+/** @return pixel range of the first frame in @ref content in its raw form, i.e.
+ * straight out of the decoder with no level processing, scaling etc.
+ */
+static
+pair<int, int>
+pixel_range (shared_ptr<const Film> film, shared_ptr<const Content> content)
+{
+ auto decoder = decoder_factory(film, content, false, false, shared_ptr<Decoder>());
+ optional<ContentVideo> content_video;
+ decoder->video->Data.connect ([&content_video](ContentVideo cv) {
+ content_video = cv;
+ });
+ while (!content_video) {
+ BOOST_REQUIRE (!decoder->pass());
+ }
+
+ return pixel_range (content_video->image->image(Image::Alignment::COMPACT).image);
+}
+
+
+static
+pair<int, int>
+pixel_range (boost::filesystem::path dcp_path)
+{
+ dcp::DCP dcp (dcp_path);
+ dcp.read ();
+
+ auto picture = dynamic_pointer_cast<dcp::MonoJ2KPictureAsset>(dcp.cpls().front()->reels().front()->main_picture()->asset());
+ BOOST_REQUIRE (picture);
+ auto frame = picture->start_read()->get_frame(0)->xyz_image();
+
+ int const width = frame->size().width;
+ int const height = frame->size().height;
+
+ pair<int, int> range(INT_MAX, 0);
+ for (int c = 0; c < 3; ++c) {
+ for (int y = 0; y < height; ++y) {
+ int* p = frame->data(c) + y * width;
+ for (int x = 0; x < width; ++x) {
+ range.first = min(range.first, *p);
+ range.second = max(range.second, *p);
+ ++p;
+ }
+ }
+ }
+
+ return range;
+}
+
+
+/* Functions to make a Film with different sorts of content.
+ *
+ * In these names V = video range (limited)
+ * F = full range (not limited)
+ * o = overridden
+ */
+
+
+static
+shared_ptr<Film>
+movie_V (string name)
+{
+ auto film = new_test_film(name);
+ auto content = dynamic_pointer_cast<FFmpegContent>(content_factory("test/data/rgb_grey_testcard.mp4")[0]);
+ BOOST_REQUIRE (content);
+ film->examine_and_add_content (content);
+ BOOST_REQUIRE (!wait_for_jobs());
+
+ auto range = pixel_range (film, content);
+ BOOST_CHECK_EQUAL (range.first, 15);
+ BOOST_CHECK_EQUAL (range.second, 243);
+
+ return film;
+}
+
+
+/** @return Film containing video-range content set as full-range */
+static
+shared_ptr<Film>
+movie_VoF (string name)
+{
+ auto film = new_test_film(name);
+ auto content = dynamic_pointer_cast<FFmpegContent>(content_factory("test/data/rgb_grey_testcard.mp4")[0]);
+ BOOST_REQUIRE (content);
+ film->examine_and_add_content (content);
+ BOOST_REQUIRE (!wait_for_jobs());
+ content->video->set_range (VideoRange::FULL);
+
+ auto range = pixel_range (film, content);
+ BOOST_CHECK_EQUAL (range.first, 15);
+ BOOST_CHECK_EQUAL (range.second, 243);
+
+ return film;
+}
+
+
+static
+shared_ptr<Film>
+movie_F (string name)
+{
+ auto film = new_test_film(name);
+ auto content = dynamic_pointer_cast<FFmpegContent>(content_factory("test/data/rgb_grey_testcard.mov")[0]);
+ BOOST_REQUIRE (content);
+ film->examine_and_add_content (content);
+ BOOST_REQUIRE (!wait_for_jobs());
+
+ BOOST_CHECK(content->video->range() == VideoRange::FULL);
+
+ auto range = pixel_range (film, content);
+ BOOST_CHECK_EQUAL (range.first, 0);
+ BOOST_CHECK_EQUAL (range.second, 1023);
+
+ return film;
+}
+
+
+static
+shared_ptr<Film>
+movie_FoV (string name)
+{
+ auto film = new_test_film(name);
+ auto content = dynamic_pointer_cast<FFmpegContent>(content_factory("test/data/rgb_grey_testcard.mov")[0]);
+ BOOST_REQUIRE (content);
+ film->examine_and_add_content (content);
+ BOOST_REQUIRE (!wait_for_jobs());
+ content->video->set_range (VideoRange::VIDEO);
+
+ auto range = pixel_range (film, content);
+ BOOST_CHECK_EQUAL (range.first, 0);
+ BOOST_CHECK_EQUAL (range.second, 1023);
+
+ return film;
+}
+
+
+static
+shared_ptr<Film>
+image_F (string name)
+{
+ auto film = new_test_film(name);
+ auto content = dynamic_pointer_cast<ImageContent>(content_factory("test/data/rgb_grey_testcard.png")[0]);
+ BOOST_REQUIRE (content);
+ film->examine_and_add_content (content);
+ BOOST_REQUIRE (!wait_for_jobs());
+
+ auto range = pixel_range (film, content);
+ BOOST_CHECK_EQUAL (range.first, 0);
+ BOOST_CHECK_EQUAL (range.second, 255);
+
+ return film;
+}
+
+
+static
+shared_ptr<Film>
+image_FoV (string name)
+{
+ auto film = new_test_film(name);
+ auto content = dynamic_pointer_cast<ImageContent>(content_factory("test/data/rgb_grey_testcard.png")[0]);
+ BOOST_REQUIRE (content);
+ film->examine_and_add_content (content);
+ BOOST_REQUIRE (!wait_for_jobs());
+ content->video->set_range (VideoRange::VIDEO);
+
+ auto range = pixel_range (film, content);
+ /* We are taking some full-range content and saying it should be read as video range, after which its
+ * pixels will still be full range.
+ */
+ BOOST_CHECK_EQUAL (range.first, 0);
+ BOOST_CHECK_EQUAL (range.second, 255);
+
+ return film;
+}
+
+
+static
+shared_ptr<Film>
+dcp_F (string name)
+{
+ boost::filesystem::path const dcp = "test/data/RgbGreyTestcar_TST-1_F_MOS_2K_20201115_SMPTE_OV";
+ auto film = new_test_film(name);
+ auto content = make_shared<DCPContent>(dcp);
+ film->examine_and_add_content (content);
+ BOOST_REQUIRE (!wait_for_jobs());
+
+ auto range = pixel_range (dcp);
+ BOOST_CHECK_EQUAL (range.first, 0);
+ BOOST_CHECK_EQUAL (range.second, 4081);
+
+ return film;
+}
+
+
+
+/* Functions to get the pixel range in different sorts of output */
+
+
+/** Get the pixel range in a DCP made from film */
+static
+pair<int, int>
+dcp_range (shared_ptr<Film> film)
+{
+ make_and_verify_dcp (film);
+ return pixel_range (film->dir(film->dcp_name()));
+}
+
+
+/** Get the pixel range in a video-range movie exported from film */
+static
+pair<int, int>
+V_movie_range (shared_ptr<Film> film)
+{
+ auto job = make_shared<TranscodeJob>(film, TranscodeJob::ChangedBehaviour::IGNORE);
+ job->set_encoder (
+ make_shared<FFmpegFilmEncoder>(film, job, film->file("export.mov"), ExportFormat::PRORES_HQ, true, false, false, 23)
+ );
+ JobManager::instance()->add (job);
+ BOOST_REQUIRE (!wait_for_jobs());
+
+ /* This is a bit of a hack; add the exported file into the project so we can decode it */
+ auto content = make_shared<FFmpegContent>(film->file("export.mov"));
+ film->examine_and_add_content (content);
+ BOOST_REQUIRE (!wait_for_jobs());
+
+ return pixel_range (film, content);
+}
+
+
+/* The tests */
+
+
+BOOST_AUTO_TEST_CASE (movie_V_to_dcp)
+{
+ auto range = dcp_range (movie_V("movie_V_to_dcp"));
+ /* Video range has been correctly expanded to full for the DCP */
+ check_int_close(range, {0, 4081}, 2);
+}
+
+
+BOOST_AUTO_TEST_CASE (movie_VoF_to_dcp)
+{
+ auto range = dcp_range (movie_VoF("movie_VoF_to_dcp"));
+ /* We said that video range data was really full range, so here we are in the DCP
+ * with video-range data.
+ */
+ check_int_close (range, {350, 3832}, 2);
+}
+
+
+BOOST_AUTO_TEST_CASE (movie_F_to_dcp)
+{
+ auto range = dcp_range (movie_F("movie_F_to_dcp"));
+ /* The nearly-full-range of the input has been preserved */
+ check_int_close(range, {0, 4080}, 2);
+}
+
+
+BOOST_AUTO_TEST_CASE (video_FoV_to_dcp)
+{
+ auto range = dcp_range (movie_FoV("video_FoV_to_dcp"));
+ /* The nearly-full-range of the input has become even more full, and clipped */
+ check_int_close(range, {0, 4093}, 2);
+}
+
+
+BOOST_AUTO_TEST_CASE (image_F_to_dcp)
+{
+ auto range = dcp_range (image_F("image_F_to_dcp"));
+ check_int_close(range, {0, 4080}, 3);
+}
+
+
+BOOST_AUTO_TEST_CASE (image_FoV_to_dcp)
+{
+ auto range = dcp_range (image_FoV("image_FoV_to_dcp"));
+ /* The nearly-full-range of the input has become even more full, and clipped.
+ * XXX: I'm not sure why this doesn't quite hit 4095.
+ */
+ check_int_close (range, {0, 4095}, 16);
+}
+
+
+BOOST_AUTO_TEST_CASE (movie_V_to_V_movie)
+{
+ auto range = V_movie_range (movie_V("movie_V_to_V_movie"));
+ BOOST_CHECK_EQUAL (range.first, 60);
+ BOOST_CHECK_EQUAL (range.second, 998);
+}
+
+
+BOOST_AUTO_TEST_CASE (movie_VoF_to_V_movie)
+{
+ auto range = V_movie_range (movie_VoF("movie_VoF_to_V_movie"));
+ BOOST_CHECK_EQUAL (range.first, 116);
+ BOOST_CHECK_EQUAL (range.second, 939);
+}
+
+
+BOOST_AUTO_TEST_CASE (movie_F_to_V_movie)
+{
+ auto range = V_movie_range (movie_F("movie_F_to_V_movie"));
+ /* A full range input has been converted to video range, so that what was black at 0
+ * is not black at 64 (with the corresponding change to white)
+ */
+ BOOST_CHECK_EQUAL(range.first, 64);
+ BOOST_CHECK_EQUAL(range.second, 963);
+}
+
+
+BOOST_AUTO_TEST_CASE (movie_FoV_to_V_movie)
+{
+ auto range = V_movie_range (movie_FoV("movie_FoV_to_V_movie"));
+ BOOST_CHECK_EQUAL (range.first, 4);
+ BOOST_CHECK_EQUAL (range.second, 1019);
+}
+
+
+BOOST_AUTO_TEST_CASE (image_F_to_V_movie)
+{
+ auto range = V_movie_range (image_F("image_F_to_V_movie"));
+ BOOST_CHECK_EQUAL (range.first, 64);
+ BOOST_CHECK_EQUAL (range.second, 960);
+}
+
+
+BOOST_AUTO_TEST_CASE (image_FoV_to_V_movie)
+{
+ auto range = V_movie_range (image_FoV("image_FoV_to_V_movie"));
+ BOOST_CHECK_EQUAL (range.first, 64);
+ BOOST_CHECK_EQUAL (range.second, 960);
+}
+
+
+BOOST_AUTO_TEST_CASE (dcp_F_to_V_movie)
+{
+ auto range = V_movie_range (dcp_F("dcp_F_to_V_movie"));
+ BOOST_CHECK_EQUAL (range.first, 64);
+ BOOST_CHECK_EQUAL (range.second, 944);
+}
+
diff --git a/test/lib/video_mxf_content_test.cc b/test/lib/video_mxf_content_test.cc
new file mode 100644
index 000000000..af5e7c621
--- /dev/null
+++ b/test/lib/video_mxf_content_test.cc
@@ -0,0 +1,70 @@
+/*
+ Copyright (C) 2016-2021 Carl Hetherington <cth@carlh.net>
+
+ This file is part of DCP-o-matic.
+
+ DCP-o-matic is free software; you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation; either version 2 of the License, or
+ (at your option) any later version.
+
+ DCP-o-matic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with DCP-o-matic. If not, see <http://www.gnu.org/licenses/>.
+
+*/
+
+
+/** @file test/video_mxf_content_test.cc
+ * @brief Test use of Video MXF content.
+ * @ingroup feature
+ */
+
+
+
+#include "lib/content_factory.h"
+#include "lib/dcp_content_type.h"
+#include "lib/film.h"
+#include "lib/ratio.h"
+#include "lib/video_mxf_content.h"
+#include "../test.h"
+#include <dcp/equality_options.h>
+#include <dcp/mono_j2k_picture_asset.h>
+#include <boost/test/unit_test.hpp>
+
+
+using std::dynamic_pointer_cast;
+using std::make_shared;
+
+
+static void note (dcp::NoteType, std::string)
+{
+
+}
+
+
+/** Basic test of using video MXF content */
+BOOST_AUTO_TEST_CASE (video_mxf_content_test)
+{
+ auto const ref_mxf = find_file("test/data/scaling_test_185_185", "j2c");
+ auto content = content_factory(ref_mxf);
+
+ auto film = new_test_film("video_mxf_content_test", content);
+ make_and_verify_dcp (
+ film,
+ {
+ dcp::VerificationNote::Code::MISSING_FFEC_IN_FEATURE,
+ dcp::VerificationNote::Code::MISSING_FFMC_IN_FEATURE,
+ dcp::VerificationNote::Code::INVALID_JPEG2000_GUARD_BITS_FOR_2K
+ });
+
+ auto ref = make_shared<dcp::MonoJ2KPictureAsset>(ref_mxf);
+ auto comp_mxf = find_file(film->file(film->dcp_name()), "j2c_");
+ auto comp = make_shared<dcp::MonoJ2KPictureAsset>(comp_mxf);
+ dcp::EqualityOptions op;
+ BOOST_CHECK (ref->equals (comp, op, note));
+}
diff --git a/test/lib/video_trim_test.cc b/test/lib/video_trim_test.cc
new file mode 100644
index 000000000..8952d0cd5
--- /dev/null
+++ b/test/lib/video_trim_test.cc
@@ -0,0 +1,57 @@
+/*
+ Copyright (C) 2025 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/content_factory.h"
+#include "lib/dcpomatic_time.h"
+#include "lib/image_png.h"
+#include "lib/player.h"
+#include "lib/player_video.h"
+#include "../test.h"
+#include <boost/test/unit_test.hpp>
+
+
+using std::make_shared;
+using std::shared_ptr;
+
+
+
+BOOST_AUTO_TEST_CASE(video_trim_test)
+{
+ auto content = content_factory("test/data/count300bd24.m2ts")[0];
+ auto film = new_test_film("trim_video_test", { content });
+
+ content->set_trim_start(film, dcpomatic::ContentTime::from_frames(8, 24));
+
+ shared_ptr<PlayerVideo> first_video;
+
+ auto player = make_shared<Player>(film, Image::Alignment::COMPACT, false);
+ player->Video.connect([&first_video](shared_ptr<PlayerVideo> video, dcpomatic::DCPTime) {
+ first_video = video;
+ });
+
+ while (!first_video) {
+ BOOST_REQUIRE(!player->pass());
+ }
+
+ image_as_png(first_video->image([](AVPixelFormat) { return AV_PIX_FMT_RGB24; }, VideoRange::FULL, true)).write("build/test/video_trim_test.png");
+ check_image("test/data/video_trim_test.png", "build/test/video_trim_test.png");
+}
+
diff --git a/test/lib/writer_test.cc b/test/lib/writer_test.cc
new file mode 100644
index 000000000..36f319f9e
--- /dev/null
+++ b/test/lib/writer_test.cc
@@ -0,0 +1,155 @@
+/*
+ Copyright (C) 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 "lib/audio_buffers.h"
+#include "lib/content.h"
+#include "lib/content_factory.h"
+#include "lib/cross.h"
+#include "lib/dcp_film_encoder.h"
+#include "lib/film.h"
+#include "lib/job.h"
+#include "lib/video_content.h"
+#include "lib/writer.h"
+#include "../test.h"
+#include <dcp/openjpeg_image.h>
+#include <dcp/j2k_transcode.h>
+#include <boost/test/unit_test.hpp>
+#include <memory>
+
+
+using std::make_shared;
+using std::shared_ptr;
+using std::string;
+using std::vector;
+
+
+BOOST_AUTO_TEST_CASE (test_write_odd_amount_of_silence)
+{
+ auto content = content_factory("test/data/flat_red.png");
+ auto film = new_test_film("test_write_odd_amount_of_silence", content);
+ content[0]->video->set_length(24);
+ auto writer = make_shared<Writer>(film, shared_ptr<Job>(), "foo");
+
+ auto audio = make_shared<AudioBuffers>(6, 48000);
+ audio->make_silent ();
+ writer->write (audio, dcpomatic::DCPTime(1));
+}
+
+
+BOOST_AUTO_TEST_CASE (interrupt_writer)
+{
+ Cleanup cl;
+
+ auto film = new_test_film("test_interrupt_writer", {}, &cl);
+
+ auto content = content_factory("test/data/check_image0.png")[0];
+ film->examine_and_add_content (content);
+ BOOST_REQUIRE (!wait_for_jobs());
+
+ /* Add some dummy content to the film so that it has a reel of the right length */
+ auto constexpr frames = 24 * 60;
+ content->video->set_length (frames);
+
+ /* Make a random J2K image */
+ auto size = dcp::Size(1998, 1080);
+ auto image = make_shared<dcp::OpenJPEGImage>(size);
+ for (int i = 0; i < 3; ++i) {
+ for (int j = 0; j < (size.width * size.height); ++j) {
+ image->data(i)[j] = rand() % 4095;
+ }
+ }
+
+ /* Write some data */
+ auto video = dcp::compress_j2k(image, 100000000, 24, false, false);
+ auto video_ptr = make_shared<dcp::ArrayData>(video.data(), video.size());
+ auto audio = make_shared<AudioBuffers>(6, 48000 / 24);
+
+ auto writer = make_shared<Writer>(film, shared_ptr<Job>(), film->dir(film->dcp_name()));
+ writer->start ();
+
+ for (int i = 0; i < frames; ++i) {
+ writer->write (video_ptr, i, Eyes::BOTH);
+ writer->write (audio, dcpomatic::DCPTime::from_frames(i, 24));
+ }
+
+ /* Start digest calculations then abort them; there should be no crash or error */
+ boost::thread thread([film, writer]() {
+ writer->finish();
+ });
+
+ dcpomatic_sleep_seconds (1);
+
+ thread.interrupt ();
+ thread.join ();
+
+ dcpomatic_sleep_seconds (1);
+ cl.run ();
+}
+
+
+BOOST_AUTO_TEST_CASE(writer_progress_test)
+{
+ class TestJob : public Job
+ {
+ public:
+ explicit TestJob(shared_ptr<const Film> film)
+ : Job(film)
+ {}
+
+ ~TestJob()
+ {
+ stop_thread();
+ }
+
+ std::string name() const override {
+ return "test";
+ }
+ std::string json_name() const override {
+ return "test";
+ }
+ void run() override {};
+ };
+
+ auto picture1 = content_factory("test/data/flat_red.png")[0];
+ auto picture2 = content_factory("test/data/flat_red.png")[0];
+
+ auto film = new_test_film("writer_progress_test", { picture1, picture2 });
+ film->set_reel_type(ReelType::BY_VIDEO_CONTENT);
+ picture1->video->set_length(240);
+ picture2->video->set_length(240);
+ picture2->set_position(film, dcpomatic::DCPTime::from_seconds(10));
+
+ auto job = std::make_shared<TestJob>(film);
+ job->set_rate_limit_progress(false);
+
+ float last_progress = 0;
+ string last_sub_name;
+ boost::signals2::scoped_connection connection = job->Progress.connect([job, &last_progress, &last_sub_name]() {
+ auto const progress = job->progress().get_value_or(0);
+ BOOST_REQUIRE(job->sub_name() != last_sub_name || progress >= last_progress);
+ last_progress = progress;
+ last_sub_name = job->sub_name();
+ });
+
+ DCPFilmEncoder encoder(film, job);
+ encoder.go();
+}
+
diff --git a/test/lib/wscript b/test/lib/wscript
new file mode 100644
index 000000000..d9b6876a8
--- /dev/null
+++ b/test/lib/wscript
@@ -0,0 +1,207 @@
+#
+# Copyright (C) 2012-2016 Carl Hetherington <cth@carlh.net>
+#
+# This file is part of DCP-o-matic.
+#
+# DCP-o-matic is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# DCP-o-matic is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with DCP-o-matic. If not, see <http://www.gnu.org/licenses/>.
+#
+
+def configure(conf):
+ boost_test_suffix=''
+ if conf.env.TARGET_WINDOWS_64:
+ boost_test_suffix='-mt-x64'
+ elif conf.env.TARGET_WINDOWS_32:
+ boost_test_suffix='-mt-x32'
+
+ conf.check_cfg(package='sndfile', args='--cflags --libs', uselib_store='SNDFILE', mandatory=True)
+
+ conf.check_cxx(fragment="""
+ #define BOOST_TEST_MODULE Config test\n
+ #include <boost/test/unit_test.hpp>\n
+ int main() {}
+ """, msg = 'Checking for boost unit testing library', lib = 'boost_unit_test_framework%s' % boost_test_suffix, uselib_store = 'BOOST_TEST')
+
+def build(bld):
+ obj = bld(features='cxx cxxprogram')
+ obj.name = 'unit-tests'
+ obj.uselib = 'BOOST_TEST BOOST_THREAD BOOST_FILESYSTEM BOOST_DATETIME SNDFILE SAMPLERATE DCP FONTCONFIG CAIROMM PANGOMM XMLPP '
+ obj.uselib += 'AVFORMAT AVFILTER AVCODEC AVUTIL SWSCALE SWRESAMPLE POSTPROC CXML SUB GLIB CURL SSH XMLSEC BOOST_REGEX ICU NETTLE PNG JPEG '
+ obj.uselib += 'LEQM_NRT ZIP SQLITE3 '
+ if bld.env.TARGET_WINDOWS_64 or bld.env.TARGET_WINDOWS_32:
+ obj.uselib += 'WINSOCK2 DBGHELP SHLWAPI MSWSOCK BOOST_LOCALE '
+ if bld.env.TARGET_LINUX:
+ obj.uselib += 'DL '
+ obj.use = 'libdcpomatic2'
+ obj.source = """
+ 2536_regression_test.cc
+ 2986_regression_test.cc
+ 4k_test.cc
+ analytics_test.cc
+ atmos_test.cc
+ audio_analysis_test.cc
+ audio_buffers_test.cc
+ audio_content_test.cc
+ audio_delay_test.cc
+ audio_filter_test.cc
+ audio_mapping_test.cc
+ audio_merger_test.cc
+ audio_processor_test.cc
+ audio_processor_delay_test.cc
+ audio_ring_buffers_test.cc
+ burnt_subtitle_test.cc
+ butler_test.cc
+ bv20_test.cc
+ cinema_list_test.cc
+ cinema_sound_processor_test.cc
+ client_server_test.cc
+ closed_caption_test.cc
+ collator_test.cc
+ colour_conversion_test.cc
+ config_test.cc
+ content_test.cc
+ copy_dcp_details_to_film_test.cc
+ cpl_hash_test.cc
+ cpl_metadata_test.cc
+ create_cli_test.cc
+ dcpomatic_time_test.cc
+ dcp_decoder_test.cc
+ dcp_digest_file_test.cc
+ dcp_examiner_test.cc
+ dcp_metadata_test.cc
+ dcp_playback_test.cc
+ dcp_subtitle_test.cc
+ digest_test.cc
+ dkdm_recipient_list_test.cc
+ email_test.cc
+ empty_caption_test.cc
+ empty_test.cc
+ encode_cli_test.cc
+ encryption_test.cc
+ file_extension_test.cc
+ ffmpeg_audio_only_test.cc
+ ffmpeg_audio_test.cc
+ ffmpeg_dcp_test.cc
+ ffmpeg_decoder_error_test.cc
+ ffmpeg_decoder_seek_test.cc
+ ffmpeg_decoder_sequential_test.cc
+ ffmpeg_encoder_test.cc
+ ffmpeg_examiner_test.cc
+ ffmpeg_properties_test.cc
+ ffmpeg_pts_offset_test.cc
+ ffmpeg_subtitles_test.cc
+ file_group_test.cc
+ file_log_test.cc
+ file_naming_test.cc
+ filename_charset_test.cc
+ film_test.cc
+ film_metadata_test.cc
+ find_missing_test.cc
+ font_comparator_test.cc
+ font_id_allocator_test.cc
+ frame_interval_checker_test.cc
+ frame_rate_test.cc
+ grok_util_test.cc
+ guess_crop_test.cc
+ hints_test.cc
+ image_content_fade_test.cc
+ image_filename_sorter_test.cc
+ image_test.cc
+ image_proxy_test.cc
+ import_dcp_test.cc
+ interrupt_encoder_test.cc
+ isdcf_name_test.cc
+ j2k_encode_threading_test.cc
+ j2k_encoder_test.cc
+ job_manager_test.cc
+ j2k_video_bit_rate_test.cc
+ kdm_cli_test.cc
+ kdm_naming_test.cc
+ kdm_util_test.cc
+ low_bitrate_test.cc
+ markers_test.cc
+ map_cli_test.cc
+ mca_subdescriptors_test.cc
+ mpeg2_dcp_test.cc
+ no_use_video_test.cc
+ open_caption_test.cc
+ optimise_stills_test.cc
+ overlap_video_test.cc
+ pixel_formats_test.cc
+ player_test.cc
+ playlist_test.cc
+ pulldown_detect_test.cc
+ ratio_test.cc
+ relative_paths_test.cc
+ release_notes_test.cc
+ repeat_frame_test.cc
+ recover_test.cc
+ rect_test.cc
+ reels_test.cc
+ reel_writer_test.cc
+ required_disk_space_test.cc
+ remake_id_test.cc
+ remake_video_test.cc
+ remake_with_subtitle_test.cc
+ render_subtitles_test.cc
+ scaling_test.cc
+ scoped_temporary_test.cc
+ silence_padding_test.cc
+ shuffler_test.cc
+ skip_frame_test.cc
+ socket_test.cc
+ smtp_server.cc
+ srt_subtitle_test.cc
+ ssa_subtitle_test.cc
+ stream_test.cc
+ subtitle_charset_test.cc
+ subtitle_font_id_test.cc
+ subtitle_font_id_change_test.cc
+ subtitle_language_test.cc
+ subtitle_metadata_test.cc
+ subtitle_position_test.cc
+ subtitle_reel_test.cc
+ subtitle_reel_number_test.cc
+ subtitle_timing_test.cc
+ subtitle_trim_test.cc
+ template_test.cc
+ ../test.cc
+ text_decoder_test.cc
+ text_entry_point_test.cc
+ threed_test.cc
+ time_calculation_test.cc
+ torture_test.cc
+ unzipper_test.cc
+ update_checker_test.cc
+ upmixer_a_test.cc
+ util_test.cc
+ vf_test.cc
+ video_content_test.cc
+ video_content_scale_test.cc
+ video_level_test.cc
+ video_mxf_content_test.cc
+ vf_kdm_test.cc
+ writer_test.cc
+ video_trim_test.cc
+ zipper_test.cc
+ """
+
+ if bld.env.TARGET_LINUX and bld.env.ENABLE_DISK:
+ obj.source += " disk_writer_test.cc"
+ obj.uselib += "LWEXT4 NANOMSG "
+
+ # This one doesn't check anything
+ # resampler_test.cc
+
+ obj.target = 'unit-tests'
+ obj.install_path = ''
diff --git a/test/lib/zipper_test.cc b/test/lib/zipper_test.cc
new file mode 100644
index 000000000..f4d6ba3a9
--- /dev/null
+++ b/test/lib/zipper_test.cc
@@ -0,0 +1,75 @@
+/*
+ Copyright (C) 2020-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/>.
+
+*/
+
+
+/** @file test/zipper_test.cc
+ * @brief Test Zipper class.
+ * @ingroup selfcontained
+ */
+
+
+#include "lib/exceptions.h"
+#include "lib/zipper.h"
+#include "../test.h"
+#include <dcp/filesystem.h>
+#include <dcp/util.h>
+#include <boost/test/unit_test.hpp>
+#include <boost/filesystem.hpp>
+
+
+/** Basic test of Zipper working normally */
+BOOST_AUTO_TEST_CASE (zipper_test1)
+{
+ boost::system::error_code ec;
+ boost::filesystem::remove ("build/test/zipped.zip", ec);
+
+ Zipper zipper ("build/test/zipped.zip");
+ zipper.add ("foo.txt", "1234567890");
+ zipper.add ("bar.txt", "xxxxxxCCCCbbbbbbb1");
+ zipper.close ();
+
+ /* Make sure we aren't in a UNC current working directory otherwise the use of cmd.exe
+ * in system() below will fail.
+ */
+ boost::filesystem::current_path(dcp::filesystem::unfix_long_path(boost::filesystem::current_path()));
+
+ boost::filesystem::remove_all ("build/test/zipper_test1", ec);
+#ifdef DCPOMATIC_WINDOWS
+ /* unzip on windows crashes every so often (with a return code -1073740940, for some reason)
+ * so try using the built-in tar which can unzip things.
+ */
+ boost::filesystem::create_directories ("build/test/zipper_test1");
+ int const r = system ("tar -xf build\\test\\zipped.zip -C build\\test\\zipper_test1");
+#else
+ int const r = system ("unzip build/test/zipped.zip -d build/test/zipper_test1");
+#endif
+ BOOST_REQUIRE_EQUAL (r, 0);
+
+ BOOST_CHECK_EQUAL (dcp::file_to_string("build/test/zipper_test1/foo.txt"), "1234567890");
+ BOOST_CHECK_EQUAL (dcp::file_to_string("build/test/zipper_test1/bar.txt"), "xxxxxxCCCCbbbbbbb1");
+}
+
+
+/** Test failure when trying to overwrite a file */
+BOOST_AUTO_TEST_CASE (zipper_test2, * boost::unit_test::depends_on("zipper_test1"))
+{
+ BOOST_CHECK_THROW (Zipper("build/test/zipped.zip"), FileError);
+}
+