Compare commits

..
38 Commits
Author SHA1 Message Date
nils c343dd56df Show the ohm backend in the version string
Set variables for a Docker build (used by another workflow). / Set variables required for docker build. (push) Successful in 1s
Build and conditionally push docker image / docker-vars (push) Successful in 1s
Build and conditionally push docker image / Build and conditionally push docker image (SHAIRPORT_SYNC_BRANCH=. , ./docker/classic/Dockerfile, classic) (push) Failing after 31m41s
Build and conditionally push docker image / Build and conditionally push docker image (SHAIRPORT_SYNC_BRANCH=. NQPTP_BRANCH=${{ needs.docker-vars.outputs.nqptp_branch }} , ./docker/Dockerfile, main) (push) Failing after 31m43s
2026-08-29 00:18:49 +02:00
nils d6a3ca335a Add runtime and nqptp image targets to the ohm Dockerfile
Set variables for a Docker build (used by another workflow). / Set variables required for docker build. (push) Successful in 1s
Build and conditionally push docker image / docker-vars (push) Successful in 1s
Build and conditionally push docker image / Build and conditionally push docker image (SHAIRPORT_SYNC_BRANCH=. , ./docker/classic/Dockerfile, classic) (push) Failing after 31m49s
Build and conditionally push docker image / Build and conditionally push docker image (SHAIRPORT_SYNC_BRANCH=. NQPTP_BRANCH=${{ needs.docker-vars.outputs.nqptp_branch }} , ./docker/Dockerfile, main) (push) Failing after 31m50s
The runtime image carries only shairport-sync and its libraries and
expects the node's avahi via the host D-Bus socket; nqptp runs as a
sidecar sharing /dev/shm.
2026-08-29 00:15:32 +02:00
nils b75edeedc3 Add ohm, an OpenHome Songcast sender audio backend
Set variables for a Docker build (used by another workflow). / Set variables required for docker build. (push) Successful in 1s
Build and conditionally push docker image / docker-vars (push) Successful in 2s
Build and conditionally push docker image / Build and conditionally push docker image (SHAIRPORT_SYNC_BRANCH=. , ./docker/classic/Dockerfile, classic) (push) Failing after 31m52s
Build and conditionally push docker image / Build and conditionally push docker image (SHAIRPORT_SYNC_BRANCH=. NQPTP_BRANCH=${{ needs.docker-vars.outputs.nqptp_branch }} , ./docker/Dockerfile, main) (push) Failing after 31m49s
Streams AirPlay audio to OpenHome/Songcast receivers as OHU unicast PCM
(16/24-bit). Sends are paced on the PTP master clock so receivers slave
to the AirPlay group clock through their buffer-fill servo; MediaLatency
carries the phase, matching the wire behaviour of Linn's own sender
(zero flags and timestamps). delay() reports the newest frame's playtime
lead so shairport feeds a receiver latency ahead. Handles Join, Listen,
Leave and Resend with a frame history, and forwards track metadata and
metatext from the metadata hub.

With -r <name>, ohm_upnp controls the receiver over UPnP: discovered by
friendly name via SSDP at time of use, woken, switched to its Songcast
source and pointed at this sender when a session starts; volume follows
the AirPlay slider capped at the device's VolumeLimit; standby follows
some minutes after the session ends.
2026-08-28 23:54:16 +02:00
Mike Brady fadd43d06e Merge branch 'development' to 'master' 5.2.2.
Summary of changes:
* If the output rate is different to the input rate (e.g. 48,000 vs 44,100), look for the deepest output bit depth so that the transcoder has the maximum dynamic range available to minimise transcoding inaccuracies.
* Fix a bug whereby the audio backend latency offset was mishandled in AirPlay 2 operation.
* Fix an output format selection bug.
* Update the README reference to the MPRIS specification to the latest version.
* Remove the explicit `-` for `xxd` to read from `STDIN`. It's not necessary, and removing it makes it easier to package Shairport Sync on other build systems, such as OpenWrt.
2026-08-22 08:59:45 +01:00
Mike BradyandGitHub 6e10eca884 Update MPRIS specification reference in documentation [skip ci]
Updated README reference to the latest MPRIS specification version, acknowledging Dmitry Batin for the contribution.
2026-08-21 12:05:57 +01:00
Mike BradyandGitHub 5e170a67a8 Merge pull request #2255 from demosspro/patch-1
Update README.md to link to latest MPRIS information.
2026-08-21 12:00:19 +01:00
DMITRY BATINandGitHub 3ba7dfea6f Update README.md
Old link not working. Add a valid link to MPRIS spec.
- [v ] Documentation update
2026-08-21 10:56:47 +03:00
Mike BradyandGitHub 6cdd0eb794 Update release notes for version 5.3-dev-21 [skip ci]
Added enhancement regarding output rate and bit depth for transcoding.
2026-08-20 15:51:25 +01:00
Mike Brady 6966a74c82 If the output rate is different to the input rate, look for the deepest output bit depth, to give the transcoder the chance to minimise transcoding inaccuracies. 2026-08-20 15:47:37 +01:00
Mike BradyandGitHub c4e17b60ef Update release notes for version 5.3-dev-19 [skip ci]
Added a bug fix for audio backend latency offset handling in AirPlay 2 operation, including background information and acknowledgments.
2026-08-20 11:13:42 +01:00
Mike Brady b69e01cdf9 Correct the treatment of audio backend latency offset in Realtime streams. Fix a warning if the offset is too negative and that would require frames to be played before they could possibly arrive. 2026-08-20 10:55:30 +01:00
Mike BradyandGitHub d8165c1143 Merge pull request #2253 from prhodey/fix/deferred-airplay2-latency [skip ci]
Defer buffered AirPlay 2 latency offset conversion. (It needs a further little adjustment to handle Realtime streams, but that will be done in the next commit.)
2026-08-20 10:48:35 +01:00
prhodey 4df0145372 Defer buffered AirPlay 2 latency offset conversion 2026-08-19 20:10:50 -04:00
Mike Brady 08af668a5d Remove reference to log_to_syslog() if libdaemon support is included. 2026-07-29 12:15:36 +02:00
Mike Brady 42bd29d6da This is version 5.2 2026-07-28 17:15:07 +01:00
Mike Brady d6ac53bf4c Bug fix, docker-compose updates, statistics readoout improvement. 2026-07-02 14:17:53 +01:00
Mike Brady 5bd5f9c1d0 Merge branch 'master' of github.com:mikebrady/shairport-sync
Online update.
2026-06-27 18:52:36 +01:00
Mike Brady 1718f7b5af Add Jack deprecation note to the configuration file. 2026-06-22 18:53:51 +01:00
Mike Brady 368f3696c3 Add note about new --with-metadata- configuration settings. 2026-06-22 18:39:13 +01:00
Mike BradyandGitHub 4d0e9bb6c8 Remove markdown test from Bug Report template [skip ci]
Removed markdown test section from Bug Report template.
2026-06-22 16:55:05 +01:00
Mike BradyandGitHub 83961baad9 Add markdown test section to Bug Report template [skip ci]
Added a markdown test section to the Bug Report template.
2026-06-22 16:53:45 +01:00
Mike Brady d08637d3d6 Merge branch 'development'
Update of configuration flag documentation.
2026-06-22 13:09:06 +01:00
Mike Brady a6231e089c Merge branch 'development'
Update settings document.
2026-06-22 13:05:53 +01:00
Mike Brady c454867a04 Merge branch 'development' 2026-06-22 11:54:59 +01:00
Mike BradyandGitHub befe1d4a1a Fix comment for systemd-dev installation command [skip ci] 2026-05-01 15:52:01 +01:00
Mike Brady eed1b9f0b9 Fix missing configuration mapping parameter in PulseAudio backend initialisation. 2026-04-27 12:43:51 +01:00
Mike BradyandGitHub 930e523757 Update instructions for systemd-dev installation [skip ci]
Added caution regarding installing systemd-dev on backported systems.
2026-04-27 09:42:49 +01:00
Mike Brady 6ef48e59e6 5.0.3 2026-04-25 11:22:18 +01:00
Mike BradyandGitHub 06e824be0c Merge pull request #2192 from mikebrady/revert-2188-mqtt-inform-publish-errors
Revert "mqtt: promote publish-error log paths to inform() for debuggability"

This needs to be done to the `development` branch.
2026-04-20 10:10:36 +01:00
Mike BradyandGitHub 32458c08aa Revert "mqtt: promote publish-error log paths to inform() for debuggability" 2026-04-20 10:09:44 +01:00
Mike BradyandGitHub 4ecd42eda3 Merge pull request #2188 from harreh3iesh/mqtt-inform-publish-errors
MQTT: promote publish-error log paths to inform() for debuggability
2026-04-20 10:09:29 +01:00
harreh3iesh 2ee44830d3 mqtt: promote publish-error log paths to inform() for debuggability
The two error branches in mqtt_publish() logged at debug(1), which is
invisible at default syslog verbosity. A silently-dropped MQTT publish
is the exact failure mode users report as 'MQTT just stopped working'
with no errors anywhere in the logs. Raising these to inform() makes
such drops legible at default verbosity and includes the target topic
plus mosquitto_strerror() text in the non-NO_CONN branch so the
diagnostic actually names the failing call site.
2026-04-14 20:48:56 -04:00
Mike Brady 28eb383155 Fix a D-Bus Test Client compilation bug; turn off lots of less relevant debug messages; change how blocks of audio very far in the future are handled; add pw=true to the Bonjour information for classic AirPlay when built for AirPlay 2. 2026-03-28 18:42:12 +00:00
Mike BradyandGitHub dc7a048bd3 Update macOS version for build job 2026-03-27 08:14:30 +00:00
Mike BradyandGitHub dc4c334adf Update version number to 5.0.1 in configure.ac [skip ci] 2026-02-27 15:15:10 +00:00
Mike Brady 1230285491 5.0.1 2026-02-27 14:57:28 +00:00
Mike BradyandGitHub c956d763ca Fix punctuation and formatting in BUILD.md [skip ci] 2026-02-14 18:26:02 +00:00
Mike BradyandGitHub 7421421848 Refactor Docker workflow for tag handling 2026-02-14 12:54:47 +00:00
17 changed files with 2376 additions and 3784 deletions
+4 -1
View File
@@ -1,4 +1,7 @@
.github
documents
docker/Dockerfile
docker/classic/Dockerfile
docker/classic/Dockerfile
reference
ohm.txt
out-ohm
+1 -1
View File
@@ -44,7 +44,7 @@ Shairport Sync offers AirPlay 2 support for audio sources on:
- Transcoding: the output will be transcoded if necessary (e.g. from 44,100 to 48,000 fps) to match the output device's rate.
- Mixdown: mixdown to fewer output channels (e.g. 5.1 to stereo) is automatic but can be controlled.
- Shairport Sync can revert to "classic" AirPlay if necessary.
- Devices running Shairport Sync in AirPlay 2 mode can be [added](https://github.com/mikebrady/shairport-sync/blob/development/ADDINGTOHOME.md) to the Home app.
- Devices running Shairport Sync in AirPlay 2 mode can be [added](https://github.com/mikebrady/shairport-sync/blob/master/ADDINGTOHOME.md) to the Home app.
- Shairport Sync can be built to support classic AirPlay (aka "AirPlay 1") only. Classic Airplay offers only one format, ALAC/S16/44100/2, but output transcoding is available to, for example, 48000 fps.
## What Does Not Work
+1
View File
@@ -89,6 +89,7 @@ If you are building classic Shairport Sync, the list of packages is shorter:
libpopt-dev libconfig-dev libasound2-dev avahi-daemon libavahi-client-dev libssl-dev libsoxr-dev \
libavutil-dev libavcodec-dev libavformat-dev
```
Building on Ubuntu 24.10 or Debian 13 ("Trixie") and later – and possibly on other distributions – requires `systemd-dev`. Be very careful here if you are using backports -- there is a [report](https://github.com/mikebrady/shairport-sync/issues/2195#issuecomment-4325326002) that installing `systemd-dev` on a backported system can severely damage it.
```
$ apt install --dry-run --no-install-recommends systemd-dev
+4
View File
@@ -112,6 +112,10 @@ if USE_DUMMY
shairport_sync_SOURCES += audio_dummy.c
endif
if USE_OHM
shairport_sync_SOURCES += audio_ohm.c ohm_upnp.c
endif
if USE_AO
shairport_sync_SOURCES += audio_ao.c
endif
+3 -3
View File
@@ -16,15 +16,15 @@ Shairport Sync does not support AirPlay video or photo streaming.
* Next Steps and Advanced Topics are [here](ADVANCED%20TOPICS/README.md).
* Runtime settings are documented [here](scripts/shairport-sync.conf).
* Build configuration options are detailed in [CONFIGURATION FLAGS.md](CONFIGURATION%20FLAGS.md).
* The `man` page, detailing command line options, is [here](https://raw.githack.com/mikebrady/shairport-sync/development/man/shairport-sync.1.xml).
* Some advanced topics are developed in [ADVANCED TOPICS](https://github.com/mikebrady/shairport-sync/tree/master/ADVANCED%20TOPICS).
* The `man` page, detailing command line options, is [here](https://raw.githack.com/mikebrady/shairport-sync/master/man/shairport-sync.1.xml).
* Some advanced topics and developed in [ADVANCED TOPICS](https://github.com/mikebrady/shairport-sync/tree/master/ADVANCED%20TOPICS).
# Features
* Outputs AirPlay audio to [ALSA](https://www.alsa-project.org/wiki/Main_Page), [sndio](http://www.sndio.org), [PipeWire](https://pipewire.org), [PulseAudio](https://www.freedesktop.org/wiki/Software/PulseAudio/), to a unix pipe or to `STDOUT`. It also has limited support for [libao](https://xiph.org/ao/).
* Metadata — Shairport Sync can deliver metadata supplied by the source, such as Album Name, Artist Name, Cover Art, etc. through a pipe or UDP socket to a recipient application program — see https://github.com/mikebrady/shairport-sync-metadata-reader for a sample recipient. Sources that supply metadata include iTunes and the Music app in macOS and iOS.
* An interface to [MQTT](https://en.wikipedia.org/wiki/MQTT), a popular protocol for Inter Process Communication, Machine-to-Machine, Internet of Things and Home Automation projects. The interface provides access to metadata and artwork, with remote control for Classic AirPlay clients.
* Digital Signal Processing facilities – please see the [DSP Wiki Page Guide](https://github.com/mikebrady/shairport-sync/wiki/Digital-Signal-Processing-with-Shairport-Sync). (Thanks to [Yann Pomarède](https://github.com/yannpom) for the code and to [Paul Wieland](https://github.com/PaulWieland) for the guide.)
* An [MPRIS](https://specifications.freedesktop.org/mpris-spec/2.2/)-like interface, partially complete and very functional, including access to metadata and artwork, and partial remote control for Classic AirPlay clients.
* An [MPRIS](https://specifications.freedesktop.org/mpris/latest/)-like interface, partially complete and very functional, including access to metadata and artwork, and partial remote control for Classic AirPlay clients.
* A native D-Bus interface, including access to metadata, artwork and system settings. Remote control is also available for Classic AirPlay clients.
* Better Volume Control — Shairport Sync offers finer control at very top and very bottom of the volume range. See http://tangentsoft.net/audio/atten.html for a good discussion of audio "attenuators", upon which volume control in Shairport Sync is modelled. See also the diagram of the volume transfer function in the documents folder. In addition, Shairport Sync can offer an extended volume control range on devices with a restricted range.
* Flexible output rates, formats and channels with built-in transcoding.
File diff suppressed because it is too large Load Diff
+10 -4
View File
@@ -62,6 +62,9 @@ extern audio_output audio_pipe;
#ifdef CONFIG_STDOUT
extern audio_output audio_stdout;
#endif
#ifdef CONFIG_OHM
extern audio_output audio_ohm;
#endif
static audio_output *outputs[] = {
#ifdef CONFIG_ALSA
@@ -93,6 +96,9 @@ static audio_output *outputs[] = {
#endif
#ifdef CONFIG_DUMMY
&audio_dummy,
#endif
#ifdef CONFIG_OHM
&audio_ohm,
#endif
NULL};
@@ -994,7 +1000,7 @@ int32_t search_for_suitable_configuration(unsigned int channels, unsigned int ra
}
#ifdef CONFIG_FFMPEG
if (local_format == SPS_FORMAT_UNKNOWN) {
debug(3, "check for the next highest rate above %u with %u channels.", rate,
debug(3, "check for the next highest rate above %u with %u channels and the best format.", rate,
local_channels);
unsigned int rate_pointer = 0;
while ((rate_pointer < sizeof(rates) / sizeof(unsigned int)) &&
@@ -1002,7 +1008,7 @@ int32_t search_for_suitable_configuration(unsigned int channels, unsigned int ra
local_rate = rates[rate_pointer];
if (local_rate > rate) {
local_format = check_configuration_with_formats(
local_channels, local_rate, (sps_format_t)format, check_configuration);
local_channels, local_rate, SPS_FORMAT_S32, check_configuration);
}
if (local_format == SPS_FORMAT_UNKNOWN) {
rate_pointer++;
@@ -1012,13 +1018,13 @@ int32_t search_for_suitable_configuration(unsigned int channels, unsigned int ra
if (local_format == SPS_FORMAT_UNKNOWN) {
int rate_pointer = (int)(sizeof(rates) / sizeof(unsigned int) - 1);
debug(3, "check for the next lowest rate below %u with %u channels.", rate,
debug(3, "check for the next lowest rate below %u with %u channels and the best format.", rate,
local_channels);
while ((rate_pointer >= 0) && (local_format == SPS_FORMAT_UNKNOWN)) {
local_rate = rates[rate_pointer];
if (local_rate < rate) {
local_format = check_configuration_with_formats(
local_channels, local_rate, (sps_format_t)format, check_configuration);
local_channels, local_rate, SPS_FORMAT_S32, check_configuration);
}
if (local_format == SPS_FORMAT_UNKNOWN) {
rate_pointer--;
+1560
View File
File diff suppressed because it is too large Load Diff
-2
View File
@@ -768,8 +768,6 @@ void stream_write_cb(pa_stream *local_stream, size_t requested_bytes,
pthread_cleanup_pop(1); // release the mutex
if (ret != 0)
debug(1, "error writing to pa buffer");
// debug(1,"<<<Frames requested %d, written to pa: %d, corked status:
// %d.",requested_bytes/4,bytes_transferred/4,pa_stream_is_corked(local_stream));
}
void stream_success_cb(__attribute__((unused)) pa_stream *local_stream,
+3
View File
@@ -1948,6 +1948,9 @@ char *get_version_string() {
#ifdef CONFIG_PIPE
strcat(version_string, "-pipe");
#endif
#ifdef CONFIG_OHM
strcat(version_string, "-ohm");
#endif
#ifdef CONFIG_SOXR
strcat(version_string, "-soxr");
#endif
+7 -1
View File
@@ -1,7 +1,7 @@
# Process this file with autoconf to produce a configure script.
AC_PREREQ([2.50])
AC_INIT([shairport-sync], [5.3-dev], [4265913+mikebrady@users.noreply.github.com])
AC_INIT([shairport-sync], [5.2.2], [4265913+mikebrady@users.noreply.github.com])
: ${CFLAGS="-O3"}
: ${CXXFLAGS="-O3"}
AM_INIT_AUTOMAKE([subdir-objects])
@@ -98,6 +98,12 @@ if test "x$with_pipe" = "xyes" ; then
fi
AM_CONDITIONAL([USE_PIPE], [test "x$with_pipe" = "xyes" ])
AC_ARG_WITH([ohm],[AS_HELP_STRING([--with-ohm],[build the OpenHome Songcast OHM/OHU unicast sender audio backend into Shairport Sync])])
if test "x$with_ohm" = "xyes" ; then
AC_DEFINE([CONFIG_OHM], 1, [Include the OpenHome Songcast OHM/OHU unicast sender audio backend.])
fi
AM_CONDITIONAL([USE_OHM], [test "x$with_ohm" = "xyes"])
# Check to see if we should include the System V initscript
AC_ARG_WITH([systemv-startup],[AS_HELP_STRING([--with-systemv-startup],[install a System V startup script during a make install])])
+112
View File
@@ -0,0 +1,112 @@
# Minimal cross-build of shairport-sync with the OHM Songcast sender backend,
# for linux/arm64 (Raspberry Pi OS aarch64). Produces a glibc binary compatible
# with Debian bullseye (glibc 2.31) and later (e.g. bookworm).
#
# Usage from the repo root:
# docker buildx build --platform linux/arm64 --target binary \
# -f docker/Dockerfile.ohm --output type=local,dest=./out-ohm .
#
# Then copy ./out-ohm/shairport-sync to the Pi.
#
# FFmpeg is taken from Debian packages (libavcodec-dev etc.) rather than built
# from source, to keep the cross-build fast under emulation.
ARG DEBIAN_VERSION=bullseye
FROM --platform=linux/arm64 debian:${DEBIAN_VERSION} AS builder
ENV DEBIAN_FRONTEND=noninteractive
RUN apt-get update && apt-get install -y --no-install-recommends \
build-essential \
autoconf \
automake \
libtool \
pkg-config \
git \
ca-certificates \
xxd \
libasound2-dev \
libplist-dev \
libplist-utils \
libssl-dev \
libsodium-dev \
libgcrypt-dev \
uuid-dev \
libavcodec-dev \
libavformat-dev \
libswresample-dev \
libavutil-dev \
libavahi-client-dev \
avahi-daemon \
libdbus-1-dev \
libglib2.0-dev \
libconfig-dev \
libpopt-dev \
libsndfile-dev \
libsoxr-dev \
xmltoman \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /shairport-sync
COPY . .
WORKDIR /shairport-sync/build
RUN autoreconf -i ../
RUN ../configure --sysconfdir=/etc --with-alsa \
--with-avahi --with-ssl=openssl \
--with-airplay-2 --with-ffmpeg --with-metadata \
--with-ohm --with-dummy --with-pipe --with-stdout \
--with-dbus-interface --with-mpris-interface \
--with-convolution --with-soxr
RUN make -j"$(nproc)"
RUN make install DESTDIR=/install
# Target that yields just the binary on the host filesystem via --output.
FROM --platform=linux/arm64 scratch AS binary
COPY --from=builder /shairport-sync/build/shairport-sync /shairport-sync
# nqptp sidecar image for AirPlay 2 timing; shares /dev/shm with the
# shairport-sync container (see k8s/shairport-ohm.yaml).
FROM --platform=linux/arm64 debian:${DEBIAN_VERSION} AS nqptp-build
ENV DEBIAN_FRONTEND=noninteractive
RUN apt-get update && apt-get install -y --no-install-recommends \
build-essential autoconf automake libtool git ca-certificates \
&& rm -rf /var/lib/apt/lists/*
RUN git clone --depth=1 https://github.com/mikebrady/nqptp /nqptp
WORKDIR /nqptp
RUN autoreconf -i && ./configure && make
FROM --platform=linux/arm64 debian:${DEBIAN_VERSION}-slim AS nqptp
COPY --from=nqptp-build /nqptp/nqptp /usr/local/bin/nqptp
ENTRYPOINT ["/usr/local/bin/nqptp"]
# Deployable runtime image. Uses the node's avahi (host D-Bus socket mounted)
# and the nqptp sidecar (shared /dev/shm); see k8s/shairport-ohm.yaml.
#
# docker buildx build --platform linux/arm64 --target runtime \
# -t <registry>/shairport-sync-ohm:latest -f docker/Dockerfile.ohm --push .
# docker buildx build --platform linux/arm64 --target nqptp \
# -t <registry>/nqptp:latest -f docker/Dockerfile.ohm --push .
FROM --platform=linux/arm64 debian:${DEBIAN_VERSION}-slim AS runtime
ENV DEBIAN_FRONTEND=noninteractive
RUN apt-get update && apt-get install -y --no-install-recommends \
libasound2 \
libavahi-client3 \
libavcodec58 \
libavformat58 \
libavutil56 \
libconfig9 \
libgcrypt20 \
libglib2.0-0 \
libplist3 \
libpopt0 \
libsndfile1 \
libsodium23 \
libsoxr0 \
libssl1.1 \
libswresample3 \
libuuid1 \
&& rm -rf /var/lib/apt/lists/*
COPY --from=builder /install/usr/local/bin/shairport-sync /usr/local/bin/shairport-sync
ENTRYPOINT ["/usr/local/bin/shairport-sync"]
+1 -1
View File
@@ -4,7 +4,7 @@
#include "rtsp.h"
#include <pthread.h>
#define number_of_watchers 2
#define number_of_watchers 3
typedef enum {
PS_NOT_AVAILABLE = 0,
+579
View File
@@ -0,0 +1,579 @@
/*
* OpenHome receiver control for the ohm audio backend.
*
* This file is part of Shairport Sync.
* Copyright (c) Nils Schneider 2026
*
* 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 "ohm_upnp.h"
#include <arpa/inet.h>
#include <netinet/in.h>
#include <stdatomic.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <strings.h>
#include <sys/socket.h>
#include <time.h>
#include <unistd.h>
#ifdef TEST_OHM
#include <stdarg.h>
static void debug(int level, const char *fmt, ...) {
if (level > 1)
return;
va_list ap;
va_start(ap, fmt);
fprintf(stderr, "upnp: ");
vfprintf(stderr, fmt, ap);
fprintf(stderr, "\n");
va_end(ap);
}
#define inform(...) debug(1, __VA_ARGS__)
#else
#include "config.h"
#include "common.h"
#endif
#define NS_PER_SEC 1000000000LL
#define NS_PER_MIN (60LL * NS_PER_SEC)
#define OHM_CTL_HTTP_MAX (128 * 1024)
#define OHM_CTL_TIMEOUT_S 3
#define OHM_CTL_RETRY_NS (10LL * NS_PER_SEC)
#define SSDP_ADDR "239.255.255.250"
#define SSDP_PORT 1900
#define SSDP_LOCATIONS_MAX 8
#define SSDP_LOCATION_LEN 192
#define CDB_PER_DB 100 /* shairport volume unit: dB * 100 */
#define BMDB_PER_DB 1024 /* OpenHome volume unit: binary milli-dB */
typedef struct {
char type[80]; /* full urn, e.g. "urn:av-openhome-org:service:Volume:4" */
char url[160]; /* controlURL path */
} ohm_svc_t;
/* Settings, copied in ohm_upnp_configure(). */
static struct {
char *receiver;
char *iface;
char *name;
int standby_min;
int ohu_port;
} set;
/* The discovered device, valid while .found. Tick thread only. */
static struct {
int found;
char host[64];
int port;
ohm_svc_t receiver, volume, product;
int vol_limit;
int vol_step; /* binary milli-dB per volume step */
} dev;
/* Requests from the backend callbacks to the tick thread. */
static struct {
volatile int session;
volatile int activate;
volatile int vol_dirty;
volatile long vol_cdb; /* dB * 100, <= 0 */
_Atomic uint64_t standby_at; /* 64-bit accesses must not tear on 32-bit targets */
} req;
static char *scratch; /* HTTP response buffer, tick thread only */
static uint64_t retry_at;
size_t ohm_xml_escape(char *dst, size_t dst_cap, const char *src) {
size_t w = 0;
dst[0] = '\0';
if (!src)
return 0;
for (const char *s = src; *s && w + 8 < dst_cap; s++) {
switch (*s) {
case '&':
memcpy(dst + w, "&amp;", 5);
w += 5;
break;
case '<':
memcpy(dst + w, "&lt;", 4);
w += 4;
break;
case '>':
memcpy(dst + w, "&gt;", 4);
w += 4;
break;
case '"':
memcpy(dst + w, "&quot;", 6);
w += 6;
break;
default:
dst[w++] = *s;
}
}
dst[w] = '\0';
return w;
}
static uint64_t ohm_ctl_mono_ns(void) {
struct timespec t;
clock_gettime(CLOCK_MONOTONIC, &t);
return (uint64_t)t.tv_sec * NS_PER_SEC + (uint64_t)t.tv_nsec;
}
static long ohm_ctl_lround(double d) { return (long)(d < 0 ? d - 0.5 : d + 0.5); }
static const char *ohm_ctl_find_ci(const char *hay, const char *needle) {
size_t n = strlen(needle);
for (; *hay; hay++)
if (strncasecmp(hay, needle, n) == 0)
return hay;
return NULL;
}
/* ---- XML scraping ---- */
/* Copy text from p up to the next '<' into out. */
static int ohm_ctl_span(const char *p, char *out, size_t cap) {
const char *e = strchr(p, '<');
if (!e || (size_t)(e - p) >= cap)
return -1;
memcpy(out, p, e - p);
out[e - p] = '\0';
return 0;
}
/* Copy the text content of the first <tag> into out. */
static int ohm_ctl_tag(const char *xml, const char *tag, char *out, size_t cap) {
char key[96];
snprintf(key, sizeof(key), "<%s>", tag);
const char *p = strstr(xml, key);
return p ? ohm_ctl_span(p + strlen(key), out, cap) : -1;
}
/* Fill svc from the <service> element whose serviceType starts with prefix. */
static int ohm_ctl_service(const char *xml, const char *prefix, ohm_svc_t *svc) {
const char *p = strstr(xml, prefix);
if (!p || ohm_ctl_span(p, svc->type, sizeof(svc->type)) != 0)
return -1;
const char *end = strstr(p, "</service>");
const char *cu = strstr(p, "<controlURL>");
if (!cu || (end && cu > end))
return -1;
return ohm_ctl_span(cu + strlen("<controlURL>"), svc->url, sizeof(svc->url));
}
/* ---- HTTP ---- */
/* Collapse chunked transfer-encoding in place; a chunk boundary must never
* split a tag. Returns the new body length. */
static size_t ohm_ctl_dechunk(char *body, size_t body_len) {
char *end = body + body_len;
char *src = body;
char *dst = body;
while (src < end) {
char *num_end;
long sz = strtol(src, &num_end, 16);
if (num_end == src || sz <= 0)
break;
src = strstr(num_end, "\r\n");
if (src == NULL)
break;
src += 2;
if (src + sz > end)
sz = (long)(end - src);
memmove(dst, src, (size_t)sz);
dst += sz;
src += sz;
if (src + 2 <= end && src[0] == '\r')
src += 2;
}
*dst = '\0';
return (size_t)(dst - body);
}
/* One request/response exchange into scratch. Returns 0 on HTTP 200. */
static int ohm_ctl_http(const char *host, int port, const char *req_buf, size_t req_len) {
int fd = socket(AF_INET, SOCK_STREAM, 0);
if (fd < 0)
return -1;
struct timeval tv = {.tv_sec = OHM_CTL_TIMEOUT_S};
setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));
setsockopt(fd, SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof(tv));
struct sockaddr_in sin = {.sin_family = AF_INET, .sin_port = htons(port)};
if (inet_pton(AF_INET, host, &sin.sin_addr) != 1 ||
connect(fd, (struct sockaddr *)&sin, sizeof(sin)) != 0 ||
send(fd, req_buf, req_len, 0) != (ssize_t)req_len) {
close(fd);
return -1;
}
size_t len = 0;
for (;;) {
ssize_t n = recv(fd, scratch + len, OHM_CTL_HTTP_MAX - 1 - len, 0);
if (n <= 0)
break;
len += (size_t)n;
if (len >= OHM_CTL_HTTP_MAX - 1)
break;
}
close(fd);
scratch[len] = '\0';
const char *sp = strchr(scratch, ' ');
if (sp == NULL || atoi(sp + 1) != 200)
return -1;
char *body = strstr(scratch, "\r\n\r\n");
if (body == NULL)
return -1;
body += 4;
body[-4] = '\0'; /* terminate the headers for the search */
int chunked = ohm_ctl_find_ci(scratch, "transfer-encoding: chunked") != NULL;
body[-4] = '\r';
if (chunked)
ohm_ctl_dechunk(body, len - (size_t)(body - scratch));
return 0;
}
static int ohm_ctl_get(const char *host, int port, const char *path) {
char req_buf[256];
int n = snprintf(req_buf, sizeof(req_buf),
"GET %s HTTP/1.1\r\nHost: %s:%d\r\nConnection: close\r\n\r\n", path, host, port);
return ohm_ctl_http(host, port, req_buf, (size_t)n);
}
static int ohm_ctl_soap(const ohm_svc_t *svc, const char *action, const char *args) {
char body[6144];
int blen = snprintf(body, sizeof(body),
"<?xml version=\"1.0\"?>"
"<s:Envelope xmlns:s=\"http://schemas.xmlsoap.org/soap/envelope/\" "
"s:encodingStyle=\"http://schemas.xmlsoap.org/soap/encoding/\">"
"<s:Body><u:%s xmlns:u=\"%s\">%s</u:%s></s:Body></s:Envelope>",
action, svc->type, args, action);
char req_buf[8192];
int rlen = snprintf(req_buf, sizeof(req_buf),
"POST %s HTTP/1.1\r\nHost: %s:%d\r\n"
"Content-Type: text/xml; charset=\"utf-8\"\r\n"
"SOAPACTION: \"%s#%s\"\r\nContent-Length: %d\r\nConnection: close\r\n\r\n%s",
svc->url, dev.host, dev.port, svc->type, action, blen, body);
return ohm_ctl_http(dev.host, dev.port, req_buf, (size_t)rlen);
}
/* ---- Discovery ---- */
/* M-SEARCH for OpenHome receivers; collect response LOCATION urls. */
static int ohm_ctl_ssdp(char locations[][SSDP_LOCATION_LEN], int max) {
int fd = socket(AF_INET, SOCK_DGRAM, 0);
if (fd < 0)
return 0;
struct timeval tv = {.tv_usec = 500000};
setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));
if (set.iface) {
struct in_addr mcast_if;
if (inet_pton(AF_INET, set.iface, &mcast_if) == 1)
setsockopt(fd, IPPROTO_IP, IP_MULTICAST_IF, &mcast_if, sizeof(mcast_if));
}
const char *msearch = "M-SEARCH * HTTP/1.1\r\nHOST: " SSDP_ADDR ":1900\r\n"
"MAN: \"ssdp:discover\"\r\nMX: 1\r\n"
"ST: urn:av-openhome-org:service:Receiver:1\r\n\r\n";
struct sockaddr_in dst = {.sin_family = AF_INET, .sin_port = htons(SSDP_PORT)};
inet_pton(AF_INET, SSDP_ADDR, &dst.sin_addr);
sendto(fd, msearch, strlen(msearch), 0, (struct sockaddr *)&dst, sizeof(dst));
int n = 0;
char pkt[2048];
for (int i = 0; i < max && n < max; i++) {
ssize_t r = recv(fd, pkt, sizeof(pkt) - 1, 0);
if (r <= 0)
break;
pkt[r] = '\0';
const char *l = ohm_ctl_find_ci(pkt, "location:");
if (!l)
continue;
l += strlen("location:");
while (*l == ' ')
l++;
size_t len = strcspn(l, "\r\n");
if (len == 0 || len >= SSDP_LOCATION_LEN)
continue;
memcpy(locations[n], l, len);
locations[n][len] = '\0';
n++;
}
close(fd);
return n;
}
/* Fetch one device description; on a friendly-name match fill dev. */
static int ohm_ctl_probe(const char *location) {
char host[64], path[128], fname[128];
int port = 80;
if (sscanf(location, "http://%63[^:/]:%d%127s", host, &port, path) != 3 &&
sscanf(location, "http://%63[^:/]%127s", host, path) != 2)
return -1;
if (ohm_ctl_get(host, port, path) != 0 ||
ohm_ctl_tag(scratch, "friendlyName", fname, sizeof(fname)) != 0 ||
!ohm_ctl_find_ci(fname, set.receiver))
return -1;
if (ohm_ctl_service(scratch, "urn:av-openhome-org:service:Receiver:", &dev.receiver) != 0 ||
ohm_ctl_service(scratch, "urn:av-openhome-org:service:Volume:", &dev.volume) != 0 ||
ohm_ctl_service(scratch, "urn:av-openhome-org:service:Product:", &dev.product) != 0)
return -1;
snprintf(dev.host, sizeof(dev.host), "%s", host);
dev.port = port;
inform("ohm: controlling receiver \"%s\" at %s:%d", fname, dev.host, dev.port);
return 0;
}
static void ohm_ctl_read_volume_caps(void) {
dev.vol_limit = 100;
dev.vol_step = BMDB_PER_DB;
char v[32];
if (ohm_ctl_soap(&dev.volume, "Characteristics", "") == 0) {
if (ohm_ctl_tag(scratch, "VolumeMax", v, sizeof(v)) == 0)
dev.vol_limit = atoi(v);
if (ohm_ctl_tag(scratch, "VolumeMilliDbPerStep", v, sizeof(v)) == 0 && atoi(v) > 0)
dev.vol_step = atoi(v);
}
if (ohm_ctl_soap(&dev.volume, "VolumeLimit", "") == 0 &&
ohm_ctl_tag(scratch, "Value", v, sizeof(v)) == 0 && atoi(v) > 0)
dev.vol_limit = atoi(v);
debug(1, "ohm: receiver volume limit %d, %d mdB/step", dev.vol_limit, dev.vol_step);
}
static int ohm_ctl_discover(void) {
char locations[SSDP_LOCATIONS_MAX][SSDP_LOCATION_LEN];
int n = ohm_ctl_ssdp(locations, SSDP_LOCATIONS_MAX);
for (int i = 0; i < n; i++) {
if (ohm_ctl_probe(locations[i]) == 0) {
ohm_ctl_read_volume_caps();
dev.found = 1;
return 0;
}
}
return -1;
}
/* shairport delivers attenuation in cdB over the declared range; the device
* runs 0..vol_limit steps. */
static long ohm_ctl_steps_from_cdb(long cdb) {
double step_db = (double)dev.vol_step / BMDB_PER_DB;
long steps = dev.vol_limit + ohm_ctl_lround((double)cdb / CDB_PER_DB / step_db);
if (steps < 0)
steps = 0;
if (steps > dev.vol_limit)
steps = dev.vol_limit;
return steps;
}
/* ---- Actions ---- */
/* ohu:// URI of this sender as reachable from the receiver. */
static int ohm_ctl_our_uri(char *out, size_t cap) {
struct sockaddr_in sin = {.sin_family = AF_INET, .sin_port = htons(SSDP_PORT)};
inet_pton(AF_INET, dev.host, &sin.sin_addr);
int fd = socket(AF_INET, SOCK_DGRAM, 0);
if (fd < 0)
return -1;
struct sockaddr_in local;
socklen_t slen = sizeof(local);
int ok = connect(fd, (struct sockaddr *)&sin, sizeof(sin)) == 0 &&
getsockname(fd, (struct sockaddr *)&local, &slen) == 0;
close(fd);
if (!ok)
return -1;
char ip[INET_ADDRSTRLEN];
inet_ntop(AF_INET, &local.sin_addr, ip, sizeof(ip));
snprintf(out, cap, "ohu://%s:%d", ip, set.ohu_port);
return 0;
}
/* Switch the receiver's product to its Songcast source. The SourceXml body is
* XML-escaped inside the SOAP response, so count escaped <Source> elements up
* to the one of type Receiver. */
static void ohm_ctl_select_source(void) {
if (ohm_ctl_soap(&dev.product, "SourceXml", "") != 0)
return;
const char *rcv = strstr(scratch, "&lt;Type&gt;Receiver&lt;/Type&gt;");
if (!rcv)
return;
int idx = -1;
for (const char *p = scratch; (p = strstr(p, "&lt;Source&gt;")) != NULL && p < rcv; p++)
idx++;
if (idx < 0)
return;
char arg[48];
snprintf(arg, sizeof(arg), "<Value>%d</Value>", idx);
ohm_ctl_soap(&dev.product, "SetSourceIndex", arg);
}
static int ohm_ctl_set_sender(const char *uri) {
char name[256], didl[768], meta[2304], args[2560];
ohm_xml_escape(name, sizeof(name), set.name ? set.name : "Shairport Sync");
snprintf(didl, sizeof(didl),
"<DIDL-Lite xmlns:dc=\"http://purl.org/dc/elements/1.1/\" "
"xmlns:upnp=\"urn:schemas-upnp-org:metadata-1-0/upnp/\" "
"xmlns=\"urn:schemas-upnp-org:metadata-1-0/DIDL-Lite/\">"
"<item id=\"0\" restricted=\"True\"><dc:title>%s</dc:title>"
"<res protocolInfo=\"ohu:*:*:*\">%s</res>"
"<upnp:class>object.item.audioItem</upnp:class></item></DIDL-Lite>",
name, uri);
ohm_xml_escape(meta, sizeof(meta), didl);
snprintf(args, sizeof(args), "<Uri>%s</Uri><Metadata>%s</Metadata>", uri, meta);
return ohm_ctl_soap(&dev.receiver, "SetSender", args);
}
static int ohm_ctl_activate(void) {
char uri[128];
if (ohm_ctl_soap(&dev.product, "SetStandby", "<Value>0</Value>") != 0)
return -1;
ohm_ctl_select_source();
if (ohm_ctl_our_uri(uri, sizeof(uri)) != 0 || ohm_ctl_set_sender(uri) != 0 ||
ohm_ctl_soap(&dev.receiver, "Play", "") != 0)
return -1;
debug(1, "ohm: receiver pointed at %s and playing", uri);
return 0;
}
static int ohm_ctl_push_volume(void) {
long steps = ohm_ctl_steps_from_cdb(req.vol_cdb);
char arg[48];
snprintf(arg, sizeof(arg), "<Value>%ld</Value>", steps);
if (ohm_ctl_soap(&dev.volume, "SetVolume", arg) != 0)
return -1;
debug(2, "ohm: receiver volume %ld/%d", steps, dev.vol_limit);
return 0;
}
/* Standby only if the receiver is still pointed at us. */
static int ohm_ctl_standby(void) {
char uri[128];
if (ohm_ctl_soap(&dev.receiver, "Sender", "") != 0)
return -1;
if (ohm_ctl_our_uri(uri, sizeof(uri)) == 0 && !strstr(scratch, uri)) {
debug(1, "ohm: receiver plays another sender, skipping standby");
return 0;
}
if (ohm_ctl_soap(&dev.product, "SetStandby", "<Value>1</Value>") != 0)
return -1;
debug(1, "ohm: receiver put into standby");
return 0;
}
/* Run every due request in order; -1 invalidates the device. */
static int ohm_ctl_run(uint64_t now) {
if (req.activate) {
if (ohm_ctl_activate() != 0)
return -1;
req.activate = 0;
}
if (req.vol_dirty) {
req.vol_dirty = 0; /* a newer volume arriving during the push re-sets it */
if (ohm_ctl_push_volume() != 0) {
req.vol_dirty = 1;
return -1;
}
}
if (req.standby_at && now >= req.standby_at) {
if (ohm_ctl_standby() != 0)
return -1;
req.standby_at = 0;
}
return 0;
}
/* ---- Backend interface ---- */
void ohm_upnp_configure(const ohm_upnp_config_t *config) {
free(set.receiver);
free(set.iface);
free(set.name);
set.receiver = config->receiver_name ? strdup(config->receiver_name) : NULL;
set.iface = config->interface_ip ? strdup(config->interface_ip) : NULL;
set.name = config->display_name ? strdup(config->display_name) : NULL;
set.standby_min = config->standby_minutes;
set.ohu_port = config->ohu_port;
}
int ohm_upnp_enabled(void) { return set.receiver != NULL; }
void ohm_upnp_session_begin(void) {
if (set.receiver == NULL || req.session)
return;
req.session = 1;
req.standby_at = 0;
req.activate = 1;
}
void ohm_upnp_session_end(void) {
if (set.receiver == NULL)
return;
req.session = 0;
req.standby_at =
set.standby_min > 0 ? ohm_ctl_mono_ns() + (uint64_t)set.standby_min * NS_PER_MIN : 0;
}
void ohm_upnp_set_volume(long vol_cdb) {
req.vol_cdb = vol_cdb;
req.vol_dirty = 1;
}
void ohm_upnp_tick(void) {
if (set.receiver == NULL)
return;
uint64_t now = ohm_ctl_mono_ns();
int standby_due = req.standby_at && now >= req.standby_at;
if (!req.activate && !req.vol_dirty && !standby_due)
return;
if (now < retry_at)
return;
if (scratch == NULL && (scratch = malloc(OHM_CTL_HTTP_MAX)) == NULL)
return;
if (!dev.found && ohm_ctl_discover() != 0) {
retry_at = now + OHM_CTL_RETRY_NS;
debug(1, "ohm: receiver \"%s\" not found, retrying", set.receiver);
return;
}
if (ohm_ctl_run(now) != 0) {
dev.found = 0;
retry_at = now + OHM_CTL_RETRY_NS;
}
}
void ohm_upnp_deinit(void) {
free(set.receiver);
set.receiver = NULL;
free(set.iface);
set.iface = NULL;
free(set.name);
set.name = NULL;
free(scratch);
scratch = NULL;
dev.found = 0;
req.session = 0;
req.activate = 0;
req.vol_dirty = 0;
req.standby_at = 0;
}
+58
View File
@@ -0,0 +1,58 @@
/*
* OpenHome receiver control for the ohm audio backend.
*
* This file is part of Shairport Sync.
* Copyright (c) Nils Schneider 2026
*
* 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.
*/
#pragma once
#include <stddef.h>
/* XML-escape src into dst (NUL-terminated). Returns chars written (excluding NUL). */
size_t ohm_xml_escape(char *dst, size_t dst_cap, const char *src);
/* The backend declares volume as attenuation -OHM_UPNP_VOLUME_RANGE_CDB..0
* (dB * 100); shairport stretches the AirPlay slider over exactly this range. */
#define OHM_UPNP_VOLUME_RANGE_CDB 6000
typedef struct {
const char *receiver_name; /* friendly name to match; NULL disables the module */
const char *interface_ip;
const char *display_name;
int standby_minutes; /* 0 = never */
int ohu_port;
} ohm_upnp_config_t;
/* Strings are copied. */
void ohm_upnp_configure(const ohm_upnp_config_t *config);
int ohm_upnp_enabled(void);
/* Callback side: cheap, thread-safe, never block. */
void ohm_upnp_session_begin(void);
void ohm_upnp_session_end(void);
void ohm_upnp_set_volume(long vol_cdb); /* dB * 100, <= 0 */
/* Does the network work (SSDP discovery, SOAP); call periodically from one
* thread. */
void ohm_upnp_tick(void);
void ohm_upnp_deinit(void);
+31 -47
View File
@@ -1488,8 +1488,11 @@ int get_ptp_anchor_local_time_info(rtsp_conn_info *conn, uint32_t *anchorRTP,
}
if (conn->last_anchor_info_is_valid != 0) {
if (anchorRTP != NULL)
*anchorRTP = conn->last_anchor_rtptime;
if (anchorRTP != NULL) {
// Use the current rate in case the stream format has changed.
int32_t added_latency = (int32_t)(config.audio_backend_latency_offset * conn->input_rate);
*anchorRTP = conn->last_anchor_rtptime - added_latency;
}
if (anchorLocalTime != NULL)
*anchorLocalTime = conn->last_anchor_local_time;
}
@@ -1616,6 +1619,7 @@ int32_t decipher_player_put_packet(uint8_t *ciphered_audio_alt, ssize_t nread,
}
void *rtp_ap2_control_receiver(void *arg) {
const int32_t ap2_realttime_stream_latency_fudge_factor = 11025; // seems to bring everything into sync
// #include <syscall.h>
// debug(1, "rtp_ap2_control_receiver PID %d", syscall(SYS_gettid));
pthread_cleanup_push(rtp_ap2_control_handler_cleanup_handler, arg);
@@ -1674,22 +1678,6 @@ void *rtp_ap2_control_receiver(void *arg) {
switch (packet[1]) {
case 215: // code 215, effectively an anchoring announcement
{
// struct timespec tnr;
// clock_gettime(CLOCK_REALTIME, &tnr);
// uint64_t local_realtime_now = timespec_to_ns(&tnr);
/*
char obf[4096];
char *obfp = obf;
int obfc;
for (obfc=0;obfc<nread;obfc++) {
snprintf(obfp, 3, "%02X", packet[obfc]);
obfp+=2;
};
*obfp=0;
debug(1,"AP2 Timing Control Received: \"%s\"",obf);
*/
uint64_t remote_packet_time_ns = nctoh64(packet + 8);
check64conversion("remote_packet_time_ns", packet + 8, remote_packet_time_ns);
uint64_t clock_id = nctoh64(packet + 20);
@@ -1699,49 +1687,45 @@ void *rtp_ap2_control_receiver(void *arg) {
// debug(1,"remote_packet_time_ns: %" PRIx64 ", local_realtime_now_ns: %" PRIx64
// ".", remote_packet_time_ns, local_realtime_now);
uint32_t frame_1 =
nctohl(packet + 4); // this seems to be the frame with latency of 77165 included
nctohl(packet + 4); // this seems to be the frame with latency of 77175 included
check32conversion("frame_1", packet + 4, frame_1);
uint32_t frame_2 =
nctohl(packet + 16); // this seems to be the frame the time refers to
check32conversion("frame_2", packet + 16, frame_2);
// this just updates the anchor information contained in the packet
// the frame and its remote time
// add in the audio_backend_latency_offset;
int32_t notified_latency = frame_2 - frame_1;
if (notified_latency != 77175)
debug(1, "Notified latency is %d frames.", notified_latency);
int32_t added_latency =
(int32_t)(config.audio_backend_latency_offset * conn->input_rate);
// the actual latency is the notified latency plus the fixed latency + the added
// latency
// this updates the anchor information contained in the packet
int32_t stream_specified_latency = frame_2 - frame_1; // this is the latency expected
if (stream_specified_latency != 77175)
debug(1, "Stream-specified latency is %d frames. Normally it is 77175.", stream_specified_latency);
int32_t net_source_latency = stream_specified_latency + ap2_realttime_stream_latency_fudge_factor;
int32_t net_latency =
notified_latency + 11035 +
added_latency; // this is the latency between incoming frames and the DAC
net_latency = net_latency - (int32_t)(config.audio_backend_buffer_desired_length *
// Now to accommodate a backend buffer of the desired length.
// Note that it's in input-rate frames, not output-rate frames!
net_source_latency = net_source_latency - (int32_t)(config.audio_backend_buffer_desired_length *
conn->input_rate);
// debug(1, "Net latency is %d frames.", net_latency);
// Now we want to check the user-specified latency offset.
// We want to warn the user if they have asked for a negative latency that is too great --
// one that would require packets to arrive before they actually do,
// (which is about two seconds before they are to be played).
int32_t net_latency = net_source_latency + (int32_t)(config.audio_backend_latency_offset * conn->input_rate);
if (net_latency <= 0) {
if (conn->latency_warning_issued == 0) {
warn("The stream latency (%f seconds) it too short to accommodate an offset of "
"%f "
"seconds and a backend buffer of %f seconds.",
((notified_latency + 11035) * 1.0) / conn->input_rate,
warn("The stream latency (%g seconds) is too short to accommodate an audio backend latency offset of "
"%g seconds and a backend buffer of %g seconds. The audio_backend_latency_offset has been set to zero.",
((stream_specified_latency + ap2_realttime_stream_latency_fudge_factor) * 1.0) / conn->input_rate,
config.audio_backend_latency_offset,
config.audio_backend_buffer_desired_length);
warn("(FYI the stream latency needed would be %f seconds.)",
config.audio_backend_buffer_desired_length -
config.audio_backend_latency_offset);
config.audio_backend_latency_offset = 0.0;
net_latency = net_source_latency;
conn->latency_warning_issued = 1;
}
conn->latency = notified_latency + 11035;
} else {
conn->latency = notified_latency + 11035 + added_latency;
}
set_ptp_anchor_info(conn, clock_id, frame_1 - 11035 - added_latency,
remote_packet_time_ns);
conn->latency = net_latency; // this is the time window within which packets can be accepted without being too late
set_ptp_anchor_info(conn, clock_id, frame_1 - ap2_realttime_stream_latency_fudge_factor, remote_packet_time_ns);
if (conn->anchor_clock != clock_id) {
debug(2, "Connection %d: Change Anchor Clock: %" PRIx64 ".",
conn->connection_number, clock_id);
+2 -5
View File
@@ -1453,11 +1453,8 @@ void handle_setrateanchori(rtsp_conn_info *conn, rtsp_message *req, rtsp_message
// debug(1, "anchor rtpTime is %" PRId64 ".", rtpTime);
uint32_t anchorRTPTime = rtpTime;
int32_t added_latency = (int32_t)(config.audio_backend_latency_offset * conn->input_rate);
// debug(1,"anchorRTPTime: %" PRIu32 ", added latency: %" PRId32 ".", anchorRTPTime,
// added_latency);
set_ptp_anchor_info(conn, conn->networkTimeTimelineID, anchorRTPTime - added_latency,
anchorTimeNanoseconds);
// Store the raw anchor; apply the latency offset when it is used.
set_ptp_anchor_info(conn, conn->networkTimeTimelineID, anchorRTPTime, anchorTimeNanoseconds);
}
item = plist_dict_get_item(messagePlist, "rate");