2 Copyright (C) 2012-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.
34 #define BOOST_TEST_DYN_LINK
35 #define BOOST_TEST_MODULE libdcp_test
36 #include "compose.hpp"
39 #include "interop_subtitle_asset.h"
41 #include "j2k_transcode.h"
42 #include "mono_picture_asset.h"
43 #include "mono_picture_asset.h"
44 #include "openjpeg_image.h"
45 #include "picture_asset_writer.h"
46 #include "picture_asset_writer.h"
48 #include "reel_asset.h"
49 #include "reel_interop_closed_caption_asset.h"
50 #include "reel_interop_subtitle_asset.h"
51 #include "reel_markers_asset.h"
52 #include "reel_mono_picture_asset.h"
53 #include "reel_mono_picture_asset.h"
54 #include "reel_smpte_closed_caption_asset.h"
55 #include "reel_smpte_subtitle_asset.h"
56 #include "reel_sound_asset.h"
57 #include "smpte_subtitle_asset.h"
58 #include "sound_asset.h"
59 #include "sound_asset_writer.h"
63 LIBDCP_DISABLE_WARNINGS
64 #include <asdcp/KM_util.h>
65 #include <asdcp/KM_prng.h>
66 LIBDCP_ENABLE_WARNINGS
68 LIBDCP_DISABLE_WARNINGS
69 #include <libxml++/libxml++.h>
70 LIBDCP_ENABLE_WARNINGS
71 #include <boost/test/unit_test.hpp>
79 using std::shared_ptr;
80 using std::make_shared;
81 using boost::optional;
84 boost::filesystem::path private_test;
85 boost::filesystem::path xsd_test = "build/test/xsd with spaces";
93 if (boost::unit_test::framework::master_test_suite().argc >= 2) {
94 private_test = boost::unit_test::framework::master_test_suite().argv[1];
97 using namespace boost::filesystem;
98 boost::system::error_code ec;
99 remove_all (xsd_test, ec);
100 boost::filesystem::create_directory (xsd_test);
101 for (directory_iterator i = directory_iterator("xsd"); i != directory_iterator(); ++i) {
102 copy_file (*i, xsd_test / i->path().filename());
109 check_xml (xmlpp::Element* ref, xmlpp::Element* test, vector<string> ignore_tags, bool ignore_whitespace)
111 BOOST_CHECK_EQUAL (ref->get_name (), test->get_name ());
112 BOOST_CHECK_EQUAL (ref->get_namespace_prefix (), test->get_namespace_prefix ());
114 if (find(ignore_tags.begin(), ignore_tags.end(), ref->get_name()) != ignore_tags.end()) {
118 auto whitespace_content = [](xmlpp::Node* node) {
119 auto content = dynamic_cast<xmlpp::ContentNode*>(node);
120 return content && content->get_content().find_first_not_of(" \t\r\n") == string::npos;
123 auto ref_children = ref->get_children ();
124 auto test_children = test->get_children ();
126 auto k = ref_children.begin ();
127 auto l = test_children.begin ();
128 while (k != ref_children.end() && l != test_children.end()) {
130 if (dynamic_cast<xmlpp::CommentNode*>(*k)) {
135 if (dynamic_cast<xmlpp::CommentNode*>(*l)) {
140 if (whitespace_content(*k) && ignore_whitespace) {
145 if (whitespace_content(*l) && ignore_whitespace) {
150 /* XXX: should be doing xmlpp::EntityReference, xmlpp::XIncludeEnd, xmlpp::XIncludeStart */
152 auto ref_el = dynamic_cast<xmlpp::Element*> (*k);
153 auto test_el = dynamic_cast<xmlpp::Element*> (*l);
154 BOOST_CHECK ((ref_el && test_el) || (!ref_el && !test_el));
155 if (ref_el && test_el) {
156 check_xml (ref_el, test_el, ignore_tags, ignore_whitespace);
159 auto ref_cn = dynamic_cast<xmlpp::ContentNode*> (*k);
160 auto test_cn = dynamic_cast<xmlpp::ContentNode*> (*l);
161 BOOST_CHECK ((ref_cn && test_cn) || (!ref_cn && !test_cn));
162 if (ref_cn && test_cn) {
163 BOOST_CHECK_EQUAL (ref_cn->get_content(), test_cn->get_content());
170 while (k != ref_children.end() && ignore_whitespace && whitespace_content(*k)) {
174 while (l != test_children.end() && ignore_whitespace && whitespace_content(*l)) {
178 BOOST_REQUIRE (k == ref_children.end());
179 BOOST_REQUIRE (l == test_children.end());
181 auto ref_attributes = ref->get_attributes ();
182 auto test_attributes = test->get_attributes ();
183 BOOST_CHECK_EQUAL (ref_attributes.size(), test_attributes.size ());
185 auto m = ref_attributes.begin();
186 auto n = test_attributes.begin();
187 while (m != ref_attributes.end ()) {
188 BOOST_CHECK_EQUAL ((*m)->get_name(), (*n)->get_name());
189 BOOST_CHECK_EQUAL ((*m)->get_value(), (*n)->get_value());
197 check_xml (string ref, string test, vector<string> ignore, bool ignore_whitespace)
199 auto ref_parser = new xmlpp::DomParser ();
200 ref_parser->parse_memory (ref);
201 auto ref_root = ref_parser->get_document()->get_root_node ();
202 auto test_parser = new xmlpp::DomParser ();
203 test_parser->parse_memory (test);
204 auto test_root = test_parser->get_document()->get_root_node ();
206 check_xml (ref_root, test_root, ignore, ignore_whitespace);
210 check_file (boost::filesystem::path ref, boost::filesystem::path check)
212 uintmax_t size = boost::filesystem::file_size (ref);
213 BOOST_CHECK_EQUAL (size, boost::filesystem::file_size(check));
214 dcp::File ref_file(ref, "rb");
215 BOOST_REQUIRE (ref_file);
216 dcp::File check_file(check, "rb");
217 BOOST_REQUIRE (check_file);
219 int const buffer_size = 65536;
220 std::vector<uint8_t> ref_buffer(buffer_size);
221 std::vector<uint8_t> check_buffer(buffer_size);
226 uintmax_t this_time = min (uintmax_t(buffer_size), size - pos);
227 size_t r = ref_file.read(ref_buffer.data(), 1, this_time);
228 BOOST_CHECK_EQUAL (r, this_time);
229 r = check_file.read(check_buffer.data(), 1, this_time);
230 BOOST_CHECK_EQUAL (r, this_time);
232 if (memcmp(ref_buffer.data(), check_buffer.data(), this_time) != 0) {
233 for (int i = 0; i < buffer_size; ++i) {
234 if (ref_buffer[i] != check_buffer[i]) {
235 BOOST_CHECK_MESSAGE (
237 dcp::String::compose("File %1 differs from reference %2 at offset %3", check, ref, pos + i)
250 RNGFixer::RNGFixer ()
252 Kumu::cth_test = true;
253 Kumu::FortunaRNG().Reset();
257 RNGFixer::~RNGFixer ()
259 Kumu::cth_test = false;
263 shared_ptr<dcp::MonoPictureAsset>
264 simple_picture (boost::filesystem::path path, string suffix, int frames, optional<dcp::Key> key)
266 dcp::MXFMetadata mxf_meta;
267 mxf_meta.company_name = "OpenDCP";
268 mxf_meta.product_name = "OpenDCP";
269 mxf_meta.product_version = "0.0.25";
271 auto mp = make_shared<dcp::MonoPictureAsset>(dcp::Fraction (24, 1), dcp::Standard::SMPTE);
272 mp->set_metadata (mxf_meta);
276 auto picture_writer = mp->start_write(path / dcp::String::compose("video%1.mxf", suffix), dcp::PictureAsset::Behaviour::MAKE_NEW);
278 dcp::Size const size (1998, 1080);
279 auto image = make_shared<dcp::OpenJPEGImage>(size);
280 for (int i = 0; i < 3; ++i) {
281 memset (image->data(i), 0, 2 * size.width * size.height);
283 auto j2c = dcp::compress_j2k (image, 100000000, 24, false, false);
285 for (int i = 0; i < frames; ++i) {
286 picture_writer->write (j2c.data(), j2c.size());
288 picture_writer->finalize ();
294 shared_ptr<dcp::SoundAsset>
295 simple_sound(boost::filesystem::path path, string suffix, dcp::MXFMetadata mxf_meta, string language, int frames, int sample_rate, optional<dcp::Key> key, int channels)
297 /* Set a valid language, then overwrite it, so that the language parameter can be badly formed */
298 auto ms = make_shared<dcp::SoundAsset>(dcp::Fraction(24, 1), sample_rate, channels, dcp::LanguageTag("en-US"), dcp::Standard::SMPTE);
302 ms->_language = language;
303 ms->set_metadata (mxf_meta);
304 auto sound_writer = ms->start_write(path / dcp::String::compose("audio%1.mxf", suffix), {}, dcp::SoundAsset::AtmosSync::DISABLED, dcp::SoundAsset::MCASubDescriptors::ENABLED);
306 int const samples_per_frame = sample_rate / 24;
308 float* silence[channels];
309 for (auto i = 0; i < channels; ++i) {
310 silence[i] = new float[samples_per_frame];
311 memset (silence[i], 0, samples_per_frame * sizeof(float));
314 for (auto i = 0; i < frames; ++i) {
315 sound_writer->write(silence, channels, samples_per_frame);
318 sound_writer->finalize ();
320 for (auto i = 0; i < channels; ++i) {
329 make_simple (boost::filesystem::path path, int reels, int frames, dcp::Standard standard, optional<dcp::Key> key)
331 /* Some known metadata */
332 dcp::MXFMetadata mxf_meta;
333 mxf_meta.company_name = "OpenDCP";
334 mxf_meta.product_name = "OpenDCP";
335 mxf_meta.product_version = "0.0.25";
337 auto constexpr sample_rate = 48000;
339 boost::filesystem::remove_all (path);
340 boost::filesystem::create_directories (path);
341 auto d = make_shared<dcp::DCP>(path);
342 auto cpl = make_shared<dcp::CPL>("A Test DCP", dcp::ContentKind::TRAILER, standard);
343 cpl->set_annotation_text ("A Test DCP");
344 cpl->set_issuer ("OpenDCP 0.0.25");
345 cpl->set_creator ("OpenDCP 0.0.25");
346 cpl->set_issue_date ("2012-07-17T04:45:18+00:00");
347 cpl->set_content_version (
348 dcp::ContentVersion("urn:uuid:75ac29aa-42ac-1234-ecae-49251abefd11", "content-version-label-text")
350 cpl->set_main_sound_configuration(dcp::MainSoundConfiguration("51/L,R,C,LFE,Ls,Rs"));
351 cpl->set_main_sound_sample_rate(sample_rate);
352 cpl->set_main_picture_stored_area(dcp::Size(1998, 1080));
353 cpl->set_main_picture_active_area(dcp::Size(1998, 1080));
354 cpl->set_version_number(1);
356 for (int i = 0; i < reels; ++i) {
357 string suffix = reels == 1 ? "" : dcp::String::compose("%1", i);
359 auto mp = simple_picture (path, suffix, frames, key);
360 auto ms = simple_sound (path, suffix, mxf_meta, "en-US", frames, sample_rate, key);
362 auto reel = make_shared<dcp::Reel>(
363 shared_ptr<dcp::ReelMonoPictureAsset>(new dcp::ReelMonoPictureAsset(mp, 0)),
364 shared_ptr<dcp::ReelSoundAsset>(new dcp::ReelSoundAsset(ms, 0))
367 auto markers = make_shared<dcp::ReelMarkersAsset>(dcp::Fraction(24, 1), frames);
369 markers->set (dcp::Marker::FFOC, dcp::Time(0, 0, 0, 1, 24));
371 if (i == reels - 1) {
372 markers->set (dcp::Marker::LFOC, dcp::Time(0, 0, 0, frames - 1, 24));
379 d->set_annotation_text("A Test DCP");
385 shared_ptr<dcp::Subtitle>
388 return std::make_shared<dcp::SubtitleString>(
393 dcp::Colour(255, 255, 255),
396 dcp::Time(0, 0, 4, 0, 24),
397 dcp::Time(0, 0, 8, 0, 24),
406 dcp::Colour(255, 255, 255),
414 shared_ptr<dcp::ReelMarkersAsset>
415 simple_markers (int frames)
417 auto markers = make_shared<dcp::ReelMarkersAsset>(dcp::Fraction(24, 1), frames);
418 markers->set (dcp::Marker::FFOC, dcp::Time(1, 24, 24));
419 markers->set (dcp::Marker::LFOC, dcp::Time(frames - 1, 24, 24));
425 make_simple_with_interop_subs (boost::filesystem::path path)
427 auto dcp = make_simple (path, 1, 24, dcp::Standard::INTEROP);
429 auto subs = make_shared<dcp::InteropSubtitleAsset>();
430 subs->add (simple_subtitle());
432 boost::filesystem::create_directory (path / "subs");
433 dcp::ArrayData data(4096);
434 memset(data.data(), 0, data.size());
435 subs->add_font ("afont", data);
436 subs->write (path / "subs" / "subs.xml");
438 auto reel_subs = make_shared<dcp::ReelInteropSubtitleAsset>(subs, dcp::Fraction(24, 1), 240, 0);
439 dcp->cpls().front()->reels().front()->add (reel_subs);
446 make_simple_with_smpte_subs (boost::filesystem::path path)
448 auto dcp = make_simple (path, 1, 192);
450 auto subs = make_shared<dcp::SMPTESubtitleAsset>();
451 subs->set_language (dcp::LanguageTag("de-DE"));
452 subs->set_start_time (dcp::Time());
453 subs->add (simple_subtitle());
454 dcp::ArrayData fake_font(1024);
455 subs->add_font("font", fake_font);
457 subs->write (path / "subs.mxf");
459 auto reel_subs = make_shared<dcp::ReelSMPTESubtitleAsset>(subs, dcp::Fraction(24, 1), 192, 0);
460 dcp->cpls().front()->reels().front()->add (reel_subs);
467 make_simple_with_interop_ccaps (boost::filesystem::path path)
469 auto dcp = make_simple (path, 1, 24, dcp::Standard::INTEROP);
471 auto subs = make_shared<dcp::InteropSubtitleAsset>();
472 subs->add (simple_subtitle());
473 subs->write (path / "ccap.xml");
475 auto reel_caps = make_shared<dcp::ReelInteropClosedCaptionAsset>(subs, dcp::Fraction(24, 1), 240, 0);
476 dcp->cpls()[0]->reels()[0]->add (reel_caps);
483 make_simple_with_smpte_ccaps (boost::filesystem::path path)
485 auto dcp = make_simple (path, 1, 192);
487 auto subs = make_shared<dcp::SMPTESubtitleAsset>();
488 subs->set_language (dcp::LanguageTag("de-DE"));
489 subs->set_start_time (dcp::Time());
490 subs->add (simple_subtitle());
491 dcp::ArrayData fake_font(1024);
492 subs->add_font("font", fake_font);
493 subs->write (path / "ccap.mxf");
495 auto reel_caps = make_shared<dcp::ReelSMPTEClosedCaptionAsset>(subs, dcp::Fraction(24, 1), 192, 0);
496 dcp->cpls()[0]->reels()[0]->add(reel_caps);
502 shared_ptr<dcp::OpenJPEGImage>
503 black_image (dcp::Size size)
505 auto image = make_shared<dcp::OpenJPEGImage>(size);
506 int const pixels = size.width * size.height;
507 for (int i = 0; i < 3; ++i) {
508 memset (image->data(i), 0, pixels * sizeof(int));
514 shared_ptr<dcp::ReelAsset>
515 black_picture_asset (boost::filesystem::path dir, int frames)
517 auto image = black_image ();
518 auto frame = dcp::compress_j2k (image, 100000000, 24, false, false);
519 BOOST_REQUIRE (frame.size() < 230000000 / (24 * 8));
521 auto asset = make_shared<dcp::MonoPictureAsset>(dcp::Fraction(24, 1), dcp::Standard::SMPTE);
522 asset->set_metadata (dcp::MXFMetadata("libdcp", "libdcp", "1.6.4devel"));
523 boost::filesystem::create_directories (dir);
524 auto writer = asset->start_write(dir / "pic.mxf", dcp::PictureAsset::Behaviour::MAKE_NEW);
525 for (int i = 0; i < frames; ++i) {
526 writer->write (frame.data(), frame.size());
530 return make_shared<dcp::ReelMonoPictureAsset>(asset, 0);
534 boost::filesystem::path
535 find_file (boost::filesystem::path dir, string filename_part)
537 boost::optional<boost::filesystem::path> found;
538 for (auto i: boost::filesystem::directory_iterator(dir)) {
539 if (i.path().filename().string().find(filename_part) != string::npos) {
540 BOOST_REQUIRE (!found);
544 BOOST_REQUIRE (found);
549 BOOST_GLOBAL_FIXTURE (TestConfig);