command-line-formatter: Add a way to format timelines using the format
[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 static gboolean
57 _ges_command_line_formatter_add_keyframes (GESTimeline * timeline,
58     GstStructure * structure, GError ** error);
59
60 typedef struct
61 {
62   const gchar *long_name;
63   const gchar *short_name;
64   GType type;
65   const gchar *new_name;
66   const gchar *desc;
67 } Property;
68
69 // Currently Clip has the most properties.. adapt as needed
70 #define MAX_PROPERTIES 8
71 typedef struct
72 {
73   const gchar *long_name;
74   gchar short_name;
75   ActionFromStructureFunc callback;
76   const gchar *synopsis;
77   const gchar *description;
78   const gchar *examples;
79   /* The first property must be the ID on the command line */
80   Property properties[MAX_PROPERTIES];
81 } GESCommandLineOption;
82
83 /*  *INDENT-OFF* */
84 static GESCommandLineOption options[] = {
85   {
86     .long_name = "clip",
87     .short_name='c',
88     .callback=(ActionFromStructureFunc) _ges_command_line_formatter_add_clip,
89     .synopsis="<clip uri>",
90     .description="Adds a clip in the timeline. "
91                  "See documentation for the --track-types option to ges-launch-1.0, as it "
92                  " will affect the result of this command.",
93     .examples="    ges-launch-1.0 +clip /path/to/media\n\n"
94               "This will simply play the sample from its beginning to its end.\n\n"
95               "    ges-launch-1.0 +clip /path/to/media inpoint=4.0\n\n"
96               "Assuming 'media' is a 10 second long media sample, this will play the sample\n"
97               "from the 4th second to the 10th, resulting in a 6-seconds long playback.\n\n"
98               "    ges-launch-1.0 +clip /path/to/media inpoint=4.0 duration=2.0 start=4.0\n\n"
99               "Assuming \"media\" is an audio video sample longer than 6 seconds, this will play\n"
100               "a black frame and silence for 4 seconds, then the sample from its 4th second to\n"
101               "its sixth second, resulting in a 6-seconds long playback.\n\n"
102               "    ges-launch-1.0 --track-types=audio +clip /path/to/media\n\n"
103               "Assuming \"media\" is an audio video sample, this will only play the audio of the\n"
104               "sample in its entirety.\n\n"
105               "    ges-launch-1.0 +clip /path/to/media1 layer=1 set-alpha 0.9 +clip /path/to/media2 layer=0\n\n"
106               "Assume media1 and media2 both contain audio and video and last for 10 seconds.\n\n"
107               "This will first add media1 in a new layer of \"priority\" 1, thus implicitly\n"
108               "creating a layer of \"priority\" 0, the start of the clip will be 0 as no clip\n"
109               "had been added in that layer before.\n\n"
110               "It will then add media2 in the layer of \"priority\" 0 which was created\n"
111               "previously, the start of this new clip will also be 0 as no clip has been added\n"
112               "in this layer before.\n\n"
113               "Both clips will thus overlap on two layers for 10 seconds.\n\n"
114               "The \"alpha\" property of the second clip will finally be set to a value of 0.9.\n\n"
115               "All this will result in a 10 seconds playback, where media2 is barely visible\n"
116               "through media1, which is nearly opaque. If alpha was set to 0.5, both clips\n"
117               "would be equally visible, and if it was set to 0.0, media1 would be invisible\n"
118               "and media2 completely opaque.\n",
119     .properties={
120       {
121         "uri", 0, 0, "asset-id",
122         "The URI of the media file."
123       },
124       {
125         "name", "n", 0, NULL,
126         "The name of the clip, can be used as an ID later."
127       },
128       {
129         "start", "s", GST_TYPE_CLOCK_TIME, NULL,
130         "The starting position of the clip in the timeline."
131       },
132       {
133         "duration", "d", GST_TYPE_CLOCK_TIME, NULL,
134         "The duration of the clip."
135       },
136       {
137         "inpoint", "i", GST_TYPE_CLOCK_TIME, NULL,
138         "The inpoint of the clip (time in the input file to start playing from)."
139       },
140       {
141         "track-types", "tt", 0, NULL,
142         "The type of the tracks where the clip should be used (audio or video or audio+video)."
143       },
144       {
145         "layer", "l", 0, NULL,
146         "The priority of the layer into which the clip should be added."
147       },
148       {NULL, 0, 0, NULL, FALSE},
149     },
150   },
151   {
152     .long_name="effect",
153     .short_name='e',
154     .callback=(ActionFromStructureFunc) _ges_command_line_formatter_add_effect,
155     .synopsis="<effect bin description>",
156     .description="Adds an effect as specified by 'bin-description', similar to gst-launch-style"
157                  " pipeline description, without setting properties (see `set-<property-name>` for information"
158                  " about how to set properties).",
159     .examples="    ges-launch-1.0 +clip /path/to/media +effect \"agingtv\"\n\n"
160               "This will apply the agingtv effect to \"media\" and play it back.",
161     {
162       {
163         "bin-description", "d", 0, "asset-id",
164         "gst-launch style bin description."
165       },
166       {
167         "element-name", "e", 0, NULL,
168         "The name of the element to apply the effect on."
169       },
170       {
171         "inpoint", "i", GST_TYPE_CLOCK_TIME, NULL,
172         "Implies that the effect has 'internal content'"
173         "(see [ges_track_element_set_has_internal_source](ges_track_element_set_has_internal_source))",
174       },
175       {
176         "name", "n", 0, "child-name",
177         "The name to be given to the effect."
178       },
179       {NULL, NULL, 0, NULL, FALSE},
180     },
181   },
182   {
183     .long_name="test-clip",
184     .short_name=0,
185     .callback=(ActionFromStructureFunc) _ges_command_line_formatter_add_test_clip,
186     .synopsis="<test clip pattern>",
187     .description="Add a test clip in the timeline.",
188     .examples=NULL,
189     .properties={
190       {
191         "vpattern", "p", 0, NULL,
192         "The testsource pattern name."
193       },
194       {
195         "name", "n", 0, NULL,
196         "The name of the clip, can be used as an ID later."
197       },
198       {
199         "start", "s", GST_TYPE_CLOCK_TIME, NULL,
200         "The starting position of the clip in the timeline."
201       },
202       {
203         "duration", "d", GST_TYPE_CLOCK_TIME, NULL,
204         "The duration of the clip."
205       },
206       {
207         "inpoint", "i", GST_TYPE_CLOCK_TIME, NULL,
208         "The inpoint of the clip (time in the input file to start playing)."
209       },
210       {
211         "layer", "l", 0, NULL,
212         "The priority of the layer into which the clip should be added."
213       },
214       {NULL, 0, 0, NULL, FALSE},
215     },
216   },
217   {
218     .long_name="title",
219     .short_name='c',
220     .callback=(ActionFromStructureFunc) _ges_command_line_formatter_add_title_clip,
221     .synopsis="<title text>",
222     .description="Adds a clip in the timeline.",
223     .examples=NULL,
224     .properties={
225       {
226         "text", "t", 0, NULL,
227         "The text to be used as title."
228       },
229       {
230         "name", "n", 0, NULL,
231         "The name of the clip, can be used as an ID later."
232       },
233       {
234         "start", "s",GST_TYPE_CLOCK_TIME, NULL,
235         "The starting position of the clip in the timeline."
236       },
237       {
238         "duration", "d", GST_TYPE_CLOCK_TIME, NULL,
239         "The duration of the clip."
240       },
241       {
242         "inpoint", "i", GST_TYPE_CLOCK_TIME, NULL,
243         "The inpoint of the clip (time in the input file to start playing from)."
244       },
245       {
246         "track-types", "tt", 0, NULL,
247         "The type of the tracks where the clip should be used (audio or video or audio+video)."
248       },
249       {
250         "layer", "l", 0, NULL,
251         "The priority of the layer into which the clip should be added."
252       },
253       {NULL, 0, 0, NULL, FALSE},
254     },
255   },
256   {
257     .long_name="track",
258     .short_name='t',
259     .callback=(ActionFromStructureFunc) _ges_command_line_formatter_add_track,
260     .synopsis="<track type>",
261     .description="Adds a track to the timeline.",
262     .examples=NULL,
263     .properties={
264       {"track-type", 0, 0, NULL, NULL},
265       {
266         "restrictions", "r", 0, NULL,
267         "The restriction caps to set on the track."
268       },
269       {NULL, 0, 0, NULL, FALSE},
270     },
271   },
272   {
273     .long_name="keyframes",
274     .short_name='k',
275     .callback=(ActionFromStructureFunc) _ges_command_line_formatter_add_keyframes,
276     .synopsis="<property name>",
277     .description="Adds keyframes for the specified property in the form:\n\n",
278     .examples="    ges-launch-1.0 +test-clip blue d=1.0 +keyframes posx 0=0 1.0=1280 t=direct-absolute +k posy 0=0 1.0=720 t=direct-absolute\n\n"
279               "This add a testclip that will disappear in the bottom right corner",
280     .properties={
281       {"property-name", 0, 0, NULL, NULL},
282       {
283         "binding-type", "t", 0, NULL,
284         "The type of binding to use, eg. 'direct-absolute', 'direct'"
285       },
286       {
287         "interpolation-mode", "m", 0, NULL,
288         "The GstInterpolationMode to user."
289       },
290       {
291         "...", 0, 0, NULL,
292         "The list of keyframe_timestamp=value to be set."
293       },
294       {NULL, 0, 0, NULL, FALSE},
295     },
296   },
297   {
298     .long_name="set-",
299     .short_name=0,
300     .callback=NULL,
301     .synopsis="<property name> <value>",
302     .description="Set a property on the last added element."
303                  " Any child property that exists on the previously added element"
304                  " can be used as <property name>"
305                  "By default, set-<property-name> will lookup the property on the last added"
306                   "object.",
307     .examples="    ges-launch-1.0 +clip /path/to/media set-alpha 0.3\n\n"
308               "This will set the alpha property on \"media\" then play it back, assuming \"media\""
309               "contains a video stream.\n\n"
310               "    ges-launch-1.0 +clip /path/to/media +effect \"agingtv\" set-dusts false\n\n"
311               "This will set the \"dusts\" property of the agingtv to false and play the\n"
312               "timeline back.",
313     .properties={
314       {NULL, 0, 0, NULL, FALSE},
315     },
316   },
317 };
318 /*  *INDENT-ON* */
319
320 /* Should always be in the same order as the options */
321 typedef enum
322 {
323   CLIP,
324   EFFECT,
325   TEST_CLIP,
326   TITLE,
327   TRACK,
328   KEYFRAMES,
329   SET,
330 } GESCommandLineOptionType;
331
332 static gint                     /*  -1: not present, 0: failure, 1: OK */
333 _convert_to_clocktime (GstStructure * structure, const gchar * name,
334     GstClockTime default_value)
335 {
336   gint res = 1;
337   gdouble val;
338   GValue d_val = G_VALUE_INIT, converted = G_VALUE_INIT;
339   GstClockTime timestamp;
340   const GValue *gvalue = gst_structure_get_value (structure, name);
341
342   if (gvalue == NULL) {
343     timestamp = default_value;
344
345     res = -1;
346
347     goto done;
348   }
349
350   if (G_VALUE_TYPE (gvalue) == G_TYPE_STRING) {
351     const gchar *val_string = g_value_get_string (gvalue);
352     /* if starts with an 'f', interpret as a frame number, keep as
353      * a string for now */
354     if (val_string && val_string[0] == 'f')
355       return 1;
356     /* else, try convert to a GstClockTime, or a double */
357     g_value_init (&converted, GST_TYPE_CLOCK_TIME);
358     if (!gst_value_deserialize (&converted, val_string)) {
359       g_value_unset (&converted);
360       g_value_init (&converted, G_TYPE_DOUBLE);
361       if (!gst_value_deserialize (&converted, val_string)) {
362         GST_ERROR ("Could not get timestamp for %s by deserializing %s",
363             name, val_string);
364         goto error;
365       }
366     }
367   } else {
368     g_value_init (&converted, G_VALUE_TYPE (gvalue));
369     g_value_copy (gvalue, &converted);
370   }
371
372   if (G_VALUE_TYPE (&converted) == GST_TYPE_CLOCK_TIME) {
373     timestamp = g_value_get_uint64 (&converted);
374     goto done;
375   }
376
377   g_value_init (&d_val, G_TYPE_DOUBLE);
378
379   if (!g_value_transform (&converted, &d_val)) {
380     GST_ERROR ("Could not get timestamp for %s", name);
381     goto error;
382   }
383
384   val = g_value_get_double ((const GValue *) &d_val);
385   g_value_unset (&d_val);
386
387   if (val == -1.0)
388     timestamp = GST_CLOCK_TIME_NONE;
389   else
390     timestamp = val * GST_SECOND;
391
392 done:
393   gst_structure_set (structure, name, G_TYPE_UINT64, timestamp, NULL);
394   g_value_unset (&converted);
395
396   return res;
397
398 error:
399   g_value_unset (&converted);
400
401   return 0;
402 }
403
404 static gboolean
405 _cleanup_fields (const Property * field_names, GstStructure * structure,
406     GError ** error)
407 {
408   guint i;
409
410   for (i = 0; field_names[i].long_name; i++) {
411     gboolean exists = FALSE;
412
413     /* Move shortly named fields to longname variante */
414     if (field_names[i].short_name &&
415         gst_structure_has_field (structure, field_names[i].short_name)) {
416       exists = TRUE;
417
418       if (gst_structure_has_field (structure, field_names[i].long_name)) {
419         gchar *str_info = gst_structure_serialize (structure, 0);
420
421         *error =
422             g_error_new (GES_ERROR, 0,
423             "Using short (%s) and long name (%s)"
424             " at the same time s in %s, which one should I use?!",
425             field_names[i].short_name, field_names[i].long_name, str_info);
426         g_free (str_info);
427
428         return FALSE;
429       } else {
430         const GValue *val =
431             gst_structure_get_value (structure, field_names[i].short_name);
432
433         gst_structure_set_value (structure, field_names[i].long_name, val);
434         gst_structure_remove_field (structure, field_names[i].short_name);
435       }
436     } else if (gst_structure_has_field (structure, field_names[i].long_name)) {
437       exists = TRUE;
438     }
439
440     if (exists) {
441       if (field_names[i].type == GST_TYPE_CLOCK_TIME) {
442         if (_convert_to_clocktime (structure, field_names[i].long_name, 0) == 0) {
443           *error = g_error_new (GES_ERROR, 0, "Could not convert"
444               " %s to GstClockTime", field_names[i].long_name);
445
446           return FALSE;
447         }
448       }
449     }
450
451     if (field_names[i].new_name
452         && gst_structure_has_field (structure, field_names[i].long_name)) {
453       const GValue *val =
454           gst_structure_get_value (structure, field_names[i].long_name);
455
456       gst_structure_set_value (structure, field_names[i].new_name, val);
457       gst_structure_remove_field (structure, field_names[i].long_name);
458     }
459   }
460
461   return TRUE;
462 }
463
464 static gboolean
465 _ges_command_line_formatter_add_clip (GESTimeline * timeline,
466     GstStructure * structure, GError ** error)
467 {
468   GESProject *proj;
469   GESAsset *asset;
470   if (!_cleanup_fields (options[CLIP].properties, structure, error))
471     return FALSE;
472
473   gst_structure_set (structure, "type", G_TYPE_STRING, "GESUriClip", NULL);
474
475   if (!_ges_add_clip_from_struct (timeline, structure, error))
476     return FALSE;
477
478   proj = GES_PROJECT (ges_extractable_get_asset (GES_EXTRACTABLE (timeline)));
479   asset = _ges_get_asset_from_timeline (timeline, GES_TYPE_URI_CLIP,
480       gst_structure_get_string (structure, "asset-id"), NULL);
481   ges_project_add_asset (proj, asset);
482
483   return TRUE;
484 }
485
486 static gboolean
487 _ges_command_line_formatter_add_test_clip (GESTimeline * timeline,
488     GstStructure * structure, GError ** error)
489 {
490   if (!_cleanup_fields (options[TEST_CLIP].properties, structure, error))
491     return FALSE;
492
493   gst_structure_set (structure, "type", G_TYPE_STRING, "GESTestClip", NULL);
494
495   if (!gst_structure_has_field_typed (structure, "asset-id", G_TYPE_STRING))
496     gst_structure_set (structure, "asset-id", G_TYPE_STRING, "GESTestClip",
497         NULL);
498
499   return _ges_add_clip_from_struct (timeline, structure, error);
500 }
501
502 static gboolean
503 _ges_command_line_formatter_add_title_clip (GESTimeline * timeline,
504     GstStructure * structure, GError ** error)
505 {
506   if (!_cleanup_fields (options[TITLE].properties, structure, error))
507     return FALSE;
508
509   gst_structure_set (structure, "type", G_TYPE_STRING, "GESTitleClip", NULL);
510   gst_structure_set (structure, "asset-id", G_TYPE_STRING, "GESTitleClip",
511       NULL);
512
513   return _ges_add_clip_from_struct (timeline, structure, error);
514 }
515
516 static gboolean
517 _ges_command_line_formatter_add_keyframes (GESTimeline * timeline,
518     GstStructure * structure, GError ** error)
519 {
520   if (!_cleanup_fields (options[KEYFRAMES].properties, structure, error))
521     return FALSE;
522
523   if (!_ges_set_control_source_from_struct (timeline, structure, error))
524     return FALSE;
525
526   return _ges_add_remove_keyframe_from_struct (timeline, structure, error);
527 }
528
529 static gboolean
530 _ges_command_line_formatter_add_track (GESTimeline * timeline,
531     GstStructure * structure, GError ** error)
532 {
533   if (!_cleanup_fields (options[TRACK].properties, structure, error))
534     return FALSE;
535
536   return _ges_add_track_from_struct (timeline, structure, error);
537 }
538
539 static gboolean
540 _ges_command_line_formatter_add_effect (GESTimeline * timeline,
541     GstStructure * structure, GError ** error)
542 {
543   if (!_cleanup_fields (options[EFFECT].properties, structure, error))
544     return FALSE;
545
546   gst_structure_set (structure, "child-type", G_TYPE_STRING, "GESEffect", NULL);
547
548   return _ges_container_add_child_from_struct (timeline, structure, error);
549 }
550
551 gchar *
552 ges_command_line_formatter_get_help (gint nargs, gchar ** commands)
553 {
554   gint i;
555   GString *help = g_string_new (NULL);
556
557   for (i = 0; i < G_N_ELEMENTS (options); i++) {
558     gboolean print = nargs == 0;
559     GESCommandLineOption option = options[i];
560
561     if (!print) {
562       gint j;
563
564       for (j = 0; j < nargs; j++) {
565         gchar *cname = commands[j][0] == '+' ? &commands[j][1] : commands[j];
566
567         if (!g_strcmp0 (cname, option.long_name)) {
568           print = TRUE;
569           break;
570         }
571       }
572     }
573
574     if (print) {
575       gint j;
576
577       gchar *tmp = g_strdup_printf ("  `%s%s` - %s\n",
578           option.properties[0].long_name ? "+" : "",
579           option.long_name, option.synopsis);
580
581       g_string_append (help, tmp);
582       g_string_append (help, "  ");
583       g_string_append (help, "\n\n  ");
584       g_free (tmp);
585
586       for (j = 0; option.description[j] != '\0'; j++) {
587
588         if (j && (j % 80) == 0) {
589           while (option.description[j] != '\0' && option.description[j] != ' ')
590             g_string_append_c (help, option.description[j++]);
591           g_string_append (help, "\n  ");
592           continue;
593         }
594
595         g_string_append_c (help, option.description[j]);
596       }
597       g_string_append_c (help, '\n');
598
599       if (option.properties[0].long_name) {
600         gint j;
601
602         g_string_append (help, "\n  Properties:\n\n");
603
604         for (j = 1; option.properties[j].long_name; j++) {
605           Property prop = option.properties[j];
606           g_string_append_printf (help, "    * `%s`: %s\n", prop.long_name,
607               prop.desc);
608         }
609       }
610       if (option.examples) {
611         gint j;
612         gchar **examples = g_strsplit (option.examples, "\n", -1);
613
614         g_string_append (help, "\n  Examples:\n\n");
615         for (j = 0; examples[j]; j++) {
616           if (examples[j])
617             g_string_append_printf (help, "    %s", examples[j]);
618           g_string_append_c (help, '\n');
619         }
620         g_strfreev (examples);
621       }
622
623       g_string_append_c (help, '\n');
624     }
625   }
626
627   return g_string_free (help, FALSE);
628 }
629
630
631 static gboolean
632 _set_child_property (GESTimeline * timeline, GstStructure * structure,
633     GError ** error)
634 {
635   return _ges_set_child_property_from_struct (timeline, structure, error);
636 }
637
638 #define EXEC(func,structure,error) G_STMT_START { \
639   gboolean res = ((ActionFromStructureFunc)func)(timeline, structure, error); \
640   if (!res) {\
641     GST_ERROR ("Could not execute: %" GST_PTR_FORMAT ", error: %s", structure, (*error)->message); \
642     goto fail; \
643   } \
644 } G_STMT_END
645
646
647 static GESStructureParser *
648 _parse_structures (const gchar * string)
649 {
650   yyscan_t scanner;
651   GESStructureParser *parser = ges_structure_parser_new ();
652
653   priv_ges_parse_yylex_init_extra (parser, &scanner);
654   priv_ges_parse_yy_scan_string (string, scanner);
655   priv_ges_parse_yylex (scanner);
656   priv_ges_parse_yylex_destroy (scanner);
657
658   ges_structure_parser_end_of_file (parser);
659   return parser;
660 }
661
662 /* @uri: (transfer full): */
663 static gchar *
664 get_timeline_desc_from_uri (GstUri * uri)
665 {
666   gchar *res, *path;
667
668   if (!uri)
669     return NULL;
670
671   /* Working around parser requiring a space to begin with */
672   path = gst_uri_get_path (uri);
673   res = g_strconcat (" ", path, NULL);
674   g_free (path);
675
676   gst_uri_unref (uri);
677
678   return res;
679 }
680
681 static gboolean
682 _can_load (GESFormatter * dummy_formatter, const gchar * string,
683     GError ** error)
684 {
685   gboolean res = FALSE;
686   GstUri *uri;
687   const gchar *scheme;
688   gchar *timeline_desc = NULL;
689   GESStructureParser *parser;
690
691   if (string == NULL) {
692     GST_ERROR ("No URI!");
693     return FALSE;
694   }
695
696   uri = gst_uri_from_string (string);
697   if (!uri) {
698     GST_INFO_OBJECT (dummy_formatter, "Wrong uri: %s", string);
699     return FALSE;
700   }
701
702   scheme = gst_uri_get_scheme (uri);
703   if (!g_strcmp0 (scheme, "ges:")) {
704     GST_INFO_OBJECT (dummy_formatter, "Wrong scheme: %s", string);
705     gst_uri_unref (uri);
706
707     return FALSE;
708   }
709
710   timeline_desc = get_timeline_desc_from_uri (uri);
711   parser = _parse_structures (timeline_desc);
712   if (parser->structures)
713     res = TRUE;
714
715   gst_object_unref (parser);
716   g_free (timeline_desc);
717
718   return res;
719 }
720
721 static gboolean
722 _set_project_loaded (GESFormatter * self)
723 {
724   ges_project_set_loaded (self->project, self, NULL);
725   gst_object_unref (self);
726
727   return FALSE;
728 }
729
730 static gboolean
731 _load (GESFormatter * self, GESTimeline * timeline, const gchar * string,
732     GError ** error)
733 {
734   guint i;
735   GList *tmp;
736   GError *err;
737   gchar *timeline_desc =
738       get_timeline_desc_from_uri (gst_uri_from_string (string));
739   GESStructureParser *parser = _parse_structures (timeline_desc);
740
741   g_free (timeline_desc);
742
743   err = ges_structure_parser_get_error (parser);
744
745   if (err) {
746     if (error)
747       *error = err;
748
749     return FALSE;
750   }
751
752   g_object_set (timeline, "auto-transition", TRUE, NULL);
753
754   /* Here we've finished initializing our timeline, we're
755    * ready to start using it... by solely working with the layer !*/
756   for (tmp = parser->structures; tmp; tmp = tmp->next) {
757     const gchar *name = gst_structure_get_name (tmp->data);
758     if (g_str_has_prefix (name, "set-")) {
759       EXEC (_set_child_property, tmp->data, &err);
760       continue;
761     }
762
763     for (i = 0; i < G_N_ELEMENTS (options); i++) {
764       if (gst_structure_has_name (tmp->data, options[i].long_name)
765           || (strlen (name) == 1 && *name == options[i].short_name)) {
766         EXEC (((ActionFromStructureFunc) options[i].callback), tmp->data, &err);
767       }
768     }
769   }
770
771   gst_object_unref (parser);
772
773   ges_idle_add ((GSourceFunc) _set_project_loaded, g_object_ref (self), NULL);
774
775   return TRUE;
776
777 fail:
778   gst_object_unref (parser);
779   if (err) {
780     if (error)
781       *error = err;
782   }
783
784   return FALSE;
785 }
786
787 static void
788 ges_command_line_formatter_init (GESCommandLineFormatter * formatter)
789 {
790   formatter->priv = ges_command_line_formatter_get_instance_private (formatter);
791 }
792
793 static void
794 ges_command_line_formatter_finalize (GObject * object)
795 {
796   G_OBJECT_CLASS (ges_command_line_formatter_parent_class)->finalize (object);
797 }
798
799 static void
800 ges_command_line_formatter_class_init (GESCommandLineFormatterClass * klass)
801 {
802   GObjectClass *object_class = G_OBJECT_CLASS (klass);
803   GESFormatterClass *formatter_klass = GES_FORMATTER_CLASS (klass);
804
805   object_class->finalize = ges_command_line_formatter_finalize;
806
807   formatter_klass->can_load_uri = _can_load;
808   formatter_klass->load_from_uri = _load;
809   formatter_klass->rank = GST_RANK_MARGINAL;
810 }
811
812 /* Copy of GST_ASCII_IS_STRING */
813 #define ASCII_IS_STRING(c) (g_ascii_isalnum((c)) || ((c) == '_') || \
814     ((c) == '-') || ((c) == '+') || ((c) == '/') || ((c) == ':') || \
815     ((c) == '.'))
816
817 static void
818 _sanitize_argument (const gchar * arg, GString * res)
819 {
820   gboolean need_wrap = FALSE;
821   const gchar *tmp_string;
822
823   for (tmp_string = arg; *tmp_string != '\0'; tmp_string++) {
824     if (!ASCII_IS_STRING (*tmp_string) || (*tmp_string == '\n')) {
825       need_wrap = TRUE;
826       break;
827     }
828   }
829
830   if (!need_wrap) {
831     g_string_append (res, arg);
832     return;
833   }
834
835   g_string_append_c (res, '"');
836   while (*arg != '\0') {
837     if (*arg == '"' || *arg == '\\') {
838       g_string_append_c (res, '\\');
839     } else if (*arg == '\n') {
840       g_string_append (res, "\\n");
841       arg++;
842       continue;
843     }
844
845     g_string_append_c (res, *(arg++));
846   }
847   g_string_append_c (res, '"');
848 }
849
850 static gboolean
851 _serialize_control_binding (GESTrackElement * e, const gchar * prop,
852     GString * res)
853 {
854   GstInterpolationMode mode;
855   GstControlSource *source = NULL;
856   GList *timed_values, *tmp;
857   gboolean absolute = FALSE;
858   GstControlBinding *binding = ges_track_element_get_control_binding (e, prop);
859
860   if (!binding)
861     return FALSE;
862
863   if (!GST_IS_DIRECT_CONTROL_BINDING (binding)) {
864     g_warning ("Unsupported control binding type: %s",
865         G_OBJECT_TYPE_NAME (binding));
866     goto done;
867   }
868
869   g_object_get (binding, "control-source", &source,
870       "absolute", &absolute, NULL);
871
872   if (!GST_IS_INTERPOLATION_CONTROL_SOURCE (source)) {
873     g_warning ("Unsupported control source type: %s",
874         G_OBJECT_TYPE_NAME (source));
875     goto done;
876   }
877
878   g_object_get (source, "mode", &mode, NULL);
879   g_string_append_printf (res, " +keyframes %s t=%s",
880       prop, absolute ? "direct-absolute" : "direct");
881
882   if (mode != GST_INTERPOLATION_MODE_LINEAR)
883     g_string_append_printf (res, " mode=%s",
884         g_enum_get_value (g_type_class_peek (GST_TYPE_INTERPOLATION_MODE),
885             mode)->value_nick);
886
887   timed_values =
888       gst_timed_value_control_source_get_all
889       (GST_TIMED_VALUE_CONTROL_SOURCE (source));
890   for (tmp = timed_values; tmp; tmp = tmp->next) {
891     gchar strbuf[G_ASCII_DTOSTR_BUF_SIZE];
892     GstTimedValue *value;
893
894     value = (GstTimedValue *) tmp->data;
895     g_string_append_printf (res, " %f=%s",
896         (gdouble) value->timestamp / (gdouble) GST_SECOND,
897         g_ascii_dtostr (strbuf, G_ASCII_DTOSTR_BUF_SIZE, value->value));
898   }
899   g_list_free (timed_values);
900
901 done:
902   g_clear_object (&source);
903   return TRUE;
904 }
905
906 static void
907 _serialize_object_properties (GObject * object, GESCommandLineOption * option,
908     gboolean children_props, GString * res)
909 {
910   guint n_props, j;
911   GParamSpec *spec, **pspecs;
912   GObjectClass *class = G_OBJECT_GET_CLASS (object);
913   const gchar *ignored_props[] = {
914     "max-duration", "supported-formats", "priority", "video-direction",
915     "is-image", NULL,
916   };
917
918   if (!children_props)
919     pspecs = g_object_class_list_properties (class, &n_props);
920   else {
921     pspecs =
922         ges_timeline_element_list_children_properties (GES_TIMELINE_ELEMENT
923         (object), &n_props);
924     g_assert (GES_IS_TRACK_ELEMENT (object));
925   }
926
927   for (j = 0; j < n_props; j++) {
928     const gchar *name;
929     gchar *value_str = NULL;
930     GValue val = { 0 };
931     gint i;
932
933     spec = pspecs[j];
934     if (!ges_util_can_serialize_spec (spec))
935       continue;
936
937     g_value_init (&val, spec->value_type);
938     if (!children_props)
939       g_object_get_property (object, spec->name, &val);
940     else
941       ges_timeline_element_get_child_property_by_pspec (GES_TIMELINE_ELEMENT
942           (object), spec, &val);
943
944     if (gst_value_compare (g_param_spec_get_default_value (spec),
945             &val) == GST_VALUE_EQUAL) {
946       GST_INFO ("Ignoring %s as it is using the default value", spec->name);
947       goto next;
948     }
949
950     name = spec->name;
951     if (!children_props && !g_strcmp0 (name, "in-point"))
952       name = "inpoint";
953
954     for (i = 0; option->properties[i].long_name; i++) {
955       if (!g_strcmp0 (spec->name, option->properties[i].long_name)) {
956         if (children_props) {
957           name = NULL;
958         } else {
959           name = option->properties[i].short_name;
960           if (option->properties[i].type == GST_TYPE_CLOCK_TIME)
961             value_str =
962                 g_strdup_printf ("%f",
963                 (gdouble) (g_value_get_uint64 (&val) / GST_SECOND));
964         }
965         break;
966       } else if (!g_strcmp0 (spec->name, option->properties[0].long_name)) {
967         name = NULL;
968         break;
969       }
970     }
971
972     for (i = 0; i < G_N_ELEMENTS (ignored_props); i++) {
973       if (!g_strcmp0 (spec->name, ignored_props[i])) {
974         name = NULL;
975         break;
976       }
977     }
978
979     if (!name) {
980       g_free (value_str);
981       continue;
982     }
983
984     if (GES_IS_TRACK_ELEMENT (object) &&
985         _serialize_control_binding (GES_TRACK_ELEMENT (object), name, res)) {
986       g_free (value_str);
987       continue;
988     }
989
990     if (!value_str)
991       value_str = gst_value_serialize (&val);
992
993     g_string_append_printf (res, " %s%s%s",
994         children_props ? "set-" : "", name, children_props ? " " : "=");
995     _sanitize_argument (value_str, res);
996     g_free (value_str);
997
998   next:
999     g_value_unset (&val);
1000   }
1001   g_free (pspecs);
1002 }
1003
1004 static void
1005 _serialize_clip_track_types (GESClip * clip, GESTrackType tt, GString * res)
1006 {
1007   GValue v = G_VALUE_INIT;
1008   gchar *ttype_str;
1009
1010   if (ges_clip_get_supported_formats (clip) == tt)
1011     return;
1012
1013   g_value_init (&v, GES_TYPE_TRACK_TYPE);
1014   g_value_set_flags (&v, ges_clip_get_supported_formats (clip));
1015
1016   ttype_str = gst_value_serialize (&v);
1017
1018   g_string_append_printf (res, " tt=%s", ttype_str);
1019   g_value_reset (&v);
1020   g_free (ttype_str);
1021 }
1022
1023 static void
1024 _serialize_clip_effects (GESClip * clip, GString * res)
1025 {
1026   GList *tmpeffect, *effects;
1027
1028   effects = ges_clip_get_top_effects (clip);
1029   for (tmpeffect = effects; tmpeffect; tmpeffect = tmpeffect->next) {
1030     gchar *bin_desc;
1031
1032     g_object_get (tmpeffect->data, "bin-description", &bin_desc, NULL);
1033
1034     g_string_append_printf (res, " +effect %s", bin_desc);
1035     g_free (bin_desc);
1036   }
1037   g_list_free_full (effects, gst_object_unref);
1038
1039 }
1040
1041 /**
1042  * ges_command_line_formatter_get_timeline_uri:
1043  * @timeline: A GESTimeline to serialize
1044  *
1045  * Since: 1.20
1046  */
1047 gchar *
1048 ges_command_line_formatter_get_timeline_uri (GESTimeline * timeline)
1049 {
1050   gchar *tmpstr;
1051   GList *tmp;
1052   gint i;
1053   GString *res = g_string_new ("ges:");
1054   GESTrackType tt = 0;
1055
1056   if (!timeline)
1057     goto done;
1058
1059   for (tmp = timeline->tracks; tmp; tmp = tmp->next) {
1060     GstCaps *caps, *default_caps;
1061     GESTrack *tmptrack, *track = tmp->data;
1062
1063     if (GES_IS_VIDEO_TRACK (track))
1064       tmptrack = GES_TRACK (ges_video_track_new ());
1065     else if (GES_IS_AUDIO_TRACK (track))
1066       tmptrack = GES_TRACK (ges_audio_track_new ());
1067     else {
1068       g_warning ("Unhandled track type: %s", G_OBJECT_TYPE_NAME (track));
1069       continue;
1070     }
1071
1072     tt |= track->type;
1073
1074     g_string_append_printf (res, " +track %s",
1075         (track->type == GES_TRACK_TYPE_VIDEO) ? "video" : "audio");
1076
1077     default_caps = ges_track_get_restriction_caps (tmptrack);
1078     caps = ges_track_get_restriction_caps (track);
1079     if (!gst_caps_is_equal (caps, default_caps)) {
1080       tmpstr = gst_caps_serialize (caps, 0);
1081
1082       g_string_append (res, " restrictions=");
1083       _sanitize_argument (tmpstr, res);
1084       g_free (tmpstr);
1085     }
1086     gst_caps_unref (default_caps);
1087     gst_caps_unref (caps);
1088     gst_object_unref (tmptrack);
1089   }
1090
1091   for (tmp = timeline->layers, i = 0; tmp; tmp = tmp->next, i++) {
1092     GList *tmpclip, *clips = ges_layer_get_clips (tmp->data);
1093     GList *tmptrackelem;
1094
1095     for (tmpclip = clips; tmpclip; tmpclip = tmpclip->next) {
1096       GESClip *clip = tmpclip->data;
1097       GESCommandLineOption *option = NULL;
1098
1099       if (GES_IS_TEST_CLIP (clip)) {
1100         GESAsset *asset = ges_extractable_get_asset (GES_EXTRACTABLE (clip));
1101         const gchar *id = ges_asset_get_id (asset);
1102
1103         g_string_append (res, " +test-clip ");
1104
1105         _sanitize_argument (g_enum_get_value (g_type_class_peek
1106                 (GES_VIDEO_TEST_PATTERN_TYPE),
1107                 ges_test_clip_get_vpattern (GES_TEST_CLIP (clip)))->value_nick,
1108             res);
1109
1110         if (g_strcmp0 (id, "GESTestClip")) {
1111           g_string_append (res, " asset-id=");
1112           _sanitize_argument (id, res);
1113         }
1114
1115         option = &options[TEST_CLIP];
1116       } else if (GES_IS_TITLE_CLIP (clip)) {
1117         g_string_append (res, " +title ");
1118         _sanitize_argument (ges_title_clip_get_text (GES_TITLE_CLIP (clip)),
1119             res);
1120         option = &options[TITLE];
1121       } else if (GES_IS_URI_CLIP (clip)) {
1122         g_string_append (res, " +clip ");
1123
1124         _sanitize_argument (ges_uri_clip_get_uri (GES_URI_CLIP (clip)), res);
1125         option = &options[CLIP];
1126       } else {
1127         g_warning ("Unhandled clip type: %s", G_OBJECT_TYPE_NAME (clip));
1128         continue;
1129       }
1130
1131       _serialize_clip_track_types (clip, tt, res);
1132
1133       if (i)
1134         g_string_append_printf (res, " layer=%d", i);
1135
1136       _serialize_object_properties (G_OBJECT (clip), option, FALSE, res);
1137       _serialize_clip_effects (clip, res);
1138
1139       for (tmptrackelem = GES_CONTAINER_CHILDREN (clip); tmptrackelem;
1140           tmptrackelem = tmptrackelem->next)
1141         _serialize_object_properties (G_OBJECT (tmptrackelem->data), option,
1142             TRUE, res);
1143     }
1144     g_list_free_full (clips, gst_object_unref);
1145   }
1146
1147 done:
1148   return g_string_free (res, FALSE);
1149   {
1150     GstUri *uri = gst_uri_from_string (res->str);
1151     gchar *uri_str = gst_uri_to_string (uri);
1152
1153     g_string_free (res, TRUE);
1154
1155     return uri_str;
1156   }
1157 }