diff options
Diffstat (limited to 'ahl-binding')
-rw-r--r-- | ahl-binding/CMakeLists.txt | 50 | ||||
-rw-r--r-- | ahl-binding/ahl-apidef.h | 304 | ||||
-rw-r--r-- | ahl-binding/ahl-apidef.json | 565 | ||||
-rw-r--r-- | ahl-binding/ahl-binding.c | 1577 | ||||
-rw-r--r-- | ahl-binding/ahl-binding.h | 94 | ||||
-rw-r--r-- | ahl-binding/ahl-config.c | 178 | ||||
-rw-r--r-- | ahl-binding/ahl-deviceenum.c | 331 |
7 files changed, 3099 insertions, 0 deletions
diff --git a/ahl-binding/CMakeLists.txt b/ahl-binding/CMakeLists.txt new file mode 100644 index 0000000..44944e5 --- /dev/null +++ b/ahl-binding/CMakeLists.txt @@ -0,0 +1,50 @@ +########################################################################### +# Copyright 2017 Audiokinetic.com +# +# author: Francois Thibault <fthibault@audiokinetic.com> +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +########################################################################### + +# Add target to project dependency list +PROJECT_TARGET_ADD(audiohighlevel) + + # Define project Targets + ADD_LIBRARY(${TARGET_NAME} MODULE ahl-binding.c ahl-deviceenum.c ahl-config.c) + + # Binder exposes a unique public entry point + SET_TARGET_PROPERTIES(${TARGET_NAME} PROPERTIES + PREFIX "afb-" + LABELS "BINDINGV2" + LINK_FLAGS ${BINDINGS_LINK_FLAG} + OUTPUT_NAME ${TARGET_NAME} + ) + + # Change default OPenAPI file + SET_OPENAPI_FILENAME("ahl-apidef") + + # Define target includes + TARGET_INCLUDE_DIRECTORIES(${TARGET_NAME} + PUBLIC ${GLIB_PKG_INCLUDE_DIRS} + ) + + # Library dependencies (include updates automatically) + # Find package for GLIB does not seem to export + TARGET_LINK_LIBRARIES(${TARGET_NAME} + ahl-policy + ahl-utilities + afb-utilities + ${GLIB_PKG_LIBRARIES} + ${link_libraries} + ) + diff --git a/ahl-binding/ahl-apidef.h b/ahl-binding/ahl-apidef.h new file mode 100644 index 0000000..9002c6f --- /dev/null +++ b/ahl-binding/ahl-apidef.h @@ -0,0 +1,304 @@ + +static const char _afb_description_v2_ahl_4a[] = + "{\"openapi\":\"3.0.0\",\"info\":{\"description\":\"Audio high level API " + "for AGL applications\",\"title\":\"audiohighlevel\",\"version\":\"1.0\"," + "\"x-binding-c-generator\":{\"api\":\"ahl-4a\",\"version\":2,\"prefix\":\"" + "audiohlapi_\",\"postfix\":\"\",\"start\":null,\"onevent\":\"AhlOnEvent\"" + ",\"init\":\"AhlBindingInit\",\"scope\":\"\",\"private\":false}},\"server" + "s\":[{\"url\":\"ws://{host}:{port}/api/audiohl\",\"description\":\"Audio" + " high level API for AGL applications.\",\"variables\":{\"host\":{\"defau" + "lt\":\"localhost\"},\"port\":{\"default\":\"1234\"}},\"x-afb-events\":[{" + "\"$ref\":\"#/components/schemas/afb-event\"}]}],\"components\":{\"schema" + "s\":{\"afb-reply\":{\"$ref\":\"#/components/schemas/afb-reply-v2\"},\"af" + "b-event\":{\"$ref\":\"#/components/schemas/afb-event-v2\"},\"afb-reply-v" + "2\":{\"title\":\"Generic response.\",\"type\":\"object\",\"required\":[\"" + "jtype\",\"request\"],\"properties\":{\"jtype\":{\"type\":\"string\",\"co" + "nst\":\"afb-reply\"},\"request\":{\"type\":\"object\",\"required\":[\"st" + "atus\"],\"properties\":{\"status\":{\"type\":\"string\"},\"info\":{\"typ" + "e\":\"string\"},\"token\":{\"type\":\"string\"},\"uuid\":{\"type\":\"str" + "ing\"},\"reqid\":{\"type\":\"string\"}}},\"response\":{\"type\":\"object" + "\"}}},\"afb-event-v2\":{\"type\":\"object\",\"required\":[\"jtype\",\"ev" + "ent\"],\"properties\":{\"jtype\":{\"type\":\"string\",\"const\":\"afb-ev" + "ent\"},\"event\":{\"type\":\"string\"},\"data\":{\"type\":\"object\"}}}," + "\"endpoint_info\":{\"type\":\"object\",\"required\":[\"endpoint_id\",\"t" + "ype\",\"device_name\",\"device_uri\"],\"properties\":{\"endpoint_id\":{\"" + "type\":\"int\"},\"type\":{\"type\":\"enum\"},\"device_name\":{\"type\":\"" + "string\"},\"device_uri_type\":{\"type\":\"string\"}}},\"stream_info\":{\"" + "type\":\"object\",\"required\":[\"stream_id\",\"state\",\"mute\",\"endpo" + "int_info\"],\"properties\":{\"stream_id\":{\"type\":\"int\"},\"state\":{" + "\"type\":\"int\"},\"mute\":{\"type\":\"int\"},\"device_uri\":{\"type\":\"" + "string\"},\"$ref\":\"#/components/schemas/endpoint_info\"}}},\"x-permiss" + "ions\":{\"streamcontrol\":{\"permission\":\"urn:AGL:permission:audio:pub" + "lic:streamcontrol\"},\"endpointcontrol\":{\"permission\":\"urn:AGL:permi" + "ssion:audio:public:endpointcontrol\"},\"audiostream\":{\"permission\":\"" + "urn:AGL:permission:audio:public:audiostream\"},\"soundevent\":{\"permiss" + "ion\":\"urn:AGL:permission:audio:public:soundevent\"}},\"responses\":{\"" + "200\":{\"description\":\"A complex object array response\",\"content\":{" + "\"application/json\":{\"schema\":{\"$ref\":\"#/components/schemas/afb-re" + "ply\"}}}},\"400\":{\"description\":\"Invalid arguments\"}}},\"paths\":{\"" + "/get_sources\":{\"description\":\"Retrieve array of available audio sour" + "ces\",\"get\":{\"parameters\":[{\"in\":\"query\",\"name\":\"audio_role\"" + ",\"required\":true,\"schema\":{\"type\":\"string\"}}],\"responses\":{\"2" + "00\":{\"$ref\":\"#/components/responses/200\",\"response\":{\"descriptio" + "n\":\"Array of endpoint info structures\",\"type\":\"array\",\"items\":{" + "\"$ref\":\"#/components/schemas/endpoint_info\"}}},\"400\":{\"$ref\":\"#" + "/components/responses/400\"}}}},\"/get_sinks\":{\"description\":\"Retrie" + "ve array of available audio sinks\",\"get\":{\"parameters\":[{\"in\":\"q" + "uery\",\"name\":\"audio_role\",\"required\":true,\"schema\":{\"type\":\"" + "string\"}}],\"responses\":{\"200\":{\"$ref\":\"#/components/responses/20" + "0\",\"response\":{\"description\":\"Array of endpoint info structures\"," + "\"type\":\"array\",\"items\":{\"$ref\":\"#/components/schemas/endpoint_i" + "nfo\"}}},\"400\":{\"$ref\":\"#/components/responses/400\"}}}},\"/stream_" + "open\":{\"description\":\"Request opening a stream\",\"get\":{\"x-permis" + "sions\":{\"$ref\":\"#/components/x-permissions/audiostream\"},\"paramete" + "rs\":[{\"in\":\"query\",\"name\":\"audio_role\",\"required\":true,\"sche" + "ma\":{\"type\":\"string\"}},{\"in\":\"query\",\"name\":\"endpoint_type\"" + ",\"required\":true,\"schema\":{\"type\":\"enum\"}},{\"in\":\"query\",\"n" + "ame\":\"endpoint_id\",\"required\":false,\"schema\":{\"type\":\"int\"}}]" + ",\"responses\":{\"200\":{\"$ref\":\"#/components/responses/200\",\"respo" + "nse\":{\"description\":\"Stream information structure\",\"$ref\":\"#/com" + "ponents/schemas/stream_info\"}},\"400\":{\"$ref\":\"#/components/respons" + "es/400\"}}}},\"/stream_close\":{\"description\":\"Request closing a stre" + "am\",\"get\":{\"x-permissions\":{\"$ref\":\"#/components/x-permissions/a" + "udiostream\"},\"parameters\":[{\"in\":\"query\",\"name\":\"stream_id\",\"" + "required\":true,\"schema\":{\"type\":\"int\"}}],\"responses\":{\"200\":{" + "\"$ref\":\"#/components/responses/200\"},\"400\":{\"$ref\":\"#/component" + "s/responses/400\"}}}},\"/set_stream_state\":{\"description\":\"Change st" + "ream active state\",\"get\":{\"x-permissions\":{\"$ref\":\"#/components/" + "x-permissions/streamcontrol\"},\"parameters\":[{\"in\":\"query\",\"name\"" + ":\"stream_id\",\"required\":true,\"schema\":{\"type\":\"int\"}},{\"in\":" + "\"query\",\"name\":\"state\",\"required\":true,\"schema\":{\"type\":\"in" + "t\"}}],\"responses\":{\"200\":{\"$ref\":\"#/components/responses/200\"}," + "\"400\":{\"$ref\":\"#/components/responses/400\"}}}},\"/set_stream_mute\"" + ":{\"description\":\"Change stream mute state\",\"get\":{\"x-permissions\"" + ":{\"$ref\":\"#/components/x-permissions/streamcontrol\"},\"parameters\":" + "[{\"in\":\"query\",\"name\":\"stream_id\",\"required\":true,\"schema\":{" + "\"type\":\"int\"}},{\"in\":\"query\",\"name\":\"mute\",\"required\":true" + ",\"schema\":{\"type\":\"int\"}}],\"responses\":{\"200\":{\"$ref\":\"#/co" + "mponents/responses/200\"},\"400\":{\"$ref\":\"#/components/responses/400" + "\"}}}},\"/get_stream_info\":{\"description\":\"Retrieve stream informati" + "on\",\"get\":{\"parameters\":[{\"in\":\"query\",\"name\":\"stream_id\",\"" + "required\":true,\"schema\":{\"type\":\"int\"}}],\"responses\":{\"200\":{" + "\"$ref\":\"#/components/responses/200\",\"response\":{\"description\":\"" + "Stream information structure\",\"$ref\":\"#/components/schemas/stream_in" + "fo\"}},\"400\":{\"$ref\":\"#/components/responses/400\"}}}},\"/set_volum" + "e\":{\"description\":\"Set volume on endpoint\",\"get\":{\"x-permissions" + "\":{\"$ref\":\"#/components/x-permissions/endpointcontrol\"},\"parameter" + "s\":[{\"in\":\"query\",\"name\":\"endpoint_type\",\"required\":true,\"sc" + "hema\":{\"type\":\"enum\"}},{\"in\":\"query\",\"name\":\"endpoint_id\",\"" + "required\":true,\"schema\":{\"type\":\"int\"}},{\"in\":\"query\",\"name\"" + ":\"volume\",\"required\":true,\"schema\":{\"type\":\"string\"}}],\"respo" + "nses\":{\"200\":{\"$ref\":\"#/components/responses/200\"},\"400\":{\"$re" + "f\":\"#/components/responses/400\"}}}},\"/get_volume\":{\"description\":" + "\"Get endpoint volume\",\"get\":{\"parameters\":[{\"in\":\"query\",\"nam" + "e\":\"endpoint_type\",\"required\":true,\"schema\":{\"type\":\"enum\"}}," + "{\"in\":\"query\",\"name\":\"endpoint_id\",\"required\":true,\"schema\":" + "{\"type\":\"int\"}}],\"responses\":{\"200\":{\"$ref\":\"#/components/res" + "ponses/200\",\"response\":{\"description\":\"Endpoint volume value\",\"t" + "ype\":\"double\"}},\"400\":{\"$ref\":\"#/components/responses/400\"}}}}," + "\"/get_endpoint_info\":{\"description\":\"Retrieve endpoint information " + "including its properties\",\"get\":{\"parameters\":[{\"in\":\"query\",\"" + "name\":\"endpoint_type\",\"required\":true,\"schema\":{\"type\":\"enum\"" + "}},{\"in\":\"query\",\"name\":\"endpoint_id\",\"required\":false,\"schem" + "a\":{\"type\":\"int\"}}],\"responses\":{\"200\":{\"$ref\":\"#/components" + "/responses/200\"},\"400\":{\"$ref\":\"#/components/responses/400\"}}}},\"" + "/set_property\":{\"description\":\"Set endpoint property value\",\"get\"" + ":{\"x-permissions\":{\"$ref\":\"#/components/x-permissions/endpointcontr" + "ol\"},\"parameters\":[{\"in\":\"query\",\"name\":\"endpoint_type\",\"req" + "uired\":true,\"schema\":{\"type\":\"enum\"}},{\"in\":\"query\",\"name\":" + "\"endpoint_id\",\"required\":false,\"schema\":{\"type\":\"int\"}},{\"in\"" + ":\"query\",\"name\":\"property_name\",\"required\":true,\"schema\":{\"ty" + "pe\":\"string\"}},{\"in\":\"query\",\"name\":\"value\",\"required\":true" + ",\"schema\":{\"type\":\"string\"}}],\"responses\":{\"200\":{\"$ref\":\"#" + "/components/responses/200\"},\"400\":{\"$ref\":\"#/components/responses/" + "400\"}}}},\"/get_property\":{\"description\":\"Get endpoint property val" + "ue\",\"get\":{\"parameters\":[{\"in\":\"query\",\"name\":\"endpoint_type" + "\",\"required\":true,\"schema\":{\"type\":\"enum\"}},{\"in\":\"query\",\"" + "name\":\"endpoint_id\",\"required\":false,\"schema\":{\"type\":\"int\"}}" + ",{\"in\":\"query\",\"name\":\"property_name\",\"required\":true,\"schema" + "\":{\"type\":\"string\"}}],\"responses\":{\"200\":{\"$ref\":\"#/componen" + "ts/responses/200\",\"response\":{\"description\":\"Property value\",\"ty" + "pe\":\"double\"}},\"400\":{\"$ref\":\"#/components/responses/400\"}}}},\"" + "/get_list_actions\":{\"description\":\"Retrieve a list of supported acti" + "ons for a particular audio role\",\"get\":{\"parameters\":[{\"in\":\"que" + "ry\",\"name\":\"audio_role\",\"required\":true,\"schema\":{\"type\":\"st" + "ring\"}}],\"responses\":{\"200\":{\"$ref\":\"#/components/responses/200\"" + "},\"400\":{\"$ref\":\"#/components/responses/400\"}}}},\"/post_action\":" + "{\"description\":\"Post sound or audio device related action event (exte" + "ndable mechanism)\",\"get\":{\"x-permissions\":{\"$ref\":\"#/components/" + "x-permissions/soundevent\"},\"parameters\":[{\"in\":\"query\",\"name\":\"" + "action_name\",\"required\":true,\"schema\":{\"type\":\"string\"}},{\"in\"" + ":\"query\",\"name\":\"audio_role\",\"required\":true,\"schema\":{\"type\"" + ":\"string\"}},{\"in\":\"query\",\"name\":\"media_name\",\"required\":fal" + "se,\"schema\":{\"type\":\"string\"}},{\"in\":\"query\",\"name\":\"action" + "_context\",\"required\":false,\"schema\":{\"type\":\"object\"}}],\"respo" + "nses\":{\"200\":{\"$ref\":\"#/components/responses/200\"},\"400\":{\"$re" + "f\":\"#/components/responses/400\"}}}},\"/subscribe\":{\"description\":\"" + "Subscribe to audio high level events\",\"get\":{\"parameters\":[{\"in\":" + "\"query\",\"name\":\"events\",\"required\":true,\"schema\":{\"type\":\"a" + "rray\",\"items\":{\"type\":\"string\"}}}],\"responses\":{\"200\":{\"$ref" + "\":\"#/components/responses/200\"},\"400\":{\"$ref\":\"#/components/resp" + "onses/400\"}}}},\"/unsubscribe\":{\"description\":\"Unubscribe to audio " + "high level events\",\"get\":{\"parameters\":[{\"in\":\"query\",\"name\":" + "\"events\",\"required\":true,\"schema\":{\"type\":\"array\",\"items\":{\"" + "type\":\"string\"}}}],\"responses\":{\"200\":{\"$ref\":\"#/components/re" + "sponses/200\"},\"400\":{\"$ref\":\"#/components/responses/400\"}}}}}}" +; + +static const struct afb_auth _afb_auths_v2_ahl_4a[] = { + { .type = afb_auth_Permission, .text = "urn:AGL:permission:audio:public:audiostream" }, + { .type = afb_auth_Permission, .text = "urn:AGL:permission:audio:public:streamcontrol" }, + { .type = afb_auth_Permission, .text = "urn:AGL:permission:audio:public:endpointcontrol" }, + { .type = afb_auth_Permission, .text = "urn:AGL:permission:audio:public:soundevent" } +}; + + void audiohlapi_get_sources(struct afb_req req); + void audiohlapi_get_sinks(struct afb_req req); + void audiohlapi_stream_open(struct afb_req req); + void audiohlapi_stream_close(struct afb_req req); + void audiohlapi_set_stream_state(struct afb_req req); + void audiohlapi_set_stream_mute(struct afb_req req); + void audiohlapi_get_stream_info(struct afb_req req); + void audiohlapi_set_volume(struct afb_req req); + void audiohlapi_get_volume(struct afb_req req); + void audiohlapi_get_endpoint_info(struct afb_req req); + void audiohlapi_set_property(struct afb_req req); + void audiohlapi_get_property(struct afb_req req); + void audiohlapi_get_list_actions(struct afb_req req); + void audiohlapi_post_action(struct afb_req req); + void audiohlapi_subscribe(struct afb_req req); + void audiohlapi_unsubscribe(struct afb_req req); + +static const struct afb_verb_v2 _afb_verbs_v2_ahl_4a[] = { + { + .verb = "get_sources", + .callback = audiohlapi_get_sources, + .auth = NULL, + .info = "Retrieve array of available audio sources", + .session = AFB_SESSION_NONE_V2 + }, + { + .verb = "get_sinks", + .callback = audiohlapi_get_sinks, + .auth = NULL, + .info = "Retrieve array of available audio sinks", + .session = AFB_SESSION_NONE_V2 + }, + { + .verb = "stream_open", + .callback = audiohlapi_stream_open, + .auth = &_afb_auths_v2_ahl_4a[0], + .info = "Request opening a stream", + .session = AFB_SESSION_NONE_V2 + }, + { + .verb = "stream_close", + .callback = audiohlapi_stream_close, + .auth = &_afb_auths_v2_ahl_4a[0], + .info = "Request closing a stream", + .session = AFB_SESSION_NONE_V2 + }, + { + .verb = "set_stream_state", + .callback = audiohlapi_set_stream_state, + .auth = &_afb_auths_v2_ahl_4a[1], + .info = "Change stream active state", + .session = AFB_SESSION_NONE_V2 + }, + { + .verb = "set_stream_mute", + .callback = audiohlapi_set_stream_mute, + .auth = &_afb_auths_v2_ahl_4a[1], + .info = "Change stream mute state", + .session = AFB_SESSION_NONE_V2 + }, + { + .verb = "get_stream_info", + .callback = audiohlapi_get_stream_info, + .auth = NULL, + .info = "Retrieve stream information", + .session = AFB_SESSION_NONE_V2 + }, + { + .verb = "set_volume", + .callback = audiohlapi_set_volume, + .auth = &_afb_auths_v2_ahl_4a[2], + .info = "Set volume on endpoint", + .session = AFB_SESSION_NONE_V2 + }, + { + .verb = "get_volume", + .callback = audiohlapi_get_volume, + .auth = NULL, + .info = "Get endpoint volume", + .session = AFB_SESSION_NONE_V2 + }, + { + .verb = "get_endpoint_info", + .callback = audiohlapi_get_endpoint_info, + .auth = NULL, + .info = "Retrieve endpoint information including its properties", + .session = AFB_SESSION_NONE_V2 + }, + { + .verb = "set_property", + .callback = audiohlapi_set_property, + .auth = &_afb_auths_v2_ahl_4a[2], + .info = "Set endpoint property value", + .session = AFB_SESSION_NONE_V2 + }, + { + .verb = "get_property", + .callback = audiohlapi_get_property, + .auth = NULL, + .info = "Get endpoint property value", + .session = AFB_SESSION_NONE_V2 + }, + { + .verb = "get_list_actions", + .callback = audiohlapi_get_list_actions, + .auth = NULL, + .info = "Retrieve a list of supported actions for a particular audio role", + .session = AFB_SESSION_NONE_V2 + }, + { + .verb = "post_action", + .callback = audiohlapi_post_action, + .auth = &_afb_auths_v2_ahl_4a[3], + .info = "Post sound or audio device related action event (extendable mechanism)", + .session = AFB_SESSION_NONE_V2 + }, + { + .verb = "subscribe", + .callback = audiohlapi_subscribe, + .auth = NULL, + .info = "Subscribe to audio high level events", + .session = AFB_SESSION_NONE_V2 + }, + { + .verb = "unsubscribe", + .callback = audiohlapi_unsubscribe, + .auth = NULL, + .info = "Unubscribe to audio high level events", + .session = AFB_SESSION_NONE_V2 + }, + { + .verb = NULL, + .callback = NULL, + .auth = NULL, + .info = NULL, + .session = 0 + } +}; + +const struct afb_binding_v2 afbBindingV2 = { + .api = "ahl-4a", + .specification = _afb_description_v2_ahl_4a, + .info = "Audio high level API for AGL applications", + .verbs = _afb_verbs_v2_ahl_4a, + .preinit = NULL, + .init = AhlBindingInit, + .onevent = AhlOnEvent, + .noconcurrency = 0 +}; + diff --git a/ahl-binding/ahl-apidef.json b/ahl-binding/ahl-apidef.json new file mode 100644 index 0000000..81d410c --- /dev/null +++ b/ahl-binding/ahl-apidef.json @@ -0,0 +1,565 @@ +{ + "openapi": "3.0.0", + "info": { + "description": "Audio high level API for AGL applications", + "title": "audiohighlevel", + "version": "1.0", + "x-binding-c-generator": { + "api": "ahl-4a", + "version": 2, + "prefix": "audiohlapi_", + "postfix": "", + "start": null, + "onevent": "AhlOnEvent", + "init": "AhlBindingInit", + "scope": "", + "private": false + } + }, + "servers": [ + { + "url": "ws://{host}:{port}/api/audiohl", + "description": "Audio high level API for AGL applications.", + "variables": { + "host": { + "default": "localhost" + }, + "port": { + "default": "1234" + } + }, + "x-afb-events": [ + { + "$ref": "#/components/schemas/afb-event" + } + ] + } + ], + "components": { + "schemas": { + "afb-reply": { + "$ref": "#/components/schemas/afb-reply-v2" + }, + "afb-event": { + "$ref": "#/components/schemas/afb-event-v2" + }, + "afb-reply-v2": { + "title": "Generic response.", + "type": "object", + "required": ["jtype", "request"], + "properties": { + "jtype": { + "type": "string", + "const": "afb-reply" + }, + "request": { + "type": "object", + "required": ["status"], + "properties": { + "status": { + "type": "string" + }, + "info": { + "type": "string" + }, + "token": { + "type": "string" + }, + "uuid": { + "type": "string" + }, + "reqid": { + "type": "string" + } + } + }, + "response": { + "type": "object" + } + } + }, + "afb-event-v2": { + "type": "object", + "required": ["jtype", "event"], + "properties": { + "jtype": { + "type": "string", + "const": "afb-event" + }, + "event": { + "type": "string" + }, + "data": { + "type": "object" + } + } + }, + "endpoint_info": { + "type": "object", + "required": [ "endpoint_id", "type", "device_name", "device_uri" ], + "properties": { + "endpoint_id": { "type": "int" }, + "type": { "type": "enum" }, + "device_name": { "type": "string" }, + "device_uri_type": { "type": "string" } + } + }, + "stream_info": { + "type": "object", + "required": [ "stream_id", "state", "mute", "endpoint_info" ], + "properties": { + "stream_id": { "type": "int" }, + "state": { "type": "int" }, + "mute": { "type": "int" }, + "device_uri": { "type": "string" }, + "$ref": "#/components/schemas/endpoint_info" + } + } + }, + "x-permissions": { + "streamcontrol": { "permission": "urn:AGL:permission:audio:public:streamcontrol"}, + "endpointcontrol": { "permission": "urn:AGL:permission:audio:public:endpointcontrol"}, + "audiostream": { "permission": "urn:AGL:permission:audio:public:audiostream"}, + "soundevent": {"permission": "urn:AGL:permission:audio:public:soundevent"} + }, + "responses": { + "200": { + "description": "A complex object array response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/afb-reply" + } + } + } + }, + "400": { "description": "Invalid arguments" } + } + }, + "paths": { + "/get_sources": { + "description": "Retrieve array of available audio sources", + "get": { + "parameters": [ + { + "in": "query", + "name": "audio_role", + "required": true, + "schema": { "type": "string" } + } + ], + "responses": { + "200": { + "$ref": "#/components/responses/200", + "response": { + "description": "Array of endpoint info structures", + "type": "array", + "items": { "$ref": "#/components/schemas/endpoint_info"} + } + }, + "400": { "$ref": "#/components/responses/400" } + } + } + }, + "/get_sinks": { + "description": "Retrieve array of available audio sinks", + "get": { + "parameters": [ + { + "in": "query", + "name": "audio_role", + "required": true, + "schema": { "type": "string" } + } + ], + "responses": { + "200": { + "$ref": "#/components/responses/200", + "response": { + "description": "Array of endpoint info structures", + "type": "array", + "items": { "$ref": "#/components/schemas/endpoint_info"} + } + }, + "400": { "$ref": "#/components/responses/400" } + } + } + }, + "/stream_open": { + "description": "Request opening a stream", + "get": { + "x-permissions": { "$ref": "#/components/x-permissions/audiostream" }, + "parameters": [ + { + "in": "query", + "name": "audio_role", + "required": true, + "schema": { "type": "string" } + }, + { + "in": "query", + "name": "endpoint_type", + "required": true, + "schema": { "type": "enum" } + }, + { + "in": "query", + "name": "endpoint_id", + "required": false, + "schema": { "type": "int" } + } + ], + "responses": { + "200": { + "$ref": "#/components/responses/200", + "response": { + "description": "Stream information structure", + "$ref": "#/components/schemas/stream_info" + } + }, + "400": { "$ref": "#/components/responses/400" } + } + } + }, + "/stream_close": { + "description": "Request closing a stream", + "get": { + "x-permissions": { "$ref": "#/components/x-permissions/audiostream" }, + "parameters": [ + { + "in": "query", + "name": "stream_id", + "required": true, + "schema": { "type": "int" } + } + ], + "responses": { + "200": { "$ref": "#/components/responses/200" }, + "400": { "$ref": "#/components/responses/400" } + } + } + }, + "/set_stream_state": { + "description": "Change stream active state", + "get": { + "x-permissions": { + "$ref": "#/components/x-permissions/streamcontrol" + }, + "parameters": [ + { + "in": "query", + "name": "stream_id", + "required": true, + "schema": {"type": "int"} + }, + { + "in": "query", + "name": "state", + "required": true, + "schema": {"type": "int"} + } + ], + "responses": { + "200": { "$ref": "#/components/responses/200" }, + "400": { "$ref": "#/components/responses/400" } + } + } + }, + "/set_stream_mute": { + "description": "Change stream mute state", + "get": { + "x-permissions": { + "$ref": "#/components/x-permissions/streamcontrol" + }, + "parameters": [ + { + "in": "query", + "name": "stream_id", + "required": true, + "schema": {"type": "int"} + }, + { + "in": "query", + "name": "mute", + "required": true, + "schema": {"type": "int"} + } + ], + "responses": { + "200": { "$ref": "#/components/responses/200" }, + "400": { "$ref": "#/components/responses/400" } + } + } + }, + "/get_stream_info": { + "description": "Retrieve stream information", + "get": { + "parameters": [ + { + "in": "query", + "name": "stream_id", + "required": true, + "schema": {"type": "int"} + } + ], + "responses": { + "200": { + "$ref": "#/components/responses/200", + "response": { + "description": "Stream information structure", + "$ref": "#/components/schemas/stream_info" + } + }, + "400": { "$ref": "#/components/responses/400" } + } + } + }, + "/set_volume": { + "description": "Set volume on endpoint", + "get": { + "x-permissions": { "$ref": "#/components/x-permissions/endpointcontrol" }, + "parameters": [ + { + "in": "query", + "name": "endpoint_type", + "required": true, + "schema": { "type": "enum" } + }, + { + "in": "query", + "name": "endpoint_id", + "required": true, + "schema": { "type": "int" } + }, + { + "in": "query", + "name": "volume", + "required": true, + "schema": { "type": "string" } + } + ], + "responses": { + "200": { "$ref": "#/components/responses/200" }, + "400": { "$ref": "#/components/responses/400" } + } + } + }, + "/get_volume": { + "description": "Get endpoint volume", + "get": { + "parameters": [ + { + "in": "query", + "name": "endpoint_type", + "required": true, + "schema": { "type": "enum" } + }, + { + "in": "query", + "name": "endpoint_id", + "required": true, + "schema": { "type": "int" } + } + ], + "responses": { + "200": { + "$ref": "#/components/responses/200", + "response": { + "description": "Endpoint volume value", + "type": "double" + } + }, + "400": { "$ref": "#/components/responses/400" } + } + } + }, + "/get_endpoint_info": { + "description": "Retrieve endpoint information including its properties", + "get": { + "parameters": [ + { + "in": "query", + "name": "endpoint_type", + "required": true, + "schema": { "type": "enum" } + }, + { + "in": "query", + "name": "endpoint_id", + "required": false, + "schema": { "type": "int" } + } + ], + "responses": { + "200": { "$ref": "#/components/responses/200" }, + "400": { "$ref": "#/components/responses/400" } + } + } + }, + "/set_property": { + "description": "Set endpoint property value", + "get": { + "x-permissions": { "$ref": "#/components/x-permissions/endpointcontrol" }, + "parameters": [ + { + "in": "query", + "name": "endpoint_type", + "required": true, + "schema": { "type": "enum" } + }, + { + "in": "query", + "name": "endpoint_id", + "required": false, + "schema": { "type": "int" } + }, + { + "in": "query", + "name": "property_name", + "required": true, + "schema": { "type": "string" } + }, + { + "in": "query", + "name": "value", + "required": true, + "schema": { "type": "string" } + } + ], + "responses": { + "200": { "$ref": "#/components/responses/200" }, + "400": { "$ref": "#/components/responses/400" } + } + } + }, + "/get_property": { + "description": "Get endpoint property value", + "get": { + "parameters": [ + { + "in": "query", + "name": "endpoint_type", + "required": true, + "schema": { "type": "enum" } + }, + { + "in": "query", + "name": "endpoint_id", + "required": false, + "schema": { "type": "int" } + }, + { + "in": "query", + "name": "property_name", + "required": true, + "schema": { "type": "string" } + } + ], + "responses": { + "200": { + "$ref": "#/components/responses/200", + "response": { + "description": "Property value", + "type": "double" + } + }, + "400": { "$ref": "#/components/responses/400" } + } + } + }, + "/get_list_actions": { + "description": "Retrieve a list of supported actions for a particular audio role", + "get": { + "parameters": [ + { + "in": "query", + "name": "audio_role", + "required": true, + "schema": { "type": "string" } + } + ], + "responses": { + "200": { "$ref": "#/components/responses/200" }, + "400": { "$ref": "#/components/responses/400" } + } + } + }, + "/post_action": { + "description": "Post sound or audio device related action event (extendable mechanism)", + "get": { + "x-permissions": { "$ref": "#/components/x-permissions/soundevent" }, + "parameters": [ + { + "in": "query", + "name": "action_name", + "required": true, + "schema": { "type": "string" } + }, + { + "in": "query", + "name": "audio_role", + "required": true, + "schema": { "type": "string" } + }, + { + "in": "query", + "name": "media_name", + "required": false, + "schema": { "type": "string"} + }, + { + "in": "query", + "name": "action_context", + "required": false, + "schema": { "type": "object" } + } + ], + "responses": { + "200": { "$ref": "#/components/responses/200" }, + "400": { "$ref": "#/components/responses/400" } + } + } + }, + "/subscribe": { + "description": "Subscribe to audio high level events", + "get": { + "parameters": [ + { + "in": "query", + "name": "events", + "required": true, + "schema": { "type": "array", + "items": { "type": "string" } + } + } + ], + "responses": { + "200": { "$ref": "#/components/responses/200" }, + "400": { "$ref": "#/components/responses/400" } + } + } + }, + "/unsubscribe": { + "description": "Unubscribe to audio high level events", + "get": { + "parameters": [ + { + "in": "query", + "name": "events", + "required": true, + "schema": { + "type": "array", + "items": { + "type": "string" + } + } + } + ], + "responses": { + "200": { "$ref": "#/components/responses/200" }, + "400": { "$ref": "#/components/responses/400" } + } + } + } + } +} diff --git a/ahl-binding/ahl-binding.c b/ahl-binding/ahl-binding.c new file mode 100644 index 0000000..264930f --- /dev/null +++ b/ahl-binding/ahl-binding.c @@ -0,0 +1,1577 @@ +/* + * Copyright (C) 2017 "Audiokinetic Inc" + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#define _GNU_SOURCE +#include <stdio.h> +#include <string.h> + +#include "ahl-binding.h" +#include "ahl-apidef.h" // Generated from JSON OpenAPI +#include "wrap-json.h" +#include "ahl-policy.h" +#include "ahl-policy-utils.h" + +// Global high-level binding context +AHLCtxT g_AHLCtx; + +// TODO: Helpers that could be common +static EndpointTypeT EndpointTypeToEnum(char * in_pEndpointTypeStr) +{ + if (in_pEndpointTypeStr == NULL) { + return ENDPOINTTYPE_MAXVALUE; + } + else if (strcasecmp(in_pEndpointTypeStr,AHL_ENDPOINTTYPE_SOURCE)==0) { + return ENDPOINTTYPE_SOURCE; + } + else if (strcasecmp(in_pEndpointTypeStr,AHL_ENDPOINTTYPE_SINK)==0) { + return ENDPOINTTYPE_SINK; + } + else + return ENDPOINTTYPE_MAXVALUE; +} + +static StreamStateT StreamStateToEnum(char * in_pStreamStateStr) +{ + if (in_pStreamStateStr == NULL) { + return STREAM_STATE_MAXVALUE; + } + else if (strcasecmp(in_pStreamStateStr,AHL_STREAM_STATE_IDLE)==0) { + return STREAM_STATE_IDLE; + } + else if (strcasecmp(in_pStreamStateStr,AHL_STREAM_STATE_RUNNING)==0) { + return STREAM_STATE_RUNNING; + } + else if (strcasecmp(in_pStreamStateStr,AHL_STREAM_STATE_PAUSED)==0) { + return STREAM_STATE_PAUSED; + } + else + return STREAM_STATE_MAXVALUE; +} + +static StreamMuteT StreamMuteToEnum(char * in_pStreamMuteStr) +{ + if (in_pStreamMuteStr == NULL) { + return STREAM_MUTE_MAXVALUE; + } + else if (strcasecmp(in_pStreamMuteStr,AHL_STREAM_UNMUTED)==0) { + return STREAM_UNMUTED; + } + else if (strcasecmp(in_pStreamMuteStr,AHL_STREAM_MUTED)==0) { + return STREAM_MUTED; + } + else + return STREAM_MUTE_MAXVALUE; +} + +static char * DeviceURITypeEnumToStr(DeviceURITypeT in_eDeviceURIType) { + switch(in_eDeviceURIType) { + case DEVICEURITYPE_ALSA_HW: // Alsa hardware device URI + return AHL_DEVICEURITYPE_ALSA_HW; + case DEVICEURITYPE_ALSA_DMIX: // Alsa Dmix device URI (only for playback devices) + return AHL_DEVICEURITYPE_ALSA_DMIX; + case DEVICEURITYPE_ALSA_DSNOOP: // Alsa DSnoop device URI (only for capture devices) + return AHL_DEVICEURITYPE_ALSA_DSNOOP; + case DEVICEURITYPE_ALSA_SOFTVOL: // Alsa softvol device URI + return AHL_DEVICEURITYPE_ALSA_SOFTVOL; + case DEVICEURITYPE_ALSA_PLUG: // Alsa plug device URI + return AHL_DEVICEURITYPE_ALSA_PLUG; + case DEVICEURITYPE_ALSA_OTHER: // Alsa domain URI device of unspecified type + return AHL_DEVICEURITYPE_ALSA_OTHER; + case DEVICEURITYPE_NOT_ALSA: // Unknown (not ALSA domain) + return AHL_DEVICEURITYPE_NOT_ALSA; + default: + return "Unknown"; + } +} + +static char * StreamStateEnumToStr(StreamStateT in_eStreamState) { + switch(in_eStreamState) { + case STREAM_STATE_IDLE: + return AHL_STREAM_STATE_IDLE; + case STREAM_STATE_RUNNING: + return AHL_STREAM_STATE_RUNNING; + case STREAM_STATE_PAUSED: + return AHL_STREAM_STATE_PAUSED; + default: + return "Unknown"; + } +} + +static char * StreamMuteEnumToStr(StreamMuteT in_eStreamMute) { + switch(in_eStreamMute) { + case STREAM_UNMUTED: + return AHL_STREAM_UNMUTED; + case STREAM_MUTED: + return AHL_STREAM_MUTED; + default: + return "Unknown"; + } +} + +static void AudioFormatStructToJSON(json_object **audioFormatJ, AudioFormatT * pAudioFormat) +{ + wrap_json_pack(audioFormatJ, "{s:i,s:i,s:i}", + "sample_rate", pAudioFormat->sampleRate, + "num_channels", pAudioFormat->numChannels, + "sample_type", pAudioFormat->sampleType); +} + +// Package only information that can useful to application clients when selecting endpoint +static void EndpointInfoStructToJSON(json_object **endpointInfoJ, EndpointInfoT * pEndpointInfo) +{ + json_object *formatInfoJ = NULL; + wrap_json_pack(endpointInfoJ, "{s:i,s:s,s:s,s:s,s:s,s:s,s:s}", + "endpoint_id", pEndpointInfo->endpointID, + "endpoint_type", (pEndpointInfo->type == ENDPOINTTYPE_SOURCE) ? AHL_ENDPOINTTYPE_SOURCE : AHL_ENDPOINTTYPE_SINK, + "device_name", pEndpointInfo->gsDeviceName, + "display_name", pEndpointInfo->gsDisplayName, + "audio_role", pEndpointInfo->pRoleName, + "device_domain",pEndpointInfo->gsDeviceDomain, + "device_uri_type", DeviceURITypeEnumToStr(pEndpointInfo->deviceURIType)); + AudioFormatStructToJSON(&formatInfoJ,&pEndpointInfo->format); + json_object_object_add(*endpointInfoJ,"format",formatInfoJ); + + // Properties + if (pEndpointInfo->pPropTable) { + json_object *pPropTableJ = json_object_new_array(); + + GHashTableIter iter; + gpointer key, value; + g_hash_table_iter_init (&iter, pEndpointInfo->pPropTable); + while (g_hash_table_iter_next (&iter, &key, &value)) + { + if((key!=NULL) && (value!=NULL)) + { + json_object *pPropertyJ = NULL; + json_object_get((json_object*)value); // Don't let the framework free our object when the request is done + wrap_json_pack(&pPropertyJ, "{s:s,s:o}","property_name", (char*)key, "property_value", (json_object*)value); + json_object_array_add(pPropTableJ, pPropertyJ); + } + } + json_object_object_add(*endpointInfoJ,"properties",pPropTableJ); + } +} + +// Package only information that can useful to application clients when opening a stream +static void StreamInfoStructToJSON(json_object **streamInfoJ, StreamInfoT * pStreamInfo) +{ + json_object *endpointInfoJ = NULL; + EndpointInfoStructToJSON(&endpointInfoJ,pStreamInfo->pEndpointInfo); + wrap_json_pack(streamInfoJ, "{s:i,s:s,s:s,s:s}", + "stream_id", pStreamInfo->streamID, + "state", StreamStateEnumToStr(pStreamInfo->streamState), + "mute", StreamMuteEnumToStr(pStreamInfo->streamMute), + "device_uri",pStreamInfo->pEndpointInfo->gsDeviceURI); // Need to open a stream to have access to the device URI + json_object_object_add(*streamInfoJ,"endpoint_info",endpointInfoJ); +} + +static streamID_t CreateNewStreamID() +{ + streamID_t newID = g_AHLCtx.nextStreamID; + g_AHLCtx.nextStreamID++; + return newID; +} + +static EndpointInfoT * GetEndpointInfoWithRole(endpointID_t in_endpointID, EndpointTypeT in_endpointType, RoleInfoT * in_pRole) +{ + EndpointInfoT * pEndpointInfo = NULL; + GPtrArray * pDeviceArray = NULL; + if (in_endpointType == ENDPOINTTYPE_SOURCE){ + pDeviceArray = in_pRole->pSourceEndpoints; + } + else { + pDeviceArray = in_pRole->pSinkEndpoints; + } + g_assert_nonnull(pDeviceArray); + + for (int j = 0; j < pDeviceArray->len; j++) { + EndpointInfoT * pCurEndpointInfo = g_ptr_array_index(pDeviceArray,j); + g_assert_nonnull(pCurEndpointInfo); + if (pCurEndpointInfo->endpointID == in_endpointID) { + pEndpointInfo = pCurEndpointInfo; + break; + } + } + + return pEndpointInfo; +} + +static int ReplaceEndpointInfoWithRole(endpointID_t in_endpointID, EndpointTypeT in_endpointType, RoleInfoT * in_pRole, EndpointInfoT * in_pNewEndpoint) +{ + GPtrArray * pDeviceArray = NULL; + if (in_endpointType == ENDPOINTTYPE_SOURCE){ + pDeviceArray = in_pRole->pSourceEndpoints; + } + else { + pDeviceArray = in_pRole->pSinkEndpoints; + } + g_assert_nonnull(pDeviceArray); + + for (int j = 0; j < pDeviceArray->len; j++) { + EndpointInfoT * pCurEndpointInfo = g_ptr_array_index(pDeviceArray,j); + g_assert_nonnull(pCurEndpointInfo); + if (pCurEndpointInfo->endpointID == in_endpointID) { + g_ptr_array_insert(pDeviceArray,j,in_pNewEndpoint); + g_ptr_array_remove_index(pDeviceArray,j+1); + TermEndpointInfo(pCurEndpointInfo); + // GLib automatically frees item upon array removal + return AHL_SUCCESS; + } + } + + return AHL_FAIL; +} + +static EndpointInfoT * GetEndpointInfo(endpointID_t in_endpointID, EndpointTypeT in_endpointType) +{ + EndpointInfoT * pEndpointInfo = NULL; + + GHashTableIter iter; + gpointer key, value; + g_hash_table_iter_init (&iter, g_AHLCtx.policyCtx.pRoleInfo); + while (pEndpointInfo == NULL && g_hash_table_iter_next (&iter, &key, &value)) + { + RoleInfoT * pRoleInfo = (RoleInfoT*)value; + pEndpointInfo = GetEndpointInfoWithRole(in_endpointID,in_endpointType,pRoleInfo); + } + + return pEndpointInfo; +} + +static StreamInfoT * GetStream(streamID_t in_streamID) +{ + if (g_AHLCtx.policyCtx.pStreams == NULL) + return NULL; + + return g_hash_table_lookup(g_AHLCtx.policyCtx.pStreams,GINT_TO_POINTER(&in_streamID)); +} + +static RoleInfoT * GetRole(char * in_pAudioRoleName) +{ + if (g_AHLCtx.policyCtx.pRoleInfo == NULL) + return NULL; + + return g_hash_table_lookup(g_AHLCtx.policyCtx.pRoleInfo,in_pAudioRoleName); +} + +static AHLClientCtxT * AllocateClientContext() +{ + AHLClientCtxT * pClientCtx = malloc(sizeof(AHLClientCtxT)); + pClientCtx->pStreamAccessList = g_array_new(FALSE, TRUE, sizeof(streamID_t)); + return pClientCtx; +} + +static void TerminateClientContext(void * ptr) +{ + AHLClientCtxT * pClientCtx = (AHLClientCtxT *) ptr; + if (pClientCtx != NULL) { + + // Avoid having policy in bad state if client loses WS connection (e.g. app termination without close stream) + // Force close streams in those cases. + + if (pClientCtx->pStreamAccessList != NULL) { +#ifndef AHL_DISCONNECT_POLICY + for (int i = 0; i < pClientCtx->pStreamAccessList->len; i++) + { + streamID_t streamID = g_array_index(pClientCtx->pStreamAccessList,streamID_t,i); + // Call policy to verify whether creating a new audio stream is allowed in current context and possibly take other actions + StreamInfoT * pStreamInfo = GetStream(streamID); + if (pStreamInfo == NULL) { + AFB_ERROR("Specified stream not currently active stream_id -> %d",streamID); + return; + } + + json_object *pPolicyStreamJ = NULL; + int err = PolicyStreamStructToJSON(pStreamInfo, &pPolicyStreamJ); + if (err == AHL_POLICY_UTIL_FAIL) + { + AFB_ERROR("Audio policy violation, Unable to get JSON object for Policy_CloseStream"); + return; + } + Policy_CloseStream(pPolicyStreamJ); + } +#endif + g_array_free( pClientCtx->pStreamAccessList, TRUE); + pClientCtx->pStreamAccessList = NULL; + } + free(pClientCtx); + } +} + +static int CheckStreamAccessControl(AHLClientCtxT * pClientCtx, streamID_t streamID) +{ + int iAccessControl = AHL_ACCESS_CONTROL_DENIED; + if (pClientCtx && pClientCtx->pStreamAccessList) { + for (int i = 0; i < pClientCtx->pStreamAccessList->len ; i++) { + streamID_t iID = g_array_index(pClientCtx->pStreamAccessList,streamID_t,i); + if (iID == streamID) { + iAccessControl = AHL_ACCESS_CONTROL_GRANTED; + } + } + } + return iAccessControl; +} + +static int CreateEvents() +{ + int err = 0; + + g_AHLCtx.policyCtx.propertyEvent = afb_daemon_make_event(AHL_ENDPOINT_PROPERTY_EVENT); + err = !afb_event_is_valid(g_AHLCtx.policyCtx.propertyEvent); + if (err) { + AFB_ERROR("Could not create endpoint property change event"); + return err; + } + + g_AHLCtx.policyCtx.volumeEvent = afb_daemon_make_event(AHL_ENDPOINT_VOLUME_EVENT); + err = !afb_event_is_valid(g_AHLCtx.policyCtx.volumeEvent); + if (err) { + AFB_ERROR("Could not create endpoint volume change event"); + return err; + } + + g_AHLCtx.policyCtx.postActionEvent = afb_daemon_make_event(AHL_POST_ACTION_EVENT); + err = !afb_event_is_valid(g_AHLCtx.policyCtx.postActionEvent); + if (err) { + AFB_ERROR("Could not create post action event call event"); + return err; + } + + return err; +} + +static void AhlBindingTerm() +{ +#ifndef AHL_DISCONNECT_POLICY + // Policy termination + Policy_Term(); +#endif + + // Roles + if (g_AHLCtx.policyCtx.pRoleInfo != NULL) { + GHashTableIter iter; + gpointer key, value; + g_hash_table_iter_init(&iter, g_AHLCtx.policyCtx.pRoleInfo); + while (g_hash_table_iter_next (&iter, &key, &value)) + { + RoleInfoT * pRole = (RoleInfoT *)value; + if (pRole) + { + if(pRole->pRoleName) { + g_free(pRole->pRoleName); + pRole->pRoleName = NULL; + } + // Actions + if (pRole->pActionList) { + for (int i = 0; i < pRole->pActionList->len; i++) + { + g_ptr_array_remove_index( pRole->pActionList, i ); // Free char * is called by GLib + } + } + // Source endpoints + if (pRole->pSourceEndpoints) { + for (int i = 0; i < pRole->pSourceEndpoints->len; i++) + { + EndpointInfoT * pEndpoint = g_ptr_array_remove_index( pRole->pSourceEndpoints, i ); // Free endpoint * is called by GLib + if (pEndpoint) { + TermEndpointInfo(pEndpoint); + } + } + } + // Sink endpoints + if (pRole->pSinkEndpoints) { + for (int i = 0; i < pRole->pSinkEndpoints->len; i++) + { + EndpointInfoT * pEndpoint = g_ptr_array_remove_index( pRole->pSinkEndpoints, i ); // Free endpoint * is called by GLib + if (pEndpoint) { + TermEndpointInfo(pEndpoint); + } + } + } + free(pRole); + } + } + g_hash_table_remove_all(g_AHLCtx.policyCtx.pRoleInfo); + g_hash_table_destroy(g_AHLCtx.policyCtx.pRoleInfo); + g_AHLCtx.policyCtx.pRoleInfo = NULL; + } + + if (g_AHLCtx.policyCtx.pStreams) { + GHashTableIter iter; + gpointer key, value; + g_hash_table_iter_init (&iter, g_AHLCtx.policyCtx.pStreams); + while (g_hash_table_iter_next (&iter, &key, &value)) + { + if (value) + free(value); + } + g_hash_table_remove_all(g_AHLCtx.policyCtx.pStreams); + g_hash_table_destroy(g_AHLCtx.policyCtx.pStreams); + g_AHLCtx.policyCtx.pStreams = NULL; + } + + if (g_AHLCtx.policyCtx.pHALList) { + g_ptr_array_free(g_AHLCtx.policyCtx.pHALList,TRUE); + g_AHLCtx.policyCtx.pHALList = NULL; + } + + AFB_INFO("Audio high-level binding termination success"); +} + +// Binding initialization +PUBLIC int AhlBindingInit() +{ + int err = 0; + + memset(&g_AHLCtx,0,sizeof(g_AHLCtx)); + + // Register exit function + atexit(AhlBindingTerm); + + //Create AGL Events + err=CreateEvents(); + if(err) + { + //Error messages already reported inside CreateEvents + return err; + } + + // Parse high-level binding JSON configuration file (will build device lists) + err = ParseHLBConfig(); + if(err) + { + //Error messages already reported inside ParseHLBConfig + return err; + } + +#ifndef AHL_DISCONNECT_POLICY + // Policy initialization + err = Policy_Init(); + if(err == AHL_POLICY_REJECT) + { + //Error messages already reported inside PolicyInit + return err; + } + + // for all audio Roles + GHashTableIter iter; + gpointer key, value; + + g_hash_table_iter_init (&iter, g_AHLCtx.policyCtx.pRoleInfo); + while (g_hash_table_iter_next (&iter, &key, &value)) + { + RoleInfoT * pRoleInfo = (RoleInfoT*)value; + if (pRoleInfo->pSourceEndpoints){ + // for all source endpoints + for (int j = 0; j < pRoleInfo->pSourceEndpoints->len; j++) { + EndpointInfoT * pCurEndpointInfo = g_ptr_array_index(pRoleInfo->pSourceEndpoints,j); + g_assert_nonnull(pCurEndpointInfo); + json_object *pPolicyEndpointJ = NULL; + err = PolicyEndpointStructToJSON(pCurEndpointInfo, &pPolicyEndpointJ); + if (err == AHL_POLICY_UTIL_FAIL) + { + AFB_ERROR("Unable to Create Endpoint Json object error:%s ",wrap_json_get_error_string(err)); + return err; + } + else + { + err = Policy_Endpoint_Init(pPolicyEndpointJ); + if (err == AHL_POLICY_REJECT) { + AFB_ERROR("Policy endpoint properties initalization failed for endpoint_id :%d type:%d",pCurEndpointInfo->endpointID, pCurEndpointInfo->type); + } + //free pPolicyEndpointJ + json_object_put(pPolicyEndpointJ); + } + } + } + if (pRoleInfo->pSinkEndpoints){ + // for all sink endpoints + for (int j = 0; j < pRoleInfo->pSinkEndpoints->len; j++) { + EndpointInfoT * pCurEndpointInfo = g_ptr_array_index(pRoleInfo->pSinkEndpoints,j); + g_assert_nonnull(pCurEndpointInfo); + json_object *pPolicyEndpointJ = NULL; + err = PolicyEndpointStructToJSON(pCurEndpointInfo, &pPolicyEndpointJ); + if (err == AHL_POLICY_UTIL_FAIL) + { + AFB_ERROR("Unable to Create Endpoint Json object error:%s ",wrap_json_get_error_string(err)); + return err; + } + else + { + err = Policy_Endpoint_Init(pPolicyEndpointJ); + if (err== AHL_POLICY_REJECT) { + AFB_ERROR("Policy endpoint properties initalization failed for endpoint_id :%d type:%d",pCurEndpointInfo->endpointID, pCurEndpointInfo->type); + } + //free pPolicyEndpointJ + json_object_put(pPolicyEndpointJ); + } + + } + } + } +#endif + + // Initialize list of active streams + g_AHLCtx.policyCtx.pStreams = g_hash_table_new(g_int_hash, g_int_equal); + if(g_AHLCtx.policyCtx.pStreams == NULL) + { + AFB_ERROR("Unable to create Active Stream List"); + return err; + } + + // TODO: Use AGL persistence framework to retrieve and set initial volumes/properties + + AFB_DEBUG("Audio high-level Binding success"); + return err; +} + +PUBLIC void AhlOnEvent(const char *evtname, json_object *eventJ) +{ + AFB_DEBUG("AHL received event %s", evtname); + + // TODO: Handle event from the policy to update internal information (currently not possible since within the same binding) + +#ifndef AHL_DISCONNECT_POLICY + // Temp: currently forward to policy to handle events (they will be received directly when disconnected into separate binding) + Policy_OnEvent(evtname, eventJ); +#endif +} + +PUBLIC void audiohlapi_get_sources(struct afb_req req) +{ + json_object *devicesJ = NULL; + json_object *deviceJ = NULL; + json_object *queryJ = NULL; + char * audioRole = NULL; + + queryJ = afb_req_json(req); + int err = wrap_json_unpack(queryJ, "{s:s}", "audio_role", &audioRole); + if (err) { + afb_req_fail_f(req, "Invalid arguments", "Args not a valid json object query=%s", json_object_get_string(queryJ)); + return; + } + + AFB_DEBUG("Filtering devices according to specified audio role=%s", audioRole); + + RoleInfoT * pRole = GetRole(audioRole); + if ( pRole == NULL ) + { + afb_req_fail_f(req, "Invalid arguments", "Requested audio role does not exist in current configuration -> %s", json_object_get_string(queryJ)); + return; + } + else + { + devicesJ = json_object_new_array(); + GPtrArray * pDeviceArray = pRole->pSourceEndpoints; + if (pDeviceArray) { + int iNumberDevices = pDeviceArray->len; + for ( int j = 0 ; j < iNumberDevices; j++) + { + EndpointInfoT * pEndpointInfo = g_ptr_array_index(pDeviceArray,j); + if (pEndpointInfo) { + EndpointInfoStructToJSON(&deviceJ, pEndpointInfo); + json_object_array_add(devicesJ, deviceJ); + } + } + } + } + + afb_req_success(req, devicesJ, "List of sources"); +} + +PUBLIC void audiohlapi_get_sinks(struct afb_req req) +{ + json_object *devicesJ = NULL; + json_object *deviceJ = NULL; + json_object *queryJ = NULL; + char * audioRole = NULL; + + queryJ = afb_req_json(req); + int err = wrap_json_unpack(queryJ, "{s:s}", "audio_role", &audioRole); + if (err) { + afb_req_fail_f(req, "Invalid arguments", "Args not a valid json object query=%s", json_object_get_string(queryJ)); + return; + } + + AFB_DEBUG("Filtering devices according to specified audio role=%s", audioRole); + + RoleInfoT * pRole = GetRole(audioRole); + if ( pRole == NULL ) + { + afb_req_fail_f(req, "Invalid arguments", "Requested audio role does not exist in current configuration -> %s", json_object_get_string(queryJ)); + return; + } + else + { + devicesJ = json_object_new_array(); + GPtrArray * pDeviceArray = pRole->pSinkEndpoints; + if (pDeviceArray) { + int iNumberDevices = pDeviceArray->len; + for ( int j = 0 ; j < iNumberDevices; j++) + { + EndpointInfoT * pEndpointInfo = g_ptr_array_index(pDeviceArray,j); + EndpointInfoStructToJSON(&deviceJ, pEndpointInfo); + json_object_array_add(devicesJ, deviceJ); + } + } + } + + afb_req_success(req, devicesJ, "List of sinks"); +} + +PUBLIC void audiohlapi_stream_open(struct afb_req req) +{ + json_object *streamInfoJ = NULL; + StreamInfoT * pStreamInfo = NULL; + json_object *queryJ = NULL; + char * audioRole = NULL; + char * endpointTypeStr = NULL; + EndpointTypeT endpointType = ENDPOINTTYPE_MAXVALUE; + endpointID_t endpointID = AHL_UNDEFINED; + EndpointInfoT * pEndpointInfo = NULL; + EndpointSelectionModeT endpointSelMode = ENDPOINTSELMODEMAXVALUE; + + queryJ = afb_req_json(req); + int err = wrap_json_unpack(queryJ, "{s:s,s:s,s?i}", "audio_role", &audioRole, "endpoint_type", &endpointTypeStr, "endpoint_id", &endpointID); + if (err) { + afb_req_fail_f(req, "Invalid arguments", "Args not a valid json object query=%s", json_object_get_string(queryJ)); + return; + } + AFB_DEBUG("Parsed input arguments = audio_role:%s endpoint_type:%s endpoint_id:%d", audioRole,endpointTypeStr,endpointID); + endpointType = EndpointTypeToEnum(endpointTypeStr); + + // Check if there is already an existing context for this client + AHLClientCtxT * pClientCtx = afb_req_context_get(req); // Retrieve client-specific data structure + if (pClientCtx == NULL) + { + pClientCtx = AllocateClientContext(); + afb_req_context_set(req, pClientCtx, TerminateClientContext); + } + + RoleInfoT * pRole = GetRole(audioRole); + if ( pRole == NULL ) + { + afb_req_fail_f(req, "Invalid audio role", "Audio role was not found in configuration -> %s",audioRole); + return; + } + + GPtrArray * pDeviceArray = NULL; + if (endpointType == ENDPOINTTYPE_SOURCE){ + pDeviceArray = pRole->pSourceEndpoints; + } + else{ + pDeviceArray = pRole->pSinkEndpoints; + } + if (pDeviceArray == NULL || pDeviceArray->len == 0) { + afb_req_fail_f(req, "No available devices", "No available devices for role:%s and device type:%s",audioRole,endpointTypeStr); + return; + } + + if (endpointID == AHL_UNDEFINED) + { + // Assign a device based on configuration priority (first in the list for requested role and endpoint type) + pEndpointInfo = g_ptr_array_index(pDeviceArray,0); + endpointSelMode = ENDPOINTSELMODE_AUTO; + + } + else{ + endpointSelMode = ENDPOINTSELMODE_MANUAL; + // Find specified endpoint ID in list of devices + int iNumberDevices = pDeviceArray->len; + for ( int j = 0 ; j < iNumberDevices; j++) + { + pEndpointInfo = g_ptr_array_index(pDeviceArray,j); + if (pEndpointInfo && pEndpointInfo->endpointID == endpointID) { + break; + } + pEndpointInfo = NULL; + } + + } + + if (pEndpointInfo == NULL) { + afb_req_fail_f(req, "Endpoint not available", "Specified endpoint not available for role:%s and device type:%d endpoint id %d",audioRole,endpointType,endpointID); + return; + } + + pStreamInfo = (StreamInfoT*) malloc(sizeof(StreamInfoT)); + memset(pStreamInfo,0,sizeof(StreamInfoT)); + + // Create stream + pStreamInfo->streamID = CreateNewStreamID(); // create new ID + pStreamInfo->streamState = STREAM_STATE_IDLE; + pStreamInfo->streamMute = STREAM_UNMUTED; + pStreamInfo->pEndpointInfo = pEndpointInfo; + pStreamInfo->endpointSelMode = endpointSelMode; + // Directly from role config for now, but could be programmatically overriden in the future + pStreamInfo->pRoleName = pRole->pRoleName; + pStreamInfo->iPriority = pRole->iPriority; + pStreamInfo->eInterruptBehavior = pRole->eInterruptBehavior; + +#ifndef AHL_DISCONNECT_POLICY + // Call policy to verify whether creating a new audio stream is allowed in current context and possibly take other actions + json_object *pPolicyStreamJ = NULL; + err = PolicyStreamStructToJSON(pStreamInfo, &pPolicyStreamJ); + if (err == AHL_POLICY_UTIL_FAIL) + { + afb_req_fail(req, "Audio policy violation", "Unable to get JSON object for Policy_OpenStream"); + return; + } + + int policyAllowed = Policy_OpenStream(pPolicyStreamJ); + if (policyAllowed == AHL_POLICY_REJECT) + { + afb_req_fail(req, "Audio policy violation", "Open stream not allowed in current context"); + return; + } +#endif + + char streamEventName[128]; + snprintf(streamEventName,128,"ahl_streamstate_%d",pStreamInfo->streamID); + + pStreamInfo->streamStateEvent = afb_daemon_make_event(streamEventName); + err = !afb_event_is_valid(pStreamInfo->streamStateEvent); + if (err) { + afb_req_fail(req, "Stream event creation failure", "Could not create stream specific state change event"); + return; + } + + err = afb_req_subscribe(req,pStreamInfo->streamStateEvent); + if (err) { + afb_req_fail(req, "Stream event subscription failure", "Could not subscribe to stream specific state change event"); + return; + } + + // Add to client context stream ID access rights + g_array_append_val(pClientCtx->pStreamAccessList, pStreamInfo->streamID); + + // Push stream on active stream list + if (g_AHLCtx.policyCtx.pStreams) + g_hash_table_insert( g_AHLCtx.policyCtx.pStreams, GINT_TO_POINTER(&pStreamInfo->streamID), pStreamInfo ); + + StreamInfoStructToJSON(&streamInfoJ,pStreamInfo); + + afb_req_success(req, streamInfoJ, "Stream info structure"); +} + +PUBLIC void audiohlapi_stream_close(struct afb_req req) +{ + json_object *queryJ = NULL; + streamID_t streamID = AHL_UNDEFINED; + + queryJ = afb_req_json(req); + int err = wrap_json_unpack(queryJ, "{s:i}", "stream_id", &streamID); + if (err) { + afb_req_fail_f(req, "Invalid arguments", "Args not a valid json object query=%s", json_object_get_string(queryJ)); + return; + } + AFB_DEBUG("Parsed input arguments = stream_id:%d", streamID); + + + StreamInfoT * pStreamInfo = GetStream(streamID); + if (pStreamInfo == NULL) { + afb_req_fail_f(req, "Stream not found", "Specified stream not currently active stream_id -> %d",streamID); + return; + } + + // Check if there is already an existing context for this client + AHLClientCtxT * pClientCtx = afb_req_context_get(req); // Retrieve client-specific data structure + if (pClientCtx == NULL) + { + afb_req_fail(req, "Bad state", "No client context associated with the request (is there an opened stream by this client?)"); + return; + } + + // Verify that this client can control the stream + int iStreamAccessControl = CheckStreamAccessControl( pClientCtx, streamID ); + if (iStreamAccessControl == AHL_ACCESS_CONTROL_DENIED) + { + afb_req_fail(req, "Access control denied", "Close stream not allowed in current client context"); + return; + } + +#ifndef AHL_DISCONNECT_POLICY + json_object *pPolicyStreamJ = NULL; + err = PolicyStreamStructToJSON(pStreamInfo, &pPolicyStreamJ); + if (err == AHL_POLICY_UTIL_FAIL) + { + afb_req_fail(req, "Audio policy violation", "Unable to get JSON object for Policy_CloseStream"); + return; + } + // Call policy to verify whether creating a new audio stream is allowed in current context and possibly take other actions + int policyAllowed = Policy_CloseStream(pPolicyStreamJ); + if (policyAllowed == AHL_POLICY_REJECT) + { + afb_req_fail(req, "Audio policy violation", "Close stream not allowed in current context"); + return; + } +#endif + + // Unsubscribe client from stream events + char streamEventName[128]; + snprintf(streamEventName,128,"ahl_streamstate_%d",streamID); + int iValid = afb_event_is_valid(pStreamInfo->streamStateEvent); + if (iValid) { + err = afb_req_unsubscribe(req,pStreamInfo->streamStateEvent); + if (err) { + afb_req_fail(req, "Stream event subscription failure", "Could not unsubscribe to stream specific state change event"); + return; + } + } + + // Remove from stream list (if present) + if (g_AHLCtx.policyCtx.pStreams) + g_hash_table_remove(g_AHLCtx.policyCtx.pStreams,GINT_TO_POINTER(&pStreamInfo->streamID)); + free(pStreamInfo); + pStreamInfo = NULL; + + // Find index for cases where there are multiple streams per client + // Remove from client context stream ID and endpoint ID access rights + if (pClientCtx->pStreamAccessList) { + for (int i = 0; i < pClientCtx->pStreamAccessList->len ; i++) { + streamID_t iID = g_array_index(pClientCtx->pStreamAccessList,streamID_t,i); + if (iID == streamID) { + g_array_remove_index(pClientCtx->pStreamAccessList, i); + } + } + + if (pClientCtx->pStreamAccessList->len == 0) { + // If no more streams/endpoints owner, clear session + afb_req_context_clear(req); + } + } + + afb_req_success(req, NULL, "Stream close completed"); +} + + PUBLIC void audiohlapi_set_stream_state(struct afb_req req) + { + json_object *queryJ = NULL; + streamID_t streamID = AHL_UNDEFINED; + char * streamStateStr = NULL; + + queryJ = afb_req_json(req); + int err = wrap_json_unpack(queryJ, "{s:i,s:s}", "stream_id", &streamID,"state",&streamStateStr); + if (err) { + afb_req_fail_f(req, "Invalid arguments", "Args not a valid json object query=%s", json_object_get_string(queryJ)); + return; + } + AFB_DEBUG("Parsed input arguments = stream_id:%d, state:%s", streamID,streamStateStr); + + StreamInfoT * pStreamInfo = GetStream(streamID); + if (pStreamInfo == NULL) { + afb_req_fail_f(req, "Stream not found", "Specified stream not found stream_id -> %d",streamID); + return; + } + + // Check if there is already an existing context for this client + AHLClientCtxT * pClientCtx = afb_req_context_get(req); // Retrieve client-specific data structure + if (pClientCtx == NULL) + { + afb_req_fail(req, "Bad state", "No client context associated with the request (is there an opened stream by this client?)"); + return; + } + + // Verify that this client can control the stream + int iStreamAccessControl = CheckStreamAccessControl( pClientCtx, streamID ); + if (iStreamAccessControl == AHL_ACCESS_CONTROL_DENIED) + { + afb_req_fail(req, "Access control denied", "Set stream state not allowed in current client context"); + return; + } + + StreamStateT streamState = StreamStateToEnum(streamStateStr); +#ifndef AHL_DISCONNECT_POLICY + json_object *pPolicyStreamJ = NULL; + err = PolicyStreamStructToJSON(pStreamInfo, &pPolicyStreamJ); + if (err == AHL_POLICY_UTIL_FAIL) + { + afb_req_fail(req, "Audio policy violation", "Unable to get JSON object for Policy_SetStreamState"); + return; + } + + json_object *paramJ= json_object_new_int(streamState); + json_object_object_add(pPolicyStreamJ, "arg_stream_state", paramJ); + + int policyAllowed = Policy_SetStreamState(pPolicyStreamJ); + if (policyAllowed == AHL_POLICY_REJECT) + { + afb_req_fail(req, "Audio policy violation", "Change stream state not allowed in current context"); + return; + } +#else + // Simulate that policy returns target state (accepted) + pStreamInfo->streamState = streamState; +#endif + + afb_req_success(req, NULL, "Set stream state"); + } + + PUBLIC void audiohlapi_set_stream_mute(struct afb_req req) + { + json_object *queryJ = NULL; + streamID_t streamID = AHL_UNDEFINED; + char * pMuteStr = NULL; + + queryJ = afb_req_json(req); + int err = wrap_json_unpack(queryJ, "{s:i,s:s}", "stream_id", &streamID,"mute",&pMuteStr); + if (err) { + afb_req_fail_f(req, "Invalid arguments", "Args not a valid json object query=%s", json_object_get_string(queryJ)); + return; + } + AFB_DEBUG("Parsed input arguments = stream_id:%d, mute:%s", streamID,pMuteStr); + + StreamInfoT * pStreamInfo = GetStream(streamID); + if (pStreamInfo == NULL) { + afb_req_fail_f(req, "Stream not found", "Specified stream not currently active stream_id -> %d",streamID); + return; + } + + // Check if there is already an existing context for this client + AHLClientCtxT * pClientCtx = afb_req_context_get(req); // Retrieve client-specific data structure + if (pClientCtx == NULL) + { + afb_req_fail(req, "Bad state", "No client context associated with the request (is there an opened stream by this client?)"); + return; + } + + // Verify that this client can control the stream + int iStreamAccessControl = CheckStreamAccessControl( pClientCtx, streamID ); + if (iStreamAccessControl == AHL_ACCESS_CONTROL_DENIED) + { + afb_req_fail(req, "Access control denied", "Set stream mute state not allowed in current client context"); + return; + } + + + StreamMuteT muteState = StreamMuteToEnum(pMuteStr); +#ifndef AHL_DISCONNECT_POLICY + json_object *pPolicyStreamJ = NULL; + err = PolicyStreamStructToJSON(pStreamInfo, &pPolicyStreamJ); + if (err == AHL_POLICY_UTIL_FAIL) + { + afb_req_fail(req, "Audio policy violation", "Unable to get JSON object for Policy_SetStreamMute"); + return; + } + + json_object *paramJ= json_object_new_int(muteState); + json_object_object_add(pPolicyStreamJ, "mute_state", paramJ); + + int policyAllowed = Policy_SetStreamMute(pPolicyStreamJ); + if (policyAllowed == AHL_POLICY_REJECT) + { + afb_req_fail(req, "Audio policy violation", "Mute stream not allowed in current context"); + return; + } +#else + // Simulate that policy returns target state (accepted) + pStreamInfo->streamMute = muteState; +#endif + + afb_req_success(req, NULL, "Set stream mute completed"); + } + + PUBLIC void audiohlapi_get_stream_info(struct afb_req req) + { + json_object *queryJ = NULL; + streamID_t streamID = AHL_UNDEFINED; + json_object * streamInfoJ = NULL; + + queryJ = afb_req_json(req); + int err = wrap_json_unpack(queryJ, "{s:i}", "stream_id", &streamID); + if (err) { + afb_req_fail_f(req, "Invalid arguments", "Args not a valid json object query=%s", json_object_get_string(queryJ)); + return; + } + AFB_DEBUG("Parsed input arguments = stream_id:%d", streamID); + + StreamInfoT * pStreamInfo = GetStream(streamID); + if (pStreamInfo == NULL) { + afb_req_fail_f(req, "Stream not found", "Specified stream not currently active stream_id -> %d",streamID); + return; + } + + StreamInfoStructToJSON(&streamInfoJ,pStreamInfo); + + afb_req_success(req, streamInfoJ, "Get stream info completed"); + } + +PUBLIC void audiohlapi_set_volume(struct afb_req req) +{ + json_object *queryJ = NULL; + endpointID_t endpointID = AHL_UNDEFINED; + char * pEndpointTypeStr = NULL; + EndpointTypeT endpointType = ENDPOINTTYPE_MAXVALUE; + char * volumeStr = NULL; + + queryJ = afb_req_json(req); + int err = wrap_json_unpack(queryJ, "{s:s,s:i,s:s}", "endpoint_type", &pEndpointTypeStr,"endpoint_id",&endpointID,"volume",&volumeStr); + if (err) { + afb_req_fail_f(req, "Invalid arguments", "Args not a valid json object query=%s", json_object_get_string(queryJ)); + return; + } + AFB_DEBUG("Parsed input arguments = endpoint_type:%s endpoint_id:%d volume:%s", pEndpointTypeStr,endpointID,volumeStr); + endpointType = EndpointTypeToEnum(pEndpointTypeStr); + + EndpointInfoT * pEndpointInfo = GetEndpointInfo(endpointID,endpointType); + if (pEndpointInfo == NULL) + { + afb_req_fail_f(req, "Endpoint not found", "Endpoint information not found for id:%d type%d",endpointID,endpointType); + return; + } + +#ifndef AHL_DISCONNECT_POLICY + json_object *pPolicyEndpointJ = NULL; + err = PolicyEndpointStructToJSON(pEndpointInfo, &pPolicyEndpointJ); + if (err == AHL_POLICY_UTIL_FAIL) + { + afb_req_fail(req, "Audio policy violation", "Unable to get JSON object for Policy_SetVolume"); + return; + } + + json_object *paramJ= json_object_new_string(volumeStr); + json_object_object_add(pPolicyEndpointJ, "arg_volume", paramJ); + + int policyAllowed = Policy_SetVolume(pPolicyEndpointJ); + if (!policyAllowed) + { + afb_req_fail(req, "Audio policy violation", "Set volume not allowed in current context"); + return; + } +#else + // Simulate that policy returns target state (accepted) + pEndpointInfo->iVolume = atoi(volumeStr); +#endif + + afb_req_success(req, NULL, "Set volume completed"); +} + +PUBLIC void audiohlapi_get_volume(struct afb_req req) +{ + json_object *queryJ = NULL; + endpointID_t endpointID = AHL_UNDEFINED; + char * pEndpointTypeStr = NULL; + EndpointTypeT endpointType = ENDPOINTTYPE_MAXVALUE; + json_object * volumeJ = NULL; + + queryJ = afb_req_json(req); + int err = wrap_json_unpack(queryJ, "{s:s,s:i}", "endpoint_type", &pEndpointTypeStr,"endpoint_id",&endpointID); + if (err) { + afb_req_fail_f(req, "Invalid arguments", "Args not a valid json object query=%s", json_object_get_string(queryJ)); + return; + } + AFB_DEBUG("Parsed input arguments = endpoint_type:%s endpoint_id:%d", pEndpointTypeStr,endpointID); + endpointType = EndpointTypeToEnum(pEndpointTypeStr); + + EndpointInfoT * pEndpointInfo = GetEndpointInfo(endpointID,endpointType); + if (pEndpointInfo == NULL) + { + afb_req_fail_f(req, "Endpoint not found", "Endpoint information not found for id:%d type%d",endpointID,endpointType); + return; + } + + volumeJ = json_object_new_int(pEndpointInfo->iVolume); + + afb_req_success(req, volumeJ, "Retrieved volume value"); +} + +// Properties +PUBLIC void audiohlapi_get_endpoint_info(struct afb_req req) +{ + json_object *queryJ = NULL; + endpointID_t endpointID = AHL_UNDEFINED; + char * pEndpointTypeStr = NULL; + EndpointTypeT endpointType = ENDPOINTTYPE_MAXVALUE; + + queryJ = afb_req_json(req); + int err = wrap_json_unpack(queryJ, "{s:s,s:i}", "endpoint_type", &pEndpointTypeStr,"endpoint_id",&endpointID); + if (err) { + afb_req_fail_f(req, "Invalid arguments", "Args not a valid json object query=%s", json_object_get_string(queryJ)); + return; + } + AFB_DEBUG("Parsed input arguments = endpoint_type:%s endpoint_id:%d", pEndpointTypeStr,endpointID); + endpointType = EndpointTypeToEnum(pEndpointTypeStr); + + EndpointInfoT * pEndpointInfo = GetEndpointInfo(endpointID,endpointType); + if (pEndpointInfo == NULL) + { + afb_req_fail_f(req, "Endpoint not found", "Endpoint information not found for id:%d type%d",endpointID,endpointType); + return; + } + + json_object *endpointInfoJ = NULL; + EndpointInfoStructToJSON(&endpointInfoJ,pEndpointInfo); + + afb_req_success(req, endpointInfoJ, "Retrieved endpoint information and properties"); +} + +PUBLIC void audiohlapi_set_property(struct afb_req req) +{ + json_object *queryJ = NULL; + endpointID_t endpointID = AHL_UNDEFINED; + char * pEndpointTypeStr = NULL; + EndpointTypeT endpointType = ENDPOINTTYPE_MAXVALUE; + char * propertyName = NULL; + json_object * propValueJ = NULL; + + queryJ = afb_req_json(req); + int err = wrap_json_unpack(queryJ, "{s:s,s:i,s:s,s:o}", "endpoint_type", &pEndpointTypeStr,"endpoint_id",&endpointID,"property_name",&propertyName,"value",&propValueJ); + if (err) { + afb_req_fail_f(req, "Invalid arguments", "Args not a valid json object query=%s", json_object_get_string(queryJ)); + return; + } + AFB_DEBUG("Parsed input arguments = endpoint_type:%s endpoint_id:%d property_name:%s", pEndpointTypeStr,endpointID,propertyName); + endpointType = EndpointTypeToEnum(pEndpointTypeStr); + + EndpointInfoT * pEndpointInfo = GetEndpointInfo(endpointID,endpointType); + if (pEndpointInfo == NULL) + { + afb_req_fail_f(req, "Endpoint not found", "Endpoint information not found for id:%d type%d",endpointID,endpointType); + return; + } + +#ifndef AHL_DISCONNECT_POLICY + json_object *pPolicyEndpointJ = NULL; + err = PolicyEndpointStructToJSON(pEndpointInfo, &pPolicyEndpointJ); + if (err == AHL_POLICY_UTIL_FAIL) + { + afb_req_fail(req, "Audio policy violation", "Unable to get JSON object for Policy_SetVolume"); + return; + } + + json_object *paramJ= json_object_new_string(propertyName); + json_object_object_add(pPolicyEndpointJ, "arg_property_name", paramJ); + json_object_object_add(pPolicyEndpointJ, "arg_property_value", propValueJ); + + // Call policy to allow custom policy actions in current context + int policyAllowed = Policy_SetProperty(pPolicyEndpointJ); + if (!policyAllowed) + { + afb_req_fail(req, "Audio policy violation", "Set endpoint property not allowed in current context"); + return; + } +#else + // Simulate that policy returns target state (accepted) + if (pEndpointInfo->pPropTable) + g_hash_table_insert(pEndpointInfo->pPropTable, propertyName, propValueJ); +#endif + + //afb_event_push(g_AHLCtx.policyCtx.propertyEvent,queryJ); + + afb_req_success(req, NULL, "Set property completed"); +} + +PUBLIC void audiohlapi_get_property(struct afb_req req) +{ + json_object *queryJ = NULL; + endpointID_t endpointID = AHL_UNDEFINED; + char * pEndpointTypeStr = NULL; + EndpointTypeT endpointType = ENDPOINTTYPE_MAXVALUE; + char * propertyName = NULL; + + queryJ = afb_req_json(req); + int err = wrap_json_unpack(queryJ, "{s:s,s:i,s:s}", "endpoint_type", &pEndpointTypeStr,"endpoint_id",&endpointID,"property_name",&propertyName); + if (err) { + afb_req_fail_f(req, "Invalid arguments", "Args not a valid json object query=%s", json_object_get_string(queryJ)); + return; + } + AFB_DEBUG("Parsed input arguments = endpoint_type:%s endpoint_id:%d property_name:%s", pEndpointTypeStr,endpointID,propertyName); + endpointType = EndpointTypeToEnum(pEndpointTypeStr); + + EndpointInfoT * pEndpointInfo = GetEndpointInfo(endpointID,endpointType); + if (pEndpointInfo == NULL) + { + afb_req_fail_f(req, "Endpoint not found", "Endpoint information not found for id:%d type%d",endpointID,endpointType); + return; + } + + // Retrieve cached property value + json_object * propertyValJ = (json_object *)g_hash_table_lookup(pEndpointInfo->pPropTable,propertyName); + if (propertyValJ == NULL) { + afb_req_fail_f(req, "Property not found", "Property information not found: %s",propertyName); + return; + } + + json_object_get(propertyValJ); // Increase ref count so that framework does not free our JSON object + //AFB_WARNING("properties %s", json_object_get_string(propertyValJ)); + //json_object_get(propertyValJ); // Increase ref count so that framework does not free our JSON object + + afb_req_success(req, propertyValJ, "Retrieved property value"); +} + +// Audio related events + +PUBLIC void audiohlapi_get_list_actions(struct afb_req req) +{ + json_object *queryJ = NULL; + char * audioRole = NULL; + json_object * roleActionsJ = NULL; + + queryJ = afb_req_json(req); + int err = wrap_json_unpack(queryJ, "{s:s}", "audio_role",&audioRole); + if (err) { + afb_req_fail_f(req, "Invalid arguments", "Args not a valid json object query=%s", json_object_get_string(queryJ)); + return; + } + AFB_DEBUG("Parsed input arguments = audio_role:%s",audioRole); + + // Build and return list of actions for specific audio role + RoleInfoT * pRole = GetRole(audioRole); + if ( pRole == NULL ) + { + afb_req_fail_f(req, "Invalid audio role", "Audio role was not found in configuration -> %s",audioRole); + return; + } + + roleActionsJ = json_object_new_array(); + if (pRole->pActionList) { + int iNumberActions = pRole->pActionList->len; + for ( int i = 0 ; i < iNumberActions; i++) + { + char * pActionName = g_ptr_array_index(pRole->pActionList,i); + json_object * actionJ = json_object_new_string(pActionName); + json_object_array_add(roleActionsJ, actionJ); + } + } + + afb_req_success(req, roleActionsJ, "Retrieved action list for audio role"); +} + +PUBLIC void audiohlapi_post_action(struct afb_req req) +{ + json_object *queryJ = NULL; + char * actionName = NULL; + char * audioRole = NULL; + char * mediaName = NULL; + json_object *actionContext = NULL; + + queryJ = afb_req_json(req); + int err = wrap_json_unpack(queryJ, "{s:s,s:s,s?s,s?o}", "action_name", &actionName,"audio_role",&audioRole,"media_name",&mediaName,"action_context",&actionContext); + if (err) { + afb_req_fail_f(req, "Invalid arguments", "Args not a valid json object query=%s", json_object_get_string(queryJ)); + return; + } + AFB_DEBUG("Parsed input arguments = action_name:%s audio_role:%s", actionName,audioRole); + + // Verify if known action for audio role + RoleInfoT * pRole = GetRole(audioRole); + if ( pRole == NULL ) + { + afb_req_fail_f(req, "Invalid audio role", "Audio role was not found in configuration -> %s",audioRole); + return; + } + + // Check to find specific action + int iActionFound = 0; + if (pRole->pActionList) { + int iNumberActions = pRole->pActionList->len; + char * pTargetActionName = NULL; + for ( int i = 0 ; i < iNumberActions; i++) + { + pTargetActionName = g_ptr_array_index(pRole->pActionList,i); + if ( strcasecmp(pTargetActionName,actionName)==0) { + iActionFound = 1; + break; + } + } + } + + if (!iActionFound) { + afb_req_fail_f(req, "Event not found for audio role", "Event -> %s not found for role:%s",actionName,audioRole); + return; + } + +#ifndef AHL_DISCONNECT_POLICY + // Call policy to allow custom policy actions in current context (e.g. cancel playback) + int policyAllowed = Policy_PostAction(queryJ); + if (!policyAllowed) + { + afb_req_fail(req, "Audio policy violation", "Post sound action not allowed in current context"); + return; + } +#endif + + //afb_event_push(g_AHLCtx.policyCtx.postEvent,queryJ); + + afb_req_success(req, NULL, "Posted sound action"); + } + + +// Monitoring +PUBLIC void audiohlapi_subscribe(struct afb_req req) +{ + json_object *queryJ = NULL; + json_object * eventArrayJ = NULL; + + queryJ = afb_req_json(req); + int err = wrap_json_unpack(queryJ, "{s:o}", "events", &eventArrayJ); + if (err) { + afb_req_fail_f(req, "Invalid arguments", "Args not a valid json object query=%s", json_object_get_string(queryJ)); + return; + } + + int iNumEvents = json_object_array_length(eventArrayJ); + for (int i = 0; i < iNumEvents; i++) + { + char * pEventName = NULL; + json_object * jEvent = json_object_array_get_idx(eventArrayJ,i); + pEventName = (char *)json_object_get_string(jEvent); + if(pEventName == NULL) { + afb_req_fail(req, "failed", "Invalid event"); + return; + } + else if(!strcasecmp(pEventName, AHL_ENDPOINT_PROPERTY_EVENT)) { + afb_req_subscribe(req, g_AHLCtx.policyCtx.propertyEvent); + AFB_DEBUG("Client subscribed to endpoint property events"); + } + else if(!strcasecmp(pEventName, AHL_ENDPOINT_VOLUME_EVENT)) { + afb_req_subscribe(req, g_AHLCtx.policyCtx.volumeEvent); + AFB_DEBUG("Client subscribed to endpoint volume events"); + } + else if(!strcasecmp(pEventName, AHL_POST_ACTION_EVENT)) { + afb_req_subscribe(req, g_AHLCtx.policyCtx.postActionEvent); + AFB_DEBUG("Client subscribed to post event calls events"); + } + else { + afb_req_fail(req, "failed", "Invalid event"); + return; + } + } + + afb_req_success(req, NULL, "Subscribe to events finished"); +} + +PUBLIC void audiohlapi_unsubscribe(struct afb_req req) +{ + json_object *queryJ = NULL; + json_object * eventArrayJ = NULL; + + queryJ = afb_req_json(req); + int err = wrap_json_unpack(queryJ, "{s:o}", "events", &eventArrayJ); + if (err) { + afb_req_fail_f(req, "Invalid arguments", "Args not a valid json object query=%s", json_object_get_string(queryJ)); + return; + } + + int iNumEvents = json_object_array_length(eventArrayJ); + for (int i = 0; i < iNumEvents; i++) + { + char * pEventName = NULL; + json_object * jEvent = json_object_array_get_idx(eventArrayJ,i); + pEventName = (char *)json_object_get_string(jEvent); + if(pEventName == NULL) { + afb_req_fail(req, "failed", "Invalid event"); + return; + } + else if(!strcasecmp(pEventName, AHL_ENDPOINT_PROPERTY_EVENT)) { + afb_req_unsubscribe(req, g_AHLCtx.policyCtx.propertyEvent); + AFB_DEBUG("Client unsubscribed to endpoint property events"); + } + else if(!strcasecmp(pEventName, AHL_ENDPOINT_VOLUME_EVENT)) { + afb_req_unsubscribe(req, g_AHLCtx.policyCtx.volumeEvent); + AFB_DEBUG("Client unsubscribed to endpoint volume events"); + } + else if(!strcasecmp(pEventName, AHL_POST_ACTION_EVENT)) { + afb_req_unsubscribe(req, g_AHLCtx.policyCtx.postActionEvent); + AFB_DEBUG("Client unsubscribed to post event calls events"); + } + else { + afb_req_fail(req, "failed", "Invalid event"); + return; + } + } + + afb_req_success(req, NULL, "Unsubscribe to events finished"); +} + +// Since the policy is currently in the same binding, it cannot raise events on its own +// This is a first step toward isolation, when policy is migrated in its own binding it can simply raise AGL events +// This binding will register for these policy events and will execute the code below upon event reception +PUBLIC void audiohlapi_raise_event(json_object * pEventDataJ) +{ + char * pEventName = NULL; + + int err = wrap_json_unpack(pEventDataJ,"{s:s}","event_name", &pEventName); + if(err) + { + AFB_ERROR("Unable to retrieve event name"); + return; + } + + if(strcasecmp(pEventName, AHL_ENDPOINT_INIT_EVENT)==0) { + char * pAudioRole = NULL; + endpointID_t endpointID = AHL_UNDEFINED; + EndpointTypeT endpointType = ENDPOINTTYPE_MAXVALUE; + int err = wrap_json_unpack(pEventDataJ,"{s:i,s:i,s:s}", + "endpoint_id", &endpointID, + "endpoint_type", &endpointType, + "audio_role", &pAudioRole); + if(err) + { + AFB_ERROR("Unable to unpack property init event"); + return; + } + + RoleInfoT * pRole = GetRole(pAudioRole); + if ( pRole == NULL ){ + AFB_ERROR("Requested audio role does not exist in current configuration -> %s", pAudioRole); + return; + } + + EndpointInfoT * pEndpointInfo = InitEndpointInfo(); + g_assert_nonnull(pEndpointInfo); + PolicyCtxJSONToEndpoint(pEventDataJ,pEndpointInfo); + + err = ReplaceEndpointInfoWithRole(endpointID,endpointType,pRole,pEndpointInfo); + if(err == AHL_FAIL) + { + AFB_ERROR("Can't update EndpointInfo aborting"); + return; + } + + // Remove event name from object + // json_object_object_del(pEventDataJ,"event_name"); + //afb_event_push(g_AHLCtx.policyCtx.propertyEvent,pEventDataJ); // Not broadcasted to application at this time + } + else if(strcasecmp(pEventName, AHL_ENDPOINT_PROPERTY_EVENT)==0) { + char * pAudioRole = NULL; + char * pPropertyName = NULL; + endpointID_t endpointID = AHL_UNDEFINED; + EndpointTypeT endpointType = ENDPOINTTYPE_MAXVALUE; + json_object * propValueJ = NULL; + int err = wrap_json_unpack(pEventDataJ,"{s:i,s:i,s:s,s:o,s:s}", + "endpoint_id", &endpointID, + "endpoint_type", &endpointType, + "property_name", &pPropertyName, + "value",&propValueJ, + "audio_role", &pAudioRole); + if(err) + { + AFB_ERROR("Unable to unpack property event"); + return; + } + RoleInfoT * pRole = GetRole(pAudioRole); + if ( pRole == NULL ){ + AFB_ERROR("Requested audio role does not exist in current configuration -> %s", pAudioRole); + return; + } + EndpointInfoT * pEndpointInfo = GetEndpointInfoWithRole(endpointID,endpointType,pRole); + // update property value + if (pEndpointInfo->pPropTable) + { + json_type jType = json_object_get_type(propValueJ); + switch (jType) { + case json_type_double: + Add_Endpoint_Property_Double(pEndpointInfo,pPropertyName,json_object_get_double(propValueJ)); + break; + case json_type_int: + Add_Endpoint_Property_Int(pEndpointInfo,pPropertyName,json_object_get_int(propValueJ)); + break; + case json_type_string: + Add_Endpoint_Property_String(pEndpointInfo,pPropertyName,json_object_get_string(propValueJ)); + break; + default: + AFB_ERROR("Invalid property argument Property value not a valid json object query=%s", json_object_get_string(propValueJ)); + return ; + } + } + // Remove event name from object + json_object_object_del(pEventDataJ,"event_name"); + afb_event_push(g_AHLCtx.policyCtx.propertyEvent,pEventDataJ); + } + else if(strcasecmp(pEventName, AHL_ENDPOINT_VOLUME_EVENT)==0) { + char * pAudioRole = NULL; + endpointID_t endpointID = AHL_UNDEFINED; + EndpointTypeT endpointType = ENDPOINTTYPE_MAXVALUE; + int iVolume = 0; + int err = wrap_json_unpack(pEventDataJ,"{s:i,s:i,s:i,s:s}", + "endpoint_id", &endpointID, + "endpoint_type", &endpointType, + "value",&iVolume, + "audio_role", &pAudioRole); + if(err) + { + AFB_ERROR("Unable to unpack volume event data"); + return; + } + RoleInfoT * pRole = GetRole(pAudioRole); + if ( pRole == NULL ){ + AFB_ERROR("Requested audio role does not exist in current configuration -> %s", pAudioRole); + return; + } + EndpointInfoT * pEndpointInfo = GetEndpointInfoWithRole(endpointID,endpointType,pRole); + // update volume value + pEndpointInfo->iVolume = iVolume; + // Remove event name from object + json_object_object_del(pEventDataJ,"event_name"); + afb_event_push(g_AHLCtx.policyCtx.volumeEvent,pEventDataJ); + } + else if(strcasecmp(pEventName, AHL_POST_ACTION_EVENT)==0) { + // Remove event name from object + json_object_object_del(pEventDataJ,"event_name"); + afb_event_push(g_AHLCtx.policyCtx.postActionEvent,pEventDataJ); + } + else if(strcasecmp(pEventName, AHL_STREAM_STATE_EVENT)==0) { + streamID_t streamID = AHL_UNDEFINED; + StreamEventT streamEvent = STREAM_EVENT_MAXVALUE; + int err = wrap_json_unpack(pEventDataJ,"{s:i,s:i}", + "stream_id", &streamID, + "state_event", &streamEvent); + if(err) + { + AFB_ERROR("Unable to unpack stream event data"); + return; + } + + StreamInfoT * pStreamInfo = GetStream(streamID); + if (pStreamInfo == NULL) { + AFB_ERROR("Specified stream not currently active stream_id -> %d",streamID); + return; + } + + // update streamstate value + switch (streamEvent) { + case STREAM_EVENT_START: + pStreamInfo->streamState = STREAM_STATE_RUNNING; + break; + case STREAM_EVENT_STOP: + pStreamInfo->streamState = STREAM_STATE_IDLE; + break; + case STREAM_EVENT_PAUSE: + pStreamInfo->streamState = STREAM_STATE_PAUSED; + break; + case STREAM_EVENT_RESUME: + pStreamInfo->streamState = STREAM_STATE_RUNNING; + break; + case STREAM_EVENT_MUTED: + pStreamInfo->streamMute = STREAM_MUTED; + break; + case STREAM_EVENT_UNMUTED: + pStreamInfo->streamMute = STREAM_UNMUTED; + break; + default: + AFB_ERROR("Unknown stream event"); + } + + // Remove event name from object + json_object_object_del(pEventDataJ,"event_name"); + AFB_ERROR("pEventDataJ=%s", json_object_get_string(pEventDataJ)); + afb_event_push(pStreamInfo->streamStateEvent,pEventDataJ); + } + else { + AFB_ERROR("Unknown event name"); + } +}
\ No newline at end of file diff --git a/ahl-binding/ahl-binding.h b/ahl-binding/ahl-binding.h new file mode 100644 index 0000000..9b4c37e --- /dev/null +++ b/ahl-binding/ahl-binding.h @@ -0,0 +1,94 @@ +/* + * Copyright (C) 2017 "Audiokinetic Inc" + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef AHL_BINDING_INCLUDE +#define AHL_BINDING_INCLUDE + +#define AFB_BINDING_VERSION 2 + +//#define AHL_DISCONNECT_POLICY // define for debugging HLB in standalone only + +#include <json-c/json.h> +#include <glib.h> + +#include <afb/afb-binding.h> +#include "ahl-interface.h" +#include "ahl-policy-utils.h" + + +#ifndef PUBLIC + #define PUBLIC +#endif + +#define AHL_SUCCESS 0 +#define AHL_FAIL 1 + +#define AHL_ACCESS_CONTROL_GRANTED 1 +#define AHL_ACCESS_CONTROL_DENIED 0 + +#define AHL_UNDEFINED -1 +#define AHL_STR_MAX_LENGTH 256 + + +typedef struct RoleInfo { + char * pRoleName; // Role string identifier + int iPriority; // Role normalized priority (0-100) + InterruptBehaviorT eInterruptBehavior; // Role behavior when interrupting lower priority streams + GPtrArray * pActionList; // List of supported actions for the role (gchar*) + GPtrArray * pSourceEndpoints; // Source endpoints info (EndpointInfoT*) + GPtrArray * pSinkEndpoints; // Sink endpoints info (EndpointInfoT*) +} RoleInfoT; + +// Parts of the context that are visible to the policy (for state based decisions) +typedef struct AHLPolicyCtx { + GHashTable * pRoleInfo; // Hash table of role information structure (RoleInfoT*) accessed by role name + GHashTable * pStreams; // List of active streams (StreamInfoT*) accessed by streamID + GPtrArray * pHALList; // List of HAL dependencies + // TODO: Events need to be sent directly by HLB when separation with policy complete + struct afb_event propertyEvent; // AGL event used when property changes + struct afb_event volumeEvent; // AGL event used when volume changes + struct afb_event postActionEvent; // AGL event used on post action call +} AHLPolicyCtxT; + +// Global binding context +typedef struct AHLCtx { + AHLPolicyCtxT policyCtx; + endpointID_t nextSourceEndpointID; // Counter to assign new ID + endpointID_t nextSinkEndpointID; // Counter to assign new ID + endpointID_t nextStreamID; // Counter to assign new ID +} AHLCtxT; + +// Client specific binding context +typedef struct AHLClientCtx { + GArray * pStreamAccessList; // List of streams that client has control over +} AHLClientCtxT; + +// ahl-binding.c +PUBLIC int AhlBindingInit(); +PUBLIC void AhlOnEvent(const char *evtname, json_object *eventJ); + +// ahl-deviceenum.c +int EnumerateDevices(json_object * in_jDeviceArray, char * in_pAudioRole, EndpointTypeT in_deviceType, GPtrArray * out_pEndpointArray); +EndpointInfoT * InitEndpointInfo(); +void TermEndpointInfo( EndpointInfoT * out_pEndpointInfo ); +// ahl-config.c +int ParseHLBConfig(); +// ahl-policy.c +#ifndef AHL_DISCONNECT_POLICY +PUBLIC void audiohlapi_raise_event(json_object *EventDataJ); +#endif + +#endif // AHL_BINDING_INCLUDE diff --git a/ahl-binding/ahl-config.c b/ahl-binding/ahl-config.c new file mode 100644 index 0000000..5e3de08 --- /dev/null +++ b/ahl-binding/ahl-config.c @@ -0,0 +1,178 @@ +/* + * Copyright (C) 2017 "Audiokinetic Inc" + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#define _GNU_SOURCE +#include <stdio.h> +#include <string.h> +#include <json-c/json.h> +#include "wrap-json.h" + +#include "ahl-binding.h" + +extern AHLCtxT g_AHLCtx; + +static InterruptBehaviorT InterruptBehaviorToEnum(char * in_pInterruptBehaviorStr) +{ + g_assert_nonnull(in_pInterruptBehaviorStr); + if (strcasecmp(in_pInterruptBehaviorStr,AHL_INTERRUPTBEHAVIOR_CONTINUE)==0) { + return INTERRUPTBEHAVIOR_CONTINUE; + } + else if (strcasecmp(in_pInterruptBehaviorStr,AHL_INTERRUPTBEHAVIOR_CANCEL)==0) { + return INTERRUPTBEHAVIOR_CANCEL; + } + else if (strcasecmp(in_pInterruptBehaviorStr,AHL_INTERRUPTBEHAVIOR_PAUSE)==0) { + return INTERRUPTBEHAVIOR_PAUSE; + } + else + return INTERRUPTBEHAVIOR_MAXVALUE; +} + +int ParseHLBConfig() { + char * versionStr = NULL; + json_object * jAudioRoles = NULL; + json_object * jHALList = NULL; + char * policyModule = NULL; + + // TODO: This should be retrieve from binding startup arguments + char configfile_path[256]; + if(getenv("AHL_CONFIG_FILE") == NULL) + { + AFB_ERROR("Please Set Environnement Variable AHL_CONFIG_FILE"); + return 1; + } + + sprintf(configfile_path, "%s", getenv("AHL_CONFIG_FILE")); + AFB_INFO("High-level config file -> %s\n", configfile_path); + + // Open configuration file + json_object *config_JFile=json_object_from_file(configfile_path); + if(config_JFile == NULL) + { + AFB_ERROR("Error: Can't open configuration file -> %s",configfile_path); + return 1; + } + + int err = wrap_json_unpack(config_JFile, "{s:s,s:s,s:o,s:o}", "version", &versionStr,"policy_module", &policyModule,"audio_roles",&jAudioRoles,"hal_list",&jHALList); + if (err) { + AFB_ERROR("Invalid configuration file -> %s", configfile_path); + return 1; + } + AFB_INFO("High-level audio API version: %s", "1.0.0"); + AFB_INFO("Config version: %s", versionStr); + AFB_INFO("Policy module: %s", policyModule); + + int iHALListLength = json_object_array_length(jHALList); + g_AHLCtx.policyCtx.pHALList = g_ptr_array_new_with_free_func(g_free); + int iNumberOfRoles = json_object_array_length(jAudioRoles); + g_AHLCtx.policyCtx.pRoleInfo = g_hash_table_new(g_str_hash, g_str_equal); + + for (int i = 0; i < iHALListLength; i++) + { + char * pHAL = NULL; + json_object * jHAL = json_object_array_get_idx(jHALList,i); + if (jHAL) { + pHAL = (char *)json_object_get_string(jHAL); + char * pHALName = g_strdup( pHAL ); + g_ptr_array_add( g_AHLCtx.policyCtx.pHALList, pHALName ); + + // Set dependency on HAL specified + err = afb_daemon_require_api_v2(pHAL,1) ; + if( err != 0 ) + { + AFB_ERROR("Audio high level API could not set dependency on API: %s",pHAL); + return 1; + } + } + } + + for (int i = 0; i < iNumberOfRoles; i++) + { + int priority = 0; + json_object * jAudioRole = json_object_array_get_idx(jAudioRoles,i); + json_object * jOutputDevices = NULL; + json_object * jInputDevices = NULL; + json_object * jActions = NULL; + char * pRoleName = NULL; + char * pInteruptBehavior = NULL; + + int iNumOutDevices = 0; + int iNumInDevices = 0; + int iNumActions = 0; + + err = wrap_json_unpack(jAudioRole, "{s:s,s:i,s:s,s?o,s?o,s?o}", + "name", &pRoleName, + "priority",&priority, + "interupt_behavior",&pInteruptBehavior, + "output",&jOutputDevices, + "input",&jInputDevices, + "actions",&jActions + ); + if (err) { + AFB_ERROR("Invalid audio role configuration : %s", json_object_to_json_string(jAudioRole)); + return 1; + } + + if (jOutputDevices) + iNumOutDevices = json_object_array_length(jOutputDevices); + if (jInputDevices) + iNumInDevices = json_object_array_length(jInputDevices); + if (jActions) + iNumActions = json_object_array_length(jActions); + + RoleInfoT * pRoleInfo = (RoleInfoT*) malloc(sizeof(RoleInfoT)); + memset(pRoleInfo,0,sizeof(RoleInfoT)); + pRoleInfo->pRoleName = g_strdup( pRoleName ); + pRoleInfo->iPriority = priority; + pRoleInfo->eInterruptBehavior = InterruptBehaviorToEnum(pInteruptBehavior); + + // Actions + pRoleInfo->pActionList = g_ptr_array_new_with_free_func(g_free); + // Parse and validate list of available actions + for (int i = 0; i < iNumActions; i++) + { + json_object * jAction = json_object_array_get_idx(jActions,i); + char * pActionName = (char *)json_object_get_string(jAction); + if (pActionName) + g_ptr_array_add(pRoleInfo->pActionList, g_strdup(pActionName)); + } + + // Sources + pRoleInfo->pSourceEndpoints = g_ptr_array_new_with_free_func(g_free); + if (iNumInDevices) { + err = EnumerateDevices(jInputDevices,pRoleName,ENDPOINTTYPE_SOURCE,pRoleInfo->pSourceEndpoints); + if (err) { + AFB_ERROR("Invalid input devices : %s", json_object_to_json_string(jInputDevices)); + return 1; + } + } + // Sinks + pRoleInfo->pSinkEndpoints = g_ptr_array_new_with_free_func(g_free); + if (iNumOutDevices) { + err = EnumerateDevices(jOutputDevices,pRoleName,ENDPOINTTYPE_SINK,pRoleInfo->pSinkEndpoints); + if (err) { + AFB_ERROR("Invalid output devices : %s", json_object_to_json_string(jOutputDevices)); + return 1; + } + } + + g_hash_table_insert(g_AHLCtx.policyCtx.pRoleInfo, pRoleInfo->pRoleName, pRoleInfo); + + } + + // Build lists of all device URI referenced in config file (input/output) + AFB_DEBUG ("Audio high-level - Parse high-level audio configuration done"); + return 0; +} diff --git a/ahl-binding/ahl-deviceenum.c b/ahl-binding/ahl-deviceenum.c new file mode 100644 index 0000000..7e67d7a --- /dev/null +++ b/ahl-binding/ahl-deviceenum.c @@ -0,0 +1,331 @@ +/* + * Copyright (C) 2017 "Audiokinetic Inc" + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#define _GNU_SOURCE +#include <stdio.h> +#include <string.h> +#include <alsa/asoundlib.h> +#include <alsa/pcm.h> +#include <json-c/json.h> +#include "wrap-json.h" + +#include "ahl-binding.h" +#include "ahl-policy.h" + +extern AHLCtxT g_AHLCtx; + +// TODO: Hash from endpoint ID information instead +static endpointID_t CreateNewSourceID() +{ + endpointID_t newID = g_AHLCtx.nextSourceEndpointID; + g_AHLCtx.nextSourceEndpointID++; + return newID; +} + +// TODO: Hash from endpoint ID information instead +static endpointID_t CreateNewSinkID() +{ + endpointID_t newID = g_AHLCtx.nextSinkEndpointID; + g_AHLCtx.nextSinkEndpointID++; + return newID; +} + +// Watchout: This function uses strtok and is destructive on the input string (use a copy) +static int SeparateDomainFromDeviceURI( char * in_pDeviceURI, char ** out_pDomain, char ** out_pDevice) +{ + *out_pDomain = strtok(in_pDeviceURI, "."); + if (*out_pDomain == NULL) + { + AFB_ERROR("Error tokenizing device URI -> %s",in_pDeviceURI); + return 1; + } + // TODO: Validate domain is known string (e.g. ALSA,Pulse,GStreamer) + *out_pDevice = strtok(NULL, "."); + if (*out_pDevice == NULL) + { + AFB_ERROR("Error tokenizing device URI -> %s",in_pDeviceURI); + return 1; + } + return 0; +} + +static int IsAlsaDomain(const char * in_pDomainStr) +{ + return (int) (strcasecmp(in_pDomainStr,AHL_DOMAIN_ALSA) == 0); +} + +static int IsPulseDomain(const char * in_pDomainStr) +{ + return (int) (strcasecmp(in_pDomainStr,AHL_DOMAIN_PULSE) == 0); +} + +static int IsGStreamerDomain(const char * in_pDomainStr) +{ + return (int) (strcasecmp(in_pDomainStr,AHL_DOMAIN_GSTREAMER) == 0); +} + +static int IsExternalDomain(const char * in_pDomainStr) +{ + return (int) (strcasecmp(in_pDomainStr,AHL_DOMAIN_EXTERNAL) == 0); +} + +static int FillALSAPCMInfo( snd_pcm_t * in_pPcmHandle, EndpointInfoT * out_pEndpointInfo ) +{ + g_assert_nonnull(in_pPcmHandle); + g_assert_nonnull(out_pEndpointInfo); + snd_pcm_type_t pcmType = 0; + snd_pcm_info_t * pPcmInfo = NULL; + int iAlsaRet = 0; + const char * pCardName = NULL; + int retVal = 0; + snd_ctl_t * ctlHandle = NULL; + snd_ctl_card_info_t * ctlInfo = NULL; + + snd_pcm_info_alloca(&pPcmInfo); + snd_ctl_card_info_alloca(&ctlInfo); + + // retrieve PCM type + pcmType = snd_pcm_type(in_pPcmHandle); + switch (pcmType) { + case SND_PCM_TYPE_HW: + out_pEndpointInfo->deviceURIType = DEVICEURITYPE_ALSA_HW; + break; + case SND_PCM_TYPE_DMIX: + out_pEndpointInfo->deviceURIType = DEVICEURITYPE_ALSA_DMIX; + break; + case SND_PCM_TYPE_SOFTVOL: + out_pEndpointInfo->deviceURIType = DEVICEURITYPE_ALSA_SOFTVOL; + break; + case SND_PCM_TYPE_PLUG: + out_pEndpointInfo->deviceURIType = DEVICEURITYPE_ALSA_PLUG; + break; + default: + out_pEndpointInfo->deviceURIType = DEVICEURITYPE_ALSA_OTHER; + break; + } + + iAlsaRet = snd_pcm_info(in_pPcmHandle,pPcmInfo); + if (iAlsaRet < 0) + { + AFB_WARNING("Error retrieving PCM device info"); + return 1; + } + + // get card number + out_pEndpointInfo->alsaInfo.cardNum = snd_pcm_info_get_card(pPcmInfo); + if ( out_pEndpointInfo->alsaInfo.cardNum < 0 ) + { + AFB_WARNING("No Alsa card number available"); + return 1; + } + + // get device number + out_pEndpointInfo->alsaInfo.deviceNum = snd_pcm_info_get_device(pPcmInfo); + if ( out_pEndpointInfo->alsaInfo.deviceNum < 0 ) + { + AFB_WARNING("No Alsa device number available"); + return 1; + } + + // get sub-device number + out_pEndpointInfo->alsaInfo.subDeviceNum = snd_pcm_info_get_subdevice(pPcmInfo); + if ( out_pEndpointInfo->alsaInfo.subDeviceNum < 0 ) + { + AFB_WARNING("No Alsa subdevice number available"); + return 1; + } + + char cardName[32]; + sprintf(cardName, "hw:%d", out_pEndpointInfo->alsaInfo.cardNum); + iAlsaRet = snd_ctl_open(&ctlHandle, cardName, 0); + if ( iAlsaRet < 0 ) + { + AFB_WARNING("Could not open ALSA card control"); + return 1; + } + + iAlsaRet = snd_ctl_card_info(ctlHandle, ctlInfo); + if ( iAlsaRet < 0 ) + { + AFB_WARNING("Could not retrieve ALSA card info"); + snd_ctl_close(ctlHandle); + return 1; + } + + // Populate unique target card name + pCardName = snd_ctl_card_info_get_id(ctlInfo); + if (pCardName == NULL) + { + AFB_WARNING("No Alsa card name available"); + snd_ctl_close(ctlHandle); + return 1; + } + out_pEndpointInfo->gsDeviceName = g_strdup(pCardName); + + snd_ctl_close(ctlHandle); + + return retVal; +} + +EndpointInfoT * InitEndpointInfo() +{ + EndpointInfoT * pEndpointInfo = (EndpointInfoT*) malloc(sizeof(EndpointInfoT)); + memset(pEndpointInfo,0,sizeof(EndpointInfoT)); + pEndpointInfo->endpointID = AHL_UNDEFINED; + pEndpointInfo->type = ENDPOINTTYPE_MAXVALUE; + pEndpointInfo->deviceURIType = DEVICEURITYPE_MAXVALUE; + pEndpointInfo->alsaInfo.cardNum = AHL_UNDEFINED; + pEndpointInfo->alsaInfo.deviceNum = AHL_UNDEFINED; + pEndpointInfo->alsaInfo.subDeviceNum = AHL_UNDEFINED; + pEndpointInfo->format.sampleRate = AHL_UNDEFINED; + pEndpointInfo->format.numChannels = AHL_UNDEFINED; + pEndpointInfo->format.sampleType = AHL_FORMAT_UNKNOWN; + pEndpointInfo->pPropTable = g_hash_table_new(g_str_hash, g_str_equal); + return pEndpointInfo; +} + +void TermEndpointInfo( EndpointInfoT * out_pEndpointInfo ) +{ + #define SAFE_FREE(__ptr__) if(__ptr__) g_free(__ptr__); __ptr__ = NULL; + SAFE_FREE(out_pEndpointInfo->gsDeviceName); + SAFE_FREE(out_pEndpointInfo->gsDisplayName); + SAFE_FREE(out_pEndpointInfo->gsDeviceDomain); + SAFE_FREE(out_pEndpointInfo->pRoleName); + SAFE_FREE(out_pEndpointInfo->gsDeviceURI); + SAFE_FREE(out_pEndpointInfo->gsHALAPIName); + + if (out_pEndpointInfo->pPropTable) { + // Free json_object for all property values + GHashTableIter iter; + gpointer key, value; + g_hash_table_iter_init (&iter, out_pEndpointInfo->pPropTable); + while (g_hash_table_iter_next (&iter, &key, &value)) + { + if (value) + json_object_put(value); + } + g_hash_table_remove_all(out_pEndpointInfo->pPropTable); + g_hash_table_destroy(out_pEndpointInfo->pPropTable); + out_pEndpointInfo->pPropTable = NULL; + } + // GLib automatically frees item when removed from the array +} + +// For a given audio role +int EnumerateDevices(json_object * in_jDeviceArray, char * in_pAudioRole, EndpointTypeT in_deviceType, GPtrArray * out_pEndpointArray) { + + g_assert_nonnull(in_jDeviceArray); + int iNumberDevices = json_object_array_length(in_jDeviceArray); + + // Parse and validate list of available devices + for (int i = 0; i < iNumberDevices; i++) + { + char * pDeviceURIDomain = NULL; + char * pFullDeviceURI = NULL; + char * pDeviceURIPCM = NULL; + int err = 0; + + json_object * jDevice = json_object_array_get_idx(in_jDeviceArray,i); + if (jDevice == NULL) { + AFB_WARNING("Invalid device array -> %s",json_object_to_json_string(in_jDeviceArray)); + continue; + } + // strip domain name from URI + pFullDeviceURI = (char *)json_object_get_string(jDevice); + char * pFullDeviceURICopy = g_strdup(pFullDeviceURI); // strtok is destructive + err = SeparateDomainFromDeviceURI(pFullDeviceURICopy,&pDeviceURIDomain,&pDeviceURIPCM); + if (err) + { + AFB_WARNING("Invalid device URI string -> %s",pFullDeviceURICopy); + continue; + } + + EndpointInfoT * pEndpointInfo = InitEndpointInfo(); + g_assert_nonnull(pEndpointInfo); + + // non ALSA URI are simply passed to application (no validation) at this time + // In Non ALSA case devices in config are assumed to be available, if not application can fallback on explicit device selection + pEndpointInfo->gsDeviceName = g_strdup(pDeviceURIPCM); + pEndpointInfo->gsDeviceDomain = g_strdup(pDeviceURIDomain); + pEndpointInfo->gsDeviceURI = g_strdup(pDeviceURIPCM); + pEndpointInfo->pRoleName = g_strdup(in_pAudioRole); + + g_free(pFullDeviceURICopy); + pFullDeviceURICopy = NULL; + pDeviceURIDomain = NULL; //Derived from above mem + pDeviceURIPCM = NULL; //Derived from above mem + + if (IsAlsaDomain(pEndpointInfo->gsDeviceDomain)) + { + // TODO: Missing support for loose name matching + // This will require using ALSA hints to get PCM names + // And would iterate over all available devices matching string (possibly all if no filtering is desired for a certain role) + + // Get PCM handle + snd_pcm_t * pPcmHandle = NULL; + snd_pcm_stream_t streamType = in_deviceType == ENDPOINTTYPE_SOURCE ? SND_PCM_STREAM_CAPTURE : SND_PCM_STREAM_PLAYBACK; + err = snd_pcm_open(&pPcmHandle, pEndpointInfo->gsDeviceURI, streamType, 0); + if (err < 0) + { + AFB_NOTICE("Alsa PCM device was not found -> %s", pEndpointInfo->gsDeviceURI); + continue; + } + + err = FillALSAPCMInfo(pPcmHandle,pEndpointInfo); + if (err) { + AFB_WARNING("Unable to retrieve PCM information for PCM -> %s",pEndpointInfo->gsDeviceURI); + snd_pcm_close(pPcmHandle); + continue; + } + + snd_pcm_close(pPcmHandle); + } + else if (IsPulseDomain(pEndpointInfo->gsDeviceDomain)) { + // Pulse domain + // For now display name is device URI directly, could extrapolated using more heuristics or even usins Pulse API later on + pEndpointInfo->deviceURIType = DEVICEURITYPE_NOT_ALSA; + } + else if (IsGStreamerDomain(pEndpointInfo->gsDeviceDomain)){ + // GStreamer domain + // For now display name is device URI directly, could extrapolated using more heuristics or even usins GStreamer API later on + pEndpointInfo->deviceURIType = DEVICEURITYPE_NOT_ALSA; + } + else if (IsExternalDomain(pEndpointInfo->gsDeviceDomain)){ + // External domain + pEndpointInfo->deviceURIType = DEVICEURITYPE_NOT_ALSA; + } + else { + // Unknown domain + AFB_WARNING("Unknown domain in device URI string -> %s",pFullDeviceURI); + continue; + } + + pEndpointInfo->endpointID = in_deviceType == ENDPOINTTYPE_SOURCE ? CreateNewSourceID() : CreateNewSinkID(); + pEndpointInfo->type = in_deviceType; + // Assigned by policy initialization + pEndpointInfo->gsDisplayName = malloc(AHL_STR_MAX_LENGTH*sizeof(char)); + memset(pEndpointInfo->gsDisplayName,0,AHL_STR_MAX_LENGTH*sizeof(char)); + pEndpointInfo->gsHALAPIName = malloc(AHL_STR_MAX_LENGTH*sizeof(char)); + memset(pEndpointInfo->gsDisplayName,0,AHL_STR_MAX_LENGTH*sizeof(char)); + + // add to structure to list of available devices + g_ptr_array_add(out_pEndpointArray, pEndpointInfo); + + } // for all devices + + AFB_DEBUG ("Audio high-level - Enumerate devices done"); + return 0; +}
\ No newline at end of file |