3bebff5322957e4cf63461529a79930c863c16dd
[libdcp.git] / test / test.cc
1 /*
2     Copyright (C) 2012-2020 Carl Hetherington <cth@carlh.net>
3
4     This file is part of libdcp.
5
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.
10
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.
15
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/>.
18
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
23     including the two.
24
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.
32 */
33
34 #define BOOST_TEST_DYN_LINK
35 #define BOOST_TEST_MODULE libdcp_test
36 #include "compose.hpp"
37 #include "cpl.h"
38 #include "dcp.h"
39 #include "interop_subtitle_asset.h"
40 #include "j2k_transcode.h"
41 #include "mono_picture_asset.h"
42 #include "mono_picture_asset.h"
43 #include "openjpeg_image.h"
44 #include "picture_asset_writer.h"
45 #include "picture_asset_writer.h"
46 #include "reel.h"
47 #include "reel_asset.h"
48 #include "reel_closed_caption_asset.h"
49 #include "reel_interop_subtitle_asset.h"
50 #include "reel_markers_asset.h"
51 #include "reel_mono_picture_asset.h"
52 #include "reel_mono_picture_asset.h"
53 #include "reel_smpte_subtitle_asset.h"
54 #include "reel_sound_asset.h"
55 #include "smpte_subtitle_asset.h"
56 #include "sound_asset.h"
57 #include "sound_asset_writer.h"
58 #include "test.h"
59 #include "util.h"
60 #include <asdcp/KM_util.h>
61 #include <asdcp/KM_prng.h>
62 #include <sndfile.h>
63 #include <libxml++/libxml++.h>
64 #include <boost/test/unit_test.hpp>
65 #include <cstdio>
66 #include <iostream>
67
68
69 using std::string;
70 using std::min;
71 using std::vector;
72 using std::shared_ptr;
73 using std::make_shared;
74 using boost::optional;
75
76
77 boost::filesystem::path private_test;
78 boost::filesystem::path xsd_test = "build/test/xsd with spaces";
79
80
81 struct TestConfig
82 {
83         TestConfig()
84         {
85                 dcp::init ();
86                 if (boost::unit_test::framework::master_test_suite().argc >= 2) {
87                         private_test = boost::unit_test::framework::master_test_suite().argv[1];
88                 }
89
90                 using namespace boost::filesystem;
91                 boost::system::error_code ec;
92                 remove_all (xsd_test, ec);
93                 boost::filesystem::create_directory (xsd_test);
94                 for (directory_iterator i = directory_iterator("xsd"); i != directory_iterator(); ++i) {
95                         copy_file (*i, xsd_test / i->path().filename());
96                 }
97         }
98 };
99
100
101 void
102 check_xml (xmlpp::Element* ref, xmlpp::Element* test, vector<string> ignore_tags, bool ignore_whitespace)
103 {
104         BOOST_CHECK_EQUAL (ref->get_name (), test->get_name ());
105         BOOST_CHECK_EQUAL (ref->get_namespace_prefix (), test->get_namespace_prefix ());
106
107         if (find(ignore_tags.begin(), ignore_tags.end(), ref->get_name()) != ignore_tags.end()) {
108                 return;
109         }
110
111         auto whitespace_content = [](xmlpp::Node* node) {
112                 auto content = dynamic_cast<xmlpp::ContentNode*>(node);
113                 return content && content->get_content().find_first_not_of(" \t\r\n") == string::npos;
114         };
115
116         auto ref_children = ref->get_children ();
117         auto test_children = test->get_children ();
118
119         auto k = ref_children.begin ();
120         auto l = test_children.begin ();
121         while (k != ref_children.end() && l != test_children.end()) {
122
123                 if (dynamic_cast<xmlpp::CommentNode*>(*k)) {
124                         ++k;
125                         continue;
126                 }
127
128                 if (dynamic_cast<xmlpp::CommentNode*>(*l)) {
129                         ++l;
130                         continue;
131                 }
132
133                 if (whitespace_content(*k) && ignore_whitespace) {
134                         ++k;
135                         continue;
136                 }
137
138                 if (whitespace_content(*l) && ignore_whitespace) {
139                         ++l;
140                         continue;
141                 }
142
143                 /* XXX: should be doing xmlpp::EntityReference, xmlpp::XIncludeEnd, xmlpp::XIncludeStart */
144
145                 auto ref_el = dynamic_cast<xmlpp::Element*> (*k);
146                 auto test_el = dynamic_cast<xmlpp::Element*> (*l);
147                 BOOST_CHECK ((ref_el && test_el) || (!ref_el && !test_el));
148                 if (ref_el && test_el) {
149                         check_xml (ref_el, test_el, ignore_tags, ignore_whitespace);
150                 }
151
152                 auto ref_cn = dynamic_cast<xmlpp::ContentNode*> (*k);
153                 auto test_cn = dynamic_cast<xmlpp::ContentNode*> (*l);
154                 BOOST_CHECK ((ref_cn && test_cn) || (!ref_cn && !test_cn));
155                 if (ref_cn && test_cn) {
156                         BOOST_CHECK_EQUAL (ref_cn->get_content(), test_cn->get_content());
157                 }
158
159                 ++k;
160                 ++l;
161         }
162
163         while (k != ref_children.end() && ignore_whitespace && whitespace_content(*k)) {
164                 ++k;
165         }
166
167         while (l != test_children.end() && ignore_whitespace && whitespace_content(*l)) {
168                 ++l;
169         }
170
171         BOOST_REQUIRE (k == ref_children.end());
172         BOOST_REQUIRE (l == test_children.end());
173
174         auto ref_attributes = ref->get_attributes ();
175         auto test_attributes = test->get_attributes ();
176         BOOST_CHECK_EQUAL (ref_attributes.size(), test_attributes.size ());
177
178         auto m = ref_attributes.begin();
179         auto n = test_attributes.begin();
180         while (m != ref_attributes.end ()) {
181                 BOOST_CHECK_EQUAL ((*m)->get_name(), (*n)->get_name());
182                 BOOST_CHECK_EQUAL ((*m)->get_value(), (*n)->get_value());
183
184                 ++m;
185                 ++n;
186         }
187 }
188
189 void
190 check_xml (string ref, string test, vector<string> ignore, bool ignore_whitespace)
191 {
192         auto ref_parser = new xmlpp::DomParser ();
193         ref_parser->parse_memory (ref);
194         auto ref_root = ref_parser->get_document()->get_root_node ();
195         auto test_parser = new xmlpp::DomParser ();
196         test_parser->parse_memory (test);
197         auto test_root = test_parser->get_document()->get_root_node ();
198
199         check_xml (ref_root, test_root, ignore, ignore_whitespace);
200 }
201
202 void
203 check_file (boost::filesystem::path ref, boost::filesystem::path check)
204 {
205         uintmax_t size = boost::filesystem::file_size (ref);
206         BOOST_CHECK_EQUAL (size, boost::filesystem::file_size(check));
207         auto ref_file = dcp::fopen_boost (ref, "rb");
208         BOOST_REQUIRE (ref_file);
209         auto check_file = dcp::fopen_boost (check, "rb");
210         BOOST_REQUIRE (check_file);
211
212         int const buffer_size = 65536;
213         auto ref_buffer = new uint8_t[buffer_size];
214         auto check_buffer = new uint8_t[buffer_size];
215
216         uintmax_t pos = 0;
217
218         while (pos < size) {
219                 uintmax_t this_time = min (uintmax_t(buffer_size), size - pos);
220                 size_t r = fread (ref_buffer, 1, this_time, ref_file);
221                 BOOST_CHECK_EQUAL (r, this_time);
222                 r = fread (check_buffer, 1, this_time, check_file);
223                 BOOST_CHECK_EQUAL (r, this_time);
224
225                 if (memcmp(ref_buffer, check_buffer, this_time) != 0) {
226                         for (int i = 0; i < buffer_size; ++i) {
227                                 if (ref_buffer[i] != check_buffer[i]) {
228                                         BOOST_CHECK_MESSAGE (
229                                                 false,
230                                                 dcp::String::compose("File %1 differs from reference %2 at offset %3", check, ref, pos + i)
231                                                 );
232                                         break;
233                                 }
234                         }
235                         break;
236                 }
237
238                 pos += this_time;
239         }
240
241         delete[] ref_buffer;
242         delete[] check_buffer;
243
244         fclose (ref_file);
245         fclose (check_file);
246 }
247
248
249 RNGFixer::RNGFixer ()
250 {
251         Kumu::cth_test = true;
252         Kumu::FortunaRNG().Reset();
253 }
254
255
256 RNGFixer::~RNGFixer ()
257 {
258         Kumu::cth_test = false;
259 }
260
261
262 shared_ptr<dcp::MonoPictureAsset>
263 simple_picture (boost::filesystem::path path, string suffix, int frames)
264 {
265         dcp::MXFMetadata mxf_meta;
266         mxf_meta.company_name = "OpenDCP";
267         mxf_meta.product_name = "OpenDCP";
268         mxf_meta.product_version = "0.0.25";
269
270         shared_ptr<dcp::MonoPictureAsset> mp (new dcp::MonoPictureAsset (dcp::Fraction (24, 1), dcp::Standard::SMPTE));
271         mp->set_metadata (mxf_meta);
272         shared_ptr<dcp::PictureAssetWriter> picture_writer = mp->start_write (path / dcp::String::compose("video%1.mxf", suffix), false);
273
274         dcp::Size const size (1998, 1080);
275         auto image = make_shared<dcp::OpenJPEGImage>(size);
276         for (int i = 0; i < 3; ++i) {
277                 memset (image->data(i), 0, 2 * size.width * size.height);
278         }
279         auto j2c = dcp::compress_j2k (image, 100000000, 24, false, false);
280
281         for (int i = 0; i < frames; ++i) {
282                 picture_writer->write (j2c.data(), j2c.size());
283         }
284         picture_writer->finalize ();
285
286         return mp;
287 }
288
289
290 shared_ptr<dcp::SoundAsset>
291 simple_sound (boost::filesystem::path path, string suffix, dcp::MXFMetadata mxf_meta, string language, int frames, int sample_rate)
292 {
293         int const channels = 6;
294
295         /* Set a valid language, then overwrite it, so that the language parameter can be badly formed */
296         shared_ptr<dcp::SoundAsset> ms (new dcp::SoundAsset(dcp::Fraction(24, 1), sample_rate, channels, dcp::LanguageTag("en-US"), dcp::Standard::SMPTE));
297         ms->_language = language;
298         ms->set_metadata (mxf_meta);
299         shared_ptr<dcp::SoundAssetWriter> sound_writer = ms->start_write (path / dcp::String::compose("audio%1.mxf", suffix));
300
301         int const samples_per_frame = sample_rate / 24;
302
303         float* silence[channels];
304         for (auto i = 0; i < channels; ++i) {
305                 silence[i] = new float[samples_per_frame];
306                 memset (silence[i], 0, samples_per_frame * sizeof(float));
307         }
308
309         for (auto i = 0; i < frames; ++i) {
310                 sound_writer->write (silence, samples_per_frame);
311         }
312
313         sound_writer->finalize ();
314
315         for (auto i = 0; i < channels; ++i) {
316                 delete[] silence[i];
317         }
318
319         return ms;
320 }
321
322
323 shared_ptr<dcp::DCP>
324 make_simple (boost::filesystem::path path, int reels, int frames, dcp::Standard standard)
325 {
326         /* Some known metadata */
327         dcp::MXFMetadata mxf_meta;
328         mxf_meta.company_name = "OpenDCP";
329         mxf_meta.product_name = "OpenDCP";
330         mxf_meta.product_version = "0.0.25";
331
332         boost::filesystem::remove_all (path);
333         boost::filesystem::create_directories (path);
334         auto d = make_shared<dcp::DCP>(path);
335         auto cpl = make_shared<dcp::CPL>("A Test DCP", dcp::ContentKind::TRAILER, standard);
336         cpl->set_annotation_text ("A Test DCP");
337         cpl->set_issuer ("OpenDCP 0.0.25");
338         cpl->set_creator ("OpenDCP 0.0.25");
339         cpl->set_issue_date ("2012-07-17T04:45:18+00:00");
340         cpl->set_content_version (
341                 dcp::ContentVersion("urn:uuid:75ac29aa-42ac-1234-ecae-49251abefd11", "content-version-label-text")
342                 );
343         cpl->set_main_sound_configuration("51/L,R,C,LFE,Ls,Rs");
344         cpl->set_main_sound_sample_rate(48000);
345         cpl->set_main_picture_stored_area(dcp::Size(1998, 1080));
346         cpl->set_main_picture_active_area(dcp::Size(1998, 1080));
347         cpl->set_version_number(1);
348
349         for (int i = 0; i < reels; ++i) {
350                 string suffix = reels == 1 ? "" : dcp::String::compose("%1", i);
351
352                 shared_ptr<dcp::MonoPictureAsset> mp = simple_picture (path, suffix, frames);
353                 shared_ptr<dcp::SoundAsset> ms = simple_sound (path, suffix, mxf_meta, "en-US", frames);
354
355                 auto reel = make_shared<dcp::Reel>(
356                         shared_ptr<dcp::ReelMonoPictureAsset>(new dcp::ReelMonoPictureAsset(mp, 0)),
357                         shared_ptr<dcp::ReelSoundAsset>(new dcp::ReelSoundAsset(ms, 0))
358                         );
359
360                 auto markers = make_shared<dcp::ReelMarkersAsset>(dcp::Fraction(24, 1), frames, 0);
361                 if (i == 0) {
362                         markers->set (dcp::Marker::FFOC, dcp::Time(0, 0, 0, 1, 24));
363                 }
364                 if (i == reels - 1) {
365                         markers->set (dcp::Marker::LFOC, dcp::Time(0, 0, 0, frames - 1, 24));
366                 }
367                 reel->add (markers);
368
369                 cpl->add (reel);
370         }
371
372         d->add (cpl);
373         return d;
374 }
375
376
377 shared_ptr<dcp::Subtitle>
378 simple_subtitle ()
379 {
380         return make_shared<dcp::SubtitleString>(
381                 optional<string>(),
382                 false,
383                 false,
384                 false,
385                 dcp::Colour(255, 255, 255),
386                 42,
387                 1,
388                 dcp::Time(0, 0, 4, 0, 24),
389                 dcp::Time(0, 0, 8, 0, 24),
390                 0.5,
391                 dcp::HAlign::CENTER,
392                 0.8,
393                 dcp::VAlign::TOP,
394                 dcp::Direction::LTR,
395                 "Hello world",
396                 dcp::Effect::NONE,
397                 dcp::Colour(255, 255, 255),
398                 dcp::Time(),
399                 dcp::Time()
400                 );
401 }
402
403
404 shared_ptr<dcp::ReelMarkersAsset>
405 simple_markers (int frames)
406 {
407         auto markers = make_shared<dcp::ReelMarkersAsset>(dcp::Fraction(24, 1), frames, 0);
408         markers->set (dcp::Marker::FFOC, dcp::Time(1, 24, 24));
409         markers->set (dcp::Marker::LFOC, dcp::Time(frames - 1, 24, 24));
410         return markers;
411 }
412
413
414 shared_ptr<dcp::DCP>
415 make_simple_with_interop_subs (boost::filesystem::path path)
416 {
417         auto dcp = make_simple (path, 1, 24, dcp::Standard::INTEROP);
418
419         auto subs = make_shared<dcp::InteropSubtitleAsset>();
420         subs->add (simple_subtitle());
421
422         boost::filesystem::create_directory (path / "subs");
423         dcp::ArrayData data(4096);
424         subs->add_font ("afont", data);
425         subs->write (path / "subs" / "subs.xml");
426
427         auto reel_subs = make_shared<dcp::ReelInteropSubtitleAsset>(subs, dcp::Fraction(24, 1), 240, 0);
428         dcp->cpls().front()->reels().front()->add (reel_subs);
429
430         return dcp;
431 }
432
433
434 shared_ptr<dcp::DCP>
435 make_simple_with_smpte_subs (boost::filesystem::path path)
436 {
437         auto dcp = make_simple (path, 1, 192);
438
439         auto subs = make_shared<dcp::SMPTESubtitleAsset>();
440         subs->set_language (dcp::LanguageTag("de-DE"));
441         subs->set_start_time (dcp::Time());
442         subs->add (simple_subtitle());
443
444         subs->write (path / "subs.mxf");
445
446         auto reel_subs = make_shared<dcp::ReelSMPTESubtitleAsset>(subs, dcp::Fraction(24, 1), 192, 0);
447         dcp->cpls().front()->reels().front()->add (reel_subs);
448
449         return dcp;
450 }
451
452
453 shared_ptr<dcp::DCP>
454 make_simple_with_interop_ccaps (boost::filesystem::path path)
455 {
456         auto dcp = make_simple (path, 1, 24, dcp::Standard::INTEROP);
457
458         auto subs = make_shared<dcp::InteropSubtitleAsset>();
459         subs->add (simple_subtitle());
460         subs->write (path / "ccap.xml");
461
462         auto reel_caps = make_shared<dcp::ReelClosedCaptionAsset>(subs, dcp::Fraction(24, 1), 240, 0);
463         dcp->cpls().front()->reels().front()->add (reel_caps);
464
465         return dcp;
466 }
467
468
469 shared_ptr<dcp::DCP>
470 make_simple_with_smpte_ccaps (boost::filesystem::path path)
471 {
472         auto dcp = make_simple (path, 1, 192);
473
474         auto subs = make_shared<dcp::SMPTESubtitleAsset>();
475         subs->set_language (dcp::LanguageTag("de-DE"));
476         subs->set_start_time (dcp::Time());
477         subs->add (simple_subtitle());
478         subs->write (path / "ccap.mxf");
479
480         auto reel_caps = make_shared<dcp::ReelClosedCaptionAsset>(subs, dcp::Fraction(24, 1), 192, 0);
481         dcp->cpls()[0]->reels()[0]->add(reel_caps);
482
483         return dcp;
484 }
485
486
487 shared_ptr<dcp::OpenJPEGImage>
488 black_image (dcp::Size size)
489 {
490         auto image = make_shared<dcp::OpenJPEGImage>(size);
491         int const pixels = size.width * size.height;
492         for (int i = 0; i < 3; ++i) {
493                 memset (image->data(i), 0, pixels * sizeof(int));
494         }
495         return image;
496 }
497
498
499 shared_ptr<dcp::ReelAsset>
500 black_picture_asset (boost::filesystem::path dir, int frames)
501 {
502         auto image = black_image ();
503         auto frame = dcp::compress_j2k (image, 100000000, 24, false, false);
504         BOOST_REQUIRE (frame.size() < 230000000 / (24 * 8));
505
506         auto asset = make_shared<dcp::MonoPictureAsset>(dcp::Fraction(24, 1), dcp::Standard::SMPTE);
507         asset->set_metadata (dcp::MXFMetadata("libdcp", "libdcp", "1.6.4devel"));
508         boost::filesystem::create_directories (dir);
509         auto writer = asset->start_write (dir / "pic.mxf", true);
510         for (int i = 0; i < frames; ++i) {
511                 writer->write (frame.data(), frame.size());
512         }
513         writer->finalize ();
514
515         return make_shared<dcp::ReelMonoPictureAsset>(asset, 0);
516 }
517
518
519 boost::filesystem::path
520 find_file (boost::filesystem::path dir, string filename_part)
521 {
522         boost::optional<boost::filesystem::path> found;
523         for (auto i: boost::filesystem::directory_iterator(dir)) {
524                 if (i.path().filename().string().find(filename_part) != string::npos) {
525                         BOOST_REQUIRE (!found);
526                         found = i;
527                 }
528         }
529         BOOST_REQUIRE (found);
530         return *found;
531 }
532
533
534 BOOST_GLOBAL_FIXTURE (TestConfig);