1# Copyright 2023 Google LLC 2# 3# Licensed under the Apache License, Version 2.0 (the "License"); 4# you may not use this file except in compliance with the License. 5# You may obtain a copy of the License at 6# 7# https://www.apache.org/licenses/LICENSE-2.0 8# 9# Unless required by applicable law or agreed to in writing, software 10# distributed under the License is distributed on an "AS IS" BASIS, 11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12# See the License for the specific language governing permissions and 13# limitations under the License. 14 15# ----------------------------------------------------------------------------- 16# Imports 17# ----------------------------------------------------------------------------- 18from __future__ import annotations 19import asyncio 20import json 21import sys 22import os 23import logging 24import websockets 25 26from bumble.device import Device 27from bumble.transport import open_transport_or_link 28from bumble.core import BT_BR_EDR_TRANSPORT 29from bumble import avc 30from bumble import avrcp 31from bumble import avdtp 32from bumble import a2dp 33from bumble import utils 34 35 36logger = logging.getLogger(__name__) 37 38 39# ----------------------------------------------------------------------------- 40def sdp_records(): 41 a2dp_sink_service_record_handle = 0x00010001 42 avrcp_controller_service_record_handle = 0x00010002 43 avrcp_target_service_record_handle = 0x00010003 44 # pylint: disable=line-too-long 45 return { 46 a2dp_sink_service_record_handle: a2dp.make_audio_sink_service_sdp_records( 47 a2dp_sink_service_record_handle 48 ), 49 avrcp_controller_service_record_handle: avrcp.make_controller_service_sdp_records( 50 avrcp_controller_service_record_handle 51 ), 52 avrcp_target_service_record_handle: avrcp.make_target_service_sdp_records( 53 avrcp_controller_service_record_handle 54 ), 55 } 56 57 58# ----------------------------------------------------------------------------- 59def codec_capabilities(): 60 return avdtp.MediaCodecCapabilities( 61 media_type=avdtp.AVDTP_AUDIO_MEDIA_TYPE, 62 media_codec_type=a2dp.A2DP_SBC_CODEC_TYPE, 63 media_codec_information=a2dp.SbcMediaCodecInformation.from_lists( 64 sampling_frequencies=[48000, 44100, 32000, 16000], 65 channel_modes=[ 66 a2dp.SBC_MONO_CHANNEL_MODE, 67 a2dp.SBC_DUAL_CHANNEL_MODE, 68 a2dp.SBC_STEREO_CHANNEL_MODE, 69 a2dp.SBC_JOINT_STEREO_CHANNEL_MODE, 70 ], 71 block_lengths=[4, 8, 12, 16], 72 subbands=[4, 8], 73 allocation_methods=[ 74 a2dp.SBC_LOUDNESS_ALLOCATION_METHOD, 75 a2dp.SBC_SNR_ALLOCATION_METHOD, 76 ], 77 minimum_bitpool_value=2, 78 maximum_bitpool_value=53, 79 ), 80 ) 81 82 83# ----------------------------------------------------------------------------- 84def on_avdtp_connection(server): 85 # Add a sink endpoint to the server 86 sink = server.add_sink(codec_capabilities()) 87 sink.on('rtp_packet', on_rtp_packet) 88 89 90# ----------------------------------------------------------------------------- 91def on_rtp_packet(packet): 92 print(f'RTP: {packet}') 93 94 95# ----------------------------------------------------------------------------- 96def on_avrcp_start(avrcp_protocol: avrcp.Protocol, websocket_server: WebSocketServer): 97 async def get_supported_events(): 98 events = await avrcp_protocol.get_supported_events() 99 print("SUPPORTED EVENTS:", events) 100 websocket_server.send_message( 101 { 102 "type": "supported-events", 103 "params": {"events": [event.name for event in events]}, 104 } 105 ) 106 107 if avrcp.EventId.TRACK_CHANGED in events: 108 utils.AsyncRunner.spawn(monitor_track_changed()) 109 110 if avrcp.EventId.PLAYBACK_STATUS_CHANGED in events: 111 utils.AsyncRunner.spawn(monitor_playback_status()) 112 113 if avrcp.EventId.PLAYBACK_POS_CHANGED in events: 114 utils.AsyncRunner.spawn(monitor_playback_position()) 115 116 if avrcp.EventId.PLAYER_APPLICATION_SETTING_CHANGED in events: 117 utils.AsyncRunner.spawn(monitor_player_application_settings()) 118 119 if avrcp.EventId.AVAILABLE_PLAYERS_CHANGED in events: 120 utils.AsyncRunner.spawn(monitor_available_players()) 121 122 if avrcp.EventId.ADDRESSED_PLAYER_CHANGED in events: 123 utils.AsyncRunner.spawn(monitor_addressed_player()) 124 125 if avrcp.EventId.UIDS_CHANGED in events: 126 utils.AsyncRunner.spawn(monitor_uids()) 127 128 if avrcp.EventId.VOLUME_CHANGED in events: 129 utils.AsyncRunner.spawn(monitor_volume()) 130 131 utils.AsyncRunner.spawn(get_supported_events()) 132 133 async def monitor_track_changed(): 134 async for identifier in avrcp_protocol.monitor_track_changed(): 135 print("TRACK CHANGED:", identifier.hex()) 136 websocket_server.send_message( 137 {"type": "track-changed", "params": {"identifier": identifier.hex()}} 138 ) 139 140 async def monitor_playback_status(): 141 async for playback_status in avrcp_protocol.monitor_playback_status(): 142 print("PLAYBACK STATUS CHANGED:", playback_status.name) 143 websocket_server.send_message( 144 { 145 "type": "playback-status-changed", 146 "params": {"status": playback_status.name}, 147 } 148 ) 149 150 async def monitor_playback_position(): 151 async for playback_position in avrcp_protocol.monitor_playback_position( 152 playback_interval=1 153 ): 154 print("PLAYBACK POSITION CHANGED:", playback_position) 155 websocket_server.send_message( 156 { 157 "type": "playback-position-changed", 158 "params": {"position": playback_position}, 159 } 160 ) 161 162 async def monitor_player_application_settings(): 163 async for settings in avrcp_protocol.monitor_player_application_settings(): 164 print("PLAYER APPLICATION SETTINGS:", settings) 165 settings_as_dict = [ 166 {"attribute": setting.attribute_id.name, "value": setting.value_id.name} 167 for setting in settings 168 ] 169 websocket_server.send_message( 170 { 171 "type": "player-settings-changed", 172 "params": {"settings": settings_as_dict}, 173 } 174 ) 175 176 async def monitor_available_players(): 177 async for _ in avrcp_protocol.monitor_available_players(): 178 print("AVAILABLE PLAYERS CHANGED") 179 websocket_server.send_message( 180 {"type": "available-players-changed", "params": {}} 181 ) 182 183 async def monitor_addressed_player(): 184 async for player in avrcp_protocol.monitor_addressed_player(): 185 print("ADDRESSED PLAYER CHANGED") 186 websocket_server.send_message( 187 { 188 "type": "addressed-player-changed", 189 "params": { 190 "player": { 191 "player_id": player.player_id, 192 "uid_counter": player.uid_counter, 193 } 194 }, 195 } 196 ) 197 198 async def monitor_uids(): 199 async for uid_counter in avrcp_protocol.monitor_uids(): 200 print("UIDS CHANGED") 201 websocket_server.send_message( 202 { 203 "type": "uids-changed", 204 "params": { 205 "uid_counter": uid_counter, 206 }, 207 } 208 ) 209 210 async def monitor_volume(): 211 async for volume in avrcp_protocol.monitor_volume(): 212 print("VOLUME CHANGED:", volume) 213 websocket_server.send_message( 214 {"type": "volume-changed", "params": {"volume": volume}} 215 ) 216 217 218# ----------------------------------------------------------------------------- 219class WebSocketServer: 220 def __init__( 221 self, avrcp_protocol: avrcp.Protocol, avrcp_delegate: Delegate 222 ) -> None: 223 self.socket = None 224 self.delegate = None 225 self.avrcp_protocol = avrcp_protocol 226 self.avrcp_delegate = avrcp_delegate 227 228 async def start(self) -> None: 229 # pylint: disable-next=no-member 230 await websockets.serve(self.serve, 'localhost', 8989) # type: ignore 231 232 async def serve(self, socket, _path) -> None: 233 print('### WebSocket connected') 234 self.socket = socket 235 while True: 236 try: 237 message = await socket.recv() 238 print('Received: ', str(message)) 239 240 parsed = json.loads(message) 241 message_type = parsed['type'] 242 if message_type == 'send-key-down': 243 await self.on_send_key_down(parsed) 244 elif message_type == 'send-key-up': 245 await self.on_send_key_up(parsed) 246 elif message_type == 'set-volume': 247 await self.on_set_volume(parsed) 248 elif message_type == 'get-play-status': 249 await self.on_get_play_status() 250 elif message_type == 'get-element-attributes': 251 await self.on_get_element_attributes() 252 except websockets.exceptions.ConnectionClosedOK: 253 self.socket = None 254 break 255 256 async def on_send_key_down(self, message: dict) -> None: 257 key = avc.PassThroughFrame.OperationId[message["key"]] 258 await self.avrcp_protocol.send_key_event(key, True) 259 260 async def on_send_key_up(self, message: dict) -> None: 261 key = avc.PassThroughFrame.OperationId[message["key"]] 262 await self.avrcp_protocol.send_key_event(key, False) 263 264 async def on_set_volume(self, message: dict) -> None: 265 volume = message["volume"] 266 self.avrcp_delegate.volume = volume 267 self.avrcp_protocol.notify_volume_changed(volume) 268 269 async def on_get_play_status(self) -> None: 270 play_status = await self.avrcp_protocol.get_play_status() 271 self.send_message( 272 { 273 "type": "get-play-status-response", 274 "params": { 275 "song_length": play_status.song_length, 276 "song_position": play_status.song_position, 277 "play_status": play_status.play_status.name, 278 }, 279 } 280 ) 281 282 async def on_get_element_attributes(self) -> None: 283 attributes = await self.avrcp_protocol.get_element_attributes( 284 0, 285 [ 286 avrcp.MediaAttributeId.TITLE, 287 avrcp.MediaAttributeId.ARTIST_NAME, 288 avrcp.MediaAttributeId.ALBUM_NAME, 289 avrcp.MediaAttributeId.TRACK_NUMBER, 290 avrcp.MediaAttributeId.TOTAL_NUMBER_OF_TRACKS, 291 avrcp.MediaAttributeId.GENRE, 292 avrcp.MediaAttributeId.PLAYING_TIME, 293 avrcp.MediaAttributeId.DEFAULT_COVER_ART, 294 ], 295 ) 296 self.send_message( 297 { 298 "type": "get-element-attributes-response", 299 "params": [ 300 { 301 "attribute_id": attribute.attribute_id.name, 302 "attribute_value": attribute.attribute_value, 303 } 304 for attribute in attributes 305 ], 306 } 307 ) 308 309 def send_message(self, message: dict) -> None: 310 if self.socket is None: 311 print("no socket, dropping message") 312 return 313 serialized = json.dumps(message) 314 utils.AsyncRunner.spawn(self.socket.send(serialized)) 315 316 317# ----------------------------------------------------------------------------- 318class Delegate(avrcp.Delegate): 319 def __init__(self): 320 super().__init__( 321 [avrcp.EventId.VOLUME_CHANGED, avrcp.EventId.PLAYBACK_STATUS_CHANGED] 322 ) 323 self.websocket_server = None 324 325 async def set_absolute_volume(self, volume: int) -> None: 326 await super().set_absolute_volume(volume) 327 if self.websocket_server is not None: 328 self.websocket_server.send_message( 329 {"type": "set-volume", "params": {"volume": volume}} 330 ) 331 332 333# ----------------------------------------------------------------------------- 334async def main() -> None: 335 if len(sys.argv) < 3: 336 print( 337 'Usage: run_avrcp_controller.py <device-config> <transport-spec> ' 338 '<sbc-file> [<bt-addr>]' 339 ) 340 print('example: run_avrcp_controller.py classic1.json usb:0') 341 return 342 343 print('<<< connecting to HCI...') 344 async with await open_transport_or_link(sys.argv[2]) as hci_transport: 345 print('<<< connected') 346 347 # Create a device 348 device = Device.from_config_file_with_hci( 349 sys.argv[1], hci_transport.source, hci_transport.sink 350 ) 351 device.classic_enabled = True 352 353 # Setup the SDP to expose the sink service 354 device.sdp_service_records = sdp_records() 355 356 # Start the controller 357 await device.power_on() 358 359 # Create a listener to wait for AVDTP connections 360 listener = avdtp.Listener(avdtp.Listener.create_registrar(device)) 361 listener.on('connection', on_avdtp_connection) 362 363 avrcp_delegate = Delegate() 364 avrcp_protocol = avrcp.Protocol(avrcp_delegate) 365 avrcp_protocol.listen(device) 366 367 websocket_server = WebSocketServer(avrcp_protocol, avrcp_delegate) 368 avrcp_delegate.websocket_server = websocket_server 369 avrcp_protocol.on( 370 "start", lambda: on_avrcp_start(avrcp_protocol, websocket_server) 371 ) 372 await websocket_server.start() 373 374 if len(sys.argv) >= 5: 375 # Connect to the peer 376 target_address = sys.argv[4] 377 print(f'=== Connecting to {target_address}...') 378 connection = await device.connect( 379 target_address, transport=BT_BR_EDR_TRANSPORT 380 ) 381 print(f'=== Connected to {connection.peer_address}!') 382 383 # Request authentication 384 print('*** Authenticating...') 385 await connection.authenticate() 386 print('*** Authenticated') 387 388 # Enable encryption 389 print('*** Enabling encryption...') 390 await connection.encrypt() 391 print('*** Encryption on') 392 393 server = await avdtp.Protocol.connect(connection) 394 listener.set_server(connection, server) 395 sink = server.add_sink(codec_capabilities()) 396 sink.on('rtp_packet', on_rtp_packet) 397 398 await avrcp_protocol.connect(connection) 399 400 else: 401 # Start being discoverable and connectable 402 await device.set_discoverable(True) 403 await device.set_connectable(True) 404 405 await asyncio.get_event_loop().create_future() 406 407 408# ----------------------------------------------------------------------------- 409logging.basicConfig(level=os.environ.get('BUMBLE_LOGLEVEL', 'DEBUG').upper()) 410asyncio.run(main()) 411