2 This file is part of PulseAudio.
4 Copyright 2005-2009 Lennart Poettering
6 PulseAudio is free software; you can redistribute it and/or modify
7 it under the terms of the GNU Lesser General Public License as published
8 by the Free Software Foundation; either version 2.1 of the License,
9 or (at your option) any later version.
11 PulseAudio is distributed in the hope that it will be useful, but
12 WITHOUT ANY WARRANTY; without even the implied warranty of
13 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14 General Public License for more details.
16 You should have received a copy of the GNU Lesser General Public License
17 along with PulseAudio; if not, write to the Free Software
18 Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
35 #include <pulse/xmalloc.h>
36 #include <pulse/util.h>
37 #include <pulse/i18n.h>
38 #include <pulse/utf8.h>
40 #include <pulsecore/sink.h>
41 #include <pulsecore/source.h>
42 #include <pulsecore/core-util.h>
43 #include <pulsecore/log.h>
44 #include <pulsecore/modargs.h>
45 #include <pulsecore/dbus-shared.h>
46 #include <pulsecore/endianmacros.h>
47 #include <pulsecore/namereg.h>
48 #include <pulsecore/mime-type.h>
49 #include <pulsecore/strbuf.h>
51 #include "module-rygel-media-server-symdef.h"
53 PA_MODULE_AUTHOR("Lennart Poettering");
54 PA_MODULE_DESCRIPTION("UPnP MediaServer Plugin for Rygel");
55 PA_MODULE_VERSION(PACKAGE_VERSION);
56 PA_MODULE_LOAD_ONCE(TRUE);
58 "display_name=<UPnP Media Server name>");
60 /* This implements http://live.gnome.org/action/edit/Rygel/MediaProviderSpec */
62 #define SERVICE_NAME "org.Rygel.MediaServer1.PulseAudio"
64 #define OBJECT_ROOT "/org/Rygel/MediaServer1/PulseAudio"
65 #define OBJECT_SINKS "/org/Rygel/MediaServer1/PulseAudio/Sinks"
66 #define OBJECT_SOURCES "/org/Rygel/MediaServer1/PulseAudio/Sources"
68 #define CONTAINER_INTROSPECT_XML_PREFIX \
69 DBUS_INTROSPECT_1_0_XML_DOCTYPE_DECL_NODE \
71 " <!-- If you are looking for documentation make sure to check out" \
72 " http://live.gnome.org/Rygel/MediaProviderSpec -->" \
73 " <interface name=\"org.Rygel.MediaContainer1\">" \
74 " <method name=\"GetContainers\">" \
75 " <arg name=\"children\" type=\"ao\" direction=\"out\"/>" \
77 " <method name=\"GetItems\">" \
78 " <arg name=\"children\" type=\"ao\" direction=\"out\"/>" \
80 " <signal name=\"ItemAdded\">" \
81 " <arg name=\"path\" type=\"o\"/>" \
83 " <signal name=\"ItemRemoved\">" \
84 " <arg name=\"path\" type=\"o\"/>" \
86 " <signal name=\"ContainerAdded\">" \
87 " <arg name=\"path\" type=\"o\"/>" \
89 " <signal name=\"ContainerRemoved\">" \
90 " <arg name=\"path\" type=\"o\"/>" \
93 " <interface name=\"org.Rygel.MediaObject1\">" \
94 " <property name=\"display-name\" type=\"s\" access=\"read\"/>" \
96 " <interface name=\"org.freedesktop.DBus.Properties\">" \
97 " <method name=\"Get\">" \
98 " <arg name=\"interface\" direction=\"in\" type=\"s\"/>" \
99 " <arg name=\"property\" direction=\"in\" type=\"s\"/>" \
100 " <arg name=\"value\" direction=\"out\" type=\"v\"/>" \
103 " <interface name=\"org.freedesktop.DBus.Introspectable\">" \
104 " <method name=\"Introspect\">" \
105 " <arg name=\"data\" type=\"s\" direction=\"out\"/>" \
109 #define CONTAINER_INTROSPECT_XML_POSTFIX \
112 #define ROOT_INTROSPECT_XML \
113 CONTAINER_INTROSPECT_XML_PREFIX \
114 "<node name=\"Sinks\"/>" \
115 "<node name=\"Sources\"/>" \
116 CONTAINER_INTROSPECT_XML_POSTFIX
118 #define ITEM_INTROSPECT_XML \
119 DBUS_INTROSPECT_1_0_XML_DOCTYPE_DECL_NODE \
121 " <!-- If you are looking for documentation make sure to check out" \
122 " http://live.gnome.org/Rygel/MediaProviderSpec -->" \
123 " <interface name=\"org.Rygel.MediaItem1\">" \
124 " <property name=\"urls\" type=\"as\" access=\"read\"/>" \
125 " <property name=\"mime-type\" type=\"s\" access=\"read\"/>" \
126 " <property name=\"type\" type=\"s\" access=\"read\"/>" \
128 " <interface name=\"org.Rygel.MediaObject1\">" \
129 " <property name=\"display-name\" type=\"s\" access=\"read\"/>" \
131 " <interface name=\"org.freedesktop.DBus.Properties\">" \
132 " <method name=\"Get\">" \
133 " <arg name=\"interface\" direction=\"in\" type=\"s\"/>" \
134 " <arg name=\"property\" direction=\"in\" type=\"s\"/>" \
135 " <arg name=\"value\" direction=\"out\" type=\"v\"/>" \
138 " <interface name=\"org.freedesktop.DBus.Introspectable\">" \
139 " <method name=\"Introspect\">" \
140 " <arg name=\"data\" type=\"s\" direction=\"out\"/>" \
146 static const char* const valid_modargs[] = {
155 pa_dbus_connection *bus;
156 pa_bool_t got_name:1;
160 pa_hook_slot *source_new_slot, *source_unlink_slot;
163 static void send_signal(struct userdata *u, pa_source *s, const char *name) {
169 pa_source_assert_ref(s);
171 if (u->core->state == PA_CORE_SHUTDOWN)
175 parent = OBJECT_SINKS;
176 child = pa_sprintf_malloc(OBJECT_SINKS "/%s", s->monitor_of->name);
178 parent = OBJECT_SOURCES;
179 child = pa_sprintf_malloc(OBJECT_SOURCES "/%s", s->name);
182 pa_assert_se(m = dbus_message_new_signal(parent, "org.Rygel.MediaContainer1", name));
183 pa_assert_se(dbus_message_append_args(m, DBUS_TYPE_OBJECT_PATH, &child, DBUS_TYPE_INVALID));
184 pa_assert_se(dbus_connection_send(pa_dbus_connection_get(u->bus), m, NULL));
188 dbus_message_unref(m);
191 static pa_hook_result_t source_new_cb(pa_core *c, pa_source *s, struct userdata *u) {
193 pa_source_assert_ref(s);
195 send_signal(u, s, "ItemAdded");
201 static pa_hook_result_t source_unlink_cb(pa_core *c, pa_source *s, struct userdata *u) {
203 pa_source_assert_ref(s);
205 send_signal(u, s, "ItemRemoved");
210 static pa_bool_t message_is_property_get(DBusMessage *m, const char *interface, const char *property) {
214 dbus_error_init(&error);
218 if (!dbus_message_is_method_call(m, "org.freedesktop.DBus.Properties", "Get"))
221 if (!dbus_message_get_args(m, &error, DBUS_TYPE_STRING, &i, DBUS_TYPE_STRING, &p, DBUS_TYPE_INVALID) || dbus_error_is_set(&error)) {
222 dbus_error_free(&error);
226 return pa_streq(i, interface) && pa_streq(p, property);
229 static void append_variant_string(DBusMessage *m, const char *s) {
230 DBusMessageIter iter, sub;
235 dbus_message_iter_init_append(m, &iter);
236 pa_assert_se(dbus_message_iter_open_container(&iter, DBUS_TYPE_VARIANT, "s", &sub));
237 pa_assert_se(dbus_message_iter_append_basic(&sub, DBUS_TYPE_STRING, &s));
238 pa_assert_se(dbus_message_iter_close_container(&iter, &sub));
241 static DBusHandlerResult root_handler(DBusConnection *c, DBusMessage *m, void *userdata) {
242 struct userdata *u = userdata;
243 DBusMessage *r = NULL;
247 if (dbus_message_is_method_call(m, "org.Rygel.MediaContainer1", "GetContainers")) {
248 const char * array[] = { OBJECT_SINKS, OBJECT_SOURCES };
249 const char ** parray = array;
251 pa_assert_se(r = dbus_message_new_method_return(m));
252 pa_assert_se(dbus_message_append_args(
254 DBUS_TYPE_ARRAY, DBUS_TYPE_OBJECT_PATH, &parray, 2,
257 } else if (dbus_message_is_method_call(m, "org.Rygel.MediaContainer1", "GetItems")) {
258 const char * array[] = { };
259 const char ** parray = array;
261 pa_assert_se(r = dbus_message_new_method_return(m));
262 pa_assert_se(dbus_message_append_args(
264 DBUS_TYPE_ARRAY, DBUS_TYPE_OBJECT_PATH, &parray, 0,
267 } else if (message_is_property_get(m, "org.Rygel.MediaObject1", "display-name")) {
268 pa_assert_se(r = dbus_message_new_method_return(m));
269 append_variant_string(r, u->display_name);
270 } else if (dbus_message_is_method_call(m, "org.freedesktop.DBus.Introspectable", "Introspect")) {
271 const char *xml = ROOT_INTROSPECT_XML;
273 pa_assert_se(r = dbus_message_new_method_return(m));
274 pa_assert_se(dbus_message_append_args(
276 DBUS_TYPE_STRING, &xml,
280 return DBUS_HANDLER_RESULT_NOT_YET_HANDLED;
283 pa_assert_se(dbus_connection_send(pa_dbus_connection_get(u->bus), r, NULL));
284 dbus_message_unref(r);
287 return DBUS_HANDLER_RESULT_HANDLED;
290 static DBusHandlerResult sinks_and_sources_handler(DBusConnection *c, DBusMessage *m, void *userdata) {
291 struct userdata *u = userdata;
292 DBusMessage *r = NULL;
297 path = dbus_message_get_path(m);
299 if (pa_streq(path, OBJECT_SINKS) || pa_streq(path, OBJECT_SOURCES)) {
301 /* Container nodes */
303 if (dbus_message_is_method_call(m, "org.Rygel.MediaContainer1", "GetContainers")) {
305 const char * array[] = { };
306 const char ** parray = array;
308 pa_assert_se(r = dbus_message_new_method_return(m));
309 pa_assert_se(dbus_message_append_args(
311 DBUS_TYPE_ARRAY, DBUS_TYPE_OBJECT_PATH, &parray, 0,
314 } else if (dbus_message_is_method_call(m, "org.Rygel.MediaContainer1", "GetItems")) {
319 if (pa_streq(path, OBJECT_SINKS))
320 n = pa_idxset_size(u->core->sinks);
322 n = pa_idxset_size(u->core->sources);
324 array = pa_xnew(char*, n);
326 if (pa_streq(path, OBJECT_SINKS)) {
329 PA_IDXSET_FOREACH(sink, u->core->sinks, idx)
330 array[i++] = pa_sprintf_malloc(OBJECT_SINKS "/%u", sink->index);
334 PA_IDXSET_FOREACH(source, u->core->sources, idx)
335 if (!source->monitor_of)
336 array[i++] = pa_sprintf_malloc(OBJECT_SOURCES "/%u", source->index);
341 pa_assert_se(r = dbus_message_new_method_return(m));
342 pa_assert_se(dbus_message_append_args(
344 DBUS_TYPE_ARRAY, DBUS_TYPE_OBJECT_PATH, &array, i,
348 pa_xfree(array[i-1]);
352 } else if (message_is_property_get(m, "org.Rygel.MediaObject1", "display-name")) {
354 if (pa_streq(path, OBJECT_SINKS)) {
355 pa_assert_se(r = dbus_message_new_method_return(m));
356 append_variant_string(r, _("Output Devices"));
358 pa_assert_se(r = dbus_message_new_method_return(m));
359 append_variant_string(r, _("Input Devices"));
362 } else if (dbus_message_is_method_call(m, "org.freedesktop.DBus.Introspectable", "Introspect")) {
367 sb = pa_strbuf_new();
368 pa_strbuf_puts(sb, CONTAINER_INTROSPECT_XML_PREFIX);
370 if (pa_streq(path, OBJECT_SINKS)) {
373 PA_IDXSET_FOREACH(sink, u->core->sinks, idx)
374 pa_strbuf_printf(sb, "<node name=\"%u\"/>", sink->index);
378 PA_IDXSET_FOREACH(source, u->core->sources, idx)
379 if (!source->monitor_of)
380 pa_strbuf_printf(sb, "<node name=\"%u\"/>", source->index);
383 pa_strbuf_puts(sb, CONTAINER_INTROSPECT_XML_POSTFIX);
384 xml = pa_strbuf_tostring_free(sb);
386 pa_assert_se(r = dbus_message_new_method_return(m));
387 pa_assert_se(dbus_message_append_args(
389 DBUS_TYPE_STRING, &xml,
396 pa_sink *sink = NULL;
397 pa_source *source = NULL;
401 if (pa_startswith(path, OBJECT_SINKS "/"))
402 sink = pa_namereg_get(u->core, path + sizeof(OBJECT_SINKS), PA_NAMEREG_SINK);
403 else if (pa_startswith(path, OBJECT_SOURCES "/"))
404 source = pa_namereg_get(u->core, path + sizeof(OBJECT_SOURCES), PA_NAMEREG_SOURCE);
406 if (!sink && !source)
407 return DBUS_HANDLER_RESULT_NOT_YET_HANDLED;
409 if (message_is_property_get(m, "org.Rygel.MediaObject1", "display-name")) {
410 pa_assert_se(r = dbus_message_new_method_return(m));
411 append_variant_string(r, pa_strna(pa_proplist_gets(sink ? sink->proplist : source->proplist, PA_PROP_DEVICE_DESCRIPTION)));
413 } else if (message_is_property_get(m, "org.Rygel.MediaItem1", "type")) {
414 pa_assert_se(r = dbus_message_new_method_return(m));
415 append_variant_string(r, "audio");
417 } else if (message_is_property_get(m, "org.Rygel.MediaItem1", "mime-type")) {
421 t = pa_sample_spec_to_mime_type_mimefy(&sink->sample_spec, &sink->channel_map);
423 t = pa_sample_spec_to_mime_type_mimefy(&source->sample_spec, &source->channel_map);
425 pa_assert_se(r = dbus_message_new_method_return(m));
426 append_variant_string(r, t);
428 } else if (message_is_property_get(m, "org.Rygel.MediaItem1", "urls")) {
430 char ** parray = array;
432 pa_assert_se(r = dbus_message_new_method_return(m));
434 array[0] = pa_sprintf_malloc("http://@ADDRESS@:4714/listen/source/%s",
435 sink ? sink->monitor_source->name : source->name);
437 pa_assert_se(dbus_message_append_args(
439 DBUS_TYPE_ARRAY, DBUS_TYPE_STRING, &parray, 1,
444 } else if (dbus_message_is_method_call(m, "org.freedesktop.DBus.Introspectable", "Introspect")) {
448 pa_assert_se(r = dbus_message_new_method_return(m));
449 pa_assert_se(dbus_message_append_args(
451 DBUS_TYPE_STRING, &xml,
455 return DBUS_HANDLER_RESULT_NOT_YET_HANDLED;
459 pa_assert_se(dbus_connection_send(pa_dbus_connection_get(u->bus), r, NULL));
460 dbus_message_unref(r);
463 return DBUS_HANDLER_RESULT_HANDLED;
466 int pa__init(pa_module *m) {
469 pa_modargs *ma = NULL;
473 static const DBusObjectPathVTable vtable_root = {
474 .message_function = root_handler,
476 static const DBusObjectPathVTable vtable_sinks_and_sources = {
477 .message_function = sinks_and_sources_handler,
480 dbus_error_init(&error);
482 if (!(ma = pa_modargs_new(m->argument, valid_modargs))) {
483 pa_log("Failed to parse module arguments.");
487 m->userdata = u = pa_xnew0(struct userdata, 1);
491 if ((t = pa_modargs_get_value(ma, "display_name", NULL)))
492 u->display_name = pa_utf8_filter(t);
496 h = pa_get_host_name_malloc();
497 u->display_name = pa_sprintf_malloc(_("Audio on %s"), h);
501 u->source_new_slot = pa_hook_connect(&m->core->hooks[PA_CORE_HOOK_SOURCE_PUT], PA_HOOK_LATE, (pa_hook_cb_t) source_new_cb, u);
502 u->source_unlink_slot = pa_hook_connect(&m->core->hooks[PA_CORE_HOOK_SOURCE_UNLINK], PA_HOOK_LATE, (pa_hook_cb_t) source_unlink_cb, u);
504 if (!(u->bus = pa_dbus_bus_get(m->core, DBUS_BUS_SESSION, &error))) {
505 pa_log("Failed to get session bus connection: %s", error.message);
509 pa_assert_se(dbus_connection_register_object_path(pa_dbus_connection_get(u->bus), OBJECT_ROOT, &vtable_root, u));
510 pa_assert_se(dbus_connection_register_fallback(pa_dbus_connection_get(u->bus), OBJECT_SINKS, &vtable_sinks_and_sources, u));
511 pa_assert_se(dbus_connection_register_fallback(pa_dbus_connection_get(u->bus), OBJECT_SOURCES, &vtable_sinks_and_sources, u));
513 if (dbus_bus_request_name(pa_dbus_connection_get(u->bus), SERVICE_NAME, DBUS_NAME_FLAG_DO_NOT_QUEUE, &error) != DBUS_REQUEST_NAME_REPLY_PRIMARY_OWNER) {
514 pa_log("Failed to request service name " SERVICE_NAME ": %s", error.message);
530 dbus_error_free(&error);
535 void pa__done(pa_module*m) {
539 if (!(u = m->userdata))
542 if (u->source_new_slot)
543 pa_hook_slot_free(u->source_new_slot);
544 if (u->source_unlink_slot)
545 pa_hook_slot_free(u->source_unlink_slot);
550 dbus_error_init(&error);
552 dbus_connection_unregister_object_path(pa_dbus_connection_get(u->bus), OBJECT_ROOT);
553 dbus_connection_unregister_object_path(pa_dbus_connection_get(u->bus), OBJECT_SINKS);
554 dbus_connection_unregister_object_path(pa_dbus_connection_get(u->bus), OBJECT_SOURCES);
557 if (dbus_bus_release_name(pa_dbus_connection_get(u->bus), SERVICE_NAME, &error) != DBUS_RELEASE_NAME_REPLY_RELEASED) {
558 pa_log("Failed to release service name " SERVICE_NAME ": %s", error.message);
559 dbus_error_free(&error);
563 pa_dbus_connection_unref(u->bus);
566 pa_xfree(u->display_name);