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