diff options
| author | Carl Hetherington <cth@carlh.net> | 2025-05-12 17:05:44 +0200 |
|---|---|---|
| committer | Carl Hetherington <cth@carlh.net> | 2025-05-21 23:50:03 +0200 |
| commit | 6cab56ab466e821d336998cdb6769c864214e1aa (patch) | |
| tree | e28b21ed644f40545eb8de78cabf8ee63faee0fd /test/lib | |
| parent | d84cfe7de28070ea31c9a1c0bd7872ac4be4b773 (diff) | |
Move tests that only need src/lib into test/lib.
Diffstat (limited to 'test/lib')
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 <b> */ +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 & 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 & 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 (¬e, _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 (¬e, _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 (¬e, _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 */ +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 & 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); +} + |
