[FFmpeg-devel] [PATCH 2/2] movenc: add timecode track support.

Clément Bœsch ubitux at gmail.com
Wed Apr 4 16:03:18 CEST 2012


On Tue, Mar 20, 2012 at 12:27:52PM +0000, Tim Nicholson wrote:
> On 20/03/12 08:00, Clément Bœsch wrote:
> > On Tue, Mar 20, 2012 at 07:39:01AM +0000, Tim Nicholson wrote:
> >> On 09/03/12 16:51, Clément Bœsch wrote:
> >>> From: Clément Bœsch <clement.boesch at smartjog.com>
> >>>
> >>> The example in the QT specs shows a likely invalid gmin atom size, which
> >>> suggests tmcd atom is contained in the gmin one, while it is actually in
> >>> the gmhd atom, following gmin (according to the given layout on the same
> >>> page, and various samples):
> >>>
> >>> [..]
> >>
> >>
> >> ping.
> >>
> > 
> > Patch rebased for review, no functional changes from previously (IIRC).
> 
> 
> OK this sort of half works, but something isn't quite right.
> 
> Ffprobe reports the timecode OK as does FCP7 and sebsky. *However* if you scroll
> along the clip the timecode gets lost/freezes. This does not happen with a
> genuine clip.
> 
> Inspecting the clip in Quicktime7 using <Command> J and comparing with the same
> clip exported out of FCP shows a significant difference.
> 
> Whilst the duration of my sample clip is shown as 01:01.92 for all tracks in the
> exported (genuine) version. The ffmpeg version shows the timecode track as being
> only 4 frames long. (By comparison ffmbc timecode track has the same duration as
> the clip and scrolls OK)
> 

I did a full re-check and fixed a "little" thing; tcmi atom was missing 16
bits in the middle (it's not in the specs, but it seems there is indeed 16
"unknown" bits in the samples I have). I don't know if that will help (it
shifts a few things so maybe it does).

However, I couldn't find anything suspicious in the durations or timescale
related fields, so if that's still reproducible with the attached patch,
could you share the original samples, the command line you used, and
expected output (from QT7 or anything)?

Anyway, thank you for testing it and raising feedback,

-- 
Clément B.
-------------- next part --------------
From 82a71aa1d663ce4d0d6da00b274d9755ca221180 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Cl=C3=A9ment=20B=C5=93sch?= <clement.boesch at smartjog.com>
Date: Mon, 5 Mar 2012 08:51:08 +0100
Subject: [PATCH] movenc: add timecode track support.

---
 Changelog            |    1 +
 doc/general.texi     |    2 +-
 libavformat/movenc.c |  144 ++++++++++++++++++++++++++++++++++++++++++++++++--
 libavformat/movenc.h |    8 +++
 tests/ref/lavf/mov   |    8 ++--
 5 files changed, 154 insertions(+), 9 deletions(-)

diff --git a/Changelog b/Changelog
index f46ee0a..a9e8a8f 100644
--- a/Changelog
+++ b/Changelog
@@ -20,6 +20,7 @@ version next:
 - ZeroCodec decoder
 - tile video filter
 - Metal Gear Solid: The Twin Snakes demuxer
+- timecode track writting in MOV
 
 
 version 0.10:
diff --git a/doc/general.texi b/doc/general.texi
index 8f62741..ccda79e 100644
--- a/doc/general.texi
+++ b/doc/general.texi
@@ -887,7 +887,7 @@ performance on systems without hardware floating point support).
 @item Codec/format      @tab Read   @tab Write
 @item DV                @tab X      @tab X
 @item GXF               @tab X      @tab X
- at item MOV               @tab X      @tab
+ at item MOV               @tab X      @tab X
 @item MPEG1/2           @tab X      @tab X
 @item MXF               @tab X      @tab X
 @end multitable
diff --git a/libavformat/movenc.c b/libavformat/movenc.c
index 73b6c66..1a630a4 100644
--- a/libavformat/movenc.c
+++ b/libavformat/movenc.c
@@ -60,6 +60,7 @@ static const AVOption options[] = {
     { "min_frag_duration", "Minimum fragment duration", offsetof(MOVMuxContext, min_fragment_duration), AV_OPT_TYPE_INT, {.dbl = 0}, 0, INT_MAX, AV_OPT_FLAG_ENCODING_PARAM},
     { "frag_size", "Maximum fragment size", offsetof(MOVMuxContext, max_fragment_size), AV_OPT_TYPE_INT, {.dbl = 0}, 0, INT_MAX, AV_OPT_FLAG_ENCODING_PARAM},
     { "ism_lookahead", "Number of lookahead entries for ISM files", offsetof(MOVMuxContext, ism_lookahead), AV_OPT_TYPE_INT, {.dbl = 0}, 0, INT_MAX, AV_OPT_FLAG_ENCODING_PARAM},
+    { AV_TIMECODE_OPTION(MOVMuxContext, tc_opt_str, AV_OPT_FLAG_ENCODING_PARAM)},
     { NULL },
 };
 
@@ -1090,6 +1091,24 @@ static int mov_write_video_tag(AVIOContext *pb, MOVTrack *track)
     return update_size(pb, pos);
 }
 
+static int mov_write_tmcd_tag(AVIOContext *pb, MOVTrack *track)
+{
+    int64_t pos = avio_tell(pb);
+    int nb_frames = (track->timescale + track->track_duration/2) / track->track_duration;
+
+    avio_wb32(pb, 0); /* size */
+    ffio_wfourcc(pb, "tmcd");               /* Data format */
+    avio_wb32(pb, 0);                       /* Reserved */
+    avio_wb32(pb, 1);                       /* Data reference index */
+    avio_wb32(pb, 0);                       /* Flags */
+    avio_wb32(pb, track->timecode_flags);   /* Flags (timecode) */
+    avio_wb32(pb, track->timescale);        /* Timescale */
+    avio_wb32(pb, track->track_duration);   /* Frame duration */
+    avio_w8(pb, nb_frames);                 /* Number of frames */
+    avio_w8(pb, 0);                         /* TODO: source reference string */
+    return update_size(pb, pos);
+}
+
 static int mov_write_rtp_tag(AVIOContext *pb, MOVTrack *track)
 {
     int64_t pos = avio_tell(pb);
@@ -1125,6 +1144,8 @@ static int mov_write_stsd_tag(AVIOContext *pb, MOVTrack *track)
         mov_write_subtitle_tag(pb, track);
     else if (track->enc->codec_tag == MKTAG('r','t','p',' '))
         mov_write_rtp_tag(pb, track);
+    else if (track->enc->codec_tag == MKTAG('t','m','c','d'))
+        mov_write_tmcd_tag(pb, track);
     return update_size(pb, pos);
 }
 
@@ -1257,9 +1278,32 @@ static int mov_write_nmhd_tag(AVIOContext *pb)
     return 12;
 }
 
-static int mov_write_gmhd_tag(AVIOContext *pb)
+static int mov_write_tcmi_tag(AVIOContext *pb, MOVTrack *track)
+{
+    int64_t pos = avio_tell(pb);
+    const char *font = "Lucida Grande";
+    avio_wb32(pb, 0);                   /* size */
+    ffio_wfourcc(pb, "tcmi");           /* timecode media information atom */
+    avio_wb32(pb, 0);                   /* version & flags */
+    avio_wb16(pb, 0);                   /* text font */
+    avio_wb16(pb, 0);                   /* text face */
+    avio_wb16(pb, 12);                  /* text size */
+    avio_wb16(pb, 0);                   /* (unknown, not in the QT specs...) */
+    avio_wb16(pb, 0x0000);              /* text color (red) */
+    avio_wb16(pb, 0x0000);              /* text color (green) */
+    avio_wb16(pb, 0x0000);              /* text color (blue) */
+    avio_wb16(pb, 0xffff);              /* background color (red) */
+    avio_wb16(pb, 0xffff);              /* background color (green) */
+    avio_wb16(pb, 0xffff);              /* background color (blue) */
+    avio_w8(pb, strlen(font));          /* font len (part of the pascal string) */
+    avio_write(pb, font, strlen(font)); /* font name */
+    return update_size(pb, pos);
+}
+
+static int mov_write_gmhd_tag(AVIOContext *pb, MOVTrack *track)
 {
-    avio_wb32(pb, 0x20);   /* size */
+    int64_t pos = avio_tell(pb);
+    avio_wb32(pb, 0);      /* size */
     ffio_wfourcc(pb, "gmhd");
     avio_wb32(pb, 0x18);   /* gmin size */
     ffio_wfourcc(pb, "gmin");/* generic media info */
@@ -1270,7 +1314,15 @@ static int mov_write_gmhd_tag(AVIOContext *pb)
     avio_wb16(pb, 0x8000); /* opColor (b?) */
     avio_wb16(pb, 0);      /* balance */
     avio_wb16(pb, 0);      /* reserved */
-    return 0x20;
+
+    if (track->enc->codec_tag == MKTAG('t','m','c','d')) {
+        int64_t tmcd_pos = avio_tell(pb);
+        avio_wb32(pb, 0); /* size */
+        ffio_wfourcc(pb, "tmcd");
+        mov_write_tcmi_tag(pb, track);
+        update_size(pb, tmcd_pos);
+    }
+    return update_size(pb, pos);
 }
 
 static int mov_write_smhd_tag(AVIOContext *pb)
@@ -1313,6 +1365,9 @@ static int mov_write_hdlr_tag(AVIOContext *pb, MOVTrack *track)
             if (track->tag == MKTAG('t','x','3','g')) hdlr_type = "sbtl";
             else                                      hdlr_type = "text";
             descr = "SubtitleHandler";
+        } else if (track->enc->codec_tag == MKTAG('t','m','c','d')) {
+            hdlr_type = "tmcd";
+            descr = "TimeCodeHandler";
         } else if (track->enc->codec_tag == MKTAG('r','t','p',' ')) {
             hdlr_type = "hint";
             descr = "HintHandler";
@@ -1364,8 +1419,10 @@ static int mov_write_minf_tag(AVIOContext *pb, MOVTrack *track)
     else if (track->enc->codec_type == AVMEDIA_TYPE_AUDIO)
         mov_write_smhd_tag(pb);
     else if (track->enc->codec_type == AVMEDIA_TYPE_SUBTITLE) {
-        if (track->tag == MKTAG('t','e','x','t')) mov_write_gmhd_tag(pb);
+        if (track->tag == MKTAG('t','e','x','t')) mov_write_gmhd_tag(pb, track);
         else                                      mov_write_nmhd_tag(pb);
+    } else if (track->tag == MKTAG('t','m','c','d')) {
+        mov_write_gmhd_tag(pb, track);
     } else if (track->tag == MKTAG('r','t','p',' ')) {
         mov_write_hmhd_tag(pb);
     }
@@ -2122,6 +2179,11 @@ static int mov_write_moov_tag(AVIOContext *pb, MOVMuxContext *mov,
                 mov->tracks[mov->tracks[i].src_track].track_id;
         }
     }
+    if (mov->tmcd_track) {
+        i = mov->tmcd_video_track + 1;
+        mov->tracks[i].tref_tag = MKTAG('t','m','c','d');
+        mov->tracks[i].tref_id  = mov->tracks[mov->tmcd_track].track_id;
+    }
 
     mov_write_mvhd_tag(pb, mov);
     if (mov->mode != MODE_MOV && !mov->iods_skip)
@@ -3078,6 +3140,57 @@ static void mov_create_chapter_track(AVFormatContext *s, int tracknum)
     }
 }
 
+static int mov_create_timecode_track(AVFormatContext *s, int tracknum, int vst_idx)
+{
+    MOVMuxContext *mov = s->priv_data;
+    MOVTrack *track = &mov->tracks[tracknum];
+    AVTimecode tc;
+    AVPacket pkt = { .stream_index = tracknum, .flags = AV_PKT_FLAG_KEY };
+    const AVStream *vst = s->streams[vst_idx];
+    AVRational rate = {vst->codec->time_base.den, vst->codec->time_base.num};
+
+    /* compute the frame number internally */
+    int ret = av_timecode_init_from_string(&tc, rate, mov->tc_opt_str, s);
+    if (ret < 0)
+        return ret;
+
+    /* tmcd track based on video stream */
+    track->mode = mov->mode;
+    track->tag  = MKTAG('t','m','c','d');
+    track->timescale = vst->codec->time_base.den;
+    if (tc.flags & AV_TIMECODE_FLAG_DROPFRAME)
+        track->timecode_flags |= MOV_TIMECODE_FLAG_DROPFRAME;
+
+    /* encode context: tmcd data stream */
+    track->enc = avcodec_alloc_context3(NULL);
+    track->enc->codec_type = AVMEDIA_TYPE_DATA;
+    track->enc->codec_tag  = track->tag;
+
+    /* the timecode stream just contains one packet with the frame number */
+    pkt.size = 4;
+    pkt.data = av_malloc(pkt.size);
+    pkt.duration = vst->codec->time_base.num; // for track->track_duration
+    AV_WB32(pkt.data, tc.start);
+    ret = ff_mov_write_packet(s, &pkt);
+    av_free(pkt.data);
+    return ret;
+}
+
+/* select the video with the highest resolution, just like ffmpeg.c. */
+static int select_best_video_stream(AVFormatContext *s)
+{
+    int i, vst = -1;
+    unsigned best_res = 0;
+
+    for (i = 0; i < s->nb_streams; i++) {
+        unsigned res = s->streams[i]->codec->width *
+                       s->streams[i]->codec->height;
+        if (res > best_res)
+            best_res = res, vst = i;
+    }
+    return vst;
+}
+
 static int mov_write_header(AVFormatContext *s)
 {
     AVIOContext *pb = s->pb;
@@ -3140,6 +3253,22 @@ static int mov_write_header(AVFormatContext *s)
         }
     }
 
+    /* Add a stream for the timecode track if -timecode option is specified */
+    if (mov->tc_opt_str) {
+        if (mov->mode != MODE_MOV) {
+            av_log(s, AV_LOG_ERROR, "Timecode is only supported with MOV.\n");
+            goto error;
+        }
+        mov->tmcd_track = mov->nb_streams++;
+        /* TODO: add an option to select the video stream to associate timecode with */
+        mov->tmcd_video_track = select_best_video_stream(s);
+        if (mov->tmcd_video_track < 0) {
+            av_log(s, AV_LOG_ERROR,
+                   "No video stream found to associate the timecode with.\n");
+            goto error;
+        }
+    }
+
     mov->tracks = av_mallocz(mov->nb_streams*sizeof(*mov->tracks));
     if (!mov->tracks)
         return AVERROR(ENOMEM);
@@ -3269,6 +3398,10 @@ static int mov_write_header(AVFormatContext *s)
         }
     }
 
+    if (mov->tmcd_track &&
+        mov_create_timecode_track(s, mov->tmcd_track, mov->tmcd_video_track) < 0)
+        goto error;
+
     avio_flush(pb);
 
     if (mov->flags & FF_MOV_FLAG_ISML)
@@ -3352,6 +3485,9 @@ static int mov_write_trailer(AVFormatContext *s)
 
     }
 
+    if (mov->tmcd_track)
+        av_freep(&mov->tracks[mov->tmcd_track].enc);
+
     avio_flush(pb);
 
     av_freep(&mov->tracks);
diff --git a/libavformat/movenc.h b/libavformat/movenc.h
index 574de82..e6c1ccf 100644
--- a/libavformat/movenc.h
+++ b/libavformat/movenc.h
@@ -87,6 +87,10 @@ typedef struct MOVIndex {
 #define MOV_TRACK_CTTS         0x0001
 #define MOV_TRACK_STPS         0x0002
     uint32_t    flags;
+#define MOV_TIMECODE_FLAG_DROPFRAME     0x0001
+#define MOV_TIMECODE_FLAG_24HOURSMAX    0x0002
+#define MOV_TIMECODE_FLAG_ALLOWNEGATIVE 0x0004
+    uint32_t    timecode_flags;
     int         language;
     int         track_id;
     int         tag; ///< stsd fourcc
@@ -158,6 +162,10 @@ typedef struct MOVMuxContext {
     int max_fragment_size;
     int ism_lookahead;
     AVIOContext *mdat_buf;
+
+    char *tc_opt_str;       ///< timecode option string
+    int tmcd_track;         ///< qt timecode track number
+    int tmcd_video_track;   ///< video stream id to associate the timecode with
 } MOVMuxContext;
 
 #define FF_MOV_FLAG_RTP_HINT 1
diff --git a/tests/ref/lavf/mov b/tests/ref/lavf/mov
index 610a759..b640738 100644
--- a/tests/ref/lavf/mov
+++ b/tests/ref/lavf/mov
@@ -1,8 +1,8 @@
-9a0b239ff596da58debcf210dece3985 *./tests/data/lavf/lavf.mov
-357821 ./tests/data/lavf/lavf.mov
+67b44249786dc7aa2c54f9e40c215ffd *./tests/data/lavf/lavf.mov
+358391 ./tests/data/lavf/lavf.mov
 ./tests/data/lavf/lavf.mov CRC=0x2f6a9b26
-cea874222a6d40b1761d75ea11ebe681 *./tests/data/lavf/lavf.mov
-367251 ./tests/data/lavf/lavf.mov
+809cdd372f29dc8383a1183febf13ddb *./tests/data/lavf/lavf.mov
+367821 ./tests/data/lavf/lavf.mov
 ./tests/data/lavf/lavf.mov CRC=0xab307eb9
 9a0b239ff596da58debcf210dece3985 *./tests/data/lavf/lavf.mov
 357821 ./tests/data/lavf/lavf.mov
-- 
1.7.9.1

-------------- next part --------------
A non-text attachment was scrubbed...
Name: not available
Type: application/pgp-signature
Size: 490 bytes
Desc: not available
URL: <http://ffmpeg.org/pipermail/ffmpeg-devel/attachments/20120404/4b05c995/attachment.asc>


More information about the ffmpeg-devel mailing list