• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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