Merge remote-tracking branch 'origin/development'
This commit is contained in:
@@ -0,0 +1,4 @@
|
||||
.github
|
||||
documents
|
||||
docker/Dockerfile
|
||||
docker/classic/Dockerfile
|
||||
+1
-1
@@ -1 +1 @@
|
||||
custom: [paypal.me/UMBr]
|
||||
custom: [paypal.me/UMBr]
|
||||
@@ -1,12 +1,23 @@
|
||||
<<<<<<< HEAD
|
||||
name: Issue Report
|
||||
description: File an Issue
|
||||
title: "[Problem]: "
|
||||
labels: ["new issue"]
|
||||
=======
|
||||
name: Bug Report
|
||||
description: File a bug report
|
||||
title: "[Bug]: "
|
||||
labels: ["bug"]
|
||||
>>>>>>> origin/development
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
<<<<<<< HEAD
|
||||
Thanks for taking the time to fill out this report!
|
||||
=======
|
||||
Thanks for taking the time to fill out this bug report!
|
||||
>>>>>>> origin/development
|
||||
- type: textarea
|
||||
id: what-happened
|
||||
attributes:
|
||||
@@ -21,6 +32,7 @@ body:
|
||||
description: Please copy and paste any relevant log output. This will be automatically formatted into code, so no need for backticks.
|
||||
render: shell
|
||||
- type: textarea
|
||||
<<<<<<< HEAD
|
||||
id: os
|
||||
attributes:
|
||||
label: Operating System?
|
||||
@@ -28,12 +40,15 @@ body:
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
=======
|
||||
>>>>>>> origin/development
|
||||
id: version
|
||||
attributes:
|
||||
label: Version?
|
||||
description: What version of Shairport Sync are you running? (`shairport-sync -V`)
|
||||
validations:
|
||||
required: true
|
||||
<<<<<<< HEAD
|
||||
- type: dropdown
|
||||
id: install
|
||||
attributes:
|
||||
@@ -45,6 +60,8 @@ body:
|
||||
- Built from source
|
||||
validations:
|
||||
required: true
|
||||
=======
|
||||
>>>>>>> origin/development
|
||||
- type: checkboxes
|
||||
id: checked-current-issues
|
||||
attributes:
|
||||
@@ -52,4 +69,8 @@ body:
|
||||
description: Please check previous issues (including closed ones) for duplicates.
|
||||
options:
|
||||
- label: Confirm
|
||||
<<<<<<< HEAD
|
||||
required: true
|
||||
=======
|
||||
required: true
|
||||
>>>>>>> origin/development
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# Builds & pushes a docker image when a commit is made to one of the branches specified below.
|
||||
# Tag pattern: 'unstable-[BRANCH NAME]'.
|
||||
# Builds & pushes a docker image when a commit is made to one of the branches specified below.
|
||||
# Tag pattern: 'unstable-[BRANCH NAME]' & 'unstable-[BRANCH NAME]-classic'
|
||||
|
||||
name: Build and push docker image based on commit to specified branches.
|
||||
|
||||
@@ -22,6 +22,14 @@ jobs:
|
||||
- name: Set SHAIRPORT_SYNC_BRANCH env.
|
||||
run: echo "SHAIRPORT_SYNC_BRANCH=${GITHUB_REF##*/}" >> $GITHUB_ENV
|
||||
|
||||
- name: Determine NQPTP env (part 1).
|
||||
if: ${{ env.SHAIRPORT_SYNC_BRANCH == 'development' }}
|
||||
run: echo "NQPTP_BRANCH=development" >> $GITHUB_ENV
|
||||
|
||||
- name: Determine NQPTP env (part 2).
|
||||
if: ${{ env.SHAIRPORT_SYNC_BRANCH != 'development' }}
|
||||
run: echo "NQPTP_BRANCH=main" >> $GITHUB_ENV
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v1
|
||||
|
||||
@@ -35,13 +43,33 @@ jobs:
|
||||
username: ${{ secrets.DOCKER_REGISTRY_USER }}
|
||||
password: ${{ secrets.DOCKER_REGISTRY_TOKEN }}
|
||||
|
||||
- name: Set tag start (part 1).
|
||||
if: ${{ env.SHAIRPORT_SYNC_BRANCH == 'master' }}
|
||||
run: echo "IMAGE_TAG_BASE=unstable" >> $GITHUB_ENV
|
||||
|
||||
- name: Set tag start (part 2).
|
||||
if: ${{ env.SHAIRPORT_SYNC_BRANCH != 'master' }}
|
||||
run: echo "IMAGE_TAG_BASE=unstable-${{ env.SHAIRPORT_SYNC_BRANCH }}" >> $GITHUB_ENV
|
||||
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v2
|
||||
with:
|
||||
context: ./
|
||||
file: ./docker/airplay1/Dockerfile
|
||||
file: ./docker/Dockerfile
|
||||
platforms: ${{ env.DOCKER_PLATFORMS }}
|
||||
push: true
|
||||
tags: ${{ secrets.DOCKER_IMAGE_NAME }}:unstable-${{ env.SHAIRPORT_SYNC_BRANCH }}
|
||||
tags: ${{ secrets.DOCKER_IMAGE_NAME }}:${{ env.IMAGE_TAG_BASE }}
|
||||
build-args: |
|
||||
SHAIRPORT_SYNC_BRANCH=${{ env.SHAIRPORT_SYNC_BRANCH }}
|
||||
NQPTP_BRANCH=${{ env.NQPTP_BRANCH }}
|
||||
|
||||
- name: Build and push (AirPlay 1)
|
||||
uses: docker/build-push-action@v2
|
||||
with:
|
||||
context: ./
|
||||
file: ./docker/classic/Dockerfile
|
||||
platforms: ${{ env.DOCKER_PLATFORMS }}
|
||||
push: true
|
||||
tags: ${{ secrets.DOCKER_IMAGE_NAME }}:${{ env.IMAGE_TAG_BASE }}-classic
|
||||
build-args: |
|
||||
SHAIRPORT_SYNC_BRANCH=${{ env.SHAIRPORT_SYNC_BRANCH }}
|
||||
@@ -1,8 +1,8 @@
|
||||
# Builds & pushes a docker image when a tag occurs.
|
||||
# It seems this yaml has to exist on the branch the tag refers to.
|
||||
|
||||
# The following tags are created & pushed:
|
||||
# [RELEASE TAG] and latest
|
||||
# The following docker tags are created & pushed:
|
||||
# [RELEASE TAG], [RELEASE TAG]-classic,
|
||||
|
||||
name: Build and push docker images on releases.
|
||||
|
||||
@@ -31,6 +31,14 @@ jobs:
|
||||
|
||||
- name: Set tag env
|
||||
run: echo "GIT_TAG=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV
|
||||
|
||||
- name: Determine NQPTP env (part 1).
|
||||
if: ${{ env.SHAIRPORT_SYNC_BRANCH == 'development' }}
|
||||
run: echo "NQPTP_BRANCH=development" >> $GITHUB_ENV
|
||||
|
||||
- name: Determine NQPTP env (part 2).
|
||||
if: ${{ env.SHAIRPORT_SYNC_BRANCH != 'development' }}
|
||||
run: echo "NQPTP_BRANCH=main" >> $GITHUB_ENV
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v1
|
||||
@@ -49,9 +57,21 @@ jobs:
|
||||
uses: docker/build-push-action@v2
|
||||
with:
|
||||
context: ./
|
||||
file: ./docker/airplay1/Dockerfile
|
||||
file: ./docker/Dockerfile
|
||||
platforms: ${{ env.DOCKER_PLATFORMS }}
|
||||
push: true
|
||||
tags: ${{ secrets.DOCKER_IMAGE_NAME }}:latest, ${{ secrets.DOCKER_IMAGE_NAME }}:${{ env.GIT_TAG }}
|
||||
tags: ${{ secrets.DOCKER_IMAGE_NAME }}:${{ env.GIT_TAG }}
|
||||
build-args: |
|
||||
SHAIRPORT_SYNC_BRANCH=${{ env.SHAIRPORT_SYNC_BRANCH }}
|
||||
NQPTP_BRANCH=${{ env.NQPTP_BRANCH }}
|
||||
|
||||
- name: Build and push (Classic)
|
||||
uses: docker/build-push-action@v2
|
||||
with:
|
||||
context: ./
|
||||
file: ./docker/classic/Dockerfile
|
||||
platforms: ${{ env.DOCKER_PLATFORMS }}
|
||||
push: true
|
||||
tags: ${{ secrets.DOCKER_IMAGE_NAME }}:${{ env.GIT_TAG }}-classic
|
||||
build-args: |
|
||||
SHAIRPORT_SYNC_BRANCH=${{ env.SHAIRPORT_SYNC_BRANCH }}
|
||||
+9
-1
@@ -5,7 +5,6 @@
|
||||
/shairport-sync-mpris-test-client
|
||||
|
||||
*.o
|
||||
*.log
|
||||
/*~
|
||||
*.xml~
|
||||
/config.mk
|
||||
@@ -26,6 +25,7 @@ stamp-h1
|
||||
man/Makefile
|
||||
man/Makefile.in
|
||||
scripts/shairport-sync.service
|
||||
scripts/shairport-sync.service-avahi
|
||||
scripts/shairport-sync
|
||||
shairport-sync.core
|
||||
gitversion.h
|
||||
@@ -36,6 +36,7 @@ plist_xml_strings.*
|
||||
/org.gnome.ShairportSync.service
|
||||
/dbus-interface.*
|
||||
/mpris-interface.*
|
||||
/plist_xml_strings.*
|
||||
/lib_*.*
|
||||
|
||||
# Some eclipse project files
|
||||
@@ -46,4 +47,11 @@ plist_xml_strings.*
|
||||
.DS_Store
|
||||
shairport-sync.xcodeproj
|
||||
|
||||
# version string generation
|
||||
gitversion.h
|
||||
|
||||
# separate build directory
|
||||
build
|
||||
|
||||
# vscode
|
||||
.vscode
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
Here is how to add a Shairport Sync device to the Apple Home application on iOS 15.3.1. Note that Shairport Sync has to be built with AirPlay 2 support.
|
||||
|
||||
***
|
||||
|
||||
Step 1: Open the "Home" app
|
||||
---
|
||||
|
||||
Tap the `Add Accessory` panel or choose "Add Accessory" from the `+` symbol in the top right:
|
||||
|
||||

|
||||
|
||||
***
|
||||
|
||||
Step 2: "More options..."
|
||||
---
|
||||
|
||||
Step 2: Tap `More options...`
|
||||
|
||||

|
||||
|
||||
|
||||
***
|
||||
|
||||
Step 3: Select your Shairport Sync device:
|
||||
---
|
||||
|
||||
Your Shairport Sync device should appear here, though you may have to scroll. Tap the appropriate icon and follow the remaining instructions.
|
||||
|
||||

|
||||
@@ -0,0 +1,12 @@
|
||||
## Events
|
||||
Shairport Sync can run programs just before it starts to play an audio stream and just after it finishes.
|
||||
You specify them using the `sessioncontrol` group settings `run_this_before_play_begins` and `run_this_after_play_ends`.
|
||||
This is to facilitate situations where something has to be done before and after playing, e.g. switching on an amplifier beforehand
|
||||
and switching it off afterwards.
|
||||
Set the `wait_for_completion` value to `"yes"` for Shairport Sync to wait until the respective commands have been completed before continuing.
|
||||
|
||||
Note that the full path to the programs must be specified, and script files will not be executed unless they are marked as executable
|
||||
and have the appropriate shebang `#!/bin/...` as the first line. (This behaviour may be different from other Shairports.)
|
||||
|
||||
Shairport Sync can run a program whenever the volume is set or changed. You specify it using the `general` group setting `run_this_when_volume_changes`.
|
||||
This is to facilitate situations where something has to be done when the volume changes, e.g. adjust an external mixer value. Set the `wait_for_completion` value to `"yes"` for Shairport Sync to wait until the command has been completed before continuing. Again, please note that the full path to the program must be specified, and script files will not be executed unless they are marked as executable and have the appropriate shebang `#!/bin/...` as the first line.
|
||||
@@ -0,0 +1,54 @@
|
||||
# Get The Best from Shairport Sync
|
||||
Shairport Sync was designed to run best on dedicated stand-alone low-power "headless" Linux/FreeBSD systems with ALSA as the audio system
|
||||
and with a decent CD-quality Digital to Analog Converter (DAC).
|
||||
|
||||
## CPU Power and Memory
|
||||
Computer power and memory requirements are modest -- a Raspberry Pi 2 or better, including the Pi Zero 2 W, is fine.
|
||||
Unfortunately, while the original Raspberry Pi and the Pi Zero are powerful enough for AirPlay operation,
|
||||
they are not really suitable for AirPlay 2 operation.
|
||||
|
||||
## CPU Clock
|
||||
For best performance, Shairport Sync requires a stable and accurate system clock.
|
||||
This is because the output DAC's output rate is normally determined by the system clock (some high-end USB streamers use their own built-in clocks).
|
||||
If the clock drifts, or if its actual frequency is far from its nominal frequency, Shairport Sync will have to do more interpolation,
|
||||
which inevitably must degrade the audio fidelity, even if it is very hard to hear.
|
||||
Some very old laptops are known to have inaccurate clocks and and some embedded systems can suffer from temperature-related clock drift.
|
||||
|
||||
Recent Raspberry Pis seem to have very accurate clocks with low drift.
|
||||
|
||||
## Linux
|
||||
The best kind of Linux for Shairport Sync is a "bare" Linux.
|
||||
Raspberry Pi OS Lite, Debian Minimal Server, Ubuntu Server, Fedora Server and Arch Linux (Minimal Configuration) are good examples.
|
||||
|
||||
Shairport Sync also runs on "desktop" Linuxes such as Raspberry Pi OS with desktop, Debian with a desktop environment,
|
||||
Ubuntu Desktop and Fedora Workstation.
|
||||
Desktop Linuxes are less suitable because they almost always use a sound server like PulseAudio or PipeWire.
|
||||
These can interfere with Shairport Sync, which needs direct and exclusive access to the audio hardware.
|
||||
|
||||
## DAC
|
||||
A good Digital to Analog Converter (DAC) will have a huge influence on the quality of the audio.
|
||||
|
||||
Shairport Sync runs at 44,100 frames per second (FPS), each frame consisting of a pair of signed 16 bit linear samples, one for left and one for right.
|
||||
Actually, Shairport Sync will take advantage to 24- or 32-bit DACs if available, and will run at 44,100 or 88,200, 176,400 or 352,800 FPS if necessary,
|
||||
though there is no advantage to the higher rates. The 44,100 FPS rate was chosen to match the rate of AirPlay.
|
||||
|
||||
Good DACs are available at a very wide range of prices, from low-cost USB "Sound Cards" to very high-end HiFi streaming DACs.
|
||||
|
||||
In the Raspberry Pi world, many very good low-cost I2S DACs, some with integrated amplifiers, are available. Staying with the Raspberry Pi, the DAC powering the built-in audio jack is not great. It may be good enough for trying out Shairport Sync, but it has a very limited frequency response and can generate very large transients when it starts up. A separate DAC will transform the output quality.
|
||||
|
||||
**Note**
|
||||
Make sure that the DAC is capable of 44,100 FPS operation -- this is really mandatory.
|
||||
Most recent DACs are okay, but some older DACs will only run at 48,000 FPS or multiples of it, and Shairport Sync can not use them.
|
||||
## Maximum Output Level
|
||||
The `volume_max_db` setting allows you to reduce the maximum level of DAC output to prevent possible overloading of the amplifier or premplifier it feeds.
|
||||
## Volume Range
|
||||
The volume range is the difference (technically the ratio, expressed in dB) between the highest and lowest level of the volume control. Ideally, this should give the highest volume at the high end and a barely audible sound at the lowest level. Typical volume ranges are 60 dB to 75dB. If the range is much less than this, the difference between high and low volume won't seem large enough to the listener. If the range is much more, much of the low end of the volume control range will be inaudible. (The built-in DAC of the Raspberry Pi has this problem.) Use the `volume_range_db` setting to set the volume range. The built-in attenuator will be used to augment the volume range if necessary.
|
||||
## Volume Control
|
||||
Audio is sent at full volume in AirPlay and AirPlay 2 with separate information being sent to set the actual volume. This volume information can be used in three ways:
|
||||
* It can be used to control a built-in attenuator. This is the default.
|
||||
* It can be used to control a mixer built in to the DAC. To use a mixer, set the `mixer_control_name` in the configuration file to the name of the mixer you wish to use.
|
||||
* It can be ignored by Shairport Sync. This may be appropriate when you want the volume to be controlled by just one control, typically an audio system's volume control knob or the volume control of a car radio. To make Shairport Sync ignore the volume information, set `ignore_volume_control` to `"yes"`.
|
||||
* AirPlay volume changes cause events when an executable -- a program or executable script -- can be called. So, you could get Shairport Sync to ignore volume control information but call an executable to control an external volume control.
|
||||
## Other Settings
|
||||
* The `disable_standby_mode` setting can be used to prevent the DAC from transitioning between active and standby states, which can sometimes cause a faint popping noise. If activated, Shairport Sync sends frames of silence to keep the DAC busy, preventing it from entering standby mode.
|
||||
* The `disable_synchronisation` setting can be used to prevent Shairport Sync from doing any interpolation whatever. Normally, this would lead to a problem when the DAC's buffer either overflowed or underflowed, but some very high-end streamers adjust the rate at which their DACs run to match the exact rate at which audio arrives, thus preventing underflow or overflow.
|
||||
@@ -0,0 +1,205 @@
|
||||
# Initial Configuration
|
||||
Shairport Sync reads settings from a configuration file at `/etc/shairport-sync.conf` (note that in FreeBSD it will be at `/usr/local/etc/shairport-sync.conf`). When you run `$sudo make install`, a sample configuration file is installed or updated at `/etc/shairport-sync.conf.sample` (`/usr/local/etc/shairport-sync.conf.sample` in FreeBSD). This contains all the setting groups and all the settings available, but they all are commented out (comments begin with `//`) so that default values are used. The file contains explanations of the settings, useful hints and suggestions. In addition, if the file doesn't already exist, a default configuration is installed, which should work in almost any system with a sound card.
|
||||
|
||||
Settings in the configuration file are grouped. For instance, there is a `general` group within which you can use the `name` tag to set the service name. Suppose you wanted to set the name of the service to `Front Room` and give the service the password `secret`, then you should do the following:
|
||||
|
||||
```
|
||||
general =
|
||||
{
|
||||
name = "Front Room";
|
||||
password = "secret";
|
||||
// ... other general settings
|
||||
};
|
||||
|
||||
```
|
||||
The password setting is only valid for classic Shairport Sync.
|
||||
|
||||
(Remember, anything preceded by `//` is a comment and will have no effect on the setting of Shairport Sync.)
|
||||
|
||||
**Important:** You should *never* use an important password as the AirPlay password for a Shairport Sync player -- the password is stored in Shairport Sync's configuration file in plain text and is thus completely vulnerable.
|
||||
|
||||
No backend is specified here, so it will default to the `alsa` backend if more than one back end has been compiled. To route the output to PulseAudio, add:
|
||||
```
|
||||
output_backend = "pa";
|
||||
```
|
||||
to the `general` group.
|
||||
|
||||
The `alsa` group is used to specify properties of the output device. The most obvious setting is the name of the output device which you can set using the `output_device` tag.
|
||||
|
||||
The following `alsa` group settings are very important for maximum performance. If your audio device has a mixer that can be use to control the volume, then Shairport Sync can use it to give instant response to volume and mute commands and it can offload some work from the processor.
|
||||
* The `mixer_control_name` tag allows you to specify the name of the mixer volume control.
|
||||
* The `mixer_device` tag allows you specify where the mixer is. By default, the mixer is on the `output_device`, so you only need to use the `mixer_device` tag if the mixer is elsewhere. This can happen if you specify a *device* rather than a *card* with the `output_device` tag, because normally a mixer is associated with a *card* rather than a device. Suppose you wish to use the output device `5` of card `hw:0` and the mixer volume-control named `PCM`:
|
||||
|
||||
```
|
||||
alsa =
|
||||
{
|
||||
output_device = "hw:0,5";
|
||||
mixer_device = "hw:0";
|
||||
mixer_control_name = "PCM";
|
||||
// ... other alsa settings
|
||||
};
|
||||
```
|
||||
|
||||
The `pa` group is used to specify settings relevant to the PulseAudio backend. You can set the "Application Name" that will appear in the "Sound" control panel.
|
||||
|
||||
Note: Shairport Sync can take configuration settings from command line options. This is mainly for backward compatibility, but sometimes still useful. For normal use, it is strongly recommended that you use the configuration file method.
|
||||
|
||||
**Raspberry Pi**
|
||||
|
||||
The Raspberry Pi Models A and B have a built-in audio DAC that is connected to the device's headphone jack. Apart from a loud click when used for the first time after power-up, it is now quite adequate for casual listening.
|
||||
|
||||
To get the benefits of improvements in the Pi's software and firmware, you should update to the Raspian release of October 2018 or later, as a number of improvements have been made to the built-in DAC.
|
||||
|
||||
Do the usual update and upgrade:
|
||||
```
|
||||
# apt update
|
||||
# apt upgrade
|
||||
```
|
||||
To make Shairport Sync output to the built-in audio DAC and use its hardware mixer, in the `alsa` section of the configuration file, set the output device and mixer as follows:
|
||||
```
|
||||
alsa =
|
||||
{
|
||||
output_device = "hw:0"; // the name of the alsa output device. Use "alsamixer" or "aplay" to find out the names of devices, mixers, etc.
|
||||
mixer_control_name = "PCM"; // the name of the mixer to use to adjust output volume. If not specified, volume in adjusted in software.
|
||||
// ... other alsa settings
|
||||
```
|
||||
(Remember to uncomment the lines by removing the `//` at the start of each.) When these changes have been made, reboot the machine.
|
||||
|
||||
A problem with the built-in DAC that it declares itself to have a very large mixer volume control range – all the way from -102.38dB up to +4dB, a range of 106.38 dB. In reality, only the top 60 dB of it is in any way usable. To help get the most from it, consider using the `volume_range_db` setting in the `general` group to instruct Shairport Sync to use the top of the DAC mixer's declared range. For example, if you set the `volume_range_db` figure to 60, the top 60 dB of the range will the used. With this setting on the Raspberry Pi, maximum volume will be +4dB and minimum volume will be -56dB, below which muting will occur.
|
||||
|
||||
From a user's point of view, the effect of using this setting is to move the minimum usable volume all the way down to the bottom of the user's volume control, rather than have the minimum usable volume concentrated very close to the maximum volume.
|
||||
|
||||
*Command Line Arguments*
|
||||
|
||||
As previously mentioned, you can use command line arguments to provide settings to Shairport Sync as before, though newer settings will only be available via the configuration file. For full information, please read the Shairport Sync `man` page, also available at http://htmlpreview.github.io/?https://github.com/mikebrady/shairport-sync/blob/master/man/shairport-sync.html.
|
||||
|
||||
Apart from the following options, all command line options can be replaced by settings in the configuration file. Here is a brief description of command line options that are not replicated by settings in the settings file.
|
||||
|
||||
* The `-c` option allows you to specify the location of the configuration file.
|
||||
* The `-V` option gives you version information about Shairport Sync and then quits.
|
||||
* The `-d` option causes Shairport Sync to properly daemonise itself, that is, to run in the background. You may need sudo privileges for this.
|
||||
* The `-k` option causes Shairport Sync to kill an existing Shairport Sync daemon. You may need to have sudo privileges for this.
|
||||
|
||||
The System V init script at `/etc/init.d/shairport-sync` has a bare minimum :
|
||||
`-d`. Basically all it does is put the program in daemon mode. The program will read its settings from the configuration file.
|
||||
|
||||
Examples
|
||||
--------
|
||||
|
||||
Here are some examples of complete configuration files.
|
||||
|
||||
```
|
||||
general = {
|
||||
name = "Joe's Stereo";
|
||||
};
|
||||
|
||||
alsa = {
|
||||
output_device = "hw:0";
|
||||
};
|
||||
```
|
||||
|
||||
This gives the service a particular name — "Joe's Stereo" and specifies that audio device `hw:0` be used.
|
||||
|
||||
For best results with the `alsa` backend — including getting true mute and instant response to volume control and pause commands — you should access the hardware volume controls. Use `amixer` or `alsamixer` or similar to discover the name of the mixer control to be used as the `mixer_control_name`.
|
||||
|
||||
Here is an example for for a Raspberry Pi using its internal soundcard — device hw:0 — that drives the headphone jack:
|
||||
```
|
||||
general = {
|
||||
name = "Mike's Boombox";
|
||||
};
|
||||
|
||||
alsa = {
|
||||
output_device = "hw:0";
|
||||
mixer_control_name = "PCM";
|
||||
};
|
||||
```
|
||||
|
||||
Here is an example of driving a Topping TP30 Digital Amplifier, which has an integrated USB DAC and which is connected as audio device `hw:1`:
|
||||
```
|
||||
general = {
|
||||
name = "Kitchen";
|
||||
};
|
||||
|
||||
alsa = {
|
||||
output_device = "hw:1";
|
||||
mixer_control_name = "PCM";
|
||||
};
|
||||
```
|
||||
|
||||
For a cheapo "3D Sound" USB card (Stereo output and input only) on a Raspberry Pi:
|
||||
```
|
||||
general = {
|
||||
name = "Front Room";
|
||||
};
|
||||
|
||||
alsa = {
|
||||
output_device = "hw:1";
|
||||
mixer_control_name = "Speaker";
|
||||
};
|
||||
```
|
||||
|
||||
For a first generation Griffin iMic on a Raspberry Pi:
|
||||
```
|
||||
general = {
|
||||
name = "Attic";
|
||||
};
|
||||
|
||||
alsa = {
|
||||
output_device = "hw:1";
|
||||
mixer_control_name = "PCM";
|
||||
};
|
||||
```
|
||||
|
||||
For an NSLU2, which has no internal sound card, there appears to be a bug in ALSA — you can not specify a device other than "default". Thus:
|
||||
|
||||
On an NSLU2, to drive a first generation Griffin iMic:
|
||||
```
|
||||
general = {
|
||||
name = "Den";
|
||||
};
|
||||
|
||||
alsa = {
|
||||
mixer_control_name = "PCM";
|
||||
};
|
||||
```
|
||||
|
||||
On an NSLU2, to drive the "3D Sound" USB card:
|
||||
```
|
||||
general = {
|
||||
name = "TV Room";
|
||||
};
|
||||
|
||||
alsa = {
|
||||
mixer_control_name = "Speaker";
|
||||
};
|
||||
```
|
||||
|
||||
Finally, here is an example of using the PulseAudio backend:
|
||||
```
|
||||
general = {
|
||||
name = "Zoe's Computer";
|
||||
output_backend = "pa";
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
|
||||
Latency
|
||||
-------
|
||||
Latency is the exact time from a sound signal's original timestamp until that signal actually "appears" on the output of the audio output device, usually a Digital to Audio Converter (DAC), irrespective of any internal delays, processing times, etc. in the computer.
|
||||
|
||||
Shairport Sync uses latencies supplied by the source, typically either 2 seconds or just over 2.25 seconds. You shouldn't need to change them.
|
||||
|
||||
Problems can arise when you are trying to synchronise with speaker systems — typically surround-sound home theatre systems — that have their own inherent delays. You can compensate for an inherent delay using the appropriate backend (typically `alsa`) `audio_backend_latency_offset_in_seconds`. Set this offset (in frames) to compensate for a fixed delay in the audio back end; for example, if the output device delays by 100 ms, set this to -0.1.
|
||||
|
||||
Resynchronisation
|
||||
-------------
|
||||
Shairport Sync actively maintains synchronisation with the source.
|
||||
If synchronisation is lost — say due to a busy source or a congested network — Shairport Sync will mute its output and resynchronise. The loss-of-sync threshold is a very conservative 0.050 seconds — i.e. the actual time and the expected time must differ by more than 50 ms to trigger a resynchronisation. Smaller disparities are corrected by insertions or deletions, as described above.
|
||||
* You can vary the resync threshold, or turn resync off completely, with the `general` `resync_threshold_in_seconds` setting.
|
||||
|
||||
Tolerance
|
||||
---------
|
||||
Playback synchronisation is allowed to wander — to "drift" — a small amount before attempting to correct it. The default is 0.002 seconds, i.e. 2 ms. The smaller the tolerance, the more likely it is that overcorrection will occur. Overcorrection is when more corrections (insertions and deletions) are made than are strictly necessary to keep the stream in sync. Use the `statistics` setting to monitor correction levels. Corrections should not greatly exceed net corrections.
|
||||
* You can vary the tolerance with the `general` `drift_tolerance_in_seconds` setting.
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
# 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.
|
||||
|
||||
|
||||
## Metadata over UDP**
|
||||
|
||||
As an alternative to sending metadata to a pipe, the `socket_address` and `socket_port` tags may be set in the metadata group to cause Shairport Sync
|
||||
to broadcast UDP packets containing the track metadata.
|
||||
|
||||
The advantage of UDP is that packets can be sent to a single listener or, if a multicast address is used, to multiple listeners.
|
||||
It also allows metadata to be routed to a different host. However UDP has a maximum packet size of about 65000 bytes; while large enough for most data, Cover Art will often exceed this value. Any metadata exceeding this limit will not be sent over the socket interface. The maximum packet size may be set with the `socket_msglength` tag to any value between 500 and 65000 to control this - lower values may be used to ensure that each UDP packet is sent in a single network frame. The default is 500. Other than this restriction, metadata sent over the socket interface is identical to metadata sent over the pipe interface.
|
||||
|
||||
The UDP metadata format is very simple - the first four bytes are the metadata *type*, and the next four bytes are the metadata *code*
|
||||
(both are sent in network byte order - see https://github.com/mikebrady/shairport-sync-metadata-reader for a definition of those terms).
|
||||
The remaining bytes of the packet, if any, make up the raw value of the metadata.
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
# Advanced Topics
|
||||
Here you will find links to some advanced features and things you can do with Shairport Sync.
|
||||
* [Finish setting up](InitialConfiguration.md).
|
||||
* [Get the best](GetTheBest.md) from your system.
|
||||
* [Metadata](Metadata.md).
|
||||
* [Events](Events.md).
|
||||
* [Statistics](Statistics.md).
|
||||
* Setting up an [MQTT](../MQTT.md) system.
|
||||
* [Digital Signal Processing](https://github.com/mikebrady/shairport-sync/wiki/Digital-Signal-Processing-with-Shairport-Sync).
|
||||
* [Car Installation](../CAR%20INSTALL.md)
|
||||
– build an isolated WiFi network containing a Shairport Sync player on a Raspberry Pi. Suitable for a car radio with an `aux` input or for the stereo in that broadband-free holiday cottage.
|
||||
|
||||
The configuration file -- which contains lots of documentation -- is on your system. By default, the sample configuration file is
|
||||
placed at `/etc/shairport-sync.conf.sample` (`/usr/local/etc/shairport-sync.conf.sample` on FreeBSD).
|
||||
You can also view an online version [here](https://github.com/mikebrady/shairport-sync/blob/master/scripts/shairport-sync.conf).
|
||||
|
||||
Build configuration flags are discussed [here](CONFIGURATION%20FLAGS.md).
|
||||
@@ -0,0 +1,118 @@
|
||||
# Statistics
|
||||
|
||||
If you set the `statistics` setting in the `diagnostics` section of the configuration file to `"YES"`, some statistics will be logged at regular intervals. The items logged will depend on the type of stream being processed: AirPlay 1, AirPlay 2 Buffered Audio or AirPlay 2 Realtime Audio. If the `log_verbosity` is set to 1, 2 or 3, additional items will be logged.
|
||||
|
||||
From an audio enthusiast's point of view, the most important figure is possibly the `All Sync PPM` figure. This is the total amount of interpolation needed by Shairport Sync to keep the audio stream in sync. The number represents is the ratio of frames added and removed from the audio stream relative to all the frames output in the last interval, expressed in parts per million (PPM). For reference, adding or removing one frame per second into a 44,100 frames per second stream is ± 22.68 PPM. The lower this number number is, the higher the fidelity of the audio signal passed to the output device. On a well sorted system, this figure can be 0.0 for considerable periods, but it can't really be zero forever unless the output device is adapting to the true data rate (some very high-end streamers seem to do so). You may also find that the number might be higher at the start while the system settles down.
|
||||
|
||||
The second most important figure is possibly the `Sync Error ms`. This is the average synchronisation error in milliseconds in the last interval. Ideally it should be 0.0. By default, Shairport Sync has a tolerance of a sync error of ± 2.0 milliseconds without triggering interpolation.
|
||||
|
||||
Two other interesting measurements of the output rate may be available -- `Output FPS (r)` and `Output FPS (c)`, where `(r)` means "raw" and the `(c)` means "corrected".
|
||||
|
||||
The "raw" figure is the rate at which the output device (typically a DAC) accepts data measured relative to the computer's own system clock (specifically the `CLOCK_MONOTONIC_RAW` clock). The accuracy of the number depends on the accuracy of the clock, which will typically be accurate to within anything from 20 to 100 ppm.
|
||||
|
||||
The "corrected" figure is the rate at which the output device accepts data relative to the computer's network-time-disciplined clock (specifically the `CLOCK_MONOTONIC` clock). This clock is normally adjusted ("disciplined") to keep time with network time and should be accurate to with a few tens of milliseconds over a _long_ period. So (1) if you could run a play session for a long period -- say a day -- and (2) if network time synchronisation is enabled and (3) if the network connection to the network time service is fast and stable, then you should get an accurate absolute measure of exact frame rate of the output device. If your internet connection is not good, the corrected figure will be very inaccurate indeed.
|
||||
|
||||
Here is a brief description of the figures that might be provided.
|
||||
##### Sync Error ms
|
||||
Average playback synchronisation error in milliseconds in the last interval. By default, Shairport Sync will allow a sync error of ± 2.0 milliseconds without any interpolation. Positive means late, negative means early.
|
||||
##### Net Sync PPM
|
||||
This is the total amount of interpolation done by Shairport Sync to keep the audio stream in sync. The number represents is the total number of frames added and removed from the audio stream, expressed in parts per million (PPM) in the last interval. For reference, adding or removing one frame per second into a 44,100 frames per second stream is 22.68 ppm.
|
||||
##### All Sync PPM
|
||||
This is the net amount of interpolation done by Shairport Sync to keep the audio stream in sync. The number represents is the number of frames added plus the number removed from the audio stream, expressed in parts per million (PPM) in the last interval. The magnitude of this should be the same as the `net sync ppm'. If it is much larger it means that Shairport Sync is overcorrecting for sync errors -- try increasing the drift tolerance to reduce it.
|
||||
##### Packets
|
||||
This is the number of packets of audio frames received since the start of the session. A packet normally contains 352 ± 1 audio frames.
|
||||
##### Missing
|
||||
This is the number of packets of audio frames that were expected but not received in the last interval. It should be zero, and if not it usually indicates a significant problem with the network. AirPlay 1 and AirPlay 2 Realtime Streams only.
|
||||
##### Late
|
||||
This is the number of packets of audio frames that were received late -- but still in time to be used -- in the last interval. AirPlay 1 and AirPlay 2 Realtime Streams only.
|
||||
##### Too Late
|
||||
This is the number of packets of audio frames that were received too late to be used in the last interval. It is possible that these packets were already received, so those frames might not actually be missing when the time comes to play them. AirPlay 1 and AirPlay 2 Realtime Streams only.
|
||||
##### Resend Reqs
|
||||
This is the number of times Shairport Sync requests the resending of missing frames. Requests can be for one or more frames. AirPlay 1 and AirPlay 2 Realtime Streams only.
|
||||
##### Min DAC Queue
|
||||
The is the smallest number of frames of audio in the DAC's hardware queue. If it goes too low, the DAC may begin to underrun.
|
||||
##### Min Buffers
|
||||
The is the smallest number of packets of audio in the queue to be processed in the last interval. It is related to the overall latency in AirPlay 1 and AirPlay 2 Realtime Streams. If it comes close to zero it's often a sign that the network is poor.
|
||||
##### Max Buffers
|
||||
The is the largest number of packets of audio in the queue to be processed in the last interval.
|
||||
##### Min Buffer Size
|
||||
The is smallest remaining number of bytes in the Buffered Audio buffer in the last interval. It can legitimately be zero when a track ends or begins. If it reaches zero while a track is playing, it means that audio data is not arriving at Shairport Sync quickly enough and may indicate network problems. AirPlay 2 Buffered Audio streams only.
|
||||
##### Nominal FPS
|
||||
This is the rate specified in the AirPlay stream itself. AirPlay 1 only.
|
||||
##### Received FPS
|
||||
This is the rate at which frames are received from the network averaged since the start of the play session. AirPlay 1 only.
|
||||
##### Output FPS (r)
|
||||
Output rate measured relative to the computer system's clock since the start of the play session. See above for a discussion.
|
||||
##### Output FPS (c)
|
||||
Output rate measured relative to the network-clock-disciplined computer system's clock since the start of the play session. See above for a discussion.
|
||||
##### Source Drift PPM
|
||||
This is a measure of the difference between the source clock and Shairport Sync's clock expressed in parts per million. Only valid when 10 or more drift samples have been received. AirPlay 1 only.
|
||||
##### Drift Samples
|
||||
This is the number drift samples have been accepted for calculating the source drift. AirPlay 1 only.
|
||||
#### Example
|
||||
The following example is of an AirPlay 2 Buffered Audio Stream from a HomePod mini to a WiFi-connected Raspberry Pi 3 equipped with a Pimoroni "Audio DAC SHIM (Line-Out)" with `log_verbosity` set to `1` after 3 hours and 37 minutes of operation. The audio is from a radio station accessed through Apple Music:
|
||||
|
||||
```
|
||||
5.292055362 "rtsp.c:3112" Connection 1. AP2 Buffered Audio Stream.
|
||||
1.382815677 "player.c:2650" Connection 1: Playback Started -- AirPlay 2 Buffered.
|
||||
7.899808955 "player.c:2491" Sync Error ms | Net Sync PPM | All Sync PPM | Min DAC Queue | Min Buffer Size | Output FPS (r) | Output FPS (c)
|
||||
...
|
||||
8.014025361 "player.c:2491" -1.69 2.8 2.8 7341 198k 44100.04 44100.35
|
||||
7.992082237 "player.c:2491" -1.70 2.8 2.8 7332 188k 44100.04 44100.34
|
||||
8.015987549 "player.c:2491" -1.68 0.0 0.0 7334 186k 44100.04 44100.35
|
||||
8.010537393 "player.c:2491" -1.68 2.8 2.8 7335 187k 44100.04 44100.36
|
||||
7.993940934 "player.c:2491" -1.85 0.0 0.0 7329 198k 44100.04 44100.37
|
||||
8.015796195 "player.c:2491" -1.87 5.7 5.7 7325 194k 44100.04 44100.37
|
||||
8.021665934 "player.c:2491" -1.92 5.7 5.7 7329 190k 44100.04 44100.37
|
||||
7.990409477 "player.c:2491" -1.85 2.8 2.8 7347 201k 44100.04 44100.37
|
||||
8.001858278 "player.c:2491" -1.96 17.0 17.0 7332 204k 44100.04 44100.37
|
||||
8.007150986 "player.c:2491" -1.89 2.8 2.8 7327 195k 44100.04 44100.36
|
||||
7.998612862 "player.c:2491" -1.87 2.8 2.8 7317 199k 44100.04 44100.36
|
||||
8.020592653 "player.c:2491" -1.89 11.3 11.3 7323 203k 44100.04 44100.35
|
||||
8.003245674 "player.c:2491" -1.89 8.5 8.5 7325 198k 44100.04 44100.35
|
||||
7.990874789 "player.c:2491" -1.98 19.8 19.8 7103 197k 44100.04 44100.34
|
||||
8.010600257 "player.c:2491" -1.85 2.8 2.8 7327 187k 44100.04 44100.33
|
||||
8.004573122 "player.c:2491" -1.89 5.7 5.7 7327 187k 44100.04 44100.33
|
||||
8.012853278 "player.c:2491" -1.89 5.7 5.7 7326 190k 44100.04 44100.32
|
||||
8.005596664 "player.c:2491" -1.95 22.7 22.7 7322 165k 44100.04 44100.31
|
||||
8.019198695 "player.c:2491" -1.85 5.7 5.7 7333 166k 44100.04 44100.31
|
||||
7.999363382 "player.c:2491" -1.79 2.8 2.8 7349 153k 44100.04 44100.30
|
||||
7.990332549 "player.c:2491" -1.87 5.7 5.7 7328 155k 44100.04 44100.29
|
||||
8.012287445 "player.c:2491" -1.84 2.8 2.8 7352 185k 44100.04 44100.28
|
||||
7.998929528 "player.c:2491" -1.94 14.2 14.2 7331 171k 44100.04 44100.28
|
||||
8.008170622 "player.c:2491" -1.88 5.7 5.7 7327 188k 44100.04 44100.27
|
||||
8.012935257 "player.c:2491" -1.87 19.8 19.8 7270 205k 44100.04 44100.26
|
||||
8.006337966 "player.c:2491" -1.86 0.0 0.0 7334 199k 44100.04 44100.25
|
||||
8.004206612 "player.c:2491" -1.76 2.8 2.8 7331 204k 44100.04 44100.25
|
||||
7.998495257 "player.c:2491" -1.82 2.8 2.8 7329 204k 44100.04 44100.24
|
||||
8.016859059 "player.c:2491" -1.88 2.8 2.8 7335 201k 44100.04 44100.23
|
||||
7.993529320 "player.c:2491" -1.92 14.2 14.2 7329 205k 44100.04 44100.22
|
||||
8.021956820 "player.c:2491" -1.96 11.3 11.3 7322 206k 44100.04 44100.21
|
||||
8.006401820 "player.c:2491" -1.93 11.3 11.3 7322 199k 44100.04 44100.21
|
||||
8.001469111 "player.c:2491" -1.89 5.7 5.7 7336 204k 44100.04 44100.20
|
||||
7.995194320 "player.c:2491" -1.86 5.7 5.7 7330 202k 44100.04 44100.19
|
||||
8.017805206 "player.c:2491" -1.99 22.7 22.7 7322 201k 44100.04 44100.18
|
||||
7.995541038 "player.c:2491" -1.95 22.7 22.7 7331 203k 44100.04 44100.18
|
||||
8.011463643 "player.c:2491" -1.87 0.0 0.0 7331 207k 44100.04 44100.17
|
||||
7.995941403 "player.c:2491" -1.85 2.8 2.8 7331 206k 44100.04 44100.16
|
||||
8.013330570 "player.c:2491" -1.71 2.8 2.8 7344 212k 44100.04 44100.15
|
||||
8.012214580 "player.c:2491" -1.82 0.0 0.0 7333 203k 44100.04 44100.15
|
||||
7.998585049 "player.c:2491" -1.85 8.5 8.5 7332 207k 44100.04 44100.14
|
||||
8.023448799 "player.c:2491" -1.86 5.7 5.7 7328 206k 44100.04 44100.13
|
||||
7.984586612 "player.c:2491" -1.87 8.5 8.5 7277 205k 44100.04 44100.12
|
||||
8.017339372 "player.c:2491" -1.91 17.0 17.0 7327 207k 44100.04 44100.12
|
||||
8.007090309 "player.c:2491" -1.94 17.0 17.0 7286 199k 44100.04 44100.11
|
||||
8.007894164 "player.c:2491" -1.87 2.8 2.8 7334 201k 44100.04 44100.10
|
||||
7.991787497 "player.c:2491" -1.89 2.8 2.8 7327 194k 44100.04 44100.09
|
||||
8.019011611 "player.c:2491" -1.94 5.7 5.7 7323 185k 44100.04 44100.09
|
||||
7.994695362 "player.c:2491" -1.99 14.2 14.2 7321 199k 44100.04 44100.08
|
||||
8.010284632 "player.c:2491" -1.93 11.3 11.3 7325 191k 44100.04 44100.07
|
||||
8.011451612 "player.c:2491" -1.87 8.5 8.5 7329 156k 44100.04 44100.09
|
||||
7.993112549 "player.c:2491" -1.84 2.8 2.8 7330 150k 44100.04 44100.13
|
||||
8.004971455 "player.c:2491" -1.77 11.3 11.3 7325 157k 44100.04 44100.16
|
||||
8.010517133 "player.c:2491" -1.85 0.0 0.0 7322 182k 44100.04 44100.19
|
||||
8.005598851 "player.c:2491" -2.00 39.7 39.7 7207 180k 44100.04 44100.22
|
||||
8.006777965 "player.c:2491" -1.99 39.7 39.7 7320 183k 44100.04 44100.24
|
||||
7.115508696 "player.c:1643" Connection 1: Playback Stopped. Total playing time 03:37:20. Output: 44100.04 (raw), 44100.24 (corrected) frames per second.
|
||||
```
|
||||
For reference, a drift of one second per day is approximately 11.57 ppm. Left uncorrected, even a drift this small between two audio outputs will be audible after a short time.
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
# AirPlay 2
|
||||
Shairport Sync offers AirPlay 2 support for audio sources on iOS devices, Macs from macOS 10.15 (Catalina) onwards, HomePod minis and Apple TVs.
|
||||
|
||||
## What Works
|
||||
- AirPlay 2 audio for iOS, HomePod mini, AppleTV and Mac players.
|
||||
* Audio is synchronised with other AirPlay 2 devices.
|
||||
* Two types of audio are received by Shairport Sync – "Realtime" streams of CD quality ALAC (like "classic" AirPlay) and "Buffered Audio" streams of AAC stereo at 44,100 frames per second.
|
||||
* The selection of stream type is made by the player.
|
||||
* Realtime streams generally have a latency of about two seconds. Buffered Audio streams typically have a latency of half a second or less.
|
||||
* In AirPlay 2 mode, Shairport Sync reverts to "classic" AirPlay when iTunes on macOS or macOS Music plays to multiple speakers and one of more of them is compatible with AirPlay only.
|
||||
|
||||
- Devices running Shairport Sync in AirPlay 2 mode can be [added](https://github.com/mikebrady/shairport-sync/blob/development/ADDINGTOHOME.md) to the Home app.
|
||||
|
||||
## What Does Not Work
|
||||
- No AirPlay 2 for Windows iTunes.
|
||||
- Remote control facilities are not implemented.
|
||||
- AirPlay 2 from macOS prior to 10.15 (Catalina) is not supported.
|
||||
|
||||
## General
|
||||
Shairport Sync uses a companion application called [NQPTP](https://github.com/mikebrady/nqptp) ("Not Quite PTP")
|
||||
for timing and synchronisation in AirPlay 2. NQPTP must run as `root` and must have exclusive access to ports `319` and `320`.
|
||||
|
||||
Lossless and High Definition Lossless material is transcoded to AAC before it reaches Shairport Sync.
|
||||
|
||||
## What You Need
|
||||
AirPlay 2 support needs a slightly more powerful CPU for decoding and synchronisation and more memory for bigger buffers and larger libraries. A Raspberry Pi 2 or Raspberry Pi Zero 2 W or better is recommended.
|
||||
|
||||
Here are some guidelines:
|
||||
* Full access, including `root` privileges, to a system at least as powerful as a Raspberry Pi 2 or a Raspberry Pi Zero 2 W.
|
||||
* Ports 319 and 320 must be free to use (i.e. they must not be in use by another service such as a PTP service) and must not be blocked by a firewall.
|
||||
* An up-to-date Linux or FreeBSD. This is important, as some of the libraries must be the latest available.
|
||||
* Shairport Sync will not run on a Mac because NQPTP, on which it relies, needs ports 319 and 320, which are already used by macOS.
|
||||
* A version of the [FFmpeg](https://www.ffmpeg.org) library with an AAC decoder capable of decoding Floating Planar -- `fltp` -- material. There is a guide [here](https://github.com/mikebrady/shairport-sync/blob/development/TROUBLESHOOTING.md#aac-decoder-issues-airplay-2-only) to help you find out if your system has it.
|
||||
* An audio output, for example an ALSA device (or `sndio` in FreeBSD). The device must be capable of running at 44,100 frames per second. You can use an `sps-alsa-explore`](https://github.com/mikebrady/sps-alsa-explore) to test the suitability of hardware ALSA audio devices on your device.
|
||||
Other backends continue to work as with "classic" Shairport Sync.
|
||||
|
||||
## Guides
|
||||
* A building guide is available at [BUILD.md](https://github.com/mikebrady/shairport-sync/blob/development/BUILD.md).
|
||||
@@ -0,0 +1,195 @@
|
||||
# Build and Install Shairport Sync
|
||||
This guide is for a basic installation of Shairport Sync in a recent (2018 onwards) Linux or FreeBSD.
|
||||
|
||||
Shairport Sync can be built as an AirPlay 2 player (with [some limitations](https://github.com/mikebrady/shairport-sync/blob/development/AIRPLAY2.md#features-and-limitations)) or as "classic" Shairport Sync – a player for the older, but still supported, AirPlay (aka "AirPlay 1") protocol. Check ["What You Need"](https://github.com/mikebrady/shairport-sync/blob/development/AIRPLAY2.md#what-you-need) for some basic system requirements.
|
||||
|
||||
Overall, you'll be building and installing two programs – Shairport Sync itself and [NQPTP](https://github.com/mikebrady/nqptp), a companion app that Shairport Sync uses for AirPlay 2 timing. If you are building classic Shairport Sync, NQPTP is unnecessary and can be omitted.
|
||||
|
||||
In the commands below, note the convention that a `#` prompt means you are in superuser mode and a `$` prompt means you are in a regular unprivileged user mode. You can use `sudo` *("SUperuser DO")* to temporarily promote yourself from user to superuser, if permitted. For example, if you want to execute `apt-get update` in superuser mode and you are in user mode, enter `sudo apt-get update`.
|
||||
|
||||
## 1. Prepare
|
||||
#### Remove Old Copies of Shairport Sync
|
||||
Before you begin building Shairport Sync, it's best to remove any existing copies of the application, called `shairport-sync`. Use the command `$ which shairport-sync` to find them. For example, if `shairport-sync` has been installed previously, this might happen:
|
||||
```
|
||||
$ which shairport-sync
|
||||
/usr/local/bin/shairport-sync
|
||||
```
|
||||
Remove it as follows:
|
||||
```
|
||||
# rm /usr/local/bin/shairport-sync
|
||||
```
|
||||
Do this until no more copies of `shairport-sync` are found.
|
||||
#### Remove Old Service Files
|
||||
You should also remove any of the following service files that may be present:
|
||||
* `/etc/systemd/system/shairport-sync.service`
|
||||
* `/etc/systemd/user/shairport-sync.service`
|
||||
* `/lib/systemd/user/shairport-sync.service`
|
||||
* `/etc/init.d/shairport-sync`
|
||||
|
||||
New service files will be installed if necessary at the `# make install` stage.
|
||||
#### Reboot after Cleaning Up
|
||||
If you removed any installations of Shairport Sync or any of its service files in the last two steps, you should reboot.
|
||||
|
||||
## 2. Get Tools and Libraries
|
||||
Okay, now let's get the tools and libraries for building and installing Shairport Sync (and NQPTP).
|
||||
|
||||
### Debian / Raspberry Pi OS / Ubuntu
|
||||
```
|
||||
# apt update
|
||||
# apt upgrade # this is optional but recommended
|
||||
# apt install --no-install-recommends build-essential git xmltoman autoconf automake libtool \
|
||||
libpopt-dev libconfig-dev libasound2-dev avahi-daemon libavahi-client-dev libssl-dev libsoxr-dev \
|
||||
libplist-dev libsodium-dev libavutil-dev libavcodec-dev libavformat-dev uuid-dev libgcrypt-dev xxd
|
||||
```
|
||||
If you are building classic Shairport Sync, the list of packages is shorter:
|
||||
```
|
||||
# apt update
|
||||
# apt upgrade # this is optional but recommended
|
||||
# apt-get install --no-install-recommends build-essential git xmltoman autoconf automake libtool \
|
||||
libpopt-dev libconfig-dev libasound2-dev avahi-daemon libavahi-client-dev libssl-dev libsoxr-dev
|
||||
```
|
||||
### Fedora
|
||||
For AirPlay 2 operation, _before you install the libraries_, please ensure the you have [enabled](https://docs.fedoraproject.org/en-US/quick-docs/setup_rpmfusion) RPM Fusion software repositories at least to the "Free" level. If this is not done, FFmpeg libraries will be installed that lack a suitable AAC decoder, preventing Shairport Sync from working in AirPlay 2 mode.
|
||||
```
|
||||
# yum update
|
||||
# yum install make automake gcc gcc-c++ \
|
||||
git xmltoman autoconf automake avahi-devel libconfig-devel openssl-devel popt-devel soxr-devel \
|
||||
ffmpeg ffmpeg-devel libplist-devel libsodium-devel libgcrypt-devel libuuid-devel vim-common \
|
||||
alsa-lib-devel
|
||||
```
|
||||
If you are building classic Shairport Sync, the list of packages is shorter:
|
||||
```
|
||||
# yum update
|
||||
# yum install make automake gcc gcc-c++ \
|
||||
git xmltoman autoconf automake avahi-devel libconfig-devel openssl-devel popt-devel soxr-devel \
|
||||
alsa-lib-devel
|
||||
```
|
||||
### Arch Linux
|
||||
After you have installed the libraries, note that you should enable and start the `avahi-daemon` service.
|
||||
```
|
||||
# pacman -Syu
|
||||
# pacman -Sy git base-devel alsa-lib popt libsoxr avahi libconfig \
|
||||
libsndfile libsodium ffmpeg vim libplist
|
||||
```
|
||||
If you are building classic Shairport Sync, the list of packages is shorter:
|
||||
```
|
||||
# pacman -Syu
|
||||
# pacman -Sy git base-devel alsa-lib popt libsoxr avahi libconfig
|
||||
```
|
||||
Enable and start the `avahi-daemon` service.
|
||||
```
|
||||
# systemctl enable avahi-daemon
|
||||
# systemctl start avahi-daemon
|
||||
```
|
||||
### FreeBSD
|
||||
First, update everything:
|
||||
```
|
||||
# freebsd-update fetch
|
||||
# freebsd-update install
|
||||
# pkg
|
||||
# pkg update
|
||||
```
|
||||
Next, install the Avahi subsystem. FYI, `avahi-app` is chosen because it doesn’t require X11. `nss_mdns` is included to allow FreeBSD to resolve mDNS-originated addresses – it's not actually needed by Shairport Sync. Thanks to [reidransom](https://gist.github.com/reidransom/6033227) for this.
|
||||
```
|
||||
# pkg install avahi-app nss_mdns
|
||||
```
|
||||
Add these lines to `/etc/rc.conf`:
|
||||
```
|
||||
dbus_enable="YES"
|
||||
avahi_daemon_enable="YES"
|
||||
```
|
||||
Next, change the `hosts:` line in `/etc/nsswitch.conf` to
|
||||
```
|
||||
hosts: files dns mdns
|
||||
```
|
||||
Reboot for these changes to take effect.
|
||||
|
||||
Next, install the packages that are needed for Shairport Sync and NQPTP:
|
||||
```
|
||||
# pkg install git autotools pkgconf popt libconfig openssl alsa-utils \
|
||||
libplist libsodium ffmpeg e2fsprogs-libuuid vim
|
||||
```
|
||||
If you are building classic Shairport Sync, the list of packages is shorter:
|
||||
```
|
||||
# pkg install git autotools pkgconf popt libconfig openssl alsa-utils
|
||||
```
|
||||
## 3. Build
|
||||
### NQPTP
|
||||
Skip this section if you are building classic Shairport Sync – NQPTP is not needed for classic Shairport Sync.
|
||||
|
||||
Download, install, enable and start NQPTP from [here](https://github.com/mikebrady/nqptp).
|
||||
|
||||
### Shairport Sync
|
||||
#### Build and Install
|
||||
Download Shairport Sync, check out the `development` branch and configure, compile and install it. Before executing the commands, please note the following:
|
||||
|
||||
* If building for FreeBSD, replace `--with-systemd` with `--with-os=freebsd --with-freebsd-service`.
|
||||
* Omit the `--with-airplay-2` from the `./configure` options if you are building classic Shairport Sync.
|
||||
* If you wish to add extra features, for example an extra audio backend, take a look at the [configuration flags](https://github.com/mikebrady/shairport-sync/blob/development/CONFIGURATION%20FLAGS.md). For this walkthrough, though, please do not remove the `--with-alsa` flag.
|
||||
|
||||
```
|
||||
$ git clone https://github.com/mikebrady/shairport-sync.git
|
||||
$ cd shairport-sync
|
||||
$ git checkout development
|
||||
$ autoreconf -fi
|
||||
$ ./configure --sysconfdir=/etc --with-alsa \
|
||||
--with-soxr --with-avahi --with-ssl=openssl --with-systemd --with-airplay-2
|
||||
$ make
|
||||
# make install
|
||||
```
|
||||
By the way, the `autoreconf` step may take quite a while – please be patient!
|
||||
|
||||
## 4. Test
|
||||
At this point, Shairport Sync should be built and installed but not running. If the user you are logged in as is a member of the unix `audio` group, Shairport Sync should run from the command line:
|
||||
```
|
||||
$ shairport-sync
|
||||
```
|
||||
* Add the `-v` command line option to get some diagnostics.
|
||||
* Add the `--statistics` option to get some infomation about the audio received.
|
||||
|
||||
The AirPlay service should appear on the network and the audio you play should come through to the default ALSA device. (Use `alsamixer` or similar to adjust levels.)
|
||||
|
||||
If you have problems, please check the items in [Final Notes](final_notes) below, or in the [TROUBLESHOOTING.md](https://github.com/mikebrady/shairport-sync/blob/development/TROUBLESHOOTING.md) guide.
|
||||
|
||||
Note: Shairport Sync will run indefinitely -- use Control-C it to stop it.
|
||||
|
||||
## 5. Enable and Start Service
|
||||
Once you are happy that Shairport Sync runs from the command line, you should Control-C out of it and enable and start the `shairport-sync` service. This will launch Shairport Sync automatically as a background "daemon" service when the system powers up:
|
||||
|
||||
### Linux
|
||||
```
|
||||
# systemctl enable shairport-sync
|
||||
```
|
||||
### FreeBSD
|
||||
To make the `shairport-sync` daemon load at startup, add the following line to `/etc/rc.conf`:
|
||||
```
|
||||
shairport_sync_enable="YES"
|
||||
```
|
||||
## 6. Check
|
||||
Reboot the machine. The AirPlay service should once again be visible on the network and audio will be sent to the default ALSA device.
|
||||
|
||||
## 7. Final Notes
|
||||
A number of system settings can affect Shairport Sync. Please review them as follows:
|
||||
|
||||
### Power Saving
|
||||
If your computer has an `Automatic Suspend` Power Saving Option, you should experiment with disabling it, because your computer has to be available for AirPlay service at all times.
|
||||
### WiFi Power Management – Linux
|
||||
If you are using WiFi, you should turn off WiFi Power Management:
|
||||
```
|
||||
# iwconfig wlan0 power off
|
||||
```
|
||||
or
|
||||
```
|
||||
# iw dev wlan0 set power_save off
|
||||
```
|
||||
The motivation for this is that WiFi Power Management will put the WiFi system in low-power mode when the WiFi system is considered inactive. In this mode, the system may not respond to events initiated from the network, such as AirPlay requests. Hence, WiFi Power Management should be turned off. See [TROUBLESHOOTING.md](https://github.com/mikebrady/shairport-sync/blob/master/TROUBLESHOOTING.md#wifi-adapter-running-in-power-saving--low-power-mode) for more details.
|
||||
|
||||
(You can find WiFi device names (e.g. `wlan0`) with `$ ifconfig`.)
|
||||
### Firewall
|
||||
If a firewall is running (some systems, e.g. Fedora, run a firewall by default), ensure it is not blocking ports needed by Shairport Sync and [NQPTP](https://github.com/mikebrady/nqptp/blob/main/README.md#firewall).
|
||||
|
||||
## 8. Connect and enjoy...
|
||||
### Add to Home
|
||||
With AirPlay 2, you can follow the steps in [ADDINGTOHOME.md](https://github.com/mikebrady/shairport-sync/blob/development/ADDINGTOHOME.md) to add your device to the Apple Home system.
|
||||
### Wait, there's more...
|
||||
Shairport Sync has many extra [features](README.md#features) and [settings](https://github.com/mikebrady/shairport-sync/blob/development/scripts/shairport-sync.conf). For instance, you can [add an MQTT service](https://github.com/mikebrady/shairport-sync/blob/master/MQTT.md), change the AirPlay service name, adjust for delays in other equipment (many TVs and AVRs introduce slight delays in their audio processing chains) and more. Explore runtime settings in `/etc/shairport-sync.conf` or [here](https://github.com/mikebrady/shairport-sync/blob/development/scripts/shairport-sync.conf). Build configuration flags are [here](https://github.com/mikebrady/shairport-sync/blob/development/CONFIGURATION%20FLAGS.md).
|
||||
@@ -0,0 +1,3 @@
|
||||
# Build Instructions for AirPlay 1
|
||||
|
||||
This guide has been superseded by the general building guide at [BUILD.md](https://github.com/mikebrady/shairport-sync/blob/development/BUILD.md).
|
||||
@@ -0,0 +1,3 @@
|
||||
# Building Shairport Sync for AirPlay 2
|
||||
|
||||
This guide has been superseded by the general building guide at [BUILD.md](https://github.com/mikebrady/shairport-sync/blob/development/BUILD.md).
|
||||
+67
-69
@@ -1,66 +1,79 @@
|
||||
# Shairport Sync for Cars
|
||||
If your car audio has an AUX input, you can get AirPlay in your car using Shairport Sync. Together, Shairport Sync and an iPhone can give you access to internet radio, YouTube, Apple Music, Spotify, etc. on the move. While Shairport Sync is no substitute for CarPlay, the audio quality is often much better than Bluetooth. Your passengers can enjoy movies with the soundtrack on the car speakers.
|
||||
If your car audio has an AUX input, you can get AirPlay in your car using Shairport Sync. Together, Shairport Sync and an iPhone or an iPad with cellular capability can give you access to internet radio, YouTube, Apple Music, Spotify, etc. on the move. While Shairport Sync is no substitute for CarPlay, the audio quality is often much better than Bluetooth.
|
||||
|
||||
## The Basic Idea
|
||||
|
||||
The basic idea is to use a small Linux computer to create an isolated WiFi network for the car and to run Shairport Sync on it to provide an AirPlay service. The audio goes via a DAC to the AUX input of your car audio system.
|
||||
|
||||
The car WiFi network you create is isolated and local to your car, and since it isn't connected to the Internet, you don't really need to secure it with a password. Likewise, you don't really have to use a password to connect to the AirPlay service.
|
||||
|
||||
When an iPhone or an iPad with cellular capability is connected to an isolated WiFi network like this, it can use its cellular connection to access the Internet.
|
||||
This means it can connect to internet radio, YouTube, Apple Music, Spotify, etc. over the cellular network and play the audio through the car network to the AirPlay service provided by Shairport Sync.
|
||||
The basic idea is to use a small Linux computer to create an isolated WiFi network (a "car network") and run Shairport Sync on it to provide an AirPlay service. An iPhone or an iPad with cellular capability can simultaneously connect to internet radio, YouTube, Apple Music, Spotify, etc. over the cellular network and send AirPlay audio through the car network to the AirPlay service provided by Shairport Sync. This sends the audio to the computer's DAC which is connected to the AUX input of your car audio.
|
||||
|
||||
Note that Android devices can not, so far, do this trick of using the two networks simultaneously.
|
||||
|
||||
## Example
|
||||
|
||||
In this example, a Raspberry Pi Zero W and a Pimoroni PHAT DAC are used. This combination has been tested for over two years. Please note that some of the details of setting up networks are specific to the version of Linux used -- Raspbian Stretch or later.
|
||||
In this example, a Raspberry Pi Zero 2 W and a Pimoroni PHAT DAC are used. Shairport Sync will be built for AirPlay 2 operation, but you can build it for "classic" AirPlay (aka AirPlay 1) operation if you prefer. A Pi Zero W is powerful enough for classic AirPlay.
|
||||
|
||||
Please note that some of the details of setting up networks are specific to the version of Linux used -- Rasberry Pi OS (Bullseye) Lite or later.
|
||||
|
||||
### Prepare the initial SD Image
|
||||
* Download the latest version of Raspbian Lite -- Stretch Lite of 2018-03-13 at the time of writing -- and install it onto an SD Card.
|
||||
* Mount the card on a Linux machine. Two drives should appear -- a `boot` drive and a `rootfs` drive. Both of these need a little modification.
|
||||
* Enable SSH service by creating a file called `ssh` on the `boot` drive. To do this, mount the drive and CD to its `boot` partition (since my username is `mike`, the drive is at `/media/mike/boot`):
|
||||
```
|
||||
$ touch ssh
|
||||
```
|
||||
* Also in the `boot` drive, edit the `config.txt` file to add the overlay needed for the sound card. This may not be necessary in your case, but in this example a Pimoroni PHAT is being used and it needs the following entry to be added:
|
||||
```
|
||||
dtoverlay=hifiberry-dac
|
||||
```
|
||||
* Next, some modifications need to be done to the `rootfs` drive to make the Pi connect to your main WiFi network. (This is a temporary measure to enable you to connect the Pi to your main network so that you can do all the software installation and updating of the software necessary. Later, the Pi will be configured to start its own isolated network.) On the `rootfs` drive, edit the file `etc/wpa_supplicant/wpa_supplicant.conf` (you'll need root privileges) and add the name and password of your main WiFi network (substitute your own network name and password in, but keep the quotation marks):
|
||||
```
|
||||
network={
|
||||
ssid="Network Name"
|
||||
psk="Password"
|
||||
}
|
||||
* Download the latest version of Raspberry Pi OS (Lite) -- Bullseye (Lite) of 2022-04-04 at the time of writing -- and install it onto an SD Card using `Raspberry Pi Imager`. The Lite version is preferable to the Desktop version as it doesn't include a sound server like PulseAudio or PipeWire that can prevent direct access to the audio output device.
|
||||
* Before writing the image to the card, use the Settings control on `Raspberry Pi Imager` to set hostname, enable SSH and provide a username and password to use while building the system. Similarly, you can specify a wireless network the Pi will connect to while building the system. Later on the Pi will be configured to start its own isolated network.
|
||||
* The next few steps are to add the overlay needed for the sound card. This may not be necessary in your case, but in this example a Pimoroni PHAT is being used. If you do not need to add an overlay, skip these steps.
|
||||
* Mount the card on a Linux machine. Two drives should appear -- a `boot` drive and a `rootfs` drive.
|
||||
* `cd` to the `boot` drive (since my username is `mike`, it will be `$ cd /media/mike/boot`).
|
||||
* Edit the `config.txt` file to add the overlay needed for the sound card. This may not be necessary in your case, but in this example a Pimoroni PHAT is being used and it needs the following entry to be added:
|
||||
```
|
||||
dtoverlay=hifiberry-dac
|
||||
```
|
||||
* Close the file and carefully dismount and eject the two drives. *Be sure to dismount and eject the drives properly; otherwise they may be corrupted.*
|
||||
* Remove the SD card from the Linux machine, insert it into the Pi and reboot.
|
||||
|
||||
```
|
||||
Close the file and carefully dismount and eject the two drives. Remove the SD card from the Linux machine, insert it into the Pi and reboot. After a short time, the Pi should appear on your network and you can SSH into it. To check that it has appeared on the network, try to ping it at `raspberrypi.local`. It may take a minute or so to appear. Once it has appeared on your network you can SSH into it and configure it.
|
||||
After a short time, the Pi should appear on your network -- it may take a minute or so. To check, try to `ping` it at the `<hostname>.local`, e.g. if the hostname is `bmw` then use `$ ping bmw.local`. Once it has appeared, you can SSH into it and configure it.
|
||||
|
||||
### Boot, Configure, Update
|
||||
The first thing to do on a Pi would be to use the `raspi-config` tool to expand the file system to use the entire card. It might be useful to change the `hostname` too. Next, do the usual update and upgrade:
|
||||
The first thing to do on a Pi would be to use the `raspi-config` tool to expand the file system to use the entire card. Next, do the usual update and upgrade:
|
||||
```
|
||||
# apt-get update
|
||||
# apt-get upgrade
|
||||
```
|
||||
|
||||
### Shairport Sync
|
||||
First, install the packages needed by Shairport Sync:
|
||||
### Build and Install
|
||||
Let's get the tools and libraries for building and installing Shairport Sync (and NQPTP).
|
||||
|
||||
```
|
||||
# apt-get install build-essential git xmltoman autoconf automake libtool libpopt-dev libconfig-dev libasound2-dev avahi-daemon libavahi-client-dev libssl-dev libsoxr-dev
|
||||
# apt install --no-install-recommends build-essential git xmltoman autoconf automake libtool \
|
||||
libpopt-dev libconfig-dev libasound2-dev avahi-daemon libavahi-client-dev libssl-dev libsoxr-dev \
|
||||
libplist-dev libsodium-dev libavutil-dev libavcodec-dev libavformat-dev uuid-dev libgcrypt-dev xxd
|
||||
```
|
||||
Next, download Shairport Sync, configure it, compile and install it:
|
||||
If you are building classic Shairport Sync, the list of packages is shorter:
|
||||
```
|
||||
# apt-get install --no-install-recommends build-essential git xmltoman autoconf automake libtool \
|
||||
libpopt-dev libconfig-dev libasound2-dev avahi-daemon libavahi-client-dev libssl-dev libsoxr-dev
|
||||
```
|
||||
|
||||
#### NQPTP
|
||||
Skip this section if you are building classic Shairport Sync – NQPTP is not needed for classic Shairport Sync.
|
||||
|
||||
Download, install, enable and start NQPTP from [here](https://github.com/mikebrady/nqptp) following the guide for Linux.
|
||||
|
||||
#### Shairport Sync
|
||||
Download Shairport Sync, check out the `development` branch and configure, compile and install it.
|
||||
|
||||
* Omit the `--with-airplay-2` from the `./configure` options if you are building classic Shairport Sync.
|
||||
|
||||
```
|
||||
$ git clone https://github.com/mikebrady/shairport-sync.git
|
||||
$ cd shairport-sync
|
||||
$ git checkout development
|
||||
$ autoreconf -fi
|
||||
$ ./configure --sysconfdir=/etc --with-alsa --with-avahi --with-ssl=openssl --with-soxr --with-systemd
|
||||
$ ./configure --sysconfdir=/etc --with-alsa \
|
||||
--with-soxr --with-avahi --with-ssl=openssl --with-systemd --with-airplay-2
|
||||
$ make
|
||||
$ sudo make install
|
||||
# make install
|
||||
```
|
||||
*Do not* enable Shairport Sync to automatically start at boot time -- startup is organised differently.
|
||||
The `autoreconf` step may take quite a while – please be patient!
|
||||
|
||||
Third, finish by configuring Shairport Sync.
|
||||
**Note:** *Do not* enable Shairport Sync to start automatically at boot time -- later on in this installation, we will arrange for it to start after the network has been set up.
|
||||
|
||||
### Configure Shairport Sync
|
||||
Here are the important options for the Shairport Sync configuration file at `/etc/shairport-sync.conf`:
|
||||
```
|
||||
// Sample Configuration File for Shairport Sync for Car Audio with a Pimoroni PHAT
|
||||
@@ -98,51 +111,35 @@ Disable both of these services from starting at boot time (this is because we wi
|
||||
#### Configure HostAPD
|
||||
Configure `hostapd` by creating `/etc/hostapd/hostapd.conf` with the following contents which will set up an open network with the name BMW. You might wish to change the name:
|
||||
```
|
||||
# This is the name of the WiFi interface we configured above
|
||||
# Thanks to https://wiki.gentoo.org/wiki/Hostapd#802.11b.2Fg.2Fn_triple_AP
|
||||
|
||||
# The interface used by the AP
|
||||
interface=wlan0
|
||||
|
||||
# Use the nl80211 driver with the brcmfmac driver
|
||||
driver=nl80211
|
||||
|
||||
# This is the name of the network -- yours might be different
|
||||
# This is the name of the network -- yours may be different
|
||||
ssid=BMW
|
||||
|
||||
# Use the 2.4GHz band
|
||||
# "g" simply means 2.4GHz band
|
||||
hw_mode=g
|
||||
|
||||
# Use channel 6
|
||||
channel=9
|
||||
# Channel to use
|
||||
channel=11
|
||||
|
||||
# Enable 802.11n
|
||||
# Limit the frequencies used to those allowed in the country
|
||||
ieee80211d=1
|
||||
|
||||
# The country code
|
||||
country_code=IE
|
||||
|
||||
# Enable 802.11n support
|
||||
ieee80211n=1
|
||||
|
||||
# Enable WMM
|
||||
# QoS support, also required for full speed on 802.11n/ac/ax
|
||||
wmm_enabled=1
|
||||
|
||||
# Enable 40MHz channels with 20ns guard interval
|
||||
#ht_capab=[HT40][SHORT-GI-20][DSSS_CCK-40]
|
||||
|
||||
# Accept all MAC addresses
|
||||
macaddr_acl=0
|
||||
|
||||
# Use WPA authentication
|
||||
#auth_algs=1
|
||||
|
||||
# Require clients to know the network name
|
||||
ignore_broadcast_ssid=0
|
||||
|
||||
# Use WPA2
|
||||
#wpa=2
|
||||
|
||||
# Use a pre-shared key
|
||||
#wpa_key_mgmt=WPA-PSK
|
||||
|
||||
# The network passphrase
|
||||
#wpa_passphrase=none
|
||||
|
||||
# Use AES, instead of TKIP
|
||||
#rsn_pairwise=CCMP
|
||||
```
|
||||
Note that, since car network is isolated from the Internet, you don't really need to secure it with a password.
|
||||
|
||||
#### Configure DHCP server
|
||||
|
||||
First, replace the contents of `/etc/dhcp/dhcpd.conf` with this:
|
||||
@@ -183,6 +180,7 @@ Configure the startup sequence by adding commands to `/etc/rc.local` to start `h
|
||||
/bin/systemctl start shairport-sync
|
||||
|
||||
exit 0
|
||||
|
||||
```
|
||||
As you can see, the effect of these commands is to start the WiFi transmitter, give the base station the IP address `10.0.10.1`, start a DHCP server and finally start the Shairport Sync service.
|
||||
|
||||
@@ -195,7 +193,7 @@ From this point on, at least on the Raspberry Pi, if you reboot the machine, it
|
||||
|
||||
### Optimise startup time -- Raspberry Pi Specific
|
||||
|
||||
This is applicable to a Raspberry Pi only. Some of it may be applicable to other systems, but it has not been tested on them. There are quite a few services that are not necessary for this setup. Disabling them can shorten startup time. Running these commands disables them:
|
||||
This is applicable to a Raspberry Pi only. Some of it may be applicable to other systems, but it has not been tested on them. There are quite a few services that are not necessary for this setup. Disabling them can improve startup time. Running these commands disables them:
|
||||
|
||||
````
|
||||
sudo systemctl disable systemd-timesyncd.service
|
||||
|
||||
@@ -0,0 +1,178 @@
|
||||
# Configuration Flags
|
||||
When the application is being built, these flags determine what capabilities are included in Shairport Sync.
|
||||
For example, to include DSP capabilities – needed for a loudness filter – you would include the `--with-convolution` flag in the list of
|
||||
options at the `./configure ...` stage when building the application.
|
||||
|
||||
In the following sections, Shairport Sync's compilation configuration options are detailed by catgegory.
|
||||
|
||||
## AirPlay 2 or AirPlay
|
||||
|
||||
| Flag |
|
||||
| ---- |
|
||||
| `--with-airplay-2` |
|
||||
|
||||
AirPlay 2 is the current version of the AirPlay protocol. It offers multi-room operation and integration with the Home application. However, the Shairport Sync implementation is doesn't support iTunes on Windows, and its integration with the Home app and support for remote control is incomplete. Additionally, it requires a somewhat more powerful computer system (Raspberry Pi 2 equivalent or better) and a recent (2018 or later) version of a Debian-like Linux, Alpine Linux or FreeBSD. It has not been tested on other Linux distributions such as OpenWrt. Finally, AirPlay 2 can be lossy – in one mode of operation, audio is encoded in 256kbps AAC.
|
||||
|
||||
AirPlay (aka AirPlay 1) is an older version of the AirPlay protocol. If offers multi-room operation to iTunes on macOS or Windows, the Music app on macOS and some third-party computer applications such as [OwnTone](https://owntone.github.io/owntone-server/). It will run on lower powered machines, e.g. the original Raspberry Pi and many embedded devices. This version of AirPlay is lossless – audio is received in 44,100 frames per second 16-bit interleaved stereo ALAC format. It is compatible with a wider range of Linux distributions, back to around 2012. However, support for this version of AirPlay seems to be gradually waning. It does not offer multi-room operation to iOS, iPadOS or AppleTV and is incompatible with HomePod. It is not integrated with the Home app.
|
||||
|
||||
To build Shairport Sync for AirPlay 2, include the `--with-airplay-2` option in the `./configure ...` options. You will also have to include extra libraries. Omitting this option will cause Shairport Sync to be built for the older AirPlay protocol.
|
||||
|
||||
## Audio Output
|
||||
| Flags |
|
||||
| ----- |
|
||||
| `--with-alsa` |
|
||||
| `--with-sndio` |
|
||||
| `--with-pa` |
|
||||
| `--with-pw` |
|
||||
| `--with-ao` |
|
||||
| `--with-jack` |
|
||||
| `--with-soundio` |
|
||||
| `--with-stdout` |
|
||||
| `--with-pipe` |
|
||||
|
||||
Shairport Sync has a number of different "backends" that send audio to an onward destination, e.g. an audio subsystem for output to a Digital to Audio Converter and thence to loudspeakers, or to a pipe for further processing. You can use configuration options to include support for multiple backends at build time and select one of them at runtime.
|
||||
|
||||
Here are the audio backend configuration options:
|
||||
|
||||
- `--with-alsa` Output to the Advanced Linux Sound Architecture ([ALSA](https://www.alsa-project.org/wiki/Main_Page)) system. This is recommended for highest quality.
|
||||
- `--with-sndio` Output to the FreeBSD-native [sndio](https://sndio.org) system.
|
||||
- `--with-pa` Include the PulseAudio audio back end.
|
||||
- `--with-pw` Output to the [PipeWire](https://pipewire.org) system.
|
||||
- `--with-ao` Output to the [libao](https://xiph.org/ao/) system. No synchronisation.
|
||||
- `--with-jack` Output to the [Jack Audio](https://jackaudio.org) system.
|
||||
- `--with-soundio` Include an optional backend module for audio to be output through the [`soundio`](http://libsound.io) system. No synchronisation.
|
||||
- `--with-stdout` Include an optional backend module to enable raw audio to be output through standard output (`STDOUT`).
|
||||
- `--with-pipe` Include an optional backend module to enable raw audio to be output through a unix pipe.
|
||||
|
||||
### PulseAudio and PipeWire
|
||||
Many recent Linux distributions with a GUI -- "desktop" Linuxes -- use PulseAudio or PipeWire to handle sound. There are two things to consider with these sound servers:
|
||||
1. They may not always be available: a sound server generally becomes available when a user logs in via the GUI and disappears when the user logs out; it is not available when the system starts up and it is not available to non-GUI users. This means that Shairport Sync can not run as a daemon (see "Daemonisation" below) using a sound server unless the sound server is configured as a system-wide service.
|
||||
2. The fidelity of the audio is unknown: once audio is delivered to the sound server, it is unknown what happens to it as it is processed through PulseAudio to arrives eventually at the loudspeakers.
|
||||
|
||||
It should be noted that both PulseAudio and PipeWire provide a default ALSA pseudo device that enables ALSA-compatible programs to send audio. Shairport Sync can therefore use the ALSA backend with PulseAudio- or PipeWire-based systems.
|
||||
|
||||
## Audio Options
|
||||
| Flags |
|
||||
| ----- |
|
||||
| `--with-soxr` |
|
||||
| `--with-apple-alac` |
|
||||
| `--with-convolution` |
|
||||
|
||||
- `--with-soxr` Allows Shairport Sync to use [libsoxr](https://sourceforge.net/p/soxr/wiki/Home/)-based resampling for improved interpolation. Recommended.
|
||||
- `--with-apple-alac` Allows Shairport Sync to use the Apple ALAC Decoder. Requires [`libalac`](https://github.com/mikebrady/alac).
|
||||
- `--with-convolution` Includes a convolution filter that can be used to apply effects such as frequency and phase correction, and a loudness filter that compensates for the non-linearity of the human auditory system. Requires `libsndfile`.
|
||||
|
||||
## Metadata
|
||||
| Flags |
|
||||
| ----- |
|
||||
| `--with-metadata` |
|
||||
|
||||
Metadata such as track name, artist name, album name, cover art and more can be requested from the player and passed to other applications.
|
||||
|
||||
- `--with-metadata` Adds support for Shairport Sync to request metadata and to pipe it to a compatible application. See https://github.com/mikebrady/shairport-sync-metadata-reader for a sample metadata reader.
|
||||
|
||||
## Inter Process Communication
|
||||
|
||||
| Flags |
|
||||
| ----- |
|
||||
| `--with-mqtt-client` |
|
||||
| `--with-dbus-interface` |
|
||||
| `--with-dbus-test-client` |
|
||||
| `--with-mpris-interface` |
|
||||
| `--with-mpris-test-client` |
|
||||
|
||||
Shairport Sync offers three Inter Process Communication (IPC) interfaces: an [MQTT](https://mqtt.org) interface, an [MPRIS](https://specifications.freedesktop.org/mpris-spec/latest/)-like interface and a Shairport Sync specific "native" D-Bus interface loosely based on MPRIS.
|
||||
The options are as follows:
|
||||
|
||||
- `--with-mqtt-client` Includes a client for [MQTT](https://mqtt.org),
|
||||
- `--with-dbus-interface` Includes support for the native Shairport Sync D-Bus interface,
|
||||
- `--with-dbus-test-client` Compiles a D-Bus test client application,
|
||||
- `--with-mpris-interface` Includes support for a D-Bus interface conforming as far as possible with the MPRIS standard,
|
||||
- `--with-mpris-test-client` Compiles an MPRIS test client application.
|
||||
|
||||
D-Bus and MPRIS commands and properties can be viewed using utilities such as [D-Feet](https://wiki.gnome.org/Apps/DFeet).
|
||||
|
||||
##### MPRIS
|
||||
The MPRIS interface has to depart somewhat from full MPRIS compatibility due to logical differences between Shairport Sync and a full standalone audio player such as Rhythmbox. Basically there are some things that Shairport Sync itself can not control that a standalone player can control.
|
||||
A good example is volume control. MPRIS has a read-write `Volume` property, so the volume can be read or set. However, all Shairport Sync can do is *request* the player to change the volume control. This request may or may not be carried out, and it may or may not be done accurately. So, in Shairport Sync's MPRIS interface, `Volume` is a read-only property, and an extra command called `SetVolume` is provided.
|
||||
|
||||
## Other Configuration Options
|
||||
|
||||
### Configuration Files
|
||||
| Flags |
|
||||
| ----- |
|
||||
| `--sysconfdir=<directory>` |
|
||||
|
||||
Shairport Sync will look for a configuration file – `shairport-sync.conf` by default – when it starts up. By default, it will look in the directory specified by the `sysconfdir` configuration variable, which defaults to `/usr/local/etc`. This is normal in BSD Unixes, but is unusual in Linux. Hence, for Linux installations, you need to set the `sysconfdir` variable to `/etc` using the configuration setting `--sysconfdir=/etc`.
|
||||
|
||||
### Daemonisation
|
||||
| Flags |
|
||||
| ----- |
|
||||
| `--with-libdaemon` |
|
||||
| `--with-piddir=<pathname>` |
|
||||
|
||||
A [daemon](https://en.wikipedia.org/wiki/Daemon_(computing)) is a computer program that runs as a background process, rather than being under the direct control of an interactive user. Shairport Sync is designed to run as a daemon.
|
||||
FreeBSD and most recent Linux distributions can run an application as a daemon without special modifications. However, in certain older distributions and in special cases it may be necessary to enable Shairport Sync to daemonise itself. Use the `--with-libdaemon` configuration option:
|
||||
|
||||
- `--with-libdaemon` Includes a demonising library needed if you want Shairport Sync to demonise itself with the `-d` option. Not needed for `systemd`-based systems which demonise programs differently.
|
||||
- `--with-piddir=<pathname>` Specifies a pathname to a directory in which to write the PID file which is created when Shairport Sync daemonises itself and used to locate the deamon process to be killed with the `-k` command line option.
|
||||
|
||||
### Automatic Start
|
||||
| Flags |
|
||||
| ----- |
|
||||
| `--with-systemd` |
|
||||
| `--with-systemdsystemunitdir=<dir>` |
|
||||
| `--with-systemv` |
|
||||
| `--with-freebsd-service` |
|
||||
| `--with-sygwin-service` |
|
||||
|
||||
Daemon programs such as Shairport Sync need to be started automatically, so that the service they provide becomes available without further intervention. Typically this is done using startup scripts. Four options are provided – two for Linux, one for FreeBSD and one for CYGWIN. In Linux, the choice depends on whether [systemd](https://en.wikipedia.org/wiki/Systemd) is used or not. If `systemd` is installed, then the `--with-systemd` option is suggested. If not, the `--with-systemv` option is suggested.
|
||||
|
||||
- `--with-systemd` Includes a script to create a Shairport Sync service that can optionally launch automatically at startup on `systemd`-based Linuxes. Default is not to to install. Note: an associated special-purpose option allows you to specify where the `systemd` service file will be placed:
|
||||
- `--with-systemdsystemunitdir=<dir>` Specifies the directory for `systemd` service files.
|
||||
- `--with-systemv` Includes a script to create a Shairport Sync service that can optionally launch automatically at startup on System V based Linuxes. Default is not to to install.
|
||||
- `--with-freebsd-service` Includes a script to create a Shairport Sync service that can optionally launch automatically at startup on FreeBSD. Default is not to to install.
|
||||
- `--with-cygwin-service` Includes a script to create a Shairport Sync service that can optionally launch automatically at startup on CYGWIN. Default is not to to install.
|
||||
|
||||
### Cryptography
|
||||
| Flags |
|
||||
| ----- |
|
||||
| `--with-ssl=openssl` |
|
||||
| `--with-ssl=mbedtls` |
|
||||
| `--with-ssl=polarssl` |
|
||||
|
||||
AirPlay 2 operation requires the OpenSSL libraries, so the option `--with-ssl=openssl` is mandatory.
|
||||
|
||||
For classic Shairport Sync, the options are as follows:
|
||||
|
||||
- `--with-ssl=openssl` Uses the [OpenSSL](https://www.openssl.org) cryptography toolkit. This is mandatory for AirPlay 2 operation.
|
||||
- `--with-ssl=mbedtls` Uses the [Mbed TLS](https://github.com/Mbed-TLS/mbedtls) cryptography library. Only suitable for classic AirPlay operation – do not include it in an AirPlay 2 configuration.
|
||||
- `--with-ssl=polarssl` Uses the PolarSSL cryptography library. This is deprecated – PolarSSL has been replaced by Mbed TLS. Only suitable for classic AirPlay operation – do not include it in an AirPlay 2 configuration.
|
||||
|
||||
### Zeroconf/Bonjour
|
||||
| Flags |
|
||||
| ----- |
|
||||
| `--with-avahi` |
|
||||
| `--with-tinysvcmdns` |
|
||||
| `--with-external-mdns` |
|
||||
| `--with-dns_sd` |
|
||||
|
||||
AirPlay devices advertise their existence and status using [Zeroconf](http://www.zeroconf.org) (aka [Bonjour](https://en.wikipedia.org/wiki/Bonjour_(software))).
|
||||
|
||||
AirPlay 2 operation requires the [Avahi](https://www.avahi.org) libraries, so the option `--with-avahi` is mandatory.
|
||||
|
||||
For classic Shairport Sync, the options are as follows:
|
||||
|
||||
The Zeroconf-related options are as follows:
|
||||
- `--with-avahi` Chooses [Avahi](https://www.avahi.org)-based Zeroconf support. This is mandatory for AirPlay 2 operation.
|
||||
- `--with-tinysvcmdns` Chooses [tinysvcmdns](https://github.com/philippe44/TinySVCmDNS)-based Zeroconf support (deprecated).
|
||||
- `--with-external-mdns` Supports the use of ['avahi-publish-service'](https://linux.die.net/man/1/avahi-publish-service) or 'mDNSPublish' to advertise the service on Bonjour/ZeroConf.
|
||||
- `--with-dns_sd` Chooses `dns-sd` Zeroconf support.
|
||||
|
||||
### Miscellaneous
|
||||
| Flag | Significance |
|
||||
| -------------------- | ---- |
|
||||
| `--with-os=<OSType>` | Specifies the Operating System to target: One of `linux` (default), `freebsd`, `openbsd` or `darwin`. |
|
||||
| `--with-configfiles` | Installs configuration files (including a sample configuration file) during `make install`. |
|
||||
| `--with-pkg-config` | Specifies the use of `pkg-config` to find libraries. (Obselete for AirPlay 2. Special purpose use only.) |
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
Installing Shairport Sync into Cygwin
|
||||
Installing Shairport Sync into Cygwin [Airplay 2 Not Supported]
|
||||
====
|
||||
|
||||
This guide is based on installing onto a fresh installation of Cygwin 2.895 (64-bit installation) running in Windows 10
|
||||
|
||||
@@ -1,28 +1,3 @@
|
||||
Fedora Installation Guide
|
||||
-----
|
||||
Update everything...
|
||||
```
|
||||
# yum update
|
||||
```
|
||||
Install the toolchain and pre-requisites, if necessary:
|
||||
```
|
||||
# yum install make automake gcc gcc-c++ kernel-devel
|
||||
# yum install alsa-lib-devel autoconf automake avahi-devel libconfig-devel \
|
||||
libdaemon-devel openssl-devel popt-devel soxr-devel rpmbuild
|
||||
```
|
||||
Download the tarball from the "releases" tab on github or use `wget` and then use `rpmbuild`. Alternatively, download a tarball of the `master` branch, as in this example:
|
||||
```
|
||||
$ wget -O shairport-sync-master.tar.gz https://github.com/mikebrady/shairport-sync/archive/master.tar.gz
|
||||
$ rpmbuild -ta shairport-sync-master.tar.gz
|
||||
```
|
||||
The `-ta` means "build all from this tarball".
|
||||
# Fedora Installation Guide
|
||||
|
||||
The RPM will be built in a directory and will have a pathname like, for example, `~/rpmbuild/RPMS/i686/shairport-sync-2.6-1.fc22.i686.rpm` You should then install it with (for this example):
|
||||
```
|
||||
$ sudo rpm -i ~/rpmbuild/RPMS/i686/shairport-sync-2.6-1.fc22.i686.rpm
|
||||
```
|
||||
You may have to manually create the directory `/var/shairport-sync` beforehand for the installation to succeed. Having edited the configuration file `/etc/shairport-sync.conf` as appropriate (see [Configuring Shairport Sync](https://github.com/mikebrady/shairport-sync/blob/master/README.md#configuring-shairport-sync)), enable and start the service with:
|
||||
```
|
||||
# systemctl enable shairport-sync.service
|
||||
# systemctl start shairport-sync.service
|
||||
```
|
||||
For the present, this guide has been superseded by the general building guide at [BUILD.md](https://github.com/mikebrady/shairport-sync/blob/development/BUILD.md).
|
||||
|
||||
+2
-98
@@ -1,99 +1,3 @@
|
||||
Shairport Sync on FreeBSD using `sndio`
|
||||
----
|
||||
Shairport Sync runs natively on FreeBSD using the `sndio` back end, thanks to the work of [Tobias Kortkamp (t6)](https://github.com/t6).
|
||||
# Shairport Sync on FreeBSD
|
||||
|
||||
[`sndio`](http://www.sndio.org) is *"a small audio and MIDI framework part of the OpenBSD project and ported to FreeBSD, Linux and NetBSD"* developed by Alexandre Ratchov (see also [this paper](http://www.openbsd.org/papers/asiabsdcon2010_sndio.pdf) for more details).
|
||||
|
||||
This is an initial note about installing Shairport Sync on FreeBSD.
|
||||
|
||||
The build instructions here install back ends both for `sndio` and ALSA. ALSA is, or course, the Advanced Linux Sound Architecture, so it is not "native" to FreeBSD, but has been ported to some architectures under FreeBSD.
|
||||
|
||||
General
|
||||
----
|
||||
This build was done on a default build of `FreeBSD 11.0-RELEASE-p9`.
|
||||
|
||||
First, update everything:
|
||||
```
|
||||
# freebsd-update fetch
|
||||
# freebsd-update install
|
||||
```
|
||||
Next, install the `pkg` package manager and update its lists:
|
||||
|
||||
```
|
||||
# pkg
|
||||
# pkg update
|
||||
```
|
||||
|
||||
Subsystems
|
||||
----
|
||||
Install the Avahi subsystem. FYI, `avahi-app` is chosen because it doesn’t require X11. `nss_mdns` is included to allow FreeBSD to resolve mDNS-originated addresses – it's not actually needed by Shairport Sync. Thanks to [reidransom](https://gist.github.com/reidransom/6033227) for this.
|
||||
|
||||
```
|
||||
# pkg install avahi-app nss_mdns
|
||||
```
|
||||
Add these lines to `/etc/rc.conf`:
|
||||
```
|
||||
dbus_enable="YES"
|
||||
avahi_daemon_enable="YES"
|
||||
```
|
||||
Next, change the `hosts:` line in `/etc/nsswitch.conf` to
|
||||
```
|
||||
hosts: files dns mdns
|
||||
```
|
||||
Reboot for these changes to take effect.
|
||||
|
||||
Building
|
||||
----
|
||||
|
||||
Install the packages that are needed for Shairport Sync to be downloaded and built successfully:
|
||||
```
|
||||
# pkg install git autotools pkgconf popt libconfig openssl sndio alsa-utils
|
||||
```
|
||||
Add `alsa-utils` if you're wish to use ALSA. Omit `sndio` if you don't intend to use the `sndio` subsystem.
|
||||
|
||||
Now, download Shairport Sync from GitHub:
|
||||
```
|
||||
$ git clone https://github.com/mikebrady/shairport-sync.git
|
||||
$ cd shairport-sync
|
||||
```
|
||||
Next, configure the build and compile it:
|
||||
|
||||
```
|
||||
$ autoreconf -i -f
|
||||
$ ./configure --with-libdaemon --with-avahi --with-ssl=openssl --with-sndio --with-libdaemon --with-os=freebsd --with-freebsd-service
|
||||
$ make
|
||||
```
|
||||
Add `--with-alsa` if you wish to include the ALSA back end. Omit the `--with-sndio` if you don't want the `sndio` back end. Omit the `--with-freebsd-service` if you don't want to install a FreeBSD startup script, runtime folder and user and group -- see below for more details.
|
||||
|
||||
Installation
|
||||
----
|
||||
|
||||
Enter the superuser mode and do a `make install`:
|
||||
|
||||
```
|
||||
$ su
|
||||
# make install
|
||||
```
|
||||
|
||||
With the `./configure` options shown above, this will install the `shairport-sync` program along with a sample configuration file at `/usr/local/etc/shairport-sync.conf`. A service startup script will also be installed to launch Shairport Sync as a daemon. In addition, a `shairport-sync` user and group will be added and a directory will be created at `/var/run/shairport-sync` owned by the user `shairport-sync`. This will be used to hold the daemon's PID file.
|
||||
|
||||
Finally, edit `/usr/local/etc/shairport-sync.conf` to customise your installation, e.g. service name, etc. To make the `shairport-sync` daemon load at startup, add the following line to `/etc/rc.conf`:
|
||||
|
||||
```
|
||||
shairport_sync_enable="YES"
|
||||
```
|
||||
You can launch the service as superuser, or simply reboot the machine.
|
||||
|
||||
Using the `sndio` backend
|
||||
----
|
||||
|
||||
The `sndio` back end does not yet have a hardware volume control facility. You should set the volume to maximum before use, using, for example, the `mixer` command described below.
|
||||
|
||||
Setting Overall Volume
|
||||
----
|
||||
The `mixer` command can be used for setting the output device's volume settings. You may have to experiment to figure out which settings are appropriate.
|
||||
|
||||
```
|
||||
$ mixer vol 100 # sets overall volume
|
||||
```
|
||||
If you've installed `alsa-utils`, then `alsamixer` and friends will also be available.
|
||||
This guide has been superseded by the general building guide at [BUILD.md](https://github.com/mikebrady/shairport-sync/blob/development/BUILD.md).
|
||||
|
||||
-116
@@ -1,116 +0,0 @@
|
||||
Simple Installation Instructions
|
||||
==
|
||||
Here are simple instructions for building and installing Shairport Sync on a Raspberry Pi B, 2B, 3B, 3B+ or 4B. It is assumed that the Pi is running Raspbian Buster Lite – a GUI isn't needed, since Shairport Sync runs as a daemon program. For a more thorough treatment, please go to the [README.md](https://github.com/mikebrady/shairport-sync/blob/master/README.md#building-and-installing) page.
|
||||
|
||||
In the commands below, note the convention that a `#` prompt means you are in superuser mode and a `$` prompt means you are in a regular unprivileged user mode. You can use `sudo` *("SUperuser DO")* to temporarily promote yourself from user to superuser, if permitted. For example, if you want to execute `apt-get update` in superuser mode and you are in user mode, enter `sudo apt-get update`.
|
||||
|
||||
### Configure and Update
|
||||
Do the usual update and upgrade:
|
||||
```
|
||||
# apt-get update
|
||||
# apt-get upgrade
|
||||
```
|
||||
(Separately, if you haven't done so already, consider using the `raspi-config` tool to expand the file system to use the entire card.)
|
||||
|
||||
### Turn Off WiFi Power Management
|
||||
If you are using WiFi, you should turn off WiFi Power Management:
|
||||
```
|
||||
# iwconfig wlan0 power off
|
||||
```
|
||||
WiFi Power Management will put the WiFi system in low-power mode when the WiFi system is considered inactive, and in this mode it may not respond to events initiated from the network, such as AirPlay requests. Hence, WiFi Power Management should be turned off. (See [TROUBLESHOOTING.md](https://github.com/mikebrady/shairport-sync/blob/master/TROUBLESHOOTING.md#wifi-adapter-running-in-power-saving--low-power-mode) for more details.)
|
||||
|
||||
Reboot the Pi.
|
||||
|
||||
### Remove Old Copies
|
||||
Before you begin building Shairport Sync, it's best to search for and remove any existing copies of the application, called `shairport-sync`. Use the command `$ which shairport-sync` to find them. For example, if `shairport-sync` has been installed previously, this might happen:
|
||||
```
|
||||
$ which shairport-sync
|
||||
/usr/local/bin/shairport-sync
|
||||
```
|
||||
Remove it as follows:
|
||||
```
|
||||
# rm /usr/local/bin/shairport-sync
|
||||
```
|
||||
Do this until no more copies of `shairport-sync` are found.
|
||||
|
||||
### Remove Old Startup and Service Scripts
|
||||
You should also remove the startup script and service definition files `/etc/systemd/system/shairport-sync.service`, `/lib/systemd/system/shairport-sync.service`, `/etc/init.d/shairport-sync`, `/etc/dbus-1/system.d/shairport-sync-dbus.conf` and `/etc/dbus-1/system.d/shairport-sync-mpris.conf` if they exist – new ones will be installed if necessary.
|
||||
|
||||
### Reboot after Cleaning Up
|
||||
If you removed any installations of Shairport Sync or any of its startup script files in the last two steps, you should reboot.
|
||||
|
||||
### Build and Install
|
||||
Okay, now let's get the tools and sources for building and installing Shairport Sync.
|
||||
|
||||
First, install the packages needed by Shairport Sync:
|
||||
```
|
||||
# apt install --no-install-recommends build-essential git xmltoman autoconf automake libtool \
|
||||
libpopt-dev libconfig-dev libasound2-dev avahi-daemon libavahi-client-dev libssl-dev libsoxr-dev
|
||||
```
|
||||
Next, download Shairport Sync, configure it, compile and install it:
|
||||
```
|
||||
$ git clone https://github.com/mikebrady/shairport-sync.git
|
||||
$ cd shairport-sync
|
||||
$ autoreconf -fi
|
||||
$ ./configure --sysconfdir=/etc --with-alsa --with-soxr --with-avahi --with-ssl=openssl --with-systemd
|
||||
$ make
|
||||
$ sudo make install
|
||||
```
|
||||
By the way, the `autoreconf` step may take quite a while on a Raspberry Pi -- be patient!
|
||||
|
||||
### Configure
|
||||
If you don't set any configuration parameters, Shairport Sync will use the default `alsa` output device.
|
||||
|
||||
If you wish to configure Shairport Sync to use a specific output device (recommended), an experimental tool called [sps-alsa-explore](https://github.com/mikebrady/sps-alsa-explore) is available to search for all `alsa` hardware output devices that can be used by Shairport Sync. Here is the tool's output for a Raspberry Pi 4 running Buster:
|
||||
|
||||
```
|
||||
> Device: "hw:Headphones"
|
||||
Short Name: "hw:0"
|
||||
This device seems suitable for use with Shairport Sync.
|
||||
Possible mixers: "Headphone"
|
||||
Suggested rate and format:
|
||||
Rate Format
|
||||
44100 S16_LE
|
||||
|
||||
> Device: "hw:vc4hdmi0"
|
||||
Short Name: "hw:2"
|
||||
This device can not be accessed and so can not be checked.
|
||||
(Does it need to be configured or connected?)
|
||||
|
||||
> Device: "hw:vc4hdmi1"
|
||||
Short Name: "hw:3"
|
||||
This device can not be accessed and so can not be checked.
|
||||
(Does it need to be configured or connected?)
|
||||
|
||||
```
|
||||
|
||||
To use the "Headphones" device, here are the options for the Shairport Sync configuration file at `/etc/shairport-sync.conf`:
|
||||
```
|
||||
// Sample Configuration File for Shairport Sync on a Raspberry Pi using the built-in audio DAC
|
||||
general =
|
||||
{
|
||||
volume_range_db = 60;
|
||||
};
|
||||
|
||||
alsa =
|
||||
{
|
||||
output_device = "hw:Headphones";
|
||||
mixer_control_name = "Headphone";
|
||||
};
|
||||
|
||||
```
|
||||
The `volume_range_db = 60;` setting makes Shairport Sync use only the usable part of the built-in audio card mixer's attenuation range.
|
||||
|
||||
(BTW, Shairport Sync will choose the suggested speed and format automatically.)
|
||||
|
||||
The next step is to enable Shairport Sync to start automatically on boot up:
|
||||
```
|
||||
# systemctl enable shairport-sync
|
||||
```
|
||||
Finally, either reboot the Pi or start the `shairport-sync` service:
|
||||
```
|
||||
# systemctl start shairport-sync
|
||||
```
|
||||
The Shairport Sync AirPlay service should now appear on the network with a service name made from the Pi's hostname with the first letter capitalised, e.g. hostname `raspberrypi` gives a service name `Raspberrypi`. You can change the service name and set a password in the configuration file.
|
||||
|
||||
Connect and enjoy...
|
||||
@@ -0,0 +1,3 @@
|
||||
# More Information
|
||||
|
||||
Information in this document has been updated and moved to [ADVANCED TOPICS](https://github.com/mikebrady/shairport-sync/blob/development/ADVANCED%20TOPICS/README.md).
|
||||
@@ -1,3 +1,21 @@
|
||||
MQTT in Shairport Sync
|
||||
====
|
||||
To enable Shairport Sync to act as an MQTT publisher, you need to:
|
||||
1. Install the mosquitto library:
|
||||
```
|
||||
# apt install libmosquitto-dev
|
||||
```
|
||||
2. Add the configuration flag `--with-mqtt-client` to the list of parameters to the `./configure...` command, for example:
|
||||
```
|
||||
$ ./configure --with-mqtt-client --sysconfdir=/etc --with-alsa --with-avahi --with-ssl=openssl --with-systemd
|
||||
```
|
||||
|
||||
If Shairport Sync has MQTT support, it will have the string `mqtt` in its configuration string. For example:
|
||||
```
|
||||
$ shairport-sync -V
|
||||
3.3.8-OpenSSL-Avahi-ALSA-metadata-mqtt-sysconfdir:/etc
|
||||
```
|
||||
|
||||
Setting Up MQTT Publishing
|
||||
====
|
||||
|
||||
@@ -77,31 +95,38 @@ The first 4-character code, called the `type`, is either:
|
||||
* For `ssnc` metadata, the second 4-character code is used to distinguish the messages. Cover art, coming from the source, 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`. Progress information, similarly, is not tagged like other source-originated metadata, so it is sent as an `ssnc` type with the code `prgr`.
|
||||
|
||||
Here are some of the `core` codes commonly passed from the source:
|
||||
* `asar` -- "artist"
|
||||
* `asal` -- "album"
|
||||
* `minm` -- "title"
|
||||
* `asgn` -- "genre"
|
||||
* `asfm` -- "format"
|
||||
* `asal` -- "songalbum'
|
||||
* `pvol` -- "volume"
|
||||
* `clip` -- "client_ip"
|
||||
* `asal` -- album
|
||||
* `asar` -- artist
|
||||
* `ascp` -- composer
|
||||
* `asgn` -- genre
|
||||
* `astm` -- song time
|
||||
* `caps` -- play status (stopped, paused, playing)
|
||||
* `minm` -- title
|
||||
* `mper` -- track persistent id
|
||||
|
||||
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.
|
||||
* `clip` -- the payload is the IP number of the client, i.e. the sender of audio. Can be an IPv4 or an IPv6 number.
|
||||
* `acre` -- Active Remote
|
||||
* `cdid` -- Client advertised Device ID
|
||||
* `clip` -- the payload is the IP address of the client, i.e. the sender of audio. Can be an IPv4 or an IPv6 address.
|
||||
* `cmac` -- Client advertised MAC address
|
||||
* `cmod` -- Client advertised model ("iPhone14,2")
|
||||
* `daid` -- DACP ID
|
||||
* `dapo` -- DACP Port
|
||||
* `mden` -- a sequence of metadata has ended. The RTP timestamp associated with the metadata sequence is included as data, if available.
|
||||
* `mdst` -- a sequence of metadata is about to start. The RTP timestamp associated with the metadata sequence is included as data, if available.
|
||||
* `pbeg` -- play stream begin. No arguments
|
||||
* `pcen` -- a picture has been sent. The RTP timestamp associated with it is included as data, if available.
|
||||
* `pcst` -- a picture is about to be sent. The RTP timestamp associated with it is included as data, if available.
|
||||
* `pend` -- play stream end. No arguments
|
||||
* `pfls` -- play stream flush. No arguments
|
||||
* `prsm` -- play stream resume. No arguments
|
||||
* `pvol` -- play volume. The volume is sent as a string -- "airplay_volume,volume,lowest_volume,highest_volume", where "volume", "lowest_volume" and "highest_volume" are given in dB. The "airplay_volume" is what's sent by the source (e.g. iTunes) 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. If the volume setting is being ignored by Shairport Sync itself, the volume, lowest_volume and highest_volume values are zero.
|
||||
* `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.
|
||||
* `mdst` -- a sequence of metadata is about to start. The RTP timestamp associated with the metadata sequence is included as data, if available.
|
||||
* `mden` -- a sequence of metadata has ended. The RTP timestamp associated with the metadata sequence is included as data, if available.
|
||||
* `pcst` -- a picture is about to be sent. The RTP timestamp associated with it is included as data, if available.
|
||||
* `pcen` -- a picture has been sent. The RTP timestamp associated with it is included as data, if available.
|
||||
* `snam` -- a device e.g. "Joe's iPhone" has started a play session. Specifically, it's the "X-Apple-Client-Name" string.
|
||||
* `pvol` -- play volume. The volume is sent as a string -- "airplay_volume,volume,lowest_volume,highest_volume", where "volume", "lowest_volume" and "highest_volume" are given in dB. The "airplay_volume" is what's sent by the source (e.g. iTunes) 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. If the volume setting is being ignored by Shairport Sync itself, the volume, lowest_volume and highest_volume values are zero.
|
||||
* `snam` -- a device e.g. "Joe's iPhone" has started a play session. Specifically, it's the "X-Apple-Client-Name" string for AP1, or direct from the configuration Plist for AP2.
|
||||
* `snua` -- a "user agent" e.g. "iTunes/12..." has started a play session. Specifically, it's the "User-Agent" string.
|
||||
* `stal` -- this is an error message meaning that reception of a large piece of metadata, usually a large picture, has stalled; bad things may happen.
|
||||
* `svip` -- the payload is the IP address of the server, i.e. shairport-sync. Can be an IPv4 or an IPv6 address.
|
||||
|
||||
|
||||
Parsed Messages
|
||||
@@ -109,14 +134,21 @@ Parsed Messages
|
||||
|
||||
The MQTT service can parse the above raw messages into a subset of human-readable topics that include,
|
||||
|
||||
* `active_remote_id` -- Active Remote ID
|
||||
* `artist` -- text of artist name
|
||||
* `album` -- text of album name
|
||||
* `title` -- text of song title
|
||||
* `genre` -- text of genre
|
||||
* `format` -- ??
|
||||
* `songalbum` --
|
||||
* `volume` -- The volume is sent as a string -- "airplay_volume,volume,lowest_volume,highest_volume", where "volume", "lowest_volume" and "highest_volume" are given in dB. (see above)
|
||||
* `client_ip` -- IP address of the connected client
|
||||
* `client_device_id` -- Client advertised Device ID
|
||||
* `client_mac_address` -- Client advertised MAC address
|
||||
* `client_model` -- Client advertised model ("iPhone14,2")
|
||||
* `client_name` -- Client advertised name ("Joe's iPhone")
|
||||
* `dacp_id` -- DACP ID
|
||||
* `format` -- ??
|
||||
* `genre` -- text of genre
|
||||
* `server_ip` -- IP address of shairport-sync that the client is connected to
|
||||
* `songalbum` --
|
||||
* `title` -- text of song title
|
||||
* `volume` -- The volume is sent as a string -- "airplay_volume,volume,lowest_volume,highest_volume", where "volume", "lowest_volume" and "highest_volume" are given in dB. (see above)
|
||||
|
||||
and empty messages at the following topics are published.
|
||||
|
||||
@@ -156,7 +188,7 @@ binary_sensor:
|
||||
off_delay: 300
|
||||
```
|
||||
|
||||
Below parsed data is saved into the Home Assistant database as sensor data. Please not the conversion of the volume from dB to percentage.
|
||||
Below parsed data is saved into the Home Assistant database as sensor data. Please note the conversion of the volume from dB to percentage.
|
||||
|
||||
```yml
|
||||
sensor:
|
||||
|
||||
+163
-49
@@ -1,15 +1,28 @@
|
||||
SUBDIRS = man
|
||||
ARFLAGS = cr
|
||||
|
||||
lib_pair_ap_a_CFLAGS = -Wall -g -DCONFIG_GCRYPT -pthread
|
||||
lib_tinyhttp_a_CFLAGS = -pthread
|
||||
lib_dbus_interface_a_CFLAGS = -pthread
|
||||
lib_mpris_interface_a_CFLAGS = -pthread
|
||||
|
||||
|
||||
bin_PROGRAMS = shairport-sync
|
||||
|
||||
BUILT_SOURCES =
|
||||
noinst_HEADERS =
|
||||
CLEANFILES =
|
||||
shairport_sync_LDADD =
|
||||
noinst_LIBRARIES =
|
||||
|
||||
# See below for the flags for the test client program
|
||||
|
||||
shairport_sync_SOURCES = shairport.c rtsp.c mdns.c common.c rtp.c player.c alac.c audio.c loudness.c activity_monitor.c
|
||||
|
||||
if BUILD_FOR_DARWIN
|
||||
AM_CXXFLAGS = -I/usr/local/include -Wno-multichar -Wall -Wextra -Wno-deprecated-declarations -pthread -DSYSCONFDIR=\"$(sysconfdir)\"
|
||||
AM_CFLAGS = -Wno-multichar -Wall -Wextra -Wno-deprecated-declarations -pthread -DSYSCONFDIR=\"$(sysconfdir)\"
|
||||
else
|
||||
if BUILD_FOR_FREEBSD
|
||||
AM_CXXFLAGS = -I/usr/local/include -Wno-multichar -Wall -Wextra -pthread -DSYSCONFDIR=\"$(sysconfdir)\"
|
||||
AM_CFLAGS = -Wno-multichar -Wall -Wextra -pthread -DSYSCONFDIR=\"$(sysconfdir)\"
|
||||
@@ -22,6 +35,19 @@ else
|
||||
AM_CFLAGS = -fno-common -Wno-multichar -Wall -Wextra -Wno-clobbered -Wno-psabi -pthread -DSYSCONFDIR=\"$(sysconfdir)\"
|
||||
endif
|
||||
endif
|
||||
endif
|
||||
|
||||
# include information generated by 'git describe --dirty' if requested
|
||||
if USE_GIT_VERSION
|
||||
common.c: gitversion.h
|
||||
gitversion.h: .git/index
|
||||
printf "// Do not edit!\n" > gitversion.h
|
||||
printf "// This file is automatically generated by 'git describe --dirty', if available.\n" >> gitversion.h
|
||||
printf " char git_version_string[] = \"" >> gitversion.h
|
||||
git describe --dirty | tr -d '[[:space:]]' >> gitversion.h
|
||||
printf "\";\n" >> gitversion.h
|
||||
CLEANFILES += gitversion.h
|
||||
endif
|
||||
|
||||
if USE_APPLE_ALAC
|
||||
shairport_sync_SOURCES += apple_alac.cpp
|
||||
@@ -80,6 +106,10 @@ if USE_PA
|
||||
shairport_sync_SOURCES += audio_pa.c
|
||||
endif
|
||||
|
||||
if USE_PW
|
||||
shairport_sync_SOURCES += audio_pw.c
|
||||
endif
|
||||
|
||||
if USE_CONVOLUTION
|
||||
shairport_sync_SOURCES += FFTConvolver/AudioFFT.cpp FFTConvolver/FFTConvolver.cpp FFTConvolver/Utilities.cpp FFTConvolver/convolver.cpp
|
||||
AM_CXXFLAGS += -std=c++11
|
||||
@@ -90,36 +120,54 @@ shairport_sync_SOURCES += mdns_dns_sd.c
|
||||
endif
|
||||
|
||||
if USE_METADATA_HUB
|
||||
shairport_sync_SOURCES += metadata_hub.c
|
||||
shairport_sync_SOURCES += metadata_hub.c
|
||||
endif
|
||||
|
||||
if USE_MQTT
|
||||
shairport_sync_SOURCES += mqtt.c
|
||||
shairport_sync_SOURCES += mqtt.c
|
||||
endif
|
||||
|
||||
if USE_DACP_CLIENT
|
||||
shairport_sync_SOURCES += dacp.c tinyhttp/chunk.c tinyhttp/header.c tinyhttp/http.c
|
||||
shairport_sync_SOURCES += dacp.c
|
||||
shairport_sync_LDADD += lib_tinyhttp.a
|
||||
noinst_LIBRARIES += lib_tinyhttp.a
|
||||
lib_tinyhttp_a_SOURCES = tinyhttp/chunk.c tinyhttp/header.c tinyhttp/http.c
|
||||
endif
|
||||
|
||||
BUILT_SOURCES =
|
||||
noinst_HEADERS =
|
||||
CLEANFILES =
|
||||
shairport_sync_LDADD =
|
||||
noinst_LIBRARIES =
|
||||
if USE_AIRPLAY_2
|
||||
shairport_sync_SOURCES += ptp-utilities.c plist_xml_strings.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
|
||||
plist_xml_strings.h: plists/get_info_response.xml
|
||||
printf "// Do not edit!\n" > plist_xml_strings.h
|
||||
printf "// This file is automatically generated from files in the plists folder.\n\n" >> plist_xml_strings.h
|
||||
xxd -i $(top_srcdir)/plists/get_info_response.xml - | sed -e 's/[^ ]*plists_get_info_response_xml/plists_get_info_response_xml/g' >> plist_xml_strings.h
|
||||
printf "\n" >> plist_xml_strings.h
|
||||
plist_xml_strings.c: plist_xml_strings.h
|
||||
touch plist_xml_strings.c
|
||||
|
||||
BUILT_SOURCES += plist_xml_strings.c plist_xml_strings.h
|
||||
noinst_HEADERS += $(BUILT_SOURCES)
|
||||
# Correctly clean the generated headers, but keep the xml description
|
||||
CLEANFILES += $(BUILT_SOURCES)
|
||||
endif
|
||||
|
||||
if USE_DBUS
|
||||
shairport_sync_LDADD += lib_dbus_interface.a
|
||||
noinst_LIBRARIES += lib_dbus_interface.a
|
||||
lib_dbus_interface_a_SOURCES = dbus-interface.c
|
||||
shairport_sync_SOURCES += dbus-service.c
|
||||
BUILT_SOURCES += dbus-interface.h dbus-interface.c
|
||||
BUILT_SOURCES += dbus-interface.h dbus-interface.c
|
||||
# We don't want to install this header
|
||||
noinst_HEADERS += $(BUILT_SOURCES)
|
||||
# Correctly clean the generated headers, but keep the xml description
|
||||
CLEANFILES += $(BUILT_SOURCES)
|
||||
|
||||
|
||||
|
||||
dbus-interface.c: org.gnome.ShairportSync.xml
|
||||
gdbus-codegen --interface-prefix org.gnome --generate-c-code dbus-interface org.gnome.ShairportSync.xml
|
||||
gdbus-codegen --interface-prefix org.gnome --generate-c-code dbus-interface $(top_srcdir)/org.gnome.ShairportSync.xml
|
||||
dbus-interface.h: dbus-interface.c
|
||||
touch dbus-interface.h
|
||||
endif
|
||||
@@ -136,7 +184,7 @@ noinst_HEADERS += $(BUILT_SOURCES)
|
||||
CLEANFILES += $(BUILT_SOURCES)
|
||||
|
||||
mpris-interface.c: org.mpris.MediaPlayer2.xml
|
||||
gdbus-codegen --interface-prefix org.mpris --generate-c-code mpris-interface org.mpris.MediaPlayer2.xml
|
||||
gdbus-codegen --interface-prefix org.mpris --generate-c-code mpris-interface $(top_srcdir)/org.mpris.MediaPlayer2.xml
|
||||
mpris-interface.h: mpris-interface.c
|
||||
touch mpris-interface.h
|
||||
endif
|
||||
@@ -157,54 +205,120 @@ shairport_sync_mpris_test_client_SOURCES = shairport-sync-mpris-test-client.c
|
||||
shairport_sync_mpris_test_client_LDADD = lib_mpris_interface.a
|
||||
endif
|
||||
|
||||
install-exec-hook:
|
||||
if BUILD_FOR_LINUX
|
||||
DBUS_POLICY_DIR=$(DESTDIR)/etc/dbus-1/system.d
|
||||
else
|
||||
DBUS_POLICY_DIR=$(DESTDIR)$(sysconfdir)/dbus-1/system.d
|
||||
endif
|
||||
if INSTALL_CONFIG_FILES
|
||||
[ -e $(DESTDIR)$(sysconfdir) ] || mkdir $(DESTDIR)$(sysconfdir)
|
||||
cp scripts/shairport-sync.conf $(DESTDIR)$(sysconfdir)/shairport-sync.conf.sample
|
||||
[ -f $(DESTDIR)$(sysconfdir)/shairport-sync.conf ] || cp scripts/shairport-sync.conf $(DESTDIR)$(sysconfdir)/shairport-sync.conf
|
||||
|
||||
CONFIG_FILE_INSTALL_TARGET = config-file-install-local
|
||||
$(CONFIG_FILE_INSTALL_TARGET): scripts/shairport-sync.conf
|
||||
install -d $(DESTDIR)$(sysconfdir)
|
||||
install -m 0644 $(top_srcdir)/scripts/shairport-sync.conf $(DESTDIR)$(sysconfdir)/shairport-sync.conf.sample
|
||||
[ -f $(DESTDIR)$(sysconfdir)/shairport-sync.conf ] || cp $(top_srcdir)/scripts/shairport-sync.conf $(DESTDIR)$(sysconfdir)/shairport-sync.conf
|
||||
|
||||
if BUILD_FOR_LINUX
|
||||
DBUS_POLICY_DIR = /etc/dbus-1/system.d
|
||||
else
|
||||
DBUS_POLICY_DIR = $(sysconfdir)/dbus-1/system.d
|
||||
endif
|
||||
|
||||
if USE_DBUS
|
||||
[ -e $(DBUS_POLICY_DIR) ] || mkdir -p $(DBUS_POLICY_DIR)
|
||||
|
||||
if INSTALL_CYGWIN_SERVICE
|
||||
cp scripts/shairport-sync-dbus-policy-cygwin.conf $(DBUS_POLICY_DIR)/shairport-sync-dbus.conf
|
||||
DBUS_POLICY_FILE = scripts/shairport-sync-dbus-policy-cygwin.conf
|
||||
else
|
||||
cp scripts/shairport-sync-dbus-policy.conf $(DBUS_POLICY_DIR)/shairport-sync-dbus.conf
|
||||
endif
|
||||
endif
|
||||
DBUS_POLICY_FILE = scripts/shairport-sync-dbus-policy.conf
|
||||
endif # INSTALL_CYGWIN_SERVICE
|
||||
|
||||
DBUS_POLICY_INSTALL_TARGET = dbus-policy-install-local
|
||||
|
||||
$(DBUS_POLICY_INSTALL_TARGET): $(DBUS_POLICY_FILE)
|
||||
install -d $(DESTDIR)$(DBUS_POLICY_DIR)
|
||||
install -m 0644 $(top_srcdir)/$(DBUS_POLICY_FILE) $(DESTDIR)$(DBUS_POLICY_DIR)/shairport-sync-dbus.conf
|
||||
|
||||
endif # USE_DBUS
|
||||
|
||||
if USE_MPRIS
|
||||
[ -e $(DBUS_POLICY_DIR) ] || mkdir -p $(DBUS_POLICY_DIR)
|
||||
|
||||
if INSTALL_CYGWIN_SERVICE
|
||||
cp scripts/shairport-sync-mpris-policy-cygwin.conf $(DBUS_POLICY_DIR)/shairport-sync-mpris.conf
|
||||
MPRIS_POLICY_FILE = scripts/shairport-sync-mpris-policy-cygwin.conf
|
||||
else
|
||||
cp scripts/shairport-sync-mpris-policy.conf $(DBUS_POLICY_DIR)/shairport-sync-mpris.conf
|
||||
endif
|
||||
endif
|
||||
endif
|
||||
MPRIS_POLICY_FILE = scripts/shairport-sync-mpris-policy.conf
|
||||
endif # INSTALL_CYGWIN_SERVICE
|
||||
|
||||
MPRIS_POLICY_INSTALL_TARGET = mpris-policy-install-local
|
||||
|
||||
$(MPRIS_POLICY_INSTALL_TARGET): $(MPRIS_POLICY_FILE)
|
||||
install -d $(DESTDIR)$(DBUS_POLICY_DIR)
|
||||
install -m 0644 $(top_srcdir)/$(MPRIS_POLICY_FILE) $(DESTDIR)$(DBUS_POLICY_DIR)/shairport-sync-mpris.conf
|
||||
|
||||
endif # USE_MPRIS
|
||||
|
||||
endif # INSTALL_CONFIG_FILES
|
||||
|
||||
INSTALL_GROUP_TARGET = install-group-local
|
||||
|
||||
$(INSTALL_GROUP_TARGET):
|
||||
getent group shairport-sync &>/dev/null || groupadd -r shairport-sync >/dev/null
|
||||
|
||||
INSTALL_USER_TARGET = install-user-local
|
||||
|
||||
$(INSTALL_USER_TARGET): $(INSTALL_GROUP_TARGET)
|
||||
getent passwd shairport-sync &> /dev/null || useradd -r -M -g shairport-sync -s /usr/sbin/nologin -G audio shairport-sync >/dev/null
|
||||
|
||||
if INSTALL_SYSTEMV
|
||||
getent group shairport-sync &>/dev/null || groupadd -r shairport-sync >/dev/null
|
||||
getent passwd shairport-sync &> /dev/null || useradd -r -M -g shairport-sync -s /usr/bin/nologin -G audio shairport-sync >/dev/null
|
||||
[ -e $(DESTDIR)$(sysconfdir)/init.d ] || mkdir -p $(DESTDIR)$(sysconfdir)/init.d
|
||||
[ -f $(DESTDIR)$(sysconfdir)/init.d/shairport-sync ] || cp scripts/shairport-sync $(DESTDIR)$(sysconfdir)/init.d/
|
||||
endif
|
||||
|
||||
INSTALL_SYSTEMV_TARGET = install-systemv-local
|
||||
|
||||
$(INSTALL_SYSTEMV_TARGET): scripts/shairport-sync $(INSTALL_USER_TARGET)
|
||||
install -d $(DESTDIR)$(sysconfdir)/init.d
|
||||
[ -e $(DESTDIR)$(sysconfdir)/init.d/shairport-sync ] || install -m 0644 $(top_srcdir)/scripts/shairport-sync $(DESTDIR)$(sysconfdir)/init.d
|
||||
|
||||
endif # INSTALL_SYSTEMV
|
||||
|
||||
if INSTALL_SYSTEMD
|
||||
getent group shairport-sync &>/dev/null || groupadd -r shairport-sync >/dev/null
|
||||
getent passwd shairport-sync &> /dev/null || useradd -r -M -g shairport-sync -s /usr/bin/nologin -G audio shairport-sync >/dev/null
|
||||
[ -e $(DESTDIR)$(systemdsystemunitdir) ] || mkdir -p $(DESTDIR)$(systemdsystemunitdir)
|
||||
[ -f $(DESTDIR)$(systemdsystemunitdir)/shairport-sync.service ] || cp scripts/shairport-sync.service $(DESTDIR)$(systemdsystemunitdir)
|
||||
endif
|
||||
|
||||
if USE_AVAHI
|
||||
SYSTEMD_SERVICE = shairport-sync.service-avahi
|
||||
else
|
||||
SYSTEMD_SERVICE = shairport-sync.service
|
||||
endif # USE_AVAHI
|
||||
|
||||
INSTALL_SYSTEMD_TARGET = install-systemd-local
|
||||
|
||||
$(INSTALL_SYSTEMD_TARGET): scripts/$(SYSTEMD_SERVICE) $(INSTALL_USER_TARGET)
|
||||
install -d $(DESTDIR)$(systemdsystemunitdir)
|
||||
[ -e $(DESTDIR)$(systemdsystemunitdir)/shairport-sync.service ] || install -m 0644 $(top_srcdir)/scripts/$(SYSTEMD_SERVICE) $(DESTDIR)$(systemdsystemunitdir)/shairport-sync.service
|
||||
|
||||
endif # INSTALL_SYSTEMD
|
||||
|
||||
if INSTALL_FREEBSD_SERVICE
|
||||
# Choose a uid and gid of 801 completely arbitrarity, except that it should be below 1000. FreeBSD doesn't seem to allow you to say "an ID in the range of..."
|
||||
|
||||
# Choose a uid and gid of 801 completely arbitrarity, except that it should be below 1000. FreeBSD doesn't seem to allow you to say "an ID in the range of..."
|
||||
install-freebsd-user-local:
|
||||
pw showgroup shairport-sync > /dev/null 2>&1 || pw addgroup -n shairport-sync -g 801 > /dev/null 2>&1
|
||||
pw showuser shairport-sync > /dev/null 2>&1 || pw adduser -c "shairport-sync unprivileged user" -n shairport-sync -u 801 -s /usr/sbin/nologin -d /nonexistent > /dev/null 2>&1
|
||||
[ -e /var/run/shairport-sync ] || mkdir -p /var/run/shairport-sync
|
||||
chown shairport-sync:shairport-sync /var/run/shairport-sync
|
||||
[ -f /usr/local/etc/rc.d/shairport_sync ] || cp scripts/shairport-sync.freebsd /usr/local/etc/rc.d/shairport_sync
|
||||
chmod 555 /usr/local/etc/rc.d/shairport_sync
|
||||
endif
|
||||
|
||||
INSTALL_FREEBSD_TARGET = install-freebsd-local
|
||||
|
||||
$(INSTALL_FREEBSD_TARGET): scripts/shairport-sync.freebsd install-freebsd-user-local
|
||||
install -d -o shairport-sync -g shairport-sync $(DESTDIR)/var/run/shairport-sync
|
||||
install -d $(DESTDIR)/usr/local/etc/rc.d/
|
||||
install -m 0555 $(top_srcdir)/scripts/shairport-sync.freebsd $(DESTDIR)/usr/local/etc/rc.d/shairport_sync
|
||||
|
||||
endif # INSTALL_FREEBSD_SERVICE
|
||||
|
||||
if INSTALL_CYGWIN_SERVICE
|
||||
cp scripts/shairport-sync-config /usr/local/bin
|
||||
endif
|
||||
|
||||
INSTALL_CYGWIN_TARGET = install-cygwin-local
|
||||
|
||||
$(INSTALL_CYGWIN_TARGET): scripts/shairport-sync-config
|
||||
install -d $(DESTDIR)/usr/local/bin
|
||||
[ -e $(DESTDIR)/usr/local/bin/shairport-sync-config ] || install -m 0755 $(top_srcdir)/scripts/shairport-sync-config $(DESTDIR)/usr/local/bin/
|
||||
|
||||
endif # INSTALL_CYGWIN_SERVICE
|
||||
|
||||
install-exec-hook: $(CONFIG_FILE_INSTALL_TARGET) \
|
||||
$(DBUS_POLICY_INSTALL_TARGET) \
|
||||
$(MPRIS_POLICY_INSTALL_TARGET) \
|
||||
$(INSTALL_SYSTEMV_TARGET) \
|
||||
$(INSTALL_SYSTEMD_TARGET) \
|
||||
$(INSTALL_FREEBSD_TARGET) \
|
||||
$(INSTALL_CYGWIN_TARGET)
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
Shairport Sync on OpenBSD
|
||||
Shairport Sync on OpenBSD [AirPlay 2 Not Supported]
|
||||
----
|
||||
This is a note about installing Shairport Sync on OpenBSD. Shairport Sync compiles and runs natively on OpenBSD using the `sndio` back end. For general information on `sndio` please follow [this link](http://www.sndio.org).
|
||||
|
||||
|
||||
@@ -1,657 +1,71 @@
|
||||
# Shairport Sync
|
||||
Shairport Sync is an [AirPlay](https://www.pocket-lint.com/speakers/news/apple/144646-apple-airplay-2-vs-airplay-what-s-the-difference) audio player for Linux and FreeBSD. It plays audio streamed from Apple devices and from AirPlay sources such as [OwnTone](https://github.com/owntone/owntone-server) (formerly `forked-daapd`).
|
||||
|
||||
Shairport Sync
|
||||
=============
|
||||
Shairport Sync is an AirPlay 1 audio player. Switch to the `development` branch for a version with limited AirPlay 2 functionality. Shairport Sync plays audio streamed from iTunes, iOS, Apple TV and macOS devices and AirPlay sources such as Quicktime Player and [OwnTone](https://github.com/owntone/owntone-server#owntone-previously-forked-daapd), among others.
|
||||
Shairport Sync can be built as an AirPlay 2 player (with [some limitations](https://github.com/mikebrady/shairport-sync/blob/development/AIRPLAY2.md#features-and-limitations)) or as "classic" Shairport Sync – a player for the older, but still supported, AirPlay (aka "AirPlay 1") protocol.
|
||||
|
||||
Audio played by a Shairport Sync-powered device stays synchronised with the source and hence with similar devices playing the same source. In this way, synchronised multi-room audio is possible for players that support it, such as iTunes and the macOS Music app.
|
||||
Metadata such as artist information and cover art can be requested and provided to other applications. Shairport Sync can interface with other applications through MQTT, an MPRIS-like interface and D-Bus.
|
||||
|
||||
Shairport Sync runs on Linux, FreeBSD and OpenBSD. It does not support AirPlay video or photo streaming.
|
||||
Shairport Sync does not support AirPlay video or photo streaming.
|
||||
|
||||
More Information
|
||||
----------
|
||||
Shairport Sync offers *full audio synchronisation*, a feature of AirPlay that previous implementations do not provide. Full audio synchronisation means that audio is played on the output device at exactly the time specified by the audio source. To accomplish this, Shairport Sync needs access to audio systems – such as `alsa` on Linux and `sndio` on FreeBSD – that provide very accurate timing information about output devices. Shairport Sync must have direct access to the output device used, which must be a real sound card capable of working with 44,100, 88,200 or 176,400 samples per second, interleaved PCM stereo of 8, 16, 24 or 32 bits. The default is 44,100 samples per second / 16 bits (you'll get a message in the logfile if there's a problem).
|
||||
# Quick Start
|
||||
* A building guide is available [here](BUILD.md).
|
||||
* A Docker image is available on the [Docker Hub](https://hub.docker.com/r/mikebrady/shairport-sync).
|
||||
* Next Steps and Advanced Topics are [here](ADVANCED%20TOPICS/README.md).
|
||||
* Runtime settings are documented [here](scripts/shairport-sync.conf).
|
||||
* Build configuration options are detailed in [CONFIGURATION FLAGS.md](CONFIGURATION%20FLAGS.md).
|
||||
|
||||
Alternatively, Shairport Sync works well with PulseAudio, a widely used sound server found on many desktop Linuxes. While the timing information is not as accurate as that of `alsa` or `sndio`, it is often impractical to remove or disable PulseAudio. In that case, the `pa` backend can be used. An older backend for PulseAudio called `pulse` does not support synchronisation and is deprecated.
|
||||
# Features
|
||||
* Outputs AirPlay audio to [ALSA](https://www.alsa-project.org/wiki/Main_Page), [sndio](http://www.sndio.org), [PulseAudio](https://www.freedesktop.org/wiki/Software/PulseAudio/), [Jack Audio](http://jackaudio.org), to a unix pipe or to `STDOUT`. It also has experimental support for [PipeWire](https://pipewire.org) and limited support for [libao](https://xiph.org/ao/) and for [libsoundio](http://libsound.io).
|
||||
* 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 including access to metadata and artwork, and limited remote control.
|
||||
* 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.
|
||||
* 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.
|
||||
* Support for the [Apple ALAC decoder](https://macosforge.github.io/alac/) (library available [here](https://github.com/mikebrady/alac)).
|
||||
* Output bit depths of 8, 16, 24 and 32 bits, rather than the standard 16 bits.
|
||||
* Output frame rates of 44,100, 88,200, 176,000 or 352,000 frames per second.
|
||||
|
||||
For other use cases, Shairport Sync can provide synchronised audio output to a unix pipe or to standard output, or to audio systems that do not provide timing information. This could perhaps be described as *partial audio synchronisation*, where synchronised audio is provided by Shairport Sync, but what happens to it in the subsequent processing chain, before it reaches the listener's ear, is outside the control of Shairport Sync.
|
||||
Some features require configuration at build time – see [CONFIGURATION FLAGS.md](CONFIGURATION%20FLAGS.md).
|
||||
|
||||
For more about the motivation behind Shairport Sync, please see the wiki at https://github.com/mikebrady/shairport-sync/wiki.
|
||||
# Status
|
||||
Shairport Sync was designed to [run best](ADVANCED%20TOPICS/GetTheBest.md) on stable, dedicated, stand-alone low-power "headless" systems with ALSA as the audio system and with a decent CD-quality Digital to Analog Converter (DAC).
|
||||
|
||||
Synchronisation, Latency, "Stuffing"
|
||||
---------
|
||||
The AirPlay protocol uses an agreed *latency* – the time difference, or delay, between the time represented by a sound sample's `timestamp` and the time it is actually played by the audio output device, typically a Digital to Audio Converter (DAC). The latency to be used is specified by the audio source when it negotiates with Shairport Sync. Most sources set a latency of two seconds. Recent versions of iTunes and forkedDaapd use a latency of just over 2.25 seconds. A latency of this length allows AirPlay players to correct for network delays, processing time variations and so on.
|
||||
Shairport Sync runs on recent (2018 onwards) Linux systems and FreeBSD from 12.1 onwards. It requires a system with the power of a Raspberry Pi 2 or a Pi Zero 2 or better.
|
||||
|
||||
As mentioned previously, Shairport Sync implements full audio synchronisation when used with `alsa`, `sndio` or PulseAudio systems. This is done by monitoring the timestamps present in data coming from the audio source and the timing information from the audio system, e.g. `alsa`. To maintain the latency required for exact synchronisation, if the output device is running slow relative to the source, Shairport Sync will delete frames of audio to allow the device to keep up. If the output device is running fast, Shairport Sync will insert frames to keep time. The number of frames inserted or deleted is so small as to be almost inaudible on normal audio material. Frames are inserted or deleted as necessary at pseudorandom intervals. Alternatively, with `libsoxr` support, Shairport Sync can resample the audio feed to ensure the output device can keep up. This is less obtrusive than insertion and deletion but requires a good deal of processing power — most embedded devices probably can't support it. The process of insertion/deletion or resampling is rather inelegantly called “stuffing”.
|
||||
Classic Shairport Sync runs on a wider variety of Linux sytems, including OpenWrt and Cygwin and it also runs on OpenBSD. Many embedded devices are powerful enough to power classic Shairport Sync.
|
||||
|
||||
# Heritage and Acknowledgements
|
||||
The functionality offered by Shairport Sync is the result of study and analysis of the AirPlay and AirPlay 2 protocols by many people over the years. These protocols have not been officially published, and there is no assurance that Shairport Sync will continue to work in future.
|
||||
|
||||
Shairport Sync is a substantial rewrite of the fantastic work done in Shairport 1.0 by James Wah (aka [abrasive](https://github.com/abrasive)), James Laird and others — please see [this list](https://github.com/abrasive/shairport/blob/master/README.md#contributors-to-version-1x) of the contributors to Shairport 1.x and Shairport 0.x. From a "heritage" point of view, Shairport Sync is a fork of Shairport 1.0.
|
||||
|
||||
For the development of AirPlay 2 support, special thanks are due to:
|
||||
* [JD Smith](https://github.com/jdtsmith) for really thorough testing, support and encouragement.
|
||||
* [ejurgensen](https://github.com/ejurgensen) for advice and [code to deal with pairing and encryption](https://github.com/ejurgensen/pair_ap).
|
||||
* [ckdo](https://github.com/ckdo) for pointing the way, particularly with pairing and encryption protocols, with a [functional Python implementation](https://github.com/ckdo/airplay2-receiver) of AirPlay 2.
|
||||
* [invano](https://github.com/invano) for showing what might be possible and for initial Python development.
|
||||
* [Charles Omer](https://github.com/charlesomer) for Docker automation, repository management automation, testing, encouragement, enthusiasm.
|
||||
|
||||
Much of Shairport Sync's AirPlay 2 functionality is based on ideas developed at the [openairplay airplay2-receiver]( https://github.com/openairplay/airplay2-receiver) repository. It is a pleasure to acknowledge the work of the contributors there.
|
||||
|
||||
Thanks to everyone who has supported and improved Shairport Sync over the years.
|
||||
|
||||
# More about Shairport Sync
|
||||
The audio that Shairport Sync receives is sent to the computer's sound system, to a named unix pipe or to `STDOUT`. By far the best sound system to use is ALSA. This is because ALSA can give direct access to the Digital to Analog Converter (DAC) hardware of the machine. Audio samples can be sent through ALSA directly to the DAC, maximising fidelity, and accurate timing information can be obtained from the DAC, maximising synchronisation. Direct access to hardware is given through ALSA devices with names beginning with `hw:`.
|
||||
|
||||
## Synchronised Audio
|
||||
Shairport Sync offers *full audio synchronisation*. Full audio synchronisation means that audio is played on the output device at exactly the time specified by the audio source. To accomplish this, Shairport Sync needs access to audio systems – such as ALSA on Linux and `sndio` on FreeBSD – that provide very accurate timing information about audio being streamed to output devices. Ideally, Shairport Sync should have direct access to the output device used, which should be a real sound card capable of working with 44,100, 88,200 or 176,400 samples per second, interleaved PCM stereo of 8, 16, 24 or 32 bits. Using the ALSA sound system, Shairport Sync will choose the greatest bit depth available at 44,100 samples per second, resorting to multiples of 44,100 if it is not available. You'll get a message in the log if there's a problem. With all other sound systems, a sample rate of 44,100 is chosen with a bit depth of 16 bit.
|
||||
|
||||
Shairport Sync works well with PulseAudio, a widely used sound server found on many desktop Linuxes. While the timing information is not as accurate as that of ALSA or `sndio`, it is often impractical to remove or disable PulseAudio.
|
||||
|
||||
For other use cases, Shairport Sync can provide synchronised audio output to a unix pipe or to `STDOUT`, or to audio systems that do not provide timing information. This could perhaps be described as *partial audio synchronisation*, where synchronised audio is provided by Shairport Sync, but what happens to it in the subsequent processing chain, before it reaches the listener's ear, is outside the control of Shairport Sync.
|
||||
|
||||
## Latency, "Stuffing", Timing
|
||||
AirPlay protocols use an agreed *latency* – a time lag or delay – between the time represented by a sound sample's `timestamp` and the time it is actually played by the audio output device, typically a Digital to Audio Converter (DAC). Latency gives players time to compensate for network delays, processing time variations and so on. The latency is specified by the audio source when it negotiates with Shairport Sync. AirPlay sources set a latency of around 2.0 to 2.25 seconds. AirPlay 2 can use shorter latencies, around half a second.
|
||||
|
||||
As mentioned previously, Shairport Sync implements full audio synchronisation when used with ALSA, `sndio` or PulseAudio audio systems. This is done by monitoring the timestamps present in data coming from the audio source and the timing information coming back from the audio system itself. To maintain the latency required for exact synchronisation, if the output device is running slow relative to the source, Shairport Sync will delete frames of audio to allow the device to keep up. If the output device is running fast, Shairport Sync will insert ("stuff") extra frames to keep time. The number of frames inserted or deleted is so small as to be almost inaudible on normal audio material. Frames are inserted or deleted as necessary at pseudorandom intervals. Alternatively, with `libsoxr` support, Shairport Sync can resample the audio feed to ensure the output device can keep up. This is less obtrusive than insertion and deletion but requires a good deal of processing power — most embedded devices probably can't support it. If your computer is fast enough, Shairport Sync will, by default, automatically choose this method.
|
||||
|
||||
Stuffing is not done for partial audio synchronisation – the audio samples are simply presented at exactly the right time to the next stage in the processing chain.
|
||||
|
||||
Timestamps are referenced relative to the source computer's clock – the `source clock`, but timing must be done relative to the clock of the computer running Shairport Sync – the `local clock`. So, another thing Shairport Sync has to do is to synchronize the source clock and the local clock, and it does this usually to within a fraction of a millisecond, using a variant of NTP synchronisation protocols.
|
||||
|
||||
What else?
|
||||
--------------
|
||||
* 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.
|
||||
* Hardware Mute — Shairport Sync can mute properly if the hardware supports it.
|
||||
* Support for the Apple ALAC decoder.
|
||||
* Output bit depths of 8, 16, 24 and 32 bits, rather than the standard 16 bits.
|
||||
* Output frame rates of 44,100, 88,200, 176,000 or 352,000 frames per second.
|
||||
* Fast Response — With hardware volume control, response is instantaneous; otherwise the response time is 0.20 seconds with `alsa`, 0.35 seconds with `sndio`.
|
||||
* Non-Interruptible — Shairport Sync sends back a "busy" signal if it's already playing audio from another source, so other sources can't disrupt an existing Shairport Sync session. (If a source disappears without warning, the session automatically terminates after two minutes and the device becomes available again.)
|
||||
* 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.
|
||||
* Compiles on Linux, Cygwin, FreeBSD, OpenBSD.
|
||||
* Outputs to [`alsa`](https://www.alsa-project.org/wiki/Main_Page), [`sndio`](http://www.sndio.org), [PulseAudio](https://www.freedesktop.org/wiki/Software/PulseAudio/), [JACK](http://jackaudio.org), to a unix pipe or to `STDOUT`. It also has limited support for [libao](https://xiph.org/ao/) and for [`soundio`](http://libsound.io).
|
||||
* An [MPRIS](https://specifications.freedesktop.org/mpris-spec/2.2/) interface, partially complete and very functional, including access to metadata and artwork, and some limited remote control.
|
||||
* An interface to [MQTT](https://en.wikipedia.org/wiki/MQTT), an often-used protocol in home automation projects.
|
||||
* A native D-Bus interface, including access to metadata and artwork, some limited remote control and some system settings.
|
||||
* 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.)
|
||||
|
||||
Heritage
|
||||
-------
|
||||
Shairport Sync is a substantial rewrite of the fantastic work done in Shairport 1.0 by James Laird and others — please see https://github.com/abrasive/shairport/blob/master/README.md#contributors-to-version-1x for a list of the contributors to Shairport 1.x and Shairport 0.x. From a "heritage" point of view, Shairport Sync is a fork of Shairport 1.0.
|
||||
|
||||
Status
|
||||
------
|
||||
Shairport Sync works on a wide variety of Linux devices, FreeBSD and OpenBSD. It works with built-in audio and with a variety of USB-connected audio amplifiers and DACs. Shairport Sync runs well on the Raspberry Pi. It can drive the built-in sound card – see the note below on configuring the Raspberry Pi to make best use of it. It runs well on the Raspberry Pi Zero W with a suitable USB or I2S card.
|
||||
|
||||
Shairport Sync will work with PulseAudio, which is installed in many desktop Linuxes. PulseAudio normally runs in the *user mode* but can be configured to run in *system mode*, though this is not recommended. Shairport Sync can work with it in either mode.
|
||||
|
||||
Shairport Sync runs natively on FreeBSD and OpenBSD using the `sndio` sound system.
|
||||
|
||||
Shairport Sync runs on Ubuntu, OpenWrt, Debian, Arch Linux, Fedora, FreeBSD and OpenBSD inside VMWare Fusion on a Mac, but synchronisation is inaccurate — possibly because the sound card is being emulated.
|
||||
|
||||
For information about changes and updates, please refer to the RELEASENOTES.md file in the distribution.
|
||||
|
||||
Building And Installing
|
||||
---------------------
|
||||
See [here](https://github.com/mikebrady/shairport-sync/blob/master/INSTALL.md) for simple installation instructions for modern Linux systems like Rapsberry Pis.
|
||||
|
||||
**Docker:** Shairport Sync is available as a prebuilt Docker image at https://hub.docker.com/r/mikebrady/shairport-sync. Alternatively, you can roll your own using the Dockerfile provided. Note that, at present, the Shairport Sync Docker image does not work on a Mac, due to limitations in the Docker system.
|
||||
|
||||
Shairport Sync may already be available as a package in your Linux distribution (search for `shairport-sync` – the package named `shairport` is a different program). Packages are available on recent versions of Debian, Ubuntu, Arch, OpenWrt and possibly more:
|
||||
|
||||
**Ubuntu:** A `shairport-sync` installer package is available for Ubuntu. Additionally, a Personal Package Archives for Shairport Sync master and development branches are available at https://launchpad.net/~dantheperson.
|
||||
|
||||
**Debian:** shairport-sync is in the Debian archive.
|
||||
|
||||
**Ansible Playbooks for CentOS 8 Stream on Raspberry Pi:** A suite of Ansible playbooks and scripts to make deploying Shairport Sync easy on CentOS 8 Stream specifically on Raspberry Pi is available at https://github.com/p3ck/ansible-shairport.
|
||||
|
||||
**OpenWrt:** There is a Shairport Sync package in OpenWrt trunk. Also, there's an OpenWrt package at https://github.com/mikebrady/shairport-sync-for-openwrt, including one that builds back to Barrier Breaker.
|
||||
|
||||
**Arch Linux:** Shairport Sync is available for x86_64 and i686 platforms in the Arch Linux Community Repository -- search for `shairport-sync`. See also https://www.archlinux.org/packages/.
|
||||
|
||||
To build and install the latest version of Shairport Sync, an [Arch Linux build and installation guide](https://github.com/mikebrady/shairport-sync-for-arch-linux) is available, based on original work by [Elia Cereda](https://github.com/EliaCereda).
|
||||
|
||||
**Mac OS X:** A HomeBrew package exists for Shairport Sync. With HomeBrew installed, Shairport Sync can be installed using the command:
|
||||
```
|
||||
$brew install shairport-sync
|
||||
```
|
||||
Note that the installation uses the libao library and so synchronisation is not available — playback glitches will occur occasionally, when the ao system's buffers overflow or underflow.
|
||||
|
||||
**Fedora:** Please see the guide at [FEDORA.md](https://github.com/mikebrady/shairport-sync/blob/master/FEDORA.md).
|
||||
|
||||
**Gentoo:** Shairport Sync is available in griffon_overlay, please see the guide at [https://github.com/windfail/griffon_overlay/wiki/shairport-sync](https://github.com/windfail/griffon_overlay/wiki/shairport-sync).
|
||||
|
||||
**Cygwin:** Please see the guide at [CYGWIN.md](https://github.com/mikebrady/shairport-sync/blob/master/CYGWIN.md).
|
||||
|
||||
**OpenBSD:** A port exists for Shairport Sync. Packages are available for users of -current OpenBSD, the first release to include this port will be OpenBSD 7.2. To install, simply run
|
||||
|
||||
```
|
||||
pkg_add shairport_sync
|
||||
```
|
||||
Configure your system to start the service at boot time with
|
||||
```
|
||||
rcctl enable messagebus avahi_daemon shairport_sync
|
||||
```
|
||||
|
||||
Note that the README for this port includes instructions on how to share access to the audio device between Shairport Sync and another user.
|
||||
|
||||
Sincere thanks to all package contributors!
|
||||
|
||||
If you wish to build and install the latest version of Shairport Sync on Debian, Ubuntu, Fedora or Arch platforms, please continue to follow these instructions. When the program has been compiled and installed, refer to the section on Configuring Shairport Sync that follows. To build Shairport Sync from sources on FreeBSD please refer to [FREEBSD.md](https://github.com/mikebrady/shairport-sync/blob/master/FREEBSD.md).
|
||||
|
||||
**Remove Old Versions of Shairport Sync and its Startup Scripts**
|
||||
|
||||
You should check to see if `shairport-sync` is already installed – you can use the command `$ which shairport-sync` to find where it is located, if installed. If it is installed you should delete it – you may need superuser privileges. After deleting, check again in case further copies are installed elsewhere.
|
||||
|
||||
You should also remove the startup script and service definition files `/etc/systemd/system/shairport-sync.service`, `/lib/systemd/system/shairport-sync.service`, `/etc/init.d/shairport-sync`, `/etc/dbus-1/system.d/shairport-sync-dbus.conf` and `/etc/dbus-1/system.d/shairport-sync-mpris.conf` if they exist – new ones will be installed if necessary.
|
||||
|
||||
If you removed any installations of Shairport Sync or any of its startup script files, you should reboot.
|
||||
|
||||
**Determine The Configuration Needed**
|
||||
|
||||
Shairport Sync has a number of different "backends" that connect it to the system's audio handling infrastructure. Most recent Linux distributions that have a GUI – including Ubuntu, Debian and others – use PulseAudio to handle sound. In such cases, it is inadvisable to attempt to disable or remove PulseAudio. Thus, if your system uses PulseAudio, you should build Shairport Sync with the PulseAudio backend. You can check to see if PulseAudio is running by opening a Terminal window and entering the command `$ pactl info`. Here is an example of what you'll get if PulseAudio is installed, though the exact details may vary:
|
||||
```
|
||||
$ pactl info
|
||||
Server String: unix:/run/user/1000/pulse/native
|
||||
Library Protocol Version: 30
|
||||
Server Protocol Version: 30
|
||||
Is Local: yes
|
||||
Client Index: 9
|
||||
Tile Size: 65472
|
||||
User Name: mike
|
||||
Host Name: ubuntu
|
||||
Server Name: pulseaudio
|
||||
Server Version: 8.0
|
||||
Default Sample Specification: s16le 2ch 44100Hz
|
||||
Default Channel Map: front-left,front-right
|
||||
Default Sink: alsa_output.pci-0000_02_02.0.analog-stereo
|
||||
Default Source: alsa_input.pci-0000_02_02.0.analog-stereo
|
||||
Cookie: 96f9:3e8d
|
||||
$
|
||||
```
|
||||
If PulseAudio in not installed, you'll get something like this:
|
||||
```
|
||||
$ pactl info
|
||||
-bash: pactl: command not found
|
||||
$
|
||||
```
|
||||
If your system does not use PulseAudio, then it is likely that it uses the Advanced Linux Sound Architecture (ALSA), so you should build Shairport Sync with the ALSA backend. By the way, many systems with PulseAudio also have ALSA (in fact, PulseAudio is effectively a client of ALSA); in those cases you should choose the PulseAudio backend.
|
||||
|
||||
If PulseAudio is not installed, there is no necessity to install it for Shairport Sync. In fact, Shairport Sync works better without it.
|
||||
|
||||
**Building**
|
||||
|
||||
To build Shairport Sync from sources on Debian, Ubuntu, Raspbian, etc. follow these instructions.
|
||||
|
||||
The following libraries are required:
|
||||
* OpenSSL or mbed TLS (PolarSSL is supported but deprecated)
|
||||
* Avahi
|
||||
* ALSA and/or PulseAudio
|
||||
* libdaemon
|
||||
* autoconf
|
||||
* automake
|
||||
* libtool
|
||||
* libpopt
|
||||
* libconfig
|
||||
|
||||
Optional:
|
||||
* libsoxr
|
||||
* libalac (This is a library containing the Apple ALAC decoder.)
|
||||
|
||||
Many Linux distributions have Avahi and OpenSSL already in place, so normally it probably makes sense to choose those options rather than tinysvcmdns or mbed TLS. The `libsoxr` library is available in recent Linux distributions, but it requires lots of processor power.
|
||||
|
||||
Debian, Ubuntu and Raspbian users can get the basics with:
|
||||
|
||||
- `# apt install build-essential git xmltoman` – these may already be installed.
|
||||
- `# apt install autoconf automake libtool libdaemon-dev libpopt-dev libconfig-dev`
|
||||
- `# apt install libasound2-dev` for the ALSA libraries
|
||||
- `# apt install libpulse-dev` for the PulseAudio libraries
|
||||
- `# apt install avahi-daemon libavahi-client-dev` if you want to use Avahi (recommended).
|
||||
- `# apt install libssl-dev` if you want to use OpenSSL and libcrypto, or use mbed TLS otherwise.
|
||||
- `# apt install libmbedtls-dev` if you want to use mbed TLS, or use OpenSSL/libcrypto otherwise. You can still use PolarSSL with `apt install libpolarssl-dev` if you want to use PolarSSL, but it is deprecated as it's not longer being supported.
|
||||
- `# apt install libsoxr-dev` if you want support for libsoxr-based resampling. This library is in recent distributions; if not, instructions for how to build it from source on Linux are available at [LIBSOXR.md](https://github.com/mikebrady/shairport-sync/blob/master/LIBSOXR.md).
|
||||
- `# apt install libsndfile1-dev` if you want to use the convolution filter.
|
||||
|
||||
If you wish to include the Apple ALAC decoder, you need install it first – please refer to the [ALAC](https://github.com/mikebrady/alac) repository for more information.
|
||||
|
||||
**Download Shairport Sync:**
|
||||
```
|
||||
$ git clone https://github.com/mikebrady/shairport-sync.git
|
||||
```
|
||||
|
||||
Next, `cd` into the shairport-sync directory and execute the following commands:
|
||||
```
|
||||
$ autoreconf -i -f
|
||||
```
|
||||
(Note that the `autoreconf...` step may take some time on less powerful machines.)
|
||||
|
||||
**Choose the appropriate `--with-*` options:**
|
||||
|
||||
(Don't worry -- there's a recommended set of configuration options further down.)
|
||||
|
||||
- `--with-alsa` include the ALSA backend module to audio to be output through the Advanced Linux Sound Architecture (ALSA) system directly. This is recommended for highest quality.
|
||||
- `--with-pa` include the PulseAudio audio back end. This is recommended if your Linux installation already has PulseAudio installed. Although ALSA would be better, it requires direct and exclusive access to to a real (hardware) soundcard, and this is often impractical if PulseAudio is installed.
|
||||
- `--with-stdout` include an optional backend module to enable raw audio to be output through standard output (stdout).
|
||||
- `--with-pipe` include an optional backend module to enable raw audio to be output through a unix pipe.
|
||||
- `--with-soundio` include an optional backend module to enable raw audio to be output through the soundio system.
|
||||
- `--with-avahi` or `--with-tinysvcmdns` for mdns support. Avahi is a widely-used system-wide zero-configuration networking (zeroconf) service — it may already be in your system. If you don't have Avahi, or similar, then consider including tinysvcmdns, which is a tiny zeroconf service embedded inside the shairport-sync application itself. To enable multicast for `tinysvcmdns`, you may have to add a default route with the following command: `route add -net 224.0.0.0 netmask 224.0.0.0 eth0` (substitute the correct network port for `eth0`). You should not have more than one zeroconf service on the same system — bad things may happen, according to RFC 6762, §15. If you need to use an external zeroconf service (`--with-external-mdns`) to avoid this problem, you may need to install avahi-utils or avahi-tools to get the `avahi-publish-service` tool for your system.
|
||||
- `--with-ssl=openssl`, `--with-ssl=mbedtls` or `--with-ssl=polarssl` (deprecated) for encryption and related utilities using either OpenSSL, mbed TLS or PolarSSL.
|
||||
- `--with-libdaemon` include a demonising library needed if you want to be able to demonise Shairport Sync with the `-d` option. Not needed for `systemd`-based systems which demonise programs differently.
|
||||
- `--with-soxr` for libsoxr-based resampling.
|
||||
- `--with-piddir` for specifying where the PID file should be stored. This directory is normally chosen automatically. The directory must be writable. If you use this option, you may have to edit the init script to search for the PID file in your new location.
|
||||
- `--with-metadata` to add support for Shairport Sync to pipe metadata to a compatible application of your choice. See https://github.com/mikebrady/shairport-sync-metadata-reader for a sample metadata reader.
|
||||
- `--with-configfiles` to install a configuration file and a separate sample file at the `make install` stage. Default is to install. An existing `/etc/shairport-sync.conf` will not be overwritten.
|
||||
- `--with-pkg-config` to use pkg-config to find libraries. Default is to use pkg-config — this option is for special purpose use.
|
||||
- `--with-apple-alac` to include the Apple ALAC Decoder.
|
||||
- `--with-convolution` to include a convolution filter that can be used to apply effects such as frequency and phase correction, and a loudness filter that compensates for human ear non-linearity. Requires `libsndfile`.
|
||||
- `--with-systemd` to include a script to create a Shairport Sync service that can optionally launch automatically at startup on `systemd`-based Linuxes. Default is not to to install.
|
||||
- `--with-systemv` to include a script to create a Shairport Sync service that can optionally launch automatically at startup on System V based Linuxes. Default is not to to install.
|
||||
|
||||
**Determine if it's a `systemd` or a "System V" installation:**
|
||||
|
||||
If you wish to have Shairport Sync start automatically when your system boots, you need to figure out what so-called "init system" your system is using. (If you are using Shairport Sync with PulseAudio, as installed in many desktop systems, this section doesn't apply.)
|
||||
|
||||
There are a number of init systems in use: `systemd`, `upstart` and "System V" among others, and it's actually difficult to be certain which one your system is using. Fortunately, for Shairport Sync, all you have to do is figure out if it's a `systemd` init system or not. If it is not a `systemd` init system, you can assume that it is either a System V init system or else it is compatible with a System V init system. Recent systems tend to use `systemd`, whereas older systems use `upstart` or the earlier System V init system.
|
||||
|
||||
The easiest way is to look at the first few lines of the `init` manual. Enter the command:
|
||||
|
||||
```
|
||||
$ man init
|
||||
```
|
||||
In a `systemd` system, the top lines of the `man` page make it clear that it's a `systemd` system, as follows (from Ubuntu 16.04):
|
||||
```
|
||||
SYSTEMD(1) systemd SYSTEMD(1)
|
||||
|
||||
NAME
|
||||
systemd, init - systemd system and service manager
|
||||
...
|
||||
```
|
||||
Other init systems will look considerably different. For instance, in an `upstart` system, the top lines of the `man` page indicate it's using `upstart` system, as follows (from Ubuntu 14.04):
|
||||
|
||||
```
|
||||
init(8) System Manager's Manual init(8)
|
||||
|
||||
NAME
|
||||
init - Upstart process management daemon
|
||||
|
||||
SYNOPSIS
|
||||
init [OPTION]...
|
||||
...
|
||||
```
|
||||
In a System V system, the top lines of the `man` page are as follows (from Debian 7.11):
|
||||
```
|
||||
INIT(8) Linux System Administrator's Manual INIT(8)
|
||||
|
||||
NAME
|
||||
init, telinit - process control initialization
|
||||
|
||||
SYNOPSIS
|
||||
/sbin/init [ -a ] [ -s ] [ -b ] [ -z xxx ] [ 0123456Ss ]
|
||||
/sbin/telinit [ -t SECONDS ] [ 0123456sSQqabcUu ]
|
||||
/sbin/telinit [ -e VAR[=VAL] ]
|
||||
|
||||
...
|
||||
```
|
||||
If your system is definitely a `systemd` system, choose `--with-systemd` below. Otherwise, choose `--with-libdaemon --with-systemv`.
|
||||
|
||||
**Choose the location of the configuration file**
|
||||
|
||||
A final consideration is the location of the configuration file `shairport-sync.conf`. This will be placed in the directory specified by the `sysconfdir` configuration variable, which defaults to `/usr/local/etc`. This is normal in BSD Unixes, but is unusual in Linux. Hence, for Linux installations, you need to set the `sysconfdir` variable to `/etc` using the configuration setting `--sysconfdir=/etc`.
|
||||
|
||||
**Sample `./configure` command with parameters for a typical Linux `systemd` installation:**
|
||||
|
||||
Here is a recommended set of configuration options suitable for Linux installations that use `systemd`, such as Ubuntu 15.10 and later, and Raspbian Buster, Stretch and Jessie. It specifies both the ALSA and PulseAudio backends and includes a sample configuration file and an script for automatic startup on system boot:
|
||||
|
||||
`$ ./configure --sysconfdir=/etc --with-alsa --with-pa --with-avahi --with-ssl=openssl --with-metadata --with-soxr --with-systemd`
|
||||
|
||||
* Omit the `--with-soxr` if the libsoxr library is not available.
|
||||
* For installation into a System V system, replace the `--with-systemd` with `--with-libdaemon --with-systemv`.
|
||||
* If you intend to use Shairport Sync with PulseAudio in the standard user mode, it can not be a system service, so you should omit both `--with-systemd` and `--with-systemv`.
|
||||
|
||||
**Build and Install the Application:**
|
||||
|
||||
Enter:
|
||||
|
||||
```
|
||||
$ make
|
||||
```
|
||||
|
||||
to build the application.
|
||||
|
||||
Assuming you have used the `./configure` settings suggested above, you can now install the application:
|
||||
|
||||
```
|
||||
$ sudo make install
|
||||
```
|
||||
The user and group `shairport-sync` will be created if necessary, and a runtime folder will be created at `/var/run/shairport-sync` if you have chosen `--with-systemv`. In addition, a `man` page, a default configuration file and automatic power-on startup script will be installed.
|
||||
|
||||
|
||||
**Complete installation into to a `systemd` system**
|
||||
|
||||
If you have chosen the `--with-systemd` configuration option, then, to enable Shairport Sync to start automatically at system startup, enter:
|
||||
|
||||
```
|
||||
$ sudo systemctl enable shairport-sync
|
||||
```
|
||||
You should then reboot the system.
|
||||
|
||||
**Complete installation into a System V system**
|
||||
|
||||
If you have chosen the `--with-systemv` configuration option, then, to enable Shairport Sync to start automatically at system startup, enter:
|
||||
```
|
||||
$ sudo update-rc.d shairport-sync defaults 90 10
|
||||
$ sudo update-rc.d shairport-sync enable
|
||||
```
|
||||
You should then reboot the system.
|
||||
|
||||
**Man Page**
|
||||
|
||||
You can view the man page here: http://htmlpreview.github.io/?https://github.com/mikebrady/shairport-sync/blob/master/man/shairport-sync.html
|
||||
|
||||
Configuring Shairport Sync
|
||||
--------
|
||||
There are two logically distinct parts to getting Shairport Sync to run properly on your machine: (1) starting and stopping it and (2) ensuring it has the right settings.
|
||||
|
||||
**(1) Starting and Stopping:**
|
||||
Starting and stopping Shairport Sync automatically is taken care of differently in different versions of Linux – see the previous section for an example of installing into a `systemd` or a System V based system.
|
||||
|
||||
If you are using PulseAudio in the standard "user" mode, you can not start Shairport Sync automatically at boot time – it must be started when the user logs in to a GUI session. Various GUI tools exist to enable you to build a Startup Application. However, there does not appear to be a similar tool for terminating the program when you log out. Also, if the GUI switches to another user, the user's PulseAudio session is disconnected from the output device.
|
||||
|
||||
**(2) Settings:**
|
||||
To get the best from Shairport Sync, you’ll need to (a) give Shairport Sync a service name by which it will be seen in iTunes etc. and (b) specify the backend you wish to use - `alsa` for the ALSA backend, or `pa` for the PulseAudio back end. If only one backend is included at compilation, or if the backend is ALSA, there is no need to explicitly specify the backend.
|
||||
|
||||
For the ALSA backend you may need to (c) specify the output device to use and (d) specify the name of the mixer volume control to use to control the output level. To get values for (c) and (d) you might need to explore the ALSA output devices with a program like `alsamixer` or similar.
|
||||
|
||||
Shairport Sync reads settings from a configuration file at `/etc/shairport-sync.conf` (note that in FreeBSD it will be at `/usr/local/etc/shairport-sync.conf`). When you run `$sudo make install`, a sample configuration file is installed or updated at `/etc/shairport-sync.conf.sample` (`/usr/local/etc/shairport-sync.conf.sample` in FreeBSD). This contains all the setting groups and all the settings available, but they all are commented out (comments begin with `//`) so that default values are used. The file contains explanations of the settings, useful hints and suggestions. In addition, if the file doesn't already exist, a default configuration is installed, which should work in almost any system with a sound card.
|
||||
|
||||
Settings in the configuration file are grouped. For instance, there is a `general` group within which you can use the `name` tag to set the service name. Suppose you wanted to set the name of the service to `Front Room` and give the service the password `secret`, then you should do the following:
|
||||
|
||||
```
|
||||
general =
|
||||
{
|
||||
name = "Front Room";
|
||||
password = "secret";
|
||||
// ... other general settings
|
||||
};
|
||||
```
|
||||
(Remember, anything preceded by `//` is a comment and will have no effect on the setting of Shairport Sync.)
|
||||
|
||||
**Important:** You should *never* use an important password as the AirPlay password for a Shairport Sync player -- the password is stored in Shairport Sync's configuration file in plain text and is thus completely vulnerable.
|
||||
|
||||
No backend is specified here, so it will default to the `alsa` backend if more than one back end has been compiled. To route the output to PulseAudio, add:
|
||||
|
||||
```
|
||||
output_backend = "pa";
|
||||
```
|
||||
to the `general` group.
|
||||
|
||||
The `alsa` group is used to specify properties of the output device. The most obvious setting is the name of the output device which you can set using the `output_device` tag.
|
||||
|
||||
The following `alsa` group settings are very important for maximum performance. If your audio device has a mixer that can be use to control the volume, then Shairport Sync can use it to give instant response to volume and mute commands and it can offload some work from the processor.
|
||||
* The `mixer_control_name` tag allows you to specify the name of the mixer volume control.
|
||||
* The `mixer_device` tag allows you specify where the mixer is. By default, the mixer is on the `output_device`, so you only need to use the `mixer_device` tag if the mixer is elsewhere. This can happen if you specify a *device* rather than a *card* with the `output_device` tag, because normally a mixer is associated with a *card* rather than a device. Suppose you wish to use the output device `5` of card `hw:0` and the mixer volume-control named `PCM`:
|
||||
|
||||
```
|
||||
alsa =
|
||||
{
|
||||
output_device = "hw:0,5";
|
||||
mixer_device = "hw:0";
|
||||
mixer_control_name = "PCM";
|
||||
// ... other alsa settings
|
||||
};
|
||||
```
|
||||
|
||||
The `pa` group is used to specify settings relevant to the PulseAudio backend. You can set the "Application Name" that will appear in the "Sound" control panel.
|
||||
|
||||
Shairport Sync can run programs just before it starts to play an audio stream and just after it finishes. You specify them using the `sessioncontrol` group settings `run_this_before_play_begins` and `run_this_after_play_ends`. This is to facilitate situations where something has to be done before and after playing, e.g. switching on an amplifier beforehand and switching it off afterwards. Set the `wait_for_completion` value to `"yes"` for Shairport Sync to wait until the respective commands have been completed before continuing.
|
||||
|
||||
Note that the full path to the programs must be specified, and script files will not be executed unless they are marked as executable and have the appropriate shebang `#!/bin/...` as the first line. (This behaviour may be different from other Shairports.)
|
||||
|
||||
Shairport Sync can run a program whenever the volume is set or changed. You specify it using the `general` group setting `run_this_when_volume_changes`. This is to facilitate situations where something has to be done when the volume changes, e.g. adjust an external mixer value. Set the `wait_for_completion` value to `"yes"` for Shairport Sync to wait until the command has been completed before continuing. Again, please note that the full path to the program must be specified, and script files will not be executed unless they are marked as executable and have the appropriate shebang `#!/bin/...` as the first line.
|
||||
|
||||
Note: Shairport Sync can take configuration settings from command line options. This is mainly for backward compatibility, but sometimes still useful. For normal use, it is strongly recommended that you use the configuration file method.
|
||||
|
||||
**Raspberry Pi**
|
||||
|
||||
The Raspberry Pi Models A and B have a built-in audio DAC that is connected to the device's headphone jack. Apart from a loud click when used for the first time after power-up, it is now quite adequate for casual listening.
|
||||
|
||||
To get the benefits of improvements in the Pi's software and firmware, you should update to the Raspian release of October 2018 or later, as a number of improvements have been made to the built-in DAC.
|
||||
|
||||
Do the usual update and upgrade:
|
||||
```
|
||||
# apt update
|
||||
# apt upgrade
|
||||
```
|
||||
To make Shairport Sync output to the built-in audio DAC and use its hardware mixer, in the `alsa` section of the configuration file, set the output device and mixer as follows:
|
||||
```
|
||||
alsa =
|
||||
{
|
||||
output_device = "hw:0"; // the name of the alsa output device. Use "alsamixer" or "aplay" to find out the names of devices, mixers, etc.
|
||||
mixer_control_name = "PCM"; // the name of the mixer to use to adjust output volume. If not specified, volume in adjusted in software.
|
||||
// ... other alsa settings
|
||||
```
|
||||
(Remember to uncomment the lines by removing the `//` at the start of each.) When these changes have been made, reboot the machine.
|
||||
|
||||
A problem with the built-in DAC that it declares itself to have a very large mixer volume control range – all the way from -102.38dB up to +4dB, a range of 106.38 dB. In reality, only the top 60 dB of it is in any way usable. To help get the most from it, consider using the `volume_range_db` setting in the `general` group to instruct Shairport Sync to use the top of the DAC mixer's declared range. For example, if you set the `volume_range_db` figure to 60, the top 60 dB of the range will the used. With this setting on the Raspberry Pi, maximum volume will be +4dB and minimum volume will be -56dB, below which muting will occur.
|
||||
|
||||
From a user's point of view, the effect of using this setting is to move the minimum usable volume all the way down to the bottom of the user's volume control, rather than have the minimum usable volume concentrated very close to the maximum volume.
|
||||
|
||||
*Command Line Arguments*
|
||||
|
||||
As previously mentioned, you can use command line arguments to provide settings to Shairport Sync as before, though newer settings will only be available via the configuration file. For full information, please read the Shairport Sync `man` page, also available at http://htmlpreview.github.io/?https://github.com/mikebrady/shairport-sync/blob/master/man/shairport-sync.html.
|
||||
|
||||
Apart from the following options, all command line options can be replaced by settings in the configuration file. Here is a brief description of command line options that are not replicated by settings in the settings file.
|
||||
|
||||
* The `-c` option allows you to specify the location of the configuration file.
|
||||
* The `-V` option gives you version information about Shairport Sync and then quits.
|
||||
* The `-d` option causes Shairport Sync to properly daemonise itself, that is, to run in the background. You may need sudo privileges for this.
|
||||
* The `-k` option causes Shairport Sync to kill an existing Shairport Sync daemon. You may need to have sudo privileges for this.
|
||||
|
||||
The System V init script at `/etc/init.d/shairport-sync` has a bare minimum :
|
||||
`-d`. Basically all it does is put the program in daemon mode. The program will read its settings from the configuration file.
|
||||
|
||||
Examples
|
||||
--------
|
||||
|
||||
Here are some examples of complete configuration files.
|
||||
|
||||
```
|
||||
general = {
|
||||
name = "Joe's Stereo";
|
||||
};
|
||||
|
||||
alsa = {
|
||||
output_device = "hw:0";
|
||||
};
|
||||
```
|
||||
|
||||
This gives the service a particular name — "Joe's Stereo" and specifies that audio device hw:0 be used.
|
||||
|
||||
For best results with the `alsa` backend — including getting true mute and instant response to volume control and pause commands — you should access the hardware volume controls. Use `amixer` or `alsamixer` or similar to discover the name of the mixer control to be used as the `mixer_control_name`.
|
||||
|
||||
Here is an example for for a Raspberry Pi using its internal soundcard — device hw:0 — that drives the headphone jack:
|
||||
```
|
||||
general = {
|
||||
name = "Mike's Boombox";
|
||||
};
|
||||
|
||||
alsa = {
|
||||
output_device = "hw:0";
|
||||
mixer_control_name = "PCM";
|
||||
};
|
||||
```
|
||||
|
||||
Here is an example of driving a Topping TP30 Digital Amplifier, which has an integrated USB DAC and which is connected as audio device `hw:1`:
|
||||
```
|
||||
general = {
|
||||
name = "Kitchen";
|
||||
};
|
||||
|
||||
alsa = {
|
||||
output_device = "hw:1";
|
||||
mixer_control_name = "PCM";
|
||||
};
|
||||
```
|
||||
|
||||
For a cheapo "3D Sound" USB card (Stereo output and input only) on a Raspberry Pi:
|
||||
```
|
||||
general = {
|
||||
name = "Front Room";
|
||||
};
|
||||
|
||||
alsa = {
|
||||
output_device = "hw:1";
|
||||
mixer_control_name = "Speaker";
|
||||
};
|
||||
```
|
||||
|
||||
For a first generation Griffin iMic on a Raspberry Pi:
|
||||
```
|
||||
general = {
|
||||
name = "Attic";
|
||||
};
|
||||
|
||||
alsa = {
|
||||
output_device = "hw:1";
|
||||
mixer_control_name = "PCM";
|
||||
};
|
||||
```
|
||||
|
||||
For an NSLU2, which has no internal sound card, there appears to be a bug in ALSA — you can not specify a device other than "default". Thus:
|
||||
|
||||
On an NSLU2, to drive a first generation Griffin iMic:
|
||||
```
|
||||
general = {
|
||||
name = "Den";
|
||||
};
|
||||
|
||||
alsa = {
|
||||
mixer_control_name = "PCM";
|
||||
};
|
||||
```
|
||||
|
||||
On an NSLU2, to drive the "3D Sound" USB card:
|
||||
```
|
||||
general = {
|
||||
name = "TV Room";
|
||||
};
|
||||
|
||||
alsa = {
|
||||
mixer_control_name = "Speaker";
|
||||
};
|
||||
```
|
||||
|
||||
Finally, here is an example of using the PulseAudio backend:
|
||||
```
|
||||
general = {
|
||||
name = "Zoe's Computer";
|
||||
output_backend = "pa";
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
|
||||
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.
|
||||
|
||||
**Metadata broadcasting over UDP**
|
||||
|
||||
As an alternative to sending metadata to a pipe, the `socket_address` and `socket_port` tags may be set in the metadata group to cause Shairport Sync to broadcast UDP packets containing the track metadata.
|
||||
|
||||
The advantage of UDP is that packets can be sent to a single listener or, if a multicast address is used, to multiple listeners. It also allows metadata to be routed to a different host. However UDP has a maximum packet size of about 65000 bytes; while large enough for most data, Cover Art will often exceed this value. Any metadata exceeding this limit will not be sent over the socket interface. The maximum packet size may be set with the `socket_msglength` tag to any value between 500 and 65000 to control this - lower values may be used to ensure that each UDP packet is sent in a single network frame. The default is 500. Other than this restriction, metadata sent over the socket interface is identical to metadata sent over the pipe interface.
|
||||
|
||||
The UDP metadata format is very simple - the first four bytes are the metadata *type*, and the next four bytes are the metadata *code* (both are sent in network byte order - see https://github.com/mikebrady/shairport-sync-metadata-reader for a definition of those terms). The remaining bytes of the packet, if any, make up the raw value of the metadata.
|
||||
|
||||
Latency
|
||||
-------
|
||||
Latency is the exact time from a sound signal's original timestamp until that signal actually "appears" on the output of the audio output device, usually a Digital to Audio Converter (DAC), irrespective of any internal delays, processing times, etc. in the computer.
|
||||
|
||||
Shairport Sync uses latencies supplied by the source, typically either 2 seconds or just over 2.25 seconds. You shouldn't need to change them.
|
||||
|
||||
Problems can arise when you are trying to synchronise with speaker systems — typically surround-sound home theatre systems — that have their own inherent delays. You can compensate for an inherent delay using the appropriate backend (typically `alsa`) `audio_backend_latency_offset_in_seconds`. Set this offset (in frames) to compensate for a fixed delay in the audio back end; for example, if the output device delays by 100 ms, set this to -0.1.
|
||||
|
||||
Resynchronisation
|
||||
-------------
|
||||
Shairport Sync actively maintains synchronisation with the source.
|
||||
If synchronisation is lost — say due to a busy source or a congested network — Shairport Sync will mute its output and resynchronise. The loss-of-sync threshold is a very conservative 0.050 seconds — i.e. the actual time and the expected time must differ by more than 50 ms to trigger a resynchronisation. Smaller disparities are corrected by insertions or deletions, as described above.
|
||||
* You can vary the resync threshold, or turn resync off completely, with the `general` `resync_threshold_in_seconds` setting.
|
||||
|
||||
Tolerance
|
||||
---------
|
||||
Playback synchronisation is allowed to wander — to "drift" — a small amount before attempting to correct it. The default is 0.002 seconds, i.e. 2 ms. The smaller the tolerance, the more likely it is that overcorrection will occur. Overcorrection is when more corrections (insertions and deletions) are made than are strictly necessary to keep the stream in sync. Use the `statistics` setting to monitor correction levels. Corrections should not greatly exceed net corrections.
|
||||
* You can vary the tolerance with the `general` `drift_tolerance_in_seconds` setting.
|
||||
|
||||
Some Statistics
|
||||
---------------
|
||||
If you turn on the `general` `statistics` setting, a heading like this will be output to the log file (or to `STDERR` if the `-u` command line option is chosen):
|
||||
```
|
||||
sync error in milliseconds, net correction in ppm, corrections in ppm, total packets, missing packets, late packets, too late packets, resend requests, min DAC queue size, min buffer occupancy, max buffer occupancy, source nominal frames per second, source actual frames per second, output frames per second, source clock drift in ppm, source clock drift sample count, rough calculated correction in ppm
|
||||
```
|
||||
This will be followed by the statistics themselves at regular intervals, for example:
|
||||
```
|
||||
0.15, 0.0, 0.0, 1003, 0, 0, 0, 0, 8539, 208, 225, 44099.95, 44274.21, 44099.68, 0.00, 0, -6.12
|
||||
0.34, 0.0, 0.0, 2006, 0, 0, 0, 0, 8591, 206, 225, 44099.99, 44169.96, 44099.76, 0.00, 0, -5.27
|
||||
0.89, 0.0, 0.0, 3009, 0, 0, 0, 0, 8571, 218, 225, 44100.01, 44152.81, 44099.85, 0.00, 0, -3.66
|
||||
0.85, 0.0, 0.0, 4012, 0, 0, 0, 0, 8619, 218, 225, 44100.00, 44142.80, 44099.80, 0.00, 0, -4.62
|
||||
1.29, 0.0, 0.0, 5015, 0, 0, 0, 0, 8623, 218, 225, 44100.00, 44129.39, 44099.83, 0.00, 0, -3.85
|
||||
1.41, 0.0, 0.0, 6018, 0, 0, 0, 0, 8619, 216, 225, 44099.99, 44127.74, 44099.86, 0.00, 0, -2.76
|
||||
1.53, 0.0, 0.0, 7021, 0, 0, 0, 0, 8603, 218, 225, 44100.00, 44109.60, 44099.81, 0.00, 0, -4.21
|
||||
1.81, 0.0, 0.0, 8024, 0, 0, 0, 0, 8655, 215, 225, 44100.00, 44110.01, 44099.82, 0.00, 0, -4.16
|
||||
2.00, -82.1, 82.1, 9027, 0, 0, 0, 0, 8481, 208, 225, 44100.00, 44110.69, 44099.83, 0.00, 0, -3.97
|
||||
1.96, -8.5, 8.5, 10030, 0, 0, 0, 0, 8599, 209, 225, 44099.99, 44108.84, 44099.86, 0.00, 0, -3.02
|
||||
1.94, 0.0, 0.0, 11033, 0, 0, 0, 0, 8583, 217, 225, 44100.00, 44109.33, 44099.87, 0.00, 0, -2.93
|
||||
1.90, -5.7, 5.7, 12036, 0, 0, 0, 0, 8601, 219, 225, 44100.00, 44110.01, 44099.85, 20.47, 10, -23.88
|
||||
2.02, -36.8, 36.8, 13039, 0, 0, 0, 0, 8488, 216, 225, 44100.00, 44107.18, 44099.85, 20.39, 12, -23.83
|
||||
1.96, -17.0, 17.0, 14042, 0, 0, 0, 0, 8554, 218, 226, 44100.00, 44108.80, 44099.84, 20.63, 15, -24.13
|
||||
1.99, -31.2, 31.2, 15045, 0, 0, 0, 0, 8592, 216, 225, 44100.00, 44106.85, 44099.85, 20.95, 18, -24.39
|
||||
1.98, -28.3, 28.3, 16048, 0, 0, 0, 0, 8627, 219, 225, 44099.99, 44109.01, 44099.88, 21.23, 20, -23.90
|
||||
2.01, -31.2, 31.2, 17051, 0, 0, 0, 0, 8582, 217, 225, 44100.00, 44108.86, 44099.84, 21.57, 22, -25.10
|
||||
1.96, -19.8, 19.8, 18054, 0, 0, 0, 0, 8556, 215, 226, 44100.00, 44103.79, 44099.86, 21.59, 24, -24.85
|
||||
1.98, -25.5, 25.5, 19057, 0, 0, 0, 0, 8563, 219, 225, 44100.00, 44102.62, 44099.86, 22.26, 25, -25.47
|
||||
2.00, -28.3, 28.3, 20060, 0, 0, 0, 0, 8569, 214, 225, 44100.00, 44103.22, 44099.88, 22.57, 27, -25.41
|
||||
1.98, -42.5, 42.5, 21063, 0, 0, 0, 0, 8570, 216, 225, 44100.00, 44104.14, 44099.85, 22.74, 28, -26.08
|
||||
1.89, -31.2, 31.2, 22066, 0, 0, 0, 0, 8524, 218, 225, 44100.00, 44104.66, 44099.87, 22.89, 30, -25.89
|
||||
1.96, 0.0, 0.0, 23069, 0, 0, 0, 0, 8650, 218, 225, 44100.00, 44105.36, 44099.88, 22.92, 32, -25.73
|
||||
1.98, -34.0, 34.0, 24072, 0, 0, 0, 0, 8475, 219, 225, 44100.00, 44106.04, 44099.88, 22.90, 34, -25.70
|
||||
2.07, -138.8, 138.8, 25075, 0, 0, 0, 0, 8490, 218, 226, 44100.00, 44104.93, 44099.87, 23.87, 35, -26.92
|
||||
1.33, 0.0, 0.0, 26078, 0, 0, 0, 0, 8617, 216, 225, 44100.00, 44104.66, 44099.88, 23.68, 38, -26.43
|
||||
1.67, -22.7, 22.7, 27081, 0, 0, 0, 0, 8562, 217, 225, 44100.00, 44106.16, 44099.87, 25.06, 40, -27.93
|
||||
1.98, -28.3, 28.3, 28084, 0, 0, 0, 0, 8544, 218, 225, 44100.00, 44105.21, 44099.87, 25.91, 41, -28.84
|
||||
1.99, -22.7, 22.7, 29087, 0, 0, 0, 0, 8569, 219, 225, 44100.00, 44102.30, 44099.87, 25.91, 41, -28.74
|
||||
1.66, -56.6, 56.6, 30090, 0, 0, 0, 0, 8554, 218, 225, 44100.00, 44103.07, 44099.86, 26.38, 44, -29.46
|
||||
1.44, 0.0, 0.0, 31093, 0, 0, 0, 0, 8642, 218, 225, 44100.00, 44102.18, 44099.87, 26.08, 46, -28.98
|
||||
1.56, 0.0, 0.0, 32096, 0, 0, 0, 0, 8647, 218, 225, 44100.00, 44102.76, 44099.87, 25.79, 48, -28.64
|
||||
1.68, 0.0, 0.0, 33099, 0, 0, 0, 0, 8647, 217, 225, 44100.00, 44103.41, 44099.88, 25.66, 49, -28.51
|
||||
1.92, -14.2, 14.2, 34102, 0, 0, 0, 0, 8639, 217, 225, 44100.00, 44102.38, 44099.87, 25.45, 51, -28.40
|
||||
2.00, -45.3, 45.3, 35105, 0, 0, 0, 0, 8593, 216, 225, 44100.00, 44103.23, 44099.86, 25.35, 53, -28.61
|
||||
1.98, -17.0, 17.0, 36108, 0, 0, 0, 0, 8609, 218, 225, 44100.00, 44102.54, 44099.87, 25.21, 55, -28.12
|
||||
1.99, -31.2, 31.2, 37111, 0, 0, 0, 0, 8575, 217, 225, 44100.00, 44101.98, 44099.88, 25.11, 57, -27.88
|
||||
1.99, -14.2, 14.2, 38114, 0, 0, 0, 0, 8624, 218, 225, 44100.00, 44104.44, 44099.87, 24.95, 60, -27.97
|
||||
1.99, -28.3, 28.3, 39117, 0, 0, 0, 0, 8582, 209, 225, 44100.00, 44104.91, 44099.86, 24.88, 62, -27.98
|
||||
1.86, -17.0, 17.0, 40120, 0, 0, 0, 0, 8504, 207, 225, 44100.00, 44104.52, 44099.88, 24.66, 63, -27.39
|
||||
1.96, -56.6, 56.6, 41123, 0, 0, 0, 0, 8517, 212, 226, 44100.00, 44102.29, 44099.88, 24.54, 66, -27.34
|
||||
1.90, -36.8, 36.8, 42126, 0, 0, 0, 0, 8363, 218, 240, 44100.00, 44116.79, 44099.87, 24.55, 68, -27.48
|
||||
1.89, 0.0, 0.0, 43129, 0, 0, 0, 0, 8487, 227, 241, 44100.00, 44117.57, 44099.88, 24.56, 69, -27.32
|
||||
1.99, -22.7, 22.7, 44132, 0, 0, 0, 0, 8576, 227, 240, 44100.00, 44118.53, 44099.87, 24.57, 70, -27.44
|
||||
1.98, -39.7, 39.7, 45135, 0, 0, 0, 0, 8593, 228, 240, 44100.00, 44113.88, 44099.87, 24.60, 73, -27.43
|
||||
2.00, -34.0, 34.0, 46138, 0, 0, 0, 0, 8577, 227, 240, 44100.00, 44117.10, 44099.88, 24.65, 75, -27.46
|
||||
1.98, -22.7, 22.7, 47141, 0, 0, 0, 0, 8591, 204, 240, 44100.00, 44102.78, 44099.87, 24.67, 77, -27.59
|
||||
1.98, -17.0, 17.0, 48144, 0, 0, 0, 0, 8587, 216, 225, 44100.00, 44102.38, 44099.88, 24.67, 79, -27.42
|
||||
1.99, -28.3, 28.3, 49147, 0, 0, 0, 0, 8590, 214, 225, 44100.00, 44102.70, 44099.87, 24.66, 80, -27.57
|
||||
1.85, -62.3, 62.3, 50150, 0, 0, 0, 0, 8518, 217, 226, 44100.00, 44103.16, 44099.87, 24.70, 83, -27.55
|
||||
1.90, -28.3, 28.3, 51153, 0, 0, 0, 0, 8617, 216, 225, 44100.00, 44102.60, 44099.88, 24.66, 85, -27.42
|
||||
1.89, -5.7, 5.7, 52156, 0, 0, 0, 0, 8615, 218, 225, 44100.00, 44103.37, 44099.88, 24.65, 86, -27.39
|
||||
1.96, -19.8, 19.8, 53159, 0, 0, 0, 0, 8600, 219, 225, 44100.00, 44103.77, 44099.87, 24.62, 89, -27.53
|
||||
1.99, -22.7, 22.7, 54162, 0, 0, 0, 0, 8567, 216, 225, 44100.00, 44103.12, 44099.87, 24.57, 92, -27.45
|
||||
```
|
||||
|
||||
"Sync error in milliseconds" is the average deviation from exact synchronisation. The first line of the example above indicates that the output is on average 0.15 milliseconds behind exact synchronisation. Sync is allowed to drift by the `general` `drift_tolerance_in_seconds` setting — (± 0.002 seconds) by default — before a correction will be made.
|
||||
|
||||
"Net correction in ppm" is actually the net sum of corrections — the number of frame insertions less the number of frame deletions — given as a moving average in parts per million. After an initial settling period, it represents the drift — the divergence between the rate at which frames are generated at the source and the rate at which the output device consumes them. In this case, the drift is negligible, but it can routinely be up to 150 ppm, especially with older machines.
|
||||
|
||||
"Corrections in ppm" is the number of frame insertions plus the number of frame deletions (i.e. the total number of corrections), given as a moving average in parts per million. The closer this is to the net corrections, the fewer "unnecessary" corrections are being made. Third party programs tend to have much larger levels of corrections.
|
||||
|
||||
"Min DAC queue size" is the minimum size the queue of samples in the output device's hardware buffer was measured at. It is meant to stand at 0.2 seconds = 8,820 frames at 44,100 frames per second, and will go low if the processor is very busy. If it goes below about 2,000 then it's an indication that the processor can't really keep up.
|
||||
|
||||
"Source nominal frames per second" is the rate at which audio is being sent to Shairport Sync according to information supplied by the source itself.
|
||||
|
||||
"Source actual frames per second" is the rate at which frames of audio are actually received by Shairport Sync. This can vary a great deal due to network conditions, but over a long time (more than 30 minutes) it should settle down to an accurate value. It does not account for retransmitted packets.
|
||||
|
||||
"Output frames per second" is the actual rate at which frames of audio are taken by the output device. On a system with a well-conditioned `ntp`-based clock (and without output underruns) this figure should be very accurate after playing material continuously for a period.
|
||||
|
||||
"Source clock drift in ppm" is an estimate of the difference in timekeeping between the audio source and the Shairport Sync device. It is calculated from a linear regression of drift sample data. The number of samples the estimate is based on is given in the next column, "Source clock drift sample count".
|
||||
|
||||
"Rough calculated correction in ppm" is a very crude estimate of the amount of interpolation that needs to applied, on average, to keep sync. It is not really to be relied on at this time.
|
||||
|
||||
For reference, a drift of one second per day is approximately 11.57 ppm. Left uncorrected, even a drift this small between two audio outputs will be audible after a short time. The above sample is from an iPhone XS Max to Shairport Sync running on a WiFi-connected Raspberry Pi 3 using an [IQaudIO Pi-DigiAMP+](http://iqaudio.co.uk/hats/9-pi-digiamp.html).
|
||||
|
||||
It's not unusual to have resend requests, late packets and even missing packets if some part of the connection to the Shairport Sync device is over WiFi. Late packets can sometimes be asked for and received multiple times. Sometimes late packets are sent and arrive too late, but have already been sent and received in time, so weren't needed anyway...
|
||||
|
||||
|
||||
WiFi Issues
|
||||
---------
|
||||
If you are using WiFi, you should ensure that WiFi power management is off. See [TROUBLESHOOTING](https://github.com/mikebrady/shairport-sync/blob/master/TROUBLESHOOTING.md) for more details.
|
||||
|
||||
Troubleshooting
|
||||
---------------
|
||||
Please refer to [TROUBLESHOOTING](https://github.com/mikebrady/shairport-sync/blob/master/TROUBLESHOOTING.md) for a few hints, contributed by users.
|
||||
|
||||
MQTT
|
||||
---------------
|
||||
Please refer to the [MQTT INFO](https://github.com/mikebrady/shairport-sync/blob/master/MQTT.md) page for additional info on building, configuring and using MQTT to interface shairport-sync with common home automation systems (contributed by users).
|
||||
|
||||
Packaging status
|
||||
----------------
|
||||
|
||||
[](https://repology.org/project/shairport-sync/versions)
|
||||
Timestamps are referenced relative to the source computer's clock – the "source clock", but timing must be done relative to the clock of the computer running Shairport Sync – the "local clock". So, Shairport Sync synchronises the source clock and the local clock, usually to within a fraction of a millisecond. In AirPlay 2, this is done with the assistance of a companion application called [NQPTP](https://github.com/mikebrady/nqptp) using a [PTP](https://en.wikipedia.org/wiki/Precision_Time_Protocol)-based timing protocol. In classic AirPlay, a variant of [NTP](https://en.wikipedia.org/wiki/Network_Time_Protocol) synchronisation protocols is used.
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
+153
-14
@@ -6,7 +6,14 @@ In this brief document will be listed some problems and some solutions, some pro
|
||||
|
||||
1. Before starting, ensure that your software is up-to-date. This document always refers to the most recent version of Shairport Sync -- see [here](https://github.com/mikebrady/shairport-sync/releases) for information about the most recent release.
|
||||
2. If you have set `interpolation` in the `general` section of the configuration file to to `soxr`, comment it out or set it to `auto` or `basic` as the `soxr` setting can cause lower-powered devices to bog down at critical times, e.g. see [this report](https://github.com/mikebrady/shairport-sync/issues/631#issuecomment-366305203).
|
||||
3. Ensure the volume setting of your output device is set to some reasonable value and ensure it is not muted. For ALSA devices, the `alsamixer` command-line tool is very good for this. For other sound systems, please consult the relevant documentation.
|
||||
|
||||
### No/Low Sound
|
||||
Let's say you've just installed or updated Shairport Sync and you are testing it for the first time after installing or updating.
|
||||
If you are using the default ALSA backend, don't forget to check two simple things:
|
||||
1. Check that the volume on the output device is turned up. If the output device has a "mixer" i.e. a volume control, Shairport Sync does not, by default, try to control it. Therefore, if it happens to be very low or even at zero, you might not hear audio that is actually coming through to the device. (You can get Shairport Sync to control a mixer -- see [here](https://github.com/mikebrady/spsdoc/blob/main/ADVANCED%20TOPICS/InitialConfiguration.md) for some hints.)
|
||||
2. Check that the output device is not muted. Some audio applications (including very old versions of Shairport Sync) leave the output device mixer in a muted state after use to minimise the possibility of noise. However, this is not generally compatible with other audio players using the same device, as they generally expect the device to be unmuted.
|
||||
|
||||
You can use `alsamixer` for both of theses checks. A muted output has the letter(s) `M` as its value. Select it and type `M` again to unmute.
|
||||
|
||||
### Audio is Delayed!
|
||||
If the audio from your Shairport Sync device is delayed slightly by comparison with audio from other devices, it may be that the output device being fed by Shairport Sync is introducing a delay while it processes the audio. If your output device include any digital processing component, it probably delays the audio while it processing occurs.
|
||||
@@ -70,15 +77,22 @@ To see the AirPlay service Shairport Sync provides, your devices must be on the
|
||||
### Faulty WiFi
|
||||
For an example of what it can take to track down a bad WiFi situation – in this case, a faulty WiFi adapter – please look at [this report](https://github.com/mikebrady/shairport-sync/issues/689).
|
||||
|
||||
### Using your iOS device as a hotspot.
|
||||
An iOS device that is being used as a WiFi hotspot can not play audio to another device.
|
||||
|
||||
### Can't play from iTunes on Windows
|
||||
|
||||
**Problem**
|
||||
|
||||
You can play from other devices but not from your Windows PC.
|
||||
|
||||
**Possible Solution**
|
||||
**Possible Issue -- AirPlay 2**
|
||||
iTunes on Windows is not compatible with Shairport Sync when it is built for AirPlay 2. This is unlikely to change, unfortunately. However, iTunes on Windows remains compatible with "classical" Shairport Sync.
|
||||
|
||||
Allow network discovery. This setting creates a private type network and enables Windows to access the ports and protocols necessary to use Shairport Sync.
|
||||
**Possible Issue -- AirPlay**
|
||||
The Windows Firewall is preventing access to Shairport Sync.
|
||||
|
||||
Solution: Allow network discovery. This setting creates a private type network and enables Windows to access the ports and protocols necessary to use Shairport Sync.
|
||||
|
||||
### UFW firewall blocking ports (commonly includes Raspberry Pi)
|
||||
|
||||
@@ -92,7 +106,7 @@ You have installed Shairport Sync successfully, the daemon is running, you can s
|
||||
|
||||
`sudo ufw disable`
|
||||
|
||||
- Try to launch a song from your remote device on the Shairport-sync one, if this works, proceed to the next step and follow the ones described below, in the solution section.
|
||||
- Try to launch a song from your remote device on the Shairport Sync one. If this works, proceed to the next step and follow the ones described below, in the solution section.
|
||||
|
||||
- Enable UFW through the following command:
|
||||
|
||||
@@ -103,16 +117,17 @@ You have installed Shairport Sync successfully, the daemon is running, you can s
|
||||
You have to allow connections to your Shairport Sync device from remote devices. To do so, after re-enabling UFW (see last step of the previous section), enter the following commands in shell:
|
||||
|
||||
```
|
||||
sudo ufw allow from 192.168.1.1/16 to any port 3689 proto tcp
|
||||
sudo ufw allow from 192.168.1.1/16 to any port 5353
|
||||
sudo ufw allow from 192.168.1.1/16 to any port 5000:5005 proto tcp
|
||||
sudo ufw allow from 192.168.1.1/16 to any port 6000:6005 proto udp
|
||||
sudo ufw allow from 192.168.1.1/16 to any port 35000:65535 proto udp
|
||||
sudo ufw allow 319:320/udp
|
||||
sudo ufw allow 3689/tcp
|
||||
sudo ufw allow 5353
|
||||
sudo ufw allow 5000/tcp
|
||||
sudo ufw allow 7000/tcp
|
||||
sudo ufw allow 6000:6009/udp
|
||||
sudo ufw allow 35000:65535/udp
|
||||
```
|
||||
Ports 319 and 320 need to be opened for AirPlay 2 only.
|
||||
|
||||
You may have to change the IP addresses range depending on your own local network settings.
|
||||
|
||||
You can check UFW config by typing `sudo ufw status` in shell. Please make sure that UFW is active, especially if you have deactivated it previously for testing purpose.
|
||||
You can check UFW config by typing `sudo ufw status` in shell. Please make sure that UFW is active, especially if you have deactivated it previously for testing purposes. Check out the [ufw man pages](http://manpages.ubuntu.com/manpages/man8/ufw.8.html) for more.
|
||||
|
||||
Run your song from your remote device. Enjoy !
|
||||
|
||||
@@ -253,11 +268,135 @@ Audio may seem to pause or drop for several seconds. iOS devices may regularly d
|
||||
|
||||
**Possible Solution **
|
||||
|
||||
If none of the above steps completely remove the issue, try increasing the audio backend buffer setting in the backend section of shairport-sync.conf. (This section may vary depending on the value of the "output_backend" setting.)
|
||||
If none of the above steps completely remove the issue, try increasing the audio backend buffer setting in the backend section of shairport-sync.conf. (This section may vary depending on the value of the `"output_backend"` setting.)
|
||||
|
||||
For example:
|
||||
|
||||
````
|
||||
audio_backend_buffer_desired_length = 19845;
|
||||
````
|
||||
Is triple the default for the ALSA backend and effectively solves the above issue with a Pi Zero on a busy network.
|
||||
...is triple the default for the ALSA backend and effectively solves the above issue with a Pi Zero on a busy network.
|
||||
|
||||
### AAC Decoder Issues (AirPlay 2 only)
|
||||
|
||||
To play AirPlay 2 Buffered Audio streams, Shairport Sync needs an AAC decoder capable of decoding Planar Floating Point -- `fltp` -- AAC material. Unfortunately, not all systems have such a decoder. To troubleshoot this, the idea here is to use the [`ffmpeg`](https://www.ffmpeg.org) app, which may already be installed in your system, to list AAC decoders and to check that the default AAC decoder has `fplp` capability. Here is an example of where the AAC decoder _can_ decode `fltp` material. The system is a Raspberry Pi running 64-bit Raspberry Pi OS Lite 11 (Bullseye):
|
||||
```
|
||||
$ ffmpeg -decoders | grep -i aac
|
||||
|
||||
ffmpeg version 4.3.4-0+deb11u1+rpt1 Copyright (c) 2000-2021 the FFmpeg developers
|
||||
built with gcc 10 (Raspbian 10.2.1-6+rpi1)
|
||||
configuration: --prefix=/usr --extra-version=0+deb11u1+rpt1 --toolchain=hardened --incdir=/usr/include/arm-linux-gnueabihf --enable-gpl --disable-stripping --enable-avresample --disable-filter=resample --enable-gnutls --enable-ladspa --enable-libaom --enable-libass --enable-libbluray --enable-libbs2b --enable-libcaca --enable-libcdio --enable-libcodec2 --enable-libdav1d --enable-libflite --enable-libfontconfig --enable-libfreetype --enable-libfribidi --enable-libgme --enable-libgsm --enable-libjack --enable-libmp3lame --enable-libmysofa --enable-libopenjpeg --enable-libopenmpt --enable-libopus --enable-libpulse --enable-librabbitmq --enable-librsvg --enable-librubberband --enable-libshine --enable-libsnappy --enable-libsoxr --enable-libspeex --enable-libsrt --enable-libssh --enable-libtheora --enable-libtwolame --enable-libvidstab --enable-libvorbis --enable-libvpx --enable-libwavpack --enable-libwebp --enable-libx265 --enable-libxml2 --enable-libxvid --enable-libzmq --enable-libzvbi --enable-lv2 --enable-omx --enable-openal --enable-opencl --enable-opengl --enable-sdl2 --enable-mmal --enable-neon --enable-rpi --enable-v4l2-request --enable-libudev --enable-epoxy --enable-pocketsphinx --enable-libdc1394 --enable-libdrm --enable-vout-drm --enable-libiec61883 --enable-chromaprint --enable-frei0r --enable-libx264 --enable-shared --libdir=/usr/lib/arm-linux-gnueabihf --cpu=arm1176jzf-s --arch=arm
|
||||
WARNING: library configuration mismatch
|
||||
avutil configuration: --prefix=/usr --extra-version=0+deb11u1+rpt1 --toolchain=hardened --incdir=/usr/include/arm-linux-gnueabihf --enable-gpl --disable-stripping --enable-avresample --disable-filter=resample --enable-gnutls --enable-ladspa --enable-libaom --enable-libass --enable-libbluray --enable-libbs2b --enable-libcaca --enable-libcdio --enable-libcodec2 --enable-libdav1d --enable-libflite --enable-libfontconfig --enable-libfreetype --enable-libfribidi --enable-libgme --enable-libgsm --enable-libjack --enable-libmp3lame --enable-libmysofa --enable-libopenjpeg --enable-libopenmpt --enable-libopus --enable-libpulse --enable-librabbitmq --enable-librsvg --enable-librubberband --enable-libshine --enable-libsnappy --enable-libsoxr --enable-libspeex --enable-libsrt --enable-libssh --enable-libtheora --enable-libtwolame --enable-libvidstab --enable-libvorbis --enable-libvpx --enable-libwavpack --enable-libwebp --enable-libx265 --enable-libxml2 --enable-libxvid --enable-libzmq --enable-libzvbi --enable-lv2 --enable-omx --enable-openal --enable-opencl --enable-opengl --enable-sdl2 --enable-mmal --enable-neon --enable-rpi --enable-v4l2-request --enable-libudev --enable-epoxy --enable-pocketsphinx --enable-libdc1394 --enable-libdrm --enable-vout-drm --enable-libiec61883 --enable-chromaprint --enable-frei0r --enable-libx264 --libdir=/usr/lib/arm-linux-gnueabihf/neon/vfp --cpu=cortex-a7 --arch=armv6t2 --disable-thumb --enable-shared --disable-doc --disable-programs
|
||||
avcodec configuration: --prefix=/usr --extra-version=0+deb11u1+rpt1 --toolchain=hardened --incdir=/usr/include/arm-linux-gnueabihf --enable-gpl --disable-stripping --enable-avresample --disable-filter=resample --enable-gnutls --enable-ladspa --enable-libaom --enable-libass --enable-libbluray --enable-libbs2b --enable-libcaca --enable-libcdio --enable-libcodec2 --enable-libdav1d --enable-libflite --enable-libfontconfig --enable-libfreetype --enable-libfribidi --enable-libgme --enable-libgsm --enable-libjack --enable-libmp3lame --enable-libmysofa --enable-libopenjpeg --enable-libopenmpt --enable-libopus --enable-libpulse --enable-librabbitmq --enable-librsvg --enable-librubberband --enable-libshine --enable-libsnappy --enable-libsoxr --enable-libspeex --enable-libsrt --enable-libssh --enable-libtheora --enable-libtwolame --enable-libvidstab --enable-libvorbis --enable-libvpx --enable-libwavpack --enable-libwebp --enable-libx265 --enable-libxml2 --enable-libxvid --enable-libzmq --enable-libzvbi --enable-lv2 --enable-omx --enable-openal --enable-opencl --enable-opengl --enable-sdl2 --enable-mmal --enable-neon --enable-rpi --enable-v4l2-request --enable-libudev --enable-epoxy --enable-pocketsphinx --enable-libdc1394 --enable-libdrm --enable-vout-drm --enable-libiec61883 --enable-chromaprint --enable-frei0r --enable-libx264 --libdir=/usr/lib/arm-linux-gnueabihf/neon/vfp --cpu=cortex-a7 --arch=armv6t2 --disable-thumb --enable-shared --disable-doc --disable-programs
|
||||
avformat configuration: --prefix=/usr --extra-version=0+deb11u1+rpt1 --toolchain=hardened --incdir=/usr/include/arm-linux-gnueabihf --enable-gpl --disable-stripping --enable-avresample --disable-filter=resample --enable-gnutls --enable-ladspa --enable-libaom --enable-libass --enable-libbluray --enable-libbs2b --enable-libcaca --enable-libcdio --enable-libcodec2 --enable-libdav1d --enable-libflite --enable-libfontconfig --enable-libfreetype --enable-libfribidi --enable-libgme --enable-libgsm --enable-libjack --enable-libmp3lame --enable-libmysofa --enable-libopenjpeg --enable-libopenmpt --enable-libopus --enable-libpulse --enable-librabbitmq --enable-librsvg --enable-librubberband --enable-libshine --enable-libsnappy --enable-libsoxr --enable-libspeex --enable-libsrt --enable-libssh --enable-libtheora --enable-libtwolame --enable-libvidstab --enable-libvorbis --enable-libvpx --enable-libwavpack --enable-libwebp --enable-libx265 --enable-libxml2 --enable-libxvid --enable-libzmq --enable-libzvbi --enable-lv2 --enable-omx --enable-openal --enable-opencl --enable-opengl --enable-sdl2 --enable-mmal --enable-neon --enable-rpi --enable-v4l2-request --enable-libudev --enable-epoxy --enable-pocketsphinx --enable-libdc1394 --enable-libdrm --enable-vout-drm --enable-libiec61883 --enable-chromaprint --enable-frei0r --enable-libx264 --libdir=/usr/lib/arm-linux-gnueabihf/neon/vfp --cpu=cortex-a7 --arch=armv6t2 --disable-thumb --enable-shared --disable-doc --disable-programs
|
||||
avdevice configuration: --prefix=/usr --extra-version=0+deb11u1+rpt1 --toolchain=hardened --incdir=/usr/include/arm-linux-gnueabihf --enable-gpl --disable-stripping --enable-avresample --disable-filter=resample --enable-gnutls --enable-ladspa --enable-libaom --enable-libass --enable-libbluray --enable-libbs2b --enable-libcaca --enable-libcdio --enable-libcodec2 --enable-libdav1d --enable-libflite --enable-libfontconfig --enable-libfreetype --enable-libfribidi --enable-libgme --enable-libgsm --enable-libjack --enable-libmp3lame --enable-libmysofa --enable-libopenjpeg --enable-libopenmpt --enable-libopus --enable-libpulse --enable-librabbitmq --enable-librsvg --enable-librubberband --enable-libshine --enable-libsnappy --enable-libsoxr --enable-libspeex --enable-libsrt --enable-libssh --enable-libtheora --enable-libtwolame --enable-libvidstab --enable-libvorbis --enable-libvpx --enable-libwavpack --enable-libwebp --enable-libx265 --enable-libxml2 --enable-libxvid --enable-libzmq --enable-libzvbi --enable-lv2 --enable-omx --enable-openal --enable-opencl --enable-opengl --enable-sdl2 --enable-mmal --enable-neon --enable-rpi --enable-v4l2-request --enable-libudev --enable-epoxy --enable-pocketsphinx --enable-libdc1394 --enable-libdrm --enable-vout-drm --enable-libiec61883 --enable-chromaprint --enable-frei0r --enable-libx264 --libdir=/usr/lib/arm-linux-gnueabihf/neon/vfp --cpu=cortex-a7 --arch=armv6t2 --disable-thumb --enable-shared --disable-doc --disable-programs
|
||||
avfilter configuration: --prefix=/usr --extra-version=0+deb11u1+rpt1 --toolchain=hardened --incdir=/usr/include/arm-linux-gnueabihf --enable-gpl --disable-stripping --enable-avresample --disable-filter=resample --enable-gnutls --enable-ladspa --enable-libaom --enable-libass --enable-libbluray --enable-libbs2b --enable-libcaca --enable-libcdio --enable-libcodec2 --enable-libdav1d --enable-libflite --enable-libfontconfig --enable-libfreetype --enable-libfribidi --enable-libgme --enable-libgsm --enable-libjack --enable-libmp3lame --enable-libmysofa --enable-libopenjpeg --enable-libopenmpt --enable-libopus --enable-libpulse --enable-librabbitmq --enable-librsvg --enable-librubberband --enable-libshine --enable-libsnappy --enable-libsoxr --enable-libspeex --enable-libsrt --enable-libssh --enable-libtheora --enable-libtwolame --enable-libvidstab --enable-libvorbis --enable-libvpx --enable-libwavpack --enable-libwebp --enable-libx265 --enable-libxml2 --enable-libxvid --enable-libzmq --enable-libzvbi --enable-lv2 --enable-omx --enable-openal --enable-opencl --enable-opengl --enable-sdl2 --enable-mmal --enable-neon --enable-rpi --enable-v4l2-request --enable-libudev --enable-epoxy --enable-pocketsphinx --enable-libdc1394 --enable-libdrm --enable-vout-drm --enable-libiec61883 --enable-chromaprint --enable-frei0r --enable-libx264 --libdir=/usr/lib/arm-linux-gnueabihf/neon/vfp --cpu=cortex-a7 --arch=armv6t2 --disable-thumb --enable-shared --disable-doc --disable-programs
|
||||
avresample configuration: --prefix=/usr --extra-version=0+deb11u1+rpt1 --toolchain=hardened --incdir=/usr/include/arm-linux-gnueabihf --enable-gpl --disable-stripping --enable-avresample --disable-filter=resample --enable-gnutls --enable-ladspa --enable-libaom --enable-libass --enable-libbluray --enable-libbs2b --enable-libcaca --enable-libcdio --enable-libcodec2 --enable-libdav1d --enable-libflite --enable-libfontconfig --enable-libfreetype --enable-libfribidi --enable-libgme --enable-libgsm --enable-libjack --enable-libmp3lame --enable-libmysofa --enable-libopenjpeg --enable-libopenmpt --enable-libopus --enable-libpulse --enable-librabbitmq --enable-librsvg --enable-librubberband --enable-libshine --enable-libsnappy --enable-libsoxr --enable-libspeex --enable-libsrt --enable-libssh --enable-libtheora --enable-libtwolame --enable-libvidstab --enable-libvorbis --enable-libvpx --enable-libwavpack --enable-libwebp --enable-libx265 --enable-libxml2 --enable-libxvid --enable-libzmq --enable-libzvbi --enable-lv2 --enable-omx --enable-openal --enable-opencl --enable-opengl --enable-sdl2 --enable-mmal --enable-neon --enable-rpi --enable-v4l2-request --enable-libudev --enable-epoxy --enable-pocketsphinx --enable-libdc1394 --enable-libdrm --enable-vout-drm --enable-libiec61883 --enable-chromaprint --enable-frei0r --enable-libx264 --libdir=/usr/lib/arm-linux-gnueabihf/neon/vfp --cpu=cortex-a7 --arch=armv6t2 --disable-thumb --enable-shared --disable-doc --disable-programs
|
||||
swscale configuration: --prefix=/usr --extra-version=0+deb11u1+rpt1 --toolchain=hardened --incdir=/usr/include/arm-linux-gnueabihf --enable-gpl --disable-stripping --enable-avresample --disable-filter=resample --enable-gnutls --enable-ladspa --enable-libaom --enable-libass --enable-libbluray --enable-libbs2b --enable-libcaca --enable-libcdio --enable-libcodec2 --enable-libdav1d --enable-libflite --enable-libfontconfig --enable-libfreetype --enable-libfribidi --enable-libgme --enable-libgsm --enable-libjack --enable-libmp3lame --enable-libmysofa --enable-libopenjpeg --enable-libopenmpt --enable-libopus --enable-libpulse --enable-librabbitmq --enable-librsvg --enable-librubberband --enable-libshine --enable-libsnappy --enable-libsoxr --enable-libspeex --enable-libsrt --enable-libssh --enable-libtheora --enable-libtwolame --enable-libvidstab --enable-libvorbis --enable-libvpx --enable-libwavpack --enable-libwebp --enable-libx265 --enable-libxml2 --enable-libxvid --enable-libzmq --enable-libzvbi --enable-lv2 --enable-omx --enable-openal --enable-opencl --enable-opengl --enable-sdl2 --enable-mmal --enable-neon --enable-rpi --enable-v4l2-request --enable-libudev --enable-epoxy --enable-pocketsphinx --enable-libdc1394 --enable-libdrm --enable-vout-drm --enable-libiec61883 --enable-chromaprint --enable-frei0r --enable-libx264 --libdir=/usr/lib/arm-linux-gnueabihf/neon/vfp --cpu=cortex-a7 --arch=armv6t2 --disable-thumb --enable-shared --disable-doc --disable-programs
|
||||
swresample configuration: --prefix=/usr --extra-version=0+deb11u1+rpt1 --toolchain=hardened --incdir=/usr/include/arm-linux-gnueabihf --enable-gpl --disable-stripping --enable-avresample --disable-filter=resample --enable-gnutls --enable-ladspa --enable-libaom --enable-libass --enable-libbluray --enable-libbs2b --enable-libcaca --enable-libcdio --enable-libcodec2 --enable-libdav1d --enable-libflite --enable-libfontconfig --enable-libfreetype --enable-libfribidi --enable-libgme --enable-libgsm --enable-libjack --enable-libmp3lame --enable-libmysofa --enable-libopenjpeg --enable-libopenmpt --enable-libopus --enable-libpulse --enable-librabbitmq --enable-librsvg --enable-librubberband --enable-libshine --enable-libsnappy --enable-libsoxr --enable-libspeex --enable-libsrt --enable-libssh --enable-libtheora --enable-libtwolame --enable-libvidstab --enable-libvorbis --enable-libvpx --enable-libwavpack --enable-libwebp --enable-libx265 --enable-libxml2 --enable-libxvid --enable-libzmq --enable-libzvbi --enable-lv2 --enable-omx --enable-openal --enable-opencl --enable-opengl --enable-sdl2 --enable-mmal --enable-neon --enable-rpi --enable-v4l2-request --enable-libudev --enable-epoxy --enable-pocketsphinx --enable-libdc1394 --enable-libdrm --enable-vout-drm --enable-libiec61883 --enable-chromaprint --enable-frei0r --enable-libx264 --libdir=/usr/lib/arm-linux-gnueabihf/neon/vfp --cpu=cortex-a7 --arch=armv6t2 --disable-thumb --enable-shared --disable-doc --disable-programs
|
||||
postproc configuration: --prefix=/usr --extra-version=0+deb11u1+rpt1 --toolchain=hardened --incdir=/usr/include/arm-linux-gnueabihf --enable-gpl --disable-stripping --enable-avresample --disable-filter=resample --enable-gnutls --enable-ladspa --enable-libaom --enable-libass --enable-libbluray --enable-libbs2b --enable-libcaca --enable-libcdio --enable-libcodec2 --enable-libdav1d --enable-libflite --enable-libfontconfig --enable-libfreetype --enable-libfribidi --enable-libgme --enable-libgsm --enable-libjack --enable-libmp3lame --enable-libmysofa --enable-libopenjpeg --enable-libopenmpt --enable-libopus --enable-libpulse --enable-librabbitmq --enable-librsvg --enable-librubberband --enable-libshine --enable-libsnappy --enable-libsoxr --enable-libspeex --enable-libsrt --enable-libssh --enable-libtheora --enable-libtwolame --enable-libvidstab --enable-libvorbis --enable-libvpx --enable-libwavpack --enable-libwebp --enable-libx265 --enable-libxml2 --enable-libxvid --enable-libzmq --enable-libzvbi --enable-lv2 --enable-omx --enable-openal --enable-opencl --enable-opengl --enable-sdl2 --enable-mmal --enable-neon --enable-rpi --enable-v4l2-request --enable-libudev --enable-epoxy --enable-pocketsphinx --enable-libdc1394 --enable-libdrm --enable-vout-drm --enable-libiec61883 --enable-chromaprint --enable-frei0r --enable-libx264 --libdir=/usr/lib/arm-linux-gnueabihf/neon/vfp --cpu=cortex-a7 --arch=armv6t2 --disable-thumb --enable-shared --disable-doc --disable-programs
|
||||
libavutil 56. 51.100 / 56. 51.100
|
||||
libavcodec 58. 91.100 / 58. 91.100
|
||||
libavformat 58. 45.100 / 58. 45.100
|
||||
libavdevice 58. 10.100 / 58. 10.100
|
||||
libavfilter 7. 85.100 / 7. 85.100
|
||||
libavresample 4. 0. 0 / 4. 0. 0
|
||||
libswscale 5. 7.100 / 5. 7.100
|
||||
libswresample 3. 7.100 / 3. 7.100
|
||||
libpostproc 55. 7.100 / 55. 7.100
|
||||
A....D aac AAC (Advanced Audio Coding)
|
||||
A....D aac_fixed AAC (Advanced Audio Coding) (codec aac)
|
||||
A....D aac_latm AAC LATM (Advanced Audio Coding LATM syntax)
|
||||
```
|
||||
At the bottom you can see three decoders. The first one, called `aac`, is the decoder chosen by Shairport Sync. Checking its capabilities:
|
||||
|
||||
```
|
||||
$ ffmpeg -h decoder=aac
|
||||
ffmpeg version 4.3.4-0+deb11u1+rpt1 Copyright (c) 2000-2021 the FFmpeg developers
|
||||
built with gcc 10 (Raspbian 10.2.1-6+rpi1)
|
||||
configuration: --prefix=/usr --extra-version=0+deb11u1+rpt1 --toolchain=hardened --incdir=/usr/include/arm-linux-gnueabihf --enable-gpl --disable-stripping --enable-avresample --disable-filter=resample --enable-gnutls --enable-ladspa --enable-libaom --enable-libass --enable-libbluray --enable-libbs2b --enable-libcaca --enable-libcdio --enable-libcodec2 --enable-libdav1d --enable-libflite --enable-libfontconfig --enable-libfreetype --enable-libfribidi --enable-libgme --enable-libgsm --enable-libjack --enable-libmp3lame --enable-libmysofa --enable-libopenjpeg --enable-libopenmpt --enable-libopus --enable-libpulse --enable-librabbitmq --enable-librsvg --enable-librubberband --enable-libshine --enable-libsnappy --enable-libsoxr --enable-libspeex --enable-libsrt --enable-libssh --enable-libtheora --enable-libtwolame --enable-libvidstab --enable-libvorbis --enable-libvpx --enable-libwavpack --enable-libwebp --enable-libx265 --enable-libxml2 --enable-libxvid --enable-libzmq --enable-libzvbi --enable-lv2 --enable-omx --enable-openal --enable-opencl --enable-opengl --enable-sdl2 --enable-mmal --enable-neon --enable-rpi --enable-v4l2-request --enable-libudev --enable-epoxy --enable-pocketsphinx --enable-libdc1394 --enable-libdrm --enable-vout-drm --enable-libiec61883 --enable-chromaprint --enable-frei0r --enable-libx264 --enable-shared --libdir=/usr/lib/arm-linux-gnueabihf --cpu=arm1176jzf-s --arch=arm
|
||||
WARNING: library configuration mismatch
|
||||
avutil configuration: --prefix=/usr --extra-version=0+deb11u1+rpt1 --toolchain=hardened --incdir=/usr/include/arm-linux-gnueabihf --enable-gpl --disable-stripping --enable-avresample --disable-filter=resample --enable-gnutls --enable-ladspa --enable-libaom --enable-libass --enable-libbluray --enable-libbs2b --enable-libcaca --enable-libcdio --enable-libcodec2 --enable-libdav1d --enable-libflite --enable-libfontconfig --enable-libfreetype --enable-libfribidi --enable-libgme --enable-libgsm --enable-libjack --enable-libmp3lame --enable-libmysofa --enable-libopenjpeg --enable-libopenmpt --enable-libopus --enable-libpulse --enable-librabbitmq --enable-librsvg --enable-librubberband --enable-libshine --enable-libsnappy --enable-libsoxr --enable-libspeex --enable-libsrt --enable-libssh --enable-libtheora --enable-libtwolame --enable-libvidstab --enable-libvorbis --enable-libvpx --enable-libwavpack --enable-libwebp --enable-libx265 --enable-libxml2 --enable-libxvid --enable-libzmq --enable-libzvbi --enable-lv2 --enable-omx --enable-openal --enable-opencl --enable-opengl --enable-sdl2 --enable-mmal --enable-neon --enable-rpi --enable-v4l2-request --enable-libudev --enable-epoxy --enable-pocketsphinx --enable-libdc1394 --enable-libdrm --enable-vout-drm --enable-libiec61883 --enable-chromaprint --enable-frei0r --enable-libx264 --libdir=/usr/lib/arm-linux-gnueabihf/neon/vfp --cpu=cortex-a7 --arch=armv6t2 --disable-thumb --enable-shared --disable-doc --disable-programs
|
||||
avcodec configuration: --prefix=/usr --extra-version=0+deb11u1+rpt1 --toolchain=hardened --incdir=/usr/include/arm-linux-gnueabihf --enable-gpl --disable-stripping --enable-avresample --disable-filter=resample --enable-gnutls --enable-ladspa --enable-libaom --enable-libass --enable-libbluray --enable-libbs2b --enable-libcaca --enable-libcdio --enable-libcodec2 --enable-libdav1d --enable-libflite --enable-libfontconfig --enable-libfreetype --enable-libfribidi --enable-libgme --enable-libgsm --enable-libjack --enable-libmp3lame --enable-libmysofa --enable-libopenjpeg --enable-libopenmpt --enable-libopus --enable-libpulse --enable-librabbitmq --enable-librsvg --enable-librubberband --enable-libshine --enable-libsnappy --enable-libsoxr --enable-libspeex --enable-libsrt --enable-libssh --enable-libtheora --enable-libtwolame --enable-libvidstab --enable-libvorbis --enable-libvpx --enable-libwavpack --enable-libwebp --enable-libx265 --enable-libxml2 --enable-libxvid --enable-libzmq --enable-libzvbi --enable-lv2 --enable-omx --enable-openal --enable-opencl --enable-opengl --enable-sdl2 --enable-mmal --enable-neon --enable-rpi --enable-v4l2-request --enable-libudev --enable-epoxy --enable-pocketsphinx --enable-libdc1394 --enable-libdrm --enable-vout-drm --enable-libiec61883 --enable-chromaprint --enable-frei0r --enable-libx264 --libdir=/usr/lib/arm-linux-gnueabihf/neon/vfp --cpu=cortex-a7 --arch=armv6t2 --disable-thumb --enable-shared --disable-doc --disable-programs
|
||||
avformat configuration: --prefix=/usr --extra-version=0+deb11u1+rpt1 --toolchain=hardened --incdir=/usr/include/arm-linux-gnueabihf --enable-gpl --disable-stripping --enable-avresample --disable-filter=resample --enable-gnutls --enable-ladspa --enable-libaom --enable-libass --enable-libbluray --enable-libbs2b --enable-libcaca --enable-libcdio --enable-libcodec2 --enable-libdav1d --enable-libflite --enable-libfontconfig --enable-libfreetype --enable-libfribidi --enable-libgme --enable-libgsm --enable-libjack --enable-libmp3lame --enable-libmysofa --enable-libopenjpeg --enable-libopenmpt --enable-libopus --enable-libpulse --enable-librabbitmq --enable-librsvg --enable-librubberband --enable-libshine --enable-libsnappy --enable-libsoxr --enable-libspeex --enable-libsrt --enable-libssh --enable-libtheora --enable-libtwolame --enable-libvidstab --enable-libvorbis --enable-libvpx --enable-libwavpack --enable-libwebp --enable-libx265 --enable-libxml2 --enable-libxvid --enable-libzmq --enable-libzvbi --enable-lv2 --enable-omx --enable-openal --enable-opencl --enable-opengl --enable-sdl2 --enable-mmal --enable-neon --enable-rpi --enable-v4l2-request --enable-libudev --enable-epoxy --enable-pocketsphinx --enable-libdc1394 --enable-libdrm --enable-vout-drm --enable-libiec61883 --enable-chromaprint --enable-frei0r --enable-libx264 --libdir=/usr/lib/arm-linux-gnueabihf/neon/vfp --cpu=cortex-a7 --arch=armv6t2 --disable-thumb --enable-shared --disable-doc --disable-programs
|
||||
avdevice configuration: --prefix=/usr --extra-version=0+deb11u1+rpt1 --toolchain=hardened --incdir=/usr/include/arm-linux-gnueabihf --enable-gpl --disable-stripping --enable-avresample --disable-filter=resample --enable-gnutls --enable-ladspa --enable-libaom --enable-libass --enable-libbluray --enable-libbs2b --enable-libcaca --enable-libcdio --enable-libcodec2 --enable-libdav1d --enable-libflite --enable-libfontconfig --enable-libfreetype --enable-libfribidi --enable-libgme --enable-libgsm --enable-libjack --enable-libmp3lame --enable-libmysofa --enable-libopenjpeg --enable-libopenmpt --enable-libopus --enable-libpulse --enable-librabbitmq --enable-librsvg --enable-librubberband --enable-libshine --enable-libsnappy --enable-libsoxr --enable-libspeex --enable-libsrt --enable-libssh --enable-libtheora --enable-libtwolame --enable-libvidstab --enable-libvorbis --enable-libvpx --enable-libwavpack --enable-libwebp --enable-libx265 --enable-libxml2 --enable-libxvid --enable-libzmq --enable-libzvbi --enable-lv2 --enable-omx --enable-openal --enable-opencl --enable-opengl --enable-sdl2 --enable-mmal --enable-neon --enable-rpi --enable-v4l2-request --enable-libudev --enable-epoxy --enable-pocketsphinx --enable-libdc1394 --enable-libdrm --enable-vout-drm --enable-libiec61883 --enable-chromaprint --enable-frei0r --enable-libx264 --libdir=/usr/lib/arm-linux-gnueabihf/neon/vfp --cpu=cortex-a7 --arch=armv6t2 --disable-thumb --enable-shared --disable-doc --disable-programs
|
||||
avfilter configuration: --prefix=/usr --extra-version=0+deb11u1+rpt1 --toolchain=hardened --incdir=/usr/include/arm-linux-gnueabihf --enable-gpl --disable-stripping --enable-avresample --disable-filter=resample --enable-gnutls --enable-ladspa --enable-libaom --enable-libass --enable-libbluray --enable-libbs2b --enable-libcaca --enable-libcdio --enable-libcodec2 --enable-libdav1d --enable-libflite --enable-libfontconfig --enable-libfreetype --enable-libfribidi --enable-libgme --enable-libgsm --enable-libjack --enable-libmp3lame --enable-libmysofa --enable-libopenjpeg --enable-libopenmpt --enable-libopus --enable-libpulse --enable-librabbitmq --enable-librsvg --enable-librubberband --enable-libshine --enable-libsnappy --enable-libsoxr --enable-libspeex --enable-libsrt --enable-libssh --enable-libtheora --enable-libtwolame --enable-libvidstab --enable-libvorbis --enable-libvpx --enable-libwavpack --enable-libwebp --enable-libx265 --enable-libxml2 --enable-libxvid --enable-libzmq --enable-libzvbi --enable-lv2 --enable-omx --enable-openal --enable-opencl --enable-opengl --enable-sdl2 --enable-mmal --enable-neon --enable-rpi --enable-v4l2-request --enable-libudev --enable-epoxy --enable-pocketsphinx --enable-libdc1394 --enable-libdrm --enable-vout-drm --enable-libiec61883 --enable-chromaprint --enable-frei0r --enable-libx264 --libdir=/usr/lib/arm-linux-gnueabihf/neon/vfp --cpu=cortex-a7 --arch=armv6t2 --disable-thumb --enable-shared --disable-doc --disable-programs
|
||||
avresample configuration: --prefix=/usr --extra-version=0+deb11u1+rpt1 --toolchain=hardened --incdir=/usr/include/arm-linux-gnueabihf --enable-gpl --disable-stripping --enable-avresample --disable-filter=resample --enable-gnutls --enable-ladspa --enable-libaom --enable-libass --enable-libbluray --enable-libbs2b --enable-libcaca --enable-libcdio --enable-libcodec2 --enable-libdav1d --enable-libflite --enable-libfontconfig --enable-libfreetype --enable-libfribidi --enable-libgme --enable-libgsm --enable-libjack --enable-libmp3lame --enable-libmysofa --enable-libopenjpeg --enable-libopenmpt --enable-libopus --enable-libpulse --enable-librabbitmq --enable-librsvg --enable-librubberband --enable-libshine --enable-libsnappy --enable-libsoxr --enable-libspeex --enable-libsrt --enable-libssh --enable-libtheora --enable-libtwolame --enable-libvidstab --enable-libvorbis --enable-libvpx --enable-libwavpack --enable-libwebp --enable-libx265 --enable-libxml2 --enable-libxvid --enable-libzmq --enable-libzvbi --enable-lv2 --enable-omx --enable-openal --enable-opencl --enable-opengl --enable-sdl2 --enable-mmal --enable-neon --enable-rpi --enable-v4l2-request --enable-libudev --enable-epoxy --enable-pocketsphinx --enable-libdc1394 --enable-libdrm --enable-vout-drm --enable-libiec61883 --enable-chromaprint --enable-frei0r --enable-libx264 --libdir=/usr/lib/arm-linux-gnueabihf/neon/vfp --cpu=cortex-a7 --arch=armv6t2 --disable-thumb --enable-shared --disable-doc --disable-programs
|
||||
swscale configuration: --prefix=/usr --extra-version=0+deb11u1+rpt1 --toolchain=hardened --incdir=/usr/include/arm-linux-gnueabihf --enable-gpl --disable-stripping --enable-avresample --disable-filter=resample --enable-gnutls --enable-ladspa --enable-libaom --enable-libass --enable-libbluray --enable-libbs2b --enable-libcaca --enable-libcdio --enable-libcodec2 --enable-libdav1d --enable-libflite --enable-libfontconfig --enable-libfreetype --enable-libfribidi --enable-libgme --enable-libgsm --enable-libjack --enable-libmp3lame --enable-libmysofa --enable-libopenjpeg --enable-libopenmpt --enable-libopus --enable-libpulse --enable-librabbitmq --enable-librsvg --enable-librubberband --enable-libshine --enable-libsnappy --enable-libsoxr --enable-libspeex --enable-libsrt --enable-libssh --enable-libtheora --enable-libtwolame --enable-libvidstab --enable-libvorbis --enable-libvpx --enable-libwavpack --enable-libwebp --enable-libx265 --enable-libxml2 --enable-libxvid --enable-libzmq --enable-libzvbi --enable-lv2 --enable-omx --enable-openal --enable-opencl --enable-opengl --enable-sdl2 --enable-mmal --enable-neon --enable-rpi --enable-v4l2-request --enable-libudev --enable-epoxy --enable-pocketsphinx --enable-libdc1394 --enable-libdrm --enable-vout-drm --enable-libiec61883 --enable-chromaprint --enable-frei0r --enable-libx264 --libdir=/usr/lib/arm-linux-gnueabihf/neon/vfp --cpu=cortex-a7 --arch=armv6t2 --disable-thumb --enable-shared --disable-doc --disable-programs
|
||||
swresample configuration: --prefix=/usr --extra-version=0+deb11u1+rpt1 --toolchain=hardened --incdir=/usr/include/arm-linux-gnueabihf --enable-gpl --disable-stripping --enable-avresample --disable-filter=resample --enable-gnutls --enable-ladspa --enable-libaom --enable-libass --enable-libbluray --enable-libbs2b --enable-libcaca --enable-libcdio --enable-libcodec2 --enable-libdav1d --enable-libflite --enable-libfontconfig --enable-libfreetype --enable-libfribidi --enable-libgme --enable-libgsm --enable-libjack --enable-libmp3lame --enable-libmysofa --enable-libopenjpeg --enable-libopenmpt --enable-libopus --enable-libpulse --enable-librabbitmq --enable-librsvg --enable-librubberband --enable-libshine --enable-libsnappy --enable-libsoxr --enable-libspeex --enable-libsrt --enable-libssh --enable-libtheora --enable-libtwolame --enable-libvidstab --enable-libvorbis --enable-libvpx --enable-libwavpack --enable-libwebp --enable-libx265 --enable-libxml2 --enable-libxvid --enable-libzmq --enable-libzvbi --enable-lv2 --enable-omx --enable-openal --enable-opencl --enable-opengl --enable-sdl2 --enable-mmal --enable-neon --enable-rpi --enable-v4l2-request --enable-libudev --enable-epoxy --enable-pocketsphinx --enable-libdc1394 --enable-libdrm --enable-vout-drm --enable-libiec61883 --enable-chromaprint --enable-frei0r --enable-libx264 --libdir=/usr/lib/arm-linux-gnueabihf/neon/vfp --cpu=cortex-a7 --arch=armv6t2 --disable-thumb --enable-shared --disable-doc --disable-programs
|
||||
postproc configuration: --prefix=/usr --extra-version=0+deb11u1+rpt1 --toolchain=hardened --incdir=/usr/include/arm-linux-gnueabihf --enable-gpl --disable-stripping --enable-avresample --disable-filter=resample --enable-gnutls --enable-ladspa --enable-libaom --enable-libass --enable-libbluray --enable-libbs2b --enable-libcaca --enable-libcdio --enable-libcodec2 --enable-libdav1d --enable-libflite --enable-libfontconfig --enable-libfreetype --enable-libfribidi --enable-libgme --enable-libgsm --enable-libjack --enable-libmp3lame --enable-libmysofa --enable-libopenjpeg --enable-libopenmpt --enable-libopus --enable-libpulse --enable-librabbitmq --enable-librsvg --enable-librubberband --enable-libshine --enable-libsnappy --enable-libsoxr --enable-libspeex --enable-libsrt --enable-libssh --enable-libtheora --enable-libtwolame --enable-libvidstab --enable-libvorbis --enable-libvpx --enable-libwavpack --enable-libwebp --enable-libx265 --enable-libxml2 --enable-libxvid --enable-libzmq --enable-libzvbi --enable-lv2 --enable-omx --enable-openal --enable-opencl --enable-opengl --enable-sdl2 --enable-mmal --enable-neon --enable-rpi --enable-v4l2-request --enable-libudev --enable-epoxy --enable-pocketsphinx --enable-libdc1394 --enable-libdrm --enable-vout-drm --enable-libiec61883 --enable-chromaprint --enable-frei0r --enable-libx264 --libdir=/usr/lib/arm-linux-gnueabihf/neon/vfp --cpu=cortex-a7 --arch=armv6t2 --disable-thumb --enable-shared --disable-doc --disable-programs
|
||||
libavutil 56. 51.100 / 56. 51.100
|
||||
libavcodec 58. 91.100 / 58. 91.100
|
||||
libavformat 58. 45.100 / 58. 45.100
|
||||
libavdevice 58. 10.100 / 58. 10.100
|
||||
libavfilter 7. 85.100 / 7. 85.100
|
||||
libavresample 4. 0. 0 / 4. 0. 0
|
||||
libswscale 5. 7.100 / 5. 7.100
|
||||
libswresample 3. 7.100 / 3. 7.100
|
||||
libpostproc 55. 7.100 / 55. 7.100
|
||||
Decoder aac [AAC (Advanced Audio Coding)]:
|
||||
General capabilities: dr1 chconf
|
||||
Threading capabilities: none
|
||||
Supported sample formats: fltp
|
||||
Supported channel layouts: mono stereo 3.0 4.0 5.0 5.1 7.1(wide)
|
||||
AAC decoder AVOptions:
|
||||
-dual_mono_mode <int> .D..A..... Select the channel to decode for dual mono (from -1 to 2) (default auto)
|
||||
auto -1 .D..A..... autoselection
|
||||
main 1 .D..A..... Select Main/Left channel
|
||||
sub 2 .D..A..... Select Sub/Right channel
|
||||
both 0 .D..A..... Select both channels
|
||||
```
|
||||
You can see the line:
|
||||
```
|
||||
Supported sample formats: fltp
|
||||
```
|
||||
towards the bottom. This means that the AAC decoder is suitable for use by Shairport Sync
|
||||
|
||||
Here are the same commands, performed on a system which does not support `fltp` decoding:
|
||||
```
|
||||
$ ffmpeg -decoders | grep -i aac
|
||||
ffmpeg version 5.0.1 Copyright (c) 2000-2022 the FFmpeg developers
|
||||
built with gcc 12 (GCC)
|
||||
configuration: --prefix=/usr --bindir=/usr/bin --datadir=/usr/share/ffmpeg --docdir=/usr/share/doc/ffmpeg --incdir=/usr/include/ffmpeg --libdir=/usr/lib64 --mandir=/usr/share/man --arch=x86_64 --optflags='-O2 -flto=auto -ffat-lto-objects -fexceptions -g -grecord-gcc-switches -pipe -Wall -Werror=format-security -Wp,-D_FORTIFY_SOURCE=2 -Wp,-D_GLIBCXX_ASSERTIONS -specs=/usr/lib/rpm/redhat/redhat-hardened-cc1 -fstack-protector-strong -specs=/usr/lib/rpm/redhat/redhat-annobin-cc1 -m64 -mtune=generic -fasynchronous-unwind-tables -fstack-clash-protection -fcf-protection' --extra-ldflags='-Wl,-z,relro -Wl,--as-needed -Wl,-z,now -specs=/usr/lib/rpm/redhat/redhat-hardened-ld -specs=/usr/lib/rpm/redhat/redhat-annobin-cc1 -Wl,--build-id=sha1 -Wl,-dT,/builddir/build/BUILD/ffmpeg-5.0.1/.package_note-ffmpeg-5.0.1-6.fc36.x86_64.ld' --disable-htmlpages --enable-pic --disable-stripping --enable-shared --disable-static --enable-gpl --enable-version3 --enable-libsmbclient --disable-openssl --enable-bzlib --enable-frei0r --enable-chromaprint --enable-gcrypt --enable-gnutls --enable-ladspa --enable-libshaderc --enable-vulkan --disable-cuda-sdk --enable-libaom --enable-libass --enable-libbluray --enable-libbs2b --enable-libcdio --enable-libcodec2 --enable-libdav1d --enable-libdc1394 --enable-libdrm --enable-libfdk-aac --enable-libfontconfig --enable-libfreetype --enable-libfribidi --enable-libgsm --enable-libilbc --enable-libjack --enable-libmodplug --enable-libmp3lame --enable-libmysofa --enable-libopenh264-dlopen --enable-libopenjpeg --enable-libopenmpt --enable-libopus --enable-libpulse --enable-librav1e --enable-librsvg --enable-librubberband --enable-libsnappy --enable-libsvtav1 --enable-libsoxr --enable-libspeex --enable-libssh --enable-libsrt --enable-libtesseract --enable-libtheora --enable-libtwolame --enable-libvidstab --enable-libvmaf --enable-libvorbis --enable-libv4l2 --enable-libvpx --enable-libwebp --enable-libxml2 --enable-libzimg --enable-libzmq --enable-libzvbi --enable-lto --enable-libmfx --enable-vaapi --enable-vdpau --enable-openal --enable-opencl --enable-opengl --enable-pthreads --enable-vapoursynth --enable-muxers --enable-demuxers --enable-hwaccels --disable-encoders --disable-decoders --disable-decoder='mpeg4,h263,h264,hevc,vc1' --enable-encoder=',libfdk_aac,ac3,apng,ass,ayuv,bmp,ffv1,ffvhuff,flac,gif,h263_v4l2m2m,h264_amf,h264_nvenc,h264_qsv,h264_v4l2m2m,h264_vaapi,hevc_amf,hevc_nvenc,hevc_qsv,hevc_v4l2m2m,hevc_vaapi,huffyuv,ilbc,jpegls,jpeg2000,libaom,libaom_av1,libcodec2,libgsm,libilbc,libmp3lame,libopenh264,libopenjpeg,libopus,librav1e,libschroedinger,libspeex,libsvtav1,libtheora,libtwolame,libvorbis,libvpx_vp8,libvpx_vp9,libwebp,libwebp_anim,mjpeg,mjpeg_qsv,mjpeg_vaapi,mp2,mp2fixed,mpeg1video,mpeg2video,mpeg2_qsv,mpeg2_vaapi,mpeg4_v4l2m2m,opus,pam,pbm,pcm_alaw,pcm_f32be,pcm_f32le,pcm_f64be,pcm_f64le,pcm_mulaw,pcm_s16be,pcm_s16be_planar,pcm_s16le,pcm_s16le_planar,pcm_s24be,pcm_s24le,pcm_s24le_planar,pcm_s32be,pcm_s32le,pcm_s32le_planar,pcm_s8,pcm_s8_planar,pcm_u16be,pcm_u16le,pcm_u24be,pcm_u24le,pcm_u32be,pcm_u32le,pcm_u8,pcx,pgm,pgmyuv,png,ppm,rawvideo,sgi,srt,ssa,sunrast,targa,text,tiff,v210,v308,v408,v410,vc1_qsv,vc1_v4l2m2m,vorbis,vp8_qsv,vp8_v4l2m2m,vp8_vaapi,vp9_qsv,vp9_vaapi,webvtt,wrapped_avframe,xbm,xwd,y41p,yuv4,zlib,' --enable-decoder=',libfdk_aac,ac3,ansi,apng,ass,av1_qsv,ayuv,bmp,dirac,exr,ffv1,ffvhuff,ffwavesynth,flac,gif,gsm,huffyuv,ilbc,jpeg2000,libaom,libaom_av1,libcodec2,libdav1d,libgsm,libilbc,libopenh264,libopenjpeg,libopus,libschroedinger,libspeex,libvorbis,libvpx_vp8,libvpx_vp9,mjpeg,mjpeg_qsv,mp1,mp1float,mp2,mp2float,mp3,mp3float,mpeg1video,mpeg1_v4l2m2m,mpeg2video,mpeg2_qsv,mpeg2_v4l2m2m,opus,pam,pbm,pcm_alaw,pcm_bluray,pcm_dvd,pcm_f32be,pcm_f32le,pcm_f64be,pcm_f64le,pcm_mulaw,pcm_s16be,pcm_s16be_planar,pcm_s16le,pcm_s16le_planar,pcm_s24be,pcm_s24le,pcm_s24le_planar,pcm_s32be,pcm_s32le,pcm_s32le_planar,pcm_s8,pcm_s8_planar,pcm_u16be,pcm_u16le,pcm_u24be,pcm_u24le,pcm_u32be,pcm_u32le,pcm_u8,pcx,pgm,pgmyuv,pgssub,pgx,png,ppm,rawvideo,sgi,srt,ssa,sunrast,targa,text,theora,tiff,v210,v210x,v308,v408,v410,vorbis,vp3,vp5,vp6,vp6a,vp6f,vp8,vp8_qsv,vp8_v4l2m2m,vp9,vp9_qsv,vp9_v4l2m2m,webp,webvtt,wrapped_avframe,xbm,xwd,y41p,yuv4,zlib,'
|
||||
libavutil 57. 17.100 / 57. 17.100
|
||||
libavcodec 59. 18.100 / 59. 18.100
|
||||
libavformat 59. 16.100 / 59. 16.100
|
||||
libavdevice 59. 4.100 / 59. 4.100
|
||||
libavfilter 8. 24.100 / 8. 24.100
|
||||
libswscale 6. 4.100 / 6. 4.100
|
||||
libswresample 4. 3.100 / 4. 3.100
|
||||
libpostproc 56. 3.100 / 56. 3.100
|
||||
A....D libfdk_aac Fraunhofer FDK AAC (codec aac)
|
||||
```
|
||||
As you can see just one AAC decoder in available, the `Fraunhofer FDK AAC`. Examining it more closely:
|
||||
```
|
||||
$ ffmpeg -h decoder=aac
|
||||
ffmpeg version 5.0.1 Copyright (c) 2000-2022 the FFmpeg developers
|
||||
built with gcc 12 (GCC)
|
||||
configuration: --prefix=/usr --bindir=/usr/bin --datadir=/usr/share/ffmpeg --docdir=/usr/share/doc/ffmpeg --incdir=/usr/include/ffmpeg --libdir=/usr/lib64 --mandir=/usr/share/man --arch=x86_64 --optflags='-O2 -flto=auto -ffat-lto-objects -fexceptions -g -grecord-gcc-switches -pipe -Wall -Werror=format-security -Wp,-D_FORTIFY_SOURCE=2 -Wp,-D_GLIBCXX_ASSERTIONS -specs=/usr/lib/rpm/redhat/redhat-hardened-cc1 -fstack-protector-strong -specs=/usr/lib/rpm/redhat/redhat-annobin-cc1 -m64 -mtune=generic -fasynchronous-unwind-tables -fstack-clash-protection -fcf-protection' --extra-ldflags='-Wl,-z,relro -Wl,--as-needed -Wl,-z,now -specs=/usr/lib/rpm/redhat/redhat-hardened-ld -specs=/usr/lib/rpm/redhat/redhat-annobin-cc1 -Wl,--build-id=sha1 -Wl,-dT,/builddir/build/BUILD/ffmpeg-5.0.1/.package_note-ffmpeg-5.0.1-6.fc36.x86_64.ld' --disable-htmlpages --enable-pic --disable-stripping --enable-shared --disable-static --enable-gpl --enable-version3 --enable-libsmbclient --disable-openssl --enable-bzlib --enable-frei0r --enable-chromaprint --enable-gcrypt --enable-gnutls --enable-ladspa --enable-libshaderc --enable-vulkan --disable-cuda-sdk --enable-libaom --enable-libass --enable-libbluray --enable-libbs2b --enable-libcdio --enable-libcodec2 --enable-libdav1d --enable-libdc1394 --enable-libdrm --enable-libfdk-aac --enable-libfontconfig --enable-libfreetype --enable-libfribidi --enable-libgsm --enable-libilbc --enable-libjack --enable-libmodplug --enable-libmp3lame --enable-libmysofa --enable-libopenh264-dlopen --enable-libopenjpeg --enable-libopenmpt --enable-libopus --enable-libpulse --enable-librav1e --enable-librsvg --enable-librubberband --enable-libsnappy --enable-libsvtav1 --enable-libsoxr --enable-libspeex --enable-libssh --enable-libsrt --enable-libtesseract --enable-libtheora --enable-libtwolame --enable-libvidstab --enable-libvmaf --enable-libvorbis --enable-libv4l2 --enable-libvpx --enable-libwebp --enable-libxml2 --enable-libzimg --enable-libzmq --enable-libzvbi --enable-lto --enable-libmfx --enable-vaapi --enable-vdpau --enable-openal --enable-opencl --enable-opengl --enable-pthreads --enable-vapoursynth --enable-muxers --enable-demuxers --enable-hwaccels --disable-encoders --disable-decoders --disable-decoder='mpeg4,h263,h264,hevc,vc1' --enable-encoder=',libfdk_aac,ac3,apng,ass,ayuv,bmp,ffv1,ffvhuff,flac,gif,h263_v4l2m2m,h264_amf,h264_nvenc,h264_qsv,h264_v4l2m2m,h264_vaapi,hevc_amf,hevc_nvenc,hevc_qsv,hevc_v4l2m2m,hevc_vaapi,huffyuv,ilbc,jpegls,jpeg2000,libaom,libaom_av1,libcodec2,libgsm,libilbc,libmp3lame,libopenh264,libopenjpeg,libopus,librav1e,libschroedinger,libspeex,libsvtav1,libtheora,libtwolame,libvorbis,libvpx_vp8,libvpx_vp9,libwebp,libwebp_anim,mjpeg,mjpeg_qsv,mjpeg_vaapi,mp2,mp2fixed,mpeg1video,mpeg2video,mpeg2_qsv,mpeg2_vaapi,mpeg4_v4l2m2m,opus,pam,pbm,pcm_alaw,pcm_f32be,pcm_f32le,pcm_f64be,pcm_f64le,pcm_mulaw,pcm_s16be,pcm_s16be_planar,pcm_s16le,pcm_s16le_planar,pcm_s24be,pcm_s24le,pcm_s24le_planar,pcm_s32be,pcm_s32le,pcm_s32le_planar,pcm_s8,pcm_s8_planar,pcm_u16be,pcm_u16le,pcm_u24be,pcm_u24le,pcm_u32be,pcm_u32le,pcm_u8,pcx,pgm,pgmyuv,png,ppm,rawvideo,sgi,srt,ssa,sunrast,targa,text,tiff,v210,v308,v408,v410,vc1_qsv,vc1_v4l2m2m,vorbis,vp8_qsv,vp8_v4l2m2m,vp8_vaapi,vp9_qsv,vp9_vaapi,webvtt,wrapped_avframe,xbm,xwd,y41p,yuv4,zlib,' --enable-decoder=',libfdk_aac,ac3,ansi,apng,ass,av1_qsv,ayuv,bmp,dirac,exr,ffv1,ffvhuff,ffwavesynth,flac,gif,gsm,huffyuv,ilbc,jpeg2000,libaom,libaom_av1,libcodec2,libdav1d,libgsm,libilbc,libopenh264,libopenjpeg,libopus,libschroedinger,libspeex,libvorbis,libvpx_vp8,libvpx_vp9,mjpeg,mjpeg_qsv,mp1,mp1float,mp2,mp2float,mp3,mp3float,mpeg1video,mpeg1_v4l2m2m,mpeg2video,mpeg2_qsv,mpeg2_v4l2m2m,opus,pam,pbm,pcm_alaw,pcm_bluray,pcm_dvd,pcm_f32be,pcm_f32le,pcm_f64be,pcm_f64le,pcm_mulaw,pcm_s16be,pcm_s16be_planar,pcm_s16le,pcm_s16le_planar,pcm_s24be,pcm_s24le,pcm_s24le_planar,pcm_s32be,pcm_s32le,pcm_s32le_planar,pcm_s8,pcm_s8_planar,pcm_u16be,pcm_u16le,pcm_u24be,pcm_u24le,pcm_u32be,pcm_u32le,pcm_u8,pcx,pgm,pgmyuv,pgssub,pgx,png,ppm,rawvideo,sgi,srt,ssa,sunrast,targa,text,theora,tiff,v210,v210x,v308,v408,v410,vorbis,vp3,vp5,vp6,vp6a,vp6f,vp8,vp8_qsv,vp8_v4l2m2m,vp9,vp9_qsv,vp9_v4l2m2m,webp,webvtt,wrapped_avframe,xbm,xwd,y41p,yuv4,zlib,'
|
||||
libavutil 57. 17.100 / 57. 17.100
|
||||
libavcodec 59. 18.100 / 59. 18.100
|
||||
libavformat 59. 16.100 / 59. 16.100
|
||||
libavdevice 59. 4.100 / 59. 4.100
|
||||
libavfilter 8. 24.100 / 8. 24.100
|
||||
libswscale 6. 4.100 / 6. 4.100
|
||||
libswresample 4. 3.100 / 4. 3.100
|
||||
libpostproc 56. 3.100 / 56. 3.100
|
||||
Decoder libfdk_aac [Fraunhofer FDK AAC]:
|
||||
General capabilities: dr1 chconf
|
||||
Threading capabilities: none
|
||||
libfdk-aac decoder AVOptions:
|
||||
-conceal <int> .D..A...... Error concealment method (from 0 to 2) (default noise)
|
||||
spectral 0 .D..A...... Spectral muting
|
||||
noise 1 .D..A...... Noise Substitution
|
||||
energy 2 .D..A...... Energy Interpolation
|
||||
-drc_boost <int> .D..A...... Dynamic Range Control: boost, where [0] is none and [127] is max boost (from -1 to 127) (default -1)
|
||||
-drc_cut <int> .D..A...... Dynamic Range Control: attenuation factor, where [0] is none and [127] is max compression (from -1 to 127) (default -1)
|
||||
-drc_level <int> .D..A...... Dynamic Range Control: reference level, quantized to 0.25dB steps where [0] is 0dB and [127] is -31.75dB, -1 for auto, and -2 for disabled (from -2 to 127) (default -1)
|
||||
-drc_heavy <int> .D..A...... Dynamic Range Control: heavy compression, where [1] is on (RF mode) and [0] is off (from -1 to 1) (default -1)
|
||||
-level_limit <boolean> .D..A...... Signal level limiting (default auto)
|
||||
-drc_effect <int> .D..A...... Dynamic Range Control: effect type, where e.g. [0] is none and [6] is general (from -1 to 8) (default -1)
|
||||
|
||||
```
|
||||
It does not indicate that it supports `fltp` decoding (and, indeed, it definitely does not support it). The decoder is [based on fixed-point arithmetic](https://wiki.hydrogenaud.io/index.php?title=Fraunhofer_FDK_AAC), and will [only accept 16-bit PCM input](https://wiki.hydrogenaud.io/index.php?title=Fraunhofer_FDK_AAC#Sample_Format) (presumably for encoding, not decoding, which is our focus), so this is not unexpected.
|
||||
|
||||
+1
-84
@@ -1,86 +1,3 @@
|
||||
|
||||
### Updating Shairport Sync
|
||||
This guide is for building and updating an installation of Shairport Sync. If you wish to build an installation of Shairport Sync to replace an installation that came from a package, this doesn't really apply – instead, please follow the guide at [INSTALL.md](https://github.com/mikebrady/shairport-sync/blob/master/INSTALL.md).
|
||||
|
||||
To do an update, you basically have to go through the whole process of building Shairport Sync again,
|
||||
but a few steps are shorter because you've done them before; you won't have to reinstall the build tools or libraries needed, and you won't have to define the user and group or reconfigure the settings in the configuration file.
|
||||
|
||||
But before you begin, you should update and upgrade any packages.
|
||||
|
||||
Note that in this guide, a `$` prefix means you should be in the normal user mode; a `#` prefix means you should be in the superuser or root mode.
|
||||
|
||||
Here is the sequence for Raspberry Pi OS (Buster), which is based on Debian Buster. The same commands work for Ubuntu, and maybe more. Here, a non-`root` user with `sudo` privileges is assumed.
|
||||
|
||||
```
|
||||
$ sudo apt-get update
|
||||
$ sudo apt-get upgrade
|
||||
```
|
||||
Next, stop playing music to your device.
|
||||
|
||||
### Remove Old Copies
|
||||
It's a good idea to search for and remove any existing copies of the application, called `shairport-sync`. Use the command `$ which shairport-sync` to find them. For example, if `shairport-sync` has been installed previously, this might happen:
|
||||
```
|
||||
$ which shairport-sync
|
||||
/usr/local/bin/shairport-sync
|
||||
```
|
||||
Remove it as follows:
|
||||
```
|
||||
# rm /usr/local/bin/shairport-sync
|
||||
```
|
||||
Do this until no more copies of `shairport-sync` are found. This is especially important if you are building Shairport Sync over an old version that was installed from packages, as the built application will be installed in a different directory to the packaged installation.
|
||||
|
||||
### Remove Old Startup and Service Scripts
|
||||
You should also remove the startup script and service definition files `/etc/systemd/system/shairport-sync.service`, `/lib/systemd/system/shairport-sync.service`, `/etc/init.d/shairport-sync`, `/etc/dbus-1/system.d/shairport-sync-dbus.conf` and `/etc/dbus-1/system.d/shairport-sync-mpris.conf` if they exist – new ones will be installed if necessary. As with the previous section, this is especially important if you are building Shairport Sync over an old version that was installed from packages, as the scripts for the built application may be different to those of the packaged version. If they are left in place, bad things can happen.
|
||||
|
||||
### Reboot after Cleaning Up
|
||||
If you removed any installations of Shairport Sync or any of its startup script files in the last two steps, you should reboot.
|
||||
|
||||
### Building the Updated Shairport Sync
|
||||
Now, to build, update and install Shairport Sync, if you still have the directory in which you previously built Shairport Sync, it will contain the repository you originally downloaded. Navigate your way to it and 'pull' the changes from GitHub:
|
||||
|
||||
```
|
||||
$ git pull
|
||||
```
|
||||
Otherwise – say if you deleted the repository – just pull Shairport Sync from GitHub again and move into the new directory:
|
||||
```
|
||||
$ git clone https://github.com/mikebrady/shairport-sync.git
|
||||
$ cd shairport-sync
|
||||
```
|
||||
(If you wish to use the `development` branch, you should enter the command `$ git checkout development` at this point. Everything else is the same.)
|
||||
|
||||
Now, while in the `shairport-sync` directory, perform the following commands (note that there is a choice you must make in there):
|
||||
```
|
||||
$ autoreconf -fi
|
||||
```
|
||||
Please review the release notes to see if any configuration settings have been changed. For instance, in the transitions from version 2 to version 3, the `--with-ssl=polarssl` has been deprecated in favour of `--with-ssl=mbedtls`.
|
||||
```
|
||||
#The following is the standard configuration for a Linux that uses the alsa backend and systemd initialisation system:
|
||||
$ ./configure --with-alsa --with-avahi --with-ssl=openssl --with-metadata --with-soxr --with-systemd --sysconfdir=/etc
|
||||
#OR
|
||||
#The following is the standard configuration for a Linux that uses the alsa backend and the older System V initialisation system:
|
||||
$ ./configure --with-libdaemon --with-alsa --with-avahi --with-ssl=openssl --with-metadata --with-soxr --with-systemv --sysconfdir=/etc
|
||||
|
||||
$ make
|
||||
$ sudo make install
|
||||
```
|
||||
At this point you have downloaded, compiled and installed the updated Shairport Sync. However, the older version is still running. So, you need to do a little more:
|
||||
|
||||
If you are on a `systemd`-based system such as Raspbian Jessie or recent versions of Ubuntu and Debian, execute the following commands:
|
||||
```
|
||||
$ sudo systemctl daemon-reload
|
||||
$ sudo systemctl restart shairport-sync
|
||||
```
|
||||
Otherwise execute the following command:
|
||||
```
|
||||
$ sudo service shairport-sync restart
|
||||
```
|
||||
|
||||
Your Shairport Sync should be upgraded now.
|
||||
|
||||
### Post Update Tasks
|
||||
1 **Unmute the Output Device Mixer** (This applies only if you are using a hardware mixer for volume control.) Certain older versions of Shairport Sync could leave the output device mixer in a muted state after use to minimise the possibility of noise. However, this is not generally compatible with other audio players using the same device, as they would generally expect the device to be unmuted. Recent versions of Shairport Sync therefore do not use the mute facility by default. When you update Shairport Sync, the output device mixer might have been muted by the previous version, so you should unmute it, using, for instance, the following command:
|
||||
```
|
||||
$ sudo amixer sset <mixer_name> unmute
|
||||
```
|
||||
Alternatively you can use `alsamixer`. A muted output has the letter(s) `M` as its value. Select it and type `M` again to unmute.
|
||||
|
||||
This guide has been superseded by the general building guide at [BUILD.md](https://github.com/mikebrady/shairport-sync/blob/development/BUILD.md).
|
||||
|
||||
+24
-38
@@ -74,11 +74,8 @@ void going_active(int block) {
|
||||
#ifdef CONFIG_DBUS_INTERFACE
|
||||
if (dbus_service_is_running())
|
||||
shairport_sync_set_disable_standby(SHAIRPORT_SYNC(shairportSyncSkeleton), TRUE);
|
||||
else
|
||||
config.keep_dac_busy = 1;
|
||||
#else
|
||||
config.keep_dac_busy = 1;
|
||||
#endif
|
||||
config.keep_dac_busy = 1;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -101,21 +98,18 @@ void going_inactive(int block) {
|
||||
#ifdef CONFIG_DBUS_INTERFACE
|
||||
if (dbus_service_is_running())
|
||||
shairport_sync_set_disable_standby(SHAIRPORT_SYNC(shairportSyncSkeleton), FALSE);
|
||||
else
|
||||
config.keep_dac_busy = 0;
|
||||
#else
|
||||
config.keep_dac_busy = 0;
|
||||
#endif
|
||||
config.keep_dac_busy = 0;
|
||||
}
|
||||
}
|
||||
|
||||
void activity_monitor_signify_activity(int active) {
|
||||
// this could be pthread_cancelled and they is likely to be cancellation points in the
|
||||
// this could be pthread_cancelled and there is likely to be cancellation points in the
|
||||
// hooked-on procedures
|
||||
pthread_cleanup_debug_mutex_lock(&activity_monitor_mutex, 10000, 1);
|
||||
pthread_mutex_lock(&activity_monitor_mutex);
|
||||
player_state = active == 0 ? ps_inactive : ps_active;
|
||||
// Now, although we could simply let the state machine in the activity monitor thread
|
||||
// look after eveything, we will change state here in two situations:
|
||||
// look after everything, we will change state here in two situations:
|
||||
// 1. If the state machine is am_inactive and the player is ps_active
|
||||
// we will change the state to am_active and execute the going_active() function.
|
||||
// 2. If the state machine is am_active and the player is ps_inactive and
|
||||
@@ -126,18 +120,23 @@ void activity_monitor_signify_activity(int active) {
|
||||
// and wait for them to complete before continuing. If they were performed in the
|
||||
// activity monitor thread, then we couldn't wait for them to complete.
|
||||
|
||||
// Thus, the only time the thread will execute a going_... function is when a non-zero
|
||||
// timeout actually matures.
|
||||
// So, if the active end procedure is on a timer, it will be executed when the
|
||||
// timeout occurs and the "blocking" status is ignored.
|
||||
|
||||
if ((state == am_inactive) && (player_state == ps_active)) {
|
||||
going_active(
|
||||
config.cmd_blocking); // note -- will be executed with the mutex locked, but that's okay
|
||||
state = am_active;
|
||||
pthread_mutex_unlock(&activity_monitor_mutex);
|
||||
going_active(config.cmd_blocking);
|
||||
} else if ((state == am_active) && (player_state == ps_inactive) &&
|
||||
(config.active_state_timeout == 0.0)) {
|
||||
going_inactive(
|
||||
config.cmd_blocking); // note -- will be executed with the mutex locked, but that's okay
|
||||
state = am_inactive;
|
||||
pthread_mutex_unlock(&activity_monitor_mutex);
|
||||
going_inactive(config.cmd_blocking);
|
||||
} else {
|
||||
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_cond_signal(&activity_monitor_cv);
|
||||
pthread_cleanup_pop(1); // release the mutex
|
||||
}
|
||||
@@ -153,18 +152,7 @@ void *activity_monitor_thread_code(void *arg) {
|
||||
if (rc)
|
||||
die("activity_monitor: error %d initialising activity_monitor_mutex.", rc);
|
||||
|
||||
// set the flowcontrol condition variable to wait on a monotonic clock
|
||||
#ifdef COMPILE_FOR_LINUX_AND_FREEBSD_AND_CYGWIN_AND_OPENBSD
|
||||
pthread_condattr_t attr;
|
||||
pthread_condattr_init(&attr);
|
||||
pthread_condattr_setclock(&attr, CLOCK_MONOTONIC); // can't do this in OS X, and don't need it.
|
||||
rc = pthread_cond_init(&activity_monitor_cv, &attr);
|
||||
pthread_condattr_destroy(&attr);
|
||||
|
||||
#endif
|
||||
#ifdef COMPILE_FOR_OSX
|
||||
rc = pthread_cond_init(&activity_monitor_cv, NULL);
|
||||
#endif
|
||||
if (rc)
|
||||
die("activity_monitor: error %d initialising activity_monitor_cv.");
|
||||
pthread_cleanup_push(activity_thread_cleanup_handler, arg);
|
||||
@@ -180,26 +168,25 @@ void *activity_monitor_thread_code(void *arg) {
|
||||
do {
|
||||
switch (state) {
|
||||
case am_inactive:
|
||||
// debug(1,"am_state: am_inactive");
|
||||
debug(2, "am_state: am_inactive");
|
||||
while (player_state != ps_active)
|
||||
pthread_cond_wait(&activity_monitor_cv, &activity_monitor_mutex);
|
||||
state = am_active;
|
||||
// going_active(); // this is done in activity_monitor_signify_activity
|
||||
// state = am_active; this is done by the activity_monitor_signify_activity(1) function
|
||||
debug(2, "am_state: am_active");
|
||||
break;
|
||||
case am_active:
|
||||
// debug(1,"am_state: am_active");
|
||||
while (player_state != ps_inactive)
|
||||
pthread_cond_wait(&activity_monitor_cv, &activity_monitor_mutex);
|
||||
if (config.active_state_timeout == 0.0) {
|
||||
state = am_inactive;
|
||||
// going_inactive(); // this is done in activity_monitor_signify_activity
|
||||
} else {
|
||||
|
||||
// if it's not already am_inactive, the it should be beginning to time out...
|
||||
if (state != am_inactive) {
|
||||
state = am_timing_out;
|
||||
|
||||
uint64_t time_to_wait_for_wakeup_ns = (uint64_t)(config.active_state_timeout * 1000000000);
|
||||
|
||||
#ifdef COMPILE_FOR_LINUX_AND_FREEBSD_AND_CYGWIN_AND_OPENBSD
|
||||
uint64_t time_of_wakeup_ns = get_absolute_time_in_ns() + time_to_wait_for_wakeup_ns;
|
||||
uint64_t time_of_wakeup_ns = get_realtime_in_ns() + time_to_wait_for_wakeup_ns;
|
||||
sec = time_of_wakeup_ns / 1000000000;
|
||||
nsec = time_of_wakeup_ns % 1000000000;
|
||||
time_for_wait.tv_sec = sec;
|
||||
@@ -215,7 +202,6 @@ void *activity_monitor_thread_code(void *arg) {
|
||||
}
|
||||
break;
|
||||
case am_timing_out:
|
||||
// debug(1,"am_state: am_timing_out");
|
||||
rc = 0;
|
||||
while ((player_state != ps_active) && (rc != ETIMEDOUT)) {
|
||||
#ifdef COMPILE_FOR_LINUX_AND_FREEBSD_AND_CYGWIN_AND_OPENBSD
|
||||
|
||||
@@ -43,6 +43,9 @@ extern audio_output audio_ao;
|
||||
#ifdef CONFIG_SOUNDIO
|
||||
extern audio_output audio_soundio;
|
||||
#endif
|
||||
#ifdef CONFIG_PW
|
||||
extern audio_output audio_pw;
|
||||
#endif
|
||||
#ifdef CONFIG_PA
|
||||
extern audio_output audio_pa;
|
||||
#endif
|
||||
@@ -66,6 +69,9 @@ static audio_output *outputs[] = {
|
||||
#ifdef CONFIG_SNDIO
|
||||
&audio_sndio,
|
||||
#endif
|
||||
#ifdef CONFIG_PW
|
||||
&audio_pw,
|
||||
#endif
|
||||
#ifdef CONFIG_PA
|
||||
&audio_pa,
|
||||
#endif
|
||||
|
||||
@@ -4,6 +4,19 @@
|
||||
#include <libconfig.h>
|
||||
#include <stdint.h>
|
||||
|
||||
// clang-format off
|
||||
// Play samples provided may be from the source, in which case they will be timed
|
||||
// or they may be generated by Shairport Sync, in which case they will not be timed.
|
||||
|
||||
// Typically these would be samples of silence, which may be dithered, sent during the lead-in to
|
||||
// the start of the material, or inserted instead of a missing frame, or after a flush.
|
||||
// clang-format on
|
||||
|
||||
typedef enum {
|
||||
play_samples_are_untimed = 0, // typically the samples are (possibly dithered) silence
|
||||
play_samples_are_timed, // timed and numbered.
|
||||
} play_samples_type;
|
||||
|
||||
typedef struct {
|
||||
double current_volume_dB;
|
||||
int32_t minimum_volume_dB;
|
||||
@@ -24,7 +37,7 @@ typedef struct {
|
||||
void (*start)(int sample_rate, int sample_format);
|
||||
|
||||
// block of samples
|
||||
int (*play)(void *buf, int samples);
|
||||
int (*play)(void *buf, int samples, int sample_type, uint32_t timestamp, uint64_t playtime);
|
||||
void (*stop)(void);
|
||||
|
||||
// may be null if no implemented
|
||||
@@ -39,8 +52,9 @@ typedef struct {
|
||||
// will change dynamically, so keep watching it. Implemented in ALSA only.
|
||||
// returns a negative error code if there's a problem
|
||||
int (*delay)(long *the_delay); // snd_pcm_sframes_t is a signed long
|
||||
int (*rate_info)(uint64_t *elapsed_time,
|
||||
uint64_t *frames_played); // use this to get the true rate of the DAC
|
||||
int (*stats)(uint64_t *raw_measurement_time, uint64_t *corrected_measurement_time,
|
||||
uint64_t *delay,
|
||||
uint64_t *frames_sent_to_dac); // use this to get the true rate of the DAC
|
||||
|
||||
// may be NULL, in which case soft volume is applied
|
||||
void (*volume)(double vol);
|
||||
|
||||
+418
-329
File diff suppressed because it is too large
Load Diff
+110
-63
@@ -1,6 +1,7 @@
|
||||
/*
|
||||
* libao output driver. This file is part of Shairport.
|
||||
* Copyright (c) James Laird 2013
|
||||
* Copyright (c) Mike Brady 2014 -- 2022
|
||||
* All rights reserved.
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person
|
||||
@@ -32,98 +33,143 @@
|
||||
#include <unistd.h>
|
||||
|
||||
ao_device *dev = NULL;
|
||||
ao_option *ao_opts = NULL;
|
||||
ao_sample_format fmt;
|
||||
int driver = 0;
|
||||
|
||||
static void help(void) {
|
||||
printf(" -d driver set the output driver\n"
|
||||
" -o name=value set an arbitrary ao option\n"
|
||||
" -i id shorthand for -o id=<id>\n"
|
||||
" -n name shorthand for -o dev=<name> -o dsp=<name>\n");
|
||||
// get a list of drivers available
|
||||
ao_initialize();
|
||||
int defaultDriver = ao_default_driver_id();
|
||||
if (defaultDriver == -1) {
|
||||
printf(" No usable drivers available.\n");
|
||||
} else {
|
||||
ao_info *defaultDriverInfo = ao_driver_info(defaultDriver);
|
||||
int driver_count;
|
||||
ao_info **driver_list = ao_driver_info_list(&driver_count);
|
||||
int i = 0;
|
||||
if (driver_count == 0) {
|
||||
printf(" Driver list unavailable.\n");
|
||||
} else {
|
||||
printf(" Drivers:\n");
|
||||
for (i = 0; i < driver_count; i++) {
|
||||
ao_info *the_driver = driver_list[i];
|
||||
if (strcmp(the_driver->short_name, defaultDriverInfo->short_name) == 0)
|
||||
printf(" \"%s\" (default)\n", the_driver->short_name);
|
||||
else
|
||||
printf(" \"%s\"\n", the_driver->short_name);
|
||||
}
|
||||
}
|
||||
}
|
||||
ao_shutdown();
|
||||
}
|
||||
|
||||
static int init(int argc, char **argv) {
|
||||
// const char *str;
|
||||
// int value;
|
||||
// double dvalue;
|
||||
ao_initialize();
|
||||
int driver = ao_default_driver_id();
|
||||
ao_option *ao_opts = NULL;
|
||||
driver = ao_default_driver_id();
|
||||
if (driver == -1) {
|
||||
warn("libao can not find a usable driver!");
|
||||
} else {
|
||||
|
||||
// set up default values first
|
||||
// set up default values first
|
||||
|
||||
config.audio_backend_buffer_desired_length = 1.0;
|
||||
config.audio_backend_latency_offset = 0;
|
||||
config.audio_backend_buffer_desired_length = 1.0;
|
||||
config.audio_backend_latency_offset = 0;
|
||||
|
||||
// get settings from settings file first, allow them to be overridden by
|
||||
// command line options
|
||||
// get settings from settings file first, allow them to be overridden by
|
||||
// command line options
|
||||
|
||||
// do the "general" audio options. Note, these options are in the "general" stanza!
|
||||
parse_general_audio_options();
|
||||
// do the "general" audio options. Note, these options are in the "general" stanza!
|
||||
parse_general_audio_options();
|
||||
|
||||
optind = 1; // optind=0 is equivalent to optind=1 plus special behaviour
|
||||
argv--; // so we shift the arguments to satisfy getopt()
|
||||
argc++;
|
||||
optind = 1; // optind=0 is equivalent to optind=1 plus special behaviour
|
||||
argv--; // so we shift the arguments to satisfy getopt()
|
||||
argc++;
|
||||
|
||||
// some platforms apparently require optreset = 1; - which?
|
||||
int opt;
|
||||
char *mid;
|
||||
while ((opt = getopt(argc, argv, "d:i:n:o:")) > 0) {
|
||||
switch (opt) {
|
||||
case 'd':
|
||||
driver = ao_driver_id(optarg);
|
||||
if (driver < 0)
|
||||
die("could not find ao driver %s", optarg);
|
||||
break;
|
||||
case 'i':
|
||||
ao_append_option(&ao_opts, "id", optarg);
|
||||
break;
|
||||
case 'n':
|
||||
ao_append_option(&ao_opts, "dev", optarg);
|
||||
// Old libao versions (for example, 0.8.8) only support
|
||||
// "dsp" instead of "dev".
|
||||
ao_append_option(&ao_opts, "dsp", optarg);
|
||||
break;
|
||||
case 'o':
|
||||
mid = strchr(optarg, '=');
|
||||
if (!mid)
|
||||
die("Expected an = in audio option %s", optarg);
|
||||
*mid = 0;
|
||||
ao_append_option(&ao_opts, optarg, mid + 1);
|
||||
break;
|
||||
default:
|
||||
help();
|
||||
die("Invalid audio option -%c specified", opt);
|
||||
// some platforms apparently require optreset = 1; - which?
|
||||
int opt;
|
||||
char *mid;
|
||||
while ((opt = getopt(argc, argv, "d:i:n:o:")) > 0) {
|
||||
switch (opt) {
|
||||
case 'd':
|
||||
driver = ao_driver_id(optarg);
|
||||
if (driver < 0)
|
||||
die("could not find ao driver %s", optarg);
|
||||
break;
|
||||
case 'i':
|
||||
ao_append_option(&ao_opts, "id", optarg);
|
||||
break;
|
||||
case 'n':
|
||||
ao_append_option(&ao_opts, "dev", optarg);
|
||||
// Old libao versions (for example, 0.8.8) only support
|
||||
// "dsp" instead of "dev".
|
||||
ao_append_option(&ao_opts, "dsp", optarg);
|
||||
break;
|
||||
case 'o':
|
||||
mid = strchr(optarg, '=');
|
||||
if (!mid)
|
||||
die("Expected an = in audio option %s", optarg);
|
||||
*mid = 0;
|
||||
ao_append_option(&ao_opts, optarg, mid + 1);
|
||||
break;
|
||||
default:
|
||||
help();
|
||||
die("Invalid audio option -%c specified", opt);
|
||||
}
|
||||
}
|
||||
|
||||
if (optind < argc)
|
||||
die("Invalid audio argument: %s", argv[optind]);
|
||||
|
||||
memset(&fmt, 0, sizeof(fmt));
|
||||
|
||||
fmt.bits = 16;
|
||||
fmt.rate = 44100;
|
||||
fmt.channels = 2;
|
||||
fmt.byte_format = AO_FMT_NATIVE;
|
||||
}
|
||||
|
||||
if (optind < argc)
|
||||
die("Invalid audio argument: %s", argv[optind]);
|
||||
|
||||
ao_sample_format fmt;
|
||||
memset(&fmt, 0, sizeof(fmt));
|
||||
|
||||
fmt.bits = 16;
|
||||
fmt.rate = 44100;
|
||||
fmt.channels = 2;
|
||||
fmt.byte_format = AO_FMT_NATIVE;
|
||||
|
||||
dev = ao_open_live(driver, &fmt, ao_opts);
|
||||
|
||||
return dev ? 0 : 1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void deinit(void) {
|
||||
if (dev)
|
||||
if (dev != NULL)
|
||||
ao_close(dev);
|
||||
dev = NULL;
|
||||
ao_shutdown();
|
||||
}
|
||||
|
||||
static void start(__attribute__((unused)) int sample_rate,
|
||||
__attribute__((unused)) int sample_format) {}
|
||||
__attribute__((unused)) int sample_format) {
|
||||
// debug(1,"libao start");
|
||||
}
|
||||
|
||||
static int play(void *buf, int samples) { return ao_play(dev, buf, samples * 4); }
|
||||
static int play(void *buf, int samples, __attribute__((unused)) int sample_type,
|
||||
__attribute__((unused)) uint32_t timestamp,
|
||||
__attribute__((unused)) uint64_t playtime) {
|
||||
int response = 0;
|
||||
int oldState;
|
||||
pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, &oldState); // make this un-cancellable
|
||||
if (driver != -1) {
|
||||
if (dev == NULL)
|
||||
dev = ao_open_live(driver, &fmt, ao_opts);
|
||||
if (dev != NULL)
|
||||
response = ao_play(dev, buf, samples * 4);
|
||||
}
|
||||
pthread_setcancelstate(oldState, NULL);
|
||||
return response;
|
||||
}
|
||||
|
||||
static void stop(void) {}
|
||||
static void stop(void) {
|
||||
// debug(1,"libao stop");
|
||||
if (dev != NULL) {
|
||||
ao_close(dev);
|
||||
dev = NULL;
|
||||
}
|
||||
}
|
||||
|
||||
audio_output audio_ao = {.name = "ao",
|
||||
.help = &help,
|
||||
@@ -135,6 +181,7 @@ audio_output audio_ao = {.name = "ao",
|
||||
.is_running = NULL,
|
||||
.flush = NULL,
|
||||
.delay = NULL,
|
||||
.stats = NULL,
|
||||
.play = &play,
|
||||
.volume = NULL,
|
||||
.parameters = NULL,
|
||||
|
||||
+22
-2
@@ -4,7 +4,7 @@
|
||||
* All rights reserved.
|
||||
*
|
||||
* Modifications for audio synchronisation
|
||||
* and related work, copyright (c) Mike Brady 2014
|
||||
* and related work, copyright (c) Mike Brady 2014 -- 2022
|
||||
* All rights reserved.
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person
|
||||
@@ -30,6 +30,7 @@
|
||||
|
||||
#include "audio.h"
|
||||
#include "common.h"
|
||||
#include <inttypes.h> // for PRId64 and friends
|
||||
#include <stdio.h>
|
||||
#include <sys/time.h>
|
||||
#include <unistd.h>
|
||||
@@ -41,8 +42,26 @@ static void deinit(void) {}
|
||||
static void start(int sample_rate, __attribute__((unused)) int sample_format) {
|
||||
debug(1, "dummy audio output started at %d frames per second.", sample_rate);
|
||||
}
|
||||
// clang-format off
|
||||
// Here is a brief explanation of the parameters:
|
||||
// sample_type is 'play_samples_are_untimed' or 'play_samples_are_timed' defined in audio.h.
|
||||
// untimed samples are typically silence used by Shairport Sync itself to set up synchronisation or to fill for missing packets of audio -- the timestamp and playtime are zero.
|
||||
// timed samples come from the client.
|
||||
// timestamp is the RTP timestamp given to the first frame by the client
|
||||
// playtime is when the first frame should to be played. It is given in "local time".
|
||||
// local time is CLOCK_MONOTONIC_RAW if available, or CLOCK_MONOTONIC otherwise, expressed in nanoseconds as an unsigned 64-bit integer (See get_absolute_time_in_ns() in common.c)
|
||||
// clang-format on
|
||||
|
||||
static int play(__attribute__((unused)) void *buf, __attribute__((unused)) int samples,
|
||||
int sample_type, uint32_t timestamp, uint64_t playtime) {
|
||||
|
||||
// sample code using some of the extra information
|
||||
if (sample_type == play_samples_are_timed) {
|
||||
int64_t lead_time = playtime - get_absolute_time_in_ns();
|
||||
debug(1, "leadtime for frame %u is %" PRId64 " nanoseconds, i.e. %f seconds.", timestamp,
|
||||
lead_time, 0.000000001 * lead_time);
|
||||
}
|
||||
|
||||
static int play(__attribute__((unused)) void *buf, __attribute__((unused)) int samples) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -58,6 +77,7 @@ audio_output audio_dummy = {.name = "dummy",
|
||||
.is_running = NULL,
|
||||
.flush = NULL,
|
||||
.delay = NULL,
|
||||
.stats = NULL,
|
||||
.play = &play,
|
||||
.volume = NULL,
|
||||
.parameters = NULL,
|
||||
|
||||
+43
-48
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* jack output driver. This file is part of Shairport Sync.
|
||||
* Copyright (c) 2019 Mike Brady <mikebrady@iercom.net>,
|
||||
* Copyright (c) 2019 -- 2022 Mike Brady <4265913+mikebrady@users.noreply.github.com>,
|
||||
* Jörn Nettingsmeier <nettings@luchtbeweging.nl>
|
||||
*
|
||||
* All rights reserved.
|
||||
@@ -40,48 +40,25 @@ typedef jack_default_audio_sample_t sample_t;
|
||||
#define jack_sample_size sizeof(sample_t)
|
||||
|
||||
// Two-channel, 32bit audio:
|
||||
static const int bytes_per_frame = NPORTS * jack_sample_size;
|
||||
const int bytes_per_frame = NPORTS * jack_sample_size;
|
||||
|
||||
static pthread_mutex_t buffer_mutex = PTHREAD_MUTEX_INITIALIZER;
|
||||
static pthread_mutex_t client_mutex = PTHREAD_MUTEX_INITIALIZER;
|
||||
|
||||
int jack_init(int, char **);
|
||||
void jack_deinit(void);
|
||||
void jack_start(int, int);
|
||||
int play(void *, int);
|
||||
void jack_stop(void);
|
||||
int jack_delay(long *);
|
||||
void jack_flush(void);
|
||||
|
||||
audio_output audio_jack = {.name = "jack",
|
||||
.help = NULL,
|
||||
.init = &jack_init,
|
||||
.deinit = &jack_deinit,
|
||||
.prepare = NULL,
|
||||
.start = &jack_start,
|
||||
.stop = NULL,
|
||||
.is_running = NULL,
|
||||
.flush = &jack_flush,
|
||||
.delay = &jack_delay,
|
||||
.play = &play,
|
||||
.volume = NULL,
|
||||
.parameters = NULL,
|
||||
.mute = NULL};
|
||||
pthread_mutex_t buffer_mutex = PTHREAD_MUTEX_INITIALIZER;
|
||||
pthread_mutex_t client_mutex = PTHREAD_MUTEX_INITIALIZER;
|
||||
|
||||
// This also affects deinterlacing.
|
||||
// So make it exactly the number of incoming audio channels!
|
||||
static jack_port_t *port[NPORTS];
|
||||
static const char *port_name[NPORTS] = {"out_L", "out_R"};
|
||||
jack_port_t *port[NPORTS];
|
||||
const char *port_name[NPORTS] = {"out_L", "out_R"};
|
||||
|
||||
static jack_client_t *client;
|
||||
static jack_nframes_t sample_rate;
|
||||
static jack_nframes_t jack_latency;
|
||||
jack_client_t *client;
|
||||
jack_nframes_t sample_rate;
|
||||
jack_nframes_t jack_latency;
|
||||
|
||||
static jack_ringbuffer_t *jackbuf;
|
||||
static int flush_please = 0;
|
||||
jack_ringbuffer_t *jackbuf;
|
||||
int flush_please = 0;
|
||||
|
||||
static jack_latency_range_t latest_latency_range[NPORTS];
|
||||
static int64_t time_of_latest_transfer;
|
||||
jack_latency_range_t latest_latency_range[NPORTS];
|
||||
int64_t time_of_latest_transfer;
|
||||
|
||||
#ifdef CONFIG_SOXR
|
||||
typedef struct soxr_quality {
|
||||
@@ -89,9 +66,9 @@ typedef struct soxr_quality {
|
||||
const char *name;
|
||||
} soxr_quality_t;
|
||||
|
||||
static soxr_quality_t soxr_quality_table[] = {{SOXR_VHQ, "very high"}, {SOXR_HQ, "high"},
|
||||
{SOXR_MQ, "medium"}, {SOXR_LQ, "low"},
|
||||
{SOXR_QQ, "quick"}, {-1, NULL}};
|
||||
soxr_quality_t soxr_quality_table[] = {{SOXR_VHQ, "very high"}, {SOXR_HQ, "high"},
|
||||
{SOXR_MQ, "medium"}, {SOXR_LQ, "low"},
|
||||
{SOXR_QQ, "quick"}, {-1, NULL}};
|
||||
|
||||
static int parse_soxr_quality_name(const char *name) {
|
||||
for (soxr_quality_t *s = soxr_quality_table; s->name != NULL; ++s) {
|
||||
@@ -102,9 +79,9 @@ static int parse_soxr_quality_name(const char *name) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
static soxr_t soxr = NULL;
|
||||
static soxr_quality_spec_t quality_spec;
|
||||
static soxr_io_spec_t io_spec;
|
||||
soxr_t soxr = NULL;
|
||||
soxr_quality_spec_t quality_spec;
|
||||
soxr_io_spec_t io_spec;
|
||||
#endif
|
||||
|
||||
static inline sample_t sample_conv(short sample) {
|
||||
@@ -203,7 +180,7 @@ static void error(const char *desc) { warn("JACK error: \"%s\"", desc); }
|
||||
// This is the function JACK will call in case of a non-critical event in the library.
|
||||
static void info(const char *desc) { inform("JACK information: \"%s\"", desc); }
|
||||
|
||||
int jack_init(__attribute__((unused)) int argc, __attribute__((unused)) char **argv) {
|
||||
static int jack_init(__attribute__((unused)) int argc, __attribute__((unused)) char **argv) {
|
||||
int i;
|
||||
int bufsz = -1;
|
||||
config.audio_backend_latency_offset = 0;
|
||||
@@ -331,7 +308,7 @@ int jack_init(__attribute__((unused)) int argc, __attribute__((unused)) char **a
|
||||
return 0;
|
||||
}
|
||||
|
||||
void jack_deinit() {
|
||||
static void jack_deinit() {
|
||||
pthread_mutex_lock(&client_mutex);
|
||||
if (jack_deactivate(client))
|
||||
warn("Error deactivating jack client");
|
||||
@@ -347,7 +324,7 @@ void jack_deinit() {
|
||||
#endif
|
||||
}
|
||||
|
||||
void jack_start(int i_sample_rate, __attribute__((unused)) int i_sample_format) {
|
||||
static void jack_start(int i_sample_rate, __attribute__((unused)) int i_sample_format) {
|
||||
// Nothing to do, JACK client has already been set up at jack_init().
|
||||
// Also, we have no say over the sample rate or sample format of JACK,
|
||||
// We convert the 16bit samples to float, and die if the sample rate is != 44k1 without soxr.
|
||||
@@ -366,13 +343,13 @@ void jack_start(int i_sample_rate, __attribute__((unused)) int i_sample_format)
|
||||
#endif
|
||||
}
|
||||
|
||||
void jack_flush() {
|
||||
static void jack_flush() {
|
||||
debug(2, "Only the consumer can safely flush a lock-free ringbuffer. Asking the"
|
||||
" process callback to do it...");
|
||||
flush_please = 1;
|
||||
}
|
||||
|
||||
int jack_delay(long *the_delay) {
|
||||
static int jack_delay(long *the_delay) {
|
||||
// Semantics change: we now look at the last transfer into the lock-free
|
||||
// ringbuffer, not into the jack buffers directly (because locking those would
|
||||
// violate real-time constraints). On average, that should lead to just a
|
||||
@@ -398,7 +375,9 @@ int jack_delay(long *the_delay) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
int play(void *buf, int samples) {
|
||||
static int play(void *buf, int samples, __attribute__((unused)) int sample_type,
|
||||
__attribute__((unused)) uint32_t timestamp,
|
||||
__attribute__((unused)) uint64_t playtime) {
|
||||
jack_ringbuffer_data_t v[2] = {0};
|
||||
size_t i, j, c;
|
||||
jack_nframes_t thisbuf;
|
||||
@@ -444,3 +423,19 @@ int play(void *buf, int samples) {
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
audio_output audio_jack = {.name = "jack",
|
||||
.help = NULL,
|
||||
.init = &jack_init,
|
||||
.deinit = &jack_deinit,
|
||||
.prepare = NULL,
|
||||
.start = &jack_start,
|
||||
.stop = NULL,
|
||||
.is_running = NULL,
|
||||
.flush = &jack_flush,
|
||||
.delay = &jack_delay,
|
||||
.stats = NULL,
|
||||
.play = &play,
|
||||
.volume = NULL,
|
||||
.parameters = NULL,
|
||||
.mute = NULL};
|
||||
|
||||
+4
-1
@@ -223,7 +223,9 @@ static void start(__attribute__((unused)) int sample_rate,
|
||||
pa_threaded_mainloop_unlock(mainloop);
|
||||
}
|
||||
|
||||
static int play(void *buf, int samples) {
|
||||
static int play(void *buf, int samples, __attribute__((unused)) int sample_type,
|
||||
__attribute__((unused)) uint32_t timestamp,
|
||||
__attribute__((unused)) uint64_t playtime) {
|
||||
// debug(1,"pa_play of %d samples.",samples);
|
||||
// copy the samples into the queue
|
||||
size_t bytes_to_transfer = samples * 2 * 2;
|
||||
@@ -315,6 +317,7 @@ audio_output audio_pa = {.name = "pa",
|
||||
.is_running = NULL,
|
||||
.flush = &flush,
|
||||
.delay = &pa_delay,
|
||||
.stats = NULL,
|
||||
.play = &play,
|
||||
.volume = NULL,
|
||||
.parameters = NULL,
|
||||
|
||||
+4
-1
@@ -66,7 +66,9 @@ static void start(__attribute__((unused)) int sample_rate,
|
||||
}
|
||||
}
|
||||
|
||||
static int play(void *buf, int samples) {
|
||||
static int play(void *buf, int samples, __attribute__((unused)) int sample_type,
|
||||
__attribute__((unused)) uint32_t timestamp,
|
||||
__attribute__((unused)) uint64_t playtime) {
|
||||
// if the file is not open, try to open it.
|
||||
char errorstring[1024];
|
||||
if (fd == -1) {
|
||||
@@ -151,6 +153,7 @@ audio_output audio_pipe = {.name = "pipe",
|
||||
.is_running = NULL,
|
||||
.flush = NULL,
|
||||
.delay = NULL,
|
||||
.stats = NULL,
|
||||
.play = &play,
|
||||
.volume = NULL,
|
||||
.parameters = NULL,
|
||||
|
||||
+527
@@ -0,0 +1,527 @@
|
||||
/*
|
||||
* Asynchronous Pipewire Backend. This file is part of Shairport Sync.
|
||||
* Copyright (c) Shairport Sync 2021--2022
|
||||
* 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 "audio.h"
|
||||
#include "common.h"
|
||||
|
||||
#include <pipewire/pipewire.h>
|
||||
#include <pipewire/stream.h>
|
||||
#include <spa/param/audio/format-utils.h>
|
||||
#include <spa/param/audio/layout.h>
|
||||
#include <spa/utils/result.h>
|
||||
|
||||
#include <math.h>
|
||||
|
||||
#define PW_TIMEOUT_S 5
|
||||
#define SECONDS_TO_NANOSECONDS 1000000000L
|
||||
#define PW_TIMEOUT_NS (PW_TIMEOUT_S * SECONDS_TO_NANOSECONDS)
|
||||
|
||||
struct pw_data {
|
||||
struct pw_thread_loop *mainloop;
|
||||
struct pw_context *context;
|
||||
|
||||
struct pw_core *core;
|
||||
struct spa_hook core_listener;
|
||||
|
||||
struct pw_registry *registry;
|
||||
struct spa_hook registry_listener;
|
||||
|
||||
struct pw_stream *stream;
|
||||
struct spa_hook stream_listener;
|
||||
|
||||
struct pw_buffer *pw_buffer;
|
||||
|
||||
struct pw_properties *props;
|
||||
int sync;
|
||||
|
||||
enum spa_audio_format format;
|
||||
uint32_t rate;
|
||||
uint32_t channels;
|
||||
uint32_t stride;
|
||||
uint32_t latency;
|
||||
|
||||
} data;
|
||||
|
||||
static void on_core_info(__attribute__((unused)) void *userdata, const struct pw_core_info *info) {
|
||||
debug(1, "pw: remote %" PRIu32 " is named \"%s\"", info->id, info->name);
|
||||
}
|
||||
|
||||
static void on_core_error(__attribute__((unused)) void *userdata, uint32_t id, int seq, int res,
|
||||
const char *message) {
|
||||
warn("pw: remote error: id=%" PRIu32 " seq:%d res:%d (%s): %s", id, seq, res, spa_strerror(res),
|
||||
message);
|
||||
}
|
||||
|
||||
static const struct pw_core_events core_events = {
|
||||
PW_VERSION_CORE_EVENTS,
|
||||
.info = on_core_info,
|
||||
.error = on_core_error,
|
||||
};
|
||||
|
||||
static void registry_event_global(__attribute__((unused)) void *userdata, uint32_t id,
|
||||
__attribute__((unused)) uint32_t permissions, const char *type,
|
||||
__attribute__((unused)) uint32_t version,
|
||||
const struct spa_dict *props) {
|
||||
const struct spa_dict_item *item;
|
||||
const char *name, *media_class;
|
||||
|
||||
if (strcmp(type, PW_TYPE_INTERFACE_Node) == 0) {
|
||||
name = spa_dict_lookup(props, PW_KEY_NODE_NAME);
|
||||
media_class = spa_dict_lookup(props, PW_KEY_MEDIA_CLASS);
|
||||
|
||||
if (!name || !media_class)
|
||||
return;
|
||||
|
||||
debug(1, "pw: registry: id=%" PRIu32 " type=%s name=\"%s\" media_class=\"%s\"", id, type, name,
|
||||
media_class);
|
||||
|
||||
spa_dict_for_each(item, props) { debug(1, "pw: \t\t%s = \"%s\"", item->key, item->value); }
|
||||
}
|
||||
}
|
||||
|
||||
static void registry_event_global_remove(__attribute__((unused)) void *userdata, uint32_t id) {
|
||||
debug(1, "pw: registry: remove id=%" PRIu32 "", id);
|
||||
}
|
||||
|
||||
static const struct pw_registry_events registry_events = {
|
||||
PW_VERSION_REGISTRY_EVENTS,
|
||||
.global = registry_event_global,
|
||||
.global_remove = registry_event_global_remove,
|
||||
};
|
||||
|
||||
static void on_state_changed(void *userdata, enum pw_stream_state old, enum pw_stream_state state,
|
||||
const char *error) {
|
||||
struct pw_data *pipewire = userdata;
|
||||
|
||||
debug(1, "pw: stream state changed %s -> %s", pw_stream_state_as_string(old),
|
||||
pw_stream_state_as_string(state));
|
||||
|
||||
if (state == PW_STREAM_STATE_STREAMING)
|
||||
debug(1, "pw: stream node %" PRIu32 "", pw_stream_get_node_id(pipewire->stream));
|
||||
|
||||
if (state == PW_STREAM_STATE_ERROR)
|
||||
debug(1, "pw: stream node %" PRIu32 " error: %s", pw_stream_get_node_id(pipewire->stream),
|
||||
error);
|
||||
|
||||
pw_thread_loop_signal(pipewire->mainloop, 0);
|
||||
}
|
||||
|
||||
static void on_process(void *userdata) {
|
||||
struct pw_data *pipewire = userdata;
|
||||
|
||||
pw_thread_loop_signal(pipewire->mainloop, 0);
|
||||
}
|
||||
|
||||
static void on_drained(void *userdata) {
|
||||
struct pw_data *pipewire = userdata;
|
||||
|
||||
pw_stream_set_active(pipewire->stream, false);
|
||||
|
||||
pw_thread_loop_signal(pipewire->mainloop, 0);
|
||||
}
|
||||
|
||||
static const struct pw_stream_events stream_events = {
|
||||
PW_VERSION_STREAM_EVENTS,
|
||||
.state_changed = on_state_changed,
|
||||
.process = on_process,
|
||||
.drained = on_drained,
|
||||
};
|
||||
|
||||
static void deinit() {
|
||||
pw_thread_loop_stop(data.mainloop);
|
||||
|
||||
if (data.stream) {
|
||||
pw_stream_destroy(data.stream);
|
||||
data.stream = NULL;
|
||||
}
|
||||
|
||||
if (data.registry) {
|
||||
pw_proxy_destroy((struct pw_proxy *)data.registry);
|
||||
data.registry = NULL;
|
||||
}
|
||||
|
||||
if (data.core) {
|
||||
pw_core_disconnect(data.core);
|
||||
data.core = NULL;
|
||||
}
|
||||
|
||||
if (data.context) {
|
||||
pw_context_destroy(data.context);
|
||||
data.context = NULL;
|
||||
}
|
||||
|
||||
if (data.mainloop) {
|
||||
pw_thread_loop_destroy(data.mainloop);
|
||||
data.mainloop = NULL;
|
||||
}
|
||||
|
||||
if (data.props) {
|
||||
pw_properties_free(data.props);
|
||||
data.props = NULL;
|
||||
}
|
||||
|
||||
pthread_setcancelstate(PTHREAD_CANCEL_ENABLE, NULL);
|
||||
}
|
||||
|
||||
static int init(__attribute__((unused)) int argc, __attribute__((unused)) char **argv) {
|
||||
|
||||
struct pw_loop *loop;
|
||||
struct pw_properties *props;
|
||||
|
||||
// set up default values first
|
||||
config.audio_backend_buffer_desired_length = 0.35;
|
||||
config.audio_backend_buffer_interpolation_threshold_in_seconds = 0.02;
|
||||
config.audio_backend_latency_offset = 0;
|
||||
|
||||
pw_init(NULL, NULL);
|
||||
|
||||
debug(1, "pw: compiled with libpipewire %s", pw_get_headers_version());
|
||||
debug(1, "pw: linked with libpipewire: %s", pw_get_library_version());
|
||||
|
||||
data.props = pw_properties_new(PW_KEY_MEDIA_TYPE, "Audio", PW_KEY_MEDIA_CATEGORY, "Playback",
|
||||
PW_KEY_MEDIA_ROLE, "Music", PW_KEY_APP_NAME, "shairport-sync",
|
||||
PW_KEY_NODE_NAME, "shairport-sync", NULL);
|
||||
|
||||
if (!data.props) {
|
||||
deinit();
|
||||
die("pw: pw_properties_new() failed: %m");
|
||||
}
|
||||
|
||||
data.mainloop = pw_thread_loop_new("pipewire", NULL);
|
||||
if (!data.mainloop) {
|
||||
deinit();
|
||||
die("pw: pw_thread_loop_new_full() failed: %m");
|
||||
}
|
||||
|
||||
props = pw_properties_new(PW_KEY_CONFIG_NAME, "client-rt.conf", NULL);
|
||||
if (!props) {
|
||||
deinit();
|
||||
die("pw: pw_properties_new() failed: %m");
|
||||
}
|
||||
|
||||
loop = pw_thread_loop_get_loop(data.mainloop);
|
||||
|
||||
data.context = pw_context_new(loop, props, 0);
|
||||
if (!data.context) {
|
||||
deinit();
|
||||
die("pw: pw_context_new() failed: %m");
|
||||
}
|
||||
|
||||
props = pw_properties_new(PW_KEY_REMOTE_NAME, NULL, NULL);
|
||||
if (!props) {
|
||||
deinit();
|
||||
die("pw: pw_properties_new() failed: %m");
|
||||
}
|
||||
|
||||
pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, NULL);
|
||||
pw_thread_loop_lock(data.mainloop);
|
||||
|
||||
if (pw_thread_loop_start(data.mainloop) != 0) {
|
||||
deinit();
|
||||
die("pw: pw_thread_loop_start() failed: %m");
|
||||
}
|
||||
|
||||
data.core = pw_context_connect(data.context, props, 0);
|
||||
if (!data.core) {
|
||||
deinit();
|
||||
die("pw: pw_context_connect() failed: %m");
|
||||
}
|
||||
|
||||
pw_core_add_listener(data.core, &data.core_listener, &core_events, &data);
|
||||
|
||||
data.registry = pw_core_get_registry(data.core, PW_VERSION_REGISTRY, 0);
|
||||
if (!data.registry) {
|
||||
deinit();
|
||||
die("pw: pw_core_get_registry() failed: %m");
|
||||
}
|
||||
|
||||
pw_registry_add_listener(data.registry, &data.registry_listener, ®istry_events, &data);
|
||||
|
||||
data.sync = pw_core_sync(data.core, 0, data.sync);
|
||||
|
||||
pw_thread_loop_unlock(data.mainloop);
|
||||
pthread_setcancelstate(PTHREAD_CANCEL_ENABLE, NULL);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static enum spa_audio_format sps_format_to_spa_format(sps_format_t sps_format) {
|
||||
switch (sps_format) {
|
||||
case SPS_FORMAT_S8:
|
||||
return SPA_AUDIO_FORMAT_S8;
|
||||
case SPS_FORMAT_U8:
|
||||
return SPA_AUDIO_FORMAT_U8;
|
||||
case SPS_FORMAT_S16:
|
||||
return SPA_AUDIO_FORMAT_S16;
|
||||
case SPS_FORMAT_S16_LE:
|
||||
return SPA_AUDIO_FORMAT_S16_LE;
|
||||
case SPS_FORMAT_S16_BE:
|
||||
return SPA_AUDIO_FORMAT_S16_BE;
|
||||
case SPS_FORMAT_S24:
|
||||
return SPA_AUDIO_FORMAT_S24_32;
|
||||
case SPS_FORMAT_S24_LE:
|
||||
return SPA_AUDIO_FORMAT_S24_32_LE;
|
||||
case SPS_FORMAT_S24_BE:
|
||||
return SPA_AUDIO_FORMAT_S24_32_BE;
|
||||
case SPS_FORMAT_S24_3LE:
|
||||
return SPA_AUDIO_FORMAT_S24_LE;
|
||||
case SPS_FORMAT_S24_3BE:
|
||||
return SPA_AUDIO_FORMAT_S24_BE;
|
||||
case SPS_FORMAT_S32:
|
||||
return SPA_AUDIO_FORMAT_S32;
|
||||
case SPS_FORMAT_S32_LE:
|
||||
return SPA_AUDIO_FORMAT_S32_LE;
|
||||
case SPS_FORMAT_S32_BE:
|
||||
return SPA_AUDIO_FORMAT_S32_BE;
|
||||
|
||||
case SPS_FORMAT_UNKNOWN:
|
||||
case SPS_FORMAT_AUTO:
|
||||
case SPS_FORMAT_INVALID:
|
||||
default:
|
||||
return SPA_AUDIO_FORMAT_S16;
|
||||
}
|
||||
}
|
||||
|
||||
static int spa_format_samplesize(enum spa_audio_format audio_format) {
|
||||
switch (audio_format) {
|
||||
case SPA_AUDIO_FORMAT_S8:
|
||||
case SPA_AUDIO_FORMAT_U8:
|
||||
return 1;
|
||||
case SPA_AUDIO_FORMAT_S16:
|
||||
return 2;
|
||||
case SPA_AUDIO_FORMAT_S24:
|
||||
return 3;
|
||||
case SPA_AUDIO_FORMAT_S24_32:
|
||||
case SPA_AUDIO_FORMAT_S32:
|
||||
return 4;
|
||||
default:
|
||||
die("pw: unhandled spa_audio_format: %d", audio_format);
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
static const char *spa_format_to_str(enum spa_audio_format audio_format) {
|
||||
switch (audio_format) {
|
||||
case SPA_AUDIO_FORMAT_U8:
|
||||
return "u8";
|
||||
case SPA_AUDIO_FORMAT_S8:
|
||||
return "s8";
|
||||
case SPA_AUDIO_FORMAT_S16:
|
||||
return "s16";
|
||||
case SPA_AUDIO_FORMAT_S24:
|
||||
case SPA_AUDIO_FORMAT_S24_32:
|
||||
return "s24";
|
||||
case SPA_AUDIO_FORMAT_S32:
|
||||
return "s32";
|
||||
default:
|
||||
die("pw: unhandled spa_audio_format: %d", audio_format);
|
||||
return "(invalid)";
|
||||
}
|
||||
}
|
||||
|
||||
static void start(int sample_rate, int sample_format) {
|
||||
|
||||
pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, NULL);
|
||||
|
||||
pw_thread_loop_lock(data.mainloop);
|
||||
|
||||
const struct spa_pod *params[1];
|
||||
uint8_t buffer[1024];
|
||||
struct spa_pod_builder pod_builder = SPA_POD_BUILDER_INIT(buffer, sizeof(buffer));
|
||||
struct spa_audio_info_raw info;
|
||||
uint32_t nom;
|
||||
int ret;
|
||||
|
||||
data.format = sps_format_to_spa_format(sample_format);
|
||||
data.rate = sample_rate;
|
||||
data.channels = 2;
|
||||
data.stride = spa_format_samplesize(data.format) * data.channels;
|
||||
data.latency = 20000;
|
||||
|
||||
nom = nearbyint((data.latency * data.rate) / 1000000.0);
|
||||
|
||||
pw_properties_setf(data.props, PW_KEY_NODE_LATENCY, "%u/%u", nom, data.rate);
|
||||
|
||||
debug(1, "pw: rate: %d", data.rate);
|
||||
debug(1, "pw: channgels: %d", data.channels);
|
||||
debug(1, "pw: format: %s", spa_format_to_str(data.format));
|
||||
debug(1, "pw: samplesize: %d", spa_format_samplesize(data.format));
|
||||
debug(1, "pw: stride: %d", data.stride);
|
||||
if (data.rate != 0)
|
||||
debug(1, "pw: latency: %d samples (%.3fs)", nom, (double)nom / data.rate);
|
||||
|
||||
info = SPA_AUDIO_INFO_RAW_INIT(.flags = SPA_AUDIO_FLAG_NONE, .format = data.format,
|
||||
.rate = data.rate, .channels = data.channels);
|
||||
|
||||
params[0] = spa_format_audio_raw_build(&pod_builder, SPA_PARAM_EnumFormat, &info);
|
||||
|
||||
data.stream = pw_stream_new(data.core, "shairport-sync", data.props);
|
||||
|
||||
if (!data.stream) {
|
||||
deinit();
|
||||
die("pw: pw_stream_new() failed: %m");
|
||||
}
|
||||
|
||||
debug(1, "pw: connecting stream: target_id=%" PRIu32 "", PW_ID_ANY);
|
||||
|
||||
pw_stream_add_listener(data.stream, &data.stream_listener, &stream_events, &data);
|
||||
|
||||
ret = pw_stream_connect(
|
||||
data.stream, PW_DIRECTION_OUTPUT, PW_ID_ANY,
|
||||
PW_STREAM_FLAG_INACTIVE | PW_STREAM_FLAG_AUTOCONNECT | PW_STREAM_FLAG_MAP_BUFFERS, params, 1);
|
||||
|
||||
if (ret < 0) {
|
||||
deinit();
|
||||
die("pw: pw_stream_connect() failed: %s", spa_strerror(ret));
|
||||
}
|
||||
|
||||
const struct pw_properties *props;
|
||||
void *pstate;
|
||||
const char *key, *val;
|
||||
|
||||
if ((props = pw_stream_get_properties(data.stream)) != NULL) {
|
||||
debug(1, "pw: stream properties:");
|
||||
pstate = NULL;
|
||||
while ((key = pw_properties_iterate(props, &pstate)) != NULL &&
|
||||
(val = pw_properties_get(props, key)) != NULL) {
|
||||
debug(1, "pw: \t%s = \"%s\"", key, val);
|
||||
}
|
||||
}
|
||||
|
||||
while (1) {
|
||||
enum pw_stream_state stream_state = pw_stream_get_state(data.stream, NULL);
|
||||
if (stream_state == PW_STREAM_STATE_PAUSED)
|
||||
break;
|
||||
|
||||
struct timespec abstime;
|
||||
|
||||
pw_thread_loop_get_time(data.mainloop, &abstime, PW_TIMEOUT_NS);
|
||||
|
||||
ret = pw_thread_loop_timed_wait_full(data.mainloop, &abstime);
|
||||
if (ret == -ETIMEDOUT) {
|
||||
deinit();
|
||||
die("pw: pw_thread_loop_timed_wait_full timed out: %s", strerror(ret));
|
||||
}
|
||||
}
|
||||
|
||||
pw_thread_loop_unlock(data.mainloop);
|
||||
|
||||
pthread_setcancelstate(PTHREAD_CANCEL_ENABLE, NULL);
|
||||
}
|
||||
|
||||
static void stop() {
|
||||
pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, NULL);
|
||||
pw_thread_loop_lock(data.mainloop);
|
||||
|
||||
pw_stream_flush(data.stream, true);
|
||||
|
||||
pw_thread_loop_unlock(data.mainloop);
|
||||
pthread_setcancelstate(PTHREAD_CANCEL_ENABLE, NULL);
|
||||
}
|
||||
|
||||
static void flush() {
|
||||
pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, NULL);
|
||||
pw_thread_loop_lock(data.mainloop);
|
||||
|
||||
pw_stream_flush(data.stream, false);
|
||||
|
||||
pw_thread_loop_unlock(data.mainloop);
|
||||
pthread_setcancelstate(PTHREAD_CANCEL_ENABLE, NULL);
|
||||
}
|
||||
|
||||
static int play(void *buf, int samples, __attribute__((unused)) int sample_type,
|
||||
__attribute__((unused)) uint32_t timestamp,
|
||||
__attribute__((unused)) uint64_t playtime) {
|
||||
struct pw_buffer *pw_buffer = NULL;
|
||||
struct spa_buffer *spa_buffer;
|
||||
struct spa_data *spa_data;
|
||||
int ret;
|
||||
|
||||
pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, NULL);
|
||||
pw_thread_loop_lock(data.mainloop);
|
||||
|
||||
if (pw_stream_get_state(data.stream, NULL) == PW_STREAM_STATE_PAUSED)
|
||||
pw_stream_set_active(data.stream, true);
|
||||
|
||||
while (pw_buffer == NULL) {
|
||||
pw_buffer = pw_stream_dequeue_buffer(data.stream);
|
||||
if (pw_buffer)
|
||||
break;
|
||||
|
||||
struct timespec abstime;
|
||||
|
||||
pw_thread_loop_get_time(data.mainloop, &abstime, PW_TIMEOUT_NS);
|
||||
|
||||
ret = pw_thread_loop_timed_wait_full(data.mainloop, &abstime);
|
||||
if (ret == -ETIMEDOUT) {
|
||||
pw_thread_loop_unlock(data.mainloop);
|
||||
pthread_setcancelstate(PTHREAD_CANCEL_ENABLE, NULL);
|
||||
return ret;
|
||||
}
|
||||
}
|
||||
|
||||
spa_buffer = pw_buffer->buffer;
|
||||
spa_data = &spa_buffer->datas[0];
|
||||
|
||||
size_t bytes_to_copy = samples * data.stride;
|
||||
|
||||
debug(3, "pw: bytes_to_copy: %d", bytes_to_copy);
|
||||
|
||||
if (spa_data->maxsize < bytes_to_copy)
|
||||
bytes_to_copy = spa_data->maxsize;
|
||||
|
||||
debug(3, "pw: spa_data->maxsize: %d", spa_data->maxsize);
|
||||
|
||||
memcpy(spa_data->data, buf, bytes_to_copy);
|
||||
|
||||
spa_data->chunk->offset = 0;
|
||||
spa_data->chunk->stride = data.stride;
|
||||
spa_data->chunk->size = bytes_to_copy;
|
||||
|
||||
pw_stream_queue_buffer(data.stream, pw_buffer);
|
||||
|
||||
pw_thread_loop_unlock(data.mainloop);
|
||||
|
||||
pthread_setcancelstate(PTHREAD_CANCEL_ENABLE, NULL);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
audio_output audio_pw = {.name = "pw",
|
||||
.help = NULL,
|
||||
.init = &init,
|
||||
.deinit = &deinit,
|
||||
.prepare = NULL,
|
||||
.start = &start,
|
||||
.stop = &stop,
|
||||
.is_running = NULL,
|
||||
.flush = &flush,
|
||||
.delay = NULL,
|
||||
.stats = NULL,
|
||||
.play = &play,
|
||||
.volume = NULL,
|
||||
.parameters = NULL,
|
||||
.mute = NULL};
|
||||
+79
-54
@@ -4,7 +4,7 @@
|
||||
* Copyright (c) 2017 Tobias Kortkamp <t@tobik.me>
|
||||
*
|
||||
* Modifications for audio synchronisation
|
||||
* and related work, copyright (c) Mike Brady 2014 -- 2017
|
||||
* and related work, copyright (c) Mike Brady 2014 -- 2022
|
||||
* All rights reserved.
|
||||
*
|
||||
* Permission to use, copy, modify, and distribute this software for any
|
||||
@@ -28,39 +28,14 @@
|
||||
#include <string.h>
|
||||
#include <unistd.h>
|
||||
|
||||
static void help(void);
|
||||
static int init(int, char **);
|
||||
static void onmove_cb(void *, int);
|
||||
static void deinit(void);
|
||||
static void start(int, int);
|
||||
static int play(void *, int);
|
||||
static void stop(void);
|
||||
static void onmove_cb(void *, int);
|
||||
static int delay(long *);
|
||||
static void flush(void);
|
||||
|
||||
audio_output audio_sndio = {.name = "sndio",
|
||||
.help = &help,
|
||||
.init = &init,
|
||||
.deinit = &deinit,
|
||||
.prepare = NULL,
|
||||
.start = &start,
|
||||
.stop = &stop,
|
||||
.is_running = NULL,
|
||||
.flush = &flush,
|
||||
.delay = &delay,
|
||||
.play = &play,
|
||||
.volume = NULL,
|
||||
.parameters = NULL,
|
||||
.mute = NULL};
|
||||
|
||||
static pthread_mutex_t sndio_mutex = PTHREAD_MUTEX_INITIALIZER;
|
||||
static struct sio_hdl *hdl;
|
||||
static int framesize;
|
||||
static size_t played;
|
||||
static size_t written;
|
||||
int64_t time_of_last_onmove_cb;
|
||||
uint64_t time_of_last_onmove_cb;
|
||||
int at_least_one_onmove_cb_seen;
|
||||
|
||||
struct sio_par par;
|
||||
|
||||
struct sndio_formats {
|
||||
@@ -81,10 +56,16 @@ static struct sndio_formats formats[] = {{"S8", SPS_FORMAT_S8, 44100, 8, 1, 1, S
|
||||
{"S24", SPS_FORMAT_S24, 44100, 24, 4, 1, SIO_LE_NATIVE},
|
||||
{"S24_3LE", SPS_FORMAT_S24_3LE, 44100, 24, 3, 1, 1},
|
||||
{"S24_3BE", SPS_FORMAT_S24_3BE, 44100, 24, 3, 1, 0},
|
||||
{"S32", SPS_FORMAT_S32, 44100, 24, 4, 1, SIO_LE_NATIVE}};
|
||||
{"S32", SPS_FORMAT_S32, 44100, 32, 4, 1, SIO_LE_NATIVE}};
|
||||
|
||||
static void help() { printf(" -d output-device set the output device [default*|...]\n"); }
|
||||
|
||||
void onmove_cb(__attribute__((unused)) void *arg, int delta) {
|
||||
time_of_last_onmove_cb = get_absolute_time_in_ns();
|
||||
at_least_one_onmove_cb_seen = 1;
|
||||
played += delta;
|
||||
}
|
||||
|
||||
static int init(int argc, char **argv) {
|
||||
int found, opt, round, rate, bufsz;
|
||||
unsigned int i;
|
||||
@@ -168,8 +149,12 @@ static int init(int argc, char **argv) {
|
||||
}
|
||||
if (optind < argc)
|
||||
die("Invalid audio argument: %s", argv[optind]);
|
||||
pthread_mutex_lock(&sndio_mutex);
|
||||
debug(1, "Output device name is \"%s\".", devname);
|
||||
pthread_cleanup_debug_mutex_lock(&sndio_mutex, 1000, 1);
|
||||
// pthread_mutex_lock(&sndio_mutex);
|
||||
debug(1, "sndio: output device name is \"%s\".", devname);
|
||||
debug(1, "sndio: rate: %u.", par.rate);
|
||||
debug(1, "sndio: bits: %u.", par.bits);
|
||||
|
||||
hdl = sio_open(devname, SIO_PLAY, 0);
|
||||
if (!hdl)
|
||||
die("sndio: cannot open audio device");
|
||||
@@ -203,57 +188,71 @@ static int init(int argc, char **argv) {
|
||||
|
||||
framesize = par.bps * par.pchan;
|
||||
config.output_rate = par.rate;
|
||||
if (par.rate == 0) {
|
||||
die("sndio: par.rate set to zero.");
|
||||
}
|
||||
|
||||
config.audio_backend_buffer_desired_length = 1.0 * par.bufsz / par.rate;
|
||||
config.audio_backend_latency_offset = 0;
|
||||
|
||||
sio_onmove(hdl, onmove_cb, NULL);
|
||||
|
||||
pthread_mutex_unlock(&sndio_mutex);
|
||||
// pthread_mutex_unlock(&sndio_mutex);
|
||||
pthread_cleanup_pop(1); // unlock the mutex
|
||||
if (framesize == 0) {
|
||||
die("sndio: framesize set to zero.");
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void deinit() {
|
||||
pthread_mutex_lock(&sndio_mutex);
|
||||
// pthread_mutex_lock(&sndio_mutex);
|
||||
pthread_cleanup_debug_mutex_lock(&sndio_mutex, 1000, 1);
|
||||
sio_close(hdl);
|
||||
pthread_mutex_unlock(&sndio_mutex);
|
||||
// pthread_mutex_unlock(&sndio_mutex);
|
||||
pthread_cleanup_pop(1); // unlock the mutex
|
||||
}
|
||||
|
||||
static void start(__attribute__((unused)) int sample_rate,
|
||||
__attribute__((unused)) int sample_format) {
|
||||
pthread_mutex_lock(&sndio_mutex);
|
||||
// pthread_mutex_lock(&sndio_mutex);
|
||||
pthread_cleanup_debug_mutex_lock(&sndio_mutex, 1000, 1);
|
||||
at_least_one_onmove_cb_seen = 0;
|
||||
// any previously-reported frame count
|
||||
|
||||
if (!sio_start(hdl))
|
||||
die("sndio: unable to start");
|
||||
written = played = 0;
|
||||
time_of_last_onmove_cb = 0;
|
||||
at_least_one_onmove_cb_seen = 0;
|
||||
pthread_mutex_unlock(&sndio_mutex);
|
||||
// pthread_mutex_unlock(&sndio_mutex);
|
||||
pthread_cleanup_pop(1); // unlock the mutex
|
||||
}
|
||||
|
||||
static int play(void *buf, int frames) {
|
||||
static int play(void *buf, int frames, __attribute__((unused)) int sample_type,
|
||||
__attribute__((unused)) uint32_t timestamp,
|
||||
__attribute__((unused)) uint64_t playtime) {
|
||||
if (frames > 0) {
|
||||
pthread_mutex_lock(&sndio_mutex);
|
||||
// pthread_mutex_lock(&sndio_mutex);
|
||||
pthread_cleanup_debug_mutex_lock(&sndio_mutex, 1000, 1);
|
||||
written += sio_write(hdl, buf, frames * framesize);
|
||||
pthread_mutex_unlock(&sndio_mutex);
|
||||
// pthread_mutex_unlock(&sndio_mutex);
|
||||
pthread_cleanup_pop(1); // unlock the mutex
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void stop() {
|
||||
pthread_mutex_lock(&sndio_mutex);
|
||||
pthread_cleanup_debug_mutex_lock(&sndio_mutex, 1000, 1);
|
||||
|
||||
if (!sio_stop(hdl))
|
||||
die("sndio: unable to stop");
|
||||
written = played = 0;
|
||||
pthread_mutex_unlock(&sndio_mutex);
|
||||
pthread_cleanup_pop(1); // unlock the mutex
|
||||
}
|
||||
|
||||
static void onmove_cb(__attribute__((unused)) void *arg, int delta) {
|
||||
time_of_last_onmove_cb = get_absolute_time_in_ns();
|
||||
at_least_one_onmove_cb_seen = 1;
|
||||
played += delta;
|
||||
}
|
||||
|
||||
static int delay(long *_delay) {
|
||||
pthread_mutex_lock(&sndio_mutex);
|
||||
int get_delay(long *delay) {
|
||||
int response = 0;
|
||||
size_t estimated_extra_frames_output = 0;
|
||||
if (at_least_one_onmove_cb_seen) { // when output starts, the onmove_cb callback will be made
|
||||
// calculate the difference in time between now and when the last callback occurred,
|
||||
@@ -269,15 +268,41 @@ static int delay(long *_delay) {
|
||||
// debug(1,"Frames played to last cb: %d, estimated to current time:
|
||||
// %d.",played,estimated_extra_frames_output);
|
||||
}
|
||||
*_delay = (written / framesize) - (played + estimated_extra_frames_output);
|
||||
pthread_mutex_unlock(&sndio_mutex);
|
||||
return 0;
|
||||
if (delay != NULL)
|
||||
*delay = (written / framesize) - (played + estimated_extra_frames_output);
|
||||
return response;
|
||||
}
|
||||
|
||||
static int delay(long *delay) {
|
||||
int result = 0;
|
||||
pthread_cleanup_debug_mutex_lock(&sndio_mutex, 1000, 1);
|
||||
result = get_delay(delay);
|
||||
pthread_cleanup_pop(1); // unlock the mutex
|
||||
return result;
|
||||
}
|
||||
|
||||
static void flush() {
|
||||
pthread_mutex_lock(&sndio_mutex);
|
||||
// pthread_mutex_lock(&sndio_mutex);
|
||||
pthread_cleanup_debug_mutex_lock(&sndio_mutex, 1000, 1);
|
||||
if (!sio_stop(hdl) || !sio_start(hdl))
|
||||
die("sndio: unable to flush");
|
||||
written = played = 0;
|
||||
pthread_mutex_unlock(&sndio_mutex);
|
||||
// pthread_mutex_unlock(&sndio_mutex);
|
||||
pthread_cleanup_pop(1); // unlock the mutex
|
||||
}
|
||||
|
||||
audio_output audio_sndio = {.name = "sndio",
|
||||
.help = &help,
|
||||
.init = &init,
|
||||
.deinit = &deinit,
|
||||
.prepare = NULL,
|
||||
.start = &start,
|
||||
.stop = &stop,
|
||||
.is_running = NULL,
|
||||
.flush = &flush,
|
||||
.delay = &delay,
|
||||
.stats = NULL,
|
||||
.play = &play,
|
||||
.volume = NULL,
|
||||
.parameters = NULL,
|
||||
.mute = NULL};
|
||||
|
||||
+6
-1
@@ -22,6 +22,8 @@ static void write_callback(struct SoundIoOutStream *outstream, int frame_count_m
|
||||
|
||||
char *read_ptr = soundio_ring_buffer_read_ptr(ring_buffer);
|
||||
int fill_bytes = soundio_ring_buffer_fill_count(ring_buffer);
|
||||
if (outstream->bytes_per_frame == 0)
|
||||
die("soundio: outstream->bytes_per_frame is zero.");
|
||||
int fill_count = fill_bytes / outstream->bytes_per_frame;
|
||||
|
||||
debug(3,
|
||||
@@ -162,7 +164,9 @@ static void start(int sample_rate, int sample_format) {
|
||||
debug(1, "libsoundio output started\n");
|
||||
}
|
||||
|
||||
static int play(void *buf, int samples) {
|
||||
static int play(void *buf, int samples, __attribute__((unused)) int sample_type,
|
||||
__attribute__((unused)) uint32_t timestamp,
|
||||
__attribute__((unused)) uint64_t playtime) {
|
||||
// int err;
|
||||
int free_bytes = soundio_ring_buffer_free_count(ring_buffer);
|
||||
int written_bytes = 0;
|
||||
@@ -216,6 +220,7 @@ audio_output audio_soundio = {.name = "soundio",
|
||||
.is_running = NULL,
|
||||
.flush = &flush,
|
||||
.delay = NULL,
|
||||
.stats = NULL,
|
||||
.play = &play,
|
||||
.volume = NULL,
|
||||
.parameters = ¶meters,
|
||||
|
||||
+4
-1
@@ -43,7 +43,9 @@ static void start(__attribute__((unused)) int sample_rate,
|
||||
fd = STDOUT_FILENO;
|
||||
}
|
||||
|
||||
static int play(void *buf, int samples) {
|
||||
static int play(void *buf, int samples, __attribute__((unused)) int sample_type,
|
||||
__attribute__((unused)) uint32_t timestamp,
|
||||
__attribute__((unused)) uint64_t playtime) {
|
||||
char errorstring[1024];
|
||||
int warned = 0;
|
||||
int rc = write(fd, buf, samples * 4);
|
||||
@@ -84,6 +86,7 @@ audio_output audio_stdout = {.name = "stdout",
|
||||
.is_running = NULL,
|
||||
.flush = NULL,
|
||||
.delay = NULL,
|
||||
.stats = NULL,
|
||||
.play = &play,
|
||||
.volume = NULL,
|
||||
.parameters = NULL,
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
* Utility routines. This file is part of Shairport.
|
||||
* Copyright (c) James Laird 2013
|
||||
* The volume to attenuation function vol2attn copyright (c) Mike Brady 2014
|
||||
* Further changes and additions (c) Mike Brady 2014 -- 2019
|
||||
* Further changes and additions (c) Mike Brady 2014 -- 2021
|
||||
* All rights reserved.
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person
|
||||
@@ -27,9 +27,15 @@
|
||||
*/
|
||||
|
||||
#include "common.h"
|
||||
|
||||
#ifdef CONFIG_USE_GIT_VERSION_STRING
|
||||
#include "gitversion.h"
|
||||
#endif
|
||||
|
||||
#include <assert.h>
|
||||
#include <errno.h>
|
||||
#include <fcntl.h>
|
||||
#include <inttypes.h> // PRIdPTR
|
||||
#include <libgen.h>
|
||||
#include <memory.h>
|
||||
#include <poll.h>
|
||||
@@ -43,10 +49,25 @@
|
||||
#include <time.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include <ifaddrs.h>
|
||||
|
||||
#ifdef COMPILE_FOR_LINUX
|
||||
#include <netpacket/packet.h>
|
||||
#endif
|
||||
|
||||
#ifdef COMPILE_FOR_BSD
|
||||
#include <net/if_dl.h>
|
||||
#include <net/if_types.h>
|
||||
#include <netinet/in.h>
|
||||
#endif
|
||||
|
||||
#ifdef COMPILE_FOR_OSX
|
||||
#include <CoreServices/CoreServices.h>
|
||||
#include <mach/mach.h>
|
||||
#include <mach/mach_time.h>
|
||||
#include <net/if_dl.h>
|
||||
#include <net/if_types.h>
|
||||
#include <netinet/in.h>
|
||||
#endif
|
||||
|
||||
#ifdef CONFIG_OPENSSL
|
||||
@@ -90,7 +111,7 @@ void set_alsa_out_dev(char *);
|
||||
#endif
|
||||
|
||||
config_t config_file_stuff;
|
||||
int emergency_exit;
|
||||
int type_of_exit_cleanup;
|
||||
pthread_t main_thread_id;
|
||||
uint64_t ns_time_at_startup, ns_time_at_last_debug_message;
|
||||
|
||||
@@ -124,7 +145,7 @@ static void (*sps_log)(int prio, const char *t, ...) = syslog;
|
||||
#endif
|
||||
|
||||
void do_sps_log_to_stderr(__attribute__((unused)) int prio, const char *t, ...) {
|
||||
char s[1024];
|
||||
char s[16384];
|
||||
va_list args;
|
||||
va_start(args, t);
|
||||
vsnprintf(s, sizeof(s), t, args);
|
||||
@@ -133,7 +154,7 @@ void do_sps_log_to_stderr(__attribute__((unused)) int prio, const char *t, ...)
|
||||
}
|
||||
|
||||
void do_sps_log_to_stdout(__attribute__((unused)) int prio, const char *t, ...) {
|
||||
char s[1024];
|
||||
char s[16384];
|
||||
va_list args;
|
||||
va_start(args, t);
|
||||
vsnprintf(s, sizeof(s), t, args);
|
||||
@@ -193,7 +214,7 @@ int create_log_file(const char *path) {
|
||||
}
|
||||
|
||||
void do_sps_log_to_fd(__attribute__((unused)) int prio, const char *t, ...) {
|
||||
char s[1024];
|
||||
char s[16384];
|
||||
va_list args;
|
||||
va_start(args, t);
|
||||
vsnprintf(s, sizeof(s), t, args);
|
||||
@@ -266,41 +287,179 @@ uint16_t nextFreeUDPPort() {
|
||||
return UDPPortIndex;
|
||||
}
|
||||
|
||||
// if port is zero, pick any port
|
||||
// otherwise, try the given port only
|
||||
int bind_socket_and_port(int type, int ip_family, const char *self_ip_address, uint32_t scope_id,
|
||||
uint16_t *port, int *sock) {
|
||||
int ret = 0; // no error
|
||||
int local_socket = socket(ip_family, type, 0);
|
||||
if (local_socket == -1)
|
||||
ret = errno;
|
||||
if (ret == 0) {
|
||||
SOCKADDR myaddr;
|
||||
memset(&myaddr, 0, sizeof(myaddr));
|
||||
if (ip_family == AF_INET) {
|
||||
struct sockaddr_in *sa = (struct sockaddr_in *)&myaddr;
|
||||
sa->sin_family = AF_INET;
|
||||
sa->sin_port = ntohs(*port);
|
||||
inet_pton(AF_INET, self_ip_address, &(sa->sin_addr));
|
||||
ret = bind(local_socket, (struct sockaddr *)sa, sizeof(struct sockaddr_in));
|
||||
}
|
||||
#ifdef AF_INET6
|
||||
if (ip_family == AF_INET6) {
|
||||
struct sockaddr_in6 *sa6 = (struct sockaddr_in6 *)&myaddr;
|
||||
sa6->sin6_family = AF_INET6;
|
||||
sa6->sin6_port = ntohs(*port);
|
||||
inet_pton(AF_INET6, self_ip_address, &(sa6->sin6_addr));
|
||||
sa6->sin6_scope_id = scope_id;
|
||||
ret = bind(local_socket, (struct sockaddr *)sa6, sizeof(struct sockaddr_in6));
|
||||
}
|
||||
#endif
|
||||
if (ret < 0) {
|
||||
ret = errno;
|
||||
close(local_socket);
|
||||
char errorstring[1024];
|
||||
strerror_r(errno, (char *)errorstring, sizeof(errorstring));
|
||||
warn("error %d: \"%s\". Could not bind a port!", errno, errorstring);
|
||||
} else {
|
||||
uint16_t sport;
|
||||
SOCKADDR local;
|
||||
socklen_t local_len = sizeof(local);
|
||||
ret = getsockname(local_socket, (struct sockaddr *)&local, &local_len);
|
||||
if (ret < 0) {
|
||||
ret = errno;
|
||||
close(local_socket);
|
||||
char errorstring[1024];
|
||||
strerror_r(errno, (char *)errorstring, sizeof(errorstring));
|
||||
warn("error %d: \"%s\". Could not retrieve socket's port!", errno, errorstring);
|
||||
} else {
|
||||
#ifdef AF_INET6
|
||||
if (local.SAFAMILY == AF_INET6) {
|
||||
struct sockaddr_in6 *sa6 = (struct sockaddr_in6 *)&local;
|
||||
sport = ntohs(sa6->sin6_port);
|
||||
} else
|
||||
#endif
|
||||
{
|
||||
struct sockaddr_in *sa = (struct sockaddr_in *)&local;
|
||||
sport = ntohs(sa->sin_port);
|
||||
}
|
||||
*sock = local_socket;
|
||||
*port = sport;
|
||||
}
|
||||
}
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
uint16_t bind_UDP_port(int ip_family, const char *self_ip_address, uint32_t scope_id, int *sock) {
|
||||
// look for a port in the range, if any was specified.
|
||||
int ret = 0;
|
||||
|
||||
int local_socket = socket(ip_family, SOCK_DGRAM, IPPROTO_UDP);
|
||||
if (local_socket == -1)
|
||||
die("Could not allocate a socket.");
|
||||
|
||||
/*
|
||||
int val = 1;
|
||||
ret = setsockopt(local_socket, SOL_SOCKET, SO_REUSEADDR, &val, sizeof(val));
|
||||
if (ret < 0) {
|
||||
char errorstring[1024];
|
||||
strerror_r(errno, (char *)errorstring, sizeof(errorstring));
|
||||
debug(1, "Error %d: \"%s\". Couldn't set SO_REUSEADDR");
|
||||
}
|
||||
*/
|
||||
|
||||
SOCKADDR myaddr;
|
||||
int tryCount = 0;
|
||||
uint16_t desired_port;
|
||||
do {
|
||||
tryCount++;
|
||||
desired_port = nextFreeUDPPort();
|
||||
memset(&myaddr, 0, sizeof(myaddr));
|
||||
if (ip_family == AF_INET) {
|
||||
struct sockaddr_in *sa = (struct sockaddr_in *)&myaddr;
|
||||
sa->sin_family = AF_INET;
|
||||
sa->sin_port = ntohs(desired_port);
|
||||
inet_pton(AF_INET, self_ip_address, &(sa->sin_addr));
|
||||
ret = bind(local_socket, (struct sockaddr *)sa, sizeof(struct sockaddr_in));
|
||||
}
|
||||
#ifdef AF_INET6
|
||||
if (ip_family == AF_INET6) {
|
||||
struct sockaddr_in6 *sa6 = (struct sockaddr_in6 *)&myaddr;
|
||||
sa6->sin6_family = AF_INET6;
|
||||
sa6->sin6_port = ntohs(desired_port);
|
||||
inet_pton(AF_INET6, self_ip_address, &(sa6->sin6_addr));
|
||||
sa6->sin6_scope_id = scope_id;
|
||||
ret = bind(local_socket, (struct sockaddr *)sa6, sizeof(struct sockaddr_in6));
|
||||
}
|
||||
#endif
|
||||
|
||||
} while ((ret < 0) && (errno == EADDRINUSE) && (desired_port != 0) &&
|
||||
(tryCount < config.udp_port_range));
|
||||
|
||||
// debug(1,"UDP port chosen: %d.",desired_port);
|
||||
|
||||
if (ret < 0) {
|
||||
close(local_socket);
|
||||
char errorstring[1024];
|
||||
strerror_r(errno, (char *)errorstring, sizeof(errorstring));
|
||||
die("error %d: \"%s\". Could not bind a UDP port! Check the udp_port_range is large enough -- "
|
||||
"it must be "
|
||||
"at least 3, and 10 or more is suggested -- or "
|
||||
"check for restrictive firewall settings or a bad router! UDP base is %u, range is %u and "
|
||||
"current suggestion is %u.",
|
||||
errno, errorstring, config.udp_port_base, config.udp_port_range, desired_port);
|
||||
}
|
||||
|
||||
uint16_t sport;
|
||||
SOCKADDR local;
|
||||
socklen_t local_len = sizeof(local);
|
||||
getsockname(local_socket, (struct sockaddr *)&local, &local_len);
|
||||
#ifdef AF_INET6
|
||||
if (local.SAFAMILY == AF_INET6) {
|
||||
struct sockaddr_in6 *sa6 = (struct sockaddr_in6 *)&local;
|
||||
sport = ntohs(sa6->sin6_port);
|
||||
} else
|
||||
#endif
|
||||
{
|
||||
struct sockaddr_in *sa = (struct sockaddr_in *)&local;
|
||||
sport = ntohs(sa->sin_port);
|
||||
}
|
||||
*sock = local_socket;
|
||||
return sport;
|
||||
}
|
||||
|
||||
int get_requested_connection_state_to_output() { return requested_connection_state_to_output; }
|
||||
|
||||
void set_requested_connection_state_to_output(int v) { requested_connection_state_to_output = v; }
|
||||
|
||||
char *generate_preliminary_string(char *buffer, size_t buffer_length, double tss, double tsl,
|
||||
const char *filename, const int linenumber, const char *prefix) {
|
||||
size_t space_remaining = buffer_length;
|
||||
char *insertion_point = buffer;
|
||||
if (config.debugger_show_elapsed_time) {
|
||||
snprintf(insertion_point, space_remaining, "% 20.9f", tss);
|
||||
snprintf(insertion_point, buffer_length, "% 20.9f", tss);
|
||||
insertion_point = insertion_point + strlen(insertion_point);
|
||||
space_remaining = space_remaining - strlen(insertion_point);
|
||||
}
|
||||
if (config.debugger_show_relative_time) {
|
||||
snprintf(insertion_point, space_remaining, "% 20.9f", tsl);
|
||||
snprintf(insertion_point, buffer_length, "% 20.9f", tsl);
|
||||
insertion_point = insertion_point + strlen(insertion_point);
|
||||
space_remaining = space_remaining - strlen(insertion_point);
|
||||
}
|
||||
if (config.debugger_show_file_and_line) {
|
||||
snprintf(insertion_point, space_remaining, " \"%s:%d\"", filename, linenumber);
|
||||
snprintf(insertion_point, buffer_length, " \"%s:%d\"", filename, linenumber);
|
||||
insertion_point = insertion_point + strlen(insertion_point);
|
||||
space_remaining = space_remaining - strlen(insertion_point);
|
||||
}
|
||||
if (prefix) {
|
||||
snprintf(insertion_point, space_remaining, "%s", prefix);
|
||||
snprintf(insertion_point, buffer_length, "%s", prefix);
|
||||
insertion_point = insertion_point + strlen(insertion_point);
|
||||
space_remaining = space_remaining - strlen(insertion_point);
|
||||
}
|
||||
return insertion_point;
|
||||
}
|
||||
|
||||
void _die(const char *filename, const int linenumber, const char *format, ...) {
|
||||
void _die(const char *thefilename, const int linenumber, const char *format, ...) {
|
||||
int oldState;
|
||||
pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, &oldState);
|
||||
char b[1024];
|
||||
|
||||
char b[16384];
|
||||
b[0] = 0;
|
||||
char *s;
|
||||
if (debuglev) {
|
||||
@@ -310,9 +469,12 @@ void _die(const char *filename, const int linenumber, const char *format, ...) {
|
||||
uint64_t time_since_last_debug_message = time_now - ns_time_at_last_debug_message;
|
||||
ns_time_at_last_debug_message = time_now;
|
||||
pthread_mutex_unlock(&debug_timing_lock);
|
||||
char *basec = strdup(thefilename);
|
||||
char *filename = basename(basec);
|
||||
s = generate_preliminary_string(b, sizeof(b), 1.0 * time_since_start / 1000000000,
|
||||
1.0 * time_since_last_debug_message / 1000000000, filename,
|
||||
linenumber, " *fatal error: ");
|
||||
free(basec);
|
||||
} else {
|
||||
strncpy(b, "fatal error: ", sizeof(b));
|
||||
s = b + strlen(b);
|
||||
@@ -323,14 +485,14 @@ void _die(const char *filename, const int linenumber, const char *format, ...) {
|
||||
va_end(args);
|
||||
sps_log(LOG_ERR, "%s", b);
|
||||
pthread_setcancelstate(oldState, NULL);
|
||||
emergency_exit = 1;
|
||||
type_of_exit_cleanup = TOE_emergency;
|
||||
exit(EXIT_FAILURE);
|
||||
}
|
||||
|
||||
void _warn(const char *filename, const int linenumber, const char *format, ...) {
|
||||
void _warn(const char *thefilename, const int linenumber, const char *format, ...) {
|
||||
int oldState;
|
||||
pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, &oldState);
|
||||
char b[1024];
|
||||
char b[16384];
|
||||
b[0] = 0;
|
||||
char *s;
|
||||
if (debuglev) {
|
||||
@@ -340,9 +502,12 @@ void _warn(const char *filename, const int linenumber, const char *format, ...)
|
||||
uint64_t time_since_last_debug_message = time_now - ns_time_at_last_debug_message;
|
||||
ns_time_at_last_debug_message = time_now;
|
||||
pthread_mutex_unlock(&debug_timing_lock);
|
||||
char *basec = strdup(thefilename);
|
||||
char *filename = basename(basec);
|
||||
s = generate_preliminary_string(b, sizeof(b), 1.0 * time_since_start / 1000000000,
|
||||
1.0 * time_since_last_debug_message / 1000000000, filename,
|
||||
linenumber, " *warning: ");
|
||||
free(basec);
|
||||
} else {
|
||||
strncpy(b, "warning: ", sizeof(b));
|
||||
s = b + strlen(b);
|
||||
@@ -355,12 +520,13 @@ void _warn(const char *filename, const int linenumber, const char *format, ...)
|
||||
pthread_setcancelstate(oldState, NULL);
|
||||
}
|
||||
|
||||
void _debug(const char *filename, const int linenumber, int level, const char *format, ...) {
|
||||
void _debug(const char *thefilename, const int linenumber, int level, const char *format, ...) {
|
||||
if (level > debuglev)
|
||||
return;
|
||||
int oldState;
|
||||
pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, &oldState);
|
||||
char b[1024];
|
||||
|
||||
char b[16384];
|
||||
b[0] = 0;
|
||||
pthread_mutex_lock(&debug_timing_lock);
|
||||
uint64_t time_now = get_absolute_time_in_ns();
|
||||
@@ -368,21 +534,24 @@ void _debug(const char *filename, const int linenumber, int level, const char *f
|
||||
uint64_t time_since_last_debug_message = time_now - ns_time_at_last_debug_message;
|
||||
ns_time_at_last_debug_message = time_now;
|
||||
pthread_mutex_unlock(&debug_timing_lock);
|
||||
char *basec = strdup(thefilename);
|
||||
char *filename = basename(basec);
|
||||
char *s = generate_preliminary_string(b, sizeof(b), 1.0 * time_since_start / 1000000000,
|
||||
1.0 * time_since_last_debug_message / 1000000000, filename,
|
||||
linenumber, " ");
|
||||
free(basec);
|
||||
va_list args;
|
||||
va_start(args, format);
|
||||
vsnprintf(s, sizeof(b) - (s - b), format, args);
|
||||
va_end(args);
|
||||
sps_log(LOG_DEBUG, "%s", b);
|
||||
sps_log(LOG_INFO, b); // LOG_DEBUG is hard to read on macOS terminal
|
||||
pthread_setcancelstate(oldState, NULL);
|
||||
}
|
||||
|
||||
void _inform(const char *filename, const int linenumber, const char *format, ...) {
|
||||
void _inform(const char *thefilename, const int linenumber, const char *format, ...) {
|
||||
int oldState;
|
||||
pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, &oldState);
|
||||
char b[1024];
|
||||
char b[16384];
|
||||
b[0] = 0;
|
||||
char *s;
|
||||
if (debuglev) {
|
||||
@@ -392,9 +561,12 @@ void _inform(const char *filename, const int linenumber, const char *format, ...
|
||||
uint64_t time_since_last_debug_message = time_now - ns_time_at_last_debug_message;
|
||||
ns_time_at_last_debug_message = time_now;
|
||||
pthread_mutex_unlock(&debug_timing_lock);
|
||||
char *basec = strdup(thefilename);
|
||||
char *filename = basename(basec);
|
||||
s = generate_preliminary_string(b, sizeof(b), 1.0 * time_since_start / 1000000000,
|
||||
1.0 * time_since_last_debug_message / 1000000000, filename,
|
||||
linenumber, " ");
|
||||
free(basec);
|
||||
} else {
|
||||
s = b;
|
||||
}
|
||||
@@ -920,7 +1092,7 @@ void command_start(void) {
|
||||
void command_execute(const char *command, const char *extra_argument, const int block) {
|
||||
// this has a cancellation point if waiting is enabled
|
||||
if (command) {
|
||||
char new_command_buffer[1024];
|
||||
char new_command_buffer[2048];
|
||||
char *full_command = (char *)command;
|
||||
if (extra_argument != NULL) {
|
||||
memset(new_command_buffer, 0, sizeof(new_command_buffer));
|
||||
@@ -1028,6 +1200,8 @@ double vol2attn(double vol, long max_db, long min_db) {
|
||||
int i;
|
||||
for (i = 0; i < order; i++) {
|
||||
if (vol <= lines[i][0]) {
|
||||
if ((-30 - lines[i][0]) == 0.0)
|
||||
die("(-30 - lines[%d][0]) == 0.0!", i);
|
||||
double tvol = lines[i][1] * (vol - lines[i][0]) / (-30 - lines[i][0]);
|
||||
// debug(1,"On line %d, end point of %f, input vol %f yields output vol
|
||||
// %f.",i,lines[i][1],vol,tvol);
|
||||
@@ -1049,62 +1223,11 @@ double vol2attn(double vol, long max_db, long min_db) {
|
||||
return vol_setting;
|
||||
}
|
||||
|
||||
uint64_t get_absolute_time_in_fp() {
|
||||
uint64_t time_now_fp;
|
||||
#ifdef COMPILE_FOR_LINUX_AND_FREEBSD_AND_CYGWIN_AND_OPENBSD
|
||||
struct timespec tn;
|
||||
// can't use CLOCK_MONOTONIC_RAW as it's not implemented in OpenWrt
|
||||
clock_gettime(CLOCK_MONOTONIC, &tn);
|
||||
uint64_t tnfpsec = tn.tv_sec;
|
||||
if (tnfpsec > 0x100000000)
|
||||
warn("clock_gettime seconds overflow!");
|
||||
uint64_t tnfpnsec = tn.tv_nsec;
|
||||
if (tnfpnsec > 0x100000000)
|
||||
warn("clock_gettime nanoseconds seconds overflow!");
|
||||
tnfpsec = tnfpsec << 32;
|
||||
tnfpnsec = tnfpnsec << 32;
|
||||
tnfpnsec = tnfpnsec / 1000000000;
|
||||
|
||||
time_now_fp = tnfpsec + tnfpnsec; // types okay
|
||||
#endif
|
||||
#ifdef COMPILE_FOR_OSX
|
||||
uint64_t time_now_mach;
|
||||
uint64_t elapsedNano;
|
||||
static mach_timebase_info_data_t sTimebaseInfo = {0, 0};
|
||||
|
||||
time_now_mach = mach_absolute_time();
|
||||
|
||||
// If this is the first time we've run, get the timebase.
|
||||
// We can use denom == 0 to indicate that sTimebaseInfo is
|
||||
// uninitialised because it makes no sense to have a zero
|
||||
// denominator in a fraction.
|
||||
|
||||
if (sTimebaseInfo.denom == 0) {
|
||||
debug(1, "Mac initialise timebase info.");
|
||||
(void)mach_timebase_info(&sTimebaseInfo);
|
||||
}
|
||||
|
||||
// Do the maths. We hope that the multiplication doesn't
|
||||
// overflow; the price you pay for working in fixed point.
|
||||
|
||||
// this gives us nanoseconds
|
||||
uint64_t time_now_ns = time_now_mach * sTimebaseInfo.numer / sTimebaseInfo.denom;
|
||||
|
||||
// take the units and shift them to the upper half of the fp, and take the nanoseconds, shift them
|
||||
// to the upper half and then divide the result to 1000000000
|
||||
time_now_fp =
|
||||
((time_now_ns / 1000000000) << 32) + (((time_now_ns % 1000000000) << 32) / 1000000000);
|
||||
|
||||
#endif
|
||||
return time_now_fp;
|
||||
}
|
||||
|
||||
uint64_t get_absolute_time_in_ns() {
|
||||
uint64_t get_monotonic_time_in_ns() {
|
||||
uint64_t time_now_ns;
|
||||
|
||||
#ifdef COMPILE_FOR_LINUX_AND_FREEBSD_AND_CYGWIN_AND_OPENBSD
|
||||
struct timespec tn;
|
||||
// can't use CLOCK_MONOTONIC_RAW as it's not implemented in OpenWrt
|
||||
clock_gettime(CLOCK_MONOTONIC, &tn);
|
||||
uint64_t tnnsec = tn.tv_sec;
|
||||
tnnsec = tnnsec * 1000000000;
|
||||
@@ -1114,9 +1237,71 @@ uint64_t get_absolute_time_in_ns() {
|
||||
|
||||
#ifdef COMPILE_FOR_OSX
|
||||
uint64_t time_now_mach;
|
||||
uint64_t elapsedNano;
|
||||
static mach_timebase_info_data_t sTimebaseInfo = {0, 0};
|
||||
|
||||
// this actually give you a monotonic clock
|
||||
// see https://news.ycombinator.com/item?id=6303755
|
||||
time_now_mach = mach_absolute_time();
|
||||
|
||||
// If this is the first time we've run, get the timebase.
|
||||
// We can use denom == 0 to indicate that sTimebaseInfo is
|
||||
// uninitialised because it makes no sense to have a zero
|
||||
// denominator in a fraction.
|
||||
|
||||
if (sTimebaseInfo.denom == 0) {
|
||||
debug(1, "Mac initialise timebase info.");
|
||||
(void)mach_timebase_info(&sTimebaseInfo);
|
||||
}
|
||||
|
||||
if (sTimebaseInfo.denom == 0)
|
||||
die("could not initialise Mac timebase info in get_monotonic_time_in_ns().");
|
||||
|
||||
// Do the maths. We hope that the multiplication doesn't
|
||||
// overflow; the price you pay for working in fixed point.
|
||||
|
||||
// this gives us nanoseconds
|
||||
time_now_ns = time_now_mach * sTimebaseInfo.numer / sTimebaseInfo.denom;
|
||||
#endif
|
||||
|
||||
return time_now_ns;
|
||||
}
|
||||
|
||||
#ifdef COMPILE_FOR_LINUX_AND_FREEBSD_AND_CYGWIN_AND_OPENBSD
|
||||
// Not defined for macOS
|
||||
uint64_t get_realtime_in_ns() {
|
||||
uint64_t time_now_ns;
|
||||
struct timespec tn;
|
||||
clock_gettime(CLOCK_REALTIME, &tn);
|
||||
uint64_t tnnsec = tn.tv_sec;
|
||||
tnnsec = tnnsec * 1000000000;
|
||||
uint64_t tnjnsec = tn.tv_nsec;
|
||||
time_now_ns = tnnsec + tnjnsec;
|
||||
return time_now_ns;
|
||||
}
|
||||
#endif
|
||||
|
||||
uint64_t get_absolute_time_in_ns() {
|
||||
// CLOCK_MONOTONIC_RAW/CLOCK_MONOTONIC in Linux/FreeBSD etc, monotonic in MacOSX
|
||||
uint64_t time_now_ns;
|
||||
|
||||
#ifdef COMPILE_FOR_LINUX_AND_FREEBSD_AND_CYGWIN_AND_OPENBSD
|
||||
struct timespec tn;
|
||||
#ifdef CLOCK_MONOTONIC_RAW
|
||||
clock_gettime(CLOCK_MONOTONIC_RAW, &tn);
|
||||
#else
|
||||
clock_gettime(CLOCK_MONOTONIC, &tn);
|
||||
#endif
|
||||
uint64_t tnnsec = tn.tv_sec;
|
||||
tnnsec = tnnsec * 1000000000;
|
||||
uint64_t tnjnsec = tn.tv_nsec;
|
||||
time_now_ns = tnnsec + tnjnsec;
|
||||
#endif
|
||||
|
||||
#ifdef COMPILE_FOR_OSX
|
||||
uint64_t time_now_mach;
|
||||
static mach_timebase_info_data_t sTimebaseInfo = {0, 0};
|
||||
|
||||
// this actually give you a monotonic clock
|
||||
time_now_mach = mach_absolute_time();
|
||||
|
||||
// If this is the first time we've run, get the timebase.
|
||||
@@ -1132,6 +1317,9 @@ uint64_t get_absolute_time_in_ns() {
|
||||
// Do the maths. We hope that the multiplication doesn't
|
||||
// overflow; the price you pay for working in fixed point.
|
||||
|
||||
if (sTimebaseInfo.denom == 0)
|
||||
die("could not initialise Mac timebase info in get_absolute_time_in_ns().");
|
||||
|
||||
// this gives us nanoseconds
|
||||
time_now_ns = time_now_mach * sTimebaseInfo.numer / sTimebaseInfo.denom;
|
||||
#endif
|
||||
@@ -1266,6 +1454,16 @@ uint16_t nctohs(const uint8_t *p) { // read 2 characters from *p and do ntohs on
|
||||
return ntohs(holder);
|
||||
}
|
||||
|
||||
uint64_t nctoh64(const uint8_t *p) {
|
||||
uint32_t landing = nctohl(p); // get the high order 32 bits
|
||||
uint64_t vl = landing;
|
||||
vl = vl << 32; // shift them into the correct location
|
||||
landing = nctohl(p + sizeof(uint32_t)); // and the low order 32 bits
|
||||
uint64_t ul = landing;
|
||||
vl = vl + ul;
|
||||
return vl;
|
||||
}
|
||||
|
||||
pthread_mutex_t barrier_mutex = PTHREAD_MUTEX_INITIALIZER;
|
||||
|
||||
void memory_barrier() {
|
||||
@@ -1289,64 +1487,30 @@ void sps_nanosleep(const time_t sec, const long nanosec) {
|
||||
// Mac OS X doesn't have pthread_mutex_timedlock
|
||||
// Also note that timing must be relative to CLOCK_REALTIME
|
||||
|
||||
/*
|
||||
#ifdef COMPILE_FOR_LINUX_AND_FREEBSD_AND_CYGWIN_AND_OPENBSD
|
||||
int sps_pthread_mutex_timedlock(pthread_mutex_t *mutex, useconds_t dally_time,
|
||||
const char *debugmessage, int debuglevel) {
|
||||
int sps_pthread_mutex_timedlock(pthread_mutex_t *mutex, useconds_t dally_time) {
|
||||
|
||||
int oldState;
|
||||
pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, &oldState);
|
||||
struct timespec tn;
|
||||
clock_gettime(CLOCK_REALTIME, &tn);
|
||||
uint64_t tnfpsec = tn.tv_sec;
|
||||
if (tnfpsec > 0x100000000)
|
||||
warn("clock_gettime seconds overflow!");
|
||||
uint64_t tnfpnsec = tn.tv_nsec;
|
||||
if (tnfpnsec > 0x100000000)
|
||||
warn("clock_gettime nanoseconds seconds overflow!");
|
||||
tnfpsec = tnfpsec << 32;
|
||||
tnfpnsec = tnfpnsec << 32;
|
||||
tnfpnsec = tnfpnsec / 1000000000;
|
||||
|
||||
uint64_t time_now_in_fp = tnfpsec + tnfpnsec; // types okay
|
||||
|
||||
uint64_t dally_time_in_fp = dally_time; // microseconds
|
||||
dally_time_in_fp = (dally_time_in_fp << 32) / 1000000; // convert to fp format
|
||||
uint64_t time_then = time_now_in_fp + dally_time_in_fp;
|
||||
|
||||
uint64_t time_then_nsec = time_then & 0xffffffff; // remove integral part
|
||||
time_then_nsec = time_then_nsec * 1000000000; // multiply fractional part to nanoseconds
|
||||
|
||||
struct timespec timeoutTime;
|
||||
|
||||
time_then = time_then >> 32; // get the seconds
|
||||
time_then_nsec = time_then_nsec >> 32; // and the nanoseconds
|
||||
|
||||
timeoutTime.tv_sec = time_then;
|
||||
timeoutTime.tv_nsec = time_then_nsec;
|
||||
uint64_t start_time = get_absolute_time_in_ns();
|
||||
uint64_t wait_until_time = dally_time * 1000; // to nanoseconds
|
||||
uint64_t start_time = get_realtime_in_ns(); // this is from CLOCK_REALTIME
|
||||
wait_until_time = wait_until_time + start_time;
|
||||
uint64_t wait_until_sec = wait_until_time / 1000000000;
|
||||
uint64_t wait_until_nsec = wait_until_time % 1000000000;
|
||||
timeoutTime.tv_sec = wait_until_sec;
|
||||
timeoutTime.tv_nsec = wait_until_nsec;
|
||||
int r = pthread_mutex_timedlock(mutex, &timeoutTime);
|
||||
uint64_t et = get_absolute_time_in_ns() - start_time;
|
||||
|
||||
if ((debuglevel != 0) && (r != 0) && (debugmessage != NULL)) {
|
||||
char errstr[1000];
|
||||
if (r == ETIMEDOUT)
|
||||
debug(debuglevel,
|
||||
"timed out waiting for a mutex, having waited %f microseconds, with a maximum "
|
||||
"waiting time of %d microseconds. \"%s\".",
|
||||
(1.0E6 * et) / 1000000000, dally_time, debugmessage);
|
||||
else
|
||||
debug(debuglevel, "error %d: \"%s\" waiting for a mutex: \"%s\".", r,
|
||||
strerror_r(r, errstr, sizeof(errstr)), debugmessage);
|
||||
}
|
||||
pthread_setcancelstate(oldState, NULL);
|
||||
return r;
|
||||
}
|
||||
#endif
|
||||
#ifdef COMPILE_FOR_OSX
|
||||
int sps_pthread_mutex_timedlock(pthread_mutex_t *mutex, useconds_t dally_time,
|
||||
const char *debugmessage, int debuglevel) {
|
||||
*/
|
||||
int sps_pthread_mutex_timedlock(pthread_mutex_t *mutex, useconds_t dally_time) {
|
||||
|
||||
// this is not pthread_cancellation safe because is contains a cancellation point
|
||||
// this would not be not pthread_cancellation safe because is contains a cancellation point
|
||||
int oldState;
|
||||
pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, &oldState);
|
||||
int time_to_wait = dally_time;
|
||||
@@ -1359,22 +1523,10 @@ int sps_pthread_mutex_timedlock(pthread_mutex_t *mutex, useconds_t dally_time,
|
||||
time_to_wait -= st;
|
||||
r = pthread_mutex_trylock(mutex);
|
||||
}
|
||||
if ((debuglevel != 0) && (r != 0) && (debugmessage != NULL)) {
|
||||
char errstr[1000];
|
||||
if (r == EBUSY) {
|
||||
debug(debuglevel,
|
||||
"waiting for a mutex, maximum expected time of %d microseconds exceeded \"%s\".",
|
||||
dally_time, debugmessage);
|
||||
r = ETIMEDOUT; // for compatibility
|
||||
} else {
|
||||
debug(debuglevel, "error %d: \"%s\" waiting for a mutex: \"%s\".", r,
|
||||
strerror_r(r, errstr, sizeof(errstr)), debugmessage);
|
||||
}
|
||||
}
|
||||
pthread_setcancelstate(oldState, NULL);
|
||||
return r;
|
||||
}
|
||||
#endif
|
||||
// #endif
|
||||
|
||||
int _debug_mutex_lock(pthread_mutex_t *mutex, useconds_t dally_time, const char *mutexname,
|
||||
const char *filename, const int line, int debuglevel) {
|
||||
@@ -1382,19 +1534,20 @@ int _debug_mutex_lock(pthread_mutex_t *mutex, useconds_t dally_time, const char
|
||||
return pthread_mutex_lock(mutex);
|
||||
int oldState;
|
||||
pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, &oldState);
|
||||
uint64_t time_at_start = get_absolute_time_in_ns();
|
||||
char dstring[1000];
|
||||
memset(dstring, 0, sizeof(dstring));
|
||||
snprintf(dstring, sizeof(dstring), "%s:%d", filename, line);
|
||||
if (debuglevel != 0)
|
||||
debug(3, "mutex_lock \"%s\" at \"%s\".", mutexname, dstring); // only if you really ask for it!
|
||||
int result = sps_pthread_mutex_timedlock(mutex, dally_time, dstring, debuglevel);
|
||||
_debug(filename, line, 3, "mutex_lock \"%s\".", mutexname); // only if you really ask for it!
|
||||
int result = sps_pthread_mutex_timedlock(mutex, dally_time);
|
||||
if (result == ETIMEDOUT) {
|
||||
_debug(
|
||||
filename, line, debuglevel,
|
||||
"mutex_lock \"%s\" failed to lock after %f ms -- now waiting unconditionally to lock it.",
|
||||
mutexname, dally_time * 1E-3);
|
||||
result = pthread_mutex_lock(mutex);
|
||||
uint64_t time_delay = get_absolute_time_in_ns() - time_at_start;
|
||||
debug(debuglevel,
|
||||
"mutex_lock \"%s\" at \"%s\" expected max wait: %0.9f, actual wait: %0.9f microseconds.",
|
||||
mutexname, dstring, (1.0 * dally_time), 0.001 * time_delay);
|
||||
if (result == 0)
|
||||
_debug(filename, line, debuglevel, " ...mutex_lock \"%s\" locked successfully.", mutexname);
|
||||
else
|
||||
_debug(filename, line, debuglevel, " ...mutex_lock \"%s\" exited with error code: %u",
|
||||
mutexname, result);
|
||||
}
|
||||
pthread_setcancelstate(oldState, NULL);
|
||||
return result;
|
||||
@@ -1422,7 +1575,44 @@ int _debug_mutex_unlock(pthread_mutex_t *mutex, const char *mutexname, const cha
|
||||
void malloc_cleanup(void *arg) {
|
||||
// debug(1, "malloc cleanup called.");
|
||||
free(arg);
|
||||
arg = NULL;
|
||||
}
|
||||
|
||||
#ifdef CONFIG_AIRPLAY_2
|
||||
void plist_cleanup(void *arg) {
|
||||
// debug(1, "plist cleanup called.");
|
||||
plist_free((plist_t)arg);
|
||||
}
|
||||
#endif
|
||||
|
||||
void socket_cleanup(void *arg) {
|
||||
intptr_t fdp = (intptr_t)arg;
|
||||
debug(3, "socket_cleanup called for socket: %" PRIdPTR ".", fdp);
|
||||
close(fdp);
|
||||
}
|
||||
|
||||
void cv_cleanup(void *arg) {
|
||||
// debug(1, "cv_cleanup called.");
|
||||
pthread_cond_t *cv = (pthread_cond_t *)arg;
|
||||
pthread_cond_destroy(cv);
|
||||
}
|
||||
|
||||
void mutex_cleanup(void *arg) {
|
||||
// debug(1, "mutex_cleanup called.");
|
||||
pthread_mutex_t *mutex = (pthread_mutex_t *)arg;
|
||||
pthread_mutex_destroy(mutex);
|
||||
}
|
||||
|
||||
void mutex_unlock(void *arg) { pthread_mutex_unlock((pthread_mutex_t *)arg); }
|
||||
|
||||
void thread_cleanup(void *arg) {
|
||||
debug(3, "thread_cleanup called.");
|
||||
pthread_t *thread = (pthread_t *)arg;
|
||||
pthread_cancel(*thread);
|
||||
int oldState;
|
||||
pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, &oldState);
|
||||
pthread_join(*thread, NULL);
|
||||
pthread_setcancelstate(oldState, NULL);
|
||||
debug(3, "thread_cleanup done.");
|
||||
}
|
||||
|
||||
void pthread_cleanup_debug_mutex_unlock(void *arg) { pthread_mutex_unlock((pthread_mutex_t *)arg); }
|
||||
@@ -1430,8 +1620,15 @@ void pthread_cleanup_debug_mutex_unlock(void *arg) { pthread_mutex_unlock((pthre
|
||||
char *get_version_string() {
|
||||
char *version_string = malloc(1024);
|
||||
if (version_string) {
|
||||
strcpy(version_string, PACKAGE_VERSION);
|
||||
|
||||
#ifdef CONFIG_USE_GIT_VERSION_STRING
|
||||
if (git_version_string[0] != '\0')
|
||||
strcpy(version_string, git_version_string);
|
||||
else
|
||||
#endif
|
||||
strcpy(version_string, PACKAGE_VERSION);
|
||||
#ifdef CONFIG_AIRPLAY_2
|
||||
strcat(version_string, "-AirPlay2");
|
||||
#endif
|
||||
#ifdef CONFIG_APPLE_ALAC
|
||||
strcat(version_string, "-alac");
|
||||
#endif
|
||||
@@ -1474,6 +1671,9 @@ char *get_version_string() {
|
||||
#ifdef CONFIG_PA
|
||||
strcat(version_string, "-pa");
|
||||
#endif
|
||||
#ifdef CONFIG_PW
|
||||
strcat(version_string, "-pw");
|
||||
#endif
|
||||
#ifdef CONFIG_SOUNDIO
|
||||
strcat(version_string, "-soundio");
|
||||
#endif
|
||||
@@ -1723,3 +1923,78 @@ void *memdup(const void *mem, size_t size) {
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
// This will allocate memory and place the NUL-terminated hex character equivalent of
|
||||
// the bytearray passed in whose length is given.
|
||||
char *debug_malloc_hex_cstring(void *packet, size_t nread) {
|
||||
char *response = malloc(nread * 3 + 1);
|
||||
unsigned char *q = packet;
|
||||
char *obfp = response;
|
||||
size_t obfc;
|
||||
for (obfc = 0; obfc < nread; obfc++) {
|
||||
snprintf(obfp, 4, "%02x ", *q);
|
||||
obfp += 3; // two digit characters and a space
|
||||
q++;
|
||||
};
|
||||
obfp--; // overwrite the last space with a NUL
|
||||
*obfp = 0;
|
||||
return response;
|
||||
}
|
||||
|
||||
// the difference between two unsigned 32-bit modulo values as a signed 32-bit result
|
||||
// now, if the two numbers are constrained to be within 2^(n-1)-1 of one another,
|
||||
// we can use their as a signed 2^n bit number which will be positive
|
||||
// if the first number is the same or "after" the second, and
|
||||
// negative otherwise
|
||||
|
||||
int32_t mod32Difference(uint32_t a, uint32_t b) {
|
||||
int32_t result = a - b;
|
||||
return result;
|
||||
}
|
||||
|
||||
int get_device_id(uint8_t *id, int int_length) {
|
||||
int response = 0;
|
||||
struct ifaddrs *ifaddr = NULL;
|
||||
struct ifaddrs *ifa = NULL;
|
||||
int i = 0;
|
||||
uint8_t *t = id;
|
||||
for (i = 0; i < int_length; i++) {
|
||||
*t++ = 0;
|
||||
}
|
||||
|
||||
if (getifaddrs(&ifaddr) == -1) {
|
||||
response = -1;
|
||||
} else {
|
||||
t = id;
|
||||
int found = 0;
|
||||
|
||||
for (ifa = ifaddr; ((ifa != NULL) && (found == 0)); ifa = ifa->ifa_next) {
|
||||
#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 ((strcmp(ifa->ifa_name, "lo") != 0)) {
|
||||
found = 1;
|
||||
for (i = 0; ((i < s->sll_halen) && (i < int_length)); i++) {
|
||||
*t++ = s->sll_addr[i];
|
||||
}
|
||||
}
|
||||
}
|
||||
#else
|
||||
#ifdef AF_LINK
|
||||
struct sockaddr_dl *sdl = (struct sockaddr_dl *)ifa->ifa_addr;
|
||||
if ((sdl) && (sdl->sdl_family == AF_LINK)) {
|
||||
if (sdl->sdl_type == IFT_ETHER) {
|
||||
found = 1;
|
||||
uint8_t *s = (uint8_t *)LLADDR(sdl);
|
||||
for (i = 0; ((i < sdl->sdl_alen) && (i < int_length)); i++) {
|
||||
*t++ = *s++;
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
#endif
|
||||
}
|
||||
freeifaddrs(ifaddr);
|
||||
}
|
||||
return response;
|
||||
}
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
#ifndef _COMMON_H
|
||||
#define _COMMON_H
|
||||
|
||||
@@ -30,6 +34,12 @@ typedef enum {
|
||||
} dbus_session_type;
|
||||
#endif
|
||||
|
||||
typedef enum {
|
||||
TOE_normal,
|
||||
TOE_emergency,
|
||||
TOE_dbus // a dbus request was made -- don't wait for the dbus thread to exit
|
||||
} type_of_exit_type;
|
||||
|
||||
#define sps_extra_code_output_stalled 32768
|
||||
#define sps_extra_code_output_state_cannot_make_ready 32769
|
||||
|
||||
@@ -147,8 +157,9 @@ typedef struct {
|
||||
int mqtt_publish_parsed;
|
||||
int mqtt_publish_cover;
|
||||
int mqtt_enable_remote;
|
||||
char *mqtt_empty_payload_substitute;
|
||||
#endif
|
||||
uint8_t hw_addr[6];
|
||||
uint8_t hw_addr[8]; // only needs 6 but 8 is handy when converting this to a number
|
||||
int port;
|
||||
int udp_port_base;
|
||||
int udp_port_range;
|
||||
@@ -205,9 +216,11 @@ typedef struct {
|
||||
// char *errfile;
|
||||
char *configfile;
|
||||
char *regtype; // The regtype is the service type followed by the protocol, separated by a dot, by
|
||||
// default “_raop._tcp.”.
|
||||
char *interface; // a string containg the interface name, or NULL if nothing specified
|
||||
int interface_index; // only valid if the interface string is non-NULL
|
||||
// default “_raop._tcp.” for AirPlay 1.
|
||||
char *regtype2; // The regtype is the service type followed by the protocol, separated by a dot,
|
||||
// by default “_raop._tcp.” for AirPlay 2.
|
||||
char *interface; // a string containg the interface name, or NULL if nothing specified
|
||||
int interface_index; // only valid if the interface string is non-NULL
|
||||
double audio_backend_buffer_desired_length; // this will be the length in seconds of the
|
||||
// audio backend buffer -- the DAC buffer for ALSA
|
||||
double audio_backend_buffer_interpolation_threshold_in_seconds; // below this, soxr interpolation
|
||||
@@ -289,14 +302,24 @@ typedef struct {
|
||||
// can't use IP numbers as they might be given to different devices
|
||||
// can't get hold of MAC addresses.
|
||||
// can't define the nvll linked list struct here
|
||||
|
||||
#ifdef CONFIG_AIRPLAY_2
|
||||
uint64_t airplay_features;
|
||||
uint32_t airplay_statusflags;
|
||||
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 *nqptp_shared_memory_interface_name; // client name for nqptp service
|
||||
#endif
|
||||
} shairport_cfg;
|
||||
|
||||
// accessors to config for multi-thread access
|
||||
double get_config_airplay_volume();
|
||||
void set_config_airplay_volume(double v);
|
||||
|
||||
uint32_t nctohl(const uint8_t *p); // read 4 characters from *p and do ntohl on them
|
||||
uint16_t nctohs(const uint8_t *p); // read 2 characters from *p and do ntohs on them
|
||||
uint32_t nctohl(const uint8_t *p); // read 4 characters from *p and do ntohl on them
|
||||
uint16_t nctohs(const uint8_t *p); // read 2 characters from *p and do ntohs on them
|
||||
uint64_t nctoh64(const uint8_t *p); // read 8 characters from *p to a uint64_t
|
||||
|
||||
void memory_barrier();
|
||||
|
||||
@@ -363,9 +386,13 @@ double flat_vol2attn(double vol, long max_db, long min_db);
|
||||
// dB), return an attenuation depending on the transfer function
|
||||
double vol2attn(double vol, long max_db, long min_db);
|
||||
|
||||
// return a monolithic (always increasing) time in nanoseconds
|
||||
// uint64_t get_absolute_time_in_fp(void); // obselete
|
||||
uint64_t get_absolute_time_in_ns(void);
|
||||
// return a time in nanoseconds
|
||||
#ifdef COMPILE_FOR_LINUX_AND_FREEBSD_AND_CYGWIN_AND_OPENBSD
|
||||
// Not defined for macOS
|
||||
uint64_t get_realtime_in_ns(void);
|
||||
#endif
|
||||
uint64_t get_absolute_time_in_ns(void); // monotonic_raw or monotonic
|
||||
uint64_t get_monotonic_time_in_ns(void); // NTP-disciplined
|
||||
|
||||
// time at startup for debugging timing
|
||||
extern uint64_t ns_time_at_startup, ns_time_at_last_debug_message;
|
||||
@@ -379,7 +406,7 @@ extern pthread_t main_thread_id;
|
||||
|
||||
extern shairport_cfg config;
|
||||
extern config_t config_file_stuff;
|
||||
extern int emergency_exit;
|
||||
extern int type_of_exit_cleanup; // normal, emergency, dbus requested...
|
||||
|
||||
int config_set_lookup_bool(config_t *cfg, char *where, int *dst);
|
||||
|
||||
@@ -402,8 +429,8 @@ extern pthread_mutex_t the_conn_lock;
|
||||
pthread_mutex_unlock(&the_conn_lock);
|
||||
|
||||
// wait for the specified time in microseconds -- it checks every 20 milliseconds
|
||||
int sps_pthread_mutex_timedlock(pthread_mutex_t *mutex, useconds_t dally_time,
|
||||
const char *debugmessage, int debuglevel);
|
||||
// int sps_pthread_mutex_timedlock(pthread_mutex_t *mutex, useconds_t dally_time,
|
||||
// const char *debugmessage, int debuglevel);
|
||||
// wait for the specified time, checking every 20 milliseconds, and block if it can't acquire the
|
||||
// lock
|
||||
int _debug_mutex_lock(pthread_mutex_t *mutex, useconds_t dally_time, const char *mutexName,
|
||||
@@ -447,4 +474,43 @@ int string_update_with_size(char **str, int *flag, char *s, size_t len);
|
||||
// from https://stackoverflow.com/questions/13663617/memdup-function-in-c, with thanks
|
||||
void *memdup(const void *mem, size_t size);
|
||||
|
||||
int bind_socket_and_port(int type, int ip_family, const char *self_ip_address, uint32_t scope_id,
|
||||
uint16_t *port, int *sock);
|
||||
|
||||
uint16_t bind_UDP_port(int ip_family, const char *self_ip_address, uint32_t scope_id, int *sock);
|
||||
|
||||
void socket_cleanup(void *arg);
|
||||
void mutex_unlock(void *arg);
|
||||
void mutex_cleanup(void *arg);
|
||||
void cv_cleanup(void *arg);
|
||||
void thread_cleanup(void *arg);
|
||||
#ifdef CONFIG_AIRPLAY_2
|
||||
void plist_cleanup(void *arg);
|
||||
#endif
|
||||
|
||||
char *debug_malloc_hex_cstring(void *packet, size_t nread);
|
||||
|
||||
// from https://stackoverflow.com/questions/13663617/memdup-function-in-c, with thanks
|
||||
// allocates memory and copies the content to it
|
||||
// analogous to strndup;
|
||||
void *memdup(const void *mem, size_t size);
|
||||
|
||||
// the difference between two unsigned 32-bit modulo values as a signed 32-bit result
|
||||
// now, if the two numbers are constrained to be within 2^(n-1)-1 of one another,
|
||||
// we can use their as a signed 2^n bit number which will be positive
|
||||
// if the first number is the same or "after" the second, and
|
||||
// negative otherwise
|
||||
|
||||
int32_t mod32Difference(uint32_t a, uint32_t b);
|
||||
|
||||
int get_device_id(uint8_t *id, int int_length);
|
||||
|
||||
#ifdef CONFIG_USE_GIT_VERSION_STRING
|
||||
extern char git_version_string[];
|
||||
#endif
|
||||
|
||||
#endif // _COMMON_H
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
+71
-14
@@ -2,24 +2,33 @@
|
||||
# Process this file with autoconf to produce a configure script.
|
||||
|
||||
AC_PREREQ([2.50])
|
||||
AC_INIT([shairport-sync], [3.3.9], [4265913+mikebrady@users.noreply.github.com])
|
||||
AC_INIT([shairport-sync], [4.1-dev], [4265913+mikebrady@users.noreply.github.com])
|
||||
AM_INIT_AUTOMAKE([subdir-objects])
|
||||
AC_CONFIG_SRCDIR([shairport.c])
|
||||
AC_CONFIG_HEADERS([config.h])
|
||||
AC_PROG_RANLIB
|
||||
|
||||
AC_CHECK_PROGS([GIT], [git])
|
||||
if test -n "$GIT" && test -e "$srcdir/.git/index" ; then
|
||||
AC_DEFINE([CONFIG_USE_GIT_VERSION_STRING], 1, [Use the version string produced by running 'git describe --dirty'.])
|
||||
fi
|
||||
AM_CONDITIONAL([USE_GIT_VERSION], [test -n "$GIT" && test -e "$srcdir/.git/index" ])
|
||||
|
||||
# Derived from the Avahi configure.ac file
|
||||
# Specifying the OS type, defaulting to linux.
|
||||
#
|
||||
AC_ARG_WITH(os, AS_HELP_STRING([--with-os=OSType],[Specify the distribution to target: One of linux freebsd or darwin]))
|
||||
AC_ARG_WITH(os, AS_HELP_STRING([--with-os=OSType],[Specify the distribution to target: One of linux, freebsd, openbsd or darwin]))
|
||||
if test "z$with_os" = "z"; then
|
||||
with_os="linux"
|
||||
fi
|
||||
with_os=`echo ${with_os} | tr '[[:upper:]]' '[[:lower:]]' `
|
||||
|
||||
if test "x${with_os}" != xlinux && "x${with_os}" != xfreebsd && "x${with_os}" != xdarwin ; then
|
||||
AC_MSG_ERROR(--with-os=<arg> argument must linux, freebsd, openbsd or darwin)
|
||||
fi
|
||||
case "x${with_os}" in
|
||||
xlinux|xfreebsd|xopenbsd|xdarwin)
|
||||
;;
|
||||
*)
|
||||
AC_MSG_ERROR([--with-os=<arg> argument must be one of linux, freebsd, openbsd or darwin])
|
||||
;;
|
||||
esac
|
||||
|
||||
# Checks for programs.
|
||||
AC_PROG_CC
|
||||
@@ -37,10 +46,11 @@ if test "x${with_os}" = xopenbsd ; then
|
||||
AC_CHECK_LIB([c],[clock_gettime], , AC_MSG_ERROR(libc needed))
|
||||
fi
|
||||
|
||||
# Pass the conditionals to automake
|
||||
AM_CONDITIONAL([BUILD_FOR_LINUX], [test "x${with_os}" = xlinux ])
|
||||
AM_CONDITIONAL([BUILD_FOR_FREEBSD], [test "x${with_os}" = xfreebsd ])
|
||||
AM_CONDITIONAL([BUILD_FOR_OPENBSD], [test "x${with_os}" = xopenbsd ])
|
||||
|
||||
AM_CONDITIONAL([BUILD_FOR_DARWIN], [test "x${with_os}" = xdarwin ])
|
||||
|
||||
##### Some build systems are not fully using pkg-config, so we can use the flag ${with_pkg_config} on a case-by-case basis
|
||||
##### to control how to deal with them
|
||||
@@ -65,21 +75,18 @@ fi
|
||||
|
||||
AC_ARG_WITH([dummy],[AS_HELP_STRING([--with-dummy],[include the dummy audio back end])])
|
||||
if test "x$with_dummy" = "xyes" ; then
|
||||
AC_MSG_RESULT(include the dummy audio back end)
|
||||
AC_DEFINE([CONFIG_DUMMY], 1, [Include a fake audio backend.])
|
||||
fi
|
||||
AM_CONDITIONAL([USE_DUMMY], [test "x$with_dummy" = "xyes" ])
|
||||
|
||||
AC_ARG_WITH([stdout],[AS_HELP_STRING([--with-stdout],[include the stdout audio back end])])
|
||||
if test "x$with_stdout" = "xyes" ; then
|
||||
AC_MSG_RESULT(include the stdout audio back end)
|
||||
AC_DEFINE([CONFIG_STDOUT], 1, [Include an audio backend to output to standard output (stdout).])
|
||||
fi
|
||||
AM_CONDITIONAL([USE_STDOUT], [test "x$with_stdout" = "xyes"])
|
||||
|
||||
AC_ARG_WITH([pipe],[AS_HELP_STRING([--with-pipe],[include the pipe audio back end])])
|
||||
if test "x$with_pipe" = "xyes" ; then
|
||||
AC_MSG_RESULT(include the pipe audio back end)
|
||||
AC_DEFINE([CONFIG_PIPE], 1, [Include an audio backend to output to a unix pipe.])
|
||||
fi
|
||||
AM_CONDITIONAL([USE_PIPE], [test "x$with_pipe" = "xyes" ])
|
||||
@@ -221,6 +228,9 @@ if test "x$with_avahi" = "xyes" ; then
|
||||
AC_DEFINE([CONFIG_AVAHI], 1, [Include Avahi-based mDNS support.])
|
||||
AC_CHECK_LIB([avahi-client], [avahi_client_new], , AC_MSG_ERROR(Avahi support requires the avahi-client library!))
|
||||
AC_CHECK_LIB([avahi-common],[avahi_strerror], , AC_MSG_ERROR(Avahi support requires the avahi-common library!))
|
||||
AC_CONFIG_FILES([scripts/shairport-sync.service-avahi])
|
||||
else
|
||||
AC_CONFIG_FILES([scripts/shairport-sync.service])
|
||||
fi
|
||||
AM_CONDITIONAL([USE_AVAHI], [test "x$with_avahi" = "xyes"])
|
||||
|
||||
@@ -299,11 +309,33 @@ if test "x$with_pa" = "xyes" ; then
|
||||
fi
|
||||
AM_CONDITIONAL([USE_PA], [test "x$with_pa" = "xyes"])
|
||||
|
||||
# Look for pipewire flag
|
||||
AC_ARG_WITH(pw, [AS_HELP_STRING([--with-pw],[choose Pipewire support.])])
|
||||
if test "x$with_pw" = "xyes" ; then
|
||||
AC_DEFINE([CONFIG_PW], 1, [Include Pipewire support.])
|
||||
if test "x${with_pkg_config}" = xyes ; then
|
||||
PKG_CHECK_MODULES(
|
||||
[PIPEWIRE], [libpipewire-0.3 >= 0.3.24],
|
||||
[CFLAGS="${PIPEWIRE_CFLAGS} ${CFLAGS} -Wno-missing-field-initializers" LIBS="${PIPEWIRE_LIBS} ${LIBS}"],
|
||||
[AC_MSG_ERROR(Pipewire support requires the libpipewire library -- libpipewire-0.3-dev suggested!)])
|
||||
else
|
||||
AC_CHECK_LIB([pipewire], [pw_stream_queue_buffer], , AC_MSG_ERROR(Pipewire support requires the libpipewire library -- libpipewire-0.3-dev suggested!))
|
||||
fi
|
||||
fi
|
||||
AM_CONDITIONAL([USE_PW], [test "x$with_pw" = "xyes"])
|
||||
|
||||
# Look for Convolution flag
|
||||
AC_ARG_WITH(convolution, [AS_HELP_STRING([--with-convolution],[choose audio DSP convolution support])])
|
||||
if test "x$with_convolution" = "xyes" ; then
|
||||
AC_DEFINE([CONFIG_CONVOLUTION], 1, [Include audio DSP convolution support.])
|
||||
AC_CHECK_LIB([sndfile], [sf_open], , AC_MSG_ERROR(Convolution support requires the sndfile library -- libsndfile1-dev suggested!))
|
||||
if test "x${with_pkg_config}" = xyes ; then
|
||||
PKG_CHECK_MODULES(
|
||||
[sndfile], [sndfile],
|
||||
[CFLAGS="${sndfile_CFLAGS} ${CFLAGS}"
|
||||
LIBS="${sndfile_LIBS} ${LIBS}"], AC_MSG_ERROR(Convolution support requires the sndfile library -- libsndfile1-dev suggested!))
|
||||
else
|
||||
AC_CHECK_LIB([sndfile], [sf_open], , AC_MSG_ERROR(Convolution support requires the sndfile library -- libsndfile1-dev suggested!))
|
||||
fi
|
||||
fi
|
||||
AM_CONDITIONAL([USE_CONVOLUTION], [test "x$with_convolution" = "xyes"])
|
||||
|
||||
@@ -342,7 +374,7 @@ AC_ARG_WITH(mpris-test-client, [AS_HELP_STRING([--with-mpris-test-client],[compi
|
||||
AM_CONDITIONAL([USE_MPRIS_CLIENT], [test "x$with_mpris_test_client" = "xyes"])
|
||||
|
||||
if test "x$with_mpris_test_client" = "xyes" || test "x$with_dbus_test_client" = "xyes" || test "x$with_mpris_interface" = "xyes" || test "x$with_dbus_interface" = "xyes" ; then
|
||||
PKG_CHECK_MODULES([glib], [gio-unix-2.0 >= 2.30.0],[CFLAGS="${glib_CFLAGS} ${CFLAGS}" LIBS="${glib_LIBS} ${LIBS}"],[AC_MSG_ERROR(MPRIS client support requires the glib 2.0 library -- libglib2.0-dev suggested!)])
|
||||
PKG_CHECK_MODULES([glib], [gio-unix-2.0 >= 2.30.0],[CFLAGS="${glib_CFLAGS} ${CFLAGS}" LIBS="${glib_LIBS} ${LIBS}"],[AC_MSG_ERROR(the glib 2.0 library is required -- libglib2.0-dev suggested!)])
|
||||
fi
|
||||
|
||||
# Look for mqtt flag
|
||||
@@ -370,6 +402,31 @@ if test "x$REQUESTED_EXTENDED_METADATA_SUPPORT" = "x1" || test "x$with_metadata"
|
||||
fi
|
||||
AM_CONDITIONAL([USE_METADATA], [test "x$with_metadata" = "xyes"])
|
||||
|
||||
# Look for AirPlay 2 flag
|
||||
AC_ARG_WITH(airplay-2, [AS_HELP_STRING([--with-airplay-2],[Build for AirPlay 2])])
|
||||
if test "x$with_airplay_2" = "xyes" ; then
|
||||
AC_DEFINE([CONFIG_AIRPLAY_2], 1, [Build for AirPlay 2])
|
||||
PKG_CHECK_MODULES([libplist], [libplist >= 2.0.0],[CFLAGS="${libplist_CFLAGS} ${CFLAGS}" LIBS="${libplist_LIBS} ${LIBS}"],[
|
||||
PKG_CHECK_MODULES([libplist], [libplist-2.0 >= 2.0.0],[CFLAGS="${libplist_CFLAGS} ${CFLAGS}" LIBS="${libplist_LIBS} ${LIBS}"],[
|
||||
AC_MSG_ERROR(AirPlay 2 support requires libplist 2.0.0 or later -- search for pkg libplist-dev on Debian or libplist-2.2.0 or later on FreeBSD!)
|
||||
])
|
||||
])
|
||||
PKG_CHECK_MODULES([libsodium], [libsodium],[CFLAGS="${libsodium_CFLAGS} ${CFLAGS}" LIBS="${libsodium_LIBS} ${LIBS}"],[AC_MSG_ERROR(AirPlay 2 support requires libsodium -- libsodium-dev suggested)])
|
||||
PKG_CHECK_MODULES([libgcrypt], [libgcrypt],[CFLAGS="${libgcrypt_CFLAGS} ${CFLAGS}" LIBS="${libgcrypt_LIBS} ${LIBS}"],[
|
||||
AC_CHECK_LIB([gcrypt], [gcry_control], [], [
|
||||
AC_MSG_ERROR([Airplay 2 support requires libgcrypt -- libgcrypt-dev suggested])
|
||||
])
|
||||
])
|
||||
PKG_CHECK_MODULES([libavutil], [libavutil],[CFLAGS="${libavutil_CFLAGS} ${CFLAGS}" LIBS="${libavutil_LIBS} ${LIBS}"],[
|
||||
AC_MSG_ERROR(AirPlay 2 support requires libavutil -- libavutil-dev suggested)])
|
||||
PKG_CHECK_MODULES([libavcodec], [libavcodec],[CFLAGS="${libavcodec_CFLAGS} ${CFLAGS}" LIBS="${libavcodec_LIBS} ${LIBS}"],[
|
||||
AC_MSG_ERROR(AirPlay 2 support requires libavcodec -- libavcodec-dev suggested)])
|
||||
AC_CHECK_LIB([avformat],[avformat_new_stream], [], [AC_MSG_ERROR([AirPlay 2 support requires libavformat -- libavformat-dev suggested])])
|
||||
AC_CHECK_LIB([swresample],[swr_convert], [], [AC_MSG_ERROR([AirPlay 2 support requires libswresample -- libswresample-dev suggested])])
|
||||
AC_CHECK_LIB([uuid],[uuid_generate], [], [AC_MSG_ERROR([AirPlay 2 support requires the uuid library -- uuid-dev suggested])])
|
||||
fi
|
||||
AM_CONDITIONAL([USE_AIRPLAY_2], [test "x$with_airplay_2" = "xyes"])
|
||||
|
||||
if test "x${with_systemd}" = xyes ; then
|
||||
# Find systemd unit dir
|
||||
AC_ARG_WITH([systemdsystemunitdir],
|
||||
@@ -395,7 +452,6 @@ fi
|
||||
AM_CONDITIONAL([HAVE_XMLTOMAN], [test -n "$XMLTOMAN"])
|
||||
|
||||
# Checks for header files.
|
||||
AC_HEADER_STDC
|
||||
AC_CHECK_HEADERS([getopt_long.h])
|
||||
AC_CHECK_HEADERS([arpa/inet.h fcntl.h limits.h mach/mach.h memory.h netdb.h netinet/in.h stdint.h stdlib.h string.h sys/ioctl.h sys/socket.h sys/time.h syslog.h unistd.h])
|
||||
|
||||
@@ -418,6 +474,7 @@ AC_FUNC_ERROR_AT_LINE
|
||||
AC_FUNC_FORK
|
||||
AC_CHECK_FUNCS([atexit clock_gettime gethostname inet_ntoa memchr memmove memset mkfifo pow select socket stpcpy strcasecmp strchr strdup strerror strstr strtol strtoul])
|
||||
|
||||
AC_CONFIG_FILES([Makefile man/Makefile scripts/shairport-sync.service])
|
||||
# Note -- there are AC_CONFIG_FILES directives further back, conditional on Avahi
|
||||
AC_CONFIG_FILES([Makefile man/Makefile])
|
||||
AC_CONFIG_FILES([scripts/shairport-sync],[chmod +x scripts/shairport-sync])
|
||||
AC_OUTPUT
|
||||
|
||||
@@ -206,7 +206,9 @@ int dacp_send_command(const char *command, char **body, ssize_t *bodysize) {
|
||||
pthread_cleanup_push(addrinfo_cleanup, (void *)&res);
|
||||
// only do this one at a time -- not sure it is necessary, but better safe than sorry
|
||||
|
||||
int mutex_reply = sps_pthread_mutex_timedlock(&dacp_conversation_lock, 2000000, command, 1);
|
||||
// int mutex_reply = sps_pthread_mutex_timedlock(&dacp_conversation_lock, 2000000, command,
|
||||
// 1);
|
||||
int mutex_reply = debug_mutex_lock(&dacp_conversation_lock, 2000000, 1);
|
||||
// int mutex_reply = pthread_mutex_lock(&dacp_conversation_lock);
|
||||
if (mutex_reply == 0) {
|
||||
pthread_cleanup_push(mutex_lock_cleanup, (void *)&dacp_conversation_lock);
|
||||
@@ -415,10 +417,12 @@ void set_dacp_server_information(rtsp_conn_info *conn) {
|
||||
// This is different to other AirPlay clients
|
||||
// which return immediately with a 403 code if there are no changes.
|
||||
dacp_server.always_use_revision_number_1 = 0;
|
||||
char *p = strstr(conn->UserAgent, "forked-daapd");
|
||||
if ((p != 0) &&
|
||||
(p == conn->UserAgent)) { // must exist and be at the start of the UserAgent string
|
||||
dacp_server.always_use_revision_number_1 = 1;
|
||||
if (conn->UserAgent != NULL) {
|
||||
char *p = strstr(conn->UserAgent, "forked-daapd");
|
||||
if ((p != 0) &&
|
||||
(p == conn->UserAgent)) { // must exist and be at the start of the UserAgent string
|
||||
dacp_server.always_use_revision_number_1 = 1;
|
||||
}
|
||||
}
|
||||
|
||||
metadata_hub_modify_prolog();
|
||||
@@ -463,7 +467,7 @@ void dacp_monitor_port_update_callback(char *dacp_id, uint16_t port) {
|
||||
"dacp_monitor_port_update_callback with Remote ID \"%s\", target ID \"%s\" and port "
|
||||
"number %d.",
|
||||
dacp_id, dacp_server.dacp_id, port);
|
||||
if ((dacp_id == NULL) || (dacp_server.dacp_id == NULL)) {
|
||||
if ((dacp_id == NULL) || (dacp_server.dacp_id[0] == '\0')) {
|
||||
warn("dacp_id or dacp_server.dacp_id NULL detected");
|
||||
} else {
|
||||
if (strcmp(dacp_id, dacp_server.dacp_id) == 0) {
|
||||
@@ -479,18 +483,13 @@ void dacp_monitor_port_update_callback(char *dacp_id, uint16_t port) {
|
||||
// metadata_store.dacp_server_active = dacp_server.scan_enable;
|
||||
// metadata_hub_modify_epilog(ch);
|
||||
} else {
|
||||
debug(1, "dacp port monitor reporting on an out-of-use remote.");
|
||||
debug(2, "dacp port monitor reporting on an out-of-use remote.");
|
||||
}
|
||||
}
|
||||
pthread_cond_signal(&dacp_server_information_cv);
|
||||
debug_mutex_unlock(&dacp_server_information_lock, 3);
|
||||
}
|
||||
|
||||
void dacp_monitor_thread_code_cleanup(__attribute__((unused)) void *arg) {
|
||||
// debug(1, "dacp_monitor_thread_code_cleanup called.");
|
||||
pthread_mutex_unlock(&dacp_server_information_lock);
|
||||
}
|
||||
|
||||
void *dacp_monitor_thread_code(__attribute__((unused)) void *na) {
|
||||
int scan_index = 0;
|
||||
int always_use_revision_number_1 = 0;
|
||||
@@ -502,12 +501,8 @@ void *dacp_monitor_thread_code(__attribute__((unused)) void *na) {
|
||||
int idle_scan_count = 0;
|
||||
while (1) {
|
||||
int result = 0;
|
||||
sps_pthread_mutex_timedlock(
|
||||
&dacp_server_information_lock, 500000,
|
||||
"dacp_monitor_thread_code couldn't get DACP server information lock in 0.5 second!.", 2);
|
||||
int32_t the_volume;
|
||||
|
||||
pthread_cleanup_push(dacp_monitor_thread_code_cleanup, NULL);
|
||||
pthread_cleanup_debug_mutex_lock(&dacp_server_information_lock, 500000, 2);
|
||||
if (dacp_server.scan_enable == 0) {
|
||||
metadata_hub_modify_prolog();
|
||||
int ch = (metadata_store.dacp_server_active != 0) ||
|
||||
@@ -527,7 +522,7 @@ void *dacp_monitor_thread_code(__attribute__((unused)) void *na) {
|
||||
(uint64_t)(1000000000 * config.missing_port_dacp_scan_interval_seconds);
|
||||
|
||||
#ifdef COMPILE_FOR_LINUX_AND_FREEBSD_AND_CYGWIN_AND_OPENBSD
|
||||
uint64_t time_of_wakeup_ns = get_absolute_time_in_ns() + time_to_wait_for_wakeup_ns;
|
||||
uint64_t time_of_wakeup_ns = get_realtime_in_ns() + time_to_wait_for_wakeup_ns;
|
||||
uint64_t sec = time_of_wakeup_ns / 1000000000;
|
||||
uint64_t nsec = time_of_wakeup_ns % 1000000000;
|
||||
#endif
|
||||
@@ -568,7 +563,7 @@ void *dacp_monitor_thread_code(__attribute__((unused)) void *na) {
|
||||
pthread_cleanup_pop(1);
|
||||
|
||||
if (result == 490) { // 490 means no port was specified
|
||||
if (((char *)dacp_server.dacp_id != NULL) && (strlen(dacp_server.dacp_id) != 0)) {
|
||||
if (strlen(dacp_server.dacp_id) != 0) {
|
||||
// debug(1,"mdns_dacp_monitor_set_id");
|
||||
mdns_dacp_monitor_set_id(dacp_server.dacp_id);
|
||||
}
|
||||
@@ -936,18 +931,9 @@ void *dacp_monitor_thread_code(__attribute__((unused)) void *na) {
|
||||
void dacp_monitor_start() {
|
||||
int rc;
|
||||
|
||||
#ifdef COMPILE_FOR_LINUX_AND_FREEBSD_AND_CYGWIN_AND_OPENBSD
|
||||
pthread_condattr_t attr;
|
||||
pthread_condattr_init(&attr);
|
||||
pthread_condattr_setclock(&attr, CLOCK_MONOTONIC); // can't do this in OS X, and don't need it.
|
||||
rc = pthread_cond_init(&dacp_server_information_cv, &attr);
|
||||
rc = pthread_cond_init(&dacp_server_information_cv, NULL);
|
||||
if (rc)
|
||||
debug(1, "Error initialising the DACP Server Information Condition Variable");
|
||||
pthread_condattr_destroy(&attr);
|
||||
#endif
|
||||
#ifdef COMPILE_FOR_OSX
|
||||
rc = pthread_cond_init(&dacp_server_information_cv, NULL);
|
||||
#endif
|
||||
|
||||
pthread_mutexattr_t mta;
|
||||
|
||||
@@ -1358,12 +1344,14 @@ int dacp_set_volume(int32_t vo) {
|
||||
machine_number,
|
||||
highest_other_volume); // set the overall volume to the highest one
|
||||
}
|
||||
int32_t desired_relative_volume =
|
||||
(vo * 100 + (highest_other_volume / 2)) / highest_other_volume;
|
||||
debug(2, "Set our speaker volume relative to the highest volume.");
|
||||
http_response = dacp_set_speaker_volume(
|
||||
machine_number,
|
||||
desired_relative_volume); // set the overall volume to the highest one
|
||||
if (highest_other_volume != 0) {
|
||||
int32_t desired_relative_volume =
|
||||
(vo * 100 + (highest_other_volume / 2)) / highest_other_volume;
|
||||
debug(2, "Set our speaker volume relative to the highest volume.");
|
||||
http_response = dacp_set_speaker_volume(
|
||||
machine_number,
|
||||
desired_relative_volume); // set the overall volume to the highest one
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+49
-7
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* This file is part of Shairport Sync.
|
||||
* Copyright (c) Mike Brady 2018 -- 2019
|
||||
* Copyright (c) Mike Brady 2018 -- 2022
|
||||
* All rights reserved.
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person
|
||||
@@ -423,6 +423,8 @@ gboolean notify_statistics_callback(ShairportSyncDiagnostics *skeleton,
|
||||
// debug(1, "\"notify_statistics_callback\" called.");
|
||||
if (shairport_sync_diagnostics_get_statistics(skeleton)) {
|
||||
debug(1, ">> start logging statistics");
|
||||
if (config.statistics_requested == 0)
|
||||
statistics_row = 0; // redraw the header line
|
||||
config.statistics_requested = 1;
|
||||
} else {
|
||||
debug(1, ">> stop logging statistics");
|
||||
@@ -437,6 +439,8 @@ gboolean notify_verbosity_callback(ShairportSyncDiagnostics *skeleton,
|
||||
if ((th >= 0) && (th <= 3)) {
|
||||
if (th == 0)
|
||||
debug(1, ">> log verbosity set to %d.", th);
|
||||
if (((debuglev == 0) && (th != 0)) || ((debuglev != 0) && (th == 0)))
|
||||
statistics_row = 0; // if the debug level changes, redraw the header line
|
||||
debuglev = th;
|
||||
debug(1, ">> log verbosity set to %d.", th);
|
||||
} else {
|
||||
@@ -558,7 +562,7 @@ gboolean notify_drift_tolerance_callback(ShairportSync *skeleton,
|
||||
__attribute__((unused)) gpointer user_data) {
|
||||
gdouble dt = shairport_sync_get_drift_tolerance(skeleton);
|
||||
if ((dt >= 0.0) && (dt <= 2.0)) {
|
||||
debug(1, ">> setting drift tolerance to %f seconds", dt);
|
||||
debug(1, ">> setting drift tolerance to %f seconds.", dt);
|
||||
config.tolerance = dt;
|
||||
} else {
|
||||
debug(1, ">> invalid drift tolerance: %f seconds. Ignored.", dt);
|
||||
@@ -567,6 +571,25 @@ gboolean notify_drift_tolerance_callback(ShairportSync *skeleton,
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
gboolean notify_volume_callback(ShairportSync *skeleton,
|
||||
__attribute__((unused)) gpointer user_data) {
|
||||
gdouble iv = shairport_sync_get_volume(skeleton);
|
||||
if (((iv >= -30.0) && (iv <= 0.0)) || (iv == -144.0)) {
|
||||
debug(2, ">> setting volume to %7.4f.", iv);
|
||||
|
||||
lock_player();
|
||||
config.airplay_volume = iv;
|
||||
if (playing_conn != NULL)
|
||||
player_volume(iv, playing_conn);
|
||||
unlock_player();
|
||||
|
||||
} else {
|
||||
debug(1, ">> invalid volume: %f. Ignored.", iv);
|
||||
shairport_sync_set_volume(skeleton, config.airplay_volume);
|
||||
}
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
gboolean notify_disable_standby_mode_callback(ShairportSync *skeleton,
|
||||
__attribute__((unused)) gpointer user_data) {
|
||||
char *th = (char *)shairport_sync_get_disable_standby_mode(skeleton);
|
||||
@@ -749,11 +772,9 @@ gboolean notify_loop_status_callback(ShairportSyncAdvancedRemoteControl *skeleto
|
||||
static gboolean on_handle_quit(ShairportSync *skeleton, GDBusMethodInvocation *invocation,
|
||||
__attribute__((unused)) const gchar *command,
|
||||
__attribute__((unused)) gpointer user_data) {
|
||||
debug(1, "quit requested (native interface)");
|
||||
if (main_thread_id)
|
||||
debug(1, "Cancelling main thread results in %d.", pthread_cancel(main_thread_id));
|
||||
else
|
||||
debug(1, "Main thread ID is NULL.");
|
||||
debug(1, ">> quit requested");
|
||||
type_of_exit_cleanup = TOE_dbus; // request an exit cleanup that is compatible with dbus
|
||||
exit(EXIT_SUCCESS);
|
||||
shairport_sync_complete_quit(skeleton, invocation);
|
||||
return TRUE;
|
||||
}
|
||||
@@ -784,6 +805,15 @@ static gboolean on_handle_remote_command(ShairportSync *skeleton, GDBusMethodInv
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
static gboolean on_handle_drop_session(ShairportSync *skeleton, GDBusMethodInvocation *invocation,
|
||||
__attribute__((unused)) gpointer user_data) {
|
||||
if (playing_conn != NULL)
|
||||
debug(1, ">> stopping current play session");
|
||||
get_play_lock(NULL, 1); // stop any current session and don't replace it
|
||||
shairport_sync_complete_drop_session(skeleton, invocation);
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
static void on_dbus_name_acquired(GDBusConnection *connection, const gchar *name,
|
||||
__attribute__((unused)) gpointer user_data) {
|
||||
|
||||
@@ -831,12 +861,17 @@ static void on_dbus_name_acquired(GDBusConnection *connection, const gchar *name
|
||||
G_CALLBACK(notify_loudness_threshold_callback), NULL);
|
||||
g_signal_connect(shairportSyncSkeleton, "notify::drift-tolerance",
|
||||
G_CALLBACK(notify_drift_tolerance_callback), NULL);
|
||||
g_signal_connect(shairportSyncSkeleton, "notify::volume", G_CALLBACK(notify_volume_callback),
|
||||
NULL);
|
||||
|
||||
g_signal_connect(shairportSyncSkeleton, "handle-quit", G_CALLBACK(on_handle_quit), NULL);
|
||||
|
||||
g_signal_connect(shairportSyncSkeleton, "handle-remote-command",
|
||||
G_CALLBACK(on_handle_remote_command), NULL);
|
||||
|
||||
g_signal_connect(shairportSyncSkeleton, "handle-drop-session", G_CALLBACK(on_handle_drop_session),
|
||||
NULL);
|
||||
|
||||
g_signal_connect(shairportSyncDiagnosticsSkeleton, "notify::verbosity",
|
||||
G_CALLBACK(notify_verbosity_callback), NULL);
|
||||
|
||||
@@ -895,6 +930,7 @@ static void on_dbus_name_acquired(GDBusConnection *connection, const gchar *name
|
||||
shairport_sync_set_loudness_threshold(SHAIRPORT_SYNC(shairportSyncSkeleton),
|
||||
config.loudness_reference_volume_db);
|
||||
shairport_sync_set_drift_tolerance(SHAIRPORT_SYNC(shairportSyncSkeleton), config.tolerance);
|
||||
shairport_sync_set_volume(SHAIRPORT_SYNC(shairportSyncSkeleton), config.airplay_volume);
|
||||
|
||||
#ifdef CONFIG_APPLE_ALAC
|
||||
if (config.use_apple_decoder == 0) {
|
||||
@@ -983,6 +1019,12 @@ static void on_dbus_name_acquired(GDBusConnection *connection, const gchar *name
|
||||
// NULL);
|
||||
#endif
|
||||
|
||||
#ifdef CONFIG_AIRPLAY_2
|
||||
shairport_sync_set_protocol(SHAIRPORT_SYNC(shairportSyncSkeleton), "AirPlay 2");
|
||||
#else
|
||||
shairport_sync_set_protocol(SHAIRPORT_SYNC(shairportSyncSkeleton), "AirPlay");
|
||||
#endif
|
||||
|
||||
shairport_sync_set_version(SHAIRPORT_SYNC(shairportSyncSkeleton), PACKAGE_VERSION);
|
||||
char *vs = get_version_string();
|
||||
shairport_sync_set_version_string(SHAIRPORT_SYNC(shairportSyncSkeleton), vs);
|
||||
|
||||
+12
-5
@@ -1,14 +1,9 @@
|
||||
#ifndef _DEFINITIONS_H
|
||||
#define _DEFINITIONS_H
|
||||
|
||||
//#include <libconfig.h>
|
||||
//#include <signal.h>
|
||||
//#include <stdint.h>
|
||||
#include <sys/socket.h>
|
||||
|
||||
//#include "audio.h"
|
||||
#include "config.h"
|
||||
//#include "mdns.h"
|
||||
|
||||
#if defined(__APPLE__) && defined(__MACH__)
|
||||
/* Apple OSX and iOS (Darwin). ------------------------------ */
|
||||
@@ -23,6 +18,18 @@
|
||||
#define COMPILE_FOR_LINUX_AND_FREEBSD_AND_CYGWIN_AND_OPENBSD 1
|
||||
#endif
|
||||
|
||||
#if defined(__linux__)
|
||||
#define COMPILE_FOR_LINUX 1
|
||||
#endif
|
||||
|
||||
#if defined(__FreeBSD__) || defined(__OpenBSD__)
|
||||
#define COMPILE_FOR_BSD 1
|
||||
#endif
|
||||
|
||||
#if defined(__FreeBSD__)
|
||||
#define COMPILE_FOR_FREEBSD 1
|
||||
#endif
|
||||
|
||||
// struct sockaddr_in6 is bigger than struct sockaddr. derp
|
||||
#ifdef AF_INET6
|
||||
#define SOCKADDR struct sockaddr_storage
|
||||
|
||||
@@ -0,0 +1,116 @@
|
||||
FROM alpine:3.12 AS builder
|
||||
|
||||
# Check required arguments exist. These will be provided by the Github Action
|
||||
# Workflow and are required to ensure the correct branches are being used.
|
||||
ARG SHAIRPORT_SYNC_BRANCH
|
||||
RUN test -n "$SHAIRPORT_SYNC_BRANCH"
|
||||
ARG NQPTP_BRANCH
|
||||
RUN test -n "$NQPTP_BRANCH"
|
||||
|
||||
RUN apk -U add \
|
||||
git \
|
||||
build-base \
|
||||
autoconf \
|
||||
automake \
|
||||
libtool \
|
||||
dbus \
|
||||
alsa-lib-dev \
|
||||
popt-dev \
|
||||
mbedtls-dev \
|
||||
soxr-dev \
|
||||
avahi-dev \
|
||||
libconfig-dev \
|
||||
libsndfile-dev \
|
||||
mosquitto-dev \
|
||||
xmltoman \
|
||||
openssh-client \
|
||||
libsodium-dev \
|
||||
ffmpeg-dev \
|
||||
xxd \
|
||||
libressl-dev \
|
||||
openssl-dev \
|
||||
libplist-dev \
|
||||
libgcrypt-dev
|
||||
|
||||
##### ALAC #####
|
||||
RUN git clone https://github.com/mikebrady/alac
|
||||
WORKDIR /alac
|
||||
RUN autoreconf -i
|
||||
RUN ./configure
|
||||
RUN make
|
||||
RUN make install
|
||||
WORKDIR /
|
||||
##### ALAC END #####
|
||||
|
||||
##### NQPTP #####
|
||||
RUN git clone https://github.com/mikebrady/nqptp
|
||||
WORKDIR /nqptp
|
||||
RUN git checkout "$NQPTP_BRANCH"
|
||||
RUN autoreconf -i
|
||||
RUN ./configure
|
||||
RUN make
|
||||
RUN make install
|
||||
WORKDIR /
|
||||
##### NQPTP END #####
|
||||
|
||||
##### SPS #####
|
||||
WORKDIR /shairport-sync
|
||||
COPY . .
|
||||
RUN git checkout "$SHAIRPORT_SYNC_BRANCH"
|
||||
WORKDIR /shairport-sync/build
|
||||
RUN autoreconf -i ../
|
||||
RUN ../configure --sysconfdir=/etc --with-alsa --with-soxr --with-avahi --with-ssl=openssl --with-airplay-2 \
|
||||
--with-metadata --with-dummy --with-pipe --with-dbus-interface \
|
||||
--with-stdout --with-mpris-interface --with-mqtt-client \
|
||||
--with-apple-alac --with-convolution
|
||||
RUN make -j $(nproc)
|
||||
RUN DESTDIR=install make install
|
||||
WORKDIR /
|
||||
##### SPS END #####
|
||||
|
||||
# Shairport Sync Runtime System
|
||||
FROM crazymax/alpine-s6:3.12-3.1.1.2
|
||||
|
||||
RUN apk -U add \
|
||||
alsa-lib \
|
||||
dbus \
|
||||
popt \
|
||||
glib \
|
||||
mbedtls \
|
||||
soxr \
|
||||
avahi \
|
||||
avahi-tools \
|
||||
libconfig \
|
||||
libsndfile \
|
||||
mosquitto \
|
||||
libuuid \
|
||||
ffmpeg \
|
||||
libsodium \
|
||||
libplist \
|
||||
libgcrypt
|
||||
|
||||
# Copy build files.
|
||||
COPY --from=builder /shairport-sync/build/install/usr/local/bin/shairport-sync /usr/local/bin/shairport-sync
|
||||
COPY --from=builder /usr/local/bin/nqptp /usr/local/bin/nqptp
|
||||
COPY --from=builder /usr/local/lib/libalac.* /usr/local/lib/
|
||||
COPY --from=builder /shairport-sync/build/install/etc/dbus-1/system.d/shairport-sync-dbus.conf /etc/dbus-1/system.d/
|
||||
COPY --from=builder /shairport-sync/build/install/etc/dbus-1/system.d/shairport-sync-mpris.conf /etc/dbus-1/system.d/
|
||||
|
||||
COPY ./docker/etc/s6-overlay/s6-rc.d /etc/s6-overlay/s6-rc.d
|
||||
RUN chmod +x /etc/s6-overlay/s6-rc.d/startup/script.sh
|
||||
|
||||
# Create non-root user for running the container -- running as the user 'shairport-sync' also allows
|
||||
# Shairport Sync to provide the D-Bus and MPRIS interfaces within the container
|
||||
|
||||
RUN addgroup shairport-sync
|
||||
RUN adduser -D shairport-sync -G shairport-sync
|
||||
|
||||
# Add the shairport-sync user to the pre-existing audio group, which has ID 29, for access to the ALSA stuff
|
||||
RUN addgroup -g 29 docker_audio && addgroup shairport-sync docker_audio && addgroup shairport-sync audio
|
||||
|
||||
# Remove anything we don't need.
|
||||
RUN rm -rf /lib/apk/db/*
|
||||
|
||||
ENTRYPOINT [ "/init" ]
|
||||
|
||||
CMD [ "/usr/local/bin/shairport-sync", "-v", "--statistics" ]
|
||||
+47
-4
@@ -2,12 +2,55 @@
|
||||
|
||||
Available at: https://hub.docker.com/r/mikebrady/shairport-sync
|
||||
|
||||
## Original AirPlay 1 Image
|
||||
Please see: [airplay1/README.md](airplay1/README.md)
|
||||
Please note if you want the development version of the image including Airplay 2 support, please pull the image with the ```unstable development``` tag using the following command:
|
||||
```
|
||||
docker pull mikebrady/shairport-sync:unstable-development
|
||||
```
|
||||
|
||||
When using the below commands, you should replace mikebrady/shairport-sync with mikbrady/shairport-sync:unstable-development.
|
||||
|
||||
## Example Docker Compose File
|
||||
See the `docker-compose.yaml` file in this folder for an example.
|
||||
|
||||
## Docker Run
|
||||
|
||||
```
|
||||
$ docker run -d --restart unless-stopped --net host --device /dev/snd \
|
||||
mikebrady/shairport-sync
|
||||
```
|
||||
|
||||
### Options
|
||||
|
||||
You can change the default commands passed to Shairport Sync. Here is an example:
|
||||
```
|
||||
$ docker run -d --restart unless-stopped --net host --device /dev/snd \
|
||||
mikebrady/shairport-sync:unstable-development shairport-sync -v \
|
||||
--statistics -a DenSystem -d hw:0 -c PCM
|
||||
```
|
||||
This will sent audio to alsa hardware device `hw:0` and make use of the that device's mixer control called `PCM`. The service will be visible as `DenSystem` on the network.
|
||||
|
||||
## Configuration File
|
||||
|
||||
To get access to the full range of configuration options, pass the configuration file to `/etc/shairport-sync.conf` in the container using the `-v` option or docker compose.
|
||||
|
||||
## Building
|
||||
### Build Example (for arm7 devices)
|
||||
|
||||
```
|
||||
docker buildx build --platform linux/arm/v7 -f ./docker/Dockerfile --build-arg SHAIRPORT_SYNC_BRANCH=development --build-arg NQPTP_BRANCH=development --no-cache -t shairport-sync:unstable-development .
|
||||
```
|
||||
|
||||
`SHAIRPORT_SYNC_BRANCH` and `NQPTP_BRANCH` are required to ensure the image is built using the expected branch.
|
||||
`--no-cache` needs to be used to force buildx to pull the NQPTP branch for new updates. This slows down the build time though so can be removed when it is not beneficial during testing.
|
||||
|
||||
### AirPlay 1 Only
|
||||
|
||||
The AirPlay 1 only dockerfile is in the `classic` folder. This also includes the `start.sh` script used by the container. Please note that the AirPlay 1 image built via the AirPlay 2 branch has missing functionality, e.g. it does not work with multiple instances on the same hardware; does not support iTunes for Windows etc.
|
||||
|
||||
### GitHub Action Builds
|
||||
|
||||
## GitHub Action Builds
|
||||
Requires the following secrets to be set in the repo:
|
||||
- `DOCKER_REGISTRY` - docker.io if using Docker Hub, else set to your registry URL.
|
||||
- `DOCKER_REGISTRY_TOKEN` - Access token for your registry.
|
||||
- `DOCKER_REGISTRY_USER` - Login user for your registry.
|
||||
- `DOCKER_IMAGE_NAME` - The name of the image, for example `your-registry.com/shairport-sync` or just `your-username/shairport-sync` if using Docker Hub.
|
||||
- `DOCKER_IMAGE_NAME` - The name of the image, for example `your-registry.com/shairport-sync` or just `your-username/shairport-sync` if using Docker Hub.
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
FROM alpine AS builder-base
|
||||
# General Build System:
|
||||
RUN apk -U add \
|
||||
git \
|
||||
build-base \
|
||||
autoconf \
|
||||
automake \
|
||||
libtool \
|
||||
dbus \
|
||||
su-exec \
|
||||
alsa-lib-dev \
|
||||
libdaemon-dev \
|
||||
popt-dev \
|
||||
mbedtls-dev \
|
||||
soxr-dev \
|
||||
avahi-dev \
|
||||
libconfig-dev \
|
||||
libsndfile-dev \
|
||||
mosquitto-dev \
|
||||
xmltoman
|
||||
|
||||
# ALAC Build System:
|
||||
FROM builder-base AS builder-alac
|
||||
|
||||
RUN git clone https://github.com/mikebrady/alac
|
||||
WORKDIR alac
|
||||
RUN autoreconf -fi
|
||||
RUN ./configure
|
||||
RUN make
|
||||
RUN make install
|
||||
|
||||
# Shairport Sync Build System:
|
||||
FROM builder-base AS builder-sps
|
||||
|
||||
# This will be modified by the Github Action Workflow and is required
|
||||
# to ensure the correct branch is being used.
|
||||
ARG SHAIRPORT_SYNC_BRANCH
|
||||
RUN test -n "$SHAIRPORT_SYNC_BRANCH"
|
||||
|
||||
COPY --from=builder-alac /usr/local/lib/libalac.* /usr/local/lib/
|
||||
COPY --from=builder-alac /usr/local/lib/pkgconfig/alac.pc /usr/local/lib/pkgconfig/alac.pc
|
||||
COPY --from=builder-alac /usr/local/include /usr/local/include
|
||||
|
||||
WORKDIR /shairport-sync
|
||||
COPY . .
|
||||
RUN git checkout "$SHAIRPORT_SYNC_BRANCH"
|
||||
RUN autoreconf -fi
|
||||
RUN ./configure \
|
||||
--with-alsa \
|
||||
--with-dummy \
|
||||
--with-pipe \
|
||||
--with-stdout \
|
||||
--with-avahi \
|
||||
--with-ssl=mbedtls \
|
||||
--with-soxr \
|
||||
--sysconfdir=/etc \
|
||||
--with-dbus-interface \
|
||||
--with-mpris-interface \
|
||||
--with-mqtt-client \
|
||||
--with-apple-alac \
|
||||
--with-convolution
|
||||
RUN make -j $(nproc)
|
||||
RUN make install
|
||||
|
||||
# Shairport Sync Runtime System:
|
||||
FROM alpine
|
||||
|
||||
RUN apk -U add \
|
||||
alsa-lib \
|
||||
dbus \
|
||||
popt \
|
||||
glib \
|
||||
mbedtls \
|
||||
soxr \
|
||||
avahi \
|
||||
avahi-tools \
|
||||
libconfig \
|
||||
libsndfile \
|
||||
mosquitto-libs \
|
||||
su-exec \
|
||||
libgcc \
|
||||
libgc++
|
||||
|
||||
RUN rm -rf /lib/apk/db/*
|
||||
|
||||
COPY --from=builder-alac /usr/local/lib/libalac.* /usr/local/lib/
|
||||
COPY --from=builder-sps /etc/shairport-sync* /etc/
|
||||
COPY --from=builder-sps /etc/dbus-1/system.d/shairport-sync-dbus.conf /etc/dbus-1/system.d/
|
||||
COPY --from=builder-sps /etc/dbus-1/system.d/shairport-sync-mpris.conf /etc/dbus-1/system.d/
|
||||
COPY --from=builder-sps /usr/local/bin/shairport-sync /usr/local/bin/shairport-sync
|
||||
|
||||
# Create non-root user for running the container -- running as the user 'shairport-sync' also allows
|
||||
# Shairport Sync to provide the D-Bus and MPRIS interfaces within the container
|
||||
|
||||
RUN addgroup shairport-sync
|
||||
RUN adduser -D shairport-sync -G shairport-sync
|
||||
|
||||
# Add the shairport-sync user to the pre-existing audio group, which has ID 29, for access to the ALSA stuff
|
||||
RUN addgroup -g 29 docker_audio && addgroup shairport-sync docker_audio
|
||||
|
||||
COPY ./docker/classic/start.sh /
|
||||
RUN chmod +x /start.sh
|
||||
|
||||
ENTRYPOINT [ "/start.sh" ]
|
||||
@@ -0,0 +1,3 @@
|
||||
# Classic (AirPlay 1 Only) Docker Image
|
||||
|
||||
See the [Shairport Sync Docker Hub Repo](https://hub.docker.com/r/mikebrady/shairport-sync) for available tags.
|
||||
@@ -0,0 +1,13 @@
|
||||
#!/bin/sh
|
||||
|
||||
set -e
|
||||
|
||||
rm -rf /var/run/dbus.pid
|
||||
#mkdir -p /var/run/dbus
|
||||
|
||||
dbus-uuidgen --ensure
|
||||
dbus-daemon --system
|
||||
|
||||
avahi-daemon --daemonize --no-chroot
|
||||
|
||||
su-exec shairport-sync shairport-sync "$@"
|
||||
@@ -0,0 +1,2 @@
|
||||
#!/command/execlineb -S0
|
||||
/run/s6/basedir/bin/halt
|
||||
@@ -0,0 +1,4 @@
|
||||
#!/command/with-contenv sh
|
||||
rm -rf /var/run/dbus.pid
|
||||
echo "Starting dbus"
|
||||
exec dbus-daemon --system --nofork
|
||||
@@ -0,0 +1 @@
|
||||
longrun
|
||||
@@ -0,0 +1,2 @@
|
||||
#!/command/execlineb -S0
|
||||
/run/s6/basedir/bin/halt
|
||||
@@ -0,0 +1,8 @@
|
||||
#!/command/with-contenv sh
|
||||
sleep 5
|
||||
while [ ! -f /var/run/dbus.pid ]; do
|
||||
"dbus is not running, sleeping for 5 seconds before trying to start avahi"
|
||||
sleep 5
|
||||
done
|
||||
echo "Starting avahi"
|
||||
exec avahi-daemon --no-chroot
|
||||
@@ -0,0 +1 @@
|
||||
longrun
|
||||
@@ -0,0 +1,2 @@
|
||||
#!/command/execlineb -S0
|
||||
/run/s6/basedir/bin/halt
|
||||
@@ -0,0 +1,3 @@
|
||||
#!/command/with-contenv sh
|
||||
echo "Starting nqptp"
|
||||
exec /usr/local/bin/nqptp -v
|
||||
@@ -0,0 +1 @@
|
||||
longrun
|
||||
@@ -0,0 +1,2 @@
|
||||
#!/bin/sh
|
||||
echo "STARTING - $(date)"
|
||||
@@ -0,0 +1 @@
|
||||
oneshot
|
||||
@@ -0,0 +1 @@
|
||||
/etc/s6-overlay/s6-rc.d/startup/script.sh
|
||||
@@ -1,5 +1,9 @@
|
||||
# For the "Native" Shairport Sync D-Bus Interface support, Shairport Sync must be built with the D-Bus interface support. Add the '--with-dbus-interface' flag at the ./configure stage.
|
||||
# There is a simple test client that you can have built -- add the '--with-dbus-test-client' flag at the ./configure stage. You'll get an executable called shairport-sync-dbus-test-client
|
||||
|
||||
# There is a simple test client that you can build -- add the '--with-dbus-test-client' flag at the ./configure stage. You'll get an executable called shairport-sync-dbus-test-client
|
||||
# After attempting to send some commands, it will listen for property changes on the D-Bus interface and report them on the console.
|
||||
|
||||
# Additionally, the following are examples of using the CLI command "dbus-send" to send D-Bus commands to Shairport Sync:
|
||||
|
||||
# Get Log Verbosity
|
||||
dbus-send --print-reply --system --dest=org.gnome.ShairportSync /org/gnome/ShairportSync org.freedesktop.DBus.Properties.Get string:org.gnome.ShairportSync.Diagnostics string:Verbosity
|
||||
@@ -23,10 +27,6 @@ dbus-send --print-reply --system --dest=org.gnome.ShairportSync /org/gnome/Shair
|
||||
# Include Elapsed Time in Log Entries
|
||||
dbus-send --print-reply --system --dest=org.gnome.ShairportSync /org/gnome/ShairportSync org.freedesktop.DBus.Properties.Set string:org.gnome.ShairportSync.Diagnostics string:ElapsedTime variant:boolean:true
|
||||
|
||||
# Play using regular Remote Control
|
||||
dbus-send --system --print-reply --type=method_call --dest=org.gnome.ShairportSync '/org/gnome/ShairportSync' org.gnome.ShairportSync.RemoteControl.Play
|
||||
#Remote Control commands include: Play, Pause, PlayPause, Resume, Stop, Next, Previous, VolumeUp, VolumeDown, ToggleMute, FastForward, Rewind, ShuffleSongs
|
||||
|
||||
# Get Drift Tolerance
|
||||
dbus-send --print-reply --system --dest=org.gnome.ShairportSync /org/gnome/ShairportSync org.freedesktop.DBus.Properties.Get string:org.gnome.ShairportSync string:DriftTolerance
|
||||
# Set Drift Tolerance to 1 millisecond
|
||||
@@ -62,19 +62,37 @@ dbus-send --print-reply --system --dest=org.gnome.ShairportSync /org/gnome/Shair
|
||||
# Set Convolution Impulse Response File:
|
||||
dbus-send --print-reply --system --dest=org.gnome.ShairportSync /org/gnome/ShairportSync org.freedesktop.DBus.Properties.Set string:org.gnome.ShairportSync string:ConvolutionImpulseResponseFile variant:string:"/etc/shairport-sync/boom.wav"
|
||||
|
||||
# Get the Protocol Shairport Sync was built for -- AirPlay or AirPlay 2:
|
||||
dbus-send --print-reply --system --dest=org.gnome.ShairportSync /org/gnome/ShairportSync org.freedesktop.DBus.Properties.Get string:org.gnome.ShairportSync string:Protocol
|
||||
|
||||
# Get the current volume level. This returns a value between -30.0 and 0.0, which is linear on the UI volume control. It can also return -144.0 to indicate mute.
|
||||
dbus-send --print-reply --system --dest=org.gnome.ShairportSync /org/gnome/ShairportSync org.freedesktop.DBus.Properties.Get string:org.gnome.ShairportSync string:Volume
|
||||
|
||||
# Set the current volume level. This should be a value between -30.0 and 0.0. Set a value of -144.0 to mute. Note that all this is done locally on
|
||||
# the Shairport Sync device itself. The volume control of audio source (e.g. iTunes / macOS Music / iOS) is not updated to reflect any changes.
|
||||
|
||||
dbus-send --print-reply --system --dest=org.gnome.ShairportSync /org/gnome/ShairportSync org.freedesktop.DBus.Properties.Set string:org.gnome.ShairportSync string:Volume variant:double:-10.0
|
||||
|
||||
# Drop the current session immediately. Shairport Sync will immediately stop playing and drop the connection from the audio source.
|
||||
# This may show up as an error at the source.
|
||||
|
||||
dbus-send --system --print-reply --type=method_call --dest=org.gnome.ShairportSync '/org/gnome/ShairportSync' org.gnome.ShairportSync.DropSession
|
||||
|
||||
# Control the player using 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. iOS seems to implement most of them (the first nine in the list).
|
||||
dbus-send --system --print-reply --type=method_call --dest=org.gnome.ShairportSync '/org/gnome/ShairportSync' org.gnome.ShairportSync.RemoteControl.Play
|
||||
#Remote Control commands include: Play, Pause, PlayPause, Resume, Stop, Next, Previous, VolumeUp, VolumeDown, ToggleMute, FastForward, Rewind, ShuffleSongs
|
||||
|
||||
# Set Airplay Volume using Remote Control. Airplay Volume is between -30.0 and 0.0 and maps linearly onto the slider, with -30.0 being lowest and 0.0 being highest.
|
||||
dbus-send --system --print-reply --type=method_call --dest=org.gnome.ShairportSync '/org/gnome/ShairportSync' org.gnome.ShairportSync.RemoteControl.SetAirplayVolume double:-10.0
|
||||
|
||||
|
||||
# AdvancedRemoteControl interface.
|
||||
# Some commands and properties are accessible only through the AdvancedRemoteControl interface.
|
||||
# However, only iTunes or the macOS Music app from 10.15.2 onwards
|
||||
# provide the functionality needed for the AdvancedRemoteControl interface to work.
|
||||
# However, support for this is very limited in recent implementations.
|
||||
|
||||
# You can check to see if AdvancedRemoteControl is available using the command:
|
||||
|
||||
dbus-send --print-reply --system --dest=org.gnome.ShairportSync /org/gnome/ShairportSync org.freedesktop.DBus.Properties.Get string:org.gnome.ShairportSync.AdvancedRemoteControl string:Available
|
||||
|
||||
# Set Volume using Advanced Remote Control -- only works if the org.gnome.ShairportSync.AdvancedRemoteControl is available.
|
||||
dbus-send --system --print-reply --type=method_call --dest=org.gnome.ShairportSync '/org/gnome/ShairportSync' org.gnome.ShairportSync.AdvancedRemoteControl.SetVolume int32:50
|
||||
|
||||
@@ -83,5 +101,6 @@ dbus-send --system --print-reply --type=method_call --dest=org.gnome.ShairportSy
|
||||
# For MPRIS support, Shairport Sync must be built with the MPRIS interface support. Add the '--with-mpris-interface' flag at the ./configure stage.
|
||||
# There is a simple test client that you can have built -- add the '--with-mpris-test-client' flag at the ./configure stage. You'll get an executable called shairport-sync-mpris-test-client.
|
||||
# This is mostly compatible with the MPRIS standard, except that Volume is read-only, with a separate SetVolume method.
|
||||
|
||||
# Set Volume, which must be between 0.0 and 1.0 and maps linearly onto the slider.
|
||||
dbus-send --system --print-reply --type=method_call --dest=org.mpris.MediaPlayer2.ShairportSync '/org/mpris/MediaPlayer2' org.mpris.MediaPlayer2.Player.SetVolume double:0.3
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
/*
|
||||
* mDNS registration handler. This file is part of Shairport.
|
||||
* Copyright (c) James Laird 2013
|
||||
* Modifications, updates and additions (c) Mike Brady 2014 -- 2020
|
||||
* All rights reserved.
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person
|
||||
@@ -63,9 +64,9 @@ static mdns_backend *mdns_backends[] = {
|
||||
#endif
|
||||
NULL};
|
||||
|
||||
void mdns_register(void) {
|
||||
char *mdns_service_name = alloca(strlen(config.service_name) + 14);
|
||||
char *p = mdns_service_name;
|
||||
void mdns_register(char **txt_records, char **secondary_txt_records) {
|
||||
char *ap1_service_name = alloca(strlen(config.service_name) + 14);
|
||||
char *p = ap1_service_name;
|
||||
int i;
|
||||
for (i = 0; i < 6; i++) {
|
||||
snprintf(p, 3, "%02X", config.hw_addr[i]);
|
||||
@@ -80,7 +81,8 @@ void mdns_register(void) {
|
||||
for (b = mdns_backends; *b; b++) {
|
||||
if (strcmp((*b)->name, config.mdns_name) != 0) // Not the one we are looking for
|
||||
continue;
|
||||
int error = (*b)->mdns_register(mdns_service_name, config.port);
|
||||
int error = (*b)->mdns_register(ap1_service_name, config.service_name, config.port,
|
||||
txt_records, secondary_txt_records);
|
||||
if (error >= 0) {
|
||||
config.mdns = *b;
|
||||
}
|
||||
@@ -90,8 +92,10 @@ void mdns_register(void) {
|
||||
if (*b == NULL)
|
||||
warn("%s mDNS backend not found");
|
||||
} else {
|
||||
// default -- pick the first back end
|
||||
for (b = mdns_backends; *b; b++) {
|
||||
int error = (*b)->mdns_register(mdns_service_name, config.port);
|
||||
int error = (*b)->mdns_register(ap1_service_name, config.service_name, config.port,
|
||||
txt_records, secondary_txt_records);
|
||||
if (error >= 0) {
|
||||
config.mdns = *b;
|
||||
break;
|
||||
@@ -105,6 +109,13 @@ void mdns_register(void) {
|
||||
mdns_dacp_monitor_start(); // create a dacp monitor thread
|
||||
}
|
||||
|
||||
void mdns_update(char **txt_records, char **secondary_txt_records) {
|
||||
if ((config.mdns) && (config.mdns->mdns_update)) {
|
||||
config.mdns->mdns_update(txt_records, secondary_txt_records);
|
||||
} else
|
||||
debug(1, "Can't mdns_update -- no mdns_update registered.");
|
||||
}
|
||||
|
||||
void mdns_unregister(void) {
|
||||
mdns_dacp_monitor_stop();
|
||||
if (config.mdns) {
|
||||
|
||||
@@ -8,7 +8,8 @@
|
||||
extern int mdns_pid;
|
||||
|
||||
void mdns_unregister(void);
|
||||
void mdns_register(void);
|
||||
void mdns_register(char **txt_records, char **secondary_txt_records);
|
||||
void mdns_update(char **txt_records, char **secondary_txt_records);
|
||||
void mdns_dacp_monitor_start();
|
||||
void mdns_dacp_monitor_stop(void);
|
||||
void mdns_dacp_monitor_set_id(const char *dacp_id);
|
||||
@@ -17,7 +18,9 @@ void mdns_ls_backends(void);
|
||||
|
||||
typedef struct {
|
||||
char *name;
|
||||
int (*mdns_register)(char *apname, int port);
|
||||
int (*mdns_register)(char *ap1name, char *ap2name, int port, char **txt_records,
|
||||
char **secondary_txt_records);
|
||||
int (*mdns_update)(char **txt_records, char **secondary_txt_records);
|
||||
void (*mdns_unregister)(void);
|
||||
void (*mdns_dacp_monitor_start)();
|
||||
void (*mdns_dacp_monitor_set_id)(const char *);
|
||||
|
||||
+148
-49
@@ -47,6 +47,12 @@
|
||||
#include <avahi-client/lookup.h>
|
||||
#include <avahi-common/alternative.h>
|
||||
|
||||
void threaded_poll_unlock(void *arg) { avahi_threaded_poll_unlock((AvahiThreadedPoll *)arg); }
|
||||
|
||||
#define pthread_avahi_threaded_poll_lock_and_push(mu) \
|
||||
avahi_threaded_poll_lock(mu); \
|
||||
pthread_cleanup_push(threaded_poll_unlock, (void *)mu)
|
||||
|
||||
#define check_avahi_response(debugLevelArg, veryUnLikelyArgumentName) \
|
||||
{ \
|
||||
int rc = veryUnLikelyArgumentName; \
|
||||
@@ -61,6 +67,9 @@ typedef struct {
|
||||
|
||||
dacp_browser_struct private_dbs;
|
||||
|
||||
AvahiStringList *text_record_string_list = NULL;
|
||||
AvahiStringList *ap2_text_record_string_list = NULL;
|
||||
|
||||
// static AvahiServiceBrowser *sb = NULL;
|
||||
static AvahiClient *client = NULL;
|
||||
// static AvahiClient *service_client = NULL;
|
||||
@@ -69,6 +78,8 @@ static AvahiThreadedPoll *tpoll = NULL;
|
||||
// static AvahiThreadedPoll *service_poll = NULL;
|
||||
|
||||
static char *service_name = NULL;
|
||||
char *ap2_service_name = NULL;
|
||||
|
||||
static int port = 0;
|
||||
|
||||
static void resolve_callback(AvahiServiceResolver *r, AVAHI_GCC_UNUSED AvahiIfIndex interface,
|
||||
@@ -107,7 +118,7 @@ static void resolve_callback(AvahiServiceResolver *r, AVAHI_GCC_UNUSED AvahiIfIn
|
||||
char portstring[20];
|
||||
memset(portstring, 0, sizeof(portstring));
|
||||
snprintf(portstring, sizeof(portstring), "%u", port);
|
||||
send_ssnc_metadata('dapo', strdup(portstring), strlen(portstring), 0);
|
||||
send_ssnc_metadata('dapo', portstring, strlen(portstring), 0);
|
||||
#endif
|
||||
}
|
||||
} else {
|
||||
@@ -185,7 +196,7 @@ static void egroup_callback(AvahiEntryGroup *g, AvahiEntryGroupState state,
|
||||
|
||||
/* A service name collision with a remote service
|
||||
* happened. Let's pick a new name */
|
||||
debug(2, "avahi name collision -- look for another");
|
||||
debug(1, "avahi name collision -- look for another");
|
||||
n = avahi_alternative_service_name(service_name);
|
||||
if (service_name)
|
||||
avahi_free(service_name);
|
||||
@@ -229,36 +240,38 @@ static void register_service(AvahiClient *c) {
|
||||
if (!avahi_entry_group_is_empty(group))
|
||||
return;
|
||||
|
||||
int ret;
|
||||
int ret = 0;
|
||||
|
||||
AvahiIfIndex selected_interface;
|
||||
if (config.interface != NULL)
|
||||
selected_interface = config.interface_index;
|
||||
else
|
||||
selected_interface = AVAHI_IF_UNSPEC;
|
||||
#ifdef CONFIG_METADATA
|
||||
if (config.metadata_enabled) {
|
||||
ret = avahi_entry_group_add_service(group, selected_interface, AVAHI_PROTO_UNSPEC, 0,
|
||||
service_name, config.regtype, NULL, NULL, port,
|
||||
MDNS_RECORD_WITH_METADATA, NULL);
|
||||
if (ret == 0)
|
||||
debug(2, "avahi: request to add \"%s\" service with metadata", config.regtype);
|
||||
} else {
|
||||
#endif
|
||||
ret = avahi_entry_group_add_service(group, selected_interface, AVAHI_PROTO_UNSPEC, 0,
|
||||
service_name, config.regtype, NULL, NULL, port,
|
||||
MDNS_RECORD_WITHOUT_METADATA, NULL);
|
||||
if (ret == 0)
|
||||
debug(2, "avahi: request to add \"%s\" service without metadata", config.regtype);
|
||||
#ifdef CONFIG_METADATA
|
||||
if (ap2_text_record_string_list) {
|
||||
ret = avahi_entry_group_add_service_strlst(group, selected_interface, AVAHI_PROTO_UNSPEC, 0,
|
||||
ap2_service_name, config.regtype2, NULL, NULL,
|
||||
port, ap2_text_record_string_list);
|
||||
if (ret == AVAHI_ERR_COLLISION) {
|
||||
die("Error: AirPlay 2 name \"%s\" is already in use.", ap2_service_name);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
if (ret < 0)
|
||||
debug(1, "avahi: avahi_entry_group_add_service failed");
|
||||
else {
|
||||
if ((ret == 0) && (text_record_string_list)) {
|
||||
ret = avahi_entry_group_add_service_strlst(group, selected_interface, AVAHI_PROTO_UNSPEC, 0,
|
||||
service_name, config.regtype, NULL, NULL, port,
|
||||
text_record_string_list);
|
||||
if (ret == AVAHI_ERR_COLLISION) {
|
||||
die("Error: AirPlay 1 name \"%s\" is already in use.", service_name);
|
||||
}
|
||||
}
|
||||
if (ret == 0) {
|
||||
ret = avahi_entry_group_commit(group);
|
||||
debug(2, "avahi: avahi_entry_group_commit %d", ret);
|
||||
if (ret < 0)
|
||||
debug(1, "avahi: avahi_entry_group_commit failed");
|
||||
} else if (ret < 0) {
|
||||
debug(1, "avahi: avahi_entry_group_add_service failed");
|
||||
} else {
|
||||
debug(1, "avahi: unexpected positive return");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -317,9 +330,65 @@ static void client_callback(AvahiClient *c, AvahiClientState state,
|
||||
}
|
||||
}
|
||||
|
||||
static int avahi_register(char *srvname, int srvport) {
|
||||
static int avahi_update(char **txt_records, char **secondary_txt_records) {
|
||||
// debug(1, "avahi_update.");
|
||||
|
||||
/*
|
||||
service_name = strdup(ap1name);
|
||||
if (ap2name != NULL)
|
||||
ap2_service_name = strdup(ap2name);
|
||||
port = srvport;
|
||||
*/
|
||||
int err = 0;
|
||||
AvahiIfIndex selected_interface;
|
||||
if (config.interface != NULL)
|
||||
selected_interface = config.interface_index;
|
||||
else
|
||||
selected_interface = AVAHI_IF_UNSPEC;
|
||||
pthread_avahi_threaded_poll_lock_and_push(tpoll);
|
||||
// avahi_threaded_poll_lock(tpoll);
|
||||
// pthread_cleanup_push(threaded_poll_unlock, (void *)tpoll)
|
||||
|
||||
if (txt_records != NULL) {
|
||||
if (text_record_string_list)
|
||||
avahi_string_list_free(text_record_string_list);
|
||||
text_record_string_list = avahi_string_list_new_from_array((const char **)txt_records, -1);
|
||||
err = avahi_entry_group_update_service_txt_strlst(group, selected_interface, AVAHI_PROTO_UNSPEC,
|
||||
0, service_name, config.regtype, NULL,
|
||||
text_record_string_list);
|
||||
if (err != 0)
|
||||
debug(1, "avahi_update error updating primary txt records.");
|
||||
}
|
||||
|
||||
if (secondary_txt_records != NULL) {
|
||||
if (ap2_text_record_string_list)
|
||||
avahi_string_list_free(ap2_text_record_string_list);
|
||||
ap2_text_record_string_list =
|
||||
avahi_string_list_new_from_array((const char **)secondary_txt_records, -1);
|
||||
err = avahi_entry_group_update_service_txt_strlst(group, selected_interface, AVAHI_PROTO_UNSPEC,
|
||||
0, ap2_service_name, config.regtype2, NULL,
|
||||
ap2_text_record_string_list);
|
||||
if (err != 0)
|
||||
debug(1, "avahi_update error updating secondary txt records.");
|
||||
}
|
||||
|
||||
pthread_cleanup_pop(1); // unlock the avahi_threaded_poll_lock
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int avahi_register(char *ap1name, char *ap2name, int srvport, char **txt_records,
|
||||
char **secondary_txt_records) {
|
||||
// debug(1, "avahi_register.");
|
||||
service_name = strdup(srvname);
|
||||
service_name = strdup(ap1name);
|
||||
if (ap2name != NULL)
|
||||
ap2_service_name = strdup(ap2name);
|
||||
|
||||
text_record_string_list = avahi_string_list_new_from_array((const char **)txt_records, -1);
|
||||
|
||||
if (secondary_txt_records != NULL)
|
||||
ap2_text_record_string_list =
|
||||
avahi_string_list_new_from_array((const char **)secondary_txt_records, -1);
|
||||
|
||||
port = srvport;
|
||||
|
||||
int err;
|
||||
@@ -342,7 +411,7 @@ static int avahi_register(char *srvname, int srvport) {
|
||||
}
|
||||
|
||||
static void avahi_unregister(void) {
|
||||
// debug(1, "avahi_unregister.");
|
||||
debug(2, "avahi_unregister.");
|
||||
if (tpoll) {
|
||||
debug(2, "avahi: stop the threaded poll.");
|
||||
avahi_threaded_poll_stop(tpoll);
|
||||
@@ -364,15 +433,34 @@ static void avahi_unregister(void) {
|
||||
if (service_name) {
|
||||
debug(2, "avahi: free the service name.");
|
||||
free(service_name);
|
||||
service_name = NULL;
|
||||
} else
|
||||
debug(1, "avahi attempt to free NULL service name");
|
||||
service_name = NULL;
|
||||
|
||||
if (ap2_service_name) {
|
||||
debug(2, "avahi: free the ap2 service_name.");
|
||||
free(ap2_service_name);
|
||||
ap2_service_name = NULL;
|
||||
}
|
||||
|
||||
if (text_record_string_list) {
|
||||
debug(2, "avahi free text_record_string_list");
|
||||
avahi_string_list_free(text_record_string_list);
|
||||
text_record_string_list = NULL;
|
||||
} else
|
||||
debug(1, "avahi attempt to free NULL text_record_string_list");
|
||||
|
||||
if (ap2_text_record_string_list) {
|
||||
debug(2, "avahi free ap_2text_record_string_list");
|
||||
avahi_string_list_free(ap2_text_record_string_list);
|
||||
ap2_text_record_string_list = NULL;
|
||||
}
|
||||
}
|
||||
|
||||
void avahi_dacp_monitor_start(void) {
|
||||
// debug(1, "avahi_dacp_monitor_start.");
|
||||
memset((void *)&private_dbs, 0, sizeof(dacp_browser_struct));
|
||||
debug(1, "avahi_dacp_monitor_start Avahi DACP monitor successfully started");
|
||||
debug(2, "avahi_dacp_monitor_start Avahi DACP monitor successfully started");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -380,27 +468,35 @@ void avahi_dacp_monitor_set_id(const char *dacp_id) {
|
||||
// debug(1, "avahi_dacp_monitor_set_id: Search for DACP ID \"%s\".", t);
|
||||
dacp_browser_struct *dbs = &private_dbs;
|
||||
|
||||
if (dbs->dacp_id)
|
||||
free(dbs->dacp_id);
|
||||
if (dacp_id == NULL)
|
||||
dbs->dacp_id = NULL;
|
||||
else {
|
||||
char *t = strdup(dacp_id);
|
||||
if (t) {
|
||||
dbs->dacp_id = t;
|
||||
avahi_threaded_poll_lock(tpoll);
|
||||
if (dbs->service_browser)
|
||||
avahi_service_browser_free(dbs->service_browser);
|
||||
if (((dbs->dacp_id) && (dacp_id) && (strcmp(dbs->dacp_id, dacp_id) == 0)) ||
|
||||
((dbs->dacp_id == NULL) && (dacp_id == NULL))) {
|
||||
debug(3, "no change...");
|
||||
} else {
|
||||
if (dbs->dacp_id)
|
||||
free(dbs->dacp_id);
|
||||
if (dacp_id == NULL)
|
||||
dbs->dacp_id = NULL;
|
||||
else {
|
||||
char *t = strdup(dacp_id);
|
||||
if (t) {
|
||||
dbs->dacp_id = t;
|
||||
pthread_avahi_threaded_poll_lock_and_push(tpoll);
|
||||
// avahi_threaded_poll_lock(tpoll);
|
||||
if (dbs->service_browser)
|
||||
avahi_service_browser_free(dbs->service_browser);
|
||||
|
||||
if (!(dbs->service_browser =
|
||||
avahi_service_browser_new(client, AVAHI_IF_UNSPEC, AVAHI_PROTO_UNSPEC, "_dacp._tcp",
|
||||
NULL, 0, browse_callback, (void *)dbs))) {
|
||||
warn("failed to create avahi service browser: %s\n",
|
||||
avahi_strerror(avahi_client_errno(client)));
|
||||
if (!(dbs->service_browser =
|
||||
avahi_service_browser_new(client, AVAHI_IF_UNSPEC, AVAHI_PROTO_UNSPEC,
|
||||
"_dacp._tcp", NULL, 0, browse_callback, (void *)dbs))) {
|
||||
warn("failed to create avahi service browser: %s\n",
|
||||
avahi_strerror(avahi_client_errno(client)));
|
||||
}
|
||||
pthread_cleanup_pop(1); // unlock the avahi_threaded_poll_lock
|
||||
// avahi_threaded_poll_unlock(tpoll);
|
||||
debug(2, "dacp_monitor for \"%s\"", dacp_id);
|
||||
} else {
|
||||
warn("avahi_dacp_set_id: can not allocate a dacp_id string in dacp_browser_struct.");
|
||||
}
|
||||
avahi_threaded_poll_unlock(tpoll);
|
||||
} else {
|
||||
warn("avahi_dacp_set_id: can not allocate a dacp_id string in dacp_browser_struct.");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -409,18 +505,21 @@ void avahi_dacp_monitor_stop() {
|
||||
// debug(1, "avahi_dacp_monitor_stop");
|
||||
dacp_browser_struct *dbs = &private_dbs;
|
||||
// stop and dispose of everything
|
||||
avahi_threaded_poll_lock(tpoll);
|
||||
pthread_avahi_threaded_poll_lock_and_push(tpoll);
|
||||
// avahi_threaded_poll_lock(tpoll);
|
||||
if (dbs->service_browser) {
|
||||
avahi_service_browser_free(dbs->service_browser);
|
||||
dbs->service_browser = NULL;
|
||||
}
|
||||
avahi_threaded_poll_unlock(tpoll);
|
||||
pthread_cleanup_pop(1); // unlock the avahi_threaded_poll_lock
|
||||
// avahi_threaded_poll_unlock(tpoll);
|
||||
free(dbs->dacp_id);
|
||||
debug(2, "avahi_dacp_monitor_stop Avahi DACP monitor successfully stopped");
|
||||
}
|
||||
|
||||
mdns_backend mdns_avahi = {.name = "avahi",
|
||||
.mdns_register = avahi_register,
|
||||
.mdns_update = avahi_update,
|
||||
.mdns_unregister = avahi_unregister,
|
||||
.mdns_dacp_monitor_start = avahi_dacp_monitor_start,
|
||||
.mdns_dacp_monitor_set_id = avahi_dacp_monitor_set_id,
|
||||
|
||||
+4
-2
@@ -33,7 +33,9 @@
|
||||
|
||||
static DNSServiceRef service;
|
||||
|
||||
static int mdns_dns_sd_register(char *apname, int port) {
|
||||
static int mdns_dns_sd_register(char *ap1name, __attribute__((unused)) char *ap2name, int port,
|
||||
__attribute__((unused)) char **txt_records,
|
||||
__attribute__((unused)) char **secondary_txt_records) {
|
||||
char *recordwithoutmetadata[] = {MDNS_RECORD_WITHOUT_METADATA, NULL};
|
||||
#ifdef CONFIG_METADATA
|
||||
char *recordwithmetadata[] = {MDNS_RECORD_WITH_METADATA, NULL};
|
||||
@@ -70,7 +72,7 @@ static int mdns_dns_sd_register(char *apname, int port) {
|
||||
}
|
||||
|
||||
DNSServiceErrorType error;
|
||||
error = DNSServiceRegister(&service, 0, kDNSServiceInterfaceIndexAny, apname, config.regtype, "",
|
||||
error = DNSServiceRegister(&service, 0, kDNSServiceInterfaceIndexAny, ap1name, config.regtype, "",
|
||||
NULL, htons((uint16_t)port), length, buf, NULL, NULL);
|
||||
|
||||
free(buf);
|
||||
|
||||
+12
-6
@@ -80,14 +80,17 @@ static int fork_execvp(const char *file, char *const argv[]) {
|
||||
return response;
|
||||
}
|
||||
|
||||
static int mdns_external_avahi_register(char *apname, __attribute__((unused)) int port) {
|
||||
static int mdns_external_avahi_register(char *ap1name, __attribute__((unused)) char *ap2name,
|
||||
__attribute__((unused)) int port,
|
||||
__attribute__((unused)) char **txt_records,
|
||||
__attribute__((unused)) char **secondary_txt_records) {
|
||||
char mdns_port[6];
|
||||
snprintf(mdns_port, sizeof(mdns_port), "%d", config.port);
|
||||
|
||||
char *argvwithoutmetadata[] = {
|
||||
NULL, apname, config.regtype, mdns_port, MDNS_RECORD_WITHOUT_METADATA, NULL};
|
||||
NULL, ap1name, config.regtype, mdns_port, MDNS_RECORD_WITHOUT_METADATA, NULL};
|
||||
#ifdef CONFIG_METADATA
|
||||
char *argvwithmetadata[] = {NULL, apname, config.regtype, mdns_port, MDNS_RECORD_WITH_METADATA,
|
||||
char *argvwithmetadata[] = {NULL, ap1name, config.regtype, mdns_port, MDNS_RECORD_WITH_METADATA,
|
||||
NULL};
|
||||
#endif
|
||||
char **argv;
|
||||
@@ -119,15 +122,18 @@ static int mdns_external_avahi_register(char *apname, __attribute__((unused)) in
|
||||
return -1;
|
||||
}
|
||||
|
||||
static int mdns_external_dns_sd_register(char *apname, __attribute__((unused)) int port) {
|
||||
static int mdns_external_dns_sd_register(char *ap1name, __attribute__((unused)) char *ap2name,
|
||||
__attribute__((unused)) int port,
|
||||
__attribute__((unused)) char **txt_records,
|
||||
__attribute__((unused)) char **secondary_txt_records) {
|
||||
char mdns_port[6];
|
||||
snprintf(mdns_port, sizeof(mdns_port), "%d", config.port);
|
||||
|
||||
char *argvwithoutmetadata[] = {
|
||||
NULL, apname, config.regtype, mdns_port, MDNS_RECORD_WITHOUT_METADATA, NULL};
|
||||
NULL, ap1name, config.regtype, mdns_port, MDNS_RECORD_WITHOUT_METADATA, NULL};
|
||||
|
||||
#ifdef CONFIG_METADATA
|
||||
char *argvwithmetadata[] = {NULL, apname, config.regtype, mdns_port, MDNS_RECORD_WITH_METADATA,
|
||||
char *argvwithmetadata[] = {NULL, ap1name, config.regtype, mdns_port, MDNS_RECORD_WITH_METADATA,
|
||||
NULL};
|
||||
#endif
|
||||
|
||||
|
||||
+4
-2
@@ -38,7 +38,9 @@
|
||||
|
||||
static struct mdnsd *svr = NULL;
|
||||
|
||||
static int mdns_tinysvcmdns_register(char *apname, int port) {
|
||||
static int mdns_tinysvcmdns_register(char *ap1name, __attribute__((unused)) char *ap2name, int port,
|
||||
__attribute__((unused)) char **txt_records,
|
||||
__attribute__((unused)) char **secondary_txt_records) {
|
||||
struct ifaddrs *ifalist;
|
||||
struct ifaddrs *ifa;
|
||||
|
||||
@@ -145,7 +147,7 @@ static int mdns_tinysvcmdns_register(char *apname, int port) {
|
||||
strcat(extendedregtype, ".local");
|
||||
|
||||
struct mdns_service *svc =
|
||||
mdnsd_register_svc(svr, apname, extendedregtype, port, NULL,
|
||||
mdnsd_register_svc(svr, ap1name, extendedregtype, port, NULL,
|
||||
(const char **)txt); // TTL should be 75 minutes, i.e. 4500 seconds
|
||||
mdns_service_destroy(svc);
|
||||
|
||||
|
||||
+4
-1
@@ -298,8 +298,11 @@ char *metadata_write_image_file(const char *buf, int len) {
|
||||
}
|
||||
}
|
||||
}
|
||||
if (close(dir_fd) < 0)
|
||||
debug(1, "Error %d closing directory \"%s\"", errno, config.cover_art_cache_dir);
|
||||
} else {
|
||||
debug(1, "Can't open the directory for deletion.");
|
||||
debug(1, "Can't open the directory \"%s\" for deletion -- error %d.",
|
||||
config.cover_art_cache_dir, errno);
|
||||
}
|
||||
free(full_filename);
|
||||
closedir(d);
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
#include <inttypes.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
@@ -11,6 +12,7 @@
|
||||
#include "rtp.h"
|
||||
|
||||
#include "dacp.h"
|
||||
#include "metadata_hub.h"
|
||||
#include "mqtt.h"
|
||||
#include <mosquitto.h>
|
||||
|
||||
@@ -91,7 +93,15 @@ void on_connect(struct mosquitto *mosq, __attribute__((unused)) void *userdata,
|
||||
}
|
||||
|
||||
// helper function to publish under a topic and automatically append the main topic
|
||||
void mqtt_publish(char *topic, char *data, uint32_t length) {
|
||||
void mqtt_publish(char *topic, char *data_in, uint32_t length_in) {
|
||||
char *data = data_in;
|
||||
uint32_t length = length_in;
|
||||
|
||||
if ((length == 0) && (config.mqtt_empty_payload_substitute != NULL)) {
|
||||
length = strlen(config.mqtt_empty_payload_substitute);
|
||||
data = config.mqtt_empty_payload_substitute;
|
||||
}
|
||||
|
||||
char fulltopic[strlen(config.mqtt_topic) + strlen(topic) + 3];
|
||||
snprintf(fulltopic, strlen(config.mqtt_topic) + strlen(topic) + 2, "%s/%s", config.mqtt_topic,
|
||||
topic);
|
||||
@@ -129,6 +139,10 @@ void mqtt_process_metadata(uint32_t type, uint32_t code, char *data, uint32_t le
|
||||
}
|
||||
if (config.mqtt_publish_parsed) {
|
||||
if (type == 'core') {
|
||||
int32_t r;
|
||||
uint64_t trackid;
|
||||
char trackidstring[32];
|
||||
|
||||
switch (code) {
|
||||
case 'asar':
|
||||
mqtt_publish("artist", data, length);
|
||||
@@ -136,32 +150,48 @@ void mqtt_process_metadata(uint32_t type, uint32_t code, char *data, uint32_t le
|
||||
case 'asal':
|
||||
mqtt_publish("album", data, length);
|
||||
break;
|
||||
case 'minm':
|
||||
mqtt_publish("title", data, length);
|
||||
case 'asfm':
|
||||
mqtt_publish("format", data, length);
|
||||
break;
|
||||
case 'asgn':
|
||||
mqtt_publish("genre", data, length);
|
||||
break;
|
||||
case 'asfm':
|
||||
mqtt_publish("format", data, length);
|
||||
case 'minm':
|
||||
mqtt_publish("title", data, length);
|
||||
break;
|
||||
case 'mper':
|
||||
trackid = *(uint64_t *)(data);
|
||||
r = snprintf(trackidstring, sizeof(trackidstring), "%" PRIX64 "", trackid);
|
||||
mqtt_publish("track_id", trackidstring, r);
|
||||
}
|
||||
} else if (type == 'ssnc') {
|
||||
switch (code) {
|
||||
case 'abeg':
|
||||
mqtt_publish("active_start", data, length);
|
||||
break;
|
||||
case 'acre':
|
||||
mqtt_publish("active_remote_id", data, length);
|
||||
break;
|
||||
case 'aend':
|
||||
mqtt_publish("active_end", data, length);
|
||||
break;
|
||||
case 'asal':
|
||||
mqtt_publish("songalbum", data, length);
|
||||
break;
|
||||
case 'pvol':
|
||||
mqtt_publish("volume", data, length);
|
||||
break;
|
||||
case 'clip':
|
||||
mqtt_publish("client_ip", data, length);
|
||||
break;
|
||||
case 'abeg':
|
||||
mqtt_publish("active_start", data, length);
|
||||
case 'cdid':
|
||||
mqtt_publish("client_device_id", data, length);
|
||||
break;
|
||||
case 'aend':
|
||||
mqtt_publish("active_end", data, length);
|
||||
case 'cmac':
|
||||
mqtt_publish("client_mac_address", data, length);
|
||||
break;
|
||||
case 'cmod':
|
||||
mqtt_publish("client_model", data, length);
|
||||
break;
|
||||
case 'daid':
|
||||
mqtt_publish("dacp_id", data, length);
|
||||
break;
|
||||
case 'pbeg':
|
||||
mqtt_publish("play_start", data, length);
|
||||
@@ -172,14 +202,23 @@ void mqtt_process_metadata(uint32_t type, uint32_t code, char *data, uint32_t le
|
||||
case 'pfls':
|
||||
mqtt_publish("play_flush", data, length);
|
||||
break;
|
||||
case 'prsm':
|
||||
mqtt_publish("play_resume", data, length);
|
||||
break;
|
||||
case 'PICT':
|
||||
if (config.mqtt_publish_cover) {
|
||||
mqtt_publish("cover", data, length);
|
||||
}
|
||||
break;
|
||||
case 'prsm':
|
||||
mqtt_publish("play_resume", data, length);
|
||||
break;
|
||||
case 'pvol':
|
||||
mqtt_publish("volume", data, length);
|
||||
break;
|
||||
case 'snam':
|
||||
mqtt_publish("client_name", data, length);
|
||||
break;
|
||||
case 'svip':
|
||||
mqtt_publish("server_ip", data, length);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
/*
|
||||
* This file is part of the nqptp distribution (https://github.com/mikebrady/nqptp).
|
||||
* Copyright (c) 2021--2022 Mike Brady.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, version 2.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
* General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
* Commercial licensing is also available.
|
||||
*/
|
||||
|
||||
#ifndef NQPTP_SHM_STRUCTURES_H
|
||||
#define NQPTP_SHM_STRUCTURES_H
|
||||
|
||||
#define NQPTP_INTERFACE_NAME "/nqptp"
|
||||
|
||||
#define NQPTP_SHM_STRUCTURES_VERSION 8
|
||||
#define NQPTP_CONTROL_PORT 9000
|
||||
|
||||
// The control port expects a UDP packet with the first space-delimited string
|
||||
// being the name of the shared memory interface (SMI) to be used.
|
||||
// This allows client applications to have a dedicated named SMI interface with
|
||||
// a timing peer list independent of other clients.
|
||||
// The name given must be a valid SMI name and must contain no spaces.
|
||||
// If the named SMI interface doesn't exist it will be created by NQPTP.
|
||||
// The SMI name should be delimited by a space and followed by a command letter.
|
||||
// At present, the only command is "T", which must followed by nothing or by
|
||||
// a space and a space-delimited list of IPv4 or IPv6 numbers,
|
||||
// the whole not to exceed 4096 characters in total.
|
||||
// The IPs, if provided, will become the new list of timing peers, replacing any
|
||||
// previous list. If the master clock of the new list is the same as that of the old list,
|
||||
// the master clock is retained without resynchronisation; this means that non-master
|
||||
// devices can be added and removed without disturbing the SMI's existing master clock.
|
||||
// If no timing list is provided, the existing timing list is deleted.
|
||||
// (In future version of NQPTP the SMI interface may also be deleted at this point.)
|
||||
// SMI interfaces are not currently deleted or garbage collected.
|
||||
|
||||
#include <inttypes.h>
|
||||
#include <pthread.h>
|
||||
|
||||
struct shm_structure {
|
||||
pthread_mutex_t shm_mutex; // for safely accessing the structure
|
||||
uint16_t version; // check this is equal to NQPTP_SHM_STRUCTURES_VERSION
|
||||
uint64_t master_clock_id; // the current master clock
|
||||
char master_clock_ip[64]; // where it's coming from
|
||||
uint64_t local_time; // the time when the offset was calculated
|
||||
uint64_t local_to_master_time_offset; // add this to the local time to get master clock time
|
||||
uint64_t master_clock_start_time; // this is when the master clock became master
|
||||
};
|
||||
|
||||
#endif
|
||||
@@ -11,6 +11,8 @@
|
||||
<property name="ConvolutionGain" type="d" access="readwrite" />
|
||||
<property name="ConvolutionImpulseResponseFile" type="s" access="readwrite" />
|
||||
<property name="DriftTolerance" type="d" access="readwrite" />
|
||||
<property name='Volume' type='d' access='readwrite'/>
|
||||
<method name="DropSession"/>
|
||||
<method name="RemoteCommand">
|
||||
<arg name="command" type="s" direction="in" />
|
||||
<arg name="reply" type="i" direction="out" />
|
||||
@@ -21,6 +23,7 @@
|
||||
<property name="ALACDecoder" type="s" access="readwrite" />
|
||||
<property name="Version" type="s" access="read" />
|
||||
<property name="VersionString" type="s" access="read" />
|
||||
<property name="Protocol" type="s" access="read" />
|
||||
</interface>
|
||||
<interface name="org.gnome.ShairportSync.Diagnostics">
|
||||
<property name="Verbosity" type="i" access="readwrite" />
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
*.o
|
||||
*.lo
|
||||
*.a
|
||||
*.la
|
||||
.dirstamp
|
||||
.deps/
|
||||
.libs/
|
||||
|
||||
client-example
|
||||
server-example
|
||||
@@ -0,0 +1,73 @@
|
||||
# pair_ap
|
||||
C client implementation of pairing for:
|
||||
* Apple TV device verification, which became mandatory with tvOS 10.2 (this is
|
||||
called fruit mode in pair_ap)
|
||||
* Homekit pairing (also for AirPlay 2)
|
||||
|
||||
Credit goes to @funtax and @ViktoriiaKh for doing some of the heavy lifting.
|
||||
|
||||
## Requirements
|
||||
- libsodium
|
||||
- libgcrypt or libopenssl
|
||||
- libplist (only for Apple TV device verification)
|
||||
|
||||
To build the example client and server you also need libevent2. If the
|
||||
dependencies are met you can build simply by running 'make'.
|
||||
|
||||
## Homekit pairing
|
||||
Since I haven't been able to find much information on the internet on how
|
||||
Homekit pairing is designed, here is a write-up of my current understanding. If
|
||||
you know better, please help improve this.
|
||||
|
||||
With Homekit pairing, there may be a controller (e.g. the Home app), and a
|
||||
number of devices/accessories (e.g. speakers). The controller acts as a client
|
||||
and can make requests for pairing. After it is paired it can also add other
|
||||
"third-party" pairings to the device, it can remove pairings and it can ask for
|
||||
a list of pairings.
|
||||
|
||||
Other parties, e.g. the Music app or just iOS as an Airplay sender, can also
|
||||
pair with devices/accesssories in a similar manner, but they are not full-
|
||||
fledged Homekit controllers and thus don't make requests for adding, removing
|
||||
or listing pairings.
|
||||
|
||||
The controller uses `/pair-add` to make sure that all devices on a network get
|
||||
the ID and public key of all the other devices, so that the user only needs to
|
||||
pair a device once.
|
||||
|
||||
### Normal pairing with one-time code
|
||||
For a normal first-time pairing, the client needs a one-time code (the device
|
||||
announces via mDNS whether a code is required). The client calls
|
||||
`/pair-pin-start` and the device displays the code. There is also QR-based
|
||||
pairing, which is (probably?) an encoded code.
|
||||
|
||||
After obtaining the code, the client initiates a three step `/pair-setup`
|
||||
sequence, which results in both peers registering each other's ID and public
|
||||
key. Henceforth, a pairing is verified with the two step `/pair-verify`, where
|
||||
the parties check eachothers identify. Saving the peer's ID + public key isn't
|
||||
strictly necessary if client or server doesn't care about verifying the peer,
|
||||
i.e. that `/pair-setup` has actually been completed.
|
||||
|
||||
The result of `/pair-verify` is a shared secret that is used for symmetric
|
||||
encryption of the following communication between the parties.
|
||||
|
||||
### Transient pairing
|
||||
Some devices don't require a code from the user for pairing (e.g. an Airport
|
||||
Express 2). If so, the client just needs to go through a two-step `/pair-setup`
|
||||
sequence which results in a shared secret, which is then used for encrypted
|
||||
communication. A fixed code of 3939 is used.
|
||||
|
||||
The controller can still use `/pair-add` etc. towards such devices.
|
||||
|
||||
## "fruit" pairing
|
||||
Like normal Homekit pairing, this consists of first requesting a code with
|
||||
`/pair-pin-start`, then a three-step `/pair-setup` and finally a two-step
|
||||
`/pair-verify`. After that the communication is encrypted with the resulting
|
||||
shared secret.
|
||||
|
||||
|
||||
## Acknowledgments
|
||||
- [AirPlayAuth](https://github.com/funtax/AirPlayAuth)
|
||||
- [AirPlayAuth-ObjC](https://github.com/ViktoriiaKh/AirPlayAuth-ObjC)
|
||||
- [ap2-sender](https://github.com/ViktoriiaKh/ap2-sender)
|
||||
- [airplay2-receiver](https://github.com/ckdo/airplay2-receiver)
|
||||
- [csrp](https://github.com/cocagne/csrp)
|
||||
@@ -0,0 +1,593 @@
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <stdint.h>
|
||||
#include <string.h>
|
||||
|
||||
#include <event2/event.h>
|
||||
#include <event2/buffer.h>
|
||||
|
||||
#include "evrtsp/evrtsp.h"
|
||||
#include "pair.h"
|
||||
|
||||
#ifdef CONFIG_GCRYPT
|
||||
# include <gcrypt.h>
|
||||
#endif
|
||||
|
||||
#define DEVICE_ID "AABBCCDD11223344"
|
||||
#define ACTIVE_REMOTE "3515324763"
|
||||
#define DACP_ID "FF1DB45949E6CBD3"
|
||||
#define USER_AGENT "AirPlay/381.13"
|
||||
|
||||
#define ENDPOINT_SETUP_FRUIT "/pair-setup-pin"
|
||||
#define CONTENTTYPE_SETUP_FRUIT "application/x-apple-binary-plist"
|
||||
|
||||
#define ENDPOINT_SETUP_HOMEKIT "/pair-setup"
|
||||
#define CONTENTTYPE_SETUP_HOMEKIT "application/octet-stream"
|
||||
|
||||
|
||||
typedef void (*request_cb)(struct evrtsp_request *, void *);
|
||||
|
||||
static struct event_base *evbase;
|
||||
static struct evrtsp_connection *evcon;
|
||||
static int cseq;
|
||||
|
||||
static const char *endpoint_setup;
|
||||
static const char *content_type_setup;
|
||||
|
||||
static enum pair_type pair_type;
|
||||
|
||||
static struct pair_cipher_context *cipher_ctx;
|
||||
static struct pair_verify_context *verify_ctx;
|
||||
static struct pair_setup_context *setup_ctx;
|
||||
|
||||
|
||||
static char *
|
||||
prompt_pin(void)
|
||||
{
|
||||
char *pin = NULL;
|
||||
size_t len;
|
||||
|
||||
printf ("Enter pin: ");
|
||||
fflush (stdout);
|
||||
|
||||
len = getline(&pin, &len, stdin);
|
||||
if (len != 5) // Includes EOL
|
||||
{
|
||||
printf ("Bad pin length %zu\n", len);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
return pin;
|
||||
}
|
||||
|
||||
static int
|
||||
response_process(uint8_t **response, struct evrtsp_request *req)
|
||||
{
|
||||
if (!req)
|
||||
{
|
||||
printf("failed, could not read response\n");
|
||||
return -1;
|
||||
}
|
||||
else if (req->response_code != 200)
|
||||
{
|
||||
printf("failed with error code %d: %s (body is %zu bytes)\n\n", req->response_code, req->response_code_line, evbuffer_get_length(req->input_buffer));
|
||||
return -1;
|
||||
}
|
||||
|
||||
printf("success\n\n");
|
||||
|
||||
*response = evbuffer_pullup(req->input_buffer, -1);
|
||||
|
||||
return evbuffer_get_length(req->input_buffer);
|
||||
}
|
||||
|
||||
static int
|
||||
make_request(const char *url, const void *data, size_t len, const char *content_type, request_cb cb)
|
||||
{
|
||||
struct evrtsp_request *req;
|
||||
char buffer[1024];
|
||||
|
||||
req = evrtsp_request_new(cb, NULL);
|
||||
|
||||
if (data)
|
||||
evbuffer_add(req->output_buffer, data, len);
|
||||
|
||||
if (content_type)
|
||||
evrtsp_add_header(req->output_headers, "Content-Type", content_type);
|
||||
|
||||
cseq++;
|
||||
snprintf(buffer, sizeof(buffer), "%d", cseq);
|
||||
evrtsp_add_header(req->output_headers, "CSeq", buffer);
|
||||
|
||||
evrtsp_add_header(req->output_headers, "User-Agent", USER_AGENT);
|
||||
// evrtsp_add_header(req->output_headers, "DACP-ID", DACP_ID);
|
||||
// evrtsp_add_header(req->output_headers, "Active-Remote", ACTIVE_REMOTE);
|
||||
|
||||
if (pair_type == PAIR_CLIENT_HOMEKIT_NORMAL)
|
||||
evrtsp_add_header(req->output_headers, "X-Apple-HKP", "3");
|
||||
else if (pair_type == PAIR_CLIENT_HOMEKIT_TRANSIENT)
|
||||
evrtsp_add_header(req->output_headers, "X-Apple-HKP", "4");
|
||||
|
||||
printf("Making request %d to '%s' (len %zu)... ", cseq, url, len);
|
||||
|
||||
return evrtsp_make_request(evcon, req, EVRTSP_REQ_POST, url);
|
||||
}
|
||||
|
||||
static int
|
||||
make_request_options(const char *url, const void *data, size_t len, const char *content_type, request_cb cb)
|
||||
{
|
||||
struct evrtsp_request *req;
|
||||
char buffer[1024];
|
||||
|
||||
req = evrtsp_request_new(cb, NULL);
|
||||
|
||||
if (data)
|
||||
evbuffer_add(req->output_buffer, data, len);
|
||||
|
||||
if (content_type)
|
||||
evrtsp_add_header(req->output_headers, "Content-Type", content_type);
|
||||
|
||||
cseq++;
|
||||
snprintf(buffer, sizeof(buffer), "%d", cseq);
|
||||
evrtsp_add_header(req->output_headers, "CSeq", buffer);
|
||||
|
||||
evrtsp_add_header(req->output_headers, "User-Agent", USER_AGENT);
|
||||
// evrtsp_add_header(req->output_headers, "DACP-ID", DACP_ID);
|
||||
// evrtsp_add_header(req->output_headers, "Active-Remote", ACTIVE_REMOTE);
|
||||
evrtsp_add_header(req->output_headers, "X-Apple-HKP", "3");
|
||||
|
||||
printf("Making request %d to '%s' (len %zu)... ", cseq, url, len);
|
||||
|
||||
return evrtsp_make_request(evcon, req, EVRTSP_REQ_OPTIONS, url);
|
||||
}
|
||||
|
||||
|
||||
static void
|
||||
options_response(struct evrtsp_request *req, void *arg)
|
||||
{
|
||||
uint8_t *response;
|
||||
int ret;
|
||||
|
||||
ret = response_process(&response, req);
|
||||
if (ret >= 0)
|
||||
printf("OPTIONS complete\n");
|
||||
|
||||
printf("Done\n");
|
||||
|
||||
event_base_loopbreak(evbase);
|
||||
}
|
||||
|
||||
static int
|
||||
options_request(void)
|
||||
{
|
||||
return make_request_options("*", NULL, 0, NULL, options_response);
|
||||
}
|
||||
|
||||
static void
|
||||
rtsp_cipher(struct evbuffer *evbuf, void *arg, int encrypt)
|
||||
{
|
||||
uint8_t *out = NULL;
|
||||
size_t out_len = 0;
|
||||
ssize_t processed;
|
||||
|
||||
uint8_t *in = evbuffer_pullup(evbuf, -1);
|
||||
size_t in_len = evbuffer_get_length(evbuf);
|
||||
|
||||
if (encrypt)
|
||||
processed = pair_encrypt(&out, &out_len, in, in_len, cipher_ctx);
|
||||
else
|
||||
processed = pair_decrypt(&out, &out_len, in, in_len, cipher_ctx);
|
||||
|
||||
evbuffer_drain(evbuf, in_len);
|
||||
|
||||
if (processed < 0)
|
||||
{
|
||||
printf("Error while ciphering: %s\n", pair_cipher_errmsg(cipher_ctx));
|
||||
return;
|
||||
}
|
||||
else if (processed != in_len)
|
||||
{
|
||||
printf("Partial ciphering, only %zd of %zu input bytes were processed\n", processed, in_len);
|
||||
}
|
||||
|
||||
evbuffer_add(evbuf, out, out_len);
|
||||
}
|
||||
|
||||
static void
|
||||
verify_step2_response(struct evrtsp_request *req, void *arg)
|
||||
{
|
||||
uint8_t *response;
|
||||
struct pair_result *result;
|
||||
int ret;
|
||||
|
||||
ret = response_process(&response, req);
|
||||
if (ret < 0)
|
||||
goto error;
|
||||
|
||||
ret = pair_verify_response2(verify_ctx, response, ret);
|
||||
if (ret < 0)
|
||||
goto error;
|
||||
|
||||
printf("Verify complete!\n\n");
|
||||
|
||||
ret = pair_verify_result(&result, verify_ctx);
|
||||
if (ret < 0)
|
||||
goto error;
|
||||
|
||||
cipher_ctx = pair_cipher_new(pair_type, 0, result->shared_secret, result->shared_secret_len);
|
||||
if (!cipher_ctx)
|
||||
goto error;
|
||||
|
||||
evrtsp_connection_set_ciphercb(evcon, rtsp_cipher, NULL);
|
||||
|
||||
ret = options_request();
|
||||
if (ret < 0)
|
||||
goto error;
|
||||
|
||||
pair_verify_free(verify_ctx);
|
||||
|
||||
return;
|
||||
|
||||
error:
|
||||
printf("Error: %s\n", pair_verify_errmsg(verify_ctx));
|
||||
pair_verify_free(verify_ctx);
|
||||
pair_cipher_free(cipher_ctx);
|
||||
event_base_loopbreak(evbase);
|
||||
}
|
||||
|
||||
static int
|
||||
verify_step2_request(void)
|
||||
{
|
||||
uint8_t *request;
|
||||
size_t len;
|
||||
int ret;
|
||||
|
||||
request = pair_verify_request2(&len, verify_ctx);
|
||||
if (!request)
|
||||
return -1;
|
||||
|
||||
ret = make_request("/pair-verify", request, len, "application/octet-stream", verify_step2_response);
|
||||
|
||||
free(request);
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
static void
|
||||
verify_step1_response(struct evrtsp_request *req, void *arg)
|
||||
{
|
||||
uint8_t *response;
|
||||
int ret;
|
||||
|
||||
ret = response_process(&response, req);
|
||||
if (ret <= 0)
|
||||
goto error;
|
||||
|
||||
ret = pair_verify_response1(verify_ctx, response, ret);
|
||||
if (ret < 0)
|
||||
goto error;
|
||||
|
||||
ret = verify_step2_request();
|
||||
if (ret < 0)
|
||||
goto error;
|
||||
|
||||
return;
|
||||
|
||||
error:
|
||||
printf("Error: %s\n", pair_verify_errmsg(verify_ctx));
|
||||
pair_verify_free(verify_ctx);
|
||||
event_base_loopbreak(evbase);
|
||||
}
|
||||
|
||||
static int
|
||||
verify_step1_request(const char *authorisation_key)
|
||||
{
|
||||
uint8_t *request = NULL;
|
||||
size_t len;
|
||||
int ret;
|
||||
|
||||
verify_ctx = pair_verify_new(pair_type, authorisation_key, NULL, NULL, DEVICE_ID);
|
||||
if (!verify_ctx)
|
||||
return -1;
|
||||
|
||||
request = pair_verify_request1(&len, verify_ctx);
|
||||
if (!request)
|
||||
goto error;
|
||||
|
||||
ret = make_request("/pair-verify", request, len, "application/octet-stream", verify_step1_response);
|
||||
if (ret < 0)
|
||||
goto error;
|
||||
|
||||
free(request);
|
||||
return ret;
|
||||
|
||||
error:
|
||||
printf("Error: %s\n", pair_verify_errmsg(verify_ctx));
|
||||
pair_verify_free(verify_ctx);
|
||||
free(request);
|
||||
return -1;
|
||||
}
|
||||
|
||||
static void
|
||||
setup_step3_response(struct evrtsp_request *req, void *arg)
|
||||
{
|
||||
const char *key;
|
||||
uint8_t *response;
|
||||
struct pair_result *result;
|
||||
int ret;
|
||||
|
||||
ret = response_process(&response, req);
|
||||
if (ret <= 0)
|
||||
goto error;
|
||||
|
||||
ret = pair_setup_response3(setup_ctx, response, ret);
|
||||
if (ret < 0)
|
||||
goto error;
|
||||
|
||||
ret = pair_setup_result(&key, &result, setup_ctx);
|
||||
if (ret < 0)
|
||||
goto error;
|
||||
|
||||
printf("Setup of device ID %s complete, got key: %s\n", result->device_id, key);
|
||||
|
||||
ret = verify_step1_request(key);
|
||||
if (ret < 0)
|
||||
goto error;
|
||||
|
||||
pair_setup_free(setup_ctx);
|
||||
|
||||
return;
|
||||
|
||||
error:
|
||||
printf("Error: %s\n", pair_setup_errmsg(setup_ctx));
|
||||
pair_setup_free(setup_ctx);
|
||||
event_base_loopbreak(evbase);
|
||||
}
|
||||
|
||||
static int
|
||||
setup_step3_request(void)
|
||||
{
|
||||
uint8_t *request;
|
||||
size_t len;
|
||||
int ret;
|
||||
|
||||
request = pair_setup_request3(&len, setup_ctx);
|
||||
if (!request)
|
||||
return -1;
|
||||
|
||||
ret = make_request(endpoint_setup, request, len, content_type_setup, setup_step3_response);
|
||||
|
||||
free(request);
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
static void
|
||||
setup_step2_response(struct evrtsp_request *req, void *arg)
|
||||
{
|
||||
uint8_t *response;
|
||||
struct pair_result *result;
|
||||
int ret;
|
||||
|
||||
ret = response_process(&response, req);
|
||||
if (ret <= 0)
|
||||
goto error;
|
||||
|
||||
ret = pair_setup_response2(setup_ctx, response, ret);
|
||||
if (ret < 0)
|
||||
goto error;
|
||||
|
||||
printf("Setup SRP stage complete\n");
|
||||
|
||||
if (pair_type == PAIR_CLIENT_HOMEKIT_TRANSIENT)
|
||||
{
|
||||
ret = pair_setup_result(NULL, &result, setup_ctx);
|
||||
if (ret < 0)
|
||||
goto error;
|
||||
|
||||
cipher_ctx = pair_cipher_new(pair_type, 0, result->shared_secret, result->shared_secret_len);
|
||||
if (!cipher_ctx)
|
||||
goto error;
|
||||
|
||||
evrtsp_connection_set_ciphercb(evcon, rtsp_cipher, NULL);
|
||||
|
||||
ret = options_request();
|
||||
if (ret < 0)
|
||||
goto error;
|
||||
|
||||
pair_setup_free(setup_ctx);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
ret = setup_step3_request();
|
||||
if (ret < 0)
|
||||
goto error;
|
||||
|
||||
return;
|
||||
|
||||
error:
|
||||
printf("Error: %s\n", pair_setup_errmsg(setup_ctx));
|
||||
pair_setup_free(setup_ctx);
|
||||
event_base_loopbreak(evbase);
|
||||
}
|
||||
|
||||
static int
|
||||
setup_step2_request(void)
|
||||
{
|
||||
uint8_t *request;
|
||||
size_t len;
|
||||
int ret;
|
||||
|
||||
request = pair_setup_request2(&len, setup_ctx);
|
||||
if (!request)
|
||||
return -1;
|
||||
|
||||
ret = make_request(endpoint_setup, request, len, content_type_setup, setup_step2_response);
|
||||
|
||||
free(request);
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
static void
|
||||
setup_step1_response(struct evrtsp_request *req, void *arg)
|
||||
{
|
||||
uint8_t *response;
|
||||
int ret;
|
||||
|
||||
ret = response_process(&response, req);
|
||||
if (ret <= 0)
|
||||
goto error;
|
||||
|
||||
ret = pair_setup_response1(setup_ctx, response, ret);
|
||||
if (ret < 0)
|
||||
goto error;
|
||||
|
||||
ret = setup_step2_request();
|
||||
if (ret < 0)
|
||||
goto error;
|
||||
|
||||
return;
|
||||
|
||||
error:
|
||||
printf("Error: %s\n", pair_setup_errmsg(setup_ctx));
|
||||
pair_setup_free(setup_ctx);
|
||||
event_base_loopbreak(evbase);
|
||||
}
|
||||
|
||||
static int
|
||||
setup_step1_request(void)
|
||||
{
|
||||
uint8_t *request;
|
||||
size_t len;
|
||||
int ret;
|
||||
|
||||
request = pair_setup_request1(&len, setup_ctx);
|
||||
if (!request)
|
||||
return -1;
|
||||
|
||||
ret = make_request(endpoint_setup, request, len, content_type_setup, setup_step1_response);
|
||||
|
||||
free(request);
|
||||
return ret;
|
||||
}
|
||||
|
||||
static void
|
||||
setup_start_response(struct evrtsp_request *req, void *arg)
|
||||
{
|
||||
uint8_t *response;
|
||||
char *pin = NULL;
|
||||
int ret;
|
||||
|
||||
if (req)
|
||||
{
|
||||
ret = response_process(&response, req);
|
||||
if (ret < 0)
|
||||
goto error;
|
||||
}
|
||||
|
||||
if (pair_type != PAIR_CLIENT_HOMEKIT_TRANSIENT)
|
||||
{
|
||||
pin = prompt_pin();
|
||||
if (!pin)
|
||||
goto error;
|
||||
}
|
||||
|
||||
setup_ctx = pair_setup_new(pair_type, pin, NULL, NULL, DEVICE_ID);
|
||||
if (!setup_ctx)
|
||||
goto error;
|
||||
|
||||
ret = setup_step1_request();
|
||||
if (ret < 0)
|
||||
goto error;
|
||||
|
||||
free(pin);
|
||||
return;
|
||||
|
||||
error:
|
||||
if (setup_ctx)
|
||||
printf("Error: %s\n", pair_setup_errmsg(setup_ctx));
|
||||
free(pin);
|
||||
pair_setup_free(setup_ctx);
|
||||
event_base_loopbreak(evbase);
|
||||
}
|
||||
|
||||
static int
|
||||
setup_start_request(void)
|
||||
{
|
||||
return make_request("/pair-pin-start", NULL, 0, "application/x-apple-binary-plist", setup_start_response);
|
||||
}
|
||||
|
||||
|
||||
int
|
||||
main( int argc, char * argv[] )
|
||||
{
|
||||
int ret;
|
||||
|
||||
if (argc < 4 || argc > 5)
|
||||
{
|
||||
printf("%s ip_address port homekit|fruit|transient [skip_pin]\n", argv[0]);
|
||||
return -1;
|
||||
}
|
||||
|
||||
const char *address = argv[1];
|
||||
const char *port = argv[2];
|
||||
int skip_pin = (argc == 5);
|
||||
|
||||
if (strcmp(argv[3], "fruit") == 0)
|
||||
{
|
||||
printf("Pair type is fruit\n");
|
||||
pair_type = PAIR_CLIENT_FRUIT;
|
||||
endpoint_setup = ENDPOINT_SETUP_FRUIT;
|
||||
content_type_setup = CONTENTTYPE_SETUP_FRUIT;
|
||||
}
|
||||
else if (strcmp(argv[3], "homekit") == 0)
|
||||
{
|
||||
printf("Pair type is homekit (normal)\n");
|
||||
pair_type = PAIR_CLIENT_HOMEKIT_NORMAL;
|
||||
endpoint_setup = ENDPOINT_SETUP_HOMEKIT;
|
||||
content_type_setup = CONTENTTYPE_SETUP_HOMEKIT;
|
||||
}
|
||||
else if (strcmp(argv[3], "transient") == 0)
|
||||
{
|
||||
printf("Pair type is homekit (transient)\n");
|
||||
pair_type = PAIR_CLIENT_HOMEKIT_TRANSIENT;
|
||||
endpoint_setup = ENDPOINT_SETUP_HOMEKIT;
|
||||
content_type_setup = CONTENTTYPE_SETUP_HOMEKIT;
|
||||
}
|
||||
|
||||
// libgcrypt requires that the application initializes the library
|
||||
#ifdef CONFIG_GCRYPT
|
||||
if (!gcry_check_version(NULL))
|
||||
{
|
||||
printf("libgcrypt not initialized\n");
|
||||
return -1;
|
||||
}
|
||||
gcry_control(GCRYCTL_DISABLE_SECMEM, 0);
|
||||
gcry_control(GCRYCTL_INITIALIZATION_FINISHED, 0);
|
||||
#endif
|
||||
|
||||
evbase = event_base_new();
|
||||
evcon = evrtsp_connection_new(address, atoi(port));
|
||||
evrtsp_connection_set_base(evcon, evbase);
|
||||
|
||||
if (pair_type == PAIR_CLIENT_HOMEKIT_TRANSIENT || skip_pin)
|
||||
{
|
||||
setup_start_response(NULL, NULL);
|
||||
}
|
||||
else
|
||||
{
|
||||
ret = setup_start_request();
|
||||
if (ret < 0)
|
||||
goto the_end;
|
||||
}
|
||||
|
||||
event_base_dispatch(evbase);
|
||||
|
||||
the_end:
|
||||
evrtsp_connection_free(evcon);
|
||||
event_base_free(evbase);
|
||||
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,176 @@
|
||||
/*
|
||||
* Copyright (C) 2010 Julien BLACHE <jb@jblache.org>
|
||||
* Based on evhttp from libevent 1.4.x
|
||||
*
|
||||
* Copyright (c) 2000-2004 Niels Provos <provos@citi.umich.edu>
|
||||
* All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions
|
||||
* are met:
|
||||
* 1. Redistributions of source code must retain the above copyright
|
||||
* notice, this list of conditions and the following disclaimer.
|
||||
* 2. Redistributions in binary form must reproduce the above copyright
|
||||
* notice, this list of conditions and the following disclaimer in the
|
||||
* documentation and/or other materials provided with the distribution.
|
||||
* 3. The name of the author may not be used to endorse or promote products
|
||||
* derived from this software without specific prior written permission.
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
|
||||
* IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
|
||||
* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
|
||||
* IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
|
||||
* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
|
||||
* NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
|
||||
* THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
#ifndef _EVRTSP_H_
|
||||
#define _EVRTSP_H_
|
||||
|
||||
#include <event2/event.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
#ifdef WIN32
|
||||
#define WIN32_LEAN_AND_MEAN
|
||||
#include <winsock2.h>
|
||||
#include <windows.h>
|
||||
#undef WIN32_LEAN_AND_MEAN
|
||||
#endif
|
||||
|
||||
/* Response codes */
|
||||
#define RTSP_OK 200
|
||||
#define RTSP_UNAUTHORIZED 401
|
||||
#define RTSP_FORBIDDEN 403
|
||||
|
||||
struct evrtsp_connection;
|
||||
|
||||
/*
|
||||
* Interfaces for making requests
|
||||
*/
|
||||
enum evrtsp_cmd_type {
|
||||
EVRTSP_REQ_ANNOUNCE,
|
||||
EVRTSP_REQ_OPTIONS,
|
||||
EVRTSP_REQ_SETUP,
|
||||
EVRTSP_REQ_RECORD,
|
||||
EVRTSP_REQ_PAUSE,
|
||||
EVRTSP_REQ_GET_PARAMETER, // Careful using this, some devices do not support it or will hang up eg RAOP_DEV_APEX1_80211G
|
||||
EVRTSP_REQ_SET_PARAMETER,
|
||||
EVRTSP_REQ_FLUSH,
|
||||
EVRTSP_REQ_TEARDOWN,
|
||||
EVRTSP_REQ_POST,
|
||||
};
|
||||
|
||||
enum evrtsp_request_kind { EVRTSP_REQUEST, EVRTSP_RESPONSE };
|
||||
|
||||
struct evrtsp_request {
|
||||
#if defined(TAILQ_ENTRY)
|
||||
TAILQ_ENTRY(evrtsp_request) next;
|
||||
#else
|
||||
struct {
|
||||
struct evrtsp_request *tqe_next;
|
||||
struct evrtsp_request **tqe_prev;
|
||||
} next;
|
||||
#endif
|
||||
|
||||
/* the connection object that this request belongs to */
|
||||
struct evrtsp_connection *evcon;
|
||||
int flags;
|
||||
#define EVRTSP_REQ_OWN_CONNECTION 0x0001
|
||||
|
||||
struct evkeyvalq *input_headers;
|
||||
struct evkeyvalq *output_headers;
|
||||
|
||||
enum evrtsp_request_kind kind;
|
||||
enum evrtsp_cmd_type type;
|
||||
|
||||
char *uri; /* uri after RTSP request was parsed */
|
||||
|
||||
char major; /* RTSP Major number */
|
||||
char minor; /* RTSP Minor number */
|
||||
|
||||
int response_code; /* RTSP Response code */
|
||||
char *response_code_line; /* Readable response */
|
||||
|
||||
struct evbuffer *input_buffer; /* read data */
|
||||
ev_int64_t ntoread;
|
||||
|
||||
struct evbuffer *output_buffer; /* outgoing post or data */
|
||||
|
||||
/* Callback */
|
||||
void (*cb)(struct evrtsp_request *, void *);
|
||||
void *cb_arg;
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates a new request object that needs to be filled in with the request
|
||||
* parameters. The callback is executed when the request completed or an
|
||||
* error occurred.
|
||||
*/
|
||||
struct evrtsp_request *evrtsp_request_new(
|
||||
void (*cb)(struct evrtsp_request *, void *), void *arg);
|
||||
|
||||
/** Frees the request object and removes associated events. */
|
||||
void evrtsp_request_free(struct evrtsp_request *req);
|
||||
|
||||
/**
|
||||
* A connection object that can be used to for making RTSP requests. The
|
||||
* connection object tries to establish the connection when it is given an
|
||||
* rtsp request object.
|
||||
*/
|
||||
struct evrtsp_connection *evrtsp_connection_new(
|
||||
const char *address, unsigned short port);
|
||||
|
||||
/** Frees an rtsp connection */
|
||||
void evrtsp_connection_free(struct evrtsp_connection *evcon);
|
||||
|
||||
/** Set a callback for connection close. */
|
||||
void evrtsp_connection_set_closecb(struct evrtsp_connection *evcon,
|
||||
void (*)(struct evrtsp_connection *, void *), void *);
|
||||
|
||||
/** Set a callback for encryption/decryption. */
|
||||
void evrtsp_connection_set_ciphercb(struct evrtsp_connection *evcon,
|
||||
void (*)(struct evbuffer *, void *, int encrypt), void *);
|
||||
|
||||
/**
|
||||
* Associates an event base with the connection - can only be called
|
||||
* on a freshly created connection object that has not been used yet.
|
||||
*/
|
||||
void evrtsp_connection_set_base(struct evrtsp_connection *evcon,
|
||||
struct event_base *base);
|
||||
|
||||
/** Get the remote address and port associated with this connection. */
|
||||
void evrtsp_connection_get_peer(struct evrtsp_connection *evcon,
|
||||
char **address, u_short *port);
|
||||
|
||||
/** Get the local address, port and family associated with this connection. */
|
||||
void
|
||||
evrtsp_connection_get_local_address(struct evrtsp_connection *evcon,
|
||||
char **address, u_short *port, int *family);
|
||||
|
||||
/** The connection gets ownership of the request */
|
||||
int evrtsp_make_request(struct evrtsp_connection *evcon,
|
||||
struct evrtsp_request *req,
|
||||
enum evrtsp_cmd_type type, const char *uri);
|
||||
|
||||
const char *evrtsp_request_uri(struct evrtsp_request *req);
|
||||
|
||||
/* Interfaces for dealing with headers */
|
||||
|
||||
const char *evrtsp_find_header(const struct evkeyvalq *, const char *);
|
||||
int evrtsp_remove_header(struct evkeyvalq *, const char *);
|
||||
int evrtsp_add_header(struct evkeyvalq *, const char *, const char *);
|
||||
void evrtsp_clear_headers(struct evkeyvalq *);
|
||||
|
||||
const char *evrtsp_method(enum evrtsp_cmd_type type);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* !_EVRTSP_H_ */
|
||||
@@ -0,0 +1,51 @@
|
||||
/*
|
||||
* Copyright (c) 2000-2004 Niels Provos <provos@citi.umich.edu>
|
||||
* All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions
|
||||
* are met:
|
||||
* 1. Redistributions of source code must retain the above copyright
|
||||
* notice, this list of conditions and the following disclaimer.
|
||||
* 2. Redistributions in binary form must reproduce the above copyright
|
||||
* notice, this list of conditions and the following disclaimer in the
|
||||
* documentation and/or other materials provided with the distribution.
|
||||
* 3. The name of the author may not be used to endorse or promote products
|
||||
* derived from this software without specific prior written permission.
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
|
||||
* IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
|
||||
* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
|
||||
* IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
|
||||
* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
|
||||
* NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
|
||||
* THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
#ifndef _LOG_H_
|
||||
#define _LOG_H_
|
||||
|
||||
#ifdef __GNUC__
|
||||
#define EV_CHECK_FMT(a,b) __attribute__((format(printf, a, b)))
|
||||
#else
|
||||
#define EV_CHECK_FMT(a,b)
|
||||
#endif
|
||||
|
||||
void event_err(int eval, const char *fmt, ...) EV_CHECK_FMT(2,3);
|
||||
void event_warn(const char *fmt, ...) EV_CHECK_FMT(1,2);
|
||||
void event_errx(int eval, const char *fmt, ...) EV_CHECK_FMT(2,3);
|
||||
void event_warnx(const char *fmt, ...) EV_CHECK_FMT(1,2);
|
||||
void event_msgx(const char *fmt, ...) EV_CHECK_FMT(1,2);
|
||||
void _event_debugx(const char *fmt, ...) EV_CHECK_FMT(1,2);
|
||||
|
||||
#ifdef USE_DEBUG
|
||||
#define event_debug(x) _event_debugx x
|
||||
#else
|
||||
#define event_debug(x) do {;} while (0)
|
||||
#endif
|
||||
|
||||
#undef EV_CHECK_FMT
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,115 @@
|
||||
/*
|
||||
* Copyright (C) 2010 Julien BLACHE <jb@jblache.org>
|
||||
* Based on evhttp from libevent 1.4.x
|
||||
*
|
||||
* Copyright 2001 Niels Provos <provos@citi.umich.edu>
|
||||
* All rights reserved.
|
||||
*
|
||||
* This header file contains definitions for dealing with RTSP requests
|
||||
* that are internal to libevent. As user of the library, you should not
|
||||
* need to know about these.
|
||||
*/
|
||||
|
||||
#ifndef _RTSP_H_
|
||||
#define _RTSP_H_
|
||||
|
||||
#include <event2/buffer.h>
|
||||
#include <event2/event_struct.h>
|
||||
|
||||
#define RTSP_CONNECT_TIMEOUT 5
|
||||
#define RTSP_WRITE_TIMEOUT 30
|
||||
#define RTSP_READ_TIMEOUT 30
|
||||
|
||||
#define RTSP_PREFIX "rtsp://"
|
||||
|
||||
enum message_read_status {
|
||||
ALL_DATA_READ = 1,
|
||||
MORE_DATA_EXPECTED = 0,
|
||||
DATA_CORRUPTED = -1,
|
||||
REQUEST_CANCELED = -2
|
||||
};
|
||||
|
||||
enum evrtsp_connection_error {
|
||||
EVCON_RTSP_TIMEOUT,
|
||||
EVCON_RTSP_EOF,
|
||||
EVCON_RTSP_INVALID_HEADER
|
||||
};
|
||||
|
||||
struct evbuffer;
|
||||
struct addrinfo;
|
||||
struct evrtsp_request;
|
||||
|
||||
/* A stupid connection object - maybe make this a bufferevent later */
|
||||
|
||||
enum evrtsp_connection_state {
|
||||
EVCON_DISCONNECTED, /**< not currently connected not trying either*/
|
||||
EVCON_CONNECTING, /**< tries to currently connect */
|
||||
EVCON_IDLE, /**< connection is established */
|
||||
EVCON_READING_FIRSTLINE,/**< reading Request-Line (incoming conn) or
|
||||
**< Status-Line (outgoing conn) */
|
||||
EVCON_READING_HEADERS, /**< reading request/response headers */
|
||||
EVCON_READING_BODY, /**< reading request/response body */
|
||||
EVCON_READING_TRAILER, /**< reading request/response chunked trailer */
|
||||
EVCON_WRITING /**< writing request/response headers/body */
|
||||
};
|
||||
|
||||
struct event_base;
|
||||
|
||||
struct evrtsp_connection {
|
||||
int fd;
|
||||
struct event ev;
|
||||
struct event close_ev;
|
||||
struct evbuffer *input_buffer;
|
||||
struct evbuffer *output_buffer;
|
||||
|
||||
char *bind_address; /* address to use for binding the src */
|
||||
u_short bind_port; /* local port for binding the src */
|
||||
|
||||
char *address; /* address to connect to */
|
||||
int family;
|
||||
u_short port;
|
||||
|
||||
int flags;
|
||||
#define EVRTSP_CON_CLOSEDETECT 0x0004 /* detecting if persistent close */
|
||||
|
||||
int timeout; /* timeout in seconds for events */
|
||||
|
||||
enum evrtsp_connection_state state;
|
||||
int cseq;
|
||||
|
||||
TAILQ_HEAD(evcon_requestq, evrtsp_request) requests;
|
||||
|
||||
void (*cb)(struct evrtsp_connection *, void *);
|
||||
void *cb_arg;
|
||||
|
||||
void (*closecb)(struct evrtsp_connection *, void *);
|
||||
void *closecb_arg;
|
||||
|
||||
void (*ciphercb)(struct evbuffer *evbuf, void *, int encrypt);
|
||||
void *ciphercb_arg;
|
||||
|
||||
struct event_base *base;
|
||||
};
|
||||
|
||||
/* resets the connection; can be reused for more requests */
|
||||
void evrtsp_connection_reset(struct evrtsp_connection *);
|
||||
|
||||
/* connects if necessary */
|
||||
int evrtsp_connection_connect(struct evrtsp_connection *);
|
||||
|
||||
/* notifies the current request that it failed; resets connection */
|
||||
void evrtsp_connection_fail(struct evrtsp_connection *,
|
||||
enum evrtsp_connection_error error);
|
||||
|
||||
int evrtsp_hostportfile(char *, char **, u_short *, char **);
|
||||
|
||||
int evrtsp_parse_firstline(struct evrtsp_request *, struct evbuffer*);
|
||||
int evrtsp_parse_headers(struct evrtsp_request *, struct evbuffer*);
|
||||
|
||||
void evrtsp_start_read(struct evrtsp_connection *);
|
||||
void evrtsp_make_header(struct evrtsp_connection *, struct evrtsp_request *);
|
||||
|
||||
void evrtsp_write_buffer(struct evrtsp_connection *,
|
||||
void (*)(struct evrtsp_connection *, void *), void *);
|
||||
|
||||
#endif /* _RTSP_H */
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,390 @@
|
||||
#include <stdint.h>
|
||||
#include <stdbool.h>
|
||||
#include <sodium.h>
|
||||
|
||||
#include "pair.h"
|
||||
|
||||
#define RETURN_ERROR(s, m) \
|
||||
do { handle->status = (s); handle->errmsg = (m); goto error; } while(0)
|
||||
|
||||
|
||||
struct SRPUser;
|
||||
struct SRPVerifier;
|
||||
|
||||
struct pair_client_setup_context
|
||||
{
|
||||
struct SRPUser *user;
|
||||
|
||||
uint8_t pin[4];
|
||||
char device_id[PAIR_AP_DEVICE_ID_LEN_MAX];
|
||||
|
||||
pair_cb add_cb;
|
||||
void *add_cb_arg;
|
||||
|
||||
uint8_t public_key[crypto_sign_PUBLICKEYBYTES];
|
||||
uint8_t private_key[crypto_sign_SECRETKEYBYTES];
|
||||
|
||||
const uint8_t *pkA;
|
||||
int pkA_len;
|
||||
|
||||
uint8_t *pkB;
|
||||
uint64_t pkB_len;
|
||||
|
||||
const uint8_t *M1;
|
||||
int M1_len;
|
||||
|
||||
uint8_t *M2;
|
||||
uint64_t M2_len;
|
||||
|
||||
uint8_t *salt;
|
||||
uint64_t salt_len;
|
||||
|
||||
// We don't actually use the server's epk and authtag for anything
|
||||
uint8_t *epk;
|
||||
uint64_t epk_len;
|
||||
uint8_t *authtag;
|
||||
uint64_t authtag_len;
|
||||
};
|
||||
|
||||
struct pair_server_setup_context
|
||||
{
|
||||
struct SRPVerifier *verifier;
|
||||
|
||||
uint8_t pin[4];
|
||||
char device_id[PAIR_AP_DEVICE_ID_LEN_MAX];
|
||||
|
||||
pair_cb add_cb;
|
||||
void *add_cb_arg;
|
||||
|
||||
uint8_t public_key[crypto_sign_PUBLICKEYBYTES];
|
||||
uint8_t private_key[crypto_sign_SECRETKEYBYTES];
|
||||
|
||||
bool is_transient;
|
||||
|
||||
uint8_t *pkA;
|
||||
uint64_t pkA_len;
|
||||
|
||||
uint8_t *pkB;
|
||||
int pkB_len;
|
||||
|
||||
uint8_t *b;
|
||||
int b_len;
|
||||
|
||||
uint8_t *M1;
|
||||
uint64_t M1_len;
|
||||
|
||||
const uint8_t *M2;
|
||||
int M2_len;
|
||||
|
||||
uint8_t *v;
|
||||
int v_len;
|
||||
|
||||
uint8_t *salt;
|
||||
int salt_len;
|
||||
};
|
||||
|
||||
enum pair_status
|
||||
{
|
||||
PAIR_STATUS_IN_PROGRESS,
|
||||
PAIR_STATUS_COMPLETED,
|
||||
PAIR_STATUS_AUTH_FAILED,
|
||||
PAIR_STATUS_INVALID,
|
||||
};
|
||||
|
||||
struct pair_setup_context
|
||||
{
|
||||
struct pair_definition *type;
|
||||
|
||||
enum pair_status status;
|
||||
const char *errmsg;
|
||||
|
||||
struct pair_result result;
|
||||
char result_str[256]; // Holds the hex string version of the keys that pair_verify_new() needs
|
||||
|
||||
// Hex-formatet concatenation of public + private, 0-terminated
|
||||
char auth_key[2 * (crypto_sign_PUBLICKEYBYTES + crypto_sign_SECRETKEYBYTES) + 1];
|
||||
|
||||
union pair_setup_union
|
||||
{
|
||||
struct pair_client_setup_context client;
|
||||
struct pair_server_setup_context server;
|
||||
} sctx;
|
||||
};
|
||||
|
||||
struct pair_client_verify_context
|
||||
{
|
||||
char device_id[PAIR_AP_DEVICE_ID_LEN_MAX];
|
||||
|
||||
// These are the keys that were registered with the server in pair-setup
|
||||
uint8_t client_public_key[crypto_sign_PUBLICKEYBYTES]; // 32
|
||||
uint8_t client_private_key[crypto_sign_SECRETKEYBYTES]; // 64
|
||||
|
||||
bool verify_server_signature;
|
||||
uint8_t server_fruit_public_key[64]; // Not sure why it has this length in fruit mode
|
||||
uint8_t server_public_key[crypto_sign_PUBLICKEYBYTES]; // 32
|
||||
|
||||
// For establishing the shared secret for encrypted communication
|
||||
uint8_t client_eph_public_key[crypto_box_PUBLICKEYBYTES]; // 32
|
||||
uint8_t client_eph_private_key[crypto_box_SECRETKEYBYTES]; // 32
|
||||
|
||||
uint8_t server_eph_public_key[crypto_box_PUBLICKEYBYTES]; // 32
|
||||
|
||||
uint8_t shared_secret[crypto_scalarmult_BYTES]; // 32
|
||||
};
|
||||
|
||||
struct pair_server_verify_context
|
||||
{
|
||||
char device_id[PAIR_AP_DEVICE_ID_LEN_MAX];
|
||||
|
||||
// Same keys as used for pair-setup, derived from device_id
|
||||
uint8_t server_public_key[crypto_sign_PUBLICKEYBYTES]; // 32
|
||||
uint8_t server_private_key[crypto_sign_SECRETKEYBYTES]; // 64
|
||||
|
||||
bool verify_client_signature;
|
||||
pair_cb get_cb;
|
||||
void *get_cb_arg;
|
||||
|
||||
// For establishing the shared secret for encrypted communication
|
||||
uint8_t server_eph_public_key[crypto_box_PUBLICKEYBYTES]; // 32
|
||||
uint8_t server_eph_private_key[crypto_box_SECRETKEYBYTES]; // 32
|
||||
|
||||
uint8_t client_eph_public_key[crypto_box_PUBLICKEYBYTES]; // 32
|
||||
|
||||
uint8_t shared_secret[crypto_scalarmult_BYTES]; // 32
|
||||
};
|
||||
|
||||
struct pair_verify_context
|
||||
{
|
||||
struct pair_definition *type;
|
||||
|
||||
enum pair_status status;
|
||||
const char *errmsg;
|
||||
|
||||
struct pair_result result;
|
||||
|
||||
union pair_verify_union
|
||||
{
|
||||
struct pair_client_verify_context client;
|
||||
struct pair_server_verify_context server;
|
||||
} vctx;
|
||||
};
|
||||
|
||||
struct pair_cipher_context
|
||||
{
|
||||
struct pair_definition *type;
|
||||
|
||||
uint8_t encryption_key[32];
|
||||
uint8_t decryption_key[32];
|
||||
|
||||
uint64_t encryption_counter;
|
||||
uint64_t decryption_counter;
|
||||
|
||||
// For rollback
|
||||
uint64_t encryption_counter_prev;
|
||||
uint64_t decryption_counter_prev;
|
||||
|
||||
const char *errmsg;
|
||||
};
|
||||
|
||||
struct pair_definition
|
||||
{
|
||||
int (*pair_setup_new)(struct pair_setup_context *sctx, const char *pin, pair_cb add_cb, void *cb_arg, const char *device_id);
|
||||
void (*pair_setup_free)(struct pair_setup_context *sctx);
|
||||
int (*pair_setup_result)(struct pair_setup_context *sctx);
|
||||
|
||||
uint8_t *(*pair_setup_request1)(size_t *len, struct pair_setup_context *sctx);
|
||||
uint8_t *(*pair_setup_request2)(size_t *len, struct pair_setup_context *sctx);
|
||||
uint8_t *(*pair_setup_request3)(size_t *len, struct pair_setup_context *sctx);
|
||||
|
||||
int (*pair_setup_response1)(struct pair_setup_context *sctx, const uint8_t *in, size_t in_len);
|
||||
int (*pair_setup_response2)(struct pair_setup_context *sctx, const uint8_t *in, size_t in_len);
|
||||
int (*pair_setup_response3)(struct pair_setup_context *sctx, const uint8_t *in, size_t in_len);
|
||||
|
||||
int (*pair_verify_new)(struct pair_verify_context *vctx, const char *client_setup_keys, pair_cb cb, void *cb_arg, const char *device_id);
|
||||
void (*pair_verify_free)(struct pair_verify_context *vctx);
|
||||
int (*pair_verify_result)(struct pair_verify_context *vctx);
|
||||
|
||||
uint8_t *(*pair_verify_request1)(size_t *len, struct pair_verify_context *vctx);
|
||||
uint8_t *(*pair_verify_request2)(size_t *len, struct pair_verify_context *vctx);
|
||||
|
||||
int (*pair_verify_response1)(struct pair_verify_context *vctx, const uint8_t *in, size_t in_len);
|
||||
int (*pair_verify_response2)(struct pair_verify_context *vctx, const uint8_t *in, size_t in_len);
|
||||
|
||||
int (*pair_add)(uint8_t **out, size_t *out_len, pair_cb cb, void *cb_arg, const uint8_t *in, size_t in_len);
|
||||
int (*pair_remove)(uint8_t **out, size_t *out_len, pair_cb cb, void *cb_arg, const uint8_t *in, size_t in_len);
|
||||
int (*pair_list)(uint8_t **out, size_t *out_len, pair_list_cb cb, void *cb_arg, const uint8_t *in, size_t in_len);
|
||||
|
||||
struct pair_cipher_context *(*pair_cipher_new)(struct pair_definition *type, int channel, const uint8_t *shared_secret, size_t shared_secret_len);
|
||||
void (*pair_cipher_free)(struct pair_cipher_context *cctx);
|
||||
|
||||
ssize_t (*pair_encrypt)(uint8_t **ciphertext, size_t *ciphertext_len, const uint8_t *plaintext, size_t plaintext_len, struct pair_cipher_context *cctx);
|
||||
ssize_t (*pair_decrypt)(uint8_t **plaintext, size_t *plaintext_len, const uint8_t *ciphertext, size_t ciphertext_len, struct pair_cipher_context *cctx);
|
||||
|
||||
int (*pair_state_get)(const char **errmsg, const uint8_t *in, size_t in_len);
|
||||
void (*pair_public_key_get)(uint8_t server_public_key[32], const char *device_id);
|
||||
};
|
||||
|
||||
|
||||
/* ----------------------------- INITIALIZATION ---------------------------- */
|
||||
|
||||
bool
|
||||
is_initialized(void);
|
||||
|
||||
|
||||
/* -------------------- GCRYPT AND OPENSSL COMPABILITY --------------------- */
|
||||
/* partly borrowed from ffmpeg (rtmpdh.c) */
|
||||
|
||||
#if CONFIG_GCRYPT
|
||||
#include <gcrypt.h>
|
||||
#define SHA512_DIGEST_LENGTH 64
|
||||
#define bnum_new(bn) \
|
||||
do { \
|
||||
if (!gcry_control(GCRYCTL_INITIALIZATION_FINISHED_P)) { \
|
||||
if (!gcry_check_version("1.5.4")) \
|
||||
abort(); \
|
||||
gcry_control(GCRYCTL_DISABLE_SECMEM, 0); \
|
||||
gcry_control(GCRYCTL_INITIALIZATION_FINISHED, 0); \
|
||||
} \
|
||||
bn = gcry_mpi_new(1); \
|
||||
} while (0)
|
||||
#define bnum_free(bn) gcry_mpi_release(bn)
|
||||
#define bnum_num_bytes(bn) (gcry_mpi_get_nbits(bn) + 7) / 8
|
||||
#define bnum_is_zero(bn) (gcry_mpi_cmp_ui(bn, (unsigned long)0) == 0)
|
||||
#define bnum_bn2bin(bn, buf, len) gcry_mpi_print(GCRYMPI_FMT_USG, buf, len, NULL, bn)
|
||||
#define bnum_bin2bn(bn, buf, len) gcry_mpi_scan(&bn, GCRYMPI_FMT_USG, buf, len, NULL)
|
||||
#define bnum_hex2bn(bn, buf) gcry_mpi_scan(&bn, GCRYMPI_FMT_HEX, buf, 0, 0)
|
||||
#define bnum_random(bn, num_bits) gcry_mpi_randomize(bn, num_bits, GCRY_WEAK_RANDOM)
|
||||
#define bnum_add(bn, a, b) gcry_mpi_add(bn, a, b)
|
||||
#define bnum_sub(bn, a, b) gcry_mpi_sub(bn, a, b)
|
||||
#define bnum_mul(bn, a, b) gcry_mpi_mul(bn, a, b)
|
||||
#define bnum_mod(bn, a, b) gcry_mpi_mod(bn, a, b)
|
||||
typedef gcry_mpi_t bnum;
|
||||
__attribute__((unused)) static void bnum_modexp(bnum bn, bnum y, bnum q, bnum p)
|
||||
{
|
||||
gcry_mpi_powm(bn, y, q, p);
|
||||
}
|
||||
__attribute__((unused)) static void bnum_modadd(bnum bn, bnum a, bnum b, bnum m)
|
||||
{
|
||||
gcry_mpi_addm(bn, a, b, m);
|
||||
}
|
||||
#elif CONFIG_OPENSSL
|
||||
#include <openssl/crypto.h>
|
||||
#include <openssl/bn.h>
|
||||
#include <openssl/rand.h>
|
||||
#include <openssl/sha.h>
|
||||
#include <openssl/evp.h>
|
||||
#define bnum_new(bn) bn = BN_new()
|
||||
#define bnum_free(bn) BN_free(bn)
|
||||
#define bnum_num_bytes(bn) BN_num_bytes(bn)
|
||||
#define bnum_is_zero(bn) BN_is_zero(bn)
|
||||
#define bnum_bn2bin(bn, buf, len) BN_bn2bin(bn, buf)
|
||||
#define bnum_bin2bn(bn, buf, len) bn = BN_bin2bn(buf, len, 0)
|
||||
#define bnum_hex2bn(bn, buf) BN_hex2bn(&bn, buf)
|
||||
#define bnum_random(bn, num_bits) BN_rand(bn, num_bits, 0, 0)
|
||||
#define bnum_add(bn, a, b) BN_add(bn, a, b)
|
||||
#define bnum_sub(bn, a, b) BN_sub(bn, a, b)
|
||||
typedef BIGNUM* bnum;
|
||||
__attribute__((unused)) static void bnum_mul(bnum bn, bnum a, bnum b)
|
||||
{
|
||||
// No error handling
|
||||
BN_CTX *ctx = BN_CTX_new();
|
||||
BN_mul(bn, a, b, ctx);
|
||||
BN_CTX_free(ctx);
|
||||
}
|
||||
__attribute__((unused)) static void bnum_mod(bnum bn, bnum a, bnum b)
|
||||
{
|
||||
// No error handling
|
||||
BN_CTX *ctx = BN_CTX_new();
|
||||
BN_mod(bn, a, b, ctx);
|
||||
BN_CTX_free(ctx);
|
||||
}
|
||||
__attribute__((unused)) static void bnum_modexp(bnum bn, bnum y, bnum q, bnum p)
|
||||
{
|
||||
// No error handling
|
||||
BN_CTX *ctx = BN_CTX_new();
|
||||
BN_mod_exp(bn, y, q, p, ctx);
|
||||
BN_CTX_free(ctx);
|
||||
}
|
||||
__attribute__((unused)) static void bnum_modadd(bnum bn, bnum a, bnum b, bnum m)
|
||||
{
|
||||
// No error handling
|
||||
BN_CTX *ctx = BN_CTX_new();
|
||||
BN_mod_add(bn, a, b, m, ctx);
|
||||
BN_CTX_free(ctx);
|
||||
}
|
||||
#endif
|
||||
|
||||
|
||||
/* -------------------------- SHARED HASHING HELPERS ------------------------ */
|
||||
|
||||
#ifdef CONFIG_OPENSSL
|
||||
enum hash_alg
|
||||
{
|
||||
HASH_SHA1,
|
||||
HASH_SHA224,
|
||||
HASH_SHA256,
|
||||
HASH_SHA384,
|
||||
HASH_SHA512,
|
||||
};
|
||||
#elif CONFIG_GCRYPT
|
||||
enum hash_alg
|
||||
{
|
||||
HASH_SHA1 = GCRY_MD_SHA1,
|
||||
HASH_SHA224 = GCRY_MD_SHA224,
|
||||
HASH_SHA256 = GCRY_MD_SHA256,
|
||||
HASH_SHA384 = GCRY_MD_SHA384,
|
||||
HASH_SHA512 = GCRY_MD_SHA512,
|
||||
};
|
||||
#endif
|
||||
|
||||
#if CONFIG_OPENSSL
|
||||
typedef union
|
||||
{
|
||||
SHA_CTX sha;
|
||||
SHA256_CTX sha256;
|
||||
SHA512_CTX sha512;
|
||||
} HashCTX;
|
||||
#elif CONFIG_GCRYPT
|
||||
typedef gcry_md_hd_t HashCTX;
|
||||
#endif
|
||||
|
||||
int
|
||||
hash_init(enum hash_alg alg, HashCTX *c);
|
||||
|
||||
int
|
||||
hash_update(enum hash_alg alg, HashCTX *c, const void *data, size_t len);
|
||||
|
||||
int
|
||||
hash_final(enum hash_alg alg, HashCTX *c, unsigned char *md);
|
||||
|
||||
unsigned char *
|
||||
hash(enum hash_alg alg, const unsigned char *d, size_t n, unsigned char *md);
|
||||
|
||||
int
|
||||
hash_length(enum hash_alg alg);
|
||||
|
||||
int
|
||||
hash_ab(enum hash_alg alg, unsigned char *md, const unsigned char *m1, int m1_len, const unsigned char *m2, int m2_len);
|
||||
|
||||
bnum
|
||||
H_nn_pad(enum hash_alg alg, const bnum n1, const bnum n2, int padded_len);
|
||||
|
||||
bnum
|
||||
H_ns(enum hash_alg alg, const bnum n, const unsigned char *bytes, int len_bytes);
|
||||
|
||||
void
|
||||
update_hash_n(enum hash_alg alg, HashCTX *ctx, const bnum n);
|
||||
|
||||
void
|
||||
hash_num(enum hash_alg alg, const bnum n, unsigned char *dest);
|
||||
|
||||
|
||||
/* ----------------------------- OTHER HELPERS -------------------------------*/
|
||||
|
||||
#ifdef DEBUG_PAIR
|
||||
void
|
||||
hexdump(const char *msg, uint8_t *mem, size_t len);
|
||||
|
||||
void
|
||||
bnum_dump(const char *msg, bnum n);
|
||||
#endif
|
||||
@@ -0,0 +1,218 @@
|
||||
/*
|
||||
* TLV helpers are adapted from ESP homekit:
|
||||
* <https://github.com/maximkulkin/esp-homekit>
|
||||
*
|
||||
* The MIT License (MIT)
|
||||
*
|
||||
* 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 <inttypes.h>
|
||||
|
||||
#include "pair-tlv.h"
|
||||
#include "pair-internal.h"
|
||||
|
||||
|
||||
static int
|
||||
tlv_add_value_(pair_tlv_values_t *values, uint8_t type, uint8_t *value, size_t size) {
|
||||
pair_tlv_t *tlv = malloc(sizeof(pair_tlv_t));
|
||||
if (!tlv) {
|
||||
return PAIR_TLV_ERROR_MEMORY;
|
||||
}
|
||||
tlv->type = type;
|
||||
tlv->size = size;
|
||||
tlv->value = value;
|
||||
tlv->next = NULL;
|
||||
|
||||
if (!values->head) {
|
||||
values->head = tlv;
|
||||
} else {
|
||||
pair_tlv_t *t = values->head;
|
||||
while (t->next) {
|
||||
t = t->next;
|
||||
}
|
||||
t->next = tlv;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
pair_tlv_values_t *
|
||||
pair_tlv_new() {
|
||||
pair_tlv_values_t *values = malloc(sizeof(pair_tlv_values_t));
|
||||
if (!values)
|
||||
return NULL;
|
||||
|
||||
values->head = NULL;
|
||||
return values;
|
||||
}
|
||||
|
||||
void
|
||||
pair_tlv_free(pair_tlv_values_t *values) {
|
||||
pair_tlv_t *t = values->head;
|
||||
while (t) {
|
||||
pair_tlv_t *t2 = t;
|
||||
t = t->next;
|
||||
if (t2->value)
|
||||
free(t2->value);
|
||||
free(t2);
|
||||
}
|
||||
free(values);
|
||||
}
|
||||
|
||||
int
|
||||
pair_tlv_add_value(pair_tlv_values_t *values, uint8_t type, const uint8_t *value, size_t size) {
|
||||
uint8_t *data = NULL;
|
||||
int ret;
|
||||
if (size) {
|
||||
data = malloc(size);
|
||||
if (!data) {
|
||||
return PAIR_TLV_ERROR_MEMORY;
|
||||
}
|
||||
memcpy(data, value, size);
|
||||
}
|
||||
ret = tlv_add_value_(values, type, data, size);
|
||||
if (ret < 0)
|
||||
free(data);
|
||||
return ret;
|
||||
}
|
||||
|
||||
pair_tlv_t *
|
||||
pair_tlv_get_value(const pair_tlv_values_t *values, uint8_t type) {
|
||||
pair_tlv_t *t = values->head;
|
||||
while (t) {
|
||||
if (t->type == type)
|
||||
return t;
|
||||
t = t->next;
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
int
|
||||
pair_tlv_format(const pair_tlv_values_t *values, uint8_t *buffer, size_t *size) {
|
||||
size_t required_size = 0;
|
||||
pair_tlv_t *t = values->head;
|
||||
while (t) {
|
||||
required_size += t->size + 2 * ((t->size + 254) / 255);
|
||||
t = t->next;
|
||||
}
|
||||
|
||||
if (*size < required_size) {
|
||||
*size = required_size;
|
||||
return PAIR_TLV_ERROR_INSUFFICIENT_SIZE;
|
||||
}
|
||||
|
||||
*size = required_size;
|
||||
|
||||
t = values->head;
|
||||
while (t) {
|
||||
uint8_t *data = t->value;
|
||||
if (!t->size) {
|
||||
buffer[0] = t->type;
|
||||
buffer[1] = 0;
|
||||
buffer += 2;
|
||||
t = t->next;
|
||||
continue;
|
||||
}
|
||||
|
||||
size_t remaining = t->size;
|
||||
|
||||
while (remaining) {
|
||||
buffer[0] = t->type;
|
||||
size_t chunk_size = (remaining > 255) ? 255 : remaining;
|
||||
buffer[1] = chunk_size;
|
||||
memcpy(&buffer[2], data, chunk_size);
|
||||
remaining -= chunk_size;
|
||||
buffer += chunk_size + 2;
|
||||
data += chunk_size;
|
||||
}
|
||||
|
||||
t = t->next;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
int
|
||||
pair_tlv_parse(const uint8_t *buffer, size_t length, pair_tlv_values_t *values) {
|
||||
size_t i = 0;
|
||||
int ret;
|
||||
while (i < length) {
|
||||
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) {
|
||||
size_t chunk_size = buffer[j+1];
|
||||
size += chunk_size;
|
||||
j += chunk_size + 2;
|
||||
}
|
||||
if (j < length && buffer[j] == type) {
|
||||
size_t chunk_size = buffer[j+1];
|
||||
size += chunk_size;
|
||||
}
|
||||
|
||||
// allocate memory to hold all pieces of chunked data and copy data there
|
||||
if (size != 0) {
|
||||
data = malloc(size);
|
||||
if (!data)
|
||||
return PAIR_TLV_ERROR_MEMORY;
|
||||
|
||||
uint8_t *p = data;
|
||||
|
||||
size_t remaining = size;
|
||||
while (remaining) {
|
||||
size_t chunk_size = buffer[i+1];
|
||||
memcpy(p, &buffer[i+2], chunk_size);
|
||||
p += chunk_size;
|
||||
i += chunk_size + 2;
|
||||
remaining -= chunk_size;
|
||||
}
|
||||
}
|
||||
|
||||
ret = tlv_add_value_(values, type, data, size);
|
||||
if (ret < 0) {
|
||||
free(data);
|
||||
return ret;
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
#ifdef DEBUG_PAIR
|
||||
void
|
||||
pair_tlv_debug(const pair_tlv_values_t *values)
|
||||
{
|
||||
printf("Received TLV values\n");
|
||||
for (pair_tlv_t *t=values->head; t; t=t->next)
|
||||
{
|
||||
printf("Type %d value (%zu bytes): \n", t->type, t->size);
|
||||
hexdump("", t->value, t->size);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
#ifndef __PAIR_AP_TLV_H__
|
||||
#define __PAIR_AP_TLV_H__
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
#define PAIR_TLV_ERROR_MEMORY -1
|
||||
#define PAIR_TLV_ERROR_INSUFFICIENT_SIZE -2
|
||||
|
||||
typedef enum {
|
||||
TLVType_Method = 0, // (integer) Method to use for pairing. See PairMethod
|
||||
TLVType_Identifier = 1, // (UTF-8) Identifier for authentication
|
||||
TLVType_Salt = 2, // (bytes) 16+ bytes of random salt
|
||||
TLVType_PublicKey = 3, // (bytes) Curve25519, SRP public key or signed Ed25519 key
|
||||
TLVType_Proof = 4, // (bytes) Ed25519 or SRP proof
|
||||
TLVType_EncryptedData = 5, // (bytes) Encrypted data with auth tag at end
|
||||
TLVType_State = 6, // (integer) State of the pairing process. 1=M1, 2=M2, etc.
|
||||
TLVType_Error = 7, // (integer) Error code. Must only be present if error code is
|
||||
// not 0. See TLVError
|
||||
TLVType_RetryDelay = 8, // (integer) Seconds to delay until retrying a setup code
|
||||
TLVType_Certificate = 9, // (bytes) X.509 Certificate
|
||||
TLVType_Signature = 10, // (bytes) Ed25519
|
||||
TLVType_Permissions = 11, // (integer) Bit value describing permissions of the controller
|
||||
// being added.
|
||||
// None (0x00): Regular user
|
||||
// Bit 1 (0x01): Admin that is able to add and remove
|
||||
// pairings against the accessory
|
||||
TLVType_FragmentData = 13, // (bytes) Non-last fragment of data. If length is 0,
|
||||
// it's an ACK.
|
||||
TLVType_FragmentLast = 14, // (bytes) Last fragment of data
|
||||
TLVType_Flags = 19, // Added from airplay2_receiver
|
||||
TLVType_Separator = 0xff,
|
||||
} TLVType;
|
||||
|
||||
|
||||
typedef enum {
|
||||
TLVError_Unknown = 1, // Generic error to handle unexpected errors
|
||||
TLVError_Authentication = 2, // Setup code or signature verification failed
|
||||
TLVError_Backoff = 3, // Client must look at the retry delay TLV item and
|
||||
// wait that many seconds before retrying
|
||||
TLVError_MaxPeers = 4, // Server cannot accept any more pairings
|
||||
TLVError_MaxTries = 5, // Server reached its maximum number of
|
||||
// authentication attempts
|
||||
TLVError_Unavailable = 6, // Server pairing method is unavailable
|
||||
TLVError_Busy = 7, // Server is busy and cannot accept a pairing
|
||||
// request at this time
|
||||
} TLVError;
|
||||
|
||||
typedef struct _tlv {
|
||||
struct _tlv *next;
|
||||
uint8_t type;
|
||||
uint8_t *value;
|
||||
size_t size;
|
||||
} pair_tlv_t;
|
||||
|
||||
|
||||
typedef struct {
|
||||
pair_tlv_t *head;
|
||||
} pair_tlv_values_t;
|
||||
|
||||
|
||||
pair_tlv_values_t *pair_tlv_new();
|
||||
|
||||
void pair_tlv_free(pair_tlv_values_t *values);
|
||||
|
||||
int pair_tlv_add_value(pair_tlv_values_t *values, uint8_t type, const uint8_t *value, size_t size);
|
||||
|
||||
pair_tlv_t *pair_tlv_get_value(const pair_tlv_values_t *values, uint8_t type);
|
||||
|
||||
int pair_tlv_format(const pair_tlv_values_t *values, uint8_t *buffer, size_t *size);
|
||||
|
||||
int pair_tlv_parse(const uint8_t *buffer, size_t length, pair_tlv_values_t *values);
|
||||
|
||||
#ifdef DEBUG_PAIR
|
||||
void pair_tlv_debug(const pair_tlv_values_t *values);
|
||||
#endif
|
||||
|
||||
#endif /* !__PAIR_AP_TLV_H__ */
|
||||
+806
@@ -0,0 +1,806 @@
|
||||
/*
|
||||
* The MIT License (MIT)
|
||||
*
|
||||
* 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 <ctype.h> // for isprint()
|
||||
#include <assert.h>
|
||||
|
||||
#include <sodium.h>
|
||||
|
||||
#include "pair.h"
|
||||
#include "pair-internal.h"
|
||||
|
||||
extern struct pair_definition pair_client_fruit;
|
||||
extern struct pair_definition pair_client_homekit_normal;
|
||||
extern struct pair_definition pair_client_homekit_transient;
|
||||
extern struct pair_definition pair_server_homekit;
|
||||
|
||||
// Must be in sync with enum pair_type
|
||||
static struct pair_definition *pair[] = {
|
||||
&pair_client_fruit,
|
||||
&pair_client_homekit_normal,
|
||||
&pair_client_homekit_transient,
|
||||
&pair_server_homekit,
|
||||
};
|
||||
|
||||
/* ------------------------------ INITIALIZATION ---------------------------- */
|
||||
|
||||
bool
|
||||
is_initialized(void)
|
||||
{
|
||||
if (sodium_init() == -1)
|
||||
return false;
|
||||
|
||||
#if CONFIG_GCRYPT
|
||||
// According to libgcrypt documentation: "It is important that these
|
||||
// initialization steps are not done by a library but by the actual
|
||||
// application. A library using Libgcrypt might want to check for finished
|
||||
// initialization using:"
|
||||
if (!gcry_control (GCRYCTL_INITIALIZATION_FINISHED_P))
|
||||
return false;
|
||||
#endif
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
/* -------------------------- SHARED HASHING HELPERS ------------------------ */
|
||||
|
||||
int
|
||||
hash_init(enum hash_alg alg, HashCTX *c)
|
||||
{
|
||||
#if CONFIG_OPENSSL
|
||||
switch (alg)
|
||||
{
|
||||
case HASH_SHA1 : return SHA1_Init(&c->sha);
|
||||
case HASH_SHA224: return SHA224_Init(&c->sha256);
|
||||
case HASH_SHA256: return SHA256_Init(&c->sha256);
|
||||
case HASH_SHA384: return SHA384_Init(&c->sha512);
|
||||
case HASH_SHA512: return SHA512_Init(&c->sha512);
|
||||
default:
|
||||
return -1;
|
||||
};
|
||||
#elif CONFIG_GCRYPT
|
||||
gcry_error_t err;
|
||||
|
||||
err = gcry_md_open(c, alg, 0);
|
||||
|
||||
if (err)
|
||||
return -1;
|
||||
|
||||
return 0;
|
||||
#endif
|
||||
}
|
||||
|
||||
int
|
||||
hash_update(enum hash_alg alg, HashCTX *c, const void *data, size_t len)
|
||||
{
|
||||
#if CONFIG_OPENSSL
|
||||
switch (alg)
|
||||
{
|
||||
case HASH_SHA1 : return SHA1_Update(&c->sha, data, len);
|
||||
case HASH_SHA224: return SHA224_Update(&c->sha256, data, len);
|
||||
case HASH_SHA256: return SHA256_Update(&c->sha256, data, len);
|
||||
case HASH_SHA384: return SHA384_Update(&c->sha512, data, len);
|
||||
case HASH_SHA512: return SHA512_Update(&c->sha512, data, len);
|
||||
default:
|
||||
return -1;
|
||||
};
|
||||
#elif CONFIG_GCRYPT
|
||||
gcry_md_write(*c, data, len);
|
||||
return 0;
|
||||
#endif
|
||||
}
|
||||
|
||||
int
|
||||
hash_final(enum hash_alg alg, HashCTX *c, unsigned char *md)
|
||||
{
|
||||
#if CONFIG_OPENSSL
|
||||
switch (alg)
|
||||
{
|
||||
case HASH_SHA1 : return SHA1_Final(md, &c->sha);
|
||||
case HASH_SHA224: return SHA224_Final(md, &c->sha256);
|
||||
case HASH_SHA256: return SHA256_Final(md, &c->sha256);
|
||||
case HASH_SHA384: return SHA384_Final(md, &c->sha512);
|
||||
case HASH_SHA512: return SHA512_Final(md, &c->sha512);
|
||||
default:
|
||||
return -1;
|
||||
};
|
||||
#elif CONFIG_GCRYPT
|
||||
unsigned char *buf = gcry_md_read(*c, alg);
|
||||
if (!buf)
|
||||
return -1;
|
||||
|
||||
memcpy(md, buf, gcry_md_get_algo_dlen(alg));
|
||||
gcry_md_close(*c);
|
||||
return 0;
|
||||
#endif
|
||||
}
|
||||
|
||||
unsigned char *
|
||||
hash(enum hash_alg alg, const unsigned char *d, size_t n, unsigned char *md)
|
||||
{
|
||||
#if CONFIG_OPENSSL
|
||||
switch (alg)
|
||||
{
|
||||
case HASH_SHA1 : return SHA1(d, n, md);
|
||||
case HASH_SHA224: return SHA224(d, n, md);
|
||||
case HASH_SHA256: return SHA256(d, n, md);
|
||||
case HASH_SHA384: return SHA384(d, n, md);
|
||||
case HASH_SHA512: return SHA512(d, n, md);
|
||||
default:
|
||||
return NULL;
|
||||
};
|
||||
#elif CONFIG_GCRYPT
|
||||
gcry_md_hash_buffer(alg, md, d, n);
|
||||
return md;
|
||||
#endif
|
||||
}
|
||||
|
||||
int
|
||||
hash_length(enum hash_alg alg)
|
||||
{
|
||||
#if CONFIG_OPENSSL
|
||||
switch (alg)
|
||||
{
|
||||
case HASH_SHA1 : return SHA_DIGEST_LENGTH;
|
||||
case HASH_SHA224: return SHA224_DIGEST_LENGTH;
|
||||
case HASH_SHA256: return SHA256_DIGEST_LENGTH;
|
||||
case HASH_SHA384: return SHA384_DIGEST_LENGTH;
|
||||
case HASH_SHA512: return SHA512_DIGEST_LENGTH;
|
||||
default:
|
||||
return -1;
|
||||
};
|
||||
#elif CONFIG_GCRYPT
|
||||
return gcry_md_get_algo_dlen(alg);
|
||||
#endif
|
||||
}
|
||||
|
||||
int
|
||||
hash_ab(enum hash_alg alg, unsigned char *md, const unsigned char *m1, int m1_len, const unsigned char *m2, int m2_len)
|
||||
{
|
||||
HashCTX ctx;
|
||||
|
||||
hash_init(alg, &ctx);
|
||||
hash_update(alg, &ctx, m1, m1_len);
|
||||
hash_update(alg, &ctx, m2, m2_len);
|
||||
return hash_final(alg, &ctx, md);
|
||||
}
|
||||
|
||||
// See rfc5054 PAD()
|
||||
bnum
|
||||
H_nn_pad(enum hash_alg alg, const bnum n1, const bnum n2, int padded_len)
|
||||
{
|
||||
bnum bn;
|
||||
unsigned char *bin;
|
||||
unsigned char buff[SHA512_DIGEST_LENGTH];
|
||||
int len_n1 = bnum_num_bytes(n1);
|
||||
int len_n2 = bnum_num_bytes(n2);
|
||||
int nbytes = 2 * padded_len;
|
||||
int offset_n1 = padded_len - len_n1;
|
||||
int offset_n2 = nbytes - len_n2;
|
||||
|
||||
assert(len_n1 <= padded_len);
|
||||
assert(len_n2 <= padded_len);
|
||||
|
||||
bin = calloc(1, nbytes);
|
||||
|
||||
bnum_bn2bin(n1, bin + offset_n1, len_n1);
|
||||
bnum_bn2bin(n2, bin + offset_n2, len_n2);
|
||||
hash(alg, bin, nbytes, buff);
|
||||
free(bin);
|
||||
bnum_bin2bn(bn, buff, hash_length(alg));
|
||||
return bn;
|
||||
}
|
||||
|
||||
bnum
|
||||
H_ns(enum hash_alg alg, const bnum n, const unsigned char *bytes, int len_bytes)
|
||||
{
|
||||
bnum bn;
|
||||
unsigned char buff[SHA512_DIGEST_LENGTH];
|
||||
int len_n = bnum_num_bytes(n);
|
||||
int nbytes = len_n + len_bytes;
|
||||
unsigned char *bin = malloc(nbytes);
|
||||
|
||||
bnum_bn2bin(n, bin, len_n);
|
||||
memcpy(bin + len_n, bytes, len_bytes);
|
||||
hash(alg, bin, nbytes, buff);
|
||||
free(bin);
|
||||
bnum_bin2bn(bn, buff, hash_length(alg));
|
||||
return bn;
|
||||
}
|
||||
|
||||
void
|
||||
update_hash_n(enum hash_alg alg, HashCTX *ctx, const bnum n)
|
||||
{
|
||||
unsigned long len = bnum_num_bytes(n);
|
||||
unsigned char *n_bytes = malloc(len);
|
||||
|
||||
bnum_bn2bin(n, n_bytes, len);
|
||||
hash_update(alg, ctx, n_bytes, len);
|
||||
free(n_bytes);
|
||||
}
|
||||
|
||||
void
|
||||
hash_num(enum hash_alg alg, const bnum n, unsigned char *dest)
|
||||
{
|
||||
int nbytes = bnum_num_bytes(n);
|
||||
unsigned char *bin = malloc(nbytes);
|
||||
|
||||
bnum_bn2bin(n, bin, nbytes);
|
||||
hash( alg, bin, nbytes, dest );
|
||||
free(bin);
|
||||
}
|
||||
|
||||
|
||||
/* ----------------------------- OTHER HELPERS -------------------------------*/
|
||||
|
||||
#ifdef DEBUG_PAIR
|
||||
void
|
||||
hexdump(const char *msg, uint8_t *mem, size_t len)
|
||||
{
|
||||
int i, j;
|
||||
int hexdump_cols = 16;
|
||||
|
||||
if (msg)
|
||||
printf("%s", msg);
|
||||
|
||||
for (i = 0; i < len + ((len % hexdump_cols) ? (hexdump_cols - len % hexdump_cols) : 0); i++)
|
||||
{
|
||||
if(i % hexdump_cols == 0)
|
||||
printf("0x%06x: ", i);
|
||||
|
||||
if (i < len)
|
||||
printf("%02x ", 0xFF & ((char*)mem)[i]);
|
||||
else
|
||||
printf(" ");
|
||||
|
||||
if (i % hexdump_cols == (hexdump_cols - 1))
|
||||
{
|
||||
for (j = i - (hexdump_cols - 1); j <= i; j++)
|
||||
{
|
||||
if (j >= len)
|
||||
putchar(' ');
|
||||
else if (isprint(((char*)mem)[j]))
|
||||
putchar(0xFF & ((char*)mem)[j]);
|
||||
else
|
||||
putchar('.');
|
||||
}
|
||||
|
||||
putchar('\n');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
bnum_dump(const char *msg, bnum n)
|
||||
{
|
||||
int len_n = bnum_num_bytes(n);
|
||||
uint8_t *bin = calloc(1, len_n);
|
||||
bnum_bn2bin(n, bin, len_n);
|
||||
hexdump(msg, bin, len_n);
|
||||
free(bin);
|
||||
}
|
||||
#endif
|
||||
|
||||
|
||||
/* ----------------------------------- API -----------------------------------*/
|
||||
|
||||
struct pair_setup_context *
|
||||
pair_setup_new(enum pair_type type, const char *pin, pair_cb add_cb, void *cb_arg, const char *device_id)
|
||||
{
|
||||
struct pair_setup_context *sctx;
|
||||
|
||||
if (!pair[type]->pair_setup_new)
|
||||
return NULL;
|
||||
|
||||
sctx = calloc(1, sizeof(struct pair_setup_context));
|
||||
if (!sctx)
|
||||
return NULL;
|
||||
|
||||
sctx->type = pair[type];
|
||||
|
||||
if (pair[type]->pair_setup_new(sctx, pin, add_cb, cb_arg, device_id) < 0)
|
||||
{
|
||||
free(sctx);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
return sctx;
|
||||
}
|
||||
|
||||
void
|
||||
pair_setup_free(struct pair_setup_context *sctx)
|
||||
{
|
||||
if (!sctx)
|
||||
return;
|
||||
|
||||
if (sctx->type->pair_setup_free)
|
||||
sctx->type->pair_setup_free(sctx);
|
||||
|
||||
free(sctx);
|
||||
}
|
||||
|
||||
const char *
|
||||
pair_setup_errmsg(struct pair_setup_context *sctx)
|
||||
{
|
||||
return sctx->errmsg;
|
||||
}
|
||||
|
||||
int
|
||||
pair_setup(uint8_t **out, size_t *out_len, struct pair_setup_context *sctx, const uint8_t *in, size_t in_len)
|
||||
{
|
||||
int state;
|
||||
int ret = -1;
|
||||
|
||||
if (!sctx->type->pair_state_get)
|
||||
{
|
||||
sctx->errmsg = "Getting pair state unsupported";
|
||||
return -1;
|
||||
}
|
||||
|
||||
*out = NULL;
|
||||
*out_len = 0;
|
||||
|
||||
state = sctx->type->pair_state_get(&sctx->errmsg, in, in_len);
|
||||
if (state < 0)
|
||||
return -1;
|
||||
|
||||
switch (state)
|
||||
{
|
||||
case 0:
|
||||
*out = pair_setup_request1(out_len, sctx);
|
||||
break;
|
||||
case 1:
|
||||
ret = pair_setup_response1(sctx, in, in_len);
|
||||
if (ret < 0)
|
||||
break;
|
||||
*out = pair_setup_request1(out_len, sctx);
|
||||
break;
|
||||
case 2:
|
||||
ret = pair_setup_response1(sctx, in, in_len);
|
||||
if (ret < 0)
|
||||
break;
|
||||
*out = pair_setup_request2(out_len, sctx);
|
||||
break;
|
||||
case 3:
|
||||
ret = pair_setup_response2(sctx, in, in_len);
|
||||
if (ret < 0)
|
||||
break;
|
||||
*out = pair_setup_request2(out_len, sctx);
|
||||
break;
|
||||
case 4:
|
||||
ret = pair_setup_response2(sctx, in, in_len);
|
||||
if (ret < 0)
|
||||
break;
|
||||
*out = pair_setup_request3(out_len, sctx);
|
||||
break;
|
||||
case 5:
|
||||
ret = pair_setup_response3(sctx, in, in_len);
|
||||
if (ret < 0)
|
||||
break;
|
||||
*out = pair_setup_request3(out_len, sctx);
|
||||
break;
|
||||
case 6:
|
||||
ret = pair_setup_response3(sctx, in, in_len);
|
||||
if (ret < 0)
|
||||
break;
|
||||
break;
|
||||
default:
|
||||
sctx->errmsg = "Setup: Unsupported state";
|
||||
ret = -1;
|
||||
}
|
||||
|
||||
if (ret < 0 || !(*out))
|
||||
return -1;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
uint8_t *
|
||||
pair_setup_request1(size_t *len, struct pair_setup_context *sctx)
|
||||
{
|
||||
if (!sctx->type->pair_setup_request1)
|
||||
{
|
||||
sctx->errmsg = "Setup request 1: Unsupported";
|
||||
return NULL;
|
||||
}
|
||||
|
||||
return sctx->type->pair_setup_request1(len, sctx);
|
||||
}
|
||||
|
||||
uint8_t *
|
||||
pair_setup_request2(size_t *len, struct pair_setup_context *sctx)
|
||||
{
|
||||
if (!sctx->type->pair_setup_request2)
|
||||
{
|
||||
sctx->errmsg = "Setup request 2: Unsupported";
|
||||
return NULL;
|
||||
}
|
||||
|
||||
return sctx->type->pair_setup_request2(len, sctx);
|
||||
}
|
||||
|
||||
uint8_t *
|
||||
pair_setup_request3(size_t *len, struct pair_setup_context *sctx)
|
||||
{
|
||||
if (!sctx->type->pair_setup_request3)
|
||||
{
|
||||
sctx->errmsg = "Setup request 3: Unsupported";
|
||||
return NULL;
|
||||
}
|
||||
|
||||
return sctx->type->pair_setup_request3(len, sctx);
|
||||
}
|
||||
|
||||
int
|
||||
pair_setup_response1(struct pair_setup_context *sctx, const uint8_t *in, size_t in_len)
|
||||
{
|
||||
if (!sctx->type->pair_setup_response1)
|
||||
{
|
||||
sctx->errmsg = "Setup response 1: Unsupported";
|
||||
return -1;
|
||||
}
|
||||
|
||||
return sctx->type->pair_setup_response1(sctx, in, in_len);
|
||||
}
|
||||
|
||||
int
|
||||
pair_setup_response2(struct pair_setup_context *sctx, const uint8_t *in, size_t in_len)
|
||||
{
|
||||
if (!sctx->type->pair_setup_response2)
|
||||
{
|
||||
sctx->errmsg = "Setup response 2: Unsupported";
|
||||
return -1;
|
||||
}
|
||||
|
||||
return sctx->type->pair_setup_response2(sctx, in, in_len);
|
||||
}
|
||||
|
||||
int
|
||||
pair_setup_response3(struct pair_setup_context *sctx, const uint8_t *in, size_t in_len)
|
||||
{
|
||||
if (!sctx->type->pair_setup_response3)
|
||||
{
|
||||
sctx->errmsg = "Setup response 3: Unsupported";
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (sctx->type->pair_setup_response3(sctx, in, in_len) != 0)
|
||||
return -1;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
int
|
||||
pair_setup_result(const char **client_setup_keys, struct pair_result **result, struct pair_setup_context *sctx)
|
||||
{
|
||||
if (sctx->status != PAIR_STATUS_COMPLETED)
|
||||
{
|
||||
sctx->errmsg = "Setup result: Pair setup has not been completed";
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (sctx->type->pair_setup_result)
|
||||
{
|
||||
if (sctx->type->pair_setup_result(sctx) != 0)
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (client_setup_keys)
|
||||
*client_setup_keys = sctx->result_str;
|
||||
if (result)
|
||||
*result = &sctx->result;
|
||||
return 0;
|
||||
}
|
||||
|
||||
struct pair_verify_context *
|
||||
pair_verify_new(enum pair_type type, const char *client_setup_keys, pair_cb get_cb, void *cb_arg, const char *device_id)
|
||||
{
|
||||
struct pair_verify_context *vctx;
|
||||
|
||||
if (!pair[type]->pair_verify_new)
|
||||
return NULL;
|
||||
|
||||
vctx = calloc(1, sizeof(struct pair_verify_context));
|
||||
if (!vctx)
|
||||
return NULL;
|
||||
|
||||
vctx->type = pair[type];
|
||||
|
||||
if (pair[type]->pair_verify_new(vctx, client_setup_keys, get_cb, cb_arg, device_id) < 0)
|
||||
{
|
||||
free(vctx);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
return vctx;
|
||||
}
|
||||
|
||||
void
|
||||
pair_verify_free(struct pair_verify_context *vctx)
|
||||
{
|
||||
if (!vctx)
|
||||
return;
|
||||
|
||||
if (vctx->type->pair_verify_free)
|
||||
vctx->type->pair_verify_free(vctx);
|
||||
|
||||
free(vctx);
|
||||
}
|
||||
|
||||
const char *
|
||||
pair_verify_errmsg(struct pair_verify_context *vctx)
|
||||
{
|
||||
return vctx->errmsg;
|
||||
}
|
||||
|
||||
int
|
||||
pair_verify(uint8_t **out, size_t *out_len, struct pair_verify_context *vctx, const uint8_t *in, size_t in_len)
|
||||
{
|
||||
int state;
|
||||
int ret = -1;
|
||||
|
||||
if (!vctx->type->pair_state_get)
|
||||
{
|
||||
vctx->errmsg = "Getting pair state unsupported";
|
||||
return -1;
|
||||
}
|
||||
|
||||
*out = NULL;
|
||||
*out_len = 0;
|
||||
|
||||
state = vctx->type->pair_state_get(&vctx->errmsg, in, in_len);
|
||||
if (state < 0)
|
||||
return -1;
|
||||
|
||||
switch (state)
|
||||
{
|
||||
case 0:
|
||||
*out = pair_verify_request1(out_len, vctx);
|
||||
break;
|
||||
case 1:
|
||||
ret = pair_verify_response1(vctx, in, in_len);
|
||||
if (ret < 0)
|
||||
break;
|
||||
*out = pair_verify_request1(out_len, vctx);
|
||||
break;
|
||||
case 2:
|
||||
ret = pair_verify_response1(vctx, in, in_len);
|
||||
if (ret < 0)
|
||||
break;
|
||||
*out = pair_verify_request2(out_len, vctx);
|
||||
break;
|
||||
case 3:
|
||||
ret = pair_verify_response2(vctx, in, in_len);
|
||||
if (ret < 0)
|
||||
break;
|
||||
*out = pair_verify_request2(out_len, vctx);
|
||||
break;
|
||||
case 4:
|
||||
ret = pair_verify_response2(vctx, in, in_len);
|
||||
if (ret < 0)
|
||||
break;
|
||||
break;
|
||||
default:
|
||||
vctx->errmsg = "Verify: Unsupported state";
|
||||
ret = -1;
|
||||
}
|
||||
|
||||
if (ret < 0 || !(*out))
|
||||
return -1;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
uint8_t *
|
||||
pair_verify_request1(size_t *len, struct pair_verify_context *vctx)
|
||||
{
|
||||
if (!vctx->type->pair_verify_request1)
|
||||
{
|
||||
vctx->errmsg = "Verify request 1: Unsupported";
|
||||
return NULL;
|
||||
}
|
||||
|
||||
return vctx->type->pair_verify_request1(len, vctx);
|
||||
}
|
||||
|
||||
uint8_t *
|
||||
pair_verify_request2(size_t *len, struct pair_verify_context *vctx)
|
||||
{
|
||||
if (!vctx->type->pair_verify_request2)
|
||||
{
|
||||
vctx->errmsg = "Verify request 2: Unsupported";
|
||||
return NULL;
|
||||
}
|
||||
|
||||
return vctx->type->pair_verify_request2(len, vctx);
|
||||
}
|
||||
|
||||
int
|
||||
pair_verify_response1(struct pair_verify_context *vctx, const uint8_t *in, size_t in_len)
|
||||
{
|
||||
if (!vctx->type->pair_verify_response1)
|
||||
{
|
||||
vctx->errmsg = "Verify response 1: Unsupported";
|
||||
return -1;
|
||||
}
|
||||
|
||||
return vctx->type->pair_verify_response1(vctx, in, in_len);
|
||||
}
|
||||
|
||||
int
|
||||
pair_verify_response2(struct pair_verify_context *vctx, const uint8_t *in, size_t in_len)
|
||||
{
|
||||
if (!vctx->type->pair_verify_response2)
|
||||
{
|
||||
vctx->errmsg = "Verify response 2: Unsupported";
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (vctx->type->pair_verify_response2(vctx, in, in_len) != 0)
|
||||
return -1;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
int
|
||||
pair_verify_result(struct pair_result **result, struct pair_verify_context *vctx)
|
||||
{
|
||||
if (vctx->status != PAIR_STATUS_COMPLETED)
|
||||
{
|
||||
vctx->errmsg = "Verify result: The pairing verification did not complete";
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (vctx->type->pair_verify_result)
|
||||
{
|
||||
if (vctx->type->pair_verify_result(vctx) != 0)
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (result)
|
||||
*result = &vctx->result;
|
||||
return 0;
|
||||
}
|
||||
|
||||
struct pair_cipher_context *
|
||||
pair_cipher_new(enum pair_type type, int channel, const uint8_t *shared_secret, size_t shared_secret_len)
|
||||
{
|
||||
if (!pair[type]->pair_cipher_new)
|
||||
return NULL;
|
||||
|
||||
return pair[type]->pair_cipher_new(pair[type], channel, shared_secret, shared_secret_len);
|
||||
}
|
||||
|
||||
void
|
||||
pair_cipher_free(struct pair_cipher_context *cctx)
|
||||
{
|
||||
if (!cctx)
|
||||
return;
|
||||
|
||||
if (!cctx->type->pair_cipher_free)
|
||||
return;
|
||||
|
||||
return cctx->type->pair_cipher_free(cctx);
|
||||
}
|
||||
|
||||
const char *
|
||||
pair_cipher_errmsg(struct pair_cipher_context *cctx)
|
||||
{
|
||||
return cctx->errmsg;
|
||||
}
|
||||
|
||||
ssize_t
|
||||
pair_encrypt(uint8_t **ciphertext, size_t *ciphertext_len, const uint8_t *plaintext, size_t plaintext_len, struct pair_cipher_context *cctx)
|
||||
{
|
||||
if (!cctx->type->pair_encrypt)
|
||||
{
|
||||
cctx->errmsg = "Encryption unsupported";
|
||||
return -1;
|
||||
}
|
||||
|
||||
return cctx->type->pair_encrypt(ciphertext, ciphertext_len, plaintext, plaintext_len, cctx);
|
||||
}
|
||||
|
||||
ssize_t
|
||||
pair_decrypt(uint8_t **plaintext, size_t *plaintext_len, const uint8_t *ciphertext, size_t ciphertext_len, struct pair_cipher_context *cctx)
|
||||
{
|
||||
if (!cctx->type->pair_decrypt)
|
||||
{
|
||||
cctx->errmsg = "Decryption unsupported";
|
||||
return -1;
|
||||
}
|
||||
|
||||
return cctx->type->pair_decrypt(plaintext, plaintext_len, ciphertext, ciphertext_len, cctx);
|
||||
}
|
||||
|
||||
void
|
||||
pair_encrypt_rollback(struct pair_cipher_context *cctx)
|
||||
{
|
||||
cctx->encryption_counter = cctx->encryption_counter_prev;
|
||||
}
|
||||
|
||||
void
|
||||
pair_decrypt_rollback(struct pair_cipher_context *cctx)
|
||||
{
|
||||
cctx->decryption_counter = cctx->decryption_counter_prev;
|
||||
}
|
||||
|
||||
int
|
||||
pair_add(enum pair_type type, uint8_t **out, size_t *out_len, pair_cb add_cb, void *cb_arg, const uint8_t *in, size_t in_len)
|
||||
{
|
||||
if (!pair[type]->pair_add)
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
|
||||
return pair[type]->pair_add(out, out_len, add_cb, cb_arg, in, in_len);
|
||||
}
|
||||
|
||||
int
|
||||
pair_remove(enum pair_type type, uint8_t **out, size_t *out_len, pair_cb remove_cb, void *cb_arg, const uint8_t *in, size_t in_len)
|
||||
{
|
||||
if (!pair[type]->pair_remove)
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
|
||||
return pair[type]->pair_remove(out, out_len, remove_cb, cb_arg, in, in_len);
|
||||
}
|
||||
|
||||
int
|
||||
pair_list(enum pair_type type, uint8_t **out, size_t *out_len, pair_list_cb list_cb, void *cb_arg, const uint8_t *in, size_t in_len)
|
||||
{
|
||||
if (!pair[type]->pair_list)
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
|
||||
return pair[type]->pair_list(out, out_len, list_cb, cb_arg, in, in_len);
|
||||
}
|
||||
|
||||
int
|
||||
pair_state_get(enum pair_type type, const char **errmsg, const uint8_t *in, size_t in_len)
|
||||
{
|
||||
if (!pair[type]->pair_state_get)
|
||||
{
|
||||
*errmsg = "Getting pair state unsupported";
|
||||
return -1;
|
||||
}
|
||||
|
||||
return pair[type]->pair_state_get(errmsg, in, in_len);
|
||||
}
|
||||
|
||||
void
|
||||
pair_public_key_get(enum pair_type type, uint8_t server_public_key[32], const char *device_id)
|
||||
{
|
||||
if (!pair[type]->pair_public_key_get)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
pair[type]->pair_public_key_get(server_public_key, device_id);
|
||||
}
|
||||
+266
@@ -0,0 +1,266 @@
|
||||
#ifndef __PAIR_AP_H__
|
||||
#define __PAIR_AP_H__
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
#define PAIR_AP_VERSION_MAJOR 0
|
||||
#define PAIR_AP_VERSION_MINOR 12
|
||||
|
||||
#define PAIR_AP_DEVICE_ID_LEN_MAX 64
|
||||
|
||||
#define PAIR_AP_POST_PIN_START "POST /pair-pin-start"
|
||||
#define PAIR_AP_POST_SETUP "POST /pair-setup"
|
||||
#define PAIR_AP_POST_VERIFY "POST /pair-verify"
|
||||
#define PAIR_AP_POST_ADD "POST /pair-add"
|
||||
#define PAIR_AP_POST_LIST "POST /pair-list"
|
||||
#define PAIR_AP_POST_REMOVE "POST /pair-remove"
|
||||
|
||||
enum pair_type
|
||||
{
|
||||
// This is the pairing type required for Apple TV device verification, which
|
||||
// became mandatory with tvOS 10.2.
|
||||
PAIR_CLIENT_FRUIT,
|
||||
// This is the Homekit type required for AirPlay 2 with both PIN setup and
|
||||
// verification
|
||||
PAIR_CLIENT_HOMEKIT_NORMAL,
|
||||
// Same as normal except PIN is fixed to 3939 and stops after setup step 2,
|
||||
// when session key is established
|
||||
PAIR_CLIENT_HOMEKIT_TRANSIENT,
|
||||
// Server side implementation supporting both transient and normal mode,
|
||||
// letting client choose mode. If a PIN is with pair_setup_new() then only
|
||||
// normal mode will be possible.
|
||||
PAIR_SERVER_HOMEKIT,
|
||||
};
|
||||
|
||||
/* This struct stores the various forms of pairing results. The shared secret
|
||||
* is used to initialise an encrypted session via pair_cipher_new(). For
|
||||
* non-transient client pair setup, you also get a key string (client_setup_keys) from
|
||||
* pair_setup_result() that you can store and use to later initialise
|
||||
* pair_verify_new(). For non-transient server pair setup, you can either:
|
||||
* - Register an "add pairing" callback (add_cb) with pair_setup_new(), and
|
||||
* then save the client id and key in the callback (see server-example.c for
|
||||
* this approach).
|
||||
* - Check pairing result with pair_setup_result() and if successful read and
|
||||
* store the client id and key from the result struct.
|
||||
* - Decide not to authenticate clients during pair-verify (set get_cb to NULL)
|
||||
* in which case you don't need to save client ids and keys from pair-setup.
|
||||
*
|
||||
* Table showing returned data (everything else will be zeroed):
|
||||
*
|
||||
* | pair-setup | pair-verify
|
||||
* --------------------------------|-------------------------------|--------------
|
||||
* PAIR_CLIENT_FRUIT | client keys | shared secret
|
||||
* PAIR_CLIENT_HOMEKIT_NORMAL | client keys, server public | shared secret
|
||||
| key, server id | shared secret
|
||||
* PAIR_CLIENT_HOMEKIT_TRANSIENT | shared secret | n/a
|
||||
* PAIR_SERVER_HOMEKIT (normal) | client public key, client id | shared secret
|
||||
* PAIR_SERVER_HOMEKIT (transient) | shared secret | n/a
|
||||
*/
|
||||
struct pair_result
|
||||
{
|
||||
char device_id[PAIR_AP_DEVICE_ID_LEN_MAX]; // ID of the peer
|
||||
uint8_t client_private_key[64];
|
||||
uint8_t client_public_key[32];
|
||||
uint8_t server_public_key[32];
|
||||
uint8_t shared_secret[64];
|
||||
size_t shared_secret_len; // Will be 32 (normal) or 64 (transient)
|
||||
};
|
||||
|
||||
struct pair_setup_context;
|
||||
struct pair_verify_context;
|
||||
struct pair_cipher_context;
|
||||
|
||||
typedef int (*pair_cb)(uint8_t public_key[32], const char *device_id, void *cb_arg);
|
||||
typedef void (*pair_list_cb)(pair_cb list_cb, void *list_cb_arg, void *cb_arg);
|
||||
|
||||
|
||||
/* ------------------------------- pair setup ------------------------------- */
|
||||
|
||||
/* Client
|
||||
* When you have the pin-code (must be 4 chars), create a new context with this
|
||||
* function and then call pair_setup() or pair_setup_request1(). device_id is
|
||||
* only required for Homekit pairing. If the client previously paired
|
||||
* (non-transient) and has saved credentials, it should instead skip setup and
|
||||
* only do verification. The callback is only for Homekit, and you can leave it
|
||||
* at NULL if you don't care about saving ID and key of the server for later
|
||||
* verification (then you also set get_cb to NULL in pair_verify_new), or if you
|
||||
* will read the id and key via pair_setup_result.
|
||||
*
|
||||
* Server
|
||||
* The client will make a connection and then at some point make a /pair-setup
|
||||
* or a /pair-verify. The server should:
|
||||
* - new /pair-setup: create a setup context with a pin-code (or NULL to allow
|
||||
* transient pairing), and then call pair_setup() to process request and
|
||||
* construct reply (also for subsequent /pair-setup requests)
|
||||
* - new /pair_verify: create a verify context and then call pair_verify()
|
||||
* to process request and construct reply (also for subsequent /pair-verify
|
||||
* requests)
|
||||
*/
|
||||
struct pair_setup_context *
|
||||
pair_setup_new(enum pair_type type, const char *pin, pair_cb add_cb, void *cb_arg, const char *device_id);
|
||||
void
|
||||
pair_setup_free(struct pair_setup_context *sctx);
|
||||
|
||||
/* Returns last error message
|
||||
*/
|
||||
const char *
|
||||
pair_setup_errmsg(struct pair_setup_context *sctx);
|
||||
|
||||
/* Will create a request (if client) or response (if server) based on the setup
|
||||
* context and last message from the peer. If this is the first client request
|
||||
* then set *in to NULL. Returns negative on error.
|
||||
*/
|
||||
int
|
||||
pair_setup(uint8_t **out, size_t *out_len, struct pair_setup_context *sctx, const uint8_t *in, size_t in_len);
|
||||
|
||||
/* Returns the result of a pairing, or negative if pairing is not completed. See
|
||||
* 'struct pair_result' for info about pairing results. The string is a
|
||||
* representation of the result that is easy to persist and can be used to feed
|
||||
* back into pair_verify_new. The result and string becomes invalid when you
|
||||
* free sctx.
|
||||
*/
|
||||
int
|
||||
pair_setup_result(const char **client_setup_keys, struct pair_result **result, struct pair_setup_context *sctx);
|
||||
|
||||
/* These are for constructing specific message types and reading specific
|
||||
* message types. Not needed for Homekit pairing if you use pair_setup().
|
||||
*/
|
||||
uint8_t *
|
||||
pair_setup_request1(size_t *len, struct pair_setup_context *sctx);
|
||||
uint8_t *
|
||||
pair_setup_request2(size_t *len, struct pair_setup_context *sctx);
|
||||
uint8_t *
|
||||
pair_setup_request3(size_t *len, struct pair_setup_context *sctx);
|
||||
|
||||
int
|
||||
pair_setup_response1(struct pair_setup_context *sctx, const uint8_t *in, size_t in_len);
|
||||
int
|
||||
pair_setup_response2(struct pair_setup_context *sctx, const uint8_t *in, size_t in_len);
|
||||
int
|
||||
pair_setup_response3(struct pair_setup_context *sctx, const uint8_t *in, size_t in_len);
|
||||
|
||||
|
||||
/* ------------------------------ pair verify ------------------------------- */
|
||||
|
||||
/* Client
|
||||
* When you have completed pair setup you get a string containing some keys
|
||||
* from pair_setup_result(). Give the string as input to this function to create
|
||||
* a verification context. Set the callback to NULL. Then call pair_verify().
|
||||
* The device_id is required for Homekit pairing.
|
||||
*
|
||||
* Server
|
||||
* When you get a pair verify request from a new peer, create a new context with
|
||||
* client_setup_keys set to NULL, with a callback set and the server's device ID
|
||||
* (same as for setup). Then call pair_verify(). The callback is used to get
|
||||
* the persisted client public key (saved after pair setup), so the client can
|
||||
* be verified. You can set the callback to NULL if you don't care about that.
|
||||
* If set, the callback is made as part of pair_verify_response2. The job of the
|
||||
* callback is to fill out the public_key with the public key from the setup
|
||||
* stage (see 'struct pair_result'). If the client device id is not known (i.e.
|
||||
* it has not completed pair-setup), return -1.
|
||||
*/
|
||||
struct pair_verify_context *
|
||||
pair_verify_new(enum pair_type type, const char *client_setup_keys, pair_cb get_cb, void *cb_arg, const char *device_id);
|
||||
void
|
||||
pair_verify_free(struct pair_verify_context *vctx);
|
||||
|
||||
/* Returns last error message
|
||||
*/
|
||||
const char *
|
||||
pair_verify_errmsg(struct pair_verify_context *vctx);
|
||||
|
||||
/* Will create a request (if client) or response (if server) based on the verify
|
||||
* context and last message from the peer. If this is the first client request
|
||||
* then set *in to NULL. Returns negative on error.
|
||||
*/
|
||||
int
|
||||
pair_verify(uint8_t **out, size_t *out_len, struct pair_verify_context *sctx, const uint8_t *in, size_t in_len);
|
||||
|
||||
/* Returns a pointer to the result of the pairing. Only the shared secret will
|
||||
* be filled out. Note that the result become invalid when you free vctx.
|
||||
*/
|
||||
int
|
||||
pair_verify_result(struct pair_result **result, struct pair_verify_context *vctx);
|
||||
|
||||
/* These are for constructing specific message types and reading specific
|
||||
* message types. Not needed for Homekit pairing where you can use pair_verify().
|
||||
*/
|
||||
uint8_t *
|
||||
pair_verify_request1(size_t *len, struct pair_verify_context *vctx);
|
||||
uint8_t *
|
||||
pair_verify_request2(size_t *len, struct pair_verify_context *vctx);
|
||||
|
||||
int
|
||||
pair_verify_response1(struct pair_verify_context *vctx, const uint8_t *in, size_t in_len);
|
||||
int
|
||||
pair_verify_response2(struct pair_verify_context *vctx, const uint8_t *in, size_t in_len);
|
||||
|
||||
|
||||
/* ------------------------------- ciphering -------------------------------- */
|
||||
|
||||
/* When you have completed the verification you can extract a shared secret with
|
||||
* pair_verify_result() - or, in case of transient pairing, from
|
||||
* pair_setup_result(). Give the shared secret as input to this function to
|
||||
* create a ciphering context.
|
||||
*/
|
||||
struct pair_cipher_context *
|
||||
pair_cipher_new(enum pair_type type, int channel, const uint8_t *shared_secret, size_t shared_secret_len);
|
||||
void
|
||||
pair_cipher_free(struct pair_cipher_context *cctx);
|
||||
|
||||
/* Returns last error message
|
||||
*/
|
||||
const char *
|
||||
pair_cipher_errmsg(struct pair_cipher_context *cctx);
|
||||
|
||||
/* The return value equals length of plaintext that was encrypted, so if the
|
||||
* return value == plaintext_len then everything was encrypted. On error -1 is
|
||||
* returned.
|
||||
*/
|
||||
ssize_t
|
||||
pair_encrypt(uint8_t **ciphertext, size_t *ciphertext_len, const uint8_t *plaintext, size_t plaintext_len, struct pair_cipher_context *cctx);
|
||||
|
||||
/* The return value equals length of ciphertext that was decrypted, so if the
|
||||
* return value == ciphertext_len then everything was decrypted. On error -1 is
|
||||
* returned.
|
||||
*/
|
||||
ssize_t
|
||||
pair_decrypt(uint8_t **plaintext, size_t *plaintext_len, const uint8_t *ciphertext, size_t ciphertext_len, struct pair_cipher_context *cctx);
|
||||
|
||||
/* Rolls back the nonce
|
||||
*/
|
||||
void
|
||||
pair_encrypt_rollback(struct pair_cipher_context *cctx);
|
||||
void
|
||||
pair_decrypt_rollback(struct pair_cipher_context *cctx);
|
||||
|
||||
|
||||
/* --------------------------------- other ---------------------------------- */
|
||||
|
||||
/* These are for Homekit pairing where they are called by the controller, e.g.
|
||||
* the Home app
|
||||
*
|
||||
* TODO this part is currenly not working
|
||||
*/
|
||||
int
|
||||
pair_add(enum pair_type type, uint8_t **out, size_t *out_len, pair_cb add_cb, void *cb_arg, const uint8_t *in, size_t in_len);
|
||||
|
||||
int
|
||||
pair_remove(enum pair_type type, uint8_t **out, size_t *out_len, pair_cb remove_cb, void *cb_arg, const uint8_t *in, size_t in_len);
|
||||
|
||||
int
|
||||
pair_list(enum pair_type type, uint8_t **out, size_t *out_len, pair_list_cb list_cb, void *cb_arg, const uint8_t *in, size_t in_len);
|
||||
|
||||
/* For parsing an incoming message to see what type ("state") it is. Mostly
|
||||
* useful for servers. Returns 1-6 for pair-setup and 1-4 for pair-verify.
|
||||
*/
|
||||
int
|
||||
pair_state_get(enum pair_type type, const char **errmsg, const uint8_t *in, size_t in_len);
|
||||
|
||||
/* For servers, pair_ap calculates the public key using device_id as a seed.
|
||||
* This function returns that public key.
|
||||
*/
|
||||
void
|
||||
pair_public_key_get(enum pair_type type, uint8_t server_public_key[32], const char *device_id);
|
||||
|
||||
#endif /* !__PAIR_AP_H__ */
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,700 @@
|
||||
/*
|
||||
*
|
||||
* The MIT License (MIT)
|
||||
*
|
||||
* 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 <inttypes.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include <assert.h>
|
||||
|
||||
#include <event2/event.h>
|
||||
#include <event2/buffer.h>
|
||||
#include <event2/bufferevent.h>
|
||||
#include <event2/listener.h>
|
||||
|
||||
#include "pair.h"
|
||||
|
||||
#ifdef CONFIG_GCRYPT
|
||||
# include <gcrypt.h>
|
||||
#endif
|
||||
|
||||
#define DEVICE_ID "FFEEDDCCBBAA9988"
|
||||
#define LISTEN_PORT 7000
|
||||
#define CONTENT_TYPE_OCTET "application/octet-stream"
|
||||
#define RTSP_VERSION "RTSP/1.0"
|
||||
#define OPTIONS "OPTIONS *"
|
||||
|
||||
struct connection_ctx
|
||||
{
|
||||
struct evbuffer *pending;
|
||||
struct pair_setup_context *setup_ctx;
|
||||
struct pair_verify_context *verify_ctx;
|
||||
struct pair_cipher_context *cipher_ctx;
|
||||
|
||||
int pair_completed;
|
||||
};
|
||||
|
||||
struct rtsp_msg
|
||||
{
|
||||
int content_length;
|
||||
char *content_type;
|
||||
char *first_line;
|
||||
int cseq;
|
||||
|
||||
const uint8_t *body;
|
||||
size_t bodylen;
|
||||
|
||||
const uint8_t *data;
|
||||
size_t datalen;
|
||||
};
|
||||
|
||||
struct pairings
|
||||
{
|
||||
char device_id[PAIR_AP_DEVICE_ID_LEN_MAX];
|
||||
uint8_t public_key[32];
|
||||
|
||||
struct pairings *next;
|
||||
} *pairings;
|
||||
|
||||
|
||||
static void
|
||||
connection_free(struct connection_ctx *conn_ctx)
|
||||
{
|
||||
if (!conn_ctx)
|
||||
return;
|
||||
|
||||
evbuffer_free(conn_ctx->pending);
|
||||
pair_setup_free(conn_ctx->setup_ctx);
|
||||
pair_cipher_free(conn_ctx->cipher_ctx);
|
||||
|
||||
free(conn_ctx);
|
||||
}
|
||||
|
||||
static void
|
||||
response_headers_add(struct evbuffer *response, int cseq, size_t content_length, const char *content_type)
|
||||
{
|
||||
evbuffer_add_printf(response, "%s 200 OK\r\n", RTSP_VERSION);
|
||||
evbuffer_add_printf(response, "Server: MyServer/1.0\r\n");
|
||||
if (content_length)
|
||||
evbuffer_add_printf(response, "Content-Length: %zu\r\n", content_length);
|
||||
if (content_type)
|
||||
evbuffer_add_printf(response, "Content-Type: %s\r\n", content_type);
|
||||
evbuffer_add_printf(response, "CSeq: %d\r\n", cseq);
|
||||
evbuffer_add_printf(response, "\r\n");
|
||||
}
|
||||
|
||||
static void
|
||||
response_create_from_raw(struct evbuffer *response, uint8_t *body, size_t body_len, int cseq, const char *content_type)
|
||||
{
|
||||
response_headers_add(response, cseq, body_len, content_type);
|
||||
|
||||
if (body)
|
||||
evbuffer_add(response, body, body_len);
|
||||
}
|
||||
|
||||
static int
|
||||
encryption_enable(struct connection_ctx *conn_ctx, const uint8_t *shared_secret, size_t shared_secret_len)
|
||||
{
|
||||
conn_ctx->cipher_ctx = pair_cipher_new(PAIR_SERVER_HOMEKIT, 2, shared_secret, shared_secret_len);
|
||||
if (!conn_ctx->cipher_ctx)
|
||||
{
|
||||
printf("Error setting up ciphering\n");
|
||||
return -1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int
|
||||
buffer_encrypt(struct evbuffer *output, uint8_t *in, size_t in_len, struct connection_ctx *conn_ctx)
|
||||
{
|
||||
uint8_t *out;
|
||||
size_t out_len;
|
||||
int ret;
|
||||
|
||||
ret = pair_encrypt(&out, &out_len, in, in_len, conn_ctx->cipher_ctx);
|
||||
if (ret < 0)
|
||||
{
|
||||
printf("Error encrypting: %s\n", pair_cipher_errmsg(conn_ctx->cipher_ctx));
|
||||
return -1;
|
||||
}
|
||||
|
||||
evbuffer_add(output, out, out_len);
|
||||
free(out);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int
|
||||
buffer_decrypt(struct evbuffer *output, struct evbuffer *input, struct connection_ctx *conn_ctx)
|
||||
{
|
||||
uint8_t *in;
|
||||
size_t in_len;
|
||||
ssize_t bytes_decrypted;
|
||||
uint8_t *plain;
|
||||
size_t plain_len;
|
||||
|
||||
in = evbuffer_pullup(input, -1);
|
||||
in_len = evbuffer_get_length(input);
|
||||
|
||||
// Note that bytes_decrypted is not necessarily equal to plain_len
|
||||
bytes_decrypted = pair_decrypt(&plain, &plain_len, in, in_len, conn_ctx->cipher_ctx);
|
||||
if (bytes_decrypted < 0)
|
||||
{
|
||||
printf("Error decrypting: %s\n", pair_cipher_errmsg(conn_ctx->cipher_ctx));
|
||||
return -1;
|
||||
}
|
||||
|
||||
evbuffer_add(output, plain, plain_len);
|
||||
evbuffer_drain(input, bytes_decrypted);
|
||||
free(plain);
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
/* ---------------------------- Pairing callbacks --------------------------- */
|
||||
/* Note that none of these callbacks are required if you don't care about */
|
||||
/* securely verifying the client + don't require support for the pair-add, */
|
||||
/* pair-remove and pair-list methods. */
|
||||
|
||||
static struct pairings *
|
||||
pairing_find(const char *device_id)
|
||||
{
|
||||
struct pairings *pairing;
|
||||
|
||||
for (pairing = pairings; pairing; pairing = pairing->next)
|
||||
{
|
||||
if (strcmp(device_id, pairing->device_id) == 0)
|
||||
break;
|
||||
}
|
||||
|
||||
return pairing;
|
||||
}
|
||||
|
||||
static int
|
||||
pairing_add_cb(uint8_t public_key[32], const char *device_id, void *cb_arg)
|
||||
{
|
||||
struct pairings *pairing;
|
||||
|
||||
printf("Adding paired device %s\n", device_id);
|
||||
|
||||
pairing = pairing_find(device_id);
|
||||
if (pairing)
|
||||
{
|
||||
memcpy(pairing->public_key, public_key, sizeof(pairing->public_key));
|
||||
return 0;
|
||||
}
|
||||
|
||||
pairing = calloc(1, sizeof(struct pairings));
|
||||
snprintf(pairing->device_id, sizeof(pairing->device_id), "%s", device_id);
|
||||
memcpy(pairing->public_key, public_key, sizeof(pairing->public_key));
|
||||
|
||||
pairing->next = pairings;
|
||||
pairings = pairing;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int
|
||||
pairing_remove_cb(uint8_t public_key[32], const char *device_id, void *cb_arg)
|
||||
{
|
||||
struct pairings *pairing;
|
||||
struct pairings *iter;
|
||||
|
||||
printf("Removing paired device %s\n", device_id);
|
||||
|
||||
pairing = pairing_find(device_id);
|
||||
if (!pairing)
|
||||
{
|
||||
printf("Remove callback for unknown device\n");
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (pairing == pairings)
|
||||
pairings = pairing->next;
|
||||
else
|
||||
{
|
||||
for (iter = pairings; iter && (iter->next != pairing); iter = iter->next)
|
||||
; /* EMPTY */
|
||||
|
||||
if (iter)
|
||||
iter->next = pairing->next;
|
||||
}
|
||||
|
||||
free(pairing);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void
|
||||
pairing_list_cb(pair_cb enum_cb, void *enum_cb_arg, void *cb_arg)
|
||||
{
|
||||
struct pairings *pairing;
|
||||
|
||||
printf("Listing paired devices\n");
|
||||
|
||||
for (pairing = pairings; pairing; pairing = pairing->next)
|
||||
{
|
||||
enum_cb(pairing->public_key, pairing->device_id, enum_cb_arg);
|
||||
}
|
||||
}
|
||||
|
||||
static int
|
||||
pairing_get_cb(uint8_t public_key[32], const char *device_id, void *cb_arg)
|
||||
{
|
||||
struct pairings *pairing;
|
||||
|
||||
printf("Returning public key for paired device %s\n", device_id);
|
||||
|
||||
pairing = pairing_find(device_id);
|
||||
if (!pairing)
|
||||
return -1;
|
||||
|
||||
memcpy(public_key, pairing->public_key, sizeof(pairing->public_key));
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
/* -------------------------- Pair request handlers ------------------------- */
|
||||
|
||||
static int
|
||||
handle_pin_start(struct evbuffer *output, struct connection_ctx *conn_ctx, struct rtsp_msg *msg)
|
||||
{
|
||||
printf("Please pair with code 3939\n");
|
||||
|
||||
response_create_from_raw(output, NULL, 0, msg->cseq, NULL);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int
|
||||
handle_pair_setup(struct evbuffer *output, struct connection_ctx *conn_ctx, struct rtsp_msg *msg)
|
||||
{
|
||||
uint8_t *out;
|
||||
size_t out_len;
|
||||
struct pair_result *result;
|
||||
int ret;
|
||||
|
||||
if (!conn_ctx->setup_ctx)
|
||||
{
|
||||
conn_ctx->setup_ctx = pair_setup_new(PAIR_SERVER_HOMEKIT, NULL, pairing_add_cb, NULL, DEVICE_ID);
|
||||
if (!conn_ctx->setup_ctx)
|
||||
{
|
||||
printf("Error creating setup context\n");
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
ret = pair_setup(&out, &out_len, conn_ctx->setup_ctx, msg->body, msg->bodylen);
|
||||
if (ret < 0)
|
||||
{
|
||||
printf("Pair setup error: %s\n", pair_setup_errmsg(conn_ctx->setup_ctx));
|
||||
return -1;
|
||||
}
|
||||
|
||||
ret = pair_setup_result(NULL, &result, conn_ctx->setup_ctx);
|
||||
if (ret == 0 && result->shared_secret_len > 0) // Transient pairing completed (step 2)
|
||||
{
|
||||
encryption_enable(conn_ctx, result->shared_secret, result->shared_secret_len);
|
||||
conn_ctx->pair_completed = 1;
|
||||
}
|
||||
|
||||
response_create_from_raw(output, out, out_len, msg->cseq, CONTENT_TYPE_OCTET);
|
||||
free(out);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int
|
||||
handle_pair_verify(struct evbuffer *output, struct connection_ctx *conn_ctx, struct rtsp_msg *msg)
|
||||
{
|
||||
uint8_t *out;
|
||||
size_t out_len;
|
||||
struct pair_result *result;
|
||||
int ret;
|
||||
|
||||
if (!conn_ctx->verify_ctx)
|
||||
{
|
||||
conn_ctx->verify_ctx = pair_verify_new(PAIR_SERVER_HOMEKIT, NULL, pairing_get_cb, NULL, DEVICE_ID);
|
||||
if (!conn_ctx->verify_ctx)
|
||||
{
|
||||
printf("Error creating verify context\n");
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
ret = pair_verify(&out, &out_len, conn_ctx->verify_ctx, msg->body, msg->bodylen);
|
||||
if (ret < 0)
|
||||
{
|
||||
printf("Pair verify error: %s\n", pair_verify_errmsg(conn_ctx->verify_ctx));
|
||||
return -1;
|
||||
}
|
||||
|
||||
ret = pair_verify_result(&result, conn_ctx->verify_ctx);
|
||||
if (ret == 0)
|
||||
{
|
||||
encryption_enable(conn_ctx, result->shared_secret, result->shared_secret_len);
|
||||
conn_ctx->pair_completed = 1;
|
||||
}
|
||||
|
||||
response_create_from_raw(output, out, out_len, msg->cseq, CONTENT_TYPE_OCTET);
|
||||
free(out);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int
|
||||
handle_pair_add(struct evbuffer *output, struct connection_ctx *conn_ctx, struct rtsp_msg *msg)
|
||||
{
|
||||
uint8_t *out;
|
||||
size_t out_len;
|
||||
int ret;
|
||||
|
||||
ret = pair_add(PAIR_SERVER_HOMEKIT, &out, &out_len, pairing_add_cb, NULL, msg->body, msg->bodylen);
|
||||
if (ret < 0)
|
||||
{
|
||||
printf("Error adding device to list\n");
|
||||
return -1;
|
||||
}
|
||||
|
||||
response_create_from_raw(output, out, out_len, msg->cseq, CONTENT_TYPE_OCTET);
|
||||
free(out);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int
|
||||
handle_pair_remove(struct evbuffer *output, struct connection_ctx *conn_ctx, struct rtsp_msg *msg)
|
||||
{
|
||||
uint8_t *out;
|
||||
size_t out_len;
|
||||
int ret;
|
||||
|
||||
ret = pair_remove(PAIR_SERVER_HOMEKIT, &out, &out_len, pairing_remove_cb, NULL, msg->body, msg->bodylen);
|
||||
if (ret < 0)
|
||||
{
|
||||
printf("Error removing device from list\n");
|
||||
return -1;
|
||||
}
|
||||
|
||||
response_create_from_raw(output, out, out_len, msg->cseq, CONTENT_TYPE_OCTET);
|
||||
free(out);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int
|
||||
handle_pair_list(struct evbuffer *output, struct connection_ctx *conn_ctx, struct rtsp_msg *msg)
|
||||
{
|
||||
uint8_t *out;
|
||||
size_t out_len;
|
||||
int ret;
|
||||
|
||||
ret = pair_list(PAIR_SERVER_HOMEKIT, &out, &out_len, pairing_list_cb, NULL, msg->body, msg->bodylen);
|
||||
if (ret < 0)
|
||||
{
|
||||
printf("Error creating list of paired devices\n");
|
||||
return -1;
|
||||
}
|
||||
|
||||
response_create_from_raw(output, out, out_len, msg->cseq, CONTENT_TYPE_OCTET);
|
||||
free(out);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int
|
||||
handle_options(struct evbuffer *output, struct connection_ctx *conn_ctx, struct rtsp_msg *msg)
|
||||
{
|
||||
struct evbuffer *response;
|
||||
uint8_t *plain;
|
||||
size_t plain_len;
|
||||
int ret;
|
||||
|
||||
response = evbuffer_new();
|
||||
|
||||
response_create_from_raw(response, NULL, 0, msg->cseq, NULL);
|
||||
|
||||
if (!conn_ctx->cipher_ctx)
|
||||
{
|
||||
evbuffer_add_buffer(output, response);
|
||||
evbuffer_free(response);
|
||||
return 0;
|
||||
}
|
||||
|
||||
plain = evbuffer_pullup(response, -1);
|
||||
plain_len = evbuffer_get_length(response);
|
||||
|
||||
ret = buffer_encrypt(output, plain, plain_len, conn_ctx);
|
||||
|
||||
evbuffer_free(response);
|
||||
return ret;
|
||||
}
|
||||
|
||||
static int
|
||||
response_send(struct evbuffer *output, struct connection_ctx *conn_ctx, struct rtsp_msg *msg)
|
||||
{
|
||||
if (!msg->first_line)
|
||||
return -1;
|
||||
|
||||
if (strncmp(msg->first_line, PAIR_AP_POST_PIN_START, strlen(PAIR_AP_POST_PIN_START)) == 0)
|
||||
return handle_pin_start(output, conn_ctx, msg);
|
||||
else if (strncmp(msg->first_line, PAIR_AP_POST_SETUP, strlen(PAIR_AP_POST_SETUP)) == 0)
|
||||
return handle_pair_setup(output, conn_ctx, msg);
|
||||
else if (strncmp(msg->first_line, PAIR_AP_POST_VERIFY, strlen(PAIR_AP_POST_VERIFY)) == 0)
|
||||
return handle_pair_verify(output, conn_ctx, msg);
|
||||
else if (strncmp(msg->first_line, PAIR_AP_POST_ADD, strlen(PAIR_AP_POST_ADD)) == 0)
|
||||
return handle_pair_add(output, conn_ctx, msg);
|
||||
else if (strncmp(msg->first_line, PAIR_AP_POST_LIST, strlen(PAIR_AP_POST_LIST)) == 0)
|
||||
return handle_pair_list(output, conn_ctx, msg);
|
||||
else if (strncmp(msg->first_line, PAIR_AP_POST_REMOVE, strlen(PAIR_AP_POST_REMOVE)) == 0)
|
||||
return handle_pair_remove(output, conn_ctx, msg);
|
||||
else if (strncmp(msg->first_line, OPTIONS, strlen(OPTIONS)) == 0)
|
||||
return handle_options(output, conn_ctx, msg);
|
||||
|
||||
printf("Unknown method: %s\n", msg->first_line);
|
||||
return -1;
|
||||
}
|
||||
|
||||
|
||||
/* --------------------- A basic RTSP server implementation ----------------- */
|
||||
|
||||
static void
|
||||
rtsp_clear(struct rtsp_msg *msg)
|
||||
{
|
||||
free(msg->first_line);
|
||||
free(msg->content_type);
|
||||
}
|
||||
|
||||
// Very primitive RTSP message parser, hope you have a better one
|
||||
static int
|
||||
rtsp_parse(struct rtsp_msg *msg, uint8_t *in, size_t in_len)
|
||||
{
|
||||
char *line;
|
||||
int i;
|
||||
|
||||
line = (char *)in;
|
||||
for (i = 0; i < in_len; i++)
|
||||
{
|
||||
if (in[i] != '\n' && in[i - 1] != '\r')
|
||||
continue;
|
||||
|
||||
if (in[i - 2] == '\n' && in[i - 3] == '\r')
|
||||
{
|
||||
msg->bodylen = in_len - (i + 1);
|
||||
if (msg->bodylen != msg->content_length)
|
||||
{
|
||||
printf("Incomplete read (have %zu, content-length %d), waiting for more data\n\n", msg->bodylen, msg->content_length);
|
||||
rtsp_clear(msg);
|
||||
return 1;
|
||||
}
|
||||
else if (msg->bodylen > 0)
|
||||
msg->body = in + i + 1;
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
in[i - 1] = '\0';
|
||||
|
||||
if (!msg->first_line)
|
||||
msg->first_line = strdup(line);
|
||||
|
||||
if (strncmp(line, "CSeq: ", strlen("CSeq: ")) == 0)
|
||||
msg->cseq = atoi(line + strlen("CSeq: "));
|
||||
|
||||
if (strncmp(line, "Content-Length: ", strlen("Content-Length: ")) == 0)
|
||||
msg->content_length = atoi(line + strlen("Content-Length: "));
|
||||
|
||||
if (strncmp(line, "Content-Type: ", strlen("Content-Type: ")) == 0 && !msg->content_type)
|
||||
msg->content_type = strdup(line + strlen("Content-Type: "));
|
||||
|
||||
in[i - 1] = '\r';
|
||||
|
||||
line = (char *)in + i + 1;
|
||||
}
|
||||
|
||||
msg->data = in;
|
||||
msg->datalen = in_len;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void
|
||||
in_read_cb(struct bufferevent *bev, void *arg)
|
||||
{
|
||||
struct connection_ctx *conn_ctx = arg;
|
||||
struct evbuffer *input;
|
||||
struct evbuffer *output;
|
||||
uint8_t *plain;
|
||||
size_t plain_len;
|
||||
struct rtsp_msg msg = { 0 };
|
||||
int ret;
|
||||
|
||||
input = bufferevent_get_input(bev);
|
||||
output = bufferevent_get_output(bev);
|
||||
|
||||
printf("\n--------------------------------------------------------------------------\n");
|
||||
|
||||
if (conn_ctx->pair_completed)
|
||||
{
|
||||
buffer_decrypt(conn_ctx->pending, input, conn_ctx);
|
||||
}
|
||||
else
|
||||
{
|
||||
evbuffer_add_buffer(conn_ctx->pending, input);
|
||||
}
|
||||
|
||||
// Pending holds all the message we have received so far, incl. what parts we
|
||||
// might have received in previous callbacks
|
||||
plain = evbuffer_pullup(conn_ctx->pending, -1);
|
||||
plain_len = evbuffer_get_length(conn_ctx->pending);
|
||||
|
||||
ret = rtsp_parse(&msg, plain, plain_len);
|
||||
if (ret < 0)
|
||||
{
|
||||
printf("Could not parse RTSP message\n");
|
||||
goto error;
|
||||
}
|
||||
else if (ret == 1)
|
||||
return; // Message incomplete, wait for more data
|
||||
|
||||
ret = response_send(output, conn_ctx, &msg);
|
||||
if (ret < 0)
|
||||
{
|
||||
goto error;
|
||||
}
|
||||
|
||||
error:
|
||||
rtsp_clear(&msg);
|
||||
evbuffer_drain(conn_ctx->pending, evbuffer_get_length(conn_ctx->pending));
|
||||
return;
|
||||
}
|
||||
|
||||
static void
|
||||
in_event_cb(struct bufferevent *bev, short events, void *arg)
|
||||
{
|
||||
struct connection_ctx *conn_ctx = arg;
|
||||
|
||||
if (events & BEV_EVENT_ERROR)
|
||||
printf("Error from bufferevent: %s\n", evutil_socket_error_to_string(EVUTIL_SOCKET_ERROR()));
|
||||
|
||||
if (events & (BEV_EVENT_EOF | BEV_EVENT_ERROR))
|
||||
bufferevent_free(bev);
|
||||
|
||||
connection_free(conn_ctx);
|
||||
}
|
||||
|
||||
|
||||
/*------------------------- General server stuff ----------------------------*/
|
||||
|
||||
static void
|
||||
in_accept_cb(struct evconnlistener *listener, evutil_socket_t sock, struct sockaddr *address, int socklen, void *ctx)
|
||||
{
|
||||
struct event_base *base = evconnlistener_get_base(listener);
|
||||
struct bufferevent *bev = bufferevent_socket_new(base, sock, BEV_OPT_CLOSE_ON_FREE);
|
||||
struct connection_ctx *conn_ctx;
|
||||
|
||||
conn_ctx = calloc(1, sizeof(struct connection_ctx));
|
||||
conn_ctx->pending = evbuffer_new();
|
||||
|
||||
bufferevent_setcb(bev, in_read_cb, NULL, in_event_cb, conn_ctx);
|
||||
bufferevent_enable(bev, EV_READ | EV_WRITE);
|
||||
|
||||
printf("New connection accepted\n");
|
||||
}
|
||||
|
||||
static void
|
||||
in_error_cb(struct evconnlistener *listener, void *ctx)
|
||||
{
|
||||
int err = EVUTIL_SOCKET_ERROR();
|
||||
printf("Error occured %d (%s) on the listener\n", err, evutil_socket_error_to_string(err));
|
||||
}
|
||||
|
||||
static struct evconnlistener *
|
||||
listen_add(struct event_base *evbase, evconnlistener_cb req_cb, evconnlistener_errorcb err_cb, unsigned short port)
|
||||
{
|
||||
struct evconnlistener *listener;
|
||||
struct addrinfo hints = { 0 };
|
||||
struct addrinfo *servinfo;
|
||||
char strport[8];
|
||||
int ret;
|
||||
|
||||
hints.ai_socktype = SOCK_STREAM;
|
||||
hints.ai_family = AF_UNSPEC;
|
||||
hints.ai_flags = AI_PASSIVE;
|
||||
|
||||
snprintf(strport, sizeof(strport), "%hu", port);
|
||||
ret = getaddrinfo(NULL, strport, &hints, &servinfo);
|
||||
if (ret < 0)
|
||||
{
|
||||
printf("getaddrinf() failed: %s\n", gai_strerror(ret));
|
||||
return NULL;
|
||||
}
|
||||
|
||||
listener = evconnlistener_new_bind(evbase, req_cb, NULL, LEV_OPT_CLOSE_ON_FREE | LEV_OPT_REUSEABLE, -1, servinfo->ai_addr, servinfo->ai_addrlen);
|
||||
freeaddrinfo(servinfo);
|
||||
if (!listener)
|
||||
{
|
||||
printf("Could not create listener for port %hu\n", port);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
evconnlistener_set_error_cb(listener, err_cb);
|
||||
|
||||
return listener;
|
||||
}
|
||||
|
||||
int
|
||||
main(int argc, char * argv[])
|
||||
{
|
||||
struct event_base *evbase;
|
||||
struct evconnlistener *listener;
|
||||
|
||||
// libgcrypt requires that the application initializes the library
|
||||
#ifdef CONFIG_GCRYPT
|
||||
if (!gcry_check_version(NULL))
|
||||
{
|
||||
printf("libgcrypt not initialized\n");
|
||||
return -1;
|
||||
}
|
||||
gcry_control(GCRYCTL_DISABLE_SECMEM, 0);
|
||||
gcry_control(GCRYCTL_INITIALIZATION_FINISHED, 0);
|
||||
#endif
|
||||
|
||||
evbase = event_base_new();
|
||||
|
||||
listener = listen_add(evbase, in_accept_cb, in_error_cb, LISTEN_PORT);
|
||||
if (!listener)
|
||||
return -1;
|
||||
|
||||
printf("Listening for pairing requests on port %d\n", LISTEN_PORT);
|
||||
|
||||
event_base_dispatch(evbase);
|
||||
|
||||
evconnlistener_free(listener);
|
||||
event_base_free(evbase);
|
||||
|
||||
return 0;
|
||||
}
|
||||
@@ -21,14 +21,19 @@
|
||||
#include <openssl/aes.h>
|
||||
#endif
|
||||
|
||||
#ifdef CONFIG_AIRPLAY_2
|
||||
#include "pair_ap/pair.h"
|
||||
#include <plist/plist.h>
|
||||
#endif
|
||||
|
||||
#include "alac.h"
|
||||
#include "audio.h"
|
||||
|
||||
#define time_ping_history_power_of_two 7
|
||||
// this must now be zero, otherwise bad things will happen
|
||||
#define time_ping_history \
|
||||
(1 << time_ping_history_power_of_two) // 2^7 is 128. At 1 per three seconds, approximately six
|
||||
// minutes of records
|
||||
|
||||
typedef struct time_ping_record {
|
||||
uint64_t dispersion;
|
||||
uint64_t local_time;
|
||||
@@ -37,6 +42,20 @@ typedef struct time_ping_record {
|
||||
int chosen;
|
||||
} time_ping_record;
|
||||
|
||||
// these are for reporting the status of the clock
|
||||
typedef enum {
|
||||
clock_no_anchor_info,
|
||||
clock_ok,
|
||||
clock_service_unavailable,
|
||||
clock_access_error,
|
||||
clock_data_unavailable,
|
||||
clock_no_master,
|
||||
clock_version_mismatch,
|
||||
clock_not_synchronised,
|
||||
clock_not_valid,
|
||||
clock_not_ready,
|
||||
} clock_status_t;
|
||||
|
||||
typedef uint16_t seq_t;
|
||||
|
||||
typedef struct audio_buffer_entry { // decoded audio packets
|
||||
@@ -83,19 +102,64 @@ typedef struct {
|
||||
audio_stream_type type;
|
||||
} stream_cfg;
|
||||
|
||||
// the following is used even when not built for AirPlay 2
|
||||
typedef enum {
|
||||
unspecified_stream_category = 0,
|
||||
ptp_stream,
|
||||
ntp_stream,
|
||||
remote_control_stream
|
||||
} airplay_stream_c; // "c" for category
|
||||
|
||||
#ifdef CONFIG_AIRPLAY_2
|
||||
typedef enum { ts_ntp, ts_ptp } timing_t;
|
||||
typedef enum { ap_1, ap_2 } airplay_t;
|
||||
typedef enum { realtime_stream, buffered_stream } airplay_stream_t;
|
||||
|
||||
typedef struct {
|
||||
int connection_number; // for debug ID purposes, nothing else...
|
||||
int resend_interval; // this is really just for debugging
|
||||
char *UserAgent; // free this on teardown
|
||||
int AirPlayVersion; // zero if not an AirPlay session. Used to help calculate latency
|
||||
uint8_t *data;
|
||||
size_t length;
|
||||
size_t size;
|
||||
} sized_buffer;
|
||||
|
||||
typedef struct {
|
||||
struct pair_cipher_context *cipher_ctx;
|
||||
sized_buffer encrypted_read_buffer;
|
||||
sized_buffer plaintext_read_buffer;
|
||||
int is_encrypted;
|
||||
} pair_cipher_bundle; // cipher context and buffers
|
||||
|
||||
typedef struct {
|
||||
struct pair_setup_context *setup_ctx;
|
||||
struct pair_verify_context *verify_ctx;
|
||||
pair_cipher_bundle control_cipher_bundle;
|
||||
} ap2_pairing;
|
||||
|
||||
// flush requests are stored in order of flushFromSeq
|
||||
// on the basis that block numbers are monotonic modulo 2^24
|
||||
typedef struct flush_request_t {
|
||||
int flushNow; // if true, the flushFrom stuff is invalid
|
||||
uint32_t flushFromSeq;
|
||||
uint32_t flushFromTS;
|
||||
uint32_t flushUntilSeq;
|
||||
uint32_t flushUntilTS;
|
||||
struct flush_request_t *next;
|
||||
} flush_request_t;
|
||||
|
||||
#endif
|
||||
|
||||
typedef struct {
|
||||
int connection_number; // for debug ID purposes, nothing else...
|
||||
int resend_interval; // this is really just for debugging
|
||||
int rtsp_link_is_idle; // if true, this indicates if the client asleep
|
||||
char *UserAgent; // free this on teardown
|
||||
int AirPlayVersion; // zero if not an AirPlay session. Used to help calculate latency
|
||||
int latency_warning_issued;
|
||||
uint32_t latency; // the actual latency used for this play session
|
||||
uint32_t minimum_latency; // set if an a=min-latency: line appears in the ANNOUNCE message; zero
|
||||
// otherwise
|
||||
uint32_t maximum_latency; // set if an a=max-latency: line appears in the ANNOUNCE message; zero
|
||||
// otherwise
|
||||
int software_mute_enabled; // if we don't have a real mute that we can use
|
||||
int unachievable_audio_backend_latency_offset_notified; // set when a latency warning is issued
|
||||
|
||||
int fd;
|
||||
int authorized; // set if a password is required and has been supplied
|
||||
char *auth_nonce; // the session nonce, if needed
|
||||
@@ -107,12 +171,14 @@ typedef struct {
|
||||
volatile int watchdog_barks; // number of times the watchdog has timed out and done something
|
||||
int unfixable_error_reported; // set when an unfixable error command has been executed.
|
||||
|
||||
time_t playstart;
|
||||
uint64_t playstart;
|
||||
uint64_t connection_start_time; // the time the device is selected, which could be a long time
|
||||
// before a play
|
||||
pthread_t thread, timer_requester, rtp_audio_thread, rtp_control_thread, rtp_timing_thread,
|
||||
player_watchdog_thread;
|
||||
|
||||
// buffers to delete on exit
|
||||
signed short *tbuf;
|
||||
int32_t *tbuf;
|
||||
int32_t *sbuf;
|
||||
char *outbuf;
|
||||
|
||||
@@ -121,8 +187,9 @@ typedef struct {
|
||||
stats_t *statistics;
|
||||
|
||||
// for holding the output rate information until printed out at the end of a session
|
||||
double frame_rate;
|
||||
int frame_rate_status;
|
||||
double raw_frame_rate;
|
||||
double corrected_frame_rate;
|
||||
int frame_rate_valid;
|
||||
|
||||
// for holding input rate information until printed out at the end of a session
|
||||
|
||||
@@ -156,7 +223,11 @@ typedef struct {
|
||||
// mutexes and condition variables
|
||||
pthread_cond_t flowcontrol;
|
||||
pthread_mutex_t ab_mutex, flush_mutex, volume_control_mutex;
|
||||
|
||||
int fix_volume;
|
||||
double initial_airplay_volume;
|
||||
int initial_airplay_volume_set;
|
||||
|
||||
uint32_t timestamp_epoch, last_timestamp,
|
||||
maximum_timestamp_interval; // timestamp_epoch of zero means not initialised, could start at 2
|
||||
// or 1.
|
||||
@@ -192,11 +263,13 @@ typedef struct {
|
||||
int rtp_running;
|
||||
uint64_t rtp_time_of_last_resend_request_error_ns;
|
||||
|
||||
char client_ip_string[INET6_ADDRSTRLEN]; // the ip string pointing to the client
|
||||
char self_ip_string[INET6_ADDRSTRLEN]; // the ip string being used by this program -- it
|
||||
// could be one of many, so we need to know it
|
||||
uint32_t self_scope_id; // if it's an ipv6 connection, this will be its scope
|
||||
short connection_ip_family; // AF_INET / AF_INET6
|
||||
char client_ip_string[INET6_ADDRSTRLEN]; // the ip string of the client
|
||||
uint16_t client_rtsp_port;
|
||||
char self_ip_string[INET6_ADDRSTRLEN]; // the ip string being used by this program -- it
|
||||
uint16_t self_rtsp_port; // could be one of many, so we need to know it
|
||||
|
||||
uint32_t self_scope_id; // if it's an ipv6 connection, this will be its scope
|
||||
short connection_ip_family; // AF_INET / AF_INET6
|
||||
|
||||
SOCKADDR rtp_client_control_socket; // a socket pointing to the control port of the client
|
||||
SOCKADDR rtp_client_timing_socket; // a socket pointing to the timing port of the client
|
||||
@@ -214,8 +287,83 @@ typedef struct {
|
||||
|
||||
// this is what connects an rtp timestamp to the remote time
|
||||
|
||||
uint32_t reference_timestamp;
|
||||
uint64_t remote_reference_timestamp_time;
|
||||
int udp_clock_is_initialised;
|
||||
int udp_clock_sender_is_initialised;
|
||||
|
||||
int anchor_remote_info_is_valid;
|
||||
|
||||
// these can be modified if the master clock changes over time
|
||||
uint64_t anchor_clock;
|
||||
uint64_t anchor_time; // this is the time according to the clock
|
||||
uint32_t anchor_rtptime;
|
||||
|
||||
// these are used to identify when the master clock becomes equal to the
|
||||
// actual anchor clock information, so it can be used to avoid accumulating errors
|
||||
uint64_t actual_anchor_clock;
|
||||
uint64_t actual_anchor_time;
|
||||
uint32_t actual_anchor_rtptime;
|
||||
|
||||
clock_status_t clock_status;
|
||||
|
||||
airplay_stream_c
|
||||
airplay_stream_category; // is it a remote control stream or a normal "full service" stream?
|
||||
// (will be unspecified if not build for AirPlay 2)
|
||||
|
||||
#ifdef CONFIG_AIRPLAY_2
|
||||
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?
|
||||
airplay_stream_t airplay_stream_type; // is it realtime audio or buffered audio...
|
||||
timing_t timing_type; // are we using NTP or PTP on this connection?
|
||||
|
||||
pthread_t *rtp_event_thread;
|
||||
pthread_t *rtp_data_thread;
|
||||
pthread_t rtp_ap2_control_thread;
|
||||
pthread_t rtp_realtime_audio_thread;
|
||||
pthread_t rtp_buffered_audio_thread;
|
||||
|
||||
int last_anchor_info_is_valid;
|
||||
uint32_t last_anchor_rtptime;
|
||||
uint64_t last_anchor_local_time;
|
||||
uint64_t last_anchor_time_of_update;
|
||||
uint64_t last_anchor_validity_start_time;
|
||||
|
||||
ssize_t ap2_audio_buffer_size;
|
||||
ssize_t ap2_audio_buffer_minimum_size;
|
||||
flush_request_t *flush_requests; // if non-null, there are flush requests, mutex protected
|
||||
int ap2_flush_requested;
|
||||
int ap2_flush_from_valid;
|
||||
uint32_t ap2_flush_from_rtp_timestamp;
|
||||
uint32_t ap2_flush_from_sequence_number;
|
||||
uint32_t ap2_flush_until_rtp_timestamp;
|
||||
uint32_t ap2_flush_until_sequence_number;
|
||||
int ap2_rate; // protect with flush mutex, 0 means don't play, 1 means play
|
||||
int ap2_play_enabled; // protect with flush mutex
|
||||
|
||||
ap2_pairing ap2_pairing_context;
|
||||
|
||||
int event_socket;
|
||||
int data_socket;
|
||||
SOCKADDR ap2_remote_control_socket_addr; // a socket pointing to the control port of the client
|
||||
socklen_t ap2_remote_control_socket_addr_length;
|
||||
int ap2_control_socket;
|
||||
int realtime_audio_socket;
|
||||
int buffered_audio_socket;
|
||||
|
||||
uint16_t local_data_port;
|
||||
uint16_t local_event_port;
|
||||
uint16_t local_ap2_control_port;
|
||||
uint16_t local_realtime_audio_port;
|
||||
uint16_t local_buffered_audio_port;
|
||||
|
||||
uint64_t audio_format;
|
||||
uint64_t compression;
|
||||
unsigned char *session_key; // needs to be free'd at the end
|
||||
uint64_t frames_packet;
|
||||
uint64_t type;
|
||||
uint64_t networkTimeTimelineID; // the clock ID used by the player
|
||||
uint8_t groupContainsGroupLeader; // information coming from the SETUP
|
||||
#endif
|
||||
|
||||
// used as the initials values for calculating the rate at which the source thinks it's sending
|
||||
// frames
|
||||
@@ -271,17 +419,28 @@ typedef struct {
|
||||
uint64_t dac_buffer_queue_minimum_length;
|
||||
} rtsp_conn_info;
|
||||
|
||||
uint32_t modulo_32_offset(uint32_t from, uint32_t to);
|
||||
uint64_t modulo_64_offset(uint64_t from, uint64_t to);
|
||||
extern pthread_mutex_t playing_conn_lock;
|
||||
extern int statistics_row; // will be reset to zero when debug level changes or statistics enabled
|
||||
|
||||
void reset_buffer(rtsp_conn_info *conn);
|
||||
|
||||
void get_audio_buffer_size_and_occupancy(unsigned int *size, unsigned int *occupancy,
|
||||
rtsp_conn_info *conn);
|
||||
|
||||
int32_t modulo_32_offset(uint32_t from, uint32_t to);
|
||||
|
||||
void ab_resync(rtsp_conn_info *conn);
|
||||
|
||||
int player_prepare_to_play(rtsp_conn_info *conn);
|
||||
int player_play(rtsp_conn_info *conn);
|
||||
int player_stop(rtsp_conn_info *conn);
|
||||
|
||||
void player_volume(double f, rtsp_conn_info *conn);
|
||||
void player_volume_without_notification(double f, rtsp_conn_info *conn);
|
||||
void player_flush(uint32_t timestamp, rtsp_conn_info *conn);
|
||||
void player_put_packet(seq_t seqno, uint32_t actual_timestamp, uint8_t *data, int len,
|
||||
rtsp_conn_info *conn);
|
||||
// void player_full_flush(rtsp_conn_info *conn);
|
||||
void player_put_packet(int original_format, seq_t seqno, uint32_t actual_timestamp, uint8_t *data,
|
||||
int len, rtsp_conn_info *conn);
|
||||
int64_t monotonic_timestamp(uint32_t timestamp,
|
||||
rtsp_conn_info *conn); // add an epoch to the timestamp. The monotonic
|
||||
// timestamp guaranteed to start between 2^32 2^33
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user