NOTE: This patch is taken from http://sourceforge.net/forum/forum.php?thread_id=1837284&forum_id=59136 diff -urN ../mpeg4ip-1.6.1-orig/lib/mp4v2/Makefile.am lib/mp4v2/Makefile.am --- ../mpeg4ip-1.6.1-orig/lib/mp4v2/Makefile.am 2007-09-19 05:51:59 +0900 +++ lib/mp4v2/Makefile.am 2008-05-30 22:51:27 +0900 @@ -13,6 +13,7 @@ atom_amr.cpp \ atom_avc1.cpp \ atom_avcC.cpp \ + atom_chpl.cpp \ atom_d263.cpp \ atom_damr.cpp \ atom_dref.cpp \ diff -urN ../mpeg4ip-1.6.1-orig/lib/mp4v2/atom_chpl.cpp lib/mp4v2/atom_chpl.cpp --- ../mpeg4ip-1.6.1-orig/lib/mp4v2/atom_chpl.cpp 1970-01-01 09:00:00 +0900 +++ lib/mp4v2/atom_chpl.cpp 2008-05-30 22:51:27 +0900 @@ -0,0 +1,59 @@ +/* + * 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 MPEG4IP. + * + * The Initial Developer of the Original Code is Cisco Systems Inc. + * Portions created by Cisco Systems Inc. are + * Copyright (C) Cisco Systems Inc. 2001. All Rights Reserved. + * + * Contributed to MPEG4IP + * by Ullrich Pollaehne + * + * Nero chapter atom + */ + +#include "mp4common.h" + +// MP4ChplAtom is for Nero chapter list atom which is a child of udta +MP4ChplAtom::MP4ChplAtom () : MP4Atom("chpl") +{ + // it is not completely clear if version, flags, reserved and chaptercount + // have the right sizes but + // one thing is clear: chaptercount is not only 8-bit it is at least 16-bit + + // add the version + AddVersionAndFlags(); + + // add reserved bytes + AddReserved("reserved", 1); + + // define the chaptercount + MP4Integer32Property * counter = new MP4Integer32Property("chaptercount"); + AddProperty(counter); + + // define the chapterlist + MP4TableProperty * list = new MP4TableProperty("chapters", counter); + + // the start time as 100 nanoseconds units + list->AddProperty(new MP4Integer64Property("starttime")); + + // the chapter name as UTF-8 + list->AddProperty(new MP4StringProperty("name", true)); + + // add the chapterslist + AddProperty(list); +} + +void MP4ChplAtom::Generate () +{ + SetVersion(1); +} diff -urN ../mpeg4ip-1.6.1-orig/lib/mp4v2/atom_udta.cpp lib/mp4v2/atom_udta.cpp --- ../mpeg4ip-1.6.1-orig/lib/mp4v2/atom_udta.cpp 2006-02-23 09:28:57 +0900 +++ lib/mp4v2/atom_udta.cpp 2008-05-30 22:51:27 +0900 @@ -24,6 +24,7 @@ MP4UdtaAtom::MP4UdtaAtom() : MP4Atom("udta") { + ExpectChildAtom("chpl", Optional, OnlyOne); ExpectChildAtom("cprt", Optional, Many); ExpectChildAtom("hnti", Optional, OnlyOne); ExpectChildAtom("meta", Optional, OnlyOne); diff -urN ../mpeg4ip-1.6.1-orig/lib/mp4v2/atoms.h lib/mp4v2/atoms.h --- ../mpeg4ip-1.6.1-orig/lib/mp4v2/atoms.h 2007-09-19 05:51:59 +0900 +++ lib/mp4v2/atoms.h 2008-05-30 22:51:27 +0900 @@ -405,4 +405,10 @@ void Generate(void); }; +class MP4ChplAtom : public MP4Atom { + public: + MP4ChplAtom(); + void Generate(void); +}; + #endif /* __MP4_ATOMS_INCLUDED__ */ diff -urN ../mpeg4ip-1.6.1-orig/lib/mp4v2/libmp4v2.vcproj lib/mp4v2/libmp4v2.vcproj --- ../mpeg4ip-1.6.1-orig/lib/mp4v2/libmp4v2.vcproj 2007-09-19 05:51:59 +0900 +++ lib/mp4v2/libmp4v2.vcproj 2008-05-30 22:51:27 +0900 @@ -163,6 +163,10 @@ > + + diff -urN ../mpeg4ip-1.6.1-orig/lib/mp4v2/mp4.cpp lib/mp4v2/mp4.cpp --- ../mpeg4ip-1.6.1-orig/lib/mp4v2/mp4.cpp 2007-09-29 05:45:11 +0900 +++ lib/mp4v2/mp4.cpp 2008-05-30 22:51:27 +0900 @@ -1116,11 +1116,13 @@ } extern "C" MP4TrackId MP4AddChapterTextTrack( - MP4FileHandle hFile, MP4TrackId refTrackId) + MP4FileHandle hFile, + MP4TrackId refTrackId, + u_int32_t timescale) { if (MP4_IS_VALID_FILE_HANDLE(hFile)) { try { - return ((MP4File*)hFile)->AddChapterTextTrack(refTrackId); + return ((MP4File*)hFile)->AddChapterTextTrack(refTrackId, timescale); } catch (MP4Error* e) { PRINT_ERROR(e); @@ -1131,6 +1133,93 @@ } +extern "C" void MP4AddQTChapter( + MP4FileHandle hFile, + MP4TrackId chapterTrackId, + MP4Duration chapterDuration, + u_int32_t chapterNr, + const char * chapterTitle) +{ + if (MP4_IS_VALID_FILE_HANDLE(hFile)) { + try { + ((MP4File*)hFile)->AddChapter(chapterTrackId, chapterDuration, chapterNr, chapterTitle); + } + catch (MP4Error* e) { + PRINT_ERROR(e); + delete e; + } + } +} + + +extern "C" void MP4AddChapter( + MP4FileHandle hFile, + MP4Timestamp chapterStart, + const char * chapterTitle) +{ + if (MP4_IS_VALID_FILE_HANDLE(hFile)) { + try { + ((MP4File*)hFile)->AddChapter(chapterStart, chapterTitle); + } + catch (MP4Error* e) { + PRINT_ERROR(e); + delete e; + } + } +} + + +extern "C" void MP4ConvertChapters( + MP4FileHandle hFile, + bool toQT) +{ + if (MP4_IS_VALID_FILE_HANDLE(hFile)) { + try { + ((MP4File*)hFile)->ConvertChapters(toQT); + } + catch (MP4Error* e) { + PRINT_ERROR(e); + delete e; + } + } +} + + +extern "C" void MP4DeleteChapters( + MP4FileHandle hFile, + MP4TrackId chapterTrackId, + bool deleteQT) +{ + if (MP4_IS_VALID_FILE_HANDLE(hFile)) { + try { + ((MP4File*)hFile)->DeleteChapters(chapterTrackId, deleteQT); + } + catch (MP4Error* e) { + PRINT_ERROR(e); + delete e; + } + } +} + + +extern "C" void MP4GetChaptersList( + MP4FileHandle hFile, + MP4Chapters_t ** chapterList, + u_int32_t * chapterCount, + bool getQT) +{ + if (MP4_IS_VALID_FILE_HANDLE(hFile)) { + try { + ((MP4File*)hFile)->GetChaptersList(chapterList, chapterCount, getQT); + } + catch (MP4Error* e) { + PRINT_ERROR(e); + delete e; + } + } +} + + extern "C" MP4TrackId MP4CloneTrack (MP4FileHandle srcFile, MP4TrackId srcTrackId, MP4FileHandle dstFile, diff -urN ../mpeg4ip-1.6.1-orig/lib/mp4v2/mp4.h lib/mp4v2/mp4.h --- ../mpeg4ip-1.6.1-orig/lib/mp4v2/mp4.h 2007-09-29 05:45:11 +0900 +++ lib/mp4v2/mp4.h 2008-05-30 22:51:27 +0900 @@ -304,6 +304,15 @@ #define MPEG4_FGSP_L4 (0xfc) #define MPEG4_FGSP_L5 (0xfd) +/* chapter related definitions */ +#define CHAPTERTITLELEN 1023 +typedef struct MP4ChapterStruct { + MP4Duration duration; /* duration of a chapter in milliseconds*/ + char title[CHAPTERTITLELEN+1]; /* title of the chapter */ +} MP4Chapters_t; +/* milliseconds to 100 nanoseconds */ +#define MILLI2HUNDREDNANO 10000 + /* MP4 API declarations */ #ifdef __cplusplus @@ -599,7 +608,35 @@ MP4TrackId MP4AddChapterTextTrack( MP4FileHandle hFile, - MP4TrackId refTrackId); + MP4TrackId refTrackId, + u_int32_t timescale DEFAULT(0)); + +void MP4AddQTChapter( + MP4FileHandle hFile, + MP4TrackId chapterTrackId, + MP4Duration chapterDuration, + u_int32_t chapterNr, + const char * chapterTitle DEFAULT(0)); + +void MP4AddChapter( + MP4FileHandle hFile, + MP4Timestamp chapterStart, + const char * chapterTitle DEFAULT(0)); + +void MP4ConvertChapters( + MP4FileHandle hFile, + bool toQT DEFAULT(true)); + +void MP4DeleteChapters( + MP4FileHandle hFile, + MP4TrackId chapterTrackId DEFAULT(MP4_INVALID_TRACK_ID), + bool deleteQT DEFAULT(true)); + +void MP4GetChaptersList( + MP4FileHandle hFile, + MP4Chapters_t ** chapterList, + u_int32_t * chapterCount, + bool getQT DEFAULT(true)); MP4TrackId MP4CloneTrack( MP4FileHandle srcFile, diff -urN ../mpeg4ip-1.6.1-orig/lib/mp4v2/mp4atom.cpp lib/mp4v2/mp4atom.cpp --- ../mpeg4ip-1.6.1-orig/lib/mp4v2/mp4atom.cpp 2007-05-01 05:29:28 +0900 +++ lib/mp4v2/mp4atom.cpp 2008-05-30 22:51:27 +0900 @@ -93,6 +93,8 @@ case 'c': if (ATOMID(type) == ATOMID("chap")) { pAtom = new MP4TrefTypeAtom(type); + } else if (ATOMID(type) == ATOMID("chpl")) { + pAtom = new MP4ChplAtom(); } break; case 'd': @@ -269,11 +271,13 @@ static const char cpy[5]={0251,'c', 'p', 'y', '\0'}; static const char des[5]={0251,'d', 'e', 's','\0'}; static const char prd[5]={0251, 'p', 'r', 'd', '\0'}; + static const char lyr[5]={0251, 'l', 'y', 'r', '\0'}; if (ATOMID(type) == ATOMID(name) || ATOMID(type) == ATOMID(cmt) || ATOMID(type) == ATOMID(cpy) || ATOMID(type) == ATOMID(prd) || - ATOMID(type) == ATOMID(des)) { + ATOMID(type) == ATOMID(des) || + ATOMID(type) == ATOMID(lyr)) { pAtom = new MP4Meta2Atom(type); } break; diff -urN ../mpeg4ip-1.6.1-orig/lib/mp4v2/mp4file.cpp lib/mp4v2/mp4file.cpp --- ../mpeg4ip-1.6.1-orig/lib/mp4v2/mp4file.cpp 2007-09-29 05:45:11 +0900 +++ lib/mp4v2/mp4file.cpp 2008-05-30 22:51:27 +0900 @@ -2124,13 +2124,16 @@ return trackId; } -MP4TrackId MP4File::AddChapterTextTrack(MP4TrackId refTrackId) +MP4TrackId MP4File::AddChapterTextTrack(MP4TrackId refTrackId, u_int32_t timescale) { // validate reference track id (void)FindTrackIndex(refTrackId); - MP4TrackId trackId = - AddTrack(MP4_TEXT_TRACK_TYPE, GetTrackTimeScale(refTrackId)); + if (0 == timescale) { + timescale = GetTrackTimeScale(refTrackId); + } + + MP4TrackId trackId = AddTrack(MP4_TEXT_TRACK_TYPE, timescale); (void)InsertChildAtom(MakeTrackName(trackId, "mdia.minf"), "gmhd", 0); @@ -2168,6 +2171,340 @@ return trackId; } +void MP4File::AddChapter(MP4TrackId chapterTrackId, MP4Duration chapterDuration, u_int32_t chapterNr, const char * chapterTitle) +{ + if (0 == chapterTrackId) { + throw new MP4Error("No chapter track given","AddChapter"); + } + + uint32_t sampleLength = 0; + uint8_t sample[1040] = {0}; + int stringLen = 0; + char *string = (char *)&(sample[2]); + + if( chapterTitle != NULL ) + { + stringLen = strlen(chapterTitle); + strncpy( string, chapterTitle, MIN(stringLen, 1023) ); + } + + if( stringLen == 0 || stringLen >= 1024 ) + { + snprintf( string, 1023, "Chapter %03i", chapterNr ); + stringLen = strlen(string); + } + + sampleLength = stringLen + 2 + 12; // Account for text length code and other marker + + // 2-byte length marker + sample[0] = (stringLen >> 8) & 0xff; + sample[1] = stringLen & 0xff; + + int x = 2 + stringLen; + + // Modifier Length Marker + sample[x] = 0x00; + sample[x+1] = 0x00; + sample[x+2] = 0x00; + sample[x+3] = 0x0C; + + // Modifier Type Code + sample[x+4] = 'e'; + sample[x+5] = 'n'; + sample[x+6] = 'c'; + sample[x+7] = 'd'; + + // Modifier Value + sample[x+8] = 0x00; + sample[x+9] = 0x00; + sample[x+10] = (256 >> 8) & 0xff; + sample[x+11] = 256 & 0xff; + + WriteSample(chapterTrackId, sample, sampleLength, chapterDuration); +} + +void MP4File::AddChapter(MP4Timestamp chapterStart, const char * chapterTitle) +{ + MP4Atom * pChpl = FindAtom("moov.udta.chpl"); + if (!pChpl) { + pChpl = AddDescendantAtoms("", "moov.udta.chpl"); + } + + char buffer[256]; + int bufferLen = 0; + + MP4Integer32Property * pCount = (MP4Integer32Property*)pChpl->GetProperty(3); + pCount->IncrementValue(); + u_int32_t count = pCount->GetValue(); + + if (0 == chapterTitle) { + snprintf( buffer, 255, "Chapter %03i", count ); + } else { + int len = MIN(255, strlen(chapterTitle)); + strncpy( buffer, chapterTitle, len ); + buffer[len] = 0; + } + bufferLen = strlen(buffer); + + MP4TableProperty * pTable; + if (pChpl->FindProperty("chpl.chapters", (MP4Property **)&pTable)) { + MP4Integer64Property * pStartTime = (MP4Integer64Property *) pTable->GetProperty(0); + MP4StringProperty * pName = (MP4StringProperty *) pTable->GetProperty(1); + if (pStartTime && pTable) { + pStartTime->AddValue(chapterStart); + pName->AddValue(buffer); + } + } +} + +void MP4File::ConvertChapters(bool toQT) +{ + if (toQT) { + MP4Chapters_t * chapters = 0; + u_int32_t chapterCount = 0; + const char * name = 0; + MP4Duration chapterDurationSum = 0; + + GetChaptersList(&chapters, &chapterCount, false); + if (0 == chapterCount) { + throw new MP4Error("Could not find chapter markers", "ConvertChapters"); + } + + // remove chapter track if there is an existing one + DeleteChapters(); + + // create the chapter track + MP4TrackId refTrack = FindTrackId(0, MP4_AUDIO_TRACK_TYPE); + MP4TrackId chapterTrack = AddChapterTextTrack(refTrack, MP4_MILLISECONDS_TIME_SCALE); + + // calculate the duration of the chapter track + MP4Duration chapterTrackDuration = MP4ConvertTime(GetTrackDuration(refTrack), + GetTrackTimeScale(refTrack), + MP4_MILLISECONDS_TIME_SCALE); + + for (u_int32_t chapterIndex = 0 ; chapterIndex < chapterCount; ++chapterIndex) { + // calculate the duration + MP4Duration duration = chapters[chapterIndex].duration; + + // sum up the chapter duration + chapterDurationSum += duration; + + // create and write the chapter track sample for the previous chapter + AddChapter( chapterTrack, duration, chapterIndex+1, chapters[chapterIndex].title ); + } + + MP4Free(chapters); + } else { + MP4Chapters_t * chapters = 0; + u_int32_t chapterCount = 0; + + GetChaptersList(&chapters, &chapterCount); + if (0 == chapterCount) { + throw new MP4Error("Could not find chapter markers", "ConvertChapters"); + } + + // remove existing chapters + DeleteChapters(0, false); + + MP4Duration startTime = 0; + for (u_int32_t i = 0; i < chapterCount; ++i) { + const char * title = chapters[i].title; + MP4Duration duration = chapters[i].duration; + + AddChapter(startTime, title); + startTime += duration * MILLI2HUNDREDNANO; + } + + MP4Free(chapters); + } +} + +void MP4File::DeleteChapters(MP4TrackId chapterTrackId, bool deleteQT) +{ + if (!deleteQT) { + MP4Atom * pChpl = FindAtom("moov.udta.chpl"); + if (pChpl) { + MP4Atom * pParent = pChpl->GetParentAtom(); + pParent->DeleteChildAtom(pChpl); + } + return; + } + + char trackName[128] = {0}; + + // no text track given, find a suitable + if (0 == chapterTrackId) { + chapterTrackId = FindChapterTrack(trackName, 127); + } else { + FindChapterReferenceTrack(chapterTrackId, trackName, 127); + } + + if (0 != chapterTrackId && 0 != trackName[0]) { + // remove the reference + RemoveTrackReference(trackName, chapterTrackId); + + // remove the chapter track + DeleteTrack(chapterTrackId); + } +} + +void MP4File::GetChaptersList(MP4Chapters_t ** chapterList, + u_int32_t * chapterCount, + bool getQT) +{ + *chapterList = 0; + *chapterCount = 0; + + if (!getQT) { + MP4Atom * pChpl = FindAtom("moov.udta.chpl"); + if (!pChpl) { + throw new MP4Error("Atom moov.udta.chpl does not exist ", "GetChaptersList"); + } + + MP4Integer32Property * pCounter = 0; + MP4TableProperty * pTable = 0; + MP4Integer64Property * pStartTime = 0; + MP4StringProperty * pName = 0; + MP4Duration chapterDurationSum = 0; + const char * name = 0; + + if (!pChpl->FindProperty("chpl.chaptercount", (MP4Property **)&pCounter)) { + throw new MP4Error("Chapter count does not exist ", "GetChaptersList"); + } + + u_int32_t counter = pCounter->GetValue(); + if (0 == counter) { + return; + } + + if (!pChpl->FindProperty("chpl.chapters", (MP4Property **)&pTable)) { + throw new MP4Error("Chapter list does not exist ", "GetChaptersList"); + } + + if (0 == (pStartTime = (MP4Integer64Property *) pTable->GetProperty(0))) { + throw new MP4Error("List of Chapter starttimes does not exist ", "GetChaptersList"); + } + if (0 == (pName = (MP4StringProperty *) pTable->GetProperty(1))) { + throw new MP4Error("List of Chapter titles does not exist ", "GetChaptersList"); + } + + MP4Chapters_t * chapters = (MP4Chapters_t*)MP4Malloc(sizeof(MP4Chapters_t) * counter); + + // get the name of the first chapter + name = pName->GetValue(); + + // process remaining chapters + u_int32_t i, j; + for (i = 0, j = 1; i < counter; ++i, ++j) { + // insert the chapter title + u_int32_t len = MIN(strlen(name), CHAPTERTITLELEN); + strncpy(chapters[i].title, name, len); + chapters[i].title[len] = 0; + + // calculate the duration + MP4Duration duration = 0; + if (j < counter) { + duration = MP4ConvertTime(pStartTime->GetValue(j), + (MP4_NANOSECONDS_TIME_SCALE / 100), + MP4_MILLISECONDS_TIME_SCALE) - chapterDurationSum; + + // now get the name of the chapter (to be written next) + name = pName->GetValue(j); + } else { + // last chapter + duration = MP4ConvertTime(GetDuration(), GetTimeScale(), MP4_MILLISECONDS_TIME_SCALE) - chapterDurationSum; + } + + // sum up the chapter duration + chapterDurationSum += duration; + + // insert the chapter duration + chapters[i].duration = duration; + } + + *chapterList = chapters; + *chapterCount = counter; + + // ok, we're done + return; + } + + + u_int8_t * sample = 0; + u_int32_t sampleSize = 0; + MP4Timestamp startTime = 0; + MP4Duration duration = 0; + + // get the chapter track + MP4TrackId chapterTrackId = FindChapterTrack(); + if (0 == chapterTrackId) { + throw new MP4Error("Could not find a chapter track", "GetChaptersList"); + } + + // get infos about the chapters + MP4Track * pChapterTrack = GetTrack(chapterTrackId); + u_int32_t counter = pChapterTrack->GetNumberOfSamples(); + u_int32_t timescale = pChapterTrack->GetTimeScale(); + + MP4Chapters_t * chapters = (MP4Chapters_t*)MP4Malloc(sizeof(MP4Chapters_t) * counter); + + // process all chapter sample + for (u_int32_t i = 0; i < counter; ++i) { + // get the sample corresponding to the starttime + MP4SampleId sampleId = pChapterTrack->GetSampleIdFromTime(startTime + duration, true); + pChapterTrack->ReadSample(sampleId, &sample, &sampleSize); + + // get the starttime and duration + pChapterTrack->GetSampleTimes(sampleId, &startTime, &duration); + + // we know that sample+2 contains the title + const char * title = (const char *)&(sample[2]); + int len = MIN(strlen(title), CHAPTERTITLELEN); + strncpy(chapters[i].title, title, len); + chapters[i].title[len] = 0; + + // write the duration (in milliseconds) + chapters[i].duration = MP4ConvertTime(duration, timescale, MP4_MILLISECONDS_TIME_SCALE); + + // we're done with this sample + MP4Free(sample); + sample = 0; + } + + *chapterList = chapters; + *chapterCount = counter; +} + +MP4TrackId MP4File::FindChapterTrack(char * trackName, int trackNameSize) +{ + for (u_int32_t i = 0; i < m_pTracks.Size(); i++) { + if (!strcmp(MP4_TEXT_TRACK_TYPE, m_pTracks[i]->GetType())) { + MP4TrackId refTrackId = FindChapterReferenceTrack(m_pTracks[i]->GetId(), trackName, trackNameSize); + if (0 != refTrackId) { + return m_pTracks[i]->GetId(); + } + } + } + return 0; +} + +MP4TrackId MP4File::FindChapterReferenceTrack(MP4TrackId chapterTrackId, char * trackName, int trackNameSize) +{ + for (u_int32_t i = 0; i < m_pTracks.Size(); i++) { + if (!strcmp(MP4_AUDIO_TRACK_TYPE, m_pTracks[i]->GetType())) { + MP4TrackId refTrackId = m_pTracks[i]->GetId(); + char * name = MakeTrackName(refTrackId, "tref.chap"); + if (FindTrackReference(name, chapterTrackId)) { + if (0 != trackName) { + strncpy(trackName, name, MIN(strlen(name),trackNameSize)); + } + return m_pTracks[i]->GetId(); + } + } + } + return 0; +} + void MP4File::DeleteTrack(MP4TrackId trackId) { ProtectWriteOperation("MP4DeleteTrack"); diff -urN ../mpeg4ip-1.6.1-orig/lib/mp4v2/mp4file.h lib/mp4v2/mp4file.h --- ../mpeg4ip-1.6.1-orig/lib/mp4v2/mp4file.h 2007-09-29 05:45:11 +0900 +++ lib/mp4v2/mp4file.h 2008-05-30 22:51:27 +0900 @@ -310,7 +310,20 @@ MP4TrackId AddHintTrack(MP4TrackId refTrackId); MP4TrackId AddTextTrack(MP4TrackId refTrackId); - MP4TrackId AddChapterTextTrack(MP4TrackId refTrackId); + MP4TrackId AddChapterTextTrack(MP4TrackId refTrackId, u_int32_t timescale = 0); + void AddChapter(MP4TrackId chapterTrackId, + MP4Duration chapterDuration, + u_int32_t chapterNr, + const char * chapterTitle = 0); + void AddChapter(MP4Timestamp chapterStart, + const char * chapterTitle = 0); + void ConvertChapters(bool toQT = true); + void DeleteChapters(MP4TrackId chapterTrackId = 0, bool deleteQT = true); + void GetChaptersList(MP4Chapters_t ** chapterList, + u_int32_t * chapterCount, + bool getQT = true); + MP4TrackId FindChapterTrack(char * trackName = 0, int trackNameSize = 0); + MP4TrackId FindChapterReferenceTrack(MP4TrackId chapterTrackId, char * trackName = 0, int trackNameSize = 0); MP4SampleId GetTrackNumberOfSamples(MP4TrackId trackId); diff -urN ../mpeg4ip-1.6.1-orig/lib/mp4v2/util/Makefile.am lib/mp4v2/util/Makefile.am --- ../mpeg4ip-1.6.1-orig/lib/mp4v2/util/Makefile.am 2007-09-19 05:52:00 +0900 +++ lib/mp4v2/util/Makefile.am 2008-05-30 22:51:27 +0900 @@ -3,9 +3,13 @@ AM_CXXFLAGS = @BILLS_CPPWARNINGS@ -bin_PROGRAMS = mp4dump mp4extract mp4info mp4trackdump mp4tags mp4art mp4videoinfo +bin_PROGRAMS = mp4dump mp4extract mp4info mp4trackdump mp4tags mp4chaps mp4art mp4videoinfo check_PROGRAMS = mp4syncfiles +mp4chaps_SOURCES = mp4chaps.cpp +mp4chaps_LDADD = $(top_builddir)/lib/mp4v2/libmp4v2.la \ + $(top_builddir)/lib/gnu/libmpeg4ip_gnu.la + mp4dump_SOURCES = mp4dump.cpp mp4dump_LDADD = $(top_builddir)/lib/mp4v2/libmp4v2.la \ $(top_builddir)/lib/gnu/libmpeg4ip_gnu.la @@ -40,4 +44,4 @@ $(top_builddir)/lib/gnu/libmpeg4ip_gnu.la EXTRA_DIST = mp4dump60.dsp \ - mp4info.dsp mp4tags.dsp mp4dump.vcproj mp4info.vcproj mp4tags.vcproj + mp4info.dsp mp4tags.dsp mp4dump.vcproj mp4info.vcproj mp4tags.vcproj mp4chaps.vcproj diff -urN ../mpeg4ip-1.6.1-orig/lib/mp4v2/util/mp4chaps.cpp lib/mp4v2/util/mp4chaps.cpp --- ../mpeg4ip-1.6.1-orig/lib/mp4v2/util/mp4chaps.cpp 1970-01-01 09:00:00 +0900 +++ lib/mp4v2/util/mp4chaps.cpp 2008-05-30 22:51:27 +0900 @@ -0,0 +1,380 @@ +/* mp4chaps -- tool to set iTunes-compatible chapter markers from Nero chapter markers + * + * 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. + * + * Contributed to MPEG4IP + * by Ullrich Pollaehne + * with help from Handbrake + */ + +#include "mp4common.h" +#include "mp4file.h" +#include "mp4.h" +#include "mpeg4ip_getopt.h" + +/* One-letter options -- if you want to rearrange these, change them + here, immediately below in OPT_STRING, and in the help text. */ +#define OPT_HELP 'h' +#define OPT_VERSION 'v' +#define OPT_REMOVE 'r' +#define OPT_CONVERT 'c' +#define OPT_OPTIMIZE 'o' +#define OPT_QT 'Q' +#define OPT_NERO 'N' +#define OPT_EVERY 'e' +#define OPT_LIST 'l' +#define OPT_STRING "hvrcoNQe:l" + +static const char* help_text = +"OPTION... FILE...\n" +"\nConvert/Manage Nero chapter markers or QT/iTunes/iPod\n" +" chapter markers in MP4 Audio files\n" +"\n" +" -h, -help Display this help text and exit\n" +" -v, -version Display version information and exit\n" +" -r, -remove Remove chapter markers\n" +" -c, -convert Convert chapter markers\n" +" -o, -optimize re-write file in optimized form\n" +" -N, -Nero write/remove/convert to Nero chapter markers\n" +" -Q, -QuickTime write/remove/convert to QuickTime chapter markers\n" +" -e, -every n creates chapter markers every n seconds\n" +" -l, -list list available chapter markers\n" +"\n"; + + +// function prototypes +void ListChapters(MP4FileHandle hFile, bool useQT = true); +int CreateQTChaptersEvery(MP4FileHandle hFile, int milliSeconds, const char * filename); +int CreateChaptersEvery(MP4FileHandle hFile, int milliSeconds, const char * filename); + + + +int main(int argc, char** argv) +{ + static struct option long_options[] = { + { "help", 0, 0, OPT_HELP }, + { "version", 0, 0, OPT_VERSION }, + { "remove", 0, 0, OPT_REMOVE }, + { "convert", 0, 0, OPT_CONVERT }, + { "optimize", 0, 0, OPT_OPTIMIZE }, + { "Nero", 0, 0, OPT_NERO }, + { "QuickTime", 0, 0, OPT_QT }, + { "every", 1, 0, OPT_EVERY }, + { "list", 0, 0, OPT_LIST }, + { NULL, 0, 0, 0 } + }; + + + bool optEvery = false; + bool optDelete = false; + bool optConvert = false; + bool optOptimize = false; + bool optQT = false; + bool optNero = false; + bool optList = false; + bool needsModification = false; + u_int32_t seconds = 0; + + /* Option-processing loop. */ + int c = getopt_long_only(argc, argv, OPT_STRING, long_options, NULL); + while (c != -1) { + switch(c) { + /* getopt() returns '?' if there was an error. It already + printed the error message, so just return. */ + case '?': + return 1; + + /* Help and version requests handled here. */ + case OPT_HELP: + fprintf(stderr, "usage %s %s", argv[0], help_text); + return 0; + case OPT_VERSION: + fprintf(stderr, "%s - %s version %s\n", argv[0], MPEG4IP_PACKAGE, + MPEG4IP_VERSION); + return 0; + + case OPT_NERO: + optNero = true; + break; + + case OPT_QT: + optQT = true; + break; + + case OPT_REMOVE: + optDelete = true; + needsModification = true; + break; + + case OPT_CONVERT: + optConvert = true; + needsModification = true; + break; + + case OPT_OPTIMIZE: + optOptimize = true; + needsModification = true; + break; + + case OPT_LIST: + optList = true; + break; + + /* Numeric arguments: convert them using sscanf(). */ + case OPT_EVERY: + { + int r = sscanf(optarg, "%ul", &seconds); + if (r < 1) { + fprintf(stderr, "%s: option requires numeric argument -- %c\n", + argv[0], c); + return 2; + } + optEvery = true; + needsModification = true; + } + break; + + default: + break; + } /* end switch */ + + c = getopt_long_only(argc, argv, OPT_STRING, long_options, NULL); + } /* end while */ + + /* Check that we have at least one non-option argument */ + if ((argc - optind) < 1) { + fprintf(stderr, + "%s: You must specify at least one MP4 file.\n", + argv[0]); + fprintf(stderr, "usage %s %s", argv[0], help_text); + return 3; + } + + /* Loop through the non-option arguments, and do the requested chapter work */ + int rc = 0; + bool needsOptimize = false; + while (optind < argc) { + char *filename = argv[optind++]; + + MP4FileHandle h = 0; + if (needsModification) { + h = MP4Modify( filename, MP4_DETAILS_ERROR); + } else { + h = MP4Read( filename, MP4_DETAILS_ERROR ); + } + + if (h == MP4_INVALID_FILE_HANDLE) { + fprintf(stderr, "Could not open '%s'... aborting\n", filename); + return 5; + } + + if(optDelete) { + if (optQT) { + MP4DeleteChapters(h); + } + if (optNero) { + MP4DeleteChapters(h, MP4_INVALID_TRACK_ID, false); + } + if (!optQT && ! optNero) { + MP4DeleteChapters(h); + MP4DeleteChapters(h, MP4_INVALID_TRACK_ID, false); + } + } + + if(optEvery) { + if (optQT) { + rc = CreateQTChaptersEvery(h, seconds*MP4_MILLISECONDS_TIME_SCALE, filename); + } + if (optNero && 0 == rc) { + rc = CreateChaptersEvery(h, seconds*MP4_MILLISECONDS_TIME_SCALE, filename); + } + if (!optQT && !optNero) { + rc = CreateChaptersEvery(h, seconds*MP4_MILLISECONDS_TIME_SCALE, filename); + if (0 == rc) { + MP4ConvertChapters(h); + } + } + if (0 != rc) { + MP4Close(h); + continue; + } + needsOptimize = true; + } + + if (optConvert) { + if (optQT) { + MP4ConvertChapters(h, optQT); + needsOptimize = true; + } else if (optNero) { + MP4ConvertChapters(h, false); + needsOptimize = true; + } + } + + if (optList) { + ListChapters(h, optQT); + } + + if (0 == rc) { + MP4Close(h); + + if (optOptimize || needsOptimize) { + MP4Optimize(filename); + } + } + } /* end while optind < argc */ + + return 0; +} + +/* + * format a duration to a readable format ("stolen" from gpac MP4Box) + */ +static char *format_duration(u_int64_t dur, char *szDur) +{ + u_int32_t h, m, s, ms; + + h = (u_int32_t) (dur / 3600000); + m = (u_int32_t) (dur/ 60000) - h*60; + s = (u_int32_t) (dur/1000) - h*3600 - m*60; + ms = (u_int32_t) (dur) - h*3600000 - m*60000 - s*1000; + if (h<=24) { + sprintf(szDur, "%02d:%02d:%02d.%03d", h, m, s, ms); + } else { + u_int32_t d = (u_int32_t) (dur / 3600000 / 24); + h = (u_int32_t) (dur/3600000)-24*d; + if (d<=365) { + sprintf(szDur, "%d Days, %02d:%02d:%02d.%03d", d, h, m, s, ms); + } else { + u_int32_t y=0; + while (d>365) { + y++; + d-=365; + if (y%4) d--; + } + sprintf(szDur, "%d Years %d Days, %02d:%02d:%02d.%03d", y, d, h, m, s, ms); + } + } + return szDur; +} + +/* + * List chapter start time and title + */ +void ListChapters(MP4FileHandle hFile, bool useQT) +{ + MP4Chapters_t * chapters = 0; + u_int32_t chapterCount = 0; + char szDur[20] = {0}; + MP4Duration durationSum = 0; + + // get the list of chapters + MP4GetChaptersList(hFile, &chapters, &chapterCount, useQT); + if (0 == chapterCount) { + return; + } + + // start output (in mp4box format) + fprintf(stdout, "\nChapters:\n"); + + for(u_int32_t i = 0; i < chapterCount; ++i) { + // get the tile + const char * title = chapters[i].title; + // format the start time + const char * formattedDuration = format_duration(durationSum, szDur); + + // print the infos + fprintf(stdout, "\tChapter #%u - %s - \"%s\"\n", i+1, formattedDuration, title); + + // add the duration of this chapter to the sum (is the start time of the next chapter) + durationSum += chapters[i].duration; + } + + // free up the memory + MP4Free(chapters); +} + +/* + * Create QT/iTunes/iPod chapter markers with a duration of 'milliSeconds' milliseconds + */ +int CreateQTChaptersEvery(MP4FileHandle hFile, int milliSeconds, const char * filename) +{ + // delete previous chapter markers + MP4DeleteChapters(hFile); + + // get the audio track that will reference the chapter track + MP4TrackId refTrackId = MP4FindTrackId(hFile, MP4_INVALID_TRACK_ID, MP4_AUDIO_TRACK_TYPE); + if (! MP4_IS_VALID_TRACK_ID(refTrackId)) { + MP4Close(hFile); + fprintf(stderr, "Could not find audio track in file '%s'... aborting\n", filename); + return 6; + } + + // create the chapter track ... + MP4TrackId chapterTrack = MP4AddChapterTextTrack(hFile, refTrackId, MP4_MILLISECONDS_TIME_SCALE); + + // get informations about the audio track + MP4Duration refTrackDuration = MP4GetTrackDuration(hFile, refTrackId); + uint32_t refTrackTimeScale = MP4GetTrackTimeScale(hFile, refTrackId); + + MP4Duration chapterDuration = milliSeconds; + //MP4Duration chapterTrackDuration = (refTrackDuration * MP4_MILLISECONDS_TIME_SCALE) / refTrackTimeScale; + MP4Duration chapterTrackDuration = MP4ConvertTime(refTrackDuration, refTrackTimeScale, MP4_MILLISECONDS_TIME_SCALE); + + // create chapters + MP4Duration chapterSum; + int i; + for (chapterSum = 0, i = 1; chapterTrackDuration > chapterSum; chapterSum += chapterDuration, ++i) { + if (chapterTrackDuration < (chapterSum + chapterDuration)) { + chapterDuration = chapterTrackDuration - chapterSum; + } + + // chapterDuration is expected as count of samples related to the timescale of chapterTrack + MP4AddQTChapter( hFile, chapterTrack, chapterDuration, i ); + } + + return 0; +} + + +/* + * Create Nero chapter markers with a duration of 'milliSeconds' milliseconds + */ +int CreateChaptersEvery(MP4FileHandle hFile, int milliSeconds, const char * filename) +{ + // delete previous chapter markers + MP4DeleteChapters(hFile, MP4_INVALID_TRACK_ID, false); + + // get the audio track that will reference the chapter track + MP4TrackId refTrackId = MP4FindTrackId(hFile, MP4_INVALID_TRACK_ID, MP4_AUDIO_TRACK_TYPE); + if (! MP4_IS_VALID_TRACK_ID(refTrackId)) { + MP4Close(hFile); + fprintf(stderr, "Could not find audio track in file '%s'... aborting\n", filename); + return 6; + } + + // get informations about the audio track + MP4Duration refTrackDuration = MP4GetTrackDuration(hFile, refTrackId); + uint32_t refTrackTimeScale = MP4GetTrackTimeScale(hFile, refTrackId); + + // we need the duration in 100 nanosecond units + MP4Duration durationIn100Nanos = MP4ConvertTime( refTrackDuration, refTrackTimeScale, MP4_NANOSECONDS_TIME_SCALE / 100 ); + //MP4Duration durationIn100Nanos = ((refTrackDuration * MP4_MILLISECONDS_TIME_SCALE) / refTrackTimeScale) * 10000; + MP4Duration chapterDuration = milliSeconds * 10000; + + // create the chapters + for (MP4Duration startTime = 0; durationIn100Nanos > startTime; startTime += chapterDuration) { + // starttime is expected to be in 100-nanosecond units + MP4AddChapter( hFile, startTime ); + } + + return 0; +} diff -urN ../mpeg4ip-1.6.1-orig/lib/mp4v2/util/mp4chaps.vcproj lib/mp4v2/util/mp4chaps.vcproj --- ../mpeg4ip-1.6.1-orig/lib/mp4v2/util/mp4chaps.vcproj 1970-01-01 09:00:00 +0900 +++ lib/mp4v2/util/mp4chaps.vcproj 2008-05-30 22:51:27 +0900 @@ -0,0 +1,208 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + --- ../mpeg4ip-1.6.1-orig/tools.sln 2007-09-29 05:45:08 +0900 +++ tools.sln 2008-05-30 22:52:53 +0900 @@ -2,6 +2,12 @@ # Visual Studio 2005 Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "libmp4v2", "lib\mp4v2\libmp4v2.vcproj", "{56EAC9DA-F67D-433B-AEBB-7C61CF2EF505}" EndProject +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "mp4chaps", "lib\mp4v2\util\mp4chaps.vcproj", "{990E370A-FEA4-461A-80FA-CF9B59A5350D}" + ProjectSection(ProjectDependencies) = postProject + {9EF07C35-1D27-424D-970E-0E89D97DD111} = {9EF07C35-1D27-424D-970E-0E89D97DD111} + {56EAC9DA-F67D-433B-AEBB-7C61CF2EF505} = {56EAC9DA-F67D-433B-AEBB-7C61CF2EF505} + EndProjectSection +EndProject Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "mp4info", "lib\mp4v2\util\mp4info.vcproj", "{22B77017-61F0-4D4A-A8F4-BE726F2E917C}" ProjectSection(ProjectDependencies) = postProject {56EAC9DA-F67D-433B-AEBB-7C61CF2EF505} = {56EAC9DA-F67D-433B-AEBB-7C61CF2EF505} @@ -79,6 +85,10 @@ {56EAC9DA-F67D-433B-AEBB-7C61CF2EF505}.Debug|Win32.Build.0 = Debug|Win32 {56EAC9DA-F67D-433B-AEBB-7C61CF2EF505}.Release|Win32.ActiveCfg = Release|Win32 {56EAC9DA-F67D-433B-AEBB-7C61CF2EF505}.Release|Win32.Build.0 = Release|Win32 + {990E370A-FEA4-461A-80FA-CF9B59A5350D}.Debug|Win32.ActiveCfg = Debug|Win32 + {990E370A-FEA4-461A-80FA-CF9B59A5350D}.Debug|Win32.Build.0 = Debug|Win32 + {990E370A-FEA4-461A-80FA-CF9B59A5350D}.Release|Win32.ActiveCfg = Release|Win32 + {990E370A-FEA4-461A-80FA-CF9B59A5350D}.Release|Win32.Build.0 = Release|Win32 {22B77017-61F0-4D4A-A8F4-BE726F2E917C}.Debug|Win32.ActiveCfg = Debug|Win32 {22B77017-61F0-4D4A-A8F4-BE726F2E917C}.Debug|Win32.Build.0 = Debug|Win32 {22B77017-61F0-4D4A-A8F4-BE726F2E917C}.Release|Win32.ActiveCfg = Release|Win32