From 298bbf445a731b85cb8d5d19a3b595e8870d8701 Mon Sep 17 00:00:00 2001 From: Scott Murray Date: Fri, 17 Jun 2022 20:24:27 -0400 Subject: Repurpose into VIS client Repurpose repository for a spiritual successor of the previous binding. The replacement is a daemon that demonstrates servicing the volume actuator from the VSS schema via VIS signals from KUKSA.val. Currently the connection to KUKSA.val is websocket based using the boost::asio framework, but the plan is to migrate to gRPC as that becomes more robust in KUKSA.val. As well, this new code will serve as the base for implementing a gRPC API to expose the full set of WirePlumber controls as was done with the previous binding. Notable changes: - New code is completely C++, partly to leverage using Boost, but also to futureproof future work with gRPC. The WirePlumber interfacing code that has been kept from the old binding is still C for now, converting it to C++ is a planned future rework. - Switch from CMake to meson for ease of development and some degree of futureproofing. - Use with systemd is assumed; behavior follows the systemd daemon guidelines barring the use of journald logging prefixes, which may be addressed with future work. A systemd unit is also installed as part of the build. - SPDX license headers using SPDX "short identifiers" are used in source files rather than the full copyright headers used in the previous codebase. This follows the direction that projects such as the Linux kernel are going in. Bug-AGL: SPEC-4409 Signed-off-by: Scott Murray Change-Id: Ibb7091c4354432bb094147d1419ab475486a4abc --- CMakeLists.txt | 3 - autobuild/agl/autobuild | 128 --------- autobuild/linux/autobuild | 128 --------- binding/CMakeLists.txt | 19 -- binding/audiomixer-binding.c | 402 -------------------------- binding/audiomixer.c | 463 ------------------------------ binding/audiomixer.h | 59 ---- conf.d/cmake/config.cmake | 160 ----------- conf.d/wgt/config.xml.in | 27 -- meson.build | 10 + src/audiomixer-service.cpp | 129 +++++++++ src/audiomixer-service.hpp | 44 +++ src/audiomixer.c | 463 ++++++++++++++++++++++++++++++ src/audiomixer.h | 71 +++++ src/main.cpp | 34 +++ src/meson.build | 18 ++ src/vis-config.cpp | 157 ++++++++++ src/vis-config.hpp | 43 +++ src/vis-session.cpp | 374 ++++++++++++++++++++++++ src/vis-session.hpp | 78 +++++ systemd/agl-service-audiomixer.service | 11 + systemd/meson.build | 3 + test/CMakeLists.txt | 28 -- test/afb-test/CMakeLists.txt | 21 -- test/afb-test/etc/CMakeLists.txt | 32 --- test/afb-test/etc/aft-agl-audiomixer.json | 22 -- test/afb-test/tests/CMakeLists.txt | 31 -- test/afb-test/tests/audiomixer.lua | 40 --- 28 files changed, 1435 insertions(+), 1563 deletions(-) delete mode 100644 CMakeLists.txt delete mode 100755 autobuild/agl/autobuild delete mode 100755 autobuild/linux/autobuild delete mode 100644 binding/CMakeLists.txt delete mode 100644 binding/audiomixer-binding.c delete mode 100644 binding/audiomixer.c delete mode 100644 binding/audiomixer.h delete mode 100644 conf.d/cmake/config.cmake delete mode 100644 conf.d/wgt/config.xml.in create mode 100644 meson.build create mode 100644 src/audiomixer-service.cpp create mode 100644 src/audiomixer-service.hpp create mode 100644 src/audiomixer.c create mode 100644 src/audiomixer.h create mode 100644 src/main.cpp create mode 100644 src/meson.build create mode 100644 src/vis-config.cpp create mode 100644 src/vis-config.hpp create mode 100644 src/vis-session.cpp create mode 100644 src/vis-session.hpp create mode 100644 systemd/agl-service-audiomixer.service create mode 100644 systemd/meson.build delete mode 100644 test/CMakeLists.txt delete mode 100644 test/afb-test/CMakeLists.txt delete mode 100644 test/afb-test/etc/CMakeLists.txt delete mode 100644 test/afb-test/etc/aft-agl-audiomixer.json delete mode 100644 test/afb-test/tests/CMakeLists.txt delete mode 100644 test/afb-test/tests/audiomixer.lua diff --git a/CMakeLists.txt b/CMakeLists.txt deleted file mode 100644 index f757721..0000000 --- a/CMakeLists.txt +++ /dev/null @@ -1,3 +0,0 @@ -CMAKE_MINIMUM_REQUIRED(VERSION 3.3) - -include(${CMAKE_CURRENT_SOURCE_DIR}/conf.d/cmake/config.cmake) diff --git a/autobuild/agl/autobuild b/autobuild/agl/autobuild deleted file mode 100755 index 16181b8..0000000 --- a/autobuild/agl/autobuild +++ /dev/null @@ -1,128 +0,0 @@ -#!/usr/bin/make -f -# Copyright (C) 2015 - 2018 "IoT.bzh" -# Copyright (C) 2020 Konsulko Group -# Author "Romain Forlot" -# -# 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. - -THISFILE := $(lastword $(MAKEFILE_LIST)) -ROOT_DIR := $(abspath $(dir $(THISFILE))/../..) - -# Build directories -# Note that the debug/test/coverage directories are defined in relation -# to the release directory (BUILD_DIR), this needs to be kept in mind -# if over-riding it and building those widget types, the specific widget -# type variable (e.g. BUILD_DIR_DEBUG) may also need to be specified -# to yield the desired output hierarchy. -BUILD_DIR = $(ROOT_DIR)/build -BUILD_DIR_DEBUG = $(abspath $(BUILD_DIR)/../build-debug) -BUILD_DIR_TEST = $(abspath $(BUILD_DIR)/../build-test) -BUILD_DIR_COVERAGE = $(abspath $(BUILD_DIR)/../build-coverage) - -# Output directory variable for use in pattern rules. -# This is intended for internal use only, hence the explicit override -# definition. -override OUTPUT_DIR = $(BUILD_DIR) - -# Final install directory for widgets -DEST = $(OUTPUT_DIR) - -# Default build type for release/test builds -BUILD_TYPE = RELEASE - -.PHONY: all help update install distclean -.PHONY: clean clean-release clean-debug clean-test clean-coverage clean-all -.PHONY: configure configure-release configure-debug configure-test configure-coverage -.PHONY: build build-release build-debug build-test build-coverage build-all -.PHONY: package package-release package-debug package-test package-coverage package-all - -help: - @echo "List of targets available:" - @echo "" - @echo "- all" - @echo "- help" - @echo "- clean" - @echo "- distclean" - @echo "- configure" - @echo "- build: compilation, link and prepare files for package into a widget" - @echo "- package: output a widget file '*.wgt'" - @echo "- install: install in your $(CMAKE_INSTALL_DIR) directory" - @echo "" - @echo "Usage: ./autobuild/agl/autobuild package DEST=${HOME}/opt" - @echo "Don't use your build dir as DEST as wgt file is generated at this location" - -all: package-all - -# Target specific variable over-rides so static pattern rules can be -# used for the various type-specific targets. - -configure-test build-test package-test clean-test: OUTPUT_DIR = $(BUILD_DIR_TEST) - -configure-coverage build-coverage package-coverage clean-coverage: OUTPUT_DIR = $(BUILD_DIR_COVERAGE) -configure-coverage build-coverage package-coverage: BUILD_TYPE = COVERAGE - -configure-debug build-debug package-debug clean-debug: OUTPUT_DIR = $(BUILD_DIR_DEBUG) -configure-debug build-debug package-debug: BUILD_TYPE = DEBUG - -clean-release clean-test clean-debug clean-coverage: - @if [ -d $(OUTPUT_DIR) ]; then \ - $(MAKE) -C $(OUTPUT_DIR) $(CLEAN_ARGS) clean; \ - else \ - echo Nothing to clean; \ - fi - -clean: clean-release - -clean-all: clean-release clean-test clean-debug clean-coverage - -distclean: clean-all - -configure-release configure-test configure-debug configure-coverage: - @mkdir -p $(OUTPUT_DIR) - @if [ ! -f $(OUTPUT_DIR)/Makefile ]; then \ - (cd $(OUTPUT_DIR) && cmake -S $(ROOT_DIR) -DCMAKE_BUILD_TYPE=$(BUILD_TYPE) $(CONFIGURE_ARGS)); \ - fi - -configure: configure-release - -build-release build-debug build-coverage: build-%: configure-% - @cmake --build $(OUTPUT_DIR) $(BUILD_ARGS) --target all - -# Kept for consistency, empty to avoid building everything for test widget -build-test: configure-test - -build: build-release - -build-all: build-release build-debug build-test build-coverage - -package-release package-debug package-coverage: package-%: build-% - @cmake --build $(OUTPUT_DIR) $(PACKAGE_ARGS) --target widget - @if [ "$(abspath $(DEST))" != "$(abspath $(OUTPUT_DIR))" ]; then \ - mkdir -p $(DEST) && cp $(OUTPUT_DIR)/*.wgt $(DEST); \ - fi - -package-test: build-test - @cmake --build $(OUTPUT_DIR) $(PACKAGE_ARGS) --target test_widget - @if [ "$(abspath $(DEST))" != "$(abspath $(OUTPUT_DIR))" ]; then \ - mkdir -p $(DEST) && cp $(OUTPUT_DIR)/*.wgt $(DEST); \ - fi - -package: package-release - -package-all: package-release package-test package-coverage package-debug - -update: configure - @cmake --build $(BUILD_DIR) --target autobuild - -install: build - @cmake --build $(BUILD_DIR) $(INSTALL_ARGS) --target install diff --git a/autobuild/linux/autobuild b/autobuild/linux/autobuild deleted file mode 100755 index 16181b8..0000000 --- a/autobuild/linux/autobuild +++ /dev/null @@ -1,128 +0,0 @@ -#!/usr/bin/make -f -# Copyright (C) 2015 - 2018 "IoT.bzh" -# Copyright (C) 2020 Konsulko Group -# Author "Romain Forlot" -# -# 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. - -THISFILE := $(lastword $(MAKEFILE_LIST)) -ROOT_DIR := $(abspath $(dir $(THISFILE))/../..) - -# Build directories -# Note that the debug/test/coverage directories are defined in relation -# to the release directory (BUILD_DIR), this needs to be kept in mind -# if over-riding it and building those widget types, the specific widget -# type variable (e.g. BUILD_DIR_DEBUG) may also need to be specified -# to yield the desired output hierarchy. -BUILD_DIR = $(ROOT_DIR)/build -BUILD_DIR_DEBUG = $(abspath $(BUILD_DIR)/../build-debug) -BUILD_DIR_TEST = $(abspath $(BUILD_DIR)/../build-test) -BUILD_DIR_COVERAGE = $(abspath $(BUILD_DIR)/../build-coverage) - -# Output directory variable for use in pattern rules. -# This is intended for internal use only, hence the explicit override -# definition. -override OUTPUT_DIR = $(BUILD_DIR) - -# Final install directory for widgets -DEST = $(OUTPUT_DIR) - -# Default build type for release/test builds -BUILD_TYPE = RELEASE - -.PHONY: all help update install distclean -.PHONY: clean clean-release clean-debug clean-test clean-coverage clean-all -.PHONY: configure configure-release configure-debug configure-test configure-coverage -.PHONY: build build-release build-debug build-test build-coverage build-all -.PHONY: package package-release package-debug package-test package-coverage package-all - -help: - @echo "List of targets available:" - @echo "" - @echo "- all" - @echo "- help" - @echo "- clean" - @echo "- distclean" - @echo "- configure" - @echo "- build: compilation, link and prepare files for package into a widget" - @echo "- package: output a widget file '*.wgt'" - @echo "- install: install in your $(CMAKE_INSTALL_DIR) directory" - @echo "" - @echo "Usage: ./autobuild/agl/autobuild package DEST=${HOME}/opt" - @echo "Don't use your build dir as DEST as wgt file is generated at this location" - -all: package-all - -# Target specific variable over-rides so static pattern rules can be -# used for the various type-specific targets. - -configure-test build-test package-test clean-test: OUTPUT_DIR = $(BUILD_DIR_TEST) - -configure-coverage build-coverage package-coverage clean-coverage: OUTPUT_DIR = $(BUILD_DIR_COVERAGE) -configure-coverage build-coverage package-coverage: BUILD_TYPE = COVERAGE - -configure-debug build-debug package-debug clean-debug: OUTPUT_DIR = $(BUILD_DIR_DEBUG) -configure-debug build-debug package-debug: BUILD_TYPE = DEBUG - -clean-release clean-test clean-debug clean-coverage: - @if [ -d $(OUTPUT_DIR) ]; then \ - $(MAKE) -C $(OUTPUT_DIR) $(CLEAN_ARGS) clean; \ - else \ - echo Nothing to clean; \ - fi - -clean: clean-release - -clean-all: clean-release clean-test clean-debug clean-coverage - -distclean: clean-all - -configure-release configure-test configure-debug configure-coverage: - @mkdir -p $(OUTPUT_DIR) - @if [ ! -f $(OUTPUT_DIR)/Makefile ]; then \ - (cd $(OUTPUT_DIR) && cmake -S $(ROOT_DIR) -DCMAKE_BUILD_TYPE=$(BUILD_TYPE) $(CONFIGURE_ARGS)); \ - fi - -configure: configure-release - -build-release build-debug build-coverage: build-%: configure-% - @cmake --build $(OUTPUT_DIR) $(BUILD_ARGS) --target all - -# Kept for consistency, empty to avoid building everything for test widget -build-test: configure-test - -build: build-release - -build-all: build-release build-debug build-test build-coverage - -package-release package-debug package-coverage: package-%: build-% - @cmake --build $(OUTPUT_DIR) $(PACKAGE_ARGS) --target widget - @if [ "$(abspath $(DEST))" != "$(abspath $(OUTPUT_DIR))" ]; then \ - mkdir -p $(DEST) && cp $(OUTPUT_DIR)/*.wgt $(DEST); \ - fi - -package-test: build-test - @cmake --build $(OUTPUT_DIR) $(PACKAGE_ARGS) --target test_widget - @if [ "$(abspath $(DEST))" != "$(abspath $(OUTPUT_DIR))" ]; then \ - mkdir -p $(DEST) && cp $(OUTPUT_DIR)/*.wgt $(DEST); \ - fi - -package: package-release - -package-all: package-release package-test package-coverage package-debug - -update: configure - @cmake --build $(BUILD_DIR) --target autobuild - -install: build - @cmake --build $(BUILD_DIR) $(INSTALL_ARGS) --target install diff --git a/binding/CMakeLists.txt b/binding/CMakeLists.txt deleted file mode 100644 index c199ad2..0000000 --- a/binding/CMakeLists.txt +++ /dev/null @@ -1,19 +0,0 @@ -PROJECT_TARGET_ADD(audiomixer-binding) - - add_definitions(-DAFB_BINDING_VERSION=3) - - set(audiomixer_SOURCES - audiomixer-binding.c - audiomixer.c - ) - - add_library(${TARGET_NAME} MODULE ${audiomixer_SOURCES}) - - SET_TARGET_PROPERTIES(${TARGET_NAME} PROPERTIES - PREFIX "libafm-" - LABELS "BINDING" - LINK_FLAGS ${BINDINGS_LINK_FLAG} - OUTPUT_NAME ${TARGET_NAME} - ) - - TARGET_LINK_LIBRARIES(${TARGET_NAME} ${link_libraries}) diff --git a/binding/audiomixer-binding.c b/binding/audiomixer-binding.c deleted file mode 100644 index 2c8981a..0000000 --- a/binding/audiomixer-binding.c +++ /dev/null @@ -1,402 +0,0 @@ -/* - * Copyright © 2019 Collabora Ltd. - * Copyright © 2019 Konsulko Group - * @author George Kiagiadakis - * - * SPDX-License-Identifier: MIT - */ - -#include -#include -#include -#include -#include "audiomixer.h" - -static struct audiomixer *audiomixer; -static afb_event_t controls_changed; -static afb_event_t volume_changed; -static afb_event_t mute_changed; - -static const char *signalcomposer_events[] = { - "event.volume.up", - "event.volume.down", - "event.volume.mute", - NULL, -}; - -static void -audiomixer_controls_changed_deferred(int signum, void *arg) -{ - AFB_DEBUG("controls changed"); - afb_event_push(controls_changed, NULL); -} - -struct value_changed_data -{ - unsigned int change_mask; - struct mixer_control control; -}; - -static void -audiomixer_value_changed_deferred(int signum, void *data) -{ - struct value_changed_data *d = data; - json_object *json; - - AFB_DEBUG("value changed"); - - json = json_object_new_object(); - json_object_object_add(json, "control", - json_object_new_string(d->control.name)); - - if (d->change_mask & MIXER_CONTROL_CHANGE_FLAG_VOLUME) { - json_object_object_add(json, "value", - json_object_new_double(d->control.volume)); - afb_event_push(volume_changed, json); - } else if (d->change_mask & MIXER_CONTROL_CHANGE_FLAG_MUTE) { - json_object_object_add(json, "value", - json_object_new_int(d->control.mute)); - afb_event_push(mute_changed, json); - } - - free(d); -} - -/* called in audiomixer's thread */ -static void -audiomixer_controls_changed(void *data) -{ - afb_api_t api = data; - afb_api_queue_job (api, audiomixer_controls_changed_deferred, NULL, - (void *) 0x1, 0); -} - - -/* called in audiomixer's thread */ -static void -audiomixer_value_changed(void *data, - unsigned int change_mask, - const struct mixer_control *control) -{ - afb_api_t api = data; - struct value_changed_data *d = calloc(1, sizeof(*d)); - - d->change_mask = change_mask; - d->control = *control; - - afb_api_queue_job (api, audiomixer_value_changed_deferred, d, - (void *) 0x1, 0); -} - -static const struct audiomixer_events audiomixer_events = { - .controls_changed = audiomixer_controls_changed, - .value_changed = audiomixer_value_changed, -}; - -static int -cleanup(sd_event_source *s, void *data) -{ - audiomixer_free(audiomixer); - audiomixer = NULL; - return 0; -} - -static int -init(afb_api_t api) -{ - int ret; - - ret = afb_daemon_require_api("signal-composer", 1); - if (ret) { - AFB_WARNING("unable to initialize signal-composer binding"); - } else { - const char **tmp = signalcomposer_events; - json_object *args = json_object_new_object(); - json_object *signals = json_object_new_array(); - - while (*tmp) { - json_object_array_add(signals, json_object_new_string(*tmp++)); - } - json_object_object_add(args, "signal", signals); - if(json_object_array_length(signals)) { - afb_api_call_sync(api, "signal-composer", "subscribe", - args, NULL, NULL, NULL); - } else { - json_object_put(args); - } - } - - sd_event *e = afb_daemon_get_event_loop(); - - controls_changed = afb_api_make_event(api, "controls_changed"); - volume_changed = afb_api_make_event(api, "volume_changed"); - mute_changed = afb_api_make_event(api, "mute_changed"); - - audiomixer = audiomixer_new(); - sd_event_add_exit(e, NULL, cleanup, NULL); - - audiomixer_add_event_listener(audiomixer, &audiomixer_events, api); - - return 0; -} - -static void -list_controls_cb(afb_req_t request) -{ - json_object *ret_json, *nest_json; - const struct mixer_control **ctls; - unsigned int n_controls, i; - - audiomixer_lock(audiomixer); - - if (audiomixer_ensure_controls(audiomixer, 3) < 0) { - AFB_REQ_NOTICE(request, "No mixer controls were exposed " - "in PipeWire after 3 seconds"); - } - - ctls = audiomixer_get_active_controls(audiomixer, &n_controls); - - ret_json = json_object_new_array(); - for (i = 0; i < n_controls; i++) { - nest_json = json_object_new_object(); - json_object_object_add(nest_json, "control", - json_object_new_string(ctls[i]->name)); - json_object_object_add(nest_json, "volume", - json_object_new_double(ctls[i]->volume)); - json_object_object_add(nest_json, "mute", - json_object_new_int(ctls[i]->mute)); - json_object_array_add(ret_json, nest_json); - } - afb_req_success(request, ret_json, NULL); - - audiomixer_unlock(audiomixer); -} - -static void -volume_cb(afb_req_t request) -{ - json_object *ret_json; - const char *control = afb_req_value(request, "control"); - const char *value = afb_req_value(request, "value"); - const struct mixer_control *ctl; - double volume; - - audiomixer_lock(audiomixer); - - if (!control) { - afb_req_fail(request, "failed", - "Invalid arguments: missing 'control'"); - goto unlock; - } - - if (audiomixer_ensure_controls(audiomixer, 3) < 0) { - AFB_REQ_NOTICE(request, "No mixer controls were exposed " - "in PipeWire after 3 seconds"); - } - - ctl = audiomixer_find_control(audiomixer, control); - if (!ctl) { - afb_req_fail(request, "failed", "Could not find control"); - goto unlock; - } - - if(value) { - char *endptr; - volume = strtod(value, &endptr); - if (endptr == value || volume < -0.00001 || volume > 1.00001) { - afb_req_fail(request, "failed", - "Invalid volume value (must be between 0.0 and 1.0)"); - goto unlock; - } - - audiomixer_change_volume(audiomixer, ctl, volume); - } else { - volume = ctl->volume; - } - - ret_json = json_object_new_object(); - json_object_object_add(ret_json, "volume", json_object_new_double(volume)); - afb_req_success(request, ret_json, NULL); - -unlock: - audiomixer_unlock(audiomixer); -} - -static void -mute_cb(afb_req_t request) -{ - json_object *ret_json; - const char *control = afb_req_value(request, "control"); - const char *value = afb_req_value(request, "value"); - const struct mixer_control *ctl; - int mute; - - audiomixer_lock(audiomixer); - - if (!control) { - afb_req_fail(request, "failed", - "Invalid arguments: missing 'control'"); - goto unlock; - } - - if (audiomixer_ensure_controls(audiomixer, 3) < 0) { - AFB_REQ_NOTICE(request, "No mixer controls were exposed " - "in PipeWire after 3 seconds"); - } - - ctl = audiomixer_find_control(audiomixer, control); - if (!ctl) { - afb_req_fail(request, "failed", "Could not find control"); - goto unlock; - } - - if(value) { - char *endptr; - mute = (int) strtol(value, &endptr, 10); - if (endptr == value || mute < 0 || mute > 1) { - afb_req_fail(request, "failed", - "Invalid mute value (must be integer 0 or 1)"); - goto unlock; - } - - audiomixer_change_mute(audiomixer, ctl, mute); - } else { - mute = ctl->mute; - } - - ret_json = json_object_new_object(); - json_object_object_add(ret_json, "mute", json_object_new_int(mute)); - afb_req_success(request, ret_json, NULL); - -unlock: - audiomixer_unlock(audiomixer); -} - -static void -subscribe_cb(afb_req_t request) -{ - const char *eventstr = afb_req_value(request, "event"); - afb_event_t event; - - if (!eventstr) { - afb_req_fail(request, "failed", - "Invalid arguments: missing 'event'"); - return; - } - - if (!strcmp(eventstr, "controls_changed")) - event = controls_changed; - else if (!strcmp(eventstr, "volume_changed")) - event = volume_changed; - else if (!strcmp(eventstr, "mute_changed")) - event = mute_changed; - else { - afb_req_fail(request, "failed", "Invalid event name"); - return; - } - - if (afb_req_subscribe(request, event) != 0) - afb_req_fail(request, "failed", "Failed to subscribe to event"); - else - afb_req_success(request, NULL, "Subscribed"); -} - -static void -unsubscribe_cb(afb_req_t request) -{ - const char *eventstr = afb_req_value(request, "event"); - afb_event_t event; - - if (!eventstr) { - afb_req_fail(request, "failed", - "Invalid arguments: missing 'event'"); - return; - } - - if (!strcmp(eventstr, "controls_changed")) - event = controls_changed; - else if (!strcmp(eventstr, "volume_changed")) - event = volume_changed; - else if (!strcmp(eventstr, "mute_changed")) - event = mute_changed; - else { - afb_req_fail(request, "failed", "Invalid event name"); - return; - } - - if (afb_req_unsubscribe(request, event) != 0) - afb_req_fail(request, "failed", "Failed to unsubscribe from event"); - else - afb_req_success(request, NULL, "Unsubscribed"); -} - -static void -onevent(afb_api_t api, const char *event, struct json_object *object) -{ - const struct mixer_control *ctl; - json_object *tmp = NULL; - const char *uid; - const char *value; - - json_object_object_get_ex(object, "uid", &tmp); - if (tmp == NULL) - return; - - uid = json_object_get_string(tmp); - if (strncmp(uid, "event.volume.", 13)) - return; - - json_object_object_get_ex(object, "value", &tmp); - if (tmp == NULL) - return; - - value = json_object_get_string(tmp); - if (strncmp(value, "true", 4)) - return; - - audiomixer_lock(audiomixer); - - ctl = audiomixer_find_control(audiomixer, "Master Playback"); - if (!ctl) - goto unlock; - - if (!strcmp(uid, "event.volume.mute")) { - audiomixer_change_mute(audiomixer, ctl, !ctl->mute); - } else { - double volume = ctl->volume; - - if (!strcmp(uid, "event.volume.up")) { - volume += 0.05; // up 5% - if (volume > 1.0) - volume = 1.0; // clamp to 100% - } else if (!strcmp(uid, "event.volume.down")) { - volume -= 0.05; // down 5% - if (volume < 0.0) - volume = 0.0; // clamp to 0% - } else { - AFB_WARNING("Unhandled signal-composer uid '%s'", uid); - goto unlock; - } - audiomixer_change_volume(audiomixer, ctl, volume); - } - -unlock: - audiomixer_unlock(audiomixer); -} - -static const afb_verb_t verbs[]= { - { .verb = "list_controls", .callback = list_controls_cb, .info = "List the available controls" }, - { .verb = "volume", .callback = volume_cb, .info = "Get/Set volume" }, - { .verb = "mute", .callback = mute_cb, .info = "Get/Set mute" }, - { .verb = "subscribe", .callback = subscribe_cb, .info = "Subscribe to mixer events" }, - { .verb = "unsubscribe", .callback = unsubscribe_cb, .info = "Unsubscribe from mixer events" }, - { } -}; - -const afb_binding_t afbBindingV3 = { - .api = "audiomixer", - .specification = "AudioMixer API", - .verbs = verbs, - .onevent = onevent, - .init = init, -}; diff --git a/binding/audiomixer.c b/binding/audiomixer.c deleted file mode 100644 index 97ad622..0000000 --- a/binding/audiomixer.c +++ /dev/null @@ -1,463 +0,0 @@ -/* - * Copyright © 2019 Collabora Ltd. - * @author George Kiagiadakis - * - * SPDX-License-Identifier: MIT - */ - -#include "audiomixer.h" -#include -#include - -struct audiomixer -{ - WpCore *core; - GMainLoop *loop; - GMainContext *context; - GThread *thread; - GMutex lock; - GCond cond; - - GPtrArray *mixer_controls; - - gint initialized; - WpObjectManager *om; - WpPlugin *default_nodes_api; - WpPlugin *mixer_api; - - const struct audiomixer_events *events; - void *events_data; -}; - -struct mixer_control_impl -{ - struct mixer_control pub; - guint32 node_id; -}; - -struct action -{ - struct audiomixer *audiomixer; - union { - struct { - guint32 id; - gfloat volume; - } change_volume; - struct { - guint32 id; - gboolean mute; - } change_mute; - }; -}; - -static gboolean -get_mixer_controls (struct audiomixer * self, guint32 node_id, gdouble * vol, gboolean * mute) -{ - g_autoptr (GVariant) v = NULL; - g_signal_emit_by_name (self->mixer_api, "get-volume", node_id, &v); - return v && - g_variant_lookup (v, "volume", "d", vol) && - g_variant_lookup (v, "mute", "b", mute); -} - -static void -add_control (struct audiomixer *self, const char *name, guint32 node_id) -{ - struct mixer_control_impl *mixctl = NULL; - gdouble volume = 1.0; - gboolean mute = FALSE; - - /* get current values */ - if (!get_mixer_controls (self, node_id, &volume, &mute)) { - g_warning ("failed to get object controls when populating controls"); - return; - } - - /* create the control */ - mixctl = g_new0 (struct mixer_control_impl, 1); - snprintf (mixctl->pub.name, sizeof (mixctl->pub.name), "%s", name); - mixctl->pub.volume = volume; - mixctl->pub.mute = mute; - mixctl->node_id = node_id; - g_ptr_array_add (self->mixer_controls, mixctl); - - g_debug ("added control %s", mixctl->pub.name); -} - -static void -volume_changed (struct audiomixer * self, guint32 node_id) -{ - g_autoptr (GMutexLocker) locker = g_mutex_locker_new (&self->lock); - gdouble vol = 1.0; - gboolean mute = FALSE; - - for (guint i = 0; i < self->mixer_controls->len; i++) { - struct mixer_control_impl *ctl; - guint change_mask = 0; - - ctl = g_ptr_array_index (self->mixer_controls, i); - if (ctl->node_id != node_id) - continue; - - if (!get_mixer_controls (self, node_id, &vol, &mute)) { - g_warning ("failed to get object controls when volume changed"); - return; - } - - if ((ctl->pub.volume - 0.01f) > vol || (ctl->pub.volume + 0.01f) < vol) { - ctl->pub.volume = vol; - change_mask |= MIXER_CONTROL_CHANGE_FLAG_VOLUME; - } - if (ctl->pub.mute != mute) { - ctl->pub.mute = mute; - change_mask |= MIXER_CONTROL_CHANGE_FLAG_MUTE; - } - - if (self->events && self->events->value_changed) - self->events->value_changed (self->events_data, change_mask, &ctl->pub); - break; - } -} - -static void -rescan_controls (struct audiomixer * self) -{ - g_autoptr (GMutexLocker) locker = g_mutex_locker_new (&self->lock); - g_autoptr (WpIterator) it = NULL; - g_auto (GValue) val = G_VALUE_INIT; - guint32 id = -1; - - g_debug ("rescan"); - - /* clear previous */ - g_ptr_array_set_size (self->mixer_controls, 0); - - /* add master controls */ - g_signal_emit_by_name (self->default_nodes_api, "get-default-node", - "Audio/Sink", &id); - if (id != (guint32)-1) - add_control (self, "Master Playback", id); - - g_signal_emit_by_name (self->default_nodes_api, "get-default-node", - "Audio/Source", &id); - if (id != (guint32)-1) - add_control (self, "Master Capture", id); - - /* add endpoints */ - it = wp_object_manager_new_iterator (self->om); - for (; wp_iterator_next (it, &val); g_value_unset (&val)) { - WpPipewireObject *ep = g_value_get_object (&val); - const gchar *name = wp_pipewire_object_get_property (ep, "endpoint.description"); - const gchar *node = wp_pipewire_object_get_property (ep, "node.id"); - id = node ? atoi(node) : 0; - if (name && id != 0 && id != (guint32)-1) - add_control (self, name, id); - } - - /* notify subscribers */ - if (self->events && self->events->controls_changed) - self->events->controls_changed (self->events_data); - g_cond_broadcast (&self->cond); -} - -static void -on_default_nodes_activated (WpObject * p, GAsyncResult * res, struct audiomixer * self) -{ - g_autoptr (GError) error = NULL; - if (!wp_object_activate_finish (p, res, &error)) { - g_warning ("%s", error->message); - } - - if (wp_object_get_active_features (WP_OBJECT (self->mixer_api)) - & WP_PLUGIN_FEATURE_ENABLED) - wp_core_install_object_manager (self->core, self->om); - - g_signal_connect_swapped (self->default_nodes_api, "changed", - (GCallback) rescan_controls, self); -} - -static void -on_mixer_activated (WpObject * p, GAsyncResult * res, struct audiomixer * self) -{ - g_autoptr (GError) error = NULL; - if (!wp_object_activate_finish (p, res, &error)) { - g_warning ("%s", error->message); - } - - if (wp_object_get_active_features (WP_OBJECT (self->default_nodes_api)) - & WP_PLUGIN_FEATURE_ENABLED) - wp_core_install_object_manager (self->core, self->om); - - g_signal_connect_swapped (self->mixer_api, "changed", - (GCallback) volume_changed, self); -} - -static void -on_core_connected (struct audiomixer * self) -{ - self->om = wp_object_manager_new (); - wp_object_manager_add_interest (self->om, WP_TYPE_ENDPOINT, - WP_CONSTRAINT_TYPE_PW_GLOBAL_PROPERTY, - PW_KEY_MEDIA_CLASS, "#s", "Audio/*", NULL); - wp_object_manager_request_object_features (self->om, - WP_TYPE_ENDPOINT, WP_OBJECT_FEATURES_ALL); - g_signal_connect_swapped (self->om, "objects-changed", - (GCallback) rescan_controls, self); - - wp_object_activate (WP_OBJECT (self->default_nodes_api), - WP_PLUGIN_FEATURE_ENABLED, NULL, - (GAsyncReadyCallback) on_default_nodes_activated, self); - - wp_object_activate (WP_OBJECT (self->mixer_api), - WP_PLUGIN_FEATURE_ENABLED, NULL, - (GAsyncReadyCallback) on_mixer_activated, self); -} - -static void -on_core_disconnected (struct audiomixer * self) -{ - g_ptr_array_set_size (self->mixer_controls, 0); - g_clear_object (&self->om); - g_signal_handlers_disconnect_by_data (self->default_nodes_api, self); - g_signal_handlers_disconnect_by_data (self->mixer_api, self); - wp_object_deactivate (WP_OBJECT (self->default_nodes_api), WP_PLUGIN_FEATURE_ENABLED); - wp_object_deactivate (WP_OBJECT (self->mixer_api), WP_PLUGIN_FEATURE_ENABLED); -} - -static void -audiomixer_init_in_thread (struct audiomixer * self) -{ - g_autoptr (GMutexLocker) locker = g_mutex_locker_new (&self->lock); - g_autoptr (GError) error = NULL; - - self->context = g_main_context_new (); - g_main_context_push_thread_default (self->context); - - self->loop = g_main_loop_new (self->context, FALSE); - self->core = wp_core_new (self->context, NULL); - - /* load required API modules */ - if (!wp_core_load_component (self->core, - "libwireplumber-module-default-nodes-api", "module", NULL, &error)) { - g_warning ("%s", error->message); - self->initialized = -1; - goto out; - } - if (!wp_core_load_component (self->core, - "libwireplumber-module-mixer-api", "module", NULL, &error)) { - g_warning ("%s", error->message); - self->initialized = -1; - goto out; - } - - self->default_nodes_api = wp_plugin_find (self->core, "default-nodes-api"); - self->mixer_api = wp_plugin_find (self->core, "mixer-api"); - g_object_set (G_OBJECT (self->mixer_api), "scale", 1 /* cubic */, NULL); - - g_signal_connect_swapped (self->core, "connected", - G_CALLBACK (on_core_connected), self); - g_signal_connect_swapped (self->core, "disconnected", - G_CALLBACK (on_core_disconnected), self); - - self->initialized = 1; - -out: - g_cond_broadcast (&self->cond); -} - -static void * -audiomixer_thread (struct audiomixer * self) -{ - audiomixer_init_in_thread (self); - - /* main loop for the thread; quits only when audiomixer_free() is called */ - g_main_loop_run (self->loop); - - wp_core_disconnect (self->core); - g_clear_object (&self->default_nodes_api); - g_clear_object (&self->mixer_api); - g_object_unref (self->core); - - g_main_context_pop_thread_default (self->context); - g_main_loop_unref (self->loop); - g_main_context_unref (self->context); - - return NULL; -} - -struct audiomixer * -audiomixer_new (void) -{ - struct audiomixer *self = calloc(1, sizeof(struct audiomixer)); - - wp_init (WP_INIT_ALL); - - g_mutex_init (&self->lock); - g_cond_init (&self->cond); - self->mixer_controls = g_ptr_array_new_with_free_func (g_free); - - g_mutex_lock (&self->lock); - self->initialized = 0; - self->thread = g_thread_new ("audiomixer", (GThreadFunc) audiomixer_thread, - self); - while (self->initialized == 0) - g_cond_wait (&self->cond, &self->lock); - g_mutex_unlock (&self->lock); - - return self; -} - -void -audiomixer_free(struct audiomixer *self) -{ - g_main_loop_quit (self->loop); - g_thread_join (self->thread); - - g_ptr_array_unref (self->mixer_controls); - g_cond_clear (&self->cond); - g_mutex_clear (&self->lock); - - free (self); -} - -void -audiomixer_lock(struct audiomixer *self) -{ - g_mutex_lock (&self->lock); -} - -void -audiomixer_unlock(struct audiomixer *self) -{ - g_mutex_unlock (&self->lock); -} - -static gboolean -do_connect (WpCore * core) -{ - if (!wp_core_connect (core)) - g_warning ("Failed to connect to PipeWire"); - return G_SOURCE_REMOVE; -} - -int -audiomixer_ensure_controls(struct audiomixer *self, int timeout_sec) -{ - gint64 end_time = g_get_monotonic_time () + timeout_sec * G_TIME_SPAN_SECOND; - - g_return_val_if_fail (self->initialized == 1, -EIO); - - if (!wp_core_is_connected (self->core)) - g_main_context_invoke (self->context, (GSourceFunc) do_connect, self->core); - - while (self->mixer_controls->len == 0) { - if (!g_cond_wait_until (&self->cond, &self->lock, end_time)) - return -ETIMEDOUT; - } - return 0; -} - -const struct mixer_control ** -audiomixer_get_active_controls(struct audiomixer *self, - unsigned int *n_controls) -{ - *n_controls = self->mixer_controls->len; - return (const struct mixer_control **) self->mixer_controls->pdata; -} - -const struct mixer_control * -audiomixer_find_control(struct audiomixer *self, const char *name) -{ - struct mixer_control *ctl; - - for (guint i = 0; i < self->mixer_controls->len; i++) { - ctl = g_ptr_array_index (self->mixer_controls, i); - if (!strcmp(ctl->name, name)) { - return ctl; - } - } - return NULL; -} - -void -audiomixer_add_event_listener(struct audiomixer *self, - const struct audiomixer_events *events, - void *data) -{ - self->events = events; - self->events_data = data; -} - -static gboolean -do_change_volume (struct action * action) -{ - struct audiomixer *self = action->audiomixer; - GVariantBuilder b = G_VARIANT_BUILDER_INIT (G_VARIANT_TYPE_VARDICT); - gboolean ret = FALSE; - - g_variant_builder_add (&b, "{sv}", "volume", - g_variant_new_double (action->change_volume.volume)); - g_signal_emit_by_name (self->mixer_api, "set-volume", - action->change_volume.id, g_variant_builder_end (&b), &ret); - if (!ret) - g_warning ("mixer api set-volume failed"); - - return G_SOURCE_REMOVE; -} - -void -audiomixer_change_volume(struct audiomixer *self, - const struct mixer_control *control, - double volume) -{ - const struct mixer_control_impl *impl = - (const struct mixer_control_impl *) control; - struct action * action; - - g_return_if_fail (self->initialized == 1); - - /* schedule the action to run on the audiomixer thread */ - action = g_new0 (struct action, 1); - action->audiomixer = self; - action->change_volume.id = impl->node_id; - action->change_volume.volume = (gfloat) volume; - wp_core_idle_add (self->core, NULL, (GSourceFunc) do_change_volume, action, - g_free); -} - -static gboolean -do_change_mute (struct action * action) -{ - struct audiomixer *self = action->audiomixer; - GVariantBuilder b = G_VARIANT_BUILDER_INIT (G_VARIANT_TYPE_VARDICT); - gboolean ret = FALSE; - - g_variant_builder_add (&b, "{sv}", "mute", - g_variant_new_double (action->change_mute.mute)); - g_signal_emit_by_name (self->mixer_api, "set-volume", - action->change_mute.id, g_variant_builder_end (&b), &ret); - if (!ret) - g_warning ("mixer api set-volume failed"); - - return G_SOURCE_REMOVE; -} - -void -audiomixer_change_mute(struct audiomixer *self, - const struct mixer_control *control, - bool mute) -{ - const struct mixer_control_impl *impl = - (const struct mixer_control_impl *) control; - struct action * action; - - g_return_if_fail (self->initialized == 1); - - /* schedule the action to run on the audiomixer thread */ - action = g_new0 (struct action, 1); - action->audiomixer = self; - action->change_mute.id = impl->node_id; - action->change_mute.mute = mute; - wp_core_idle_add (self->core, NULL, (GSourceFunc) do_change_mute, action, - g_free); -} diff --git a/binding/audiomixer.h b/binding/audiomixer.h deleted file mode 100644 index 47bd703..0000000 --- a/binding/audiomixer.h +++ /dev/null @@ -1,59 +0,0 @@ -/* - * Copyright © 2019 Collabora Ltd. - * @author George Kiagiadakis - * - * SPDX-License-Identifier: MIT - */ - -#include - -struct audiomixer; - -struct mixer_control -{ - char name[32]; - double volume; - bool mute; -}; - -struct audiomixer_events -{ - void (*controls_changed) (void *data); - - void (*value_changed) (void *data, -#define MIXER_CONTROL_CHANGE_FLAG_VOLUME (1<<0) -#define MIXER_CONTROL_CHANGE_FLAG_MUTE (1<<1) - unsigned int change_mask, - const struct mixer_control *control); -}; - -struct audiomixer * audiomixer_new(void); -void audiomixer_free(struct audiomixer *self); - -/* locking is required to call any of the methods below - * and to access any structure maintained by audiomixer */ -void audiomixer_lock(struct audiomixer *self); -void audiomixer_unlock(struct audiomixer *self); - -int audiomixer_ensure_controls(struct audiomixer *self, int timeout_sec); - -const struct mixer_control ** audiomixer_get_active_controls( - struct audiomixer *self, - unsigned int *n_controls); - -const struct mixer_control * audiomixer_find_control( - struct audiomixer *self, - const char *name); - -void audiomixer_add_event_listener(struct audiomixer *self, - const struct audiomixer_events *events, - void *data); - -void audiomixer_change_volume(struct audiomixer *self, - const struct mixer_control *control, - double volume); - -void audiomixer_change_mute(struct audiomixer *self, - const struct mixer_control *control, - bool mute); - diff --git a/conf.d/cmake/config.cmake b/conf.d/cmake/config.cmake deleted file mode 100644 index 09d91ae..0000000 --- a/conf.d/cmake/config.cmake +++ /dev/null @@ -1,160 +0,0 @@ -########################################################################### -# Copyright 2015, 2016, 2017 IoT.bzh -# -# author: Fulup Ar Foll -# -# 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. -########################################################################### - -# Project Info -# ------------------ -set(PROJECT_NAME agl-service-audiomixer) -set(PROJECT_PRETTY_NAME "Audio mixer binding service") -set(PROJECT_DESCRIPTION "Expose PipeWire mixer controls through the AGL Framework") -set(PROJECT_VERSION "0.1") -set(PROJECT_URL https://gerrit.automotivelinux.org/gerrit/admin/repos/apps/agl-service-audiomixer) -set(PROJECT_ICON "icon.png") -set(PROJECT_AUTHOR "George Kiagiadakis") -set(PROJECT_AUTHOR_MAIL "george.kiagiadakis@collabora.com") -set(PROJECT_LICENSE "MIT") -set(PROJECT_LANGUAGES,"C") -set(API_NAME "audiomixer") - -# Where are stored the project configuration files -# relative to the root project directory -set(PROJECT_CMAKE_CONF_DIR "conf.d") - -# Where are stored your external libraries for your project. This is 3rd party library that you don't maintain -# but used and must be built and linked. -# set(PROJECT_LIBDIR "libs") - -# Where are stored data for your application. Pictures, static resources must be placed in that folder. -# set(PROJECT_RESOURCES "data") - -# Which directories inspect to find CMakeLists.txt target files -# set(PROJECT_SRC_DIR_PATTERN "*") - -# Compilation Mode (DEBUG, RELEASE) -# ---------------------------------- -set(BUILD_TYPE "RELEASE") - -# Kernel selection if needed. You can choose between a -# mandatory version to impose a minimal version. -# Or check Kernel minimal version and just print a Warning -# about missing features and define a preprocessor variable -# to be used as preprocessor condition in code to disable -# incompatibles features. Preprocessor define is named -# KERNEL_MINIMAL_VERSION_OK. -# -# NOTE*** FOR NOW IT CHECKS KERNEL Yocto environment and -# Yocto SDK Kernel version. -# ----------------------------------------------- -#set(kernel_mandatory_version 4.8) - -# Compiler selection if needed. Impose a minimal version. -# ----------------------------------------------- -set (gcc_minimal_version 4.9) - -# PKG_CONFIG required packages -# ----------------------------- -set (PKG_REQUIRED_LIST - json-c - libsystemd>=222 - afb-daemon - libpipewire-0.3 - wireplumber-0.4 -) - -# Static constante definition -# ----------------------------- -add_compile_options($<$:-pthread>) - -# Customize link option -# ----------------------------- -list (APPEND link_libraries -pthread) - -# --------------------------------------------------------------------- -set(INSTALL_PREFIX $ENV{HOME}/opt) - -# Optional location for config.xml.in -# ----------------------------------- -set(WIDGET_CONFIG_TEMPLATE ${CMAKE_CURRENT_SOURCE_DIR}/conf.d/wgt/config.xml.in) - -# Mandatory widget Mimetype specification of the main unit -# -------------------------------------------------------------------------- -# Choose between : -#- text/html : HTML application, -# content.src designates the home page of the application -# -#- application/vnd.agl.native : AGL compatible native, -# content.src designates the relative path of the binary. -# -# - application/vnd.agl.service: AGL service, content.src is not used. -# -#- ***application/x-executable***: Native application, -# content.src designates the relative path of the binary. -# For such application, only security setup is made. -# -set(WIDGET_TYPE application/vnd.agl.service) - -# Mandatory Widget entry point file of the main unit -# -------------------------------------------------------------- -# This is the file that will be executed, loaded, -# at launch time by the application framework. -# -set(WIDGET_ENTRY_POINT lib/libafm-audiomixer-binding.so) - -# Print a helper message when every thing is finished -# ---------------------------------------------------- -set(CLOSING_MESSAGE "Test with: afb-daemon --rootdir=\$\$(pwd)/package --binding=\$\$(pwd)/package/${WIDGET_ENTRY_POINT} --port=1234 --tracereq=common --token=\"1\" --verbose") -set(PACKAGE_MESSAGE "Install widget file using in the target : afm-util install ${PROJECT_NAME}.wgt") - - - -# Optional dependencies order -# --------------------------- -#set(EXTRA_DEPENDENCIES_ORDER) - -# Optional Extra global include path -# ----------------------------------- -#set(EXTRA_INCLUDE_DIRS) - -# Optional extra libraries -# ------------------------- -#set(EXTRA_LINK_LIBRARIES) - -# Optional force binding installation -# ------------------------------------ -# set(BINDINGS_INSTALL_PREFIX PrefixPath ) - -# Optional force binding Linking flag -# ------------------------------------ -# set(BINDINGS_LINK_FLAG LinkOptions ) - -# Optional force package prefix generation, like widget -# ----------------------------------------------------- -# set(PKG_PREFIX DestinationPath) - -# Optional Application Framework security token -# and port use for remote debugging. -#------------------------------------------------------------ -#set(AFB_TOKEN "" CACHE PATH "Default AFB_TOKEN") -#set(AFB_REMPORT "1234" CACHE PATH "Default AFB_TOKEN") - -# This include is mandatory and MUST happens at the end -# of this file, else you expose you to unexpected behavior -# -# This CMake module could be found at the following url: -# https://gerrit.automotivelinux.org/gerrit/#/admin/projects/src/cmake-apps-module -# ----------------------------------------------------------- -include(CMakeAfbTemplates) diff --git a/conf.d/wgt/config.xml.in b/conf.d/wgt/config.xml.in deleted file mode 100644 index 056c64b..0000000 --- a/conf.d/wgt/config.xml.in +++ /dev/null @@ -1,27 +0,0 @@ - - - @PROJECT_NAME@ - - - @PROJECT_DESCRIPTION@ - @PROJECT_AUTHOR@ <@PROJECT_AUTHOR_MAIL@> - @PROJECT_LICENSE@ - - - - - - - - - - - - - - - - - - - diff --git a/meson.build b/meson.build new file mode 100644 index 0000000..47e88a4 --- /dev/null +++ b/meson.build @@ -0,0 +1,10 @@ +project('agl-service-audiomixer', + ['cpp', 'c'], + license : 'Apache-2.0', + default_options : ['c_std=c17', 'cpp_std=c++17']) + +systemd_dep = dependency('systemd') + +subdir('src') +subdir('systemd') + diff --git a/src/audiomixer-service.cpp b/src/audiomixer-service.cpp new file mode 100644 index 0000000..5787153 --- /dev/null +++ b/src/audiomixer-service.cpp @@ -0,0 +1,129 @@ +// SPDX-License-Identifier: Apache-2.0 + +#include "audiomixer-service.hpp" +#include +#include + + +AudiomixerService::AudiomixerService(const VisConfig &config, net::io_context& ioc, ssl::context& ctx) : + VisSession(config, ioc, ctx) +{ + m_audiomixer = audiomixer_new(); + if (m_audiomixer) { + // Set up callbacks for WirePlumber events + m_audiomixer_events.controls_changed = audiomixer_control_change_cb; + m_audiomixer_events.value_changed = audiomixer_value_change_cb; + audiomixer_add_event_listener(m_audiomixer, &m_audiomixer_events, this); + + // Drive connecting to PipeWire core and refreshing controls list + audiomixer_lock(m_audiomixer); + audiomixer_ensure_controls(m_audiomixer, 3); + audiomixer_unlock(m_audiomixer); + } else { + std::cerr << "Could not create WirePlumber connection" << std::endl; + } +} + +AudiomixerService::~AudiomixerService() +{ + audiomixer_free(m_audiomixer); +} + +void AudiomixerService::handle_authorized_response(void) +{ + subscribe("Vehicle.Cabin.Infotainment.Media.Volume"); + subscribe("Vehicle.Cabin.SteeringWheel.Switches.VolumeUp"); + subscribe("Vehicle.Cabin.SteeringWheel.Switches.VolumeDown"); + subscribe("Vehicle.Cabin.SteeringWheel.Switches.VolumeMute"); + + // Set initial volume in VSS + // For now a value of 50 matches the default in the homescreen app. + // Ideally there would be some form of persistence scheme to restore + // the last value on restart. + set("Vehicle.Cabin.Infotainment.Media.Volume", "50"); +} + +void AudiomixerService::handle_get_response(std::string &path, std::string &value, std::string ×tamp) +{ + // Placeholder since no gets are performed ATM +} + +void AudiomixerService::handle_notification(std::string &path, std::string &value, std::string ×tamp) +{ + if (!m_audiomixer) { + return; + } + + audiomixer_lock(m_audiomixer); + + const struct mixer_control *ctl = audiomixer_find_control(m_audiomixer, "Master Playback"); + if (!ctl) { + audiomixer_unlock(m_audiomixer); + return; + } + + if (path == "Vehicle.Cabin.Infotainment.Media.Volume") { + try { + int volume = std::stoi(value); + if (volume >= 0 && volume <= 100) { + double v = (double) volume / 100.0; + if (m_config.verbose() > 1) + std::cout << "Setting volume to " << v << std::endl; + audiomixer_change_volume(m_audiomixer, ctl, v); + } + } + catch (std::exception ex) { + // ignore bad value + } + } else if (path == "Vehicle.Cabin.SteeringWheel.Switches.VolumeUp" && value == "true") { + double volume = ctl->volume; + volume += 0.05; // up 5% + if (volume > 1.0) + volume = 1.0; // clamp to 100% + if (m_config.verbose() > 1) + std::cout << "Increasing volume to " << volume << std::endl; + audiomixer_change_volume(m_audiomixer, ctl, volume); + + } else if (path == "Vehicle.Cabin.SteeringWheel.Switches.VolumeDown" && value == "true") { + double volume = ctl->volume; + volume -= 0.05; // down 5% + if (volume < 0.0) + volume = 0.0; // clamp to 0% + if (m_config.verbose() > 1) + std::cout << "Decreasing volume to " << volume << std::endl; + audiomixer_change_volume(m_audiomixer, ctl, volume); + + } else if (path == "Vehicle.Cabin.SteeringWheel.Switches.VolumeMute" && value == "true") { + if (m_config.verbose() > 1) { + if (ctl->mute) + std::cout << "Unmuting" << std::endl; + else + std::cout << "Muting" << std::endl; + } + audiomixer_change_mute(m_audiomixer, ctl, !ctl->mute); + } + // else ignore + + audiomixer_unlock(m_audiomixer); +} + +void AudiomixerService::handle_control_change(void) +{ + // Ignore for now +} + +void AudiomixerService::handle_value_change(unsigned int change_mask, const struct mixer_control *control) +{ + if (!control) + return; + + if (change_mask & MIXER_CONTROL_CHANGE_FLAG_VOLUME) { + if (std::string(control->name) == "Master Playback") { + // Push change into VIS + std::string value = std::to_string((int) (control->volume * 100.0)); + set("Vehicle.Cabin.Infotainment.Media.Volume", value); + } + } else if (change_mask & MIXER_CONTROL_CHANGE_FLAG_MUTE) { + // For now, do nothing, new state is in control->mute + } +} diff --git a/src/audiomixer-service.hpp b/src/audiomixer-service.hpp new file mode 100644 index 0000000..cb00584 --- /dev/null +++ b/src/audiomixer-service.hpp @@ -0,0 +1,44 @@ +// SPDX-License-Identifier: Apache-2.0 + +#ifndef _AUDIOMIXER_SERVICE_HPP +#define _AUDIOMIXER_SERVICE_HPP + +#include "vis-session.hpp" +#include "audiomixer.h" + +class AudiomixerService : public VisSession +{ + struct audiomixer *m_audiomixer; + +public: + AudiomixerService(const VisConfig &config, net::io_context& ioc, ssl::context& ctx); + + ~AudiomixerService(); + + static void audiomixer_control_change_cb(void *data) { + if (data) + ((AudiomixerService*) data)->handle_control_change(); + }; + + static void audiomixer_value_change_cb(void *data, + unsigned int change_mask, + const struct mixer_control *control) { + if (data) + ((AudiomixerService*) data)->handle_value_change(change_mask, control); + } + +protected: + struct audiomixer_events m_audiomixer_events; + + virtual void handle_authorized_response(void) override; + + virtual void handle_get_response(std::string &path, std::string &value, std::string ×tamp) override; + + virtual void handle_notification(std::string &path, std::string &value, std::string ×tamp) override; + + virtual void handle_control_change(void); + + virtual void handle_value_change(unsigned int change_mask, const struct mixer_control *control); +}; + +#endif // _AUDIOMIXER_SERVICE_HPP diff --git a/src/audiomixer.c b/src/audiomixer.c new file mode 100644 index 0000000..a40a38e --- /dev/null +++ b/src/audiomixer.c @@ -0,0 +1,463 @@ +/* + * Copyright © 2019 Collabora Ltd. + * @author George Kiagiadakis + * + * SPDX-License-Identifier: MIT + */ + +#include "audiomixer.h" +#include +#include + +struct audiomixer +{ + WpCore *core; + GMainLoop *loop; + GMainContext *context; + GThread *thread; + GMutex lock; + GCond cond; + + GPtrArray *mixer_controls; + + gint initialized; + WpObjectManager *om; + WpPlugin *default_nodes_api; + WpPlugin *mixer_api; + + const struct audiomixer_events *events; + void *events_data; +}; + +struct mixer_control_impl +{ + struct mixer_control pub; + guint32 node_id; +}; + +struct action +{ + struct audiomixer *audiomixer; + union { + struct { + guint32 id; + gfloat volume; + } change_volume; + struct { + guint32 id; + gboolean mute; + } change_mute; + }; +}; + +static gboolean +get_mixer_controls (struct audiomixer * self, guint32 node_id, gdouble * vol, gboolean * mute) +{ + g_autoptr (GVariant) v = NULL; + g_signal_emit_by_name (self->mixer_api, "get-volume", node_id, &v); + return v && + g_variant_lookup (v, "volume", "d", vol) && + g_variant_lookup (v, "mute", "b", mute); +} + +static void +add_control (struct audiomixer *self, const char *name, guint32 node_id) +{ + struct mixer_control_impl *mixctl = NULL; + gdouble volume = 1.0; + gboolean mute = FALSE; + + /* get current values */ + if (!get_mixer_controls (self, node_id, &volume, &mute)) { + g_warning ("failed to get object controls when populating controls"); + return; + } + + /* create the control */ + mixctl = g_new0 (struct mixer_control_impl, 1); + snprintf (mixctl->pub.name, sizeof (mixctl->pub.name), "%s", name); + mixctl->pub.volume = volume; + mixctl->pub.mute = mute; + mixctl->node_id = node_id; + g_ptr_array_add (self->mixer_controls, mixctl); + + g_debug ("added control %s", mixctl->pub.name); +} + +static void +volume_changed (struct audiomixer * self, guint32 node_id) +{ + g_autoptr (GMutexLocker) locker = g_mutex_locker_new (&self->lock); + gdouble vol = 1.0; + gboolean mute = FALSE; + + for (guint i = 0; i < self->mixer_controls->len; i++) { + struct mixer_control_impl *ctl; + guint change_mask = 0; + + ctl = g_ptr_array_index (self->mixer_controls, i); + if (ctl->node_id != node_id) + continue; + + if (!get_mixer_controls (self, node_id, &vol, &mute)) { + g_warning ("failed to get object controls when volume changed"); + return; + } + + if ((ctl->pub.volume - 0.01f) > vol || (ctl->pub.volume + 0.01f) < vol) { + ctl->pub.volume = vol; + change_mask |= MIXER_CONTROL_CHANGE_FLAG_VOLUME; + } + if (ctl->pub.mute != mute) { + ctl->pub.mute = mute; + change_mask |= MIXER_CONTROL_CHANGE_FLAG_MUTE; + } + + if (self->events && self->events->value_changed) + self->events->value_changed (self->events_data, change_mask, &ctl->pub); + break; + } +} + +static void +rescan_controls (struct audiomixer * self) +{ + g_autoptr (GMutexLocker) locker = g_mutex_locker_new (&self->lock); + g_autoptr (WpIterator) it = NULL; + g_auto (GValue) val = G_VALUE_INIT; + guint32 id = -1; + + g_debug ("rescan"); + + /* clear previous */ + g_ptr_array_set_size (self->mixer_controls, 0); + + /* add master controls */ + g_signal_emit_by_name (self->default_nodes_api, "get-default-node", + "Audio/Sink", &id); + if (id != (guint32)-1) + add_control (self, "Master Playback", id); + + g_signal_emit_by_name (self->default_nodes_api, "get-default-node", + "Audio/Source", &id); + if (id != (guint32)-1) + add_control (self, "Master Capture", id); + + /* add endpoints */ + it = wp_object_manager_new_iterator (self->om); + for (; wp_iterator_next (it, &val); g_value_unset (&val)) { + WpPipewireObject *ep = g_value_get_object (&val); + const gchar *name = wp_pipewire_object_get_property (ep, "endpoint.description"); + const gchar *node = wp_pipewire_object_get_property (ep, "node.id"); + id = node ? atoi(node) : 0; + if (name && id != 0 && id != (guint32)-1) + add_control (self, name, id); + } + + /* notify subscribers */ + if (self->events && self->events->controls_changed) + self->events->controls_changed (self->events_data); + g_cond_broadcast (&self->cond); +} + +static void +on_default_nodes_activated (WpObject * p, GAsyncResult * res, struct audiomixer * self) +{ + g_autoptr (GError) error = NULL; + if (!wp_object_activate_finish (p, res, &error)) { + g_warning ("%s", error->message); + } + + if (wp_object_get_active_features (WP_OBJECT (self->mixer_api)) + & WP_PLUGIN_FEATURE_ENABLED) + wp_core_install_object_manager (self->core, self->om); + + g_signal_connect_swapped (self->default_nodes_api, "changed", + (GCallback) rescan_controls, self); +} + +static void +on_mixer_activated (WpObject * p, GAsyncResult * res, struct audiomixer * self) +{ + g_autoptr (GError) error = NULL; + if (!wp_object_activate_finish (p, res, &error)) { + g_warning ("%s", error->message); + } + + if (wp_object_get_active_features (WP_OBJECT (self->default_nodes_api)) + & WP_PLUGIN_FEATURE_ENABLED) + wp_core_install_object_manager (self->core, self->om); + + g_signal_connect_swapped (self->mixer_api, "changed", + (GCallback) volume_changed, self); +} + +static void +on_core_connected (struct audiomixer * self) +{ + self->om = wp_object_manager_new (); + wp_object_manager_add_interest (self->om, WP_TYPE_ENDPOINT, + WP_CONSTRAINT_TYPE_PW_GLOBAL_PROPERTY, + PW_KEY_MEDIA_CLASS, "#s", "Audio/*", NULL); + wp_object_manager_request_object_features (self->om, + WP_TYPE_ENDPOINT, WP_OBJECT_FEATURES_ALL); + g_signal_connect_swapped (self->om, "objects-changed", + (GCallback) rescan_controls, self); + + wp_object_activate (WP_OBJECT (self->default_nodes_api), + WP_PLUGIN_FEATURE_ENABLED, NULL, + (GAsyncReadyCallback) on_default_nodes_activated, self); + + wp_object_activate (WP_OBJECT (self->mixer_api), + WP_PLUGIN_FEATURE_ENABLED, NULL, + (GAsyncReadyCallback) on_mixer_activated, self); +} + +static void +on_core_disconnected (struct audiomixer * self) +{ + g_ptr_array_set_size (self->mixer_controls, 0); + g_clear_object (&self->om); + g_signal_handlers_disconnect_by_data (self->default_nodes_api, self); + g_signal_handlers_disconnect_by_data (self->mixer_api, self); + wp_object_deactivate (WP_OBJECT (self->default_nodes_api), WP_PLUGIN_FEATURE_ENABLED); + wp_object_deactivate (WP_OBJECT (self->mixer_api), WP_PLUGIN_FEATURE_ENABLED); +} + +static void +audiomixer_init_in_thread (struct audiomixer * self) +{ + g_autoptr (GMutexLocker) locker = g_mutex_locker_new (&self->lock); + g_autoptr (GError) error = NULL; + + self->context = g_main_context_new (); + g_main_context_push_thread_default (self->context); + + self->loop = g_main_loop_new (self->context, FALSE); + self->core = wp_core_new (self->context, NULL); + + /* load required API modules */ + if (!wp_core_load_component (self->core, + "libwireplumber-module-default-nodes-api", "module", NULL, &error)) { + g_warning ("%s", error->message); + self->initialized = -1; + goto out; + } + if (!wp_core_load_component (self->core, + "libwireplumber-module-mixer-api", "module", NULL, &error)) { + g_warning ("%s", error->message); + self->initialized = -1; + goto out; + } + + self->default_nodes_api = wp_plugin_find (self->core, "default-nodes-api"); + self->mixer_api = wp_plugin_find (self->core, "mixer-api"); + g_object_set (G_OBJECT (self->mixer_api), "scale", 1 /* cubic */, NULL); + + g_signal_connect_swapped (self->core, "connected", + G_CALLBACK (on_core_connected), self); + g_signal_connect_swapped (self->core, "disconnected", + G_CALLBACK (on_core_disconnected), self); + + self->initialized = 1; + +out: + g_cond_broadcast (&self->cond); +} + +static void * +audiomixer_thread (struct audiomixer * self) +{ + audiomixer_init_in_thread (self); + + /* main loop for the thread; quits only when audiomixer_free() is called */ + g_main_loop_run (self->loop); + + wp_core_disconnect (self->core); + g_clear_object (&self->default_nodes_api); + g_clear_object (&self->mixer_api); + g_object_unref (self->core); + + g_main_context_pop_thread_default (self->context); + g_main_loop_unref (self->loop); + g_main_context_unref (self->context); + + return NULL; +} + +struct audiomixer * +audiomixer_new (void) +{ + struct audiomixer *self = calloc(1, sizeof(struct audiomixer)); + + wp_init (WP_INIT_ALL); + + g_mutex_init (&self->lock); + g_cond_init (&self->cond); + self->mixer_controls = g_ptr_array_new_with_free_func (g_free); + + g_mutex_lock (&self->lock); + self->initialized = 0; + self->thread = g_thread_new ("audiomixer", (GThreadFunc) audiomixer_thread, + self); + while (self->initialized == 0) + g_cond_wait (&self->cond, &self->lock); + g_mutex_unlock (&self->lock); + + return self; +} + +void +audiomixer_free(struct audiomixer *self) +{ + g_main_loop_quit (self->loop); + g_thread_join (self->thread); + + g_ptr_array_unref (self->mixer_controls); + g_cond_clear (&self->cond); + g_mutex_clear (&self->lock); + + free (self); +} + +void +audiomixer_lock(struct audiomixer *self) +{ + g_mutex_lock (&self->lock); +} + +void +audiomixer_unlock(struct audiomixer *self) +{ + g_mutex_unlock (&self->lock); +} + +static gboolean +do_connect (WpCore * core) +{ + if (!wp_core_connect (core)) + g_warning ("Failed to connect to PipeWire"); + return G_SOURCE_REMOVE; +} + +int +audiomixer_ensure_controls(struct audiomixer *self, int timeout_sec) +{ + gint64 end_time = g_get_monotonic_time () + timeout_sec * G_TIME_SPAN_SECOND; + + g_return_val_if_fail (self->initialized == 1, -EIO); + + if (!wp_core_is_connected (self->core)) + g_main_context_invoke (self->context, (GSourceFunc) do_connect, self->core); + + while (self->mixer_controls->len == 0) { + if (!g_cond_wait_until (&self->cond, &self->lock, end_time)) + return -ETIMEDOUT; + } + return 0; +} + +const struct mixer_control ** +audiomixer_get_active_controls(struct audiomixer *self, + unsigned int *n_controls) +{ + *n_controls = self->mixer_controls->len; + return (const struct mixer_control **) self->mixer_controls->pdata; +} + +const struct mixer_control * +audiomixer_find_control(struct audiomixer *self, const char *name) +{ + struct mixer_control *ctl; + + for (guint i = 0; i < self->mixer_controls->len; i++) { + ctl = g_ptr_array_index (self->mixer_controls, i); + if (!strcmp(ctl->name, name)) { + return ctl; + } + } + return NULL; +} + +void +audiomixer_add_event_listener(struct audiomixer *self, + const struct audiomixer_events *events, + void *data) +{ + self->events = events; + self->events_data = data; +} + +static gboolean +do_change_volume (struct action * action) +{ + struct audiomixer *self = action->audiomixer; + GVariantBuilder b = G_VARIANT_BUILDER_INIT (G_VARIANT_TYPE_VARDICT); + gboolean ret = FALSE; + + g_variant_builder_add (&b, "{sv}", "volume", + g_variant_new_double (action->change_volume.volume)); + g_signal_emit_by_name (self->mixer_api, "set-volume", + action->change_volume.id, g_variant_builder_end (&b), &ret); + if (!ret) + g_warning ("mixer api set-volume failed"); + + return G_SOURCE_REMOVE; +} + +void +audiomixer_change_volume(struct audiomixer *self, + const struct mixer_control *control, + double volume) +{ + const struct mixer_control_impl *impl = + (const struct mixer_control_impl *) control; + struct action * action; + + g_return_if_fail (self->initialized == 1); + + /* schedule the action to run on the audiomixer thread */ + action = g_new0 (struct action, 1); + action->audiomixer = self; + action->change_volume.id = impl->node_id; + action->change_volume.volume = (gfloat) volume; + wp_core_idle_add (self->core, NULL, (GSourceFunc) do_change_volume, action, + g_free); +} + +static gboolean +do_change_mute (struct action * action) +{ + struct audiomixer *self = action->audiomixer; + GVariantBuilder b = G_VARIANT_BUILDER_INIT (G_VARIANT_TYPE_VARDICT); + gboolean ret = FALSE; + + g_variant_builder_add (&b, "{sv}", "mute", + g_variant_new_boolean (action->change_mute.mute)); + g_signal_emit_by_name (self->mixer_api, "set-volume", + action->change_mute.id, g_variant_builder_end (&b), &ret); + if (!ret) + g_warning ("mixer api set-volume failed"); + + return G_SOURCE_REMOVE; +} + +void +audiomixer_change_mute(struct audiomixer *self, + const struct mixer_control *control, + bool mute) +{ + const struct mixer_control_impl *impl = + (const struct mixer_control_impl *) control; + struct action * action; + + g_return_if_fail (self->initialized == 1); + + /* schedule the action to run on the audiomixer thread */ + action = g_new0 (struct action, 1); + action->audiomixer = self; + action->change_mute.id = impl->node_id; + action->change_mute.mute = mute; + wp_core_idle_add (self->core, NULL, (GSourceFunc) do_change_mute, action, + g_free); +} diff --git a/src/audiomixer.h b/src/audiomixer.h new file mode 100644 index 0000000..cc67a83 --- /dev/null +++ b/src/audiomixer.h @@ -0,0 +1,71 @@ +/* + * Copyright © 2019 Collabora Ltd. + * @author George Kiagiadakis + * + * SPDX-License-Identifier: MIT + */ + +#ifndef _AUDIOMIXER_H +#define _AUDIOMIXER_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include + +struct audiomixer; + +struct mixer_control +{ + char name[32]; + double volume; + bool mute; +}; + +struct audiomixer_events +{ + void (*controls_changed) (void *data); + + void (*value_changed) (void *data, +#define MIXER_CONTROL_CHANGE_FLAG_VOLUME (1<<0) +#define MIXER_CONTROL_CHANGE_FLAG_MUTE (1<<1) + unsigned int change_mask, + const struct mixer_control *control); +}; + +struct audiomixer * audiomixer_new(void); +void audiomixer_free(struct audiomixer *self); + +/* locking is required to call any of the methods below + * and to access any structure maintained by audiomixer */ +void audiomixer_lock(struct audiomixer *self); +void audiomixer_unlock(struct audiomixer *self); + +int audiomixer_ensure_controls(struct audiomixer *self, int timeout_sec); + +const struct mixer_control ** audiomixer_get_active_controls( + struct audiomixer *self, + unsigned int *n_controls); + +const struct mixer_control * audiomixer_find_control( + struct audiomixer *self, + const char *name); + +void audiomixer_add_event_listener(struct audiomixer *self, + const struct audiomixer_events *events, + void *data); + +void audiomixer_change_volume(struct audiomixer *self, + const struct mixer_control *control, + double volume); + +void audiomixer_change_mute(struct audiomixer *self, + const struct mixer_control *control, + bool mute); + +#ifdef __cplusplus +} +#endif + +#endif // _AUDIOMIXER_H diff --git a/src/main.cpp b/src/main.cpp new file mode 100644 index 0000000..0960f1b --- /dev/null +++ b/src/main.cpp @@ -0,0 +1,34 @@ +// SPDX-License-Identifier: Apache-2.0 + +#include +#include +#include +#include +#include "audiomixer-service.hpp" + +using work_guard_type = boost::asio::executor_work_guard; + +int main(int argc, char** argv) +{ + // The io_context is required for all I/O + net::io_context ioc; + + // Register to stop I/O context on SIGINT and SIGTERM + net::signal_set signals(ioc, SIGINT, SIGTERM); + signals.async_wait(boost::bind(&net::io_context::stop, &ioc)); + + // The SSL context is required, and holds certificates + ssl::context ctx{ssl::context::tlsv12_client}; + + // Launch the asynchronous operation + VisConfig config("agl-service-audiomixer"); + std::make_shared(config, ioc, ctx)->run(); + + // Ensure I/O context continues running even if there's no work + work_guard_type work_guard(ioc.get_executor()); + + // Run the I/O context + ioc.run(); + + return 0; +} diff --git a/src/meson.build b/src/meson.build new file mode 100644 index 0000000..282d130 --- /dev/null +++ b/src/meson.build @@ -0,0 +1,18 @@ +boost_dep = dependency('boost', + version : '>=1.72', + modules : [ 'thread', 'filesystem', 'program_options', 'log', 'system' ]) +openssl_dep = dependency('openssl') +thread_dep = dependency('threads') +wp_dep = dependency('wireplumber-0.4') + +src = [ 'vis-config.cpp', + 'vis-session.cpp', + 'audiomixer-service.cpp', + 'audiomixer.c', + 'main.cpp' +] +executable('agl-service-audiomixer', + src, + dependencies: [boost_dep, openssl_dep, thread_dep, systemd_dep, wp_dep], + install: true, + install_dir : get_option('sbindir')) diff --git a/src/vis-config.cpp b/src/vis-config.cpp new file mode 100644 index 0000000..b8d9266 --- /dev/null +++ b/src/vis-config.cpp @@ -0,0 +1,157 @@ +// SPDX-License-Identifier: Apache-2.0 + +#include "vis-config.hpp" +#include +#include +#include +#include +#include +#include + +namespace property_tree = boost::property_tree; +namespace filesystem = boost::filesystem; + +#define DEFAULT_CLIENT_KEY_FILE "/etc/kuksa-val/Client.key" +#define DEFAULT_CLIENT_CERT_FILE "/etc/kuksa-val/Client.pem" +#define DEFAULT_CA_CERT_FILE "/etc/kuksa-val/CA.pem" + + +VisConfig::VisConfig(const std::string &hostname, + const unsigned port, + const std::string &clientKey, + const std::string &clientCert, + const std::string &caCert, + const std::string &authToken, + bool verifyPeer) : + m_hostname(hostname), + m_port(port), + m_clientKey(clientKey), + m_clientCert(clientCert), + m_caCert(caCert), + m_authToken(authToken), + m_verifyPeer(verifyPeer), + m_verbose(0), + m_valid(true) +{ + // Potentially could do some certificate validation here... +} + +VisConfig::VisConfig(const std::string &appname) : + m_valid(false) +{ + std::string config("/etc/xdg/AGL/"); + config += appname; + config += ".conf"; + char *home = getenv("XDG_CONFIG_HOME"); + if (home) { + config = home; + config += "/AGL/"; + config += appname; + config += ".conf"; + } + + std::cout << "Using configuration " << config << std::endl; + property_tree::ptree pt; + try { + property_tree::ini_parser::read_ini(config, pt); + } + catch (std::exception &ex) { + std::cerr << "Could not read " << config << std::endl; + return; + } + const property_tree::ptree &settings = + pt.get_child("vis-client", property_tree::ptree()); + + m_hostname = settings.get("server", "localhost"); + std::stringstream ss; + ss << m_hostname; + ss >> std::quoted(m_hostname); + if (m_hostname.empty()) { + std::cerr << "Invalid server hostname" << std::endl; + return; + } + + m_port = settings.get("port", 8090); + if (m_port == 0) { + std::cerr << "Invalid server port" << std::endl; + return; + } + + // Default to disabling peer verification for now to be able + // to use the default upstream KUKSA.val certificates for + // testing. Wrangling server and CA certificate generation + // and management to be able to verify will require further + // investigation. + m_verifyPeer = settings.get("verify-server", false); + + std::string keyFileName = settings.get("key", DEFAULT_CLIENT_KEY_FILE); + std::stringstream().swap(ss); + ss << keyFileName; + ss >> std::quoted(keyFileName); + ss.str(""); + if (keyFileName.empty()) { + std::cerr << "Invalid client key filename" << std::endl; + return; + } + filesystem::load_string_file(keyFileName, m_clientKey); + if (m_clientKey.empty()) { + std::cerr << "Invalid client key file" << std::endl; + return; + } + + std::string certFileName = settings.get("certificate", DEFAULT_CLIENT_CERT_FILE); + std::stringstream().swap(ss); + ss << certFileName; + ss >> std::quoted(certFileName); + if (certFileName.empty()) { + std::cerr << "Invalid client certificate filename" << std::endl; + return; + } + filesystem::load_string_file(certFileName, m_clientCert); + if (m_clientCert.empty()) { + std::cerr << "Invalid client certificate file" << std::endl; + return; + } + + std::string caCertFileName = settings.get("ca-certificate", DEFAULT_CA_CERT_FILE); + std::stringstream().swap(ss); + ss << caCertFileName; + ss >> std::quoted(caCertFileName); + if (caCertFileName.empty()) { + std::cerr << "Invalid CA certificate filename" << std::endl; + return; + } + filesystem::load_string_file(caCertFileName, m_caCert); + if (m_caCert.empty()) { + std::cerr << "Invalid CA certificate file" << std::endl; + return; + } + + std::string authTokenFileName = settings.get("authorization", ""); + std::stringstream().swap(ss); + ss << authTokenFileName; + ss >> std::quoted(authTokenFileName); + if (authTokenFileName.empty()) { + std::cerr << "Invalid authorization token filename" << std::endl; + return; + } + filesystem::load_string_file(authTokenFileName, m_authToken); + if (m_authToken.empty()) { + std::cerr << "Invalid authorization token file" << std::endl; + return; + } + + m_verbose = 0; + std::string verbose = settings.get("verbose", ""); + std::stringstream().swap(ss); + ss << verbose; + ss >> std::quoted(verbose); + if (!verbose.empty()) { + if (verbose == "true" || verbose == "1") + m_verbose = 1; + if (verbose == "2") + m_verbose = 2; + } + + m_valid = true; +} diff --git a/src/vis-config.hpp b/src/vis-config.hpp new file mode 100644 index 0000000..b0f72f9 --- /dev/null +++ b/src/vis-config.hpp @@ -0,0 +1,43 @@ +// SPDX-License-Identifier: Apache-2.0 + +#ifndef _VIS_CONFIG_HPP +#define _VIS_CONFIG_HPP + +#include + +class VisConfig +{ +public: + explicit VisConfig(const std::string &hostname, + const unsigned port, + const std::string &clientKey, + const std::string &clientCert, + const std::string &caCert, + const std::string &authToken, + bool verifyPeer = true); + explicit VisConfig(const std::string &appname); + ~VisConfig() {}; + + std::string hostname() { return m_hostname; }; + unsigned port() { return m_port; }; + std::string clientKey() { return m_clientKey; }; + std::string clientCert() { return m_clientCert; }; + std::string caCert() { return m_caCert; }; + std::string authToken() { return m_authToken; }; + bool verifyPeer() { return m_verifyPeer; }; + bool valid() { return m_valid; }; + unsigned verbose() { return m_verbose; }; + +private: + std::string m_hostname; + unsigned m_port; + std::string m_clientKey; + std::string m_clientCert; + std::string m_caCert; + std::string m_authToken; + bool m_verifyPeer; + unsigned m_verbose; + bool m_valid; +}; + +#endif // _VIS_CONFIG_HPP diff --git a/src/vis-session.cpp b/src/vis-session.cpp new file mode 100644 index 0000000..880e3ae --- /dev/null +++ b/src/vis-session.cpp @@ -0,0 +1,374 @@ +// SPDX-License-Identifier: Apache-2.0 + +#include "vis-session.hpp" +#include +#include +#include + + +// Logging helper +static void log_error(beast::error_code error, char const* what) +{ + std::cerr << what << " error: " << error.message() << std::endl; +} + + +// Resolver and socket require an io_context +VisSession::VisSession(const VisConfig &config, net::io_context& ioc, ssl::context& ctx) : + m_config(config), + m_resolver(net::make_strand(ioc)), + m_ws(net::make_strand(ioc), ctx) +{ +} + +// Start the asynchronous operation +void VisSession::run() +{ + if (!m_config.valid()) { + return; + } + + // Start by resolving hostname + m_resolver.async_resolve(m_config.hostname(), + std::to_string(m_config.port()), + beast::bind_front_handler(&VisSession::on_resolve, + shared_from_this())); +} + +void VisSession::on_resolve(beast::error_code error, + tcp::resolver::results_type results) +{ + if(error) { + log_error(error, "resolve"); + return; + } + + // Set a timeout on the connect operation + beast::get_lowest_layer(m_ws).expires_after(std::chrono::seconds(30)); + + // Connect to resolved address + if (m_config.verbose()) + std::cout << "Connecting" << std::endl; + m_results = results; + connect(); +} + +void VisSession::connect() +{ + beast::get_lowest_layer(m_ws).async_connect(m_results, + beast::bind_front_handler(&VisSession::on_connect, + shared_from_this())); +} + +void VisSession::on_connect(beast::error_code error, + tcp::resolver::results_type::endpoint_type endpoint) +{ + if(error) { + // The server can take a while to be ready to accept connections, + // so keep retrying until we hit the timeout. + if (error == net::error::timed_out) { + log_error(error, "connect"); + return; + } + + // Delay 500 ms before retrying + std::this_thread::sleep_for(std::chrono::milliseconds(500)); + + if (m_config.verbose()) + std::cout << "Connecting" << std::endl; + + connect(); + return; + } + + if (m_config.verbose()) + std::cout << "Connected" << std::endl; + + // Set handshake timeout + beast::get_lowest_layer(m_ws).expires_after(std::chrono::seconds(30)); + + // Set SNI Hostname (many hosts need this to handshake successfully) + if(!SSL_set_tlsext_host_name(m_ws.next_layer().native_handle(), + m_config.hostname().c_str())) + { + error = beast::error_code(static_cast(::ERR_get_error()), + net::error::get_ssl_category()); + log_error(error, "connect"); + return; + } + + // Update the hostname. This will provide the value of the + // Host HTTP header during the WebSocket handshake. + // See https://tools.ietf.org/html/rfc7230#section-5.4 + m_hostname = m_config.hostname() + ':' + std::to_string(endpoint.port()); + + if (m_config.verbose()) + std::cout << "Negotiating SSL handshake" << std::endl; + + // Perform the SSL handshake + m_ws.next_layer().async_handshake(ssl::stream_base::client, + beast::bind_front_handler(&VisSession::on_ssl_handshake, + shared_from_this())); +} + +void VisSession::on_ssl_handshake(beast::error_code error) +{ + if(error) { + log_error(error, "SSL handshake"); + return; + } + + // Turn off the timeout on the tcp_stream, because + // the websocket stream has its own timeout system. + beast::get_lowest_layer(m_ws).expires_never(); + + // NOTE: Explicitly not setting websocket stream timeout here, + // as the client is long-running. + + if (m_config.verbose()) + std::cout << "Negotiating WSS handshake" << std::endl; + + // Perform handshake + m_ws.async_handshake(m_hostname, + "/", + beast::bind_front_handler(&VisSession::on_handshake, + shared_from_this())); +} + +void VisSession::on_handshake(beast::error_code error) +{ + if(error) { + log_error(error, "WSS handshake"); + return; + } + + if (m_config.verbose()) + std::cout << "Authorizing" << std::endl; + + // Authorize + json req; + req["requestId"] = std::to_string(m_requestid++); + req["action"]= "authorize"; + req["tokens"] = m_config.authToken(); + + m_ws.async_write(net::buffer(req.dump(4)), + beast::bind_front_handler(&VisSession::on_authorize, + shared_from_this())); +} + +void VisSession::on_authorize(beast::error_code error, std::size_t bytes_transferred) +{ + boost::ignore_unused(bytes_transferred); + + if(error) { + log_error(error, "authorize"); + return; + } + + // Read response + m_ws.async_read(m_buffer, + beast::bind_front_handler(&VisSession::on_read, + shared_from_this())); +} + +// NOTE: Placeholder for now +void VisSession::on_write(beast::error_code error, std::size_t bytes_transferred) +{ + boost::ignore_unused(bytes_transferred); + + if(error) { + log_error(error, "write"); + return; + } + + // Do nothing... +} + +void VisSession::on_read(beast::error_code error, std::size_t bytes_transferred) +{ + boost::ignore_unused(bytes_transferred); + + if(error) { + log_error(error, "read"); + return; + } + + // Handle message + std::string s = beast::buffers_to_string(m_buffer.data()); + json response = json::parse(s, nullptr, false); + if (!response.is_discarded()) { + handle_message(response); + } else { + std::cerr << "json::parse failed? got " << s << std::endl; + } + m_buffer.consume(m_buffer.size()); + + // Read next message + m_ws.async_read(m_buffer, + beast::bind_front_handler(&VisSession::on_read, + shared_from_this())); +} + +void VisSession::get(const std::string &path) +{ + if (!m_config.valid()) { + return; + } + + json req; + req["requestId"] = std::to_string(m_requestid++); + req["action"] = "get"; + req["path"] = path; + req["tokens"] = m_config.authToken(); + + m_ws.write(net::buffer(req.dump(4))); +} + +void VisSession::set(const std::string &path, const std::string &value) +{ + if (!m_config.valid()) { + return; + } + + json req; + req["requestId"] = std::to_string(m_requestid++); + req["action"] = "set"; + req["path"] = path; + req["value"] = value; + req["tokens"] = m_config.authToken(); + + m_ws.write(net::buffer(req.dump(4))); +} + +void VisSession::subscribe(const std::string &path) +{ + if (!m_config.valid()) { + return; + } + + json req; + req["requestId"] = std::to_string(m_requestid++); + req["action"] = "subscribe"; + req["path"] = path; + req["tokens"] = m_config.authToken(); + + m_ws.write(net::buffer(req.dump(4))); +} + +bool VisSession::parseData(const json &message, std::string &path, std::string &value, std::string ×tamp) +{ + if (message.contains("error")) { + std::string error = message["error"]; + return false; + } + + if (!(message.contains("data") && message["data"].is_object())) { + std::cerr << "Malformed message (data missing)" << std::endl; + return false; + } + auto data = message["data"]; + if (!(data.contains("path") && data["path"].is_string())) { + std::cerr << "Malformed message (path missing)" << std::endl; + return false; + } + path = data["path"]; + // Convert '/' to '.' in paths to ensure consistency for clients + std::replace(path.begin(), path.end(), '/', '.'); + + if (!(data.contains("dp") && data["dp"].is_object())) { + std::cerr << "Malformed message (datapoint missing)" << std::endl; + return false; + } + auto dp = data["dp"]; + if (!dp.contains("value")) { + std::cerr << "Malformed message (value missing)" << std::endl; + return false; + } else if (dp["value"].is_string()) { + value = dp["value"]; + } else if (dp["value"].is_number_float()) { + double num = dp["value"]; + value = std::to_string(num); + } else if (dp["value"].is_boolean()) { + value = dp["value"] ? "true" : "false"; + } else { + std::cerr << "Malformed message (unsupported value type)" << std::endl; + return false; + } + + if (!(dp.contains("ts") && dp["ts"].is_string())) { + std::cerr << "Malformed message (timestamp missing)" << std::endl; + return false; + } + timestamp = dp["ts"]; + + return true; +} + +void VisSession::handle_message(const json &message) +{ + if (m_config.verbose() > 1) + std::cout << "VisSession::handle_message: enter, message = " << to_string(message) << std::endl; + + if (!message.contains("action")) { + std::cerr << "Received unknown message (no action), discarding" << std::endl; + return; + } + + std::string action = message["action"]; + if (action == "authorize") { + if (message.contains("error")) { + std::string error = "unknown"; + if (message["error"].is_object() && message["error"].contains("message")) + error = message["error"]["message"]; + std::cerr << "VIS authorization failed: " << error << std::endl; + } else { + if (m_config.verbose() > 1) + std::cout << "authorized" << std::endl; + + handle_authorized_response(); + } + } else if (action == "subscribe") { + if (message.contains("error")) { + std::string error = "unknown"; + if (message["error"].is_object() && message["error"].contains("message")) + error = message["error"]["message"]; + std::cerr << "VIS subscription failed: " << error << std::endl; + } + } else if (action == "get") { + if (message.contains("error")) { + std::string error = "unknown"; + if (message["error"].is_object() && message["error"].contains("message")) + error = message["error"]["message"]; + std::cerr << "VIS get failed: " << error << std::endl; + } else { + std::string path, value, ts; + if (parseData(message, path, value, ts)) { + if (m_config.verbose() > 1) + std::cout << "VisSession::handle_message: got response " << path << " = " << value << std::endl; + + handle_get_response(path, value, ts); + } + } + } else if (action == "set") { + if (message.contains("error")) { + std::string error = "unknown"; + if (message["error"].is_object() && message["error"].contains("message")) + error = message["error"]["message"]; + std::cerr << "VIS set failed: " << error; + } + } else if (action == "subscription") { + std::string path, value, ts; + if (parseData(message, path, value, ts)) { + if (m_config.verbose() > 1) + std::cout << "VisSession::handle_message: got notification " << path << " = " << value << std::endl; + + handle_notification(path, value, ts); + } + } else { + std::cerr << "unhandled VIS response of type: " << action; + } + + if (m_config.verbose() > 1) + std::cout << "VisSession::handle_message: exit" << std::endl; +} + diff --git a/src/vis-session.hpp b/src/vis-session.hpp new file mode 100644 index 0000000..8c7b0d9 --- /dev/null +++ b/src/vis-session.hpp @@ -0,0 +1,78 @@ +// SPDX-License-Identifier: Apache-2.0 + +#ifndef _VIS_SESSION_HPP +#define _VIS_SESSION_HPP + +#include "vis-config.hpp" +#include +#include +#include +#include +#include +#include +#include +#include + +namespace beast = boost::beast; +namespace websocket = beast::websocket; +namespace net = boost::asio; +namespace ssl = boost::asio::ssl; +using tcp = boost::asio::ip::tcp; +using json = nlohmann::json; + + +class VisSession : public std::enable_shared_from_this +{ + //net::io_context m_ioc; + tcp::resolver m_resolver; + tcp::resolver::results_type m_results; + std::string m_hostname; + websocket::stream> m_ws; + beast::flat_buffer m_buffer; + +public: + // Resolver and socket require an io_context + explicit VisSession(const VisConfig &config, net::io_context& ioc, ssl::context& ctx); + + // Start the asynchronous operation + void run(); + +protected: + VisConfig m_config; + std::atomic_uint m_requestid; + + void on_resolve(beast::error_code error, tcp::resolver::results_type results); + + void connect(); + + void on_connect(beast::error_code error, tcp::resolver::results_type::endpoint_type endpoint); + + void on_ssl_handshake(beast::error_code error); + + void on_handshake(beast::error_code error); + + void on_authorize(beast::error_code error, std::size_t bytes_transferred); + + void on_write(beast::error_code error, std::size_t bytes_transferred); + + void on_read(beast::error_code error, std::size_t bytes_transferred); + + void get(const std::string &path); + + void set(const std::string &path, const std::string &value); + + void subscribe(const std::string &path); + + void handle_message(const json &message); + + bool parseData(const json &message, std::string &path, std::string &value, std::string ×tamp); + + virtual void handle_authorized_response(void) = 0; + + virtual void handle_get_response(std::string &path, std::string &value, std::string ×tamp) = 0; + + virtual void handle_notification(std::string &path, std::string &value, std::string ×tamp) = 0; + +}; + +#endif // _VIS_SESSION_HPP diff --git a/systemd/agl-service-audiomixer.service b/systemd/agl-service-audiomixer.service new file mode 100644 index 0000000..b6603fa --- /dev/null +++ b/systemd/agl-service-audiomixer.service @@ -0,0 +1,11 @@ +[Unit] +Requires=kuksa-val.service +After=kuksa-val.service + +[Service] +Type=simple +ExecStart=/usr/sbin/agl-service-audiomixer +Restart=on-failure + +[Install] +WantedBy=default.target diff --git a/systemd/meson.build b/systemd/meson.build new file mode 100644 index 0000000..c94ab80 --- /dev/null +++ b/systemd/meson.build @@ -0,0 +1,3 @@ +systemd_system_unit_dir = systemd_dep.get_pkgconfig_variable('systemdsystemunitdir') + +install_data('agl-service-audiomixer.service', install_dir : systemd_system_unit_dir) diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt deleted file mode 100644 index 8d8fbdc..0000000 --- a/test/CMakeLists.txt +++ /dev/null @@ -1,28 +0,0 @@ -########################################################################### -# Copyright 2020 Fujitsu -# -# author: Li Xiaoming -# -# 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. -########################################################################### - - -# Include any directory not starting with _ -# ----------------------------------------------------- -PROJECT_SUBDIRS_ADD(${PROJECT_SRC_DIR_PATTERN}) - -ADD_TEST(NAME AGL_SERVICE_GPS_TESTS - WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} - COMMAND afm-test ${CMAKE_BINARY_DIR}/package ${CMAKE_BINARY_DIR}/package-test - ) - diff --git a/test/afb-test/CMakeLists.txt b/test/afb-test/CMakeLists.txt deleted file mode 100644 index 4adafb0..0000000 --- a/test/afb-test/CMakeLists.txt +++ /dev/null @@ -1,21 +0,0 @@ -########################################################################### -# Copyright 2020 Fujitsu -# -# author: Li Xiaoming -# -# 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. -########################################################################### - -# Include any directory not starting with _ -# ----------------------------------------------------- -PROJECT_SUBDIRS_ADD(${PROJECT_SRC_DIR_PATTERN}) diff --git a/test/afb-test/etc/CMakeLists.txt b/test/afb-test/etc/CMakeLists.txt deleted file mode 100644 index 12e2efc..0000000 --- a/test/afb-test/etc/CMakeLists.txt +++ /dev/null @@ -1,32 +0,0 @@ -########################################################################### -# Copyright 2020 Fujitsu -# -# author: Li Xiaoming -# -# 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. -########################################################################### - -################################################## -# audiomixer test configuration files -################################################## -PROJECT_TARGET_ADD(afb-test-config) - - file(GLOB CONF_FILES "*.json") - - add_input_files("${CONF_FILES}") - - SET_TARGET_PROPERTIES(${TARGET_NAME} PROPERTIES - LABELS "TEST-CONFIG" - OUTPUT_NAME ${TARGET_NAME} - ) - diff --git a/test/afb-test/etc/aft-agl-audiomixer.json b/test/afb-test/etc/aft-agl-audiomixer.json deleted file mode 100644 index c1e1c96..0000000 --- a/test/afb-test/etc/aft-agl-audiomixer.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "id": "http://iot.bzh/download/public/schema/json/ctl-schema.json#", - "": "http://iot.bzh/download/public/schema/json/ctl-schema.json#", - "metadata": { - "uid": "Test", - "version": "1.0", - "api": "aft-audiomixer", - "info": "AFB-test binding configuration file to test audiomixer api.", - "require": [ - "audiomixer" - ] - }, - "testVerb": { - "uid": "launch_all_tests", - "info": "Launch all the tests", - "action": "lua://AFT#_launch_test", - "args": { - "trace": "audiomixer", - "files": ["audiomixer.lua"] - } - } -} diff --git a/test/afb-test/tests/CMakeLists.txt b/test/afb-test/tests/CMakeLists.txt deleted file mode 100644 index 4153151..0000000 --- a/test/afb-test/tests/CMakeLists.txt +++ /dev/null @@ -1,31 +0,0 @@ -########################################################################### -# Copyright 2020 Fujitsu -# -# author:Li Xiaoming -# -# 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. -########################################################################### - -################################################## -# audiomixer Lua Scripts -################################################## -PROJECT_TARGET_ADD(test-files) - - file(GLOB LUA_FILES "*.lua" "*.sh") - add_input_files("${LUA_FILES}") - - SET_TARGET_PROPERTIES(${TARGET_NAME} PROPERTIES - LABELS "TEST-DATA" - OUTPUT_NAME ${TARGET_NAME} - ) - diff --git a/test/afb-test/tests/audiomixer.lua b/test/afb-test/tests/audiomixer.lua deleted file mode 100644 index a978508..0000000 --- a/test/afb-test/tests/audiomixer.lua +++ /dev/null @@ -1,40 +0,0 @@ ---[[ - Copyright 2020 Fujitsu - - author: Li Xiaoming - - 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. ---]] - - - -_AFT.testVerbStatusSuccess("testListControlSuccess","audiomixer","list_controls", {}) - -_AFT.testVerbStatusSuccess("testMuteChange0Success","audiomixer","mute", {control="Master", value="0"}) -_AFT.testVerbStatusSuccess("testMuteChange1Success","audiomixer","mute", {control="Master", value="1"}) -_AFT.testVerbStatusSuccess("testMuteNotChangeSuccess","audiomixer","mute", {control="Master"}) - -_AFT.testVerbStatusSuccess("testVolumeChangeSuccess","audiomixer","volume", {control="Master", value="0.5"}) -_AFT.testVerbStatusSuccess("testVolumeNotChangeSuccess","audiomixer","volume", {control="Master"}) - -_AFT.testVerbStatusSuccess("testSubscribeControls_changedSuccess","audiomixer","subscribe", {event="controls_changed"}) -_AFT.testVerbStatusSuccess("testSubscribeVolume_changedSuccess","audiomixer","subscribe", {event="volume_changed"}) -_AFT.testVerbStatusSuccess("testSubscribeMute_changedSuccess","audiomixer","subscribe", {event="mute_changed"}) - -_AFT.testVerbStatusSuccess("testUnSubscribeControls_changedSuccess","audiomixer","unsubscribe", {event="controls_changed"}) -_AFT.testVerbStatusSuccess("testUnSubscribeVolume_changedSuccess","audiomixer","unsubscribe", {event="volume_changed"}) -_AFT.testVerbStatusSuccess("testUnSubscribeMute_changedSuccess","audiomixer","unsubscribe", {event="mute_changed"}) - - - - -- cgit 1.2.3-korg