Bv2.1 8.5: Features must have FFEC/FFMC markers.
[libdcp.git] / tools / dcpinfo.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 #include "dcp.h"
35 #include "exceptions.h"
36 #include "reel.h"
37 #include "sound_asset.h"
38 #include "picture_asset.h"
39 #include "subtitle_asset.h"
40 #include "reel_picture_asset.h"
41 #include "reel_sound_asset.h"
42 #include "reel_subtitle_asset.h"
43 #include "subtitle_string.h"
44 #include "subtitle_image.h"
45 #include "interop_subtitle_asset.h"
46 #include "smpte_subtitle_asset.h"
47 #include "mono_picture_asset.h"
48 #include "encrypted_kdm.h"
49 #include "decrypted_kdm.h"
50 #include "cpl.h"
51 #include "common.h"
52 #include "compose.hpp"
53 #include <getopt.h>
54 #include <boost/filesystem.hpp>
55 #include <boost/foreach.hpp>
56 #include <boost/algorithm/string.hpp>
57 #include <iostream>
58 #include <cstdlib>
59 #include <sstream>
60 #include <inttypes.h>
61
62 using std::string;
63 using std::cerr;
64 using std::cout;
65 using std::list;
66 using std::pair;
67 using std::min;
68 using std::max;
69 using std::exception;
70 using std::vector;
71 using std::stringstream;
72 using std::shared_ptr;
73 using std::dynamic_pointer_cast;
74 using boost::optional;
75 using namespace dcp;
76
77 static void
78 help (string n)
79 {
80         cerr << "Syntax: " << n << " [options] [<DCP>] [<CPL>]\n"
81              << "  -s, --subtitles              list all subtitles\n"
82              << "  -p, --picture                analyse picture\n"
83              << "  -d, --decompress             decompress picture when analysing (this is slow)\n"
84              << "  -o, --only                   only output certain pieces of information; see below.\n"
85              << "      --kdm                    KDM to decrypt DCP\n"
86              << "      --private-key            private key for the certificate that the KDM is targeted at\n"
87              << "      --ignore-missing-assets  ignore missing asset files\n";
88
89         cerr << "--only takes a comma-separated list of strings, one or more of:\n"
90                 "    dcp-path     DCP path\n"
91                 "    cpl-name-id  CPL name and ID\n"
92                 "    picture      picture information\n"
93                 "    sound        sound information\n"
94                 "    subtitle     picture information\n"
95                 "    total-time   total DCP time\n";
96 }
97
98 static double
99 mbits_per_second (int size, Fraction frame_rate)
100 {
101         return size * 8 * frame_rate.as_float() / 1e6;
102 }
103
104 #define OUTPUT_DCP_PATH(...)    maybe_output(only, "dcp-path", String::compose(__VA_ARGS__));
105 #define OUTPUT_CPL_NAME_ID(...) maybe_output(only, "cpl-name-id", String::compose(__VA_ARGS__));
106 #define OUTPUT_PICTURE(...)     maybe_output(only, "picture", String::compose(__VA_ARGS__));
107 #define OUTPUT_PICTURE_NC(x)    maybe_output(only, "picture", (x));
108 #define SHOULD_PICTURE          should_output(only, "picture")
109 #define OUTPUT_SOUND(...)       maybe_output(only, "sound", String::compose(__VA_ARGS__));
110 #define OUTPUT_SOUND_NC(x)      maybe_output(only, "sound", (x));
111 #define OUTPUT_SUBTITLE(...)    maybe_output(only, "subtitle", String::compose(__VA_ARGS__));
112 #define OUTPUT_SUBTITLE_NC(x)   maybe_output(only, "subtitle", (x));
113 #define OUTPUT_TOTAL_TIME(...)  maybe_output(only, "total-time", String::compose(__VA_ARGS__));
114
115 static bool
116 should_output(vector<string> const& only, string t)
117 {
118         return only.empty() || find(only.begin(), only.end(), t) != only.end();
119 }
120
121 static void
122 maybe_output(vector<string> const& only, string t, string s)
123 {
124         if (should_output(only, t)) {
125                 cout << s;
126         }
127 }
128
129 static
130 dcp::Time
131 main_picture (vector<string> const& only, shared_ptr<Reel> reel, bool analyse, bool decompress)
132 {
133         shared_ptr<dcp::ReelPictureAsset> mp = reel->main_picture ();
134         if (!mp) {
135                 return dcp::Time();
136         }
137
138         OUTPUT_PICTURE("      Picture ID:  %1", mp->id());
139         if (mp->entry_point()) {
140                 OUTPUT_PICTURE(" entry %1", *mp->entry_point());
141         }
142         if (mp->duration()) {
143                 OUTPUT_PICTURE(
144                         " duration %1 (%2) intrinsic %3",
145                         *mp->duration(),
146                         dcp::Time(*mp->duration(), mp->frame_rate().as_float(), mp->frame_rate().as_float()).as_string(dcp::SMPTE),
147                         mp->intrinsic_duration()
148                         );
149         } else {
150                 OUTPUT_PICTURE(" intrinsic duration %1", mp->intrinsic_duration());
151         }
152
153         if (mp->asset_ref().resolved()) {
154                 if (mp->asset()) {
155                         OUTPUT_PICTURE("\n      Picture:     %1x%2\n", mp->asset()->size().width, mp->asset()->size().height);
156                 }
157
158                 shared_ptr<MonoPictureAsset> ma = dynamic_pointer_cast<MonoPictureAsset>(mp->asset());
159                 if (analyse && ma) {
160                         shared_ptr<MonoPictureAssetReader> reader = ma->start_read ();
161                         pair<int, int> j2k_size_range (INT_MAX, 0);
162                         for (int64_t i = 0; i < ma->intrinsic_duration(); ++i) {
163                                 shared_ptr<const MonoPictureFrame> frame = reader->get_frame (i);
164                                 if (SHOULD_PICTURE) {
165                                         printf("Frame %" PRId64 " J2K size %7d", i, frame->size());
166                                 }
167                                 j2k_size_range.first = min(j2k_size_range.first, frame->size());
168                                 j2k_size_range.second = max(j2k_size_range.second, frame->size());
169
170                                 if (decompress) {
171                                         try {
172                                                 frame->xyz_image();
173                                                 if (SHOULD_PICTURE) {
174                                                         printf(" decrypted OK");
175                                                 }
176                                         } catch (exception& e) {
177                                                 if (SHOULD_PICTURE) {
178                                                         printf(" decryption FAILED");
179                                                 }
180                                         }
181                                 }
182
183                                 if (SHOULD_PICTURE) {
184                                         printf("\n");
185                                 }
186
187                         }
188                         if (SHOULD_PICTURE) {
189                                 printf(
190                                                 "J2K size ranges from %d (%.1f Mbit/s) to %d (%.1f Mbit/s)\n",
191                                                 j2k_size_range.first, mbits_per_second(j2k_size_range.first, ma->frame_rate()),
192                                                 j2k_size_range.second, mbits_per_second(j2k_size_range.second, ma->frame_rate())
193                                       );
194                         }
195                 }
196         } else {
197                 OUTPUT_PICTURE_NC(" - not present in this DCP.\n");
198         }
199
200         return dcp::Time (
201                         mp->duration().get_value_or(mp->intrinsic_duration()),
202                         mp->frame_rate().as_float(),
203                         mp->frame_rate().as_float()
204                         );
205 }
206
207 static
208 void
209 main_sound (vector<string> const& only, shared_ptr<Reel> reel)
210 {
211         shared_ptr<dcp::ReelSoundAsset> ms = reel->main_sound ();
212         if (!ms) {
213                 return;
214         }
215
216         OUTPUT_SOUND("      Sound ID:    %1", ms->id());
217         if (ms->entry_point()) {
218                 OUTPUT_SOUND(" entry %1", *ms->entry_point());
219         }
220         if (ms->duration()) {
221                 OUTPUT_SOUND(" duration %1 intrinsic %2", *ms->duration(), ms->intrinsic_duration());
222         } else {
223                 OUTPUT_SOUND(" intrinsic duration %1", ms->intrinsic_duration());
224         }
225
226         if (ms->asset_ref().resolved()) {
227                 if (ms->asset()) {
228                         OUTPUT_SOUND(
229                                 "\n      Sound:       %1 channels at %2Hz\n",
230                                 ms->asset()->channels(),
231                                 ms->asset()->sampling_rate()
232                                 );
233                 }
234         } else {
235                 OUTPUT_SOUND_NC(" - not present in this DCP.\n");
236         }
237 }
238
239 static
240 void
241 main_subtitle (vector<string> const& only, shared_ptr<Reel> reel, bool list_subtitles)
242 {
243         shared_ptr<dcp::ReelSubtitleAsset> ms = reel->main_subtitle ();
244         if (!ms) {
245                 return;
246         }
247
248         OUTPUT_SUBTITLE("      Subtitle ID: %1", ms->id());
249
250         if (ms->asset_ref().resolved()) {
251                 auto subs = ms->asset()->subtitles ();
252                 OUTPUT_SUBTITLE("\n      Subtitle:    %1 subtitles", subs.size());
253                 shared_ptr<InteropSubtitleAsset> iop = dynamic_pointer_cast<InteropSubtitleAsset> (ms->asset());
254                 if (iop) {
255                         OUTPUT_SUBTITLE(" in %1\n", iop->language());
256                 }
257                 shared_ptr<SMPTESubtitleAsset> smpte = dynamic_pointer_cast<SMPTESubtitleAsset> (ms->asset());
258                 if (smpte && smpte->language ()) {
259                         OUTPUT_SUBTITLE(" in %1\n", smpte->language().get());
260                 }
261                 if (list_subtitles) {
262                         for (auto k: subs) {
263                                 auto ks = dynamic_pointer_cast<const SubtitleString>(k);
264                                 if (ks) {
265                                         stringstream s;
266                                         s << *ks;
267                                         OUTPUT_SUBTITLE("%1\n", s.str());
268                                 }
269                                 auto is = dynamic_pointer_cast<const SubtitleImage>(k);
270                                 if (is) {
271                                         stringstream s;
272                                         s << *is;
273                                         OUTPUT_SUBTITLE("%1\n", s.str());
274                                 }
275                         }
276                 }
277         } else {
278                 OUTPUT_SUBTITLE_NC(" - not present in this DCP.\n");
279         }
280 }
281
282
283 int
284 main (int argc, char* argv[])
285 {
286         dcp::init ();
287
288         bool subtitles = false;
289         bool picture = false;
290         bool decompress = false;
291         bool ignore_missing_assets = false;
292         optional<boost::filesystem::path> kdm;
293         optional<boost::filesystem::path> private_key;
294         optional<string> only_string;
295
296         int option_index = 0;
297         while (true) {
298                 static struct option long_options[] = {
299                         { "version", no_argument, 0, 'v' },
300                         { "help", no_argument, 0, 'h' },
301                         { "subtitles", no_argument, 0, 's' },
302                         { "picture", no_argument, 0, 'p' },
303                         { "decompress", no_argument, 0, 'd' },
304                         { "only", required_argument, 0, 'o' },
305                         { "ignore-missing-assets", no_argument, 0, 'A' },
306                         { "kdm", required_argument, 0, 'B' },
307                         { "private-key", required_argument, 0, 'C' },
308                         { 0, 0, 0, 0 }
309                 };
310
311                 int c = getopt_long (argc, argv, "vhspdo:AB:C:", long_options, &option_index);
312
313                 if (c == -1) {
314                         break;
315                 }
316
317                 switch (c) {
318                 case 'v':
319                         cout << "libdcp version " << LIBDCP_VERSION << "\n";
320                         exit (EXIT_SUCCESS);
321                 case 'h':
322                         help (argv[0]);
323                         exit (EXIT_SUCCESS);
324                 case 's':
325                         subtitles = true;
326                         break;
327                 case 'p':
328                         picture = true;
329                         break;
330                 case 'd':
331                         decompress = true;
332                         break;
333                 case 'o':
334                         only_string = optarg;
335                         break;
336                 case 'A':
337                         ignore_missing_assets = true;
338                         break;
339                 case 'B':
340                         kdm = optarg;
341                         break;
342                 case 'C':
343                         private_key = optarg;
344                         break;
345                 }
346         }
347
348         if (argc <= optind || argc > (optind + 1)) {
349                 help (argv[0]);
350                 exit (EXIT_FAILURE);
351         }
352
353         if (!boost::filesystem::exists (argv[optind])) {
354                 cerr << argv[0] << ": DCP or CPL " << argv[optind] << " not found.\n";
355                 exit (EXIT_FAILURE);
356         }
357
358         vector<string> only;
359         if (only_string) {
360                 only = boost::split(only, *only_string, boost::is_any_of(","));
361         }
362
363         vector<shared_ptr<CPL> > cpls;
364         if (boost::filesystem::is_directory(argv[optind])) {
365                 DCP* dcp = 0;
366                 vector<dcp::VerificationNote> notes;
367                 try {
368                         dcp = new DCP (argv[optind]);
369                         dcp->read (&notes);
370                         if (kdm && private_key) {
371                                 dcp->add(DecryptedKDM(EncryptedKDM(file_to_string(*kdm)), file_to_string(*private_key)));
372                         }
373                 } catch (FileError& e) {
374                         cerr << "Could not read DCP " << argv[optind] << "; " << e.what() << "\n";
375                         exit (EXIT_FAILURE);
376                 } catch (ReadError& e) {
377                         cerr << "Could not read DCP " << argv[optind] << "; " << e.what() << "\n";
378                         exit (EXIT_FAILURE);
379                 } catch (KDMDecryptionError& e) {
380                         cerr << e.what() << "\n";
381                         exit (EXIT_FAILURE);
382                 }
383
384                 OUTPUT_DCP_PATH("DCP: %1\n", boost::filesystem::path(argv[optind]).string());
385
386                 dcp::filter_notes (notes, ignore_missing_assets);
387                 BOOST_FOREACH (dcp::VerificationNote i, notes) {
388                         cerr << "Error: " << note_to_string(i) << "\n";
389                 }
390
391                 cpls = dcp->cpls ();
392         } else {
393                 cpls.push_back (shared_ptr<CPL>(new CPL(boost::filesystem::path(argv[optind]))));
394                 ignore_missing_assets = true;
395         }
396
397         dcp::Time total_time;
398
399         BOOST_FOREACH (shared_ptr<CPL> i, cpls) {
400                 OUTPUT_CPL_NAME_ID("  CPL: %1 %2\n", i->annotation_text().get_value_or(""), i->id());
401
402                 int R = 1;
403                 BOOST_FOREACH (shared_ptr<Reel> j, i->reels()) {
404                         if (should_output(only, "picture") || should_output(only, "sound") || should_output(only, "subtitle")) {
405                                 cout << "    Reel " << R << "\n";
406                         }
407
408                         try {
409                                 total_time += main_picture(only, j, picture, decompress);
410                         } catch (UnresolvedRefError& e) {
411                                 if (!ignore_missing_assets) {
412                                         cerr << e.what() << " (for main picture)\n";
413                                 }
414                         }
415
416                         try {
417                                 main_sound(only, j);
418                         } catch (UnresolvedRefError& e) {
419                                 if (!ignore_missing_assets) {
420                                         cerr << e.what() << " (for main sound)\n";
421                                 }
422                         }
423
424                         try {
425                                 main_subtitle (only, j, subtitles);
426                         } catch (UnresolvedRefError& e) {
427                                 if (!ignore_missing_assets) {
428                                         cerr << e.what() << " (for main subtitle)\n";
429                                 }
430                         }
431
432                         ++R;
433                 }
434         }
435
436         OUTPUT_TOTAL_TIME("Total: %1\n", total_time.as_string(dcp::SMPTE));
437
438         return 0;
439 }