Merge branch 'development'

This commit is contained in:
Mike Brady
2026-06-22 11:54:59 +01:00
99 changed files with 5373 additions and 4472 deletions
@@ -13,7 +13,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6.0.2
- uses: actions/checkout@v7.0.0
- name: Install Dependencies
run: sudo apt-get -y --no-install-recommends install libpipewire-0.3-dev libplist-utils xmltoman libpopt-dev libconfig-dev libasound2-dev avahi-daemon libavahi-client-dev libsoxr-dev libplist-dev libsodium-dev libavutil-dev libavcodec-dev libavformat-dev libgcrypt-dev uuid-dev
- name: Configure
+1 -1
View File
@@ -13,7 +13,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6.0.2
- uses: actions/checkout@v7.0.0
- name: Install Dependencies
run: sudo apt-get -y --no-install-recommends install libglib2.0-dev libplist-utils xmltoman libpopt-dev libconfig-dev libasound2-dev libao-dev libjack-dev libmosquitto-dev avahi-daemon libavahi-client-dev libsoxr-dev libplist-dev libsodium-dev libavutil-dev libavcodec-dev libavformat-dev libgcrypt-dev
- name: Configure
@@ -13,7 +13,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6.0.2
- uses: actions/checkout@v7.0.0
- name: Install Dependencies
run: sudo apt-get -y --no-install-recommends install libpipewire-0.3-dev libplist-utils xmltoman libpopt-dev libconfig-dev libasound2-dev libao-dev libjack-dev libglib2.0-dev libmosquitto-dev avahi-daemon libavahi-client-dev libssl-dev libsoxr-dev libplist-dev libsodium-dev libavutil-dev libavcodec-dev libavformat-dev libgcrypt-dev
- name: Configure
+1 -1
View File
@@ -13,7 +13,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6.0.2
- uses: actions/checkout@v7.0.0
- name: Install Dependencies
run: sudo apt-get -y --no-install-recommends install libpipewire-0.3-dev libplist-utils xmltoman libpopt-dev libdaemon-dev libconfig-dev libasound2-dev libao-dev libjack-dev libglib2.0-dev libmosquitto-dev avahi-daemon libavahi-client-dev libssl-dev libsoxr-dev libplist-dev libsodium-dev libavutil-dev libavcodec-dev libavformat-dev uuid-dev libgcrypt-dev
- name: Configure
@@ -11,7 +11,7 @@ jobs:
build:
runs-on: macos-latest
steps:
- uses: actions/checkout@v6.0.2
- uses: actions/checkout@v7.0.0
- name: Install Dependencies
run: |
brew install automake
@@ -13,7 +13,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6.0.2
- uses: actions/checkout@v7.0.0
- name: Install Dependencies
run: sudo apt-get -y --no-install-recommends install xmltoman libpopt-dev libconfig-dev libasound2-dev avahi-daemon libavahi-client-dev libssl-dev libsoxr-dev
- name: Configure
@@ -13,7 +13,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6.0.2
- uses: actions/checkout@v7.0.0
- name: Install Dependencies
run: sudo apt-get -y --no-install-recommends install xmltoman libpipewire-0.3-dev libpopt-dev libconfig-dev libasound2-dev libao-dev libjack-dev libglib2.0-dev libmosquitto-dev avahi-daemon libavahi-client-dev libssl-dev libsoxr-dev libavutil-dev libavcodec-dev libavformat-dev
- name: Configure
@@ -38,7 +38,7 @@ jobs:
NQPTP_BRANCH=${{ needs.docker-vars.outputs.nqptp_branch }}
steps:
- name: Checkout shairport sync repo
uses: actions/checkout@v6.0.2
uses: actions/checkout@v7.0.0
with:
fetch-depth: 0
@@ -59,7 +59,7 @@ jobs:
fi
- name: Login to Docker Registry
uses: docker/login-action@v4.1.0
uses: docker/login-action@v4.2.0
with:
registry: ${{ secrets.DOCKER_REGISTRY }}
username: ${{ secrets.DOCKER_REGISTRY_USER }}
@@ -67,13 +67,13 @@ jobs:
if: needs.docker-vars.outputs.push_docker_image == 'true'
- name: Set up QEMU
uses: docker/setup-qemu-action@v4.0.0
uses: docker/setup-qemu-action@v4.1.0
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v4.0.0
uses: docker/setup-buildx-action@v4.1.0
- name: Build and push ${{ matrix.name }}
uses: docker/build-push-action@v7.1.0
uses: docker/build-push-action@v7.2.0
env:
registry_and_name: ${{ secrets.DOCKER_REGISTRY }}/${{ secrets.DOCKER_IMAGE_NAME }}
with:
+1 -1
View File
@@ -23,7 +23,7 @@ jobs:
outputs:
nqptp_branch: ${{ env.NQPTP_BRANCH }}
push_docker_image: ${{ env.PUSH_DOCKER_IMAGE }}
docker_platforms: linux/386,linux/amd64,linux/arm/v6,linux/arm/v7,linux/arm64
docker_platforms: linux/386,linux/amd64,linux/arm/v7,linux/arm64
steps:
- name: Push docker image if this is a tag.
if: github.ref_type == 'tag'
+1 -1
View File
@@ -11,7 +11,7 @@ jobs:
stale:
runs-on: ubuntu-latest
steps:
- uses: actions/stale@v10.2.0
- uses: actions/stale@v10.3.0
with:
stale-issue-message: 'This issue has been inactive for 28 days so will be closed 7 days from now. To prevent this, please remove the "stale" label or post a comment.'
stale-pr-message: 'This PR has been inactive for 28 days so will be closed 7 days from now. To prevent this, please remove the "stale" label or post a comment.'
+6 -24
View File
@@ -1,6 +1,9 @@
# Working with PulseAudio or PipeWire
Many Linux systems, especially desktop Linuxes with a GUI, have [PipeWire](https://pipewire.org) or [PulseAudio](https://www.freedesktop.org/wiki/Software/PulseAudio/) installed as [sound servers](https://en.wikipedia.org/wiki/Sound_server).
PipeWire and PulseAudio are widely used and have the great advantage of being easily able to mix audio from multiple sources. The slight downside is that audio may be further processed (e.g. transcoded) on its way to the output device.
PipeWire and PulseAudio are widely used and have the great advantage of being easily able to mix audio from multiple sources.
However, the main thing to remember about PipeWire and PulseAudio sound servers is they only become available when a user logs in -- that is, they are set up as _user services_.
Shairport Sync relies on them and therefore it must also be set up as a user service. Shairport Sync can not be set up as a system service because the PipeWire or PulseAudio services are not available when system services are launched just after system startup.
To use PipeWire or PulseAudio-based systems, Shairport Sync must be set up as a user service.
@@ -10,26 +13,5 @@ To use PipeWire or PulseAudio-based systems, Shairport Sync must be set up as a
3. To check if PipeWire support is built into Shairport Sync, check that the string `PipeWire` is included in the version string. (Enter `$ shairport-sync -V` to get the version string.) Similarly, the version string will include `PulseAudio` if the PulseAudio backend is built in.
4. Remember to specify which backend Shairport Sync should use in the configuration file or on the command line.
## Automatic Startup of Shairport Sync
The main thing to remember about PipeWire and PulseAudio sound servers is that the services they offer only become available when a user logs in -- that is, they are set up as _user services_.
Shairport Sync relies on them, so it must also be set up as a user service; it can not be set up as a system service because the PipeWire or PulseAudio services needed by Shairport Sync are not available when system services are launched just after system startup.
### Starting Shairport Sync as a User Service
To make Shairport Sync start as a user service, (assuming you built and installed Shairport Sync using the BUILD.md guide), ensure you are logged in as the appropriate user and enter:
```
$ systemctl --user enable shairport-sync
```
Make sure it is _not enabled_ as a system service:
```
# systemctl disable shairport-sync
```
and then reboot.
#### Problems with User Service
Shairport Sync will function perfectly well as a user service, but there are a number of things to bear in mind:
1. The AirPlay service will only be available when the user is logged in. When the user logs out, Shairport Sync will terminate and the AirPlay service will disappear.
2. If the Linux system is a desktop Linux with a GUI, audio will be sent to the default output only when the user is logged in through the GUI.
Automatic user login may help address these problems.
## Starting Shairport Sync as a User Service
Please refer to [this](https://github.com/mikebrady/shairport-sync/blob/development/BUILD.md#5-enable-and-start-service) section to discover how to start Shairport Sync as a user service.
+5 -1
View File
@@ -83,8 +83,12 @@ If you are building classic Shairport Sync, the list of packages is shorter:
libpopt-dev libconfig-dev libasound2-dev avahi-daemon libavahi-client-dev libssl-dev libsoxr-dev \
libavutil-dev libavcodec-dev libavformat-dev
```
Building on Ubuntu 24.10 or Debian 13 ("Trixie") and later – and possibly on other distributions – requires `systemd-dev`. Be very careful here if you are using backports -- there is a [report](https://github.com/mikebrady/shairport-sync/issues/2195#issuecomment-4325326002) that installing `systemd-dev` on a backported system can severely damage it:
Building on Ubuntu 24.10 or Debian 13 ("Trixie") and later – and possibly on other distributions – requires `systemd-dev`. Be very careful here if you are using backports -- there is a [report](https://github.com/mikebrady/shairport-sync/issues/2195#issuecomment-4325326002) that installing `systemd-dev` on a backported system can severely damage it.
```
$ apt install --dry-run --no-install-recommends systemd-dev
```
Only if that looks okay, proceed to:
```
# apt install --no-install-recommends systemd-dev
```
Executable → Regular
+63 -195
View File
@@ -25,7 +25,6 @@
#include <cmath>
#include <cstring>
#if defined(AUDIOFFT_APPLE_ACCELERATE)
#define AUDIOFFT_APPLE_ACCELERATE_USED
#include <Accelerate/Accelerate.h>
@@ -41,15 +40,11 @@
#include <vector>
#endif
namespace audiofft {
namespace audiofft
{
namespace detail {
namespace detail
{
class AudioFFTImpl
{
class AudioFFTImpl {
public:
AudioFFTImpl() = default;
AudioFFTImpl(const AudioFFTImpl &) = delete;
@@ -60,38 +55,26 @@ namespace audiofft
virtual void ifft(float *data, const float *re, const float *im) = 0;
};
constexpr bool IsPowerOf2(size_t val)
{
return (val == 1 || (val & (val-1)) == 0);
}
constexpr bool IsPowerOf2(size_t val) { return (val == 1 || (val & (val - 1)) == 0); }
template <typename TypeDest, typename TypeSrc>
void ConvertBuffer(TypeDest* dest, const TypeSrc* src, size_t len)
{
for (size_t i=0; i<len; ++i)
{
void ConvertBuffer(TypeDest *dest, const TypeSrc *src, size_t len) {
for (size_t i = 0; i < len; ++i) {
dest[i] = static_cast<TypeDest>(src[i]);
}
}
template <typename TypeDest, typename TypeSrc, typename TypeFactor>
void ScaleBuffer(TypeDest* dest, const TypeSrc* src, const TypeFactor factor, size_t len)
{
for (size_t i=0; i<len; ++i)
{
void ScaleBuffer(TypeDest *dest, const TypeSrc *src, const TypeFactor factor, size_t len) {
for (size_t i = 0; i < len; ++i) {
dest[i] = static_cast<TypeDest>(static_cast<TypeFactor>(src[i]) * factor);
}
}
} // End of namespace detail
// ================================================================
#ifdef AUDIOFFT_OOURA_USED
/**
@@ -99,25 +82,15 @@ namespace audiofft
* @class OouraFFT
* @brief FFT implementation based on the great radix-4 routines by Takuya Ooura
*/
class OouraFFT : public detail::AudioFFTImpl
{
class OouraFFT : public detail::AudioFFTImpl {
public:
OouraFFT() :
detail::AudioFFTImpl(),
_size(0),
_ip(),
_w(),
_buffer()
{
}
OouraFFT() : detail::AudioFFTImpl(), _size(0), _ip(), _w(), _buffer() {}
OouraFFT(const OouraFFT &) = delete;
OouraFFT &operator=(const OouraFFT &) = delete;
virtual void init(size_t size) override
{
if (_size != size)
{
virtual void init(size_t size) override {
if (_size != size) {
_ip.resize(2 + static_cast<int>(std::sqrt(static_cast<double>(size))));
_w.resize(size / 2);
_buffer.resize(size);
@@ -129,8 +102,7 @@ namespace audiofft
}
}
virtual void fft(const float* data, float* re, float* im) override
{
virtual void fft(const float *data, float *re, float *im) override {
// Convert into the format as required by the Ooura FFT
detail::ConvertBuffer(_buffer.data(), data, _size);
@@ -142,8 +114,7 @@ namespace audiofft
double *bEnd = b + _size;
float *r = re;
float *i = im;
while (b != bEnd)
{
while (b != bEnd) {
*(r++) = static_cast<float>(*(b++));
*(i++) = static_cast<float>(-(*(b++)));
}
@@ -154,16 +125,14 @@ namespace audiofft
im[size2] = 0.0;
}
virtual void ifft(float* data, const float* re, const float* im) override
{
virtual void ifft(float *data, const float *re, const float *im) override {
// Convert into the format as required by the Ooura FFT
{
double *b = _buffer.data();
double *bEnd = b + _size;
const float *r = re;
const float *i = im;
while (b != bEnd)
{
while (b != bEnd) {
*(b++) = static_cast<double>(*(r++));
*(b++) = -static_cast<double>(*(i++));
}
@@ -182,49 +151,37 @@ namespace audiofft
std::vector<double> _w;
std::vector<double> _buffer;
void rdft(int n, int isgn, double *a, int *ip, double *w)
{
void rdft(int n, int isgn, double *a, int *ip, double *w) {
int nw = ip[0];
int nc = ip[1];
if (isgn >= 0)
{
if (n > 4)
{
if (isgn >= 0) {
if (n > 4) {
bitrv2(n, ip + 2, a);
cftfsub(n, a, w);
rftfsub(n, a, nc, w + nw);
}
else if (n == 4)
{
} else if (n == 4) {
cftfsub(n, a, w);
}
double xi = a[0] - a[1];
a[0] += a[1];
a[1] = xi;
}
else
{
} else {
a[1] = 0.5 * (a[0] - a[1]);
a[0] -= a[1];
if (n > 4)
{
if (n > 4) {
rftbsub(n, a, nc, w + nw);
bitrv2(n, ip + 2, a);
cftbsub(n, a, w);
}
else if (n == 4)
{
} else if (n == 4) {
cftfsub(n, a, w);
}
}
}
/* -------- initializing routines -------- */
void makewt(int nw, int *ip, double *w)
{
void makewt(int nw, int *ip, double *w) {
int j, nwh;
double delta, x, y;
@@ -251,9 +208,7 @@ namespace audiofft
}
}
void makect(int nc, int *ip, double *c)
{
void makect(int nc, int *ip, double *c) {
int j, nch;
double delta;
@@ -270,12 +225,9 @@ namespace audiofft
}
}
/* -------- child routines -------- */
void bitrv2(int n, int *ip, double *a)
{
void bitrv2(int n, int *ip, double *a) {
int j, j1, k, k1, l, m, m2;
double xr, xi, yr, yi;
@@ -373,9 +325,7 @@ namespace audiofft
}
}
void cftfsub(int n, double *a, double *w)
{
void cftfsub(int n, double *a, double *w) {
int j, j1, j2, j3, l;
double x0r, x0i, x1r, x1i, x2r, x2i, x3r, x3i;
@@ -423,9 +373,7 @@ namespace audiofft
}
}
void cftbsub(int n, double *a, double *w)
{
void cftbsub(int n, double *a, double *w) {
int j, j1, j2, j3, l;
double x0r, x0i, x1r, x1i, x2r, x2i, x3r, x3i;
@@ -473,9 +421,7 @@ namespace audiofft
}
}
void cft1st(int n, double *a, double *w)
{
void cft1st(int n, double *a, double *w) {
int j, k1, k2;
double wk1r, wk1i, wk2r, wk2i, wk3r, wk3i;
double x0r, x0i, x1r, x1i, x2r, x2i, x3r, x3i;
@@ -578,9 +524,7 @@ namespace audiofft
}
}
void cftmdl(int n, int l, double *a, double *w)
{
void cftmdl(int n, int l, double *a, double *w) {
int j, j1, j2, j3, k, k1, k2, m, m2;
double wk1r, wk1i, wk2r, wk2i, wk3r, wk3i;
double x0r, x0i, x1r, x1i, x2r, x2i, x3r, x3i;
@@ -705,9 +649,7 @@ namespace audiofft
}
}
void rftfsub(int n, double *a, int nc, double *c)
{
void rftfsub(int n, double *a, int nc, double *c) {
int j, k, kk, ks, m;
double wkr, wki, xr, xi, yr, yi;
@@ -730,9 +672,7 @@ namespace audiofft
}
}
void rftbsub(int n, double *a, int nc, double *c)
{
void rftbsub(int n, double *a, int nc, double *c) {
int j, k, kk, ks, m;
double wkr, wki, xr, xi, yr, yi;
@@ -758,53 +698,35 @@ namespace audiofft
}
};
/**
* @internal
* @brief Concrete FFT implementation
*/
typedef OouraFFT AudioFFTImplementation;
#endif // AUDIOFFT_OOURA_USED
// ================================================================
#ifdef AUDIOFFT_APPLE_ACCELERATE_USED
/**
* @internal
* @class AppleAccelerateFFT
* @brief FFT implementation using the Apple Accelerate framework internally
*/
class AppleAccelerateFFT : public detail::AudioFFTImpl
{
class AppleAccelerateFFT : public detail::AudioFFTImpl {
public:
AppleAccelerateFFT() :
detail::AudioFFTImpl(),
_size(0),
_powerOf2(0),
_fftSetup(0),
_re(),
_im()
{
}
AppleAccelerateFFT()
: detail::AudioFFTImpl(), _size(0), _powerOf2(0), _fftSetup(0), _re(), _im() {}
AppleAccelerateFFT(const AppleAccelerateFFT &) = delete;
AppleAccelerateFFT &operator=(const AppleAccelerateFFT &) = delete;
virtual ~AppleAccelerateFFT()
{
init(0);
}
virtual ~AppleAccelerateFFT() { init(0); }
virtual void init(size_t size) override
{
if (_fftSetup)
{
virtual void init(size_t size) override {
if (_fftSetup) {
vDSP_destroy_fftsetup(_fftSetup);
_size = 0;
_powerOf2 = 0;
@@ -813,12 +735,10 @@ namespace audiofft
_im.clear();
}
if (size > 0)
{
if (size > 0) {
_size = size;
_powerOf2 = 0;
while ((1 << _powerOf2) < _size)
{
while ((1 << _powerOf2) < _size) {
++_powerOf2;
}
_fftSetup = vDSP_create_fftsetup(_powerOf2, FFT_RADIX2);
@@ -827,8 +747,7 @@ namespace audiofft
}
}
virtual void fft(const float* data, float* re, float* im) override
{
virtual void fft(const float *data, float *re, float *im) override {
const size_t size2 = _size / 2;
DSPSplitComplex splitComplex;
splitComplex.realp = re;
@@ -843,8 +762,7 @@ namespace audiofft
im[size2] = 0.0f;
}
virtual void ifft(float* data, const float* re, const float* im) override
{
virtual void ifft(float *data, const float *re, const float *im) override {
const size_t size2 = _size / 2;
::memcpy(_re.data(), re, size2 * sizeof(float));
::memcpy(_im.data(), im, size2 * sizeof(float));
@@ -866,57 +784,37 @@ namespace audiofft
std::vector<float> _im;
};
/**
* @internal
* @brief Concrete FFT implementation
*/
typedef AppleAccelerateFFT AudioFFTImplementation;
#endif // AUDIOFFT_APPLE_ACCELERATE_USED
// ================================================================
#ifdef AUDIOFFT_FFTW3_USED
/**
* @internal
* @class FFTW3FFT
* @brief FFT implementation using FFTW3 internally (see fftw.org)
*/
class FFTW3FFT : public detail::AudioFFTImpl
{
class FFTW3FFT : public detail::AudioFFTImpl {
public:
FFTW3FFT() :
detail::AudioFFTImpl(),
_size(0),
_complexSize(0),
_planForward(0),
_planBackward(0),
_data(0),
_re(0),
_im(0)
{
}
FFTW3FFT()
: detail::AudioFFTImpl(), _size(0), _complexSize(0), _planForward(0), _planBackward(0),
_data(0), _re(0), _im(0) {}
FFTW3FFT(const FFTW3FFT &) = delete;
FFTW3FFT &operator=(const FFTW3FFT &) = delete;
virtual ~FFTW3FFT()
{
init(0);
}
virtual ~FFTW3FFT() { init(0); }
virtual void init(size_t size) override
{
if (_size != size)
{
if (_size > 0)
{
virtual void init(size_t size) override {
if (_size != size) {
if (_size > 0) {
fftwf_destroy_plan(_planForward);
fftwf_destroy_plan(_planBackward);
_planForward = 0;
@@ -924,27 +822,23 @@ namespace audiofft
_size = 0;
_complexSize = 0;
if (_data)
{
if (_data) {
fftwf_free(_data);
_data = 0;
}
if (_re)
{
if (_re) {
fftwf_free(_re);
_re = 0;
}
if (_im)
{
if (_im) {
fftwf_free(_im);
_im = 0;
}
}
if (size > 0)
{
if (size > 0) {
_size = size;
_complexSize = AudioFFT::ComplexSize(_size);
@@ -963,16 +857,14 @@ namespace audiofft
}
}
virtual void fft(const float* data, float* re, float* im) override
{
virtual void fft(const float *data, float *re, float *im) override {
::memcpy(_data, data, _size * sizeof(float));
fftwf_execute_split_dft_r2c(_planForward, _data, _re, _im);
::memcpy(re, _re, _complexSize * sizeof(float));
::memcpy(im, _im, _complexSize * sizeof(float));
}
virtual void ifft(float* data, const float* re, const float* im) override
{
virtual void ifft(float *data, const float *re, const float *im) override {
::memcpy(_re, re, _complexSize * sizeof(float));
::memcpy(_im, im, _complexSize * sizeof(float));
fftwf_execute_split_dft_c2r(_planBackward, _re, _im, _data);
@@ -989,53 +881,29 @@ namespace audiofft
float *_im;
};
/**
* @internal
* @brief Concrete FFT implementation
*/
typedef FFTW3FFT AudioFFTImplementation;
#endif // AUDIOFFT_FFTW3_USED
// =============================================================
AudioFFT::AudioFFT() : _impl(new AudioFFTImplementation()) {}
AudioFFT::AudioFFT() :
_impl(new AudioFFTImplementation())
{
}
AudioFFT::~AudioFFT() {}
AudioFFT::~AudioFFT()
{
}
void AudioFFT::init(size_t size)
{
void AudioFFT::init(size_t size) {
assert(detail::IsPowerOf2(size));
_impl->init(size);
}
void AudioFFT::fft(const float *data, float *re, float *im) { _impl->fft(data, re, im); }
void AudioFFT::fft(const float* data, float* re, float* im)
{
_impl->fft(data, re, im);
}
void AudioFFT::ifft(float *data, const float *re, const float *im) { _impl->ifft(data, re, im); }
size_t AudioFFT::ComplexSize(size_t size) { return (size / 2) + 1; }
void AudioFFT::ifft(float* data, const float* re, const float* im)
{
_impl->ifft(data, re, im);
}
size_t AudioFFT::ComplexSize(size_t size)
{
return (size / 2) + 1;
}
} // End of namespace
} // namespace audiofft
Executable → Regular
+14 -18
View File
@@ -22,7 +22,6 @@
#ifndef _AUDIOFFT_H
#define _AUDIOFFT_H
/**
* AudioFFT provides real-to-complex/complex-to-real FFT routines.
*
@@ -30,7 +29,8 @@
*
* - Real-complex FFT and complex-real inverse FFT for power-of-2-sized real data.
*
* - Uniform interface to different FFT implementations (currently Ooura, FFTW3 and Apple Accelerate).
* - Uniform interface to different FFT implementations (currently Ooura, FFTW3 and Apple
* Accelerate).
*
* - Complex data is handled in "split-complex" format, i.e. there are separate
* arrays for the real and imaginary parts which can be useful for SIMD optimizations
@@ -86,29 +86,22 @@
* @endcode
*/
#include <cstddef>
#include <memory>
namespace audiofft {
namespace audiofft
{
namespace detail
{
namespace detail {
class AudioFFTImpl;
}
// =============================================================
/**
* @class AudioFFT
* @brief Performs 1D FFTs
*/
class AudioFFT
{
class AudioFFT {
public:
/**
* @brief Constructor
@@ -132,8 +125,10 @@ namespace audiofft
/**
* @brief Performs the forward FFT
* @param data The real input data (has to be of the length as specified in init())
* @param re The real part of the complex output (has to be of length as returned by ComplexSize())
* @param im The imaginary part of the complex output (has to be of length as returned by ComplexSize())
* @param re The real part of the complex output (has to be of length as returned by
* ComplexSize())
* @param im The imaginary part of the complex output (has to be of length as returned by
* ComplexSize())
*/
void fft(const float *data, float *re, float *im);
@@ -141,7 +136,8 @@ namespace audiofft
* @brief Performs the inverse FFT
* @param data The real output data (has to be of the length as specified in init())
* @param re The real part of the complex input (has to be of length as returned by ComplexSize())
* @param im The imaginary part of the complex input (has to be of length as returned by ComplexSize())
* @param im The imaginary part of the complex input (has to be of length as returned by
* ComplexSize())
*/
void ifft(float *data, const float *re, const float *im);
@@ -156,13 +152,13 @@ namespace audiofft
std::unique_ptr<detail::AudioFFTImpl> _impl;
};
/**
* @deprecated
* @brief Let's keep an AudioFFTBase type around for now because it has been here already in the 1st version in order to avoid breaking existing code.
* @brief Let's keep an AudioFFTBase type around for now because it has been here already in the 1st
* version in order to avoid breaking existing code.
*/
typedef AudioFFT AudioFFTBase;
} // End of namespace
} // namespace audiofft
#endif // Header guard
+4 -1
View File
@@ -140,7 +140,10 @@ void ConvolverThreadPool::clearState(size_t convolverId) {
if (convolverId < _convolvers.size()) {
waitForAll();
} else {
debug(1, "assert(convolverId < _convolvers.size()) failed, with convolverId: %u and _convolvers.size(): %u.", convolverId, _convolvers.size());
debug(1,
"assert(convolverId < _convolvers.size()) failed, with convolverId: %u and "
"_convolvers.size(): %u.",
convolverId, _convolvers.size());
}
}
Executable → Regular
+27 -63
View File
@@ -24,39 +24,17 @@
#include <xmmintrin.h>
#endif
namespace fftconvolver {
namespace fftconvolver
{
FFTConvolver::FFTConvolver()
: _blockSize(0), _segSize(0), _segCount(0), _fftComplexSize(0), _segments(), _segmentsIR(),
_fftBuffer(), _fft(), _preMultiplied(), _conv(), _overlap(), _current(0), _inputBuffer(),
_inputBufferFill(0) {}
FFTConvolver::FFTConvolver() :
_blockSize(0),
_segSize(0),
_segCount(0),
_fftComplexSize(0),
_segments(),
_segmentsIR(),
_fftBuffer(),
_fft(),
_preMultiplied(),
_conv(),
_overlap(),
_current(0),
_inputBuffer(),
_inputBufferFill(0)
{
}
FFTConvolver::~FFTConvolver() { reset(); }
FFTConvolver::~FFTConvolver()
{
reset();
}
void FFTConvolver::reset()
{
for (size_t i=0; i<_segCount; ++i)
{
void FFTConvolver::reset() {
for (size_t i = 0; i < _segCount; ++i) {
delete _segments[i];
delete _segmentsIR[i];
}
@@ -77,10 +55,8 @@ void FFTConvolver::reset()
_inputBufferFill = 0;
}
void FFTConvolver::clearState()
{
if (_segCount == 0)
{
void FFTConvolver::clearState() {
if (_segCount == 0) {
return; // Not initialized
}
@@ -88,8 +64,7 @@ void FFTConvolver::clearState()
_inputBufferFill = 0;
_overlap.setZero();
for (size_t i = 0; i < _segCount; ++i)
{
for (size_t i = 0; i < _segCount; ++i) {
_segments[i]->setZero();
}
@@ -98,29 +73,26 @@ void FFTConvolver::clearState()
_current = 0;
}
bool FFTConvolver::init(size_t blockSize, const Sample* ir, size_t irLen)
{
bool FFTConvolver::init(size_t blockSize, const Sample *ir, size_t irLen) {
reset();
if (blockSize == 0)
{
if (blockSize == 0) {
return false;
}
// Ignore zeros at the end of the impulse response because they only waste computation time
while (irLen > 0 && ::fabs(ir[irLen-1]) < 0.000001f)
{
while (irLen > 0 && ::fabs(ir[irLen - 1]) < 0.000001f) {
--irLen;
}
if (irLen == 0)
{
if (irLen == 0) {
return true;
}
_blockSize = NextPowerOf2(blockSize);
_segSize = 2 * _blockSize;
_segCount = static_cast<size_t>(::ceil(static_cast<float>(irLen) / static_cast<float>(_blockSize)));
_segCount =
static_cast<size_t>(::ceil(static_cast<float>(irLen) / static_cast<float>(_blockSize)));
_fftComplexSize = audiofft::AudioFFT::ComplexSize(_segSize);
// FFT
@@ -128,14 +100,12 @@ bool FFTConvolver::init(size_t blockSize, const Sample* ir, size_t irLen)
_fftBuffer.resize(_segSize);
// Prepare segments
for (size_t i=0; i<_segCount; ++i)
{
for (size_t i = 0; i < _segCount; ++i) {
_segments.push_back(new SplitComplex(_fftComplexSize));
}
// Prepare IR
for (size_t i=0; i<_segCount; ++i)
{
for (size_t i = 0; i < _segCount; ++i) {
SplitComplex *segment = new SplitComplex(_fftComplexSize);
const size_t remaining = irLen - (i * _blockSize);
const size_t sizeCopy = (remaining >= _blockSize) ? _blockSize : remaining;
@@ -159,18 +129,14 @@ bool FFTConvolver::init(size_t blockSize, const Sample* ir, size_t irLen)
return true;
}
void FFTConvolver::process(const Sample* input, Sample* output, size_t len)
{
if (_segCount == 0)
{
void FFTConvolver::process(const Sample *input, Sample *output, size_t len) {
if (_segCount == 0) {
::memset(output, 0, len * sizeof(Sample));
return;
}
size_t processed = 0;
while (processed < len)
{
while (processed < len) {
const bool inputBufferWasEmpty = (_inputBufferFill == 0);
const size_t processing = std::min(len - processed, _blockSize - _inputBufferFill);
const size_t inputBufferPos = _inputBufferFill;
@@ -181,11 +147,9 @@ void FFTConvolver::process(const Sample* input, Sample* output, size_t len)
_fft.fft(_fftBuffer.data(), _segments[_current]->re(), _segments[_current]->im());
// Complex multiplication
if (inputBufferWasEmpty)
{
if (inputBufferWasEmpty) {
_preMultiplied.setZero();
for (size_t i=1; i<_segCount; ++i)
{
for (size_t i = 1; i < _segCount; ++i) {
const size_t indexIr = i;
const size_t indexAudio = (_current + i) % _segCount;
ComplexMultiplyAccumulate(_preMultiplied, *_segmentsIR[indexIr], *_segments[indexAudio]);
@@ -198,12 +162,12 @@ void FFTConvolver::process(const Sample* input, Sample* output, size_t len)
_fft.ifft(_fftBuffer.data(), _conv.re(), _conv.im());
// Add overlap
Sum(output+processed, _fftBuffer.data()+inputBufferPos, _overlap.data()+inputBufferPos, processing);
Sum(output + processed, _fftBuffer.data() + inputBufferPos, _overlap.data() + inputBufferPos,
processing);
// Input buffer full => Next block
_inputBufferFill += processing;
if (_inputBufferFill == _blockSize)
{
if (_inputBufferFill == _blockSize) {
// Input buffer is empty again now
_inputBuffer.setZero();
_inputBufferFill = 0;
Executable → Regular
+2 -5
View File
@@ -27,9 +27,7 @@
#include <vector>
namespace fftconvolver
{
namespace fftconvolver {
/**
* @class FFTConvolver
@@ -50,8 +48,7 @@ namespace fftconvolver
* performed during processing (all necessary allocations and preparations take
* place during initialization).
*/
class FFTConvolver
{
class FFTConvolver {
public:
FFTConvolver();
virtual ~FFTConvolver();
+15 -33
View File
@@ -21,12 +21,9 @@
#include "Utilities.h"
namespace fftconvolver {
namespace fftconvolver
{
bool SSEEnabled()
{
bool SSEEnabled() {
#if defined(FFTCONVOLVER_USE_SSE)
return true;
#else
@@ -34,47 +31,35 @@ bool SSEEnabled()
#endif
}
void Sum(Sample* FFTCONVOLVER_RESTRICT result,
const Sample* FFTCONVOLVER_RESTRICT a,
const Sample* FFTCONVOLVER_RESTRICT b,
size_t len)
{
void Sum(Sample *FFTCONVOLVER_RESTRICT result, const Sample *FFTCONVOLVER_RESTRICT a,
const Sample *FFTCONVOLVER_RESTRICT b, size_t len) {
const size_t end4 = 4 * (len / 4);
for (size_t i=0; i<end4; i+=4)
{
for (size_t i = 0; i < end4; i += 4) {
result[i + 0] = a[i + 0] + b[i + 0];
result[i + 1] = a[i + 1] + b[i + 1];
result[i + 2] = a[i + 2] + b[i + 2];
result[i + 3] = a[i + 3] + b[i + 3];
}
for (size_t i=end4; i<len; ++i)
{
for (size_t i = end4; i < len; ++i) {
result[i] = a[i] + b[i];
}
}
void ComplexMultiplyAccumulate(SplitComplex& result, const SplitComplex& a, const SplitComplex& b)
{
void ComplexMultiplyAccumulate(SplitComplex &result, const SplitComplex &a, const SplitComplex &b) {
assert(result.size() == a.size());
assert(result.size() == b.size());
ComplexMultiplyAccumulate(result.re(), result.im(), a.re(), a.im(), b.re(), b.im(), result.size());
ComplexMultiplyAccumulate(result.re(), result.im(), a.re(), a.im(), b.re(), b.im(),
result.size());
}
void ComplexMultiplyAccumulate(Sample* FFTCONVOLVER_RESTRICT re,
Sample* FFTCONVOLVER_RESTRICT im,
void ComplexMultiplyAccumulate(Sample *FFTCONVOLVER_RESTRICT re, Sample *FFTCONVOLVER_RESTRICT im,
const Sample *FFTCONVOLVER_RESTRICT reA,
const Sample *FFTCONVOLVER_RESTRICT imA,
const Sample *FFTCONVOLVER_RESTRICT reB,
const Sample* FFTCONVOLVER_RESTRICT imB,
const size_t len)
{
const Sample *FFTCONVOLVER_RESTRICT imB, const size_t len) {
#if defined(FFTCONVOLVER_USE_SSE)
const size_t end4 = 4 * (len / 4);
for (size_t i=0; i<end4; i+=4)
{
for (size_t i = 0; i < end4; i += 4) {
const __m128 ra = _mm_load_ps(&reA[i]);
const __m128 rb = _mm_load_ps(&reB[i]);
const __m128 ia = _mm_load_ps(&imA[i]);
@@ -88,15 +73,13 @@ void ComplexMultiplyAccumulate(Sample* FFTCONVOLVER_RESTRICT re,
imag = _mm_add_ps(imag, _mm_mul_ps(ia, rb));
_mm_store_ps(&im[i], imag);
}
for (size_t i=end4; i<len; ++i)
{
for (size_t i = end4; i < len; ++i) {
re[i] += reA[i] * reB[i] - imA[i] * imB[i];
im[i] += reA[i] * imB[i] + imA[i] * reB[i];
}
#else
const size_t end4 = 4 * (len / 4);
for (size_t i=0; i<end4; i+=4)
{
for (size_t i = 0; i < end4; i += 4) {
re[i + 0] += reA[i + 0] * reB[i + 0] - imA[i + 0] * imB[i + 0];
re[i + 1] += reA[i + 1] * reB[i + 1] - imA[i + 1] * imB[i + 1];
re[i + 2] += reA[i + 2] * reB[i + 2] - imA[i + 2] * imB[i + 2];
@@ -106,8 +89,7 @@ void ComplexMultiplyAccumulate(Sample* FFTCONVOLVER_RESTRICT re,
im[i + 2] += reA[i + 2] * imB[i + 2] + imA[i + 2] * reB[i + 2];
im[i + 3] += reA[i + 3] * imB[i + 3] + imA[i + 3] * reB[i + 3];
}
for (size_t i=end4; i<len; ++i)
{
for (size_t i = end4; i < len; ++i) {
re[i] += reA[i] * reB[i] - imA[i] * imB[i];
im[i] += reA[i] * imB[i] + imA[i] * reB[i];
}
+39 -127
View File
@@ -28,9 +28,7 @@
#include <cstring>
#include <new>
namespace fftconvolver
{
namespace fftconvolver {
#if defined(__SSE__) || (defined(_M_IX86_FP) && _M_IX86_FP >= 2)
#if !defined(FFTCONVOLVER_USE_SSE) && !defined(FFTCONVOLVER_DONT_USE_SSE)
@@ -38,61 +36,43 @@ namespace fftconvolver
#endif
#endif
#if defined(FFTCONVOLVER_USE_SSE)
#include <xmmintrin.h>
#endif
#if defined(__GNUC__)
#define FFTCONVOLVER_RESTRICT __restrict__
#else
#define FFTCONVOLVER_RESTRICT
#endif
/**
* @brief Returns whether SSE optimization for the convolver is enabled
* @return true: Enabled - false: Disabled
*/
bool SSEEnabled();
/**
* @class Buffer
* @brief Simple buffer implementation (uses 16-byte alignment if SSE optimization is enabled)
*/
template<typename T>
class Buffer
{
template <typename T> class Buffer {
public:
explicit Buffer(size_t initialSize = 0) :
_data(0),
_size(0)
{
resize(initialSize);
}
explicit Buffer(size_t initialSize = 0) : _data(0), _size(0) { resize(initialSize); }
virtual ~Buffer()
{
clear();
}
virtual ~Buffer() { clear(); }
void clear()
{
void clear() {
deallocate(_data);
_data = 0;
_size = 0;
}
void resize(size_t size)
{
if (_size != size)
{
void resize(size_t size) {
if (_size != size) {
clear();
if (size > 0)
{
if (size > 0) {
assert(!_data && _size == 0);
_data = allocate(size);
_size = size;
@@ -101,61 +81,40 @@ public:
setZero();
}
size_t size() const
{
return _size;
}
size_t size() const { return _size; }
void setZero()
{
::memset(_data, 0, _size * sizeof(T));
}
void setZero() { ::memset(_data, 0, _size * sizeof(T)); }
void copyFrom(const Buffer<T>& other)
{
void copyFrom(const Buffer<T> &other) {
assert(_size == other._size);
if (this != &other)
{
if (this != &other) {
::memcpy(_data, other._data, _size * sizeof(T));
}
}
T& operator[](size_t index)
{
T &operator[](size_t index) {
assert(_data && index < _size);
return _data[index];
}
const T& operator[](size_t index) const
{
const T &operator[](size_t index) const {
assert(_data && index < _size);
return _data[index];
}
operator bool() const
{
return (_data != 0 && _size > 0);
}
operator bool() const { return (_data != 0 && _size > 0); }
T* data()
{
return _data;
}
T *data() { return _data; }
const T* data() const
{
return _data;
}
const T *data() const { return _data; }
static void Swap(Buffer<T>& a, Buffer<T>& b)
{
static void Swap(Buffer<T> &a, Buffer<T> &b) {
std::swap(a._data, b._data);
std::swap(a._size, b._size);
}
private:
T* allocate(size_t size)
{
T *allocate(size_t size) {
#if defined(FFTCONVOLVER_USE_SSE)
return static_cast<T *>(_mm_malloc(size * sizeof(T), 16));
#else
@@ -163,8 +122,7 @@ private:
#endif
}
void deallocate(T* ptr)
{
void deallocate(T *ptr) {
#if defined(FFTCONVOLVER_USE_SSE)
_mm_free(ptr);
#else
@@ -180,19 +138,16 @@ private:
Buffer &operator=(const Buffer &);
};
/**
* @brief Type of one sample
*/
typedef float Sample;
/**
* @brief Buffer for samples
*/
typedef Buffer<Sample> SampleBuffer;
/**
* @class SplitComplex
* @brief Buffer for split-complex representation of FFT results
@@ -201,72 +156,43 @@ typedef Buffer<Sample> SampleBuffer;
* of FFT results in two different memory buffers which is useful e.g. for
* SIMD optimizations.
*/
class SplitComplex
{
class SplitComplex {
public:
explicit SplitComplex(size_t initialSize = 0) :
_size(0),
_re(),
_im()
{
resize(initialSize);
}
explicit SplitComplex(size_t initialSize = 0) : _size(0), _re(), _im() { resize(initialSize); }
~SplitComplex()
{
clear();
}
~SplitComplex() { clear(); }
void clear()
{
void clear() {
_re.clear();
_im.clear();
_size = 0;
}
void resize(size_t newSize)
{
void resize(size_t newSize) {
_re.resize(newSize);
_im.resize(newSize);
_size = newSize;
}
void setZero()
{
void setZero() {
_re.setZero();
_im.setZero();
}
void copyFrom(const SplitComplex& other)
{
void copyFrom(const SplitComplex &other) {
_re.copyFrom(other._re);
_im.copyFrom(other._im);
}
Sample* re()
{
return _re.data();
}
Sample *re() { return _re.data(); }
const Sample* re() const
{
return _re.data();
}
const Sample *re() const { return _re.data(); }
Sample* im()
{
return _im.data();
}
Sample *im() { return _im.data(); }
const Sample* im() const
{
return _im.data();
}
const Sample *im() const { return _im.data(); }
size_t size() const
{
return _size;
}
size_t size() const { return _size; }
private:
size_t _size;
@@ -278,24 +204,19 @@ private:
SplitComplex &operator=(const SplitComplex &);
};
/**
* @brief Returns the next power of 2 of a given number
* @param val The number
* @return The next power of 2
*/
template<typename T>
T NextPowerOf2(const T& val)
{
template <typename T> T NextPowerOf2(const T &val) {
T nextPowerOf2 = 1;
while (nextPowerOf2 < val)
{
while (nextPowerOf2 < val) {
nextPowerOf2 *= 2;
}
return nextPowerOf2;
}
/**
* @brief Sums two given sample arrays
* @param result The result array
@@ -303,11 +224,8 @@ T NextPowerOf2(const T& val)
* @param b The 2nd array
* @param len The length of the arrays
*/
void Sum(Sample* FFTCONVOLVER_RESTRICT result,
const Sample* FFTCONVOLVER_RESTRICT a,
const Sample* FFTCONVOLVER_RESTRICT b,
size_t len);
void Sum(Sample *FFTCONVOLVER_RESTRICT result, const Sample *FFTCONVOLVER_RESTRICT a,
const Sample *FFTCONVOLVER_RESTRICT b, size_t len);
/**
* @brief Copies a source array into a destination buffer and pads the destination buffer with zeros
@@ -315,15 +233,12 @@ void Sum(Sample* FFTCONVOLVER_RESTRICT result,
* @param src The source array
* @param srcSize The size of the source array
*/
template<typename T>
void CopyAndPad(Buffer<T>& dest, const T* src, size_t srcSize)
{
template <typename T> void CopyAndPad(Buffer<T> &dest, const T *src, size_t srcSize) {
assert(dest.size() >= srcSize);
::memcpy(dest.data(), src, srcSize * sizeof(T));
::memset(dest.data() + srcSize, 0, (dest.size() - srcSize) * sizeof(T));
}
/**
* @brief Adds the complex product of two split-complex buffers to a result buffer
* @param result The result buffer
@@ -332,7 +247,6 @@ void CopyAndPad(Buffer<T>& dest, const T* src, size_t srcSize)
*/
void ComplexMultiplyAccumulate(SplitComplex &result, const SplitComplex &a, const SplitComplex &b);
/**
* @brief Adds the complex product of two split-complex arrays to a result array
* @param re The real part of the result buffer
@@ -342,13 +256,11 @@ void ComplexMultiplyAccumulate(SplitComplex& result, const SplitComplex& a, cons
* @param reB The real part of the 2nd factor of the complex product
* @param imB The imaginary part of the 2nd factor of the complex product
*/
void ComplexMultiplyAccumulate(Sample* FFTCONVOLVER_RESTRICT re,
Sample* FFTCONVOLVER_RESTRICT im,
void ComplexMultiplyAccumulate(Sample *FFTCONVOLVER_RESTRICT re, Sample *FFTCONVOLVER_RESTRICT im,
const Sample *FFTCONVOLVER_RESTRICT reA,
const Sample *FFTCONVOLVER_RESTRICT imA,
const Sample *FFTCONVOLVER_RESTRICT reB,
const Sample* FFTCONVOLVER_RESTRICT imB,
const size_t len);
const Sample *FFTCONVOLVER_RESTRICT imB, const size_t len);
} // End of namespace fftconvolver
+5 -7
View File
@@ -46,8 +46,8 @@ void convolver_pool_init(size_t numThreads, size_t numConvolvers) {
if (!pool.init(numThreads, numConvolvers)) {
debug(1, "failed to initialize thread pool!");
} else {
debug(1, "thread pool initialized with %u thread%s and %u convolver%s.", numThreads, numThreads == 1 ? "" : "s",
numConvolvers, numConvolvers == 1 ? "" : "s");
debug(1, "thread pool initialized with %u thread%s and %u convolver%s.", numThreads,
numThreads == 1 ? "" : "s", numConvolvers, numConvolvers == 1 ? "" : "s");
}
}
@@ -56,8 +56,8 @@ void convolver_pool_closedown() {
debug(3, "thread pool shut down");
}
int convolver_init(const char *filename, unsigned char channel_count,
double max_length_in_seconds, size_t block_size) {
int convolver_init(const char *filename, unsigned char channel_count, double max_length_in_seconds,
size_t block_size) {
debug(3, "convolver_init");
int success = 0;
SF_INFO info = {}; // Zero everything, including format
@@ -124,9 +124,7 @@ void convolver_process(unsigned int channel, float *data, int length) {
void convolver_wait_for_all() { pool.waitForAll(); }
void convolver_clear_state() {
pool.clearAllStates();
}
void convolver_clear_state() { pool.clearAllStates(); }
const unsigned int max_channels = 8;
fftconvolver::FFTConvolver convolvers[max_channels];
+4 -2
View File
@@ -7,7 +7,8 @@ extern "C" {
#include <stddef.h>
// int convolver_init(const char* file, unsigned char channel_count, double max_length_in_seconds, size_t block_size);
// int convolver_init(const char* file, unsigned char channel_count, double max_length_in_seconds,
// size_t block_size);
void convolver_reset();
// void convolver_clear_state();
// void convolver_process(unsigned int channel, float *data, int length);
@@ -16,7 +17,8 @@ void convolver_reset();
void convolver_pool_init(size_t numThreads, size_t numConvolvers);
void convolver_pool_closedown();
int convolver_init(const char* file, unsigned char channel_count, double max_length_in_seconds, size_t block_size);
int convolver_init(const char *file, unsigned char channel_count, double max_length_in_seconds,
size_t block_size);
void convolver_process(unsigned int channel, float *data, int length);
void convolver_clear_state();
void convolver_wait_for_all();
+28 -8
View File
@@ -5,13 +5,13 @@ homedir = @HOME@
# Note that xmlmantohtml isn't producing correct HTML.
# Uncomment the if/else and SUBDIR lines in this stanza to make xmltoman build shairport-sync.1
# if USE_XMLTOMAN
# SUBDIRS = man
# else
if USE_XMLTOMAN
SUBDIRS = man
else
man_MANS = $(top_srcdir)/man/shairport-sync.1
# endif
endif
lib_pair_ap_a_CFLAGS = -Wall -g -DCONFIG_GCRYPT -pthread
lib_pair_ap_a_CFLAGS = -Wall -g -DCONFIG_GCRYPT -pthread --include=utilities/debug.h
lib_tinyhttp_a_CFLAGS = -pthread
lib_dbus_interface_a_CFLAGS = -pthread
lib_mpris_interface_a_CFLAGS = -pthread
@@ -27,7 +27,11 @@ noinst_LIBRARIES =
# See below for the flags for the test client program
shairport_sync_SOURCES = shairport.c bonjour_strings.c rtsp.c mdns.c common.c rtp.c player.c audio.c loudness.c activity_monitor.c utilities/debug.c utilities/network_utilities.c
shairport_sync_SOURCES = shairport.c bonjour_strings.c mdns.c common.c rtp.c \
player.c audio.c loudness.c activity_monitor.c \
rtsp.c \
utilities/debug.c utilities/network_utilities.c utilities/rtsp_message_utilities.c \
utilities/string_utilities.c
if BUILD_FOR_DARWIN
AM_CXXFLAGS = -I/usr/local/include -Wno-multichar -Wall -Wextra -Wno-deprecated-declarations -pthread -DSYSCONFDIR=\"$(sysconfdir)\"
@@ -132,8 +136,20 @@ if USE_DNS_SD
shairport_sync_SOURCES += mdns_dns_sd.c
endif
if USE_METADATA
shairport_sync_SOURCES += metadata/pc_queue.c metadata/core.c
endif
if USE_METADATA_PIPE
shairport_sync_SOURCES += metadata/pipe.c
endif
if USE_METADATA_MULTICAST
shairport_sync_SOURCES += metadata/multicast.c
endif
if USE_METADATA_HUB
shairport_sync_SOURCES += metadata_hub.c
shairport_sync_SOURCES += metadata/hub.c
endif
if USE_MQTT
@@ -148,7 +164,11 @@ lib_tinyhttp_a_SOURCES = tinyhttp/chunk.c tinyhttp/header.c tinyhttp/http.c
endif
if USE_AIRPLAY_2
shairport_sync_SOURCES += ap2_buffered_audio_processor.c ap2_event_receiver.c ap2_rc_event_receiver.c ptp-utilities.c utilities/buffered_read.c utilities/structured_buffer.c utilities/mod23.c plists/get_info_response.c
shairport_sync_SOURCES += ap2_buffered_audio_processor.c ap2_event_receiver.c \
ap2_event_message_handler.c ptp-utilities.c \
utilities/buffered_read.c utilities/structured_buffer.c utilities/mod23.c \
utilities/generate_random_uuid.c utilities/generate_device_uuid.c \
plists/get_info_response.c
shairport_sync_LDADD += lib_pair_ap.a
lib_pair_ap_a_SOURCES = pair_ap/pair.c pair_ap/pair_fruit.c pair_ap/pair_homekit.c pair_ap/pair-tlv.c
noinst_LIBRARIES += lib_pair_ap.a
+4 -3
View File
@@ -22,12 +22,13 @@ Shairport Sync does not support AirPlay video or photo streaming.
# Features
* Outputs AirPlay audio to [ALSA](https://www.alsa-project.org/wiki/Main_Page), [sndio](http://www.sndio.org), [PipeWire](https://pipewire.org), [PulseAudio](https://www.freedesktop.org/wiki/Software/PulseAudio/), to a unix pipe or to `STDOUT`. It also has limited support for [libao](https://xiph.org/ao/).
* Metadata — Shairport Sync can deliver metadata supplied by the source, such as Album Name, Artist Name, Cover Art, etc. through a pipe or UDP socket to a recipient application program — see https://github.com/mikebrady/shairport-sync-metadata-reader for a sample recipient. Sources that supply metadata include iTunes and the Music app in macOS and iOS.
* An interface to [MQTT](https://en.wikipedia.org/wiki/MQTT), a popular protocol for Inter Process Communication, Machine-to-Machine, Internet of Things and Home Automation projects. The interface provides access to metadata and artwork, and has limited remote control.
* An interface to [MQTT](https://en.wikipedia.org/wiki/MQTT), a popular protocol for Inter Process Communication, Machine-to-Machine, Internet of Things and Home Automation projects. The interface provides access to metadata and artwork, with remote control for Classic AirPlay clients.
* Digital Signal Processing facilities – please see the [DSP Wiki Page Guide](https://github.com/mikebrady/shairport-sync/wiki/Digital-Signal-Processing-with-Shairport-Sync). (Thanks to [Yann Pomarède](https://github.com/yannpom) for the code and to [Paul Wieland](https://github.com/PaulWieland) for the guide.)
* An [MPRIS](https://specifications.freedesktop.org/mpris-spec/2.2/)-like interface, partially complete and very functional, including access to metadata and artwork, and limited remote control.
* A native D-Bus interface, including access to metadata and artwork, limited remote control and system settings.
* An [MPRIS](https://specifications.freedesktop.org/mpris-spec/2.2/)-like interface, partially complete and very functional, including access to metadata and artwork, and partial remote control for Classic AirPlay clients.
* A native D-Bus interface, including access to metadata, artwork and system settings. Remote control is also available for Classic AirPlay clients.
* Better Volume Control — Shairport Sync offers finer control at very top and very bottom of the volume range. See http://tangentsoft.net/audio/atten.html for a good discussion of audio "attenuators", upon which volume control in Shairport Sync is modelled. See also the diagram of the volume transfer function in the documents folder. In addition, Shairport Sync can offer an extended volume control range on devices with a restricted range.
* Flexible output rates, formats and channels with built-in transcoding.
* Remote Control (Classic AirPLay only) – Remote control commands -- such as `play`, `pause`, and volume-setting commands -- can be sent to Classic AirPlay clients via the D-Bus, MPRIS and MQTT interfaces. Remote Control is unfortunately not available for AirPlay 2 clients, as the system used has not been discovered.
Some features require configuration at build time – see [CONFIGURATION FLAGS.md](CONFIGURATION%20FLAGS.md).
+5 -2
View File
@@ -40,7 +40,10 @@
#include "activity_monitor.h"
#include "common.h"
#include "rtsp.h"
#ifdef CONFIG_METADATA
#include "metadata/core.h"
#endif
#ifdef CONFIG_DBUS_INTERFACE
#include "dbus-service.h"
@@ -128,7 +131,7 @@ void activity_monitor_signify_activity(int active) {
pthread_mutex_unlock(&activity_monitor_mutex);
}
// lock the mutex again to send a signal
pthread_cleanup_debug_mutex_lock(&activity_monitor_mutex, 10000, 1);
pthread_cleanup_debug_mutex_lock(&activity_monitor_mutex, 10000, 4);
pthread_cond_signal(&activity_monitor_cv);
pthread_cleanup_pop(1); // release the mutex
}
+2 -2
View File
@@ -44,8 +44,8 @@ static const int host_bigendian = 0;
#define _Swap32(v) \
do { \
v = (((v)&0x000000FF) << 0x18) | (((v)&0x0000FF00) << 0x08) | (((v)&0x00FF0000) >> 0x08) | \
(((v)&0xFF000000) >> 0x18); \
v = (((v) & 0x000000FF) << 0x18) | (((v) & 0x0000FF00) << 0x08) | \
(((v) & 0x00FF0000) >> 0x08) | (((v) & 0xFF000000) >> 0x18); \
} while (0)
#define _Swap16(v) \
+49 -31
View File
@@ -30,6 +30,7 @@
#include "rtp.h"
#include "utilities/buffered_read.h"
#include "utilities/mod23.h"
#include "utilities/network_utilities.h"
#include <sodium.h>
#include <stdint.h>
@@ -91,10 +92,9 @@ void addADTStoPacket(uint8_t *packet, int packetLen, int rate, int channel_confi
void rtp_buffered_audio_cleanup_handler(__attribute__((unused)) void *arg) {
debug(2, "Buffered Audio Receiver Cleanup Start.");
rtsp_conn_info *conn = (rtsp_conn_info *)arg;
close(conn->buffered_audio_socket);
safe_socket_close(&conn->buffered_audio_socket);
debug(3, "Connection %d: closing TCP Buffered Audio port: %u.", conn->connection_number,
conn->local_buffered_audio_port);
conn->buffered_audio_socket = 0;
debug(2, "Connection %d: rtp_buffered_audio_processor exit.", conn->connection_number);
}
@@ -277,14 +277,13 @@ void *rtp_buffered_audio_processor(void *arg) {
previous_ssrc = payload_ssrc;
payload_ssrc = nctohl(&packet[8]);
if ((payload_ssrc != previous_ssrc) && (payload_ssrc != SSRC_NONE)) {
if (ssrc_is_recognised(payload_ssrc) == 0) {
debug(2, "Unrecognised SSRC: %u.", payload_ssrc);
} else {
debug(2, "Connection %d: incoming audio encoding is%s \"%s\".",
conn->connection_number, previous_ssrc == SSRC_NONE ? "" : " switching to", get_ssrc_name(payload_ssrc));
conn->connection_number, previous_ssrc == SSRC_NONE ? "" : " switching to",
get_ssrc_name(payload_ssrc));
}
}
@@ -327,7 +326,8 @@ void *rtp_buffered_audio_processor(void *arg) {
finished = 1;
} else if (nread < 0) {
char errorstring[1024];
strerror_r(errno, (char *)errorstring, sizeof(errorstring));
(void)!strerror_r(errno, (char *)errorstring,
sizeof(errorstring)); // (void) ! to suppress unused response warning
debug(1, "error in rtp_buffered_audio_processor %d: \"%s\". Could not recv a data_len .",
errno, errorstring);
finished = 1;
@@ -343,23 +343,32 @@ void *rtp_buffered_audio_processor(void *arg) {
debug(2, "immediate flush started at sequence number %u until sequence number of %u.",
seq_no, conn->ap2_immediate_flush_until_sequence_number);
}
if ((blocks_read != 0) && ((a_minus_b_mod23(seq_no, conn->ap2_immediate_flush_until_sequence_number) > 0))) {
debug(1, "immediate flush may have escaped its endpoint! Seq_no is %u, conn->ap2_immediate_flush_until_sequence_number is %u.", seq_no, conn->ap2_immediate_flush_until_sequence_number);
if ((blocks_read != 0) &&
((a_minus_b_mod23(seq_no, conn->ap2_immediate_flush_until_sequence_number) > 0))) {
debug(1,
"immediate flush may have escaped its endpoint! Seq_no is %u, "
"conn->ap2_immediate_flush_until_sequence_number is %u.",
seq_no, conn->ap2_immediate_flush_until_sequence_number);
}
if ((blocks_read != 0) && ((a_minus_b_mod23(seq_no, conn->ap2_immediate_flush_until_sequence_number) >= 0))) {
debug(2, "immediate flush completed at seq_no: %u, conn->ap2_immediate_flush_until_sequence_number: %u.", seq_no, conn->ap2_immediate_flush_until_sequence_number);
if ((blocks_read != 0) &&
((a_minus_b_mod23(seq_no, conn->ap2_immediate_flush_until_sequence_number) >= 0))) {
debug(2,
"immediate flush completed at seq_no: %u, "
"conn->ap2_immediate_flush_until_sequence_number: %u.",
seq_no, conn->ap2_immediate_flush_until_sequence_number);
conn->ap2_immediate_flush_requested = 0;
ap2_immediate_flush_requested = 0;
// turn off all deferred requests. Not sure if this is right...
unsigned int f = 0;
for (f = 0; f < MAX_DEFERRED_FLUSH_REQUESTS; f++) {
if ((conn->ap2_deferred_flush_requests[f].inUse != 0) && (conn->ap2_deferred_flush_requests[f].active = 0)) {
if ((conn->ap2_deferred_flush_requests[f].inUse != 0) &&
(conn->ap2_deferred_flush_requests[f].active = 0)) {
debug(1,
"deferred flush cancelled by an immediate flush: flushFromTS: %12u, flushFromSeq: %12u, "
"deferred flush cancelled by an immediate flush: flushFromTS: %12u, "
"flushFromSeq: %12u, "
"flushUntilTS: %12u, flushUntilSeq: %12u, timestamp: %12u.",
conn->ap2_deferred_flush_requests[f].flushFromTS,
conn->ap2_deferred_flush_requests[f].flushFromSeq,
@@ -370,7 +379,6 @@ void *rtp_buffered_audio_processor(void *arg) {
conn->ap2_deferred_flush_requests[f].active = 0;
}
} else {
debug(4, "immediate flush of block %u until block %u", seq_no,
conn->ap2_immediate_flush_until_sequence_number);
@@ -426,11 +434,10 @@ void *rtp_buffered_audio_processor(void *arg) {
} else if (conn->ap2_deferred_flush_requests[f].active != 0) {
new_audio_block_needed = 1;
debug(4,
"deferred flush of block: %u, timestamp: %u, SSRC: \"%s\". flushFromTS: %12u, flushFromSeq: %12u, "
"deferred flush of block: %u, timestamp: %u, SSRC: \"%s\". flushFromTS: %12u, "
"flushFromSeq: %12u, "
"flushUntilTS: %12u, flushUntilSeq: %12u, timestamp: %12u.",
seq_no,
timestamp,
get_ssrc_name(payload_ssrc),
seq_no, timestamp, get_ssrc_name(payload_ssrc),
conn->ap2_deferred_flush_requests[f].flushFromTS,
conn->ap2_deferred_flush_requests[f].flushFromSeq,
conn->ap2_deferred_flush_requests[f].flushUntilTS,
@@ -454,9 +461,13 @@ void *rtp_buffered_audio_processor(void *arg) {
// calculate if there is room in the decoded audio buffer...
// debug(1, "frames buffered: %f seconds, desired length: %f seconds.", (1.0 * player_buffer_occupancy * conn->frames_per_packet) / conn->input_rate, config.audio_decoded_buffer_desired_length);
// debug(1, "frames buffered: %f seconds, desired length: %f seconds.", (1.0 *
// player_buffer_occupancy * conn->frames_per_packet) / conn->input_rate,
// config.audio_decoded_buffer_desired_length);
// int audio_decoded_buffer_below_desired_length = ((1.0 * player_buffer_occupancy * conn->frames_per_packet) / conn->input_rate) <= config.audio_decoded_buffer_desired_length;
// int audio_decoded_buffer_below_desired_length = ((1.0 * player_buffer_occupancy *
// conn->frames_per_packet) / conn->input_rate) <=
// config.audio_decoded_buffer_desired_length;
uint64_t buffer_should_be_time;
int have_valid_time = (frame_to_local_time(timestamp, &buffer_should_be_time, conn) == 0);
@@ -464,9 +475,11 @@ void *rtp_buffered_audio_processor(void *arg) {
// calculate the lead time to make sure it's not too early...
int64_t lead_time = buffer_should_be_time - get_absolute_time_in_ns();
// debug(1,"play_enabled: %d, have_valid_time: %d, audio_decoded_buffer_below_desired_length: %d, lead_time * 1E-9: %f, (config.audio_decoded_buffer_desired_length + 0.1): %f, player_buffer_occupancy: %zu",
// play_enabled, have_valid_time, audio_decoded_buffer_below_desired_length, lead_time * 1E-9, (config.audio_decoded_buffer_desired_length + 0.1), player_buffer_occupancy
// debug(1,"play_enabled: %d, have_valid_time: %d,
// audio_decoded_buffer_below_desired_length: %d, lead_time * 1E-9: %f,
// (config.audio_decoded_buffer_desired_length + 0.1): %f, player_buffer_occupancy: %zu",
// play_enabled, have_valid_time, audio_decoded_buffer_below_desired_length, lead_time *
// 1E-9, (config.audio_decoded_buffer_desired_length + 0.1), player_buffer_occupancy
// );
// A slight problem here is that counting the number of buffers may not be sufficient,
@@ -483,12 +496,9 @@ void *rtp_buffered_audio_processor(void *arg) {
// But it also means that Shairport Sync's buffers must be sufficient to hold all the
// entire lead-time's amount of audio in case the device has a zero-sized buffer.
if ((play_enabled != 0) && (have_valid_time != 0)
// (audio_decoded_buffer_below_desired_length != 0) &&
&& (lead_time * 1E-9 < (config.audio_decoded_buffer_desired_length + 0.1))
if (have_valid_time != 0) {
if ((play_enabled != 0) && (lead_time * 1E-9 < (config.audio_decoded_buffer_desired_length + 0.1))) {
// && (audio_decoded_buffer_below_desired_length != 0)
) {
very_early_packets_signalled = 0; // reset very early packet warning signaller
// try to identify blocks that are timed to before the last buffer, and drop 'em
@@ -661,11 +671,19 @@ void *rtp_buffered_audio_processor(void *arg) {
}
new_audio_block_needed = 1; // the block has been used up and is no longer current
} else {
if ((have_valid_time != 0) && (very_early_packets_signalled == 0) && (lead_time * 1E-9 > (config.audio_decoded_buffer_desired_length + 0.2))) {
debug(1, "incoming frame suddenly (?) has a lead time of %f seconds, with a desired decoded buffer length of %f.", 1.0 * lead_time * 1E-9, config.audio_decoded_buffer_desired_length);
if ((very_early_packets_signalled == 0) && (lead_time * 1E-9 > (config.audio_decoded_buffer_desired_length + 0.2))) {
debug(1,
"incoming frame suddenly (?) has a lead time of %f seconds, with a desired "
"decoded buffer length of %f.",
1.0 * lead_time * 1E-9, config.audio_decoded_buffer_desired_length);
very_early_packets_signalled = 1;
}
usleep(((1000000 * conn->frames_per_packet) / conn->input_rate) * 2); // wait for approximately the length of two packets
usleep(((1000000 * conn->frames_per_packet) / conn->input_rate) *
2); // wait for approximately the length of two packets
}
} else {
debug(3, "just you wait, Henry Higgins, without valid timing information...");
usleep(20000); // just you wait, Henry Higgins...
}
}
}
+139
View File
@@ -0,0 +1,139 @@
/*
* AirPlay 2 Event Port Message Handler. This file is part of Shairport Sync
* Copyright (c) Mike Brady 2025
* All rights reserved.
*
* 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.
*/
#include "ap2_event_message_handler.h"
#include "common.h"
#include "rtsp.h"
#include "utilities/generate_random_uuid.h"
#include "utilities/structured_buffer.h"
void decodeAndLogPlist(plist_t plist_to_log) {
if (plist_to_log != NULL) {
char *plist_as_string = plist_as_xml_text(plist_to_log);
if (plist_as_string != NULL) {
debug(3, "--\n%s\n--\n", plist_as_string);
free(plist_as_string);
}
}
}
// will return -1 if there is an error or port is not open, 0 if the port was closed and a positive
// number if okay
ssize_t ap2_event_port_send_message(rtsp_conn_info *conn, char *data, size_t data_length) {
ssize_t result = -1; // assume a problem
debug_mutex_lock(&conn->event_sender_mutex, 1000000, 4);
pthread_cleanup_push(mutex_unlock, &conn->event_sender_mutex);
if (conn->event_channel_fd != 0) {
result = write_encrypted(conn->event_channel_fd, &conn->ap2_pairing_context.event_cipher_bundle,
data, data_length);
if ((result != -1) && ((size_t)result == data_length)) {
debug(3, "Connection %d: Packet of %zu bytes successfully written on the Event Port.",
conn->connection_number, result);
uint8_t packet[4096];
result =
read_encrypted(conn->event_channel_fd, &conn->ap2_pairing_context.event_cipher_bundle,
packet, sizeof(packet));
debug(3, "Connection %d: Packet of %zu bytes successfully read on the Event Port.",
conn->connection_number, result);
if (result > 0) {
packet[result] = '\0';
debug(3, "Connection %d: Packet Received on Event Port with contents: \n--\n%s\n--\n",
conn->connection_number, packet);
} else {
debug(2, "Connection %d: Event Port connection closed by client", conn->connection_number);
}
} else {
result = -1; // this covers a situation where the result is positive but not the same as the
// data_length
}
} else {
debug(1, "Connection %d: attempt to send a command to the event port over a closed socket",
conn->connection_number);
}
pthread_cleanup_pop(1); // unlock the mutex
return result;
}
ssize_t ap2_event_port_post_command(rtsp_conn_info *conn, plist_t command) {
ssize_t result = 0;
decodeAndLogPlist(command);
structured_buffer *sbuf = sbuf_new(4096);
if (sbuf != NULL) {
pthread_cleanup_push(sbuf_cleanup, sbuf);
char *plistString = NULL;
uint32_t plistStringLength = 0;
plist_to_bin(command, &plistString, &plistStringLength);
if (plistString != NULL) {
sbuf_printf(sbuf, "POST /command RTSP/1.0\r\nContent-Length: %u\r\n", plistStringLength);
sbuf_printf(sbuf, "Content-Type: application/x-apple-binary-plist\r\n\r\n");
sbuf_append(sbuf, plistString, plistStringLength);
free(plistString); // should be plist_to_bin_free, but it's not defined in older
// libraries
char *b = 0;
size_t l = 0;
sbuf_buf_and_length(sbuf, &b, &l);
result = ap2_event_port_send_message(conn, b, l);
debug(3, "Connection %d: POST /command sent on the event port. Result is %zd.",
conn->connection_number, result);
sbuf_clear(sbuf);
}
pthread_cleanup_pop(1); // delete the structured buffer
}
return result;
}
ssize_t ap2_event_send_update_info(rtsp_conn_info *conn) {
// sends the updateInfo plist on the event port
ssize_t result = -1;
plist_t value_plist = generateInfoPlist(conn);
if (value_plist != NULL) {
void *txtData = NULL;
size_t txtDataLength = 0;
generateTxtDataValueInfo(conn, &txtData, &txtDataLength);
plist_dict_set_item(value_plist, "txtAirPlay", plist_new_data(txtData, txtDataLength));
free(txtData);
plist_t update_info_plist = plist_new_dict();
if (update_info_plist != NULL) {
plist_dict_set_item(update_info_plist, "type", plist_new_string("updateInfo"));
plist_dict_set_item(update_info_plist, "value", value_plist);
char *plist_as_string = plist_as_xml_text(update_info_plist);
if (plist_as_string != NULL) {
debug(3, "update_info_plist is:\n--\n\"%s\"\n--\n", plist_as_string);
free(plist_as_string);
}
result = ap2_event_port_post_command(conn, update_info_plist);
plist_free(update_info_plist);
} else {
debug(1, "Could not build an updateInfo plist");
}
} else {
debug(1, "Could not build an updateInfo value plist");
}
return result;
}
+15
View File
@@ -0,0 +1,15 @@
#ifndef _AP2_EVENT_COMMAND_SENDER_H
#define _AP2_EVENT_COMMAND_SENDER_H
#include "player.h"
#include <stddef.h>
#include <sys/types.h>
ssize_t ap2_event_send_unit_volume_notification(rtsp_conn_info *conn, double volume);
ssize_t ap2_event_send_modern_media_remote_command(rtsp_conn_info *conn,
unsigned int command_number);
ssize_t ap2_event_send_update_info(rtsp_conn_info *conn);
ssize_t ap2_event_send_dev_mule();
#endif // _AP2_EVENT_COMMAND_SENDER_H
+26 -141
View File
@@ -1,6 +1,6 @@
/*
* Apple AirPlay 2 Event Receiver. This file is part of Shairport Sync.
* Copyright (c) Mike Brady 2014--2025
* Copyright (c) Mike Brady 2014--2026
* All rights reserved.
*
* Permission is hereby granted, free of charge, to any person
@@ -25,23 +25,26 @@
*/
#include "ap2_event_receiver.h"
#include "bonjour_strings.h"
#include "ap2_event_message_handler.h"
#include "common.h"
#include "player.h"
#include "ptp-utilities.h"
#include "rtsp.h"
#include "utilities/network_utilities.h"
#include "utilities/structured_buffer.h"
#ifdef CONFIG_METADATA
#include "metadata/core.h"
#endif
void ap2_event_receiver_cleanup_handler(void *arg) {
rtsp_conn_info *conn = (rtsp_conn_info *)arg;
// debug(1, "Connection %d: AP2 Event Receiver Cleanup start.", conn->connection_number);
// only update these things if you're (still) the principal conn
#ifdef CONFIG_METADATA
// this is here to ensure it's only performed once during a teardown of a ptp stream
send_ssnc_metadata('disc', conn->client_ip_string, strlen(conn->client_ip_string), 1);
#endif
debug_mutex_lock(&conn->event_sender_mutex, 1000000, 4);
pthread_cleanup_push(mutex_unlock, &conn->event_sender_mutex);
safe_socket_close(&conn->event_channel_fd);
pthread_cleanup_pop(1); // unlock the mutex
if (conn->airplay_gid != NULL) {
free(conn->airplay_gid);
@@ -52,161 +55,43 @@ void ap2_event_receiver_cleanup_handler(void *arg) {
free(conn->dacp_active_remote);
conn->dacp_active_remote = NULL;
}
if (conn->ap2_client_name) {
free(conn->ap2_client_name);
conn->ap2_client_name = NULL;
}
/*
pthread_rwlock_wrlock(&principal_conn_lock); // don't let the principal_conn be changed
pthread_cleanup_push(rwlock_unlock, (void *)&principal_conn_lock);
if (principal_conn)
debug(1, "principal_conn: %d.", principal_conn->connection_number);
else
debug(1, "principal_conn: is NULL.");
if (principal_conn == conn) {
config.airplay_statusflags &= (0xffffffff - (1 << 11)); // DeviceSupportsRelay
build_bonjour_strings(conn);
debug(1, "Connection %d: SETUP mdns_update on %s.", conn->connection_number,
get_category_string(conn->airplay_stream_category));
mdns_update(NULL, secondary_txt_records);
principal_conn = NULL;
}
pthread_cleanup_pop(1); // release the principal_conn lock
*/
debug(2, "Connection %d: AP2 Event Receiver Cleanup is complete.", conn->connection_number);
}
void *ap2_event_receiver(void *arg) {
// #include <syscall.h>
// debug(1, "rtp_event_receiver PID %d", syscall(SYS_gettid));
rtsp_conn_info *conn = (rtsp_conn_info *)arg;
debug(2, "Connection %d: AP2 Event Receiver started", conn->connection_number);
structured_buffer *sbuf = sbuf_new(4096);
if (sbuf != NULL) {
pthread_cleanup_push(sbuf_cleanup, sbuf);
/*
// only update these things if you're (still) the principal conn
pthread_rwlock_wrlock(&principal_conn_lock); // don't let the principal_conn be changed
pthread_cleanup_push(rwlock_unlock, (void *)&principal_conn_lock);
if (principal_conn == conn) {
config.airplay_statusflags |= 1 << 11; // DeviceSupportsRelay
// config.airplay_statusflags |= 1 << 17; // ReceiverSessionIsActive
build_bonjour_strings(conn);
debug(2, "Connection %d: SETUP mdns_update on %s.", conn->connection_number,
get_category_string(conn->airplay_stream_category));
mdns_update(NULL, secondary_txt_records);
}
pthread_cleanup_pop(1); // release the principal_conn lock
*/
pthread_cleanup_push(ap2_event_receiver_cleanup_handler, arg);
// listen(conn->event_socket, 5); // this is now done in the handle_setup_2 code
uint8_t packet[4096];
ssize_t nread;
SOCKADDR remote_addr;
memset(&remote_addr, 0, sizeof(remote_addr));
socklen_t addr_size = sizeof(remote_addr);
int fd = eintr_checked_accept(conn->event_socket, (struct sockaddr *)&remote_addr, &addr_size);
debug_mutex_lock(&conn->event_sender_mutex, 1000000, 4);
pthread_cleanup_push(mutex_unlock, &conn->event_sender_mutex);
conn->event_channel_fd =
eintr_checked_accept(conn->event_socket, (struct sockaddr *)&remote_addr, &addr_size);
pthread_cleanup_pop(1); // unlock the mutex
if (conn->event_channel_fd > 0) {
debug(2,
"Connection %d: ap2_event_receiver accepted a connection on socket %d and moved to a new "
"socket %d.",
conn->connection_number, conn->event_socket, fd);
intptr_t pfd = fd;
pthread_cleanup_push(socket_cleanup, (void *)pfd);
int finished = 0;
do {
conn->connection_number, conn->event_socket, conn->event_channel_fd);
pthread_cleanup_push(ap2_event_receiver_cleanup_handler, arg);
plist_t value_plist = generateInfoPlist(conn);
if (value_plist != NULL) {
void *txtData = NULL;
size_t txtDataLength = 0;
generateTxtDataValueInfo(conn, &txtData, &txtDataLength);
plist_dict_set_item(value_plist, "txtAirPlay", plist_new_data(txtData, txtDataLength));
free(txtData);
plist_t update_info_plist = plist_new_dict();
if (update_info_plist != NULL) {
plist_dict_set_item(update_info_plist, "type", plist_new_string("updateInfo"));
plist_dict_set_item(update_info_plist, "value", value_plist);
char *plistString = NULL;
uint32_t plistStringLength = 0;
plist_to_bin(update_info_plist, &plistString, &plistStringLength);
if (plistString != NULL) {
char *plist_as_string = plist_as_xml_text(update_info_plist);
if (plist_as_string != NULL) {
debug(3, "Plist is: \"%s\".", plist_as_string);
free(plist_as_string);
}
sbuf_printf(sbuf, "POST /command RTSP/1.0\r\nContent-Length: %u\r\n",
plistStringLength);
sbuf_printf(sbuf, "Content-Type: application/x-apple-binary-plist\r\n\r\n");
sbuf_append(sbuf, plistString, plistStringLength);
ap2_event_send_update_info(conn);
free(plistString); // should be plist_to_bin_free, but it's not defined in older
// libraries
char *b = 0;
size_t l = 0;
sbuf_buf_and_length(sbuf, &b, &l);
ssize_t wres =
write_encrypted(fd, &conn->ap2_pairing_context.event_cipher_bundle, b, l);
if ((wres == -1) || ((size_t)wres != l))
debug(1, "Encrypted write error");
sbuf_clear(sbuf);
} else {
debug(1, "plist string not created!");
}
plist_free(update_info_plist);
} else {
debug(1, "Could not build an updateInfo plist");
}
// plist_free(value_plist);
} else {
debug(1, "Could not build an value plist");
}
while (finished == 0) {
nread = read_encrypted(fd, &conn->ap2_pairing_context.event_cipher_bundle, packet,
sizeof(packet));
// nread = recv(fd, packet, sizeof(packet), 0);
if (nread < 0) {
char errorstring[1024];
strerror_r(errno, (char *)errorstring, sizeof(errorstring));
debug(1,
"Connection %d: error in ap2_event_receiver %d: \"%s\". Could not recv a packet.",
conn->connection_number, errno, errorstring);
// if ((config.diagnostic_drop_packet_fraction == 0.0) ||
// (drand48() > config.diagnostic_drop_packet_fraction)) {
} else if (nread > 0) {
// ssize_t plen = nread;
packet[nread] = '\0';
debug(3, "Connection %d: Packet Received on Event Port with contents: \"%s\".",
conn->connection_number, packet);
} else {
debug(2, "Connection %d: Event Port connection closed by client",
conn->connection_number);
finished = 1;
}
}
} while (finished == 0);
while (1) {
usleep(100000);
};
debug(3, "Connection %d: AP2 Event Receiver RTP thread starting \"normal\" exit.",
conn->connection_number);
pthread_cleanup_pop(1); // close the socket
pthread_cleanup_pop(1); // do the cleanup
pthread_cleanup_pop(1); // delete the structured buffer
} else {
debug(1, "Connection %d: could not accept an event socket", conn->connection_number);
conn->event_channel_fd = -1;
}
debug(2, "Connection %d: AP2 Event Receiver RTP thread \"normal\" exit.",
conn->connection_number);
} else {
debug(1, "Could not allocate a structured buffer!");
}
conn->ap2_event_receiver_exited = 1;
pthread_exit(NULL);
}
-161
View File
@@ -1,161 +0,0 @@
/*
* Apple AirPlay 2 Remote Control (RC) Event Receiver. This file is part of Shairport Sync.
* Copyright (c) Mike Brady 2014--2025
* All rights reserved.
*
* 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.
*/
#include "ap2_rc_event_receiver.h"
#include "common.h"
#include "player.h"
#include "rtsp.h"
#include "utilities/network_utilities.h"
#include "utilities/structured_buffer.h"
void ap2_rc_event_receiver_cleanup_handler(void *arg) {
rtsp_conn_info *conn = (rtsp_conn_info *)arg;
debug(2, "Connection %d: AP2 Event Receiver Cleanup.", conn->connection_number);
}
void *ap2_rc_event_receiver(void *arg) {
// #include <syscall.h>
// debug(1, "rtp_event_receiver PID %d", syscall(SYS_gettid));
rtsp_conn_info *conn = (rtsp_conn_info *)arg;
if (conn->airplay_stream_category == remote_control_stream)
debug(2, "Connection %d (RC): AP2 RC Event Receiver started", conn->connection_number);
else
debug(2, "Connection %d: AP2 RC Event Receiver started", conn->connection_number);
structured_buffer *sbuf = sbuf_new(4096);
if (sbuf != NULL) {
pthread_cleanup_push(sbuf_cleanup, sbuf);
pthread_cleanup_push(ap2_rc_event_receiver_cleanup_handler, arg);
// listen(conn->event_socket, 5); // this is now done in the handle_setup_2 code
uint8_t packet[4096];
ssize_t nread;
SOCKADDR remote_addr;
memset(&remote_addr, 0, sizeof(remote_addr));
socklen_t addr_size = sizeof(remote_addr);
int fd = eintr_checked_accept(conn->event_socket, (struct sockaddr *)&remote_addr, &addr_size);
debug(2,
"Connection %d: ap2_rc_event_receiver accepted a connection on socket %d and moved to a "
"new "
"socket %d.",
conn->connection_number, conn->event_socket, fd);
intptr_t pfd = fd;
pthread_cleanup_push(socket_cleanup, (void *)pfd);
int finished = 0;
do {
plist_t value_plist = generateInfoPlist(conn);
if (value_plist != NULL) {
void *txtData = NULL;
size_t txtDataLength = 0;
generateTxtDataValueInfo(conn, &txtData, &txtDataLength);
plist_dict_set_item(value_plist, "txtAirPlay", plist_new_data(txtData, txtDataLength));
free(txtData);
plist_t update_info_plist = plist_new_dict();
if (update_info_plist != NULL) {
plist_dict_set_item(update_info_plist, "type", plist_new_string("updateInfo"));
plist_dict_set_item(update_info_plist, "value", value_plist);
char *plistString = NULL;
uint32_t plistStringLength = 0;
plist_to_bin(update_info_plist, &plistString, &plistStringLength);
if (plistString != NULL) {
char *plist_as_string = plist_as_xml_text(update_info_plist);
if (plist_as_string != NULL) {
debug(3, "Plist is: \"%s\".", plist_as_string);
free(plist_as_string);
}
sbuf_printf(sbuf, "POST /command RTSP/1.0\r\nContent-Length: %u\r\n",
plistStringLength);
sbuf_printf(sbuf, "Content-Type: application/x-apple-binary-plist\r\n\r\n");
sbuf_append(sbuf, plistString, plistStringLength);
free(plistString); // should be plist_to_bin_free, but it's not defined in older
// libraries
char *b = 0;
size_t l = 0;
sbuf_buf_and_length(sbuf, &b, &l);
ssize_t wres =
write_encrypted(fd, &conn->ap2_pairing_context.event_cipher_bundle, b, l);
if ((wres == -1) || ((size_t)wres != l))
debug(1, "Encrypted write error");
sbuf_clear(sbuf);
} else {
debug(1, "plist string not created!");
}
plist_free(update_info_plist);
} else {
debug(1, "Could not build an updateInfo plist");
}
// plist_free(value_plist);
} else {
debug(1, "Could not build an value plist");
}
while (finished == 0) {
nread = read_encrypted(fd, &conn->ap2_pairing_context.event_cipher_bundle, packet,
sizeof(packet));
// nread = recv(fd, packet, sizeof(packet), 0);
if (nread < 0) {
char errorstring[1024];
strerror_r(errno, (char *)errorstring, sizeof(errorstring));
debug(
1,
"Connection %d: error in ap2_rc_event_receiver %d: \"%s\". Could not recv a packet.",
conn->connection_number, errno, errorstring);
// if ((config.diagnostic_drop_packet_fraction == 0.0) ||
// (drand48() > config.diagnostic_drop_packet_fraction)) {
} else if (nread > 0) {
// ssize_t plen = nread;
packet[nread] = '\0';
debug(2,
"Connection %d: ap2_rc_event_receiver Packet Received on Event Port with contents: "
"\"%s\".",
conn->connection_number, packet);
} else {
debug(2, "Connection %d: ap2_rc_event_receiver Event Port connection closed by client",
conn->connection_number);
finished = 1;
}
}
} while (finished == 0);
pthread_cleanup_pop(1); // close the socket
pthread_cleanup_pop(1); // do the cleanup
pthread_cleanup_pop(1); // delete the structured buffer
debug(2, "Connection %d: AP2 ap2_rc_event_receiver \"normal\" exit.", conn->connection_number);
} else {
debug(1, "Could not allocate a structured buffer!");
}
conn->ap2_event_receiver_exited = 1;
pthread_exit(NULL);
}
-8
View File
@@ -1,8 +0,0 @@
// RC means Remote Control
#ifndef _AP2_RC_EVENT_RECEIVER_H
#define _AP2_RC_EVENT_RECEIVER_H
void *ap2_rc_event_receiver(void *arg);
#endif // _AP2_RC_EVENT_RECEIVER_H
+2 -2
View File
@@ -458,7 +458,7 @@ uint32_t get_channel_settings(const char *stanza_name, const char *setting_name)
for (i = 0; i < channel_counts_count; i++) {
debug(3, "channel count setting %d: %d.", i, channel_counts[i]);
if ((channel_counts[i] >= 1) && (channel_counts[i] <= 8)) {
if ((channel_counts[i] >= 1) && (channel_counts[i] <= SPS_GREATEST_CHANNEL_COUNT)) {
#ifdef CONFIG_FFMPEG
channel_set |= (1 << channel_counts[i]);
#else
@@ -492,7 +492,7 @@ uint32_t get_channel_settings(const char *stanza_name, const char *setting_name)
char numbuf[32];
unsigned int c;
uint32_t t_channel_set = channel_set;
for (c = 0; c <= 8; c++) {
for (c = 0; c <= SPS_GREATEST_CHANNEL_COUNT; c++) {
if ((t_channel_set & (1 << c)) != 0) {
snprintf(numbuf, sizeof(numbuf) - 1, "%u", c);
snprintf(p, sizeof(buf) - (p - buf) - 1, "%s", numbuf);
+19 -13
View File
@@ -40,6 +40,7 @@
#include "activity_monitor.h"
#include "audio.h"
#include "common.h"
#include "utilities/string_utilities.h"
enum alsa_backend_mode {
abm_disconnected,
@@ -306,7 +307,7 @@ static int get_permissible_configuration_settings() {
debug(3, "\"%s\" can handle %u channels.", alsa_out_dev, c);
} else {
// the device can't handle this number of channels
debug(3, "\"%s\" can not handle %u channels.", alsa_out_dev, c);
debug(4, "\"%s\" can not handle %u channels.", alsa_out_dev, c);
config.channel_set &=
~(1 << c); // the alsa device can't accommodate this number of channels
}
@@ -332,7 +333,7 @@ static int get_permissible_configuration_settings() {
debug(3, "\"%s\" can handle a rate of %u fps.", alsa_out_dev,
sps_rate_actual_rate(r));
} else {
debug(3, "\"%s\" can not handle a rate of %u fps.", alsa_out_dev,
debug(4, "\"%s\" can not handle a rate of %u fps.", alsa_out_dev,
sps_rate_actual_rate(r));
config.rate_set &= ~(1 << r); // the alsa device doesn't do this rate
}
@@ -357,7 +358,7 @@ static int get_permissible_configuration_settings() {
debug(3, "\"%s\" can handle the %s format.", alsa_out_dev,
sps_format_description_string(f));
} else {
debug(3, "\"%s\" can not handle the %s format.", alsa_out_dev,
debug(4, "\"%s\" can not handle the %s format.", alsa_out_dev,
sps_format_description_string(f));
config.format_set &= ~(1 << f); // the alsa device doesn't do this format
}
@@ -951,8 +952,8 @@ static int actual_open_alsa_device() {
if ((snd_pcm_hw_params_get_rate_numden(alsa_params, &uval, &uval2) == 0) && (uval2 != 0))
// watch for a divide by zero too!
debug(log_level, " precise (rational) rate = %.3f frames per second (i.e. %u/%u).", (1.0 * uval) / uval2, uval,
uval2);
debug(log_level, " precise (rational) rate = %.3f frames per second (i.e. %u/%u).",
(1.0 * uval) / uval2, uval, uval2);
else
debug(log_level, " precise (rational) rate information unavailable.");
@@ -1260,7 +1261,8 @@ static int init(int argc, char **argv) {
else {
warn("Invalid disable_synchronization option choice \"%s\". It should "
"be \"yes\" or "
"\"no\". It is set to \"no\".", str);
"\"no\". It is set to \"no\".",
str);
config.no_sync = 0;
}
}
@@ -1276,7 +1278,8 @@ static int init(int argc, char **argv) {
else {
warn("Invalid mute_using_playback_switch option choice \"%s\". It "
"should be \"yes\" or "
"\"no\". It is set to \"no\".", str);
"\"no\". It is set to \"no\".",
str);
config.alsa_use_hardware_mute = 0;
}
}
@@ -1290,7 +1293,8 @@ static int init(int argc, char **argv) {
else {
warn("Invalid use_hardware_mute_if_available option choice \"%s\". It "
"should be \"yes\" or "
"\"no\". It is set to \"no\".", str);
"\"no\". It is set to \"no\".",
str);
config.alsa_use_hardware_mute = 0;
}
}
@@ -1304,7 +1308,8 @@ static int init(int argc, char **argv) {
else {
warn("Invalid use_mmap_if_available option choice \"%s\". It should be "
"\"yes\" or \"no\". "
"It remains set to \"yes\".", str);
"It remains set to \"yes\".",
str);
config.no_mmap = 0;
}
}
@@ -1424,7 +1429,8 @@ static int init(int argc, char **argv) {
else {
warn("Invalid use_precision_timing option choice \"%s\". It should be "
"\"yes\", \"auto\" or \"no\". "
"It remains set to \"%s\".", str,
"It remains set to \"%s\".",
str,
config.use_precision_timing == YNA_NO ? "no"
: config.use_precision_timing == YNA_AUTO ? "auto"
: "yes");
@@ -2177,14 +2183,14 @@ static void flush(void) {
}
static void stop(void) {
pthread_cleanup_debug_mutex_lock(&alsa_mutex, 10000, 1);
pthread_cleanup_debug_mutex_lock(&alsa_mutex, 10000, 4);
if (alsa_backend_state != abm_disconnected) { // must be playing or connected...
if (config.keep_dac_busy == 0) {
do_close();
}
} else
debug(3, "alsa: stop() -- called on a disconnected alsa backend");
debug_mutex_unlock(&alsa_mutex, 3);
debug_mutex_unlock(&alsa_mutex, 4);
pthread_cleanup_pop(0); // release the mutex
}
@@ -2252,7 +2258,7 @@ static void *alsa_buffer_monitor_thread_code(__attribute__((unused)) void *arg)
// #include <syscall.h>
// debug(1, "alsa_buffer_monitor_thread_code PID %d", syscall(SYS_gettid));
// Wait until the output configuration has been set by the main program
debug(2, "alsa: alsa_buffer_monitor_thread_code started.");
debug(1, "alsa: alsa_buffer_monitor_thread_code started.");
int frame_count = 0;
int error_count = 0;
int error_detected = 0;
+2 -3
View File
@@ -506,7 +506,8 @@ static int init(__attribute__((unused)) int argc, __attribute__((unused)) char *
// get the default channel mapping setting basis -- "alsa" or "pulseaudio".
if (config_lookup_non_empty_string(config.cfg, "pulseaudio.default_channel_layouts", &default_channel_layouts)) {
if (config_lookup_non_empty_string(config.cfg, "pulseaudio.default_channel_layouts",
&default_channel_layouts)) {
if ((strcasecmp(default_channel_layouts, "alsa") == 0) ||
(strcasecmp(default_channel_layouts, "pulseaudio") == 0)) {
debug(1, "pulseaudio default_channel_layouts setting: \"%s\".", default_channel_layouts);
@@ -767,8 +768,6 @@ void stream_write_cb(pa_stream *local_stream, size_t requested_bytes,
pthread_cleanup_pop(1); // release the mutex
if (ret != 0)
debug(1, "error writing to pa buffer");
// debug(1,"<<<Frames requested %d, written to pa: %d, corked status:
// %d.",requested_bytes/4,bytes_transferred/4,pa_stream_is_corked(local_stream));
}
void stream_success_cb(__attribute__((unused)) pa_stream *local_stream,
+3 -4
View File
@@ -39,6 +39,8 @@
#include <sys/types.h>
#include <unistd.h>
#include "utilities/network_utilities.h"
static int fd = -1;
static int warned = 0;
static unsigned int bytes_per_frame = 0;
@@ -146,10 +148,7 @@ static int init(int argc, char **argv) {
return 0;
}
static void deinit(void) {
if (fd > 0)
close(fd);
}
static void deinit(void) { safe_socket_close(&fd); }
static void help(void) {
printf(" Provide the pipe's pathname. The default is \"%s\".\n", default_pipe_name);
+11 -7
View File
@@ -76,15 +76,17 @@ void build_bonjour_strings(__attribute((unused)) rtsp_conn_info *conn) {
// the secondary_txt_records are for the _airplay._tcp items.
#ifdef CONFIG_AIRPLAY_2
uint64_t features_hi = config.airplay_features;
features_hi = (features_hi >> 32) & 0xffffffff;
uint64_t features_lo = config.airplay_features;
features_lo = features_lo & 0xffffffff;
if (config.service_type == APST_airplay2) {
txt_records[entry_number++] = "cn=0,1";
txt_records[entry_number++] = "da=true";
txt_records[entry_number++] = "et=0,1";
if (config.password != NULL)
txt_records[entry_number++] = "pw=true";
uint64_t features_hi = config.airplay_features;
features_hi = (features_hi >> 32) & 0xffffffff;
uint64_t features_lo = config.airplay_features;
features_lo = features_lo & 0xffffffff;
txt_records[entry_number++] =
bnprintf(ap1_featuresString, sizeof(ap1_featuresString), "ft=0x%" PRIX64 ",0x%" PRIX64 "",
@@ -110,8 +112,8 @@ void build_bonjour_strings(__attribute((unused)) rtsp_conn_info *conn) {
txt_records[entry_number++] =
bnprintf(ap1OsversString, sizeof(ap1OsversString), "ov=%s", config.osvers);
txt_records[entry_number++] = NULL;
#else
} else {
#endif
// here, just replicate what happens in mdns.h when using those #defines
txt_records[entry_number++] =
bnprintf(ap1StatusFlagsString, sizeof(ap1StatusFlagsString), "sf=0x4");
@@ -142,6 +144,8 @@ void build_bonjour_strings(__attribute((unused)) rtsp_conn_info *conn) {
else
txt_records[entry_number++] = "pw=true";
txt_records[entry_number++] = NULL;
#ifdef CONFIG_AIRPLAY_2
}
#endif
#ifdef CONFIG_AIRPLAY_2
@@ -149,7 +153,7 @@ void build_bonjour_strings(__attribute((unused)) rtsp_conn_info *conn) {
entry_number = 0;
secondary_txt_records[entry_number++] = "acl=0";
secondary_txt_records[entry_number++] = "btaddr=00:00:00:00:00:00";
// secondary_txt_records[entry_number++] = "btaddr=00:00:00:00:00:00";
secondary_txt_records[entry_number++] =
bnprintf(deviceIdString, sizeof(deviceIdString), "deviceid=%s", config.airplay_device_id);
secondary_txt_records[entry_number++] =
+82 -63
View File
@@ -27,6 +27,7 @@
*/
#include "common.h"
#include "utilities/network_utilities.h"
#ifdef CONFIG_USE_GIT_VERSION_STRING
#include "gitversion.h"
@@ -51,8 +52,8 @@
#include <time.h>
#include <unistd.h>
#include <net/if.h>
#include <ifaddrs.h>
#include <net/if.h>
#ifdef COMPILE_FOR_LINUX
#include <netpacket/packet.h>
@@ -411,7 +412,7 @@ int bind_socket_and_port(int type, int ip_family, const char *self_ip_address, u
#endif
if (ret < 0) {
ret = errno;
close(local_socket);
safe_socket_close(&local_socket);
char errorstring[1024];
getErrorText((char *)errorstring, sizeof(errorstring));
warn("error %d: \"%s\". Could not bind a port!", errno, errorstring);
@@ -422,7 +423,7 @@ int bind_socket_and_port(int type, int ip_family, const char *self_ip_address, u
ret = getsockname(local_socket, (struct sockaddr *)&local, &local_len);
if (ret < 0) {
ret = errno;
close(local_socket);
safe_socket_close(&local_socket);
char errorstring[1024];
getErrorText((char *)errorstring, sizeof(errorstring));
warn("error %d: \"%s\". Could not retrieve socket's port!", errno, errorstring);
@@ -494,7 +495,7 @@ uint16_t bind_UDP_port(int ip_family, const char *self_ip_address, uint32_t scop
// debug(1,"UDP port chosen: %d.",desired_port);
if (ret < 0) {
close(local_socket);
safe_socket_close(&local_socket);
char errorstring[1024];
getErrorText((char *)errorstring, sizeof(errorstring));
die("error %d: \"%s\". Could not bind a UDP port! Check the udp_port_range is large enough -- "
@@ -950,7 +951,8 @@ uint8_t *rsa_apply(uint8_t *input, int inlen, int *outlen, int mode) {
mbedtls_rsa_set_padding(trsa, MBEDTLS_RSA_PKCS_V15, MBEDTLS_MD_NONE);
outbuf = malloc(trsa->MBEDTLS_PRIVATE_V3_ONLY(len));
#if MBEDTLS_VERSION_MAJOR == 3
rc = mbedtls_pk_sign(&pkctx, MBEDTLS_MD_NONE, input, inlen, outbuf, mbedtls_pk_get_len(&pkctx), &olen, mbedtls_ctr_drbg_random, &ctr_drbg);
rc = mbedtls_pk_sign(&pkctx, MBEDTLS_MD_NONE, input, inlen, outbuf, mbedtls_pk_get_len(&pkctx),
&olen, mbedtls_ctr_drbg_random, &ctr_drbg);
*outlen = olen;
#else
rc = mbedtls_rsa_pkcs1_encrypt(trsa, mbedtls_ctr_drbg_random, &ctr_drbg, MBEDTLS_RSA_PRIVATE,
@@ -1076,11 +1078,14 @@ int config_set_lookup_bool(config_t *cfg, const char *where, int *dst) {
(*dst) = 1;
response = CONFIG_TRUE;
} else {
die("invalid boolean parameter \"%s\" option choice \"%s\". It should be \"yes\" or \"no\"", where, str);
die("invalid boolean parameter \"%s\" option choice \"%s\". It should be \"yes\" or \"no\"",
where, str);
return 0;
}
} else {
warn("the \"%s\" parameter is not a string with a value of \"yes\" or \"no\", as required, and has been ignored.", where);
warn("the \"%s\" parameter is not a string with a value of \"yes\" or \"no\", as required, "
"and has been ignored.",
where);
}
}
return response;
@@ -1245,6 +1250,59 @@ int check_int_or_list_setting(config_setting_t *setting, const int item) {
return result;
}
void service_type_to_string(APST_t service_type, char *string_space) {
if (string_space != NULL) {
string_space[0] = '\0';
switch (service_type) {
case APST_auto:
strcpy(string_space, "auto");
break;
case APST_classic:
strcpy(string_space, "classic");
break;
case APST_forced_classic:
strcpy(string_space, "forced_classic");
break;
case APST_airplay2:
strcpy(string_space, "airplay2");
break;
}
}
}
APST_t string_to_service_type(const char *parameter, const char *setting_name) {
APST_t response = APST_auto;
if (parameter != NULL) {
if (strcasecmp(parameter, "auto") == 0) {
response = APST_auto;
} else if (strcasecmp(parameter, "classic") == 0) {
response = APST_classic;
} else if (strcasecmp(parameter, "airplay1") == 0) {
response = APST_classic;
} else if (strcasecmp(parameter, "airplay2") == 0) {
response = APST_airplay2;
} else {
warn("The %s \"%s\" was ignored. It must be \"auto\", \"classic\" or \"airplay2\". (You can "
"use \"airplay1\" instead of \"classic\".)",
setting_name, parameter);
}
#ifndef CONFIG_AIRPLAY_2
if (response == APST_airplay2) {
warn("This version of Shairport Sync supports does not support AirPlay 2. The %s \"%s\" "
"setting has been ignored.",
setting_name, parameter);
response = APST_auto; // reset to default
}
#endif
}
/*
char service_type_string[32];
service_type_to_string(response, service_type_string);
debug(1, "config.service_type read from %s is: \"%s\".", setting_name, service_type_string);
*/
return response;
}
void command_set_volume(double volume) {
// this has a cancellation point if waiting is enabled
if (config.cmd_set_volume) {
@@ -1310,11 +1368,11 @@ void command_start(void) {
char **argV;
if (config.cmd_start_returns_output) {
close(pipes[0]);
safe_socket_close(&pipes[0]);
if (dup2(pipes[1], 1) < 0) {
warn("Unable to reopen pipe as stdout for popen of start command");
debug(1, "dup2 finished with error %d", errno);
close(pipes[1]);
safe_socket_close(&pipes[1]);
return;
}
}
@@ -1344,9 +1402,9 @@ void command_start(void) {
if (config.cmd_start_returns_output) {
static char buffer[256];
int len;
close(pipes[1]);
safe_socket_close(&pipes[1]);
len = read(pipes[0], buffer, 255);
close(pipes[0]);
safe_socket_close(&pipes[0]);
buffer[len] = '\0';
if (buffer[len - 1] == '\n')
buffer[len - 1] = '\0'; // strip trailing newlines
@@ -1705,45 +1763,6 @@ int try_to_open_pipe_for_writing(const char *pathname) {
return fdis;
}
/* from
* http://coding.debuntu.org/c-implementing-str_replace-replace-all-occurrences-substring#comment-722
*/
char *str_replace(const char *string, const char *substr, const char *replacement) {
char *tok = NULL;
char *newstr = NULL;
char *oldstr = NULL;
char *head = NULL;
/* if either substr or replacement is NULL, duplicate string a let caller handle it */
if (substr == NULL || replacement == NULL)
return strdup(string);
newstr = strdup(string);
head = newstr;
if (head) {
while ((tok = strstr(head, substr))) {
oldstr = newstr;
newstr = malloc(strlen(oldstr) - strlen(substr) + strlen(replacement) + 1);
/*failed to alloc mem, free old string and return NULL */
if (newstr == NULL) {
free(oldstr);
return NULL;
}
memcpy(newstr, oldstr, tok - oldstr);
memcpy(newstr + (tok - oldstr), replacement, strlen(replacement));
memcpy(newstr + (tok - oldstr) + strlen(replacement), tok + strlen(substr),
strlen(oldstr) - strlen(substr) - (tok - oldstr));
memset(newstr + strlen(oldstr) - strlen(substr) + strlen(replacement), 0, 1);
/* move back head right after the last replacement */
head = newstr + (tok - oldstr) + strlen(replacement);
free(oldstr);
}
} else {
die("failed to allocate memory in str_replace.");
}
return newstr;
}
/* from http://burtleburtle.net/bob/rand/smallprng.html */
// this is not thread-safe, so we need a mutex on it to use it properly.
@@ -1826,7 +1845,8 @@ void sps_nanosleep(const time_t sec, const long nanosec) {
rem = req;
} while ((result == -1) && (errno == EINTR));
if (result == -1)
debug(1, "Error in sps_nanosleep of %" PRIdMAX " sec and %ld nanoseconds: %d.", (intmax_t)sec, nanosec, errno);
debug(1, "Error in sps_nanosleep of %" PRIdMAX " sec and %ld nanoseconds: %d.", (intmax_t)sec,
nanosec, errno);
}
// Mac OS X doesn't have pthread_mutex_timedlock
@@ -1938,10 +1958,9 @@ void plist_cleanup(void *arg) {
#endif
void socket_cleanup(void *arg) {
intptr_t fdp = (intptr_t)arg;
int soc = fdp;
debug(3, "socket_cleanup called for socket: %d.", soc);
close(fdp);
int *p = (int *)arg;
debug(3, "socket_cleanup called for socket: %d.", *p);
safe_socket_close(p);
}
void cv_cleanup(void *arg) {
@@ -2339,6 +2358,8 @@ int get_device_id(uint8_t *id, int int_length) {
int64_t time_to_wait;
do {
int oldState;
pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, &oldState); // make this un-cancellable
if (getifaddrs(&ifaddr) == 0) {
t = id;
int found = 0;
@@ -2347,12 +2368,8 @@ int get_device_id(uint8_t *id, int int_length) {
#ifdef AF_PACKET
if ((ifa->ifa_addr) && (ifa->ifa_addr->sa_family == AF_PACKET)) {
struct sockaddr_ll *s = (struct sockaddr_ll *)ifa->ifa_addr;
if (
((ifa->ifa_flags & IFF_UP) != 0) &&
((ifa->ifa_flags & IFF_RUNNING) != 0) &&
((ifa->ifa_flags & IFF_LOOPBACK) == 0) &&
(ifa->ifa_addr != 0)
) {
if (((ifa->ifa_flags & IFF_UP) != 0) && ((ifa->ifa_flags & IFF_RUNNING) != 0) &&
((ifa->ifa_flags & IFF_LOOPBACK) == 0) && (ifa->ifa_addr != 0)) {
found = 1;
response = 0;
for (i = 0; ((i < s->sll_halen) && (i < int_length)); i++) {
@@ -2378,6 +2395,7 @@ int get_device_id(uint8_t *id, int int_length) {
}
freeifaddrs(ifaddr);
}
pthread_setcancelstate(oldState, NULL);
// wait a little time if we haven't got a response
if (response != 0) {
usleep(100000);
@@ -2427,7 +2445,7 @@ int named_pthread_create(pthread_t *thread, const pthread_attr_t *attr,
debug(1, "error creating thread \"%s\"", actual_name);
}
#ifndef COMPILE_FOR_OSX
else {
if (response == 0) {
pthread_setname_np(*thread, actual_name);
}
#endif
@@ -2478,6 +2496,7 @@ int named_pthread_create_with_priority(pthread_t *thread, int priority,
}
// ret == 0 if creating and setting up the attributes was successful
if (ret == 0) {
ret = pthread_create(thread, &attr, start_routine, arg);
pthread_attr_destroy(&attr);
}
@@ -2486,7 +2505,7 @@ int named_pthread_create_with_priority(pthread_t *thread, int priority,
if (ret != 0) {
ret = pthread_create(thread, NULL, start_routine, arg);
if (failed_to_set_rt == 0) {
inform("Can not set realtime properties of a thread.");
inform("Can not set realtime properties of thread \"%s\".", actual_name);
failed_to_set_rt = 1;
}
}
+25 -20
View File
@@ -14,6 +14,14 @@ extern "C" {
#include "definitions.h"
#include "mdns.h"
// service type to be offered
typedef enum {
APST_auto = 0,
APST_classic, // must be classic / airplay 1
APST_forced_classic, // must be classic / airplay 1 because no nqptp
APST_airplay2, // must be airplay 2; error if no NQPTP
} APST_t; // AirPlay Service Type
// struct sockaddr_in6 is bigger than struct sockaddr. derp
#ifdef AF_INET6
#define SOCKADDR struct sockaddr_storage
@@ -24,8 +32,10 @@ extern "C" {
#endif
#if defined(CONFIG_CONVOLUTION)
// impulse response filter file status
typedef enum { ev_unchecked, ev_okay, ev_invalid } ir_file_evaluation;
// impulse response filter file record
typedef struct {
unsigned int samplerate; // initialized to 0, will be filter frame rate
unsigned int channels;
@@ -146,7 +156,14 @@ typedef enum {
// these sets omit the _UNKNOWN, _AUTO and _ILLEGAL values
#define SPS_FORMAT_SET (((1 << (SPS_FORMAT_HIGHEST_NATIVE + 1)) - 1) - (1 << SPS_FORMAT_UNKNOWN))
#define SPS_RATE_SET (((1 << (SPS_RATE_HIGHEST + 1)) - 1) - (1 << SPS_RATE_UNKNOWN))
#define SPS_CHANNEL_SET (((1 << (8 + 1)) - 1) - (1 << 0)) // channels 1 to 8, not 0-based!
// in SPS_CHANNEL_SET, bit 0 set means a channel set of no channels, bit 1 set means a channel set
// of 1 channel and so on to bit 31 meaning a channel set of 31 channels. We want to consider all
// possible channel sets apart from channel set 0.
#define SPS_GREATEST_CHANNEL_COUNT 31 // should be 32 to be fully in line with ALSA limits
#define SPS_CHANNEL_SET 0xFFFFFFFE // channel sets 31 to 1, but no channel set 0
// #define SPS_CHANNEL_SET (((1 << (SPS_GREATEST_CHANNEL_COUNT + 1)) - 1) - (1 << 0)) // channels 1
// to 31, not 0-based!
#ifndef CONFIG_AIRPLAY_2
#define SPS_FORMAT_NON_FFMPEG_SET SPS_FORMAT_SET
@@ -396,14 +413,17 @@ typedef struct {
uint32_t airplay_statusflags;
char *airplay_fex; // a base64-encoded version of the airplay_features in little-endian form
char *airplay_device_id; // for the Bonjour advertisement and the GETINFO PList
char *airplay_pin; // non-NULL, 4 char PIN, if required for pairing
char *airplay_pi; // UUID in the Bonjour advertisement and the GETINFO Plist
char *airplay_pgid; // UUID in the txtAirPlay data sent on the event channel
char *airplay_psi; // type 4 fixed UUID
uint8_t airplay_pk[32]; // public key
char *pk_string;
char *nqptp_shared_memory_interface_name; // client name for nqptp service
int enable_HK_Access_Control; // true if the device is part of an Apple Home
#endif
APST_t service_type; // APST_auto, APST_classic, APST_forced_classic, APST_airplay2
int unfixable_error_reported; // only report once.
uint64_t eight_channel_layout; // non-zero means enabled and is a channel layout
@@ -454,11 +474,6 @@ void set_requested_connection_state_to_output(int v);
int try_to_open_pipe_for_writing(
const char *pathname); // open it without blocking if it's not hooked up
/* from
* http://coding.debuntu.org/c-implementing-str_replace-replace-all-occurrences-substring#comment-722
*/
char *str_replace(const char *string, const char *substr, const char *replacement);
// based on http://burtleburtle.net/bob/rand/smallprng.html
void r64init(uint64_t seed);
@@ -478,19 +493,6 @@ uint16_t nextFreeUDPPort();
extern volatile int debuglev;
void _die(const char *filename, const int linenumber, const char *format, ...);
void _warn(const char *filename, const int linenumber, const char *format, ...);
void _inform(const char *filename, const int linenumber, const char *format, ...);
void _debug(const char *filename, const int linenumber, int level, const char *format, ...);
void _debug_print_buffer(const char *thefilename, const int linenumber, int level, void *buf,
size_t buf_len);
#define die(...) _die(__FILE__, __LINE__, __VA_ARGS__)
#define debug(...) _debug(__FILE__, __LINE__, __VA_ARGS__)
#define warn(...) _warn(__FILE__, __LINE__, __VA_ARGS__)
#define inform(...) _inform(__FILE__, __LINE__, __VA_ARGS__)
#define debug_print_buffer(...) _debug_print_buffer(__FILE__, __LINE__, __VA_ARGS__)
// Thanks to https://stackoverflow.com/a/1597129 for the inspiration for this identifier generation
#define MAKEUNIQUEID2(x, y) x##y
#define MADEID(x, y) MAKEUNIQUEID2(x, y)
@@ -569,6 +571,9 @@ unsigned int config_get_string_settings_as_string_array(config_setting_t *settin
const char ***result);
unsigned int config_get_int_settings_as_int_array(config_setting_t *setting, int **result);
APST_t string_to_service_type(const char *parameter, const char *setting_name);
void service_type_to_string(APST_t service_type, char *string_space);
void command_start(void);
void command_stop(void);
void command_execute(const char *command, const char *extra_argument, const int block);
+33 -4
View File
@@ -1,7 +1,7 @@
# Process this file with autoconf to produce a configure script.
AC_PREREQ([2.50])
AC_INIT([shairport-sync], [5.0.4], [4265913+mikebrady@users.noreply.github.com])
AC_INIT([shairport-sync], [5.1], [4265913+mikebrady@users.noreply.github.com])
: ${CFLAGS="-O3"}
: ${CXXFLAGS="-O3"}
AM_INIT_AUTOMAKE([subdir-objects])
@@ -219,6 +219,12 @@ fi
# Look for metadata flag and resolve it further down the script
AC_ARG_WITH(metadata,[AS_HELP_STRING([--with-metadata],[include support for a metadata feed])])
# Look for metadata_pipe flag and resolve it further down the script
AC_ARG_WITH(metadata_pipe,[AS_HELP_STRING([--with-metadata-pipe],[include support for piping metadata out through a unix pipe])])
# Look for metadata_multicast flag and resolve it further down the script
AC_ARG_WITH(metadata_multicast,[AS_HELP_STRING([--with-metadata-multicast],[include support for metadata multicasting])])
# What follows is a bit messy, because if the relevant library is requested, a compiler flag is defined, a file is included in the compilation
# and the relevant link files are added.
@@ -404,10 +410,28 @@ fi
AM_CONDITIONAL([USE_METADATA_HUB], [test "x$REQUESTED_EXTENDED_METADATA_SUPPORT" = "x1"])
AM_CONDITIONAL([USE_DACP_CLIENT], [test "x$REQUESTED_EXTENDED_METADATA_SUPPORT" = "x1"])
if test "x$REQUESTED_EXTENDED_METADATA_SUPPORT" = "x1" || test "x$with_metadata" = "xyes" ; then
if test "x$REQUESTED_EXTENDED_METADATA_SUPPORT" = "x1" || test "x$with_metadata" = "xyes" || test "x$with_metadata_pipe" = "xyes" || test "x$with_metadata_multicast" = "xyes" ; then
REQUESTED_METADATA_SUPPORT=1
fi
if test "x$REQUESTED_METADATA_SUPPORT" = "x1" ; then
AC_DEFINE([CONFIG_METADATA], 1, [Needed by the compiler.])
fi
AM_CONDITIONAL([USE_METADATA], [test "x$with_metadata" = "xyes"])
AM_CONDITIONAL([USE_METADATA], [test "x$REQUESTED_METADATA_SUPPORT" = "x1"])
if test "x$with_metadata" = "xyes" ; then
DEFAULT_INCLUDE_PIPE_METADATA=1
fi
if test "x$DEFAULT_INCLUDE_PIPE_METADATA" = "x1" || test "x$with_metadata_pipe" = "xyes" ; then
AC_DEFINE([CONFIG_METADATA_PIPE], 1, [Needed by the compiler.])
fi
AM_CONDITIONAL([USE_METADATA_PIPE], [test "x$DEFAULT_INCLUDE_PIPE_METADATA" = "x1" || test "x$with_metadata_pipe" = "xyes"])
if test "x$with_metadata_multicast" = "xyes" ; then
AC_DEFINE([CONFIG_METADATA_MULTICAST], 1, [Needed by the compiler.])
fi
AM_CONDITIONAL([USE_METADATA_MULTICAST], [test "x$with_metadata_multicast" = "xyes"])
# Look for AirPlay 2 flag
AC_ARG_WITH(airplay-2, [AS_HELP_STRING([--with-airplay-2],[Build for AirPlay 2])])
@@ -445,7 +469,7 @@ if test "x$with_airplay_2" = "xyes" ; then
])
AC_CHECK_LIB([uuid],[uuid_generate], [], [AC_MSG_ERROR([AirPlay 2 support requires the uuid library -- uuid-dev suggested])])
systemd_after_args="${systemd_after_args}${systemd_after_args:+ }nqptp.service"
systemd_requires_args="${systemd_requires_args}${systemd_requires_args:+ }nqptp.service"
systemd_wants_args="${systemd_wants_args}${systemd_wants_args:+ }nqptp.service"
fi
AM_CONDITIONAL([USE_AIRPLAY_2], [test "x$with_airplay_2" = "xyes"])
@@ -562,8 +586,13 @@ if test "x${systemd_requires_args}" != x ; then
systemd_requires_args="Requires=${systemd_requires_args}"
fi
if test "x${systemd_wants_args}" != x ; then
systemd_wants_args="Wants=${systemd_wants_args}"
fi
AC_SUBST([SYSTEMD_AFTER_ARGS], [${systemd_after_args}])
AC_SUBST([SYSTEMD_REQUIRES_ARGS], [${systemd_requires_args}])
AC_SUBST([SYSTEMD_WANTS_ARGS], [${systemd_wants_args}])
# The man directory will not be recursively made unless you set the SUBDIR in Makefile.am.
# But leave this as is to enable a distclean to be done.
+9 -16
View File
@@ -43,8 +43,9 @@
#include <time.h>
#include <unistd.h>
#include "metadata_hub.h"
#include "metadata/hub.h"
#include "tinyhttp/http.h"
#include "utilities/network_utilities.h"
typedef struct {
int players_connection_thread_index; // the connection thread index when a player thread is
@@ -117,8 +118,7 @@ void response_body(void *opaque, const char *data, int size) {
static void
response_header(__attribute__((unused)) void *opaque, __attribute__((unused)) const char *ckey,
__attribute__((unused)) int nkey, __attribute__((unused)) const char *cvalue,
__attribute__((unused)) int nvalue) { /* example doesn't care about headers */
}
__attribute__((unused)) int nvalue) { /* example doesn't care about headers */ }
static void response_code(void *opaque, int code) {
struct HttpResponse *response = (struct HttpResponse *)opaque;
@@ -150,12 +150,6 @@ void mutex_lock_cleanup(void *arg) {
debug(1, "Error releasing mutex.");
}
void connect_cleanup(void *arg) {
int *fd = (int *)arg;
// debug(2, "dacp_send_command: close socket %d.",*fd);
close(*fd);
}
void http_cleanup(void *arg) {
// debug(1, "http cleanup called.");
struct http_roundtripper *rt = (struct http_roundtripper *)arg;
@@ -234,7 +228,7 @@ int dacp_send_command(const char *command, char **body, ssize_t *bodysize) {
// \"%s\".",errno,strerror(errno));
response.code = 497; // Can't establish a socket to the DACP server
} else {
pthread_cleanup_push(connect_cleanup, (void *)&sockfd);
pthread_cleanup_push(socket_cleanup, (void *)&sockfd);
// debug(2, "dacp_send_command: open socket %d.",sockfd);
// This is for limiting the time to be spent waiting for a response.
@@ -350,8 +344,7 @@ int dacp_send_command(const char *command, char **body, ssize_t *bodysize) {
0); // this should *not* free the malloced buffer -- just pop the malloc cleanup
}
}
pthread_cleanup_pop(1); // this should close the socket
// close(sockfd);
pthread_cleanup_pop(1); // this will close the socket
// debug(1,"DACP socket closed.");
}
pthread_cleanup_pop(1); // this should unlock the dacp_conversation_lock);
@@ -397,10 +390,10 @@ void relinquish_dacp_server_information(rtsp_conn_info *conn) {
// as the conn's connection number
// this is to signify that the player has stopped, but only if another thread (with a different
// index) hasn't already taken over the dacp service
debug_mutex_lock(&dacp_server_information_lock, 500000, 2);
debug_mutex_lock(&dacp_server_information_lock, 500000, 4);
if (dacp_server.players_connection_thread_index == conn->connection_number)
dacp_server.players_connection_thread_index = 0;
debug_mutex_unlock(&dacp_server_information_lock, 3);
debug_mutex_unlock(&dacp_server_information_lock, 4);
}
// this will be running on the thread of its caller, not of the conversation thread...
@@ -1302,8 +1295,8 @@ int dacp_set_volume(int32_t vo) {
int32_t active_speakers = 0;
for (i = 0; i < speaker_count; i++) {
if (speaker_info[i].speaker_number == machine_number) {
debug(2, "Our speaker number found: %" PRId64 " with relative volume %" PRId32 ".", machine_number,
speaker_info[i].volume);
debug(2, "Our speaker number found: %" PRId64 " with relative volume %" PRId32 ".",
machine_number, speaker_info[i].volume);
}
if (speaker_info[i].active == 1) {
active_speakers++;
+73 -34
View File
@@ -37,8 +37,11 @@
#include "rtp.h"
#ifdef CONFIG_DACP_CLIENT
#include "dacp.h"
#include "metadata_hub.h"
#endif
#include "metadata/hub.h"
#include "dbus-service.h"
@@ -335,7 +338,9 @@ static gboolean on_handle_set_volume(ShairportSyncAdvancedRemoteControl *skeleto
GDBusMethodInvocation *invocation, const gint volume,
__attribute__((unused)) gpointer user_data) {
debug(2, "Set volume to %d.", volume);
#ifdef CONFIG_DACP_CLIENT
dacp_set_volume(volume);
#endif
shairport_sync_advanced_remote_control_complete_set_volume(skeleton, invocation);
return TRUE;
}
@@ -343,7 +348,9 @@ static gboolean on_handle_set_volume(ShairportSyncAdvancedRemoteControl *skeleto
static gboolean on_handle_fast_forward(ShairportSyncRemoteControl *skeleton,
GDBusMethodInvocation *invocation,
__attribute__((unused)) gpointer user_data) {
#ifdef CONFIG_DACP_CLIENT
send_simple_dacp_command("beginff");
#endif
shairport_sync_remote_control_complete_fast_forward(skeleton, invocation);
return TRUE;
}
@@ -351,7 +358,9 @@ static gboolean on_handle_fast_forward(ShairportSyncRemoteControl *skeleton,
static gboolean on_handle_rewind(ShairportSyncRemoteControl *skeleton,
GDBusMethodInvocation *invocation,
__attribute__((unused)) gpointer user_data) {
#ifdef CONFIG_DACP_CLIENT
send_simple_dacp_command("beginrew");
#endif
shairport_sync_remote_control_complete_rewind(skeleton, invocation);
return TRUE;
}
@@ -359,7 +368,9 @@ static gboolean on_handle_rewind(ShairportSyncRemoteControl *skeleton,
static gboolean on_handle_toggle_mute(ShairportSyncRemoteControl *skeleton,
GDBusMethodInvocation *invocation,
__attribute__((unused)) gpointer user_data) {
#ifdef CONFIG_DACP_CLIENT
send_simple_dacp_command("mutetoggle");
#endif
shairport_sync_remote_control_complete_toggle_mute(skeleton, invocation);
return TRUE;
}
@@ -367,7 +378,9 @@ static gboolean on_handle_toggle_mute(ShairportSyncRemoteControl *skeleton,
static gboolean on_handle_next(ShairportSyncRemoteControl *skeleton,
GDBusMethodInvocation *invocation,
__attribute__((unused)) gpointer user_data) {
#ifdef CONFIG_DACP_CLIENT
send_simple_dacp_command("nextitem");
#endif
shairport_sync_remote_control_complete_next(skeleton, invocation);
return TRUE;
}
@@ -375,7 +388,9 @@ static gboolean on_handle_next(ShairportSyncRemoteControl *skeleton,
static gboolean on_handle_previous(ShairportSyncRemoteControl *skeleton,
GDBusMethodInvocation *invocation,
__attribute__((unused)) gpointer user_data) {
#ifdef CONFIG_DACP_CLIENT
send_simple_dacp_command("previtem");
#endif
shairport_sync_remote_control_complete_previous(skeleton, invocation);
return TRUE;
}
@@ -383,7 +398,9 @@ static gboolean on_handle_previous(ShairportSyncRemoteControl *skeleton,
static gboolean on_handle_pause(ShairportSyncRemoteControl *skeleton,
GDBusMethodInvocation *invocation,
__attribute__((unused)) gpointer user_data) {
#ifdef CONFIG_DACP_CLIENT
send_simple_dacp_command("pause");
#endif
shairport_sync_remote_control_complete_pause(skeleton, invocation);
return TRUE;
}
@@ -391,7 +408,9 @@ static gboolean on_handle_pause(ShairportSyncRemoteControl *skeleton,
static gboolean on_handle_play_pause(ShairportSyncRemoteControl *skeleton,
GDBusMethodInvocation *invocation,
__attribute__((unused)) gpointer user_data) {
#ifdef CONFIG_DACP_CLIENT
send_simple_dacp_command("playpause");
#endif
shairport_sync_remote_control_complete_play_pause(skeleton, invocation);
return TRUE;
}
@@ -399,7 +418,9 @@ static gboolean on_handle_play_pause(ShairportSyncRemoteControl *skeleton,
static gboolean on_handle_play(ShairportSyncRemoteControl *skeleton,
GDBusMethodInvocation *invocation,
__attribute__((unused)) gpointer user_data) {
#ifdef CONFIG_DACP_CLIENT
send_simple_dacp_command("play");
#endif
shairport_sync_remote_control_complete_play(skeleton, invocation);
return TRUE;
}
@@ -407,7 +428,9 @@ static gboolean on_handle_play(ShairportSyncRemoteControl *skeleton,
static gboolean on_handle_stop(ShairportSyncRemoteControl *skeleton,
GDBusMethodInvocation *invocation,
__attribute__((unused)) gpointer user_data) {
#ifdef CONFIG_DACP_CLIENT
send_simple_dacp_command("stop");
#endif
shairport_sync_remote_control_complete_stop(skeleton, invocation);
return TRUE;
}
@@ -415,7 +438,9 @@ static gboolean on_handle_stop(ShairportSyncRemoteControl *skeleton,
static gboolean on_handle_resume(ShairportSyncRemoteControl *skeleton,
GDBusMethodInvocation *invocation,
__attribute__((unused)) gpointer user_data) {
#ifdef CONFIG_DACP_CLIENT
send_simple_dacp_command("playresume");
#endif
shairport_sync_remote_control_complete_resume(skeleton, invocation);
return TRUE;
}
@@ -423,7 +448,9 @@ static gboolean on_handle_resume(ShairportSyncRemoteControl *skeleton,
static gboolean on_handle_shuffle_songs(ShairportSyncRemoteControl *skeleton,
GDBusMethodInvocation *invocation,
__attribute__((unused)) gpointer user_data) {
#ifdef CONFIG_DACP_CLIENT
send_simple_dacp_command("shuffle_songs");
#endif
shairport_sync_remote_control_complete_shuffle_songs(skeleton, invocation);
return TRUE;
}
@@ -431,7 +458,9 @@ static gboolean on_handle_shuffle_songs(ShairportSyncRemoteControl *skeleton,
static gboolean on_handle_volume_up(ShairportSyncRemoteControl *skeleton,
GDBusMethodInvocation *invocation,
__attribute__((unused)) gpointer user_data) {
#ifdef CONFIG_DACP_CLIENT
send_simple_dacp_command("volumeup");
#endif
shairport_sync_remote_control_complete_volume_up(skeleton, invocation);
return TRUE;
}
@@ -439,7 +468,9 @@ static gboolean on_handle_volume_up(ShairportSyncRemoteControl *skeleton,
static gboolean on_handle_volume_down(ShairportSyncRemoteControl *skeleton,
GDBusMethodInvocation *invocation,
__attribute__((unused)) gpointer user_data) {
#ifdef CONFIG_DACP_CLIENT
send_simple_dacp_command("volumedown");
#endif
shairport_sync_remote_control_complete_volume_down(skeleton, invocation);
return TRUE;
}
@@ -449,9 +480,11 @@ static gboolean on_handle_set_airplay_volume(ShairportSyncRemoteControl *skeleto
const gdouble volume,
__attribute__((unused)) gpointer user_data) {
debug(2, "Set airplay volume to %.6f.", volume);
#ifdef CONFIG_DACP_CLIENT
char command[256] = "";
snprintf(command, sizeof(command), "setproperty?dmcp.device-volume=%.6f", volume);
send_simple_dacp_command(command);
#endif
shairport_sync_remote_control_complete_set_airplay_volume(skeleton, invocation);
return TRUE;
}
@@ -563,9 +596,9 @@ gboolean notify_convolution_enabled_callback(__attribute__((unused)) ShairportSy
#endif
#ifdef CONFIG_CONVOLUTION
gboolean notify_convolution_maximum_length_in_seconds_callback(ShairportSync *skeleton,
__attribute__((unused))
gpointer user_data) {
gboolean
notify_convolution_maximum_length_in_seconds_callback(ShairportSync *skeleton,
__attribute__((unused)) gpointer user_data) {
gdouble th = shairport_sync_get_convolution_maximum_length_in_seconds(skeleton);
if ((th >= 0.0) && (th <= 15.0)) {
@@ -579,10 +612,8 @@ gboolean notify_convolution_maximum_length_in_seconds_callback(ShairportSync *sk
return TRUE;
}
#else
gboolean notify_convolution_maximum_length_in_seconds_callback(__attribute__((unused))
ShairportSync *skeleton,
__attribute__((unused))
gpointer user_data) {
gboolean notify_convolution_maximum_length_in_seconds_callback(
__attribute__((unused)) ShairportSync *skeleton, __attribute__((unused)) gpointer user_data) {
warn(">> Convolution support is not built in to this build of Shairport Sync.");
return TRUE;
}
@@ -610,9 +641,9 @@ gboolean notify_convolution_gain_callback(__attribute__((unused)) ShairportSync
}
#endif
#ifdef CONFIG_CONVOLUTION
gboolean notify_convolution_impulse_response_files_callback(ShairportSync *skeleton,
__attribute__((unused))
gpointer user_data) {
gboolean
notify_convolution_impulse_response_files_callback(ShairportSync *skeleton,
__attribute__((unused)) gpointer user_data) {
char *th = (char *)shairport_sync_get_convolution_impulse_response_files(skeleton);
if (th != NULL) {
debug(1, ">> freeing current configuration impulse response filter files.");
@@ -629,10 +660,9 @@ gboolean notify_convolution_impulse_response_files_callback(ShairportSync *skele
return TRUE;
}
#else
gboolean notify_convolution_impulse_response_files_callback(__attribute__((unused))
ShairportSync *skeleton,
__attribute__((unused))
gpointer user_data) {
gboolean
notify_convolution_impulse_response_files_callback(__attribute__((unused)) ShairportSync *skeleton,
__attribute__((unused)) gpointer user_data) {
__attribute__((unused)) char *th =
(char *)shairport_sync_get_convolution_impulse_response_files(skeleton);
return TRUE;
@@ -683,16 +713,6 @@ gboolean notify_volume_callback(ShairportSync *skeleton,
gdouble iv = shairport_sync_get_volume(skeleton);
if (((iv >= -30.0) && (iv <= 0.0)) || (iv == -144.0)) {
debug(2, ">> set volume to %7.4f.", iv);
pthread_rwlock_rdlock(&principal_conn_lock); // don't let the principal_conn be changed
pthread_cleanup_push(rwlock_unlock, (void *)&principal_conn_lock);
if (principal_conn != NULL) {
player_volume(iv, principal_conn);
principal_conn->own_airplay_volume = iv;
principal_conn->own_airplay_volume_set = 1;
}
pthread_cleanup_pop(1); // release the principal_conn lock
config.airplay_volume = iv;
} else {
debug(1, ">> invalid volume: %f. Ignored.", iv);
@@ -739,7 +759,8 @@ gboolean notify_alacdecoder_callback(ShairportSync *skeleton,
#ifdef CONFIG_AIRPLAY_2
if (strcasecmp(th, "ffmpeg") != 0) {
warn(" This request, to set the decoder to \"%s\", is ignored. For AirPlay 2, the FFmpeg decoder is always used.",
warn(" This request, to set the decoder to \"%s\", is ignored. For AirPlay 2, the FFmpeg "
"decoder is always used.",
th);
}
#else
@@ -845,6 +866,7 @@ gboolean notify_volume_control_profile_callback(ShairportSync *skeleton,
return TRUE;
}
#ifdef CONFIG_DACP_CLIENT
gboolean notify_shuffle_callback(ShairportSyncAdvancedRemoteControl *skeleton,
__attribute__((unused)) gpointer user_data) {
// debug(1,"notify_shuffle_callback called");
@@ -854,9 +876,18 @@ gboolean notify_shuffle_callback(ShairportSyncAdvancedRemoteControl *skeleton,
send_simple_dacp_command("setproperty?dacp.shufflestate=0");
return TRUE;
}
#else
gboolean notify_shuffle_callback(__attribute__((unused))
ShairportSyncAdvancedRemoteControl *skeleton,
__attribute__((unused)) gpointer user_data) {
return TRUE;
}
#endif
#ifdef CONFIG_DACP_CLIENT
gboolean notify_loop_status_callback(ShairportSyncAdvancedRemoteControl *skeleton,
__attribute__((unused)) gpointer user_data) {
// debug(1,"notify_loop_status_callback called");
char *th = (char *)shairport_sync_advanced_remote_control_get_loop_status(skeleton);
// enum volume_control_profile_type previous_volume_control_profile =
@@ -891,6 +922,13 @@ gboolean notify_loop_status_callback(ShairportSyncAdvancedRemoteControl *skeleto
}
return TRUE;
}
#else
gboolean notify_loop_status_callback(__attribute__((unused))
ShairportSyncAdvancedRemoteControl *skeleton,
__attribute__((unused)) gpointer user_data) {
return TRUE;
}
#endif
static gboolean on_handle_quit(ShairportSync *skeleton, GDBusMethodInvocation *invocation,
__attribute__((unused)) const gchar *command,
@@ -907,10 +945,12 @@ static gboolean on_handle_remote_command(ShairportSync *skeleton, GDBusMethodInv
__attribute__((unused)) gpointer user_data) {
debug(1, "RemoteCommand with command \"%s\".", command);
int reply = 0;
char *client_reply_hex = "";
#ifdef CONFIG_DACP_CLIENT
char *client_reply = NULL;
ssize_t reply_size = 0;
reply = dacp_send_command((const char *)command, &client_reply, &reply_size);
char *client_reply_hex = alloca(reply_size * 2 + 1);
client_reply_hex = alloca(reply_size * 2 + 1);
if (client_reply_hex) {
char *p = client_reply_hex;
if (client_reply) {
@@ -924,22 +964,21 @@ static gboolean on_handle_remote_command(ShairportSync *skeleton, GDBusMethodInv
}
*p = '\0';
}
#endif
shairport_sync_complete_remote_command(skeleton, invocation, reply, client_reply_hex);
return TRUE;
}
static gboolean on_handle_drop_session(ShairportSync *skeleton, GDBusMethodInvocation *invocation,
__attribute__((unused)) gpointer user_data) {
release_play_lock(NULL); // stop any current session and don't replace it
stop_play(); // stop any current session and don't replace it
shairport_sync_complete_drop_session(skeleton, invocation);
return TRUE;
}
static gboolean on_handle_set_frame_position_update_interval(ShairportSync *skeleton,
GDBusMethodInvocation *invocation,
const gdouble seconds,
__attribute__((unused))
gpointer user_data) {
static gboolean on_handle_set_frame_position_update_interval(
ShairportSync *skeleton, GDBusMethodInvocation *invocation, const gdouble seconds,
__attribute__((unused)) gpointer user_data) {
debug(1, ">> set frame position update interval to %.6f.", seconds);
config.metadata_progress_interval = seconds;
shairport_sync_complete_set_frame_position_update_interval(skeleton, invocation);
@@ -965,7 +1004,6 @@ static void on_dbus_name_acquired(GDBusConnection *connection, const gchar *name
shairportSyncAdvancedRemoteControlSkeleton =
shairport_sync_advanced_remote_control_skeleton_new();
g_dbus_interface_skeleton_export(
G_DBUS_INTERFACE_SKELETON(shairportSyncAdvancedRemoteControlSkeleton), connection,
"/org/gnome/ShairportSync", NULL);
@@ -1025,6 +1063,7 @@ static void on_dbus_name_acquired(GDBusConnection *connection, const gchar *name
g_signal_connect(shairportSyncRemoteControlSkeleton, "handle-fast-forward",
G_CALLBACK(on_handle_fast_forward), NULL);
g_signal_connect(shairportSyncRemoteControlSkeleton, "handle-rewind",
G_CALLBACK(on_handle_rewind), NULL);
g_signal_connect(shairportSyncRemoteControlSkeleton, "handle-toggle-mute",
+3
View File
@@ -5,6 +5,9 @@
#include "config.h"
// for generating our own UUIDs
#define SHAIRPORT_SYNC_DEVICE_NAMESPACE "01d7c137-6316-455d-a52f-dfb529f26adf"
#if defined(__APPLE__) && defined(__MACH__)
/* Apple OSX and iOS (Darwin). ------------------------------ */
#include <TargetConditionals.h>
+32 -6
View File
@@ -1,7 +1,7 @@
ARG NQPTP_BRANCH=main
ARG FFMPEG_BRANCH=release/7.1
ARG FFMPEG_BRANCH=release/8.1
ARG SHAIRPORT_SYNC_BRANCH=.
ARG ALPINE_VERSION=20250108
ARG ALPINE_VERSION=3.23.4
##### FFMPEGLITE #####
FROM alpine:$ALPINE_VERSION AS ffmpeglite
@@ -28,10 +28,9 @@ RUN ./configure \
--disable-network \
--disable-iamf \
--disable-pixelutils \
--disable-postproc \
--enable-decoder=alac \
--enable-decoder=aac
RUN make -j $(nproc)
RUN make
RUN make install
##### FFMPEGLITE END #####
@@ -51,7 +50,7 @@ RUN git clone --depth=1 -b "$NQPTP_BRANCH" https://github.com/mikebrady/nqptp
WORKDIR /nqptp
RUN autoreconf -i
RUN ./configure
RUN make -j $(nproc)
RUN make
WORKDIR /
##### NQPTP END #####
@@ -102,11 +101,38 @@ RUN ../configure --sysconfdir=/etc --with-alsa --with-pulseaudio --with-soxr --w
--with-airplay-2 --with-metadata --with-dummy --with-pipe --with-dbus-interface \
--with-stdout --with-mpris-interface --with-mqtt-client \
--with-convolution --with-pipewire
RUN make -j $(nproc)
RUN make
RUN DESTDIR=install make install
WORKDIR /
##### SPS END #####
# Shairport Sync Development Environment
# This (large) image contains the custom-built FFmpeg library and NQPTP, Avahi, D-Bus, etc., Shairport Sync source and all tools.
# When started, D-Bus, Avahi and NQPTP are set to run in the background.
# To build it, here is a sample local build instruction:
# $ docker build --build-arg SHAIRPORT_SYNC_BRANCH=development --build-arg NQPTP_BRANCH=main --target dev -t shairport-sync-dev -f docker/Dockerfile .
# Sample local use (remember to save your changes):
# $ docker run -it \
# --cap-add=SYS_NICE --net host --device /dev/snd \
# -v /etc/shairport-sync.conf:/etc/shairport-sync.conf \
# shairport-sync-dev
##### DEV #####
FROM shairport-sync AS dev
RUN apk add bash
RUN addgroup shairport-sync && \
adduser -D shairport-sync -G shairport-sync && \
addgroup -g 29 docker_audio && \
addgroup shairport-sync docker_audio && \
addgroup shairport-sync audio && \
mkdir -p /run/dbus
COPY --from=nqptp /nqptp/nqptp /usr/local/bin/nqptp
COPY --chmod=755 ./docker/devrun.sh ./devrun.sh
ENTRYPOINT ["./devrun.sh"]
##### DEV END #####
##### STATIC FILES #####
FROM scratch AS files
+2 -3
View File
@@ -1,8 +1,8 @@
# Classic Shairport-Sync build. Note: the only decoder in FFmpeg is ALAC.
ARG SHAIRPORT_SYNC_BRANCH=.
ARG FFMPEG_BRANCH=release/7.1
ARG ALPINE_VERSION=20250108
ARG FFMPEG_BRANCH=release/8.1
ARG ALPINE_VERSION=3.23.4
##### FFMPEGLITE #####
FROM alpine:$ALPINE_VERSION AS ffmpeglite
@@ -29,7 +29,6 @@ RUN ./configure \
--disable-network \
--disable-iamf \
--disable-pixelutils \
--disable-postproc \
--enable-decoder=alac
RUN make
RUN make install
+36
View File
@@ -0,0 +1,36 @@
#!/bin/bash
# exit if any command returns a non-zero result
set -e
echo "Shairport Sync Dev Startup ($(date))"
if [ -z ${ENABLE_AVAHI+x} ] || [ $ENABLE_AVAHI -eq 1 ]; then
rm -rf /run/dbus/dbus.pid
rm -rf /run/avahi-daemon/pid
dbus-uuidgen --ensure
dbus-daemon --system
avahi-daemon --daemonize --no-chroot
fi
# Don't launch NQPTP if classic only
SERVICE_TYPE=""
for arg in "$@"; do
case "$arg" in
--service-type=classic|--service-type=airplay1)
SERVICE_TYPE="${arg#--service-type=}"
;;
esac
done
if [ -z "$SERVICE_TYPE" ]; then
# not looking for Classic aka AirPlay 1 so start NQPTP for AirPlay 2
echo "Starting NQPTP ($(date))"
(/usr/local/bin/nqptp > /dev/null 2>&1) &
fi
while [ ! -f /var/run/avahi-daemon/pid ]; do
echo "Warning: avahi is not running, sleeping for 5 seconds before checking again"
sleep 5
done
# for PipeWire
export XDG_RUNTIME_DIR=/tmp
# for PulseAudio
export PULSE_SERVER=unix:/tmp/pulseaudio.socket
export PULSE_COOKIE=/tmp/pulseaudio.cookie
echo "Finished startup tasks ($(date)), starting bash."
exec /bin/bash
+15 -1
View File
@@ -15,9 +15,23 @@ if [ -z ${ENABLE_AVAHI+x} ] || [ $ENABLE_AVAHI -eq 1 ]; then
avahi-daemon --daemonize --no-chroot
fi
echo "Starting NQPTP ($(date))"
# Don't launch NQPTP if it classic only
SERVICE_TYPE=""
for arg in "$@"; do
case "$arg" in
--service-type=classic|--service-type=airplay1)
SERVICE_TYPE="${arg#--service-type=}"
;;
esac
done
if [ -z "$SERVICE_TYPE" ]; then
# not looking for Classic aka AirPlay 1 so start NQPTP for AirPlay 2
echo "Starting NQPTP ($(date))"
(/usr/local/bin/nqptp > /dev/null 2>&1) &
fi
while [ ! -f /var/run/avahi-daemon/pid ]; do
echo "Warning: avahi is not running, sleeping for 5 seconds before trying to start shairport-sync"
+4 -2
View File
@@ -2,6 +2,8 @@
Shairport Sync can have a D-Bus interface, which can be used to control aspects of its operation and get status information from it.
For Classic AirPlay only, remote control commands, for example `play`, `pause` and volume control, can be sent to the player.
To include the D-Bus interface at build time, add the `--with-dbus-interface` flag at the `./configure…` stage. When the D-Bus interface is included in Shairport Sync, its Version String will include the term `dbus`, for instance:
```
$ shairport-sync -V
@@ -143,7 +145,7 @@ The examples below are based on Shairport Sync running as a `system` service and
#### Remote Control
Remote Control commands are sent as requests to the player (iOS, iTunes, macOS Music, etc.). Different versions of the players implement different subsets of the following commands.
**Note:** Unfortunately, at this time -- early 2026 -- these requests are ignored, so remote control doesn't work.
**Note:** Remote Control only works on Classic Airplay
* Check if Remote Control is available:
```
@@ -162,7 +164,7 @@ Remote Control commands are sent as requests to the player (iOS, iTunes, macOS M
#### Advanced Remote Control
Some commands and properties are accessible only through the `AdvancedRemoteControl` interface.
**Note:** Unfortunately, at this time -- early 2026 -- these requests are ignored, so advanced remote control doesn't work.
**Note:** Advanced Remote Control only works on Classic Airplay and has only been observed to work with the macOS Music app.
* Check if Advanced Remote Control is available:
```
+12 -4
View File
@@ -2,7 +2,7 @@
.SH NAME
shairport-sync \- AirPlay and AirPlay 2 Audio Player
.SH SYNOPSIS
\fBshairport-sync [-djvw]\fB [-a \fB\fIservice-name\fB | --name=\fB\fIservice-name\fB]\fB [-B \fB\fIcommand\fB | --onstart=\fB\fIcommand\fB]\fB [-c \fB\fIconfigurationfile\fB | --configfile=\fB\fIconfigurationfile\fB]\fB [-d | --daemon]\fB [-E \fB\fIcommand\fB | --onstop=\fB\fIcommand\fB]\fB [-g | --get-cover-art]\fB [-j | --justDaemoniseNoPIDFile]\fB [--logOutputLevel]\fB [--log-to-syslog]\fB [-L \fB\fIlatency\fB | --latency=\fB\fIlatency\fB]\fB [-m \fB\fIbackend\fB | --mdns=\fB\fIbackend\fB]\fB [-M | --metadata-enable]\fB [-o \fB\fIbackend\fB | --output=\fB\fIbackend\fB]\fB [-p \fB\fIport\fB | --port=\fB\fIport\fB]\fB [--password=\fB\fIsecret\fB]\fB [-r \fB\fIthreshold\fB | --resync=\fB\fIthreshold\fB]\fB [--statistics]\fB [-S \fB\fImode\fB | --stuffing=\fB\fImode\fB]\fB [-t \fB\fItimeout\fB | --timeout=\fB\fItimeout\fB]\fB [--tolerance=\fB\fIframes\fB]\fB [-v | --verbose]\fB [-w | --wait-cmd]\fB [-- \fB\fIaudio_backend_options\fB]\fB
\fBshairport-sync [-djvw]\fB [-a \fB\fIservice-name\fB | --name=\fB\fIservice-name\fB]\fB [-B \fB\fIcommand\fB | --onstart=\fB\fIcommand\fB]\fB [-c \fB\fIconfigurationfile\fB | --configfile=\fB\fIconfigurationfile\fB]\fB [-d | --daemon]\fB [--dbus-default-message-bus=\fB\fIbus\fB]\fB [-E \fB\fIcommand\fB | --onstop=\fB\fIcommand\fB]\fB [-g | --get-cover-art]\fB [-j | --justDaemoniseNoPIDFile]\fB [--logOutputLevel]\fB [--log-to-syslog]\fB [-L \fB\fIlatency\fB | --latency=\fB\fIlatency\fB]\fB [-m \fB\fIbackend\fB | --mdns=\fB\fIbackend\fB]\fB [-M | --metadata-enable]\fB [-o \fB\fIbackend\fB | --output=\fB\fIbackend\fB]\fB [-p \fB\fIport\fB | --port=\fB\fIport\fB]\fB [--password=\fB\fIsecret\fB]\fB [-r \fB\fIthreshold\fB | --resync=\fB\fIthreshold\fB]\fB [--service-type=\fB\fItype\fB]\fB [--statistics]\fB [-S \fB\fImode\fB | --stuffing=\fB\fImode\fB]\fB [-t \fB\fItimeout\fB | --timeout=\fB\fItimeout\fB]\fB [--tolerance=\fB\fIframes\fB]\fB [-v | --verbose]\fB [-w | --wait-cmd]\fB [-- \fB\fIaudio_backend_options\fB]\fB
shairport-sync -X | --displayConfig\fB
@@ -73,6 +73,9 @@ Read configuration settings from \fIfilename\f1. The default is to read them fro
\fB-d | --daemon\f1
Instruct shairport-sync to demonise itself. It will write its Process ID (PID) to a file, usually at \fI/var/run/shairport-sync/shairport-sync.pid\f1, which is used by the \fB-k\f1, \fB-D\f1 and \fB-R\f1 options to locate the daemon at a later time. See also the \fB-j\f1 option. Only available if shairport-sync has been compiled with libdaemon support.
.TP
\fB--dbus-default-message-bus=\f1\fIbus\f1
Use the \fIbus\f1 specified for the native D-Bus and MPRIS interfaces. The \fIbus\f1 must be \fBsystem\f1 or \fBsession\f1. This option is only available if Shairport Sync is built with the D-Bus or MPRIS interface.
.TP
\fB-E \f1\fIprogram\f1\fB | --on-stop=\f1\fIprogram\f1
Execute \fIprogram\f1 when playback has ended. Specify the full path to the program, e.g. \fI/usr/bin/logger\f1. Executable scripts can be used, but they must have the appropriate shebang (\fI#!/bin/sh\f1) in the headline.
@@ -124,11 +127,14 @@ Require the password \fIsecret\f1 to be able to connect and stream to the servic
\fB-r \f1\fIthreshold\f1\fB | --resync=\f1\fIthreshold\f1
Resynchronise if timings differ by more than \fIthreshold\f1 frames. If the output timing differs from the source timing by more than the threshold, output will be muted and a full resynchronisation will occur. The default threshold is 2,205 frames, i.e. 50 milliseconds. Specify \fB0\f1 to disable resynchronisation. This setting is deprecated and will be removed in a future version of shairport-sync.
.TP
\fB--service-type=\f1\fItype\f1
On an AirPlay-2-capable Shairport Sync, if you need offer only the older "classic" AirPlay (sometimes called AirPlay 1) service, set \fB--service-type=classic\f1. A potential use of this would be for Apple Music on Windows, which is not compatible with the AirPlay 2 service offered by Shairport Sync, but which is compatible with the classic-only service. To offer AirPlay 2 service only, set \fB--service-type=airplay2\f1 (note that this requires NQPTP). Set \fB--service-type=auto\f1 to allow Shairport Sync to choose AirPlay 2 service if NQPTP is present or classic AirPlay service if not, with "(Classic)" appended to the default service name.
.TP
\fB--statistics\f1
Print some performance information to \fISTDERR\f1, or to \fBsyslog\f1 if the \fB-log-to-syslog\f1 command line option is also chosen.
Print some performance information to \fISTDERR\f1, or to \fBsyslog\f1 if the \fB--log-to-syslog\f1 command line option is also chosen.
.TP
\fB-S \f1\fImode\f1\fB | --stuffing=\f1\fImode\f1
Interpolate ("stuff") the audio stream using the \fImode\f1. "Stuffing" refers to the process of adding or removing frames of audio to or from the stream sent to the output device in order to keep it synchronised with the player. The \fBbasic\f1 mode is normally almost completely inaudible. The alternative mode, \fBsoxr\f1, is even less obtrusive but requires much more processing power. For this mode, support for \fBlibsoxr\f1, the SoX Resampler Library, must be selected when \fBshairport-sync\f1 is built. The default setting, \fBauto\f1, allows Shairport Sync to choose \fBsoxr\f1 mode if the system is powerful enough.
Interpolate ("stuff") the audio stream using the \fImode\f1. "Stuffing" refers to the process of adding or removing frames of audio to or from the stream sent to the output device in order to keep it synchronised with the player. The \fBvernier\f1 mode is normally almost completely inaudible and suitable for low-powered devices. An alternative mode, \fBsoxr\f1, is less obtrusive but requires much more processing power. For this mode, support for \fBlibsoxr\f1, the SoX Resampler Library, must be selected when \fBshairport-sync\f1 is built. The \fBbasic\f1 mode is also suitable for low-powered device, but \fBvernier\f1 is better. The default setting, \fBauto\f1, allows Shairport Sync to choose \fBsoxr\f1 mode if the system is powerful enough.
.TP
\fB-t \f1\fItimeout\f1\fB | --timeout=\f1\fItimeout\f1
Exit play mode if the stream disappears for more than \fItimeout\f1 seconds.
@@ -142,7 +148,7 @@ Allow playback to be up to \fIframes\f1 out of exact synchronization before atte
Print version information and exit.
.TP
\fB-v | --verbose\f1
Print debug information to the \fISTDERR\f1, or to \fBsyslog\f1 if the \fB-log-to-syslog\f1 command line option is also chosen. Repeat up to three times (i.e. \fB-vv\f1 or \fB-vvv\f1) for more detail. You should use \fB-vvv\f1 very sparingly -- it is really noisy.
Print debug information to the \fISTDERR\f1, or to \fBsyslog\f1 if the \fB--log-to-syslog\f1 command line option is also chosen. Repeat up to three times (i.e. \fB-vv\f1 or \fB-vvv\f1) for more detail. You should use \fB-vvv\f1 very sparingly -- it is really noisy.
.TP
\fB-w | --wait-cmd\f1
Wait for commands specified using \fB-B\f1 or \fB-E\f1 to complete before continuing execution.
@@ -165,6 +171,8 @@ The program will be visible as "Joe's Stereo" ( \fB-a "Joe's Stereo"\f1 ). The p
The example above is slightly contrived: Firstly, if the \fBalsa\f1 backend has been included in the build, it will be the default, so it doesn't need to be specified and the \fB-o alsa\f1 option could be omitted. Secondly, subdevice 0 is the default for a soundcard, so the output device could simply be written \fB-d hw:1\f1. Thirdly, when a mixer name is given ( \fB-c "PCM"\f1 ), the default is that the mixer is on the output device, so the \fB-m hw:1\f1 is unnecessary here. Using these defaults and simplifications gives the following command:
shairport-sync \fB-a "Joe's Stereo"\f1 \fB--\f1 \fB-d hw:1\f1 \fB-c PCM\f1
.SH NOTE
The \fBdacquery\f1 utility (\fBhttps://github.com/mikebrady/dacquery\f1) might be useful for discovering the properties of your system's DACs.
.SH CREDITS
Mike Brady (\fBhttps://github.com/mikebrady\f1) developed Shairport Sync from Shairport by James Wah (\fBhttps://github.com/abrasive\f1).
.SH COMMENTS
+37 -3
View File
@@ -4,7 +4,7 @@
<!--
This man page source file is part of shairport-sync.
Copyright (c) Mike Brady 2014-2022
Copyright (c) Mike Brady 2014-2026
All rights reserved.
@@ -40,6 +40,7 @@
<opt>[-B </opt><arg>command</arg><opt> | --onstart=</opt><arg>command</arg><opt>]</opt>
<opt>[-c </opt><arg>configurationfile</arg><opt> | --configfile=</opt><arg>configurationfile</arg><opt>]</opt>
<opt>[-d | --daemon]</opt>
<opt>[--dbus-default-message-bus=</opt><arg>bus</arg><opt>]</opt>
<opt>[-E </opt><arg>command</arg><opt> | --onstop=</opt><arg>command</arg><opt>]</opt>
<opt>[-g | --get-cover-art]</opt>
<opt>[-j | --justDaemoniseNoPIDFile]</opt>
@@ -52,6 +53,7 @@
<opt>[-p </opt><arg>port</arg><opt> | --port=</opt><arg>port</arg><opt>]</opt>
<opt>[--password=</opt><arg>secret</arg><opt>]</opt>
<opt>[-r </opt><arg>threshold</arg><opt> | --resync=</opt><arg>threshold</arg><opt>]</opt>
<opt>[--service-type=</opt><arg>type</arg><opt>]</opt>
<opt>[--statistics]</opt>
<opt>[-S </opt><arg>mode</arg><opt> | --stuffing=</opt><arg>mode</arg><opt>]</opt>
<opt>[-t </opt><arg>timeout</arg><opt> | --timeout=</opt><arg>timeout</arg><opt>]</opt>
@@ -203,6 +205,15 @@
</p></optdesc>
</option>
<option>
<p><opt>--dbus-default-message-bus=</opt><arg>bus</arg></p>
<optdesc><p>
Use the <arg>bus</arg> specified for the native D-Bus and MPRIS interfaces.
The <arg>bus</arg> must be <opt>system</opt> or <opt>session</opt>. This option is
only available if Shairport Sync is built with the D-Bus or MPRIS interface.
</p></optdesc>
</option>
<option>
<p><opt>-E </opt><arg>program</arg><opt> | --on-stop=</opt><arg>program</arg></p>
<optdesc><p>
@@ -350,6 +361,23 @@
</p></optdesc>
</option>
<option>
<p><opt>--service-type=</opt><arg>type</arg></p>
<optdesc><p>
On an AirPlay-2-capable Shairport Sync, if you need offer only the older
"classic" AirPlay (sometimes called AirPlay 1) service, set
<opt>--service-type=classic</opt>.
A potential use of this would be for Apple Music on Windows, which is not compatible
with the AirPlay 2 service offered by Shairport Sync,
but which is compatible with the classic-only service.
To offer AirPlay 2 service only, set <opt>--service-type=airplay2</opt> (note that this
requires NQPTP).
Set <opt>--service-type=auto</opt> to allow Shairport Sync to choose AirPlay 2
service if NQPTP is present or classic AirPlay service if not, with
"(Classic)" appended to the default service name.
</p></optdesc>
</option>
<option>
<p><opt>--statistics</opt></p>
<optdesc><p>
@@ -365,11 +393,13 @@
process of adding or removing frames of audio to or from the
stream sent to the output device in order to keep it synchronised
with the player.
The <opt>basic</opt> mode is normally almost completely inaudible.
The alternative mode, <opt>soxr</opt>, is even less obtrusive but
The <opt>vernier</opt> mode is normally almost completely inaudible and suitable for low-powered devices.
An alternative mode, <opt>soxr</opt>, is less obtrusive but
requires much more processing power. For this mode, support for
<opt>libsoxr</opt>, the SoX Resampler Library, must be selected when
<opt>shairport-sync</opt> is built.
The <opt>basic</opt> mode is also suitable for low-powered device,
but <opt>vernier</opt> is better.
The default setting, <opt>auto</opt>, allows Shairport Sync to choose
<opt>soxr</opt> mode if the system is powerful enough.
@@ -485,6 +515,10 @@
</section>
<section name="Note">
<p>The <opt>dacquery</opt> utility (<url href="https://github.com/mikebrady/dacquery"/>) might be useful for discovering the properties of your system's DACs.</p>
</section>
<section name="Credits">
<p>Mike Brady (<url href="https://github.com/mikebrady"/>) developed Shairport Sync from Shairport by James Wah (<url href="https://github.com/abrasive"/>).</p>
</section>
+19 -11
View File
@@ -32,7 +32,11 @@
#include "common.h"
#include "mdns.h"
#include "rtsp.h"
#ifdef CONFIG_METADATA
#include "metadata/core.h"
#endif
#ifdef CONFIG_DACP_CLIENT
#include "dacp.h"
#endif
@@ -86,7 +90,7 @@ static void resolve_callback(AvahiServiceResolver *r, AVAHI_GCC_UNUSED AvahiIfIn
AVAHI_GCC_UNUSED AvahiProtocol protocol, AvahiResolverEvent event,
const char *name, const char *type, const char *domain,
__attribute__((unused)) const char *host_name,
#ifdef CONFIG_METADATA
#if defined(CONFIG_METADATA) || defined(CONFIG_DACP_CLIENT)
__attribute__((unused)) const AvahiAddress *address, uint16_t lport,
#else
__attribute__((unused)) const AvahiAddress *address,
@@ -115,14 +119,14 @@ static void resolve_callback(AvahiServiceResolver *r, AVAHI_GCC_UNUSED AvahiIfIn
while (*dacpid == '0')
dacpid++; // skip any leading zeroes
if (strcmp(dacpid, dbs->dacp_id) == 0) {
debug(3, "resolve_callback: client dacp_id \"%s\" dacp port: %u.", dbs->dacp_id, port);
debug(4, "resolve_callback: client dacp_id \"%s\" dacp port: %u.", dbs->dacp_id, lport);
#ifdef CONFIG_DACP_CLIENT
dacp_monitor_port_update_callback(dacpid, port);
dacp_monitor_port_update_callback(dacpid, lport);
#endif
#ifdef CONFIG_METADATA
uint32_t nport = lport;
nport = htonl(nport);
send_ssnc_metadata('dapo', (const char *)&nport, sizeof(nport), 0);
char port_in_chars[16];
snprintf(port_in_chars, 7, "%u", lport);
send_ssnc_metadata('dapo', port_in_chars, strlen(port_in_chars), 0);
#endif
}
} else {
@@ -162,19 +166,23 @@ static void browse_callback(AvahiServiceBrowser *b, AvahiIfIndex interface, Avah
break;
case AVAHI_BROWSER_REMOVE:
debug(3, "(Browser) REMOVE: service '%s' of type '%s' in domain '%s'.", name, type, domain);
#ifdef CONFIG_DACP_CLIENT
dacp_browser_struct *dbs = (dacp_browser_struct *)userdata;
char *dacpid = strstr(name, "iTunes_Ctrl_");
const char *dacpid = strstr(name, "iTunes_Ctrl_");
if (dacpid) {
dacpid += strlen("iTunes_Ctrl_");
while (*dacpid == '0')
dacpid++; // skip any leading zeroes
if ((dbs->dacp_id) && (strcmp(dacpid, dbs->dacp_id) == 0))
if ((dbs->dacp_id) && (strcmp(dacpid, dbs->dacp_id) == 0)) {
#ifdef CONFIG_DACP_CLIENT
dacp_monitor_port_update_callback(dbs->dacp_id, 0); // say the port is withdrawn
#endif
#ifdef CONFIG_METADATA
send_ssnc_metadata('dapo', "0", strlen("0"), 0);
#endif
}
} else {
debug(1, "Browse callback: Can't see a DACP string in a DACP Record!");
}
#endif
break;
case AVAHI_BROWSER_ALL_FOR_NOW:
case AVAHI_BROWSER_CACHE_EXHAUSTED:
+305
View File
@@ -0,0 +1,305 @@
/*
* Core metadata handler.
* This file is part of Shairport Sync.
* Copyright (c) Mike Brady 2017--2026
* All rights reserved.
*
* 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.
*/
#include "core.h"
#include "pc_queue.h"
#include <errno.h>
#include <inttypes.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#ifdef CONFIG_METADATA_PIPE
#include "pipe.h"
#endif
#ifdef CONFIG_METADATA_MULTICAST
#include "multicast.h"
#endif
#ifdef CONFIG_METADATA_HUB
#include "hub.h"
#endif
#ifdef CONFIG_MQTT
#include "mqtt.h"
#endif
// Metadata is not used by shairport-sync.
// Instead we send all metadata to a fifo pipe, so that other apps can listen to
// the pipe and use the metadata.
// We use two 4-character codes to identify each piece of data and we send the
// data itself, if any,
// in base64 form.
// The first 4-character code, called the "type", is either:
// 'core' for all the regular metadadata coming from iTunes, etc., or
// 'ssnc' (for 'shairport-sync') for all metadata coming from Shairport Sync
// itself, such as
// start/end delimiters, etc.
// For 'core' metadata, the second 4-character code is the 4-character metadata
// code coming from
// iTunes etc.
// For 'ssnc' metadata, the second 4-character code is used to distinguish the
// messages.
// Cover art is not tagged in the same way as other metadata, it seems, so is
// sent as an 'ssnc' type
// metadata message with the code 'PICT'
// Here are the 'ssnc' codes defined so far:
// 'PICT' -- the payload is a picture, either a JPEG or a PNG. Check the
// first few bytes to see
// which.
// 'abeg' -- active mode entered. No arguments
// 'aend' -- active mode exited. No arguments
// 'pbeg' -- play stream begin. No arguments
// 'pend' -- play stream end. No arguments
// 'pfls' -- play stream flush. The argument is an unsigned 32-bit
// frame number. It seems that all frames up to but not
// including this frame are to be flushed.
//
// 'prsm' -- play stream resume. No arguments. (deprecated)
// 'paus' -- buffered audio stream paused. No arguments.
// 'pres' -- buffered audio stream resumed. No arguments.
// 'pffr' -- the first frame of a play session has been received and has been validly
// timed. The argument is
// "<frame_number>/<time_it_should_be_played_in_64_bit_nanoseconds>"."
// 'pdis' -- a discontinuity in the timestamps of incoming frames has been detected.
// timed. The argument is "<frame_number>/<discontinuity>".
// discontinuity is the actual frame number less the expected frame number
// positive means a gap
// 'pvol' -- play volume. The volume is sent as a string --
// "airplay_volume,volume,lowest_volume,highest_volume"
// volume, lowest_volume and highest_volume are given in dB.
// The "airplay_volume" is what's sent to the player, and is from
// 0.00 down to -30.00,
// with -144.00 meaning mute.
// This is linear on the volume control slider of iTunes or iOS
// AirPlay.
// 'prgr' -- progress -- this is metadata from AirPlay consisting of RTP
// timestamps for the start
// of the current play sequence, the current play point and the end of the
// play sequence.
// I guess the timestamps wrap at 2^32.
// 'mdst' -- a sequence of metadata is about to start; will have, as data,
// the rtptime associated with the metadata, if available
// 'mden' -- a sequence of metadata has ended; will have, as data, the
// rtptime associated with the metadata, if available
// 'pcst' -- a picture is about to be sent; will have, as data, the rtptime
// associated with the picture, if available
// 'pcen' -- a picture has been sent; will have, as data, the rtptime
// associated with the metadata, if available
// 'snam' -- A device -- e.g. "Joe's iPhone" -- has opened a play session.
// Specifically, it's the "X-Apple-Client-Name" string
// 'snua' -- A "user agent" -- e.g. "iTunes/12..." -- has opened a play
// session. Specifically, it's the "User-Agent" string
// The next two two tokens are to facilitate remote control of the source.
// There is some information at http://nto.github.io/AirPlay.html about
// remote control of the source.
//
// 'daid' -- this is the source's DACP-ID (if it has one -- it's not
// guaranteed), useful if you want to remotely control the source. Use this
// string to identify the source's remote control on the network.
// 'acre' -- this is the source's Active-Remote token, necessary if you want
// to send commands to the source's remote control (if it has one).
// `clip` -- the payload is the IP number of the client, i.e. the sender of audio.
// Can be an IPv4 or an IPv6 number. In AirPlay 2 operation, it is sent as soon
// as the client has exclusive access to the player and after any existing
// play session has been interrupted and terminated.
// `conn` -- the payload is the IP number of the client, i.e. the sender of audio.
// Can be an IPv4 or an IPv6 number. This is an AirPlay-2-only message.
// It is sent as soon as the client requests access to the player.
// If Shairport Sync is already playing, this message is sent before the current
// play session is stopped.
// `svip` -- the payload is the IP number of the server, i.e. the player itself.
// Can be an IPv4 or an IPv6 number.
// `svna` -- the payload is the service name of the player, i.e. the name by
// which it is seen in the AirPlay menu.
// `disc` -- the payload is the IP number of the client, i.e. the sender of audio.
// Can be an IPv4 or an IPv6 number. This is an AirPlay-2-only message.
// It is sent when a client has been disconnected.
// `dapo` -- the payload is the port number (as text) on the server to which remote
// control commands should be sent. It is 3689 for iTunes but varies for iOS devices.
// ``
int metadata_running = 0;
void metadata_pack_cleanup_function(void *arg) {
// debug(1, "metadata_pack_cleanup_function called");
metadata_package *pack = (metadata_package *)arg;
if (pack->carrier)
msg_free(&pack->carrier); // release the message
else if (pack->data)
free(pack->data);
// debug(1, "metadata_pack_cleanup_function exit");
}
void metadata_init(void) {
#ifdef CONFIG_METADATA_PIPE
metadata_pipe_queue_init();
#endif
#ifdef CONFIG_METADATA_MULTICAST
metadata_multicast_queue_init();
#endif
#ifdef CONFIG_METADATA_HUB
metadata_hub_queue_init();
#endif
#ifdef CONFIG_MQTT
metadata_mqtt_queue_init();
#endif
metadata_running = 1;
}
void metadata_stop(void) {
if (metadata_running) {
debug(2, "metadata_stop called.");
#ifdef CONFIG_MQTT
metadata_mqtt_queue_stop();
#endif
#ifdef CONFIG_METADATA_HUB
metadata_hub_queue_stop();
#endif
#ifdef CONFIG_METADATA_MULTICAST
metadata_multicast_queue_stop();
#endif
#ifdef CONFIG_METADATA_PIPE
metadata_pipe_queue_stop();
#endif
}
}
int send_metadata_to_queue(pc_queue *queue, const uint32_t type, const uint32_t code,
const char *data, const uint32_t length, rtsp_message *carrier,
int block) {
// clang-format off
// parameters:
// type,
// code,
// pointer to data or NULL,
// length of data or NULL,
// the rtsp_message or NULL,
// the rtsp_message is sent for 'core' messages, because it contains the data
// and must not be freed until the data has been read.
// So, it is passed to send_metadata to be retained, sent to the thread where metadata
// is processed and released (and probably freed).
// The rtsp_message is also sent for certain non-'core' messages.
// The reading of the parameters is a bit complex:
// If the rtsp_message field is non-null, then it represents an rtsp_message
// and the data pointer is assumed to point to something within it.
// The reference counter of the rtsp_message is incremented here and
// should be decremented by the metadata handler when finished.
// If the reference count reduces to zero, the message will be freed.
// If the rtsp_message is NULL, then if the pointer is non-null then the data it
// points to, of the length specified, is memcpy'd and passed to the metadata
// handler. The handler should free it when done.
// If the rtsp_message is NULL and the pointer is also NULL, nothing further
// is done.
// clang-format on
metadata_package pack;
pack.type = type;
pack.code = code;
pack.length = length;
pack.carrier = carrier;
pack.data = (char *)data;
if (pack.carrier) {
msg_retain(pack.carrier);
} else {
if (data)
pack.data = memdup(data, length); // only if it's not a null
}
// debug(1, "send_metadata_to_queue %x/%x", type, code);
int rc = pc_queue_add_item(queue, &pack, block);
if (rc != 0) {
if (pack.carrier) {
if (rc == EWOULDBLOCK)
debug(2,
"metadata queue \"%s\" full, dropping message item: type %x, code %x, data %" PRIxPTR
", "
"length %u, message %d.",
queue->name, pack.type, pack.code, (uintptr_t)pack.data, pack.length,
pack.carrier->index_number);
msg_free(&pack.carrier);
} else {
if (rc == EWOULDBLOCK)
debug(2,
"metadata queue \"%s\" full, dropping data item: type %x, code %x, data %" PRIxPTR
", length %u.",
queue->name, pack.type, pack.code, (uintptr_t)pack.data, pack.length);
if (pack.data)
free(pack.data);
}
}
return rc;
}
int send_metadata(const uint32_t type, const uint32_t code, const char *data, const uint32_t length,
rtsp_message *carrier, int block) {
int rc = 0;
if (config.metadata_enabled) {
#ifdef CONFIG_METADATA_PIPE
rc = send_metadata_to_pipe_queue(type, code, data, length, carrier, block);
#endif
#ifdef CONFIG_METADATA_MULTICAST
rc = send_metadata_to_multicast_queue(type, code, data, length, carrier, block);
#endif
#ifdef CONFIG_METADATA_HUB
rc = send_metadata_to_hub_queue(type, code, data, length, carrier, block);
#endif
#ifdef CONFIG_MQTT
rc = send_metadata_to_mqtt_queue(type, code, data, length, carrier, block);
#endif
}
return rc;
}
int send_ssnc_metadata(const uint32_t code, const char *data, const uint32_t length,
const int block) {
return send_metadata('ssnc', code, data, length, NULL, block);
}
+38
View File
@@ -0,0 +1,38 @@
#pragma once
#include "../rtsp.h"
#include "common.h"
#include "config.h"
#include "pc_queue.h"
#include <pthread.h>
typedef struct {
uint32_t type;
uint32_t code;
char *data;
uint32_t length;
rtsp_message *carrier;
} metadata_package;
void metadata_pack_cleanup_function(void *arg);
// initialise and completely delete the metadata stuff
void metadata_init(void);
void metadata_stop(void);
int send_metadata_to_queue(pc_queue *queue, const uint32_t type, const uint32_t code,
const char *data, const uint32_t length, rtsp_message *carrier,
int block);
// sends metadata out to the metadata system.
int send_metadata(const uint32_t type, const uint32_t code, const char *data, const uint32_t length,
rtsp_message *carrier, int block);
// It is sent with the type 'ssnc' the given code, data and length
// The handler at the other end must know what to do with the data
// e.g. if it's malloced, to free it, etc.
// nothing is done automatically
int send_ssnc_metadata(const uint32_t code, const char *data, const uint32_t length,
const int block);
+62 -2
View File
@@ -5,7 +5,7 @@
* then you need a metadata hub,
* where everything is stored
* This file is part of Shairport Sync.
* Copyright (c) Mike Brady 2017--2025
* Copyright (c) Mike Brady 2017--2026
* All rights reserved.
*
* Permission is hereby granted, free of charge, to any person
@@ -43,8 +43,10 @@
#include "config.h"
#include "common.h"
#include "core.h"
#include "dacp.h"
#include "metadata_hub.h"
#include "hub.h"
#include "pc_queue.h"
#ifdef CONFIG_MBEDTLS
#include <mbedtls/md5.h>
@@ -59,6 +61,12 @@
#include <openssl/evp.h>
#endif
// metadata queue definitions
pc_queue metadata_hub_queue;
#define metadata_hub_queue_size 500
metadata_package metadata_hub_queue_items[metadata_hub_queue_size];
pthread_t metadata_hub_thread;
struct metadata_bundle metadata_store;
int metadata_hub_initialised = 0;
@@ -710,3 +718,55 @@ void metadata_hub_process_metadata(uint32_t type, uint32_t code, char *data, uin
pthread_cleanup_pop(0); // don't remove the lock
metadata_hub_modify_epilog(changed);
}
void metadata_hub_close(void) {}
void metadata_hub_thread_cleanup_function(__attribute__((unused)) void *arg) {
// debug(2, "metadata_hub_thread_cleanup_function called");
metadata_hub_close();
}
void *metadata_hub_thread_function(__attribute__((unused)) void *ignore) {
// #include <syscall.h>
// debug(1, "metadata_hub_thread_function PID %d", syscall(SYS_gettid));
metadata_package pack;
pthread_cleanup_push(metadata_hub_thread_cleanup_function, NULL);
while (1) {
pc_queue_get_item(&metadata_hub_queue, &pack);
pthread_cleanup_push(metadata_pack_cleanup_function, (void *)&pack);
if (pack.carrier) {
debug(4, " hub: type %x, code %x, length %u, message %d.", pack.type,
pack.code, pack.length, pack.carrier->index_number);
} else {
debug(4, " hub: type %x, code %x, length %u.", pack.type, pack.code,
pack.length);
}
metadata_hub_process_metadata(pack.type, pack.code, pack.data, pack.length);
debug(4, " hub: done.");
pthread_cleanup_pop(1);
}
pthread_cleanup_pop(1); // will never happen
pthread_exit(NULL);
}
void metadata_hub_queue_init() {
// create a pc_queue for the metadata hub
pc_queue_init(&metadata_hub_queue, (char *)&metadata_hub_queue_items, sizeof(metadata_package),
metadata_hub_queue_size, "hub");
if (named_pthread_create(&metadata_hub_thread, NULL, metadata_hub_thread_function, NULL,
"metadata hub") != 0)
debug(1, "Failed to create metadata hub thread!");
}
void metadata_hub_queue_stop() {
// debug(2, "metadata stop hub thread.");
pthread_cancel(metadata_hub_thread);
pthread_join(metadata_hub_thread, NULL);
pc_queue_delete(&metadata_hub_queue);
// debug(2, "metadata stop hub done.");
}
int send_metadata_to_hub_queue(const uint32_t type, const uint32_t code, const char *data,
const uint32_t length, rtsp_message *carrier, int block) {
return send_metadata_to_queue(&metadata_hub_queue, type, code, data, length, carrier, block);
}
+7
View File
@@ -1,6 +1,7 @@
#pragma once
#include "common.h"
#include "config.h"
#include "rtsp.h"
#include <pthread.h>
#define number_of_watchers 2
@@ -189,3 +190,9 @@ void _metadata_hub_read_epilog(const char *filename, const int linenumber);
#define metadata_hub_read_prolog(void) _metadata_hub_read_prolog(__FILE__, __LINE__)
#define metadata_hub_read_epilog(void) _metadata_hub_modify_epilog(__FILE__, __LINE__)
// metadata queue stuff
void metadata_hub_queue_init();
void metadata_hub_queue_stop();
int send_metadata_to_hub_queue(const uint32_t type, const uint32_t code, const char *data,
const uint32_t length, rtsp_message *carrier, int block);
+179
View File
@@ -0,0 +1,179 @@
#include "multicast.h"
#include "core.h"
#include "pc_queue.h"
#include <stdlib.h>
#include <string.h>
// A special sub-protocol is used for sending large data items over UDP
// If the payload exceeded 4 MB, it is chunked using the following format:
// "ssnc", "chnk", packet_ix, packet_counts, packet_tag, packet_type, chunked_data.
// Notice that the number of items is different to the standard
#define METADATA_SNDBUF (4 * 1024 * 1024)
static int metadata_sock = -1;
static struct sockaddr_in metadata_sockaddr;
static char *metadata_sockmsg;
pc_queue metadata_multicast_queue;
#define metadata_multicast_queue_size 500
metadata_package metadata_multicast_queue_items[metadata_multicast_queue_size];
pthread_t metadata_multicast_thread;
int send_metadata_to_multicast_queue(const uint32_t type, const uint32_t code, const char *data,
const uint32_t length, rtsp_message *carrier, int block) {
return send_metadata_to_queue(&metadata_multicast_queue, type, code, data, length, carrier,
block);
}
void metadata_create_multicast_socket(void) {
if (config.metadata_enabled == 0)
return;
// Unlike metadata pipe, socket is opened once and stays open,
// so we can call it in create
if (config.metadata_sockaddr && config.metadata_sockport) {
metadata_sock = socket(AF_INET, SOCK_DGRAM, 0);
if (metadata_sock < 0) {
debug(1, "Could not open metadata socket");
} else {
int buffer_size = METADATA_SNDBUF;
setsockopt(metadata_sock, SOL_SOCKET, SO_SNDBUF, &buffer_size, sizeof(buffer_size));
bzero((char *)&metadata_sockaddr, sizeof(metadata_sockaddr));
metadata_sockaddr.sin_family = AF_INET;
metadata_sockaddr.sin_addr.s_addr = inet_addr(config.metadata_sockaddr);
metadata_sockaddr.sin_port = htons(config.metadata_sockport);
metadata_sockmsg = malloc(config.metadata_sockmsglength);
if (metadata_sockmsg) {
memset(metadata_sockmsg, 0, config.metadata_sockmsglength);
} else {
die("Could not malloc metadata multicast socket buffer");
}
}
}
}
void metadata_delete_multicast_socket(void) {
if (config.metadata_enabled == 0)
return;
if (metadata_sock != -1) {
shutdown(metadata_sock, SHUT_RDWR); // we want to immediately deallocate the buffer
close(metadata_sock);
}
if (metadata_sockmsg)
free(metadata_sockmsg);
}
void metadata_multicast_process(uint32_t type, uint32_t code, char *data, uint32_t length) {
// debug(1, "Process multicast metadata with type %x, code %x and length %u.", type, code,
// length);
if (metadata_sock >= 0 && length < config.metadata_sockmsglength - 8) {
char *ptr = metadata_sockmsg;
uint32_t v;
v = htonl(type);
memcpy(ptr, &v, 4);
ptr += 4;
v = htonl(code);
memcpy(ptr, &v, 4);
ptr += 4;
memcpy(ptr, data, length);
sendto(metadata_sock, metadata_sockmsg, length + 8, 0, (struct sockaddr *)&metadata_sockaddr,
sizeof(metadata_sockaddr));
} else if (metadata_sock >= 0) {
// send metadata in numbered chunks using the protocol:
// ("ssnc", "chnk", packet_ix, packet_counts, packet_tag, packet_type, chunked_data)
uint32_t chunk_ix = 0;
if (config.metadata_sockmsglength == 24)
die("A divide by zero almost occurred (config.metadata_sockmsglength = 24).");
uint32_t chunk_total = length / (config.metadata_sockmsglength - 24);
if (chunk_total * (config.metadata_sockmsglength - 24) < length) {
chunk_total++;
}
uint32_t remaining = length;
uint32_t v;
char *data_crsr = data;
do {
char *ptr = metadata_sockmsg;
memcpy(ptr, "ssncchnk", 8);
ptr += 8;
v = htonl(chunk_ix);
memcpy(ptr, &v, 4);
ptr += 4;
v = htonl(chunk_total);
memcpy(ptr, &v, 4);
ptr += 4;
v = htonl(type);
memcpy(ptr, &v, 4);
ptr += 4;
v = htonl(code);
memcpy(ptr, &v, 4);
ptr += 4;
size_t datalen = remaining;
if (datalen > config.metadata_sockmsglength - 24) {
datalen = config.metadata_sockmsglength - 24;
}
memcpy(ptr, data_crsr, datalen);
data_crsr += datalen;
sendto(metadata_sock, metadata_sockmsg, datalen + 24, 0,
(struct sockaddr *)&metadata_sockaddr, sizeof(metadata_sockaddr));
chunk_ix++;
remaining -= datalen;
if (remaining == 0)
break;
} while (1);
}
}
void metadata_multicast_thread_cleanup_function(__attribute__((unused)) void *arg) {
// debug(2, "metadata_multicast_thread_cleanup_function called");
metadata_delete_multicast_socket();
}
void *metadata_multicast_thread_function(__attribute__((unused)) void *ignore) {
// #include <syscall.h>
// debug(1, "metadata_multicast_thread_function PID %d", syscall(SYS_gettid));
metadata_create_multicast_socket();
metadata_package pack;
pthread_cleanup_push(metadata_multicast_thread_cleanup_function, NULL);
while (1) {
pc_queue_get_item(&metadata_multicast_queue, &pack);
pthread_cleanup_push(metadata_pack_cleanup_function, (void *)&pack);
if (config.metadata_enabled) {
if (pack.carrier) {
debug(4,
" multicast: type "
"%x, code %x, length %u, message %d.",
pack.type, pack.code, pack.length, pack.carrier->index_number);
} else {
debug(4,
" multicast: type "
"%x, code %x, length %u.",
pack.type, pack.code, pack.length);
}
metadata_multicast_process(pack.type, pack.code, pack.data, pack.length);
debug(4,
" multicast: done.");
}
pthread_cleanup_pop(1);
}
pthread_cleanup_pop(1); // will never happen
pthread_exit(NULL);
}
void metadata_multicast_queue_init() {
// create a pc_queue for the metadata_multicast_queue
pc_queue_init(&metadata_multicast_queue, (char *)&metadata_multicast_queue_items,
sizeof(metadata_package), metadata_multicast_queue_size, "multicast");
if (named_pthread_create(&metadata_multicast_thread, NULL, metadata_multicast_thread_function,
NULL, "metadata mcst") != 0)
debug(1, "Failed to create metadata multicast thread!");
}
void metadata_multicast_queue_stop() {
if (metadata_multicast_thread) {
pthread_cancel(metadata_multicast_thread);
pthread_join(metadata_multicast_thread, NULL);
pc_queue_delete(&metadata_multicast_queue);
// debug(2, "metadata stop multicast done.");
}
}
+9
View File
@@ -0,0 +1,9 @@
#pragma once
#include "rtsp.h"
#include <inttypes.h>
void metadata_multicast_queue_init();
void metadata_multicast_queue_stop();
int send_metadata_to_multicast_queue(const uint32_t type, const uint32_t code, const char *data,
const uint32_t length, rtsp_message *carrier, int block);
+171
View File
@@ -0,0 +1,171 @@
/*
* Producer--consumer queue and access methods.
* This file is part of Shairport Sync.
* Copyright (c) Mike Brady 2017--2026
* All rights reserved.
*
* 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.
*/
#include "pc_queue.h"
#include <errno.h>
#include <stdlib.h>
#include <string.h>
void pc_queue_init(pc_queue *the_queue, char *items, size_t item_size, uint32_t number_of_items,
const char *name) {
if (name)
debug(4, "Creating metadata queue \"%s\".", name);
else
debug(1, "Creating an unnamed metadata queue.");
pthread_mutex_init(&the_queue->pc_queue_lock, NULL);
pthread_cond_init(&the_queue->pc_queue_item_added_signal, NULL);
pthread_cond_init(&the_queue->pc_queue_item_removed_signal, NULL);
the_queue->item_size = item_size;
the_queue->items = items;
the_queue->count = 0;
the_queue->capacity = number_of_items;
the_queue->toq = 0;
the_queue->eoq = 0;
if (name == NULL)
the_queue->name = NULL;
else
the_queue->name = strdup(name);
}
void pc_queue_delete(pc_queue *the_queue) {
if (the_queue->name)
debug(2, "Deleting metadata queue \"%s\".", the_queue->name);
else
debug(1, "Deleting an unnamed metadata queue.");
if (the_queue->name != NULL)
free(the_queue->name);
// debug(2, "destroying pc_queue_item_removed_signal");
pthread_cond_destroy(&the_queue->pc_queue_item_removed_signal);
// debug(2, "destroying pc_queue_item_added_signal");
pthread_cond_destroy(&the_queue->pc_queue_item_added_signal);
// debug(2, "destroying pc_queue_lock");
pthread_mutex_destroy(&the_queue->pc_queue_lock);
// debug(2, "destroying signals and locks done");
}
void pc_queue_cleanup_handler(void *arg) {
// debug(1, "pc_queue_cleanup_handler called.");
pc_queue *the_queue = (pc_queue *)arg;
int rc = pthread_mutex_unlock(&the_queue->pc_queue_lock);
if (rc)
debug(1, "Error unlocking for pc_queue_add_item or pc_queue_get_item.");
}
int pc_queue_add_item(pc_queue *the_queue, const void *the_stuff, int block) {
int response = 0;
int rc;
if (the_queue) {
if (block == 0) {
rc = debug_mutex_lock(&the_queue->pc_queue_lock, 10000, 4);
if (rc == EBUSY)
return EBUSY;
} else
rc = debug_mutex_lock(&the_queue->pc_queue_lock, 50000, 4);
if (rc)
debug(1, "Error %d (\"%s\") locking for pc_queue_add_item. Block is %d.", rc, strerror(rc),
block);
pthread_cleanup_push(pc_queue_cleanup_handler, (void *)the_queue);
// leave this out if you want this to return if the queue is already full
// irrespective of the block flag.
/*
while (the_queue->count == the_queue->capacity) {
rc = pthread_cond_wait(&the_queue->pc_queue_item_removed_signal,
&the_queue->pc_queue_lock); if (rc) debug(1, "Error waiting for item to be removed");
}
*/
if (the_queue->count < the_queue->capacity) {
uint32_t i = the_queue->eoq;
void *p = the_queue->items + the_queue->item_size * i;
// void * p = &the_queue->qbase + the_queue->item_size*the_queue->eoq;
memcpy(p, the_stuff, the_queue->item_size);
// update the pointer
i++;
if (i == the_queue->capacity)
// fold pointer if necessary
i = 0;
the_queue->eoq = i;
the_queue->count++;
// debug(2,"metadata queue+ \"%s\" %d/%d.", the_queue->name, the_queue->count,
// the_queue->capacity);
if (the_queue->count == the_queue->capacity)
debug(3, "metadata queue \"%s\": is now full with %d items in it!", the_queue->name,
the_queue->count);
rc = pthread_cond_signal(&the_queue->pc_queue_item_added_signal);
if (rc)
debug(1, "metadata queue \"%s\": error signalling after pc_queue_add_item",
the_queue->name);
} else {
response = EWOULDBLOCK; // a bit arbitrary, this.
debug(3,
"metadata queue \"%s\": is already full with %d items in it. Not adding this item to "
"the queue.",
the_queue->name, the_queue->count);
}
pthread_cleanup_pop(1); // unlock the queue lock.
} else {
debug(1, "Adding an item to a NULL queue");
}
return response;
}
int pc_queue_get_item(pc_queue *the_queue, void *the_stuff) {
int rc;
if (the_queue) {
rc = debug_mutex_lock(&the_queue->pc_queue_lock, 50000, 4);
if (rc)
debug(1, "metadata queue \"%s\": error locking for pc_queue_get_item", the_queue->name);
pthread_cleanup_push(pc_queue_cleanup_handler, (void *)the_queue);
while (the_queue->count == 0) {
rc = pthread_cond_wait(&the_queue->pc_queue_item_added_signal, &the_queue->pc_queue_lock);
if (rc)
debug(1, "metadata queue \"%s\": error waiting for item to be added", the_queue->name);
}
uint32_t i = the_queue->toq;
// void * p = &the_queue->qbase + the_queue->item_size*the_queue->toq;
void *p = the_queue->items + the_queue->item_size * i;
memcpy(the_stuff, p, the_queue->item_size);
// update the pointer
i++;
if (i == the_queue->capacity)
// fold pointer if necessary
i = 0;
the_queue->toq = i;
the_queue->count--;
debug(4, "metadata queue- \"%s\" %d/%d.", the_queue->name, the_queue->count,
the_queue->capacity);
rc = pthread_cond_signal(&the_queue->pc_queue_item_removed_signal);
if (rc)
debug(1, "metadata queue \"%s\": error signalling after pc_queue_get_item", the_queue->name);
pthread_cleanup_pop(1); // unlock the queue lock.
} else {
debug(1, "Removing an item from a NULL queue");
}
return 0;
}
+26
View File
@@ -0,0 +1,26 @@
#pragma once
#include "common.h"
#include "config.h"
#include <pthread.h>
typedef struct {
pthread_mutex_t pc_queue_lock;
pthread_cond_t pc_queue_item_added_signal;
pthread_cond_t pc_queue_item_removed_signal;
char *name;
size_t item_size; // number of bytes in each item
uint32_t count; // number of items in the queue
uint32_t capacity; // maximum number of items
uint32_t toq; // first item to take
uint32_t eoq; // free space at end of queue
void *items; // a pointer to where the items are actually stored
} pc_queue; // producer-consumer queue
void pc_queue_init(pc_queue *the_queue, char *items, size_t item_size, uint32_t number_of_items,
const char *name);
void pc_queue_delete(pc_queue *the_queue);
int pc_queue_add_item(pc_queue *the_queue, const void *the_stuff, int block);
int pc_queue_get_item(pc_queue *the_queue, void *the_stuff);
+185
View File
@@ -0,0 +1,185 @@
#include "pipe.h"
#include "core.h"
#include "pc_queue.h"
#include <errno.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
static int fd = -1; // the pipe file descriptor
pc_queue metadata_queue;
#define metadata_queue_size 500
metadata_package metadata_queue_items[metadata_queue_size];
pthread_t metadata_thread;
void metadata_open(void) {
if (config.metadata_enabled == 0)
return;
size_t pl = strlen(config.metadata_pipename) + 1;
char *path = malloc(pl + 1);
snprintf(path, pl + 1, "%s", config.metadata_pipename);
fd = try_to_open_pipe_for_writing(path);
free(path);
}
static void metadata_close(void) {
if (fd < 0)
return;
close(fd);
fd = -1;
}
void metadata_process(uint32_t type, uint32_t code, char *data, uint32_t length) {
// debug(1, "Process metadata with type %x, code %x and length %u.", type, code, length);
int ret = 0;
// readers may go away and come back
if (fd < 0)
metadata_open();
if (fd < 0)
return;
char thestring[1024];
snprintf(thestring, 1024, "<item><type>%x</type><code>%x</code><length>%u</length>", type, code,
length);
// ret = non_blocking_write(fd, thestring, strlen(thestring));
ret = write(fd, thestring, strlen(thestring));
if (ret < 0) {
// debug(1,"metadata_process error %d exit 1",ret);
return;
}
if ((data != NULL) && (length > 0)) {
snprintf(thestring, 1024, "\n<data encoding=\"base64\">\n");
// ret = non_blocking_write(fd, thestring, strlen(thestring));
ret = write(fd, thestring, strlen(thestring));
if (ret < 0) {
// debug(1,"metadata_process error %d exit 2",ret);
return;
}
// here, we write the data in base64 form using our nice base64 encoder
// but, we break it into lines of 76 output characters, except for the last
// one.
// thus, we send groups of (76/4)*3 = 57 bytes to the encoder at a time
size_t remaining_count = length;
char *remaining_data = data;
// size_t towrite_count;
char outbuf[76];
while ((remaining_count) && (ret >= 0)) {
size_t towrite_count = remaining_count;
if (towrite_count > 57)
towrite_count = 57;
size_t outbuf_size = 76; // size of output buffer on entry, length of result on exit
if (base64_encode_so((unsigned char *)remaining_data, towrite_count, outbuf, &outbuf_size) ==
NULL)
debug(1, "Error encoding base64 data.");
// debug(1,"Remaining count: %d ret: %d, outbuf_size:
// %d.",remaining_count,ret,outbuf_size);
// ret = non_blocking_write(fd, outbuf, outbuf_size);
ret = write(fd, outbuf, outbuf_size);
if (ret < 0) {
// debug(1,"metadata_process error %d exit 3",ret);
return;
}
remaining_data += towrite_count;
remaining_count -= towrite_count;
}
snprintf(thestring, 1024, "</data>");
// ret = non_blocking_write(fd, thestring, strlen(thestring));
ret = write(fd, thestring, strlen(thestring));
if (ret < 0) {
// debug(1,"metadata_process error %d exit 4",ret);
return;
}
}
snprintf(thestring, 1024, "</item>\n");
// ret = non_blocking_write(fd, thestring, strlen(thestring));
ret = write(fd, thestring, strlen(thestring));
if (ret < 0) {
// debug(1,"metadata_process error %d exit 5",ret);
return;
}
}
void metadata_thread_cleanup_function(__attribute__((unused)) void *arg) {
// debug(2, "metadata_thread_cleanup_function called");
metadata_close();
}
void *metadata_thread_function(__attribute__((unused)) void *ignore) {
// #include <syscall.h>
// debug(1, "metadata_thread_function PID %d", syscall(SYS_gettid));
metadata_package pack;
pthread_cleanup_push(metadata_thread_cleanup_function, NULL);
while (1) {
pc_queue_get_item(&metadata_queue, &pack);
pthread_cleanup_push(metadata_pack_cleanup_function, (void *)&pack);
if (config.metadata_enabled) {
if (pack.carrier) {
debug(4, " pipe: type %x, code %x, length %u, message %d.", pack.type, pack.code,
pack.length, pack.carrier->index_number);
} else {
debug(4, " pipe: type %x, code %x, length %u.", pack.type, pack.code, pack.length);
}
metadata_process(pack.type, pack.code, pack.data, pack.length);
debug(4, " pipe: done.");
}
pthread_cleanup_pop(1);
}
pthread_cleanup_pop(1); // will never happen
pthread_exit(NULL);
}
void metadata_pipe_queue_init() {
// create the metadata pipe, if necessary
size_t pl = strlen(config.metadata_pipename) + 1;
char *path = malloc(pl + 1);
snprintf(path, pl + 1, "%s", config.metadata_pipename);
mode_t oldumask = umask(000);
if (mkfifo(path, 0666) && errno != EEXIST)
die("Could not create metadata pipe \"%s\".", path);
umask(oldumask);
debug(1, "metadata pipe name is \"%s\".", path);
// try to open it
fd = try_to_open_pipe_for_writing(path);
// we check that it's not a "real" error. From the "man 2 open" page:
// "ENXIO O_NONBLOCK | O_WRONLY is set, the named file is a FIFO, and no process has the FIFO
// open for reading." Which is okay.
if ((fd == -1) && (errno != ENXIO)) {
char errorstring[1024];
strerror_r(errno, (char *)errorstring, sizeof(errorstring));
debug(1, "metadata_init -- error %d (\"%s\") opening pipe: \"%s\".", errno, (char *)errorstring,
path);
warn("can not open metadata pipe -- error %d (\"%s\") opening pipe: \"%s\".", errno,
(char *)errorstring, path);
}
free(path);
// initialise the metadata queues first, otherwise the might be a race condition
// create a pc_queue for the metadata pipe
pc_queue_init(&metadata_queue, (char *)&metadata_queue_items, sizeof(metadata_package),
metadata_queue_size, "pipe");
if (named_pthread_create(&metadata_thread, NULL, metadata_thread_function, NULL,
"metadata pipe") != 0)
debug(1, "Failed to create metadata thread!");
}
void metadata_pipe_queue_stop() {
if (metadata_thread) {
// debug(2, "metadata stop metadata_thread thread.");
pthread_cancel(metadata_thread);
pthread_join(metadata_thread, NULL);
pc_queue_delete(&metadata_queue);
// debug(2, "metadata_stop finished successfully.");
}
}
int send_metadata_to_pipe_queue(const uint32_t type, const uint32_t code, const char *data,
const uint32_t length, rtsp_message *carrier, int block) {
return send_metadata_to_queue(&metadata_queue, type, code, data, length, carrier, block);
}
+9
View File
@@ -0,0 +1,9 @@
#pragma once
#include "rtsp.h"
#include <inttypes.h>
void metadata_pipe_queue_init();
void metadata_pipe_queue_stop();
int send_metadata_to_pipe_queue(const uint32_t type, const uint32_t code, const char *data,
const uint32_t length, rtsp_message *carrier, int block);
+17 -1
View File
@@ -35,9 +35,11 @@
#include "rtp.h"
#ifdef CONFIG_DACP_CLIENT
#include "dacp.h"
#endif
#include "metadata_hub.h"
#include "metadata/hub.h"
#include "mpris-service.h"
static guint ownerID = 0;
@@ -240,28 +242,36 @@ static gboolean on_handle_quit(MediaPlayer2 *skeleton, GDBusMethodInvocation *in
static gboolean on_handle_next(MediaPlayer2Player *skeleton, GDBusMethodInvocation *invocation,
__attribute__((unused)) gpointer user_data) {
#ifdef CONFIG_DACP_CLIENT
send_simple_dacp_command("nextitem");
#endif
media_player2_player_complete_next(skeleton, invocation);
return TRUE;
}
static gboolean on_handle_previous(MediaPlayer2Player *skeleton, GDBusMethodInvocation *invocation,
__attribute__((unused)) gpointer user_data) {
#ifdef CONFIG_DACP_CLIENT
send_simple_dacp_command("previtem");
#endif
media_player2_player_complete_previous(skeleton, invocation);
return TRUE;
}
static gboolean on_handle_stop(MediaPlayer2Player *skeleton, GDBusMethodInvocation *invocation,
__attribute__((unused)) gpointer user_data) {
#ifdef CONFIG_DACP_CLIENT
send_simple_dacp_command("stop");
#endif
media_player2_player_complete_stop(skeleton, invocation);
return TRUE;
}
static gboolean on_handle_pause(MediaPlayer2Player *skeleton, GDBusMethodInvocation *invocation,
__attribute__((unused)) gpointer user_data) {
#ifdef CONFIG_DACP_CLIENT
send_simple_dacp_command("pause");
#endif
media_player2_player_complete_pause(skeleton, invocation);
return TRUE;
}
@@ -269,14 +279,18 @@ static gboolean on_handle_pause(MediaPlayer2Player *skeleton, GDBusMethodInvocat
static gboolean on_handle_play_pause(MediaPlayer2Player *skeleton,
GDBusMethodInvocation *invocation,
__attribute__((unused)) gpointer user_data) {
#ifdef CONFIG_DACP_CLIENT
send_simple_dacp_command("playpause");
#endif
media_player2_player_complete_play_pause(skeleton, invocation);
return TRUE;
}
static gboolean on_handle_play(MediaPlayer2Player *skeleton, GDBusMethodInvocation *invocation,
__attribute__((unused)) gpointer user_data) {
#ifdef CONFIG_DACP_CLIENT
send_simple_dacp_command("play");
#endif
media_player2_player_complete_play(skeleton, invocation);
return TRUE;
}
@@ -284,11 +298,13 @@ static gboolean on_handle_play(MediaPlayer2Player *skeleton, GDBusMethodInvocati
static gboolean on_handle_set_volume(MediaPlayer2Player *skeleton,
GDBusMethodInvocation *invocation, const gdouble volume,
__attribute__((unused)) gpointer user_data) {
#ifdef CONFIG_DACP_CLIENT
double ap_volume = mpris_volume_to_airplay_volume(volume);
debug(2, "Set mpris volume to %.6f, i.e. airplay volume to %.6f.", volume, ap_volume);
char command[256] = "";
snprintf(command, sizeof(command), "setproperty?dmcp.device-volume=%.6f", ap_volume);
send_simple_dacp_command(command);
#endif
media_player2_player_complete_play(skeleton, invocation);
return TRUE;
}
+77 -6
View File
@@ -8,13 +8,26 @@
#include "common.h"
#include "player.h"
#include "rtsp.h"
#include "utilities/string_utilities.h"
#include "rtp.h"
#ifdef CONFIG_DACP_CLIENT
#include "dacp.h"
#include "metadata_hub.h"
#endif
#include "metadata/core.h"
#include "metadata/hub.h"
#include "metadata/pc_queue.h"
#include "mqtt.h"
#include <mosquitto.h>
// this is for receiving metadata
pc_queue metadata_mqtt_queue;
#define metadata_mqtt_queue_size 500
metadata_package metadata_mqtt_queue_items[metadata_mqtt_queue_size];
pthread_t metadata_mqtt_thread;
// this holds the mosquitto client
struct mosquitto *global_mosq = NULL;
@@ -68,10 +81,12 @@ void on_message(__attribute__((unused)) struct mosquitto *mosq,
debug(2, "[MQTT]: Received Recognized Command: %s\n", commands[it]);
if (strcmp(commands[it], "disconnect") == 0) {
debug(2, "[MQTT]: Disconnect Command: %s\n", commands[it]);
release_play_lock(NULL); // stop any current session and don't replace it
stop_play(); // stop any current session and don't replace it
} else {
debug(2, "[MQTT]: DACP Command: %s\n", commands[it]);
#ifdef CONFIG_DACP_CLIENT
send_simple_dacp_command(commands[it]);
#endif
}
break;
}
@@ -200,8 +215,8 @@ void send_autodiscovery_messages(struct mosquitto *mosq) {
const char *entity_type = is_binary_sensor ? "binary_sensor" : "sensor";
snprintf(topic, sizeof(topic), "%s/%s/%s_%s/%s/config", autodiscovery_prefix,
entity_type, model, device_id_no_colons, sensors[i]);
snprintf(topic, sizeof(topic), "%s/%s/%s_%s/%s/config", autodiscovery_prefix, entity_type,
model, device_id_no_colons, sensors[i]);
snprintf(id_string, sizeof(id_string), "%s_%s_%s", model, device_name, sensors[i]);
@@ -255,8 +270,7 @@ void mqtt_publish(char *topic, char *data_in, uint32_t length_in) {
int rc;
if ((rc = mosquitto_publish(global_mosq, NULL, fulltopic, length, data, 0,
config.mqtt_publish_retain)) !=
MOSQ_ERR_SUCCESS) {
config.mqtt_publish_retain)) != MOSQ_ERR_SUCCESS) {
switch (rc) {
case MOSQ_ERR_NO_CONN:
debug(1, "[MQTT]: Publish failed: not connected to broker");
@@ -443,3 +457,60 @@ int initialise_mqtt() {
return 0;
}
// metadata handling stuff
void metadata_mqtt_close(void) {}
void metadata_mqtt_thread_cleanup_function(__attribute__((unused)) void *arg) {
// debug(2, "metadata_mqtt_thread_cleanup_function called");
metadata_mqtt_close();
// debug(2, "metadata_mqtt_thread_cleanup_function done");
}
void *metadata_mqtt_thread_function(__attribute__((unused)) void *ignore) {
// #include <syscall.h>
// debug(1, "metadata_mqtt_thread_function PID %d", syscall(SYS_gettid));
metadata_package pack;
pthread_cleanup_push(metadata_mqtt_thread_cleanup_function, NULL);
while (1) {
pc_queue_get_item(&metadata_mqtt_queue, &pack);
pthread_cleanup_push(metadata_pack_cleanup_function, (void *)&pack);
if (config.mqtt_enabled) {
if (pack.carrier) {
debug(3,
" mqtt: type %x, code %x, length %u, message "
"%d.",
pack.type, pack.code, pack.length, pack.carrier->index_number);
} else {
debug(3, " mqtt: type %x, code %x, length %u.",
pack.type, pack.code, pack.length);
}
mqtt_process_metadata(pack.type, pack.code, pack.data, pack.length);
debug(3, " mqtt: done.");
}
pthread_cleanup_pop(1);
}
pthread_cleanup_pop(1); // will never happen
pthread_exit(NULL);
}
void metadata_mqtt_queue_init() {
// create a pc_queue for the MQTT handler
pc_queue_init(&metadata_mqtt_queue, (char *)&metadata_mqtt_queue_items, sizeof(metadata_package),
metadata_mqtt_queue_size, "mqtt");
if (named_pthread_create(&metadata_mqtt_thread, NULL, metadata_mqtt_thread_function, NULL,
"metadata mqtt") != 0)
debug(1, "Failed to create metadata mqtt thread!");
}
void metadata_mqtt_queue_stop() {
// debug(2, "metadata stop mqtt thread.");
pthread_cancel(metadata_mqtt_thread);
pthread_join(metadata_mqtt_thread, NULL);
pc_queue_delete(&metadata_mqtt_queue);
// debug(2, "metadata stop mqtt done.");
}
int send_metadata_to_mqtt_queue(const uint32_t type, const uint32_t code, const char *data,
const uint32_t length, rtsp_message *carrier, int block) {
return send_metadata_to_queue(&metadata_mqtt_queue, type, code, data, length, carrier, block);
}
+5
View File
@@ -12,4 +12,9 @@ void on_connect(struct mosquitto *mosq, void *userdata, int rc);
void on_disconnect(struct mosquitto *mosq, void *userdata, int rc);
void on_message(struct mosquitto *mosq, void *userdata, const struct mosquitto_message *msg);
void _cb_log(struct mosquitto *mosq, void *userdata, int level, const char *str);
void metadata_mqtt_queue_init();
void metadata_mqtt_queue_stop();
int send_metadata_to_mqtt_queue(const uint32_t type, const uint32_t code, const char *data,
const uint32_t length, rtsp_message *carrier, int block);
#endif /* #ifndef MQTT_H */
+15 -2
View File
@@ -162,18 +162,21 @@ pair_tlv_parse(const uint8_t *buffer, size_t length, pair_tlv_values_t *values)
size_t i = 0;
int ret;
while (i < length) {
if (i + 2 > length)
return PAIR_TLV_ERROR_INSUFFICIENT_SIZE;
uint8_t type = buffer[i];
size_t size = 0;
uint8_t *data = NULL;
// scan TLVs to accumulate total size of subsequent TLVs with same type (chunked data)
size_t j = i;
while (j < length && buffer[j] == type && buffer[j+1] == 255) {
while (j + 1 < length && buffer[j] == type && buffer[j+1] == 255) {
size_t chunk_size = buffer[j+1];
size += chunk_size;
j += chunk_size + 2;
}
if (j < length && buffer[j] == type) {
if (j + 1 < length && buffer[j] == type) {
size_t chunk_size = buffer[j+1];
size += chunk_size;
}
@@ -188,12 +191,22 @@ pair_tlv_parse(const uint8_t *buffer, size_t length, pair_tlv_values_t *values)
size_t remaining = size;
while (remaining) {
if (i + 2 > length) {
free(data);
return PAIR_TLV_ERROR_INSUFFICIENT_SIZE;
}
size_t chunk_size = buffer[i+1];
if (chunk_size > length - i - 2) {
free(data);
return PAIR_TLV_ERROR_INSUFFICIENT_SIZE;
}
memcpy(p, &buffer[i+2], chunk_size);
p += chunk_size;
i += chunk_size + 2;
remaining -= chunk_size;
}
} else {
i += 2;
}
ret = tlv_add_value_(values, type, data, size);
+1 -1
View File
@@ -95,7 +95,7 @@ hash_init(enum hash_alg alg, HashCTX *c)
}
int
hash_update(enum hash_alg alg, HashCTX *c, const void *data, size_t len)
hash_update(__attribute__((unused)) enum hash_alg alg, HashCTX *c, const void *data, size_t len)
{
#if CONFIG_OPENSSL
switch (alg)
+5 -5
View File
@@ -593,7 +593,7 @@ encrypt_ctr(unsigned char *ciphertext, int ciphertext_len,
/* -------------------------- IMPLEMENTATION -------------------------------- */
static int
client_setup_new(struct pair_setup_context *handle, const char *pin, pair_cb add_cb, void *cb_arg, const char *device_id)
client_setup_new(struct pair_setup_context *handle, const char *pin, __attribute__((unused)) pair_cb add_cb, __attribute__((unused)) void *cb_arg, __attribute__((unused)) const char *device_id)
{
struct pair_client_setup_context *sctx = &handle->sctx.client;
@@ -853,7 +853,7 @@ client_setup_result(struct pair_setup_context *handle)
{
struct pair_client_setup_context *sctx = &handle->sctx.client;
char *ptr;
int i;
unsigned int i;
// Last 32 bytes of the private key is the public key, so we don't need to
// explicitly export that
@@ -867,13 +867,13 @@ client_setup_result(struct pair_setup_context *handle)
static int
client_verify_new(struct pair_verify_context *handle, const char *client_setup_keys, pair_cb cb, void *cb_arg, const char *device_id)
client_verify_new(struct pair_verify_context *handle, const char *client_setup_keys, __attribute__((unused)) pair_cb cb, __attribute__((unused)) void *cb_arg, const char *device_id)
{
struct pair_client_verify_context *vctx = &handle->vctx.client;
char hex[] = { 0, 0, 0 };
size_t hexkey_len;
const char *ptr;
int i;
unsigned int i;
if (!is_initialized())
return -1;
@@ -1024,7 +1024,7 @@ client_verify_response1(struct pair_verify_context *handle, const uint8_t *data,
}
static int
client_verify_response2(struct pair_verify_context *handle, const uint8_t *data, size_t data_len)
client_verify_response2(struct pair_verify_context *handle, __attribute__((unused)) const uint8_t *data, __attribute__((unused)) size_t data_len)
{
struct pair_client_verify_context *vctx = &handle->vctx.client;
// TODO actually check response
+4 -4
View File
@@ -822,7 +822,7 @@ static void
hexread(uint8_t *out, size_t out_len, const char *in)
{
char hex[] = { 0, 0, 0 };
int i;
unsigned int i;
for (i = 0; i < out_len; i++, in+=2)
{
@@ -1668,7 +1668,7 @@ static int
client_setup_result(struct pair_setup_context *handle)
{
char *ptr;
int i;
unsigned int i;
assert(sizeof(handle->result_str) >= 2 * sizeof(handle->result.client_private_key) + 2 * sizeof(handle->result.server_public_key) + 1);
@@ -1686,7 +1686,7 @@ client_setup_result(struct pair_setup_context *handle)
}
static int
client_verify_new(struct pair_verify_context *handle, const char *client_setup_keys, pair_cb cb, void *cb_arg, const char *device_id)
client_verify_new(struct pair_verify_context *handle, const char *client_setup_keys, __attribute__((unused)) pair_cb cb, __attribute__((unused)) void *cb_arg, __attribute__((unused)) const char *device_id)
{
struct pair_client_verify_context *vctx = &handle->vctx.client;
size_t hexkey_len;
@@ -2888,7 +2888,7 @@ server_list_response(size_t *len, pair_list_cb cb, void *cb_arg)
}
static int
server_list(uint8_t **out, size_t *out_len, pair_list_cb cb, void *cb_arg, const uint8_t *in, size_t in_len)
server_list(uint8_t **out, size_t *out_len, pair_list_cb cb, void *cb_arg, __attribute__((unused)) const uint8_t *in, __attribute__((unused)) size_t in_len)
{
// Skip reading the request, it just has state = 1 and pair method =
// PairingMethodListPairings
+152 -111
View File
@@ -72,8 +72,12 @@
#include <FFTConvolver/convolver.h>
#endif
#ifdef CONFIG_METADATA
#include "metadata/core.h"
#endif
#ifdef CONFIG_METADATA_HUB
#include "metadata_hub.h"
#include "metadata/hub.h"
#endif
#ifdef CONFIG_DACP_CLIENT
@@ -383,18 +387,6 @@ const char *get_category_string(airplay_stream_c cat) {
#ifdef CONFIG_FFMPEG
static void avcodec_alloc_context3_cleanup_handler(void *arg) {
debug(3, "avcodec_alloc_context3_cleanup_handler");
AVCodecContext *codec_context = arg;
av_free(codec_context);
}
static void avcodec_open2_cleanup_handler(__attribute__((unused)) void *arg) {
debug(3, "avcodec_open2_cleanup_handler");
// AVCodecContext *codec_context = arg;
// avcodec_close(codec_context);
}
static void swr_alloc_cleanup_handler(void *arg) {
debug(3, "swr_alloc_cleanup_handler");
SwrContext **swr = arg;
@@ -407,25 +399,9 @@ static void av_packet_alloc_cleanup_handler(void *arg) {
av_packet_free(pkt);
}
/*
static void av_frame_alloc_cleanup_handler(void *arg) {
debug(3, "av_frame_alloc_cleanup_handler");
AVFrame **frame = arg;
av_frame_free(frame);
}
*/
void clear_decoding_chain(rtsp_conn_info *conn) {
if (conn->incoming_ssrc != 0) {
// debug_mutex_lock(&conn->ab_mutex, 30000, 0);
// ab_resync(conn);
// debug_mutex_unlock(&conn->ab_mutex, 0);
pthread_cleanup_push(avcodec_alloc_context3_cleanup_handler, conn->codec_context);
pthread_cleanup_push(malloc_cleanup, &conn->codec_context->extradata);
pthread_cleanup_push(avcodec_open2_cleanup_handler, conn->codec_context);
pthread_cleanup_pop(1); // avcodec_open2_cleanup_handler
pthread_cleanup_pop(1); // deallocate the malloc
pthread_cleanup_pop(1); // avcodec_alloc_context3_cleanup_handler
avcodec_free_context(&conn->codec_context);
conn->incoming_ssrc = SSRC_NONE;
}
}
@@ -676,28 +652,52 @@ int setup_software_resampler(rtsp_conn_info *conn, ssrc_t ssrc) {
av_opt_set_sample_fmt(swr, "in_sample_fmt", input_format, 0);
// remember that if mixdown is enabled,
// set the resampler's channel layout either automatically or use the
// setting that has been given
// If mixdown is enabled, set the resampler's channel layout,
// either automatically based on the number of ooutput channels,
// or by using the setting that has been given.
// Upmixing will not be done if the setting is "auto".
// Similarly, on the "auto" setting, downmixing will be done
// only if the number of output channels available
// is strictly less than the number of input channels.
// If the number of output channels equals the number of input channels,
// no mixing is done on the "auto" setting.
// To do an upmix or a custom downmix, specify
// the target format, e.g. "7.1" in the mixdown setting.
// NOTE: upmixing, by default, simply copies the input channels to their
// equivalents in the output channels. All other channels are
// left silent.
#if LIBAVUTIL_VERSION_MAJOR >= 57
{
AVChannelLayout input_channel_layout;
av_channel_layout_from_mask(&input_channel_layout, input_layout);
int input_channel_count = input_channel_layout.nb_channels;
av_opt_set_chlayout(swr, "in_chlayout", &input_channel_layout, 0);
av_channel_layout_uninit(&input_channel_layout);
AVChannelLayout output_channel_layout;
if (config.mixdown_enable != 0) {
if (config.mixdown_channel_layout == 0) {
if (CHANNELS_FROM_ENCODED_FORMAT(output_configuration) < (unsigned)input_channel_count) {
av_channel_layout_default(&output_channel_layout,
CHANNELS_FROM_ENCODED_FORMAT(output_configuration));
} else {
av_channel_layout_from_mask(&output_channel_layout, input_layout);
}
} else {
av_channel_layout_from_mask(&output_channel_layout, config.mixdown_channel_layout);
}
} else {
av_channel_layout_from_mask(&output_channel_layout, output_layout);
}
char layout_desc[2048];
av_channel_layout_describe(&output_channel_layout, layout_desc, sizeof(layout_desc));
// debug(1,"output channel layout: \"%s\"", layout_desc);
av_opt_set_chlayout(swr, "out_chlayout", &output_channel_layout, 0);
av_channel_layout_uninit(&output_channel_layout);
}
@@ -705,8 +705,11 @@ int setup_software_resampler(rtsp_conn_info *conn, ssrc_t ssrc) {
av_opt_set_int(swr, "in_channel_layout", input_layout, 0);
if (config.mixdown_enable != 0) {
if (config.mixdown_channel_layout == 0) {
if ((signed)CHANNELS_FROM_ENCODED_FORMAT(output_configuration) <
(av_get_channel_layout_nb_channels(input_layout))) {
output_layout =
av_get_default_channel_layout(CHANNELS_FROM_ENCODED_FORMAT(output_configuration));
} // else leave output_layout as it was: the sames as the input_layout.
} else {
output_layout = config.mixdown_channel_layout;
}
@@ -777,6 +780,9 @@ int setup_software_resampler(rtsp_conn_info *conn, ssrc_t ssrc) {
int64_t resampler_output_format = 0;
int resampler_channels_found = 0;
int oldState;
pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, &oldState);
#if LIBAVUTIL_VERSION_MAJOR >= 57
AVChannelLayout output_channel_layout = {0};
@@ -887,6 +893,8 @@ int setup_software_resampler(rtsp_conn_info *conn, ssrc_t ssrc) {
}
}
pthread_setcancelstate(oldState, NULL);
if (output_channel_map_faulty != 0) {
once(inform("The output device's %u-channel map is incomplete or faulty: \"%s\".",
CHANNELS_FROM_ENCODED_FORMAT(config.current_output_configuration),
@@ -981,7 +989,7 @@ int setup_software_resampler(rtsp_conn_info *conn, ssrc_t ssrc) {
if (output_configuration_changed != 0) {
char channel_mapping_list[256] = "";
for (c = 0; c < 8; c++) {
for (c = 0; c < SPS_GREATEST_CHANNEL_COUNT; c++) {
if ((output_channels[c].allocated != 0) &&
(conn->output_channel_to_resampler_channel_map[c] != silent_channel_index)) {
char channel_mapping[32] = "";
@@ -1028,17 +1036,26 @@ int setup_software_resampler(rtsp_conn_info *conn, ssrc_t ssrc) {
return response; // 0 if everything is okay
}
void prepare_decoding_chain(rtsp_conn_info *conn, ssrc_t ssrc) {
if ((ssrc_is_recognised(ssrc)) && (ssrc != conn->incoming_ssrc)) {
if ((config.statistics_requested != 0) && (ssrc != SSRC_NONE) &&
(conn->incoming_ssrc != SSRC_NONE)) {
debug(2, "Connection %d: incoming audio switching to \"%s\".", conn->connection_number,
get_ssrc_name(ssrc));
if ((ssrc != SSRC_NONE) && (ssrc != conn->incoming_ssrc) && (ssrc_is_recognised(ssrc) != 0)) {
// conn->incoming_ssrc will be SSRC_NONE only before the first valid encoding is found
if ((config.statistics_requested) && (conn->incoming_ssrc != SSRC_NONE))
inform("Connection %d: Incoming Audio Encoding is switching to: \"%s\".",
conn->connection_number, get_ssrc_name(ssrc));
// conn->incoming_ssrc = payload_ssrc;
#ifdef CONFIG_METADATA
send_ssnc_metadata('sdsc', get_ssrc_name(ssrc), strlen(get_ssrc_name(ssrc)), 1);
#endif
}
if ((ssrc_is_recognised(ssrc)) && (ssrc != conn->incoming_ssrc)) {
/*
if ((config.statistics_requested != 0) && (ssrc != SSRC_NONE) &&
(conn->incoming_ssrc != SSRC_NONE)) {
debug(2, "Connection %d: incoming audio switching to \"%s\".", conn->connection_number,
get_ssrc_name(ssrc));
}
*/
// the ssrc of the incoming packet is different to the ssrc of the decoding chain
// so the decoding chain must be rebuilt
@@ -1064,17 +1081,19 @@ void prepare_decoding_chain(rtsp_conn_info *conn, ssrc_t ssrc) {
conn->codec = avcodec_find_decoder(AV_CODEC_ID_AAC);
break;
default:
die("Can't find a suitable codec for SSRC: %s", get_ssrc_name(ssrc));
die("Connection %d: can't find a suitable codec for SSRC: %s", conn->connection_number,
get_ssrc_name(ssrc));
break;
}
int oldState;
pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, &oldState);
// Get a decoder-dependent codec context
conn->codec_context = avcodec_alloc_context3(conn->codec);
if (conn->codec_context == NULL) {
debug(1, "Could not allocate a codec context!");
}
if (conn->codec_context != NULL) {
// push a deallocator -- av_free(codec_context)
pthread_cleanup_push(avcodec_alloc_context3_cleanup_handler, conn->codec_context);
// pthread_cleanup_push(avcodec_alloc_context3_cleanup_handler, &conn->codec_context);
// prepare to open the codec context with that codec
// but first, if it's the ALAC decoder, prepare a magic cookie
@@ -1082,7 +1101,8 @@ void prepare_decoding_chain(rtsp_conn_info *conn, ssrc_t ssrc) {
alac_ffmpeg_magic_cookie *extradata =
malloc(sizeof(alac_ffmpeg_magic_cookie)); // might not use it
if (extradata == NULL)
die("Could not allocate memory for a magic cookie.");
die("connection %d: could not allocate memory for a magic cookie.",
conn->connection_number);
// creata a magic cookie preceded by the 12-byte "atom" (?) expected by FFMPEG (?)
memset(extradata, 0, sizeof(alac_ffmpeg_magic_cookie));
extradata->cookie_size = htonl(sizeof(alac_ffmpeg_magic_cookie));
@@ -1105,29 +1125,29 @@ void prepare_decoding_chain(rtsp_conn_info *conn, ssrc_t ssrc) {
} else {
conn->codec_context->extradata = NULL;
}
pthread_cleanup_push(malloc_cleanup, &conn->codec_context->extradata);
// pthread_cleanup_push(malloc_cleanup, &conn->codec_context->extradata);
// avcodec_free_context() will free extradata
if (avcodec_open2(conn->codec_context, conn->codec, NULL) < 0) {
die("Could not initialise the codec context");
pthread_setcancelstate(oldState, NULL);
die("connection %d: could not initialise the codec context", conn->connection_number);
}
// push a closer -- avcodec_close(codec_context);
pthread_cleanup_push(avcodec_open2_cleanup_handler, conn->codec_context);
} else {
die("connection %d: could not allocate a codec context!", conn->connection_number);
}
pthread_setcancelstate(oldState, NULL);
conn->input_rate = get_ssrc_rate(ssrc);
debug(2, "Connection %d: set conn->input_rate: %u.", conn->connection_number,
conn->input_rate);
if ((ssrc == ALAC_48000_S24_2) || (ssrc == ALAC_44100_S16_2)) {
conn->frames_per_packet = 352;
} else {
conn->frames_per_packet = 1024;
}
conn->codec_context->sample_rate = conn->input_rate;
conn->ffmpeg_decoding_chain_initialised = 1;
pthread_cleanup_pop(0); // successful exit -- don't run the avcodec_open2_cleanup_handler
pthread_cleanup_pop(0); // successful exit -- don't deallocate the malloc
pthread_cleanup_pop(
0); // successful exit -- don't run the avcodec_alloc_context3_cleanup_handler
conn->input_format_is_valid = 1;
}
}
}
@@ -1142,6 +1162,9 @@ int64_t avframe_to_audio(rtsp_conn_info *conn, AVFrame *decoded_frame, uint8_t *
uint8_t *pcm_audio = NULL;
int dst_linesize;
int oldState;
pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, &oldState); // make this un-cancellable
int number_of_output_samples_expected = swr_get_out_samples(conn->swr, decoded_frame->nb_samples);
debug(4, "A maximum of %d output samples expected for %d input samples.",
@@ -1243,10 +1266,12 @@ int64_t avframe_to_audio(rtsp_conn_info *conn, AVFrame *decoded_frame, uint8_t *
*decoded_audio_samples_count = 0;
}
av_freep(&pcm_audio);
return swr_get_delay(
int64_t response = swr_get_delay(
conn->swr,
RATE_FROM_ENCODED_FORMAT(
config.current_output_configuration)); // number of frames left in the resampler
pthread_setcancelstate(oldState, NULL);
return response;
}
// take a block of incoming data and decode it.
@@ -1254,8 +1279,11 @@ int64_t avframe_to_audio(rtsp_conn_info *conn, AVFrame *decoded_frame, uint8_t *
// transcoded and maybe resampled later
AVFrame *block_to_avframe(rtsp_conn_info *conn, uint8_t *incoming_data,
size_t incoming_data_length) {
AVFrame *decoded_frame = NULL;
if (incoming_data_length > 8) {
int oldState;
pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, &oldState); // make this un-cancellable
AVPacket *pkt = av_packet_alloc();
if (pkt) {
// push a deallocator -- av_packet_free(pkt);
@@ -1314,6 +1342,7 @@ AVFrame *block_to_avframe(rtsp_conn_info *conn, uint8_t *incoming_data,
} else {
debug(1, "Can't allocate an AVPacket!");
}
pthread_setcancelstate(oldState, NULL);
}
return decoded_frame;
}
@@ -1567,7 +1596,6 @@ uint32_t player_put_packet(uint32_t ssrc, seq_t seqno, uint32_t actual_timestamp
} else if (config.decoder_in_use == 1 << decoder_ffmpeg_alac) {
#ifdef CONFIG_FFMPEG
prepare_decoding_chain(conn, ALAC_44100_S16_2);
// if (len > 8) {
abuf->avframe = block_to_avframe(conn, data_to_use, len);
abuf->ssrc = ALAC_44100_S16_2;
if (abuf->avframe) {
@@ -1581,14 +1609,13 @@ uint32_t player_put_packet(uint32_t ssrc, seq_t seqno, uint32_t actual_timestamp
av_frame_free(&abuf->avframe);
abuf->avframe = NULL;
}
// } else {
if (len <= 8) {
debug(1,
"Using the FFMPEG ALAC_44100_S16_2 decoder, a short audio packet %u, rtptime %u, of length %zu has been decoded but not discarded. Contents follow:", seqno,
actual_timestamp, len);
debug_print_buffer(1, data, len);
// abuf->length = conn->frames_per_packet;
// abuf->avframe = NULL;
debug(2,
"Connection %d, using FFMPEG on an ALAC_44100_S16_2 stream, a short audio packet "
"%u, rtptime %u, of length %zu has been decoded but not discarded. Contents "
"follow:",
conn->connection_number, seqno, actual_timestamp, len);
debug_print_buffer(2, data, len);
}
#else
debug(1, "FFMPEG support has not been built into this version Shairport Sync!");
@@ -1620,7 +1647,6 @@ uint32_t player_put_packet(uint32_t ssrc, seq_t seqno, uint32_t actual_timestamp
prepare_decoding_chain(conn, ssrc); // dynamically set the decoding environment
// if (len > 8) {
abuf->avframe = block_to_avframe(conn, data, len);
abuf->ssrc = ssrc; // tag the avframe with its specific SSRC
if (abuf->avframe) {
@@ -1633,14 +1659,13 @@ uint32_t player_put_packet(uint32_t ssrc, seq_t seqno, uint32_t actual_timestamp
av_frame_free(&abuf->avframe);
abuf->avframe = NULL;
}
//} else {
if (len <= 8) {
debug(1,
"Using an FFMPEG decoder, a short audio packet %u, rtptime %u, of length %zu has been decoded but not discarded. Contents follow:", seqno,
actual_timestamp, len);
debug_print_buffer(1, data, len);
// abuf->length = 0;
// abuf->avframe = NULL;
debug(2,
"Connection %d: using FFMPEG on a %s stream, a short audio packet %u, rtptime %u, "
"of length %zu has been decoded but not discarded. Contents follow:",
conn->connection_number, get_ssrc_name(ssrc), seqno, actual_timestamp, len);
debug_print_buffer(2, data, len);
}
abuf->ready = 1;
abuf->status = 0; // signifying that it was received
@@ -2032,7 +2057,7 @@ static abuf_t *buffer_get_frame(rtsp_conn_info *conn, int resync_requested) {
do {
// debug(3, "buffer_get_frame is iterating");
// we must have timing information before we can do anything here
if (have_timestamp_timing_information(conn)) {
if ((have_timestamp_timing_information(conn)) && (conn->input_format_is_valid != 0)) {
int rco = get_requested_connection_state_to_output();
@@ -2481,10 +2506,12 @@ static abuf_t *buffer_get_frame(rtsp_conn_info *conn, int resync_requested) {
RATE_FROM_ENCODED_FORMAT(config.current_output_configuration)) /
1000000000;
int64_t exact_frame_gap = gross_frame_gap - dac_delay;
debug(3,
"Exact frame gap: %" PRId64
". DAC delay: %ld. First packet timestamp: %u.",
exact_frame_gap, dac_delay, conn->first_packet_timestamp);
debug(4,
"Exact frame gap: %" PRId64 ". DAC delay: %ld. Total: %" PRId64
". First packet timestamp: %u. ",
exact_frame_gap, dac_delay, gross_frame_gap,
conn->first_packet_timestamp);
// int64_t frames_needed_to_maintain_desired_buffer =
// (int64_t)(config.audio_backend_buffer_desired_length *
// config.current_output_configuration->rate) -
@@ -2701,14 +2728,12 @@ static abuf_t *buffer_get_frame(rtsp_conn_info *conn, int resync_requested) {
wait = 1; // keep waiting until the timing information becomes available
}
if (wait) {
if (conn->frames_per_packet == 0)
debug(1, "frames_per_packet is zero!");
uint64_t time_to_wait_for_wakeup_ns = 20000000; // default if no input rate is set
if (conn->input_rate != 0) {
uint64_t time_to_wait_for_wakeup_ns = 10000000; // default
if (conn->input_format_is_valid != 0) {
time_to_wait_for_wakeup_ns =
1000000000 / conn->input_rate; // this is time period of one frame
time_to_wait_for_wakeup_ns *= 4 * conn->frames_per_packet;
time_to_wait_for_wakeup_ns *=
4 * conn->frames_per_packet; // about 4 * 7 mS for 352 frames per second
}
#ifdef COMPILE_FOR_LINUX_AND_FREEBSD_AND_CYGWIN_AND_OPENBSD
@@ -2719,12 +2744,14 @@ static abuf_t *buffer_get_frame(rtsp_conn_info *conn, int resync_requested) {
struct timespec time_of_wakeup;
time_of_wakeup.tv_sec = sec;
time_of_wakeup.tv_nsec = nsec;
// debug(1, "wait for up to %f mS or for the next packet...", time_to_wait_for_wakeup_ns *
// 1E-6);
int rc = pthread_cond_timedwait(&conn->flowcontrol, &conn->ab_mutex,
&time_of_wakeup); // this is a pthread cancellation point
if ((rc != 0) && (rc != ETIMEDOUT))
// if (rc)
debug(3, "pthread_cond_timedwait returned error code %d.", rc);
// debug(1, "waited");
#endif
#ifdef COMPILE_FOR_OSX
uint64_t sec = time_to_wait_for_wakeup_ns / 1000000000;
@@ -2774,8 +2801,8 @@ static abuf_t *buffer_get_frame(rtsp_conn_info *conn, int resync_requested) {
debug(2, "setting up software resampler for %s for the first time.",
get_ssrc_name(curframe->ssrc));
} else {
debug(1, "Connection %d: queued audio buffers switching to \"%s\".", conn->connection_number,
get_ssrc_name(curframe->ssrc));
debug(2, "Connection %d: queued audio buffers switching to \"%s\".",
conn->connection_number, get_ssrc_name(curframe->ssrc));
clear_software_resampler(conn);
// ask the backend if it can give us its best choice for an ffmpeg configuration:
}
@@ -2804,12 +2831,15 @@ static abuf_t *buffer_get_frame(rtsp_conn_info *conn, int resync_requested) {
debug(1, "error %d", ret);
// We need to get those frames of silence out of the resampler
// so we'll pass in an empty AVFrame to flush them through
AVFrame *avf = av_frame_alloc(); // empty frame
int oldState;
pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, &oldState);
AVFrame *avf = av_frame_alloc();
conn->frames_retained_in_the_resampler =
avframe_to_audio(conn, avf, &pp, &pl, &number_of_output_frames);
av_frame_free(&avf);
pthread_setcancelstate(oldState, NULL);
curframe->data = (short *)pp;
curframe->length = number_of_output_frames;
av_frame_free(&avf);
}
}
#ifndef CONFIG_AIRPLAY_2
@@ -3011,8 +3041,8 @@ static int stuff_buffer_vernier(int32_t *inptr, int length, sps_format_t l_outpu
debug(1,
"Can't see how this could ever happen, but "
"current_input_sample_floor_index %" PRId64
" has just stepped outside the frame of %d samples, with stuff %d and current_input_sample_index_fp at %" PRId64 ".%05" PRId64
".",
" has just stepped outside the frame of %d samples, with stuff %d and "
"current_input_sample_index_fp at %" PRId64 ".%05" PRId64 ".",
current_input_sample_floor_index, length, stuff, current_input_sample_index_int,
current_input_sample_index_low);
current_input_sample_floor_index = length - 1; // hack
@@ -3034,8 +3064,8 @@ static int stuff_buffer_vernier(int32_t *inptr, int length, sps_format_t l_outpu
debug(1,
"Can't see how this could ever happen, but "
"current_input_sample_ceil_index %" PRId64
" has just stepped outside the frame of %d samples, with stuff %d and current_input_sample_index_fp at %" PRId64
".%05" PRId64 ".",
" has just stepped outside the frame of %d samples, with stuff %d and "
"current_input_sample_index_fp at %" PRId64 ".%05" PRId64 ".",
current_input_sample_floor_index, length, stuff, current_input_sample_index_int,
current_input_sample_index_low);
}
@@ -3296,17 +3326,10 @@ void player_thread_cleanup_handler(void *arg) {
rtsp_conn_info *conn = (rtsp_conn_info *)arg;
// debug(1, "Connection %d: player_thread_cleanup_handler start.", conn->connection_number);
#ifdef CONFIG_FFMPEG
// debug(1, "FFmpeg clearup");
clear_software_resampler(conn);
clear_decoding_chain(conn);
// debug(1, "FFmpeg clearup done");
#endif
if (config.output->stop) {
#ifdef CONFIG_FFMPEG
if (avflush(conn) > 1)
debug(3, "ffmpeg flush at stop!");
if ((config.decoder_in_use == 1 << decoder_ffmpeg_alac) && (avflush(conn) > 1))
debug(1, "ffmpeg flush at stop!");
#endif
debug(2, "Connection %d: player: stop the output backend.", conn->connection_number);
config.output->stop();
@@ -3400,6 +3423,15 @@ void player_thread_cleanup_handler(void *arg) {
ptp_send_control_message_string("E");
#endif
#ifdef CONFIG_FFMPEG
if (config.decoder_in_use == 1 << decoder_ffmpeg_alac) {
// debug(1, "FFmpeg clearup");
clear_software_resampler(conn);
clear_decoding_chain(conn);
// debug(1, "FFmpeg clearup done");
}
#endif
if (conn->outbuf) {
free(conn->outbuf);
conn->outbuf = NULL;
@@ -3494,11 +3526,17 @@ void *player_thread_func(void *arg) {
init_alac_decoder((int32_t *)&conn->stream.fmtp,
conn); // this sets up incoming rate, bit depth, channels.
// No pthread cancellation point in here
#ifdef CONFIG_METADATA
send_ssnc_metadata('sdsc', "ALAC/44100/S16_LE/2", strlen("ALAC/44100/S16_LE/2"), 1);
#endif
}
#endif
#ifdef CONFIG_APPLE_ALAC
if (config.decoder_in_use == 1 << decoder_apple_alac) {
apple_alac_init(conn->stream.fmtp); // no pthread cancellation point in here
#ifdef CONFIG_METADATA
send_ssnc_metadata('sdsc', "ALAC/44100/S16_LE/2", strlen("ALAC/44100/S16_LE/2"), 1);
#endif
}
#endif
}
@@ -4127,7 +4165,7 @@ void *player_thread_func(void *arg) {
send_ssnc_stream_description("Classic", get_ssrc_name(conn->incoming_ssrc));
#endif
if (config.statistics_requested)
inform("Connection %d: Classic AirPlay (\"AirPlay 1\") Compatible playback. "
inform("Connection %d: Classic AirPlay (\"AirPlay 1\") playback. "
"Input format: %s. Output format: %s.",
conn->connection_number, get_ssrc_name(conn->incoming_ssrc),
short_description);
@@ -4351,10 +4389,14 @@ void *player_thread_func(void *arg) {
if (inframe->timestamp_gap < 0) {
gap_to_fix = -inframe->timestamp_gap; // this is frames at the input rate
int64_t gap_to_fix_ns = (gap_to_fix * 1000000000) / conn->input_rate;
gap_to_fix = (gap_to_fix_ns *
RATE_FROM_ENCODED_FORMAT(config.current_output_configuration) + 1000000000/2) /
gap_to_fix = (gap_to_fix_ns * RATE_FROM_ENCODED_FORMAT(
config.current_output_configuration) +
1000000000 / 2) /
1000000000; // this is frames at the output rate
debug(4, "gap_to_fix: %u frames at input rate, %" PRId64 " frames at output rate.", -inframe->timestamp_gap, gap_to_fix);
debug(4,
"gap_to_fix: %u frames at input rate, %" PRId64
" frames at output rate.",
-inframe->timestamp_gap, gap_to_fix);
// debug(3, "due to timstamp gap of %d frames, skip %" PRId64 " output
// frames.", inframe->timestamp_gap, gap_to_fix);
}
@@ -4776,9 +4818,8 @@ void *player_thread_func(void *arg) {
frames_to_skip -= play_samples;
} else {
size_t bytes_to_skip = frames_to_skip *
size_t bytes_to_skip =
frames_to_skip *
CHANNELS_FROM_ENCODED_FORMAT(config.current_output_configuration) *
sps_format_sample_size(
FORMAT_FROM_ENCODED_FORMAT(config.current_output_configuration));
@@ -5262,7 +5303,7 @@ int player_stop(rtsp_conn_info *conn) {
// note -- this may be called from another connection thread.
debug(2, "Connection %d: player_stop.", conn->connection_number);
int response = 0; // okay
pthread_cleanup_debug_mutex_lock(&conn->player_create_delete_mutex, 5000, 1);
pthread_cleanup_debug_mutex_lock(&conn->player_create_delete_mutex, 5000, 4);
pthread_t *pt = conn->player_thread;
if (pt) {
debug(3, "player_thread cancel...");
+6 -1
View File
@@ -257,6 +257,7 @@ typedef struct {
typedef struct {
int connection_number; // for debug ID purposes, nothing else...
int is_playing; // set true by player_play, set false by player_stop
int input_format_is_valid; // set when the input format is known and set in this structure
unsigned int sync_samples_index; // for estimating the gap between the highest and lowest timing
// error over the past n samples
unsigned int sync_samples_count; // the array of samples is defined locally
@@ -272,7 +273,8 @@ typedef struct {
// otherwise
int software_mute_enabled; // if we don't have a real mute that we can use
int fd;
int authorized; // set if a password is required and has been supplied
int authorized; // set if a password is required and has been supplied or not required. Also
// always set in AirPlay 2 mode.
char *auth_nonce; // the session nonce, if needed
stream_cfg stream;
SOCKADDR remote, local;
@@ -412,6 +414,7 @@ typedef struct {
// (will be unspecified if not build for AirPlay 2)
#ifdef CONFIG_AIRPLAY_2
plist_t sessionPlist;
char *airplay_gid; // UUID in the Bonjour advertisement -- if NULL, the group UUID is the same as
// the pi UUID
airplay_t airplay_type; // are we using AirPlay 1 or AirPlay 2 protocol on this connection?
@@ -423,6 +426,8 @@ typedef struct {
pthread_t rtp_ap2_control_thread;
pthread_t rtp_realtime_audio_thread;
pthread_t rtp_buffered_audio_thread;
pthread_mutex_t event_sender_mutex;
int event_channel_fd; // zero if closed, controlled by event_sender_mutex
int ap2_event_receiver_exited;
+2 -1
View File
@@ -42,6 +42,7 @@
#define __STDC_FORMAT_MACROS
#include "common.h"
#include "ptp-utilities.h"
#include "utilities/network_utilities.h"
#include <inttypes.h>
#include <unistd.h>
@@ -253,7 +254,7 @@ void ptp_send_control_message_string(const char *msg) {
die("error sending timing_peer_list to NQPTP");
}
/* Deallocate the socket */
close(s);
safe_socket_close(&s);
freeaddrinfo(info);
/* deallocate the message string */
+13 -20
View File
@@ -29,6 +29,7 @@
#include "common.h"
#include "player.h"
#include "rtsp.h"
#include "utilities/network_utilities.h"
#include <arpa/inet.h>
#include <errno.h>
#include <fcntl.h>
@@ -311,11 +312,6 @@ void *rtp_audio_receiver(void *arg) {
}
}
/*
debug(3, "Audio receiver -- Server RTP thread interrupted. terminating.");
close(conn->audio_socket);
*/
debug(1, "Audio receiver thread \"normal\" exit -- this can't happen. Hah!");
pthread_cleanup_pop(0); // don't execute anything here.
debug(2, "Audio receiver thread exit.");
@@ -981,7 +977,8 @@ void *rtp_timing_receiver(void *arg) {
return_time);
}
} else {
debug(1, "Timing port -- Unknown RTP packet of type 0x%02X length %zd.", packet[1], nread);
debug(1, "Timing port -- Unknown RTP packet of type 0x%02X length %zd.", packet[1],
nread);
}
} else {
debug(3, "Timing Receiver Thread -- dropping incoming packet to simulate a bad network.");
@@ -1387,9 +1384,11 @@ int get_ptp_anchor_local_time_info(rtsp_conn_info *conn, uint32_t *anchorRTP,
if ((previous_offset != 0) && (previous_clock_id == actual_clock_id) &&
((jitter > 3000000) || (jitter < -3000000)))
debug(1,
"Clock jitter: %.3f mS. Time since sample: %.3f mS. Time since start of mastership: %.3f "
"Clock jitter: %.3f mS. Time since sample: %.3f mS. Time since start of "
"mastership: %.3f "
"seconds.",
jitter * 0.000001, time_since_sample * 0.000001, time_since_start_of_mastership * 0.000000001);
jitter * 0.000001, time_since_sample * 0.000001,
time_since_start_of_mastership * 0.000000001);
previous_offset = actual_offset;
previous_clock_id = actual_clock_id;
@@ -1509,16 +1508,11 @@ int frame_to_ptp_local_time(uint32_t timestamp, uint64_t *time, rtsp_conn_info *
int result = -1;
uint32_t anchor_rtptime = 0;
uint64_t anchor_local_time = 0;
if (get_ptp_anchor_local_time_info(conn, &anchor_rtptime, &anchor_local_time) == clock_ok) {
if ((conn->input_rate != 0) && (get_ptp_anchor_local_time_info(conn, &anchor_rtptime, &anchor_local_time) == clock_ok)) {
int32_t frame_difference = timestamp - anchor_rtptime;
int64_t time_difference = frame_difference;
time_difference = time_difference * 1000000000;
if (conn->input_rate == 0) {
debug(1,"in a call to frame_to_ptp_local_time, conn->input_rate is zero!");
time_difference = 0;
} else {
time_difference = time_difference / conn->input_rate;
}
uint64_t ltime = anchor_local_time + time_difference;
*time = ltime;
result = 0;
@@ -1532,7 +1526,7 @@ int local_ptp_time_to_frame(uint64_t time, uint32_t *frame, rtsp_conn_info *conn
int result = -1;
uint32_t anchor_rtptime = 0;
uint64_t anchor_local_time = 0;
if (get_ptp_anchor_local_time_info(conn, &anchor_rtptime, &anchor_local_time) == clock_ok) {
if ((conn->input_rate != 0) && (get_ptp_anchor_local_time_info(conn, &anchor_rtptime, &anchor_local_time) == clock_ok)) {
int64_t time_difference = time - anchor_local_time;
int64_t frame_difference = time_difference;
frame_difference = frame_difference * conn->input_rate; // but this is by 10^9
@@ -1550,10 +1544,9 @@ int local_ptp_time_to_frame(uint64_t time, uint32_t *frame, rtsp_conn_info *conn
void rtp_ap2_control_handler_cleanup_handler(void *arg) {
rtsp_conn_info *conn = (rtsp_conn_info *)arg;
debug(2, "Connection %d: AP2 Control Receiver Cleanup.", conn->connection_number);
close(conn->ap2_control_socket);
safe_socket_close(&conn->ap2_control_socket);
debug(2, "Connection %d: UDP control port %u closed.", conn->connection_number,
conn->local_ap2_control_port);
conn->ap2_control_socket = 0;
conn->ap2_remote_control_socket_addr_length =
0; // indicates to the control receiver thread that the socket address need to be
// recreated (needed for resend requests in the realtime mode)
@@ -1806,9 +1799,9 @@ void *rtp_ap2_control_receiver(void *arg) {
void rtp_realtime_audio_cleanup_handler(__attribute__((unused)) void *arg) {
debug(2, "Realtime Audio Receiver Cleanup Start.");
rtsp_conn_info *conn = (rtsp_conn_info *)arg;
close(conn->realtime_audio_socket);
debug(2, "Connection %d: closing realtime audio port %u", conn->connection_number, conn->local_realtime_audio_port);
conn->realtime_audio_socket = 0;
debug(2, "Connection %d: closing realtime audio port %u", conn->connection_number,
conn->local_realtime_audio_port);
safe_socket_close(&conn->realtime_audio_socket);
debug(2, "Realtime Audio Receiver Cleanup Done.");
}
+543 -1436
View File
File diff suppressed because it is too large Load Diff
+22 -31
View File
@@ -3,43 +3,34 @@
#include "player.h"
extern pthread_rwlock_t principal_conn_lock;
extern rtsp_conn_info *principal_conn;
extern rtsp_conn_info **conns;
typedef struct {
int index_number;
uint32_t referenceCount; // we might start using this...
unsigned int nheaders;
char *name[16];
char *value[16];
uint32_t contentlength;
char *content;
// for requests
char method[16];
char path[256];
// for responses
int respcode;
} rtsp_message;
void msg_retain(rtsp_message *msg);
void msg_free(rtsp_message **msgh);
void *rtsp_listen_loop(__attribute((unused)) void *arg);
void lock_player();
void unlock_player();
// result of trying to acquire or release the play lock
typedef enum {
play_lock_released,
play_lock_already_released,
play_lock_already_acquired,
play_lock_acquired_without_breaking_in,
play_lock_acquired_by_breaking_in,
play_lock_aquisition_failed
} play_lock_r;
// this can be used to [try to] forcibly stop a play session
play_lock_r get_play_lock(rtsp_conn_info *conn, int allow_session_interruption);
// play_lock_r get_play_lock(rtsp_conn_info *conn, int allow_session_interruption);
// this will release the play lock only if the conn has it or if the conn is NULL
void release_play_lock(rtsp_conn_info *conn);
// initialise and completely delete the metadata stuff
void metadata_init(void);
void metadata_stop(void);
// sends metadata out to the metadata pipe, if enabled.
// It is sent with the type 'ssnc' the given code, data and length
// The handler at the other end must know what to do with the data
// e.g. if it's malloced, to free it, etc.
// nothing is done automatically
int send_ssnc_metadata(const uint32_t code, const char *data, const uint32_t length,
const int block);
void stop_play(); // stop and drop a playing connection
#ifdef CONFIG_AIRPLAY_2
ssize_t read_encrypted(int fd, pair_cipher_bundle *ctx, void *buf, size_t count);
+7 -2
View File
@@ -14,11 +14,16 @@ general =
// %v for the version number, e.g. 3.0 and
// %V for the full version string, e.g. 3.3-OpenSSL-Avahi-ALSA-soxr-metadata-sysconfdir:/etc
// Overall length can not exceed 50 characters. Example: "Shairport Sync %v on %H".
// password = "secret"; // (AirPlay 1 only) leave this commented out if you don't want to require a password
//
// password = "secret"; // leave this commented out if you don't want to require a password
// About the "service_type" setting below: On an AirPlay-2-capable Shairport Sync, if you wish to offer only the older "classic" AirPlay (sometimes called AirPlay 1), set service_type to "classic".
// A potential use of this would be for Apple Music on Windows, which is not compatible with the AirPlay 2 service offered by Shairport Sync, but which is compatible with the classic service.
// The default is "auto", which means that the AirPlay 2 service will be provided if NQPTP is present and "classic" AirPlay will be provided otherwise, with "(Classic)" appended to the service name (see above).
// service_type = "auto"; // This can be "auto", "airplay2" or "classic".
// The interpolation setting below controls how Shairport Sync adds or removes frames of audio to keep in sync.
// "auto" (default) measures the processor's floating point speed and chooses "soxr" if available and it is fast enough. Otherwise, "vernier" is selected.
// "soxr" uses the SoX library to recode a packet of frames to a new packet containing more or fewer frames. This needs a processor with fast floating point capability.
// "vernier" recodes a packet of frames to a new packet containing more or fewer frames. This is recommended for low powered devices.
// "soxr" uses the SoX library to recode a packet of frames to a new packet containing more or fewer frames. This needs a processor with fast floating point capability.
// "basic" causes the simple removal or insertion of frames in a packet of frames. Not recommended.
// interpolation = "auto"; // aka "stuffing". Default is "auto". Alternatives are "vernier", "basic" or "soxr". Choose "soxr" only if you have a reasonably fast processor and Shairport Sync has been built with "soxr" support.
// output_backend = "alsa"; // Run "shairport-sync -h" to get a list of all output_backends, e.g. "alsa", "pipe", "stdout". The default is the first one.
+1
View File
@@ -4,6 +4,7 @@ After=sound.target
Wants=network-online.target
After=network.target network-online.target
@SYSTEMD_AFTER_ARGS@
@SYSTEMD_WANTS_ARGS@
@SYSTEMD_REQUIRES_ARGS@
StartLimitIntervalSec=300
StartLimitBurst=5
+3 -2
View File
@@ -1,6 +1,6 @@
/*
* This file is part of Shairport Sync.
* Copyright (c) Mike Brady 2019--2025
* Copyright (c) Mike Brady 2019--2026
* All rights reserved.
*
* Permission is hereby granted, free of charge, to any person
@@ -28,6 +28,7 @@
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <locale.h>
GMainLoop *loop;
@@ -99,7 +100,7 @@ void *dbus_thread_func(__attribute__((unused)) void *arg) {
}
int main(int argc, char *argv[]) {
setlocale(LC_ALL, "");
GBusType gbus_type_selected = G_BUS_TYPE_SYSTEM; // set default
// get the options --system or --session for system bus or session bus
signed char c; /* used for argument parsing */
+3 -1
View File
@@ -1,6 +1,6 @@
/*
* This file is part of Shairport Sync.
* Copyright (c) Mike Brady 2019--2025
* Copyright (c) Mike Brady 2019--2026
* All rights reserved.
*
* Permission is hereby granted, free of charge, to any person
@@ -29,6 +29,7 @@
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <locale.h>
GMainLoop *loop;
@@ -75,6 +76,7 @@ void *dbus_thread_func(__attribute__((unused)) void *arg) {
}
int main(int argc, char *argv[]) {
setlocale(LC_ALL, "");
GBusType gbus_type_selected = G_BUS_TYPE_SYSTEM; // set default
// get the options --system or --session for system bus or session bus
signed char c; /* used for argument parsing */
+225 -221
View File
@@ -2,7 +2,7 @@
* Shairport, an Apple Airplay receiver
* Copyright (c) James Laird 2013
* All rights reserved.
* Modifications and additions (c) Mike Brady 2014--2025
* Modifications and additions (c) Mike Brady 2014--2026
*
* Permission is hereby granted, free of charge, to any person
* obtaining a copy of this software and associated documentation
@@ -50,10 +50,13 @@
#ifdef CONFIG_AIRPLAY_2
#include "ptp-utilities.h"
#include "utilities/generate_device_uuid.h"
#include "utilities/generate_random_uuid.h"
#include <gcrypt.h>
#include <libavcodec/avcodec.h>
#include <sodium.h>
#include <uuid/uuid.h>
#endif
#ifdef CONFIG_MBEDTLS
@@ -79,13 +82,18 @@
#include "common.h"
#include "rtp.h"
#include "rtsp.h"
#include "utilities/string_utilities.h"
#if defined(CONFIG_DACP_CLIENT)
#include "dacp.h"
#endif
#if defined(CONFIG_METADATA)
#include "metadata/core.h"
#endif
#if defined(CONFIG_METADATA_HUB)
#include "metadata_hub.h"
#include "metadata/hub.h"
#endif
#ifdef CONFIG_DBUS_INTERFACE
@@ -314,19 +322,21 @@ void usage(char *progname) {
printf("Options:\n");
printf(" -h, --help Show this help.\n");
printf(" -V, --version Show version information -- the version string.\n");
printf(" --service-type Select the type of AirPlay service: \"auto\", \"airplay2\" or \"classic\". (You can use \"airplay1\" in place of \"classic\".)\n");
printf(" -X, --displayConfig Output OS information, version string, command line, configuration file and active settings to the log.\n");
printf(" --statistics Print some interesting statistics. More will be printed if -v / -vv / -vvv are also chosen.\n");
printf(" -v, --verbose Print debug information; -v some; -vv more; -vvv lots -- generally too much.\n");
printf(" -c, --configfile=FILE Read configuration settings from FILE. Default is %s.\n", configuration_file_path);
printf(" -a, --name=NAME Set service name. Default is the hostname with first letter capitalised.\n");
printf(" --password=PASSWORD Require PASSWORD to connect. Default is no password. (Classic AirPlay only.)\n");
printf(" --password=PASSWORD Require PASSWORD to connect. Default is no password.\n");
printf(" -p, --port=PORT Set RTSP listening port. Default 5000; 7000 for AirPlay 2.\n");
printf(" -L, --latency=FRAMES [Deprecated] Set the latency for audio sent from an unknown device.\n");
printf(" The default is to set it automatically.\n");
printf(" -S, --stuffing=MODE Set how to adjust current latency to match desired latency, where:\n");
printf(" \"basic\" inserts or deletes audio frames from packet frames with low processor overhead, and\n");
printf(" \"soxr\" uses libsoxr to minimally resample packet frames -- moderate processor overhead.\n");
printf(" The default \"auto\" setting chooses basic or soxr depending on processor capability.\n");
printf(" \"vernier\" recodes a packet of frames to a new packet containing more or fewer frames. Recommended for low powered devices;\n");
printf(" \"basic\" inserts or deletes audio frames from packet frames with low processor overhead; and\n");
printf(" \"soxr\" uses libsoxr to minimally resample packet frames -- moderate floating point processor overhead.\n");
printf(" The default \"auto\" setting chooses vernier or soxr depending on processor capability.\n");
printf(" The \"soxr\" option is only available if built with soxr support.\n");
printf(" -B, --on-start=PROGRAM Run PROGRAM when playback is about to begin.\n");
printf(" -E, --on-stop=PROGRAM Run PROGRAM when playback has ended.\n");
@@ -341,6 +351,7 @@ void usage(char *progname) {
printf(" -t, --timeout=SECONDS Go back to idle mode from play mode after a break in communications of this many seconds (default 60). Set to 0 never to exit play mode.\n");
printf(" --tolerance=TOLERANCE [Deprecated] Allow a synchronization error of TOLERANCE frames (default 88) before trying to correct it.\n");
printf(" --logOutputLevel Log the output level setting -- a debugging option, useful for determining the optimum maximum volume.\n");
#ifdef CONFIG_LIBDAEMON
printf(" -d, --daemon Daemonise.\n");
printf(" -j, --justDaemoniseNoPIDFile Daemonise without a PID file.\n");
@@ -351,6 +362,10 @@ void usage(char *progname) {
printf(" --metadata-pipename=PIPE send metadata to PIPE, e.g. --metadata-pipename=/tmp/%s-metadata.\n", config.appName);
printf(" The default is /tmp/%s-metadata.\n", config.appName);
printf(" -g, --get-coverart Include cover art in the metadata to be gathered and sent.\n");
#endif
#if defined(CONFIG_DBUS_INTERFACE) || defined(CONFIG_MPRIS_INTERFACE)
printf(" --dbus-default-message-bus=BUS Use the BUS specified for the native D-Bus and MPRIS interfaces.\n");
printf(" BUS must be \"system\" (default) or \"session\".\n");
#endif
printf(" --log-to-syslog Send debug and statistics information through syslog\n");
printf(" If used, this should be the first command line argument.\n");
@@ -369,6 +384,7 @@ void usage(char *progname) {
int parse_options(int argc, char **argv) {
// there are potential memory leaks here -- it's called a second time, previously allocated
// strings will dangle.
char *cli_service_type_string = NULL;
char *raw_service_name = NULL; /* Used to pick up the service name before possibly expanding it */
char *stuffing = NULL; /* used for picking up the stuffing option */
#if defined(CONFIG_DBUS_INTERFACE) || defined(CONFIG_MPRIS_INTERFACE)
@@ -402,6 +418,7 @@ int parse_options(int argc, char **argv) {
{"resync", 'r', POPT_ARG_INT, &resync_threshold_in_frames, 'r', NULL, NULL},
{"timeout", 't', POPT_ARG_INT, &config.timeout, 't', NULL, NULL},
{"password", 0, POPT_ARG_STRING, &config.password, 0, NULL, NULL},
{"service-type", 0, POPT_ARG_STRING, &cli_service_type_string, 0, NULL, NULL},
#if defined(CONFIG_DBUS_INTERFACE) || defined(CONFIG_MPRIS_INTERFACE)
{"dbus-default-message-bus", 0, POPT_ARG_STRING, &dbus_default_message_bus, 0, NULL, NULL},
#endif
@@ -413,8 +430,7 @@ int parse_options(int argc, char **argv) {
{"metadata-pipename", 0, POPT_ARG_STRING, &config.metadata_pipename, 0, NULL, NULL},
{"get-coverart", 'g', POPT_ARG_NONE, &config.get_coverart, 'g', NULL, NULL},
#endif
POPT_AUTOHELP{NULL, 0, 0, NULL, 0, NULL, NULL}
};
POPT_AUTOHELP{NULL, 0, 0, NULL, 0, NULL, NULL}};
// we have to parse the command line arguments to look for a config file
int optind;
@@ -539,6 +555,7 @@ int parse_options(int argc, char **argv) {
// for unexpected circumstances
config.model = strdup("ShairportSync");
// config.model = strdup("AirPort10,115");
// config.model = strdup("AudioAccessory5,1");
// config.srcvers = strdup(PACKAGE_VERSION);
@@ -557,6 +574,9 @@ int parse_options(int argc, char **argv) {
#endif
config.firmware_version = strdup(PACKAGE_VERSION);
free(config.firmware_version);
config.firmware_version = strdup("5.2");
#ifdef CONFIG_METADATA
/* Get the metadata setting. */
config.metadata_enabled = 1; // if metadata support is included, then enable it by default
@@ -585,83 +605,6 @@ int parse_options(int argc, char **argv) {
// not made active again (not used)
#endif
#ifdef CONFIG_AIRPLAY_2
// the features code is a 64-bit number, but in the mDNS advertisement, the least significant 32
// bit are given first for example, if the features number is 0x1C340405F4A00, it will be given as
// features=0x405F4A00,0x1C340 in the mDNS string, and in a signed decimal number in the plist:
// 496155702020608 this setting here is the source of both the plist features response and the
// mDNS string.
// note: 0x300401F4A00 works but with weird delays and stuff
// config.airplay_features = 0x1C340405FCA00;
uint64_t mask =
((uint64_t)1 << 17) | ((uint64_t)1 << 16) | ((uint64_t)1 << 15) | ((uint64_t)1 << 50);
config.airplay_features =
// 0x114BD04A5FCA00 & (~mask);
0x1C340405D4A00 & (~mask); // APX + Authentication4 (b14) with no metadata (see below)
/*
config.airplay_features |= (uint64_t)1 << 21; // Audio Format 4
config.airplay_features |= (uint64_t)1 << 25; // Unknown
config.airplay_features |= (uint64_t)1 << 36; // Unknown
config.airplay_features |= (uint64_t)1 << 39; // Unknown
config.airplay_features |= (uint64_t)1 << 43; // Supports System Pairing
config.airplay_features |= (uint64_t)1 << 47; // Unknown
// 0xB
config.airplay_features |= (uint64_t)1 << 63; // Unknown
config.airplay_features |= (uint64_t)1 << 61; // Unknown
config.airplay_features |= (uint64_t)1 << 60; // Unknown
// ...0xC
// // config.airplay_features |= (uint64_t)1 << 59; // Unknown
config.airplay_features |= (uint64_t)1 << 58; // Unknown
// ...0x3
config.airplay_features |= (uint64_t)1 << 53; // Unknown
config.airplay_features |= (uint64_t)1 << 52; // Unknown
*/
// config.airplay_features |= ((uint64_t)1 << 58) | ((uint64_t)1 << 60) | ((uint64_t)1 << 58);
// 60 seems to interfere with disconnecting from a group
// Advertised with mDNS and returned with GET /info, see
// https://openairplay.github.io/airplay-spec/status_flags.html 0x4: Audio cable attached, no PIN
// required (transient pairing), 0x204: Audio cable attached, OneTimePairingRequired 0x604: Audio
// cable attached, OneTimePairingRequired, device was setup for Homekit access control
config.airplay_statusflags = 0;
config.airplay_statusflags |= 1 << 2; // Audio cable is attached
// config.airplay_statusflags |= 1 << 10; // DeviceWasSetupForHKAccessControl
// config.airplay_statusflags |= 1 << 11; // DeviceSupportsRelay
// config.airplay_statusflags |= 1 << 19; // Unknown. Seems to control whether individual volume
// controls are shown and whether the SPS devices shows when its active.
// Set to NULL to work with transient pairing
config.airplay_pin = NULL;
// use the MAC address placed in config.hw_addr to generate the default airplay_device_id
uint64_t temporary_airplay_id = nctoh64(config.hw_addr);
temporary_airplay_id =
temporary_airplay_id >> 16; // we only use the first 6 bytes but have imported 8.
// now generate a UUID
// from https://stackoverflow.com/questions/51053568/generating-a-random-uuid-in-c
// with thanks
uuid_t binuuid;
uuid_generate_random(binuuid);
char *uuid = malloc(UUID_STR_LEN + 1); // leave space for the NUL at the end
// Produces a UUID string at uuid consisting of lower-case letters
uuid_unparse_lower(binuuid, uuid);
config.airplay_pi = uuid;
char *pgid_uuid = malloc(UUID_STR_LEN + 1); // leave space for the NUL at the end
uuid_generate_random(binuuid);
uuid_unparse_lower(binuuid, pgid_uuid);
config.airplay_pgid = pgid_uuid;
#endif
// config_setting_t *setting;
const char *str = NULL;
int value = 0;
@@ -669,6 +612,11 @@ int parse_options(int argc, char **argv) {
// debug(1, "Looking for the configuration file \"%s\".", config.configfile);
// use the MAC address placed in config.hw_addr to generate the default airplay_device_id
uint64_t temporary_airplay_id = nctoh64(config.hw_addr);
temporary_airplay_id =
temporary_airplay_id >> 16; // we only use the first 6 bytes but have imported 8.
config_init(&config_file_stuff);
config_file_real_path = realpath(config.configfile, NULL);
@@ -683,6 +631,10 @@ int parse_options(int argc, char **argv) {
// make config.cfg point to it
config.cfg = &config_file_stuff;
/* See if a specific service type has been requested */
if (config_lookup_non_empty_string(config.cfg, "general.service_type", &str)) {
config.service_type = string_to_service_type(str, "general service_type");
}
/* Get the Service Name. */
if (config_lookup_non_empty_string(config.cfg, "general.name", &str)) {
raw_service_name = (char *)str;
@@ -735,8 +687,7 @@ int parse_options(int argc, char **argv) {
* starting at the port base. Only three ports are needed. */
if (config_lookup_int(config.cfg, "general.udp_port_range", &value)) {
if ((value < 3) || (value > 65535))
die("Invalid port range \"%d\". It should be between 3 and 65535, default is 10",
value);
die("Invalid port range \"%d\". It should be between 3 and 65535, default is 10", value);
else
config.udp_port_range = value;
}
@@ -1352,15 +1303,6 @@ int parse_options(int argc, char **argv) {
"controlled by a hardware mixer. "
"You must not use a hardware mixer when using the loudness filter.");
} else {
if (config_error_type(&config_file_stuff) == CONFIG_ERR_FILE_IO)
die("Error reading configuration file \"%s\": \"%s\".", config_file_real_path,
config_error_text(&config_file_stuff));
else {
die("Line %d of the configuration file \"%s\":\n%s", config_error_line(&config_file_stuff),
config_error_file(&config_file_stuff), config_error_text(&config_file_stuff));
}
}
#if defined(CONFIG_DBUS_INTERFACE)
/* Get the dbus service sbus setting. */
if (config_lookup_string(config.cfg, "general.dbus_service_bus", &str)) {
@@ -1383,7 +1325,8 @@ int parse_options(int argc, char **argv) {
else if (strcasecmp(str, "session") == 0)
config.mpris_service_bus_type = DBT_session;
else
die("Invalid mpris_service_bus option choice \"%s\". It should be \"system\" (default) or "
die("Invalid mpris_service_bus option choice \"%s\". It should be \"system\" (default) "
"or "
"\"session\"",
str);
}
@@ -1446,7 +1389,8 @@ int parse_options(int argc, char **argv) {
config_set_lookup_bool(config.cfg, "mqtt.publish_cover", &config.mqtt_publish_cover);
config_set_lookup_bool(config.cfg, "mqtt.publish_retain", &config.mqtt_publish_retain);
if (config.mqtt_publish_cover && !config.get_coverart) {
die("You need to have metadata.include_cover_art enabled in order to use mqtt.publish_cover");
die("You need to have metadata.include_cover_art enabled in order to use "
"mqtt.publish_cover");
}
config_set_lookup_bool(config.cfg, "mqtt.enable_autodiscovery",
&config.mqtt_enable_autodiscovery);
@@ -1470,7 +1414,6 @@ int parse_options(int argc, char **argv) {
}
#endif
#endif
#ifdef CONFIG_AIRPLAY_2
long long aid;
@@ -1485,6 +1428,16 @@ int parse_options(int argc, char **argv) {
}
#endif
} else {
if (config_error_type(&config_file_stuff) == CONFIG_ERR_FILE_IO)
die("Error reading configuration file \"%s\": \"%s\".", config_file_real_path,
config_error_text(&config_file_stuff));
else {
die("Line %d of the configuration file \"%s\":\n%s", config_error_line(&config_file_stuff),
config_error_file(&config_file_stuff), config_error_text(&config_file_stuff));
}
}
}
// now, do the command line options again, but this time do them fully -- it's a unix convention
@@ -1551,6 +1504,10 @@ int parse_options(int argc, char **argv) {
die("%s: %s", poptBadOption(optCon, POPT_BADOPTION_NOALIAS), poptStrerror(c));
}
if (cli_service_type_string != NULL)
config.service_type = string_to_service_type(cli_service_type_string,
"command line option \"--service-type\" argument");
poptFreeContext(optCon);
#if defined(CONFIG_DBUS_INTERFACE) || (CONFIG_MPRIS_INTERFACE)
@@ -1581,6 +1538,7 @@ int parse_options(int argc, char **argv) {
config.nqptp_shared_memory_interface_name = strdup(NQPTP_INTERFACE_NAME);
// create the config.ap1_prefix[i]
char apids[6 * 2 + 5 + 1]; // six pairs of digits, 5 colons and a NUL
apids[6 * 2 + 5] = 0; // NUL termination
int i;
@@ -1604,17 +1562,18 @@ int parse_options(int argc, char **argv) {
// a uuid_t and an md5 hash are both 128 bits, 16 bytes
uuid_t result;
memset(result, 0, sizeof(result));
if (sizeof(config.ap1_prefix) < sizeof(result))
memcpy(result, config.ap1_prefix, sizeof(config.ap1_prefix));
else
memcpy(result, config.ap1_prefix, sizeof(result));
// OpenSSL is mandatory for AirPlay 2 anyway
#ifdef CONFIG_OPENSSL
// OpenSSL is mandatory for AirPlay 2
EVP_MD_CTX *mdctx = EVP_MD_CTX_new();
EVP_DigestInit_ex(mdctx, EVP_md5(), NULL);
EVP_DigestUpdate(mdctx, config.ap1_prefix, sizeof(config.ap1_prefix));
unsigned int md5_digest_len = EVP_MD_size(EVP_md5());
EVP_DigestFinal_ex(mdctx, result, &md5_digest_len);
EVP_MD_CTX_free(mdctx);
#endif
// now, convert it into a type 4 UUID
// see https://stackoverflow.com/questions/10867405/generating-v5-uuid-what-is-name-and-namespace
@@ -1631,8 +1590,8 @@ int parse_options(int argc, char **argv) {
// Produces a UUID string at uuid consisting of lower-case letters
uuid_unparse_lower(result, psi_uuid);
config.airplay_psi = psi_uuid;
debug(3, "size of pk is %zu.", sizeof(config.airplay_pk));
debug(3, "size of pk is %zu.", sizeof(config.airplay_pk));
pair_public_key_get(PAIR_SERVER_HOMEKIT, config.airplay_pk, config.airplay_device_id);
char buf[128];
char *ptr = buf;
@@ -1642,17 +1601,39 @@ int parse_options(int argc, char **argv) {
*ptr = '\0';
config.pk_string = strdup(buf);
// the features code is a 64-bit number, but in the mDNS advertisement, the least significant 32
// bit are given first for example, if the features number is 0x1C340405F4A00, it will be given as
// features=0x405F4A00,0x1C340 in the mDNS string, and in a signed decimal number in the plist:
// 496155702020608 this setting here is the source of both the plist features response and the
// mDNS string.
config.airplay_features = 0x00018340405C4A00; // no AP2 metadata (b50), no AP1 text (b17), no AP1
// progress (b16), no AP1 artwork (b15)
// 0x0001C340405C4A00; // no AP2 metadata (b50), no AP1 text (b17), no AP1 progress (b16), no
// AP1 artwork (b15) 0x0001C340445D0A00;
// config.airplay_features |= (1 << 26); // 0x0x4000000
// features=0x0001C340445D0A00 -- AirPort Express
#ifdef CONFIG_METADATA
// If we are asking for metadata, turn on the relevant bits
if (config.metadata_enabled != 0) {
config.airplay_features |= (uint64_t)1 << 16; // progress, 17 is text, 50 is in a binary plist
config.airplay_features |= (uint64_t)1 << 17; // text, 50 is in a binary plist
// config.airplay_features |=
// (uint64_t)1 << 50; // binary plist
// If we are asking for artwork, turn on the relevant bit
if (config.get_coverart)
config.airplay_features |= (uint64_t)1 << 15; // artwork
// If bit 50 is set, metadata is sent via plists in POST /command payloads.
// The data consists of textual information about what is playing and cover art
// It does not seem possible to turn off the cover art.
// While bit 50 is set, no data comes through the "classic" way. That is
// no progress, text or picture data comes through in the way that
// it comes through in Classic AirPlay (aka AirPlay 1).
// Although it is less flexible about what metadata is sent, bit 50 being set
// provides much more information, so should be the default for AirPlay 2
if (config.metadata_enabled != 0) {
config.airplay_features |=
(uint64_t)1 << 50; // metadata in a binary plist, including more state information
// config.airplay_features |= ((uint64_t)1 << 15) | ((uint64_t)1 << 16) | ((uint64_t)1 << 17);
// // older metadata flags artwork, progress and text respectively
}
#endif
@@ -1673,6 +1654,23 @@ int parse_options(int argc, char **argv) {
if (padding)
*padding = 0;
debug(2, "airplay_fex is \"%s\"", config.airplay_fex);
// now the status flags
// Advertised with mDNS and returned with GET /info, see
// https://openairplay.github.io/airplay-spec/status_flags.html
config.airplay_statusflags = 0;
config.airplay_statusflags |= 1 << 2; // Audio cable is attached
if (config.password != NULL) {
config.airplay_statusflags |= 1 << 7; // Password required
}
// config.airplay_statusflags |= 1 << 10; // DeviceWasSetupForHKAccessControl
// config.airplay_statusflags |= 1 << 11; // DeviceSupportsRelay
// config.airplay_statusflags |= 1 << 19; // Unknown. Seems to control whether individual volume
// controls are shown and whether the SPS devices shows when its active.
config.airplay_pi = generate_device_uuid(config.airplay_device_id);
config.airplay_pgid = generate_random_uuid();
#endif
#ifdef CONFIG_LIBDAEMON
@@ -1722,34 +1720,88 @@ int parse_options(int argc, char **argv) {
config.airplay_volume =
config.default_airplay_volume; // if no volume is ever set or requested, default to initial
// default value if nothing else comes in first.
// now, do the substitutions in the service name
char hostname[100];
gethostname(hostname, 100);
// strip off a terminating .<anything>, e.g. .local from the hostname
char *last_dot = strrchr(hostname, '.');
if (last_dot != NULL)
*last_dot = '\0';
// let's see if we have AirPlay 2 and NQPTP is installed or not and set service_type accordingly
// or exit. the outcome will be APST_airplay2, APST_classic or APST_forced_classic, and APST_auto
// will be gone.
char *i0;
if (raw_service_name == NULL)
i0 = strdup("%H"); // this is the default it the Service Name wasn't specified
else
i0 = strdup(raw_service_name);
char service_type_string[32];
service_type_to_string(config.service_type, service_type_string);
debug(1, "config.service_type is: \"%s\".", service_type_string);
// here, do the substitutions for %h, %H, %v and %V
char *i1 = str_replace(i0, "%h", hostname);
if ((hostname[0] >= 'a') && (hostname[0] <= 'z'))
hostname[0] = hostname[0] - 0x20; // convert a lowercase first letter into a capital letter
char *i2 = str_replace(i1, "%H", hostname);
char *i3 = str_replace(i2, "%v", PACKAGE_VERSION);
char *vs = get_version_string();
config.service_name = str_replace(i3, "%V", vs); // service name complete
free(i0);
free(i1);
free(i2);
free(i3);
free(vs);
#ifdef CONFIG_AIRPLAY_2
// don't bother checking for NQPTP if we are providing a classic service
if (config.service_type != APST_classic) {
ptp_send_control_message_string(
"T"); // send this message to get nqptp to create the named shm interface
int response = 0;
/*
uint64_t nqptp_start_waiting_time = get_absolute_time_in_ns();
int continue_waiting = 0;
int64_t time_spent_waiting = 0;
do {
continue_waiting = 0;
response = ptp_shm_interface_open();
if ((response == -1) && (errno == ENOENT)) {
time_spent_waiting = get_absolute_time_in_ns() - nqptp_start_waiting_time;
if (time_spent_waiting < 10000000000L) {
continue_waiting = 1;
usleep(50000);
}
}
} while (continue_waiting != 0);
*/
response = ptp_shm_interface_open(); // look for NQPTP service
if ((response == -1) && (errno == ENOENT)) {
debug(1, "NQPTP service not found.");
// change auto to forced classic
if (config.service_type == APST_auto) {
config.service_type = APST_forced_classic;
} else if (config.service_type == APST_airplay2) {
die("The NQPTP service can not be found. NQPTP must be installed and running to provide "
"AirPlay 2 service.");
}
} else if ((response == -1) && (errno == EACCES)) {
die("Shairport Sync must have read access to the NQPTP shared memory file in /dev/shm/.");
} else if (response != 0) {
die("an error occurred accessing the NQPTP service.");
}
if (response == 0) {
// change "auto" to "airplay2"
if (config.service_type == APST_auto) {
config.service_type = APST_airplay2;
}
// check that the version of Shairport Sync and NQPTP match...
debug(1, "NQPTP service found.");
if (config.service_type == APST_airplay2) {
// now that we are using AirPlay 2, check NQPTP and SPS match...
int ptp_clock_version = ptp_get_clock_version();
if (ptp_clock_version == 0) {
die("The NQPTP service on this system, which is required for Shairport Sync to operate, "
"does "
"not seem to be initialised.");
} else if (ptp_clock_version < NQPTP_SHM_STRUCTURES_VERSION) {
die("The NQPTP service (SMI Version %d) on this system is too old for this version of "
"Shairport Sync, which requires SMI Version %d. Please update.",
ptp_clock_version, NQPTP_SHM_STRUCTURES_VERSION);
} else if (ptp_clock_version > NQPTP_SHM_STRUCTURES_VERSION) {
die("This version of Shairport Sync (SMI Version %d) is too old for the version of NQPTP "
"(SMI "
"Version %d) on this system. Please update.",
NQPTP_SHM_STRUCTURES_VERSION, ptp_clock_version);
}
}
}
}
#else
// if we don't have AirPLay 2, the service type must be classic
config.service_type = APST_classic;
#endif
config.service_name = service_name(raw_service_name);
#ifdef CONFIG_MQTT
// mqtt topic was not set. As we have the service name just now, set it
@@ -1919,9 +1971,6 @@ void exit_function() {
#endif
if (conns)
free(conns); // make sure the connections have been deleted first
if (config.service_name)
free(config.service_name);
@@ -1955,8 +2004,6 @@ void exit_function() {
free(config.nqptp_shared_memory_interface_name);
if (config.airplay_device_id)
free(config.airplay_device_id);
if (config.airplay_pin)
free(config.airplay_pin);
if (config.airplay_pi)
free(config.airplay_pi);
if (config.airplay_pgid)
@@ -2344,7 +2391,6 @@ int main(int argc, char **argv) {
pid = getpid();
config.log_fd = -1;
conns = NULL; // no connections active
#ifdef CONFIG_LIBDAEMON
daemon_set_verbosity(LOG_DEBUG);
@@ -2356,6 +2402,8 @@ int main(int argc, char **argv) {
debug(1, "adding the exit function");
atexit(exit_function);
config.service_type = APST_auto; // this may be changed by the settings...
// get a device id -- the first non-local MAC address
get_device_id((uint8_t *)&config.hw_addr, 6);
@@ -2405,12 +2453,6 @@ int main(int argc, char **argv) {
config.resync_threshold = 0.050; // default
config.tolerance = 0.002;
#ifdef CONFIG_AIRPLAY_2
config.port = 7000;
#else
config.port = 5000;
#endif
#ifdef CONFIG_SOXR
config.packet_stuffing = ST_auto; // use soxr interpolation by default if support has been
// included and if the CPU is fast enough
@@ -2418,10 +2460,6 @@ int main(int argc, char **argv) {
config.packet_stuffing = ST_vernier; // you need to explicitly ask for "basic" (ST_basic)
#endif
// char hostname[100];
// gethostname(hostname, 100);
// config.service_name = malloc(20 + 100);
// snprintf(config.service_name, 20 + 100, "Shairport Sync on %s", hostname);
set_requested_connection_state_to_output(
1); // we expect to be able to connect to the output device
config.audio_backend_buffer_desired_length = 0.15; // seconds
@@ -2481,13 +2519,6 @@ int main(int argc, char **argv) {
// parse arguments into config -- needed to locate pid_dir
int audio_arg = parse_options(argc, argv);
// mDNS supports maximum of 63-character names (we append 13).
if (strlen(config.service_name) > 50) {
warn("The service name \"%s\" is too long (max 50 characters) and has been truncated.",
config.service_name);
config.service_name[50] = '\0'; // truncate it and carry on...
}
if (display_config_selected != 0) {
display_config(argc, argv);
if (argc == 2) {
@@ -2617,21 +2648,36 @@ int main(int argc, char **argv) {
#endif
#ifdef CONFIG_AIRPLAY_2
if (config.service_type == APST_airplay2) {
config.port = 7000;
} else {
config.port = 5000;
}
#else
config.port = 5000;
#endif
#ifdef CONFIG_AIRPLAY_2
if (config.service_type == APST_airplay2) {
if (has_fltp_capable_aac_decoder() == 0) {
die("Shairport Sync can not run on this system. Run \"shairport-sync -h\" for more "
"information.");
}
}
uint64_t apf = config.airplay_features;
uint64_t apfh = config.airplay_features;
apfh = apfh >> 32;
uint32_t apf32 = apf;
uint32_t apfh32 = apfh;
debug(1, "Startup in AirPlay 2 mode, with features 0x%" PRIx32 ",0x%" PRIx32 " on device \"%s\".",
if (config.service_type == APST_airplay2) {
debug(1,
"Startup in AirPlay 2 mode, with features 0x%" PRIx32 ",0x%" PRIx32 " on device \"%s\".",
apf32, apfh32, config.airplay_device_id);
} else {
debug(1, "Startup in Classic AirPlay (aka \"AirPlay 1\") mode. (AirPlay2 build.)");
}
#else
debug(1, "Startup in classic Airplay (aka \"AirPlay 1\") mode.");
debug(1, "Startup in Classic AirPlay (aka \"AirPlay 1\") mode.");
#endif
// control-c (SIGINT) cleanly
@@ -2966,8 +3012,8 @@ int main(int argc, char **argv) {
debug(2, "output channel %d is \"%s\".", i, config.output_channel_map[i]);
} else {
warn("during channel mapping, \"%s\" was not recognised as a channel name -- as a result, output channel %d will be silent.",
warn("during channel mapping, \"%s\" was not recognised as a channel name -- as a "
"result, output channel %d will be silent.",
channel_id, i);
config.output_channel_map[i] = strdup("--");
}
@@ -3174,7 +3220,6 @@ int main(int argc, char **argv) {
debug(1, "couldn't get memory to start the soxr_checker");
}
pthread_setcancelstate(oldState, NULL); // make this un-cancellable
}
#endif
@@ -3202,7 +3247,7 @@ int main(int argc, char **argv) {
if (idx != 0)
strncat(channel_map, " ", sizeof(channel_map) - 1);
strncat(channel_map, chName, sizeof(channel_map) - 1 - strlen(channel_map));
debug(3, "Channel %d: \"%s\"", idx, chName);
debug(4, "Channel %d: \"%s\"", idx, chName);
} else {
debug(1, "Insufficient space for the name of channel %d.", idx);
}
@@ -3224,11 +3269,14 @@ int main(int argc, char **argv) {
#endif
// In AirPlay 2 mode, the AP1 prefix is the same as the device ID less the colons
// and has already been calculated.
// In AirPlay 1 mode, the AP1 prefix is calculated by hashing the service name.
#ifndef CONFIG_AIRPLAY_2
#ifdef CONFIG_AIRPLAY_2
if (config.service_type != APST_airplay2) {
#endif
uint8_t ap_md5[16];
// debug(1, "size of hw_addr is %u.", sizeof(config.hw_addr));
#ifdef CONFIG_OPENSSL
EVP_MD_CTX *mdctx = EVP_MD_CTX_new();
@@ -3244,7 +3292,8 @@ int main(int argc, char **argv) {
#if MBEDTLS_VERSION_MINOR >= 7
mbedtls_md5_context tctx;
mbedtls_md5_starts_ret(&tctx);
mbedtls_md5_update_ret(&tctx, (unsigned char *)config.service_name, strlen(config.service_name));
mbedtls_md5_update_ret(&tctx, (unsigned char *)config.service_name,
strlen(config.service_name));
mbedtls_md5_update_ret(&tctx, (unsigned char *)config.hw_addr, sizeof(config.hw_addr));
mbedtls_md5_finish_ret(&tctx, ap_md5);
#else
@@ -3263,8 +3312,10 @@ int main(int argc, char **argv) {
md5_update(&tctx, (unsigned char *)config.hw_addr, sizeof(config.hw_addr));
md5_finish(&tctx, ap_md5);
#endif
memcpy(config.ap1_prefix, ap_md5, sizeof(config.ap1_prefix));
#ifdef CONFIG_AIRPLAY_2
}
#endif
#ifdef CONFIG_METADATA
@@ -3291,55 +3342,6 @@ int main(int argc, char **argv) {
glib_worker_thread = g_thread_new("glib worker", glib_worker_thread_function, NULL);
#endif
#ifdef CONFIG_AIRPLAY_2
ptp_send_control_message_string(
"T"); // send this message to get nqptp to create the named shm interface
uint64_t nqptp_start_waiting_time = get_absolute_time_in_ns();
int continue_waiting = 0;
int response = 0;
int64_t time_spent_waiting = 0;
do {
continue_waiting = 0;
response = ptp_shm_interface_open();
if ((response == -1) && (errno == ENOENT)) {
time_spent_waiting = get_absolute_time_in_ns() - nqptp_start_waiting_time;
if (time_spent_waiting < 10000000000L) {
continue_waiting = 1;
usleep(50000);
}
}
} while (continue_waiting != 0);
if ((response == -1) && (errno == ENOENT)) {
die("Shairport Sync can not find the nqptp service on this system. Is nqptp installed and "
"running?");
} else if ((response == -1) && (errno == EACCES)) {
die("Shairport Sync must have read access to the nqptp shared memory file in /dev/shm/.");
} else if (response != 0) {
die("an error occurred accessing the nqptp service.");
}
int ptp_clock_version = ptp_get_clock_version();
if (ptp_clock_version == 0) {
die("The nqptp service on this system, which is required for Shairport Sync to operate, does "
"not seem to be initialised.");
} else if (ptp_clock_version < NQPTP_SHM_STRUCTURES_VERSION) {
die("The nqptp service (SMI Version %d) on this system is too old for this version of "
"Shairport Sync, which requires SMI Version %d. Please update.",
ptp_clock_version, NQPTP_SHM_STRUCTURES_VERSION);
} else if (ptp_clock_version > NQPTP_SHM_STRUCTURES_VERSION) {
die("This version of Shairport Sync (SMI Version %d) is too old for the version of nqptp (SMI "
"Version %d) on this system. Please update.",
NQPTP_SHM_STRUCTURES_VERSION, ptp_clock_version);
}
if (time_spent_waiting == 0)
debug(1, "NQPTP is online.");
else
debug(1, "NQPTP came online after %.3f milliseconds.", 0.000001 * time_spent_waiting);
#endif
#ifdef CONFIG_METADATA
send_ssnc_metadata('svna', config.service_name, strlen(config.service_name), 1);
#endif
@@ -3348,8 +3350,10 @@ int main(int argc, char **argv) {
convolver_pool_init(config.convolution_threads, 8); // 8 channels
#endif
activity_monitor_start();
debug(3, "create an RTSP listener");
named_pthread_create(&rtsp_listener_thread, NULL, &rtsp_listen_loop, NULL, "bonjour");
debug(4, "create an RTSP listener");
// note: the Avahi Threaded Poll thread will be named after whatever name you use here too, so
// you'll see two threads named "listener" or whatever...
named_pthread_create(&rtsp_listener_thread, NULL, &rtsp_listen_loop, NULL, "listener");
atexit(exit_rtsp_listener);
pthread_join(rtsp_listener_thread, NULL);
return 0;
+7 -6
View File
@@ -34,6 +34,7 @@
#include "tinysvcmdns.h"
#include "common.h"
#include "utilities/network_utilities.h"
#define DEBUG_PRINTF(...) debug(3, __VA_ARGS__)
#define log_message(level, ...) \
@@ -1457,11 +1458,11 @@ int write_pipe(int s, char *buf, int len) {
#endif
}
int close_pipe(int s) {
int close_pipe(int *s) {
#ifdef _WIN32
return closesocket(s);
return closesocket(*s);
#else
return close(s);
return safe_socket_close(s);
#endif
}
@@ -1566,7 +1567,7 @@ void *main_loop(struct mdnsd *svr) {
free(pkt_buffer);
close_pipe(svr->sockfd);
close_pipe(&svr->sockfd);
svr->stop_flag = 2;
return NULL;
@@ -1749,8 +1750,8 @@ void mdnsd_stop(struct mdnsd *s) {
while (s->stop_flag != 2)
select(0, NULL, NULL, NULL, &tv);
close_pipe(s->notify_pipe[0]);
close_pipe(s->notify_pipe[1]);
close_pipe(&s->notify_pipe[0]);
close_pipe(&s->notify_pipe[1]);
pthread_mutex_destroy(&s->data_lock);
rr_group_destroy(s->group);
+122 -24
View File
@@ -7,6 +7,7 @@ SERVICE_SOURCE="./scripts/shairport-sync.user.service"
USER_SYSTEMD_DIR="$HOME/.config/systemd/user"
SERVICE_DEST="$USER_SYSTEMD_DIR/$SERVICE_NAME"
APP_NAME="shairport-sync"
APP_DISPLAY_NAME="Shairport Sync"
DRY_RUN=0
@@ -40,16 +41,24 @@ while [ $# -gt 0 ]; do
shift
done
# Check we are not running as root or under sudo
if [ "$(id -u)" -eq 0 ]; then
echo "[FAIL] This script must not be run as root or with sudo." >&2
echo " [ADVICE] Please run it as the regular user who will own the service." >&2
exit 1
fi
echo "--------------------------------------------------"
echo "Systemd User Service Installer for $APP_NAME"
echo "Systemd User Service Installer for $APP_DISPLAY_NAME"
echo "This script will:"
echo " - Check for system/initd conflicts"
echo " - Verify the app is not already running"
echo " - Check for systemd/initd conflicts"
echo " - Verify that Shairport Sync is not already running"
echo " - Detect PipeWire or PulseAudio"
echo " - Install the user service"
echo " - Enable it to run at login"
if [ "$DRY_RUN" -eq 1 ]; then
echo ""
echo "Dry-run mode: no changes will be made."
echo "Note: Dry-run mode: no changes will be made."
fi
echo "--------------------------------------------------"
echo ""
@@ -64,8 +73,9 @@ fi
# Check for system-level service
if systemctl is-enabled "$APP_NAME" >/dev/null 2>&1; then
echo "[FAIL] A system-level systemd service for '$APP_NAME' is currently enabled." >&2
echo " [ADVICE] Please disable it before installing a user-level service." >&2
echo "[FAIL] A system-level systemd service (\"$APP_NAME\") for $APP_DISPLAY_NAME is currently enabled." >&2
echo " [ADVICE] Please disable it before installing a user-level service. For example, try:" >&2
echo " [ADVICE] $ sudo systemctl disable --now $APP_NAME" >&2
exit 1
else
echo "[OK] No conflicting system-level systemd service enabled."
@@ -73,8 +83,9 @@ fi
# Check for init.d service
if [ -x "/etc/init.d/$APP_NAME" ]; then
echo "[FAIL] An init.d script for '$APP_NAME' exists at /etc/init.d/$APP_NAME." >&2
echo " [ADVICE] Please remove or disable the init.d version before proceeding." >&2
echo "[FAIL] An init.d script for $APP_DISPLAY_NAME exists at /etc/init.d/$APP_NAME." >&2
echo " [ADVICE] Please remove or disable the init.d version before proceeding. For example, try:" >&2
echo " [ADVICE] $ sudo rm /etc/init.d/$APP_NAME" >&2
exit 1
else
echo "[OK] No conflicting init.d service found."
@@ -82,16 +93,78 @@ fi
# Note if the user-level service is already running
if systemctl --user is-enabled "$APP_NAME" >/dev/null 2>&1; then
echo "[NOTE] A user-level systemd service for '$APP_NAME' is currently enabled."
echo "[NOTE] A user-level systemd service -- \"$APP_NAME\" -- for $APP_DISPLAY_NAME is currently enabled."
fi
# Check if the application is already running
if pgrep -x "$APP_NAME" >/dev/null 2>&1; then
echo "[FAIL] $APP_NAME is already running." >&2
if pgrep -f "$APP_NAME" >/dev/null 2>&1; then
echo "[FAIL] $APP_DISPLAY_NAME is already running." >&2
echo " [ADVICE] Please stop $APP_NAME before proceeding." >&2
exit 1
else
echo "[OK] $APP_NAME is not currently running."
echo "[OK] $APP_DISPLAY_NAME is not running."
fi
# Detect PipeWire or PulseAudio (prefer PipeWire)
AUDIO_BACKEND=""
AUDIO_REQUIRES=""
AUDIO_AFTER=""
AUDIO_OUTPUT_FLAG=""
detect_pipewire() {
# Check if pipewire is running as a user service
if systemctl --user is-active pipewire >/dev/null 2>&1; then
return 0
fi
# Also check if the process is running
if pgrep -x pipewire >/dev/null 2>&1; then
return 0
fi
return 1
}
detect_pulseaudio() {
# Check if pulseaudio is running as a user service
if systemctl --user is-active pulseaudio >/dev/null 2>&1; then
return 0
fi
# Also check if the process is running
if pgrep -x pulseaudio >/dev/null 2>&1; then
return 0
fi
return 1
}
# Check whether shairport-sync was built with support for a given backend
# Usage: built_with_support <backend> e.g. built_with_support pipewire
built_with_support() {
shairport-sync -V 2>&1 | grep -qi "$1"
}
if detect_pipewire; then
echo "[OK] PipeWire is running."
if built_with_support pipewire; then
echo "[OK] $APP_DISPLAY_NAME was built with PipeWire support -- it will be configured to use it."
AUDIO_BACKEND="pipewire"
AUDIO_REQUIRES="Requires=pipewire.service"
AUDIO_AFTER="After=pipewire.service"
AUDIO_OUTPUT_FLAG="-o pipewire"
else
echo "[NOTE] PipeWire is active, but $APP_DISPLAY_NAME was not built with PipeWire support -- falling back to the default ALSA audio output device."
fi
elif detect_pulseaudio; then
echo "[OK] PulseAudio is running."
if built_with_support pulseaudio; then
echo "[OK] $APP_DISPLAY_NAME was built with PulseAudio support -- it will be configured to use it."
AUDIO_BACKEND="pulseaudio"
AUDIO_REQUIRES="Requires=pulseaudio.service"
AUDIO_AFTER="After=pulseaudio.service"
AUDIO_OUTPUT_FLAG="-o pulseaudio"
else
echo "[NOTE] PulseAudio is active, but $APP_DISPLAY_NAME was not built with PulseAudio support -- falling back to the default ALSA audio output device."
fi
else
echo "[NOTE] Neither PipeWire nor PulseAudio detected -- $APP_DISPLAY_NAME will use the default ALSA audio output device."
fi
# Create directory
@@ -105,7 +178,6 @@ if [ ! -d "$USER_SYSTEMD_DIR" ]; then
echo "[FAIL] Failed to create directory $USER_SYSTEMD_DIR" >&2
exit 1
fi
fi
else
echo "[OK] User systemd directory already exists."
@@ -117,30 +189,52 @@ if [ -d "$USER_SYSTEMD_DIR" ] && [ ! -w "$USER_SYSTEMD_DIR" ]; then
exit 1
fi
# If the service file already exists, check that we can replace it
if [ -f "$SERVICE_DEST" ] && [ ! -w "$SERVICE_DEST" ]; then
echo "[FAIL] The existing $SERVICE_DEST can not be replaced due to its ownership or permissions." >&2
echo " [ADVICE] Please delete $SERVICE_DEST or ensure it is owned and writable by user \"$(whoami)\" before proceeding." >&2
exit 1
fi
# Create the service file
if [ "$DRY_RUN" -eq 0 ]; then
echo "Creating service file at $SERVICE_DEST..."
cat > "$SERVICE_DEST" <<EOF
[Unit]
# Build the service file content from a single source
if [ -n "$AUDIO_REQUIRES" ]; then
UNIT_SECTION="[Unit]
Description=Shairport Sync - AirPlay Audio Receiver
After=sound.target
$AUDIO_REQUIRES
$AUDIO_AFTER"
else
UNIT_SECTION="[Unit]
Description=Shairport Sync - AirPlay Audio Receiver
After=sound.target"
fi
if [ -n "$AUDIO_OUTPUT_FLAG" ]; then
EXEC_START="ExecStart=shairport-sync --log-to-syslog $AUDIO_OUTPUT_FLAG"
else
EXEC_START="ExecStart=shairport-sync --log-to-syslog"
fi
SERVICE_CONTENT="$UNIT_SECTION
[Service]
ExecStart=/usr/local/bin/shairport-sync --log-to-syslog
$EXEC_START
[Install]
WantedBy=default.target
EOF
WantedBy=default.target"
# Show the service file in dry-run mode, or write it for real
if [ "$DRY_RUN" -eq 1 ]; then
echo ""
echo "[DRY-RUN] Would write the following service file to $SERVICE_DEST:"
echo ""
echo "$SERVICE_CONTENT" | sed 's/^/ /'
echo ""
else
echo "Creating service file at $SERVICE_DEST..."
printf '%s\n' "$SERVICE_CONTENT" > "$SERVICE_DEST"
if [ $? -eq 0 ]; then
echo "[OK] Service file created."
else
@@ -179,5 +273,9 @@ if [ "$DRY_RUN" -eq 1 ]; then
echo "Dry run completed successfully."
else
echo "Installation complete."
echo "The user-level systemd service for $APP_NAME is now installed and enabled."
if [ -n "$AUDIO_BACKEND" ]; then
echo "The user-level systemd service for $APP_DISPLAY_NAME is now installed and enabled, using $AUDIO_BACKEND."
else
echo "The user-level systemd service for $APP_DISPLAY_NAME is now installed and enabled."
fi
fi
+7 -5
View File
@@ -27,9 +27,10 @@
* OTHER DEALINGS IN THE SOFTWARE.
*/
#include <sys/types.h>
#include "buffered_read.h"
#include "common.h"
#include <pthread.h>
#include <sys/types.h>
ssize_t buffered_read(buffered_tcp_desc *descriptor, void *buf, size_t count,
size_t *bytes_remaining) {
@@ -110,8 +111,8 @@ void *buffered_tcp_reader(void *arg) {
int finished = 0;
int fd = accept(descriptor->sock_fd, (struct sockaddr *)&remote_addr, &addr_size);
// debug(1, "buffered_tcp_reader: the client has opened a buffered audio link.");
intptr_t pfd = fd;
pthread_cleanup_push(socket_cleanup, (void *)pfd);
// intptr_t pfd = fd;
pthread_cleanup_push(socket_cleanup, (void *)&fd);
do {
int have_time_to_sleep = 0;
@@ -126,7 +127,7 @@ void *buffered_tcp_reader(void *arg) {
// now we know it is not full, so go ahead and try to read some more into it
// wrap
// wrap if we're up at the end of the buffer
if ((size_t)(descriptor->eoq - descriptor->buffer) == descriptor->buffer_max_size)
descriptor->eoq = descriptor->buffer;
@@ -136,6 +137,7 @@ void *buffered_tcp_reader(void *arg) {
if (bytes_to_request > free_space)
bytes_to_request = free_space; // don't ask for more than will fit
// we know that this can't be zero.
size_t gap_to_end_of_buffer =
descriptor->buffer + descriptor->buffer_max_size - descriptor->eoq;
if (gap_to_end_of_buffer < bytes_to_request)
@@ -161,7 +163,7 @@ void *buffered_tcp_reader(void *arg) {
descriptor->closed = 1;
debug(
2,
"buffered audio port closed by remote end. Terminating the buffered_tcp_reader thread.");
"buffered audio port closed by remote end when asking for %zu bytes. Terminating the buffered_tcp_reader thread.", bytes_to_request);
finished = 1;
} else if (nread > 0) {
descriptor->eoq += nread;
+4 -4
View File
@@ -1,5 +1,7 @@
#ifndef _BUFFERED_READ_H
#define _BUFFERED_READ_H
#pragma once
#include <pthread.h>
#include <sys/types.h>
typedef struct {
int closed;
@@ -20,5 +22,3 @@ void *buffered_tcp_reader(void *arg);
// read the number of bytes specified by "count".
ssize_t read_sized_block(buffered_tcp_desc *descriptor, void *buf, size_t count,
size_t *bytes_remaining);
#endif // _BUFFERED_READ_H
+92 -25
View File
@@ -23,6 +23,7 @@ SOFTWARE.
*/
#include "debug.h"
#include <ctype.h> // for isprint()
#include <inttypes.h>
#include <pthread.h>
#include <stdarg.h>
@@ -64,13 +65,9 @@ void debug_init(int level, int show_elapsed_time, int show_relative_time, int sh
debugger_show_file_and_line = show_file_and_line;
}
int debug_level() {
return debuglev;
};
int debug_level() { return debuglev; };
void set_debug_level(int level) {
debuglev = level;
}
void set_debug_level(int level) { debuglev = level; }
void increase_debug_level() {
if (debuglev < 3)
@@ -82,28 +79,15 @@ void decrease_debug_level() {
debuglev--;
}
int get_show_elapsed_time() {
return debugger_show_elapsed_time;
int get_show_elapsed_time() { return debugger_show_elapsed_time; }
void set_show_elapsed_time(int setting) { debugger_show_elapsed_time = setting; }
int get_show_relative_timel() { return debugger_show_relative_time; }
}
void set_show_elapsed_time(int setting) {
debugger_show_elapsed_time = setting;
}
int get_show_relative_timel() {
return debugger_show_relative_time;
}
void set_show_relative_time(int setting) { debugger_show_relative_time = setting; }
void set_show_relative_time(int setting) {
debugger_show_relative_time = setting;
}
int get_show_file_and_line() { return debugger_show_file_and_line; }
int get_show_file_and_line() {
return debugger_show_file_and_line;
}
void set_show_file_and_line(int setting) {
debugger_show_file_and_line = setting;
}
void set_show_file_and_line(int setting) { debugger_show_file_and_line = setting; }
char *generate_preliminary_string(char *buffer, size_t buffer_length, double tss, double tsl,
const char *filename, const int linenumber, const char *prefix) {
@@ -244,10 +228,92 @@ void _inform(const char *filename, const int linenumber, const char *format, ...
pthread_setcancelstate(oldState, NULL);
}
void _debug_print_buffer(const char *thefilename, const int linenumber, int level, void *ibuf,
size_t buf_len) {
if (level > debuglev)
return;
char *vbuf = (char *)ibuf; // this to stop the compiler complaining about indexing through a void
unsigned int i;
unsigned int hexdump_cols = 16;
// 0x123456: <hexdump_cols * 3> <hexdump_cols>\n
const size_t buffer_size = 2 + 6 + 2 + hexdump_cols * 3 + hexdump_cols + 1;
char *buf = malloc(buffer_size);
if (buf) {
// char *bufp = buf;
// *buf = '\0';
// if (msg)
// printf("%s", msg);
#define ADDR_PREFIX_LEN 10 // "0x000000: "
// each complete row
for (i = 0; i < (buf_len / hexdump_cols); i++) {
char *bufp = buf;
*buf = '\0';
snprintf(bufp, ADDR_PREFIX_LEN + 1, "0x%06x ", (i * hexdump_cols) & 0xFFFFFF);
bufp += ADDR_PREFIX_LEN;
unsigned int j;
for (j = 0; j < hexdump_cols; j++) {
snprintf(bufp, strlen("12 ") + 1, "%02x ", 0xFF & vbuf[i * hexdump_cols + j]);
bufp += strlen("12 ");
}
for (j = 0; j < hexdump_cols; j++) {
if (isprint(vbuf[i * hexdump_cols + j])) {
*bufp = 0xFF & vbuf[i * hexdump_cols + j];
} else {
*bufp = '.';
}
bufp++;
}
*bufp = '\0';
_debug(thefilename, linenumber, level, "%s", buf);
}
size_t remaining = buf_len % hexdump_cols;
if (remaining != 0) {
size_t starting_offset = buf_len - remaining;
char *bufp = buf;
*buf = '\0';
snprintf(bufp, ADDR_PREFIX_LEN + 1, "0x%06zx ", starting_offset & 0xFFFFFF);
bufp += ADDR_PREFIX_LEN;
unsigned int j;
for (j = 0; j < hexdump_cols; j++) {
if (j < remaining) {
snprintf(bufp, strlen("12 ") + 1, "%02x ", 0xFF & vbuf[starting_offset + j]);
} else {
snprintf(bufp, strlen(" ") + 1, " "); // fille with blanks
}
bufp += strlen("12 ");
}
for (j = 0; j < remaining; j++) {
if (isprint(vbuf[i * hexdump_cols + j])) {
*bufp = 0xFF & vbuf[starting_offset + j];
} else {
*bufp = '.';
}
bufp++;
}
*bufp = '\0';
_debug(thefilename, linenumber, level, "%s", buf);
}
free(buf);
}
}
/*
void _debug_print_buffer(const char *thefilename, const int linenumber, int level, void *vbuf,
size_t buf_len) {
if (level > debuglev)
return;
new_debug_print_buffer(thefilename, linenumber, level, vbuf, buf_len);
char *buf = (char *)vbuf;
char *obf =
malloc(buf_len * 4 + 1); // to be on the safe side -- 4 characters on average for each byte
@@ -275,3 +341,4 @@ void _debug_print_buffer(const char *thefilename, const int linenumber, int leve
free(obf);
}
}
*/
+10 -5
View File
@@ -58,11 +58,16 @@ EXTERNC void set_show_file_and_line(int setting);
#endif
// Function declarations with printf-style format checking
EXTERNC void _die(const char *filename, const int linenumber, const char *format, ...) PRINTF_LIKE(3,4);
EXTERNC void _warn(const char *filename, const int linenumber, const char *format, ...) PRINTF_LIKE(3,4);
EXTERNC void _inform(const char *filename, const int linenumber, const char *format, ...) PRINTF_LIKE(3,4);
EXTERNC void _debug(const char *filename, const int linenumber, int level, const char *format, ...) PRINTF_LIKE(4,5);
EXTERNC void _debug_print_buffer(const char *thefilename, const int linenumber, int level, void *buf,
EXTERNC void _die(const char *filename, const int linenumber, const char *format, ...)
PRINTF_LIKE(3, 4);
EXTERNC void _warn(const char *filename, const int linenumber, const char *format, ...)
PRINTF_LIKE(3, 4);
EXTERNC void _inform(const char *filename, const int linenumber, const char *format, ...)
PRINTF_LIKE(3, 4);
EXTERNC void _debug(const char *filename, const int linenumber, int level, const char *format, ...)
PRINTF_LIKE(4, 5);
EXTERNC void _debug_print_buffer(const char *thefilename, const int linenumber, int level,
void *buf,
size_t buf_len); // not printf-style, no change needed
#define die(...) _die(__FILE__, __LINE__, __VA_ARGS__)
#define debug(...) _debug(__FILE__, __LINE__, __VA_ARGS__)
+45
View File
@@ -0,0 +1,45 @@
/*
* Generate a device-related UUID. This file is part of Shairport Sync
* Copyright (c) Mike Brady 2026
* All rights reserved.
*
* 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.
*/
#include <stdlib.h>
#include <string.h>
#include <uuid/uuid.h>
#include "definitions.h"
#include "generate_device_uuid.h"
// user is responsible for deallocating returned string
char *generate_device_uuid(const char *device_id) {
uuid_t namespace_uuid;
uuid_t derived_uuid;
uuid_parse(SHAIRPORT_SYNC_DEVICE_NAMESPACE, namespace_uuid);
uuid_generate_sha1(derived_uuid, namespace_uuid, device_id, strlen(device_id));
char *uuid = malloc(UUID_STR_LEN + 1);
uuid_unparse_lower(derived_uuid, uuid);
return uuid;
}
+3
View File
@@ -0,0 +1,3 @@
#pragma once
char *generate_device_uuid(const char *device_id);
+44
View File
@@ -0,0 +1,44 @@
/*
* Generate a random UUID. This file is part of Shairport Sync
* Copyright (c) Mike Brady 2026
* All rights reserved.
*
* 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.
*/
#include <stdlib.h>
#include <uuid/uuid.h>
#include "generate_random_uuid.h"
// user is responsible for deallocating returned string
char *generate_random_uuid() {
// generate a UUID
// from https://stackoverflow.com/questions/51053568/generating-a-random-uuid-in-c
// with thanks
uuid_t binuuid;
uuid_generate_random(binuuid);
char *uuid = malloc(UUID_STR_LEN + 1); // leave space for the NUL at the end
// Produces a UUID string at uuid consisting of lower-case letters
uuid_unparse_lower(binuuid, uuid);
return uuid;
}
+6
View File
@@ -0,0 +1,6 @@
#ifndef _GENERATE_RANDOM_UUID_H
#define _GENERATE_RANDOM_UUID_H
char *generate_random_uuid();
#endif // _GENERATE_RANDOM_UUID_H
+1 -4
View File
@@ -1,9 +1,6 @@
/*
* 23-bit modular arithmetic. This file is part of Shairport Sync
* Copyright (c) Mike Brady 2025
* Modifications, including those associated with audio synchronization, multithreading and
* metadata handling copyright (c) Mike Brady 2014--2025
* All rights reserved.
*
* Permission is hereby granted, free of charge, to any person
@@ -27,8 +24,8 @@
* OTHER DEALINGS IN THE SOFTWARE.
*/
#include <stdint.h>
#include "mod23.h"
#include <stdint.h>
#define MOD_23BIT 0x7FFFFF // 2^23 - 1
+3 -4
View File
@@ -1,6 +1,5 @@
#ifndef _MOD23_H
#define _MOD23_H
#pragma once
#include <stdint.h>
int32_t a_minus_b_mod23(uint32_t a, uint32_t b);
#endif // _MOD23_H
+31 -8
View File
@@ -1,6 +1,6 @@
/*
* Network Utilities. This file is part of Shairport Sync.
* Copyright (c) Mike Brady 2014--2025
* Copyright (c) Mike Brady 2026
* All rights reserved.
*
* Permission is hereby granted, free of charge, to any person
@@ -24,12 +24,13 @@
* OTHER DEALINGS IN THE SOFTWARE.
*/
#include <errno.h>
#include <string.h>
#include "network_utilities.h"
#include "common.h"
#include <errno.h>
#include <pthread.h>
#include <string.h>
int eintr_checked_accept(int sockfd, struct sockaddr *addr,
socklen_t *addrlen) {
int eintr_checked_accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen) {
int response;
do {
response = accept(sockfd, addr, addrlen);
@@ -37,11 +38,33 @@ int eintr_checked_accept(int sockfd, struct sockaddr *addr,
if (response == -1) {
char errorstring[1024];
strerror_r(errno, (char *)errorstring, sizeof(errorstring));
debug(1,
"error %d accept()ing a socketin ap2_event_receiver: \"%s\". (Note: error %d will be ignored.)",
errno, errorstring, EINTR);
debug(1, "error %d accept()ing a socket %d: \"%s\". (Note: error %d will be ignored.)", errno,
sockfd, errorstring, EINTR);
}
} while ((response == -1) && (errno == EINTR));
return response;
}
pthread_mutex_t safe_socket_lock = PTHREAD_MUTEX_INITIALIZER;
int _safe_socket_close(const char *filename, const int linenumber, int *sockfd) {
int result = 0;
int oldstate;
pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, &oldstate);
debug_mutex_lock(&safe_socket_lock, 1000000, 1);
if (*sockfd == 0) {
_debug(filename, linenumber, 1, "_safe_socket_close: socket is zero!");
}
if ((*sockfd != -1) && (*sockfd != 0)) {
_debug(filename, linenumber, 4, "_safe_socket_close: closing socket %d.", *sockfd);
result = close(*sockfd);
if (result == 0)
*sockfd = -1;
} else {
_debug(filename, linenumber, 1, "_safe_socket_close: socket already closed!");
}
debug_mutex_unlock(&safe_socket_lock, 4);
pthread_setcancelstate(oldstate, NULL);
return result;
}
+5 -5
View File
@@ -1,10 +1,10 @@
#ifndef _NETWORK_UTILITIES_H
#define _NETWORK_UTILITIES_H
#pragma once
#include <sys/socket.h>
#define restrict
int eintr_checked_accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
int eintr_checked_accept(int sockfd, struct sockaddr *addr,
socklen_t *addrlen);
#endif // _NETWORK_UTILITIES_H
int _safe_socket_close(const char *filename, const int linenumber, int *sockfd);
#define safe_socket_close(sockfd) _safe_socket_close(__FILE__, __LINE__, sockfd)
+294
View File
@@ -0,0 +1,294 @@
/*
* RTSP Message Utilities. This file is part of Shairport Sync.
* Copyright (c) Mike Brady 2026
* All rights reserved.
*
* 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.
*/
#include "rtsp_message_utilities.h"
#include "../common.h"
#include "../rtsp.h"
#include <inttypes.h>
#include <stdlib.h>
#include <string.h>
#ifdef CONFIG_AIRPLAY_2
#include <plist/plist.h>
#ifdef HAVE_LIBPLIST_GE_2_3_0
#define plist_from_memory(plist_data, length, plist) \
plist_from_memory((plist_data), (length), (plist), NULL)
#endif
#endif
// every time we want to retain or release a reference count, lock it with this
// if a reference count is read as zero, it means the it's being deallocated.
static pthread_mutex_t reference_counter_lock = PTHREAD_MUTEX_INITIALIZER;
static int msg_indexes = 1;
void msg_retain(rtsp_message *msg) {
int rc = debug_mutex_lock(&reference_counter_lock, 500000, 4);
if (rc)
debug(1, "Error %d locking reference counter lock", rc);
if (msg > (rtsp_message *)0x00010000) {
msg->referenceCount++;
debug(4, "msg_free increment reference counter message %d to %d.", msg->index_number,
msg->referenceCount);
// debug(1,"msg_retain -- item %d reference count %d.", msg->index_number, msg->referenceCount);
rc = pthread_mutex_unlock(&reference_counter_lock);
if (rc)
debug(1, "Error %d unlocking reference counter lock", rc);
} else {
debug(1, "invalid rtsp_message pointer 0x%" PRIxPTR " passed to retain", (uintptr_t)msg);
}
}
rtsp_message *msg_init(void) {
// no thread cancellation points here
int rc = debug_mutex_lock(&reference_counter_lock, 500000, 4);
if (rc)
debug(1, "Error %d locking reference counter lock", rc);
rtsp_message *msg = malloc(sizeof(rtsp_message));
if (msg) {
memset(msg, 0, sizeof(rtsp_message));
msg->referenceCount = 1; // from now on, any access to this must be protected with the lock
msg->index_number = msg_indexes++;
debug(4, "msg_init message %d", msg->index_number);
} else {
die("msg_init -- can not allocate memory for rtsp_message %d.", msg_indexes);
}
// debug(1,"msg_init -- create item %d.", msg->index_number);
rc = pthread_mutex_unlock(&reference_counter_lock);
if (rc)
debug(1, "Error %d unlocking reference counter lock", rc);
return msg;
}
int msg_add_header(rtsp_message *msg, char *name, char *value) {
if (msg->nheaders >= sizeof(msg->name) / sizeof(char *)) {
warn("too many headers?!");
return 1;
}
msg->name[msg->nheaders] = strdup(name);
msg->value[msg->nheaders] = strdup(value);
msg->nheaders++;
return 0;
}
char *msg_get_header(rtsp_message *msg, char *name) {
unsigned int i;
for (i = 0; i < msg->nheaders; i++)
if (!strcasecmp(msg->name[i], name))
return msg->value[i];
return NULL;
}
void _debug_print_msg_headers(rtsp_conn_info *conn, const char *filename, const int linenumber,
int level, rtsp_message *msg) {
unsigned int i;
if (conn != NULL) {
if (msg->respcode != 0)
_debug(filename, linenumber, level, "Connection: %d, Response Code: %d.",
conn->connection_number, msg->respcode);
for (i = 0; i < msg->nheaders; i++) {
_debug(filename, linenumber, level, "Connection: %d, Type: \"%s\", content: \"%s\"",
conn->connection_number, msg->name[i], msg->value[i]);
}
} else {
if (msg->respcode != 0)
_debug(filename, linenumber, level, " Response Code: %d.", msg->respcode);
for (i = 0; i < msg->nheaders; i++) {
_debug(filename, linenumber, level, " Type: \"%s\", content: \"%s\"", msg->name[i],
msg->value[i]);
}
}
}
void msg_free(rtsp_message **msgh) {
debug_mutex_lock(&reference_counter_lock, 1000, 0);
if (*msgh > (rtsp_message *)0x00010000) {
rtsp_message *msg = *msgh;
msg->referenceCount--;
if (msg->referenceCount)
debug(4, "msg_free decrement reference counter message %d to %d", msg->index_number,
msg->referenceCount);
if (msg->referenceCount == 0) {
unsigned int i;
for (i = 0; i < msg->nheaders; i++) {
free(msg->name[i]);
free(msg->value[i]);
}
if (msg->content)
free(msg->content);
// debug(1,"msg_free item %d -- free.",msg->index_number);
uintptr_t index = (msg->index_number) & 0xFFFF;
if (index == 0)
index = 0x10000; // ensure it doesn't fold to zero.
*msgh =
(rtsp_message *)(index); // put a version of the index number of the freed message in here
debug(4, "msg_free freed message %d", msg->index_number);
free(msg);
} else {
// debug(1,"msg_free item %d -- decrement reference to
// %d.",msg->index_number,msg->referenceCount);
}
} else if (*msgh != NULL) {
debug(1,
"msg_free: error attempting to free an allocated but already-freed rtsp_message, number "
"%" PRIxPTR ".",
(uintptr_t)*msgh);
}
debug_mutex_unlock(&reference_counter_lock, 0);
}
int msg_handle_line(rtsp_message **pmsg, char *line) {
rtsp_message *msg = *pmsg;
if (!msg) {
msg = msg_init();
*pmsg = msg;
char *sp, *p;
sp = NULL; // this is to quieten a compiler warning
debug(4, "RTSP/HTTP Message Received: \"%s\".", line);
p = strtok_r(line, " ", &sp);
if (!p)
goto fail;
strncpy(msg->method, p, sizeof(msg->method) - 1);
p = strtok_r(NULL, " ", &sp);
if (!p)
goto fail;
strncpy(msg->path, p, sizeof(msg->path) - 1);
p = strtok_r(NULL, " ", &sp);
if (!p)
goto fail;
if ((strcmp(p, "RTSP/1.0") != 0) && (strcmp(p, "HTTP/1.1") != 0)) {
debug(1, "Problem with Message: \"%s\"", p);
goto fail;
}
return -1;
}
if (strlen(line)) {
char *p;
p = strstr(line, ": ");
if (!p) {
warn("bad header: >>%s<<", line);
goto fail;
}
*p = 0;
p += 2;
msg_add_header(msg, line, p);
debug(4, " %s: %s.", line, p);
return -1;
} else {
char *cl = msg_get_header(msg, "Content-Length");
if (cl)
return atoi(cl);
else
return 0;
}
fail:
debug(1, "msg_handle_line fail");
msg_free(pmsg);
*pmsg = NULL;
return 0;
}
#ifdef CONFIG_AIRPLAY_2
int rtsp_message_contains_plist(rtsp_message *message) {
int reply = 0; // assume there is no plist in the message
if ((message->contentlength >= strlen("bplist00")) &&
(strstr(message->content, "bplist00") == message->content))
reply = 1;
return reply;
}
plist_t plist_from_rtsp_content(rtsp_message *message) {
plist_t the_plist = NULL;
if (rtsp_message_contains_plist(message)) {
plist_from_memory(message->content, message->contentlength, &the_plist);
}
return the_plist;
}
char *plist_as_xml_text(plist_t the_plist) {
// caller must free the returned character buffer
// convert it to xml format
uint32_t size;
char *plist_out = NULL;
plist_to_xml(the_plist, &plist_out, &size);
// put it into a NUL-terminated string
char *reply = malloc(size + 1);
if (reply) {
memcpy(reply, plist_out, size);
reply[size] = '\0';
}
if (plist_out)
free(plist_out);
return reply;
}
// caller must free the returned character buffer
char *rtsp_plist_content(rtsp_message *message) {
char *reply = NULL;
// first, check if it has binary plist content
if (rtsp_message_contains_plist(message))
reply = plist_as_xml_text(plist_from_rtsp_content(message));
return reply;
}
#endif
void _debug_log_rtsp_message(rtsp_conn_info *conn, const char *filename, const int linenumber,
int level, char *prompt, rtsp_message *message) {
if (level > debug_level())
return;
if ((prompt) && (*prompt != '\0')) // okay to pass NULL or an empty list...
_debug(filename, linenumber, level, "%s", prompt);
_debug_print_msg_headers(conn, filename, linenumber, level, message);
#ifdef CONFIG_AIRPLAY_2
char *plist_content = rtsp_plist_content(message);
if (plist_content) {
_debug(filename, linenumber, level, " Content length: %u. Content Plist (as XML):\n--\n%s--",
message->contentlength, plist_content);
free(plist_content);
} else
#endif
{
_debug(filename, linenumber, level, " Content length: %u.", message->contentlength);
if (message->contentlength > 0) {
_debug_print_buffer(filename, linenumber, level, message->content, message->contentlength);
}
}
}
+31
View File
@@ -0,0 +1,31 @@
#pragma once
#include "player.h"
#include "rtsp.h"
rtsp_message *msg_init(void);
int msg_handle_line(rtsp_message **pmsg, char *line);
int msg_add_header(rtsp_message *msg, char *name, char *value);
char *msg_get_header(rtsp_message *msg, char *name);
void _debug_log_rtsp_message(rtsp_conn_info *conn, const char *filename, const int linenumber,
int level, char *prompt, rtsp_message *message);
#define debug_log_rtsp_message_conn(conn, level, prompt, message) \
_debug_log_rtsp_message(conn, __FILE__, __LINE__, level, prompt, message)
#define debug_log_rtsp_message(level, prompt, message) \
_debug_log_rtsp_message(NULL, __FILE__, __LINE__, level, prompt, message)
void _debug_print_msg_headers(rtsp_conn_info *conn, const char *filename, const int linenumber,
int level, rtsp_message *msg);
#define debug_print_msg_headers(level, message) \
_debug_print_msg_headers(NULL, __FILE__, __LINE__, level, message)
#define debug_print_msg_headers_conn(level, message) \
_debug_print_msg_headers(conn, __FILE__, __LINE__, level, message)
#ifdef CONFIG_AIRPLAY_2
int rtsp_message_contains_plist(rtsp_message *message);
plist_t plist_from_rtsp_content(rtsp_message *message);
#endif
+194
View File
@@ -0,0 +1,194 @@
/*
* Some character string utilities. This file is part of Shairport Sync
* Copyright (c) Mike Brady 2026
* All rights reserved.
*
* 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.
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include "common.h"
#include "string_utilities.h"
// mDNS supports maximum of 63-character names (we prepend 13, so only 50 left).
#define MAX_AIRPLAY_SERVICE_NAME_LENGTH 50
/* from
* http://coding.debuntu.org/c-implementing-str_replace-replace-all-occurrences-substring#comment-722
* with thanks.
*/
char *str_replace(const char *string, const char *substr, const char *replacement) {
char *tok = NULL;
char *newstr = NULL;
char *oldstr = NULL;
char *head = NULL;
/* if either substr or replacement is NULL, duplicate string a let caller handle it */
if (substr == NULL || replacement == NULL)
return strdup(string);
newstr = strdup(string);
head = newstr;
if (head) {
while ((tok = strstr(head, substr))) {
oldstr = newstr;
newstr = malloc(strlen(oldstr) - strlen(substr) + strlen(replacement) + 1);
/*failed to alloc mem, free old string and return NULL */
if (newstr == NULL) {
free(oldstr);
return NULL;
}
memcpy(newstr, oldstr, tok - oldstr);
memcpy(newstr + (tok - oldstr), replacement, strlen(replacement));
memcpy(newstr + (tok - oldstr) + strlen(replacement), tok + strlen(substr),
strlen(oldstr) - strlen(substr) - (tok - oldstr));
memset(newstr + strlen(oldstr) - strlen(substr) + strlen(replacement), 0, 1);
/* move back head right after the last replacement */
head = newstr + (tok - oldstr) + strlen(replacement);
free(oldstr);
}
} else {
die("failed to allocate memory in str_replace.");
}
return newstr;
}
/*
* append_truncated - Append `suffix` to `base`, returning a newly malloc'd
* string. If the combined result would exceed `limit` bytes (excluding the
* NUL terminator), the base is truncated so that the output is:
*
* <truncated base>...<suffix>
*
* The returned string is always NUL-terminated and never exceeds `limit`
* bytes of content. The caller is responsible for free()'ing it.
*
* Parameters:
* base - the primary string
* suffix - the string to append
* limit - maximum byte count of the resulting string (excl. NUL)
*
* Returns:
* A newly malloc'd string on success, or NULL on failure (limit too small
* to fit "..." + suffix, or malloc failure).
*/
char *append_truncated(const char *base, const char *suffix, size_t limit) {
const char *ellipsis = "...";
size_t ellipsis_len = strlen(ellipsis);
size_t suffix_len = strlen(suffix);
size_t base_len = strlen(base);
size_t combined_len = base_len + suffix_len;
size_t result_len;
size_t head_len;
char *out;
if (combined_len <= limit) {
/* Fits without truncation. */
result_len = combined_len;
out = malloc(result_len + 1);
if (!out)
return NULL;
memcpy(out, base, base_len);
memcpy(out + base_len, suffix, suffix_len + 1); /* +1 for NUL */
return out;
}
/* Not enough room to fit even "..." + suffix. */
if (limit < ellipsis_len + suffix_len)
return NULL;
head_len = limit - ellipsis_len - suffix_len;
/* Respect UTF-8: don't cut in the middle of a multibyte sequence.
* Walk back from head_len until we're at a character boundary
* (i.e. a byte that is not a UTF-8 continuation byte 0x80–0xBF). */
while (head_len > 0 && (base[head_len] & 0xC0) == 0x80)
head_len--;
result_len = head_len + ellipsis_len + suffix_len;
out = malloc(result_len + 1);
if (!out)
return NULL;
char *p = out;
memcpy(p, base, head_len);
p += head_len;
memcpy(p, ellipsis, ellipsis_len);
p += ellipsis_len;
memcpy(p, suffix, suffix_len);
p += suffix_len;
*p = '\0';
return out;
}
char *service_name(const char *raw_service_name) {
char *response = NULL;
// now, do the substitutions in the service name
char hostname[256];
gethostname(hostname, sizeof(hostname));
// strip off a terminating .<anything>, e.g. .local from the hostname
char *last_dot = strrchr(hostname, '.');
if (last_dot != NULL)
*last_dot = '\0';
char *i0;
if (raw_service_name == NULL)
i0 = strdup("%H"); // default
else
i0 = strdup(raw_service_name); // this is the string provided in the configuration or on the
// command line.
// here, do the substitutions for %h, %H, %v and %V
char *i1 = str_replace(i0, "%h", hostname);
if ((hostname[0] >= 'a') && (hostname[0] <= 'z'))
hostname[0] = hostname[0] - 0x20; // convert a lowercase first letter into a capital letter
char *i2 = str_replace(i1, "%H", hostname);
char *i3 = str_replace(i2, "%v", PACKAGE_VERSION);
char *vs = get_version_string();
char *i4 = str_replace(i3, "%V", vs); // service name complete
// now, we may need to add "(Classic)" and/or truncate it to MAX_AIRPLAY_SERVICE_NAME_LENGTH
// characters.
#ifdef CONFIG_AIRPLAY_2
if ((raw_service_name == NULL) && (config.service_type == APST_forced_classic)) {
response = append_truncated(
i4, " (Classic)", MAX_AIRPLAY_SERVICE_NAME_LENGTH); // append "(Classic)" to default service
// name if forced to Classic
} else {
#endif
response = append_truncated(
i4, "",
MAX_AIRPLAY_SERVICE_NAME_LENGTH); // make sure it doesn't exceed the max length: 63 - 13
#ifdef CONFIG_AIRPLAY_2
}
#endif
free(i0);
free(i1);
free(i2);
free(i3);
free(i4);
free(vs);
return response;
}
+8
View File
@@ -0,0 +1,8 @@
#pragma once
/* from
* http://coding.debuntu.org/c-implementing-str_replace-replace-all-occurrences-substring#comment-722
*/
char *str_replace(const char *string, const char *substr, const char *replacement);
char *service_name(const char *raw_service_name);
+3 -7
View File
@@ -1,9 +1,6 @@
/*
* Structured Buffer. This file is part of Shairport Sync
* Copyright (c) Mike Brady 2025
* Modifications, including those associated with audio synchronization, multithreading and
* metadata handling copyright (c) Mike Brady 2014--2025
* All rights reserved.
*
* Permission is hereby granted, free of charge, to any person
@@ -27,12 +24,11 @@
* OTHER DEALINGS IN THE SOFTWARE.
*/
#include <stdarg.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdarg.h>
#include <stdio.h>
#include <stdint.h>
typedef struct {
char *buf;