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