| /* 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. |
| */ |
| |
| #include "gstvideotimecode.h" |
| |
| G_DEFINE_BOXED_TYPE (GstVideoTimeCode, gst_video_time_code, |
| (GBoxedCopyFunc) gst_video_time_code_copy, |
| (GBoxedFreeFunc) gst_video_time_code_free); |
| |
| /** |
| * 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) |
| { |
| g_return_val_if_fail (tc != NULL, FALSE); |
| |
| 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 > tc->config.fps_n / tc->config.fps_d) |
| && (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) |
| return FALSE; |
| } else if (tc->config.fps_n % tc->config.fps_d != 0) { |
| 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; |
| |
| g_return_val_if_fail (gst_video_time_code_is_valid (tc), NULL); |
| |
| /* 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: #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); |
| 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); |
| 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_nsec_since_daily_jam: |
| * @tc: a #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 #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); |
| g_assert (tc->hours <= 24); |
| g_assert (tc->minutes < 60); |
| g_assert (tc->seconds < 60); |
| g_assert (tc->frames <= tc->config.fps_n / tc->config.fps_d); |
| |
| 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 #GstVideoTimeCode |
| * |
| * Adds one frame to @tc . |
| * |
| * Since: 1.10 |
| */ |
| void |
| gst_video_time_code_increment_frame (GstVideoTimeCode * tc) |
| { |
| return gst_video_time_code_add_frames (tc, 1); |
| } |
| |
| /** |
| * gst_video_time_code_add_frames: |
| * @tc: a #GstVideoTimeCode |
| * @frames: How many frames to add or subtract |
| * |
| * Adds or subtracts @frames amount of frames to @tc . |
| * |
| * 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)); |
| g_assert (tc->hours <= 24); |
| g_assert (tc->minutes < 60); |
| g_assert (tc->seconds < 60); |
| g_assert (tc->frames <= tc->config.fps_n / tc->config.fps_d); |
| |
| 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. |
| * |
| * 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. |
| * |
| * 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; |
| } |
| |
| /** |
| * 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. |
| * |
| * 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; |
| |
| g_return_if_fail (gst_video_time_code_is_valid (tc)); |
| } |
| |
| /** |
| * 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); |
| } |