/* libmp4v2 handle */
MP4FileHandle file;
- /* 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
+ int64_t sum_dur; // sum of video frame durations so far
- // bias to keep render offsets in ctts atom positive (set up by encx264)
- int64_t init_delay;
+ hb_buffer_t *delay_buf;
/* 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
{
- MP4TrackId track;
+ MP4TrackId track;
+ uint8_t subtitle;
+ int sub_format;
+
+ uint64_t sum_dur; // sum of the frame durations so far
};
+/* 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;
+}
/**********************************************************************
* MP4Init
hb_audio_t * audio;
hb_mux_data_t * mux_data;
int i;
+ int subtitle_default;
/* 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 */
if (job->largeFileSize)
/* Use 64-bit MP4 file */
}
/* Video track */
- mux_data = malloc( sizeof( hb_mux_data_t ) );
+ mux_data = calloc(1, 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, m->samplerate )))
+ if (!(MP4SetTimeScale( m->file, 90000 )))
{
hb_error("muxmp4.c: MP4SetTimeScale failed!");
*job->die = 1;
{
/* Stolen from mp4creator */
MP4SetVideoProfileLevel( m->file, 0x7F );
- mux_data->track = MP4AddH264VideoTrack( m->file, m->samplerate,
+ 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 );
hb_deep_log( 2, "muxmp4: adding iPod atom");
MP4AddIPodUUID(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, m->samplerate,
+ 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)
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,
MP4AddColr(m->file, mux_data->track, 6, 1, 6);
}
- if( job->pixel_ratio )
+ if( job->anamorphic.mode )
{
/* PASP atom for anamorphic video */
float width, height;
- width = job->pixel_aspect_width;
+ width = job->anamorphic.par_width;
- height = job->pixel_aspect_height;
+ height = job->anamorphic.par_height;
MP4AddPixelAspectRatio(m->file, mux_data->track, (uint32_t)width, (uint32_t)height);
for( i = 0; i < hb_list_count( title->list_audio ); i++ )
{
audio = hb_list_item( title->list_audio, i );
- mux_data = malloc( sizeof( hb_mux_data_t ) );
+ mux_data = calloc(1, sizeof( hb_mux_data_t ) );
audio->priv.mux_data = mux_data;
if( audio->config.out.codec == HB_ACODEC_AC3 )
mux_data->track = MP4AddAC3AudioTrack(
m->file,
- m->samplerate,
+ audio->config.out.samplerate,
fscod,
bsid,
bsmod,
lfeon,
bit_rate_code);
+ /* Tune track chunk duration */
+ MP4TuneTrackDurationPerChunk( m, mux_data->track );
+
if (audio->config.out.name == NULL) {
MP4SetTrackBytesProperty(
m->file, mux_data->track,
} else {
mux_data->track = MP4AddAudioTrack(
m->file,
- m->samplerate, 1024, MP4_MPEG4_AUDIO_TYPE );
+ audio->config.out.samplerate, 1024, MP4_MPEG4_AUDIO_TYPE );
+
+ /* Tune track chunk duration */
+ MP4TuneTrackDurationPerChunk( m, mux_data->track );
+
if (audio->config.out.name == NULL) {
MP4SetTrackBytesProperty(
m->file, mux_data->track,
them all at once. */
{
MP4SetTrackIntegerProperty(m->file, mux_data->track, "tkhd.flags", (TRACK_DISABLED | TRACK_IN_MOVIE));
- hb_deep_log( 2, "muxp4: disabled extra audio track %i", mux_data->track-1);
+ 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));
+ }
+ }
+ }
+
if (job->chapter_markers)
{
/* add a text track for the chapters. We add the 'chap' atom to track
/* 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);
+ 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;
}
+typedef struct stylerecord_s {
+ enum style_s {ITALIC, BOLD, UNDERLINE} style;
+ uint16_t start;
+ uint16_t stop;
+ struct stylerecord_s *next;
+} stylerecord;
+
+static void hb_makestylerecord( stylerecord **stack,
+ enum style_s style, int start )
+{
+ stylerecord *record = calloc( sizeof( stylerecord ), 1 );
+
+ if( record )
+ {
+ record->style = style;
+ record->start = start;
+ record->next = *stack;
+ *stack = record;
+ }
+}
+
+static void hb_makestyleatom( stylerecord *record, uint8_t *style)
+{
+ uint8_t face = 1;
+ hb_deep_log(3, "Made style '%s' from %d to %d",
+ record->style == ITALIC ? "Italic" : record->style == BOLD ? "Bold" : "Underline", record->start, record->stop);
+
+ switch( record->style )
+ {
+ case ITALIC:
+ face = 2;
+ break;
+ case BOLD:
+ face = 1;
+ break;
+ case UNDERLINE:
+ face = 4;
+ break;
+ default:
+ face = 2;
+ break;
+ }
+
+ style[0] = (record->start >> 8) & 0xff; // startChar
+ style[1] = record->start & 0xff;
+ style[2] = (record->stop >> 8) & 0xff; // endChar
+ style[3] = record->stop & 0xff;
+ style[4] = (1 >> 8) & 0xff; // font-ID
+ style[5] = 1 & 0xff;
+ style[6] = face; // face-style-flags: 1 bold; 2 italic; 4 underline
+ style[7] = 24; // font-size
+ style[8] = 255; // r
+ style[9] = 255; // g
+ style[10] = 255; // b
+ style[11] = 255; // a
+
+}
+
+/*
+ * Copy the input to output removing markup and adding markup to the style
+ * atom where appropriate.
+ */
+static void hb_muxmp4_process_subtitle_style( uint8_t *input,
+ uint8_t *output,
+ uint8_t *style, uint16_t *stylesize )
+{
+ uint8_t *reader = input;
+ uint8_t *writer = output;
+ uint8_t stylecount = 0;
+ uint16_t utf8_count = 0; // utf8 count from start of subtitle
+ stylerecord *stylestack = NULL;
+ stylerecord *oldrecord = NULL;
+
+ while(*reader != '\0') {
+ if( ( *reader & 0xc0 ) == 0x80 )
+ {
+ /*
+ * Track the utf8_count when doing markup so that we get the tx3g stops
+ * based on UTF8 chr counts rather than bytes.
+ */
+ utf8_count++;
+ hb_deep_log( 3, "MuxMP4: Counted %d UTF-8 chrs within subtitle so far",
+ utf8_count);
+ }
+ if (*reader == '<') {
+ /*
+ * possible markup, peek at the next chr
+ */
+ switch(*(reader+1)) {
+ case 'i':
+ if (*(reader+2) == '>') {
+ reader += 3;
+ hb_makestylerecord(&stylestack, ITALIC, (writer - output - utf8_count));
+ } else {
+ *writer++ = *reader++;
+ }
+ break;
+ case 'b':
+ if (*(reader+2) == '>') {
+ reader += 3;
+ hb_makestylerecord(&stylestack, BOLD, (writer - output - utf8_count));
+ } else {
+ *writer++ = *reader++;
+ }
+ break;
+ case 'u':
+ if (*(reader+2) == '>') {
+ reader += 3;
+ hb_makestylerecord(&stylestack, UNDERLINE, (writer - output - utf8_count));
+ } else {
+ *writer++ = *reader++;
+ }
+ break;
+ case '/':
+ switch(*(reader+2)) {
+ case 'i':
+ if (*(reader+3) == '>') {
+ /*
+ * Check whether we then immediately start more markup of the same type, if so then
+ * lets not close it now and instead continue this markup.
+ */
+ if ((*(reader+4) && *(reader+4) == '<') &&
+ (*(reader+5) && *(reader+5) == 'i') &&
+ (*(reader+6) && *(reader+6) == '>')) {
+ /*
+ * Opening italics right after, so don't close off these italics.
+ */
+ hb_deep_log(3, "Joining two sets of italics");
+ reader += (4 + 3);
+ continue;
+ }
+
+
+ if ((*(reader+4) && *(reader+4) == ' ') &&
+ (*(reader+5) && *(reader+5) == '<') &&
+ (*(reader+6) && *(reader+6) == 'i') &&
+ (*(reader+7) && *(reader+7) == '>')) {
+ /*
+ * Opening italics right after, so don't close off these italics.
+ */
+ hb_deep_log(3, "Joining two sets of italics (plus space)");
+ reader += (4 + 4);
+ *writer++ = ' ';
+ continue;
+ }
+ if (stylestack && stylestack->style == ITALIC) {
+ uint8_t style_record[12];
+ stylestack->stop = writer - output - utf8_count;
+ hb_makestyleatom(stylestack, style_record);
+
+ memcpy(style + 10 + (12 * stylecount), style_record, 12);
+ stylecount++;
+
+ oldrecord = stylestack;
+ stylestack = stylestack->next;
+ free(oldrecord);
+ } else {
+ hb_error("Mismatched Subtitle markup '%s'", input);
+ }
+ reader += 4;
+ } else {
+ *writer++ = *reader++;
+ }
+ break;
+ case 'b':
+ if (*(reader+3) == '>') {
+ if (stylestack && stylestack->style == BOLD) {
+ uint8_t style_record[12];
+ stylestack->stop = writer - output - utf8_count;
+ hb_makestyleatom(stylestack, style_record);
+
+ memcpy(style + 10 + (12 * stylecount), style_record, 12);
+ stylecount++;
+ oldrecord = stylestack;
+ stylestack = stylestack->next;
+ free(oldrecord);
+ } else {
+ hb_error("Mismatched Subtitle markup '%s'", input);
+ }
+
+ reader += 4;
+ } else {
+ *writer++ = *reader++;
+ }
+ break;
+ case 'u':
+ if (*(reader+3) == '>') {
+ if (stylestack && stylestack->style == UNDERLINE) {
+ uint8_t style_record[12];
+ stylestack->stop = writer - output - utf8_count;
+ hb_makestyleatom(stylestack, style_record);
+
+ memcpy(style + 10 + (12 * stylecount), style_record, 12);
+ stylecount++;
+
+ oldrecord = stylestack;
+ stylestack = stylestack->next;
+ free(oldrecord);
+ } else {
+ hb_error("Mismatched Subtitle markup '%s'", input);
+ }
+ reader += 4;
+ } else {
+ *writer++ = *reader++;
+ }
+ break;
+ default:
+ *writer++ = *reader++;
+ break;
+ }
+ break;
+ default:
+ *writer++ = *reader++;
+ break;
+ }
+ } else {
+ *writer++ = *reader++;
+ }
+ }
+ *writer = '\0';
+
+ if( stylecount )
+ {
+ *stylesize = 10 + ( stylecount * 12 );
+
+ memcpy( style + 4, "styl", 4);
+
+ style[0] = 0;
+ style[1] = 0;
+ style[2] = (*stylesize >> 8) & 0xff;
+ style[3] = *stylesize & 0xff;
+ style[8] = (stylecount >> 8) & 0xff;
+ style[9] = stylecount & 0xff;
+
+ }
+
+}
+
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;
int64_t offset = 0;
+ hb_buffer_t *tmp;
if( mux_data == job->mux_data )
{
/* Video */
- // 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 )
+ if( job->vcodec == HB_VCODEC_X264 )
+ {
+ if ( buf && buf->start < buf->renderOffset )
+ {
+ hb_log("MP4Mux: PTS %"PRId64" < DTS %"PRId64,
+ buf->start, buf->renderOffset );
+ buf->renderOffset = buf->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;
+
+ if( job->vcodec == HB_VCODEC_X264 )
{
- offset = ( buf->start + m->init_delay ) * m->samplerate / 90000 -
- m->sum_dur;
+ // x264 supplies us with DTS, so offset is PTS - DTS
+ offset = buf->start - buf->renderOffset;
}
/* Add the sample before the new frame.
// 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 <= 0 )
+ if ( duration >= (90000*3)/2 )
{
- /* 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;
- }
+ chapter = hb_list_item( m->job->title->list_chapter,
+ buf->new_chap - 2 );
- chapter = hb_list_item( m->job->title->list_chapter,
- buf->new_chap - 2 );
+ MP4AddChapter( m->file,
+ m->chapter_track,
+ duration,
+ (chapter != NULL) ? chapter->title : NULL);
- MP4AddChapter( m->file,
- m->chapter_track,
- duration,
- (chapter != NULL) ? chapter->title : NULL);
+ m->current_chapter = buf->new_chap;
+ m->chapter_duration += duration;
+ }
+ }
- m->current_chapter = buf->new_chap;
- m->chapter_duration += duration;
+ if( job->vcodec == HB_VCODEC_X264 )
+ {
+ // x264 supplies us with DTS
+ if ( m->delay_buf )
+ {
+ duration = m->delay_buf->renderOffset - buf->renderOffset;
+ }
+ else
+ {
+ duration = buf->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->stop - buf->start;
}
- // 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 );
+ hb_log("MP4Mux: illegal duration %"PRId64", start %"PRId64","
+ "stop %"PRId64", sum_dur %"PRId64,
+ duration, buf->start, 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;
+ 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;
- m->sum_dur_in += dur_in;
}
else
{
duration = MP4_INVALID_DURATION;
}
- // 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) ) )
+ /* Here's where the sample actually gets muxed. */
+ if( job->vcodec == HB_VCODEC_X264 && mux_data == job->mux_data )
{
- hb_error("Failed to write to output file, disk full?");
- *job->die = 1;
+ /* 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->flags & HB_FRAME_REF )
+ dflags |= MP4_SDT_HAS_DEPENDENTS;
+ else
+ dflags |= MP4_SDT_HAS_NO_DEPENDENTS; /* disposable */
+
+ switch( buf->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 )
+ {
+ /* Write an empty sample */
+ if ( mux_data->sum_dur < buf->start )
+ {
+ uint8_t empty[2] = {0,0};
+ if( !MP4WriteSample( m->file,
+ mux_data->track,
+ empty,
+ 2,
+ buf->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->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":%"PRId64": %s",
+ (float)buf->start / 90000, buf->start, buf->stop,
+ (buf->stop - buf->start), 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->stop - buf->start,
+ 0,
+ 1 ))
+ {
+ hb_error("Failed to write to output file, disk full?");
+ *job->die = 1;
+ }
+
+ mux_data->sum_dur += (buf->stop - buf->start);
+ }
}
+ else
+ {
+ /*
+ * Audio
+ */
+ if( !MP4WriteSample( m->file,
+ mux_data->track,
+ buf->data,
+ buf->size,
+ duration,
+ offset,
+ ( buf->frametype & HB_FRAME_KEY ) != 0 ))
+ {
+ hb_error("Failed to write to output file, disk full?");
+ *job->die = 1;
+ }
+ }
+ hb_buffer_close( &buf );
return 0;
}
hb_job_t * job = m->job;
hb_title_t * title = job->title;
+ // 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 a second just skip it. */
- if ( duration >= m->samplerate )
+ * than 1.5 seconds just skip it. */
+ if ( duration >= (90000*3)/2 )
{
chapter = hb_list_item( m->job->title->list_chapter,
}
}
- if (job->areBframes)
+ 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 = m->init_delay * m->samplerate / 90000;
+ 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 )
if( title->metadata )
{
hb_metadata_t *md = title->metadata;
+ const MP4Tags* tags;
hb_deep_log( 2, "Writing Metadata to output file...");
- MP4SetMetadataName( m->file, md->name );
- MP4SetMetadataArtist( m->file, md->artist );
- MP4SetMetadataComposer( m->file, md->composer );
- MP4SetMetadataComment( m->file, md->comment );
- MP4SetMetadataReleaseDate( m->file, md->release_date );
- MP4SetMetadataAlbum( m->file, md->album );
- MP4SetMetadataGenre( m->file, md->genre );
+ /* allocate tags structure */
+ tags = MP4TagsAlloc();
+ /* fetch data from MP4 file (in case it already has some data) */
+ MP4TagsFetch( tags, m->file );
+
+ /* populate */
+ if( strlen( md->name ))
+ MP4TagsSetName( tags, md->name );
+ if( strlen( md->artist ))
+ MP4TagsSetArtist( tags, md->artist );
+ if( strlen( md->composer ))
+ MP4TagsSetComposer( tags, md->composer );
+ if( strlen( md->comment ))
+ MP4TagsSetComments( tags, md->comment );
+ if( strlen( md->release_date ))
+ MP4TagsSetReleaseDate( tags, md->release_date );
+ if( strlen( md->album ))
+ MP4TagsSetAlbum( tags, md->album );
+ if( strlen( md->genre ))
+ MP4TagsSetGenre( tags, md->genre );
+
if( md->coverart )
{
- MP4SetMetadataCoverArt( m->file, md->coverart, md->coverart_size);
+ MP4TagArtwork art;
+ art.data = md->coverart;
+ art.size = md->coverart_size;
+ art.type = MP4_ART_UNDEFINED; // delegate typing to libmp4v2
+ MP4TagsAddArtwork( tags, &art );
}
+
+ /* push data to MP4 file */
+ MP4TagsStore( tags, m->file );
+ /* free memory associated with structure */
+ MP4TagsFree( tags );
}
MP4Close( m->file );