1# Copyright 2021-2024 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# ----------------------------------------------------------------------------- 17# Imports 18# ----------------------------------------------------------------------------- 19from __future__ import annotations 20 21import asyncio 22import dataclasses 23import enum 24import struct 25 26from bumble import core 27from bumble import device 28from bumble import gatt 29from bumble import gatt_client 30from bumble import utils 31 32from typing import Type, Optional, ClassVar, Dict, TYPE_CHECKING 33from typing_extensions import Self 34 35# ----------------------------------------------------------------------------- 36# Constants 37# ----------------------------------------------------------------------------- 38 39 40class PlayingOrder(utils.OpenIntEnum): 41 '''See Media Control Service 3.15. Playing Order.''' 42 43 SINGLE_ONCE = 0x01 44 SINGLE_REPEAT = 0x02 45 IN_ORDER_ONCE = 0x03 46 IN_ORDER_REPEAT = 0x04 47 OLDEST_ONCE = 0x05 48 OLDEST_REPEAT = 0x06 49 NEWEST_ONCE = 0x07 50 NEWEST_REPEAT = 0x08 51 SHUFFLE_ONCE = 0x09 52 SHUFFLE_REPEAT = 0x0A 53 54 55class PlayingOrderSupported(enum.IntFlag): 56 '''See Media Control Service 3.16. Playing Orders Supported.''' 57 58 SINGLE_ONCE = 0x0001 59 SINGLE_REPEAT = 0x0002 60 IN_ORDER_ONCE = 0x0004 61 IN_ORDER_REPEAT = 0x0008 62 OLDEST_ONCE = 0x0010 63 OLDEST_REPEAT = 0x0020 64 NEWEST_ONCE = 0x0040 65 NEWEST_REPEAT = 0x0080 66 SHUFFLE_ONCE = 0x0100 67 SHUFFLE_REPEAT = 0x0200 68 69 70class MediaState(utils.OpenIntEnum): 71 '''See Media Control Service 3.17. Media State.''' 72 73 INACTIVE = 0x00 74 PLAYING = 0x01 75 PAUSED = 0x02 76 SEEKING = 0x03 77 78 79class MediaControlPointOpcode(utils.OpenIntEnum): 80 '''See Media Control Service 3.18. Media Control Point.''' 81 82 PLAY = 0x01 83 PAUSE = 0x02 84 FAST_REWIND = 0x03 85 FAST_FORWARD = 0x04 86 STOP = 0x05 87 MOVE_RELATIVE = 0x10 88 PREVIOUS_SEGMENT = 0x20 89 NEXT_SEGMENT = 0x21 90 FIRST_SEGMENT = 0x22 91 LAST_SEGMENT = 0x23 92 GOTO_SEGMENT = 0x24 93 PREVIOUS_TRACK = 0x30 94 NEXT_TRACK = 0x31 95 FIRST_TRACK = 0x32 96 LAST_TRACK = 0x33 97 GOTO_TRACK = 0x34 98 PREVIOUS_GROUP = 0x40 99 NEXT_GROUP = 0x41 100 FIRST_GROUP = 0x42 101 LAST_GROUP = 0x43 102 GOTO_GROUP = 0x44 103 104 105class MediaControlPointResultCode(enum.IntFlag): 106 '''See Media Control Service 3.18.2. Media Control Point Notification.''' 107 108 SUCCESS = 0x01 109 OPCODE_NOT_SUPPORTED = 0x02 110 MEDIA_PLAYER_INACTIVE = 0x03 111 COMMAND_CANNOT_BE_COMPLETED = 0x04 112 113 114class MediaControlPointOpcodeSupported(enum.IntFlag): 115 '''See Media Control Service 3.19. Media Control Point Opcodes Supported.''' 116 117 PLAY = 0x00000001 118 PAUSE = 0x00000002 119 FAST_REWIND = 0x00000004 120 FAST_FORWARD = 0x00000008 121 STOP = 0x00000010 122 MOVE_RELATIVE = 0x00000020 123 PREVIOUS_SEGMENT = 0x00000040 124 NEXT_SEGMENT = 0x00000080 125 FIRST_SEGMENT = 0x00000100 126 LAST_SEGMENT = 0x00000200 127 GOTO_SEGMENT = 0x00000400 128 PREVIOUS_TRACK = 0x00000800 129 NEXT_TRACK = 0x00001000 130 FIRST_TRACK = 0x00002000 131 LAST_TRACK = 0x00004000 132 GOTO_TRACK = 0x00008000 133 PREVIOUS_GROUP = 0x00010000 134 NEXT_GROUP = 0x00020000 135 FIRST_GROUP = 0x00040000 136 LAST_GROUP = 0x00080000 137 GOTO_GROUP = 0x00100000 138 139 140class SearchControlPointItemType(utils.OpenIntEnum): 141 '''See Media Control Service 3.20. Search Control Point.''' 142 143 TRACK_NAME = 0x01 144 ARTIST_NAME = 0x02 145 ALBUM_NAME = 0x03 146 GROUP_NAME = 0x04 147 EARLIEST_YEAR = 0x05 148 LATEST_YEAR = 0x06 149 GENRE = 0x07 150 ONLY_TRACKS = 0x08 151 ONLY_GROUPS = 0x09 152 153 154class ObjectType(utils.OpenIntEnum): 155 '''See Media Control Service 4.4.1. Object Type field.''' 156 157 TASK = 0 158 GROUP = 1 159 160 161# ----------------------------------------------------------------------------- 162# Classes 163# ----------------------------------------------------------------------------- 164 165 166class ObjectId(int): 167 '''See Media Control Service 4.4.2. Object ID field.''' 168 169 @classmethod 170 def create_from_bytes(cls: Type[Self], data: bytes) -> Self: 171 return cls(int.from_bytes(data, byteorder='little', signed=False)) 172 173 def __bytes__(self) -> bytes: 174 return self.to_bytes(6, 'little') 175 176 177@dataclasses.dataclass 178class GroupObjectType: 179 '''See Media Control Service 4.4. Group Object Type.''' 180 181 object_type: ObjectType 182 object_id: ObjectId 183 184 @classmethod 185 def from_bytes(cls: Type[Self], data: bytes) -> Self: 186 return cls( 187 object_type=ObjectType(data[0]), 188 object_id=ObjectId.create_from_bytes(data[1:]), 189 ) 190 191 def __bytes__(self) -> bytes: 192 return bytes([self.object_type]) + bytes(self.object_id) 193 194 195# ----------------------------------------------------------------------------- 196# Server 197# ----------------------------------------------------------------------------- 198class MediaControlService(gatt.TemplateService): 199 '''Media Control Service server implementation, only for testing currently.''' 200 201 UUID = gatt.GATT_MEDIA_CONTROL_SERVICE 202 203 def __init__(self, media_player_name: Optional[str] = None) -> None: 204 self.track_position = 0 205 206 self.media_player_name_characteristic = gatt.Characteristic( 207 uuid=gatt.GATT_MEDIA_PLAYER_NAME_CHARACTERISTIC, 208 properties=gatt.Characteristic.Properties.READ 209 | gatt.Characteristic.Properties.NOTIFY, 210 permissions=gatt.Characteristic.Permissions.READ_REQUIRES_ENCRYPTION, 211 value=media_player_name or 'Bumble Player', 212 ) 213 self.track_changed_characteristic = gatt.Characteristic( 214 uuid=gatt.GATT_TRACK_CHANGED_CHARACTERISTIC, 215 properties=gatt.Characteristic.Properties.NOTIFY, 216 permissions=gatt.Characteristic.Permissions.READ_REQUIRES_ENCRYPTION, 217 value=b'', 218 ) 219 self.track_title_characteristic = gatt.Characteristic( 220 uuid=gatt.GATT_TRACK_TITLE_CHARACTERISTIC, 221 properties=gatt.Characteristic.Properties.READ 222 | gatt.Characteristic.Properties.NOTIFY, 223 permissions=gatt.Characteristic.Permissions.READ_REQUIRES_ENCRYPTION, 224 value=b'', 225 ) 226 self.track_duration_characteristic = gatt.Characteristic( 227 uuid=gatt.GATT_TRACK_DURATION_CHARACTERISTIC, 228 properties=gatt.Characteristic.Properties.READ 229 | gatt.Characteristic.Properties.NOTIFY, 230 permissions=gatt.Characteristic.Permissions.READ_REQUIRES_ENCRYPTION, 231 value=b'', 232 ) 233 self.track_position_characteristic = gatt.Characteristic( 234 uuid=gatt.GATT_TRACK_POSITION_CHARACTERISTIC, 235 properties=gatt.Characteristic.Properties.READ 236 | gatt.Characteristic.Properties.WRITE 237 | gatt.Characteristic.Properties.WRITE_WITHOUT_RESPONSE 238 | gatt.Characteristic.Properties.NOTIFY, 239 permissions=gatt.Characteristic.Permissions.READ_REQUIRES_ENCRYPTION 240 | gatt.Characteristic.Permissions.WRITE_REQUIRES_ENCRYPTION, 241 value=b'', 242 ) 243 self.media_state_characteristic = gatt.Characteristic( 244 uuid=gatt.GATT_MEDIA_STATE_CHARACTERISTIC, 245 properties=gatt.Characteristic.Properties.READ 246 | gatt.Characteristic.Properties.NOTIFY, 247 permissions=gatt.Characteristic.Permissions.READ_REQUIRES_ENCRYPTION, 248 value=b'', 249 ) 250 self.media_control_point_characteristic = gatt.Characteristic( 251 uuid=gatt.GATT_MEDIA_CONTROL_POINT_CHARACTERISTIC, 252 properties=gatt.Characteristic.Properties.WRITE 253 | gatt.Characteristic.Properties.WRITE_WITHOUT_RESPONSE 254 | gatt.Characteristic.Properties.NOTIFY, 255 permissions=gatt.Characteristic.Permissions.READ_REQUIRES_ENCRYPTION 256 | gatt.Characteristic.Permissions.WRITE_REQUIRES_ENCRYPTION, 257 value=gatt.CharacteristicValue(write=self.on_media_control_point), 258 ) 259 self.media_control_point_opcodes_supported_characteristic = gatt.Characteristic( 260 uuid=gatt.GATT_MEDIA_CONTROL_POINT_OPCODES_SUPPORTED_CHARACTERISTIC, 261 properties=gatt.Characteristic.Properties.READ 262 | gatt.Characteristic.Properties.NOTIFY, 263 permissions=gatt.Characteristic.Permissions.READ_REQUIRES_ENCRYPTION, 264 value=b'', 265 ) 266 self.content_control_id_characteristic = gatt.Characteristic( 267 uuid=gatt.GATT_CONTENT_CONTROL_ID_CHARACTERISTIC, 268 properties=gatt.Characteristic.Properties.READ, 269 permissions=gatt.Characteristic.Permissions.READ_REQUIRES_ENCRYPTION, 270 value=b'', 271 ) 272 273 super().__init__( 274 [ 275 self.media_player_name_characteristic, 276 self.track_changed_characteristic, 277 self.track_title_characteristic, 278 self.track_duration_characteristic, 279 self.track_position_characteristic, 280 self.media_state_characteristic, 281 self.media_control_point_characteristic, 282 self.media_control_point_opcodes_supported_characteristic, 283 self.content_control_id_characteristic, 284 ] 285 ) 286 287 async def on_media_control_point( 288 self, connection: Optional[device.Connection], data: bytes 289 ) -> None: 290 if not connection: 291 raise core.InvalidStateError() 292 293 opcode = MediaControlPointOpcode(data[0]) 294 295 await connection.device.notify_subscriber( 296 connection, 297 self.media_control_point_characteristic, 298 value=bytes([opcode, MediaControlPointResultCode.SUCCESS]), 299 ) 300 301 302class GenericMediaControlService(MediaControlService): 303 UUID = gatt.GATT_GENERIC_MEDIA_CONTROL_SERVICE 304 305 306# ----------------------------------------------------------------------------- 307# Client 308# ----------------------------------------------------------------------------- 309class MediaControlServiceProxy( 310 gatt_client.ProfileServiceProxy, utils.CompositeEventEmitter 311): 312 SERVICE_CLASS = MediaControlService 313 314 _CHARACTERISTICS: ClassVar[Dict[str, core.UUID]] = { 315 'media_player_name': gatt.GATT_MEDIA_PLAYER_NAME_CHARACTERISTIC, 316 'media_player_icon_object_id': gatt.GATT_MEDIA_PLAYER_ICON_OBJECT_ID_CHARACTERISTIC, 317 'media_player_icon_url': gatt.GATT_MEDIA_PLAYER_ICON_URL_CHARACTERISTIC, 318 'track_changed': gatt.GATT_TRACK_CHANGED_CHARACTERISTIC, 319 'track_title': gatt.GATT_TRACK_TITLE_CHARACTERISTIC, 320 'track_duration': gatt.GATT_TRACK_DURATION_CHARACTERISTIC, 321 'track_position': gatt.GATT_TRACK_POSITION_CHARACTERISTIC, 322 'playback_speed': gatt.GATT_PLAYBACK_SPEED_CHARACTERISTIC, 323 'seeking_speed': gatt.GATT_SEEKING_SPEED_CHARACTERISTIC, 324 'current_track_segments_object_id': gatt.GATT_CURRENT_TRACK_SEGMENTS_OBJECT_ID_CHARACTERISTIC, 325 'current_track_object_id': gatt.GATT_CURRENT_TRACK_OBJECT_ID_CHARACTERISTIC, 326 'next_track_object_id': gatt.GATT_NEXT_TRACK_OBJECT_ID_CHARACTERISTIC, 327 'parent_group_object_id': gatt.GATT_PARENT_GROUP_OBJECT_ID_CHARACTERISTIC, 328 'current_group_object_id': gatt.GATT_CURRENT_GROUP_OBJECT_ID_CHARACTERISTIC, 329 'playing_order': gatt.GATT_PLAYING_ORDER_CHARACTERISTIC, 330 'playing_orders_supported': gatt.GATT_PLAYING_ORDERS_SUPPORTED_CHARACTERISTIC, 331 'media_state': gatt.GATT_MEDIA_STATE_CHARACTERISTIC, 332 'media_control_point': gatt.GATT_MEDIA_CONTROL_POINT_CHARACTERISTIC, 333 'media_control_point_opcodes_supported': gatt.GATT_MEDIA_CONTROL_POINT_OPCODES_SUPPORTED_CHARACTERISTIC, 334 'search_control_point': gatt.GATT_SEARCH_CONTROL_POINT_CHARACTERISTIC, 335 'search_results_object_id': gatt.GATT_SEARCH_RESULTS_OBJECT_ID_CHARACTERISTIC, 336 'content_control_id': gatt.GATT_CONTENT_CONTROL_ID_CHARACTERISTIC, 337 } 338 339 media_player_name: Optional[gatt_client.CharacteristicProxy] = None 340 media_player_icon_object_id: Optional[gatt_client.CharacteristicProxy] = None 341 media_player_icon_url: Optional[gatt_client.CharacteristicProxy] = None 342 track_changed: Optional[gatt_client.CharacteristicProxy] = None 343 track_title: Optional[gatt_client.CharacteristicProxy] = None 344 track_duration: Optional[gatt_client.CharacteristicProxy] = None 345 track_position: Optional[gatt_client.CharacteristicProxy] = None 346 playback_speed: Optional[gatt_client.CharacteristicProxy] = None 347 seeking_speed: Optional[gatt_client.CharacteristicProxy] = None 348 current_track_segments_object_id: Optional[gatt_client.CharacteristicProxy] = None 349 current_track_object_id: Optional[gatt_client.CharacteristicProxy] = None 350 next_track_object_id: Optional[gatt_client.CharacteristicProxy] = None 351 parent_group_object_id: Optional[gatt_client.CharacteristicProxy] = None 352 current_group_object_id: Optional[gatt_client.CharacteristicProxy] = None 353 playing_order: Optional[gatt_client.CharacteristicProxy] = None 354 playing_orders_supported: Optional[gatt_client.CharacteristicProxy] = None 355 media_state: Optional[gatt_client.CharacteristicProxy] = None 356 media_control_point: Optional[gatt_client.CharacteristicProxy] = None 357 media_control_point_opcodes_supported: Optional[gatt_client.CharacteristicProxy] = ( 358 None 359 ) 360 search_control_point: Optional[gatt_client.CharacteristicProxy] = None 361 search_results_object_id: Optional[gatt_client.CharacteristicProxy] = None 362 content_control_id: Optional[gatt_client.CharacteristicProxy] = None 363 364 if TYPE_CHECKING: 365 media_control_point_notifications: asyncio.Queue[bytes] 366 367 def __init__(self, service_proxy: gatt_client.ServiceProxy) -> None: 368 utils.CompositeEventEmitter.__init__(self) 369 self.service_proxy = service_proxy 370 self.lock = asyncio.Lock() 371 self.media_control_point_notifications = asyncio.Queue() 372 373 for field, uuid in self._CHARACTERISTICS.items(): 374 if characteristics := service_proxy.get_characteristics_by_uuid(uuid): 375 setattr(self, field, characteristics[0]) 376 377 async def subscribe_characteristics(self) -> None: 378 if self.media_control_point: 379 await self.media_control_point.subscribe(self._on_media_control_point) 380 if self.media_state: 381 await self.media_state.subscribe(self._on_media_state) 382 if self.track_changed: 383 await self.track_changed.subscribe(self._on_track_changed) 384 if self.track_title: 385 await self.track_title.subscribe(self._on_track_title) 386 if self.track_duration: 387 await self.track_duration.subscribe(self._on_track_duration) 388 if self.track_position: 389 await self.track_position.subscribe(self._on_track_position) 390 391 async def write_control_point( 392 self, opcode: MediaControlPointOpcode 393 ) -> MediaControlPointResultCode: 394 '''Writes a Media Control Point Opcode to peer and waits for the notification. 395 396 The write operation will be executed when there isn't other pending commands. 397 398 Args: 399 opcode: opcode defined in `MediaControlPointOpcode`. 400 401 Returns: 402 Response code provided in `MediaControlPointResultCode` 403 404 Raises: 405 InvalidOperationError: Server does not have Media Control Point Characteristic. 406 InvalidStateError: Server replies a notification with mismatched opcode. 407 ''' 408 if not self.media_control_point: 409 raise core.InvalidOperationError("Peer does not have media control point") 410 411 async with self.lock: 412 await self.media_control_point.write_value( 413 bytes([opcode]), 414 with_response=False, 415 ) 416 417 ( 418 response_opcode, 419 response_code, 420 ) = await self.media_control_point_notifications.get() 421 if response_opcode != opcode: 422 raise core.InvalidStateError( 423 f"Expected {opcode} notification, but get {response_opcode}" 424 ) 425 return MediaControlPointResultCode(response_code) 426 427 def _on_media_control_point(self, data: bytes) -> None: 428 self.media_control_point_notifications.put_nowait(data) 429 430 def _on_media_state(self, data: bytes) -> None: 431 self.emit('media_state', MediaState(data[0])) 432 433 def _on_track_changed(self, data: bytes) -> None: 434 del data 435 self.emit('track_changed') 436 437 def _on_track_title(self, data: bytes) -> None: 438 self.emit('track_title', data.decode("utf-8")) 439 440 def _on_track_duration(self, data: bytes) -> None: 441 self.emit('track_duration', struct.unpack_from('<i', data)[0]) 442 443 def _on_track_position(self, data: bytes) -> None: 444 self.emit('track_position', struct.unpack_from('<i', data)[0]) 445 446 447class GenericMediaControlServiceProxy(MediaControlServiceProxy): 448 SERVICE_CLASS = GenericMediaControlService 449