mirror of
https://gitlab.freedesktop.org/gstreamer/gstreamer.git
synced 2024-12-27 02:30:35 +00:00
53582b7430
The drop-frame rules are specified in “SMPTE ST 12-3:2016” and are consistent with the traditional ones: “ To minimize fractional time deviation from real time, the first two super-frame numbers (00 and 01) shall be omitted from the count at the start of each minute except minutes 00, 10, 20, 30, 40, and 50. Thus the first eight frame numbers (0 through 7) are omitted from the count at the start of each minute except minutes 00, 10, 20, 30, 40, and 50. ” Where “super-frame” is a group of 4 frames for 120 FPS. Fixes #2797 Part-of: <https://gitlab.freedesktop.org/gstreamer/gstreamer/-/merge_requests/5028>
1132 lines
33 KiB
C
1132 lines
33 KiB
C
/* GStreamer
|
|
* Copyright (C) <2016> Vivia Nikolaidou <vivia@toolsonair.com>
|
|
*
|
|
* This library is free software; you can redistribute it and/or
|
|
* modify it under the terms of the GNU Library General Public
|
|
* License as published by the Free Software Foundation; either
|
|
* version 2 of the License, or (at your option) any later version.
|
|
*
|
|
* This library is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
|
* Library General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU Library General Public
|
|
* License along with this library; if not, write to the
|
|
* Free Software Foundation, Inc., 51 Franklin St, Fifth Floor,
|
|
* Boston, MA 02110-1301, USA.
|
|
*/
|
|
#ifdef HAVE_CONFIG_H
|
|
#include "config.h"
|
|
#endif
|
|
|
|
#include <stdio.h>
|
|
#include "gstvideotimecode.h"
|
|
|
|
static void
|
|
gst_video_time_code_gvalue_to_string (const GValue * tc_val, GValue * str_val);
|
|
static void
|
|
gst_video_time_code_gvalue_from_string (const GValue * str_val,
|
|
GValue * tc_val);
|
|
static gboolean gst_video_time_code_deserialize (GValue * dest,
|
|
const gchar * tc_str);
|
|
static gchar *gst_video_time_code_serialize (const GValue * val);
|
|
|
|
static void
|
|
_init (GType type)
|
|
{
|
|
static GstValueTable table =
|
|
{ 0, (GstValueCompareFunc) gst_video_time_code_compare,
|
|
(GstValueSerializeFunc) gst_video_time_code_serialize,
|
|
(GstValueDeserializeFunc) gst_video_time_code_deserialize
|
|
};
|
|
|
|
table.type = type;
|
|
gst_value_register (&table);
|
|
g_value_register_transform_func (type, G_TYPE_STRING,
|
|
(GValueTransform) gst_video_time_code_gvalue_to_string);
|
|
g_value_register_transform_func (G_TYPE_STRING, type,
|
|
(GValueTransform) gst_video_time_code_gvalue_from_string);
|
|
}
|
|
|
|
G_DEFINE_BOXED_TYPE_WITH_CODE (GstVideoTimeCode, gst_video_time_code,
|
|
(GBoxedCopyFunc) gst_video_time_code_copy,
|
|
(GBoxedFreeFunc) gst_video_time_code_free, _init (g_define_type_id));
|
|
|
|
/**
|
|
* gst_video_time_code_is_valid:
|
|
* @tc: #GstVideoTimeCode to check
|
|
*
|
|
* Returns: whether @tc is a valid timecode (supported frame rate,
|
|
* hours/minutes/seconds/frames not overflowing)
|
|
*
|
|
* Since: 1.10
|
|
*/
|
|
gboolean
|
|
gst_video_time_code_is_valid (const GstVideoTimeCode * tc)
|
|
{
|
|
guint fr;
|
|
|
|
g_return_val_if_fail (tc != NULL, FALSE);
|
|
|
|
if (tc->config.fps_n == 0 || tc->config.fps_d == 0)
|
|
return FALSE;
|
|
|
|
if (tc->hours >= 24)
|
|
return FALSE;
|
|
if (tc->minutes >= 60)
|
|
return FALSE;
|
|
if (tc->seconds >= 60)
|
|
return FALSE;
|
|
|
|
/* We can't have more frames than rounded up frames per second */
|
|
fr = (tc->config.fps_n + (tc->config.fps_d >> 1)) / tc->config.fps_d;
|
|
if (tc->config.fps_d > tc->config.fps_n) {
|
|
guint64 s;
|
|
|
|
if (tc->frames > 0)
|
|
return FALSE;
|
|
/* For less than 1 fps only certain second values are allowed */
|
|
s = tc->seconds + (60 * (tc->minutes + (60 * tc->hours)));
|
|
if ((s * tc->config.fps_n) % tc->config.fps_d != 0)
|
|
return FALSE;
|
|
} else {
|
|
if (tc->frames >= fr && (tc->config.fps_n != 0 || tc->config.fps_d != 1))
|
|
return FALSE;
|
|
}
|
|
|
|
/* We need either a specific X/1001 framerate, or less than 1 FPS,
|
|
* otherwise an integer framerate. */
|
|
if (tc->config.fps_d == 1001) {
|
|
if (tc->config.fps_n != 30000 && tc->config.fps_n != 60000 &&
|
|
tc->config.fps_n != 24000 && tc->config.fps_n != 120000)
|
|
return FALSE;
|
|
} else if (tc->config.fps_n >= tc->config.fps_d
|
|
&& tc->config.fps_n % tc->config.fps_d != 0) {
|
|
return FALSE;
|
|
}
|
|
|
|
/* We support only 30000/1001, 60000/1001, and 120000/1001 (see above) as
|
|
* drop-frame framerates. 24000/1001 is *not* a drop-frame framerate! */
|
|
if (tc->config.flags & GST_VIDEO_TIME_CODE_FLAGS_DROP_FRAME) {
|
|
if (tc->config.fps_d != 1001 || tc->config.fps_n == 24000)
|
|
return FALSE;
|
|
}
|
|
|
|
/* Drop-frame framerates require skipping over the first two
|
|
* timecodes every minute except for every tenth minute in case
|
|
* of 30000/1001, the first four timecodes for 60000/1001,
|
|
* and the first eight timecodes for 120000/1001. */
|
|
if ((tc->config.flags & GST_VIDEO_TIME_CODE_FLAGS_DROP_FRAME) &&
|
|
tc->minutes % 10 && tc->seconds == 0 && tc->frames < fr / 15) {
|
|
return FALSE;
|
|
}
|
|
|
|
return TRUE;
|
|
}
|
|
|
|
/**
|
|
* gst_video_time_code_to_string:
|
|
* @tc: A #GstVideoTimeCode to convert
|
|
*
|
|
* Returns: the SMPTE ST 2059-1:2015 string representation of @tc. That will
|
|
* take the form hh:mm:ss:ff. The last separator (between seconds and frames)
|
|
* may vary:
|
|
*
|
|
* ';' for drop-frame, non-interlaced content and for drop-frame interlaced
|
|
* field 2
|
|
* ',' for drop-frame interlaced field 1
|
|
* ':' for non-drop-frame, non-interlaced content and for non-drop-frame
|
|
* interlaced field 2
|
|
* '.' for non-drop-frame interlaced field 1
|
|
*
|
|
* Since: 1.10
|
|
*/
|
|
gchar *
|
|
gst_video_time_code_to_string (const GstVideoTimeCode * tc)
|
|
{
|
|
gchar *ret;
|
|
gboolean top_dot_present;
|
|
gchar sep;
|
|
|
|
/* Top dot is present for non-interlaced content, and for field 2 in
|
|
* interlaced content */
|
|
top_dot_present =
|
|
!((tc->config.flags & GST_VIDEO_TIME_CODE_FLAGS_INTERLACED) != 0
|
|
&& tc->field_count == 1);
|
|
|
|
if (tc->config.flags & GST_VIDEO_TIME_CODE_FLAGS_DROP_FRAME)
|
|
sep = top_dot_present ? ';' : ',';
|
|
else
|
|
sep = top_dot_present ? ':' : '.';
|
|
|
|
ret =
|
|
g_strdup_printf ("%02d:%02d:%02d%c%02d", tc->hours, tc->minutes,
|
|
tc->seconds, sep, tc->frames);
|
|
|
|
return ret;
|
|
}
|
|
|
|
/**
|
|
* gst_video_time_code_to_date_time:
|
|
* @tc: A valid #GstVideoTimeCode to convert
|
|
*
|
|
* The @tc.config->latest_daily_jam is required to be non-NULL.
|
|
*
|
|
* Returns: (nullable): the #GDateTime representation of @tc or %NULL if @tc
|
|
* has no daily jam.
|
|
*
|
|
* Since: 1.10
|
|
*/
|
|
GDateTime *
|
|
gst_video_time_code_to_date_time (const GstVideoTimeCode * tc)
|
|
{
|
|
GDateTime *ret;
|
|
GDateTime *ret2;
|
|
gdouble add_us;
|
|
|
|
g_return_val_if_fail (gst_video_time_code_is_valid (tc), NULL);
|
|
|
|
if (tc->config.latest_daily_jam == NULL) {
|
|
gchar *tc_str = gst_video_time_code_to_string (tc);
|
|
GST_WARNING
|
|
("Asked to convert time code %s to GDateTime, but its latest daily jam is NULL",
|
|
tc_str);
|
|
g_free (tc_str);
|
|
return NULL;
|
|
}
|
|
|
|
ret = g_date_time_ref (tc->config.latest_daily_jam);
|
|
|
|
gst_util_fraction_to_double (tc->frames * tc->config.fps_d, tc->config.fps_n,
|
|
&add_us);
|
|
if ((tc->config.flags & GST_VIDEO_TIME_CODE_FLAGS_INTERLACED)
|
|
&& tc->field_count == 1) {
|
|
gdouble sub_us;
|
|
|
|
gst_util_fraction_to_double (tc->config.fps_d, 2 * tc->config.fps_n,
|
|
&sub_us);
|
|
add_us -= sub_us;
|
|
}
|
|
|
|
ret2 = g_date_time_add_seconds (ret, add_us + tc->seconds);
|
|
g_date_time_unref (ret);
|
|
ret = g_date_time_add_minutes (ret2, tc->minutes);
|
|
g_date_time_unref (ret2);
|
|
ret2 = g_date_time_add_hours (ret, tc->hours);
|
|
g_date_time_unref (ret);
|
|
|
|
return ret2;
|
|
}
|
|
|
|
/**
|
|
* gst_video_time_code_init_from_date_time:
|
|
* @tc: an uninitialized #GstVideoTimeCode
|
|
* @fps_n: Numerator of the frame rate
|
|
* @fps_d: Denominator of the frame rate
|
|
* @dt: #GDateTime to convert
|
|
* @flags: #GstVideoTimeCodeFlags
|
|
* @field_count: Interlaced video field count
|
|
*
|
|
* The resulting config->latest_daily_jam is set to midnight, and timecode is
|
|
* set to the given time.
|
|
*
|
|
* Will assert on invalid parameters, use gst_video_time_code_init_from_date_time_full()
|
|
* for being able to handle invalid parameters.
|
|
*
|
|
* Since: 1.12
|
|
*/
|
|
void
|
|
gst_video_time_code_init_from_date_time (GstVideoTimeCode * tc,
|
|
guint fps_n, guint fps_d,
|
|
GDateTime * dt, GstVideoTimeCodeFlags flags, guint field_count)
|
|
{
|
|
if (!gst_video_time_code_init_from_date_time_full (tc, fps_n, fps_d, dt,
|
|
flags, field_count))
|
|
g_return_if_fail (gst_video_time_code_is_valid (tc));
|
|
}
|
|
|
|
/**
|
|
* gst_video_time_code_init_from_date_time_full:
|
|
* @tc: a #GstVideoTimeCode
|
|
* @fps_n: Numerator of the frame rate
|
|
* @fps_d: Denominator of the frame rate
|
|
* @dt: #GDateTime to convert
|
|
* @flags: #GstVideoTimeCodeFlags
|
|
* @field_count: Interlaced video field count
|
|
*
|
|
* The resulting config->latest_daily_jam is set to
|
|
* midnight, and timecode is set to the given time.
|
|
*
|
|
* Returns: %TRUE if @tc could be correctly initialized to a valid timecode
|
|
*
|
|
* Since: 1.16
|
|
*/
|
|
gboolean
|
|
gst_video_time_code_init_from_date_time_full (GstVideoTimeCode * tc,
|
|
guint fps_n, guint fps_d,
|
|
GDateTime * dt, GstVideoTimeCodeFlags flags, guint field_count)
|
|
{
|
|
GDateTime *jam;
|
|
|
|
g_return_val_if_fail (tc != NULL, FALSE);
|
|
g_return_val_if_fail (dt != NULL, FALSE);
|
|
g_return_val_if_fail (fps_n != 0 && fps_d != 0, FALSE);
|
|
|
|
gst_video_time_code_clear (tc);
|
|
|
|
jam = g_date_time_new_local (g_date_time_get_year (dt),
|
|
g_date_time_get_month (dt), g_date_time_get_day_of_month (dt), 0, 0, 0.0);
|
|
|
|
if (fps_d > fps_n) {
|
|
guint64 hour, min, sec;
|
|
|
|
sec =
|
|
g_date_time_get_second (dt) + (60 * (g_date_time_get_minute (dt) +
|
|
(60 * g_date_time_get_hour (dt))));
|
|
sec -= (sec * fps_n) % fps_d;
|
|
|
|
min = sec / 60;
|
|
sec = sec % 60;
|
|
hour = min / 60;
|
|
min = min % 60;
|
|
|
|
gst_video_time_code_init (tc, fps_n, fps_d, jam, flags,
|
|
hour, min, sec, 0, field_count);
|
|
} else {
|
|
guint64 frames;
|
|
gboolean add_a_frame = FALSE;
|
|
|
|
/* Note: This might be inaccurate for 1 frame
|
|
* in case we have a drop frame timecode */
|
|
frames =
|
|
gst_util_uint64_scale_round (g_date_time_get_microsecond (dt) *
|
|
G_GINT64_CONSTANT (1000), fps_n, fps_d * GST_SECOND);
|
|
if (G_UNLIKELY (((frames == fps_n) && (fps_d == 1)) ||
|
|
((frames == fps_n / 1000) && (fps_d == 1001)))) {
|
|
/* Avoid invalid timecodes */
|
|
frames--;
|
|
add_a_frame = TRUE;
|
|
}
|
|
|
|
gst_video_time_code_init (tc, fps_n, fps_d, jam, flags,
|
|
g_date_time_get_hour (dt), g_date_time_get_minute (dt),
|
|
g_date_time_get_second (dt), frames, field_count);
|
|
|
|
if (tc->config.flags & GST_VIDEO_TIME_CODE_FLAGS_DROP_FRAME) {
|
|
guint df = (tc->config.fps_n + (tc->config.fps_d >> 1)) /
|
|
(15 * tc->config.fps_d);
|
|
if (tc->minutes % 10 && tc->seconds == 0 && tc->frames < df) {
|
|
tc->frames = df;
|
|
}
|
|
}
|
|
if (add_a_frame)
|
|
gst_video_time_code_increment_frame (tc);
|
|
}
|
|
|
|
g_date_time_unref (jam);
|
|
|
|
return gst_video_time_code_is_valid (tc);
|
|
}
|
|
|
|
/**
|
|
* gst_video_time_code_nsec_since_daily_jam:
|
|
* @tc: a valid #GstVideoTimeCode
|
|
*
|
|
* Returns: how many nsec have passed since the daily jam of @tc.
|
|
*
|
|
* Since: 1.10
|
|
*/
|
|
guint64
|
|
gst_video_time_code_nsec_since_daily_jam (const GstVideoTimeCode * tc)
|
|
{
|
|
guint64 frames, nsec;
|
|
|
|
g_return_val_if_fail (gst_video_time_code_is_valid (tc), -1);
|
|
|
|
frames = gst_video_time_code_frames_since_daily_jam (tc);
|
|
nsec =
|
|
gst_util_uint64_scale (frames, GST_SECOND * tc->config.fps_d,
|
|
tc->config.fps_n);
|
|
|
|
return nsec;
|
|
}
|
|
|
|
/**
|
|
* gst_video_time_code_frames_since_daily_jam:
|
|
* @tc: a valid #GstVideoTimeCode
|
|
*
|
|
* Returns: how many frames have passed since the daily jam of @tc.
|
|
*
|
|
* Since: 1.10
|
|
*/
|
|
guint64
|
|
gst_video_time_code_frames_since_daily_jam (const GstVideoTimeCode * tc)
|
|
{
|
|
guint ff_nom;
|
|
gdouble ff;
|
|
|
|
g_return_val_if_fail (gst_video_time_code_is_valid (tc), -1);
|
|
|
|
gst_util_fraction_to_double (tc->config.fps_n, tc->config.fps_d, &ff);
|
|
if (tc->config.fps_d == 1001) {
|
|
ff_nom = tc->config.fps_n / 1000;
|
|
} else {
|
|
ff_nom = ff;
|
|
}
|
|
if (tc->config.flags & GST_VIDEO_TIME_CODE_FLAGS_DROP_FRAME) {
|
|
/* these need to be truncated to integer: side effect, code looks cleaner
|
|
* */
|
|
guint ff_minutes = 60 * ff;
|
|
guint ff_hours = 3600 * ff;
|
|
/* for 30000/1001 we drop the first 2 frames per minute, for 60000/1001 we
|
|
* drop the first 4 : so we use this number */
|
|
guint dropframe_multiplier;
|
|
|
|
if (tc->config.fps_n == 30000) {
|
|
dropframe_multiplier = 2;
|
|
} else if (tc->config.fps_n == 60000) {
|
|
dropframe_multiplier = 4;
|
|
} else {
|
|
/* already checked by gst_video_time_code_is_valid() */
|
|
g_assert_not_reached ();
|
|
}
|
|
|
|
return tc->frames + (ff_nom * tc->seconds) +
|
|
(ff_minutes * tc->minutes) +
|
|
dropframe_multiplier * ((gint) (tc->minutes / 10)) +
|
|
(ff_hours * tc->hours);
|
|
} else if (tc->config.fps_d > tc->config.fps_n) {
|
|
return gst_util_uint64_scale (tc->seconds + (60 * (tc->minutes +
|
|
(60 * tc->hours))), tc->config.fps_n, tc->config.fps_d);
|
|
} else {
|
|
return tc->frames + (ff_nom * (tc->seconds + (60 * (tc->minutes +
|
|
(60 * tc->hours)))));
|
|
}
|
|
|
|
}
|
|
|
|
/**
|
|
* gst_video_time_code_increment_frame:
|
|
* @tc: a valid #GstVideoTimeCode
|
|
*
|
|
* Adds one frame to @tc.
|
|
*
|
|
* Since: 1.10
|
|
*/
|
|
void
|
|
gst_video_time_code_increment_frame (GstVideoTimeCode * tc)
|
|
{
|
|
gst_video_time_code_add_frames (tc, 1);
|
|
}
|
|
|
|
/**
|
|
* gst_video_time_code_add_frames:
|
|
* @tc: a valid #GstVideoTimeCode
|
|
* @frames: How many frames to add or subtract
|
|
*
|
|
* Adds or subtracts @frames amount of frames to @tc. tc needs to
|
|
* contain valid data, as verified by gst_video_time_code_is_valid().
|
|
*
|
|
* Since: 1.10
|
|
*/
|
|
void
|
|
gst_video_time_code_add_frames (GstVideoTimeCode * tc, gint64 frames)
|
|
{
|
|
guint64 framecount;
|
|
guint64 h_notmod24;
|
|
guint64 h_new, min_new, sec_new, frames_new;
|
|
gdouble ff;
|
|
guint ff_nom;
|
|
/* This allows for better readability than putting G_GUINT64_CONSTANT(60)
|
|
* into a long calculation line */
|
|
const guint64 sixty = 60;
|
|
/* formulas found in SMPTE ST 2059-1:2015 section 9.4.3
|
|
* and adapted for 60/1.001 as well as 30/1.001 */
|
|
|
|
g_return_if_fail (gst_video_time_code_is_valid (tc));
|
|
|
|
gst_util_fraction_to_double (tc->config.fps_n, tc->config.fps_d, &ff);
|
|
if (tc->config.fps_d == 1001) {
|
|
ff_nom = tc->config.fps_n / 1000;
|
|
} else {
|
|
ff_nom = ff;
|
|
}
|
|
|
|
if (tc->config.flags & GST_VIDEO_TIME_CODE_FLAGS_DROP_FRAME) {
|
|
/* these need to be truncated to integer: side effect, code looks cleaner
|
|
* */
|
|
guint ff_minutes = 60 * ff;
|
|
guint ff_hours = 3600 * ff;
|
|
/* a bunch of intermediate variables, to avoid monster code with possible
|
|
* integer overflows */
|
|
guint64 min_new_tmp1, min_new_tmp2, min_new_tmp3, min_new_denom;
|
|
/* for 30000/1001 we drop the first 2 frames per minute, for 60000/1001 we
|
|
* drop the first 4 : so we use this number */
|
|
guint dropframe_multiplier;
|
|
|
|
if (tc->config.fps_n == 30000) {
|
|
dropframe_multiplier = 2;
|
|
} else if (tc->config.fps_n == 60000) {
|
|
dropframe_multiplier = 4;
|
|
} else {
|
|
/* already checked by gst_video_time_code_is_valid() */
|
|
g_assert_not_reached ();
|
|
}
|
|
|
|
framecount =
|
|
frames + tc->frames + (ff_nom * tc->seconds) +
|
|
(ff_minutes * tc->minutes) +
|
|
dropframe_multiplier * ((gint) (tc->minutes / 10)) +
|
|
(ff_hours * tc->hours);
|
|
h_notmod24 = gst_util_uint64_scale_int (framecount, 1, ff_hours);
|
|
|
|
min_new_denom = sixty * ff_nom;
|
|
min_new_tmp1 = (framecount - (h_notmod24 * ff_hours)) / min_new_denom;
|
|
min_new_tmp2 = framecount + dropframe_multiplier * min_new_tmp1;
|
|
min_new_tmp1 =
|
|
(framecount - (h_notmod24 * ff_hours)) / (sixty * 10 * ff_nom);
|
|
min_new_tmp3 =
|
|
dropframe_multiplier * min_new_tmp1 + (h_notmod24 * ff_hours);
|
|
min_new =
|
|
gst_util_uint64_scale_int (min_new_tmp2 - min_new_tmp3, 1,
|
|
min_new_denom);
|
|
|
|
sec_new =
|
|
(guint64) ((framecount - (ff_minutes * min_new) -
|
|
dropframe_multiplier * ((gint) (min_new / 10)) -
|
|
(ff_hours * h_notmod24)) / ff_nom);
|
|
|
|
frames_new =
|
|
framecount - (ff_nom * sec_new) - (ff_minutes * min_new) -
|
|
(dropframe_multiplier * ((gint) (min_new / 10))) -
|
|
(ff_hours * h_notmod24);
|
|
} else if (tc->config.fps_d > tc->config.fps_n) {
|
|
frames_new =
|
|
frames + gst_util_uint64_scale (tc->seconds + (60 * (tc->minutes +
|
|
(60 * tc->hours))), tc->config.fps_n, tc->config.fps_d);
|
|
sec_new =
|
|
gst_util_uint64_scale (frames_new, tc->config.fps_d, tc->config.fps_n);
|
|
frames_new = 0;
|
|
min_new = sec_new / 60;
|
|
sec_new = sec_new % 60;
|
|
h_notmod24 = min_new / 60;
|
|
min_new = min_new % 60;
|
|
} else {
|
|
framecount =
|
|
frames + tc->frames + (ff_nom * (tc->seconds + (sixty * (tc->minutes +
|
|
(sixty * tc->hours)))));
|
|
h_notmod24 =
|
|
gst_util_uint64_scale_int (framecount, 1, ff_nom * sixty * sixty);
|
|
min_new =
|
|
gst_util_uint64_scale_int ((framecount -
|
|
(ff_nom * sixty * sixty * h_notmod24)), 1, (ff_nom * sixty));
|
|
sec_new =
|
|
gst_util_uint64_scale_int ((framecount - (ff_nom * sixty * (min_new +
|
|
(sixty * h_notmod24)))), 1, ff_nom);
|
|
frames_new =
|
|
framecount - (ff_nom * (sec_new + sixty * (min_new +
|
|
(sixty * h_notmod24))));
|
|
if (frames_new > ff_nom)
|
|
frames_new = 0;
|
|
}
|
|
|
|
h_new = h_notmod24 % 24;
|
|
|
|
/* The calculations above should always give correct results */
|
|
g_assert (min_new < 60);
|
|
g_assert (sec_new < 60);
|
|
g_assert (frames_new < ff_nom || (ff_nom == 0 && frames_new == 0));
|
|
|
|
tc->hours = h_new;
|
|
tc->minutes = min_new;
|
|
tc->seconds = sec_new;
|
|
tc->frames = frames_new;
|
|
}
|
|
|
|
/**
|
|
* gst_video_time_code_compare:
|
|
* @tc1: a valid #GstVideoTimeCode
|
|
* @tc2: another valid #GstVideoTimeCode
|
|
*
|
|
* Compares @tc1 and @tc2. If both have latest daily jam information, it is
|
|
* taken into account. Otherwise, it is assumed that the daily jam of both
|
|
* @tc1 and @tc2 was at the same time. Both time codes must be valid.
|
|
*
|
|
* Returns: 1 if @tc1 is after @tc2, -1 if @tc1 is before @tc2, 0 otherwise.
|
|
*
|
|
* Since: 1.10
|
|
*/
|
|
gint
|
|
gst_video_time_code_compare (const GstVideoTimeCode * tc1,
|
|
const GstVideoTimeCode * tc2)
|
|
{
|
|
g_return_val_if_fail (gst_video_time_code_is_valid (tc1), -1);
|
|
g_return_val_if_fail (gst_video_time_code_is_valid (tc2), -1);
|
|
|
|
if (tc1->config.latest_daily_jam == NULL
|
|
|| tc2->config.latest_daily_jam == NULL) {
|
|
guint64 nsec1, nsec2;
|
|
#ifndef GST_DISABLE_GST_DEBUG
|
|
gchar *str1, *str2;
|
|
|
|
str1 = gst_video_time_code_to_string (tc1);
|
|
str2 = gst_video_time_code_to_string (tc2);
|
|
GST_INFO
|
|
("Comparing time codes %s and %s, but at least one of them has no "
|
|
"latest daily jam information. Assuming they started together",
|
|
str1, str2);
|
|
g_free (str1);
|
|
g_free (str2);
|
|
#endif
|
|
if (tc1->hours > tc2->hours) {
|
|
return 1;
|
|
} else if (tc1->hours < tc2->hours) {
|
|
return -1;
|
|
}
|
|
if (tc1->minutes > tc2->minutes) {
|
|
return 1;
|
|
} else if (tc1->minutes < tc2->minutes) {
|
|
return -1;
|
|
}
|
|
if (tc1->seconds > tc2->seconds) {
|
|
return 1;
|
|
} else if (tc1->seconds < tc2->seconds) {
|
|
return -1;
|
|
}
|
|
|
|
nsec1 =
|
|
gst_util_uint64_scale (GST_SECOND,
|
|
tc1->frames * tc1->config.fps_n, tc1->config.fps_d);
|
|
nsec2 =
|
|
gst_util_uint64_scale (GST_SECOND,
|
|
tc2->frames * tc2->config.fps_n, tc2->config.fps_d);
|
|
if (nsec1 > nsec2) {
|
|
return 1;
|
|
} else if (nsec1 < nsec2) {
|
|
return -1;
|
|
}
|
|
if (tc1->config.flags & GST_VIDEO_TIME_CODE_FLAGS_INTERLACED) {
|
|
if (tc1->field_count > tc2->field_count)
|
|
return 1;
|
|
else if (tc1->field_count < tc2->field_count)
|
|
return -1;
|
|
}
|
|
return 0;
|
|
} else {
|
|
GDateTime *dt1, *dt2;
|
|
gint ret;
|
|
|
|
dt1 = gst_video_time_code_to_date_time (tc1);
|
|
dt2 = gst_video_time_code_to_date_time (tc2);
|
|
|
|
ret = g_date_time_compare (dt1, dt2);
|
|
|
|
g_date_time_unref (dt1);
|
|
g_date_time_unref (dt2);
|
|
|
|
return ret;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* gst_video_time_code_new:
|
|
* @fps_n: Numerator of the frame rate
|
|
* @fps_d: Denominator of the frame rate
|
|
* @latest_daily_jam: The latest daily jam of the #GstVideoTimeCode
|
|
* @flags: #GstVideoTimeCodeFlags
|
|
* @hours: the hours field of #GstVideoTimeCode
|
|
* @minutes: the minutes field of #GstVideoTimeCode
|
|
* @seconds: the seconds field of #GstVideoTimeCode
|
|
* @frames: the frames field of #GstVideoTimeCode
|
|
* @field_count: Interlaced video field count
|
|
*
|
|
* @field_count is 0 for progressive, 1 or 2 for interlaced.
|
|
* @latest_daiy_jam reference is stolen from caller.
|
|
*
|
|
* Returns: a new #GstVideoTimeCode with the given values.
|
|
* The values are not checked for being in a valid range. To see if your
|
|
* timecode actually has valid content, use gst_video_time_code_is_valid().
|
|
*
|
|
* Since: 1.10
|
|
*/
|
|
GstVideoTimeCode *
|
|
gst_video_time_code_new (guint fps_n, guint fps_d, GDateTime * latest_daily_jam,
|
|
GstVideoTimeCodeFlags flags, guint hours, guint minutes, guint seconds,
|
|
guint frames, guint field_count)
|
|
{
|
|
GstVideoTimeCode *tc;
|
|
|
|
tc = g_new0 (GstVideoTimeCode, 1);
|
|
gst_video_time_code_init (tc, fps_n, fps_d, latest_daily_jam, flags, hours,
|
|
minutes, seconds, frames, field_count);
|
|
return tc;
|
|
}
|
|
|
|
/**
|
|
* gst_video_time_code_new_empty:
|
|
*
|
|
* Returns: a new empty, invalid #GstVideoTimeCode
|
|
*
|
|
* Since: 1.10
|
|
*/
|
|
GstVideoTimeCode *
|
|
gst_video_time_code_new_empty (void)
|
|
{
|
|
GstVideoTimeCode *tc;
|
|
|
|
tc = g_new0 (GstVideoTimeCode, 1);
|
|
gst_video_time_code_clear (tc);
|
|
return tc;
|
|
}
|
|
|
|
static void
|
|
gst_video_time_code_gvalue_from_string (const GValue * str_val, GValue * tc_val)
|
|
{
|
|
const gchar *tc_str = g_value_get_string (str_val);
|
|
GstVideoTimeCode *tc;
|
|
|
|
tc = gst_video_time_code_new_from_string (tc_str);
|
|
g_value_take_boxed (tc_val, tc);
|
|
}
|
|
|
|
static void
|
|
gst_video_time_code_gvalue_to_string (const GValue * tc_val, GValue * str_val)
|
|
{
|
|
const GstVideoTimeCode *tc = g_value_get_boxed (tc_val);
|
|
gchar *tc_str;
|
|
|
|
tc_str = gst_video_time_code_to_string (tc);
|
|
g_value_take_string (str_val, tc_str);
|
|
}
|
|
|
|
static gchar *
|
|
gst_video_time_code_serialize (const GValue * val)
|
|
{
|
|
GstVideoTimeCode *tc = g_value_get_boxed (val);
|
|
return gst_video_time_code_to_string (tc);
|
|
}
|
|
|
|
static gboolean
|
|
gst_video_time_code_deserialize (GValue * dest, const gchar * tc_str)
|
|
{
|
|
GstVideoTimeCode *tc = gst_video_time_code_new_from_string (tc_str);
|
|
|
|
if (tc == NULL) {
|
|
return FALSE;
|
|
}
|
|
|
|
g_value_take_boxed (dest, tc);
|
|
return TRUE;
|
|
}
|
|
|
|
/**
|
|
* gst_video_time_code_new_from_string:
|
|
* @tc_str: The string that represents the #GstVideoTimeCode
|
|
*
|
|
* Returns: (nullable): a new #GstVideoTimeCode from the given string or %NULL
|
|
* if the string could not be passed.
|
|
*
|
|
* Since: 1.12
|
|
*/
|
|
GstVideoTimeCode *
|
|
gst_video_time_code_new_from_string (const gchar * tc_str)
|
|
{
|
|
GstVideoTimeCode *tc;
|
|
guint hours, minutes, seconds, frames;
|
|
|
|
if (sscanf (tc_str, "%02u:%02u:%02u:%02u", &hours, &minutes, &seconds,
|
|
&frames)
|
|
== 4
|
|
|| sscanf (tc_str, "%02u:%02u:%02u.%02u", &hours, &minutes, &seconds,
|
|
&frames)
|
|
== 4) {
|
|
tc = gst_video_time_code_new (0, 1, NULL, GST_VIDEO_TIME_CODE_FLAGS_NONE,
|
|
hours, minutes, seconds, frames, 0);
|
|
|
|
return tc;
|
|
} else if (sscanf (tc_str, "%02u:%02u:%02u;%02u", &hours, &minutes, &seconds,
|
|
&frames)
|
|
== 4 || sscanf (tc_str, "%02u:%02u:%02u,%02u", &hours, &minutes, &seconds,
|
|
&frames)
|
|
== 4) {
|
|
tc = gst_video_time_code_new (0, 1, NULL,
|
|
GST_VIDEO_TIME_CODE_FLAGS_DROP_FRAME, hours, minutes, seconds, frames,
|
|
0);
|
|
|
|
return tc;
|
|
} else {
|
|
GST_ERROR ("Warning: Could not parse timecode %s. "
|
|
"Please input a timecode in the form 00:00:00:00", tc_str);
|
|
return NULL;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* gst_video_time_code_new_from_date_time:
|
|
* @fps_n: Numerator of the frame rate
|
|
* @fps_d: Denominator of the frame rate
|
|
* @dt: #GDateTime to convert
|
|
* @flags: #GstVideoTimeCodeFlags
|
|
* @field_count: Interlaced video field count
|
|
*
|
|
* The resulting config->latest_daily_jam is set to
|
|
* midnight, and timecode is set to the given time.
|
|
*
|
|
* This might return a completely invalid timecode, use
|
|
* gst_video_time_code_new_from_date_time_full() to ensure
|
|
* that you would get %NULL instead in that case.
|
|
*
|
|
* Returns: the #GstVideoTimeCode representation of @dt.
|
|
*
|
|
* Since: 1.12
|
|
*/
|
|
GstVideoTimeCode *
|
|
gst_video_time_code_new_from_date_time (guint fps_n, guint fps_d,
|
|
GDateTime * dt, GstVideoTimeCodeFlags flags, guint field_count)
|
|
{
|
|
GstVideoTimeCode *tc;
|
|
tc = gst_video_time_code_new_empty ();
|
|
gst_video_time_code_init_from_date_time_full (tc, fps_n, fps_d, dt, flags,
|
|
field_count);
|
|
return tc;
|
|
}
|
|
|
|
/**
|
|
* gst_video_time_code_new_from_date_time_full:
|
|
* @fps_n: Numerator of the frame rate
|
|
* @fps_d: Denominator of the frame rate
|
|
* @dt: #GDateTime to convert
|
|
* @flags: #GstVideoTimeCodeFlags
|
|
* @field_count: Interlaced video field count
|
|
*
|
|
* The resulting config->latest_daily_jam is set to
|
|
* midnight, and timecode is set to the given time.
|
|
*
|
|
* Returns: (nullable): the #GstVideoTimeCode representation of @dt, or %NULL if
|
|
* no valid timecode could be created.
|
|
*
|
|
* Since: 1.16
|
|
*/
|
|
GstVideoTimeCode *
|
|
gst_video_time_code_new_from_date_time_full (guint fps_n, guint fps_d,
|
|
GDateTime * dt, GstVideoTimeCodeFlags flags, guint field_count)
|
|
{
|
|
GstVideoTimeCode *tc;
|
|
tc = gst_video_time_code_new_empty ();
|
|
if (!gst_video_time_code_init_from_date_time_full (tc, fps_n, fps_d, dt,
|
|
flags, field_count)) {
|
|
gst_video_time_code_free (tc);
|
|
return NULL;
|
|
}
|
|
return tc;
|
|
}
|
|
|
|
/**
|
|
* gst_video_time_code_init:
|
|
* @tc: a #GstVideoTimeCode
|
|
* @fps_n: Numerator of the frame rate
|
|
* @fps_d: Denominator of the frame rate
|
|
* @latest_daily_jam: (allow-none): The latest daily jam of the #GstVideoTimeCode
|
|
* @flags: #GstVideoTimeCodeFlags
|
|
* @hours: the hours field of #GstVideoTimeCode
|
|
* @minutes: the minutes field of #GstVideoTimeCode
|
|
* @seconds: the seconds field of #GstVideoTimeCode
|
|
* @frames: the frames field of #GstVideoTimeCode
|
|
* @field_count: Interlaced video field count
|
|
*
|
|
* @field_count is 0 for progressive, 1 or 2 for interlaced.
|
|
* @latest_daiy_jam reference is stolen from caller.
|
|
*
|
|
* Initializes @tc with the given values.
|
|
* The values are not checked for being in a valid range. To see if your
|
|
* timecode actually has valid content, use gst_video_time_code_is_valid().
|
|
*
|
|
* Since: 1.10
|
|
*/
|
|
void
|
|
gst_video_time_code_init (GstVideoTimeCode * tc, guint fps_n, guint fps_d,
|
|
GDateTime * latest_daily_jam, GstVideoTimeCodeFlags flags, guint hours,
|
|
guint minutes, guint seconds, guint frames, guint field_count)
|
|
{
|
|
tc->hours = hours;
|
|
tc->minutes = minutes;
|
|
tc->seconds = seconds;
|
|
tc->frames = frames;
|
|
tc->field_count = field_count;
|
|
tc->config.fps_n = fps_n;
|
|
tc->config.fps_d = fps_d;
|
|
if (latest_daily_jam != NULL)
|
|
tc->config.latest_daily_jam = g_date_time_ref (latest_daily_jam);
|
|
else
|
|
tc->config.latest_daily_jam = NULL;
|
|
tc->config.flags = flags;
|
|
}
|
|
|
|
/**
|
|
* gst_video_time_code_clear:
|
|
* @tc: a #GstVideoTimeCode
|
|
*
|
|
* Initializes @tc with empty/zero/NULL values and frees any memory
|
|
* it might currently use.
|
|
*
|
|
* Since: 1.10
|
|
*/
|
|
void
|
|
gst_video_time_code_clear (GstVideoTimeCode * tc)
|
|
{
|
|
tc->hours = 0;
|
|
tc->minutes = 0;
|
|
tc->seconds = 0;
|
|
tc->frames = 0;
|
|
tc->field_count = 0;
|
|
tc->config.fps_n = 0;
|
|
tc->config.fps_d = 1;
|
|
if (tc->config.latest_daily_jam != NULL)
|
|
g_date_time_unref (tc->config.latest_daily_jam);
|
|
tc->config.latest_daily_jam = NULL;
|
|
tc->config.flags = 0;
|
|
}
|
|
|
|
/**
|
|
* gst_video_time_code_copy:
|
|
* @tc: a #GstVideoTimeCode
|
|
*
|
|
* Returns: a new #GstVideoTimeCode with the same values as @tc.
|
|
*
|
|
* Since: 1.10
|
|
*/
|
|
GstVideoTimeCode *
|
|
gst_video_time_code_copy (const GstVideoTimeCode * tc)
|
|
{
|
|
return gst_video_time_code_new (tc->config.fps_n, tc->config.fps_d,
|
|
tc->config.latest_daily_jam, tc->config.flags, tc->hours, tc->minutes,
|
|
tc->seconds, tc->frames, tc->field_count);
|
|
}
|
|
|
|
/**
|
|
* gst_video_time_code_free:
|
|
* @tc: a #GstVideoTimeCode
|
|
*
|
|
* Frees @tc.
|
|
*
|
|
* Since: 1.10
|
|
*/
|
|
void
|
|
gst_video_time_code_free (GstVideoTimeCode * tc)
|
|
{
|
|
if (tc->config.latest_daily_jam != NULL)
|
|
g_date_time_unref (tc->config.latest_daily_jam);
|
|
|
|
g_free (tc);
|
|
}
|
|
|
|
/**
|
|
* gst_video_time_code_add_interval:
|
|
* @tc: The #GstVideoTimeCode where the diff should be added. This
|
|
* must contain valid timecode values.
|
|
* @tc_inter: The #GstVideoTimeCodeInterval to add to @tc.
|
|
* The interval must contain valid values, except that for drop-frame
|
|
* timecode, it may also contain timecodes which would normally
|
|
* be dropped. These are then corrected to the next reasonable timecode.
|
|
*
|
|
* This makes a component-wise addition of @tc_inter to @tc. For example,
|
|
* adding ("01:02:03:04", "00:01:00:00") will return "01:03:03:04".
|
|
* When it comes to drop-frame timecodes,
|
|
* adding ("00:00:00;00", "00:01:00:00") will return "00:01:00;02"
|
|
* because of drop-frame oddities. However,
|
|
* adding ("00:09:00;02", "00:01:00:00") will return "00:10:00;00"
|
|
* because this time we can have an exact minute.
|
|
*
|
|
* Returns: (nullable): A new #GstVideoTimeCode with @tc_inter added or %NULL
|
|
* if the interval can't be added.
|
|
*
|
|
* Since: 1.12
|
|
*/
|
|
GstVideoTimeCode *
|
|
gst_video_time_code_add_interval (const GstVideoTimeCode * tc,
|
|
const GstVideoTimeCodeInterval * tc_inter)
|
|
{
|
|
GstVideoTimeCode *ret;
|
|
guint frames_to_add;
|
|
guint df;
|
|
gboolean needs_correction;
|
|
|
|
g_return_val_if_fail (gst_video_time_code_is_valid (tc), NULL);
|
|
|
|
ret = gst_video_time_code_new (tc->config.fps_n, tc->config.fps_d,
|
|
tc->config.latest_daily_jam, tc->config.flags, tc_inter->hours,
|
|
tc_inter->minutes, tc_inter->seconds, tc_inter->frames, 0);
|
|
|
|
df = (tc->config.fps_n + (tc->config.fps_d >> 1)) / (tc->config.fps_d * 15);
|
|
|
|
/* Drop-frame compensation: Create a valid timecode from the
|
|
* interval */
|
|
needs_correction = (tc->config.flags & GST_VIDEO_TIME_CODE_FLAGS_DROP_FRAME)
|
|
&& ret->minutes % 10 && ret->seconds == 0 && ret->frames < df;
|
|
if (needs_correction) {
|
|
ret->minutes--;
|
|
ret->seconds = 59;
|
|
ret->frames = df * 14;
|
|
}
|
|
|
|
if (!gst_video_time_code_is_valid (ret)) {
|
|
GST_ERROR ("Unsupported time code interval");
|
|
gst_video_time_code_free (ret);
|
|
return NULL;
|
|
}
|
|
|
|
frames_to_add = gst_video_time_code_frames_since_daily_jam (tc);
|
|
|
|
/* Drop-frame compensation: 00:01:00;00 is falsely interpreted as
|
|
* 00:00:59;28 */
|
|
if (needs_correction) {
|
|
/* User wants us to split at invalid timecodes */
|
|
if (tc->minutes % 10 == 0 && tc->frames <= df) {
|
|
/* Apply compensation every 10th minute: before adding the frames,
|
|
* but only if we are before the "invalid frame" mark */
|
|
frames_to_add += df;
|
|
needs_correction = FALSE;
|
|
}
|
|
}
|
|
gst_video_time_code_add_frames (ret, frames_to_add);
|
|
if (needs_correction && ret->minutes % 10 == 0 && tc->frames > df) {
|
|
gst_video_time_code_add_frames (ret, df);
|
|
}
|
|
|
|
return ret;
|
|
}
|
|
|
|
G_DEFINE_BOXED_TYPE (GstVideoTimeCodeInterval, gst_video_time_code_interval,
|
|
(GBoxedCopyFunc) gst_video_time_code_interval_copy,
|
|
(GBoxedFreeFunc) gst_video_time_code_interval_free);
|
|
|
|
/**
|
|
* gst_video_time_code_interval_new:
|
|
* @hours: the hours field of #GstVideoTimeCodeInterval
|
|
* @minutes: the minutes field of #GstVideoTimeCodeInterval
|
|
* @seconds: the seconds field of #GstVideoTimeCodeInterval
|
|
* @frames: the frames field of #GstVideoTimeCodeInterval
|
|
*
|
|
* Returns: a new #GstVideoTimeCodeInterval with the given values.
|
|
*
|
|
* Since: 1.12
|
|
*/
|
|
GstVideoTimeCodeInterval *
|
|
gst_video_time_code_interval_new (guint hours, guint minutes, guint seconds,
|
|
guint frames)
|
|
{
|
|
GstVideoTimeCodeInterval *tc;
|
|
|
|
tc = g_new0 (GstVideoTimeCodeInterval, 1);
|
|
gst_video_time_code_interval_init (tc, hours, minutes, seconds, frames);
|
|
return tc;
|
|
}
|
|
|
|
/**
|
|
* gst_video_time_code_interval_new_from_string:
|
|
* @tc_inter_str: The string that represents the #GstVideoTimeCodeInterval
|
|
*
|
|
* @tc_inter_str must only have ":" as separators.
|
|
*
|
|
* Returns: (nullable): a new #GstVideoTimeCodeInterval from the given string
|
|
* or %NULL if the string could not be passed.
|
|
*
|
|
* Since: 1.12
|
|
*/
|
|
GstVideoTimeCodeInterval *
|
|
gst_video_time_code_interval_new_from_string (const gchar * tc_inter_str)
|
|
{
|
|
GstVideoTimeCodeInterval *tc;
|
|
guint hours, minutes, seconds, frames;
|
|
|
|
if (sscanf (tc_inter_str, "%02u:%02u:%02u:%02u", &hours, &minutes, &seconds,
|
|
&frames)
|
|
== 4
|
|
|| sscanf (tc_inter_str, "%02u:%02u:%02u;%02u", &hours, &minutes,
|
|
&seconds, &frames)
|
|
== 4
|
|
|| sscanf (tc_inter_str, "%02u:%02u:%02u.%02u", &hours, &minutes,
|
|
&seconds, &frames)
|
|
== 4
|
|
|| sscanf (tc_inter_str, "%02u:%02u:%02u,%02u", &hours, &minutes,
|
|
&seconds, &frames)
|
|
== 4) {
|
|
tc = gst_video_time_code_interval_new (hours, minutes, seconds, frames);
|
|
|
|
return tc;
|
|
} else {
|
|
GST_ERROR ("Warning: Could not parse timecode %s. "
|
|
"Please input a timecode in the form 00:00:00:00", tc_inter_str);
|
|
return NULL;
|
|
}
|
|
|
|
}
|
|
|
|
/**
|
|
* gst_video_time_code_interval_init:
|
|
* @tc: a #GstVideoTimeCodeInterval
|
|
* @hours: the hours field of #GstVideoTimeCodeInterval
|
|
* @minutes: the minutes field of #GstVideoTimeCodeInterval
|
|
* @seconds: the seconds field of #GstVideoTimeCodeInterval
|
|
* @frames: the frames field of #GstVideoTimeCodeInterval
|
|
*
|
|
* Initializes @tc with the given values.
|
|
*
|
|
* Since: 1.12
|
|
*/
|
|
void
|
|
gst_video_time_code_interval_init (GstVideoTimeCodeInterval * tc, guint hours,
|
|
guint minutes, guint seconds, guint frames)
|
|
{
|
|
tc->hours = hours;
|
|
tc->minutes = minutes;
|
|
tc->seconds = seconds;
|
|
tc->frames = frames;
|
|
}
|
|
|
|
/**
|
|
* gst_video_time_code_interval_clear:
|
|
* @tc: a #GstVideoTimeCodeInterval
|
|
*
|
|
* Initializes @tc with empty/zero/NULL values.
|
|
*
|
|
* Since: 1.12
|
|
*/
|
|
void
|
|
gst_video_time_code_interval_clear (GstVideoTimeCodeInterval * tc)
|
|
{
|
|
tc->hours = 0;
|
|
tc->minutes = 0;
|
|
tc->seconds = 0;
|
|
tc->frames = 0;
|
|
}
|
|
|
|
/**
|
|
* gst_video_time_code_interval_copy:
|
|
* @tc: a #GstVideoTimeCodeInterval
|
|
*
|
|
* Returns: a new #GstVideoTimeCodeInterval with the same values as @tc.
|
|
*
|
|
* Since: 1.12
|
|
*/
|
|
GstVideoTimeCodeInterval *
|
|
gst_video_time_code_interval_copy (const GstVideoTimeCodeInterval * tc)
|
|
{
|
|
return gst_video_time_code_interval_new (tc->hours, tc->minutes,
|
|
tc->seconds, tc->frames);
|
|
}
|
|
|
|
/**
|
|
* gst_video_time_code_interval_free:
|
|
* @tc: a #GstVideoTimeCodeInterval
|
|
*
|
|
* Frees @tc.
|
|
*
|
|
* Since: 1.12
|
|
*/
|
|
void
|
|
gst_video_time_code_interval_free (GstVideoTimeCodeInterval * tc)
|
|
{
|
|
g_free (tc);
|
|
}
|