mirror of
https://github.com/morgan9e/UxPlay
synced 2026-04-15 00:34:05 +09:00
Merge pull request #158 from FDH2/master
ipv6 support + various other fixes.
This commit is contained in:
@@ -47,6 +47,12 @@ if ( GST_MACOS )
|
||||
message ( STATUS "define GST_MACOS" )
|
||||
endif()
|
||||
|
||||
if ( GST_124 )
|
||||
add_definitions( -DGST_124 )
|
||||
message ( STATUS "define GST_124" )
|
||||
endif()
|
||||
|
||||
|
||||
add_executable( uxplay uxplay.cpp )
|
||||
target_link_libraries( uxplay
|
||||
renderers
|
||||
|
||||
133
README.html
133
README.html
@@ -1,29 +1,27 @@
|
||||
<h1
|
||||
id="uxplay-1.68-airplay-mirror-and-airplay-audio-server-for-linux-macos-and-unix-now-also-runs-on-windows.">UxPlay
|
||||
1.68: AirPlay-Mirror and AirPlay-Audio server for Linux, macOS, and Unix
|
||||
id="uxplay-1.69-airplay-mirror-and-airplay-audio-server-for-linux-macos-and-unix-now-also-runs-on-windows.">UxPlay
|
||||
1.69: AirPlay-Mirror and AirPlay-Audio server for Linux, macOS, and Unix
|
||||
(now also runs on Windows).</h1>
|
||||
<h3
|
||||
id="now-developed-at-the-github-site-httpsgithub.comfdh2uxplay-where-all-user-issues-should-be-posted-and-latest-versions-can-be-found.">Now
|
||||
id="now-developed-at-the-github-site-httpsgithub.comfdh2uxplay-where-all-user-issues-should-be-posted-and-latest-versions-can-be-found."><strong>Now
|
||||
developed at the GitHub site <a
|
||||
href="https://github.com/FDH2/UxPlay">https://github.com/FDH2/UxPlay</a>
|
||||
(where ALL user issues should be posted, and latest versions can be
|
||||
found).</h3>
|
||||
found).</strong></h3>
|
||||
<ul>
|
||||
<li><em><strong>NEW in v1.68</strong>: Volume-control improvements, plus
|
||||
improved support for Apple-style one-time “pin” codes introduced in
|
||||
1.67: a register of pin-registered clients can now optionally be
|
||||
maintained to check returning clients; a simpler method for generating a
|
||||
persistent public key (based on the MAC address, which can be set in the
|
||||
UxPlay startup file) is now the default. (The OpenSSL “pem-file” method
|
||||
introduced in 1.67 is still available with the “-key” option.)</em></li>
|
||||
<li><em><strong>NEW in v1.69</strong>: minor changes for users:
|
||||
-nofreeze option to NOT leave frozen video in place when a network
|
||||
failure occurs; internal changes/improvements needed for planned future
|
||||
HLS video streaming support.</em></li>
|
||||
</ul>
|
||||
<h2 id="highlights">Highlights:</h2>
|
||||
<ul>
|
||||
<li>GPLv3, open source.</li>
|
||||
<li>Originally supported only AirPlay Mirror protocol, now has added
|
||||
support for AirPlay Audio-only (Apple Lossless ALAC) streaming from
|
||||
current iOS/iPadOS clients. <strong>There is no support for Airplay2
|
||||
video-streaming protocol, and none is planned.</strong></li>
|
||||
current iOS/iPadOS clients. <strong>There is no current support for
|
||||
Airplay HLS video-streaming (e.g., YouTube video) but this is in
|
||||
development.</strong></li>
|
||||
<li>macOS computers (2011 or later, both Intel and “Apple Silicon” M1/M2
|
||||
systems) can act either as AirPlay clients, or as the server running
|
||||
UxPlay. Using AirPlay, UxPlay can emulate a second display for macOS
|
||||
@@ -166,12 +164,13 @@ app cannot be watched using UxPlay’s AirPlay Mirror mode (only the
|
||||
unprotected audio will be streamed, in AAC format), but both video and
|
||||
audio content from DRM-free apps like “YouTube app” will be streamed by
|
||||
UxPlay in Mirror mode.</strong></p></li>
|
||||
<li><p><strong>As UxPlay does not support non-Mirror AirPlay2 video
|
||||
streaming (where the client controls a web server on the AirPlay server
|
||||
that directly receives content to avoid it being decoded and re-encoded
|
||||
by the client), using the icon for AirPlay video in apps such as the
|
||||
YouTube app will only send audio (in lossless ALAC format) without the
|
||||
accompanying video.</strong></p></li>
|
||||
<li><p><strong>As UxPlay does not currently support non-Mirror AirPlay
|
||||
video streaming (where the client controls a web server on the AirPlay
|
||||
server that directly receives HLS content to avoid it being decoded and
|
||||
re-encoded by the client), using the icon for AirPlay video in apps such
|
||||
as the YouTube app will only send audio (in lossless ALAC format)
|
||||
without the accompanying video (there are plans to support HLS video in
|
||||
future releases of UxPlay)</strong></p></li>
|
||||
</ul>
|
||||
<h3
|
||||
id="possibility-for-using-hardware-accelerated-h264-video-decoding-if-available.">Possibility
|
||||
@@ -257,10 +256,16 @@ libplist 2.0 or later. (This means Debian 10 “Buster” based systems
|
||||
(e.g, Ubuntu 18.04) or newer; on Debian 10 systems “libplist” is an
|
||||
older version, you need “libplist3”.) If it does not, you may need to
|
||||
build and install these from source (see instructions at the end of this
|
||||
README). If you have a non-standard OpenSSL installation, you may need
|
||||
to set the environment variable OPENSSL_ROOT_DIR (<em>e.g.</em> ,
|
||||
README).</p>
|
||||
<p>If you have a non-standard OpenSSL installation, you may need to set
|
||||
the environment variable OPENSSL_ROOT_DIR (<em>e.g.</em> ,
|
||||
“<code>export OPENSSL_ROOT_DIR=/usr/local/lib64</code>” if that is where
|
||||
it is installed).</p>
|
||||
it is installed). Similarly, for non-standard (or multiple) GStreamer
|
||||
installations, set the environment variable GSTREAMER_ROOT_DIR to the
|
||||
directory that contains the “…/gstreamer-1.0/” directory of the
|
||||
gstreamer installation that UxPlay should use (if this is <em>e.g.</em>
|
||||
“~/my_gstreamer/lib/gstreamer-1.0/”, set this location with
|
||||
“<code>export GSTREAMER_ROOT_DIR=$HOME/my_gstreamer/lib</code>”).</p>
|
||||
<ul>
|
||||
<li>Most users will use the GStreamer supplied by their distribution,
|
||||
but a few (in particular users of Raspberry Pi OS Lite Legacy (Buster)
|
||||
@@ -382,8 +387,10 @@ decoding)</li>
|
||||
<li>“<strong>plugins-bad</strong>” (for h264 decoding).</li>
|
||||
</ol>
|
||||
<p>Plugins that may also be needed include “<strong>gl</strong>” for
|
||||
OpenGL support (which may be useful, and should be used with h264
|
||||
decoding by the NVIDIA GPU), and “<strong>x</strong>” for X11 support,
|
||||
OpenGL support (this provides the “-vs glimagesink” videosink, which can
|
||||
be very useful in many systems, and should always be used when using
|
||||
h264 decoding by a NVIDIA GPU), “<strong>gtk3</strong>” (which provides
|
||||
the “-vs gtksink” videosink), and “<strong>x</strong>” for X11 support,
|
||||
although these may already be installed; “<strong>vaapi</strong>” is
|
||||
needed for hardware-accelerated h264 video decoding by Intel or AMD
|
||||
graphics (but not for use with NVIDIA using proprietary drivers). If
|
||||
@@ -443,11 +450,12 @@ omitting the initial <code>"-"</code> of the command-line option. Lines
|
||||
in the configuration file beginning with <code>"#"</code> are treated as
|
||||
comments and ignored.</p>
|
||||
<p><strong>Run uxplay in a terminal window</strong>. On some systems,
|
||||
you can 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
|
||||
you can specify fullscreen mode with the <code>-fs</code> 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
|
||||
<code>systemctl status avahi-daemon</code>. If this shows the
|
||||
avahi-daemon is not running, control it with
|
||||
<code>sudo systemctl [start,stop,enable,disable] avahi-daemon</code> (on
|
||||
@@ -487,9 +495,10 @@ GStreamer internal clock used to try to keep them synchronized.
|
||||
<strong>Starting with UxPlay-1.64, the other method (GStreamer’s
|
||||
“<em>sync=true</em>” mode), which uses timestamps in the audio and video
|
||||
streams sent by the client, is the new default</strong>. On
|
||||
low-decoding-power UxPlay hosts (such as Raspberry Pi 3 models) this
|
||||
will drop video frames that cannot be decoded in time to play with the
|
||||
audio, making the video jerky, but still synchronized.</p></li>
|
||||
low-decoding-power UxPlay hosts (such as Raspberry Pi Zero W or 3 B+
|
||||
models) this will drop video frames that cannot be decoded in time to
|
||||
play with the audio, making the video jerky, but still
|
||||
synchronized.</p></li>
|
||||
</ul>
|
||||
<p>The older method which does not drop late video frames worked well on
|
||||
more powerful systems, and is still available with the UxPlay option
|
||||
@@ -513,12 +522,13 @@ before a pause or track-change initiated on the client takes effect on
|
||||
the audio played by the server.</li>
|
||||
</ul>
|
||||
<p>AirPlay volume-control attenuates volume (gain) by up to -30dB: the
|
||||
range -30dB:0dB can be rescaled from <em>Low</em>:0, or
|
||||
decibel range -30:0 can be rescaled from <em>Low</em>:0, or
|
||||
<em>Low</em>:<em>High</em>, using the option <code>-db</code> (“-db
|
||||
<em>Low</em>” or “-db <em>Low</em>:<em>High</em>”), <em>Low</em> must be
|
||||
negative. Rescaling is linear in decibels. The option
|
||||
<code>-taper</code> provides a “tapered” AirPlay volume-control profile
|
||||
some users may prefer.</p>
|
||||
negative. Rescaling is linear in decibels. Note that GStreamer’s audio
|
||||
format will “clip” any audio gain above +20db, so keep <em>High</em>
|
||||
below that level. The option <code>-taper</code> provides a “tapered”
|
||||
AirPlay volume-control profile some users may prefer.</p>
|
||||
<p>The -vsync and -async options also allow an optional positive (or
|
||||
negative) audio-delay adjustment in <em>milliseconds</em> for
|
||||
fine-tuning : <code>-vsync 20.5</code> delays audio relative to video by
|
||||
@@ -617,10 +627,10 @@ lower-power models to keep audio and video synchronized using
|
||||
timestamps. In Legacy Raspberry Pi OS (Bullseye), raspi-config
|
||||
“Performance Options” allows specifying how much memory to allocate to
|
||||
the GPU, but this setting appears to be absent in Bookworm (but it can
|
||||
still be set to e.g. 128GB by adding a line “gpu_mem=128” in
|
||||
/boot/config.txt). A Pi Zero 2 W (which has 512GB memory) worked well
|
||||
when tested in 32 bit Bullseye or Bookworm Lite with 128GB allocated to
|
||||
the GPU (default seems to be 64GB).</p>
|
||||
still be set to e.g. 128MB by adding a line “gpu_mem=128” in
|
||||
/boot/config.txt). A Pi Zero 2 W (which has 512MB memory) worked well
|
||||
when tested in 32 bit Bullseye or Bookworm Lite with 128MB allocated to
|
||||
the GPU (default seems to be 64MB).</p>
|
||||
<p>The basic uxplay options for R Pi are
|
||||
<code>uxplay [-vs <videosink>]</code>. The choice
|
||||
<code><videosink></code> = <code>glimagesink</code> is sometimes
|
||||
@@ -747,9 +757,10 @@ not affect the (small) initial OpenGL mirror window size, but the window
|
||||
can be expanded using the mouse or trackpad. In contrast, a window
|
||||
created with “-vs osxvideosink” is initially big, but has the wrong
|
||||
aspect ratio (stretched image); in this case the aspect ratio changes
|
||||
when the window width is changed by dragging its side; the option “-vs
|
||||
osxvideosink force-aspect-ratio=true” can be used to make the window
|
||||
have the correct aspect ratio when it first opens.</p></li>
|
||||
when the window width is changed by dragging its side; the option
|
||||
<code>-vs "osxvideosink force-aspect-ratio=true"</code> can be used to
|
||||
make the window have the correct aspect ratio when it first
|
||||
opens.</p></li>
|
||||
</ul>
|
||||
<h2
|
||||
id="building-uxplay-on-microsoft-windows-using-msys2-with-the-mingw-64-compiler.">Building
|
||||
@@ -790,9 +801,8 @@ environment (this uses “<code>ninja</code>” in place of
|
||||
install UxPlay dependencies (openssl is already installed with
|
||||
MSYS2):</p>
|
||||
<p><code>pacman -S mingw-w64-x86_64-libplist mingw-w64-x86_64-gstreamer mingw-w64-x86_64-gst-plugins-base</code></p>
|
||||
<p>Note that libplist will be linked statically to the uxplay
|
||||
executable. If you are trying a different Windows build system, MSVC
|
||||
versions of GStreamer for Windows are available from the <a
|
||||
<p>If you are trying a different Windows build system, MSVC versions of
|
||||
GStreamer for Windows are available from the <a
|
||||
href="https://gstreamer.freedesktop.org/download/">official GStreamer
|
||||
site</a>, but only the MinGW 64-bit build on MSYS2 has been
|
||||
tested.</p></li>
|
||||
@@ -856,14 +866,20 @@ used.</p>
|
||||
<code>-vs <videosink></code> option, some choices for
|
||||
<code><videosink></code> are <code>d3d11videosink</code>,
|
||||
<code>d3dvideosink</code>, <code>glimagesink</code>,
|
||||
<code>gtksink</code>. With Direct3D 11.0 or greater, you can get the
|
||||
ability to toggle into and out of fullscreen mode using the Alt-Enter
|
||||
key combination with option
|
||||
<code>gtksink</code>.</p>
|
||||
<ul>
|
||||
<li>With Direct3D 11.0 or greater, you can either always be in
|
||||
fullscreen mode using option
|
||||
<code>-vs "d3d11videosink fullscreen-toggle-mode=property fullscreen=true"</code>,
|
||||
or get the ability to toggle into and out of fullscreen mode using the
|
||||
Alt-Enter key combination with option
|
||||
<code>-vs "d3d11videosink fullscreen-toggle-mode=alt-enter"</code>. For
|
||||
convenience, this option will be added if just
|
||||
<code>-vs d3d11videosink</code> (by itself) is used. (You may wish to
|
||||
add “<code>vs d3d11videosink</code>” (no initial “<code>-</code>”) to
|
||||
the UxPlay startup options file; see “man uxplay” or “uxplay -h”.)</p>
|
||||
convenience, these options will be added if just
|
||||
<code>-vs d3d11videosink</code> with or without the fullscreen option
|
||||
“-fs” is used. <em>(Windows users may wish to add
|
||||
“<code>vs d3d11videosink</code>” (no initial “<code>-</code>”) to the
|
||||
UxPlay startup options file; see “man uxplay” or “uxplay -h”.)</em></li>
|
||||
</ul>
|
||||
<p>The executable uxplay.exe can also be run without the MSYS2
|
||||
environment, in the Windows Terminal, with
|
||||
<code>C:\msys64\mingw64\bin\uxplay</code>.</p>
|
||||
@@ -977,7 +993,7 @@ full-screen display that overscans, and is not displayed by gstreamer).
|
||||
Recommendation: <strong>don’t use this option</strong> unless there is
|
||||
some special reason to use it.</p>
|
||||
<p><strong>-fs</strong> uses fullscreen mode, but only works with X11,
|
||||
Wayland or VAAPI.</p>
|
||||
Wayland, VAAPI, and D3D11 (Windows).</p>
|
||||
<p><strong>-p</strong> allows you to select the network ports used by
|
||||
UxPlay (these need to be opened if the server is behind a firewall). By
|
||||
itself, -p sets “legacy” ports TCP 7100, 7000, 7001, UDP 6000, 6001,
|
||||
@@ -1082,6 +1098,10 @@ present, and synchronize with it). After <em>n</em> failures, the client
|
||||
will be presumed to be offline, and the connection will be reset to
|
||||
allow a new connection. The default value of <em>n</em> is 5; the value
|
||||
<em>n</em> = 0 means “no limit” on timeouts.</p>
|
||||
<p><strong>-nofreeze</strong> closes the video window after a reset due
|
||||
to ntp timeout (default is to leave window open to allow a smoother
|
||||
reconection to the same client). This option may be useful in fullscreen
|
||||
mode.</p>
|
||||
<p><strong>-nc</strong> maintains previous UxPlay < 1.45 behavior
|
||||
that does <strong>not close</strong> the video window when the the
|
||||
client sends the “Stop Mirroring” signal. <em>This option is currently
|
||||
@@ -1512,6 +1532,11 @@ 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.</p>
|
||||
<h1 id="changelog">Changelog</h1>
|
||||
<p>1.69 2024-08-09 Internal improvements (e.g. in -nohold option,
|
||||
identifying GStreamer videosink selected by autovideosink, finding X11
|
||||
display) in anticipation of future HLS video support. New -nofreeze
|
||||
option to not leave frozen video in place when a network connection is
|
||||
reset. Fixes for GStreamer-1.24.x changes.</p>
|
||||
<p>1.68 2023-12-31 New simpler (default) method for generating a
|
||||
persistent public key from the server MAC address (which can now be set
|
||||
with the -m option). (The previous method is still available with -key
|
||||
|
||||
79
README.md
79
README.md
@@ -1,19 +1,21 @@
|
||||
# UxPlay 1.68: AirPlay-Mirror and AirPlay-Audio server for Linux, macOS, and Unix (now also runs on Windows).
|
||||
# UxPlay 1.69: AirPlay-Mirror and AirPlay-Audio server for Linux, macOS, and Unix (now also runs on Windows).
|
||||
|
||||
### Now developed at the GitHub site [https://github.com/FDH2/UxPlay](https://github.com/FDH2/UxPlay) (where ALL user issues should be posted, and latest versions can be found).
|
||||
### **Now developed at the GitHub site [https://github.com/FDH2/UxPlay](https://github.com/FDH2/UxPlay) (where ALL user issues should be posted, and latest versions can be found).**
|
||||
|
||||
* _**NEW in v1.69**: minor changes for users: -nofreeze option to NOT leave frozen
|
||||
video in place when a network failure occurs; internal changes/improvements
|
||||
needed for planned future HLS video streaming support._
|
||||
|
||||
* **An experimental ("beta") version of UxPlay with support for HLS streaming of YouTube Videos from the YouTube app on an iOS client is now available at** https://github.com/FDH2/UxPlay/tree/video .
|
||||
_See the [Wiki page](https://github.com/FDH2/UxPlay/wiki/experimental-version-of-UxPlay-with-support-for-HLS-video-streaming-(you-tube-movies)) for details._
|
||||
|
||||
* _**NEW in v1.68**: Volume-control improvements, plus improved support for Apple-style one-time "pin" codes introduced in 1.67: a
|
||||
register of pin-registered clients can now optionally be maintained to check returning clients; a simpler method for generating
|
||||
a persistent public key (based on the MAC address, which can be set in the UxPlay startup file) is now the default. (The OpenSSL
|
||||
"pem-file" method introduced in 1.67 is still available with the "-key" option.)_
|
||||
|
||||
|
||||
## Highlights:
|
||||
|
||||
* GPLv3, open source.
|
||||
* Originally supported only AirPlay Mirror protocol, now has added support
|
||||
for AirPlay Audio-only (Apple Lossless ALAC) streaming
|
||||
from current iOS/iPadOS clients. **There is no support for Airplay2 video-streaming protocol, and none is planned.**
|
||||
from current iOS/iPadOS clients. **There is no current support for Airplay HLS
|
||||
video-streaming (e.g., YouTube video) but this is in development.**
|
||||
* macOS computers (2011 or later, both Intel and "Apple Silicon" M1/M2
|
||||
systems) can act either as AirPlay clients, or
|
||||
as the server running UxPlay. Using AirPlay, UxPlay can
|
||||
@@ -125,11 +127,12 @@ switch back by initiating a_ **Mirror** _mode connection; cover-art display stop
|
||||
the Apple TV app cannot be watched using UxPlay's AirPlay Mirror mode (only the unprotected audio will be streamed, in AAC format),
|
||||
but both video and audio content from DRM-free apps like "YouTube app" will be streamed by UxPlay in Mirror mode.**
|
||||
|
||||
* **As UxPlay does not support non-Mirror AirPlay2 video streaming (where the
|
||||
* **As UxPlay does not currently support non-Mirror AirPlay video streaming (where the
|
||||
client controls a web server on the AirPlay server that directly receives
|
||||
content to avoid it being decoded and re-encoded by the client),
|
||||
HLS content to avoid it being decoded and re-encoded by the client),
|
||||
using the icon for AirPlay video in apps such as the YouTube app
|
||||
will only send audio (in lossless ALAC format) without the accompanying video.**
|
||||
will only send audio (in lossless ALAC format) without the accompanying
|
||||
video (there are plans to support HLS video in future releases of UxPlay)**
|
||||
|
||||
### Possibility for using hardware-accelerated h264 video-decoding, if available.
|
||||
|
||||
@@ -210,9 +213,16 @@ Make sure that your distribution provides OpenSSL 1.1.1 or later, and
|
||||
libplist 2.0 or later. (This means Debian 10 "Buster" based systems (e.g, Ubuntu 18.04) or newer;
|
||||
on Debian 10 systems "libplist" is an older version, you need "libplist3".) If it does
|
||||
not, you may need to build and install these from
|
||||
source (see instructions at the end of this README). If you have a non-standard OpenSSL
|
||||
source (see instructions at the end of this README).
|
||||
|
||||
If you have a non-standard OpenSSL
|
||||
installation, you may need to set the environment variable OPENSSL_ROOT_DIR
|
||||
(_e.g._ , "`export OPENSSL_ROOT_DIR=/usr/local/lib64`" if that is where it is installed).
|
||||
Similarly, for non-standard (or multiple) GStreamer installations, set the
|
||||
environment variable GSTREAMER_ROOT_DIR to the directory that contains the
|
||||
".../gstreamer-1.0/" directory of the gstreamer installation that UxPlay should use
|
||||
(if this is _e.g._ "~/my_gstreamer/lib/gstreamer-1.0/", set this location
|
||||
with "`export GSTREAMER_ROOT_DIR=$HOME/my_gstreamer/lib`").
|
||||
|
||||
* Most users will use the GStreamer supplied by their distribution, but a few (in particular users
|
||||
of Raspberry Pi OS Lite Legacy (Buster) on a Raspberry Pi model 4B who wish to stay on that
|
||||
@@ -311,8 +321,9 @@ Values of `<plugin>` required are:
|
||||
3. "**plugins-good**" (for v4l2 hardware h264 decoding)
|
||||
4. "**plugins-bad**" (for h264 decoding).
|
||||
|
||||
Plugins that may also be needed include "**gl**" for OpenGL support (which may be useful, and should
|
||||
be used with h264 decoding by the NVIDIA GPU), and "**x**" for
|
||||
Plugins that may also be needed include "**gl**" for OpenGL support (this provides the "-vs glimagesink" videosink, which
|
||||
can be very useful in many systems, and should always be used when using h264 decoding by a NVIDIA GPU), "**gtk3**" (which
|
||||
provides the "-vs gtksink" videosink), and "**x**" for
|
||||
X11 support, although these may already be installed; "**vaapi**"
|
||||
is needed for hardware-accelerated h264 video decoding by Intel
|
||||
or AMD graphics (but not for use with NVIDIA using proprietary drivers). If sound is
|
||||
@@ -361,7 +372,8 @@ Since UxPlay-1.64, UxPlay can be started with options read from a configuration
|
||||
directory ("~"), (3) ``~/.config/uxplayrc``. The format is one option per line, omitting the initial ``"-"`` of
|
||||
the command-line option. Lines in the configuration file beginning with `"#"` are treated as comments and ignored.
|
||||
|
||||
**Run uxplay in a terminal window**. On some systems, you can toggle into and out of fullscreen mode
|
||||
**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
|
||||
@@ -415,9 +427,9 @@ delays the video on the client to match audio on the server, so leads to
|
||||
a slight delay before a pause or track-change initiated on the client takes effect on the audio played by the server.
|
||||
|
||||
AirPlay volume-control attenuates volume (gain) by up to -30dB: the decibel range -30:0 can be rescaled from _Low_:0, or _Low_:_High_, using the
|
||||
option `-db` ("-db _Low_ " or "-db _Low_:_High_ "), _Low_ must be negative. Rescaling is linear in decibels. The
|
||||
option ```-taper``` provides a "tapered" AirPlay volume-control
|
||||
profile some users may prefer.
|
||||
option `-db` ("-db _Low_ " or "-db _Low_:_High_ "), _Low_ must be negative. Rescaling is linear in decibels.
|
||||
Note that GStreamer's audio format will "clip" any audio gain above +20db, so keep *High* below that level. The
|
||||
option ```-taper``` provides a "tapered" AirPlay volume-control profile some users may prefer.
|
||||
|
||||
The -vsync and -async options
|
||||
also allow an optional positive (or negative) audio-delay adjustment in _milliseconds_ for fine-tuning : `-vsync 20.5`
|
||||
@@ -493,9 +505,9 @@ See [Usage](#usage) for more run-time options.
|
||||
|
||||
Even with GPU video decoding, some frames may be dropped by the lower-power models to keep audio and video synchronized
|
||||
using timestamps. In Legacy Raspberry Pi OS (Bullseye), raspi-config "Performance Options" allows specifying how much memory
|
||||
to allocate to the GPU, but this setting appears to be absent in Bookworm (but it can still be set to e.g. 128GB by adding a line "gpu_mem=128" in /boot/config.txt).
|
||||
A Pi Zero 2 W (which has 512GB memory) worked well when tested in 32 bit Bullseye or Bookworm Lite
|
||||
with 128GB allocated to the GPU (default seems to be 64GB).
|
||||
to allocate to the GPU, but this setting appears to be absent in Bookworm (but it can still be set to e.g. 128MB by adding a line "gpu_mem=128" in /boot/config.txt).
|
||||
A Pi Zero 2 W (which has 512MB memory) worked well when tested in 32 bit Bullseye or Bookworm Lite
|
||||
with 128MB allocated to the GPU (default seems to be 64MB).
|
||||
|
||||
The basic uxplay options for R Pi are ```uxplay [-vs <videosink>]```. The
|
||||
choice `<videosink>` = ``glimagesink`` is sometimes useful.
|
||||
@@ -633,7 +645,6 @@ After installing GStreamer, build and install uxplay: open a terminal and change
|
||||
|
||||
`pacman -S mingw-w64-x86_64-libplist mingw-w64-x86_64-gstreamer mingw-w64-x86_64-gst-plugins-base`
|
||||
|
||||
Note that libplist will be linked statically to the uxplay executable.
|
||||
If you are trying a different Windows build system, MSVC versions of GStreamer
|
||||
for Windows are available from the [official GStreamer site](https://gstreamer.freedesktop.org/download/),
|
||||
but only the MinGW 64-bit build on MSYS2 has been tested.
|
||||
@@ -687,9 +698,15 @@ default audio device is used.
|
||||
|
||||
If you wish to specify the videosink using the `-vs <videosink>` option, some choices for `<videosink>` are
|
||||
`d3d11videosink`, ``d3dvideosink``, ```glimagesink```,
|
||||
`gtksink`. With Direct3D 11.0 or greater, you can get the ability to toggle into and out of fullscreen mode using the Alt-Enter key combination with
|
||||
option `-vs "d3d11videosink fullscreen-toggle-mode=alt-enter"`. For convenience, this option will be added if just ``-vs d3d11videosink`` (by itself) is used.
|
||||
(You may wish to add "``vs d3d11videosink``" (no initial "`-`") to the UxPlay startup options file; see "man uxplay" or "uxplay -h".)
|
||||
`gtksink`.
|
||||
|
||||
* With Direct3D 11.0 or greater, you can either always be in fullscreen mode using
|
||||
option `-vs "d3d11videosink fullscreen-toggle-mode=property fullscreen=true"`, or
|
||||
get the ability to toggle into and out of fullscreen mode using the Alt-Enter key combination with
|
||||
option `-vs "d3d11videosink fullscreen-toggle-mode=alt-enter"`.
|
||||
For convenience, these options will be added if just ``-vs d3d11videosink`` with or without the fullscreen
|
||||
option "-fs" is used. _(Windows users may wish to add "``vs d3d11videosink``" (no initial "`-`") to the
|
||||
UxPlay startup options file; see "man uxplay" or "uxplay -h".)_
|
||||
|
||||
The executable uxplay.exe can also be run without the MSYS2 environment, in
|
||||
the Windows Terminal, with `C:\msys64\mingw64\bin\uxplay`.
|
||||
@@ -775,7 +792,7 @@ using UxPlay as a second monitor for a mac computer, or monitoring a webcam; wit
|
||||
Recommendation: **don't use this option** unless there is some special
|
||||
reason to use it.
|
||||
|
||||
**-fs** uses fullscreen mode, but only works with X11, Wayland or VAAPI.
|
||||
**-fs** uses fullscreen mode, but only works with X11, Wayland, VAAPI, and D3D11 (Windows).
|
||||
|
||||
**-p** allows you to select the network ports used by UxPlay (these need
|
||||
to be opened if the server is behind a firewall). By itself, -p sets
|
||||
@@ -861,6 +878,9 @@ which will not work if a firewall is running.
|
||||
_n_ failures, the client will be presumed to be offline, and the connection will be reset to allow a new
|
||||
connection. The default value of _n_ is 5; the value _n_ = 0 means "no limit" on timeouts.
|
||||
|
||||
**-nofreeze** closes the video window after a reset due to ntp timeout (default is to leave window
|
||||
open to allow a smoother reconection to the same client). This option may be useful in fullscreen mode.
|
||||
|
||||
**-nc** maintains previous UxPlay < 1.45 behavior that does **not close** the video window when the the client
|
||||
sends the "Stop Mirroring" signal. _This option is currently used by default in macOS,
|
||||
as the window created in macOS by GStreamer does not terminate correctly (it causes a segfault)
|
||||
@@ -1191,6 +1211,11 @@ tvOS 12.2.1), so it does not seem to matter what version UxPlay claims to be.
|
||||
|
||||
|
||||
# Changelog
|
||||
1.69 2024-08-09 Internal improvements (e.g. in -nohold option, identifying GStreamer videosink
|
||||
selected by autovideosink, finding X11 display) in anticipation of future HLS video support.
|
||||
New -nofreeze option to not leave frozen video in place when a network connection is reset.
|
||||
Fixes for GStreamer-1.24.x changes.
|
||||
|
||||
1.68 2023-12-31 New simpler (default) method for generating a persistent public key from the server MAC
|
||||
address (which can now be set with the -m option). (The previous method is still available
|
||||
with -key option). New option -reg to maintain a register of pin-authenticated clients. Corrected
|
||||
|
||||
148
README.txt
148
README.txt
@@ -1,23 +1,20 @@
|
||||
# UxPlay 1.68: AirPlay-Mirror and AirPlay-Audio server for Linux, macOS, and Unix (now also runs on Windows).
|
||||
# UxPlay 1.69: AirPlay-Mirror and AirPlay-Audio server for Linux, macOS, and Unix (now also runs on Windows).
|
||||
|
||||
### Now developed at the GitHub site <https://github.com/FDH2/UxPlay> (where ALL user issues should be posted, and latest versions can be found).
|
||||
### **Now developed at the GitHub site <https://github.com/FDH2/UxPlay> (where ALL user issues should be posted, and latest versions can be found).**
|
||||
|
||||
- ***NEW in v1.68**: Volume-control improvements, plus improved
|
||||
support for Apple-style one-time "pin" codes introduced in 1.67: a
|
||||
register of pin-registered clients can now optionally be maintained
|
||||
to check returning clients; a simpler method for generating a
|
||||
persistent public key (based on the MAC address, which can be set in
|
||||
the UxPlay startup file) is now the default. (The OpenSSL "pem-file"
|
||||
method introduced in 1.67 is still available with the "-key"
|
||||
option.)*
|
||||
- ***NEW in v1.69**: minor changes for users: -nofreeze option to NOT
|
||||
leave frozen video in place when a network failure occurs; internal
|
||||
changes/improvements needed for planned future HLS video streaming
|
||||
support.*
|
||||
|
||||
## Highlights:
|
||||
|
||||
- GPLv3, open source.
|
||||
- Originally supported only AirPlay Mirror protocol, now has added
|
||||
support for AirPlay Audio-only (Apple Lossless ALAC) streaming from
|
||||
current iOS/iPadOS clients. **There is no support for Airplay2
|
||||
video-streaming protocol, and none is planned.**
|
||||
current iOS/iPadOS clients. **There is no current support for
|
||||
Airplay HLS video-streaming (e.g., YouTube video) but this is in
|
||||
development.**
|
||||
- macOS computers (2011 or later, both Intel and "Apple Silicon" M1/M2
|
||||
systems) can act either as AirPlay clients, or as the server running
|
||||
UxPlay. Using AirPlay, UxPlay can emulate a second display for macOS
|
||||
@@ -158,12 +155,13 @@ stops/restarts as you leave/re-enter* **Audio** *mode.*
|
||||
and audio content from DRM-free apps like "YouTube app" will be
|
||||
streamed by UxPlay in Mirror mode.**
|
||||
|
||||
- **As UxPlay does not support non-Mirror AirPlay2 video streaming
|
||||
(where the client controls a web server on the AirPlay server that
|
||||
directly receives content to avoid it being decoded and re-encoded
|
||||
by the client), using the icon for AirPlay video in apps such as the
|
||||
YouTube app will only send audio (in lossless ALAC format) without
|
||||
the accompanying video.**
|
||||
- **As UxPlay does not currently support non-Mirror AirPlay video
|
||||
streaming (where the client controls a web server on the AirPlay
|
||||
server that directly receives HLS content to avoid it being decoded
|
||||
and re-encoded by the client), using the icon for AirPlay video in
|
||||
apps such as the YouTube app will only send audio (in lossless ALAC
|
||||
format) without the accompanying video (there are plans to support
|
||||
HLS video in future releases of UxPlay)**
|
||||
|
||||
### Possibility for using hardware-accelerated h264 video-decoding, if available.
|
||||
|
||||
@@ -257,10 +255,17 @@ libplist 2.0 or later. (This means Debian 10 "Buster" based systems
|
||||
(e.g, Ubuntu 18.04) or newer; on Debian 10 systems "libplist" is an
|
||||
older version, you need "libplist3".) If it does not, you may need to
|
||||
build and install these from source (see instructions at the end of this
|
||||
README). If you have a non-standard OpenSSL installation, you may need
|
||||
to set the environment variable OPENSSL_ROOT_DIR (*e.g.* ,
|
||||
README).
|
||||
|
||||
If you have a non-standard OpenSSL installation, you may need to set the
|
||||
environment variable OPENSSL_ROOT_DIR (*e.g.* ,
|
||||
"`export OPENSSL_ROOT_DIR=/usr/local/lib64`" if that is where it is
|
||||
installed).
|
||||
installed). Similarly, for non-standard (or multiple) GStreamer
|
||||
installations, set the environment variable GSTREAMER_ROOT_DIR to the
|
||||
directory that contains the ".../gstreamer-1.0/" directory of the
|
||||
gstreamer installation that UxPlay should use (if this is *e.g.*
|
||||
"\~/my_gstreamer/lib/gstreamer-1.0/", set this location with
|
||||
"`export GSTREAMER_ROOT_DIR=$HOME/my_gstreamer/lib`").
|
||||
|
||||
- Most users will use the GStreamer supplied by their distribution,
|
||||
but a few (in particular users of Raspberry Pi OS Lite Legacy
|
||||
@@ -378,13 +383,15 @@ are:
|
||||
4. "**plugins-bad**" (for h264 decoding).
|
||||
|
||||
Plugins that may also be needed include "**gl**" for OpenGL support
|
||||
(which may be useful, and should be used with h264 decoding by the
|
||||
NVIDIA GPU), and "**x**" for X11 support, although these may already be
|
||||
installed; "**vaapi**" is needed for hardware-accelerated h264 video
|
||||
decoding by Intel or AMD graphics (but not for use with NVIDIA using
|
||||
proprietary drivers). If sound is not working,
|
||||
"**alsa**"","**pulseaudio**", or "**pipewire**" plugins may need to be
|
||||
installed, depending on how your audio is set up.
|
||||
(this provides the "-vs glimagesink" videosink, which can be very useful
|
||||
in many systems, and should always be used when using h264 decoding by a
|
||||
NVIDIA GPU), "**gtk3**" (which provides the "-vs gtksink" videosink),
|
||||
and "**x**" for X11 support, although these may already be installed;
|
||||
"**vaapi**" is needed for hardware-accelerated h264 video decoding by
|
||||
Intel or AMD graphics (but not for use with NVIDIA using proprietary
|
||||
drivers). If sound is not working, "**alsa**"","**pulseaudio**", or
|
||||
"**pipewire**" plugins may need to be installed, depending on how your
|
||||
audio is set up.
|
||||
|
||||
- Also install "**gstreamer1.0-tools**" to get the utility
|
||||
gst-inspect-1.0 for examining the GStreamer installation.
|
||||
@@ -436,14 +443,14 @@ one option per line, omitting the initial `"-"` of the command-line
|
||||
option. Lines in the configuration file beginning with `"#"` are treated
|
||||
as comments and ignored.
|
||||
|
||||
**Run uxplay in a terminal window**. On some systems, you can 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
|
||||
**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
|
||||
`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
|
||||
@@ -481,9 +488,9 @@ below for help with this or other problems.
|
||||
with UxPlay-1.64, the other method (GStreamer's "*sync=true*" mode),
|
||||
which uses timestamps in the audio and video streams sent by the
|
||||
client, is the new default**. On low-decoding-power UxPlay hosts
|
||||
(such as Raspberry Pi 3 models) this will drop video frames that
|
||||
cannot be decoded in time to play with the audio, making the video
|
||||
jerky, but still synchronized.
|
||||
(such as Raspberry Pi Zero W or 3 B+ models) this will drop video
|
||||
frames that cannot be decoded in time to play with the audio, making
|
||||
the video jerky, but still synchronized.
|
||||
|
||||
The older method which does not drop late video frames worked well on
|
||||
more powerful systems, and is still available with the UxPlay option
|
||||
@@ -506,10 +513,12 @@ helped to prevent this previously when timestamps were not being used.)
|
||||
takes effect on the audio played by the server.
|
||||
|
||||
AirPlay volume-control attenuates volume (gain) by up to -30dB: the
|
||||
range -30dB:0dB can be rescaled from *Low*:0, or *Low*:*High*, using the
|
||||
option `-db` ("-db *Low*" or "-db *Low*:*High*"), *Low* must be
|
||||
negative. Rescaling is linear in decibels. The option `-taper` provides
|
||||
a "tapered" AirPlay volume-control profile some users may prefer.
|
||||
decibel range -30:0 can be rescaled from *Low*:0, or *Low*:*High*, using
|
||||
the option `-db` ("-db *Low*" or "-db *Low*:*High*"), *Low* must be
|
||||
negative. Rescaling is linear in decibels. Note that GStreamer's audio
|
||||
format will "clip" any audio gain above +20db, so keep *High* below that
|
||||
level. The option `-taper` provides a "tapered" AirPlay volume-control
|
||||
profile some users may prefer.
|
||||
|
||||
The -vsync and -async options also allow an optional positive (or
|
||||
negative) audio-delay adjustment in *milliseconds* for fine-tuning :
|
||||
@@ -615,10 +624,10 @@ lower-power models to keep audio and video synchronized using
|
||||
timestamps. In Legacy Raspberry Pi OS (Bullseye), raspi-config
|
||||
"Performance Options" allows specifying how much memory to allocate to
|
||||
the GPU, but this setting appears to be absent in Bookworm (but it can
|
||||
still be set to e.g. 128GB by adding a line "gpu_mem=128" in
|
||||
/boot/config.txt). A Pi Zero 2 W (which has 512GB memory) worked well
|
||||
when tested in 32 bit Bullseye or Bookworm Lite with 128GB allocated to
|
||||
the GPU (default seems to be 64GB).
|
||||
still be set to e.g. 128MB by adding a line "gpu_mem=128" in
|
||||
/boot/config.txt). A Pi Zero 2 W (which has 512MB memory) worked well
|
||||
when tested in 32 bit Bullseye or Bookworm Lite with 128MB allocated to
|
||||
the GPU (default seems to be 64MB).
|
||||
|
||||
The basic uxplay options for R Pi are `uxplay [-vs <videosink>]`. The
|
||||
choice `<videosink>` = `glimagesink` is sometimes useful. With the
|
||||
@@ -751,7 +760,7 @@ downloads, "UxPlay" for "git clone" downloads) and build/install with
|
||||
created with "-vs osxvideosink" is initially big, but has the wrong
|
||||
aspect ratio (stretched image); in this case the aspect ratio
|
||||
changes when the window width is changed by dragging its side; the
|
||||
option "-vs osxvideosink force-aspect-ratio=true" can be used to
|
||||
option `-vs "osxvideosink force-aspect-ratio=true"` can be used to
|
||||
make the window have the correct aspect ratio when it first opens.
|
||||
|
||||
## Building UxPlay on Microsoft Windows, using MSYS2 with the MinGW-64 compiler.
|
||||
@@ -794,11 +803,10 @@ downloads, "UxPlay" for "git clone" downloads) and build/install with
|
||||
|
||||
`pacman -S mingw-w64-x86_64-libplist mingw-w64-x86_64-gstreamer mingw-w64-x86_64-gst-plugins-base`
|
||||
|
||||
Note that libplist will be linked statically to the uxplay
|
||||
executable. If you are trying a different Windows build system, MSVC
|
||||
versions of GStreamer for Windows are available from the [official
|
||||
GStreamer site](https://gstreamer.freedesktop.org/download/), but
|
||||
only the MinGW 64-bit build on MSYS2 has been tested.
|
||||
If you are trying a different Windows build system, MSVC versions of
|
||||
GStreamer for Windows are available from the [official GStreamer
|
||||
site](https://gstreamer.freedesktop.org/download/), but only the
|
||||
MinGW 64-bit build on MSYS2 has been tested.
|
||||
|
||||
5. cd to the UxPlay source directory, then "`mkdir build`" and
|
||||
"`cd build`". The build process assumes that the Bonjour SDK is
|
||||
@@ -860,14 +868,19 @@ like `\{0.0.0.00000000\}.\{98e35b2b-8eba-412e-b840-fd2c2492cf44\}`. If
|
||||
|
||||
If you wish to specify the videosink using the `-vs <videosink>` option,
|
||||
some choices for `<videosink>` are `d3d11videosink`, `d3dvideosink`,
|
||||
`glimagesink`, `gtksink`. With Direct3D 11.0 or greater, you can get the
|
||||
ability to toggle into and out of fullscreen mode using the Alt-Enter
|
||||
key combination with option
|
||||
`-vs "d3d11videosink fullscreen-toggle-mode=alt-enter"`. For
|
||||
convenience, this option will be added if just `-vs d3d11videosink` (by
|
||||
itself) is used. (You may wish to add "`vs d3d11videosink`" (no initial
|
||||
"`-`") to the UxPlay startup options file; see "man uxplay" or "uxplay
|
||||
-h".)
|
||||
`glimagesink`, `gtksink`.
|
||||
|
||||
- With Direct3D 11.0 or greater, you can either always be in
|
||||
fullscreen mode using option
|
||||
`-vs "d3d11videosink fullscreen-toggle-mode=property fullscreen=true"`,
|
||||
or get the ability to toggle into and out of fullscreen mode using
|
||||
the Alt-Enter key combination with option
|
||||
`-vs "d3d11videosink fullscreen-toggle-mode=alt-enter"`. For
|
||||
convenience, these options will be added if just
|
||||
`-vs d3d11videosink` with or without the fullscreen option "-fs" is
|
||||
used. *(Windows users may wish to add "`vs d3d11videosink`" (no
|
||||
initial "`-`") to the UxPlay startup options file; see "man uxplay"
|
||||
or "uxplay -h".)*
|
||||
|
||||
The executable uxplay.exe can also be run without the MSYS2 environment,
|
||||
in the Windows Terminal, with `C:\msys64\mingw64\bin\uxplay`.
|
||||
@@ -987,7 +1000,8 @@ display that overscans, and is not displayed by gstreamer).
|
||||
Recommendation: **don't use this option** unless there is some special
|
||||
reason to use it.
|
||||
|
||||
**-fs** uses fullscreen mode, but only works with X11, Wayland or VAAPI.
|
||||
**-fs** uses fullscreen mode, but only works with X11, Wayland, VAAPI,
|
||||
and D3D11 (Windows).
|
||||
|
||||
**-p** allows you to select the network ports used by UxPlay (these need
|
||||
to be opened if the server is behind a firewall). By itself, -p sets
|
||||
@@ -1101,6 +1115,10 @@ it). After *n* failures, the client will be presumed to be offline, and
|
||||
the connection will be reset to allow a new connection. The default
|
||||
value of *n* is 5; the value *n* = 0 means "no limit" on timeouts.
|
||||
|
||||
**-nofreeze** closes the video window after a reset due to ntp timeout
|
||||
(default is to leave window open to allow a smoother reconection to the
|
||||
same client). This option may be useful in fullscreen mode.
|
||||
|
||||
**-nc** maintains previous UxPlay \< 1.45 behavior that does **not
|
||||
close** the video window when the the client sends the "Stop Mirroring"
|
||||
signal. *This option is currently used by default in macOS, as the
|
||||
@@ -1554,6 +1572,12 @@ what version UxPlay claims to be.
|
||||
|
||||
# Changelog
|
||||
|
||||
1.69 2024-08-09 Internal improvements (e.g. in -nohold option,
|
||||
identifying GStreamer videosink selected by autovideosink, finding X11
|
||||
display) in anticipation of future HLS video support. New -nofreeze
|
||||
option to not leave frozen video in place when a network connection is
|
||||
reset. Fixes for GStreamer-1.24.x changes.
|
||||
|
||||
1.68 2023-12-31 New simpler (default) method for generating a persistent
|
||||
public key from the server MAC address (which can now be set with the -m
|
||||
option). (The previous method is still available with -key option). New
|
||||
|
||||
@@ -80,17 +80,19 @@ else()
|
||||
endif()
|
||||
|
||||
# libplist
|
||||
if( APPLE OR WIN32 )
|
||||
pkg_search_module(PLIST REQUIRED libplist-2.0)
|
||||
if ( PLIST_FOUND )
|
||||
message( STATUS "found libplist-${PLIST_VERSION}" )
|
||||
endif()
|
||||
if( APPLE )
|
||||
# use static linking
|
||||
pkg_search_module( PLIST REQUIRED libplist-2.0 )
|
||||
find_library( LIBPLIST libplist-2.0.a REQUIRED )
|
||||
message( STATUS "(Static linking) LIBPLIST " ${LIBPLIST} )
|
||||
target_link_libraries ( airplay ${LIBPLIST} )
|
||||
elseif( WIN32)
|
||||
find_library( LIBPLIST ${PLIST_LIBRARIES} PATH ${PLIST_LIBDIR} )
|
||||
target_link_libraries ( airplay ${LIBPLIST} )
|
||||
else ()
|
||||
pkg_search_module(PLIST libplist>=2.0)
|
||||
if(NOT PLIST_FOUND)
|
||||
pkg_search_module(PLIST REQUIRED libplist-2.0)
|
||||
endif()
|
||||
find_library( LIBPLIST ${PLIST_LIBRARIES} PATH ${PLIST_LIBDIR} )
|
||||
target_link_libraries ( airplay PUBLIC ${LIBPLIST} )
|
||||
endif()
|
||||
|
||||
@@ -29,6 +29,7 @@ struct http_request_s {
|
||||
|
||||
const char *method;
|
||||
char *url;
|
||||
char protocol[9];
|
||||
|
||||
char **headers;
|
||||
int headers_size;
|
||||
@@ -51,6 +52,9 @@ on_url(llhttp_t *parser, const char *at, size_t length)
|
||||
|
||||
request->url[urllen] = '\0';
|
||||
strncat(request->url, at, length);
|
||||
|
||||
strncpy(request->protocol, at + length + 1, 8);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -183,8 +187,11 @@ http_request_add_data(http_request_t *request, const char *data, int datalen)
|
||||
|
||||
assert(request);
|
||||
|
||||
ret = llhttp_execute(&request->parser,
|
||||
data, datalen);
|
||||
ret = llhttp_execute(&request->parser, data, datalen);
|
||||
|
||||
/* support for "Upgrade" to reverse http ("PTTH/1.0") protocol */
|
||||
llhttp_resume_after_upgrade(&request->parser);
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
@@ -230,6 +237,13 @@ http_request_get_url(http_request_t *request)
|
||||
return request->url;
|
||||
}
|
||||
|
||||
const char *
|
||||
http_request_get_protocol(http_request_t *request)
|
||||
{
|
||||
assert(request);
|
||||
return request->protocol;
|
||||
}
|
||||
|
||||
const char *
|
||||
http_request_get_header(http_request_t *request, const char *name)
|
||||
{
|
||||
|
||||
@@ -28,6 +28,7 @@ const char *http_request_get_error_name(http_request_t *request);
|
||||
const char *http_request_get_error_description(http_request_t *request);
|
||||
const char *http_request_get_method(http_request_t *request);
|
||||
const char *http_request_get_url(http_request_t *request);
|
||||
const char *http_request_get_protocol(http_request_t *request);
|
||||
const char *http_request_get_header(http_request_t *request, const char *name);
|
||||
const char *http_request_get_data(http_request_t *request, int *datalen);
|
||||
int http_request_get_header_string(http_request_t *request, char **header_str);
|
||||
|
||||
@@ -51,10 +51,29 @@ http_response_add_data(http_response_t *response, const char *data, int datalen)
|
||||
response->data_length += datalen;
|
||||
}
|
||||
|
||||
|
||||
http_response_t *
|
||||
http_response_init(const char *protocol, int code, const char *message)
|
||||
http_response_create()
|
||||
{
|
||||
http_response_t *response;
|
||||
http_response_t *response = (http_response_t *) calloc(1, sizeof(http_response_t));
|
||||
if (!response) {
|
||||
return NULL;
|
||||
}
|
||||
/* Allocate response data */
|
||||
response->data_size = 1024;
|
||||
response->data = (char *) malloc(response->data_size);
|
||||
if (!response->data) {
|
||||
free(response);
|
||||
return NULL;
|
||||
}
|
||||
return response;
|
||||
}
|
||||
|
||||
void
|
||||
http_response_init(http_response_t *response, const char *protocol, int code, const char *message)
|
||||
{
|
||||
assert(response);
|
||||
response->data_length = 0; /* can be used to reinitialize a previously-initialized response */
|
||||
char codestr[4];
|
||||
|
||||
assert(code >= 100 && code < 1000);
|
||||
@@ -63,19 +82,6 @@ http_response_init(const char *protocol, int code, const char *message)
|
||||
memset(codestr, 0, sizeof(codestr));
|
||||
snprintf(codestr, sizeof(codestr), "%u", code);
|
||||
|
||||
response = calloc(1, sizeof(http_response_t));
|
||||
if (!response) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/* Allocate response data */
|
||||
response->data_size = 1024;
|
||||
response->data = malloc(response->data_size);
|
||||
if (!response->data) {
|
||||
free(response);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/* Add first line of response to the data array */
|
||||
http_response_add_data(response, protocol, strlen(protocol));
|
||||
http_response_add_data(response, " ", 1);
|
||||
@@ -83,8 +89,6 @@ http_response_init(const char *protocol, int code, const char *message)
|
||||
http_response_add_data(response, " ", 1);
|
||||
http_response_add_data(response, message, strlen(message));
|
||||
http_response_add_data(response, "\r\n", 2);
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
void
|
||||
|
||||
@@ -10,6 +10,9 @@
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
* Lesser General Public License for more details.
|
||||
*
|
||||
*==============================================================
|
||||
* modified fduncanh 2024
|
||||
*/
|
||||
|
||||
#ifndef HTTP_RESPONSE_H
|
||||
@@ -17,7 +20,8 @@
|
||||
|
||||
typedef struct http_response_s http_response_t;
|
||||
|
||||
http_response_t *http_response_init(const char *protocol, int code, const char *message);
|
||||
http_response_t *http_response_create();
|
||||
void http_response_init(http_response_t *response, const char *protocol, int code, const char *message);
|
||||
|
||||
void http_response_add_header(http_response_t *response, const char *name, const char *value);
|
||||
void http_response_finish(http_response_t *response, const char *data, int datalen);
|
||||
|
||||
115
lib/httpd.c
115
lib/httpd.c
@@ -19,6 +19,7 @@
|
||||
#include <string.h>
|
||||
#include <stdio.h>
|
||||
#include <assert.h>
|
||||
#include <stdbool.h>
|
||||
|
||||
#include "httpd.h"
|
||||
#include "netutils.h"
|
||||
@@ -31,6 +32,7 @@ struct http_connection_s {
|
||||
|
||||
int socket_fd;
|
||||
void *user_data;
|
||||
connection_type_t type;
|
||||
http_request_t *request;
|
||||
};
|
||||
typedef struct http_connection_s http_connection_t;
|
||||
@@ -42,6 +44,7 @@ struct httpd_s {
|
||||
int max_connections;
|
||||
int open_connections;
|
||||
http_connection_t *connections;
|
||||
char nohold;
|
||||
|
||||
/* These variables only edited mutex locked */
|
||||
int running;
|
||||
@@ -54,14 +57,43 @@ struct httpd_s {
|
||||
int server_fd6;
|
||||
};
|
||||
|
||||
int
|
||||
httpd_set_connection_type (httpd_t *httpd, void *user_data, connection_type_t type) {
|
||||
for (int i = 0; i < httpd->max_connections; i++) {
|
||||
http_connection_t *connection = &httpd->connections[i];
|
||||
if (!connection->connected) {
|
||||
continue;
|
||||
}
|
||||
if (connection->user_data == user_data) {
|
||||
connection->type = type;
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
int
|
||||
httpd_count_connection_type (httpd_t *httpd, connection_type_t type) {
|
||||
int count = 0;
|
||||
for (int i = 0; i < httpd->max_connections; i++) {
|
||||
http_connection_t *connection = &httpd->connections[i];
|
||||
if (!connection->connected) {
|
||||
continue;
|
||||
}
|
||||
if (connection->type == type) {
|
||||
count++;
|
||||
}
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
#define MAX_CONNECTIONS 12 /* value used in AppleTV 3*/
|
||||
httpd_t *
|
||||
httpd_init(logger_t *logger, httpd_callbacks_t *callbacks, int max_connections)
|
||||
httpd_init(logger_t *logger, httpd_callbacks_t *callbacks, int nohold)
|
||||
{
|
||||
httpd_t *httpd;
|
||||
|
||||
assert(logger);
|
||||
assert(callbacks);
|
||||
assert(max_connections > 0);
|
||||
|
||||
/* Allocate the httpd_t structure */
|
||||
httpd = calloc(1, sizeof(httpd_t));
|
||||
@@ -69,8 +101,10 @@ httpd_init(logger_t *logger, httpd_callbacks_t *callbacks, int max_connections)
|
||||
return NULL;
|
||||
}
|
||||
|
||||
httpd->max_connections = max_connections;
|
||||
httpd->connections = calloc(max_connections, sizeof(http_connection_t));
|
||||
|
||||
httpd->nohold = (nohold ? 1 : 0);
|
||||
httpd->max_connections = MAX_CONNECTIONS;
|
||||
httpd->connections = calloc(httpd->max_connections, sizeof(http_connection_t));
|
||||
if (!httpd->connections) {
|
||||
free(httpd);
|
||||
return NULL;
|
||||
@@ -111,11 +145,14 @@ httpd_remove_connection(httpd_t *httpd, http_connection_t *connection)
|
||||
shutdown(connection->socket_fd, SHUT_WR);
|
||||
closesocket(connection->socket_fd);
|
||||
connection->connected = 0;
|
||||
connection->user_data = NULL;
|
||||
connection->type = CONNECTION_TYPE_UNKNOWN;
|
||||
httpd->open_connections--;
|
||||
}
|
||||
|
||||
static int
|
||||
httpd_add_connection(httpd_t *httpd, int fd, unsigned char *local, int local_len, unsigned char *remote, int remote_len)
|
||||
httpd_add_connection(httpd_t *httpd, int fd, unsigned char *local, int local_len, unsigned char *remote,
|
||||
int remote_len, unsigned int zone_id)
|
||||
{
|
||||
void *user_data;
|
||||
int i;
|
||||
@@ -130,8 +167,7 @@ httpd_add_connection(httpd_t *httpd, int fd, unsigned char *local, int local_len
|
||||
logger_log(httpd->logger, LOGGER_INFO, "Max connections reached");
|
||||
return -1;
|
||||
}
|
||||
|
||||
user_data = httpd->callbacks.conn_init(httpd->callbacks.opaque, local, local_len, remote, remote_len);
|
||||
user_data = httpd->callbacks.conn_init(httpd->callbacks.opaque, local, local_len, remote, remote_len, zone_id);
|
||||
if (!user_data) {
|
||||
logger_log(httpd->logger, LOGGER_ERR, "Error initializing HTTP request handler");
|
||||
return -1;
|
||||
@@ -141,6 +177,7 @@ httpd_add_connection(httpd_t *httpd, int fd, unsigned char *local, int local_len
|
||||
httpd->connections[i].socket_fd = fd;
|
||||
httpd->connections[i].connected = 1;
|
||||
httpd->connections[i].user_data = user_data;
|
||||
httpd->connections[i].type = CONNECTION_TYPE_UNKNOWN; //should not be necessary ...
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -152,6 +189,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 int local_zone_id, remote_zone_id;
|
||||
int local_len, remote_len;
|
||||
int ret, fd;
|
||||
|
||||
@@ -172,25 +210,11 @@ httpd_accept_connection(httpd_t *httpd, int server_fd, int is_ipv6)
|
||||
|
||||
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);
|
||||
remote = netutils_get_address(&remote_saddr, &remote_len);
|
||||
|
||||
#ifdef NOHOLD
|
||||
/* remove existing connections to make way for new connections:
|
||||
* this will only occur if max_connections > 2 */
|
||||
if (httpd->open_connections >= 2) {
|
||||
logger_log(httpd->logger, LOGGER_INFO, "Destroying current connections to allow connection by new client");
|
||||
for (int i = 0; i<httpd->max_connections; i++) {
|
||||
http_connection_t *connection = &httpd->connections[i];
|
||||
if (!connection->connected) {
|
||||
continue;
|
||||
}
|
||||
httpd_remove_connection(httpd, connection);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
local = netutils_get_address(&local_saddr, &local_len, &local_zone_id);
|
||||
remote = netutils_get_address(&remote_saddr, &remote_len, &remote_zone_id);
|
||||
assert (local_zone_id == remote_zone_id);
|
||||
|
||||
ret = httpd_add_connection(httpd, fd, local, local_len, remote, remote_len);
|
||||
ret = httpd_add_connection(httpd, fd, local, local_len, remote, remote_len, local_zone_id);
|
||||
if (ret == -1) {
|
||||
shutdown(fd, SHUT_RDWR);
|
||||
closesocket(fd);
|
||||
@@ -199,13 +223,30 @@ httpd_accept_connection(httpd_t *httpd, int server_fd, int is_ipv6)
|
||||
return 1;
|
||||
}
|
||||
|
||||
bool
|
||||
httpd_nohold(httpd_t *httpd) {
|
||||
return (httpd->nohold ? true: false);
|
||||
}
|
||||
|
||||
void
|
||||
httpd_remove_known_connections(httpd_t *httpd) {
|
||||
for (int i = 0; i < httpd->max_connections; i++) {
|
||||
http_connection_t *connection = &httpd->connections[i];
|
||||
if (!connection->connected || connection->type == CONNECTION_TYPE_UNKNOWN) {
|
||||
continue;
|
||||
}
|
||||
httpd_remove_connection(httpd, connection);
|
||||
}
|
||||
}
|
||||
|
||||
static THREAD_RETVAL
|
||||
httpd_thread(void *arg)
|
||||
{
|
||||
httpd_t *httpd = arg;
|
||||
char buffer[1024];
|
||||
int i;
|
||||
|
||||
bool logger_debug = (logger_get_level(httpd->logger) >= LOGGER_DEBUG);
|
||||
|
||||
assert(httpd);
|
||||
|
||||
while (1) {
|
||||
@@ -298,7 +339,7 @@ httpd_thread(void *arg)
|
||||
assert(connection->request);
|
||||
}
|
||||
|
||||
logger_log(httpd->logger, LOGGER_DEBUG, "httpd receiving on socket %d", connection->socket_fd);
|
||||
logger_log(httpd->logger, LOGGER_DEBUG, "httpd receiving on socket %d, connection %d", connection->socket_fd, i);
|
||||
ret = recv(connection->socket_fd, buffer, sizeof(buffer), 0);
|
||||
if (ret == 0) {
|
||||
logger_log(httpd->logger, LOGGER_INFO, "Connection closed for socket %d", connection->socket_fd);
|
||||
@@ -318,6 +359,13 @@ httpd_thread(void *arg)
|
||||
if (http_request_is_complete(connection->request)) {
|
||||
http_response_t *response = NULL;
|
||||
// Callback the received data to raop
|
||||
if (logger_debug) {
|
||||
const char *method = http_request_get_method(connection->request);
|
||||
const char *url = http_request_get_url(connection->request);
|
||||
const char *protocol = http_request_get_protocol(connection->request);
|
||||
logger_log(httpd->logger, LOGGER_INFO, "httpd request received on socket %d, connection %d, "
|
||||
"method = %s, url = %s, protocol = %s", connection->socket_fd, i, method, url, protocol);
|
||||
}
|
||||
httpd->callbacks.conn_request(connection->user_data, connection->request, &response);
|
||||
http_request_destroy(connection->request);
|
||||
connection->request = NULL;
|
||||
@@ -409,11 +457,11 @@ httpd_start(httpd_t *httpd, unsigned short *port)
|
||||
MUTEX_UNLOCK(httpd->run_mutex);
|
||||
return -1;
|
||||
}
|
||||
httpd->server_fd6 = -1;/*= netutils_init_socket(port, 1, 0);
|
||||
if (httpd->server_fd6 == -1) {
|
||||
logger_log(httpd->logger, LOGGER_WARNING, "Error initialising IPv6 socket %d", SOCKET_GET_ERROR());
|
||||
logger_log(httpd->logger, LOGGER_WARNING, "Continuing without IPv6 support");
|
||||
}*/
|
||||
httpd->server_fd6 = netutils_init_socket(port, 1, 0);
|
||||
if (httpd->server_fd6 == -1) {
|
||||
logger_log(httpd->logger, LOGGER_WARNING, "Error initialising IPv6 socket %d", SOCKET_GET_ERROR());
|
||||
logger_log(httpd->logger, LOGGER_WARNING, "Continuing without IPv6 support");
|
||||
}
|
||||
|
||||
if (httpd->server_fd4 != -1 && listen(httpd->server_fd4, backlog) == -1) {
|
||||
logger_log(httpd->logger, LOGGER_ERR, "Error listening to IPv4 socket");
|
||||
@@ -473,4 +521,3 @@ httpd_stop(httpd_t *httpd)
|
||||
httpd->joined = 1;
|
||||
MUTEX_UNLOCK(httpd->run_mutex);
|
||||
}
|
||||
|
||||
|
||||
20
lib/httpd.h
20
lib/httpd.h
@@ -21,16 +21,26 @@
|
||||
|
||||
typedef struct httpd_s httpd_t;
|
||||
|
||||
typedef enum connectype_type_e {
|
||||
CONNECTION_TYPE_UNKNOWN,
|
||||
CONNECTION_TYPE_RAOP
|
||||
} connection_type_t;
|
||||
|
||||
struct httpd_callbacks_s {
|
||||
void* opaque;
|
||||
void* (*conn_init)(void *opaque, unsigned char *local, int locallen, unsigned char *remote, int remotelen);
|
||||
void (*conn_request)(void *ptr, http_request_t *request, http_response_t **response);
|
||||
void (*conn_destroy)(void *ptr);
|
||||
void* opaque;
|
||||
void* (*conn_init)(void *opaque, unsigned char *local, int locallen, unsigned char *remote,
|
||||
int remotelen, unsigned int zone_id);
|
||||
void (*conn_request)(void *ptr, http_request_t *request, http_response_t **response);
|
||||
void (*conn_destroy)(void *ptr);
|
||||
};
|
||||
typedef struct httpd_callbacks_s httpd_callbacks_t;
|
||||
bool httpd_nohold(httpd_t *httpd);
|
||||
void httpd_remove_known_connections(httpd_t *httpd);
|
||||
|
||||
int httpd_set_connection_type (httpd_t *http, void *user_data, connection_type_t type);
|
||||
int httpd_count_connection_type (httpd_t *http, connection_type_t type);
|
||||
|
||||
httpd_t *httpd_init(logger_t *logger, httpd_callbacks_t *callbacks, int max_connections);
|
||||
httpd_t *httpd_init(logger_t *logger, httpd_callbacks_t *callbacks, int nohold);
|
||||
|
||||
int httpd_is_running(httpd_t *httpd);
|
||||
|
||||
|
||||
@@ -53,17 +53,17 @@ netutils_cleanup()
|
||||
}
|
||||
|
||||
unsigned char *
|
||||
netutils_get_address(void *sockaddr, int *length)
|
||||
netutils_get_address(void *sockaddr, int *length, unsigned int *zone_id)
|
||||
{
|
||||
unsigned char ipv4_prefix[] = { 0,0,0,0,0,0,0,0,0,0,255,255 };
|
||||
struct sockaddr *address = sockaddr;
|
||||
|
||||
assert(address);
|
||||
assert(length);
|
||||
|
||||
assert(zone_id);
|
||||
if (address->sa_family == AF_INET) {
|
||||
struct sockaddr_in *sin;
|
||||
|
||||
*zone_id = 0;
|
||||
sin = (struct sockaddr_in *)address;
|
||||
*length = sizeof(sin->sin_addr.s_addr);
|
||||
return (unsigned char *)&sin->sin_addr.s_addr;
|
||||
@@ -73,9 +73,11 @@ netutils_get_address(void *sockaddr, int *length)
|
||||
sin6 = (struct sockaddr_in6 *)address;
|
||||
if (!memcmp(sin6->sin6_addr.s6_addr, ipv4_prefix, 12)) {
|
||||
/* Actually an embedded IPv4 address */
|
||||
*zone_id = 0;
|
||||
*length = sizeof(sin6->sin6_addr.s6_addr)-12;
|
||||
return (sin6->sin6_addr.s6_addr+12);
|
||||
}
|
||||
*zone_id = (unsigned int) sin6->sin6_scope_id;
|
||||
*length = sizeof(sin6->sin6_addr.s6_addr);
|
||||
return sin6->sin6_addr.s6_addr;
|
||||
}
|
||||
|
||||
@@ -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 char *netutils_get_address(void *sockaddr, int *length, unsigned int *zone_id);
|
||||
int netutils_parse_address(int family, const char *src, void *dst, int dstlen);
|
||||
|
||||
#endif
|
||||
|
||||
191
lib/raop.c
191
lib/raop.c
@@ -88,6 +88,10 @@ struct raop_conn_s {
|
||||
unsigned char *remote;
|
||||
int remotelen;
|
||||
|
||||
unsigned int zone_id;
|
||||
|
||||
connection_type_t connection_type;
|
||||
|
||||
bool have_active_remote;
|
||||
};
|
||||
typedef struct raop_conn_s raop_conn_t;
|
||||
@@ -95,10 +99,10 @@ typedef struct raop_conn_s raop_conn_t;
|
||||
#include "raop_handlers.h"
|
||||
|
||||
static void *
|
||||
conn_init(void *opaque, unsigned char *local, int locallen, unsigned char *remote, int remotelen) {
|
||||
conn_init(void *opaque, unsigned char *local, int locallen, unsigned char *remote, int remotelen, unsigned int zone_id) {
|
||||
raop_t *raop = opaque;
|
||||
raop_conn_t *conn;
|
||||
|
||||
char ip_address[40];
|
||||
assert(raop);
|
||||
|
||||
conn = calloc(1, sizeof(raop_conn_t));
|
||||
@@ -122,26 +126,12 @@ conn_init(void *opaque, unsigned char *local, int locallen, unsigned char *remot
|
||||
return NULL;
|
||||
}
|
||||
|
||||
if (locallen == 4) {
|
||||
logger_log(conn->raop->logger, LOGGER_INFO,
|
||||
"Local: %d.%d.%d.%d",
|
||||
local[0], local[1], local[2], local[3]);
|
||||
} else if (locallen == 16) {
|
||||
logger_log(conn->raop->logger, LOGGER_INFO,
|
||||
"Local: %02x%02x:%02x%02x:%02x%02x:%02x%02x:%02x%02x:%02x%02x:%02x%02x:%02x%02x",
|
||||
local[0], local[1], local[2], local[3], local[4], local[5], local[6], local[7],
|
||||
local[8], local[9], local[10], local[11], local[12], local[13], local[14], local[15]);
|
||||
}
|
||||
if (remotelen == 4) {
|
||||
logger_log(conn->raop->logger, LOGGER_INFO,
|
||||
"Remote: %d.%d.%d.%d",
|
||||
remote[0], remote[1], remote[2], remote[3]);
|
||||
} else if (remotelen == 16) {
|
||||
logger_log(conn->raop->logger, LOGGER_INFO,
|
||||
"Remote: %02x%02x:%02x%02x:%02x%02x:%02x%02x:%02x%02x:%02x%02x:%02x%02x:%02x%02x",
|
||||
remote[0], remote[1], remote[2], remote[3], remote[4], remote[5], remote[6], remote[7],
|
||||
remote[8], remote[9], remote[10], remote[11], remote[12], remote[13], remote[14], remote[15]);
|
||||
}
|
||||
|
||||
utils_ipaddress_to_string(locallen, local, zone_id, ip_address, (int) sizeof(ip_address));
|
||||
logger_log(conn->raop->logger, LOGGER_INFO, "Local : %s", ip_address);
|
||||
|
||||
utils_ipaddress_to_string(remotelen, remote, zone_id, ip_address, (int) sizeof(ip_address));
|
||||
logger_log(conn->raop->logger, LOGGER_INFO, "Remote: %s", ip_address);
|
||||
|
||||
conn->local = malloc(locallen);
|
||||
assert(conn->local);
|
||||
@@ -151,8 +141,13 @@ conn_init(void *opaque, unsigned char *local, int locallen, unsigned char *remot
|
||||
assert(conn->remote);
|
||||
memcpy(conn->remote, remote, remotelen);
|
||||
|
||||
conn->zone_id = zone_id;
|
||||
|
||||
conn->locallen = locallen;
|
||||
conn->remotelen = remotelen;
|
||||
|
||||
conn->connection_type = CONNECTION_TYPE_UNKNOWN;
|
||||
|
||||
conn->have_active_remote = false;
|
||||
|
||||
if (raop->callbacks.conn_init) {
|
||||
@@ -164,18 +159,40 @@ conn_init(void *opaque, unsigned char *local, int locallen, unsigned char *remot
|
||||
|
||||
static void
|
||||
conn_request(void *ptr, http_request_t *request, http_response_t **response) {
|
||||
raop_conn_t *conn = ptr;
|
||||
const char *method;
|
||||
const char *url;
|
||||
const char *cseq;
|
||||
char *response_data = NULL;
|
||||
int response_datalen = 0;
|
||||
logger_log(conn->raop->logger, LOGGER_DEBUG, "conn_request");
|
||||
raop_conn_t *conn = ptr;
|
||||
|
||||
bool logger_debug = (logger_get_level(conn->raop->logger) >= LOGGER_DEBUG);
|
||||
|
||||
method = http_request_get_method(request);
|
||||
url = http_request_get_url(request);
|
||||
cseq = http_request_get_header(request, "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);
|
||||
const char *cseq = http_request_get_header(request, "CSeq");
|
||||
|
||||
if (conn->connection_type == CONNECTION_TYPE_UNKNOWN) {
|
||||
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)));
|
||||
if (httpd_nohold(conn->raop->httpd)) {
|
||||
logger_log(conn->raop->logger, LOGGER_INFO, "\"nohold\" feature: switch to new connection request from %s", ipaddr);
|
||||
if (conn->raop->callbacks.video_reset) {
|
||||
printf("**************************video_reset*************************\n");
|
||||
conn->raop->callbacks.video_reset(conn->raop->callbacks.cls);
|
||||
}
|
||||
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();
|
||||
http_response_init(*response, protocol, 409, "Conflict: Server is connected to another client");
|
||||
goto finish;
|
||||
}
|
||||
}
|
||||
httpd_set_connection_type(conn->raop->httpd, ptr, CONNECTION_TYPE_RAOP);
|
||||
conn->connection_type = CONNECTION_TYPE_RAOP;
|
||||
}
|
||||
|
||||
if (!conn->have_active_remote) {
|
||||
const char *active_remote = http_request_get_header(request, "Active-Remote");
|
||||
if (active_remote) {
|
||||
@@ -187,16 +204,22 @@ conn_request(void *ptr, http_request_t *request, http_response_t **response) {
|
||||
}
|
||||
}
|
||||
|
||||
if (!method || !cseq) {
|
||||
if (!method) {
|
||||
return;
|
||||
}
|
||||
logger_log(conn->raop->logger, LOGGER_DEBUG, "\n%s %s RTSP/1.0", method, url);
|
||||
|
||||
/* this rejects unsupported messages from _airplay._tcp for video streaming protocol*/
|
||||
if (!cseq) {
|
||||
return;
|
||||
}
|
||||
|
||||
logger_log(conn->raop->logger, LOGGER_DEBUG, "\n%s %s %s", method, url, protocol);
|
||||
char *header_str= NULL;
|
||||
http_request_get_header_string(request, &header_str);
|
||||
if (header_str) {
|
||||
logger_log(conn->raop->logger, LOGGER_DEBUG, "%s", header_str);
|
||||
bool data_is_plist = (strstr(header_str,"apple-binary-plist") != NULL);
|
||||
bool data_is_text = (strstr(header_str,"text/parameters") != NULL);
|
||||
bool data_is_text = (strstr(header_str,"text/") != NULL);
|
||||
free(header_str);
|
||||
int request_datalen;
|
||||
const char *request_data = http_request_get_data(request, &request_datalen);
|
||||
@@ -224,11 +247,11 @@ conn_request(void *ptr, http_request_t *request, http_response_t **response) {
|
||||
}
|
||||
}
|
||||
|
||||
*response = http_response_init("RTSP/1.0", 200, "OK");
|
||||
*response = http_response_create();
|
||||
http_response_init(*response, protocol, 200, "OK");
|
||||
|
||||
http_response_add_header(*response, "CSeq", cseq);
|
||||
//http_response_add_header(*response, "Apple-Jack-Status", "connected; type=analog");
|
||||
http_response_add_header(*response, "Server", "AirTunes/"GLOBAL_VERSION);
|
||||
|
||||
|
||||
logger_log(conn->raop->logger, LOGGER_DEBUG, "Handling request %s with URL %s", method, url);
|
||||
raop_handler_t handler = NULL;
|
||||
@@ -267,6 +290,9 @@ conn_request(void *ptr, http_request_t *request, http_response_t **response) {
|
||||
if (handler != NULL) {
|
||||
handler(conn, request, *response, &response_data, &response_datalen);
|
||||
}
|
||||
finish:;
|
||||
http_response_add_header(*response, "Server", "AirTunes/"GLOBAL_VERSION);
|
||||
http_response_add_header(*response, "CSeq", cseq);
|
||||
http_response_finish(*response, response_data, response_datalen);
|
||||
|
||||
int len;
|
||||
@@ -342,15 +368,10 @@ conn_destroy(void *ptr) {
|
||||
}
|
||||
|
||||
raop_t *
|
||||
raop_init(int max_clients, raop_callbacks_t *callbacks, const char *device_id, const char *keyfile) {
|
||||
raop_init(raop_callbacks_t *callbacks) {
|
||||
raop_t *raop;
|
||||
pairing_t *pairing;
|
||||
httpd_t *httpd;
|
||||
httpd_callbacks_t httpd_cbs;
|
||||
|
||||
assert(callbacks);
|
||||
assert(max_clients > 0);
|
||||
assert(max_clients < 100);
|
||||
|
||||
/* Initialize the network */
|
||||
if (netutils_init() < 0) {
|
||||
@@ -372,46 +393,8 @@ raop_init(int max_clients, raop_callbacks_t *callbacks, const char *device_id, c
|
||||
/* Initialize the logger */
|
||||
raop->logger = logger_init();
|
||||
|
||||
/* create a new public key for pairing */
|
||||
int new_key;
|
||||
pairing = pairing_init_generate(device_id, keyfile, &new_key);
|
||||
if (!pairing) {
|
||||
free(raop);
|
||||
return NULL;
|
||||
}
|
||||
/* store PK as a string in raop->pk_str */
|
||||
memset(raop->pk_str, 0, sizeof(raop->pk_str));
|
||||
#ifdef PK
|
||||
strncpy(raop->pk_str, PK, 2*ED25519_KEY_SIZE);
|
||||
#else
|
||||
unsigned char public_key[ED25519_KEY_SIZE];
|
||||
pairing_get_public_key(pairing, public_key);
|
||||
char *pk_str = utils_pk_to_string(public_key, ED25519_KEY_SIZE);
|
||||
strncpy(raop->pk_str, (const char *) pk_str, 2*ED25519_KEY_SIZE);
|
||||
free(pk_str);
|
||||
#endif
|
||||
if (new_key) {
|
||||
printf("*** A new Public Key has been created and stored in %s\n", keyfile);
|
||||
}
|
||||
|
||||
/* Set HTTP callbacks to our handlers */
|
||||
memset(&httpd_cbs, 0, sizeof(httpd_cbs));
|
||||
httpd_cbs.opaque = raop;
|
||||
httpd_cbs.conn_init = &conn_init;
|
||||
httpd_cbs.conn_request = &conn_request;
|
||||
httpd_cbs.conn_destroy = &conn_destroy;
|
||||
|
||||
/* Initialize the http daemon */
|
||||
httpd = httpd_init(raop->logger, &httpd_cbs, max_clients);
|
||||
if (!httpd) {
|
||||
pairing_destroy(pairing);
|
||||
free(raop);
|
||||
return NULL;
|
||||
}
|
||||
/* Copy callbacks structure */
|
||||
memcpy(&raop->callbacks, callbacks, sizeof(raop_callbacks_t));
|
||||
raop->pairing = pairing;
|
||||
raop->httpd = httpd;
|
||||
|
||||
/* initialize network port list */
|
||||
raop->port = 0;
|
||||
@@ -440,6 +423,54 @@ raop_init(int max_clients, raop_callbacks_t *callbacks, const char *device_id, c
|
||||
return raop;
|
||||
}
|
||||
|
||||
int
|
||||
raop_init2(raop_t *raop, int nohold, const char *device_id, const char *keyfile) {
|
||||
pairing_t *pairing;
|
||||
httpd_t *httpd;
|
||||
httpd_callbacks_t httpd_cbs;
|
||||
|
||||
/* create a new public key for pairing */
|
||||
int new_key;
|
||||
pairing = pairing_init_generate(device_id, keyfile, &new_key);
|
||||
if (!pairing) {
|
||||
logger_log(raop->logger, LOGGER_ERR, "failed to create new public key for pairing");
|
||||
return -1;
|
||||
}
|
||||
/* store PK as a string in raop->pk_str */
|
||||
memset(raop->pk_str, 0, sizeof(raop->pk_str));
|
||||
#ifdef PK
|
||||
strncpy(raop->pk_str, PK, 2*ED25519_KEY_SIZE);
|
||||
#else
|
||||
unsigned char public_key[ED25519_KEY_SIZE];
|
||||
pairing_get_public_key(pairing, public_key);
|
||||
char *pk_str = utils_pk_to_string(public_key, ED25519_KEY_SIZE);
|
||||
strncpy(raop->pk_str, (const char *) pk_str, 2*ED25519_KEY_SIZE);
|
||||
free(pk_str);
|
||||
#endif
|
||||
if (new_key) {
|
||||
logger_log(raop->logger, LOGGER_INFO,"*** A new Public Key has been created and stored in %s", keyfile);
|
||||
}
|
||||
|
||||
/* Set HTTP callbacks to our handlers */
|
||||
memset(&httpd_cbs, 0, sizeof(httpd_cbs));
|
||||
httpd_cbs.opaque = raop;
|
||||
httpd_cbs.conn_init = &conn_init;
|
||||
httpd_cbs.conn_request = &conn_request;
|
||||
httpd_cbs.conn_destroy = &conn_destroy;
|
||||
|
||||
/* Initialize the http daemon */
|
||||
httpd = httpd_init(raop->logger, &httpd_cbs, nohold);
|
||||
if (!httpd) {
|
||||
logger_log(raop->logger, LOGGER_ERR, "failed to initialize http daemon");
|
||||
pairing_destroy(pairing);
|
||||
return -1;
|
||||
}
|
||||
|
||||
raop->pairing = pairing;
|
||||
raop->httpd = httpd;
|
||||
return 0;
|
||||
}
|
||||
|
||||
void
|
||||
raop_destroy(raop_t *raop) {
|
||||
if (raop) {
|
||||
|
||||
@@ -63,12 +63,14 @@ struct raop_callbacks_s {
|
||||
void (*register_client) (void *cls, const char *device_id, const char *pk_str, const char *name);
|
||||
bool (*check_register) (void *cls, const char *pk_str);
|
||||
void (*export_dacp) (void *cls, const char *active_remote, const char *dacp_id);
|
||||
void (*video_reset) (void *cls);
|
||||
};
|
||||
typedef struct raop_callbacks_s raop_callbacks_t;
|
||||
raop_ntp_t *raop_ntp_init(logger_t *logger, raop_callbacks_t *callbacks, const char *remote, int remote_addr_len,
|
||||
unsigned short timing_rport, timing_protocol_t *time_protocol);
|
||||
raop_ntp_t *raop_ntp_init(logger_t *logger, raop_callbacks_t *callbacks, const char *remote,
|
||||
int remote_addr_len, unsigned short timing_rport, timing_protocol_t *time_protocol);
|
||||
|
||||
RAOP_API raop_t *raop_init(int max_clients, raop_callbacks_t *callbacks, const char *device_id, const char *keyfile);
|
||||
RAOP_API raop_t *raop_init(raop_callbacks_t *callbacks);
|
||||
RAOP_API int raop_init2(raop_t *raop, int nohold, const char *device_id, const char *keyfile);
|
||||
RAOP_API void raop_set_log_level(raop_t *raop, int level);
|
||||
RAOP_API void raop_set_log_callback(raop_t *raop, raop_log_callback_t callback, void *cls);
|
||||
RAOP_API int raop_set_plist(raop_t *raop, const char *plist_item, const int value);
|
||||
|
||||
@@ -36,33 +36,37 @@ raop_handler_info(raop_conn_t *conn,
|
||||
{
|
||||
assert(conn->raop->dnssd);
|
||||
|
||||
int airplay_txt_len = 0;
|
||||
const char *airplay_txt = dnssd_get_airplay_txt(conn->raop->dnssd, &airplay_txt_len);
|
||||
|
||||
int name_len = 0;
|
||||
const char *name = dnssd_get_name(conn->raop->dnssd, &name_len);
|
||||
plist_t res_node = plist_new_dict();
|
||||
|
||||
/* 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);
|
||||
|
||||
char *hw_addr = calloc(1, 3 * hw_addr_raw_len);
|
||||
//int hw_addr_len =
|
||||
utils_hwaddr_airplay(hw_addr, 3 * hw_addr_raw_len, hw_addr_raw, hw_addr_raw_len);
|
||||
plist_t device_id_node = plist_new_string(hw_addr);
|
||||
plist_dict_set_item(res_node, "deviceID", device_id_node);
|
||||
|
||||
/* 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);
|
||||
|
||||
plist_t r_node = plist_new_dict();
|
||||
|
||||
/* 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(r_node, "txtAirPlay", txt_airplay_node);
|
||||
plist_dict_set_item(res_node, "txtAirPlay", txt_airplay_node);
|
||||
|
||||
uint64_t features = dnssd_get_airplay_features(conn->raop->dnssd);
|
||||
plist_t features_node = plist_new_uint(features);
|
||||
plist_dict_set_item(r_node, "features", features_node);
|
||||
plist_dict_set_item(res_node, "features", features_node);
|
||||
|
||||
int name_len = 0;
|
||||
const char *name = dnssd_get_name(conn->raop->dnssd, &name_len);
|
||||
plist_t name_node = plist_new_string(name);
|
||||
plist_dict_set_item(r_node, "name", name_node);
|
||||
plist_dict_set_item(res_node, "name", name_node);
|
||||
|
||||
plist_t audio_formats_node = plist_new_array();
|
||||
plist_t audio_format_0_node = plist_new_dict();
|
||||
@@ -81,31 +85,25 @@ raop_handler_info(raop_conn_t *conn,
|
||||
plist_dict_set_item(audio_format_1_node, "audioInputFormats", audio_format_1_audio_input_formats_node);
|
||||
plist_dict_set_item(audio_format_1_node, "audioOutputFormats", audio_format_1_audio_output_formats_node);
|
||||
plist_array_append_item(audio_formats_node, audio_format_1_node);
|
||||
plist_dict_set_item(r_node, "audioFormats", audio_formats_node);
|
||||
plist_dict_set_item(res_node, "audioFormats", audio_formats_node);
|
||||
|
||||
plist_t pi_node = plist_new_string(AIRPLAY_PI);
|
||||
plist_dict_set_item(r_node, "pi", pi_node);
|
||||
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(r_node, "vv", vv_node);
|
||||
plist_dict_set_item(res_node, "vv", vv_node);
|
||||
|
||||
plist_t status_flags_node = plist_new_uint(68);
|
||||
plist_dict_set_item(r_node, "statusFlags", status_flags_node);
|
||||
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(r_node, "keepAliveLowPower", keep_alive_low_power_node);
|
||||
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(r_node, "sourceVersion", source_version_node);
|
||||
|
||||
plist_t pk_node = plist_new_data(pk, pk_len);
|
||||
plist_dict_set_item(r_node, "pk", pk_node);
|
||||
plist_dict_set_item(res_node, "sourceVersion", source_version_node);
|
||||
|
||||
plist_t keep_alive_send_stats_as_body_node = plist_new_uint(1);
|
||||
plist_dict_set_item(r_node, "keepAliveSendStatsAsBody", keep_alive_send_stats_as_body_node);
|
||||
|
||||
plist_t device_id_node = plist_new_string(hw_addr);
|
||||
plist_dict_set_item(r_node, "deviceID", device_id_node);
|
||||
plist_dict_set_item(res_node, "keepAliveSendStatsAsBody", keep_alive_send_stats_as_body_node);
|
||||
|
||||
plist_t audio_latencies_node = plist_new_array();
|
||||
plist_t audio_latencies_0_node = plist_new_dict();
|
||||
@@ -128,13 +126,13 @@ raop_handler_info(raop_conn_t *conn,
|
||||
plist_dict_set_item(audio_latencies_1_node, "audioType", audio_latencies_1_audio_type_node);
|
||||
plist_dict_set_item(audio_latencies_1_node, "inputLatencyMicros", audio_latencies_1_input_latency_micros_node);
|
||||
plist_array_append_item(audio_latencies_node, audio_latencies_1_node);
|
||||
plist_dict_set_item(r_node, "audioLatencies", audio_latencies_node);
|
||||
plist_dict_set_item(res_node, "audioLatencies", audio_latencies_node);
|
||||
|
||||
plist_t model_node = plist_new_string(GLOBAL_MODEL);
|
||||
plist_dict_set_item(r_node, "model", model_node);
|
||||
plist_dict_set_item(res_node, "model", model_node);
|
||||
|
||||
plist_t mac_address_node = plist_new_string(hw_addr);
|
||||
plist_dict_set_item(r_node, "macAddress", mac_address_node);
|
||||
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();
|
||||
@@ -164,10 +162,10 @@ raop_handler_info(raop_conn_t *conn,
|
||||
plist_dict_set_item(displays_0_node, "overscanned", displays_0_overscanned_node);
|
||||
plist_dict_set_item(displays_0_node, "features", displays_0_features);
|
||||
plist_array_append_item(displays_node, displays_0_node);
|
||||
plist_dict_set_item(r_node, "displays", displays_node);
|
||||
plist_dict_set_item(res_node, "displays", displays_node);
|
||||
|
||||
plist_to_bin(r_node, response_data, (uint32_t *) response_datalen);
|
||||
plist_free(r_node);
|
||||
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);
|
||||
@@ -205,8 +203,8 @@ raop_handler_pairsetup_pin(raop_conn_t *conn,
|
||||
http_request_t *request, http_response_t *response,
|
||||
char **response_data, int *response_datalen) {
|
||||
|
||||
const char *request_data;
|
||||
int request_datalen;
|
||||
const char *request_data = NULL;;
|
||||
int request_datalen = 0;
|
||||
bool data_is_plist = false;
|
||||
bool logger_debug = (logger_get_level(conn->raop->logger) >= LOGGER_DEBUG);
|
||||
request_data = http_request_get_data(request, &request_datalen);
|
||||
@@ -219,7 +217,8 @@ raop_handler_pairsetup_pin(raop_conn_t *conn,
|
||||
free(header_str);
|
||||
}
|
||||
if (!data_is_plist) {
|
||||
logger_log(conn->raop->logger, LOGGER_INFO, "did not receive expected plist from client, request_datalen = %d");
|
||||
logger_log(conn->raop->logger, LOGGER_INFO, "did not receive expected plist from client, request_datalen = %d",
|
||||
request_datalen);
|
||||
goto authentication_failed;
|
||||
}
|
||||
|
||||
@@ -355,14 +354,7 @@ raop_handler_pairsetup_pin(raop_conn_t *conn,
|
||||
return;
|
||||
}
|
||||
authentication_failed:;
|
||||
http_response_destroy(response);
|
||||
response = http_response_init("RTSP/1.0", 470, "Client Authentication Failure");
|
||||
const char *cseq = http_request_get_header(request, "CSeq");
|
||||
http_response_add_header(response, "CSeq", cseq);
|
||||
http_response_add_header(response, "Server", "AirTunes/"GLOBAL_VERSION);
|
||||
*response_data = NULL;
|
||||
response_datalen = 0;
|
||||
return;
|
||||
http_response_init(response, "RTSP/1.0", 470, "Client Authentication Failure");
|
||||
}
|
||||
|
||||
static void
|
||||
@@ -737,36 +729,31 @@ raop_handler_setup(raop_conn_t *conn,
|
||||
conn->raop_ntp = NULL;
|
||||
conn->raop_rtp = NULL;
|
||||
conn->raop_rtp_mirror = NULL;
|
||||
if (conn->remotelen == 4 || conn->remotelen == 16) {
|
||||
char remote[40];
|
||||
if (conn->remotelen == 4) {
|
||||
/* IPV4 */
|
||||
snprintf(remote, sizeof(remote), "%d.%d.%d.%d", conn->remote[0], conn->remote[1],
|
||||
conn->remote[2], conn->remote[3]);
|
||||
} else {
|
||||
/*IPV6*/
|
||||
logger_log(conn->raop->logger, LOGGER_INFO, "client is using IPV6 (untested!)");
|
||||
snprintf(remote, sizeof(remote), "%02x%02x:%02x%02x:%02x%02x:%02x%02x:%02x%02x:%02x%02x:%02x%02x:%02x%02x",
|
||||
conn->remote[0], conn->remote[1], conn->remote[2], conn->remote[3],
|
||||
conn->remote[4], conn->remote[5], conn->remote[6], conn->remote[7],
|
||||
conn->remote[8], conn->remote[9], conn->remote[10], conn->remote[11],
|
||||
conn->remote[12], conn->remote[13], conn->remote[14], conn->remote[15]);
|
||||
}
|
||||
conn->raop_ntp = raop_ntp_init(conn->raop->logger, &conn->raop->callbacks, remote,
|
||||
conn->remotelen, (unsigned short) timing_rport, &time_protocol);
|
||||
raop_ntp_start(conn->raop_ntp, &timing_lport, conn->raop->max_ntp_timeouts);
|
||||
conn->raop_rtp = raop_rtp_init(conn->raop->logger, &conn->raop->callbacks, conn->raop_ntp,
|
||||
remote, conn->remotelen, aeskey, aesiv);
|
||||
conn->raop_rtp_mirror = raop_rtp_mirror_init(conn->raop->logger, &conn->raop->callbacks,
|
||||
conn->raop_ntp, remote, conn->remotelen, aeskey);
|
||||
char remote[40];
|
||||
int len = utils_ipaddress_to_string(conn->remotelen, conn->remote, conn->zone_id, remote, (int) sizeof(remote));
|
||||
if (!len || len > sizeof(remote)) {
|
||||
char *str = utils_data_to_string(conn->remote, conn->remotelen, 16);
|
||||
logger_log(conn->raop->logger, LOGGER_ERR, "failed to extract valid client ip address:\n"
|
||||
"*** UxPlay will be unable to send communications to client.\n"
|
||||
"*** address length %d, zone_id %u address data:\n%sparser returned \"%s\"\n",
|
||||
conn->remotelen, conn->zone_id, str, remote);
|
||||
free(str);
|
||||
}
|
||||
conn->raop_ntp = raop_ntp_init(conn->raop->logger, &conn->raop->callbacks, remote,
|
||||
conn->remotelen, (unsigned short) timing_rport, &time_protocol);
|
||||
raop_ntp_start(conn->raop_ntp, &timing_lport, conn->raop->max_ntp_timeouts);
|
||||
conn->raop_rtp = raop_rtp_init(conn->raop->logger, &conn->raop->callbacks, conn->raop_ntp,
|
||||
remote, conn->remotelen, aeskey, aesiv);
|
||||
conn->raop_rtp_mirror = raop_rtp_mirror_init(conn->raop->logger, &conn->raop->callbacks,
|
||||
conn->raop_ntp, remote, conn->remotelen, aeskey);
|
||||
|
||||
plist_t res_event_port_node = plist_new_uint(conn->raop->port);
|
||||
// plist_t res_event_port_node = plist_new_uint(conn->raop->port);
|
||||
plist_t res_event_port_node = plist_new_uint(0);
|
||||
plist_t res_timing_port_node = plist_new_uint(timing_lport);
|
||||
plist_dict_set_item(res_root_node, "timingPort", res_timing_port_node);
|
||||
plist_dict_set_item(res_root_node, "eventPort", res_event_port_node);
|
||||
|
||||
logger_log(conn->raop->logger, LOGGER_DEBUG, "eport = %d, tport = %d", conn->raop->port, timing_lport);
|
||||
logger_log(conn->raop->logger, LOGGER_DEBUG, "eport = %d, tport = %d", 0, timing_lport);
|
||||
}
|
||||
|
||||
// Process stream setup requests
|
||||
@@ -793,8 +780,8 @@ raop_handler_setup(raop_conn_t *conn,
|
||||
" key and iv): %llu", stream_connection_id);
|
||||
|
||||
if (conn->raop_rtp_mirror) {
|
||||
raop_rtp_init_mirror_aes(conn->raop_rtp_mirror, &stream_connection_id);
|
||||
raop_rtp_start_mirror(conn->raop_rtp_mirror, &dport, conn->raop->clientFPSdata);
|
||||
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!");
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <stdbool.h>
|
||||
#include <errno.h>
|
||||
#ifdef _WIN32
|
||||
#define CAST (char *)
|
||||
#else
|
||||
@@ -296,7 +297,9 @@ raop_ntp_thread(void *arg)
|
||||
free(str);
|
||||
}
|
||||
if (send_len < 0) {
|
||||
logger_log(raop_ntp->logger, LOGGER_ERR, "raop_ntp error sending request");
|
||||
int sock_err = SOCKET_GET_ERROR();
|
||||
logger_log(raop_ntp->logger, LOGGER_ERR, "raop_ntp error sending request. Error %d:%s",
|
||||
sock_err, strerror(sock_err));
|
||||
} else {
|
||||
// Read response
|
||||
response_len = recvfrom(raop_ntp->tsock, (char *)response, sizeof(response), 0, NULL, NULL);
|
||||
|
||||
@@ -165,7 +165,7 @@ raop_rtp_mirror_t *raop_rtp_mirror_init(logger_t *logger, raop_callbacks_t *call
|
||||
}
|
||||
|
||||
void
|
||||
raop_rtp_init_mirror_aes(raop_rtp_mirror_t *raop_rtp_mirror, uint64_t *streamConnectionID)
|
||||
raop_rtp_mirror_init_aes(raop_rtp_mirror_t *raop_rtp_mirror, uint64_t *streamConnectionID)
|
||||
{
|
||||
mirror_buffer_init_aes(raop_rtp_mirror->buffer, streamConnectionID);
|
||||
}
|
||||
@@ -291,7 +291,7 @@ raop_rtp_mirror_thread(void *arg)
|
||||
|
||||
if (payload == NULL && ret == 0) {
|
||||
logger_log(raop_rtp_mirror->logger, LOGGER_DEBUG,
|
||||
"raop_rtp_mirror tcp socket is closed, got %d bytes of 128 byte header",readstart);
|
||||
"raop_rtp_mirror tcp socket was closed by client (recv returned 0); got %d bytes of 128 byte header",readstart);
|
||||
FD_CLR(stream_fd, &rfds);
|
||||
stream_fd = -1;
|
||||
continue;
|
||||
@@ -354,7 +354,7 @@ raop_rtp_mirror_thread(void *arg)
|
||||
}
|
||||
|
||||
if (ret == 0) {
|
||||
logger_log(raop_rtp_mirror->logger, LOGGER_ERR, "raop_rtp_mirror tcp socket is closed");
|
||||
logger_log(raop_rtp_mirror->logger, LOGGER_ERR, "raop_rtp_mirror tcp socket was closed by client (recv returned 0)");
|
||||
break;
|
||||
} else if (ret == -1) {
|
||||
if (errno == EAGAIN || errno == EWOULDBLOCK) continue; // Timeouts can happen even if the connection is fine
|
||||
@@ -609,6 +609,11 @@ raop_rtp_mirror_thread(void *arg)
|
||||
memcpy(sps_pps + sps_size + 8, payload + sps_size + 11, pps_size);
|
||||
prepend_sps_pps = true;
|
||||
|
||||
uint64_t ntp_offset = 0;
|
||||
ntp_offset = raop_ntp_convert_remote_time(raop_rtp_mirror->ntp, ntp_offset);
|
||||
if (!ntp_offset) {
|
||||
logger_log(raop_rtp_mirror->logger, LOGGER_WARNING, "ntp synchronization has not yet started: synchronized video may fail");
|
||||
}
|
||||
// h264codec_t h264;
|
||||
// h264.version = payload[0];
|
||||
// h264.profile_high = payload[1];
|
||||
@@ -694,7 +699,7 @@ raop_rtp_mirror_thread(void *arg)
|
||||
}
|
||||
|
||||
static int
|
||||
raop_rtp_init_mirror_sockets(raop_rtp_mirror_t *raop_rtp_mirror, int use_ipv6)
|
||||
raop_rtp_mirror_init_socket(raop_rtp_mirror_t *raop_rtp_mirror, int use_ipv6)
|
||||
{
|
||||
assert(raop_rtp_mirror);
|
||||
|
||||
@@ -724,7 +729,7 @@ raop_rtp_init_mirror_sockets(raop_rtp_mirror_t *raop_rtp_mirror, int use_ipv6)
|
||||
}
|
||||
|
||||
void
|
||||
raop_rtp_start_mirror(raop_rtp_mirror_t *raop_rtp_mirror, unsigned short *mirror_data_lport,
|
||||
raop_rtp_mirror_start(raop_rtp_mirror_t *raop_rtp_mirror, unsigned short *mirror_data_lport,
|
||||
uint8_t show_client_FPS_data)
|
||||
{
|
||||
logger_log(raop_rtp_mirror->logger, LOGGER_INFO, "raop_rtp_mirror starting mirroring");
|
||||
@@ -746,8 +751,8 @@ raop_rtp_start_mirror(raop_rtp_mirror_t *raop_rtp_mirror, unsigned short *mirror
|
||||
//use_ipv6 = 0;
|
||||
|
||||
raop_rtp_mirror->mirror_data_lport = *mirror_data_lport;
|
||||
if (raop_rtp_init_mirror_sockets(raop_rtp_mirror, use_ipv6) < 0) {
|
||||
logger_log(raop_rtp_mirror->logger, LOGGER_ERR, "raop_rtp_mirror initializing sockets failed");
|
||||
if (raop_rtp_mirror_init_socket(raop_rtp_mirror, use_ipv6) < 0) {
|
||||
logger_log(raop_rtp_mirror->logger, LOGGER_ERR, "raop_rtp_mirror initializing socket failed");
|
||||
MUTEX_UNLOCK(raop_rtp_mirror->run_mutex);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -27,8 +27,8 @@ typedef struct h264codec_s h264codec_t;
|
||||
|
||||
raop_rtp_mirror_t *raop_rtp_mirror_init(logger_t *logger, raop_callbacks_t *callbacks, raop_ntp_t *ntp,
|
||||
const char *remote, int remotelen, const unsigned char *aeskey);
|
||||
void raop_rtp_init_mirror_aes(raop_rtp_mirror_t *raop_rtp_mirror, uint64_t *streamConnectionID);
|
||||
void raop_rtp_start_mirror(raop_rtp_mirror_t *raop_rtp_mirror, unsigned short *mirror_data_lport, uint8_t show_client_FPS_data);
|
||||
void raop_rtp_mirror_init_aes(raop_rtp_mirror_t *raop_rtp_mirror, uint64_t *streamConnectionID);
|
||||
void raop_rtp_mirror_start(raop_rtp_mirror_t *raop_rtp_mirror, unsigned short *mirror_data_lport, uint8_t show_client_FPS_data);
|
||||
void raop_rtp_mirror_stop(raop_rtp_mirror_t *raop_rtp_mirror);
|
||||
void raop_rtp_mirror_destroy(raop_rtp_mirror_t *raop_rtp_mirror);
|
||||
#endif //RAOP_RTP_MIRROR_H
|
||||
|
||||
@@ -594,9 +594,11 @@ static void init_random()
|
||||
fp = fopen("/dev/urandom", "r");
|
||||
if (fp)
|
||||
{
|
||||
fread(buff, sizeof(buff), 1, fp);
|
||||
size_t count = fread(buff, sizeof(buff), 1, fp);
|
||||
fclose(fp);
|
||||
g_initialized = 1;
|
||||
if (count == 1) {
|
||||
g_initialized = 1;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
if (g_initialized)
|
||||
|
||||
26
lib/utils.c
26
lib/utils.c
@@ -256,3 +256,29 @@ void ntp_timestamp_to_seconds(uint64_t ntp_timestamp, char *timestamp, size_t ma
|
||||
strftime(timestamp, 3, "%S", &ts);
|
||||
snprintf(timestamp + 2, 11,".%9.9lu", (unsigned long) ntp_timestamp % SECOND_IN_NSECS);
|
||||
}
|
||||
|
||||
int utils_ipaddress_to_string(int addresslen, const unsigned char *address, unsigned int zone_id, char *string, int sizeof_string) {
|
||||
int ret = 0;
|
||||
unsigned char ipv6_link_local_prefix[] = { 0xfe, 0x80, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0 };
|
||||
assert(sizeof_string > 0);
|
||||
assert(string);
|
||||
if (addresslen != 4 && addresslen != 16) { //invalid address length (only ipv4 and ipv6 allowed)
|
||||
string[0] = '\0';
|
||||
}
|
||||
if (addresslen == 4) { /* IPV4 */
|
||||
ret = snprintf(string, sizeof_string, "%d.%d.%d.%d", address[0], address[1], address[2], address[3]);
|
||||
} else if (zone_id) { /* IPV6 link-local */
|
||||
if (memcmp(address, ipv6_link_local_prefix, 8)) {
|
||||
string[0] = '\0'; //only link-local ipv6 addresses can have a zone_id
|
||||
} else {
|
||||
ret = snprintf(string, sizeof_string, "fe80::%02x%02x:%02x%02x:%02x%02x:%02x%02x%%%u",
|
||||
address[8], address[9], address[10], address[11],
|
||||
address[12], address[13], address[14], address[15], zone_id);
|
||||
}
|
||||
} else { /* IPV6 standard*/
|
||||
ret = snprintf(string, sizeof_string, "%02x%02x:%02x%02x:%02x%02x:%02x%02x:%02x%02x:%02x%02x:%02x%02x:%02x%02x",
|
||||
address[0], address[1], address[2], address[3], address[4], address[5], address[6], address[7],
|
||||
address[8], address[9], address[10], address[11], address[12], address[13], address[14], address[15]);
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
@@ -30,5 +30,6 @@ char *utils_data_to_string(const unsigned char *data, int datalen, int chars_per
|
||||
char *utils_data_to_text(const char *data, int datalen);
|
||||
void ntp_timestamp_to_time(uint64_t ntp_timestamp, char *timestamp, size_t maxsize);
|
||||
void ntp_timestamp_to_seconds(uint64_t ntp_timestamp, char *timestamp, size_t maxsize);
|
||||
|
||||
int utils_ipaddress_to_string(int addresslen, const unsigned char *address,
|
||||
unsigned int zone_id, char *string, int len);
|
||||
#endif
|
||||
|
||||
@@ -9,6 +9,12 @@ if (APPLE )
|
||||
find_program( PKG_CONFIG_EXECUTABLE pkg-config PATHS /Library/FrameWorks/GStreamer.framework/Commands )
|
||||
set(PKG_CONFIG_EXECUTABLE ${PKG_CONFIG_EXECUTABLE} --define-prefix )
|
||||
else()
|
||||
if ( DEFINED ENV{GSTREAMER_ROOT_DIR} )
|
||||
if ( EXISTS "$ENV{GSTREAMER_ROOT_DIR}/pkgconfig" )
|
||||
message ( STATUS "*** Using GSTREAMER_ROOT_DIR = " $ENV{GSTREAMER_ROOT_DIR} )
|
||||
set( ENV{PKG_CONFIG_PATH} "$ENV{GSTREAMER_ROOT_DIR}/pkgconfig:$ENV{PKG_CONFIG_PATH}" )
|
||||
endif()
|
||||
endif()
|
||||
set( ENV{PKG_CONFIG_PATH} "$ENV{PKG_CONFIG_PATH}:/usr/local/lib/pkgconfig" ) # standard location for self-installed gstreamer
|
||||
endif()
|
||||
|
||||
@@ -32,6 +38,13 @@ pkg_check_modules(GST REQUIRED gstreamer-1.0>=1.4
|
||||
gstreamer-app-1.0>=1.4
|
||||
)
|
||||
|
||||
# temporary hack to deal with an issue in gstreamer 1.24
|
||||
pkg_check_modules ( GST124 gstreamer-1.0>=1.24 )
|
||||
if ( GST124_FOUND )
|
||||
message( STATUS "*** GStreamer >= 1.24: GST_124 will be defined" )
|
||||
set( GST_124 "1" CACHE STRING "define GST_124" )
|
||||
endif()
|
||||
|
||||
add_library( renderers
|
||||
STATIC
|
||||
audio_renderer_gstreamer.c
|
||||
|
||||
@@ -30,7 +30,6 @@
|
||||
#include "x_display_fix.h"
|
||||
static bool fullscreen = false;
|
||||
static bool alt_keypress = false;
|
||||
#define MAX_X11_SEARCH_ATTEMPTS 5 /*should be less than 256 */
|
||||
static unsigned char X11_search_attempts;
|
||||
#endif
|
||||
|
||||
@@ -40,12 +39,17 @@ static logger_t *logger = NULL;
|
||||
static unsigned short width, height, width_source, height_source; /* not currently used */
|
||||
static bool first_packet = false;
|
||||
static bool sync = false;
|
||||
static bool auto_videosink;
|
||||
#ifdef X_DISPLAY_FIX
|
||||
static bool use_x11 = false;
|
||||
#endif
|
||||
|
||||
|
||||
struct video_renderer_s {
|
||||
GstElement *appsrc, *pipeline, *sink;
|
||||
GstElement *appsrc, *pipeline;
|
||||
GstBus *bus;
|
||||
#ifdef X_DISPLAY_FIX
|
||||
const char * server_name;
|
||||
const char * server_name;
|
||||
X11_Window_t * gst_window;
|
||||
#endif
|
||||
};
|
||||
@@ -56,56 +60,56 @@ static void append_videoflip (GString *launch, const videoflip_t *flip, const vi
|
||||
case INVERT:
|
||||
switch (*rot) {
|
||||
case LEFT:
|
||||
g_string_append(launch, "videoflip method=clockwise ! ");
|
||||
g_string_append(launch, "videoflip video-direction=GST_VIDEO_ORIENTATION_90R ! ");
|
||||
break;
|
||||
case RIGHT:
|
||||
g_string_append(launch, "videoflip method=counterclockwise ! ");
|
||||
g_string_append(launch, "videoflip video-direction=GST_VIDEO_ORIENTATION_90L ! ");
|
||||
break;
|
||||
default:
|
||||
g_string_append(launch, "videoflip method=rotate-180 ! ");
|
||||
g_string_append(launch, "videoflip video-direction=GST_VIDEO_ORIENTATION_180 ! ");
|
||||
break;
|
||||
}
|
||||
break;
|
||||
case HFLIP:
|
||||
switch (*rot) {
|
||||
case LEFT:
|
||||
g_string_append(launch, "videoflip method=upper-left-diagonal ! ");
|
||||
g_string_append(launch, "videoflip video-direction=GST_VIDEO_ORIENTATION_UL_LR ! ");
|
||||
break;
|
||||
case RIGHT:
|
||||
g_string_append(launch, "videoflip method=upper-right-diagonal ! ");
|
||||
g_string_append(launch, "videoflip video-direction=GST_VIDEO_ORIENTATION_UR_LL ! ");
|
||||
break;
|
||||
default:
|
||||
g_string_append(launch, "videoflip method=horizontal-flip ! ");
|
||||
g_string_append(launch, "videoflip video-direction=GST_VIDEO_ORIENTATION_HORIZ ! ");
|
||||
break;
|
||||
}
|
||||
break;
|
||||
case VFLIP:
|
||||
switch (*rot) {
|
||||
case LEFT:
|
||||
g_string_append(launch, "videoflip method=upper-right-diagonal ! ");
|
||||
g_string_append(launch, "videoflip video-direction=GST_VIDEO_ORIENTATION_UR_LL ! ");
|
||||
break;
|
||||
case RIGHT:
|
||||
g_string_append(launch, "videoflip method=upper-left-diagonal ! ");
|
||||
case RIGHT:
|
||||
g_string_append(launch, "videoflip video-direction=GST_VIDEO_ORIENTATION_UL_LR ! ");
|
||||
break;
|
||||
default:
|
||||
g_string_append(launch, "videoflip method=vertical-flip ! ");
|
||||
g_string_append(launch, "videoflip video-direction=GST_VIDEO_ORIENTATION_VERT ! ");
|
||||
break;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
switch (*rot) {
|
||||
case LEFT:
|
||||
g_string_append(launch, "videoflip method=counterclockwise ! ");
|
||||
g_string_append(launch, "videoflip video-direction=GST_VIDEO_ORIENTATION_90L ! ");
|
||||
break;
|
||||
case RIGHT:
|
||||
g_string_append(launch, "videoflip method=clockwise ! ");
|
||||
case RIGHT:
|
||||
g_string_append(launch, "videoflip video-direction=GST_VIDEO_ORIENTATION_90R ! ");
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* apple uses colorimetry=1:3:5:1 *
|
||||
* (not recognized by v4l2 plugin in Gstreamer < 1.20.4) *
|
||||
@@ -135,6 +139,9 @@ void video_renderer_init(logger_t *render_logger, const char *server_name, vide
|
||||
GstClock *clock = gst_system_clock_obtain();
|
||||
g_object_set(clock, "clock-type", GST_CLOCK_TYPE_REALTIME, NULL);
|
||||
|
||||
/* videosink choices that are auto */
|
||||
auto_videosink = (strstr(videosink, "autovideosink") || strstr(videosink, "fpsdisplaysink"));
|
||||
|
||||
logger = render_logger;
|
||||
|
||||
/* this call to g_set_application_name makes server_name appear in the X11 display window title bar, */
|
||||
@@ -153,11 +160,11 @@ void video_renderer_init(logger_t *render_logger, const char *server_name, vide
|
||||
g_string_append(launch, " ! ");
|
||||
g_string_append(launch, decoder);
|
||||
g_string_append(launch, " ! ");
|
||||
g_string_append(launch, converter);
|
||||
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 ! ");
|
||||
g_string_append(launch, videosink);
|
||||
g_string_append(launch, " name=video_sink");
|
||||
if (*video_sync) {
|
||||
g_string_append(launch, " sync=true");
|
||||
sync = true;
|
||||
@@ -182,23 +189,13 @@ void video_renderer_init(logger_t *render_logger, const char *server_name, vide
|
||||
gst_caps_unref(caps);
|
||||
gst_object_unref(clock);
|
||||
|
||||
renderer->sink = gst_bin_get_by_name (GST_BIN (renderer->pipeline), "video_sink");
|
||||
g_assert(renderer->sink);
|
||||
|
||||
#ifdef X_DISPLAY_FIX
|
||||
use_x11 = (strstr(videosink, "xvimagesink") || strstr(videosink, "ximagesink") || auto_videosink);
|
||||
fullscreen = *initial_fullscreen;
|
||||
renderer->server_name = server_name;
|
||||
renderer->gst_window = NULL;
|
||||
bool x_display_fix = false;
|
||||
/* only include X11 videosinks that provide fullscreen mode, or need ZOOMFIX */
|
||||
/* limit searching for X11 Windows in case autovideosink selects an incompatible videosink */
|
||||
if (strncmp(videosink,"autovideosink", strlen("autovideosink")) == 0 ||
|
||||
strncmp(videosink,"ximagesink", strlen("ximagesink")) == 0 ||
|
||||
strncmp(videosink,"xvimagesink", strlen("xvimagesink")) == 0 ||
|
||||
strncmp(videosink,"fpsdisplaysink", strlen("fpsdisplaysink")) == 0 ) {
|
||||
x_display_fix = true;
|
||||
}
|
||||
if (x_display_fix) {
|
||||
X11_search_attempts = 0;
|
||||
if (use_x11) {
|
||||
renderer->gst_window = calloc(1, sizeof(X11_Window_t));
|
||||
g_assert(renderer->gst_window);
|
||||
get_X11_Display(renderer->gst_window);
|
||||
@@ -284,7 +281,7 @@ void video_renderer_render_buffer(unsigned char* data, int *data_len, int *nal_c
|
||||
gst_buffer_fill(buffer, 0, data, *data_len);
|
||||
gst_app_src_push_buffer (GST_APP_SRC(renderer->appsrc), buffer);
|
||||
#ifdef X_DISPLAY_FIX
|
||||
if (renderer->gst_window && !(renderer->gst_window->window) && X11_search_attempts < MAX_X11_SEARCH_ATTEMPTS) {
|
||||
if (renderer->gst_window && !(renderer->gst_window->window) && use_x11) {
|
||||
X11_search_attempts++;
|
||||
logger_log(logger, LOGGER_DEBUG, "Looking for X11 UxPlay Window, attempt %d", (int) X11_search_attempts);
|
||||
get_x_window(renderer->gst_window, renderer->server_name);
|
||||
@@ -293,8 +290,6 @@ void video_renderer_render_buffer(unsigned char* data, int *data_len, int *nal_c
|
||||
if (fullscreen) {
|
||||
set_fullscreen(renderer->gst_window, &fullscreen);
|
||||
}
|
||||
} else if (X11_search_attempts == MAX_X11_SEARCH_ATTEMPTS) {
|
||||
logger_log(logger, LOGGER_DEBUG, "X11 UxPlay Window not found in %d search attempts", MAX_X11_SEARCH_ATTEMPTS);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -305,10 +300,10 @@ void video_renderer_flush() {
|
||||
}
|
||||
|
||||
void video_renderer_stop() {
|
||||
if (renderer) {
|
||||
gst_app_src_end_of_stream (GST_APP_SRC(renderer->appsrc));
|
||||
gst_element_set_state (renderer->pipeline, GST_STATE_NULL);
|
||||
}
|
||||
if (renderer) {
|
||||
gst_app_src_end_of_stream (GST_APP_SRC(renderer->appsrc));
|
||||
gst_element_set_state (renderer->pipeline, GST_STATE_NULL);
|
||||
}
|
||||
}
|
||||
|
||||
void video_renderer_destroy() {
|
||||
@@ -320,7 +315,6 @@ void video_renderer_destroy() {
|
||||
gst_element_set_state (renderer->pipeline, GST_STATE_NULL);
|
||||
}
|
||||
gst_object_unref(renderer->bus);
|
||||
gst_object_unref(renderer->sink);
|
||||
gst_object_unref (renderer->appsrc);
|
||||
gst_object_unref (renderer->pipeline);
|
||||
#ifdef X_DISPLAY_FIX
|
||||
@@ -328,7 +322,7 @@ void video_renderer_destroy() {
|
||||
free(renderer->gst_window);
|
||||
renderer->gst_window = NULL;
|
||||
}
|
||||
#endif
|
||||
#endif
|
||||
free (renderer);
|
||||
renderer = NULL;
|
||||
}
|
||||
@@ -370,6 +364,19 @@ gboolean gstreamer_pipeline_bus_callback(GstBus *bus, GstMessage *message, gpoin
|
||||
logger_log(logger, LOGGER_INFO, "GStreamer: End-Of-Stream");
|
||||
// g_main_loop_quit( (GMainLoop *) loop);
|
||||
break;
|
||||
case GST_MESSAGE_STATE_CHANGED:
|
||||
if (auto_videosink) {
|
||||
char *sink = strstr(GST_MESSAGE_SRC_NAME(message), "-actual-sink-");
|
||||
if (sink) {
|
||||
sink += strlen("-actual-sink-");
|
||||
logger_log(logger, LOGGER_DEBUG, "GStreamer: automatically-selected videosink is \"%ssink\"", sink);
|
||||
auto_videosink = false;
|
||||
#ifdef X_DISPLAY_FIX
|
||||
use_x11 = (strstr(sink, "ximage") || strstr(sink, "xvimage"));
|
||||
#endif
|
||||
}
|
||||
}
|
||||
break;
|
||||
#ifdef X_DISPLAY_FIX
|
||||
case GST_MESSAGE_ELEMENT:
|
||||
if (renderer->gst_window && renderer->gst_window->window) {
|
||||
@@ -417,4 +424,4 @@ gboolean gstreamer_pipeline_bus_callback(GstBus *bus, GstMessage *message, gpoin
|
||||
unsigned int video_renderer_listen(void *loop) {
|
||||
return (unsigned int) gst_bus_add_watch(renderer->bus, (GstBusFunc)
|
||||
gstreamer_pipeline_bus_callback, (gpointer) loop);
|
||||
}
|
||||
}
|
||||
|
||||
10
uxplay.1
10
uxplay.1
@@ -1,11 +1,11 @@
|
||||
.TH UXPLAY "1" "December 2023" "1.68" "User Commands"
|
||||
.TH UXPLAY "1" "August 2024" "1.69" "User Commands"
|
||||
.SH NAME
|
||||
uxplay \- start AirPlay server
|
||||
.SH SYNOPSIS
|
||||
.B uxplay
|
||||
[\fI\,-n name\/\fR] [\fI\,-s wxh\/\fR] [\fI\,-p \/\fR[\fI\,n\/\fR]] [more \fI OPTIONS \/\fR ...]
|
||||
.SH DESCRIPTION
|
||||
UxPlay 1.68: An open\-source AirPlay mirroring (+ audio streaming) server:
|
||||
UxPlay 1.69: An open\-source AirPlay mirroring (+ audio streaming) server:
|
||||
.SH OPTIONS
|
||||
.TP
|
||||
.B
|
||||
@@ -42,7 +42,7 @@ UxPlay 1.68: An open\-source AirPlay mirroring (+ audio streaming) server:
|
||||
.TP
|
||||
\fB\-o\fR Set display "overscanned" mode on (not usually needed)
|
||||
.TP
|
||||
\fB-fs\fR Full-screen (only works with X11, Wayland and VAAPI)
|
||||
\fB-fs\fR Full-screen (only works with X11, Wayland, VAAPI, D3D11)
|
||||
.TP
|
||||
\fB\-p\fR Use legacy ports UDP 6000:6001:7011 TCP 7000:7001:7100
|
||||
.TP
|
||||
@@ -95,7 +95,9 @@ UxPlay 1.68: An open\-source AirPlay mirroring (+ audio streaming) server:
|
||||
.TP
|
||||
\fB\-reset\fR n Reset after 3n seconds client silence (default 5, 0=never).
|
||||
.TP
|
||||
\fB\-nc\fR Do not close video window when client stops mirroring
|
||||
\fB\-nofreeze\fR Do NOT leave frozen screen in place after reset.
|
||||
.TP
|
||||
\fB\-nc\fR Do NOT close video window when client stops mirroring
|
||||
.TP
|
||||
\fB\-nohold\fR Drop current connection when new client connects.
|
||||
.TP
|
||||
|
||||
162
uxplay.cpp
162
uxplay.cpp
@@ -62,7 +62,7 @@
|
||||
#include "renderers/video_renderer.h"
|
||||
#include "renderers/audio_renderer.h"
|
||||
|
||||
#define VERSION "1.68"
|
||||
#define VERSION "1.69"
|
||||
|
||||
#define SECOND_IN_USECS 1000000
|
||||
#define SECOND_IN_NSECS 1000000000UL
|
||||
@@ -121,7 +121,8 @@ static unsigned short display[5] = {0}, tcp[3] = {0}, udp[3] = {0};
|
||||
static bool debug_log = DEFAULT_DEBUG_LOG;
|
||||
static int log_level = LOGGER_INFO;
|
||||
static bool bt709_fix = false;
|
||||
static int max_connections = 2;
|
||||
static int nohold = 0;
|
||||
static bool nofreeze = false;
|
||||
static unsigned short raop_port;
|
||||
static unsigned short airplay_port;
|
||||
static uint64_t remote_clock_offset = 0;
|
||||
@@ -583,7 +584,7 @@ static void print_info (char *name) {
|
||||
printf("-taper Use a \"tapered\" AirPlay volume-control profile\n");
|
||||
printf("-s wxh[@r]Set display resolution [refresh_rate] default 1920x1080[@60]\n");
|
||||
printf("-o Set display \"overscanned\" mode on (not usually needed)\n");
|
||||
printf("-fs Full-screen (only works with X11, Wayland and VAAPI)\n");
|
||||
printf("-fs Full-screen (only works with X11, Wayland, VAAPI, D3D11)\n");
|
||||
printf("-p Use legacy ports UDP 6000:6001:7011 TCP 7000:7001:7100\n");
|
||||
printf("-p n Use TCP and UDP ports n,n+1,n+2. range %d-%d\n", LOWEST_ALLOWED_PORT, HIGHEST_PORT);
|
||||
printf(" use \"-p n1,n2,n3\" to set each port, \"n1,n2\" for n3 = n2+1\n");
|
||||
@@ -609,7 +610,8 @@ static void print_info (char *name) {
|
||||
printf("-al x Audio latency in seconds (default 0.25) reported to client.\n");
|
||||
printf("-ca <fn> In Airplay Audio (ALAC) mode, write cover-art to file <fn>\n");
|
||||
printf("-reset n Reset after 3n seconds client silence (default %d, 0=never)\n", NTP_TIMEOUT_LIMIT);
|
||||
printf("-nc do Not Close video window when client stops mirroring\n");
|
||||
printf("-nofreeze Do NOT leave frozen screen in place after reset\n");
|
||||
printf("-nc Do NOT Close video window when client stops mirroring\n");
|
||||
printf("-nohold Drop current connection when new client connects.\n");
|
||||
printf("-restrict Restrict clients to those specified by \"-allow <deviceID>\"\n");
|
||||
printf(" UxPlay displays deviceID when a client attempts to connect\n");
|
||||
@@ -1033,7 +1035,7 @@ static void parse_arguments (int argc, char *argv[]) {
|
||||
} else if (arg == "-bt709") {
|
||||
bt709_fix = true;
|
||||
} else if (arg == "-nohold") {
|
||||
max_connections = 3;
|
||||
nohold = 1;
|
||||
} else if (arg == "-al") {
|
||||
int n;
|
||||
char *end;
|
||||
@@ -1125,6 +1127,8 @@ static void parse_arguments (int argc, char *argv[]) {
|
||||
db_low = db1;
|
||||
db_high = db2;
|
||||
printf("db range %f:%f\n", db_low, db_high);
|
||||
} else if (arg == "-nofreeze") {
|
||||
nofreeze = true;
|
||||
} else {
|
||||
fprintf(stderr, "unknown option %s, stopping (for help use option \"-h\")\n",argv[i]);
|
||||
exit(1);
|
||||
@@ -1330,6 +1334,95 @@ static int start_dnssd(std::vector<char> hw_addr, std::string name) {
|
||||
LOGE("Could not initialize dnssd library!: error %d", dnssd_error);
|
||||
return 1;
|
||||
}
|
||||
|
||||
/* after dnssd starts, reset the default feature set here
|
||||
* (overwrites features set in dnssdint.h).
|
||||
* default: FEATURES_1 = 0x5A7FFEE6, FEATURES_2 = 0 */
|
||||
|
||||
dnssd_set_airplay_features(dnssd, 0, 0); // AirPlay video supported
|
||||
dnssd_set_airplay_features(dnssd, 1, 1); // photo supported
|
||||
dnssd_set_airplay_features(dnssd, 2, 1); // video protected with FairPlay DRM
|
||||
dnssd_set_airplay_features(dnssd, 3, 0); // volume control supported for videos
|
||||
|
||||
dnssd_set_airplay_features(dnssd, 4, 0); // http live streaming (HLS) supported
|
||||
dnssd_set_airplay_features(dnssd, 5, 1); // slideshow supported
|
||||
dnssd_set_airplay_features(dnssd, 6, 1); //
|
||||
dnssd_set_airplay_features(dnssd, 7, 1); // mirroring supported
|
||||
|
||||
dnssd_set_airplay_features(dnssd, 8, 0); // screen rotation supported
|
||||
dnssd_set_airplay_features(dnssd, 9, 1); // audio supported
|
||||
dnssd_set_airplay_features(dnssd, 10, 1); //
|
||||
dnssd_set_airplay_features(dnssd, 11, 1); // audio packet redundancy supported
|
||||
|
||||
dnssd_set_airplay_features(dnssd, 12, 1); // FaiPlay secure auth supported
|
||||
dnssd_set_airplay_features(dnssd, 13, 1); // photo preloading supported
|
||||
dnssd_set_airplay_features(dnssd, 14, 1); // Authentication bit 4: FairPlay authentication
|
||||
dnssd_set_airplay_features(dnssd, 15, 1); // Metadata bit 1 support: Artwork
|
||||
|
||||
dnssd_set_airplay_features(dnssd, 16, 1); // Metadata bit 2 support: Soundtrack Progress
|
||||
dnssd_set_airplay_features(dnssd, 17, 1); // Metadata bit 0 support: Text (DAACP) "Now Playing" info.
|
||||
dnssd_set_airplay_features(dnssd, 18, 1); // Audio format 1 support:
|
||||
dnssd_set_airplay_features(dnssd, 19, 1); // Audio format 2 support: must be set for AirPlay 2 multiroom audio
|
||||
|
||||
dnssd_set_airplay_features(dnssd, 20, 1); // Audio format 3 support: must be set for AirPlay 2 multiroom audio
|
||||
dnssd_set_airplay_features(dnssd, 21, 1); // Audio format 4 support:
|
||||
dnssd_set_airplay_features(dnssd, 22, 1); // Authentication type 4: FairPlay authentication
|
||||
dnssd_set_airplay_features(dnssd, 23, 0); // Authentication type 1: RSA Authentication
|
||||
|
||||
dnssd_set_airplay_features(dnssd, 24, 0); //
|
||||
dnssd_set_airplay_features(dnssd, 25, 1); //
|
||||
dnssd_set_airplay_features(dnssd, 26, 0); // Has Unified Advertiser info
|
||||
dnssd_set_airplay_features(dnssd, 27, 1); // Supports Legacy Pairing
|
||||
|
||||
dnssd_set_airplay_features(dnssd, 28, 1); //
|
||||
dnssd_set_airplay_features(dnssd, 29, 0); //
|
||||
dnssd_set_airplay_features(dnssd, 30, 1); // RAOP support: with this bit set, the AirTunes service is not required.
|
||||
dnssd_set_airplay_features(dnssd, 31, 0); //
|
||||
|
||||
for (int i = 32; i < 64; i++) {
|
||||
dnssd_set_airplay_features(dnssd, i, 0);
|
||||
}
|
||||
|
||||
/* bits 32-63 are not used here: see https://emanualcozzi.net/docs/airplay2/features
|
||||
dnssd_set_airplay_features(dnssd, 32, 0); // isCarPlay when ON,; Supports InitialVolume when OFF
|
||||
dnssd_set_airplay_features(dnssd, 33, 0); // Supports Air Play Video Play Queue
|
||||
dnssd_set_airplay_features(dnssd, 34, 0); // Supports Air Play from cloud (requires that bit 6 is ON)
|
||||
dnssd_set_airplay_features(dnssd, 35, 0); // Supports TLS_PSK
|
||||
|
||||
dnssd_set_airplay_features(dnssd, 36, 0); //
|
||||
dnssd_set_airplay_features(dnssd, 37, 0); //
|
||||
dnssd_set_airplay_features(dnssd, 38, 0); // Supports Unified Media Control (CoreUtils Pairing and Encryption)
|
||||
dnssd_set_airplay_features(dnssd, 39, 0); //
|
||||
|
||||
dnssd_set_airplay_features(dnssd, 40, 0); // Supports Buffered Audio
|
||||
dnssd_set_airplay_features(dnssd, 41, 0); // Supports PTP
|
||||
dnssd_set_airplay_features(dnssd, 42, 0); // Supports Screen Multi Codec
|
||||
dnssd_set_airplay_features(dnssd, 43, 0); // Supports System Pairing
|
||||
|
||||
dnssd_set_airplay_features(dnssd, 44, 0); // is AP Valeria Screen Sender
|
||||
dnssd_set_airplay_features(dnssd, 45, 0); //
|
||||
dnssd_set_airplay_features(dnssd, 46, 0); // Supports HomeKit Pairing and Access Control
|
||||
dnssd_set_airplay_features(dnssd, 47, 0); //
|
||||
|
||||
dnssd_set_airplay_features(dnssd, 48, 0); // Supports CoreUtils Pairing and Encryption
|
||||
dnssd_set_airplay_features(dnssd, 49, 0); //
|
||||
dnssd_set_airplay_features(dnssd, 50, 0); // Metadata bit 3: "Now Playing" info sent by bplist not DAACP test
|
||||
dnssd_set_airplay_features(dnssd, 51, 0); // Supports Unified Pair Setup and MFi Authentication
|
||||
|
||||
dnssd_set_airplay_features(dnssd, 52, 0); // Supports Set Peers Extended Message
|
||||
dnssd_set_airplay_features(dnssd, 53, 0); //
|
||||
dnssd_set_airplay_features(dnssd, 54, 0); // Supports AP Sync
|
||||
dnssd_set_airplay_features(dnssd, 55, 0); // Supports WoL
|
||||
|
||||
dnssd_set_airplay_features(dnssd, 56, 0); // Supports Wol
|
||||
dnssd_set_airplay_features(dnssd, 57, 0); //
|
||||
dnssd_set_airplay_features(dnssd, 58, 0); // Supports Hangdog Remote Control
|
||||
dnssd_set_airplay_features(dnssd, 59, 0); // Supports AudioStreamConnection setup
|
||||
|
||||
dnssd_set_airplay_features(dnssd, 60, 0); // Supports Audo Media Data Control
|
||||
dnssd_set_airplay_features(dnssd, 61, 0); // Supports RFC2198 redundancy
|
||||
*/
|
||||
|
||||
/* bit 27 of Features determines whether the AirPlay2 client-pairing protocol will be used (1) or not (0) */
|
||||
dnssd_set_airplay_features(dnssd, 27, (int) setup_legacy_pairing);
|
||||
return 0;
|
||||
@@ -1361,6 +1454,14 @@ static bool check_blocked_client(char *deviceid) {
|
||||
|
||||
// Server callbacks
|
||||
|
||||
extern "C" void video_reset(void *cls) {
|
||||
reset_loop = true;
|
||||
remote_clock_offset = 0;
|
||||
relaunch_video = true;
|
||||
}
|
||||
|
||||
|
||||
|
||||
extern "C" void display_pin(void *cls, char *pin) {
|
||||
int margin = 10;
|
||||
int spacing = 3;
|
||||
@@ -1413,8 +1514,9 @@ extern "C" void conn_reset (void *cls, int timeouts, bool reset_video) {
|
||||
LOGI(" Sometimes the network connection may recover after a longer delay:\n"
|
||||
" the default timeout limit n = %d can be changed with the \"-reset n\" option", NTP_TIMEOUT_LIMIT);
|
||||
}
|
||||
printf("reset_video %d\n",(int) reset_video);
|
||||
close_window = reset_video; /* leave "frozen" window open if reset_video is false */
|
||||
if (!nofreeze) {
|
||||
close_window = reset_video; /* leave "frozen" window open if reset_video is false */
|
||||
}
|
||||
raop_stop(raop);
|
||||
reset_loop = true;
|
||||
}
|
||||
@@ -1484,12 +1586,18 @@ extern "C" void video_process (void *cls, raop_ntp_t *ntp, h264_decode_struct *d
|
||||
}
|
||||
|
||||
extern "C" void video_pause (void *cls) {
|
||||
#ifdef GST_124
|
||||
return; //pause/resume changes in GStreamer-1.24 break this code
|
||||
#endif
|
||||
if (use_video) {
|
||||
video_renderer_pause();
|
||||
}
|
||||
}
|
||||
|
||||
extern "C" void video_resume (void *cls) {
|
||||
#ifdef GST_124
|
||||
return; //pause/resume changes in GStreamer-1.24 break this code
|
||||
#endif
|
||||
if (use_video) {
|
||||
video_renderer_resume();
|
||||
}
|
||||
@@ -1595,6 +1703,14 @@ extern "C" void audio_set_coverart(void *cls, const void *buffer, int buflen) {
|
||||
}
|
||||
}
|
||||
|
||||
extern "C" void audio_set_progress(void *cls, unsigned int start, unsigned int curr, unsigned int end) {
|
||||
int duration = (int) (end - start)/44100;
|
||||
int position = (int) (curr - start)/44100;
|
||||
int remain = duration - position;
|
||||
printf("audio progress (min:sec): %d:%2.2d; remaining: %d:%2.2d; track length %d:%2.2d\n",
|
||||
position/60, position%60, remain/60, remain%60, duration/60, duration%60);
|
||||
}
|
||||
|
||||
extern "C" void audio_set_metadata(void *cls, const void *buffer, int buflen) {
|
||||
char dmap_tag[5] = {0x0};
|
||||
const unsigned char *metadata = (const unsigned char *) buffer;
|
||||
@@ -1708,18 +1824,27 @@ static int start_raop_server (unsigned short display[5], unsigned short tcp[3],
|
||||
raop_cbs.video_report_size = video_report_size;
|
||||
raop_cbs.audio_set_metadata = audio_set_metadata;
|
||||
raop_cbs.audio_set_coverart = audio_set_coverart;
|
||||
raop_cbs.audio_set_progress = audio_set_progress;
|
||||
raop_cbs.report_client_request = report_client_request;
|
||||
raop_cbs.display_pin = display_pin;
|
||||
raop_cbs.register_client = register_client;
|
||||
raop_cbs.check_register = check_register;
|
||||
raop_cbs.export_dacp = export_dacp;
|
||||
raop_cbs.video_reset = video_reset;
|
||||
|
||||
/* set max number of connections = 2 to protect against capture by new client */
|
||||
raop = raop_init(max_connections, &raop_cbs, mac_address.c_str(), keyfile.c_str());
|
||||
raop = raop_init(&raop_cbs);
|
||||
if (raop == NULL) {
|
||||
LOGE("Error initializing raop!");
|
||||
return -1;
|
||||
}
|
||||
raop_set_log_callback(raop, log_callback, NULL);
|
||||
raop_set_log_level(raop, log_level);
|
||||
/* set nohold = 1 to allow capture by new client */
|
||||
if (raop_init2(raop, nohold, mac_address.c_str(), keyfile.c_str())){
|
||||
LOGE("Error initializing raop (2)!");
|
||||
free (raop);
|
||||
return -1;
|
||||
}
|
||||
|
||||
/* write desired display pixel width, pixel height, refresh_rate, max_fps, overscanned. */
|
||||
/* use 0 for default values 1920,1080,60,30,0; these are sent to the Airplay client */
|
||||
@@ -1738,21 +1863,14 @@ static int start_raop_server (unsigned short display[5], unsigned short tcp[3],
|
||||
/* network port selection (ports listed as "0" will be dynamically assigned) */
|
||||
raop_set_tcp_ports(raop, tcp);
|
||||
raop_set_udp_ports(raop, udp);
|
||||
|
||||
raop_set_log_callback(raop, log_callback, NULL);
|
||||
raop_set_log_level(raop, log_level);
|
||||
|
||||
raop_port = raop_get_port(raop);
|
||||
raop_start(raop, &raop_port);
|
||||
raop_set_port(raop, raop_port);
|
||||
|
||||
if (tcp[2]) {
|
||||
airplay_port = tcp[2];
|
||||
} else {
|
||||
/* is there a problem if this coincides with a randomly-selected tcp raop_mirror_data port?
|
||||
* probably not, as the airplay port is only used for initial client contact */
|
||||
airplay_port = (raop_port != HIGHEST_PORT ? raop_port + 1 : raop_port - 1);
|
||||
}
|
||||
/* use raop_port for airplay_port (instead of tcp[2]) */
|
||||
airplay_port = raop_port;
|
||||
|
||||
if (dnssd) {
|
||||
raop_set_dnssd(raop, dnssd);
|
||||
} else {
|
||||
@@ -1930,7 +2048,11 @@ int main (int argc, char *argv[]) {
|
||||
}
|
||||
|
||||
if (videosink == "d3d11videosink" && use_video) {
|
||||
videosink.append(" fullscreen-toggle-mode=alt-enter");
|
||||
if (fullscreen) {
|
||||
videosink.append(" fullscreen-toggle-mode=GST_D3D11_WINDOW_FULLSCREEN_TOGGLE_MODE_PROPERTY fullscreen=true ");
|
||||
} else {
|
||||
videosink.append(" fullscreen-toggle-mode=GST_D3D11_WINDOW_FULLSCREEN_TOGGLE_MODE_ALT_ENTER ");
|
||||
}
|
||||
LOGI("d3d11videosink is being used with option fullscreen-toggle-mode=alt-enter\n"
|
||||
"Use Alt-Enter key combination to toggle into/out of full-screen mode");
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
Name: uxplay
|
||||
Version: 1.68.2
|
||||
Version: 1.69
|
||||
Release: 1%{?dist}
|
||||
|
||||
%global gittag v%{version}
|
||||
@@ -135,7 +135,7 @@ cd build
|
||||
%{_docdir}/%{name}/llhttp/LICENSE-MIT
|
||||
|
||||
%changelog
|
||||
* Fri Dec 29 2023 UxPlay maintainer <https://github.com/FDH2/UxPlay>
|
||||
* Fri Aug 09 2024 UxPlay maintainer <https://github.com/FDH2/UxPlay>
|
||||
Initial uxplay.spec: tested on Fedora 38, Rocky Linux 9.2, OpenSUSE
|
||||
Leap 15.5, Mageia 9, OpenMandriva ROME, PCLinuxOS
|
||||
-
|
||||
|
||||
Reference in New Issue
Block a user