2 Copyright (C) 2013-2020 Carl Hetherington <cth@carlh.net>
4 This file is part of libdcp.
6 libdcp is free software; you can redistribute it and/or modify
7 it under the terms of the GNU General Public License as published by
8 the Free Software Foundation; either version 2 of the License, or
9 (at your option) any later version.
11 libdcp is distributed in the hope that it will be useful,
12 but WITHOUT ANY WARRANTY; without even the implied warranty of
13 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 GNU General Public License for more details.
16 You should have received a copy of the GNU General Public License
17 along with libdcp. If not, see <http://www.gnu.org/licenses/>.
19 In addition, as a special exception, the copyright holders give
20 permission to link the code of portions of this program with the
21 OpenSSL library under certain conditions as described in each
22 individual source file, and distribute linked combinations
25 You must obey the GNU General Public License in all respects
26 for all of the code used other than OpenSSL. If you modify
27 file(s) with this exception, you may extend this exception to your
28 version of the file(s), but you are not obligated to do so. If you
29 do not wish to do so, delete this exception statement from your
30 version. If you delete this exception statement from all source
31 files in the program, then also delete it here.
35 #include "atmos_asset.h"
36 #include "compose.hpp"
40 #include "mono_picture_asset.h"
41 #include "picture_asset_writer.h"
43 #include "reel_atmos_asset.h"
44 #include "reel_markers_asset.h"
45 #include "reel_mono_picture_asset.h"
46 #include "reel_picture_asset.h"
47 #include "reel_sound_asset.h"
48 #include "reel_stereo_picture_asset.h"
49 #include "sound_asset.h"
50 #include "sound_asset_writer.h"
51 #include "stereo_picture_asset.h"
53 #include <asdcp/KM_util.h>
55 #include <boost/test/unit_test.hpp>
58 using std::dynamic_pointer_cast;
59 using std::make_shared;
60 using std::shared_ptr;
63 #if BOOST_VERSION >= 106100
64 using namespace boost::placeholders;
68 /** Test creation of a 2D SMPTE DCP from very simple inputs */
69 BOOST_AUTO_TEST_CASE (dcp_test1)
73 auto dcp = make_simple("build/test/DCP/dcp_test1");
74 dcp->set_issuer("OpenDCP 0.0.25");
75 dcp->set_creator("OpenDCP 0.0.25");
76 dcp->set_issue_date("2012-07-17T04:45:18+00:00");
77 dcp->set_annotation_text("A Test DCP");
80 /* build/test/DCP/dcp_test1 is checked against test/ref/DCP/dcp_test1 by run/tests */
83 /** Test creation of a 3D DCP from very simple inputs */
84 BOOST_AUTO_TEST_CASE (dcp_test2)
88 /* Some known metadata */
89 dcp::MXFMetadata mxf_meta;
90 mxf_meta.company_name = "OpenDCP";
91 mxf_meta.product_name = "OpenDCP";
92 mxf_meta.product_version = "0.0.25";
94 /* We're making build/test/DCP/dcp_test2 */
95 boost::filesystem::remove_all ("build/test/DCP/dcp_test2");
96 boost::filesystem::create_directories ("build/test/DCP/dcp_test2");
97 dcp::DCP d ("build/test/DCP/dcp_test2");
98 auto cpl = make_shared<dcp::CPL>("A Test DCP", dcp::ContentKind::FEATURE, dcp::Standard::SMPTE);
99 cpl->set_content_version (
100 dcp::ContentVersion("urn:uri:81fb54df-e1bf-4647-8788-ea7ba154375b_2012-07-17T04:45:18+00:00", "81fb54df-e1bf-4647-8788-ea7ba154375b_2012-07-17T04:45:18+00:00")
102 cpl->set_issuer ("OpenDCP 0.0.25");
103 cpl->set_creator ("OpenDCP 0.0.25");
104 cpl->set_issue_date ("2012-07-17T04:45:18+00:00");
105 cpl->set_annotation_text ("A Test DCP");
107 auto mp = make_shared<dcp::StereoPictureAsset>(dcp::Fraction (24, 1), dcp::Standard::SMPTE);
108 mp->set_metadata (mxf_meta);
109 auto picture_writer = mp->start_write("build/test/DCP/dcp_test2/video.mxf", dcp::PictureAsset::Behaviour::MAKE_NEW);
110 dcp::ArrayData j2c ("test/data/flat_red.j2c");
111 for (int i = 0; i < 24; ++i) {
113 picture_writer->write (j2c.data (), j2c.size ());
115 picture_writer->write (j2c.data (), j2c.size ());
117 picture_writer->finalize ();
119 auto ms = make_shared<dcp::SoundAsset>(dcp::Fraction(24, 1), 48000, 1, dcp::LanguageTag("en-GB"), dcp::Standard::SMPTE);
120 ms->set_metadata (mxf_meta);
121 auto sound_writer = ms->start_write("build/test/DCP/dcp_test2/audio.mxf", {}, dcp::SoundAsset::AtmosSync::DISABLED, dcp::SoundAsset::MCASubDescriptors::ENABLED);
125 auto sndfile = sf_open ("test/data/1s_24-bit_48k_silence.wav", SFM_READ, &info);
126 BOOST_CHECK (sndfile);
127 float buffer[4096*6];
129 channels[0] = buffer;
131 auto N = sf_readf_float (sndfile, buffer, 4096);
132 sound_writer->write(channels, 1, N);
138 sound_writer->finalize ();
140 cpl->add (make_shared<dcp::Reel>(
141 make_shared<dcp::ReelStereoPictureAsset>(mp, 0),
142 make_shared<dcp::ReelSoundAsset>(ms, 0)
147 d.set_issuer("OpenDCP 0.0.25");
148 d.set_creator("OpenDCP 0.0.25");
149 d.set_issue_date("2012-07-17T04:45:18+00:00");
150 d.set_annotation_text("Created by libdcp");
153 /* build/test/DCP/dcp_test2 is checked against test/ref/DCP/dcp_test2 by run/tests */
157 note (dcp::NoteType, string)
162 /** Test comparison of a DCP with itself */
163 BOOST_AUTO_TEST_CASE (dcp_test3)
165 dcp::DCP A ("test/ref/DCP/dcp_test1");
167 dcp::DCP B ("test/ref/DCP/dcp_test1");
170 BOOST_CHECK (A.equals (B, dcp::EqualityOptions(), boost::bind (¬e, _1, _2)));
173 /** Test comparison of a DCP with a different DCP */
174 BOOST_AUTO_TEST_CASE (dcp_test4)
176 dcp::DCP A ("test/ref/DCP/dcp_test1");
178 dcp::DCP B ("test/ref/DCP/dcp_test2");
181 BOOST_CHECK (!A.equals (B, dcp::EqualityOptions(), boost::bind (¬e, _1, _2)));
186 test_rewriting_sound(string name, bool modify)
188 using namespace boost::filesystem;
190 dcp::DCP A ("test/ref/DCP/dcp_test1");
193 BOOST_REQUIRE (!A.cpls().empty());
194 BOOST_REQUIRE (!A.cpls().front()->reels().empty());
195 auto A_picture = dynamic_pointer_cast<dcp::ReelMonoPictureAsset>(A.cpls().front()->reels().front()->main_picture());
196 BOOST_REQUIRE (A_picture);
197 auto A_sound = dynamic_pointer_cast<dcp::ReelSoundAsset>(A.cpls().front()->reels().front()->main_sound());
199 string const picture = "j2c_5279f9aa-94d7-42a6-b0e0-e4eaec4e2a15.mxf";
201 remove_all ("build/test/" + name);
202 dcp::DCP B ("build/test/" + name);
203 auto reel = make_shared<dcp::Reel>();
205 BOOST_REQUIRE (A_picture->mono_asset());
206 BOOST_REQUIRE (A_picture->mono_asset()->file());
207 copy_file (A_picture->mono_asset()->file().get(), path("build") / "test" / name / picture);
208 reel->add(make_shared<dcp::ReelMonoPictureAsset>(make_shared<dcp::MonoPictureAsset>(path("build") / "test" / name / picture), 0));
210 auto reader = A_sound->asset()->start_read();
211 auto sound = make_shared<dcp::SoundAsset>(A_sound->asset()->edit_rate(), A_sound->asset()->sampling_rate(), A_sound->asset()->channels(), dcp::LanguageTag("en-US"), dcp::Standard::SMPTE);
212 auto writer = sound->start_write(path("build") / "test" / name / "pcm_8246f87f-e1df-4c42-a290-f3b3069ff021.mxf", {}, dcp::SoundAsset::AtmosSync::DISABLED, dcp::SoundAsset::MCASubDescriptors::ENABLED);
214 bool need_to_modify = modify;
215 for (int i = 0; i < A_sound->asset()->intrinsic_duration(); ++i) {
216 auto sf = reader->get_frame (i);
217 float* out[sf->channels()];
218 for (int j = 0; j < sf->channels(); ++j) {
219 out[j] = new float[sf->samples()];
221 for (int j = 0; j < sf->samples(); ++j) {
222 for (int k = 0; k < sf->channels(); ++k) {
223 out[k][j] = static_cast<float>(sf->get(k, j)) / (1 << 23);
224 if (need_to_modify) {
225 out[k][j] += 1.0 / (1 << 23);
226 need_to_modify = false;
230 writer->write(out, sf->channels(), sf->samples());
231 for (int j = 0; j < sf->channels(); ++j) {
237 reel->add(make_shared<dcp::ReelSoundAsset>(sound, 0));
238 reel->add(simple_markers());
240 auto cpl = make_shared<dcp::CPL>("A Test DCP", dcp::ContentKind::TRAILER, dcp::Standard::SMPTE);
246 dcp::EqualityOptions eq;
247 eq.reel_hashes_can_differ = true;
248 eq.max_audio_sample_error = 0;
250 BOOST_CHECK (!A.equals(B, eq, boost::bind(¬e, _1, _2)));
252 BOOST_CHECK (A.equals(B, eq, boost::bind(¬e, _1, _2)));
256 /** Test comparison of a DCP with another that has the same picture and the same (but re-written) sound */
257 BOOST_AUTO_TEST_CASE (dcp_test9)
259 test_rewriting_sound ("dcp_test9", false);
262 /** Test comparison of a DCP with another that has the same picture and very slightly modified sound */
263 BOOST_AUTO_TEST_CASE (dcp_test10)
265 test_rewriting_sound ("dcp_test10", true);
268 /** Test creation of a 2D DCP with an Atmos track */
269 BOOST_AUTO_TEST_CASE (dcp_test5)
273 /* Some known metadata */
274 dcp::MXFMetadata mxf_meta;
275 mxf_meta.company_name = "OpenDCP";
276 mxf_meta.product_name = "OpenDCP";
277 mxf_meta.product_version = "0.0.25";
279 /* We're making build/test/DCP/dcp_test5 */
280 boost::filesystem::remove_all ("build/test/DCP/dcp_test5");
281 boost::filesystem::create_directories ("build/test/DCP/dcp_test5");
282 dcp::DCP d ("build/test/DCP/dcp_test5");
283 auto cpl = make_shared<dcp::CPL>("A Test DCP", dcp::ContentKind::FEATURE, dcp::Standard::SMPTE);
284 cpl->set_content_version (
285 dcp::ContentVersion("urn:uri:81fb54df-e1bf-4647-8788-ea7ba154375b_2012-07-17T04:45:18+00:00", "81fb54df-e1bf-4647-8788-ea7ba154375b_2012-07-17T04:45:18+00:00")
287 cpl->set_issuer ("OpenDCP 0.0.25");
288 cpl->set_creator ("OpenDCP 0.0.25");
289 cpl->set_issue_date ("2012-07-17T04:45:18+00:00");
290 cpl->set_annotation_text ("A Test DCP");
292 auto mp = make_shared<dcp::MonoPictureAsset>(dcp::Fraction (24, 1), dcp::Standard::SMPTE);
293 mp->set_metadata (mxf_meta);
294 auto picture_writer = mp->start_write("build/test/DCP/dcp_test5/video.mxf", dcp::PictureAsset::Behaviour::MAKE_NEW);
295 dcp::ArrayData j2c ("test/data/flat_red.j2c");
296 for (int i = 0; i < 24; ++i) {
297 picture_writer->write (j2c.data (), j2c.size ());
299 picture_writer->finalize ();
301 auto ms = make_shared<dcp::SoundAsset>(dcp::Fraction(24, 1), 48000, 1, dcp::LanguageTag("en-GB"), dcp::Standard::SMPTE);
302 ms->set_metadata (mxf_meta);
303 auto sound_writer = ms->start_write("build/test/DCP/dcp_test5/audio.mxf", {}, dcp::SoundAsset::AtmosSync::DISABLED, dcp::SoundAsset::MCASubDescriptors::ENABLED);
307 SNDFILE* sndfile = sf_open ("test/data/1s_24-bit_48k_silence.wav", SFM_READ, &info);
308 BOOST_CHECK (sndfile);
309 float buffer[4096*6];
311 channels[0] = buffer;
313 sf_count_t N = sf_readf_float (sndfile, buffer, 4096);
314 sound_writer->write(channels, 1, N);
320 sound_writer->finalize ();
322 auto am = make_shared<dcp::AtmosAsset>(private_test / "20160218_NameOfFilm_FTR_OV_EN_A_dcs_r01.mxf");
324 cpl->add(make_shared<dcp::Reel>(
325 make_shared<dcp::ReelMonoPictureAsset>(mp, 0),
326 make_shared<dcp::ReelSoundAsset>(ms, 0),
327 shared_ptr<dcp::ReelSubtitleAsset>(),
328 shared_ptr<dcp::ReelMarkersAsset>(),
329 make_shared<dcp::ReelAtmosAsset>(am, 0)
334 d.set_issuer("OpenDCP 0.0.25");
335 d.set_creator("OpenDCP 0.0.25");
336 d.set_issue_date("2012-07-17T04:45:18+00:00");
337 d.set_annotation_text("Created by libdcp");
340 /* build/test/DCP/dcp_test5 is checked against test/ref/DCP/dcp_test5 by run/tests */
343 /** Basic tests of reading a 2D DCP with an Atmos track */
344 BOOST_AUTO_TEST_CASE (dcp_test6)
346 dcp::DCP dcp ("test/ref/DCP/dcp_test5");
349 BOOST_REQUIRE_EQUAL (dcp.cpls().size(), 1U);
350 BOOST_REQUIRE_EQUAL (dcp.cpls()[0]->reels().size(), 1U);
351 BOOST_CHECK (dcp.cpls().front()->reels().front()->main_picture());
352 BOOST_CHECK (dcp.cpls().front()->reels().front()->main_sound());
353 BOOST_CHECK (!dcp.cpls().front()->reels().front()->main_subtitle());
354 BOOST_CHECK (dcp.cpls().front()->reels().front()->atmos());
357 /** Test creation of a 2D Interop DCP from very simple inputs */
358 BOOST_AUTO_TEST_CASE (dcp_test7)
362 auto dcp = make_simple("build/test/DCP/dcp_test7", 1, 24, dcp::Standard::INTEROP);
363 dcp->set_issuer("OpenDCP 0.0.25");
364 dcp->set_creator("OpenDCP 0.0.25");
365 dcp->set_issue_date("2012-07-17T04:45:18+00:00");
366 dcp->set_annotation_text("Created by libdcp");
369 /* build/test/DCP/dcp_test7 is checked against test/ref/DCP/dcp_test7 by run/tests */
372 /** Test reading of a DCP with multiple CPLs */
373 BOOST_AUTO_TEST_CASE (dcp_test8)
375 dcp::DCP dcp (private_test / "data/SMPTE_TST-B1PB2P_S_EN-EN-CCAP_5171-HI-VI_2K_ISDCF_20151123_DPPT_SMPTE_combo/");
378 BOOST_REQUIRE_EQUAL (dcp.cpls().size(), 2U);
382 /** Test reading a DCP whose ASSETMAP contains assets not used by any PKL */
383 BOOST_AUTO_TEST_CASE (dcp_things_in_assetmap_not_in_pkl)
385 dcp::DCP dcp ("test/data/extra_assetmap");
386 BOOST_CHECK_NO_THROW (dcp.read());
390 /** Test that writing the XML for a DCP with no CPLs throws */
391 BOOST_AUTO_TEST_CASE (dcp_with_no_cpls)
393 dcp::DCP dcp ("build/test/dcp_with_no_cpls");
394 BOOST_REQUIRE_THROW (dcp.write_xml(), dcp::MiscError);
398 /** Test that writing the XML for a DCP with Interop CPLs makes a SMPTE assetmap */
399 BOOST_AUTO_TEST_CASE (dcp_with_interop_cpls)
401 boost::filesystem::path path = "build/test/dcp_with_interop_cpls";
402 boost::filesystem::remove_all (path);
404 auto cpl1 = make_shared<dcp::CPL>("", dcp::ContentKind::FEATURE, dcp::Standard::INTEROP);
405 cpl1->add(make_shared<dcp::Reel>());
407 auto cpl2 = make_shared<dcp::CPL>("", dcp::ContentKind::FEATURE, dcp::Standard::INTEROP);
408 cpl2->add(make_shared<dcp::Reel>());
411 BOOST_REQUIRE (boost::filesystem::exists(path / "ASSETMAP"));
412 BOOST_REQUIRE (!boost::filesystem::exists(path / "ASSETMAP.xml"));
416 /** Test that writing the XML for a DCP with SMPTE CPLs makes a SMPTE assetmap */
417 BOOST_AUTO_TEST_CASE (dcp_with_smpte_cpls)
419 boost::filesystem::path path = "build/test/dcp_with_smpte_cpls";
420 boost::filesystem::remove_all (path);
422 auto cpl1 = make_shared<dcp::CPL>("", dcp::ContentKind::FEATURE, dcp::Standard::SMPTE);
423 cpl1->add(make_shared<dcp::Reel>());
425 auto cpl2 = make_shared<dcp::CPL>("", dcp::ContentKind::FEATURE, dcp::Standard::SMPTE);
426 cpl2->add(make_shared<dcp::Reel>());
429 BOOST_REQUIRE (!boost::filesystem::exists(path / "ASSETMAP"));
430 BOOST_REQUIRE (boost::filesystem::exists(path / "ASSETMAP.xml"));
434 /** Test that writing the XML for a DCP with mixed-standard CPLs throws */
435 BOOST_AUTO_TEST_CASE (dcp_with_mixed_cpls)
437 dcp::DCP dcp ("build/test/dcp_with_mixed_cpls");
438 dcp.add(make_shared<dcp::CPL>("", dcp::ContentKind::FEATURE, dcp::Standard::SMPTE));
439 dcp.add(make_shared<dcp::CPL>("", dcp::ContentKind::FEATURE, dcp::Standard::INTEROP));
440 dcp.add(make_shared<dcp::CPL>("", dcp::ContentKind::FEATURE, dcp::Standard::SMPTE));
441 BOOST_REQUIRE_THROW (dcp.write_xml(), dcp::MiscError);
445 BOOST_AUTO_TEST_CASE (dcp_add_kdm_test)
447 /* Some CPLs, each with a reel */
449 shared_ptr<dcp::CPL> cpls[] = {
450 make_shared<dcp::CPL>("", dcp::ContentKind::FEATURE, dcp::Standard::SMPTE),
451 make_shared<dcp::CPL>("", dcp::ContentKind::FEATURE, dcp::Standard::SMPTE),
452 make_shared<dcp::CPL>("", dcp::ContentKind::FEATURE, dcp::Standard::SMPTE)
455 shared_ptr<dcp::Reel> reels[] = {
456 make_shared<dcp::Reel>(),
457 make_shared<dcp::Reel>(),
458 make_shared<dcp::Reel>()
461 for (auto i = 0; i < 3; ++i) {
462 cpls[i]->add(reels[i]);
465 dcp::DCP dcp ("build/test/dcp_add_kdm_test");
470 /* Simple KDM with one key that should be given to cpls[0] */
472 auto kdm_1 = dcp::DecryptedKDM({}, {}, "", "", "");
473 auto kdm_1_uuid = dcp::make_uuid();
474 kdm_1.add_key (dcp::DecryptedKDMKey(string("MDIK"), kdm_1_uuid, dcp::Key(), cpls[0]->id(), dcp::Standard::SMPTE));
476 BOOST_REQUIRE_EQUAL (reels[0]->_kdms.size(), 1U);
477 BOOST_CHECK_EQUAL (reels[0]->_kdms[0].keys().size(), 1U);
478 BOOST_CHECK_EQUAL (reels[0]->_kdms[0].keys()[0].id(), kdm_1_uuid);
479 BOOST_CHECK_EQUAL (reels[1]->_kdms.size(), 0U);
480 BOOST_CHECK_EQUAL (reels[2]->_kdms.size(), 0U);
482 /* KDM with two keys that should be given to cpls[1] and cpls[2] */
484 auto kdm_2 = dcp::DecryptedKDM({}, {}, "", "", "");
485 auto kdm_2_uuid_1 = dcp::make_uuid();
486 kdm_2.add_key (dcp::DecryptedKDMKey(string("MDIK"), kdm_2_uuid_1, dcp::Key(), cpls[1]->id(), dcp::Standard::SMPTE));
487 auto kdm_2_uuid_2 = dcp::make_uuid();
488 kdm_2.add_key (dcp::DecryptedKDMKey(string("MDIK"), kdm_2_uuid_2, dcp::Key(), cpls[2]->id(), dcp::Standard::SMPTE));
490 /* Unchanged from previous test */
491 BOOST_CHECK (reels[0]->_kdms.size() == 1);
492 /* kdm_2 should have been added to both the other CPLs */
493 BOOST_REQUIRE_EQUAL (reels[1]->_kdms.size(), 1U);
494 BOOST_REQUIRE_EQUAL (reels[1]->_kdms[0].keys().size(), 2U);
495 BOOST_CHECK_EQUAL (reels[1]->_kdms[0].keys()[0].id(), kdm_2_uuid_1);
496 BOOST_CHECK_EQUAL (reels[1]->_kdms[0].keys()[1].id(), kdm_2_uuid_2);
497 BOOST_REQUIRE_EQUAL (reels[2]->_kdms.size(), 1U);
498 BOOST_REQUIRE_EQUAL (reels[2]->_kdms[0].keys().size(), 2U);
499 BOOST_CHECK_EQUAL (reels[2]->_kdms[0].keys()[0].id(), kdm_2_uuid_1);
500 BOOST_CHECK_EQUAL (reels[2]->_kdms[0].keys()[1].id(), kdm_2_uuid_2);
504 BOOST_AUTO_TEST_CASE(hashes_preserved_when_loading_corrupted_dcp)
506 boost::filesystem::path const dir = "build/test/hashes_preserved_when_loading_corrupted_dcp";
507 boost::filesystem::remove_all(dir);
509 auto dcp = make_simple(dir / "1");
512 auto asset_1_id = dcp::MonoPictureAsset(dir / "1" / "video.mxf").id();
513 auto asset_1_hash = dcp::MonoPictureAsset(dir / "1" / "video.mxf").hash();
515 /* Replace the hash in the CPL (the one that corresponds to the actual file)
516 * with an incorrect one new_hash.
520 Editor editor(find_file(dir / "1", "cpl_"));
521 auto const after = "<Duration>24</Duration>";
522 editor.delete_lines_after(after, 1);
524 if (asset_1_hash[0] == 'A') {
525 new_hash = 'B' + asset_1_hash.substr(1);
527 new_hash = 'A' + asset_1_hash.substr(1);
530 editor.insert(after, dcp::String::compose(" <Hash>%1</Hash>", new_hash));
533 dcp::DCP read_back(dir / "1");
536 BOOST_REQUIRE_EQUAL(read_back.cpls().size(), 1U);
537 auto cpl = read_back.cpls()[0];
538 BOOST_REQUIRE_EQUAL(cpl->reels().size(), 1U);
539 auto reel = cpl->reels()[0];
540 BOOST_REQUIRE(reel->main_picture());
541 /* Now the asset should think it has the wrong hash written to the PKL file; it shouldn't have
542 * checked the file again.
544 BOOST_CHECK_EQUAL(reel->main_picture()->asset_ref()->hash(), new_hash);