Unlike an Apple TV, the UxPlay server does not by default require
clients to initially “pair” with it using a pin code displayed by the
@@ -986,10 +1028,11 @@ used.
<videosink> are d3d12videosink,
d3d11videosink, d3dvideosink,
glimagesink, gtksink,
-autovideosink. If you do not specify the videosink, the
-d3d11videosink will be used (users have reported segfaults of the newer
-d3d12 videodecoder on certain older Nvidia cards when the image
-resolution changes: d3d11 will used by default until this is fixed).
+autovideosink. There have been reports of segfaults of
+the newer d3d12 videodecoder on certain older Nvidia cards when the
+image resolution changes, e.g., when the iOS client is rotated between
+portrait and landcape modes: this was a GStreamer issue that is
+apparently now fixed (a workaround is to use d3d11).
- With Direct3D 11.0 or greater, various options can be set using
e.g.
-vs "d3d11videosink <options>" (see the
@@ -1001,6 +1044,22 @@ is added.
The executable uxplay.exe can also be run without the MSYS2
environment, in the Windows Terminal, with
C:\msys64\ucrt64\bin\uxplay.
+There is a new modernized Windows Terminal application available from
+Microsoft that provides various terminals, and can be customized to also
+provide the MSYS2 terminals. See https://www.msys2.org/docs/terminals/
+(to make those instructions clearer: in the dropdown “Settings” menu,
+there is a second “settings” icon in the lower left corner: click on
+that to edit the settings.json file as described).
+The server name (-n option) can be given in internationalized
+UTF-8 encoding: To enter UTF-8 characters in the MSYS2 or other Windows
+terminals, use the numerical keypad with “Num Lock” on: while holding
+down the “Alt” key, type “+” on the keypad, followed by the UTF-8 hex
+code for the character (using the keypad for numbers), then release the
+“Alt” key. (The UTF-8 hex codes have 4 hex digits: for example, the
+“copyright” symbol has hex code 00a9.) This method must be activated in
+the Windows Registry: using regedit, find the Registry section
+’HKEY_Current_User/Control Panel/Input Method”, and add a new Key
+“EnableHexNumpad” with value “1”, then reboot the computer.
Usage
Options:
@@ -1018,7 +1077,8 @@ startup file location: this overrides $UXPLAYRC,
server_name@_hostname_ will be the name that appears offering AirPlay
services to your iPad, iPhone etc, where hostname is the name
of the server running uxplay. This will also now be the name shown above
-the mirror display (X11) window.
+the mirror display (X11) window. Internationalized server names
+encoded as UTF-8 are accepted.
-nh Do not append “@_hostname_” at the end of the AirPlay
server name.
@@ -1042,6 +1102,16 @@ default: 3) allows selection of the version of GStreamer’s "playbin"
video player to use for playing HLS video. (Playbin v3 is the
recommended player, but if some videos fail to play, you can try with
version 2.)
+-scrsv n. (since 1.73) (So far, only implemented on
+Linux/*BSD systems using D-Bus). Inhibit the screensaver in the absence
+of keyboard input (e.g., while watching video), using the
+org.freedesktop.ScreenSaver D-Bus service: n = 0: (off) n= 1 (on during
+video activity) n=2 (always on). Note: to verify this feature is
+working, you can use dbus-monitor to view events on the
+D-Bus; depending on the Desktop Environment, commands like
+gnome-session-inhibit -l,
+xfce4-screensaver-commannd -q, etc., should list UxPlay
+when it is inhibiting the screensaver.
-pin [nnnn]: (since v1.67) use Apple-style
(one-time) “pin” authentication when a new client connects for the first
time: a four-digit pin code is displayed on the terminal, and the client
@@ -1069,16 +1139,23 @@ options -restrict, -block, -allow for more ways to control client
access). (Add a line “reg” in the startup file if you wish to use
this feature.)
-pw [pwd]. (since 1.72). As an alternative
-to -pin, client access can be controlled with a password set when uxplay
-starts (set it in the .uxplay startup file, where it is stored as
-cleartext.) All users must then know this password. This uses HTTP md5
-Digest authentication, which is now regarded as providing weak security,
-but it is only used to validate the uxplay password, and no user
-credentials are exposed. If pwd is not
-specified, a random 4-digit pin code is displayed, and must be entered
-on the client at each new connection. Note: -pin
-and -pw are alternatives: if both are specified at startup, the earlier
-of these two options is discarded.
+to -pin, client access can be controlled with a password. If a password
+pwd (of length at least six characters) is set when uxplay
+starts (usually set in the startup file, where it is stored as
+cleartext), all users must know this password to connect to UxPlay (the
+client prompts for it). This method uses HTTP md5 Digest authentication,
+which is now regarded as providing weak security, but it is only used to
+validate the uxplay password, and no user credentials are exposed. After
+a successful authentication, the client stores the password, and will
+use it initially for future authentications without prompting, so long
+as the UxPlay deviceID has not changed (this initial authentication will
+fail if the UxPlay password has changed). If pwd is
+not specified with the -pw option when UxPlay starts, a
+new random 4-digit pin code is generated and displayed on the UxPlay
+terminal for each new connection, and must be entered
+on the client (there are three chances to enter it, before it is
+changed). Note: -pin and -pw are alternatives: if both are specified
+at startup, the earlier of these two options is discarded.
-vsync [x] (In Mirror mode:) this option
(now the default) uses timestamps to synchronize audio
with video on the server, with an optional audio delay in (decimal)
@@ -1200,6 +1277,12 @@ display video), and only used to render audio, which will be AAC
lossily-compressed audio in mirror mode with unrendered video, and
superior-quality ALAC Apple Lossless audio in Airplay audio-only
mode.
+-vrtp pipeline: forward rtp packets of
+decrypted video to somewhere else, without rendering. Uses rtph264pay or
+rtph265pay as appropriate: pipeline should start with any
+rtph26xpay options (such as config_interval= or aggregate-mode =),
+followed by a sending method: e.g.,
+"config-interval=1 ! udpsink host=127.0.0.1 port=5000“.
-v4l2 Video settings for hardware h264 video
decoding in the GPU by Video4Linux2. Equivalent to
-vd v4l2h264dec -vc v4l2convert.
@@ -1374,6 +1457,13 @@ to a file to n or less. To change the name audiodump,
use -admp [n] filename. Note that (unlike dumped video) the
dumped audio is currently only useful for debugging, as it is not
containerized to make it playable with standard audio players.
+-ble [filename]. Enable Bluetooth beacon
+Service Discovery. The port, PID and process name of the UxPlay process
+is recorded by default in ~/.uxplay.ble : (this file is
+created when UxPlay starts and deleted when it stops.) Optionally the
+file filename, which must be the full path to a writeable file
+can instead be used. See below for beacon setup
+instructions.
-d [n] Enable debug output; optional argument n=1
suppresses audio/video packet data in debug output. Note: this does not
show GStreamer error or debug messages. To see GStreamer error and
@@ -1381,6 +1471,94 @@ warning messages, set the environment variable GST_DEBUG with “export
GST_DEBUG=2” before running uxplay. To see GStreamer information
messages, set GST_DEBUG=4; for DEBUG messages, GST_DEBUG=5; increase
this to see even more of the GStreamer inner workings.
+Bluetooth LE beacon setup
+The python>=3.6 script for running a Bluetooth-LE Service
+Discovery beacon is uxplay-beacon.py. It comes in two versions, one (for
+Linux and *BSD) is only installed on systems which support DBUS, and
+another only for Windows 10/11. Bluetooth >= 4.0 hardware on the host
+computer is required: a cheap USB bluetooth dongle can be used.
+On Linux/*BSD, Bluetooth support (BlueZ) must be installed (on
+Debian-based systems: sudo apt install bluez bluez-tools;
+recent Ubuntu releases provide bluez as a snap package). In addition to
+standard Python3 libraries, you may need to install the gi, dbus, and
+psutil Python libraries used by uxplay-beacon.py. On Debian-based
+systems:
+
sudo apt install python3-gi python3-dbus python3-psutil
+For Windows support on MSYS2 UCRT systems, use pacman -S to install
+mingw-w64-ucrt-x86_64-python,
+*-python-gobject, *-python-psutil, and
+*-python-pip. Then install winrt bindings
+pip install winrt-Windows.Foundation.Collections,
+winrt-Windows.Devices.Bluetooth.Advertisement and
+winrt-Windows.Storage.Streams.
+If uxplay will be run with option “uxplay -ble” (so it
+writes data for the Bluetooth beacon in the default BLE data file
+~/.uxplay.ble), just run uxplay-beacon.py in a
+separate terminal. The python script will start Bluetooth LE
+Service-Discovery advertising when it detects that UxPlay is running by
+checking if the BLE data file exists, and stop when it no longer detects
+a running UxPlay plus this file (it will restart advertising if UxPlay
+later reappears). The script will remain active until stopped with
+Ctrl+C in its terminal window (or its terminal window is closed).
+The beacon script can be more finely controlled using certain
+options: these can be given on the command line, or read from a
+configuration file ~/.uxplay.beacon, if it exists.
+Configuration file entries are like the command line forms, one per line
+(e.g., --ipv4 192.168.1.100). Lines commented out with an
+initial # are ignored. Command line options override the
+configuration file options. Get help with man uxplay-beacon
+or uxplay-beacon.py --help. Options are
+
+--file <config file> read beacon options from
+<config file> instead of
+~/.uxplay.beacon.
+--ipv4 <ipv4 address>. This option can be
+used to specify the ipv4 address at which the UxPlay server should be
+contacted by the client. If it is not given, an address will be obtained
+automatically using gethostbyname. Only ipv4 addresses are
+supported.
+--path <BLE data file>. This overrides the
+default choice of BLE data file (~/.uxplay.ble) that is
+monitored by the beacon script. This also requires that uxplay is run
+with option “uxplay -ble <BLE data file>”.
+
+The BlueZ/Dbus version has thee more options not offered by the
+Windows version:
+
+--AdvMin x, --AdvMax y. These controls
+the interval between BLE advertisement broadcasts. This interval is in
+the range [x, y], given in units of msecs. Allowed ranges are 100 <=
+x <= y <= 10240. If AdvMin=AdvMax, the interval is fixed: if
+AdvMin < AdvMax it is chosen flexibly in this range to avoid
+interfering with other tasks the Bluetooth device is carrying out. The
+default values are AdvMin = AdvMax = 100. The advertisement is broadcast
+on all three Bluetooth LE advertising channels: 37,38,39.
+--index x (default x = 0, x >= 0). This should be
+used to distinguish between multiple simultaneous instances of
+uxplay-beacon.py that are running to support multiple instances of
+UxPlay. Each instance must have its own BLE Data file (just as each
+instance of UxPlay must also have its own MAC address and ports).
+Note: running multiple beacons simultaneously on the same host has
+not been tested.
+
+If you wish to test Bluetooth LE Service Discovery on Linux/*BSD, you
+can disable DNS_SD Service discovery by the avahi-daemon with
+$ sudo systemctl mask avahi-daemon.socket
+$ sudo systemctl stop avahi-daemon
+To restore DNS_SD Service discovery, replace “mask” by “unmask”, and
+“stop” by “start”.
+On Windows, the Bonjour Service is controlled using Services
+Management: press “Windows + R” to open the Run dialog, run
+services.msc, and click on Bonjour Service
+in the alphabetic list. This will show links for it to be stopped and
+restarted.
+For more information, see the wiki
+page
+
+- Note that Bluetooth LE AirPlay Service Discovery only
+supports broadcast of IPv4 addresses.
+
Troubleshooting
Note: uxplay is run from a terminal command line, and
informational messages are written to the terminal.
@@ -1710,8 +1888,12 @@ an AppleTV6,2 with sourceVersion 380.20.1 (an AppleTV 4K 1st gen,
introduced 2017, running tvOS 12.2.1), so it does not seem to matter
what version UxPlay claims to be.
Changelog
-xxxx 2025-07-07 Render Audio cover-art inside UxPlay with -ca option
-(no file specified).
+xxxx 2025-09-25 Render Audio cover-art inside UxPlay with -ca option
+(no file specified). (D-Bus based) option -scrsv to inhibit
+screensaver while UxPlay is running (Linux/*BSD only). Add support for
+Service Discovery using a Bluetooth LE beacon. Add -vrtp option for
+forwarding decrypted h264/5 video to an external renderer (e.g., OBS
+Studio). Check that option input strings have valid UTF-8 encoding.
1.72.2 2025-07-07 Fix bug (typo) in DNS_SD advertisement introduced
with -pw option. Update llhttp to v 9.3.0
1.72.1 2025-06-06 minor update: fix regression in -reg option; add
diff --git a/README.md b/README.md
index 2821164..b6eff8e 100644
--- a/README.md
+++ b/README.md
@@ -2,6 +2,26 @@
### **Now developed at the GitHub site (where ALL user issues should be posted, and latest versions can be found).**
+- **NEW on github**: Support for **service discovery using a Bluetooth LE "beacon"** for both Linux/\*BSD and Windows (as an alternative to Bonjour/Rendezvous DNS-SD
+ service discovery). The user must set up a Bluetooth LE "beacon", (a USB 4.0 or later "dongle" can be used). See instructions
+ below. The beacon runs independently of UxPlay and regularly broadcasts a Bluetooth LE ("Low Energy") 46 byte packet informing nearby iOS/macOS devices of
+ the local IPv4 network address of the UxPlay server, and which TCP port to contact UxPlay on. Two versions of a Python script (Python >=3.6) "uxplay-beacon.py",
+ (one for Linux/*BSD using BlueZ LEAdvertisingManager1 with DBus, and one for Windows using winrt/BluetoothLEAdvertisementPublisher) are ready for users to
+ run: the appropriate version will be installed when UxPlay is built. They independently run Service-Discovery beacons that iOS devices respond to.
+ Instructions are [given below](#bluetooth-le-beacon-setup).
+
+- **NEW on github**: option `-vrtp ` bypasses rendering by UxPlay, and instead
+ transmits rtp packets of decrypted h264 or h265 video to
+ an external renderer (e.g. OBS Studio) at an address specified in `rest-of-pipeline`.
+ (Note: this is video only, an option "-rtp" which muxes audio and video into a mpeg4 container still needs to be created:
+ Pull Requests welcomed).
+
+- **NEW on github**: (for Linux/*BSD Desktop Environments using D-Bus). New option `-scrsv ` provides screensaver inhibition (e.g., to
+ prevent screensaver function while watching mirrored videos without keyboard or mouse
+ activity): n = 0 (off) n=1 (on during video activity) n=2 (always on while UxPlay is running).
+ Tested on Gnome/KDE/Cinnamon/Mate/Xfce 4: may need adjustment for other Desktop Environments (please report).
+ (watch output of `dbus-monitor` to verify that inhibition is working). _Might not work on Wayland_.
+
- **NEW on github**: option -ca (with no filename given) will now render
Apple Music cover art (in audio-only mode) inside
UxPlay. (-ca `` will continue to export cover art for
@@ -114,6 +134,9 @@ status](https://repology.org/badge/vertical-allrepos/uxplay.svg)](https://repolo
from terminal commands "ps waux \| grep pulse" or "pactl info" will
contain "pipewire" if your Linux/BSD system uses it).*
+- For Linux/*BSD systems using D-Bus, the option `-scrsv 1` inhibits the screensaver while
+ there is video activity on UxPlay (`-scrsv 2` inhibits it whenever UxPlay is running).
+
- For Linux systems using systemd, there is a **systemd** service file **uxplay.service**
found in the UxPlay top directory of the distribution, and also installed
in `/uxplay/systemd/` (where DOCDIR is usually ``/usr/local/share/doc``), that allows users to start
@@ -192,7 +215,10 @@ necessary that the local network also be of the ".local" mDNS-based
type). On Linux and BSD Unix servers, this is usually provided by
[Avahi](https://www.avahi.org), through the avahi-daemon service, and is
included in most Linux distributions (this service can also be provided
-by macOS, iOS or Windows servers).
+by macOS, iOS or Windows servers). There is now an alternative Service
+discovery method, using a Bluetooth LE "beacon" See below
+for [instructions](#bluetooth-le-beacon-setup).
+
Connections to the UxPlay server by iOS/MacOS clients can be initiated
both in **AirPlay Mirror** mode (which streams lossily-compressed AAC
@@ -523,7 +549,9 @@ as comments and ignored.
**Run uxplay in a terminal window**. On some systems, you can specify
fullscreen mode with the `-fs` option, or toggle into and out of
fullscreen mode with F11 or (held-down left Alt)+Enter keys. Use Ctrl-C
-(or close the window) to terminate it when done. If the UxPlay server is
+(or close the window) to terminate it when done.
+
+If the UxPlay server is
not seen by the iOS client's drop-down "Screen Mirroring" panel, check
that your DNS-SD server (usually avahi-daemon) is running: do this in a
terminal window with `systemctl status avahi-daemon`. If this shows the
@@ -538,6 +566,10 @@ opened: **if a firewall is active, also open UDP port 5353 (for mDNS
queries) needed by Avahi**. See [Troubleshooting](#troubleshooting)
below for help with this or other problems.
+Note that there is now an
+alternative Service Discovery method using a Bluetooth LE beacon.
+See the instructions on [Bluetooth beacon setup](#bluetooth-le-beacon-setup).
+
- Unlike an Apple TV, the UxPlay server does not by default require
clients to initially "pair" with it using a pin code displayed by
the server (after which the client "trusts" the server, and does not
@@ -978,10 +1010,11 @@ like `\{0.0.0.00000000\}.\{98e35b2b-8eba-412e-b840-fd2c2492cf44\}`. If
If you wish to specify the videosink using the `-vs ` option,
some choices for `` are `d3d12videosink`, ``d3d11videosink``, ```d3dvideosink```,
-`glimagesink`, ``gtksink``, ```autovideosink```. If you do not specify the videosink,
-the d3d11videosink will be used (users have reported segfaults of the newer d3d12 videodecoder
-on certain older Nvidia cards when the image resolution changes:
-d3d11 will used by default until this is fixed).
+`glimagesink`, ``gtksink``, ```autovideosink```. _There have been reports of
+segfaults of the newer d3d12 videodecoder
+on certain older Nvidia cards when the image resolution changes, e.g., when the iOS client
+is rotated between portrait and landcape modes: this was a GStreamer issue
+that is apparently now fixed (a workaround is to use d3d11)._
- With Direct3D 11.0 or greater, various options can be set
using e.g. `-vs "d3d11videosink "` (see the gstreamer videosink
@@ -992,6 +1025,22 @@ d3d11 will used by default until this is fixed).
The executable uxplay.exe can also be run without the MSYS2 environment,
in the Windows Terminal, with `C:\msys64\ucrt64\bin\uxplay`.
+There is a new modernized Windows Terminal application available from Microsoft that
+provides various terminals, and can be customized to also provide the MSYS2 terminals.
+See https://www.msys2.org/docs/terminals/ (to make those instructions clearer:
+in the dropdown "Settings" menu, there is a second "settings" icon in the lower left corner:
+click on that to edit the settings.json file as described).
+
+The server name (-n option) can be given in internationalized UTF-8 encoding:
+To enter UTF-8 characters in the MSYS2 or other Windows terminals, use the numerical keypad
+with "Num Lock" on: while holding down the "Alt" key, type "+" on the keypad, followed
+by the UTF-8 hex code for the character (using the keypad for numbers), then release the "Alt" key.
+(The UTF-8 hex codes have 4 hex digits: for example, the "copyright" symbol has hex code 00a9.)
+This method must be activated in the Windows Registry: using
+regedit, find the Registry section 'HKEY_Current_User/Control Panel/Input Method",
+and add a new Key "EnableHexNumpad" with value "1", then reboot the computer.
+
+
# Usage
Options:
@@ -1010,6 +1059,7 @@ overrides `$UXPLAYRC`, ``~/.uxplayrc``, etc.
the name that appears offering AirPlay services to your iPad, iPhone
etc, where *hostname* is the name of the server running uxplay. This
will also now be the name shown above the mirror display (X11) window.
+**Internationalized server names encoded as UTF-8 are accepted.**
**-nh** Do not append "@_hostname_" at the end of the AirPlay server
name.
@@ -1036,6 +1086,18 @@ allows selection of the version of GStreamer's
is the recommended player, but if some videos fail to play, you can try
with version 2.)_
+**-scrsv n**. (since 1.73) (So far, only implemented
+on Linux/*BSD systems using D-Bus). Inhibit the screensaver in the
+absence of keyboard input (e.g., while watching video), using the
+org.freedesktop.ScreenSaver D-Bus service:
+n = 0: (off) n= 1 (on during video activity) n=2 (always on).
+_Note: to verify this feature is working, you can use `dbus-monitor`
+to view events on the D-Bus; depending on the Desktop Environment,
+commands like
+`gnome-session-inhibit -l`, ``xfce4-screensaver-commannd -q``, etc.,
+should list UxPlay when it is inhibiting
+the screensaver._
+
**-pin \[nnnn\]**: (since v1.67) use Apple-style (one-time) "pin"
authentication when a new client connects for the first time: a
four-digit pin code is displayed on the terminal, and the client screen
@@ -1064,13 +1126,23 @@ deregisters the corresponding client (see options -restrict, -block,
the startup file if you wish to use this feature.)*
**-pw** [*pwd*]. (since 1.72). As an alternative to -pin, client access
-can be controlled with a password set when uxplay starts (set it in
-the .uxplay startup file, where it is stored as cleartext.) All users must
-then know this password. This uses HTTP md5 Digest authentication,
+can be controlled with a password. If a password *pwd* (of length at least
+six characters) is set when uxplay
+starts (usually set in the startup file, where it is stored as
+cleartext), all users must know this password to connect to UxPlay
+(the client prompts for it).
+This method uses HTTP md5 Digest authentication,
which is now regarded as providing weak security, but it is only used to
validate the uxplay password, and no user credentials are exposed.
-If *pwd* is **not** specified, a random 4-digit pin code is displayed, and must
-be entered on the client at **each** new connection.
+After a successful authentication, the client stores the password, and will use
+it initially for future authentications without prompting, so long as
+the UxPlay deviceID has not changed (this initial authentication will fail
+if the UxPlay password has changed).
+If *pwd* is **not** specified with the -pw option when UxPlay starts, a
+new random 4-digit pin code is generated and displayed on the UxPlay terminal
+for **each** new connection, and must
+be entered on the client (there are three
+chances to enter it, before it is changed).
_Note: -pin and -pw are alternatives: if both are specified at startup, the
earlier of these two options is discarded._
@@ -1203,6 +1275,11 @@ video), and only used to render audio, which will be AAC
lossily-compressed audio in mirror mode with unrendered video, and
superior-quality ALAC Apple Lossless audio in Airplay audio-only mode.
+**-vrtp *pipeline***: forward rtp packets of decrypted video to somewhere else, without rendering.
+Uses rtph264pay or rtph265pay as appropriate: *pipeline* should start with any
+rtph26xpay options (such as config_interval= or aggregate-mode =), followed by
+a sending method: *e.g.*, `"config-interval=1 ! udpsink host=127.0.0.1 port=5000`".
+
**-v4l2** Video settings for hardware h264 video decoding in the GPU by
Video4Linux2. Equivalent to `-vd v4l2h264dec -vc v4l2convert`.
@@ -1392,6 +1469,15 @@ that (unlike dumped video) the dumped audio is currently only useful for
debugging, as it is not containerized to make it playable with standard
audio players.*
+**-ble [*filename*]**. Enable Bluetooth beacon Service Discovery.
+The port, PID and process name of the UxPlay process is recorded by default in
+`~/.uxplay.ble` : (this file is created
+when UxPlay starts and deleted when it stops.)
+Optionally the file
+*filename*, which must be the full path to a writeable file can instead be used.
+__See below for beacon setup
+instructions.__
+
**-d \[n\]** Enable debug output; optional argument n=1 suppresses audio/video
packet data in debug output.
Note: this does not show GStreamer error or
@@ -1401,8 +1487,80 @@ uxplay. To see GStreamer information messages, set GST_DEBUG=4; for
DEBUG messages, GST_DEBUG=5; increase this to see even more of the
GStreamer inner workings.
-# Troubleshooting
+# Bluetooth LE beacon setup
+The python>=3.6 script for running a Bluetooth-LE Service Discovery beacon is uxplay-beacon.py.
+It comes in two versions, one (for Linux and *BSD) is only installed on systems which
+support DBUS, and another only for Windows 10/11. Bluetooth >= 4.0 hardware on the host computer is required: a cheap USB bluetooth dongle
+can be used.
+
+On Linux/*BSD,
+Bluetooth support (BlueZ) must be installed (on Debian-based systems: `sudo apt install bluez bluez-tools`;
+recent Ubuntu releases provide bluez as a snap package).
+In addition to standard Python3 libraries, you may need to install the gi, dbus, and psutil Python libraries used by
+uxplay-beacon.py. On Debian-based systems:
+
+```
+sudo apt install python3-gi python3-dbus python3-psutil
+```
+
+For Windows support on MSYS2 UCRT systems, use pacman -S to
+install `mingw-w64-ucrt-x86_64-python`, ``*-python-gobject``,
+`*-python-psutil`, and ``*-python-pip``. Then install winrt
+bindings `pip install winrt-Windows.Foundation.Collections`,
+``winrt-Windows.Devices.Bluetooth.Advertisement`` and
+``winrt-Windows.Storage.Streams``.
+
+If uxplay will be run with option "`uxplay -ble`" (so it writes data for the Bluetooth beacon in the default BLE data file
+`~/.uxplay.ble`), just run ``uxplay-beacon.py`` in a separate terminal. The python script will start
+Bluetooth LE Service-Discovery advertising when it detects that UxPlay is running by checking if the BLE data file exists, and stop when it no longer detects
+a running UxPlay plus this file (it will restart advertising if UxPlay later reappears). The script will remain active until stopped with Ctrl+C in its
+terminal window (or its terminal window is closed).
+
+The beacon script can be more finely controlled using certain options: these can be given on the command line, or read from
+a configuration file `~/.uxplay.beacon`, if it exists. Configuration file entries are like the command line forms, one per line (e.g.,
+`--ipv4 192.168.1.100`). Lines commented out with an initial ``#`` are ignored. Command line options override the configuration file
+options. Get help with `man uxplay-beacon` or ``uxplay-beacon.py --help``. Options are
+
+* `--file ` read beacon options from ```` instead of
+`~/.uxplay.beacon`.
+
+* `--ipv4 `. This option can be used to specify the ipv4 address at which the UxPlay server should be contacted by the client. If
+it is not given, an address will be obtained automatically using `gethostbyname`. Only ipv4 addresses are supported.
+
+* `--path `. This overrides the default choice of BLE data file (``~/.uxplay.ble``) that is monitored by the beacon script. This also requires
+that uxplay is run with option "`uxplay -ble `".
+
+The BlueZ/Dbus version has thee more options not offered by the Windows version:
+
+* `--AdvMin x`, ``--AdvMax y``. These controls the interval between BLE advertisement broadcasts. This interval is in the range
+[x, y], given in units of msecs. Allowed ranges are 100 <= x <= y <= 10240. If AdvMin=AdvMax, the interval is fixed: if AdvMin < AdvMax
+it is chosen flexibly in this range to avoid interfering with other tasks the Bluetooth device is carrying out. The default values are
+AdvMin = AdvMax = 100. The advertisement is broadcast on all three Bluetooth LE advertising channels: 37,38,39.
+
+* `--index x` (default x = 0, x >= 0). This should be used to distinguish between multiple simultaneous instances of uxplay-beacon.py that are running to support multiple
+ instances of UxPlay. Each instance must have its own BLE Data file (just as each instance of UxPlay must also have its own MAC address and ports). _Note:
+ running multiple beacons simultaneously on the same host has not been tested._
+
+If you wish to test Bluetooth LE Service Discovery on Linux/*BSD, you can disable DNS_SD Service discovery by the avahi-daemon with
+
+```
+$ sudo systemctl mask avahi-daemon.socket
+$ sudo systemctl stop avahi-daemon
+```
+
+To restore DNS_SD Service discovery, replace "mask" by "unmask", and "stop" by "start".
+
+On Windows, the Bonjour Service is controlled using **Services Management**: press "Windows + R" to open the Run dialog,
+run `services.msc`, and click on **Bonjour Service** in the alphabetic list. This
+will show links for it to be stopped and restarted.
+
+For more information, see the [wiki page](https://github.com/FDH2/UxPlay/wiki/Bluetooth_LE_beacon)
+
+* **Note that Bluetooth LE AirPlay Service Discovery only supports
+broadcast of IPv4 addresses**.
+
+# Troubleshooting
Note: `uxplay` is run from a terminal command line, and informational
messages are written to the terminal.
@@ -1742,8 +1900,12 @@ introduced 2017, running tvOS 12.2.1), so it does not seem to matter
what version UxPlay claims to be.
# Changelog
-xxxx 2025-07-07 Render Audio cover-art inside UxPlay with -ca option (no file
-specified).
+xxxx 2025-09-25 Render Audio cover-art inside UxPlay with -ca option (no file
+specified). (D-Bus based) option -scrsv to inhibit screensaver while UxPlay
+is running (Linux/*BSD only). Add support for Service Discovery using a
+Bluetooth LE beacon. Add -vrtp option for forwarding decrypted h264/5 video
+to an external renderer (e.g., OBS Studio). Check that option input strings
+have valid UTF-8 encoding.
1.72.2 2025-07-07 Fix bug (typo) in DNS_SD advertisement introduced with -pw
option. Update llhttp to v 9.3.0
diff --git a/README.txt b/README.txt
index c2283ef..bdcc108 100644
--- a/README.txt
+++ b/README.txt
@@ -2,6 +2,39 @@
### **Now developed at the GitHub site (where ALL user issues should be posted, and latest versions can be found).**
+- **NEW on github**: Support for **service discovery using a Bluetooth
+ LE "beacon"** for both Linux/\*BSD and Windows (as an alternative to
+ Bonjour/Rendezvous DNS-SD service discovery). The user must set up a
+ Bluetooth LE "beacon", (a USB 4.0 or later "dongle" can be used).
+ See instructions below. The beacon runs independently of UxPlay and
+ regularly broadcasts a Bluetooth LE ("Low Energy") 46 byte packet
+ informing nearby iOS/macOS devices of the local IPv4 network address
+ of the UxPlay server, and which TCP port to contact UxPlay on. Two
+ versions of a Python script (Python \>=3.6) "uxplay-beacon.py", (one
+ for Linux/\*BSD using BlueZ LEAdvertisingManager1 with DBus, and one
+ for Windows using winrt/BluetoothLEAdvertisementPublisher) are ready
+ for users to run: the appropriate version will be installed when
+ UxPlay is built. They independently run Service-Discovery beacons
+ that iOS devices respond to. Instructions are [given
+ below](#bluetooth-le-beacon-setup).
+
+- **NEW on github**: option `-vrtp ` bypasses
+ rendering by UxPlay, and instead transmits rtp packets of decrypted
+ h264 or h265 video to an external renderer (e.g. OBS Studio) at an
+ address specified in `rest-of-pipeline`. (Note: this is video only,
+ an option "-rtp" which muxes audio and video into a mpeg4 container
+ still needs to be created: Pull Requests welcomed).
+
+- **NEW on github**: (for Linux/\*BSD Desktop Environments using
+ D-Bus). New option `-scrsv ` provides screensaver inhibition
+ (e.g., to prevent screensaver function while watching mirrored
+ videos without keyboard or mouse activity): n = 0 (off) n=1 (on
+ during video activity) n=2 (always on while UxPlay is running).
+ Tested on Gnome/KDE/Cinnamon/Mate/Xfce 4: may need adjustment for
+ other Desktop Environments (please report). (watch output of
+ `dbus-monitor` to verify that inhibition is working). *Might not
+ work on Wayland*.
+
- **NEW on github**: option -ca (with no filename given) will now
render Apple Music cover art (in audio-only mode) inside UxPlay.
(-ca `` will continue to export cover art for display by
@@ -121,6 +154,10 @@ status](https://repology.org/badge/vertical-allrepos/uxplay.svg)](https://repolo
commands "ps waux \| grep pulse" or "pactl info" will contain
"pipewire" if your Linux/BSD system uses it).*
+- For Linux/\*BSD systems using D-Bus, the option `-scrsv 1` inhibits
+ the screensaver while there is video activity on UxPlay (`-scrsv 2`
+ inhibits it whenever UxPlay is running).
+
- For Linux systems using systemd, there is a **systemd** service file
**uxplay.service** found in the UxPlay top directory of the
distribution, and also installed in `/uxplay/systemd/`
@@ -205,7 +242,9 @@ necessary that the local network also be of the ".local" mDNS-based
type). On Linux and BSD Unix servers, this is usually provided by
[Avahi](https://www.avahi.org), through the avahi-daemon service, and is
included in most Linux distributions (this service can also be provided
-by macOS, iOS or Windows servers).
+by macOS, iOS or Windows servers). There is now an alternative Service
+discovery method, using a Bluetooth LE "beacon" See below for
+[instructions](#bluetooth-le-beacon-setup).
Connections to the UxPlay server by iOS/MacOS clients can be initiated
both in **AirPlay Mirror** mode (which streams lossily-compressed AAC
@@ -536,11 +575,13 @@ as comments and ignored.
**Run uxplay in a terminal window**. On some systems, you can specify
fullscreen mode with the `-fs` option, or toggle into and out of
fullscreen mode with F11 or (held-down left Alt)+Enter keys. Use Ctrl-C
-(or close the window) to terminate it when done. If the UxPlay server is
-not seen by the iOS client's drop-down "Screen Mirroring" panel, check
-that your DNS-SD server (usually avahi-daemon) is running: do this in a
-terminal window with `systemctl status avahi-daemon`. If this shows the
-avahi-daemon is not running, control it with
+(or close the window) to terminate it when done.
+
+If the UxPlay server is not seen by the iOS client's drop-down "Screen
+Mirroring" panel, check that your DNS-SD server (usually avahi-daemon)
+is running: do this in a terminal window with
+`systemctl status avahi-daemon`. If this shows the avahi-daemon is not
+running, control it with
`sudo systemctl [start,stop,enable,disable] avahi-daemon` (on
non-systemd systems, such as \*BSD, use
`sudo service avahi-daemon [status, start, stop, restart, ...]`). If
@@ -551,6 +592,10 @@ opened: **if a firewall is active, also open UDP port 5353 (for mDNS
queries) needed by Avahi**. See [Troubleshooting](#troubleshooting)
below for help with this or other problems.
+Note that there is now an alternative Service Discovery method using a
+Bluetooth LE beacon. See the instructions on [Bluetooth beacon
+setup](#bluetooth-le-beacon-setup).
+
- Unlike an Apple TV, the UxPlay server does not by default require
clients to initially "pair" with it using a pin code displayed by
the server (after which the client "trusts" the server, and does not
@@ -997,11 +1042,12 @@ like `\{0.0.0.00000000\}.\{98e35b2b-8eba-412e-b840-fd2c2492cf44\}`. If
If you wish to specify the videosink using the `-vs ` option,
some choices for `` are `d3d12videosink`, `d3d11videosink`,
-`d3dvideosink`, `glimagesink`, `gtksink`, `autovideosink`. If you do not
-specify the videosink, the d3d11videosink will be used (users have
-reported segfaults of the newer d3d12 videodecoder on certain older
-Nvidia cards when the image resolution changes: d3d11 will used by
-default until this is fixed).
+`d3dvideosink`, `glimagesink`, `gtksink`, `autovideosink`. *There have
+been reports of segfaults of the newer d3d12 videodecoder on certain
+older Nvidia cards when the image resolution changes, e.g., when the iOS
+client is rotated between portrait and landcape modes: this was a
+GStreamer issue that is apparently now fixed (a workaround is to use
+d3d11).*
- With Direct3D 11.0 or greater, various options can be set using
e.g. `-vs "d3d11videosink "` (see the gstreamer videosink
@@ -1012,6 +1058,24 @@ default until this is fixed).
The executable uxplay.exe can also be run without the MSYS2 environment,
in the Windows Terminal, with `C:\msys64\ucrt64\bin\uxplay`.
+There is a new modernized Windows Terminal application available from
+Microsoft that provides various terminals, and can be customized to also
+provide the MSYS2 terminals. See https://www.msys2.org/docs/terminals/
+(to make those instructions clearer: in the dropdown "Settings" menu,
+there is a second "settings" icon in the lower left corner: click on
+that to edit the settings.json file as described).
+
+The server name (-n ``{=html} option) can be given in
+internationalized UTF-8 encoding: To enter UTF-8 characters in the MSYS2
+or other Windows terminals, use the numerical keypad with "Num Lock" on:
+while holding down the "Alt" key, type "+" on the keypad, followed by
+the UTF-8 hex code for the character (using the keypad for numbers),
+then release the "Alt" key. (The UTF-8 hex codes have 4 hex digits: for
+example, the "copyright" symbol has hex code 00a9.) This method must be
+activated in the Windows Registry: using regedit, find the Registry
+section 'HKEY_Current_User/Control Panel/Input Method", and add a new
+Key "EnableHexNumpad" with value "1", then reboot the computer.
+
# Usage
Options:
@@ -1030,6 +1094,7 @@ this overrides `$UXPLAYRC`, `~/.uxplayrc`, etc.
the name that appears offering AirPlay services to your iPad, iPhone
etc, where *hostname* is the name of the server running uxplay. This
will also now be the name shown above the mirror display (X11) window.
+**Internationalized server names encoded as UTF-8 are accepted.**
**-nh** Do not append "@_hostname_" at the end of the AirPlay server
name.
@@ -1055,6 +1120,16 @@ allows selection of the version of GStreamer's \"playbin\" video player
to use for playing HLS video. *(Playbin v3 is the recommended player,
but if some videos fail to play, you can try with version 2.)*
+**-scrsv n**. (since 1.73) (So far, only implemented on Linux/\*BSD
+systems using D-Bus). Inhibit the screensaver in the absence of keyboard
+input (e.g., while watching video), using the
+org.freedesktop.ScreenSaver D-Bus service: n = 0: (off) n= 1 (on during
+video activity) n=2 (always on). *Note: to verify this feature is
+working, you can use `dbus-monitor` to view events on the D-Bus;
+depending on the Desktop Environment, commands like
+`gnome-session-inhibit -l`, `xfce4-screensaver-commannd -q`, etc.,
+should list UxPlay when it is inhibiting the screensaver.*
+
**-pin \[nnnn\]**: (since v1.67) use Apple-style (one-time) "pin"
authentication when a new client connects for the first time: a
four-digit pin code is displayed on the terminal, and the client screen
@@ -1083,15 +1158,22 @@ deregisters the corresponding client (see options -restrict, -block,
the startup file if you wish to use this feature.)*
**-pw** \[*pwd*\]. (since 1.72). As an alternative to -pin, client
-access can be controlled with a password set when uxplay starts (set it
-in the .uxplay startup file, where it is stored as cleartext.) All users
-must then know this password. This uses HTTP md5 Digest authentication,
-which is now regarded as providing weak security, but it is only used to
-validate the uxplay password, and no user credentials are exposed. If
-*pwd* is **not** specified, a random 4-digit pin code is displayed, and
-must be entered on the client at **each** new connection. *Note: -pin
-and -pw are alternatives: if both are specified at startup, the earlier
-of these two options is discarded.*
+access can be controlled with a password. If a password *pwd* (of length
+at least six characters) is set when uxplay starts (usually set in the
+startup file, where it is stored as cleartext), all users must know this
+password to connect to UxPlay (the client prompts for it). This method
+uses HTTP md5 Digest authentication, which is now regarded as providing
+weak security, but it is only used to validate the uxplay password, and
+no user credentials are exposed. After a successful authentication, the
+client stores the password, and will use it initially for future
+authentications without prompting, so long as the UxPlay deviceID has
+not changed (this initial authentication will fail if the UxPlay
+password has changed). If *pwd* is **not** specified with the -pw option
+when UxPlay starts, a new random 4-digit pin code is generated and
+displayed on the UxPlay terminal for **each** new connection, and must
+be entered on the client (there are three chances to enter it, before it
+is changed). *Note: -pin and -pw are alternatives: if both are specified
+at startup, the earlier of these two options is discarded.*
**-vsync \[x\]** (In Mirror mode:) this option (**now the default**)
uses timestamps to synchronize audio with video on the server, with an
@@ -1222,6 +1304,12 @@ video), and only used to render audio, which will be AAC
lossily-compressed audio in mirror mode with unrendered video, and
superior-quality ALAC Apple Lossless audio in Airplay audio-only mode.
+**-vrtp *pipeline***: forward rtp packets of decrypted video to
+somewhere else, without rendering. Uses rtph264pay or rtph265pay as
+appropriate: *pipeline* should start with any rtph26xpay options (such
+as config_interval= or aggregate-mode =), followed by a sending method:
+*e.g.*, `"config-interval=1 ! udpsink host=127.0.0.1 port=5000`".
+
**-v4l2** Video settings for hardware h264 video decoding in the GPU by
Video4Linux2. Equivalent to `-vd v4l2h264dec -vc v4l2convert`.
@@ -1411,6 +1499,13 @@ that (unlike dumped video) the dumped audio is currently only useful for
debugging, as it is not containerized to make it playable with standard
audio players.*
+**-ble \[*filename*\]**. Enable Bluetooth beacon Service Discovery. The
+port, PID and process name of the UxPlay process is recorded by default
+in `~/.uxplay.ble` : (this file is created when UxPlay starts and
+deleted when it stops.) Optionally the file *filename*, which must be
+the full path to a writeable file can instead be used. **See below for
+beacon setup instructions.**
+
**-d \[n\]** Enable debug output; optional argument n=1 suppresses
audio/video packet data in debug output. Note: this does not show
GStreamer error or debug messages. To see GStreamer error and warning
@@ -1419,6 +1514,103 @@ GST_DEBUG=2" before running uxplay. To see GStreamer information
messages, set GST_DEBUG=4; for DEBUG messages, GST_DEBUG=5; increase
this to see even more of the GStreamer inner workings.
+# Bluetooth LE beacon setup
+
+The python\>=3.6 script for running a Bluetooth-LE Service Discovery
+beacon is uxplay-beacon.py. It comes in two versions, one (for Linux and
+\*BSD) is only installed on systems which support DBUS, and another only
+for Windows 10/11. Bluetooth \>= 4.0 hardware on the host computer is
+required: a cheap USB bluetooth dongle can be used.
+
+On Linux/\*BSD, Bluetooth support (BlueZ) must be installed (on
+Debian-based systems: `sudo apt install bluez bluez-tools`; recent
+Ubuntu releases provide bluez as a snap package). In addition to
+standard Python3 libraries, you may need to install the gi, dbus, and
+psutil Python libraries used by uxplay-beacon.py. On Debian-based
+systems:
+
+ sudo apt install python3-gi python3-dbus python3-psutil
+
+For Windows support on MSYS2 UCRT systems, use pacman -S to install
+`mingw-w64-ucrt-x86_64-python`, `*-python-gobject`, `*-python-psutil`,
+and `*-python-pip`. Then install winrt bindings
+`pip install winrt-Windows.Foundation.Collections`,
+`winrt-Windows.Devices.Bluetooth.Advertisement` and
+`winrt-Windows.Storage.Streams`.
+
+If uxplay will be run with option "`uxplay -ble`" (so it writes data for
+the Bluetooth beacon in the default BLE data file `~/.uxplay.ble`), just
+run `uxplay-beacon.py` in a separate terminal. The python script will
+start Bluetooth LE Service-Discovery advertising when it detects that
+UxPlay is running by checking if the BLE data file exists, and stop when
+it no longer detects a running UxPlay plus this file (it will restart
+advertising if UxPlay later reappears). The script will remain active
+until stopped with Ctrl+C in its terminal window (or its terminal window
+is closed).
+
+The beacon script can be more finely controlled using certain options:
+these can be given on the command line, or read from a configuration
+file `~/.uxplay.beacon`, if it exists. Configuration file entries are
+like the command line forms, one per line (e.g.,
+`--ipv4 192.168.1.100`). Lines commented out with an initial `#` are
+ignored. Command line options override the configuration file options.
+Get help with `man uxplay-beacon` or `uxplay-beacon.py --help`. Options
+are
+
+- `--file ` read beacon options from ``
+ instead of `~/.uxplay.beacon`.
+
+- `--ipv4 `. This option can be used to specify the
+ ipv4 address at which the UxPlay server should be contacted by the
+ client. If it is not given, an address will be obtained
+ automatically using `gethostbyname`. Only ipv4 addresses are
+ supported.
+
+- `--path `. This overrides the default choice of BLE
+ data file (`~/.uxplay.ble`) that is monitored by the beacon script.
+ This also requires that uxplay is run with option
+ "`uxplay -ble `".
+
+The BlueZ/Dbus version has thee more options not offered by the Windows
+version:
+
+- `--AdvMin x`, `--AdvMax y`. These controls the interval between BLE
+ advertisement broadcasts. This interval is in the range \[x, y\],
+ given in units of msecs. Allowed ranges are 100 \<= x \<= y
+ \<= 10240. If AdvMin=AdvMax, the interval is fixed: if AdvMin \<
+ AdvMax it is chosen flexibly in this range to avoid interfering with
+ other tasks the Bluetooth device is carrying out. The default values
+ are AdvMin = AdvMax = 100. The advertisement is broadcast on all
+ three Bluetooth LE advertising channels: 37,38,39.
+
+- `--index x` (default x = 0, x \>= 0). This should be used to
+ distinguish between multiple simultaneous instances of
+ uxplay-beacon.py that are running to support multiple instances of
+ UxPlay. Each instance must have its own BLE Data file (just as each
+ instance of UxPlay must also have its own MAC address and ports).
+ *Note: running multiple beacons simultaneously on the same host has
+ not been tested.*
+
+If you wish to test Bluetooth LE Service Discovery on Linux/\*BSD, you
+can disable DNS_SD Service discovery by the avahi-daemon with
+
+ $ sudo systemctl mask avahi-daemon.socket
+ $ sudo systemctl stop avahi-daemon
+
+To restore DNS_SD Service discovery, replace "mask" by "unmask", and
+"stop" by "start".
+
+On Windows, the Bonjour Service is controlled using **Services
+Management**: press "Windows + R" to open the Run dialog, run
+`services.msc`, and click on **Bonjour Service** in the alphabetic list.
+This will show links for it to be stopped and restarted.
+
+For more information, see the [wiki
+page](https://github.com/FDH2/UxPlay/wiki/Bluetooth_LE_beacon)
+
+- **Note that Bluetooth LE AirPlay Service Discovery only supports
+ broadcast of IPv4 addresses**.
+
# Troubleshooting
Note: `uxplay` is run from a terminal command line, and informational
@@ -1761,8 +1953,12 @@ what version UxPlay claims to be.
# Changelog
-xxxx 2025-07-07 Render Audio cover-art inside UxPlay with -ca option (no
-file specified).
+xxxx 2025-09-25 Render Audio cover-art inside UxPlay with -ca option (no
+file specified). (D-Bus based) option -scrsv ``{=html} to inhibit
+screensaver while UxPlay is running (Linux/\*BSD only). Add support for
+Service Discovery using a Bluetooth LE beacon. Add -vrtp option for
+forwarding decrypted h264/5 video to an external renderer (e.g., OBS
+Studio). Check that option input strings have valid UTF-8 encoding.
1.72.2 2025-07-07 Fix bug (typo) in DNS_SD advertisement introduced with
-pw option. Update llhttp to v 9.3.0
diff --git a/lib/airplay_video.c b/lib/airplay_video.c
index 9b0ccd2..dfcea64 100644
--- a/lib/airplay_video.c
+++ b/lib/airplay_video.c
@@ -58,7 +58,7 @@ int airplay_video_service_init(raop_t *raop, unsigned short http_port,
airplay_video_t *airplay_video = deregister_airplay_video(raop);
if (airplay_video) {
- airplay_video_service_destroy(airplay_video);
+ airplay_video_service_destroy(airplay_video);
}
/* calloc guarantees that the 36-character strings apple_session_id and
@@ -74,11 +74,11 @@ int airplay_video_service_init(raop_t *raop, unsigned short http_port,
snprintf(ptr, 6, "%-5u", http_port);
ptr = strstr(airplay_video->local_uri_prefix, " ");
if (ptr) {
- *ptr = '\0';
+ *ptr = '\0';
}
if (!register_airplay_video(raop, airplay_video)) {
- return -2;
+ return -2;
}
//printf(" %p %p\n", airplay_video, get_airplay_video(raop));
@@ -118,7 +118,6 @@ airplay_video_service_destroy(airplay_video_t *airplay_video)
free (airplay_video->master_playlist);
}
-
free (airplay_video);
}
@@ -146,19 +145,19 @@ const char *get_playback_uuid(airplay_video_t *airplay_video) {
}
void set_uri_prefix(airplay_video_t *airplay_video, char *uri_prefix, int uri_prefix_len) {
- if (airplay_video->uri_prefix) {
- free (airplay_video->uri_prefix);
- }
- airplay_video->uri_prefix = (char *) calloc(uri_prefix_len + 1, sizeof(char));
- memcpy(airplay_video->uri_prefix, uri_prefix, uri_prefix_len);
+ if (airplay_video->uri_prefix) {
+ free (airplay_video->uri_prefix);
+ }
+ airplay_video->uri_prefix = (char *) calloc(uri_prefix_len + 1, sizeof(char));
+ memcpy(airplay_video->uri_prefix, uri_prefix, uri_prefix_len);
}
char *get_uri_prefix(airplay_video_t *airplay_video) {
- return airplay_video->uri_prefix;
+ return airplay_video->uri_prefix;
}
char *get_uri_local_prefix(airplay_video_t *airplay_video) {
- return airplay_video->local_uri_prefix;
+ return airplay_video->local_uri_prefix;
}
char *get_master_uri(airplay_video_t *airplay_video) {
@@ -198,7 +197,7 @@ void destroy_media_data_store(airplay_video_t *airplay_video) {
media_item_t *media_data_store = airplay_video->media_data_store;
if (media_data_store) {
for (int i = 0; i < airplay_video->num_uri ; i ++ ) {
- if (media_data_store[i].uri) {
+ if (media_data_store[i].uri) {
free (media_data_store[i].uri);
}
if (media_data_store[i].playlist) {
@@ -336,7 +335,7 @@ char *adjust_master_playlist (char *fcup_response_data, int fcup_response_datale
while (ptr != NULL) {
counter++;
ptr++;
- ptr = strstr(ptr, uri_prefix);
+ ptr = strstr(ptr, uri_prefix);
}
size_t len = uri_local_prefix_len - uri_prefix_len;
diff --git a/lib/dnssd.c b/lib/dnssd.c
index b66563c..44adac2 100644
--- a/lib/dnssd.c
+++ b/lib/dnssd.c
@@ -310,16 +310,16 @@ dnssd_register_raop(dnssd_t *dnssd, unsigned short port)
case 2:
case 3:
dnssd->TXTRecordSetValue(&dnssd->raop_record, "pw", strlen("true"), "true");
- dnssd->TXTRecordSetValue(&dnssd->raop_record, "sf", 4, "0x84");
- break;
+ dnssd->TXTRecordSetValue(&dnssd->raop_record, "sf", 4, "0x84");
+ break;
case 1:
dnssd->TXTRecordSetValue(&dnssd->raop_record, "pw", strlen("true"), "true");
- dnssd->TXTRecordSetValue(&dnssd->raop_record, "sf", 3, "0x8c");
- break;
+ dnssd->TXTRecordSetValue(&dnssd->raop_record, "sf", 3, "0x8c");
+ break;
default:
dnssd->TXTRecordSetValue(&dnssd->raop_record, "pw", strlen("false"), "false");
- dnssd->TXTRecordSetValue(&dnssd->raop_record, "sf", strlen(RAOP_SF), RAOP_SF);
- break;
+ dnssd->TXTRecordSetValue(&dnssd->raop_record, "sf", strlen(RAOP_SF), RAOP_SF);
+ break;
}
dnssd->TXTRecordSetValue(&dnssd->raop_record, "sr", strlen(RAOP_SR), RAOP_SR);
dnssd->TXTRecordSetValue(&dnssd->raop_record, "ss", strlen(RAOP_SS), RAOP_SS);
@@ -382,8 +382,8 @@ dnssd_register_airplay(dnssd_t *dnssd, unsigned short port)
switch (dnssd->pin_pw) {
case 1: // display onscreen pin
dnssd->TXTRecordSetValue(&dnssd->airplay_record, "pw", strlen("true"), "true");
- dnssd->TXTRecordSetValue(&dnssd->airplay_record, "flags", 3, "0x4");
- break;
+ dnssd->TXTRecordSetValue(&dnssd->airplay_record, "flags", 3, "0x4");
+ break;
case 2: // require password
case 3:
dnssd->TXTRecordSetValue(&dnssd->airplay_record, "pw", strlen("true"), "true");
@@ -412,6 +412,13 @@ dnssd_register_airplay(dnssd_t *dnssd, unsigned short port)
return (int) retval; /* error codes are listed in Apple's dns_sd.h */
}
+const char *
+dnssd_get_raop_txt(dnssd_t *dnssd, int *length)
+{
+ *length = dnssd->TXTRecordGetLength(&dnssd->raop_record);
+ return dnssd->TXTRecordGetBytesPtr(&dnssd->raop_record);
+}
+
const char *
dnssd_get_airplay_txt(dnssd_t *dnssd, int *length)
{
@@ -476,13 +483,13 @@ dnssd_unregister_airplay(dnssd_t *dnssd)
}
uint64_t dnssd_get_airplay_features(dnssd_t *dnssd) {
- uint64_t features = ((uint64_t) dnssd->features2) << 32;
- features += (uint64_t) dnssd->features1;
- return features;
+ uint64_t features = ((uint64_t) dnssd->features2) << 32;
+ features += (uint64_t) dnssd->features1;
+ return features;
}
void dnssd_set_pk(dnssd_t *dnssd, char * pk_str) {
- dnssd->pk = pk_str;
+ dnssd->pk = pk_str;
}
void dnssd_set_airplay_features(dnssd_t *dnssd, int bit, int val) {
@@ -498,8 +505,8 @@ void dnssd_set_airplay_features(dnssd_t *dnssd, int bit, int val) {
features = &(dnssd->features1);
}
if (val) {
- *features = *features | mask;
+ *features = *features | mask;
} else {
- *features = *features & ~mask;
+ *features = *features & ~mask;
}
}
diff --git a/lib/dnssd.h b/lib/dnssd.h
index 968d523..007a83a 100644
--- a/lib/dnssd.h
+++ b/lib/dnssd.h
@@ -43,6 +43,7 @@ DNSSD_API int dnssd_register_airplay(dnssd_t *dnssd, unsigned short port);
DNSSD_API void dnssd_unregister_raop(dnssd_t *dnssd);
DNSSD_API void dnssd_unregister_airplay(dnssd_t *dnssd);
+DNSSD_API const char *dnssd_get_raop_txt(dnssd_t *dnssd, int *length);
DNSSD_API const char *dnssd_get_airplay_txt(dnssd_t *dnssd, int *length);
DNSSD_API const char *dnssd_get_name(dnssd_t *dnssd, int *length);
DNSSD_API const char *dnssd_get_hw_addr(dnssd_t *dnssd, int *length);
diff --git a/lib/http_handlers.h b/lib/http_handlers.h
index 25c83d9..93c65c9 100644
--- a/lib/http_handlers.h
+++ b/lib/http_handlers.h
@@ -124,7 +124,7 @@ http_handler_rate(raop_conn_t *conn, http_request_t *request, http_response_t *r
if (end && end != rate) {
rate_value = value;
logger_log(conn->raop->logger, LOGGER_DEBUG, "http_handler_rate: got rate = %.6f", rate_value);
- }
+ }
}
conn->raop->callbacks.on_video_rate(conn->raop->callbacks.cls, rate_value);
}
@@ -142,7 +142,7 @@ http_handler_stop(raop_conn_t *conn, http_request_t *request, http_response_t *r
static void
http_handler_set_property(raop_conn_t *conn,
http_request_t *request, http_response_t *response,
- char **response_data, int *response_datalen) {
+ char **response_data, int *response_datalen) {
const char *url = http_request_get_url(request);
const char *property = url + strlen("/setProperty?");
@@ -247,12 +247,12 @@ int create_playback_info_plist_xml(playback_info_t *playback_info, char **plist_
plist_t loaded_time_ranges_node = plist_new_array();
time_range_to_plist(playback_info->loadedTimeRanges, playback_info->num_loaded_time_ranges,
- loaded_time_ranges_node);
+ loaded_time_ranges_node);
plist_dict_set_item(res_root_node, "loadedTimeRanges", loaded_time_ranges_node);
plist_t seekable_time_ranges_node = plist_new_array();
time_range_to_plist(playback_info->seekableTimeRanges, playback_info->num_seekable_time_ranges,
- seekable_time_ranges_node);
+ seekable_time_ranges_node);
plist_dict_set_item(res_root_node, "seekableTimeRanges", seekable_time_ranges_node);
int len;
@@ -264,7 +264,6 @@ int create_playback_info_plist_xml(playback_info_t *playback_info, char **plist_
return len;
}
-
/* this handles requests from the Client for "Playback information" while the Media is playing on the
Media Player. (The Server gets this information by monitoring the Media Player). The Client could use
the information to e.g. update the slider it shows with progress to the player (0%-100%).
@@ -287,11 +286,11 @@ http_handler_playback_info(raop_conn_t *conn, http_request_t *request, http_resp
conn->raop->callbacks.on_video_acquire_playback_info(conn->raop->callbacks.cls, &playback_info);
if (playback_info.duration == -1.0) {
/* video has finished, reset */
- logger_log(conn->raop->logger, LOGGER_DEBUG, "playback_info not available (finishing)");
- //httpd_remove_known_connections(conn->raop->httpd);
- http_response_set_disconnect(response,1);
- conn->raop->callbacks.video_reset(conn->raop->callbacks.cls);
- return;
+ logger_log(conn->raop->logger, LOGGER_DEBUG, "playback_info not available (finishing)");
+ //httpd_remove_known_connections(conn->raop->httpd);
+ http_response_set_disconnect(response,1);
+ conn->raop->callbacks.video_reset(conn->raop->callbacks.cls);
+ return;
} else if (playback_info.position == -1.0) {
logger_log(conn->raop->logger, LOGGER_DEBUG, "playback_info not available");
return;
@@ -345,9 +344,8 @@ http_handler_reverse(raop_conn_t *conn, http_request_t *request, http_response_t
if (type_PTTH == 1) {
logger_log(conn->raop->logger, LOGGER_DEBUG, "will use socket %d for %s connections", socket_fd, purpose);
http_response_init(response, "HTTP/1.1", 101, "Switching Protocols");
- http_response_add_header(response, "Connection", "Upgrade");
- http_response_add_header(response, "Upgrade", "PTTH/1.0");
-
+ http_response_add_header(response, "Connection", "Upgrade");
+ http_response_add_header(response, "Upgrade", "PTTH/1.0");
} else {
logger_log(conn->raop->logger, LOGGER_ERR, "multiple TPPH connections (%d) are forbidden", type_PTTH );
}
@@ -368,7 +366,6 @@ http_handler_action(raop_conn_t *conn, http_request_t *request, http_response_t
int request_id = 0;
int fcup_response_statuscode = 0;
bool logger_debug = (logger_get_level(conn->raop->logger) >= LOGGER_DEBUG);
-
const char* session_id = http_request_get_header(request, "X-Apple-Session-ID");
if (!session_id) {
@@ -405,7 +402,7 @@ http_handler_action(raop_conn_t *conn, http_request_t *request, http_response_t
/* determine type of data */
plist_t req_type_node = plist_dict_get_item(req_root_node, "type");
if (!PLIST_IS_STRING(req_type_node)) {
- goto post_action_error;
+ goto post_action_error;
}
plist_t req_params_node = NULL;
@@ -414,9 +411,13 @@ http_handler_action(raop_conn_t *conn, http_request_t *request, http_response_t
char *type = NULL;
plist_get_string_val(req_type_node, &type);
logger_log(conn->raop->logger, LOGGER_DEBUG, "action type is %s", type);
- if (strstr(type, "unhandledURLResponse")) {
+ bool unhandled_url_response = strstr(type, "unhandledURLResponse");
+ bool playlist_remove = strstr(type, "playlistRemove");
+ bool playlist_insert = strstr(type, "playlistInsert");
+ plist_mem_free(type);
+ if (unhandled_url_response) {
goto unhandledURLResponse;
- } else if (strstr(type, "playlistRemove")) {
+ } else if (playlist_remove) {
logger_log(conn->raop->logger, LOGGER_INFO, "unhandled action type playlistRemove (stop playback)");
req_params_node = plist_dict_get_item(req_root_node, "params");
if (!req_params_node || !PLIST_IS_DICT (req_params_node)) {
@@ -431,38 +432,49 @@ http_handler_action(raop_conn_t *conn, http_request_t *request, http_response_t
}
plist_t req_params_item_uuid_node = plist_dict_get_item(req_params_item_node, "uuid");
char* remove_uuid = NULL;
- plist_get_string_val(req_params_item_uuid_node, &remove_uuid);
+ plist_get_string_val(req_params_item_uuid_node, &remove_uuid);
const char *playback_uuid = get_playback_uuid(conn->raop->airplay_video);
- if (strcmp(remove_uuid, playback_uuid)) {
+ if (strcmp(remove_uuid, playback_uuid)) {
logger_log(conn->raop->logger, LOGGER_ERR, "uuid of playlist removal action request did not match current playlist:\n"
" current: %s\n remove: %s", playback_uuid, remove_uuid);
} else {
logger_log(conn->raop->logger, LOGGER_DEBUG, "removal_uuid matches playback_uuid\n");
- }
- free (remove_uuid);
+ }
+ plist_mem_free (remove_uuid);
}
logger_log(conn->raop->logger, LOGGER_ERR, "FIXME: playlist removal not yet implemented");
goto finish;
- } else if (strstr(type, "playlistInsert")) {
+ } else if (playlist_insert) {
logger_log(conn->raop->logger, LOGGER_INFO, "unhandled action type playlistInsert (add new playback)");
printf("\n***************FIXME************************\nPlaylist insertion needs more information for it to be implemented:\n"
"please report following output as an \"Issue\" at http://github.com/FDH2/UxPlay:\n");
char *header_str = NULL;
http_request_get_header_string(request, &header_str);
printf("\n\n%s\n", header_str);
- bool is_plist = (bool) strstr(header_str,"apple-binary-plist");
+ bool data_is_plist = (strstr(header_str,"apple-binary-plist") != NULL);
free(header_str);
- if (is_plist) {
+ if (data_is_plist) {
int request_datalen;
const char *request_data = http_request_get_data(request, &request_datalen);
plist_t req_root_node = NULL;
plist_from_bin(request_data, request_datalen, &req_root_node);
- char * plist_xml;
- uint32_t plist_len;
+ char *plist_xml = NULL;
+ char *stripped_xml = NULL;
+ uint32_t plist_len = 0;
plist_to_xml(req_root_node, &plist_xml, &plist_len);
- plist_xml = utils_strip_data_from_plist_xml(plist_xml);
- printf("%s", plist_xml);
- free(plist_xml);
+ printf("plist_len = %u\n", plist_len);
+ stripped_xml = utils_strip_data_from_plist_xml(plist_xml);
+ printf("%s", stripped_xml ? stripped_xml : plist_xml);
+ if (stripped_xml) {
+ free(stripped_xml);
+ }
+ if (plist_xml) {
+#ifdef PLIST_230
+ plist_mem_free(plist_xml);
+#else
+ plist_to_xml_free(plist_xml);
+#endif
+ }
plist_free(req_root_node);
}
assert(0);
@@ -510,12 +522,14 @@ http_handler_action(raop_conn_t *conn, http_request_t *request, http_response_t
char *fcup_response_url = NULL;
plist_get_string_val(req_params_fcup_response_url_node, &fcup_response_url);
if (!fcup_response_url) {
+ plist_mem_free(fcup_response_url);
goto post_action_error;
}
logger_log(conn->raop->logger, LOGGER_DEBUG, "FCUP_Response_URL = %s", fcup_response_url);
plist_t req_params_fcup_response_data_node = plist_dict_get_item(req_params_node, "FCUP_Response_Data");
if (!PLIST_IS_DATA(req_params_fcup_response_data_node)){
+ plist_mem_free(fcup_response_url);
goto post_action_error;
}
@@ -526,16 +540,17 @@ http_handler_action(raop_conn_t *conn, http_request_t *request, http_response_t
if (!fcup_response_data) {
free (fcup_response_url);
+ plist_mem_free(fcup_response_url);
goto post_action_error;
}
if (logger_debug) {
logger_log(conn->raop->logger, LOGGER_DEBUG, "FCUP_Response datalen = %d", fcup_response_datalen);
- char *data = malloc(fcup_response_datalen + 1);
- memcpy(data, fcup_response_data, fcup_response_datalen);
- data[fcup_response_datalen] = '\0';
- logger_log(conn->raop->logger, LOGGER_DEBUG, "begin FCUP Response data:\n%s\nend FCUP Response data",data);
- free (data);
+ char *data = malloc(fcup_response_datalen + 1);
+ memcpy(data, fcup_response_data, fcup_response_datalen);
+ data[fcup_response_datalen] = '\0';
+ logger_log(conn->raop->logger, LOGGER_DEBUG, "begin FCUP Response data:\n%s\nend FCUP Response data",data);
+ free (data);
}
@@ -543,24 +558,24 @@ http_handler_action(raop_conn_t *conn, http_request_t *request, http_response_t
if (ptr) {
/* this is a master playlist */
char *uri_prefix = get_uri_prefix(conn->raop->airplay_video);
- char ** media_data_store = NULL;
+ char ** media_data_store = NULL;
int num_uri = 0;
char *uri_local_prefix = get_uri_local_prefix(conn->raop->airplay_video);
char *new_master = adjust_master_playlist (fcup_response_data, fcup_response_datalen, uri_prefix, uri_local_prefix);
store_master_playlist(conn->raop->airplay_video, new_master);
create_media_uri_table(uri_prefix, fcup_response_data, fcup_response_datalen, &media_data_store, &num_uri);
- create_media_data_store(conn->raop->airplay_video, media_data_store, num_uri);
- num_uri = get_num_media_uri(conn->raop->airplay_video);
- set_next_media_uri_id(conn->raop->airplay_video, 0);
+ create_media_data_store(conn->raop->airplay_video, media_data_store, num_uri);
+ num_uri = get_num_media_uri(conn->raop->airplay_video);
+ set_next_media_uri_id(conn->raop->airplay_video, 0);
} else {
/* this is a media playlist */
assert(fcup_response_data);
- char *playlist = (char *) calloc(fcup_response_datalen + 1, sizeof(char));
- memcpy(playlist, fcup_response_data, fcup_response_datalen);
+ char *playlist = (char *) calloc(fcup_response_datalen + 1, sizeof(char));
+ memcpy(playlist, fcup_response_data, fcup_response_datalen);
int uri_num = get_next_media_uri_id(conn->raop->airplay_video);
- --uri_num; // (next num is current num + 1)
- store_media_playlist(conn->raop->airplay_video, playlist, uri_num);
+ --uri_num; // (next num is current num + 1)
+ store_media_playlist(conn->raop->airplay_video, playlist, uri_num);
float duration = 0.0f;
int count = analyze_media_playlist(playlist, &duration);
if (count) {
@@ -573,9 +588,7 @@ http_handler_action(raop_conn_t *conn, http_request_t *request, http_response_t
if (fcup_response_data) {
free (fcup_response_data);
}
- if (fcup_response_url) {
- free (fcup_response_url);
- }
+ plist_mem_free(fcup_response_url);
int num_uri = get_num_media_uri(conn->raop->airplay_video);
int uri_num = get_next_media_uri_id(conn->raop->airplay_video);
@@ -583,7 +596,7 @@ http_handler_action(raop_conn_t *conn, http_request_t *request, http_response_t
fcup_request((void *) conn, get_media_uri_by_num(conn->raop->airplay_video, uri_num),
apple_session_id,
get_next_FCUP_RequestID(conn->raop->airplay_video));
- set_next_media_uri_id(conn->raop->airplay_video, ++uri_num);
+ set_next_media_uri_id(conn->raop->airplay_video, ++uri_num);
} else {
char * uri_local_prefix = get_uri_local_prefix(conn->raop->airplay_video);
conn->raop->callbacks.on_video_play(conn->raop->callbacks.cls,
@@ -599,7 +612,7 @@ http_handler_action(raop_conn_t *conn, http_request_t *request, http_response_t
http_response_init(response, "HTTP/1.1", 400, "Bad Request");
if (req_root_node) {
- plist_free(req_root_node);
+ plist_free(req_root_node);
}
}
@@ -643,23 +656,23 @@ http_handler_play(raop_conn_t *conn, http_request_t *request, http_response_t *r
char *header_str = NULL;
http_request_get_header_string(request, &header_str);
logger_log(conn->raop->logger, LOGGER_DEBUG, "request header:\n%s", header_str);
- data_is_binary_plist = (strstr(header_str, "x-apple-binary-plist") != NULL);
- data_is_text = (strstr(header_str, "text/parameters") != NULL);
- data_is_octet = (strstr(header_str, "octet-stream") != NULL);
- free (header_str);
+ data_is_binary_plist = (strstr(header_str, "x-apple-binary-plist") != NULL);
+ data_is_text = (strstr(header_str, "text/parameters") != NULL);
+ data_is_octet = (strstr(header_str, "octet-stream") != NULL);
+ free (header_str);
}
if (!data_is_text && !data_is_octet && !data_is_binary_plist) {
- goto play_error;
+ goto play_error;
}
if (data_is_text) {
logger_log(conn->raop->logger, LOGGER_ERR, "Play request Content is text (unsupported)");
- goto play_error;
+ goto play_error;
}
if (data_is_octet) {
logger_log(conn->raop->logger, LOGGER_ERR, "Play request Content is octet-stream (unsupported)");
- goto play_error;
+ goto play_error;
}
if (data_is_binary_plist) {
@@ -671,9 +684,9 @@ http_handler_play(raop_conn_t *conn, http_request_t *request, http_response_t *r
} else {
char* playback_uuid = NULL;
plist_get_string_val(req_uuid_node, &playback_uuid);
- set_playback_uuid(conn->raop->airplay_video, playback_uuid);
- free (playback_uuid);
- }
+ set_playback_uuid(conn->raop->airplay_video, playback_uuid);
+ plist_mem_free (playback_uuid);
+ }
plist_t req_content_location_node = plist_dict_get_item(req_root_node, "Content-Location");
if (!req_content_location_node) {
@@ -691,32 +704,31 @@ http_handler_play(raop_conn_t *conn, http_request_t *request, http_response_t *r
logger_log(conn->raop->logger, LOGGER_WARNING, "Unsupported HLS streaming format: clientProcName %s not found in supported list: %s",
client_proc_name, supported_hls_proc_names);
}
+ plist_mem_free(client_proc_name);
}
plist_t req_start_position_seconds_node = plist_dict_get_item(req_root_node, "Start-Position-Seconds");
if (!req_start_position_seconds_node) {
logger_log(conn->raop->logger, LOGGER_INFO, "No Start-Position-Seconds in Play request");
- } else {
+ } else {
double start_position = 0.0;
plist_get_real_val(req_start_position_seconds_node, &start_position);
- start_position_seconds = (float) start_position;
+ start_position_seconds = (float) start_position;
}
- set_start_position_seconds(conn->raop->airplay_video, (float) start_position_seconds);
+ set_start_position_seconds(conn->raop->airplay_video, (float) start_position_seconds);
}
char *ptr = strstr(playback_location, "/master.m3u8");
if (!ptr) {
logger_log(conn->raop->logger, LOGGER_ERR, "Content-Location has unsupported form:\n%s\n", playback_location);
- goto play_error;
+ goto play_error;
}
int prefix_len = (int) (ptr - playback_location);
set_uri_prefix(conn->raop->airplay_video, playback_location, prefix_len);
set_next_media_uri_id(conn->raop->airplay_video, 0);
fcup_request((void *) conn, playback_location, apple_session_id, get_next_FCUP_RequestID(conn->raop->airplay_video));
- if (playback_location) {
- free (playback_location);
- }
+ plist_mem_free(playback_location);
if (req_root_node) {
plist_free(req_root_node);
@@ -724,6 +736,7 @@ http_handler_play(raop_conn_t *conn, http_request_t *request, http_response_t *r
return;
play_error:;
+ plist_mem_free(playback_location);
if (req_root_node) {
plist_free(req_root_node);
}
@@ -760,7 +773,7 @@ http_handler_hls(raop_conn_t *conn, http_request_t *request, http_response_t *r
if (!strcmp(url, "/master.m3u8")){
char * master_playlist = get_master_playlist(conn->raop->airplay_video);
- if (master_playlist) {
+ if (master_playlist) {
size_t len = strlen(master_playlist);
char * data = (char *) malloc(len + 1);
memcpy(data, master_playlist, len);
diff --git a/lib/http_request.c b/lib/http_request.c
index 76bece6..d7e800e 100644
--- a/lib/http_request.c
+++ b/lib/http_request.c
@@ -81,11 +81,11 @@ on_header_field(llhttp_t *parser, const char *at, size_t length)
/* Allocate space in the current header string */
if (request->headers[request->headers_index] == NULL) {
- request->headers[request->headers_index] = calloc(1, length+1);
+ request->headers[request->headers_index] = calloc(1, length + 1);
} else {
request->headers[request->headers_index] = realloc(
request->headers[request->headers_index],
- strlen(request->headers[request->headers_index])+length+1
+ strlen(request->headers[request->headers_index]) + length + 1
);
}
assert(request->headers[request->headers_index]);
@@ -106,11 +106,11 @@ on_header_value(llhttp_t *parser, const char *at, size_t length)
/* Allocate space in the current header string */
if (request->headers[request->headers_index] == NULL) {
- request->headers[request->headers_index] = calloc(1, length+1);
+ request->headers[request->headers_index] = calloc(1, length + 1);
} else {
request->headers[request->headers_index] = realloc(
request->headers[request->headers_index],
- strlen(request->headers[request->headers_index])+length+1
+ strlen(request->headers[request->headers_index]) + length + 1
);
}
assert(request->headers[request->headers_index]);
@@ -124,7 +124,7 @@ on_body(llhttp_t *parser, const char *at, size_t length)
{
http_request_t *request = parser->data;
- request->data = realloc(request->data, request->datalen+length);
+ request->data = realloc(request->data, request->datalen + length);
assert(request->data);
memcpy(request->data+request->datalen, at, length);
@@ -172,7 +172,7 @@ http_request_destroy(http_request_t *request)
if (request) {
free(request->url);
- for (i=0; iheaders_size; i++) {
+ for (i = 0; i < request->headers_size; i++) {
free(request->headers[i]);
}
free(request->headers);
@@ -273,7 +273,7 @@ http_request_get_header(http_request_t *request, const char *name)
return NULL;
}
- for (i=0; iheaders_size; i+=2) {
+ for (i = 0; i < request->headers_size; i += 2) {
if (!strcmp(request->headers[i], name)) {
return request->headers[i+1];
}
@@ -305,7 +305,7 @@ http_request_get_header_string(http_request_t *request, char **header_str)
int len = 0;
for (int i = 0; i < request->headers_size; i++) {
len += strlen(request->headers[i]);
- if (i%2 == 0) {
+ if (i % 2 == 0) {
len += 2;
} else {
len++;
@@ -321,12 +321,12 @@ http_request_get_header_string(http_request_t *request, char **header_str)
snprintf(p, n, "%s", request->headers[i]);
n -= hlen;
p += hlen;
- if (i%2 == 0) {
+ if (i % 2 == 0) {
snprintf(p, n, ": ");
n -= 2;
p += 2;
} else {
- snprintf(p, n, "\n");
+ snprintf(p, n, "\n");
n--;
p++;
}
diff --git a/lib/httpd.c b/lib/httpd.c
index 9a4b990..df939d9 100644
--- a/lib/httpd.c
+++ b/lib/httpd.c
@@ -201,23 +201,26 @@ httpd_destroy(httpd_t *httpd)
static void
httpd_remove_connection(httpd_t *httpd, http_connection_t *connection)
{
+ int socket_fd = connection->socket_fd;
+ connection->socket_fd = 0;
if (connection->request) {
http_request_destroy(connection->request);
connection->request = NULL;
}
logger_log(httpd->logger, LOGGER_DEBUG, "removing connection type %s socket %d conn %p", typename[connection->type],
- connection->socket_fd, connection->user_data);
+ socket_fd, connection->user_data);
if (connection->user_data) {
httpd->callbacks.conn_destroy(connection->user_data);
connection->user_data = NULL;
}
- if (connection->socket_fd) {
- shutdown(connection->socket_fd, SHUT_WR);
- int ret = closesocket(connection->socket_fd);
+ if (socket_fd) {
+ shutdown(socket_fd, SHUT_WR);
+ int ret = closesocket(socket_fd);
if (ret == -1) {
logger_log(httpd->logger, LOGGER_ERR, "httpd error in closesocket (close): %d %s", errno, strerror(errno));
+ } else {
+ logger_log(httpd->logger, LOGGER_INFO, "Connection closed on socket %d", socket_fd);
}
- connection->socket_fd = 0;
}
if (connection->connected) {
connection->connected = 0;
@@ -265,6 +268,7 @@ httpd_accept_connection(httpd_t *httpd, int server_fd, int is_ipv6)
struct sockaddr_storage local_saddr;
socklen_t local_saddrlen;
unsigned char *local, *remote;
+ unsigned short port;
unsigned int local_zone_id, remote_zone_id;
int local_len, remote_len;
int ret, fd;
@@ -284,10 +288,16 @@ httpd_accept_connection(httpd_t *httpd, int server_fd, int is_ipv6)
return 0;
}
- logger_log(httpd->logger, LOGGER_INFO, "Accepted %s client on socket %d",
- (is_ipv6 ? "IPv6" : "IPv4"), fd);
- local = netutils_get_address(&local_saddr, &local_len, &local_zone_id);
- remote = netutils_get_address(&remote_saddr, &remote_len, &remote_zone_id);
+ local = netutils_get_address(&local_saddr, &local_len, &local_zone_id, &port);
+ logger_log(httpd->logger, LOGGER_INFO, "Accepted %s client on socket %d, port %u",
+ (is_ipv6 ? "IPv6" : "IPv4"), fd, port);
+ remote = netutils_get_address(&remote_saddr, &remote_len, &remote_zone_id, NULL);
+
+ // is it correct that ipv6 link-local local and remote zone id should be the same, as asserted below?
+ if (local_zone_id != remote_zone_id) {
+ logger_log(httpd->logger, LOGGER_INFO, "ipv6 zone_id mismatch: local_zone_id = %u, remote_zone_id = %u",
+ local_zone_id, remote_zone_id);
+ }
assert (local_zone_id == remote_zone_id);
ret = httpd_add_connection(httpd, fd, local, local_len, remote, remote_len, local_zone_id);
@@ -342,7 +352,7 @@ httpd_thread(void *arg)
struct timeval tv;
int nfds=0;
int ret;
- int new_request;
+ int new_request;
MUTEX_LOCK(httpd->run_mutex);
if (!httpd->running) {
@@ -388,7 +398,9 @@ httpd_thread(void *arg)
/* Timeout happened */
continue;
} else if (ret == -1) {
- logger_log(httpd->logger, LOGGER_ERR, "httpd error in select: %d %s", errno, strerror(errno));
+ int sock_err = SOCKET_GET_ERROR();
+ logger_log(httpd->logger, LOGGER_ERR,
+ "httpd error in select: %d %s", sock_err, SOCKET_ERROR_STRING(sock_err));
break;
}
@@ -430,11 +442,11 @@ httpd_thread(void *arg)
if (connection->type == CONNECTION_TYPE_PTTH) {
http_request_is_reverse(connection->request);
}
- logger_log(httpd->logger, LOGGER_DEBUG, "new request, connection %d, socket %d type %s",
+ logger_log(httpd->logger, LOGGER_DEBUG, "new request, connection %d, socket %d type %s",
i, connection->socket_fd, typename [connection->type]);
} else {
new_request = 0;
- }
+ }
logger_log(httpd->logger, LOGGER_DEBUG, "httpd receiving on socket %d, connection %d",
connection->socket_fd, i);
@@ -442,54 +454,64 @@ httpd_thread(void *arg)
logger_log(httpd->logger, LOGGER_DEBUG,"\nhttpd: current connections:");
for (int i = 0; i < httpd->max_connections; i++) {
http_connection_t *connection = &httpd->connections[i];
- if(!connection->connected) {
+ if (!connection->connected || !connection->socket_fd) {
continue;
}
if (!FD_ISSET(connection->socket_fd, &rfds)) {
logger_log(httpd->logger, LOGGER_DEBUG, "connection %d type %d socket %d conn %p %s", i,
connection->type, connection->socket_fd,
connection->user_data, typename [connection->type]);
- } else {
- logger_log(httpd->logger, LOGGER_DEBUG, "connection %d type %d socket %d conn %p %s ACTIVE CONNECTION",
- i, connection->type, connection->socket_fd, connection->user_data, typename [connection->type]);
+ } else {
+ logger_log(httpd->logger, LOGGER_DEBUG, "connection %d type %d socket %d conn %p %s ACTIVE CONNECTION",
+ i, connection->type, connection->socket_fd, connection->user_data, typename [connection->type]);
}
}
- logger_log(httpd->logger, LOGGER_DEBUG, " ");
- }
+ logger_log(httpd->logger, LOGGER_DEBUG, " ");
+ }
/* reverse-http responses from the client must not be sent to the llhttp parser:
* such messages start with "HTTP/1.1" */
if (new_request) {
int readstart = 0;
new_request = 0;
while (readstart < 8) {
+ if (!connection->socket_fd) {
+ break;
+ }
ret = recv(connection->socket_fd, buffer + readstart, sizeof(buffer) - 1 - readstart, 0);
if (ret == 0) {
- logger_log(httpd->logger, LOGGER_INFO, "Connection closed for socket %d",
+ logger_log(httpd->logger, LOGGER_DEBUG, "client closed connection on socket %d",
connection->socket_fd);
break;
} else if (ret == -1) {
- if (errno == EAGAIN) {
- continue;
- } else {
- int sock_err = SOCKET_GET_ERROR();
- logger_log(httpd->logger, LOGGER_ERR, "httpd: recv socket error %d:%s",
- sock_err, SOCKET_ERROR_STRING(sock_err));
- break;
- }
+ if (errno == EAGAIN) {
+ continue;
+ } else {
+ int sock_err = SOCKET_GET_ERROR();
+ logger_log(httpd->logger, LOGGER_ERR, "httpd: recv error %d on socket %d: %s",
+ sock_err, connection->socket_fd, SOCKET_ERROR_STRING(sock_err));
+ break;
+ }
} else {
readstart += ret;
ret = readstart;
}
}
+ if (!connection->socket_fd) {
+ /* connection was recently removed */
+ continue;
+ }
if (!memcmp(buffer, http, 8)) {
http_request_set_reverse(connection->request);
}
} else {
- ret = recv(connection->socket_fd, buffer, sizeof(buffer) - 1, 0);
- if (ret == 0) {
- logger_log(httpd->logger, LOGGER_INFO, "Connection closed for socket %d",
- connection->socket_fd);
- httpd_remove_connection(httpd, connection);
+ if (connection->socket_fd) {
+ ret = recv(connection->socket_fd, buffer, sizeof(buffer) - 1, 0);
+ if (ret == 0) {
+ httpd_remove_connection(httpd, connection);
+ continue;
+ }
+ } else {
+ /* connection was recently removed */
continue;
}
}
diff --git a/lib/netutils.c b/lib/netutils.c
index 642efba..ed68530 100644
--- a/lib/netutils.c
+++ b/lib/netutils.c
@@ -53,7 +53,7 @@ netutils_cleanup()
}
unsigned char *
-netutils_get_address(void *sockaddr, int *length, unsigned int *zone_id)
+netutils_get_address(void *sockaddr, int *length, unsigned int *zone_id, unsigned short *port)
{
unsigned char ipv4_prefix[] = { 0,0,0,0,0,0,0,0,0,0,255,255 };
struct sockaddr *address = sockaddr;
@@ -66,11 +66,17 @@ netutils_get_address(void *sockaddr, int *length, unsigned int *zone_id)
*zone_id = 0;
sin = (struct sockaddr_in *)address;
*length = sizeof(sin->sin_addr.s_addr);
+ if (port) {
+ *port = ntohs(sin->sin_port);
+ }
return (unsigned char *)&sin->sin_addr.s_addr;
} else if (address->sa_family == AF_INET6) {
struct sockaddr_in6 *sin6;
sin6 = (struct sockaddr_in6 *)address;
+ if (port) {
+ *port = ntohs(sin6->sin6_port);
+ }
if (!memcmp(sin6->sin6_addr.s6_addr, ipv4_prefix, 12)) {
/* Actually an embedded IPv4 address */
*zone_id = 0;
diff --git a/lib/netutils.h b/lib/netutils.h
index e64c638..d6b7542 100644
--- a/lib/netutils.h
+++ b/lib/netutils.h
@@ -19,7 +19,7 @@ int netutils_init();
void netutils_cleanup();
int netutils_init_socket(unsigned short *port, int use_ipv6, int use_udp);
-unsigned char *netutils_get_address(void *sockaddr, int *length, unsigned int *zone_id);
+unsigned char *netutils_get_address(void *sockaddr, int *length, unsigned int *zone_id, unsigned short *port);
int netutils_parse_address(int family, const char *src, void *dst, int dstlen);
#endif
diff --git a/lib/raop.c b/lib/raop.c
index d9939ba..ee0c5e4 100644
--- a/lib/raop.c
+++ b/lib/raop.c
@@ -188,26 +188,35 @@ conn_request(void *ptr, http_request_t *request, http_response_t **response) {
All requests arriving here have been parsed by llhttp to obtain
method | url | protocol (RTSP/1.0 or HTTP/1.1)
- There are three types of connections supplying these requests:
+ There are four types of connections supplying these requests:
Connections from the AirPlay client:
(1) type RAOP connections with CSeq seqence header, and no X-Apple-Session-ID header
(2) type AIRPLAY connection with an X-Apple-Sequence-ID header and no Cseq header
Connections from localhost:
(3) type HLS internal connections from the local HLS server (gstreamer) at localhost with neither
of these headers, but a Host: localhost:[port] header. method = GET.
+ (4) a special RAOP connection trigggered by a Bluetooth LE beacon: Protocol RTSP/1.0, method: GET
+ url /info?txtAirPlay?txtRAOP, and no headers including CSeq
*/
const char *method = http_request_get_method(request);
const char *url = http_request_get_url(request);
+ const char *protocol = http_request_get_protocol(request);
- if (!method || !url) {
+ if (!method || !url || !protocol) {
return;
}
-/* this rejects messages from _airplay._tcp for video streaming protocol unless bool raop->hls_support is true*/
+ /* ¨idenitfy if request is a response to a BLE beaconn */
const char *cseq = http_request_get_header(request, "CSeq");
- const char *protocol = http_request_get_protocol(request);
- if (!cseq && !conn->raop->hls_support) {
+ bool ble = false;
+ if (!strcmp(protocol,"RTSP/1.0") && !cseq && (strstr(url, "txtAirPlay") || strstr(url, "txtRAOP") )) {
+ logger_log(conn->raop->logger, LOGGER_INFO, "response to Bluetooth LE beacon advertisement received)");
+ ble = true;
+ }
+
+ /* this rejects messages from _airplay._tcp for video streaming protocol unless bool raop->hls_support is true*/
+ if (!cseq && !conn->raop->hls_support && !ble) {
logger_log(conn->raop->logger, LOGGER_INFO, "ignoring AirPlay video streaming request (use option -hls to activate HLS support)");
return;
}
@@ -217,7 +226,7 @@ conn_request(void *ptr, http_request_t *request, http_response_t **response) {
hls_request = (host && !cseq && !client_session_id);
if (conn->connection_type == CONNECTION_TYPE_UNKNOWN) {
- if (cseq) {
+ if (cseq || ble) {
if (httpd_count_connection_type(conn->raop->httpd, CONNECTION_TYPE_RAOP)) {
char ipaddr[40];
utils_ipaddress_to_string(conn->remotelen, conn->remote, conn->zone_id, ipaddr, (int) (sizeof(ipaddr)));
@@ -225,8 +234,8 @@ conn_request(void *ptr, http_request_t *request, http_response_t **response) {
logger_log(conn->raop->logger, LOGGER_INFO, "\"nohold\" feature: switch to new connection request from %s", ipaddr);
if (conn->raop->callbacks.video_reset) {
conn->raop->callbacks.video_reset(conn->raop->callbacks.cls);
- }
- httpd_remove_known_connections(conn->raop->httpd);
+ }
+ httpd_remove_known_connections(conn->raop->httpd);
} else {
logger_log(conn->raop->logger, LOGGER_WARNING, "rejecting new connection request from %s", ipaddr);
*response = http_response_create();
@@ -245,7 +254,7 @@ conn_request(void *ptr, http_request_t *request, http_response_t **response) {
conn->client_session_id = (char *) malloc(len);
strncpy(conn->client_session_id, client_session_id, len);
/* airplay video has been requested: shut down any running RAOP udp services */
- raop_conn_t *raop_conn = (raop_conn_t *) httpd_get_connection_by_type(conn->raop->httpd, CONNECTION_TYPE_RAOP, 1);
+ raop_conn_t *raop_conn = (raop_conn_t *) httpd_get_connection_by_type(conn->raop->httpd, CONNECTION_TYPE_RAOP, 1);
if (raop_conn) {
raop_rtp_mirror_t *raop_rtp_mirror = raop_conn->raop_rtp_mirror;
if (raop_rtp_mirror) {
@@ -273,7 +282,7 @@ conn_request(void *ptr, http_request_t *request, http_response_t **response) {
httpd_set_connection_type(conn->raop->httpd, ptr, CONNECTION_TYPE_HLS);
conn->connection_type = CONNECTION_TYPE_HLS;
} else {
- logger_log(conn->raop->logger, LOGGER_WARNING, "connection from unknown connection type");
+ logger_log(conn->raop->logger, LOGGER_WARNING, "connection from unknown connection type");
}
}
@@ -310,9 +319,9 @@ conn_request(void *ptr, http_request_t *request, http_response_t **response) {
if (request_data && logger_debug) {
if (request_datalen > 0) {
/* logger has a buffer limit of 4096 */
- if (data_is_plist) {
- plist_t req_root_node = NULL;
- plist_from_bin(request_data, request_datalen, &req_root_node);
+ if (data_is_plist) {
+ plist_t req_root_node = NULL;
+ plist_from_bin(request_data, request_datalen, &req_root_node);
char * plist_xml = NULL;
char * stripped_xml = NULL;
uint32_t plist_len;
@@ -353,7 +362,7 @@ conn_request(void *ptr, http_request_t *request, http_response_t **response) {
if (!strcmp(method, "POST")) {
if (!strcmp(url, "/feedback")) {
handler = &raop_handler_feedback;
- } else if (!strcmp(url, "/pair-pin-start")) {
+ } else if (!strcmp(url, "/pair-pin-start")) {
handler = &raop_handler_pairpinstart;
} else if (!strcmp(url, "/pair-setup-pin")) {
handler = &raop_handler_pairsetup_pin;
@@ -363,13 +372,11 @@ conn_request(void *ptr, http_request_t *request, http_response_t **response) {
handler = &raop_handler_pairverify;
} else if (!strcmp(url, "/fp-setup")) {
handler = &raop_handler_fpsetup;
- } else if (!strcmp(url, "/getProperty")) {
- handler = &http_handler_get_property;
} else if (!strcmp(url, "/audioMode")) {
- //handler = &http_handler_audioMode;
+ handler = &raop_handler_audiomode;
}
} else if (!strcmp(method, "GET")) {
- if (!strcmp(url, "/info")) {
+ if (strstr(url, "/info")) {
handler = &raop_handler_info;
}
} else if (!strcmp(method, "OPTIONS")) {
@@ -388,7 +395,7 @@ conn_request(void *ptr, http_request_t *request, http_response_t **response) {
handler = &raop_handler_teardown;
} else {
http_response_init(*response, protocol, 501, "Not Implemented");
- }
+ }
} else if (!hls_request && !strcmp(protocol, "HTTP/1.1")) {
if (!strcmp(method, "POST")) {
if (!strcmp(url, "/reverse")) {
@@ -415,9 +422,9 @@ conn_request(void *ptr, http_request_t *request, http_response_t **response) {
handler = &http_handler_playback_info;
}
} else if (!strcmp(method, "PUT")) {
- if (!strncmp (url, "/setProperty?", strlen("/setProperty?"))) {
+ if (!strncmp (url, "/setProperty?", strlen("/setProperty?"))) {
handler = &http_handler_set_property;
- }
+ }
}
} else if (hls_request) {
handler = &http_handler_hls;
@@ -426,8 +433,8 @@ conn_request(void *ptr, http_request_t *request, http_response_t **response) {
if (handler != NULL) {
handler(conn, request, *response, &response_data, &response_datalen);
} else {
- logger_log(conn->raop->logger, LOGGER_INFO,
- "Unhandled Client Request: %s %s %s", method, url, protocol);
+ logger_log(conn->raop->logger, LOGGER_INFO,
+ "Unhandled Client Request: %s %s %s", method, url, protocol);
}
finish:;
@@ -435,7 +442,7 @@ conn_request(void *ptr, http_request_t *request, http_response_t **response) {
http_response_add_header(*response, "Server", "AirTunes/"GLOBAL_VERSION);
if (cseq) {
http_response_add_header(*response, "CSeq", cseq);
- }
+ }
}
http_response_finish(*response, response_data, response_datalen);
@@ -488,7 +495,7 @@ conn_request(void *ptr, http_request_t *request, http_response_t **response) {
}
if (response_data) {
free(response_data);
- }
+ }
}
}
@@ -646,10 +653,10 @@ raop_destroy(raop_t *raop) {
pairing_destroy(raop->pairing);
httpd_destroy(raop->httpd);
logger_destroy(raop->logger);
- if (raop->nonce) {
+ if (raop->nonce) {
free(raop->nonce);
}
- if (raop->random_pw) {
+ if (raop->random_pw) {
free(raop->random_pw);
}
diff --git a/lib/raop.h b/lib/raop.h
index 6cbe3d6..3eb1054 100644
--- a/lib/raop.h
+++ b/lib/raop.h
@@ -12,7 +12,7 @@
* Lesser General Public License for more details.
*
*===================================================================
- * modified by fduncanh 2021-23
+ * modified by fduncanh 2021-25
*/
#ifndef RAOP_H
@@ -84,9 +84,10 @@ struct raop_callbacks_s {
void (*audio_set_coverart)(void *cls, const void *buffer, int buflen);
void (*audio_stop_coverart_rendering) (void* cls);
void (*audio_remote_control_id)(void *cls, const char *dacp_id, const char *active_remote_header);
- void (*audio_set_progress)(void *cls, unsigned int start, unsigned int curr, unsigned int end);
+ void (*audio_set_progress)(void *cls, uint32_t *start, uint32_t *curr, uint32_t *end);
void (*audio_get_format)(void *cls, unsigned char *ct, unsigned short *spf, bool *usingScreen, bool *isMedia, uint64_t *audioFormat);
void (*video_report_size)(void *cls, float *width_source, float *height_source, float *width, float *height);
+ void (*mirror_video_activity)(void *cls, double *txusage);
void (*report_client_request) (void *cls, char *deviceid, char *model, char *name, bool *admit);
void (*display_pin) (void *cls, char * pin);
void (*register_client) (void *cls, const char *device_id, const char *pk_str, const char *name);
diff --git a/lib/raop_buffer.c b/lib/raop_buffer.c
index 99cd961..29f1f3e 100644
--- a/lib/raop_buffer.c
+++ b/lib/raop_buffer.c
@@ -175,7 +175,7 @@ raop_buffer_enqueue(raop_buffer_t *raop_buffer, unsigned char *data, unsigned sh
return -1;
}
/* before time is synchronized, some empty data packets are sent */
- if (datalen == 16 && !memcmp(&data[12], empty_packet_marker, 4)) {
+ if (datalen == 12 || (datalen == 16 && !memcmp(&data[12], empty_packet_marker, 4))) {
return 0;
}
int payload_size = datalen - 12;
diff --git a/lib/raop_handlers.h b/lib/raop_handlers.h
index 4dcfbab..1e1bd8b 100644
--- a/lib/raop_handlers.h
+++ b/lib/raop_handlers.h
@@ -22,15 +22,24 @@
#include "utils.h"
#include
#include
+#include
#include
#define AUDIO_SAMPLE_RATE 44100 /* all supported AirPlay audio format use this sample rate */
#define SECOND_IN_USECS 1000000
#define SECOND_IN_NSECS 1000000000
-#define MAX_PW_ATTEMPTS 5
+#define MAX_PW_ATTEMPTS 3
typedef void (*raop_handler_t)(raop_conn_t *, http_request_t *,
http_response_t *, char **, int *);
+#ifndef PLIST_230
+void plist_mem_free(void* ptr) {
+ if (ptr) {
+ free(ptr);
+ }
+}
+#endif
+
static void
raop_handler_info(raop_conn_t *conn,
http_request_t *request, http_response_t *response,
@@ -38,11 +47,27 @@ raop_handler_info(raop_conn_t *conn,
{
assert(conn->raop->dnssd);
-#if 0
- /* initial GET/info request sends plist with string "txtAirPlay" */
- bool txtAirPlay = false;
+ /* There are three possible RTSP/1.0 GET/info requests
+ (1) with a CSeq number and with a plist
+ (2) with a CSeq number and without a plist
+ (3) without a CSeq number or a Header (part of Bluetooth LE Service Discovery) */
+
+ const char* url = NULL;
const char* content_type = NULL;
+ const char* cseq = NULL;
+ url = http_request_get_url(request);
content_type = http_request_get_header(request, "Content-Type");
+ cseq = http_request_get_header(request, "CSeq");
+ int len;
+ bool add_txt_airplay = false;
+ bool add_txt_raop = false;
+ const char txtRAOP[] = "txtRAOP";
+ const char txtAirPlay[] = "txtAirPlay";
+
+
+ plist_t res_node = plist_new_dict();
+
+ /* initial GET/info request sends plist with string "txtAirPlay" */
if (content_type && strstr(content_type, "application/x-apple-binary-plist")) {
char *qualifier_string = NULL;
const char *data = NULL;
@@ -56,17 +81,40 @@ raop_handler_info(raop_conn_t *conn,
plist_t req_string_node = plist_array_get_item(req_qualifier_node, 0);
plist_get_string_val(req_string_node, &qualifier_string);
}
- if (qualifier_string && !strcmp(qualifier_string, "txtAirPlay")) {
- printf("qualifier: %s\n", qualifier_string);
- txtAirPlay = true;
+ if (qualifier_string) {
+ if (!strcmp(qualifier_string, txtAirPlay )) {
+ add_txt_airplay = true;
+ } else if (!strcmp(qualifier_string, txtRAOP)) {
+ add_txt_raop = true;
+ }
+ plist_mem_free(qualifier_string);
}
- if (qualifier_string) {
- free(qualifier_string);
- }
}
-#endif
- plist_t res_node = plist_new_dict();
+
+ /* Bluetoth LE discovery protocol request */
+ if (!cseq) {
+ add_txt_airplay = (bool) strstr(url, txtAirPlay);
+ add_txt_raop = (bool) strstr(url, txtRAOP);
+ }
+
+ if (add_txt_airplay) {
+ const char *txt = dnssd_get_airplay_txt(conn->raop->dnssd, &len);
+ plist_t txt_airplay_node = plist_new_data(txt, len);
+ plist_dict_set_item(res_node, txtAirPlay, txt_airplay_node);
+ }
+
+ if (add_txt_raop) {
+ const char *txt = dnssd_get_raop_txt(conn->raop->dnssd, &len);
+ plist_t txt_raop_node = plist_new_data(txt, len);
+ plist_dict_set_item(res_node, txtRAOP, txt_raop_node);
+ }
+
+ /* don't need anything below here in the response to initial "txtAirPlay" GET/info request */
+ if (content_type) {
+ goto finished;
+ }
+
/* deviceID is the physical hardware address, and will not change */
int hw_addr_raw_len = 0;
const char *hw_addr_raw = dnssd_get_hw_addr(conn->raop->dnssd, &hw_addr_raw_len);
@@ -76,17 +124,16 @@ raop_handler_info(raop_conn_t *conn,
plist_t device_id_node = plist_new_string(hw_addr);
plist_dict_set_item(res_node, "deviceID", device_id_node);
+ plist_t mac_address_node = plist_new_string(hw_addr);
+ plist_dict_set_item(res_node, "macAddress", mac_address_node);
+ free(hw_addr);
+
/* Persistent Public Key */
int pk_len = 0;
char *pk = utils_parse_hex(conn->raop->pk_str, strlen(conn->raop->pk_str), &pk_len);
plist_t pk_node = plist_new_data(pk, pk_len);
plist_dict_set_item(res_node, "pk", pk_node);
-
- /* airplay_txt is from the _airplay._tcp dnssd announuncement, may not be necessary */
- int airplay_txt_len = 0;
- const char *airplay_txt = dnssd_get_airplay_txt(conn->raop->dnssd, &airplay_txt_len);
- plist_t txt_airplay_node = plist_new_data(airplay_txt, airplay_txt_len);
- plist_dict_set_item(res_node, "txtAirPlay", txt_airplay_node);
+ free(pk);
uint64_t features = dnssd_get_airplay_features(conn->raop->dnssd);
plist_t features_node = plist_new_uint(features);
@@ -97,6 +144,32 @@ raop_handler_info(raop_conn_t *conn,
plist_t name_node = plist_new_string(name);
plist_dict_set_item(res_node, "name", name_node);
+ plist_t pi_node = plist_new_string(AIRPLAY_PI);
+ plist_dict_set_item(res_node, "pi", pi_node);
+
+ plist_t vv_node = plist_new_uint(strtol(AIRPLAY_VV, NULL, 10));
+ plist_dict_set_item(res_node, "vv", vv_node);
+
+ plist_t status_flags_node = plist_new_uint(68);
+ plist_dict_set_item(res_node, "statusFlags", status_flags_node);
+
+ plist_t keep_alive_low_power_node = plist_new_uint(1);
+ plist_dict_set_item(res_node, "keepAliveLowPower", keep_alive_low_power_node);
+
+ plist_t source_version_node = plist_new_string(GLOBAL_VERSION);
+ plist_dict_set_item(res_node, "sourceVersion", source_version_node);
+
+ plist_t keep_alive_send_stats_as_body_node = plist_new_bool(1);
+ plist_dict_set_item(res_node, "keepAliveSendStatsAsBody", keep_alive_send_stats_as_body_node);
+
+ plist_t model_node = plist_new_string(GLOBAL_MODEL);
+ plist_dict_set_item(res_node, "model", model_node);
+
+ /* dont need anything below here in the Bluetooth LE response */
+ if (cseq == NULL) {
+ goto finished;
+ }
+
plist_t audio_latencies_node = plist_new_array();
plist_t audio_latencies_0_node = plist_new_dict();
plist_t audio_latencies_0_output_latency_micros_node = plist_new_bool(0);
@@ -141,30 +214,6 @@ raop_handler_info(raop_conn_t *conn,
plist_array_append_item(audio_formats_node, audio_format_1_node);
plist_dict_set_item(res_node, "audioFormats", audio_formats_node);
- plist_t pi_node = plist_new_string(AIRPLAY_PI);
- plist_dict_set_item(res_node, "pi", pi_node);
-
- plist_t vv_node = plist_new_uint(strtol(AIRPLAY_VV, NULL, 10));
- plist_dict_set_item(res_node, "vv", vv_node);
-
- plist_t status_flags_node = plist_new_uint(68);
- plist_dict_set_item(res_node, "statusFlags", status_flags_node);
-
- plist_t keep_alive_low_power_node = plist_new_uint(1);
- plist_dict_set_item(res_node, "keepAliveLowPower", keep_alive_low_power_node);
-
- plist_t source_version_node = plist_new_string(GLOBAL_VERSION);
- plist_dict_set_item(res_node, "sourceVersion", source_version_node);
-
- plist_t keep_alive_send_stats_as_body_node = plist_new_bool(1);
- plist_dict_set_item(res_node, "keepAliveSendStatsAsBody", keep_alive_send_stats_as_body_node);
-
- plist_t model_node = plist_new_string(GLOBAL_MODEL);
- plist_dict_set_item(res_node, "model", model_node);
-
- plist_t mac_address_node = plist_new_string(hw_addr);
- plist_dict_set_item(res_node, "macAddress", mac_address_node);
-
plist_t displays_node = plist_new_array();
plist_t displays_0_node = plist_new_dict();
plist_t displays_0_width_physical_node = plist_new_uint(0);
@@ -195,11 +244,10 @@ raop_handler_info(raop_conn_t *conn,
plist_array_append_item(displays_node, displays_0_node);
plist_dict_set_item(res_node, "displays", displays_node);
+ finished:
plist_to_bin(res_node, response_data, (uint32_t *) response_datalen);
plist_free(res_node);
http_response_add_header(response, "Content-Type", "application/x-apple-binary-plist");
- free(pk);
- free(hw_addr);
}
static void
@@ -265,9 +313,9 @@ raop_handler_pairsetup_pin(raop_conn_t *conn,
if (PLIST_IS_STRING(req_method_node) && PLIST_IS_STRING(req_user_node)) {
/* this is the initial pair-setup-pin request */
const char *salt;
- char pin[6];
- const char *pk;
- int len_pk, len_salt;
+ char pin[6];
+ const char *pk;
+ int len_pk, len_salt;
char *method = NULL;
char *user = NULL;
plist_get_string_val(req_method_node, &method);
@@ -279,16 +327,18 @@ raop_handler_pairsetup_pin(raop_conn_t *conn,
plist_free (req_root_node);
return;
}
- free (method);
- plist_get_string_val(req_user_node, &user);
+ plist_mem_free(method);
+ method = NULL;
+ plist_get_string_val(req_user_node, &user);
logger_log(conn->raop->logger, LOGGER_INFO, "pair-setup-pin: device_id = %s", user);
snprintf(pin, 6, "%04u", conn->raop->pin % 10000);
if (conn->raop->pin < 10000) {
conn->raop->pin = 0;
}
- int ret = srp_new_user(conn->session, conn->raop->pairing, (const char *) user,
+ int ret = srp_new_user(conn->session, conn->raop->pairing, (const char *) user,
(const char *) pin, &salt, &len_salt, &pk, &len_pk);
- free(user);
+ plist_mem_free(user);
+ user = NULL;
plist_free(req_root_node);
if (ret < 0) {
logger_log(conn->raop->logger, LOGGER_ERR, "failed to create user, err = %d", ret);
@@ -302,19 +352,19 @@ raop_handler_pairsetup_pin(raop_conn_t *conn,
plist_to_bin(res_root_node, response_data, (uint32_t*) response_datalen);
plist_free(res_root_node);
http_response_add_header(response, "Content-Type", "application/x-apple-binary-plist");
- return;
+ return;
} else if (PLIST_IS_DATA(req_pk_node) && PLIST_IS_DATA(req_proof_node)) {
/* this is the second part of pair-setup-pin request */
char *client_pk = NULL;
char *client_proof = NULL;
- unsigned char proof[64];
- memset(proof, 0, sizeof(proof));
+ unsigned char proof[64];
+ memset(proof, 0, sizeof(proof));
uint64_t client_pk_len;
uint64_t client_proof_len;
plist_get_data_val(req_pk_node, &client_pk, &client_pk_len);
plist_get_data_val(req_proof_node, &client_proof, &client_proof_len);
if (logger_debug) {
- char *str = utils_data_to_string((const unsigned char *) client_proof, client_proof_len, 20);
+ char *str = utils_data_to_string((const unsigned char *) client_proof, client_proof_len, 20);
logger_log(conn->raop->logger, LOGGER_DEBUG, "client SRP6a proof :\n%s", str);
free (str);
}
@@ -339,7 +389,7 @@ raop_handler_pairsetup_pin(raop_conn_t *conn,
plist_to_bin(res_root_node, response_data, (uint32_t*) response_datalen);
plist_free(res_root_node);
http_response_add_header(response, "Content-Type", "application/x-apple-binary-plist");
- return;
+ return;
} else if (PLIST_IS_DATA(req_epk_node) && PLIST_IS_DATA(req_authtag_node)) {
/* this is the third part of pair-setup-pin request */
char *client_epk = NULL;
@@ -347,25 +397,25 @@ raop_handler_pairsetup_pin(raop_conn_t *conn,
uint64_t client_epk_len;
uint64_t client_authtag_len;
unsigned char epk[ED25519_KEY_SIZE];
- unsigned char authtag[GCM_AUTHTAG_SIZE];
- int ret;
+ unsigned char authtag[GCM_AUTHTAG_SIZE];
+ int ret;
plist_get_data_val(req_epk_node, &client_epk, &client_epk_len);
plist_get_data_val(req_authtag_node, &client_authtag, &client_authtag_len);
- if (logger_debug) {
+ if (logger_debug) {
char *str = utils_data_to_string((const unsigned char *) client_epk, client_epk_len, 16);
logger_log(conn->raop->logger, LOGGER_DEBUG, "client_epk %d:\n%s\n", (int) client_epk_len, str);
str = utils_data_to_string((const unsigned char *) client_authtag, client_authtag_len, 16);
logger_log(conn->raop->logger, LOGGER_DEBUG, "client_authtag %d:\n%s\n", (int) client_authtag_len, str);
free (str);
- }
+ }
- memcpy(epk, client_epk, ED25519_KEY_SIZE);
- memcpy(authtag, client_authtag, GCM_AUTHTAG_SIZE);
+ memcpy(epk, client_epk, ED25519_KEY_SIZE);
+ memcpy(authtag, client_authtag, GCM_AUTHTAG_SIZE);
free (client_authtag);
free (client_epk);
plist_free(req_root_node);
- ret = srp_confirm_pair_setup(conn->session, conn->raop->pairing, epk, authtag);
+ ret = srp_confirm_pair_setup(conn->session, conn->raop->pairing, epk, authtag);
if (ret < 0) {
logger_log(conn->raop->logger, LOGGER_ERR, "pair-pin-setup (step 3): client authentication failed\n");
goto authentication_failed;
@@ -375,13 +425,13 @@ raop_handler_pairsetup_pin(raop_conn_t *conn,
pairing_session_set_setup_status(conn->session);
plist_t res_root_node = plist_new_dict();
plist_t res_epk_node = plist_new_data((const char *) epk, 32);
- plist_t res_authtag_node = plist_new_data((const char *) authtag, 16);
+ plist_t res_authtag_node = plist_new_data((const char *) authtag, 16);
plist_dict_set_item(res_root_node, "epk", res_epk_node);
plist_dict_set_item(res_root_node, "authTag", res_authtag_node);
plist_to_bin(res_root_node, response_data, (uint32_t*) response_datalen);
plist_free(res_root_node);
http_response_add_header(response, "Content-Type", "application/x-apple-binary-plist");
- return;
+ return;
}
authentication_failed:;
http_response_init(response, "RTSP/1.0", 470, "Client Authentication Failure");
@@ -439,58 +489,58 @@ raop_handler_pairverify(raop_conn_t *conn,
return;
}
switch (data[0]) {
- case 1:
- if (datalen != 4 + X25519_KEY_SIZE + ED25519_KEY_SIZE) {
- logger_log(conn->raop->logger, LOGGER_ERR, "Invalid pair-verify data");
- return;
- }
- /* We can fall through these errors, the result will just be garbage... */
- if (pairing_session_handshake(conn->session, data + 4, data + 4 + X25519_KEY_SIZE)) {
- logger_log(conn->raop->logger, LOGGER_ERR, "Error initializing pair-verify handshake");
- }
- if (pairing_session_get_public_key(conn->session, public_key)) {
- logger_log(conn->raop->logger, LOGGER_ERR, "Error getting ECDH public key");
- }
- if (pairing_session_get_signature(conn->session, signature)) {
- logger_log(conn->raop->logger, LOGGER_ERR, "Error getting ED25519 signature");
- }
- if (register_check) {
- bool registered_client = true;
- if (conn->raop->callbacks.check_register) {
- const unsigned char *pk = data + 4 + X25519_KEY_SIZE;
- char *pk64;
- ed25519_pk_to_base64(pk, &pk64);
- registered_client = conn->raop->callbacks.check_register(conn->raop->callbacks.cls, pk64);
- free (pk64);
- }
-
- if (!registered_client) {
- return;
- }
- }
- *response_data = malloc(sizeof(public_key) + sizeof(signature));
- if (*response_data) {
- http_response_add_header(response, "Content-Type", "application/octet-stream");
- memcpy(*response_data, public_key, sizeof(public_key));
- memcpy(*response_data + sizeof(public_key), signature, sizeof(signature));
- *response_datalen = sizeof(public_key) + sizeof(signature);
- }
- break;
- case 0:
- logger_log(conn->raop->logger, LOGGER_DEBUG, "2nd pair-verify step: checking signature");
- if (datalen != 4 + PAIRING_SIG_SIZE) {
- logger_log(conn->raop->logger, LOGGER_ERR, "Invalid pair-verify data");
- return;
+ case 1:
+ if (datalen != 4 + X25519_KEY_SIZE + ED25519_KEY_SIZE) {
+ logger_log(conn->raop->logger, LOGGER_ERR, "Invalid pair-verify data");
+ return;
+ }
+ /* We can fall through these errors, the result will just be garbage... */
+ if (pairing_session_handshake(conn->session, data + 4, data + 4 + X25519_KEY_SIZE)) {
+ logger_log(conn->raop->logger, LOGGER_ERR, "Error initializing pair-verify handshake");
+ }
+ if (pairing_session_get_public_key(conn->session, public_key)) {
+ logger_log(conn->raop->logger, LOGGER_ERR, "Error getting ECDH public key");
+ }
+ if (pairing_session_get_signature(conn->session, signature)) {
+ logger_log(conn->raop->logger, LOGGER_ERR, "Error getting ED25519 signature");
+ }
+ if (register_check) {
+ bool registered_client = true;
+ if (conn->raop->callbacks.check_register) {
+ const unsigned char *pk = data + 4 + X25519_KEY_SIZE;
+ char *pk64;
+ ed25519_pk_to_base64(pk, &pk64);
+ registered_client = conn->raop->callbacks.check_register(conn->raop->callbacks.cls, pk64);
+ free (pk64);
}
- if (pairing_session_finish(conn->session, data + 4)) {
- logger_log(conn->raop->logger, LOGGER_ERR, "Incorrect pair-verify signature");
- http_response_set_disconnect(response, 1);
+ if (!registered_client) {
return;
}
- logger_log(conn->raop->logger, LOGGER_DEBUG, "pair-verify: signature is verified");
+ }
+ *response_data = malloc(sizeof(public_key) + sizeof(signature));
+ if (*response_data) {
http_response_add_header(response, "Content-Type", "application/octet-stream");
- break;
+ memcpy(*response_data, public_key, sizeof(public_key));
+ memcpy(*response_data + sizeof(public_key), signature, sizeof(signature));
+ *response_datalen = sizeof(public_key) + sizeof(signature);
+ }
+ break;
+ case 0:
+ logger_log(conn->raop->logger, LOGGER_DEBUG, "2nd pair-verify step: checking signature");
+ if (datalen != 4 + PAIRING_SIG_SIZE) {
+ logger_log(conn->raop->logger, LOGGER_ERR, "Invalid pair-verify data");
+ return;
+ }
+
+ if (pairing_session_finish(conn->session, data + 4)) {
+ logger_log(conn->raop->logger, LOGGER_ERR, "Incorrect pair-verify signature");
+ http_response_set_disconnect(response, 1);
+ return;
+ }
+ logger_log(conn->raop->logger, LOGGER_DEBUG, "pair-verify: signature is verified");
+ http_response_add_header(response, "Content-Type", "application/octet-stream");
+ break;
}
}
@@ -593,15 +643,21 @@ raop_handler_setup(raop_conn_t *conn,
/* RFC2617 Digest authentication (md5 hash) of uxplay client-access password, if set */
if (!conn->authenticated && conn->raop->callbacks.passwd) {
size_t pin_len = 4;
- if (conn->raop->random_pw && strncmp(conn->raop->random_pw + pin_len + 1, deviceID, 17)) {
- conn->raop->auth_fail_count = MAX_PW_ATTEMPTS;
+ const char *authorization = NULL;
+ authorization = http_request_get_header(request, "Authorization");
+ if (!authorization) {
+ // if random_pw is set, but client has changed, unset it
+ if (conn->raop->random_pw && strncmp(conn->raop->random_pw + pin_len + 1, deviceID, 17)) {
+ free(conn->raop->random_pw);
+ conn->raop->random_pw = NULL;
+ }
}
int len;
const char *password = conn->raop->callbacks.passwd(conn->raop->callbacks.cls, &len);
// len = -1 means use a random password for this connection; len = 0 means no password
if (len == -1 && conn->raop->random_pw && conn->raop->auth_fail_count >= MAX_PW_ATTEMPTS) {
// change random_pw after MAX_PW_ATTEMPTS failed authentication attempts
- logger_log(conn->raop->logger, LOGGER_INFO, "Too many authentication failures or new client: generate new random password");
+ logger_log(conn->raop->logger, LOGGER_INFO, "Too many authentication failures: generate new random password");
free(conn->raop->random_pw);
conn->raop->random_pw = NULL;
}
@@ -612,16 +668,19 @@ raop_handler_setup(raop_conn_t *conn,
logger_log(conn->raop->logger, LOGGER_ERR, "Failed to generate random pin");
pin_4 = 1234;
}
- conn->raop->random_pw = (char *) malloc(pin_len + 1 + 18);
+ conn->raop->random_pw = (char *) malloc(pin_len + 1 + 18);
char *pin = conn->raop->random_pw;
snprintf(pin, pin_len + 1, "%04u", pin_4 % 10000);
pin[pin_len] = '\0';
snprintf(pin + pin_len + 1, 18, "%s", deviceID);
conn->raop->auth_fail_count = 0;
+ }
+ if (len == -1 && !authorization && conn->raop->random_pw) {
if (conn->raop->callbacks.display_pin) {
- conn->raop->callbacks.display_pin(conn->raop->callbacks.cls, pin);
+ conn->raop->callbacks.display_pin(conn->raop->callbacks.cls, conn->raop->random_pw);
}
- logger_log(conn->raop->logger, LOGGER_INFO, "*** CLIENT MUST NOW ENTER PIN = \"%s\" AS AIRPLAY PASSWORD", pin);
+ logger_log(conn->raop->logger, LOGGER_INFO, "*** CLIENT MUST NOW ENTER PIN = \"%s\" AS AIRPLAY PASSWORD", conn->raop->random_pw);
+ conn->raop->auth_fail_count++;
}
if (len && !conn->authenticated) {
if (len == -1) {
@@ -629,20 +688,17 @@ raop_handler_setup(raop_conn_t *conn,
}
char nonce_string[33] = { '\0' };
//bool stale = false; //not implemented
- const char *authorization = NULL;
- authorization = http_request_get_header(request, "Authorization");
- if (authorization) {
+ if (len && authorization) {
char *ptr = strstr(authorization, "nonce=\"") + strlen("nonce=\"");
strncpy(nonce_string, ptr, 32);
const char *method = http_request_get_method(request);
conn->authenticated = pairing_digest_verify(method, authorization, password);
if (!conn->authenticated) {
- conn->raop->auth_fail_count++;
- logger_log(conn->raop->logger, LOGGER_INFO, "*** authentication failure: count = %u", conn->raop->auth_fail_count);
- if (conn->raop->callbacks.display_pin && conn->raop->auth_fail_count > 1) {
- conn->raop->callbacks.display_pin(conn->raop->callbacks.cls, conn->raop->random_pw);
+ // if random_pw is used, the auth_fail_count will be the number of times it is displayed after creation
+ if (len != -1) {
+ conn->raop->auth_fail_count++;
}
- logger_log(conn->raop->logger, LOGGER_INFO, "*** CLIENT MUST NOW ENTER PIN = \"%s\" AS AIRPLAY PASSWORD", conn->raop->random_pw);
+ logger_log(conn->raop->logger, LOGGER_INFO, "*** authentication failure: count = %u", conn->raop->auth_fail_count);
}
if (conn->authenticated) {
//printf("initial authenticatication OK\n");
@@ -655,7 +711,7 @@ raop_handler_setup(raop_conn_t *conn,
if (conn->authenticated && conn->raop->random_pw) {
free (conn->raop->random_pw);
conn->raop->random_pw = NULL;
- }
+ }
if (conn->raop->nonce) {
free(conn->raop->nonce);
conn->raop->nonce = NULL;
@@ -685,17 +741,17 @@ raop_handler_setup(raop_conn_t *conn,
char* eiv = NULL;
uint64_t eiv_len = 0;
- char *model = NULL;
+ char *model = NULL;
char *name = NULL;
bool admit_client = true;
plist_t req_model_node = plist_dict_get_item(req_root_node, "model");
plist_get_string_val(req_model_node, &model);
plist_t req_name_node = plist_dict_get_item(req_root_node, "name");
plist_get_string_val(req_name_node, &name);
- if (conn->raop->callbacks.report_client_request) {
+ if (conn->raop->callbacks.report_client_request) {
conn->raop->callbacks.report_client_request(conn->raop->callbacks.cls, deviceID, model, name, &admit_client);
}
- if (admit_client && deviceID && name && conn->raop->callbacks.register_client) {
+ if (admit_client && deviceID && name && conn->raop->callbacks.register_client) {
char *client_device_id = NULL;
char *client_pk = NULL; /* encoded as null-terminated base64 string, must be freed*/
get_pairing_session_client_data(conn->session, &client_device_id, &client_pk);
@@ -703,35 +759,29 @@ raop_handler_setup(raop_conn_t *conn,
conn->raop->callbacks.register_client(conn->raop->callbacks.cls, client_device_id, client_pk, name);
free (client_pk);
}
- }
- if (deviceID) {
- free (deviceID);
- deviceID = NULL;
- }
- if (model) {
- free (model);
- model = NULL;
- }
- if (name) {
- free (name);
- name = NULL;
}
+ plist_mem_free(deviceID);
+ deviceID = NULL;
+ plist_mem_free(model);
+ model = NULL;
+ plist_mem_free(name);
+ name = NULL;
if (admit_client == false) {
/* client is not authorized to connect */
plist_free(res_root_node);
plist_free(req_root_node);
return;
- }
+ }
plist_get_data_val(req_eiv_node, &eiv, &eiv_len);
memcpy(aesiv, eiv, 16);
free(eiv);
logger_log(conn->raop->logger, LOGGER_DEBUG, "eiv_len = %llu", eiv_len);
- if (logger_debug) {
+ if (logger_debug) {
char* str = utils_data_to_string(aesiv, 16, 16);
logger_log(conn->raop->logger, LOGGER_DEBUG, "16 byte aesiv (needed for AES-CBC audio decryption iv):\n%s", str);
free(str);
- }
+ }
char* ekey = NULL;
uint64_t ekey_len = 0;
@@ -740,7 +790,7 @@ raop_handler_setup(raop_conn_t *conn,
free(ekey);
logger_log(conn->raop->logger, LOGGER_DEBUG, "ekey_len = %llu", ekey_len);
// eaeskey is 72 bytes, aeskey is 16 bytes
- if (logger_debug) {
+ if (logger_debug) {
char *str = utils_data_to_string((unsigned char *) eaeskey, ekey_len, 16);
logger_log(conn->raop->logger, LOGGER_DEBUG, "ekey:\n%s", str);
free (str);
@@ -759,6 +809,7 @@ raop_handler_setup(raop_conn_t *conn,
bool old_protocol = false;
#ifdef OLD_PROTOCOL_CLIENT_USER_AGENT_LIST /* set in global.h */
if (strstr(OLD_PROTOCOL_CLIENT_USER_AGENT_LIST, user_agent)) old_protocol = true;
+ if (strstr(user_agent, "AirMyPC")) old_protocol = true; //AirMyPC/7200 still uses old protocol: unlikely to change (?)
#endif
if (old_protocol) { /* some windows AirPlay-client emulators use old AirPlay 1 protocol with unhashed AES key */
logger_log(conn->raop->logger, LOGGER_INFO, "Client identifed as using old protocol (unhashed) AES audio key)");
@@ -808,9 +859,9 @@ raop_handler_setup(raop_conn_t *conn,
logger_log(conn->raop->logger, LOGGER_ERR, "Client specified AirPlay2 \"Remote Control\" protocol\n"
" Only AirPlay v1 protocol (using NTP and timing port) is supported");
}
- }
+ }
char *timing_protocol = NULL;
- timing_protocol_t time_protocol = TP_NONE;
+ timing_protocol_t time_protocol = TP_NONE;
plist_t req_timing_protocol_node = plist_dict_get_item(req_root_node, "timingProtocol");
plist_get_string_val(req_timing_protocol_node, &timing_protocol);
if (timing_protocol) {
@@ -826,7 +877,7 @@ raop_handler_setup(raop_conn_t *conn,
logger_log(conn->raop->logger, LOGGER_ERR, "Client specified timingProtocol=%s,"
" but timingProtocol= NTP is required here", timing_protocol);
}
- free (timing_protocol);
+ plist_mem_free (timing_protocol);
timing_protocol = NULL;
} else {
logger_log(conn->raop->logger, LOGGER_DEBUG, "Client did not specify timingProtocol,"
@@ -835,7 +886,7 @@ raop_handler_setup(raop_conn_t *conn,
}
uint64_t timing_rport = 0;
plist_t req_timing_port_node = plist_dict_get_item(req_root_node, "timingPort");
- if (req_timing_port_node) {
+ if (req_timing_port_node) {
plist_get_uint_val(req_timing_port_node, &timing_rport);
}
if (timing_rport) {
@@ -891,106 +942,107 @@ raop_handler_setup(raop_conn_t *conn,
logger_log(conn->raop->logger, LOGGER_DEBUG, "type = %llu", type);
switch (type) {
- case 110: {
- // Mirroring
- unsigned short dport = conn->raop->mirror_data_lport;
- plist_t stream_id_node = plist_dict_get_item(req_stream_node, "streamConnectionID");
- uint64_t stream_connection_id = 0;
- plist_get_uint_val(stream_id_node, &stream_connection_id);
- logger_log(conn->raop->logger, LOGGER_DEBUG, "streamConnectionID (needed for AES-CTR video decryption"
- " key and iv): %llu", stream_connection_id);
+ case 110: {
+ // Mirroring
+ unsigned short dport = conn->raop->mirror_data_lport;
+ plist_t stream_id_node = plist_dict_get_item(req_stream_node, "streamConnectionID");
+ uint64_t stream_connection_id = 0;
+ plist_get_uint_val(stream_id_node, &stream_connection_id);
+ logger_log(conn->raop->logger, LOGGER_DEBUG, "streamConnectionID (needed for AES-CTR video decryption"
+ " key and iv): %llu", stream_connection_id);
- if (conn->raop_rtp_mirror) {
- raop_rtp_mirror_init_aes(conn->raop_rtp_mirror, &stream_connection_id);
- raop_rtp_mirror_start(conn->raop_rtp_mirror, &dport, conn->raop->clientFPSdata);
- logger_log(conn->raop->logger, LOGGER_DEBUG, "Mirroring initialized successfully");
- } else {
- logger_log(conn->raop->logger, LOGGER_ERR, "Mirroring not initialized at SETUP, playing will fail!");
- http_response_set_disconnect(response, 1);
- }
-
- plist_t res_stream_node = plist_new_dict();
- plist_t res_stream_data_port_node = plist_new_uint(dport);
- plist_t res_stream_type_node = plist_new_uint(110);
- plist_dict_set_item(res_stream_node, "dataPort", res_stream_data_port_node);
- plist_dict_set_item(res_stream_node, "type", res_stream_type_node);
- plist_array_append_item(res_streams_node, res_stream_node);
-
- break;
- } case 96: {
- // Audio
- unsigned short cport = conn->raop->control_lport, dport = conn->raop->data_lport;
- unsigned short remote_cport = 0;
- unsigned char ct = 0;
- unsigned int sr = AUDIO_SAMPLE_RATE; /* all AirPlay audio formats supported so far have sample rate 44.1kHz */
-
- uint64_t uint_val = 0;
- plist_t req_stream_control_port_node = plist_dict_get_item(req_stream_node, "controlPort");
- plist_get_uint_val(req_stream_control_port_node, &uint_val);
- remote_cport = (unsigned short) uint_val; /* must != 0 to activate audio resend requests */
-
- plist_t req_stream_ct_node = plist_dict_get_item(req_stream_node, "ct");
- plist_get_uint_val(req_stream_ct_node, &uint_val);
- ct = (unsigned char) uint_val;
-
- if (conn->raop->callbacks.audio_get_format) {
- /* get additional audio format parameters */
- uint64_t audioFormat = 0;
- unsigned short spf = 0;
- bool isMedia = false;
- bool usingScreen = false;
- uint8_t bool_val = 0;
-
- plist_t req_stream_spf_node = plist_dict_get_item(req_stream_node, "spf");
- plist_get_uint_val(req_stream_spf_node, &uint_val);
- spf = (unsigned short) uint_val;
-
- plist_t req_stream_audio_format_node = plist_dict_get_item(req_stream_node, "audioFormat");
- plist_get_uint_val(req_stream_audio_format_node, &audioFormat);
-
- plist_t req_stream_is_media_node = plist_dict_get_item(req_stream_node, "isMedia");
- if (req_stream_is_media_node) {
- plist_get_bool_val(req_stream_is_media_node, &bool_val);
- isMedia = (bool) bool_val;
- } else {
- isMedia = false;
- }
-
- plist_t req_stream_using_screen_node = plist_dict_get_item(req_stream_node, "usingScreen");
- if (req_stream_using_screen_node) {
- plist_get_bool_val(req_stream_using_screen_node, &bool_val);
- usingScreen = (bool) bool_val;
- } else {
- usingScreen = false;
- }
-
- conn->raop->callbacks.audio_get_format(conn->raop->callbacks.cls, &ct, &spf, &usingScreen, &isMedia, &audioFormat);
- }
-
- if (conn->raop_rtp) {
- raop_rtp_start_audio(conn->raop_rtp, &remote_cport, &cport, &dport, &ct, &sr);
- logger_log(conn->raop->logger, LOGGER_DEBUG, "RAOP initialized success");
- } else {
- logger_log(conn->raop->logger, LOGGER_ERR, "RAOP not initialized at SETUP, playing will fail!");
- http_response_set_disconnect(response, 1);
- }
-
- plist_t res_stream_node = plist_new_dict();
- plist_t res_stream_data_port_node = plist_new_uint(dport);
- plist_t res_stream_control_port_node = plist_new_uint(cport);
- plist_t res_stream_type_node = plist_new_uint(96);
- plist_dict_set_item(res_stream_node, "dataPort", res_stream_data_port_node);
- plist_dict_set_item(res_stream_node, "controlPort", res_stream_control_port_node);
- plist_dict_set_item(res_stream_node, "type", res_stream_type_node);
- plist_array_append_item(res_streams_node, res_stream_node);
-
- break;
+ if (conn->raop_rtp_mirror) {
+ raop_rtp_mirror_init_aes(conn->raop_rtp_mirror, &stream_connection_id);
+ raop_rtp_mirror_start(conn->raop_rtp_mirror, &dport, conn->raop->clientFPSdata);
+ logger_log(conn->raop->logger, LOGGER_DEBUG, "Mirroring initialized successfully");
+ } else {
+ logger_log(conn->raop->logger, LOGGER_ERR, "Mirroring not initialized at SETUP, playing will fail!");
+ http_response_set_disconnect(response, 1);
}
- default:
- logger_log(conn->raop->logger, LOGGER_ERR, "SETUP tries to setup stream of unknown type %llu", type);
+ plist_t res_stream_node = plist_new_dict();
+ plist_t res_stream_data_port_node = plist_new_uint(dport);
+ plist_t res_stream_type_node = plist_new_uint(110);
+ plist_dict_set_item(res_stream_node, "dataPort", res_stream_data_port_node);
+ plist_dict_set_item(res_stream_node, "type", res_stream_type_node);
+ plist_array_append_item(res_streams_node, res_stream_node);
+
+ break;
+ }
+ case 96: {
+ // Audio
+ unsigned short cport = conn->raop->control_lport, dport = conn->raop->data_lport;
+ unsigned short remote_cport = 0;
+ unsigned char ct = 0;
+ unsigned int sr = AUDIO_SAMPLE_RATE; /* all AirPlay audio formats supported so far have sample rate 44.1kHz */
+
+ uint64_t uint_val = 0;
+ plist_t req_stream_control_port_node = plist_dict_get_item(req_stream_node, "controlPort");
+ plist_get_uint_val(req_stream_control_port_node, &uint_val);
+ remote_cport = (unsigned short) uint_val; /* must != 0 to activate audio resend requests */
+
+ plist_t req_stream_ct_node = plist_dict_get_item(req_stream_node, "ct");
+ plist_get_uint_val(req_stream_ct_node, &uint_val);
+ ct = (unsigned char) uint_val;
+
+ if (conn->raop->callbacks.audio_get_format) {
+ /* get additional audio format parameters */
+ uint64_t audioFormat = 0;
+ unsigned short spf = 0;
+ bool isMedia = false;
+ bool usingScreen = false;
+ uint8_t bool_val = 0;
+
+ plist_t req_stream_spf_node = plist_dict_get_item(req_stream_node, "spf");
+ plist_get_uint_val(req_stream_spf_node, &uint_val);
+ spf = (unsigned short) uint_val;
+
+ plist_t req_stream_audio_format_node = plist_dict_get_item(req_stream_node, "audioFormat");
+ plist_get_uint_val(req_stream_audio_format_node, &audioFormat);
+
+ plist_t req_stream_is_media_node = plist_dict_get_item(req_stream_node, "isMedia");
+ if (req_stream_is_media_node) {
+ plist_get_bool_val(req_stream_is_media_node, &bool_val);
+ isMedia = (bool) bool_val;
+ } else {
+ isMedia = false;
+ }
+
+ plist_t req_stream_using_screen_node = plist_dict_get_item(req_stream_node, "usingScreen");
+ if (req_stream_using_screen_node) {
+ plist_get_bool_val(req_stream_using_screen_node, &bool_val);
+ usingScreen = (bool) bool_val;
+ } else {
+ usingScreen = false;
+ }
+
+ conn->raop->callbacks.audio_get_format(conn->raop->callbacks.cls, &ct, &spf, &usingScreen, &isMedia, &audioFormat);
+ }
+
+ if (conn->raop_rtp) {
+ raop_rtp_start_audio(conn->raop_rtp, &remote_cport, &cport, &dport, &ct, &sr);
+ logger_log(conn->raop->logger, LOGGER_DEBUG, "RAOP initialized success");
+ } else {
+ logger_log(conn->raop->logger, LOGGER_ERR, "RAOP not initialized at SETUP, playing will fail!");
http_response_set_disconnect(response, 1);
- break;
+ }
+
+ plist_t res_stream_node = plist_new_dict();
+ plist_t res_stream_data_port_node = plist_new_uint(dport);
+ plist_t res_stream_control_port_node = plist_new_uint(cport);
+ plist_t res_stream_type_node = plist_new_uint(96);
+ plist_dict_set_item(res_stream_node, "dataPort", res_stream_data_port_node);
+ plist_dict_set_item(res_stream_node, "controlPort", res_stream_control_port_node);
+ plist_dict_set_item(res_stream_node, "type", res_stream_type_node);
+ plist_array_append_item(res_streams_node, res_stream_node);
+
+ break;
+ }
+
+ default:
+ logger_log(conn->raop->logger, LOGGER_ERR, "SETUP tries to setup stream of unknown type %llu", type);
+ http_response_set_disconnect(response, 1);
+ break;
}
}
@@ -1030,7 +1082,7 @@ raop_handler_get_parameter(raop_conn_t *conn,
char volume[25] = "volume: 0.0\r\n";
if (conn->raop->callbacks.audio_set_client_volume) {
snprintf(volume, 25, "volume: %9.6f\r\n", conn->raop->callbacks.audio_set_client_volume(conn->raop->callbacks.cls));
- }
+ }
http_response_add_header(response, "Content-Type", "text/parameters");
*response_data = strdup(volume);
if (*response_data) {
@@ -1039,9 +1091,11 @@ raop_handler_get_parameter(raop_conn_t *conn,
return;
}
- for (next = current ; (datalen - (next - data) > 0) ; ++next)
- if (*next == '\r')
+ for (next = current ; (datalen - (next - data) > 0) ; ++next) {
+ if (*next == '\r') {
break;
+ }
+ }
if ((datalen - (next - data) >= 2) && !strncmp(next, "\r\n", 2)) {
if ((next - current) > 0) {
@@ -1081,8 +1135,8 @@ raop_handler_set_parameter(raop_conn_t *conn,
sscanf(datastr+8, "%f", &vol);
raop_rtp_set_volume(conn->raop_rtp, vol);
} else if ((datalen >= 10) && !strncmp(datastr, "progress: ", 10)) {
- unsigned int start, curr, end;
- sscanf(datastr+10, "%u/%u/%u", &start, &curr, &end);
+ uint32_t start, curr, end;
+ sscanf(datastr+10, "%"PRIu32"/%"PRIu32"/%"PRIu32, &start, &curr, &end);
raop_rtp_set_progress(conn->raop_rtp, start, curr, end);
}
} else if (!conn->raop_rtp) {
@@ -1106,6 +1160,25 @@ raop_handler_set_parameter(raop_conn_t *conn,
}
}
+static void
+raop_handler_audiomode(raop_conn_t *conn,
+ http_request_t *request, http_response_t *response,
+ char **response_data, int *response_datalen)
+{
+ const char *data = NULL;
+ char *audiomode = NULL;
+ int data_len;
+ data = http_request_get_data(request, &data_len);
+ plist_t req_root_node = NULL;
+ plist_from_bin(data, data_len, &req_root_node);
+ plist_t req_audiomode_node = plist_dict_get_item(req_root_node, "audioMode");
+ plist_get_string_val(req_audiomode_node, &audiomode);
+ /* not sure what should be done with this request: usually audioMode requested is "default" */
+ int log_level = (strstr(audiomode, "default") ? LOGGER_DEBUG : LOGGER_INFO);
+ logger_log(conn->raop->logger, log_level, "Unhandled RTSP request \"audioMode: %s\"", audiomode);
+ plist_mem_free(audiomode);
+ plist_free(req_root_node);
+}
static void
raop_handler_feedback(raop_conn_t *conn,
@@ -1164,11 +1237,6 @@ raop_handler_teardown(raop_conn_t *conn,
data = http_request_get_data(request, &data_len);
plist_t req_root_node = NULL;
plist_from_bin(data, data_len, &req_root_node);
- char * plist_xml;
- uint32_t plist_len;
- plist_to_xml(req_root_node, &plist_xml, &plist_len);
- logger_log(conn->raop->logger, LOGGER_DEBUG, "%s", plist_xml);
- free(plist_xml);
plist_t req_streams_node = plist_dict_get_item(req_root_node, "streams");
/* Process stream teardown requests */
if (PLIST_IS_ARRAY(req_streams_node)) {
@@ -1181,14 +1249,11 @@ raop_handler_teardown(raop_conn_t *conn,
if (val == 96) {
teardown_96 = true;
} else if (val == 110) {
- teardown_110 = true;
+ teardown_110 = true;
}
}
}
plist_free(req_root_node);
- if (conn->raop->callbacks.conn_teardown) {
- conn->raop->callbacks.conn_teardown(conn->raop->callbacks.cls, &teardown_96, &teardown_110);
- }
logger_log(conn->raop->logger, LOGGER_DEBUG, "TEARDOWN request, 96=%d, 110=%d", teardown_96, teardown_110);
http_response_add_header(response, "Connection", "close");
@@ -1218,7 +1283,10 @@ raop_handler_teardown(raop_conn_t *conn,
raop_rtp_mirror_destroy(conn->raop_rtp_mirror);
conn->raop_rtp_mirror = NULL;
}
- /* shut down any HLS connections */
- httpd_remove_connections_by_type(conn->raop->httpd, CONNECTION_TYPE_HLS);
- }
+ /* shut down any HLS connections */
+ httpd_remove_connections_by_type(conn->raop->httpd, CONNECTION_TYPE_HLS);
+ }
+ if (conn->raop->callbacks.conn_teardown) {
+ conn->raop->callbacks.conn_teardown(conn->raop->callbacks.cls, &teardown_96, &teardown_110);
+ }
}
diff --git a/lib/raop_ntp.c b/lib/raop_ntp.c
index 2264883..7d9c225 100644
--- a/lib/raop_ntp.c
+++ b/lib/raop_ntp.c
@@ -352,7 +352,7 @@ raop_ntp_thread(void *arg)
(double) t2 / SECOND_IN_NSECS, str);
free(str);
}
- // The iOS client device sends its time in seconds relative to an arbitrary Epoch (the last boot).
+ // The iOS client device sends its time in seconds relative to an arbitrary Epoch (the last boot).
// For a little bonus confusion, they add SECONDS_FROM_1900_TO_1970.
// This means we have to expect some rather huge offset, but its growth or shrink over time should be small.
diff --git a/lib/raop_rtp.c b/lib/raop_rtp.c
index f51eeab..d36b47a 100644
--- a/lib/raop_rtp.c
+++ b/lib/raop_rtp.c
@@ -93,9 +93,9 @@ struct raop_rtp_s {
int coverart_len;
char *dacp_id;
char *active_remote_header;
- unsigned int progress_start;
- unsigned int progress_curr;
- unsigned int progress_end;
+ uint32_t progress_start;
+ uint32_t progress_curr;
+ uint32_t progress_end;
int progress_changed;
int flush;
@@ -195,7 +195,6 @@ raop_rtp_init(logger_t *logger, raop_callbacks_t *callbacks, raop_ntp_t *ntp, co
return raop_rtp;
}
-
void
raop_rtp_destroy(raop_rtp_t *raop_rtp)
{
@@ -288,9 +287,9 @@ raop_rtp_process_events(raop_rtp_t *raop_rtp, void *cb_data)
int coverart_len;
char *dacp_id;
char *active_remote_header;
- unsigned int progress_start;
- unsigned int progress_curr;
- unsigned int progress_end;
+ uint32_t progress_start;
+ uint32_t progress_curr;
+ uint32_t progress_end;
int progress_changed;
assert(raop_rtp);
@@ -379,7 +378,7 @@ raop_rtp_process_events(raop_rtp_t *raop_rtp, void *cb_data)
if (progress_changed) {
if (raop_rtp->callbacks.audio_set_progress) {
- raop_rtp->callbacks.audio_set_progress(raop_rtp->callbacks.cls, progress_start, progress_curr, progress_end);
+ raop_rtp->callbacks.audio_set_progress(raop_rtp->callbacks.cls, &progress_start, &progress_curr, &progress_end);
}
}
return 0;
@@ -430,6 +429,19 @@ raop_rtp_thread_udp(void *arg)
logger_log(raop_rtp->logger, LOGGER_DEBUG, "raop_rtp start_time = %8.6f (raop_rtp audio)",
((double) raop_rtp->ntp_start_time) / SEC);
+ char type[8] = {'\0'};
+ switch (raop_rtp->ct) {
+ case 2:
+ snprintf(type, 8, "ALAC");
+ break;
+ case 8:
+ snprintf(type, 8, "AAC_ELD");
+ break;
+ default:
+ snprintf(type, 8, "TYPE=?");
+ break;
+ }
+
while(1) {
fd_set rfds;
struct timeval tv;
@@ -474,7 +486,7 @@ raop_rtp_thread_udp(void *arg)
raop_rtp->control_saddr_len = saddrlen;
got_remote_control_saddr = true;
}
- } else {
+ } else {
packetlen = recvfrom(raop_rtp->csock, (char *)packet, sizeof(packet), 0, NULL, NULL);
}
int type_c = packet[1] & ~0x80;
@@ -515,7 +527,7 @@ raop_rtp_thread_udp(void *arg)
} else {
client_ntp_sync_prev = raop_rtp->client_ntp_sync;
rtp_sync_prev = raop_rtp->rtp_sync;
- }
+ }
raop_rtp->rtp_sync = byteutils_get_int_be(packet, 4);
uint64_t sync_ntp_raw = byteutils_get_long_be(packet, 8);
raop_rtp->client_ntp_sync = raop_remote_timestamp_to_nano_seconds(raop_rtp->ntp, sync_ntp_raw);
@@ -564,20 +576,20 @@ raop_rtp_thread_udp(void *arg)
* three times; the secnum and rtp_timestamp increment according to the same pattern as
* AAC-ELD packets with audio content.*/
- /* When the ALAC audio stream starts, the initial packets are length-44 packets with
- * the same 32-byte encrypted payload which after decryption is the beginning of a
- * 32-byte ALAC packet, presumably with format information, but not actual audio data.
- * The secnum and rtp_timestamp in the packet header increment according to the same
- * pattern as ALAC packets with audio content */
+ /* When the ALAC audio stream starts, the initial packets are length-44 packets with
+ * the same 32-byte encrypted payload which after decryption is the beginning of a
+ * 32-byte ALAC packet, presumably with format information, but not actual audio data.
+ * The secnum and rtp_timestamp in the packet header increment according to the same
+ * pattern as ALAC packets with audio content */
- /* The first ALAC packet with data seems to be decoded just before the first sync event
- * so its dequeuing should be delayed until the first rtp sync has occurred */
+ /* The first ALAC packet with data seems to be decoded just before the first sync event
+ * so its dequeuing should be delayed until the first rtp sync has occurred */
- if (FD_ISSET(raop_rtp->dsock, &rfds)) {
- if (!raop_rtp->initial_sync && !video_arrival_offset) {
+ if (FD_ISSET(raop_rtp->dsock, &rfds)) {
+ if (!raop_rtp->initial_sync && !video_arrival_offset) {
video_arrival_offset = raop_ntp_get_video_arrival_offset(raop_rtp->ntp);
- }
+ }
//logger_log(raop_rtp->logger, LOGGER_INFO, "Would have data packet in queue");
// Receiving audio data here
saddrlen = sizeof(saddr);
@@ -594,30 +606,30 @@ raop_rtp_thread_udp(void *arg)
free (str);
}
continue;
- }
+ }
- if (!raop_rtp->initial_sync && raop_rtp->ct == 8 && video_arrival_offset) {
+ if (!raop_rtp->initial_sync && raop_rtp->ct == 8 && video_arrival_offset) {
/* estimate a fake initial remote timestamp for video synchronization with AAC audio before the first rtp sync */
- uint64_t ts = raop_ntp_get_local_time() - video_arrival_offset;
- double delay = DELAY_AAC;
- ts += (uint64_t) (delay * SEC);
- raop_rtp->client_ntp_sync = ts;
- raop_rtp->rtp_sync = byteutils_get_int_be(packet, 4);
- raop_rtp->initial_sync = true;
- }
+ uint64_t ts = raop_ntp_get_local_time() - video_arrival_offset;
+ double delay = DELAY_AAC;
+ ts += (uint64_t) (delay * SEC);
+ raop_rtp->client_ntp_sync = ts;
+ raop_rtp->rtp_sync = byteutils_get_int_be(packet, 4);
+ raop_rtp->initial_sync = true;
+ }
- if (packetlen == 16 && memcmp(packet + 12, no_data_marker, 4) == 0) {
+ if (packetlen == 12 ||(packetlen == 16 && memcmp(packet + 12, no_data_marker, 4) == 0)) {
/* this is a "no data" packet */
/* the first such packet could be used to provide the initial rtptime and seqnum formerly given in the RECORD request */
continue;
}
- if (raop_rtp->ct == 2 && packetlen == 44) continue; /* ignore the ALAC packets with format information only. */
+ if (raop_rtp->ct == 2 && packetlen == 44) continue; /* ignore the ALAC packets with format information only. */
int result = raop_buffer_enqueue(raop_rtp->buffer, packet, packetlen, 1);
assert(result >= 0);
- if (!raop_rtp->initial_sync) {
+ if (!raop_rtp->initial_sync) {
/* wait until the first sync before dequeing ALAC */
continue;
} else {
@@ -641,9 +653,9 @@ raop_rtp_thread_udp(void *arg)
uint64_t ntp_now = raop_ntp_get_local_time();
int64_t latency = (audio_data.ntp_time_local ? ((int64_t) ntp_now) - ((int64_t) audio_data.ntp_time_local) : 0);
logger_log(raop_rtp->logger, LOGGER_DEBUG,
- "raop_rtp audio: now = %8.6f, ntp = %8.6f, latency = %9.6f, ts = %8.6f, rtp_time=%u seqnum = %u",
+ "raop_rtp audio: now = %8.6f, ntp = %8.6f, latency = %9.6f, ts = %8.6f, rtp_time=%u seqnum = %5u %s %d",
(double) ntp_now / SEC, (double) audio_data.ntp_time_local / SEC, (double) latency / SEC,
- (double) audio_data.ntp_time_remote /SEC, rtp_timestamp, seqnum);
+ (double) audio_data.ntp_time_remote /SEC, rtp_timestamp, seqnum, type, payload_size);
}
raop_rtp->callbacks.audio_process(raop_rtp->callbacks.cls, raop_rtp->ntp, &audio_data);
@@ -793,7 +805,7 @@ raop_rtp_remote_control_id(raop_rtp_t *raop_rtp, const char *dacp_id, const char
}
void
-raop_rtp_set_progress(raop_rtp_t *raop_rtp, unsigned int start, unsigned int curr, unsigned int end)
+raop_rtp_set_progress(raop_rtp_t *raop_rtp, uint32_t start, uint32_t curr, uint32_t end)
{
assert(raop_rtp);
diff --git a/lib/raop_rtp.h b/lib/raop_rtp.h
index e1276bd..d9f5476 100644
--- a/lib/raop_rtp.h
+++ b/lib/raop_rtp.h
@@ -39,7 +39,7 @@ void raop_rtp_set_volume(raop_rtp_t *raop_rtp, float volume);
void raop_rtp_set_metadata(raop_rtp_t *raop_rtp, const char *data, int datalen);
void raop_rtp_set_coverart(raop_rtp_t *raop_rtp, const char *data, int datalen);
void raop_rtp_remote_control_id(raop_rtp_t *raop_rtp, const char *dacp_id, const char *active_remote_header);
-void raop_rtp_set_progress(raop_rtp_t *raop_rtp, unsigned int start, unsigned int curr, unsigned int end);
+void raop_rtp_set_progress(raop_rtp_t *raop_rtp, uint32_t start, uint32_t curr, uint32_t end);
void raop_rtp_flush(raop_rtp_t *raop_rtp, int next_seq);
void raop_rtp_stop(raop_rtp_t *raop_rtp);
int raop_rtp_is_running(raop_rtp_t *raop_rtp);
diff --git a/lib/raop_rtp_mirror.c b/lib/raop_rtp_mirror.c
index 82c55df..1ca548d 100644
--- a/lib/raop_rtp_mirror.c
+++ b/lib/raop_rtp_mirror.c
@@ -330,13 +330,13 @@ raop_rtp_mirror_thread(void *arg)
/*packet[0:3] contains the payload size */
int payload_size = byteutils_get_int(packet, 0);
char packet_description[13] = {0};
- char *p = packet_description;
+ char *p = packet_description;
int n = sizeof(packet_description);
- for (int i = 4; i < 8; i++) {
+ for (int i = 4; i < 8; i++) {
snprintf(p, n, "%2.2x ", (unsigned int) packet[i]);
n -= 3;
p += 3;
- }
+ }
ntp_timestamp_raw = byteutils_get_long(packet, 8);
ntp_timestamp_remote = raop_ntp_timestamp_to_nano_seconds(ntp_timestamp_raw, false);
if (first_packet) {
@@ -345,14 +345,14 @@ raop_rtp_mirror_thread(void *arg)
first_packet = false;
}
- /* packet[4] + packet[5] identify the payload type: values seen are: *
+ /* packet[4] + packet[5] identify the payload type: values seen are: *
* 0x00 0x00: encrypted packet containing a non-IDR type 1 VCL NAL unit *
* 0x00 0x10: encrypted packet containing an IDR type 5 VCL NAL unit *
* 0x01 0x00: unencrypted packet containing a type 7 SPS NAL + a type 8 PPS NAL unit *
* 0x02 0x00: unencrypted packet (old protocol) no payload, sent once every second *
* 0x05 0x00 unencrypted packet with a "streaming report", sent once per second. */
- /* packet[6] + packet[7] may list a payload "option": values seen are: *
+ /* packet[6] + packet[7] may list a payload "option": values seen are: *
* 0x00 0x00 : encrypted and "streaming report" packets *
* 0x1e 0x00 : old protocol (seen in AirMyPC) no-payload once-per-second packets *
* 0x16 0x01 : seen in most unencrypted h264 SPS+PPS packets *
@@ -395,7 +395,7 @@ raop_rtp_mirror_thread(void *arg)
break;
}
- switch (packet[4]) {
+ switch (packet[4]) {
case 0x00:
// Normal video data (VCL NAL)
@@ -409,13 +409,13 @@ raop_rtp_mirror_thread(void *arg)
uint64_t ntp_now = raop_ntp_get_local_time();
int64_t latency = (ntp_timestamp_local ? ((int64_t) ntp_now) - ((int64_t) ntp_timestamp_local) : 0);
logger_log(raop_rtp_mirror->logger, LOGGER_DEBUG,
- "raop_rtp video: now = %8.6f, ntp = %8.6f, latency = %9.6f, ts = %8.6f, %s %s",
+ "raop_rtp video: now = %8.6f, ntp = %8.6f, latency = %9.6f, ts = %8.6f, %s %s, size: %d",
(double) ntp_now / SEC, (double) ntp_timestamp_local / SEC, (double) latency / SEC,
- (double) ntp_timestamp_remote / SEC, packet_description, h265_video ? h265 : h264);
+ (double) ntp_timestamp_remote / SEC, packet_description, (h265_video ? h265 : h264), payload_size);
}
unsigned char* payload_out;
- unsigned char* payload_decrypted;
+ unsigned char* payload_decrypted;
/*
* nal_types:1 Coded non-partitioned slice of a non-IDR picture
* 5 Coded non-partitioned slice of an IDR picture
@@ -437,7 +437,7 @@ raop_rtp_mirror_thread(void *arg)
"raop_rtp_mirror: prepended sps_pps timestamp does not match timestamp of "
"video payload\n%llu\n%llu , discarding", ntp_timestamp_raw, ntp_timestamp_nal);
free (sps_pps);
- sps_pps = NULL;
+ sps_pps = NULL;
prepend_sps_pps = false;
}
@@ -447,12 +447,12 @@ raop_rtp_mirror_thread(void *arg)
payload_decrypted = payload_out + sps_pps_len;
memcpy(payload_out, sps_pps, sps_pps_len);
free (sps_pps);
- sps_pps = NULL;
+ sps_pps = NULL;
} else {
payload_out = (unsigned char*) malloc(payload_size);
payload_decrypted = payload_out;
}
- // Decrypt data
+ // Decrypt data: AES-CTR encryption/decryption does not change the size of the data
mirror_buffer_decrypt(raop_rtp_mirror->buffer, payload, payload_decrypted, payload_size);
// It seems the AirPlay protocol prepends NALs with their size, which we're replacing with the 4-byte
@@ -474,11 +474,11 @@ raop_rtp_mirror_thread(void *arg)
valid_data = false;
break;
}
- int nalu_type;
- if (h265_video) {
+ int nalu_type;
+ if (h265_video) {
nalu_type = payload_decrypted[nalu_size] & 0x7e >> 1;;
//logger_log(raop_rtp_mirror->logger, LOGGER_DEBUG," h265 video, NALU type %d, size %d", nalu_type, nc_len);
- } else {
+ } else {
nalu_type = payload_decrypted[nalu_size] & 0x1f;
int ref_idc = (payload_decrypted[nalu_size] >> 5);
switch (nalu_type) {
@@ -486,7 +486,7 @@ raop_rtp_mirror_thread(void *arg)
case 5: /*IDR, slice_layer_without_partitioning */
case 1: /*non-IDR, slice_layer_without_partitioning */
break;
- case 2: /* slice data partition A */
+ case 2: /* slice data partition A */
case 3: /* slice data partition B */
case 4: /* slice data partition C */
logger_log(raop_rtp_mirror->logger, LOGGER_INFO,
@@ -526,9 +526,9 @@ raop_rtp_mirror_thread(void *arg)
"unexpected non-VCL NAL unit: nalu_type = %d, ref_idc = %d, nalu_size = %d,"
"processed bytes %d, payloadsize = %d nalus_count = %d",
nalu_type, ref_idc, nc_len, nalu_size, payload_size, nalus_count);
- break;
- }
- }
+ break;
+ }
+ }
nalu_size += nc_len;
}
if (nalu_size != payload_size) valid_data = false;
@@ -540,7 +540,7 @@ raop_rtp_mirror_thread(void *arg)
payload_decrypted = NULL;
video_decode_struct video_data;
- video_data.is_h265 = h265_video;
+ video_data.is_h265 = h265_video;
video_data.ntp_time_local = ntp_timestamp_local;
video_data.ntp_time_remote = ntp_timestamp_remote;
video_data.nal_count = nalus_count; /*nal_count will be the number of nal units in the packet */
@@ -571,13 +571,13 @@ raop_rtp_mirror_thread(void *arg)
bytes 56-59 width
bytes 60-63 height
bytes 64-127 all 0x0
- */
+ */
// The information in the payload contains an SPS and a PPS NAL
// The sps_pps is not encrypted
logger_log(raop_rtp_mirror->logger, LOGGER_DEBUG, "\nReceived unencrypted codec packet from client:"
" payload_size %d header %s ts_client = %8.6f",
- payload_size, packet_description, (double) ntp_timestamp_remote / SEC);
+ payload_size, packet_description, (double) ntp_timestamp_remote / SEC);
if (packet[6] == 0x56 || packet[6] == 0x5e) {
logger_log(raop_rtp_mirror->logger, LOGGER_DEBUG, "This packet indicates video stream is stopping");
@@ -614,7 +614,7 @@ raop_rtp_mirror_thread(void *arg)
logger_log(raop_rtp_mirror->logger, LOGGER_DEBUG, "raop_rtp_mirror width_source = %f height_source = %f width = %f height = %f",
width_source, height_source, width, height);
- if (payload_size == 0) {
+ if (payload_size == 0) {
logger_log(raop_rtp_mirror->logger, LOGGER_ERR, "raop_rtp_mirror: received type 0x01 packet with no payload:\n"
"this indicates non-h264 video but Airplay features bit 42 (SupportsScreenMultiCodec) is not set\n"
"use startup option \"-h265\" to set this bit and support h265 (4K) video");
@@ -625,7 +625,7 @@ raop_rtp_mirror_thread(void *arg)
free(sps_pps);
sps_pps = NULL;
}
- /* test for a H265 VPS/SPS/PPS */
+ /* test for a H265 VPS/SPS/PPS */
unsigned char hvc1[] = { 0x68, 0x76, 0x63, 0x31 };
if (!memcmp(payload + 4, hvc1, 4)) {
@@ -677,7 +677,7 @@ raop_rtp_mirror_thread(void *arg)
break;
}
sps_size = byteutils_get_short_be(ptr, 3);
- ptr += 5;
+ ptr += 5;
sps = ptr;
if (logger_debug) {
char *str = utils_data_to_string(sps, sps_size, 16);
@@ -690,12 +690,12 @@ raop_rtp_mirror_thread(void *arg)
raop_rtp_mirror->callbacks.video_pause(raop_rtp_mirror->callbacks.cls);
break;
}
- pps_size = byteutils_get_short_be(ptr, 3);
+ pps_size = byteutils_get_short_be(ptr, 3);
ptr += 5;
pps = ptr;
if (logger_debug) {
char *str = utils_data_to_string(pps, pps_size, 16);
- logger_log(raop_rtp_mirror->logger, LOGGER_INFO, "h265 pps size %d\n%s",pps_size, str);
+ logger_log(raop_rtp_mirror->logger, LOGGER_INFO, "h265 pps size %d\n%s",pps_size, str);
free(str);
}
@@ -788,6 +788,7 @@ raop_rtp_mirror_thread(void *arg)
logger_log(raop_rtp_mirror->logger, LOGGER_DEBUG, "\nReceived old-protocol once-per-second packet from client:"
" payload_size %d header %s ts_raw = %llu", payload_size, packet_description, ntp_timestamp_raw);
/* "old protocol" (used by AirMyPC), rest of 128-byte packet is empty */
+ break;
case 0x05:
logger_log(raop_rtp_mirror->logger, LOGGER_DEBUG, "\nReceived video streaming performance info packet from client:"
" payload_size %d header %s ts_raw = %llu", payload_size, packet_description, ntp_timestamp_raw);
@@ -795,14 +796,14 @@ raop_rtp_mirror_thread(void *arg)
* Sometimes (e.g, when the client has a locked screen), there is a 25kB trailer attached to the packet. *
* This 25000 Byte trailer with unidentified content seems to be the same data each time it is sent. */
- if (payload_size && raop_rtp_mirror->show_client_FPS_data) {
+ if (payload_size) {
//char *str = utils_data_to_string(packet, 128, 16);
//logger_log(raop_rtp_mirror->logger, LOGGER_WARNING, "type 5 video packet header:\n%s", str);
//free (str);
int plist_size = payload_size;
if (payload_size > 25000) {
- plist_size = payload_size - 25000;
+ plist_size = payload_size - 25000;
if (logger_debug) {
char *str = utils_data_to_string(payload + plist_size, 16, 16);
logger_log(raop_rtp_mirror->logger, LOGGER_DEBUG,
@@ -815,9 +816,17 @@ raop_rtp_mirror_thread(void *arg)
uint32_t plist_len;
plist_t root_node = NULL;
plist_from_bin((char *) payload, plist_size, &root_node);
- plist_to_xml(root_node, &plist_xml, &plist_len);
- logger_log(raop_rtp_mirror->logger, LOGGER_INFO, "%s", plist_xml);
- free(plist_xml);
+ if (raop_rtp_mirror->callbacks.mirror_video_activity) {
+ double txusage = 0.0;
+ plist_t tx_usage_avg_node = plist_dict_get_item(root_node, "txUsageAvg");
+ plist_get_real_val(tx_usage_avg_node, &txusage);
+ raop_rtp_mirror->callbacks.mirror_video_activity(raop_rtp_mirror->callbacks.cls, &txusage);
+ }
+ if (raop_rtp_mirror->show_client_FPS_data) {
+ plist_to_xml(root_node, &plist_xml, &plist_len);
+ logger_log(raop_rtp_mirror->logger, LOGGER_INFO, "%s", plist_xml);
+ free(plist_xml);
+ }
}
}
break;
@@ -848,7 +857,7 @@ raop_rtp_mirror_thread(void *arg)
logger_log(raop_rtp_mirror->logger, LOGGER_DEBUG, "raop_rtp_mirror exiting TCP thread");
if (conn_reset&& raop_rtp_mirror->callbacks.conn_reset) {
- raop_rtp_mirror->callbacks.conn_reset(raop_rtp_mirror->callbacks.cls, 1);
+ raop_rtp_mirror->callbacks.conn_reset(raop_rtp_mirror->callbacks.cls, 1);
}
if (unsupported_codec) {
diff --git a/lib/stream.h b/lib/stream.h
index 6905dcd..c544886 100644
--- a/lib/stream.h
+++ b/lib/stream.h
@@ -37,7 +37,7 @@ typedef struct {
int sync_status;
uint64_t ntp_time_local;
uint64_t ntp_time_remote;
- uint64_t rtp_time;
+ uint32_t rtp_time;
unsigned short seqnum;
} audio_decode_struct;
diff --git a/lib/utils.c b/lib/utils.c
index ce2623e..f354b66 100644
--- a/lib/utils.c
+++ b/lib/utils.c
@@ -312,20 +312,20 @@ char *utils_strip_data_from_plist_xml(char *plist_xml) {
nchars = eol + 1 - ptr1;
memcpy(ptr2, ptr1, nchars);
ptr2 += nchars;
- ptr1 += nchars;
+ ptr1 += nchars;
end = strstr(ptr1, "");
- assert(end);
+ assert(end);
count = 0;
do {
eol_data = eol;
eol = strchr(eol + 1, '\n');
count++;
} while (eol < end);
- count--; // last '\n' counted ends the first non-data line (contains "")
- if (count > 1) {
+ count--; // last '\n' counted ends the first non-data line (contains "")
+ if (count > 1) {
snprintf(line, sizeof(line), " (%d lines data omitted, 64 chars/line)\n", count);
nchars = strlen(line);
- memcpy(ptr2, line, nchars);
+ memcpy(ptr2, line, nchars);
ptr2 += nchars;
ptr1 = eol_data + 1;
} else {
diff --git a/renderers/audio_renderer.c b/renderers/audio_renderer.c
index c390e68..9a669d3 100644
--- a/renderers/audio_renderer.c
+++ b/renderers/audio_renderer.c
@@ -45,6 +45,7 @@ typedef struct audio_renderer_s {
GstElement *appsrc;
GstElement *pipeline;
GstElement *volume;
+ GstBus *bus;
unsigned char ct;
} audio_renderer_t ;
static audio_renderer_t *renderer_type[NFORMATS];
@@ -102,15 +103,15 @@ static gboolean check_plugin_feature (const gchar *needed_feature)
plugin_feature = gst_registry_find_feature (registry, needed_feature, GST_TYPE_ELEMENT_FACTORY);
if (!plugin_feature) {
- g_print ("Required gstreamer libav plugin feature '%s' not found:\n\n"
- "This may be missing because the FFmpeg package used by GStreamer-1.x-libav is incomplete.\n"
- "(Some distributions provide an incomplete FFmpeg due to License or Patent issues:\n"
- "in such cases a complete version for that distribution is usually made available elsewhere)\n",
- needed_feature);
- ret = FALSE;
+ g_print ("Required gstreamer libav plugin feature '%s' not found:\n\n"
+ "This may be missing because the FFmpeg package used by GStreamer-1.x-libav is incomplete.\n"
+ "(Some distributions provide an incomplete FFmpeg due to License or Patent issues:\n"
+ "in such cases a complete version for that distribution is usually made available elsewhere)\n",
+ needed_feature);
+ ret = FALSE;
} else {
- gst_object_unref (plugin_feature);
- plugin_feature = NULL;
+ gst_object_unref (plugin_feature);
+ plugin_feature = NULL;
}
if (ret == FALSE) {
g_print ("\nif the plugin feature is installed, but not found, your gstreamer registry may have been corrupted.\n"
@@ -186,7 +187,7 @@ void audio_renderer_init(logger_t *render_logger, const char* audiosink, const b
g_assert (renderer_type[i]->pipeline);
gst_pipeline_use_clock(GST_PIPELINE_CAST(renderer_type[i]->pipeline), clock);
-
+ renderer_type[i]->bus = gst_element_get_bus(renderer_type[i]->pipeline);
renderer_type[i]->appsrc = gst_bin_get_by_name (GST_BIN (renderer_type[i]->pipeline), "audio_source");
renderer_type[i]->volume = gst_bin_get_by_name (GST_BIN (renderer_type[i]->pipeline), "volume");
switch (i) {
@@ -367,12 +368,52 @@ void audio_renderer_flush() {
void audio_renderer_destroy() {
audio_renderer_stop();
for (int i = 0; i < NFORMATS ; i++ ) {
+ gst_object_unref (renderer_type[i]->bus);
+ renderer_type[i]->bus = NULL;
gst_object_unref (renderer_type[i]->volume);
- renderer_type[i]->volume = NULL;
+ renderer_type[i]->volume = NULL;
gst_object_unref (renderer_type[i]->appsrc);
renderer_type[i]->appsrc = NULL;
- gst_object_unref (renderer_type[i]->pipeline);
+ gst_object_unref (renderer_type[i]->pipeline);
renderer_type[i]->pipeline = NULL;
free(renderer_type[i]);
}
}
+
+static gboolean gstreamer_audio_pipeline_bus_callback(GstBus *bus, GstMessage *message, void *loop) {
+ switch (GST_MESSAGE_TYPE(message)) {
+ case GST_MESSAGE_ERROR: {
+ GError *err;
+ gchar *debug;
+ gst_message_parse_error (message, &err, &debug);
+ logger_log(logger, LOGGER_INFO, "GStreamer error (audio): %s %s", GST_MESSAGE_SRC_NAME(message),err->message);
+ g_error_free(err);
+ g_free(debug);
+ if (renderer->appsrc) {
+ gst_app_src_end_of_stream (GST_APP_SRC(renderer->appsrc));
+ }
+ gst_bus_set_flushing(bus, TRUE);
+ gst_element_set_state (renderer->pipeline, GST_STATE_READY);
+ g_main_loop_quit( (GMainLoop *) loop);
+ break;
+ }
+ case GST_MESSAGE_EOS:
+ logger_log(logger, LOGGER_INFO, "GStreamer: End-Of-Stream (audio)");
+ break;
+ case GST_MESSAGE_ELEMENT:
+ // many "level" messages may be sent
+ break;
+ default:
+ /* unhandled message */
+ logger_log(logger, LOGGER_DEBUG,"GStreamer unhandled audio bus message: src = %s type = %s",
+ GST_MESSAGE_SRC_NAME(message), GST_MESSAGE_TYPE_NAME(message));
+ break;
+ }
+ return TRUE;
+}
+
+unsigned int audio_renderer_listen(void *loop, int id) {
+ g_assert(id >= 0 && id < NFORMATS);
+ return (unsigned int) gst_bus_add_watch(renderer_type[id]->bus,(GstBusFunc)
+ gstreamer_audio_pipeline_bus_callback, (gpointer) loop);
+}
diff --git a/renderers/audio_renderer.h b/renderers/audio_renderer.h
index 232da28..d981e20 100644
--- a/renderers/audio_renderer.h
+++ b/renderers/audio_renderer.h
@@ -40,7 +40,7 @@ void audio_renderer_render_buffer(unsigned char* data, int *data_len, unsigned s
void audio_renderer_set_volume(double volume);
void audio_renderer_flush();
void audio_renderer_destroy();
-
+unsigned int audio_renderer_listen(void *loop, int id);
#ifdef __cplusplus
}
#endif
diff --git a/renderers/video_renderer.c b/renderers/video_renderer.c
index c701c88..96ce25e 100644
--- a/renderers/video_renderer.c
+++ b/renderers/video_renderer.c
@@ -54,7 +54,10 @@ static gboolean hls_seek_enabled;
static gboolean hls_playing;
static gboolean hls_buffer_empty;
static gboolean hls_buffer_full;
-
+static int type_264;
+static int type_265;
+static int type_hls;
+static int type_jpeg;
typedef enum {
//GST_PLAY_FLAG_VIDEO = (1 << 0),
@@ -178,7 +181,7 @@ void video_renderer_size(float *f_width_source, float *f_height_source, float *f
}
GstElement *make_video_sink(const char *videosink, const char *videosink_options) {
- /* used to build a videosink for playbin, using the user-specified string "videosink" */
+ /* used to build a videosink for playbin, using the user-specified string "videosink" */
GstElement *video_sink = gst_element_factory_make(videosink, "videosink");
if (!video_sink) {
return NULL;
@@ -211,19 +214,20 @@ GstElement *make_video_sink(const char *videosink, const char *videosink_options
pval++;
const gchar *property_name = (const gchar *) token;
const gchar *value = (const gchar *) pval;
- g_print("playbin_videosink property: \"%s\" \"%s\"\n", property_name, value);
- gst_util_set_object_arg(G_OBJECT (video_sink), property_name, value);
+ g_print("playbin_videosink property: \"%s\" \"%s\"\n", property_name, value);
+ gst_util_set_object_arg(G_OBJECT (video_sink), property_name, value);
}
}
free(options);
return video_sink;
}
-void video_renderer_init(logger_t *render_logger, const char *server_name, videoflip_t videoflip[2], const char *parser,
+void video_renderer_init(logger_t *render_logger, const char *server_name, videoflip_t videoflip[2], const char *parser, const char * rtp_pipeline,
const char *decoder, const char *converter, const char *videosink, const char *videosink_options,
- bool initial_fullscreen, bool video_sync, bool h265_support, guint playbin_version, const char *uri) {
+ bool initial_fullscreen, bool video_sync, bool h265_support, bool coverart_support, guint playbin_version, const char *uri) {
GError *error = NULL;
GstCaps *caps = NULL;
+ bool rtp = (bool) strlen(rtp_pipeline);
hls_video = (uri != NULL);
/* videosink choices that are auto */
auto_videosink = (strstr(videosink, "autovideosink") || strstr(videosink, "fpsdisplaysink"));
@@ -238,7 +242,10 @@ void video_renderer_init(logger_t *render_logger, const char *server_name, vide
hls_duration = -1;
hls_buffer_empty = TRUE;
hls_buffer_empty = FALSE;
-
+ type_hls = -1;
+ type_264 = -1;
+ type_265 = -1;
+ type_jpeg = -1;
/* this call to g_set_application_name makes server_name appear in the X11 display window title bar, */
/* (instead of the program name uxplay taken from (argv[0]). It is only set one time. */
@@ -246,18 +253,22 @@ void video_renderer_init(logger_t *render_logger, const char *server_name, vide
const gchar *appname = g_get_application_name();
if (!appname || strcmp(appname,server_name)) g_set_application_name(server_name);
appname = NULL;
-
+ n_renderers = 1;
/* the renderer for hls video will only be built if a HLS uri is provided in
* the call to video_renderer_init, in which case the h264/h265 mirror-mode and jpeg
* audio-mode renderers will not be built. This is because it appears that we cannot
* put playbin into GST_STATE_READY before knowing the uri (?), so cannot use a
* unified renderer structure with h264, h265, jpeg and hls */
if (hls_video) {
- n_renderers = 1;
- /* renderer[0]: playbin (hls) */
+ type_hls = 0;
} else {
- n_renderers = h265_support ? 3 : 2;
- /* renderer[0]: jpeg; [1]: h264; [2]: h265 */
+ type_264 = 0;
+ if (h265_support) {
+ type_265 = n_renderers++;
+ }
+ if (coverart_support) {
+ type_jpeg = n_renderers++;
+ }
}
g_assert (n_renderers <= NCODECS);
for (int i = 0; i < n_renderers; i++) {
@@ -267,6 +278,7 @@ void video_renderer_init(logger_t *render_logger, const char *server_name, vide
renderer_type[i]->autovideo = auto_videosink;
renderer_type[i]->id = i;
renderer_type[i]->bus = NULL;
+ renderer_type[i]->appsrc = NULL;
if (hls_video) {
/* use playbin3 to play HLS video: replace "playbin3" by "playbin" to use playbin2 */
switch (playbin_version) {
@@ -282,8 +294,7 @@ void video_renderer_init(logger_t *render_logger, const char *server_name, vide
}
logger_log(logger, LOGGER_INFO, "Will use GStreamer playbin version %u to play HLS streamed video", playbin_version);
g_assert(renderer_type[i]->pipeline);
- renderer_type[i]->appsrc = NULL;
- renderer_type[i]->codec = hls;
+ renderer_type[i]->codec = hls;
/* if we are not using an autovideosink, build a videosink based on the string "videosink" */
if (!auto_videosink) {
GstElement *playbin_videosink = make_video_sink(videosink, videosink_options);
@@ -297,59 +308,61 @@ void video_renderer_init(logger_t *render_logger, const char *server_name, vide
gint flags;
g_object_get(renderer_type[i]->pipeline, "flags", &flags, NULL);
flags |= GST_PLAY_FLAG_DOWNLOAD;
- flags |= GST_PLAY_FLAG_BUFFERING; // set by default in playbin3, but not in playbin2; is it needed?
+ flags |= GST_PLAY_FLAG_BUFFERING; // set by default in playbin3, but not in playbin2; is it needed?
g_object_set(renderer_type[i]->pipeline, "flags", flags, NULL);
- g_object_set (G_OBJECT (renderer_type[i]->pipeline), "uri", uri, NULL);
+ g_object_set (G_OBJECT (renderer_type[i]->pipeline), "uri", uri, NULL);
} else {
bool jpeg_pipeline = false;
- switch (i) {
- case 0:
+ if (i == type_264) {
+ renderer_type[i]->codec = h264;
+ caps = gst_caps_from_string(h264_caps);
+ } else if (i == type_265) {
+ renderer_type[i]->codec = h265;
+ caps = gst_caps_from_string(h265_caps);
+ } else if (i == type_jpeg) {
jpeg_pipeline = true;
renderer_type[i]->codec = jpeg;
caps = gst_caps_from_string(jpeg_caps);
- break;
- case 1:
- renderer_type[i]->codec = h264;
- caps = gst_caps_from_string(h264_caps);
- break;
- case 2:
- renderer_type[i]->codec = h265;
- caps = gst_caps_from_string(h265_caps);
- break;
- default:
+ } else {
g_assert(0);
}
GString *launch = g_string_new("appsrc name=video_source ! ");
- if (jpeg_pipeline) {
+ if (jpeg_pipeline) {
g_string_append(launch, "jpegdec ");
- } else {
+ } else {
g_string_append(launch, "queue ! ");
g_string_append(launch, parser);
g_string_append(launch, " ! ");
- g_string_append(launch, decoder);
+ if (!rtp) {
+ g_string_append(launch, decoder);
+ } else {
+ g_string_append(launch, "rtph264pay ");
+ g_string_append(launch, rtp_pipeline);
+ }
}
- g_string_append(launch, " ! ");
- append_videoflip(launch, &videoflip[0], &videoflip[1]);
- g_string_append(launch, converter);
- g_string_append(launch, " ! ");
- g_string_append(launch, "videoscale ! ");
- if (jpeg_pipeline) {
- g_string_append(launch, " imagefreeze allow-replace=TRUE ! ");
+ if (!rtp || jpeg_pipeline) {
+ g_string_append(launch, " ! ");
+ append_videoflip(launch, &videoflip[0], &videoflip[1]);
+ g_string_append(launch, converter);
+ g_string_append(launch, " ! ");
+ g_string_append(launch, "videoscale ! ");
+ if (jpeg_pipeline) {
+ g_string_append(launch, " imagefreeze allow-replace=TRUE ! ");
+ }
+ g_string_append(launch, videosink);
+ g_string_append(launch, " name=");
+ g_string_append(launch, videosink);
+ g_string_append(launch, "_");
+ g_string_append(launch, renderer_type[i]->codec);
+ g_string_append(launch, videosink_options);
+ if (video_sync && !jpeg_pipeline) {
+ g_string_append(launch, " sync=true");
+ sync = true;
+ } else {
+ g_string_append(launch, " sync=false");
+ sync = false;
+ }
}
- g_string_append(launch, videosink);
- g_string_append(launch, " name=");
- g_string_append(launch, videosink);
- g_string_append(launch, "_");
- g_string_append(launch, renderer_type[i]->codec);
- g_string_append(launch, videosink_options);
- if (video_sync && !jpeg_pipeline) {
- g_string_append(launch, " sync=true");
- sync = true;
- } else {
- g_string_append(launch, " sync=false");
- sync = false;
- }
-
if (!strcmp(renderer_type[i]->codec, h264)) {
char *pos = launch->str;
while ((pos = strstr(pos,h265))){
@@ -370,14 +383,13 @@ void video_renderer_init(logger_t *render_logger, const char *server_name, vide
logger_log(logger, LOGGER_ERR, "GStreamer gst_parse_launch failed to create video pipeline %d\n"
"*** error message from gst_parse_launch was:\n%s\n"
"launch string parsed was \n[%s]", i + 1, error->message, launch->str);
- if (strstr(error->message, "no element")) {
+ if (strstr(error->message, "no element")) {
logger_log(logger, LOGGER_ERR, "This error usually means that a uxplay option was mistyped\n"
" or some requested part of GStreamer is not installed\n");
}
g_clear_error (&error);
}
g_assert (renderer_type[i]->pipeline);
-
GstClock *clock = gst_system_clock_obtain();
g_object_set(clock, "clock-type", GST_CLOCK_TYPE_REALTIME, NULL);
gst_pipeline_use_clock(GST_PIPELINE_CAST(renderer_type[i]->pipeline), clock);
@@ -386,7 +398,7 @@ void video_renderer_init(logger_t *render_logger, const char *server_name, vide
g_object_set(renderer_type[i]->appsrc, "caps", caps, "stream-type", 0, "is-live", TRUE, "format", GST_FORMAT_TIME, NULL);
g_string_free(launch, TRUE);
gst_caps_unref(caps);
- gst_object_unref(clock);
+ gst_object_unref(clock);
}
#ifdef X_DISPLAY_FIX
use_x11 = (strstr(videosink, "xvimagesink") || strstr(videosink, "ximagesink") || auto_videosink);
@@ -395,9 +407,9 @@ void video_renderer_init(logger_t *render_logger, const char *server_name, vide
renderer_type[i]->gst_window = NULL;
renderer_type[i]->use_x11 = false;
X11_search_attempts = 0;
- /* setting char *x11_display_name to NULL means the value is taken from $DISPLAY in the environment
+ /* setting char *x11_display_name to NULL means the value is taken from $DISPLAY in the environment
* (a uxplay option to specify a different value is possible) */
- char *x11_display_name = NULL;
+ char *x11_display_name = NULL;
if (use_x11) {
if (i == 0) {
renderer_type[0]->gst_window = (X11_Window_t *) calloc(1, sizeof(X11_Window_t));
@@ -417,19 +429,22 @@ void video_renderer_init(logger_t *render_logger, const char *server_name, vide
}
}
#endif
+ renderer_type[i]->bus = gst_element_get_bus(renderer_type[i]->pipeline);
gst_element_set_state (renderer_type[i]->pipeline, GST_STATE_READY);
GstState state;
- if (gst_element_get_state (renderer_type[i]->pipeline, &state, NULL, 100 * GST_MSECOND)) {
- if (state == GST_STATE_READY) {
- logger_log(logger, LOGGER_DEBUG, "Initialized GStreamer video renderer %d", i + 1);
- if (hls_video && i == 0) {
- renderer = renderer_type[i];
- }
- } else {
- logger_log(logger, LOGGER_ERR, "Failed to initialize GStreamer video renderer %d", i + 1);
+ GstStateChangeReturn ret = gst_element_get_state (renderer_type[i]->pipeline, &state, NULL, 100 * GST_MSECOND);
+ if (ret == GST_STATE_CHANGE_SUCCESS) {
+ logger_log(logger, LOGGER_DEBUG, "Initialized GStreamer video renderer %d", i + 1);
+ if (hls_video && i == 0) {
+ renderer = renderer_type[i];
}
} else {
logger_log(logger, LOGGER_ERR, "Failed to initialize GStreamer video renderer %d", i + 1);
+ logger_log(logger, LOGGER_INFO, "\nPerhaps your GStreamer installation is missing some required plugins,"
+ "\nor your choices of video options (-vs -vd -vc -fs etc.) are incompatible on"
+ "\nthis computer architecture. (An example: kmssink with fullscreen option -fs"
+ "\nmay work on some systems, but fail on others)");
+ exit(1);
}
}
}
@@ -461,20 +476,18 @@ void video_renderer_start() {
GstState state;
const gchar *state_name;
if (hls_video) {
- renderer->bus = gst_element_get_bus(renderer->pipeline);
gst_element_set_state (renderer->pipeline, GST_STATE_PAUSED);
gst_element_get_state(renderer->pipeline, &state, NULL, 1000 * GST_MSECOND);
- state_name= gst_element_state_get_name(state);
+ state_name = gst_element_state_get_name(state);
logger_log(logger, LOGGER_DEBUG, "video renderer_start: state %s", state_name);
return;
}
- /* when not hls, start both h264 and h265 pipelines; will shut down the "wrong" one when we know the codec */
+ /* when not hls, start both h264 and h265 pipelines; will shut down the "wrong" one when we know the codec */
for (int i = 0; i < n_renderers; i++) {
- renderer_type[i]->bus = gst_element_get_bus(renderer_type[i]->pipeline);
gst_element_set_state (renderer_type[i]->pipeline, GST_STATE_PAUSED);
- gst_element_get_state(renderer_type[i]->pipeline, &state, NULL, 1000 * GST_MSECOND);
- state_name= gst_element_state_get_name(state);
- logger_log(logger, LOGGER_DEBUG, "video renderer_start: renderer %d state %s", i, state_name);
+ gst_element_get_state(renderer_type[i]->pipeline, &state, NULL, 1000 * GST_MSECOND);
+ state_name = gst_element_state_get_name(state);
+ logger_log(logger, LOGGER_DEBUG, "video renderer_start: renderer %d %p state %s", i, renderer_type[i], state_name);
}
renderer = NULL;
first_packet = true;
@@ -503,8 +516,53 @@ bool waiting_for_x11_window() {
return false;
}
+/* use this to cycle the jpeg renderer to remove expired coverart when no new coverart has replaced it */
+int video_renderer_cycle() {
+ if (!renderer || !strstr(renderer->codec, jpeg)) {
+ return -1;
+ }
+ GstState state, pending_state, target_state;
+ GstStateChangeReturn ret;
+ gst_element_get_state(renderer->pipeline, &state, NULL, 0);
+ logger_log(logger, LOGGER_DEBUG, "renderer_cycle renderer %p: initial pipeline state is %s", renderer,
+ gst_element_state_get_name(state));
+
+ for (int i = 0 ; i < 2; i++) {
+ int count = 0;
+ if (i == 0 ) {
+ target_state = GST_STATE_NULL;
+ video_renderer_stop();
+ } else {
+ target_state = GST_STATE_PLAYING;
+ gst_element_set_state (renderer->pipeline, target_state);
+ }
+ while (state != target_state) {
+ ret = gst_element_get_state(renderer->pipeline, &state, &pending_state, 1000 * GST_MSECOND);
+ if (ret == GST_STATE_CHANGE_SUCCESS) {
+ logger_log(logger, LOGGER_DEBUG, "current pipeline state is %s", gst_element_state_get_name(state));
+ if (pending_state != GST_STATE_VOID_PENDING) {
+ logger_log(logger, LOGGER_DEBUG, "pending pipeline state is %s", gst_element_state_get_name(pending_state));
+ }
+ } else if (ret == GST_STATE_CHANGE_FAILURE) {
+ logger_log(logger, LOGGER_ERR, "pipeline %s: state change to %s failed", renderer->codec, gst_element_state_get_name(target_state));
+ count++;
+ if (count > 10) {
+ return -1;
+ }
+ } else if (ret == GST_STATE_CHANGE_ASYNC) {
+ logger_log(logger, LOGGER_DEBUG, "state change to %s is asynchronous, waiting for completion ...",
+ gst_element_state_get_name(target_state));
+ }
+ }
+ }
+ return 0;
+}
+
void video_renderer_display_jpeg(const void *data, int *data_len) {
GstBuffer *buffer;
+ if (type_jpeg == -1) {
+ return;
+ }
if (renderer && !strcmp(renderer->codec, jpeg)) {
buffer = gst_buffer_new_allocate(NULL, *data_len, NULL);
g_assert(buffer != NULL);
@@ -539,6 +597,10 @@ uint64_t video_renderer_render_buffer(unsigned char* data, int *data_len, int *n
logger_log(logger, LOGGER_INFO, "Begin streaming to GStreamer video pipeline");
first_packet = false;
}
+ if (!renderer || !(renderer->appsrc)) {
+ logger_log(logger, LOGGER_DEBUG, "*** no video renderer found");
+ return 0;
+ }
buffer = gst_buffer_new_allocate(NULL, *data_len, NULL);
g_assert(buffer != NULL);
//g_print("video latency %8.6f\n", (double) latency / SECOND_IN_NSECS);
@@ -580,25 +642,27 @@ void video_renderer_stop() {
static void video_renderer_destroy_instance(video_renderer_t *renderer) {
if (renderer) {
- logger_log(logger, LOGGER_DEBUG,"destroying renderer instance %p", renderer);
+ logger_log(logger, LOGGER_DEBUG,"destroying renderer instance %p codec=%s ", renderer, renderer->codec);
GstState state;
- GstStateChangeReturn ret;
+ GstStateChangeReturn ret;
gst_element_get_state(renderer->pipeline, &state, NULL, 100 * GST_MSECOND);
- logger_log(logger, LOGGER_DEBUG,"pipeline state is %s", gst_element_state_get_name(state));
+ logger_log(logger, LOGGER_DEBUG,"pipeline state is %s", gst_element_state_get_name(state));
if (state != GST_STATE_NULL) {
if (!hls_video) {
gst_app_src_end_of_stream (GST_APP_SRC(renderer->appsrc));
}
ret = gst_element_set_state (renderer->pipeline, GST_STATE_NULL);
- logger_log(logger, LOGGER_DEBUG,"pipeline_state_change_return: %s",
- gst_element_state_change_return_get_name(ret));
- gst_element_get_state(renderer->pipeline, NULL, NULL, 1000 * GST_MSECOND);
+ logger_log(logger, LOGGER_DEBUG,"pipeline_state_change_return: %s",
+ gst_element_state_change_return_get_name(ret));
+ gst_element_get_state(renderer->pipeline, &state, NULL, 1000 * GST_MSECOND);
+ logger_log(logger, LOGGER_DEBUG,"pipeline state is %s", gst_element_state_get_name(state));
+ }
+ if (renderer->appsrc) {
+ gst_object_unref (renderer->appsrc);
+ renderer->appsrc = NULL;
}
gst_object_unref(renderer->bus);
- if (renderer->appsrc) {
- gst_object_unref (renderer->appsrc);
- }
- gst_object_unref (renderer->pipeline);
+ gst_object_unref(renderer->pipeline);
#ifdef X_DISPLAY_FIX
if (renderer->gst_window) {
free(renderer->gst_window);
@@ -607,6 +671,7 @@ static void video_renderer_destroy_instance(video_renderer_t *renderer) {
#endif
free (renderer);
renderer = NULL;
+ logger_log(logger, LOGGER_DEBUG,"renderer destroyed\n");
}
}
@@ -619,35 +684,35 @@ void video_renderer_destroy() {
}
static void get_stream_status_name(GstStreamStatusType type, char *name, size_t len) {
- switch (type) {
- case GST_STREAM_STATUS_TYPE_CREATE:
- strncpy(name, "CREATE", len);
- return;
- case GST_STREAM_STATUS_TYPE_ENTER:
- strncpy(name, "ENTER", len);
- return;
- case GST_STREAM_STATUS_TYPE_LEAVE:
- strncpy(name, "LEAVE", len);
- return;
- case GST_STREAM_STATUS_TYPE_DESTROY:
- strncpy(name, "DESTROY", len);
- return;
- case GST_STREAM_STATUS_TYPE_START:
- strncpy(name, "START", len);
- return;
- case GST_STREAM_STATUS_TYPE_PAUSE:
- strncpy(name, "PAUSE", len);
- return;
- case GST_STREAM_STATUS_TYPE_STOP:
- strncpy(name, "STOP", len);
- return;
- default:
- strncpy(name, "", len);
- return;
- }
+ switch (type) {
+ case GST_STREAM_STATUS_TYPE_CREATE:
+ strncpy(name, "CREATE", len);
+ return;
+ case GST_STREAM_STATUS_TYPE_ENTER:
+ strncpy(name, "ENTER", len);
+ return;
+ case GST_STREAM_STATUS_TYPE_LEAVE:
+ strncpy(name, "LEAVE", len);
+ return;
+ case GST_STREAM_STATUS_TYPE_DESTROY:
+ strncpy(name, "DESTROY", len);
+ return;
+ case GST_STREAM_STATUS_TYPE_START:
+ strncpy(name, "START", len);
+ return;
+ case GST_STREAM_STATUS_TYPE_PAUSE:
+ strncpy(name, "PAUSE", len);
+ return;
+ case GST_STREAM_STATUS_TYPE_STOP:
+ strncpy(name, "STOP", len);
+ return;
+ default:
+ strncpy(name, "", len);
+ return;
+ }
}
-gboolean gstreamer_pipeline_bus_callback(GstBus *bus, GstMessage *message, void *loop) {
+static gboolean gstreamer_video_pipeline_bus_callback(GstBus *bus, GstMessage *message, void *loop) {
GstState old_state, new_state;
const gchar no_state[] = "";
const gchar *old_state_name = no_state, *new_state_name = no_state;
@@ -678,7 +743,7 @@ gboolean gstreamer_pipeline_bus_callback(GstBus *bus, GstMessage *message, void
if (logger_debug) {
gchar *name = NULL;
- GstElement *element = NULL;
+ GstElement *element = NULL;
gchar type_name[8] = { 0 };
if (GST_MESSAGE_TYPE(message) == GST_MESSAGE_STREAM_STATUS) {
GstStreamStatusType type;
@@ -699,9 +764,9 @@ gboolean gstreamer_pipeline_bus_callback(GstBus *bus, GstMessage *message, void
g_print("GStreamer %s bus message %s %s %s %s\n", renderer_type[type]->codec,
GST_MESSAGE_SRC_NAME(message), GST_MESSAGE_TYPE_NAME(message), old_state_name, new_state_name);
}
- if (name) {
- g_free(name);
- }
+ if (name) {
+ g_free(name);
+ }
}
/* monitor hls video position until seek to hls_start_position is achieved */
@@ -711,21 +776,21 @@ gboolean gstreamer_pipeline_bus_callback(GstBus *bus, GstMessage *message, void
if (!GST_CLOCK_TIME_IS_VALID(hls_duration)) {
gst_element_query_duration (renderer->pipeline, GST_FORMAT_TIME, &hls_duration);
}
- gst_element_query_position (renderer_type[type]->pipeline, GST_FORMAT_TIME, &pos);
+ gst_element_query_position (renderer_type[type]->pipeline, GST_FORMAT_TIME, &pos);
//g_print("HLS position %" GST_TIME_FORMAT " requested_start_position %" GST_TIME_FORMAT " duration %" GST_TIME_FORMAT " %s\n",
// GST_TIME_ARGS(pos), GST_TIME_ARGS(hls_requested_start_position), GST_TIME_ARGS(hls_duration),
// (hls_seek_enabled ? "seek enabled" : "seek not enabled"));
if (pos > hls_requested_start_position) {
hls_requested_start_position = 0;
}
- if ( hls_requested_start_position && pos < hls_requested_start_position && hls_seek_enabled) {
+ if ( hls_requested_start_position && pos < hls_requested_start_position && hls_seek_enabled) {
g_print("***************** seek to hls_requested_start_position %" GST_TIME_FORMAT "\n", GST_TIME_ARGS(hls_requested_start_position));
if (gst_element_seek_simple (renderer_type[type]->pipeline, GST_FORMAT_TIME,
GST_SEEK_FLAG_FLUSH | GST_SEEK_FLAG_KEY_UNIT, hls_requested_start_position)) {
hls_requested_start_position = 0;
}
- }
- }
+ }
+ }
}
switch (GST_MESSAGE_TYPE (message)) {
@@ -737,8 +802,8 @@ gboolean gstreamer_pipeline_bus_callback(GstBus *bus, GstMessage *message, void
gint percent = -1;
gst_message_parse_buffering(message, &percent);
hls_buffer_empty = TRUE;
- hls_buffer_full = FALSE;
- if (percent > 0) {
+ hls_buffer_full = FALSE;
+ if (percent > 0) {
hls_buffer_empty = FALSE;
renderer_type[type]->buffering_level = percent;
logger_log(logger, LOGGER_DEBUG, "Buffering :%d percent done", percent);
@@ -756,7 +821,7 @@ gboolean gstreamer_pipeline_bus_callback(GstBus *bus, GstMessage *message, void
gchar *debug;
gboolean flushing;
gst_message_parse_error (message, &err, &debug);
- logger_log(logger, LOGGER_INFO, "GStreamer error: %s %s", GST_MESSAGE_SRC_NAME(message),err->message);
+ logger_log(logger, LOGGER_INFO, "GStreamer error (video): %s %s", GST_MESSAGE_SRC_NAME(message),err->message);
if (!hls_video && strstr(err->message,"Internal data stream error")) {
logger_log(logger, LOGGER_INFO,
"*** This is a generic GStreamer error that usually means that GStreamer\n"
@@ -767,11 +832,11 @@ gboolean gstreamer_pipeline_bus_callback(GstBus *bus, GstMessage *message, void
"*** to select a videosink of your choice (see \"man uxplay\").\n\n"
"*** Raspberry Pi models 4B and earlier using Video4Linux2 may need \"-bt709\" uxplay option");
}
- g_error_free (err);
+ g_error_free (err);
g_free (debug);
- if (renderer_type[type]->appsrc) {
+ if (renderer_type[type]->appsrc) {
gst_app_src_end_of_stream (GST_APP_SRC(renderer_type[type]->appsrc));
- }
+ }
gst_bus_set_flushing(bus, TRUE);
gst_element_set_state (renderer_type[type]->pipeline, GST_STATE_READY);
renderer_type[type]->terminate = TRUE;
@@ -779,12 +844,12 @@ gboolean gstreamer_pipeline_bus_callback(GstBus *bus, GstMessage *message, void
break;
}
case GST_MESSAGE_EOS:
- /* end-of-stream */
- logger_log(logger, LOGGER_INFO, "GStreamer: End-Of-Stream");
- if (hls_video) {
+ /* end-of-stream */
+ logger_log(logger, LOGGER_INFO, "GStreamer: End-Of-Stream (video)");
+ if (hls_video) {
gst_bus_set_flushing(bus, TRUE);
gst_element_set_state (renderer_type[type]->pipeline, GST_STATE_READY);
- renderer_type[type]->terminate = TRUE;
+ renderer_type[type]->terminate = TRUE;
g_main_loop_quit( (GMainLoop *) loop);
}
break;
@@ -819,14 +884,14 @@ gboolean gstreamer_pipeline_bus_callback(GstBus *bus, GstMessage *message, void
char *sink = strstr(GST_MESSAGE_SRC_NAME(message), "-actual-sink-");
if (sink) {
sink += strlen("-actual-sink-");
- if (strstr(GST_MESSAGE_SRC_NAME(message), renderer_type[type]->codec)) {
+ if (strstr(GST_MESSAGE_SRC_NAME(message), renderer_type[type]->codec)) {
logger_log(logger, LOGGER_DEBUG, "GStreamer: automatically-selected videosink"
" (renderer %d: %s) is \"%ssink\"", renderer_type[type]->id + 1,
renderer_type[type]->codec, sink);
#ifdef X_DISPLAY_FIX
renderer_type[type]->use_x11 = (strstr(sink, "ximage") || strstr(sink, "xvimage"));
#endif
- renderer_type[type]->autovideo = false;
+ renderer_type[type]->autovideo = false;
}
}
}
@@ -879,17 +944,21 @@ int video_renderer_choose_codec (bool video_is_jpeg, bool video_is_h265) {
video_renderer_t *renderer_used = NULL;
g_assert(!hls_video);
if (video_is_jpeg) {
- renderer_used = renderer_type[0];
- } else if (n_renderers == 2) {
- if (video_is_h265) {
- logger_log(logger, LOGGER_ERR, "video is h265 but the -h265 option was not used");
- return -1;
- }
- renderer_used = renderer_type[1];
+ g_assert(type_jpeg != -1);
+ renderer_used = renderer_type[type_jpeg];
} else {
- renderer_used = video_is_h265 ? renderer_type[2] : renderer_type[1];
+ if (video_is_h265) {
+ if (type_265 == -1) {
+ logger_log(logger, LOGGER_ERR, "video is h265 but the -h265 option was not used");
+ return -1;
+ }
+ renderer_used = renderer_type[type_265];
+ } else {
+ g_assert(type_264 != -1);
+ renderer_used = renderer_type[type_264];
+ }
}
- if (renderer_used == NULL) {
+ if (renderer_used == NULL) {
return -1;
} else if (renderer_used == renderer) {
return 0;
@@ -901,16 +970,16 @@ int video_renderer_choose_codec (bool video_is_jpeg, bool video_is_h265) {
GstState old_state, new_state;
if (gst_element_get_state(renderer->pipeline, &old_state, &new_state, 100 * GST_MSECOND) == GST_STATE_CHANGE_FAILURE) {
g_error("video pipeline failed to go into playing state");
- return -1;
+ return -1;
}
logger_log(logger, LOGGER_DEBUG, "video_pipeline state change from %s to %s\n",
gst_element_state_get_name (old_state),gst_element_state_get_name (new_state));
gst_video_pipeline_base_time = gst_element_get_base_time(renderer->appsrc);
- if (n_renderers > 2 && renderer == renderer_type[2]) {
+ if (strstr(renderer->codec, h265)) {
logger_log(logger, LOGGER_INFO, "*** video format is h265 high definition (HD/4K) video %dx%d", width, height);
}
/* destroy unused renderers */
- for (int i = 1; i < n_renderers; i++) {
+ for (int i = 0; i < n_renderers; i++) {
if (renderer_type[i] == renderer) {
continue;
}
@@ -927,12 +996,12 @@ unsigned int video_reset_callback(void * loop) {
if (video_terminate) {
video_terminate = false;
if (renderer->appsrc) {
- gst_app_src_end_of_stream (GST_APP_SRC(renderer->appsrc));
+ gst_app_src_end_of_stream (GST_APP_SRC(renderer->appsrc));
}
gboolean flushing = TRUE;
gst_bus_set_flushing(renderer->bus, flushing);
- gst_element_set_state (renderer->pipeline, GST_STATE_NULL);
- g_main_loop_quit( (GMainLoop *) loop);
+ gst_element_set_state (renderer->pipeline, GST_STATE_NULL);
+ g_main_loop_quit( (GMainLoop *) loop);
}
return (unsigned int) TRUE;
}
@@ -1007,5 +1076,5 @@ void video_renderer_seek(float position) {
unsigned int video_renderer_listen(void *loop, int id) {
g_assert(id >= 0 && id < n_renderers);
return (unsigned int) gst_bus_add_watch(renderer_type[id]->bus,(GstBusFunc)
- gstreamer_pipeline_bus_callback, (gpointer) loop);
+ gstreamer_video_pipeline_bus_callback, (gpointer) loop);
}
diff --git a/renderers/video_renderer.h b/renderers/video_renderer.h
index 9c307e8..d280592 100644
--- a/renderers/video_renderer.h
+++ b/renderers/video_renderer.h
@@ -47,15 +47,17 @@ typedef enum videoflip_e {
typedef struct video_renderer_s video_renderer_t;
-void video_renderer_init (logger_t *logger, const char *server_name, videoflip_t videoflip[2], const char *parser,
+ void video_renderer_init (logger_t *logger, const char *server_name, videoflip_t videoflip[2], const char *parser, const char *rtp_pipeline,
const char *decoder, const char *converter, const char *videosink, const char *videosink_options,
- bool initial_fullscreen, bool video_sync, bool h265_support, guint playbin_version, const char *uri);
+ bool initial_fullscreen, bool video_sync, bool h265_support, bool coverart_support,
+ guint playbin_version, const char *uri);
void video_renderer_start ();
void video_renderer_stop ();
void video_renderer_pause ();
void video_renderer_seek(float position);
void video_renderer_set_start(float position);
void video_renderer_resume ();
+int video_renderer_cycle ();
bool video_renderer_is_paused();
uint64_t video_renderer_render_buffer (unsigned char* data, int *data_len, int *nal_count, uint64_t *ntp_time);
void video_renderer_display_jpeg(const void *data, int *data_len);
diff --git a/uxplay.1 b/uxplay.1
index 3cda48d..dba778b 100644
--- a/uxplay.1
+++ b/uxplay.1
@@ -1,4 +1,4 @@
-.TH UXPLAY "1" "May 2025" "1.72" "User Commands"
+.TH UXPLAY "1" "2025-10-26" "UxPlay 1.72" "User Commands"
.SH NAME
uxplay \- start AirPlay server
.SH SYNOPSIS
@@ -9,7 +9,7 @@ UxPlay 1.72: An open\-source AirPlay mirroring (+ audio streaming) server:
.SH OPTIONS
.TP
.B
-\fB\-n\fR name Specify the network name of the AirPlay server
+\fB\-n\fR name Specify the network name of the AirPlay server (UTF-8/ascii)
.TP
\fB\-nh\fR Do \fBNOT\fR append "@\fIhostname\fR" at end of AirPlay server name
.TP
@@ -19,6 +19,8 @@ UxPlay 1.72: An open\-source AirPlay mirroring (+ audio streaming) server:
.IP
v = 2 or 3 (default 3) optionally selects video player version
.TP
+\fB\-scrsv\fI n\fR Screensaver override \fIn\fR:0=off 1=on during activity 2=always on.
+.TP
\fB\-pin\fI[xxxx]\fRUse a 4-digit pin code to control client access (default: no)
.IP
without option, pin is random: optionally use fixed pin xxxx.
@@ -89,11 +91,20 @@ UxPlay 1.72: An open\-source AirPlay mirroring (+ audio streaming) server:
.IP
choices: ximagesink,xvimagesink,vaapisink,glimagesink,
.IP
- gtksink,waylandsink,osxvideosink,kmssink,d3d11videosink,...
+ gtksink,waylandsink,kmssink,fbdevsink,osxvideosink,
+.IP
+ d3d11videosink,d3d12videosink ...
.PP
.TP
\fB\-vs\fR 0 Streamed audio only, with no video display window.
.TP
+\fB\-vrtp\fI pl\fR Use rtph26[4,5]pay to send decoded video elsewhere: "pl"
+.IP
+ is the remaining pipeline, starting with rtph26*pay options:
+.IP
+ e.g. "config-interval=1 ! udpsink host=127.0.0.1 port=5000"
+.PP
+.TP
\fB\-v4l2\fR Use Video4Linux2 for GPU hardware h264 video decoding.
.TP
\fB\-bt709\fR Sometimes needed for Raspberry Pi models using Video4Linux2.
@@ -180,6 +191,10 @@ UxPlay 1.72: An open\-source AirPlay mirroring (+ audio streaming) server:
audio packets are dumped. "aud"= unknown format.
.PP
.TP
+\fB\-ble\fI [fn]\fR For BluetoothLE beacon: write data to default file ~/.uxplay.ble
+.IP
+ optional: write to file "fn" ("fn" = "off" to cancel)
+.TP
\fB\-d [n]\fR Enable debug logging; optional: n=1 to skip normal packet data.
.TP
\fB\-v\fR Displays version information
diff --git a/uxplay.cpp b/uxplay.cpp
index 48f3a3b..c3d7ef3 100644
--- a/uxplay.cpp
+++ b/uxplay.cpp
@@ -22,7 +22,6 @@
#include
#include
-#include
#include
#include
#include
@@ -35,13 +34,16 @@
#include
#include
#include
+#include
#ifdef _WIN32 /*modifications for Windows compilation */
#include
#include
#include
#include
+#include //for pthreads in MSYS2 UCRT
#else
+#include
#include
#include
#include
@@ -62,8 +64,13 @@
#include "lib/stream.h"
#include "lib/logger.h"
#include "lib/dnssd.h"
+#include "lib/crypto.h"
#include "renderers/video_renderer.h"
#include "renderers/audio_renderer.h"
+#ifdef DBUS
+#include
+#endif
+
#define VERSION "1.72"
@@ -85,6 +92,7 @@
#endif
static std::string server_name = DEFAULT_NAME;
+static bool server_name_is_utf8 = false;
static dnssd_t *dnssd = NULL;
static raop_t *raop = NULL;
static logger_t *render_logger = NULL;
@@ -164,16 +172,50 @@ static double db_high = 0.0;
static bool taper_volume = false;
static double initial_volume = 0.0;
static bool h265_support = false;
-static int n_renderers = 0;
+static int n_video_renderers = 0;
+static int n_audio_renderers = 0;
static bool hls_support = false;
static std::string url = "";
static guint gst_x11_window_id = 0;
+static guint progress_id = 0;
static guint gst_hls_position_id = 0;
static bool preserve_connections = false;
static guint missed_feedback_limit = MISSED_FEEDBACK_LIMIT;
static guint missed_feedback = 0;
static guint playbin_version = DEFAULT_PLAYBIN_VERSION;
static bool reset_httpd = false;
+static bool monitor_progress = false;
+static uint32_t rtptime = 0;
+static uint32_t rtptime_start = 0;
+static uint32_t rtptime_end = 0;
+static uint32_t rtptime_coverart_expired = 0;
+static std::string artist;
+static std::string coverart_artist;
+static std::string ble_filename = "";
+static std::string rtp_pipeline = "";
+static GMainLoop *gmainloop = NULL;
+
+//Support for D-Bus-based screensaver inhibition (org.freedesktop.ScreenSaver)
+static unsigned int scrsv;
+#ifdef DBUS
+/* these strings can be changed at startup if a non-conforming Desktop Environmemt is detected */
+static std::string dbus_service = "org.freedesktop.ScreenSaver";
+static std::string dbus_path = "/org/freedesktop/ScreenSaver";
+static std::string dbus_interface = "org.freedesktop.ScreenSaver";
+static std::string dbus_inhibit = "Inhibit";
+static std::string dbus_uninhibit = "UnInhibit";
+static DBusConnection *dbus_connection = NULL;
+static dbus_uint32_t dbus_cookie = 0;
+static DBusPendingCall *dbus_pending = NULL;
+static bool dbus_last_message = false;
+static const char *appname = DEFAULT_NAME;
+static const char *reason_always = "mirroring client: inhibit always";
+static const char *reason_active = "actively receiving video";
+static int activity_count;
+static double activity_threshold = 500000.0; // threshold for FPSdata item txUsageAvg to classify mirror video as "active"
+#define MAX_ACTIVITY_COUNT 60
+#endif
+
/* logging */
static void log(int level, const char* format, ...) {
@@ -203,6 +245,73 @@ static void log(int level, const char* format, ...) {
#define LOGW(...) log(LOGGER_WARNING, __VA_ARGS__)
#define LOGE(...) log(LOGGER_ERR, __VA_ARGS__)
+#ifdef DBUS
+static void dbus_screensaver_inhibiter(bool inhibit) {
+ g_assert(inhibit != dbus_last_message);
+ g_assert(scrsv);
+ /* receive reply from previous request, whenever that was sent
+ * (may have been sent hours ago ... !)
+ * (code modeled on vlc/modules/misc/inhibit/dbus.c) */
+ if (dbus_pending != NULL) {
+ DBusMessage *reply;
+ dbus_pending_call_block(dbus_pending);
+ reply = dbus_pending_call_steal_reply(dbus_pending);
+ dbus_pending_call_unref(dbus_pending);
+ dbus_pending = NULL;
+ if (reply != NULL) {
+ if (!dbus_message_get_args(reply, NULL,
+ DBUS_TYPE_UINT32, &dbus_cookie,
+ DBUS_TYPE_INVALID)) {
+ dbus_cookie = 0;
+ }
+ dbus_message_unref(reply);
+ }
+ LOGD("screen_saver: got D-Bus cookie %" PRIu32, (uint32_t) dbus_cookie);
+ }
+
+ if (!dbus_cookie && !inhibit) {
+ return; /* nothing to do */
+ }
+
+ /* send request */
+ const char *dbus_method = inhibit ? dbus_inhibit.c_str() : dbus_uninhibit.c_str();
+ DBusMessage *dbus_message = dbus_message_new_method_call(dbus_service.c_str(),
+ dbus_path.c_str(),
+ dbus_interface.c_str(),
+ dbus_method);
+ g_assert (dbus_message);
+
+ if (inhibit) {
+ dbus_bool_t ret;
+ const char *reason = (scrsv == 1) ? reason_active : reason_always;
+
+ ret = dbus_message_append_args(dbus_message,
+ DBUS_TYPE_STRING, &appname,
+ DBUS_TYPE_STRING, &reason,
+ DBUS_TYPE_INVALID);
+ g_assert(ret);
+
+ ret = dbus_connection_send_with_reply(dbus_connection, dbus_message, &dbus_pending, -1);
+ if (!ret) {
+ dbus_pending = NULL;
+ }
+ } else {
+ g_assert(dbus_cookie);
+ LOGD("screen_saver: releasing D-Bus cookie %" PRIu32, (uint32_t) dbus_cookie);
+ if (dbus_message_append_args(dbus_message,
+ DBUS_TYPE_UINT32, &dbus_cookie,
+ DBUS_TYPE_INVALID)
+ && dbus_connection_send(dbus_connection, dbus_message, NULL)) {
+ dbus_cookie = 0;
+ }
+ }
+
+ dbus_connection_flush(dbus_connection);
+ dbus_message_unref(dbus_message);
+ dbus_last_message = inhibit;
+}
+#endif
+
static bool file_has_write_access (const char * filename) {
bool exists = false;
bool write = false;
@@ -249,6 +358,18 @@ static size_t write_metadata(const char *filename, const char *text) {
return count;
}
+static int write_bledata( const uint32_t *pid, const char *process_name, const char *filename) {
+ char name[16] { 0 };
+ size_t len = strlen(process_name);
+ FILE *fp = fopen(filename, "wb");
+ printf("port %u\n", raop_port);
+ size_t count = sizeof(uint16_t) * fwrite(&raop_port, sizeof(uint16_t), 1, fp);
+ count += sizeof(uint32_t) * fwrite(pid, sizeof(uint32_t), 1, fp);
+ count += sizeof(char) * len * fwrite(process_name, 1, len * sizeof(char), fp);
+ fclose(fp);
+ return (int) count;
+}
+
static char *create_pin_display(char *pin_str, int margin, int gap) {
char *ptr;
char num[2] = { 0 };
@@ -371,7 +492,7 @@ static void dump_video_to_file(unsigned char *data, int datalen) {
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) {
@@ -404,7 +525,7 @@ static gboolean feedback_callback(gpointer loop) {
g_main_loop_quit((GMainLoop *) loop);
return TRUE;
} else if (missed_feedback > 2) {
- LOGE("%u missed client feedback signals (expected once per second); client may be offline", missed_feedback);
+ LOGE("%u missed client feedback signals (expected every two seconds); client may be offline", missed_feedback);
}
missed_feedback++;
} else {
@@ -430,6 +551,39 @@ static gboolean x11_window_callback(gpointer loop) {
return FALSE;
}
+/* signals handlers (ctrl-c, etc )*/
+
+static void cleanup();
+
+#ifdef _WIN32
+static gboolean handle_signal(gpointer data) {
+ relaunch_video = false;
+ g_main_loop_quit(gmainloop);
+ return G_SOURCE_REMOVE;
+}
+
+static BOOL WINAPI CtrlHandler(DWORD signal) {
+ switch (signal) {
+ case CTRL_C_EVENT:
+ case CTRL_CLOSE_EVENT:
+ case CTRL_SHUTDOWN_EVENT:
+ if (gmainloop) {
+ g_idle_add(handle_signal, NULL);
+ return TRUE;
+ } else {
+ cleanup();
+ exit(0);
+ }
+ default:
+ return FALSE;
+ }
+}
+#else
+static void CtrlHandler(int signum) {
+ cleanup();
+ exit(0);
+}
+
static gboolean sigint_callback(gpointer loop) {
relaunch_video = false;
g_main_loop_quit((GMainLoop *) loop);
@@ -442,65 +596,128 @@ static gboolean sigterm_callback(gpointer loop) {
return TRUE;
}
-#ifdef _WIN32
-struct signal_handler {
- GSourceFunc handler;
- gpointer user_data;
-};
-
-static std::unordered_map 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;
+static gboolean sighup_callback(gpointer loop) {
+ relaunch_video = false;
+ g_main_loop_quit((GMainLoop *) loop);
+ return TRUE;
}
#endif
+static void display_progress(uint32_t start, uint32_t curr, uint32_t end) {
+ if (curr < start || curr > end) {
+ return;
+ }
+ int duration = (int) (end - start)/44100;
+ int position = (int) (curr - start)/44100;
+ int remain = duration - position;
+ printf("audio progress (min:sec): %3d:%2.2d; remaining: %3d:%2.2d; track length %d:%2.2d\r",
+ position/60, position%60, remain/60, remain%60, duration/60, duration%60);
+ fflush(NULL);
+}
+
+static gboolean progress_callback (gpointer loop) {
+ if (monitor_progress) {
+ if (rtptime_start || rtptime_end) {
+ display_progress(rtptime_start, rtptime, rtptime_end);
+ }
+ if (render_coverart && coverart_artist == "_expired_" && rtptime - rtptime_coverart_expired > 44100 * 5) {
+ /* remove any expired coverart still being rendered more than 5 secs after it expired */
+ coverart_artist.erase();
+ video_renderer_cycle();
+ }
+ return TRUE;
+ } else {
+ progress_id = 0;
+ return FALSE;
+ }
+}
+
+#define MAX_VIDEO_RENDERERS 3
+#define MAX_AUDIO_RENDERERS 2
static void main_loop() {
- guint gst_bus_watch_id[2] = { 0 };
- g_assert(n_renderers <= 2);
+ guint gst_video_bus_watch_id[MAX_VIDEO_RENDERERS] = { 0 };
+ guint gst_audio_bus_watch_id[MAX_AUDIO_RENDERERS] = { 0 };
GMainLoop *loop = g_main_loop_new(NULL,FALSE);
relaunch_video = false;
+ monitor_progress = false;
reset_loop = false;
reset_httpd = false;
preserve_connections = false;
+ n_video_renderers = 0;
+ n_audio_renderers = 0;
if (use_video) {
+ n_video_renderers = 1;
relaunch_video = true;
if (url.empty()) {
- n_renderers = h265_support ? 2 : 1;
+ if (h265_support) {
+ n_video_renderers++;
+ }
+ if (render_coverart) {
+ n_video_renderers++;
+ }
+ /* renderer[0] : h264 video; followed by h265 video (optional) and jpeg (optional) */
gst_x11_window_id = 0;
} else {
- /* hls video will be rendered */
- n_renderers = 1;
+ /* hls video will be rendered: renderer[0] : hls */
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);
+ g_assert(n_video_renderers <= MAX_VIDEO_RENDERERS);
+ for (int i = 0; i < n_video_renderers; i++) {
+ gst_video_bus_watch_id[i] = (guint) video_renderer_listen((void *)loop, i);
}
}
+ if (use_audio) {
+ rtptime_start = 0;
+ rtptime_end = 0;
+ monitor_progress = true;
+ artist.erase();
+ coverart_artist.erase();
+ progress_id = g_timeout_add_seconds(1,(GSourceFunc) progress_callback, (gpointer) loop);
+ n_audio_renderers = 2;
+ g_assert(n_audio_renderers <= MAX_AUDIO_RENDERERS);
+ for (int i = 0; i < n_audio_renderers; i++) {
+ gst_audio_bus_watch_id[i] = (guint) audio_renderer_listen((void *)loop, i);
+ }
+ }
+
missed_feedback = 0;
guint feedback_watch_id = g_timeout_add_seconds(1, (GSourceFunc) feedback_callback, (gpointer) loop);
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);
+
+#ifdef _WIN32
+ gmainloop = loop;
+#else
+ signal(SIGINT, SIG_DFL);
+ signal(SIGTERM, SIG_DFL);
+ signal(SIGHUP, SIG_DFL);
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);
+ guint sighup_watch_id = g_unix_signal_add(SIGHUP, (GSourceFunc) sigint_callback, (gpointer) loop);
+#endif
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);
+#ifdef _WIN32
+ gmainloop = NULL;
+#else
+ signal(SIGINT, CtrlHandler); //switch back to non-mainloop CtrlHandler
+ signal(SIGTERM, CtrlHandler);
+ signal(SIGHUP, CtrlHandler);
if (sigint_watch_id > 0) g_source_remove(sigint_watch_id);
if (sigterm_watch_id > 0) g_source_remove(sigterm_watch_id);
+ if (sighup_watch_id > 0) g_source_remove(sighup_watch_id);
+#endif
+
+ for (int i = 0; i < n_video_renderers; i++) {
+ if (gst_video_bus_watch_id[i] > 0) g_source_remove(gst_video_bus_watch_id[i]);
+ }
+ for (int i = 0; i < n_audio_renderers; i++) {
+ if (gst_audio_bus_watch_id[i] > 0) g_source_remove(gst_audio_bus_watch_id[i]);
+ }
+ if (gst_x11_window_id > 0) g_source_remove(gst_x11_window_id);
if (reset_watch_id > 0) g_source_remove(reset_watch_id);
+ if (progress_id > 0) g_source_remove(progress_id);
if (video_reset_watch_id > 0) g_source_remove(video_reset_watch_id);
if (feedback_watch_id > 0) g_source_remove(feedback_watch_id);
g_main_loop_unref(loop);
@@ -622,10 +839,6 @@ static std::string find_mac () {
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;
@@ -644,16 +857,16 @@ static bool validate_mac(char * mac_address) {
}
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);
+ char str[4];
+ unsigned char random[6];
+ get_random_bytes(random, sizeof(random));
+ /* mark MAC address as locally administered, i.e. random */
+ random[0] = random[0] & ~0x01;
+ random[0] = random[0] | 0x02;
+ snprintf(str,3,"%2.2x", random[0]);
std::string mac_address(str);
- for (int i = 1; i < OCTETS; i++) {
- mac_address = mac_address + ":";
- octet = rand() % 256;
- snprintf(str,3,"%02x",octet);
+ for (int i = 1; i < 6; i++) {
+ snprintf(str,4,":%2.2x", random[i]);
mac_address = mac_address + str;
}
return mac_address;
@@ -664,11 +877,12 @@ static void print_info (char *name) {
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("-n name Specify network name of the AirPlay server (UTF-8/ascii)\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 [v] Support HTTP Live Streaming (currently Youtube video only) \n");
printf(" v = 2 or 3 (default 3) optionally selects video player version\n");
+ printf("-scrsv n Screensaver override n: 0=off 1=on during activity 2=always on\n");
printf("-pin[xxxx]Use a 4-digit pin code to control client access (default: no)\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");
@@ -688,7 +902,7 @@ static void print_info (char *name) {
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 with X11, Wayland, VAAPI, D3D11, kms)\n");
+ printf("-fs Full-screen (only with X11, Wayland, VAAPI, D3D11/12, kms)\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");
@@ -703,8 +917,12 @@ static void print_info (char *name) {
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(" gtksink,waylandsink,kmssink,fbdevsink,osxvideosink,\n");
+ printf(" d3d11videosink,d3v12videosink, etc.\n");
printf("-vs 0 Streamed audio only, with no video display window\n");
+ printf("-vrtp pl Use rtph26[4,5]pay to send decoded video elsewhere: \"pl\"\n");
+ printf(" is the remaining pipeline, starting with rtph26*pay options:\n");
+ printf(" e.g. \"config-interval=1 ! udpsink host=127.0.0.1 port=5000\"\n");
printf("-v4l2 Use Video4Linux2 for GPU hardware h264 decoding\n");
printf("-bt709 Sometimes needed for Raspberry Pi models using Video4Linux2 \n");
printf("-srgb Display \"Full range\" [0-255] color, not \"Limited Range\"[16-235]\n");
@@ -744,6 +962,8 @@ static void print_info (char *name) {
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("-ble [fn] For BluetoothLE beacon: write data to file ~/.uxplay.ble\n");
+ printf(" optional: write to file \"fn\" (\"fn\" = \"off\" to cancel)\n");
printf("-d [n] Enable debug logging; optional: n=1 to skip normal packet data\n");
printf("-v Displays version information\n");
printf("-h Displays this help\n");
@@ -834,17 +1054,17 @@ static bool get_ports (int nports, std::string option, const char * value, unsig
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;
+ case 'I':
+ *videoflip = INVERT;
+ break;
+ case 'H':
+ *videoflip = HFLIP;
+ break;
+ case 'V':
+ *videoflip = VFLIP;
+ break;
+ default:
+ return false;
}
return true;
}
@@ -852,14 +1072,14 @@ static bool get_videoflip (const char *str, videoflip_t *videoflip) {
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;
+ case 'L':
+ *videoflip = LEFT;
+ break;
+ case 'R':
+ *videoflip = RIGHT;
+ break;
+ default:
+ return false;
}
return true;
}
@@ -885,21 +1105,75 @@ static void append_hostname(std::string &server_name) {
#endif
}
+bool is_utf8(const char *string, bool *is_printable_ascii) {
+ /* test if C-string is printable ascii or valid UTF-8 (max 4 bytes) */
+ if (is_printable_ascii) {
+ *is_printable_ascii = true;
+ }
+ int len = (int) strlen(string);
+ for (int i = 0; i < len; i++) {
+ unsigned char c = (unsigned char) string[i];
+ int n = 0;
+ if (0x20 <= c && c <= 0x7e) {
+ continue; //printable ascii, no control characters.
+ } else if (is_printable_ascii) {
+ *is_printable_ascii = false;
+ }
+ if (0x00 <= c && c <= 0x7f) {
+ continue; //one byte code, 0bbbbbbb
+ } else if (c == 0xc0 || c == 0xc1) {
+ return false; //two byte code, invalid start byte
+ } else if ((c & 0xe0) == 0xc0) {
+ n = 1; //two byte code, 110bbbbb
+ } else if (c == 0xe0 && i < len - 1 && (unsigned char) string[i + 1] < 0xa0) {
+ return false; //three byte code, overlong encoding
+ } else if (c == 0xed && i < len - 1 && (unsigned char) string[i + 1] > 0x9f) {
+ return false; //three byte code, exclude U+dc00 to U+dfff
+ } else if ((c & 0xf0) == 0xe0) {
+ n = 2; //three byte code 1110bbbb
+ } else if (c >= 0xf5) {
+ return false; //four byte code, invalid start byte
+ } else if (c == 0xf0 && i < len - 1 && (unsigned char) string[i + 1] < 0x90) {
+ return false; //four byte code, overlong encoding
+ } else if (c == 0xf4 && i < len - 1 && (unsigned char) string[i + 1] > 0x8f) {
+ return false; //four byte code, out of range character (> U+10ffff)
+ } else if ((c & 0xf8) == 0xf0) {
+ n = 3; //four byte code, 11110bbb
+ } else {
+ return false; //more than 4 bytes
+ }
+ for (int j = 0; j < n && i < len ; j++) { // n bytes matching 10bbbbbb must follow ?
+ if ((++i == len) || (((unsigned char) string[i] & 0xc0) != 0x80)) {
+ return false;
+ }
+ }
+
+ }
+ return true;
+}
+
static void parse_arguments (int argc, char *argv[]) {
// Parse arguments
+ for (int i = 1; i < argc; i++) {
+ if (!is_utf8(argv[i], NULL)) {
+ fprintf(stderr,"Error: detected a non-ascii or non-UTF-8 string \"%s\""
+ "while parsing input arguments", argv[i]);
+ exit(0);
+ }
+ }
for (int i = 1; i < argc; i++) {
std::string arg(argv[i]);
- if (arg == "-rc") {
+ if (arg == "-rc") {
i++; //specifies startup file: has already been processed
} 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") {
+ 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") {
+ 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;
@@ -910,7 +1184,20 @@ static void parse_arguments (int argc, char *argv[]) {
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]);
+ bool ascii;
+ server_name_is_utf8 = false;
+ server_name.erase();
+ bool utf8 = is_utf8(argv[++i], &ascii);
+ if (!utf8) {
+ fprintf(stderr, "invalid (non-UTF-8/ascii) server name in \"-n %s\"", argv[i]);
+ exit(1);
+ }
+ server_name = std::string(argv[i]);
+ if (!ascii) {
+ server_name_is_utf8 = true;
+ printf("WARNING: a non-ascii (UTF-8) server-name \"%s\" was specified:"
+ " ensure correct locale settings to display it\n",server_name.c_str());
+ }
} else if (arg == "-nh") {
do_append_hostname = false;
} else if (arg == "-async") {
@@ -928,11 +1215,24 @@ static void parse_arguments (int argc, char *argv[]) {
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] );
+ fprintf(stderr, "invalid -async %s: requested delays must be smaller than +/- 1000 millisecs\n", argv[i] );
exit (1);
}
}
}
+ } else if (arg == "-scrsv") {
+ if (!option_has_value(i, argc, argv[i], argv[i+1])) exit(1);
+ unsigned int n = 0;
+ if (!get_value(argv[++i], &n) || n > 2) {
+ fprintf(stderr, "invalid \"-scrsv %s\"; values 0, 1, 2 allowed\n", argv[i]);
+ exit(1);
+ }
+#ifdef DBUS
+ scrsv = n;
+#else
+ fprintf(stderr,"invalid: option \"-scrsv\" is currently only implemented for Linux/*BSD systems with D-Bus service\n");
+ exit(1);
+#endif
} else if (arg == "-vsync") {
video_sync = true;
if (i < argc - 1) {
@@ -948,7 +1248,7 @@ static void parse_arguments (int argc, char *argv[]) {
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]);
+ fprintf(stderr, "invalid -vsync %s: requested delays must be smaller than +/- 1000 millisecs\n", argv[i]);
exit (1);
}
}
@@ -1003,7 +1303,7 @@ static void parse_arguments (int argc, char *argv[]) {
}
}
} else if (arg == "-m") {
- if (i < argc - 1 && *argv[i+1] != '-') {
+ if (i < argc - 1 && *argv[i+1] != '-') {
if (validate_mac(argv[++i])) {
mac_address.erase();
mac_address = argv[i];
@@ -1019,18 +1319,18 @@ static void parse_arguments (int argc, char *argv[]) {
} else if (arg == "-a") {
use_audio = false;
} else if (arg == "-d") {
- if (i < argc - 1 && *argv[i+1] != '-') {
+ if (i < argc - 1 && *argv[i+1] != '-') {
unsigned int n = 1;
if (!get_value(argv[++i], &n)) {
fprintf(stderr, "invalid \"-d %s\"; -d n : max n=1 (suppress packet data in debug output)\n", argv[i]);
exit(1);
}
debug_log = true;
- suppress_packet_debug_data = true;
- } else {
+ suppress_packet_debug_data = true;
+ } else {
debug_log = !debug_log;
- suppress_packet_debug_data = false;
- }
+ suppress_packet_debug_data = false;
+ }
} else if (arg == "-h" || arg == "--help" || arg == "-?" || arg == "-help") {
print_info(argv[0]);
exit(0);
@@ -1054,10 +1354,10 @@ static void parse_arguments (int argc, char *argv[]) {
videosink.erase();
videosink.append(argv[++i]);
std::size_t pos = videosink.find(" ");
- if (pos != std::string::npos) {
+ if (pos != std::string::npos) {
videosink_options.erase();
videosink_options = videosink.substr(pos);
- videosink.erase(pos);
+ videosink.erase(pos);
}
} else if (arg == "-as") {
if (!option_has_value(i, argc, arg, argv[i+1])) exit(1);
@@ -1069,7 +1369,7 @@ static void parse_arguments (int argc, char *argv[]) {
exit(1);
} else if (arg == "-nc") {
new_window_closing_behavior = false;
- if (i < argc - 1) {
+ if (i < argc - 1) {
if (strlen(argv[i+1]) == 2 && strncmp(argv[i+1], "no", 2) == 0) {
new_window_closing_behavior = true;
i++;
@@ -1099,7 +1399,7 @@ static void parse_arguments (int argc, char *argv[]) {
exit(1);
} else if (arg == "-fs" ) {
fullscreen = true;
- } else if (arg == "-FPSdata") {
+ } else if (arg == "-FPSdata") {
show_client_FPS_data = true;
} else if (arg == "-reset") {
/* now using feedback (every 1 sec ) instead of ntp timeouts (every 3 secs) to detect offline client and reset connections */
@@ -1109,6 +1409,14 @@ static void parse_arguments (int argc, char *argv[]) {
fprintf(stderr, "invalid \"-reset %s\"; -reset n must have n >= 0, default n = %d seconds\n", argv[i], MISSED_FEEDBACK_LIMIT);
exit(1);
}
+ } else if (arg == "-vrtp") {
+ if (!option_has_value(i, argc, arg, argv[i+1])) {
+ fprintf(stderr,"option \"-vrtp\" must be followed by a pipeline for sending the video stream:\n"
+ "e.g., \" ! udpsink host=127.0.0.1 port -= 5000\"\n");
+ exit(1);
+ }
+ rtp_pipeline.erase();
+ rtp_pipeline.append(argv[++i]);
} else if (arg == "-vdmp") {
dump_video = true;
if (i < argc - 1 && *argv[i+1] != '-') {
@@ -1183,6 +1491,31 @@ static void parse_arguments (int argc, char *argv[]) {
fprintf(stderr,"option -md must be followed by a filename for metadata text output\n");
exit(1);
}
+ } else if (arg == "-ble" ) {
+ ble_filename.erase();
+ if (i < argc - 1 && *argv[i+1] != '-') {
+ i++;
+ if (strlen(argv[i]) != 3 || strncmp(argv[i], "off", 3)) {
+ ble_filename.append(argv[i]);
+ if (!file_has_write_access(argv[i])) {
+ fprintf(stderr, "%s cannot be written to:\noption \"-ble\" must be to a file with write access\n", argv[i]);
+ exit(1);
+ }
+ }
+ } else {
+ static const char* homedir = get_homedir();
+ if (homedir) {
+ ble_filename = homedir;
+ ble_filename.append("/.uxplay.ble");
+ if (!file_has_write_access(ble_filename.c_str())) {
+ fprintf(stderr, "%s cannot be written to\n",ble_filename.c_str()) ;
+ exit(1);
+ }
+ } else {
+ fprintf(stderr,"failed to obtain home directory\n");
+ exit(1);
+ }
+ }
} else if (arg == "-bt709") {
bt709_fix = true;
} else if (arg == "-srgb") {
@@ -1200,7 +1533,7 @@ static void parse_arguments (int argc, char *argv[]) {
int n;
char *end;
if (i < argc - 1 && *argv[i+1] != '-') {
- n = (int) (strtof(argv[++i], &end) * SECOND_IN_USECS);
+ n = (int) (strtof(argv[++i], &end) * SECOND_IN_USECS);
if (*end == '\0' && n >=0 && n <= 10 * SECOND_IN_USECS) {
audiodelay = n;
continue;
@@ -1212,7 +1545,7 @@ static void parse_arguments (int argc, char *argv[]) {
} else if (arg == "-pin") {
setup_legacy_pairing = true;
pin_pw = 1;
- if (i < argc - 1 && *argv[i+1] != '-') {
+ 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]);
@@ -1241,10 +1574,10 @@ static void parse_arguments (int argc, char *argv[]) {
exit(1);
}
} else {
- // fprintf(stderr, "option \"-key \" requires a path to a file for persistent key storage\n");
- // exit(1);
- keyfile.erase();
- keyfile.append("0");
+ // fprintf(stderr, "option \"-key \" requires a path to a file for persistent key storage\n");
+ // exit(1);
+ keyfile.erase();
+ keyfile.append("0");
}
} else if (arg == "-pw") {
setup_legacy_pairing = false;
@@ -1268,15 +1601,15 @@ static void parse_arguments (int argc, char *argv[]) {
fprintf(stderr, "%s cannot be written to:\noption \"-dacp \" must be to a file with write access\n", fn);
exit(1);
}
- } else {
+ } else {
dacpfile.append(get_homedir());
dacpfile.append("/.uxplay.dacp");
}
- } else if (arg == "-taper") {
+ } else if (arg == "-taper") {
taper_volume = true;
} else if (arg == "-db") {
bool db_bad = true;
- double db1, db2;
+ double db1, db2;
if ( i < argc -1) {
char *end1, *end2;
db1 = strtod(argv[i+1], &end1);
@@ -1294,7 +1627,7 @@ static void parse_arguments (int argc, char *argv[]) {
fprintf(stderr, "invalid \"-db %s\": db value must be \"low\" or \"low:high\", low < 0 and high > low are decibel gains\n", argv[i+1]);
exit(1);
}
- i++;
+ i++;
db_low = db1;
db_high = db2;
printf("db range %f:%f\n", db_low, db_high);
@@ -1323,7 +1656,7 @@ static void parse_arguments (int argc, char *argv[]) {
fprintf(stderr, "invalid \"-vol %s\", value must be between 0.0 (mute) and 1.0 (full volume)\n", argv[i+1]);
exit(1);
}
- i++;
+ i++;
} else if (arg == "-hls") {
hls_support = true;
if (i < argc - 1 && *argv[i+1] != '-') {
@@ -1332,7 +1665,7 @@ static void parse_arguments (int argc, char *argv[]) {
fprintf(stderr, "invalid \"-hls %s\"; -hls n only allows \"playbin\" video player versions 2 or 3\n", argv[i]);
exit(1);
}
- playbin_version = (guint) n;
+ playbin_version = (guint) n;
}
} else if (arg == "-h265") {
h265_support = true;
@@ -1374,6 +1707,17 @@ static void process_metadata(int count, const char *dmap_tag, const unsigned cha
break;
case 'r':
metadata_text->append("Artist: "); /*asar*/
+ if (render_coverart) {
+ artist.erase();
+ artist.append(metadata, metadata + datalen);
+ if (coverart_artist == "_pending_") {
+ coverart_artist = artist;
+ }
+ if (coverart_artist != "_expired_" && coverart_artist != artist) {
+ coverart_artist = "_expired_";
+ rtptime_coverart_expired = rtptime;
+ }
+ }
break;
default:
dmap_type = 0;
@@ -1449,7 +1793,7 @@ static void process_metadata(int count, const char *dmap_tag, const unsigned cha
char *str = (char *) calloc(datalen + 1, sizeof(char));
memcpy(str, metadata, datalen);
metadata_text->append(str);
- metadata_text->append("\n");
+ metadata_text->append("\n");
free(str);
} else if (debug_log) {
std::string md = "";
@@ -1471,7 +1815,7 @@ static int parse_dmap_header(const unsigned char *metadata, char *tag, int *len)
bool istag = true;
for (int i = 0; i < 4; i++) {
tag[i] = (char) *header;
- if (!isalpha(tag[i])) {
+ if (!isalpha(tag[i])) {
istag = false;
}
header++;
@@ -1493,26 +1837,37 @@ 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);
+ dnssd_error = dnssd_register_raop(dnssd, raop_port);
+ if (dnssd_error) {
+ if (ble_filename.empty()) {
+ 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;
} 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);
+ LOGI("dnssd_register_raop failed: ignoring because Bluetooth LE service discovery may be available");
}
- 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;
+
+ dnssd_error = dnssd_register_airplay(dnssd, airplay_port);
+ if (dnssd_error) {
+ if (ble_filename.empty()) {
+ 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;
+ } else {
+ LOGI("dnssd_register_airplay failed: ignoring because Bluetooth LE service discovery may be available");
+ }
}
LOGD("register_dnssd: advertised AirPlay service with \"Features\" code = 0x%llX",
@@ -1787,10 +2142,10 @@ extern "C" void report_client_request(void *cls, char *deviceid, char * model, c
LOGI("connection request from %s (%s) with deviceID = %s\n", name, model, deviceid);
if (restrict_clients) {
*admit = check_client(deviceid);
- if (*admit == false) {
+ 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;
}
@@ -1812,12 +2167,15 @@ extern "C" void audio_process (void *cls, raop_ntp_t *ntp, audio_decode_struct *
data->ntp_time_remote = data->ntp_time_remote + remote_clock_offset;
switch (data->ct) {
case 2:
+ /* for progress monitor (ALAC audio only) */
+ rtptime = data->rtp_time;
if (audio_delay_alac) {
data->ntp_time_remote = (uint64_t) ((int64_t) data->ntp_time_remote + audio_delay_alac);
}
break;
case 4:
case 8:
+ monitor_progress = false;
if (audio_delay_aac) {
data->ntp_time_remote = (uint64_t) ((int64_t) data->ntp_time_remote + audio_delay_aac);
}
@@ -1839,8 +2197,8 @@ extern "C" void video_process (void *cls, raop_ntp_t *ntp, video_decode_struct *
remote_clock_offset = local_time - data->ntp_time_remote;
}
int count = 0;
- uint64_t pts_mismatch = 0;
- do {
+ uint64_t pts_mismatch = 0;
+ do {
data->ntp_time_remote = data->ntp_time_remote + remote_clock_offset;
pts_mismatch = video_renderer_render_buffer(data->data, &(data->data_len), &(data->nal_count), &(data->ntp_time_remote));
if (pts_mismatch) {
@@ -1852,6 +2210,27 @@ extern "C" void video_process (void *cls, raop_ntp_t *ntp, video_decode_struct *
}
}
+#ifdef DBUS
+extern "C" void mirror_video_activity (void *cls, double *txusage) {
+ if (scrsv != 1) {
+ return;
+ }
+ if (*txusage > activity_threshold) {
+ if (activity_count < MAX_ACTIVITY_COUNT) {
+ activity_count++;
+ } else if (activity_count == MAX_ACTIVITY_COUNT && !dbus_last_message) {
+ dbus_screensaver_inhibiter(true);
+ }
+ } else {
+ if (activity_count > 0) {
+ activity_count--;
+ } else if (activity_count == 0 && dbus_last_message) {
+ dbus_screensaver_inhibiter(false);
+ }
+ }
+}
+#endif
+
extern "C" void video_pause (void *cls) {
if (use_video) {
video_renderer_pause();
@@ -1912,15 +2291,15 @@ extern "C" void audio_set_volume (void *cls, float volume) {
/* 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) */
+ /* 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 {
+ } else {
db = db_flat;
}
- /* conversion from (gain) decibels to GStreamer's linear volume scale */
+ /* conversion from (gain) decibels to GStreamer's linear volume scale */
gst_volume = pow(10.0, 0.05*db);
}
audio_renderer_set_volume(gst_volume);
@@ -1970,7 +2349,8 @@ extern "C" void audio_set_coverart(void *cls, const void *buffer, int buflen) {
LOGI("coverart size %d written to %s", buflen, coverart_filename.c_str());
} else if (buffer && render_coverart) {
video_renderer_choose_codec(true, false); /* video_is_jpeg = true */
- video_renderer_display_jpeg(buffer, &buflen);
+ video_renderer_display_jpeg(buffer, &buflen);
+ coverart_artist = "_pending_";
}
}
@@ -1980,12 +2360,11 @@ extern "C" void audio_stop_coverart_rendering(void *cls) {
}
}
-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_progress(void *cls, uint32_t *start, uint32_t *curr, uint32_t *end) {
+ rtptime_start = *start;
+ rtptime = *curr;
+ rtptime_end = *end;
+ display_progress(rtptime_start, rtptime, rtptime_end);
}
extern "C" void audio_set_metadata(void *cls, const void *buffer, int buflen) {
@@ -1994,7 +2373,7 @@ extern "C" void audio_set_metadata(void *cls, const void *buffer, int buflen) {
int datalen;
int count = 0;
- printf("==============Audio Metadata=============\n");
+ printf("====================Audio Metadata==================\n");
if (buflen < 8) {
LOGE("received invalid metadata, length %d < 8", buflen);
@@ -2118,24 +2497,20 @@ extern "C" void on_video_acquire_playback_info (void *cls, playback_info_t *play
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;
+ 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;
}
}
@@ -2169,6 +2544,9 @@ static int start_raop_server (unsigned short display[5], unsigned short tcp[3],
raop_cbs.export_dacp = export_dacp;
raop_cbs.video_reset = video_reset;
raop_cbs.video_set_codec = video_set_codec;
+#ifdef DBUS
+ raop_cbs.mirror_video_activity = mirror_video_activity;
+#endif
raop_cbs.on_video_play = on_video_play;
raop_cbs.on_video_scrub = on_video_scrub;
raop_cbs.on_video_rate = on_video_rate;
@@ -2292,7 +2670,7 @@ static void read_config_file(const char * filename, const char * uxplay_name) {
} else {
options.push_back(token.c_str());
}
- }
+ }
}
}
file.close();
@@ -2300,6 +2678,7 @@ static void read_config_file(const char * filename, const char * uxplay_name) {
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++) {
@@ -2309,6 +2688,7 @@ static void read_config_file(const char * filename, const char * uxplay_name) {
free (argv);
}
}
+
#ifdef GST_MACOS
/* workaround for GStreamer >= 1.22 "Official Builds" on macOS */
#include
@@ -2327,6 +2707,17 @@ int main (int argc, char *argv[]) {
std::vector server_hw_addr;
std::string config_file = "";
+#ifdef _WIN32
+ if (!SetConsoleCtrlHandler(CtrlHandler, TRUE)) {
+ LOGE("Could not set control handler");
+ exit(1);
+ }
+#else
+ signal(SIGINT, CtrlHandler);
+ signal(SIGTERM, CtrlHandler);
+ signal(SIGHUP, CtrlHandler);
+#endif
+
#ifdef __OpenBSD__
if (unveil("/", "rwc") == -1 || unveil(NULL, NULL) == -1) {
err(1, "unveil");
@@ -2351,7 +2742,7 @@ int main (int argc, char *argv[]) {
exit(1);
}
rcfile = argv[i+1];
- if (stat(rcfile, &sb) == -1) {
+ if (stat(rcfile, &sb) == -1) {
LOGE("startup file %s specified by option -rc was not found", rcfile);
exit(0);
}
@@ -2383,6 +2774,52 @@ int main (int argc, char *argv[]) {
LOGI("UxPlay %s: An Open-Source AirPlay mirroring and audio-streaming server.", VERSION);
+#ifdef DBUS
+ if (scrsv) {
+ DBusError dbus_error;
+ dbus_error_init(&dbus_error);
+ dbus_connection = dbus_bus_get(DBUS_BUS_SESSION, &dbus_error);
+ if (dbus_error_is_set(&dbus_error)) {
+ dbus_error_free(&dbus_error);
+ scrsv = 0;
+ LOGI ("D-Bus session not found: screensaver inhibition option (\"-scrsv\") will not be active");
+ }
+ }
+ if (scrsv) {
+ LOGD ("D-Bus session support is available, connection %p", dbus_connection);
+ std::string desktop = getenv("XDG_CURRENT_DESKTOP");
+ LOGD("Desktop Environment: %s", desktop.c_str());
+
+ /* if dbus_service, dbus_path, dbus_interface, dbus_inhibit, dbus_uninhibit *
+ * in the detected Desktop Environments are still non-conforming to the *
+ * org.freedesktop.ScreenSaver interface, they can be modifed here */
+
+ /* some desktop environments (e.g. Xfce 4, Mate) modify the D-Bus service name */
+ std::string name;
+ if (strstr(desktop.c_str(), "XFCE")) {
+ name = "xfce";
+ } else if (strstr(desktop.c_str(), "MATE")) {
+ name = "mate";
+ }
+
+ if (!name.empty()) {
+ size_t pos;
+ std::string replace_word = "freedesktop";
+ pos = dbus_service.find(replace_word);
+ dbus_service.replace(pos, replace_word.size(), name);
+ pos = dbus_path.find(replace_word);
+ dbus_path.replace(pos, replace_word.size(), name);
+ pos = dbus_interface.find(replace_word);
+ dbus_interface.replace(pos, replace_word.size(), name);
+ }
+
+ LOGI("Will attempt to use %s (D-Bus screensaver inhibition) %s", dbus_service.c_str(),
+ (scrsv == 1 ? "only during screen activity" : "always"));
+ if (scrsv == 2) {
+ dbus_screensaver_inhibiter(true);
+ }
+ }
+#endif
if (audiosink == "0") {
use_audio = false;
dump_audio = false;
@@ -2390,7 +2827,7 @@ int main (int argc, char *argv[]) {
if (dump_video) {
if (video_dump_limit > 0) {
LOGI("dump video using \"-vdmp %d %s\"", video_dump_limit, video_dumpfile_name.c_str());
- } else {
+ } else {
LOGI("dump video using \"-vdmp %s\"", video_dumpfile_name.c_str());
}
}
@@ -2409,20 +2846,6 @@ int main (int argc, char *argv[]) {
}
#endif
-#ifdef _WIN32
- /* because of issues in videosink dvd312videosink (segfault when resolution changes
- with certain Nvdia graphics cards) make the default videosink d3d11videosink, and
- use its decoder */
- if (videosink == "autovideosink") {
- videosink.erase();
- videosink.append("d3d11videosink");
- }
- if (videosink == "d3d11videosink") {
- video_decoder.erase();
- video_decoder.append("d3d11h264dec");
- }
-#endif
-
if (videosink == "0") {
use_video = false;
videosink.erase();
@@ -2435,14 +2858,14 @@ int main (int argc, char *argv[]) {
if (fullscreen && use_video) {
if (videosink == "waylandsink" || videosink == "vaapisink") {
videosink_options.append(" fullscreen=true");
- } else if (videosink == "kmssink") {
- videosink_options.append(" force_modesetting=true");
- }
+ } else if (videosink == "kmssink") {
+ videosink_options.append(" force-modesetting=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");
+ 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("Use Alt-Enter key combination to toggle into/out of full-screen mode");
@@ -2451,7 +2874,7 @@ int main (int argc, char *argv[]) {
if (videosink == "d3d12videosink" && videosink_options.empty() && use_video) {
if (fullscreen) {
- videosink_options.append("fullscreen=TRUE");
+ videosink_options.append(" fullscreen=TRUE ");
} else {
videosink_options.append(" fullscreen-on-alt-enter=TRUE ");
LOGI("Use Alt-Enter key combination to toggle into/out of full-screen mode");
@@ -2473,8 +2896,8 @@ int main (int argc, char *argv[]) {
if (pairing_register == "") {
const char * homedir = get_homedir();
if (homedir) {
- pairing_register = homedir;
- pairing_register.append("/.uxplay.register");
+ pairing_register = homedir;
+ pairing_register.append("/.uxplay.register");
}
}
}
@@ -2484,13 +2907,13 @@ int main (int argc, char *argv[]) {
size_t len = 0;
std::string key;
int clients = 0;
- std::ifstream file(pairing_register);
+ 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();
+ std::string pk = line.c_str();
registered_keys.push_back(key.assign(pk));
clients ++;
}
@@ -2530,14 +2953,15 @@ int main (int argc, char *argv[]) {
logger_set_level(render_logger, log_level);
if (use_audio) {
- audio_renderer_init(render_logger, audiosink.c_str(), &audio_sync, &video_sync);
+ 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_renderer_init(render_logger, server_name.c_str(), videoflip, video_parser.c_str(), rtp_pipeline.c_str(),
video_decoder.c_str(), video_converter.c_str(), videosink.c_str(),
- videosink_options.c_str(), fullscreen, video_sync, h265_support, playbin_version, NULL);
+ videosink_options.c_str(), fullscreen, video_sync, h265_support,
+ render_coverart, playbin_version, NULL);
video_renderer_start();
#ifdef __OpenBSD__
} else {
@@ -2560,7 +2984,6 @@ int main (int argc, char *argv[]) {
}
}
if (mac_address.empty()) {
- srand(time(NULL) * getpid());
mac_address = random_mac();
LOGI("using randomly-generated MAC address %s",mac_address.c_str());
}
@@ -2588,21 +3011,35 @@ int main (int argc, char *argv[]) {
}
if (start_dnssd(server_hw_addr, server_name)) {
- goto cleanup;
+ cleanup();
}
if (start_raop_server(display, tcp, udp, debug_log)) {
stop_dnssd();
- goto cleanup;
+ cleanup();
}
+
+#define PID_MAX 4194304 // 2^22
+ if (ble_filename.length()) {
+#ifdef _WIN32
+ DWORD winpid = GetCurrentProcessId();
+ uint32_t pid = (uint32_t) winpid;
+ g_assert(pid <= PID_MAX);
+#else
+ pid_t pid = getpid();
+ g_assert (pid <= PID_MAX && pid >= 0);
+#endif
+ write_bledata((uint32_t *) &pid, argv[0], ble_filename.c_str());
+ LOGI("Bluetooth LE beacon-based service discovery is possible: PID data written to %s", ble_filename.c_str());
+ }
+
if (register_dnssd()) {
stop_raop_server();
stop_dnssd();
- goto cleanup;
+ cleanup();
}
reconnect:
compression_type = 0;
close_window = new_window_closing_behavior;
-
main_loop();
if (relaunch_video) {
if (reset_httpd) {
@@ -2618,10 +3055,11 @@ int main (int argc, char *argv[]) {
url.erase();
raop_remove_known_connections(raop);
}
- const char *uri = (url.empty() ? NULL : url.c_str());
- video_renderer_init(render_logger, server_name.c_str(), videoflip, video_parser.c_str(),
+ const char *uri = (url.empty() ? NULL : url.c_str());
+ video_renderer_init(render_logger, server_name.c_str(), videoflip, video_parser.c_str(),rtp_pipeline.c_str(),
video_decoder.c_str(), video_converter.c_str(), videosink.c_str(),
- videosink_options.c_str(), fullscreen, video_sync, h265_support, playbin_version, uri);
+ videosink_options.c_str(), fullscreen, video_sync, h265_support,
+ render_coverart, playbin_version, uri);
video_renderer_start();
}
if (reset_httpd) {
@@ -2635,7 +3073,10 @@ int main (int argc, char *argv[]) {
stop_raop_server();
stop_dnssd();
}
- cleanup:
+ cleanup();
+}
+
+static void cleanup() {
if (use_audio) {
audio_renderer_destroy();
}
@@ -2652,9 +3093,25 @@ int main (int argc, char *argv[]) {
fclose(video_dumpfile);
}
if (coverart_filename.length()) {
- remove (coverart_filename.c_str());
+ remove (coverart_filename.c_str());
}
if (metadata_filename.length()) {
- remove (metadata_filename.c_str());
+ remove (metadata_filename.c_str());
}
+ if (ble_filename.length()) {
+ remove (ble_filename.c_str());
+ }
+#ifdef DBUS
+ if (dbus_connection) {
+ LOGD("Ending D-Bus connection %p", dbus_connection);
+ if (dbus_last_message) {
+ dbus_screensaver_inhibiter(false);
+ }
+ if (dbus_pending) {
+ dbus_pending_call_cancel(dbus_pending);
+ dbus_pending_call_unref(dbus_pending);
+ }
+ dbus_connection_unref(dbus_connection);
+ }
+#endif
}
diff --git a/uxplay.spec b/uxplay.spec
index e302bb4..f484518 100644
--- a/uxplay.spec
+++ b/uxplay.spec
@@ -129,6 +129,7 @@ cd build
%{_docdir}/%{name}/README.txt
%{_docdir}/%{name}/README.html
%{_docdir}/%{name}/README.md
+%{_docdir}/%{name}/systemd/uxplay.service
%license
%{_docdir}/%{name}/LICENSE