From 5817c659e67c98f1d32db9d630ea1103ddd2b261 Mon Sep 17 00:00:00 2001 From: Vivia Nikolaidou Date: Mon, 22 Oct 2018 15:41:56 +0300 Subject: [PATCH] qtmux: Add option to create a timecode trak in non-mov flavors Even if timecode trak is officially unsupported in non-mov flavors, some software still supports it, e.g. Final Cut Pro X: https://developer.apple.com/library/archive/technotes/tn2174/_index.html The user might still expect to see the timecode information in the non-mov file despite it being officially unsupported , because other software e.g. QuickTime will create a timecode trak even in mp4 files. Furthermore, software that supports timecode trak in non-mov flavors will also display the file duration in "timecode units" instead of real clock time, which is not necessarily the same for 29.97 fps and friends. This might confuse users, who see a different duration for the same framerate and amount of frames depending on whether the container is mp4 or mov. Fixes https://gitlab.freedesktop.org/gstreamer/gst-plugins-good/issues/512 --- gst/isomp4/atoms.c | 92 +++++++++++++++++++++++++++++++++++++++++++-------- gst/isomp4/atoms.h | 10 +++++- gst/isomp4/fourcc.h | 1 + gst/isomp4/gstqtmux.c | 30 ++++++++++++++--- gst/isomp4/gstqtmux.h | 2 ++ 5 files changed, 116 insertions(+), 19 deletions(-) diff --git a/gst/isomp4/atoms.c b/gst/isomp4/atoms.c index e1441d4..7478c37 100644 --- a/gst/isomp4/atoms.c +++ b/gst/isomp4/atoms.c @@ -54,10 +54,11 @@ * Creates a new AtomsContext for the given flavor. */ AtomsContext * -atoms_context_new (AtomsTreeFlavor flavor) +atoms_context_new (AtomsTreeFlavor flavor, gboolean force_create_timecode_trak) { AtomsContext *context = g_new0 (AtomsContext, 1); context->flavor = flavor; + context->force_create_timecode_trak = force_create_timecode_trak; return context; } @@ -494,6 +495,34 @@ atom_gmhd_free (AtomGMHD * gmhd) } static void +atom_nmhd_init (AtomNMHD * nmhd) +{ + atom_header_set (&nmhd->header, FOURCC_nmhd, 0, 0); + nmhd->flags = 0; +} + +static void +atom_nmhd_clear (AtomNMHD * nmhd) +{ + atom_clear (&nmhd->header); +} + +static AtomNMHD * +atom_nmhd_new (void) +{ + AtomNMHD *nmhd = g_new0 (AtomNMHD, 1); + atom_nmhd_init (nmhd); + return nmhd; +} + +static void +atom_nmhd_free (AtomNMHD * nmhd) +{ + atom_nmhd_clear (nmhd); + g_free (nmhd); +} + +static void atom_sample_entry_init (SampleTableEntry * se, guint32 type) { atom_header_set (&se->header, type, 0, 0); @@ -1129,6 +1158,10 @@ atom_minf_clear_handlers (AtomMINF * minf) atom_gmhd_free (minf->gmhd); minf->gmhd = NULL; } + if (minf->nmhd) { + atom_nmhd_free (minf->nmhd); + minf->nmhd = NULL; + } } static void @@ -1955,6 +1988,21 @@ atom_gmhd_copy_data (AtomGMHD * gmhd, guint8 ** buffer, guint64 * size, return original_offset - *offset; } +static guint64 +atom_nmhd_copy_data (AtomNMHD * nmhd, guint8 ** buffer, guint64 * size, + guint64 * offset) +{ + guint64 original_offset = *offset; + + if (!atom_copy_data (&nmhd->header, buffer, size, offset)) { + return 0; + } + prop_copy_uint32 (nmhd->flags, buffer, size, offset); + + atom_write_size (buffer, size, offset, original_offset); + return original_offset - *offset; +} + static gboolean atom_url_same_file_flag (AtomURL * url) { @@ -2620,6 +2668,10 @@ atom_minf_copy_data (AtomMINF * minf, guint8 ** buffer, guint64 * size, if (!atom_gmhd_copy_data (minf->gmhd, buffer, size, offset)) { return 0; } + } else if (minf->nmhd) { + if (!atom_nmhd_copy_data (minf->nmhd, buffer, size, offset)) { + return 0; + } } if (minf->hdlr) { @@ -4076,24 +4128,36 @@ atom_trak_set_timecode_type (AtomTRAK * trak, AtomsContext * context, guint32 trak_timescale, GstVideoTimeCode * tc) { SampleTableEntryTMCD *ste; - AtomGMHD *gmhd = trak->mdia.minf.gmhd; - if (context->flavor != ATOMS_TREE_FLAVOR_MOV) { + if (context->flavor != ATOMS_TREE_FLAVOR_MOV && + !context->force_create_timecode_trak) { return NULL; } - ste = atom_trak_add_timecode_entry (trak, context, trak_timescale, tc); - - gmhd = atom_gmhd_new (); - gmhd->gmin.graphics_mode = 0x0040; - gmhd->gmin.opcolor[0] = 0x8000; - gmhd->gmin.opcolor[1] = 0x8000; - gmhd->gmin.opcolor[2] = 0x8000; - gmhd->tmcd = atom_tmcd_new (); - gmhd->tmcd->tcmi.text_size = 12; - gmhd->tmcd->tcmi.font_name = g_strdup ("Chicago"); /* Pascal string */ - trak->mdia.minf.gmhd = gmhd; + if (context->flavor == ATOMS_TREE_FLAVOR_MOV) { + AtomGMHD *gmhd = trak->mdia.minf.gmhd; + + gmhd = atom_gmhd_new (); + gmhd->gmin.graphics_mode = 0x0040; + gmhd->gmin.opcolor[0] = 0x8000; + gmhd->gmin.opcolor[1] = 0x8000; + gmhd->gmin.opcolor[2] = 0x8000; + gmhd->tmcd = atom_tmcd_new (); + gmhd->tmcd->tcmi.text_size = 12; + gmhd->tmcd->tcmi.font_name = g_strdup ("Chicago"); /* Pascal string */ + + trak->mdia.minf.gmhd = gmhd; + } else if (context->force_create_timecode_trak) { + AtomNMHD *nmhd = trak->mdia.minf.nmhd; + /* MOV files use GMHD, other files use NMHD */ + + nmhd = atom_nmhd_new (); + trak->mdia.minf.nmhd = nmhd; + } else { + return NULL; + } + ste = atom_trak_add_timecode_entry (trak, context, trak_timescale, tc); trak->is_video = FALSE; trak->is_h264 = FALSE; diff --git a/gst/isomp4/atoms.h b/gst/isomp4/atoms.h index de93e22..b2587d9 100644 --- a/gst/isomp4/atoms.h +++ b/gst/isomp4/atoms.h @@ -103,9 +103,10 @@ typedef enum _AtomsTreeFlavor typedef struct _AtomsContext { AtomsTreeFlavor flavor; + gboolean force_create_timecode_trak; } AtomsContext; -AtomsContext* atoms_context_new (AtomsTreeFlavor flavor); +AtomsContext* atoms_context_new (AtomsTreeFlavor flavor, gboolean force_create_timecode_trak); void atoms_context_free (AtomsContext *context); #define METADATA_DATA_FLAG 0x0 @@ -325,6 +326,12 @@ typedef struct _AtomGMHD } AtomGMHD; +typedef struct _AtomNMHD +{ + Atom header; + guint32 flags; +} AtomNMHD; + typedef struct _AtomURL { AtomFull header; @@ -600,6 +607,7 @@ typedef struct _AtomMINF AtomSMHD *smhd; AtomHMHD *hmhd; AtomGMHD *gmhd; + AtomNMHD *nmhd; AtomHDLR *hdlr; AtomDINF dinf; diff --git a/gst/isomp4/fourcc.h b/gst/isomp4/fourcc.h index 6893736..51405bb 100644 --- a/gst/isomp4/fourcc.h +++ b/gst/isomp4/fourcc.h @@ -184,6 +184,7 @@ G_BEGIN_DECLS #define FOURCC_name GST_MAKE_FOURCC('n','a','m','e') #define FOURCC_nclc GST_MAKE_FOURCC('n','c','l','c') #define FOURCC_nclx GST_MAKE_FOURCC('n','c','l','x') +#define FOURCC_nmhd GST_MAKE_FOURCC('n','m','h','d') #define FOURCC_opus GST_MAKE_FOURCC('O','p','u','s') #define FOURCC_dops GST_MAKE_FOURCC('d','O','p','s') #define FOURCC_pasp GST_MAKE_FOURCC('p','a','s','p') diff --git a/gst/isomp4/gstqtmux.c b/gst/isomp4/gstqtmux.c index e5944dd6..1c0234f 100644 --- a/gst/isomp4/gstqtmux.c +++ b/gst/isomp4/gstqtmux.c @@ -368,6 +368,7 @@ enum PROP_INTERLEAVE_TIME, PROP_MAX_RAW_AUDIO_DRIFT, PROP_START_GAP_THRESHOLD, + PROP_FORCE_CREATE_TIMECODE_TRAK, }; /* some spare for header size as well */ @@ -392,6 +393,7 @@ enum #define DEFAULT_INTERLEAVE_TIME 250*GST_MSECOND #define DEFAULT_MAX_RAW_AUDIO_DRIFT 40 * GST_MSECOND #define DEFAULT_START_GAP_THRESHOLD 0 +#define DEFAULT_FORCE_CREATE_TIMECODE_TRAK FALSE static void gst_qt_mux_finalize (GObject * object); @@ -635,6 +637,13 @@ gst_qt_mux_class_init (GstQTMuxClass * klass) "Threshold for creating an edit list for gaps at the start in nanoseconds", 0, G_MAXUINT64, DEFAULT_START_GAP_THRESHOLD, G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)); + g_object_class_install_property (gobject_class, + PROP_FORCE_CREATE_TIMECODE_TRAK, + g_param_spec_boolean ("force-create-timecode-trak", + "Force Create Timecode Trak", + "Create a timecode trak even in unsupported flavors", + DEFAULT_FORCE_CREATE_TIMECODE_TRAK, + G_PARAM_READWRITE | G_PARAM_CONSTRUCT | G_PARAM_STATIC_STRINGS)); gstelement_class->request_new_pad = GST_DEBUG_FUNCPTR (gst_qt_mux_request_new_pad); @@ -855,10 +864,12 @@ gst_qt_mux_init (GstQTMux * qtmux, GstQTMuxClass * qtmux_klass) qtmux->interleave_time = DEFAULT_INTERLEAVE_TIME; qtmux->max_raw_audio_drift = DEFAULT_MAX_RAW_AUDIO_DRIFT; qtmux->start_gap_threshold = DEFAULT_START_GAP_THRESHOLD; + qtmux->force_create_timecode_trak = DEFAULT_FORCE_CREATE_TIMECODE_TRAK; /* always need this */ qtmux->context = - atoms_context_new (gst_qt_mux_map_format_to_flavor (qtmux_klass->format)); + atoms_context_new (gst_qt_mux_map_format_to_flavor (qtmux_klass->format), + qtmux->force_create_timecode_trak); /* internals to initial state */ gst_qt_mux_reset (qtmux, TRUE); @@ -2862,7 +2873,8 @@ gst_qt_mux_prefill_samples (GstQTMux * qtmux) } GST_OBJECT_UNLOCK (qtmux); - if (qtmux_klass->format == GST_QT_MUX_FORMAT_QT) { + if (qtmux_klass->format == GST_QT_MUX_FORMAT_QT || + qtmux->force_create_timecode_trak) { /* For the first sample check/update timecode as needed. We do that before * all actual samples as the code in gst_qt_mux_add_buffer() does it with * initial buffer directly, not with last_buf */ @@ -3660,7 +3672,8 @@ gst_qt_mux_update_timecode (GstQTMux * qtmux, GstQTMuxPad * qtpad) guint64 offset = qtpad->tc_pos; GstQTMuxClass *qtmux_klass = (GstQTMuxClass *) (G_OBJECT_GET_CLASS (qtmux)); - if (qtmux_klass->format != GST_QT_MUX_FORMAT_QT) + if (qtmux_klass->format != GST_QT_MUX_FORMAT_QT && + !qtmux->force_create_timecode_trak) return GST_FLOW_OK; g_assert (qtpad->tc_pos != -1); @@ -4496,7 +4509,8 @@ gst_qt_mux_check_and_update_timecode (GstQTMux * qtmux, GstQTMuxPad * pad, if (!pad->trak->is_video) return ret; - if (qtmux_klass->format != GST_QT_MUX_FORMAT_QT) + if (qtmux_klass->format != GST_QT_MUX_FORMAT_QT && + !qtmux->force_create_timecode_trak) return ret; if (buf == NULL || (pad->tc_trak != NULL && pad->tc_pos == -1)) @@ -6654,6 +6668,9 @@ gst_qt_mux_get_property (GObject * object, case PROP_START_GAP_THRESHOLD: g_value_set_uint64 (value, qtmux->start_gap_threshold); break; + case PROP_FORCE_CREATE_TIMECODE_TRAK: + g_value_set_boolean (value, qtmux->force_create_timecode_trak); + break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); break; @@ -6748,6 +6765,11 @@ gst_qt_mux_set_property (GObject * object, case PROP_START_GAP_THRESHOLD: qtmux->start_gap_threshold = g_value_get_uint64 (value); break; + case PROP_FORCE_CREATE_TIMECODE_TRAK: + qtmux->force_create_timecode_trak = g_value_get_boolean (value); + qtmux->context->force_create_timecode_trak = + qtmux->force_create_timecode_trak; + break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); break; diff --git a/gst/isomp4/gstqtmux.h b/gst/isomp4/gstqtmux.h index d18871b..0fff736 100644 --- a/gst/isomp4/gstqtmux.h +++ b/gst/isomp4/gstqtmux.h @@ -313,6 +313,8 @@ struct _GstQTMux GstClockTime start_gap_threshold; + gboolean force_create_timecode_trak; + /* for request pad naming */ guint video_pads, audio_pads, subtitle_pads, caption_pads; }; -- 2.7.4