This commit is contained in:
Gilles Boccon-Gibod
2023-05-15 14:29:58 -07:00
parent 7b7ef85b14
commit 55a01033a0
5 changed files with 148 additions and 58 deletions

View File

@@ -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) {

View File

@@ -22,6 +22,7 @@
</td>
</tr></table>
<span id="streamStateText">IDLE</span>
<span id="connectionStateText">NOT CONNECTED</span>
<div id="controlsDiv">
<button id="audioOnButton">Audio On</button>
<span id="audioSupportMessageText"></span>

View File

@@ -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,6 +98,7 @@ function initAnalyzer() {
function startAnalyzer() {
// FFT
if (audioElement.captureStream !== undefined) {
audioContext = new AudioContext();
audioAnalyzer = audioContext.createAnalyser();
audioAnalyzer.fftSize = 128;
@@ -102,12 +107,11 @@ function startAnalyzer() {
const stream = audioElement.captureStream();
const source = audioContext.createMediaStreamSource(stream);
source.connect(audioAnalyzer);
}
// Bandwidth
bandwidthBinCount = BANDWIDTH_WIDTH / 2;
bandwidthBins = [];
requestAnimationFrame(onAnimationFrame);
}
function setConnectionText(message) {
@@ -121,6 +125,7 @@ function setConnectionText(message) {
function onAnimationFrame() {
// FFT
if (audioAnalyzer !== undefined) {
audioAnalyzer.getByteFrequencyData(audioFrequencyData);
fftCanvasContext.fillStyle = "rgb(0, 0, 0)";
fftCanvasContext.fillRect(0, 0, FFT_WIDTH, FFT_HEIGHT);
@@ -131,6 +136,7 @@ function onAnimationFrame() {
fftCanvasContext.fillStyle = `rgb(${barHeight / 256 * 200 + 50}, 50, ${50 + 2 * bar})`;
fftCanvasContext.fillRect(bar * (barWidth + 1), FFT_HEIGHT - barHeight, barWidth, barHeight);
}
}
// Bandwidth
bandwidthCanvasContext.fillStyle = "rgb(255, 255, 255)";
@@ -175,13 +181,10 @@ 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);
}
packetsReceived += 1;
packetsReceivedText.innerText = packetsReceived;
@@ -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) => {

View File

@@ -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
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()

View File

@@ -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(