mirror of
https://gitlab.freedesktop.org/gstreamer/gstreamer.git
synced 2024-11-23 10:11:08 +00:00
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:
parent
db69f02dd8
commit
5817c659e6
5 changed files with 116 additions and 19 deletions
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -493,6 +494,34 @@ atom_gmhd_free (AtomGMHD * 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
|
||||
atom_sample_entry_init (SampleTableEntry * se, guint32 type)
|
||||
{
|
||||
|
@ -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,13 +4128,15 @@ 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);
|
||||
|
||||
if (context->flavor == ATOMS_TREE_FLAVOR_MOV) {
|
||||
AtomGMHD *gmhd = trak->mdia.minf.gmhd;
|
||||
|
||||
gmhd = atom_gmhd_new ();
|
||||
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 */
|
||||
|
||||
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;
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
|
|
Loading…
Reference in a new issue