From 60d7c35e67b27890986322976269eb534908fcb9 Mon Sep 17 00:00:00 2001 From: Matthias Ringwald Date: Fri, 21 Apr 2023 16:37:24 +0200 Subject: [PATCH] example/a2dp_sink_demo: support cover art --- example/a2dp_sink_demo.c | 229 ++++++++++++++++++++++++++++++++++----- 1 file changed, 200 insertions(+), 29 deletions(-) diff --git a/example/a2dp_sink_demo.c b/example/a2dp_sink_demo.c index 1e7a9c224..16ee46b91 100644 --- a/example/a2dp_sink_demo.c +++ b/example/a2dp_sink_demo.c @@ -92,7 +92,7 @@ #define MAX_SBC_FRAME_SIZE 120 #ifdef HAVE_BTSTACK_STDIN -static const char * device_addr_string = "5C:F3:70:60:7B:87"; // pts +static const char * device_addr_string = "00:1B:DC:08:E2:72"; // pts v5.0 static bd_addr_t device_addr; #endif @@ -152,6 +152,30 @@ static int request_frames; static int volume_percentage = 0; static avrcp_battery_status_t battery_status = AVRCP_BATTERY_STATUS_WARNING; +#ifdef ENABLE_AVRCP_COVER_ART +static char a2dp_sink_demo_image_handle[8]; +static avrcp_cover_art_client_t a2dp_sink_demo_cover_art_client; +static bool a2dp_sink_demo_cover_art_client_connected; +static uint16_t a2dp_sink_demo_cover_art_cid; +static uint8_t a2dp_sink_demo_ertm_buffer[2000]; +static l2cap_ertm_config_t a2dp_sink_demo_ertm_config = { + 1, // ertm mandatory + 2, // max transmit, some tests require > 1 + 2000, + 12000, + 512, // l2cap ertm mtu + 2, + 2, + 1, // 16-bit FCS +}; +static bool a2dp_sink_cover_art_download_active; +static uint32_t a2dp_sink_cover_art_file_size; +#ifdef HAVE_POSIX_FILE_IO +static const char * a2dp_sink_demo_thumbnail_path = "cover.jpg"; +static FILE * a2dp_sink_cover_art_file; +#endif +#endif + typedef struct { uint8_t reconfigure; uint8_t num_channels; @@ -236,7 +260,10 @@ static int setup_demo(void){ // Initialize LE Security Manager. Needed for cross-transport key derivation sm_init(); #endif - +#ifdef ENABLE_AVRCP_COVER_ART + goep_client_init(); + avrcp_cover_art_client_init(); +#endif // Init profiles a2dp_sink_init(); @@ -273,9 +300,12 @@ static int setup_demo(void){ // - Create AVRCP Controller service record and register it with SDP. We send Category 1 commands to the media player, e.g. play/pause memset(sdp_avrcp_controller_service_buffer, 0, sizeof(sdp_avrcp_controller_service_buffer)); - uint16_t controller_supported_features = AVRCP_FEATURE_MASK_CATEGORY_PLAYER_OR_RECORDER; + uint16_t controller_supported_features = 1 << AVRCP_CONTROLLER_SUPPORTED_FEATURE_CATEGORY_PLAYER_OR_RECORDER; #ifdef AVRCP_BROWSING_ENABLED - controller_supported_features |= AVRCP_FEATURE_MASK_BROWSING; + controller_supported_features |= 1 << AVRCP_CONTROLLER_SUPPORTED_FEATURE_BROWSING; +#endif +#ifdef ENABLE_AVRCP_COVER_ART + controller_supported_features |= 1 << AVRCP_CONTROLLER_SUPPORTED_FEATURE_COVER_ART_GET_LINKED_THUMBNAIL; #endif avrcp_controller_create_sdp_record(sdp_avrcp_controller_service_buffer, sdp_create_service_record_handle(), controller_supported_features, NULL, NULL); @@ -284,7 +314,7 @@ static int setup_demo(void){ // - Create and register A2DP Sink service record // - We receive Category 2 commands from the media player, e.g. volume up/down memset(sdp_avrcp_target_service_buffer, 0, sizeof(sdp_avrcp_target_service_buffer)); - uint16_t target_supported_features = AVRCP_FEATURE_MASK_CATEGORY_MONITOR_OR_AMPLIFIER; + uint16_t target_supported_features = 1 << AVRCP_TARGET_SUPPORTED_FEATURE_CATEGORY_MONITOR_OR_AMPLIFIER; avrcp_target_create_sdp_record(sdp_avrcp_target_service_buffer, sdp_create_service_record_handle(), target_supported_features, NULL, NULL); sdp_register_service(sdp_avrcp_target_service_buffer); @@ -622,6 +652,96 @@ static void dump_sbc_configuration(media_codec_configuration_sbc_t * configurati printf("\n"); } +static void hci_packet_handler(uint8_t packet_type, uint16_t channel, uint8_t *packet, uint16_t size){ + UNUSED(channel); + UNUSED(size); + if (packet_type != HCI_EVENT_PACKET) return; + if (hci_event_packet_get_type(packet) == HCI_EVENT_PIN_CODE_REQUEST) { + bd_addr_t address; + printf("Pin code request - using '0000'\n"); + hci_event_pin_code_request_get_bd_addr(packet, address); + gap_pin_code_response(address, "0000"); + } +} + +#ifdef ENABLE_AVRCP_COVER_ART +static void a2dp_sink_demo_cover_art_packet_handler(uint8_t packet_type, uint16_t channel, uint8_t *packet, uint16_t size) { + UNUSED(channel); + UNUSED(size); + uint8_t status; + uint16_t cid; + switch (packet_type){ + case BIP_DATA_PACKET: + if (a2dp_sink_cover_art_download_active){ + a2dp_sink_cover_art_file_size += size; +#ifdef HAVE_POSIX_FILE_IO + fwrite(packet, 1, size, a2dp_sink_cover_art_file); +#else + printf("Cover art : TODO - store %u bytes image data\n", size); +#endif + } else { + uint16_t i; + for (i=0;iavrcp_cid); +#ifdef HAVE_BTSTACK_STDIN + // use address for outgoing connections + avrcp_subevent_connection_established_get_bd_addr(packet, device_addr); +#endif + avrcp_target_support_event(connection->avrcp_cid, AVRCP_NOTIFICATION_EVENT_VOLUME_CHANGED); avrcp_target_support_event(connection->avrcp_cid, AVRCP_NOTIFICATION_EVENT_BATT_STATUS_CHANGED); avrcp_target_battery_status_changed(connection->avrcp_cid, battery_status); @@ -688,7 +813,7 @@ static void avrcp_controller_packet_handler(uint8_t packet_type, uint16_t channe break; case AVRCP_SUBEVENT_GET_CAPABILITY_EVENT_ID_DONE: - printf("\nSupported remote AVRCP Target notifications:\n"); + printf("AVRCP Controller: supported notifications by target:\n"); for (event_id = (uint8_t) AVRCP_NOTIFICATION_EVENT_FIRST_INDEX; event_id < (uint8_t) AVRCP_NOTIFICATION_EVENT_LAST_INDEX; event_id++){ printf(" - [%s] %s\n", (avrcp_connection->notifications_supported_by_target & (1 << event_id)) != 0 ? "X" : " ", @@ -700,6 +825,13 @@ static void avrcp_controller_packet_handler(uint8_t packet_type, uint16_t channe avrcp_controller_enable_notification(avrcp_connection->avrcp_cid, AVRCP_NOTIFICATION_EVENT_PLAYBACK_STATUS_CHANGED); avrcp_controller_enable_notification(avrcp_connection->avrcp_cid, AVRCP_NOTIFICATION_EVENT_NOW_PLAYING_CONTENT_CHANGED); avrcp_controller_enable_notification(avrcp_connection->avrcp_cid, AVRCP_NOTIFICATION_EVENT_TRACK_CHANGED); + +#ifdef ENABLE_AVRCP_COVER_ART + // image handles become invalid on player change, registe for notifications + avrcp_controller_enable_notification(a2dp_sink_demo_avrcp_connection.avrcp_cid, AVRCP_NOTIFICATION_EVENT_UIDS_CHANGED); + // trigger cover art client connection + a2dp_sink_demo_cover_art_connect(); +#endif break; case AVRCP_SUBEVENT_NOTIFICATION_STATE: @@ -776,7 +908,7 @@ static void avrcp_controller_packet_handler(uint8_t packet_type, uint16_t channe printf("AVRCP Controller: Genre %s\n", avrcp_subevent_value); } break; - + case AVRCP_SUBEVENT_PLAY_STATUS: printf("AVRCP Controller: Song length %"PRIu32" ms, Song position %"PRIu32" ms, Play status %s\n", avrcp_subevent_play_status_get_song_length(packet), @@ -799,7 +931,23 @@ static void avrcp_controller_packet_handler(uint8_t packet_type, uint16_t channe case AVRCP_SUBEVENT_PLAYER_APPLICATION_VALUE_RESPONSE: printf("AVRCP Controller: Set Player App Value %s\n", avrcp_ctype2str(avrcp_subevent_player_application_value_response_get_command_type(packet))); break; - + +#ifdef ENABLE_AVRCP_COVER_ART + case AVRCP_SUBEVENT_NOTIFICATION_EVENT_UIDS_CHANGED: + if (a2dp_sink_demo_cover_art_client_connected){ + printf("AVRCP Controller: UIDs changed -> disconnect cover art client\n"); + avrcp_cover_art_client_disconnect(a2dp_sink_demo_cover_art_cid); + } + break; + + case AVRCP_SUBEVENT_NOW_PLAYING_COVER_ART_INFO: + if (avrcp_subevent_now_playing_cover_art_info_get_value_len(packet) == 7){ + memcpy(a2dp_sink_demo_image_handle, avrcp_subevent_now_playing_cover_art_info_get_value(packet), 7); + printf("AVRCP Controller: Cover Art %s\n", a2dp_sink_demo_image_handle); + } + break; +#endif + default: break; } @@ -851,18 +999,6 @@ static void avrcp_target_packet_handler(uint8_t packet_type, uint16_t channel, u } } -static void hci_packet_handler(uint8_t packet_type, uint16_t channel, uint8_t *packet, uint16_t size){ - UNUSED(channel); - UNUSED(size); - if (packet_type != HCI_EVENT_PACKET) return; - if (hci_event_packet_get_type(packet) == HCI_EVENT_PIN_CODE_REQUEST) { - bd_addr_t address; - printf("Pin code request - using '0000'\n"); - hci_event_pin_code_request_get_bd_addr(packet, address); - gap_pin_code_response(address, "0000"); - } -} - static void a2dp_sink_packet_handler(uint8_t packet_type, uint16_t channel, uint8_t *packet, uint16_t size){ UNUSED(channel); UNUSED(size); @@ -978,15 +1114,13 @@ static void a2dp_sink_packet_handler(uint8_t packet_type, uint16_t channel, uint static void show_usage(void){ bd_addr_t iut_address; gap_local_bd_addr(iut_address); - printf("\n--- Bluetooth AVDTP Sink/AVRCP Connection Test Console %s ---\n", bd_addr_to_str(iut_address)); - printf("b - AVDTP Sink create connection to addr %s\n", bd_addr_to_str(device_addr)); - printf("B - AVDTP Sink disconnect\n"); - printf("c - AVRCP create connection to addr %s\n", bd_addr_to_str(device_addr)); - printf("C - AVRCP disconnect\n"); + printf("\n--- A2DP Sink Demo Console %s ---\n", bd_addr_to_str(iut_address)); + printf("b - A2DP Sink create connection to addr %s\n", bd_addr_to_str(device_addr)); + printf("B - A2DP Sink disconnect\n"); - printf("w - delay report\n"); - - printf("\n--- Bluetooth AVRCP Commands %s ---\n", bd_addr_to_str(iut_address)); + printf("\n--- AVRCP Controller ---\n"); + printf("c - AVRCP create connection to addr %s\n", bd_addr_to_str(device_addr)); + printf("C - AVRCP disconnect\n"); printf("O - get play status\n"); printf("j - get now playing info\n"); printf("k - play\n"); @@ -1002,6 +1136,7 @@ static void show_usage(void){ printf("r - skip\n"); printf("q - query repeat and shuffle mode\n"); printf("v - repeat single track\n"); + printf("w - delay report\n"); printf("x - repeat all tracks\n"); printf("X - disable repeat mode\n"); printf("z - shuffle all tracks\n"); @@ -1016,11 +1151,28 @@ static void show_usage(void){ printf("t - volume up for 10 percent\n"); printf("T - volume down for 10 percent\n"); printf("V - toggle Battery status from AVRCP_BATTERY_STATUS_NORMAL to AVRCP_BATTERY_STATUS_FULL_CHARGE\n"); + +#ifdef ENABLE_AVRCP_COVER_ART + printf("\n--- Cover Art Client ---\n"); + printf("d - connect to addr %s\n", bd_addr_to_str(device_addr)); + printf("D - disconnect\n"); + if (a2dp_sink_demo_cover_art_client_connected == false){ + if (a2dp_sink_demo_avrcp_connection.avrcp_cid == 0){ + printf("Not connected, press 'b' or 'c' to first connect AVRCP, then press 'd' to connect cover art client\n"); + } else { + printf("Not connected, press 'd' to connect cover art client\n"); + } + } else if (a2dp_sink_demo_image_handle[0] == 0){ + printf("No image handle, use 'j' to get current track info\n"); + } printf("---\n"); +#endif + } #endif #ifdef HAVE_BTSTACK_STDIN + static void stdin_process(char cmd){ uint8_t status = ERROR_CODE_SUCCESS; uint8_t volume; @@ -1048,7 +1200,7 @@ static void stdin_process(char cmd){ printf(" - AVRCP disconnect from addr %s.\n", bd_addr_to_str(device_addr)); status = avrcp_disconnect(avrcp_connection->avrcp_cid); break; - + case '\n': case '\r': break; @@ -1182,6 +1334,25 @@ static void stdin_process(char cmd){ printf("AVRCP: release long button press REWIND\n"); status = avrcp_controller_release_press_and_hold_cmd(avrcp_connection->avrcp_cid); break; +#ifdef ENABLE_AVRCP_COVER_ART + case 'd': + printf(" - Create AVRCP Cover Art connection to addr %s.\n", bd_addr_to_str(device_addr)); + status = a2dp_sink_demo_cover_art_connect(); + break; + case 'D': + printf(" - AVRCP Cover Art disconnect from addr %s.\n", bd_addr_to_str(device_addr)); + status = avrcp_cover_art_client_disconnect(a2dp_sink_demo_cover_art_cid); + break; + case '@': + printf("Get linked thumbnail for '%s'\n", a2dp_sink_demo_image_handle); +#ifdef HAVE_POSIX_FILE_IO + a2dp_sink_cover_art_file = fopen(a2dp_sink_demo_thumbnail_path, "w"); +#endif + a2dp_sink_cover_art_download_active = true; + a2dp_sink_cover_art_file_size = 0; + status = avrcp_cover_art_client_get_linked_thumbnail(a2dp_sink_demo_cover_art_cid, a2dp_sink_demo_image_handle); + break; +#endif default: show_usage(); return;