ges-project.c 36.1 KB
Newer Older
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
/* GStreamer Editing Services
 *
 * Copyright (C) <2012> Thibault Saunier <thibault.saunier@collabora.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., 59 Temple Place - Suite 330,
 * Boston, MA 02111-1307, USA.
 */
/**
21
 * SECTION: gesproject
22
 * @title: GESProject
Thibault Saunier's avatar
Thibault Saunier committed
23
 * @short_description: A GESAsset that is used to manage projects
24 25
 *
 * The #GESProject is used to control a set of #GESAsset and is a
Thibault Saunier's avatar
Thibault Saunier committed
26
 * #GESAsset with `GES_TYPE_TIMELINE` as @extractable_type itself. That
27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45
 * means that you can extract #GESTimeline from a project as followed:
 *
 * |[
 *  GESProject *project;
 *  GESTimeline *timeline;
 *
 *  project = ges_project_new ("file:///path/to/a/valid/project/uri");
 *
 *  // Here you can connect to the various signal to get more infos about
 *  // what is happening and recover from errors if possible
 *  ...
 *
 *  timeline = ges_asset_extract (GES_ASSET (project));
 * ]|
 *
 * The #GESProject class offers a higher level API to handle #GESAsset-s.
 * It lets you request new asset, and it informs you about new assets through
 * a set of signals. Also it handles problem such as missing files/missing
 * #GstElement and lets you try to recover from those.
Thibault Saunier's avatar
Thibault Saunier committed
46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63
 *
 * ## Subprojects
 *
 * In order to add a subproject, the only thing to do is to add the subproject
 * with to the main project:
 *
 * ``` c
 * ges_project_add_asset (project, GES_ASSET (subproject));
 * ```
 * then the subproject will be serialized in the project files. To use
 * the subproject in a timeline, you should use a #GESUriClip with the
 * same subproject URI.
 *
 * When loading a project with subproject, subprojects URIs will be temporary
 * writable local files. If you want to edit the subproject timeline,
 * you should retrieve the subproject from the parent project asset list and
 * extract the timeline with ges_asset_extract() and save it at
 * the same temporary location.
64
 */
65 66 67 68
#ifdef HAVE_CONFIG_H
#include "config.h"
#endif

69 70 71
#include "ges.h"
#include "ges-internal.h"

72 73 74
static GPtrArray *new_paths = NULL;
static GHashTable *tried_uris = NULL;

75 76 77
struct _GESProjectPrivate
{
  GHashTable *assets;
78 79
  /* Set of asset ID being loaded */
  GHashTable *loading_assets;
80
  GHashTable *loaded_with_error;
81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97
  GESAsset *formatter_asset;

  GList *formatters;

  gchar *uri;

  GList *encoding_profiles;
};

typedef struct EmitLoadedInIdle
{
  GESProject *project;
  GESTimeline *timeline;
} EmitLoadedInIdle;

enum
{
98
  LOADING_SIGNAL,
99 100 101 102 103
  LOADED_SIGNAL,
  ERROR_LOADING_ASSET,
  ASSET_ADDED_SIGNAL,
  ASSET_REMOVED_SIGNAL,
  MISSING_URI_SIGNAL,
104
  ASSET_LOADING_SIGNAL,
105 106 107
  LAST_SIGNAL
};

108 109
G_DEFINE_TYPE_WITH_PRIVATE (GESProject, ges_project, GES_TYPE_ASSET);

110 111 112 113
static guint _signals[LAST_SIGNAL] = { 0 };

static guint nb_projects = 0;

114 115 116 117 118 119 120 121 122 123 124 125
/* Find the type that implemented the GESExtractable interface */
static inline const gchar *
_extractable_type_name (GType type)
{
  while (1) {
    if (g_type_is_a (g_type_parent (type), GES_TYPE_EXTRACTABLE))
      type = g_type_parent (type);
    else
      return g_type_name (type);
  }
}

126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154
enum
{
  PROP_0,
  PROP_URI,
  PROP_LAST,
};

static GParamSpec *_properties[LAST_SIGNAL] = { 0 };

static gboolean
_emit_loaded_in_idle (EmitLoadedInIdle * data)
{
  g_signal_emit (data->project, _signals[LOADED_SIGNAL], 0, data->timeline);

  gst_object_unref (data->project);
  gst_object_unref (data->timeline);
  g_slice_free (EmitLoadedInIdle, data);

  return FALSE;
}

static void
ges_project_add_formatter (GESProject * project, GESFormatter * formatter)
{
  GESProjectPrivate *priv = GES_PROJECT (project)->priv;

  ges_formatter_set_project (formatter, project);
  priv->formatters = g_list_append (priv->formatters, formatter);

155
  gst_object_ref_sink (formatter);
156 157 158 159 160 161 162 163 164 165 166
}

static void
ges_project_remove_formatter (GESProject * project, GESFormatter * formatter)
{
  GList *tmp;
  GESProjectPrivate *priv = GES_PROJECT (project)->priv;

  for (tmp = priv->formatters; tmp; tmp = tmp->next) {
    if (tmp->data == formatter) {
      gst_object_unref (formatter);
167
      priv->formatters = g_list_delete_link (priv->formatters, tmp);
168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187

      return;
    }
  }
}

static void
ges_project_set_uri (GESProject * project, const gchar * uri)
{
  GESProjectPrivate *priv;

  g_return_if_fail (GES_IS_PROJECT (project));

  priv = project->priv;
  if (priv->uri) {
    GST_WARNING_OBJECT (project, "Trying to rest URI, this is prohibited");

    return;
  }

188 189
  if (uri == NULL) {
    GST_LOG_OBJECT (project, "Uri should not be NULL");
190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209
    return;
  }

  priv->uri = g_strdup (uri);

  /* We use that URI as ID */
  ges_asset_set_id (GES_ASSET (project), uri);

  return;
}

static gboolean
_load_project (GESProject * project, GESTimeline * timeline, GError ** error)
{
  GError *lerr = NULL;
  GESProjectPrivate *priv;
  GESFormatter *formatter;

  priv = GES_PROJECT (project)->priv;

210
  g_signal_emit (project, _signals[LOADING_SIGNAL], 0, timeline);
211 212
  if (priv->uri == NULL) {

213 214 215 216 217
    if (gst_uri_is_valid (ges_asset_get_id (GES_ASSET (project)))) {
      ges_project_set_uri (project, ges_asset_get_id (GES_ASSET (project)));
      GST_INFO_OBJECT (project, "Using asset ID %s as URI.", priv->uri);
    } else {
      EmitLoadedInIdle *data = g_slice_new (EmitLoadedInIdle);
218

219 220 221
      GST_INFO_OBJECT (project, "%s, Loading an empty timeline %s"
          " as no URI set yet", GST_OBJECT_NAME (timeline),
          ges_asset_get_id (GES_ASSET (project)));
222

223 224 225 226
      data->timeline = gst_object_ref (timeline);
      data->project = gst_object_ref (project);

      /* Make sure the signal is emitted after the functions ends */
227
      ges_idle_add ((GSourceFunc) _emit_loaded_in_idle, data, NULL);
228 229
      return TRUE;
    }
230 231 232
  }

  if (priv->formatter_asset == NULL)
233
    priv->formatter_asset = _find_formatter_asset_for_id (priv->uri);
234

235 236
  if (priv->formatter_asset == NULL) {
    lerr = g_error_new (GES_ERROR, 0, "Could not find a suitable formatter");
237
    goto failed;
238
  }
239 240 241 242

  formatter = GES_FORMATTER (ges_asset_extract (priv->formatter_asset, &lerr));
  if (lerr) {
    GST_WARNING_OBJECT (project, "Could not create the formatter: %s",
243
        lerr->message);
244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269

    goto failed;
  }

  ges_project_add_formatter (GES_PROJECT (project), formatter);
  ges_formatter_load_from_uri (formatter, timeline, priv->uri, &lerr);
  if (lerr) {
    GST_WARNING_OBJECT (project, "Could not load the timeline,"
        " returning: %s", lerr->message);
    goto failed;
  }

  return TRUE;

failed:
  if (lerr)
    g_propagate_error (error, lerr);
  return FALSE;
}

static gboolean
_uri_missing_accumulator (GSignalInvocationHint * ihint, GValue * return_accu,
    const GValue * handler_return, gpointer data)
{
  const gchar *ret = g_value_get_string (handler_return);

270 271 272 273 274 275
  if (ret) {
    if (!gst_uri_is_valid (ret)) {
      GST_INFO ("The uri %s was not valid, can not work with it!", ret);
      return TRUE;
    }

276 277 278 279 280 281 282 283 284 285 286
    g_value_set_string (return_accu, ret);
    return FALSE;
  }

  return TRUE;
}

/* GESAsset vmethod implementation */
static GESExtractable *
ges_project_extract (GESAsset * project, GError ** error)
{
287
  GESTimeline *timeline = g_object_new (GES_TYPE_TIMELINE, NULL);
288

289
  ges_extractable_set_asset (GES_EXTRACTABLE (timeline), GES_ASSET (project));
290 291 292 293 294 295 296
  if (_load_project (GES_PROJECT (project), timeline, error))
    return GES_EXTRACTABLE (timeline);

  gst_object_unref (timeline);
  return NULL;
}

297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312
static void
_add_media_new_paths_recursing (const gchar * value)
{
  GFileInfo *info;
  GFileEnumerator *fenum;
  GFile *file = g_file_new_for_uri (value);

  if (!(fenum = g_file_enumerate_children (file,
              "standard::*", G_FILE_QUERY_INFO_NONE, NULL, NULL))) {
    GST_INFO ("%s is not a folder", value);

    goto done;
  }

  GST_INFO ("Adding folder: %s", value);
  g_ptr_array_add (new_paths, g_strdup (value));
Justin Kim's avatar
Justin Kim committed
313 314
  info = g_file_enumerator_next_file (fenum, NULL, NULL);
  while (info != NULL) {
315 316 317 318 319 320 321 322
    if (g_file_info_get_file_type (info) == G_FILE_TYPE_DIRECTORY) {
      GFile *f = g_file_enumerator_get_child (fenum, info);
      gchar *uri = g_file_get_uri (f);

      _add_media_new_paths_recursing (uri);
      gst_object_unref (f);
      g_free (uri);
    }
Justin Kim's avatar
Justin Kim committed
323 324
    g_object_unref (info);
    info = g_file_enumerator_next_file (fenum, NULL, NULL);
325 326 327 328 329 330 331 332 333
  }

done:
  gst_object_unref (file);
  if (fenum)
    gst_object_unref (fenum);
}

gboolean
334
ges_add_missing_uri_relocation_uri (const gchar * uri, gboolean recurse)
335
{
336
  g_return_val_if_fail (gst_uri_is_valid (uri), FALSE);
337 338 339 340

  if (new_paths == NULL)
    new_paths = g_ptr_array_new_with_free_func (g_free);

341 342 343 344
  if (recurse)
    _add_media_new_paths_recursing (uri);
  else
    g_ptr_array_add (new_paths, g_strdup (uri));
345 346 347 348 349 350 351 352 353 354

  return TRUE;
}

static gchar *
ges_missing_uri_default (GESProject * self, GError * error,
    GESAsset * wrong_asset)
{
  guint i;
  const gchar *old_uri = ges_asset_get_id (wrong_asset);
355 356 357 358 359 360 361
  gchar *new_id = NULL;

  if (ges_asset_request_id_update (wrong_asset, &new_id, error) && new_id) {
    GST_INFO_OBJECT (self, "Returned guessed new ID: %s", new_id);

    return new_id;
  }
362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390

  if (new_paths == NULL)
    return NULL;

  if (tried_uris == NULL)
    tried_uris = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL);

  for (i = 0; i < new_paths->len; i++) {
    gchar *basename, *res;

    basename = g_path_get_basename (old_uri);
    res = g_build_filename (new_paths->pdata[i], basename, NULL);
    g_free (basename);

    if (g_strcmp0 (old_uri, res) == 0) {
      g_hash_table_add (tried_uris, res);
    } else if (g_hash_table_lookup (tried_uris, res)) {
      GST_DEBUG_OBJECT (self, "File already tried: %s", res);
      g_free (res);
    } else {
      g_hash_table_add (tried_uris, g_strdup (res));
      GST_DEBUG_OBJECT (self, "Trying: %s\n", res);
      return res;
    }
  }

  return NULL;
}

391 392 393 394 395 396
gchar *
ges_uri_asset_try_update_id (GError * error, GESAsset * wrong_asset)
{
  return ges_missing_uri_default (NULL, error, wrong_asset);
}

397 398 399 400 401 402 403
static void
ges_uri_assets_validate_uri (const gchar * nid)
{
  if (tried_uris)
    g_hash_table_remove (tried_uris, nid);
}

404 405 406 407 408 409 410 411
/* GObject vmethod implementation */
static void
_dispose (GObject * object)
{
  GESProjectPrivate *priv = GES_PROJECT (object)->priv;

  if (priv->assets)
    g_hash_table_unref (priv->assets);
412 413
  if (priv->loading_assets)
    g_hash_table_unref (priv->loading_assets);
414 415
  if (priv->loaded_with_error)
    g_hash_table_unref (priv->loaded_with_error);
416 417 418
  if (priv->formatter_asset)
    gst_object_unref (priv->formatter_asset);

419 420
  while (priv->formatters)
    ges_project_remove_formatter (GES_PROJECT (object), priv->formatters->data);
421 422 423 424 425 426 427 428 429 430 431

  G_OBJECT_CLASS (ges_project_parent_class)->dispose (object);
}

static void
_finalize (GObject * object)
{
  GESProjectPrivate *priv = GES_PROJECT (object)->priv;

  if (priv->uri)
    g_free (priv->uri);
432
  g_list_free_full (priv->encoding_profiles, g_object_unref);
433 434

  G_OBJECT_CLASS (ges_project_parent_class)->finalize (object);
435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470
}

static void
_get_property (GESProject * project, guint property_id,
    GValue * value, GParamSpec * pspec)
{
  GESProjectPrivate *priv = project->priv;

  switch (property_id) {
    case PROP_URI:
      g_value_set_string (value, priv->uri);
      break;
    default:
      G_OBJECT_WARN_INVALID_PROPERTY_ID (project, property_id, pspec);
  }
}

static void
_set_property (GESProject * project, guint property_id,
    const GValue * value, GParamSpec * pspec)
{
  switch (property_id) {
    case PROP_URI:
      project->priv->uri = g_value_dup_string (value);
      break;
    default:
      G_OBJECT_WARN_INVALID_PROPERTY_ID (project, property_id, pspec);
  }
}

static void
ges_project_class_init (GESProjectClass * klass)
{
  GObjectClass *object_class = G_OBJECT_CLASS (klass);

  klass->asset_added = NULL;
471
  klass->missing_uri = ges_missing_uri_default;
472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488
  klass->loading_error = NULL;
  klass->asset_removed = NULL;
  object_class->get_property = (GObjectGetPropertyFunc) _get_property;
  object_class->set_property = (GObjectSetPropertyFunc) _set_property;

  /**
   * GESProject::uri:
   *
   * The location of the project to use.
   */
  _properties[PROP_URI] = g_param_spec_string ("uri", "URI",
      "uri of the project", NULL, G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY);

  g_object_class_install_properties (object_class, PROP_LAST, _properties);

  /**
   * GESProject::asset-added:
489
   * @project: the #GESProject
490 491 492 493
   * @asset: The #GESAsset that has been added to @project
   */
  _signals[ASSET_ADDED_SIGNAL] =
      g_signal_new ("asset-added", G_TYPE_FROM_CLASS (klass),
494
      G_SIGNAL_RUN_LAST, G_STRUCT_OFFSET (GESProjectClass, asset_added),
495 496
      NULL, NULL, g_cclosure_marshal_generic, G_TYPE_NONE, 1, GES_TYPE_ASSET);

497 498 499 500 501 502 503 504 505 506 507 508
  /**
   * GESProject::asset-loading:
   * @project: the #GESProject
   * @asset: The #GESAsset that started loading
   *
   * Since: 1.8
   */
  _signals[ASSET_LOADING_SIGNAL] =
      g_signal_new ("asset-loading", G_TYPE_FROM_CLASS (klass),
      G_SIGNAL_RUN_LAST, G_STRUCT_OFFSET (GESProjectClass, asset_loading),
      NULL, NULL, g_cclosure_marshal_generic, G_TYPE_NONE, 1, GES_TYPE_ASSET);

509 510
  /**
   * GESProject::asset-removed:
511
   * @project: the #GESProject
512 513 514 515 516 517 518
   * @asset: The #GESAsset that has been removed from @project
   */
  _signals[ASSET_REMOVED_SIGNAL] =
      g_signal_new ("asset-removed", G_TYPE_FROM_CLASS (klass),
      G_SIGNAL_RUN_LAST, G_STRUCT_OFFSET (GESProjectClass, asset_removed),
      NULL, NULL, g_cclosure_marshal_generic, G_TYPE_NONE, 1, GES_TYPE_ASSET);

519 520 521 522 523 524 525 526 527 528 529 530 531
  /**
   * GESProject::loading:
   * @project: the #GESProject that is starting to load a timeline
   * @timeline: The #GESTimeline that started loading
   *
   * Since: 1.18
   */
  _signals[LOADING_SIGNAL] =
      g_signal_new ("loading", G_TYPE_FROM_CLASS (klass),
      G_SIGNAL_RUN_FIRST, G_STRUCT_OFFSET (GESProjectClass, loading),
      NULL, NULL, g_cclosure_marshal_generic, G_TYPE_NONE,
      1, GES_TYPE_TIMELINE);

532 533
  /**
   * GESProject::loaded:
534 535
   * @project: the #GESProject that is done loading a timeline.
   * @timeline: The #GESTimeline that completed loading
536 537 538
   */
  _signals[LOADED_SIGNAL] =
      g_signal_new ("loaded", G_TYPE_FROM_CLASS (klass),
539
      G_SIGNAL_RUN_FIRST, G_STRUCT_OFFSET (GESProjectClass, loaded),
540 541 542 543 544 545 546 547 548 549 550 551
      NULL, NULL, g_cclosure_marshal_generic, G_TYPE_NONE,
      1, GES_TYPE_TIMELINE);

  /**
   * GESProject::missing-uri:
   * @project: the #GESProject reporting that a file has moved
   * @error: The error that happened
   * @wrong_asset: The asset with the wrong ID, you should us it and its content
   * only to find out what the new location is.
   *
   * |[
   * static gchar
552
   * source_moved_cb (GESProject *project, GError *error, GESAsset *asset_with_error)
553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578
   * {
   *   return g_strdup ("file:///the/new/uri.ogg");
   * }
   *
   * static int
   * main (int argc, gchar ** argv)
   * {
   *   GESTimeline *timeline;
   *   GESProject *project = ges_project_new ("file:///some/uri.xges");
   *
   *   g_signal_connect (project, "missing-uri", source_moved_cb, NULL);
   *   timeline = ges_asset_extract (GES_ASSET (project));
   * }
   * ]|
   *
   * Returns: (transfer full) (allow-none): The new URI of @wrong_asset
   */
  _signals[MISSING_URI_SIGNAL] =
      g_signal_new ("missing-uri", G_TYPE_FROM_CLASS (klass),
      G_SIGNAL_RUN_LAST, G_STRUCT_OFFSET (GESProjectClass, missing_uri),
      _uri_missing_accumulator, NULL, g_cclosure_marshal_generic,
      G_TYPE_STRING, 2, G_TYPE_ERROR, GES_TYPE_ASSET);

  /**
   * GESProject::error-loading-asset:
   * @project: the #GESProject on which a problem happend when creted a #GESAsset
Thibault Saunier's avatar
Thibault Saunier committed
579
   * @error: The #GError defining the error that occured, might be %NULL
580 581 582 583 584 585 586 587 588 589 590 591 592 593 594
   * @id: The @id of the asset that failed loading
   * @extractable_type: The @extractable_type of the asset that
   * failed loading
   *
   * Informs you that a #GESAsset could not be created. In case of
   * missing GStreamer plugins, the error will be set to #GST_CORE_ERROR
   * #GST_CORE_ERROR_MISSING_PLUGIN
   */
  _signals[ERROR_LOADING_ASSET] =
      g_signal_new ("error-loading-asset", G_TYPE_FROM_CLASS (klass),
      G_SIGNAL_RUN_LAST, G_STRUCT_OFFSET (GESProjectClass, loading_error),
      NULL, NULL, g_cclosure_marshal_generic,
      G_TYPE_NONE, 3, G_TYPE_ERROR, G_TYPE_STRING, G_TYPE_GTYPE);

  object_class->dispose = _dispose;
Dan Williams's avatar
Dan Williams committed
595
  object_class->finalize = _finalize;
596 597 598 599 600 601 602 603

  GES_ASSET_CLASS (klass)->extract = ges_project_extract;
}

static void
ges_project_init (GESProject * project)
{
  GESProjectPrivate *priv = project->priv =
604
      ges_project_get_instance_private (project);
605 606 607 608 609 610 611

  priv->uri = NULL;
  priv->formatters = NULL;
  priv->formatter_asset = NULL;
  priv->encoding_profiles = NULL;
  priv->assets = g_hash_table_new_full (g_str_hash, g_str_equal,
      g_free, gst_object_unref);
612 613
  priv->loading_assets = g_hash_table_new_full (g_str_hash, g_str_equal,
      g_free, gst_object_unref);
614 615
  priv->loaded_with_error = g_hash_table_new_full (g_str_hash, g_str_equal,
      g_free, NULL);
616 617
}

618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633
static gchar *
ges_project_internal_extractable_type_id (GType extractable_type,
    const gchar * id)
{
  return g_strdup_printf ("%s:%s", _extractable_type_name (extractable_type),
      id);
}

static gchar *
ges_project_internal_asset_id (GESAsset * asset)
{
  return
      ges_project_internal_extractable_type_id (ges_asset_get_extractable_type
      (asset), ges_asset_get_id (asset));
}

634 635 636 637
static void
_send_error_loading_asset (GESProject * project, GESAsset * asset,
    GError * error)
{
638
  gchar *internal_id = ges_project_internal_asset_id (asset);
639 640 641
  const gchar *id = ges_asset_get_id (asset);

  GST_DEBUG_OBJECT (project, "Sending error loading asset for %s", id);
642 643 644 645
  g_hash_table_remove (project->priv->loading_assets, internal_id);
  g_hash_table_add (project->priv->loaded_with_error, internal_id);
  g_signal_emit (project, _signals[ERROR_LOADING_ASSET], 0, error,
      id, ges_asset_get_extractable_type (asset));
646 647
}

648 649 650 651 652
gchar *
ges_project_try_updating_id (GESProject * project, GESAsset * asset,
    GError * error)
{
  gchar *new_id = NULL;
653
  const gchar *id;
654
  gchar *internal_id;
655 656 657 658 659

  g_return_val_if_fail (GES_IS_PROJECT (project), NULL);
  g_return_val_if_fail (GES_IS_ASSET (asset), NULL);
  g_return_val_if_fail (error, NULL);

660 661
  id = ges_asset_get_id (asset);
  GST_DEBUG_OBJECT (project, "Try to proxy %s", id);
662
  if (ges_asset_request_id_update (asset, &new_id, error) == FALSE) {
663 664 665
    GST_DEBUG_OBJECT (project, "Type: %s can not be proxied for id: %s "
        "and error: %s", g_type_name (G_OBJECT_TYPE (asset)), id,
        error->message);
666
    _send_error_loading_asset (project, asset, error);
667 668 669 670

    return NULL;
  }

671 672 673 674
  /* Always send the MISSING_URI signal if requesting new ID is possible
   * so that subclasses of GESProject are aware of the missing-uri */
  g_signal_emit (project, _signals[MISSING_URI_SIGNAL], 0, error, asset,
      &new_id);
675 676

  if (new_id) {
677
    GST_DEBUG_OBJECT (project, "new id found: %s", new_id);
678
    if (!ges_asset_try_proxy (asset, new_id)) {
679
      g_free (new_id);
680 681
      new_id = NULL;
    }
682 683
  } else {
    GST_DEBUG_OBJECT (project, "No new id found for %s", id);
684 685
  }

686 687 688
  internal_id = ges_project_internal_asset_id (asset);
  g_hash_table_remove (project->priv->loading_assets, internal_id);
  g_free (internal_id);
689 690 691 692

  if (new_id == NULL)
    _send_error_loading_asset (project, asset, error);

693

694
  return new_id;
695 696 697 698 699 700 701 702 703 704 705
}

static void
new_asset_cb (GESAsset * source, GAsyncResult * res, GESProject * project)
{
  GError *error = NULL;
  gchar *possible_id = NULL;
  GESAsset *asset = ges_asset_request_finish (res, &error);

  if (error) {
    possible_id = ges_project_try_updating_id (project, source, error);
706
    g_clear_error (&error);
707

708
    if (possible_id == NULL)
709
      return;
710 711 712

    ges_project_create_asset (project, possible_id,
        ges_asset_get_extractable_type (source));
713 714 715 716 717

    g_free (possible_id);
    return;
  }

718
  ges_asset_set_proxy (NULL, asset);
719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736
  ges_project_add_asset (project, asset);
  if (asset)
    gst_object_unref (asset);
}

/**
 * ges_project_set_loaded:
 * @project: The #GESProject from which to emit the "project-loaded" signal
 *
 * Emits the "loaded" signal. This method should be called by sublasses when
 * the project is fully loaded.
 *
 * Returns: %TRUE if the signale could be emitted %FALSE otherwize
 */
gboolean
ges_project_set_loaded (GESProject * project, GESFormatter * formatter)
{
  GST_INFO_OBJECT (project, "Emit project loaded");
737 738 739 740 741 742
  if (GST_STATE (formatter->timeline) < GST_STATE_PAUSED) {
    timeline_fill_gaps (formatter->timeline);
  } else {
    ges_timeline_commit (formatter->timeline);
  }

743 744 745
  g_signal_emit (project, _signals[LOADED_SIGNAL], 0, formatter->timeline);

  /* We are now done with that formatter */
Lubosz Sarnecki's avatar
Lubosz Sarnecki committed
746
  ges_project_remove_formatter (project, formatter);
747 748 749
  return TRUE;
}

750 751 752 753 754 755
void
ges_project_add_loading_asset (GESProject * project, GType extractable_type,
    const gchar * id)
{
  GESAsset *asset;

756
  if ((asset = ges_asset_cache_lookup (extractable_type, id))) {
757 758
    if (g_hash_table_insert (project->priv->loading_assets,
            ges_project_internal_asset_id (asset), gst_object_ref (asset)))
759 760
      g_signal_emit (project, _signals[ASSET_LOADING_SIGNAL], 0, asset);
  }
761 762
}

763 764 765 766 767 768 769 770 771
/**************************************
 *                                    *
 *         API Implementation         *
 *                                    *
 **************************************/

/**
 * ges_project_create_asset:
 * @project: A #GESProject
772
 * @id: (allow-none): The id of the asset to create and add to @project
773 774 775 776 777 778 779 780 781 782 783 784 785
 * @extractable_type: The #GType of the asset to create
 *
 * Create and add a #GESAsset to @project. You should connect to the
 * "asset-added" signal to get the asset when it finally gets added to
 * @project
 *
 * Returns: %TRUE if the asset started to be added %FALSE it was already
 * in the project
 */
gboolean
ges_project_create_asset (GESProject * project, const gchar * id,
    GType extractable_type)
{
786
  gchar *internal_id;
787 788 789 790
  g_return_val_if_fail (GES_IS_PROJECT (project), FALSE);
  g_return_val_if_fail (g_type_is_a (extractable_type, GES_TYPE_EXTRACTABLE),
      FALSE);

791 792
  if (id == NULL)
    id = g_type_name (extractable_type);
793 794 795 796 797
  internal_id = ges_project_internal_extractable_type_id (extractable_type, id);

  if (g_hash_table_lookup (project->priv->assets, internal_id) ||
      g_hash_table_lookup (project->priv->loading_assets, internal_id) ||
      g_hash_table_lookup (project->priv->loaded_with_error, internal_id)) {
798

799
    g_free (internal_id);
800
    return FALSE;
801 802
  }
  g_free (internal_id);
803 804 805 806

  /* TODO Add a GCancellable somewhere in our API */
  ges_asset_request_async (extractable_type, id, NULL,
      (GAsyncReadyCallback) new_asset_cb, project);
807
  ges_project_add_loading_asset (project, extractable_type, id);
808 809 810 811

  return TRUE;
}

812 813 814 815 816 817 818 819 820 821 822
/**
 * ges_project_create_asset_sync:
 * @project: A #GESProject
 * @id: (allow-none): The id of the asset to create and add to @project
 * @extractable_type: The #GType of the asset to create
 * @error: A #GError to be set in case of error
 *
 * Create and add a #GESAsset to @project. You should connect to the
 * "asset-added" signal to get the asset when it finally gets added to
 * @project
 *
823
 * Returns: (transfer full) (nullable): The newly created #GESAsset or %NULL.
824 825 826 827 828 829
 */
GESAsset *
ges_project_create_asset_sync (GESProject * project, const gchar * id,
    GType extractable_type, GError ** error)
{
  GESAsset *asset;
830
  gchar *possible_id = NULL, *internal_id;
831 832 833 834 835 836 837 838 839
  gboolean retry = TRUE;

  g_return_val_if_fail (GES_IS_PROJECT (project), FALSE);
  g_return_val_if_fail (g_type_is_a (extractable_type, GES_TYPE_EXTRACTABLE),
      FALSE);

  if (id == NULL)
    id = g_type_name (extractable_type);

840 841 842 843
  internal_id = ges_project_internal_extractable_type_id (extractable_type, id);
  if ((asset = g_hash_table_lookup (project->priv->assets, internal_id))) {
    g_free (internal_id);

844
    return asset;
845 846 847 848
  } else if (g_hash_table_lookup (project->priv->loading_assets, internal_id) ||
      g_hash_table_lookup (project->priv->loaded_with_error, internal_id)) {
    g_free (internal_id);

849
    return NULL;
850
  }
851 852 853 854 855 856 857 858 859 860 861 862

  /* TODO Add a GCancellable somewhere in our API */
  while (retry) {

    if (g_type_is_a (extractable_type, GES_TYPE_URI_CLIP)) {
      asset = GES_ASSET (ges_uri_clip_asset_request_sync (id, error));
    } else {
      asset = ges_asset_request (extractable_type, id, error);
    }

    if (asset) {
      retry = FALSE;
863 864 865
      internal_id =
          ges_project_internal_extractable_type_id (extractable_type, id);
      if ((!g_hash_table_lookup (project->priv->assets, internal_id)))
866
        g_signal_emit (project, _signals[ASSET_LOADING_SIGNAL], 0, asset);
867
      g_free (internal_id);
868

869 870 871 872 873 874 875 876 877 878 879 880
      if (possible_id) {
        g_free (possible_id);
        ges_uri_assets_validate_uri (id);
      }

      break;
    } else {
      GESAsset *tmpasset;

      tmpasset = ges_asset_cache_lookup (extractable_type, id);
      possible_id = ges_project_try_updating_id (project, tmpasset, *error);

881 882 883 884
      if (possible_id == NULL) {
        g_signal_emit (project, _signals[ASSET_LOADING_SIGNAL], 0, tmpasset);
        g_signal_emit (project, _signals[ERROR_LOADING_ASSET], 0, *error, id,
            extractable_type);
885
        return NULL;
886 887
      }

888

889
      g_clear_error (error);
890 891 892 893 894

      id = possible_id;
    }
  }

895 896 897
  if (!ges_asset_get_proxy_target (asset))
    ges_asset_set_proxy (NULL, asset);

898 899 900 901 902
  ges_project_add_asset (project, asset);

  return asset;
}

903 904 905 906 907
/**
 * ges_project_add_asset:
 * @project: A #GESProject
 * @asset: (transfer none): A #GESAsset to add to @project
 *
Thibault Saunier's avatar
Thibault Saunier committed
908
 * Adds a #GESAsset to @project, the project will keep a reference on
909 910 911 912 913 914 915 916
 * @asset.
 *
 * Returns: %TRUE if the asset could be added %FALSE it was already
 * in the project
 */
gboolean
ges_project_add_asset (GESProject * project, GESAsset * asset)
{
917
  gchar *internal_id;
918 919
  g_return_val_if_fail (GES_IS_PROJECT (project), FALSE);

920 921 922
  internal_id = ges_project_internal_asset_id (asset);
  if (g_hash_table_lookup (project->priv->assets, internal_id)) {
    g_free (internal_id);
923
    return TRUE;
924
  }
925

926 927 928
  g_hash_table_insert (project->priv->assets, internal_id,
      gst_object_ref (asset));
  g_hash_table_remove (project->priv->loading_assets, internal_id);
929 930 931 932 933 934 935 936 937 938 939 940 941 942 943 944 945 946 947
  GST_DEBUG_OBJECT (project, "Asset added: %s", ges_asset_get_id (asset));
  g_signal_emit (project, _signals[ASSET_ADDED_SIGNAL], 0, asset);

  return TRUE;
}

/**
 * ges_project_remove_asset:
 * @project: A #GESProject
 * @asset: (transfer none): A #GESAsset to remove from @project
 *
 * remove a @asset to from @project.
 *
 * Returns: %TRUE if the asset could be removed %FALSE otherwise
 */
gboolean
ges_project_remove_asset (GESProject * project, GESAsset * asset)
{
  gboolean ret;
948
  gchar *internal_id;
949 950 951

  g_return_val_if_fail (GES_IS_PROJECT (project), FALSE);

952 953 954
  internal_id = ges_project_internal_asset_id (asset);
  ret = g_hash_table_remove (project->priv->assets, internal_id);
  g_free (internal_id);
955 956 957 958 959 960 961 962 963 964 965 966 967 968 969 970 971 972 973 974
  g_signal_emit (project, _signals[ASSET_REMOVED_SIGNAL], 0, asset);

  return ret;
}

/**
 * ges_project_get_asset:
 * @project: A #GESProject
 * @id: The id of the asset to retrieve
 * @extractable_type: The extractable_type of the asset
 * to retrieve from @object
 *
 * Returns: (transfer full) (allow-none): The #GESAsset with
 * @id or %NULL if no asset with @id as an ID
 */
GESAsset *
ges_project_get_asset (GESProject * project, const gchar * id,
    GType extractable_type)
{
  GESAsset *asset;
975
  gchar *internal_id;
976 977 978 979 980

  g_return_val_if_fail (GES_IS_PROJECT (project), NULL);
  g_return_val_if_fail (g_type_is_a (extractable_type, GES_TYPE_EXTRACTABLE),
      NULL);

981 982 983
  internal_id = ges_project_internal_extractable_type_id (extractable_type, id);
  asset = g_hash_table_lookup (project->priv->assets, internal_id);
  g_free (internal_id);
984 985 986 987 988 989 990 991 992 993

  if (asset)
    return gst_object_ref (asset);

  return NULL;
}

/**
 * ges_project_list_assets:
 * @project: A #GESProject
Thibault Saunier's avatar
Thibault Saunier committed
994
 * @filter: Type of assets to list, `GES_TYPE_EXTRACTABLE` will list
995 996 997 998 999 1000 1001 1002 1003 1004 1005 1006 1007 1008 1009 1010 1011 1012 1013 1014 1015 1016
 * all assets
 *
 * List all @asset contained in @project filtering per extractable_type
 * as defined by @filter. It copies the asset and thus will not be updated
 * in time.
 *
 * Returns: (transfer full) (element-type GESAsset): The list of
 * #GESAsset the object contains
 */
GList *
ges_project_list_assets (GESProject * project, GType filter)
{
  GList *ret = NULL;
  GHashTableIter iter;
  gpointer key, value;

  g_return_val_if_fail (GES_IS_PROJECT (project), NULL);

  g_hash_table_iter_init (&iter, project->priv->assets);
  while (g_hash_table_iter_next (&iter, &key, &value)) {
    if (g_type_is_a (ges_asset_get_extractable_type (GES_ASSET (value)),
            filter))
1017
      ret = g_list_append (ret, gst_object_ref (value));
1018 1019 1020 1021 1022 1023 1024 1025 1026 1027 1028 1029 1030 1031 1032 1033 1034 1035 1036 1037 1038 1039 1040 1041 1042 1043 1044 1045 1046 1047 1048 1049 1050 1051 1052 1053 1054 1055 1056 1057 1058 1059 1060 1061 1062 1063 1064 1065 1066 1067 1068 1069 1070 1071 1072 1073 1074 1075 1076 1077 1078 1079 1080 1081
  }

  return ret;
}

/**
 * ges_project_save:
 * @project: A #GESProject to save
 * @timeline: The #GESTimeline to save, it must have been extracted from @project
 * @uri: The uri where to save @project and @timeline
 * @formatter_asset: (allow-none): The formatter asset to use or %NULL. If %NULL,
 * will try to save in the same format as the one from which the timeline as been loaded
 * or default to the formatter with highest rank
 * @overwrite: %TRUE to overwrite file if it exists
 * @error: (out) (allow-none): An error to be set in case something wrong happens or %NULL
 *
 * Save the timeline of @project to @uri. You should make sure that @timeline
 * is one of the timelines that have been extracted from @project
 * (using ges_asset_extract (@project);)
 *
 * Returns: %TRUE if the project could be save, %FALSE otherwize
 */
gboolean
ges_project_save (GESProject * project, GESTimeline * timeline,
    const gchar * uri, GESAsset * formatter_asset, gboolean overwrite,
    GError ** error)
{
  GESAsset *tl_asset;
  gboolean ret = TRUE;
  GESFormatter *formatter = NULL;

  g_return_val_if_fail (GES_IS_PROJECT (project), FALSE);
  g_return_val_if_fail (formatter_asset == NULL ||
      g_type_is_a (ges_asset_get_extractable_type (formatter_asset),
          GES_TYPE_FORMATTER), FALSE);
  g_return_val_if_fail ((error == NULL || *error == NULL), FALSE);

  tl_asset = ges_extractable_get_asset (GES_EXTRACTABLE (timeline));
  if (tl_asset == NULL && project->priv->uri == NULL) {
    GESAsset *asset = ges_asset_cache_lookup (GES_TYPE_PROJECT, uri);

    if (asset) {
      GST_WARNING_OBJECT (project, "Trying to save project to %s but we already"
          "have %" GST_PTR_FORMAT " for that uri, can not save", uri, asset);
      goto out;
    }

    GST_DEBUG_OBJECT (project, "Timeline %" GST_PTR_FORMAT " has no asset"
        " we have no uri set, so setting ourself as asset", timeline);

    ges_extractable_set_asset (GES_EXTRACTABLE (timeline), GES_ASSET (project));
  } else if (tl_asset != GES_ASSET (project)) {
    GST_WARNING_OBJECT (project, "Timeline %" GST_PTR_FORMAT
        " not created by this project can not save", timeline);

    ret = FALSE;
    goto out;
  }

  if (formatter_asset == NULL)
    formatter_asset = gst_object_ref (ges_formatter_get_default ());

  formatter = GES_FORMATTER (ges_asset_extract (formatter_asset, error));
  if (formatter == NULL) {
1082 1083 1084
    GST_WARNING_OBJECT (project, "Could not create the formatter %p %s: %s",
        formatter_asset, ges_asset_get_id (formatter_asset),
        (error && *error) ? (*error)->message : "Unknown Error");
1085 1086 1087 1088 1089 1090 1091 1092 1093 1094 1095 1096 1097 1098 1099 1100 1101 1102 1103 1104 1105 1106 1107 1108 1109 1110 1111 1112 1113 1114 1115 1116 1117 1118 1119 1120 1121 1122 1123 1124 1125 1126 1127 1128

    ret = FALSE;
    goto out;
  }

  ges_project_add_formatter (project, formatter);
  ret = ges_formatter_save_to_uri (formatter, timeline, uri, overwrite, error);
  if (ret && project->priv->uri == NULL)
    ges_project_set_uri (project, uri);

out:
  if (formatter_asset)
    gst_object_unref (formatter_asset);
  ges_project_remove_formatter (project, formatter);

  return ret;
}

/**
 * ges_project_new:
 * @uri: (allow-none): The uri to be set after creating the project.
 *
 * Creates a new #GESProject and sets its uri to @uri if provided. Note that
 * if @uri is not valid or %NULL, the uri of the project will then be set
 * the first time you save the project. If you then save the project to
 * other locations, it will never be updated again and the first valid URI is
 * the URI it will keep refering to.
 *
 * Returns: A newly created #GESProject
 */
GESProject *
ges_project_new (const gchar * uri)
{
  gchar *id = (gchar *) uri;
  GESProject *project;

  if (uri == NULL)
    id = g_strdup_printf ("project-%i", nb_projects++);

  project = GES_PROJECT (ges_asset_request (GES_TYPE_TIMELINE, id, NULL));

  if (project && uri)
    ges_project_set_uri (project, uri);

Tim-Philipp Müller's avatar
Tim-Philipp Müller committed
1129 1130 1131
  if (uri == NULL)
    g_free (id);

1132 1133 1134 1135 1136 1137 1138 1139 1140 1141 1142 1143 1144 1145 1146 1147 1148 1149 1150
  return project;
}

/**
 * ges_project_load:
 * @project: A #GESProject that has an @uri set already
 * @timeline: A blank timeline to load @project into
 * @error: (out) (allow-none): An error to be set in case something wrong happens or %NULL
 *
 * Loads @project into @timeline
 *
 * Returns: %TRUE if the project could be loaded %FALSE otherwize.
 */
gboolean
ges_project_load (GESProject * project, GESTimeline * timeline, GError ** error)
{
  g_return_val_if_fail (GES_IS_TIMELINE (timeline), FALSE);
  g_return_val_if_fail (GES_IS_PROJECT (project), FALSE);
  g_return_val_if_fail (ges_project_get_uri (project), FALSE);
1151
  g_return_val_if_fail (timeline->tracks == NULL, FALSE);
1152 1153 1154 1155 1156 1157 1158 1159 1160 1161 1162 1163 1164 1165 1166

  if (!_load_project (project, timeline, error))
    return FALSE;

  ges_extractable_set_asset (GES_EXTRACTABLE (timeline), GES_ASSET (project));

  return TRUE;
}

/**
 * ges_project_get_uri:
 * @project: A #GESProject
 *
 * Retrieve the uri that is currently set on @project
 *
Justin Kim's avatar
Justin Kim committed
1167
 * Returns: (transfer full) (nullable): a newly allocated string representing uri.
1168 1169 1170 1171 1172 1173 1174 1175 1176 1177 1178 1179 1180 1181 1182 1183 1184 1185 1186 1187 1188 1189 1190 1191 1192 1193 1194 1195 1196 1197 1198 1199 1200 1201 1202 1203 1204 1205 1206 1207 1208 1209 1210 1211 1212 1213 1214 1215 1216 1217 1218 1219 1220 1221 1222 1223 1224 1225 1226 1227 1228 1229 1230 1231 1232 1233 1234 1235 1236 1237 1238 1239 1240 1241
 */
gchar *
ges_project_get_uri (GESProject * project)
{
  GESProjectPrivate *priv;

  g_return_val_if_fail (GES_IS_PROJECT (project), FALSE);

  priv = project->priv;
  if (priv->uri)
    return g_strdup (priv->uri);
  return NULL;
}

/**
 * ges_project_add_encoding_profile:
 * @project: A #GESProject
 * @profile: A #GstEncodingProfile to add to the project. If a profile with
 * the same name already exists, it will be replaced
 *
 * Adds @profile to the project. It lets you save in what format
 * the project has been renders and keep a reference to those formats.
 * Also, those formats will be saves to the project file when possible.
 *
 * Returns: %TRUE if @profile could be added, %FALSE otherwize
 */
gboolean
ges_project_add_encoding_profile (GESProject * project,
    GstEncodingProfile * profile)
{
  GList *tmp;
  GESProjectPrivate *priv;

  g_return_val_if_fail (GES_IS_PROJECT (project), FALSE);
  g_return_val_if_fail (GST_IS_ENCODING_PROFILE (profile), FALSE);

  priv = project->priv;
  for (tmp = priv->encoding_profiles; tmp; tmp = tmp->next) {
    GstEncodingProfile *tmpprofile = GST_ENCODING_PROFILE (tmp->data);

    if (g_strcmp0 (gst_encoding_profile_get_name (tmpprofile),
            gst_encoding_profile_get_name (profile)) == 0) {
      GST_INFO_OBJECT (project, "Already have profile: %s, replacing it",
          gst_encoding_profile_get_name (profile));

      gst_object_unref (tmp->data);
      tmp->data = gst_object_ref (profile);
      return TRUE;
    }
  }

  priv->encoding_profiles = g_list_prepend (priv->encoding_profiles,
      gst_object_ref (profile));

  return TRUE;
}

/**
 * ges_project_list_encoding_profiles:
 * @project: A #GESProject
 *
 * Lists the encoding profile that have been set to @project. The first one
 * is the latest added.
 *
 * Returns: (transfer none) (element-type GstPbutils.EncodingProfile) (allow-none): The
 * list of #GstEncodingProfile used in @project
 */
const GList *
ges_project_list_encoding_profiles (GESProject * project)
{
  g_return_val_if_fail (GES_IS_PROJECT (project), FALSE);

  return project->priv->encoding_profiles;
}
1242 1243 1244 1245 1246 1247 1248 1249 1250 1251 1252 1253 1254 1255 1256 1257 1258 1259 1260 1261 1262 1263 1264 1265 1266 1267 1268

/**
 * ges_project_get_loading_assets:
 * @project: A #GESProject
 *
 * Get the assets that are being loaded
 *
 * Returns: (transfer full) (element-type GES.Asset): A set of loading asset
 * that will be added to @project. Note that those Asset are *not* loaded yet,
 * and thus can not be used
 */
GList *
ges_project_get_loading_assets (GESProject * project)
{
  GHashTableIter iter;
  gpointer key, value;

  GList *ret = NULL;

  g_return_val_if_fail (GES_IS_PROJECT (project), NULL);

  g_hash_table_iter_init (&iter, project->priv->loading_assets);
  while (g_hash_table_iter_next (&iter, &key, &value))
    ret = g_list_prepend (ret, gst_object_ref (value));

  return ret;
}