X-Git-Url: https://git.carlh.net/gitweb/?a=blobdiff_plain;f=src%2Fasdcp-test.cpp;h=7889974b59b1fd63c878b75c7ecfe4b3109933f4;hb=ec832f280ffa16392808ee4976deb5b190a6e205;hp=e43569efa41179f230c016422442bd7c2fc8864c;hpb=c589ee9d47d9f00aa4be32c5832a44ce466f014d;p=asdcplib.git diff --git a/src/asdcp-test.cpp b/src/asdcp-test.cpp index e43569e..7889974 100755 --- a/src/asdcp-test.cpp +++ b/src/asdcp-test.cpp @@ -1,5 +1,5 @@ /* -Copyright (c) 2003-2006, John Hurst +Copyright (c) 2003-2008, John Hurst All rights reserved. Redistribution and use in source and binary forms, with or without @@ -47,25 +47,26 @@ THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. these features. */ -#include -#include - #include #include #include #include #include #include +#include + +#include +#include using namespace ASDCP; -const ui32_t FRAME_BUFFER_SIZE = 4*1024*1024; +const ui32_t FRAME_BUFFER_SIZE = 4 * Kumu::Megabyte; //------------------------------------------------------------------------------------------ // // command line option parser class -static const char* PACKAGE = "asdcp-test"; // program name for messages +static const char* PROGRAM_NAME = "asdcp-test"; // program name for messages const ui32_t MAX_IN_FILES = 16; // maximum number of input files handled by // the command option parser @@ -82,27 +83,11 @@ public: memcpy(ProductUUID, default_ProductUUID_Data, UUIDlen); CompanyName = "WidgetCo"; ProductName = "asdcp-test"; - - char s_buf[128]; - snprintf(s_buf, 128, "%u.%u.%u", VERSION_MAJOR, VERSION_APIMINOR, VERSION_IMPMINOR); - ProductVersion = s_buf; + ProductVersion = ASDCP::Version(); } } s_MyInfo; -// Macros used to test command option data state. - -// True if a major mode has already been selected. -#define TEST_MAJOR_MODE() ( info_flag||create_flag||extract_flag||genkey_flag||genid_flag||gop_start_flag ) - -// Causes the caller to return if a major mode has already been selected, -// otherwise sets the given flag. -#define TEST_SET_MAJOR_MODE(f) if ( TEST_MAJOR_MODE() ) \ - { \ - fputs("Conflicting major mode, choose one of -(gcixG)).\n", stderr); \ - return; \ - } \ - (f) = true; // Increment the iterator, test for an additional non-option command line argument. // Causes the caller to return if there are no remaining arguments or if the next @@ -118,11 +103,11 @@ banner(FILE* stream = stdout) { fprintf(stream, "\n\ %s (asdcplib %s)\n\n\ -Copyright (c) 2003-2006 John Hurst\n\n\ +Copyright (c) 2003-2008 John Hurst\n\n\ asdcplib may be copied only under the terms of the license found at\n\ the top of every file in the asdcplib distribution kit.\n\n\ Specify the -h (help) option for further information about %s\n\n", - PACKAGE, ASDCP::Version(), PACKAGE); + PROGRAM_NAME, ASDCP::Version(), PROGRAM_NAME); } // @@ -130,10 +115,10 @@ void usage(FILE* stream = stdout) { fprintf(stream, "\ -USAGE: %s -c [-b ] [-d ] [-e|-E]\n\ +USAGE: %s -c [-3] [-b ] [-d ] [-e|-E]\n\ [-f ] [-j ] [-k ] [-L] [-M]\n\ [-p ] [-R] [-s ] [-v] [-W]\n\ - [ ...]\n\ + [ ...]\n\ \n\ %s [-h|-help] [-V]\n\ \n\ @@ -143,18 +128,27 @@ USAGE: %s -c [-b ] [-d ] [-e|-E]\n\ \n\ %s -G [-v] \n\ \n\ - %s -x [-b ] [-d ]\n\ - [-f ] [-m] [-p ] [-R] [-s ] [-S]\n\ - [-v] [-W] \n\ -\n", PACKAGE, PACKAGE, PACKAGE, PACKAGE, PACKAGE, PACKAGE); + %s -t \n\ +\n\ + %s -x [-3] [-b ] [-d ]\n\ + [-f ] [-m] [-p ] [-R] [-s ] [-S|-1]\n\ + [-v] [-W] [-w] \n\ +\n", PROGRAM_NAME, PROGRAM_NAME, PROGRAM_NAME, PROGRAM_NAME, PROGRAM_NAME, PROGRAM_NAME, PROGRAM_NAME); fprintf(stream, "\ Major modes:\n\ - -c - Create AS-DCP track file from input(s)\n\ + -3 - With -c, create a stereoscopic image file. Expects two\n\ + directories of JP2K codestreams (directories must have\n\ + an equal number of frames; left eye is first).\n\ + - With -x, force stereoscopic interpretation of a JP2K\n\ + track file.\n\ + -c - Create an AS-DCP track file from input(s)\n\ -g - Generate a random 16 byte value to stdout\n\ -G - Perform GOP start lookup test on MXF+Interop MPEG file\n\ -h | -help - Show help\n\ -i - Show file info\n\ + -t - Calculate message digest of input file\n\ + -U - Dump UL catalog to stdout\n\ -u - Generate a random UUID value to stdout\n\ -V - Show version information\n\ -x - Extract essence from AS-DCP file to named file(s)\n\ @@ -183,7 +177,11 @@ Read/Write Options:\n\ essence only, requires -c, -d)\n\ -S - Split Wave essence to stereo WAV files during extract.\n\ Default is multichannel WAV\n\ + -1 - Split Wave essence to mono WAV files during extract.\n\ + Default is multichannel WAV\n\ -W - Read input file only, do not write source file\n\ + -w - Width of numeric element in a series of frame file names\n\ + (use with -x, default 6).\n\ \n"); fprintf(stream, "\ @@ -203,6 +201,21 @@ Other Options:\n\ \n"); } +// +enum MajorMode_t +{ + MMT_NONE, + MMT_INFO, + MMT_CREATE, + MMT_EXTRACT, + MMT_GEN_ID, + MMT_GEN_KEY, + MMT_GOP_START, + MMT_DIGEST, + MMT_UL_LIST, +}; + + // // class CommandOptions @@ -210,19 +223,15 @@ class CommandOptions CommandOptions(); public: + MajorMode_t mode; bool error_flag; // true if the given options are in error or not complete - bool info_flag; // true if the file info mode was selected - bool create_flag; // true if the file create mode was selected - bool extract_flag; // true if the file extract mode was selected - bool genkey_flag; // true if we are to generate a new key value - bool genid_flag; // true if we are to generate a new UUID value - bool gop_start_flag; // true if we are to perform a GOP start lookup test bool key_flag; // true if an encryption key was given bool key_id_flag; // true if a key ID was given bool encrypt_header_flag; // true if mpeg headers are to be encrypted bool write_hmac; // true if HMAC values are to be generated and written bool read_hmac; // true if HMAC values are to be validated bool split_wav; // true if PCM is to be extracted to stereo WAV files + bool mono_wav; // true if PCM is to be extracted to mono WAV files bool verbose_flag; // true if the verbose option was selected ui32_t fb_dump_size; // number of bytes of frame buffer to dump bool showindex_flag; // true if index is to be displayed @@ -230,6 +239,8 @@ public: bool no_write_flag; // true if no output files are to be written bool version_flag; // true if the version display option was selected bool help_flag; // true if the help display option was selected + bool stereo_image_flag; // if true, expect stereoscopic JP2K input (left eye first) + ui32_t number_width; // number of digits in a serialized filename (for JPEG extract) ui32_t start_frame; // frame number to begin processing ui32_t duration; // number of frames to be processed bool duration_flag; // true if duration argument given @@ -262,12 +273,11 @@ public: // CommandOptions(int argc, const char** argv) : - error_flag(true), info_flag(false), create_flag(false), - extract_flag(false), genkey_flag(false), genid_flag(false), gop_start_flag(false), - key_flag(false), key_id_flag(false), encrypt_header_flag(true), - write_hmac(true), read_hmac(false), split_wav(false), + mode(MMT_NONE), error_flag(true), key_flag(false), key_id_flag(false), encrypt_header_flag(true), + write_hmac(true), read_hmac(false), split_wav(false), mono_wav(false), verbose_flag(false), fb_dump_size(0), showindex_flag(false), showheader_flag(false), - no_write_flag(false), version_flag(false), help_flag(false), start_frame(0), + no_write_flag(false), version_flag(false), help_flag(false), stereo_image_flag(false), + number_width(6), start_frame(0), duration(0xffffffff), duration_flag(false), do_repeat(false), use_smpte_labels(false), picture_rate(24), fb_size(FRAME_BUFFER_SIZE), file_count(0), file_root(0), out_file(0) { @@ -283,96 +293,114 @@ public: continue; } - if ( argv[i][0] == '-' && isalpha(argv[i][1]) && argv[i][2] == 0 ) + if ( argv[i][0] == '-' + && ( isalpha(argv[i][1]) || isdigit(argv[i][1]) ) + && argv[i][2] == 0 ) { switch ( argv[i][1] ) { - case 'i': TEST_SET_MAJOR_MODE(info_flag); break; - case 'G': TEST_SET_MAJOR_MODE(gop_start_flag); break; - case 'W': no_write_flag = true; break; - case 'n': showindex_flag = true; break; - case 'H': showheader_flag = true; break; - case 'R': do_repeat = true; break; - case 'S': split_wav = true; break; - case 'V': version_flag = true; break; - case 'h': help_flag = true; break; - case 'v': verbose_flag = true; break; - case 'g': genkey_flag = true; break; - case 'u': genid_flag = true; break; - case 'e': encrypt_header_flag = true; break; - case 'E': encrypt_header_flag = false; break; - case 'M': write_hmac = false; break; - case 'm': read_hmac = true; break; - case 'L': use_smpte_labels = true; break; + case '1': mono_wav = true; break; + case '2': split_wav = true; break; + case '3': stereo_image_flag = true; break; + + case 'b': + TEST_EXTRA_ARG(i, 'b'); + fb_size = abs(atoi(argv[i])); + + if ( verbose_flag ) + fprintf(stderr, "Frame Buffer size: %u bytes.\n", fb_size); + + break; case 'c': - TEST_SET_MAJOR_MODE(create_flag); TEST_EXTRA_ARG(i, 'c'); + mode = MMT_CREATE; out_file = argv[i]; break; - case 'x': - TEST_SET_MAJOR_MODE(extract_flag); - TEST_EXTRA_ARG(i, 'x'); - file_root = argv[i]; + case 'd': + TEST_EXTRA_ARG(i, 'd'); + duration_flag = true; + duration = abs(atoi(argv[i])); break; - case 'k': key_flag = true; - TEST_EXTRA_ARG(i, 'k'); + case 'E': encrypt_header_flag = false; break; + case 'e': encrypt_header_flag = true; break; + + case 'f': + TEST_EXTRA_ARG(i, 'f'); + start_frame = abs(atoi(argv[i])); + break; + + case 'G': mode = MMT_GOP_START; break; + case 'g': mode = MMT_GEN_KEY; break; + case 'H': showheader_flag = true; break; + case 'h': help_flag = true; break; + case 'i': mode = MMT_INFO; break; + + case 'j': key_id_flag = true; + TEST_EXTRA_ARG(i, 'j'); { ui32_t length; - Kumu::hex2bin(argv[i], key_value, KeyLen, &length); + Kumu::hex2bin(argv[i], key_id_value, UUIDlen, &length); - if ( length != KeyLen ) + if ( length != UUIDlen ) { - fprintf(stderr, "Unexpected key length: %u, expecting %u characters.\n", KeyLen, length); + fprintf(stderr, "Unexpected key ID length: %u, expecting %u characters.\n", length, UUIDlen); return; } } break; - case 'j': key_id_flag = true; - TEST_EXTRA_ARG(i, 'j'); + case 'k': key_flag = true; + TEST_EXTRA_ARG(i, 'k'); { ui32_t length; - Kumu::hex2bin(argv[i], key_id_value, UUIDlen, &length); + Kumu::hex2bin(argv[i], key_value, KeyLen, &length); - if ( length != UUIDlen ) + if ( length != KeyLen ) { - fprintf(stderr, "Unexpected key ID length: %u, expecting %u characters.\n", UUIDlen, length); + fprintf(stderr, "Unexpected key length: %u, expecting %u characters.\n", length, KeyLen); return; } } break; - case 'f': - TEST_EXTRA_ARG(i, 'f'); - start_frame = atoi(argv[i]); // TODO: test for negative value, should use strtol() - break; - case 'd': - TEST_EXTRA_ARG(i, 'd'); - duration_flag = true; - duration = atoi(argv[i]); // TODO: test for negative value, should use strtol() - break; + case 'L': use_smpte_labels = true; break; + case 'M': write_hmac = false; break; + case 'm': read_hmac = true; break; + case 'n': showindex_flag = true; break; case 'p': TEST_EXTRA_ARG(i, 'p'); - picture_rate = atoi(argv[i]); + picture_rate = abs(atoi(argv[i])); break; + case 'R': do_repeat = true; break; + case 'S': split_wav = true; break; + case 's': TEST_EXTRA_ARG(i, 's'); - fb_dump_size = atoi(argv[i]); + fb_dump_size = abs(atoi(argv[i])); break; - case 'b': - TEST_EXTRA_ARG(i, 'b'); - fb_size = atoi(argv[i]); + case 't': mode = MMT_DIGEST; break; + case 'U': mode = MMT_UL_LIST; break; + case 'u': mode = MMT_GEN_ID; break; + case 'V': version_flag = true; break; + case 'v': verbose_flag = true; break; + case 'W': no_write_flag = true; break; - if ( verbose_flag ) - fprintf(stderr, "Frame Buffer size: %u bytes.\n", fb_size); + case 'w': + TEST_EXTRA_ARG(i, 'w'); + number_width = abs(atoi(argv[i])); + break; + case 'x': + TEST_EXTRA_ARG(i, 'x'); + mode = MMT_EXTRACT; + file_root = argv[i]; break; default: @@ -389,7 +417,7 @@ public: } else { - fprintf(stderr, "Unrecognized option: %s\n", argv[i]); + fprintf(stderr, "Unrecognized argument: %s\n", argv[i]); return; } @@ -404,18 +432,19 @@ public: if ( help_flag || version_flag ) return; - if ( TEST_MAJOR_MODE() ) + if ( ( mode == MMT_INFO + || mode == MMT_CREATE + || mode == MMT_EXTRACT + || mode == MMT_GOP_START + || mode == MMT_DIGEST ) && file_count == 0 ) { - if ( ! genkey_flag && ! genid_flag && file_count == 0 ) - { - fputs("Option requires at least one filename argument.\n", stderr); - return; - } + fputs("Option requires at least one filename argument.\n", stderr); + return; } - if ( ! TEST_MAJOR_MODE() && ! help_flag && ! version_flag ) + if ( mode == MMT_NONE && ! help_flag && ! version_flag ) { - fputs("No operation selected (use one of -(gcixG) or -h for help).\n", stderr); + fputs("No operation selected (use one of -[gGcitux] or -h for help).\n", stderr); return; } @@ -674,34 +703,42 @@ gop_start_test(CommandOptions& Options) //------------------------------------------------------------------------------------------ // JPEG 2000 essence -// Write one or more plaintext JPEG 2000 codestreams to a plaintext ASDCP file -// Write one or more plaintext JPEG 2000 codestreams to a ciphertext ASDCP file +// Write one or more plaintext JPEG 2000 stereoscopic codestream pairs to a plaintext ASDCP file +// Write one or more plaintext JPEG 2000 stereoscopic codestream pairs to a ciphertext ASDCP file // Result_t -write_JP2K_file(CommandOptions& Options) +write_JP2K_S_file(CommandOptions& Options) { AESEncContext* Context = 0; HMACContext* HMAC = 0; - JP2K::MXFWriter Writer; + JP2K::MXFSWriter Writer; JP2K::FrameBuffer FrameBuffer(Options.fb_size); JP2K::PictureDescriptor PDesc; - JP2K::SequenceParser Parser; + JP2K::SequenceParser ParserLeft, ParserRight; byte_t IV_buf[CBC_BLOCK_SIZE]; Kumu::FortunaRNG RNG; + if ( Options.file_count != 2 ) + { + fprintf(stderr, "Two inputs are required for stereoscopic option.\n"); + return RESULT_FAIL; + } + // set up essence parser - Result_t result = Parser.OpenRead(Options.filenames[0]); + Result_t result = ParserLeft.OpenRead(Options.filenames[0]); + + if ( ASDCP_SUCCESS(result) ) + result = ParserRight.OpenRead(Options.filenames[1]); // set up MXF writer if ( ASDCP_SUCCESS(result) ) { - Parser.FillPictureDescriptor(PDesc); + ParserLeft.FillPictureDescriptor(PDesc); PDesc.EditRate = Options.PictureRate(); if ( Options.verbose_flag ) { - fprintf(stderr, "JPEG 2000 pictures\n"); - fputs("PictureDescriptor:\n", stderr); + fputs("JPEG 2000 stereoscopic pictures\nPictureDescriptor:\n", stderr); fprintf(stderr, "Frame Buffer size: %u\n", Options.fb_size); JP2K::PictureDescriptorDump(PDesc); } @@ -750,35 +787,39 @@ write_JP2K_file(CommandOptions& Options) if ( ASDCP_SUCCESS(result) ) { ui32_t duration = 0; - result = Parser.Reset(); + result = ParserLeft.Reset(); + if ( ASDCP_SUCCESS(result) ) result = ParserRight.Reset(); while ( ASDCP_SUCCESS(result) && duration++ < Options.duration ) { - if ( ! Options.do_repeat || duration == 1 ) - { - result = Parser.ReadFrame(FrameBuffer); + result = ParserLeft.ReadFrame(FrameBuffer); - if ( ASDCP_SUCCESS(result) ) - { - if ( Options.verbose_flag ) - FrameBuffer.Dump(stderr, Options.fb_dump_size); + if ( ASDCP_SUCCESS(result) ) + { + if ( Options.verbose_flag ) + FrameBuffer.Dump(stderr, Options.fb_dump_size); - if ( Options.encrypt_header_flag ) - FrameBuffer.PlaintextOffset(0); - } + if ( Options.encrypt_header_flag ) + FrameBuffer.PlaintextOffset(0); } if ( ASDCP_SUCCESS(result) && ! Options.no_write_flag ) - { - result = Writer.WriteFrame(FrameBuffer, Context, HMAC); + result = Writer.WriteFrame(FrameBuffer, JP2K::SP_LEFT, Context, HMAC); - // The Writer class will forward the last block of ciphertext - // to the encryption context for use as the IV for the next - // frame. If you want to use non-sequitur IV values, un-comment - // the following line of code. - // if ( ASDCP_SUCCESS(result) && Options.key_flag ) - // Context->SetIVec(RNG.FillRandom(IV_buf, CBC_BLOCK_SIZE)); + if ( ASDCP_SUCCESS(result) ) + result = ParserRight.ReadFrame(FrameBuffer); + + if ( ASDCP_SUCCESS(result) ) + { + if ( Options.verbose_flag ) + FrameBuffer.Dump(stderr, Options.fb_dump_size); + + if ( Options.encrypt_header_flag ) + FrameBuffer.PlaintextOffset(0); } + + if ( ASDCP_SUCCESS(result) && ! Options.no_write_flag ) + result = Writer.WriteFrame(FrameBuffer, JP2K::SP_RIGHT, Context, HMAC); } if ( result == RESULT_ENDOFFILE ) @@ -791,16 +832,15 @@ write_JP2K_file(CommandOptions& Options) return result; } -// Read one or more plaintext JPEG 2000 codestreams from a plaintext ASDCP file -// Read one or more plaintext JPEG 2000 codestreams from a ciphertext ASDCP file -// Read one or more ciphertext JPEG 2000 codestreams from a ciphertext ASDCP file -// +// Read one or more plaintext JPEG 2000 stereoscopic codestream pairs from a plaintext ASDCP file +// Read one or more plaintext JPEG 2000 stereoscopic codestream pairs from a ciphertext ASDCP file +// Read one or more ciphertext JPEG 2000 stereoscopic codestream pairs from a ciphertext ASDCP file Result_t -read_JP2K_file(CommandOptions& Options) +read_JP2K_S_file(CommandOptions& Options) { AESDecContext* Context = 0; HMACContext* HMAC = 0; - JP2K::MXFReader Reader; + JP2K::MXFSReader Reader; JP2K::FrameBuffer FrameBuffer(Options.fb_size); ui32_t frame_count = 0; @@ -842,20 +882,25 @@ read_JP2K_file(CommandOptions& Options) } } + const int filename_max = 1024; + char filename[filename_max]; ui32_t last_frame = Options.start_frame + ( Options.duration ? Options.duration : frame_count); if ( last_frame > frame_count ) last_frame = frame_count; + char left_format[64]; char right_format[64]; + snprintf(left_format, 64, "%%s%%0%duL.j2c", Options.number_width); + snprintf(right_format, 64, "%%s%%0%duR.j2c", Options.number_width); + for ( ui32_t i = Options.start_frame; ASDCP_SUCCESS(result) && i < last_frame; i++ ) { - result = Reader.ReadFrame(i, FrameBuffer, Context, HMAC); + result = Reader.ReadFrame(i, JP2K::SP_LEFT, FrameBuffer, Context, HMAC); if ( ASDCP_SUCCESS(result) ) { Kumu::FileWriter OutFile; - char filename[256]; ui32_t write_count; - snprintf(filename, 256, "%s%06u.j2c", Options.file_root, i); + snprintf(filename, filename_max, left_format, Options.file_root, i); result = OutFile.OpenWrite(filename); if ( ASDCP_SUCCESS(result) ) @@ -864,49 +909,57 @@ read_JP2K_file(CommandOptions& Options) if ( Options.verbose_flag ) FrameBuffer.Dump(stderr, Options.fb_dump_size); } + + if ( ASDCP_SUCCESS(result) ) + result = Reader.ReadFrame(i, JP2K::SP_RIGHT, FrameBuffer, Context, HMAC); + + if ( ASDCP_SUCCESS(result) ) + { + Kumu::FileWriter OutFile; + ui32_t write_count; + snprintf(filename, filename_max, right_format, Options.file_root, i); + result = OutFile.OpenWrite(filename); + + if ( ASDCP_SUCCESS(result) ) + result = OutFile.Write(FrameBuffer.Data(), FrameBuffer.Size(), &write_count); + } } return result; } -//------------------------------------------------------------------------------------------ -// PCM essence -// Write one or more plaintext PCM audio streams to a plaintext ASDCP file -// Write one or more plaintext PCM audio streams to a ciphertext ASDCP file +// Write one or more plaintext JPEG 2000 codestreams to a plaintext ASDCP file +// Write one or more plaintext JPEG 2000 codestreams to a ciphertext ASDCP file // Result_t -write_PCM_file(CommandOptions& Options) +write_JP2K_file(CommandOptions& Options) { - AESEncContext* Context = 0; - HMACContext* HMAC = 0; - PCMParserList Parser; - PCM::MXFWriter Writer; - PCM::FrameBuffer FrameBuffer; - PCM::AudioDescriptor ADesc; - Rational PictureRate = Options.PictureRate(); - byte_t IV_buf[CBC_BLOCK_SIZE]; - Kumu::FortunaRNG RNG; + AESEncContext* Context = 0; + HMACContext* HMAC = 0; + JP2K::MXFWriter Writer; + JP2K::FrameBuffer FrameBuffer(Options.fb_size); + JP2K::PictureDescriptor PDesc; + JP2K::SequenceParser Parser; + byte_t IV_buf[CBC_BLOCK_SIZE]; + Kumu::FortunaRNG RNG; // set up essence parser - Result_t result = Parser.OpenRead(Options.file_count, Options.filenames, PictureRate); + Result_t result = Parser.OpenRead(Options.filenames[0]); // set up MXF writer if ( ASDCP_SUCCESS(result) ) { - Parser.FillAudioDescriptor(ADesc); - - ADesc.SampleRate = PictureRate; - FrameBuffer.Capacity(PCM::CalcFrameBufferSize(ADesc)); + Parser.FillPictureDescriptor(PDesc); + PDesc.EditRate = Options.PictureRate(); if ( Options.verbose_flag ) { - fprintf(stderr, "48Khz PCM Audio, %s fps (%u spf)\n", - Options.szPictureRate(), - PCM::CalcSamplesPerFrame(ADesc)); - fputs("AudioDescriptor:\n", stderr); - PCM::AudioDescriptorDump(ADesc); + fprintf(stderr, "JPEG 2000 pictures\n"); + fputs("PictureDescriptor:\n", stderr); + fprintf(stderr, "Frame Buffer size: %u\n", Options.fb_size); + JP2K::PictureDescriptorDump(PDesc); } } @@ -947,42 +1000,40 @@ write_PCM_file(CommandOptions& Options) } if ( ASDCP_SUCCESS(result) ) - result = Writer.OpenWrite(Options.out_file, Info, ADesc); + result = Writer.OpenWrite(Options.out_file, Info, PDesc); } if ( ASDCP_SUCCESS(result) ) { - result = Parser.Reset(); ui32_t duration = 0; + result = Parser.Reset(); while ( ASDCP_SUCCESS(result) && duration++ < Options.duration ) { - result = Parser.ReadFrame(FrameBuffer); - - if ( ASDCP_SUCCESS(result) ) + if ( ! Options.do_repeat || duration == 1 ) { - if ( FrameBuffer.Size() != FrameBuffer.Capacity() ) + result = Parser.ReadFrame(FrameBuffer); + + if ( ASDCP_SUCCESS(result) ) { - fprintf(stderr, "WARNING: Last frame read was short, PCM input is possibly not frame aligned.\n"); - fprintf(stderr, "Expecting %u bytes, got %u.\n", FrameBuffer.Capacity(), FrameBuffer.Size()); - result = RESULT_ENDOFFILE; - continue; + if ( Options.verbose_flag ) + FrameBuffer.Dump(stderr, Options.fb_dump_size); + + if ( Options.encrypt_header_flag ) + FrameBuffer.PlaintextOffset(0); } + } - if ( Options.verbose_flag ) - FrameBuffer.Dump(stderr, Options.fb_dump_size); - - if ( ! Options.no_write_flag ) - { - result = Writer.WriteFrame(FrameBuffer, Context, HMAC); + if ( ASDCP_SUCCESS(result) && ! Options.no_write_flag ) + { + result = Writer.WriteFrame(FrameBuffer, Context, HMAC); - // The Writer class will forward the last block of ciphertext - // to the encryption context for use as the IV for the next - // frame. If you want to use non-sequitur IV values, un-comment - // the following line of code. - // if ( ASDCP_SUCCESS(result) && Options.key_flag ) - // Context->SetIVec(RNG.FillRandom(IV_buf, CBC_BLOCK_SIZE)); - } + // The Writer class will forward the last block of ciphertext + // to the encryption context for use as the IV for the next + // frame. If you want to use non-sequitur IV values, un-comment + // the following line of code. + // if ( ASDCP_SUCCESS(result) && Options.key_flag ) + // Context->SetIVec(RNG.FillRandom(IV_buf, CBC_BLOCK_SIZE)); } } @@ -996,9 +1047,217 @@ write_PCM_file(CommandOptions& Options) return result; } -// Read one or more plaintext PCM audio streams from a plaintext ASDCP file -// Read one or more plaintext PCM audio streams from a ciphertext ASDCP file -// Read one or more ciphertext PCM audio streams from a ciphertext ASDCP file +// Read one or more plaintext JPEG 2000 codestreams from a plaintext ASDCP file +// Read one or more plaintext JPEG 2000 codestreams from a ciphertext ASDCP file +// Read one or more ciphertext JPEG 2000 codestreams from a ciphertext ASDCP file +// +Result_t +read_JP2K_file(CommandOptions& Options) +{ + AESDecContext* Context = 0; + HMACContext* HMAC = 0; + JP2K::MXFReader Reader; + JP2K::FrameBuffer FrameBuffer(Options.fb_size); + ui32_t frame_count = 0; + + Result_t result = Reader.OpenRead(Options.filenames[0]); + + if ( ASDCP_SUCCESS(result) ) + { + JP2K::PictureDescriptor PDesc; + Reader.FillPictureDescriptor(PDesc); + + frame_count = PDesc.ContainerDuration; + + if ( Options.verbose_flag ) + { + fprintf(stderr, "Frame Buffer size: %u\n", Options.fb_size); + JP2K::PictureDescriptorDump(PDesc); + } + } + + if ( ASDCP_SUCCESS(result) && Options.key_flag ) + { + Context = new AESDecContext; + result = Context->InitKey(Options.key_value); + + if ( ASDCP_SUCCESS(result) && Options.read_hmac ) + { + WriterInfo Info; + Reader.FillWriterInfo(Info); + + if ( Info.UsesHMAC ) + { + HMAC = new HMACContext; + result = HMAC->InitKey(Options.key_value, Info.LabelSetType); + } + else + { + fputs("File does not contain HMAC values, ignoring -m option.\n", stderr); + } + } + } + + ui32_t last_frame = Options.start_frame + ( Options.duration ? Options.duration : frame_count); + if ( last_frame > frame_count ) + last_frame = frame_count; + + char name_format[64]; + snprintf(name_format, 64, "%%s%%0%du.j2c", Options.number_width); + + for ( ui32_t i = Options.start_frame; ASDCP_SUCCESS(result) && i < last_frame; i++ ) + { + result = Reader.ReadFrame(i, FrameBuffer, Context, HMAC); + + if ( ASDCP_SUCCESS(result) ) + { + Kumu::FileWriter OutFile; + char filename[256]; + ui32_t write_count; + snprintf(filename, 256, name_format, Options.file_root, i); + result = OutFile.OpenWrite(filename); + + if ( ASDCP_SUCCESS(result) ) + result = OutFile.Write(FrameBuffer.Data(), FrameBuffer.Size(), &write_count); + + if ( Options.verbose_flag ) + FrameBuffer.Dump(stderr, Options.fb_dump_size); + } + } + + return result; +} + +//------------------------------------------------------------------------------------------ +// PCM essence + + +// Write one or more plaintext PCM audio streams to a plaintext ASDCP file +// Write one or more plaintext PCM audio streams to a ciphertext ASDCP file +// +Result_t +write_PCM_file(CommandOptions& Options) +{ + AESEncContext* Context = 0; + HMACContext* HMAC = 0; + PCMParserList Parser; + PCM::MXFWriter Writer; + PCM::FrameBuffer FrameBuffer; + PCM::AudioDescriptor ADesc; + Rational PictureRate = Options.PictureRate(); + byte_t IV_buf[CBC_BLOCK_SIZE]; + Kumu::FortunaRNG RNG; + + // set up essence parser + Result_t result = Parser.OpenRead(Options.file_count, Options.filenames, PictureRate); + + // set up MXF writer + if ( ASDCP_SUCCESS(result) ) + { + Parser.FillAudioDescriptor(ADesc); + + ADesc.SampleRate = PictureRate; + FrameBuffer.Capacity(PCM::CalcFrameBufferSize(ADesc)); + + if ( Options.verbose_flag ) + { + fprintf(stderr, "48Khz PCM Audio, %s fps (%u spf)\n", + Options.szPictureRate(), + PCM::CalcSamplesPerFrame(ADesc)); + fputs("AudioDescriptor:\n", stderr); + PCM::AudioDescriptorDump(ADesc); + } + } + + if ( ASDCP_SUCCESS(result) && ! Options.no_write_flag ) + { + WriterInfo Info = s_MyInfo; // fill in your favorite identifiers here + Kumu::GenRandomUUID(Info.AssetUUID); + + if ( Options.use_smpte_labels ) + { + Info.LabelSetType = LS_MXF_SMPTE; + fprintf(stderr, "ATTENTION! Writing SMPTE Universal Labels\n"); + } + + // configure encryption + if( Options.key_flag ) + { + Kumu::GenRandomUUID(Info.ContextID); + Info.EncryptedEssence = true; + + if ( Options.key_id_flag ) + memcpy(Info.CryptographicKeyID, Options.key_id_value, UUIDlen); + else + RNG.FillRandom(Info.CryptographicKeyID, UUIDlen); + + Context = new AESEncContext; + result = Context->InitKey(Options.key_value); + + if ( ASDCP_SUCCESS(result) ) + result = Context->SetIVec(RNG.FillRandom(IV_buf, CBC_BLOCK_SIZE)); + + if ( ASDCP_SUCCESS(result) && Options.write_hmac ) + { + Info.UsesHMAC = true; + HMAC = new HMACContext; + result = HMAC->InitKey(Options.key_value, Info.LabelSetType); + } + } + + if ( ASDCP_SUCCESS(result) ) + result = Writer.OpenWrite(Options.out_file, Info, ADesc); + } + + if ( ASDCP_SUCCESS(result) ) + { + result = Parser.Reset(); + ui32_t duration = 0; + + while ( ASDCP_SUCCESS(result) && duration++ < Options.duration ) + { + result = Parser.ReadFrame(FrameBuffer); + + if ( ASDCP_SUCCESS(result) ) + { + if ( FrameBuffer.Size() != FrameBuffer.Capacity() ) + { + fprintf(stderr, "WARNING: Last frame read was short, PCM input is possibly not frame aligned.\n"); + fprintf(stderr, "Expecting %u bytes, got %u.\n", FrameBuffer.Capacity(), FrameBuffer.Size()); + result = RESULT_ENDOFFILE; + continue; + } + + if ( Options.verbose_flag ) + FrameBuffer.Dump(stderr, Options.fb_dump_size); + + if ( ! Options.no_write_flag ) + { + result = Writer.WriteFrame(FrameBuffer, Context, HMAC); + + // The Writer class will forward the last block of ciphertext + // to the encryption context for use as the IV for the next + // frame. If you want to use non-sequitur IV values, un-comment + // the following line of code. + // if ( ASDCP_SUCCESS(result) && Options.key_flag ) + // Context->SetIVec(RNG.FillRandom(IV_buf, CBC_BLOCK_SIZE)); + } + } + } + + if ( result == RESULT_ENDOFFILE ) + result = RESULT_OK; + } + + if ( ASDCP_SUCCESS(result) && ! Options.no_write_flag ) + result = Writer.Finalize(); + + return result; +} + +// Read one or more plaintext PCM audio streams from a plaintext ASDCP file +// Read one or more plaintext PCM audio streams from a ciphertext ASDCP file +// Read one or more ciphertext PCM audio streams from a ciphertext ASDCP file // Result_t read_PCM_file(CommandOptions& Options) @@ -1047,7 +1306,9 @@ read_PCM_file(CommandOptions& Options) } ADesc.ContainerDuration = last_frame - Options.start_frame; - OutWave.OpenWrite(ADesc, Options.file_root, Options.split_wav); + OutWave.OpenWrite(ADesc, Options.file_root, + ( Options.split_wav ? WavFileWriter::ST_STEREO : + ( Options.mono_wav ? WavFileWriter::ST_MONO : WavFileWriter::ST_NONE ) )); } if ( ASDCP_SUCCESS(result) && Options.key_flag ) @@ -1089,6 +1350,198 @@ read_PCM_file(CommandOptions& Options) } +//------------------------------------------------------------------------------------------ +// TimedText essence + + +// Write one or more plaintext timed text streams to a plaintext ASDCP file +// Write one or more plaintext timed text streams to a ciphertext ASDCP file +// +Result_t +write_timed_text_file(CommandOptions& Options) +{ + AESEncContext* Context = 0; + HMACContext* HMAC = 0; + TimedText::DCSubtitleParser Parser; + TimedText::MXFWriter Writer; + TimedText::FrameBuffer FrameBuffer; + TimedText::TimedTextDescriptor TDesc; + byte_t IV_buf[CBC_BLOCK_SIZE]; + Kumu::FortunaRNG RNG; + + // set up essence parser + Result_t result = Parser.OpenRead(Options.filenames[0]); + + // set up MXF writer + if ( ASDCP_SUCCESS(result) ) + { + Parser.FillDescriptor(TDesc); + FrameBuffer.Capacity(2*Kumu::Megabyte); + + if ( Options.verbose_flag ) + { + fputs("D-Cinema Timed-Text Descriptor:\n", stderr); + TimedText::DescriptorDump(TDesc); + } + } + + if ( ASDCP_SUCCESS(result) && ! Options.no_write_flag ) + { + WriterInfo Info = s_MyInfo; // fill in your favorite identifiers here + Kumu::GenRandomUUID(Info.AssetUUID); + + if ( Options.use_smpte_labels ) + { + Info.LabelSetType = LS_MXF_SMPTE; + fprintf(stderr, "ATTENTION! Writing SMPTE Universal Labels\n"); + } + + // configure encryption + if( Options.key_flag ) + { + Kumu::GenRandomUUID(Info.ContextID); + Info.EncryptedEssence = true; + + if ( Options.key_id_flag ) + memcpy(Info.CryptographicKeyID, Options.key_id_value, UUIDlen); + else + RNG.FillRandom(Info.CryptographicKeyID, UUIDlen); + + Context = new AESEncContext; + result = Context->InitKey(Options.key_value); + + if ( ASDCP_SUCCESS(result) ) + result = Context->SetIVec(RNG.FillRandom(IV_buf, CBC_BLOCK_SIZE)); + + if ( ASDCP_SUCCESS(result) && Options.write_hmac ) + { + Info.UsesHMAC = true; + HMAC = new HMACContext; + result = HMAC->InitKey(Options.key_value, Info.LabelSetType); + } + } + + if ( ASDCP_SUCCESS(result) ) + result = Writer.OpenWrite(Options.out_file, Info, TDesc); + } + + if ( ASDCP_FAILURE(result) ) + return result; + + std::string XMLDoc; + TimedText::ResourceList_t::const_iterator ri; + + result = Parser.ReadTimedTextResource(XMLDoc); + + if ( ASDCP_SUCCESS(result) ) + result = Writer.WriteTimedTextResource(XMLDoc, Context, HMAC); + + for ( ri = TDesc.ResourceList.begin() ; ri != TDesc.ResourceList.end() && ASDCP_SUCCESS(result); ri++ ) + { + result = Parser.ReadAncillaryResource((*ri).ResourceID, FrameBuffer); + + if ( ASDCP_SUCCESS(result) ) + { + if ( Options.verbose_flag ) + FrameBuffer.Dump(stderr, Options.fb_dump_size); + + if ( ! Options.no_write_flag ) + { + result = Writer.WriteAncillaryResource(FrameBuffer, Context, HMAC); + + // The Writer class will forward the last block of ciphertext + // to the encryption context for use as the IV for the next + // frame. If you want to use non-sequitur IV values, un-comment + // the following line of code. + // if ( ASDCP_SUCCESS(result) && Options.key_flag ) + // Context->SetIVec(RNG.FillRandom(IV_buf, CBC_BLOCK_SIZE)); + } + } + + if ( result == RESULT_ENDOFFILE ) + result = RESULT_OK; + } + + if ( ASDCP_SUCCESS(result) && ! Options.no_write_flag ) + result = Writer.Finalize(); + + return result; +} + + +// Read one or more timed text streams from a plaintext ASDCP file +// Read one or more timed text streams from a ciphertext ASDCP file +// Read one or more timed text streams from a ciphertext ASDCP file +// +Result_t +read_timed_text_file(CommandOptions& Options) +{ + AESDecContext* Context = 0; + HMACContext* HMAC = 0; + TimedText::MXFReader Reader; + TimedText::FrameBuffer FrameBuffer; + TimedText::TimedTextDescriptor TDesc; + + Result_t result = Reader.OpenRead(Options.filenames[0]); + + if ( ASDCP_SUCCESS(result) ) + { + Reader.FillDescriptor(TDesc); + FrameBuffer.Capacity(2*Kumu::Megabyte); + + if ( Options.verbose_flag ) + TimedText::DescriptorDump(TDesc); + } + + if ( ASDCP_SUCCESS(result) && Options.key_flag ) + { + Context = new AESDecContext; + result = Context->InitKey(Options.key_value); + + if ( ASDCP_SUCCESS(result) && Options.read_hmac ) + { + WriterInfo Info; + Reader.FillWriterInfo(Info); + + if ( Info.UsesHMAC ) + { + HMAC = new HMACContext; + result = HMAC->InitKey(Options.key_value, Info.LabelSetType); + } + else + { + fputs("File does not contain HMAC values, ignoring -m option.\n", stderr); + } + } + } + + if ( ASDCP_FAILURE(result) ) + return result; + + std::string XMLDoc; + TimedText::ResourceList_t::const_iterator ri; + + result = Reader.ReadTimedTextResource(XMLDoc, Context, HMAC); + + // do something with the XML here + fprintf(stderr, "XMLDoc size: %lu\n", XMLDoc.size()); + + for ( ri = TDesc.ResourceList.begin() ; ri != TDesc.ResourceList.end() && ASDCP_SUCCESS(result); ri++ ) + { + result = Reader.ReadAncillaryResource((*ri).ResourceID, FrameBuffer, Context, HMAC); + + if ( ASDCP_SUCCESS(result) ) + { + // if ( Options.verbose_flag ) + FrameBuffer.Dump(stderr, Options.fb_dump_size); + + // do something with the resource data here + } + } + + return result; +} + //------------------------------------------------------------------------------------------ // @@ -1121,6 +1574,18 @@ class MyPictureDescriptor : public JP2K::PictureDescriptor } }; +class MyStereoPictureDescriptor : public JP2K::PictureDescriptor +{ + public: + void FillDescriptor(JP2K::MXFSReader& Reader) { + Reader.FillPictureDescriptor(*this); + } + + void Dump(FILE* stream) { + JP2K::PictureDescriptorDump(*this, stream); + } +}; + class MyAudioDescriptor : public PCM::AudioDescriptor { public: @@ -1133,24 +1598,41 @@ class MyAudioDescriptor : public PCM::AudioDescriptor } }; +class MyTextDescriptor : public TimedText::TimedTextDescriptor +{ + public: + void FillDescriptor(TimedText::MXFReader& Reader) { + Reader.FillDescriptor(*this); + } + + void Dump(FILE* stream) { + TimedText::DescriptorDump(*this, stream); + } +}; // MSVC didn't like the function template, so now it's a static class method template class FileInfoWrapper { public: - static void file_info(CommandOptions& Options, FILE* stream = 0) + static Result_t + file_info(CommandOptions& Options, const char* type_string, FILE* stream = 0) { + assert(type_string); if ( stream == 0 ) stream = stdout; + Result_t result = RESULT_OK; + if ( Options.verbose_flag || Options.showheader_flag ) { ReaderT Reader; - Result_t result = Reader.OpenRead(Options.filenames[0]); + result = Reader.OpenRead(Options.filenames[0]); if ( ASDCP_SUCCESS(result) ) { + fprintf(stdout, "File essence type is %s.\n", type_string); + if ( Options.showheader_flag ) Reader.DumpHeaderMetadata(stream); @@ -1170,6 +1652,8 @@ public: Reader.DumpHeaderMetadata(stream); } } + + return result; } }; @@ -1185,20 +1669,28 @@ show_file_info(CommandOptions& Options) return result; if ( EssenceType == ESS_MPEG2_VES ) - { - fputs("File essence type is MPEG2 video.\n", stdout); - FileInfoWrapper::file_info(Options); - } + result = FileInfoWrapper::file_info(Options, "MPEG2 video"); + else if ( EssenceType == ESS_PCM_24b_48k ) - { - fputs("File essence type is PCM audio.\n", stdout); - FileInfoWrapper::file_info(Options); - } + result = FileInfoWrapper::file_info(Options, "PCM audio"); + else if ( EssenceType == ESS_JPEG_2000 ) { - fputs("File essence type is JPEG 2000 pictures.\n", stdout); - FileInfoWrapper::file_info(Options); + if ( Options.stereo_image_flag ) + result = FileInfoWrapper::file_info(Options, "JPEG 2000 stereoscopic pictures"); + + else + result = FileInfoWrapper::file_info(Options, "JPEG 2000 pictures"); } + else if ( EssenceType == ESS_JPEG_2000_S ) + result = FileInfoWrapper::file_info(Options, "JPEG 2000 stereoscopic pictures"); + + else if ( EssenceType == ESS_TIMED_TEXT ) + result = FileInfoWrapper::file_info(Options, "Timed Text"); + else { fprintf(stderr, "File is not AS-DCP: %s\n", Options.filenames[0]); @@ -1234,11 +1726,54 @@ show_file_info(CommandOptions& Options) } +// +Result_t +digest_file(const char* filename) +{ + using namespace Kumu; + + ASDCP_TEST_NULL_STR(filename); + FileReader Reader; + SHA_CTX Ctx; + SHA1_Init(&Ctx); + ByteString Buf(8192); + + Result_t result = Reader.OpenRead(filename); + + while ( ASDCP_SUCCESS(result) ) + { + ui32_t read_count = 0; + result = Reader.Read(Buf.Data(), Buf.Capacity(), &read_count); + + if ( result == RESULT_ENDOFFILE ) + { + result = RESULT_OK; + break; + } + + if ( ASDCP_SUCCESS(result) ) + SHA1_Update(&Ctx, Buf.Data(), read_count); + } + + if ( ASDCP_SUCCESS(result) ) + { + const ui32_t sha_len = 20; + byte_t bin_buf[sha_len]; + char sha_buf[64]; + SHA1_Final(bin_buf, &Ctx); + + fprintf(stdout, "%s %s\n", base64encode(bin_buf, sha_len, sha_buf, 64), filename); + } + + return result; +} + // int main(int argc, const char** argv) { Result_t result = RESULT_OK; + char str_buf[64]; CommandOptions Options(argc, argv); if ( Options.version_flag ) @@ -1252,35 +1787,50 @@ main(int argc, const char** argv) if ( Options.error_flag ) { - fprintf(stderr, "There was a problem. Type %s -h for help.\n", PACKAGE); + fprintf(stderr, "There was a problem. Type %s -h for help.\n", PROGRAM_NAME); return 3; } - if ( Options.info_flag ) + if ( Options.mode == MMT_INFO ) { result = show_file_info(Options); } - else if ( Options.gop_start_flag ) + else if ( Options.mode == MMT_GOP_START ) { result = gop_start_test(Options); } - else if ( Options.genkey_flag ) + else if ( Options.mode == MMT_GEN_KEY ) { Kumu::FortunaRNG RNG; byte_t bin_buf[KeyLen]; - char str_buf[40]; RNG.FillRandom(bin_buf, KeyLen); - printf("%s\n", Kumu::bin2hex(bin_buf, KeyLen, str_buf, 40)); + printf("%s\n", Kumu::bin2hex(bin_buf, KeyLen, str_buf, 64)); } - else if ( Options.genid_flag ) + else if ( Options.mode == MMT_GEN_ID ) { UUID TmpID; Kumu::GenRandomValue(TmpID); - char str_buf[40]; - printf("%s\n", TmpID.EncodeHex(str_buf, 40)); + printf("%s\n", TmpID.EncodeHex(str_buf, 64)); + } + else if ( Options.mode == MMT_DIGEST ) + { + for ( ui32_t i = 0; i < Options.file_count && ASDCP_SUCCESS(result); i++ ) + result = digest_file(Options.filenames[i]); + } + else if ( Options.mode == MMT_UL_LIST ) + { + MDD_t di = (MDD_t)0; + + while ( di < MDD_Max ) + { + MDDEntry TmpType = Dict::Type(di); + UL TmpUL(TmpType.ul); + fprintf(stdout, "%s: %s\n", TmpUL.EncodeString(str_buf, 64), TmpType.name); + di = (MDD_t)(di + 1); + } } - else if ( Options.extract_flag ) + else if ( Options.mode == MMT_EXTRACT ) { EssenceType_t EssenceType; result = ASDCP::EssenceType(Options.filenames[0], EssenceType); @@ -1294,20 +1844,31 @@ main(int argc, const char** argv) break; case ESS_JPEG_2000: - result = read_JP2K_file(Options); + if ( Options.stereo_image_flag ) + result = read_JP2K_S_file(Options); + else + result = read_JP2K_file(Options); + break; + + case ESS_JPEG_2000_S: + result = read_JP2K_S_file(Options); break; case ESS_PCM_24b_48k: result = read_PCM_file(Options); break; + case ESS_TIMED_TEXT: + result = read_timed_text_file(Options); + break; + default: fprintf(stderr, "%s: Unknown file type, not ASDCP essence.\n", Options.filenames[0]); return 5; } } } - else if ( Options.create_flag ) + else if ( Options.mode == MMT_CREATE ) { if ( Options.do_repeat && ! Options.duration_flag ) { @@ -1327,13 +1888,22 @@ main(int argc, const char** argv) break; case ESS_JPEG_2000: - result = write_JP2K_file(Options); + if ( Options.stereo_image_flag ) + result = write_JP2K_S_file(Options); + + else + result = write_JP2K_file(Options); + break; case ESS_PCM_24b_48k: result = write_PCM_file(Options); break; + case ESS_TIMED_TEXT: + result = write_timed_text_file(Options); + break; + default: fprintf(stderr, "%s: Unknown file type, not ASDCP-compatible essence.\n", Options.filenames[0]); @@ -1341,12 +1911,21 @@ main(int argc, const char** argv) } } } + else + { + fprintf(stderr, "Unhandled mode: %d.\n", Options.mode); + return 6; + } if ( ASDCP_FAILURE(result) ) { fputs("Program stopped on error.\n", stderr); - if ( result != RESULT_FAIL ) + if ( result == RESULT_SFORMAT ) + { + fputs("Use option '-3' to force stereoscopic mode.\n", stderr); + } + else if ( result != RESULT_FAIL ) { fputs(result, stderr); fputc('\n', stderr);