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 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(service_record_handle) 50 } 51 52 53# ----------------------------------------------------------------------------- 54def codec_capabilities(): 55 # NOTE: this shouldn't be hardcoded, but should be inferred from the input file instead 56 return MediaCodecCapabilities( 57 media_type = AVDTP_AUDIO_MEDIA_TYPE, 58 media_codec_type = A2DP_SBC_CODEC_TYPE, 59 media_codec_information = SbcMediaCodecInformation.from_discrete_values( 60 sampling_frequency = 44100, 61 channel_mode = SBC_JOINT_STEREO_CHANNEL_MODE, 62 block_length = 16, 63 subbands = 8, 64 allocation_method = SBC_LOUDNESS_ALLOCATION_METHOD, 65 minimum_bitpool_value = 2, 66 maximum_bitpool_value = 53 67 ) 68 ) 69 70 71# ----------------------------------------------------------------------------- 72def on_avdtp_connection(read_function, protocol): 73 packet_source = SbcPacketSource(read_function, protocol.l2cap_channel.mtu, codec_capabilities()) 74 packet_pump = MediaPacketPump(packet_source.packets) 75 protocol.add_source(packet_source.codec_capabilities, packet_pump) 76 77 78# ----------------------------------------------------------------------------- 79async def stream_packets(read_function, protocol): 80 # Discover all endpoints on the remote device 81 endpoints = await protocol.discover_remote_endpoints() 82 for endpoint in endpoints: 83 print('@@@', endpoint) 84 85 # Select a sink 86 sink = protocol.find_remote_sink_by_codec(AVDTP_AUDIO_MEDIA_TYPE, A2DP_SBC_CODEC_TYPE) 87 if sink is None: 88 print(color('!!! no SBC sink found', 'red')) 89 return 90 print(f'### Selected sink: {sink.seid}') 91 92 # Stream the packets 93 packet_source = SbcPacketSource(read_function, protocol.l2cap_channel.mtu, codec_capabilities()) 94 packet_pump = MediaPacketPump(packet_source.packets) 95 source = protocol.add_source(packet_source.codec_capabilities, packet_pump) 96 stream = await protocol.create_stream(source, sink) 97 await stream.start() 98 await asyncio.sleep(5) 99 await stream.stop() 100 await asyncio.sleep(5) 101 await stream.start() 102 await asyncio.sleep(5) 103 await stream.stop() 104 await stream.close() 105 106 107# ----------------------------------------------------------------------------- 108async def main(): 109 if len(sys.argv) < 4: 110 print('Usage: run_a2dp_source.py <device-config> <transport-spec> <sbc-file> [<bluetooth-address>]') 111 print('example: run_a2dp_source.py classic1.json usb:0 test.sbc E1:CA:72:48:C4:E8') 112 return 113 114 print('<<< connecting to HCI...') 115 async with await open_transport_or_link(sys.argv[2]) as (hci_source, hci_sink): 116 print('<<< connected') 117 118 # Create a device 119 device = Device.from_config_file_with_hci(sys.argv[1], hci_source, hci_sink) 120 device.classic_enabled = True 121 122 # Setup the SDP to expose the SRC service 123 device.sdp_service_records = sdp_records() 124 125 # Start 126 await device.power_on() 127 128 with open(sys.argv[3], 'rb') as sbc_file: 129 # NOTE: this should be using asyncio file reading, but blocking reads are good enough for testing 130 async def read(byte_count): 131 return sbc_file.read(byte_count) 132 133 if len(sys.argv) > 4: 134 # Connect to a peer 135 target_address = sys.argv[4] 136 print(f'=== Connecting to {target_address}...') 137 connection = await device.connect(target_address, transport=BT_BR_EDR_TRANSPORT) 138 print(f'=== Connected to {connection.peer_address}!') 139 140 # Request authentication 141 print('*** Authenticating...') 142 await connection.authenticate() 143 print('*** Authenticated') 144 145 # Enable encryption 146 print('*** Enabling encryption...') 147 await connection.encrypt() 148 print('*** Encryption on') 149 150 # Look for an A2DP service 151 avdtp_version = await find_avdtp_service_with_connection(device, connection) 152 if not avdtp_version: 153 print(color('!!! no A2DP service found')) 154 return 155 156 # Create a client to interact with the remote device 157 protocol = await Protocol.connect(connection, avdtp_version) 158 159 # Start streaming 160 await stream_packets(read, protocol) 161 else: 162 # Create a listener to wait for AVDTP connections 163 listener = Listener(Listener.create_registrar(device), version=(1, 2)) 164 listener.on('connection', lambda protocol: on_avdtp_connection(read, protocol)) 165 166 # Become connectable and wait for a connection 167 await device.set_discoverable(True) 168 await device.set_connectable(True) 169 170 await hci_source.wait_for_termination() 171 172 173# ----------------------------------------------------------------------------- 174logging.basicConfig(level = os.environ.get('BUMBLE_LOGLEVEL', 'DEBUG').upper()) 175asyncio.run(main()) 176