diff --git a/src/modules/module-protocol-pulse/ext-stream-restore.c b/src/modules/module-protocol-pulse/ext-stream-restore.c index cb7108370..20e0dece6 100644 --- a/src/modules/module-protocol-pulse/ext-stream-restore.c +++ b/src/modules/module-protocol-pulse/ext-stream-restore.c @@ -37,15 +37,202 @@ static int do_extension_stream_restore_test(struct client *client, uint32_t comm return send_message(client, reply); } +static int key_from_name(const char *name, char *key, size_t maxlen) +{ + const char *media_class, *select, *str; + + if (strstr(name, "sink-input-") == name) + media_class = "Output/Audio"; + else if (strstr(name, "source-output-") == name) + media_class = "Input/Audio"; + else + return -1; + + if ((str = strstr(name, "-by-media-role:")) != NULL) { + const struct str_map *map; + str += strlen("-by-media-role:"); + map = str_map_find(media_role_map, NULL, str); + str = map ? map->pw_str : str; + select = "media.role"; + } + else if ((str = strstr(name, "-by-application-id:")) != NULL) { + str += strlen("-by-application-id:"); + select = "application.id"; + } + else if ((str = strstr(name, "-by-application-name:")) != NULL) { + str += strlen("-by-application-name:"); + select = "application.name"; + } + else if ((str = strstr(name, "-by-media-name:")) != NULL) { + str += strlen("-by-media-name:"); + select = "media.name"; + } else + return -1; + + snprintf(key, maxlen, "restore.stream.%s.%s:%s", + media_class, select, str); + return 0; +} + +static int key_to_name(const char *key, char *name, size_t maxlen) +{ + const char *type, *select, *str; + + if (strstr(key, "restore.stream.Output/Audio.") == key) + type = "sink-input"; + else if (strstr(key, "restore.stream.Input/Audio.") == key) + type = "source-output"; + else + type = "stream"; + + if ((str = strstr(key, ".media.role:")) != NULL) { + const struct str_map *map; + str += strlen(".media.role:"); + map = str_map_find(media_role_map, str, NULL); + select = "media-role"; + str = map ? map->pa_str : str; + } + else if ((str = strstr(key, ".application.id:")) != NULL) { + str += strlen(".application.id:"); + select = "application-id"; + } + else if ((str = strstr(key, ".application.name:")) != NULL) { + str += strlen(".application.name:"); + select = "application-name"; + } + else if ((str = strstr(key, ".media.name:")) != NULL) { + str += strlen(".media.name:"); + select = "media-name"; + } + else + return -1; + + snprintf(name, maxlen, "%s-by-%s:%s", type, select, str); + return 0; + +} + static int do_extension_stream_restore_read(struct client *client, uint32_t command, uint32_t tag, struct message *m) { struct message *reply; + struct volume vol = VOLUME_INIT; + struct channel_map map; + float volume; + char device_name[1024] = "\0"; + const struct spa_dict_item *item; + + spa_zero(map); + reply = reply_new(client, tag); + + spa_dict_for_each(item, &client->routes->dict) { + struct spa_json it[3]; + const char *value; + char name[1024]; + bool mute = false; + int len; + + if (key_to_name(item->key, name, sizeof(name)) < 0) + continue; + + pw_log_info("%s -> %s", item->key, name); + + spa_json_init(&it[0], item->value, strlen(item->value)); + if (spa_json_enter_object(&it[0], &it[1]) <= 0) + continue; + + while ((len = spa_json_next(&it[1], &value)) > 0) { + if (strncmp(value, "\"volume\"", len) == 0) { + if (spa_json_get_float(&it[1], &volume) <= 0) + continue; + } + else if (strncmp(value, "\"mute\"", len) == 0) { + if (spa_json_get_bool(&it[1], &mute) <= 0) + continue; + } + else if (strncmp(value, "\"volumes\"", len) == 0) { + if (spa_json_enter_array(&it[1], &it[2]) <= 0) + continue; + + for (vol.channels = 0; vol.channels < CHANNELS_MAX; vol.channels++) { + if (spa_json_get_float(&it[2], &vol.values[vol.channels]) <= 0) + break; + } + } + else if (strncmp(value, "\"target-node\"", len) == 0) { + if (spa_json_get_string(&it[1], device_name, sizeof(device_name)) <= 0) + continue; + } + else if (spa_json_next(&it[1], &value) <= 0) + break; + } + map.channels = vol.channels; + message_put(reply, + TAG_STRING, name, + TAG_CHANNEL_MAP, &map, + TAG_CVOLUME, &vol, + TAG_STRING, device_name[0] ? device_name : NULL, + TAG_BOOLEAN, mute, + TAG_INVALID); + } return send_message(client, reply); } static int do_extension_stream_restore_write(struct client *client, uint32_t command, uint32_t tag, struct message *m) { + int res; + uint32_t mode; + bool apply; + + if ((res = message_get(m, + TAG_U32, &mode, + TAG_BOOLEAN, &apply, + TAG_INVALID)) < 0) + return -EPROTO; + + while (m->offset < m->length) { + const char *name, *device_name; + struct channel_map map; + struct volume vol; + bool mute = false; + uint32_t i; + FILE *f; + char *ptr; + size_t size; + char key[1024]; + + message_get(m, + TAG_STRING, &name, + TAG_CHANNEL_MAP, &map, + TAG_CVOLUME, &vol, + TAG_STRING, &device_name, + TAG_BOOLEAN, &mute, + TAG_INVALID); + + if (name == NULL || name[0] == '\0') + return -EPROTO; + + f = open_memstream(&ptr, &size); + fprintf(f, "{"); + fprintf(f, " \"mute\": %s ", mute ? "true" : "false"); + fprintf(f, ", \"volumes\": ["); + for (i = 0; i < vol.channels; i++) + fprintf(f, "%s%f", (i == 0 ? " ":", "), vol.values[i]); + fprintf(f, " ] "); + if (device_name != NULL && device_name[0]) + fprintf(f, ", \"target-node\": \"%s\"", device_name); + fprintf(f, "}"); + fclose(f); + + if (key_from_name(name, key, sizeof(key)) >= 0) { + pw_log_info("%s -> %s", name, key); + pw_manager_set_metadata(client->manager, + client->metadata_routes, + PW_ID_CORE, key, "Spa:String:JSON", "%s", ptr); + } + free(ptr); + } + return reply_simple_ack(client, tag); } diff --git a/src/modules/module-protocol-pulse/json.h b/src/modules/module-protocol-pulse/json.h new file mode 100644 index 000000000..6fd354818 --- /dev/null +++ b/src/modules/module-protocol-pulse/json.h @@ -0,0 +1,296 @@ +/* Simple Plugin API + * + * Copyright © 2020 Wim Taymans + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#ifndef SPA_UTILS_JSON_H +#define SPA_UTILS_JSON_H + +#ifdef __cplusplus +extern "C" { +#else +#include +#endif +#include +#include +#include + + +/* a simple JSON compatible tokenizer */ +struct spa_json { + const char *cur; + const char *end; + struct spa_json *parent; + uint32_t state; + uint32_t depth; +}; + +#define SPA_JSON_INIT(data,size) (struct spa_json) { (data), (data)+(size), } + +static inline void spa_json_init(struct spa_json * iter, const char *data, size_t size) +{ + *iter = SPA_JSON_INIT(data, size); +} +#define SPA_JSON_ENTER(iter) (struct spa_json) { (iter)->cur, (iter)->end, (iter), } + +static inline void spa_json_enter(struct spa_json * iter, struct spa_json * sub) +{ + *sub = SPA_JSON_ENTER(iter); +} + +/** Get the next token. \a value points to the token and the return value + * is the length. */ +static inline int spa_json_next(struct spa_json * iter, const char **value) +{ + int utf8_remain = 0; + enum { __NONE, __STRUCT, __BARE, __STRING, __UTF8, __ESC }; + + for (; iter->cur < iter->end; iter->cur++) { + unsigned char cur = (unsigned char)*iter->cur; + again: + switch (iter->state) { + case __NONE: + iter->state = __STRUCT; + iter->depth = 0; + goto again; + case __STRUCT: + switch (cur) { + case '\t': case ' ': case '\r': case '\n': case ':': case ',': + continue; + case '"': + *value = iter->cur; + iter->state = __STRING; + continue; + case '[': case '{': + *value = iter->cur; + if (++iter->depth > 1) + continue; + iter->cur++; + return 1; + case '}': case ']': + if (iter->depth == 0) { + if (iter->parent) + iter->parent->cur = iter->cur; + return 0; + } + --iter->depth; + continue; + case '-': case 'a' ... 'z': case 'A' ... 'Z': case '0' ... '9': + *value = iter->cur; + iter->state = __BARE; + continue; + } + return -1; + case __BARE: + switch (cur) { + case '\t': case ' ': case '\r': case '\n': + case ':': case ',': case ']': case '}': + iter->state = __STRUCT; + if (iter->depth > 0) + goto again; + return iter->cur - *value; + default: + if (cur >= 32 && cur <= 126) + continue; + } + return -1; + case __STRING: + switch (cur) { + case '\\': + iter->state = __ESC; + continue; + case '"': + iter->state = __STRUCT; + if (iter->depth > 0) + continue; + iter->cur++; + return iter->cur - *value; + case 240 ... 247: + utf8_remain++; + SPA_FALLTHROUGH; + case 224 ... 239: + utf8_remain++; + SPA_FALLTHROUGH; + case 192 ... 223: + utf8_remain++; + iter->state = __UTF8; + continue; + default: + if (cur >= 32 && cur <= 126) + continue; + } + return -1; + case __UTF8: + switch (cur) { + case 128 ... 191: + if (--utf8_remain == 0) + iter->state = __STRING; + continue; + } + return -1; + case __ESC: + switch (cur) { + case '"': case '\\': case '/': case 'b': case 'f': + case 'n': case 'r': case 't': case 'u': + iter->state = __STRING; + continue; + } + return -1; + } + } + return iter->depth == 0 ? 0 : -1; +} + +static inline int spa_json_enter_container(struct spa_json *iter, struct spa_json *sub, char type) +{ + const char *value; + if (spa_json_next(iter, &value) < 0 || *value != type) + return -1; + spa_json_enter(iter, sub); + return 1; +} + +/* object */ +static inline int spa_json_is_object(const char *val, int len) +{ + return len > 0 && *val == '{'; +} +static inline int spa_json_enter_object(struct spa_json *iter, struct spa_json *sub) +{ + return spa_json_enter_container(iter, sub, '{'); +} + +/* array */ +static inline bool spa_json_is_array(const char *val, int len) +{ + return len > 0 && *val == '['; +} +static inline int spa_json_enter_array(struct spa_json *iter, struct spa_json *sub) +{ + return spa_json_enter_container(iter, sub, '['); +} + +/* null */ +static inline bool spa_json_is_null(const char *val, int len) +{ + return len == 4 && strncmp(val, "null", 4) == 0; +} + +/* float */ +static inline int spa_json_parse_float(const char *val, int len, float *result) +{ + char *end; + *result = strtof(val, &end); + return end == val + len; +} +static inline bool spa_json_is_float(const char *val, int len) +{ + float dummy; + return spa_json_parse_float(val, len, &dummy); +} +static inline int spa_json_get_float(struct spa_json *iter, float *res) +{ + const char *value; + int len; + if ((len = spa_json_next(iter, &value)) <= 0) + return -1; + return spa_json_parse_float(value, len, res); +} + +/* bool */ +static inline bool spa_json_is_true(const char *val, int len) +{ + return len == 4 && strncmp(val, "true", 4) == 0; +} + +static inline bool spa_json_is_false(const char *val, int len) +{ + return len == 5 && strncmp(val, "false", 5) == 0; +} + +static inline bool spa_json_is_bool(const char *val, int len) +{ + return spa_json_is_true(val, len) || spa_json_is_false(val, len); +} + +static inline int spa_json_parse_bool(const char *val, int len, bool *result) +{ + if ((*result = spa_json_is_true(val, len))) + return 1; + if (!(*result = !spa_json_is_false(val, len))) + return 1; + return -1; +} +static inline int spa_json_get_bool(struct spa_json *iter, bool *res) +{ + const char *value; + int len; + if ((len = spa_json_next(iter, &value)) <= 0) + return -1; + return spa_json_parse_bool(value, len, res); +} + +/* string */ +static inline bool spa_json_is_string(const char *val, int len) +{ + return len > 1 && *val == '"'; +} + +static inline int spa_json_parse_string(const char *val, int len, char *result) +{ + const char *p; + if (!spa_json_is_string(val, len)) + return -1; + for (p = val+1; p < val + len-1; p++) { + if (*p == '\\') { + p++; + if (*p == 'n') + *result++ = '\n'; + else if (*p == 'r') + *result++ = '\r'; + else if (*p == 'b') + *result++ = '\b'; + else if (*p == 't') + *result++ = '\t'; + else + *result++ = *p; + } else + *result++ = *p; + } + *result++ = '\0'; + return 1; +} + +static inline int spa_json_get_string(struct spa_json *iter, char *res, int maxlen) +{ + const char *value; + int len; + if ((len = spa_json_next(iter, &value)) <= 0 || maxlen < len) + return -1; + return spa_json_parse_string(value, len, res); +} + +#ifdef __cplusplus +} /* extern "C" */ +#endif + +#endif /* SPA_UTILS_JSON_H */ diff --git a/src/modules/module-protocol-pulse/pulse-server.c b/src/modules/module-protocol-pulse/pulse-server.c index bdb51225b..0085e0276 100644 --- a/src/modules/module-protocol-pulse/pulse-server.c +++ b/src/modules/module-protocol-pulse/pulse-server.c @@ -67,6 +67,7 @@ #include "pulse-server.h" #include "defs.h" +#include "json.h" #include "format.c" #include "volume.c"