diff options
author | Regis Merlino <regis.merlino@intel.com> | 2013-03-11 10:54:56 -0700 |
---|---|---|
committer | Regis Merlino <regis.merlino@intel.com> | 2013-03-11 10:54:56 -0700 |
commit | c5a606affb909010ab43c1ef00e5750546fd1bb3 (patch) | |
tree | 19f1780f78caee4bfa33c9c42aa3f0b95dc68d29 /libdleyna/renderer/device.c | |
parent | 8de18eed056cefc2b91e6bb16e00e52378718ac0 (diff) | |
download | dleyna-renderer-c5a606affb909010ab43c1ef00e5750546fd1bb3.tar.gz |
[Architecture] Change directory structure to enable build from a master project
Signed-off-by: Regis Merlino <regis.merlino@intel.com>
Diffstat (limited to 'libdleyna/renderer/device.c')
-rw-r--r-- | libdleyna/renderer/device.c | 2373 |
1 files changed, 2373 insertions, 0 deletions
diff --git a/libdleyna/renderer/device.c b/libdleyna/renderer/device.c new file mode 100644 index 0000000..7ff06b8 --- /dev/null +++ b/libdleyna/renderer/device.c @@ -0,0 +1,2373 @@ +/* + * dLeyna + * + * Copyright (C) 2012-2013 Intel Corporation. All rights reserved. + * + * This program is free software; you can redistribute it and/or modify it + * under the terms and conditions of the GNU Lesser General Public License, + * version 2.1, as published by the Free Software Foundation. + * + * This program is distributed in the hope it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License + * for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin St - Fifth Floor, Boston, MA 02110-1301 USA. + * + * Mark Ryan <mark.d.ryan@intel.com> + * + */ + + +#include <string.h> +#include <math.h> + +#include <libgupnp/gupnp-control-point.h> +#include <libgupnp-av/gupnp-av.h> + +#include <libdleyna/core/error.h> +#include <libdleyna/core/log.h> +#include <libdleyna/core/service-task.h> + +#include "async.h" +#include "device.h" +#include "prop-defs.h" +#include "server.h" + +typedef void (*dlr_device_local_cb_t)(dlr_async_task_t *cb_data); + +typedef struct dlr_device_data_t_ dlr_device_data_t; +struct dlr_device_data_t_ { + dlr_device_local_cb_t local_cb; +}; + +/* Private structure used in chain task */ +typedef struct prv_new_device_ct_t_ prv_new_device_ct_t; +struct prv_new_device_ct_t_ { + dlr_device_t *dev; + const dleyna_connector_dispatch_cb_t *dispatch_table; +}; + +static void prv_last_change_cb(GUPnPServiceProxy *proxy, + const char *variable, + GValue *value, + gpointer user_data); + +static void prv_sink_change_cb(GUPnPServiceProxy *proxy, + const char *variable, + GValue *value, + gpointer user_data); + +static void prv_rc_last_change_cb(GUPnPServiceProxy *proxy, + const char *variable, + GValue *value, + gpointer user_data); + +static void prv_props_update(dlr_device_t *device, dlr_task_t *task); + +static void prv_unref_variant(gpointer variant) +{ + GVariant *var = variant; + if (var) + g_variant_unref(var); +} + +static void prv_props_init(dlr_props_t *props) +{ + props->root_props = g_hash_table_new_full(g_str_hash, g_str_equal, + NULL, prv_unref_variant); + props->player_props = g_hash_table_new_full(g_str_hash, g_str_equal, + NULL, prv_unref_variant); + props->device_props = g_hash_table_new_full(g_str_hash, g_str_equal, + NULL, prv_unref_variant); + props->synced = FALSE; +} + +static void prv_props_free(dlr_props_t *props) +{ + g_hash_table_unref(props->root_props); + g_hash_table_unref(props->player_props); + g_hash_table_unref(props->device_props); +} + +static void prv_service_proxies_free(dlr_service_proxies_t *service_proxies) +{ + if (service_proxies->av_proxy) + g_object_unref(service_proxies->av_proxy); + if (service_proxies->rc_proxy) + g_object_unref(service_proxies->rc_proxy); + if (service_proxies->cm_proxy) + g_object_unref(service_proxies->cm_proxy); +} + +static void prv_context_unsubscribe(dlr_device_context_t *ctx) +{ + DLEYNA_LOG_DEBUG("Enter"); + + if (ctx->timeout_id_cm) { + (void) g_source_remove(ctx->timeout_id_cm); + ctx->timeout_id_cm = 0; + } + if (ctx->timeout_id_av) { + (void) g_source_remove(ctx->timeout_id_av); + ctx->timeout_id_av = 0; + } + if (ctx->timeout_id_rc) { + (void) g_source_remove(ctx->timeout_id_rc); + ctx->timeout_id_rc = 0; + } + + if (ctx->subscribed_cm) { + (void) gupnp_service_proxy_remove_notify( + ctx->service_proxies.cm_proxy, "SinkProtocolInfo", + prv_sink_change_cb, ctx->device); + + gupnp_service_proxy_set_subscribed( + ctx->service_proxies.cm_proxy, FALSE); + + ctx->subscribed_cm = FALSE; + } + if (ctx->subscribed_av) { + (void) gupnp_service_proxy_remove_notify( + ctx->service_proxies.av_proxy, "LastChange", + prv_last_change_cb, ctx->device); + + gupnp_service_proxy_set_subscribed( + ctx->service_proxies.av_proxy, FALSE); + + ctx->subscribed_av = FALSE; + } + if (ctx->subscribed_rc) { + (void) gupnp_service_proxy_remove_notify( + ctx->service_proxies.rc_proxy, "LastChange", + prv_rc_last_change_cb, ctx->device); + + gupnp_service_proxy_set_subscribed( + ctx->service_proxies.rc_proxy, FALSE); + + ctx->subscribed_rc = FALSE; + } + + DLEYNA_LOG_DEBUG("Exit"); +} + +static void prv_dlr_context_delete(gpointer context) +{ + dlr_device_context_t *ctx = context; + + if (ctx) { + prv_context_unsubscribe(ctx); + + g_free(ctx->ip_address); + if (ctx->device_proxy) + g_object_unref(ctx->device_proxy); + prv_service_proxies_free(&ctx->service_proxies); + g_free(ctx); + } +} + +static void prv_change_props(GHashTable *props, + const gchar *key, + GVariant *value, + GVariantBuilder *changed_props_vb) +{ + g_hash_table_insert(props, (gpointer) key, value); + if (changed_props_vb) + g_variant_builder_add(changed_props_vb, "{sv}", key, value); +} + +static void prv_emit_signal_properties_changed(dlr_device_t *device, + const char *interface, + GVariant *changed_props) +{ +#if DLR_LOG_LEVEL & DLR_LOG_LEVEL_DEBUG + gchar *params; +#endif + GVariant *val = g_variant_ref_sink(g_variant_new("(s@a{sv}as)", + interface, + changed_props, + NULL)); + + DLEYNA_LOG_DEBUG("Emitted Signal: %s.%s - ObjectPath: %s", + DLR_INTERFACE_PROPERTIES, + DLR_INTERFACE_PROPERTIES_CHANGED, + device->path); + +#if DLR_LOG_LEVEL & DLR_LOG_LEVEL_DEBUG + params = g_variant_print(val, FALSE); + DLEYNA_LOG_DEBUG("Params: %s", params); + g_free(params); +#endif + + dlr_renderer_get_connector()->notify(device->connection, + device->path, + DLR_INTERFACE_PROPERTIES, + DLR_INTERFACE_PROPERTIES_CHANGED, + val, + NULL); + + g_variant_unref(val); +} + +static void prv_merge_meta_data(dlr_device_t *device, + const gchar *key, + GVariant *value, + GVariantBuilder *changed_props_vb) +{ + GVariant *current_meta_data; + GVariantIter viter; + GVariantBuilder *vb; + GVariant *val; + gchar *vkey; + gboolean replaced = FALSE; + GVariant *new_val; + + vb = g_variant_builder_new(G_VARIANT_TYPE("a{sv}")); + + current_meta_data = g_hash_table_lookup(device->props.player_props, + DLR_INTERFACE_PROP_METADATA); + if (current_meta_data) { + g_variant_iter_init(&viter, current_meta_data); + while (g_variant_iter_next(&viter, "{&sv}", &vkey, &val)) { + if (!strcmp(key, vkey)) { + new_val = value; + replaced = TRUE; + } else { + new_val = val; + } + g_variant_builder_add(vb, "{sv}", vkey, new_val); + g_variant_unref(val); + } + } + + if (!replaced) + g_variant_builder_add(vb, "{sv}", key, value); + + val = g_variant_ref_sink(g_variant_builder_end(vb)); + prv_change_props(device->props.player_props, + DLR_INTERFACE_PROP_METADATA, + val, + changed_props_vb); + g_variant_builder_unref(vb); +} + +static void prv_context_new(const gchar *ip_address, + GUPnPDeviceProxy *proxy, + dlr_device_t *device, + dlr_device_context_t **context) +{ + const gchar *cm_type = + "urn:schemas-upnp-org:service:ConnectionManager"; + const gchar *av_type = + "urn:schemas-upnp-org:service:AVTransport"; + const gchar *rc_type = + "urn:schemas-upnp-org:service:RenderingControl"; + dlr_device_context_t *ctx = g_new(dlr_device_context_t, 1); + dlr_service_proxies_t *service_proxies = &ctx->service_proxies; + + ctx->ip_address = g_strdup(ip_address); + ctx->device_proxy = proxy; + ctx->device = device; + ctx->subscribed_av = FALSE; + ctx->subscribed_cm = FALSE; + ctx->subscribed_rc = FALSE; + ctx->timeout_id_av = 0; + ctx->timeout_id_cm = 0; + ctx->timeout_id_rc = 0; + + g_object_ref(proxy); + + service_proxies->cm_proxy = (GUPnPServiceProxy *) + gupnp_device_info_get_service((GUPnPDeviceInfo *)proxy, + cm_type); + service_proxies->av_proxy = (GUPnPServiceProxy *) + gupnp_device_info_get_service((GUPnPDeviceInfo *)proxy, + av_type); + service_proxies->rc_proxy = (GUPnPServiceProxy *) + gupnp_device_info_get_service((GUPnPDeviceInfo *)proxy, + rc_type); + + *context = ctx; +} + +static dlr_device_context_t *prv_device_get_subscribed_context( + const dlr_device_t *device) +{ + dlr_device_context_t *context; + unsigned int i; + + for (i = 0; i < device->contexts->len; ++i) { + context = g_ptr_array_index(device->contexts, i); + if (context->subscribed_av || context->subscribed_cm || + context->subscribed_rc) + goto on_found; + } + + return NULL; + +on_found: + + return context; +} + +static void prv_device_append_new_context(dlr_device_t *device, + const gchar *ip_address, + GUPnPDeviceProxy *proxy) +{ + dlr_device_context_t *new_context; + + prv_context_new(ip_address, proxy, device, &new_context); + g_ptr_array_add(device->contexts, new_context); +} + +static void prv_device_subscribe_context(dlr_device_t *device) +{ + dlr_device_context_t *subscribed_context; + dlr_device_context_t *preferred_context; + + subscribed_context = prv_device_get_subscribed_context(device); + preferred_context = dlr_device_get_context(device); + + if (subscribed_context != preferred_context) { + if (subscribed_context) { + DLEYNA_LOG_DEBUG( + "Subscription switch from <%s> to <%s>", + subscribed_context->ip_address, + preferred_context->ip_address); + prv_context_unsubscribe(subscribed_context); + } + dlr_device_subscribe_to_service_changes(device); + } +} + +void dlr_device_append_new_context(dlr_device_t *device, + const gchar *ip_address, + GUPnPDeviceProxy *proxy) +{ + prv_device_append_new_context(device, ip_address, proxy); + prv_device_subscribe_context(device); +} + +void dlr_device_delete(void *device) +{ + unsigned int i; + dlr_device_t *dev = device; + + if (dev) { + if (dev->timeout_id) + (void) g_source_remove(dev->timeout_id); + + for (i = 0; i < DLR_INTERFACE_INFO_MAX && dev->ids[i]; ++i) + (void) dlr_renderer_get_connector()->unpublish_object( + dev->connection, + dev->ids[i]); + g_ptr_array_unref(dev->contexts); + g_free(dev->path); + prv_props_free(&dev->props); + + if (dev->transport_play_speeds != NULL) + g_ptr_array_free(dev->transport_play_speeds, TRUE); + g_free(dev->rate); + g_free(dev); + } +} + +void dlr_device_unsubscribe(void *device) +{ + unsigned int i; + dlr_device_t *dev = device; + dlr_device_context_t *context; + + if (dev) { + for (i = 0; i < dev->contexts->len; ++i) { + context = g_ptr_array_index(dev->contexts, i); + prv_context_unsubscribe(context); + } + } +} + +static gboolean prv_re_enable_cm_subscription(gpointer user_data) +{ + dlr_device_context_t *context = user_data; + + context->timeout_id_cm = 0; + + return FALSE; +} + +static void prv_cm_subscription_lost_cb(GUPnPServiceProxy *proxy, + const GError *reason, + gpointer user_data) +{ + dlr_device_context_t *context = user_data; + dlr_service_proxies_t *service_proxies = &context->service_proxies; + + if (!context->timeout_id_cm) { + gupnp_service_proxy_set_subscribed(service_proxies->cm_proxy, + TRUE); + context->timeout_id_cm = g_timeout_add_seconds(10, + prv_re_enable_cm_subscription, + context); + } else { + g_source_remove(context->timeout_id_cm); + (void) gupnp_service_proxy_remove_notify( + service_proxies->cm_proxy, "SinkProtocolInfo", + prv_sink_change_cb, context->device); + + context->timeout_id_cm = 0; + context->subscribed_cm = FALSE; + } +} + +static gboolean prv_re_enable_av_subscription(gpointer user_data) +{ + dlr_device_context_t *context = user_data; + + context->timeout_id_av = 0; + + return FALSE; +} + +static void prv_av_subscription_lost_cb(GUPnPServiceProxy *proxy, + const GError *reason, + gpointer user_data) +{ + dlr_device_context_t *context = user_data; + dlr_service_proxies_t *service_proxies = &context->service_proxies; + + if (!context->timeout_id_av) { + gupnp_service_proxy_set_subscribed(service_proxies->av_proxy, + TRUE); + context->timeout_id_av = g_timeout_add_seconds(10, + prv_re_enable_av_subscription, + context); + } else { + g_source_remove(context->timeout_id_av); + (void) gupnp_service_proxy_remove_notify( + service_proxies->av_proxy, "LastChange", + prv_last_change_cb, context->device); + + context->timeout_id_av = 0; + context->subscribed_av = FALSE; + } +} + +static gboolean prv_re_enable_rc_subscription(gpointer user_data) +{ + dlr_device_context_t *context = user_data; + + context->timeout_id_rc = 0; + + return FALSE; +} + +static void prv_rc_subscription_lost_cb(GUPnPServiceProxy *proxy, + const GError *reason, + gpointer user_data) +{ + dlr_device_context_t *context = user_data; + dlr_service_proxies_t *service_proxies = &context->service_proxies; + + if (!context->timeout_id_rc) { + gupnp_service_proxy_set_subscribed(service_proxies->rc_proxy, + TRUE); + context->timeout_id_rc = g_timeout_add_seconds(10, + prv_re_enable_rc_subscription, + context); + } else { + g_source_remove(context->timeout_id_rc); + (void) gupnp_service_proxy_remove_notify( + service_proxies->rc_proxy, "LastChange", + prv_rc_last_change_cb, context->device); + + context->timeout_id_rc = 0; + context->subscribed_rc = FALSE; + } +} + +void dlr_device_subscribe_to_service_changes(dlr_device_t *device) +{ + dlr_device_context_t *context; + dlr_service_proxies_t *service_proxies; + + context = dlr_device_get_context(device); + service_proxies = &context->service_proxies; + + DLEYNA_LOG_DEBUG("Subscribing through context <%s>", + context->ip_address); + + if (service_proxies->cm_proxy) { + gupnp_service_proxy_set_subscribed(service_proxies->cm_proxy, + TRUE); + (void) gupnp_service_proxy_add_notify(service_proxies->cm_proxy, + "SinkProtocolInfo", + G_TYPE_STRING, + prv_sink_change_cb, + device); + context->subscribed_cm = TRUE; + + g_signal_connect(service_proxies->cm_proxy, + "subscription-lost", + G_CALLBACK(prv_cm_subscription_lost_cb), + context); + } + + if (service_proxies->av_proxy) { + gupnp_service_proxy_set_subscribed(service_proxies->av_proxy, + TRUE); + (void) gupnp_service_proxy_add_notify(service_proxies->av_proxy, + "LastChange", + G_TYPE_STRING, + prv_last_change_cb, + device); + context->subscribed_av = TRUE; + + g_signal_connect(service_proxies->av_proxy, + "subscription-lost", + G_CALLBACK(prv_av_subscription_lost_cb), + context); + } + + if (service_proxies->rc_proxy) { + gupnp_service_proxy_set_subscribed(service_proxies->rc_proxy, + TRUE); + (void) gupnp_service_proxy_add_notify(service_proxies->rc_proxy, + "LastChange", + G_TYPE_STRING, + prv_rc_last_change_cb, + device); + context->subscribed_rc = TRUE; + + g_signal_connect(service_proxies->av_proxy, + "subscription-lost", + G_CALLBACK(prv_rc_subscription_lost_cb), + context); + } +} + +static void prv_as_prop_from_hash_table(const gchar *prop_name, + GHashTable *values, GHashTable *props) +{ + GVariantBuilder vb; + GHashTableIter iter; + gpointer key; + GVariant *val; + + g_variant_builder_init(&vb, G_VARIANT_TYPE("as")); + g_hash_table_iter_init(&iter, values); + + while (g_hash_table_iter_next(&iter, &key, NULL)) + g_variant_builder_add(&vb, "s", key); + + val = g_variant_ref_sink(g_variant_builder_end(&vb)); + g_hash_table_insert(props, (gchar *)prop_name, val); +} + +static void prv_process_protocol_info(dlr_device_t *device, + const gchar *protocol_info) +{ + GVariant *val; + gchar **entries; + gchar **type_info; + unsigned int i; + GHashTable *protocols; + GHashTable *types; + const char http_prefix[] = "http-"; + + DLEYNA_LOG_DEBUG("Enter"); + DLEYNA_LOG_DEBUG("prv_process_protocol_info: %s", protocol_info); + + protocols = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, + NULL); + types = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, NULL); + + val = g_variant_ref_sink(g_variant_new_string(protocol_info)); + g_hash_table_insert(device->props.device_props, + DLR_INTERFACE_PROP_PROTOCOL_INFO, + val); + + entries = g_strsplit(protocol_info, ",", 0); + + for (i = 0; entries[i]; ++i) { + type_info = g_strsplit(entries[i], ":", 0); + + if (type_info[0] && type_info[1] && type_info[2]) { + if (!g_ascii_strncasecmp(http_prefix, type_info[0], + sizeof(http_prefix) - 1)) { + type_info[0][sizeof(http_prefix) - 2] = 0; + } + + g_hash_table_insert(protocols, + g_ascii_strdown(type_info[0], -1), + NULL); + g_hash_table_insert(types, + g_ascii_strdown(type_info[2], -1), + NULL); + } + + g_strfreev(type_info); + } + + g_strfreev(entries); + + prv_as_prop_from_hash_table(DLR_INTERFACE_PROP_SUPPORTED_URIS, + protocols, + device->props.root_props); + + prv_as_prop_from_hash_table(DLR_INTERFACE_PROP_SUPPORTED_MIME, + types, + device->props.root_props); + + g_hash_table_unref(types); + g_hash_table_unref(protocols); + + DLEYNA_LOG_DEBUG("Exit"); +} + +static void prv_get_protocol_info_cb(GUPnPServiceProxy *proxy, + GUPnPServiceProxyAction *action, + gpointer user_data) +{ + gchar *result = NULL; + GError *error = NULL; + prv_new_device_ct_t *priv_t = (prv_new_device_ct_t *)user_data; + + DLEYNA_LOG_DEBUG("Enter"); + + if (!gupnp_service_proxy_end_action(proxy, action, &error, "Sink", + G_TYPE_STRING, &result, NULL)) { + DLEYNA_LOG_WARNING("GetProtocolInfo operation failed: %s", + error->message); + goto on_error; + } + + prv_process_protocol_info(priv_t->dev, result); + +on_error: + + if (error) + g_error_free(error); + + g_free(result); + + DLEYNA_LOG_DEBUG("Exit"); +} + +static GUPnPServiceProxyAction *prv_get_protocol_info( + dleyna_service_task_t *task, + GUPnPServiceProxy *proxy, + gboolean *failed) +{ + *failed = FALSE; + + return gupnp_service_proxy_begin_action( + proxy, "GetProtocolInfo", + dleyna_service_task_begin_action_cb, + task, NULL); +} + +static GUPnPServiceProxyAction *prv_subscribe(dleyna_service_task_t *task, + GUPnPServiceProxy *proxy, + gboolean *failed) +{ + dlr_device_t *device; + + DLEYNA_LOG_DEBUG("Enter"); + + device = (dlr_device_t *)dleyna_service_task_get_user_data(task); + + prv_device_subscribe_context(device); + + *failed = FALSE; + + DLEYNA_LOG_DEBUG("Exit"); + + return NULL; +} + +static GUPnPServiceProxyAction *prv_declare(dleyna_service_task_t *task, + GUPnPServiceProxy *proxy, + gboolean *failed) +{ + unsigned int i; + dlr_device_t *device; + prv_new_device_ct_t *priv_t; + const dleyna_connector_dispatch_cb_t *table; + + DLEYNA_LOG_DEBUG("Enter"); + + *failed = FALSE; + + priv_t = (prv_new_device_ct_t *)dleyna_service_task_get_user_data(task); + device = priv_t->dev; + + table = priv_t->dispatch_table; + + for (i = 0; i < DLR_INTERFACE_INFO_MAX; ++i) { + device->ids[i] = dlr_renderer_get_connector()->publish_object( + device->connection, + device->path, + FALSE, + i, + table + i); + + if (!device->ids[i]) { + *failed = TRUE; + goto on_error; + } + } + +on_error: + +DLEYNA_LOG_DEBUG("Exit"); + + return NULL; +} + +dlr_device_t *dlr_device_new( + dleyna_connector_id_t connection, + GUPnPDeviceProxy *proxy, + const gchar *ip_address, + guint counter, + const dleyna_connector_dispatch_cb_t *dispatch_table, + const dleyna_task_queue_key_t *queue_id) +{ + dlr_device_t *dev = g_new0(dlr_device_t, 1); + prv_new_device_ct_t *priv_t; + gchar *new_path; + dlr_device_context_t *context; + GUPnPServiceProxy *s_proxy; + + DLEYNA_LOG_DEBUG("New Device on %s", ip_address); + + new_path = g_strdup_printf("%s/%u", DLEYNA_SERVER_PATH, counter); + DLEYNA_LOG_DEBUG("Server Path %s", new_path); + + dev = g_new0(dlr_device_t, 1); + priv_t = g_new0(prv_new_device_ct_t, 1); + + dev->connection = connection; + dev->contexts = g_ptr_array_new_with_free_func(prv_dlr_context_delete); + dev->path = new_path; + dev->rate = g_strdup("1"); + + priv_t->dev = dev; + priv_t->dispatch_table = dispatch_table; + + prv_props_init(&dev->props); + + prv_device_append_new_context(dev, ip_address, proxy); + + context = dlr_device_get_context(dev); + s_proxy = context->service_proxies.cm_proxy; + + dleyna_service_task_add(queue_id, prv_get_protocol_info, s_proxy, + prv_get_protocol_info_cb, NULL, priv_t); + + dleyna_service_task_add(queue_id, prv_subscribe, s_proxy, + NULL, NULL, dev); + + dleyna_service_task_add(queue_id, prv_declare, s_proxy, + NULL, g_free, priv_t); + + dleyna_task_queue_start(queue_id); + + DLEYNA_LOG_DEBUG("Exit"); + + return dev; +} + +dlr_device_t *dlr_device_from_path(const gchar *path, GHashTable *device_list) +{ + GHashTableIter iter; + gpointer value; + dlr_device_t *device; + dlr_device_t *retval = NULL; + + g_hash_table_iter_init(&iter, device_list); + while (g_hash_table_iter_next(&iter, NULL, &value)) { + device = value; + if (!strcmp(device->path, path)) { + retval = device; + break; + } + } + + return retval; +} + +dlr_device_context_t *dlr_device_get_context(dlr_device_t *device) +{ + dlr_device_context_t *context; + unsigned int i; + const char ip4_local_prefix[] = "127.0.0."; + + for (i = 0; i < device->contexts->len; ++i) { + context = g_ptr_array_index(device->contexts, i); + if (!strncmp(context->ip_address, ip4_local_prefix, + sizeof(ip4_local_prefix) - 1) || + !strcmp(context->ip_address, "::1") || + !strcmp(context->ip_address, "0:0:0:0:0:0:0:1")) + break; + } + + if (i == device->contexts->len) + context = g_ptr_array_index(device->contexts, 0); + + return context; +} + +static void prv_get_prop(dlr_async_task_t *cb_data) +{ + dlr_task_get_prop_t *get_prop = &cb_data->task.ut.get_prop; + GVariant *res = NULL; + + DLEYNA_LOG_DEBUG("Enter"); + + if (!strcmp(get_prop->interface_name, + DLEYNA_SERVER_INTERFACE_RENDERER_DEVICE)) { + res = g_hash_table_lookup(cb_data->device->props.device_props, + get_prop->prop_name); + } else if (!strcmp(get_prop->interface_name, DLR_INTERFACE_SERVER)) { + res = g_hash_table_lookup(cb_data->device->props.root_props, + get_prop->prop_name); + } else if (!strcmp(get_prop->interface_name, DLR_INTERFACE_PLAYER)) { + res = g_hash_table_lookup(cb_data->device->props.player_props, + get_prop->prop_name); + } else if (!strcmp(get_prop->interface_name, "")) { + res = g_hash_table_lookup(cb_data->device->props.root_props, + get_prop->prop_name); + if (!res) + res = g_hash_table_lookup( + cb_data->device->props.player_props, + get_prop->prop_name); + + if (!res) + res = g_hash_table_lookup( + cb_data->device->props.device_props, + get_prop->prop_name); + } else { + cb_data->error = g_error_new(DLEYNA_SERVER_ERROR, + DLEYNA_ERROR_UNKNOWN_INTERFACE, + "Unknown Interface"); + } + + if (!res) { + if (!cb_data->error) + cb_data->error = + g_error_new(DLEYNA_SERVER_ERROR, + DLEYNA_ERROR_UNKNOWN_PROPERTY, + "Property not defined for object"); + } else { + cb_data->task.result = g_variant_ref(res); + } + + DLEYNA_LOG_DEBUG("Exit"); +} + +static void prv_add_props(GHashTable *props, GVariantBuilder *vb) +{ + GHashTableIter iter; + gpointer key; + gpointer value; + + g_hash_table_iter_init(&iter, props); + + while (g_hash_table_iter_next(&iter, &key, &value)) + g_variant_builder_add(vb, "{sv}", (gchar *)key, + (GVariant *)value); +} + +static void prv_get_props(dlr_async_task_t *cb_data) +{ + dlr_task_get_props_t *get_props = &cb_data->task.ut.get_props; + GVariantBuilder *vb; + + DLEYNA_LOG_DEBUG("Enter"); + + vb = g_variant_builder_new(G_VARIANT_TYPE("a{sv}")); + + if (!strcmp(get_props->interface_name, + DLEYNA_SERVER_INTERFACE_RENDERER_DEVICE)) { + prv_add_props(cb_data->device->props.device_props, vb); + } else if (!strcmp(get_props->interface_name, DLR_INTERFACE_SERVER)) { + prv_add_props(cb_data->device->props.root_props, vb); + prv_add_props(cb_data->device->props.device_props, vb); + } else if (!strcmp(get_props->interface_name, DLR_INTERFACE_PLAYER)) { + prv_add_props(cb_data->device->props.player_props, vb); + } else if (!strcmp(get_props->interface_name, "")) { + prv_add_props(cb_data->device->props.root_props, vb); + prv_add_props(cb_data->device->props.player_props, vb); + prv_add_props(cb_data->device->props.device_props, vb); + } else { + cb_data->error = g_error_new(DLEYNA_SERVER_ERROR, + DLEYNA_ERROR_UNKNOWN_INTERFACE, + "Unknown Interface"); + goto on_error; + } + + cb_data->task.result = g_variant_ref_sink(g_variant_builder_end(vb)); + +on_error: + + g_variant_builder_unref(vb); + + DLEYNA_LOG_DEBUG("Exit"); +} + +static const gchar *prv_map_transport_state(const gchar *upnp_state) +{ + const gchar *retval; + + if (!strcmp(upnp_state, "PLAYING")) + retval = "Playing"; + else if (!strcmp(upnp_state, "PAUSED_PLAYBACK") || + !strcmp(upnp_state, "PAUSED_RECORDING")) + retval = "Paused"; + else + retval = "Stopped"; + + return retval; +} + +static gdouble prv_map_transport_speed(const gchar *upnp_speed) +{ + gdouble retval = 1; + gchar **parts = NULL; + gint num; + gint dom; + + if (upnp_speed[0]) { + parts = g_strsplit(upnp_speed, "/", 0); + if (!parts[0]) + goto on_error; + + g_strstrip(parts[0]); + num = atoi(parts[0]); + + if (parts[1]) { + if (parts[2]) + goto on_error; + + g_strstrip(parts[1]); + dom = atoi(parts[1]); + if (dom == 0) + goto on_error; + + retval = num / (gdouble) dom; + } else { + retval = num; + } + } + +on_error: + + if (parts) + g_strfreev(parts); + + return retval; +} + +static void prv_add_actions(dlr_device_t *device, + const gchar *actions, + GVariantBuilder *changed_props_vb) +{ + gchar **parts; + unsigned int i = 0; + GVariant *true_val; + GVariant *false_val; + gboolean play = FALSE; + gboolean ppause = FALSE; + gboolean seek = FALSE; + gboolean next = FALSE; + gboolean previous = FALSE; + GVariant *val; + + parts = g_strsplit(actions, ",", 0); + + true_val = g_variant_ref_sink(g_variant_new_boolean(TRUE)); + false_val = g_variant_ref_sink(g_variant_new_boolean(FALSE)); + + while (parts[i]) { + g_strstrip(parts[i]); + + if (!strcmp(parts[i], "Play")) + play = TRUE; + else if (!strcmp(parts[i], "Pause")) + ppause = TRUE; + else if (!strcmp(parts[i], "Seek")) + seek = TRUE; + else if (!strcmp(parts[i], "Next")) + next = TRUE; + else if (!strcmp(parts[i], "Previous")) + previous = TRUE; + ++i; + } + + g_variant_ref(false_val); + prv_change_props(device->props.player_props, + DLR_INTERFACE_PROP_CAN_CONTROL, false_val, + changed_props_vb); + + val = play ? true_val : false_val; + g_variant_ref(val); + prv_change_props(device->props.player_props, + DLR_INTERFACE_PROP_CAN_PLAY, val, + changed_props_vb); + + val = ppause ? true_val : false_val; + g_variant_ref(val); + prv_change_props(device->props.player_props, + DLR_INTERFACE_PROP_CAN_PAUSE, val, + changed_props_vb); + + val = seek ? true_val : false_val; + g_variant_ref(val); + prv_change_props(device->props.player_props, + DLR_INTERFACE_PROP_CAN_SEEK, val, + changed_props_vb); + + val = next ? true_val : false_val; + g_variant_ref(val); + prv_change_props(device->props.player_props, + DLR_INTERFACE_PROP_CAN_NEXT, val, + changed_props_vb); + + val = previous ? true_val : false_val; + g_variant_ref(val); + prv_change_props(device->props.player_props, + DLR_INTERFACE_PROP_CAN_PREVIOUS, val, + changed_props_vb); + + g_variant_unref(true_val); + g_variant_unref(false_val); + g_strfreev(parts); +} + +static void prv_add_all_actions(dlr_device_t *device, + GVariantBuilder *changed_props_vb) +{ + GVariant *val; + + val = g_variant_ref_sink(g_variant_new_boolean(TRUE)); + prv_change_props(device->props.player_props, + DLR_INTERFACE_PROP_CAN_PLAY, val, + changed_props_vb); + prv_change_props(device->props.player_props, + DLR_INTERFACE_PROP_CAN_PAUSE, g_variant_ref(val), + changed_props_vb); + prv_change_props(device->props.player_props, + DLR_INTERFACE_PROP_CAN_SEEK, g_variant_ref(val), + changed_props_vb); + prv_change_props(device->props.player_props, + DLR_INTERFACE_PROP_CAN_NEXT, g_variant_ref(val), + changed_props_vb); + prv_change_props(device->props.player_props, + DLR_INTERFACE_PROP_CAN_PREVIOUS, g_variant_ref(val), + changed_props_vb); + prv_change_props(device->props.player_props, + DLR_INTERFACE_PROP_CAN_CONTROL, g_variant_ref(val), + changed_props_vb); +} + +static gint64 prv_duration_to_int64(const gchar *duration) +{ + gchar **parts; + unsigned int i = 0; + unsigned int count; + gint64 pos = 0; + + parts = g_strsplit(duration, ":", 0); + for (count = 0; parts[count]; ++count) + ; + + if (count != 3) + goto on_error; + + /* TODO: This does not handle fractional seconds */ + + i = 1; + do { + --count; + g_strstrip(parts[count]); + pos += atoi(parts[count]) * i; + i *= 60; + } while (count > 0); + + pos *= 1000000; + +on_error: + + g_strfreev(parts); + + return pos; +} + +static gchar *prv_int64_to_duration(gint64 micro_seconds) +{ + GString *retval; + unsigned int seconds; + + if (micro_seconds < 0) { + retval = g_string_new("-"); + micro_seconds = -micro_seconds; + } else { + retval = g_string_new(""); + } + + /* TODO: This does not handle fractional seconds */ + + seconds = micro_seconds / 1000000; + g_string_append_printf(retval, "%02u:%02u:%02u", + seconds / 3600, + (seconds / 60) % 60, + seconds % 60); + + return g_string_free(retval, FALSE); +} + +static void prv_add_reltime(dlr_device_t *device, + const gchar *reltime, + GVariantBuilder *changed_props_vb) +{ + GVariant *val; + gint64 pos = prv_duration_to_int64(reltime); + + val = g_variant_ref_sink(g_variant_new_int64(pos)); + prv_change_props(device->props.player_props, + DLR_INTERFACE_PROP_POSITION, val, + changed_props_vb); +} + +static void prv_found_item(GUPnPDIDLLiteParser *parser, + GUPnPDIDLLiteObject *object, + gpointer user_data) +{ + GVariantBuilder *vb = user_data; + gchar *track_id; + int track_number = gupnp_didl_lite_object_get_track_number(object); + GVariant *value; + const gchar *str_value; + GVariantBuilder *artists_vb; + GVariantBuilder *album_artists_vb; + GList *artists; + GList *head; + const gchar *artist_name; + const gchar *artist_role; + + track_id = g_strdup_printf(DLEYNA_SERVER_OBJECT"/track/%u", + track_number != -1 ? track_number : 0); + + value = g_variant_new_string(track_id); + g_variant_builder_add(vb, "{sv}", "mpris:trackid", value); + g_free(track_id); + + if (track_number != -1) { + value = g_variant_new_int32(track_number); + g_variant_builder_add(vb, "{sv}", "mpris:trackNumber", value); + } + + str_value = gupnp_didl_lite_object_get_title(object); + if (str_value) { + value = g_variant_new_string(str_value); + g_variant_builder_add(vb, "{sv}", "xesam:title", value); + } + + str_value = gupnp_didl_lite_object_get_album_art(object); + if (str_value) { + value = g_variant_new_string(str_value); + g_variant_builder_add(vb, "{sv}", "mpris:artUrl", value); + } + + str_value = gupnp_didl_lite_object_get_album(object); + if (str_value) { + value = g_variant_new_string(str_value); + g_variant_builder_add(vb, "{sv}", "xesam:album", value); + } + + str_value = gupnp_didl_lite_object_get_genre(object); + if (str_value) { + value = g_variant_new_string(str_value); + g_variant_builder_add(vb, "{sv}", "xesam:genre", value); + } + + artists = gupnp_didl_lite_object_get_artists(object); + head = artists; + + if (artists) { + artists_vb = g_variant_builder_new(G_VARIANT_TYPE("as")); + album_artists_vb = g_variant_builder_new(G_VARIANT_TYPE("as")); + do { + artist_name = + gupnp_didl_lite_contributor_get_name( + artists->data); + artist_role = gupnp_didl_lite_contributor_get_role( + artists->data); + if (!artist_role) + g_variant_builder_add(artists_vb, "s", + artist_name); + else if (!strcmp(artist_role, "AlbumArtist")) + g_variant_builder_add(album_artists_vb, "s", + artist_name); + g_object_unref(artists->data); + artists = g_list_next(artists); + } while (artists); + g_list_free(head); + value = g_variant_builder_end(artists_vb); + g_variant_builder_add(vb, "{sv}", "xesam:artist", value); + value = g_variant_builder_end(album_artists_vb); + g_variant_builder_add(vb, "{sv}", "xesam:albumArtist", value); + g_variant_builder_unref(artists_vb); + g_variant_builder_unref(album_artists_vb); + } +} + +static void prv_add_track_meta_data(dlr_device_t *device, + const gchar *metadata, + const gchar *duration, + const gchar *uri, + GVariantBuilder *changed_props_vb) +{ + gchar *didl = g_strdup_printf("<DIDL-Lite>%s</DIDL-Lite>", metadata); + GUPnPDIDLLiteParser *parser = NULL; + GVariantBuilder *vb; + GError *upnp_error = NULL; + GVariant *val; + gint error_code; + + parser = gupnp_didl_lite_parser_new(); + + vb = g_variant_builder_new(G_VARIANT_TYPE("a{sv}")); + + if (duration) { + val = g_variant_new_int64(prv_duration_to_int64(duration)); + g_variant_builder_add(vb, "{sv}", "mpris:length", val); + } + + if (uri) { + val = g_variant_new_string(uri); + g_variant_builder_add(vb, "{sv}", "xesam:url", val); + } + + g_signal_connect(parser, "object-available" , + G_CALLBACK(prv_found_item), vb); + + if (!gupnp_didl_lite_parser_parse_didl(parser, didl, &upnp_error)) { + error_code = upnp_error->code; + g_error_free(upnp_error); + if (error_code != GUPNP_XML_ERROR_EMPTY_NODE) + goto on_error; + } + + prv_change_props(device->props.player_props, + DLR_INTERFACE_PROP_METADATA, + g_variant_ref_sink(g_variant_builder_end(vb)), + changed_props_vb); + +on_error: + + if (parser) + g_object_unref(parser); + + g_variant_builder_unref(vb); + g_free(didl); +} + +static void prv_last_change_cb(GUPnPServiceProxy *proxy, + const char *variable, + GValue *value, + gpointer user_data) +{ + GUPnPLastChangeParser *parser; + dlr_device_t *device = user_data; + GVariantBuilder *changed_props_vb; + GVariant *changed_props; + gchar *meta_data = NULL; + gchar *actions = NULL; + gchar *play_speed = NULL; + gchar *state = NULL; + gchar *duration = NULL; + gchar *uri = NULL; + guint tracks_number = G_MAXUINT; + guint current_track = G_MAXUINT; + GVariant *val; + + parser = gupnp_last_change_parser_new(); + + if (!gupnp_last_change_parser_parse_last_change( + parser, 0, + g_value_get_string(value), + NULL, + "CurrentTrackMetaData", G_TYPE_STRING, &meta_data, + "CurrentTransportActions", G_TYPE_STRING, &actions, + "TransportPlaySpeed", G_TYPE_STRING, &play_speed, + "TransportState", G_TYPE_STRING, &state, + "CurrentTrackDuration", G_TYPE_STRING, &duration, + "CurrentTrackURI", G_TYPE_STRING, &uri, + "NumberOfTracks", G_TYPE_UINT, &tracks_number, + "CurrentTrack", G_TYPE_UINT, ¤t_track, + NULL)) + goto on_error; + + changed_props_vb = g_variant_builder_new(G_VARIANT_TYPE("a{sv}")); + + if (meta_data) { + prv_add_track_meta_data(device, + meta_data, + duration, + uri, + changed_props_vb); + g_free(meta_data); + } else { + if (duration) { + val = g_variant_new_int64(prv_duration_to_int64( + duration)); + val = g_variant_ref_sink(val); + prv_merge_meta_data(device, + "mpris:length", + val, + changed_props_vb); + g_variant_unref(val); + } + + if (uri) { + val = g_variant_ref_sink(g_variant_new_string(uri)); + prv_merge_meta_data(device, + "xesam:url", + val, + changed_props_vb); + g_variant_unref(val); + } + } + + g_free(duration); + g_free(uri); + + if (actions) { + prv_add_actions(device, actions, changed_props_vb); + g_free(actions); + } + + if (play_speed) { + val = g_variant_ref_sink( + g_variant_new_double( + prv_map_transport_speed(play_speed))); + prv_change_props(device->props.player_props, + DLR_INTERFACE_PROP_RATE, val, + changed_props_vb); + + g_free(device->rate); + device->rate = play_speed; + } + + if (state) { + val = g_variant_ref_sink( + g_variant_new_string( + prv_map_transport_state(state))); + prv_change_props(device->props.player_props, + DLR_INTERFACE_PROP_PLAYBACK_STATUS, val, + changed_props_vb); + g_free(state); + } + + if (tracks_number != G_MAXUINT) { + val = g_variant_ref_sink(g_variant_new_uint32(tracks_number)); + prv_change_props(device->props.player_props, + DLR_INTERFACE_PROP_NUMBER_OF_TRACKS, val, + changed_props_vb); + } + + if (current_track != G_MAXUINT) { + val = g_variant_ref_sink(g_variant_new_uint32(current_track)); + prv_change_props(device->props.player_props, + DLR_INTERFACE_PROP_CURRENT_TRACK, val, + changed_props_vb); + } + + changed_props = g_variant_ref_sink( + g_variant_builder_end(changed_props_vb)); + prv_emit_signal_properties_changed(device, + DLR_INTERFACE_PLAYER, + changed_props); + g_variant_unref(changed_props); + g_variant_builder_unref(changed_props_vb); + +on_error: + + g_object_unref(parser); +} + +static void prv_rc_last_change_cb(GUPnPServiceProxy *proxy, + const char *variable, + GValue *value, + gpointer user_data) +{ + GUPnPLastChangeParser *parser; + dlr_device_t *device = user_data; + GVariantBuilder *changed_props_vb; + GVariant *changed_props; + GVariant *val; + guint device_volume; + double mpris_volume; + + parser = gupnp_last_change_parser_new(); + + if (!gupnp_last_change_parser_parse_last_change( + parser, 0, + g_value_get_string(value), + NULL, + "Volume", G_TYPE_UINT, &device_volume, + NULL)) + goto on_error; + + if (device->props.synced == FALSE) + prv_props_update(device, NULL); + + if (device->max_volume == 0) + goto on_error; + + changed_props_vb = g_variant_builder_new(G_VARIANT_TYPE("a{sv}")); + + mpris_volume = (double)device_volume / (double)device->max_volume; + val = g_variant_ref_sink(g_variant_new_double(mpris_volume)); + prv_change_props(device->props.player_props, + DLR_INTERFACE_PROP_VOLUME, val, + changed_props_vb); + + changed_props = g_variant_ref_sink( + g_variant_builder_end(changed_props_vb)); + prv_emit_signal_properties_changed(device, + DLR_INTERFACE_PLAYER, + changed_props); + g_variant_unref(changed_props); + g_variant_builder_unref(changed_props_vb); + +on_error: + + g_object_unref(parser); +} + +static void prv_sink_change_cb(GUPnPServiceProxy *proxy, + const char *variable, + GValue *value, + gpointer user_data) +{ + dlr_device_t *device = user_data; + const gchar *sink; + + sink = g_value_get_string(value); + + if (sink) + prv_process_protocol_info(device, sink); +} + +static void prv_get_position_info_cb(GUPnPServiceProxy *proxy, + GUPnPServiceProxyAction *action, + gpointer user_data) +{ + gchar *rel_pos = NULL; + dlr_async_task_t *cb_data = user_data; + GError *upnp_error = NULL; + dlr_device_data_t *device_data = cb_data->private; + GVariantBuilder *changed_props_vb; + GVariant *changed_props; + + if (!gupnp_service_proxy_end_action(cb_data->proxy, cb_data->action, + &upnp_error, + "RelTime", + G_TYPE_STRING, &rel_pos, NULL)) { + cb_data->error = g_error_new(DLEYNA_SERVER_ERROR, + DLEYNA_ERROR_OPERATION_FAILED, + "GetPositionInfo operation failed: %s", + upnp_error->message); + g_error_free(upnp_error); + + goto on_error; + } + + changed_props_vb = g_variant_builder_new(G_VARIANT_TYPE("a{sv}")); + + g_strstrip(rel_pos); + prv_add_reltime(cb_data->device, rel_pos, changed_props_vb); + g_free(rel_pos); + + changed_props = g_variant_ref_sink( + g_variant_builder_end(changed_props_vb)); + prv_emit_signal_properties_changed(cb_data->device, + DLR_INTERFACE_PLAYER, + changed_props); + g_variant_unref(changed_props); + g_variant_builder_unref(changed_props_vb); + +on_error: + + device_data->local_cb(cb_data); +} + +static void prv_get_position_info(dlr_async_task_t *cb_data) +{ + dlr_device_context_t *context; + + context = dlr_device_get_context(cb_data->device); + + cb_data->cancel_id = + g_cancellable_connect(cb_data->cancellable, + G_CALLBACK(dlr_async_task_cancelled), + cb_data, NULL); + cb_data->proxy = context->service_proxies.av_proxy; + + g_object_add_weak_pointer((G_OBJECT(context->service_proxies.av_proxy)), + (gpointer *)&cb_data->proxy); + + cb_data->action = + gupnp_service_proxy_begin_action(cb_data->proxy, + "GetPositionInfo", + prv_get_position_info_cb, + cb_data, + "InstanceID", G_TYPE_INT, 0, + NULL); +} + +/***********************************************************************/ +/* Rational numbers parameters of the following functions are formed */ +/* like this : «2» «5/6». A decimal notation like «2.6» is not allowed */ +/***********************************************************************/ +static inline long prv_rational_get_numerator(const char *r) +{ + return strtol(r, NULL, 10); +} + +static long prv_rational_get_denominator(const char *r) +{ + char *div_pos = strstr(r, "/"); + if (div_pos == NULL) + goto exit; + + return strtol(div_pos + 1, NULL, 10); + +exit: + return 1; +} + +static double prv_rational_to_double(const char *r, double precision) +{ + long p; + long q; + double result; + + p = prv_rational_get_numerator(r); + if (p == 0) + goto error; + + q = prv_rational_get_denominator(r); + if (q == 0) + goto error; + + result = (double)p/(double)q; + + if (precision != 0) + result = round(result/precision) * precision; + + return result; + +error: + return 0.0; +} + +static inline gboolean prv_rational_is_invalid(const char *val) +{ + return (prv_rational_get_numerator(val) == 0) || + (prv_rational_get_denominator(val) == 0); +} + +static gint prv_compare_rationals(const gchar *a, const gchar *b) +{ + long a_numerator = prv_rational_get_numerator(a); + long b_numerator = prv_rational_get_numerator(b); + long a_denominator = prv_rational_get_denominator(a); + long b_denominator = prv_rational_get_denominator(b); + + return (a_numerator * b_denominator) - (b_numerator * a_denominator); +} + +static void prv_get_rates_values(const GUPnPServiceStateVariableInfo *svi, + GVariant **mpris_tp_speeds, + GPtrArray **upnp_tp_speeds, + double *min_rate, double *max_rate) +{ + char *rate; + char *min_rate_str; + char *max_rate_str; + GList *list; + GVariantBuilder vb; + const double precision = 0.01; + + if ((svi == NULL) || (svi->allowed_values == NULL)) + goto exit; + + g_variant_builder_init(&vb, G_VARIANT_TYPE("ad")); + + list = svi->allowed_values; + + min_rate_str = list->data; + max_rate_str = min_rate_str; + + if (*upnp_tp_speeds != NULL) + g_ptr_array_free(*upnp_tp_speeds, TRUE); + + *upnp_tp_speeds = g_ptr_array_new_with_free_func(g_free); + + for (; list != NULL; list = list->next) { + rate = (char *)list->data; + + if (prv_rational_is_invalid(rate)) + continue; + + g_ptr_array_add(*upnp_tp_speeds, g_strdup(rate)); + + g_variant_builder_add(&vb, "d", + prv_rational_to_double(rate, precision)); + + if (prv_compare_rationals(min_rate_str, rate) > 0) + min_rate_str = rate; + else if (prv_compare_rationals(max_rate_str, rate) < 0) + max_rate_str = rate; + } + + *mpris_tp_speeds = g_variant_builder_end(&vb); + + *min_rate = prv_rational_to_double(min_rate_str, precision); + *max_rate = prv_rational_to_double(max_rate_str, precision); + +exit: + return; +} + +static void prv_get_av_service_states_values(GUPnPServiceProxy *av_proxy, + GVariant **mpris_tp_speeds, + GPtrArray **upnp_tp_speeds, + double *min_rate, + double *max_rate) +{ + const GUPnPServiceStateVariableInfo *svi; + GUPnPServiceIntrospection *introspection; + GError *error = NULL; + + introspection = gupnp_service_info_get_introspection( + GUPNP_SERVICE_INFO(av_proxy), + &error); + if (error != NULL) { + DLEYNA_LOG_DEBUG( + "failed to fetch AV service introspection file"); + + g_error_free(error); + + goto exit; + } + + svi = gupnp_service_introspection_get_state_variable( + introspection, + "TransportPlaySpeed"); + + prv_get_rates_values(svi, + mpris_tp_speeds, upnp_tp_speeds, + min_rate, max_rate); + + g_object_unref(introspection); + +exit: + + return; +} + +static void prv_get_rc_service_states_values(GUPnPServiceProxy *rc_proxy, + guint *max_volume) +{ + const GUPnPServiceStateVariableInfo *svi; + GUPnPServiceIntrospection *introspection; + GError *error = NULL; + + introspection = gupnp_service_info_get_introspection( + GUPNP_SERVICE_INFO(rc_proxy), + &error); + if (error != NULL) { + DLEYNA_LOG_DEBUG( + "failed to fetch RC service introspection file"); + + g_error_free(error); + + goto exit; + } + + svi = gupnp_service_introspection_get_state_variable(introspection, + "Volume"); + if (svi != NULL) + *max_volume = g_value_get_uint(&svi->maximum); + + g_object_unref(introspection); + +exit: + + return; +} + +static void prv_update_device_props(GUPnPDeviceInfo *proxy, GHashTable *props) +{ + GVariant *val; + gchar *str; + + val = g_variant_ref_sink(g_variant_new_string( + gupnp_device_info_get_device_type(proxy))); + g_hash_table_insert(props, DLR_INTERFACE_PROP_DEVICE_TYPE, val); + + val = g_variant_ref_sink(g_variant_new_string( + gupnp_device_info_get_udn(proxy))); + g_hash_table_insert(props, DLR_INTERFACE_PROP_UDN, val); + + str = gupnp_device_info_get_friendly_name(proxy); + val = g_variant_ref_sink(g_variant_new_string(str)); + g_hash_table_insert(props, DLR_INTERFACE_PROP_FRIENDLY_NAME, val); + g_free(str); + + str = gupnp_device_info_get_icon_url(proxy, NULL, -1, -1, -1, FALSE, + NULL, NULL, NULL, NULL); + val = g_variant_ref_sink(g_variant_new_string(str)); + g_hash_table_insert(props, DLR_INTERFACE_PROP_ICON_URL, val); + g_free(str); + + str = gupnp_device_info_get_manufacturer(proxy); + val = g_variant_ref_sink(g_variant_new_string(str)); + g_hash_table_insert(props, DLR_INTERFACE_PROP_MANUFACTURER, val); + g_free(str); + + str = gupnp_device_info_get_manufacturer_url(proxy); + val = g_variant_ref_sink(g_variant_new_string(str)); + g_hash_table_insert(props, DLR_INTERFACE_PROP_MANUFACTURER_URL, val); + g_free(str); + + str = gupnp_device_info_get_model_description(proxy); + val = g_variant_ref_sink(g_variant_new_string(str)); + g_hash_table_insert(props, DLR_INTERFACE_PROP_MODEL_DESCRIPTION, val); + g_free(str); + + str = gupnp_device_info_get_model_name(proxy); + val = g_variant_ref_sink(g_variant_new_string(str)); + g_hash_table_insert(props, DLR_INTERFACE_PROP_MODEL_NAME, val); + g_free(str); + + str = gupnp_device_info_get_model_number(proxy); + val = g_variant_ref_sink(g_variant_new_string(str)); + g_hash_table_insert(props, DLR_INTERFACE_PROP_MODEL_NUMBER, val); + g_free(str); + + str = gupnp_device_info_get_serial_number(proxy); + val = g_variant_ref_sink(g_variant_new_string(str)); + g_hash_table_insert(props, DLR_INTERFACE_PROP_SERIAL_NUMBER, val); + g_free(str); + + str = gupnp_device_info_get_presentation_url(proxy); + val = g_variant_ref_sink(g_variant_new_string(str)); + g_hash_table_insert(props, DLR_INTERFACE_PROP_PRESENTATION_URL, val); + g_free(str); + +} + +static void prv_props_update(dlr_device_t *device, dlr_task_t *task) +{ + GVariant *val; + GUPnPDeviceInfo *info; + dlr_device_context_t *context; + dlr_service_proxies_t *service_proxies; + dlr_props_t *props = &device->props; + GVariantBuilder *changed_props_vb; + GVariant *changed_props; + double min_rate = 0; + double max_rate = 0; + GVariant *mpris_transport_play_speeds = NULL; + + context = dlr_device_get_context(device); + + val = g_variant_ref_sink(g_variant_new_boolean(FALSE)); + g_hash_table_insert(props->root_props, DLR_INTERFACE_PROP_CAN_QUIT, + val); + + g_hash_table_insert(props->root_props, DLR_INTERFACE_PROP_CAN_RAISE, + g_variant_ref(val)); + + g_hash_table_insert(props->root_props, + DLR_INTERFACE_PROP_CAN_SET_FULLSCREEN, + g_variant_ref(val)); + + g_hash_table_insert(props->root_props, + DLR_INTERFACE_PROP_HAS_TRACK_LIST, + g_variant_ref(val)); + + info = (GUPnPDeviceInfo *)context->device_proxy; + + prv_update_device_props(info, props->device_props); + + val = g_hash_table_lookup(props->device_props, + DLR_INTERFACE_PROP_FRIENDLY_NAME); + g_hash_table_insert(props->root_props, DLR_INTERFACE_PROP_IDENTITY, + g_variant_ref(val)); + + changed_props_vb = g_variant_builder_new(G_VARIANT_TYPE("a{sv}")); + + service_proxies = &context->service_proxies; + + if (service_proxies->av_proxy) + prv_get_av_service_states_values(service_proxies->av_proxy, + &mpris_transport_play_speeds, + &device->transport_play_speeds, + &min_rate, + &max_rate); + + if (service_proxies->rc_proxy) + prv_get_rc_service_states_values(service_proxies->rc_proxy, + &device->max_volume); + + if (min_rate != 0) { + val = g_variant_ref_sink(g_variant_new_double(min_rate)); + prv_change_props(device->props.player_props, + DLR_INTERFACE_PROP_MINIMUM_RATE, val, + changed_props_vb); + } + + if (max_rate != 0) { + val = g_variant_ref_sink(g_variant_new_double(max_rate)); + prv_change_props(device->props.player_props, + DLR_INTERFACE_PROP_MAXIMUM_RATE, + val, changed_props_vb); + } + + if (mpris_transport_play_speeds != NULL) { + val = g_variant_ref_sink(mpris_transport_play_speeds); + prv_change_props(device->props.player_props, + DLR_INTERFACE_PROP_TRANSPORT_PLAY_SPEEDS, + val, changed_props_vb); + } + + prv_add_all_actions(device, changed_props_vb); + device->props.synced = TRUE; + + changed_props = g_variant_ref_sink( + g_variant_builder_end(changed_props_vb)); + prv_emit_signal_properties_changed(device, + DLR_INTERFACE_PLAYER, + changed_props); + g_variant_unref(changed_props); + g_variant_builder_unref(changed_props_vb); +} + +static void prv_complete_get_prop(dlr_async_task_t *cb_data) +{ + prv_get_prop(cb_data); + (void) g_idle_add(dlr_async_task_complete, cb_data); + g_cancellable_disconnect(cb_data->cancellable, cb_data->cancel_id); +} + +static void prv_complete_get_props(dlr_async_task_t *cb_data) +{ + prv_get_props(cb_data); + (void) g_idle_add(dlr_async_task_complete, cb_data); + g_cancellable_disconnect(cb_data->cancellable, cb_data->cancel_id); +} + +static void prv_simple_call_cb(GUPnPServiceProxy *proxy, + GUPnPServiceProxyAction *action, + gpointer user_data) +{ + dlr_async_task_t *cb_data = user_data; + GError *upnp_error = NULL; + + if (!gupnp_service_proxy_end_action(cb_data->proxy, cb_data->action, + &upnp_error, NULL)) { + cb_data->error = g_error_new(DLEYNA_SERVER_ERROR, + DLEYNA_ERROR_OPERATION_FAILED, + "Operation failed: %s", + upnp_error->message); + g_error_free(upnp_error); + } + + (void) g_idle_add(dlr_async_task_complete, cb_data); + g_cancellable_disconnect(cb_data->cancellable, cb_data->cancel_id); +} + +static void prv_set_volume(dlr_async_task_t *cb_data, GVariant *params) +{ + double volume; + + volume = g_variant_get_double(params) * cb_data->device->max_volume; + + DLEYNA_LOG_INFO("Set device volume to %d/%d", (guint)volume, + cb_data->device->max_volume); + + cb_data->action = + gupnp_service_proxy_begin_action(cb_data->proxy, "SetVolume", + prv_simple_call_cb, cb_data, + "InstanceID", G_TYPE_INT, 0, + "Channel", + G_TYPE_STRING, "Master", + "DesiredVolume", + G_TYPE_UINT, (guint) volume, + NULL); +} + +static GVariant *prv_get_rate_value_from_double(GVariant *params, + gchar **upnp_rate, + dlr_async_task_t *cb_data) +{ + GVariant *val = NULL; + GVariant *tps; + GVariantIter iter; + double tps_value; + double mpris_rate; + GPtrArray *upnp_tp_speeds; + int i; + + tps = g_hash_table_lookup(cb_data->device->props.player_props, + DLR_INTERFACE_PROP_TRANSPORT_PLAY_SPEEDS); + + if (tps == NULL) { + cb_data->error = g_error_new(DLEYNA_SERVER_ERROR, + DLEYNA_ERROR_OPERATION_FAILED, + "TransportPlaySpeeds list is empty"); + goto exit; + } + + mpris_rate = g_variant_get_double(params); + + upnp_tp_speeds = cb_data->device->transport_play_speeds; + + i = 0; + + g_variant_iter_init(&iter, tps); + while (g_variant_iter_next(&iter, "d", &tps_value)) { + + if (fabs(mpris_rate - tps_value) <= 0.01) { + val = g_variant_ref_sink( + g_variant_new_double(tps_value)); + + *upnp_rate = g_ptr_array_index(upnp_tp_speeds, i); + + break; + } + + i++; + } + + if (val == NULL) + cb_data->error = + g_error_new(DLEYNA_SERVER_ERROR, DLEYNA_ERROR_BAD_QUERY, + "Value %.2f not in TransportPlaySpeeds", + mpris_rate); + +exit: + + return val; +} + +static void prv_set_rate(GVariant *params, dlr_async_task_t *cb_data) +{ + GVariant *val; + gchar *rate; + + if (g_variant_is_of_type(params, G_VARIANT_TYPE_DOUBLE) == FALSE) { + cb_data->error = g_error_new(DLEYNA_SERVER_ERROR, + DLEYNA_ERROR_BAD_QUERY, + "Parameter is not a double"); + goto exit; + } + + rate = NULL; + + val = prv_get_rate_value_from_double(params, &rate, cb_data); + if (val == NULL) + goto exit; + + g_free(cb_data->device->rate); + cb_data->device->rate = g_strdup(rate); + + DLEYNA_LOG_INFO("Set device rate to %s", cb_data->device->rate); + + prv_change_props(cb_data->device->props.player_props, + DLR_INTERFACE_PROP_RATE, val, NULL); + +exit: + + return; +} + +void dlr_device_set_prop(dlr_device_t *device, dlr_task_t *task, + dlr_upnp_task_complete_t cb) +{ + dlr_async_task_t *cb_data = (dlr_async_task_t *)task; + dlr_device_context_t *context; + dlr_task_set_prop_t *set_prop = &task->ut.set_prop; + + cb_data->cb = cb; + cb_data->device = device; + + if (g_strcmp0(set_prop->interface_name, DLR_INTERFACE_PLAYER) != 0 && + g_strcmp0(set_prop->interface_name, "") != 0) { + cb_data->error = g_error_new(DLEYNA_SERVER_ERROR, + DLEYNA_ERROR_UNKNOWN_INTERFACE, + "Interface %s not managed for property setting", + set_prop->interface_name); + goto exit; + } + + if (g_strcmp0(set_prop->prop_name, DLR_INTERFACE_PROP_RATE) == 0) { + prv_set_rate(set_prop->params, cb_data); + goto exit; + } + + if (g_strcmp0(set_prop->prop_name, DLR_INTERFACE_PROP_VOLUME) != 0) { + cb_data->error = g_error_new(DLEYNA_SERVER_ERROR, + DLEYNA_ERROR_UNKNOWN_PROPERTY, + "Property %s not managed for setting", + set_prop->prop_name); + goto exit; + } + + context = dlr_device_get_context(device); + + cb_data->cancel_id = + g_cancellable_connect(cb_data->cancellable, + G_CALLBACK(dlr_async_task_cancelled), + cb_data, NULL); + cb_data->proxy = context->service_proxies.rc_proxy; + + g_object_add_weak_pointer((G_OBJECT(context->service_proxies.rc_proxy)), + (gpointer *)&cb_data->proxy); + + prv_set_volume(cb_data, set_prop->params); + return; + +exit: + + g_idle_add(dlr_async_task_complete, cb_data); +} + +void dlr_device_get_prop(dlr_device_t *device, dlr_task_t *task, + dlr_upnp_task_complete_t cb) +{ + dlr_async_task_t *cb_data = (dlr_async_task_t *)task; + dlr_task_get_prop_t *get_prop = &task->ut.get_prop; + dlr_device_data_t *device_cb_data; + + /* Need to check to see if the property is DLR_INTERFACE_PROP_POSITION. + If it is we need to call GetPositionInfo. This value is not evented. + Otherwise we can just update the value straight away. */ + + if ((!strcmp(get_prop->interface_name, DLR_INTERFACE_PLAYER) || + !strcmp(get_prop->interface_name, "")) && + (!strcmp(task->ut.get_prop.prop_name, + DLR_INTERFACE_PROP_POSITION))) { + /* Need to read the current position. This property is not + evented */ + + device_cb_data = g_new(dlr_device_data_t, 1); + device_cb_data->local_cb = prv_complete_get_prop; + + cb_data->cb = cb; + cb_data->private = device_cb_data; + cb_data->free_private = g_free; + cb_data->device = device; + + prv_get_position_info(cb_data); + } else { + cb_data->cb = cb; + cb_data->device = device; + + if (!device->props.synced) + prv_props_update(device, task); + + prv_get_prop(cb_data); + (void) g_idle_add(dlr_async_task_complete, cb_data); + } +} + +void dlr_device_get_all_props(dlr_device_t *device, dlr_task_t *task, + dlr_upnp_task_complete_t cb) +{ + dlr_async_task_t *cb_data = (dlr_async_task_t *)task; + dlr_task_get_props_t *get_props = &task->ut.get_props; + dlr_device_data_t *device_cb_data; + + if (!device->props.synced) + prv_props_update(device, task); + + if ((!strcmp(get_props->interface_name, DLR_INTERFACE_PLAYER) || + !strcmp(get_props->interface_name, ""))) { + + /* Need to read the current position. This property is not + evented */ + + device_cb_data = g_new(dlr_device_data_t, 1); + device_cb_data->local_cb = prv_complete_get_props; + + cb_data->cb = cb; + cb_data->private = device_cb_data; + cb_data->device = device; + cb_data->free_private = g_free; + + prv_get_position_info(cb_data); + } else { + cb_data->cb = cb; + cb_data->device = device; + + prv_get_props(cb_data); + (void) g_idle_add(dlr_async_task_complete, cb_data); + } +} + +void dlr_device_play(dlr_device_t *device, dlr_task_t *task, + dlr_upnp_task_complete_t cb) +{ + dlr_device_context_t *context; + dlr_async_task_t *cb_data = (dlr_async_task_t *)task; + + DLEYNA_LOG_INFO("Play at speed %s", device->rate); + + context = dlr_device_get_context(device); + cb_data->cb = cb; + cb_data->device = device; + + cb_data->cancel_id = + g_cancellable_connect(cb_data->cancellable, + G_CALLBACK(dlr_async_task_cancelled), + cb_data, NULL); + cb_data->proxy = context->service_proxies.av_proxy; + + g_object_add_weak_pointer((G_OBJECT(context->service_proxies.av_proxy)), + (gpointer *)&cb_data->proxy); + + cb_data->action = + gupnp_service_proxy_begin_action(cb_data->proxy, + "Play", + prv_simple_call_cb, + cb_data, + "InstanceID", G_TYPE_INT, 0, + "Speed", G_TYPE_STRING, + device->rate, NULL); +} + +void dlr_device_play_pause(dlr_device_t *device, dlr_task_t *task, + dlr_upnp_task_complete_t cb) +{ + GVariant *state; + + state = g_hash_table_lookup(device->props.player_props, + DLR_INTERFACE_PROP_PLAYBACK_STATUS); + + if (state && !strcmp(g_variant_get_string(state, NULL), "Playing")) + dlr_device_pause(device, task, cb); + else + dlr_device_play(device, task, cb); +} + +static void prv_simple_command(dlr_device_t *device, dlr_task_t *task, + const gchar *command_name, + dlr_upnp_task_complete_t cb) +{ + dlr_device_context_t *context; + dlr_async_task_t *cb_data = (dlr_async_task_t *)task; + + DLEYNA_LOG_INFO("%s", command_name); + + context = dlr_device_get_context(device); + cb_data->cb = cb; + cb_data->device = device; + + cb_data->cancel_id = + g_cancellable_connect(cb_data->cancellable, + G_CALLBACK(dlr_async_task_cancelled), + cb_data, NULL); + cb_data->proxy = context->service_proxies.av_proxy; + + g_object_add_weak_pointer((G_OBJECT(context->service_proxies.av_proxy)), + (gpointer *)&cb_data->proxy); + + cb_data->action = + gupnp_service_proxy_begin_action(cb_data->proxy, + command_name, + prv_simple_call_cb, + cb_data, + "InstanceID", G_TYPE_INT, 0, + NULL); +} + +void dlr_device_pause(dlr_device_t *device, dlr_task_t *task, + dlr_upnp_task_complete_t cb) +{ + prv_simple_command(device, task, "Pause", cb); +} + +void dlr_device_stop(dlr_device_t *device, dlr_task_t *task, + dlr_upnp_task_complete_t cb) +{ + prv_simple_command(device, task, "Stop", cb); +} + +void dlr_device_next(dlr_device_t *device, dlr_task_t *task, + dlr_upnp_task_complete_t cb) +{ + prv_simple_command(device, task, "Next", cb); +} + +void dlr_device_previous(dlr_device_t *device, dlr_task_t *task, + dlr_upnp_task_complete_t cb) +{ + prv_simple_command(device, task, "Previous", cb); +} + +void dlr_device_open_uri(dlr_device_t *device, dlr_task_t *task, + dlr_upnp_task_complete_t cb) +{ + dlr_device_context_t *context; + dlr_async_task_t *cb_data = (dlr_async_task_t *)task; + dlr_task_open_uri_t *open_uri_data = &task->ut.open_uri; + + DLEYNA_LOG_INFO("URI: %s", open_uri_data->uri); + + context = dlr_device_get_context(device); + cb_data->cb = cb; + cb_data->device = device; + + cb_data->cancel_id = + g_cancellable_connect(cb_data->cancellable, + G_CALLBACK(dlr_async_task_cancelled), + cb_data, NULL); + cb_data->proxy = context->service_proxies.av_proxy; + + g_object_add_weak_pointer((G_OBJECT(context->service_proxies.av_proxy)), + (gpointer *)&cb_data->proxy); + + cb_data->action = + gupnp_service_proxy_begin_action(cb_data->proxy, + "SetAVTransportURI", + prv_simple_call_cb, + cb_data, + "InstanceID", G_TYPE_INT, 0, + "CurrentURI", G_TYPE_STRING, + open_uri_data->uri, + "CurrentURIMetaData", + G_TYPE_STRING, "", + NULL); +} + +static void prv_device_set_position(dlr_device_t *device, dlr_task_t *task, + const gchar *pos_type, + dlr_upnp_task_complete_t cb) +{ + dlr_device_context_t *context; + dlr_async_task_t *cb_data = (dlr_async_task_t *)task; + dlr_task_seek_t *seek_data = &task->ut.seek; + gchar *position; + + context = dlr_device_get_context(device); + cb_data->cb = cb; + cb_data->device = device; + + if (!strcmp(pos_type, "TRACK_NR")) + position = g_strdup_printf("%u", seek_data->track_number); + else + position = prv_int64_to_duration(seek_data->position); + + DLEYNA_LOG_INFO("set %s position : %s", pos_type, position); + + cb_data->cancel_id = + g_cancellable_connect(cb_data->cancellable, + G_CALLBACK(dlr_async_task_cancelled), + cb_data, NULL); + cb_data->cancellable = cb_data->cancellable; + cb_data->proxy = context->service_proxies.av_proxy; + + g_object_add_weak_pointer((G_OBJECT(context->service_proxies.av_proxy)), + (gpointer *)&cb_data->proxy); + + cb_data->action = + gupnp_service_proxy_begin_action(cb_data->proxy, + "Seek", + prv_simple_call_cb, + cb_data, + "InstanceID", G_TYPE_INT, 0, + "Unit", G_TYPE_STRING, + pos_type, + "Target", + G_TYPE_STRING, position, + NULL); + + g_free(position); +} + +void dlr_device_seek(dlr_device_t *device, dlr_task_t *task, + dlr_upnp_task_complete_t cb) +{ + prv_device_set_position(device, task, "REL_TIME", cb); +} + +void dlr_device_set_position(dlr_device_t *device, dlr_task_t *task, + dlr_upnp_task_complete_t cb) +{ + prv_device_set_position(device, task, "ABS_TIME", cb); +} + +void dlr_device_goto_track(dlr_device_t *device, dlr_task_t *task, + dlr_upnp_task_complete_t cb) +{ + prv_device_set_position(device, task, "TRACK_NR", cb); +} + +void dlr_device_host_uri(dlr_device_t *device, dlr_task_t *task, + dlr_host_service_t *host_service, + dlr_upnp_task_complete_t cb) +{ + dlr_device_context_t *context; + dlr_async_task_t *cb_data = (dlr_async_task_t *)task; + dlr_task_host_uri_t *host_uri = &task->ut.host_uri; + gchar *url; + GError *error = NULL; + + context = dlr_device_get_context(device); + url = dlr_host_service_add(host_service, context->ip_address, + host_uri->client, host_uri->uri, + &error); + + cb_data->cb = cb; + cb_data->device = device; + if (url) { + cb_data->task.result = g_variant_ref_sink( + g_variant_new_string(url)); + g_free(url); + } else { + cb_data->error = error; + } + + (void) g_idle_add(dlr_async_task_complete, cb_data); +} + +void dlr_device_remove_uri(dlr_device_t *device, dlr_task_t *task, + dlr_host_service_t *host_service, + dlr_upnp_task_complete_t cb) +{ + dlr_device_context_t *context; + dlr_async_task_t *cb_data = (dlr_async_task_t *)task; + dlr_task_host_uri_t *host_uri = &task->ut.host_uri; + + context = dlr_device_get_context(device); + cb_data->cb = cb; + cb_data->device = device; + + if (!dlr_host_service_remove(host_service, context->ip_address, + host_uri->client, host_uri->uri)) { + cb_data->error = g_error_new(DLEYNA_SERVER_ERROR, + DLEYNA_ERROR_OBJECT_NOT_FOUND, + "File not hosted for specified device"); + } + + (void) g_idle_add(dlr_async_task_complete, cb_data); +} |