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