command-line-formatter: Add track management to timeline description
[platform/upstream/gstreamer.git] / ges / ges-command-line-formatter.c
1 /* GStreamer Editing Services
2  *
3  * Copyright (C) <2015> Thibault Saunier <tsaunier@gnome.org>
4  *
5  * This library is free software; you can redistribute it and/or
6  * modify it under the terms of the GNU Library General Public
7  * License as published by the Free Software Foundation; either
8  * version 2 of the License, or (at your option) any later version.
9  *
10  * This library is distributed in the hope that it will be useful,
11  * but WITHOUT ANY WARRANTY; without even the implied warranty of
12  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
13  * Library General Public License for more details.
14  *
15  * You should have received a copy of the GNU Library General Public
16  * License along with this library; if not, write to the
17  * Free Software Foundation, Inc., 59 Temple Place - Suite 330,
18  * Boston, MA 02111-1307, USA.
19  */
20 #ifdef HAVE_CONFIG_H
21 #include "config.h"
22 #endif
23
24 #include "ges-command-line-formatter.h"
25
26 #include "ges/ges-structured-interface.h"
27 #include "ges-structure-parser.h"
28 #include "ges-internal.h"
29 #define YY_NO_UNISTD_H
30 #include "ges-parse-lex.h"
31
32 struct _GESCommandLineFormatterPrivate
33 {
34   gpointer dummy;
35 };
36
37
38 G_DEFINE_TYPE_WITH_PRIVATE (GESCommandLineFormatter, ges_command_line_formatter,
39     GES_TYPE_FORMATTER);
40
41 static gboolean
42 _ges_command_line_formatter_add_clip (GESTimeline * timeline,
43     GstStructure * structure, GError ** error);
44 static gboolean
45 _ges_command_line_formatter_add_effect (GESTimeline * timeline,
46     GstStructure * structure, GError ** error);
47 static gboolean
48 _ges_command_line_formatter_add_test_clip (GESTimeline * timeline,
49     GstStructure * structure, GError ** error);
50 static gboolean
51 _ges_command_line_formatter_add_title_clip (GESTimeline * timeline,
52     GstStructure * structure, GError ** error);
53 static gboolean
54 _ges_command_line_formatter_add_track (GESTimeline * timeline,
55     GstStructure * structure, GError ** error);
56
57 typedef struct
58 {
59   const gchar *long_name;
60   const gchar *short_name;
61   GType type;
62   const gchar *new_name;
63   const gchar *desc;
64 } Property;
65
66 // Currently Clip has the most properties.. adapt as needed
67 #define MAX_PROPERTIES 8
68 typedef struct
69 {
70   const gchar *long_name;
71   gchar short_name;
72   ActionFromStructureFunc callback;
73   const gchar *synopsis;
74   const gchar *description;
75   const gchar *examples;
76   /* The first property must be the ID on the command line */
77   Property properties[MAX_PROPERTIES];
78 } GESCommandLineOption;
79
80 /*  *INDENT-OFF* */
81 static GESCommandLineOption options[] = {
82   {"clip", 'c', (ActionFromStructureFunc) _ges_command_line_formatter_add_clip,
83     "<clip uri>",
84     "Adds a clip in the timeline. "
85     "See documentation for the --track-types option to ges-launch-1.0, as it "
86     " will affect the result of this command.",
87     "    ges-launch-1.0 +clip /path/to/media\n\n"
88     "This will simply play the sample from its beginning to its end.\n\n"
89     "    ges-launch-1.0 +clip /path/to/media inpoint=4.0\n\n"
90     "Assuming 'media' is a 10 second long media sample, this will play the sample\n"
91     "from the 4th second to the 10th, resulting in a 6-seconds long playback.\n\n"
92     "    ges-launch-1.0 +clip /path/to/media inpoint=4.0 duration=2.0 start=4.0\n\n"
93     "Assuming \"media\" is an audio video sample longer than 6 seconds, this will play\n"
94     "a black frame and silence for 4 seconds, then the sample from its 4th second to\n"
95     "its sixth second, resulting in a 6-seconds long playback.\n\n"
96     "    ges-launch-1.0 --track-types=audio +clip /path/to/media\n\n"
97     "Assuming \"media\" is an audio video sample, this will only play the audio of the\n"
98     "sample in its entirety.\n\n"
99     "    ges-launch-1.0 +clip /path/to/media1 layer=1 set-alpha 0.9 +clip /path/to/media2 layer=0\n\n"
100     "Assume media1 and media2 both contain audio and video and last for 10 seconds.\n\n"
101     "This will first add media1 in a new layer of \"priority\" 1, thus implicitly\n"
102     "creating a layer of \"priority\" 0, the start of the clip will be 0 as no clip\n"
103     "had been added in that layer before.\n\n"
104     "It will then add media2 in the layer of \"priority\" 0 which was created\n"
105     "previously, the start of this new clip will also be 0 as no clip has been added\n"
106     "in this layer before.\n\n"
107     "Both clips will thus overlap on two layers for 10 seconds.\n\n"
108     "The \"alpha\" property of the second clip will finally be set to a value of 0.9.\n\n"
109     "All this will result in a 10 seconds playback, where media2 is barely visible\n"
110     "through media1, which is nearly opaque. If alpha was set to 0.5, both clips\n"
111     "would be equally visible, and if it was set to 0.0, media1 would be invisible\n"
112     "and media2 completely opaque.\n",
113     {
114       {
115         "uri", "u", 0, "asset-id",
116         "The URI of the media file."
117       },
118       {
119         "name", "n", 0, NULL,
120         "The name of the clip, can be used as an ID later."
121       },
122       {
123         "start", "s",GST_TYPE_CLOCK_TIME, NULL,
124         "The starting position of the clip in the timeline."
125       },
126       {
127         "duration", "d", GST_TYPE_CLOCK_TIME, NULL,
128         "The duration of the clip."
129       },
130       {
131         "inpoint", "i", GST_TYPE_CLOCK_TIME, NULL,
132         "The inpoint of the clip (time in the input file to start playing from)."
133       },
134       {
135         "track-types", "tt", 0, NULL,
136         "The type of the tracks where the clip should be used (audio or video or audio+video)."
137       },
138       {
139         "layer", "l", 0, NULL,
140         "The priority of the layer into which the clip should be added."
141       },
142       {NULL, 0, 0, NULL, FALSE},
143     },
144   },
145   {"effect", 'e', (ActionFromStructureFunc) _ges_command_line_formatter_add_effect,
146     "<effect bin description>",
147     "Adds an effect as specified by 'bin-description', similar to gst-launch-style"
148     " pipeline description, without setting properties (see `set-<property-name>` for information"
149     " about how to set properties).",
150     "    ges-launch-1.0 +clip /path/to/media +effect \"agingtv\"\n\n"
151     "This will apply the agingtv effect to \"media\" and play it back.",
152     {
153       {
154         "bin-description", "d", 0, "asset-id",
155         "gst-launch style bin description."
156       },
157       {
158         "element-name", "e", 0, NULL,
159         "The name of the element to apply the effect on."
160       },
161       {
162         "inpoint", "i", GST_TYPE_CLOCK_TIME, NULL,
163         "Implies that the effect has 'internal content'"
164         "(see [ges_track_element_set_has_internal_source](ges_track_element_set_has_internal_source))",
165       },
166       {
167         "name", "n", 0, "child-name",
168         "The name to be given to the effect."
169       },
170       {NULL, NULL, 0, NULL, FALSE},
171     },
172   },
173   {"test-clip", 0, (ActionFromStructureFunc) _ges_command_line_formatter_add_test_clip,
174     "<test clip pattern>", "Add a test clip in the timeline.",
175     NULL,
176     {
177       {
178         "pattern", "p", 0, NULL,
179         "The testsource pattern name."
180       },
181       {
182         "name", "n", 0, NULL,
183         "The name of the clip, can be used as an ID later."
184       },
185       {
186         "start", "s", GST_TYPE_CLOCK_TIME, NULL,
187         "The starting position of the clip in the timeline."
188       },
189       {
190         "duration", "d", GST_TYPE_CLOCK_TIME, NULL,
191         "The duration of the clip."
192       },
193       {
194         "inpoint", "i", GST_TYPE_CLOCK_TIME, NULL,
195         "The inpoint of the clip (time in the input file to start playing)."
196       },
197       {
198         "layer", "l", 0, NULL,
199         "The priority of the layer into which the clip should be added."
200       },
201       {NULL, 0, 0, NULL, FALSE},
202     },
203   },
204   {"title", 'c', (ActionFromStructureFunc) _ges_command_line_formatter_add_title_clip,
205     "<title text>", "Adds a clip in the timeline.", NULL,
206     {
207       {
208         "text", "t", 0, NULL,
209         "The text to be used as title."
210       },
211       {
212         "name", "n", 0, NULL,
213         "The name of the clip, can be used as an ID later."
214       },
215       {
216         "start", "s",GST_TYPE_CLOCK_TIME, NULL,
217         "The starting position of the clip in the timeline."
218       },
219       {
220         "duration", "d", GST_TYPE_CLOCK_TIME, NULL,
221         "The duration of the clip."
222       },
223       {
224         "inpoint", "i", GST_TYPE_CLOCK_TIME, NULL,
225         "The inpoint of the clip (time in the input file to start playing from)."
226       },
227       {
228         "track-types", "tt", 0, NULL,
229         "The type of the tracks where the clip should be used (audio or video or audio+video)."
230       },
231       {
232         "layer", "l", 0, NULL,
233         "The priority of the layer into which the clip should be added."
234       },
235       {NULL, 0, 0, NULL, FALSE},
236     },
237   },
238   {"track", 't', (ActionFromStructureFunc) _ges_command_line_formatter_add_track,
239     "<track type>", "Adds a track to the timeline.", NULL,
240     {
241       {
242         "restrictions", "r", 0, NULL,
243         "The restriction caps to set on the track."
244       },
245       {NULL, 0, 0, NULL, FALSE},
246     },
247   },
248   {
249     "set-", 0, NULL,
250     "<property name> <value>", "Set a property on the last added element."
251     " Any child property that exists on the previously added element"
252     " can be used as <property name>"
253     "By default, set-<property-name> will lookup the property on the last added"
254     "object.",
255     "    ges-launch-1.0 +clip /path/to/media set-alpha 0.3\n\n"
256     "This will set the alpha property on \"media\" then play it back, assuming \"media\""
257     "contains a video stream.\n\n"
258     "    ges-launch-1.0 +clip /path/to/media +effect \"agingtv\" set-dusts false\n\n"
259     "This will set the \"dusts\" property of the agingtv to false and play the\n"
260     "timeline back.",
261     {
262       {NULL, NULL, 0, NULL, FALSE},
263     },
264   },
265 };
266 /*  *INDENT-ON* */
267
268 /* Should always be in the same order as the options */
269 typedef enum
270 {
271   CLIP,
272   EFFECT,
273   TEST_CLIP,
274   TITLE,
275   TRACK,
276   SET,
277 } GESCommandLineOptionType;
278
279 static gint                     /*  -1: not present, 0: failure, 1: OK */
280 _convert_to_clocktime (GstStructure * structure, const gchar * name,
281     GstClockTime default_value)
282 {
283   gint res = 1;
284   gdouble val;
285   GValue d_val = G_VALUE_INIT, converted = G_VALUE_INIT;
286   GstClockTime timestamp;
287   const GValue *gvalue = gst_structure_get_value (structure, name);
288
289   if (gvalue == NULL) {
290     timestamp = default_value;
291
292     res = -1;
293
294     goto done;
295   }
296
297   if (G_VALUE_TYPE (gvalue) == G_TYPE_STRING) {
298     const gchar *val_string = g_value_get_string (gvalue);
299     /* if starts with an 'f', interpret as a frame number, keep as
300      * a string for now */
301     if (val_string && val_string[0] == 'f')
302       return 1;
303     /* else, try convert to a GstClockTime, or a double */
304     g_value_init (&converted, GST_TYPE_CLOCK_TIME);
305     if (!gst_value_deserialize (&converted, val_string)) {
306       g_value_unset (&converted);
307       g_value_init (&converted, G_TYPE_DOUBLE);
308       if (!gst_value_deserialize (&converted, val_string)) {
309         GST_ERROR ("Could not get timestamp for %s by deserializing %s",
310             name, val_string);
311         goto error;
312       }
313     }
314   } else {
315     g_value_init (&converted, G_VALUE_TYPE (gvalue));
316     g_value_copy (gvalue, &converted);
317   }
318
319   if (G_VALUE_TYPE (&converted) == GST_TYPE_CLOCK_TIME) {
320     timestamp = g_value_get_uint64 (&converted);
321     goto done;
322   }
323
324   g_value_init (&d_val, G_TYPE_DOUBLE);
325
326   if (!g_value_transform (&converted, &d_val)) {
327     GST_ERROR ("Could not get timestamp for %s", name);
328     goto error;
329   }
330
331   val = g_value_get_double ((const GValue *) &d_val);
332   g_value_unset (&d_val);
333
334   if (val == -1.0)
335     timestamp = GST_CLOCK_TIME_NONE;
336   else
337     timestamp = val * GST_SECOND;
338
339 done:
340   gst_structure_set (structure, name, G_TYPE_UINT64, timestamp, NULL);
341   g_value_unset (&converted);
342
343   return res;
344
345 error:
346   g_value_unset (&converted);
347
348   return 0;
349 }
350
351 static gboolean
352 _cleanup_fields (const Property * field_names, GstStructure * structure,
353     GError ** error)
354 {
355   guint i;
356
357   for (i = 0; field_names[i].long_name; i++) {
358     gboolean exists = FALSE;
359
360     /* Move shortly named fields to longname variante */
361     if (gst_structure_has_field (structure, field_names[i].short_name)) {
362       exists = TRUE;
363
364       if (gst_structure_has_field (structure, field_names[i].long_name)) {
365         *error = g_error_new (GES_ERROR, 0, "Using short and long name"
366             " at the same time for property: %s, which one should I use?!",
367             field_names[i].long_name);
368
369         return FALSE;
370       } else {
371         const GValue *val =
372             gst_structure_get_value (structure, field_names[i].short_name);
373
374         gst_structure_set_value (structure, field_names[i].long_name, val);
375         gst_structure_remove_field (structure, field_names[i].short_name);
376       }
377     } else if (gst_structure_has_field (structure, field_names[i].long_name)) {
378       exists = TRUE;
379     }
380
381     if (exists) {
382       if (field_names[i].type == GST_TYPE_CLOCK_TIME) {
383         if (_convert_to_clocktime (structure, field_names[i].long_name, 0) == 0) {
384           *error = g_error_new (GES_ERROR, 0, "Could not convert"
385               " %s to GstClockTime", field_names[i].long_name);
386
387           return FALSE;
388         }
389       }
390     }
391
392     if (field_names[i].new_name
393         && gst_structure_has_field (structure, field_names[i].long_name)) {
394       const GValue *val =
395           gst_structure_get_value (structure, field_names[i].long_name);
396
397       gst_structure_set_value (structure, field_names[i].new_name, val);
398       gst_structure_remove_field (structure, field_names[i].long_name);
399     }
400   }
401
402   return TRUE;
403 }
404
405 static gboolean
406 _ges_command_line_formatter_add_clip (GESTimeline * timeline,
407     GstStructure * structure, GError ** error)
408 {
409   GESProject *proj;
410   GESAsset *asset;
411   if (!_cleanup_fields (options[CLIP].properties, structure, error))
412     return FALSE;
413
414   gst_structure_set (structure, "type", G_TYPE_STRING, "GESUriClip", NULL);
415
416   if (!_ges_add_clip_from_struct (timeline, structure, error))
417     return FALSE;
418
419   proj = GES_PROJECT (ges_extractable_get_asset (GES_EXTRACTABLE (timeline)));
420   asset = _ges_get_asset_from_timeline (timeline, GES_TYPE_URI_CLIP,
421       gst_structure_get_string (structure, "asset-id"), NULL);
422   ges_project_add_asset (proj, asset);
423
424   return TRUE;
425 }
426
427 static gboolean
428 _ges_command_line_formatter_add_test_clip (GESTimeline * timeline,
429     GstStructure * structure, GError ** error)
430 {
431   if (!_cleanup_fields (options[TEST_CLIP].properties, structure, error))
432     return FALSE;
433
434   gst_structure_set (structure, "type", G_TYPE_STRING, "GESTestClip", NULL);
435
436   if (!gst_structure_has_field_typed (structure, "asset-id", G_TYPE_STRING))
437     gst_structure_set (structure, "asset-id", G_TYPE_STRING, "GESTestClip",
438         NULL);
439
440   return _ges_add_clip_from_struct (timeline, structure, error);
441 }
442
443 static gboolean
444 _ges_command_line_formatter_add_title_clip (GESTimeline * timeline,
445     GstStructure * structure, GError ** error)
446 {
447   if (!_cleanup_fields (options[TITLE].properties, structure, error))
448     return FALSE;
449
450   gst_structure_set (structure, "type", G_TYPE_STRING, "GESTitleClip", NULL);
451   gst_structure_set (structure, "asset-id", G_TYPE_STRING, "GESTitleClip",
452       NULL);
453   GST_ERROR ("Structure: %" GST_PTR_FORMAT, structure);
454
455   return _ges_add_clip_from_struct (timeline, structure, error);
456 }
457
458 static gboolean
459 _ges_command_line_formatter_add_track (GESTimeline * timeline,
460     GstStructure * structure, GError ** error)
461 {
462   if (!_cleanup_fields (options[TRACK].properties, structure, error))
463     return FALSE;
464
465   return _ges_add_track_from_struct (timeline, structure, error);
466 }
467
468 static gboolean
469 _ges_command_line_formatter_add_effect (GESTimeline * timeline,
470     GstStructure * structure, GError ** error)
471 {
472   if (!_cleanup_fields (options[EFFECT].properties, structure, error))
473     return FALSE;
474
475   gst_structure_set (structure, "child-type", G_TYPE_STRING, "GESEffect", NULL);
476
477   return _ges_container_add_child_from_struct (timeline, structure, error);
478 }
479
480 gchar *
481 ges_command_line_formatter_get_help (gint nargs, gchar ** commands)
482 {
483   gint i;
484   GString *help = g_string_new (NULL);
485
486   for (i = 0; i < G_N_ELEMENTS (options); i++) {
487     gboolean print = nargs == 0;
488     GESCommandLineOption option = options[i];
489
490     if (!print) {
491       gint j;
492
493       for (j = 0; j < nargs; j++) {
494         gchar *cname = commands[j][0] == '+' ? &commands[j][1] : commands[j];
495
496         if (!g_strcmp0 (cname, option.long_name)) {
497           print = TRUE;
498           break;
499         }
500       }
501     }
502
503     if (print) {
504       gint j;
505
506       gchar *tmp = g_strdup_printf ("  `%s%s` - %s\n",
507           option.properties[0].long_name ? "+" : "",
508           option.long_name, option.synopsis);
509
510       g_string_append (help, tmp);
511       g_string_append (help, "  ");
512       g_string_append (help, "\n\n  ");
513       g_free (tmp);
514
515       for (j = 0; option.description[j] != '\0'; j++) {
516
517         if (j && (j % 80) == 0) {
518           while (option.description[j] != '\0' && option.description[j] != ' ')
519             g_string_append_c (help, option.description[j++]);
520           g_string_append (help, "\n  ");
521           continue;
522         }
523
524         g_string_append_c (help, option.description[j]);
525       }
526       g_string_append_c (help, '\n');
527
528       if (option.properties[0].long_name) {
529         gint j;
530
531         g_string_append (help, "\n  Properties:\n\n");
532
533         for (j = 1; option.properties[j].long_name; j++) {
534           Property prop = option.properties[j];
535           g_string_append_printf (help, "    * `%s`: %s\n", prop.long_name,
536               prop.desc);
537         }
538       }
539       if (option.examples) {
540         gint j;
541         gchar **examples = g_strsplit (option.examples, "\n", -1);
542
543         g_string_append (help, "\n  Examples:\n\n");
544         for (j = 0; examples[j]; j++) {
545           if (examples[j])
546             g_string_append_printf (help, "    %s", examples[j]);
547           g_string_append_c (help, '\n');
548         }
549         g_strfreev (examples);
550       }
551
552       g_string_append_c (help, '\n');
553     }
554   }
555
556   return g_string_free (help, FALSE);
557 }
558
559
560 static gboolean
561 _set_child_property (GESTimeline * timeline, GstStructure * structure,
562     GError ** error)
563 {
564   return _ges_set_child_property_from_struct (timeline, structure, error);
565 }
566
567 #define EXEC(func,structure,error) G_STMT_START { \
568   gboolean res = ((ActionFromStructureFunc)func)(timeline, structure, error); \
569   if (!res) {\
570     GST_ERROR ("Could not execute: %" GST_PTR_FORMAT ", error: %s", structure, (*error)->message); \
571     goto fail; \
572   } \
573 } G_STMT_END
574
575
576 static GESStructureParser *
577 _parse_structures (const gchar * string)
578 {
579   yyscan_t scanner;
580   GESStructureParser *parser = ges_structure_parser_new ();
581
582   priv_ges_parse_yylex_init_extra (parser, &scanner);
583   priv_ges_parse_yy_scan_string (string, scanner);
584   priv_ges_parse_yylex (scanner);
585   priv_ges_parse_yylex_destroy (scanner);
586
587   ges_structure_parser_end_of_file (parser);
588   return parser;
589 }
590
591 static gboolean
592 _can_load (GESFormatter * dummy_formatter, const gchar * string,
593     GError ** error)
594 {
595   gboolean res = FALSE;
596   GESStructureParser *parser;
597
598   if (string == NULL)
599     return FALSE;
600
601   parser = _parse_structures (string);
602
603   if (parser->structures)
604     res = TRUE;
605
606   gst_object_unref (parser);
607
608   return res;
609 }
610
611 static gboolean
612 _load (GESFormatter * self, GESTimeline * timeline, const gchar * string,
613     GError ** error)
614 {
615   guint i;
616   GList *tmp;
617   GError *err;
618   GESStructureParser *parser = _parse_structures (string);
619
620   err = ges_structure_parser_get_error (parser);
621
622   if (err) {
623     if (error)
624       *error = err;
625
626     return FALSE;
627   }
628
629   g_object_set (timeline, "auto-transition", TRUE, NULL);
630
631   /* Here we've finished initializing our timeline, we're
632    * ready to start using it... by solely working with the layer !*/
633   for (tmp = parser->structures; tmp; tmp = tmp->next) {
634     const gchar *name = gst_structure_get_name (tmp->data);
635     if (g_str_has_prefix (name, "set-")) {
636       EXEC (_set_child_property, tmp->data, &err);
637       continue;
638     }
639
640     for (i = 0; i < G_N_ELEMENTS (options); i++) {
641       if (gst_structure_has_name (tmp->data, options[i].long_name)
642           || (strlen (name) == 1 && *name == options[i].short_name)) {
643         EXEC (((ActionFromStructureFunc) options[i].callback), tmp->data, &err);
644       }
645     }
646   }
647
648   gst_object_unref (parser);
649
650   return TRUE;
651
652 fail:
653   gst_object_unref (parser);
654   if (err) {
655     if (error)
656       *error = err;
657   }
658
659   return FALSE;
660 }
661
662 static void
663 ges_command_line_formatter_init (GESCommandLineFormatter * formatter)
664 {
665   formatter->priv = ges_command_line_formatter_get_instance_private (formatter);
666 }
667
668 static void
669 ges_command_line_formatter_finalize (GObject * object)
670 {
671   G_OBJECT_CLASS (ges_command_line_formatter_parent_class)->finalize (object);
672 }
673
674 static void
675 ges_command_line_formatter_class_init (GESCommandLineFormatterClass * klass)
676 {
677   GObjectClass *object_class = G_OBJECT_CLASS (klass);
678   GESFormatterClass *formatter_klass = GES_FORMATTER_CLASS (klass);
679
680   object_class->finalize = ges_command_line_formatter_finalize;
681
682   formatter_klass->can_load_uri = _can_load;
683   formatter_klass->load_from_uri = _load;
684   formatter_klass->rank = GST_RANK_MARGINAL;
685 }