/* muxmp4.c Copyright (c) 2003-2013 HandBrake Team This file is part of the HandBrake source code Homepage: . It may be used under the terms of the GNU General Public License v2. For full terms see the file COPYING file or visit http://www.gnu.org/licenses/gpl-2.0.html */ #include "hb.h" #if defined(USE_MP4V2) #include "mp4v2/mp4v2.h" #include "a52dec/a52.h" struct hb_mux_object_s { HB_MUX_COMMON; hb_job_t * job; /* output file name in current code page */ char * path; /* libmp4v2 handle */ MP4FileHandle file; int64_t sum_dur; // sum of video frame durations so far hb_buffer_t *delay_buf; /* Chapter state information for muxing */ MP4TrackId chapter_track; int current_chapter; uint64_t chapter_duration; }; struct hb_mux_data_s { MP4TrackId track; uint8_t subtitle; int sub_format; uint64_t sum_dur; // sum of the frame durations so far // audio frame duration info int sample_rate; int samples_per_frame; }; /* Tune video track chunk duration. * libmp4v2 default duration == dusamplerate == 1 second. * Per van's suggestion we desire duration == 4 frames. * Should be invoked immediately after track creation. * * return true on fail, false on success. */ static int MP4TuneTrackDurationPerChunk( hb_mux_object_t* m, MP4TrackId trackId ) { uint32_t tscale; MP4Duration dur; tscale = MP4GetTrackTimeScale( m->file, trackId ); dur = (MP4Duration)ceil( (double)tscale * (double)m->job->vrate_base / (double)m->job->vrate * 4.0 ); if( !MP4SetTrackDurationPerChunk( m->file, trackId, dur )) { hb_error( "muxmp4.c: MP4SetTrackDurationPerChunk failed!" ); *m->job->die = 1; return 0; } hb_deep_log( 2, "muxmp4: track %u, chunk duration %"PRIu64, MP4FindTrackIndex( m->file, trackId ), dur ); return 1; } static const uint16_t ac3_sample_rate_tab[3] = { 48000, 44100, 32000 }; /* possible bitrates */ static const uint16_t ac3_bitrate_tab[19] = { 32, 40, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320, 384, 448, 512, 576, 640 }; /********************************************************************** * MP4Init ********************************************************************** * Allocates hb_mux_data_t structures, create file and write headers *********************************************************************/ 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; int subtitle_default; /* Flags for enabling/disabling tracks in an MP4. */ enum { TRACK_DISABLED = 0x0, TRACK_ENABLED = 0x1, TRACK_IN_MOVIE = 0x2, TRACK_IN_PREVIEW = 0x4, TRACK_IN_POSTER = 0x8 }; m->path = hb_utf8_to_cp(job->file); if (m->path == NULL) { hb_error("Could not convert string, out of memory?"); *job->die = 1; return 0; } /* Create an empty mp4 file */ if (job->largeFileSize) /* Use 64-bit MP4 file */ { m->file = MP4Create(m->path, MP4_DETAILS_ERROR, MP4_CREATE_64BIT_DATA); hb_deep_log( 2, "muxmp4: using 64-bit MP4 formatting."); } else /* Limit MP4s to less than 4 GB */ { m->file = MP4Create(m->path, 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 = calloc(1, sizeof( hb_mux_data_t ) ); job->mux_data = mux_data; if (!(MP4SetTimeScale( m->file, 90000 ))) { hb_error("muxmp4.c: MP4SetTimeScale failed!"); *job->die = 1; return 0; } if (job->vcodec & HB_VCODEC_H264_MASK) { /* Stolen from mp4creator */ MP4SetVideoProfileLevel( m->file, 0x7F ); mux_data->track = MP4AddH264VideoTrack( m->file, 90000, 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 */ if ( mux_data->track == MP4_INVALID_TRACK_ID ) { hb_error( "muxmp4.c: MP4AddH264VideoTrack failed!" ); *job->die = 1; return 0; } /* Tune track chunk duration */ if( !MP4TuneTrackDurationPerChunk( m, mux_data->track )) { return 0; } 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->ipod_atom ) { hb_deep_log( 2, "muxmp4: adding iPod atom"); MP4AddIPodUUID(m->file, mux_data->track); } } else if ( job->vcodec == HB_VCODEC_FFMPEG_MPEG4 ) /* FFmpeg MPEG-4 */ { MP4SetVideoProfileLevel( m->file, MPEG4_SP_L3 ); mux_data->track = MP4AddVideoTrack( m->file, 90000, 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; } /* Tune track chunk duration */ if( !MP4TuneTrackDurationPerChunk( m, mux_data->track )) { return 0; } /* 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; } } else if ( job->vcodec == HB_VCODEC_FFMPEG_MPEG2 ) /* FFmpeg MPEG-2 */ { mux_data->track = MP4AddVideoTrack( m->file, 90000, MP4_INVALID_DURATION, job->width, job->height, MP4_MPEG2_VIDEO_TYPE ); if (mux_data->track == MP4_INVALID_TRACK_ID) { hb_error("muxmp4.c: MP4AddVideoTrack failed!"); *job->die = 1; return 0; } /* Tune track chunk duration */ if( !MP4TuneTrackDurationPerChunk( m, mux_data->track )) { return 0; } /* VOL from FFmpeg */ 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; } } else { hb_error("muxmp4.c: Unsupported video encoder!"); } /* 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 * The user can set it from job->color_matrix_code. */ if( job->color_matrix_code == 4 ) { // Custom MP4AddColr(m->file, mux_data->track, job->color_prim, job->color_transfer, job->color_matrix); } else if( job->color_matrix_code == 3 ) { // ITU BT.709 HD content MP4AddColr(m->file, mux_data->track, HB_COLR_PRI_BT709, HB_COLR_TRA_BT709, HB_COLR_MAT_BT709); } else if( job->color_matrix_code == 2 ) { // ITU BT.601 DVD or SD TV content (PAL) MP4AddColr(m->file, mux_data->track, HB_COLR_PRI_EBUTECH, HB_COLR_TRA_BT709, HB_COLR_MAT_SMPTE170M); } else if( job->color_matrix_code == 1 ) { // ITU BT.601 DVD or SD TV content (NTSC) MP4AddColr(m->file, mux_data->track, HB_COLR_PRI_SMPTEC, HB_COLR_TRA_BT709, HB_COLR_MAT_SMPTE170M); } else { // detected during scan MP4AddColr(m->file, mux_data->track, title->color_prim, title->color_transfer, title->color_matrix); } if( job->anamorphic.mode ) { /* PASP atom for anamorphic video */ float width, height; width = job->anamorphic.par_width; height = job->anamorphic.par_height; MP4AddPixelAspectRatio(m->file, mux_data->track, (uint32_t)width, (uint32_t)height); MP4SetTrackFloatProperty(m->file, mux_data->track, "tkhd.width", job->width * (width / height)); } /* add the audio tracks */ for( i = 0; i < hb_list_count( job->list_audio ); i++ ) { audio = hb_list_item( job->list_audio, i ); mux_data = calloc(1, sizeof( hb_mux_data_t ) ); audio->priv.mux_data = mux_data; switch ( audio->config.out.codec & HB_ACODEC_MASK ) { case HB_ACODEC_AC3: { int ii, jj; int bitrate; int sr_shift, sr_code; uint8_t bsid; uint8_t bsmod; uint8_t acmod; uint8_t lfeon; uint8_t br_code = 0; if ( audio->config.out.codec & HB_ACODEC_PASS_FLAG ) { bsmod = audio->config.in.mode; acmod = audio->config.in.flags & 0x7; lfeon = !!(audio->config.in.flags & A52_LFE); bitrate = audio->config.in.bitrate; mux_data->sample_rate = audio->config.in.samplerate; mux_data->samples_per_frame = audio->config.in.samples_per_frame; } else { bsmod = 0; bitrate = audio->config.out.bitrate * 1000; mux_data->sample_rate = audio->config.out.samplerate; mux_data->samples_per_frame = audio->config.out.samples_per_frame; switch (audio->config.out.mixdown) { case HB_AMIXDOWN_MONO: acmod = 1; lfeon = 0; break; case HB_AMIXDOWN_STEREO: case HB_AMIXDOWN_DOLBY: case HB_AMIXDOWN_DOLBYPLII: acmod = 2; lfeon = 0; break; case HB_AMIXDOWN_5POINT1: acmod = 7; lfeon = 1; break; default: hb_log("MP4Init: bad mixdown"); acmod = 2; lfeon = 0; break; } } for (ii = 0; ii < 3; ii++) { for (jj = 0; jj < 3; jj++) { if ((ac3_sample_rate_tab[jj] >> ii) == mux_data->sample_rate) { goto rate_found1; } } } hb_error("Unknown AC3 samplerate"); ii = jj = 0; rate_found1: sr_shift = ii; sr_code = jj; bsid = 8 + ii; for (ii = 0; ii < 19; ii++) { if ((ac3_bitrate_tab[ii] >> sr_shift)*1000 == bitrate) break; } if ( ii >= 19 ) { hb_error("Unknown AC3 bitrate"); ii = 0; } br_code = ii; mux_data->track = MP4AddAC3AudioTrack(m->file, mux_data->sample_rate, sr_code, bsid, bsmod, acmod, lfeon, br_code); /* Tune track chunk duration */ MP4TuneTrackDurationPerChunk( m, mux_data->track ); if (audio->config.out.name == NULL) { MP4SetTrackBytesProperty( m->file, mux_data->track, "udta.name.value", (const uint8_t*)"Surround", strlen("Surround")); } else { MP4SetTrackBytesProperty( m->file, mux_data->track, "udta.name.value", (const uint8_t*)(audio->config.out.name), strlen(audio->config.out.name)); } } break; case HB_ACODEC_FAAC: case HB_ACODEC_FFAAC: case HB_ACODEC_CA_AAC: case HB_ACODEC_CA_HAAC: case HB_ACODEC_FDK_AAC: case HB_ACODEC_FDK_HAAC: case HB_ACODEC_LAME: case HB_ACODEC_MP3: case HB_ACODEC_DCA_HD: case HB_ACODEC_DCA: { uint8_t audio_type = MP4_MPEG4_AUDIO_TYPE; int channels, config_len = 0; uint8_t *config_bytes = NULL; switch ( audio->config.out.codec & HB_ACODEC_MASK ) { case HB_ACODEC_FAAC: case HB_ACODEC_FFAAC: case HB_ACODEC_CA_AAC: case HB_ACODEC_CA_HAAC: case HB_ACODEC_FDK_AAC: case HB_ACODEC_FDK_HAAC: { audio_type = MP4_MPEG4_AUDIO_TYPE; config_bytes = audio->priv.config.extradata.bytes; config_len = audio->priv.config.extradata.length; } break; case HB_ACODEC_LAME: case HB_ACODEC_MP3: { audio_type = MP4_MPEG2_AUDIO_TYPE; } break; case HB_ACODEC_DCA: case HB_ACODEC_DCA_HD: { audio_type = 0xA9; } break; } if (audio->config.out.codec & HB_ACODEC_PASS_FLAG) { mux_data->sample_rate = audio->config.in.samplerate; mux_data->samples_per_frame = audio->config.in.samples_per_frame; channels = av_get_channel_layout_nb_channels(audio->config.in.channel_layout); } else { mux_data->sample_rate = audio->config.out.samplerate; mux_data->samples_per_frame = audio->config.out.samples_per_frame; channels = hb_mixdown_get_discrete_channel_count(audio->config.out.mixdown); } mux_data->track = MP4AddAudioTrack(m->file, mux_data->sample_rate, // fixed frame duration, if applicable mux_data->samples_per_frame > 0 ? mux_data->samples_per_frame : MP4_INVALID_DURATION, audio_type); /* Tune track chunk duration */ MP4TuneTrackDurationPerChunk( m, mux_data->track ); if (audio->config.out.name == NULL) { MP4SetTrackBytesProperty( m->file, mux_data->track, "udta.name.value", (const uint8_t*)"Stereo", strlen("Stereo")); } else { MP4SetTrackBytesProperty( m->file, mux_data->track, "udta.name.value", (const uint8_t*)(audio->config.out.name), strlen(audio->config.out.name)); } MP4SetAudioProfileLevel( m->file, 0x0F ); if ( config_bytes ) { MP4SetTrackESConfiguration( m->file, mux_data->track, config_bytes, config_len ); } /* Set the correct number of channels for this track */ MP4SetTrackIntegerProperty(m->file, mux_data->track, "mdia.minf.stbl.stsd.*.channels", channels); } break; default: { hb_log("MP4Mux: Unsupported audio codec %x", audio->config.out.codec); } break; } /* Set the language for this track */ MP4SetTrackLanguage(m->file, mux_data->track, audio->config.lang.iso639_2); if( hb_list_count( job->list_audio ) > 1 ) { /* Set the audio track alternate group */ MP4SetTrackIntegerProperty(m->file, mux_data->track, "tkhd.alternate_group", 1); } 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_deep_log( 2, "muxmp4: disabled extra audio track %u", MP4FindTrackIndex( m->file, mux_data->track )); } } // Quicktime requires that at least one subtitle is enabled, // else it doesn't show any of the subtitles. // So check to see if any of the subtitles are flagged to be // the defualt. The default will the the enabled track, else // enable the first track. subtitle_default = 0; for( i = 0; i < hb_list_count( job->list_subtitle ); i++ ) { hb_subtitle_t *subtitle = hb_list_item( job->list_subtitle, i ); if( subtitle && subtitle->format == TEXTSUB && subtitle->config.dest == PASSTHRUSUB ) { if ( subtitle->config.default_track ) subtitle_default = 1; } } for( i = 0; i < hb_list_count( job->list_subtitle ); i++ ) { hb_subtitle_t *subtitle = hb_list_item( job->list_subtitle, i ); if( subtitle && subtitle->format == TEXTSUB && subtitle->config.dest == PASSTHRUSUB ) { uint64_t width, height = 60; if( job->anamorphic.mode ) width = job->width * ( (float) job->anamorphic.par_width / job->anamorphic.par_height ); else width = job->width; mux_data = calloc(1, sizeof( hb_mux_data_t ) ); subtitle->mux_data = mux_data; mux_data->subtitle = 1; mux_data->sub_format = subtitle->format; mux_data->track = MP4AddSubtitleTrack( m->file, 90000, width, height ); MP4SetTrackLanguage(m->file, mux_data->track, subtitle->iso639_2); /* Tune track chunk duration */ MP4TuneTrackDurationPerChunk( m, mux_data->track ); const uint8_t textColor[4] = { 255,255,255,255 }; MP4SetTrackIntegerProperty(m->file, mux_data->track, "tkhd.alternate_group", 2); MP4SetTrackIntegerProperty(m->file, mux_data->track, "mdia.minf.stbl.stsd.tx3g.dataReferenceIndex", 1); MP4SetTrackIntegerProperty(m->file, mux_data->track, "mdia.minf.stbl.stsd.tx3g.horizontalJustification", 1); MP4SetTrackIntegerProperty(m->file, mux_data->track, "mdia.minf.stbl.stsd.tx3g.verticalJustification", 255); MP4SetTrackIntegerProperty(m->file, mux_data->track, "mdia.minf.stbl.stsd.tx3g.bgColorAlpha", 255); MP4SetTrackIntegerProperty(m->file, mux_data->track, "mdia.minf.stbl.stsd.tx3g.defTextBoxBottom", height); MP4SetTrackIntegerProperty(m->file, mux_data->track, "mdia.minf.stbl.stsd.tx3g.defTextBoxRight", width); MP4SetTrackIntegerProperty(m->file, mux_data->track, "mdia.minf.stbl.stsd.tx3g.fontID", 1); MP4SetTrackIntegerProperty(m->file, mux_data->track, "mdia.minf.stbl.stsd.tx3g.fontSize", 24); MP4SetTrackIntegerProperty(m->file, mux_data->track, "mdia.minf.stbl.stsd.tx3g.fontColorRed", textColor[0]); MP4SetTrackIntegerProperty(m->file, mux_data->track, "mdia.minf.stbl.stsd.tx3g.fontColorGreen", textColor[1]); MP4SetTrackIntegerProperty(m->file, mux_data->track, "mdia.minf.stbl.stsd.tx3g.fontColorBlue", textColor[2]); MP4SetTrackIntegerProperty(m->file, mux_data->track, "mdia.minf.stbl.stsd.tx3g.fontColorAlpha", textColor[3]); /* translate the track */ uint8_t* val; uint8_t nval[36]; uint32_t *ptr32 = (uint32_t*) nval; uint32_t size; MP4GetTrackBytesProperty(m->file, mux_data->track, "tkhd.matrix", &val, &size); memcpy(nval, val, size); const uint32_t ytranslation = (job->height - height) * 0x10000; #ifdef WORDS_BIGENDIAN ptr32[7] = ytranslation; #else /* we need to switch the endianness, as the file format expects big endian */ ptr32[7] = ((ytranslation & 0x000000FF) << 24) + ((ytranslation & 0x0000FF00) << 8) + ((ytranslation & 0x00FF0000) >> 8) + ((ytranslation & 0xFF000000) >> 24); #endif MP4SetTrackBytesProperty(m->file, mux_data->track, "tkhd.matrix", nval, size); if ( !subtitle_default || subtitle->config.default_track ) { /* Enable the default subtitle track */ MP4SetTrackIntegerProperty(m->file, mux_data->track, "tkhd.flags", (TRACK_ENABLED | TRACK_IN_MOVIE)); subtitle_default = 1; } else { MP4SetTrackIntegerProperty(m->file, mux_data->track, "tkhd.flags", (TRACK_DISABLED | TRACK_IN_MOVIE)); } } else if( subtitle && subtitle->format == PICTURESUB && subtitle->config.dest == PASSTHRUSUB ) { mux_data = calloc(1, sizeof( hb_mux_data_t ) ); subtitle->mux_data = mux_data; mux_data->subtitle = 1; mux_data->sub_format = subtitle->format; mux_data->track = MP4AddSubpicTrack( m->file, 90000, subtitle->width, subtitle->height ); MP4SetTrackLanguage(m->file, mux_data->track, subtitle->iso639_2); /* Tune track chunk duration */ MP4TuneTrackDurationPerChunk( m, mux_data->track ); uint8_t palette[16][4]; int ii; for ( ii = 0; ii < 16; ii++ ) { palette[ii][0] = 0; palette[ii][1] = (subtitle->palette[ii] >> 16) & 0xff; palette[ii][2] = (subtitle->palette[ii] >> 8) & 0xff; palette[ii][3] = (subtitle->palette[ii]) & 0xff; } if (!(MP4SetTrackESConfiguration( m->file, mux_data->track, (uint8_t*)palette, 16 * 4 ))) { hb_error("muxmp4.c: MP4SetTrackESConfiguration failed!"); *job->die = 1; return 0; } if ( !subtitle_default || subtitle->config.default_track ) { /* Enable the default subtitle track */ MP4SetTrackIntegerProperty(m->file, mux_data->track, "tkhd.flags", (TRACK_ENABLED | TRACK_IN_MOVIE)); subtitle_default = 1; } else { MP4SetTrackIntegerProperty(m->file, mux_data->track, "tkhd.flags", (TRACK_DISABLED | TRACK_IN_MOVIE)); } } } 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, 0); 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_PROJECT_VERSION, HB_PROJECT_BUILD); /* allocate,fetch,populate,store,free tags structure */ const MP4Tags* tags; tags = MP4TagsAlloc(); MP4TagsFetch( tags, m->file ); MP4TagsSetEncodingTool( tags, tool_string ); MP4TagsStore( tags, m->file ); MP4TagsFree( tags ); free(tool_string); return 0; } static int MP4Mux( hb_mux_object_t * m, hb_mux_data_t * mux_data, hb_buffer_t * buf ) { hb_job_t * job = m->job; int64_t duration, stop = -1; int64_t offset = 0; hb_buffer_t *tmp; if (buf != NULL) { if (buf->s.duration >= 0) { stop = buf->s.start + buf->s.duration; } else if (mux_data->subtitle) { buf->s.duration = 10 * 90000; stop = buf->s.start + buf->s.duration; } } if( mux_data == job->mux_data ) { /* Video */ if ((job->vcodec & HB_VCODEC_H264_MASK) || (job->vcodec & HB_VCODEC_FFMPEG_MASK)) { if ( buf && buf->s.start < buf->s.renderOffset ) { hb_log("MP4Mux: PTS %"PRId64" < DTS %"PRId64, buf->s.start, buf->s.renderOffset ); buf->s.renderOffset = buf->s.start; } } // We delay muxing video by one frame so that we can calculate // the dts to dts duration of the frames. tmp = buf; buf = m->delay_buf; m->delay_buf = tmp; if ( !buf ) return 0; stop = buf->s.start + buf->s.duration; if ((job->vcodec & HB_VCODEC_H264_MASK) || (job->vcodec & HB_VCODEC_FFMPEG_MASK)) { // x264 supplies us with DTS, so offset is PTS - DTS offset = buf->s.start - buf->s.renderOffset; } /* 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->s.new_chap ) { hb_chapter_t *chapter = NULL; // 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. // The initial and final chapters can be very short (a second or // less) since they're not really chapters but just a placeholder to // insert a cell command. We don't write chapters shorter than 1.5 sec. duration = m->sum_dur - m->chapter_duration + offset; if ( duration >= (90000*3)/2 ) { chapter = hb_list_item( m->job->list_chapter, buf->s.new_chap - 2 ); MP4AddChapter( m->file, m->chapter_track, duration, (chapter != NULL) ? chapter->title : NULL); m->current_chapter = buf->s.new_chap; m->chapter_duration += duration; } } if ((job->vcodec & HB_VCODEC_H264_MASK) || (job->vcodec & HB_VCODEC_FFMPEG_MASK)) { // x264 supplies us with DTS if ( m->delay_buf ) { duration = m->delay_buf->s.renderOffset - buf->s.renderOffset; } else { duration = stop - m->sum_dur; // Due to how libx264 generates DTS, it's possible for the // above calculation to be negative. // // x264 generates DTS by rearranging PTS in this sequence: // pts0 - delay, pts1 - delay, pts2 - delay, pts1, pts2, pts3... // // where delay == pts2. This guarantees that DTS <= PTS for // any frame, but also generates this sequence of durations: // d0 + d1 + d0 + d1 + d2 + d3 ... + d(N-2) // // so the sum up to the last frame is: // sum_dur = d0 + d1 + d0 + d1 + d2 + d3 ... + d(N-3) // // while the original total duration of the video was: // duration = d0 + d1 + d2 + d3 ... + d(N) // // Note that if d0 + d1 != d(N-1) + d(N), the total // length of the video changes since d(N-1) and d(N) are // replaced by d0 and d1 in the final duration sum. // // To keep the total length of the video the same as the source // we try to make // d(N-2) = duration - sum_dur // // But if d0 + d1 >= d(N-1) + d(N), the above calculation // results in a nagative value and we need to fix it. if ( duration <= 0 ) duration = 90000. / ((double)job->vrate / (double)job->vrate_base); } } else { // We're getting the frames in decode order but the timestamps are // for presentation so we have to use durations and effectively // compute a DTS. duration = buf->s.duration; } 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 %"PRId64", start %"PRId64"," "stop %"PRId64", sum_dur %"PRId64, duration, buf->s.start, stop, m->sum_dur ); /* we don't know when the next frame starts so we can't pick a valid duration for this one. we pick something "short" (roughly 1/3 of an NTSC frame time) to take time from the next frame. */ duration = 1000; } m->sum_dur += duration; } else { /* Audio */ if (mux_data->samples_per_frame > 0) // frame size is fixed and known duration = MP4_INVALID_DURATION; else // frame size has to be computed duration = buf->s.duration * mux_data->sample_rate / 90000; } /* Here's where the sample actually gets muxed. */ if (mux_data == job->mux_data && ((job->vcodec & HB_VCODEC_H264_MASK) || (job->vcodec & HB_VCODEC_FFMPEG_MASK))) { /* Compute dependency flags. * * This mechanism is (optionally) used by media players such as QuickTime * to offer better scrubbing performance. The most influential bits are * MP4_SDT_HAS_NO_DEPENDENTS and MP4_SDT_EARLIER_DISPLAY_TIMES_ALLOWED. * * Other bits are possible but no example media using such bits have been * found. * * It is acceptable to supply 0-bits for any samples which characteristics * cannot be positively guaranteed. */ int sync = 0; uint32_t dflags = 0; /* encoding layer signals if frame is referenced by other frames */ if( buf->s.flags & HB_FRAME_REF ) dflags |= MP4_SDT_HAS_DEPENDENTS; else dflags |= MP4_SDT_HAS_NO_DEPENDENTS; /* disposable */ switch( buf->s.frametype ) { case HB_FRAME_IDR: sync = 1; break; case HB_FRAME_I: dflags |= MP4_SDT_EARLIER_DISPLAY_TIMES_ALLOWED; break; case HB_FRAME_P: dflags |= MP4_SDT_EARLIER_DISPLAY_TIMES_ALLOWED; break; case HB_FRAME_BREF: case HB_FRAME_B: default: break; /* nothing to mark */ } if( !MP4WriteSampleDependency( m->file, mux_data->track, buf->data, buf->size, duration, offset, sync, dflags )) { hb_error("Failed to write to output file, disk full?"); *job->die = 1; } } else if (mux_data->subtitle) { if( mux_data->sub_format == TEXTSUB ) { /* MPEG4 timed text does not allow overlapping samples; upstream code should coalesce overlapping subtitle lines. */ if( buf->s.start < mux_data->sum_dur ) { if ( stop - mux_data->sum_dur > 90*500 ) { hb_log("MP4Mux: shortening overlapping subtitle, " "start %"PRId64", stop %"PRId64", sum_dur %"PRId64, buf->s.start, stop, m->sum_dur); buf->s.start = mux_data->sum_dur; } } if( buf->s.start < mux_data->sum_dur ) { hb_log("MP4Mux: skipping overlapping subtitle, " "start %"PRId64", stop %"PRId64", sum_dur %"PRId64, buf->s.start, stop, m->sum_dur); } else { /* Write an empty sample */ if ( mux_data->sum_dur < buf->s.start ) { uint8_t empty[2] = {0,0}; if( !MP4WriteSample( m->file, mux_data->track, empty, 2, buf->s.start - mux_data->sum_dur, 0, 1 )) { hb_error("Failed to write to output file, disk full?"); *job->die = 1; } mux_data->sum_dur += buf->s.start - mux_data->sum_dur; } uint8_t styleatom[2048];; uint16_t stylesize = 0; uint8_t buffer[2048]; uint16_t buffersize = 0; uint8_t output[2048]; *buffer = '\0'; /* * Copy the subtitle into buffer stripping markup and creating * style atoms for them. */ hb_muxmp4_process_subtitle_style( buf->data, buffer, styleatom, &stylesize ); buffersize = strlen((char*)buffer); hb_deep_log(3, "MuxMP4:Sub:%fs:%"PRId64":%"PRId64":%f: %s", (float)buf->s.start / 90000, buf->s.start, stop, buf->s.duration, buffer); /* Write the subtitle sample */ memcpy( output + 2, buffer, buffersize ); memcpy( output + 2 + buffersize, styleatom, stylesize); output[0] = ( buffersize >> 8 ) & 0xff; output[1] = buffersize & 0xff; if( !MP4WriteSample( m->file, mux_data->track, output, buffersize + stylesize + 2, buf->s.duration, 0, 1 )) { hb_error("Failed to write to output file, disk full?"); *job->die = 1; } mux_data->sum_dur += buf->s.duration; } } else if( mux_data->sub_format == PICTURESUB ) { /* Write an empty sample */ if ( mux_data->sum_dur < buf->s.start ) { uint8_t empty[2] = {0,0}; if( !MP4WriteSample( m->file, mux_data->track, empty, 2, buf->s.start - mux_data->sum_dur, 0, 1 )) { hb_error("Failed to write to output file, disk full?"); *job->die = 1; } mux_data->sum_dur += buf->s.start - mux_data->sum_dur; } if( !MP4WriteSample( m->file, mux_data->track, buf->data, buf->size, buf->s.duration, 0, 1 )) { hb_error("Failed to write to output file, disk full?"); *job->die = 1; } mux_data->sum_dur += buf->s.duration; } } else { /* * Audio */ if( !MP4WriteSample( m->file, mux_data->track, buf->data, buf->size, duration, offset, ( buf->s.frametype & HB_FRAME_KEY ) != 0 )) { hb_error("Failed to write to output file, disk full?"); *job->die = 1; } } hb_buffer_close( &buf ); return 0; } static int MP4End( hb_mux_object_t * m ) { hb_job_t * job = m->job; int i; if (m->file != MP4_INVALID_FILE_HANDLE) { // Flush the delayed frame if ( m->delay_buf ) MP4Mux( m, job->mux_data, NULL ); /* Write our final chapter marker */ if( m->job->chapter_markers ) { hb_chapter_t *chapter = NULL; int64_t duration = m->sum_dur - m->chapter_duration; /* The final chapter can have a very short duration - if it's less * than 1.5 seconds just skip it. */ if ( duration >= (90000*3)/2 ) { chapter = hb_list_item( m->job->list_chapter, m->current_chapter - 1 ); MP4AddChapter( m->file, m->chapter_track, duration, (chapter != NULL) ? chapter->title : NULL); } } if ( job->config.h264.init_delay ) { // Insert track edit to get A/V back in sync. The edit amount is // the init_delay. int64_t edit_amt = job->config.h264.init_delay; 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); } } // Check for audio preroll and add edit entries for audio for( i = 0; i < hb_list_count( job->list_audio ); i++ ) { hb_audio_t *audio = hb_list_item( job->list_audio, i ); hb_mux_data_t *mux_data = audio->priv.mux_data; if (audio->config.out.delay > 0) { int64_t edit_amt = audio->config.out.delay; MP4AddTrackEdit(m->file, mux_data->track, MP4_INVALID_EDIT_ID, edit_amt, MP4GetTrackDuration(m->file, 1), 0); } } /* * Write the MP4 iTunes metadata if we have any metadata */ if( job->metadata ) { hb_metadata_t *md = job->metadata; const MP4Tags* tags; hb_deep_log( 2, "Writing Metadata to output file..."); /* allocate tags structure */ tags = MP4TagsAlloc(); /* fetch data from MP4 file (in case it already has some data) */ MP4TagsFetch( tags, m->file ); /* populate */ if( md->name ) MP4TagsSetName( tags, md->name ); if( md->artist ) MP4TagsSetArtist( tags, md->artist ); if( md->composer ) MP4TagsSetComposer( tags, md->composer ); if( md->comment ) MP4TagsSetComments( tags, md->comment ); if( md->release_date ) MP4TagsSetReleaseDate( tags, md->release_date ); if( md->album ) MP4TagsSetAlbum( tags, md->album ); if( md->album_artist ) MP4TagsSetAlbumArtist( tags, md->album_artist ); if( md->genre ) MP4TagsSetGenre( tags, md->genre ); if( md->description ) MP4TagsSetDescription( tags, md->description ); if( md->long_description ) MP4TagsSetLongDescription( tags, md->long_description ); if( md->list_coverart ) { hb_coverart_t * coverart; int ii; for ( ii = 0; ii < hb_list_count( md->list_coverart ); ii++ ) { coverart = hb_list_item( md->list_coverart, ii ); MP4TagArtwork art; int type; switch ( coverart->type ) { case HB_ART_BMP: type = MP4_ART_BMP; break; case HB_ART_GIF: type = MP4_ART_GIF; break; case HB_ART_JPEG: type = MP4_ART_JPEG; break; case HB_ART_PNG: type = MP4_ART_PNG; break; default: type = MP4_ART_UNDEFINED; break; } art.data = coverart->data; art.size = coverart->size; art.type = type; MP4TagsAddArtwork( tags, &art ); } } /* push data to MP4 file */ MP4TagsStore( tags, m->file ); /* free memory associated with structure */ MP4TagsFree( tags ); } MP4Close( m->file ); if ( job->mp4_optimize ) { hb_log( "muxmp4: optimizing file" ); char filename[1024]; memset( filename, 0, 1024 ); snprintf(filename, 1024, "%s.tmp", m->path); MP4Optimize(m->path, filename, MP4_DETAILS_ERROR); remove(m->path); rename(filename, m->path); } } free(m->path); return 0; } hb_mux_object_t * hb_mux_mp4_init( hb_job_t * job ) { hb_mux_object_t * m = calloc( sizeof( hb_mux_object_t ), 1 ); m->init = MP4Init; m->mux = MP4Mux; m->end = MP4End; m->job = job; return m; } #endif // USE_MP4V2