OSDN Git Service

Don't discard titles during scan just because of a read failure on one or more of...
[handbrake-jp/handbrake-jp-git.git] / libhb / muxmp4.c
index 21aea7a..4f7c4eb 100644 (file)
@@ -4,15 +4,16 @@
    Homepage: <http://handbrake.m0k.org/>.
    It may be used under the terms of the GNU General Public License. */
 
-#include <ffmpeg/avformat.h>
+/* libmp4v2 header */
+#include "mp4.h"
 
 #include "hb.h"
 
-int64_t ff_gcd(int64_t a, int64_t b);
-static inline int ff_get_fourcc(const char *s)
-{
-    return (s[0]) + (s[1]<<8) + (s[2]<<16) + (s[3]<<24);
-}
+void AddIPodUUID(MP4FileHandle, MP4TrackId);
+
+/* B-frame muxing variables */
+MP4SampleId thisSample = 0;
+uint64_t initDelay;
 
 struct hb_mux_object_s
 {
@@ -20,15 +21,109 @@ struct hb_mux_object_s
 
     hb_job_t * job;
 
-    AVFormatContext * format;
+    /* libmp4v2 handle */
+    MP4FileHandle file;
+
+    /* Cumulated durations so far, in timescale units (see MP4Mux) */
+    uint64_t sum_dur;
+       
+    /* Chapter state information for muxing */
+    MP4TrackId chapter_track;
+    int current_chapter;
+    uint64_t chapter_duration;
 };
 
 struct hb_mux_data_s
 {
-    int track;
+    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 = m->current_chapter;
+    hb_chapter_t *chapter_data = hb_list_item( m->job->title->list_chapter, chapter - 1 );
+    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 );
+        string = tmp_buffer;
+    }
+    
+    return MP4CreateTextSample( string, duration );
+}
+
+/**********************************************************************
  * MP4Init
  **********************************************************************
  * Allocates hb_mux_data_t structures, create file and write headers
@@ -39,141 +134,358 @@ static int MP4Init( hb_mux_object_t * m )
     hb_title_t * title = job->title;
     
     hb_audio_t    * audio;
+    hb_mux_data_t * mux_data;
     int i;
-    AVFormatContext * oc;
-    AVStream *st;
-    AVFormatParameters params;
-
-    register_protocol(&file_protocol);
-    movenc_init();
-
-    oc = av_alloc_format_context();
+    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( job->mux & HB_MUX_PSP )
+    /* Create an empty mp4 file */
+    if (job->largeFileSize)
+    /* Use 64-bit MP4 file */
     {
-        oc->oformat = guess_format( "psp", NULL, NULL );
+        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 */
     {
-        oc->oformat = guess_format( "mp4", NULL, NULL );
+        m->file = MP4Create( job->file, MP4_DETAILS_ERROR, 0 );
     }
-    if( !oc->oformat )
+    
+    if (m->file == MP4_INVALID_FILE_HANDLE)
     {
-        hb_log( "guess_format failed" );
-        return 1;
+        hb_error("muxmp4.c: MP4Create failed!");
+        *job->die = 1;
+        return 0;
     }
-    snprintf( oc->filename, sizeof( oc->filename ),
-              "%s", job->file );
 
-    st = av_new_stream( oc, oc->nb_streams );
-    if( !st )
+    /* Video track */
+    mux_data      = malloc( sizeof( hb_mux_data_t ) );
+    job->mux_data = mux_data;
+
+    /* When using the standard 90000 timescale, QuickTime tends to have
+       synchronization issues (audio not playing at the correct speed).
+       To workaround this, we use the audio samplerate as the
+       timescale */
+    if (!(MP4SetTimeScale( m->file, job->arate )))
     {
-        hb_log( "av_new_stream failed" );
-        return 1;
+        hb_error("muxmp4.c: MP4SetTimeScale failed!");
+        *job->die = 1;
+        return 0;
     }
-    st->stream_copy = 1;
-    st->codec->flags |= CODEC_FLAG_GLOBAL_HEADER;
-    st->codec->codec_type = CODEC_TYPE_VIDEO;
-    st->codec->codec_id = ( job->vcodec == HB_VCODEC_X264 ) ?
-                            CODEC_ID_H264 : CODEC_ID_MPEG4;
-    st->codec->extradata= job->config.mpeg4.bytes;
-    st->codec->extradata_size= job->config.mpeg4.length;
-    st->codec->bit_rate = 1000 * job->vbitrate;
-    i = ff_gcd( job->vrate_base, job->vrate );
-    st->codec->time_base = (AVRational){ job->vrate_base / i, job->vrate / i };
-
-    st->codec->pix_fmt = PIX_FMT_YUV420P;
-    st->codec->width = job->width;
-    st->codec->height = job->height;
-    st->codec->has_b_frames = 0;
-
-    job->mux_data = malloc( sizeof( hb_mux_data_t ) );
-    job->mux_data->track = 0;
 
-    for( i = 0; i < hb_list_count( title->list_audio ); i++ )
+    if( job->vcodec == HB_VCODEC_X264 )
     {
-        audio = hb_list_item( title->list_audio, i );
+        /* Stolen from mp4creator */
+        if(!(MP4SetVideoProfileLevel( m->file, 0x7F )))
+        {
+            hb_error("muxmp4.c: MP4SetVideoProfileLevel failed!");
+            *job->die = 1;
+            return 0;
+        }
 
-        audio->mux_data = malloc( sizeof( hb_mux_data_t ) );
-        audio->mux_data->track = i + 1;
+               mux_data->track = MP4AddH264VideoTrack( m->file, job->arate,
+                       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)
+               {
+                       hb_log("About to add iPod atom");
+                       AddIPodUUID(m->file, mux_data->track);
+               }
 
-        st = av_new_stream( oc, oc->nb_streams );
-        if( !st )
+    }
+    else /* FFmpeg or XviD */
+    {
+        if(!(MP4SetVideoProfileLevel( m->file, MPEG4_SP_L3 )))
         {
-            hb_log( "av_new_stream failed" );
-            return 1;
+            hb_error("muxmp4.c: MP4SetVideoProfileLevel failed!");
+            *job->die = 1;
+            return 0;
         }
-        st->stream_copy = 1;
-        st->codec->flags |= CODEC_FLAG_GLOBAL_HEADER;
-        st->codec->codec_type = CODEC_TYPE_AUDIO;
-        st->codec->codec_id = CODEC_ID_AAC;
-        st->codec->bit_rate = 1000 * job->abitrate;
-        st->codec->extradata= audio->config.aac.bytes;
-        st->codec->extradata_size= audio->config.aac.length;
-        st->codec->time_base = (AVRational){ 1, job->arate };
-        st->codec->channels = 2;
-        st->codec->sample_rate = job->arate;
-        st->codec->frame_size = 1024;
-        st->codec->block_align = 0;
-    }
+        mux_data->track = MP4AddVideoTrack( m->file, job->arate,
+                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;
+        }
+        
 
-    oc->timestamp = 0;
-    if( url_fopen( &oc->pb, job->file, URL_WRONLY ) < 0 )
-    {
-        hb_log( "url_fopen failed (%s)", job->file );
-        return 1;
+        /* VOL from FFmpeg or XviD */
+        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;
+        }
     }
 
-    memset( &params, 0, sizeof( params ) );
-    if( av_set_parameters( oc, &params ) < 0 )
-    {
-        hb_log( "av_set_parameters failed" );
-        return 1;
-    }
+       /* 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;
 
-    oc->packet_size= 0;
-    oc->mux_rate= 0;
-    oc->preload= (int)(0.5*AV_TIME_BASE);
-    oc->max_delay= (int)(0.7*AV_TIME_BASE);
-    oc->loop_output = AVFMT_NOOUTPUTLOOP;
+                       uint32_t widthRatioInt;
+                       widthRatioInt = (uint32_t)widthRatio;
 
-    if( av_write_header( oc ) < 0 )
+#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(!MP4SetBytesProperty(m->file, "moov.trak.tkhd.reserved3", nval, size)) {
+                               hb_log("Problem setting transform matrix");
+                       }
+                       
+               }
+
+       }
+
+       /* end of transformation matrix */
+
+       /* firstAudioTrack will be used to reference the first audio track when we add a chapter track */
+       MP4TrackId firstAudioTrack = 0;
+
+       /* add the audio tracks */
+    for( i = 0; i < hb_list_count( title->list_audio ); i++ )
     {
-        hb_log( "av_write_header failed" );
-        return 1;
-    }
+       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 );
+                
+        /* 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;
+        MP4SetTrackIntegerProperty(m->file, mux_data->track, "mdia.mdhd.language", language_code);
+        
+        /* Set the correct number of channels for this track */
+        reserved2[9] = (u_int8_t)HB_AMIXDOWN_GET_DISCRETE_CHANNEL_COUNT(audio->amixdown);
+        MP4SetTrackBytesProperty(m->file, mux_data->track, "mdia.minf.stbl.stsd.mp4a.reserved2", reserved2, sizeof(reserved2));
+        
+        /* 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;
+            
+            /* Enable the first audio track */
+            MP4SetTrackIntegerProperty(m->file, mux_data->track, "tkhd.flags", (TRACK_ENABLED | TRACK_IN_MOVIE));
+        }
 
-    m->format = oc;
+        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);
+        }
+               
+    }
 
+       if (job->chapter_markers) 
+    {
+               /* add a text track for the chapters */
+               MP4TrackId textTrack;
+               textTrack = MP4AddChapterTextTrack(m->file, firstAudioTrack);
+        
+        m->chapter_track = textTrack;
+        m->chapter_duration = 0;
+        m->current_chapter = job->chapter_start;
+       }
+       
     return 0;
 }
 
 static int MP4Mux( hb_mux_object_t * m, hb_mux_data_t * mux_data,
                    hb_buffer_t * buf )
 {
-    AVPacket pkt;
-    av_init_packet(&pkt);
+    hb_job_t * job = m->job;
 
-    pkt.stream_index = mux_data->track;
-    pkt.data         = buf->data;
-    pkt.size         = buf->size;
-    pkt.pts          = buf->start;
+    uint64_t duration;
 
-    if( buf->key )
+    if( mux_data == job->mux_data )
+    {    
+        /* 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 = MP4GenerateChapterSample( m, (m->sum_dur - m->chapter_duration) );
+            
+            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++;
+            m->chapter_duration = m->sum_dur;
+        }
+    
+        /* 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;
+        m->sum_dur += duration;
+    }
+    else
     {
-        pkt.flags |= PKT_FLAG_KEY;
+        /* Audio */
+        duration = MP4_INVALID_DURATION;
     }
 
-    av_interleaved_write_frame( m->format, &pkt );
+    /* 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->frametype & HB_FRAME_KEY) && (job->vcodec == HB_VCODEC_X264))
+    {
+        initDelay = buf->renderOffset;
+        thisSample++;
+    }
 
+    /* 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. */
+    if( !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->frametype & HB_FRAME_KEY) != 0) ) )
+    {
+        hb_error("Failed to write to output file, disk full?");   
+        *job->die = 1;
+    }
+                                
     return 0;
 }
 
 static int MP4End( hb_mux_object_t * m )
-{
-    av_write_trailer( m->format );
-    url_fclose( &m->format->pb );
-    av_free( m->format );
+{ 
+    hb_job_t   * job   = m->job;
+
+    /* Write our final chapter marker */
+    if( m->job->chapter_markers )
+    {
+        struct hb_text_sample_s *sample = MP4GenerateChapterSample( m, (m->sum_dur - m->chapter_duration) );
+    
+        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);
+    }
+    
+#if 0
+    hb_job_t * job = m->job;
+    char filename[1024]; memset( filename, 0, 1024 );
+#endif
+
+    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);
+     }
+
+    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
 
     return 0;
 }