uxplayer/uxplay.cpp
2025-05-03 17:09:23 +08:00

2361 lines
88 KiB
C++

/**
* RPiPlay - An open-source AirPlay mirroring server for Raspberry Pi
* Copyright (C) 2019 Florian Draschbacher
* Modified extensively to become
* UxPlay - An open-souce AirPlay mirroring server.
* Modifications Copyright (C) 2021-23 F. Duncanh
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*/
#include <stddef.h>
#include <cstring>
#include <signal.h>
#include <unistd.h>
#include <ctype.h>
#include <string>
#include <algorithm>
#include <vector>
#include <fstream>
#include <sstream>
#include <iterator>
#include <sys/stat.h>
#include <cstdio>
#include <stdarg.h>
#include <math.h>
#ifdef _WIN32 /*modifications for Windows compilation */
#include <glib.h>
#include <unordered_map>
#include <winsock2.h>
#include <iphlpapi.h>
#else
#include <glib-unix.h>
#include <sys/utsname.h>
#include <sys/socket.h>
#include <ifaddrs.h>
#include <sys/types.h>
#include <pwd.h>
# ifdef __linux__
# include <netpacket/packet.h>
# else
# include <net/if_dl.h>
# endif
#endif
#include "lib/raop.h"
#include "lib/stream.h"
#include "lib/logger.h"
#include "lib/dnssd.h"
#include "renderers/video_renderer.h"
#include "renderers/audio_renderer.h"
#define VERSION "1.71"
#define SECOND_IN_USECS 1000000
#define SECOND_IN_NSECS 1000000000UL
#define DEFAULT_NAME "UxPlay"
#define DEFAULT_DEBUG_LOG false
#define LOWEST_ALLOWED_PORT 1024
#define HIGHEST_PORT 65535
#define NTP_TIMEOUT_LIMIT 5
#define BT709_FIX "capssetter caps=\"video/x-h264, colorimetry=bt709\""
static std::string server_name = DEFAULT_NAME;
static dnssd_t *dnssd = NULL;
static raop_t *raop = NULL;
static logger_t *render_logger = NULL;
static bool audio_sync = false;
static bool video_sync = true;
static int64_t audio_delay_alac = 0;
static int64_t audio_delay_aac = 0;
static bool relaunch_video = false;
static bool reset_loop = false;
static unsigned int open_connections= 0;
static std::string videosink = "autovideosink";
static std::string videosink_options = "";
static std::string default_image = ""; // Add this line
static videoflip_t videoflip[2] = { NONE , NONE };
static bool use_video = true;
static unsigned char compression_type = 0;
static std::string audiosink = "autoaudiosink";
static int audiodelay = -1;
static bool use_audio = true;
static bool new_window_closing_behavior = true;
static bool close_window;
static std::string video_parser = "h264parse";
static std::string video_decoder = "decodebin";
static std::string video_converter = "videoconvert";
static bool show_client_FPS_data = false;
static unsigned int max_ntp_timeouts = NTP_TIMEOUT_LIMIT;
static FILE *video_dumpfile = NULL;
static std::string video_dumpfile_name = "videodump";
static int video_dump_limit = 0;
static int video_dumpfile_count = 0;
static int video_dump_count = 0;
static bool dump_video = false;
static unsigned char mark[] = { 0x00, 0x00, 0x00, 0x01 };
static FILE *audio_dumpfile = NULL;
static std::string audio_dumpfile_name = "audiodump";
static int audio_dump_limit = 0;
static int audio_dumpfile_count = 0;
static int audio_dump_count = 0;
static bool dump_audio = false;
static unsigned char audio_type = 0x00;
static unsigned char previous_audio_type = 0x00;
static bool fullscreen = false;
static std::string coverart_filename = "";
static bool do_append_hostname = true;
static bool use_random_hw_addr = false;
static unsigned short display[5] = {0}, tcp[3] = {0}, udp[3] = {0};
static bool debug_log = DEFAULT_DEBUG_LOG;
static int log_level = LOGGER_INFO;
static bool bt709_fix = false;
static int nohold = 0;
static bool nofreeze = false;
static unsigned short raop_port;
static unsigned short airplay_port;
static uint64_t remote_clock_offset = 0;
static std::vector<std::string> allowed_clients;
static std::vector<std::string> blocked_clients;
static bool restrict_clients;
static bool setup_legacy_pairing = false;
static bool require_password = false;
static unsigned short pin = 0;
static std::string keyfile = "";
static std::string mac_address = "";
static std::string dacpfile = "";
static bool registration_list = false;
static std::string pairing_register = "";
static std::vector <std::string> registered_keys;
static double db_low = -30.0;
static double db_high = 0.0;
static bool taper_volume = false;
static bool h265_support = false;
static int n_renderers = 0;
static bool hls_support = false;
static std::string url = "";
static guint gst_x11_window_id = 0;
static guint gst_hls_position_id = 0;
static bool preserve_connections = false;
/* logging */
static void log(int level, const char* format, ...) {
va_list vargs;
if (level > log_level) return;
switch (level) {
case 0:
case 1:
case 2:
case 3:
printf("*** ERROR: ");
break;
case 4:
printf("*** WARNING: ");
break;
default:
break;
}
va_start(vargs, format);
vprintf(format, vargs);
printf("\n");
va_end(vargs);
}
#define LOGD(...) log(LOGGER_DEBUG, __VA_ARGS__)
#define LOGI(...) log(LOGGER_INFO, __VA_ARGS__)
#define LOGW(...) log(LOGGER_WARNING, __VA_ARGS__)
#define LOGE(...) log(LOGGER_ERR, __VA_ARGS__)
static bool file_has_write_access (const char * filename) {
bool exists = false;
bool write = false;
#ifdef _WIN32
if ((exists = _access(filename, 0) != -1)) {
write = (_access(filename, 2) != -1);
}
#else
if ((exists = access(filename, F_OK) != -1)) {
write = (access(filename, W_OK) != -1);
}
#endif
if (!exists) {
FILE *fp = fopen(filename, "w");
if (fp) {
write = true;
fclose(fp);
remove(filename);
}
}
return write;
}
/* 95 byte png file with a 1x1 white square (single pixel): placeholder for coverart*/
static const unsigned char empty_image[] = {
0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0x00, 0x00, 0x00, 0x0d, 0x49, 0x48, 0x44, 0x52,
0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x01, 0x03, 0x00, 0x00, 0x00, 0x25, 0xdb, 0x56,
0xca, 0x00, 0x00, 0x00, 0x03, 0x50, 0x4c, 0x54, 0x45, 0x00, 0x00, 0x00, 0xa7, 0x7a, 0x3d, 0xda,
0x00, 0x00, 0x00, 0x01, 0x74, 0x52, 0x4e, 0x53, 0x00, 0x40, 0xe6, 0xd8, 0x66, 0x00, 0x00, 0x00,
0x0a, 0x49, 0x44, 0x41, 0x54, 0x08, 0xd7, 0x63, 0x60, 0x00, 0x00, 0x00, 0x02, 0x00, 0x01, 0xe2,
0x21, 0xbc, 0x33, 0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x4e, 0x44, 0xae, 0x42, 0x60, 0x82 };
static size_t write_coverart(const char *filename, const void *image, size_t len) {
FILE *fp = fopen(filename, "wb");
size_t count = fwrite(image, 1, len, fp);
fclose(fp);
return count;
}
static char *create_pin_display(char *pin_str, int margin, int gap) {
char *ptr;
char num[2] = { 0 };
int w = 10;
int h = 8;
char digits[10][8][11] = { "0821111380", "2114005113", "1110000111", "1110000111", "1110000111", "1110000111", "5113002114", "0751111470",
"0002111000", "0021111000", "0000111000", "0000111000", "0000111000", "0000111000", "0000111000", "0011111110",
"0811112800", "2114005113", "0000000111", "0000082114", "0862111470", "2114700000", "1117000000", "1111111111",
"0821111380", "2114005113", "0000082114", "0000111170", "0000075130", "1110000111", "5113002114", "0751111470",
"0000211110", "0001401110", "0021401110", "0214001110", "2110001110", "1111111111", "0000001110", "0000001110",
"1111111110", "1110000000", "1110000000", "1112111380", "0000075113", "0000000111", "5113002114", "0711114700",
"0821111380", "2114005113", "1110000000", "1112111380", "1114075113", "1110000111", "5113002114", "0751111470",
"1111111111", "0000002114", "0000021140", "0000211400", "0002114000", "0021140000", "0211400000", "2114000000",
"0831111280", "2114002114", "5113802114", "0751111170", "8214775138", "1110000111", "5113002114", "0751111470",
"0821111380", "2114005113", "1110000111", "5113802111", "0751114111", "0000000111", "5113002114", "0751111470"
};
char pixels[9] = { ' ', '8', 'd', 'b', 'P', 'Y', 'o', '"', '.' };
/* Ascii art used here is derived from the FIGlet font "collosal" */
int pin_val = (int) strtoul(pin_str, &ptr, 10);
if (*ptr) {
return NULL;
}
int len = strlen(pin_str);
int *pin = (int *) calloc( len, sizeof(int));
if(!pin) {
return NULL;
}
for (int i = 0; i < len; i++) {
pin[len - 1 - i] = pin_val % 10;
pin_val = pin_val / 10;
}
int size = 4 + h*(margin + len*(w + gap + 1));
char *pin_image = (char *) calloc(size, sizeof(char));
if (!pin_image) {
return NULL;
}
char *pos = pin_image;
snprintf(pos, 2, "\n");
pos++;
for (int i = 0; i < h; i++) {
for (int j = 0; j < margin; j++) {
snprintf(pos, 2, " ");
pos++;
}
for (int j = 0; j < len; j++) {
int l = pin[j];
char *p = digits[l][i];
for (int k = 0; k < w; k++) {
char *ptr;
strncpy(num, p++, 1);
int r = (int) strtoul(num, &ptr, 10);
snprintf(pos, 2, "%c", pixels[r]);
pos++;
}
for (int n=0; n < gap ; n++) {
snprintf(pos, 2, " ");
pos++;
}
}
snprintf(pos, 2, "\n");
pos++;
}
snprintf(pos, 2, "\n");
return pin_image;
}
static void dump_audio_to_file(unsigned char *data, int datalen, unsigned char type) {
if (!audio_dumpfile && audio_type != previous_audio_type) {
char suffix[20];
std::string fn = audio_dumpfile_name;
previous_audio_type = audio_type;
audio_dumpfile_count++;
audio_dump_count = 0;
/* type 0x20 is lossless ALAC, type 0x80 is compressed AAC-ELD, type 0x10 is "other" */
if (audio_type == 0x20) {
snprintf(suffix, sizeof(suffix), ".%d.alac", audio_dumpfile_count);
} else if (audio_type == 0x80) {
snprintf(suffix, sizeof(suffix), ".%d.aac", audio_dumpfile_count);
} else {
snprintf(suffix, sizeof(suffix), ".%d.aud", audio_dumpfile_count);
}
fn.append(suffix);
audio_dumpfile = fopen(fn.c_str(),"w");
if (audio_dumpfile == NULL) {
LOGE("could not open file %s for dumping audio frames",fn.c_str());
}
}
if (audio_dumpfile) {
fwrite(data, 1, datalen, audio_dumpfile);
if (audio_dump_limit) {
audio_dump_count++;
if (audio_dump_count == audio_dump_limit) {
fclose(audio_dumpfile);
audio_dumpfile = NULL;
}
}
}
}
static void dump_video_to_file(unsigned char *data, int datalen) {
/* SPS NAL has (data[4] & 0x1f) = 0x07 */
if ((data[4] & 0x1f) == 0x07 && video_dumpfile && video_dump_limit) {
fwrite(mark, 1, sizeof(mark), video_dumpfile);
fclose(video_dumpfile);
video_dumpfile = NULL;
video_dump_count = 0;
}
if (!video_dumpfile) {
std::string fn = video_dumpfile_name;
if (video_dump_limit) {
char suffix[20];
video_dumpfile_count++;
snprintf(suffix, sizeof(suffix), ".%d", video_dumpfile_count);
fn.append(suffix);
}
fn.append(".h264");
video_dumpfile = fopen (fn.c_str(),"w");
if (video_dumpfile == NULL) {
LOGE("could not open file %s for dumping h264 frames",fn.c_str());
}
}
if (video_dumpfile) {
if (video_dump_limit == 0) {
fwrite(data, 1, datalen, video_dumpfile);
} else if (video_dump_count < video_dump_limit) {
video_dump_count++;
fwrite(data, 1, datalen, video_dumpfile);
}
}
}
static gboolean reset_callback(gpointer loop) {
if (reset_loop) {
g_main_loop_quit((GMainLoop *) loop);
}
return TRUE;
}
static gboolean x11_window_callback(gpointer loop) {
/* called while trying to find an x11 window used by playbin (HLS mode) */
if (waiting_for_x11_window()) {
return TRUE;
}
g_source_remove(gst_x11_window_id);
gst_x11_window_id = 0;
return FALSE;
}
static gboolean sigint_callback(gpointer loop) {
relaunch_video = false;
g_main_loop_quit((GMainLoop *) loop);
return TRUE;
}
static gboolean sigterm_callback(gpointer loop) {
relaunch_video = false;
g_main_loop_quit((GMainLoop *) loop);
return TRUE;
}
#ifdef _WIN32
struct signal_handler {
GSourceFunc handler;
gpointer user_data;
};
static std::unordered_map<gint, signal_handler> u = {};
static void SignalHandler(int signum) {
if (signum == SIGTERM || signum == SIGINT) {
u[signum].handler(u[signum].user_data);
}
}
static guint g_unix_signal_add(gint signum, GSourceFunc handler, gpointer user_data) {
u[signum] = signal_handler{handler, user_data};
(void) signal(signum, SignalHandler);
return 0;
}
#endif
static void main_loop() {
guint gst_bus_watch_id[2] = { 0 };
g_assert(n_renderers <= 2);
GMainLoop *loop = g_main_loop_new(NULL,FALSE);
relaunch_video = false;
if (use_video) {
relaunch_video = true;
if (url.empty()) {
n_renderers = h265_support ? 2 : 1;
gst_x11_window_id = 0;
} else {
/* hls video will be rendered */
n_renderers = 1;
url.erase();
gst_x11_window_id = g_timeout_add(100, (GSourceFunc) x11_window_callback, (gpointer) loop);
}
for (int i = 0; i < n_renderers; i++) {
gst_bus_watch_id[i] = (guint) video_renderer_listen((void *)loop, i);
}
}
guint reset_watch_id = g_timeout_add(100, (GSourceFunc) reset_callback, (gpointer) loop);
guint video_reset_watch_id = g_timeout_add(100, (GSourceFunc) video_reset_callback, (gpointer) loop);
guint sigterm_watch_id = g_unix_signal_add(SIGTERM, (GSourceFunc) sigterm_callback, (gpointer) loop);
guint sigint_watch_id = g_unix_signal_add(SIGINT, (GSourceFunc) sigint_callback, (gpointer) loop);
g_main_loop_run(loop);
for (int i = 0; i < n_renderers; i++) {
if (gst_bus_watch_id[i] > 0) g_source_remove(gst_bus_watch_id[i]);
}
if (gst_x11_window_id > 0) g_source_remove(gst_x11_window_id);
if (sigint_watch_id > 0) g_source_remove(sigint_watch_id);
if (sigterm_watch_id > 0) g_source_remove(sigterm_watch_id);
if (reset_watch_id > 0) g_source_remove(reset_watch_id);
if (video_reset_watch_id > 0) g_source_remove(video_reset_watch_id);
g_main_loop_unref(loop);
}
static int parse_hw_addr (std::string str, std::vector<char> &hw_addr) {
for (int i = 0; i < (int) str.length(); i += 3) {
hw_addr.push_back((char) stol(str.substr(i), NULL, 16));
}
return 0;
}
static const char *get_homedir() {
const char *homedir = getenv("XDG_CONFIG_HOMEDIR");
if (homedir == NULL) {
homedir = getenv("HOME");
}
#ifndef _WIN32
if (homedir == NULL){
homedir = getpwuid(getuid())->pw_dir;
}
#endif
return homedir;
}
static std::string find_uxplay_config_file() {
std::string no_config_file = "";
const char *homedir = NULL;
const char *uxplayrc = NULL;
std::string config0, config1, config2;
struct stat sb;
uxplayrc = getenv("UXPLAYRC"); /* first look for $UXPLAYRC */
if (uxplayrc) {
config0 = uxplayrc;
if (stat(config0.c_str(), &sb) == 0) return config0;
}
homedir = get_homedir();
if (homedir) {
config1 = homedir;
config1.append("/.uxplayrc");
if (stat(config1.c_str(), &sb) == 0) return config1; /* look for ~/.uxplayrc */
config2 = homedir;
config2.append("/.config/uxplayrc"); /* look for ~/.config/uxplayrc */
if (stat(config2.c_str(), &sb) == 0) return config2;
}
return no_config_file;
}
static std::string find_mac () {
/* finds the MAC address of a network interface *
* in a Windows, Linux, *BSD or macOS system. */
std::string mac = "";
char str[3];
#ifdef _WIN32
ULONG buflen = sizeof(IP_ADAPTER_ADDRESSES);
PIP_ADAPTER_ADDRESSES addresses = (IP_ADAPTER_ADDRESSES*) malloc(buflen);
if (addresses == NULL) {
return mac;
}
if (GetAdaptersAddresses(AF_UNSPEC, 0, NULL, addresses, &buflen) == ERROR_BUFFER_OVERFLOW) {
free(addresses);
addresses = (IP_ADAPTER_ADDRESSES*) malloc(buflen);
if (addresses == NULL) {
return mac;
}
}
if (GetAdaptersAddresses(AF_UNSPEC, 0, NULL, addresses, &buflen) == NO_ERROR) {
for (PIP_ADAPTER_ADDRESSES address = addresses; address != NULL; address = address->Next) {
if (address->PhysicalAddressLength != 6 /* MAC has 6 octets */
|| (address->IfType != 6 && address->IfType != 71) /* Ethernet or Wireless interface */
|| address->OperStatus != 1) { /* interface is up */
continue;
}
mac.erase();
for (int i = 0; i < 6; i++) {
snprintf(str, sizeof(str), "%02x", int(address->PhysicalAddress[i]));
mac = mac + str;
if (i < 5) mac = mac + ":";
}
break;
}
}
free(addresses);
return mac;
#else
struct ifaddrs *ifap, *ifaptr;
int non_null_octets = 0;
unsigned char octet[6];
if (getifaddrs(&ifap) == 0) {
for(ifaptr = ifap; ifaptr != NULL; ifaptr = ifaptr->ifa_next) {
if(ifaptr->ifa_addr == NULL) continue;
#ifdef __linux__
if (ifaptr->ifa_addr->sa_family != AF_PACKET) continue;
struct sockaddr_ll *s = (struct sockaddr_ll*) ifaptr->ifa_addr;
for (int i = 0; i < 6; i++) {
if ((octet[i] = s->sll_addr[i]) != 0) non_null_octets++;
}
#else /* macOS and *BSD */
if (ifaptr->ifa_addr->sa_family != AF_LINK) continue;
unsigned char *ptr = (unsigned char *) LLADDR((struct sockaddr_dl *) ifaptr->ifa_addr);
for (int i= 0; i < 6 ; i++) {
if ((octet[i] = *ptr) != 0) non_null_octets++;
ptr++;
}
#endif
if (non_null_octets) {
mac.erase();
for (int i = 0; i < 6 ; i++) {
snprintf(str, sizeof(str), "%02x", octet[i]);
mac = mac + str;
if (i < 5) mac = mac + ":";
}
break;
}
}
}
freeifaddrs(ifap);
#endif
return mac;
}
#define MULTICAST 0
#define LOCAL 1
#define OCTETS 6
static bool validate_mac(char * mac_address) {
char c;
if (strlen(mac_address) != 17) return false;
for (int i = 0; i < 17; i++) {
c = *(mac_address + i);
if (i % 3 == 2) {
if (c != ':') return false;
} else {
if (c < '0') return false;
if (c > '9' && c < 'A') return false;
if (c > 'F' && c < 'a') return false;
if (c > 'f') return false;
}
}
return true;
}
static std::string random_mac () {
char str[3];
int octet = rand() % 64;
octet = (octet << 1) + LOCAL;
octet = (octet << 1) + MULTICAST;
snprintf(str,3,"%02x",octet);
std::string mac_address(str);
for (int i = 1; i < OCTETS; i++) {
mac_address = mac_address + ":";
octet = rand() % 256;
snprintf(str,3,"%02x",octet);
mac_address = mac_address + str;
}
return mac_address;
}
static void print_info (char *name) {
printf("UxPlay %s: An open-source AirPlay mirroring server.\n", VERSION);
printf("=========== Website: https://github.com/FDH2/UxPlay ==========\n");
printf("Usage: %s [-n name] [-s wxh] [-p [n]] [(other options)]\n", name);
printf("Options:\n");
printf("-n name Specify the network name of the AirPlay server\n");
printf("-nh Do not add \"@hostname\" at the end of AirPlay server name\n");
printf("-h265 Support h265 (4K) video (with h265 versions of h264 plugins)\n");
printf("-hls Support HTTP Live Streaming (currently Youtube video only) \n");
printf("-pin[xxxx]Use a 4-digit pin code to control client access (default: no)\n");
printf("-di <fn> Set default image file to display when video is not available\n");
printf(" default pin is random: optionally use fixed pin xxxx\n");
printf("-reg [fn] Keep a register in $HOME/.uxplay.register to verify returning\n");
printf(" client pin-registration; (option: use file \"fn\" for this)\n");
printf("-vsync [x]Mirror mode: sync audio to video using timestamps (default)\n");
printf(" x is optional audio delay: millisecs, decimal, can be neg.\n");
printf("-vsync no Switch off audio/(server)video timestamp synchronization \n");
printf("-async [x]Audio-Only mode: sync audio to client video (default: no)\n");
printf("-async no Switch off audio/(client)video timestamp synchronization\n");
printf("-db l[:h] Set minimum volume attenuation to l dB (decibels, negative);\n");
printf(" optional: set maximum to h dB (+ or -) default: -30.0:0.0 dB\n");
printf("-taper Use a \"tapered\" AirPlay volume-control profile\n");
printf("-s wxh[@r]Request to client for video display resolution [refresh_rate]\n");
printf(" default 1920x1080[@60] (or 3840x2160[@60] with -h265 option)\n");
printf("-o Set display \"overscanned\" mode on (not usually needed)\n");
printf("-fs Full-screen (only works with X11, Wayland, VAAPI, D3D11)\n");
printf("-p Use legacy ports UDP 6000:6001:7011 TCP 7000:7001:7100\n");
printf("-p n Use TCP and UDP ports n,n+1,n+2. range %d-%d\n", LOWEST_ALLOWED_PORT, HIGHEST_PORT);
printf(" use \"-p n1,n2,n3\" to set each port, \"n1,n2\" for n3 = n2+1\n");
printf(" \"-p tcp n\" or \"-p udp n\" sets TCP or UDP ports separately\n");
printf("-avdec Force software h264 video decoding with libav decoder\n");
printf("-vp ... Choose the GSteamer h264 parser: default \"h264parse\"\n");
printf("-vd ... Choose the GStreamer h264 decoder; default \"decodebin\"\n");
printf(" choices: (software) avdec_h264; (hardware) v4l2h264dec,\n");
printf(" nvdec, nvh264dec, vaapih64dec, vtdec,etc.\n");
printf(" choices: avdec_h264,vaapih264dec,nvdec,nvh264dec,v4l2h264dec\n");
printf("-vc ... Choose the GStreamer videoconverter; default \"videoconvert\"\n");
printf(" another choice when using v4l2h264dec: v4l2convert\n");
printf("-vs ... Choose the GStreamer videosink; default \"autovideosink\"\n");
printf(" some choices: ximagesink,xvimagesink,vaapisink,glimagesink,\n");
printf(" gtksink,waylandsink,osxvideosink,kmssink,d3d11videosink etc.\n");
printf("-vs 0 Streamed audio only, with no video display window\n");
printf("-v4l2 Use Video4Linux2 for GPU hardware h264 decoding\n");
printf("-bt709 Sometimes needed for Raspberry Pi models using Video4Linux2 \n");
printf("-as ... Choose the GStreamer audiosink; default \"autoaudiosink\"\n");
printf(" some choices:pulsesink,alsasink,pipewiresink,jackaudiosink,\n");
printf(" osssink,oss4sink,osxaudiosink,wasapisink,directsoundsink.\n");
printf("-as 0 (or -a) Turn audio off, streamed video only\n");
printf("-al x Audio latency in seconds (default 0.25) reported to client.\n");
printf("-ca <fn> In Airplay Audio (ALAC) mode, write cover-art to file <fn>\n");
printf("-reset n Reset after 3n seconds client silence (default %d, 0=never)\n", NTP_TIMEOUT_LIMIT);
printf("-nofreeze Do NOT leave frozen screen in place after reset\n");
printf("-nc Do NOT Close video window when client stops mirroring\n");
printf("-nohold Drop current connection when new client connects.\n");
printf("-restrict Restrict clients to those specified by \"-allow <deviceID>\"\n");
printf(" UxPlay displays deviceID when a client attempts to connect\n");
printf(" Use \"-restrict no\" for no client restrictions (default)\n");
printf("-allow <i>Permit deviceID = <i> to connect if restrictions are imposed\n");
printf("-block <i>Always block connections from deviceID = <i>\n");
printf("-FPSdata Show video-streaming performance reports sent by client.\n");
printf("-fps n Set maximum allowed streaming framerate, default 30\n");
printf("-f {H|V|I}Horizontal|Vertical flip, or both=Inversion=rotate 180 deg\n");
printf("-r {R|L} Rotate 90 degrees Right (cw) or Left (ccw)\n");
printf("-m [mac] Set MAC address (also Device ID);use for concurrent UxPlays\n");
printf(" if mac xx:xx:xx:xx:xx:xx is not given, a random MAC is used\n");
printf("-key [fn] Store private key in $HOME/.uxplay.pem (or in file \"fn\")\n");
printf("-dacp [fn]Export client DACP information to file $HOME/.uxplay.dacp\n");
printf(" (option to use file \"fn\" instead); used for client remote\n");
printf("-vdmp [n] Dump h264 video output to \"fn.h264\"; fn=\"videodump\",change\n");
printf(" with \"-vdmp [n] filename\". If [n] is given, file fn.x.h264\n");
printf(" x=1,2,.. opens whenever a new SPS/PPS NAL arrives, and <=n\n");
printf(" NAL units are dumped.\n");
printf("-admp [n] Dump audio output to \"fn.x.fmt\", fmt ={aac, alac, aud}, x\n");
printf(" =1,2,..; fn=\"audiodump\"; change with \"-admp [n] filename\".\n");
printf(" x increases when audio format changes. If n is given, <= n\n");
printf(" audio packets are dumped. \"aud\"= unknown format.\n");
printf("-d Enable debug logging\n");
printf("-v Displays version information\n");
printf("-h Displays this help\n");
printf("Startup options in $UXPLAYRC, ~/.uxplayrc, or ~/.config/uxplayrc are\n");
printf("applied first (command-line options may modify them): format is one \n");
printf("option per line, no initial \"-\"; lines starting with \"#\" are ignored.\n");
}
static bool option_has_value(const int i, const int argc, std::string option, const char *next_arg) {
if (i >= argc - 1 || next_arg[0] == '-') {
LOGE("invalid: \"%s\" had no argument", option.c_str());
return false;
}
return true;
}
static bool get_display_settings (std::string value, unsigned short *w, unsigned short *h, unsigned short *r) {
// assume str = wxh@r is valid if w and h are positive decimal integers
// with no more than 4 digits, r < 256 (stored in one byte).
char *end;
std::size_t pos = value.find_first_of("x");
if (pos == std::string::npos) return false;
std::string str1 = value.substr(pos+1);
value.erase(pos);
if (value.length() == 0 || value.length() > 4 || value[0] == '-') return false;
*w = (unsigned short) strtoul(value.c_str(), &end, 10);
if (*end || *w == 0) return false;
pos = str1.find_first_of("@");
if(pos != std::string::npos) {
std::string str2 = str1.substr(pos+1);
if (str2.length() == 0 || str2.length() > 3 || str2[0] == '-') return false;
*r = (unsigned short) strtoul(str2.c_str(), &end, 10);
if (*end || *r == 0 || *r > 255) return false;
str1.erase(pos);
}
if (str1.length() == 0 || str1.length() > 4 || str1[0] == '-') return false;
*h = (unsigned short) strtoul(str1.c_str(), &end, 10);
if (*end || *h == 0) return false;
return true;
}
static bool get_value (const char *str, unsigned int *n) {
// if n > 0 str must be a positive decimal <= input value *n
// if n = 0, str must be a non-negative decimal
if (strlen(str) == 0 || strlen(str) > 10 || str[0] == '-') return false;
char *end;
unsigned long l = strtoul(str, &end, 10);
if (*end) return false;
if (*n && (l == 0 || l > *n)) return false;
*n = (unsigned int) l;
return true;
}
static bool get_ports (int nports, std::string option, const char * value, unsigned short * const port) {
/*valid entries are comma-separated values port_1,port_2,...,port_r, 0 < r <= nports */
/*where ports are distinct, and are in the allowed range. */
/*missing values are consecutive to last given value (at least one value needed). */
char *end;
unsigned long l;
std::size_t pos;
std::string val(value), str;
for (int i = 0; i <= nports ; i++) {
if(i == nports) break;
pos = val.find_first_of(',');
str = val.substr(0,pos);
if(str.length() == 0 || str.length() > 5 || str[0] == '-') break;
l = strtoul(str.c_str(), &end, 10);
if (*end || l < LOWEST_ALLOWED_PORT || l > HIGHEST_PORT) break;
*(port + i) = (unsigned short) l;
for (int j = 0; j < i ; j++) {
if( *(port + j) == *(port + i)) break;
}
if(pos == std::string::npos) {
if (nports + *(port + i) > i + 1 + HIGHEST_PORT) break;
for (int j = i + 1; j < nports; j++) {
*(port + j) = *(port + j - 1) + 1;
}
return true;
}
val.erase(0, pos+1);
}
LOGE("invalid \"%s %s\", all %d ports must be in range [%d,%d]",
option.c_str(), value, nports, LOWEST_ALLOWED_PORT, HIGHEST_PORT);
return false;
}
static bool get_videoflip (const char *str, videoflip_t *videoflip) {
if (strlen(str) > 1) return false;
switch (str[0]) {
case 'I':
*videoflip = INVERT;
break;
case 'H':
*videoflip = HFLIP;
break;
case 'V':
*videoflip = VFLIP;
break;
default:
return false;
}
return true;
}
static bool get_videorotate (const char *str, videoflip_t *videoflip) {
if (strlen(str) > 1) return false;
switch (str[0]) {
case 'L':
*videoflip = LEFT;
break;
case 'R':
*videoflip = RIGHT;
break;
default:
return false;
}
return true;
}
static void append_hostname(std::string &server_name) {
#ifdef _WIN32 /*modification for compilation on Windows */
char buffer[256] = "";
unsigned long size = sizeof(buffer);
if (GetComputerNameA(buffer, &size)) {
std::string name = server_name;
name.append("@");
name.append(buffer);
server_name = name;
}
#else
struct utsname buf;
if (!uname(&buf)) {
std::string name = server_name;
name.append("@");
name.append(buf.nodename);
server_name = name;
}
#endif
}
static void parse_arguments (int argc, char *argv[]) {
// Parse arguments
for (int i = 1; i < argc; i++) {
std::string arg(argv[i]);
if (arg == "-di") {
if (!option_has_value(i, argc, arg, argv[i+1])) exit(1);
default_image = std::string(argv[++i]);
LOGI("Using default image: %s", default_image.c_str());
} else if (arg == "-allow") {
if (!option_has_value(i, argc, arg, argv[i+1])) exit(1);
i++;
allowed_clients.push_back(argv[i]);
} else if (arg == "-block") {
if (!option_has_value(i, argc, arg, argv[i+1])) exit(1);
i++;
blocked_clients.push_back(argv[i]);
} else if (arg == "-restrict") {
if (i < argc - 1) {
if (strlen(argv[i+1]) == 2 && strncmp(argv[i+1], "no", 2) == 0) {
restrict_clients = false;
i++;
continue;
}
}
restrict_clients = true;
} else if (arg == "-n") {
if (!option_has_value(i, argc, arg, argv[i+1])) exit(1);
server_name = std::string(argv[++i]);
} else if (arg == "-nh") {
do_append_hostname = false;
} else if (arg == "-async") {
audio_sync = true;
if (i < argc - 1) {
if (strlen(argv[i+1]) == 2 && strncmp(argv[i+1], "no", 2) == 0) {
audio_sync = false;
i++;
continue;
}
char *end;
int n = (int) (strtof(argv[i + 1], &end) * 1000);
if (*end == '\0') {
i++;
if (n > -SECOND_IN_USECS && n < SECOND_IN_USECS) {
audio_delay_alac = n * 1000; /* units are nsecs */
} else {
fprintf(stderr, "invalid -async %s: requested delays must be smaller than +/- 1000 millisecs\n", argv[i] );
exit (1);
}
}
}
} else if (arg == "-vsync") {
video_sync = true;
if (i < argc - 1) {
if (strlen(argv[i+1]) == 2 && strncmp(argv[i+1], "no", 2) == 0) {
video_sync = false;
i++;
continue;
}
char *end;
int n = (int) (strtof(argv[i + 1], &end) * 1000);
if (*end == '\0') {
i++;
if (n > -SECOND_IN_USECS && n < SECOND_IN_USECS) {
audio_delay_aac = n * 1000; /* units are nsecs */
} else {
fprintf(stderr, "invalid -vsync %s: requested delays must be smaller than +/- 1000 millisecs\n", argv[i]);
exit (1);
}
}
}
} else if (arg == "-s") {
if (!option_has_value(i, argc, argv[i], argv[i+1])) exit(1);
std::string value(argv[++i]);
if (!get_display_settings(value, &display[0], &display[1], &display[2])) {
fprintf(stderr, "invalid \"-s %s\"; -s wxh : max w,h=9999; -s wxh@r : max r=255\n",
argv[i]);
exit(1);
}
} else if (arg == "-fps") {
if (!option_has_value(i, argc, arg, argv[i+1])) exit(1);
unsigned int n = 255;
if (!get_value(argv[++i], &n)) {
fprintf(stderr, "invalid \"-fps %s\"; -fps n : max n=255, default n=30\n", argv[i]);
exit(1);
}
display[3] = (unsigned short) n;
} else if (arg == "-o") {
display[4] = 1;
} else if (arg == "-f") {
if (!option_has_value(i, argc, arg, argv[i+1])) exit(1);
if (!get_videoflip(argv[++i], &videoflip[0])) {
fprintf(stderr,"invalid \"-f %s\" , unknown flip type, choices are H, V, I\n",argv[i]);
exit(1);
}
} else if (arg == "-r") {
if (!option_has_value(i, argc, arg, argv[i+1])) exit(1);
if (!get_videorotate(argv[++i], &videoflip[1])) {
fprintf(stderr,"invalid \"-r %s\" , unknown rotation type, choices are R, L\n",argv[i]);
exit(1);
}
} else if (arg == "-p") {
if (i == argc - 1 || argv[i + 1][0] == '-') {
tcp[0] = 7100; tcp[1] = 7000; tcp[2] = 7001;
udp[0] = 7011; udp[1] = 6001; udp[2] = 6000;
continue;
}
std::string value(argv[++i]);
if (value == "tcp") {
arg.append(" tcp");
if(!get_ports(3, arg, argv[++i], tcp)) exit(1);
} else if (value == "udp") {
arg.append( " udp");
if(!get_ports(3, arg, argv[++i], udp)) exit(1);
} else {
if(!get_ports(3, arg, argv[i], tcp)) exit(1);
for (int j = 1; j < 3; j++) {
udp[j] = tcp[j];
}
}
} else if (arg == "-m") {
if (i < argc - 1 && *argv[i+1] != '-') {
if (validate_mac(argv[++i])) {
mac_address.erase();
mac_address = argv[i];
use_random_hw_addr = false;
} else {
fprintf(stderr,"invalid mac address \"%s\": address must have form"
" \"xx:xx:xx:xx:xx:xx\", x = 0-9, A-F or a-f\n", argv[i]);
exit(1);
}
} else {
use_random_hw_addr = true;
}
} else if (arg == "-a") {
use_audio = false;
} else if (arg == "-d") {
debug_log = !debug_log;
} else if (arg == "-h" || arg == "--help" || arg == "-?" || arg == "-help") {
print_info(argv[0]);
exit(0);
} else if (arg == "-v") {
printf("UxPlay version %s; for help, use option \"-h\"\n", VERSION);
exit(0);
} else if (arg == "-vp") {
if (!option_has_value(i, argc, arg, argv[i+1])) exit(1);
video_parser.erase();
video_parser.append(argv[++i]);
} else if (arg == "-vd") {
if (!option_has_value(i, argc, arg, argv[i+1])) exit(1);
video_decoder.erase();
video_decoder.append(argv[++i]);
} else if (arg == "-vc") {
if (!option_has_value(i, argc, arg, argv[i+1])) exit(1);
video_converter.erase();
video_converter.append(argv[++i]);
} else if (arg == "-vs") {
if (!option_has_value(i, argc, arg, argv[i+1])) exit(1);
videosink.erase();
videosink.append(argv[++i]);
std::size_t pos = videosink.find(" ");
if (pos != std::string::npos) {
videosink_options.erase();
videosink_options = videosink.substr(pos);
videosink.erase(pos);
}
} else if (arg == "-as") {
if (!option_has_value(i, argc, arg, argv[i+1])) exit(1);
audiosink.erase();
audiosink.append(argv[++i]);
} else if (arg == "-t") {
fprintf(stderr,"The uxplay option \"-t\" has been removed: it was a workaround for an Avahi issue.\n");
fprintf(stderr,"The correct solution is to open network port UDP 5353 in the firewall for mDNS queries\n");
exit(1);
} else if (arg == "-nc") {
new_window_closing_behavior = false;
} else if (arg == "-avdec") {
video_parser.erase();
video_parser = "h264parse";
video_decoder.erase();
video_decoder = "avdec_h264";
video_converter.erase();
video_converter = "videoconvert";
} else if (arg == "-v4l2") {
video_decoder.erase();
video_decoder = "v4l2h264dec";
video_converter.erase();
video_converter = "v4l2convert";
} else if (arg == "-rpi" || arg == "-rpifb" || arg == "-rpigl" || arg == "-rpiwl") {
fprintf(stderr,"*** -rpi* options do not apply to Raspberry Pi model 5, and have been removed\n");
fprintf(stderr," For models 3 and 4, use their equivalents, if needed:\n");
fprintf(stderr," -rpi was equivalent to \"-v4l2\"\n");
fprintf(stderr," -rpifb was equivalent to \"-v4l2 -vs kmssink\"\n");
fprintf(stderr," -rpigl was equivalent to \"-v4l2 -vs glimagesink\"\n");
fprintf(stderr," -rpiwl was equivalent to \"-v4l2 -vs waylandsink\"\n");
fprintf(stderr," for GStreamer < 1.22, \"-bt709\" may also be needed\n");
exit(1);
} else if (arg == "-fs" ) {
fullscreen = true;
} else if (arg == "-FPSdata") {
show_client_FPS_data = true;
} else if (arg == "-reset") {
max_ntp_timeouts = 0;
if (!get_value(argv[++i], &max_ntp_timeouts)) {
fprintf(stderr, "invalid \"-reset %s\"; -reset n must have n >= 0, default n = %d\n", argv[i], NTP_TIMEOUT_LIMIT);
exit(1);
}
} else if (arg == "-vdmp") {
dump_video = true;
if (i < argc - 1 && *argv[i+1] != '-') {
unsigned int n = 0;
if (get_value (argv[++i], &n)) {
if (n == 0) {
fprintf(stderr, "invalid \"-vdmp 0 %s\"; -vdmp n needs a non-zero value of n\n", argv[i]);
exit(1);
}
video_dump_limit = n;
if (option_has_value(i, argc, arg, argv[i+1])) {
video_dumpfile_name.erase();
video_dumpfile_name.append(argv[++i]);
}
} else {
video_dumpfile_name.erase();
video_dumpfile_name.append(argv[i]);
}
const char *fn = video_dumpfile_name.c_str();
if (!file_has_write_access(fn)) {
fprintf(stderr, "%s cannot be written to:\noption \"-vdmp <fn>\" must be to a file with write access\n", fn);
exit(1);
}
}
} else if (arg == "-admp") {
dump_audio = true;
if (i < argc - 1 && *argv[i+1] != '-') {
unsigned int n = 0;
if (get_value (argv[++i], &n)) {
if (n == 0) {
fprintf(stderr, "invalid \"-admp 0 %s\"; -admp n needs a non-zero value of n\n", argv[i]);
exit(1);
}
audio_dump_limit = n;
if (option_has_value(i, argc, arg, argv[i+1])) {
audio_dumpfile_name.erase();
audio_dumpfile_name.append(argv[++i]);
}
} else {
audio_dumpfile_name.erase();
audio_dumpfile_name.append(argv[i]);
}
const char *fn = audio_dumpfile_name.c_str();
if (!file_has_write_access(fn)) {
fprintf(stderr, "%s cannot be written to:\noption \"-admp <fn>\" must be to a file with write access\n", fn);
exit(1);
}
}
} else if (arg == "-ca" ) {
if (option_has_value(i, argc, arg, argv[i+1])) {
coverart_filename.erase();
coverart_filename.append(argv[++i]);
const char *fn = coverart_filename.c_str();
if (!file_has_write_access(fn)) {
fprintf(stderr, "%s cannot be written to:\noption \"-ca <fn>\" must be to a file with write access\n", fn);
exit(1);
}
} else {
fprintf(stderr,"option -ca must be followed by a filename for cover-art output\n");
exit(1);
}
} else if (arg == "-bt709") {
bt709_fix = true;
} else if (arg == "-nohold") {
nohold = 1;
} else if (arg == "-al") {
int n;
char *end;
if (i < argc - 1 && *argv[i+1] != '-') {
n = (int) (strtof(argv[++i], &end) * SECOND_IN_USECS);
if (*end == '\0' && n >=0 && n <= 10 * SECOND_IN_USECS) {
audiodelay = n;
continue;
}
}
fprintf(stderr, "invalid -al %s: value must be a decimal time offset in seconds, range [0,10]\n"
"(like 5 or 4.8, which will be converted to a whole number of microseconds)\n", argv[i]);
exit(1);
} else if (arg == "-pin") {
setup_legacy_pairing = true;
require_password = true;
if (i < argc - 1 && *argv[i+1] != '-') {
unsigned int n = 9999;
if (!get_value(argv[++i], &n)) {
fprintf(stderr, "invalid \"-pin %s\"; -pin nnnn : max nnnn=9999, (4 digits)\n", argv[i]);
exit(1);
}
pin = n + 10000;
}
} else if (arg == "-reg") {
registration_list = true;
pairing_register.erase();
if (i < argc - 1 && *argv[i+1] != '-') {
pairing_register.append(argv[++i]);
const char * fn = pairing_register.c_str();
if (!file_has_write_access(fn)) {
fprintf(stderr, "%s cannot be written to:\noption \"-key <fn>\" must be to a file with write access\n", fn);
exit(1);
}
}
} else if (arg == "-key") {
keyfile.erase();
if (i < argc - 1 && *argv[i+1] != '-') {
keyfile.append(argv[++i]);
const char * fn = keyfile.c_str();
if (!file_has_write_access(fn)) {
fprintf(stderr, "%s cannot be written to:\noption \"-key <fn>\" must be to a file with write access\n", fn);
exit(1);
}
} else {
// fprintf(stderr, "option \"-key <fn>\" requires a path <fn> to a file for persistent key storage\n");
// exit(1);
keyfile.erase();
keyfile.append("0");
}
} else if (arg == "-dacp") {
dacpfile.erase();
if (i < argc - 1 && *argv[i+1] != '-') {
dacpfile.append(argv[++i]);
const char *fn = dacpfile.c_str();
if (!file_has_write_access(fn)) {
fprintf(stderr, "%s cannot be written to:\noption \"-dacp <fn>\" must be to a file with write access\n", fn);
exit(1);
}
} else {
dacpfile.append(get_homedir());
dacpfile.append("/.uxplay.dacp");
}
} else if (arg == "-taper") {
taper_volume = true;
} else if (arg == "-db") {
bool db_bad = true;
double db1, db2;
char *start = NULL;
if ( i < argc -1) {
char *end1, *end2;
start = argv[i+1];
db1 = strtod(start, &end1);
if (end1 > start && *end1 == ':') {
db2 = strtod(++end1, &end2);
if ( *end2 == '\0' && end2 > end1 && db1 < 0 && db1 < db2) {
db_bad = false;
}
} else if (*end1 =='\0' && end1 > start && db1 < 0 ) {
db_bad = false;
db2 = 0.0;
}
}
if (db_bad) {
fprintf(stderr, "invalid %s %s: db value must be \"low\" or \"low:high\", low < 0 and high > low are decibel gains\n", argv[i], start);
exit(1);
}
i++;
db_low = db1;
db_high = db2;
printf("db range %f:%f\n", db_low, db_high);
} else if (arg == "-hls") {
hls_support = true;
} else if (arg == "-h265") {
h265_support = true;
} else if (arg == "-nofreeze") {
nofreeze = true;
} else {
fprintf(stderr, "unknown option %s, stopping (for help use option \"-h\")\n",argv[i]);
exit(1);
}
}
}
static void process_metadata(int count, const char *dmap_tag, const unsigned char* metadata, int datalen) {
int dmap_type = 0;
/* DMAP metadata items can be strings (dmap_type = 9); other types are byte, short, int, long, date, and list. *
* The DMAP item begins with a 4-character (4-letter) "dmap_tag" string that identifies the type. */
if (debug_log) {
printf("%d: dmap_tag [%s], %d\n", count, dmap_tag, datalen);
}
/* UTF-8 String-type DMAP tags seen in Apple Music Radio are processed here. *
* (DMAP tags "asal", "asar", "ascp", "asgn", "minm" ). TODO expand this */
if (datalen == 0) {
return;
}
if (dmap_tag[0] == 'a' && dmap_tag[1] == 's') {
dmap_type = 9;
switch (dmap_tag[2]) {
case 'a':
switch (dmap_tag[3]) {
case 'a':
printf("Album artist: "); /*asaa*/
break;
case 'l':
printf("Album: "); /*asal*/
break;
case 'r':
printf("Artist: "); /*asar*/
break;
default:
dmap_type = 0;
break;
}
break;
case 'c':
switch (dmap_tag[3]) {
case 'm':
printf("Comment: "); /*ascm*/
break;
case 'n':
printf("Content description: "); /*ascn*/
break;
case 'p':
printf("Composer: "); /*ascp*/
break;
case 't':
printf("Category: "); /*asct*/
break;
default:
dmap_type = 0;
break;
}
break;
case 's':
switch (dmap_tag[3]) {
case 'a':
printf("Sort Artist: "); /*assa*/
break;
case 'c':
printf("Sort Composer: "); /*assc*/
break;
case 'l':
printf("Sort Album artist: "); /*assl*/
break;
case 'n':
printf("Sort Name: "); /*assn*/
break;
case 's':
printf("Sort Series: "); /*asss*/
break;
case 'u':
printf("Sort Album: "); /*assu*/
break;
default:
dmap_type = 0;
break;
}
break;
default:
if (strcmp(dmap_tag, "asdt") == 0) {
printf("Description: ");
} else if (strcmp (dmap_tag, "asfm") == 0) {
printf("Format: ");
} else if (strcmp (dmap_tag, "asgn") == 0) {
printf("Genre: ");
} else if (strcmp (dmap_tag, "asky") == 0) {
printf("Keywords: ");
} else if (strcmp (dmap_tag, "aslc") == 0) {
printf("Long Content Description: ");
} else {
dmap_type = 0;
}
break;
}
} else if (strcmp (dmap_tag, "minm") == 0) {
dmap_type = 9;
printf("Title: ");
}
if (dmap_type == 9) {
char *str = (char *) calloc(1, datalen + 1);
memcpy(str, metadata, datalen);
printf("%s", str);
free(str);
} else if (debug_log) {
for (int i = 0; i < datalen; i++) {
if (i > 0 && i % 16 == 0) printf("\n");
printf("%2.2x ", (int) metadata[i]);
}
}
printf("\n");
}
static int parse_dmap_header(const unsigned char *metadata, char *tag, int *len) {
const unsigned char *header = metadata;
bool istag = true;
for (int i = 0; i < 4; i++) {
tag[i] = (char) *header;
if (!isalpha(tag[i])) {
istag = false;
}
header++;
}
*len = 0;
for (int i = 0; i < 4; i++) {
*len <<= 8;
*len += (int) *header;
header++;
}
if (!istag || *len < 0) {
return 1;
}
return 0;
}
static int register_dnssd() {
int dnssd_error;
uint64_t features;
if ((dnssd_error = dnssd_register_raop(dnssd, raop_port))) {
if (dnssd_error == -65537) {
LOGE("No DNS-SD Server found (DNSServiceRegister call returned kDNSServiceErr_Unknown)");
} else if (dnssd_error == -65548) {
LOGE("DNSServiceRegister call returned kDNSServiceErr_NameConflict");
LOGI("Is another instance of %s running with the same DeviceID (MAC address) or using same network ports?",
DEFAULT_NAME);
LOGI("Use options -m ... and -p ... to allow multiple instances of %s to run concurrently", DEFAULT_NAME);
} else {
LOGE("dnssd_register_raop failed with error code %d\n"
"mDNS Error codes are in range FFFE FF00 (-65792) to FFFE FFFF (-65537) "
"(see Apple's dns_sd.h)", dnssd_error);
}
return -3;
}
if ((dnssd_error = dnssd_register_airplay(dnssd, airplay_port))) {
LOGE("dnssd_register_airplay failed with error code %d\n"
"mDNS Error codes are in range FFFE FF00 (-65792) to FFFE FFFF (-65537) "
"(see Apple's dns_sd.h)", dnssd_error);
return -4;
}
LOGD("register_dnssd: advertised AirPlay service with \"Features\" code = 0x%llX",
dnssd_get_airplay_features(dnssd));
return 0;
}
static void unregister_dnssd() {
if (dnssd) {
dnssd_unregister_raop(dnssd);
dnssd_unregister_airplay(dnssd);
}
return;
}
static void stop_dnssd() {
if (dnssd) {
unregister_dnssd();
dnssd_destroy(dnssd);
dnssd = NULL;
return;
}
}
static int start_dnssd(std::vector<char> hw_addr, std::string name) {
int dnssd_error;
int require_pw = (require_password ? 1 : 0);
if (dnssd) {
LOGE("start_dnssd error: dnssd != NULL");
return 2;
}
dnssd = dnssd_init(name.c_str(), strlen(name.c_str()), hw_addr.data(), hw_addr.size(), &dnssd_error, require_pw);
if (dnssd_error) {
LOGE("Could not initialize dnssd library!: error %d", dnssd_error);
return 1;
}
/* after dnssd starts, reset the default feature set here
* (overwrites features set in dnssdint.h)
* default: FEATURES_1 = 0x5A7FFEE6, FEATURES_2 = 0 */
dnssd_set_airplay_features(dnssd, 0, 0); // AirPlay video supported
dnssd_set_airplay_features(dnssd, 1, 1); // photo supported
dnssd_set_airplay_features(dnssd, 2, 1); // video protected with FairPlay DRM
dnssd_set_airplay_features(dnssd, 3, 0); // volume control supported for videos
dnssd_set_airplay_features(dnssd, 4, 0); // http live streaming (HLS) supported
dnssd_set_airplay_features(dnssd, 5, 1); // slideshow supported
dnssd_set_airplay_features(dnssd, 6, 1); //
dnssd_set_airplay_features(dnssd, 7, 1); // mirroring supported
dnssd_set_airplay_features(dnssd, 8, 0); // screen rotation supported
dnssd_set_airplay_features(dnssd, 9, 1); // audio supported
dnssd_set_airplay_features(dnssd, 10, 1); //
dnssd_set_airplay_features(dnssd, 11, 1); // audio packet redundancy supported
dnssd_set_airplay_features(dnssd, 12, 1); // FaiPlay secure auth supported
dnssd_set_airplay_features(dnssd, 13, 1); // photo preloading supported
dnssd_set_airplay_features(dnssd, 14, 1); // Authentication bit 4: FairPlay authentication
dnssd_set_airplay_features(dnssd, 15, 1); // Metadata bit 1 support: Artwork
dnssd_set_airplay_features(dnssd, 16, 1); // Metadata bit 2 support: Soundtrack Progress
dnssd_set_airplay_features(dnssd, 17, 1); // Metadata bit 0 support: Text (DAACP) "Now Playing" info.
dnssd_set_airplay_features(dnssd, 18, 1); // Audio format 1 support:
dnssd_set_airplay_features(dnssd, 19, 1); // Audio format 2 support: must be set for AirPlay 2 multiroom audio
dnssd_set_airplay_features(dnssd, 20, 1); // Audio format 3 support: must be set for AirPlay 2 multiroom audio
dnssd_set_airplay_features(dnssd, 21, 1); // Audio format 4 support:
dnssd_set_airplay_features(dnssd, 22, 1); // Authentication type 4: FairPlay authentication
dnssd_set_airplay_features(dnssd, 23, 0); // Authentication type 1: RSA Authentication
dnssd_set_airplay_features(dnssd, 24, 0); //
dnssd_set_airplay_features(dnssd, 25, 1); //
dnssd_set_airplay_features(dnssd, 26, 0); // Has Unified Advertiser info
dnssd_set_airplay_features(dnssd, 27, 1); // Supports Legacy Pairing
dnssd_set_airplay_features(dnssd, 28, 1); //
dnssd_set_airplay_features(dnssd, 29, 0); //
dnssd_set_airplay_features(dnssd, 30, 1); // RAOP support: with this bit set, the AirTunes service is not required.
dnssd_set_airplay_features(dnssd, 31, 0); //
/* bits 32-63: see https://emanualcozzi.net/docs/airplay2/features
dnssd_set_airplay_features(dnssd, 32, 0); // isCarPlay when ON,; Supports InitialVolume when OFF
dnssd_set_airplay_features(dnssd, 33, 0); // Supports Air Play Video Play Queue
dnssd_set_airplay_features(dnssd, 34, 0); // Supports Air Play from cloud (requires that bit 6 is ON)
dnssd_set_airplay_features(dnssd, 35, 0); // Supports TLS_PSK
dnssd_set_airplay_features(dnssd, 36, 0); //
dnssd_set_airplay_features(dnssd, 37, 0); //
dnssd_set_airplay_features(dnssd, 38, 0); // Supports Unified Media Control (CoreUtils Pairing and Encryption)
dnssd_set_airplay_features(dnssd, 39, 0); //
dnssd_set_airplay_features(dnssd, 40, 0); // Supports Buffered Audio
dnssd_set_airplay_features(dnssd, 41, 0); // Supports PTP
dnssd_set_airplay_features(dnssd, 42, 0); // Supports Screen Multi Codec (allows h265 video)
dnssd_set_airplay_features(dnssd, 43, 0); // Supports System Pairing
dnssd_set_airplay_features(dnssd, 44, 0); // is AP Valeria Screen Sender
dnssd_set_airplay_features(dnssd, 45, 0); //
dnssd_set_airplay_features(dnssd, 46, 0); // Supports HomeKit Pairing and Access Control
dnssd_set_airplay_features(dnssd, 47, 0); //
dnssd_set_airplay_features(dnssd, 48, 0); // Supports CoreUtils Pairing and Encryption
dnssd_set_airplay_features(dnssd, 49, 0); //
dnssd_set_airplay_features(dnssd, 50, 0); // Metadata bit 3: "Now Playing" info sent by bplist not DAACP test
dnssd_set_airplay_features(dnssd, 51, 0); // Supports Unified Pair Setup and MFi Authentication
dnssd_set_airplay_features(dnssd, 52, 0); // Supports Set Peers Extended Message
dnssd_set_airplay_features(dnssd, 53, 0); //
dnssd_set_airplay_features(dnssd, 54, 0); // Supports AP Sync
dnssd_set_airplay_features(dnssd, 55, 0); // Supports WoL
dnssd_set_airplay_features(dnssd, 56, 0); // Supports Wol
dnssd_set_airplay_features(dnssd, 57, 0); //
dnssd_set_airplay_features(dnssd, 58, 0); // Supports Hangdog Remote Control
dnssd_set_airplay_features(dnssd, 59, 0); // Supports AudioStreamConnection setup
dnssd_set_airplay_features(dnssd, 60, 0); // Supports Audo Media Data Control
dnssd_set_airplay_features(dnssd, 61, 0); // Supports RFC2198 redundancy
*/
/* needed for HLS video support */
dnssd_set_airplay_features(dnssd, 0, (int) hls_support);
dnssd_set_airplay_features(dnssd, 4, (int) hls_support);
// not sure about this one (bit 8, screen rotation supported):
//dnssd_set_airplay_features(dnssd, 8, (int) hls_support);
/* needed for h265 video support */
dnssd_set_airplay_features(dnssd, 42, (int) h265_support);
/* bit 27 of Features determines whether the AirPlay2 client-pairing protocol will be used (1) or not (0) */
dnssd_set_airplay_features(dnssd, 27, (int) setup_legacy_pairing);
return 0;
}
static bool check_client(char *deviceid) {
bool ret = false;
int list = allowed_clients.size();
for (int i = 0; i < list ; i++) {
if (!strcmp(deviceid,allowed_clients[i].c_str())) {
ret = true;
break;
}
}
return ret;
}
static bool check_blocked_client(char *deviceid) {
bool ret = false;
int list = blocked_clients.size();
for (int i = 0; i < list ; i++) {
if (!strcmp(deviceid,blocked_clients[i].c_str())) {
ret = true;
break;
}
}
return ret;
}
// Server callbacks
extern "C" void video_reset(void *cls) {
LOGD("video_reset");
url.erase();
reset_loop = true;
remote_clock_offset = 0;
relaunch_video = true;
}
extern "C" void video_set_codec(void *cls, video_codec_t codec) {
if (use_video) {
bool video_is_h265 = (codec == VIDEO_CODEC_H265);
video_renderer_choose_codec(video_is_h265);
}
}
extern "C" void display_pin(void *cls, char *pin) {
int margin = 10;
int spacing = 3;
char *image = create_pin_display(pin, margin, spacing);
if (!image) {
LOGE("create_pin_display could not create pin image, pin = %s", pin);
} else {
LOGI("%s\n",image);
free (image);
}
}
extern "C" void export_dacp(void *cls, const char *active_remote, const char *dacp_id) {
if (dacpfile.length()) {
FILE *fp = fopen(dacpfile.c_str(), "w");
if (fp) {
fprintf(fp,"%s\n%s\n", dacp_id, active_remote);
fclose(fp);
} else {
LOGE("failed to open DACP export file \"%s\"", dacpfile.c_str());
}
}
}
extern "C" void conn_init (void *cls) {
open_connections++;
LOGD("Open connections: %i", open_connections);
//video_renderer_update_background(1);
}
extern "C" void conn_destroy (void *cls) {
//video_renderer_update_background(-1);
open_connections--;
LOGD("Open connections: %i", open_connections);
if (open_connections == 0) {
remote_clock_offset = 0;
if (use_audio) {
audio_renderer_stop();
}
if (dacpfile.length()) {
remove (dacpfile.c_str());
}
}
}
extern "C" void conn_reset (void *cls, int timeouts, bool reset_video) {
LOGI("***ERROR lost connection with client (network problem?)");
if (timeouts) {
LOGI(" Client no-response limit of %d timeouts (%d seconds) reached:", timeouts, 3*timeouts);
LOGI(" Sometimes the network connection may recover after a longer delay:\n"
" the default timeout limit n = %d can be changed with the \"-reset n\" option", NTP_TIMEOUT_LIMIT);
}
if (!nofreeze) {
close_window = reset_video; /* leave "frozen" window open if reset_video is false */
}
raop_stop(raop);
reset_loop = true;
}
extern "C" void conn_teardown(void *cls, bool *teardown_96, bool *teardown_110) {
if (*teardown_110 && close_window) {
reset_loop = true;
}
}
extern "C" void report_client_request(void *cls, char *deviceid, char * model, char *name, bool * admit) {
LOGI("connection request from %s (%s) with deviceID = %s\n", name, model, deviceid);
if (restrict_clients) {
*admit = check_client(deviceid);
if (*admit == false) {
LOGI("client connections have been restricted to those with listed deviceID,\nuse \"-allow %s\" to allow this client to connect.\n",
deviceid);
}
} else {
*admit = true;
}
if (check_blocked_client(deviceid)) {
*admit = false;
LOGI("*** attempt to connect by blocked client (clientID %s): DENIED\n", deviceid);
}
}
extern "C" void audio_process (void *cls, raop_ntp_t *ntp, audio_decode_struct *data) {
if (dump_audio) {
dump_audio_to_file(data->data, data->data_len, (data->data)[0] & 0xf0);
}
if (use_audio) {
if (!remote_clock_offset) {
remote_clock_offset = data->ntp_time_local - data->ntp_time_remote;
}
data->ntp_time_remote = data->ntp_time_remote + remote_clock_offset;
switch (data->ct) {
case 2:
if (audio_delay_alac) {
data->ntp_time_remote = (uint64_t) ((int64_t) data->ntp_time_remote + audio_delay_alac);
}
break;
case 4:
case 8:
if (audio_delay_aac) {
data->ntp_time_remote = (uint64_t) ((int64_t) data->ntp_time_remote + audio_delay_aac);
}
break;
default:
break;
}
audio_renderer_render_buffer(data->data, &(data->data_len), &(data->seqnum), &(data->ntp_time_remote));
}
}
extern "C" void video_process (void *cls, raop_ntp_t *ntp, video_decode_struct *data) {
if (dump_video) {
dump_video_to_file(data->data, data->data_len);
}
if (use_video) {
if (!remote_clock_offset) {
remote_clock_offset = data->ntp_time_local - data->ntp_time_remote;
}
data->ntp_time_remote = data->ntp_time_remote + remote_clock_offset;
video_renderer_render_buffer(data->data, &(data->data_len), &(data->nal_count), &(data->ntp_time_remote));
}
}
extern "C" void video_pause (void *cls) {
if (use_video) {
video_renderer_pause();
}
}
extern "C" void video_resume (void *cls) {
if (use_video) {
video_renderer_resume();
}
}
extern "C" void audio_flush (void *cls) {
if (use_audio) {
audio_renderer_flush();
}
}
extern "C" void video_flush (void *cls) {
if (use_video) {
video_renderer_flush();
}
}
extern "C" void audio_set_volume (void *cls, float volume) {
double db, db_flat, frac, gst_volume;
if (!use_audio) {
return;
}
/* convert from AirPlay dB volume in range {-30dB : 0dB}, to GStreamer volume */
if (volume == -144.0f) { /* AirPlay "mute" signal */
frac = 0.0;
} else if (volume < -30.0f) {
LOGE(" invalid AirPlay volume %f", volume);
frac = 0.0;
} else if (volume > 0.0f) {
LOGE(" invalid AirPlay volume %f", volume);
frac = 1.0;
} else if (volume == -30.0f) {
frac = 0.0;
} else if (volume == 0.0f) {
frac = 1.0;
} else {
frac = (double) ( (30.0f + volume) / 30.0f);
frac = (frac > 1.0) ? 1.0 : frac;
}
/* frac is length of volume slider as fraction of max length */
/* also (steps/16) where steps is number of discrete steps above mute (16 = full volume) */
if (frac == 0.0) {
gst_volume = 0.0;
} else {
/* flat rescaling of decibel range from {-30dB : 0dB} to {db_low : db_high} */
db_flat = db_low + (db_high-db_low) * frac;
if (taper_volume) {
/* taper the volume reduction by the (rescaled) Airplay {-30:0} range so each reduction of
* the remaining slider length by 50% reduces the perceived volume by 50% (-10dB gain)
* (This is the "dasl-tapering" scheme offered by shairport-sync) */
db = db_high + 10.0 * (log10(frac) / log10(2.0));
db = (db > db_flat) ? db : db_flat;
} else {
db = db_flat;
}
/* conversion from (gain) decibels to GStreamer's linear volume scale */
gst_volume = pow(10.0, 0.05*db);
}
audio_renderer_set_volume(gst_volume);
}
extern "C" void audio_get_format (void *cls, unsigned char *ct, unsigned short *spf, bool *usingScreen, bool *isMedia, uint64_t *audioFormat) {
unsigned char type;
LOGI("ct=%d spf=%d usingScreen=%d isMedia=%d audioFormat=0x%lx",*ct, *spf, *usingScreen, *isMedia, (unsigned long) *audioFormat);
switch (*ct) {
case 2:
type = 0x20;
break;
case 8:
type = 0x80;
break;
default:
type = 0x10;
break;
}
if (audio_dumpfile && type != audio_type) {
fclose(audio_dumpfile);
audio_dumpfile = NULL;
}
audio_type = type;
if (use_audio) {
audio_renderer_start(ct);
}
if (coverart_filename.length()) {
write_coverart(coverart_filename.c_str(), (const void *) empty_image, sizeof(empty_image));
}
}
extern "C" void video_report_size(void *cls, float *width_source, float *height_source, float *width, float *height) {
if (use_video) {
video_renderer_size(width_source, height_source, width, height);
}
}
extern "C" void audio_set_coverart(void *cls, const void *buffer, int buflen) {
if (buffer && coverart_filename.length()) {
write_coverart(coverart_filename.c_str(), buffer, buflen);
LOGI("coverart size %d written to %s", buflen, coverart_filename.c_str());
}
}
extern "C" void audio_set_progress(void *cls, unsigned int start, unsigned int curr, unsigned int end) {
int duration = (int) (end - start)/44100;
int position = (int) (curr - start)/44100;
int remain = duration - position;
printf("audio progress (min:sec): %d:%2.2d; remaining: %d:%2.2d; track length %d:%2.2d\n",
position/60, position%60, remain/60, remain%60, duration/60, duration%60);
}
extern "C" void audio_set_metadata(void *cls, const void *buffer, int buflen) {
char dmap_tag[5] = {0x0};
const unsigned char *metadata = (const unsigned char *) buffer;
int datalen;
int count = 0;
printf("==============Audio Metadata=============\n");
if (buflen < 8) {
LOGE("received invalid metadata, length %d < 8", buflen);
return;
} else if (parse_dmap_header(metadata, dmap_tag, &datalen)) {
LOGE("received invalid metadata, tag [%s] datalen %d", dmap_tag, datalen);
return;
}
metadata += 8;
buflen -= 8;
if (strcmp(dmap_tag, "mlit") != 0 || datalen != buflen) {
LOGE("received metadata with tag %s, but is not a DMAP listingitem, or datalen = %d != buflen %d",
dmap_tag, datalen, buflen);
return;
}
while (buflen >= 8) {
count++;
if (parse_dmap_header(metadata, dmap_tag, &datalen)) {
LOGE("received metadata with invalid DMAP header: tag = [%s], datalen = %d", dmap_tag, datalen);
return;
}
metadata += 8;
buflen -= 8;
process_metadata(count, (const char *) dmap_tag, metadata, datalen);
metadata += datalen;
buflen -= datalen;
}
if (buflen != 0) {
LOGE("%d bytes of metadata were not processed", buflen);
}
}
extern "C" void register_client(void *cls, const char *device_id, const char *client_pk, const char *client_name) {
if (!registration_list) {
/* we are not maintaining a list of registered clients */
return;
}
LOGI("registered new client: %s DeviceID = %s PK = \n%s", client_name, device_id, client_pk);
registered_keys.push_back(client_pk);
if (strlen(pairing_register.c_str())) {
FILE *fp = fopen(pairing_register.c_str(), "a");
if (fp) {
fprintf(fp, "%s,%s,%s\n", client_pk, device_id, client_name);
fclose(fp);
}
}
}
extern "C" bool check_register(void *cls, const char *client_pk) {
if (!registration_list) {
/* we are not maintaining a list of registered clients */
return true;
}
LOGD("check returning client's pairing registration");
std::string pk = client_pk;
if (std::find(registered_keys.rbegin(), registered_keys.rend(), pk) != registered_keys.rend()) {
LOGD("registration found: PK=%s", client_pk);
return true;
} else {
LOGE("returning client's pairing registration not found: PK=%s", client_pk);
return false;
}
}
/* control callbacks for video player (unimplemented) */
extern "C" void on_video_play(void *cls, const char* location, const float start_position) {
/* start_position needs to be implemented */
url.erase();
url.append(location);
reset_loop = true;
relaunch_video = true;
preserve_connections = true;
LOGD("********************on_video_play: location = %s***********************", url.c_str());
}
extern "C" void on_video_scrub(void *cls, const float position) {
LOGI("on_video_scrub: position = %7.5f\n", position);
video_renderer_seek(position);
}
extern "C" void on_video_rate(void *cls, const float rate) {
LOGI("on_video_rate = %7.5f\n", rate);
if (rate == 1.0f) {
video_renderer_resume();
} else if (rate == 0.0f) {
video_renderer_pause();
} else {
LOGI("on_video_rate: ignoring unexpected value rate = %f\n", rate);
}
}
extern "C" void on_video_stop(void *cls) {
LOGI("on_video_stop\n");
}
extern "C" void on_video_acquire_playback_info (void *cls, playback_info_t *playback_info) {
int buffering_level;
LOGD("on_video_acquire_playback info\n");
bool still_playing = video_get_playback_info(&playback_info->duration, &playback_info->position,
&playback_info->rate);
LOGD("on_video_acquire_playback info done\n");
if (!still_playing) {
LOGI(" video has finished, %f", playback_info->position);
playback_info->position = -1.0;
playback_info->duration = -1.0;
video_renderer_stop();
}
}
extern "C" void log_callback (void *cls, int level, const char *msg) {
switch (level) {
case LOGGER_DEBUG: {
LOGD("%s", msg);
break;
}
case LOGGER_WARNING: {
LOGW("%s", msg);
break;
}
case LOGGER_INFO: {
LOGI("%s", msg);
break;
}
case LOGGER_ERR: {
LOGE("%s", msg);
break;
}
default:
break;
}
}
static int start_raop_server (unsigned short display[5], unsigned short tcp[3], unsigned short udp[3], bool debug_log) {
raop_callbacks_t raop_cbs;
memset(&raop_cbs, 0, sizeof(raop_cbs));
raop_cbs.conn_init = conn_init;
raop_cbs.conn_destroy = conn_destroy;
raop_cbs.conn_reset = conn_reset;
raop_cbs.conn_teardown = conn_teardown;
raop_cbs.audio_process = audio_process;
raop_cbs.video_process = video_process;
raop_cbs.audio_flush = audio_flush;
raop_cbs.video_flush = video_flush;
raop_cbs.video_pause = video_pause;
raop_cbs.video_resume = video_resume;
raop_cbs.audio_set_volume = audio_set_volume;
raop_cbs.audio_get_format = audio_get_format;
raop_cbs.video_report_size = video_report_size;
raop_cbs.audio_set_metadata = audio_set_metadata;
raop_cbs.audio_set_coverart = audio_set_coverart;
raop_cbs.audio_set_progress = audio_set_progress;
raop_cbs.report_client_request = report_client_request;
raop_cbs.display_pin = display_pin;
raop_cbs.register_client = register_client;
raop_cbs.check_register = check_register;
raop_cbs.export_dacp = export_dacp;
raop_cbs.video_reset = video_reset;
raop_cbs.video_set_codec = video_set_codec;
raop_cbs.on_video_play = on_video_play;
raop_cbs.on_video_scrub = on_video_scrub;
raop_cbs.on_video_rate = on_video_rate;
raop_cbs.on_video_stop = on_video_stop;
raop_cbs.on_video_acquire_playback_info = on_video_acquire_playback_info;
raop = raop_init(&raop_cbs);
if (raop == NULL) {
LOGE("Error initializing raop!");
return -1;
}
raop_set_log_callback(raop, log_callback, NULL);
raop_set_log_level(raop, log_level);
/* set nohold = 1 to allow capture by new client */
if (raop_init2(raop, nohold, mac_address.c_str(), keyfile.c_str())){
LOGE("Error initializing raop (2)!");
free (raop);
return -1;
}
/* write desired display pixel width, pixel height, refresh_rate, max_fps, overscanned. */
/* use 0 for default values 1920,1080,60,30,0; these are sent to the Airplay client */
if (display[0]) raop_set_plist(raop, "width", (int) display[0]);
if (display[1]) raop_set_plist(raop, "height", (int) display[1]);
if (display[2]) raop_set_plist(raop, "refreshRate", (int) display[2]);
if (display[3]) raop_set_plist(raop, "maxFPS", (int) display[3]);
if (display[4]) raop_set_plist(raop, "overscanned", (int) display[4]);
if (show_client_FPS_data) raop_set_plist(raop, "clientFPSdata", 1);
raop_set_plist(raop, "max_ntp_timeouts", max_ntp_timeouts);
if (audiodelay >= 0) raop_set_plist(raop, "audio_delay_micros", audiodelay);
if (require_password) raop_set_plist(raop, "pin", (int) pin);
if (hls_support) raop_set_plist(raop, "hls", 1);
/* network port selection (ports listed as "0" will be dynamically assigned) */
raop_set_tcp_ports(raop, tcp);
raop_set_udp_ports(raop, udp);
raop_port = raop_get_port(raop);
raop_start(raop, &raop_port);
raop_set_port(raop, raop_port);
/* use raop_port for airplay_port (instead of tcp[2]) */
airplay_port = raop_port;
if (dnssd) {
raop_set_dnssd(raop, dnssd);
} else {
LOGE("raop_set failed to set dnssd");
return -2;
}
return 0;
}
static void stop_raop_server () {
if (raop) {
raop_destroy(raop);
raop = NULL;
}
return;
}
static void read_config_file(const char * filename, const char * uxplay_name) {
std::string config_file = filename;
std::string option_char = "-";
std::vector<std::string> options;
options.push_back(uxplay_name);
std::ifstream file(config_file);
if (file.is_open()) {
fprintf(stdout,"UxPlay: reading configuration from %s\n", config_file.c_str());
std::string line;
while (std::getline(file, line)) {
if (line[0] == '#') continue;
// first process line into separate option items with '\0' as delimiter
bool is_part_of_item, in_quotes;
char endchar;
is_part_of_item = false;
for (int i = 0; i < (int) line.size(); i++) {
if (is_part_of_item == false) {
if (line[i] == ' ') {
line[i] = '\0';
} else {
// start of new item
is_part_of_item = true;
switch (line[i]) {
case '\'':
case '\"':
endchar = line[i];
line[i] = '\0';
in_quotes = true;
break;
default:
in_quotes = false;
endchar = ' ';
break;
}
}
} else {
/* previous character was inside this item */
if (line[i] == endchar) {
if (in_quotes) {
/* cases where endchar is inside quoted item */
if (i > 0 && line[i - 1] == '\\') continue;
if (i + 1 < (int) line.size() && line[i + 1] != ' ') continue;
}
line[i] = '\0';
is_part_of_item = false;
}
}
}
// now tokenize the processed line
std::istringstream iss(line);
std::string token;
bool first = true;
while (std::getline(iss, token, '\0')) {
if (token.size() > 0) {
if (first) {
options.push_back(option_char + token.c_str());
first = false;
} else {
options.push_back(token.c_str());
}
}
}
}
file.close();
} else {
fprintf(stderr,"UxPlay: failed to open configuration file at %s\n", config_file.c_str());
}
if (options.size() > 1) {
int argc = options.size();
char **argv = (char **) malloc(sizeof(char*) * argc);
for (int i = 0; i < argc; i++) {
argv[i] = (char *) options[i].c_str();
}
parse_arguments (argc, argv);
free (argv);
}
}
#ifdef GST_MACOS
/* workaround for GStreamer >= 1.22 "Official Builds" on macOS */
#include <TargetConditionals.h>
#include <gst/gstmacos.h>
void real_main (int argc, char *argv[]);
int main (int argc, char *argv[]) {
LOGI("*=== Using gst_macos_main wrapper for GStreamer >= 1.22 on macOS ===*");
return gst_macos_main ((GstMainFunc) real_main, argc, argv , NULL);
}
void real_main (int argc, char *argv[]) {
#else
int main (int argc, char *argv[]) {
#endif
std::vector<char> server_hw_addr;
std::string config_file = "";
#ifdef SUPPRESS_AVAHI_COMPAT_WARNING
// suppress avahi_compat nag message. avahi emits a "nag" warning (once)
// if getenv("AVAHI_COMPAT_NOWARN") returns null.
static char avahi_compat_nowarn[] = "AVAHI_COMPAT_NOWARN=1";
if (!getenv("AVAHI_COMPAT_NOWARN")) putenv(avahi_compat_nowarn);
#endif
config_file = find_uxplay_config_file();
if (config_file.length()) {
read_config_file(config_file.c_str(), argv[0]);
}
parse_arguments (argc, argv);
log_level = (debug_log ? LOGGER_DEBUG : LOGGER_INFO);
#ifdef _WIN32 /* use utf-8 terminal output; don't buffer stdout in WIN32 when debug_log = false */
SetConsoleOutputCP(CP_UTF8);
if (!debug_log) {
setbuf(stdout, NULL);
}
#endif
LOGI("UxPlay %s: An Open-Source AirPlay mirroring and audio-streaming server.", VERSION);
if (audiosink == "0") {
use_audio = false;
dump_audio = false;
}
if (dump_video) {
if (video_dump_limit > 0) {
LOGI("dump video using \"-vdmp %d %s\"", video_dump_limit, video_dumpfile_name.c_str());
} else {
LOGI("dump video using \"-vdmp %s\"", video_dumpfile_name.c_str());
}
}
if (dump_audio) {
if (audio_dump_limit > 0) {
LOGI("dump audio using \"-admp %d %s\"", audio_dump_limit, audio_dumpfile_name.c_str());
} else {
LOGI("dump audio using \"-admp %s\"", audio_dumpfile_name.c_str());
}
}
#if __APPLE__
/* force use of -nc option on macOS */
LOGI("macOS detected: using -nc option as workaround for GStreamer problem");
new_window_closing_behavior = false;
#endif
if (videosink == "0") {
use_video = false;
videosink.erase();
videosink.append("fakesink");
videosink_options.erase();
LOGI("video_disabled");
display[3] = 1; /* set fps to 1 frame per sec when no video will be shown */
}
if (fullscreen && use_video) {
if (videosink == "waylandsink" || videosink == "vaapisink") {
videosink_options.append(" fullscreen=true");
}
}
if (videosink == "d3d11videosink" && videosink_options.empty() && use_video) {
if (fullscreen) {
videosink_options.append(" fullscreen-toggle-mode=GST_D3D11_WINDOW_FULLSCREEN_TOGGLE_MODE_PROPERTY fullscreen=true ");
} else {
videosink_options.append(" fullscreen-toggle-mode=GST_D3D11_WINDOW_FULLSCREEN_TOGGLE_MODE_ALT_ENTER ");
}
LOGI("d3d11videosink is being used with option fullscreen-toggle-mode=alt-enter\n"
"Use Alt-Enter key combination to toggle into/out of full-screen mode");
}
if (bt709_fix && use_video) {
video_parser.append(" ! ");
video_parser.append(BT709_FIX);
}
if (require_password && registration_list) {
if (pairing_register == "") {
const char * homedir = get_homedir();
if (homedir) {
pairing_register = homedir;
pairing_register.append("/.uxplay.register");
}
}
}
/* read in public keys that were previously registered with pair-setup-pin */
if (require_password && registration_list && strlen(pairing_register.c_str())) {
size_t len = 0;
std::string key;
int clients = 0;
std::ifstream file(pairing_register);
if (file.is_open()) {
std::string line;
while (std::getline(file, line)) {
/*32 bytes pk -> base64 -> strlen(pk64) = 44 chars = line[0:43]; add '\0' at line[44] */
line[44] = '\0';
std::string pk = line.c_str();
registered_keys.push_back(key.assign(pk));
clients ++;
}
if (clients) {
LOGI("Register %s lists %d pin-registered clients", pairing_register.c_str(), clients);
}
file.close();
}
}
if (require_password && keyfile == "0") {
const char * homedir = get_homedir();
if (homedir) {
keyfile.erase();
keyfile = homedir;
keyfile.append("/.uxplay.pem");
} else {
LOGE("could not determine $HOME: public key wiil not be saved, and so will not be persistent");
}
}
if (keyfile != "") {
LOGI("public key storage (for persistence) is in %s", keyfile.c_str());
}
if (do_append_hostname) {
append_hostname(server_name);
}
if (!gstreamer_init()) {
LOGE ("stopping");
exit (1);
}
render_logger = logger_init();
logger_set_callback(render_logger, log_callback, NULL);
logger_set_level(render_logger, log_level);
if (use_audio) {
audio_renderer_init(render_logger, audiosink.c_str(), &audio_sync, &video_sync);
} else {
LOGI("audio_disabled");
}
if (use_video) {
video_renderer_init(render_logger, server_name.c_str(), videoflip, video_parser.c_str(),
video_decoder.c_str(), video_converter.c_str(), videosink.c_str(),
videosink_options.c_str(), fullscreen, video_sync, h265_support, NULL, default_image.c_str());
video_renderer_start();
}
if (udp[0]) {
LOGI("using network ports UDP %d %d %d TCP %d %d %d", udp[0], udp[1], udp[2], tcp[0], tcp[1], tcp[2]);
}
if (!use_random_hw_addr) {
if (strlen(mac_address.c_str()) == 0) {
mac_address = find_mac();
LOGI("using system MAC address %s",mac_address.c_str());
} else {
LOGI("using user-set MAC address %s",mac_address.c_str());
}
}
if (mac_address.empty()) {
srand(time(NULL) * getpid());
mac_address = random_mac();
LOGI("using randomly-generated MAC address %s",mac_address.c_str());
}
parse_hw_addr(mac_address, server_hw_addr);
if (coverart_filename.length()) {
LOGI("any AirPlay audio cover-art will be written to file %s",coverart_filename.c_str());
write_coverart(coverart_filename.c_str(), (const void *) empty_image, sizeof(empty_image));
}
/* set default resolutions for h264 or h265*/
if (!display[0] && !display[1]) {
if (h265_support) {
display[0] = 3840;
display[1] = 2160;
} else {
display[0] = 1920;
display[1] = 1080;
}
}
restart:
if (start_dnssd(server_hw_addr, server_name)) {
goto cleanup;
}
if (start_raop_server(display, tcp, udp, debug_log)) {
stop_dnssd();
goto cleanup;
}
if (register_dnssd()) {
stop_raop_server();
stop_dnssd();
goto cleanup;
}
reconnect:
compression_type = 0;
close_window = new_window_closing_behavior;
main_loop();
if (relaunch_video || reset_loop) {
if(reset_loop) {
reset_loop = false;
} else {
raop_stop(raop);
}
if (use_audio) audio_renderer_stop();
if (use_video && (close_window || preserve_connections)) {
video_renderer_destroy();
if (!preserve_connections) {
raop_destroy_airplay_video(raop);
url.erase();
raop_remove_known_connections(raop);
}
preserve_connections = false;
const char *uri = (url.empty() ? NULL : url.c_str());
video_renderer_init(render_logger, server_name.c_str(), videoflip, video_parser.c_str(),
video_decoder.c_str(), video_converter.c_str(), videosink.c_str(),
videosink_options.c_str(), fullscreen, video_sync, h265_support, uri);
video_renderer_start();
}
if (relaunch_video) {
unsigned short port = raop_get_port(raop);
raop_start(raop, &port);
raop_set_port(raop, port);
goto reconnect;
} else {
LOGI("Re-launching RAOP server...");
stop_raop_server();
stop_dnssd();
goto restart;
}
} else {
LOGI("Stopping...");
stop_raop_server();
stop_dnssd();
}
cleanup:
if (use_audio) {
audio_renderer_destroy();
}
if (use_video) {
video_renderer_destroy();
}
logger_destroy(render_logger);
render_logger = NULL;
if(audio_dumpfile) {
fclose(audio_dumpfile);
}
if (video_dumpfile) {
fwrite(mark, 1, sizeof(mark), video_dumpfile);
fclose(video_dumpfile);
}
if (coverart_filename.length()) {
remove (coverart_filename.c_str());
}
}