diff options
Diffstat (limited to 'util/mp4chaps.cpp')
-rw-r--r-- | util/mp4chaps.cpp | 1159 |
1 files changed, 1159 insertions, 0 deletions
diff --git a/util/mp4chaps.cpp b/util/mp4chaps.cpp new file mode 100644 index 0000000..9008940 --- /dev/null +++ b/util/mp4chaps.cpp @@ -0,0 +1,1159 @@ +/////////////////////////////////////////////////////////////////////////////// +// +// 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. +// All Rights Reserved. +// +// Contributors: +// Kona Blend, kona8lend@@gmail.com +// Ullrich Pollaehne, u.pollaehne@@gmail.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<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(), _debugVerbosity ); + 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 <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 ) << endl; + + verbose1f( "%s", oss.str().c_str() ); + if( dryrunAbort() ) + { + return SUCCESS; + } + + job.fileHandle = MP4Modify( job.file.c_str(), _debugVerbosity ); + 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 << '"' << endl; + + verbose1f( "%s", oss.str().c_str() ); + if( dryrunAbort() ) + { + return SUCCESS; + } + + job.fileHandle = MP4Modify( job.file.c_str(), _debugVerbosity ); + 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 ); + Timecode durationSum( 0, CHAPTERTIMESCALE ); + durationSum.setFormat( Timecode::DECIMAL ); + vector<MP4Chapter_t> chapters; + + while( durationSum + chapterDuration < refTrackDuration ) + { + MP4Chapter_t chap; + chap.duration = chapterDuration.duration; + sprintf(chap.title, "Chapter %lu", chapters.size()+1); + + chapters.push_back( chap ); + + durationSum += chapterDuration; + } + + if( 0 < chapters.size() ) + { + chapters.back().duration = (refTrackDuration - (durationSum - chapterDuration)).duration; + + MP4SetChapters(job.fileHandle, &chapters[0], 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(), _debugVerbosity ); + 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 <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 ) +{ + 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 << '"' << 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(), _debugVerbosity ); + 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<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 + uint64_t framerate = CHAPTERTIMESCALE; + if( Timecode::FRAME == format ) + { + // get the framerate + MP4SampleId sampleCount = MP4GetTrackNumberOfSamples( job.fileHandle, refTrackId ); + Timecode tmpcd( refTrackDuration.svalue, CHAPTERTIMESCALE ); + framerate = std::ceil( ((double)sampleCount / (double)tmpcd.duration) * CHAPTERTIMESCALE ); + } + + for( 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], 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 << '"' << endl; + + verbose1f( "%s", oss.str().c_str() ); + if( dryrunAbort() ) + { + return SUCCESS; + } + + job.fileHandle = MP4Modify( job.file.c_str(), _debugVerbosity ); + 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) * MP4GetTimeScale(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(); + 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, 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 = 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 = 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(); +} |