Fix silent stereo mixdown exports when the project audio channel count is > 6.
[dcpomatic.git] / test / map_cli_test.cc
1 /*
2     Copyright (C) 2023 Carl Hetherington <cth@carlh.net>
3
4     This file is part of DCP-o-matic.
5
6     DCP-o-matic 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     DCP-o-matic 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 DCP-o-matic.  If not, see <http://www.gnu.org/licenses/>.
18
19 */
20
21
22 #include "lib/config.h"
23 #include "lib/content.h"
24 #include "lib/dcp_content.h"
25 #include "lib/content_factory.h"
26 #include "lib/film.h"
27 #include "lib/map_cli.h"
28 #include "lib/text_content.h"
29 #include "test.h"
30 #include <dcp/cpl.h>
31 #include <dcp/dcp.h>
32 #include <dcp/reel.h>
33 #include <dcp/reel_picture_asset.h>
34 #include <dcp/reel_sound_asset.h>
35 #include <boost/algorithm/string.hpp>
36 #include <boost/filesystem.hpp>
37 #include <boost/optional.hpp>
38 #include <boost/test/unit_test.hpp>
39
40
41 using std::dynamic_pointer_cast;
42 using std::make_shared;
43 using std::shared_ptr;
44 using std::string;
45 using std::vector;
46 using boost::optional;
47
48
49 static
50 optional<string>
51 run(vector<string> const& args, vector<string>& output)
52 {
53         vector<char*> argv(args.size() + 1);
54         for (auto i = 0U; i < args.size(); ++i) {
55                 argv[i] = const_cast<char*>(args[i].c_str());
56         }
57         argv[args.size()] = nullptr;
58
59         auto error = map_cli(args.size(), argv.data(), [&output](string s) { output.push_back(s); });
60         if (error) {
61                 std::cout << *error << "\n";
62         }
63
64         return error;
65 }
66
67
68 static
69 boost::filesystem::path
70 find_prefix(boost::filesystem::path dir, string prefix)
71 {
72         auto iter = std::find_if(boost::filesystem::directory_iterator(dir), boost::filesystem::directory_iterator(), [prefix](boost::filesystem::path const& p) {
73                 return boost::starts_with(p.filename().string(), prefix);
74         });
75
76         BOOST_REQUIRE(iter != boost::filesystem::directory_iterator());
77         return iter->path();
78 }
79
80
81 static
82 boost::filesystem::path
83 find_cpl(boost::filesystem::path dir)
84 {
85         return find_prefix(dir, "cpl_");
86 }
87
88
89 /** Map a single DCP into a new DCP */
90 BOOST_AUTO_TEST_CASE(map_simple_dcp_copy)
91 {
92         string const name = "map_simple_dcp_copy";
93         string const out = String::compose("build/test/%1_out", name);
94
95         auto content = content_factory("test/data/flat_red.png");
96         auto film = new_test_film2(name + "_in", content);
97         make_and_verify_dcp(film);
98
99         vector<string> const args = {
100                 "map_cli",
101                 "-o", out,
102                 "-d", film->dir(film->dcp_name()).string(),
103                 find_cpl(film->dir(film->dcp_name())).string()
104         };
105
106         boost::filesystem::remove_all(out);
107
108         vector<string> output_messages;
109         auto error = run(args, output_messages);
110         BOOST_CHECK(!error);
111
112         verify_dcp(out, {});
113
114         BOOST_CHECK(boost::filesystem::is_regular_file(find_prefix(out, "j2c_")));
115         BOOST_CHECK(boost::filesystem::is_regular_file(find_prefix(out, "pcm_")));
116 }
117
118
119 /** Map a single DCP into a new DCP, referring to the CPL by ID */
120 BOOST_AUTO_TEST_CASE(map_simple_dcp_copy_by_id)
121 {
122         string const name = "map_simple_dcp_copy_by_id";
123         string const out = String::compose("build/test/%1_out", name);
124
125         auto content = content_factory("test/data/flat_red.png");
126         auto film = new_test_film2(name + "_in", content);
127         make_and_verify_dcp(film);
128
129         dcp::CPL cpl(find_cpl(film->dir(film->dcp_name())));
130
131         vector<string> const args = {
132                 "map_cli",
133                 "-o", out,
134                 "-d", film->dir(film->dcp_name()).string(),
135                 cpl.id()
136         };
137
138         boost::filesystem::remove_all(out);
139
140         vector<string> output_messages;
141         auto error = run(args, output_messages);
142         BOOST_CHECK(!error);
143
144         verify_dcp(out, {});
145
146         BOOST_CHECK(boost::filesystem::is_regular_file(find_prefix(out, "j2c_")));
147         BOOST_CHECK(boost::filesystem::is_regular_file(find_prefix(out, "pcm_")));
148 }
149
150
151 /** Map a single DCP into a new DCP using the symlink option */
152 BOOST_AUTO_TEST_CASE(map_simple_dcp_copy_with_symlinks)
153 {
154         string const name = "map_simple_dcp_copy_with_symlinks";
155         string const out = String::compose("build/test/%1_out", name);
156
157         auto content = content_factory("test/data/flat_red.png");
158         auto film = new_test_film2(name + "_in", content);
159         make_and_verify_dcp(film);
160
161         vector<string> const args = {
162                 "map_cli",
163                 "-o", out,
164                 "-d", film->dir(film->dcp_name()).string(),
165                 "-s",
166                 find_cpl(film->dir(film->dcp_name())).string()
167         };
168
169         boost::filesystem::remove_all(out);
170
171         vector<string> output_messages;
172         auto error = run(args, output_messages);
173         BOOST_CHECK(!error);
174
175         /* We can't verify this DCP because the symlinks will make it fail
176          * (as it should be, I think).
177          */
178
179         BOOST_CHECK(boost::filesystem::is_symlink(find_prefix(out, "j2c_")));
180         BOOST_CHECK(boost::filesystem::is_symlink(find_prefix(out, "pcm_")));
181 }
182
183
184 /** Map a single DCP into a new DCP using the hardlink option */
185 BOOST_AUTO_TEST_CASE(map_simple_dcp_copy_with_hardlinks)
186 {
187         string const name = "map_simple_dcp_copy_with_hardlinks";
188         string const out = String::compose("build/test/%1_out", name);
189
190         auto content = content_factory("test/data/flat_red.png");
191         auto film = new_test_film2(name + "_in", content);
192         make_and_verify_dcp(film);
193
194         vector<string> const args = {
195                 "map_cli",
196                 "-o", out,
197                 "-d", film->dir(film->dcp_name()).string(),
198                 "-l",
199                 find_cpl(film->dir(film->dcp_name())).string()
200         };
201
202         boost::filesystem::remove_all(out);
203
204         vector<string> output_messages;
205         auto error = run(args, output_messages);
206         BOOST_CHECK(!error);
207
208         verify_dcp(out, {});
209
210         /* The video file will have 3 links because DoM also makes a link into the video directory */
211         BOOST_CHECK_EQUAL(boost::filesystem::hard_link_count(find_prefix(out, "j2c_")), 3U);
212         BOOST_CHECK_EQUAL(boost::filesystem::hard_link_count(find_prefix(out, "pcm_")), 2U);
213 }
214
215
216 /** Map a single Interop DCP with subs into a new DCP */
217 BOOST_AUTO_TEST_CASE(map_simple_interop_dcp_with_subs)
218 {
219         string const name = "map_simple_interop_dcp_with_subs";
220         string const out = String::compose("build/test/%1_out", name);
221
222         auto picture = content_factory("test/data/flat_red.png").front();
223         auto subs = content_factory("test/data/15s.srt").front();
224         auto film = new_test_film2(name + "_in", { picture, subs });
225         film->set_interop(true);
226         subs->only_text()->set_language(dcp::LanguageTag("de"));
227         make_and_verify_dcp(film, {dcp::VerificationNote::Code::INVALID_STANDARD});
228
229         vector<string> const args = {
230                 "map_cli",
231                 "-o", out,
232                 "-d", film->dir(film->dcp_name()).string(),
233                 find_cpl(film->dir(film->dcp_name())).string()
234         };
235
236         boost::filesystem::remove_all(out);
237
238         vector<string> output_messages;
239         auto error = run(args, output_messages);
240         BOOST_CHECK(!error);
241
242         verify_dcp(out, {dcp::VerificationNote::Code::INVALID_STANDARD});
243 }
244
245
246 static
247 void
248 test_map_ov_vf_copy(vector<string> extra_args = {})
249 {
250         string const name = "map_ov_vf_copy";
251         string const out = String::compose("build/test/%1_out", name);
252
253         auto ov_content = content_factory("test/data/flat_red.png");
254         auto ov_film = new_test_film2(name + "_ov", ov_content);
255         make_and_verify_dcp(ov_film);
256
257         auto const ov_dir = ov_film->dir(ov_film->dcp_name());
258         auto vf_ov = make_shared<DCPContent>(ov_dir);
259         auto vf_sound = content_factory("test/data/sine_440.wav").front();
260         auto vf_film = new_test_film2(name + "_vf", { vf_ov, vf_sound });
261         vf_ov->set_reference_video(true);
262         make_and_verify_dcp(vf_film, {dcp::VerificationNote::Code::EXTERNAL_ASSET}, false);
263
264         auto const vf_dir = vf_film->dir(vf_film->dcp_name());
265
266         vector<string> args = {
267                 "map_cli",
268                 "-o", out,
269                 "-d", ov_dir.string(),
270                 "-d", vf_dir.string(),
271                 find_cpl(vf_dir).string()
272         };
273
274         args.insert(std::end(args), std::begin(extra_args), std::end(extra_args));
275
276         boost::filesystem::remove_all(out);
277
278         vector<string> output_messages;
279         auto error = run(args, output_messages);
280         BOOST_CHECK(!error);
281
282         verify_dcp(out, {});
283
284         check_file(find_file(out, "cpl_"), find_file(vf_dir, "cpl_"));
285         check_file(find_file(out, "j2c_"), find_file(ov_dir, "j2c_"));
286         check_file(find_file(out, "pcm_"), find_file(vf_dir, "pcm_"));
287 }
288
289
290 /** Map an OV and a VF into a single DCP */
291 BOOST_AUTO_TEST_CASE(map_ov_vf_copy)
292 {
293         test_map_ov_vf_copy();
294         test_map_ov_vf_copy({"-l"});
295 }
296
297
298 /** Map an OV and VF into a single DCP, where the VF refers to the OV's assets multiple times */
299 BOOST_AUTO_TEST_CASE(map_ov_vf_copy_multiple_reference)
300 {
301         string const name = "map_ov_vf_copy_multiple_reference";
302         string const out = String::compose("build/test/%1_out", name);
303
304         auto ov_content = content_factory("test/data/flat_red.png");
305         auto ov_film = new_test_film2(name + "_ov", ov_content);
306         make_and_verify_dcp(ov_film);
307
308         auto const ov_dir = ov_film->dir(ov_film->dcp_name());
309
310         auto vf_ov1 = make_shared<DCPContent>(ov_dir);
311         auto vf_ov2 = make_shared<DCPContent>(ov_dir);
312         auto vf_sound = content_factory("test/data/sine_440.wav").front();
313         auto vf_film = new_test_film2(name + "_vf", { vf_ov1, vf_ov2, vf_sound });
314         vf_film->set_reel_type(ReelType::BY_VIDEO_CONTENT);
315         vf_ov2->set_position(vf_film, vf_ov1->end(vf_film));
316         vf_ov1->set_reference_video(true);
317         vf_ov2->set_reference_video(true);
318         make_and_verify_dcp(vf_film, {dcp::VerificationNote::Code::EXTERNAL_ASSET}, false);
319
320         auto const vf_dir = vf_film->dir(vf_film->dcp_name());
321
322         vector<string> const args = {
323                 "map_cli",
324                 "-o", out,
325                 "-d", ov_dir.string(),
326                 "-d", vf_dir.string(),
327                 "-l",
328                 find_cpl(vf_dir).string()
329         };
330
331         boost::filesystem::remove_all(out);
332
333         vector<string> output_messages;
334         auto error = run(args, output_messages);
335         BOOST_CHECK(!error);
336
337         verify_dcp(out, {});
338
339         check_file(find_file(out, "cpl_"), find_file(vf_dir, "cpl_"));
340         check_file(find_file(out, "j2c_"), find_file(ov_dir, "j2c_"));
341 }
342
343
344 /** Map a single DCP into a new DCP using the rename option */
345 BOOST_AUTO_TEST_CASE(map_simple_dcp_copy_with_rename)
346 {
347         ConfigRestorer cr;
348         Config::instance()->set_dcp_asset_filename_format(dcp::NameFormat("hello%c"));
349         string const name = "map_simple_dcp_copy_with_rename";
350         string const out = String::compose("build/test/%1_out", name);
351
352         auto content = content_factory("test/data/flat_red.png");
353         auto film = new_test_film2(name + "_in", content);
354         make_and_verify_dcp(film);
355
356         vector<string> const args = {
357                 "map_cli",
358                 "-o", out,
359                 "-d", film->dir(film->dcp_name()).string(),
360                 "-r",
361                 find_cpl(film->dir(film->dcp_name())).string()
362         };
363
364         boost::filesystem::remove_all(out);
365
366         vector<string> output_messages;
367         auto error = run(args, output_messages);
368         BOOST_CHECK(!error);
369
370         verify_dcp(out, {});
371
372         dcp::DCP out_dcp(out);
373         out_dcp.read();
374
375         BOOST_REQUIRE_EQUAL(out_dcp.cpls().size(), 1U);
376         auto const cpl = out_dcp.cpls()[0];
377         BOOST_REQUIRE_EQUAL(cpl->reels().size(), 1U);
378         auto const reel = cpl->reels()[0];
379         BOOST_REQUIRE(reel->main_picture());
380         BOOST_REQUIRE(reel->main_sound());
381         auto const picture = reel->main_picture()->asset();
382         BOOST_REQUIRE(picture);
383         auto const sound = reel->main_sound()->asset();
384         BOOST_REQUIRE(sound);
385
386         BOOST_REQUIRE(picture->file());
387         BOOST_CHECK(picture->file().get().filename() == picture->id() + ".mxf");
388
389         BOOST_REQUIRE(sound->file());
390         BOOST_CHECK(sound->file().get().filename() == sound->id() + ".mxf");
391 }
392
393
394 static
395 void
396 test_two_cpls_each_with_subs(string name, bool interop)
397 {
398         string const out = String::compose("build/test/%1_out", name);
399
400         vector<dcp::VerificationNote::Code> acceptable_errors;
401         if (interop) {
402                 acceptable_errors.push_back(dcp::VerificationNote::Code::INVALID_STANDARD);
403         } else {
404                 acceptable_errors.push_back(dcp::VerificationNote::Code::MISSING_SUBTITLE_LANGUAGE);
405                 acceptable_errors.push_back(dcp::VerificationNote::Code::INVALID_SUBTITLE_FIRST_TEXT_TIME);
406         }
407
408         shared_ptr<Film> films[2];
409         for (auto i = 0; i < 2; ++i) {
410                 auto picture = content_factory("test/data/flat_red.png").front();
411                 auto subs = content_factory("test/data/15s.srt").front();
412                 films[i] = new_test_film2(String::compose("%1_%2_in", name, i), { picture, subs });
413                 films[i]->set_interop(interop);
414                 subs->only_text()->set_language(dcp::LanguageTag("de"));
415                 make_and_verify_dcp(films[i], acceptable_errors);
416         }
417
418         vector<string> const args = {
419                 "map_cli",
420                 "-o", out,
421                 "-d", films[0]->dir(films[0]->dcp_name()).string(),
422                 "-d", films[1]->dir(films[1]->dcp_name()).string(),
423                 find_cpl(films[0]->dir(films[0]->dcp_name())).string(),
424                 find_cpl(films[1]->dir(films[1]->dcp_name())).string()
425         };
426
427         boost::filesystem::remove_all(out);
428
429         vector<string> output_messages;
430         auto error = run(args, output_messages);
431         BOOST_CHECK(!error);
432
433         verify_dcp(out, acceptable_errors);
434 }
435
436
437 BOOST_AUTO_TEST_CASE(map_two_interop_cpls_each_with_subs)
438 {
439         test_two_cpls_each_with_subs("map_two_interop_cpls_each_with_subs", true);
440 }
441
442
443 BOOST_AUTO_TEST_CASE(map_two_smpte_cpls_each_with_subs)
444 {
445         test_two_cpls_each_with_subs("map_two_smpte_cpls_each_with_subs", false);
446 }
447
448
449 BOOST_AUTO_TEST_CASE(map_with_given_config)
450 {
451         ConfigRestorer cr;
452
453         string const name = "map_with_given_config";
454         string const out = String::compose("build/test/%1_out", name);
455
456         auto content = content_factory("test/data/flat_red.png");
457         auto film = new_test_film2(name + "_in", content);
458         make_and_verify_dcp(film);
459
460         vector<string> const args = {
461                 "map_cli",
462                 "-o", out,
463                 "-d", film->dir(film->dcp_name()).string(),
464                 "--config", "test/data/map_with_given_config",
465                 find_cpl(film->dir(film->dcp_name())).string()
466         };
467
468         boost::filesystem::remove_all(out);
469
470         Config::instance()->drop();
471         vector<string> output_messages;
472         auto error = run(args, output_messages);
473         BOOST_CHECK(!error);
474
475         /* It should be signed by the key in test/data/map_with_given_config, not the one in test/data/signer_key */
476         BOOST_CHECK(dcp::file_to_string(find_file(out, "cpl_")).find("dnQualifier=\\+uOcNN2lPuxpxgd/5vNkkBER0GE=,CN=CS.dcpomatic.smpte-430-2.LEAF,OU=dcpomatic.com,O=dcpomatic.com") != std::string::npos);
477 }
478
479
480 BOOST_AUTO_TEST_CASE(map_multireel_interop_ov_and_vf_adding_ccaps)
481 {
482         string const name = "map_multireel_interop_ov_and_vf_adding_ccaps";
483         string const out = String::compose("build/test/%1_out", name);
484
485         vector<shared_ptr<Content>> video = {
486                 content_factory("test/data/flat_red.png")[0],
487                 content_factory("test/data/flat_red.png")[0],
488                 content_factory("test/data/flat_red.png")[0]
489         };
490
491         auto ov = new_test_film2(name + "_ov", { video[0], video[1], video[2] });
492         ov->set_reel_type(ReelType::BY_VIDEO_CONTENT);
493         ov->set_interop(true);
494         make_and_verify_dcp(ov, { dcp::VerificationNote::Code::INVALID_STANDARD });
495
496         auto ov_dcp = make_shared<DCPContent>(ov->dir(ov->dcp_name()));
497
498         vector<shared_ptr<Content>> ccap = {
499                 content_factory("test/data/short.srt")[0],
500                 content_factory("test/data/short.srt")[0],
501                 content_factory("test/data/short.srt")[0]
502         };
503
504         auto vf = new_test_film2(name + "_vf", { ov_dcp, ccap[0], ccap[1], ccap[2] });
505         vf->set_interop(true);
506         vf->set_reel_type(ReelType::BY_VIDEO_CONTENT);
507         ov_dcp->set_reference_video(true);
508         ov_dcp->set_reference_audio(true);
509         for (auto i = 0; i < 3; ++i) {
510                 ccap[i]->text[0]->set_use(true);
511                 ccap[i]->text[0]->set_type(TextType::CLOSED_CAPTION);
512         }
513         make_and_verify_dcp(
514                 vf,
515                 {
516                         dcp::VerificationNote::Code::INVALID_STANDARD,
517                         dcp::VerificationNote::Code::INVALID_SUBTITLE_FIRST_TEXT_TIME,
518                         dcp::VerificationNote::Code::MISSING_SUBTITLE_LANGUAGE,
519                         dcp::VerificationNote::Code::EXTERNAL_ASSET
520                 });
521
522         vector<string> const args = {
523                 "map_cli",
524                 "-o", out,
525                 "-d", ov->dir(ov->dcp_name()).string(),
526                 "-d", vf->dir(vf->dcp_name()).string(),
527                 find_cpl(vf->dir(vf->dcp_name())).string()
528         };
529
530         boost::filesystem::remove_all(out);
531
532         vector<string> output_messages;
533         auto error = run(args, output_messages);
534         BOOST_CHECK(!error);
535
536         verify_dcp(out, { dcp::VerificationNote::Code::INVALID_STANDARD });
537 }
538
539
540 BOOST_AUTO_TEST_CASE(map_uses_config_for_issuer_and_creator)
541 {
542         ConfigRestorer cr;
543
544         Config::instance()->set_dcp_issuer("ostrabagalous");
545         Config::instance()->set_dcp_creator("Fred");
546
547         string const name = "map_uses_config_for_issuer_and_creator";
548         string const out = String::compose("build/test/%1_out", name);
549
550         auto content = content_factory("test/data/flat_red.png");
551         auto film = new_test_film2(name + "_in", content);
552         make_and_verify_dcp(film);
553
554         vector<string> const args = {
555                 "map_cli",
556                 "-o", out,
557                 "-d", film->dir(film->dcp_name()).string(),
558                 find_cpl(film->dir(film->dcp_name())).string()
559         };
560
561         boost::filesystem::remove_all(out);
562
563         vector<string> output_messages;
564         auto error = run(args, output_messages);
565         BOOST_CHECK(!error);
566
567         cxml::Document assetmap("AssetMap");
568         assetmap.read_file(film->dir(film->dcp_name()) / "ASSETMAP.xml");
569         BOOST_CHECK(assetmap.string_child("Issuer") == "ostrabagalous");
570         BOOST_CHECK(assetmap.string_child("Creator") == "Fred");
571
572         cxml::Document pkl("PackingList");
573         pkl.read_file(find_prefix(out, "pkl_"));
574         BOOST_CHECK(pkl.string_child("Issuer") == "ostrabagalous");
575         BOOST_CHECK(pkl.string_child("Creator") == "Fred");
576 }
577
578
579 BOOST_AUTO_TEST_CASE(map_handles_interop_png_subs)
580 {
581         string const name = "map_handles_interop_png_subs";
582         auto arrietty = content_factory(TestPaths::private_data() / "arrietty_JP-EN.mkv")[0];
583         auto film = new_test_film2(name + "_input", { arrietty });
584         film->set_interop(true);
585         arrietty->set_trim_end(dcpomatic::ContentTime::from_seconds(110));
586         arrietty->text[0]->set_use(true);
587         arrietty->text[0]->set_language(dcp::LanguageTag("de"));
588         make_and_verify_dcp(
589                 film,
590                 {
591                         dcp::VerificationNote::Code::INVALID_SUBTITLE_FIRST_TEXT_TIME,
592                         dcp::VerificationNote::Code::MISSING_SUBTITLE_LANGUAGE,
593                         dcp::VerificationNote::Code::INVALID_STANDARD
594                 });
595
596         auto const out = boost::filesystem::path("build") / "test" / (name + "_output");
597
598         vector<string> const args = {
599                 "map_cli",
600                 "-o", out.string(),
601                 "-d", film->dir(film->dcp_name()).string(),
602                 find_cpl(film->dir(film->dcp_name())).string()
603         };
604
605         boost::filesystem::remove_all(out);
606
607         vector<string> output_messages;
608         auto error = run(args, output_messages);
609         BOOST_CHECK(!error);
610
611         verify_dcp(out, { dcp::VerificationNote::Code::INVALID_STANDARD });
612 }
613