diff --git a/apps/speaker/speaker.css b/apps/speaker/speaker.css
index 2d3bcfbf..075068b0 100644
--- a/apps/speaker/speaker.css
+++ b/apps/speaker/speaker.css
@@ -41,17 +41,27 @@ body, h1, h2, h3, h4, h5, h6 {
margin: 6px;
}
+#connectionStateText {
+ background-color: rgb(112, 146, 206);
+ border: none;
+ border-radius: 8px;
+ padding: 10px 20px;
+ display: inline-block;
+ margin: 6px;
+}
+
#propertiesTable {
border: grey;
border-style: solid;
border-radius: 4px;
padding: 4px;
margin: 6px;
+ margin-left: 0px;
}
th, td {
- padding-left: 8px;
- padding-right: 8px;
+ padding-left: 6px;
+ padding-right: 6px;
}
.properties td:nth-child(even) {
diff --git a/apps/speaker/speaker.html b/apps/speaker/speaker.html
index 4786aabe..f68abccb 100644
--- a/apps/speaker/speaker.html
+++ b/apps/speaker/speaker.html
@@ -22,6 +22,7 @@
IDLE
+ NOT CONNECTED
diff --git a/apps/speaker/speaker.js b/apps/speaker/speaker.js
index 68094d46..7010524c 100644
--- a/apps/speaker/speaker.js
+++ b/apps/speaker/speaker.js
@@ -8,6 +8,7 @@ let codecText;
let packetsReceivedText;
let bytesReceivedText;
let streamStateText;
+let connectionStateText;
let controlsDiv;
let audioOnButton;
let mediaSource;
@@ -27,7 +28,7 @@ let fftCanvasContext;
let bandwidthCanvas;
let bandwidthCanvasContext;
let bandwidthBinCount;
-let bandwidthBins;
+let bandwidthBins = [];
const FFT_WIDTH = 800;
const FFT_HEIGHT = 256;
@@ -56,11 +57,14 @@ function initUI() {
packetsReceivedText = document.getElementById("packetsReceivedText");
bytesReceivedText = document.getElementById("bytesReceivedText");
streamStateText = document.getElementById("streamStateText");
+ connectionStateText = document.getElementById("connectionStateText");
audioSupportMessageText = document.getElementById("audioSupportMessageText");
audioOnButton.onclick = () => startAudio();
setConnectionText("");
+
+ requestAnimationFrame(onAnimationFrame);
}
function initMediaSource() {
@@ -94,20 +98,20 @@ function initAnalyzer() {
function startAnalyzer() {
// FFT
- audioContext = new AudioContext();
- audioAnalyzer = audioContext.createAnalyser();
- audioAnalyzer.fftSize = 128;
- audioFrequencyBinCount = audioAnalyzer.frequencyBinCount;
- audioFrequencyData = new Uint8Array(audioFrequencyBinCount);
- const stream = audioElement.captureStream();
- const source = audioContext.createMediaStreamSource(stream);
- source.connect(audioAnalyzer);
+ if (audioElement.captureStream !== undefined) {
+ audioContext = new AudioContext();
+ audioAnalyzer = audioContext.createAnalyser();
+ audioAnalyzer.fftSize = 128;
+ audioFrequencyBinCount = audioAnalyzer.frequencyBinCount;
+ audioFrequencyData = new Uint8Array(audioFrequencyBinCount);
+ const stream = audioElement.captureStream();
+ const source = audioContext.createMediaStreamSource(stream);
+ source.connect(audioAnalyzer);
+ }
// Bandwidth
bandwidthBinCount = BANDWIDTH_WIDTH / 2;
bandwidthBins = [];
-
- requestAnimationFrame(onAnimationFrame);
}
function setConnectionText(message) {
@@ -121,15 +125,17 @@ function setConnectionText(message) {
function onAnimationFrame() {
// FFT
- audioAnalyzer.getByteFrequencyData(audioFrequencyData);
- fftCanvasContext.fillStyle = "rgb(0, 0, 0)";
- fftCanvasContext.fillRect(0, 0, FFT_WIDTH, FFT_HEIGHT);
- const barCount = audioFrequencyBinCount;
- const barWidth = (FFT_WIDTH / audioFrequencyBinCount) - 1;
- for (let bar = 0; bar < barCount; bar++) {
- const barHeight = audioFrequencyData[bar];
- fftCanvasContext.fillStyle = `rgb(${barHeight / 256 * 200 + 50}, 50, ${50 + 2 * bar})`;
- fftCanvasContext.fillRect(bar * (barWidth + 1), FFT_HEIGHT - barHeight, barWidth, barHeight);
+ if (audioAnalyzer !== undefined) {
+ audioAnalyzer.getByteFrequencyData(audioFrequencyData);
+ fftCanvasContext.fillStyle = "rgb(0, 0, 0)";
+ fftCanvasContext.fillRect(0, 0, FFT_WIDTH, FFT_HEIGHT);
+ const barCount = audioFrequencyBinCount;
+ const barWidth = (FFT_WIDTH / audioFrequencyBinCount) - 1;
+ for (let bar = 0; bar < barCount; bar++) {
+ const barHeight = audioFrequencyData[bar];
+ fftCanvasContext.fillStyle = `rgb(${barHeight / 256 * 200 + 50}, 50, ${50 + 2 * bar})`;
+ fftCanvasContext.fillRect(bar * (barWidth + 1), FFT_HEIGHT - barHeight, barWidth, barHeight);
+ }
}
// Bandwidth
@@ -175,14 +181,11 @@ async function startAudio() {
}
function onAudioPacket(packet) {
- if (audioState == "stopped") {
- // Drop the packet, we're not ready to play.
- return;
+ if (audioState != "stopped") {
+ // Queue the audio packet.
+ sourceBuffer.appendBuffer(packet);
}
- // Queue the audio packet.
- sourceBuffer.appendBuffer(packet);
-
packetsReceived += 1;
packetsReceivedText.innerText = packetsReceived;
bytesReceived += packet.byteLength;
@@ -268,6 +271,14 @@ function onSuspendMessage(params) {
streamStateText.innerText = streamState;
}
+function onConnectionMessage(params) {
+ connectionStateText.innerText = `CONNECTED: ${params.peer_name} (${params.peer_address})`;
+}
+
+function onDisconnectionMessage(params) {
+ connectionStateText.innerText = "DISCONNECTED";
+}
+
function sendMessage(message) {
channelSocket.send(JSON.stringify(message));
}
@@ -287,7 +298,9 @@ const messageHandlers = {
onHelloMessage,
onStartMessage,
onStopMessage,
- onSuspendMessage
+ onSuspendMessage,
+ onConnectionMessage,
+ onDisconnectionMessage
}
window.onload = (event) => {
diff --git a/apps/speaker/speaker.py b/apps/speaker/speaker.py
index 3bf17ddc..8de2badd 100644
--- a/apps/speaker/speaker.py
+++ b/apps/speaker/speaker.py
@@ -1,4 +1,4 @@
-# Copyright 2021-2022 Google LLC
+# Copyright 2021-2023 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
@@ -23,6 +23,7 @@ import json
import os
import logging
import pathlib
+import subprocess
from typing import Dict, List, Optional
import weakref
@@ -33,7 +34,7 @@ from aiohttp import web
import bumble
from bumble.colors import color
from bumble.core import BT_BR_EDR_TRANSPORT
-from bumble.device import Device, DeviceConfiguration
+from bumble.device import Connection, Device, DeviceConfiguration, Peer
from bumble.sdp import ServiceAttribute
from bumble.transport import open_transport
from bumble.avdtp import (
@@ -61,6 +62,12 @@ from bumble.utils import AsyncRunner
from bumble.codecs import AacAudioRtpPacket
+# -----------------------------------------------------------------------------
+# Logging
+# -----------------------------------------------------------------------------
+logger = logging.getLogger(__name__)
+
+
# -----------------------------------------------------------------------------
# Constants
# -----------------------------------------------------------------------------
@@ -100,13 +107,22 @@ class SbcAudioExtractor:
# -----------------------------------------------------------------------------
class Output:
- async def start(self):
+ async def start(self) -> None:
pass
- async def stop(self):
+ async def stop(self) -> None:
pass
- async def suspend(self):
+ async def suspend(self) -> None:
+ pass
+
+ async def on_connection(self, connection: Connection) -> None:
+ pass
+
+ async def on_disconnection(self, reason: int) -> None:
+ pass
+
+ def on_rtp_packet(self, packet: MediaPacket) -> None:
pass
@@ -157,7 +173,7 @@ class QueuedOutput(Output):
def on_rtp_packet(self, packet: MediaPacket) -> None:
if self.packets.qsize() > self.MAX_QUEUE_SIZE:
- print("queue full, dropping")
+ logger.debug("queue full, dropping")
return
self.packets.put_nowait(self.extractor.extract_audio(packet))
@@ -170,6 +186,19 @@ class WebSocketOutput(QueuedOutput):
self.send_audio = send_audio
self.send_message = send_message
+ async def on_connection(self, connection: Connection) -> None:
+ await connection.request_remote_name()
+ peer_name = '' if connection.peer_name is None else connection.peer_name
+ peer_address = str(connection.peer_address).replace('/P', '')
+ await self.send_message(
+ 'connection',
+ peer_address=peer_address,
+ peer_name=peer_name,
+ )
+
+ async def on_disconnection(self, reason) -> None:
+ await self.send_message('disconnection')
+
async def on_audio_packet(self, packet: bytes) -> None:
await self.send_audio(packet)
@@ -202,7 +231,7 @@ class FfplayOutput(QueuedOutput):
if self.started:
return
- super().start()
+ await super().start()
self.subprocess = await asyncio.create_subprocess_shell(
'ffplay -acodec aac pipe:0',
@@ -225,7 +254,7 @@ class FfplayOutput(QueuedOutput):
async def read_stream(name, stream):
while True:
data = await stream.read()
- print(f'{name}:', data)
+ logger.debug(f'{name}:', data)
await asyncio.wait(
[
@@ -238,13 +267,13 @@ class FfplayOutput(QueuedOutput):
asyncio.create_task(self.subprocess.wait()),
]
)
- print("FFPLAY done")
+ logger.debug("FFPLAY done")
async def on_audio_packet(self, packet):
try:
self.subprocess.stdin.write(packet)
except Exception:
- print('!!!! exception while sending audio to ffplay pipe')
+ logger.warning('!!!! exception while sending audio to ffplay pipe')
# -----------------------------------------------------------------------------
@@ -307,13 +336,15 @@ class UiServer:
self.channel_socket = ws
async for message in ws:
if message.type == aiohttp.WSMsgType.TEXT:
- print(f'<<< received message: {message.data}')
+ logger.debug(f'<<< received message: {message.data}')
await self.on_message(message.data)
elif message.type == aiohttp.WSMsgType.ERROR:
- print(f'channel connection closed with exception {ws.exception()}')
+ logger.debug(
+ f'channel connection closed with exception {ws.exception()}'
+ )
self.channel_socket = None
- print('--- channel connection closed')
+ logger.debug('--- channel connection closed')
return ws
@@ -329,10 +360,16 @@ class UiServer:
await handler(**message_params)
async def on_hello_message(self):
- print('HELLO')
+ logger.debug('HELLO')
await self.send_message(
'hello', bumble_version=bumble.__version__, codec=self.speaker().codec
)
+ if connection := self.speaker().connection:
+ await self.send_message(
+ 'connection',
+ peer_address=str(connection.peer_address).replace('/P', ''),
+ peer_name=connection.peer_name,
+ )
async def send_message(self, message_type: str, **kwargs) -> None:
if self.channel_socket is None:
@@ -345,7 +382,10 @@ class UiServer:
if self.channel_socket is None:
return
- await self.channel_socket.send_bytes(data)
+ try:
+ await self.channel_socket.send_bytes(data)
+ except Exception as error:
+ logger.warning(f'exception while sending audio packet: {error}')
# -----------------------------------------------------------------------------
@@ -356,6 +396,7 @@ class Speaker:
self.discover = discover
self.ui_port = ui_port
self.device = None
+ self.connection = None
self.listener = None
self.packets_received = 0
self.bytes_received = 0
@@ -424,16 +465,28 @@ class Speaker:
),
)
+ async def dispatch_to_outputs(self, function):
+ for output in self.outputs:
+ await function(output)
+
def on_bluetooth_connection(self, connection):
- print(f"Connection: {connection}")
+ print(f'Connection: {connection}')
+ self.connection = connection
connection.on('disconnection', self.on_bluetooth_disconnection)
+ AsyncRunner.spawn(
+ self.dispatch_to_outputs(lambda output: output.on_connection(connection))
+ )
def on_bluetooth_disconnection(self, reason):
- print(f"Disconnection ({reason})")
+ print(f'Disconnection ({reason})')
+ self.connection = None
AsyncRunner.spawn(self.advertise())
+ AsyncRunner.spawn(
+ self.dispatch_to_outputs(lambda output: output.on_disconnection(reason))
+ )
def on_avdtp_connection(self, protocol):
- print("Audio Stream Open")
+ print('Audio Stream Open')
# Add a sink endpoint to the server
sink = protocol.add_sink(self.codec_capabilities())
@@ -456,19 +509,16 @@ class Speaker:
print("Audio Stream Closed")
def on_sink_start(self):
- print("Sink Start")
- for output in self.outputs:
- AsyncRunner.spawn(output.start())
+ print("Sink Started\u001b[0K")
+ AsyncRunner.spawn(self.dispatch_to_outputs(lambda output: output.start()))
def on_sink_stop(self):
- print("Sink Stop")
- for output in self.outputs:
- AsyncRunner.spawn(output.stop())
+ print("Sink Stopped\u001b[0K")
+ AsyncRunner.spawn(self.dispatch_to_outputs(lambda output: output.stop()))
def on_sink_suspend(self):
- print("Sink Suspend")
- for output in self.outputs:
- AsyncRunner.spawn(output.suspend())
+ print("Sink Suspended\u001b[0K")
+ AsyncRunner.spawn(self.dispatch_to_outputs(lambda output: output.suspend()))
def on_sink_configuration(self, config):
print("Sink Configuration:")
@@ -625,6 +675,16 @@ def play(ctx, transport, codec, connect_address, discover, output, ui_port):
)
output = list(filter(lambda x: x != '@ffplay', output))
+ if '@ffplay' in output:
+ # Check if ffplay is installed
+ try:
+ subprocess.run(['ffplay', '-version'], capture_output=True, check=True)
+ except FileNotFoundError:
+ print(
+ color('ffplay not installed, @ffplay output will be disabled', 'yellow')
+ )
+ output = list(filter(lambda x: x != '@ffplay', output))
+
asyncio.run(
Speaker(transport, codec, discover, output, ui_port).run(connect_address)
)
@@ -632,7 +692,7 @@ def play(ctx, transport, codec, connect_address, discover, output, ui_port):
# -----------------------------------------------------------------------------
def main():
- logging.basicConfig(level=os.environ.get('BUMBLE_LOGLEVEL', 'INFO').upper())
+ logging.basicConfig(level=os.environ.get('BUMBLE_LOGLEVEL', 'WARNING').upper())
speaker_cli()
diff --git a/bumble/host.py b/bumble/host.py
index afde2ee6..a33efc80 100644
--- a/bumble/host.py
+++ b/bumble/host.py
@@ -62,6 +62,7 @@ from .hci import (
HCI_Read_Local_Version_Information_Command,
HCI_Reset_Command,
HCI_Set_Event_Mask_Command,
+ map_null_terminated_utf8_string,
)
from .core import (
BT_BR_EDR_TRANSPORT,
@@ -887,7 +888,12 @@ class Host(AbortableEventEmitter):
if event.status != HCI_SUCCESS:
self.emit('remote_name_failure', event.bd_addr, event.status)
else:
- self.emit('remote_name', event.bd_addr, event.remote_name)
+ utf8_name = event.remote_name
+ terminator = utf8_name.find(0)
+ if terminator >= 0:
+ utf8_name = utf8_name[0:terminator]
+
+ self.emit('remote_name', event.bd_addr, utf8_name)
def on_hci_remote_host_supported_features_notification_event(self, event):
self.emit(