# 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.get_running_loop().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.get_running_loop().create_task(self.start()) def on_suspend_command(self): asyncio.get_running_loop().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)