1 /*
2  * GStreamer
3  * Copyright (C) 2016 Vivia Nikolaidou <vivia@toolsonair.com>
4  *
5  * gsttimecodestamper.c
6  *
7  * This library is free software; you can redistribute it and/or
8  * modify it under the terms of the GNU Library General Public
9  * License as published by the Free Software Foundation; either
10  * version 2 of the License, or (at your option) any later version.
11  *
12  * This library is distributed in the hope that it will be useful,
13  * but WITHOUT ANY WARRANTY; without even the implied warranty of
14  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
15  * Library General Public License for more details.
16  *
17  * You should have received a copy of the GNU Library General Public
18  * License along with this library; if not, write to the
19  * Free Software Foundation, Inc., 51 Franklin St, Fifth Floor,
20  * Boston, MA 02110-1301, USA.
21  */
22 
23 /**
24  * SECTION:element-timecodestamper
25  * @title: timecodestamper
26  * @short_description: Attach a timecode into incoming video frames
27  *
28  * This element attaches a timecode into every incoming video frame. It starts
29  * counting from the stream time of each segment start, which it converts into
30  * a timecode.
31  *
32  * ## Example launch line
33  * |[
34  * gst-launch-1.0 videotestsrc ! timecodestamper ! autovideosink
35  * ]|
36  *
37  */
38 
39 #ifdef HAVE_CONFIG_H
40 #include "config.h"
41 #endif
42 
43 #include "gsttimecodestamper.h"
44 
45 #include <gst/gst.h>
46 #include <gst/video/video.h>
47 #include <stdlib.h>
48 #include <string.h>
49 
50 GST_DEBUG_CATEGORY_STATIC (timecodestamper_debug);
51 #define GST_CAT_DEFAULT timecodestamper_debug
52 
53 /* GstTimeCodeStamper properties */
54 enum
55 {
56   PROP_0,
57   PROP_OVERRIDE_EXISTING,
58   PROP_DROP_FRAME,
59   PROP_DAILY_JAM,
60   PROP_POST_MESSAGES,
61   PROP_FIRST_TIMECODE,
62   PROP_FIRST_NOW
63 };
64 
65 #define DEFAULT_OVERRIDE_EXISTING FALSE
66 #define DEFAULT_DROP_FRAME FALSE
67 #define DEFAULT_DAILY_JAM NULL
68 #define DEFAULT_POST_MESSAGES FALSE
69 #define DEFAULT_FIRST_NOW FALSE
70 
71 static GstStaticPadTemplate gst_timecodestamper_src_template =
72 GST_STATIC_PAD_TEMPLATE ("src",
73     GST_PAD_SRC,
74     GST_PAD_ALWAYS,
75     GST_STATIC_CAPS ("video/x-raw")
76     );
77 
78 static GstStaticPadTemplate gst_timecodestamper_sink_template =
79 GST_STATIC_PAD_TEMPLATE ("sink",
80     GST_PAD_SINK,
81     GST_PAD_ALWAYS,
82     GST_STATIC_CAPS ("video/x-raw")
83     );
84 
85 static void gst_timecodestamper_set_property (GObject * object, guint prop_id,
86     const GValue * value, GParamSpec * pspec);
87 static void gst_timecodestamper_get_property (GObject * object, guint prop_id,
88     GValue * value, GParamSpec * pspec);
89 static void gst_timecodestamper_dispose (GObject * object);
90 static gboolean gst_timecodestamper_sink_event (GstBaseTransform * trans,
91     GstEvent * event);
92 static GstFlowReturn gst_timecodestamper_transform_ip (GstBaseTransform *
93     vfilter, GstBuffer * buffer);
94 static gboolean gst_timecodestamper_stop (GstBaseTransform * trans);
95 
96 G_DEFINE_TYPE (GstTimeCodeStamper, gst_timecodestamper,
97     GST_TYPE_BASE_TRANSFORM);
98 
99 static void
gst_timecodestamper_class_init(GstTimeCodeStamperClass * klass)100 gst_timecodestamper_class_init (GstTimeCodeStamperClass * klass)
101 {
102   GObjectClass *gobject_class = (GObjectClass *) klass;
103   GstElementClass *element_class = (GstElementClass *) klass;
104   GstBaseTransformClass *trans_class = (GstBaseTransformClass *) klass;
105 
106   GST_DEBUG_CATEGORY_INIT (timecodestamper_debug, "timecodestamper", 0,
107       "timecodestamper");
108   gst_element_class_set_static_metadata (element_class, "Timecode stamper",
109       "Filter/Video", "Attaches a timecode meta into each video frame",
110       "Vivia Nikolaidou <vivia@toolsonair.com");
111 
112   gobject_class->set_property = gst_timecodestamper_set_property;
113   gobject_class->get_property = gst_timecodestamper_get_property;
114   gobject_class->dispose = gst_timecodestamper_dispose;
115 
116   g_object_class_install_property (gobject_class, PROP_OVERRIDE_EXISTING,
117       g_param_spec_boolean ("override-existing", "Override existing timecode",
118           "If set to true, any existing timecode will be overridden",
119           DEFAULT_OVERRIDE_EXISTING,
120           G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
121   g_object_class_install_property (gobject_class, PROP_DROP_FRAME,
122       g_param_spec_boolean ("drop-frame", "Override existing timecode",
123           "Use drop-frame timecodes for 29.97 and 59.94 FPS",
124           DEFAULT_DROP_FRAME, G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
125   g_object_class_install_property (gobject_class, PROP_DAILY_JAM,
126       g_param_spec_boxed ("daily-jam",
127           "Daily jam",
128           "The daily jam of the timecode",
129           G_TYPE_DATE_TIME, G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
130   g_object_class_install_property (gobject_class, PROP_POST_MESSAGES,
131       g_param_spec_boolean ("post-messages", "Post element message",
132           "Post element message containing the current timecode",
133           DEFAULT_POST_MESSAGES, G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
134   g_object_class_install_property (gobject_class, PROP_FIRST_TIMECODE,
135       g_param_spec_boxed ("first-timecode",
136           "Timecode at the first frame",
137           "If set, take this timecode for the first frame and increment from "
138           "it. Only the values itself are taken, flags and frame rate are "
139           "always determined by timecodestamper itself. "
140           "If unset (and to-now is also not set), the timecode will start at 0",
141           GST_TYPE_VIDEO_TIME_CODE,
142           G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
143   g_object_class_install_property (gobject_class, PROP_FIRST_NOW,
144       g_param_spec_boolean ("first-timecode-to-now",
145           "Sets first timecode to system time",
146           "If true and first-timecode is unset, set it to system time "
147           "automatically when the first media segment is received.",
148           DEFAULT_FIRST_NOW, G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
149 
150   gst_element_class_add_pad_template (element_class,
151       gst_static_pad_template_get (&gst_timecodestamper_sink_template));
152   gst_element_class_add_pad_template (element_class,
153       gst_static_pad_template_get (&gst_timecodestamper_src_template));
154 
155   trans_class->sink_event = GST_DEBUG_FUNCPTR (gst_timecodestamper_sink_event);
156   trans_class->stop = GST_DEBUG_FUNCPTR (gst_timecodestamper_stop);
157 
158   trans_class->transform_ip =
159       GST_DEBUG_FUNCPTR (gst_timecodestamper_transform_ip);
160 }
161 
162 static void
gst_timecodestamper_init(GstTimeCodeStamper * timecodestamper)163 gst_timecodestamper_init (GstTimeCodeStamper * timecodestamper)
164 {
165   timecodestamper->override_existing = DEFAULT_OVERRIDE_EXISTING;
166   timecodestamper->drop_frame = DEFAULT_DROP_FRAME;
167   timecodestamper->current_tc = gst_video_time_code_new_empty ();
168   timecodestamper->first_tc = NULL;
169   timecodestamper->current_tc->config.latest_daily_jam = DEFAULT_DAILY_JAM;
170   timecodestamper->post_messages = DEFAULT_POST_MESSAGES;
171   timecodestamper->first_tc_now = DEFAULT_FIRST_NOW;
172 }
173 
174 static void
gst_timecodestamper_dispose(GObject * object)175 gst_timecodestamper_dispose (GObject * object)
176 {
177   GstTimeCodeStamper *timecodestamper = GST_TIME_CODE_STAMPER (object);
178 
179   if (timecodestamper->current_tc != NULL) {
180     gst_video_time_code_free (timecodestamper->current_tc);
181     timecodestamper->current_tc = NULL;
182   }
183 
184   if (timecodestamper->first_tc != NULL) {
185     gst_video_time_code_free (timecodestamper->first_tc);
186     timecodestamper->first_tc = NULL;
187   }
188 
189   G_OBJECT_CLASS (gst_timecodestamper_parent_class)->dispose (object);
190 }
191 
192 static void
gst_timecodestamper_set_property(GObject * object,guint prop_id,const GValue * value,GParamSpec * pspec)193 gst_timecodestamper_set_property (GObject * object, guint prop_id,
194     const GValue * value, GParamSpec * pspec)
195 {
196   GstTimeCodeStamper *timecodestamper = GST_TIME_CODE_STAMPER (object);
197 
198   switch (prop_id) {
199     case PROP_OVERRIDE_EXISTING:
200       timecodestamper->override_existing = g_value_get_boolean (value);
201       break;
202     case PROP_DROP_FRAME:
203       timecodestamper->drop_frame = g_value_get_boolean (value);
204       break;
205     case PROP_DAILY_JAM:
206       if (timecodestamper->current_tc->config.latest_daily_jam)
207         g_date_time_unref (timecodestamper->current_tc->
208             config.latest_daily_jam);
209       timecodestamper->current_tc->config.latest_daily_jam =
210           g_value_dup_boxed (value);
211       break;
212     case PROP_POST_MESSAGES:
213       timecodestamper->post_messages = g_value_get_boolean (value);
214       break;
215     case PROP_FIRST_TIMECODE:
216       if (timecodestamper->first_tc)
217         gst_video_time_code_free (timecodestamper->first_tc);
218       timecodestamper->first_tc = g_value_dup_boxed (value);
219       break;
220     case PROP_FIRST_NOW:
221       timecodestamper->first_tc_now = g_value_get_boolean (value);
222       break;
223     default:
224       G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
225       break;
226   }
227 }
228 
229 static void
gst_timecodestamper_get_property(GObject * object,guint prop_id,GValue * value,GParamSpec * pspec)230 gst_timecodestamper_get_property (GObject * object, guint prop_id,
231     GValue * value, GParamSpec * pspec)
232 {
233   GstTimeCodeStamper *timecodestamper = GST_TIME_CODE_STAMPER (object);
234 
235   switch (prop_id) {
236     case PROP_OVERRIDE_EXISTING:
237       g_value_set_boolean (value, timecodestamper->override_existing);
238       break;
239     case PROP_DROP_FRAME:
240       g_value_set_boolean (value, timecodestamper->drop_frame);
241       break;
242     case PROP_DAILY_JAM:
243       g_value_set_boxed (value,
244           timecodestamper->current_tc->config.latest_daily_jam);
245       break;
246     case PROP_POST_MESSAGES:
247       g_value_set_boolean (value, timecodestamper->post_messages);
248       break;
249     case PROP_FIRST_TIMECODE:
250       g_value_set_boxed (value, timecodestamper->first_tc);
251       break;
252     case PROP_FIRST_NOW:
253       g_value_set_boolean (value, timecodestamper->first_tc_now);
254       break;
255     default:
256       G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
257       break;
258   }
259 }
260 
261 static void
gst_timecodestamper_set_drop_frame(GstTimeCodeStamper * timecodestamper)262 gst_timecodestamper_set_drop_frame (GstTimeCodeStamper * timecodestamper)
263 {
264   if (timecodestamper->drop_frame && timecodestamper->vinfo.fps_d == 1001 &&
265       (timecodestamper->vinfo.fps_n == 30000 ||
266           timecodestamper->vinfo.fps_n == 60000))
267     timecodestamper->current_tc->config.flags |=
268         GST_VIDEO_TIME_CODE_FLAGS_DROP_FRAME;
269   else
270     timecodestamper->current_tc->config.flags &=
271         ~GST_VIDEO_TIME_CODE_FLAGS_DROP_FRAME;
272 }
273 
274 static gboolean
gst_timecodestamper_stop(GstBaseTransform * trans)275 gst_timecodestamper_stop (GstBaseTransform * trans)
276 {
277   GstTimeCodeStamper *timecodestamper = GST_TIME_CODE_STAMPER (trans);
278 
279   gst_video_info_init (&timecodestamper->vinfo);
280 
281   return TRUE;
282 }
283 
284 /* Must be called with object lock */
285 static void
gst_timecodestamper_reset_timecode(GstTimeCodeStamper * timecodestamper)286 gst_timecodestamper_reset_timecode (GstTimeCodeStamper * timecodestamper)
287 {
288   GDateTime *jam = NULL;
289 
290   if (timecodestamper->first_tc &&
291       timecodestamper->first_tc->config.latest_daily_jam)
292     jam = g_date_time_ref (timecodestamper->first_tc->config.latest_daily_jam);
293   else if (timecodestamper->current_tc->config.latest_daily_jam)
294     jam =
295         g_date_time_ref (timecodestamper->current_tc->config.latest_daily_jam);
296   gst_video_time_code_clear (timecodestamper->current_tc);
297   /* FIXME: What if the buffer doesn't contain both top and bottom fields? */
298   gst_video_time_code_init (timecodestamper->current_tc,
299       timecodestamper->vinfo.fps_n,
300       timecodestamper->vinfo.fps_d,
301       jam,
302       timecodestamper->vinfo.interlace_mode ==
303       GST_VIDEO_INTERLACE_MODE_PROGRESSIVE ? 0 :
304       GST_VIDEO_TIME_CODE_FLAGS_INTERLACED, 0, 0, 0, 0, 0);
305   if (jam)
306     g_date_time_unref (jam);
307   if (timecodestamper->first_tc) {
308     timecodestamper->current_tc->hours = timecodestamper->first_tc->hours;
309     timecodestamper->current_tc->minutes = timecodestamper->first_tc->minutes;
310     timecodestamper->current_tc->seconds = timecodestamper->first_tc->seconds;
311     timecodestamper->current_tc->frames = timecodestamper->first_tc->frames;
312     timecodestamper->current_tc->field_count =
313         timecodestamper->first_tc->field_count;
314   }
315   gst_timecodestamper_set_drop_frame (timecodestamper);
316 }
317 
318 static gboolean
gst_timecodestamper_sink_event(GstBaseTransform * trans,GstEvent * event)319 gst_timecodestamper_sink_event (GstBaseTransform * trans, GstEvent * event)
320 {
321   gboolean ret = FALSE;
322   GstTimeCodeStamper *timecodestamper = GST_TIME_CODE_STAMPER (trans);
323 
324   GST_DEBUG_OBJECT (trans, "received event %" GST_PTR_FORMAT, event);
325   switch (GST_EVENT_TYPE (event)) {
326     case GST_EVENT_SEGMENT:
327     {
328       GstSegment segment;
329       guint64 frames;
330       gchar *tc_str;
331       gboolean notify = FALSE;
332 
333       GST_OBJECT_LOCK (timecodestamper);
334 
335       gst_event_copy_segment (event, &segment);
336       if (segment.format != GST_FORMAT_TIME) {
337         GST_OBJECT_UNLOCK (timecodestamper);
338         GST_ERROR_OBJECT (timecodestamper, "Invalid segment format");
339         return FALSE;
340       }
341       if (GST_VIDEO_INFO_FORMAT (&timecodestamper->vinfo) ==
342           GST_VIDEO_FORMAT_UNKNOWN) {
343         GST_ERROR_OBJECT (timecodestamper,
344             "Received segment event without caps");
345         GST_OBJECT_UNLOCK (timecodestamper);
346         return FALSE;
347       }
348 
349       if (timecodestamper->first_tc_now && !timecodestamper->first_tc) {
350         GDateTime *dt = g_date_time_new_now_local ();
351         GstVideoTimeCode *tc;
352 
353         gst_timecodestamper_set_drop_frame (timecodestamper);
354 
355         tc = gst_video_time_code_new_from_date_time_full
356             (timecodestamper->vinfo.fps_n, timecodestamper->vinfo.fps_d, dt,
357             timecodestamper->current_tc->config.flags, 0);
358 
359         g_date_time_unref (dt);
360 
361         if (!tc) {
362           GST_ERROR_OBJECT (timecodestamper,
363               "Can't convert current time to a timecode");
364           GST_OBJECT_UNLOCK (timecodestamper);
365           return FALSE;
366         }
367 
368         timecodestamper->first_tc = tc;
369         notify = TRUE;
370       }
371 
372       frames =
373           gst_util_uint64_scale (segment.time, timecodestamper->vinfo.fps_n,
374           timecodestamper->vinfo.fps_d * GST_SECOND);
375       gst_timecodestamper_reset_timecode (timecodestamper);
376       gst_video_time_code_add_frames (timecodestamper->current_tc, frames);
377       GST_DEBUG_OBJECT (timecodestamper,
378           "Got %" G_GUINT64_FORMAT " frames when segment time is %"
379           GST_TIME_FORMAT, frames, GST_TIME_ARGS (segment.time));
380       tc_str = gst_video_time_code_to_string (timecodestamper->current_tc);
381       GST_DEBUG_OBJECT (timecodestamper, "New timecode is %s", tc_str);
382       g_free (tc_str);
383       GST_OBJECT_UNLOCK (timecodestamper);
384       if (notify)
385         g_object_notify (G_OBJECT (timecodestamper), "first-timecode");
386       break;
387     }
388     case GST_EVENT_CAPS:
389     {
390       GstCaps *caps;
391 
392       GST_OBJECT_LOCK (timecodestamper);
393       gst_event_parse_caps (event, &caps);
394       if (!gst_video_info_from_caps (&timecodestamper->vinfo, caps)) {
395         GST_OBJECT_UNLOCK (timecodestamper);
396         return FALSE;
397       }
398       gst_timecodestamper_reset_timecode (timecodestamper);
399       GST_OBJECT_UNLOCK (timecodestamper);
400       break;
401     }
402     default:
403       break;
404   }
405   ret =
406       GST_BASE_TRANSFORM_CLASS (gst_timecodestamper_parent_class)->sink_event
407       (trans, event);
408   return ret;
409 }
410 
411 static gboolean
remove_timecode_meta(GstBuffer * buffer,GstMeta ** meta,gpointer user_data)412 remove_timecode_meta (GstBuffer * buffer, GstMeta ** meta, gpointer user_data)
413 {
414   if (meta && *meta && (*meta)->info->api == GST_VIDEO_TIME_CODE_META_API_TYPE) {
415     *meta = NULL;
416   }
417 
418   return TRUE;
419 }
420 
421 static GstFlowReturn
gst_timecodestamper_transform_ip(GstBaseTransform * vfilter,GstBuffer * buffer)422 gst_timecodestamper_transform_ip (GstBaseTransform * vfilter,
423     GstBuffer * buffer)
424 {
425   GstTimeCodeStamper *timecodestamper = GST_TIME_CODE_STAMPER (vfilter);
426   GstVideoTimeCodeMeta *tc_meta;
427   GstVideoTimeCode *tc;
428 
429   GST_OBJECT_LOCK (timecodestamper);
430   tc_meta = gst_buffer_get_video_time_code_meta (buffer);
431   if (tc_meta && !timecodestamper->override_existing) {
432     GST_OBJECT_UNLOCK (timecodestamper);
433     tc = gst_video_time_code_copy (&tc_meta->tc);
434     goto beach;
435   } else if (timecodestamper->override_existing) {
436     gst_buffer_foreach_meta (buffer, remove_timecode_meta, NULL);
437   }
438 
439   gst_buffer_add_video_time_code_meta (buffer, timecodestamper->current_tc);
440   tc = gst_video_time_code_copy (timecodestamper->current_tc);
441   gst_video_time_code_increment_frame (timecodestamper->current_tc);
442   GST_OBJECT_UNLOCK (timecodestamper);
443 
444 beach:
445   if (timecodestamper->post_messages) {
446     GstClockTime stream_time, running_time, duration;
447     GstStructure *s;
448     GstMessage *msg;
449 
450     running_time =
451         gst_segment_to_running_time (&vfilter->segment, GST_FORMAT_TIME,
452         GST_BUFFER_PTS (buffer));
453     stream_time =
454         gst_segment_to_stream_time (&vfilter->segment, GST_FORMAT_TIME,
455         GST_BUFFER_PTS (buffer));
456     duration =
457         gst_util_uint64_scale_int (GST_SECOND, timecodestamper->vinfo.fps_d,
458         timecodestamper->vinfo.fps_n);
459     s = gst_structure_new ("timecodestamper", "timestamp", G_TYPE_UINT64,
460         GST_BUFFER_PTS (buffer), "stream-time", G_TYPE_UINT64, stream_time,
461         "running-time", G_TYPE_UINT64, running_time, "duration", G_TYPE_UINT64,
462         duration, "timecode", GST_TYPE_VIDEO_TIME_CODE, tc, NULL);
463     msg = gst_message_new_element (GST_OBJECT (timecodestamper), s);
464     gst_element_post_message (GST_ELEMENT (timecodestamper), msg);
465   }
466   gst_video_time_code_free (tc);
467   return GST_FLOW_OK;
468 }
469