| # Copyright 2021-2022 Google LLC |
| # |
| # Licensed under the Apache License, Version 2.0 (the "License"); |
| # you may not use this file except in compliance with the License. |
| # You may obtain a copy of the License at |
| # |
| # https://www.apache.org/licenses/LICENSE-2.0 |
| # |
| # Unless required by applicable law or agreed to in writing, software |
| # distributed under the License is distributed on an "AS IS" BASIS, |
| # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| # See the License for the specific language governing permissions and |
| # limitations under the License. |
| |
| # ----------------------------------------------------------------------------- |
| # Imports |
| # ----------------------------------------------------------------------------- |
| import asyncio |
| import struct |
| import time |
| import logging |
| from colors import color |
| from pyee import EventEmitter |
| |
| from .core import ( |
| BT_ADVANCED_AUDIO_DISTRIBUTION_SERVICE, |
| InvalidStateError, |
| ProtocolError, |
| name_or_number |
| ) |
| from .a2dp import ( |
| A2DP_CODEC_TYPE_NAMES, |
| A2DP_MPEG_2_4_AAC_CODEC_TYPE, |
| A2DP_NON_A2DP_CODEC_TYPE, |
| A2DP_SBC_CODEC_TYPE, |
| AacMediaCodecInformation, |
| SbcMediaCodecInformation, |
| VendorSpecificMediaCodecInformation |
| ) |
| from . import sdp |
| |
| # ----------------------------------------------------------------------------- |
| # Logging |
| # ----------------------------------------------------------------------------- |
| logger = logging.getLogger(__name__) |
| |
| |
| # ----------------------------------------------------------------------------- |
| # Constants |
| # ----------------------------------------------------------------------------- |
| AVDTP_PSM = 0x0019 |
| |
| AVDTP_DEFAULT_RTX_SIG_TIMER = 5 # Seconds |
| |
| # Signal Identifiers (AVDTP spec - 8.5 Signal Command Set) |
| AVDTP_DISCOVER = 0x01 |
| AVDTP_GET_CAPABILITIES = 0x02 |
| AVDTP_SET_CONFIGURATION = 0x03 |
| AVDTP_GET_CONFIGURATION = 0x04 |
| AVDTP_RECONFIGURE = 0x05 |
| AVDTP_OPEN = 0x06 |
| AVDTP_START = 0x07 |
| AVDTP_CLOSE = 0x08 |
| AVDTP_SUSPEND = 0x09 |
| AVDTP_ABORT = 0x0A |
| AVDTP_SECURITY_CONTROL = 0x0B |
| AVDTP_GET_ALL_CAPABILITIES = 0x0C |
| AVDTP_DELAYREPORT = 0x0D |
| |
| AVDTP_SIGNAL_NAMES = { |
| AVDTP_DISCOVER: 'AVDTP_DISCOVER', |
| AVDTP_GET_CAPABILITIES: 'AVDTP_GET_CAPABILITIES', |
| AVDTP_SET_CONFIGURATION: 'AVDTP_SET_CONFIGURATION', |
| AVDTP_GET_CONFIGURATION: 'AVDTP_GET_CONFIGURATION', |
| AVDTP_RECONFIGURE: 'AVDTP_RECONFIGURE', |
| AVDTP_OPEN: 'AVDTP_OPEN', |
| AVDTP_START: 'AVDTP_START', |
| AVDTP_CLOSE: 'AVDTP_CLOSE', |
| AVDTP_SUSPEND: 'AVDTP_SUSPEND', |
| AVDTP_ABORT: 'AVDTP_ABORT', |
| AVDTP_SECURITY_CONTROL: 'AVDTP_SECURITY_CONTROL', |
| AVDTP_GET_ALL_CAPABILITIES: 'AVDTP_GET_ALL_CAPABILITIES', |
| AVDTP_DELAYREPORT: 'AVDTP_DELAYREPORT' |
| } |
| |
| AVDTP_SIGNAL_IDENTIFIERS = { |
| 'AVDTP_DISCOVER': AVDTP_DISCOVER, |
| 'AVDTP_GET_CAPABILITIES': AVDTP_GET_CAPABILITIES, |
| 'AVDTP_SET_CONFIGURATION': AVDTP_SET_CONFIGURATION, |
| 'AVDTP_GET_CONFIGURATION': AVDTP_GET_CONFIGURATION, |
| 'AVDTP_RECONFIGURE': AVDTP_RECONFIGURE, |
| 'AVDTP_OPEN': AVDTP_OPEN, |
| 'AVDTP_START': AVDTP_START, |
| 'AVDTP_CLOSE': AVDTP_CLOSE, |
| 'AVDTP_SUSPEND': AVDTP_SUSPEND, |
| 'AVDTP_ABORT': AVDTP_ABORT, |
| 'AVDTP_SECURITY_CONTROL': AVDTP_SECURITY_CONTROL, |
| 'AVDTP_GET_ALL_CAPABILITIES': AVDTP_GET_ALL_CAPABILITIES, |
| 'AVDTP_DELAYREPORT': AVDTP_DELAYREPORT |
| } |
| |
| # Error codes (AVDTP spec - 8.20.6.2 ERROR_CODE tables) |
| AVDTP_BAD_HEADER_FORMAT_ERROR = 0x01 |
| AVDTP_BAD_LENGTH_ERROR = 0x11 |
| AVDTP_BAD_ACP_SEID_ERROR = 0x12 |
| AVDTP_SEP_IN_USE_ERROR = 0x13 |
| AVDTP_SEP_NOT_IN_USE_ERROR = 0x14 |
| AVDTP_BAD_SERV_CATEGORY_ERROR = 0x17 |
| AVDTP_BAD_PAYLOAD_FORMAT_ERROR = 0x18 |
| AVDTP_NOT_SUPPORTED_COMMAND_ERROR = 0x19 |
| AVDTP_INVALID_CAPABILITIES_ERROR = 0x1A |
| AVDTP_BAD_RECOVERY_TYPE_ERROR = 0x22 |
| AVDTP_BAD_MEDIA_TRANSPORT_FORMAT_ERROR = 0x23 |
| AVDTP_BAD_RECOVERY_FORMAT_ERROR = 0x25 |
| AVDTP_BAD_ROHC_FORMAT_ERROR = 0x26 |
| AVDTP_BAD_CP_FORMAT_ERROR = 0x27 |
| AVDTP_BAD_MULTIPLEXING_FORMAT_ERROR = 0x28 |
| AVDTP_UNSUPPORTED_CONFIGURATION_ERROR = 0x29 |
| AVDTP_BAD_STATE_ERROR = 0x31 |
| |
| AVDTP_ERROR_NAMES = { |
| AVDTP_BAD_HEADER_FORMAT_ERROR: 'AVDTP_BAD_HEADER_FORMAT_ERROR', |
| AVDTP_BAD_LENGTH_ERROR: 'AVDTP_BAD_LENGTH_ERROR', |
| AVDTP_BAD_ACP_SEID_ERROR: 'AVDTP_BAD_ACP_SEID_ERROR', |
| AVDTP_SEP_IN_USE_ERROR: 'AVDTP_SEP_IN_USE_ERROR', |
| AVDTP_SEP_NOT_IN_USE_ERROR: 'AVDTP_SEP_NOT_IN_USE_ERROR', |
| AVDTP_BAD_SERV_CATEGORY_ERROR: 'AVDTP_BAD_SERV_CATEGORY_ERROR', |
| AVDTP_BAD_PAYLOAD_FORMAT_ERROR: 'AVDTP_BAD_PAYLOAD_FORMAT_ERROR', |
| AVDTP_NOT_SUPPORTED_COMMAND_ERROR: 'AVDTP_NOT_SUPPORTED_COMMAND_ERROR', |
| AVDTP_INVALID_CAPABILITIES_ERROR: 'AVDTP_INVALID_CAPABILITIES_ERROR', |
| AVDTP_BAD_RECOVERY_TYPE_ERROR: 'AVDTP_BAD_RECOVERY_TYPE_ERROR', |
| AVDTP_BAD_MEDIA_TRANSPORT_FORMAT_ERROR: 'AVDTP_BAD_MEDIA_TRANSPORT_FORMAT_ERROR', |
| AVDTP_BAD_RECOVERY_FORMAT_ERROR: 'AVDTP_BAD_RECOVERY_FORMAT_ERROR', |
| AVDTP_BAD_ROHC_FORMAT_ERROR: 'AVDTP_BAD_ROHC_FORMAT_ERROR', |
| AVDTP_BAD_CP_FORMAT_ERROR: 'AVDTP_BAD_CP_FORMAT_ERROR', |
| AVDTP_BAD_MULTIPLEXING_FORMAT_ERROR: 'AVDTP_BAD_MULTIPLEXING_FORMAT_ERROR', |
| AVDTP_UNSUPPORTED_CONFIGURATION_ERROR: 'AVDTP_UNSUPPORTED_CONFIGURATION_ERROR', |
| AVDTP_BAD_STATE_ERROR: 'AVDTP_BAD_STATE_ERROR' |
| } |
| |
| AVDTP_AUDIO_MEDIA_TYPE = 0x00 |
| AVDTP_VIDEO_MEDIA_TYPE = 0x01 |
| AVDTP_MULTIMEDIA_MEDIA_TYPE = 0x02 |
| |
| AVDTP_MEDIA_TYPE_NAMES = { |
| AVDTP_AUDIO_MEDIA_TYPE: 'AVDTP_AUDIO_MEDIA_TYPE', |
| AVDTP_VIDEO_MEDIA_TYPE: 'AVDTP_VIDEO_MEDIA_TYPE', |
| AVDTP_MULTIMEDIA_MEDIA_TYPE: 'AVDTP_MULTIMEDIA_MEDIA_TYPE' |
| } |
| |
| # TSEP (AVDTP spec - 8.20.3 Stream End-point Type, Source or Sink (TSEP)) |
| AVDTP_TSEP_SRC = 0x00 |
| AVDTP_TSEP_SNK = 0x01 |
| |
| AVDTP_TSEP_NAMES = { |
| AVDTP_TSEP_SRC: 'AVDTP_TSEP_SRC', |
| AVDTP_TSEP_SNK: 'AVDTP_TSEP_SNK' |
| } |
| |
| # Service Categories (AVDTP spec - Table 8.47: Service Category information element field values) |
| AVDTP_MEDIA_TRANSPORT_SERVICE_CATEGORY = 0x01 |
| AVDTP_REPORTING_SERVICE_CATEGORY = 0x02 |
| AVDTP_RECOVERY_SERVICE_CATEGORY = 0x03 |
| AVDTP_CONTENT_PROTECTION_SERVICE_CATEGORY = 0x04 |
| AVDTP_HEADER_COMPRESSION_SERVICE_CATEGORY = 0x05 |
| AVDTP_MULTIPLEXING_SERVICE_CATEGORY = 0x06 |
| AVDTP_MEDIA_CODEC_SERVICE_CATEGORY = 0x07 |
| AVDTP_DELAY_REPORTING_SERVICE_CATEGORY = 0x08 |
| |
| AVDTP_SERVICE_CATEGORY_NAMES = { |
| AVDTP_MEDIA_TRANSPORT_SERVICE_CATEGORY: 'AVDTP_MEDIA_TRANSPORT_SERVICE_CATEGORY', |
| AVDTP_REPORTING_SERVICE_CATEGORY: 'AVDTP_REPORTING_SERVICE_CATEGORY', |
| AVDTP_RECOVERY_SERVICE_CATEGORY: 'AVDTP_RECOVERY_SERVICE_CATEGORY', |
| AVDTP_CONTENT_PROTECTION_SERVICE_CATEGORY: 'AVDTP_CONTENT_PROTECTION_SERVICE_CATEGORY', |
| AVDTP_HEADER_COMPRESSION_SERVICE_CATEGORY: 'AVDTP_HEADER_COMPRESSION_SERVICE_CATEGORY', |
| AVDTP_MULTIPLEXING_SERVICE_CATEGORY: 'AVDTP_MULTIPLEXING_SERVICE_CATEGORY', |
| AVDTP_MEDIA_CODEC_SERVICE_CATEGORY: 'AVDTP_MEDIA_CODEC_SERVICE_CATEGORY', |
| AVDTP_DELAY_REPORTING_SERVICE_CATEGORY: 'AVDTP_DELAY_REPORTING_SERVICE_CATEGORY' |
| } |
| |
| # States (AVDTP spec - 9.1 State Definitions) |
| AVDTP_IDLE_STATE = 0x00 |
| AVDTP_CONFIGURED_STATE = 0x01 |
| AVDTP_OPEN_STATE = 0x02 |
| AVDTP_STREAMING_STATE = 0x03 |
| AVDTP_CLOSING_STATE = 0x04 |
| AVDTP_ABORTING_STATE = 0x05 |
| |
| AVDTP_STATE_NAMES = { |
| AVDTP_IDLE_STATE: 'AVDTP_IDLE_STATE', |
| AVDTP_CONFIGURED_STATE: 'AVDTP_CONFIGURED_STATE', |
| AVDTP_OPEN_STATE: 'AVDTP_OPEN_STATE', |
| AVDTP_STREAMING_STATE: 'AVDTP_STREAMING_STATE', |
| AVDTP_CLOSING_STATE: 'AVDTP_CLOSING_STATE', |
| AVDTP_ABORTING_STATE: 'AVDTP_ABORTING_STATE' |
| } |
| |
| |
| # ----------------------------------------------------------------------------- |
| async def find_avdtp_service_with_sdp_client(sdp_client): |
| ''' |
| Find an AVDTP service, using a connected SDP client, and return its version, |
| or None if none is found |
| ''' |
| |
| # Search for services with an Audio Sink service class |
| search_result = await sdp_client.search_attributes( |
| [BT_ADVANCED_AUDIO_DISTRIBUTION_SERVICE], |
| [ |
| sdp.SDP_BLUETOOTH_PROFILE_DESCRIPTOR_LIST_ATTRIBUTE_ID |
| ] |
| ) |
| for attribute_list in search_result: |
| profile_descriptor_list = sdp.ServiceAttribute.find_attribute_in_list( |
| attribute_list, |
| sdp.SDP_BLUETOOTH_PROFILE_DESCRIPTOR_LIST_ATTRIBUTE_ID |
| ) |
| if profile_descriptor_list: |
| for profile_descriptor in profile_descriptor_list.value: |
| if len(profile_descriptor.value) >= 2: |
| avdtp_version_major = profile_descriptor.value[1].value >> 8 |
| avdtp_version_minor = profile_descriptor.value[1].value & 0xFF |
| return (avdtp_version_major, avdtp_version_minor) |
| |
| |
| # ----------------------------------------------------------------------------- |
| async def find_avdtp_service_with_connection(device, connection): |
| ''' |
| Find an AVDTP service, for a connection, and return its version, |
| or None if none is found |
| ''' |
| |
| sdp_client = sdp.Client(device) |
| await sdp_client.connect(connection) |
| service_version = await find_avdtp_service_with_sdp_client(sdp_client) |
| await sdp_client.disconnect() |
| |
| return service_version |
| |
| |
| # ----------------------------------------------------------------------------- |
| class RealtimeClock: |
| def now(self): |
| return time.time() |
| |
| async def sleep(self, duration): |
| await asyncio.sleep(duration) |
| |
| |
| # ----------------------------------------------------------------------------- |
| class MediaPacket: |
| @staticmethod |
| def from_bytes(data): |
| version = (data[0] >> 6) & 0x03 |
| padding = (data[0] >> 5) & 0x01 |
| extension = (data[0] >> 4) & 0x01 |
| csrc_count = data[0] & 0x0F |
| marker = (data[1] >> 7) & 0x01 |
| payload_type = data[1] & 0x7F |
| sequence_number = struct.unpack_from('>H', data, 2)[0] |
| timestamp = struct.unpack_from('>I', data, 4)[0] |
| ssrc = struct.unpack_from('>I', data, 8)[0] |
| csrc_list = [struct.unpack_from('>I', data, 12 + i)[0] for i in range(csrc_count)] |
| payload = data[12 + csrc_count * 4:] |
| |
| return MediaPacket( |
| version, |
| padding, |
| extension, |
| marker, |
| sequence_number, |
| timestamp, |
| ssrc, |
| csrc_list, |
| payload_type, |
| payload |
| ) |
| |
| def __init__( |
| self, |
| version, |
| padding, |
| extension, |
| marker, |
| sequence_number, |
| timestamp, |
| ssrc, |
| csrc_list, |
| payload_type, |
| payload |
| ): |
| self.version = version |
| self.padding = padding |
| self.extension = extension |
| self.marker = marker |
| self.sequence_number = sequence_number |
| self.timestamp = timestamp |
| self.ssrc = ssrc |
| self.csrc_list = csrc_list |
| self.payload_type = payload_type |
| self.payload = payload |
| |
| def __bytes__(self): |
| header = ( |
| bytes([ |
| self.version << 6 | self.padding << 5 | self.extension << 4 | len(self.csrc_list), |
| self.marker << 7 | self.payload_type |
| ]) + |
| struct.pack('>HII', self.sequence_number, self.timestamp, self.ssrc) |
| ) |
| for csrc in self.csrc_list: |
| header += struct.pack('>I', csrc) |
| return header + self.payload |
| |
| def __str__(self): |
| return f'RTP(v={self.version},p={self.padding},x={self.extension},m={self.marker},pt={self.payload_type},sn={self.sequence_number},ts={self.timestamp},ssrc={self.ssrc},csrcs={self.csrc_list},payload_size={len(self.payload)})' |
| |
| |
| # ----------------------------------------------------------------------------- |
| class MediaPacketPump: |
| def __init__(self, packets, clock=RealtimeClock()): |
| self.packets = packets |
| self.clock = clock |
| self.pump_task = None |
| |
| async def start(self, rtp_channel): |
| async def pump_packets(): |
| start_time = 0 |
| start_timestamp = 0 |
| |
| try: |
| logger.debug('pump starting') |
| async for packet in self.packets: |
| # Capture the timestamp of the first packet |
| if start_time == 0: |
| start_time = self.clock.now() |
| start_timestamp = packet.timestamp_seconds |
| |
| # Wait until we can send |
| when = start_time + (packet.timestamp_seconds - start_timestamp) |
| now = self.clock.now() |
| if when > now: |
| delay = when - now |
| logger.debug(f'waiting for {delay}') |
| await self.clock.sleep(delay) |
| |
| # Emit |
| rtp_channel.send_pdu(bytes(packet)) |
| logger.debug(f'{color(">>> sending RTP packet:", "green")} {packet}') |
| except asyncio.exceptions.CancelledError: |
| logger.debug('pump canceled') |
| |
| # Pump packets |
| self.pump_task = asyncio.create_task(pump_packets()) |
| |
| async def stop(self): |
| # Stop the pump |
| if self.pump_task: |
| self.pump_task.cancel() |
| await self.pump_task |
| self.pump_task = None |
| |
| |
| # ----------------------------------------------------------------------------- |
| class MessageAssembler: |
| def __init__(self, callback): |
| self.callback = callback |
| self.reset() |
| |
| def reset(self): |
| self.transaction_label = 0 |
| self.message = None |
| self.message_type = 0 |
| self.signal_identifier = 0 |
| self.number_of_signal_packets = 0 |
| self.packet_count = 0 |
| |
| def on_pdu(self, pdu): |
| self.packet_count += 1 |
| |
| transaction_label = pdu[0] >> 4 |
| packet_type = (pdu[0] >> 2) & 3 |
| message_type = pdu[0] & 3 |
| |
| logger.debug(f'transaction_label={transaction_label}, packet_type={Protocol.packet_type_name(packet_type)}, message_type={Message.message_type_name(message_type)}') |
| if packet_type == Protocol.SINGLE_PACKET or packet_type == Protocol.START_PACKET: |
| if self.message is not None: |
| # The previous message has not been terminated |
| logger.warning('received a start or single packet when expecting an end or continuation') |
| self.reset() |
| |
| self.transaction_label = transaction_label |
| self.signal_identifier = pdu[1] & 0x3F |
| self.message_type = message_type |
| |
| if packet_type == Protocol.SINGLE_PACKET: |
| self.message = pdu[2:] |
| self.on_message_complete() |
| else: |
| self.number_of_signal_packets = pdu[2] |
| self.message = pdu[3:] |
| elif packet_type == Protocol.CONTINUE_PACKET or packet_type == Protocol.END_PACKET: |
| if self.packet_count == 0: |
| logger.warning('unexpected continuation') |
| return |
| |
| if transaction_label != self.transaction_label: |
| logger.warning(f'transaction label mismatch: expected {self.transaction_label}, received {transaction_label}') |
| return |
| |
| if message_type != self.message_type: |
| logger.warning(f'message type mismatch: expected {self.message_type}, received {message_type}') |
| return |
| |
| self.message += pdu[1:] |
| |
| if packet_type == Protocol.END_PACKET: |
| if self.packet_count != self.number_of_signal_packets: |
| logger.warning(f'incomplete fragmented message: expected {self.number_of_signal_packets} packets, received {self.packet_count}') |
| self.reset() |
| return |
| |
| self.on_message_complete() |
| else: |
| if self.packet_count > self.number_of_signal_packets: |
| logger.warning(f'too many packets: expected {self.number_of_signal_packets}, received {self.packet_count}') |
| self.reset() |
| return |
| |
| def on_message_complete(self): |
| message = Message.create(self.signal_identifier, self.message_type, self.message) |
| |
| try: |
| self.callback(self.transaction_label, message) |
| except Exception as error: |
| logger.warning(color(f'!!! exception in callback: {error}')) |
| |
| self.reset() |
| |
| |
| # ----------------------------------------------------------------------------- |
| class ServiceCapabilities: |
| @staticmethod |
| def create(service_category, service_capabilities_bytes): |
| # Select the appropriate subclass |
| if service_category == AVDTP_MEDIA_CODEC_SERVICE_CATEGORY: |
| cls = MediaCodecCapabilities |
| else: |
| cls = ServiceCapabilities |
| |
| # Create an instance and initialize it |
| instance = cls.__new__(cls) |
| instance.service_category = service_category |
| instance.service_capabilities_bytes = service_capabilities_bytes |
| instance.init_from_bytes() |
| |
| return instance |
| |
| @staticmethod |
| def parse_capabilities(payload): |
| capabilities = [] |
| while payload: |
| service_category = payload[0] |
| length_of_service_capabilities = payload[1] |
| service_capabilities_bytes = payload[2:2 + length_of_service_capabilities] |
| capabilities.append(ServiceCapabilities.create(service_category, service_capabilities_bytes)) |
| |
| payload = payload[2 + length_of_service_capabilities:] |
| |
| return capabilities |
| |
| @staticmethod |
| def serialize_capabilities(capabilities): |
| serialized = b'' |
| for item in capabilities: |
| serialized += bytes([ |
| item.service_category, |
| len(item.service_capabilities_bytes) |
| ]) + item.service_capabilities_bytes |
| return serialized |
| |
| def init_from_bytes(self): |
| pass |
| |
| def __init__(self, service_category, service_capabilities_bytes=b''): |
| self.service_category = service_category |
| self.service_capabilities_bytes = service_capabilities_bytes |
| |
| def to_string(self, details=[]): |
| attributes = ','.join([name_or_number(AVDTP_SERVICE_CATEGORY_NAMES, self.service_category)] + details) |
| return f'ServiceCapabilities({attributes})' |
| |
| def __str__(self): |
| if self.service_capabilities_bytes: |
| details = [self.service_capabilities_bytes.hex()] |
| else: |
| details = [] |
| return self.to_string(details) |
| |
| |
| # ----------------------------------------------------------------------------- |
| class MediaCodecCapabilities(ServiceCapabilities): |
| def init_from_bytes(self): |
| self.media_type = self.service_capabilities_bytes[0] |
| self.media_codec_type = self.service_capabilities_bytes[1] |
| self.media_codec_information = self.service_capabilities_bytes[2:] |
| |
| if self.media_codec_type == A2DP_SBC_CODEC_TYPE: |
| self.media_codec_information = SbcMediaCodecInformation.from_bytes(self.media_codec_information) |
| elif self.media_codec_type == A2DP_MPEG_2_4_AAC_CODEC_TYPE: |
| self.media_codec_information = AacMediaCodecInformation.from_bytes(self.media_codec_information) |
| elif self.media_codec_type == A2DP_NON_A2DP_CODEC_TYPE: |
| self.media_codec_information = VendorSpecificMediaCodecInformation.from_bytes(self.media_codec_information) |
| |
| def __init__(self, media_type, media_codec_type, media_codec_information): |
| super().__init__( |
| AVDTP_MEDIA_CODEC_SERVICE_CATEGORY, |
| bytes([media_type, media_codec_type]) + bytes(media_codec_information) |
| ) |
| self.media_type = media_type |
| self.media_codec_type = media_codec_type |
| self.media_codec_information = media_codec_information |
| |
| def __str__(self): |
| details = [ |
| f'media_type={name_or_number(AVDTP_MEDIA_TYPE_NAMES, self.media_type)}', |
| f'codec={name_or_number(A2DP_CODEC_TYPE_NAMES, self.media_codec_type)}', |
| f'codec_info={self.media_codec_information.hex() if type(self.media_codec_information) is bytes else str(self.media_codec_information)}' |
| ] |
| return self.to_string(details) |
| |
| |
| # ----------------------------------------------------------------------------- |
| class EndPointInfo: |
| @staticmethod |
| def from_bytes(payload): |
| return EndPointInfo( |
| payload[0] >> 2, |
| payload[0] >> 1 & 1, |
| payload[1] >> 4, |
| payload[1] >> 3 & 1 |
| ) |
| |
| def __bytes__(self): |
| return bytes([ |
| self.seid << 2 | self.in_use << 1, |
| self.media_type << 4 | self.tsep << 3 |
| ]) |
| |
| def __init__(self, seid, in_use, media_type, tsep): |
| self.seid = seid |
| self.in_use = in_use |
| self.media_type = media_type |
| self.tsep = tsep |
| |
| |
| # ----------------------------------------------------------------------------- |
| class Message: |
| COMMAND = 0 |
| GENERAL_REJECT = 1 |
| RESPONSE_ACCEPT = 2 |
| RESPONSE_REJECT = 3 |
| |
| MESSAGE_TYPE_NAMES = { |
| COMMAND: 'COMMAND', |
| GENERAL_REJECT: 'GENERAL_REJECT', |
| RESPONSE_ACCEPT: 'RESPONSE_ACCEPT', |
| RESPONSE_REJECT: 'RESPONSE_REJECT' |
| } |
| |
| subclasses = {} # Subclasses, by signal identifier and message type |
| |
| @staticmethod |
| def message_type_name(message_type): |
| return name_or_number(Message.MESSAGE_TYPE_NAMES, message_type) |
| |
| @staticmethod |
| def subclass(cls): |
| # Infer the signal identifier and message subtype from the class name |
| name = cls.__name__ |
| if name == 'General_Reject': |
| cls.signal_identifier = 0 |
| signal_identifier_str = None |
| message_type = Message.COMMAND |
| elif name.endswith('_Command'): |
| signal_identifier_str = name[:-8] |
| message_type = Message.COMMAND |
| elif name.endswith('_Response'): |
| signal_identifier_str = name[:-9] |
| message_type = Message.RESPONSE_ACCEPT |
| elif name.endswith('_Reject'): |
| signal_identifier_str = name[:-7] |
| message_type = Message.RESPONSE_REJECT |
| else: |
| raise ValueError('invalid class name') |
| |
| cls.message_type = message_type |
| |
| if signal_identifier_str is not None: |
| for (name, signal_identifier) in AVDTP_SIGNAL_IDENTIFIERS.items(): |
| if name.lower().endswith(signal_identifier_str.lower()): |
| cls.signal_identifier = signal_identifier |
| break |
| |
| # Register the subclass |
| Message.subclasses.setdefault(cls.signal_identifier, {})[cls.message_type] = cls |
| |
| return cls |
| |
| # Factory method to create a subclass based on the signal identifier and message type |
| @staticmethod |
| def create(signal_identifier, message_type, payload): |
| # Look for a registered subclass |
| subclasses = Message.subclasses.get(signal_identifier) |
| if subclasses: |
| subclass = subclasses.get(message_type) |
| if subclass: |
| instance = subclass.__new__(subclass) |
| instance.payload = payload |
| instance.init_from_payload() |
| return instance |
| |
| # Instantiate the appropriate class based on the message type |
| if message_type == Message.RESPONSE_REJECT: |
| # Assume a simple reject message |
| instance = Simple_Reject(payload) |
| instance.init_from_payload() |
| else: |
| instance = Message(payload) |
| instance.signal_identifier = signal_identifier |
| instance.message_type = message_type |
| return instance |
| |
| def init_from_payload(self): |
| pass |
| |
| def __init__(self, payload=b''): |
| self.payload = payload |
| |
| def to_string(self, details): |
| base = f'{color(f"{name_or_number(AVDTP_SIGNAL_NAMES, self.signal_identifier)}_{Message.message_type_name(self.message_type)}", "yellow")}' |
| if details: |
| if type(details) is str: |
| return f'{base}: {details}' |
| else: |
| return base + ':\n' + '\n'.join([' ' + color(detail, 'cyan') for detail in details]) |
| else: |
| return base |
| |
| def __str__(self): |
| return self.to_string(self.payload.hex()) |
| |
| |
| # ----------------------------------------------------------------------------- |
| class Simple_Command(Message): |
| ''' |
| Command message with just one seid |
| ''' |
| |
| def init_from_payload(self): |
| self.acp_seid = self.payload[0] >> 2 |
| |
| def __init__(self, seid): |
| self.acp_seid = seid |
| self.payload = bytes([seid << 2]) |
| |
| def __str__(self): |
| return self.to_string([f'ACP SEID: {self.acp_seid}']) |
| |
| |
| # ----------------------------------------------------------------------------- |
| class Simple_Reject(Message): |
| ''' |
| Reject messages with just an error code |
| ''' |
| |
| def init_from_payload(self): |
| self.error_code = self.payload[0] |
| |
| def __init__(self, error_code): |
| self.error_code = error_code |
| self.payload = bytes([self.error_code]) |
| |
| def __str__(self): |
| details = [ |
| f'error_code: {name_or_number(AVDTP_ERROR_NAMES, self.error_code)}' |
| ] |
| return self.to_string(details) |
| |
| |
| # ----------------------------------------------------------------------------- |
| @Message.subclass |
| class Discover_Command(Message): |
| ''' |
| See Bluetooth AVDTP spec - 8.6.1 Stream End Point Discovery Command |
| ''' |
| |
| |
| # ----------------------------------------------------------------------------- |
| @Message.subclass |
| class Discover_Response(Message): |
| ''' |
| See Bluetooth AVDTP spec - 8.6.2 Stream End Point Discovery Response |
| ''' |
| |
| def init_from_payload(self): |
| self.endpoints = [] |
| endpoint_count = len(self.payload) // 2 |
| for i in range(endpoint_count): |
| self.endpoints.append(EndPointInfo.from_bytes(self.payload[i * 2:(i + 1) * 2])) |
| |
| def __init__(self, endpoints): |
| self.endpoints = endpoints |
| self.payload = b''.join([bytes(endpoint) for endpoint in endpoints]) |
| |
| def __str__(self): |
| details = [] |
| for endpoint in self.endpoints: |
| details.extend( |
| [ |
| f'ACP SEID: {endpoint.seid}', |
| f' in_use: {endpoint.in_use}', |
| f' media_type: {name_or_number(AVDTP_MEDIA_TYPE_NAMES, endpoint.media_type)}', |
| f' tsep: {name_or_number(AVDTP_TSEP_NAMES, endpoint.tsep)}' |
| ] |
| ) |
| return self.to_string(details) |
| |
| |
| # ----------------------------------------------------------------------------- |
| @Message.subclass |
| class Get_Capabilities_Command(Simple_Command): |
| ''' |
| See Bluetooth AVDTP spec - 8.7.1 Get Capabilities Command |
| ''' |
| |
| |
| # ----------------------------------------------------------------------------- |
| @Message.subclass |
| class Get_Capabilities_Response(Message): |
| ''' |
| See Bluetooth AVDTP spec - 8.7.2 Get All Capabilities Response |
| ''' |
| |
| def init_from_payload(self): |
| self.capabilities = ServiceCapabilities.parse_capabilities(self.payload) |
| |
| def __init__(self, capabilities): |
| self.capabilities = capabilities |
| self.payload = ServiceCapabilities.serialize_capabilities(capabilities) |
| |
| def __str__(self): |
| details = [str(capability) for capability in self.capabilities] |
| return self.to_string(details) |
| |
| |
| # ----------------------------------------------------------------------------- |
| @Message.subclass |
| class Get_Capabilities_Reject(Simple_Reject): |
| ''' |
| See Bluetooth AVDTP spec - 8.7.3 Get Capabilities Reject |
| ''' |
| |
| |
| # ----------------------------------------------------------------------------- |
| @Message.subclass |
| class Get_All_Capabilities_Command(Get_Capabilities_Command): |
| ''' |
| See Bluetooth AVDTP spec - 8.8.1 Get All Capabilities Command |
| ''' |
| |
| |
| # ----------------------------------------------------------------------------- |
| @Message.subclass |
| class Get_All_Capabilities_Response(Get_Capabilities_Response): |
| ''' |
| See Bluetooth AVDTP spec - 8.8.2 Get All Capabilities Response |
| ''' |
| |
| |
| # ----------------------------------------------------------------------------- |
| @Message.subclass |
| class Get_All_Capabilities_Reject(Simple_Reject): |
| ''' |
| See Bluetooth AVDTP spec - 8.8.3 Get All Capabilities Reject |
| ''' |
| |
| |
| # ----------------------------------------------------------------------------- |
| @Message.subclass |
| class Set_Configuration_Command(Message): |
| ''' |
| See Bluetooth AVDTP spec - 8.9.1 Set Configuration Command |
| ''' |
| |
| def init_from_payload(self): |
| self.acp_seid = self.payload[0] >> 2 |
| self.int_seid = self.payload[1] >> 2 |
| self.capabilities = ServiceCapabilities.parse_capabilities(self.payload[2:]) |
| |
| def __init__(self, acp_seid, int_seid, capabilities): |
| self.acp_seid = acp_seid |
| self.int_seid = int_seid |
| self.capabilities = capabilities |
| self.payload = bytes([acp_seid << 2, int_seid << 2]) + ServiceCapabilities.serialize_capabilities(capabilities) |
| |
| def __str__(self): |
| details = [ |
| f'ACP SEID: {self.acp_seid}', |
| f'INT SEID: {self.int_seid}' |
| ] + [str(capability) for capability in self.capabilities] |
| return self.to_string(details) |
| |
| |
| # ----------------------------------------------------------------------------- |
| @Message.subclass |
| class Set_Configuration_Response(Message): |
| ''' |
| See Bluetooth AVDTP spec - 8.9.2 Set Configuration Response |
| ''' |
| |
| |
| # ----------------------------------------------------------------------------- |
| @Message.subclass |
| class Set_Configuration_Reject(Message): |
| ''' |
| See Bluetooth AVDTP spec - 8.9.3 Set Configuration Reject |
| ''' |
| |
| def init_from_payload(self): |
| self.service_category = self.payload[0] |
| self.error_code = self.payload[1] |
| |
| def __init__(self, service_category, error_code): |
| self.service_category = service_category |
| self.error_code = error_code |
| self.payload = bytes([service_category, self.error_code]) |
| |
| def __str__(self): |
| details = [ |
| f'service_category: {name_or_number(AVDTP_SERVICE_CATEGORY_NAMES, self.service_category)}', |
| f'error_code: {name_or_number(AVDTP_ERROR_NAMES, self.error_code)}' |
| ] |
| return self.to_string(details) |
| |
| |
| # ----------------------------------------------------------------------------- |
| @Message.subclass |
| class Get_Configuration_Command(Simple_Command): |
| ''' |
| See Bluetooth AVDTP spec - 8.10.1 Get Configuration Command |
| ''' |
| |
| |
| # ----------------------------------------------------------------------------- |
| @Message.subclass |
| class Get_Configuration_Response(Message): |
| ''' |
| See Bluetooth AVDTP spec - 8.10.2 Get Configuration Response |
| ''' |
| |
| def init_from_payload(self): |
| self.capabilities = ServiceCapabilities.parse_capabilities(self.payload) |
| |
| def __init__(self, capabilities): |
| self.capabilities = capabilities |
| self.payload = ServiceCapabilities.serialize_capabilities(capabilities) |
| |
| def __str__(self): |
| details = [str(capability) for capability in self.capabilities] |
| return self.to_string(details) |
| |
| |
| # ----------------------------------------------------------------------------- |
| @Message.subclass |
| class Get_Configuration_Reject(Simple_Reject): |
| ''' |
| See Bluetooth AVDTP spec - 8.10.3 Get Configuration Reject |
| ''' |
| |
| |
| # ----------------------------------------------------------------------------- |
| @Message.subclass |
| class Reconfigure_Command(Message): |
| ''' |
| See Bluetooth AVDTP spec - 8.11.1 Reconfigure Command |
| ''' |
| |
| def init_from_payload(self): |
| self.acp_seid = self.payload[0] >> 2 |
| self.capabilities = ServiceCapabilities.parse_capabilities(self.payload[1:]) |
| |
| def __str__(self): |
| details = [ |
| f'ACP SEID: {self.acp_seid}', |
| ] + [str(capability) for capability in self.capabilities] |
| return self.to_string(details) |
| |
| |
| # ----------------------------------------------------------------------------- |
| @Message.subclass |
| class Reconfigure_Response(Message): |
| ''' |
| See Bluetooth AVDTP spec - 8.11.2 Reconfigure Response |
| ''' |
| |
| |
| # ----------------------------------------------------------------------------- |
| @Message.subclass |
| class Reconfigure_Reject(Set_Configuration_Reject): |
| ''' |
| See Bluetooth AVDTP spec - 8.11.3 Reconfigure Reject |
| ''' |
| |
| |
| # ----------------------------------------------------------------------------- |
| @Message.subclass |
| class Open_Command(Simple_Command): |
| ''' |
| See Bluetooth AVDTP spec - 8.12.1 Open Stream Command |
| ''' |
| |
| |
| # ----------------------------------------------------------------------------- |
| @Message.subclass |
| class Open_Response(Message): |
| ''' |
| See Bluetooth AVDTP spec - 8.12.2 Open Stream Response |
| ''' |
| |
| |
| # ----------------------------------------------------------------------------- |
| @Message.subclass |
| class Open_Reject(Simple_Reject): |
| ''' |
| See Bluetooth AVDTP spec - 8.12.3 Open Stream Reject |
| ''' |
| |
| |
| # ----------------------------------------------------------------------------- |
| @Message.subclass |
| class Start_Command(Message): |
| ''' |
| See Bluetooth AVDTP spec - 8.13.1 Start Stream Command |
| ''' |
| |
| def init_from_payload(self): |
| self.acp_seids = [x >> 2 for x in self.payload] |
| |
| def __init__(self, seids): |
| self.acp_seids = seids |
| self.payload = bytes([seid << 2 for seid in self.acp_seids]) |
| |
| def __str__(self): |
| return self.to_string([f'ACP SEIDs: {self.acp_seids}']) |
| |
| |
| # ----------------------------------------------------------------------------- |
| @Message.subclass |
| class Start_Response(Message): |
| ''' |
| See Bluetooth AVDTP spec - 8.13.2 Start Stream Response |
| ''' |
| |
| |
| # ----------------------------------------------------------------------------- |
| @Message.subclass |
| class Start_Reject(Message): |
| ''' |
| See Bluetooth AVDTP spec - 8.13.3 Set Configuration Reject |
| ''' |
| |
| def init_from_payload(self): |
| self.acp_seid = self.payload[0] >> 2 |
| self.error_code = self.payload[1] |
| |
| def __init__(self, acp_seid, error_code): |
| self.acp_seid = acp_seid |
| self.error_code = error_code |
| self.payload = bytes([self.acp_seid << 2, self.error_code]) |
| |
| def __str__(self): |
| details = [ |
| f'acp_seid: {self.acp_seid}', |
| f'error_code: {name_or_number(AVDTP_ERROR_NAMES, self.error_code)}' |
| ] |
| return self.to_string(details) |
| |
| |
| # ----------------------------------------------------------------------------- |
| @Message.subclass |
| class Close_Command(Simple_Command): |
| ''' |
| See Bluetooth AVDTP spec - 8.14.1 Close Stream Command |
| ''' |
| |
| |
| # ----------------------------------------------------------------------------- |
| @Message.subclass |
| class Close_Response(Message): |
| ''' |
| See Bluetooth AVDTP spec - 8.14.2 Close Stream Response |
| ''' |
| |
| |
| # ----------------------------------------------------------------------------- |
| @Message.subclass |
| class Close_Reject(Simple_Reject): |
| ''' |
| See Bluetooth AVDTP spec - 8.14.3 Close Stream Reject |
| ''' |
| |
| |
| # ----------------------------------------------------------------------------- |
| @Message.subclass |
| class Suspend_Command(Start_Command): |
| ''' |
| See Bluetooth AVDTP spec - 8.15.1 Suspend Command |
| ''' |
| |
| |
| # ----------------------------------------------------------------------------- |
| @Message.subclass |
| class Suspend_Response(Message): |
| ''' |
| See Bluetooth AVDTP spec - 8.15.2 Suspend Response |
| ''' |
| |
| |
| # ----------------------------------------------------------------------------- |
| @Message.subclass |
| class Suspend_Reject(Start_Reject): |
| ''' |
| See Bluetooth AVDTP spec - 8.15.3 Suspend Reject |
| ''' |
| |
| |
| # ----------------------------------------------------------------------------- |
| @Message.subclass |
| class Abort_Command(Simple_Command): |
| ''' |
| See Bluetooth AVDTP spec - 8.16.1 Abort Command |
| ''' |
| |
| |
| # ----------------------------------------------------------------------------- |
| @Message.subclass |
| class Abort_Response(Message): |
| ''' |
| See Bluetooth AVDTP spec - 8.16.2 Abort Response |
| ''' |
| |
| |
| # ----------------------------------------------------------------------------- |
| @Message.subclass |
| class Security_Control_Command(Message): |
| ''' |
| See Bluetooth AVDTP spec - 8.17.1 Security Control Command |
| ''' |
| |
| |
| # ----------------------------------------------------------------------------- |
| @Message.subclass |
| class Security_Control_Response(Message): |
| ''' |
| See Bluetooth AVDTP spec - 8.17.2 Security Control Response |
| ''' |
| |
| |
| # ----------------------------------------------------------------------------- |
| @Message.subclass |
| class Security_Control_Reject(Simple_Reject): |
| ''' |
| See Bluetooth AVDTP spec - 8.17.3 Security Control Reject |
| ''' |
| |
| |
| # ----------------------------------------------------------------------------- |
| @Message.subclass |
| class General_Reject(Message): |
| ''' |
| See Bluetooth AVDTP spec - 8.18 General Reject |
| ''' |
| |
| def to_string(self, details): |
| return f'{color(f"GENERAL_REJECT", "yellow")}' |
| |
| |
| # ----------------------------------------------------------------------------- |
| @Message.subclass |
| class DelayReport_Command(Message): |
| ''' |
| See Bluetooth AVDTP spec - 8.19.1 Delay Report Command |
| ''' |
| |
| def init_from_payload(self): |
| self.acp_seid = self.payload[0] >> 2 |
| self.delay = (self.payload[1] << 8) | (self.payload[2]) |
| |
| def __str__(self): |
| return self.to_string([ |
| f'ACP_SEID: {self.acp_seid}', |
| f'delay: {self.delay}' |
| ]) |
| |
| |
| # ----------------------------------------------------------------------------- |
| @Message.subclass |
| class DelayReport_Response(Message): |
| ''' |
| See Bluetooth AVDTP spec - 8.19.2 Delay Report Response |
| ''' |
| |
| |
| # ----------------------------------------------------------------------------- |
| @Message.subclass |
| class DelayReport_Reject(Simple_Reject): |
| ''' |
| See Bluetooth AVDTP spec - 8.19.3 Delay Report Reject |
| ''' |
| |
| |
| # ----------------------------------------------------------------------------- |
| class Protocol: |
| SINGLE_PACKET = 0 |
| START_PACKET = 1 |
| CONTINUE_PACKET = 2 |
| END_PACKET = 3 |
| |
| PACKET_TYPE_NAMES = { |
| SINGLE_PACKET: 'SINGLE_PACKET', |
| START_PACKET: 'START_PACKET', |
| CONTINUE_PACKET: 'CONTINUE_PACKET', |
| END_PACKET: 'END_PACKET' |
| } |
| |
| @staticmethod |
| def packet_type_name(packet_type): |
| return name_or_number(Protocol.PACKET_TYPE_NAMES, packet_type) |
| |
| @staticmethod |
| async def connect(connection, version=(1, 3)): |
| connector = connection.create_l2cap_connector(AVDTP_PSM) |
| channel = await connector() |
| protocol = Protocol(channel, version) |
| protocol.channel_connector = connector |
| |
| return protocol |
| |
| def __init__(self, l2cap_channel, version=(1, 3)): |
| self.l2cap_channel = l2cap_channel |
| self.version = version |
| self.rtx_sig_timer = AVDTP_DEFAULT_RTX_SIG_TIMER |
| self.message_assembler = MessageAssembler(self.on_message) |
| self.transaction_results = [None] * 16 # Futures for up to 16 transactions |
| self.transaction_semaphore = asyncio.Semaphore(16) |
| self.transaction_count = 0 |
| self.channel_acceptor = None |
| self.channel_connector = None |
| self.local_endpoints = [] # Local endpoints, with contiguous seid values |
| self.remote_endpoints = {} # Remote stream endpoints, by seid |
| self.streams = {} # Streams, by seid |
| |
| # Register to receive PDUs from the channel |
| l2cap_channel.sink = self.on_pdu |
| l2cap_channel.on('open', self.on_l2cap_channel_open) |
| |
| def get_local_endpoint_by_seid(self, seid): |
| if seid > 0 and seid <= len(self.local_endpoints): |
| return self.local_endpoints[seid - 1] |
| |
| def add_source(self, codec_capabilities, packet_pump): |
| seid = len(self.local_endpoints) + 1 |
| source = LocalSource(self, seid, codec_capabilities, packet_pump) |
| self.local_endpoints.append(source) |
| |
| return source |
| |
| def add_sink(self, codec_capabilities): |
| seid = len(self.local_endpoints) + 1 |
| sink = LocalSink(self, seid, codec_capabilities) |
| self.local_endpoints.append(sink) |
| |
| return sink |
| |
| async def create_stream(self, source, sink): |
| # Check that the source isn't already used in a stream |
| if source.in_use: |
| raise InvalidStateError('source already in use') |
| |
| # Create or reuse a new stream to associate the source and the sink |
| if source.seid in self.streams: |
| stream = self.streams[source.seid] |
| else: |
| stream = Stream(self, source, sink) |
| self.streams[source.seid] = stream |
| |
| # The stream can now be configured |
| await stream.configure() |
| |
| return stream |
| |
| async def discover_remote_endpoints(self): |
| self.remote_endpoints = {} |
| |
| response = await self.send_command(Discover_Command()) |
| for endpoint_entry in response.endpoints: |
| logger.debug(f'getting endpoint capabilities for endpoint {endpoint_entry.seid}') |
| get_capabilities_response = await self.get_capabilities(endpoint_entry.seid) |
| endpoint = DiscoveredStreamEndPoint( |
| self, |
| endpoint_entry.seid, |
| endpoint_entry.media_type, |
| endpoint_entry.tsep, |
| endpoint_entry.in_use, |
| get_capabilities_response.capabilities |
| ) |
| self.remote_endpoints[endpoint_entry.seid] = endpoint |
| |
| return self.remote_endpoints.values() |
| |
| def find_remote_sink_by_codec(self, media_type, codec_type): |
| for endpoint in self.remote_endpoints.values(): |
| if not endpoint.in_use and endpoint.media_type == media_type and endpoint.tsep == AVDTP_TSEP_SNK: |
| has_media_transport = False |
| has_codec = False |
| for capabilities in endpoint.capabilities: |
| if capabilities.service_category == AVDTP_MEDIA_TRANSPORT_SERVICE_CATEGORY: |
| has_media_transport = True |
| elif capabilities.service_category == AVDTP_MEDIA_CODEC_SERVICE_CATEGORY: |
| if capabilities.media_type == AVDTP_AUDIO_MEDIA_TYPE and capabilities.media_codec_type == codec_type: |
| has_codec = True |
| if has_media_transport and has_codec: |
| return endpoint |
| |
| def on_pdu(self, pdu): |
| self.message_assembler.on_pdu(pdu) |
| |
| def on_message(self, transaction_label, message): |
| logger.debug(f'{color("<<< Received AVDTP message", "magenta")}: [{transaction_label}] {message}') |
| |
| # Check that the identifier is not reserved |
| if message.signal_identifier == 0: |
| logger.warning('!!! reserved signal identifier') |
| return |
| |
| # Check that the identifier is valid |
| if message.signal_identifier < 0 or message.signal_identifier > AVDTP_DELAYREPORT: |
| logger.warning('!!! invalid signal identifier') |
| self.send_message(transaction_label, General_Reject()) |
| |
| if message.message_type == Message.COMMAND: |
| # Command |
| handler_name = f'on_{AVDTP_SIGNAL_NAMES.get(message.signal_identifier,"").replace("AVDTP_","").lower()}_command' |
| handler = getattr(self, handler_name, None) |
| if handler: |
| try: |
| response = handler(message) |
| self.send_message(transaction_label, response) |
| except Exception as error: |
| logger.warning(f'{color("!!! Exception in handler:", "red")} {error}') |
| else: |
| logger.warning('unhandled command') |
| else: |
| # Response, look for a pending transaction with the same label |
| transaction_result = self.transaction_results[transaction_label] |
| if transaction_result is None: |
| logger.warning(color('!!! no pending transaction for label', 'red')) |
| return |
| |
| transaction_result.set_result(message) |
| self.transaction_results[transaction_label] = None |
| self.transaction_semaphore.release() |
| |
| def on_l2cap_connection(self, channel): |
| # Forward the channel to the endpoint that's expecting it |
| if self.channel_acceptor: |
| self.channel_acceptor.on_l2cap_connection(channel) |
| |
| def on_l2cap_channel_open(self): |
| logger.debug(color('<<< L2CAP channel open', 'magenta')) |
| |
| def send_message(self, transaction_label, message): |
| logger.debug(f'{color(">>> Sending AVDTP message", "magenta")}: [{transaction_label}] {message}') |
| max_fragment_size = self.l2cap_channel.mtu - 3 # Enough space for a 3-byte start packet header |
| payload = message.payload |
| if len(payload) + 2 <= self.l2cap_channel.mtu: |
| # Fits in a single packet |
| packet_type = self.SINGLE_PACKET |
| else: |
| packet_type = self.START_PACKET |
| |
| done = False |
| while not done: |
| first_header_byte = transaction_label << 4 | packet_type << 2 | message.message_type |
| |
| if packet_type == self.SINGLE_PACKET: |
| header = bytes([first_header_byte, message.signal_identifier]) |
| elif packet_type == self.START_PACKET: |
| packet_count = (max_fragment_size - 1 + len(payload)) // max_fragment_size |
| header = bytes([first_header_byte, message.signal_identifier, packet_count]) |
| else: |
| header = bytes([first_header_byte]) |
| |
| # Send one packet |
| self.l2cap_channel.send_pdu(header + payload[:max_fragment_size]) |
| |
| # Prepare for the next packet |
| payload = payload[max_fragment_size:] |
| if payload: |
| packet_type = self.CONTINUE_PACKET if payload > max_fragment_size else self.END_PACKET |
| else: |
| done = True |
| |
| async def send_command(self, command): |
| # TODO: support timeouts |
| # Send the command |
| (transaction_label, transaction_result) = await self.start_transaction() |
| self.send_message(transaction_label, command) |
| |
| # Wait for the response |
| response = await transaction_result |
| |
| # Check for errors |
| if response.message_type == Message.GENERAL_REJECT or response.message_type == Message.RESPONSE_REJECT: |
| raise ProtocolError(response.error_code, 'avdtp') |
| |
| return response |
| |
| async def start_transaction(self): |
| # Wait until we can start a new transaction |
| await self.transaction_semaphore.acquire() |
| |
| # Look for the next free entry to store the transaction result |
| for i in range(16): |
| transaction_label = (self.transaction_count + i) % 16 |
| if self.transaction_results[transaction_label] is None: |
| transaction_result = asyncio.get_running_loop().create_future() |
| self.transaction_results[transaction_label] = transaction_result |
| self.transaction_count += 1 |
| return (transaction_label, transaction_result) |
| |
| assert(False) # Should never reach this |
| |
| async def get_capabilities(self, seid): |
| if self.version > (1, 2): |
| return await self.send_command(Get_All_Capabilities_Command(seid)) |
| else: |
| return await self.send_command(Get_Capabilities_Command(seid)) |
| |
| async def set_configuration(self, acp_seid, int_seid, capabilities): |
| return await self.send_command(Set_Configuration_Command(acp_seid, int_seid, capabilities)) |
| |
| async def get_configuration(self, seid): |
| response = await self.send_command(Get_Configuration_Command(seid)) |
| return response.capabilities |
| |
| async def open(self, seid): |
| return await self.send_command(Open_Command(seid)) |
| |
| async def start(self, seids): |
| return await self.send_command(Start_Command(seids)) |
| |
| async def suspend(self, seids): |
| return await self.send_command(Suspend_Command(seids)) |
| |
| async def close(self, seid): |
| return await self.send_command(Close_Command(seid)) |
| |
| async def abort(self, seid): |
| return await self.send_command(Abort_Command(seid)) |
| |
| def on_discover_command(self, command): |
| endpoint_infos = [ |
| EndPointInfo(endpoint.seid, 0, endpoint.media_type, endpoint.tsep) |
| for endpoint in self.local_endpoints |
| ] |
| return Discover_Response(endpoint_infos) |
| |
| def on_get_capabilities_command(self, command): |
| endpoint = self.get_local_endpoint_by_seid(command.acp_seid) |
| if endpoint is None: |
| return Get_Capabilities_Reject(AVDTP_BAD_ACP_SEID_ERROR) |
| |
| return Get_Capabilities_Response(endpoint.capabilities) |
| |
| def on_get_all_capabilities_command(self, command): |
| endpoint = self.get_local_endpoint_by_seid(command.acp_seid) |
| if endpoint is None: |
| return Get_All_Capabilities_Reject(AVDTP_BAD_ACP_SEID_ERROR) |
| |
| return Get_All_Capabilities_Response(endpoint.capabilities) |
| |
| def on_set_configuration_command(self, command): |
| endpoint = self.get_local_endpoint_by_seid(command.acp_seid) |
| if endpoint is None: |
| return Set_Configuration_Reject(AVDTP_BAD_ACP_SEID_ERROR) |
| |
| # Check that the local endpoint isn't in use |
| if endpoint.in_use: |
| return Set_Configuration_Reject(AVDTP_SEP_IN_USE_ERROR) |
| |
| # Create a stream object for the pair of endpoints |
| stream = Stream(self, endpoint, StreamEndPointProxy(self, command.int_seid)) |
| self.streams[command.acp_seid] = stream |
| |
| result = stream.on_set_configuration_command(command.capabilities) |
| return result or Set_Configuration_Response() |
| |
| def on_get_configuration_command(self, command): |
| endpoint = self.get_local_endpoint_by_seid(command.acp_seid) |
| if endpoint is None: |
| return Get_Configuration_Reject(AVDTP_BAD_ACP_SEID_ERROR) |
| if endpoint.stream is None: |
| return Get_Configuration_Reject(AVDTP_BAD_STATE_ERROR) |
| |
| return endpoint.stream.on_get_configuration_command() |
| |
| def on_reconfigure_command(self, command): |
| endpoint = self.get_local_endpoint_by_seid(command.acp_seid) |
| if endpoint is None: |
| return Reconfigure_Reject(0, AVDTP_BAD_ACP_SEID_ERROR) |
| if endpoint.stream is None: |
| return Reconfigure_Reject(0, AVDTP_BAD_STATE_ERROR) |
| |
| result = endpoint.stream.on_reconfigure_command(command.capabilities) |
| return result or Reconfigure_Response() |
| |
| def on_open_command(self, command): |
| endpoint = self.get_local_endpoint_by_seid(command.acp_seid) |
| if endpoint is None: |
| return Open_Reject(AVDTP_BAD_ACP_SEID_ERROR) |
| if endpoint.stream is None: |
| return Open_Reject(AVDTP_BAD_STATE_ERROR) |
| |
| result = endpoint.stream.on_open_command() |
| return result or Open_Response() |
| |
| def on_start_command(self, command): |
| for seid in command.acp_seids: |
| endpoint = self.get_local_endpoint_by_seid(seid) |
| if endpoint is None: |
| return Start_Reject(seid, AVDTP_BAD_ACP_SEID_ERROR) |
| if endpoint.stream is None: |
| return Start_Reject(AVDTP_BAD_STATE_ERROR) |
| |
| # Start all streams |
| # TODO: deal with partial failures |
| for seid in command.acp_seids: |
| endpoint = self.get_local_endpoint_by_seid(seid) |
| result = endpoint.stream.on_start_command() |
| if result is not None: |
| return result |
| |
| return Start_Response() |
| |
| def on_suspend_command(self, command): |
| for seid in command.acp_seids: |
| endpoint = self.get_local_endpoint_by_seid(seid) |
| if endpoint is None: |
| return Suspend_Reject(seid, AVDTP_BAD_ACP_SEID_ERROR) |
| if endpoint.stream is None: |
| return Suspend_Reject(seid, AVDTP_BAD_STATE_ERROR) |
| |
| # Suspend all streams |
| # TODO: deal with partial failures |
| for seid in command.acp_seids: |
| endpoint = self.get_local_endpoint_by_seid(seid) |
| result = endpoint.stream.on_suspend_command() |
| if result is not None: |
| return result |
| |
| return Suspend_Response() |
| |
| def on_close_command(self, command): |
| endpoint = self.get_local_endpoint_by_seid(command.acp_seid) |
| if endpoint is None: |
| return Close_Reject(AVDTP_BAD_ACP_SEID_ERROR) |
| if endpoint.stream is None: |
| return Close_Reject(AVDTP_BAD_STATE_ERROR) |
| |
| result = endpoint.stream.on_close_command() |
| return result or Close_Response() |
| |
| def on_abort_command(self, command): |
| endpoint = self.get_local_endpoint_by_seid(command.acp_seid) |
| if endpoint is None or endpoint.stream is None: |
| return Abort_Response() |
| |
| endpoint.stream.on_abort_command() |
| return Abort_Response() |
| |
| def on_security_control_command(self, command): |
| endpoint = self.get_local_endpoint_by_seid(command.acp_seid) |
| if endpoint is None: |
| return Security_Control_Reject(AVDTP_BAD_ACP_SEID_ERROR) |
| |
| result = endpoint.on_security_control_command(command.payload) |
| return result or Security_Control_Response() |
| |
| def on_delayreport_command(self, command): |
| endpoint = self.get_local_endpoint_by_seid(command.acp_seid) |
| if endpoint is None: |
| return DelayReport_Reject(AVDTP_BAD_ACP_SEID_ERROR) |
| |
| result = endpoint.on_delayreport_command(command.delay) |
| return result or DelayReport_Response() |
| |
| |
| # ----------------------------------------------------------------------------- |
| class Listener(EventEmitter): |
| @staticmethod |
| def create_registrar(device): |
| return device.create_l2cap_registrar(AVDTP_PSM) |
| |
| def set_server(self, connection, server): |
| self.servers[connection.handle] = server |
| |
| def __init__(self, registrar, version=(1, 3)): |
| super().__init__() |
| self.version = version |
| self.servers = {} # Servers, by connection handle |
| |
| # Listen for incoming L2CAP connections |
| registrar(self.on_l2cap_connection) |
| |
| def on_l2cap_connection(self, channel): |
| logger.debug(f'{color("<<< incoming L2CAP connection:", "magenta")} {channel}') |
| |
| if channel.connection.handle in self.servers: |
| # This is a channel for a stream endpoint |
| server = self.servers[channel.connection.handle] |
| server.on_l2cap_connection(channel) |
| else: |
| # This is a new command/response channel |
| def on_channel_open(): |
| server = Protocol(channel, self.version) |
| self.set_server(channel.connection, server) |
| self.emit('connection', server) |
| channel.on('open', on_channel_open) |
| |
| |
| # ----------------------------------------------------------------------------- |
| class Stream: |
| ''' |
| Pair of a local and a remote stream endpoint that can stream from one to the other |
| ''' |
| |
| @staticmethod |
| def state_name(state): |
| return name_or_number(AVDTP_STATE_NAMES, state) |
| |
| def change_state(self, state): |
| logger.debug(f'{self} state change -> {color(self.state_name(state), "cyan")}') |
| self.state = state |
| |
| def send_media_packet(self, packet): |
| self.rtp_channel.send_pdu(bytes(packet)) |
| |
| async def configure(self): |
| if self.state != AVDTP_IDLE_STATE: |
| raise InvalidStateError('current state is not IDLE') |
| |
| await self.remote_endpoint.set_configuration( |
| self.local_endpoint.seid, |
| self.local_endpoint.configuration |
| ) |
| self.change_state(AVDTP_CONFIGURED_STATE) |
| |
| async def open(self): |
| if self.state != AVDTP_CONFIGURED_STATE: |
| raise InvalidStateError('current state is not CONFIGURED') |
| |
| logger.debug('opening remote endpoint') |
| await self.remote_endpoint.open() |
| |
| self.change_state(AVDTP_OPEN_STATE) |
| |
| # Create a channel for RTP packets |
| self.rtp_channel = await self.protocol.channel_connector() |
| |
| async def start(self): |
| # Auto-open if needed |
| if self.state == AVDTP_CONFIGURED_STATE: |
| await self.open() |
| |
| if self.state != AVDTP_OPEN_STATE: |
| raise InvalidStateError('current state is not OPEN') |
| |
| logger.debug('starting remote endpoint') |
| await self.remote_endpoint.start() |
| |
| logger.debug('starting local endpoint') |
| await self.local_endpoint.start() |
| |
| self.change_state(AVDTP_STREAMING_STATE) |
| |
| async def stop(self): |
| if self.state != AVDTP_STREAMING_STATE: |
| raise InvalidStateError('current state is not STREAMING') |
| |
| logger.debug('stopping local endpoint') |
| await self.local_endpoint.stop() |
| |
| logger.debug('stopping remote endpoint') |
| await self.remote_endpoint.stop() |
| |
| self.change_state(AVDTP_OPEN_STATE) |
| |
| async def close(self): |
| if self.state not in {AVDTP_OPEN_STATE, AVDTP_STREAMING_STATE}: |
| raise InvalidStateError('current state is not OPEN or STREAMING') |
| |
| logger.debug('closing local endpoint') |
| await self.local_endpoint.close() |
| |
| logger.debug('closing remote endpoint') |
| await self.remote_endpoint.close() |
| |
| # Release any channels we may have created |
| self.change_state(AVDTP_CLOSING_STATE) |
| if self.rtp_channel: |
| await self.rtp_channel.disconnect() |
| self.rtp_channel = None |
| |
| # Release the endpoint |
| self.local_endpoint.in_use = 0 |
| |
| self.change_state(AVDTP_IDLE_STATE) |
| |
| def on_set_configuration_command(self, configuration): |
| if self.state != AVDTP_IDLE_STATE: |
| return Set_Configuration_Reject(AVDTP_BAD_STATE_ERROR) |
| |
| result = self.local_endpoint.on_set_configuration_command(configuration) |
| if result is not None: |
| return result |
| |
| self.change_state(AVDTP_CONFIGURED_STATE) |
| |
| def on_get_configuration_command(self, configuration): |
| if self.state not in {AVDTP_CONFIGURED_STATE, AVDTP_OPEN_STATE, AVDTP_STREAMING_STATE}: |
| return Get_Configuration_Reject(AVDTP_BAD_STATE_ERROR) |
| |
| return self.local_endpoint.on_get_configuration_command(configuration) |
| |
| def on_reconfigure_command(self, configuration): |
| if self.state != AVDTP_OPEN_STATE: |
| return Reconfigure_Reject(AVDTP_BAD_STATE_ERROR) |
| |
| result = self.local_endpoint.on_reconfigure_command(configuration) |
| if result is not None: |
| return result |
| |
| def on_open_command(self): |
| if self.state != AVDTP_CONFIGURED_STATE: |
| return Open_Reject(AVDTP_BAD_STATE_ERROR) |
| |
| result = self.local_endpoint.on_open_command() |
| if result is not None: |
| return result |
| |
| # Register to accept the next channel |
| self.protocol.channel_acceptor = self |
| |
| self.change_state(AVDTP_OPEN_STATE) |
| |
| def on_start_command(self): |
| if self.state != AVDTP_OPEN_STATE: |
| return Open_Reject(AVDTP_BAD_STATE_ERROR) |
| |
| # Check that we have an RTP channel |
| if self.rtp_channel is None: |
| logger.warning('received start command before RTP channel establishment') |
| return Open_Reject(AVDTP_BAD_STATE_ERROR) |
| |
| result = self.local_endpoint.on_start_command() |
| if result is not None: |
| return result |
| |
| self.change_state(AVDTP_STREAMING_STATE) |
| |
| def on_suspend_command(self): |
| if self.state != AVDTP_STREAMING_STATE: |
| return Open_Reject(AVDTP_BAD_STATE_ERROR) |
| |
| result = self.local_endpoint.on_suspend_command() |
| if result is not None: |
| return result |
| |
| self.change_state(AVDTP_OPEN_STATE) |
| |
| def on_close_command(self): |
| if self.state not in {AVDTP_OPEN_STATE, AVDTP_STREAMING_STATE}: |
| return Open_Reject(AVDTP_BAD_STATE_ERROR) |
| |
| result = self.local_endpoint.on_close_command() |
| if result is not None: |
| return result |
| |
| self.change_state(AVDTP_CLOSING_STATE) |
| |
| if self.rtp_channel is None: |
| # No channel to release, we're done |
| self.change_state(AVDTP_IDLE_STATE) |
| else: |
| # TODO: set a timer as we wait for the RTP channel to be closed |
| pass |
| |
| def on_abort_command(self): |
| if self.rtp_channel is None: |
| # No need to wait |
| self.change_state(AVDTP_IDLE_STATE) |
| else: |
| # Wait for the RTP channel to be closed |
| self.change_state(AVDTP_ABORTING_STATE) |
| |
| def on_l2cap_connection(self, channel): |
| logger.debug(color('<<< stream channel connected', 'magenta')) |
| self.rtp_channel = channel |
| channel.on('open', self.on_l2cap_channel_open) |
| channel.on('close', self.on_l2cap_channel_close) |
| |
| # We don't need more channels |
| self.protocol.channel_acceptor = None |
| |
| def on_l2cap_channel_open(self): |
| logger.debug(color('<<< stream channel open', 'magenta')) |
| self.local_endpoint.on_rtp_channel_open() |
| |
| def on_l2cap_channel_close(self): |
| logger.debug(color('<<< stream channel closed', 'magenta')) |
| self.local_endpoint.on_rtp_channel_close() |
| self.local_endpoint.in_use = 0 |
| self.rtp_channel = None |
| |
| if self.state in {AVDTP_CLOSING_STATE, AVDTP_ABORTING_STATE}: |
| self.change_state(AVDTP_IDLE_STATE) |
| else: |
| logger.warning('unexpected channel close while not CLOSING or ABORTING') |
| |
| def __init__(self, protocol, local_endpoint, remote_endpoint): |
| ''' |
| remote_endpoint must be a subclass of StreamEndPointProxy |
| |
| ''' |
| self.protocol = protocol |
| self.local_endpoint = local_endpoint |
| self.remote_endpoint = remote_endpoint |
| self.rtp_channel = None |
| self.state = AVDTP_IDLE_STATE |
| |
| local_endpoint.stream = self |
| local_endpoint.in_use = 1 |
| |
| def __str__(self): |
| return f'Stream({self.local_endpoint.seid} -> {self.remote_endpoint.seid} {self.state_name(self.state)})' |
| |
| |
| # ----------------------------------------------------------------------------- |
| class StreamEndPoint: |
| def __init__(self, seid, media_type, tsep, in_use, capabilities): |
| self.seid = seid |
| self.media_type = media_type |
| self.tsep = tsep |
| self.in_use = in_use |
| self.capabilities = capabilities |
| |
| def __str__(self): |
| return '\n'.join([ |
| 'SEP(', |
| f' seid={self.seid}', |
| f' media_type={name_or_number(AVDTP_MEDIA_TYPE_NAMES, self.media_type)}', |
| f' tsep={name_or_number(AVDTP_TSEP_NAMES, self.tsep)}', |
| f' in_use={self.in_use}', |
| ' capabilities=[', |
| '\n'.join([f' {x}' for x in self.capabilities]), |
| ' ]', |
| ')' |
| ]) |
| |
| |
| # ----------------------------------------------------------------------------- |
| class StreamEndPointProxy: |
| def __init__(self, protocol, seid): |
| self.seid = seid |
| self.protocol = protocol |
| |
| async def set_configuration(self, int_seid, configuration): |
| return await self.protocol.set_configuration( |
| self.seid, |
| int_seid, |
| configuration |
| ) |
| |
| async def open(self): |
| return await self.protocol.open(self.seid) |
| |
| async def start(self): |
| return await self.protocol.start([self.seid]) |
| |
| async def stop(self): |
| return await self.protocol.suspend([self.seid]) |
| |
| async def close(self): |
| return await self.protocol.close(self.seid) |
| |
| async def abort(self): |
| return await self.protocol.abort(self.seid) |
| |
| |
| # ----------------------------------------------------------------------------- |
| class DiscoveredStreamEndPoint(StreamEndPoint, StreamEndPointProxy): |
| def __init__(self, protocol, seid, media_type, tsep, in_use, capabilities): |
| StreamEndPoint.__init__(self, seid, media_type, tsep, in_use, capabilities) |
| StreamEndPointProxy.__init__(self, protocol, seid) |
| |
| |
| # ----------------------------------------------------------------------------- |
| class LocalStreamEndPoint(StreamEndPoint): |
| def __init__(self, protocol, seid, media_type, tsep, capabilities, configuration=[]): |
| super().__init__(seid, media_type, tsep, 0, capabilities) |
| self.protocol = protocol |
| self.configuration = configuration |
| self.stream = None |
| |
| async def start(self): |
| pass |
| |
| async def stop(self): |
| pass |
| |
| async def close(self): |
| pass |
| |
| def on_reconfigure_command(self, command): |
| pass |
| |
| def on_get_configuration_command(self): |
| return Get_Configuration_Response(self.configuration) |
| |
| def on_open_command(self): |
| pass |
| |
| def on_start_command(self): |
| pass |
| |
| def on_suspend_command(self): |
| pass |
| |
| def on_close_command(self): |
| pass |
| |
| def on_abort_command(self): |
| pass |
| |
| def on_rtp_channel_open(self): |
| pass |
| |
| def on_rtp_channel_close(self): |
| pass |
| |
| |
| # ----------------------------------------------------------------------------- |
| class LocalSource(LocalStreamEndPoint, EventEmitter): |
| def __init__(self, protocol, seid, codec_capabilities, packet_pump): |
| capabilities = [ |
| ServiceCapabilities(AVDTP_MEDIA_TRANSPORT_SERVICE_CATEGORY), |
| codec_capabilities |
| ] |
| LocalStreamEndPoint.__init__(self, protocol, seid, codec_capabilities.media_type, AVDTP_TSEP_SRC, capabilities, capabilities) |
| EventEmitter.__init__(self) |
| self.packet_pump = packet_pump |
| |
| async def start(self): |
| if self.packet_pump: |
| return await self.packet_pump.start(self.stream.rtp_channel) |
| else: |
| self.emit('start', self.stream.rtp_channel) |
| |
| async def stop(self): |
| if self.packet_pump: |
| return await self.packet_pump.stop() |
| else: |
| self.emit('stop') |
| |
| def on_set_configuration_command(self, configuration): |
| # For now, blindly accept the configuration |
| logger.debug(f'<<< received source configuration: {configuration}') |
| self.configuration = configuration |
| |
| def on_start_command(self): |
| asyncio.create_task(self.start()) |
| |
| def on_suspend_command(self): |
| asyncio.create_task(self.stop()) |
| |
| |
| # ----------------------------------------------------------------------------- |
| class LocalSink(LocalStreamEndPoint, EventEmitter): |
| def __init__(self, protocol, seid, codec_capabilities): |
| capabilities = [ |
| ServiceCapabilities(AVDTP_MEDIA_TRANSPORT_SERVICE_CATEGORY), |
| codec_capabilities |
| ] |
| LocalStreamEndPoint.__init__(self, protocol, seid, codec_capabilities.media_type, AVDTP_TSEP_SNK, capabilities) |
| EventEmitter.__init__(self) |
| |
| def on_set_configuration_command(self, configuration): |
| # For now, blindly accept the configuration |
| logger.debug(f'<<< received sink configuration: {configuration}') |
| self.configuration = configuration |
| |
| def on_rtp_channel_open(self): |
| logger.debug(color('<<< RTP channel open', 'magenta')) |
| self.stream.rtp_channel.sink = self.on_avdtp_packet |
| |
| def on_avdtp_packet(self, packet): |
| rtp_packet = MediaPacket.from_bytes(packet) |
| logger.debug(f'{color("<<< RTP Packet:", "green")} {rtp_packet} {rtp_packet.payload[:16].hex()}') |
| self.emit('rtp_packet', rtp_packet) |