Compare commits
38
Commits
427061fb48
..
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c343dd56df | ||
|
|
d6a3ca335a | ||
|
|
b75edeedc3 | ||
|
|
fadd43d06e | ||
|
|
6e10eca884 | ||
|
|
5e170a67a8 | ||
|
|
3ba7dfea6f | ||
|
|
6cdd0eb794 | ||
|
|
6966a74c82 | ||
|
|
c4e17b60ef | ||
|
|
b69e01cdf9 | ||
|
|
d8165c1143 | ||
|
|
4df0145372 | ||
|
|
08af668a5d | ||
|
|
42bd29d6da | ||
|
|
d6ac53bf4c | ||
|
|
5bd5f9c1d0 | ||
|
|
1718f7b5af | ||
|
|
368f3696c3 | ||
|
|
4d0e9bb6c8 | ||
|
|
83961baad9 | ||
|
|
d08637d3d6 | ||
|
|
a6231e089c | ||
|
|
c454867a04 | ||
|
|
befe1d4a1a | ||
|
|
eed1b9f0b9 | ||
|
|
930e523757 | ||
|
|
6ef48e59e6 | ||
|
|
06e824be0c | ||
|
|
32458c08aa | ||
|
|
4ecd42eda3 | ||
|
|
2ee44830d3 | ||
|
|
28eb383155 | ||
|
|
dc7a048bd3 | ||
|
|
dc4c334adf | ||
|
|
1230285491 | ||
|
|
c956d763ca | ||
|
|
7421421848 |
+4
-1
@@ -1,4 +1,7 @@
|
||||
.github
|
||||
documents
|
||||
docker/Dockerfile
|
||||
docker/classic/Dockerfile
|
||||
docker/classic/Dockerfile
|
||||
reference
|
||||
ohm.txt
|
||||
out-ohm
|
||||
+1
-1
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
File diff suppressed because it is too large
Load Diff
@@ -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,
|
||||
|
||||
@@ -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
@@ -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])])
|
||||
|
||||
@@ -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
@@ -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
@@ -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, "&", 5);
|
||||
w += 5;
|
||||
break;
|
||||
case '<':
|
||||
memcpy(dst + w, "<", 4);
|
||||
w += 4;
|
||||
break;
|
||||
case '>':
|
||||
memcpy(dst + w, ">", 4);
|
||||
w += 4;
|
||||
break;
|
||||
case '"':
|
||||
memcpy(dst + w, """, 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, "<Type>Receiver</Type>");
|
||||
if (!rcv)
|
||||
return;
|
||||
int idx = -1;
|
||||
for (const char *p = scratch; (p = strstr(p, "<Source>")) != 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
@@ -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);
|
||||
@@ -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);
|
||||
|
||||
@@ -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");
|
||||
|
||||
Reference in New Issue
Block a user