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+ # -----------------------------------------------------------------------------
18+ import asyncio
19+ import sys
20+
21+ import bumble .logging
22+ from bumble .a2dp import (
23+ A2DP_SBC_CODEC_TYPE ,
24+ SbcMediaCodecInformation ,
25+ SbcPacketSource ,
26+ make_audio_source_service_sdp_records ,
27+ )
28+ from bumble .avdtp import (
29+ AVDTP_AUDIO_MEDIA_TYPE ,
30+ Listener ,
31+ MediaCodecCapabilities ,
32+ MediaPacketPump ,
33+ Protocol ,
34+ find_avdtp_service_with_connection ,
35+ )
36+ from bumble .colors import color
37+ from bumble .core import PhysicalTransport
38+ from bumble .device import Device
39+ from bumble .transport import open_transport
40+
41+
42+ # -----------------------------------------------------------------------------
43+ def sdp_records ():
44+ service_record_handle = 0x00010001
45+ return {
46+ service_record_handle : make_audio_source_service_sdp_records (
47+ service_record_handle
48+ )
49+ }
50+
51+
52+ # -----------------------------------------------------------------------------
53+ def codec_capabilities ():
54+ # NOTE: this shouldn't be hardcoded, but should be inferred from the input file
55+ # instead
56+ return MediaCodecCapabilities (
57+ media_type = AVDTP_AUDIO_MEDIA_TYPE ,
58+ media_codec_type = A2DP_SBC_CODEC_TYPE ,
59+ media_codec_information = SbcMediaCodecInformation (
60+ sampling_frequency = SbcMediaCodecInformation .SamplingFrequency .SF_44100 ,
61+ channel_mode = SbcMediaCodecInformation .ChannelMode .JOINT_STEREO ,
62+ block_length = SbcMediaCodecInformation .BlockLength .BL_16 ,
63+ subbands = SbcMediaCodecInformation .Subbands .S_8 ,
64+ allocation_method = SbcMediaCodecInformation .AllocationMethod .LOUDNESS ,
65+ minimum_bitpool_value = 2 ,
66+ maximum_bitpool_value = 53 ,
67+ ),
68+ )
69+
70+
71+ # -----------------------------------------------------------------------------
72+ def on_avdtp_connection (read_function , protocol ):
73+ packet_source = SbcPacketSource (read_function , protocol .l2cap_channel .peer_mtu )
74+ packet_pump = MediaPacketPump (packet_source .packets )
75+ protocol .add_source (codec_capabilities (), packet_pump )
76+
77+
78+ # -----------------------------------------------------------------------------
79+ async 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 (
87+ AVDTP_AUDIO_MEDIA_TYPE , A2DP_SBC_CODEC_TYPE
88+ )
89+ if sink is None :
90+ print (color ('!!! no SBC sink found' , 'red' ))
91+ return
92+ print (f'### Selected sink: { sink .seid } ' )
93+
94+ # Stream the packets
95+ packet_source = SbcPacketSource (read_function , protocol .l2cap_channel .peer_mtu )
96+ packet_pump = MediaPacketPump (packet_source .packets )
97+ source = protocol .add_source (codec_capabilities (), packet_pump )
98+ stream = await protocol .create_stream (source , sink )
99+ await stream .start ()
100+ await asyncio .sleep (5 )
101+ await stream .stop ()
102+ await asyncio .sleep (5 )
103+ await stream .start ()
104+ await asyncio .sleep (5 )
105+ await stream .stop ()
106+ await stream .close ()
107+
108+
109+ # -----------------------------------------------------------------------------
110+ async def main () -> None :
111+ if len (sys .argv ) < 4 :
112+ print (
113+ 'Usage: run_a2dp_source.py <device-config> <transport-spec> <sbc-file> '
114+ '[<bluetooth-address>]'
115+ )
116+ print (
117+ 'example: run_a2dp_source.py classic1.json usb:0 test.sbc E1:CA:72:48:C4:E8'
118+ )
119+ return
120+
121+ print ('<<< connecting to HCI...' )
122+ async with await open_transport (sys .argv [2 ]) as hci_transport :
123+ print ('<<< connected' )
124+
125+ # Create a device
126+ device = Device .from_config_file_with_hci (
127+ sys .argv [1 ], hci_transport .source , hci_transport .sink
128+ )
129+ device .classic_enabled = True
130+
131+ # Setup the SDP to expose the SRC service
132+ device .sdp_service_records = sdp_records ()
133+
134+ # Start
135+ await device .power_on ()
136+
137+ with open (sys .argv [3 ], 'rb' ) as sbc_file :
138+ # NOTE: this should be using asyncio file reading, but blocking reads are
139+ # good enough for testing
140+ async def read (byte_count ):
141+ return sbc_file .read (byte_count )
142+
143+ if len (sys .argv ) > 4 :
144+ # Connect to a peer
145+ target_address = sys .argv [4 ]
146+ print (f'=== Connecting to { target_address } ...' )
147+ connection = await device .connect (
148+ target_address , transport = PhysicalTransport .BR_EDR
149+ )
150+ print (f'=== Connected to { connection .peer_address } !' )
151+
152+ # Request authentication
153+ print ('*** Authenticating...' )
154+ await connection .authenticate ()
155+ print ('*** Authenticated' )
156+
157+ # Enable encryption
158+ print ('*** Enabling encryption...' )
159+ await connection .encrypt ()
160+ print ('*** Encryption on' )
161+
162+ # Look for an A2DP service
163+ avdtp_version = await find_avdtp_service_with_connection (connection )
164+ if not avdtp_version :
165+ print (color ('!!! no A2DP service found' ))
166+ return
167+
168+ # Create a client to interact with the remote device
169+ protocol = await Protocol .connect (connection , avdtp_version )
170+
171+ # Start streaming
172+ await stream_packets (read , protocol )
173+ else :
174+ # Create a listener to wait for AVDTP connections
175+ listener = Listener .for_device (device = device , version = (1 , 2 ))
176+ listener .on (
177+ 'connection' , lambda protocol : on_avdtp_connection (read , protocol )
178+ )
179+
180+ # Become connectable and wait for a connection
181+ await device .set_discoverable (True )
182+ await device .set_connectable (True )
183+
184+ await hci_transport .source .wait_for_termination ()
185+
186+
187+ # -----------------------------------------------------------------------------
188+ bumble .logging .setup_basic_logging ('DEBUG' )
189+ asyncio .run (main ())
0 commit comments