From 4d9f5168158387e37fc2d9fe9332aeb6ce4491d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Asbj=C3=B8rn=20S=C3=A6b=C3=B8?= Date: Fri, 22 Oct 2021 15:16:14 +0200 Subject: [PATCH] Bluetooth: Audio: Media control service MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This commit adds the Media Control Service, and a dummy media player, from the topic-le-audio branch. This is a part of the upmerge of the le-audio media control files. This service has been developed and maintained over a couple of years now, and is mature. During the development it has passed both IOP-testing and PTS qualification testing. The commit is a pure copy of the files and content in the topic-le-audio branch, with the following exceptions: - files are in bluetooth/audio instead of bluetooth/host/audio, with some include paths updated as a consequence - as a consequence, CMake files and Kconfig files updates are done in other locations - copyrights have been updated Signed-off-by: Asbjørn Sæbø --- include/bluetooth/audio/mcs.h | 199 ++ subsys/bluetooth/audio/CMakeLists.txt | 4 + subsys/bluetooth/audio/mcs.c | 989 +++++++++ subsys/bluetooth/audio/mcs_internal.h | 25 + subsys/bluetooth/audio/mpl.c | 2923 +++++++++++++++++++++++++ subsys/bluetooth/audio/mpl_internal.h | 153 ++ 6 files changed, 4293 insertions(+) create mode 100644 include/bluetooth/audio/mcs.h create mode 100644 subsys/bluetooth/audio/mcs.c create mode 100644 subsys/bluetooth/audio/mcs_internal.h create mode 100644 subsys/bluetooth/audio/mpl.c create mode 100644 subsys/bluetooth/audio/mpl_internal.h diff --git a/include/bluetooth/audio/mcs.h b/include/bluetooth/audio/mcs.h new file mode 100644 index 00000000000..558f0f43f83 --- /dev/null +++ b/include/bluetooth/audio/mcs.h @@ -0,0 +1,199 @@ +/* + * Copyright (c) 2019 - 2021 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#ifndef ZEPHYR_INCLUDE_BLUETOOTH_AUDIO_MCS_H_ +#define ZEPHYR_INCLUDE_BLUETOOTH_AUDIO_MCS_H_ + +/** + * @brief Media Control Service (MCS) + * + * @defgroup bt_mcs Media Control Service (MCS) + * + * @ingroup bluetooth + * @{ + * + * [Experimental] Users should note that the APIs can change + * as a part of ongoing development. + * + * Definitions and types related to the Media Control Service and Media Control + * Profile specifications. + */ + +#include + +#ifdef __cplusplus +extern "C" { +#endif + + +/** @brief Playback speeds + * + * All values from -128 to 127 allowed, only some defined + */ +#define BT_MCS_PLAYBACK_SPEED_MIN -128 +#define BT_MCS_PLAYBACK_SPEED_QUARTER -128 +#define BT_MCS_PLAYBACK_SPEED_HALF -64 +#define BT_MCS_PLAYBACK_SPEED_UNITY 0 +#define BT_MCS_PLAYBACK_SPEED_DOUBLE 64 +#define BT_MCS_PLAYBACK_SPEED_MAX 127 + +/** @brief Seeking speed + * + * The allowed values for seeking speed are the range -64 to -4 + * (endpoints included), the value 0, and the range 4 to 64 + * (endpoints included). + */ +#define BT_MCS_SEEKING_SPEED_FACTOR_MAX 64 +#define BT_MCS_SEEKING_SPEED_FACTOR_MIN 4 +#define BT_MCS_SEEKING_SPEED_FACTOR_ZERO 0 + +/** Playing orders */ +#define BT_MCS_PLAYING_ORDER_SINGLE_ONCE 0X01 +#define BT_MCS_PLAYING_ORDER_SINGLE_REPEAT 0x02 +#define BT_MCS_PLAYING_ORDER_INORDER_ONCE 0x03 +#define BT_MCS_PLAYING_ORDER_INORDER_REPEAT 0x04 +#define BT_MCS_PLAYING_ORDER_OLDEST_ONCE 0x05 +#define BT_MCS_PLAYING_ORDER_OLDEST_REPEAT 0x06 +#define BT_MCS_PLAYING_ORDER_NEWEST_ONCE 0x07 +#define BT_MCS_PLAYING_ORDER_NEWEST_REPEAT 0x08 +#define BT_MCS_PLAYING_ORDER_SHUFFLE_ONCE 0x09 +#define BT_MCS_PLAYING_ORDER_SHUFFLE_REPEAT 0x0a + +/** @brief Playing orders supported + * + * A bitmap, in the same order as the playing orders above. + * Note that playing order 1 corresponds to bit 0, and so on. + */ +#define BT_MCS_PLAYING_ORDERS_SUPPORTED_SINGLE_ONCE BIT(0) +#define BT_MCS_PLAYING_ORDERS_SUPPORTED_SINGLE_REPEAT BIT(1) +#define BT_MCS_PLAYING_ORDERS_SUPPORTED_INORDER_ONCE BIT(2) +#define BT_MCS_PLAYING_ORDERS_SUPPORTED_INORDER_REPEAT BIT(3) +#define BT_MCS_PLAYING_ORDERS_SUPPORTED_OLDEST_ONCE BIT(4) +#define BT_MCS_PLAYING_ORDERS_SUPPORTED_OLDEST_REPEAT BIT(5) +#define BT_MCS_PLAYING_ORDERS_SUPPORTED_NEWEST_ONCE BIT(6) +#define BT_MCS_PLAYING_ORDERS_SUPPORTED_NEWEST_REPEAT BIT(7) +#define BT_MCS_PLAYING_ORDERS_SUPPORTED_SHUFFLE_ONCE BIT(8) +#define BT_MCS_PLAYING_ORDERS_SUPPORTED_SHUFFLE_REPEAT BIT(9) + +/** Media states */ +#define BT_MCS_MEDIA_STATE_INACTIVE 0x00 +#define BT_MCS_MEDIA_STATE_PLAYING 0x01 +#define BT_MCS_MEDIA_STATE_PAUSED 0x02 +#define BT_MCS_MEDIA_STATE_SEEKING 0x03 +#define BT_MCS_MEDIA_STATE_LAST 0x04 + +/** Media control point opcodes */ +#define BT_MCS_OPC_PLAY 0x01 +#define BT_MCS_OPC_PAUSE 0x02 +#define BT_MCS_OPC_FAST_REWIND 0x03 +#define BT_MCS_OPC_FAST_FORWARD 0x04 +#define BT_MCS_OPC_STOP 0x05 + +#define BT_MCS_OPC_MOVE_RELATIVE 0x10 + +#define BT_MCS_OPC_PREV_SEGMENT 0x20 +#define BT_MCS_OPC_NEXT_SEGMENT 0x21 +#define BT_MCS_OPC_FIRST_SEGMENT 0x22 +#define BT_MCS_OPC_LAST_SEGMENT 0x23 +#define BT_MCS_OPC_GOTO_SEGMENT 0x24 + +#define BT_MCS_OPC_PREV_TRACK 0x30 +#define BT_MCS_OPC_NEXT_TRACK 0x31 +#define BT_MCS_OPC_FIRST_TRACK 0x32 +#define BT_MCS_OPC_LAST_TRACK 0x33 +#define BT_MCS_OPC_GOTO_TRACK 0x34 + +#define BT_MCS_OPC_PREV_GROUP 0x40 +#define BT_MCS_OPC_NEXT_GROUP 0x41 +#define BT_MCS_OPC_FIRST_GROUP 0x42 +#define BT_MCS_OPC_LAST_GROUP 0x43 +#define BT_MCS_OPC_GOTO_GROUP 0x44 + +/** Media control point supported opcodes length */ +#define BT_MCS_OPCODES_SUPPORTED_LEN 4 + +/** Media control point supported opcodes values */ +#define BT_MCS_OPC_SUP_PLAY BIT(0) +#define BT_MCS_OPC_SUP_PAUSE BIT(1) +#define BT_MCS_OPC_SUP_FAST_REWIND BIT(2) +#define BT_MCS_OPC_SUP_FAST_FORWARD BIT(3) +#define BT_MCS_OPC_SUP_STOP BIT(4) + +#define BT_MCS_OPC_SUP_MOVE_RELATIVE BIT(5) + +#define BT_MCS_OPC_SUP_PREV_SEGMENT BIT(6) +#define BT_MCS_OPC_SUP_NEXT_SEGMENT BIT(7) +#define BT_MCS_OPC_SUP_FIRST_SEGMENT BIT(8) +#define BT_MCS_OPC_SUP_LAST_SEGMENT BIT(9) +#define BT_MCS_OPC_SUP_GOTO_SEGMENT BIT(10) + +#define BT_MCS_OPC_SUP_PREV_TRACK BIT(11) +#define BT_MCS_OPC_SUP_NEXT_TRACK BIT(12) +#define BT_MCS_OPC_SUP_FIRST_TRACK BIT(13) +#define BT_MCS_OPC_SUP_LAST_TRACK BIT(14) +#define BT_MCS_OPC_SUP_GOTO_TRACK BIT(15) + +#define BT_MCS_OPC_SUP_PREV_GROUP BIT(16) +#define BT_MCS_OPC_SUP_NEXT_GROUP BIT(17) +#define BT_MCS_OPC_SUP_FIRST_GROUP BIT(18) +#define BT_MCS_OPC_SUP_LAST_GROUP BIT(19) +#define BT_MCS_OPC_SUP_GOTO_GROUP BIT(20) + +/** Media control point notification result codes */ +#define BT_MCS_OPC_NTF_SUCCESS 0x01 +#define BT_MCS_OPC_NTF_NOT_SUPPORTED 0x02 +#define BT_MCS_OPC_NTF_PLAYER_INACTIVE 0x03 +#define BT_MCS_OPC_NTF_CANNOT_BE_COMPLETED 0x04 + +/** Search control point type values */ +/* Reference: Media Control Service spec v1.0 section 3.20.2 */ +#define BT_MCS_SEARCH_TYPE_TRACK_NAME 0x01 +#define BT_MCS_SEARCH_TYPE_ARTIST_NAME 0x02 +#define BT_MCS_SEARCH_TYPE_ALBUM_NAME 0x03 +#define BT_MCS_SEARCH_TYPE_GROUP_NAME 0x04 +#define BT_MCS_SEARCH_TYPE_EARLIEST_YEAR 0x05 +#define BT_MCS_SEARCH_TYPE_LATEST_YEAR 0x06 +#define BT_MCS_SEARCH_TYPE_GENRE 0x07 +#define BT_MCS_SEARCH_TYPE_ONLY_TRACKS 0x08 +#define BT_MCS_SEARCH_TYPE_ONLY_GROUPS 0x09 + +/** Search control point values */ +#define SEARCH_LEN_MIN 2 /* At least one search control item (SCI), + * consisting of the length octet and the type + * octet. (The parameter field may be empty.) + */ + +#define SEARCH_SCI_LEN_MIN 1 /* An SCI length can be as little as one byte, + * for an SCI that has only the type field. + * (The SCI len is the length of type + param.) + */ + +#define SEARCH_LEN_MAX 64 /* Max total length of search, defined by spec */ + +#define SEARCH_PARAM_MAX 62 /* A search may have a single search control item + * consisting of length, type and parameter + */ + +/** Search notification result codes */ +/* Reference: Media Control Service spec v1.0 section 3.20.2 */ +#define BT_MCS_SCP_NTF_SUCCESS 0x01 +#define BT_MCS_SCP_NTF_FAILURE 0x02 + +/* Group object object types */ +/* Reference: Media Control Service spec v1.0 section 4.4.1 */ +#define BT_MCS_GROUP_OBJECT_TRACK_TYPE 0x00 +#define BT_MCS_GROUP_OBJECT_GROUP_TYPE 0x01 + + +#ifdef __cplusplus +} +#endif + +/** + * @} + */ + +#endif /* ZEPHYR_INCLUDE_BLUETOOTH_AUDIO_MCS_H_ */ diff --git a/subsys/bluetooth/audio/CMakeLists.txt b/subsys/bluetooth/audio/CMakeLists.txt index 40d8254313d..d02c5a3103c 100644 --- a/subsys/bluetooth/audio/CMakeLists.txt +++ b/subsys/bluetooth/audio/CMakeLists.txt @@ -36,4 +36,8 @@ zephyr_library_sources_ifdef(CONFIG_BT_OTC otc.c) zephyr_library_sources_ifdef(CONFIG_BT_MCC mcc.c) +zephyr_library_sources_ifdef(CONFIG_BT_MCS mcs.c) + +zephyr_library_sources_ifdef(CONFIG_BT_MCS mpl.c) + zephyr_library_sources_ifdef(CONFIG_BT_MCS media_proxy.c) diff --git a/subsys/bluetooth/audio/mcs.c b/subsys/bluetooth/audio/mcs.c new file mode 100644 index 00000000000..3dc56644595 --- /dev/null +++ b/subsys/bluetooth/audio/mcs.c @@ -0,0 +1,989 @@ +/** @file + * @brief Bluetooth Media Control Service + */ + +/* + * Copyright (c) 2019 - 2021 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: Apache-2.0 + */ + + +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#include "media_proxy_internal.h" + +#define BT_DBG_ENABLED IS_ENABLED(CONFIG_BT_DEBUG_MCS) +#define LOG_MODULE_NAME bt_mcs +#include "common/log.h" + +static struct media_proxy_sctrl_cbs cbs; + +/* Functions for reading and writing attributes, and for keeping track + * of attribute configuration changes. + * Functions for notifications are placed after the service defition. + */ +static ssize_t read_player_name(struct bt_conn *conn, + const struct bt_gatt_attr *attr, void *buf, + uint16_t len, uint16_t offset) +{ + const char *name = media_proxy_sctrl_get_player_name(); + + BT_DBG("Player name read: %s", log_strdup(name)); + + return bt_gatt_attr_read(conn, attr, buf, len, offset, name, + strlen(name)); +} + +static void player_name_cfg_changed(const struct bt_gatt_attr *attr, + uint16_t value) +{ + BT_DBG("value 0x%04x", value); +} + +#ifdef CONFIG_BT_OTS +static ssize_t read_icon_id(struct bt_conn *conn, + const struct bt_gatt_attr *attr, void *buf, + uint16_t len, uint16_t offset) +{ + uint64_t icon_id = media_proxy_sctrl_get_icon_id(); + + BT_DBG_OBJ_ID("Icon object read: ", icon_id); + return bt_gatt_attr_read(conn, attr, buf, len, offset, &icon_id, + BT_OTS_OBJ_ID_SIZE); +} +#endif /* CONFIG_BT_OTS */ + +static ssize_t read_icon_url(struct bt_conn *conn, + const struct bt_gatt_attr *attr, void *buf, + uint16_t len, uint16_t offset) +{ + const char *url = media_proxy_sctrl_get_icon_url(); + + BT_DBG("Icon URL read, offset: %d, len:%d, URL: %s", offset, len, + log_strdup(url)); + + return bt_gatt_attr_read(conn, attr, buf, len, offset, url, + strlen(url)); +} + +static void track_cfg_changed(const struct bt_gatt_attr *attr, uint16_t value) +{ + BT_DBG("value 0x%04x", value); +} + +static ssize_t read_track_title(struct bt_conn *conn, + const struct bt_gatt_attr *attr, + void *buf, uint16_t len, uint16_t offset) +{ + const char *title = media_proxy_sctrl_get_track_title(); + + BT_DBG("Track title read, offset: %d, len:%d, title: %s", offset, len, + log_strdup(title)); + + return bt_gatt_attr_read(conn, attr, buf, len, offset, title, + strlen(title)); +} + +static void track_title_cfg_changed(const struct bt_gatt_attr *attr, + uint16_t value) +{ + BT_DBG("value 0x%04x", value); +} + +static ssize_t read_track_duration(struct bt_conn *conn, + const struct bt_gatt_attr *attr, void *buf, + uint16_t len, uint16_t offset) +{ + int32_t duration = media_proxy_sctrl_get_track_duration(); + + BT_DBG("Track duration read: %d (0x%08x)", duration, duration); + + return bt_gatt_attr_read(conn, attr, buf, len, offset, &duration, + sizeof(duration)); +} + +static void track_duration_cfg_changed(const struct bt_gatt_attr *attr, + uint16_t value) +{ + BT_DBG("value 0x%04x", value); +} + +static ssize_t read_track_position(struct bt_conn *conn, + const struct bt_gatt_attr *attr, void *buf, + uint16_t len, uint16_t offset) +{ + int32_t position = media_proxy_sctrl_get_track_position(); + + BT_DBG("Track position read: %d (0x%08x)", position, position); + + return bt_gatt_attr_read(conn, attr, buf, len, offset, &position, + sizeof(position)); +} + +static ssize_t write_track_position(struct bt_conn *conn, + const struct bt_gatt_attr *attr, + const void *buf, uint16_t len, + uint16_t offset, uint8_t flags) +{ + int32_t position; + + if (offset != 0) { + return BT_GATT_ERR(BT_ATT_ERR_INVALID_OFFSET); + } + if (len != sizeof(position)) { + return BT_GATT_ERR(BT_ATT_ERR_INVALID_ATTRIBUTE_LEN); + } + + memcpy(&position, buf, len); + + media_proxy_sctrl_set_track_position(position); + + BT_DBG("Track position write: %d", position); + + return len; +} + +static void track_position_cfg_changed(const struct bt_gatt_attr *attr, + uint16_t value) +{ + BT_DBG("value 0x%04x", value); +} + +static ssize_t read_playback_speed(struct bt_conn *conn, + const struct bt_gatt_attr *attr, void *buf, + uint16_t len, uint16_t offset) +{ + int8_t speed = media_proxy_sctrl_get_playback_speed(); + + BT_DBG("Playback speed read: %d", speed); + + return bt_gatt_attr_read(conn, attr, buf, len, offset, &speed, + sizeof(speed)); +} + +static ssize_t write_playback_speed(struct bt_conn *conn, + const struct bt_gatt_attr *attr, + const void *buf, uint16_t len, uint16_t offset, + uint8_t flags) +{ + int8_t speed; + + if (offset != 0) { + return BT_GATT_ERR(BT_ATT_ERR_INVALID_OFFSET); + } + if (len != sizeof(speed)) { + return BT_GATT_ERR(BT_ATT_ERR_INVALID_ATTRIBUTE_LEN); + } + + memcpy(&speed, buf, len); + + media_proxy_sctrl_set_playback_speed(speed); + + BT_DBG("Playback speed write: %d", speed); + + return len; +} + +static void playback_speed_cfg_changed(const struct bt_gatt_attr *attr, + uint16_t value) +{ + BT_DBG("value 0x%04x", value); +} + +static ssize_t read_seeking_speed(struct bt_conn *conn, + const struct bt_gatt_attr *attr, void *buf, + uint16_t len, uint16_t offset) +{ + int8_t speed = media_proxy_sctrl_get_seeking_speed(); + + BT_DBG("Seeking speed read: %d", speed); + + return bt_gatt_attr_read(conn, attr, buf, len, offset, &speed, + sizeof(speed)); +} + +static void seeking_speed_cfg_changed(const struct bt_gatt_attr *attr, + uint16_t value) +{ + BT_DBG("value 0x%04x", value); +} + +#ifdef CONFIG_BT_OTS +static ssize_t read_track_segments_id(struct bt_conn *conn, + const struct bt_gatt_attr *attr, + void *buf, uint16_t len, uint16_t offset) +{ + uint64_t track_segments_id = media_proxy_sctrl_get_track_segments_id(); + + BT_DBG_OBJ_ID("Track segments ID read: ", track_segments_id); + return bt_gatt_attr_read(conn, attr, buf, len, offset, + &track_segments_id, BT_OTS_OBJ_ID_SIZE); +} + +static ssize_t read_current_track_id(struct bt_conn *conn, + const struct bt_gatt_attr *attr, void *buf, + uint16_t len, uint16_t offset) +{ + uint64_t track_id = media_proxy_sctrl_get_current_track_id(); + + BT_DBG_OBJ_ID("Current track ID read: ", track_id); + return bt_gatt_attr_read(conn, attr, buf, len, offset, &track_id, + BT_OTS_OBJ_ID_SIZE); +} + +static ssize_t write_current_track_id(struct bt_conn *conn, + const struct bt_gatt_attr *attr, + const void *buf, uint16_t len, uint16_t offset, + uint8_t flags) +{ + uint64_t id; + + if (offset != 0) { + BT_DBG("Invalid offset"); + return BT_GATT_ERR(BT_ATT_ERR_INVALID_OFFSET); + } + + if (len != BT_OTS_OBJ_ID_SIZE) { + BT_DBG("Invalid length"); + return BT_GATT_ERR(BT_ATT_ERR_INVALID_ATTRIBUTE_LEN); + } + + id = sys_get_le48((uint8_t *)buf); + + if (IS_ENABLED(CONFIG_BT_DEBUG_MCS)) { + char str[BT_OTS_OBJ_ID_STR_LEN]; + (void)bt_ots_obj_id_to_str(id, str, sizeof(str)); + BT_DBG("Current track write: offset: %d, len: %d, track ID: %s", + offset, len, log_strdup(str)); + } + + media_proxy_sctrl_set_current_track_id(id); + + return BT_OTS_OBJ_ID_SIZE; +} + +static void current_track_id_cfg_changed(const struct bt_gatt_attr *attr, + uint16_t value) +{ + BT_DBG("value 0x%04x", value); +} + +static ssize_t read_next_track_id(struct bt_conn *conn, + const struct bt_gatt_attr *attr, void *buf, + uint16_t len, uint16_t offset) +{ + uint64_t track_id = media_proxy_sctrl_get_next_track_id(); + + if (track_id == MPL_NO_TRACK_ID) { + BT_DBG("Next track read, but it is empty"); + /* "If the media player has no next track, the length of the */ + /* characteristic shall be zero." */ + return bt_gatt_attr_read(conn, attr, buf, len, offset, NULL, 0); + } + + BT_DBG_OBJ_ID("Next track read: ", track_id); + return bt_gatt_attr_read(conn, attr, buf, len, offset, + &track_id, BT_OTS_OBJ_ID_SIZE); +} + +static ssize_t write_next_track_id(struct bt_conn *conn, + const struct bt_gatt_attr *attr, + const void *buf, uint16_t len, uint16_t offset, + uint8_t flags) +{ + uint64_t id; + + if (offset != 0) { + BT_DBG("Invalid offset"); + return BT_GATT_ERR(BT_ATT_ERR_INVALID_OFFSET); + } + + if (len != BT_OTS_OBJ_ID_SIZE) { + BT_DBG("Invalid length"); + return BT_GATT_ERR(BT_ATT_ERR_INVALID_ATTRIBUTE_LEN); + } + + id = sys_get_le48((uint8_t *)buf); + + if (IS_ENABLED(CONFIG_BT_DEBUG_MCS)) { + char str[BT_OTS_OBJ_ID_STR_LEN]; + (void)bt_ots_obj_id_to_str(id, str, sizeof(str)); + BT_DBG("Next track write: offset: %d, len: %d, track ID: %s", + offset, len, log_strdup(str)); + } + + media_proxy_sctrl_set_next_track_id(id); + + return BT_OTS_OBJ_ID_SIZE; +} + +static void next_track_id_cfg_changed(const struct bt_gatt_attr *attr, + uint16_t value) +{ + BT_DBG("value 0x%04x", value); +} + +static ssize_t read_parent_group_id(struct bt_conn *conn, + const struct bt_gatt_attr *attr, void *buf, + uint16_t len, uint16_t offset) +{ + uint64_t group_id = media_proxy_sctrl_get_parent_group_id(); + + BT_DBG_OBJ_ID("Parent group read: ", group_id); + return bt_gatt_attr_read(conn, attr, buf, len, offset, &group_id, + BT_OTS_OBJ_ID_SIZE); +} + +static void parent_group_id_cfg_changed(const struct bt_gatt_attr *attr, + uint16_t value) +{ + BT_DBG("value 0x%04x", value); +} + +static ssize_t read_current_group_id(struct bt_conn *conn, + const struct bt_gatt_attr *attr, void *buf, + uint16_t len, uint16_t offset) +{ + uint64_t group_id = media_proxy_sctrl_get_current_group_id(); + + BT_DBG_OBJ_ID("Current group read: ", group_id); + return bt_gatt_attr_read(conn, attr, buf, len, offset, &group_id, + BT_OTS_OBJ_ID_SIZE); +} + +static ssize_t write_current_group_id(struct bt_conn *conn, + const struct bt_gatt_attr *attr, + const void *buf, uint16_t len, uint16_t offset, + uint8_t flags) +{ + uint64_t id; + + if (offset != 0) { + BT_DBG("Invalid offset"); + return BT_GATT_ERR(BT_ATT_ERR_INVALID_OFFSET); + } + + if (len != BT_OTS_OBJ_ID_SIZE) { + BT_DBG("Invalid length"); + return BT_GATT_ERR(BT_ATT_ERR_INVALID_ATTRIBUTE_LEN); + } + + id = sys_get_le48((uint8_t *)buf); + + if (IS_ENABLED(CONFIG_BT_DEBUG_MCS)) { + char str[BT_OTS_OBJ_ID_STR_LEN]; + (void)bt_ots_obj_id_to_str(id, str, sizeof(str)); + BT_DBG("Current group ID write: offset: %d, len: %d, track ID: %s", + offset, len, log_strdup(str)); + } + + media_proxy_sctrl_set_current_group_id(id); + + return BT_OTS_OBJ_ID_SIZE; +} + +static void current_group_id_cfg_changed(const struct bt_gatt_attr *attr, uint16_t value) +{ + BT_DBG("value 0x%04x", value); +} +#endif /* CONFIG_BT_OTS */ + +static ssize_t read_playing_order(struct bt_conn *conn, + const struct bt_gatt_attr *attr, void *buf, + uint16_t len, uint16_t offset) +{ + uint8_t order = media_proxy_sctrl_get_playing_order(); + + BT_DBG("Playing order read: %d (0x%02x)", order, order); + + return bt_gatt_attr_read(conn, attr, buf, len, offset, &order, + sizeof(order)); +} + +static ssize_t write_playing_order(struct bt_conn *conn, + const struct bt_gatt_attr *attr, + const void *buf, uint16_t len, uint16_t offset, + uint8_t flags) +{ + BT_DBG("Playing order write"); + + int8_t order; + + if (offset != 0) { + return BT_GATT_ERR(BT_ATT_ERR_INVALID_OFFSET); + } + if (len != sizeof(order)) { + return BT_GATT_ERR(BT_ATT_ERR_INVALID_ATTRIBUTE_LEN); + } + + memcpy(&order, buf, len); + + media_proxy_sctrl_set_playing_order(order); + + BT_DBG("Playing order write: %d", order); + + return len; +} + +static void playing_order_cfg_changed(const struct bt_gatt_attr *attr, + uint16_t value) +{ + BT_DBG("value 0x%04x", value); +} + +static ssize_t read_playing_orders_supported(struct bt_conn *conn, + const struct bt_gatt_attr *attr, + void *buf, uint16_t len, uint16_t offset) +{ + uint16_t orders = media_proxy_sctrl_get_playing_orders_supported(); + + BT_DBG("Playing orders read: %d (0x%04x)", orders, orders); + + return bt_gatt_attr_read(conn, attr, buf, len, offset, &orders, + sizeof(orders)); +} + +static ssize_t read_media_state(struct bt_conn *conn, + const struct bt_gatt_attr *attr, void *buf, + uint16_t len, uint16_t offset) +{ + uint8_t state = media_proxy_sctrl_get_media_state(); + + BT_DBG("Media state read: %d", state); + + return bt_gatt_attr_read(conn, attr, buf, len, offset, &state, + sizeof(state)); +} + +static void media_state_cfg_changed(const struct bt_gatt_attr *attr, + uint16_t value) +{ + BT_DBG("value 0x%04x", value); +} + +static ssize_t write_control_point(struct bt_conn *conn, + const struct bt_gatt_attr *attr, + const void *buf, uint16_t len, uint16_t offset, + uint8_t flags) +{ + struct mpl_cmd command; + + if (offset != 0) { + return BT_GATT_ERR(BT_ATT_ERR_INVALID_OFFSET); + } + + if (len != sizeof(command.opcode) && + len != sizeof(command.opcode) + sizeof(command.param)) { + return BT_GATT_ERR(BT_ATT_ERR_INVALID_ATTRIBUTE_LEN); + } + + memcpy(&command.opcode, buf, sizeof(command.opcode)); + BT_DBG("Opcode: %d", command.opcode); + command.use_param = false; + + if (len == sizeof(command.opcode) + sizeof(command.param)) { + memcpy(&command.param, + (char *)buf + sizeof(command.opcode), + sizeof(command.param)); + command.use_param = true; + BT_DBG("Parameter: %d", command.param); + } + + media_proxy_sctrl_send_command(command); + + return len; +} + +static void control_point_cfg_changed(const struct bt_gatt_attr *attr, + uint16_t value) +{ + BT_DBG("value 0x%04x", value); +} + +static ssize_t read_opcodes_supported(struct bt_conn *conn, + const struct bt_gatt_attr *attr, + void *buf, uint16_t len, uint16_t offset) +{ + uint32_t opcodes = media_proxy_sctrl_get_commands_supported(); + + BT_DBG("Opcodes_supported read: %d (0x%08x)", opcodes, opcodes); + + return bt_gatt_attr_read(conn, attr, buf, len, offset, + &opcodes, BT_MCS_OPCODES_SUPPORTED_LEN); +} + +static void opcodes_supported_cfg_changed(const struct bt_gatt_attr *attr, + uint16_t value) +{ + BT_DBG("value 0x%04x", value); +} + +#ifdef CONFIG_BT_OTS +static ssize_t write_search_control_point(struct bt_conn *conn, + const struct bt_gatt_attr *attr, + const void *buf, uint16_t len, + uint16_t offset, uint8_t flags) +{ + struct mpl_search search = {0}; + + if (offset != 0) { + return BT_GATT_ERR(BT_ATT_ERR_INVALID_OFFSET); + } + + if (len > SEARCH_LEN_MAX || len < SEARCH_LEN_MIN) { + return BT_GATT_ERR(BT_ATT_ERR_INVALID_ATTRIBUTE_LEN); + } + + memcpy(&search.search, (char *)buf, len); + search.len = len; + BT_DBG("Search length: %d", len); + BT_HEXDUMP_DBG(&search.search, search.len, "Search content"); + + media_proxy_sctrl_send_search(search); + + return len; +} + +static void search_control_point_cfg_changed(const struct bt_gatt_attr *attr, + uint16_t value) +{ + BT_DBG("value 0x%04x", value); +} + +static ssize_t read_search_results_id(struct bt_conn *conn, + const struct bt_gatt_attr *attr, + void *buf, uint16_t len, uint16_t offset) +{ + uint64_t search_id = media_proxy_sctrl_get_search_results_id(); + + BT_DBG_OBJ_ID("Search results id read: ", search_id); + + /* TODO: The permanent solution here should be that the call to */ + /* mpl should fill the UUID in a pointed-to value, and return a */ + /* length or an error code, to indicate whether this ID has a */ + /* value now. This should be done for all functions of this kind. */ + /* For now, fix the issue here - send zero-length data if the */ + /* ID is zero. */ + /* *Spec requirement - IDs may not be valid, in which case the */ + /* characteristic shall be zero length. */ + + if (search_id == 0) { + return bt_gatt_attr_read(conn, attr, buf, len, offset, + NULL, 0); + } else { + return bt_gatt_attr_read(conn, attr, buf, len, offset, + &search_id, BT_OTS_OBJ_ID_SIZE); + } +} + +static void search_results_id_cfg_changed(const struct bt_gatt_attr *attr, + uint16_t value) +{ + BT_DBG("value 0x%04x", value); +} +#endif /* CONFIG_BT_OTS */ + +static ssize_t read_content_ctrl_id(struct bt_conn *conn, + const struct bt_gatt_attr *attr, void *buf, + uint16_t len, uint16_t offset) +{ + uint8_t id = media_proxy_sctrl_get_content_ctrl_id(); + + BT_DBG("Content control ID read: %d", id); + + return bt_gatt_attr_read(conn, attr, buf, len, offset, &id, + sizeof(id)); +} + +/* Defines for OTS-dependent characteristics - empty if no OTS */ +#ifdef CONFIG_BT_OTS +#define ICON_OBJ_ID_CHARACTERISTIC_IF_OTS \ + BT_GATT_CHARACTERISTIC(BT_UUID_MCS_ICON_OBJ_ID, \ + BT_GATT_CHRC_READ, BT_GATT_PERM_READ_ENCRYPT, \ + read_icon_id, NULL, NULL), +#define SEGMENTS_TRACK_GROUP_ID_CHARACTERISTICS_IF_OTS \ + BT_GATT_CHARACTERISTIC(BT_UUID_MCS_TRACK_SEGMENTS_OBJ_ID, \ + BT_GATT_CHRC_READ, BT_GATT_PERM_READ_ENCRYPT, \ + read_track_segments_id, NULL, NULL), \ + BT_GATT_CHARACTERISTIC(BT_UUID_MCS_CURRENT_TRACK_OBJ_ID, \ + BT_GATT_CHRC_READ | BT_GATT_CHRC_WRITE | \ + BT_GATT_CHRC_WRITE_WITHOUT_RESP | \ + BT_GATT_CHRC_NOTIFY, \ + BT_GATT_PERM_READ_ENCRYPT | \ + BT_GATT_PERM_WRITE_ENCRYPT, \ + read_current_track_id, write_current_track_id, \ + NULL), \ + BT_GATT_CCC(current_track_id_cfg_changed, \ + BT_GATT_PERM_READ | BT_GATT_PERM_WRITE_ENCRYPT), \ + BT_GATT_CHARACTERISTIC(BT_UUID_MCS_NEXT_TRACK_OBJ_ID, \ + BT_GATT_CHRC_READ | BT_GATT_CHRC_WRITE | \ + BT_GATT_CHRC_WRITE_WITHOUT_RESP | \ + BT_GATT_CHRC_NOTIFY, \ + BT_GATT_PERM_READ_ENCRYPT | \ + BT_GATT_PERM_WRITE_ENCRYPT, \ + read_next_track_id, write_next_track_id, NULL), \ + BT_GATT_CCC(next_track_id_cfg_changed, \ + BT_GATT_PERM_READ | BT_GATT_PERM_WRITE_ENCRYPT), \ + BT_GATT_CHARACTERISTIC(BT_UUID_MCS_PARENT_GROUP_OBJ_ID, \ + BT_GATT_CHRC_READ | BT_GATT_CHRC_NOTIFY, \ + BT_GATT_PERM_READ_ENCRYPT, \ + read_parent_group_id, NULL, NULL), \ + BT_GATT_CCC(parent_group_id_cfg_changed, \ + BT_GATT_PERM_READ | BT_GATT_PERM_WRITE_ENCRYPT), \ + BT_GATT_CHARACTERISTIC(BT_UUID_MCS_CURRENT_GROUP_OBJ_ID, \ + BT_GATT_CHRC_READ | BT_GATT_CHRC_WRITE | \ + BT_GATT_CHRC_WRITE_WITHOUT_RESP | \ + BT_GATT_CHRC_NOTIFY, \ + BT_GATT_PERM_READ_ENCRYPT | \ + BT_GATT_PERM_WRITE_ENCRYPT, \ + read_current_group_id, write_current_group_id, NULL), \ + BT_GATT_CCC(current_group_id_cfg_changed, \ + BT_GATT_PERM_READ | BT_GATT_PERM_WRITE_ENCRYPT), +#define SEARCH_CHARACTERISTICS_IF_OTS \ + BT_GATT_CHARACTERISTIC(BT_UUID_MCS_SEARCH_CONTROL_POINT, \ + BT_GATT_CHRC_WRITE | \ + BT_GATT_CHRC_WRITE_WITHOUT_RESP | \ + BT_GATT_CHRC_NOTIFY, \ + BT_GATT_PERM_WRITE_ENCRYPT, \ + NULL, write_search_control_point, NULL), \ + BT_GATT_CCC(search_control_point_cfg_changed, \ + BT_GATT_PERM_READ | BT_GATT_PERM_WRITE_ENCRYPT), \ + BT_GATT_CHARACTERISTIC(BT_UUID_MCS_SEARCH_RESULTS_OBJ_ID, \ + BT_GATT_CHRC_READ | BT_GATT_CHRC_NOTIFY, \ + BT_GATT_PERM_READ_ENCRYPT, \ + read_search_results_id, NULL, NULL), \ + BT_GATT_CCC(search_results_id_cfg_changed, \ + BT_GATT_PERM_READ | BT_GATT_PERM_WRITE_ENCRYPT), + +#else +#define ICON_OBJ_ID_CHARACTERISTIC_IF_OTS +#define SEGMENTS_TRACK_GROUP_ID_CHARACTERISTICS_IF_OTS +#define SEARCH_CHARACTERISTICS_IF_OTS +#endif /* CONFIG_BT_OTS */ + +/* Media control service attributes */ +#define BT_MCS_SERVICE_DEFINITION \ + BT_GATT_PRIMARY_SERVICE(BT_UUID_GMCS), \ + BT_GATT_INCLUDE_SERVICE(NULL), /* To be overwritten */ \ + BT_GATT_CHARACTERISTIC(BT_UUID_MCS_PLAYER_NAME, \ + BT_GATT_CHRC_READ | BT_GATT_CHRC_NOTIFY, \ + BT_GATT_PERM_READ_ENCRYPT, \ + read_player_name, NULL, NULL), \ + BT_GATT_CCC(player_name_cfg_changed, \ + BT_GATT_PERM_READ | BT_GATT_PERM_WRITE_ENCRYPT), \ + ICON_OBJ_ID_CHARACTERISTIC_IF_OTS \ + BT_GATT_CHARACTERISTIC(BT_UUID_MCS_ICON_URL, \ + BT_GATT_CHRC_READ, BT_GATT_PERM_READ_ENCRYPT, \ + read_icon_url, NULL, NULL), \ + BT_GATT_CHARACTERISTIC(BT_UUID_MCS_TRACK_CHANGED, \ + BT_GATT_CHRC_NOTIFY, BT_GATT_PERM_NONE, \ + NULL, NULL, NULL), \ + BT_GATT_CCC(track_cfg_changed, \ + BT_GATT_PERM_READ | BT_GATT_PERM_WRITE_ENCRYPT), \ + BT_GATT_CHARACTERISTIC(BT_UUID_MCS_TRACK_TITLE, \ + BT_GATT_CHRC_READ | BT_GATT_CHRC_NOTIFY, \ + BT_GATT_PERM_READ_ENCRYPT, \ + read_track_title, NULL, NULL), \ + BT_GATT_CCC(track_title_cfg_changed, \ + BT_GATT_PERM_READ | BT_GATT_PERM_WRITE_ENCRYPT), \ + BT_GATT_CHARACTERISTIC(BT_UUID_MCS_TRACK_DURATION, \ + BT_GATT_CHRC_READ | BT_GATT_CHRC_NOTIFY, \ + BT_GATT_PERM_READ_ENCRYPT, \ + read_track_duration, NULL, NULL), \ + BT_GATT_CCC(track_duration_cfg_changed, \ + BT_GATT_PERM_READ | BT_GATT_PERM_WRITE_ENCRYPT), \ + BT_GATT_CHARACTERISTIC(BT_UUID_MCS_TRACK_POSITION, \ + BT_GATT_CHRC_READ | BT_GATT_CHRC_WRITE | \ + BT_GATT_CHRC_WRITE_WITHOUT_RESP | \ + BT_GATT_CHRC_NOTIFY, \ + BT_GATT_PERM_READ_ENCRYPT | \ + BT_GATT_PERM_WRITE_ENCRYPT, \ + read_track_position, \ + write_track_position, NULL), \ + BT_GATT_CCC(track_position_cfg_changed, \ + BT_GATT_PERM_READ | BT_GATT_PERM_WRITE_ENCRYPT), \ + BT_GATT_CHARACTERISTIC(BT_UUID_MCS_PLAYBACK_SPEED, \ + BT_GATT_CHRC_READ | BT_GATT_CHRC_WRITE | \ + BT_GATT_CHRC_WRITE_WITHOUT_RESP | \ + BT_GATT_CHRC_NOTIFY, \ + BT_GATT_PERM_READ_ENCRYPT | \ + BT_GATT_PERM_WRITE_ENCRYPT, \ + read_playback_speed, write_playback_speed, \ + NULL), \ + BT_GATT_CCC(playback_speed_cfg_changed, \ + BT_GATT_PERM_READ | BT_GATT_PERM_WRITE_ENCRYPT), \ + BT_GATT_CHARACTERISTIC(BT_UUID_MCS_SEEKING_SPEED, \ + BT_GATT_CHRC_READ | BT_GATT_CHRC_NOTIFY, \ + BT_GATT_PERM_READ_ENCRYPT, \ + read_seeking_speed, NULL, NULL), \ + BT_GATT_CCC(seeking_speed_cfg_changed, \ + BT_GATT_PERM_READ | BT_GATT_PERM_WRITE_ENCRYPT), \ + SEGMENTS_TRACK_GROUP_ID_CHARACTERISTICS_IF_OTS \ + BT_GATT_CHARACTERISTIC(BT_UUID_MCS_PLAYING_ORDER, \ + BT_GATT_CHRC_READ | BT_GATT_CHRC_WRITE | \ + BT_GATT_CHRC_WRITE_WITHOUT_RESP | \ + BT_GATT_CHRC_NOTIFY, \ + BT_GATT_PERM_READ_ENCRYPT | \ + BT_GATT_PERM_WRITE_ENCRYPT, \ + read_playing_order, write_playing_order, NULL), \ + BT_GATT_CCC(playing_order_cfg_changed, \ + BT_GATT_PERM_READ | BT_GATT_PERM_WRITE_ENCRYPT), \ + BT_GATT_CHARACTERISTIC(BT_UUID_MCS_PLAYING_ORDERS, \ + BT_GATT_CHRC_READ, BT_GATT_PERM_READ_ENCRYPT, \ + read_playing_orders_supported, NULL, NULL), \ + BT_GATT_CHARACTERISTIC(BT_UUID_MCS_MEDIA_STATE, \ + BT_GATT_CHRC_READ | BT_GATT_CHRC_NOTIFY, \ + BT_GATT_PERM_READ_ENCRYPT, \ + read_media_state, NULL, NULL), \ + BT_GATT_CCC(media_state_cfg_changed, \ + BT_GATT_PERM_READ | BT_GATT_PERM_WRITE_ENCRYPT), \ + BT_GATT_CHARACTERISTIC(BT_UUID_MCS_MEDIA_CONTROL_POINT, \ + BT_GATT_CHRC_WRITE | \ + BT_GATT_CHRC_WRITE_WITHOUT_RESP | \ + BT_GATT_CHRC_NOTIFY, \ + BT_GATT_PERM_WRITE_ENCRYPT, \ + NULL, write_control_point, NULL), \ + BT_GATT_CCC(control_point_cfg_changed, \ + BT_GATT_PERM_READ | BT_GATT_PERM_WRITE_ENCRYPT), \ + BT_GATT_CHARACTERISTIC(BT_UUID_MCS_MEDIA_CONTROL_OPCODES, \ + BT_GATT_CHRC_READ | BT_GATT_CHRC_NOTIFY, \ + BT_GATT_PERM_READ_ENCRYPT, \ + read_opcodes_supported, NULL, NULL), \ + BT_GATT_CCC(opcodes_supported_cfg_changed, \ + BT_GATT_PERM_READ | BT_GATT_PERM_WRITE_ENCRYPT), \ + SEARCH_CHARACTERISTICS_IF_OTS \ + BT_GATT_CHARACTERISTIC(BT_UUID_CCID, \ + BT_GATT_CHRC_READ, \ + BT_GATT_PERM_READ_ENCRYPT, \ + read_content_ctrl_id, NULL, NULL) + +static struct bt_gatt_attr svc_attrs[] = { BT_MCS_SERVICE_DEFINITION }; +static struct bt_gatt_service mcs; +#ifdef CONFIG_BT_OTS +static struct bt_ots *ots; +#endif /* CONFIG_BT_OTS */ + +#ifdef CONFIG_BT_OTS +struct bt_ots *bt_mcs_get_ots(void) +{ + return ots; +} +#endif /* CONFIG_BT_OTS */ + +/* Callback functions from the media player, notifying attributes */ +/* Placed here, after the service definition, because they reference it. */ + +/* Helper function to shorten functions that notify */ +static void notify(const struct bt_uuid *uuid, const void *data, uint16_t len) +{ + int err = bt_gatt_notify_uuid(NULL, uuid, mcs.attrs, data, len); + + if (err) { + if (err == -ENOTCONN) { + BT_DBG("Notification error: ENOTCONN (%d)", err); + } else { + BT_ERR("Notification error: %d", err); + } + } +} + +void media_proxy_sctrl_track_changed_cb(void) +{ + BT_DBG("Notifying track change"); + notify(BT_UUID_MCS_TRACK_CHANGED, NULL, 0); +} + +void media_proxy_sctrl_track_title_cb(const char *title) +{ + BT_DBG("Notifying track title: %s", log_strdup(title)); + notify(BT_UUID_MCS_TRACK_TITLE, title, strlen(title)); +} + +void media_proxy_sctrl_track_position_cb(int32_t position) +{ + BT_DBG("Notifying track position: %d", position); + notify(BT_UUID_MCS_TRACK_POSITION, &position, sizeof(position)); +} + +void media_proxy_sctrl_track_duration_cb(int32_t duration) +{ + BT_DBG("Notifying track duration: %d", duration); + notify(BT_UUID_MCS_TRACK_DURATION, &duration, sizeof(duration)); +} + +void media_proxy_sctrl_playback_speed_cb(int8_t speed) +{ + BT_DBG("Notifying playback speed: %d", speed); + notify(BT_UUID_MCS_PLAYBACK_SPEED, &speed, sizeof(speed)); +} + +void media_proxy_sctrl_seeking_speed_cb(int8_t speed) +{ + BT_DBG("Notifying seeking speed: %d", speed); + notify(BT_UUID_MCS_SEEKING_SPEED, &speed, sizeof(speed)); +} + +void media_proxy_sctrl_current_track_id_cb(uint64_t id) +{ + BT_DBG_OBJ_ID("Notifying current track ID: ", id); + notify(BT_UUID_MCS_CURRENT_TRACK_OBJ_ID, &id, BT_OTS_OBJ_ID_SIZE); +} + +void media_proxy_sctrl_next_track_id_cb(uint64_t id) +{ + if (id == MPL_NO_TRACK_ID) { + /* "If the media player has no next track, the length of the */ + /* characteristic shall be zero." */ + BT_DBG_OBJ_ID("Notifying EMPTY next track ID: ", id); + notify(BT_UUID_MCS_NEXT_TRACK_OBJ_ID, NULL, 0); + } else { + BT_DBG_OBJ_ID("Notifying next track ID: ", id); + notify(BT_UUID_MCS_NEXT_TRACK_OBJ_ID, &id, BT_OTS_OBJ_ID_SIZE); + } +} + +void media_proxy_sctrl_parent_group_id_cb(uint64_t id) +{ + BT_DBG_OBJ_ID("Notifying parent group ID: ", id); + notify(BT_UUID_MCS_PARENT_GROUP_OBJ_ID, &id, BT_OTS_OBJ_ID_SIZE); +} + +void media_proxy_sctrl_current_group_id_cb(uint64_t id) +{ + BT_DBG_OBJ_ID("Notifying current group ID: ", id); + notify(BT_UUID_MCS_CURRENT_GROUP_OBJ_ID, &id, BT_OTS_OBJ_ID_SIZE); +} + +void media_proxy_sctrl_playing_order_cb(uint8_t order) +{ + BT_DBG("Notifying playing order: %d", order); + notify(BT_UUID_MCS_PLAYING_ORDER, &order, sizeof(order)); +} + +void media_proxy_sctrl_media_state_cb(uint8_t state) +{ + BT_DBG("Notifying media state: %d", state); + notify(BT_UUID_MCS_MEDIA_STATE, &state, sizeof(state)); +} + +void media_proxy_sctrl_command_cb(struct mpl_cmd_ntf cmd_ntf) +{ + BT_DBG("Notifying control point command - opcode: %d, result: %d", + cmd_ntf.requested_opcode, cmd_ntf.result_code); + notify(BT_UUID_MCS_MEDIA_CONTROL_POINT, &cmd_ntf, sizeof(cmd_ntf)); +} + +void media_proxy_sctrl_commands_supported_cb(uint32_t opcodes) +{ + BT_DBG("Notifying command opcodes supported: %d (0x%08x)", opcodes, + opcodes); + notify(BT_UUID_MCS_MEDIA_CONTROL_OPCODES, &opcodes, + BT_MCS_OPCODES_SUPPORTED_LEN); +} + +void media_proxy_sctrl_search_cb(uint8_t result_code) +{ + BT_DBG("Notifying search control point - result: %d", result_code); + notify(BT_UUID_MCS_SEARCH_CONTROL_POINT, &result_code, + sizeof(result_code)); +} + +void media_proxy_sctrl_search_results_id_cb(uint64_t id) +{ + BT_DBG_OBJ_ID("Notifying search results ID: ", id); + notify(BT_UUID_MCS_SEARCH_RESULTS_OBJ_ID, &id, BT_OTS_OBJ_ID_SIZE); +} + +/* Register the service */ +int bt_mcs_init(struct bt_ots_cb *ots_cbs) +{ + static bool initialized; + int err; + + if (initialized) { + BT_DBG("Already initialized"); + return -EALREADY; + } + + + mcs = (struct bt_gatt_service)BT_GATT_SERVICE(svc_attrs); + +#ifdef CONFIG_BT_OTS + struct bt_ots_init ots_init; + + ots = bt_ots_free_instance_get(); + if (!ots) { + BT_ERR("Failed to retrieve OTS instance\n"); + return -ENOMEM; + } + + /* Configure OTS initialization. */ + memset(&ots_init, 0, sizeof(ots_init)); + BT_OTS_OACP_SET_FEAT_READ(ots_init.features.oacp); + BT_OTS_OLCP_SET_FEAT_GO_TO(ots_init.features.olcp); + ots_init.cb = ots_cbs; + + /* Initialize OTS instance. */ + err = bt_ots_init(ots, &ots_init); + if (err) { + BT_ERR("Failed to init OTS (err:%d)\n", err); + return err; + } + + /* TODO: Maybe the user_data pointer can be in a different way */ + for (int i = 0; i < mcs.attr_count; i++) { + if (!bt_uuid_cmp(mcs.attrs[i].uuid, BT_UUID_GATT_INCLUDE)) { + mcs.attrs[i].user_data = bt_ots_svc_decl_get(ots); + } + } +#endif /* CONFIG_BT_OTS */ + + err = bt_gatt_service_register(&mcs); + + if (err) { + BT_ERR("Could not register the MCS service"); +#ifdef CONFIG_BT_OTS + /* TODO: How does one un-register the OTS? */ +#endif /* CONFIG_BT_OTS */ + return -ENOEXEC; + } + + /* Set up the callback structure */ + cbs.track_changed = media_proxy_sctrl_track_changed_cb; + cbs.track_title = media_proxy_sctrl_track_title_cb; + cbs.track_duration = media_proxy_sctrl_track_duration_cb; + cbs.track_position = media_proxy_sctrl_track_position_cb; + cbs.playback_speed = media_proxy_sctrl_playback_speed_cb; + cbs.seeking_speed = media_proxy_sctrl_seeking_speed_cb; +#ifdef CONFIG_BT_OTS + cbs.current_track_id = media_proxy_sctrl_current_track_id_cb; + cbs.next_track_id = media_proxy_sctrl_next_track_id_cb; + cbs.parent_group_id = media_proxy_sctrl_parent_group_id_cb; + cbs.current_group_id = media_proxy_sctrl_current_group_id_cb; +#endif /* CONFIG_BT_OTS */ + cbs.playing_order = media_proxy_sctrl_playing_order_cb; + cbs.media_state = media_proxy_sctrl_media_state_cb; + cbs.command = media_proxy_sctrl_command_cb; + cbs.commands_supported = media_proxy_sctrl_commands_supported_cb; +#ifdef CONFIG_BT_OTS + cbs.search = media_proxy_sctrl_search_cb; + cbs.search_results_id = media_proxy_sctrl_search_results_id_cb; +#endif /* CONFIG_BT_OTS */ + + media_proxy_sctrl_register(&cbs); + + initialized = true; + return 0; +} diff --git a/subsys/bluetooth/audio/mcs_internal.h b/subsys/bluetooth/audio/mcs_internal.h new file mode 100644 index 00000000000..1dfda65ab7e --- /dev/null +++ b/subsys/bluetooth/audio/mcs_internal.h @@ -0,0 +1,25 @@ +/* @file + * @brief Media Control Service internal header file + * + * Copyright (c) 2020 - 2021 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: Apache-2.0 + */ +#ifndef ZEPHYR_SUBSYS_BLUETOOTH_HOST_AUDIO_MCS_INTERNAL_H_ +#define ZEPHYR_SUBSYS_BLUETOOTH_HOST_AUDIO_MCS_INTERNAL_H_ + +#include +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +int bt_mcs_init(struct bt_ots_cb *ots_cbs); + +#ifdef __cplusplus +} +#endif + +#endif /* ZEPHYR_SUBSYS_BLUETOOTH_HOST_AUDIO_MCS_INTERNAL_H_ */ diff --git a/subsys/bluetooth/audio/mpl.c b/subsys/bluetooth/audio/mpl.c new file mode 100644 index 00000000000..d37be0d9a8b --- /dev/null +++ b/subsys/bluetooth/audio/mpl.c @@ -0,0 +1,2923 @@ +/* Media player skeleton implementation */ + +/* + * Copyright (c) 2019 - 2021 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include + +#include +#include + +#include "media_proxy_internal.h" +#include "mpl_internal.h" + +#define BT_DBG_ENABLED IS_ENABLED(CONFIG_BT_DEBUG_MCS) +#define LOG_MODULE_NAME bt_mpl +#include "common/log.h" +#include "ccid_internal.h" +#include "mcs_internal.h" + +#define TRACK_STATUS_INVALID 0x00 +#define TRACK_STATUS_VALID 0x01 + +#define PLAYBACK_SPEED_PARAM_DEFAULT BT_MCS_PLAYBACK_SPEED_UNITY + +/* Temporary hardcoded setup for groups, tracks and segements */ +/* There is one parent group, which is the parent of a number of groups. */ +/* The groups have a number of tracks. */ +/* (There is only one level of groups, there are no groups of groups.) */ +/* The first track of the first group has track segments, other tracks not. */ + +/* Track segments */ +static struct mpl_tseg seg_2; +static struct mpl_tseg seg_3; + +static struct mpl_tseg seg_1 = { + .name_len = 5, + .name = "Start", + .pos = 0, + .prev = NULL, + .next = &seg_2, +}; + +static struct mpl_tseg seg_2 = { + .name_len = 6, + .name = "Middle", + .pos = 2000, + .prev = &seg_1, + .next = &seg_3, +}; + +static struct mpl_tseg seg_3 = { + .name_len = 3, + .name = "End", + .pos = 5000, + .prev = &seg_2, + .next = NULL, +}; + +static struct mpl_track track_1_2; +static struct mpl_track track_1_3; +static struct mpl_track track_1_4; +static struct mpl_track track_1_5; + +/* Tracks */ +static struct mpl_track track_1_1 = { + .title = "Interlude #1 (Song for Alison)", + .duration = 6300, + .segment = &seg_1, + .prev = NULL, + .next = &track_1_2, +}; + + +static struct mpl_track track_1_2 = { + .title = "Interlude #2 (For Bobbye)", + .duration = 7500, + .segment = NULL, + .prev = &track_1_1, + .next = &track_1_3, +}; + +static struct mpl_track track_1_3 = { + .title = "Interlude #3 (Levanto Seventy)", + .duration = 7800, + .segment = NULL, + .prev = &track_1_2, + .next = &track_1_4, +}; + +static struct mpl_track track_1_4 = { + .title = "Interlude #4 (Vesper Dreams)", + .duration = 13500, + .segment = NULL, + .prev = &track_1_3, + .next = &track_1_5, +}; + +static struct mpl_track track_1_5 = { + .title = "Interlude #5 (Shasti)", + .duration = 7500, + .segment = NULL, + .prev = &track_1_4, + .next = NULL, +}; + +static struct mpl_track track_2_2; +static struct mpl_track track_2_3; + +static struct mpl_track track_2_1 = { + .title = "Track 2.1", + .duration = 30000, + .segment = NULL, + .prev = NULL, + .next = &track_2_2, +}; + +static struct mpl_track track_2_2 = { + .title = "Track 2.2", + .duration = 30000, + .segment = NULL, + .prev = &track_2_1, + .next = &track_2_3, +}; + +static struct mpl_track track_2_3 = { + .title = "Track 2.3", + .duration = 30000, + .segment = NULL, + .prev = &track_2_2, + .next = NULL, +}; + +static struct mpl_track track_3_2; +static struct mpl_track track_3_3; + +static struct mpl_track track_3_1 = { + .title = "Track 3.1", + .duration = 30000, + .segment = NULL, + .prev = NULL, + .next = &track_3_2, +}; + +static struct mpl_track track_3_2 = { + .title = "Track 3.2", + .duration = 30000, + .segment = NULL, + .prev = &track_3_1, + .next = &track_3_3, +}; + +static struct mpl_track track_3_3 = { + .title = "Track 3.3", + .duration = 30000, + .segment = NULL, + .prev = &track_3_2, + .next = NULL, +}; + +static struct mpl_track track_4_2; + +static struct mpl_track track_4_1 = { + .title = "Track 4.1", + .duration = 30000, + .segment = NULL, + .prev = NULL, + .next = &track_4_2, +}; + +static struct mpl_track track_4_2 = { + .title = "Track 4.2", + .duration = 30000, + .segment = NULL, + .prev = &track_4_1, + .next = NULL, +}; + +/* Groups */ +static struct mpl_group group_2; +static struct mpl_group group_3; +static struct mpl_group group_4; +static struct mpl_group group_p; + +static struct mpl_group group_1 = { + .title = "Joe Pass - Guitar Interludes", + .track = &track_1_1, + .parent = &group_p, + .prev = NULL, + .next = &group_2, +}; + +static struct mpl_group group_2 = { + .title = "Group 2", + .track = &track_2_2, + .parent = &group_p, + .prev = &group_1, + .next = &group_3, +}; + +static struct mpl_group group_3 = { + .title = "Group 3", + .track = &track_3_3, + .parent = &group_p, + .prev = &group_2, + .next = &group_4, +}; + +static struct mpl_group group_4 = { + .title = "Group 4", + .track = &track_4_2, + .parent = &group_p, + .prev = &group_3, + .next = NULL, +}; + +static struct mpl_group group_p = { + .title = "Parent group", + .track = &track_4_1, + .parent = &group_p, + .prev = NULL, + .next = NULL, +}; + +static struct mpl_mediaplayer pl = { + .name = CONFIG_BT_MCS_MEDIA_PLAYER_NAME, + .icon_url = CONFIG_BT_MCS_ICON_URL, + .group = &group_1, + .track_pos = 0, + .state = BT_MCS_MEDIA_STATE_PAUSED, + .playback_speed_param = PLAYBACK_SPEED_PARAM_DEFAULT, + .seeking_speed_factor = BT_MCS_SEEKING_SPEED_FACTOR_ZERO, + .playing_order = BT_MCS_PLAYING_ORDER_INORDER_REPEAT, + .playing_orders_supported = BT_MCS_PLAYING_ORDERS_SUPPORTED_INORDER_ONCE | + BT_MCS_PLAYING_ORDERS_SUPPORTED_INORDER_REPEAT, + .opcodes_supported = 0x001fffff, /* All opcodes */ +#ifdef CONFIG_BT_OTS + .search_results_id = 0, + .calls = { 0 }, +#endif /* CONFIG_BT_OTS */ + .next_track_set = false +}; + +#ifdef CONFIG_BT_OTS + +/* The types of objects we keep in the Object Transfer Service */ +enum mpl_objects { + MPL_OBJ_NONE = 0, + MPL_OBJ_ICON, + MPL_OBJ_TRACK_SEGMENTS, + MPL_OBJ_TRACK, + MPL_OBJ_PARENT_GROUP, + MPL_OBJ_GROUP, + MPL_OBJ_SEARCH_RESULTS, +}; + +/* The active object */ +/* Only a single object is selected or being added (active) at a time. */ +/* And, except for the icon object, all objects can be created dynamically. */ +/* So a single buffer to hold object content is sufficient. */ +struct obj_t { + uint64_t selected_id; /* ID of the currently selected object*/ + bool busy; + uint8_t add_type; /* Type of object being added, e.g. MPL_OBJ_ICON */ + union { + struct mpl_track *add_track; /* Pointer to track being added */ + struct mpl_group *add_group; /* Pointer to group being added */ + }; + struct net_buf_simple *content; +}; + +static struct obj_t obj = { + .selected_id = 0, + .add_type = MPL_OBJ_NONE, + .busy = false, + .add_track = NULL, + .add_group = NULL, + .content = NET_BUF_SIMPLE(CONFIG_BT_MCS_MAX_OBJ_SIZE) +}; + +/* Set up content buffer for the icon object */ +static int setup_icon_object(void) +{ + uint16_t index; + uint8_t k; + + /* The icon object is supposed to be a bitmap. */ + /* For now, fill it with dummy data. */ + + net_buf_simple_reset(obj.content); + + /* Size may be larger than what fits in 8 bits, use 16-bit for index */ + for (index = 0, k = 0; + index < MIN(CONFIG_BT_MCS_MAX_OBJ_SIZE, + CONFIG_BT_MCS_ICON_BITMAP_SIZE); + index++, k++) { + net_buf_simple_add_u8(obj.content, k); + } + + return obj.content->len; +} + +/* Set up content buffer for a track segments object */ +static uint32_t setup_segments_object(struct mpl_track *track) +{ + struct mpl_tseg *seg = track->segment; + + net_buf_simple_reset(obj.content); + + if (seg) { + uint32_t tot_size = 0; + + while (seg->prev) { + seg = seg->prev; + } + while (seg) { + uint32_t seg_size = sizeof(seg->name_len); + + seg_size += seg->name_len; + seg_size += sizeof(seg->pos); + if (tot_size + seg_size > obj.content->size) { + BT_DBG("Segments object out of space"); + break; + } + net_buf_simple_add_u8(obj.content, seg->name_len); + net_buf_simple_add_mem(obj.content, seg->name, + seg->name_len); + net_buf_simple_add_le32(obj.content, seg->pos); + + tot_size += seg_size; + seg = seg->next; + } + + BT_HEXDUMP_DBG(obj.content->data, obj.content->len, + "Segments Object"); + BT_DBG("Segments object length: %d", obj.content->len); + } else { + BT_ERR("No seg!"); + } + + return obj.content->len; +} + +/* Set up content buffer for a track object */ +static uint32_t setup_track_object(struct mpl_track *track) +{ + uint16_t index; + uint8_t k; + + /* The track object is supposed to be in Id3v2 format */ + /* For now, fill it with dummy data */ + + net_buf_simple_reset(obj.content); + + /* Size may be larger than what fits in 8 bits, use 16-bit for index */ + for (index = 0, k = 0; + index < MIN(CONFIG_BT_MCS_MAX_OBJ_SIZE, + CONFIG_BT_MCS_TRACK_MAX_SIZE); + index++, k++) { + net_buf_simple_add_u8(obj.content, k); + } + + return obj.content->len; +} + +/* Set up content buffer for the parent group object */ +static uint32_t setup_parent_group_object(struct mpl_group *group) +{ + /* This function actually does not use the parent. */ + /* It just follows the list of groups. */ + /* The implementation has a fixed structure, with one parent group, */ + /* and one level of groups containing tracks only. */ + /* The track groups have a pointer to the parent, but there is no */ + /* poinbter in the other direction, so it is not possible to go from */ + /* the parent group to a group of tracks. */ + + uint8_t type = BT_MCS_GROUP_OBJECT_GROUP_TYPE; + uint8_t record_size = sizeof(type) + BT_OTS_OBJ_ID_SIZE; + int next_size = record_size; + + net_buf_simple_reset(obj.content); + + if (group) { + while (group->prev) { + group = group->prev; + } + /* While there is a group, and the record fits in the object */ + while (group && (next_size <= obj.content->size)) { + net_buf_simple_add_u8(obj.content, type); + net_buf_simple_add_le48(obj.content, group->id); + group = group->next; + next_size += record_size; + } + if (next_size > obj.content->size) { + BT_WARN("Not room for full group in object"); + } + BT_HEXDUMP_DBG(obj.content->data, obj.content->len, + "Parent Group Object"); + BT_DBG("Group object length: %d", obj.content->len); + } + return obj.content->len; +} + +/* Set up contents for a group object */ +/* The group object contains a concatenated list of records, where each */ +/* record consists of a type byte and a UUID */ +static uint32_t setup_group_object(struct mpl_group *group) +{ + struct mpl_track *track = group->track; + uint8_t type = BT_MCS_GROUP_OBJECT_TRACK_TYPE; + uint8_t record_size = sizeof(type) + BT_OTS_OBJ_ID_SIZE; + int next_size = record_size; + + net_buf_simple_reset(obj.content); + + if (track) { + while (track->prev) { + track = track->prev; + } + /* While there is a track, and the record fits in the object */ + while (track && (next_size <= obj.content->size)) { + net_buf_simple_add_u8(obj.content, type); + net_buf_simple_add_le48(obj.content, track->id); + track = track->next; + next_size += record_size; + } + if (next_size > obj.content->size) { + BT_WARN("Not room for full group in object"); + } + BT_HEXDUMP_DBG(obj.content->data, obj.content->len, + "Group Object"); + BT_DBG("Group object length: %d", obj.content->len); + } + return obj.content->len; +} + +/* Add the icon object to the OTS */ +static int add_icon_object(struct mpl_mediaplayer *pl) +{ + int ret; + struct bt_ots_obj_metadata icon = {0}; + struct bt_uuid *icon_type = BT_UUID_OTS_TYPE_MPL_ICON; + static char *icon_name = "Icon"; + + if (obj.busy) { + /* TODO: Can there be a collision between select and internal */ + /* activities, like adding new objects? */ + BT_ERR("Object busy"); + return 0; + } + obj.busy = true; + obj.add_type = MPL_OBJ_ICON; + + icon.size.alloc = icon.size.cur = setup_icon_object(); + icon.name = icon_name; + icon.type.uuid.type = BT_UUID_TYPE_16; + icon.type.uuid_16.val = BT_UUID_16(icon_type)->val; + BT_OTS_OBJ_SET_PROP_READ(icon.props); + + ret = bt_ots_obj_add(bt_mcs_get_ots(), &icon); + + if (ret) { + BT_WARN("Unable to add icon object, error %d", ret); + obj.busy = false; + } + return ret; +} + +/* Add a track segments object to the OTS */ +static int add_current_track_segments_object(struct mpl_mediaplayer *pl) +{ + int ret; + struct bt_ots_obj_metadata segs = {0}; + struct bt_uuid *segs_type = BT_UUID_OTS_TYPE_TRACK_SEGMENT; + + if (obj.busy) { + BT_ERR("Object busy"); + return 0; + } + obj.busy = true; + obj.add_type = MPL_OBJ_TRACK_SEGMENTS; + + segs.size.alloc = segs.size.cur = setup_segments_object(pl->group->track); + segs.name = pl->group->track->title; + segs.type.uuid.type = BT_UUID_TYPE_16; + segs.type.uuid_16.val = BT_UUID_16(segs_type)->val; + BT_OTS_OBJ_SET_PROP_READ(segs.props); + + ret = bt_ots_obj_add(bt_mcs_get_ots(), &segs); + if (ret) { + BT_WARN("Unable to add track segments object: %d", ret); + obj.busy = false; + } + return ret; +} + +/* Add a single track to the OTS */ +static int add_track_object(struct mpl_track *track) +{ + struct bt_ots_obj_metadata track_meta = {0}; + struct bt_uuid *track_type = BT_UUID_OTS_TYPE_TRACK; + int ret; + + if (obj.busy) { + BT_ERR("Object busy"); + return 0; + } + if (!track) { + BT_ERR("No track"); + return -EINVAL; + } + + obj.busy = true; + + obj.add_type = MPL_OBJ_TRACK; + obj.add_track = track; + + track_meta.size.alloc = track_meta.size.cur = setup_track_object(track); + track_meta.name = track->title; + track_meta.type.uuid.type = BT_UUID_TYPE_16; + track_meta.type.uuid_16.val = BT_UUID_16(track_type)->val; + BT_OTS_OBJ_SET_PROP_READ(track_meta.props); + ret = bt_ots_obj_add(bt_mcs_get_ots(), &track_meta); + + if (ret) { + BT_WARN("Unable to add track object: %d", ret); + obj.busy = false; + } + + return ret; +} + +/* Add the parent group to the OTS */ +static int add_parent_group_object(struct mpl_mediaplayer *pl) +{ + int ret; + struct bt_ots_obj_metadata group_meta = {0}; + struct bt_uuid *group_type = BT_UUID_OTS_TYPE_GROUP; + + if (obj.busy) { + BT_ERR("Object busy"); + return 0; + } + obj.busy = true; + obj.add_type = MPL_OBJ_PARENT_GROUP; + + group_meta.size.alloc = group_meta.size.cur = setup_parent_group_object(pl->group); + group_meta.name = pl->group->parent->title; + group_meta.type.uuid.type = BT_UUID_TYPE_16; + group_meta.type.uuid_16.val = BT_UUID_16(group_type)->val; + BT_OTS_OBJ_SET_PROP_READ(group_meta.props); + + ret = bt_ots_obj_add(bt_mcs_get_ots(), &group_meta); + if (ret) { + BT_WARN("Unable to add parent group object"); + obj.busy = false; + } + return ret; +} + +/* Add a single group to the OTS */ +static int add_group_object(struct mpl_group *group) +{ + struct bt_ots_obj_metadata group_meta = {0}; + struct bt_uuid *group_type = BT_UUID_OTS_TYPE_GROUP; + int ret; + + if (obj.busy) { + BT_ERR("Object busy"); + return 0; + } + + if (!group) { + BT_ERR("No group"); + return -EINVAL; + } + + obj.busy = true; + + obj.add_type = MPL_OBJ_GROUP; + obj.add_group = group; + + group_meta.size.alloc = group_meta.size.cur = setup_group_object(group); + group_meta.name = group->title; + group_meta.type.uuid.type = BT_UUID_TYPE_16; + group_meta.type.uuid_16.val = BT_UUID_16(group_type)->val; + BT_OTS_OBJ_SET_PROP_READ(group_meta.props); + + ret = bt_ots_obj_add(bt_mcs_get_ots(), &group_meta); + + if (ret) { + BT_WARN("Unable to add group object: %d", ret); + obj.busy = false; + } + + return ret; +} + +/* Add all tracks of a group to the OTS */ +static int add_group_tracks(struct mpl_group *group) +{ + int ret_overall = 0; + struct mpl_track *track = group->track; + + if (track) { + while (track->prev) { + track = track->prev; + } + + while (track) { + int ret = add_track_object(track); + + if (ret && !ret_overall) { + ret_overall = ret; + } + track = track->next; + } + } + return ret_overall; +} + +/* Add all groups (except the parent group) and their tracks to the OTS */ +static int add_group_and_track_objects(struct mpl_mediaplayer *pl) +{ + int ret_overall = 0; + int ret; + struct mpl_group *group = pl->group; + + if (group) { + while (group->prev) { + group = group->prev; + } + + while (group) { + ret = add_group_tracks(group); + if (ret && !ret_overall) { + ret_overall = ret; + } + + ret = add_group_object(group); + if (ret && !ret_overall) { + ret_overall = ret; + } + group = group->next; + } + } + + ret = add_parent_group_object(pl); + if (ret && !ret_overall) { + ret_overall = ret; + } + + return ret_overall; +} + +/**** Callbacks from the object transfer service ******************************/ + +static void on_obj_deleted(struct bt_ots *ots, struct bt_conn *conn, + uint64_t id) +{ + BT_DBG_OBJ_ID("Object Id deleted: ", id); +} + +static void on_obj_selected(struct bt_ots *ots, struct bt_conn *conn, + uint64_t id) +{ + if (obj.busy) { + /* TODO: Can there be a collision between select and internal */ + /* activities, like adding new objects? */ + BT_ERR("Object busy - select not performed"); + return; + } + obj.busy = true; + + BT_DBG_OBJ_ID("Object Id selected: ", id); + + if (id == pl.icon_id) { + BT_DBG("Icon Object ID"); + (void)setup_icon_object(); + } else if (id == pl.group->track->segments_id) { + BT_DBG("Current Track Segments Object ID"); + (void)setup_segments_object(pl.group->track); + } else if (id == pl.group->track->id) { + BT_DBG("Current Track Object ID"); + (void)setup_track_object(pl.group->track); + } else if (pl.next_track_set && id == pl.next.track->id) { + /* Next track, if the next track has been explicitly set */ + BT_DBG("Next Track Object ID"); + (void)setup_track_object(pl.next.track); + } else if (id == pl.group->track->next->id) { + /* Next track, if next track has not been explicitly set */ + BT_DBG("Next Track Object ID"); + (void)setup_track_object(pl.group->track->next); + } else if (id == pl.group->parent->id) { + BT_DBG("Parent Group Object ID"); + (void)setup_parent_group_object(pl.group); + } else if (id == pl.group->id) { + BT_DBG("Current Group Object ID"); + (void)setup_group_object(pl.group); + } else { + BT_ERR("Unknown Object ID"); + obj.busy = false; + return; + } + + obj.selected_id = id; + obj.busy = false; +} + +static int on_obj_created(struct bt_ots *ots, struct bt_conn *conn, + uint64_t id, + const struct bt_ots_obj_metadata *metadata) +{ + BT_DBG_OBJ_ID("Object Id created: ", id); + + if (!bt_uuid_cmp(&metadata->type.uuid, BT_UUID_OTS_TYPE_MPL_ICON)) { + BT_DBG("Icon Obj Type"); + if (obj.add_type == MPL_OBJ_ICON) { + obj.add_type = MPL_OBJ_NONE; + pl.icon_id = id; + } else { + BT_DBG("Unexpected object creation"); + } + + } else if (!bt_uuid_cmp(&metadata->type.uuid, + BT_UUID_OTS_TYPE_TRACK_SEGMENT)) { + BT_DBG("Track Segments Obj Type"); + if (obj.add_type == MPL_OBJ_TRACK_SEGMENTS) { + obj.add_type = MPL_OBJ_NONE; + pl.group->track->segments_id = id; + } else { + BT_DBG("Unexpected object creation"); + } + + } else if (!bt_uuid_cmp(&metadata->type.uuid, + BT_UUID_OTS_TYPE_TRACK)) { + BT_DBG("Track Obj Type"); + if (obj.add_type == MPL_OBJ_TRACK) { + obj.add_type = MPL_OBJ_NONE; + obj.add_track->id = id; + obj.add_track = NULL; + } else { + BT_DBG("Unexpected object creation"); + } + + } else if (!bt_uuid_cmp(&metadata->type.uuid, + BT_UUID_OTS_TYPE_GROUP)) { + BT_DBG("Group Obj Type"); + if (obj.add_type == MPL_OBJ_PARENT_GROUP) { + BT_DBG("Parent group"); + obj.add_type = MPL_OBJ_NONE; + pl.group->parent->id = id; + } else if (obj.add_type == MPL_OBJ_GROUP) { + BT_DBG("Other group"); + obj.add_type = MPL_OBJ_NONE; + obj.add_group->id = id; + obj.add_group = NULL; + } else { + BT_DBG("Unexpected object creation"); + } + + } else { + BT_DBG("Unknown Object ID"); + } + + if (obj.add_type == MPL_OBJ_NONE) { + obj.busy = false; + } + return 0; +} + + +static ssize_t on_object_send(struct bt_ots *ots, struct bt_conn *conn, + uint64_t id, void **data, size_t len, + off_t offset) +{ + if (obj.busy) { + /* TODO: Can there be a collision between select and internal */ + /* activities, like adding new objects? */ + BT_ERR("Object busy"); + return 0; + } + obj.busy = true; + + if (IS_ENABLED(CONFIG_BT_DEBUG_MCS)) { + char t[BT_OTS_OBJ_ID_STR_LEN]; + (void)bt_ots_obj_id_to_str(id, t, sizeof(t)); + BT_DBG("Object Id %s, offset %lu, length %zu", log_strdup(t), + (long)offset, len); + } + + if (id != obj.selected_id) { + BT_ERR("Read from unselected object"); + obj.busy = false; + return 0; + } + + if (!data) { + BT_DBG("Read complete"); + obj.busy = false; + return 0; + } + + if (offset >= obj.content->len) { + BT_DBG("Offset too large"); + obj.busy = false; + return 0; + } + + if (IS_ENABLED(CONFIG_BT_DEBUG_MCS)) { + if (len > obj.content->len - offset) { + BT_DBG("Requested len too large"); + } + } + + *data = &obj.content->data[offset]; + obj.busy = false; + return MIN(len, obj.content->len - offset); +} + +static struct bt_ots_cb ots_cbs = { + .obj_created = on_obj_created, + .obj_read = on_object_send, + .obj_selected = on_obj_selected, + .obj_deleted = on_obj_deleted, +}; + +#endif /* CONFIG_BT_OTS */ + + +/* TODO: It must be possible to replace the do_prev_segment(), do_prev_track */ +/* and do_prev_group() with a generic do_prev() command that can be used at */ +/* all levels. Similarly for do_next, do_prev, and so on. */ + +void do_prev_segment(struct mpl_mediaplayer *pl) +{ + BT_DBG("Segment name before: %s", + log_strdup(pl->group->track->segment->name)); + + if (pl->group->track->segment->prev != NULL) { + pl->group->track->segment = pl->group->track->segment->prev; + } + + BT_DBG("Segment name after: %s", + log_strdup(pl->group->track->segment->name)); +} + +void do_next_segment(struct mpl_mediaplayer *pl) +{ + BT_DBG("Segment name before: %s", + log_strdup(pl->group->track->segment->name)); + + if (pl->group->track->segment->next != NULL) { + pl->group->track->segment = pl->group->track->segment->next; + } + + BT_DBG("Segment name after: %s", + log_strdup(pl->group->track->segment->name)); +} + +void do_first_segment(struct mpl_mediaplayer *pl) +{ + BT_DBG("Segment name before: %s", + log_strdup(pl->group->track->segment->name)); + + while (pl->group->track->segment->prev != NULL) { + pl->group->track->segment = pl->group->track->segment->prev; + } + + BT_DBG("Segment name after: %s", + log_strdup(pl->group->track->segment->name)); +} + +void do_last_segment(struct mpl_mediaplayer *pl) +{ + BT_DBG("Segment name before: %s", + log_strdup(pl->group->track->segment->name)); + + while (pl->group->track->segment->next != NULL) { + pl->group->track->segment = pl->group->track->segment->next; + } + + BT_DBG("Segment name after: %s", + log_strdup(pl->group->track->segment->name)); +} + +void do_goto_segment(struct mpl_mediaplayer *pl, int32_t segnum) +{ + int32_t k; + + BT_DBG("Segment name before: %s", + log_strdup(pl->group->track->segment->name)); + + if (segnum > 0) { + /* Goto first segment */ + while (pl->group->track->segment->prev != NULL) { + pl->group->track->segment = + pl->group->track->segment->prev; + } + + /* Then go segnum - 1 tracks forward */ + for (k = 0; k < (segnum - 1); k++) { + if (pl->group->track->segment->next != NULL) { + pl->group->track->segment = + pl->group->track->segment->next; + } + } + } else if (segnum < 0) { + /* Goto last track */ + while (pl->group->track->segment->next != NULL) { + pl->group->track->segment = + pl->group->track->segment->next; + } + + /* Then go |segnum - 1| tracks back */ + for (k = 0; k < (-segnum - 1); k++) { + if (pl->group->track->segment->prev != NULL) { + pl->group->track->segment = + pl->group->track->segment->prev; + } + } + } + + BT_DBG("Segment name after: %s", + log_strdup(pl->group->track->segment->name)); +} + +static bool do_prev_track(struct mpl_mediaplayer *pl) +{ + bool track_changed = false; + +#ifdef CONFIG_BT_OTS + BT_DBG_OBJ_ID("Track ID before: ", pl->group->track->id); +#endif /* CONFIG_BT_OTS */ + + if (pl->group->track->prev != NULL) { + pl->group->track = pl->group->track->prev; + track_changed = true; + } + +#ifdef CONFIG_BT_OTS + BT_DBG_OBJ_ID("Track ID after: ", pl->group->track->id); +#endif /* CONFIG_BT_OTS */ + + return track_changed; +} + +/* Change to next track according to the current track's next track */ +static bool do_next_track_normal_order(struct mpl_mediaplayer *pl) +{ + bool track_changed = false; + +#ifdef CONFIG_BT_OTS + BT_DBG_OBJ_ID("Track ID before: ", pl->group->track->id); +#endif /* CONFIG_BT_OTS */ + + if (pl->group->track->next != NULL) { + pl->group->track = pl->group->track->next; + track_changed = true; + } + +#ifdef CONFIG_BT_OTS + BT_DBG_OBJ_ID("Track ID after: ", pl->group->track->id); +#endif /* CONFIG_BT_OTS */ + + return track_changed; +} + +/* Change to next track when the next track has been explicitly set + * + * ALWAYS changes the track, changes the group if required + * Resets the next_track_set and the "next" pointers + * + * Returns true if the _group_ has been changed, otherwise false + */ +static bool do_next_track_next_track_set(struct mpl_mediaplayer *pl) +{ + bool group_changed = false; + + if (pl->next.group != pl->group) { + pl->group = pl->next.group; + group_changed = true; + } + + pl->group->track = pl->next.track; + + pl->next.track = NULL; + pl->next.group = NULL; + pl->next_track_set = false; + + return group_changed; +} + +static bool do_first_track(struct mpl_mediaplayer *pl) +{ + bool track_changed = false; + +#ifdef CONFIG_BT_OTS + BT_DBG_OBJ_ID("Track ID before: ", pl->group->track->id); +#endif /* CONFIG_BT_OTS */ + + if (pl->group->track->prev != NULL) { + pl->group->track = pl->group->track->prev; + track_changed = true; + } + while (pl->group->track->prev != NULL) { + pl->group->track = pl->group->track->prev; + } + +#ifdef CONFIG_BT_OTS + BT_DBG_OBJ_ID("Track ID after: ", pl->group->track->id); +#endif /* CONFIG_BT_OTS */ + + return track_changed; +} + +static bool do_last_track(struct mpl_mediaplayer *pl) +{ + bool track_changed = false; + +#ifdef CONFIG_BT_OTS + BT_DBG_OBJ_ID("Track ID before: ", pl->group->track->id); +#endif /* CONFIG_BT_OTS */ + + if (pl->group->track->next != NULL) { + pl->group->track = pl->group->track->next; + track_changed = true; + } + while (pl->group->track->next != NULL) { + pl->group->track = pl->group->track->next; + } + +#ifdef CONFIG_BT_OTS + BT_DBG_OBJ_ID("Track ID after: ", pl->group->track->id); +#endif /* CONFIG_BT_OTS */ + + return track_changed; +} + +static bool do_goto_track(struct mpl_mediaplayer *pl, int32_t tracknum) +{ + int32_t count = 0; + int32_t k; + +#ifdef CONFIG_BT_OTS + BT_DBG_OBJ_ID("Track ID before: ", pl->group->track->id); +#endif /* CONFIG_BT_OTS */ + + if (tracknum > 0) { + /* Goto first track */ + while (pl->group->track->prev != NULL) { + pl->group->track = pl->group->track->prev; + count--; + } + + /* Then go tracknum - 1 tracks forward */ + for (k = 0; k < (tracknum - 1); k++) { + if (pl->group->track->next != NULL) { + pl->group->track = pl->group->track->next; + count++; + } + } + } else if (tracknum < 0) { + /* Goto last track */ + while (pl->group->track->next != NULL) { + pl->group->track = pl->group->track->next; + count++; + } + + /* Then go |tracknum - 1| tracks back */ + for (k = 0; k < (-tracknum - 1); k++) { + if (pl->group->track->prev != NULL) { + pl->group->track = pl->group->track->prev; + count--; + } + } + } + +#ifdef CONFIG_BT_OTS + BT_DBG_OBJ_ID("Track ID after: ", pl->group->track->id); +#endif /* CONFIG_BT_OTS */ + + /* The track has changed if we have moved more in one direction */ + /* than in the other */ + return (count != 0); +} + + +static bool do_prev_group(struct mpl_mediaplayer *pl) +{ + bool group_changed = false; + +#ifdef CONFIG_BT_OTS + BT_DBG_OBJ_ID("Group ID before: ", pl->group->id); +#endif /* CONFIG_BT_OTS */ + + if (pl->group->prev != NULL) { + pl->group = pl->group->prev; + group_changed = true; + } + +#ifdef CONFIG_BT_OTS + BT_DBG_OBJ_ID("Group ID after: ", pl->group->id); +#endif /* CONFIG_BT_OTS */ + + return group_changed; +} + +static bool do_next_group(struct mpl_mediaplayer *pl) +{ + bool group_changed = false; + +#ifdef CONFIG_BT_OTS + BT_DBG_OBJ_ID("Group ID before: ", pl->group->id); +#endif /* CONFIG_BT_OTS */ + + if (pl->group->next != NULL) { + pl->group = pl->group->next; + group_changed = true; + } + +#ifdef CONFIG_BT_OTS + BT_DBG_OBJ_ID("Group ID after: ", pl->group->id); +#endif /* CONFIG_BT_OTS */ + + return group_changed; +} + +static bool do_first_group(struct mpl_mediaplayer *pl) +{ + bool group_changed = false; + +#ifdef CONFIG_BT_OTS + BT_DBG_OBJ_ID("Group ID before: ", pl->group->id); +#endif /* CONFIG_BT_OTS */ + + if (pl->group->prev != NULL) { + pl->group = pl->group->prev; + group_changed = true; + } + while (pl->group->prev != NULL) { + pl->group = pl->group->prev; + } + +#ifdef CONFIG_BT_OTS + BT_DBG_OBJ_ID("Group ID after: ", pl->group->id); +#endif /* CONFIG_BT_OTS */ + + return group_changed; +} + +static bool do_last_group(struct mpl_mediaplayer *pl) +{ + bool group_changed = false; + +#ifdef CONFIG_BT_OTS + BT_DBG_OBJ_ID("Group ID before: ", pl->group->id); +#endif /* CONFIG_BT_OTS */ + + if (pl->group->next != NULL) { + pl->group = pl->group->next; + group_changed = true; + } + while (pl->group->next != NULL) { + pl->group = pl->group->next; + } + +#ifdef CONFIG_BT_OTS + BT_DBG_OBJ_ID("Group ID after: ", pl->group->id); +#endif /* CONFIG_BT_OTS */ + + return group_changed; +} + +static bool do_goto_group(struct mpl_mediaplayer *pl, int32_t groupnum) +{ + int32_t count = 0; + int32_t k; + +#ifdef CONFIG_BT_OTS + BT_DBG_OBJ_ID("Group ID before: ", pl->group->id); +#endif /* CONFIG_BT_OTS */ + + if (groupnum > 0) { + /* Goto first group */ + while (pl->group->prev != NULL) { + pl->group = pl->group->prev; + count--; + } + + /* Then go groupnum - 1 groups forward */ + for (k = 0; k < (groupnum - 1); k++) { + if (pl->group->next != NULL) { + pl->group = pl->group->next; + count++; + } + } + } else if (groupnum < 0) { + /* Goto last group */ + while (pl->group->next != NULL) { + pl->group = pl->group->next; + count++; + } + + /* Then go |groupnum - 1| groups back */ + for (k = 0; k < (-groupnum - 1); k++) { + if (pl->group->prev != NULL) { + pl->group = pl->group->prev; + count--; + } + } + } + +#ifdef CONFIG_BT_OTS + BT_DBG_OBJ_ID("Group ID after: ", pl->group->id); +#endif /* CONFIG_BT_OTS */ + + /* The group has changed if we have moved more in one direction */ + /* than in the other */ + return (count != 0); +} + +void do_track_change_notifications(struct mpl_mediaplayer *pl) +{ + media_proxy_pl_track_changed_cb(); + media_proxy_pl_track_title_cb(pl->group->track->title); + media_proxy_pl_track_duration_cb(pl->group->track->duration); + media_proxy_pl_track_position_cb(pl->track_pos); +#ifdef CONFIG_BT_OTS + media_proxy_pl_current_track_id_cb(pl->group->track->id); + if (pl->group->track->next) { + media_proxy_pl_next_track_id_cb(pl->group->track->next->id); + } else { + /* Send a zero value to indicate that there is no next track */ + media_proxy_pl_next_track_id_cb(MPL_NO_TRACK_ID); + } +#endif /* CONFIG_BT_OTS */ +} + +void do_group_change_notifications(struct mpl_mediaplayer *pl) +{ +#ifdef CONFIG_BT_OTS + media_proxy_pl_current_group_id_cb(pl->group->id); +#endif /* CONFIG_BT_OTS */ +} + +void do_full_prev_group(struct mpl_mediaplayer *pl) +{ + /* Change the group (if not already on first group) */ + if (do_prev_group(pl)) { + do_group_change_notifications(pl); + /* If group change, also go to first track in group. */ + /* Notify the track info in all cases - it is a track */ + /* change for the player even if the group was set to */ + /* this track already. */ + (void) do_first_track(pl); + pl->track_pos = 0; + do_track_change_notifications(pl); + /* If no group change, still switch to first track, if needed */ + } else if (do_first_track(pl)) { + pl->track_pos = 0; + do_track_change_notifications(pl); + /* If no track change, still reset track position, if needed */ + } else if (pl->track_pos != 0) { + pl->track_pos = 0; + media_proxy_pl_track_position_cb(pl->track_pos); + } +} + +void do_full_next_group(struct mpl_mediaplayer *pl) +{ + /* Change the group (if not already on last group) */ + if (do_next_group(pl)) { + do_group_change_notifications(pl); + /* If group change, also go to first track in group. */ + /* Notify the track info in all cases - it is a track */ + /* change for the player even if the group was set to */ + /* this track already. */ + (void) do_first_track(pl); + pl->track_pos = 0; + do_track_change_notifications(pl); + /* If no group change, still switch to first track, if needed */ + } else if (do_first_track(pl)) { + pl->track_pos = 0; + do_track_change_notifications(pl); + /* If no track change, still reset track position, if needed */ + } else if (pl->track_pos != 0) { + pl->track_pos = 0; + media_proxy_pl_track_position_cb(pl->track_pos); + } +} + +void do_full_first_group(struct mpl_mediaplayer *pl) +{ + /* Change the group (if not already on first group) */ + if (do_first_group(pl)) { + do_group_change_notifications(pl); + /* If group change, also go to first track in group. */ + /* Notify the track info in all cases - it is a track */ + /* change for the player even if the group was set to */ + /* this track already. */ + (void) do_first_track(pl); + pl->track_pos = 0; + do_track_change_notifications(pl); + /* If no group change, still switch to first track, if needed */ + } else if (do_first_track(pl)) { + pl->track_pos = 0; + do_track_change_notifications(pl); + /* If no track change, still reset track position, if needed */ + } else if (pl->track_pos != 0) { + pl->track_pos = 0; + media_proxy_pl_track_position_cb(pl->track_pos); + } +} + +void do_full_last_group(struct mpl_mediaplayer *pl) +{ + /* Change the group (if not already on last group) */ + if (do_last_group(pl)) { + do_group_change_notifications(pl); + /* If group change, also go to first track in group. */ + /* Notify the track info in all cases - it is a track */ + /* change for the player even if the group was set to */ + /* this track already. */ + (void) do_first_track(pl); + pl->track_pos = 0; + do_track_change_notifications(pl); + /* If no group change, still switch to first track, if needed */ + } else if (do_first_track(pl)) { + pl->track_pos = 0; + do_track_change_notifications(pl); + /* If no track change, still reset track position, if needed */ + } else if (pl->track_pos != 0) { + pl->track_pos = 0; + media_proxy_pl_track_position_cb(pl->track_pos); + } +} + +void do_full_goto_group(struct mpl_mediaplayer *pl, int32_t groupnum) +{ + /* Change the group (if not already on given group) */ + if (do_goto_group(pl, groupnum)) { + do_group_change_notifications(pl); + /* If group change, also go to first track in group. */ + /* Notify the track info in all cases - it is a track */ + /* change for the player even if the group was set to */ + /* this track already. */ + (void) do_first_track(pl); + pl->track_pos = 0; + do_track_change_notifications(pl); + /* If no group change, still switch to first track, if needed */ + } else if (do_first_track(pl)) { + pl->track_pos = 0; + do_track_change_notifications(pl); + /* If no track change, still reset track position, if needed */ + } else if (pl->track_pos != 0) { + pl->track_pos = 0; + media_proxy_pl_track_position_cb(pl->track_pos); + } +} + +/* Command handlers (state machines) */ +void inactive_state_command_handler(struct mpl_cmd command, + struct mpl_cmd_ntf ntf) +{ + BT_DBG("Command opcode: %d", command.opcode); + if (IS_ENABLED(CONFIG_BT_DEBUG_MCS)) { + if (command.use_param) { + BT_DBG("Command parameter: %d", command.param); + } + } + switch (command.opcode) { + case BT_MCS_OPC_PLAY: /* Fall-through - handle several cases identically */ + case BT_MCS_OPC_PAUSE: + case BT_MCS_OPC_FAST_REWIND: + case BT_MCS_OPC_FAST_FORWARD: + case BT_MCS_OPC_STOP: + case BT_MCS_OPC_MOVE_RELATIVE: + case BT_MCS_OPC_PREV_SEGMENT: + case BT_MCS_OPC_NEXT_SEGMENT: + case BT_MCS_OPC_FIRST_SEGMENT: + case BT_MCS_OPC_LAST_SEGMENT: + case BT_MCS_OPC_GOTO_SEGMENT: + ntf.result_code = BT_MCS_OPC_NTF_PLAYER_INACTIVE; + media_proxy_pl_command_cb(ntf); + break; + case BT_MCS_OPC_PREV_TRACK: + if (do_prev_track(&pl)) { + pl.track_pos = 0; + do_track_change_notifications(&pl); + } else { + /* For previous track, the position is reset to 0 */ + /* even if we stay at the same track (goto start of */ + /* track) */ + pl.track_pos = 0; + media_proxy_pl_track_position_cb(pl.track_pos); + } + pl.state = BT_MCS_MEDIA_STATE_PAUSED; + media_proxy_pl_media_state_cb(pl.state); + ntf.result_code = BT_MCS_OPC_NTF_SUCCESS; + media_proxy_pl_command_cb(ntf); + break; + case BT_MCS_OPC_NEXT_TRACK: + /* TODO: + * The case where the next track has been set explicitly breaks somewhat + * with the "next" order hardcoded into the group and track structure + */ + if (pl.next_track_set) { + BT_DBG("Next track set"); + if (do_next_track_next_track_set(&pl)) { + do_group_change_notifications(&pl); + } + pl.track_pos = 0; + do_track_change_notifications(&pl); + } else if (do_next_track_normal_order(&pl)) { + pl.track_pos = 0; + do_track_change_notifications(&pl); + } + /* For next track, the position is kept if the track */ + /* does not change */ + pl.state = BT_MCS_MEDIA_STATE_PAUSED; + media_proxy_pl_media_state_cb(pl.state); + ntf.result_code = BT_MCS_OPC_NTF_SUCCESS; + media_proxy_pl_command_cb(ntf); + break; + case BT_MCS_OPC_FIRST_TRACK: + if (do_first_track(&pl)) { + pl.track_pos = 0; + do_track_change_notifications(&pl); + } else { + /* For first track, the position is reset to 0 even */ + /* if we stay at the same track (goto start of track) */ + pl.track_pos = 0; + media_proxy_pl_track_position_cb(pl.track_pos); + } + pl.state = BT_MCS_MEDIA_STATE_PAUSED; + media_proxy_pl_media_state_cb(pl.state); + ntf.result_code = BT_MCS_OPC_NTF_SUCCESS; + media_proxy_pl_command_cb(ntf); + break; + case BT_MCS_OPC_LAST_TRACK: + if (do_last_track(&pl)) { + pl.track_pos = 0; + do_track_change_notifications(&pl); + } else { + /* For last track, the position is reset to 0 even */ + /* if we stay at the same track (goto start of track) */ + pl.track_pos = 0; + media_proxy_pl_track_position_cb(pl.track_pos); + } + pl.state = BT_MCS_MEDIA_STATE_PAUSED; + media_proxy_pl_media_state_cb(pl.state); + ntf.result_code = BT_MCS_OPC_NTF_SUCCESS; + media_proxy_pl_command_cb(ntf); + break; + case BT_MCS_OPC_GOTO_TRACK: + if (command.use_param) { + if (do_goto_track(&pl, command.param)) { + pl.track_pos = 0; + do_track_change_notifications(&pl); + } else { + /* For goto track, the position is reset to 0 */ + /* even if we stay at the same track (goto */ + /* start of track) */ + pl.track_pos = 0; + media_proxy_pl_track_position_cb(pl.track_pos); + } + pl.state = BT_MCS_MEDIA_STATE_PAUSED; + media_proxy_pl_media_state_cb(pl.state); + ntf.result_code = BT_MCS_OPC_NTF_SUCCESS; + } else { + ntf.result_code = BT_MCS_OPC_NTF_CANNOT_BE_COMPLETED; + } + media_proxy_pl_command_cb(ntf); + break; + case BT_MCS_OPC_PREV_GROUP: + do_full_prev_group(&pl); + pl.state = BT_MCS_MEDIA_STATE_PAUSED; + media_proxy_pl_media_state_cb(pl.state); + ntf.result_code = BT_MCS_OPC_NTF_SUCCESS; + media_proxy_pl_command_cb(ntf); + break; + case BT_MCS_OPC_NEXT_GROUP: + do_full_next_group(&pl); + pl.state = BT_MCS_MEDIA_STATE_PAUSED; + media_proxy_pl_media_state_cb(pl.state); + ntf.result_code = BT_MCS_OPC_NTF_SUCCESS; + media_proxy_pl_command_cb(ntf); + break; + case BT_MCS_OPC_FIRST_GROUP: + do_full_first_group(&pl); + pl.state = BT_MCS_MEDIA_STATE_PAUSED; + media_proxy_pl_media_state_cb(pl.state); + ntf.result_code = BT_MCS_OPC_NTF_SUCCESS; + media_proxy_pl_command_cb(ntf); + break; + case BT_MCS_OPC_LAST_GROUP: + do_full_last_group(&pl); + pl.state = BT_MCS_MEDIA_STATE_PAUSED; + media_proxy_pl_media_state_cb(pl.state); + ntf.result_code = BT_MCS_OPC_NTF_SUCCESS; + media_proxy_pl_command_cb(ntf); + break; + case BT_MCS_OPC_GOTO_GROUP: + if (command.use_param) { + do_full_goto_group(&pl, command.param); + pl.state = BT_MCS_MEDIA_STATE_PAUSED; + media_proxy_pl_media_state_cb(pl.state); + ntf.result_code = BT_MCS_OPC_NTF_SUCCESS; + } else { + ntf.result_code = BT_MCS_OPC_NTF_CANNOT_BE_COMPLETED; + } + media_proxy_pl_command_cb(ntf); + break; + default: + BT_DBG("Invalid command: %d", command.opcode); + ntf.result_code = BT_MCS_OPC_NTF_NOT_SUPPORTED; + media_proxy_pl_command_cb(ntf); + break; + } +} + +void playing_state_command_handler(struct mpl_cmd command, + struct mpl_cmd_ntf ntf) +{ + BT_DBG("Command opcode: %d", command.opcode); + if (IS_ENABLED(CONFIG_BT_DEBUG_MCS)) { + if (command.use_param) { + BT_DBG("Command parameter: %d", command.param); + } + } + switch (command.opcode) { + case BT_MCS_OPC_PLAY: + /* Continue playing - i.e. do nothing */ + ntf.result_code = BT_MCS_OPC_NTF_SUCCESS; + media_proxy_pl_command_cb(ntf); + break; + case BT_MCS_OPC_PAUSE: + pl.state = BT_MCS_MEDIA_STATE_PAUSED; + media_proxy_pl_media_state_cb(pl.state); + ntf.result_code = BT_MCS_OPC_NTF_SUCCESS; + media_proxy_pl_command_cb(ntf); + break; + case BT_MCS_OPC_FAST_REWIND: + /* We're in playing state, seeking speed must have been zero */ + pl.seeking_speed_factor = -MPL_SEEKING_SPEED_FACTOR_STEP; + pl.state = BT_MCS_MEDIA_STATE_SEEKING; + media_proxy_pl_media_state_cb(pl.state); + media_proxy_pl_seeking_speed_cb(pl.seeking_speed_factor); + ntf.result_code = BT_MCS_OPC_NTF_SUCCESS; + media_proxy_pl_command_cb(ntf); + break; + case BT_MCS_OPC_FAST_FORWARD: + /* We're in playing state, seeking speed must have been zero */ + pl.seeking_speed_factor = MPL_SEEKING_SPEED_FACTOR_STEP; + pl.state = BT_MCS_MEDIA_STATE_SEEKING; + media_proxy_pl_media_state_cb(pl.state); + media_proxy_pl_seeking_speed_cb(pl.seeking_speed_factor); + ntf.result_code = BT_MCS_OPC_NTF_SUCCESS; + media_proxy_pl_command_cb(ntf); + break; + case BT_MCS_OPC_STOP: + pl.track_pos = 0; + pl.state = BT_MCS_MEDIA_STATE_PAUSED; + media_proxy_pl_media_state_cb(pl.state); + media_proxy_pl_track_position_cb(pl.track_pos); + ntf.result_code = BT_MCS_OPC_NTF_SUCCESS; + media_proxy_pl_command_cb(ntf); + break; + case BT_MCS_OPC_MOVE_RELATIVE: + if (command.use_param) { + /* Keep within track - i.e. in the range 0 - duration */ + if (command.param > + pl.group->track->duration - pl.track_pos) { + pl.track_pos = pl.group->track->duration; + } else if (command.param < -pl.track_pos) { + pl.track_pos = 0; + } else { + pl.track_pos += command.param; + } + ntf.result_code = BT_MCS_OPC_NTF_SUCCESS; + } else { + ntf.result_code = BT_MCS_OPC_NTF_CANNOT_BE_COMPLETED; + } + media_proxy_pl_track_position_cb(pl.track_pos); + media_proxy_pl_command_cb(ntf); + break; + case BT_MCS_OPC_PREV_SEGMENT: + /* Switch to previous segment if we are less than */ + /* into the segment, otherwise go to start of segment */ + if (pl.track_pos - PREV_MARGIN < + pl.group->track->segment->pos) { + do_prev_segment(&pl); + } + pl.track_pos = pl.group->track->segment->pos; + media_proxy_pl_track_position_cb(pl.track_pos); + ntf.result_code = BT_MCS_OPC_NTF_SUCCESS; + media_proxy_pl_command_cb(ntf); + break; + case BT_MCS_OPC_NEXT_SEGMENT: + do_next_segment(&pl); + pl.track_pos = pl.group->track->segment->pos; + media_proxy_pl_track_position_cb(pl.track_pos); + ntf.result_code = BT_MCS_OPC_NTF_SUCCESS; + media_proxy_pl_command_cb(ntf); + break; + case BT_MCS_OPC_FIRST_SEGMENT: + do_first_segment(&pl); + pl.track_pos = pl.group->track->segment->pos; + media_proxy_pl_track_position_cb(pl.track_pos); + ntf.result_code = BT_MCS_OPC_NTF_SUCCESS; + media_proxy_pl_command_cb(ntf); + break; + case BT_MCS_OPC_LAST_SEGMENT: + do_last_segment(&pl); + pl.track_pos = pl.group->track->segment->pos; + media_proxy_pl_track_position_cb(pl.track_pos); + ntf.result_code = BT_MCS_OPC_NTF_SUCCESS; + media_proxy_pl_command_cb(ntf); + break; + case BT_MCS_OPC_GOTO_SEGMENT: + if (command.use_param) { + if (command.param != 0) { + do_goto_segment(&pl, command.param); + pl.track_pos = pl.group->track->segment->pos; + media_proxy_pl_track_position_cb(pl.track_pos); + } + /* If the argument to "goto segment" is zero, */ + /* the segment shal stay the same, and the */ + /* track position shall not change. */ + ntf.result_code = BT_MCS_OPC_NTF_SUCCESS; + } else { + ntf.result_code = BT_MCS_OPC_NTF_CANNOT_BE_COMPLETED; + } + media_proxy_pl_command_cb(ntf); + break; + case BT_MCS_OPC_PREV_TRACK: + if (do_prev_track(&pl)) { + pl.track_pos = 0; + do_track_change_notifications(&pl); + } else { + /* For previous track, the position is reset to 0 */ + /* even if we stay at the same track (goto start of */ + /* track) */ + pl.track_pos = 0; + media_proxy_pl_track_position_cb(pl.track_pos); + } + ntf.result_code = BT_MCS_OPC_NTF_SUCCESS; + media_proxy_pl_command_cb(ntf); + break; + case BT_MCS_OPC_NEXT_TRACK: + if (pl.next_track_set) { + BT_DBG("Next track set"); + if (do_next_track_next_track_set(&pl)) { + do_group_change_notifications(&pl); + } + pl.track_pos = 0; + do_track_change_notifications(&pl); + } else if (do_next_track_normal_order(&pl)) { + pl.track_pos = 0; + do_track_change_notifications(&pl); + } + /* For next track, the position is kept if the track */ + /* does not change */ + ntf.result_code = BT_MCS_OPC_NTF_SUCCESS; + media_proxy_pl_command_cb(ntf); + break; + case BT_MCS_OPC_FIRST_TRACK: + if (do_first_track(&pl)) { + pl.track_pos = 0; + do_track_change_notifications(&pl); + } else { + /* For first track, the position is reset to 0 even */ + /* if we stay at the same track (goto start of track) */ + pl.track_pos = 0; + media_proxy_pl_track_position_cb(pl.track_pos); + } + ntf.result_code = BT_MCS_OPC_NTF_SUCCESS; + media_proxy_pl_command_cb(ntf); + break; + case BT_MCS_OPC_LAST_TRACK: + if (do_last_track(&pl)) { + pl.track_pos = 0; + do_track_change_notifications(&pl); + } else { + /* For last track, the position is reset to 0 even */ + /* if we stay at the same track (goto start of track) */ + pl.track_pos = 0; + media_proxy_pl_track_position_cb(pl.track_pos); + } + ntf.result_code = BT_MCS_OPC_NTF_SUCCESS; + media_proxy_pl_command_cb(ntf); + break; + case BT_MCS_OPC_GOTO_TRACK: + if (command.use_param) { + if (do_goto_track(&pl, command.param)) { + pl.track_pos = 0; + do_track_change_notifications(&pl); + } else { + /* For goto track, the position is reset to 0 */ + /* even if we stay at the same track (goto */ + /* start of track) */ + pl.track_pos = 0; + media_proxy_pl_track_position_cb(pl.track_pos); + } + ntf.result_code = BT_MCS_OPC_NTF_SUCCESS; + } else { + ntf.result_code = BT_MCS_OPC_NTF_CANNOT_BE_COMPLETED; + } + media_proxy_pl_command_cb(ntf); + break; + case BT_MCS_OPC_PREV_GROUP: + do_full_prev_group(&pl); + ntf.result_code = BT_MCS_OPC_NTF_SUCCESS; + media_proxy_pl_command_cb(ntf); + break; + case BT_MCS_OPC_NEXT_GROUP: + do_full_next_group(&pl); + ntf.result_code = BT_MCS_OPC_NTF_SUCCESS; + media_proxy_pl_command_cb(ntf); + break; + case BT_MCS_OPC_FIRST_GROUP: + do_full_first_group(&pl); + ntf.result_code = BT_MCS_OPC_NTF_SUCCESS; + media_proxy_pl_command_cb(ntf); + break; + case BT_MCS_OPC_LAST_GROUP: + do_full_last_group(&pl); + ntf.result_code = BT_MCS_OPC_NTF_SUCCESS; + media_proxy_pl_command_cb(ntf); + break; + case BT_MCS_OPC_GOTO_GROUP: + if (command.use_param) { + do_full_goto_group(&pl, command.param); + ntf.result_code = BT_MCS_OPC_NTF_SUCCESS; + } else { + ntf.result_code = BT_MCS_OPC_NTF_CANNOT_BE_COMPLETED; + } + media_proxy_pl_command_cb(ntf); + break; + default: + BT_DBG("Invalid command: %d", command.opcode); + ntf.result_code = BT_MCS_OPC_NTF_NOT_SUPPORTED; + media_proxy_pl_command_cb(ntf); + break; + } +} + +void paused_state_command_handler(struct mpl_cmd command, + struct mpl_cmd_ntf ntf) +{ + BT_DBG("Command opcode: %d", command.opcode); + if (IS_ENABLED(CONFIG_BT_DEBUG_MCS)) { + if (command.use_param) { + BT_DBG("Command parameter: %d", command.param); + } + } + switch (command.opcode) { + case BT_MCS_OPC_PLAY: + pl.state = BT_MCS_MEDIA_STATE_PLAYING; + media_proxy_pl_media_state_cb(pl.state); + ntf.result_code = BT_MCS_OPC_NTF_SUCCESS; + media_proxy_pl_command_cb(ntf); + break; + case BT_MCS_OPC_PAUSE: + /* No change */ + ntf.result_code = BT_MCS_OPC_NTF_SUCCESS; + media_proxy_pl_command_cb(ntf); + break; + case BT_MCS_OPC_FAST_REWIND: + /* We're in paused state, seeking speed must have been zero */ + pl.seeking_speed_factor = -MPL_SEEKING_SPEED_FACTOR_STEP; + pl.state = BT_MCS_MEDIA_STATE_SEEKING; + media_proxy_pl_media_state_cb(pl.state); + media_proxy_pl_seeking_speed_cb(pl.seeking_speed_factor); + ntf.result_code = BT_MCS_OPC_NTF_SUCCESS; + media_proxy_pl_command_cb(ntf); + break; + case BT_MCS_OPC_FAST_FORWARD: + /* We're in paused state, seeking speed must have been zero */ + pl.seeking_speed_factor = MPL_SEEKING_SPEED_FACTOR_STEP; + pl.state = BT_MCS_MEDIA_STATE_SEEKING; + media_proxy_pl_media_state_cb(pl.state); + media_proxy_pl_seeking_speed_cb(pl.seeking_speed_factor); + ntf.result_code = BT_MCS_OPC_NTF_SUCCESS; + media_proxy_pl_command_cb(ntf); + break; + case BT_MCS_OPC_STOP: + pl.track_pos = 0; + pl.state = BT_MCS_MEDIA_STATE_PAUSED; + media_proxy_pl_media_state_cb(pl.state); + media_proxy_pl_track_position_cb(pl.track_pos); + ntf.result_code = BT_MCS_OPC_NTF_SUCCESS; + media_proxy_pl_command_cb(ntf); + break; + case BT_MCS_OPC_MOVE_RELATIVE: + if (command.use_param) { + /* Keep within track - i.e. in the range 0 - duration */ + if (command.param > + pl.group->track->duration - pl.track_pos) { + pl.track_pos = pl.group->track->duration; + } else if (command.param < -pl.track_pos) { + pl.track_pos = 0; + } else { + pl.track_pos += command.param; + } + ntf.result_code = BT_MCS_OPC_NTF_SUCCESS; + } else { + ntf.result_code = BT_MCS_OPC_NTF_CANNOT_BE_COMPLETED; + } + media_proxy_pl_track_position_cb(pl.track_pos); + media_proxy_pl_command_cb(ntf); + break; + case BT_MCS_OPC_PREV_SEGMENT: + /* Switch to previous segment if we are less than 5 seconds */ + /* into the segment, otherwise go to start of segment */ + if (pl.track_pos - PREV_MARGIN < + pl.group->track->segment->pos) { + do_prev_segment(&pl); + } + pl.track_pos = pl.group->track->segment->pos; + media_proxy_pl_track_position_cb(pl.track_pos); + ntf.result_code = BT_MCS_OPC_NTF_SUCCESS; + media_proxy_pl_command_cb(ntf); + break; + case BT_MCS_OPC_NEXT_SEGMENT: + do_next_segment(&pl); + pl.track_pos = pl.group->track->segment->pos; + media_proxy_pl_track_position_cb(pl.track_pos); + ntf.result_code = BT_MCS_OPC_NTF_SUCCESS; + media_proxy_pl_command_cb(ntf); + break; + case BT_MCS_OPC_FIRST_SEGMENT: + do_first_segment(&pl); + pl.track_pos = pl.group->track->segment->pos; + media_proxy_pl_track_position_cb(pl.track_pos); + ntf.result_code = BT_MCS_OPC_NTF_SUCCESS; + media_proxy_pl_command_cb(ntf); + break; + case BT_MCS_OPC_LAST_SEGMENT: + do_last_segment(&pl); + pl.track_pos = pl.group->track->segment->pos; + media_proxy_pl_track_position_cb(pl.track_pos); + ntf.result_code = BT_MCS_OPC_NTF_SUCCESS; + media_proxy_pl_command_cb(ntf); + break; + case BT_MCS_OPC_GOTO_SEGMENT: + if (command.use_param) { + if (command.param != 0) { + do_goto_segment(&pl, command.param); + pl.track_pos = pl.group->track->segment->pos; + media_proxy_pl_track_position_cb(pl.track_pos); + } + /* If the argument to "goto segment" is zero, */ + /* the segment shal stay the same, and the */ + /* track position shall not change. */ + ntf.result_code = BT_MCS_OPC_NTF_SUCCESS; + } else { + ntf.result_code = BT_MCS_OPC_NTF_CANNOT_BE_COMPLETED; + } + media_proxy_pl_command_cb(ntf); + break; + case BT_MCS_OPC_PREV_TRACK: + if (do_prev_track(&pl)) { + pl.track_pos = 0; + do_track_change_notifications(&pl); + } else { + /* For previous track, the position is reset to 0 */ + /* even if we stay at the same track (goto start of */ + /* track) */ + pl.track_pos = 0; + media_proxy_pl_track_position_cb(pl.track_pos); + } + ntf.result_code = BT_MCS_OPC_NTF_SUCCESS; + media_proxy_pl_command_cb(ntf); + break; + case BT_MCS_OPC_NEXT_TRACK: + if (pl.next_track_set) { + BT_DBG("Next track set"); + if (do_next_track_next_track_set(&pl)) { + do_group_change_notifications(&pl); + } + pl.track_pos = 0; + do_track_change_notifications(&pl); + } else if (do_next_track_normal_order(&pl)) { + pl.track_pos = 0; + do_track_change_notifications(&pl); + } + /* For next track, the position is kept if the track */ + /* does not change */ + ntf.result_code = BT_MCS_OPC_NTF_SUCCESS; + media_proxy_pl_command_cb(ntf); + break; + case BT_MCS_OPC_FIRST_TRACK: + if (do_first_track(&pl)) { + pl.track_pos = 0; + do_track_change_notifications(&pl); + } else { + /* For first track, the position is reset to 0 even */ + /* if we stay at the same track (goto start of track) */ + pl.track_pos = 0; + media_proxy_pl_track_position_cb(pl.track_pos); + } + ntf.result_code = BT_MCS_OPC_NTF_SUCCESS; + media_proxy_pl_command_cb(ntf); + break; + case BT_MCS_OPC_LAST_TRACK: + if (do_last_track(&pl)) { + pl.track_pos = 0; + do_track_change_notifications(&pl); + } else { + /* For last track, the position is reset to 0 even */ + /* if we stay at the same track (goto start of track) */ + pl.track_pos = 0; + media_proxy_pl_track_position_cb(pl.track_pos); + } + ntf.result_code = BT_MCS_OPC_NTF_SUCCESS; + media_proxy_pl_command_cb(ntf); + break; + case BT_MCS_OPC_GOTO_TRACK: + if (command.use_param) { + if (do_goto_track(&pl, command.param)) { + pl.track_pos = 0; + do_track_change_notifications(&pl); + } else { + /* For goto track, the position is reset to 0 */ + /* even if we stay at the same track (goto */ + /* start of track) */ + pl.track_pos = 0; + media_proxy_pl_track_position_cb(pl.track_pos); + } + ntf.result_code = BT_MCS_OPC_NTF_SUCCESS; + } else { + ntf.result_code = BT_MCS_OPC_NTF_CANNOT_BE_COMPLETED; + } + media_proxy_pl_command_cb(ntf); + break; + case BT_MCS_OPC_PREV_GROUP: + do_full_prev_group(&pl); + ntf.result_code = BT_MCS_OPC_NTF_SUCCESS; + media_proxy_pl_command_cb(ntf); + break; + case BT_MCS_OPC_NEXT_GROUP: + do_full_next_group(&pl); + ntf.result_code = BT_MCS_OPC_NTF_SUCCESS; + media_proxy_pl_command_cb(ntf); + break; + case BT_MCS_OPC_FIRST_GROUP: + do_full_first_group(&pl); + ntf.result_code = BT_MCS_OPC_NTF_SUCCESS; + media_proxy_pl_command_cb(ntf); + break; + case BT_MCS_OPC_LAST_GROUP: + do_full_last_group(&pl); + ntf.result_code = BT_MCS_OPC_NTF_SUCCESS; + media_proxy_pl_command_cb(ntf); + break; + case BT_MCS_OPC_GOTO_GROUP: + if (command.use_param) { + do_full_goto_group(&pl, command.param); + ntf.result_code = BT_MCS_OPC_NTF_SUCCESS; + } else { + ntf.result_code = BT_MCS_OPC_NTF_CANNOT_BE_COMPLETED; + } + media_proxy_pl_command_cb(ntf); + break; + default: + BT_DBG("Invalid command: %d", command.opcode); + ntf.result_code = BT_MCS_OPC_NTF_NOT_SUPPORTED; + media_proxy_pl_command_cb(ntf); + break; + } +} + +void seeking_state_command_handler(struct mpl_cmd command, + struct mpl_cmd_ntf ntf) +{ + BT_DBG("Command opcode: %d", command.opcode); + if (IS_ENABLED(CONFIG_BT_DEBUG_MCS)) { + if (command.use_param) { + BT_DBG("Command parameter: %d", command.param); + } + } + switch (command.opcode) { + case BT_MCS_OPC_PLAY: + pl.seeking_speed_factor = BT_MCS_SEEKING_SPEED_FACTOR_ZERO; + pl.state = BT_MCS_MEDIA_STATE_PLAYING; + media_proxy_pl_media_state_cb(pl.state); + media_proxy_pl_seeking_speed_cb(pl.seeking_speed_factor); + ntf.result_code = BT_MCS_OPC_NTF_SUCCESS; + media_proxy_pl_command_cb(ntf); + break; + case BT_MCS_OPC_PAUSE: + pl.seeking_speed_factor = BT_MCS_SEEKING_SPEED_FACTOR_ZERO; + /* TODO: Set track and track position */ + pl.state = BT_MCS_MEDIA_STATE_PAUSED; + media_proxy_pl_media_state_cb(pl.state); + media_proxy_pl_seeking_speed_cb(pl.seeking_speed_factor); + ntf.result_code = BT_MCS_OPC_NTF_SUCCESS; + media_proxy_pl_command_cb(ntf); + break; + case BT_MCS_OPC_FAST_REWIND: + /* TODO: Here, and for FAST_FORWARD */ + /* Decide on algorithm for muliple presses - add step (as */ + /* now) or double/half? */ + /* What about FR followed by FF? */ + /* Currently, the seeking speed may also become zero */ + /* Lowest value allowed by spec is -64, notify on change only */ + if (pl.seeking_speed_factor >= -(BT_MCS_SEEKING_SPEED_FACTOR_MAX + - MPL_SEEKING_SPEED_FACTOR_STEP)) { + pl.seeking_speed_factor -= MPL_SEEKING_SPEED_FACTOR_STEP; + media_proxy_pl_seeking_speed_cb(pl.seeking_speed_factor); + } + ntf.result_code = BT_MCS_OPC_NTF_SUCCESS; + media_proxy_pl_command_cb(ntf); + break; + case BT_MCS_OPC_FAST_FORWARD: + /* Highest value allowed by spec is 64, notify on change only */ + if (pl.seeking_speed_factor <= (BT_MCS_SEEKING_SPEED_FACTOR_MAX + - MPL_SEEKING_SPEED_FACTOR_STEP)) { + pl.seeking_speed_factor += MPL_SEEKING_SPEED_FACTOR_STEP; + media_proxy_pl_seeking_speed_cb(pl.seeking_speed_factor); + } + ntf.result_code = BT_MCS_OPC_NTF_SUCCESS; + media_proxy_pl_command_cb(ntf); + break; + case BT_MCS_OPC_STOP: + pl.seeking_speed_factor = BT_MCS_SEEKING_SPEED_FACTOR_ZERO; + pl.track_pos = 0; + pl.state = BT_MCS_MEDIA_STATE_PAUSED; + media_proxy_pl_media_state_cb(pl.state); + media_proxy_pl_seeking_speed_cb(pl.seeking_speed_factor); + media_proxy_pl_track_position_cb(pl.track_pos); + ntf.result_code = BT_MCS_OPC_NTF_SUCCESS; + media_proxy_pl_command_cb(ntf); + break; + case BT_MCS_OPC_MOVE_RELATIVE: + if (command.use_param) { + /* Keep within track - i.e. in the range 0 - duration */ + if (command.param > + pl.group->track->duration - pl.track_pos) { + pl.track_pos = pl.group->track->duration; + } else if (command.param < -pl.track_pos) { + pl.track_pos = 0; + } else { + pl.track_pos += command.param; + } + ntf.result_code = BT_MCS_OPC_NTF_SUCCESS; + } else { + ntf.result_code = BT_MCS_OPC_NTF_CANNOT_BE_COMPLETED; + } + media_proxy_pl_track_position_cb(pl.track_pos); + media_proxy_pl_command_cb(ntf); + break; + case BT_MCS_OPC_PREV_SEGMENT: + /* Switch to previous segment if we are less than 5 seconds */ + /* into the segment, otherwise go to start of segment */ + if (pl.track_pos - PREV_MARGIN < + pl.group->track->segment->pos) { + do_prev_segment(&pl); + } + pl.track_pos = pl.group->track->segment->pos; + media_proxy_pl_track_position_cb(pl.track_pos); + ntf.result_code = BT_MCS_OPC_NTF_SUCCESS; + media_proxy_pl_command_cb(ntf); + break; + case BT_MCS_OPC_NEXT_SEGMENT: + do_next_segment(&pl); + pl.track_pos = pl.group->track->segment->pos; + media_proxy_pl_track_position_cb(pl.track_pos); + ntf.result_code = BT_MCS_OPC_NTF_SUCCESS; + media_proxy_pl_command_cb(ntf); + break; + case BT_MCS_OPC_FIRST_SEGMENT: + do_first_segment(&pl); + pl.track_pos = pl.group->track->segment->pos; + media_proxy_pl_track_position_cb(pl.track_pos); + ntf.result_code = BT_MCS_OPC_NTF_SUCCESS; + media_proxy_pl_command_cb(ntf); + break; + case BT_MCS_OPC_LAST_SEGMENT: + do_last_segment(&pl); + pl.track_pos = pl.group->track->segment->pos; + media_proxy_pl_track_position_cb(pl.track_pos); + ntf.result_code = BT_MCS_OPC_NTF_SUCCESS; + media_proxy_pl_command_cb(ntf); + break; + case BT_MCS_OPC_GOTO_SEGMENT: + if (command.use_param) { + if (command.param != 0) { + do_goto_segment(&pl, command.param); + pl.track_pos = pl.group->track->segment->pos; + media_proxy_pl_track_position_cb(pl.track_pos); + } + /* If the argument to "goto segment" is zero, */ + /* the segment shal stay the same, and the */ + /* track position shall not change. */ + ntf.result_code = BT_MCS_OPC_NTF_SUCCESS; + } else { + ntf.result_code = BT_MCS_OPC_NTF_CANNOT_BE_COMPLETED; + } + media_proxy_pl_command_cb(ntf); + break; + case BT_MCS_OPC_PREV_TRACK: + if (do_prev_track(&pl)) { + pl.track_pos = 0; + do_track_change_notifications(&pl); + } else { + /* For previous track, the position is reset to 0 */ + /* even if we stay at the same track (goto start of */ + /* track) */ + pl.track_pos = 0; + media_proxy_pl_track_position_cb(pl.track_pos); + } + pl.seeking_speed_factor = BT_MCS_SEEKING_SPEED_FACTOR_ZERO; + pl.state = BT_MCS_MEDIA_STATE_PAUSED; + media_proxy_pl_media_state_cb(pl.state); + ntf.result_code = BT_MCS_OPC_NTF_SUCCESS; + media_proxy_pl_command_cb(ntf); + break; + case BT_MCS_OPC_NEXT_TRACK: + if (pl.next_track_set) { + BT_DBG("Next track set"); + if (do_next_track_next_track_set(&pl)) { + do_group_change_notifications(&pl); + } + pl.track_pos = 0; + do_track_change_notifications(&pl); + } else if (do_next_track_normal_order(&pl)) { + pl.track_pos = 0; + do_track_change_notifications(&pl); + } + /* For next track, the position is kept if the track */ + /* does not change */ + pl.seeking_speed_factor = BT_MCS_SEEKING_SPEED_FACTOR_ZERO; + pl.state = BT_MCS_MEDIA_STATE_PAUSED; + media_proxy_pl_media_state_cb(pl.state); + ntf.result_code = BT_MCS_OPC_NTF_SUCCESS; + media_proxy_pl_command_cb(ntf); + break; + case BT_MCS_OPC_FIRST_TRACK: + if (do_first_track(&pl)) { + pl.track_pos = 0; + do_track_change_notifications(&pl); + } else { + /* For first track, the position is reset to 0 even */ + /* if we stay at the same track (goto start of track) */ + pl.track_pos = 0; + media_proxy_pl_track_position_cb(pl.track_pos); + } + pl.seeking_speed_factor = BT_MCS_SEEKING_SPEED_FACTOR_ZERO; + pl.state = BT_MCS_MEDIA_STATE_PAUSED; + media_proxy_pl_media_state_cb(pl.state); + ntf.result_code = BT_MCS_OPC_NTF_SUCCESS; + media_proxy_pl_command_cb(ntf); + break; + case BT_MCS_OPC_LAST_TRACK: + if (do_last_track(&pl)) { + pl.track_pos = 0; + do_track_change_notifications(&pl); + } else { + /* For last track, the position is reset to 0 even */ + /* if we stay at the same track (goto start of track) */ + pl.track_pos = 0; + media_proxy_pl_track_position_cb(pl.track_pos); + } + pl.seeking_speed_factor = BT_MCS_SEEKING_SPEED_FACTOR_ZERO; + pl.state = BT_MCS_MEDIA_STATE_PAUSED; + media_proxy_pl_media_state_cb(pl.state); + ntf.result_code = BT_MCS_OPC_NTF_SUCCESS; + media_proxy_pl_command_cb(ntf); + break; + case BT_MCS_OPC_GOTO_TRACK: + if (command.use_param) { + if (do_goto_track(&pl, command.param)) { + pl.track_pos = 0; + do_track_change_notifications(&pl); + } else { + /* For goto track, the position is reset to 0 */ + /* even if we stay at the same track (goto */ + /* start of track) */ + pl.track_pos = 0; + media_proxy_pl_track_position_cb(pl.track_pos); + } + pl.seeking_speed_factor = BT_MCS_SEEKING_SPEED_FACTOR_ZERO; + pl.state = BT_MCS_MEDIA_STATE_PAUSED; + media_proxy_pl_media_state_cb(pl.state); + ntf.result_code = BT_MCS_OPC_NTF_SUCCESS; + } else { + ntf.result_code = BT_MCS_OPC_NTF_CANNOT_BE_COMPLETED; + } + media_proxy_pl_command_cb(ntf); + break; + case BT_MCS_OPC_PREV_GROUP: + do_full_prev_group(&pl); + pl.state = BT_MCS_MEDIA_STATE_PAUSED; + media_proxy_pl_media_state_cb(pl.state); + ntf.result_code = BT_MCS_OPC_NTF_SUCCESS; + media_proxy_pl_command_cb(ntf); + break; + case BT_MCS_OPC_NEXT_GROUP: + do_full_next_group(&pl); + pl.state = BT_MCS_MEDIA_STATE_PAUSED; + media_proxy_pl_media_state_cb(pl.state); + ntf.result_code = BT_MCS_OPC_NTF_SUCCESS; + media_proxy_pl_command_cb(ntf); + break; + case BT_MCS_OPC_FIRST_GROUP: + do_full_first_group(&pl); + pl.state = BT_MCS_MEDIA_STATE_PAUSED; + media_proxy_pl_media_state_cb(pl.state); + ntf.result_code = BT_MCS_OPC_NTF_SUCCESS; + media_proxy_pl_command_cb(ntf); + break; + case BT_MCS_OPC_LAST_GROUP: + do_full_last_group(&pl); + pl.state = BT_MCS_MEDIA_STATE_PAUSED; + media_proxy_pl_media_state_cb(pl.state); + ntf.result_code = BT_MCS_OPC_NTF_SUCCESS; + media_proxy_pl_command_cb(ntf); + break; + case BT_MCS_OPC_GOTO_GROUP: + if (command.use_param) { + do_full_goto_group(&pl, command.param); + pl.state = BT_MCS_MEDIA_STATE_PAUSED; + media_proxy_pl_media_state_cb(pl.state); + ntf.result_code = BT_MCS_OPC_NTF_SUCCESS; + } else { + ntf.result_code = BT_MCS_OPC_NTF_CANNOT_BE_COMPLETED; + } + media_proxy_pl_command_cb(ntf); + break; + default: + BT_DBG("Invalid command: %d", command.opcode); + ntf.result_code = BT_MCS_OPC_NTF_NOT_SUPPORTED; + media_proxy_pl_command_cb(ntf); + break; + } +} + +void (*command_handlers[BT_MCS_MEDIA_STATE_LAST])(struct mpl_cmd command, + struct mpl_cmd_ntf ntf) = { + inactive_state_command_handler, + playing_state_command_handler, + paused_state_command_handler, + seeking_state_command_handler +}; + +#ifdef CONFIG_BT_OTS +/* Find a track by ID + * + * If found, return pointers to the group of the track and the track, + * otherwise, the pointers returned are NULL + * + * Returns true if found, false otherwise + */ +static bool find_track_by_id(const struct mpl_mediaplayer *pl, uint64_t id, + struct mpl_group **group, struct mpl_track **track) +{ + struct mpl_group *tmp_group = pl->group; + struct mpl_track *tmp_track; + + while (tmp_group->prev != NULL) { + tmp_group = tmp_group->prev; + } + + while (tmp_group != NULL) { + tmp_track = tmp_group->track; + + while (tmp_track->prev != NULL) { + tmp_track = tmp_track->prev; + } + + while (tmp_track != 0) { + if (tmp_track->id == id) { + /* Found the track */ + *group = tmp_group; + *track = tmp_track; + return true; + } + + tmp_track = tmp_track->next; + } + + tmp_group = tmp_group->next; + } + + /* Track not found */ + *group = NULL; + *track = NULL; + return false; +} + +/* Find a group by ID + * + * If found, return pointer to the group, otherwise, the pointer returned is NULL + * + * Returns true if found, false otherwise + */ +static bool find_group_by_id(const struct mpl_mediaplayer *pl, uint64_t id, + struct mpl_group **group) +{ + struct mpl_group *tmp_group = pl->group; + + while (tmp_group->prev != NULL) { + tmp_group = tmp_group->prev; + } + + while (tmp_group != NULL) { + if (tmp_group->id == id) { + /* Found the group */ + *group = tmp_group; + return true; + } + + tmp_group = tmp_group->next; + } + + /* Group not found */ + *group = NULL; + return false; +} +#endif /* CONFIG_BT_OTS */ + +const char *get_player_name(void) +{ + return pl.name; +} + +#ifdef CONFIG_BT_OTS +uint64_t get_icon_id(void) +{ + return pl.icon_id; +} +#endif /* CONFIG_BT_OTS */ + +const char *get_icon_url(void) +{ + return pl.icon_url; +} + +const char *get_track_title(void) +{ + return pl.group->track->title; +} + +int32_t get_track_duration(void) +{ + return pl.group->track->duration; +} + +int32_t get_track_position(void) +{ + return pl.track_pos; +} + +void set_track_position(int32_t position) +{ + int32_t old_pos = pl.track_pos; + int32_t new_pos; + + if (position >= 0) { + if (position > pl.group->track->duration) { + /* Do not go beyond end of track */ + new_pos = pl.group->track->duration; + } else { + new_pos = position; + } + } else { + /* Negative position, handle as offset from _end_ of track */ + /* (Note minus sign below) */ + if (position < -pl.group->track->duration) { + new_pos = 0; + } else { + /* (Remember position is negative) */ + new_pos = pl.group->track->duration + position; + } + } + + BT_DBG("Pos. given: %d, resulting pos.: %d (duration is %d)", + position, new_pos, pl.group->track->duration); + + if (new_pos != old_pos) { + /* Set new position and notify it */ + pl.track_pos = new_pos; + media_proxy_pl_track_position_cb(new_pos); + } +} + +int8_t get_playback_speed(void) +{ + return pl.playback_speed_param; +} + +void set_playback_speed(int8_t speed) +{ + /* Set new speed parameter and notify, if different from current */ + if (speed != pl.playback_speed_param) { + pl.playback_speed_param = speed; + media_proxy_pl_playback_speed_cb(pl.playback_speed_param); + } +} + +int8_t get_seeking_speed(void) +{ + return pl.seeking_speed_factor; +} + +#ifdef CONFIG_BT_OTS +uint64_t get_track_segments_id(void) +{ + return pl.group->track->segments_id; +} + +uint64_t get_current_track_id(void) +{ + return pl.group->track->id; +} + +void set_current_track_id(uint64_t id) +{ + struct mpl_group *group; + struct mpl_track *track; + + BT_DBG_OBJ_ID("Track ID to set: ", id); + + if (find_track_by_id(&pl, id, &group, &track)) { + + if (pl.group != group) { + pl.group = group; + do_group_change_notifications(&pl); + + /* Group change implies track change (even if same track in other group) */ + pl.group->track = track; + do_track_change_notifications(&pl); + + } else if (pl.group->track != track) { + pl.group->track = track; + do_track_change_notifications(&pl); + } + return; + } + + BT_DBG("Track not found"); + + /* TODO: Should an error be returned here? + * That would require a rewrite of the MPL api to add return values to the functions. + */ +} + +uint64_t get_next_track_id(void) +{ + /* If the next track has been set explicitly */ + if (pl.next_track_set) { + return pl.next.track->id; + } + + /* Normal playing order */ + if (pl.group->track->next) { + return pl.group->track->next->id; + } + + /* Return zero value to indicate that there is no next track */ + return MPL_NO_TRACK_ID; +} + +void set_next_track_id(uint64_t id) +{ + struct mpl_group *group; + struct mpl_track *track; + + BT_DBG_OBJ_ID("Next Track ID to set: ", id); + + if (find_track_by_id(&pl, id, &group, &track)) { + + pl.next_track_set = true; + pl.next.group = group; + pl.next.track = track; + media_proxy_pl_next_track_id_cb(id); + return; + } + + BT_DBG("Track not found"); +} + +uint64_t get_parent_group_id(void) +{ + return pl.group->parent->id; +} + +uint64_t get_current_group_id(void) +{ + return pl.group->id; +} + +void set_current_group_id(uint64_t id) +{ + struct mpl_group *group; + bool track_change; + + BT_DBG_OBJ_ID("Group ID to set: ", id); + + if (find_group_by_id(&pl, id, &group)) { + + if (pl.group != group) { + /* Change to found group */ + pl.group = group; + do_group_change_notifications(&pl); + + /* And change to first track in group */ + track_change = do_first_track(&pl); + if (track_change) { + do_track_change_notifications(&pl); + } + } + return; + } + + BT_DBG("Group not found"); +} +#endif /* CONFIG_BT_OTS */ + +uint8_t get_playing_order(void) +{ + return pl.playing_order; +} + +void set_playing_order(uint8_t order) +{ + if (order != pl.playing_order) { + if (BIT(order - 1) & pl.playing_orders_supported) { + pl.playing_order = order; + media_proxy_pl_playing_order_cb(pl.playing_order); + } + } +} + +uint16_t get_playing_orders_supported(void) +{ + return pl.playing_orders_supported; +} + +uint8_t get_media_state(void) +{ + return pl.state; +} + +void send_command(struct mpl_cmd command) +{ + struct mpl_cmd_ntf ntf; + + BT_DBG("opcode: %d, param: %d", command.opcode, command.param); + + if (pl.state < BT_MCS_MEDIA_STATE_LAST) { + ntf.requested_opcode = command.opcode; + command_handlers[pl.state](command, ntf); + } else { + BT_DBG("INVALID STATE"); + } +} + +uint32_t get_commands_supported(void) +{ + return pl.opcodes_supported; +} + +#ifdef CONFIG_BT_OTS +static void parse_search(struct mpl_search search) +{ + uint8_t index = 0; + struct mpl_sci sci; + uint8_t sci_num = 0; + bool search_failed = false; + + if (search.len > SEARCH_LEN_MAX) { + BT_WARN("Search too long (%d) - truncating", search.len); + search.len = SEARCH_LEN_MAX; + } + BT_DBG("Parsing %d octets search", search.len); + + while (search.len - index > 0) { + sci.len = (uint8_t)search.search[index++]; + if (sci.len < SEARCH_SCI_LEN_MIN) { + BT_WARN("Invalid length field - too small"); + search_failed = true; + break; + } + if (sci.len > (search.len - index)) { + BT_WARN("Incomplete search control item"); + search_failed = true; + break; + } + sci.type = (uint8_t)search.search[index++]; + if (sci.type < BT_MCS_SEARCH_TYPE_TRACK_NAME || + sci.type > BT_MCS_SEARCH_TYPE_ONLY_GROUPS) { + search_failed = true; + break; + } + memcpy(&sci.param, &search.search[index], sci.len - 1); + index += sci.len - 1; + + BT_DBG("SCI # %d: type: %d", sci_num, sci.type); + BT_HEXDUMP_DBG(sci.param, sci.len-1, "param:"); + sci_num++; + } + + /* TODO: Add real search functionality. */ + /* For now, just fake it. */ + + if (search_failed) { + pl.search_results_id = 0; + media_proxy_pl_search_cb(BT_MCS_SCP_NTF_FAILURE); + } else { + /* Use current group as search result for now */ + pl.search_results_id = pl.group->id; + media_proxy_pl_search_cb(BT_MCS_SCP_NTF_SUCCESS); + } + + media_proxy_pl_search_results_id_cb(pl.search_results_id); +} + +void send_search(struct mpl_search search) +{ + if (search.len > SEARCH_LEN_MAX) { + BT_WARN("Search too long: %d", search.len); + } + + BT_HEXDUMP_DBG(search.search, search.len, "Search"); + + parse_search(search); +} + +uint64_t get_search_results_id(void) +{ + return pl.search_results_id; +} +#endif /* CONFIG_BT_OTS */ + +uint8_t get_content_ctrl_id(void) +{ + return pl.content_ctrl_id; +} + +int media_proxy_pl_init(void) +{ + static bool initialized; + int ret; + + if (initialized) { + BT_DBG("Already initialized"); + return -EALREADY; + } + + /* Set up the media control service */ +#ifdef CONFIG_BT_OTS + ret = bt_mcs_init(&ots_cbs); +#else + ret = bt_mcs_init(NULL); +#endif /* CONFIG_BT_OTS */ + + if (ret < 0) { + BT_ERR("Could not init MCS: %d", ret); + return ret; + } + + /* Get a Content Control ID */ + pl.content_ctrl_id = bt_ccid_get_value(); + +#ifdef CONFIG_BT_OTS + /* Initialize the object content buffer */ + net_buf_simple_init(obj.content, 0); + + /* Icon Object */ + ret = add_icon_object(&pl); + if (ret < 0) { + BT_ERR("Unable to add icon object, error %d", ret); + return ret; + } + + /* Add all tracks and groups to OTS */ + ret = add_group_and_track_objects(&pl); + if (ret < 0) { + BT_ERR("Error adding tracks and groups to OTS, error %d", ret); + return ret; + } + + /* Initial setup of Track Segments Object */ + /* TODO: Later, this should be done when the tracks are added */ + /* but for no only one of the tracks has segments .*/ + ret = add_current_track_segments_object(&pl); + if (ret < 0) { + BT_ERR("Error adding Track Segments Object to OTS, error %d", ret); + return ret; + } +#endif /* CONFIG_BT_OTS */ + + /* Set up the calls structure */ + pl.calls.get_player_name = get_player_name; +#ifdef CONFIG_BT_OTS + pl.calls.get_icon_id = get_icon_id; +#endif /* CONFIG_BT_OTS */ + pl.calls.get_icon_url = get_icon_url; + pl.calls.get_track_title = get_track_title; + pl.calls.get_track_duration = get_track_duration; + pl.calls.get_track_position = get_track_position; + pl.calls.set_track_position = set_track_position; + pl.calls.get_playback_speed = get_playback_speed; + pl.calls.set_playback_speed = set_playback_speed; + pl.calls.get_seeking_speed = get_seeking_speed; +#ifdef CONFIG_BT_OTS + pl.calls.get_track_segments_id = get_track_segments_id; + pl.calls.get_current_track_id = get_current_track_id; + pl.calls.set_current_track_id = set_current_track_id; + pl.calls.get_next_track_id = get_next_track_id; + pl.calls.set_next_track_id = set_next_track_id; + pl.calls.get_parent_group_id = get_parent_group_id; + pl.calls.get_current_group_id = get_current_group_id; + pl.calls.set_current_group_id = set_current_group_id; +#endif /* CONFIG_BT_OTS */ + pl.calls.get_playing_order = get_playing_order; + pl.calls.set_playing_order = set_playing_order; + pl.calls.get_playing_orders_supported = get_playing_orders_supported; + pl.calls.get_media_state = get_media_state; + pl.calls.send_command = send_command; +#ifdef CONFIG_BT_OTS + pl.calls.send_search = send_search; + pl.calls.get_search_results_id = get_search_results_id; +#endif /* CONFIG_BT_OTS */ + pl.calls.get_content_ctrl_id = get_content_ctrl_id; + + ret = media_proxy_pl_register(&pl.calls); + if (ret < 0) { + BT_ERR("Unable to register player"); + return ret; + } + + initialized = true; + return 0; +} + +#if CONFIG_BT_DEBUG_MCS /* Special commands for debugging */ + +void mpl_debug_dump_state(void) +{ +#if CONFIG_BT_OTS + char t[BT_OTS_OBJ_ID_STR_LEN]; + struct mpl_group *group; + struct mpl_track *track; +#endif /* CONFIG_BT_OTS */ + + BT_DBG("Mediaplayer name: %s", log_strdup(pl.name)); + +#if CONFIG_BT_OTS + (void)bt_ots_obj_id_to_str(pl.icon_id, t, sizeof(t)); + BT_DBG("Icon ID: %s", log_strdup(t)); +#endif /* CONFIG_BT_OTS */ + + BT_DBG("Icon URL: %s", log_strdup(pl.icon_url)); + BT_DBG("Track position: %d", pl.track_pos); + BT_DBG("Media state: %d", pl.state); + BT_DBG("Playback speed parameter: %d", pl.playback_speed_param); + BT_DBG("Seeking speed factor: %d", pl.seeking_speed_factor); + BT_DBG("Playing order: %d", pl.playing_order); + BT_DBG("Playing orders supported: 0x%x", pl.playing_orders_supported); + BT_DBG("Opcodes supported: %d", pl.opcodes_supported); + BT_DBG("Content control ID: %d", pl.content_ctrl_id); + +#if CONFIG_BT_OTS + (void)bt_ots_obj_id_to_str(pl.group->parent->id, t, sizeof(t)); + BT_DBG("Current group's parent: %s", log_strdup(t)); + + (void)bt_ots_obj_id_to_str(pl.group->id, t, sizeof(t)); + BT_DBG("Current group: %s", log_strdup(t)); + + (void)bt_ots_obj_id_to_str(pl.group->track->id, t, sizeof(t)); + BT_DBG("Current track: %s", log_strdup(t)); + + if (pl.next_track_set) { + (void)bt_ots_obj_id_to_str(pl.next.track->id, t, sizeof(t)); + BT_DBG("Next track: %s", log_strdup(t)); + } else if (pl.group->track->next) { + (void)bt_ots_obj_id_to_str(pl.group->track->next->id, t, + sizeof(t)); + BT_DBG("Next track: %s", log_strdup(t)); + } else { + BT_DBG("No next track"); + } + + if (pl.search_results_id) { + (void)bt_ots_obj_id_to_str(pl.search_results_id, t, sizeof(t)); + BT_DBG("Search results: %s", log_strdup(t)); + } else { + BT_DBG("No search results"); + } + + BT_DBG("Groups and tracks:"); + group = pl.group; + + while (group->prev != NULL) { + group = group->prev; + } + + while (group) { + (void)bt_ots_obj_id_to_str(group->id, t, sizeof(t)); + BT_DBG("Group: %s, %s", log_strdup(t), + log_strdup(group->title)); + + (void)bt_ots_obj_id_to_str(group->parent->id, t, sizeof(t)); + BT_DBG("\tParent: %s, %s", log_strdup(t), + log_strdup(group->parent->title)); + + track = group->track; + while (track->prev != NULL) { + track = track->prev; + } + + while (track) { + (void)bt_ots_obj_id_to_str(track->id, t, sizeof(t)); + BT_DBG("\tTrack: %s, %s, duration: %d", log_strdup(t), + log_strdup(track->title), track->duration); + track = track->next; + } + + group = group->next; + } +#endif /* CONFIG_BT_OTS */ +} +#endif /* CONFIG_BT_DEBUG_MCS */ + + +#if defined(CONFIG_BT_DEBUG_MCS) && defined(CONFIG_BT_TESTING) /* Special commands for testing */ + +#if CONFIG_BT_OTS +void mpl_test_unset_parent_group(void) +{ + BT_DBG("Setting current group to be it's own parent"); + pl.group->parent = pl.group; +} +#endif /* CONFIG_BT_OTS */ + +void mpl_test_media_state_set(uint8_t state) +{ + pl.state = state; + media_proxy_pl_media_state_cb(pl.state); +} + +void mpl_test_track_changed_cb(void) +{ + media_proxy_pl_track_changed_cb(); +} + +void mpl_test_title_changed_cb(void) +{ + media_proxy_pl_track_title_cb(pl.group->track->title); +} + +void mpl_test_duration_changed_cb(void) +{ + media_proxy_pl_track_duration_cb(pl.group->track->duration); +} + +void mpl_test_position_changed_cb(void) +{ + media_proxy_pl_track_position_cb(pl.track_pos); +} + +void mpl_test_playback_speed_changed_cb(void) +{ + media_proxy_pl_playback_speed_cb(pl.playback_speed_param); +} + +void mpl_test_seeking_speed_changed_cb(void) +{ + media_proxy_pl_seeking_speed_cb(pl.seeking_speed_factor); +} + +#ifdef CONFIG_BT_OTS +void mpl_test_current_track_id_changed_cb(void) +{ + media_proxy_pl_current_track_id_cb(pl.group->track->id); +} + +void mpl_test_next_track_id_changed_cb(void) +{ + media_proxy_pl_next_track_id_cb(pl.group->track->next->id); +} + +void mpl_test_parent_group_id_changed_cb(void) +{ + media_proxy_pl_parent_group_id_cb(pl.group->id); +} + +void mpl_test_current_group_id_changed_cb(void) +{ + media_proxy_pl_current_group_id_cb(pl.group->id); +} +#endif /* CONFIG_BT_OTS */ + +void mpl_test_playing_order_changed_cb(void) +{ + media_proxy_pl_playing_order_cb(pl.playing_order); +} + +void mpl_test_media_state_changed_cb(void) +{ + media_proxy_pl_media_state_cb(pl.playing_order); +} + +void mpl_test_opcodes_supported_changed_cb(void) +{ + media_proxy_pl_commands_supported_cb(pl.opcodes_supported); +} + +#ifdef CONFIG_BT_OTS +void mpl_test_search_results_changed_cb(void) +{ + media_proxy_pl_search_cb(pl.search_results_id); +} +#endif /* CONFIG_BT_OTS */ + +#endif /* CONFIG_BT_DEBUG_MCS && CONFIG_BT_TESTING */ diff --git a/subsys/bluetooth/audio/mpl_internal.h b/subsys/bluetooth/audio/mpl_internal.h new file mode 100644 index 00000000000..eed0b5beb7a --- /dev/null +++ b/subsys/bluetooth/audio/mpl_internal.h @@ -0,0 +1,153 @@ +/** + * @file + * @brief Internal header for the media player (MPL). + * + * Copyright (c) 2019 - 2021 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#ifndef ZEPHYR_SUBSYS_BLUETOOTH_AUDIO_MPL_INTERNAL_ +#define ZEPHYR_SUBSYS_BLUETOOTH_AUDIO_MPL_INTERNAL_ + +#include + +#ifdef __cplusplus +extern "C" { +#endif + + +/* Offset into a segment/track before the "previous" command goes to start of */ +/* current segment/track instead to previous */ +#define PREV_MARGIN 500 /* 500 * 0.01 = 5 seconds */ + +/* Increase/decrease in seeking sped factor for fast rewind/forward commands + * Set this equal to the minimum speed factor, to ensure only valid speed factors + * are used when changing to/from zero + */ +#define MPL_SEEKING_SPEED_FACTOR_STEP BT_MCS_SEEKING_SPEED_FACTOR_MIN + + +/* Track segments */ +struct mpl_tseg { + uint8_t name_len; + char name[CONFIG_BT_MCS_SEGMENT_NAME_MAX]; + int32_t pos; + struct mpl_tseg *prev; + struct mpl_tseg *next; +}; + +/* Tracks */ +struct mpl_track { +#if defined(CONFIG_BT_OTS) || defined(CONFIG_BT_OTC) + uint64_t id; +#endif /* CONFIG_BT_OTS || CONFIG_BT_OTC */ + char title[CONFIG_BT_MCS_TRACK_TITLE_MAX]; + int32_t duration; + struct mpl_tseg *segment; +#if defined(CONFIG_BT_OTS) || defined(CONFIG_BT_OTC) + uint64_t segments_id; +#endif /* CONFIG_BT_OTS || CONFIG_BT_OTC */ + struct mpl_track *prev; + struct mpl_track *next; +}; + +/* Groups */ +struct mpl_group { +#if defined(CONFIG_BT_OTS) || defined(CONFIG_BT_OTC) + uint64_t id; +#endif /* CONFIG_BT_OTS || CONFIG_BT_OTC */ + char title[CONFIG_BT_MCS_GROUP_TITLE_MAX]; + struct mpl_track *track; + struct mpl_group *parent; + struct mpl_group *prev; + struct mpl_group *next; +}; + +/** @brief Media Player */ +struct mpl_mediaplayer { + char name[CONFIG_BT_MCS_MEDIA_PLAYER_NAME_MAX]; +#if defined(CONFIG_BT_OTS) || defined(CONFIG_BT_OTC) + uint64_t icon_id; +#endif /* CONFIG_BT_OTS || CONFIG_BT_OTC */ + char icon_url[CONFIG_BT_MCS_ICON_URL_MAX]; + struct mpl_group *group; + int32_t track_pos; + uint8_t state; + int8_t playback_speed_param; + int8_t seeking_speed_factor; + uint8_t playing_order; + uint16_t playing_orders_supported; + uint32_t opcodes_supported; +#if defined(CONFIG_BT_OTS) || defined(CONFIG_BT_OTC) + uint64_t search_results_id; +#endif /* CONFIG_BT_OTS || CONFIG_BT_OTC */ + uint8_t content_ctrl_id; + struct media_proxy_pl_calls calls; + + bool next_track_set; /* If next track explicitly set */ + struct { + struct mpl_track *track; /* The track explicitly set as next track */ + struct mpl_group *group; /* The group of the set track */ + } next; +}; + + +/* Special calls for testing */ + +/* For IOP testing - set current group to be it's own parent */ +void mpl_test_unset_parent_group(void); + +/* Force the media player into a given state */ +void mpl_test_media_state_set(uint8_t state); + +/* Trigger track changed callback */ +void mpl_test_track_changed_cb(void); + +/* Trigger title changed callback */ +void mpl_test_title_changed_cb(void); + +/* Trigger duration changed callback */ +void mpl_test_duration_changed_cb(void); + +/* Trigger position changed callback */ +void mpl_test_position_changed_cb(void); + +/* Trigger playback speed changed callback */ +void mpl_test_playback_speed_changed_cb(void); + +/* Trigger seeking speed changed callback */ +void mpl_test_seeking_speed_changed_cb(void); + +/* Trigger current track id changed callback */ +void mpl_test_current_track_id_changed_cb(void); + +/* Trigger next track id changed callback */ +void mpl_test_next_track_id_changed_cb(void); + +/* Trigger current group id changed callback */ +void mpl_test_current_group_id_changed_cb(void); + +/* Trigger parent group id changed callback */ +void mpl_test_parent_group_id_changed_cb(void); + +/* Trigger playinge order changed callback */ +void mpl_test_playing_order_changed_cb(void); + +/* Trigger media state changed callback */ +void mpl_test_media_state_changed_cb(void); + +/* Trigger operations supported changed callback */ +void mpl_test_opcodes_supported_changed_cb(void); + +/* Trigger search results changed callback */ +void mpl_test_search_results_changed_cb(void); + +/* Output the mediaplayer's state information */ +void mpl_debug_dump_state(void); + +#ifdef __cplusplus +} +#endif + +#endif /* ZEPHYR_SUBSYS_BLUETOOTH_AUDIO_MPL_INTERNAL_*/