wip: black pieces; sad part is that Shuffler can't cope with content that doesn't...
[dcpomatic.git] / test / player_test.cc
1 /*
2     Copyright (C) 2014-2021 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 /** @file  test/player_test.cc
23  *  @brief Test Player class.
24  *  @ingroup selfcontained
25  */
26
27
28 #include "lib/audio_buffers.h"
29 #include "lib/black_content.h"
30 #include "lib/butler.h"
31 #include "lib/compose.hpp"
32 #include "lib/content_factory.h"
33 #include "lib/cross.h"
34 #include "lib/dcp_content.h"
35 #include "lib/dcp_content_type.h"
36 #include "lib/ffmpeg_content.h"
37 #include "lib/film.h"
38 #include "lib/image_content.h"
39 #include "lib/piece.h"
40 #include "lib/player.h"
41 #include "lib/ratio.h"
42 #include "lib/string_text_file_content.h"
43 #include "lib/text_content.h"
44 #include "lib/video_content.h"
45 #include "test.h"
46 #include <boost/test/unit_test.hpp>
47 #include <boost/algorithm/string.hpp>
48 #include <iostream>
49
50
51 using std::cout;
52 using std::dynamic_pointer_cast;
53 using std::list;
54 using std::shared_ptr;
55 using std::make_shared;
56 using boost::bind;
57 using boost::optional;
58 #if BOOST_VERSION >= 106100
59 using namespace boost::placeholders;
60 #endif
61 using namespace dcpomatic;
62
63
64 static shared_ptr<AudioBuffers> accumulated;
65
66
67 static void
68 accumulate (shared_ptr<AudioBuffers> audio, DCPTime)
69 {
70         BOOST_REQUIRE (accumulated);
71         accumulated->append (audio);
72 }
73
74
75 /** Check that the Player correctly generates silence when used with a silent FFmpegContent */
76 BOOST_AUTO_TEST_CASE (player_silence_padding_test)
77 {
78         auto film = new_test_film ("player_silence_padding_test");
79         film->set_name ("player_silence_padding_test");
80         auto c = std::make_shared<FFmpegContent>("test/data/test.mp4");
81         film->set_container (Ratio::from_id ("185"));
82         film->set_audio_channels (6);
83
84         film->examine_and_add_content (c);
85         BOOST_REQUIRE (!wait_for_jobs());
86
87         accumulated = std::make_shared<AudioBuffers>(film->audio_channels(), 0);
88
89         auto player = std::make_shared<Player>(film, Image::Alignment::COMPACT);
90         player->Audio.connect (bind (&accumulate, _1, _2));
91         while (!player->pass ()) {}
92         BOOST_REQUIRE (accumulated->frames() >= 48000);
93         BOOST_CHECK_EQUAL (accumulated->channels(), film->audio_channels ());
94
95         for (int i = 0; i < 48000; ++i) {
96                 for (int c = 0; c < accumulated->channels(); ++c) {
97                         BOOST_CHECK_EQUAL (accumulated->data()[c][i], 0);
98                 }
99         }
100 }
101
102
103 /* Test insertion of black frames between separate bits of video content */
104 BOOST_AUTO_TEST_CASE (player_black_fill_test)
105 {
106         auto film = new_test_film ("black_fill_test");
107         film->set_dcp_content_type (DCPContentType::from_isdcf_name ("FTR"));
108         film->set_name ("black_fill_test");
109         film->set_container (Ratio::from_id ("185"));
110         film->set_sequence (false);
111         film->set_interop (false);
112         auto contentA = std::make_shared<ImageContent>("test/data/simple_testcard_640x480.png");
113         auto contentB = std::make_shared<ImageContent>("test/data/simple_testcard_640x480.png");
114
115         film->examine_and_add_content (contentA);
116         film->examine_and_add_content (contentB);
117         BOOST_REQUIRE (!wait_for_jobs());
118
119         contentA->video->set_length (3);
120         contentA->set_position (film, DCPTime::from_frames(2, film->video_frame_rate()));
121         contentA->video->set_custom_ratio (1.85);
122         contentB->video->set_length (1);
123         contentB->set_position (film, DCPTime::from_frames(7, film->video_frame_rate()));
124         contentB->video->set_custom_ratio (1.85);
125
126         make_and_verify_dcp (
127                 film,
128                 {
129                         dcp::VerificationNote::Code::MISSING_FFMC_IN_FEATURE,
130                         dcp::VerificationNote::Code::MISSING_FFEC_IN_FEATURE
131                 });
132
133         boost::filesystem::path ref;
134         ref = "test";
135         ref /= "data";
136         ref /= "black_fill_test";
137
138         boost::filesystem::path check;
139         check = "build";
140         check /= "test";
141         check /= "black_fill_test";
142         check /= film->dcp_name();
143
144         check_dcp (ref.string(), check.string());
145 }
146
147
148 /** Check behaviour with an awkward playlist whose data does not end on a video frame start */
149 BOOST_AUTO_TEST_CASE (player_subframe_test)
150 {
151         auto film = new_test_film ("reels_test7");
152         film->set_name ("reels_test7");
153         film->set_container (Ratio::from_id("185"));
154         film->set_dcp_content_type (DCPContentType::from_isdcf_name("TST"));
155         auto A = content_factory("test/data/flat_red.png").front();
156         film->examine_and_add_content (A);
157         BOOST_REQUIRE (!wait_for_jobs());
158         auto B = content_factory("test/data/awkward_length.wav").front();
159         film->examine_and_add_content (B);
160         BOOST_REQUIRE (!wait_for_jobs());
161         film->set_video_frame_rate (24);
162         A->video->set_length (3 * 24);
163
164         BOOST_CHECK (A->full_length(film) == DCPTime::from_frames(3 * 24, 24));
165         BOOST_CHECK (B->full_length(film) == DCPTime(289920));
166         /* Length should be rounded up from B's length to the next video frame */
167         BOOST_CHECK (film->length() == DCPTime::from_frames(3 * 24 + 1, 24));
168
169         auto player = std::make_shared<Player>(film, Image::Alignment::COMPACT);
170
171         player->setup_pieces ();
172         list<std::shared_ptr<Piece>> black;
173         std::copy_if(
174                 player->_pieces.begin(),
175                 player->_pieces.end(),
176                 std::back_inserter(black),
177                 [](shared_ptr<Piece> piece) {
178                         return static_cast<bool>(dynamic_pointer_cast<BlackContent>(piece->content));
179                 });
180         BOOST_REQUIRE_EQUAL (black.size(), 1U);
181         BOOST_CHECK (black.front()->content->position() == DCPTime::from_frames(3 * 24, 24));
182         BOOST_CHECK (black.front()->content->full_length(film) == DCPTime::from_frames(1, 24));
183
184         BOOST_REQUIRE_EQUAL (player->_silent._periods.size(), 1U);
185         BOOST_CHECK (player->_silent._periods.front() == DCPTimePeriod(DCPTime(289920), DCPTime::from_frames(3 * 24 + 1, 24)));
186 }
187
188
189 static Frame video_frames;
190 static Frame audio_frames;
191
192
193 static void
194 video (shared_ptr<PlayerVideo>, DCPTime)
195 {
196         ++video_frames;
197 }
198
199 static void
200 audio (shared_ptr<AudioBuffers> audio, DCPTime)
201 {
202         audio_frames += audio->frames();
203 }
204
205
206 /** Check with a video-only file that the video and audio emissions happen more-or-less together */
207 BOOST_AUTO_TEST_CASE (player_interleave_test)
208 {
209         auto film = new_test_film ("ffmpeg_transcoder_basic_test_subs");
210         film->set_name ("ffmpeg_transcoder_basic_test");
211         film->set_container (Ratio::from_id ("185"));
212         film->set_audio_channels (6);
213
214         auto c = std::make_shared<FFmpegContent>("test/data/test.mp4");
215         film->examine_and_add_content (c);
216         BOOST_REQUIRE (!wait_for_jobs ());
217
218         auto s = std::make_shared<StringTextFileContent>("test/data/subrip.srt");
219         film->examine_and_add_content (s);
220         BOOST_REQUIRE (!wait_for_jobs ());
221
222         auto player = std::make_shared<Player>(film, Image::Alignment::COMPACT);
223         player->Video.connect (bind (&video, _1, _2));
224         player->Audio.connect (bind (&audio, _1, _2));
225         video_frames = audio_frames = 0;
226         while (!player->pass ()) {
227                 BOOST_CHECK (abs(video_frames - (audio_frames / 2000)) <= 8);
228         }
229 }
230
231
232 /** Test some seeks towards the start of a DCP with awkward subtitles; see mantis #1085
233  *  and a number of others.  I thought this was a player seek bug but in fact it was
234  *  caused by the subtitle starting just after the start of the video frame and hence
235  *  being faded out.
236  */
237 BOOST_AUTO_TEST_CASE (player_seek_test)
238 {
239         auto film = std::make_shared<Film>(optional<boost::filesystem::path>());
240         auto dcp = std::make_shared<DCPContent>(TestPaths::private_data() / "awkward_subs");
241         film->examine_and_add_content (dcp, true);
242         BOOST_REQUIRE (!wait_for_jobs ());
243         dcp->only_text()->set_use (true);
244
245         auto player = std::make_shared<Player>(film, Image::Alignment::COMPACT);
246         player->set_fast ();
247         player->set_always_burn_open_subtitles ();
248         player->set_play_referenced ();
249
250         auto butler = std::make_shared<Butler>(film, player, AudioMapping(), 2, bind(PlayerVideo::force, AV_PIX_FMT_RGB24), VideoRange::FULL, Image::Alignment::PADDED, true, false);
251         butler->disable_audio();
252
253         for (int i = 0; i < 10; ++i) {
254                 auto t = DCPTime::from_frames (i, 24);
255                 butler->seek (t, true);
256                 auto video = butler->get_video(Butler::Behaviour::BLOCKING, 0);
257                 BOOST_CHECK_EQUAL(video.second.get(), t.get());
258                 write_image(video.first->image(bind(PlayerVideo::force, AV_PIX_FMT_RGB24), VideoRange::FULL, true), String::compose("build/test/player_seek_test_%1.png", i));
259                 /* This 14.08 is empirically chosen (hopefully) to accept changes in rendering between the reference and a test machine
260                    (17.10 and 16.04 seem to anti-alias a little differently) but to reject gross errors e.g. missing fonts or missing
261                    text altogether.
262                 */
263                 check_image(TestPaths::private_data() / String::compose("player_seek_test_%1.png", i), String::compose("build/test/player_seek_test_%1.png", i), 14.08);
264         }
265 }
266
267
268 /** Test some more seeks towards the start of a DCP with awkward subtitles */
269 BOOST_AUTO_TEST_CASE (player_seek_test2)
270 {
271         auto film = std::make_shared<Film>(optional<boost::filesystem::path>());
272         auto dcp = std::make_shared<DCPContent>(TestPaths::private_data() / "awkward_subs2");
273         film->examine_and_add_content (dcp, true);
274         BOOST_REQUIRE (!wait_for_jobs ());
275         dcp->only_text()->set_use (true);
276
277         auto player = std::make_shared<Player>(film, Image::Alignment::COMPACT);
278         player->set_fast ();
279         player->set_always_burn_open_subtitles ();
280         player->set_play_referenced ();
281
282         auto butler = std::make_shared<Butler>(film, player, AudioMapping(), 2, bind(PlayerVideo::force, AV_PIX_FMT_RGB24), VideoRange::FULL, Image::Alignment::PADDED, true, false);
283         butler->disable_audio();
284
285         butler->seek(DCPTime::from_seconds(5), true);
286
287         for (int i = 0; i < 10; ++i) {
288                 auto t = DCPTime::from_seconds(5) + DCPTime::from_frames (i, 24);
289                 butler->seek (t, true);
290                 auto video = butler->get_video(Butler::Behaviour::BLOCKING, 0);
291                 BOOST_CHECK_EQUAL(video.second.get(), t.get());
292                 write_image(
293                         video.first->image(bind(PlayerVideo::force, AV_PIX_FMT_RGB24), VideoRange::FULL, true), String::compose("build/test/player_seek_test2_%1.png", i)
294                         );
295                 check_image(TestPaths::private_data() / String::compose("player_seek_test2_%1.png", i), String::compose("build/test/player_seek_test2_%1.png", i), 14.08);
296         }
297 }
298
299
300 /** Test a bug when trimmed content follows other content */
301 BOOST_AUTO_TEST_CASE (player_trim_test)
302 {
303        auto film = new_test_film2 ("player_trim_test");
304        auto A = content_factory("test/data/flat_red.png").front();
305        film->examine_and_add_content (A);
306        BOOST_REQUIRE (!wait_for_jobs ());
307        A->video->set_length (10 * 24);
308        auto B = content_factory("test/data/flat_red.png").front();
309        film->examine_and_add_content (B);
310        BOOST_REQUIRE (!wait_for_jobs ());
311        B->video->set_length (10 * 24);
312        B->set_position (film, DCPTime::from_seconds(10));
313        B->set_trim_start (ContentTime::from_seconds (2));
314
315        make_and_verify_dcp (film);
316 }
317
318
319 struct Sub {
320         PlayerText text;
321         TextType type;
322         optional<DCPTextTrack> track;
323         DCPTimePeriod period;
324 };
325
326
327 static void
328 store (list<Sub>* out, PlayerText text, TextType type, optional<DCPTextTrack> track, DCPTimePeriod period)
329 {
330         Sub s;
331         s.text = text;
332         s.type = type;
333         s.track = track;
334         s.period = period;
335         out->push_back (s);
336 }
337
338
339 /** Test ignoring both video and audio */
340 BOOST_AUTO_TEST_CASE (player_ignore_video_and_audio_test)
341 {
342         auto film = new_test_film2 ("player_ignore_video_and_audio_test");
343         auto ff = content_factory(TestPaths::private_data() / "boon_telly.mkv").front();
344         film->examine_and_add_content (ff);
345         auto text = content_factory("test/data/subrip.srt").front();
346         film->examine_and_add_content (text);
347         BOOST_REQUIRE (!wait_for_jobs());
348         text->only_text()->set_type (TextType::CLOSED_CAPTION);
349         text->only_text()->set_use (true);
350
351         auto player = std::make_shared<Player>(film, Image::Alignment::COMPACT);
352         player->set_ignore_video ();
353         player->set_ignore_audio ();
354
355         list<Sub> out;
356         player->Text.connect (bind (&store, &out, _1, _2, _3, _4));
357         while (!player->pass ()) {}
358
359         BOOST_CHECK_EQUAL (out.size(), 6U);
360 }
361
362
363 /** Trigger a crash due to the assertion failure in Player::emit_audio */
364 BOOST_AUTO_TEST_CASE (player_trim_crash)
365 {
366         auto film = new_test_film2 ("player_trim_crash");
367         auto boon = content_factory(TestPaths::private_data() / "boon_telly.mkv").front();
368         film->examine_and_add_content (boon);
369         BOOST_REQUIRE (!wait_for_jobs());
370
371         auto player = std::make_shared<Player>(film, Image::Alignment::COMPACT);
372         player->set_fast ();
373         auto butler = std::make_shared<Butler>(film, player, AudioMapping(), 6, bind(&PlayerVideo::force, AV_PIX_FMT_RGB24), VideoRange::FULL, Image::Alignment::COMPACT, true, false);
374
375         /* Wait for the butler to fill */
376         dcpomatic_sleep_seconds (5);
377
378         boon->set_trim_start (ContentTime::from_seconds(5));
379
380         butler->seek (DCPTime(), true);
381
382         /* Wait for the butler to refill */
383         dcpomatic_sleep_seconds (5);
384
385         butler->rethrow ();
386 }
387
388
389 /** Test a crash when the gap between the last audio and the start of a silent period is more than 1 sample */
390 BOOST_AUTO_TEST_CASE (player_silence_crash)
391 {
392         auto film = new_test_film2 ("player_silence_crash");
393         auto sine = content_factory("test/data/impulse_train.wav").front();
394         film->examine_and_add_content (sine);
395         BOOST_REQUIRE (!wait_for_jobs());
396
397         sine->set_video_frame_rate (23.976);
398         film->write_metadata ();
399         make_and_verify_dcp (film, {dcp::VerificationNote::Code::MISSING_CPL_METADATA});
400 }
401
402
403 /** Test a crash when processing a 3D DCP */
404 BOOST_AUTO_TEST_CASE (player_3d_test_1)
405 {
406         auto film = new_test_film2 ("player_3d_test_1a");
407         auto left = content_factory("test/data/flat_red.png").front();
408         film->examine_and_add_content (left);
409         auto right = content_factory("test/data/flat_blue.png").front();
410         film->examine_and_add_content (right);
411         BOOST_REQUIRE (!wait_for_jobs());
412
413         left->video->set_frame_type (VideoFrameType::THREE_D_LEFT);
414         left->set_position (film, DCPTime());
415         right->video->set_frame_type (VideoFrameType::THREE_D_RIGHT);
416         right->set_position (film, DCPTime());
417         film->set_three_d (true);
418
419         make_and_verify_dcp (film);
420
421         auto dcp = std::make_shared<DCPContent>(film->dir(film->dcp_name()));
422         auto film2 = new_test_film2 ("player_3d_test_1b", {dcp});
423
424         film2->set_three_d (true);
425         make_and_verify_dcp (film2);
426 }
427
428
429 /** Test a crash when processing a 3D DCP as content in a 2D project */
430 BOOST_AUTO_TEST_CASE (player_3d_test_2)
431 {
432         auto left = content_factory("test/data/flat_red.png").front();
433         auto right = content_factory("test/data/flat_blue.png").front();
434         auto film = new_test_film2 ("player_3d_test_2a", {left, right});
435
436         left->video->set_frame_type (VideoFrameType::THREE_D_LEFT);
437         left->set_position (film, DCPTime());
438         right->video->set_frame_type (VideoFrameType::THREE_D_RIGHT);
439         right->set_position (film, DCPTime());
440         film->set_three_d (true);
441
442         make_and_verify_dcp (film);
443
444         auto dcp = std::make_shared<DCPContent>(film->dir(film->dcp_name()));
445         auto film2 = new_test_film2 ("player_3d_test_2b", {dcp});
446
447         make_and_verify_dcp (film2);
448 }
449
450
451 /** Test a crash when there is video-only content at the end of the DCP and a frame-rate conversion is happening;
452  *  #1691.
453  */
454 BOOST_AUTO_TEST_CASE (player_silence_at_end_crash)
455 {
456         /* 25fps DCP with some audio */
457         auto content1 = content_factory("test/data/flat_red.png").front();
458         auto film1 = new_test_film2 ("player_silence_at_end_crash_1", {content1});
459         content1->video->set_length (25);
460         film1->set_video_frame_rate (25);
461         make_and_verify_dcp (film1);
462
463         /* Make another project importing this DCP */
464         auto content2 = std::make_shared<DCPContent>(film1->dir(film1->dcp_name()));
465         auto film2 = new_test_film2 ("player_silence_at_end_crash_2", {content2});
466
467         /* and importing just the video MXF on its own at the end */
468         optional<boost::filesystem::path> video;
469         for (auto i: boost::filesystem::directory_iterator(film1->dir(film1->dcp_name()))) {
470                 if (boost::starts_with(i.path().filename().string(), "j2c_")) {
471                         video = i.path();
472                 }
473         }
474
475         BOOST_REQUIRE (video);
476         auto content3 = content_factory(*video).front();
477         film2->examine_and_add_content (content3);
478         BOOST_REQUIRE (!wait_for_jobs());
479         content3->set_position (film2, DCPTime::from_seconds(1.5));
480         film2->set_video_frame_rate (24);
481         make_and_verify_dcp (film2);
482 }