/////////////////////////////////////////////////////////////////////////////// // // 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, vector&, 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 job.file * * * @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 << '"' << endl; Timecode duration(0, CHAPTERTIMESCALE); duration.setFormat( Timecode::DECIMAL ); for (uint32_t i = 0; i < chapterCount; ++i) { // print the infos report << '\t' << "Chapter #" << setw( 3 ) << setfill( '0' ) << i+1 << " - " << duration.svalue << " - " << '"' << chapters[i].title << '"' << 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 job.file * * * @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 ) << 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 job.file * * * @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 << '"' << 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 ); vector 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 job.file * * * @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 << '"' << 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" << setw( width ) << setfill( '0' ) << i+1 << '=' << duration.svalue << LINEND << "CHAPTER" << setw( width ) << 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 job.file * * * @param job the job to process * @return mp4v2::util::SUCCESS if successful, mp4v2::util::FAILURE otherwise */ bool ChapterUtility::actionImport( JobContext& job ) { vector 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 << '"' << 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( vector::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( CHAPTERTIMESCALE ); if( Timecode::FRAME == format ) { // get the framerate MP4SampleId sampleCount = MP4GetTrackNumberOfSamples( job.fileHandle, refTrackId ); Timecode tmpcd( refTrackDuration.svalue, CHAPTERTIMESCALE ); framerate = static_cast( std::ceil( ((double)sampleCount / (double)tmpcd.duration) * CHAPTERTIMESCALE ) ); } for( vector::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 job.file * * * @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 << '"' << 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 file 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 file. * 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 MP4TrackId 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 MP4ChapterType. * * @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 filename into a buffer allocated * by malloc and returns the pointer to this buffer in buffer and the size * of this buffer in fileSize. * * @param filename the name of the file. * @param buffer receives a pointer to the created buffer * @param fileSize reference to a io::StdioFile::Size 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( malloc( fileSize+1 ) ); if( in.read( inBuf, fileSize, nin ) ) { in.close(); 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 * MP4Chapter_t elements. * * @param filename the name of the file. * @param vector receives a vector of chapters * @param format receives the Timecode::Format of the timestamps * @return true if there was an error, false otherwise */ bool ChapterUtility::parseChapterFile( const string filename, vector& 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( 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 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(); }