diff --git a/CMakeLists.txt b/CMakeLists.txt index 2a2ab75..225583d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -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 diff --git a/README.html b/README.html index f0df080..7a0da6c 100644 --- a/README.html +++ b/README.html @@ -1,29 +1,27 @@

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).

Now +id="now-developed-at-the-github-site-httpsgithub.comfdh2uxplay-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).

+found).

Highlights:

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 (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).

+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”).

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.

AirPlay volume-control attenuates volume (gain) by up to -30dB: the -range -30dB:0dB can be rescaled from Low:0, or +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.

+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 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).

+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 @@ -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.

+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.

Building @@ -790,9 +801,8 @@ environment (this uses “ninja” in place of install UxPlay dependencies (openssl is already installed with MSYS2):

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 If you are trying a different Windows build system, MSVC versions of +GStreamer for Windows are available from the official GStreamer site, but only the MinGW 64-bit build on MSYS2 has been tested.

@@ -856,14 +866,20 @@ used.

-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 +gtksink.

+

The executable uxplay.exe can also be run without the MSYS2 environment, in the Windows Terminal, with C:\msys64\mingw64\bin\uxplay.

@@ -977,7 +993,7 @@ full-screen 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.

+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 “legacy” ports TCP 7100, 7000, 7001, UDP 6000, 6001, @@ -1082,6 +1098,10 @@ present, and synchronize with 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 @@ -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.

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 diff --git a/README.md b/README.md index 8bc4aa4..1a5d562 100644 --- a/README.md +++ b/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 `` 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 ]```. The choice `` = ``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 ` option, some choices for `` 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 diff --git a/README.txt b/README.txt index 9de8972..e630f5a 100644 --- a/README.txt +++ b/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 (where ALL user issues should be posted, and latest versions can be found). +### **Now developed at the GitHub site (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 ]`. The choice `` = `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 ` option, some choices for `` 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 diff --git a/lib/CMakeLists.txt b/lib/CMakeLists.txt index 4dbdd37..7167c94 100644 --- a/lib/CMakeLists.txt +++ b/lib/CMakeLists.txt @@ -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() diff --git a/lib/http_request.c b/lib/http_request.c index e259a60..5a6343a 100644 --- a/lib/http_request.c +++ b/lib/http_request.c @@ -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) { diff --git a/lib/http_request.h b/lib/http_request.h index e8ebfd7..d72a6f1 100644 --- a/lib/http_request.h +++ b/lib/http_request.h @@ -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); diff --git a/lib/http_response.c b/lib/http_response.c index 2a18a77..50f1d89 100644 --- a/lib/http_response.c +++ b/lib/http_response.c @@ -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 diff --git a/lib/http_response.h b/lib/http_response.h index 43e1128..fcc7899 100644 --- a/lib/http_response.h +++ b/lib/http_response.h @@ -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); diff --git a/lib/httpd.c b/lib/httpd.c index 70e607d..7140e2e 100644 --- a/lib/httpd.c +++ b/lib/httpd.c @@ -19,6 +19,7 @@ #include #include #include +#include #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; imax_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); } - diff --git a/lib/httpd.h b/lib/httpd.h index fc184e1..f64d166 100644 --- a/lib/httpd.h +++ b/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); diff --git a/lib/netutils.c b/lib/netutils.c index 5832c43..642efba 100644 --- a/lib/netutils.c +++ b/lib/netutils.c @@ -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; } diff --git a/lib/netutils.h b/lib/netutils.h index 63854ad..e64c638 100644 --- a/lib/netutils.h +++ b/lib/netutils.h @@ -19,7 +19,7 @@ int netutils_init(); void netutils_cleanup(); int netutils_init_socket(unsigned short *port, int use_ipv6, int use_udp); -unsigned char *netutils_get_address(void *sockaddr, int *length); +unsigned 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 diff --git a/lib/raop.c b/lib/raop.c index 56a48fc..af14baf 100644 --- a/lib/raop.c +++ b/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) { diff --git a/lib/raop.h b/lib/raop.h index 2888d3d..dc1f767 100644 --- a/lib/raop.h +++ b/lib/raop.h @@ -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); diff --git a/lib/raop_handlers.h b/lib/raop_handlers.h index 50470a2..6493c33 100644 --- a/lib/raop_handlers.h +++ b/lib/raop_handlers.h @@ -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!"); diff --git a/lib/raop_ntp.c b/lib/raop_ntp.c index 57cec8d..3f18027 100644 --- a/lib/raop_ntp.c +++ b/lib/raop_ntp.c @@ -23,6 +23,7 @@ #include #include #include +#include #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); diff --git a/lib/raop_rtp_mirror.c b/lib/raop_rtp_mirror.c index 4b0603e..141c65e 100644 --- a/lib/raop_rtp_mirror.c +++ b/lib/raop_rtp_mirror.c @@ -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; } diff --git a/lib/raop_rtp_mirror.h b/lib/raop_rtp_mirror.h index f0e9e56..2e7914d 100644 --- a/lib/raop_rtp_mirror.h +++ b/lib/raop_rtp_mirror.h @@ -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 diff --git a/lib/srp.c b/lib/srp.c index 6190de7..d909cbf 100644 --- a/lib/srp.c +++ b/lib/srp.c @@ -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) diff --git a/lib/utils.c b/lib/utils.c index 561e79b..f6c89f0 100644 --- a/lib/utils.c +++ b/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; +} diff --git a/lib/utils.h b/lib/utils.h index 671115a..be5db30 100644 --- a/lib/utils.h +++ b/lib/utils.h @@ -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 diff --git a/renderers/CMakeLists.txt b/renderers/CMakeLists.txt index 72c73d3..1fb025d 100644 --- a/renderers/CMakeLists.txt +++ b/renderers/CMakeLists.txt @@ -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 diff --git a/renderers/video_renderer_gstreamer.c b/renderers/video_renderer_gstreamer.c index 9c27c91..1bd133b 100644 --- a/renderers/video_renderer_gstreamer.c +++ b/renderers/video_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); -} +} diff --git a/uxplay.1 b/uxplay.1 index 9a0d526..d68fcdf 100644 --- a/uxplay.1 +++ b/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 diff --git a/uxplay.cpp b/uxplay.cpp index 0983467..1a7ce2f 100644 --- a/uxplay.cpp +++ b/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 In Airplay Audio (ALAC) mode, write cover-art to file \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 \"\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 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"); } diff --git a/uxplay.spec b/uxplay.spec index 225b1e5..07157a7 100644 --- a/uxplay.spec +++ b/uxplay.spec @@ -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 +* Fri Aug 09 2024 UxPlay maintainer Initial uxplay.spec: tested on Fedora 38, Rocky Linux 9.2, OpenSUSE Leap 15.5, Mageia 9, OpenMandriva ROME, PCLinuxOS -