| /////////////////////////////////////////////////////////////////////////////// |
| // |
| // The contents of this file are subject to the Mozilla Public License |
| // Version 1.1 (the "License"); you may not use this file except in |
| // compliance with the License. You may obtain a copy of the License at |
| // http://www.mozilla.org/MPL/ |
| // |
| // Software distributed under the License is distributed on an "AS IS" |
| // basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See the |
| // License for the specific language governing rights and limitations |
| // under the License. |
| // |
| // The Original Code is MP4v2. |
| // |
| // The Initial Developer of the Original Code is Ullrich Pollaehne. |
| // Portions created by Kona Blend are Copyright (C) 2008. |
| // Portions created by David Byron are Copyright (C) 2010. |
| // All Rights Reserved. |
| // |
| // Contributors: |
| // Kona Blend, kona8lend@@gmail.com |
| // Ullrich Pollaehne, u.pollaehne@@gmail.com |
| // David Byron, dbyron@dbyron.com |
| // |
| /////////////////////////////////////////////////////////////////////////////// |
| #include "util/impl.h" |
| |
| namespace mp4v2 { namespace util { |
| |
| /////////////////////////////////////////////////////////////////////////////// |
| /// |
| /// Chapter utility program class. |
| /// |
| /// This class provides an implementation for a QuickTime/Nero chapter utility which |
| /// allows to add, delete, convert export or import QuickTime and Nero chapters |
| /// in MP4 container files. |
| /// |
| /// |
| /// @see Utility |
| /// |
| /////////////////////////////////////////////////////////////////////////////// |
| class ChapterUtility : public Utility |
| { |
| private: |
| static const double CHAPTERTIMESCALE; //!< the timescale used for chapter tracks (1000) |
| |
| enum FileLongCode { |
| LC_CHPT_ANY = _LC_MAX, |
| LC_CHPT_QT, |
| LC_CHPT_NERO, |
| LC_CHPT_COMMON, |
| LC_CHP_LIST, |
| LC_CHP_CONVERT, |
| LC_CHP_EVERY, |
| LC_CHP_EXPORT, |
| LC_CHP_IMPORT, |
| LC_CHP_REMOVE |
| }; |
| |
| enum ChapterFormat { |
| CHPT_FMT_NATIVE, |
| CHPT_FMT_COMMON |
| }; |
| |
| enum FormatState { |
| FMT_STATE_INITIAL, |
| FMT_STATE_TIME_LINE, |
| FMT_STATE_TITLE_LINE, |
| FMT_STATE_FINISH |
| }; |
| |
| public: |
| ChapterUtility( int, char** ); |
| |
| protected: |
| // delegates implementation |
| bool utility_option( int, bool& ); |
| bool utility_job( JobContext& ); |
| |
| private: |
| bool actionList ( JobContext& ); |
| bool actionConvert ( JobContext& ); |
| bool actionEvery ( JobContext& ); |
| bool actionExport ( JobContext& ); |
| bool actionImport ( JobContext& ); |
| bool actionRemove ( JobContext& ); |
| |
| private: |
| Group _actionGroup; |
| Group _parmGroup; |
| |
| bool (ChapterUtility::*_action)( JobContext& ); |
| void fixQtScale(MP4FileHandle ); |
| MP4TrackId getReferencingTrack( MP4FileHandle, bool& ); |
| string getChapterTypeName( MP4ChapterType ) const; |
| bool parseChapterFile( const string, std::vector<MP4Chapter_t>&, Timecode::Format& ); |
| bool readChapterFile( const string, char**, File::Size& ); |
| MP4Duration convertFrameToMillis( MP4Duration, uint32_t ); |
| |
| MP4ChapterType _ChapterType; |
| ChapterFormat _ChapterFormat; |
| uint32_t _ChaptersEvery; |
| string _ChapterFile; |
| }; |
| |
| /////////////////////////////////////////////////////////////////////////////// |
| |
| const double ChapterUtility::CHAPTERTIMESCALE = 1000.0; |
| |
| /////////////////////////////////////////////////////////////////////////////// |
| |
| ChapterUtility::ChapterUtility( int argc, char** argv ) |
| : Utility ( "mp4chaps", argc, argv ) |
| , _actionGroup ( "ACTIONS" ) |
| , _parmGroup ( "ACTION PARAMETERS" ) |
| , _action ( NULL ) |
| , _ChapterType ( MP4ChapterTypeAny ) |
| , _ChapterFormat ( CHPT_FMT_NATIVE ) |
| , _ChaptersEvery ( 0 ) |
| { |
| // add standard options which make sense for this utility |
| _group.add( STD_OPTIMIZE ); |
| _group.add( STD_DRYRUN ); |
| _group.add( STD_KEEPGOING ); |
| _group.add( STD_OVERWRITE ); |
| _group.add( STD_FORCE ); |
| _group.add( STD_QUIET ); |
| _group.add( STD_DEBUG ); |
| _group.add( STD_VERBOSE ); |
| _group.add( STD_HELP ); |
| _group.add( STD_VERSION ); |
| _group.add( STD_VERSIONX ); |
| |
| _parmGroup.add( 'A', false, "chapter-any", false, LC_CHPT_ANY, "act on any chapter type (default)" ); |
| _parmGroup.add( 'Q', false, "chapter-qt", false, LC_CHPT_QT, "act on QuickTime chapters" ); |
| _parmGroup.add( 'N', false, "chapter-nero", false, LC_CHPT_NERO, "act on Nero chapters" ); |
| _parmGroup.add( 'C', false, "format-common", false, LC_CHPT_COMMON, "export chapters in common format" ); |
| _groups.push_back( &_parmGroup ); |
| |
| _actionGroup.add( 'l', false, "list", false, LC_CHP_LIST, "list available chapters" ); |
| _actionGroup.add( 'c', false, "convert", false, LC_CHP_CONVERT, "convert available chapters" ); |
| _actionGroup.add( 'e', true, "every", true, LC_CHP_EVERY, "create chapters every NUM seconds", "NUM" ); |
| _actionGroup.add( 'x', false, "export", false, LC_CHP_EXPORT, "export chapters to mp4file.chapters.txt", "TXT" ); |
| _actionGroup.add( 'i', false, "import", false, LC_CHP_IMPORT, "import chapters from mp4file.chapters.txt", "TXT" ); |
| _actionGroup.add( 'r', false, "remove", false, LC_CHP_REMOVE, "remove all chapters" ); |
| _groups.push_back( &_actionGroup ); |
| |
| _usage = "[OPTION]... ACTION [ACTION PARAMETERS] mp4file..."; |
| _description = |
| // 79-cols, inclusive, max desired width |
| // |----------------------------------------------------------------------------| |
| "\nFor each mp4 file specified, perform the specified ACTION. An action must be" |
| "\nspecified. Some options are not applicable to some actions."; |
| } |
| |
| /////////////////////////////////////////////////////////////////////////////// |
| |
| /** Action for listing chapters from <b>job.file</b> |
| * |
| * |
| * @param job the job to process |
| * @return mp4v2::util::SUCCESS if successful, mp4v2::util::FAILURE otherwise |
| */ |
| bool |
| ChapterUtility::actionList( JobContext& job ) |
| { |
| job.fileHandle = MP4Read( job.file.c_str() ); |
| if( job.fileHandle == MP4_INVALID_FILE_HANDLE ) |
| { |
| return herrf( "unable to open for read: %s\n", job.file.c_str() ); |
| } |
| |
| MP4Chapter_t * chapters = 0; |
| uint32_t chapterCount = 0; |
| |
| // get the list of chapters |
| MP4ChapterType chtp = MP4GetChapters(job.fileHandle, &chapters, &chapterCount, _ChapterType); |
| if (0 == chapterCount) |
| { |
| verbose1f( "File \"%s\" does not contain chapters of type %s\n", job.file.c_str(), |
| getChapterTypeName( _ChapterType ).c_str() ); |
| return SUCCESS; |
| } |
| |
| // start output (more or less like mp4box does) |
| ostringstream report; |
| report << getChapterTypeName(chtp) << ' ' << "Chapters of " << '"' |
| << job.file << '"' << std::endl; |
| |
| Timecode duration(0, CHAPTERTIMESCALE); |
| duration.setFormat( Timecode::DECIMAL ); |
| for (uint32_t i = 0; i < chapterCount; ++i) |
| { |
| // print the infos |
| report << '\t' << "Chapter #" << std::setw(3) << std::setfill('0') |
| << i + 1 << " - " << duration.svalue << " - " << '"' |
| << chapters[i].title << '"' << std::endl; |
| |
| // add the duration of this chapter to the sum (is the start time of the next chapter) |
| duration += Timecode(chapters[i].duration, CHAPTERTIMESCALE); |
| } |
| |
| verbose1f( "%s", report.str().c_str() ); |
| |
| // free up the memory |
| MP4Free(chapters); |
| |
| return SUCCESS; |
| } |
| |
| /////////////////////////////////////////////////////////////////////////////// |
| |
| /** Action for converting chapters in <b>job.file</b> |
| * |
| * |
| * @param job the job to process |
| * @return mp4v2::util::SUCCESS if successful, mp4v2::util::FAILURE otherwise |
| */ |
| bool |
| ChapterUtility::actionConvert( JobContext& job ) |
| { |
| MP4ChapterType sourceType; |
| |
| switch( _ChapterType ) |
| { |
| case MP4ChapterTypeNero: |
| sourceType = MP4ChapterTypeQt; |
| break; |
| case MP4ChapterTypeQt: |
| sourceType = MP4ChapterTypeNero; |
| break; |
| default: |
| return herrf( "invalid chapter type \"%s\" define the chapter type to convert to\n", |
| getChapterTypeName( _ChapterType ).c_str() ); |
| } |
| |
| ostringstream oss; |
| oss << "converting chapters in file " << '"' << job.file << '"' << " from " |
| << getChapterTypeName(sourceType) << " to " |
| << getChapterTypeName(_ChapterType) << std::endl; |
| |
| verbose1f( "%s", oss.str().c_str() ); |
| if( dryrunAbort() ) |
| { |
| return SUCCESS; |
| } |
| |
| job.fileHandle = MP4Modify( job.file.c_str() ); |
| if( job.fileHandle == MP4_INVALID_FILE_HANDLE ) |
| { |
| return herrf( "unable to open for write: %s\n", job.file.c_str() ); |
| } |
| |
| MP4ChapterType chtp = MP4ConvertChapters( job.fileHandle, _ChapterType ); |
| if( MP4ChapterTypeNone == chtp ) |
| { |
| return herrf( "File %s does not contain chapters of type %s\n", job.file.c_str(), |
| getChapterTypeName( sourceType ).c_str() ); |
| } |
| |
| fixQtScale( job.fileHandle ); |
| job.optimizeApplicable = true; |
| |
| return SUCCESS; |
| } |
| |
| /////////////////////////////////////////////////////////////////////////////// |
| |
| /** Action for setting chapters every n second in <b>job.file</b> |
| * |
| * |
| * @param job the job to process |
| * @return mp4v2::util::SUCCESS if successful, mp4v2::util::FAILURE otherwise |
| */ |
| bool |
| ChapterUtility::actionEvery( JobContext& job ) |
| { |
| ostringstream oss; |
| oss << "Setting " << getChapterTypeName(_ChapterType) << " chapters every " |
| << _ChaptersEvery << " seconds in file " << '"' << job.file << '"' |
| << std::endl; |
| |
| verbose1f( "%s", oss.str().c_str() ); |
| if( dryrunAbort() ) |
| { |
| return SUCCESS; |
| } |
| |
| job.fileHandle = MP4Modify( job.file.c_str() ); |
| if( job.fileHandle == MP4_INVALID_FILE_HANDLE ) |
| { |
| return herrf( "unable to open for write: %s\n", job.file.c_str() ); |
| } |
| |
| bool isVideoTrack = false; |
| MP4TrackId refTrackId = getReferencingTrack( job.fileHandle, isVideoTrack ); |
| if( !MP4_IS_VALID_TRACK_ID(refTrackId) ) |
| { |
| return herrf( "unable to find a video or audio track in file %s\n", job.file.c_str() ); |
| } |
| |
| Timecode refTrackDuration( MP4GetTrackDuration( job.fileHandle, refTrackId ), MP4GetTrackTimeScale( job.fileHandle, refTrackId ) ); |
| refTrackDuration.setScale( CHAPTERTIMESCALE ); |
| |
| Timecode chapterDuration( _ChaptersEvery * 1000, CHAPTERTIMESCALE ); |
| chapterDuration.setFormat( Timecode::DECIMAL ); |
| std::vector<MP4Chapter_t> chapters; |
| |
| do |
| { |
| MP4Chapter_t chap; |
| chap.duration = refTrackDuration.duration > chapterDuration.duration ? chapterDuration.duration : refTrackDuration.duration; |
| sprintf(chap.title, "Chapter %lu", (unsigned long)chapters.size()+1); |
| |
| chapters.push_back( chap ); |
| refTrackDuration -= chapterDuration; |
| } |
| while( refTrackDuration.duration > 0 ); |
| |
| if( 0 < chapters.size() ) |
| { |
| MP4SetChapters(job.fileHandle, &chapters[0], (uint32_t)chapters.size(), _ChapterType); |
| } |
| |
| fixQtScale( job.fileHandle ); |
| job.optimizeApplicable = true; |
| |
| return SUCCESS; |
| } |
| |
| /////////////////////////////////////////////////////////////////////////////// |
| |
| /** Action for exporting chapters from the <b>job.file</b> |
| * |
| * |
| * @param job the job to process |
| * @return mp4v2::util::SUCCESS if successful, mp4v2::util::FAILURE otherwise |
| */ |
| bool |
| ChapterUtility::actionExport( JobContext& job ) |
| { |
| job.fileHandle = MP4Read( job.file.c_str() ); |
| if( job.fileHandle == MP4_INVALID_FILE_HANDLE ) |
| { |
| return herrf( "unable to open for read: %s\n", job.file.c_str() ); |
| } |
| |
| // get the list of chapters |
| MP4Chapter_t* chapters = 0; |
| uint32_t chapterCount = 0; |
| MP4ChapterType chtp = MP4GetChapters( job.fileHandle, &chapters, &chapterCount, _ChapterType ); |
| if (0 == chapterCount) |
| { |
| return herrf( "File \"%s\" does not contain chapters of type %s\n", job.file.c_str(), |
| getChapterTypeName( chtp ).c_str() ); |
| } |
| |
| // build the filename |
| string outName = job.file; |
| if( _ChapterFile.empty() ) |
| { |
| FileSystem::pathnameStripExtension( outName ); |
| outName.append( ".chapters.txt" ); |
| } |
| else |
| { |
| outName = _ChapterFile; |
| } |
| |
| ostringstream oss; |
| oss << "Exporting " << chapterCount << " " << getChapterTypeName( chtp ); |
| oss << " chapters from file " << '"' << job.file << '"' |
| << " into chapter file " << '"' << outName << '"' << std::endl; |
| |
| verbose1f( "%s", oss.str().c_str() ); |
| if( dryrunAbort() ) |
| { |
| // free up the memory |
| MP4Free(chapters); |
| |
| return SUCCESS; |
| } |
| |
| // open the file |
| File out( outName, File::MODE_CREATE ); |
| if( openFileForWriting( out ) ) |
| { |
| // free up the memory |
| MP4Free(chapters); |
| |
| return FAILURE; |
| } |
| |
| // write the chapters |
| #if defined( _WIN32 ) |
| static const char* LINEND = "\r\n"; |
| #else |
| static const char* LINEND = "\n"; |
| #endif |
| File::Size nout; |
| bool failure = SUCCESS; |
| int width = 2; |
| if( CHPT_FMT_COMMON == _ChapterFormat && (chapterCount / 100) >= 1 ) |
| { |
| width = 3; |
| } |
| Timecode duration( 0, CHAPTERTIMESCALE ); |
| duration.setFormat( Timecode::DECIMAL ); |
| for( uint32_t i = 0; i < chapterCount; ++i ) |
| { |
| // print the infos |
| ostringstream oss; |
| switch( _ChapterFormat ) |
| { |
| case CHPT_FMT_COMMON: |
| oss << "CHAPTER" << std::setw(width) << std::setfill('0') << i + 1 |
| << '=' << duration.svalue << LINEND << "CHAPTER" |
| << std::setw(width) << std::setfill('0') << i + 1 |
| << "NAME=" << chapters[i].title << LINEND; |
| break; |
| case CHPT_FMT_NATIVE: |
| default: |
| oss << duration.svalue << ' ' << chapters[i].title << LINEND; |
| } |
| |
| string str = oss.str(); |
| if( out.write( str.c_str(), str.size(), nout ) ) |
| { |
| failure = herrf( "write to %s failed: %s\n", outName.c_str(), sys::getLastErrorStr() ); |
| break; |
| } |
| |
| // add the duration of this chapter to the sum (the start time of the next chapter) |
| duration += Timecode(chapters[i].duration, CHAPTERTIMESCALE); |
| } |
| out.close(); |
| if( failure ) |
| { |
| verbose1f( "removing file %s\n", outName.c_str() ); |
| ::remove( outName.c_str() ); |
| } |
| |
| // free up the memory |
| MP4Free(chapters); |
| |
| return SUCCESS; |
| } |
| |
| /////////////////////////////////////////////////////////////////////////////// |
| |
| /** Action for importing chapters into the <b>job.file</b> |
| * |
| * |
| * @param job the job to process |
| * @return mp4v2::util::SUCCESS if successful, mp4v2::util::FAILURE otherwise |
| */ |
| bool |
| ChapterUtility::actionImport( JobContext& job ) |
| { |
| std::vector<MP4Chapter_t> chapters; |
| Timecode::Format format; |
| |
| // create the chapter file name |
| string inName = job.file; |
| if( _ChapterFile.empty() ) |
| { |
| FileSystem::pathnameStripExtension( inName ); |
| inName.append( ".chapters.txt" ); |
| } |
| else |
| { |
| inName = _ChapterFile; |
| } |
| |
| if( parseChapterFile( inName, chapters, format ) ) |
| { |
| return FAILURE; |
| } |
| |
| ostringstream oss; |
| oss << "Importing " << chapters.size() << " " << getChapterTypeName( _ChapterType ); |
| oss << " chapters from file " << inName << " into file " << '"' << job.file |
| << '"' << std::endl; |
| |
| verbose1f( "%s", oss.str().c_str() ); |
| if( dryrunAbort() ) |
| { |
| return SUCCESS; |
| } |
| |
| if( 0 == chapters.size() ) |
| { |
| return herrf( "No chapters found in file %s\n", inName.c_str() ); |
| } |
| |
| job.fileHandle = MP4Modify( job.file.c_str() ); |
| if( job.fileHandle == MP4_INVALID_FILE_HANDLE ) |
| { |
| return herrf( "unable to open for write: %s\n", job.file.c_str() ); |
| } |
| |
| bool isVideoTrack = false; |
| MP4TrackId refTrackId = getReferencingTrack( job.fileHandle, isVideoTrack ); |
| if( !MP4_IS_VALID_TRACK_ID(refTrackId) ) |
| { |
| return herrf( "unable to find a video or audio track in file %s\n", job.file.c_str() ); |
| } |
| if( Timecode::FRAME == format && !isVideoTrack ) |
| { |
| // we need a video track for this |
| return herrf( "unable to find a video track in file %s but chapter file contains frame timestamps\n", job.file.c_str() ); |
| } |
| |
| // get duration and recalculate scale |
| Timecode refTrackDuration( MP4GetTrackDuration( job.fileHandle, refTrackId ), |
| MP4GetTrackTimeScale( job.fileHandle, refTrackId ) ); |
| refTrackDuration.setScale( CHAPTERTIMESCALE ); |
| |
| // check for chapters starting after duration of reftrack |
| for( std::vector<MP4Chapter_t>::iterator it = chapters.begin(); it != chapters.end(); ) |
| { |
| Timecode curr( (*it).duration, CHAPTERTIMESCALE ); |
| if( refTrackDuration <= curr ) |
| { |
| hwarnf( "Chapter '%s' start: %s, playlength of file: %s, chapter cannot be set\n", |
| (*it).title, curr.svalue.c_str(), refTrackDuration.svalue.c_str() ); |
| it = chapters.erase( it ); |
| } |
| else |
| { |
| ++it; |
| } |
| } |
| if( 0 == chapters.size() ) |
| { |
| return SUCCESS; |
| } |
| |
| // convert start time into duration |
| uint32_t framerate = static_cast<uint32_t>( CHAPTERTIMESCALE ); |
| if( Timecode::FRAME == format ) |
| { |
| // get the framerate |
| MP4SampleId sampleCount = MP4GetTrackNumberOfSamples( job.fileHandle, refTrackId ); |
| Timecode tmpcd( refTrackDuration.svalue, CHAPTERTIMESCALE ); |
| framerate = static_cast<uint32_t>( std::ceil( ((double)sampleCount / (double)tmpcd.duration) * CHAPTERTIMESCALE ) ); |
| } |
| |
| for( std::vector<MP4Chapter_t>::iterator it = chapters.begin(); it != chapters.end(); ++it ) |
| { |
| MP4Duration currDur = (*it).duration; |
| MP4Duration nextDur = chapters.end() == it+1 ? refTrackDuration.duration : (*(it+1)).duration; |
| |
| if( Timecode::FRAME == format ) |
| { |
| // convert from frame nr to milliseconds |
| currDur = convertFrameToMillis( (*it).duration, framerate ); |
| |
| if( chapters.end() != it+1 ) |
| { |
| nextDur = convertFrameToMillis( (*(it+1)).duration, framerate ); |
| } |
| } |
| |
| (*it).duration = nextDur - currDur; |
| } |
| |
| // now set the chapters |
| MP4SetChapters( job.fileHandle, &chapters[0], (uint32_t)chapters.size(), _ChapterType ); |
| |
| fixQtScale( job.fileHandle ); |
| job.optimizeApplicable = true; |
| |
| return SUCCESS; |
| } |
| |
| /////////////////////////////////////////////////////////////////////////////// |
| |
| /** Action for removing chapters from the <b>job.file</b> |
| * |
| * |
| * @param job the job to process |
| * @return mp4v2::util::SUCCESS if successful, mp4v2::util::FAILURE otherwise |
| */ |
| bool |
| ChapterUtility::actionRemove( JobContext& job ) |
| { |
| ostringstream oss; |
| oss << "Deleting " << getChapterTypeName(_ChapterType) |
| << " chapters from file " << '"' << job.file << '"' << std::endl; |
| |
| verbose1f( "%s", oss.str().c_str() ); |
| if( dryrunAbort() ) |
| { |
| return SUCCESS; |
| } |
| |
| job.fileHandle = MP4Modify( job.file.c_str() ); |
| if( job.fileHandle == MP4_INVALID_FILE_HANDLE ) |
| { |
| return herrf( "unable to open for write: %s\n", job.file.c_str() ); |
| } |
| |
| MP4ChapterType chtp = MP4DeleteChapters( job.fileHandle, _ChapterType ); |
| if( MP4ChapterTypeNone == chtp ) |
| { |
| return FAILURE; |
| } |
| |
| fixQtScale( job.fileHandle ); |
| job.optimizeApplicable = true; |
| |
| return SUCCESS; |
| } |
| |
| /////////////////////////////////////////////////////////////////////////////// |
| |
| /** process positional argument |
| * |
| * @see Utility::utility_job( JobContext& ) |
| */ |
| bool |
| ChapterUtility::utility_job( JobContext& job ) |
| { |
| if( !_action ) |
| { |
| return herrf( "no action specified\n" ); |
| } |
| |
| return (this->*_action)( job ); |
| } |
| |
| /////////////////////////////////////////////////////////////////////////////// |
| |
| /** process command-line option |
| * |
| * @see Utility::utility_option( int, bool& ) |
| */ |
| bool |
| ChapterUtility::utility_option( int code, bool& handled ) |
| { |
| handled = true; |
| |
| switch( code ) { |
| case 'A': |
| case LC_CHPT_ANY: |
| _ChapterType = MP4ChapterTypeAny; |
| break; |
| |
| case 'Q': |
| case LC_CHPT_QT: |
| _ChapterType = MP4ChapterTypeQt; |
| break; |
| |
| case 'N': |
| case LC_CHPT_NERO: |
| _ChapterType = MP4ChapterTypeNero; |
| break; |
| |
| case 'C': |
| case LC_CHPT_COMMON: |
| _ChapterFormat = CHPT_FMT_COMMON; |
| break; |
| |
| case 'l': |
| case LC_CHP_LIST: |
| _action = &ChapterUtility::actionList; |
| break; |
| |
| case 'e': |
| case LC_CHP_EVERY: |
| { |
| istringstream iss( prog::optarg ); |
| iss >> _ChaptersEvery; |
| if( iss.rdstate() != ios::eofbit ) |
| { |
| return herrf( "invalid number of seconds: %s\n", prog::optarg ); |
| } |
| _action = &ChapterUtility::actionEvery; |
| break; |
| } |
| |
| case 'x': |
| _action = &ChapterUtility::actionExport; |
| break; |
| |
| case LC_CHP_EXPORT: |
| _action = &ChapterUtility::actionExport; |
| /* currently not supported since the chapters of n input files would be written to one chapter file |
| _ChapterFile = prog::optarg; |
| if( _ChapterFile.empty() ) |
| { |
| return herrf( "invalid TXT file: empty-string\n" ); |
| } |
| */ |
| break; |
| |
| case 'i': |
| _action = &ChapterUtility::actionImport; |
| break; |
| |
| case LC_CHP_IMPORT: |
| _action = &ChapterUtility::actionImport; |
| /* currently not supported since the chapters of n input files would be read from one chapter file |
| _ChapterFile = prog::optarg; |
| if( _ChapterFile.empty() ) |
| { |
| return herrf( "invalid TXT file: empty-string\n" ); |
| } |
| */ |
| break; |
| |
| case 'c': |
| case LC_CHP_CONVERT: |
| _action = &ChapterUtility::actionConvert; |
| break; |
| |
| case 'r': |
| case LC_CHP_REMOVE: |
| _action = &ChapterUtility::actionRemove; |
| break; |
| |
| default: |
| handled = false; |
| break; |
| } |
| |
| return SUCCESS; |
| } |
| |
| /////////////////////////////////////////////////////////////////////////////// |
| |
| /** Fix a QuickTime/iPod issue with long audio files. |
| * |
| * This function checks if the <b>file</b> is a long audio file (more than |
| * about 6 1/2 hours) and modifies the timescale if necessary to allow |
| * playback of the file in QuickTime player and on some iPod models. |
| * |
| * @param file the opened MP4 file |
| */ |
| void |
| ChapterUtility::fixQtScale(MP4FileHandle file) |
| { |
| // get around a QuickTime/iPod issue with storing the number of samples in a signed 32Bit value |
| if( INT_MAX < MP4GetDuration(file)) |
| { |
| bool isVideoTrack = false; |
| if( MP4_IS_VALID_TRACK_ID(getReferencingTrack( file, isVideoTrack )) & isVideoTrack ) |
| { |
| // if it is a video, everything is different |
| return; |
| } |
| |
| // timescale too high, lower it |
| MP4ChangeMovieTimeScale(file, 1000); |
| } |
| } |
| |
| /////////////////////////////////////////////////////////////////////////////// |
| |
| /** Finds a suitable track that can reference a chapter track. |
| * |
| * This function returns the first video or audio track that is found |
| * in the <b>file</b>. |
| * This track ca be used to reference the QuickTime chapter track. |
| * |
| * @param file the opened MP4 file |
| * @param isVideoTrack receives true if the found track is video, false otherwise |
| * @return the <b>MP4TrackId</b> of the found track |
| */ |
| MP4TrackId |
| ChapterUtility::getReferencingTrack( MP4FileHandle file, bool& isVideoTrack ) |
| { |
| isVideoTrack = false; |
| |
| uint32_t trackCount = MP4GetNumberOfTracks( file ); |
| if( 0 == trackCount ) |
| { |
| return MP4_INVALID_TRACK_ID; |
| } |
| |
| MP4TrackId refTrackId = MP4_INVALID_TRACK_ID; |
| for( uint32_t i = 0; i < trackCount; ++i ) |
| { |
| MP4TrackId id = MP4FindTrackId( file, i ); |
| const char* type = MP4GetTrackType( file, id ); |
| if( MP4_IS_VIDEO_TRACK_TYPE( type ) ) |
| { |
| refTrackId = id; |
| isVideoTrack = true; |
| break; |
| } |
| else if( MP4_IS_AUDIO_TRACK_TYPE( type ) ) |
| { |
| refTrackId = id; |
| break; |
| } |
| } |
| |
| return refTrackId; |
| } |
| |
| /////////////////////////////////////////////////////////////////////////////// |
| |
| /** Return a human readable representation of a <b>MP4ChapterType</b>. |
| * |
| * @param chapterType the chapter type |
| * @return a string representing the chapter type |
| */ |
| string |
| ChapterUtility::getChapterTypeName( MP4ChapterType chapterType) const |
| { |
| switch( chapterType ) |
| { |
| case MP4ChapterTypeQt: |
| return string( "QuickTime" ); |
| break; |
| |
| case MP4ChapterTypeNero: |
| return string( "Nero" ); |
| break; |
| |
| case MP4ChapterTypeAny: |
| return string( "QuickTime and Nero" ); |
| break; |
| |
| default: |
| return string( "Unknown" ); |
| } |
| } |
| |
| /////////////////////////////////////////////////////////////////////////////// |
| |
| /** Read a file into a buffer. |
| * |
| * This function reads the file named by <b>filename</b> into a buffer allocated |
| * by malloc and returns the pointer to this buffer in <b>buffer</b> and the size |
| * of this buffer in <b>fileSize</b>. |
| * |
| * @param filename the name of the file. |
| * @param buffer receives a pointer to the created buffer |
| * @param fileSize reference to a <b>io::StdioFile::Size</b> that receives the size of the file |
| * @return true if there was an error, false otherwise |
| */ |
| bool |
| ChapterUtility::readChapterFile( const string filename, char** buffer, File::Size& fileSize ) |
| { |
| // open the file |
| File in( filename, File::MODE_READ ); |
| File::Size nin; |
| if( in.open() ) { |
| return herrf( "opening chapter file '%s' failed: %s\n", filename.c_str(), sys::getLastErrorStr() ); |
| } |
| |
| // get the file size |
| fileSize = in.size; |
| if( 0 >= fileSize ) |
| { |
| in.close(); |
| return herrf( "getting size of chapter file '%s' failed: %s\n", filename.c_str(), sys::getLastErrorStr() ); |
| } |
| |
| // allocate a buffer for the file and read the content |
| char* inBuf = static_cast<char*>( malloc( fileSize+1 ) ); |
| if( in.read( inBuf, fileSize, nin ) ) |
| { |
| in.close(); |
| free(inBuf); |
| return herrf( "reading chapter file '%s' failed: %s\n", filename.c_str(), sys::getLastErrorStr() ); |
| } |
| in.close(); |
| inBuf[fileSize] = 0; |
| |
| *buffer = inBuf; |
| |
| return SUCCESS; |
| } |
| |
| /////////////////////////////////////////////////////////////////////////////// |
| |
| /** Read and parse a chapter file. |
| * |
| * This function reads and parses a chapter file and returns a vector of |
| * <b>MP4Chapter_t</b> elements. |
| * |
| * @param filename the name of the file. |
| * @param vector receives a vector of chapters |
| * @param format receives the <b>Timecode::Format</b> of the timestamps |
| * @return true if there was an error, false otherwise |
| */ |
| bool |
| ChapterUtility::parseChapterFile( const string filename, std::vector<MP4Chapter_t>& chapters, Timecode::Format& format ) |
| { |
| // get the content |
| char * inBuf; |
| File::Size fileSize; |
| if( readChapterFile( filename, &inBuf, fileSize ) ) |
| { |
| return FAILURE; |
| } |
| |
| // separate the text lines |
| char* pos = inBuf; |
| while (pos < inBuf + fileSize) |
| { |
| if (*pos == '\n' || *pos == '\r') |
| { |
| *pos = 0; |
| if (pos > inBuf) |
| { |
| // remove trailing whitespace |
| char* tmp = pos-1; |
| while ((*tmp == ' ' || *tmp == '\t') && tmp > inBuf) |
| { |
| *tmp = 0; |
| tmp--; |
| } |
| } |
| } |
| pos++; |
| } |
| pos = inBuf; |
| |
| // check for a BOM |
| char bom[5] = {0}; |
| int bomLen = 0; |
| const unsigned char* uPos = reinterpret_cast<unsigned char*>( pos ); |
| if( 0xEF == *uPos && 0xBB == *(uPos+1) && 0xBF == *(uPos+2) ) |
| { |
| // UTF-8 (we do not need the BOM) |
| pos += 3; |
| } |
| else if( ( 0xFE == *uPos && 0xFF == *(uPos+1) ) // UTF-16 big endian |
| || ( 0xFF == *uPos && 0xFE == *(uPos+1) ) ) // UTF-16 little endian |
| { |
| // store the BOM to prepend the title strings |
| bom[0] = *pos++; |
| bom[1] = *pos++; |
| bomLen = 2; |
| return herrf( "chapter file '%s' has UTF-16 encoding which is not supported (only UTF-8 is allowed)\n", |
| filename.c_str() ); |
| } |
| else if( ( 0x0 == *uPos && 0x0 == *(uPos+1) && 0xFE == *(uPos+2) && 0xFF == *(uPos+3) ) // UTF-32 big endian |
| || ( 0xFF == *uPos && *(uPos+1) == 0xFE && *(uPos+2) == 0x0 && 0x0 == *(uPos+3) ) ) // UTF-32 little endian |
| { |
| // store the BOM to prepend the title strings |
| bom[0] = *pos++; |
| bom[1] = *pos++; |
| bom[2] = *pos++; |
| bom[3] = *pos++; |
| bomLen = 4; |
| return herrf( "chapter file '%s' has UTF-32 encoding which is not supported (only UTF-8 is allowed)\n", |
| filename.c_str() ); |
| } |
| |
| // parse the lines |
| bool failure = false; |
| uint32_t currentChapter = 0; |
| FormatState formatState = FMT_STATE_INITIAL; |
| char* titleStart = 0; |
| uint32_t titleLen = 0; |
| char* timeStart = 0; |
| while( pos < inBuf + fileSize ) |
| { |
| if( 0 == *pos || ' ' == *pos || '\t' == *pos ) |
| { |
| // uninteresting chars |
| pos++; |
| continue; |
| } |
| else if( '#' == *pos ) |
| { |
| // comment line |
| pos += strlen( pos ); |
| continue; |
| } |
| else if( isdigit( *pos ) ) |
| { |
| // mp4chaps native format: hh:mm:ss.sss <title> |
| |
| timeStart = pos; |
| |
| // read the title if there is one |
| titleStart = strchr( timeStart, ' ' ); |
| if( NULL == titleStart ) |
| { |
| titleStart = strchr( timeStart, '\t' ); |
| } |
| |
| if( NULL != titleStart ) |
| { |
| *titleStart = 0; |
| pos = ++titleStart; |
| |
| while( ' ' == *titleStart || '\t' == *titleStart ) |
| { |
| titleStart++; |
| } |
| |
| titleLen = (uint32_t)strlen( titleStart ); |
| |
| // advance to the end of the line |
| pos = titleStart + 1 + titleLen; |
| } |
| else |
| { |
| // advance to the end of the line |
| pos += strlen( pos ); |
| } |
| |
| formatState = FMT_STATE_FINISH; |
| } |
| #if defined( _MSC_VER ) |
| else if( 0 == strnicmp( pos, "CHAPTER", 7 ) ) |
| #else |
| else if( 0 == strncasecmp( pos, "CHAPTER", 7 ) ) |
| #endif |
| { |
| // common format: CHAPTERxx=hh:mm:ss.sss\nCHAPTERxxNAME=<title> |
| |
| char* equalsPos = strchr( pos+7, '=' ); |
| if( NULL == equalsPos ) |
| { |
| herrf( "Unable to parse line \"%s\"\n", pos ); |
| failure = true; |
| break; |
| } |
| |
| *equalsPos = 0; |
| |
| char* tlwr = pos; |
| while( equalsPos != tlwr ) |
| { |
| *tlwr = tolower( *tlwr ); |
| tlwr++; |
| } |
| |
| if( NULL != strstr( pos, "name" ) ) |
| { |
| // mark the chapter title |
| uint32_t chNr = 0; |
| sscanf( pos, "chapter%dname", &chNr ); |
| if( chNr != currentChapter ) |
| { |
| // different chapter number => different chapter definition pair |
| if( FMT_STATE_INITIAL != formatState ) |
| { |
| herrf( "Chapter lines are not consecutive before line \"%s\"\n", pos ); |
| failure = true; |
| break; |
| } |
| |
| currentChapter = chNr; |
| } |
| formatState = FMT_STATE_TIME_LINE == formatState ? FMT_STATE_FINISH |
| : FMT_STATE_TITLE_LINE; |
| |
| titleStart = equalsPos + 1; |
| titleLen = (uint32_t)strlen( titleStart ); |
| |
| // advance to the end of the line |
| pos = titleStart + titleLen; |
| } |
| else |
| { |
| // mark the chapter start time |
| uint32_t chNr = 0; |
| sscanf( pos, "chapter%d", &chNr ); |
| if( chNr != currentChapter ) |
| { |
| // different chapter number => different chapter definition pair |
| if( FMT_STATE_INITIAL != formatState ) |
| { |
| herrf( "Chapter lines are not consecutive at line \"%s\"\n", pos ); |
| failure = true; |
| break; |
| } |
| |
| currentChapter = chNr; |
| } |
| formatState = FMT_STATE_TITLE_LINE == formatState ? FMT_STATE_FINISH |
| : FMT_STATE_TIME_LINE; |
| |
| timeStart = equalsPos + 1; |
| |
| // advance to the end of the line |
| pos = timeStart + strlen( timeStart ); |
| } |
| } |
| |
| if( FMT_STATE_FINISH == formatState ) |
| { |
| // now we have title and start time |
| MP4Chapter_t chap; |
| |
| strncpy( chap.title, titleStart, min( titleLen, (uint32_t)MP4V2_CHAPTER_TITLE_MAX ) ); |
| chap.title[titleLen] = 0; |
| |
| Timecode tc( 0, CHAPTERTIMESCALE ); |
| string tm( timeStart ); |
| if( tc.parse( tm ) ) |
| { |
| herrf( "Unable to parse time code from \"%s\"\n", tm.c_str() ); |
| failure = true; |
| break; |
| } |
| chap.duration = tc.duration; |
| format = tc.format; |
| |
| // ad the chapter to the list |
| chapters.push_back( chap ); |
| |
| // re-initialize |
| formatState = FMT_STATE_INITIAL; |
| titleStart = timeStart = NULL; |
| titleLen = 0; |
| } |
| } |
| free( inBuf ); |
| if( failure ) |
| { |
| return failure; |
| } |
| |
| return SUCCESS; |
| } |
| |
| /////////////////////////////////////////////////////////////////////////////// |
| |
| /** Convert from frame to millisecond timestamp. |
| * |
| * This function converts a timestamp from hh:mm:ss:ff to hh:mm:ss.sss |
| * |
| * @param duration the timestamp in hours:minutes:seconds:frames. |
| * @param framerate the frames per second |
| * @return the timestamp in milliseconds |
| */ |
| MP4Duration |
| ChapterUtility::convertFrameToMillis( MP4Duration duration, uint32_t framerate ) |
| { |
| Timecode tc( duration, CHAPTERTIMESCALE ); |
| if( framerate < tc.subseconds ) |
| { |
| uint64_t seconds = tc.subseconds / framerate; |
| tc.setSeconds( tc.seconds + seconds ); |
| tc.setSubseconds( (tc.subseconds - (seconds * framerate)) * framerate ); |
| } |
| else |
| { |
| tc.setSubseconds( tc.subseconds * framerate ); |
| } |
| |
| return tc.duration; |
| } |
| |
| /////////////////////////////////////////////////////////////////////////////// |
| |
| }} // namespace mp4v2::util |
| |
| /////////////////////////////////////////////////////////////////////////////// |
| |
| extern "C" |
| int main( int argc, char** argv ) |
| { |
| mp4v2::util::ChapterUtility util( argc, argv ); |
| return util.process(); |
| } |