Files
Mike Brady f30dd7aee6 Enhancements
Enable the AirPlay 2 build to operate an optional classic-AirPlay-only service or to gracefully degrade to classic AirPlay, as follows:
  1. Add a new command-line option '-—service-type=<type>' and an equivalent configuration entry 'service_type = "<type>"' in the 'general' section of the configuration file,
     where <type> can be "auto", "classic" or "airplay2":
      1. "auto" (default) means that the service will be AirPlay 2 if NQPTP is running. If NQPTP is not running, classic AirPlay service will be provided instead, and in that case,
      "(Classic)" will be appended to the default AirPlay service name visible to AirPlay clients like Apple Music, for example "RaspberryPi3B (Classic)".
      2. "classic" means the service will be classic AirPlay (aka AirPlay 1).
      3. "airplay2" means the service will be the modern AirPlay 2. In this case, as distinct from "auto", if NQPTP is not running, Shairport Sync will log an error and terminate.
  2. In the systemd service file, NQPTP is now a "Want" rather than a "Require". If it's present, then it will be launched before Shairport Sync. If it's absent, Shairport Sync will launch anyway.
  3. Improve the delivery of input format changes and emit 'sdsc' metadata when changes occur.
  4. Emit new format information in the log if statistics is enabled.

Docker Changes
  1. Support for linux/arm/v6 has been dropped, as Docker is no longer supported.
  2. NQPTP is not started in the AirPlay 2 Docker image if '--service-type=classic' or '--service-type=airplay1' is in the command line options at the end of the docker run command.
     The purpose is to ensure that ports 319 and 320 are left alone when the AirPlay 2 image is set to provide Classic service only.
     Note that setting the configuration file 'service_type' to 'classic' will not prevent NQPTP from starting up -- you must use the command line option.
  3. A new "dev" target has been added. It is a large image containing the custom-built FFmpeg library, NQPTP, Avahi and D-Bus along with the Shairport Sync source and
     all necessary development tools. When started, Avahi, D-Bus and NQPTP are all installed and running. The bash shell has also been added and is entered.

Stability Improvements
  Reorganise session preemption to fully terminate the existing session before starting a new one.
  Don't delay closing the event port to wait for it to be closed at the client end.
  Add a safe_socket_close() function to ensure sockets are fully closed. Use -1 to designate closed rather than 0, to prevent attempts to reclose sockets, causing mayhem.
  Re-order FFmpeg decommissioning during teardown.
2026-05-18 09:57:12 +01:00

194 lines
6.6 KiB
C
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/*
* Some character string utilities. This file is part of Shairport Sync
* Copyright (c) Mike Brady 2026
* All rights reserved.
*
* Permission is hereby granted, free of charge, to any person
* obtaining a copy of this software and associated documentation
* files (the "Software"), to deal in the Software without
* restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or
* sell copies of the Software, and to permit persons to whom the
* Software is furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
* OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
* HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
* OTHER DEALINGS IN THE SOFTWARE.
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include "common.h"
#include "string_utilities.h"
// mDNS supports maximum of 63-character names (we prepend 13, so only 50 left).
#define MAX_AIRPLAY_SERVICE_NAME_LENGTH 50
/* from
* http://coding.debuntu.org/c-implementing-str_replace-replace-all-occurrences-substring#comment-722
* with thanks.
*/
char *str_replace(const char *string, const char *substr, const char *replacement) {
char *tok = NULL;
char *newstr = NULL;
char *oldstr = NULL;
char *head = NULL;
/* if either substr or replacement is NULL, duplicate string a let caller handle it */
if (substr == NULL || replacement == NULL)
return strdup(string);
newstr = strdup(string);
head = newstr;
if (head) {
while ((tok = strstr(head, substr))) {
oldstr = newstr;
newstr = malloc(strlen(oldstr) - strlen(substr) + strlen(replacement) + 1);
/*failed to alloc mem, free old string and return NULL */
if (newstr == NULL) {
free(oldstr);
return NULL;
}
memcpy(newstr, oldstr, tok - oldstr);
memcpy(newstr + (tok - oldstr), replacement, strlen(replacement));
memcpy(newstr + (tok - oldstr) + strlen(replacement), tok + strlen(substr),
strlen(oldstr) - strlen(substr) - (tok - oldstr));
memset(newstr + strlen(oldstr) - strlen(substr) + strlen(replacement), 0, 1);
/* move back head right after the last replacement */
head = newstr + (tok - oldstr) + strlen(replacement);
free(oldstr);
}
} else {
die("failed to allocate memory in str_replace.");
}
return newstr;
}
/*
* append_truncated - Append `suffix` to `base`, returning a newly malloc'd
* string. If the combined result would exceed `limit` bytes (excluding the
* NUL terminator), the base is truncated so that the output is:
*
* <truncated base>...<suffix>
*
* The returned string is always NUL-terminated and never exceeds `limit`
* bytes of content. The caller is responsible for free()'ing it.
*
* Parameters:
* base - the primary string
* suffix - the string to append
* limit - maximum byte count of the resulting string (excl. NUL)
*
* Returns:
* A newly malloc'd string on success, or NULL on failure (limit too small
* to fit "..." + suffix, or malloc failure).
*/
char *append_truncated(const char *base, const char *suffix, size_t limit) {
const char *ellipsis = "...";
size_t ellipsis_len = strlen(ellipsis);
size_t suffix_len = strlen(suffix);
size_t base_len = strlen(base);
size_t combined_len = base_len + suffix_len;
size_t result_len;
size_t head_len;
char *out;
if (combined_len <= limit) {
/* Fits without truncation. */
result_len = combined_len;
out = malloc(result_len + 1);
if (!out)
return NULL;
memcpy(out, base, base_len);
memcpy(out + base_len, suffix, suffix_len + 1); /* +1 for NUL */
return out;
}
/* Not enough room to fit even "..." + suffix. */
if (limit < ellipsis_len + suffix_len)
return NULL;
head_len = limit - ellipsis_len - suffix_len;
/* Respect UTF-8: don't cut in the middle of a multibyte sequence.
* Walk back from head_len until we're at a character boundary
* (i.e. a byte that is not a UTF-8 continuation byte 0x80–0xBF). */
while (head_len > 0 && (base[head_len] & 0xC0) == 0x80)
head_len--;
result_len = head_len + ellipsis_len + suffix_len;
out = malloc(result_len + 1);
if (!out)
return NULL;
char *p = out;
memcpy(p, base, head_len);
p += head_len;
memcpy(p, ellipsis, ellipsis_len);
p += ellipsis_len;
memcpy(p, suffix, suffix_len);
p += suffix_len;
*p = '\0';
return out;
}
char *service_name(const char *raw_service_name) {
char *response = NULL;
// now, do the substitutions in the service name
char hostname[256];
gethostname(hostname, sizeof(hostname));
// strip off a terminating .<anything>, e.g. .local from the hostname
char *last_dot = strrchr(hostname, '.');
if (last_dot != NULL)
*last_dot = '\0';
char *i0;
if (raw_service_name == NULL)
i0 = strdup("%H"); // default
else
i0 = strdup(raw_service_name); // this is the string provided in the configuration or on the
// command line.
// here, do the substitutions for %h, %H, %v and %V
char *i1 = str_replace(i0, "%h", hostname);
if ((hostname[0] >= 'a') && (hostname[0] <= 'z'))
hostname[0] = hostname[0] - 0x20; // convert a lowercase first letter into a capital letter
char *i2 = str_replace(i1, "%H", hostname);
char *i3 = str_replace(i2, "%v", PACKAGE_VERSION);
char *vs = get_version_string();
char *i4 = str_replace(i3, "%V", vs); // service name complete
// now, we may need to add "(Classic)" and/or truncate it to MAX_AIRPLAY_SERVICE_NAME_LENGTH
// characters.
#ifdef CONFIG_AIRPLAY_2
if ((raw_service_name == NULL) && (config.service_type == APST_forced_classic)) {
response = append_truncated(
i4, " (Classic)", MAX_AIRPLAY_SERVICE_NAME_LENGTH); // append "(Classic)" to default service
// name if forced to Classic
} else {
#endif
response = append_truncated(
i4, "",
MAX_AIRPLAY_SERVICE_NAME_LENGTH); // make sure it doesn't exceed the max length: 63 - 13
#ifdef CONFIG_AIRPLAY_2
}
#endif
free(i0);
free(i1);
free(i2);
free(i3);
free(i4);
free(vs);
return response;
}