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