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