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"
39 #include "equality_options.h"
41 #include "mono_j2k_picture_asset.h"
42 #include "j2k_picture_asset_writer.h"
44 #include "reel_atmos_asset.h"
45 #include "reel_markers_asset.h"
46 #include "reel_mono_picture_asset.h"
47 #include "reel_picture_asset.h"
48 #include "reel_sound_asset.h"
49 #include "reel_stereo_picture_asset.h"
50 #include "sound_asset.h"
51 #include "sound_asset_writer.h"
52 #include "stereo_j2k_picture_asset.h"
54 #include <asdcp/KM_util.h>
56 #include <boost/test/unit_test.hpp>
59 using std::dynamic_pointer_cast;
60 using std::make_shared;
61 using std::shared_ptr;
64 #if BOOST_VERSION >= 106100
65 using namespace boost::placeholders;
69 /** Test creation of a 2D SMPTE DCP from very simple inputs */
70 BOOST_AUTO_TEST_CASE (dcp_test1)
74 auto dcp = make_simple("build/test/DCP/dcp_test1");
75 dcp->set_issuer("OpenDCP 0.0.25");
76 dcp->set_creator("OpenDCP 0.0.25");
77 dcp->set_issue_date("2012-07-17T04:45:18+00:00");
78 dcp->set_annotation_text("A Test DCP");
81 /* build/test/DCP/dcp_test1 is checked against test/ref/DCP/dcp_test1 by run/tests */
84 /** Test creation of a 3D DCP from very simple inputs */
85 BOOST_AUTO_TEST_CASE (dcp_test2)
89 /* Some known metadata */
90 dcp::MXFMetadata mxf_meta;
91 mxf_meta.company_name = "OpenDCP";
92 mxf_meta.product_name = "OpenDCP";
93 mxf_meta.product_version = "0.0.25";
95 /* We're making build/test/DCP/dcp_test2 */
96 boost::filesystem::remove_all ("build/test/DCP/dcp_test2");
97 boost::filesystem::create_directories ("build/test/DCP/dcp_test2");
98 dcp::DCP d ("build/test/DCP/dcp_test2");
99 auto cpl = make_shared<dcp::CPL>("A Test DCP", dcp::ContentKind::FEATURE, dcp::Standard::SMPTE);
100 cpl->set_content_version (
101 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")
103 cpl->set_issuer ("OpenDCP 0.0.25");
104 cpl->set_creator ("OpenDCP 0.0.25");
105 cpl->set_issue_date ("2012-07-17T04:45:18+00:00");
106 cpl->set_annotation_text ("A Test DCP");
108 auto mp = make_shared<dcp::StereoJ2KPictureAsset>(dcp::Fraction (24, 1), dcp::Standard::SMPTE);
109 mp->set_metadata (mxf_meta);
110 auto picture_writer = mp->start_write("build/test/DCP/dcp_test2/video.mxf", dcp::Behaviour::MAKE_NEW);
111 dcp::ArrayData j2c ("test/data/flat_red.j2c");
112 for (int i = 0; i < 24; ++i) {
114 picture_writer->write (j2c.data (), j2c.size ());
116 picture_writer->write (j2c.data (), j2c.size ());
118 picture_writer->finalize ();
120 auto ms = make_shared<dcp::SoundAsset>(dcp::Fraction(24, 1), 48000, 1, dcp::LanguageTag("en-GB"), dcp::Standard::SMPTE);
121 ms->set_metadata (mxf_meta);
122 auto sound_writer = ms->start_write("build/test/DCP/dcp_test2/audio.mxf", {}, dcp::SoundAsset::AtmosSync::DISABLED, dcp::SoundAsset::MCASubDescriptors::ENABLED);
126 auto sndfile = sf_open ("test/data/1s_24-bit_48k_silence.wav", SFM_READ, &info);
127 BOOST_CHECK (sndfile);
128 float buffer[4096*6];
130 channels[0] = buffer;
132 auto N = sf_readf_float (sndfile, buffer, 4096);
133 sound_writer->write(channels, 1, N);
139 sound_writer->finalize ();
141 cpl->add (make_shared<dcp::Reel>(
142 make_shared<dcp::ReelStereoPictureAsset>(mp, 0),
143 make_shared<dcp::ReelSoundAsset>(ms, 0)
148 d.set_issuer("OpenDCP 0.0.25");
149 d.set_creator("OpenDCP 0.0.25");
150 d.set_issue_date("2012-07-17T04:45:18+00:00");
151 d.set_annotation_text("Created by libdcp");
154 /* build/test/DCP/dcp_test2 is checked against test/ref/DCP/dcp_test2 by run/tests */
158 note (dcp::NoteType, string)
163 /** Test comparison of a DCP with itself */
164 BOOST_AUTO_TEST_CASE (dcp_test3)
166 dcp::DCP A ("test/ref/DCP/dcp_test1");
168 dcp::DCP B ("test/ref/DCP/dcp_test1");
171 BOOST_CHECK (A.equals (B, dcp::EqualityOptions(), boost::bind (¬e, _1, _2)));
174 /** Test comparison of a DCP with a different DCP */
175 BOOST_AUTO_TEST_CASE (dcp_test4)
177 dcp::DCP A ("test/ref/DCP/dcp_test1");
179 dcp::DCP B ("test/ref/DCP/dcp_test2");
182 BOOST_CHECK (!A.equals (B, dcp::EqualityOptions(), boost::bind (¬e, _1, _2)));
187 test_rewriting_sound(string name, bool modify)
189 using namespace boost::filesystem;
191 dcp::DCP A ("test/ref/DCP/dcp_test1");
194 BOOST_REQUIRE (!A.cpls().empty());
195 BOOST_REQUIRE (!A.cpls().front()->reels().empty());
196 auto A_picture = dynamic_pointer_cast<dcp::ReelMonoPictureAsset>(A.cpls().front()->reels().front()->main_picture());
197 BOOST_REQUIRE (A_picture);
198 auto A_sound = dynamic_pointer_cast<dcp::ReelSoundAsset>(A.cpls().front()->reels().front()->main_sound());
200 string const picture = "j2c_5279f9aa-94d7-42a6-b0e0-e4eaec4e2a15.mxf";
202 remove_all ("build/test/" + name);
203 dcp::DCP B ("build/test/" + name);
204 auto reel = make_shared<dcp::Reel>();
206 BOOST_REQUIRE(A_picture->mono_j2k_asset());
207 BOOST_REQUIRE(A_picture->mono_j2k_asset()->file());
208 copy_file(A_picture->mono_j2k_asset()->file().get(), path("build") / "test" / name / picture);
209 reel->add(make_shared<dcp::ReelMonoPictureAsset>(make_shared<dcp::MonoJ2KPictureAsset>(path("build") / "test" / name / picture), 0));
211 auto reader = A_sound->asset()->start_read();
212 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);
213 auto writer = sound->start_write(path("build") / "test" / name / "pcm_8246f87f-e1df-4c42-a290-f3b3069ff021.mxf", {}, dcp::SoundAsset::AtmosSync::DISABLED, dcp::SoundAsset::MCASubDescriptors::ENABLED);
215 bool need_to_modify = modify;
216 for (int i = 0; i < A_sound->asset()->intrinsic_duration(); ++i) {
217 auto sf = reader->get_frame (i);
218 float* out[sf->channels()];
219 for (int j = 0; j < sf->channels(); ++j) {
220 out[j] = new float[sf->samples()];
222 for (int j = 0; j < sf->samples(); ++j) {
223 for (int k = 0; k < sf->channels(); ++k) {
224 out[k][j] = static_cast<float>(sf->get(k, j)) / (1 << 23);
225 if (need_to_modify) {
226 out[k][j] += 1.0 / (1 << 23);
227 need_to_modify = false;
231 writer->write(out, sf->channels(), sf->samples());
232 for (int j = 0; j < sf->channels(); ++j) {
238 reel->add(make_shared<dcp::ReelSoundAsset>(sound, 0));
239 reel->add(simple_markers());
241 auto cpl = make_shared<dcp::CPL>("A Test DCP", dcp::ContentKind::TRAILER, dcp::Standard::SMPTE);
247 dcp::EqualityOptions eq;
248 eq.reel_hashes_can_differ = true;
249 eq.max_audio_sample_error = 0;
251 BOOST_CHECK (!A.equals(B, eq, boost::bind(¬e, _1, _2)));
253 BOOST_CHECK (A.equals(B, eq, boost::bind(¬e, _1, _2)));
257 /** Test comparison of a DCP with another that has the same picture and the same (but re-written) sound */
258 BOOST_AUTO_TEST_CASE (dcp_test9)
260 test_rewriting_sound ("dcp_test9", false);
263 /** Test comparison of a DCP with another that has the same picture and very slightly modified sound */
264 BOOST_AUTO_TEST_CASE (dcp_test10)
266 test_rewriting_sound ("dcp_test10", true);
269 /** Test creation of a 2D DCP with an Atmos track */
270 BOOST_AUTO_TEST_CASE (dcp_test5)
274 /* Some known metadata */
275 dcp::MXFMetadata mxf_meta;
276 mxf_meta.company_name = "OpenDCP";
277 mxf_meta.product_name = "OpenDCP";
278 mxf_meta.product_version = "0.0.25";
280 /* We're making build/test/DCP/dcp_test5 */
281 boost::filesystem::remove_all ("build/test/DCP/dcp_test5");
282 boost::filesystem::create_directories ("build/test/DCP/dcp_test5");
283 dcp::DCP d ("build/test/DCP/dcp_test5");
284 auto cpl = make_shared<dcp::CPL>("A Test DCP", dcp::ContentKind::FEATURE, dcp::Standard::SMPTE);
285 cpl->set_content_version (
286 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")
288 cpl->set_issuer ("OpenDCP 0.0.25");
289 cpl->set_creator ("OpenDCP 0.0.25");
290 cpl->set_issue_date ("2012-07-17T04:45:18+00:00");
291 cpl->set_annotation_text ("A Test DCP");
293 auto mp = make_shared<dcp::MonoJ2KPictureAsset>(dcp::Fraction (24, 1), dcp::Standard::SMPTE);
294 mp->set_metadata (mxf_meta);
295 auto picture_writer = mp->start_write("build/test/DCP/dcp_test5/video.mxf", dcp::Behaviour::MAKE_NEW);
296 dcp::ArrayData j2c ("test/data/flat_red.j2c");
297 for (int i = 0; i < 24; ++i) {
298 picture_writer->write (j2c.data (), j2c.size ());
300 picture_writer->finalize ();
302 auto ms = make_shared<dcp::SoundAsset>(dcp::Fraction(24, 1), 48000, 1, dcp::LanguageTag("en-GB"), dcp::Standard::SMPTE);
303 ms->set_metadata (mxf_meta);
304 auto sound_writer = ms->start_write("build/test/DCP/dcp_test5/audio.mxf", {}, dcp::SoundAsset::AtmosSync::DISABLED, dcp::SoundAsset::MCASubDescriptors::ENABLED);
308 SNDFILE* sndfile = sf_open ("test/data/1s_24-bit_48k_silence.wav", SFM_READ, &info);
309 BOOST_CHECK (sndfile);
310 float buffer[4096*6];
312 channels[0] = buffer;
314 sf_count_t N = sf_readf_float (sndfile, buffer, 4096);
315 sound_writer->write(channels, 1, N);
321 sound_writer->finalize ();
323 auto am = make_shared<dcp::AtmosAsset>(private_test / "20160218_NameOfFilm_FTR_OV_EN_A_dcs_r01.mxf");
325 cpl->add(make_shared<dcp::Reel>(
326 make_shared<dcp::ReelMonoPictureAsset>(mp, 0),
327 make_shared<dcp::ReelSoundAsset>(ms, 0),
328 shared_ptr<dcp::ReelSubtitleAsset>(),
329 shared_ptr<dcp::ReelMarkersAsset>(),
330 make_shared<dcp::ReelAtmosAsset>(am, 0)
335 d.set_issuer("OpenDCP 0.0.25");
336 d.set_creator("OpenDCP 0.0.25");
337 d.set_issue_date("2012-07-17T04:45:18+00:00");
338 d.set_annotation_text("Created by libdcp");
341 /* build/test/DCP/dcp_test5 is checked against test/ref/DCP/dcp_test5 by run/tests */
344 /** Basic tests of reading a 2D DCP with an Atmos track */
345 BOOST_AUTO_TEST_CASE (dcp_test6)
347 dcp::DCP dcp ("test/ref/DCP/dcp_test5");
350 BOOST_REQUIRE_EQUAL (dcp.cpls().size(), 1U);
351 BOOST_REQUIRE_EQUAL (dcp.cpls()[0]->reels().size(), 1U);
352 BOOST_CHECK (dcp.cpls().front()->reels().front()->main_picture());
353 BOOST_CHECK (dcp.cpls().front()->reels().front()->main_sound());
354 BOOST_CHECK (!dcp.cpls().front()->reels().front()->main_subtitle());
355 BOOST_CHECK (dcp.cpls().front()->reels().front()->atmos());
358 /** Test creation of a 2D Interop DCP from very simple inputs */
359 BOOST_AUTO_TEST_CASE (dcp_test7)
363 auto dcp = make_simple("build/test/DCP/dcp_test7", 1, 24, dcp::Standard::INTEROP);
364 dcp->set_issuer("OpenDCP 0.0.25");
365 dcp->set_creator("OpenDCP 0.0.25");
366 dcp->set_issue_date("2012-07-17T04:45:18+00:00");
367 dcp->set_annotation_text("Created by libdcp");
370 /* build/test/DCP/dcp_test7 is checked against test/ref/DCP/dcp_test7 by run/tests */
373 /** Test reading of a DCP with multiple CPLs */
374 BOOST_AUTO_TEST_CASE (dcp_test8)
376 dcp::DCP dcp (private_test / "data/SMPTE_TST-B1PB2P_S_EN-EN-CCAP_5171-HI-VI_2K_ISDCF_20151123_DPPT_SMPTE_combo/");
379 BOOST_REQUIRE_EQUAL (dcp.cpls().size(), 2U);
383 /** Test reading a DCP whose ASSETMAP contains assets not used by any PKL */
384 BOOST_AUTO_TEST_CASE (dcp_things_in_assetmap_not_in_pkl)
386 dcp::DCP dcp ("test/data/extra_assetmap");
387 BOOST_CHECK_NO_THROW (dcp.read());
391 /** Test that writing the XML for a DCP with no CPLs throws */
392 BOOST_AUTO_TEST_CASE (dcp_with_no_cpls)
394 dcp::DCP dcp ("build/test/dcp_with_no_cpls");
395 BOOST_REQUIRE_THROW (dcp.write_xml(), dcp::MiscError);
399 /** Test that writing the XML for a DCP with Interop CPLs makes a SMPTE assetmap */
400 BOOST_AUTO_TEST_CASE (dcp_with_interop_cpls)
402 boost::filesystem::path path = "build/test/dcp_with_interop_cpls";
403 boost::filesystem::remove_all (path);
405 auto cpl1 = make_shared<dcp::CPL>("", dcp::ContentKind::FEATURE, dcp::Standard::INTEROP);
406 cpl1->add(make_shared<dcp::Reel>());
408 auto cpl2 = make_shared<dcp::CPL>("", dcp::ContentKind::FEATURE, dcp::Standard::INTEROP);
409 cpl2->add(make_shared<dcp::Reel>());
412 BOOST_REQUIRE (boost::filesystem::exists(path / "ASSETMAP"));
413 BOOST_REQUIRE (!boost::filesystem::exists(path / "ASSETMAP.xml"));
417 /** Test that writing the XML for a DCP with SMPTE CPLs makes a SMPTE assetmap */
418 BOOST_AUTO_TEST_CASE (dcp_with_smpte_cpls)
420 boost::filesystem::path path = "build/test/dcp_with_smpte_cpls";
421 boost::filesystem::remove_all (path);
423 auto cpl1 = make_shared<dcp::CPL>("", dcp::ContentKind::FEATURE, dcp::Standard::SMPTE);
424 cpl1->add(make_shared<dcp::Reel>());
426 auto cpl2 = make_shared<dcp::CPL>("", dcp::ContentKind::FEATURE, dcp::Standard::SMPTE);
427 cpl2->add(make_shared<dcp::Reel>());
430 BOOST_REQUIRE (!boost::filesystem::exists(path / "ASSETMAP"));
431 BOOST_REQUIRE (boost::filesystem::exists(path / "ASSETMAP.xml"));
435 /** Test that writing the XML for a DCP with mixed-standard CPLs throws */
436 BOOST_AUTO_TEST_CASE (dcp_with_mixed_cpls)
438 dcp::DCP dcp ("build/test/dcp_with_mixed_cpls");
439 dcp.add(make_shared<dcp::CPL>("", dcp::ContentKind::FEATURE, dcp::Standard::SMPTE));
440 dcp.add(make_shared<dcp::CPL>("", dcp::ContentKind::FEATURE, dcp::Standard::INTEROP));
441 dcp.add(make_shared<dcp::CPL>("", dcp::ContentKind::FEATURE, dcp::Standard::SMPTE));
442 BOOST_REQUIRE_THROW (dcp.write_xml(), dcp::MiscError);
446 BOOST_AUTO_TEST_CASE (dcp_add_kdm_test)
448 /* Some CPLs, each with a reel */
450 shared_ptr<dcp::CPL> cpls[] = {
451 make_shared<dcp::CPL>("", dcp::ContentKind::FEATURE, dcp::Standard::SMPTE),
452 make_shared<dcp::CPL>("", dcp::ContentKind::FEATURE, dcp::Standard::SMPTE),
453 make_shared<dcp::CPL>("", dcp::ContentKind::FEATURE, dcp::Standard::SMPTE)
456 shared_ptr<dcp::Reel> reels[] = {
457 make_shared<dcp::Reel>(),
458 make_shared<dcp::Reel>(),
459 make_shared<dcp::Reel>()
462 for (auto i = 0; i < 3; ++i) {
463 cpls[i]->add(reels[i]);
466 dcp::DCP dcp ("build/test/dcp_add_kdm_test");
471 /* Simple KDM with one key that should be given to cpls[0] */
473 auto kdm_1 = dcp::DecryptedKDM({}, {}, "", "", "");
474 auto kdm_1_uuid = dcp::make_uuid();
475 kdm_1.add_key (dcp::DecryptedKDMKey(string("MDIK"), kdm_1_uuid, dcp::Key(), cpls[0]->id(), dcp::Standard::SMPTE));
477 BOOST_REQUIRE_EQUAL (reels[0]->_kdms.size(), 1U);
478 BOOST_CHECK_EQUAL (reels[0]->_kdms[0].keys().size(), 1U);
479 BOOST_CHECK_EQUAL (reels[0]->_kdms[0].keys()[0].id(), kdm_1_uuid);
480 BOOST_CHECK_EQUAL (reels[1]->_kdms.size(), 0U);
481 BOOST_CHECK_EQUAL (reels[2]->_kdms.size(), 0U);
483 /* KDM with two keys that should be given to cpls[1] and cpls[2] */
485 auto kdm_2 = dcp::DecryptedKDM({}, {}, "", "", "");
486 auto kdm_2_uuid_1 = dcp::make_uuid();
487 kdm_2.add_key (dcp::DecryptedKDMKey(string("MDIK"), kdm_2_uuid_1, dcp::Key(), cpls[1]->id(), dcp::Standard::SMPTE));
488 auto kdm_2_uuid_2 = dcp::make_uuid();
489 kdm_2.add_key (dcp::DecryptedKDMKey(string("MDIK"), kdm_2_uuid_2, dcp::Key(), cpls[2]->id(), dcp::Standard::SMPTE));
491 /* Unchanged from previous test */
492 BOOST_CHECK (reels[0]->_kdms.size() == 1);
493 /* kdm_2 should have been added to both the other CPLs */
494 BOOST_REQUIRE_EQUAL (reels[1]->_kdms.size(), 1U);
495 BOOST_REQUIRE_EQUAL (reels[1]->_kdms[0].keys().size(), 2U);
496 BOOST_CHECK_EQUAL (reels[1]->_kdms[0].keys()[0].id(), kdm_2_uuid_1);
497 BOOST_CHECK_EQUAL (reels[1]->_kdms[0].keys()[1].id(), kdm_2_uuid_2);
498 BOOST_REQUIRE_EQUAL (reels[2]->_kdms.size(), 1U);
499 BOOST_REQUIRE_EQUAL (reels[2]->_kdms[0].keys().size(), 2U);
500 BOOST_CHECK_EQUAL (reels[2]->_kdms[0].keys()[0].id(), kdm_2_uuid_1);
501 BOOST_CHECK_EQUAL (reels[2]->_kdms[0].keys()[1].id(), kdm_2_uuid_2);
505 BOOST_AUTO_TEST_CASE(hashes_preserved_when_loading_corrupted_dcp)
507 boost::filesystem::path const dir = "build/test/hashes_preserved_when_loading_corrupted_dcp";
508 boost::filesystem::remove_all(dir);
510 auto dcp = make_simple(dir / "1");
513 auto asset_1_id = dcp::MonoJ2KPictureAsset(dir / "1" / "video.mxf").id();
514 auto asset_1_hash = dcp::MonoJ2KPictureAsset(dir / "1" / "video.mxf").hash();
516 /* Replace the hash in the CPL (the one that corresponds to the actual file)
517 * with an incorrect one new_hash.
521 Editor editor(find_file(dir / "1", "cpl_"));
522 auto const after = "<Duration>24</Duration>";
523 editor.delete_lines_after(after, 1);
525 if (asset_1_hash[0] == 'A') {
526 new_hash = 'B' + asset_1_hash.substr(1);
528 new_hash = 'A' + asset_1_hash.substr(1);
531 editor.insert(after, dcp::String::compose(" <Hash>%1</Hash>", new_hash));
534 dcp::DCP read_back(dir / "1");
537 BOOST_REQUIRE_EQUAL(read_back.cpls().size(), 1U);
538 auto cpl = read_back.cpls()[0];
539 BOOST_REQUIRE_EQUAL(cpl->reels().size(), 1U);
540 auto reel = cpl->reels()[0];
541 BOOST_REQUIRE(reel->main_picture());
542 /* Now the asset should think it has the wrong hash written to the PKL file; it shouldn't have
543 * checked the file again.
545 BOOST_CHECK_EQUAL(reel->main_picture()->asset_ref()->hash(), new_hash);