qtmux: Add option to create a timecode trak in non-mov flavors
authorVivia Nikolaidou <vivia@ahiru.eu>
Mon, 22 Oct 2018 12:41:56 +0000 (15:41 +0300)
committerGStreamer Merge Bot <gitlab-merge-bot@gstreamer-foundation.org>
Fri, 3 Apr 2020 18:19:38 +0000 (18:19 +0000)
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
gst/isomp4/atoms.h
gst/isomp4/fourcc.h
gst/isomp4/gstqtmux.c
gst/isomp4/gstqtmux.h

index e1441d4..7478c37 100644 (file)
  * 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;
 
index de93e22..b2587d9 100644 (file)
@@ -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;
index 6893736..51405bb 100644 (file)
@@ -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')
index e5944dd..1c0234f 100644 (file)
@@ -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;
index d18871b..0fff736 100644 (file)
@@ -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;
 };