1# Copyright 2021-2022 Google LLC 2# 3# Licensed under the Apache License, Version 2.0 (the "License"); 4# you may not use this file except in compliance with the License. 5# You may obtain a copy of the License at 6# 7# https://www.apache.org/licenses/LICENSE-2.0 8# 9# Unless required by applicable law or agreed to in writing, software 10# distributed under the License is distributed on an "AS IS" BASIS, 11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12# See the License for the specific language governing permissions and 13# limitations under the License. 14 15# ----------------------------------------------------------------------------- 16# Imports 17# ----------------------------------------------------------------------------- 18import asyncio 19import sys 20import os 21import logging 22 23from bumble.colors import color 24from bumble.device import Device 25from bumble.transport import open_transport_or_link 26from bumble.core import BT_BR_EDR_TRANSPORT 27from bumble.avdtp import ( 28 find_avdtp_service_with_connection, 29 AVDTP_AUDIO_MEDIA_TYPE, 30 MediaCodecCapabilities, 31 MediaPacketPump, 32 Protocol, 33 Listener, 34) 35from bumble.a2dp import ( 36 SBC_JOINT_STEREO_CHANNEL_MODE, 37 SBC_LOUDNESS_ALLOCATION_METHOD, 38 make_audio_source_service_sdp_records, 39 A2DP_SBC_CODEC_TYPE, 40 SbcMediaCodecInformation, 41 SbcPacketSource, 42) 43 44 45# ----------------------------------------------------------------------------- 46def sdp_records(): 47 service_record_handle = 0x00010001 48 return { 49 service_record_handle: make_audio_source_service_sdp_records( 50 service_record_handle 51 ) 52 } 53 54 55# ----------------------------------------------------------------------------- 56def codec_capabilities(): 57 # NOTE: this shouldn't be hardcoded, but should be inferred from the input file 58 # instead 59 return MediaCodecCapabilities( 60 media_type=AVDTP_AUDIO_MEDIA_TYPE, 61 media_codec_type=A2DP_SBC_CODEC_TYPE, 62 media_codec_information=SbcMediaCodecInformation.from_discrete_values( 63 sampling_frequency=44100, 64 channel_mode=SBC_JOINT_STEREO_CHANNEL_MODE, 65 block_length=16, 66 subbands=8, 67 allocation_method=SBC_LOUDNESS_ALLOCATION_METHOD, 68 minimum_bitpool_value=2, 69 maximum_bitpool_value=53, 70 ), 71 ) 72 73 74# ----------------------------------------------------------------------------- 75def on_avdtp_connection(read_function, protocol): 76 packet_source = SbcPacketSource( 77 read_function, protocol.l2cap_channel.peer_mtu, codec_capabilities() 78 ) 79 packet_pump = MediaPacketPump(packet_source.packets) 80 protocol.add_source(packet_source.codec_capabilities, packet_pump) 81 82 83# ----------------------------------------------------------------------------- 84async def stream_packets(read_function, protocol): 85 # Discover all endpoints on the remote device 86 endpoints = await protocol.discover_remote_endpoints() 87 for endpoint in endpoints: 88 print('@@@', endpoint) 89 90 # Select a sink 91 sink = protocol.find_remote_sink_by_codec( 92 AVDTP_AUDIO_MEDIA_TYPE, A2DP_SBC_CODEC_TYPE 93 ) 94 if sink is None: 95 print(color('!!! no SBC sink found', 'red')) 96 return 97 print(f'### Selected sink: {sink.seid}') 98 99 # Stream the packets 100 packet_source = SbcPacketSource( 101 read_function, protocol.l2cap_channel.peer_mtu, codec_capabilities() 102 ) 103 packet_pump = MediaPacketPump(packet_source.packets) 104 source = protocol.add_source(packet_source.codec_capabilities, packet_pump) 105 stream = await protocol.create_stream(source, sink) 106 await stream.start() 107 await asyncio.sleep(5) 108 await stream.stop() 109 await asyncio.sleep(5) 110 await stream.start() 111 await asyncio.sleep(5) 112 await stream.stop() 113 await stream.close() 114 115 116# ----------------------------------------------------------------------------- 117async def main() -> None: 118 if len(sys.argv) < 4: 119 print( 120 'Usage: run_a2dp_source.py <device-config> <transport-spec> <sbc-file> ' 121 '[<bluetooth-address>]' 122 ) 123 print( 124 'example: run_a2dp_source.py classic1.json usb:0 test.sbc E1:CA:72:48:C4:E8' 125 ) 126 return 127 128 print('<<< connecting to HCI...') 129 async with await open_transport_or_link(sys.argv[2]) as hci_transport: 130 print('<<< connected') 131 132 # Create a device 133 device = Device.from_config_file_with_hci( 134 sys.argv[1], hci_transport.source, hci_transport.sink 135 ) 136 device.classic_enabled = True 137 138 # Setup the SDP to expose the SRC service 139 device.sdp_service_records = sdp_records() 140 141 # Start 142 await device.power_on() 143 144 with open(sys.argv[3], 'rb') as sbc_file: 145 # NOTE: this should be using asyncio file reading, but blocking reads are 146 # good enough for testing 147 async def read(byte_count): 148 return sbc_file.read(byte_count) 149 150 if len(sys.argv) > 4: 151 # Connect to a peer 152 target_address = sys.argv[4] 153 print(f'=== Connecting to {target_address}...') 154 connection = await device.connect( 155 target_address, transport=BT_BR_EDR_TRANSPORT 156 ) 157 print(f'=== Connected to {connection.peer_address}!') 158 159 # Request authentication 160 print('*** Authenticating...') 161 await connection.authenticate() 162 print('*** Authenticated') 163 164 # Enable encryption 165 print('*** Enabling encryption...') 166 await connection.encrypt() 167 print('*** Encryption on') 168 169 # Look for an A2DP service 170 avdtp_version = await find_avdtp_service_with_connection(connection) 171 if not avdtp_version: 172 print(color('!!! no A2DP service found')) 173 return 174 175 # Create a client to interact with the remote device 176 protocol = await Protocol.connect(connection, avdtp_version) 177 178 # Start streaming 179 await stream_packets(read, protocol) 180 else: 181 # Create a listener to wait for AVDTP connections 182 listener = Listener.for_device(device=device, version=(1, 2)) 183 listener.on( 184 'connection', lambda protocol: on_avdtp_connection(read, protocol) 185 ) 186 187 # Become connectable and wait for a connection 188 await device.set_discoverable(True) 189 await device.set_connectable(True) 190 191 await hci_transport.source.wait_for_termination() 192 193 194# ----------------------------------------------------------------------------- 195logging.basicConfig(level=os.environ.get('BUMBLE_LOGLEVEL', 'DEBUG').upper()) 196asyncio.run(main()) 197