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
This commit is contained in:
Vivia Nikolaidou 2018-10-22 15:41:56 +03:00 committed by GStreamer Merge Bot
parent db69f02dd8
commit 5817c659e6
5 changed files with 116 additions and 19 deletions

View file

@ -54,10 +54,11 @@
* Creates a new AtomsContext for the given flavor. * Creates a new AtomsContext for the given flavor.
*/ */
AtomsContext * AtomsContext *
atoms_context_new (AtomsTreeFlavor flavor) atoms_context_new (AtomsTreeFlavor flavor, gboolean force_create_timecode_trak)
{ {
AtomsContext *context = g_new0 (AtomsContext, 1); AtomsContext *context = g_new0 (AtomsContext, 1);
context->flavor = flavor; context->flavor = flavor;
context->force_create_timecode_trak = force_create_timecode_trak;
return context; return context;
} }
@ -493,6 +494,34 @@ atom_gmhd_free (AtomGMHD * gmhd)
g_free (gmhd); g_free (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 static void
atom_sample_entry_init (SampleTableEntry * se, guint32 type) atom_sample_entry_init (SampleTableEntry * se, guint32 type)
{ {
@ -1129,6 +1158,10 @@ atom_minf_clear_handlers (AtomMINF * minf)
atom_gmhd_free (minf->gmhd); atom_gmhd_free (minf->gmhd);
minf->gmhd = NULL; minf->gmhd = NULL;
} }
if (minf->nmhd) {
atom_nmhd_free (minf->nmhd);
minf->nmhd = NULL;
}
} }
static void static void
@ -1955,6 +1988,21 @@ atom_gmhd_copy_data (AtomGMHD * gmhd, guint8 ** buffer, guint64 * size,
return original_offset - *offset; 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 static gboolean
atom_url_same_file_flag (AtomURL * url) 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)) { if (!atom_gmhd_copy_data (minf->gmhd, buffer, size, offset)) {
return 0; return 0;
} }
} else if (minf->nmhd) {
if (!atom_nmhd_copy_data (minf->nmhd, buffer, size, offset)) {
return 0;
}
} }
if (minf->hdlr) { if (minf->hdlr) {
@ -4076,13 +4128,15 @@ atom_trak_set_timecode_type (AtomTRAK * trak, AtomsContext * context,
guint32 trak_timescale, GstVideoTimeCode * tc) guint32 trak_timescale, GstVideoTimeCode * tc)
{ {
SampleTableEntryTMCD *ste; 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; return NULL;
} }
ste = atom_trak_add_timecode_entry (trak, context, trak_timescale, tc);
if (context->flavor == ATOMS_TREE_FLAVOR_MOV) {
AtomGMHD *gmhd = trak->mdia.minf.gmhd;
gmhd = atom_gmhd_new (); gmhd = atom_gmhd_new ();
gmhd->gmin.graphics_mode = 0x0040; gmhd->gmin.graphics_mode = 0x0040;
@ -4094,6 +4148,16 @@ atom_trak_set_timecode_type (AtomTRAK * trak, AtomsContext * context,
gmhd->tmcd->tcmi.font_name = g_strdup ("Chicago"); /* Pascal string */ gmhd->tmcd->tcmi.font_name = g_strdup ("Chicago"); /* Pascal string */
trak->mdia.minf.gmhd = gmhd; 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_video = FALSE;
trak->is_h264 = FALSE; trak->is_h264 = FALSE;

View file

@ -103,9 +103,10 @@ typedef enum _AtomsTreeFlavor
typedef struct _AtomsContext typedef struct _AtomsContext
{ {
AtomsTreeFlavor flavor; AtomsTreeFlavor flavor;
gboolean force_create_timecode_trak;
} AtomsContext; } AtomsContext;
AtomsContext* atoms_context_new (AtomsTreeFlavor flavor); AtomsContext* atoms_context_new (AtomsTreeFlavor flavor, gboolean force_create_timecode_trak);
void atoms_context_free (AtomsContext *context); void atoms_context_free (AtomsContext *context);
#define METADATA_DATA_FLAG 0x0 #define METADATA_DATA_FLAG 0x0
@ -325,6 +326,12 @@ typedef struct _AtomGMHD
} AtomGMHD; } AtomGMHD;
typedef struct _AtomNMHD
{
Atom header;
guint32 flags;
} AtomNMHD;
typedef struct _AtomURL typedef struct _AtomURL
{ {
AtomFull header; AtomFull header;
@ -600,6 +607,7 @@ typedef struct _AtomMINF
AtomSMHD *smhd; AtomSMHD *smhd;
AtomHMHD *hmhd; AtomHMHD *hmhd;
AtomGMHD *gmhd; AtomGMHD *gmhd;
AtomNMHD *nmhd;
AtomHDLR *hdlr; AtomHDLR *hdlr;
AtomDINF dinf; AtomDINF dinf;

View file

@ -184,6 +184,7 @@ G_BEGIN_DECLS
#define FOURCC_name GST_MAKE_FOURCC('n','a','m','e') #define FOURCC_name GST_MAKE_FOURCC('n','a','m','e')
#define FOURCC_nclc GST_MAKE_FOURCC('n','c','l','c') #define FOURCC_nclc GST_MAKE_FOURCC('n','c','l','c')
#define FOURCC_nclx GST_MAKE_FOURCC('n','c','l','x') #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_opus GST_MAKE_FOURCC('O','p','u','s')
#define FOURCC_dops GST_MAKE_FOURCC('d','O','p','s') #define FOURCC_dops GST_MAKE_FOURCC('d','O','p','s')
#define FOURCC_pasp GST_MAKE_FOURCC('p','a','s','p') #define FOURCC_pasp GST_MAKE_FOURCC('p','a','s','p')

View file

@ -368,6 +368,7 @@ enum
PROP_INTERLEAVE_TIME, PROP_INTERLEAVE_TIME,
PROP_MAX_RAW_AUDIO_DRIFT, PROP_MAX_RAW_AUDIO_DRIFT,
PROP_START_GAP_THRESHOLD, PROP_START_GAP_THRESHOLD,
PROP_FORCE_CREATE_TIMECODE_TRAK,
}; };
/* some spare for header size as well */ /* some spare for header size as well */
@ -392,6 +393,7 @@ enum
#define DEFAULT_INTERLEAVE_TIME 250*GST_MSECOND #define DEFAULT_INTERLEAVE_TIME 250*GST_MSECOND
#define DEFAULT_MAX_RAW_AUDIO_DRIFT 40 * GST_MSECOND #define DEFAULT_MAX_RAW_AUDIO_DRIFT 40 * GST_MSECOND
#define DEFAULT_START_GAP_THRESHOLD 0 #define DEFAULT_START_GAP_THRESHOLD 0
#define DEFAULT_FORCE_CREATE_TIMECODE_TRAK FALSE
static void gst_qt_mux_finalize (GObject * object); 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", "Threshold for creating an edit list for gaps at the start in nanoseconds",
0, G_MAXUINT64, DEFAULT_START_GAP_THRESHOLD, 0, G_MAXUINT64, DEFAULT_START_GAP_THRESHOLD,
G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)); 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 = gstelement_class->request_new_pad =
GST_DEBUG_FUNCPTR (gst_qt_mux_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->interleave_time = DEFAULT_INTERLEAVE_TIME;
qtmux->max_raw_audio_drift = DEFAULT_MAX_RAW_AUDIO_DRIFT; qtmux->max_raw_audio_drift = DEFAULT_MAX_RAW_AUDIO_DRIFT;
qtmux->start_gap_threshold = DEFAULT_START_GAP_THRESHOLD; qtmux->start_gap_threshold = DEFAULT_START_GAP_THRESHOLD;
qtmux->force_create_timecode_trak = DEFAULT_FORCE_CREATE_TIMECODE_TRAK;
/* always need this */ /* always need this */
qtmux->context = 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 */ /* internals to initial state */
gst_qt_mux_reset (qtmux, TRUE); gst_qt_mux_reset (qtmux, TRUE);
@ -2862,7 +2873,8 @@ gst_qt_mux_prefill_samples (GstQTMux * qtmux)
} }
GST_OBJECT_UNLOCK (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 /* 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 * all actual samples as the code in gst_qt_mux_add_buffer() does it with
* initial buffer directly, not with last_buf */ * 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; guint64 offset = qtpad->tc_pos;
GstQTMuxClass *qtmux_klass = (GstQTMuxClass *) (G_OBJECT_GET_CLASS (qtmux)); 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; return GST_FLOW_OK;
g_assert (qtpad->tc_pos != -1); 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) if (!pad->trak->is_video)
return ret; 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; return ret;
if (buf == NULL || (pad->tc_trak != NULL && pad->tc_pos == -1)) 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: case PROP_START_GAP_THRESHOLD:
g_value_set_uint64 (value, qtmux->start_gap_threshold); g_value_set_uint64 (value, qtmux->start_gap_threshold);
break; break;
case PROP_FORCE_CREATE_TIMECODE_TRAK:
g_value_set_boolean (value, qtmux->force_create_timecode_trak);
break;
default: default:
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
break; break;
@ -6748,6 +6765,11 @@ gst_qt_mux_set_property (GObject * object,
case PROP_START_GAP_THRESHOLD: case PROP_START_GAP_THRESHOLD:
qtmux->start_gap_threshold = g_value_get_uint64 (value); qtmux->start_gap_threshold = g_value_get_uint64 (value);
break; 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: default:
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
break; break;

View file

@ -313,6 +313,8 @@ struct _GstQTMux
GstClockTime start_gap_threshold; GstClockTime start_gap_threshold;
gboolean force_create_timecode_trak;
/* for request pad naming */ /* for request pad naming */
guint video_pads, audio_pads, subtitle_pads, caption_pads; guint video_pads, audio_pads, subtitle_pads, caption_pads;
}; };