Spotify: Advertise across different networks better, Swap Devices More Seemlessly

This commit is contained in:
Danny
2026-10-20 11:44:29 -07:00
parent c777a9ec8a
commit 5814e26a53
15 changed files with 1121 additions and 39 deletions
+1
View File
@@ -15,6 +15,7 @@ openpilot/starpilot/galaxy/web/vendor/vue/vue.esm-browser.js -text
*.apk -text
openpilot/starpilot/galaxy/bin/hev-socks5-tunnel_linux_arm64 -text
openpilot/starpilot/soloist/pulseaudio/bin/pulseaudio -text
openpilot/starpilot/soloist/pulseaudio/bin/pactl -text
openpilot/selfdrive/car/tests/test_models_segs.txt -text
openpilot/common/hardware/comma/updater -text
@@ -38,6 +38,8 @@ assert.equal(serviceLabel({ enabled: false }), "Off")
assert.equal(serviceLabel({ enabled: true, service: { state: "waiting_output", message: "Waiting for Car to connect over Bluetooth." } }),
"Waiting for Car to connect over Bluetooth.")
assert.match(serviceLabel({ enabled: true, service: { state: "waiting", message: "" } }), /pick this device/)
assert.match(serviceLabel({ enabled: true, service: { state: "waiting", message: "", outputWaiting: "Car" } }),
/pick this device.*Audio waits for Car\./)
assert.equal(serviceLabel({ enabled: true, service: { state: "ready", player: { status: "paused" } } }), "Paused")
assert.equal(positionNow({ ms: 1000, at: 10_000, speed: 1 }, 5000, 12_000), 3000)
+15 -5
View File
@@ -99,7 +99,8 @@ export function serviceLabel(setup) {
const service = setup.service
if (!service) return "Starting…"
if (service.state === "ready" && service.player?.status === "paused") return "Paused"
return service.message && !["ready", "playing", "waiting"].includes(service.state) ? service.message : STATE_TEXT[service.state] || "Starting…"
const text = service.message && !["ready", "playing", "waiting"].includes(service.state) ? service.message : STATE_TEXT[service.state] || "Starting…"
return service.outputWaiting ? `${text} Audio waits for ${service.outputWaiting}.` : text
}
// Where the track is now, from Soloist's anchor and the browser time elapsed since it.
@@ -479,8 +480,11 @@ export const SpotifyPage = {
<input type="range" class="gx-slider" min="0" max="100" step="1" :value="volumeDraft ?? player.volume" aria-label="Volume"
@input="volumeDraft = Number($event.target.value)" @change="commitVolume" /></label>
</div>
<button v-if="setup.enabled && player?.loggedIn && !player.active" type="button" class="gx-btn gx-btn--tonal" @click="control('activate')">
<i class="bi bi-broadcast" aria-hidden="true"></i> Play Here</button>
<div v-if="setup.enabled && player?.loggedIn && !player.active" class="gx-stack">
<button type="button" class="gx-btn gx-btn--tonal" @click="control('activate')">
<i class="bi bi-broadcast" aria-hidden="true"></i> Move Spotify to {{ setup.preferences.deviceName || 'StarPilot' }}</button>
<small class="gx-note">Takes over from the phone or speaker Spotify is playing on now. Music plays through {{ outputName }}. You might need to confirm the switch in the Spotify app.</small>
</div>
<GalaxySettingRow :row="switchRow('Spotify', setup.enabled, 'Turn off to stop playing and free up the comma. Your setup stays saved.')"
:index="0" :busy="busy" :save-value="(index, value) => setEnabled(value === 'On')" />
<div class="gx-actions">
@@ -571,7 +575,7 @@ export const SpotifyPage = {
<GalaxySettingRow :row="switchRow('Spotify', setup.enabled, 'Runs Spotify on your comma. Turn off to stop playing; your setup stays saved.')"
:index="0" :busy="busy" :save-value="(index, value) => setEnabled(value === 'On')" />
<p v-if="setup.enabled" class="gx-note" role="status">{{ statusText }}</p>
<p class="gx-note">The first time, your phone has to be on the same Wi‑Fi or hotspot as your comma. After that, Spotify remembers it.</p>
<p class="gx-note">Pair once on the same Wi‑Fi or hotspot. After pairing, keep Spotify enabled and both devices online; your comma can use cellular or a phone hotspot. Use the same Spotify account on your phone.</p>
<form class="gx-stack" @submit.prevent="saveName">
<label class="gx-stack"><span>Name in Spotify</span>
<input class="gx-field" type="text" v-model="nameValue" maxlength="40" autocomplete="off" /></label>
@@ -601,11 +605,17 @@ export const SpotifyPage = {
</details>
<details class="gx-aa-wiki"><summary>Spotify doesn’t show my comma</summary>
<ol>
<li>Make sure step 4 says <strong>Ready to pair</strong>.</li>
<li>Keep Spotify enabled in step 4. A disconnected Bluetooth speaker can wait while you pair; music will be silent until it connects.</li>
<li>Put your phone on the same Wi‑Fi or hotspot as your comma. Some hotspots and guest networks block devices from seeing each other.</li>
<li>For cellular or different networks after pairing, turn off <strong>Local device visibility</strong> in Spotify → Settings → Apps and devices. Both devices need internet access.</li>
<li>On iPhone, allow Spotify’s <strong>Local Network</strong> permission in iPhone Settings. If your phone’s hotspot blocks initial pairing, pair once on another shared Wi‑Fi network.</li>
<li>If it still does not appear, turn Spotify off and back on here. This reconnects using your saved account; you do not need to clear the account or cache.</li>
<li>In Spotify, tap the device icon and look for <strong>{{ setup?.preferences.deviceName || 'StarPilot' }}</strong>.</li>
</ol>
</details>
<details class="gx-aa-wiki"><summary>Can I use the comma hotspot or wireless Android Auto?</summary>
<p>The comma hotspot can provide the local connection for pairing, but the comma also needs internet access to stream. A wireless Android Auto connection alone does not guarantee Spotify discovery or internet access. After pairing, Spotify can use the saved account over the comma’s cellular connection.</p>
</details>
<details class="gx-aa-wiki"><summary>Does it use my data plan?</summary>
<p>Yes, when your comma streams over cellular. Music usually uses about 40–150 MB an hour depending on quality. Songs you’ve played recently come from the cache instead.</p>
</details>
+65 -1
View File
@@ -4,7 +4,8 @@ Galaxy → Spotify and Phone Audio supports two mutually exclusive music sources
Spotify Connect (Soloist) and a paired phone's Bluetooth A2DP audio. Both use the
private PulseAudio output, either Android Auto's PCM media channel or an existing
BlueALSA Bluetooth output. Phone mode needs neither Soloist nor a Spotify API key.
The feature's existing enable flag starts `spotifyd` for either source.
The feature's existing enable flag starts `spotifyd` for either source; with the phone source it
never launches Soloist, and switching to it stops a running Soloist.
## Phone setup
@@ -82,3 +83,66 @@ artist and progress of Spotify or of the phone's Bluetooth player, with a backgr
supervisor publishes the snapshot in its status file; the car screen reads it once a second only while the
card is enabled, and the card stays hidden when nothing is playing or an alert is up. Phone metadata comes
from the phone's Bluetooth media player (BlueZ `MediaPlayer1`), so it depends on the phone sharing it.
## Spotify Connect on hotspots and cellular
Enable Spotify and pair once from Spotify's device picker on a shared local network
(home Wi-Fi, a phone hotspot, or the comma's Galaxy hotspot). Both local discovery
and device-to-device traffic must be allowed. A hotspot-hosting phone may restrict
this traffic; if initial pairing fails, use another shared Wi-Fi network. Wireless
Android Auto by itself is not a promise of local discovery or an internet uplink.
Soloist keeps its device identity and Connect session in `state/` across restarts,
output changes, network handoffs and cache clearing. After pairing, keep both devices
online and use the same Spotify account. On the phone, disable Spotify's **Local
device visibility** under Settings → Apps and devices to allow devices on other
networks. On iPhone also grant Spotify **Local Network** access for local pairing.
The comma needs its own usable internet connection (cellular or tethered internet
is fine); the local Galaxy hotspot alone does not supply one.
The supervisor has no Wi-Fi, unmetered-network or ignition requirement, and it never
restarts Soloist because the network changed. Soloist follows network changes itself:
it binds one mDNS socket on `0.0.0.0:5353` and answers on interfaces that appear after
it starts, because the device's avahi-daemon joins the mDNS group on every interface.
Checked on the device 2026-10-20 with network namespaces: a running Soloist answered
`_spotify-connect._tcp` queries on a link created after launch within ten seconds.
A disconnected selected Bluetooth speaker no longer prevents Soloist from starting.
It uses a private silent sink so the device can be paired and remain online; Galaxy
still shows the pairing state and adds that audio is waiting for the speaker. The
supervisor checks the selected speaker every twenty seconds in the background, so that
periodic check never blocks Galaxy commands. After two checks in a row agree that availability
changed, it uses the vendored `pactl` to load the speaker (or silent) sink into the running audio
server and move Soloist's stream onto it, so Soloist keeps its session, any pairing in progress,
and playback. Soloist plays to the server's default sink rather than a named one, so later streams
follow too. Only if `pactl` is missing or the switch fails does it restart the audio/player with
the saved session.
It never takes over playback from another Spotify device automatically.
The local API is polled for authentication state every fifteen seconds. A response
missing for forty-five seconds reconnects that socket; two minutes without a working
API restarts Soloist without clearing the account. A build that never answers at all
counts as a failed start, so repeated silence still rolls back to the previous build.
Loss of a previously observed login gets the same two-minute grace period, or ten
minutes if it was playing just before (cached music can outlast a coverage gap); an
initial pairing can wait indefinitely. A live API/login is not proof of internet reachability. If discovery still
fails, switch Spotify off/on in Galaxy to refresh it without resetting the account.
These behaviors cannot override phone hotspot isolation, Spotify app permissions,
cloud availability, expired Soloist builds, or a sleeping/powered-off comma.
The optional control WebSocket stays on loopback; exposing it or the Galaxy tunnel
is not how Spotify Connect discovers a device.
References checked 2026-10-20:
- https://developer.spotify.com/documentation/soloist/concepts/authentication
- https://developer.spotify.com/documentation/soloist/tutorials/getting-started
- https://developer.spotify.com/documentation/soloist/reference/websocket-api
- https://support.spotify.com/us/article/spotify-connect/
Hardware acceptance (not covered by unit tests): pair from Android and iPhone on
home Wi-Fi and each phone's hotspot; pair through the Galaxy hotspot with a separate
comma internet uplink; move a paired comma from Wi-Fi to cellular and IPv6 tethering;
control it from a phone on a different network with Local device visibility off;
restart the comma and confirm the saved account returns; connect/disconnect the
selected Bluetooth speaker; verify availability after a tunnel/coverage outage.
Confirm another Spotify player is never activated or interrupted by background recovery.
+46 -7
View File
@@ -17,6 +17,8 @@ soundd and the warning speaker are untouched. Outputs:
from __future__ import annotations
import os
import subprocess
from collections.abc import Callable
from dataclasses import dataclass
from pathlib import Path
@@ -26,6 +28,7 @@ from openpilot.starpilot.soloist.preferences import ADDRESS, Preferences
SERVER_ROOT = Path(__file__).resolve().parent / 'pulseaudio'
PCM_NAME = 'starpilot_spotify'
SINK_NAME = 'spotify'
SILENT_SINK = 'spotify_silent'
SYSTEM_ALSA_CONFIG = Path('/usr/share/alsa/alsa.conf')
# The Android Auto media channel format every head unit must offer; the pipe runs on the system clock,
# so it produces exactly real-time audio (silence when paused) whether or not anyone reads it.
@@ -40,6 +43,10 @@ def bundle_ready(root: Path = SERVER_ROOT) -> bool:
return os.access(root / 'bin' / 'pulseaudio', os.X_OK) and (root / 'modules').is_dir()
def can_switch(root: Path = SERVER_ROOT) -> bool:
return os.access(root / 'bin' / 'pactl', os.X_OK)
def alsa_config(address: str, base: Path = SYSTEM_ALSA_CONFIG) -> str:
"""Process-local ALSA config: the system configuration plus one named BlueALSA A2DP PCM."""
if not ADDRESS.fullmatch(address):
@@ -66,7 +73,17 @@ def android_auto_pipe(layout: Layout | None = None) -> Path:
return runtime_paths(layout or Layout.default())['android_auto']
def server_launch(layout: Layout, prefs: Preferences, *, alsa_base: Path = SYSTEM_ALSA_CONFIG, root: Path = SERVER_ROOT) -> Launch:
def _sink(output: str, paths: dict[str, Path]) -> tuple[str, str]:
if output == 'bluetooth':
return 'module-alsa-sink', f'device={PCM_NAME} sink_name={SINK_NAME} channels=2 tsched=0'
if output == 'android_auto':
return 'module-pipe-sink', (f'file={paths["android_auto"]} sink_name={SINK_NAME} format=s16le rate={PIPE_RATE} ' +
f'channels={PIPE_CHANNELS} use_system_clock_for_timing=yes')
return 'module-null-sink', f'sink_name={SILENT_SINK if output == "silent" else SINK_NAME}'
def server_launch(layout: Layout, prefs: Preferences, *, silent: bool = False, alsa_base: Path = SYSTEM_ALSA_CONFIG,
root: Path = SERVER_ROOT) -> Launch:
paths = runtime_paths(layout)
for key in ('run', 'home', 'pulse'):
private_dir(paths[key])
@@ -77,16 +94,13 @@ def server_launch(layout: Layout, prefs: Preferences, *, alsa_base: Path = SYSTE
'LD_LIBRARY_PATH': str(root / 'lib'), 'PATH': '/usr/bin:/bin',
}
if prefs.output == 'bluetooth':
# Written even when starting silent, so the speaker can be added to the running server later.
paths['alsa'].write_text(alsa_config(prefs.output_address, alsa_base))
os.chmod(paths['alsa'], 0o600)
env['ALSA_CONFIG_PATH'] = str(paths['alsa'])
sink = f'module-alsa-sink device={PCM_NAME} sink_name={SINK_NAME} channels=2 tsched=0'
elif prefs.output == 'android_auto':
paths['android_auto'].unlink(missing_ok=True)
sink = (f'module-pipe-sink file={paths["android_auto"]} sink_name={SINK_NAME} format=s16le rate={PIPE_RATE} ' +
f'channels={PIPE_CHANNELS} use_system_clock_for_timing=yes')
else:
sink = f'module-null-sink sink_name={SINK_NAME}'
sink = ' '.join(_sink('silent' if silent else prefs.output, paths))
argv = [str(root / 'bin' / 'pulseaudio'), '--daemonize=no', '--system=no', '--use-pid-file=no', '-n',
'--exit-idle-time=-1', '--realtime=no', '--high-priority=no', '--disallow-exit', '--log-target=stderr',
'--log-level=notice', f'--dl-search-path={root / "modules"}',
@@ -103,5 +117,30 @@ def player_launch(layout: Layout, prefs: Preferences, api_key: str) -> Launch:
'--data-dir', str(layout.state), '--cache-dir', str(layout.cache), '--cache-size', str(prefs.cache_mb),
'--initial-volume', str(prefs.initial_volume), '--ws', '127.0.0.1:0']
env = {'HOME': str(paths['home']), 'XDG_RUNTIME_DIR': str(paths['run']), 'PATH': '/usr/bin:/bin',
'PULSE_SERVER': f'unix:{paths["socket"]}', 'PULSE_SINK': SINK_NAME}
'PULSE_SERVER': f'unix:{paths["socket"]}'}
return Launch(argv, env)
def _pactl(layout: Layout, root: Path, run: Callable[..., subprocess.CompletedProcess], *args: str) -> str:
paths = runtime_paths(layout)
env = {'HOME': str(paths['home']), 'XDG_RUNTIME_DIR': str(paths['run']), 'PATH': '/usr/bin:/bin',
'PULSE_SERVER': f'unix:{paths["socket"]}'}
return run([str(root / 'bin' / 'pactl'), *args], env=env, capture_output=True, text=True, timeout=5, check=True).stdout
def switch_output(layout: Layout, speaker: bool, *, root: Path = SERVER_ROOT,
run: Callable[..., subprocess.CompletedProcess] = subprocess.run) -> None:
"""Move Soloist between the speaker and the silent sink inside the running server, without restarting it."""
new, old = (SINK_NAME, SILENT_SINK) if speaker else (SILENT_SINK, SINK_NAME)
module, args = _sink('bluetooth' if speaker else 'silent', runtime_paths(layout))
def pactl(*args: str) -> str:
return _pactl(layout, root, run, *args)
pactl('load-module', module, *args.split())
pactl('set-default-sink', new)
for line in pactl('list', 'short', 'sink-inputs').splitlines():
if line.strip():
pactl('move-sink-input', line.split()[0], new)
for line in pactl('list', 'short', 'modules').splitlines():
if f'sink_name={old}' in line.split():
pactl('unload-module', line.split()[0])
+20 -2
View File
@@ -20,6 +20,8 @@ ARTWORK_HOSTS = ('i.scdn.co', 'mosaic.scdn.co', 'image-cdn-ak.spotifycdn.com', '
STATUSES = ('idle', 'playing', 'paused', 'buffering')
REPEATS = ('off', 'context', 'track')
ACTIONS = ('play', 'pause', 'skip_next', 'skip_prev', 'seek', 'shuffle', 'set_repeat')
AUTH_POLL_SECONDS = 15.0
AUTH_TIMEOUT_SECONDS = 45.0
def _text(value: Any) -> str:
@@ -161,8 +163,10 @@ def _connect(url: str):
class PlayerClient:
def __init__(self, data_dir: Path, *, connect: Callable[[str], Any] = _connect, on_change: Callable[[], None] = lambda: None):
def __init__(self, data_dir: Path, *, connect: Callable[[str], Any] = _connect, on_change: Callable[[], None] = lambda: None,
clock: Callable[[], float] = time.monotonic):
self.data_dir, self.connect, self.on_change = data_dir, connect, on_change
self.clock = clock
self.state = PlayerState()
self.lock = threading.Lock()
self.outbox: queue.Queue[dict] = queue.Queue(maxsize=16)
@@ -227,12 +231,20 @@ class PlayerClient:
connection.settimeout(0.25)
with self.lock:
self.state.reset()
self.state.value['connected'] = True
while not self.outbox.empty(): # never replay commands from before a reconnect
self.outbox.get_nowait()
connection.send(json.dumps({'type': 'command', 'command': 'get_auth_state'}))
pending_auth = self.clock()
next_auth = pending_auth + AUTH_POLL_SECONDS
asked_state = False
while not self.stop_event.is_set():
now = self.clock()
if pending_auth is not None and now - pending_auth >= AUTH_TIMEOUT_SECONDS:
raise TimeoutError('Soloist did not answer its local health check')
if pending_auth is None and now >= next_auth:
connection.send(json.dumps({'type': 'command', 'command': 'get_auth_state'}))
pending_auth = now
next_auth = now + AUTH_POLL_SECONDS
try:
while True:
connection.send(json.dumps(self.outbox.get_nowait()))
@@ -254,10 +266,16 @@ class PlayerClient:
continue
if not isinstance(message, dict):
continue
if message.get('type') == 'auth_state':
pending_auth = None
with self.lock:
self.state.apply(message)
if message.get('type') == 'auth_state':
self.state.value['connected'] = True
logged_in = self.state.value['loggedIn']
if logged_in and not asked_state:
asked_state = True
connection.send(json.dumps({'type': 'command', 'command': 'get_state'}))
elif not logged_in:
asked_state = False
self.on_change()
@@ -3,4 +3,5 @@ cdb454b2f42c494dd19e9d097f0f6c3d57befe8e75c1a40c436959c219c6cf2885841f621dfe4050
95e377f3c58f78d65a2e3bf151d6d47d9b6c66dc9a0810babbcd0a47d3a02f16d4522110f8ea92946ef35d004029dc19859a1c61192683377c233d9c5c608740 libsoxr0_0.1.3-4build3_arm64.deb
422d97994b50973e5c4a2b07f7dede00ce15cb9a58fddb6d3e2d530686bb9cc01ee971de5a7b66a4c3ada15f735e6b4405fe22738e230b099bfdf1deb2ab0f24 libspeexdsp1_1.2.1-1ubuntu3_arm64.deb
68345e041b86ca08a6b3be7a8f55abcda1a18537bc15b1f83be781638e53117380c93e52af506ba5aa2b5fc425f4d3dde623b86c3419d9ee3686512e81b8334c libtdb1_1.4.10-1build1_arm64.deb
b871ab5448683f6bb57f898b857fa17a0a1f13b9011b7a0e73dc6f6fc22e47f90880eef99f2f9f94bccc24c8aa2b2f9c9d4d90658e0a4eb446e6964276b98f01 pulseaudio-utils_1%3a16.1+dfsg1-2ubuntu10.1_arm64.deb
7fb6688c36b2d1de13d2f6ac0e45487ee61850fab983a0ee2112630d8e98c9f185ff246ae6e7d753a511bcea36936d48b7954ecdfdf040c91f8cf1661256d72b pulseaudio_1%3a16.1+dfsg1-2ubuntu10.1_arm64.deb
@@ -2,10 +2,12 @@
Spotify Soloist plays only through PulseAudio or PipeWire, and AGNOS ships PulseAudio's client library
without a server. This directory is the minimal server needed to carry Soloist's audio to one output:
the `pulseaudio` daemon, its private libraries, and four modules (native protocol, ALSA sink for
the `pulseaudio` daemon, `pactl` (to switch between the speaker and the silent sink without
restarting Soloist), their private libraries, and four modules (native protocol, ALSA sink for
BlueALSA, null sink, and the real-time pipe sink used for Android Auto).
- Source: Ubuntu 24.04 (noble) arm64 binary packages, `pulseaudio 1:16.1+dfsg1-2ubuntu10.1` and the
- Source: Ubuntu 24.04 (noble) arm64 binary packages, `pulseaudio` and `pulseaudio-utils`
`1:16.1+dfsg1-2ubuntu10.1`, and the
libraries listed in `PACKAGES.sha512` (SHA-512 of each `.deb` as published by Ubuntu).
- The comma's own `libpulse0`/`libpulsecommon-16.1.so` is byte-identical to that Ubuntu build, which is
why only these files are needed. Regenerate with `./update.sh`; `SHA256SUMS` lists the extracted files.
@@ -1,3 +1,4 @@
b3b6955f8437125be03061b575757b255da00f0ac21d5079ee83257bac54326e bin/pactl
c08fc5b6e751bb83af5fc65c052a317d88ed8d506c2ca53a813bfc0a22703c78 bin/pulseaudio
b51278275776288a97699c06f0d377adba603b5e83edac1c7e9bf1891bc41e3e lib/libalsa-util.so
0ad17dfc2ef3f6e322934da1cec6193bfd44ba89c75d77d1e409973cf7f11401 lib/libltdl.so.7
Binary file not shown.
@@ -0,0 +1,605 @@
Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
Upstream-Name: Pulseeaudio
Upstream-Contact: General PulseAudio Discussion <pulseaudio-discuss@lists.freedesktop.org>
Files-Excluded: src/modules/rtp/rfc*
Files: *
Copyright:
Copyright (C) 2004-2009 Lennart Poettering
Copyright (C) 2006-2007 Pierre Ossman <ossman@cendio.se> for Cendio AB
License: LGPL-2.1+
The upstream license clarifies pretty well that the sources of pulseaudio are
LGPL (please see LGPL license grant below), but that some parts will be
effectively GPL since they rely on GPL libraries, quoting the upstream
LICENSE:
.
"""All PulseAudio source files are licensed under the GNU Lesser General
Public License. (see file LGPL for details)
.
However, the server side links to the GPL-only library 'libsamplerate'
which practically downgrades the license of the server part to GPL (see
file GPL for details), exercising section 3 of the LGPL.
.
Hence you should treat the client library ('libpulse') of PulseAudio as
being LGPL licensed and the server part ('libpulsecore') as being GPL
licensed. Since the PulseAudio daemon and the modules link to
'libpulsecore' they are of course also GPL licensed.
.
-- Lennart Poettering, April 20th, 2006."""
.
On Debian systems, the complete text of the LGPL-2.1 can be found in
/usr/share/common-licenses/LGPL-2.1.
Files: src/pulsecore/g711.c
Copyright:
Copyright (C) Sun Microsystems, Inc
License: other
This source code is a product of Sun Microsystems, Inc. and is provided
for unrestricted use. Users may copy or modify this source code without
charge.
.
SUN SOURCE CODE IS PROVIDED AS IS WITH NO WARRANTIES OF ANY KIND INCLUDING
THE WARRANTIES OF DESIGN, MERCHANTIBILITY AND FITNESS FOR A PARTICULAR
PURPOSE, OR ARISING FROM A COURSE OF DEALING, USAGE OR TRADE PRACTICE.
.
Sun source code is provided with no support and without any obligation on
the part of Sun Microsystems, Inc. to assist in its use, correction,
modification or enhancement.
.
SUN MICROSYSTEMS, INC. SHALL HAVE NO LIABILITY WITH RESPECT TO THE
INFRINGEMENT OF COPYRIGHTS, TRADE SECRETS OR ANY PATENTS BY THIS SOFTWARE
OR ANY PART THEREOF.
.
In no event will Sun Microsystems, Inc. be liable for any lost revenue
or profits or other special, indirect and consequential damages, even if
Sun has been advised of the possibility of such damages.
Files: src/pulsecore/g711.h
Copyright:
Copyright (C) 2001 Chris Bagwell
License: other
Permission to use, copy, modify, and distribute this software and its
documentation for any purpose and without fee is hereby granted, provided
that the above copyright notice appear in all copies and that both that
copyright notice and this permission notice appear in supporting
documentation. This software is provided "as is" without express or
implied warranty.
Files: src/pulsecore/poll.*
Copyright:
Copyright 2006 Pierre Ossman <ossman@cendio.se> for Cendio AB
Copyright (C)1994,96,97,98,99,2000,2001,2004 Free Software Foundation, Inc.
License: LGPL-2.1+
On Debian systems, the complete text of the LGPL-2.1 can be found in
/usr/share/common-licenses/LGPL-2.1.
Files: src/pulse/utf8.c
Copyright:
Copyright (C) 1999 Tom Tromey
Copyright (C) 2000 Red Hat, Inc.
License: LGPL-2.1+
On Debian systems, the complete text of the LGPL-2.1 can be found in
/usr/share/common-licenses/LGPL-2.1.
Files: src/modules/bluetooth/ipc.* src/modules/bluetooth/rtp.*
Copyright:
Copyright (C) 2004-2009 Marcel Holtmann <marcel@holtmann.org>
License: LGPL-2.1+
On Debian systems, the complete text of the LGPL-2.1 can be found in
/usr/share/common-licenses/LGPL-2.1.
Files: src/modules/bluetooth/module-bluetooth-device.c
src/modules/bluetooth/module-bluetooth-discover.c
src/modules/bluetooth/bluetooth-util.*
Copyright:
Copyright (C) 2008-2009 Joao Paulo Rechi Vita
License: LGPL-2.1+
On Debian systems, the complete text of the LGPL-2.1 can be found in
/usr/share/common-licenses/LGPL-2.
Files: src/modules/bluetooth/proximity-helper.c
Copyright:
Copyright (C) 2000-2001 Qualcomm Incorporated
Copyright (C) 2002-2003 Maxim Krasnyansky <maxk@qualcomm.com>
Copyright (C) 2002-2007 Marcel Holtmann <marcel@holtmann.org>
License: GPL-2+
On Debian systems, the complete text of the GPL-2 can be found in
/usr/share/common-licenses/GPL-2.
Files: src/modules/bluetooth/sbc*
Copyright:
Copyright (C) 2004-2009 Marcel Holtmann <marcel@holtmann.org>
Copyright (C) 2004-2005 Henryk Ploetz <henryk@ploetzli.ch>
Copyright (C) 2005-2006 Brad Midgley <bmidgley@xmission.com>
License: LGPL-2.1+
On Debian systems, the complete text of the LGPL-2.1 can be found in
/usr/share/common-licenses/LGPL-2.1.
Files: src/modules/dbus/iface-client.c
src/modules/dbus/iface-stream.c:
Copyright:
Copyright 2009 Tanu Kaskinen
Copyright 2009 Vincent Filali-Ansary <filali.v@azurdigitalnetworks.net>
License: LGPL-2.1+
On Debian systems, the complete text of the LGPL-2.1 can be found in
/usr/share/common-licenses/LGPL-2.1.
Files: src/modules/dbus/module-dbus-protocol.c
Copyright:
Copyright 2009 Tanu Kaskinen
Copyright 2006 Lennart Poettering
Copyright 2006 Shams E. King
License: LGPL-2.1+
On Debian systems, the complete text of the LGPL-2.1 can be found in
/usr/share/common-licenses/LGPL-2.1.
Files: src/modules/dbus/*
Copyright:
Copyright 2009 Tanu Kaskinen
License: LGPL-2.1+
On Debian systems, the complete text of the LGPL-2.1 can be found in
/usr/share/common-licenses/LGPL-2.1.
Files: src/modules/echo-cancel/adrian*.*
Copyright:
Copyright (C) DFS Deutsche Flugsicherung (2004). All Rights Reserved.
License: other
You are allowed to use this source code in any open source or closed
source software you want. You are allowed to use the algorithms for a
hardware solution. You are allowed to modify the source code.
You are not allowed to remove the name of the author from this memo or
from the source code files. You are not allowed to monopolize the
source code or the algorithms behind the source code as your
intellectual property. This source code is free of royalty and comes
with no warranty.
Files: src/modules/echo-cancel/echo-cancel.h
Copyright:
Copyright 2010 Arun Raghavan <arun.raghavan@collabora.co.uk>
License: LGPL-2.1+
On Debian systems, the complete text of the LGPL-2.1 can be found in
/usr/share/common-licenses/LGPL-2.1.
Files: src/modules/echo-cancel/module-echo-cancel.c
Copyright:
Copyright 2010 Wim Taymans <wim.taymans@gmail.com>
License: LGPL-2.1+
On Debian systems, the complete text of the LGPL-2.1 can be found in
/usr/share/common-licenses/LGPL-2.1.
Files: src/modules/echo-cancel/speex.c
Copyright:
Copyright 2010 Wim Taymans <wim.taymans@gmail.com>
Contributor: Arun Raghavan <arun.raghavan@collabora.co.uk>
License: LGPL-2.1+
On Debian systems, the complete text of the LGPL-2.1 can be found in
/usr/share/common-licenses/LGPL-2.1.
Files: src/modules/jack/module-jackdbus-detect.c
Copyright:
Written by David Henningsson <david.henningsson@canonical.com>
Copyright 2010 Canonical Ltd.
License: LGPL-2.1+
On Debian systems, the complete text of the LGPL-2.1 can be found in
/usr/share/common-licenses/LGPL-2.1.
Files: src/modules/module-device-manager.*
Copyright:
Copyright 2006-2008 Lennart Poettering
Copyright (C) 2009 Colin Guthrie
License: LGPL-2.1+
On Debian systems, the complete text of the LGPL-2.1 can be found in
/usr/share/common-licenses/LGPL-2.1.
Files: src/modules/module-equalizer-sink.c
Copyright:
Copyright 2004-2008 Lennart Poettering
Copyright 2009 Jason Newton <nevion@gmail.com>
License: LGPL-2.1+
On Debian systems, the complete text of the LGPL-2.1 can be found in
/usr/share/common-licenses/LGPL-2.1.
Files: src/pulsecore/dbus-*.* src/modules/module-hal-detect.c
Copyright:
Copyright 2006 Lennart Poettering
Copyright 2006 Shams E. King
License: LGPL-2.1+
On Debian systems, the complete text of the LGPL-2.1 can be found in
/usr/share/common-licenses/LGPL-2.1.
Files: src/modules/ladspa.h
Copyright:
Copyright (C) 2000-2002 Richard W.E. Furse, Paul Barton-Davis, Stefan
Westerfeld.
License: LGPL-2.1+
On Debian systems, the complete text of the LGPL-2.1 can be found in
/usr/share/common-licenses/LGPL-2.1.
Files: src/modules/module-always-sink.c src/modules/rtp/rtsp_client.*
Copyright:
Copyright (C) 2008 Colin Guthrie
License: LGPL-2.1+
On Debian systems, the complete text of the LGPL-2.1 can be found in
/usr/share/common-licenses/LGPL-2.1.
Files: src/modules/module-detect.c
Copyright:
Copyright 2006 Lennart Poettering
Copyright 2006 Pierre Ossman <ossman@cendio.se> for Cendio AB
Copyright 2006 Diego Petteno
License: LGPL-2.1+
On Debian systems, the complete text of the LGPL-2.1 can be found in
/usr/share/common-licenses/LGPL-2.1.
Files: src/modules/roap/*roap* src/modules/rtp/headerlist.*
Copyright:
Copyright 2005-2007 Lennart Poettering
Copyright (C) 2008 Colin Guthrie
License: LGPL-2.1+
On Debian systems, the complete text of the LGPL-2.1 can be found in
/usr/share/common-licenses/LGPL-2.1.
Files: src/modules/roap/base64.*
Copyright:
Copyright (C) 2008 Colin Guthrie
Copyright (C) Kungliga Tekniska Hogskolan
License: LGPL-2.1+
On Debian systems, the complete text of the LGPL-2.1 can be found in
/usr/share/common-licenses/LGPL-2.1.
Files: src/modules/module-device-manager.c
Copyright:
Copyright 2006-2008 Lennart Poettering
Copyright 2009 Colin Guthrie
License: LGPL-2.1+
On Debian systems, the complete text of the LGPL-2.1 can be found in
/usr/share/common-licenses/LGPL-2.1.
Files: src/modules/module-filter-apply*.c
src/modules/module-filter-heuristics*.c
Copyright:
Copyright 2011 Colin Guthrie
License: LGPL-2.1+
On Debian systems, the complete text of the LGPL-2.1 can be found in
/usr/share/common-licenses/LGPL-2.1.
Files: src/modules/module-solaris.c
Copyright:
Copyright 2006 Lennart Poettering
Copyright 2006-2007 Pierre Ossman <ossman@cendio.se> for Cendio AB
Copyright 2009 Finn Thain
License: LGPL-2.1+
On Debian systems, the complete text of the LGPL-2.1 can be found in
/usr/share/common-licenses/LGPL-2.1.
Files: src/modules/module-switch-on-connect.c
Copyright:
Copyright 2006 Lennart Poettering
Copyright 2009 Canonical Ltd
License: LGPL-2.1+
On Debian systems, the complete text of the LGPL-2.1 can be found in
/usr/share/common-licenses/LGPL-2.1.
Files: src/modules/module-virtual-sink.c
src/modules/module-virtual-source.c
Copyright:
Copyright 2010 Intel Corporation
Contributor: Pierre-Louis Bossart <pierre-louis.bossart@intel.com>
License: LGPL-2.1+
On Debian systems, the complete text of the LGPL-2.1 can be found in
/usr/share/common-licenses/LGPL-2.1.
Files: src/modules/reserve.*
Copyright:
Copyright 2009 (C) Lennart Poettering
License: other
Permission is hereby granted, free of charge, to any person
obtaining a copy of this software and associated documentation files
(the "Software"), to deal in the Software without restriction,
including without limitation the rights to use, copy, modify, merge,
publish, distribute, sublicense, and/or sell copies of the Software,
and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
.
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
Files: src/pulsecore/atomic.h
Copyright:
Copyright (C) 2006-2008 Lennart Poettering
Copyright (C) 2008 Nokia Corporation
License: LGPL-2.1+
On Debian systems, the complete text of the LGPL-2.1 can be found in
/usr/share/common-licenses/LGPL-2.1.
Files: src/pulsecore/core-util.c
Copyright:
Copyright (C) 2004-2006 Lennart Poettering
Copyright (C) 2004 Joe Marcus Clarke
Copyright (C) 2006-2007 Pierre Ossman <ossman@cendio.se> for Cendio AB
License: LGPL-2.1+
On Debian systems, the complete text of the LGPL-2.1 can be found in
/usr/share/common-licenses/LGPL-2.1.
Files: src/pulse/ext-device-manager.*
Copyright:
Copyright (C) 2008 Lennart Poettering
Copyright (C) 2009 Colin Guthrie
License: LGPL-2.1+
On Debian systems, the complete text of the LGPL-2.1 can be found in
/usr/share/common-licenses/LGPL-2.1.
Files: src/pulsecore/ffmpeg/avcodec.h
Copyright:
Copyright (c) 2001 Fabrice Bellard
License: LGPL-2.1+
On Debian systems, the complete text of the LGPL-2.1 can be found in
/usr/share/common-licenses/LGPL-2.1.
Files: src/pulsecore/ffmpeg/resample2.c
Copyright:
Copyright (c) 2004 Michael Niedermayer <michaelni@gmx.at>
License: LGPL-2.1+
On Debian systems, the complete text of the LGPL-2.1 can be found in
/usr/share/common-licenses/LGPL-2.1.
Files: src/pulsecore/socket-util.c
Copyright:
Copyright (C) 2004-2006 Lennart Poettering
Copyright (C) 2004 Joe Marcus Clarke
Copyright (C) 2006-2007 Pierre Ossman <ossman@cendio.se> for Cendio AB
License: LGPL-2.1+
On Debian systems, the complete text of the LGPL-2.1 can be found in
/usr/share/common-licenses/LGPL-2.1.
Files: man/xmltoman
Copyright:
Copyright (C) 2000-2002 Oliver Kurth <oku@masqmail.cx>
Copyright (C) 2003 Lennart Poettering <mzkzygbzna@0pointer.de>
License: LGPL-2+
On Debian systems, the complete text of the LGPL-2 can be found in
/usr/share/common-licenses/LGPL-2.
Files: po/ca.po
Copyright:
Copyright (C) 2008 Xavier Conde Rueda <xavi.conde@gmail.com>
Copyright (C) 2009 Agustí Grau <fletxa@gmail.com>, 2009.
Copyright (C) Judith Pintó Subirada <judithp@gmail.com>
Copyright (C) 2009 Josep Torné Llavall <josep.torne@gmail.com>
License: LGPL-2.1+
On Debian systems, the complete text of the LGPL-2.1 can be found in
/usr/share/common-licenses/LGPL-2.1.
Files: po/cs.po
Copyright:
Copyright (C) 2008,2009 Petr Kovar <pknbe@volny.cz>
License: LGPL-2.1+
On Debian systems, the complete text of the LGPL-2.1 can be found in
/usr/share/common-licenses/LGPL-2.1.
Files: po/de.po
Copyright:
Copyright (C) 2008,2009 Fabian Affolter <fab@fedoraproject.org>
Copyright (C) 2008,2009 Micha Pietsch <barney@fedoraproject.org>
License: LGPL-2.1+
On Debian systems, the complete text of the LGPL-2.1 can be found in
/usr/share/common-licenses/LGPL-2.1.
Files: po/de_CH.po
Copyright:
Copyright (C) 2008, 2009 Fabian Affolter <fab@fedoraproject.org>
Copyright (C) 2008, 2009 Micha Pietsch <barney@fedoraproject.org>
License: LGPL-2.1+
On Debian systems, the complete text of the LGPL-2.1 can be found in
/usr/share/common-licenses/LGPL-2.1.
Files: po/el.po
Copyright:
Copyright (C) 2008 Dimitris Glezos <dimitris@glezos.com>
Copyright (C) 2009 Thalia Papoutsaki <saliyath@gmail.com>
License: LGPL-2.1+
On Debian systems, the complete text of the LGPL-2.1 can be found in
/usr/share/common-licenses/LGPL-2.1.
Files: po/es.po
Copyright:
Copyright (C) 2009 Domingo Becker <domingobecker@gmail.com>
Copyright (C) 2008 Hector Daniel Cabrera <h.daniel.cabrera@gmail.com>
Copyright (C) 2009 Fernando Gonzalez Blanco <fgonz@fedoraproject.org>
License: LGPL-2.1+
On Debian systems, the complete text of the LGPL-2.1 can be found in
/usr/share/common-licenses/LGPL-2.1.
Files: po/fi.po
Copyright:
Copyright (C) 2009 Timo Jyrinki <timo.jyrinki@iki.fi>
Copyright (C) 2009 Ville-Pekka Vainio <vpivaini@cs.helsinki.fi>
License: LGPL-2.1+
On Debian systems, the complete text of the LGPL-2.1 can be found in
/usr/share/common-licenses/LGPL-2.1.
Files: po/fr.po
Copyright:
Copyright (C) 2008 Robert-Andre Mauchin <zebob.m@pengzone.org>
Copyright (C) 2008 Michael Ughetto <telimektar esraonline com>
Copyright (C) 2008 Pablo Martin-Gomez <pablo.martin-gomez@laposte.net>
Copyright (C) 2009 Corentin Perard <corentin.perard@gmail.com>
License: LGPL-2.1+
On Debian systems, the complete text of the LGPL-2.1 can be found in
/usr/share/common-licenses/LGPL-2.1.
Files: po/gu.po
Copyright:
Copyright (C) 2009 Sweta Kothari <swkothar@redhat.com>
License: LGPL-2.1+
On Debian systems, the complete text of the LGPL-2.1 can be found in
/usr/share/common-licenses/LGPL-2.1.
Files: po/hi.po
Copyright:
Copyright (C) 2009 Rajesh Ranjan <rajesh672@gmail.com>
License: LGPL-2.1+
On Debian systems, the complete text of the LGPL-2.1 can be found in
/usr/share/common-licenses/LGPL-2.1.
Files: po/it.po
Copyright:
Copyright (C) 2008,2009 Luca Ferretti <elle.uca@libero.it>
Copyright (C) 2009 Milo Casagrande <milo@ubuntu.com>
Copyright (C) 2009 ario_santagiuliana <mario at marionline.it>
Copyright (C) 2009 Milo Casagrande <milo@ubuntu.com>
License: LGPL-2.1+
On Debian systems, the complete text of the LGPL-2.1 can be found in
/usr/share/common-licenses/LGPL-2.1.
Files: po/ja.po
Copyright:
Copyright (C) 2009 Hyu_gabaru Ryu_ichi <hyu_gabaru@yahoo.co.jp>
License: LGPL-2.1+
On Debian systems, the complete text of the LGPL-2.1 can be found in
/usr/share/common-licenses/LGPL-2.1.
Files: po/kn.po
Copyright:
Copyright (C) 2009 Shankar Prasad <svenkate@redhat.com>
License: LGPL-2.1+
On Debian systems, the complete text of the LGPL-2.1 can be found in
/usr/share/common-licenses/LGPL-2.1.
Files: po/mr.po
Copyright:
Copyright (C) 2009 Sandeep Shedmake <sandeep.shedmake@gmail.com>
Copyright (C) 2009 Sandeep Shedmake <sshedmak@redhat.com>
License: LGPL-2.1+
On Debian systems, the complete text of the LGPL-2.1 can be found in
/usr/share/common-licenses/LGPL-2.1.
Files: po/nl.po
Copyright:
Copyright (C) 2009 Geert Warrink <geert.warrink@onsnet.nu>
Copyright (C) 2009 Reinout van Schouwen <reinout@gmail.com>
License: LGPL-2.1+
On Debian systems, the complete text of the LGPL-2.1 can be found in
/usr/share/common-licenses/LGPL-2.1.
Files: po/or.po
Copyright:
Copyright (C) 2009 Manoj Kumar Giri <mgiri@redhat.com>
License: LGPL-2.1+
On Debian systems, the complete text of the LGPL-2.1 can be found in
/usr/share/common-licenses/LGPL-2.1.
Files: po/pa.po
Copyright:
Copyright (C) 2009 Amanpreet Singh Alam <aalam@users.sf.net>
Copyright (C) 2009 Jaswinder Singh <jsingh@redhat.com>
Copyright (C) 2009 A S Alam <aalam@users.sf.net>
License: LGPL-2.1+
On Debian systems, the complete text of the LGPL-2.1 can be found in
/usr/share/common-licenses/LGPL-2.1.
Files: po/pl.po
Copyright:
Copyright (C) 2008 Piotr Drag <piotrdrag@gmail.com>
License: LGPL-2.1+
On Debian systems, the complete text of the LGPL-2.1 can be found in
/usr/share/common-licenses/LGPL-2.1.
Files: po/pt_BR.po
Copyright:
Copyright (C) 2008 Fabian Affolter <fab@fedoraproject.org>
License: LGPL-2.1+
On Debian systems, the complete text of the LGPL-2.1 can be found in
/usr/share/common-licenses/LGPL-2.1.
Files: po/sr.po po/sr@latin.po
Copyright:
Copyright (C) 2009 Igor Miletic (Игор Милетић) <grejigl-gnomeprevod@yahoo.ca>, 2009.
Copyright (C) 2009 Miloš Komarčević <kmilos@gmail.com>, 2009.
License: LGPL-2.1+
On Debian systems, the complete text of the LGPL-2.1 can be found in
/usr/share/common-licenses/LGPL-2.1.
Files: po/sv.po
Copyright:
Copyright (C) 2008 Daniel Nylander <po@danielnylander.se>
License: LGPL-2.1+
On Debian systems, the complete text of the LGPL-2.1 can be found in
/usr/share/common-licenses/LGPL-2.1.
Files: po/ta.po
Copyright:
Copyright (C) 2009 I. Felix <ifelix@redhat.com>
License: LGPL-2.1+
On Debian systems, the complete text of the LGPL-2.1 can be found in
/usr/share/common-licenses/LGPL-2.1.
Files: po/te.po
Copyright:
Copyright (C) 2009 Krishna Babu K <kkrothap@redhat.com>
License: LGPL-2.1+
On Debian systems, the complete text of the LGPL-2.1 can be found in
/usr/share/common-licenses/LGPL-2.1.
Files: po/uk.po
Copyright:
Copyright (C) 2009 Yuri Chornoivan <yurchor@ukr.net>
License: LGPL-2.1+
On Debian systems, the complete text of the LGPL-2.1 can be found in
/usr/share/common-licenses/LGPL-2.1.
Files: po/zh_CN.po
Copyright:
Copyright (C) 2008 闫丰刚 (sainry)<sainry@gmail.com>
License: LGPL-2.1+
On Debian systems, the complete text of the LGPL-2.1 can be found in
/usr/share/common-licenses/LGPL-2.1.
Files: debian/*
Copyright:
Copyright 2006-2009 Sjoerd Simons <sjoerd@debian.org>
Copyright 2006-2008 CJ van den Berg <cj@vdbonline.com>
License: GPL-2+
On Debian systems, the complete text of the GPL-2 can be found in
/usr/share/common-licenses/GPL-2.
Files: src/pulsecore/cpu-* src/pulsecore/remap*
src/pulsecore/svolume_mmx.c
src/pulsecore/svolume_arm.c
src/pulsecore/svolume_sse.c
Copyright:
Copyright 2004-2006 Lennart Poettering
Copyright 2009 Wim Taymans <wim.taymans@collabora.co.uk>
License: LGPL-2.1+
On Debian systems, the complete text of the LGPL-2.1 can be found in
/usr/share/common-licenses/LGPL-2.1.
Files: src/pulsecore/sconv_sse.c
Copyright:
Copyright 2004-2006 Lennart Poettering
Copyright 2006 Pierre Ossman <ossman@cendio.se> for Cendio AB
License: LGPL-2.1+
On Debian systems, the complete text of the LGPL-2.1 can be found in
/usr/share/common-licenses/LGPL-2.1.
Files: src/pulsecore/usergroup.*
Copyright:
Copyright 2009 Ted Percival
License: LGPL-2.1+
On Debian systems, the complete text of the LGPL-2.1 can be found in
/usr/share/common-licenses/LGPL-2.1.
Files: src/utils/qpaeq
Copyright: 2009 Jason Newton <nevion@gmail.com>
License: LGPL-2.1+
On Debian systems, the complete text of the LGPL-2.1 can be found in
/usr/share/common-licenses/LGPL-2.1.
@@ -8,16 +8,16 @@ docker run --rm --platform linux/arm64 -v "$PWD:/out" ubuntu:24.04 bash -euc '
apt-get -qq update >/dev/null
DEBIAN_FRONTEND=noninteractive apt-get -qq install -y --no-install-recommends libpulse0 libasound2t64 libsndfile1 libwrap0 \
libx11-xcb1 libgomp1 libasyncns0 libsystemd0 libudev1 libdbus-1-3 >/dev/null
cd /tmp && apt-get download pulseaudio libltdl7 liborc-0.4-0t64 libsoxr0 libspeexdsp1 libtdb1 >/dev/null
cd /tmp && apt-get download pulseaudio pulseaudio-utils libltdl7 liborc-0.4-0t64 libsoxr0 libspeexdsp1 libtdb1 >/dev/null
sha512sum *.deb > /out/PACKAGES.sha512
for deb in *.deb; do dpkg-deb -x "$deb" root; done
R=/tmp/root; M=$R/usr/lib/pulse-16.1+dfsg1/modules
rm -rf /out/bin /out/lib /out/modules /out/licenses && mkdir -p /out/bin /out/lib /out/modules /out/licenses
cp $R/usr/bin/pulseaudio /out/bin/
cp $R/usr/bin/pulseaudio $R/usr/bin/pactl /out/bin/
for m in module-native-protocol-unix module-alsa-sink module-null-sink module-pipe-sink; do cp $M/$m.so /out/modules/; done
export LD_LIBRARY_PATH=$R/usr/lib/aarch64-linux-gnu:$R/usr/lib/aarch64-linux-gnu/pulseaudio:$M
for f in /out/bin/pulseaudio /out/modules/*.so; do ldd "$f"; done | awk "/=>/{print \$3}" | sort -u | grep "^$R" |
for f in /out/bin/* /out/modules/*.so; do ldd "$f"; done | awk "/=>/{print \$3}" | sort -u | grep "^$R" |
while read -r lib; do cp -L "$lib" /out/lib/; done
for p in pulseaudio libltdl7 liborc-0.4-0t64 libsoxr0 libspeexdsp1 libtdb1; do cp $R/usr/share/doc/$p/copyright /out/licenses/$p.copyright; done
chmod 755 /out/bin/pulseaudio && chmod 644 /out/lib/* /out/modules/* /out/licenses/*
for p in pulseaudio pulseaudio-utils libltdl7 liborc-0.4-0t64 libsoxr0 libspeexdsp1 libtdb1; do cp -L $R/usr/share/doc/$p/copyright /out/licenses/$p.copyright; done
chmod 755 /out/bin/* && chmod 644 /out/lib/* /out/modules/* /out/licenses/*
cd /out && sha256sum bin/* lib/* modules/* > SHA256SUMS'
+95 -10
View File
@@ -31,6 +31,8 @@ STOP_SECONDS = 5.0
OUTPUT_RETRY_SECONDS = 20.0
PAIRING_PROFILE_SECONDS = 180.0 # the pairing window plus time for the phone to save the comma's services
MAX_FAST_FAILURES = 5
UNRESPONSIVE_SECONDS = 120.0
PLAYING_GRACE_SECONDS = 600.0
LOG_LINES = 200
EXPIRES_IN = re.compile(r'client expires in (\d+) days?')
@@ -128,6 +130,20 @@ class Supervisor:
self.phone_track = None
self.phone_profile = False
self.pairing_profile_until = 0.0
self.output_available = True
self.output_check_at = 0.0
self.output_lock = threading.Lock()
self.output_seen: tuple[int, bool] | None = None
self.output_generation = 0
self.output_checking = False
self.output_flips = 0
self.waiting_for = ''
self.run_async: Callable[[Callable[[], None]], None] = lambda fn: threading.Thread(
target=fn, name='spotify_output_check', daemon=True).start()
self.unresponsive_since: float | None = None
self.authenticated_once = False
self.answered = False
self.last_playing = False
# ---- status ---------------------------------------------------------------------------------------
def _set(self, state: str, message: str = '') -> None:
@@ -146,7 +162,7 @@ class Supervisor:
with self.log_lock:
log = list(self.log)[-20:]
return {'state': state, 'message': self.message, 'daysLeft': self.days_left, 'player': player,
'nowPlaying': now_playing, 'log': log}
'nowPlaying': now_playing, 'outputWaiting': self.waiting_for if self.player is not None else '', 'log': log}
def publish(self) -> None:
try:
@@ -177,6 +193,9 @@ class Supervisor:
return process
def stop_children(self, *, release_phone: bool = True) -> None:
self.unresponsive_since = None
self.output_generation += 1
self.output_seen, self.output_flips, self.waiting_for = None, 0, ''
if self.client is not None:
self.client.stop()
self.client = None
@@ -190,6 +209,45 @@ class Supervisor:
if self.clock() >= self.pairing_profile_until:
self._release_profile()
def _output_ready(self, address: str) -> bool:
with self.output_lock:
return self.output.ready(address)
def _output_changed(self, now: float, address: str) -> bool:
seen, self.output_seen = self.output_seen, None
if seen is not None and seen[0] == self.output_generation:
self.output_flips = self.output_flips + 1 if seen[1] != self.output_available else 0
if self.output_flips >= 2:
return True
if now >= self.output_check_at and not self.output_checking:
self.output_check_at = now + OUTPUT_RETRY_SECONDS
self.output_checking = True
generation = self.output_generation
def check() -> None:
try:
self.output_seen = (generation, self._output_ready(address))
finally:
self.output_checking = False
self.run_async(check)
return False
def _switch_output(self, prefs: preferences.Preferences) -> bool:
"""Move audio to or from the speaker inside the running server, so Soloist keeps its session."""
speaker = not self.output_available
if not backend.can_switch():
return False
try:
backend.switch_output(self.layout, speaker)
except (OSError, subprocess.SubprocessError) as error:
with self.log_lock:
self.log.append(f'audio: could not switch output: {error}')
return False
self.output_available, self.output_flips = speaker, 0
self.waiting_for = '' if speaker else prefs.output_name or 'your Bluetooth speaker'
self.publish()
return True
def _release_profile(self) -> None:
if self.phone_profile:
phone_audio.profile('release')
@@ -225,7 +283,7 @@ class Supervisor:
self._set('waiting_phone', f'Waiting for {prefs.phone_name or "your phone"}. Select the comma as its Bluetooth audio output.')
self.retry_at = now + 5
return
if prefs.output == 'bluetooth' and not self.output.ready(prefs.output_address):
if prefs.output == 'bluetooth' and not self._output_ready(prefs.output_address):
self.stop_children(release_phone=False)
self._set('waiting_output', f'Waiting for {prefs.output_name or "your speaker"} to connect.')
self.retry_at = now + OUTPUT_RETRY_SECONDS
@@ -295,19 +353,23 @@ class Supervisor:
if self.release_target != target:
self.release_target, self.fast_failures, self.retry_at = target, 0, 0.0
if self.player is not None:
if prefs.output == 'bluetooth' and self._output_changed(now, prefs.output_address):
if not self._switch_output(prefs):
self.stop_children()
self.retry_at = 0.0
return
self._watch(now, target)
return
if now < self.retry_at:
return
if prefs.output == 'bluetooth' and not self.output.ready(prefs.output_address):
name = prefs.output_name or 'your Bluetooth speaker'
self._set('waiting_output', f'Waiting for {name} to connect over Bluetooth.')
self.retry_at = now + OUTPUT_RETRY_SECONDS
return
self.output_available = prefs.output != 'bluetooth' or self._output_ready(prefs.output_address)
self.output_check_at = now + OUTPUT_RETRY_SECONDS
self.waiting_for = '' if self.output_available else prefs.output_name or 'your Bluetooth speaker'
# A private null sink keeps Connect available until the speaker returns; never the warning speaker.
self.secret = key
self._set('starting', 'Starting Spotify…')
try:
self.server = self._start(backend.server_launch(self.layout, prefs), 'audio')
self.server = self._start(backend.server_launch(self.layout, prefs, silent=not self.output_available), 'audio')
deadline = now + 5.0
socket_path = backend.runtime_paths(self.layout)['socket']
while not socket_path.exists() and self.server.poll() is None and self.clock() < deadline:
@@ -319,7 +381,7 @@ class Supervisor:
self.stop_children()
self._fail(now, f'Could not start Spotify: {error}')
return
self.launched, self.started_at = launch_key, now
self.launched, self.started_at, self.answered, self.last_playing = launch_key, now, False, False
self.client = self.make_client(self.layout.state)
self.client.on_change = self.publish
self.client.start()
@@ -329,7 +391,29 @@ class Supervisor:
code = self.player.poll() if self.player is not None else None
server_dead = self.server is not None and self.server.poll() is not None
if code is None and not server_dead:
if now - self.started_at > 60:
snapshot = self.client.snapshot() if self.client is not None else {}
connected = bool(snapshot.get('connected'))
self.answered = self.answered or connected
if snapshot.get('loggedIn'):
self.authenticated_once = True
self.last_playing = bool(snapshot.get('active') and snapshot.get('status') in ('playing', 'buffering'))
lost_login = connected and self.authenticated_once and not snapshot.get('loggedIn')
# Logout clears the playback state, so cached music from before a coverage gap gets longer.
limit = PLAYING_GRACE_SECONDS if lost_login and self.last_playing else UNRESPONSIVE_SECONDS
if self.client is not None and (not connected or lost_login):
if self.unresponsive_since is None:
self.unresponsive_since = now
elif now - self.unresponsive_since >= limit:
self.stop_children()
if not self.answered:
self._fail(now, 'Spotify didn’t respond. Retrying…')
return
self.retry_at = now + 10
self._set('retrying', 'Reconnecting Spotify with your saved account…')
return
else:
self.unresponsive_since = None
if self.answered and now - self.started_at > 60:
self.fast_failures = 0
return
self.stop_children()
@@ -393,6 +477,7 @@ class Supervisor:
if command == 'reset_session':
with self.lock:
self.stop_children()
self.authenticated_once = False
shutil.rmtree(self.layout.state, ignore_errors=True)
shutil.rmtree(self.layout.cache, ignore_errors=True)
return {'ok': True}
@@ -0,0 +1,200 @@
import json
import pytest
from openpilot.starpilot.soloist import client, supervisor
from openpilot.starpilot.soloist.tests.test_runtime import make_supervisor, ready_layout
from openpilot.starpilot.soloist.paths import Layout
def test_local_api_failure_recovers_without_rollback_or_erasing_login(tmp_path, monkeypatch):
layout = Layout(tmp_path / 'soloist')
ready_layout(layout, monkeypatch)
monkeypatch.setattr(supervisor, '_kill_group', lambda _p: None)
spawned = []
sup, now = make_supervisor(layout, spawned)
sup.tick()
sup.tick() # the default fake answered once
(layout.state / 'account').write_text('saved')
sup.client.snapshot = lambda: {'connected': False}
sup.tick()
now[0] += 121
sup.tick()
assert sup.player is None and sup.fast_failures == 0
now[0] += 11
sup.tick()
assert len(spawned) == 4
assert (layout.state / 'account').read_text() == 'saved'
def test_auth_heartbeat_refreshes_state_after_relogin(tmp_path):
now = [0.0]
player = client.PlayerClient(tmp_path, clock=lambda: now[0])
events = [True, False, True]
class Connection:
sent = []
def settimeout(self, _timeout): pass
def send(self, message): self.sent.append(json.loads(message)['command'])
def recv(self):
if not events:
player.stop_event.set()
return ''
now[0] += 16
return json.dumps({'type': 'auth_state', 'logged_in': events.pop(0)})
connection = Connection()
player._session(connection)
assert connection.sent.count('get_auth_state') >= 3
assert connection.sent.count('get_state') == 2
def test_unresponsive_socket_is_not_a_healthy_player(tmp_path):
now = [0.0]
player = client.PlayerClient(tmp_path, clock=lambda: now[0])
class Connection:
def settimeout(self, _timeout): pass
def send(self, _message): pass
def recv(self):
now[0] += 16
raise TimeoutError()
with pytest.raises(TimeoutError, match='health check'):
player._session(Connection())
assert not player.snapshot()['connected']
def test_lost_login_recovers_but_first_pairing_waits_without_restart(tmp_path, monkeypatch):
layout = Layout(tmp_path / 'soloist')
ready_layout(layout, monkeypatch)
monkeypatch.setattr(supervisor, '_kill_group', lambda _p: None)
spawned = []
sup, now = make_supervisor(layout, spawned)
sup.tick()
for _ in range(5):
now[0] += 200
sup.tick()
assert len(spawned) == 2 # unlimited time for first pairing
sup.client.snapshot = lambda: {'connected': True, 'loggedIn': True}
sup.tick()
sup.client.snapshot = lambda: {'connected': True, 'loggedIn': False}
sup.tick()
now[0] += 121
sup.tick()
assert sup.player is None
now[0] += 11
sup.tick()
assert len(spawned) == 4
sup.handle({'command': 'reset_session'})
assert not sup.authenticated_once
def test_lost_login_while_playing_gets_a_longer_grace(tmp_path, monkeypatch):
layout = Layout(tmp_path / 'soloist')
ready_layout(layout, monkeypatch)
monkeypatch.setattr(supervisor, '_kill_group', lambda _p: None)
spawned = []
sup, now = make_supervisor(layout, spawned)
sup.tick()
state = client.PlayerState()
state.apply({'type': 'auth_state', 'logged_in': True, 'is_active': True})
state.apply({'type': 'playback_changed', 'status': 'playing'})
state.value['connected'] = True
sup.client.snapshot = lambda: dict(state.value)
sup.tick()
state.apply({'type': 'auth_state', 'logged_in': False}) # the real client clears playback on logout
for _ in range(5):
now[0] += 100
sup.tick()
assert sup.player is not None and len(spawned) == 2
now[0] += supervisor.PLAYING_GRACE_SECONDS
sup.tick()
assert sup.player is None
def test_silent_build_counts_toward_rollback(tmp_path, monkeypatch):
layout = Layout(tmp_path / 'soloist')
ready_layout(layout, monkeypatch)
monkeypatch.setattr(supervisor, '_kill_group', lambda _p: None)
rolled = []
monkeypatch.setattr(supervisor.package, 'rollback', lambda _l: rolled.append(True) or True)
spawned = []
sup, now = make_supervisor(layout, spawned)
def silent(): return {'connected': False}
for _ in range(supervisor.MAX_FAST_FAILURES):
sup.tick()
sup.client.snapshot = silent
sup.tick()
now[0] += supervisor.UNRESPONSIVE_SECONDS + 1
sup.tick()
assert sup.player is None
now[0] += 120
assert rolled == [True]
def test_speaker_check_runs_off_the_tick(tmp_path, monkeypatch):
layout = Layout(tmp_path / 'soloist')
ready_layout(layout, monkeypatch)
supervisor.preferences.save(supervisor.preferences.Preferences(output_address='AA:BB:CC:DD:EE:FF', output_name='Car'), layout)
monkeypatch.setattr(supervisor, '_kill_group', lambda _p: None)
class Output:
calls = 0
def ready(self, _address):
self.calls += 1
return self.calls % 2 == 0 # flaps on every check
spawned, queued = [], []
sup, now = make_supervisor(layout, spawned)
sup.output = Output()
sup.tick()
sup.run_async = queued.append
for _ in range(6):
now[0] += supervisor.OUTPUT_RETRY_SECONDS + 1
sup.tick()
assert len(queued) <= 1 # the tick only queued the blocking check
if queued:
queued.pop()()
assert len(spawned) == 2
def test_phone_source_never_runs_soloist(tmp_path, monkeypatch):
layout = Layout(tmp_path / 'soloist')
ready_layout(layout, monkeypatch)
monkeypatch.setattr(supervisor, '_kill_group', lambda _p: None)
spawned = []
sup, now = make_supervisor(layout, spawned)
sup.tick()
client = sup.client
assert any('soloist' in process.argv[0] for process in spawned)
supervisor.preferences.save(supervisor.preferences.Preferences(source='phone', output='test'), layout)
spawned.clear()
for _ in range(5):
now[0] += 60
sup.tick()
assert client.stopped and sup.client is None
assert not any('soloist' in process.argv[0] for process in spawned)
assert sup.status()['state'] == 'no_phone'
def test_phone_page_never_shows_the_spotify_speaker(tmp_path, monkeypatch):
layout = Layout(tmp_path / 'soloist')
ready_layout(layout, monkeypatch)
monkeypatch.setattr(supervisor, '_kill_group', lambda _p: None)
monkeypatch.setattr(supervisor.phone_audio, 'profile', lambda _action: None)
supervisor.preferences.save(supervisor.preferences.Preferences(output_address='AA:BB:CC:DD:EE:FF', output_name='Car'), layout)
class Output:
connected = False
def ready(self, _address): return self.connected
class Phone:
def powered(self): return True
def ready(self, _address, _now): return True
def track(self, _address): return None
def close(self): pass
spawned = []
sup, now = make_supervisor(layout, spawned)
sup.output, sup.phone = Output(), Phone()
sup.tick()
assert sup.status()['outputWaiting'] == 'Car'
sup.output.connected = True
supervisor.preferences.save(supervisor.preferences.Preferences(source='phone', phone_address='11:22:33:44:55:66',
output_address='AA:BB:CC:DD:EE:FF', output_name='Car'), layout)
now[0] += 1
sup.tick()
assert sup.player is not None and 'soloist' not in sup.player.argv[0]
assert sup.status()['outputWaiting'] == ''
@@ -1,4 +1,5 @@
import os
import subprocess
from pathlib import Path
import pytest
@@ -65,7 +66,10 @@ def test_launch_commands_isolate_the_server_and_player(layout):
assert (layout.runtime / 'alsa.conf').stat().st_mode & 0o777 == 0o600
test = backend.server_launch(layout, preferences.Preferences(output='test'))
assert any(arg.startswith('module-null-sink') for arg in test.argv) and 'ALSA_CONFIG_PATH' not in test.env
silent = backend.server_launch(layout, prefs, silent=True, alsa_base=Path('/usr/share/alsa/alsa.conf'))
assert 'module-null-sink sink_name=spotify_silent' in silent.argv and 'ALSA_CONFIG_PATH' in silent.env
player = backend.player_launch(layout, prefs, 'sk_live_0123456789abcdef')
assert 'PULSE_SINK' not in player.env # follows the server's default sink, so outputs can switch live
assert player.argv[player.argv.index('--ws') + 1] == '127.0.0.1:0'
assert player.argv[player.argv.index('--cache-size') + 1] == '256'
assert player.env['PULSE_SERVER'] == f'unix:{layout.runtime}/pulse/native'
@@ -179,6 +183,7 @@ def make_supervisor(layout, spawned):
return process
now = [100.0]
sup = supervisor.Supervisor(layout, spawn=spawn, clock=lambda: now[0], make_client=FakeClient) # ty: ignore[invalid-argument-type] # test double
sup.run_async = lambda fn: fn()
return sup, now
@@ -247,25 +252,74 @@ def test_supervisor_backs_off_and_rolls_back_a_failing_build(layout, monkeypatch
assert rolled == [True]
def test_supervisor_waits_for_the_selected_bluetooth_output(layout, monkeypatch):
def _speaker_supervisor(layout, monkeypatch, *, can_switch):
ready_layout(layout, monkeypatch)
preferences.save(preferences.Preferences(output_address='AA:BB:CC:DD:EE:FF', output_name='Car'), layout)
class Output:
connected = False
def ready(self, address):
return self.connected
output = Output()
switched = []
monkeypatch.setattr(supervisor.backend, 'can_switch', lambda: can_switch)
monkeypatch.setattr(supervisor.backend, 'switch_output', lambda _layout, speaker: switched.append(speaker))
monkeypatch.setattr(supervisor, '_kill_group', lambda _p: None)
spawned = []
sup, now = make_supervisor(layout, spawned)
sup.output = output
sup.output = Output()
sup.tick()
assert sup.status()['state'] == 'waiting_output' and 'Car' in sup.status()['message'] and spawned == []
output.connected = True
now[0] += supervisor.OUTPUT_RETRY_SECONDS + 1
assert sup.status()['state'] == 'waiting' and sup.status()['outputWaiting'] == 'Car'
assert len(spawned) == 2 and sup.client.started
assert 'module-null-sink sink_name=spotify_silent' in spawned[0].argv
sup.output.connected = True
for _ in range(2): # one check is not enough; the speaker has to stay connected
now[0] += supervisor.OUTPUT_RETRY_SECONDS + 1
sup.tick()
assert len(spawned) == 2 and switched == []
sup.tick()
assert len(spawned) == 2
return sup, now, spawned, switched
def test_speaker_returning_switches_audio_without_restarting_soloist(layout, monkeypatch):
sup, now, spawned, switched = _speaker_supervisor(layout, monkeypatch, can_switch=True)
client = sup.client
assert switched == [True] and len(spawned) == 2 and sup.client is client and not client.stopped
assert sup.status()['outputWaiting'] == ''
sup.output.connected = False
for _ in range(3):
now[0] += supervisor.OUTPUT_RETRY_SECONDS + 1
sup.tick()
assert switched == [True, False] and len(spawned) == 2
assert sup.status()['outputWaiting'] == 'Car'
def test_speaker_switch_falls_back_to_a_restart(layout, monkeypatch):
sup, _now, spawned, switched = _speaker_supervisor(layout, monkeypatch, can_switch=False)
(layout.state / 'saved-session').write_text('retained')
sup.tick()
assert switched == [] and len(spawned) == 4
assert any(arg.startswith('module-alsa-sink') for arg in spawned[2].argv)
assert (layout.state / 'saved-session').read_text() == 'retained'
assert sup.status()['outputWaiting'] == ''
def test_switch_output_moves_streams_and_drops_the_old_sink(layout):
calls = []
outputs = {('list', 'short', 'sink-inputs'): '7\t1\t3\tprotocol-native.c\ts16le 2ch 44100Hz\n',
('list', 'short', 'modules'): ('0\tmodule-native-protocol-unix\tsocket=/x\n' +
'1\tmodule-null-sink\tsink_name=spotify_silent\n' +
'2\tmodule-alsa-sink\tdevice=starpilot_spotify sink_name=spotify channels=2 tsched=0\n')}
def run(argv, **kwargs):
calls.append(tuple(argv[1:]))
assert kwargs['env']['PULSE_SERVER'].startswith('unix:') and kwargs['timeout'] and kwargs['check']
return subprocess.CompletedProcess(argv, 0, outputs.get(tuple(argv[1:]), ''))
backend.switch_output(layout, True, run=run)
assert calls == [('load-module', 'module-alsa-sink', 'device=starpilot_spotify', 'sink_name=spotify', 'channels=2', 'tsched=0'),
('set-default-sink', 'spotify'), ('list', 'short', 'sink-inputs'), ('move-sink-input', '7', 'spotify'),
('list', 'short', 'modules'), ('unload-module', '1')]
calls.clear()
backend.switch_output(layout, False, run=run)
assert calls[0] == ('load-module', 'module-null-sink', 'sink_name=spotify_silent') and calls[-1] == ('unload-module', '2')
def test_control_socket_round_trip(layout, tmp_path):
import threading
from openpilot.starpilot.galaxy.spotify_setup import SpotifyClient