OSDN Git Service

Jamfile - Fixes tab instead of 4 spaces.
[handbrake-jp/handbrake-jp-git.git] / libhb / muxmp4.c
index 7aae352..fe6cc27 100644 (file)
@@ -1,7 +1,7 @@
 /* $Id: muxmp4.c,v 1.24 2005/11/04 13:09:41 titer Exp $
 
    This file is part of the HandBrake source code.
-   Homepage: <http://handbrake.m0k.org/>.
+   Homepage: <http://handbrake.fr/>.
    It may be used under the terms of the GNU General Public License. */
 
 /* libmp4v2 header */
 
 void AddIPodUUID(MP4FileHandle, MP4TrackId);
 
-/* B-frame muxing variables */
-MP4SampleId thisSample = 0;
-uint64_t initDelay;
-
 struct hb_mux_object_s
 {
     HB_MUX_COMMON;
@@ -24,9 +20,22 @@ struct hb_mux_object_s
     /* libmp4v2 handle */
     MP4FileHandle file;
 
-    /* Cumulated durations so far, in timescale units (see MP4Mux) */
-    uint64_t sum_dur;
-       
+    /* Cumulated durations so far, in output & input timescale units (see MP4Mux) */
+    int64_t sum_dur;        // duration in output timescale units
+    int64_t sum_dur_in;     // duration in input 90KHz timescale units
+
+    // bias to keep render offsets in ctts atom positive (set up by encx264)
+    int64_t init_delay;
+
+    /* Chapter state information for muxing */
+    MP4TrackId chapter_track;
+    int current_chapter;
+    uint64_t chapter_duration;
+
+    /* Sample rate of the first audio track.
+     * Used for the timescale
+     */
+    int samplerate;
 };
 
 struct hb_mux_data_s
@@ -34,6 +43,97 @@ struct hb_mux_data_s
     MP4TrackId track;
 };
 
+struct hb_text_sample_s
+{
+    uint8_t     sample[1280];
+    uint32_t    length;
+    MP4Duration duration;
+};
+
+/**********************************************************************
+ * MP4CreateTextSample
+ **********************************************************************
+ * Creates a buffer for a text track sample
+ *********************************************************************/
+static struct hb_text_sample_s *MP4CreateTextSample( char *textString, uint64_t duration )
+{
+    struct hb_text_sample_s *sample = NULL;
+    int stringLength = strlen(textString);
+    int x;
+
+    if( stringLength < 1024 )
+    {
+        sample = malloc( sizeof( struct hb_text_sample_s ) );
+
+        //textLength = (stringLength; // Account for BOM
+        sample->length = stringLength + 2 + 12; // Account for text length code and other marker
+        sample->duration = (MP4Duration)duration;
+
+        // 2-byte length marker
+        sample->sample[0] = (stringLength >> 8) & 0xff;
+        sample->sample[1] = stringLength & 0xff;
+
+        strncpy( (char *)&(sample->sample[2]), textString, stringLength );
+
+        x = 2 + stringLength;
+
+        // Modifier Length Marker
+        sample->sample[x] = 0x00;
+        sample->sample[x+1] = 0x00;
+        sample->sample[x+2] = 0x00;
+        sample->sample[x+3] = 0x0C;
+
+        // Modifier Type Code
+        sample->sample[x+4] = 'e';
+        sample->sample[x+5] = 'n';
+        sample->sample[x+6] = 'c';
+        sample->sample[x+7] = 'd';
+
+        // Modifier Value
+        sample->sample[x+8] = 0x00;
+        sample->sample[x+9] = 0x00;
+        sample->sample[x+10] = (256 >> 8) & 0xff;
+        sample->sample[x+11] = 256 & 0xff;
+    }
+
+    return sample;
+}
+
+/**********************************************************************
+ * MP4GenerateChapterSample
+ **********************************************************************
+ * Creates a buffer for a text track sample
+ *********************************************************************/
+static struct hb_text_sample_s *MP4GenerateChapterSample( hb_mux_object_t * m,
+                                                          uint64_t duration,
+                                                          int chapter )
+{
+    // We substract 1 from the chapter number because the chapters start at
+    // 1 but our name array starts at 0. We substract another 1 because we're
+    // writing the text of the previous chapter mark (when we get the start
+    // of chapter 2 we know the duration of chapter 1 & can write its mark).
+    hb_chapter_t *chapter_data = hb_list_item( m->job->title->list_chapter,
+                                               chapter - 2 );
+    char tmp_buffer[1024];
+    char *string = tmp_buffer;
+
+    tmp_buffer[0] = '\0';
+
+    if( chapter_data != NULL )
+    {
+        string = chapter_data->title;
+    }
+
+    if( strlen(string) == 0 || strlen(string) >= 1024 )
+    {
+        snprintf( tmp_buffer, 1023, "Chapter %03i", chapter - 2 );
+        string = tmp_buffer;
+    }
+
+    return MP4CreateTextSample( string, duration );
+}
+
+
 /**********************************************************************
  * MP4Init
  **********************************************************************
@@ -43,14 +143,45 @@ static int MP4Init( hb_mux_object_t * m )
 {
     hb_job_t   * job   = m->job;
     hb_title_t * title = job->title;
-    
+
     hb_audio_t    * audio;
     hb_mux_data_t * mux_data;
     int i;
     u_int16_t language_code;
 
+    /* Flags for enabling/disabling tracks in an MP4. */
+    typedef enum { TRACK_DISABLED = 0x0, TRACK_ENABLED = 0x1, TRACK_IN_MOVIE = 0x2, TRACK_IN_PREVIEW = 0x4, TRACK_IN_POSTER = 0x8}  track_header_flags;
+
+    if( (audio = hb_list_item(title->list_audio, 0)) != NULL )
+    {
+        /* Need the sample rate of the first audio track to use as the timescale. */
+        m->samplerate = audio->config.out.samplerate;
+        audio = NULL;
+    }
+    else
+    {
+        m->samplerate = 90000;
+    }
+
     /* Create an empty mp4 file */
-    m->file = MP4Create( job->file, MP4_DETAILS_ERROR, 0 );
+    if (job->largeFileSize)
+    /* Use 64-bit MP4 file */
+    {
+        m->file = MP4Create( job->file, MP4_DETAILS_ERROR, MP4_CREATE_64BIT_DATA );
+        hb_log("Using 64-bit MP4 formatting.");
+    }
+    else
+    /* Limit MP4s to less than 4 GB */
+    {
+        m->file = MP4Create( job->file, MP4_DETAILS_ERROR, 0 );
+    }
+
+    if (m->file == MP4_INVALID_FILE_HANDLE)
+    {
+        hb_error("muxmp4.c: MP4Create failed!");
+        *job->die = 1;
+        return 0;
+    }
 
     /* Video track */
     mux_data      = malloc( sizeof( hb_mux_data_t ) );
@@ -60,149 +191,196 @@ static int MP4Init( hb_mux_object_t * m )
        synchronization issues (audio not playing at the correct speed).
        To workaround this, we use the audio samplerate as the
        timescale */
-    MP4SetTimeScale( m->file, job->arate );
+    if (!(MP4SetTimeScale( m->file, m->samplerate )))
+    {
+        hb_error("muxmp4.c: MP4SetTimeScale failed!");
+        *job->die = 1;
+        return 0;
+    }
 
     if( job->vcodec == HB_VCODEC_X264 )
     {
         /* Stolen from mp4creator */
-        MP4SetVideoProfileLevel( m->file, 0x7F );
-
-               mux_data->track = MP4AddH264VideoTrack( m->file, job->arate,
+        if(!(MP4SetVideoProfileLevel( m->file, 0x7F )))
+        {
+            hb_error("muxmp4.c: MP4SetVideoProfileLevel failed!");
+            *job->die = 1;
+            return 0;
+        }
+
+               mux_data->track = MP4AddH264VideoTrack( m->file, m->samplerate,
                        MP4_INVALID_DURATION, job->width, job->height,
                        job->config.h264.sps[1], /* AVCProfileIndication */
                        job->config.h264.sps[2], /* profile_compat */
                        job->config.h264.sps[3], /* AVCLevelIndication */
                        3 );      /* 4 bytes length before each NAL unit */
-               
+
 
         MP4AddH264SequenceParameterSet( m->file, mux_data->track,
                 job->config.h264.sps, job->config.h264.sps_length );
         MP4AddH264PictureParameterSet( m->file, mux_data->track,
                 job->config.h264.pps, job->config.h264.pps_length );
 
-               if( job->h264_level == 30)
+               if( job->h264_level == 30 || job->ipod_atom)
                {
                        hb_log("About to add iPod atom");
                        AddIPodUUID(m->file, mux_data->track);
                }
 
+        m->init_delay = job->config.h264.init_delay;
     }
     else /* FFmpeg or XviD */
     {
-        MP4SetVideoProfileLevel( m->file, MPEG4_SP_L3 );
-        mux_data->track = MP4AddVideoTrack( m->file, job->arate,
+        if(!(MP4SetVideoProfileLevel( m->file, MPEG4_SP_L3 )))
+        {
+            hb_error("muxmp4.c: MP4SetVideoProfileLevel failed!");
+            *job->die = 1;
+            return 0;
+        }
+        mux_data->track = MP4AddVideoTrack( m->file, m->samplerate,
                 MP4_INVALID_DURATION, job->width, job->height,
                 MP4_MPEG4_VIDEO_TYPE );
+        if (mux_data->track == MP4_INVALID_TRACK_ID)
+        {
+            hb_error("muxmp4.c: MP4AddVideoTrack failed!");
+            *job->die = 1;
+            return 0;
+        }
+
 
         /* VOL from FFmpeg or XviD */
-        MP4SetTrackESConfiguration( m->file, mux_data->track,
-                job->config.mpeg4.bytes, job->config.mpeg4.length );
+        if (!(MP4SetTrackESConfiguration( m->file, mux_data->track,
+                job->config.mpeg4.bytes, job->config.mpeg4.length )))
+        {
+            hb_error("muxmp4.c: MP4SetTrackESConfiguration failed!");
+            *job->die = 1;
+            return 0;
+        }
     }
 
-       /* apply the anamorphic transformation matrix if needed */
-
-       if( job->pixel_ratio ) {
-
-               uint8_t* val;
-               uint8_t nval[38];
-               uint32_t *ptr32 = (uint32_t*) (nval + 2);
-               uint32_t size;
-
-               MP4GetBytesProperty(m->file, "moov.trak.tkhd.reserved3", &val, &size);
-
-               if (size == 38) {
-
-                       memcpy(nval, val, size);
-
-                       float width, height;
-                       float widthRatio;
-                       width = job->pixel_aspect_width;
-                       height = job->pixel_aspect_height;
-                       widthRatio = (width / height) * 0x10000;
-
-                       uint32_t widthRatioInt;
-                       widthRatioInt = (uint32_t)widthRatio;
+    // COLR atom for color and gamma correction.
+    // Per the notes at:
+    //   http://developer.apple.com/quicktime/icefloe/dispatch019.html#colr
+    //   http://forum.doom9.org/showthread.php?t=133982#post1090068
+    // we say anything that's likely to be HD content is ITU BT.709 and
+    // DVD, SD TV & other content is ITU BT.601.  We look at the title height
+    // rather than the job height here to get uncropped input dimensions.
+    if ( job->title->height >= 720 )
+    {
+        // we guess that 720p or above is ITU BT.709 HD content
+        MP4AddColr(m->file, mux_data->track, 1, 1, 1);
+    }
+    else
+    {
+        // ITU BT.601 DVD or SD TV content
+        MP4AddColr(m->file, mux_data->track, 6, 1, 6);
+    }
 
-#ifdef WORDS_BIGENDIAN
-                       ptr32[0] = widthRatioInt;
-#else
-                       /* we need to switch the endianness, as the file format expects big endian */
-                       ptr32[0] = ((widthRatioInt & 0x000000FF) << 24) + ((widthRatioInt & 0x0000FF00) << 8) + ((widthRatioInt & 0x00FF0000) >> 8) + ((widthRatioInt & 0xFF000000) >> 24);
-#endif
+    if( job->pixel_ratio )
+    {
+        /* PASP atom for anamorphic video */
+        float width, height;
 
-                       if(!MP4SetBytesProperty(m->file, "moov.trak.tkhd.reserved3", nval, size)) {
-                               hb_log("Problem setting transform matrix");
-                       }
-                       
-               }
+        width = job->pixel_aspect_width;
 
-       }
+        height = job->pixel_aspect_height;
 
-       /* end of transformation matrix */
+        MP4AddPixelAspectRatio(m->file, mux_data->track, (uint32_t)width, (uint32_t)height);
 
-       /* firstAudioTrack will be used to reference the first audio track when we add a chapter track */
-       MP4TrackId firstAudioTrack = 0;
+        MP4SetTrackFloatProperty(m->file, mux_data->track, "tkhd.width", job->width * (width / height));
+    }
 
        /* add the audio tracks */
     for( i = 0; i < hb_list_count( title->list_audio ); i++ )
     {
+       static u_int8_t reserved2[16] = {
+               0x00, 0x00, 0x00, 0x00,
+               0x00, 0x00, 0x00, 0x00,
+               0x00, 0x02, 0x00, 0x10,
+               0x00, 0x00, 0x00, 0x00,
+           };
+
         audio = hb_list_item( title->list_audio, i );
         mux_data = malloc( sizeof( hb_mux_data_t ) );
-        audio->mux_data = mux_data;
-
-        mux_data->track = MP4AddAudioTrack( m->file,
-                job->arate, 1024, MP4_MPEG4_AUDIO_TYPE );
-        MP4SetAudioProfileLevel( m->file, 0x0F );
-        MP4SetTrackESConfiguration( m->file, mux_data->track,
-                audio->config.aac.bytes, audio->config.aac.length );
-                
+        audio->priv.mux_data = mux_data;
+
+        if( audio->config.out.codec == HB_ACODEC_AC3 )
+        {
+            mux_data->track = MP4AddAC3AudioTrack(
+                m->file,
+                m->samplerate, 1536, MP4_MPEG4_AUDIO_TYPE );
+            MP4SetTrackBytesProperty(
+                m->file, mux_data->track,
+                "udta.name.value",
+                (const u_int8_t*)"Surround", strlen("Surround"));
+        } else {
+            mux_data->track = MP4AddAudioTrack(
+                m->file,
+                m->samplerate, 1024, MP4_MPEG4_AUDIO_TYPE );
+            MP4SetTrackBytesProperty(
+                m->file, mux_data->track,
+                "udta.name.value",
+                (const u_int8_t*)"Stereo", strlen("Stereo"));
+
+            MP4SetAudioProfileLevel( m->file, 0x0F );
+            MP4SetTrackESConfiguration(
+                m->file, mux_data->track,
+                audio->priv.config.aac.bytes, audio->priv.config.aac.length );
+
+            /* Set the correct number of channels for this track */
+            reserved2[9] = (u_int8_t)HB_AMIXDOWN_GET_DISCRETE_CHANNEL_COUNT(audio->config.out.mixdown);
+            MP4SetTrackBytesProperty(m->file, mux_data->track, "mdia.minf.stbl.stsd.mp4a.reserved2", reserved2, sizeof(reserved2));
+
+        }
         /* Set the language for this track */
         /* The language is stored as 5-bit text - 0x60 */
-        language_code = audio->iso639_2[0] - 0x60;   language_code <<= 5;
-        language_code |= audio->iso639_2[1] - 0x60;  language_code <<= 5;
-        language_code |= audio->iso639_2[2] - 0x60;
+        language_code = audio->config.lang.iso639_2[0] - 0x60;   language_code <<= 5;
+        language_code |= audio->config.lang.iso639_2[1] - 0x60;  language_code <<= 5;
+        language_code |= audio->config.lang.iso639_2[2] - 0x60;
         MP4SetTrackIntegerProperty(m->file, mux_data->track, "mdia.mdhd.language", language_code);
-                               
-               /* store a reference to the first audio track,
-               so we can use it to feed the chapter text track's sample rate */
-               if (i == 0) {
-                       firstAudioTrack = mux_data->track;
-               }
-               
-    }
 
-       if (job->chapter_markers) {
 
-               /* add a text track for the chapters */
-               MP4TrackId textTrack;
+        /* Set the audio track alternate group */
+        MP4SetTrackIntegerProperty(m->file, mux_data->track, "tkhd.alternate_group", 1);
 
-               textTrack = MP4AddChapterTextTrack(m->file, firstAudioTrack);
+        /* If we ever upgrade mpeg4ip, the line above should be replaced with the line below.*/
+//        MP4SetTrackIntegerProperty(m->file, mux_data->track, "mdia.minf.stbl.stsd.mp4a.channels",  (u_int16_t)HB_AMIXDOWN_GET_DISCRETE_CHANNEL_COUNT(audio->amixdown));
 
-               /* write the chapter markers for each selected chapter */
-               char markerBuf[13];
-               hb_chapter_t  * chapter;
-               MP4Duration chapterDuration;
-               float fOrigDuration, fTimescale;
-               float fTSDuration;
+        if (i == 0) {
+            /* Enable the first audio track */
+            MP4SetTrackIntegerProperty(m->file, mux_data->track, "tkhd.flags", (TRACK_ENABLED | TRACK_IN_MOVIE));
+        }
+        else
+            /* Disable the other audio tracks so QuickTime doesn't play
+               them all at once. */
+        {
+            MP4SetTrackIntegerProperty(m->file, mux_data->track, "tkhd.flags", (TRACK_DISABLED | TRACK_IN_MOVIE));
+            hb_log("Disabled extra audio track %i", mux_data->track-1);
+        }
 
-               for( i = job->chapter_start - 1; i <= job->chapter_end - 1; i++ )
-               {
-                       chapter = hb_list_item( title->list_chapter, i );
-
-                       fOrigDuration = chapter->duration;
-                       fTimescale = job->arate;
-                       fTSDuration = (fOrigDuration / 90000) * fTimescale;
-                       chapterDuration = (MP4Duration)fTSDuration;
-                       
-                       sprintf(markerBuf, "  Chapter %03i", i + 1);
-                       markerBuf[0] = 0;
-                       markerBuf[1] = 11; // "Chapter xxx"
-                       MP4WriteSample(m->file, textTrack, (u_int8_t*)markerBuf, 13, chapterDuration, 0, true);
+    }
+
+       if (job->chapter_markers)
+    {
+        /* add a text track for the chapters. We add the 'chap' atom to track
+           one which is usually the video track & should never be disabled.
+           The Quicktime spec says it doesn't matter which media track the
+           chap atom is on but it has to be an enabled track. */
+        MP4TrackId textTrack;
+        textTrack = MP4AddChapterTextTrack(m->file, 1);
+
+        m->chapter_track = textTrack;
+        m->chapter_duration = 0;
+        m->current_chapter = job->chapter_start;
+    }
+
+    /* Add encoded-by metadata listing version and build date */
+    char *tool_string;
+    tool_string = (char *)malloc(80);
+    snprintf( tool_string, 80, "HandBrake %s %i", HB_VERSION, HB_BUILD);
+    MP4SetMetadataTool(m->file, tool_string);
+    free(tool_string);
 
-               }
-               
-       }
-       
     return 0;
 }
 
@@ -210,17 +388,87 @@ static int MP4Mux( hb_mux_object_t * m, hb_mux_data_t * mux_data,
                    hb_buffer_t * buf )
 {
     hb_job_t * job = m->job;
-
-    uint64_t duration;
+    int64_t duration;
+    int64_t offset = 0;
 
     if( mux_data == job->mux_data )
     {
         /* Video */
-        /* Because we use the audio samplerate as the timescale,
-           we have to use potentially variable durations so the video
-           doesn't go out of sync */
-        duration    = ( buf->stop * job->arate / 90000 ) - m->sum_dur;
+
+        // if there are b-frames compute the render offset
+        // (we'll need it for both the video frame & the chapter track)
+        if ( m->init_delay )
+        {
+            offset = ( buf->start + m->init_delay ) * m->samplerate / 90000 -
+                     m->sum_dur;
+        }
+        /* Add the sample before the new frame.
+           It is important that this be calculated prior to the duration
+           of the new video sample, as we want to sync to right after it.
+           (This is because of how durations for text tracks work in QT) */
+        if( job->chapter_markers && buf->new_chap )
+        {
+            struct hb_text_sample_s *sample;
+
+            // this chapter is postioned by writing out the previous chapter.
+            // the duration of the previous chapter is the duration up to but
+            // not including the current frame minus the duration of all
+            // chapters up to the previous.
+            duration = m->sum_dur - m->chapter_duration + offset;
+            if ( duration <= 0 )
+            {
+                /* The initial & final chapters can have very short durations
+                 * (less than the error in our total duration estimate) so
+                 * the duration calc above can result in a negative number.
+                 * when this happens give the chapter a short duration (1/3
+                 * of an ntsc frame time). */
+                duration = 1000 * m->samplerate / 90000;
+            }
+
+            sample = MP4GenerateChapterSample( m, duration, buf->new_chap );
+
+            if( !MP4WriteSample(m->file,
+                                m->chapter_track,
+                                sample->sample,
+                                sample->length,
+                                sample->duration,
+                                0, true) )
+            {
+                hb_error("Failed to write to output file, disk full?");
+                *job->die = 1;
+            }
+            free(sample);
+            m->current_chapter = buf->new_chap;
+            m->chapter_duration += duration;
+        }
+
+        // since we're changing the sample rate we need to keep track of
+        // the truncation bias so that the audio and video don't go out
+        // of sync. m->sum_dur_in is the sum of the input durations so far.
+        // m->sum_dur is the sum of the output durations. Their difference
+        // (in output sample rate units) is the accumulated truncation bias.
+        int64_t bias = ( m->sum_dur_in * m->samplerate / 90000 ) - m->sum_dur;
+        int64_t dur_in = buf->stop - buf->start;
+        duration = dur_in * m->samplerate / 90000 + bias;
+        if ( duration <= 0 )
+        {
+            /* We got an illegal mp4/h264 duration. This shouldn't
+               be possible and usually indicates a bug in the upstream code.
+               Complain in the hope that someone will go find the bug but
+               try to fix the error so that the file will still be playable. */
+            hb_log("MP4Mux: illegal duration %lld, bias %lld, start %lld (%lld),"
+                   "stop %lld (%lld), sum_dur %lld",
+                   duration, bias, buf->start * m->samplerate / 90000, buf->start,
+                   buf->stop * m->samplerate / 90000, buf->stop, m->sum_dur );
+            /* we don't know when the next frame starts so we can't pick a
+               valid duration for this one so we pick something "short"
+               (roughly 1/3 of an NTSC frame time) and rely on the bias calc
+               for the next frame to correct things (a duration underestimate
+               just results in a large bias on the next frame). */
+            duration = 1000 * m->samplerate / 90000;
+        }
         m->sum_dur += duration;
+        m->sum_dur_in += dur_in;
     }
     else
     {
@@ -228,75 +476,74 @@ static int MP4Mux( hb_mux_object_t * m, hb_mux_data_t * mux_data,
         duration = MP4_INVALID_DURATION;
     }
 
-    /* When we do get the first keyframe, use its duration as the
-       initial delay added to the frame order offset for b-frames.
-       Because of b-pyramid, double this duration when there are
-       b-pyramids, as denoted by job->areBframes equalling 2. */
-    if ((mux_data->track == 1) && (thisSample == 0) && (buf->key == 1) && (job->vcodec == HB_VCODEC_X264))
+    // Here's where the sample actually gets muxed.
+    if( !MP4WriteSample( m->file,
+                         mux_data->track,
+                         buf->data,
+                         buf->size,
+                         duration,
+                         offset,
+                         ((buf->frametype & HB_FRAME_KEY) != 0) ) )
     {
-        initDelay = buf->renderOffset;
-        thisSample++;
+        hb_error("Failed to write to output file, disk full?");
+        *job->die = 1;
     }
 
-    /* Here's where the sample actually gets muxed. 
-       If it's an audio sample, don't offset the sample's playback.
-       If it's a video sample and there are no b-frames, ditto.
-       If there are b-frames, offset by the initDelay plus the
-       difference between the presentation time stamp x264 gives
-       and the decoding time stamp from the buffer data. */
-       MP4WriteSample( m->file, mux_data->track, buf->data, buf->size,
-            duration, ((mux_data->track != 1) || (job->areBframes==0) || (job->vcodec != HB_VCODEC_X264)) ? 0 : (  buf->renderOffset * job->arate / 90000),
-            (buf->key == 1) );
-                                
     return 0;
 }
 
 static int MP4End( hb_mux_object_t * m )
 {
-#if 0
-    hb_job_t * job = m->job;
-    char filename[1024]; memset( filename, 0, 1024 );
-#endif
+    hb_job_t   * job   = m->job;
+
+    /* Write our final chapter marker */
+    if( m->job->chapter_markers )
+    {
+        int64_t duration = m->sum_dur - m->chapter_duration;
+        /* The final chapter can have a very short duration - if it's less
+         * than a second just skip it. */
+        if ( duration >= m->samplerate )
+        {
+
+            struct hb_text_sample_s *sample = MP4GenerateChapterSample( m, duration,
+                                                    m->current_chapter + 1 );
+            if( ! MP4WriteSample(m->file, m->chapter_track, sample->sample,
+                                 sample->length, sample->duration, 0, true) )
+            {
+                hb_error("Failed to write to output file, disk full?");
+                *job->die = 1;
+            }
+            free(sample);
+        }
+    }
 
-    hb_job_t * job = m->job;
-    
     if (job->areBframes)
-    /* Walk the entire video sample table and find the minumum ctts value. */
     {
-           MP4SampleId count = MP4GetTrackNumberOfSamples( m->file, 1);
-           MP4SampleId i;
-           MP4Duration renderingOffset = 2000000000, tmp;
-           
-           // Find the smallest rendering offset
-           for(i = 1; i <= count; i++)
-           {
-               tmp = MP4GetSampleRenderingOffset(m->file, 1, i);
-               if(tmp < renderingOffset)
-                   renderingOffset = tmp;
-           }
-           
-           // Adjust all ctts values down by renderingOffset
-           for(i = 1; i <= count; i++)
-           {
-               MP4SetSampleRenderingOffset(m->file,1,i,
-                   MP4GetSampleRenderingOffset(m->file,1,i) - renderingOffset);
-           }
-           
            // Insert track edit to get A/V back in sync.  The edit amount is
-           // the rendering offset of the first sample.
-           MP4AddTrackEdit(m->file, 1, MP4_INVALID_EDIT_ID, MP4GetSampleRenderingOffset(m->file,1,1),
-               MP4GetTrackDuration(m->file, 1), 0);
+           // the init_delay.
+           int64_t edit_amt = m->init_delay * m->samplerate / 90000;
+           MP4AddTrackEdit(m->file, 1, MP4_INVALID_EDIT_ID, edit_amt,
+                           MP4GetTrackDuration(m->file, 1), 0);
+            if ( m->job->chapter_markers )
+            {
+                // apply same edit to chapter track to keep it in sync with video
+                MP4AddTrackEdit(m->file, m->chapter_track, MP4_INVALID_EDIT_ID,
+                                edit_amt,
+                                MP4GetTrackDuration(m->file, m->chapter_track), 0);
+            }
      }
 
     MP4Close( m->file );
 
-#if 0
-    hb_log( "muxmp4: optimizing file" );
-    snprintf( filename, 1024, "%s.tmp", job->file );
-    MP4Optimize( job->file, filename, MP4_DETAILS_ERROR );
-    remove( job->file );
-    rename( filename, job->file );
-#endif
+    if ( job->mp4_optimize )
+    {
+        hb_log( "muxmp4: optimizing file" );
+        char filename[1024]; memset( filename, 0, 1024 );
+        snprintf( filename, 1024, "%s.tmp", job->file );
+        MP4Optimize( job->file, filename, MP4_DETAILS_ERROR );
+        remove( job->file );
+        rename( filename, job->file );
+    }
 
     return 0;
 }