/* GStreamer * Copyright (C) <2016> Vivia Nikolaidou * * 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. */ #include #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); fr = (tc->config.fps_n + (tc->config.fps_d >> 1)) / tc->config.fps_d; if (tc->hours >= 24) return FALSE; if (tc->minutes >= 60) return FALSE; if (tc->seconds >= 60) return FALSE; if (tc->config.fps_d == 0) return FALSE; if (tc->frames >= fr && (tc->config.fps_n != 0 || tc->config.fps_d != 1)) return FALSE; if (tc->config.fps_d == 1001) { if (tc->config.fps_n != 30000 && tc->config.fps_n != 60000 && tc->config.fps_n != 24000) return FALSE; } else if (tc->config.fps_n % tc->config.fps_d != 0) { return FALSE; } 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: #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: the #GDateTime representation of @tc. * * 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); g_return_val_if_fail (tc->config.latest_daily_jam != NULL, NULL); ret = g_date_time_ref (tc->config.latest_daily_jam); if (ret == 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); g_date_time_unref (ret); return NULL; } if (tc->config.fps_n == 0 && tc->config.fps_d == 1) { gchar *tc_str = gst_video_time_code_to_string (tc); GST_WARNING ("Asked to convert time code %s to GDateTime, but its framerate is unknown", tc_str); g_free (tc_str); g_date_time_unref (ret); return NULL; } 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: 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. * * 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) { GDateTime *jam; guint64 frames; gboolean add_a_frame = FALSE; 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); /* 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); g_return_if_fail (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); if (tc->config.fps_n == 0 && tc->config.fps_d == 1) { gchar *tc_str = gst_video_time_code_to_string (tc); GST_WARNING ("Asked to calculate nsec since daily jam of time code %s, but its framerate is unknown", tc_str); g_free (tc_str); return -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 { GST_ERROR ("Unsupported drop frame rate %u/%u", tc->config.fps_n, tc->config.fps_d); return -1; } return tc->frames + (ff_nom * tc->seconds) + (ff_minutes * tc->minutes) + dropframe_multiplier * ((gint) (tc->minutes / 10)) + (ff_hours * tc->hours); } 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.fps_d != 1) GST_WARNING ("Unsupported frame rate %u/%u, results may be wrong", tc->config.fps_n, tc->config.fps_d); } 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 { GST_ERROR ("Unsupported drop frame rate %u/%u", tc->config.fps_n, tc->config.fps_d); return; } 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 { 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; g_assert (min_new < 60); g_assert (sec_new < 60); g_assert (frames_new < ff_nom); tc->hours = h_new; tc->minutes = min_new; tc->seconds = sec_new; tc->frames = frames_new; } /** * gst_video_time_code_compare: * @tc1: a #GstVideoTimeCode * @tc2: another #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 #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 || !gst_video_time_code_is_valid (tc)) 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: a new #GstVideoTimeCode from the given string * * 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 || 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 { 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. * * Returns: the #GVideoTimeCode 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 (tc, fps_n, fps_d, dt, flags, field_count); 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: 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. * * 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: A new #GstVideoTimeCode with @tc_inter 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: a new #GstVideoTimeCodeInterval from the given string * * 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); }