Merge branch 'main' of ssh://ssh.pstruebi.xyz:222/auracaster/bumble-auracast
This commit is contained in:
5
.gitignore
vendored
5
.gitignore
vendored
@@ -35,10 +35,11 @@ env/
|
|||||||
__pycache__/
|
__pycache__/
|
||||||
|
|
||||||
# Exclude .env file from all platforms
|
# Exclude .env file from all platforms
|
||||||
*/.env
|
*.env
|
||||||
|
|
||||||
wg_config/wg_confs/
|
wg_config/wg_confs/
|
||||||
records/
|
records/DISABLE_FRONTEND_PW
|
||||||
src/auracast/server/stream_settings.json
|
src/auracast/server/stream_settings.json
|
||||||
src/auracast/server/certs/per_device/
|
src/auracast/server/certs/per_device/
|
||||||
src/auracast/.env
|
src/auracast/.env
|
||||||
|
src/auracast/server/certs/ca/ca_cert.srl
|
||||||
|
|||||||
18
README.md
18
README.md
@@ -61,6 +61,22 @@ sudo ./provision_domain_hostname.sh <new_hostname> <new_domain>
|
|||||||
- If you have issues with mDNS name resolution, check for conflicting mDNS stacks (e.g., systemd-resolved, Bonjour, or other daemons).
|
- If you have issues with mDNS name resolution, check for conflicting mDNS stacks (e.g., systemd-resolved, Bonjour, or other daemons).
|
||||||
- Some Linux clients may not resolve multi-label mDNS names via NSS—test with `avahi-resolve-host-name` and try from another device if needed.
|
- Some Linux clients may not resolve multi-label mDNS names via NSS—test with `avahi-resolve-host-name` and try from another device if needed.
|
||||||
|
|
||||||
|
# record audio and save to file for debugging
|
||||||
|
pw-record --target="AVIOUSB-8f6326 : 2:receive_Left" --rate=48000 --channels=1 --format=s24 /tmp/aes67_test.wav &
|
||||||
|
RECORD_PID=$!
|
||||||
|
sleep 30
|
||||||
|
kill $RECORD_PID
|
||||||
|
|
||||||
|
# uart reset over hci does not work:
|
||||||
|
stty -F /dev/ttyAMA3 -hupcl
|
||||||
|
stty -F /dev/ttyAMA3 -a | grep -o 'hupcl' || echo "-hupcl is set"
|
||||||
|
|
||||||
|
|
||||||
|
# Audio latency
|
||||||
|
if there is hearable audio error with aes67, tune sess.latency.msec in pipewire-aes67.conf
|
||||||
|
|
||||||
|
if latency is piling up something may be blocking the event loop in multicast_server.py - the event loop must never block at any time
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
After completing these steps, your device will be discoverable as `<hostname>.<domain>` (e.g., `box1.auracast.local`) on the local network via mDNS.
|
After completing these steps, your device will be discoverable as `<hostname>.<domain>` (e.g., `box1.auracast.local`) on the local network via mDNS.
|
||||||
@@ -199,7 +215,7 @@ sudo ldconfig # refresh linker cache
|
|||||||
- sudo modprobe i2c-dev
|
- sudo modprobe i2c-dev
|
||||||
- echo i2c_bcm2835 | sudo tee -a /etc/modules
|
- echo i2c_bcm2835 | sudo tee -a /etc/modules
|
||||||
- echo i2c-dev | sudo tee -a /etc/modules
|
- echo i2c-dev | sudo tee -a /etc/modules
|
||||||
- read temp /src/scripts/temp
|
- read temp /src/scripts/temp
|
||||||
|
|
||||||
# Known issues:
|
# Known issues:
|
||||||
- When running on a laptop there might be issues switching between usb and browser audio input since they use the same audio device
|
- When running on a laptop there might be issues switching between usb and browser audio input since they use the same audio device
|
||||||
|
|||||||
108
poetry.lock
generated
108
poetry.lock
generated
@@ -332,10 +332,10 @@ files = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bumble"
|
name = "bumble"
|
||||||
version = "0.0.209.dev2+g12bcdb7"
|
version = "0.0.218.dev6+g32d448edf"
|
||||||
description = "Bluetooth Stack for Apps, Emulation, Test and Experimentation"
|
description = "Bluetooth Stack for Apps, Emulation, Test and Experimentation"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.8"
|
python-versions = ">=3.9"
|
||||||
groups = ["main"]
|
groups = ["main"]
|
||||||
files = []
|
files = []
|
||||||
develop = false
|
develop = false
|
||||||
@@ -344,7 +344,7 @@ develop = false
|
|||||||
aiohttp = {version = ">=3.8,<4.0", markers = "platform_system != \"Emscripten\""}
|
aiohttp = {version = ">=3.8,<4.0", markers = "platform_system != \"Emscripten\""}
|
||||||
appdirs = {version = ">=1.4", markers = "platform_system != \"Emscripten\""}
|
appdirs = {version = ">=1.4", markers = "platform_system != \"Emscripten\""}
|
||||||
click = {version = ">=8.1.3", markers = "platform_system != \"Emscripten\""}
|
click = {version = ">=8.1.3", markers = "platform_system != \"Emscripten\""}
|
||||||
cryptography = ">=39"
|
cryptography = ">=44.0.3"
|
||||||
grpcio = {version = ">=1.62.1", markers = "platform_system != \"Emscripten\""}
|
grpcio = {version = ">=1.62.1", markers = "platform_system != \"Emscripten\""}
|
||||||
humanize = {version = ">=4.6.0", markers = "platform_system != \"Emscripten\""}
|
humanize = {version = ">=4.6.0", markers = "platform_system != \"Emscripten\""}
|
||||||
libusb-package = {version = "1.0.26.1", markers = "platform_system != \"Emscripten\""}
|
libusb-package = {version = "1.0.26.1", markers = "platform_system != \"Emscripten\""}
|
||||||
@@ -353,7 +353,7 @@ platformdirs = {version = ">=3.10.0", markers = "platform_system != \"Emscripten
|
|||||||
prettytable = {version = ">=3.6.0", markers = "platform_system != \"Emscripten\""}
|
prettytable = {version = ">=3.6.0", markers = "platform_system != \"Emscripten\""}
|
||||||
prompt_toolkit = {version = ">=3.0.16", markers = "platform_system != \"Emscripten\""}
|
prompt_toolkit = {version = ">=3.0.16", markers = "platform_system != \"Emscripten\""}
|
||||||
protobuf = {version = ">=3.12.4", markers = "platform_system != \"Emscripten\""}
|
protobuf = {version = ">=3.12.4", markers = "platform_system != \"Emscripten\""}
|
||||||
pyee = ">=8.2.2"
|
pyee = ">=13.0.0"
|
||||||
pyserial = {version = ">=3.5", markers = "platform_system != \"Emscripten\""}
|
pyserial = {version = ">=3.5", markers = "platform_system != \"Emscripten\""}
|
||||||
pyserial-asyncio = {version = ">=0.5", markers = "platform_system != \"Emscripten\""}
|
pyserial-asyncio = {version = ">=0.5", markers = "platform_system != \"Emscripten\""}
|
||||||
pyusb = {version = ">=1.2", markers = "platform_system != \"Emscripten\""}
|
pyusb = {version = ">=1.2", markers = "platform_system != \"Emscripten\""}
|
||||||
@@ -363,7 +363,7 @@ websockets = {version = "13.1", markers = "platform_system != \"Emscripten\""}
|
|||||||
auracast = ["lc3py (>=1.1.3) ; python_version >= \"3.10\" and (platform_system == \"Linux\" and platform_machine == \"x86_64\" or platform_system == \"Darwin\" and platform_machine == \"arm64\")", "sounddevice (>=0.5.1)"]
|
auracast = ["lc3py (>=1.1.3) ; python_version >= \"3.10\" and (platform_system == \"Linux\" and platform_machine == \"x86_64\" or platform_system == \"Darwin\" and platform_machine == \"arm64\")", "sounddevice (>=0.5.1)"]
|
||||||
avatar = ["pandora-avatar (==0.0.10)", "rootcanal (==1.11.1) ; python_version >= \"3.10\""]
|
avatar = ["pandora-avatar (==0.0.10)", "rootcanal (==1.11.1) ; python_version >= \"3.10\""]
|
||||||
build = ["build (>=0.7)"]
|
build = ["build (>=0.7)"]
|
||||||
development = ["black (==24.3)", "bt-test-interfaces (>=0.0.6)", "grpcio-tools (>=1.62.1)", "invoke (>=1.7.3)", "mobly (>=1.12.2)", "mypy (==1.12.0)", "nox (>=2022)", "pylint (==3.3.1)", "pyyaml (>=6.0)", "types-appdirs (>=1.4.3)", "types-invoke (>=1.7.3)", "types-protobuf (>=4.21.0)"]
|
development = ["black (>=25.1,<26.0)", "bt-test-interfaces (>=0.0.6)", "grpcio-tools (>=1.62.1)", "invoke (>=1.7.3)", "isort (>=5.13.2,<5.14.0)", "mobly (>=1.12.2)", "mypy (==1.12.0)", "nox (>=2022)", "pylint (==3.3.1)", "pyyaml (>=6.0)", "types-appdirs (>=1.4.3)", "types-invoke (>=1.7.3)", "types-protobuf (>=4.21.0)"]
|
||||||
documentation = ["mkdocs (>=1.6.0)", "mkdocs-material (>=9.6)", "mkdocstrings[python] (>=0.27.0)"]
|
documentation = ["mkdocs (>=1.6.0)", "mkdocs-material (>=9.6)", "mkdocstrings[python] (>=0.27.0)"]
|
||||||
pandora = ["bt-test-interfaces (>=0.0.6)"]
|
pandora = ["bt-test-interfaces (>=0.0.6)"]
|
||||||
test = ["coverage (>=6.4)", "pytest (>=8.2)", "pytest-asyncio (>=0.23.5)", "pytest-html (>=3.2.0)"]
|
test = ["coverage (>=6.4)", "pytest (>=8.2)", "pytest-asyncio (>=0.23.5)", "pytest-html (>=3.2.0)"]
|
||||||
@@ -371,8 +371,8 @@ test = ["coverage (>=6.4)", "pytest (>=8.2)", "pytest-asyncio (>=0.23.5)", "pyte
|
|||||||
[package.source]
|
[package.source]
|
||||||
type = "git"
|
type = "git"
|
||||||
url = "ssh://git@ssh.pstruebi.xyz:222/auracaster/bumble_mirror.git"
|
url = "ssh://git@ssh.pstruebi.xyz:222/auracaster/bumble_mirror.git"
|
||||||
reference = "12bcdb7770c0d57a094bc0a96cd52e701f97fece"
|
reference = "32d448edf3276f6b9056765a12879054d8a01fd8"
|
||||||
resolved_reference = "12bcdb7770c0d57a094bc0a96cd52e701f97fece"
|
resolved_reference = "32d448edf3276f6b9056765a12879054d8a01fd8"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cachetools"
|
name = "cachetools"
|
||||||
@@ -610,60 +610,62 @@ files = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cryptography"
|
name = "cryptography"
|
||||||
version = "44.0.2"
|
version = "45.0.7"
|
||||||
description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers."
|
description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers."
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = "!=3.9.0,!=3.9.1,>=3.7"
|
python-versions = "!=3.9.0,!=3.9.1,>=3.7"
|
||||||
groups = ["main"]
|
groups = ["main"]
|
||||||
files = [
|
files = [
|
||||||
{file = "cryptography-44.0.2-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:efcfe97d1b3c79e486554efddeb8f6f53a4cdd4cf6086642784fa31fc384e1d7"},
|
{file = "cryptography-45.0.7-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:3be4f21c6245930688bd9e162829480de027f8bf962ede33d4f8ba7d67a00cee"},
|
||||||
{file = "cryptography-44.0.2-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:29ecec49f3ba3f3849362854b7253a9f59799e3763b0c9d0826259a88efa02f1"},
|
{file = "cryptography-45.0.7-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:67285f8a611b0ebc0857ced2081e30302909f571a46bfa7a3cc0ad303fe015c6"},
|
||||||
{file = "cryptography-44.0.2-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc821e161ae88bfe8088d11bb39caf2916562e0a2dc7b6d56714a48b784ef0bb"},
|
{file = "cryptography-45.0.7-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:577470e39e60a6cd7780793202e63536026d9b8641de011ed9d8174da9ca5339"},
|
||||||
{file = "cryptography-44.0.2-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:3c00b6b757b32ce0f62c574b78b939afab9eecaf597c4d624caca4f9e71e7843"},
|
{file = "cryptography-45.0.7-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:4bd3e5c4b9682bc112d634f2c6ccc6736ed3635fc3319ac2bb11d768cc5a00d8"},
|
||||||
{file = "cryptography-44.0.2-cp37-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:7bdcd82189759aba3816d1f729ce42ffded1ac304c151d0a8e89b9996ab863d5"},
|
{file = "cryptography-45.0.7-cp311-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:465ccac9d70115cd4de7186e60cfe989de73f7bb23e8a7aa45af18f7412e75bf"},
|
||||||
{file = "cryptography-44.0.2-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:4973da6ca3db4405c54cd0b26d328be54c7747e89e284fcff166132eb7bccc9c"},
|
{file = "cryptography-45.0.7-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:16ede8a4f7929b4b7ff3642eba2bf79aa1d71f24ab6ee443935c0d269b6bc513"},
|
||||||
{file = "cryptography-44.0.2-cp37-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:4e389622b6927d8133f314949a9812972711a111d577a5d1f4bee5e58736b80a"},
|
{file = "cryptography-45.0.7-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:8978132287a9d3ad6b54fcd1e08548033cc09dc6aacacb6c004c73c3eb5d3ac3"},
|
||||||
{file = "cryptography-44.0.2-cp37-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:f514ef4cd14bb6fb484b4a60203e912cfcb64f2ab139e88c2274511514bf7308"},
|
{file = "cryptography-45.0.7-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:b6a0e535baec27b528cb07a119f321ac024592388c5681a5ced167ae98e9fff3"},
|
||||||
{file = "cryptography-44.0.2-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:1bc312dfb7a6e5d66082c87c34c8a62176e684b6fe3d90fcfe1568de675e6688"},
|
{file = "cryptography-45.0.7-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:a24ee598d10befaec178efdff6054bc4d7e883f615bfbcd08126a0f4931c83a6"},
|
||||||
{file = "cryptography-44.0.2-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:3b721b8b4d948b218c88cb8c45a01793483821e709afe5f622861fc6182b20a7"},
|
{file = "cryptography-45.0.7-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:fa26fa54c0a9384c27fcdc905a2fb7d60ac6e47d14bc2692145f2b3b1e2cfdbd"},
|
||||||
{file = "cryptography-44.0.2-cp37-abi3-win32.whl", hash = "sha256:51e4de3af4ec3899d6d178a8c005226491c27c4ba84101bfb59c901e10ca9f79"},
|
{file = "cryptography-45.0.7-cp311-abi3-win32.whl", hash = "sha256:bef32a5e327bd8e5af915d3416ffefdbe65ed975b646b3805be81b23580b57b8"},
|
||||||
{file = "cryptography-44.0.2-cp37-abi3-win_amd64.whl", hash = "sha256:c505d61b6176aaf982c5717ce04e87da5abc9a36a5b39ac03905c4aafe8de7aa"},
|
{file = "cryptography-45.0.7-cp311-abi3-win_amd64.whl", hash = "sha256:3808e6b2e5f0b46d981c24d79648e5c25c35e59902ea4391a0dcb3e667bf7443"},
|
||||||
{file = "cryptography-44.0.2-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:8e0ddd63e6bf1161800592c71ac794d3fb8001f2caebe0966e77c5234fa9efc3"},
|
{file = "cryptography-45.0.7-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:bfb4c801f65dd61cedfc61a83732327fafbac55a47282e6f26f073ca7a41c3b2"},
|
||||||
{file = "cryptography-44.0.2-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:81276f0ea79a208d961c433a947029e1a15948966658cf6710bbabb60fcc2639"},
|
{file = "cryptography-45.0.7-cp37-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:81823935e2f8d476707e85a78a405953a03ef7b7b4f55f93f7c2d9680e5e0691"},
|
||||||
{file = "cryptography-44.0.2-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9a1e657c0f4ea2a23304ee3f964db058c9e9e635cc7019c4aa21c330755ef6fd"},
|
{file = "cryptography-45.0.7-cp37-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3994c809c17fc570c2af12c9b840d7cea85a9fd3e5c0e0491f4fa3c029216d59"},
|
||||||
{file = "cryptography-44.0.2-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:6210c05941994290f3f7f175a4a57dbbb2afd9273657614c506d5976db061181"},
|
{file = "cryptography-45.0.7-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:dad43797959a74103cb59c5dac71409f9c27d34c8a05921341fb64ea8ccb1dd4"},
|
||||||
{file = "cryptography-44.0.2-cp39-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:d1c3572526997b36f245a96a2b1713bf79ce99b271bbcf084beb6b9b075f29ea"},
|
{file = "cryptography-45.0.7-cp37-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:ce7a453385e4c4693985b4a4a3533e041558851eae061a58a5405363b098fcd3"},
|
||||||
{file = "cryptography-44.0.2-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:b042d2a275c8cee83a4b7ae30c45a15e6a4baa65a179a0ec2d78ebb90e4f6699"},
|
{file = "cryptography-45.0.7-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:b04f85ac3a90c227b6e5890acb0edbaf3140938dbecf07bff618bf3638578cf1"},
|
||||||
{file = "cryptography-44.0.2-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:d03806036b4f89e3b13b6218fefea8d5312e450935b1a2d55f0524e2ed7c59d9"},
|
{file = "cryptography-45.0.7-cp37-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:48c41a44ef8b8c2e80ca4527ee81daa4c527df3ecbc9423c41a420a9559d0e27"},
|
||||||
{file = "cryptography-44.0.2-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:c7362add18b416b69d58c910caa217f980c5ef39b23a38a0880dfd87bdf8cd23"},
|
{file = "cryptography-45.0.7-cp37-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:f3df7b3d0f91b88b2106031fd995802a2e9ae13e02c36c1fc075b43f420f3a17"},
|
||||||
{file = "cryptography-44.0.2-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:8cadc6e3b5a1f144a039ea08a0bdb03a2a92e19c46be3285123d32029f40a922"},
|
{file = "cryptography-45.0.7-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:dd342f085542f6eb894ca00ef70236ea46070c8a13824c6bde0dfdcd36065b9b"},
|
||||||
{file = "cryptography-44.0.2-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:6f101b1f780f7fc613d040ca4bdf835c6ef3b00e9bd7125a4255ec574c7916e4"},
|
{file = "cryptography-45.0.7-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:1993a1bb7e4eccfb922b6cd414f072e08ff5816702a0bdb8941c247a6b1b287c"},
|
||||||
{file = "cryptography-44.0.2-cp39-abi3-win32.whl", hash = "sha256:3dc62975e31617badc19a906481deacdeb80b4bb454394b4098e3f2525a488c5"},
|
{file = "cryptography-45.0.7-cp37-abi3-win32.whl", hash = "sha256:18fcf70f243fe07252dcb1b268a687f2358025ce32f9f88028ca5c364b123ef5"},
|
||||||
{file = "cryptography-44.0.2-cp39-abi3-win_amd64.whl", hash = "sha256:5f6f90b72d8ccadb9c6e311c775c8305381db88374c65fa1a68250aa8a9cb3a6"},
|
{file = "cryptography-45.0.7-cp37-abi3-win_amd64.whl", hash = "sha256:7285a89df4900ed3bfaad5679b1e668cb4b38a8de1ccbfc84b05f34512da0a90"},
|
||||||
{file = "cryptography-44.0.2-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:af4ff3e388f2fa7bff9f7f2b31b87d5651c45731d3e8cfa0944be43dff5cfbdb"},
|
{file = "cryptography-45.0.7-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:de58755d723e86175756f463f2f0bddd45cc36fbd62601228a3f8761c9f58252"},
|
||||||
{file = "cryptography-44.0.2-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:0529b1d5a0105dd3731fa65680b45ce49da4d8115ea76e9da77a875396727b41"},
|
{file = "cryptography-45.0.7-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:a20e442e917889d1a6b3c570c9e3fa2fdc398c20868abcea268ea33c024c4083"},
|
||||||
{file = "cryptography-44.0.2-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:7ca25849404be2f8e4b3c59483d9d3c51298a22c1c61a0e84415104dacaf5562"},
|
{file = "cryptography-45.0.7-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:258e0dff86d1d891169b5af222d362468a9570e2532923088658aa866eb11130"},
|
||||||
{file = "cryptography-44.0.2-pp310-pypy310_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:268e4e9b177c76d569e8a145a6939eca9a5fec658c932348598818acf31ae9a5"},
|
{file = "cryptography-45.0.7-pp310-pypy310_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:d97cf502abe2ab9eff8bd5e4aca274da8d06dd3ef08b759a8d6143f4ad65d4b4"},
|
||||||
{file = "cryptography-44.0.2-pp310-pypy310_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:9eb9d22b0a5d8fd9925a7764a054dca914000607dff201a24c791ff5c799e1fa"},
|
{file = "cryptography-45.0.7-pp310-pypy310_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:c987dad82e8c65ebc985f5dae5e74a3beda9d0a2a4daf8a1115f3772b59e5141"},
|
||||||
{file = "cryptography-44.0.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:2bf7bf75f7df9715f810d1b038870309342bff3069c5bd8c6b96128cb158668d"},
|
{file = "cryptography-45.0.7-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:c13b1e3afd29a5b3b2656257f14669ca8fa8d7956d509926f0b130b600b50ab7"},
|
||||||
{file = "cryptography-44.0.2-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:909c97ab43a9c0c0b0ada7a1281430e4e5ec0458e6d9244c0e821bbf152f061d"},
|
{file = "cryptography-45.0.7-pp311-pypy311_pp73-macosx_10_9_x86_64.whl", hash = "sha256:4a862753b36620af6fc54209264f92c716367f2f0ff4624952276a6bbd18cbde"},
|
||||||
{file = "cryptography-44.0.2-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:96e7a5e9d6e71f9f4fca8eebfd603f8e86c5225bb18eb621b2c1e50b290a9471"},
|
{file = "cryptography-45.0.7-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:06ce84dc14df0bf6ea84666f958e6080cdb6fe1231be2a51f3fc1267d9f3fb34"},
|
||||||
{file = "cryptography-44.0.2-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:d1b3031093a366ac767b3feb8bcddb596671b3aaff82d4050f984da0c248b615"},
|
{file = "cryptography-45.0.7-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:d0c5c6bac22b177bf8da7435d9d27a6834ee130309749d162b26c3105c0795a9"},
|
||||||
{file = "cryptography-44.0.2-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:04abd71114848aa25edb28e225ab5f268096f44cf0127f3d36975bdf1bdf3390"},
|
{file = "cryptography-45.0.7-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:2f641b64acc00811da98df63df7d59fd4706c0df449da71cb7ac39a0732b40ae"},
|
||||||
{file = "cryptography-44.0.2.tar.gz", hash = "sha256:c63454aa261a0cf0c5b4718349629793e9e634993538db841165b3df74f37ec0"},
|
{file = "cryptography-45.0.7-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:f5414a788ecc6ee6bc58560e85ca624258a55ca434884445440a810796ea0e0b"},
|
||||||
|
{file = "cryptography-45.0.7-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:1f3d56f73595376f4244646dd5c5870c14c196949807be39e79e7bd9bac3da63"},
|
||||||
|
{file = "cryptography-45.0.7.tar.gz", hash = "sha256:4b1654dfc64ea479c242508eb8c724044f1e964a47d1d1cacc5132292d851971"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[package.dependencies]
|
[package.dependencies]
|
||||||
cffi = {version = ">=1.12", markers = "platform_python_implementation != \"PyPy\""}
|
cffi = {version = ">=1.14", markers = "platform_python_implementation != \"PyPy\""}
|
||||||
|
|
||||||
[package.extras]
|
[package.extras]
|
||||||
docs = ["sphinx (>=5.3.0)", "sphinx-rtd-theme (>=3.0.0) ; python_version >= \"3.8\""]
|
docs = ["sphinx (>=5.3.0)", "sphinx-inline-tabs ; python_full_version >= \"3.8.0\"", "sphinx-rtd-theme (>=3.0.0) ; python_full_version >= \"3.8.0\""]
|
||||||
docstest = ["pyenchant (>=3)", "readme-renderer (>=30.0)", "sphinxcontrib-spelling (>=7.3.1)"]
|
docstest = ["pyenchant (>=3)", "readme-renderer (>=30.0)", "sphinxcontrib-spelling (>=7.3.1)"]
|
||||||
nox = ["nox (>=2024.4.15)", "nox[uv] (>=2024.3.2) ; python_version >= \"3.8\""]
|
nox = ["nox (>=2024.4.15)", "nox[uv] (>=2024.3.2) ; python_full_version >= \"3.8.0\""]
|
||||||
pep8test = ["check-sdist ; python_version >= \"3.8\"", "click (>=8.0.1)", "mypy (>=1.4)", "ruff (>=0.3.6)"]
|
pep8test = ["check-sdist ; python_full_version >= \"3.8.0\"", "click (>=8.0.1)", "mypy (>=1.4)", "ruff (>=0.3.6)"]
|
||||||
sdist = ["build (>=1.0.0)"]
|
sdist = ["build (>=1.0.0)"]
|
||||||
ssh = ["bcrypt (>=3.1.5)"]
|
ssh = ["bcrypt (>=3.1.5)"]
|
||||||
test = ["certifi (>=2024)", "cryptography-vectors (==44.0.2)", "pretend (>=0.7)", "pytest (>=7.4.0)", "pytest-benchmark (>=4.0)", "pytest-cov (>=2.10.1)", "pytest-xdist (>=3.5.0)"]
|
test = ["certifi (>=2024)", "cryptography-vectors (==45.0.7)", "pretend (>=0.7)", "pytest (>=7.4.0)", "pytest-benchmark (>=4.0)", "pytest-cov (>=2.10.1)", "pytest-xdist (>=3.5.0)"]
|
||||||
test-randomorder = ["pytest-randomly"]
|
test-randomorder = ["pytest-randomly"]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -1080,8 +1082,8 @@ files = [
|
|||||||
referencing = ">=0.31.0"
|
referencing = ">=0.31.0"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "lc3"
|
name = "lc3py"
|
||||||
version = "0.0.1"
|
version = "1.1.3"
|
||||||
description = "LC3 Codec library wrapper"
|
description = "LC3 Codec library wrapper"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.10"
|
python-versions = ">=3.10"
|
||||||
@@ -1095,8 +1097,8 @@ dev = ["pytest"]
|
|||||||
[package.source]
|
[package.source]
|
||||||
type = "git"
|
type = "git"
|
||||||
url = "ssh://git@ssh.pstruebi.xyz:222/auracaster/liblc3.git"
|
url = "ssh://git@ssh.pstruebi.xyz:222/auracaster/liblc3.git"
|
||||||
reference = "7558637303106c7ea971e7bb8cedf379d3e08bcc"
|
reference = "ce2e41faf8c06d038df9f32504c61109a14130be"
|
||||||
resolved_reference = "7558637303106c7ea971e7bb8cedf379d3e08bcc"
|
resolved_reference = "ce2e41faf8c06d038df9f32504c61109a14130be"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "libusb-package"
|
name = "libusb-package"
|
||||||
@@ -2950,4 +2952,4 @@ test = ["pytest", "pytest-asyncio"]
|
|||||||
[metadata]
|
[metadata]
|
||||||
lock-version = "2.1"
|
lock-version = "2.1"
|
||||||
python-versions = ">=3.11"
|
python-versions = ">=3.11"
|
||||||
content-hash = "9fe0e4746a6fca45e5aa9117ca177a5587c3a7b83cacb9427bdb960c4f0c7036"
|
content-hash = "6b5300c349ed045e8fd3e617e6262bbd7e5c48c518e4c62cedf7c17da50ce8c0"
|
||||||
|
|||||||
@@ -4,8 +4,8 @@ version = "0.0.1"
|
|||||||
requires-python = ">=3.11"
|
requires-python = ">=3.11"
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bumble @ git+ssh://git@ssh.pstruebi.xyz:222/auracaster/bumble_mirror.git@12bcdb7770c0d57a094bc0a96cd52e701f97fece",
|
"bumble @ git+ssh://git@ssh.pstruebi.xyz:222/auracaster/bumble_mirror.git@32d448edf3276f6b9056765a12879054d8a01fd8",
|
||||||
"lc3 @ git+ssh://git@ssh.pstruebi.xyz:222/auracaster/liblc3.git@7558637303106c7ea971e7bb8cedf379d3e08bcc",
|
"lc3py @ git+ssh://git@ssh.pstruebi.xyz:222/auracaster/liblc3.git@ce2e41faf8c06d038df9f32504c61109a14130be",
|
||||||
"aioconsole",
|
"aioconsole",
|
||||||
"fastapi==0.115.11",
|
"fastapi==0.115.11",
|
||||||
"uvicorn==0.34.0",
|
"uvicorn==0.34.0",
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ class AuracastGlobalConfig(BaseModel):
|
|||||||
# When true, include a zero-length LTV with type 0x09 in the subgroup metadata
|
# When true, include a zero-length LTV with type 0x09 in the subgroup metadata
|
||||||
# so receivers may render earlier than the presentation delay for lower latency.
|
# so receivers may render earlier than the presentation delay for lower latency.
|
||||||
immediate_rendering: bool = False
|
immediate_rendering: bool = False
|
||||||
|
assisted_listening_stream: bool = False
|
||||||
|
|
||||||
# "Audio input. "
|
# "Audio input. "
|
||||||
# "'device' -> use the host's default sound input device, "
|
# "'device' -> use the host's default sound input device, "
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ import bumble.device
|
|||||||
import bumble.transport
|
import bumble.transport
|
||||||
import bumble.utils
|
import bumble.utils
|
||||||
import numpy as np # for audio down-mix
|
import numpy as np # for audio down-mix
|
||||||
from bumble.device import Host, BIGInfoAdvertisement, AdvertisingChannelMap
|
from bumble.device import Host, AdvertisingChannelMap
|
||||||
from bumble.audio import io as audio_io
|
from bumble.audio import io as audio_io
|
||||||
|
|
||||||
from auracast import auracast_config
|
from auracast import auracast_config
|
||||||
@@ -101,6 +101,24 @@ class ModWaveAudioInput(audio_io.ThreadedAudioInput):
|
|||||||
audio_io.WaveAudioInput = ModWaveAudioInput
|
audio_io.WaveAudioInput = ModWaveAudioInput
|
||||||
|
|
||||||
|
|
||||||
|
def broadcast_code_bytes(broadcast_code: str) -> bytes:
|
||||||
|
"""
|
||||||
|
Convert a broadcast code string to a 16-byte value.
|
||||||
|
|
||||||
|
If `broadcast_code` is `0x` followed by 32 hex characters, it is interpreted as a
|
||||||
|
raw 16-byte raw broadcast code in big-endian byte order.
|
||||||
|
Otherwise, `broadcast_code` is converted to a 16-byte value as specified in
|
||||||
|
BLUETOOTH CORE SPECIFICATION Version 6.0 | Vol 3, Part C , section 3.2.6.3
|
||||||
|
"""
|
||||||
|
if broadcast_code.startswith("0x") and len(broadcast_code) == 34:
|
||||||
|
return bytes.fromhex(broadcast_code[2:])[::-1]
|
||||||
|
|
||||||
|
broadcast_code_utf8 = broadcast_code.encode("utf-8")
|
||||||
|
if len(broadcast_code_utf8) > 16:
|
||||||
|
raise ValueError("broadcast code must be <= 16 bytes in utf-8 encoding")
|
||||||
|
padding = bytes(16 - len(broadcast_code_utf8))
|
||||||
|
return broadcast_code_utf8 + padding
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
# Logging
|
# Logging
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
@@ -170,7 +188,15 @@ async def init_broadcast(
|
|||||||
# Broadcast Audio Immediate Rendering flag (type 0x09), zero-length value
|
# Broadcast Audio Immediate Rendering flag (type 0x09), zero-length value
|
||||||
le_audio.Metadata.Entry(tag = le_audio.Metadata.Tag.BROADCAST_AUDIO_IMMEDIATE_RENDERING_FLAG, data=b"")
|
le_audio.Metadata.Entry(tag = le_audio.Metadata.Tag.BROADCAST_AUDIO_IMMEDIATE_RENDERING_FLAG, data=b"")
|
||||||
]
|
]
|
||||||
if global_config.immediate_rendering #TODO: verify this
|
if global_config.immediate_rendering
|
||||||
|
else []
|
||||||
|
)
|
||||||
|
+ (
|
||||||
|
[
|
||||||
|
# Assisted Listening Stream tag expects a 1-octet value. Use 0x01 to indicate enabled.
|
||||||
|
le_audio.Metadata.Entry(tag = le_audio.Metadata.Tag.ASSISTED_LISTENING_STREAM, data=b"\x01")
|
||||||
|
]
|
||||||
|
if global_config.assisted_listening_stream
|
||||||
else []
|
else []
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@@ -269,7 +295,7 @@ async def init_broadcast(
|
|||||||
max_transport_latency=global_config.qos_config.max_transport_latency_ms,
|
max_transport_latency=global_config.qos_config.max_transport_latency_ms,
|
||||||
rtn=global_config.qos_config.number_of_retransmissions,
|
rtn=global_config.qos_config.number_of_retransmissions,
|
||||||
broadcast_code=(
|
broadcast_code=(
|
||||||
bytes.fromhex(conf.code) if conf.code else None
|
broadcast_code_bytes(conf.code) if conf.code else None
|
||||||
),
|
),
|
||||||
framing=frame_enable # needed if iso interval is not frame interval of codedc
|
framing=frame_enable # needed if iso interval is not frame interval of codedc
|
||||||
),
|
),
|
||||||
@@ -287,8 +313,8 @@ async def init_broadcast(
|
|||||||
|
|
||||||
bigs[f'big{i}']['iso_queue'] = iso_queue
|
bigs[f'big{i}']['iso_queue'] = iso_queue
|
||||||
|
|
||||||
logging.debug(f'big{i} parameters are:')
|
logging.info(f'big{i} parameters are:')
|
||||||
logging.debug('%s', pprint.pformat(vars(big)))
|
logging.info('%s', pprint.pformat(vars(big)))
|
||||||
logging.info(f'Finished setup of big{i}.')
|
logging.info(f'Finished setup of big{i}.')
|
||||||
|
|
||||||
await asyncio.sleep(i+1) # Wait for advertising to set up
|
await asyncio.sleep(i+1) # Wait for advertising to set up
|
||||||
@@ -349,18 +375,51 @@ class Streamer():
|
|||||||
if self.task is not None:
|
if self.task is not None:
|
||||||
self.task.cancel()
|
self.task.cancel()
|
||||||
|
|
||||||
|
# Let cancellation propagate to the stream() coroutine
|
||||||
|
await asyncio.sleep(0.01)
|
||||||
|
|
||||||
self.task = None
|
self.task = None
|
||||||
|
|
||||||
# Close audio inputs (await to ensure ALSA devices are released)
|
# Close audio inputs (await to ensure ALSA devices are released)
|
||||||
close_tasks = []
|
async_closers = []
|
||||||
|
sync_closers = []
|
||||||
for big in self.bigs.values():
|
for big in self.bigs.values():
|
||||||
ai = big.get("audio_input")
|
ai = big.get("audio_input")
|
||||||
if ai and hasattr(ai, "close"):
|
if not ai:
|
||||||
close_tasks.append(ai.close())
|
continue
|
||||||
# Remove reference so a fresh one is created next time
|
# First close any frames generator backed by the input to stop reads
|
||||||
big.pop("audio_input", None)
|
frames_gen = big.get("frames_gen")
|
||||||
if close_tasks:
|
if frames_gen and hasattr(frames_gen, "aclose"):
|
||||||
await asyncio.gather(*close_tasks, return_exceptions=True)
|
try:
|
||||||
|
await frames_gen.aclose()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
big.pop("frames_gen", None)
|
||||||
|
if hasattr(ai, "aclose") and callable(getattr(ai, "aclose")):
|
||||||
|
async_closers.append(ai.aclose())
|
||||||
|
elif hasattr(ai, "close") and callable(getattr(ai, "close")):
|
||||||
|
sync_closers.append(ai.close)
|
||||||
|
# Remove reference so a fresh one is created next time
|
||||||
|
big.pop("audio_input", None)
|
||||||
|
|
||||||
|
if async_closers:
|
||||||
|
await asyncio.gather(*async_closers, return_exceptions=True)
|
||||||
|
for fn in sync_closers:
|
||||||
|
try:
|
||||||
|
fn()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Reset PortAudio to drop lingering PipeWire capture nodes
|
||||||
|
try:
|
||||||
|
import sounddevice as _sd
|
||||||
|
if hasattr(_sd, "_terminate"):
|
||||||
|
_sd._terminate()
|
||||||
|
await asyncio.sleep(0.05)
|
||||||
|
if hasattr(_sd, "_initialize"):
|
||||||
|
_sd._initialize()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
async def stream(self):
|
async def stream(self):
|
||||||
|
|
||||||
@@ -388,6 +447,8 @@ class Streamer():
|
|||||||
big['audio_input'] = audio_source
|
big['audio_input'] = audio_source
|
||||||
big['encoder'] = encoder
|
big['encoder'] = encoder
|
||||||
big['precoded'] = False
|
big['precoded'] = False
|
||||||
|
# Prepare frames generator for graceful shutdown
|
||||||
|
big['frames_gen'] = big['audio_input'].frames(lc3_frame_samples)
|
||||||
|
|
||||||
elif audio_source == 'webrtc':
|
elif audio_source == 'webrtc':
|
||||||
big['audio_input'] = WebRTCAudioInput()
|
big['audio_input'] = WebRTCAudioInput()
|
||||||
@@ -403,6 +464,8 @@ class Streamer():
|
|||||||
big['lc3_bytes_per_frame'] = global_config.octets_per_frame
|
big['lc3_bytes_per_frame'] = global_config.octets_per_frame
|
||||||
big['encoder'] = encoder
|
big['encoder'] = encoder
|
||||||
big['precoded'] = False
|
big['precoded'] = False
|
||||||
|
# Prepare frames generator for graceful shutdown
|
||||||
|
big['frames_gen'] = big['audio_input'].frames(lc3_frame_samples)
|
||||||
|
|
||||||
# precoded lc3 from ram
|
# precoded lc3 from ram
|
||||||
elif isinstance(big_config[i].audio_source, bytes):
|
elif isinstance(big_config[i].audio_source, bytes):
|
||||||
@@ -573,7 +636,12 @@ class Streamer():
|
|||||||
stream_finished[i] = True
|
stream_finished[i] = True
|
||||||
continue
|
continue
|
||||||
else: # code lc3 on the fly
|
else: # code lc3 on the fly
|
||||||
pcm_frame = await anext(big['audio_input'].frames(big['lc3_frame_samples']), None)
|
# Use stored frames generator when available so we can aclose() it on stop
|
||||||
|
frames_gen = big.get('frames_gen')
|
||||||
|
if frames_gen is None:
|
||||||
|
frames_gen = big['audio_input'].frames(big['lc3_frame_samples'])
|
||||||
|
big['frames_gen'] = frames_gen
|
||||||
|
pcm_frame = await anext(frames_gen, None)
|
||||||
|
|
||||||
if pcm_frame is None: # Not all streams may stop at the same time
|
if pcm_frame is None: # Not all streams may stop at the same time
|
||||||
stream_finished[i] = True
|
stream_finished[i] = True
|
||||||
@@ -674,7 +742,7 @@ if __name__ == "__main__":
|
|||||||
# TODO: encrypted streams are not working
|
# TODO: encrypted streams are not working
|
||||||
|
|
||||||
for big in config.bigs:
|
for big in config.bigs:
|
||||||
#big.code = 'ff'*16 # returns hci/HCI_ENCRYPTION_MODE_NOT_ACCEPTABLE_ERROR
|
#big.code = 'abcd'
|
||||||
#big.code = '78 e5 dc f1 34 ab 42 bf c1 92 ef dd 3a fd 67 ae'
|
#big.code = '78 e5 dc f1 34 ab 42 bf c1 92 ef dd 3a fd 67 ae'
|
||||||
big.precode_wav = False
|
big.precode_wav = False
|
||||||
#big.audio_source = big.audio_source.replace('.wav', '_10_16_32.lc3') #lc3 precoded files
|
#big.audio_source = big.audio_source.replace('.wav', '_10_16_32.lc3') #lc3 precoded files
|
||||||
|
|||||||
@@ -91,6 +91,8 @@ class Multicaster:
|
|||||||
for big in self.bigs.values():
|
for big in self.bigs.values():
|
||||||
if big.get('advertising_set'):
|
if big.get('advertising_set'):
|
||||||
await big['advertising_set'].stop()
|
await big['advertising_set'].stop()
|
||||||
|
# Explicitly power off the device to ensure a clean state before closing the transport
|
||||||
|
await self.device.power_off()
|
||||||
await self.device_acm.__aexit__(None, None, None) # Manually triggering teardown
|
await self.device_acm.__aexit__(None, None, None) # Manually triggering teardown
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -44,7 +44,11 @@ import time
|
|||||||
from dotenv import load_dotenv
|
from dotenv import load_dotenv
|
||||||
from auracast import multicast
|
from auracast import multicast
|
||||||
from auracast import auracast_config
|
from auracast import auracast_config
|
||||||
from auracast.utils.sounddevice_utils import list_usb_pw_inputs, list_network_pw_inputs
|
from auracast.utils.sounddevice_utils import (
|
||||||
|
get_usb_pw_inputs,
|
||||||
|
get_network_pw_inputs,
|
||||||
|
refresh_pw_cache,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
@@ -59,12 +63,14 @@ if __name__ == "__main__":
|
|||||||
|
|
||||||
os.environ.setdefault("PULSE_LATENCY_MSEC", "3")
|
os.environ.setdefault("PULSE_LATENCY_MSEC", "3")
|
||||||
|
|
||||||
usb_inputs = list_usb_pw_inputs()
|
# Refresh device cache and list inputs
|
||||||
|
refresh_pw_cache()
|
||||||
|
usb_inputs = get_usb_pw_inputs()
|
||||||
logging.info("USB pw inputs:")
|
logging.info("USB pw inputs:")
|
||||||
for i, d in usb_inputs:
|
for i, d in usb_inputs:
|
||||||
logging.info(f"{i}: {d['name']} in={d['max_input_channels']}")
|
logging.info(f"{i}: {d['name']} in={d['max_input_channels']}")
|
||||||
|
|
||||||
aes67_inputs = list_network_pw_inputs()
|
aes67_inputs = get_network_pw_inputs()
|
||||||
logging.info("AES67 pw inputs:")
|
logging.info("AES67 pw inputs:")
|
||||||
for i, d in aes67_inputs:
|
for i, d in aes67_inputs:
|
||||||
logging.info(f"{i}: {d['name']} in={d['max_input_channels']}")
|
logging.info(f"{i}: {d['name']} in={d['max_input_channels']}")
|
||||||
@@ -85,7 +91,8 @@ if __name__ == "__main__":
|
|||||||
if iface_substr:
|
if iface_substr:
|
||||||
# Loop until a matching AES67 input becomes available
|
# Loop until a matching AES67 input becomes available
|
||||||
while True:
|
while True:
|
||||||
current = list_network_pw_inputs()
|
refresh_pw_cache()
|
||||||
|
current = get_network_pw_inputs()
|
||||||
sel = next(((i, d) for i, d in current if iface_substr in (d.get('name','').lower())), None)
|
sel = next(((i, d) for i, d in current if iface_substr in (d.get('name','').lower())), None)
|
||||||
if sel:
|
if sel:
|
||||||
input_sel = sel[0]
|
input_sel = sel[0]
|
||||||
@@ -100,7 +107,8 @@ if __name__ == "__main__":
|
|||||||
else:
|
else:
|
||||||
# Loop until a USB input becomes available (mirror AES67 retry behavior)
|
# Loop until a USB input becomes available (mirror AES67 retry behavior)
|
||||||
while True:
|
while True:
|
||||||
current = list_usb_pw_inputs()
|
refresh_pw_cache()
|
||||||
|
current = get_usb_pw_inputs()
|
||||||
if current:
|
if current:
|
||||||
input_sel, selected_dev = current[0]
|
input_sel, selected_dev = current[0]
|
||||||
logging.info(f"Selected first USB input: index={input_sel}, device={selected_dev['name']}")
|
logging.info(f"Selected first USB input: index={input_sel}, device={selected_dev['name']}")
|
||||||
@@ -146,11 +154,12 @@ if __name__ == "__main__":
|
|||||||
presentation_delay_us=40000,
|
presentation_delay_us=40000,
|
||||||
qos_config=auracast_config.AuracastQosHigh(),
|
qos_config=auracast_config.AuracastQosHigh(),
|
||||||
auracast_sampling_rate_hz = LC3_SRATE,
|
auracast_sampling_rate_hz = LC3_SRATE,
|
||||||
octets_per_frame = OCTETS_PER_FRAME, # 32kbps@16kHz
|
octets_per_frame = OCTETS_PER_FRAME,
|
||||||
transport=TRANSPORT1
|
transport=TRANSPORT1
|
||||||
)
|
)
|
||||||
#config.debug = True
|
#config.debug = True
|
||||||
|
|
||||||
|
logging.info(config.model_dump_json(indent=2))
|
||||||
multicast.run_async(
|
multicast.run_async(
|
||||||
multicast.broadcast(
|
multicast.broadcast(
|
||||||
config,
|
config,
|
||||||
|
|||||||
@@ -1,23 +1,88 @@
|
|||||||
# frontend/app.py
|
# frontend/app.py
|
||||||
import os
|
import os
|
||||||
import time
|
import time
|
||||||
import streamlit as st
|
|
||||||
import requests
|
|
||||||
from auracast import auracast_config
|
|
||||||
import logging as log
|
import logging as log
|
||||||
|
from PIL import Image
|
||||||
|
|
||||||
|
import requests
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
import streamlit as st
|
||||||
|
|
||||||
|
from auracast import auracast_config
|
||||||
|
from auracast.utils.frontend_auth import (
|
||||||
|
is_pw_disabled,
|
||||||
|
load_pw_record,
|
||||||
|
save_pw_record,
|
||||||
|
hash_password,
|
||||||
|
verify_password,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Set page configuration (tab title and icon) before using other Streamlit APIs
|
||||||
|
# Always use the favicon from the utils folder relative to this file
|
||||||
|
_THIS_DIR = os.path.dirname(__file__)
|
||||||
|
_FAVICON_PATH = os.path.abspath(os.path.join(_THIS_DIR, '..', 'utils', 'favicon.ico'))
|
||||||
|
favicon = Image.open(_FAVICON_PATH)
|
||||||
|
st.set_page_config(page_title="Castbox", page_icon=favicon, layout="centered")
|
||||||
|
|
||||||
|
# Load environment variables from a .env file if present
|
||||||
|
load_dotenv()
|
||||||
|
|
||||||
# Track whether WebRTC stream is active across Streamlit reruns
|
# Track whether WebRTC stream is active across Streamlit reruns
|
||||||
if 'stream_started' not in st.session_state:
|
if 'stream_started' not in st.session_state:
|
||||||
st.session_state['stream_started'] = False
|
st.session_state['stream_started'] = False
|
||||||
|
|
||||||
|
# Frontend authentication gate is controlled via env using shared utils
|
||||||
|
|
||||||
|
if 'frontend_authenticated' not in st.session_state:
|
||||||
|
st.session_state['frontend_authenticated'] = False
|
||||||
|
|
||||||
|
if not is_pw_disabled():
|
||||||
|
pw_rec = load_pw_record()
|
||||||
|
|
||||||
|
# First-time setup: no password set -> force user to choose one
|
||||||
|
if pw_rec is None:
|
||||||
|
st.header("Set up your frontend password")
|
||||||
|
st.info("For security, you must set a password on first access.")
|
||||||
|
with st.form("first_setup_form"):
|
||||||
|
new_pw = st.text_input("New password", type="password")
|
||||||
|
new_pw2 = st.text_input("Confirm password", type="password")
|
||||||
|
submitted = st.form_submit_button("Save password")
|
||||||
|
if submitted:
|
||||||
|
if len(new_pw) < 6:
|
||||||
|
st.error("Password should be at least 6 characters.")
|
||||||
|
elif new_pw != new_pw2:
|
||||||
|
st.error("Passwords do not match.")
|
||||||
|
else:
|
||||||
|
salt, key = hash_password(new_pw)
|
||||||
|
try:
|
||||||
|
save_pw_record(salt, key)
|
||||||
|
st.success("Password saved. You can now sign in.")
|
||||||
|
st.rerun()
|
||||||
|
except Exception as e:
|
||||||
|
st.error(f"Failed to save password: {e}")
|
||||||
|
st.stop()
|
||||||
|
|
||||||
|
# Normal sign-in gate
|
||||||
|
if not st.session_state['frontend_authenticated']:
|
||||||
|
st.header("Sign in")
|
||||||
|
with st.form("signin_form"):
|
||||||
|
pw = st.text_input("Password", type="password")
|
||||||
|
submitted = st.form_submit_button("Sign in")
|
||||||
|
if submitted:
|
||||||
|
if verify_password(pw, pw_rec):
|
||||||
|
st.session_state['frontend_authenticated'] = True
|
||||||
|
st.success("Signed in.")
|
||||||
|
st.rerun()
|
||||||
|
else:
|
||||||
|
st.error("Incorrect password. Please try again.")
|
||||||
|
# Stop rendering the rest of the app until authenticated
|
||||||
|
if not st.session_state['frontend_authenticated']:
|
||||||
|
st.stop()
|
||||||
|
|
||||||
# Global: desired packetization time in ms for Opus (should match backend)
|
# Global: desired packetization time in ms for Opus (should match backend)
|
||||||
PTIME = 40
|
PTIME = 40
|
||||||
BACKEND_URL = "http://localhost:5000"
|
BACKEND_URL = "http://localhost:5000"
|
||||||
#TRANSPORT1 = "serial:/dev/serial/by-id/usb-ZEPHYR_Zephyr_HCI_UART_sample_B53C372677E14460-if00,115200,rtscts"
|
|
||||||
#TRANSPORT2 = "serial:/dev/serial/by-id/usb-ZEPHYR_Zephyr_HCI_UART_sample_CC69A2912F84AE5E-if00,115200,rtscts"
|
|
||||||
|
|
||||||
TRANSPORT1 = 'serial:/dev/ttyAMA3,1000000,rtscts' # transport for raspberry pi gpio header
|
|
||||||
TRANSPORT2 = 'serial:/dev/ttyAMA4,1000000,rtscts' # transport for raspberry pi gpio header
|
|
||||||
QUALITY_MAP = {
|
QUALITY_MAP = {
|
||||||
"High (48kHz)": {"rate": 48000, "octets": 120},
|
"High (48kHz)": {"rate": 48000, "octets": 120},
|
||||||
"Good (32kHz)": {"rate": 32000, "octets": 80},
|
"Good (32kHz)": {"rate": 32000, "octets": 80},
|
||||||
@@ -34,19 +99,34 @@ try:
|
|||||||
except Exception:
|
except Exception:
|
||||||
saved_settings = {}
|
saved_settings = {}
|
||||||
|
|
||||||
st.title("🎙️ Auracast Audio Mode Control")
|
# Define is_streaming early from the fetched status for use throughout the UI
|
||||||
|
is_streaming = bool(saved_settings.get("is_streaming", False))
|
||||||
|
|
||||||
|
st.title("Auracast Audio Mode Control")
|
||||||
|
|
||||||
# Audio mode selection with persisted default
|
# Audio mode selection with persisted default
|
||||||
options = ["Webapp", "USB/Network", "Demo"]
|
# Note: backend persists 'USB' for any device:<name> source (including AES67). We default to 'USB' in that case.
|
||||||
saved_audio_mode = saved_settings.get("audio_mode", "Webapp")
|
options = [
|
||||||
|
"Demo",
|
||||||
|
"USB",
|
||||||
|
"AES67",
|
||||||
|
# "Webapp"
|
||||||
|
]
|
||||||
|
saved_audio_mode = saved_settings.get("audio_mode", "Demo")
|
||||||
if saved_audio_mode not in options:
|
if saved_audio_mode not in options:
|
||||||
saved_audio_mode = "Webapp"
|
# Map legacy/unknown modes to closest
|
||||||
|
mapping = {"USB/Network": "USB", "Network": "AES67"}
|
||||||
|
saved_audio_mode = mapping.get(saved_audio_mode, "Demo")
|
||||||
|
|
||||||
audio_mode = st.selectbox(
|
audio_mode = st.selectbox(
|
||||||
"Audio Mode",
|
"Audio Mode",
|
||||||
options,
|
options,
|
||||||
index=options.index(saved_audio_mode),
|
index=options.index(saved_audio_mode) if saved_audio_mode in options else options.index("Demo"),
|
||||||
help="Select the audio input source. Choose 'Webapp' for browser microphone, 'USB/Network' for a connected hardware device, or 'Demo' for a simulated stream."
|
help=(
|
||||||
|
"Select the audio input source. Choose 'Webapp' for browser microphone, "
|
||||||
|
"'USB' for a connected USB audio device (via PipeWire), 'AES67' for network RTP/AES67 sources, "
|
||||||
|
"or 'Demo' for a simulated stream."
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
if audio_mode == "Demo":
|
if audio_mode == "Demo":
|
||||||
@@ -66,15 +146,48 @@ if audio_mode == "Demo":
|
|||||||
index=0,
|
index=0,
|
||||||
help="Select the demo stream configuration."
|
help="Select the demo stream configuration."
|
||||||
)
|
)
|
||||||
|
# Stream password and flags (same as USB/AES67)
|
||||||
|
saved_pwd = saved_settings.get('stream_password', '') or ''
|
||||||
|
stream_passwort = st.text_input(
|
||||||
|
"Stream Passwort",
|
||||||
|
value=saved_pwd,
|
||||||
|
type=("password"),
|
||||||
|
help="Optional: Set a broadcast code to protect your stream. Leave empty for an open (uncoded) broadcast."
|
||||||
|
)
|
||||||
|
col_flags1, col_flags2, col_pdelay, col_rtn = st.columns([1, 1, 1, 1], gap="small")
|
||||||
|
with col_flags1:
|
||||||
|
assisted_listening = st.checkbox(
|
||||||
|
"Assistive listening",
|
||||||
|
value=bool(saved_settings.get('assisted_listening_stream', False))
|
||||||
|
)
|
||||||
|
with col_flags2:
|
||||||
|
immediate_rendering = st.checkbox(
|
||||||
|
"Immediate rendering",
|
||||||
|
value=bool(saved_settings.get('immediate_rendering', False))
|
||||||
|
)
|
||||||
|
# QoS/presentation controls inline with flags
|
||||||
|
default_pdelay = int(saved_settings.get('presentation_delay_us', 40000) or 40000)
|
||||||
|
with col_pdelay:
|
||||||
|
presentation_delay_us = st.number_input(
|
||||||
|
"Presentation delay (µs)",
|
||||||
|
min_value=10000, max_value=200000, step=1000, value=default_pdelay,
|
||||||
|
help="Delay between capture and presentation for receivers."
|
||||||
|
)
|
||||||
|
default_rtn = int(saved_settings.get('rtn', 4) or 4)
|
||||||
|
with col_rtn:
|
||||||
|
rtn = st.selectbox(
|
||||||
|
"Retransmissions (RTN)", options=[0,1,2,3,4], index=[0,1,2,3,4].index(default_rtn),
|
||||||
|
help="Number of ISO retransmissions (higher improves robustness at cost of airtime)."
|
||||||
|
)
|
||||||
#st.info(f"Demo mode selected: {demo_selected} (Streams: {demo_stream_map[demo_selected]['streams']}, Rate: {demo_stream_map[demo_selected]['rate']} Hz)")
|
#st.info(f"Demo mode selected: {demo_selected} (Streams: {demo_stream_map[demo_selected]['streams']}, Rate: {demo_stream_map[demo_selected]['rate']} Hz)")
|
||||||
# Start/Stop buttons for demo mode
|
# Start/Stop buttons for demo mode
|
||||||
if 'demo_stream_started' not in st.session_state:
|
if 'demo_stream_started' not in st.session_state:
|
||||||
st.session_state['demo_stream_started'] = False
|
st.session_state['demo_stream_started'] = False
|
||||||
col1, col2 = st.columns(2)
|
col1, col2 = st.columns(2)
|
||||||
with col1:
|
with col1:
|
||||||
start_demo = st.button("Start Demo Stream")
|
start_demo = st.button("Start Demo Stream", disabled=is_streaming)
|
||||||
with col2:
|
with col2:
|
||||||
stop_demo = st.button("Stop Demo Stream")
|
stop_demo = st.button("Stop Demo Stream", disabled=not is_streaming)
|
||||||
if start_demo:
|
if start_demo:
|
||||||
# Always stop any running stream for clean state
|
# Always stop any running stream for clean state
|
||||||
try:
|
try:
|
||||||
@@ -99,6 +212,7 @@ if audio_mode == "Demo":
|
|||||||
for i in range(demo_cfg['streams']):
|
for i in range(demo_cfg['streams']):
|
||||||
cfg_cls, lang = lang_cfgs[i % len(lang_cfgs)]
|
cfg_cls, lang = lang_cfgs[i % len(lang_cfgs)]
|
||||||
bigs1.append(cfg_cls(
|
bigs1.append(cfg_cls(
|
||||||
|
code=(stream_passwort.strip() or None),
|
||||||
audio_source=f'file:../testdata/wave_particle_5min_{lang}_{int(q["rate"]/1000)}kHz_mono.wav',
|
audio_source=f'file:../testdata/wave_particle_5min_{lang}_{int(q["rate"]/1000)}kHz_mono.wav',
|
||||||
iso_que_len=32,
|
iso_que_len=32,
|
||||||
sampling_frequency=q['rate'],
|
sampling_frequency=q['rate'],
|
||||||
@@ -115,7 +229,15 @@ if audio_mode == "Demo":
|
|||||||
config1 = auracast_config.AuracastConfigGroup(
|
config1 = auracast_config.AuracastConfigGroup(
|
||||||
auracast_sampling_rate_hz=q['rate'],
|
auracast_sampling_rate_hz=q['rate'],
|
||||||
octets_per_frame=q['octets'],
|
octets_per_frame=q['octets'],
|
||||||
transport=TRANSPORT1,
|
transport='', # is set in baccol_qoskend
|
||||||
|
assisted_listening_stream=assisted_listening,
|
||||||
|
immediate_rendering=immediate_rendering,
|
||||||
|
presentation_delay_us=presentation_delay_us,
|
||||||
|
qos_config=auracast_config.AuracastQoSConfig(
|
||||||
|
iso_int_multiple_10ms=1,
|
||||||
|
number_of_retransmissions=int(rtn),
|
||||||
|
max_transport_latency_ms=int(rtn)*10 + 3,
|
||||||
|
),
|
||||||
bigs=bigs1
|
bigs=bigs1
|
||||||
)
|
)
|
||||||
config2 = None
|
config2 = None
|
||||||
@@ -123,7 +245,15 @@ if audio_mode == "Demo":
|
|||||||
config2 = auracast_config.AuracastConfigGroup(
|
config2 = auracast_config.AuracastConfigGroup(
|
||||||
auracast_sampling_rate_hz=q['rate'],
|
auracast_sampling_rate_hz=q['rate'],
|
||||||
octets_per_frame=q['octets'],
|
octets_per_frame=q['octets'],
|
||||||
transport=TRANSPORT2,
|
transport='', # is set in backend
|
||||||
|
assisted_listening_stream=assisted_listening,
|
||||||
|
immediate_rendering=immediate_rendering,
|
||||||
|
presentation_delay_us=presentation_delay_us,
|
||||||
|
qos_config=auracast_config.AuracastQoSConfig(
|
||||||
|
iso_int_multiple_10ms=1,
|
||||||
|
number_of_retransmissions=int(rtn),
|
||||||
|
max_transport_latency_ms=int(rtn)*10 + 3,
|
||||||
|
),
|
||||||
bigs=bigs2
|
bigs=bigs2
|
||||||
)
|
)
|
||||||
# Call /init and /init2
|
# Call /init and /init2
|
||||||
@@ -151,6 +281,7 @@ if audio_mode == "Demo":
|
|||||||
st.session_state['demo_stream_started'] = False
|
st.session_state['demo_stream_started'] = False
|
||||||
if r.get('was_running'):
|
if r.get('was_running'):
|
||||||
st.info("Demo stream stopped.")
|
st.info("Demo stream stopped.")
|
||||||
|
st.rerun()
|
||||||
else:
|
else:
|
||||||
st.info("Demo stream was not running.")
|
st.info("Demo stream was not running.")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -194,61 +325,133 @@ else:
|
|||||||
value=default_lang,
|
value=default_lang,
|
||||||
help="Three-letter language code (e.g., 'eng' for English, 'deu' for German). Used by receivers to display the language of the stream. See: https://en.wikipedia.org/wiki/List_of_ISO_639-3_codes"
|
help="Three-letter language code (e.g., 'eng' for English, 'deu' for German). Used by receivers to display the language of the stream. See: https://en.wikipedia.org/wiki/List_of_ISO_639-3_codes"
|
||||||
)
|
)
|
||||||
|
# Optional broadcast code for coded streams
|
||||||
|
stream_passwort = st.text_input(
|
||||||
|
"Stream Passwort",
|
||||||
|
value="",
|
||||||
|
type="password",
|
||||||
|
help="Optional: Set a broadcast code to protect your stream. Leave empty for an open (uncoded) broadcast."
|
||||||
|
)
|
||||||
|
# Flags and QoS row (compact, four columns)
|
||||||
|
col_flags1, col_flags2, col_pdelay, col_rtn = st.columns([1, 1, 1, 1], gap="small")
|
||||||
|
with col_flags1:
|
||||||
|
assisted_listening = st.checkbox(
|
||||||
|
"Assistive listening",
|
||||||
|
value=bool(saved_settings.get('assisted_listening_stream', False))
|
||||||
|
)
|
||||||
|
with col_flags2:
|
||||||
|
immediate_rendering = st.checkbox(
|
||||||
|
"Immediate rendering",
|
||||||
|
value=bool(saved_settings.get('immediate_rendering', False))
|
||||||
|
)
|
||||||
|
# QoS/presentation controls inline with flags
|
||||||
|
default_pdelay = int(saved_settings.get('presentation_delay_us', 40000) or 40000)
|
||||||
|
with col_pdelay:
|
||||||
|
presentation_delay_us = st.number_input(
|
||||||
|
"Presentation delay (µs)",
|
||||||
|
min_value=10000, max_value=200000, step=1000, value=default_pdelay,
|
||||||
|
help="Delay between capture and presentation for receivers."
|
||||||
|
)
|
||||||
|
default_rtn = int(saved_settings.get('rtn', 4) or 4)
|
||||||
|
with col_rtn:
|
||||||
|
rtn = st.selectbox(
|
||||||
|
"Retransmissions (RTN)", options=[0,1,2,3,4], index=[0,1,2,3,4].index(default_rtn),
|
||||||
|
help="Number of ISO retransmissions (higher improves robustness at cost of airtime)."
|
||||||
|
)
|
||||||
# Gain slider for Webapp mode
|
# Gain slider for Webapp mode
|
||||||
if audio_mode == "Webapp":
|
if audio_mode == "Webapp":
|
||||||
mic_gain = st.slider("Microphone Gain", 0.0, 2.0, 1.0, 0.1, help="Adjust microphone volume sent to Auracast")
|
mic_gain = st.slider("Microphone Gain", 0.0, 2.0, 1.0, 0.1, help="Adjust microphone volume sent to Auracast")
|
||||||
else:
|
else:
|
||||||
mic_gain = 1.0
|
mic_gain = 1.0
|
||||||
|
|
||||||
# Input device selection for USB mode
|
# Input device selection for USB or AES67 mode
|
||||||
if audio_mode == "USB/Network":
|
if audio_mode in ("USB", "AES67"):
|
||||||
resp = requests.get(f"{BACKEND_URL}/audio_inputs")
|
if not is_streaming:
|
||||||
device_list = resp.json().get('inputs', [])
|
# Only query device lists when NOT streaming to avoid extra backend calls
|
||||||
# Display "name [id]" but use name as value
|
try:
|
||||||
input_options = [f"{d['name']} [{d['id']}]" for d in device_list]
|
endpoint = "/audio_inputs_pw_usb" if audio_mode == "USB" else "/audio_inputs_pw_network"
|
||||||
option_name_map = {f"{d['name']} [{d['id']}]": d['name'] for d in device_list}
|
resp = requests.get(f"{BACKEND_URL}{endpoint}")
|
||||||
device_names = [d['name'] for d in device_list]
|
device_list = resp.json().get('inputs', [])
|
||||||
|
except Exception as e:
|
||||||
|
st.error(f"Failed to fetch devices: {e}")
|
||||||
|
device_list = []
|
||||||
|
|
||||||
# Determine default input by name
|
# Display "name [id]" but use name as value
|
||||||
default_input_name = saved_settings.get('input_device')
|
input_options = [f"{d['name']} [{d['id']}]" for d in device_list]
|
||||||
if default_input_name not in device_names and device_names:
|
option_name_map = {f"{d['name']} [{d['id']}]": d['name'] for d in device_list}
|
||||||
default_input_name = device_names[0]
|
device_names = [d['name'] for d in device_list]
|
||||||
default_input_label = None
|
|
||||||
for label, name in option_name_map.items():
|
# Determine default input by name (from persisted server state)
|
||||||
if name == default_input_name:
|
default_input_name = saved_settings.get('input_device')
|
||||||
default_input_label = label
|
if default_input_name not in device_names and device_names:
|
||||||
break
|
default_input_name = device_names[0]
|
||||||
if not input_options:
|
default_input_label = None
|
||||||
st.warning("No hardware audio input devices found. Plug in a USB input device and click Refresh.")
|
for label, name in option_name_map.items():
|
||||||
if st.button("Refresh"):
|
if name == default_input_name:
|
||||||
try:
|
default_input_label = label
|
||||||
requests.post(f"{BACKEND_URL}/refresh_audio_inputs", timeout=3)
|
break
|
||||||
except Exception as e:
|
if not input_options:
|
||||||
st.error(f"Failed to refresh devices: {e}")
|
warn_text = (
|
||||||
st.rerun()
|
"No USB audio input devices found. Connect a USB input and click Refresh."
|
||||||
input_device = None
|
if audio_mode == "USB" else
|
||||||
else:
|
"No AES67/Network inputs found."
|
||||||
col1, col2 = st.columns([3, 1], vertical_alignment="bottom")
|
|
||||||
with col1:
|
|
||||||
selected_option = st.selectbox(
|
|
||||||
"Input Device",
|
|
||||||
input_options,
|
|
||||||
index=input_options.index(default_input_label) if default_input_label in input_options else 0
|
|
||||||
)
|
)
|
||||||
with col2:
|
st.warning(warn_text)
|
||||||
if st.button("Refresh"):
|
if st.button("Refresh", disabled=is_streaming):
|
||||||
try:
|
try:
|
||||||
requests.post(f"{BACKEND_URL}/refresh_audio_inputs", timeout=3)
|
r = requests.post(f"{BACKEND_URL}/refresh_audio_devices", timeout=8)
|
||||||
|
if not r.ok:
|
||||||
|
st.error(f"Failed to refresh: {r.text}")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
st.error(f"Failed to refresh devices: {e}")
|
st.error(f"Failed to refresh devices: {e}")
|
||||||
st.rerun()
|
st.rerun()
|
||||||
# Send only the device name to backend
|
input_device = None
|
||||||
input_device = option_name_map[selected_option] if selected_option in option_name_map else None
|
else:
|
||||||
|
col1, col2 = st.columns([3, 1], vertical_alignment="bottom")
|
||||||
|
with col1:
|
||||||
|
selected_option = st.selectbox(
|
||||||
|
"Input Device",
|
||||||
|
input_options,
|
||||||
|
index=input_options.index(default_input_label) if default_input_label in input_options else 0
|
||||||
|
)
|
||||||
|
with col2:
|
||||||
|
if st.button("Refresh", disabled=is_streaming):
|
||||||
|
try:
|
||||||
|
r = requests.post(f"{BACKEND_URL}/refresh_audio_devices", timeout=8)
|
||||||
|
if not r.ok:
|
||||||
|
st.error(f"Failed to refresh: {r.text}")
|
||||||
|
except Exception as e:
|
||||||
|
st.error(f"Failed to refresh devices: {e}")
|
||||||
|
st.rerun()
|
||||||
|
# Send only the device name to backend
|
||||||
|
input_device = option_name_map.get(selected_option)
|
||||||
|
else:
|
||||||
|
# When streaming, keep showing the current selection but lock editing.
|
||||||
|
input_device = saved_settings.get('input_device')
|
||||||
|
current_label = input_device or "No device selected"
|
||||||
|
st.selectbox(
|
||||||
|
"Input Device",
|
||||||
|
[current_label],
|
||||||
|
index=0,
|
||||||
|
disabled=True,
|
||||||
|
help="Stop the stream to change the input device."
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
input_device = None
|
input_device = None
|
||||||
|
|
||||||
start_stream = st.button("Start Auracast")
|
# Buttons and status on a single row (4 columns: start, stop, spacer, status)
|
||||||
stop_stream = st.button("Stop Auracast")
|
c_start, c_stop, c_spacer, c_status = st.columns([1, 1, 1, 2], gap="small", vertical_alignment="center")
|
||||||
|
with c_start:
|
||||||
|
start_stream = st.button("Start Auracast", disabled=is_streaming)
|
||||||
|
with c_stop:
|
||||||
|
stop_stream = st.button("Stop Auracast", disabled=not is_streaming)
|
||||||
|
# c_spacer intentionally left empty to push status to the far right
|
||||||
|
with c_status:
|
||||||
|
# Fetch current status from backend and render using Streamlit widgets (no HTML)
|
||||||
|
# The is_streaming variable is now defined at the top of the script.
|
||||||
|
# We only need to re-fetch here if we want the absolute latest status for the display,
|
||||||
|
# but for UI consistency, we can just use the value from the top of the script run.
|
||||||
|
st.write("🟢 Streaming" if is_streaming else "🔴 Stopped")
|
||||||
|
|
||||||
# If gain slider moved while streaming, send update to JS without restarting
|
# If gain slider moved while streaming, send update to JS without restarting
|
||||||
if audio_mode == "Webapp" and st.session_state.get('stream_started'):
|
if audio_mode == "Webapp" and st.session_state.get('stream_started'):
|
||||||
@@ -265,6 +468,7 @@ else:
|
|||||||
r = requests.post(f"{BACKEND_URL}/stop_audio").json()
|
r = requests.post(f"{BACKEND_URL}/stop_audio").json()
|
||||||
if r['was_running']:
|
if r['was_running']:
|
||||||
st.success("Stream Stopped!")
|
st.success("Stream Stopped!")
|
||||||
|
st.rerun()
|
||||||
else:
|
else:
|
||||||
st.success("Stream was not running.")
|
st.success("Stream was not running.")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -287,8 +491,8 @@ else:
|
|||||||
if start_stream:
|
if start_stream:
|
||||||
# Always send stop to ensure backend is in a clean state, regardless of current status
|
# Always send stop to ensure backend is in a clean state, regardless of current status
|
||||||
r = requests.post(f"{BACKEND_URL}/stop_audio").json()
|
r = requests.post(f"{BACKEND_URL}/stop_audio").json()
|
||||||
if r['was_running']:
|
#if r['was_running']:
|
||||||
st.success("Stream Stopped!")
|
# st.success("Stream Stopped!")
|
||||||
|
|
||||||
# Small pause lets backend fully release audio devices before re-init
|
# Small pause lets backend fully release audio devices before re-init
|
||||||
time.sleep(1)
|
time.sleep(1)
|
||||||
@@ -297,18 +501,27 @@ else:
|
|||||||
config = auracast_config.AuracastConfigGroup(
|
config = auracast_config.AuracastConfigGroup(
|
||||||
auracast_sampling_rate_hz=q['rate'],
|
auracast_sampling_rate_hz=q['rate'],
|
||||||
octets_per_frame=q['octets'],
|
octets_per_frame=q['octets'],
|
||||||
transport=TRANSPORT1, # transport for raspberry pi gpio header
|
transport='', # is set in backend
|
||||||
|
assisted_listening_stream=assisted_listening,
|
||||||
|
immediate_rendering=immediate_rendering,
|
||||||
|
presentation_delay_us=presentation_delay_us,
|
||||||
|
qos_config=auracast_config.AuracastQoSConfig(
|
||||||
|
iso_int_multiple_10ms=1,
|
||||||
|
number_of_retransmissions=int(rtn),
|
||||||
|
max_transport_latency_ms=int(rtn)*10 + 3,
|
||||||
|
),
|
||||||
bigs = [
|
bigs = [
|
||||||
auracast_config.AuracastBigConfig(
|
auracast_config.AuracastBigConfig(
|
||||||
|
code=(stream_passwort.strip() or None),
|
||||||
name=stream_name,
|
name=stream_name,
|
||||||
program_info=program_info,
|
program_info=program_info,
|
||||||
language=language,
|
language=language,
|
||||||
audio_source=(
|
audio_source=(
|
||||||
f"device:{input_device}" if audio_mode == "USB/Network" else (
|
f"device:{input_device}" if audio_mode in ("USB", "AES67") else (
|
||||||
"webrtc" if audio_mode == "Webapp" else "network"
|
"webrtc" if audio_mode == "Webapp" else "network"
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
input_format=(f"int16le,{q['rate']},1" if audio_mode == "USB/Network" else "auto"),
|
input_format=(f"int16le,{q['rate']},1" if audio_mode in ("USB", "AES67") else "auto"),
|
||||||
iso_que_len=1,
|
iso_que_len=1,
|
||||||
sampling_frequency=q['rate'],
|
sampling_frequency=q['rate'],
|
||||||
octets_per_frame=q['octets'],
|
octets_per_frame=q['octets'],
|
||||||
@@ -320,6 +533,7 @@ else:
|
|||||||
r = requests.post(f"{BACKEND_URL}/init", json=config.model_dump())
|
r = requests.post(f"{BACKEND_URL}/init", json=config.model_dump())
|
||||||
if r.status_code == 200:
|
if r.status_code == 200:
|
||||||
st.success("Stream Started!")
|
st.success("Stream Started!")
|
||||||
|
st.rerun()
|
||||||
else:
|
else:
|
||||||
st.error(f"Failed to initialize: {r.text}")
|
st.error(f"Failed to initialize: {r.text}")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -393,6 +607,47 @@ else:
|
|||||||
# else:
|
# else:
|
||||||
# st.error("Could not fetch advertised streams.")
|
# st.error("Could not fetch advertised streams.")
|
||||||
|
|
||||||
|
############################
|
||||||
|
# System expander (collapsed)
|
||||||
|
############################
|
||||||
|
with st.expander("System control", expanded=False):
|
||||||
|
|
||||||
|
st.subheader("Change password")
|
||||||
|
if is_pw_disabled():
|
||||||
|
st.info("Frontend password protection is disabled via DISABLE_FRONTEND_PW.")
|
||||||
|
else:
|
||||||
|
with st.form("change_pw_form"):
|
||||||
|
cur = st.text_input("Current password", type="password")
|
||||||
|
new1 = st.text_input("New password", type="password")
|
||||||
|
new2 = st.text_input("Confirm new password", type="password")
|
||||||
|
submit_change = st.form_submit_button("Change password")
|
||||||
|
if submit_change:
|
||||||
|
rec = load_pw_record()
|
||||||
|
if not rec or not verify_password(cur, rec):
|
||||||
|
st.error("Current password is incorrect.")
|
||||||
|
elif len(new1) < 6:
|
||||||
|
st.error("New password should be at least 6 characters.")
|
||||||
|
elif new1 != new2:
|
||||||
|
st.error("New passwords do not match.")
|
||||||
|
else:
|
||||||
|
salt, key = hash_password(new1)
|
||||||
|
try:
|
||||||
|
save_pw_record(salt, key)
|
||||||
|
st.success("Password updated.")
|
||||||
|
except Exception as e:
|
||||||
|
st.error(f"Failed to update password: {e}")
|
||||||
|
|
||||||
|
st.subheader("Reboot")
|
||||||
|
if st.button("Reboot now", type="primary"):
|
||||||
|
try:
|
||||||
|
r = requests.post(f"{BACKEND_URL}/system_reboot", timeout=1)
|
||||||
|
if r.ok:
|
||||||
|
st.success("Reboot initiated. The UI will become unreachable shortly.")
|
||||||
|
else:
|
||||||
|
st.error(f"Failed to reboot: {r.status_code} {r.text}")
|
||||||
|
except Exception as e:
|
||||||
|
st.error(f"Error calling reboot: {e}")
|
||||||
|
|
||||||
log.basicConfig(
|
log.basicConfig(
|
||||||
level=os.environ.get('LOG_LEVEL', log.DEBUG),
|
level=os.environ.get('LOG_LEVEL', log.DEBUG),
|
||||||
format='%(module)s.py:%(lineno)d %(levelname)s: %(message)s'
|
format='%(module)s.py:%(lineno)d %(levelname)s: %(message)s'
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
BIN
src/auracast/utils/favicon.ico
Normal file
BIN
src/auracast/utils/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
96
src/auracast/utils/frontend_auth.py
Normal file
96
src/auracast/utils/frontend_auth.py
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
import os
|
||||||
|
import json
|
||||||
|
import base64
|
||||||
|
import hashlib
|
||||||
|
import hmac
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Optional, Tuple, Dict
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"is_pw_disabled",
|
||||||
|
"state_dir",
|
||||||
|
"pw_file_path",
|
||||||
|
"ensure_state_dir",
|
||||||
|
"hash_password",
|
||||||
|
"save_pw_record",
|
||||||
|
"load_pw_record",
|
||||||
|
"verify_password",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
# Environment-controlled bypass
|
||||||
|
|
||||||
|
def is_pw_disabled() -> bool:
|
||||||
|
val = os.getenv("DISABLE_FRONTEND_PW", "")
|
||||||
|
return str(val).strip().lower() in ("1", "true", "yes", "on")
|
||||||
|
|
||||||
|
|
||||||
|
# Storage paths and permissions
|
||||||
|
|
||||||
|
def state_dir() -> Path:
|
||||||
|
custom = os.getenv("AURACAST_STATE_DIR")
|
||||||
|
if custom:
|
||||||
|
return Path(custom).expanduser()
|
||||||
|
return Path.home() / ".config" / "auracast"
|
||||||
|
|
||||||
|
|
||||||
|
def pw_file_path() -> Path:
|
||||||
|
return state_dir() / "frontend_pw.json"
|
||||||
|
|
||||||
|
|
||||||
|
def ensure_state_dir() -> None:
|
||||||
|
d = state_dir()
|
||||||
|
d.mkdir(parents=True, exist_ok=True)
|
||||||
|
try:
|
||||||
|
os.chmod(d, 0o700)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
# Hashing and verification
|
||||||
|
|
||||||
|
def hash_password(password: str, salt: Optional[bytes] = None) -> Tuple[bytes, bytes]:
|
||||||
|
if salt is None:
|
||||||
|
salt = os.urandom(16)
|
||||||
|
key = hashlib.pbkdf2_hmac("sha256", password.encode("utf-8"), salt, 150_000, dklen=32)
|
||||||
|
return salt, key
|
||||||
|
|
||||||
|
|
||||||
|
def save_pw_record(salt: bytes, key: bytes) -> None:
|
||||||
|
ensure_state_dir()
|
||||||
|
rec = {
|
||||||
|
"salt": base64.b64encode(salt).decode("ascii"),
|
||||||
|
"key": base64.b64encode(key).decode("ascii"),
|
||||||
|
"kdf": "pbkdf2_sha256",
|
||||||
|
"iterations": 150000,
|
||||||
|
}
|
||||||
|
p = pw_file_path()
|
||||||
|
p.write_text(json.dumps(rec))
|
||||||
|
try:
|
||||||
|
os.chmod(p, 0o600)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def load_pw_record() -> Optional[Dict]:
|
||||||
|
p = pw_file_path()
|
||||||
|
if not p.exists():
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
rec = json.loads(p.read_text())
|
||||||
|
if "salt" in rec and "key" in rec:
|
||||||
|
return rec
|
||||||
|
except Exception:
|
||||||
|
return None
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def verify_password(password: str, rec: Dict) -> bool:
|
||||||
|
try:
|
||||||
|
salt = base64.b64decode(rec["salt"])
|
||||||
|
expected = base64.b64decode(rec["key"])
|
||||||
|
iters = int(rec.get("iterations", 150000))
|
||||||
|
key = hashlib.pbkdf2_hmac("sha256", password.encode("utf-8"), salt, iters, dklen=32)
|
||||||
|
return hmac.compare_digest(key, expected)
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
77
src/auracast/utils/reset_utils.py
Normal file
77
src/auracast/utils/reset_utils.py
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
import os
|
||||||
|
import asyncio
|
||||||
|
import logging as log
|
||||||
|
|
||||||
|
async def reset_nrf54l(slot: int = 0, timeout: float = 8.0):
|
||||||
|
"""
|
||||||
|
Reset the nRF54L target using OpenOCD before starting broadcast.
|
||||||
|
|
||||||
|
Looks for interface config files in the project at `src/openocd/` relative to this module only.
|
||||||
|
Accepts filename variants per slot:
|
||||||
|
- slot 0: raspberrypi-swd0.cfg or swd0.cfg
|
||||||
|
- slot 1: raspberrypi-swd1.cfg or swd1.cfg
|
||||||
|
|
||||||
|
Executes the equivalent of:
|
||||||
|
openocd \
|
||||||
|
-f ./raspberrypi-${INTERFACE}.cfg \
|
||||||
|
-f target/nordic/nrf54l.cfg \
|
||||||
|
-c "init" \
|
||||||
|
-c "reset run" \
|
||||||
|
-c "shutdown"
|
||||||
|
|
||||||
|
Best-effort: if OpenOCD is unavailable, logs a warning and continues.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# Resolve project directory and filenames
|
||||||
|
proj_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..', 'openocd'))
|
||||||
|
names = ['raspberrypi-swd0.cfg', 'swd0.cfg'] if slot == 0 else ['raspberrypi-swd1.cfg', 'swd1.cfg']
|
||||||
|
cfg = None
|
||||||
|
for n in names:
|
||||||
|
p = os.path.join(proj_dir, n)
|
||||||
|
if os.path.exists(p):
|
||||||
|
cfg = p
|
||||||
|
break
|
||||||
|
if not cfg:
|
||||||
|
log.warning("reset_nrf54l: no interface CFG found in project dir %s; skipping reset", proj_dir)
|
||||||
|
return
|
||||||
|
|
||||||
|
# Build openocd command (no sudo required as per project setup).
|
||||||
|
cmd = ['openocd', '-f', cfg, '-f', 'target/nordic/nrf54l.cfg',
|
||||||
|
'-c', 'init', '-c', 'reset run', '-c', 'shutdown']
|
||||||
|
async def _run(cmd):
|
||||||
|
proc = await asyncio.create_subprocess_exec(
|
||||||
|
*cmd, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.STDOUT
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
out, _ = await asyncio.wait_for(proc.communicate(), timeout=timeout)
|
||||||
|
except asyncio.TimeoutError:
|
||||||
|
log.error("reset_nrf54l: %s timed out; terminating", cmd[0])
|
||||||
|
proc.kill()
|
||||||
|
return False
|
||||||
|
rc = proc.returncode
|
||||||
|
if rc != 0:
|
||||||
|
log.error("reset_nrf54l: %s exited with code %s; output: %s", cmd[0], rc, (out or b'').decode(errors='ignore'))
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
ok = await _run(cmd)
|
||||||
|
if ok:
|
||||||
|
log.info("reset_nrf54l: reset succeeded (slot %d) using %s", slot, cfg)
|
||||||
|
|
||||||
|
except FileNotFoundError:
|
||||||
|
log.error("reset_nrf54l: openocd not found; skipping reset")
|
||||||
|
except Exception:
|
||||||
|
log.error("reset_nrf54l failed", exc_info=True)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
# Basic logging setup
|
||||||
|
log.basicConfig(
|
||||||
|
level=os.environ.get('LOG_LEVEL', log.INFO),
|
||||||
|
format='%(asctime)s.%(msecs)03d %(levelname)s: %(message)s',
|
||||||
|
datefmt='%Y-%m-%d %H:%M:%S'
|
||||||
|
)
|
||||||
|
|
||||||
|
slot_to_reset = 1
|
||||||
|
log.info(f"Executing reset for slot {slot_to_reset}")
|
||||||
|
asyncio.run(reset_nrf54l(slot=slot_to_reset))
|
||||||
@@ -42,58 +42,47 @@ def _sd_matches_from_names(pa_idx, names):
|
|||||||
if d["hostapi"] != pa_idx or d["max_input_channels"] <= 0:
|
if d["hostapi"] != pa_idx or d["max_input_channels"] <= 0:
|
||||||
continue
|
continue
|
||||||
dn = d["name"].lower()
|
dn = d["name"].lower()
|
||||||
|
# Exclude monitor devices (e.g., "Monitor of ...") to avoid false positives
|
||||||
|
if "monitor" in dn:
|
||||||
|
continue
|
||||||
if any(n in dn for n in names_l):
|
if any(n in dn for n in names_l):
|
||||||
out.append((i, d))
|
out.append((i, d))
|
||||||
return out
|
return out
|
||||||
|
|
||||||
def list_usb_pw_inputs():
|
# Module-level caches for device lists
|
||||||
|
_usb_inputs_cache = []
|
||||||
|
_network_inputs_cache = []
|
||||||
|
|
||||||
|
def get_usb_pw_inputs():
|
||||||
|
"""Return cached list of USB PipeWire inputs."""
|
||||||
|
return _usb_inputs_cache
|
||||||
|
|
||||||
|
def get_network_pw_inputs():
|
||||||
|
"""Return cached list of Network/AES67 PipeWire inputs."""
|
||||||
|
return _network_inputs_cache
|
||||||
|
|
||||||
|
def refresh_pw_cache():
|
||||||
"""
|
"""
|
||||||
Return [(device_index, device_dict), ...] for PipeWire **input** nodes
|
Performs a full device scan and updates the internal caches for both USB
|
||||||
backed by **USB** devices (excludes monitor sources).
|
and Network audio devices. This is a heavy operation and should not be
|
||||||
|
called frequently or during active streams.
|
||||||
"""
|
"""
|
||||||
# Refresh PortAudio so we see newly added nodes before mapping
|
global _usb_inputs_cache, _network_inputs_cache
|
||||||
|
|
||||||
|
# Force PortAudio to re-enumerate devices
|
||||||
_sd_refresh()
|
_sd_refresh()
|
||||||
pa_idx = _pa_like_hostapi_index()
|
pa_idx = _pa_like_hostapi_index()
|
||||||
pw = _pw_dump()
|
pw = _pw_dump()
|
||||||
|
|
||||||
# Map device.id -> device.bus ("usb"/"pci"/"platform"/"network"/...)
|
# --- Pass 1: Map device.id to device.bus ---
|
||||||
device_bus = {}
|
device_bus = {}
|
||||||
for obj in pw:
|
for obj in pw:
|
||||||
if obj.get("type") == "PipeWire:Interface:Device":
|
if obj.get("type") == "PipeWire:Interface:Device":
|
||||||
props = (obj.get("info") or {}).get("props") or {}
|
props = (obj.get("info") or {}).get("props") or {}
|
||||||
device_bus[obj["id"]] = (props.get("device.bus") or "").lower()
|
device_bus[obj["id"]] = (props.get("device.bus") or "").lower()
|
||||||
|
|
||||||
# Collect names/descriptions of USB input nodes
|
# --- Pass 2: Identify all USB and Network nodes ---
|
||||||
usb_input_names = set()
|
usb_input_names = set()
|
||||||
for obj in pw:
|
|
||||||
if obj.get("type") != "PipeWire:Interface:Node":
|
|
||||||
continue
|
|
||||||
props = (obj.get("info") or {}).get("props") or {}
|
|
||||||
media = (props.get("media.class") or "").lower()
|
|
||||||
if "source" not in media and "stream/input" not in media:
|
|
||||||
continue
|
|
||||||
# skip monitor sources ("Monitor of ..." or *.monitor)
|
|
||||||
nname = (props.get("node.name") or "").lower()
|
|
||||||
ndesc = (props.get("node.description") or "").lower()
|
|
||||||
if ".monitor" in nname or "monitor" in ndesc:
|
|
||||||
continue
|
|
||||||
bus = (props.get("device.bus") or device_bus.get(props.get("device.id")) or "").lower()
|
|
||||||
if bus == "usb":
|
|
||||||
usb_input_names.add(props.get("node.description") or props.get("node.name"))
|
|
||||||
|
|
||||||
# Map to sounddevice devices on PipeWire host API
|
|
||||||
return _sd_matches_from_names(pa_idx, usb_input_names)
|
|
||||||
|
|
||||||
def list_network_pw_inputs():
|
|
||||||
"""
|
|
||||||
Return [(device_index, device_dict), ...] for PipeWire **input** nodes that
|
|
||||||
look like network/AES67/RTP sources (excludes monitor sources).
|
|
||||||
"""
|
|
||||||
# Refresh PortAudio so we see newly added nodes before mapping
|
|
||||||
_sd_refresh()
|
|
||||||
pa_idx = _pa_like_hostapi_index()
|
|
||||||
pw = _pw_dump()
|
|
||||||
|
|
||||||
network_input_names = set()
|
network_input_names = set()
|
||||||
for obj in pw:
|
for obj in pw:
|
||||||
if obj.get("type") != "PipeWire:Interface:Node":
|
if obj.get("type") != "PipeWire:Interface:Node":
|
||||||
@@ -102,26 +91,29 @@ def list_network_pw_inputs():
|
|||||||
media = (props.get("media.class") or "").lower()
|
media = (props.get("media.class") or "").lower()
|
||||||
if "source" not in media and "stream/input" not in media:
|
if "source" not in media and "stream/input" not in media:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
nname = (props.get("node.name") or "")
|
nname = (props.get("node.name") or "")
|
||||||
ndesc = (props.get("node.description") or "")
|
ndesc = (props.get("node.description") or "")
|
||||||
# skip monitor sources
|
# Skip all monitor sources
|
||||||
if ".monitor" in nname.lower() or "monitor" in ndesc.lower():
|
if ".monitor" in nname.lower() or "monitor" in ndesc.lower():
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Heuristics for network/AES67/RTP
|
# Check for USB
|
||||||
|
bus = (props.get("device.bus") or device_bus.get(props.get("device.id")) or "").lower()
|
||||||
|
if bus == "usb":
|
||||||
|
usb_input_names.add(ndesc or nname)
|
||||||
|
continue # A device is either USB or Network, not both
|
||||||
|
|
||||||
|
# Heuristics for Network/AES67/RTP
|
||||||
text = (nname + " " + ndesc).lower()
|
text = (nname + " " + ndesc).lower()
|
||||||
media_name = (props.get("media.name") or "").lower()
|
media_name = (props.get("media.name") or "").lower()
|
||||||
node_group = (props.get("node.group") or "").lower()
|
node_group = (props.get("node.group") or "").lower()
|
||||||
# Presence flags/keys that strongly indicate network RTP/AES67 sources
|
|
||||||
node_network_flag = bool(props.get("node.network"))
|
node_network_flag = bool(props.get("node.network"))
|
||||||
has_rtp_keys = any(k in props for k in (
|
has_rtp_keys = any(k in props for k in ("rtp.session", "rtp.source.ip"))
|
||||||
"rtp.session", "rtp.source.ip", "rtp.source.port", "rtp.fmtp", "rtp.rate"
|
has_sess_keys = any(k in props for k in ("sess.name", "sess.media"))
|
||||||
))
|
|
||||||
has_sess_keys = any(k in props for k in (
|
|
||||||
"sess.name", "sess.media", "sess.latency.msec"
|
|
||||||
))
|
|
||||||
is_network = (
|
is_network = (
|
||||||
(props.get("device.bus") or "").lower() == "network" or
|
bus == "network" or
|
||||||
node_network_flag or
|
node_network_flag or
|
||||||
"rtp" in media_name or
|
"rtp" in media_name or
|
||||||
any(k in text for k in ("rtp", "sap", "aes67", "network", "raop", "airplay")) or
|
any(k in text for k in ("rtp", "sap", "aes67", "network", "raop", "airplay")) or
|
||||||
@@ -132,7 +124,13 @@ def list_network_pw_inputs():
|
|||||||
if is_network:
|
if is_network:
|
||||||
network_input_names.add(ndesc or nname)
|
network_input_names.add(ndesc or nname)
|
||||||
|
|
||||||
return _sd_matches_from_names(pa_idx, network_input_names)
|
# --- Final Step: Update caches ---
|
||||||
|
_usb_inputs_cache = _sd_matches_from_names(pa_idx, usb_input_names)
|
||||||
|
_network_inputs_cache = _sd_matches_from_names(pa_idx, network_input_names)
|
||||||
|
|
||||||
|
|
||||||
|
# Populate cache on initial module load
|
||||||
|
refresh_pw_cache()
|
||||||
|
|
||||||
# Example usage:
|
# Example usage:
|
||||||
# for i, d in list_usb_pw_inputs():
|
# for i, d in list_usb_pw_inputs():
|
||||||
|
|||||||
8
src/openocd/raspberrypi-swd0.cfg
Normal file
8
src/openocd/raspberrypi-swd0.cfg
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
adapter driver bcm2835gpio
|
||||||
|
transport select swd
|
||||||
|
adapter gpio swclk 17
|
||||||
|
adapter gpio swdio 18
|
||||||
|
#adapter gpio trst 26
|
||||||
|
#reset_config trst_only
|
||||||
|
|
||||||
|
adapter speed 1000
|
||||||
8
src/openocd/raspberrypi-swd1.cfg
Normal file
8
src/openocd/raspberrypi-swd1.cfg
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
adapter driver bcm2835gpio
|
||||||
|
transport select swd
|
||||||
|
adapter gpio swclk 24
|
||||||
|
adapter gpio swdio 23
|
||||||
|
#adapter gpio trst 27
|
||||||
|
#reset_config trst_only
|
||||||
|
|
||||||
|
adapter speed 1000
|
||||||
26
src/scripts/hit_status.sh
Normal file
26
src/scripts/hit_status.sh
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Usage: ./hit_status.sh [COUNT] [SLEEP_SECONDS]
|
||||||
|
# Always targets http://127.0.0.1:5000/status
|
||||||
|
# Defaults: COUNT=100 SLEEP_SECONDS=0
|
||||||
|
# Example: ./hit_status.sh 100 0.05
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
URL="http://127.0.0.1:5000/status"
|
||||||
|
COUNT="${1:-100}"
|
||||||
|
SLEEP_SECS="${2:-0}"
|
||||||
|
|
||||||
|
# Ensure COUNT is an integer
|
||||||
|
if ! [[ "$COUNT" =~ ^[0-9]+$ ]]; then
|
||||||
|
echo "COUNT must be an integer, got: $COUNT" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
for i in $(seq 1 "$COUNT"); do
|
||||||
|
echo "[$i/$COUNT] GET $URL"
|
||||||
|
curl -sS "$URL" > /dev/null || echo "Request $i failed"
|
||||||
|
# Sleep if non-zero (supports floats, no bc needed)
|
||||||
|
if [[ "$SLEEP_SECS" != "0" && "$SLEEP_SECS" != "0.0" && "$SLEEP_SECS" != "" ]]; then
|
||||||
|
sleep "$SLEEP_SECS"
|
||||||
|
fi
|
||||||
|
done
|
||||||
@@ -21,6 +21,7 @@ context.properties = {
|
|||||||
#log.level = 2
|
#log.level = 2
|
||||||
|
|
||||||
#default.clock.quantum-limit = 8192
|
#default.clock.quantum-limit = 8192
|
||||||
|
default.clock.node = "PTP0-Driver"
|
||||||
}
|
}
|
||||||
|
|
||||||
context.spa-libs = {
|
context.spa-libs = {
|
||||||
@@ -28,7 +29,7 @@ context.spa-libs = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
context.objects = [
|
context.objects = [
|
||||||
# An example clock reading from /dev/ptp0. You can also specify the network interface name,
|
# An example clock reading f16rom /dev/ptp0. You can also specify the network interface name,
|
||||||
# pipewire will query the interface for the current active PHC index. Another option is to
|
# pipewire will query the interface for the current active PHC index. Another option is to
|
||||||
# sync the ptp clock to CLOCK_TAI and then set clock.id = tai, keep in mind that tai may
|
# sync the ptp clock to CLOCK_TAI and then set clock.id = tai, keep in mind that tai may
|
||||||
# also be synced by a NTP client.
|
# also be synced by a NTP client.
|
||||||
@@ -46,8 +47,8 @@ context.objects = [
|
|||||||
clock.interface = "eth0"
|
clock.interface = "eth0"
|
||||||
#clock.device = "/dev/ptp0"
|
#clock.device = "/dev/ptp0"
|
||||||
#clock.id = tai
|
#clock.id = tai
|
||||||
# Lower this in case of periodic out-of-sync
|
# Lower this in case16 of periodic out-of-sync
|
||||||
resync.ms = 1.5
|
resync.ms = 3
|
||||||
object.export = true
|
object.export = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -92,8 +93,8 @@ context.modules = [
|
|||||||
media.class = "Audio/Source"
|
media.class = "Audio/Source"
|
||||||
device.api = aes67
|
device.api = aes67
|
||||||
# You can adjust the latency buffering here. Use integer values only
|
# You can adjust the latency buffering here. Use integer values only
|
||||||
sess.latency.msec = 6
|
sess.latency.msec = 12
|
||||||
node.latency = "144/48000"
|
#node.latency = "192/48000"
|
||||||
node.group = pipewire.ptp0
|
node.group = pipewire.ptp0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,5 +15,4 @@ dscp_general 0
|
|||||||
# QoS for general messages
|
# QoS for general messages
|
||||||
step_threshold 1
|
step_threshold 1
|
||||||
# Fast convergence on time jumps
|
# Fast convergence on time jumps
|
||||||
|
tx_timestamp_timeout 100
|
||||||
tx_timestamp_timeout 20
|
|
||||||
@@ -1,11 +1,16 @@
|
|||||||
[Unit]
|
[Unit]
|
||||||
Description=PipeWire AES67 Service
|
Description=PipeWire AES67 Service
|
||||||
After=network.target
|
After=default.target network-online.target
|
||||||
|
Wants=network-online.target
|
||||||
|
|
||||||
[Service]
|
[Service]
|
||||||
Type=simple
|
Type=simple
|
||||||
ExecStart=/usr/bin/pipewire-aes67 -c /home/caster/bumble-auracast/src/service/aes67/pipewire-aes67.conf
|
ExecStartPre=/bin/sh -lc 'for i in $(seq 1 60); do ip route show default >/dev/null 2>&1 && break; sleep 2; done'
|
||||||
Restart=on-failure
|
ExecStart=/usr/bin/pipewire-aes67 -c /home/caster/bumble-auracast/src/service/aes67/pipewire-aes67.conf
|
||||||
|
Restart=always
|
||||||
|
RestartSec=5s
|
||||||
|
# Avoid StartLimitHit on quick failures during boot; let RestartSec handle pacing
|
||||||
|
StartLimitIntervalSec=0
|
||||||
|
|
||||||
[Install]
|
[Install]
|
||||||
WantedBy=default.target
|
WantedBy=default.target
|
||||||
|
|||||||
@@ -1,22 +1,16 @@
|
|||||||
# This script stops and disables the auracast-server and auracast-frontend services
|
# This script stops and disables the auracast-server and auracast-frontend services
|
||||||
# Requires sudo privileges
|
# Requires sudo privileges
|
||||||
|
|
||||||
echo "Stopping auracast-server.service..."
|
|
||||||
systemctl --user stop auracast-server.service
|
|
||||||
|
|
||||||
echo "Disabling auracast-server.service (user)..."
|
|
||||||
systemctl --user disable auracast-server.service
|
|
||||||
|
|
||||||
echo "Stopping auracast-frontend.service ..."
|
echo "Stopping auracast-frontend.service ..."
|
||||||
sudo systemctl stop auracast-frontend.service
|
sudo systemctl kill auracast-frontend.service
|
||||||
|
|
||||||
echo "Disabling auracast-frontend.service ..."
|
|
||||||
sudo systemctl disable auracast-frontend.service
|
sudo systemctl disable auracast-frontend.service
|
||||||
|
|
||||||
echo "\n--- auracast-server.service status ---"
|
echo "Stopping auracast-server.service..."
|
||||||
systemctl --user status auracast-server.service --no-pager
|
systemctl --user kill auracast-server.service
|
||||||
|
systemctl --user disable auracast-server.service
|
||||||
|
sudo systemctl kill auracast-server.service
|
||||||
|
sudo systemctl disable auracast-server.service
|
||||||
|
sudo rm -f /etc/systemd/system/auracast-server.service
|
||||||
|
|
||||||
echo "\n--- auracast-frontend.service status ---"
|
|
||||||
sudo systemctl status auracast-frontend.service --no-pager
|
sudo systemctl status auracast-frontend.service --no-pager
|
||||||
|
systemctl --user status auracast-server.service --no-pager
|
||||||
echo "auracast-server and auracast-frontend services stopped, disabled, and status printed successfully."
|
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ sudo cp /home/caster/bumble-auracast/src/service/ptp_aes67.service /etc/systemd/
|
|||||||
mkdir -p /home/caster/.config/systemd/user
|
mkdir -p /home/caster/.config/systemd/user
|
||||||
cp /home/caster/bumble-auracast/src/service/pipewire-aes67.service /home/caster/.config/systemd/user/pipewire-aes67.service
|
cp /home/caster/bumble-auracast/src/service/pipewire-aes67.service /home/caster/.config/systemd/user/pipewire-aes67.service
|
||||||
|
|
||||||
# Install PipeWire user config to persist 3ms@48kHz (default.clock.quantum=144)
|
# Install PipeWire user config to persist
|
||||||
mkdir -p /home/caster/.config/pipewire/pipewire.conf.d
|
mkdir -p /home/caster/.config/pipewire/pipewire.conf.d
|
||||||
cp /home/caster/bumble-auracast/src/service/pipewire/99-lowlatency.conf /home/caster/.config/pipewire/pipewire.conf.d/99-lowlatency.conf
|
cp /home/caster/bumble-auracast/src/service/pipewire/99-lowlatency.conf /home/caster/.config/pipewire/pipewire.conf.d/99-lowlatency.conf
|
||||||
|
|
||||||
@@ -25,16 +25,12 @@ systemctl --user enable pipewire-aes67.service
|
|||||||
|
|
||||||
# Restart services
|
# Restart services
|
||||||
systemctl --user restart pipewire.service pipewire-pulse.service
|
systemctl --user restart pipewire.service pipewire-pulse.service
|
||||||
sudo systemctl restart ptp_aes67.service
|
|
||||||
systemctl --user restart pipewire-aes67.service
|
systemctl --user restart pipewire-aes67.service
|
||||||
|
sudo systemctl restart ptp_aes67.service
|
||||||
|
|
||||||
echo "\n--- pipewire.service status (user) ---"
|
# print status
|
||||||
systemctl --user status pipewire.service --no-pager
|
|
||||||
|
|
||||||
echo "\n--- ptp_aes67.service status ---"
|
|
||||||
sudo systemctl status ptp_aes67.service --no-pager
|
sudo systemctl status ptp_aes67.service --no-pager
|
||||||
|
systemctl --user status pipewire.service --no-pager
|
||||||
echo "\n--- pipewire-aes67.service status (user) ---"
|
|
||||||
systemctl --user status pipewire-aes67.service --no-pager
|
systemctl --user status pipewire-aes67.service --no-pager
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ set -e
|
|||||||
# Copy system service file for frontend
|
# Copy system service file for frontend
|
||||||
sudo cp /home/caster/bumble-auracast/src/service/auracast-frontend.service /etc/systemd/system/auracast-frontend.service
|
sudo cp /home/caster/bumble-auracast/src/service/auracast-frontend.service /etc/systemd/system/auracast-frontend.service
|
||||||
|
|
||||||
# Copy user service file for backend (now using WantedBy=default.target)
|
# Copy user service file for backend
|
||||||
mkdir -p /home/caster/.config/systemd/user
|
mkdir -p /home/caster/.config/systemd/user
|
||||||
cp /home/caster/bumble-auracast/src/service/auracast-server.service /home/caster/.config/systemd/user/auracast-server.service
|
cp /home/caster/bumble-auracast/src/service/auracast-server.service /home/caster/.config/systemd/user/auracast-server.service
|
||||||
|
|
||||||
@@ -25,9 +25,6 @@ systemctl --user enable auracast-server.service
|
|||||||
sudo systemctl restart auracast-frontend.service
|
sudo systemctl restart auracast-frontend.service
|
||||||
systemctl --user restart auracast-server.service
|
systemctl --user restart auracast-server.service
|
||||||
|
|
||||||
echo "\n--- auracast-frontend.service status ---"
|
#print status
|
||||||
sudo systemctl status auracast-frontend.service --no-pager
|
sudo systemctl status auracast-frontend.service --no-pager
|
||||||
|
|
||||||
echo "\n--- auracast-server.service status---"
|
|
||||||
systemctl --user status auracast-server.service --no-pager
|
systemctl --user status auracast-server.service --no-pager
|
||||||
echo "auracast-server and auracast-frontend services updated, enabled, restarted, and status printed successfully."
|
|
||||||
|
|||||||
Reference in New Issue
Block a user