diff --git a/.github/workflows/docker-build.yaml b/.github/workflows/docker-build.yaml index 33b1c8e0..cb186043 100644 --- a/.github/workflows/docker-build.yaml +++ b/.github/workflows/docker-build.yaml @@ -6,11 +6,11 @@ name: Build and push docker image based on commit to specified branches. on: push: - branches: [ master, development, charles-dev ] + branches: [ development ] env: DOCKER_PLATFORMS: linux/386,linux/amd64,linux/arm/v6,linux/arm64,linux/arm/v7 - + jobs: main: runs-on: ubuntu-latest @@ -19,18 +19,18 @@ jobs: uses: actions/checkout@v2 with: fetch-depth: 0 - + - 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 diff --git a/.gitignore b/.gitignore index b78d32b7..276d276e 100644 --- a/.gitignore +++ b/.gitignore @@ -27,6 +27,9 @@ man/Makefile.in scripts/shairport-sync.service scripts/shairport-sync shairport-sync.core +gitversion.h +plist_xml_strings.* + #Some dbus files that are automatically generated /org.gnome.ShairportSync.service diff --git a/AIRPLAY2.md b/AIRPLAY2.md index f9d1face..b253f39c 100644 --- a/AIRPLAY2.md +++ b/AIRPLAY2.md @@ -1,15 +1,15 @@ AirPlay 2 === -Shairport Sync offers limited AirPlay 2 support for audio sources on iOS devices and Macs. In addition, it partly works if the audio source is a HomePod mini or an Apple TV. It does not work for iTunes on Windows. Unfortunately, it does not integrate with the Home app or with Siri. +Shairport Sync offers limited AirPlay 2 support for audio sources on iOS devices and Macs. In addition, it partly works if the audio source is a HomePod mini or an Apple TV. It does not work for iTunes on Windows. It has limited integration with the Home app and Siri. -The focus of the development effort has been on getting good audio performance for iOS and Mac. Features outside this focus may be missing or broken. So, for example, integration with Apple's Home app is missing; remote control doesn't work. +The focus of the development effort has been on getting good audio performance for iOS and Mac. Features outside this focus may be missing or broken. So, for example, remote control doesn't work. For AirPlay 2, Shairport Sync uses another application called [`nqptp`](https://github.com/mikebrady/nqptp) ("Not Quite PTP") for timing and synchronisation. `nqptp` must run as `root` and must have exclusive access to ports `319` and `320`. AirPlay 2 functionality is available on recent Linux and FreeBSD builds. The FreeBSD implementation has not been extensively tested. OpenBSD and Cygwin are not supported. -AirPlay 2 support definitely does not work on macOS. This is because [`nqptp`](https://github.com/mikebrady/nqptp) uses ports `319` and `320` but these ports are unavailable because there are used by macOS to support its implementation of AirPlay 2. +The AirPlay 2 version of Shairport Sync will not work if installed on macOS. This is because [`nqptp`](https://github.com/mikebrady/nqptp) uses ports `319` and `320` but these ports are unavailable because there are used by macOS to support its implementation of AirPlay 2. The AirPlay 2 build requires a good deal of extra library support and may not fit into smaller devices. It also requires more CPU power and more RAM. @@ -17,19 +17,19 @@ The AirPlay 2 build requires a good deal of extra library support and may not fi - AirPlay 2 for iOS and Mac players. ### What Does Not Work -- No integration with Siri or HomeKit - Lossless or High Definition Lossless material is transcoded to AAC before transmission over AirPlay 2. - No Remote Control - No Artwork in the Metadata - No AirPlay 2 for Windows iTunes ### What Partly Works +- Incomplete functionality with Siri and HomeKit - Incomplete functionality using AppleTV or HomePod as player. A Shairport Sync player will work -- i.e. it will play -- but the selection button won't persist and the volume can't be set. ### Limitations - Only a single instance of Shairport Sync can run on a device. -- The AirPlay 2 version of Shairport Sync will not run on macOS, since `nqptp` can not be installed on it. +- The AirPlay 2 version of Shairport Sync will not work if installed on macOS, since `nqptp` can not be installed on macOS. ### Known Issues - A crash can occur occasionally when a Shairport Sync player is being selected for output. If you can get this to happen reliably, please let us know. @@ -45,23 +45,24 @@ So, here are some guideline requirements: Guides --- -* To build Shairport Sync for AirPlay 2 on Linux, please follow the guide at [BUILDFORAP2.md](https://github.com/aillwee/shairport-sync/blob/development/BUILDFORAP2.md). +* To build Shairport Sync for AirPlay 2 on Linux, please follow the guide at [BUILDFORAP2.md](https://github.com/mikebrady/shairport-sync/blob/development/BUILDFORAP2.md). * A guide for building on FreeBSD is forthcoming. AirPlay 2 -- More About What Works --- * Two types of audio are received by Shairport Sync -- "Realtime" streams of CD quality ALAC (like AirPlay 1) and "Buffered Audio" streams of AAC stereo at 44,100 frames per second. The selection of stream type is made by the player. -* Audio is synchronised with other AirPlay 2 devices, including AirPlay 2 devices that have their own master clocks. (Note: This has not been tested with multiple nearly-identical master clock devices such as with two HomePod minis -- Shairport Sync will get confused about which is the current master.) +* Audio is synchronised with other AirPlay 2 devices, including AirPlay 2 devices that have their own master clocks. * Shairport Sync continues to support AirPlay 1, and offers an AirPlay 1 compatibility mode for situations where iTunes on macOS or macOS Music plays to multiple speakers and one of more of them is compatible with AirPlay 1 only. Acknowledgements ---- -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. - -Big thanks are due to number of individuals made direct and valuable contributions to Shairport Sync: +Huge thanks are due to a number of individuals who made direct and valuable contributions to Shairport Sync: 1. [ejurgensen](https://github.com/ejurgensen) contributed ideas and code. The `pair_ap` submodule and related code in `rtsp.c` is theirs. 2. [JD Smith](https://github.com/jdtsmith) contributed ideas, comments and suggestions along with extensive and thorough testing. -3. [Charles Omer](https://github.com/charlesomer) contributed ideas, comments, bug fixes and Docker automation (forthcoming). +3. [Charles Omer](https://github.com/charlesomer) contributed ideas, comments, bug fixes, documentation and Docker automation (forthcoming). +4. [ckdo](https://github.com/ckdo) for their pathfinding work on AirPlay 2, especially the HomeKit pairing. + +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. Finally ---- diff --git a/BUILDFORAP1.md b/BUILDFORAP1.md index 07dac81d..847346ba 100644 --- a/BUILDFORAP1.md +++ b/BUILDFORAP1.md @@ -9,14 +9,15 @@ Do the usual update and upgrade: ``` # apt-get update # apt-get upgrade -``` +``` +(Separately, on a Raspberry Pi only, 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 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.) +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 computer. @@ -32,8 +33,8 @@ Remove it as follows: ``` Do this until no more copies of `shairport-sync` are found. -### Remove Old Startup Scripts -You should also remove the startup script files `/etc/systemd/system/shairport-sync.service` and `/etc/init.d/shairport-sync` if they exist – new ones will be installed in necessary. +### 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. @@ -48,7 +49,7 @@ First, install the packages needed by Shairport Sync: ``` Next, download Shairport Sync, check out the `development` branch, configure it, compile and install it: ``` -$ git clone https://github.com/aillwee/shairport-sync.git +$ git clone https://github.com/mikebrady/shairport-sync.git $ cd shairport-sync $ git checkout development $ autoreconf -fi diff --git a/BUILDFORAP2.md b/BUILDFORAP2.md index 8bbc2a09..b6106455 100644 --- a/BUILDFORAP2.md +++ b/BUILDFORAP2.md @@ -5,9 +5,8 @@ Building Shairport Sync for AirPlay 2 * Be especially careful with audio systems capable of very high volume output -- the volume control in this software may not be reliable! * For now, leave the settings in the configuration file at default except as noted below. * At the time of writing, May 2, 2021, everything is on the latest version of the software -- macOS 11.3, iOS 14.5, Raspberry Pi OS 5.10.17-v7l+ (Buster), Ubuntu 20.04 -- fully updated. -* Shairport Sync relies on a program called [`nqptp`](https://github.com/mikebrady/nqptp) to monitor timing signals. This program uses ports 319 and 320 and replaces any PTP service you have on the computer. - (FYI, most computers do not have a PTP clock running. They often use a (totally different) [Network Timing Protocol (NTP)](http://www.ntp.org) service to keep the system clock synchronised with world time.) -* Shairport Sync has a build dependency on `nqptp`, so you should build or update `nqptp` *before* building or updating Shairport Sync. +* Shairport Sync relies on a program called [`nqptp`](https://github.com/mikebrady/nqptp) to monitor timing signals. This program uses ports 319 and 320 and replaces any PTP service you have on the computer. + (FYI, most computers do not have a PTP clock running. They often use a (totally different) [Network Timing Protocol (NTP)](http://www.ntp.org) service to keep the system clock synchronised with world time.) * The POSIX Shared Memory Interface (SMI) Version numbers of `nqptp` and Shairport Sync must match. If they don't, you'll get a message in the logs. It means that one of the programs is out of date with respect to the other. * Build instructions are likely to change. @@ -55,21 +54,18 @@ Okay, now let's get the tools and libraries for building and installing Shairpor First, install the packages needed by Shairport Sync: ``` -# apt-get install build-essential git xmltoman autoconf automake libtool \ +# 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 libglib2.0-dev \ libplist-dev libsodium-dev libavutil-dev libavcodec-dev libavformat-dev uuid-dev libgcrypt-dev ``` -(Note: if you don't want the `D-Bus` interface, you may omit `libglib2.0-dev`.) ### nqptp ### Download, install, enable and start `nqptp` from [here](https://github.com/mikebrady/nqptp). -***Note:*** Shairport Sync expects the `nqptp` repository folder to be in the same directory as the `shairport-sync` repository folder, as it will look for a header file at `../nqptp/nqptp-shm-structures.h`. - ### Shairport Sync #### Please use `git` to clone the repository! -As you probably know, you can download the repository in two ways: (1) using `git` to clone it -- recommended -- or (2) downloading the repository as a ZIP archive. Please us the `git` method. The reason it that when you use `git`, the build process can incorporate the `git` build information in the version string you get when you execute the command `$ shairport-sync -V`. This will be very useful for identifying the exact build if you are making comments or bug reports. Here is an example: +As you probably know, you can download the repository in two ways: (1) using `git` to clone it -- recommended -- or (2) downloading the repository as a ZIP archive. Please use the `git` method. The reason it that when you use `git`, the build process can incorporate the `git` build information in the version string you get when you execute the command `$ shairport-sync -V`. This will be very useful for identifying the exact build if you are making comments or bug reports. Here is an example: ``` Version with git information: 4.0-dev-138-g2789572-AirPlay2-OpenSSL-Avahi-ALSA-soxr-metadata-dbus-sysconfdir:/etc @@ -81,7 +77,7 @@ Version without git information: #### Build and Install Download Shairport Sync, check out the `development` branch and configure, compile and install it: ``` -$ git clone git@github.com:aillwee/shairport-sync.git +$ git clone https://github.com/mikebrady/shairport-sync.git $ cd shairport-sync $ git checkout development $ autoreconf -fi @@ -154,4 +150,4 @@ Connect and enjoy... ### Restart Your Mac! -At the time of writing, there seems to be a bug in the Mac Music app, particularly on Apple silicon Macs. It appears that, if the Mac has been sleeping, the AirPlay 2 infrastructure is not fully reenabled, and it will be unable to drive AirPlay 2 devices for more than a few seconds. The only know solution is to reboot. +At the time of writing, there seems to be a bug in the Mac Music app, particularly on Apple silicon Macs. It appears that, if the Mac has been sleeping, the AirPlay 2 infrastructure is not fully reenabled, and it will be unable to drive AirPlay 2 devices for more than a few seconds. The only known solution is to reboot. diff --git a/CAR INSTALL.md b/CAR INSTALL.md index 0a82b68a..27b3ae43 100644 --- a/CAR INSTALL.md +++ b/CAR INSTALL.md @@ -14,7 +14,7 @@ Note that Android devices can not, so far, do this trick of using the two networ ## Example -In this example, a Raspberry Pi Zero W and a Pimoroni PHAT DAC are used. This combination has been tested for well over a year. Please note that some of the details of setting up networks are specific to the version of Linux used -- Raspbian Stretch. +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. ### 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. @@ -42,13 +42,12 @@ The first thing to do on a Pi would be to use the `raspi-config` tool to expand ``` # apt-get update # apt-get upgrade -# rpi-update ``` ### Shairport Sync First, install the packages needed by Shairport Sync: ``` -# 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 ``` Next, download Shairport Sync, configure it, compile and install it: ``` @@ -92,6 +91,7 @@ A number of packages to enable the Pi to work as a WiFi base station are needed: ``` Disable both of these services from starting at boot time (this is because we will launch them sequentially later on): ``` +# systemctl unmask hostapd # systemctl disable hostapd # systemctl disable isc-dhcp-server ``` @@ -193,6 +193,24 @@ denyinterfaces wlan0 ``` From this point on, at least on the Raspberry Pi, if you reboot the machine, it will not reconnect to your network – instead, it will act as the WiFi base station you have configured with `hostapd` and `isc-dhcp-server`. +### 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 increase startup time. Running these commands disables them: + +```` +sudo systemctl disable systemd-timesyncd.service +sudo systemctl disable keyboard-setup.service +sudo systemctl disable triggerhappy.service +sudo systemctl disable dhcpcd.service +sudo systemctl disable wpa_supplicant.service +sudo systemctl disable dphys-swapfile.service +sudo systemctl disable networking.service +```` + +### Read-only mode -- Raspberry Pi Specific + +Run `sudo raspi-config` and then choose `Performance Options` > `Overlay Filesystem` and choose to enable the overlay filesystem, and to set the boot partition to be write-protected. + ### Ready Install the Raspberry Pi in your car. It should be powered from a source that is switched off when you leave the car, otherwise the slight current drain will eventually flatten the car's battery. diff --git a/FEDORA.md b/FEDORA.md index cf8cbc50..f105ddbe 100644 --- a/FEDORA.md +++ b/FEDORA.md @@ -1,23 +1,27 @@ Fedora Installation Guide [Needs updating for Airplay 2] ----- - +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 +# 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`. This example is for version 2.6: +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-2.6.tar.gz https://github.com/mikebrady/shairport-sync/archive/2.6.tar.gz -$ rpmbuild -ta shairport-sync-2.6.tar.gz +$ 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". 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): ``` -# rpm -i ~/rpmbuild/RPMS/i686/shairport-sync-2.6-1.fc22.i686.rpm +$ 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` 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: +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 diff --git a/MQTT.md b/MQTT.md index da21a3fc..3189a54a 100644 --- a/MQTT.md +++ b/MQTT.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 ==== diff --git a/Makefile.am b/Makefile.am index 2cb27d48..dd8a1bac 100644 --- a/Makefile.am +++ b/Makefile.am @@ -101,6 +101,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 diff --git a/OPENBSD.md b/OPENBSD.md index 6b18f979..c046390d 100644 --- a/OPENBSD.md +++ b/OPENBSD.md @@ -1,16 +1,24 @@ Shairport Sync on OpenBSD [AirPlay 2 Not Supported] ---- -This is an initial note about installing Shairport Sync on OpenBSD. Shairport Sync compiles and runs natively on OpenBSD using the `sndio` back end. +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). Unlike FreeBSD, it seems that OpenBSD does not use the directory `/usr/local/etc` as a system configuration directory ("`sysconfdir`") but follows the same practice as Linux in using `/etc` as the default `sysconfdir`. General ---- -This build was done on a default build of `OpenBSD 6.2 GENERIC.MP#134 amd64`. Following [this guide](https://www.openbsd.org/faq/faq15.html), `/etc/installurl` was created with the contents: +This build was done on a default build of `OpenBSD 6.6 GENERIC.MP#5 amd64`. Following [this guide](https://www.openbsd.org/faq/faq15.html), `/etc/installurl` was created with the to refer to the standard repository: ``` https://ftp.openbsd.org/pub/OpenBSD ``` -Next, although it may not always be necessary, [update the packages](https://unix.stackexchange.com/questions/23579/how-to-apply-updates-on-openbsd-netbsd-and-freebsd). +You might have a [closer or faster](https://www.openbsd.org/ftp.html) repository to use instead. + +Next, although it may not always be necessary, apply any outstanding system updates and [update the packages](https://unix.stackexchange.com/questions/23579/how-to-apply-updates-on-openbsd-netbsd-and-freebsd). + +First, update the system: +``` +# syspatch +``` +Now update the packages: ``` # pkg_add -Uu ``` @@ -31,10 +39,10 @@ Install the following packages (e.g. using `pkg_add` in superuser mode) needed t ``` # pkg_add autoconf automake popt libconfig git ``` -Add the relevant shell variable definitions for Autoconf and Automake -- they could be placed in the user's `.profile` file to be automatically executed at login: +Note the versions of `autoconf` and `automake` you choose (`2.69` and `1.16` at the time of writing) and add them as shell variable definitions for -- they could be placed in the user's `.profile` file to be automatically executed at login: ``` export AUTOCONF_VERSION=2.69 -export AUTOMAKE_VERSION=1.15 +export AUTOMAKE_VERSION=1.16 ``` Now, download Shairport Sync from GitHub: ``` @@ -44,7 +52,8 @@ $ cd shairport-sync Next, configure and compile shairport-sync: ``` $ autoreconf -fi -$ ./configure --sysconfdir=/etc --with-avahi --with-ssl=openssl --with-libdaemon --with-sndio --with-os=openbsd +$ ./configure --sysconfdir=/etc --with-avahi --with-ssl=openssl --with-libdaemon \ + --with-sndio --with-os=openbsd $ make ``` The application is called `shairport-sync`. Check that it's running correctly by executing the following command: @@ -53,11 +62,11 @@ $ ./shairport-sync -V ``` This will execute the application and it will return its version information and terminate, for example: ``` -3.2-libdaemon-OpenSSL-Avahi-sndio-sysconfdir:/etc +3.3.6-libdaemon-OpenSSL-Avahi-sndio-sysconfdir:/etc ``` There is no make install yet -- you're on your own. Using the `sndio` backend ---- -The `sndio` back end does not have a hardware volume control facility. +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 `mixerctl` command. diff --git a/README-DEVELOPMENT.md b/README-DEVELOPMENT.md deleted file mode 100644 index 8193f84e..00000000 --- a/README-DEVELOPMENT.md +++ /dev/null @@ -1,589 +0,0 @@ -Shairport Sync -============= -Shairport Sync is an AirPlay audio player – it plays audio streamed from iTunes, iOS, Apple TV and macOS devices and AirPlay sources such as Quicktime Player and [ForkedDaapd](http://ejurgensen.github.io/forked-daapd/), among others. - -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. - -Shairport Sync runs on Linux and FreeBSD. It does not support AirPlay video or photo streaming. - -This is the unstable "development" branch. Changes and updates are incorporated into this branch quickly. To access the stable version, where changes are made after due time, please switch to the "master" branch. - -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). - -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. - -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. - -For more about the motivation behind Shairport Sync, please see the wiki at https://github.com/mikebrady/shairport-sync/wiki. - -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. - -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”. - -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. -* Fast Response — With hardware volume control, response is instantaneous; otherwise the response time is 0.2 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 iOS. -* Support for the [MPRIS](https://specifications.freedesktop.org/mpris-spec/2.2/) protocol with some support for remote control of the source. -* A native D-Bus IPC interface with access to metadata and with some support for remote control of the source. -* A native [MQTT](https://en.wikipedia.org/wiki/MQTT) interface for metadata and/or controls. -* Raw Audio — Shairport Sync can deliver raw PCM audio to standard output or to a pipe. This output is delivered synchronously with the source after the appropriate latency and is not interpolated or "stuffed" on its way through Shairport Sync. -* Autotools and Libtool Support — the Shairport Sync build process uses GNU `autotools` and `libtool` to examine and configure the build environment — important for portability and for cross compilation. Previous versions of Shairport looked at the current system to determine which packages were available, instead of looking at the target system for what packages were available. - -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/development/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 distributions including Debian, Ubuntu, Fedora asnd OpenWrt and works on FreeBSD and on OpenBSD. It works with built-in audio and with a variety of USB-connected sound cards, audio amplifiers and DACs, and works with I2S sound cards. - -Shairport Sync runs well on the Raspberry Pi -- including the original Raspberry Pi B -- using the built-in sound card or on USB and I2S cards. - -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 Linuxes and the BSDs inside VMWare Fusion on a Mac, but synchronisation is inaccurate — possibly because the sound card is being emulated. - -In addition, Shairport Sync can output to standard output, pipes, `soundio` and `ao` devices using appropriate backends. - -For information about changes and updates, please refer to the RELEASENOTES.md file in the distribution. - -Building And Installing the Development Version ---------------------- -See [here](https://github.com/mikebrady/shairport-sync/blob/development/INSTALL.md) for simple installation instructions for modern Linux systems like Rapsberry Pis. - -The following procedures will build and install the `shairport-sync` application into your system. - -**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 -a 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 initialisation script files `/etc/systemd/system/shairport-sync.service` and `/etc/init.d/shairport-sync` if they exist – new ones will be installed in necessary. - -**FreeBSD, OpenBSD, CYGWIN** - -Please refer to the relevant pages for building for the above systems. - -**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 -* autoconf -* automake -* libtool -* libpopt -* libconfig - -If you are building for a system that needs to be able to daemonise Shairport Sync with the `-d` option and to kill the daemon with the `-k` option, you need to include the `libdaemon` library: -* libdaemon - -Most current releases of Linuxes use the `systemd` startup and system manager, and this daemonises applications differently, so the `libdaemon` library can be omitted. - -Optional: -* libsoxr -* libalac (This is a library containing the Apple ALAC decoder.) -* libmosquitto (This is to enable the mqtt-metadata-interface.) - -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 — chances are an embedded processor won't be able to keep up. - -Debian, Ubuntu and Raspbian users can get the basics with: - -- `# apt-get install build-essential git xmltoman` – these may already be installed. -- `# apt-get install autoconf automake libtool libpopt-dev libconfig-dev` -- `# apt-get install libdaemon-dev` – if you need libdaemon support. -- `# apt-get install libasound2-dev` for the ALSA libraries -- `# apt-get install libpulse-dev` for the PulseAudio libraries -- `# apt-get install avahi-daemon libavahi-client-dev` if you want to use Avahi (recommended). -- `# apt-get install libssl-dev` if you want to use OpenSSL and libcrypto, or use mbed TLS otherwise. -- `# apt-get install libmbedtls-dev` if you want to use mbed TLS, or use OpenSSL/libcrypto otherwise. You can still use PolarSSL with `apt-get install libpolarssl-dev` if you want to use PolarSSL, but it is deprecated as it's not longer being supported. -- `# apt-get install libsoxr-dev` if you want support for libsoxr-based resampling. This library is in many recent distributions; if not, instructions for how to build it from source for Rasbpian/Debian Wheezy are available at [LIBSOXR.md](https://github.com/mikebrady/shairport-sync/blob/master/LIBSOXR.md). -- `# apt-get install libmosquitto-dev` if you want to use the mqtt client - -If you just want to have the default configuration options (systemd): -- `# apt-get install build-essential git xmltoman autoconf automake libtool libpopt-dev libconfig-dev libasound2-dev libpulse-dev avahi-daemon libavahi-client-dev libssl-dev libsoxr-dev` - -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: -``` -$ git checkout development -$ 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. -- `--with-ssl=openssl`, `--with-ssl=mbedtls` or `--with-ssl=polarssl` (deprecated) for encryption and related utilities using either OpenSSL, mbed TLS or PolarSSL. -- `--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-mqtt-client` to add support for publishing metadata and remote control via [MQTT](https://en.wikipedia.org/wiki/MQTT). -- `--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-libdaemon` to allow Shairport Sync to daemonise itself. -- `--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 or to daemonise itself and run in the background, 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 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-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 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, add `--with-libdaemon` and replace the `--with-systemd` with `--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`. If you want to be able to daemonise Shairport Sync without installing it as a service, include the `--with-libdaemon` flag. - -**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 -``` - -**Complete installation into a System V system** - -If you have chosen the `--with-systemv` configuration option, enter: -``` -$ sudo update-rc.d shairport-sync defaults 90 10 -``` - -**Identifying the configuration of an installed Shairport Sync - -To find out what configuration Shairport Sync was built with, enter: -``` -$ shairport-sync -V -``` -This will return something like the following: -``` -3.3-libdaemon-OpenSSL-Avahi-ALSA-sndio-stdout-pipe-metadata-dbus-sysconfdir:/etc -``` -This information may be useful when [troubleshooting](https://github.com/mikebrady/shairport-sync/blob/master/TROUBLESHOOTING.md) or [reporting issues](https://github.com/mikebrady/shairport-sync/blob/master/CONTRIBUTING.md). -**Man Page** - -You can view the man page here: http://htmlpreview.github.io/?https://github.com/mikebrady/shairport-sync/blob/development/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, it 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, if any, to use to control the output level. To get values for (b) and (c) 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`, give the service the password `secret` and use `libsoxr` interpolation, then you should do the following: - -``` -general = -{ - name = "Front Room"; - password = "secret"; - interpolation = "soxr"; - // ... 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` stanza. - -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. You can also set the sink name that audio will be pushed to. Here's an example `pa` group configuration: - -``` -pa = -{ - application_name = "Shairport Sync - Bedroom"; - sink = "server_bedroom"; // Sink name or ID. If not specified, the default sink will be used. -}; -``` - -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 them using the `general` group settings `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 has 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 quite adequate for casual listening. A problem is 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 50 dB of it is in any way usable. To help get the most from the DAC, 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 50, the top 50 dB of the range will the used. With this setting on the Raspberry Pi, maximum volume will be +4dB and minimum volume will be -46dB, 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/development/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. This is available if Shairport Sync is built with the `--with-libdaemon` configuration flag. -* The `-k` option causes Shairport Sync to kill an existing Shairport Sync daemon. You may need to have sudo privileges for this. This is available if Shairport Sync is built with the `--with-libdaemon` configuration flag. - -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 using soxr-based resampling and driving a Topping TP30 Digital Amplifier, which has an integrated USB DAC and which is connected as audio device `hw:1`: -``` -general = { - name = "Kitchen"; - interpolation = "soxr"; -}; - -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, to drive a first generation Griffin iMic, a USB sound card: -``` -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 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 console or log file: -``` -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: -``` -1.04, 0.0, 0.0, 5015, 0, 2, 0, 2, 7847, 202, 226, 44099.99, 44103.19, 44100.30, 0.00, 0, 7.16 -1.00, 0.0, 0.0, 6018, 0, 2, 0, 2, 8375, 215, 226, 44099.99, 44106.36, 44100.32, 0.00, 0, 7.43 -0.84, 0.0, 0.0, 7021, 0, 2, 0, 2, 8453, 217, 226, 44099.99, 44102.16, 44100.23, 0.00, 0, 5.51 -0.79, 0.0, 0.0, 8024, 0, 2, 0, 2, 8215, 216, 226, 44100.00, 44098.16, 44100.31, 0.00, 0, 7.22 -0.83, 2.8, 2.8, 9027, 0, 2, 0, 2, 8078, 217, 226, 44099.99, 44105.64, 44100.31, 0.00, 0, 7.37 -1.09, 2.8, 2.8, 10030, 0, 2, 0, 2, 8117, 215, 226, 44099.99, 44100.59, 44100.31, 0.00, 0, 7.25 -1.37, 0.0, 0.0, 11033, 0, 2, 0, 2, 8429, 217, 226, 44099.99, 44081.51, 44100.32, 19.27, 10, -11.87 -1.49, 0.0, 0.0, 12036, 0, 2, 0, 2, 8423, 215, 226, 44099.99, 44095.55, 44100.31, 25.43, 12, -18.13 -1.46, 2.8, 2.8, 13039, 0, 2, 0, 2, 8064, 217, 226, 44099.99, 44097.07, 44100.31, 22.39, 14, -15.16 -1.77, 2.8, 2.8, 14042, 0, 2, 0, 2, 8195, 216, 226, 44100.00, 44095.31, 44100.32, 22.96, 16, -15.76 -1.88, 0.0, 5.7, 15045, 0, 2, 0, 2, 8054, 215, 226, 44100.00, 44100.36, 44100.32, 24.36, 18, -17.18 -2.01, -39.7, 45.3, 16048, 0, 2, 0, 2, 8138, 217, 226, 44099.99, 44098.67, 44100.32, 25.23, 19, -17.71 -1.85, -5.7, 17.0, 17051, 0, 2, 0, 2, 8063, 218, 226, 44100.00, 44100.22, 44100.32, 25.43, 22, -18.15 -1.92, 0.0, 0.0, 18054, 0, 2, 0, 2, 8381, 217, 226, 44100.00, 44101.46, 44100.31, 24.62, 23, -17.42 -1.96, -17.0, 17.0, 19057, 0, 2, 0, 2, 8479, 216, 226, 44099.99, 44098.06, 44100.32, 24.19, 24, -16.82 -2.02, -90.6, 90.6, 20060, 0, 2, 0, 2, 8438, 217, 226, 44100.00, 44097.24, 44100.33, 26.16, 26, -18.73 -2.01, -25.5, 25.5, 21063, 0, 2, 0, 2, 8417, 216, 226, 44100.00, 44103.05, 44100.31, 26.16, 26, -18.97 -2.00, -19.8, 25.5, 22066, 0, 5, 0, 5, 8121, 217, 226, 44100.00, 44101.97, 44100.32, 26.12, 27, -18.83 -1.44, 0.0, 0.0, 23069, 0, 5, 0, 5, 8451, 216, 226, 44100.00, 44102.68, 44100.31, 25.49, 30, -18.37 -1.47, 0.0, 0.0, 24072, 0, 5, 0, 5, 8439, 217, 226, 44100.00, 44102.03, 44100.31, 24.92, 33, -17.75 -1.50, 0.0, 0.0, 25075, 0, 5, 0, 5, 8401, 214, 226, 44100.00, 44102.00, 44100.32, 24.92, 33, -17.66 -1.86, 2.8, 8.5, 26078, 0, 5, 0, 5, 7778, 215, 227, 44100.00, 44103.63, 44100.31, 24.66, 35, -17.62 -1.87, -17.0, 17.0, 27081, 0, 5, 0, 5, 8489, 217, 226, 44100.00, 44100.92, 44100.32, 24.15, 37, -16.98 -1.92, -5.7, 17.0, 28084, 0, 5, 0, 5, 8172, 217, 226, 44100.00, 44100.05, 44100.31, 24.10, 39, -16.95 -2.00, -36.8, 36.8, 29087, 0, 5, 0, 5, 8468, 216, 226, 44100.00, 44100.87, 44100.32, 23.94, 41, -16.64 -1.62, -11.3, 11.3, 30090, 0, 5, 0, 5, 8433, 218, 226, 44100.00, 44097.75, 44100.28, 23.34, 43, -16.91 -1.75, 2.8, 2.8, 31093, 0, 5, 0, 5, 8224, 215, 226, 44100.00, 44101.39, 44100.31, 23.21, 44, -16.05 -1.95, -2.8, 2.8, 32096, 0, 5, 0, 5, 8472, 216, 226, 44100.00, 44101.39, 44100.31, 22.79, 46, -15.71 -1.83, -102.0, 102.0, 33099, 0, 5, 0, 5, 8411, 214, 226, 44100.00, 44100.58, 44100.31, 23.00, 49, -15.95 -1.48, 0.0, 0.0, 34102, 0, 5, 0, 5, 8419, 209, 226, 44100.00, 44101.87, 44100.31, 23.00, 49, -15.87 -1.62, 2.8, 2.8, 35105, 0, 11, 0, 11, 8074, 216, 226, 44100.00, 44102.71, 44100.31, 23.07, 50, -15.92 -1.71, -2.8, 8.5, 36108, 0, 11, 0, 11, 7835, 216, 227, 44100.00, 44102.47, 44100.32, 23.27, 51, -15.92 -1.94, -5.7, 5.7, 37111, 0, 11, 0, 11, 8470, 217, 226, 44100.00, 44102.54, 44100.31, 23.73, 53, -16.54 -1.95, -2.8, 2.8, 38114, 0, 11, 0, 11, 8461, 217, 226, 44100.00, 44102.93, 44100.32, 23.78, 54, -16.56 -``` - -"Sync error in milliseconds" is the average deviation from exact synchronisation. The first line of the example above indicates that the output drifts to 1.95 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. The first line of the example above indicates that the output device is consuming frames 25.9 ppm faster than the source is generating them. The subsequent lines indicate that the stable actual difference in the clocks is around 95 ppm. - -"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 that are being made. Third party programs tend to have much larger levels of corrections. - -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 a Raspberry Pi Zero W using a Pimoroni pHAT DAC. - -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... - -"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.20 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. - -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/development/TROUBLESHOOTING.md) for more details. - -Troubleshooting ---------------- -Please refer to [TROUBLESHOOTING](https://github.com/mikebrady/shairport-sync/blob/development/TROUBLESHOOTING.md) for a few hints, contributed by users. - diff --git a/README.md b/README.md index fe9eec75..b98156dc 100644 --- a/README.md +++ b/README.md @@ -7,17 +7,17 @@ Shairport Sync AirPlay 2 --- -For the latest on AirPlay 2, please visit [AIRPLAY2.md](https://github.com/aillwee/shairport-sync/blob/development/AIRPLAY2.md). +For the latest on AirPlay 2, please visit [AIRPLAY2.md](https://github.com/mikebrady/shairport-sync/blob/development/AIRPLAY2.md). Guides --- -* A brief guide to building classic Shairport Sync (for AirPlay 1) is available at [BUILDFORAP1.md](https://github.com/aillwee/shairport-sync/blob/development/BUILDFORAP1.md). -* A guide to building Shairport Sync for AirPlay 2 on Linux is available at [BUILDFORAP2.md](https://github.com/aillwee/shairport-sync/blob/development/BUILDFORAP2.md). +* A brief guide to building classic Shairport Sync (for AirPlay 1) is available at [BUILDFORAP1.md](https://github.com/mikebrady/shairport-sync/blob/development/BUILDFORAP1.md). +* A guide to building Shairport Sync for AirPlay 2 on Linux is available at [BUILDFORAP2.md](https://github.com/mikebrady/shairport-sync/blob/development/BUILDFORAP2.md). * A guide to building for AirPlay 2 on FreeBSD is forthcoming. More Information --- -For more information, please visit [MOREINFO.md](https://github.com/aillwee/shairport-sync/blob/development/MOREINFO.md). +For more information, please visit [MOREINFO.md](https://github.com/mikebrady/shairport-sync/blob/development/MOREINFO.md). Acknowledgements --- @@ -26,6 +26,6 @@ For the development of AirPlay 2 support, special thanks are due to: * [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 (forthcoming), testing, encouragement, enthusiasm. +* [Charles Omer](https://github.com/charlesomer) for Docker automation, repository management automation, testing, encouragement, enthusiasm. And of course, thanks to everyone who has supported and improved Shairport Sync over the years. diff --git a/RELEASENOTES-DEVELOPMENT.md b/RELEASENOTES-DEVELOPMENT.md index 5f73973d..775a74e2 100644 --- a/RELEASENOTES-DEVELOPMENT.md +++ b/RELEASENOTES-DEVELOPMENT.md @@ -1,17 +1,56 @@ -Version 4.0-dev +Version 4.0-dev-308-g23da206 +==== +#### Bug Fixes +- Restore logging to `syslog`. + +Version 4.0-dev-303-g9074069 +==== +#### New Features +- An initial [PipeWire](https://pipewire.org) backend, with thanks to [Lukas Rusak](https://github.com/lrusak). Use the `--with-pw` configuration flag at the `./configure...` stage to include support. + +#### Bug Fixes +- Fix a bug when using the `tinysvcmdns` mDNS implementation, with thanks to [fesc2000](https://github.com/fesc2000). +- When an unrecognised SETUP message is received, just ignore it and put a warning in the log. Remove a redundant `client_setup_plist` data item from the `conn` data structure. + +#### Enhancements +- Shairport Sync no longer needs to be in the same directory as NQPTP during compilation. +- Add Docker automations -- thanks to [Charles Omer](https://github.com/charlesomer). +- Add Issue management automations -- thanks to [Charles Omer](https://github.com/charlesomer). +- Reconnect the DACP scanner for AirPlay 2. (It still isn't useful, unfortunately.) +- Add initial support to allow dynamic modification of Rendezvous advertisements. +- Simplify some UFW suggestions in the Troubleshooting Guide. +- Merge some documentation changes from the `master` branch. +- Add information to the Car Install Guide about improving boot times, thanks to [vasilisvg](https://github.com/vasilisvg) + +Version 4.0-dev-185-g0c02ee2 +==== + +#### New Features +- **Home App Integration** An AirPlay 2 build of Shairport Sync now offers limited integration with the Home app. Specifically, a Shairport Sync AirPlay 2 instance can now be added as a speaker to the Home app. Thanks to [ckdo](https://github.com/ckdo) and [ejurgensen](https://github.com/ejurgensen) for figuring this out and for coding it up. It means that some Siri interaction is possible. +- **Automated Docker Builds** Thanks to the work of [Charles Omer](https://github.com/charlesomer), Docker builds on the `development` branch should appear from time to time on the [Docker Hub](https://hub.docker.com/r/mikebrady/shairport-sync). + +#### Bug Fixes +- [Crashing bug fix](https://github.com/mikebrady/nqptp/blob/main/RELEASE_NOTES.md) in `nqptp` -- thanks to [ste94pz](https://github.com/ste94pz). +- Honour the `max_volume_db` setting even when `ignore_volume_control` is true. Note that dithering is enabled if software attenuation is needed. + +#### Other Changes +- Spell out `Shairport Sync` instead of `SPS` in a few places. +- Clang format some source files. + +Version 4.0-dev-153-g9ccde50 ==== Big Update ---- -Version 4 brings limited AirPlay 2 functionality to Shairport Sync. +Version 4 brings [limited AirPlay 2 functionality](https://github.com/mikebrady/shairport-sync/blob/development/AIRPLAY2.md) to Shairport Sync. -For more information about AirPlay 2, including acknowledgements, please visit [AIRPLAY2.md]. +For information about AirPlay 2, including acknowledgements, please visit [AIRPLAY2.md](https://github.com/mikebrady/shairport-sync/blob/development/AIRPLAY2.md). #### Known Issues - In the AirPlay 2 build, a crash can occur occasionally when the player is being selected. If you can get this to happen reliably, please let us know. -Warning ----- +#### Warning + 1. Big changes have been made to the codebase to enable it to be used to build either as the "classic" AirPlay 1 player or as a new AirPlay 2 player. These changes have probably introduced bugs into the classic AirPlay 1 build. 2. There is lots we don't know about AirPlay 2. This can result in incorrect behaviour. Watch out for very high volume levels, especially if you have a powerful audio system. 3. This is a `development` branch and is probably more buggy than usual. Normal support will not be provided -- instead, we will be interested in bug reports that help improve the program. You need to be adept with Linux or FreeBSD to get involved. diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 973433a5..e86b136f 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -1,5 +1,69 @@ Please see the [Release Notes for 3.3](https://github.com/mikebrady/shairport-sync/releases/tag/3.3). +Version 3.3.8 +==== +**Enhancements** +* Documentation for the MQTT interface. Many thanks to [minix1234](https://github.com/minix1234)! + +**Bug Fixes** +* Fix a bug in the `alsa` back end. In the interval between checking that the alsa device handle was non-`NULL` and actually using it, the handle could be set to `NULL`. The interval between check and usage is now protected. +* Fix a bug in the `alsa` precision timing code. Thanks to [durwin99](https://github.com/durwin99), [Nicolas Da Mutten](https://github.com/cleverer), [mistakenideas](https://github.com/mistakenideas), [Ben Willmore](https://github.com/ben-willmore) and [giggywithit](https://github.com/giggywithit) for the [report](https://github.com/mikebrady/shairport-sync/issues/1158). +* Fix a bug that caused Shairport Sync to hang, but not actually crash, if an `on-...` script failed. +* Fix a crash that occurred if metadata support is enabled during compilation but turned off in the configuration file. Thanks to [Tim Curtis](https://github.com/moodeaudio) for the report. +* Fix a crash that occurred playing from AirPower on Android. Thanks to [Ircama](https://github.com/Ircama) for the report. +* Fix the configure.ac file so that `--without-` configuration options are not interpreted as `--with-` options instead! Thanks to [David Racine](https://github.com/bassdr) for the report. + +Version 3.3.7 +==== +**Docker Integration** +* Docker Hub integration. From now on, prebuilt Docker images should be available on the Docker Hub at https://hub.docker.com/r/mikebrady/shairport-sync. + +**New Feature** +* For the PulseAudio backend `pa`, added a new `server` entry to the `pa` section of the configuration file, allowing you to specify a connection to a remote or a local system PulseAudio instance instead of letting PulseAudio choose. Thanks to [Guillaume Revaillot](https://github.com/grevaillot) for this new feature. + +**Pesky Changes** +* The underlying timing system has been moved from 64-bit fixed-point time representation (like NTP) to a 64-bit unsigned nanoseconds representation. This should make precisely no difference to the functionality of Shairport Sync but the transition might inadvertently have introduced bugs. Problem reports gratefully received. + +**Bug Fixes** +* Fixed a number of bugs that prevented Shairport Sync from terminating cleanly in response to the `MPRIS` interface's `Quit()` command. Thanks to [João Gabriel](https://github.com/jgabriel98) for reporting this issue. +* Related to the above, the code used to terminate the application after a fatal error has been cleaned up. It now uses the correct `exit()` call rather than the rather hacky `abort()` call, returning the value of the constant `EXIT_FAILURE` (typically `1`) to the caller . +* Fixed a bug whereby the start and end of active mode tokens `abeg` and `aend` where not generated or published -- `pend` tokens were being generated instead. Thanks to [minix1234](https://github.com/minix1234) for the bug report and [fix](https://github.com/mikebrady/shairport-sync/pull/1023). +* Fixed a bug calculating the instantaneous synchronisation error. This bug could occasionally cause Shairport Sync to lose synchronisation and maybe even to mute for a few seconds before resynchronising. It was caused doing modulo arithmetic incorrectly and it's been there for a while. +* Removed a bug which would affect initial synchronisation if a `FLUSH` command was received from the player at an inopportune time. +* Added code to do calculations involving the `audio_backend_latency_offset_in_seconds` and `audio_backend_silent_lead_in_time` settings correctly. Many thanks to [Tucker Kern](https://github.com/mill1000) for discovering a number of bugs associated with this and for [proposing a solution](https://github.com/mikebrady/shairport-sync/pull/1001). This prompted a closer investigation and a number of further improvements were made, and a few "hostages to fortune" removed. +* The DACP ID looks like a 64-bit number expressed in hexadecimal. It is normally quoted with leading zeroes removed, but in the `_dacp._tcp` service string, leading zeros are not removed from the DACP ID. The bug fix removes those leading zeroes. Thanks to [julianc1969](https://github.com/juliandc1969) for tracking down this bug so tenaciously! +* Make the first output backend in the list of backends the default and make its name the default `output_name`. +* Fix a flaw in resyncing -- a flush is set up but not triggered. +* Ensure metadata and cover art are enabled if metadata support is included at compilation. +* Set convolution defaults even if no configuration file is found. +* Handle the `active_remote_id` token as a string rather than an unsigned 32-bit number -- it seems ROON uses a longer character sequence. + +**Enhancements** +* Added the strings `-alac` and `-jack` to the the version string returned by the `-V` command line option if Apple ALAC decoder support and Jack Audio support are included respectively. +* Cleaned up and simplified the code that handles `FLUSH` requests coming from the player. (Debug messages are still a little verbose.) +* Improved timing estimation. Shairport Sync has been using linear least-squares regression to estimate timing drift between the (remote) source clock and the local clock. This technique is now extended to provide an estimate of the remote-to-local clock difference itself. (The remote-to-local clock difference is used to remap the timing of audio frames from the remote device's clock to the local clock.) + + Timing drift estimates are now saved when a sessions ends, keyed to the client IP number. When a new session starts from that IP number, the stored estimate is used until a new estimate can be generated from the new session. + + In practice, the timing techniques in use up to now have been very accurate, but this should result in slightly smoother rates of correction. +* Tidied up the creation and initial opening of pipes. Suppress repeated pipe-opening error messages. +* Tidied up warnings and fatal error messages when log verbosity is zero. +* Cleaned up the code that provides a silent lead-in to play on a back end without synchronisation, e.g. a pipe or `stdout`. +* Added in commented-out code to check the timeliness of the release of audio to a back end without synchronisation, e.g. a pipe or `stdout`. TL;DR – so long as the back end does not block, frames will be released to it not more than one packet (352 frames) late. +* Logs and statistics can now be directed to the system log (default), `stdout`, `stderr` or to a file or pipe of your choice using a new setting, `log_output_to` in the `diagnostics` section of the configuration file. This is very useful when the system log is disabled or diverted. +* Audio data from the `pipe` back end and metadata from the metadata pipe are now written using standard blocking `write` commands rather than a slightly complex non-blocking write function. Pipes are now opened in non-blocking mode and changed to blocking mode when successfully opened. +* Add a default name for the pipe backend: `/tmp/shairport-sync-audio`. +* Separate threads are now used for each metadata subsystem. Until now, all metadata was processed on a single thread. This included writing to the metadata pipe and the multicast stream and supplying metadata for the `mqtt` interface and for the `dbus` and `MPRIS` interfaces. Unfortunately, that meant that a problem with any one of these subsystems could propagate into the others. Now they all run on separate threads. If one thread blocks, it will not interfere with the other subsystems. +* Cleaned up and improved the code to synchronise the first frame of audio. This should result in more accurate and reliable initial synchronisation, usually to under a millisecond, and often to within 20 or 30 microseconds. Syncronisation should improve even when the silent lead-in time is as short as 0.3 seconds or when the `audio_backend_latency_offset_in_seconds` is as much as -1.7 seconds, i.e. when only 0.3 seconds of latency are left when the latency would normally be 2.0 seconds. +* Cleaned up some confused uses of modulo arithmetic. +* Cleaned up the allocation of memory for gathering running statistics – the heap is now used instead of the stack. +* Cleaned up the display of statistics for backends that do not implement active synchronisation, e.g. the `pipe` and `STDOUT` back ends. +* Cleaned up the `audio_backend_silent_lead_in_time` setting by adding an `"auto"` setting. +* Improved synchronisation accuracy with short silence lead-ins. +* While a player is active, the DACP port number to which to send remote commands should be broadcast over ZEROCONF/Bonjour. However, if that information is not available, Shairport Sync will now check for it every two seconds. +* The timing software in the `sndio` backend does some extra sanity checking on certain time estimates, it may help a little when running on virtual machines. +* Open metadata and audio pipes with 666 permissions to allow them to be shared by other applications. + Version 3.3.6 ==== **New Features** diff --git a/REPORTING ISSUES.md b/REPORTING ISSUES.md index 5c502091..ce0db535 100644 --- a/REPORTING ISSUES.md +++ b/REPORTING ISSUES.md @@ -2,21 +2,21 @@ Reporting Issues and Making Pull Requests ==== -Pull Requests ----- -If you would like to contribute to the development of Shairport Sync, please make you changes to the `development` branch and make a pull request. - -Changes and additions in the development branch make their way eventually to the `master` branch. - Issue Reports ---- -Issue reports are welcome, but before you report an issue, please have a look though the existing [issues](https://github.com/mikebrady/shairport-sync/issues), both open and closed, and check for hints in the [TROUBLESHOOTING](TROUBLESHOOTING.md) page. It would be great to give some details of the device and version of Linux or FreeBSD in use along with the version of Shairport Sync you are using (use `$ shairport-sync -V` to get this). Then, if possible, some diagnostic information from the log or logfile would be useful. +Issue reports are welcome, but before you report an issue, please have a look though the existing [issues](https://github.com/mikebrady/shairport-sync/issues), both open and closed, and check for hints in the [TROUBLESHOOTING](TROUBLESHOOTING.md) page. + +It would be great to give some details of the device and version of Linux or FreeBSD in use along with the version of Shairport Sync you are using. Use: +``` +$ shairport-sync -V +``` +Then, if possible, some diagnostic information from the log or logfile would be useful. If your system uses `systemd`, as most recent Linuxes do, try using: ``` $ sudo systemctl status shairport-sync ``` -You might get something lke this: +You might get something like this: ``` ● shairport-sync.service - Shairport Sync - AirPlay Audio Receiver Loaded: loaded (/lib/systemd/system/shairport-sync.service; disabled; vendor preset: enabled) @@ -41,3 +41,10 @@ code or log file entries ``` \`\`\` +Pull Requests +---- +If you would like to contribute to the development of Shairport Sync, please make you changes to the `development` branch and make a pull request. + +Changes and additions in the development branch make their way eventually to the `master` branch. + + diff --git a/TROUBLESHOOTING.md b/TROUBLESHOOTING.md index fd500e60..23d1ebea 100644 --- a/TROUBLESHOOTING.md +++ b/TROUBLESHOOTING.md @@ -4,9 +4,9 @@ The installation and setup of Shairport Sync is straightforward on recent Linux In this brief document will be listed some problems and some solutions, some provided by other users. -1. Before starting, ensure that your software is up-to-date. -2. Set the `interpolation` in the `general` section of the configuration file to `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). - +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. ### WiFi adapter running in power-saving / low-power mode @@ -54,6 +54,10 @@ sudo chmod +x /etc/network/if-up.d/off-power-manager There are some more details in some the closed issues on this repository. +### VPNs + +To see the AirPlay service Shairport Sync provides, your devices must be on the same subnet as Shairport Sync. If, say, a device such as an iPhone is on a VPN and Shairport Sync is not, or vice-versa, then even though the devices might be physically on the same network, they are effectively on separate networks due to the VPN and the AirPlay service will not be accessible to the device. So, when you are troubleshooting, look out for VPN issues. + ### 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). @@ -67,7 +71,7 @@ You can play from other devices but not from your Windows PC. 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 connections on Raspbian (Raspberry Pi) +### UFW firewall blocking ports (commonly includes Raspberry Pi) **Problem** @@ -87,19 +91,18 @@ You have installed Shairport Sync successfully, the daemon is running, you can s **Solution** -You have to allow connections to your Pi from remote devices. To do so, after re-enabling UFW (see last step of the previous section), enter the following commands in shell: +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 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 ``` -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 ! diff --git a/UNINSTALLING.md b/UNINSTALLING.md deleted file mode 100644 index 33689814..00000000 --- a/UNINSTALLING.md +++ /dev/null @@ -1,21 +0,0 @@ - - -Uninstalling Shairport Sync -==== -This is under construction. - - - - - diff --git a/UPDATING.md b/UPDATING.md index 73e18fb0..cbfdc635 100644 --- a/UPDATING.md +++ b/UPDATING.md @@ -1,13 +1,17 @@ ### Updating Shairport Sync [Needs updating for Airplay 2] -This guide is for updating an installation of Shairport Sync. If you installed Shairport Sync from a package, most of these instructions don't apply – please go to the section at the end called "Post Update Tasks" +This guide is for building and updating an installation of Shairport Sync. + +If you updated Shairport Sync from a package, most of these instructions don't apply – please go to the section at the end called "Post Update Tasks". If you wish to build an installation of Shairport Sync to replace an installation that came from a package, instructions don't apply either – 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. -Here is the sequence for Raspbian Stretch, which is based on Debian Stretch. The same commands work for Ubuntu, and maybe more. Here, a non-`root` user with `sudo` privileges is assumed. +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 @@ -15,7 +19,26 @@ $ sudo apt-get upgrade ``` Next, stop playing music to your device. -Now, to 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: +### 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 diff --git a/audio.c b/audio.c index 0fe7b04f..10efbd9e 100644 --- a/audio.c +++ b/audio.c @@ -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 diff --git a/audio_alsa.c b/audio_alsa.c index ed4f3c88..f723cb7d 100644 --- a/audio_alsa.c +++ b/audio_alsa.c @@ -1,7 +1,7 @@ /* * libalsa output driver. This file is part of Shairport. * Copyright (c) Muffinman, Skaman 2013 - * Copyright (c) Mike Brady 2014 -- 2019 + * Copyright (c) Mike Brady 2014 -- 2021 * All rights reserved. * * Permission is hereby granted, free of charge, to any person @@ -438,7 +438,7 @@ int actual_open_alsa_device(int do_auto_setup) { } else { char errorstring[1024]; strerror_r(-ret, (char *)errorstring, sizeof(errorstring)); - warn("alsa: error %d (\"%s\") opening alsa device \"%s\".", ret, (char *)errorstring, + die("alsa: error %d (\"%s\") opening alsa device \"%s\".", ret, (char *)errorstring, alsa_out_dev); } return ret; @@ -1667,7 +1667,7 @@ int do_play(void *buf, int samples) { } snd_pcm_state_t prior_state = state; // keep this for afterwards.... - debug(3, "alsa: write %d frames.", samples); + // debug(3, "alsa: write %d frames.", samples); ret = alsa_pcm_write(alsa_handle, buf, samples); if (ret == samples) { stall_monitor_frame_count += samples; diff --git a/audio_pw.c b/audio_pw.c new file mode 100644 index 00000000..58a5fd72 --- /dev/null +++ b/audio_pw.c @@ -0,0 +1,501 @@ +/* + * Asynchronous Pipewire Backend. This file is part of Shairport Sync. + * Copyright (c) Shairport Sync 2021 + * 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 +#include +#include +#include +#include + +#include + +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; + } +} + +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"); + } + + 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); + + 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) { + + 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); + 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; + + pw_thread_loop_wait(data.mainloop); + } + + pw_thread_loop_unlock(data.mainloop); +} + +static void stop() { + pw_thread_loop_lock(data.mainloop); + + pw_stream_flush(data.stream, true); + + pw_thread_loop_unlock(data.mainloop); +} + +static void flush() { + pw_thread_loop_lock(data.mainloop); + + pw_stream_flush(data.stream, false); + + pw_thread_loop_unlock(data.mainloop); +} + +static int play(void *buf, int samples) { + struct pw_buffer *pw_buffer; + struct spa_buffer *spa_buffer; + struct spa_data *spa_data; + + 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; + + pw_thread_loop_wait(data.mainloop); + } + + 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); + + 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, + .play = &play, + .volume = NULL, + .parameters = NULL, + .mute = NULL +}; diff --git a/common.c b/common.c index fcbf4fb5..866222c2 100644 --- a/common.c +++ b/common.c @@ -141,7 +141,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); @@ -150,7 +150,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); @@ -210,7 +210,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); @@ -459,7 +459,7 @@ char *generate_preliminary_string(char *buffer, size_t buffer_length, double tss void _die(const char *filename, 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) { @@ -489,7 +489,7 @@ void _die(const char *filename, const int linenumber, const char *format, ...) { void _warn(const char *filename, 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) { @@ -519,7 +519,7 @@ void _debug(const char *filename, const int linenumber, int level, const char *f 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(); @@ -541,7 +541,7 @@ void _debug(const char *filename, const int linenumber, int level, const char *f void _inform(const char *filename, 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) { @@ -1079,7 +1079,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)); diff --git a/common.h b/common.h index 26e9e17d..9c3496f6 100644 --- a/common.h +++ b/common.h @@ -302,7 +302,6 @@ typedef struct { char *airplay_device_id; // for the Bonjour advertisement and the GETINFO PList char *airplay_pin; // non-NULL, 4 char PIN, if required for pairing char *airplay_pi; // UUID in the Bonjour advertisement and the GETINFO Plist - char *airplay_gid; // UUID in the Bonjour advertisement -- initially the same as the pi #endif } shairport_cfg; @@ -379,7 +378,7 @@ 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 +// return a time in nanoseconds // uint64_t get_absolute_time_in_fp(void); // obselete uint64_t get_absolute_time_in_ns(void); diff --git a/configure.ac b/configure.ac index 5e4cf244..ef32fda5 100644 --- a/configure.ac +++ b/configure.ac @@ -305,12 +305,34 @@ 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-dev library!)]) + else + AC_CHECK_LIB([pipewire], [pw_stream_queue_buffer], , AC_MSG_ERROR(Pipewire support requires the libpipewire-dev library.)) + 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 AM_INIT_AUTOMAKE([subdir-objects]) 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"]) diff --git a/documentation/Example Setups.md b/documentation/Example Setups.md deleted file mode 100644 index 7ce6f658..00000000 --- a/documentation/Example Setups.md +++ /dev/null @@ -1,21 +0,0 @@ -Example Setups -=== - -Amazon Echo Devices ---- -Since some Amazon Echo devices have either a 3.5mm input or can be used as a bluetooth speaker, Shairport Sync can be connected allowing you to Airplay to these speakers. Currently using the 3.5mm jack is the most reliable method - bluetooth connectivity can have an unreliable delay. Check first to see if your Echo device supports either of these. Volume control of the Echo device can be added via Home Assistant and a MQTT broker. Below goes through some example setups. -### _Raspberry Pi --> Echo 3.5mm Input_ -* Set the Echo device to use the 3.5mm jack as an input. -* Via the Shairport Sync config, set the correct `output_device` and `mixer_control_name` for your configuration. Likely to be -`output_device = "hw:1";` and `mixer_control_name = "Headphone";`. -* At this point, you should be able to hear music via the Echo. Remember that so far, the volume of the Echo and Shairport Sync are _independent_. If you prefer, you can disable any volume control in Shairport Sync by setting `ignore_volume_control = "yes";`. -* Amazon Echo devices appear to add a bit of latency, currently this is thought to be 0.12 seconds. This can be countered by setting the following: `audio_backend_latency_offset_in_seconds = -0.12;`. This was measured _very_ crudely using Audacity so could be wrong. If anyone is able to determine the latency more accurately, please open an issue or pull request so these documents can be updated. -* You should now be able to use your Echo device in sync with other AirPlay devices! - -### _Raspberry Pi --> Echo as Bluetooth Speaker (Docker Version)_ -* WIP - -### _Raspberry Pi --> Echo with Volume Control_ -* Before continuing with volume control via Home Assistant, please note that currently only one way volume control is tested - Shairport Sync to the Echo device. This means if the volume of the Echo is changed (e.g. by voice control or by the physical controls on the device), this will not be reflected in the volume of Shairport Sync and will be overwritten the next time Shairport Sync's volume is changed. -* This setup requires Home Assistant and a MQTT broker. The basic idea is that Shairport Sync sends a MQTT message containing the new volume amount to the MQTT broker when the volume is changed, this is then picked up by Home Assistant (which is monitoring the MQTT broker). Home Assistant will then send a command to the Amazon Echo device to change the volume. -* WIP diff --git a/mdns.c b/mdns.c index 4e44e38f..9a5465c1 100644 --- a/mdns.c +++ b/mdns.c @@ -109,6 +109,13 @@ void mdns_register(char **txt_records, char **secondary_txt_records) { 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) { diff --git a/mdns.h b/mdns.h index dc6e30d9..d43b9513 100644 --- a/mdns.h +++ b/mdns.h @@ -9,6 +9,7 @@ extern int mdns_pid; void mdns_unregister(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); @@ -19,6 +20,7 @@ typedef struct { char *name; 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 *); diff --git a/mdns_avahi.c b/mdns_avahi.c index bf24ddba..95984419 100644 --- a/mdns_avahi.c +++ b/mdns_avahi.c @@ -318,6 +318,50 @@ static void client_callback(AvahiClient *c, AvahiClientState state, } } +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; + + avahi_threaded_poll_lock(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."); + } + + avahi_threaded_poll_unlock(tpoll); + return 0; +} + static int avahi_register(char *ap1name, char *ap2name, int srvport, char **txt_records, char **secondary_txt_records) { // debug(1, "avahi_register."); @@ -402,7 +446,7 @@ static void avahi_unregister(void) { 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; } @@ -410,27 +454,33 @@ 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; + 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))); + } + 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."); } } } @@ -451,6 +501,7 @@ void avahi_dacp_monitor_stop() { 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, diff --git a/nqptp-shm-structures.h b/nqptp-shm-structures.h new file mode 100644 index 00000000..36c8a813 --- /dev/null +++ b/nqptp-shm-structures.h @@ -0,0 +1,48 @@ +/* + * This file is part of the nqptp distribution (https://github.com/mikebrady/nqptp). + * Copyright (c) 2021 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 . + * + * Commercial licensing is also available. + */ + +#ifndef NQPTP_SHM_STRUCTURES_H +#define NQPTP_SHM_STRUCTURES_H + +#define STORAGE_ID "/nqptp" +#define MAX_CLOCKS 32 +#define NQPTP_SHM_STRUCTURES_VERSION 5 +#define NQPTP_CONTROL_PORT 9000 + +// the control port will accept a UDP packet with the first letter being: +// "T", followed by a space and then a space-delimited +// list of ip numbers, either IPv4 or IPv6 +// the whole not to exceed 4096 characters in total +// The IPs will become the new list of timing peers, replacing any previous + +#include +#include + +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 + uint32_t flags; // unused + 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 diff --git a/pair_ap/pair.h b/pair_ap/pair.h index 37f85b4e..3877e2bb 100644 --- a/pair_ap/pair.h +++ b/pair_ap/pair.h @@ -4,7 +4,7 @@ #include #define PAIR_AP_VERSION_MAJOR 0 -#define PAIR_AP_VERSION_MINOR 7 +#define PAIR_AP_VERSION_MINOR 8 #define PAIR_AP_DEVICE_ID_LEN_MAX 64 diff --git a/pair_ap/pair_homekit.c b/pair_ap/pair_homekit.c index f875d237..bb45df7b 100644 --- a/pair_ap/pair_homekit.c +++ b/pair_ap/pair_homekit.c @@ -281,7 +281,7 @@ calculate_M(enum hash_alg alg, NGConstant *ng, unsigned char *dest, const char * for (i=0; i < hash_len; i++ ) H_xor[i] = H_N[i] ^ H_g[i]; - + hash_init( alg, &ctx ); hash_update( alg, &ctx, H_xor, hash_len ); @@ -410,7 +410,7 @@ srp_user_start_authentication(struct SRPUser *usr, const char **username, // BN_hex2bn(&(usr->a), "D929DFB605687233C9E9030C2280156D03BDB9FDCF3CCE3BC27D9CCFCB5FF6A1"); bnum_modexp(usr->A, usr->ng->g, usr->a, usr->ng->N); - + *len_A = bnum_num_bytes(usr->A); *bytes_A = malloc(*len_A); @@ -515,7 +515,7 @@ static int srp_create_salted_verification_key(enum hash_alg alg, SRP_NGType ng_type, const char *username, const unsigned char *password, int len_password, - unsigned char **bytes_s, int *len_s, + unsigned char **bytes_s, int *len_s, unsigned char **bytes_v, int *len_v, const char *n_hex, const char *g_hex ) { @@ -2764,8 +2764,14 @@ static int server_list_cb(uint8_t public_key[crypto_sign_PUBLICKEYBYTES], const char *device_id, void *cb_arg) { pair_tlv_values_t *response = cb_arg; + pair_tlv_t *previous_id; uint8_t permissions = 1; // Means admin (TODO don't hardcode - let caller set) + // If this isn't the first iteration (item) then we must add a separator + previous_id = pair_tlv_get_value(response, TLVType_Identifier); + if (previous_id) + pair_tlv_add_value(response, TLVType_Separator, NULL, 0); + pair_tlv_add_value(response, TLVType_Identifier, (unsigned char *)device_id, strlen(device_id)); pair_tlv_add_value(response, TLVType_PublicKey, public_key, crypto_sign_PUBLICKEYBYTES); pair_tlv_add_value(response, TLVType_Permissions, &permissions, sizeof(permissions)); diff --git a/player.c b/player.c index e2c7e801..ac332a43 100644 --- a/player.c +++ b/player.c @@ -1760,27 +1760,25 @@ void *player_thread_func(void *arg) { // successive rtptimes, at worst conn->output_sample_ratio = config.output_rate / conn->input_rate; - - -// Sign extending rtptime calculations to 64 bit is needed from time to time. -// The standard rtptime is unsigned 32 bits, -// so you can do modulo 2^32 difference calculations -// and get a signed result simply by typing the result as a signed 32-bit number. + // Sign extending rtptime calculations to 64 bit is needed from time to time. -// So long as you can be sure the numbers are within 2^31 of each other, -// the sign of the result calculated in this way indicates the order of the operands. -// For example, if you subtract a from b and the result is positive, you can conclude -// b is the same as or comes after a in module 2^32 order. + // The standard rtptime is unsigned 32 bits, + // so you can do modulo 2^32 difference calculations + // and get a signed result simply by typing the result as a signed 32-bit number. -// We want to do the same with the rtptime calculations for multiples of -// the rtptimes (1, 2, 4 or 8 times), and we want to do this in signed 64-bit/ -// Therefore we need to sign extend these modulo 2^32, 2^33, 2^34, or 2^35 bit unsigned -// numbers on the same basis. + // So long as you can be sure the numbers are within 2^31 of each other, + // the sign of the result calculated in this way indicates the order of the operands. + // For example, if you subtract a from b and the result is positive, you can conclude + // b is the same as or comes after a in module 2^32 order. -// That is what the output_rtptime_sign_bit, output_rtptime_mask, output_rtptime_mask_not and -// output_rtptime_sign_mask are for -- see later, calculating the sync error. + // We want to do the same with the rtptime calculations for multiples of + // the rtptimes (1, 2, 4 or 8 times), and we want to do this in signed 64-bit/ + // Therefore we need to sign extend these modulo 2^32, 2^33, 2^34, or 2^35 bit unsigned + // numbers on the same basis. + // That is what the output_rtptime_sign_bit, output_rtptime_mask, output_rtptime_mask_not and + // output_rtptime_sign_mask are for -- see later, calculating the sync error. int output_rtptime_sign_bit; switch (conn->output_sample_ratio) { @@ -2022,7 +2020,6 @@ void *player_thread_func(void *arg) { } #endif - #ifdef CONFIG_AIRPLAY_2 if (conn->timing_type == ts_ntp) { #endif @@ -2940,168 +2937,169 @@ void player_volume_without_notification(double airplay_volume, rtsp_conn_info *c // if it's both, we haven't decided whether hw or sw should be on top // we have to consider the settings ignore_volume_control and mute. - if (airplay_volume == -144.0) { - - if ((config.output->mute) && (config.output->mute(1) == 0)) - debug(2, - "player_volume_without_notification: volume mode is %d, airplay_volume is %f, " - "hardware mute is enabled.", - volume_mode, airplay_volume); - else { - conn->software_mute_enabled = 1; - debug(2, - "player_volume_without_notification: volume mode is %d, airplay_volume is %f, " - "software mute is enabled.", - volume_mode, airplay_volume); - } - - } else { - int32_t max_db = 0, min_db = 0; - switch (volume_mode) { - case vol_hw_only: - max_db = hw_max_db; - min_db = hw_min_db; - break; - case vol_sw_only: - max_db = sw_max_db; - min_db = sw_min_db; - break; - case vol_both: - // debug(1, "dB range passed is hw: %d, sw: %d, total: %d", hw_max_db - hw_min_db, - // sw_max_db - sw_min_db, (hw_max_db - hw_min_db) + (sw_max_db - sw_min_db)); - max_db = - (hw_max_db - hw_min_db) + (sw_max_db - sw_min_db); // this should be the range requested - min_db = 0; - break; - default: - debug(1, "player_volume_without_notification: error: not in a volume mode"); - break; - } - double scaled_attenuation = max_db; - if (config.ignore_volume_control == 0) { - if (config.volume_control_profile == VCP_standard) - scaled_attenuation = vol2attn(airplay_volume, max_db, min_db); // no cancellation points - else if (config.volume_control_profile == VCP_flat) - scaled_attenuation = - flat_vol2attn(airplay_volume, max_db, min_db); // no cancellation points - else - debug(1, "player_volume_without_notification: unrecognised volume control profile"); - } - - // so here we have the scaled attenuation. If it's for hw or sw only, it's straightforward. - double hardware_attenuation = 0.0; - double software_attenuation = 0.0; - - switch (volume_mode) { - case vol_hw_only: - hardware_attenuation = scaled_attenuation; - break; - case vol_sw_only: - software_attenuation = scaled_attenuation; - break; - case vol_both: - // here, we now the attenuation required, so we have to apportion it to the sw and hw mixers - // if we give the hw priority, that means when lowering the volume, set the hw volume to its - // lowest - // before using the sw attenuation. - // similarly, if we give the sw priority, that means when lowering the volume, set the sw - // volume to its lowest - // before using the hw attenuation. - // one imagines that hw priority is likely to be much better - // if (config.volume_range_hw_priority) { - if (config.volume_range_hw_priority != 0) { - // hw priority - if ((sw_max_db - sw_min_db) > scaled_attenuation) { - software_attenuation = sw_min_db + scaled_attenuation; - hardware_attenuation = hw_min_db; - } else { - software_attenuation = sw_max_db; - hardware_attenuation = hw_min_db + scaled_attenuation - (sw_max_db - sw_min_db); - } - } else { - // sw priority - if ((hw_max_db - hw_min_db) > scaled_attenuation) { - hardware_attenuation = hw_min_db + scaled_attenuation; - software_attenuation = sw_min_db; - } else { - hardware_attenuation = hw_max_db; - software_attenuation = sw_min_db + scaled_attenuation - (hw_max_db - hw_min_db); - } - } - break; - default: - debug(1, "player_volume_without_notification: error: not in a volume mode"); - break; - } - - if (((volume_mode == vol_hw_only) || (volume_mode == vol_both)) && (config.output->volume)) { - config.output->volume(hardware_attenuation); // otherwise set the output to the lowest value - // debug(1,"Hardware attenuation set to %f for airplay volume of - // %f.",hardware_attenuation,airplay_volume); - if (volume_mode == vol_hw_only) - conn->fix_volume = 0x10000; - } - - if ((volume_mode == vol_sw_only) || (volume_mode == vol_both)) { - double temp_fix_volume = 65536.0 * pow(10, software_attenuation / 2000); - if (config.ignore_volume_control == 0) - debug(2,"Software attenuation set to %f, i.e %f out of 65,536, for airplay volume of %f",software_attenuation,temp_fix_volume,airplay_volume); - else - debug(2,"Software attenuation set to %f, i.e %f out of 65,536. Volume control is ignored.",software_attenuation,temp_fix_volume); - - conn->fix_volume = temp_fix_volume; - - // if (config.loudness) - loudness_set_volume(software_attenuation / 100); - } - - if (config.logOutputLevel) { - inform("Output Level set to: %.2f dB.", scaled_attenuation / 100.0); - } - -#ifdef CONFIG_METADATA - // here, send the 'pvol' metadata message when the airplay volume information - // is being used by shairport sync to control the output volume - char dv[128]; - memset(dv, 0, 128); - if (volume_mode == vol_both) { - // normalise the maximum output to the hardware device's max output - snprintf(dv, 127, "%.2f,%.2f,%.2f,%.2f", airplay_volume, - (scaled_attenuation - max_db + hw_max_db) / 100.0, - (min_db - max_db + hw_max_db) / 100.0, (max_db - max_db + hw_max_db) / 100.0); - } else { - snprintf(dv, 127, "%.2f,%.2f,%.2f,%.2f", airplay_volume, scaled_attenuation / 100.0, - min_db / 100.0, max_db / 100.0); - } - send_ssnc_metadata('pvol', dv, strlen(dv), 1); -#endif - - if (config.output->mute) - config.output->mute(0); - conn->software_mute_enabled = 0; + if (airplay_volume == -144.0) { + if ((config.output->mute) && (config.output->mute(1) == 0)) debug(2, - "player_volume_without_notification: volume mode is %d, airplay volume is %f, " - "software_attenuation: %f, hardware_attenuation: %f, muting " - "is disabled.", - volume_mode, airplay_volume, software_attenuation, hardware_attenuation); + "player_volume_without_notification: volume mode is %d, airplay_volume is %f, " + "hardware mute is enabled.", + volume_mode, airplay_volume); + else { + conn->software_mute_enabled = 1; + debug(2, + "player_volume_without_notification: volume mode is %d, airplay_volume is %f, " + "software mute is enabled.", + volume_mode, airplay_volume); } - /* - } + } else { + int32_t max_db = 0, min_db = 0; + switch (volume_mode) { + case vol_hw_only: + max_db = hw_max_db; + min_db = hw_min_db; + break; + case vol_sw_only: + max_db = sw_max_db; + min_db = sw_min_db; + break; + case vol_both: + // debug(1, "dB range passed is hw: %d, sw: %d, total: %d", hw_max_db - hw_min_db, + // sw_max_db - sw_min_db, (hw_max_db - hw_min_db) + (sw_max_db - sw_min_db)); + max_db = + (hw_max_db - hw_min_db) + (sw_max_db - sw_min_db); // this should be the range requested + min_db = 0; + break; + default: + debug(1, "player_volume_without_notification: error: not in a volume mode"); + break; + } + double scaled_attenuation = max_db; + if (config.ignore_volume_control == 0) { + + if (config.volume_control_profile == VCP_standard) + scaled_attenuation = vol2attn(airplay_volume, max_db, min_db); // no cancellation points + else if (config.volume_control_profile == VCP_flat) + scaled_attenuation = + flat_vol2attn(airplay_volume, max_db, min_db); // no cancellation points + else + debug(1, "player_volume_without_notification: unrecognised volume control profile"); + } + // so here we have the scaled attenuation. If it's for hw or sw only, it's straightforward. + double hardware_attenuation = 0.0; + double software_attenuation = 0.0; + + switch (volume_mode) { + case vol_hw_only: + hardware_attenuation = scaled_attenuation; + break; + case vol_sw_only: + software_attenuation = scaled_attenuation; + break; + case vol_both: + // here, we now the attenuation required, so we have to apportion it to the sw and hw mixers + // if we give the hw priority, that means when lowering the volume, set the hw volume to its + // lowest + // before using the sw attenuation. + // similarly, if we give the sw priority, that means when lowering the volume, set the sw + // volume to its lowest + // before using the hw attenuation. + // one imagines that hw priority is likely to be much better + // if (config.volume_range_hw_priority) { + if (config.volume_range_hw_priority != 0) { + // hw priority + if ((sw_max_db - sw_min_db) > scaled_attenuation) { + software_attenuation = sw_min_db + scaled_attenuation; + hardware_attenuation = hw_min_db; + } else { + software_attenuation = sw_max_db; + hardware_attenuation = hw_min_db + scaled_attenuation - (sw_max_db - sw_min_db); + } + } else { + // sw priority + if ((hw_max_db - hw_min_db) > scaled_attenuation) { + hardware_attenuation = hw_min_db + scaled_attenuation; + software_attenuation = sw_min_db; + } else { + hardware_attenuation = hw_max_db; + software_attenuation = sw_min_db + scaled_attenuation - (hw_max_db - hw_min_db); + } + } + break; + default: + debug(1, "player_volume_without_notification: error: not in a volume mode"); + break; + } + + if (((volume_mode == vol_hw_only) || (volume_mode == vol_both)) && (config.output->volume)) { + config.output->volume(hardware_attenuation); // otherwise set the output to the lowest value + // debug(1,"Hardware attenuation set to %f for airplay volume of + // %f.",hardware_attenuation,airplay_volume); + if (volume_mode == vol_hw_only) + conn->fix_volume = 0x10000; + } + + if ((volume_mode == vol_sw_only) || (volume_mode == vol_both)) { + double temp_fix_volume = 65536.0 * pow(10, software_attenuation / 2000); + + if (config.ignore_volume_control == 0) + debug(2, "Software attenuation set to %f, i.e %f out of 65,536, for airplay volume of %f", + software_attenuation, temp_fix_volume, airplay_volume); + else + debug(2, "Software attenuation set to %f, i.e %f out of 65,536. Volume control is ignored.", + software_attenuation, temp_fix_volume); + + conn->fix_volume = temp_fix_volume; + + // if (config.loudness) + loudness_set_volume(software_attenuation / 100); + } + + if (config.logOutputLevel) { + inform("Output Level set to: %.2f dB.", scaled_attenuation / 100.0); + } #ifdef CONFIG_METADATA - else { // here, send the 'pvol' metadata message when the airplay volume information // is being used by shairport sync to control the output volume char dv[128]; memset(dv, 0, 128); - snprintf(dv, 127, "%.2f,%.2f,%.2f,%.2f", airplay_volume, 0.0, 0.0, 0.0); + if (volume_mode == vol_both) { + // normalise the maximum output to the hardware device's max output + snprintf(dv, 127, "%.2f,%.2f,%.2f,%.2f", airplay_volume, + (scaled_attenuation - max_db + hw_max_db) / 100.0, + (min_db - max_db + hw_max_db) / 100.0, (max_db - max_db + hw_max_db) / 100.0); + } else { + snprintf(dv, 127, "%.2f,%.2f,%.2f,%.2f", airplay_volume, scaled_attenuation / 100.0, + min_db / 100.0, max_db / 100.0); + } send_ssnc_metadata('pvol', dv, strlen(dv), 1); - } #endif -*/ + if (config.output->mute) + config.output->mute(0); + conn->software_mute_enabled = 0; + + debug(2, + "player_volume_without_notification: volume mode is %d, airplay volume is %f, " + "software_attenuation: %f, hardware_attenuation: %f, muting " + "is disabled.", + volume_mode, airplay_volume, software_attenuation, hardware_attenuation); + } + /* + } + + #ifdef CONFIG_METADATA + else { + // here, send the 'pvol' metadata message when the airplay volume information + // is being used by shairport sync to control the output volume + char dv[128]; + memset(dv, 0, 128); + snprintf(dv, 127, "%.2f,%.2f,%.2f,%.2f", airplay_volume, 0.0, 0.0, 0.0); + send_ssnc_metadata('pvol', dv, strlen(dv), 1); + } + #endif + */ // here, store the volume for possible use in the future config.airplay_volume = airplay_volume; debug_mutex_unlock(&conn->volume_control_mutex, 3); diff --git a/player.h b/player.h index c35e5726..2097dfdd 100644 --- a/player.h +++ b/player.h @@ -101,6 +101,15 @@ 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; @@ -146,7 +155,6 @@ typedef struct { 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 fd; int authorized; // set if a password is required and has been supplied char *auth_nonce; // the session nonce, if needed @@ -287,7 +295,12 @@ typedef struct { 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? @@ -328,13 +341,13 @@ typedef struct { uint16_t local_realtime_audio_port; uint16_t local_buffered_audio_port; - plist_t client_setup_plist; 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 + uint64_t networkTimeTimelineID; // the clock ID used by the player + uint8_t groupContainsGroupLeader; // information coming from the SETUP char *ap2_timing_peer_list_message; diff --git a/ptp-utilities.c b/ptp-utilities.c index a9cf614f..0f80e913 100644 --- a/ptp-utilities.c +++ b/ptp-utilities.c @@ -41,7 +41,7 @@ #include #include -#include "../nqptp/nqptp-shm-structures.h" +#include "nqptp-shm-structures.h" #include "ptp-utilities.h" int shm_fd; diff --git a/rtp.c b/rtp.c index 1b2cffad..86792369 100644 --- a/rtp.c +++ b/rtp.c @@ -1271,18 +1271,21 @@ int long_time_notifcation_done = 0; int get_ptp_anchor_local_time_info(rtsp_conn_info *conn, uint32_t *anchorRTP, uint64_t *anchorLocalTime) { uint64_t actual_clock_id, actual_time_of_sample, actual_offset, start_of_mastership; - int response = ptp_get_clock_info(&actual_clock_id, &actual_time_of_sample, &actual_offset, &start_of_mastership); + int response = ptp_get_clock_info(&actual_clock_id, &actual_time_of_sample, &actual_offset, + &start_of_mastership); if (response == clock_ok) { uint64_t time_now = get_absolute_time_in_ns(); int64_t time_since_sample = time_now - actual_time_of_sample; if (time_since_sample > 10000000000) { if (long_time_notifcation_done == 0) { - debug(1,"The last PTP timing sample is pretty old: %f seconds.", 0.000000001 * time_since_sample); + debug(1, "The last PTP timing sample is pretty old: %f seconds.", + 0.000000001 * time_since_sample); long_time_notifcation_done = 1; } } else if ((time_since_sample < 2000000000) && (long_time_notifcation_done != 0)) { - debug(1,"The last PTP timing sample is no longer too old: %f seconds.", 0.000000001 * time_since_sample); + debug(1, "The last PTP timing sample is no longer too old: %f seconds.", + 0.000000001 * time_since_sample); long_time_notifcation_done = 0; } @@ -1296,11 +1299,11 @@ int get_ptp_anchor_local_time_info(rtsp_conn_info *conn, uint32_t *anchorRTP, // using the master clock timing if (actual_clock_id == conn->anchor_clock) { - // if the master clock and the anchor clock are the same - // wait at least this time before using the new master clock - // note that mastershipo may be backdated + // if the master clock and the anchor clock are the same + // wait at least this time before using the new master clock + // note that mastershipo may be backdated if (duration_of_mastership < 1500000000) { - debug(3,"master not old enough yet: %f ms", 0.000001 * duration_of_mastership); + debug(3, "master not old enough yet: %f ms", 0.000001 * duration_of_mastership); response = clock_not_ready; } else if ((duration_of_mastership > 5000000000) || (conn->last_anchor_info_is_valid == 0)) { @@ -1314,7 +1317,8 @@ int get_ptp_anchor_local_time_info(rtsp_conn_info *conn, uint32_t *anchorRTP, conn->last_anchor_info_is_valid = 1; if (conn->anchor_clock_is_new != 0) debug(1, - "Connection %d: Clock %" PRIx64 " is now the new anchor clock and master clock. History: %f milliseconds.", + "Connection %d: Clock %" PRIx64 + " is now the new anchor clock and master clock. History: %f milliseconds.", conn->connection_number, conn->anchor_clock, 0.000001 * duration_of_mastership); conn->anchor_clock_is_new = 0; } @@ -1326,16 +1330,19 @@ int get_ptp_anchor_local_time_info(rtsp_conn_info *conn, uint32_t *anchorRTP, // so, if the anchor has not changed, it must be that the master clock has changed if (conn->anchor_clock_is_new != 0) - debug(1, + debug(3, "Connection %d: Anchor clock has changed to %" PRIx64 ", master clock is: %" PRIx64 ". History: %f milliseconds.", - conn->connection_number, conn->anchor_clock, actual_clock_id, 0.000001 * duration_of_mastership); + conn->connection_number, conn->anchor_clock, actual_clock_id, + 0.000001 * duration_of_mastership); if ((conn->last_anchor_info_is_valid != 0) && (conn->anchor_clock_is_new == 0)) { int64_t time_since_last_update = get_absolute_time_in_ns() - conn->last_anchor_time_of_update; if (time_since_last_update > 5000000000) { - debug(1, "Connection %d: Master clock has changed to %" PRIx64 ". History: %f milliseconds.", + debug(1, + "Connection %d: Master clock has changed to %" PRIx64 + ". History: %f milliseconds.", conn->connection_number, actual_clock_id, 0.000001 * duration_of_mastership); // here we adjust the time of the anchor rtptime // we know its local time, so we use the new clocks's offset to @@ -1557,6 +1564,7 @@ void rtp_ap2_control_handler_cleanup_handler(void *arg) { rtsp_conn_info *conn = (rtsp_conn_info *)arg; debug(2, "Connection %d: AP2 Control Receiver Cleanup.", conn->connection_number); close(conn->ap2_control_socket); + debug(1, "Connection %d: UDP control port %u closed.", conn->connection_number, conn->local_ap2_control_port); conn->ap2_control_socket = 0; conn->ap2_remote_control_socket_addr_length = 0; // indicates to the control receiver thread that the socket address need to be @@ -1741,8 +1749,9 @@ void *rtp_ap2_control_receiver(void *arg) { void rtp_realtime_audio_cleanup_handler(__attribute__((unused)) void *arg) { debug(2, "Realtime Audio Receiver Cleanup Start."); - rtsp_conn_info *conn = (rtsp_conn_info *)arg; + rtsp_conn_info *conn = (rtsp_conn_info *)arg; close(conn->realtime_audio_socket); + debug(1, "Connection %d: closing realtime audio port %u", conn->local_realtime_audio_port); conn->realtime_audio_socket = 0; debug(2, "Realtime Audio Receiver Cleanup Done."); } @@ -2021,6 +2030,7 @@ void rtp_buffered_audio_cleanup_handler(__attribute__((unused)) void *arg) { debug(2, "Buffered Audio Receiver Cleanup Start."); rtsp_conn_info *conn = (rtsp_conn_info *)arg; close(conn->buffered_audio_socket); + debug(1, "Connection %d: TCP Buffered Audio port closed: %u.", conn->connection_number, conn->local_buffered_audio_port); conn->buffered_audio_socket = 0; debug(2, "Buffered Audio Receiver Cleanup Done."); } @@ -2107,8 +2117,12 @@ void *rtp_buffered_audio_processor(void *arg) { // initialize all muxers, demuxers and protocols for libavformat // (does nothing if called twice during the course of one program execution) - // not needed in ffmpeg 4.0 and later... - // av_register_all(); + // deprecated in ffmpeg 4.0 and later... but still needed in ffmpeg 3.6 / ubuntu 18 + + #pragma GCC diagnostic push + #pragma GCC diagnostic ignored "-Wdeprecated-declarations" + avcodec_register_all(); + #pragma GCC diagnostic pop AVCodec *codec = avcodec_find_decoder(AV_CODEC_ID_AAC); if (codec == NULL) { @@ -2157,7 +2171,8 @@ void *rtp_buffered_audio_processor(void *arg) { av_opt_set_int(swr, "in_channel_layout", AV_CH_LAYOUT_STEREO, 0); av_opt_set_int(swr, "out_channel_layout", AV_CH_LAYOUT_STEREO, 0); av_opt_set_int(swr, "in_sample_rate", conn->input_rate, 0); - av_opt_set_int(swr, "out_sample_rate", conn->input_rate, 0); // must match or the timing will be wrong` + av_opt_set_int(swr, "out_sample_rate", conn->input_rate, + 0); // must match or the timing will be wrong` av_opt_set_sample_fmt(swr, "in_sample_fmt", AV_SAMPLE_FMT_FLTP, 0); enum AVSampleFormat av_format; @@ -2280,9 +2295,14 @@ void *rtp_buffered_audio_processor(void *arg) { if ((blocks_read != 0) && (seq_no >= flushUntilSeq)) { // we have reached or overshot the flushUntilSeq block if (flushUntilSeq != seq_no) - debug(2, "flush request ended with flushUntilSeq %u overshot at %u, flushUntilTS: %u, incoming timestamp: %u.", flushUntilSeq, seq_no, flushUntilTS, timestamp); + debug(2, + "flush request ended with flushUntilSeq %u overshot at %u, flushUntilTS: %u, " + "incoming timestamp: %u.", + flushUntilSeq, seq_no, flushUntilTS, timestamp); else - debug(2, "flush request ended with flushUntilSeq, flushUntilTS: %u, incoming timestamp: %u", flushUntilSeq, flushUntilTS, timestamp); + debug(2, + "flush request ended with flushUntilSeq, flushUntilTS: %u, incoming timestamp: %u", + flushUntilSeq, flushUntilTS, timestamp); conn->ap2_flush_requested = 0; flush_request_active = 0; flush_newly_requested = 0; @@ -2301,7 +2321,6 @@ void *rtp_buffered_audio_processor(void *arg) { if (flush_newly_complete) { debug(2, "Flush Complete."); blocks_read_since_flush = 0; - } if (play_newly_stopped != 0) @@ -2329,9 +2348,10 @@ void *rtp_buffered_audio_processor(void *arg) { // is there space in the player thread's buffer system? unsigned int player_buffer_size, player_buffer_occupancy; get_audio_buffer_size_and_occupancy(&player_buffer_size, &player_buffer_occupancy, conn); - //debug(1,"player buffer size and occupancy: %u and %u", player_buffer_size, player_buffer_occupancy); - if (player_buffer_occupancy > - ((requested_lead_time + 0.4) * conn->input_rate / 352)) { // must be greater than the lead time. + // debug(1,"player buffer size and occupancy: %u and %u", player_buffer_size, + // player_buffer_occupancy); + if (player_buffer_occupancy > ((requested_lead_time + 0.4) * conn->input_rate / + 352)) { // must be greater than the lead time. // if there is enough stuff in the player's buffer, sleep for a while and try again usleep(1000); // wait for a while } else { @@ -2358,26 +2378,26 @@ void *rtp_buffered_audio_processor(void *arg) { // it seems that some garbage blocks can be left after the flush, so // only accept them if they have sensible lead times if ((lead_time_ms < 5000.0) && (lead_time > -1000.0)) { - // if it's the very first block (thus no priming needed) - if ((blocks_read == 1) || (blocks_read_since_flush > 3)) { - if ((lead_time >= (int64_t)(requested_lead_time * 1000000000)) || - (streaming_has_started != 0)) { - if (streaming_has_started == 0) - debug( - 2, - "Connection %d: buffered audio starting frame: %u, lead time: %f seconds.", - conn->connection_number, pcm_buffer_read_point_rtptime, - 0.000000001 * lead_time); - // else { - // if (expected_rtptime != pcm_buffer_read_point_rtptime) - // debug(1,"actual rtptime is %u, expected rtptime is %u.", - // pcm_buffer_read_point_rtptime, expected_rtptime); - //} - // expected_rtptime = pcm_buffer_read_point_rtptime + 352; + // if it's the very first block (thus no priming needed) + if ((blocks_read == 1) || (blocks_read_since_flush > 3)) { + if ((lead_time >= (int64_t)(requested_lead_time * 1000000000)) || + (streaming_has_started != 0)) { + if (streaming_has_started == 0) + debug(2, + "Connection %d: buffered audio starting frame: %u, lead time: %f " + "seconds.", + conn->connection_number, pcm_buffer_read_point_rtptime, + 0.000000001 * lead_time); + // else { + // if (expected_rtptime != pcm_buffer_read_point_rtptime) + // debug(1,"actual rtptime is %u, expected rtptime is %u.", + // pcm_buffer_read_point_rtptime, expected_rtptime); + //} + // expected_rtptime = pcm_buffer_read_point_rtptime + 352; - // this is a diagnostic for introducing a timing error that will force the - // processing chain to resync - // clang-format off + // this is a diagnostic for introducing a timing error that will force the + // processing chain to resync + // clang-format off /* if ((not_first_time_out == 0) && (blocks_read >= 20)) { int timing_error = 150; @@ -2390,21 +2410,23 @@ void *rtp_buffered_audio_processor(void *arg) { not_first_time_out = 1; } */ - // clang-format on + // clang-format on - player_put_packet(0, 0, pcm_buffer_read_point_rtptime, - pcm_buffer + pcm_buffer_read_point, 352, conn); - streaming_has_started++; + player_put_packet(0, 0, pcm_buffer_read_point_rtptime, + pcm_buffer + pcm_buffer_read_point, 352, conn); + streaming_has_started++; + } } - } } else { - debug(2,"Dropping packet %u from block %u with out-of-range lead_time: %.3f seconds.", pcm_buffer_read_point_rtptime, seq_no, 0.001 * lead_time_ms); + debug(2, + "Dropping packet %u from block %u with out-of-range lead_time: %.3f seconds.", + pcm_buffer_read_point_rtptime, seq_no, 0.001 * lead_time_ms); } pcm_buffer_read_point_rtptime += 352; pcm_buffer_read_point += 352 * conn->input_bytes_per_frame; } else { - debug(1,"frame to local time error"); + debug(1, "frame to local time error"); } } else { usleep(1000); // wait before asking if play is enabled again @@ -2488,8 +2510,8 @@ void *rtp_buffered_audio_processor(void *arg) { // valid int64_t lead_time = should_be_time - get_absolute_time_in_ns(); debug(1, - "flush completed to seq: %u, flushUntilTS; %u with rtptime: %u, lead time: 0x%" PRIx64 - " nanoseconds, i.e. %f sec.", + "flush completed to seq: %u, flushUntilTS; %u with rtptime: %u, lead time: " + "0x%" PRIx64 " nanoseconds, i.e. %f sec.", seq_no, flushUntilTS, timestamp, lead_time, lead_time * 0.000000001); } else { debug(2, "flush completed to seq: %u with rtptime: %u.", seq_no, timestamp); diff --git a/rtsp.c b/rtsp.c index 7f9f7e61..3d16aea1 100644 --- a/rtsp.c +++ b/rtsp.c @@ -50,8 +50,8 @@ #include -#include "config.h" #include "activity_monitor.h" +#include "config.h" #ifdef CONFIG_OPENSSL #include @@ -92,6 +92,28 @@ #include "ptp-utilities.h" #endif +#include "mdns.h" + +// mDNS advertisement strings + +// Create these strings and then keep them updated. +// When necessary, update the mDNS service records, using e.g. Avahi +// from these sources. + +char *txt_records[64]; +char *secondary_txt_records[64]; + +char firmware_version[64]; +char ap1_featuresString[64]; +char pkString[128]; +#ifdef CONFIG_AIRPLAY_2 +char deviceIdString[64]; +char featuresString[64]; +char statusflagsString[32]; +char piString[64]; +char gidString[64]; +#endif + #define METADATA_SNDBUF (4 * 1024 * 1024) enum rtsp_read_request_response { @@ -108,9 +130,12 @@ rtsp_conn_info **conns; int metadata_running = 0; -// always lock use this when accessing the playing conn value +// always lock this when accessing the playing conn value pthread_mutex_t playing_conn_lock = PTHREAD_MUTEX_INITIALIZER; +// always lock this when accessing the list of connection threads +pthread_mutex_t conns_lock = PTHREAD_MUTEX_INITIALIZER; + // every time we want to retain or release a reference count, lock it with this // if a reference count is read as zero, it means the it's being deallocated. static pthread_mutex_t reference_counter_lock = PTHREAD_MUTEX_INITIALIZER; @@ -158,6 +183,131 @@ typedef struct { int respcode; } rtsp_message; +#ifdef CONFIG_AIRPLAY_2 + +static void pkString_make(char *str, size_t str_size, const char *device_id) { + uint8_t public_key[32]; + if (str_size < 2 * sizeof(public_key) + 1) { + warn("Insufficient string size"); + str[0] = '\0'; + return; + } + pair_public_key_get(PAIR_SERVER_HOMEKIT, public_key, device_id); + char *ptr = str; + for (size_t i = 0; i < sizeof(public_key); i++) + ptr += sprintf(ptr, "%02x", public_key[i]); +} +#endif + +#ifdef CONFIG_AIRPLAY_2 +void build_bonjour_strings(rtsp_conn_info *conn) { +#else +void build_bonjour_strings(__attribute((unused)) rtsp_conn_info *conn) { +#endif + + int entry_number = 0; + + // make up a firmware version +#ifdef CONFIG_USE_GIT_VERSION_STRING + if (git_version_string[0] != '\0') + snprintf(firmware_version, sizeof(firmware_version), "fv=%s", git_version_string); + else +#endif + snprintf(firmware_version, sizeof(firmware_version), "fv=%s", PACKAGE_VERSION); + +#ifdef CONFIG_AIRPLAY_2 + uint64_t features_hi = config.airplay_features; + features_hi = (features_hi >> 32) & 0xffffffff; + uint64_t features_lo = config.airplay_features; + features_lo = features_lo & 0xffffffff; + snprintf(ap1_featuresString, sizeof(ap1_featuresString), "ft=0x%" PRIX64 ",0x%" PRIX64 "", + features_lo, features_hi); + snprintf(pkString, sizeof(pkString), "pk="); + pkString_make(pkString + strlen("pk="), sizeof(pkString) - strlen("pk="), + config.airplay_device_id); + + txt_records[entry_number++] = "cn=0,1"; + txt_records[entry_number++] = "da=true"; + txt_records[entry_number++] = "et=0,4"; + txt_records[entry_number++] = ap1_featuresString; + txt_records[entry_number++] = firmware_version; + txt_records[entry_number++] = "md=2"; + txt_records[entry_number++] = "am=Shairport Sync"; + txt_records[entry_number++] = "sf=0x4"; + txt_records[entry_number++] = "tp=UDP"; + txt_records[entry_number++] = "vn=65537"; + txt_records[entry_number++] = "vs=366.0"; + txt_records[entry_number++] = pkString; + txt_records[entry_number++] = NULL; + +#else + // here, just replicate what happens in mdns.h when using those #defines + txt_records[entry_number++] = "sf=0x4"; + txt_records[entry_number++] = firmware_version; + txt_records[entry_number++] = "am=ShairportSync"; + txt_records[entry_number++] = "vs=105.1"; + txt_records[entry_number++] = "tp=TCP,UDP"; + txt_records[entry_number++] = "vn=65537"; +#ifdef CONFIG_METADATA + if (config.get_coverart == 0) + txt_records[entry_number++] = "md=0,2"; + else + txt_records[entry_number++] = "md=0,1,2"; +#endif + txt_records[entry_number++] = "ss=16"; + txt_records[entry_number++] = "sr=44100"; + txt_records[entry_number++] = "da=true"; + txt_records[entry_number++] = "sv=false"; + txt_records[entry_number++] = "et=0,1"; + txt_records[entry_number++] = "ek=1"; + txt_records[entry_number++] = "cn=0,1"; + txt_records[entry_number++] = "ch=2"; + txt_records[entry_number++] = "txtvers=1"; + if (config.password == 0) + txt_records[entry_number++] = "pw=false"; + else + txt_records[entry_number++] = "pw=true"; + txt_records[entry_number++] = NULL; +#endif + +#ifdef CONFIG_AIRPLAY_2 + // make up a secondary set of text records + entry_number = 0; + + secondary_txt_records[entry_number++] = "srcvers=366.0"; + snprintf(deviceIdString, sizeof(deviceIdString), "deviceid=%s", config.airplay_device_id); + secondary_txt_records[entry_number++] = deviceIdString; + snprintf(featuresString, sizeof(featuresString), "features=0x%" PRIX64 ",0x%" PRIX64 "", + features_lo, features_hi); + secondary_txt_records[entry_number++] = featuresString; + snprintf(statusflagsString, sizeof(statusflagsString), "flags=0x%" PRIX32, + config.airplay_statusflags); + + secondary_txt_records[entry_number++] = statusflagsString; + secondary_txt_records[entry_number++] = "protovers=1.1"; + secondary_txt_records[entry_number++] = "acl=0"; + secondary_txt_records[entry_number++] = "rsf=0x0"; + secondary_txt_records[entry_number++] = firmware_version; + secondary_txt_records[entry_number++] = "model=Shairport Sync"; + snprintf(piString, sizeof(piString), "pi=%s", config.airplay_pi); + secondary_txt_records[entry_number++] = piString; + if ((conn != NULL) && (conn->airplay_gid != 0)) { + snprintf(gidString, sizeof(gidString), "gid=%s", conn->airplay_gid); + } else { + snprintf(gidString, sizeof(gidString), "gid=%s", config.airplay_pi); + } + secondary_txt_records[entry_number++] = gidString; + if ((conn != NULL) && (conn->groupContainsGroupLeader != 0)) + secondary_txt_records[entry_number++] = "gcgl=1"; + else + secondary_txt_records[entry_number++] = "gcgl=0"; + if ((conn != NULL) && (conn->airplay_gid != 0)) // if it's in a group + secondary_txt_records[entry_number++] = "isGroupLeader=0"; + secondary_txt_records[entry_number++] = pkString; + secondary_txt_records[entry_number++] = NULL; +#endif +} + #ifdef CONFIG_METADATA typedef struct { uint32_t type; @@ -472,46 +622,76 @@ void *player_watchdog_thread_code(void *arg) { // keep track of the threads we have spawned so we can join() them static int nconns = 0; static void track_thread(rtsp_conn_info *conn) { - conns = realloc(conns, sizeof(rtsp_conn_info *) * (nconns + 1)); - if (conns) { - conns[nconns] = conn; - nconns++; - } else { - die("could not reallocate memnory for \"conns\" in rtsp.c."); + debug_mutex_lock(&conns_lock, 1000000, 3); + // look for an empty slot first + int i = 0; + int found = 0; + while ((i < nconns) && (found == 0)) { + if (conns[i] == NULL) + found = 1; + else + i++; } + if (found != 0) { + conns[i] = conn; + } else { + // make space for a new element + conns = realloc(conns, sizeof(rtsp_conn_info *) * (nconns + 1)); + if (conns) { + conns[nconns] = conn; + nconns++; + } else { + die("could not reallocate memory for conns"); + } + } + debug_mutex_unlock(&conns_lock, 3); } -void cancel_all_RTSP_threads(void) { +// note: connection numbers start at 1, so an except_this_one value of zero means "all threads" +void cancel_all_RTSP_threads(airplay_stream_c stream_category, int except_this_one) { + // if the stream category is unspecified_stream_category + // all categories are elegible for cancellation + // otherwise just the category itself + debug_mutex_lock(&conns_lock, 1000000, 3); int i; for (i = 0; i < nconns; i++) { - debug(2, "Connection %d: cancelling.", conns[i]->connection_number); - pthread_cancel(conns[i]->thread); + if ((conns[i] != NULL) && (conns[i]->running != 0) && (conns[i]->connection_number != except_this_one) && + ((stream_category == unspecified_stream_category) || + (stream_category == conns[i]->airplay_stream_category))) { + pthread_cancel(conns[i]->thread); + debug(1, "Connection %d: cancelled.", conns[i]->connection_number); + } } for (i = 0; i < nconns; i++) { - debug(2, "Connection %d: joining.", conns[i]->connection_number); - pthread_join(conns[i]->thread, NULL); - free(conns[i]); + if ((conns[i] != NULL) && (conns[i]->running != 0) && (conns[i]->connection_number != except_this_one) && + ((stream_category == unspecified_stream_category) || + (stream_category == conns[i]->airplay_stream_category))) { + pthread_join(conns[i]->thread, NULL); + debug(1, "Connection %d: joined.", conns[i]->connection_number); + free(conns[i]); + conns[i] = NULL; + } } + debug_mutex_unlock(&conns_lock, 3); } void cleanup_threads(void) { + void *retval; int i; // debug(2, "culling threads."); - for (i = 0; i < nconns;) { - if (conns[i]->running == 0) { + debug_mutex_lock(&conns_lock, 1000000, 3); + for (i = 0; i < nconns; i++) { + if ((conns[i] != NULL) && (conns[i]->running == 0)) { debug(3, "found RTSP connection thread %d in a non-running state.", conns[i]->connection_number); pthread_join(conns[i]->thread, &retval); - debug(3, "RTSP connection thread %d deleted...", conns[i]->connection_number); + debug(1, "Connection %d: deleted in cleanup.", conns[i]->connection_number); free(conns[i]); - nconns--; - if (nconns) - conns[i] = conns[nconns]; - } else { - i++; + conns[i] = NULL; } } + debug_mutex_unlock(&conns_lock, 3); } // park a null at the line ending, and return the next line pointer @@ -697,7 +877,7 @@ int msg_handle_line(rtsp_message **pmsg, char *line) { *p = 0; p += 2; msg_add_header(msg, line, p); - // debug(3, " %s: %s.", line, p); + debug(3, " %s: %s.", line, p); return -1; } else { char *cl = msg_get_header(msg, "Content-Length"); @@ -932,19 +1112,6 @@ static ssize_t write_encrypted(rtsp_conn_info *conn, const void *buf, size_t cou free(encrypted); return count; } - -static void pkString_make(char *str, size_t str_size, const char *device_id) { - uint8_t public_key[32]; - if (str_size < 2 * sizeof(public_key) + 1) { - warn("Insufficient string size"); - str[0] = '\0'; - return; - } - pair_public_key_get(PAIR_SERVER_HOMEKIT, public_key, device_id); - char *ptr = str; - for (size_t i = 0; i < sizeof(public_key); i++) - ptr += sprintf(ptr, "%02x", public_key[i]); -} #endif ssize_t read_from_rtsp_connection(rtsp_conn_info *conn, void *buf, size_t count) { @@ -1131,9 +1298,26 @@ int msg_write_response(rtsp_conn_info *conn, rtsp_message *resp) { int n; unsigned int i; - n = snprintf(p, pktfree, "RTSP/1.0 %d %s\r\n", resp->respcode, - resp->respcode == 200 ? "OK" : "Unauthorized"); - // debug(1, "sending response: %s", pkt); + struct response_t { + int code; + char *string; + }; + + struct response_t responses[] = {{200, "OK"}, {400, "Bad Request"}, {403, "Unauthorized"}}; + int found = 0; + char *respcode_text = "Unauthorized"; + for (i = 0; i < sizeof(responses) / sizeof(struct response_t); i++) { + if (resp->respcode == responses[i].code) { + found = 1; + respcode_text = responses[i].string; + } + } + + if (found == 0) + debug(1, "can't find text for response code %d. Using \"%s\" instead.", resp->respcode, + respcode_text); + + n = snprintf(p, pktfree, "RTSP/1.0 %d %s\r\n", resp->respcode, respcode_text); pktfree -= n; p += n; @@ -1203,9 +1387,33 @@ int msg_write_response(rtsp_conn_info *conn, rtsp_message *resp) { return 0; } -void handle_record_2(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp) { - debug(2, "Connection %d: RECORD (AirPlay 2)", conn->connection_number); - debug_log_rtsp_message(2, "RECORD incoming message", req); +char *get_category_string(airplay_stream_c cat) { + char *category; + switch (cat) { + case unspecified_stream_category: + category = "unspecified stream"; + break; + case ptp_stream: + category = "PTP stream"; + break; + case ntp_stream: + category = "NTP stream"; + break; + case remote_control_stream: + category = "Remote Control stream"; + break; + default: + category = "Unexpected stream code"; + break; + } + return category; +} + +void handle_record_2(rtsp_conn_info *conn, __attribute((unused)) rtsp_message *req, + rtsp_message *resp) { + debug(1, "Connection %d: RECORD on %s", conn->connection_number, + get_category_string(conn->airplay_stream_category)); + // debug_log_rtsp_message(1, "RECORD incoming message", req); resp->respcode = 200; } @@ -1261,8 +1469,19 @@ void handle_record(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp) void handle_get_info(__attribute((unused)) rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp) { - if (rtsp_message_contains_plist(req)) { // it's stage one + if (rtsp_message_contains_plist(req)) { // it's stage one + // get version of AirPlay -- it might be too old. Not using it yet. + char *hdr = msg_get_header(req, "User-Agent"); + if (hdr) { + if (strstr(hdr, "AirPlay/") == hdr) { + hdr = hdr + strlen("AirPlay/"); + // double airplay_version = 0.0; + // airplay_version = atof(hdr); + debug(2, "Connection %d: GET_INFO: Source AirPlay Version is: %s.", conn->connection_number, + hdr); + } + } plist_t info_plist = NULL; plist_from_memory(req->content, req->contentlength, &info_plist); @@ -1343,7 +1562,7 @@ void handle_get_info(__attribute((unused)) rtsp_conn_info *conn, rtsp_message *r } void handle_flushbuffered(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp) { - debug(3, "Connection %d: FLUSHBUFFERED %s :: Content-Length %d", conn->connection_number, + debug(3, "Connection %d: FLUSHBUFFERED %s : Content-Length %d", conn->connection_number, req->path, req->contentlength); debug_log_rtsp_message(2, "FLUSHBUFFERED request", req); @@ -1552,6 +1771,12 @@ void handle_get(__attribute((unused)) rtsp_conn_info *conn, __attribute((unused) req->contentlength); resp->respcode = 500; } +void handle_post(__attribute((unused)) rtsp_conn_info *conn, __attribute((unused)) rtsp_message *req, + __attribute((unused)) rtsp_message *resp) { + debug(1, "Connection %d: POST %s Content-Length %d", conn->connection_number, req->path, + req->contentlength); + resp->respcode = 500; +} #endif #ifdef CONFIG_AIRPLAY_2 @@ -1560,10 +1785,9 @@ struct pairings { uint8_t public_key[32]; struct pairings *next; -} *pairings; +} * pairings; -static struct pairings * pairing_find(const char *device_id) -{ +static struct pairings *pairing_find(const char *device_id) { for (struct pairings *pairing = pairings; pairing; pairing = pairing->next) { if (strcmp(device_id, pairing->device_id) == 0) return pairing; @@ -1595,8 +1819,9 @@ static void pairing_remove(struct pairings *pairing) { free(pairing); } -static int pairing_add_cb(uint8_t public_key[32], const char *device_id, void *cb_arg __attribute__((unused))) { - debug(2, "pair-add cb for %s", device_id); +static int pairing_add_cb(uint8_t public_key[32], const char *device_id, + void *cb_arg __attribute__((unused))) { + debug(1, "pair-add cb for %s", device_id); struct pairings *pairing = pairing_find(device_id); if (pairing) { @@ -1608,12 +1833,13 @@ static int pairing_add_cb(uint8_t public_key[32], const char *device_id, void *c return 0; } -static int pairing_remove_cb(uint8_t public_key[32] __attribute__((unused)), const char *device_id, void *cb_arg __attribute__((unused))) { - debug(2, "pair-remove cb for %s", device_id); +static int pairing_remove_cb(uint8_t public_key[32] __attribute__((unused)), const char *device_id, + void *cb_arg __attribute__((unused))) { + debug(1, "pair-remove cb for %s", device_id); struct pairings *pairing = pairing_find(device_id); if (!pairing) { - debug(2, "pair-remove callback for unknown device"); + debug(1, "pair-remove callback for unknown device"); return -1; } @@ -1621,18 +1847,21 @@ static int pairing_remove_cb(uint8_t public_key[32] __attribute__((unused)), con return 0; } -static void pairing_list_cb(pair_cb enum_cb, void *enum_cb_arg, void *cb_arg __attribute__((unused))) { - debug(2, "pair-list cb"); +static void pairing_list_cb(pair_cb enum_cb, void *enum_cb_arg, + void *cb_arg __attribute__((unused))) { + debug(1, "pair-list cb"); for (struct pairings *pairing = pairings; pairing; pairing = pairing->next) { enum_cb(pairing->public_key, pairing->device_id, enum_cb_arg); } } -void handle_pair_add(rtsp_conn_info *conn __attribute__((unused)), rtsp_message *req, rtsp_message *resp) { +void handle_pair_add(rtsp_conn_info *conn __attribute__((unused)), rtsp_message *req, + rtsp_message *resp) { uint8_t *body = NULL; size_t body_len = 0; - int ret = pair_add(PAIR_SERVER_HOMEKIT, &body, &body_len, pairing_add_cb, NULL, (const uint8_t *)req->content, req->contentlength); + int ret = pair_add(PAIR_SERVER_HOMEKIT, &body, &body_len, pairing_add_cb, NULL, + (const uint8_t *)req->content, req->contentlength); if (ret < 0) { debug(1, "pair-add returned an error"); resp->respcode = 451; @@ -1644,10 +1873,12 @@ void handle_pair_add(rtsp_conn_info *conn __attribute__((unused)), rtsp_message debug_log_rtsp_message(2, "pair-add response", resp); } -void handle_pair_list(rtsp_conn_info *conn __attribute__((unused)), rtsp_message *req, rtsp_message *resp) { +void handle_pair_list(rtsp_conn_info *conn __attribute__((unused)), rtsp_message *req, + rtsp_message *resp) { uint8_t *body = NULL; size_t body_len = 0; - int ret = pair_list(PAIR_SERVER_HOMEKIT, &body, &body_len, pairing_list_cb, NULL, (const uint8_t *)req->content, req->contentlength); + int ret = pair_list(PAIR_SERVER_HOMEKIT, &body, &body_len, pairing_list_cb, NULL, + (const uint8_t *)req->content, req->contentlength); if (ret < 0) { debug(1, "pair-list returned an error"); resp->respcode = 451; @@ -1659,10 +1890,12 @@ void handle_pair_list(rtsp_conn_info *conn __attribute__((unused)), rtsp_message debug_log_rtsp_message(2, "pair-list response", resp); } -void handle_pair_remove(rtsp_conn_info *conn __attribute__((unused)), rtsp_message *req, rtsp_message *resp) { +void handle_pair_remove(rtsp_conn_info *conn __attribute__((unused)), rtsp_message *req, + rtsp_message *resp) { uint8_t *body = NULL; size_t body_len = 0; - int ret = pair_remove(PAIR_SERVER_HOMEKIT, &body, &body_len, pairing_remove_cb, NULL, (const uint8_t *)req->content, req->contentlength); + int ret = pair_remove(PAIR_SERVER_HOMEKIT, &body, &body_len, pairing_remove_cb, NULL, + (const uint8_t *)req->content, req->contentlength); if (ret < 0) { debug(1, "pair-remove returned an error"); resp->respcode = 451; @@ -1807,7 +2040,7 @@ void handle_fp_setup(__attribute__((unused)) rtsp_conn_info *conn, rtsp_message static uint8_t server_fp_header[] = "\x46\x50\x4c\x59\x03\x01\x04\x00\x00\x00\x00\x14"; - resp->respcode = 200; // assume it's not handled + resp->respcode = 200; // assume it's handled // uint8_t *out; // size_t out_len; @@ -1867,20 +2100,21 @@ void handle_fp_setup(__attribute__((unused)) rtsp_conn_info *conn, rtsp_message } /* - Identifier - 21cc689d-d5de-4814-872c-71d1426b57e0 - Enable_HK_Access_Control - - PublicKey - - qXJDhhL5F3OACL+HO7LVLQVdy0OJtavepjpF720PaOQ= - - Device_Name - MyDevice - Access_Control_Level - 0 + Identifier + 21cc689d-d5de-4814-872c-71d1426b57e0 + Enable_HK_Access_Control + + PublicKey + + qXJDhhL5F3OACL+HO7LVLQVdy0OJtavepjpF720PaOQ= + + Device_Name + MyDevice + Access_Control_Level + 0 */ -void handle_configure(rtsp_conn_info *conn __attribute__((unused)), rtsp_message *req __attribute__((unused)), rtsp_message *resp) { +void handle_configure(rtsp_conn_info *conn __attribute__((unused)), + rtsp_message *req __attribute__((unused)), rtsp_message *resp) { uint8_t public_key[32]; pair_public_key_get(PAIR_SERVER_HOMEKIT, public_key, config.airplay_device_id); @@ -1889,7 +2123,8 @@ void handle_configure(rtsp_conn_info *conn __attribute__((unused)), rtsp_message plist_dict_set_item(response_plist, "Identifier", plist_new_string(config.airplay_pi)); plist_dict_set_item(response_plist, "Enable_HK_Access_Control", plist_new_bool(1)); - plist_dict_set_item(response_plist, "PublicKey", plist_new_data((const char *)public_key, sizeof(public_key))); + plist_dict_set_item(response_plist, "PublicKey", + plist_new_data((const char *)public_key, sizeof(public_key))); plist_dict_set_item(response_plist, "Device_Name", plist_new_string(config.service_name)); plist_dict_set_item(response_plist, "Access_Control_Level", plist_new_uint(0)); @@ -1900,6 +2135,82 @@ void handle_configure(rtsp_conn_info *conn __attribute__((unused)), rtsp_message debug_log_rtsp_message(2, "POST /configure response:", resp); } +void handle_feedback(__attribute__((unused)) rtsp_conn_info *conn, + __attribute__((unused)) rtsp_message *req, + __attribute__((unused)) rtsp_message *resp) {} + +void handle_command(__attribute__((unused)) rtsp_conn_info *conn, rtsp_message *req, + __attribute__((unused)) rtsp_message *resp) { + debug_log_rtsp_message(2, "POST /command", req); + if (rtsp_message_contains_plist(req)) { + plist_t command_dict = NULL; + plist_from_memory(req->content, req->contentlength, &command_dict); + if (command_dict != NULL) { + // we have a plist -- try to get the dict item keyed to "updateMRSupportedCommands" + plist_t item = plist_dict_get_item(command_dict, "type"); + if (item != NULL) { + char *typeValue = NULL; + plist_get_string_val(item, &typeValue); + if (strcmp(typeValue, "updateMRSupportedCommands") == 0) { + item = plist_dict_get_item(command_dict, "params"); + if (item != NULL) { + // the item should be a dict + plist_t item_array = plist_dict_get_item(item, "mrSupportedCommandsFromSender"); + if (item_array != NULL) { + // here we have an array of data items + uint32_t items = plist_array_get_size(item_array); + if (items) { + uint32_t item_number; + for (item_number = 0; item_number < items; item_number++) { + plist_t the_item = plist_array_get_item(item_array, item_number); + char *buff = NULL; + uint64_t length = 0; + plist_get_data_val(the_item, &buff, &length); + // debug(1,"Item %d, length: %" PRId64 " bytes", item_number, length); + if ((length >= strlen("bplist00")) && (strstr(buff, "bplist00") == buff)) { + // debug(1,"Contains a plist."); + plist_t subsidiary_plist = NULL; + plist_from_memory(buff, length, &subsidiary_plist); + if (subsidiary_plist) { + char *printable_plist = plist_content(subsidiary_plist); + if (printable_plist) { + debug(2, "\n%s", printable_plist); + free(printable_plist); + } else { + debug(1, "Can't print the plist!"); + } + // plist_free(subsidiary_plist); + } else { + debug(1, "Can't access the plist!"); + } + } + } + } + } else { + debug(1, "POST /command no mrSupportedCommandsFromSender item."); + } + } else { + debug(1, "POST /command no params dict."); + } + resp->respcode = 400; // say it's a bad request + } else { + debug(1, + "POST /command plist type is \"%s\", but \"updateMRSupportedCommands\" expected.", + typeValue); + } + } else { + debug(1, "Could not get the \"type\" item."); + } + + plist_free(command_dict); + } else { + debug(1, "POST /command plist cannot be inputted."); + } + } else { + debug(1, "POST /command contains no plist"); + } +} + void handle_post(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp) { resp->respcode = 200; if (strcmp(req->path, "/pair-setup") == 0) { @@ -1916,11 +2227,14 @@ void handle_post(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp) { handle_fp_setup(conn, req, resp); } else if (strcmp(req->path, "/configure") == 0) { handle_configure(conn, req, resp); + } else if (strcmp(req->path, "/feedback") == 0) { + handle_feedback(conn, req, resp); + } else if (strcmp(req->path, "/command") == 0) { + handle_command(conn, req, resp); } else { - debug(3, "Connection %d: POST %s Content-Length %d", conn->connection_number, req->path, - req->contentlength); + debug(2, "Connection %d: Unhandled POST %s Content-Length %d", conn->connection_number, + req->path, req->contentlength); debug_log_rtsp_message(2, "POST request", req); - debug(3, "Unhandled POST, path \"%s\".", req->path); } } @@ -1991,11 +2305,51 @@ void handle_options(rtsp_conn_info *conn, __attribute__((unused)) rtsp_message * "OPTIONS, POST, GET, PUT"); } +void teardown_phase_one(rtsp_conn_info *conn) { + if (conn->player_thread) { + player_stop(conn); + activity_monitor_signify_activity(0); // inactive, and should be after command_stop() + } + if (conn->session_key) { + free(conn->session_key); + conn->session_key = NULL; + } +} + +void teardown_phase_two(rtsp_conn_info *conn) { + // we are being asked to disconnect + debug(1, "Connection %d: TEARDOWN a %s connection.", conn->connection_number, + get_category_string(conn->airplay_stream_category)); + debug(2, "Connection %d: TEARDOWN Delete Event Thread.", conn->connection_number); + pthread_cancel(conn->rtp_event_thread); + pthread_join(conn->rtp_event_thread, NULL); + debug(1, "Connection %d: TEARDOWN Close Event Socket.", conn->connection_number); + if (conn->event_socket) { + close(conn->event_socket); + conn->event_socket = 0; + debug(1, "Connection %d: closing TCP event port %u.", conn->connection_number, conn->local_event_port); + } + + // if we are closing a PTP stream only, do this + if (conn->airplay_stream_category == ptp_stream) { + if (conn->airplay_gid != NULL) { + free(conn->airplay_gid); + conn->airplay_gid = NULL; + } + conn->groupContainsGroupLeader = 0; + config.airplay_statusflags &= (0xffffffff - (1 << 11)); // DeviceSupportsRelay + build_bonjour_strings(conn); + debug(1, "Connection %d: TEARDOWN mdns_update on %s.", conn->connection_number, + get_category_string(conn->airplay_stream_category)); + mdns_update(NULL, secondary_txt_records); + } +} + + void handle_teardown_2(rtsp_conn_info *conn, __attribute__((unused)) rtsp_message *req, rtsp_message *resp) { - debug(2, "Connection %d: TEARDOWN", conn->connection_number); - // debug_log_rtsp_message(1, "TEARDOWN: ", req); + debug_log_rtsp_message(2, "TEARDOWN: ", req); // if (have_player(conn)) { resp->respcode = 200; msg_add_header(resp, "Connection", "close"); @@ -2006,30 +2360,14 @@ void handle_teardown_2(rtsp_conn_info *conn, __attribute__((unused)) rtsp_messag plist_t streams = plist_dict_get_item(messagePlist, "streams"); if (streams) { + debug(1, "Connection %d: TEARDOWN a %s.", conn->connection_number, + get_category_string(conn->airplay_stream_category)); // we are being asked to close a stream - player_stop(conn); - activity_monitor_signify_activity(0); // inactive, and should be after command_stop() - if (conn->session_key) { - free(conn->session_key); - conn->session_key = NULL; - } + teardown_phase_one(conn); plist_free(streams); debug(2, "Connection %d: Stream TEARDOWN complete", conn->connection_number); } else { - // we are being asked to disconnect - debug(2, "Connection %d: TEARDOWN No \"streams\" array has been found.", - conn->connection_number); - debug(2, "Connection %d: TEARDOWN Delete Event Thread.", conn->connection_number); - pthread_cancel(conn->rtp_event_thread); - pthread_join(conn->rtp_event_thread, NULL); - debug(2, "Connection %d: TEARDOWN Close Event Socket.", conn->connection_number); - if (conn->event_socket) - close(conn->event_socket); - - if (conn->client_setup_plist) { - plist_free(conn->client_setup_plist); - conn->client_setup_plist = NULL; - } + teardown_phase_two(conn); debug(2, "Connection %d: non-stream TEARDOWN complete", conn->connection_number); } //} else { @@ -2113,300 +2451,473 @@ void handle_flush(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp) { void handle_setup_2(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp) { int err; debug(2, "Connection %d: SETUP (AirPlay 2)", conn->connection_number); - // debug_log_rtsp_message(1, "Connection %d: SETUP (AirPlay 2) SETUP incoming message", req); - // we need to get the timing peer interfaces. - // I'm guessing they are all this device's adresses that are on the same subnets as - // the timing peer info in the accompanying plist + // debug_log_rtsp_message(1, "SETUP (AirPlay 2) SETUP incoming message", req); plist_t messagePlist = plist_from_rtsp_content(req); plist_t setupResponsePlist = plist_new_dict(); + resp->respcode = 400; - // now see if the incoming plist contains a "streams" array + // see if the incoming plist contains a "streams" array plist_t streams = plist_dict_get_item(messagePlist, "streams"); - - if (streams) { + if (streams == NULL) { + // no "streams" plist, so it must (?) be an initial setup debug(2, - "SETUP on Connection %d: A \"streams\" array has been found -- create control and audio " - "threads and ports", - conn->connection_number); - // open a player) - - // if (ptp_shm_interface_open() != 0) - // warn("Error opening the nqptp clock daemon!"); - - // get stream[0] - plist_t stream0 = plist_array_get_item(streams, 0); - - plist_t streams_array = plist_new_array(); // to hold the ports and stuff - plist_t stream0dict = plist_new_dict(); - // more stuff - // set up a UDP control stream and thread and a UDP or TCP audio stream and thread - - // bind a new UDP port and get a socket - conn->local_ap2_control_port = 0; // any port - err = bind_socket_and_port(SOCK_DGRAM, conn->connection_ip_family, conn->self_ip_string, - conn->self_scope_id, &conn->local_ap2_control_port, - &conn->ap2_control_socket); - if (err) { - die("Error %d: could not find a UDP port to use as an ap2_control port", err); - } - - pthread_create(&conn->rtp_ap2_control_thread, NULL, &rtp_ap2_control_receiver, (void *)conn); - - // get the session key - - plist_t item = plist_dict_get_item(stream0, "shk"); // session key - uint64_t item_value = 0; - plist_get_data_val(item, (char **)&conn->session_key, &item_value); - - // now, get the type of the stream. - item = plist_dict_get_item(stream0, "type"); - item_value = 0; - plist_get_uint_val(item, &item_value); - - switch (item_value) { - case 96: { - debug(1, "Connection %d. Realtime Audio Stream Detected.", conn->connection_number); - debug_log_rtsp_message(2, "Realtime Audio Stream SETUP incoming message", req); - get_play_lock(conn); - conn->airplay_stream_type = realtime_stream; - // bind a new UDP port and get a socket - conn->local_realtime_audio_port = 0; // any port - err = bind_socket_and_port(SOCK_DGRAM, conn->connection_ip_family, conn->self_ip_string, - conn->self_scope_id, &conn->local_realtime_audio_port, - &conn->realtime_audio_socket); - if (err) { - die("Error %d: could not find a UDP port to use as a realtime_audio port", err); - } - - pthread_create(&conn->rtp_realtime_audio_thread, NULL, &rtp_realtime_audio_receiver, - (void *)conn); - - plist_dict_set_item(stream0dict, "type", plist_new_uint(96)); - plist_dict_set_item(stream0dict, "dataPort", plist_new_uint(conn->local_realtime_audio_port)); - - conn->stream.type = ast_apple_lossless; - debug(3, "An ALAC stream has been detected."); - - // Set reasonable connection defaults - conn->stream.fmtp[0] = 96; - conn->stream.fmtp[1] = 352; - conn->stream.fmtp[2] = 0; - conn->stream.fmtp[3] = 16; - conn->stream.fmtp[4] = 40; - conn->stream.fmtp[5] = 10; - conn->stream.fmtp[6] = 14; - conn->stream.fmtp[7] = 2; - conn->stream.fmtp[8] = 255; - conn->stream.fmtp[9] = 0; - conn->stream.fmtp[10] = 0; - conn->stream.fmtp[11] = 44100; - - // set the parameters of the player (as distinct from the parameters of the decoder -- - // that's done later). - conn->max_frames_per_packet = conn->stream.fmtp[1]; // number of audio frames per packet. - conn->input_rate = conn->stream.fmtp[11]; - conn->input_num_channels = conn->stream.fmtp[7]; - conn->input_bit_depth = conn->stream.fmtp[3]; - conn->input_bytes_per_frame = conn->input_num_channels * ((conn->input_bit_depth + 7) / 8); - debug(2, "Realtime Stream Play"); - if (conn->ap2_timing_peer_list_message) - ptp_send_control_message_string(conn->ap2_timing_peer_list_message); - else - debug(1, "No timing peer list!"); - activity_monitor_signify_activity(1); - player_prepare_to_play(conn); - player_play(conn); - - conn->rtp_running = 1; // hack! - } break; - case 103: { - debug(1, "Connection %d. Buffered Audio Stream Detected.", conn->connection_number); - debug_log_rtsp_message(2, "Buffered Audio Stream SETUP incoming message", req); - get_play_lock(conn); - conn->airplay_stream_type = buffered_stream; - // get needed stuff - - // bind a new TCP port and get a socket - conn->local_buffered_audio_port = 0; // any port - err = bind_socket_and_port(SOCK_STREAM, conn->connection_ip_family, conn->self_ip_string, - conn->self_scope_id, &conn->local_buffered_audio_port, - &conn->buffered_audio_socket); - if (err) { - die("Error %d: could not find a TCP port to use as a buffered_audio port", err); - } - - // hack. - conn->max_frames_per_packet = 352; // number of audio frames per packet. - conn->input_rate = 44100; // we are stuck with this for the moment. - conn->input_num_channels = 2; - conn->input_bit_depth = 16; - conn->input_bytes_per_frame = conn->input_num_channels * ((conn->input_bit_depth + 7) / 8); - activity_monitor_signify_activity(1); - player_prepare_to_play( - conn); // get capabilities of DAC before creating the buffered audio thread - - pthread_create(&conn->rtp_buffered_audio_thread, NULL, &rtp_buffered_audio_processor, - (void *)conn); - - plist_dict_set_item(stream0dict, "type", plist_new_uint(103)); - plist_dict_set_item(stream0dict, "dataPort", plist_new_uint(conn->local_buffered_audio_port)); - plist_dict_set_item(stream0dict, "audioBufferSize", - plist_new_uint(conn->ap2_audio_buffer_size)); - - - // this should be cancelled by an activity_monitor_signify_activity(1) - // call in the SETRATEANCHORI handler, which should come up right away - activity_monitor_signify_activity(0); - - player_play(conn); - - conn->rtp_running = 1; // hack! - } break; - default: - debug(1, "Unhandled stream type %" PRIu64 ".", item_value); - } - - plist_dict_set_item(stream0dict, "controlPort", plist_new_uint(conn->local_ap2_control_port)); - - plist_array_append_item(streams_array, stream0dict); - plist_dict_set_item(setupResponsePlist, "streams", streams_array); - - } else { - debug(2, - "SETUP on Connection %d: No \"streams\" array has been found -- create an event thread " + "Connection %d SETUP: No \"streams\" array has been found -- create an event thread " "and open a TCP port.", conn->connection_number); - // first, get and retain the incoming plist. - if (conn->client_setup_plist) { - plist_free(conn->client_setup_plist); - } - conn->client_setup_plist = messagePlist; - plist_t addresses = plist_new_array(); // to hold the device's peer interfaces + conn->airplay_stream_category = unspecified_stream_category; + // figure out what category of stream it is, by looking at the plist + plist_t timingProtocol = plist_dict_get_item(messagePlist, "timingProtocol"); + if (timingProtocol != NULL) { + char *timingProtocolString = NULL; + plist_get_string_val(timingProtocol, &timingProtocolString); + if (timingProtocolString) { + if (strcmp(timingProtocolString, "PTP") == 0) { + debug(1, "Connection %d: SETUP: PTP setup detected.", conn->connection_number); + conn->airplay_stream_category = ptp_stream; + conn->timing_type = ts_ptp; + } else if (strcmp(timingProtocolString, "NTP") == 0) { + debug(1, "Connection %d: SETUP: NTP setup detected.", conn->connection_number); + conn->airplay_stream_category = ntp_stream; + conn->timing_type = ts_ntp; + } else if (strcmp(timingProtocolString, "None") == 0) { + debug(2, "Connection %d: SETUP: a \"None\" setup detected.", conn->connection_number); + // now check to see if it's got the "isRemoteControlOnly" item and check it's true + plist_t isRemoteControlOnly = plist_dict_get_item(messagePlist, "isRemoteControlOnly"); + if (isRemoteControlOnly != NULL) { + uint8_t isRemoteControlOnlyBoolean = 0; + plist_get_bool_val(isRemoteControlOnly, &isRemoteControlOnlyBoolean); + if (isRemoteControlOnlyBoolean != 0) { + debug(1, "Connection %d: SETUP: Remote Control setup detected.", + conn->connection_number); + conn->airplay_stream_category = remote_control_stream; + } else { + debug(1, + "Connection %d: SETUP: a \"None\" setup detected, with " + "\"isRemoteControlOnly\" item set to \"false\".", + conn->connection_number); + } + } else { + debug(1, + "Connection %d: SETUP: a \"None\" setup detected, but no " + "\"isRemoteControlOnly\" item detected.", + conn->connection_number); + } + } - // now determine which of the present device's IP numbers are in the - // same subnet as any IP numbers in the timing peer info - // provided by the client + // here, we know it's an initial setup and we know the kind of setup being requested + // if it's a full service PTP stream, we get groupUUID, groupContainsGroupLeader and + // timingPeerList + if (conn->airplay_stream_category == ptp_stream) { + debug_log_rtsp_message(2, "SETUP \"PTP\" message", req); + plist_t groupUUID = plist_dict_get_item(messagePlist, "groupUUID"); + if (groupUUID) { + char *gid = NULL; + plist_get_string_val(groupUUID, &gid); + if (gid) { + if (conn->airplay_gid) + free(conn->airplay_gid); + conn->airplay_gid = gid; // it'll be free'd later on... + } else { + debug(1, "Invalid groupUUID"); + } + } else { + debug(1, "No groupUUID in SETUP"); + } - plist_t timing_peer_info = plist_dict_get_item(conn->client_setup_plist, "timingPeerInfo"); - if (timing_peer_info) { - plist_t addresses_array = plist_dict_get_item(timing_peer_info, "Addresses"); - if (addresses_array) { - // iterate through the array of items - uint32_t items = plist_array_get_size(addresses_array); - if (items) { + // now see if the group contains a group leader + plist_t groupContainsGroupLeader = + plist_dict_get_item(messagePlist, "groupContainsGroupLeader"); + if (groupContainsGroupLeader) { + uint8_t value = 0; + plist_get_bool_val(groupContainsGroupLeader, &value); + conn->groupContainsGroupLeader = value; + debug(2, "Updated groupContainsGroupLeader to %u", conn->groupContainsGroupLeader); + } else { + debug(1, "No groupContainsGroupLeader in SETUP"); + } - uint32_t item; - struct ifaddrs *addrs, *iap; - getifaddrs(&addrs); - for (item = 0; item < items; item++) { - plist_t n = plist_array_get_item(addresses_array, item); - char *ip_address = NULL; - plist_get_string_val(n, &ip_address); - // debug(1, "Timing peer: %s", ip_address); - // find its family and convert it into an IPv4/6 address + plist_t timing_peer_info = plist_dict_get_item(messagePlist, "timingPeerInfo"); + if (timing_peer_info) { + // first, get and retain the incoming plist. + plist_t addresses = plist_new_array(); // to hold the device's peer interfaces + plist_t addresses_array = plist_dict_get_item(timing_peer_info, "Addresses"); + if (addresses_array) { + // iterate through the array of items + uint32_t items = plist_array_get_size(addresses_array); + if (items) { - struct sockaddr_storage peer_address; - struct sockaddr_in6 *pa6 = (struct sockaddr_in6 *)&peer_address; - struct sockaddr_in *pa4 = (struct sockaddr_in *)&peer_address; + uint32_t item; + struct ifaddrs *addrs, *iap; + getifaddrs(&addrs); + for (item = 0; item < items; item++) { + plist_t n = plist_array_get_item(addresses_array, item); + char *ip_address = NULL; + plist_get_string_val(n, &ip_address); + // debug(1, "Timing peer: %s", ip_address); + // find its family and convert it into an IPv4/6 address - if (inet_pton(AF_INET6, ip_address, &pa6->sin6_addr) == 1) { // is an IPv6 address... - peer_address.ss_family = AF_INET6; - for (iap = addrs; iap != NULL; iap = iap->ifa_next) { - if ((iap->ifa_addr) && (iap->ifa_netmask) && (iap->ifa_flags & IFF_UP) && - ((iap->ifa_flags & IFF_LOOPBACK) == 0) && - (iap->ifa_addr->sa_family == AF_INET6)) { - struct sockaddr_in6 *addr6 = (struct sockaddr_in6 *)(iap->ifa_addr); - struct sockaddr_in6 *mask6 = (struct sockaddr_in6 *)(iap->ifa_netmask); - unsigned int i; - int different = 0; - for (i = 0; ((i < 16) && (different == 0)); i++) { - unsigned char host_byte = - (addr6->sin6_addr.s6_addr[i] & mask6->sin6_addr.s6_addr[i]); - unsigned char peer_byte = - (pa6->sin6_addr.s6_addr[i] & mask6->sin6_addr.s6_addr[i]); - if (host_byte != peer_byte) - different = 1; - } + struct sockaddr_storage peer_address; + struct sockaddr_in6 *pa6 = (struct sockaddr_in6 *)&peer_address; + struct sockaddr_in *pa4 = (struct sockaddr_in *)&peer_address; - char buf[32]; - memset(buf, 0, sizeof(buf)); - inet_ntop(AF_INET6, (void *)&addr6->sin6_addr, buf, sizeof(buf)); - if (!different) { - // debug(1, "%s is in the same subnet as %s.", buf, ip_address); - plist_array_append_item(addresses, plist_new_string(buf)); - } - } - } - } else if (inet_pton(AF_INET, ip_address, &pa4->sin_addr) == 1) { - peer_address.ss_family = AF_INET; - for (iap = addrs; iap != NULL; iap = iap->ifa_next) { - if ((iap->ifa_addr) && (iap->ifa_netmask) && (iap->ifa_flags & IFF_UP) && - ((iap->ifa_flags & IFF_LOOPBACK) == 0) && - (iap->ifa_addr->sa_family == AF_INET)) { - struct sockaddr_in *addr = (struct sockaddr_in *)(iap->ifa_addr); - struct sockaddr_in *mask = (struct sockaddr_in *)(iap->ifa_netmask); - if ((addr->sin_addr.s_addr & mask->sin_addr.s_addr) == - (pa4->sin_addr.s_addr & mask->sin_addr.s_addr)) { - char buf[32]; - memset(buf, 0, sizeof(buf)); - inet_ntop(AF_INET, (void *)&addr->sin_addr, buf, sizeof(buf)); - // debug(1, "%s is in the same subnet as %s.", buf, ip_address); - plist_array_append_item(addresses, plist_new_string(buf)); + if (inet_pton(AF_INET6, ip_address, &pa6->sin6_addr) == + 1) { // is an IPv6 address... + peer_address.ss_family = AF_INET6; + for (iap = addrs; iap != NULL; iap = iap->ifa_next) { + if ((iap->ifa_addr) && (iap->ifa_netmask) && (iap->ifa_flags & IFF_UP) && + ((iap->ifa_flags & IFF_LOOPBACK) == 0) && + (iap->ifa_addr->sa_family == AF_INET6)) { + struct sockaddr_in6 *addr6 = (struct sockaddr_in6 *)(iap->ifa_addr); + struct sockaddr_in6 *mask6 = (struct sockaddr_in6 *)(iap->ifa_netmask); + unsigned int i; + int different = 0; + for (i = 0; ((i < 16) && (different == 0)); i++) { + unsigned char host_byte = + (addr6->sin6_addr.s6_addr[i] & mask6->sin6_addr.s6_addr[i]); + unsigned char peer_byte = + (pa6->sin6_addr.s6_addr[i] & mask6->sin6_addr.s6_addr[i]); + if (host_byte != peer_byte) + different = 1; + } + + char buf[32]; + memset(buf, 0, sizeof(buf)); + inet_ntop(AF_INET6, (void *)&addr6->sin6_addr, buf, sizeof(buf)); + if (!different) { + // debug(1, "%s is in the same subnet as %s.", buf, ip_address); + plist_array_append_item(addresses, plist_new_string(buf)); + } + } + } + } else if (inet_pton(AF_INET, ip_address, &pa4->sin_addr) == 1) { + peer_address.ss_family = AF_INET; + for (iap = addrs; iap != NULL; iap = iap->ifa_next) { + if ((iap->ifa_addr) && (iap->ifa_netmask) && (iap->ifa_flags & IFF_UP) && + ((iap->ifa_flags & IFF_LOOPBACK) == 0) && + (iap->ifa_addr->sa_family == AF_INET)) { + struct sockaddr_in *addr = (struct sockaddr_in *)(iap->ifa_addr); + struct sockaddr_in *mask = (struct sockaddr_in *)(iap->ifa_netmask); + if ((addr->sin_addr.s_addr & mask->sin_addr.s_addr) == + (pa4->sin_addr.s_addr & mask->sin_addr.s_addr)) { + char buf[32]; + memset(buf, 0, sizeof(buf)); + inet_ntop(AF_INET, (void *)&addr->sin_addr, buf, sizeof(buf)); + // debug(1, "%s is in the same subnet as %s.", buf, ip_address); + plist_array_append_item(addresses, plist_new_string(buf)); + } + } + } + } else { + debug(1, "SETUP on Connection %d: Don't recognise this as an IP address.", + conn->connection_number); } + free(ip_address); } + freeifaddrs(addrs); + } else { + debug(1, "SETUP on Connection %d: No timingPeerInfo addresses in the array.", + conn->connection_number); } } else { - debug(1, "Don't recognise this as an IP address."); + debug(1, "SETUP on Connection %d: Can't find timingPeerInfo addresses", + conn->connection_number); } - free(ip_address); + // make up the timing peer info list part of the response... + // debug(1,"Create timingPeerInfoPlist"); + plist_t timingPeerInfoPlist = plist_new_dict(); + plist_dict_set_item(timingPeerInfoPlist, "Addresses", addresses); + plist_dict_set_item(timingPeerInfoPlist, "ID", plist_new_string(conn->self_ip_string)); + plist_dict_set_item(setupResponsePlist, "timingPeerInfo", timingPeerInfoPlist); + // get a port to use as an event port + // bind a new TCP port and get a socket + conn->local_event_port = 0; // any port + int err = bind_socket_and_port(SOCK_STREAM, conn->connection_ip_family, + conn->self_ip_string, conn->self_scope_id, + &conn->local_event_port, &conn->event_socket); + if (err) { + die("SETUP on Connection %d: Error %d: could not find a TCP port to use as an event " + "port", + conn->connection_number, err); + } + + debug(1, "Connection %d: TCP PTP event port opened: %u.", conn->connection_number, conn->local_event_port); + + pthread_create(&conn->rtp_event_thread, NULL, &rtp_event_receiver, (void *)conn); + + plist_dict_set_item(setupResponsePlist, "eventPort", + plist_new_uint(conn->local_event_port)); + plist_dict_set_item(setupResponsePlist, "timingPort", plist_new_uint(0)); // dummy + cancel_all_RTSP_threads(unspecified_stream_category, + conn->connection_number); // kill all the other listeners + + config.airplay_statusflags |= 1 << 11; // DeviceSupportsRelay + build_bonjour_strings(conn); + debug(1, "Connection %d: SETUP mdns_update on %s.", conn->connection_number, + get_category_string(conn->airplay_stream_category)); + mdns_update(NULL, secondary_txt_records); + + resp->respcode = 200; + } else { + debug(1, "SETUP on Connection %d: PTP setup -- no timingPeerInfo plist.", + conn->connection_number); } - freeifaddrs(addrs); + } else if (conn->airplay_stream_category == ntp_stream) { + debug(1, "SETUP on Connection %d: ntp stream handling is not implemented!", + conn->connection_number, req); + warn("Shairport Sync can not handle NTP streams."); + } else if (conn->airplay_stream_category == remote_control_stream) { + debug_log_rtsp_message(2, "SETUP \"isRemoteControlOnly\" message", req); + + // get a port to use as an event port + // bind a new TCP port and get a socket + conn->local_event_port = 0; // any port + int err = bind_socket_and_port(SOCK_STREAM, conn->connection_ip_family, + conn->self_ip_string, conn->self_scope_id, + &conn->local_event_port, &conn->event_socket); + if (err) { + die("SETUP on Connection %d: Error %d: could not find a TCP port to use as an event " + "port", + conn->connection_number, err); + } + + debug(1, "Connection %d: TCP Remote Control event port opened: %u.", conn->connection_number, conn->local_event_port); + + pthread_create(&conn->rtp_event_thread, NULL, &rtp_event_receiver, (void *)conn); + + plist_dict_set_item(setupResponsePlist, "eventPort", + plist_new_uint(conn->local_event_port)); + cancel_all_RTSP_threads( + remote_control_stream, + conn->connection_number); // kill all the other remote control listeners + resp->respcode = 200; } else { - debug(1, "No timingPeerInfo addresses in the array."); + debug(1, "SETUP on Connection %d: an unrecognised \"%s\" setup detected.", + conn->connection_number, timingProtocolString); + warn("Shairport Sync can not handle streams of this type: \"%s\".", timingProtocolString); } + free(timingProtocolString); } else { - debug(1, "Can't find timingPeerInfo addresses"); + debug(1, "SETUP on Connection %d: Can't retrieve timingProtocol string in initial SETUP.", + conn->connection_number); } } else { - debug_log_rtsp_message(2, "Unrecognised SETUP incoming message", req); + debug(1, + "SETUP on Connection %d: Unrecognised SETUP incoming message from \"%s\": no " + "timingProtocol or streams plist found.", + conn->connection_number, (const char *)conn->client_ip_string); + debug_log_rtsp_message(2, "Unrecognised SETUP incoming message.", req); + warn("Unrecognised SETUP incoming message -- ignored."); } + } else { + debug(2, "Connection %d: SETUP on %s. A \"streams\" array has been found", + conn->connection_number, get_category_string(conn->airplay_stream_category)); + if (conn->airplay_stream_category == ptp_stream) { + // get stream[0] + plist_t stream0 = plist_array_get_item(streams, 0); - // make up the timing peer info list part of the response... - // debug(1,"Create timingPeerInfoPlist"); - plist_t timingPeerInfoPlist = plist_new_dict(); - plist_dict_set_item(timingPeerInfoPlist, "Addresses", addresses); - plist_dict_set_item(timingPeerInfoPlist, "ID", plist_new_string(conn->self_ip_string)); - plist_dict_set_item(setupResponsePlist, "timingPeerInfo", timingPeerInfoPlist); - // get a port to use as an event port + plist_t streams_array = plist_new_array(); // to hold the ports and stuff + plist_t stream0dict = plist_new_dict(); + // more stuff + // set up a UDP control stream and thread and a UDP or TCP audio stream and thread - // bind a new TCP port and get a socket - conn->local_event_port = 0; // any port + // bind a new UDP port and get a socket + conn->local_ap2_control_port = 0; // any port + err = bind_socket_and_port(SOCK_DGRAM, conn->connection_ip_family, conn->self_ip_string, + conn->self_scope_id, &conn->local_ap2_control_port, + &conn->ap2_control_socket); + if (err) { + die("Error %d: could not find a UDP port to use as an ap2_control port", err); + } + debug(1, "Connection %d: UDP control port opened: %u.", conn->connection_number, conn->local_ap2_control_port); - int err = - bind_socket_and_port(SOCK_STREAM, conn->connection_ip_family, conn->self_ip_string, - conn->self_scope_id, &conn->local_event_port, &conn->event_socket); - if (err) { - die("Error %d: could not find a TCP port to use as an event port", err); + pthread_create(&conn->rtp_ap2_control_thread, NULL, &rtp_ap2_control_receiver, (void *)conn); + + // get the session key + + plist_t item = plist_dict_get_item(stream0, "shk"); // session key + uint64_t item_value = 0; + plist_get_data_val(item, (char **)&conn->session_key, &item_value); + + // get the DACP-ID and Active Remote for remote control stuff + + char *ar = msg_get_header(req, "Active-Remote"); + if (ar) { + debug(3, "Connection %d: SETUP AP2 -- Active-Remote string seen: \"%s\".", + conn->connection_number, ar); + // get the active remote + if (conn->dacp_active_remote) // this is in case SETUP was previously called + free(conn->dacp_active_remote); + conn->dacp_active_remote = strdup(ar); +#ifdef CONFIG_METADATA + send_metadata('ssnc', 'acre', ar, strlen(ar), req, 1); +#endif + } else { + debug(1, "Connection %d: SETUP AP2 no Active-Remote information the SETUP Record.", + conn->connection_number); + if (conn->dacp_active_remote) { // this is in case SETUP was previously called + free(conn->dacp_active_remote); + conn->dacp_active_remote = NULL; + } + } + + ar = msg_get_header(req, "DACP-ID"); + if (ar) { + debug(3, "Connection %d: SETUP AP2 -- DACP-ID string seen: \"%s\".", + conn->connection_number, ar); + if (conn->dacp_id) // this is in case SETUP was previously called + free(conn->dacp_id); + conn->dacp_id = strdup(ar); +#ifdef CONFIG_METADATA + send_metadata('ssnc', 'daid', ar, strlen(ar), req, 1); +#endif + } else { + debug(1, "Connection %d: SETUP AP2 doesn't include DACP-ID string information.", + conn->connection_number); + if (conn->dacp_id) { // this is in case SETUP was previously called + free(conn->dacp_id); + conn->dacp_id = NULL; + } + } + + // now, get the type of the stream. + item = plist_dict_get_item(stream0, "type"); + item_value = 0; + plist_get_uint_val(item, &item_value); + + switch (item_value) { + case 96: { + debug(1, "Connection %d. Realtime Audio Stream Detected.", conn->connection_number); + debug_log_rtsp_message(2, "Realtime Audio Stream SETUP incoming message", req); + get_play_lock(conn); + conn->airplay_stream_type = realtime_stream; + // bind a new UDP port and get a socket + conn->local_realtime_audio_port = 0; // any port + err = bind_socket_and_port(SOCK_DGRAM, conn->connection_ip_family, conn->self_ip_string, + conn->self_scope_id, &conn->local_realtime_audio_port, + &conn->realtime_audio_socket); + if (err) { + die("Error %d: could not find a UDP port to use as a realtime_audio port", err); + } + debug(1, "Connection %d: UDP realtime audio port opened: %u.", conn->connection_number, conn->local_realtime_audio_port); + + pthread_create(&conn->rtp_realtime_audio_thread, NULL, &rtp_realtime_audio_receiver, + (void *)conn); + + plist_dict_set_item(stream0dict, "type", plist_new_uint(96)); + plist_dict_set_item(stream0dict, "dataPort", + plist_new_uint(conn->local_realtime_audio_port)); + + conn->stream.type = ast_apple_lossless; + debug(3, "An ALAC stream has been detected."); + + // Set reasonable connection defaults + conn->stream.fmtp[0] = 96; + conn->stream.fmtp[1] = 352; + conn->stream.fmtp[2] = 0; + conn->stream.fmtp[3] = 16; + conn->stream.fmtp[4] = 40; + conn->stream.fmtp[5] = 10; + conn->stream.fmtp[6] = 14; + conn->stream.fmtp[7] = 2; + conn->stream.fmtp[8] = 255; + conn->stream.fmtp[9] = 0; + conn->stream.fmtp[10] = 0; + conn->stream.fmtp[11] = 44100; + + // set the parameters of the player (as distinct from the parameters of the decoder -- + // that's done later). + conn->max_frames_per_packet = conn->stream.fmtp[1]; // number of audio frames per packet. + conn->input_rate = conn->stream.fmtp[11]; + conn->input_num_channels = conn->stream.fmtp[7]; + conn->input_bit_depth = conn->stream.fmtp[3]; + conn->input_bytes_per_frame = conn->input_num_channels * ((conn->input_bit_depth + 7) / 8); + debug(2, "Realtime Stream Play"); + if (conn->ap2_timing_peer_list_message) + ptp_send_control_message_string(conn->ap2_timing_peer_list_message); + else + debug(1, "No timing peer list!"); + activity_monitor_signify_activity(1); + player_prepare_to_play(conn); + player_play(conn); + + conn->rtp_running = 1; // hack! + } break; + case 103: { + debug(1, "SETUP on Connection %d. Buffered Audio Stream Detected.", + conn->connection_number); + debug_log_rtsp_message(2, "Buffered Audio Stream SETUP incoming message", req); + get_play_lock(conn); + conn->airplay_stream_type = buffered_stream; + // get needed stuff + + // bind a new TCP port and get a socket + conn->local_buffered_audio_port = 0; // any port + err = bind_socket_and_port(SOCK_STREAM, conn->connection_ip_family, conn->self_ip_string, + conn->self_scope_id, &conn->local_buffered_audio_port, + &conn->buffered_audio_socket); + if (err) { + die("SETUP on Connection %d: Error %d: could not find a TCP port to use as a " + "buffered_audio port", + conn->connection_number, err); + } + + debug(1, "Connection %d: TCP Buffered Audio port opened: %u.", conn->connection_number, conn->local_buffered_audio_port); + + // hack. + conn->max_frames_per_packet = 352; // number of audio frames per packet. + conn->input_rate = 44100; // we are stuck with this for the moment. + conn->input_num_channels = 2; + conn->input_bit_depth = 16; + conn->input_bytes_per_frame = conn->input_num_channels * ((conn->input_bit_depth + 7) / 8); + activity_monitor_signify_activity(1); + player_prepare_to_play( + conn); // get capabilities of DAC before creating the buffered audio thread + + pthread_create(&conn->rtp_buffered_audio_thread, NULL, &rtp_buffered_audio_processor, + (void *)conn); + + plist_dict_set_item(stream0dict, "type", plist_new_uint(103)); + plist_dict_set_item(stream0dict, "dataPort", + plist_new_uint(conn->local_buffered_audio_port)); + plist_dict_set_item(stream0dict, "audioBufferSize", + plist_new_uint(conn->ap2_audio_buffer_size)); + + // this should be cancelled by an activity_monitor_signify_activity(1) + // call in the SETRATEANCHORI handler, which should come up right away + activity_monitor_signify_activity(0); + player_play(conn); + + conn->rtp_running = 1; // hack! + } break; + default: + debug(1, "SETUP on Connection %d: Unhandled stream type %" PRIu64 ".", + conn->connection_number, item_value); + debug_log_rtsp_message(1, "Unhandled stream type incoming message", req); + } + + plist_dict_set_item(stream0dict, "controlPort", plist_new_uint(conn->local_ap2_control_port)); + + plist_array_append_item(streams_array, stream0dict); + plist_dict_set_item(setupResponsePlist, "streams", streams_array); + resp->respcode = 200; + } else if (conn->airplay_stream_category == remote_control_stream) { + debug(1, "Connection %d: SETUP: Remote Control Stream received.", conn->connection_number); + debug_log_rtsp_message(1, "Remote Control Stream stream (second) message", req); + + resp->respcode = 200; + } else { + debug(1, "Connection %d: SETUP: Stream received but no airplay category set. Nothing done.", + conn->connection_number); } - - pthread_create(&conn->rtp_event_thread, NULL, &rtp_event_receiver, (void *)conn); - - plist_dict_set_item(setupResponsePlist, "eventPort", plist_new_uint(conn->local_event_port)); - plist_dict_set_item(setupResponsePlist, "timingPort", plist_new_uint(0)); // dummy } - plist_to_bin(setupResponsePlist, &resp->content, &resp->contentlength); - plist_free(setupResponsePlist); - - msg_add_header(resp, "Content-Type", "application/x-apple-binary-plist"); + if (resp->respcode == 200) { + plist_to_bin(setupResponsePlist, &resp->content, &resp->contentlength); + plist_free(setupResponsePlist); + msg_add_header(resp, "Content-Type", "application/x-apple-binary-plist"); + } debug_log_rtsp_message(2, " SETUP response", resp); - resp->respcode = 200; // it all worked out okay - // } else { - // resp->respcode = 453; - // debug(1, "Connection %d: SETUP failed because another connection is already playing.", - // conn->connection_number); - // } } #endif @@ -2417,8 +2928,8 @@ void handle_setup(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp) { uint16_t cport, tport; char *ar = msg_get_header(req, "Active-Remote"); if (ar) { - debug(2, "Connection %d: SETUP -- Active-Remote string seen: \"%s\".", - conn->connection_number, ar); + debug(2, "Connection %d: SETUP: Active-Remote string seen: \"%s\".", conn->connection_number, + ar); // get the active remote if (conn->dacp_active_remote) // this is in case SETUP was previously called free(conn->dacp_active_remote); @@ -2427,7 +2938,7 @@ void handle_setup(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp) { send_metadata('ssnc', 'acre', ar, strlen(ar), req, 1); #endif } else { - debug(2, "Connection %d: SETUP -- Note: no Active-Remote information the SETUP Record.", + debug(2, "Connection %d: SETUP: Note: no Active-Remote information seen.", conn->connection_number); if (conn->dacp_active_remote) { // this is in case SETUP was previously called free(conn->dacp_active_remote); @@ -2437,7 +2948,7 @@ void handle_setup(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp) { ar = msg_get_header(req, "DACP-ID"); if (ar) { - debug(2, "Connection %d: SETUP -- DACP-ID string seen: \"%s\".", conn->connection_number, ar); + debug(2, "Connection %d: SETUP: DACP-ID string seen: \"%s\".", conn->connection_number, ar); if (conn->dacp_id) // this is in case SETUP was previously called free(conn->dacp_id); conn->dacp_id = strdup(ar); @@ -3689,6 +4200,8 @@ static struct method_handler { void (*handler)(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp); // for AirPlay 1 only } method_handlers[] = {{"OPTIONS", handle_options}, + {"GET", handle_get}, + {"POST", handle_post}, {"ANNOUNCE", handle_announce}, {"FLUSH", handle_flush}, {"TEARDOWN", handle_teardown}, @@ -3957,26 +4470,35 @@ void rtsp_conversation_thread_cleanup_function(void *arg) { int oldState; pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, &oldState); - debug(3, "Connection %d: rtsp_conversation_thread_func_cleanup_function called.", + debug(1, "Connection %d: rtsp_conversation_thread_func_cleanup_function called.", conn->connection_number); - if (conn->player_thread) { - player_stop(conn); - activity_monitor_signify_activity(0); // inactive, and should be after command_stop() - } +#ifdef CONFIG_AIRPLAY_2 + teardown_phase_one(conn); + teardown_phase_two(conn); +#endif debug(3, "Connection %d terminating:Closing timing, control and audio sockets...", conn->connection_number); - if (conn->control_socket) + if (conn->control_socket) { close(conn->control_socket); - if (conn->timing_socket) + } + if (conn->timing_socket) { close(conn->timing_socket); - if (conn->audio_socket) + } + if (conn->audio_socket) { close(conn->audio_socket); + } + + if (conn->fd > 0) { debug(3, "Connection %d terminating: closing fd %d.", conn->connection_number, conn->fd); close(conn->fd); debug(3, "Connection %d terminating: closed fd %d.", conn->connection_number, conn->fd); + + debug(1, "Connection %d: terminating connection from %s:%u to self at %s:%u.", + conn->connection_number, conn->client_ip_string, conn->client_rtsp_port, + conn->self_ip_string, conn->self_rtsp_port); } if (conn->auth_nonce) { free(conn->auth_nonce); @@ -3984,16 +4506,15 @@ void rtsp_conversation_thread_cleanup_function(void *arg) { } #ifdef CONFIG_AIRPLAY_2 - if (conn->client_setup_plist) { - plist_free(conn->client_setup_plist); - conn->client_setup_plist = NULL; - } - buf_drain(&conn->ap2_control_pairing.plain_buf, -1); buf_drain(&conn->ap2_control_pairing.encrypted_buf, -1); pair_setup_free(conn->ap2_control_pairing.setup_ctx); pair_verify_free(conn->ap2_control_pairing.verify_ctx); pair_cipher_free(conn->ap2_control_pairing.cipher_ctx); + if (conn->airplay_gid) { + free(conn->airplay_gid); + conn->airplay_gid = NULL; + } #endif rtp_terminate(conn); @@ -4089,14 +4610,15 @@ static void *rtsp_conversation_thread_func(void *pconn) { resp = msg_init(); pthread_cleanup_push(msg_cleanup_function, (void *)&resp); resp->respcode = 400; - - if (strcmp(req->method, "OPTIONS") != - 0) // the options message is very common, so don't log it until level 3 - debug_level = 2; - debug(debug_level, - "Connection %d: Received an RTSP Packet of type \"%s\":", conn->connection_number, - req->method), - debug_print_msg_headers(debug_level, req); + /* + if (strcmp(req->method, "OPTIONS") != + 0) // the options message is very common, so don't log it until level 3 + debug_level = 2; + debug(debug_level, + "Connection %d: Received an RTSP Packet of type \"%s\":", conn->connection_number, + req->method), + debug_print_msg_headers(debug_level, req); + */ apple_challenge(conn->fd, req, resp); hdr = msg_get_header(req, "CSeq"); @@ -4254,7 +4776,7 @@ void rtsp_listen_loop_cleanup_handler(__attribute__((unused)) void *arg) { int oldState; pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, &oldState); debug(2, "rtsp_listen_loop_cleanup_handler called."); - cancel_all_RTSP_threads(); + cancel_all_RTSP_threads(unspecified_stream_category, 0); // kill all RTSP listeners int *sockfd = (int *)arg; mdns_unregister(); if (sockfd) { @@ -4346,7 +4868,7 @@ void *rtsp_listen_loop(__attribute((unused)) void *arg) { debug(1, "unable to listen on %s port %d. The error is: \"%s\".", family, config.port, strerror(errno)); } else { - listen(fd, 5); + listen(fd, 255); nsock++; sockfd = realloc(sockfd, (nsock + 1) * sizeof(int)); sockfd[nsock] = fd; @@ -4374,119 +4896,13 @@ void *rtsp_listen_loop(__attribute((unused)) void *arg) { maxfd = sockfd[i]; } - char **t1; // ap1 test records - char **t2; // two text records - - t1 = NULL; - t2 = NULL; - - // make up a txt record - char *txt_records[64]; - char **p = txt_records; - t1 = p; // first set of text records - - // make up a firmware version - char firmware_version[64]; -#ifdef CONFIG_USE_GIT_VERSION_STRING - if (git_version_string[0] != '\0') - snprintf(firmware_version, sizeof(firmware_version), "fv=%s", git_version_string); - else -#endif - snprintf(firmware_version, sizeof(firmware_version), "fv=%s", PACKAGE_VERSION); - -#ifdef CONFIG_AIRPLAY_2 - char ap1_featuresString[64]; - uint64_t features_hi = config.airplay_features; - features_hi = (features_hi >> 32) & 0xffffffff; - uint64_t features_lo = config.airplay_features; - features_lo = features_lo & 0xffffffff; - snprintf(ap1_featuresString, sizeof(ap1_featuresString), "ft=0x%" PRIX64 ",0x%" PRIX64 "", - features_lo, features_hi); - char pkString[128]; - snprintf(pkString, sizeof(pkString), "pk="); - pkString_make(pkString + strlen("pk="), sizeof(pkString) - strlen("pk="), - config.airplay_device_id); - - // the ap1 text record is different if it is set up for ap2 - *p++ = "cn=0,1"; - *p++ = "da=true"; - *p++ = "et=0,4"; - *p++ = ap1_featuresString; - *p++ = firmware_version; - *p++ = "md=2"; - *p++ = "am=Shairport Sync"; - *p++ = "sf=0x4"; - *p++ = "tp=UDP"; - *p++ = "vn=65537"; - *p++ = "vs=366.0"; - *p++ = pkString; - *p++ = NULL; - -#else - // here, just replicate what happens in mdns.h when using those #defines - *p++ = "sf=0x4"; - *p++ = firmware_version; - *p++ = "am=ShairportSync"; - *p++ = "vs=105.1"; - *p++ = "tp=TCP,UDP"; - *p++ = "vn=65537"; -#ifdef CONFIG_METADATA - if (config.get_coverart == 0) - *p++ = "md=0,2"; - else - *p++ = "md=0,1,2"; -#endif - *p++ = "ss=16"; - *p++ = "sr=44100"; - *p++ = "da=true"; - *p++ = "sv=false"; - *p++ = "et=0,1"; - *p++ = "ek=1"; - *p++ = "cn=0,1"; - *p++ = "ch=2"; - *p++ = "txtvers=1"; - if (config.password == 0) - *p++ = "pw=false"; - else - *p++ = "pw=true"; - - *p++ = NULL; -#endif - + char **t1 = txt_records; // ap1 test records + char **t2 = NULL; // possibly two text records #ifdef CONFIG_AIRPLAY_2 // make up a secondary set of text records - char *secondary_txt_records[64]; - p = secondary_txt_records; - t2 = p; // second set of text records in AirPlay 2 only - - *p++ = "srcvers=366.0"; - char deviceIdString[64]; - snprintf(deviceIdString, sizeof(deviceIdString), "deviceid=%s", config.airplay_device_id); - *p++ = deviceIdString; - char featuresString[64]; - snprintf(featuresString, sizeof(featuresString), "features=0x%" PRIX64 ",0x%" PRIX64 "", - features_lo, features_hi); - *p++ = featuresString; - char statusflagsString[32]; - snprintf(statusflagsString, sizeof(statusflagsString), "flags=0x%" PRIX32, - config.airplay_statusflags); - *p++ = statusflagsString; - *p++ = "protovers=1.1"; - *p++ = "acl=0"; - *p++ = "rsf=0x0"; - *p++ = firmware_version; - *p++ = "model=Shairport Sync"; - char piString[64]; - snprintf(piString, sizeof(piString), "pi=%s", config.airplay_pi); - *p++ = piString; - char gidString[64]; - snprintf(gidString, sizeof(gidString), "gid=%s", config.airplay_gid); - *p++ = gidString; - *p++ = "gcgl=0"; - *p++ = pkString; - *p++ = NULL; + t2 = secondary_txt_records; // second set of text records in AirPlay 2 only #endif - + build_bonjour_strings(NULL); // no conn yet mdns_register(t1, t2); pthread_setcancelstate(oldState, NULL); @@ -4573,7 +4989,7 @@ void *rtsp_listen_loop(__attribute((unused)) void *arg) { inet_ntop(conn->connection_ip_family, self_addr, conn->self_ip_string, sizeof(conn->self_ip_string)); - debug(2, "Connection %d: new connection from %s:%u to self at %s:%u.", + debug(1, "Connection %d: new connection from %s:%u to self at %s:%u.", conn->connection_number, conn->client_ip_string, conn->client_rtsp_port, conn->self_ip_string, conn->self_rtsp_port); } else { diff --git a/shairport-sync.spec b/shairport-sync.spec index 215b303c..ed0c3d49 100644 --- a/shairport-sync.spec +++ b/shairport-sync.spec @@ -1,5 +1,5 @@ Name: shairport-sync -Version: 3.3.7 +Version: 3.3.8 Release: 1%{?dist} Summary: AirTunes emulator. Multi-Room with Audio Synchronisation # MIT licensed except for tinysvcmdns under BSD, @@ -66,29 +66,31 @@ getent passwd %{name} &> /dev/null || useradd --system -c "%{name} User" \ %license LICENSES %changelog -* Tue Nov 1 2020 Mike Brady 3.3.8 +- Bug fixes and enhancements. +* Tue Nov 1 2020 Mike Brady <4265913+mikebrady@users.noreply.github.com> 3.3.7 - Jack resampling, bug fixes and additions to the pipe and stdout backends, to metadata and to the D-Bus and MPRIS interfaces. -* Thu Feb 20 2020 Mike Brady 3.3.6 - Jack resampling, bug fixes and additions to the D-Bus and MPRIS interfaces. -* Wed Nov 13 2019 Mike Brady 3.3.5 - Bug fixes and additions to the D-Bus interface. -* Mon Oct 28 2019 Mike Brady 3.3.4 - Bug fixes and minor enhancements. -* Fri Jul 26 2019 Mike Brady 3.3.2 - Minor bug fixes. -* Wed Jun 05 2019 Mike Brady 3.3.1 - Bug fixes. -* Fri May 24 2019 Mike Brady 3.3 - Audio enhancements, stability improvements, MQTT and Jack Audio interfaces. -* Sun Oct 14 2018 Mike Brady 3.2.2 - Compatibility with iOS 12 and mac OS Mojave AirPlay latencies. Minor bug fix. -* Fri Jul 13 2018 Mike Brady 3.2.1 - Stability improvements when soxr interpolation is chosen. -* Mon Jul 09 2018 Mike Brady 3.2 - New D-Bus and MPRIS Interfaces, Bug Fixes and Enhancements. -* Thu Dec 21 2017 Mike Brady 3.1.7 +* Thu Dec 21 2017 Mike Brady <4265913+mikebrady@users.noreply.github.com> 3.1.7 - Bug fix for unexpectedly resuming play at full volume from iOS 11.2 and macOS 10.3.2. -* Mon Dec 11 2017 Mike Brady 3.1.5 +* Mon Dec 11 2017 Mike Brady <4265913+mikebrady@users.noreply.github.com> 3.1.5 - Bug fixes and better compatibility with iOS 11.2 and mac OS 10.13.2. - Better AirPlay synchronisation. * Wed Sep 13 2017 Bill Peck 3.1.2-1 @@ -115,27 +117,27 @@ getent passwd %{name} &> /dev/null || useradd --system -c "%{name} User" \ - new optional loudness and convolution filters - improvements in non-synchronised backends - enhancements, stability improvements and bug fixes -* Fri Feb 24 2017 Mike Brady 2.8.6 +* Fri Feb 24 2017 Mike Brady <4265913+mikebrady@users.noreply.github.com> 2.8.6 - Many changes including 8- 16- 24- and 32-bit output -* Fri Oct 21 2016 Mike Brady 2.8.6 +* Fri Oct 21 2016 Mike Brady <4265913+mikebrady@users.noreply.github.com> 2.8.6 - Advertise self as ShairportSync rather than AirPort device 2.8.6 -* Sun Sep 25 2016 Mike Brady 2.8.5 +* Sun Sep 25 2016 Mike Brady <4265913+mikebrady@users.noreply.github.com> 2.8.5 - Bug fixes and small enhancements 2.8.5 -* Sat May 28 2016 Mike Brady 2.8.4 +* Sat May 28 2016 Mike Brady <4265913+mikebrady@users.noreply.github.com> 2.8.4 - Bug fixes and a few small enhancements 2.8.4 -* Fri Apr 15 2016 Mike Brady 2.8.2 +* Fri Apr 15 2016 Mike Brady <4265913+mikebrady@users.noreply.github.com> 2.8.2 - Stability improvements, bug fixes and a few special-purpose settings 2.8.2 -* Wed Mar 02 2016 Mike Brady 2.8.1 +* Wed Mar 02 2016 Mike Brady <4265913+mikebrady@users.noreply.github.com> 2.8.1 - Stability improvements and important bug fixes 2.8.1 -* Sat Jan 30 2016 Mike Brady 2.8.0 +* Sat Jan 30 2016 Mike Brady <4265913+mikebrady@users.noreply.github.com> 2.8.0 - Enhancements and bug fixes 2.8.0 -* Sun Oct 18 2015 Mike Brady 2.6 +* Sun Oct 18 2015 Mike Brady <4265913+mikebrady@users.noreply.github.com> 2.6 - Important enhancements and bug fixes 2.6 -* Thu Aug 27 2015 Mike Brady 2.4.1 +* Thu Aug 27 2015 Mike Brady <4265913+mikebrady@users.noreply.github.com> 2.4.1 - Minor bug fixes 2.4.1 -* Thu Aug 27 2015 Mike Brady 2.4 +* Thu Aug 27 2015 Mike Brady <4265913+mikebrady@users.noreply.github.com> 2.4 - Prepare for stable release 2.4 -* Wed Aug 26 2015 Mike Brady 2.3.13.1-1 +* Wed Aug 26 2015 Mike Brady <4265913+mikebrady@users.noreply.github.com> 2.3.13.1-1 - Harmonise release numbers * Fri Jul 24 2015 Bill Peck 2.3.7-1 - Initial spec file diff --git a/shairport.c b/shairport.c index 0f7f141f..60f6ef0c 100644 --- a/shairport.c +++ b/shairport.c @@ -1494,6 +1494,8 @@ int main(int argc, char **argv) { exit(EXIT_SUCCESS); } + log_to_syslog(); + #ifdef CONFIG_LIBDAEMON pid = getpid(); #endif @@ -1582,49 +1584,6 @@ int main(int argc, char **argv) { #ifdef CONFIG_AIRPLAY_2 config.timeout = 0; // disable watchdog config.port = 7000; - // the features code is a 64-bit number, but in the mDNS advertisement, the least significant 32 - // bit are given first for example, if the features number is 0x1C340405F4A00, it will be given as - // features=0x405F4A00,0x1C340 in the mDNS string, and in a signed decimal number in the plist: - // 496155702020608 this setting here is the source of both the plist features response and the - // mDNS string. - // note: 0x300401F4A00 works but with weird delays and stuff - config.airplay_features = 0x1C340405FCA00; - // Advertised with mDNS and returned with GET /info, see - // https://openairplay.github.io/airplay-spec/status_flags.html 0x4: Audio cable attached, no PIN - // required (transient pairing), 0x204: Audio cable attached, OneTimePairingRequired 0x604: Audio - // cable attached, OneTimePairingRequired, device was setup for Homekit access control - config.airplay_statusflags = 0x4; - // Set to NULL to work with transient pairing - config.airplay_pin = NULL; - - // use the config.hw_addr to generated an airplay_device_id - { - char obf[256] = {0}; - char *obfp = obf; - int i; - for (i = 0; i < 6; i++) { - snprintf(obfp, 4, "%02X:", config.hw_addr[i]); - obfp += 3; - } - obfp -= 1; - *obfp = 0; - config.airplay_device_id = strdup(obf); - } - debug(1, "Started in Airplay 2 mode on device \"%s\"!", config.airplay_device_id); - - // now generate a UUID - // from https://stackoverflow.com/questions/51053568/generating-a-random-uuid-in-c - // with thanks - uuid_t binuuid; - uuid_generate_random(binuuid); - - char *uuid = malloc(UUID_STR_LEN); - // Produces a UUID string at uuid consisting of lower-case letters - uuid_unparse_lower(binuuid, uuid); - config.airplay_pi = strdup(uuid); - config.airplay_gid = strdup(uuid); // initially the gid is the same as the pi - - // now we need to create the sequence of items for a Bonjour TXT record. #else config.port = 5000; #endif @@ -1818,10 +1777,58 @@ int main(int argc, char **argv) { } #endif - debug(1, "Started!"); #ifdef CONFIG_AIRPLAY_2 - debug(1, "Started in Airplay 2 mode!"); + // the features code is a 64-bit number, but in the mDNS advertisement, the least significant 32 + // bit are given first for example, if the features number is 0x1C340405F4A00, it will be given as + // features=0x405F4A00,0x1C340 in the mDNS string, and in a signed decimal number in the plist: + // 496155702020608 this setting here is the source of both the plist features response and the + // mDNS string. + // note: 0x300401F4A00 works but with weird delays and stuff + // config.airplay_features = 0x1C340405FCA00; + uint64_t mask = ((uint64_t)1 << 17) | ((uint64_t)1 << 16) | ((uint64_t)1 << 15) | ((uint64_t)1 << 50); + config.airplay_features = 0x1C340405D4A00 & ~mask; // APX + Authentication4 (b14) with no metadata (see below) + // Advertised with mDNS and returned with GET /info, see + // https://openairplay.github.io/airplay-spec/status_flags.html 0x4: Audio cable attached, no PIN + // required (transient pairing), 0x204: Audio cable attached, OneTimePairingRequired 0x604: Audio + // cable attached, OneTimePairingRequired, device was setup for Homekit access control + + // If we are asking for metadata, turn on the relevant bits + if (config.metadata_enabled != 0) + config.airplay_features |= (1 << 17) | (1 << 16); + // If we are asking for artwork, turn on the relevant bit + if (config.get_coverart) + config.airplay_features |= (1 << 15); + debug(1,"Features: 0x%" PRIx64 ".", config.airplay_features); + config.airplay_statusflags = 0x04; + // Set to NULL to work with transient pairing + config.airplay_pin = NULL; + + // use the config.hw_addr to generated an airplay_device_id + { + char obf[256] = {0}; + char *obfp = obf; + int i; + for (i = 0; i < 6; i++) { + snprintf(obfp, 4, "%02X:", config.hw_addr[i]); + obfp += 3; + } + obfp -= 1; + *obfp = 0; + config.airplay_device_id = strdup(obf); + } + + // now generate a UUID + // from https://stackoverflow.com/questions/51053568/generating-a-random-uuid-in-c + // with thanks + uuid_t binuuid; + uuid_generate_random(binuuid); + + char *uuid = malloc(UUID_STR_LEN); + // Produces a UUID string at uuid consisting of lower-case letters + uuid_unparse_lower(binuuid, uuid); + config.airplay_pi = strdup(uuid); + debug(1, "Started in Airplay 2 mode on device \"%s\"!", config.airplay_device_id); #else debug(1, "Started in Airplay 1 mode!"); #endif diff --git a/tinysvcmdns.c b/tinysvcmdns.c index 9e605e07..55e82ad2 100644 --- a/tinysvcmdns.c +++ b/tinysvcmdns.c @@ -735,7 +735,8 @@ static size_t mdns_parse_rr(uint8_t *pkt_buf, size_t pkt_len, size_t off, struct if (name == NULL) goto err; - // parse the MDNS RR section p += label_len(pkt_buf, pkt_len, off); + // parse the MDNS RR section + p += label_len(pkt_buf, pkt_len, off); rr->name = name; rr->type = mdns_read_u16(p);