Merge pull request #197 from FDH2/master

continuing development towards uxplay-1.73
This commit is contained in:
antimof
2025-11-03 15:54:44 +03:00
committed by GitHub
33 changed files with 3051 additions and 916 deletions

View File

@@ -0,0 +1,45 @@
.TH UXPLAY 1 2025-10-26 "UxPlay 1.72" "User Commands"
.SH NAME
uxplay-beacon.py \- Python (>= 3.6) script for a Bluetooth LE Service-Discovery beacon.
.SH SYNOPSIS
.B uxplay-beacon.py
[\fI\, -h, --help] + more options.
.SH DESCRIPTION
UxPlay 1.72: Standalone Python Script for Bluetooth LE Service Discovery (DBus).
.SH OPTIONS
.TP
.B
\fB\--file\fR fn Specify alternate configuration file
.TP
\fB\--path\fR fn Specify non-default Bluetooth LE data file used by uxplay
.TP
\fB\--ipv4\fR ip Override automatically-found ipv4 address for contacting UxPlay
.TP
\fB\--AdvMin\fR x Minimum Advertising interval in msecs (>= 100)
.TP
\fB\--AdvMin\fR y Maximum Advertising interval in msecs (>= AdvMin, <= 102400)
.TP
\fB\--index\fR x Used to distinguish different instances of beacons
.TP
\fB \-h, --help\fR Show help text.
.SH
FILES
Options in beacon configuration file ~/.uxplay.beacon
.TP
are applied first (command-line options may modify them). Format:
.TP
one option per line, with initial "--"; lines beginning with "#" ignored.
.SH
AUTHORS
.TP
Various, see website or distribution.
.SH
COPYRIGHT
.TP
Various, see website or distribution. License: GPL v3+:
.TP
GNU GPL version 3 or later. (some parts LGPL v.2.1+ or MIT).
.SH
SEE ALSO
.TP
Website: <https://github.com/FDH2/UxPlay>

View File

@@ -0,0 +1,477 @@
#!/usr/bin/env python3
# SPDX-License-Identifier: LGPL-2.1-or-later
# adapted from https://github.com/bluez/bluez/blob/master/test/example-advertisement
#----------------------------------------------------------------
# a standalone python-3.6 or later DBus-based AirPlay Service-Discovery Bluetooth LE beacon for UxPlay
# (c) F. Duncanh, October 2025
import gi
try:
from gi.repository import GLib
except ImportError as e:
print(f"ImportError: {e}, failed to import GLib")
import dbus
import dbus.exceptions
import dbus.mainloop.glib
import dbus.service
ad_manager = None
airplay_advertisement = None
server_address = None
BLUEZ_SERVICE_NAME = 'org.bluez'
LE_ADVERTISING_MANAGER_IFACE = 'org.bluez.LEAdvertisingManager1'
DBUS_OM_IFACE = 'org.freedesktop.DBus.ObjectManager'
DBUS_PROP_IFACE = 'org.freedesktop.DBus.Properties'
LE_ADVERTISEMENT_IFACE = 'org.bluez.LEAdvertisement1'
class InvalidArgsException(dbus.exceptions.DBusException):
_dbus_error_name = 'org.freedesktop.DBus.Error.InvalidArgs'
class NotSupportedException(dbus.exceptions.DBusException):
_dbus_error_name = 'org.bluez.Error.NotSupported'
class NotPermittedException(dbus.exceptions.DBusException):
_dbus_error_name = 'org.bluez.Error.NotPermitted'
class InvalidValueLengthException(dbus.exceptions.DBusException):
_dbus_error_name = 'org.bluez.Error.InvalidValueLength'
class FailedException(dbus.exceptions.DBusException):
_dbus_error_name = 'org.bluez.Error.Failed'
class AirPlay_Service_Discovery_Advertisement(dbus.service.Object):
PATH_BASE = '/org/bluez/airplay_service_discovery_advertisement'
def __init__(self, bus, index):
self.path = self.PATH_BASE + str(index)
self.bus = bus
self.manufacturer_data = None
self.min_intrvl = 0
self.max_intrvl = 0
dbus.service.Object.__init__(self, bus, self.path)
def get_properties(self):
properties = dict()
properties['Type'] = 'broadcast'
if self.manufacturer_data is not None:
properties['ManufacturerData'] = dbus.Dictionary(
self.manufacturer_data, signature='qv')
if self.min_intrvl > 0:
properties['MinInterval'] = dbus.UInt32(self.min_intrvl)
if self.max_intrvl > 0:
properties['MaxInterval'] = dbus.UInt32(self.max_intrvl)
return {LE_ADVERTISEMENT_IFACE: properties}
def get_path(self):
return dbus.ObjectPath(self.path)
def add_manufacturer_data(self, manuf_code, manuf_data):
if not self.manufacturer_data:
self.manufacturer_data = dbus.Dictionary({}, signature='qv')
self.manufacturer_data[manuf_code] = dbus.Array(manuf_data, signature='y')
def set_min_intrvl(self, min_intrvl):
if self.min_intrvl == 0:
self.min_intrvl = 100
self.min_intrvl = max(min_intrvl, 100)
def set_max_intrvl(self, max_intrvl):
if self.max_intrvl == 0:
self.max_intrvl = 100
self.max_intrvl = max(max_intrvl, 100)
@dbus.service.method(DBUS_PROP_IFACE,
in_signature='s',
out_signature='a{sv}')
def GetAll(self, interface):
if interface != LE_ADVERTISEMENT_IFACE:
raise InvalidArgsException()
return self.get_properties()[LE_ADVERTISEMENT_IFACE]
@dbus.service.method(LE_ADVERTISEMENT_IFACE,
in_signature='',
out_signature='')
def Release(self):
print(f'{self.path}: Released!')
class AirPlayAdvertisement(AirPlay_Service_Discovery_Advertisement):
def __init__(self, bus, index, ipv4_str, port, min_intrvl, max_intrvl):
AirPlay_Service_Discovery_Advertisement.__init__(self, bus, index)
assert port > 0
assert port <= 65535
mfg_data = bytearray([0x09, 0x08, 0x13, 0x30]) # Apple Data Unit type 9 (Airplay), length 8, flags 0001 0011, seed 30
import ipaddress
ipv4_address = ipaddress.ip_address(ipv4_str)
ipv4 = bytearray(ipv4_address.packed)
mfg_data.extend(ipv4)
port_bytes = port.to_bytes(2, 'big')
mfg_data.extend(port_bytes)
self.add_manufacturer_data(0x004c, mfg_data)
self.set_min_intrvl(min_intrvl)
self.set_max_intrvl(max_intrvl)
def register_ad_cb():
global server_address
print(f'AirPlay Service_Discovery Advertisement ({server_address}) registered')
def register_ad_error_cb(error):
print(f'Failed to register advertisement: {error}')
global ad_manager
ad_manager = None
def find_adapter(bus):
remote_om = dbus.Interface(bus.get_object(BLUEZ_SERVICE_NAME, '/'),
DBUS_OM_IFACE)
objects = remote_om.GetManagedObjects()
for o, props in objects.items():
if LE_ADVERTISING_MANAGER_IFACE in props:
return o
return None
def setup_beacon(ipv4_str, port, advmin, advmax, index):
global ad_manager
global airplay_advertisement
global server_address
server_address = f"{ipv4_str}:{port}"
dbus.mainloop.glib.DBusGMainLoop(set_as_default=True)
bus = dbus.SystemBus()
adapter = find_adapter(bus)
if not adapter:
print(f'LEAdvertisingManager1 interface not found')
return
adapter_props = dbus.Interface(bus.get_object(BLUEZ_SERVICE_NAME, adapter),
"org.freedesktop.DBus.Properties")
adapter_props.Set("org.bluez.Adapter1", "Powered", dbus.Boolean(1))
ad_manager = dbus.Interface(bus.get_object(BLUEZ_SERVICE_NAME, adapter),
LE_ADVERTISING_MANAGER_IFACE)
airplay_advertisement = AirPlayAdvertisement(bus, index, ipv4_str, port, advmin, advmax)
def beacon_on():
global ad_manager
global airplay_advertisement
ad_manager.RegisterAdvertisement(airplay_advertisement.get_path(), {},
reply_handler=register_ad_cb,
error_handler=register_ad_error_cb)
if ad_manager is None:
airplay_advertisement = None
return False
else:
return True
def beacon_off():
global ad_manager
global airplay_advertisement
ad_manager.UnregisterAdvertisement(airplay_advertisement)
print(f'AirPlay Service-Discovery beacon advertisement unregistered')
ad_manager = None
dbus.service.Object.remove_from_connection(airplay_advertisement)
airplay_advertisement = None
#==generic code (non-dbus) below here =============
import argparse
import os
import sys
import psutil
import struct
import socket
import time
# global variables
beacon_is_running = False
beacon_is_pending_on = False
beacon_is_pending_off = False
port = int(0)
advmin = int(100)
advmax = int(100)
ipv4_str = "ipv4_address"
index = int(0)
def start_beacon():
global beacon_is_running
global port
global ipv4_str
global advmin
global advmax
global index
setup_beacon(ipv4_str, port, advmin, advmax, index)
beacon_is_running = beacon_on()
def stop_beacon():
global beacon_is_running
beacon_off()
beacon_is_running = False
def pid_is_running(pid):
return psutil.pid_exists(pid)
def check_process_name(pid, pname):
try:
process = psutil.Process(pid)
if process.name().find(pname,0) == 0:
return True
else:
return False
except psutil.NoSuchProcess:
return False
def check_pending():
global beacon_is_running
global beacon_is_pending_on
global beacon_is_pending_off
if beacon_is_running:
if beacon_is_pending_off:
stop_beacon()
beacon_is_pending_off = False
else:
if beacon_is_pending_on:
start_beacon()
beacon_is_pending_on = False
return True
def check_file_exists(file_path):
global port
global beacon_is_running
global beacon_is_pending_on
global beacon_is_pending_off
if os.path.exists(file_path):
with open(file_path, 'rb') as file:
data = file.read(2)
port = struct.unpack('<H', data)[0]
data = file.read(4)
pid = struct.unpack('<I', data)[0]
if not pid_is_running(pid):
file.close()
test = False
else :
data = file.read()
file.close()
pname = data.split(b'\0',1)[0].decode('utf-8')
last_element_of_pname = os.path.basename(pname)
test = check_process_name(pid, last_element_of_pname)
if test == True:
if not beacon_is_running:
beacon_is_pending_on = True
else:
print(f'orphan beacon file {file_path} exists, but process {pname} (pid {pid}) is no longer active')
try:
os.remove(file_path)
print(f'File "{file_path}" deleted successfully.')
except FileNotFoundError:
print(f'File "{file_path}" not found.')
if beacon_is_running:
beacon_is_pending_off = True
else:
if beacon_is_running:
beacon_is_pending_off = True
def on_timeout(file_path):
check_file_exists(file_path)
return True
def process_input(value):
try:
my_integer = int(value)
return my_integer
except ValueError:
print(f'Error: could not convert "{value}" to integer: {my_integer}')
return None
#check AdvInterval
def check_adv_intrvl(min, max):
if not (100 <= min):
raise ValueError('AdvMin was smaller than 100 msecs')
if not (max >= min):
raise ValueError('AdvMax was smaller than AdvMin')
if not (max <= 10240):
raise ValueError('AdvMax was larger than 10240 msecs')
def main(file_path, ipv4_str_in, advmin_in, advmax_in, index_in):
global ipv4_str
global advmin
global advmax
global index
ipv4_str = ipv4_str_in
advmin = advmin_in
advmax = advmax_in
index = index_in
try:
while True:
try:
check_adv_intrvl(advmin, advmax)
except ValueError as e:
print(f'Error: {e}')
raise SystemExit(1)
GLib.timeout_add_seconds(1, on_timeout, file_path)
GLib.timeout_add(200, check_pending)
mainloop = GLib.MainLoop()
mainloop.run()
except KeyboardInterrupt:
print(f'\nExiting ...')
sys.exit(0)
if __name__ == '__main__':
if not sys.version_info >= (3,6):
print("uxplay-beacon.py requires Python 3.6 or higher")
# Create an ArgumentParser object
parser = argparse.ArgumentParser(
description='A program that runs an AirPlay service discovery BLE beacon.',
epilog='Example: python beacon.py --ipv4 "192.168.1.100" --path "/home/user/ble" --AdvMin 100 --AdvMax 100"'
)
home_dir = os.path.expanduser("~")
# Add arguments
parser.add_argument(
'--file',
type=str,
default= home_dir + "/.uxplay.beacon",
help='beacon startup file (optional): one entry (key, value) per line, e.g. --ipv4 192.168.1.100, (lines startng with with # are ignored)'
)
parser.add_argument(
'--path',
type=str,
default= home_dir + "/.uxplay.ble",
help='path to AirPlay server BLE beacon information file (default: ~/.uxplay.ble)).'
)
parser.add_argument(
'--ipv4',
type=str,
default='use gethostbyname',
help='ipv4 address of AirPlay server (default: use gethostbyname).'
)
parser.add_argument(
'--AdvMin',
type=str,
default="0",
help='The minimum Advertising Interval (>= 100) units=msec, default 100)'
)
parser.add_argument(
'--AdvMax',
type=str,
default="0",
help='The maximum Advertising Interval (>= AdvMin, <= 10240) units=msec, default 100)'
)
parser.add_argument(
'--index',
type=str,
default="0",
help='use index >= 0 to distinguish multiple AirPlay Service Discovery beacons, default 0)'
)
# Parse the command-line argunts
args = parser.parse_args()
ipv4_str = None
path = None
advmin = int(100)
advmax = int(100)
index = int(0)
if args.file:
print(f'Using config file: {args.file}')
if os.path.exists(args.file):
with open(args.file, 'r') as file:
for line in file:
stripped_line = line.strip()
if stripped_line.startswith('#'):
continue
parts = stripped_line.partition(" ")
part0 = parts[0]
part2 = parts[2]
key = part0.strip()
value = part2.strip()
if key == "--path":
path = value
elif key == "--ipv4":
ipv4_str = value
elif key == "--AdvMin":
if value.isdigit():
advmin = int(value)
else:
print(f'Invalid config file input (--AdvMin) {value} in {args.file}')
raise SystemExit(1)
elif key == "--AdvMax":
if value.isdigit():
advmax = int(value)
else:
print(f'Invalid config file input (--AdvMax) {value} in {args.file}')
raise SystemExit(1)
elif key == "--index":
if value.isdigit():
index = int(value)
else:
print(f'Invalid config file input (--index) {value} in {args.file}')
raise SystemExit(1)
else:
print(f'Unknown key "{key}" in config file {args.file}')
raise SystemExit(1)
if args.ipv4 == "use gethostbyname":
if (ipv4_str is None):
ipv4_str = socket.gethostbyname(socket.gethostname())
else:
ipv4_str = args.ipv4
if args.AdvMin != "0":
if args.AdvMin.isdigit():
advmin = int(args.AdvMin)
else:
print(f'Invalid input (AdvMin) {args.AdvMin}')
raise SystemExit(1)
if args.AdvMax != "0":
if args.AdvMax.isdigit():
advmax = int(args.AdvMax)
else:
print(f'Invalid input (AdvMin) {args.AdvMin}')
raise SystemExit(1)
if args.index != "0":
if args.index.isdigit():
index = int(args.index)
else:
print(f'Invalid input (AdvMin) {args.AdvMin}')
raise SystemExit(1)
if index < 0:
raise ValueError('index was negative (forbidden)')
print(f'AirPlay Service-Discovery Bluetooth LE beacon: using BLE file {args.path}, advmin:advmax {advmin}:{advmax} index:{index}')
print(f'(Press Ctrl+C to exit)')
main(args.path, ipv4_str, advmin, advmax, index)

View File

@@ -0,0 +1,39 @@
.TH UXPLAY 1 2025-10-26 "UxPlay 1.72" "User Commands"
.SH NAME
uxplay-beacon.py \- Python (>= 3.6) script for a Bluetooth LE Service-Discovery beacon.
.SH SYNOPSIS
.B uxplay-beacon.py
[\fI\, -h, --help] + more options.
.SH DESCRIPTION
UxPlay 1.72: Standalone Python Script for Bluetooth LE Service Discovery (Windows).
.SH OPTIONS
.TP
.B
\fB\--file\fR fn Specify alternate configuration file
.TP
\fB\--path\fR fn Specify non-default Bluetooth LE data file used by uxplay
.TP
\fB\--ipv4\fR ip Override automatically-obtained ipv4 address for contacting UxPlay
.TP
\fB \-h, --help\fR Show help text.
.SH
FILES
Options in beacon configuration file ~/.uxplay.beacon
.TP
are applied first (command-line options may modify them). Format:
.TP
one option per line, with initial "--"; lines beginning with "#" ignored.
.SH
AUTHORS
.TP
Various, see website or distribution.
.SH
COPYRIGHT
.TP
Various, see website or distribution. License: GPL v3+:
.TP
GNU GPL version 3 or later. (some parts LGPL v.2.1+ or MIT).
.SH
SEE ALSO
.TP
Website: <https://github.com/FDH2/UxPlay>

View File

@@ -0,0 +1,274 @@
#!/usr/bin/env python3
# SPDX-License-Identifier: LGPL-2.1-or-later
#----------------------------------------------------------------
# a standalone python-3.6 or later winrt-based AirPlay Service-Discovery Bluetooth LE beacon for UxPlay
# (c) F. Duncanh, October 2025
import gi
try:
from gi.repository import GLib
except ImportError:
print(f"ImportError: failed to import GLib")
# Import WinRT APIs
try:
import winrt.windows.foundation.collections
except ImportError:
print(f"ImportError from winrt-Windows.Foundation.Collections")
print(f"Install with 'pip install winrt-Windows.Foundation.Collections'")
try:
import winrt.windows.devices.bluetooth.advertisement as ble_adv
except ImportError:
print(f"ImportError from winrt-Windows.Devices.Bluetooth.Advertisement")
print(f"Install with 'pip install winrt-Windows.Devices.Bluetooth.Advertisement'")
try:
import winrt.windows.storage.streams as streams
except ImportError:
print(f"ImportError from winrt-Windows.Storage.Streams")
print(f"Install with 'pip install winrt-Windows.Storage.Streams'")
import struct
import ipaddress
import asyncio
#global variables used by winrt.windows.devices.bluetooth.advertisement code
publisher = None
def on_status_changed(sender, args):
global publisher
print(f"Publisher status change to: {args.status.name}")
if args.status.name == "STOPPED":
publisher = None
def create_airplay_service_discovery_advertisement_publisher(ipv4_str, port):
assert port > 0
assert port <= 65535
mfg_data = bytearray([0x09, 0x08, 0x13, 0x30]) # Apple Data Unit type 9 (Airplay), length 8, flags 0001 0011, seed 30
ipv4_address = ipaddress.ip_address(ipv4_str)
ipv4 = bytearray(ipv4_address.packed)
mfg_data.extend(ipv4)
port_bytes = port.to_bytes(2, 'big')
mfg_data.extend(port_bytes)
writer = streams.DataWriter()
writer.write_bytes(mfg_data)
manufacturer_data = ble_adv.BluetoothLEManufacturerData()
manufacturer_data.company_id = 0x004C #Apple
manufacturer_data.data = writer.detach_buffer()
advertisement = ble_adv.BluetoothLEAdvertisement()
advertisement.manufacturer_data.append(manufacturer_data)
global publisher
publisher = ble_adv.BluetoothLEAdvertisementPublisher(advertisement)
publisher.add_status_changed(on_status_changed)
async def publish_advertisement():
try:
publisher.start()
print(f"Publisher started successfully")
except Exception as e:
print(f"Failed to start Publisher: {e}")
print(f"Publisher Status: {publisher.status.name}")
def setup_beacon(ipv4_str, port):
#index will be ignored
print(f"setup_beacon for {ipv4_str}:{port}")
create_airplay_service_discovery_advertisement_publisher(ipv4_str, port)
def beacon_on():
global publisher
try:
asyncio.run( publish_advertisement())
return True
except Exception as e:
print(f"Failed to start publisher: {e}")
return False
def beacon_off():
global publisher
publisher.stop()
#==generic code (non-winrt) below here =============
import argparse
import os
import sys
import psutil
import struct
import socket
import time
# global variables
beacon_is_running = False
beacon_is_pending_on = False
beacon_is_pending_off = False
port = int(0)
ipv4_str = "ipv4_address"
def start_beacon():
global beacon_is_running
global port
global ipv4_str
setup_beacon(ipv4_str, port)
beacon_is_running = beacon_on()
def stop_beacon():
global beacon_is_running
beacon_off()
beacon_is_running = False
def pid_is_running(pid):
return psutil.pid_exists(pid)
def check_process_name(pid, pname):
try:
process = psutil.Process(pid)
if process.name().find(pname,0) == 0:
return True
else:
return False
except psutil.NoSuchProcess:
return False
def check_pending():
global beacon_is_running
global beacon_is_pending_on
global beacon_is_pending_off
if beacon_is_running:
if beacon_is_pending_off:
stop_beacon()
beacon_is_pending_off = False
else:
if beacon_is_pending_on:
start_beacon()
beacon_is_pending_on = False
return True
def check_file_exists(file_path):
global port
global beacon_is_running
global beacon_is_pending_on
global beacon_is_pending_off
if os.path.exists(file_path):
with open(file_path, 'rb') as file:
data = file.read(2)
port = struct.unpack('<H', data)[0]
data = file.read(4)
pid = struct.unpack('<I', data)[0]
if not pid_is_running(pid):
file.close()
test = False
else:
data = file.read()
file.close()
pname = data.split(b'\0',1)[0].decode('utf-8')
last_element_of_pname = os.path.basename(pname)
test = check_process_name(pid, last_element_of_pname)
if test == True:
if not beacon_is_running:
beacon_is_pending_on = True
else:
if beacon_is_running:
print(f'orphan beacon file {file_path} exists, but process {pname} (pid {pid}) is no longer active')
# PermissionError prevents deletion of orphan beacon files in Windows systems
beacon_is_pending_off = True
else:
if beacon_is_running:
beacon_is_pending_off = True
def on_timeout(file_path):
check_file_exists(file_path)
return True
def main(file_path, ipv4_str_in):
global ipv4_str
ipv4_str = ipv4_str_in
try:
while True:
GLib.timeout_add_seconds(1, on_timeout, file_path)
GLib.timeout_add(200, check_pending)
mainloop = GLib.MainLoop()
mainloop.run()
except KeyboardInterrupt:
print(f'\nExiting ...')
sys.exit(0)
if __name__ == '__main__':
if not sys.version_info >= (3,6):
print("uxplay-beacon.py requires Python 3.6 or higher")
# Create an ArgumentParser object
parser = argparse.ArgumentParser(
description='A program (for MS Windows systems only) that runs an AirPlay service discovery BLE beacon.',
epilog='Example: python beacon.py --ipv4 "192.168.1.100" --path "/home/user/ble"'
)
home_dir = os.environ.get("HOME")
print(f"homedir = {home_dir}")
# Add arguments
parser.add_argument(
'--file',
type=str,
default= home_dir + "/.uxplay.beacon",
help='beacon startup file (optional): one entry (key, value) per line, e.g. --ipv4 192.168.1.100, (lines startng with with # are ignored)'
)
parser.add_argument(
'--path',
type=str,
default= home_dir + "/.uxplay.ble",
help='path to AirPlay server BLE beacon information file (default: ~/.uxplay.ble)).'
)
parser.add_argument(
'--ipv4',
type=str,
default='use gethostbyname',
help='ipv4 address of AirPlay server (default: use gethostbyname).'
)
# Parse the command-line argunts
args = parser.parse_args()
ipv4_str = None
path = None
if args.file:
print(f'Using config file: {args.file}')
if os.path.exists(args.file):
with open(args.file, 'r') as file:
for line in file:
stripped_line = line.strip()
if stripped_line.startswith('#'):
continue
parts = stripped_line.partition(" ")
part0 = parts[0]
part2 = parts[2]
key = part0.strip()
value = part2.strip()
if key == "--path":
path = value
elif key == "--ipv4":
ipv4_str = value
else:
print(f'Unknown key "{key}" in config file {args.file}')
raise SystemExit(1)
if args.ipv4 == "use gethostbyname":
if (ipv4_str is None):
ipv4_str = socket.gethostbyname(socket.gethostname())
else:
ipv4_str = args.ipv4
print(f'AirPlay Service-Discovery Bluetooth LE beacon: using BLE file {args.path}')
print(f'(Press Ctrl+C to exit)')
main(args.path, ipv4_str)

View File

@@ -31,6 +31,15 @@ if ( ( UNIX AND NOT APPLE ) OR USE_X11 )
endif()
endif()
if( UNIX AND NOT APPLE )
find_package(PkgConfig REQUIRED)
pkg_check_modules(DBUS dbus-1>=1.4.12)
if (DBUS_FOUND )
add_definitions(-DDBUS )
include_directories(${DBUS_INCLUDE_DIRS})
endif()
endif()
if( UNIX AND NOT APPLE )
add_definitions( -DSUPPRESS_AVAHI_COMPAT_WARNING )
# convert AirPlay colormap 1:3:7:1 to sRGB (1:1:7:1), needed on Linux and BSD
@@ -54,6 +63,11 @@ target_link_libraries( uxplay
renderers
airplay
)
if (DBUS_FOUND)
target_link_libraries( uxplay
${DBUS_LIBRARIES}
)
endif()
install( TARGETS uxplay RUNTIME DESTINATION bin )
install( FILES uxplay.1 DESTINATION ${CMAKE_INSTALL_MANDIR}/man1 )
@@ -61,6 +75,22 @@ install( FILES README.md README.txt README.html LICENSE DESTINATION ${CMAKE_INST
install( FILES lib/llhttp/LICENSE-MIT DESTINATION ${CMAKE_INSTALL_DOCDIR}/llhttp )
install( FILES uxplay.service DESTINATION ${CMAKE_INSTALL_DOCDIR}/systemd )
if (DBUS_FOUND)
install( FILES Bluetooth_LE_beacon/dbus/uxplay-beacon.py
DESTINATION bin
PERMISSIONS OWNER_EXECUTE OWNER_WRITE OWNER_READ GROUP_EXECUTE GROUP_READ WORLD_EXECUTE WORLD_READ)
install( FILES Bluetooth_LE_beacon/dbus/uxplay-beacon.1 DESTINATION ${CMAKE_INSTALL_MANDIR}/man1 )
endif()
if (WIN32)
install( FILES Bluetooth_LE_beacon/winrt/uxplay-beacon.py
DESTINATION bin
PERMISSIONS OWNER_EXECUTE OWNER_WRITE OWNER_READ GROUP_EXECUTE GROUP_READ WORLD_EXECUTE WORLD_READ)
install( FILES Bluetooth_LE_beacon/winrt/uxplay-beacon.1 DESTINATION ${CMAKE_INSTALL_MANDIR}/man1 )
endif()
# uninstall target
if(NOT TARGET uninstall)
configure_file(

View File

@@ -8,6 +8,38 @@ developed at the GitHub site <a href="https://github.com/FDH2/UxPlay"
class="uri">https://github.com/FDH2/UxPlay</a> (where ALL user issues
should be posted, and latest versions can be found).</strong></h3>
<ul>
<li><p><strong>NEW on github</strong>: Support for <strong>service
discovery using a Bluetooth LE “beacon”</strong> for both Linux/*BSD and
Windows (as an alternative to Bonjour/Rendezvous DNS-SD service
discovery). The user must set up a Bluetooth LE “beacon”, (a USB 4.0 or
later “dongle” can be used). See instructions below. The beacon runs
independently of UxPlay and regularly broadcasts a Bluetooth LE (“Low
Energy”) 46 byte packet informing nearby iOS/macOS devices of the local
IPv4 network address of the UxPlay server, and which TCP port to contact
UxPlay on. Two versions of a Python script (Python &gt;=3.6)
“uxplay-beacon.py”, (one for Linux/*BSD using BlueZ
LEAdvertisingManager1 with DBus, and one for Windows using
winrt/BluetoothLEAdvertisementPublisher) are ready for users to run: the
appropriate version will be installed when UxPlay is built. They
independently run Service-Discovery beacons that iOS devices respond to.
Instructions are <a href="#bluetooth-le-beacon-setup">given
below</a>.</p></li>
<li><p><strong>NEW on github</strong>: option
<code>-vrtp &lt;rest-of-pipeline&gt;</code> bypasses rendering by
UxPlay, and instead transmits rtp packets of decrypted h264 or h265
video to an external renderer (e.g. OBS Studio) at an address specified
in <code>rest-of-pipeline</code>. (Note: this is video only, an option
“-rtp” which muxes audio and video into a mpeg4 container still needs to
be created: Pull Requests welcomed).</p></li>
<li><p><strong>NEW on github</strong>: (for Linux/*BSD Desktop
Environments using D-Bus). New option <code>-scrsv &lt;n&gt;</code>
provides screensaver inhibition (e.g., to prevent screensaver function
while watching mirrored videos without keyboard or mouse activity): n =
0 (off) n=1 (on during video activity) n=2 (always on while UxPlay is
running). Tested on Gnome/KDE/Cinnamon/Mate/Xfce 4: may need adjustment
for other Desktop Environments (please report). (watch output of
<code>dbus-monitor</code> to verify that inhibition is working).
<em>Might not work on Wayland</em>.</p></li>
<li><p><strong>NEW on github</strong>: option -ca (with no filename
given) will now render Apple Music cover art (in audio-only mode) inside
UxPlay. (-ca <code>&lt;filename&gt;</code> will continue to export cover
@@ -125,6 +157,10 @@ you may wish to add “as pipewiresink” or “vs waylandsink” as defaults to
the file. <em>(Output from terminal commands “ps waux | grep pulse” or
“pactl info” will contain “pipewire” if your Linux/BSD system uses
it).</em></p></li>
<li><p>For Linux/*BSD systems using D-Bus, the option
<code>-scrsv 1</code> inhibits the screensaver while there is video
activity on UxPlay (<code>-scrsv 2</code> inhibits it whenever UxPlay is
running).</p></li>
<li><p>For Linux systems using systemd, there is a
<strong>systemd</strong> service file <strong>uxplay.service</strong>
found in the UxPlay top directory of the distribution, and also
@@ -204,8 +240,10 @@ necessary, it is not necessary that the local network also be of the
“.local” mDNS-based type). On Linux and BSD Unix servers, this is
usually provided by <a href="https://www.avahi.org">Avahi</a>, through
the avahi-daemon service, and is included in most Linux distributions
(this service can also be provided by macOS, iOS or Windows
servers).</p>
(this service can also be provided by macOS, iOS or Windows servers).
There is now an alternative Service discovery method, using a Bluetooth
LE “beacon” See below for <a
href="#bluetooth-le-beacon-setup">instructions</a>.</p>
<p>Connections to the UxPlay server by iOS/MacOS clients can be
initiated both in <strong>AirPlay Mirror</strong> mode (which streams
lossily-compressed AAC audio while mirroring the client screen, or in
@@ -536,7 +574,8 @@ comments and ignored.</p>
you can specify fullscreen mode with the <code>-fs</code> option, or
toggle into and out of fullscreen mode with F11 or (held-down left
Alt)+Enter keys. Use Ctrl-C (or close the window) to terminate it when
done. If the UxPlay server is not seen by the iOS clients drop-down
done.</p>
<p>If the UxPlay server is not seen by the iOS clients drop-down
“Screen Mirroring” panel, check that your DNS-SD server (usually
avahi-daemon) is running: do this in a terminal window with
<code>systemctl status avahi-daemon</code>. If this shows the
@@ -551,6 +590,9 @@ opened: <strong>if a firewall is active, also open UDP port 5353 (for
mDNS queries) needed by Avahi</strong>. See <a
href="#troubleshooting">Troubleshooting</a> below for help with this or
other problems.</p>
<p>Note that there is now an alternative Service Discovery method using
a Bluetooth LE beacon. See the instructions on <a
href="#bluetooth-le-beacon-setup">Bluetooth beacon setup</a>.</p>
<ul>
<li><p>Unlike an Apple TV, the UxPlay server does not by default require
clients to initially “pair” with it using a pin code displayed by the
@@ -986,10 +1028,11 @@ used.</p>
<code>&lt;videosink&gt;</code> are <code>d3d12videosink</code>,
<code>d3d11videosink</code>, <code>d3dvideosink</code>,
<code>glimagesink</code>, <code>gtksink</code>,
<code>autovideosink</code>. If you do not specify the videosink, the
d3d11videosink will be used (users have reported segfaults of the newer
d3d12 videodecoder on certain older Nvidia cards when the image
resolution changes: d3d11 will used by default until this is fixed).</p>
<code>autovideosink</code>. <em>There have been reports of segfaults of
the newer d3d12 videodecoder on certain older Nvidia cards when the
image resolution changes, e.g., when the iOS client is rotated between
portrait and landcape modes: this was a GStreamer issue that is
apparently now fixed (a workaround is to use d3d11).</em></p>
<ul>
<li>With Direct3D 11.0 or greater, various options can be set using
e.g. <code>-vs "d3d11videosink &lt;options&gt;"</code> (see the
@@ -1001,6 +1044,22 @@ is added.</li>
<p>The executable uxplay.exe can also be run without the MSYS2
environment, in the Windows Terminal, with
<code>C:\msys64\ucrt64\bin\uxplay</code>.</p>
<p>There is a new modernized Windows Terminal application available from
Microsoft that provides various terminals, and can be customized to also
provide the MSYS2 terminals. See https://www.msys2.org/docs/terminals/
(to make those instructions clearer: in the dropdown “Settings” menu,
there is a second “settings” icon in the lower left corner: click on
that to edit the settings.json file as described).</p>
<p>The server name (-n <name> option) can be given in internationalized
UTF-8 encoding: To enter UTF-8 characters in the MSYS2 or other Windows
terminals, use the numerical keypad with “Num Lock” on: while holding
down the “Alt” key, type “+” on the keypad, followed by the UTF-8 hex
code for the character (using the keypad for numbers), then release the
“Alt” key. (The UTF-8 hex codes have 4 hex digits: for example, the
“copyright” symbol has hex code 00a9.) This method must be activated in
the Windows Registry: using regedit, find the Registry section
HKEY_Current_User/Control Panel/Input Method”, and add a new Key
“EnableHexNumpad” with value “1”, then reboot the computer.</p>
<h1 id="usage">Usage</h1>
<p>Options:</p>
<ul>
@@ -1018,7 +1077,8 @@ startup file location: this overrides <code>$UXPLAYRC</code>,
server_name@_hostname_ will be the name that appears offering AirPlay
services to your iPad, iPhone etc, where <em>hostname</em> is the name
of the server running uxplay. This will also now be the name shown above
the mirror display (X11) window.</p>
the mirror display (X11) window. <strong>Internationalized server names
encoded as UTF-8 are accepted.</strong></p>
<p><strong>-nh</strong> Do not append “<span class="citation"
data-cites="_hostname_">@_hostname_</span>” at the end of the AirPlay
server name.</p>
@@ -1042,6 +1102,16 @@ default: 3) allows selection of the version of GStreamers "playbin"
video player to use for playing HLS video. <em>(Playbin v3 is the
recommended player, but if some videos fail to play, you can try with
version 2.)</em></p>
<p><strong>-scrsv n</strong>. (since 1.73) (So far, only implemented on
Linux/*BSD systems using D-Bus). Inhibit the screensaver in the absence
of keyboard input (e.g., while watching video), using the
org.freedesktop.ScreenSaver D-Bus service: n = 0: (off) n= 1 (on during
video activity) n=2 (always on). <em>Note: to verify this feature is
working, you can use <code>dbus-monitor</code> to view events on the
D-Bus; depending on the Desktop Environment, commands like
<code>gnome-session-inhibit -l</code>,
<code>xfce4-screensaver-commannd -q</code>, etc., should list UxPlay
when it is inhibiting the screensaver.</em></p>
<p><strong>-pin [nnnn]</strong>: (since v1.67) use Apple-style
(one-time) “pin” authentication when a new client connects for the first
time: a four-digit pin code is displayed on the terminal, and the client
@@ -1069,16 +1139,23 @@ options -restrict, -block, -allow for more ways to control client
access). <em>(Add a line “reg” in the startup file if you wish to use
this feature.)</em></p>
<p><strong>-pw</strong> [<em>pwd</em>]. (since 1.72). As an alternative
to -pin, client access can be controlled with a password set when uxplay
starts (set it in the .uxplay startup file, where it is stored as
cleartext.) All users must then know this password. This uses HTTP md5
Digest authentication, which is now regarded as providing weak security,
but it is only used to validate the uxplay password, and no user
credentials are exposed. If <em>pwd</em> is <strong>not</strong>
specified, a random 4-digit pin code is displayed, and must be entered
on the client at <strong>each</strong> new connection. <em>Note: -pin
and -pw are alternatives: if both are specified at startup, the earlier
of these two options is discarded.</em></p>
to -pin, client access can be controlled with a password. If a password
<em>pwd</em> (of length at least six characters) is set when uxplay
starts (usually set in the startup file, where it is stored as
cleartext), all users must know this password to connect to UxPlay (the
client prompts for it). This method uses HTTP md5 Digest authentication,
which is now regarded as providing weak security, but it is only used to
validate the uxplay password, and no user credentials are exposed. After
a successful authentication, the client stores the password, and will
use it initially for future authentications without prompting, so long
as the UxPlay deviceID has not changed (this initial authentication will
fail if the UxPlay password has changed). If <em>pwd</em> is
<strong>not</strong> specified with the -pw option when UxPlay starts, a
new random 4-digit pin code is generated and displayed on the UxPlay
terminal for <strong>each</strong> new connection, and must be entered
on the client (there are three chances to enter it, before it is
changed). <em>Note: -pin and -pw are alternatives: if both are specified
at startup, the earlier of these two options is discarded.</em></p>
<p><strong>-vsync [x]</strong> (In Mirror mode:) this option
(<strong>now the default</strong>) uses timestamps to synchronize audio
with video on the server, with an optional audio delay in (decimal)
@@ -1200,6 +1277,12 @@ display video), and only used to render audio, which will be AAC
lossily-compressed audio in mirror mode with unrendered video, and
superior-quality ALAC Apple Lossless audio in Airplay audio-only
mode.</p>
<p><strong>-vrtp <em>pipeline</em></strong>: forward rtp packets of
decrypted video to somewhere else, without rendering. Uses rtph264pay or
rtph265pay as appropriate: <em>pipeline</em> should start with any
rtph26xpay options (such as config_interval= or aggregate-mode =),
followed by a sending method: <em>e.g.</em>,
<code>"config-interval=1 ! udpsink host=127.0.0.1 port=5000</code>“.</p>
<p><strong>-v4l2</strong> Video settings for hardware h264 video
decoding in the GPU by Video4Linux2. Equivalent to
<code>-vd v4l2h264dec -vc v4l2convert</code>.</p>
@@ -1374,6 +1457,13 @@ to a file to <em>n</em> or less. To change the name <em>audiodump</em>,
use -admp [n] <em>filename</em>. <em>Note that (unlike dumped video) the
dumped audio is currently only useful for debugging, as it is not
containerized to make it playable with standard audio players.</em></p>
<p><strong>-ble [<em>filename</em>]</strong>. Enable Bluetooth beacon
Service Discovery. The port, PID and process name of the UxPlay process
is recorded by default in <code>~/.uxplay.ble</code> : (this file is
created when UxPlay starts and deleted when it stops.) Optionally the
file <em>filename</em>, which must be the full path to a writeable file
can instead be used. <strong>See below for beacon setup
instructions.</strong></p>
<p><strong>-d [n]</strong> Enable debug output; optional argument n=1
suppresses audio/video packet data in debug output. Note: this does not
show GStreamer error or debug messages. To see GStreamer error and
@@ -1381,6 +1471,94 @@ warning messages, set the environment variable GST_DEBUG with “export
GST_DEBUG=2” before running uxplay. To see GStreamer information
messages, set GST_DEBUG=4; for DEBUG messages, GST_DEBUG=5; increase
this to see even more of the GStreamer inner workings.</p>
<h1 id="bluetooth-le-beacon-setup">Bluetooth LE beacon setup</h1>
<p>The python&gt;=3.6 script for running a Bluetooth-LE Service
Discovery beacon is uxplay-beacon.py. It comes in two versions, one (for
Linux and *BSD) is only installed on systems which support DBUS, and
another only for Windows 10/11. Bluetooth &gt;= 4.0 hardware on the host
computer is required: a cheap USB bluetooth dongle can be used.</p>
<p>On Linux/*BSD, Bluetooth support (BlueZ) must be installed (on
Debian-based systems: <code>sudo apt install bluez bluez-tools</code>;
recent Ubuntu releases provide bluez as a snap package). In addition to
standard Python3 libraries, you may need to install the gi, dbus, and
psutil Python libraries used by uxplay-beacon.py. On Debian-based
systems:</p>
<pre><code>sudo apt install python3-gi python3-dbus python3-psutil</code></pre>
<p>For Windows support on MSYS2 UCRT systems, use pacman -S to install
<code>mingw-w64-ucrt-x86_64-python</code>,
<code>*-python-gobject</code>, <code>*-python-psutil</code>, and
<code>*-python-pip</code>. Then install winrt bindings
<code>pip install winrt-Windows.Foundation.Collections</code>,
<code>winrt-Windows.Devices.Bluetooth.Advertisement</code> and
<code>winrt-Windows.Storage.Streams</code>.</p>
<p>If uxplay will be run with option “<code>uxplay -ble</code>” (so it
writes data for the Bluetooth beacon in the default BLE data file
<code>~/.uxplay.ble</code>), just run <code>uxplay-beacon.py</code> in a
separate terminal. The python script will start Bluetooth LE
Service-Discovery advertising when it detects that UxPlay is running by
checking if the BLE data file exists, and stop when it no longer detects
a running UxPlay plus this file (it will restart advertising if UxPlay
later reappears). The script will remain active until stopped with
Ctrl+C in its terminal window (or its terminal window is closed).</p>
<p>The beacon script can be more finely controlled using certain
options: these can be given on the command line, or read from a
configuration file <code>~/.uxplay.beacon</code>, if it exists.
Configuration file entries are like the command line forms, one per line
(e.g., <code>--ipv4 192.168.1.100</code>). Lines commented out with an
initial <code>#</code> are ignored. Command line options override the
configuration file options. Get help with <code>man uxplay-beacon</code>
or <code>uxplay-beacon.py --help</code>. Options are</p>
<ul>
<li><p><code>--file &lt;config file&gt;</code> read beacon options from
<code>&lt;config file&gt;</code> instead of
<code>~/.uxplay.beacon</code>.</p></li>
<li><p><code>--ipv4 &lt;ipv4 address&gt;</code>. This option can be
used to specify the ipv4 address at which the UxPlay server should be
contacted by the client. If it is not given, an address will be obtained
automatically using <code>gethostbyname</code>. Only ipv4 addresses are
supported.</p></li>
<li><p><code>--path &lt;BLE data file&gt;</code>. This overrides the
default choice of BLE data file (<code>~/.uxplay.ble</code>) that is
monitored by the beacon script. This also requires that uxplay is run
with option “<code>uxplay -ble &lt;BLE data file&gt;</code>”.</p></li>
</ul>
<p>The BlueZ/Dbus version has thee more options not offered by the
Windows version:</p>
<ul>
<li><p><code>--AdvMin x</code>, <code>--AdvMax y</code>. These controls
the interval between BLE advertisement broadcasts. This interval is in
the range [x, y], given in units of msecs. Allowed ranges are 100 &lt;=
x &lt;= y &lt;= 10240. If AdvMin=AdvMax, the interval is fixed: if
AdvMin &lt; AdvMax it is chosen flexibly in this range to avoid
interfering with other tasks the Bluetooth device is carrying out. The
default values are AdvMin = AdvMax = 100. The advertisement is broadcast
on all three Bluetooth LE advertising channels: 37,38,39.</p></li>
<li><p><code>--index x</code> (default x = 0, x &gt;= 0). This should be
used to distinguish between multiple simultaneous instances of
uxplay-beacon.py that are running to support multiple instances of
UxPlay. Each instance must have its own BLE Data file (just as each
instance of UxPlay must also have its own MAC address and ports).
<em>Note: running multiple beacons simultaneously on the same host has
not been tested.</em></p></li>
</ul>
<p>If you wish to test Bluetooth LE Service Discovery on Linux/*BSD, you
can disable DNS_SD Service discovery by the avahi-daemon with</p>
<pre><code>$ sudo systemctl mask avahi-daemon.socket
$ sudo systemctl stop avahi-daemon</code></pre>
<p>To restore DNS_SD Service discovery, replace “mask” by “unmask”, and
“stop” by “start”.</p>
<p>On Windows, the Bonjour Service is controlled using <strong>Services
Management</strong>: press “Windows + R” to open the Run dialog, run
<code>services.msc</code>, and click on <strong>Bonjour Service</strong>
in the alphabetic list. This will show links for it to be stopped and
restarted.</p>
<p>For more information, see the <a
href="https://github.com/FDH2/UxPlay/wiki/Bluetooth_LE_beacon">wiki
page</a></p>
<ul>
<li><strong>Note that Bluetooth LE AirPlay Service Discovery only
supports broadcast of IPv4 addresses</strong>.</li>
</ul>
<h1 id="troubleshooting">Troubleshooting</h1>
<p>Note: <code>uxplay</code> is run from a terminal command line, and
informational messages are written to the terminal.</p>
@@ -1710,8 +1888,12 @@ an AppleTV6,2 with sourceVersion 380.20.1 (an AppleTV 4K 1st gen,
introduced 2017, running tvOS 12.2.1), so it does not seem to matter
what version UxPlay claims to be.</p>
<h1 id="changelog">Changelog</h1>
<p>xxxx 2025-07-07 Render Audio cover-art inside UxPlay with -ca option
(no file specified).</p>
<p>xxxx 2025-09-25 Render Audio cover-art inside UxPlay with -ca option
(no file specified). (D-Bus based) option -scrsv <n> to inhibit
screensaver while UxPlay is running (Linux/*BSD only). Add support for
Service Discovery using a Bluetooth LE beacon. Add -vrtp option for
forwarding decrypted h264/5 video to an external renderer (e.g., OBS
Studio). Check that option input strings have valid UTF-8 encoding.</p>
<p>1.72.2 2025-07-07 Fix bug (typo) in DNS_SD advertisement introduced
with -pw option. Update llhttp to v 9.3.0</p>
<p>1.72.1 2025-06-06 minor update: fix regression in -reg option; add

190
README.md
View File

@@ -2,6 +2,26 @@
### **Now developed at the GitHub site <https://github.com/FDH2/UxPlay> (where ALL user issues should be posted, and latest versions can be found).**
- **NEW on github**: Support for **service discovery using a Bluetooth LE "beacon"** for both Linux/\*BSD and Windows (as an alternative to Bonjour/Rendezvous DNS-SD
service discovery). The user must set up a Bluetooth LE "beacon", (a USB 4.0 or later "dongle" can be used). See instructions
below. The beacon runs independently of UxPlay and regularly broadcasts a Bluetooth LE ("Low Energy") 46 byte packet informing nearby iOS/macOS devices of
the local IPv4 network address of the UxPlay server, and which TCP port to contact UxPlay on. Two versions of a Python script (Python >=3.6) "uxplay-beacon.py",
(one for Linux/*BSD using BlueZ LEAdvertisingManager1 with DBus, and one for Windows using winrt/BluetoothLEAdvertisementPublisher) are ready for users to
run: the appropriate version will be installed when UxPlay is built. They independently run Service-Discovery beacons that iOS devices respond to.
Instructions are [given below](#bluetooth-le-beacon-setup).
- **NEW on github**: option `-vrtp <rest-of-pipeline>` bypasses rendering by UxPlay, and instead
transmits rtp packets of decrypted h264 or h265 video to
an external renderer (e.g. OBS Studio) at an address specified in `rest-of-pipeline`.
(Note: this is video only, an option "-rtp" which muxes audio and video into a mpeg4 container still needs to be created:
Pull Requests welcomed).
- **NEW on github**: (for Linux/*BSD Desktop Environments using D-Bus). New option `-scrsv <n>` provides screensaver inhibition (e.g., to
prevent screensaver function while watching mirrored videos without keyboard or mouse
activity): n = 0 (off) n=1 (on during video activity) n=2 (always on while UxPlay is running).
Tested on Gnome/KDE/Cinnamon/Mate/Xfce 4: may need adjustment for other Desktop Environments (please report).
(watch output of `dbus-monitor` to verify that inhibition is working). _Might not work on Wayland_.
- **NEW on github**: option -ca (with no filename given) will now render
Apple Music cover art (in audio-only mode) inside
UxPlay. (-ca `<filename>` will continue to export cover art for
@@ -114,6 +134,9 @@ status](https://repology.org/badge/vertical-allrepos/uxplay.svg)](https://repolo
from terminal commands "ps waux \| grep pulse" or "pactl info" will
contain "pipewire" if your Linux/BSD system uses it).*
- For Linux/*BSD systems using D-Bus, the option `-scrsv 1` inhibits the screensaver while
there is video activity on UxPlay (`-scrsv 2` inhibits it whenever UxPlay is running).
- For Linux systems using systemd, there is a **systemd** service file **uxplay.service**
found in the UxPlay top directory of the distribution, and also installed
in `<DOCDIR>/uxplay/systemd/` (where DOCDIR is usually ``/usr/local/share/doc``), that allows users to start
@@ -192,7 +215,10 @@ necessary that the local network also be of the ".local" mDNS-based
type). On Linux and BSD Unix servers, this is usually provided by
[Avahi](https://www.avahi.org), through the avahi-daemon service, and is
included in most Linux distributions (this service can also be provided
by macOS, iOS or Windows servers).
by macOS, iOS or Windows servers). There is now an alternative Service
discovery method, using a Bluetooth LE "beacon" See below
for [instructions](#bluetooth-le-beacon-setup).
Connections to the UxPlay server by iOS/MacOS clients can be initiated
both in **AirPlay Mirror** mode (which streams lossily-compressed AAC
@@ -523,7 +549,9 @@ as comments and ignored.
**Run uxplay in a terminal window**. On some systems, you can specify
fullscreen mode with the `-fs` option, or toggle into and out of
fullscreen mode with F11 or (held-down left Alt)+Enter keys. Use Ctrl-C
(or close the window) to terminate it when done. If the UxPlay server is
(or close the window) to terminate it when done.
If the UxPlay server is
not seen by the iOS client's drop-down "Screen Mirroring" panel, check
that your DNS-SD server (usually avahi-daemon) is running: do this in a
terminal window with `systemctl status avahi-daemon`. If this shows the
@@ -538,6 +566,10 @@ opened: **if a firewall is active, also open UDP port 5353 (for mDNS
queries) needed by Avahi**. See [Troubleshooting](#troubleshooting)
below for help with this or other problems.
Note that there is now an
alternative Service Discovery method using a Bluetooth LE beacon.
See the instructions on [Bluetooth beacon setup](#bluetooth-le-beacon-setup).
- Unlike an Apple TV, the UxPlay server does not by default require
clients to initially "pair" with it using a pin code displayed by
the server (after which the client "trusts" the server, and does not
@@ -978,10 +1010,11 @@ like `\{0.0.0.00000000\}.\{98e35b2b-8eba-412e-b840-fd2c2492cf44\}`. If
If you wish to specify the videosink using the `-vs <videosink>` option,
some choices for `<videosink>` are `d3d12videosink`, ``d3d11videosink``, ```d3dvideosink```,
`glimagesink`, ``gtksink``, ```autovideosink```. If you do not specify the videosink,
the d3d11videosink will be used (users have reported segfaults of the newer d3d12 videodecoder
on certain older Nvidia cards when the image resolution changes:
d3d11 will used by default until this is fixed).
`glimagesink`, ``gtksink``, ```autovideosink```. _There have been reports of
segfaults of the newer d3d12 videodecoder
on certain older Nvidia cards when the image resolution changes, e.g., when the iOS client
is rotated between portrait and landcape modes: this was a GStreamer issue
that is apparently now fixed (a workaround is to use d3d11)._
- With Direct3D 11.0 or greater, various options can be set
using e.g. `-vs "d3d11videosink <options>"` (see the gstreamer videosink
@@ -992,6 +1025,22 @@ d3d11 will used by default until this is fixed).
The executable uxplay.exe can also be run without the MSYS2 environment,
in the Windows Terminal, with `C:\msys64\ucrt64\bin\uxplay`.
There is a new modernized Windows Terminal application available from Microsoft that
provides various terminals, and can be customized to also provide the MSYS2 terminals.
See https://www.msys2.org/docs/terminals/ (to make those instructions clearer:
in the dropdown "Settings" menu, there is a second "settings" icon in the lower left corner:
click on that to edit the settings.json file as described).
The server name (-n <name> option) can be given in internationalized UTF-8 encoding:
To enter UTF-8 characters in the MSYS2 or other Windows terminals, use the numerical keypad
with "Num Lock" on: while holding down the "Alt" key, type "+" on the keypad, followed
by the UTF-8 hex code for the character (using the keypad for numbers), then release the "Alt" key.
(The UTF-8 hex codes have 4 hex digits: for example, the "copyright" symbol has hex code 00a9.)
This method must be activated in the Windows Registry: using
regedit, find the Registry section 'HKEY_Current_User/Control Panel/Input Method",
and add a new Key "EnableHexNumpad" with value "1", then reboot the computer.
# Usage
Options:
@@ -1010,6 +1059,7 @@ overrides `$UXPLAYRC`, ``~/.uxplayrc``, etc.
the name that appears offering AirPlay services to your iPad, iPhone
etc, where *hostname* is the name of the server running uxplay. This
will also now be the name shown above the mirror display (X11) window.
**Internationalized server names encoded as UTF-8 are accepted.**
**-nh** Do not append "@_hostname_" at the end of the AirPlay server
name.
@@ -1036,6 +1086,18 @@ allows selection of the version of GStreamer's
is the recommended player, but if some videos fail to play, you can try
with version 2.)_
**-scrsv n**. (since 1.73) (So far, only implemented
on Linux/*BSD systems using D-Bus). Inhibit the screensaver in the
absence of keyboard input (e.g., while watching video), using the
org.freedesktop.ScreenSaver D-Bus service:
n = 0: (off) n= 1 (on during video activity) n=2 (always on).
_Note: to verify this feature is working, you can use `dbus-monitor`
to view events on the D-Bus; depending on the Desktop Environment,
commands like
`gnome-session-inhibit -l`, ``xfce4-screensaver-commannd -q``, etc.,
should list UxPlay when it is inhibiting
the screensaver._
**-pin \[nnnn\]**: (since v1.67) use Apple-style (one-time) "pin"
authentication when a new client connects for the first time: a
four-digit pin code is displayed on the terminal, and the client screen
@@ -1064,13 +1126,23 @@ deregisters the corresponding client (see options -restrict, -block,
the startup file if you wish to use this feature.)*
**-pw** [*pwd*]. (since 1.72). As an alternative to -pin, client access
can be controlled with a password set when uxplay starts (set it in
the .uxplay startup file, where it is stored as cleartext.) All users must
then know this password. This uses HTTP md5 Digest authentication,
can be controlled with a password. If a password *pwd* (of length at least
six characters) is set when uxplay
starts (usually set in the startup file, where it is stored as
cleartext), all users must know this password to connect to UxPlay
(the client prompts for it).
This method uses HTTP md5 Digest authentication,
which is now regarded as providing weak security, but it is only used to
validate the uxplay password, and no user credentials are exposed.
If *pwd* is **not** specified, a random 4-digit pin code is displayed, and must
be entered on the client at **each** new connection.
After a successful authentication, the client stores the password, and will use
it initially for future authentications without prompting, so long as
the UxPlay deviceID has not changed (this initial authentication will fail
if the UxPlay password has changed).
If *pwd* is **not** specified with the -pw option when UxPlay starts, a
new random 4-digit pin code is generated and displayed on the UxPlay terminal
for **each** new connection, and must
be entered on the client (there are three
chances to enter it, before it is changed).
_Note: -pin and -pw are alternatives: if both are specified at startup, the
earlier of these two options is discarded._
@@ -1203,6 +1275,11 @@ video), and only used to render audio, which will be AAC
lossily-compressed audio in mirror mode with unrendered video, and
superior-quality ALAC Apple Lossless audio in Airplay audio-only mode.
**-vrtp *pipeline***: forward rtp packets of decrypted video to somewhere else, without rendering.
Uses rtph264pay or rtph265pay as appropriate: *pipeline* should start with any
rtph26xpay options (such as config_interval= or aggregate-mode =), followed by
a sending method: *e.g.*, `"config-interval=1 ! udpsink host=127.0.0.1 port=5000`".
**-v4l2** Video settings for hardware h264 video decoding in the GPU by
Video4Linux2. Equivalent to `-vd v4l2h264dec -vc v4l2convert`.
@@ -1392,6 +1469,15 @@ that (unlike dumped video) the dumped audio is currently only useful for
debugging, as it is not containerized to make it playable with standard
audio players.*
**-ble [*filename*]**. Enable Bluetooth beacon Service Discovery.
The port, PID and process name of the UxPlay process is recorded by default in
`~/.uxplay.ble` : (this file is created
when UxPlay starts and deleted when it stops.)
Optionally the file
*filename*, which must be the full path to a writeable file can instead be used.
__See below for beacon setup
instructions.__
**-d \[n\]** Enable debug output; optional argument n=1 suppresses audio/video
packet data in debug output.
Note: this does not show GStreamer error or
@@ -1401,8 +1487,80 @@ uxplay. To see GStreamer information messages, set GST_DEBUG=4; for
DEBUG messages, GST_DEBUG=5; increase this to see even more of the
GStreamer inner workings.
# Troubleshooting
# Bluetooth LE beacon setup
The python>=3.6 script for running a Bluetooth-LE Service Discovery beacon is uxplay-beacon.py.
It comes in two versions, one (for Linux and *BSD) is only installed on systems which
support DBUS, and another only for Windows 10/11. Bluetooth >= 4.0 hardware on the host computer is required: a cheap USB bluetooth dongle
can be used.
On Linux/*BSD,
Bluetooth support (BlueZ) must be installed (on Debian-based systems: `sudo apt install bluez bluez-tools`;
recent Ubuntu releases provide bluez as a snap package).
In addition to standard Python3 libraries, you may need to install the gi, dbus, and psutil Python libraries used by
uxplay-beacon.py. On Debian-based systems:
```
sudo apt install python3-gi python3-dbus python3-psutil
```
For Windows support on MSYS2 UCRT systems, use pacman -S to
install `mingw-w64-ucrt-x86_64-python`, ``*-python-gobject``,
`*-python-psutil`, and ``*-python-pip``. Then install winrt
bindings `pip install winrt-Windows.Foundation.Collections`,
``winrt-Windows.Devices.Bluetooth.Advertisement`` and
``winrt-Windows.Storage.Streams``.
If uxplay will be run with option "`uxplay -ble`" (so it writes data for the Bluetooth beacon in the default BLE data file
`~/.uxplay.ble`), just run ``uxplay-beacon.py`` in a separate terminal. The python script will start
Bluetooth LE Service-Discovery advertising when it detects that UxPlay is running by checking if the BLE data file exists, and stop when it no longer detects
a running UxPlay plus this file (it will restart advertising if UxPlay later reappears). The script will remain active until stopped with Ctrl+C in its
terminal window (or its terminal window is closed).
The beacon script can be more finely controlled using certain options: these can be given on the command line, or read from
a configuration file `~/.uxplay.beacon`, if it exists. Configuration file entries are like the command line forms, one per line (e.g.,
`--ipv4 192.168.1.100`). Lines commented out with an initial ``#`` are ignored. Command line options override the configuration file
options. Get help with `man uxplay-beacon` or ``uxplay-beacon.py --help``. Options are
* `--file <config file>` read beacon options from ``<config file>`` instead of
`~/.uxplay.beacon`.
* `--ipv4 <ipv4 address>`. This option can be used to specify the ipv4 address at which the UxPlay server should be contacted by the client. If
it is not given, an address will be obtained automatically using `gethostbyname`. Only ipv4 addresses are supported.
* `--path <BLE data file>`. This overrides the default choice of BLE data file (``~/.uxplay.ble``) that is monitored by the beacon script. This also requires
that uxplay is run with option "`uxplay -ble <BLE data file>`".
The BlueZ/Dbus version has thee more options not offered by the Windows version:
* `--AdvMin x`, ``--AdvMax y``. These controls the interval between BLE advertisement broadcasts. This interval is in the range
[x, y], given in units of msecs. Allowed ranges are 100 <= x <= y <= 10240. If AdvMin=AdvMax, the interval is fixed: if AdvMin < AdvMax
it is chosen flexibly in this range to avoid interfering with other tasks the Bluetooth device is carrying out. The default values are
AdvMin = AdvMax = 100. The advertisement is broadcast on all three Bluetooth LE advertising channels: 37,38,39.
* `--index x` (default x = 0, x >= 0). This should be used to distinguish between multiple simultaneous instances of uxplay-beacon.py that are running to support multiple
instances of UxPlay. Each instance must have its own BLE Data file (just as each instance of UxPlay must also have its own MAC address and ports). _Note:
running multiple beacons simultaneously on the same host has not been tested._
If you wish to test Bluetooth LE Service Discovery on Linux/*BSD, you can disable DNS_SD Service discovery by the avahi-daemon with
```
$ sudo systemctl mask avahi-daemon.socket
$ sudo systemctl stop avahi-daemon
```
To restore DNS_SD Service discovery, replace "mask" by "unmask", and "stop" by "start".
On Windows, the Bonjour Service is controlled using **Services Management**: press "Windows + R" to open the Run dialog,
run `services.msc`, and click on **Bonjour Service** in the alphabetic list. This
will show links for it to be stopped and restarted.
For more information, see the [wiki page](https://github.com/FDH2/UxPlay/wiki/Bluetooth_LE_beacon)
* **Note that Bluetooth LE AirPlay Service Discovery only supports
broadcast of IPv4 addresses**.
# Troubleshooting
Note: `uxplay` is run from a terminal command line, and informational
messages are written to the terminal.
@@ -1742,8 +1900,12 @@ introduced 2017, running tvOS 12.2.1), so it does not seem to matter
what version UxPlay claims to be.
# Changelog
xxxx 2025-07-07 Render Audio cover-art inside UxPlay with -ca option (no file
specified).
xxxx 2025-09-25 Render Audio cover-art inside UxPlay with -ca option (no file
specified). (D-Bus based) option -scrsv <n> to inhibit screensaver while UxPlay
is running (Linux/*BSD only). Add support for Service Discovery using a
Bluetooth LE beacon. Add -vrtp option for forwarding decrypted h264/5 video
to an external renderer (e.g., OBS Studio). Check that option input strings
have valid UTF-8 encoding.
1.72.2 2025-07-07 Fix bug (typo) in DNS_SD advertisement introduced with -pw
option. Update llhttp to v 9.3.0

View File

@@ -2,6 +2,39 @@
### **Now developed at the GitHub site <https://github.com/FDH2/UxPlay> (where ALL user issues should be posted, and latest versions can be found).**
- **NEW on github**: Support for **service discovery using a Bluetooth
LE "beacon"** for both Linux/\*BSD and Windows (as an alternative to
Bonjour/Rendezvous DNS-SD service discovery). The user must set up a
Bluetooth LE "beacon", (a USB 4.0 or later "dongle" can be used).
See instructions below. The beacon runs independently of UxPlay and
regularly broadcasts a Bluetooth LE ("Low Energy") 46 byte packet
informing nearby iOS/macOS devices of the local IPv4 network address
of the UxPlay server, and which TCP port to contact UxPlay on. Two
versions of a Python script (Python \>=3.6) "uxplay-beacon.py", (one
for Linux/\*BSD using BlueZ LEAdvertisingManager1 with DBus, and one
for Windows using winrt/BluetoothLEAdvertisementPublisher) are ready
for users to run: the appropriate version will be installed when
UxPlay is built. They independently run Service-Discovery beacons
that iOS devices respond to. Instructions are [given
below](#bluetooth-le-beacon-setup).
- **NEW on github**: option `-vrtp <rest-of-pipeline>` bypasses
rendering by UxPlay, and instead transmits rtp packets of decrypted
h264 or h265 video to an external renderer (e.g. OBS Studio) at an
address specified in `rest-of-pipeline`. (Note: this is video only,
an option "-rtp" which muxes audio and video into a mpeg4 container
still needs to be created: Pull Requests welcomed).
- **NEW on github**: (for Linux/\*BSD Desktop Environments using
D-Bus). New option `-scrsv <n>` provides screensaver inhibition
(e.g., to prevent screensaver function while watching mirrored
videos without keyboard or mouse activity): n = 0 (off) n=1 (on
during video activity) n=2 (always on while UxPlay is running).
Tested on Gnome/KDE/Cinnamon/Mate/Xfce 4: may need adjustment for
other Desktop Environments (please report). (watch output of
`dbus-monitor` to verify that inhibition is working). *Might not
work on Wayland*.
- **NEW on github**: option -ca (with no filename given) will now
render Apple Music cover art (in audio-only mode) inside UxPlay.
(-ca `<filename>` will continue to export cover art for display by
@@ -121,6 +154,10 @@ status](https://repology.org/badge/vertical-allrepos/uxplay.svg)](https://repolo
commands "ps waux \| grep pulse" or "pactl info" will contain
"pipewire" if your Linux/BSD system uses it).*
- For Linux/\*BSD systems using D-Bus, the option `-scrsv 1` inhibits
the screensaver while there is video activity on UxPlay (`-scrsv 2`
inhibits it whenever UxPlay is running).
- For Linux systems using systemd, there is a **systemd** service file
**uxplay.service** found in the UxPlay top directory of the
distribution, and also installed in `<DOCDIR>/uxplay/systemd/`
@@ -205,7 +242,9 @@ necessary that the local network also be of the ".local" mDNS-based
type). On Linux and BSD Unix servers, this is usually provided by
[Avahi](https://www.avahi.org), through the avahi-daemon service, and is
included in most Linux distributions (this service can also be provided
by macOS, iOS or Windows servers).
by macOS, iOS or Windows servers). There is now an alternative Service
discovery method, using a Bluetooth LE "beacon" See below for
[instructions](#bluetooth-le-beacon-setup).
Connections to the UxPlay server by iOS/MacOS clients can be initiated
both in **AirPlay Mirror** mode (which streams lossily-compressed AAC
@@ -536,11 +575,13 @@ as comments and ignored.
**Run uxplay in a terminal window**. On some systems, you can specify
fullscreen mode with the `-fs` option, or toggle into and out of
fullscreen mode with F11 or (held-down left Alt)+Enter keys. Use Ctrl-C
(or close the window) to terminate it when done. If the UxPlay server is
not seen by the iOS client's drop-down "Screen Mirroring" panel, check
that your DNS-SD server (usually avahi-daemon) is running: do this in a
terminal window with `systemctl status avahi-daemon`. If this shows the
avahi-daemon is not running, control it with
(or close the window) to terminate it when done.
If the UxPlay server is not seen by the iOS client's drop-down "Screen
Mirroring" panel, check that your DNS-SD server (usually avahi-daemon)
is running: do this in a terminal window with
`systemctl status avahi-daemon`. If this shows the avahi-daemon is not
running, control it with
`sudo systemctl [start,stop,enable,disable] avahi-daemon` (on
non-systemd systems, such as \*BSD, use
`sudo service avahi-daemon [status, start, stop, restart, ...]`). If
@@ -551,6 +592,10 @@ opened: **if a firewall is active, also open UDP port 5353 (for mDNS
queries) needed by Avahi**. See [Troubleshooting](#troubleshooting)
below for help with this or other problems.
Note that there is now an alternative Service Discovery method using a
Bluetooth LE beacon. See the instructions on [Bluetooth beacon
setup](#bluetooth-le-beacon-setup).
- Unlike an Apple TV, the UxPlay server does not by default require
clients to initially "pair" with it using a pin code displayed by
the server (after which the client "trusts" the server, and does not
@@ -997,11 +1042,12 @@ like `\{0.0.0.00000000\}.\{98e35b2b-8eba-412e-b840-fd2c2492cf44\}`. If
If you wish to specify the videosink using the `-vs <videosink>` option,
some choices for `<videosink>` are `d3d12videosink`, `d3d11videosink`,
`d3dvideosink`, `glimagesink`, `gtksink`, `autovideosink`. If you do not
specify the videosink, the d3d11videosink will be used (users have
reported segfaults of the newer d3d12 videodecoder on certain older
Nvidia cards when the image resolution changes: d3d11 will used by
default until this is fixed).
`d3dvideosink`, `glimagesink`, `gtksink`, `autovideosink`. *There have
been reports of segfaults of the newer d3d12 videodecoder on certain
older Nvidia cards when the image resolution changes, e.g., when the iOS
client is rotated between portrait and landcape modes: this was a
GStreamer issue that is apparently now fixed (a workaround is to use
d3d11).*
- With Direct3D 11.0 or greater, various options can be set using
e.g. `-vs "d3d11videosink <options>"` (see the gstreamer videosink
@@ -1012,6 +1058,24 @@ default until this is fixed).
The executable uxplay.exe can also be run without the MSYS2 environment,
in the Windows Terminal, with `C:\msys64\ucrt64\bin\uxplay`.
There is a new modernized Windows Terminal application available from
Microsoft that provides various terminals, and can be customized to also
provide the MSYS2 terminals. See https://www.msys2.org/docs/terminals/
(to make those instructions clearer: in the dropdown "Settings" menu,
there is a second "settings" icon in the lower left corner: click on
that to edit the settings.json file as described).
The server name (-n `<name>`{=html} option) can be given in
internationalized UTF-8 encoding: To enter UTF-8 characters in the MSYS2
or other Windows terminals, use the numerical keypad with "Num Lock" on:
while holding down the "Alt" key, type "+" on the keypad, followed by
the UTF-8 hex code for the character (using the keypad for numbers),
then release the "Alt" key. (The UTF-8 hex codes have 4 hex digits: for
example, the "copyright" symbol has hex code 00a9.) This method must be
activated in the Windows Registry: using regedit, find the Registry
section 'HKEY_Current_User/Control Panel/Input Method", and add a new
Key "EnableHexNumpad" with value "1", then reboot the computer.
# Usage
Options:
@@ -1030,6 +1094,7 @@ this overrides `$UXPLAYRC`, `~/.uxplayrc`, etc.
the name that appears offering AirPlay services to your iPad, iPhone
etc, where *hostname* is the name of the server running uxplay. This
will also now be the name shown above the mirror display (X11) window.
**Internationalized server names encoded as UTF-8 are accepted.**
**-nh** Do not append "@_hostname_" at the end of the AirPlay server
name.
@@ -1055,6 +1120,16 @@ allows selection of the version of GStreamer's \"playbin\" video player
to use for playing HLS video. *(Playbin v3 is the recommended player,
but if some videos fail to play, you can try with version 2.)*
**-scrsv n**. (since 1.73) (So far, only implemented on Linux/\*BSD
systems using D-Bus). Inhibit the screensaver in the absence of keyboard
input (e.g., while watching video), using the
org.freedesktop.ScreenSaver D-Bus service: n = 0: (off) n= 1 (on during
video activity) n=2 (always on). *Note: to verify this feature is
working, you can use `dbus-monitor` to view events on the D-Bus;
depending on the Desktop Environment, commands like
`gnome-session-inhibit -l`, `xfce4-screensaver-commannd -q`, etc.,
should list UxPlay when it is inhibiting the screensaver.*
**-pin \[nnnn\]**: (since v1.67) use Apple-style (one-time) "pin"
authentication when a new client connects for the first time: a
four-digit pin code is displayed on the terminal, and the client screen
@@ -1083,15 +1158,22 @@ deregisters the corresponding client (see options -restrict, -block,
the startup file if you wish to use this feature.)*
**-pw** \[*pwd*\]. (since 1.72). As an alternative to -pin, client
access can be controlled with a password set when uxplay starts (set it
in the .uxplay startup file, where it is stored as cleartext.) All users
must then know this password. This uses HTTP md5 Digest authentication,
which is now regarded as providing weak security, but it is only used to
validate the uxplay password, and no user credentials are exposed. If
*pwd* is **not** specified, a random 4-digit pin code is displayed, and
must be entered on the client at **each** new connection. *Note: -pin
and -pw are alternatives: if both are specified at startup, the earlier
of these two options is discarded.*
access can be controlled with a password. If a password *pwd* (of length
at least six characters) is set when uxplay starts (usually set in the
startup file, where it is stored as cleartext), all users must know this
password to connect to UxPlay (the client prompts for it). This method
uses HTTP md5 Digest authentication, which is now regarded as providing
weak security, but it is only used to validate the uxplay password, and
no user credentials are exposed. After a successful authentication, the
client stores the password, and will use it initially for future
authentications without prompting, so long as the UxPlay deviceID has
not changed (this initial authentication will fail if the UxPlay
password has changed). If *pwd* is **not** specified with the -pw option
when UxPlay starts, a new random 4-digit pin code is generated and
displayed on the UxPlay terminal for **each** new connection, and must
be entered on the client (there are three chances to enter it, before it
is changed). *Note: -pin and -pw are alternatives: if both are specified
at startup, the earlier of these two options is discarded.*
**-vsync \[x\]** (In Mirror mode:) this option (**now the default**)
uses timestamps to synchronize audio with video on the server, with an
@@ -1222,6 +1304,12 @@ video), and only used to render audio, which will be AAC
lossily-compressed audio in mirror mode with unrendered video, and
superior-quality ALAC Apple Lossless audio in Airplay audio-only mode.
**-vrtp *pipeline***: forward rtp packets of decrypted video to
somewhere else, without rendering. Uses rtph264pay or rtph265pay as
appropriate: *pipeline* should start with any rtph26xpay options (such
as config_interval= or aggregate-mode =), followed by a sending method:
*e.g.*, `"config-interval=1 ! udpsink host=127.0.0.1 port=5000`".
**-v4l2** Video settings for hardware h264 video decoding in the GPU by
Video4Linux2. Equivalent to `-vd v4l2h264dec -vc v4l2convert`.
@@ -1411,6 +1499,13 @@ that (unlike dumped video) the dumped audio is currently only useful for
debugging, as it is not containerized to make it playable with standard
audio players.*
**-ble \[*filename*\]**. Enable Bluetooth beacon Service Discovery. The
port, PID and process name of the UxPlay process is recorded by default
in `~/.uxplay.ble` : (this file is created when UxPlay starts and
deleted when it stops.) Optionally the file *filename*, which must be
the full path to a writeable file can instead be used. **See below for
beacon setup instructions.**
**-d \[n\]** Enable debug output; optional argument n=1 suppresses
audio/video packet data in debug output. Note: this does not show
GStreamer error or debug messages. To see GStreamer error and warning
@@ -1419,6 +1514,103 @@ GST_DEBUG=2" before running uxplay. To see GStreamer information
messages, set GST_DEBUG=4; for DEBUG messages, GST_DEBUG=5; increase
this to see even more of the GStreamer inner workings.
# Bluetooth LE beacon setup
The python\>=3.6 script for running a Bluetooth-LE Service Discovery
beacon is uxplay-beacon.py. It comes in two versions, one (for Linux and
\*BSD) is only installed on systems which support DBUS, and another only
for Windows 10/11. Bluetooth \>= 4.0 hardware on the host computer is
required: a cheap USB bluetooth dongle can be used.
On Linux/\*BSD, Bluetooth support (BlueZ) must be installed (on
Debian-based systems: `sudo apt install bluez bluez-tools`; recent
Ubuntu releases provide bluez as a snap package). In addition to
standard Python3 libraries, you may need to install the gi, dbus, and
psutil Python libraries used by uxplay-beacon.py. On Debian-based
systems:
sudo apt install python3-gi python3-dbus python3-psutil
For Windows support on MSYS2 UCRT systems, use pacman -S to install
`mingw-w64-ucrt-x86_64-python`, `*-python-gobject`, `*-python-psutil`,
and `*-python-pip`. Then install winrt bindings
`pip install winrt-Windows.Foundation.Collections`,
`winrt-Windows.Devices.Bluetooth.Advertisement` and
`winrt-Windows.Storage.Streams`.
If uxplay will be run with option "`uxplay -ble`" (so it writes data for
the Bluetooth beacon in the default BLE data file `~/.uxplay.ble`), just
run `uxplay-beacon.py` in a separate terminal. The python script will
start Bluetooth LE Service-Discovery advertising when it detects that
UxPlay is running by checking if the BLE data file exists, and stop when
it no longer detects a running UxPlay plus this file (it will restart
advertising if UxPlay later reappears). The script will remain active
until stopped with Ctrl+C in its terminal window (or its terminal window
is closed).
The beacon script can be more finely controlled using certain options:
these can be given on the command line, or read from a configuration
file `~/.uxplay.beacon`, if it exists. Configuration file entries are
like the command line forms, one per line (e.g.,
`--ipv4 192.168.1.100`). Lines commented out with an initial `#` are
ignored. Command line options override the configuration file options.
Get help with `man uxplay-beacon` or `uxplay-beacon.py --help`. Options
are
- `--file <config file>` read beacon options from `<config file>`
instead of `~/.uxplay.beacon`.
- `--ipv4 <ipv4 address>`. This option can be used to specify the
ipv4 address at which the UxPlay server should be contacted by the
client. If it is not given, an address will be obtained
automatically using `gethostbyname`. Only ipv4 addresses are
supported.
- `--path <BLE data file>`. This overrides the default choice of BLE
data file (`~/.uxplay.ble`) that is monitored by the beacon script.
This also requires that uxplay is run with option
"`uxplay -ble <BLE data file>`".
The BlueZ/Dbus version has thee more options not offered by the Windows
version:
- `--AdvMin x`, `--AdvMax y`. These controls the interval between BLE
advertisement broadcasts. This interval is in the range \[x, y\],
given in units of msecs. Allowed ranges are 100 \<= x \<= y
\<= 10240. If AdvMin=AdvMax, the interval is fixed: if AdvMin \<
AdvMax it is chosen flexibly in this range to avoid interfering with
other tasks the Bluetooth device is carrying out. The default values
are AdvMin = AdvMax = 100. The advertisement is broadcast on all
three Bluetooth LE advertising channels: 37,38,39.
- `--index x` (default x = 0, x \>= 0). This should be used to
distinguish between multiple simultaneous instances of
uxplay-beacon.py that are running to support multiple instances of
UxPlay. Each instance must have its own BLE Data file (just as each
instance of UxPlay must also have its own MAC address and ports).
*Note: running multiple beacons simultaneously on the same host has
not been tested.*
If you wish to test Bluetooth LE Service Discovery on Linux/\*BSD, you
can disable DNS_SD Service discovery by the avahi-daemon with
$ sudo systemctl mask avahi-daemon.socket
$ sudo systemctl stop avahi-daemon
To restore DNS_SD Service discovery, replace "mask" by "unmask", and
"stop" by "start".
On Windows, the Bonjour Service is controlled using **Services
Management**: press "Windows + R" to open the Run dialog, run
`services.msc`, and click on **Bonjour Service** in the alphabetic list.
This will show links for it to be stopped and restarted.
For more information, see the [wiki
page](https://github.com/FDH2/UxPlay/wiki/Bluetooth_LE_beacon)
- **Note that Bluetooth LE AirPlay Service Discovery only supports
broadcast of IPv4 addresses**.
# Troubleshooting
Note: `uxplay` is run from a terminal command line, and informational
@@ -1761,8 +1953,12 @@ what version UxPlay claims to be.
# Changelog
xxxx 2025-07-07 Render Audio cover-art inside UxPlay with -ca option (no
file specified).
xxxx 2025-09-25 Render Audio cover-art inside UxPlay with -ca option (no
file specified). (D-Bus based) option -scrsv `<n>`{=html} to inhibit
screensaver while UxPlay is running (Linux/\*BSD only). Add support for
Service Discovery using a Bluetooth LE beacon. Add -vrtp option for
forwarding decrypted h264/5 video to an external renderer (e.g., OBS
Studio). Check that option input strings have valid UTF-8 encoding.
1.72.2 2025-07-07 Fix bug (typo) in DNS_SD advertisement introduced with
-pw option. Update llhttp to v 9.3.0

View File

@@ -58,7 +58,7 @@ int airplay_video_service_init(raop_t *raop, unsigned short http_port,
airplay_video_t *airplay_video = deregister_airplay_video(raop);
if (airplay_video) {
airplay_video_service_destroy(airplay_video);
airplay_video_service_destroy(airplay_video);
}
/* calloc guarantees that the 36-character strings apple_session_id and
@@ -74,11 +74,11 @@ int airplay_video_service_init(raop_t *raop, unsigned short http_port,
snprintf(ptr, 6, "%-5u", http_port);
ptr = strstr(airplay_video->local_uri_prefix, " ");
if (ptr) {
*ptr = '\0';
*ptr = '\0';
}
if (!register_airplay_video(raop, airplay_video)) {
return -2;
return -2;
}
//printf(" %p %p\n", airplay_video, get_airplay_video(raop));
@@ -118,7 +118,6 @@ airplay_video_service_destroy(airplay_video_t *airplay_video)
free (airplay_video->master_playlist);
}
free (airplay_video);
}
@@ -146,19 +145,19 @@ const char *get_playback_uuid(airplay_video_t *airplay_video) {
}
void set_uri_prefix(airplay_video_t *airplay_video, char *uri_prefix, int uri_prefix_len) {
if (airplay_video->uri_prefix) {
free (airplay_video->uri_prefix);
}
airplay_video->uri_prefix = (char *) calloc(uri_prefix_len + 1, sizeof(char));
memcpy(airplay_video->uri_prefix, uri_prefix, uri_prefix_len);
if (airplay_video->uri_prefix) {
free (airplay_video->uri_prefix);
}
airplay_video->uri_prefix = (char *) calloc(uri_prefix_len + 1, sizeof(char));
memcpy(airplay_video->uri_prefix, uri_prefix, uri_prefix_len);
}
char *get_uri_prefix(airplay_video_t *airplay_video) {
return airplay_video->uri_prefix;
return airplay_video->uri_prefix;
}
char *get_uri_local_prefix(airplay_video_t *airplay_video) {
return airplay_video->local_uri_prefix;
return airplay_video->local_uri_prefix;
}
char *get_master_uri(airplay_video_t *airplay_video) {
@@ -198,7 +197,7 @@ void destroy_media_data_store(airplay_video_t *airplay_video) {
media_item_t *media_data_store = airplay_video->media_data_store;
if (media_data_store) {
for (int i = 0; i < airplay_video->num_uri ; i ++ ) {
if (media_data_store[i].uri) {
if (media_data_store[i].uri) {
free (media_data_store[i].uri);
}
if (media_data_store[i].playlist) {
@@ -336,7 +335,7 @@ char *adjust_master_playlist (char *fcup_response_data, int fcup_response_datale
while (ptr != NULL) {
counter++;
ptr++;
ptr = strstr(ptr, uri_prefix);
ptr = strstr(ptr, uri_prefix);
}
size_t len = uri_local_prefix_len - uri_prefix_len;

View File

@@ -310,16 +310,16 @@ dnssd_register_raop(dnssd_t *dnssd, unsigned short port)
case 2:
case 3:
dnssd->TXTRecordSetValue(&dnssd->raop_record, "pw", strlen("true"), "true");
dnssd->TXTRecordSetValue(&dnssd->raop_record, "sf", 4, "0x84");
break;
dnssd->TXTRecordSetValue(&dnssd->raop_record, "sf", 4, "0x84");
break;
case 1:
dnssd->TXTRecordSetValue(&dnssd->raop_record, "pw", strlen("true"), "true");
dnssd->TXTRecordSetValue(&dnssd->raop_record, "sf", 3, "0x8c");
break;
dnssd->TXTRecordSetValue(&dnssd->raop_record, "sf", 3, "0x8c");
break;
default:
dnssd->TXTRecordSetValue(&dnssd->raop_record, "pw", strlen("false"), "false");
dnssd->TXTRecordSetValue(&dnssd->raop_record, "sf", strlen(RAOP_SF), RAOP_SF);
break;
dnssd->TXTRecordSetValue(&dnssd->raop_record, "sf", strlen(RAOP_SF), RAOP_SF);
break;
}
dnssd->TXTRecordSetValue(&dnssd->raop_record, "sr", strlen(RAOP_SR), RAOP_SR);
dnssd->TXTRecordSetValue(&dnssd->raop_record, "ss", strlen(RAOP_SS), RAOP_SS);
@@ -382,8 +382,8 @@ dnssd_register_airplay(dnssd_t *dnssd, unsigned short port)
switch (dnssd->pin_pw) {
case 1: // display onscreen pin
dnssd->TXTRecordSetValue(&dnssd->airplay_record, "pw", strlen("true"), "true");
dnssd->TXTRecordSetValue(&dnssd->airplay_record, "flags", 3, "0x4");
break;
dnssd->TXTRecordSetValue(&dnssd->airplay_record, "flags", 3, "0x4");
break;
case 2: // require password
case 3:
dnssd->TXTRecordSetValue(&dnssd->airplay_record, "pw", strlen("true"), "true");
@@ -412,6 +412,13 @@ dnssd_register_airplay(dnssd_t *dnssd, unsigned short port)
return (int) retval; /* error codes are listed in Apple's dns_sd.h */
}
const char *
dnssd_get_raop_txt(dnssd_t *dnssd, int *length)
{
*length = dnssd->TXTRecordGetLength(&dnssd->raop_record);
return dnssd->TXTRecordGetBytesPtr(&dnssd->raop_record);
}
const char *
dnssd_get_airplay_txt(dnssd_t *dnssd, int *length)
{
@@ -476,13 +483,13 @@ dnssd_unregister_airplay(dnssd_t *dnssd)
}
uint64_t dnssd_get_airplay_features(dnssd_t *dnssd) {
uint64_t features = ((uint64_t) dnssd->features2) << 32;
features += (uint64_t) dnssd->features1;
return features;
uint64_t features = ((uint64_t) dnssd->features2) << 32;
features += (uint64_t) dnssd->features1;
return features;
}
void dnssd_set_pk(dnssd_t *dnssd, char * pk_str) {
dnssd->pk = pk_str;
dnssd->pk = pk_str;
}
void dnssd_set_airplay_features(dnssd_t *dnssd, int bit, int val) {
@@ -498,8 +505,8 @@ void dnssd_set_airplay_features(dnssd_t *dnssd, int bit, int val) {
features = &(dnssd->features1);
}
if (val) {
*features = *features | mask;
*features = *features | mask;
} else {
*features = *features & ~mask;
*features = *features & ~mask;
}
}

View File

@@ -43,6 +43,7 @@ DNSSD_API int dnssd_register_airplay(dnssd_t *dnssd, unsigned short port);
DNSSD_API void dnssd_unregister_raop(dnssd_t *dnssd);
DNSSD_API void dnssd_unregister_airplay(dnssd_t *dnssd);
DNSSD_API const char *dnssd_get_raop_txt(dnssd_t *dnssd, int *length);
DNSSD_API const char *dnssd_get_airplay_txt(dnssd_t *dnssd, int *length);
DNSSD_API const char *dnssd_get_name(dnssd_t *dnssd, int *length);
DNSSD_API const char *dnssd_get_hw_addr(dnssd_t *dnssd, int *length);

View File

@@ -124,7 +124,7 @@ http_handler_rate(raop_conn_t *conn, http_request_t *request, http_response_t *r
if (end && end != rate) {
rate_value = value;
logger_log(conn->raop->logger, LOGGER_DEBUG, "http_handler_rate: got rate = %.6f", rate_value);
}
}
}
conn->raop->callbacks.on_video_rate(conn->raop->callbacks.cls, rate_value);
}
@@ -142,7 +142,7 @@ http_handler_stop(raop_conn_t *conn, http_request_t *request, http_response_t *r
static void
http_handler_set_property(raop_conn_t *conn,
http_request_t *request, http_response_t *response,
char **response_data, int *response_datalen) {
char **response_data, int *response_datalen) {
const char *url = http_request_get_url(request);
const char *property = url + strlen("/setProperty?");
@@ -247,12 +247,12 @@ int create_playback_info_plist_xml(playback_info_t *playback_info, char **plist_
plist_t loaded_time_ranges_node = plist_new_array();
time_range_to_plist(playback_info->loadedTimeRanges, playback_info->num_loaded_time_ranges,
loaded_time_ranges_node);
loaded_time_ranges_node);
plist_dict_set_item(res_root_node, "loadedTimeRanges", loaded_time_ranges_node);
plist_t seekable_time_ranges_node = plist_new_array();
time_range_to_plist(playback_info->seekableTimeRanges, playback_info->num_seekable_time_ranges,
seekable_time_ranges_node);
seekable_time_ranges_node);
plist_dict_set_item(res_root_node, "seekableTimeRanges", seekable_time_ranges_node);
int len;
@@ -264,7 +264,6 @@ int create_playback_info_plist_xml(playback_info_t *playback_info, char **plist_
return len;
}
/* this handles requests from the Client for "Playback information" while the Media is playing on the
Media Player. (The Server gets this information by monitoring the Media Player). The Client could use
the information to e.g. update the slider it shows with progress to the player (0%-100%).
@@ -287,11 +286,11 @@ http_handler_playback_info(raop_conn_t *conn, http_request_t *request, http_resp
conn->raop->callbacks.on_video_acquire_playback_info(conn->raop->callbacks.cls, &playback_info);
if (playback_info.duration == -1.0) {
/* video has finished, reset */
logger_log(conn->raop->logger, LOGGER_DEBUG, "playback_info not available (finishing)");
//httpd_remove_known_connections(conn->raop->httpd);
http_response_set_disconnect(response,1);
conn->raop->callbacks.video_reset(conn->raop->callbacks.cls);
return;
logger_log(conn->raop->logger, LOGGER_DEBUG, "playback_info not available (finishing)");
//httpd_remove_known_connections(conn->raop->httpd);
http_response_set_disconnect(response,1);
conn->raop->callbacks.video_reset(conn->raop->callbacks.cls);
return;
} else if (playback_info.position == -1.0) {
logger_log(conn->raop->logger, LOGGER_DEBUG, "playback_info not available");
return;
@@ -345,9 +344,8 @@ http_handler_reverse(raop_conn_t *conn, http_request_t *request, http_response_t
if (type_PTTH == 1) {
logger_log(conn->raop->logger, LOGGER_DEBUG, "will use socket %d for %s connections", socket_fd, purpose);
http_response_init(response, "HTTP/1.1", 101, "Switching Protocols");
http_response_add_header(response, "Connection", "Upgrade");
http_response_add_header(response, "Upgrade", "PTTH/1.0");
http_response_add_header(response, "Connection", "Upgrade");
http_response_add_header(response, "Upgrade", "PTTH/1.0");
} else {
logger_log(conn->raop->logger, LOGGER_ERR, "multiple TPPH connections (%d) are forbidden", type_PTTH );
}
@@ -368,7 +366,6 @@ http_handler_action(raop_conn_t *conn, http_request_t *request, http_response_t
int request_id = 0;
int fcup_response_statuscode = 0;
bool logger_debug = (logger_get_level(conn->raop->logger) >= LOGGER_DEBUG);
const char* session_id = http_request_get_header(request, "X-Apple-Session-ID");
if (!session_id) {
@@ -405,7 +402,7 @@ http_handler_action(raop_conn_t *conn, http_request_t *request, http_response_t
/* determine type of data */
plist_t req_type_node = plist_dict_get_item(req_root_node, "type");
if (!PLIST_IS_STRING(req_type_node)) {
goto post_action_error;
goto post_action_error;
}
plist_t req_params_node = NULL;
@@ -414,9 +411,13 @@ http_handler_action(raop_conn_t *conn, http_request_t *request, http_response_t
char *type = NULL;
plist_get_string_val(req_type_node, &type);
logger_log(conn->raop->logger, LOGGER_DEBUG, "action type is %s", type);
if (strstr(type, "unhandledURLResponse")) {
bool unhandled_url_response = strstr(type, "unhandledURLResponse");
bool playlist_remove = strstr(type, "playlistRemove");
bool playlist_insert = strstr(type, "playlistInsert");
plist_mem_free(type);
if (unhandled_url_response) {
goto unhandledURLResponse;
} else if (strstr(type, "playlistRemove")) {
} else if (playlist_remove) {
logger_log(conn->raop->logger, LOGGER_INFO, "unhandled action type playlistRemove (stop playback)");
req_params_node = plist_dict_get_item(req_root_node, "params");
if (!req_params_node || !PLIST_IS_DICT (req_params_node)) {
@@ -431,38 +432,49 @@ http_handler_action(raop_conn_t *conn, http_request_t *request, http_response_t
}
plist_t req_params_item_uuid_node = plist_dict_get_item(req_params_item_node, "uuid");
char* remove_uuid = NULL;
plist_get_string_val(req_params_item_uuid_node, &remove_uuid);
plist_get_string_val(req_params_item_uuid_node, &remove_uuid);
const char *playback_uuid = get_playback_uuid(conn->raop->airplay_video);
if (strcmp(remove_uuid, playback_uuid)) {
if (strcmp(remove_uuid, playback_uuid)) {
logger_log(conn->raop->logger, LOGGER_ERR, "uuid of playlist removal action request did not match current playlist:\n"
" current: %s\n remove: %s", playback_uuid, remove_uuid);
} else {
logger_log(conn->raop->logger, LOGGER_DEBUG, "removal_uuid matches playback_uuid\n");
}
free (remove_uuid);
}
plist_mem_free (remove_uuid);
}
logger_log(conn->raop->logger, LOGGER_ERR, "FIXME: playlist removal not yet implemented");
goto finish;
} else if (strstr(type, "playlistInsert")) {
} else if (playlist_insert) {
logger_log(conn->raop->logger, LOGGER_INFO, "unhandled action type playlistInsert (add new playback)");
printf("\n***************FIXME************************\nPlaylist insertion needs more information for it to be implemented:\n"
"please report following output as an \"Issue\" at http://github.com/FDH2/UxPlay:\n");
char *header_str = NULL;
http_request_get_header_string(request, &header_str);
printf("\n\n%s\n", header_str);
bool is_plist = (bool) strstr(header_str,"apple-binary-plist");
bool data_is_plist = (strstr(header_str,"apple-binary-plist") != NULL);
free(header_str);
if (is_plist) {
if (data_is_plist) {
int request_datalen;
const char *request_data = http_request_get_data(request, &request_datalen);
plist_t req_root_node = NULL;
plist_from_bin(request_data, request_datalen, &req_root_node);
char * plist_xml;
uint32_t plist_len;
char *plist_xml = NULL;
char *stripped_xml = NULL;
uint32_t plist_len = 0;
plist_to_xml(req_root_node, &plist_xml, &plist_len);
plist_xml = utils_strip_data_from_plist_xml(plist_xml);
printf("%s", plist_xml);
free(plist_xml);
printf("plist_len = %u\n", plist_len);
stripped_xml = utils_strip_data_from_plist_xml(plist_xml);
printf("%s", stripped_xml ? stripped_xml : plist_xml);
if (stripped_xml) {
free(stripped_xml);
}
if (plist_xml) {
#ifdef PLIST_230
plist_mem_free(plist_xml);
#else
plist_to_xml_free(plist_xml);
#endif
}
plist_free(req_root_node);
}
assert(0);
@@ -510,12 +522,14 @@ http_handler_action(raop_conn_t *conn, http_request_t *request, http_response_t
char *fcup_response_url = NULL;
plist_get_string_val(req_params_fcup_response_url_node, &fcup_response_url);
if (!fcup_response_url) {
plist_mem_free(fcup_response_url);
goto post_action_error;
}
logger_log(conn->raop->logger, LOGGER_DEBUG, "FCUP_Response_URL = %s", fcup_response_url);
plist_t req_params_fcup_response_data_node = plist_dict_get_item(req_params_node, "FCUP_Response_Data");
if (!PLIST_IS_DATA(req_params_fcup_response_data_node)){
plist_mem_free(fcup_response_url);
goto post_action_error;
}
@@ -526,16 +540,17 @@ http_handler_action(raop_conn_t *conn, http_request_t *request, http_response_t
if (!fcup_response_data) {
free (fcup_response_url);
plist_mem_free(fcup_response_url);
goto post_action_error;
}
if (logger_debug) {
logger_log(conn->raop->logger, LOGGER_DEBUG, "FCUP_Response datalen = %d", fcup_response_datalen);
char *data = malloc(fcup_response_datalen + 1);
memcpy(data, fcup_response_data, fcup_response_datalen);
data[fcup_response_datalen] = '\0';
logger_log(conn->raop->logger, LOGGER_DEBUG, "begin FCUP Response data:\n%s\nend FCUP Response data",data);
free (data);
char *data = malloc(fcup_response_datalen + 1);
memcpy(data, fcup_response_data, fcup_response_datalen);
data[fcup_response_datalen] = '\0';
logger_log(conn->raop->logger, LOGGER_DEBUG, "begin FCUP Response data:\n%s\nend FCUP Response data",data);
free (data);
}
@@ -543,24 +558,24 @@ http_handler_action(raop_conn_t *conn, http_request_t *request, http_response_t
if (ptr) {
/* this is a master playlist */
char *uri_prefix = get_uri_prefix(conn->raop->airplay_video);
char ** media_data_store = NULL;
char ** media_data_store = NULL;
int num_uri = 0;
char *uri_local_prefix = get_uri_local_prefix(conn->raop->airplay_video);
char *new_master = adjust_master_playlist (fcup_response_data, fcup_response_datalen, uri_prefix, uri_local_prefix);
store_master_playlist(conn->raop->airplay_video, new_master);
create_media_uri_table(uri_prefix, fcup_response_data, fcup_response_datalen, &media_data_store, &num_uri);
create_media_data_store(conn->raop->airplay_video, media_data_store, num_uri);
num_uri = get_num_media_uri(conn->raop->airplay_video);
set_next_media_uri_id(conn->raop->airplay_video, 0);
create_media_data_store(conn->raop->airplay_video, media_data_store, num_uri);
num_uri = get_num_media_uri(conn->raop->airplay_video);
set_next_media_uri_id(conn->raop->airplay_video, 0);
} else {
/* this is a media playlist */
assert(fcup_response_data);
char *playlist = (char *) calloc(fcup_response_datalen + 1, sizeof(char));
memcpy(playlist, fcup_response_data, fcup_response_datalen);
char *playlist = (char *) calloc(fcup_response_datalen + 1, sizeof(char));
memcpy(playlist, fcup_response_data, fcup_response_datalen);
int uri_num = get_next_media_uri_id(conn->raop->airplay_video);
--uri_num; // (next num is current num + 1)
store_media_playlist(conn->raop->airplay_video, playlist, uri_num);
--uri_num; // (next num is current num + 1)
store_media_playlist(conn->raop->airplay_video, playlist, uri_num);
float duration = 0.0f;
int count = analyze_media_playlist(playlist, &duration);
if (count) {
@@ -573,9 +588,7 @@ http_handler_action(raop_conn_t *conn, http_request_t *request, http_response_t
if (fcup_response_data) {
free (fcup_response_data);
}
if (fcup_response_url) {
free (fcup_response_url);
}
plist_mem_free(fcup_response_url);
int num_uri = get_num_media_uri(conn->raop->airplay_video);
int uri_num = get_next_media_uri_id(conn->raop->airplay_video);
@@ -583,7 +596,7 @@ http_handler_action(raop_conn_t *conn, http_request_t *request, http_response_t
fcup_request((void *) conn, get_media_uri_by_num(conn->raop->airplay_video, uri_num),
apple_session_id,
get_next_FCUP_RequestID(conn->raop->airplay_video));
set_next_media_uri_id(conn->raop->airplay_video, ++uri_num);
set_next_media_uri_id(conn->raop->airplay_video, ++uri_num);
} else {
char * uri_local_prefix = get_uri_local_prefix(conn->raop->airplay_video);
conn->raop->callbacks.on_video_play(conn->raop->callbacks.cls,
@@ -599,7 +612,7 @@ http_handler_action(raop_conn_t *conn, http_request_t *request, http_response_t
http_response_init(response, "HTTP/1.1", 400, "Bad Request");
if (req_root_node) {
plist_free(req_root_node);
plist_free(req_root_node);
}
}
@@ -643,23 +656,23 @@ http_handler_play(raop_conn_t *conn, http_request_t *request, http_response_t *r
char *header_str = NULL;
http_request_get_header_string(request, &header_str);
logger_log(conn->raop->logger, LOGGER_DEBUG, "request header:\n%s", header_str);
data_is_binary_plist = (strstr(header_str, "x-apple-binary-plist") != NULL);
data_is_text = (strstr(header_str, "text/parameters") != NULL);
data_is_octet = (strstr(header_str, "octet-stream") != NULL);
free (header_str);
data_is_binary_plist = (strstr(header_str, "x-apple-binary-plist") != NULL);
data_is_text = (strstr(header_str, "text/parameters") != NULL);
data_is_octet = (strstr(header_str, "octet-stream") != NULL);
free (header_str);
}
if (!data_is_text && !data_is_octet && !data_is_binary_plist) {
goto play_error;
goto play_error;
}
if (data_is_text) {
logger_log(conn->raop->logger, LOGGER_ERR, "Play request Content is text (unsupported)");
goto play_error;
goto play_error;
}
if (data_is_octet) {
logger_log(conn->raop->logger, LOGGER_ERR, "Play request Content is octet-stream (unsupported)");
goto play_error;
goto play_error;
}
if (data_is_binary_plist) {
@@ -671,9 +684,9 @@ http_handler_play(raop_conn_t *conn, http_request_t *request, http_response_t *r
} else {
char* playback_uuid = NULL;
plist_get_string_val(req_uuid_node, &playback_uuid);
set_playback_uuid(conn->raop->airplay_video, playback_uuid);
free (playback_uuid);
}
set_playback_uuid(conn->raop->airplay_video, playback_uuid);
plist_mem_free (playback_uuid);
}
plist_t req_content_location_node = plist_dict_get_item(req_root_node, "Content-Location");
if (!req_content_location_node) {
@@ -691,32 +704,31 @@ http_handler_play(raop_conn_t *conn, http_request_t *request, http_response_t *r
logger_log(conn->raop->logger, LOGGER_WARNING, "Unsupported HLS streaming format: clientProcName %s not found in supported list: %s",
client_proc_name, supported_hls_proc_names);
}
plist_mem_free(client_proc_name);
}
plist_t req_start_position_seconds_node = plist_dict_get_item(req_root_node, "Start-Position-Seconds");
if (!req_start_position_seconds_node) {
logger_log(conn->raop->logger, LOGGER_INFO, "No Start-Position-Seconds in Play request");
} else {
} else {
double start_position = 0.0;
plist_get_real_val(req_start_position_seconds_node, &start_position);
start_position_seconds = (float) start_position;
start_position_seconds = (float) start_position;
}
set_start_position_seconds(conn->raop->airplay_video, (float) start_position_seconds);
set_start_position_seconds(conn->raop->airplay_video, (float) start_position_seconds);
}
char *ptr = strstr(playback_location, "/master.m3u8");
if (!ptr) {
logger_log(conn->raop->logger, LOGGER_ERR, "Content-Location has unsupported form:\n%s\n", playback_location);
goto play_error;
goto play_error;
}
int prefix_len = (int) (ptr - playback_location);
set_uri_prefix(conn->raop->airplay_video, playback_location, prefix_len);
set_next_media_uri_id(conn->raop->airplay_video, 0);
fcup_request((void *) conn, playback_location, apple_session_id, get_next_FCUP_RequestID(conn->raop->airplay_video));
if (playback_location) {
free (playback_location);
}
plist_mem_free(playback_location);
if (req_root_node) {
plist_free(req_root_node);
@@ -724,6 +736,7 @@ http_handler_play(raop_conn_t *conn, http_request_t *request, http_response_t *r
return;
play_error:;
plist_mem_free(playback_location);
if (req_root_node) {
plist_free(req_root_node);
}
@@ -760,7 +773,7 @@ http_handler_hls(raop_conn_t *conn, http_request_t *request, http_response_t *r
if (!strcmp(url, "/master.m3u8")){
char * master_playlist = get_master_playlist(conn->raop->airplay_video);
if (master_playlist) {
if (master_playlist) {
size_t len = strlen(master_playlist);
char * data = (char *) malloc(len + 1);
memcpy(data, master_playlist, len);

View File

@@ -81,11 +81,11 @@ on_header_field(llhttp_t *parser, const char *at, size_t length)
/* Allocate space in the current header string */
if (request->headers[request->headers_index] == NULL) {
request->headers[request->headers_index] = calloc(1, length+1);
request->headers[request->headers_index] = calloc(1, length + 1);
} else {
request->headers[request->headers_index] = realloc(
request->headers[request->headers_index],
strlen(request->headers[request->headers_index])+length+1
strlen(request->headers[request->headers_index]) + length + 1
);
}
assert(request->headers[request->headers_index]);
@@ -106,11 +106,11 @@ on_header_value(llhttp_t *parser, const char *at, size_t length)
/* Allocate space in the current header string */
if (request->headers[request->headers_index] == NULL) {
request->headers[request->headers_index] = calloc(1, length+1);
request->headers[request->headers_index] = calloc(1, length + 1);
} else {
request->headers[request->headers_index] = realloc(
request->headers[request->headers_index],
strlen(request->headers[request->headers_index])+length+1
strlen(request->headers[request->headers_index]) + length + 1
);
}
assert(request->headers[request->headers_index]);
@@ -124,7 +124,7 @@ on_body(llhttp_t *parser, const char *at, size_t length)
{
http_request_t *request = parser->data;
request->data = realloc(request->data, request->datalen+length);
request->data = realloc(request->data, request->datalen + length);
assert(request->data);
memcpy(request->data+request->datalen, at, length);
@@ -172,7 +172,7 @@ http_request_destroy(http_request_t *request)
if (request) {
free(request->url);
for (i=0; i<request->headers_size; i++) {
for (i = 0; i < request->headers_size; i++) {
free(request->headers[i]);
}
free(request->headers);
@@ -273,7 +273,7 @@ http_request_get_header(http_request_t *request, const char *name)
return NULL;
}
for (i=0; i<request->headers_size; i+=2) {
for (i = 0; i < request->headers_size; i += 2) {
if (!strcmp(request->headers[i], name)) {
return request->headers[i+1];
}
@@ -305,7 +305,7 @@ http_request_get_header_string(http_request_t *request, char **header_str)
int len = 0;
for (int i = 0; i < request->headers_size; i++) {
len += strlen(request->headers[i]);
if (i%2 == 0) {
if (i % 2 == 0) {
len += 2;
} else {
len++;
@@ -321,12 +321,12 @@ http_request_get_header_string(http_request_t *request, char **header_str)
snprintf(p, n, "%s", request->headers[i]);
n -= hlen;
p += hlen;
if (i%2 == 0) {
if (i % 2 == 0) {
snprintf(p, n, ": ");
n -= 2;
p += 2;
} else {
snprintf(p, n, "\n");
snprintf(p, n, "\n");
n--;
p++;
}

View File

@@ -201,23 +201,26 @@ httpd_destroy(httpd_t *httpd)
static void
httpd_remove_connection(httpd_t *httpd, http_connection_t *connection)
{
int socket_fd = connection->socket_fd;
connection->socket_fd = 0;
if (connection->request) {
http_request_destroy(connection->request);
connection->request = NULL;
}
logger_log(httpd->logger, LOGGER_DEBUG, "removing connection type %s socket %d conn %p", typename[connection->type],
connection->socket_fd, connection->user_data);
socket_fd, connection->user_data);
if (connection->user_data) {
httpd->callbacks.conn_destroy(connection->user_data);
connection->user_data = NULL;
}
if (connection->socket_fd) {
shutdown(connection->socket_fd, SHUT_WR);
int ret = closesocket(connection->socket_fd);
if (socket_fd) {
shutdown(socket_fd, SHUT_WR);
int ret = closesocket(socket_fd);
if (ret == -1) {
logger_log(httpd->logger, LOGGER_ERR, "httpd error in closesocket (close): %d %s", errno, strerror(errno));
} else {
logger_log(httpd->logger, LOGGER_INFO, "Connection closed on socket %d", socket_fd);
}
connection->socket_fd = 0;
}
if (connection->connected) {
connection->connected = 0;
@@ -265,6 +268,7 @@ httpd_accept_connection(httpd_t *httpd, int server_fd, int is_ipv6)
struct sockaddr_storage local_saddr;
socklen_t local_saddrlen;
unsigned char *local, *remote;
unsigned short port;
unsigned int local_zone_id, remote_zone_id;
int local_len, remote_len;
int ret, fd;
@@ -284,10 +288,16 @@ httpd_accept_connection(httpd_t *httpd, int server_fd, int is_ipv6)
return 0;
}
logger_log(httpd->logger, LOGGER_INFO, "Accepted %s client on socket %d",
(is_ipv6 ? "IPv6" : "IPv4"), fd);
local = netutils_get_address(&local_saddr, &local_len, &local_zone_id);
remote = netutils_get_address(&remote_saddr, &remote_len, &remote_zone_id);
local = netutils_get_address(&local_saddr, &local_len, &local_zone_id, &port);
logger_log(httpd->logger, LOGGER_INFO, "Accepted %s client on socket %d, port %u",
(is_ipv6 ? "IPv6" : "IPv4"), fd, port);
remote = netutils_get_address(&remote_saddr, &remote_len, &remote_zone_id, NULL);
// is it correct that ipv6 link-local local and remote zone id should be the same, as asserted below?
if (local_zone_id != remote_zone_id) {
logger_log(httpd->logger, LOGGER_INFO, "ipv6 zone_id mismatch: local_zone_id = %u, remote_zone_id = %u",
local_zone_id, remote_zone_id);
}
assert (local_zone_id == remote_zone_id);
ret = httpd_add_connection(httpd, fd, local, local_len, remote, remote_len, local_zone_id);
@@ -342,7 +352,7 @@ httpd_thread(void *arg)
struct timeval tv;
int nfds=0;
int ret;
int new_request;
int new_request;
MUTEX_LOCK(httpd->run_mutex);
if (!httpd->running) {
@@ -388,7 +398,9 @@ httpd_thread(void *arg)
/* Timeout happened */
continue;
} else if (ret == -1) {
logger_log(httpd->logger, LOGGER_ERR, "httpd error in select: %d %s", errno, strerror(errno));
int sock_err = SOCKET_GET_ERROR();
logger_log(httpd->logger, LOGGER_ERR,
"httpd error in select: %d %s", sock_err, SOCKET_ERROR_STRING(sock_err));
break;
}
@@ -430,11 +442,11 @@ httpd_thread(void *arg)
if (connection->type == CONNECTION_TYPE_PTTH) {
http_request_is_reverse(connection->request);
}
logger_log(httpd->logger, LOGGER_DEBUG, "new request, connection %d, socket %d type %s",
logger_log(httpd->logger, LOGGER_DEBUG, "new request, connection %d, socket %d type %s",
i, connection->socket_fd, typename [connection->type]);
} else {
new_request = 0;
}
}
logger_log(httpd->logger, LOGGER_DEBUG, "httpd receiving on socket %d, connection %d",
connection->socket_fd, i);
@@ -442,54 +454,64 @@ httpd_thread(void *arg)
logger_log(httpd->logger, LOGGER_DEBUG,"\nhttpd: current connections:");
for (int i = 0; i < httpd->max_connections; i++) {
http_connection_t *connection = &httpd->connections[i];
if(!connection->connected) {
if (!connection->connected || !connection->socket_fd) {
continue;
}
if (!FD_ISSET(connection->socket_fd, &rfds)) {
logger_log(httpd->logger, LOGGER_DEBUG, "connection %d type %d socket %d conn %p %s", i,
connection->type, connection->socket_fd,
connection->user_data, typename [connection->type]);
} else {
logger_log(httpd->logger, LOGGER_DEBUG, "connection %d type %d socket %d conn %p %s ACTIVE CONNECTION",
i, connection->type, connection->socket_fd, connection->user_data, typename [connection->type]);
} else {
logger_log(httpd->logger, LOGGER_DEBUG, "connection %d type %d socket %d conn %p %s ACTIVE CONNECTION",
i, connection->type, connection->socket_fd, connection->user_data, typename [connection->type]);
}
}
logger_log(httpd->logger, LOGGER_DEBUG, " ");
}
logger_log(httpd->logger, LOGGER_DEBUG, " ");
}
/* reverse-http responses from the client must not be sent to the llhttp parser:
* such messages start with "HTTP/1.1" */
if (new_request) {
int readstart = 0;
new_request = 0;
while (readstart < 8) {
if (!connection->socket_fd) {
break;
}
ret = recv(connection->socket_fd, buffer + readstart, sizeof(buffer) - 1 - readstart, 0);
if (ret == 0) {
logger_log(httpd->logger, LOGGER_INFO, "Connection closed for socket %d",
logger_log(httpd->logger, LOGGER_DEBUG, "client closed connection on socket %d",
connection->socket_fd);
break;
} else if (ret == -1) {
if (errno == EAGAIN) {
continue;
} else {
int sock_err = SOCKET_GET_ERROR();
logger_log(httpd->logger, LOGGER_ERR, "httpd: recv socket error %d:%s",
sock_err, SOCKET_ERROR_STRING(sock_err));
break;
}
if (errno == EAGAIN) {
continue;
} else {
int sock_err = SOCKET_GET_ERROR();
logger_log(httpd->logger, LOGGER_ERR, "httpd: recv error %d on socket %d: %s",
sock_err, connection->socket_fd, SOCKET_ERROR_STRING(sock_err));
break;
}
} else {
readstart += ret;
ret = readstart;
}
}
if (!connection->socket_fd) {
/* connection was recently removed */
continue;
}
if (!memcmp(buffer, http, 8)) {
http_request_set_reverse(connection->request);
}
} else {
ret = recv(connection->socket_fd, buffer, sizeof(buffer) - 1, 0);
if (ret == 0) {
logger_log(httpd->logger, LOGGER_INFO, "Connection closed for socket %d",
connection->socket_fd);
httpd_remove_connection(httpd, connection);
if (connection->socket_fd) {
ret = recv(connection->socket_fd, buffer, sizeof(buffer) - 1, 0);
if (ret == 0) {
httpd_remove_connection(httpd, connection);
continue;
}
} else {
/* connection was recently removed */
continue;
}
}

View File

@@ -53,7 +53,7 @@ netutils_cleanup()
}
unsigned char *
netutils_get_address(void *sockaddr, int *length, unsigned int *zone_id)
netutils_get_address(void *sockaddr, int *length, unsigned int *zone_id, unsigned short *port)
{
unsigned char ipv4_prefix[] = { 0,0,0,0,0,0,0,0,0,0,255,255 };
struct sockaddr *address = sockaddr;
@@ -66,11 +66,17 @@ netutils_get_address(void *sockaddr, int *length, unsigned int *zone_id)
*zone_id = 0;
sin = (struct sockaddr_in *)address;
*length = sizeof(sin->sin_addr.s_addr);
if (port) {
*port = ntohs(sin->sin_port);
}
return (unsigned char *)&sin->sin_addr.s_addr;
} else if (address->sa_family == AF_INET6) {
struct sockaddr_in6 *sin6;
sin6 = (struct sockaddr_in6 *)address;
if (port) {
*port = ntohs(sin6->sin6_port);
}
if (!memcmp(sin6->sin6_addr.s6_addr, ipv4_prefix, 12)) {
/* Actually an embedded IPv4 address */
*zone_id = 0;

View File

@@ -19,7 +19,7 @@ int netutils_init();
void netutils_cleanup();
int netutils_init_socket(unsigned short *port, int use_ipv6, int use_udp);
unsigned char *netutils_get_address(void *sockaddr, int *length, unsigned int *zone_id);
unsigned char *netutils_get_address(void *sockaddr, int *length, unsigned int *zone_id, unsigned short *port);
int netutils_parse_address(int family, const char *src, void *dst, int dstlen);
#endif

View File

@@ -188,26 +188,35 @@ conn_request(void *ptr, http_request_t *request, http_response_t **response) {
All requests arriving here have been parsed by llhttp to obtain
method | url | protocol (RTSP/1.0 or HTTP/1.1)
There are three types of connections supplying these requests:
There are four types of connections supplying these requests:
Connections from the AirPlay client:
(1) type RAOP connections with CSeq seqence header, and no X-Apple-Session-ID header
(2) type AIRPLAY connection with an X-Apple-Sequence-ID header and no Cseq header
Connections from localhost:
(3) type HLS internal connections from the local HLS server (gstreamer) at localhost with neither
of these headers, but a Host: localhost:[port] header. method = GET.
(4) a special RAOP connection trigggered by a Bluetooth LE beacon: Protocol RTSP/1.0, method: GET
url /info?txtAirPlay?txtRAOP, and no headers including CSeq
*/
const char *method = http_request_get_method(request);
const char *url = http_request_get_url(request);
const char *protocol = http_request_get_protocol(request);
if (!method || !url) {
if (!method || !url || !protocol) {
return;
}
/* this rejects messages from _airplay._tcp for video streaming protocol unless bool raop->hls_support is true*/
/* ¨idenitfy if request is a response to a BLE beaconn */
const char *cseq = http_request_get_header(request, "CSeq");
const char *protocol = http_request_get_protocol(request);
if (!cseq && !conn->raop->hls_support) {
bool ble = false;
if (!strcmp(protocol,"RTSP/1.0") && !cseq && (strstr(url, "txtAirPlay") || strstr(url, "txtRAOP") )) {
logger_log(conn->raop->logger, LOGGER_INFO, "response to Bluetooth LE beacon advertisement received)");
ble = true;
}
/* this rejects messages from _airplay._tcp for video streaming protocol unless bool raop->hls_support is true*/
if (!cseq && !conn->raop->hls_support && !ble) {
logger_log(conn->raop->logger, LOGGER_INFO, "ignoring AirPlay video streaming request (use option -hls to activate HLS support)");
return;
}
@@ -217,7 +226,7 @@ conn_request(void *ptr, http_request_t *request, http_response_t **response) {
hls_request = (host && !cseq && !client_session_id);
if (conn->connection_type == CONNECTION_TYPE_UNKNOWN) {
if (cseq) {
if (cseq || ble) {
if (httpd_count_connection_type(conn->raop->httpd, CONNECTION_TYPE_RAOP)) {
char ipaddr[40];
utils_ipaddress_to_string(conn->remotelen, conn->remote, conn->zone_id, ipaddr, (int) (sizeof(ipaddr)));
@@ -225,8 +234,8 @@ conn_request(void *ptr, http_request_t *request, http_response_t **response) {
logger_log(conn->raop->logger, LOGGER_INFO, "\"nohold\" feature: switch to new connection request from %s", ipaddr);
if (conn->raop->callbacks.video_reset) {
conn->raop->callbacks.video_reset(conn->raop->callbacks.cls);
}
httpd_remove_known_connections(conn->raop->httpd);
}
httpd_remove_known_connections(conn->raop->httpd);
} else {
logger_log(conn->raop->logger, LOGGER_WARNING, "rejecting new connection request from %s", ipaddr);
*response = http_response_create();
@@ -245,7 +254,7 @@ conn_request(void *ptr, http_request_t *request, http_response_t **response) {
conn->client_session_id = (char *) malloc(len);
strncpy(conn->client_session_id, client_session_id, len);
/* airplay video has been requested: shut down any running RAOP udp services */
raop_conn_t *raop_conn = (raop_conn_t *) httpd_get_connection_by_type(conn->raop->httpd, CONNECTION_TYPE_RAOP, 1);
raop_conn_t *raop_conn = (raop_conn_t *) httpd_get_connection_by_type(conn->raop->httpd, CONNECTION_TYPE_RAOP, 1);
if (raop_conn) {
raop_rtp_mirror_t *raop_rtp_mirror = raop_conn->raop_rtp_mirror;
if (raop_rtp_mirror) {
@@ -273,7 +282,7 @@ conn_request(void *ptr, http_request_t *request, http_response_t **response) {
httpd_set_connection_type(conn->raop->httpd, ptr, CONNECTION_TYPE_HLS);
conn->connection_type = CONNECTION_TYPE_HLS;
} else {
logger_log(conn->raop->logger, LOGGER_WARNING, "connection from unknown connection type");
logger_log(conn->raop->logger, LOGGER_WARNING, "connection from unknown connection type");
}
}
@@ -310,9 +319,9 @@ conn_request(void *ptr, http_request_t *request, http_response_t **response) {
if (request_data && logger_debug) {
if (request_datalen > 0) {
/* logger has a buffer limit of 4096 */
if (data_is_plist) {
plist_t req_root_node = NULL;
plist_from_bin(request_data, request_datalen, &req_root_node);
if (data_is_plist) {
plist_t req_root_node = NULL;
plist_from_bin(request_data, request_datalen, &req_root_node);
char * plist_xml = NULL;
char * stripped_xml = NULL;
uint32_t plist_len;
@@ -353,7 +362,7 @@ conn_request(void *ptr, http_request_t *request, http_response_t **response) {
if (!strcmp(method, "POST")) {
if (!strcmp(url, "/feedback")) {
handler = &raop_handler_feedback;
} else if (!strcmp(url, "/pair-pin-start")) {
} else if (!strcmp(url, "/pair-pin-start")) {
handler = &raop_handler_pairpinstart;
} else if (!strcmp(url, "/pair-setup-pin")) {
handler = &raop_handler_pairsetup_pin;
@@ -363,13 +372,11 @@ conn_request(void *ptr, http_request_t *request, http_response_t **response) {
handler = &raop_handler_pairverify;
} else if (!strcmp(url, "/fp-setup")) {
handler = &raop_handler_fpsetup;
} else if (!strcmp(url, "/getProperty")) {
handler = &http_handler_get_property;
} else if (!strcmp(url, "/audioMode")) {
//handler = &http_handler_audioMode;
handler = &raop_handler_audiomode;
}
} else if (!strcmp(method, "GET")) {
if (!strcmp(url, "/info")) {
if (strstr(url, "/info")) {
handler = &raop_handler_info;
}
} else if (!strcmp(method, "OPTIONS")) {
@@ -388,7 +395,7 @@ conn_request(void *ptr, http_request_t *request, http_response_t **response) {
handler = &raop_handler_teardown;
} else {
http_response_init(*response, protocol, 501, "Not Implemented");
}
}
} else if (!hls_request && !strcmp(protocol, "HTTP/1.1")) {
if (!strcmp(method, "POST")) {
if (!strcmp(url, "/reverse")) {
@@ -415,9 +422,9 @@ conn_request(void *ptr, http_request_t *request, http_response_t **response) {
handler = &http_handler_playback_info;
}
} else if (!strcmp(method, "PUT")) {
if (!strncmp (url, "/setProperty?", strlen("/setProperty?"))) {
if (!strncmp (url, "/setProperty?", strlen("/setProperty?"))) {
handler = &http_handler_set_property;
}
}
}
} else if (hls_request) {
handler = &http_handler_hls;
@@ -426,8 +433,8 @@ conn_request(void *ptr, http_request_t *request, http_response_t **response) {
if (handler != NULL) {
handler(conn, request, *response, &response_data, &response_datalen);
} else {
logger_log(conn->raop->logger, LOGGER_INFO,
"Unhandled Client Request: %s %s %s", method, url, protocol);
logger_log(conn->raop->logger, LOGGER_INFO,
"Unhandled Client Request: %s %s %s", method, url, protocol);
}
finish:;
@@ -435,7 +442,7 @@ conn_request(void *ptr, http_request_t *request, http_response_t **response) {
http_response_add_header(*response, "Server", "AirTunes/"GLOBAL_VERSION);
if (cseq) {
http_response_add_header(*response, "CSeq", cseq);
}
}
}
http_response_finish(*response, response_data, response_datalen);
@@ -488,7 +495,7 @@ conn_request(void *ptr, http_request_t *request, http_response_t **response) {
}
if (response_data) {
free(response_data);
}
}
}
}
@@ -646,10 +653,10 @@ raop_destroy(raop_t *raop) {
pairing_destroy(raop->pairing);
httpd_destroy(raop->httpd);
logger_destroy(raop->logger);
if (raop->nonce) {
if (raop->nonce) {
free(raop->nonce);
}
if (raop->random_pw) {
if (raop->random_pw) {
free(raop->random_pw);
}

View File

@@ -12,7 +12,7 @@
* Lesser General Public License for more details.
*
*===================================================================
* modified by fduncanh 2021-23
* modified by fduncanh 2021-25
*/
#ifndef RAOP_H
@@ -84,9 +84,10 @@ struct raop_callbacks_s {
void (*audio_set_coverart)(void *cls, const void *buffer, int buflen);
void (*audio_stop_coverart_rendering) (void* cls);
void (*audio_remote_control_id)(void *cls, const char *dacp_id, const char *active_remote_header);
void (*audio_set_progress)(void *cls, unsigned int start, unsigned int curr, unsigned int end);
void (*audio_set_progress)(void *cls, uint32_t *start, uint32_t *curr, uint32_t *end);
void (*audio_get_format)(void *cls, unsigned char *ct, unsigned short *spf, bool *usingScreen, bool *isMedia, uint64_t *audioFormat);
void (*video_report_size)(void *cls, float *width_source, float *height_source, float *width, float *height);
void (*mirror_video_activity)(void *cls, double *txusage);
void (*report_client_request) (void *cls, char *deviceid, char *model, char *name, bool *admit);
void (*display_pin) (void *cls, char * pin);
void (*register_client) (void *cls, const char *device_id, const char *pk_str, const char *name);

View File

@@ -175,7 +175,7 @@ raop_buffer_enqueue(raop_buffer_t *raop_buffer, unsigned char *data, unsigned sh
return -1;
}
/* before time is synchronized, some empty data packets are sent */
if (datalen == 16 && !memcmp(&data[12], empty_packet_marker, 4)) {
if (datalen == 12 || (datalen == 16 && !memcmp(&data[12], empty_packet_marker, 4))) {
return 0;
}
int payload_size = datalen - 12;

View File

@@ -22,15 +22,24 @@
#include "utils.h"
#include <ctype.h>
#include <stdlib.h>
#include <inttypes.h>
#include <plist/plist.h>
#define AUDIO_SAMPLE_RATE 44100 /* all supported AirPlay audio format use this sample rate */
#define SECOND_IN_USECS 1000000
#define SECOND_IN_NSECS 1000000000
#define MAX_PW_ATTEMPTS 5
#define MAX_PW_ATTEMPTS 3
typedef void (*raop_handler_t)(raop_conn_t *, http_request_t *,
http_response_t *, char **, int *);
#ifndef PLIST_230
void plist_mem_free(void* ptr) {
if (ptr) {
free(ptr);
}
}
#endif
static void
raop_handler_info(raop_conn_t *conn,
http_request_t *request, http_response_t *response,
@@ -38,11 +47,27 @@ raop_handler_info(raop_conn_t *conn,
{
assert(conn->raop->dnssd);
#if 0
/* initial GET/info request sends plist with string "txtAirPlay" */
bool txtAirPlay = false;
/* There are three possible RTSP/1.0 GET/info requests
(1) with a CSeq number and with a plist
(2) with a CSeq number and without a plist
(3) without a CSeq number or a Header (part of Bluetooth LE Service Discovery) */
const char* url = NULL;
const char* content_type = NULL;
const char* cseq = NULL;
url = http_request_get_url(request);
content_type = http_request_get_header(request, "Content-Type");
cseq = http_request_get_header(request, "CSeq");
int len;
bool add_txt_airplay = false;
bool add_txt_raop = false;
const char txtRAOP[] = "txtRAOP";
const char txtAirPlay[] = "txtAirPlay";
plist_t res_node = plist_new_dict();
/* initial GET/info request sends plist with string "txtAirPlay" */
if (content_type && strstr(content_type, "application/x-apple-binary-plist")) {
char *qualifier_string = NULL;
const char *data = NULL;
@@ -56,17 +81,40 @@ raop_handler_info(raop_conn_t *conn,
plist_t req_string_node = plist_array_get_item(req_qualifier_node, 0);
plist_get_string_val(req_string_node, &qualifier_string);
}
if (qualifier_string && !strcmp(qualifier_string, "txtAirPlay")) {
printf("qualifier: %s\n", qualifier_string);
txtAirPlay = true;
if (qualifier_string) {
if (!strcmp(qualifier_string, txtAirPlay )) {
add_txt_airplay = true;
} else if (!strcmp(qualifier_string, txtRAOP)) {
add_txt_raop = true;
}
plist_mem_free(qualifier_string);
}
if (qualifier_string) {
free(qualifier_string);
}
}
#endif
plist_t res_node = plist_new_dict();
/* Bluetoth LE discovery protocol request */
if (!cseq) {
add_txt_airplay = (bool) strstr(url, txtAirPlay);
add_txt_raop = (bool) strstr(url, txtRAOP);
}
if (add_txt_airplay) {
const char *txt = dnssd_get_airplay_txt(conn->raop->dnssd, &len);
plist_t txt_airplay_node = plist_new_data(txt, len);
plist_dict_set_item(res_node, txtAirPlay, txt_airplay_node);
}
if (add_txt_raop) {
const char *txt = dnssd_get_raop_txt(conn->raop->dnssd, &len);
plist_t txt_raop_node = plist_new_data(txt, len);
plist_dict_set_item(res_node, txtRAOP, txt_raop_node);
}
/* don't need anything below here in the response to initial "txtAirPlay" GET/info request */
if (content_type) {
goto finished;
}
/* deviceID is the physical hardware address, and will not change */
int hw_addr_raw_len = 0;
const char *hw_addr_raw = dnssd_get_hw_addr(conn->raop->dnssd, &hw_addr_raw_len);
@@ -76,17 +124,16 @@ raop_handler_info(raop_conn_t *conn,
plist_t device_id_node = plist_new_string(hw_addr);
plist_dict_set_item(res_node, "deviceID", device_id_node);
plist_t mac_address_node = plist_new_string(hw_addr);
plist_dict_set_item(res_node, "macAddress", mac_address_node);
free(hw_addr);
/* Persistent Public Key */
int pk_len = 0;
char *pk = utils_parse_hex(conn->raop->pk_str, strlen(conn->raop->pk_str), &pk_len);
plist_t pk_node = plist_new_data(pk, pk_len);
plist_dict_set_item(res_node, "pk", pk_node);
/* airplay_txt is from the _airplay._tcp dnssd announuncement, may not be necessary */
int airplay_txt_len = 0;
const char *airplay_txt = dnssd_get_airplay_txt(conn->raop->dnssd, &airplay_txt_len);
plist_t txt_airplay_node = plist_new_data(airplay_txt, airplay_txt_len);
plist_dict_set_item(res_node, "txtAirPlay", txt_airplay_node);
free(pk);
uint64_t features = dnssd_get_airplay_features(conn->raop->dnssd);
plist_t features_node = plist_new_uint(features);
@@ -97,6 +144,32 @@ raop_handler_info(raop_conn_t *conn,
plist_t name_node = plist_new_string(name);
plist_dict_set_item(res_node, "name", name_node);
plist_t pi_node = plist_new_string(AIRPLAY_PI);
plist_dict_set_item(res_node, "pi", pi_node);
plist_t vv_node = plist_new_uint(strtol(AIRPLAY_VV, NULL, 10));
plist_dict_set_item(res_node, "vv", vv_node);
plist_t status_flags_node = plist_new_uint(68);
plist_dict_set_item(res_node, "statusFlags", status_flags_node);
plist_t keep_alive_low_power_node = plist_new_uint(1);
plist_dict_set_item(res_node, "keepAliveLowPower", keep_alive_low_power_node);
plist_t source_version_node = plist_new_string(GLOBAL_VERSION);
plist_dict_set_item(res_node, "sourceVersion", source_version_node);
plist_t keep_alive_send_stats_as_body_node = plist_new_bool(1);
plist_dict_set_item(res_node, "keepAliveSendStatsAsBody", keep_alive_send_stats_as_body_node);
plist_t model_node = plist_new_string(GLOBAL_MODEL);
plist_dict_set_item(res_node, "model", model_node);
/* dont need anything below here in the Bluetooth LE response */
if (cseq == NULL) {
goto finished;
}
plist_t audio_latencies_node = plist_new_array();
plist_t audio_latencies_0_node = plist_new_dict();
plist_t audio_latencies_0_output_latency_micros_node = plist_new_bool(0);
@@ -141,30 +214,6 @@ raop_handler_info(raop_conn_t *conn,
plist_array_append_item(audio_formats_node, audio_format_1_node);
plist_dict_set_item(res_node, "audioFormats", audio_formats_node);
plist_t pi_node = plist_new_string(AIRPLAY_PI);
plist_dict_set_item(res_node, "pi", pi_node);
plist_t vv_node = plist_new_uint(strtol(AIRPLAY_VV, NULL, 10));
plist_dict_set_item(res_node, "vv", vv_node);
plist_t status_flags_node = plist_new_uint(68);
plist_dict_set_item(res_node, "statusFlags", status_flags_node);
plist_t keep_alive_low_power_node = plist_new_uint(1);
plist_dict_set_item(res_node, "keepAliveLowPower", keep_alive_low_power_node);
plist_t source_version_node = plist_new_string(GLOBAL_VERSION);
plist_dict_set_item(res_node, "sourceVersion", source_version_node);
plist_t keep_alive_send_stats_as_body_node = plist_new_bool(1);
plist_dict_set_item(res_node, "keepAliveSendStatsAsBody", keep_alive_send_stats_as_body_node);
plist_t model_node = plist_new_string(GLOBAL_MODEL);
plist_dict_set_item(res_node, "model", model_node);
plist_t mac_address_node = plist_new_string(hw_addr);
plist_dict_set_item(res_node, "macAddress", mac_address_node);
plist_t displays_node = plist_new_array();
plist_t displays_0_node = plist_new_dict();
plist_t displays_0_width_physical_node = plist_new_uint(0);
@@ -195,11 +244,10 @@ raop_handler_info(raop_conn_t *conn,
plist_array_append_item(displays_node, displays_0_node);
plist_dict_set_item(res_node, "displays", displays_node);
finished:
plist_to_bin(res_node, response_data, (uint32_t *) response_datalen);
plist_free(res_node);
http_response_add_header(response, "Content-Type", "application/x-apple-binary-plist");
free(pk);
free(hw_addr);
}
static void
@@ -265,9 +313,9 @@ raop_handler_pairsetup_pin(raop_conn_t *conn,
if (PLIST_IS_STRING(req_method_node) && PLIST_IS_STRING(req_user_node)) {
/* this is the initial pair-setup-pin request */
const char *salt;
char pin[6];
const char *pk;
int len_pk, len_salt;
char pin[6];
const char *pk;
int len_pk, len_salt;
char *method = NULL;
char *user = NULL;
plist_get_string_val(req_method_node, &method);
@@ -279,16 +327,18 @@ raop_handler_pairsetup_pin(raop_conn_t *conn,
plist_free (req_root_node);
return;
}
free (method);
plist_get_string_val(req_user_node, &user);
plist_mem_free(method);
method = NULL;
plist_get_string_val(req_user_node, &user);
logger_log(conn->raop->logger, LOGGER_INFO, "pair-setup-pin: device_id = %s", user);
snprintf(pin, 6, "%04u", conn->raop->pin % 10000);
if (conn->raop->pin < 10000) {
conn->raop->pin = 0;
}
int ret = srp_new_user(conn->session, conn->raop->pairing, (const char *) user,
int ret = srp_new_user(conn->session, conn->raop->pairing, (const char *) user,
(const char *) pin, &salt, &len_salt, &pk, &len_pk);
free(user);
plist_mem_free(user);
user = NULL;
plist_free(req_root_node);
if (ret < 0) {
logger_log(conn->raop->logger, LOGGER_ERR, "failed to create user, err = %d", ret);
@@ -302,19 +352,19 @@ raop_handler_pairsetup_pin(raop_conn_t *conn,
plist_to_bin(res_root_node, response_data, (uint32_t*) response_datalen);
plist_free(res_root_node);
http_response_add_header(response, "Content-Type", "application/x-apple-binary-plist");
return;
return;
} else if (PLIST_IS_DATA(req_pk_node) && PLIST_IS_DATA(req_proof_node)) {
/* this is the second part of pair-setup-pin request */
char *client_pk = NULL;
char *client_proof = NULL;
unsigned char proof[64];
memset(proof, 0, sizeof(proof));
unsigned char proof[64];
memset(proof, 0, sizeof(proof));
uint64_t client_pk_len;
uint64_t client_proof_len;
plist_get_data_val(req_pk_node, &client_pk, &client_pk_len);
plist_get_data_val(req_proof_node, &client_proof, &client_proof_len);
if (logger_debug) {
char *str = utils_data_to_string((const unsigned char *) client_proof, client_proof_len, 20);
char *str = utils_data_to_string((const unsigned char *) client_proof, client_proof_len, 20);
logger_log(conn->raop->logger, LOGGER_DEBUG, "client SRP6a proof <M> :\n%s", str);
free (str);
}
@@ -339,7 +389,7 @@ raop_handler_pairsetup_pin(raop_conn_t *conn,
plist_to_bin(res_root_node, response_data, (uint32_t*) response_datalen);
plist_free(res_root_node);
http_response_add_header(response, "Content-Type", "application/x-apple-binary-plist");
return;
return;
} else if (PLIST_IS_DATA(req_epk_node) && PLIST_IS_DATA(req_authtag_node)) {
/* this is the third part of pair-setup-pin request */
char *client_epk = NULL;
@@ -347,25 +397,25 @@ raop_handler_pairsetup_pin(raop_conn_t *conn,
uint64_t client_epk_len;
uint64_t client_authtag_len;
unsigned char epk[ED25519_KEY_SIZE];
unsigned char authtag[GCM_AUTHTAG_SIZE];
int ret;
unsigned char authtag[GCM_AUTHTAG_SIZE];
int ret;
plist_get_data_val(req_epk_node, &client_epk, &client_epk_len);
plist_get_data_val(req_authtag_node, &client_authtag, &client_authtag_len);
if (logger_debug) {
if (logger_debug) {
char *str = utils_data_to_string((const unsigned char *) client_epk, client_epk_len, 16);
logger_log(conn->raop->logger, LOGGER_DEBUG, "client_epk %d:\n%s\n", (int) client_epk_len, str);
str = utils_data_to_string((const unsigned char *) client_authtag, client_authtag_len, 16);
logger_log(conn->raop->logger, LOGGER_DEBUG, "client_authtag %d:\n%s\n", (int) client_authtag_len, str);
free (str);
}
}
memcpy(epk, client_epk, ED25519_KEY_SIZE);
memcpy(authtag, client_authtag, GCM_AUTHTAG_SIZE);
memcpy(epk, client_epk, ED25519_KEY_SIZE);
memcpy(authtag, client_authtag, GCM_AUTHTAG_SIZE);
free (client_authtag);
free (client_epk);
plist_free(req_root_node);
ret = srp_confirm_pair_setup(conn->session, conn->raop->pairing, epk, authtag);
ret = srp_confirm_pair_setup(conn->session, conn->raop->pairing, epk, authtag);
if (ret < 0) {
logger_log(conn->raop->logger, LOGGER_ERR, "pair-pin-setup (step 3): client authentication failed\n");
goto authentication_failed;
@@ -375,13 +425,13 @@ raop_handler_pairsetup_pin(raop_conn_t *conn,
pairing_session_set_setup_status(conn->session);
plist_t res_root_node = plist_new_dict();
plist_t res_epk_node = plist_new_data((const char *) epk, 32);
plist_t res_authtag_node = plist_new_data((const char *) authtag, 16);
plist_t res_authtag_node = plist_new_data((const char *) authtag, 16);
plist_dict_set_item(res_root_node, "epk", res_epk_node);
plist_dict_set_item(res_root_node, "authTag", res_authtag_node);
plist_to_bin(res_root_node, response_data, (uint32_t*) response_datalen);
plist_free(res_root_node);
http_response_add_header(response, "Content-Type", "application/x-apple-binary-plist");
return;
return;
}
authentication_failed:;
http_response_init(response, "RTSP/1.0", 470, "Client Authentication Failure");
@@ -439,58 +489,58 @@ raop_handler_pairverify(raop_conn_t *conn,
return;
}
switch (data[0]) {
case 1:
if (datalen != 4 + X25519_KEY_SIZE + ED25519_KEY_SIZE) {
logger_log(conn->raop->logger, LOGGER_ERR, "Invalid pair-verify data");
return;
}
/* We can fall through these errors, the result will just be garbage... */
if (pairing_session_handshake(conn->session, data + 4, data + 4 + X25519_KEY_SIZE)) {
logger_log(conn->raop->logger, LOGGER_ERR, "Error initializing pair-verify handshake");
}
if (pairing_session_get_public_key(conn->session, public_key)) {
logger_log(conn->raop->logger, LOGGER_ERR, "Error getting ECDH public key");
}
if (pairing_session_get_signature(conn->session, signature)) {
logger_log(conn->raop->logger, LOGGER_ERR, "Error getting ED25519 signature");
}
if (register_check) {
bool registered_client = true;
if (conn->raop->callbacks.check_register) {
const unsigned char *pk = data + 4 + X25519_KEY_SIZE;
char *pk64;
ed25519_pk_to_base64(pk, &pk64);
registered_client = conn->raop->callbacks.check_register(conn->raop->callbacks.cls, pk64);
free (pk64);
}
if (!registered_client) {
return;
}
}
*response_data = malloc(sizeof(public_key) + sizeof(signature));
if (*response_data) {
http_response_add_header(response, "Content-Type", "application/octet-stream");
memcpy(*response_data, public_key, sizeof(public_key));
memcpy(*response_data + sizeof(public_key), signature, sizeof(signature));
*response_datalen = sizeof(public_key) + sizeof(signature);
}
break;
case 0:
logger_log(conn->raop->logger, LOGGER_DEBUG, "2nd pair-verify step: checking signature");
if (datalen != 4 + PAIRING_SIG_SIZE) {
logger_log(conn->raop->logger, LOGGER_ERR, "Invalid pair-verify data");
return;
case 1:
if (datalen != 4 + X25519_KEY_SIZE + ED25519_KEY_SIZE) {
logger_log(conn->raop->logger, LOGGER_ERR, "Invalid pair-verify data");
return;
}
/* We can fall through these errors, the result will just be garbage... */
if (pairing_session_handshake(conn->session, data + 4, data + 4 + X25519_KEY_SIZE)) {
logger_log(conn->raop->logger, LOGGER_ERR, "Error initializing pair-verify handshake");
}
if (pairing_session_get_public_key(conn->session, public_key)) {
logger_log(conn->raop->logger, LOGGER_ERR, "Error getting ECDH public key");
}
if (pairing_session_get_signature(conn->session, signature)) {
logger_log(conn->raop->logger, LOGGER_ERR, "Error getting ED25519 signature");
}
if (register_check) {
bool registered_client = true;
if (conn->raop->callbacks.check_register) {
const unsigned char *pk = data + 4 + X25519_KEY_SIZE;
char *pk64;
ed25519_pk_to_base64(pk, &pk64);
registered_client = conn->raop->callbacks.check_register(conn->raop->callbacks.cls, pk64);
free (pk64);
}
if (pairing_session_finish(conn->session, data + 4)) {
logger_log(conn->raop->logger, LOGGER_ERR, "Incorrect pair-verify signature");
http_response_set_disconnect(response, 1);
if (!registered_client) {
return;
}
logger_log(conn->raop->logger, LOGGER_DEBUG, "pair-verify: signature is verified");
}
*response_data = malloc(sizeof(public_key) + sizeof(signature));
if (*response_data) {
http_response_add_header(response, "Content-Type", "application/octet-stream");
break;
memcpy(*response_data, public_key, sizeof(public_key));
memcpy(*response_data + sizeof(public_key), signature, sizeof(signature));
*response_datalen = sizeof(public_key) + sizeof(signature);
}
break;
case 0:
logger_log(conn->raop->logger, LOGGER_DEBUG, "2nd pair-verify step: checking signature");
if (datalen != 4 + PAIRING_SIG_SIZE) {
logger_log(conn->raop->logger, LOGGER_ERR, "Invalid pair-verify data");
return;
}
if (pairing_session_finish(conn->session, data + 4)) {
logger_log(conn->raop->logger, LOGGER_ERR, "Incorrect pair-verify signature");
http_response_set_disconnect(response, 1);
return;
}
logger_log(conn->raop->logger, LOGGER_DEBUG, "pair-verify: signature is verified");
http_response_add_header(response, "Content-Type", "application/octet-stream");
break;
}
}
@@ -593,15 +643,21 @@ raop_handler_setup(raop_conn_t *conn,
/* RFC2617 Digest authentication (md5 hash) of uxplay client-access password, if set */
if (!conn->authenticated && conn->raop->callbacks.passwd) {
size_t pin_len = 4;
if (conn->raop->random_pw && strncmp(conn->raop->random_pw + pin_len + 1, deviceID, 17)) {
conn->raop->auth_fail_count = MAX_PW_ATTEMPTS;
const char *authorization = NULL;
authorization = http_request_get_header(request, "Authorization");
if (!authorization) {
// if random_pw is set, but client has changed, unset it
if (conn->raop->random_pw && strncmp(conn->raop->random_pw + pin_len + 1, deviceID, 17)) {
free(conn->raop->random_pw);
conn->raop->random_pw = NULL;
}
}
int len;
const char *password = conn->raop->callbacks.passwd(conn->raop->callbacks.cls, &len);
// len = -1 means use a random password for this connection; len = 0 means no password
if (len == -1 && conn->raop->random_pw && conn->raop->auth_fail_count >= MAX_PW_ATTEMPTS) {
// change random_pw after MAX_PW_ATTEMPTS failed authentication attempts
logger_log(conn->raop->logger, LOGGER_INFO, "Too many authentication failures or new client: generate new random password");
logger_log(conn->raop->logger, LOGGER_INFO, "Too many authentication failures: generate new random password");
free(conn->raop->random_pw);
conn->raop->random_pw = NULL;
}
@@ -612,16 +668,19 @@ raop_handler_setup(raop_conn_t *conn,
logger_log(conn->raop->logger, LOGGER_ERR, "Failed to generate random pin");
pin_4 = 1234;
}
conn->raop->random_pw = (char *) malloc(pin_len + 1 + 18);
conn->raop->random_pw = (char *) malloc(pin_len + 1 + 18);
char *pin = conn->raop->random_pw;
snprintf(pin, pin_len + 1, "%04u", pin_4 % 10000);
pin[pin_len] = '\0';
snprintf(pin + pin_len + 1, 18, "%s", deviceID);
conn->raop->auth_fail_count = 0;
}
if (len == -1 && !authorization && conn->raop->random_pw) {
if (conn->raop->callbacks.display_pin) {
conn->raop->callbacks.display_pin(conn->raop->callbacks.cls, pin);
conn->raop->callbacks.display_pin(conn->raop->callbacks.cls, conn->raop->random_pw);
}
logger_log(conn->raop->logger, LOGGER_INFO, "*** CLIENT MUST NOW ENTER PIN = \"%s\" AS AIRPLAY PASSWORD", pin);
logger_log(conn->raop->logger, LOGGER_INFO, "*** CLIENT MUST NOW ENTER PIN = \"%s\" AS AIRPLAY PASSWORD", conn->raop->random_pw);
conn->raop->auth_fail_count++;
}
if (len && !conn->authenticated) {
if (len == -1) {
@@ -629,20 +688,17 @@ raop_handler_setup(raop_conn_t *conn,
}
char nonce_string[33] = { '\0' };
//bool stale = false; //not implemented
const char *authorization = NULL;
authorization = http_request_get_header(request, "Authorization");
if (authorization) {
if (len && authorization) {
char *ptr = strstr(authorization, "nonce=\"") + strlen("nonce=\"");
strncpy(nonce_string, ptr, 32);
const char *method = http_request_get_method(request);
conn->authenticated = pairing_digest_verify(method, authorization, password);
if (!conn->authenticated) {
conn->raop->auth_fail_count++;
logger_log(conn->raop->logger, LOGGER_INFO, "*** authentication failure: count = %u", conn->raop->auth_fail_count);
if (conn->raop->callbacks.display_pin && conn->raop->auth_fail_count > 1) {
conn->raop->callbacks.display_pin(conn->raop->callbacks.cls, conn->raop->random_pw);
// if random_pw is used, the auth_fail_count will be the number of times it is displayed after creation
if (len != -1) {
conn->raop->auth_fail_count++;
}
logger_log(conn->raop->logger, LOGGER_INFO, "*** CLIENT MUST NOW ENTER PIN = \"%s\" AS AIRPLAY PASSWORD", conn->raop->random_pw);
logger_log(conn->raop->logger, LOGGER_INFO, "*** authentication failure: count = %u", conn->raop->auth_fail_count);
}
if (conn->authenticated) {
//printf("initial authenticatication OK\n");
@@ -655,7 +711,7 @@ raop_handler_setup(raop_conn_t *conn,
if (conn->authenticated && conn->raop->random_pw) {
free (conn->raop->random_pw);
conn->raop->random_pw = NULL;
}
}
if (conn->raop->nonce) {
free(conn->raop->nonce);
conn->raop->nonce = NULL;
@@ -685,17 +741,17 @@ raop_handler_setup(raop_conn_t *conn,
char* eiv = NULL;
uint64_t eiv_len = 0;
char *model = NULL;
char *model = NULL;
char *name = NULL;
bool admit_client = true;
plist_t req_model_node = plist_dict_get_item(req_root_node, "model");
plist_get_string_val(req_model_node, &model);
plist_t req_name_node = plist_dict_get_item(req_root_node, "name");
plist_get_string_val(req_name_node, &name);
if (conn->raop->callbacks.report_client_request) {
if (conn->raop->callbacks.report_client_request) {
conn->raop->callbacks.report_client_request(conn->raop->callbacks.cls, deviceID, model, name, &admit_client);
}
if (admit_client && deviceID && name && conn->raop->callbacks.register_client) {
if (admit_client && deviceID && name && conn->raop->callbacks.register_client) {
char *client_device_id = NULL;
char *client_pk = NULL; /* encoded as null-terminated base64 string, must be freed*/
get_pairing_session_client_data(conn->session, &client_device_id, &client_pk);
@@ -703,35 +759,29 @@ raop_handler_setup(raop_conn_t *conn,
conn->raop->callbacks.register_client(conn->raop->callbacks.cls, client_device_id, client_pk, name);
free (client_pk);
}
}
if (deviceID) {
free (deviceID);
deviceID = NULL;
}
if (model) {
free (model);
model = NULL;
}
if (name) {
free (name);
name = NULL;
}
plist_mem_free(deviceID);
deviceID = NULL;
plist_mem_free(model);
model = NULL;
plist_mem_free(name);
name = NULL;
if (admit_client == false) {
/* client is not authorized to connect */
plist_free(res_root_node);
plist_free(req_root_node);
return;
}
}
plist_get_data_val(req_eiv_node, &eiv, &eiv_len);
memcpy(aesiv, eiv, 16);
free(eiv);
logger_log(conn->raop->logger, LOGGER_DEBUG, "eiv_len = %llu", eiv_len);
if (logger_debug) {
if (logger_debug) {
char* str = utils_data_to_string(aesiv, 16, 16);
logger_log(conn->raop->logger, LOGGER_DEBUG, "16 byte aesiv (needed for AES-CBC audio decryption iv):\n%s", str);
free(str);
}
}
char* ekey = NULL;
uint64_t ekey_len = 0;
@@ -740,7 +790,7 @@ raop_handler_setup(raop_conn_t *conn,
free(ekey);
logger_log(conn->raop->logger, LOGGER_DEBUG, "ekey_len = %llu", ekey_len);
// eaeskey is 72 bytes, aeskey is 16 bytes
if (logger_debug) {
if (logger_debug) {
char *str = utils_data_to_string((unsigned char *) eaeskey, ekey_len, 16);
logger_log(conn->raop->logger, LOGGER_DEBUG, "ekey:\n%s", str);
free (str);
@@ -759,6 +809,7 @@ raop_handler_setup(raop_conn_t *conn,
bool old_protocol = false;
#ifdef OLD_PROTOCOL_CLIENT_USER_AGENT_LIST /* set in global.h */
if (strstr(OLD_PROTOCOL_CLIENT_USER_AGENT_LIST, user_agent)) old_protocol = true;
if (strstr(user_agent, "AirMyPC")) old_protocol = true; //AirMyPC/7200 still uses old protocol: unlikely to change (?)
#endif
if (old_protocol) { /* some windows AirPlay-client emulators use old AirPlay 1 protocol with unhashed AES key */
logger_log(conn->raop->logger, LOGGER_INFO, "Client identifed as using old protocol (unhashed) AES audio key)");
@@ -808,9 +859,9 @@ raop_handler_setup(raop_conn_t *conn,
logger_log(conn->raop->logger, LOGGER_ERR, "Client specified AirPlay2 \"Remote Control\" protocol\n"
" Only AirPlay v1 protocol (using NTP and timing port) is supported");
}
}
}
char *timing_protocol = NULL;
timing_protocol_t time_protocol = TP_NONE;
timing_protocol_t time_protocol = TP_NONE;
plist_t req_timing_protocol_node = plist_dict_get_item(req_root_node, "timingProtocol");
plist_get_string_val(req_timing_protocol_node, &timing_protocol);
if (timing_protocol) {
@@ -826,7 +877,7 @@ raop_handler_setup(raop_conn_t *conn,
logger_log(conn->raop->logger, LOGGER_ERR, "Client specified timingProtocol=%s,"
" but timingProtocol= NTP is required here", timing_protocol);
}
free (timing_protocol);
plist_mem_free (timing_protocol);
timing_protocol = NULL;
} else {
logger_log(conn->raop->logger, LOGGER_DEBUG, "Client did not specify timingProtocol,"
@@ -835,7 +886,7 @@ raop_handler_setup(raop_conn_t *conn,
}
uint64_t timing_rport = 0;
plist_t req_timing_port_node = plist_dict_get_item(req_root_node, "timingPort");
if (req_timing_port_node) {
if (req_timing_port_node) {
plist_get_uint_val(req_timing_port_node, &timing_rport);
}
if (timing_rport) {
@@ -891,106 +942,107 @@ raop_handler_setup(raop_conn_t *conn,
logger_log(conn->raop->logger, LOGGER_DEBUG, "type = %llu", type);
switch (type) {
case 110: {
// Mirroring
unsigned short dport = conn->raop->mirror_data_lport;
plist_t stream_id_node = plist_dict_get_item(req_stream_node, "streamConnectionID");
uint64_t stream_connection_id = 0;
plist_get_uint_val(stream_id_node, &stream_connection_id);
logger_log(conn->raop->logger, LOGGER_DEBUG, "streamConnectionID (needed for AES-CTR video decryption"
" key and iv): %llu", stream_connection_id);
case 110: {
// Mirroring
unsigned short dport = conn->raop->mirror_data_lport;
plist_t stream_id_node = plist_dict_get_item(req_stream_node, "streamConnectionID");
uint64_t stream_connection_id = 0;
plist_get_uint_val(stream_id_node, &stream_connection_id);
logger_log(conn->raop->logger, LOGGER_DEBUG, "streamConnectionID (needed for AES-CTR video decryption"
" key and iv): %llu", stream_connection_id);
if (conn->raop_rtp_mirror) {
raop_rtp_mirror_init_aes(conn->raop_rtp_mirror, &stream_connection_id);
raop_rtp_mirror_start(conn->raop_rtp_mirror, &dport, conn->raop->clientFPSdata);
logger_log(conn->raop->logger, LOGGER_DEBUG, "Mirroring initialized successfully");
} else {
logger_log(conn->raop->logger, LOGGER_ERR, "Mirroring not initialized at SETUP, playing will fail!");
http_response_set_disconnect(response, 1);
}
plist_t res_stream_node = plist_new_dict();
plist_t res_stream_data_port_node = plist_new_uint(dport);
plist_t res_stream_type_node = plist_new_uint(110);
plist_dict_set_item(res_stream_node, "dataPort", res_stream_data_port_node);
plist_dict_set_item(res_stream_node, "type", res_stream_type_node);
plist_array_append_item(res_streams_node, res_stream_node);
break;
} case 96: {
// Audio
unsigned short cport = conn->raop->control_lport, dport = conn->raop->data_lport;
unsigned short remote_cport = 0;
unsigned char ct = 0;
unsigned int sr = AUDIO_SAMPLE_RATE; /* all AirPlay audio formats supported so far have sample rate 44.1kHz */
uint64_t uint_val = 0;
plist_t req_stream_control_port_node = plist_dict_get_item(req_stream_node, "controlPort");
plist_get_uint_val(req_stream_control_port_node, &uint_val);
remote_cport = (unsigned short) uint_val; /* must != 0 to activate audio resend requests */
plist_t req_stream_ct_node = plist_dict_get_item(req_stream_node, "ct");
plist_get_uint_val(req_stream_ct_node, &uint_val);
ct = (unsigned char) uint_val;
if (conn->raop->callbacks.audio_get_format) {
/* get additional audio format parameters */
uint64_t audioFormat = 0;
unsigned short spf = 0;
bool isMedia = false;
bool usingScreen = false;
uint8_t bool_val = 0;
plist_t req_stream_spf_node = plist_dict_get_item(req_stream_node, "spf");
plist_get_uint_val(req_stream_spf_node, &uint_val);
spf = (unsigned short) uint_val;
plist_t req_stream_audio_format_node = plist_dict_get_item(req_stream_node, "audioFormat");
plist_get_uint_val(req_stream_audio_format_node, &audioFormat);
plist_t req_stream_is_media_node = plist_dict_get_item(req_stream_node, "isMedia");
if (req_stream_is_media_node) {
plist_get_bool_val(req_stream_is_media_node, &bool_val);
isMedia = (bool) bool_val;
} else {
isMedia = false;
}
plist_t req_stream_using_screen_node = plist_dict_get_item(req_stream_node, "usingScreen");
if (req_stream_using_screen_node) {
plist_get_bool_val(req_stream_using_screen_node, &bool_val);
usingScreen = (bool) bool_val;
} else {
usingScreen = false;
}
conn->raop->callbacks.audio_get_format(conn->raop->callbacks.cls, &ct, &spf, &usingScreen, &isMedia, &audioFormat);
}
if (conn->raop_rtp) {
raop_rtp_start_audio(conn->raop_rtp, &remote_cport, &cport, &dport, &ct, &sr);
logger_log(conn->raop->logger, LOGGER_DEBUG, "RAOP initialized success");
} else {
logger_log(conn->raop->logger, LOGGER_ERR, "RAOP not initialized at SETUP, playing will fail!");
http_response_set_disconnect(response, 1);
}
plist_t res_stream_node = plist_new_dict();
plist_t res_stream_data_port_node = plist_new_uint(dport);
plist_t res_stream_control_port_node = plist_new_uint(cport);
plist_t res_stream_type_node = plist_new_uint(96);
plist_dict_set_item(res_stream_node, "dataPort", res_stream_data_port_node);
plist_dict_set_item(res_stream_node, "controlPort", res_stream_control_port_node);
plist_dict_set_item(res_stream_node, "type", res_stream_type_node);
plist_array_append_item(res_streams_node, res_stream_node);
break;
if (conn->raop_rtp_mirror) {
raop_rtp_mirror_init_aes(conn->raop_rtp_mirror, &stream_connection_id);
raop_rtp_mirror_start(conn->raop_rtp_mirror, &dport, conn->raop->clientFPSdata);
logger_log(conn->raop->logger, LOGGER_DEBUG, "Mirroring initialized successfully");
} else {
logger_log(conn->raop->logger, LOGGER_ERR, "Mirroring not initialized at SETUP, playing will fail!");
http_response_set_disconnect(response, 1);
}
default:
logger_log(conn->raop->logger, LOGGER_ERR, "SETUP tries to setup stream of unknown type %llu", type);
plist_t res_stream_node = plist_new_dict();
plist_t res_stream_data_port_node = plist_new_uint(dport);
plist_t res_stream_type_node = plist_new_uint(110);
plist_dict_set_item(res_stream_node, "dataPort", res_stream_data_port_node);
plist_dict_set_item(res_stream_node, "type", res_stream_type_node);
plist_array_append_item(res_streams_node, res_stream_node);
break;
}
case 96: {
// Audio
unsigned short cport = conn->raop->control_lport, dport = conn->raop->data_lport;
unsigned short remote_cport = 0;
unsigned char ct = 0;
unsigned int sr = AUDIO_SAMPLE_RATE; /* all AirPlay audio formats supported so far have sample rate 44.1kHz */
uint64_t uint_val = 0;
plist_t req_stream_control_port_node = plist_dict_get_item(req_stream_node, "controlPort");
plist_get_uint_val(req_stream_control_port_node, &uint_val);
remote_cport = (unsigned short) uint_val; /* must != 0 to activate audio resend requests */
plist_t req_stream_ct_node = plist_dict_get_item(req_stream_node, "ct");
plist_get_uint_val(req_stream_ct_node, &uint_val);
ct = (unsigned char) uint_val;
if (conn->raop->callbacks.audio_get_format) {
/* get additional audio format parameters */
uint64_t audioFormat = 0;
unsigned short spf = 0;
bool isMedia = false;
bool usingScreen = false;
uint8_t bool_val = 0;
plist_t req_stream_spf_node = plist_dict_get_item(req_stream_node, "spf");
plist_get_uint_val(req_stream_spf_node, &uint_val);
spf = (unsigned short) uint_val;
plist_t req_stream_audio_format_node = plist_dict_get_item(req_stream_node, "audioFormat");
plist_get_uint_val(req_stream_audio_format_node, &audioFormat);
plist_t req_stream_is_media_node = plist_dict_get_item(req_stream_node, "isMedia");
if (req_stream_is_media_node) {
plist_get_bool_val(req_stream_is_media_node, &bool_val);
isMedia = (bool) bool_val;
} else {
isMedia = false;
}
plist_t req_stream_using_screen_node = plist_dict_get_item(req_stream_node, "usingScreen");
if (req_stream_using_screen_node) {
plist_get_bool_val(req_stream_using_screen_node, &bool_val);
usingScreen = (bool) bool_val;
} else {
usingScreen = false;
}
conn->raop->callbacks.audio_get_format(conn->raop->callbacks.cls, &ct, &spf, &usingScreen, &isMedia, &audioFormat);
}
if (conn->raop_rtp) {
raop_rtp_start_audio(conn->raop_rtp, &remote_cport, &cport, &dport, &ct, &sr);
logger_log(conn->raop->logger, LOGGER_DEBUG, "RAOP initialized success");
} else {
logger_log(conn->raop->logger, LOGGER_ERR, "RAOP not initialized at SETUP, playing will fail!");
http_response_set_disconnect(response, 1);
break;
}
plist_t res_stream_node = plist_new_dict();
plist_t res_stream_data_port_node = plist_new_uint(dport);
plist_t res_stream_control_port_node = plist_new_uint(cport);
plist_t res_stream_type_node = plist_new_uint(96);
plist_dict_set_item(res_stream_node, "dataPort", res_stream_data_port_node);
plist_dict_set_item(res_stream_node, "controlPort", res_stream_control_port_node);
plist_dict_set_item(res_stream_node, "type", res_stream_type_node);
plist_array_append_item(res_streams_node, res_stream_node);
break;
}
default:
logger_log(conn->raop->logger, LOGGER_ERR, "SETUP tries to setup stream of unknown type %llu", type);
http_response_set_disconnect(response, 1);
break;
}
}
@@ -1030,7 +1082,7 @@ raop_handler_get_parameter(raop_conn_t *conn,
char volume[25] = "volume: 0.0\r\n";
if (conn->raop->callbacks.audio_set_client_volume) {
snprintf(volume, 25, "volume: %9.6f\r\n", conn->raop->callbacks.audio_set_client_volume(conn->raop->callbacks.cls));
}
}
http_response_add_header(response, "Content-Type", "text/parameters");
*response_data = strdup(volume);
if (*response_data) {
@@ -1039,9 +1091,11 @@ raop_handler_get_parameter(raop_conn_t *conn,
return;
}
for (next = current ; (datalen - (next - data) > 0) ; ++next)
if (*next == '\r')
for (next = current ; (datalen - (next - data) > 0) ; ++next) {
if (*next == '\r') {
break;
}
}
if ((datalen - (next - data) >= 2) && !strncmp(next, "\r\n", 2)) {
if ((next - current) > 0) {
@@ -1081,8 +1135,8 @@ raop_handler_set_parameter(raop_conn_t *conn,
sscanf(datastr+8, "%f", &vol);
raop_rtp_set_volume(conn->raop_rtp, vol);
} else if ((datalen >= 10) && !strncmp(datastr, "progress: ", 10)) {
unsigned int start, curr, end;
sscanf(datastr+10, "%u/%u/%u", &start, &curr, &end);
uint32_t start, curr, end;
sscanf(datastr+10, "%"PRIu32"/%"PRIu32"/%"PRIu32, &start, &curr, &end);
raop_rtp_set_progress(conn->raop_rtp, start, curr, end);
}
} else if (!conn->raop_rtp) {
@@ -1106,6 +1160,25 @@ raop_handler_set_parameter(raop_conn_t *conn,
}
}
static void
raop_handler_audiomode(raop_conn_t *conn,
http_request_t *request, http_response_t *response,
char **response_data, int *response_datalen)
{
const char *data = NULL;
char *audiomode = NULL;
int data_len;
data = http_request_get_data(request, &data_len);
plist_t req_root_node = NULL;
plist_from_bin(data, data_len, &req_root_node);
plist_t req_audiomode_node = plist_dict_get_item(req_root_node, "audioMode");
plist_get_string_val(req_audiomode_node, &audiomode);
/* not sure what should be done with this request: usually audioMode requested is "default" */
int log_level = (strstr(audiomode, "default") ? LOGGER_DEBUG : LOGGER_INFO);
logger_log(conn->raop->logger, log_level, "Unhandled RTSP request \"audioMode: %s\"", audiomode);
plist_mem_free(audiomode);
plist_free(req_root_node);
}
static void
raop_handler_feedback(raop_conn_t *conn,
@@ -1164,11 +1237,6 @@ raop_handler_teardown(raop_conn_t *conn,
data = http_request_get_data(request, &data_len);
plist_t req_root_node = NULL;
plist_from_bin(data, data_len, &req_root_node);
char * plist_xml;
uint32_t plist_len;
plist_to_xml(req_root_node, &plist_xml, &plist_len);
logger_log(conn->raop->logger, LOGGER_DEBUG, "%s", plist_xml);
free(plist_xml);
plist_t req_streams_node = plist_dict_get_item(req_root_node, "streams");
/* Process stream teardown requests */
if (PLIST_IS_ARRAY(req_streams_node)) {
@@ -1181,14 +1249,11 @@ raop_handler_teardown(raop_conn_t *conn,
if (val == 96) {
teardown_96 = true;
} else if (val == 110) {
teardown_110 = true;
teardown_110 = true;
}
}
}
plist_free(req_root_node);
if (conn->raop->callbacks.conn_teardown) {
conn->raop->callbacks.conn_teardown(conn->raop->callbacks.cls, &teardown_96, &teardown_110);
}
logger_log(conn->raop->logger, LOGGER_DEBUG, "TEARDOWN request, 96=%d, 110=%d", teardown_96, teardown_110);
http_response_add_header(response, "Connection", "close");
@@ -1218,7 +1283,10 @@ raop_handler_teardown(raop_conn_t *conn,
raop_rtp_mirror_destroy(conn->raop_rtp_mirror);
conn->raop_rtp_mirror = NULL;
}
/* shut down any HLS connections */
httpd_remove_connections_by_type(conn->raop->httpd, CONNECTION_TYPE_HLS);
}
/* shut down any HLS connections */
httpd_remove_connections_by_type(conn->raop->httpd, CONNECTION_TYPE_HLS);
}
if (conn->raop->callbacks.conn_teardown) {
conn->raop->callbacks.conn_teardown(conn->raop->callbacks.cls, &teardown_96, &teardown_110);
}
}

View File

@@ -352,7 +352,7 @@ raop_ntp_thread(void *arg)
(double) t2 / SECOND_IN_NSECS, str);
free(str);
}
// The iOS client device sends its time in seconds relative to an arbitrary Epoch (the last boot).
// The iOS client device sends its time in seconds relative to an arbitrary Epoch (the last boot).
// For a little bonus confusion, they add SECONDS_FROM_1900_TO_1970.
// This means we have to expect some rather huge offset, but its growth or shrink over time should be small.

View File

@@ -93,9 +93,9 @@ struct raop_rtp_s {
int coverart_len;
char *dacp_id;
char *active_remote_header;
unsigned int progress_start;
unsigned int progress_curr;
unsigned int progress_end;
uint32_t progress_start;
uint32_t progress_curr;
uint32_t progress_end;
int progress_changed;
int flush;
@@ -195,7 +195,6 @@ raop_rtp_init(logger_t *logger, raop_callbacks_t *callbacks, raop_ntp_t *ntp, co
return raop_rtp;
}
void
raop_rtp_destroy(raop_rtp_t *raop_rtp)
{
@@ -288,9 +287,9 @@ raop_rtp_process_events(raop_rtp_t *raop_rtp, void *cb_data)
int coverart_len;
char *dacp_id;
char *active_remote_header;
unsigned int progress_start;
unsigned int progress_curr;
unsigned int progress_end;
uint32_t progress_start;
uint32_t progress_curr;
uint32_t progress_end;
int progress_changed;
assert(raop_rtp);
@@ -379,7 +378,7 @@ raop_rtp_process_events(raop_rtp_t *raop_rtp, void *cb_data)
if (progress_changed) {
if (raop_rtp->callbacks.audio_set_progress) {
raop_rtp->callbacks.audio_set_progress(raop_rtp->callbacks.cls, progress_start, progress_curr, progress_end);
raop_rtp->callbacks.audio_set_progress(raop_rtp->callbacks.cls, &progress_start, &progress_curr, &progress_end);
}
}
return 0;
@@ -430,6 +429,19 @@ raop_rtp_thread_udp(void *arg)
logger_log(raop_rtp->logger, LOGGER_DEBUG, "raop_rtp start_time = %8.6f (raop_rtp audio)",
((double) raop_rtp->ntp_start_time) / SEC);
char type[8] = {'\0'};
switch (raop_rtp->ct) {
case 2:
snprintf(type, 8, "ALAC");
break;
case 8:
snprintf(type, 8, "AAC_ELD");
break;
default:
snprintf(type, 8, "TYPE=?");
break;
}
while(1) {
fd_set rfds;
struct timeval tv;
@@ -474,7 +486,7 @@ raop_rtp_thread_udp(void *arg)
raop_rtp->control_saddr_len = saddrlen;
got_remote_control_saddr = true;
}
} else {
} else {
packetlen = recvfrom(raop_rtp->csock, (char *)packet, sizeof(packet), 0, NULL, NULL);
}
int type_c = packet[1] & ~0x80;
@@ -515,7 +527,7 @@ raop_rtp_thread_udp(void *arg)
} else {
client_ntp_sync_prev = raop_rtp->client_ntp_sync;
rtp_sync_prev = raop_rtp->rtp_sync;
}
}
raop_rtp->rtp_sync = byteutils_get_int_be(packet, 4);
uint64_t sync_ntp_raw = byteutils_get_long_be(packet, 8);
raop_rtp->client_ntp_sync = raop_remote_timestamp_to_nano_seconds(raop_rtp->ntp, sync_ntp_raw);
@@ -564,20 +576,20 @@ raop_rtp_thread_udp(void *arg)
* three times; the secnum and rtp_timestamp increment according to the same pattern as
* AAC-ELD packets with audio content.*/
/* When the ALAC audio stream starts, the initial packets are length-44 packets with
* the same 32-byte encrypted payload which after decryption is the beginning of a
* 32-byte ALAC packet, presumably with format information, but not actual audio data.
* The secnum and rtp_timestamp in the packet header increment according to the same
* pattern as ALAC packets with audio content */
/* When the ALAC audio stream starts, the initial packets are length-44 packets with
* the same 32-byte encrypted payload which after decryption is the beginning of a
* 32-byte ALAC packet, presumably with format information, but not actual audio data.
* The secnum and rtp_timestamp in the packet header increment according to the same
* pattern as ALAC packets with audio content */
/* The first ALAC packet with data seems to be decoded just before the first sync event
* so its dequeuing should be delayed until the first rtp sync has occurred */
/* The first ALAC packet with data seems to be decoded just before the first sync event
* so its dequeuing should be delayed until the first rtp sync has occurred */
if (FD_ISSET(raop_rtp->dsock, &rfds)) {
if (!raop_rtp->initial_sync && !video_arrival_offset) {
if (FD_ISSET(raop_rtp->dsock, &rfds)) {
if (!raop_rtp->initial_sync && !video_arrival_offset) {
video_arrival_offset = raop_ntp_get_video_arrival_offset(raop_rtp->ntp);
}
}
//logger_log(raop_rtp->logger, LOGGER_INFO, "Would have data packet in queue");
// Receiving audio data here
saddrlen = sizeof(saddr);
@@ -594,30 +606,30 @@ raop_rtp_thread_udp(void *arg)
free (str);
}
continue;
}
}
if (!raop_rtp->initial_sync && raop_rtp->ct == 8 && video_arrival_offset) {
if (!raop_rtp->initial_sync && raop_rtp->ct == 8 && video_arrival_offset) {
/* estimate a fake initial remote timestamp for video synchronization with AAC audio before the first rtp sync */
uint64_t ts = raop_ntp_get_local_time() - video_arrival_offset;
double delay = DELAY_AAC;
ts += (uint64_t) (delay * SEC);
raop_rtp->client_ntp_sync = ts;
raop_rtp->rtp_sync = byteutils_get_int_be(packet, 4);
raop_rtp->initial_sync = true;
}
uint64_t ts = raop_ntp_get_local_time() - video_arrival_offset;
double delay = DELAY_AAC;
ts += (uint64_t) (delay * SEC);
raop_rtp->client_ntp_sync = ts;
raop_rtp->rtp_sync = byteutils_get_int_be(packet, 4);
raop_rtp->initial_sync = true;
}
if (packetlen == 16 && memcmp(packet + 12, no_data_marker, 4) == 0) {
if (packetlen == 12 ||(packetlen == 16 && memcmp(packet + 12, no_data_marker, 4) == 0)) {
/* this is a "no data" packet */
/* the first such packet could be used to provide the initial rtptime and seqnum formerly given in the RECORD request */
continue;
}
if (raop_rtp->ct == 2 && packetlen == 44) continue; /* ignore the ALAC packets with format information only. */
if (raop_rtp->ct == 2 && packetlen == 44) continue; /* ignore the ALAC packets with format information only. */
int result = raop_buffer_enqueue(raop_rtp->buffer, packet, packetlen, 1);
assert(result >= 0);
if (!raop_rtp->initial_sync) {
if (!raop_rtp->initial_sync) {
/* wait until the first sync before dequeing ALAC */
continue;
} else {
@@ -641,9 +653,9 @@ raop_rtp_thread_udp(void *arg)
uint64_t ntp_now = raop_ntp_get_local_time();
int64_t latency = (audio_data.ntp_time_local ? ((int64_t) ntp_now) - ((int64_t) audio_data.ntp_time_local) : 0);
logger_log(raop_rtp->logger, LOGGER_DEBUG,
"raop_rtp audio: now = %8.6f, ntp = %8.6f, latency = %9.6f, ts = %8.6f, rtp_time=%u seqnum = %u",
"raop_rtp audio: now = %8.6f, ntp = %8.6f, latency = %9.6f, ts = %8.6f, rtp_time=%u seqnum = %5u %s %d",
(double) ntp_now / SEC, (double) audio_data.ntp_time_local / SEC, (double) latency / SEC,
(double) audio_data.ntp_time_remote /SEC, rtp_timestamp, seqnum);
(double) audio_data.ntp_time_remote /SEC, rtp_timestamp, seqnum, type, payload_size);
}
raop_rtp->callbacks.audio_process(raop_rtp->callbacks.cls, raop_rtp->ntp, &audio_data);
@@ -793,7 +805,7 @@ raop_rtp_remote_control_id(raop_rtp_t *raop_rtp, const char *dacp_id, const char
}
void
raop_rtp_set_progress(raop_rtp_t *raop_rtp, unsigned int start, unsigned int curr, unsigned int end)
raop_rtp_set_progress(raop_rtp_t *raop_rtp, uint32_t start, uint32_t curr, uint32_t end)
{
assert(raop_rtp);

View File

@@ -39,7 +39,7 @@ void raop_rtp_set_volume(raop_rtp_t *raop_rtp, float volume);
void raop_rtp_set_metadata(raop_rtp_t *raop_rtp, const char *data, int datalen);
void raop_rtp_set_coverart(raop_rtp_t *raop_rtp, const char *data, int datalen);
void raop_rtp_remote_control_id(raop_rtp_t *raop_rtp, const char *dacp_id, const char *active_remote_header);
void raop_rtp_set_progress(raop_rtp_t *raop_rtp, unsigned int start, unsigned int curr, unsigned int end);
void raop_rtp_set_progress(raop_rtp_t *raop_rtp, uint32_t start, uint32_t curr, uint32_t end);
void raop_rtp_flush(raop_rtp_t *raop_rtp, int next_seq);
void raop_rtp_stop(raop_rtp_t *raop_rtp);
int raop_rtp_is_running(raop_rtp_t *raop_rtp);

View File

@@ -330,13 +330,13 @@ raop_rtp_mirror_thread(void *arg)
/*packet[0:3] contains the payload size */
int payload_size = byteutils_get_int(packet, 0);
char packet_description[13] = {0};
char *p = packet_description;
char *p = packet_description;
int n = sizeof(packet_description);
for (int i = 4; i < 8; i++) {
for (int i = 4; i < 8; i++) {
snprintf(p, n, "%2.2x ", (unsigned int) packet[i]);
n -= 3;
p += 3;
}
}
ntp_timestamp_raw = byteutils_get_long(packet, 8);
ntp_timestamp_remote = raop_ntp_timestamp_to_nano_seconds(ntp_timestamp_raw, false);
if (first_packet) {
@@ -345,14 +345,14 @@ raop_rtp_mirror_thread(void *arg)
first_packet = false;
}
/* packet[4] + packet[5] identify the payload type: values seen are: *
/* packet[4] + packet[5] identify the payload type: values seen are: *
* 0x00 0x00: encrypted packet containing a non-IDR type 1 VCL NAL unit *
* 0x00 0x10: encrypted packet containing an IDR type 5 VCL NAL unit *
* 0x01 0x00: unencrypted packet containing a type 7 SPS NAL + a type 8 PPS NAL unit *
* 0x02 0x00: unencrypted packet (old protocol) no payload, sent once every second *
* 0x05 0x00 unencrypted packet with a "streaming report", sent once per second. */
/* packet[6] + packet[7] may list a payload "option": values seen are: *
/* packet[6] + packet[7] may list a payload "option": values seen are: *
* 0x00 0x00 : encrypted and "streaming report" packets *
* 0x1e 0x00 : old protocol (seen in AirMyPC) no-payload once-per-second packets *
* 0x16 0x01 : seen in most unencrypted h264 SPS+PPS packets *
@@ -395,7 +395,7 @@ raop_rtp_mirror_thread(void *arg)
break;
}
switch (packet[4]) {
switch (packet[4]) {
case 0x00:
// Normal video data (VCL NAL)
@@ -409,13 +409,13 @@ raop_rtp_mirror_thread(void *arg)
uint64_t ntp_now = raop_ntp_get_local_time();
int64_t latency = (ntp_timestamp_local ? ((int64_t) ntp_now) - ((int64_t) ntp_timestamp_local) : 0);
logger_log(raop_rtp_mirror->logger, LOGGER_DEBUG,
"raop_rtp video: now = %8.6f, ntp = %8.6f, latency = %9.6f, ts = %8.6f, %s %s",
"raop_rtp video: now = %8.6f, ntp = %8.6f, latency = %9.6f, ts = %8.6f, %s %s, size: %d",
(double) ntp_now / SEC, (double) ntp_timestamp_local / SEC, (double) latency / SEC,
(double) ntp_timestamp_remote / SEC, packet_description, h265_video ? h265 : h264);
(double) ntp_timestamp_remote / SEC, packet_description, (h265_video ? h265 : h264), payload_size);
}
unsigned char* payload_out;
unsigned char* payload_decrypted;
unsigned char* payload_decrypted;
/*
* nal_types:1 Coded non-partitioned slice of a non-IDR picture
* 5 Coded non-partitioned slice of an IDR picture
@@ -437,7 +437,7 @@ raop_rtp_mirror_thread(void *arg)
"raop_rtp_mirror: prepended sps_pps timestamp does not match timestamp of "
"video payload\n%llu\n%llu , discarding", ntp_timestamp_raw, ntp_timestamp_nal);
free (sps_pps);
sps_pps = NULL;
sps_pps = NULL;
prepend_sps_pps = false;
}
@@ -447,12 +447,12 @@ raop_rtp_mirror_thread(void *arg)
payload_decrypted = payload_out + sps_pps_len;
memcpy(payload_out, sps_pps, sps_pps_len);
free (sps_pps);
sps_pps = NULL;
sps_pps = NULL;
} else {
payload_out = (unsigned char*) malloc(payload_size);
payload_decrypted = payload_out;
}
// Decrypt data
// Decrypt data: AES-CTR encryption/decryption does not change the size of the data
mirror_buffer_decrypt(raop_rtp_mirror->buffer, payload, payload_decrypted, payload_size);
// It seems the AirPlay protocol prepends NALs with their size, which we're replacing with the 4-byte
@@ -474,11 +474,11 @@ raop_rtp_mirror_thread(void *arg)
valid_data = false;
break;
}
int nalu_type;
if (h265_video) {
int nalu_type;
if (h265_video) {
nalu_type = payload_decrypted[nalu_size] & 0x7e >> 1;;
//logger_log(raop_rtp_mirror->logger, LOGGER_DEBUG," h265 video, NALU type %d, size %d", nalu_type, nc_len);
} else {
} else {
nalu_type = payload_decrypted[nalu_size] & 0x1f;
int ref_idc = (payload_decrypted[nalu_size] >> 5);
switch (nalu_type) {
@@ -486,7 +486,7 @@ raop_rtp_mirror_thread(void *arg)
case 5: /*IDR, slice_layer_without_partitioning */
case 1: /*non-IDR, slice_layer_without_partitioning */
break;
case 2: /* slice data partition A */
case 2: /* slice data partition A */
case 3: /* slice data partition B */
case 4: /* slice data partition C */
logger_log(raop_rtp_mirror->logger, LOGGER_INFO,
@@ -526,9 +526,9 @@ raop_rtp_mirror_thread(void *arg)
"unexpected non-VCL NAL unit: nalu_type = %d, ref_idc = %d, nalu_size = %d,"
"processed bytes %d, payloadsize = %d nalus_count = %d",
nalu_type, ref_idc, nc_len, nalu_size, payload_size, nalus_count);
break;
}
}
break;
}
}
nalu_size += nc_len;
}
if (nalu_size != payload_size) valid_data = false;
@@ -540,7 +540,7 @@ raop_rtp_mirror_thread(void *arg)
payload_decrypted = NULL;
video_decode_struct video_data;
video_data.is_h265 = h265_video;
video_data.is_h265 = h265_video;
video_data.ntp_time_local = ntp_timestamp_local;
video_data.ntp_time_remote = ntp_timestamp_remote;
video_data.nal_count = nalus_count; /*nal_count will be the number of nal units in the packet */
@@ -571,13 +571,13 @@ raop_rtp_mirror_thread(void *arg)
bytes 56-59 width
bytes 60-63 height
bytes 64-127 all 0x0
*/
*/
// The information in the payload contains an SPS and a PPS NAL
// The sps_pps is not encrypted
logger_log(raop_rtp_mirror->logger, LOGGER_DEBUG, "\nReceived unencrypted codec packet from client:"
" payload_size %d header %s ts_client = %8.6f",
payload_size, packet_description, (double) ntp_timestamp_remote / SEC);
payload_size, packet_description, (double) ntp_timestamp_remote / SEC);
if (packet[6] == 0x56 || packet[6] == 0x5e) {
logger_log(raop_rtp_mirror->logger, LOGGER_DEBUG, "This packet indicates video stream is stopping");
@@ -614,7 +614,7 @@ raop_rtp_mirror_thread(void *arg)
logger_log(raop_rtp_mirror->logger, LOGGER_DEBUG, "raop_rtp_mirror width_source = %f height_source = %f width = %f height = %f",
width_source, height_source, width, height);
if (payload_size == 0) {
if (payload_size == 0) {
logger_log(raop_rtp_mirror->logger, LOGGER_ERR, "raop_rtp_mirror: received type 0x01 packet with no payload:\n"
"this indicates non-h264 video but Airplay features bit 42 (SupportsScreenMultiCodec) is not set\n"
"use startup option \"-h265\" to set this bit and support h265 (4K) video");
@@ -625,7 +625,7 @@ raop_rtp_mirror_thread(void *arg)
free(sps_pps);
sps_pps = NULL;
}
/* test for a H265 VPS/SPS/PPS */
/* test for a H265 VPS/SPS/PPS */
unsigned char hvc1[] = { 0x68, 0x76, 0x63, 0x31 };
if (!memcmp(payload + 4, hvc1, 4)) {
@@ -677,7 +677,7 @@ raop_rtp_mirror_thread(void *arg)
break;
}
sps_size = byteutils_get_short_be(ptr, 3);
ptr += 5;
ptr += 5;
sps = ptr;
if (logger_debug) {
char *str = utils_data_to_string(sps, sps_size, 16);
@@ -690,12 +690,12 @@ raop_rtp_mirror_thread(void *arg)
raop_rtp_mirror->callbacks.video_pause(raop_rtp_mirror->callbacks.cls);
break;
}
pps_size = byteutils_get_short_be(ptr, 3);
pps_size = byteutils_get_short_be(ptr, 3);
ptr += 5;
pps = ptr;
if (logger_debug) {
char *str = utils_data_to_string(pps, pps_size, 16);
logger_log(raop_rtp_mirror->logger, LOGGER_INFO, "h265 pps size %d\n%s",pps_size, str);
logger_log(raop_rtp_mirror->logger, LOGGER_INFO, "h265 pps size %d\n%s",pps_size, str);
free(str);
}
@@ -788,6 +788,7 @@ raop_rtp_mirror_thread(void *arg)
logger_log(raop_rtp_mirror->logger, LOGGER_DEBUG, "\nReceived old-protocol once-per-second packet from client:"
" payload_size %d header %s ts_raw = %llu", payload_size, packet_description, ntp_timestamp_raw);
/* "old protocol" (used by AirMyPC), rest of 128-byte packet is empty */
break;
case 0x05:
logger_log(raop_rtp_mirror->logger, LOGGER_DEBUG, "\nReceived video streaming performance info packet from client:"
" payload_size %d header %s ts_raw = %llu", payload_size, packet_description, ntp_timestamp_raw);
@@ -795,14 +796,14 @@ raop_rtp_mirror_thread(void *arg)
* Sometimes (e.g, when the client has a locked screen), there is a 25kB trailer attached to the packet. *
* This 25000 Byte trailer with unidentified content seems to be the same data each time it is sent. */
if (payload_size && raop_rtp_mirror->show_client_FPS_data) {
if (payload_size) {
//char *str = utils_data_to_string(packet, 128, 16);
//logger_log(raop_rtp_mirror->logger, LOGGER_WARNING, "type 5 video packet header:\n%s", str);
//free (str);
int plist_size = payload_size;
if (payload_size > 25000) {
plist_size = payload_size - 25000;
plist_size = payload_size - 25000;
if (logger_debug) {
char *str = utils_data_to_string(payload + plist_size, 16, 16);
logger_log(raop_rtp_mirror->logger, LOGGER_DEBUG,
@@ -815,9 +816,17 @@ raop_rtp_mirror_thread(void *arg)
uint32_t plist_len;
plist_t root_node = NULL;
plist_from_bin((char *) payload, plist_size, &root_node);
plist_to_xml(root_node, &plist_xml, &plist_len);
logger_log(raop_rtp_mirror->logger, LOGGER_INFO, "%s", plist_xml);
free(plist_xml);
if (raop_rtp_mirror->callbacks.mirror_video_activity) {
double txusage = 0.0;
plist_t tx_usage_avg_node = plist_dict_get_item(root_node, "txUsageAvg");
plist_get_real_val(tx_usage_avg_node, &txusage);
raop_rtp_mirror->callbacks.mirror_video_activity(raop_rtp_mirror->callbacks.cls, &txusage);
}
if (raop_rtp_mirror->show_client_FPS_data) {
plist_to_xml(root_node, &plist_xml, &plist_len);
logger_log(raop_rtp_mirror->logger, LOGGER_INFO, "%s", plist_xml);
free(plist_xml);
}
}
}
break;
@@ -848,7 +857,7 @@ raop_rtp_mirror_thread(void *arg)
logger_log(raop_rtp_mirror->logger, LOGGER_DEBUG, "raop_rtp_mirror exiting TCP thread");
if (conn_reset&& raop_rtp_mirror->callbacks.conn_reset) {
raop_rtp_mirror->callbacks.conn_reset(raop_rtp_mirror->callbacks.cls, 1);
raop_rtp_mirror->callbacks.conn_reset(raop_rtp_mirror->callbacks.cls, 1);
}
if (unsupported_codec) {

View File

@@ -37,7 +37,7 @@ typedef struct {
int sync_status;
uint64_t ntp_time_local;
uint64_t ntp_time_remote;
uint64_t rtp_time;
uint32_t rtp_time;
unsigned short seqnum;
} audio_decode_struct;

View File

@@ -312,20 +312,20 @@ char *utils_strip_data_from_plist_xml(char *plist_xml) {
nchars = eol + 1 - ptr1;
memcpy(ptr2, ptr1, nchars);
ptr2 += nchars;
ptr1 += nchars;
ptr1 += nchars;
end = strstr(ptr1, "</data>");
assert(end);
assert(end);
count = 0;
do {
eol_data = eol;
eol = strchr(eol + 1, '\n');
count++;
} while (eol < end);
count--; // last '\n' counted ends the first non-data line (contains "</data>")
if (count > 1) {
count--; // last '\n' counted ends the first non-data line (contains "</data>")
if (count > 1) {
snprintf(line, sizeof(line), " (%d lines data omitted, 64 chars/line)\n", count);
nchars = strlen(line);
memcpy(ptr2, line, nchars);
memcpy(ptr2, line, nchars);
ptr2 += nchars;
ptr1 = eol_data + 1;
} else {

View File

@@ -45,6 +45,7 @@ typedef struct audio_renderer_s {
GstElement *appsrc;
GstElement *pipeline;
GstElement *volume;
GstBus *bus;
unsigned char ct;
} audio_renderer_t ;
static audio_renderer_t *renderer_type[NFORMATS];
@@ -102,15 +103,15 @@ static gboolean check_plugin_feature (const gchar *needed_feature)
plugin_feature = gst_registry_find_feature (registry, needed_feature, GST_TYPE_ELEMENT_FACTORY);
if (!plugin_feature) {
g_print ("Required gstreamer libav plugin feature '%s' not found:\n\n"
"This may be missing because the FFmpeg package used by GStreamer-1.x-libav is incomplete.\n"
"(Some distributions provide an incomplete FFmpeg due to License or Patent issues:\n"
"in such cases a complete version for that distribution is usually made available elsewhere)\n",
needed_feature);
ret = FALSE;
g_print ("Required gstreamer libav plugin feature '%s' not found:\n\n"
"This may be missing because the FFmpeg package used by GStreamer-1.x-libav is incomplete.\n"
"(Some distributions provide an incomplete FFmpeg due to License or Patent issues:\n"
"in such cases a complete version for that distribution is usually made available elsewhere)\n",
needed_feature);
ret = FALSE;
} else {
gst_object_unref (plugin_feature);
plugin_feature = NULL;
gst_object_unref (plugin_feature);
plugin_feature = NULL;
}
if (ret == FALSE) {
g_print ("\nif the plugin feature is installed, but not found, your gstreamer registry may have been corrupted.\n"
@@ -186,7 +187,7 @@ void audio_renderer_init(logger_t *render_logger, const char* audiosink, const b
g_assert (renderer_type[i]->pipeline);
gst_pipeline_use_clock(GST_PIPELINE_CAST(renderer_type[i]->pipeline), clock);
renderer_type[i]->bus = gst_element_get_bus(renderer_type[i]->pipeline);
renderer_type[i]->appsrc = gst_bin_get_by_name (GST_BIN (renderer_type[i]->pipeline), "audio_source");
renderer_type[i]->volume = gst_bin_get_by_name (GST_BIN (renderer_type[i]->pipeline), "volume");
switch (i) {
@@ -367,12 +368,52 @@ void audio_renderer_flush() {
void audio_renderer_destroy() {
audio_renderer_stop();
for (int i = 0; i < NFORMATS ; i++ ) {
gst_object_unref (renderer_type[i]->bus);
renderer_type[i]->bus = NULL;
gst_object_unref (renderer_type[i]->volume);
renderer_type[i]->volume = NULL;
renderer_type[i]->volume = NULL;
gst_object_unref (renderer_type[i]->appsrc);
renderer_type[i]->appsrc = NULL;
gst_object_unref (renderer_type[i]->pipeline);
gst_object_unref (renderer_type[i]->pipeline);
renderer_type[i]->pipeline = NULL;
free(renderer_type[i]);
}
}
static gboolean gstreamer_audio_pipeline_bus_callback(GstBus *bus, GstMessage *message, void *loop) {
switch (GST_MESSAGE_TYPE(message)) {
case GST_MESSAGE_ERROR: {
GError *err;
gchar *debug;
gst_message_parse_error (message, &err, &debug);
logger_log(logger, LOGGER_INFO, "GStreamer error (audio): %s %s", GST_MESSAGE_SRC_NAME(message),err->message);
g_error_free(err);
g_free(debug);
if (renderer->appsrc) {
gst_app_src_end_of_stream (GST_APP_SRC(renderer->appsrc));
}
gst_bus_set_flushing(bus, TRUE);
gst_element_set_state (renderer->pipeline, GST_STATE_READY);
g_main_loop_quit( (GMainLoop *) loop);
break;
}
case GST_MESSAGE_EOS:
logger_log(logger, LOGGER_INFO, "GStreamer: End-Of-Stream (audio)");
break;
case GST_MESSAGE_ELEMENT:
// many "level" messages may be sent
break;
default:
/* unhandled message */
logger_log(logger, LOGGER_DEBUG,"GStreamer unhandled audio bus message: src = %s type = %s",
GST_MESSAGE_SRC_NAME(message), GST_MESSAGE_TYPE_NAME(message));
break;
}
return TRUE;
}
unsigned int audio_renderer_listen(void *loop, int id) {
g_assert(id >= 0 && id < NFORMATS);
return (unsigned int) gst_bus_add_watch(renderer_type[id]->bus,(GstBusFunc)
gstreamer_audio_pipeline_bus_callback, (gpointer) loop);
}

View File

@@ -40,7 +40,7 @@ void audio_renderer_render_buffer(unsigned char* data, int *data_len, unsigned s
void audio_renderer_set_volume(double volume);
void audio_renderer_flush();
void audio_renderer_destroy();
unsigned int audio_renderer_listen(void *loop, int id);
#ifdef __cplusplus
}
#endif

View File

@@ -54,7 +54,10 @@ static gboolean hls_seek_enabled;
static gboolean hls_playing;
static gboolean hls_buffer_empty;
static gboolean hls_buffer_full;
static int type_264;
static int type_265;
static int type_hls;
static int type_jpeg;
typedef enum {
//GST_PLAY_FLAG_VIDEO = (1 << 0),
@@ -178,7 +181,7 @@ void video_renderer_size(float *f_width_source, float *f_height_source, float *f
}
GstElement *make_video_sink(const char *videosink, const char *videosink_options) {
/* used to build a videosink for playbin, using the user-specified string "videosink" */
/* used to build a videosink for playbin, using the user-specified string "videosink" */
GstElement *video_sink = gst_element_factory_make(videosink, "videosink");
if (!video_sink) {
return NULL;
@@ -211,19 +214,20 @@ GstElement *make_video_sink(const char *videosink, const char *videosink_options
pval++;
const gchar *property_name = (const gchar *) token;
const gchar *value = (const gchar *) pval;
g_print("playbin_videosink property: \"%s\" \"%s\"\n", property_name, value);
gst_util_set_object_arg(G_OBJECT (video_sink), property_name, value);
g_print("playbin_videosink property: \"%s\" \"%s\"\n", property_name, value);
gst_util_set_object_arg(G_OBJECT (video_sink), property_name, value);
}
}
free(options);
return video_sink;
}
void video_renderer_init(logger_t *render_logger, const char *server_name, videoflip_t videoflip[2], const char *parser,
void video_renderer_init(logger_t *render_logger, const char *server_name, videoflip_t videoflip[2], const char *parser, const char * rtp_pipeline,
const char *decoder, const char *converter, const char *videosink, const char *videosink_options,
bool initial_fullscreen, bool video_sync, bool h265_support, guint playbin_version, const char *uri) {
bool initial_fullscreen, bool video_sync, bool h265_support, bool coverart_support, guint playbin_version, const char *uri) {
GError *error = NULL;
GstCaps *caps = NULL;
bool rtp = (bool) strlen(rtp_pipeline);
hls_video = (uri != NULL);
/* videosink choices that are auto */
auto_videosink = (strstr(videosink, "autovideosink") || strstr(videosink, "fpsdisplaysink"));
@@ -238,7 +242,10 @@ void video_renderer_init(logger_t *render_logger, const char *server_name, vide
hls_duration = -1;
hls_buffer_empty = TRUE;
hls_buffer_empty = FALSE;
type_hls = -1;
type_264 = -1;
type_265 = -1;
type_jpeg = -1;
/* this call to g_set_application_name makes server_name appear in the X11 display window title bar, */
/* (instead of the program name uxplay taken from (argv[0]). It is only set one time. */
@@ -246,18 +253,22 @@ void video_renderer_init(logger_t *render_logger, const char *server_name, vide
const gchar *appname = g_get_application_name();
if (!appname || strcmp(appname,server_name)) g_set_application_name(server_name);
appname = NULL;
n_renderers = 1;
/* the renderer for hls video will only be built if a HLS uri is provided in
* the call to video_renderer_init, in which case the h264/h265 mirror-mode and jpeg
* audio-mode renderers will not be built. This is because it appears that we cannot
* put playbin into GST_STATE_READY before knowing the uri (?), so cannot use a
* unified renderer structure with h264, h265, jpeg and hls */
if (hls_video) {
n_renderers = 1;
/* renderer[0]: playbin (hls) */
type_hls = 0;
} else {
n_renderers = h265_support ? 3 : 2;
/* renderer[0]: jpeg; [1]: h264; [2]: h265 */
type_264 = 0;
if (h265_support) {
type_265 = n_renderers++;
}
if (coverart_support) {
type_jpeg = n_renderers++;
}
}
g_assert (n_renderers <= NCODECS);
for (int i = 0; i < n_renderers; i++) {
@@ -267,6 +278,7 @@ void video_renderer_init(logger_t *render_logger, const char *server_name, vide
renderer_type[i]->autovideo = auto_videosink;
renderer_type[i]->id = i;
renderer_type[i]->bus = NULL;
renderer_type[i]->appsrc = NULL;
if (hls_video) {
/* use playbin3 to play HLS video: replace "playbin3" by "playbin" to use playbin2 */
switch (playbin_version) {
@@ -282,8 +294,7 @@ void video_renderer_init(logger_t *render_logger, const char *server_name, vide
}
logger_log(logger, LOGGER_INFO, "Will use GStreamer playbin version %u to play HLS streamed video", playbin_version);
g_assert(renderer_type[i]->pipeline);
renderer_type[i]->appsrc = NULL;
renderer_type[i]->codec = hls;
renderer_type[i]->codec = hls;
/* if we are not using an autovideosink, build a videosink based on the string "videosink" */
if (!auto_videosink) {
GstElement *playbin_videosink = make_video_sink(videosink, videosink_options);
@@ -297,59 +308,61 @@ void video_renderer_init(logger_t *render_logger, const char *server_name, vide
gint flags;
g_object_get(renderer_type[i]->pipeline, "flags", &flags, NULL);
flags |= GST_PLAY_FLAG_DOWNLOAD;
flags |= GST_PLAY_FLAG_BUFFERING; // set by default in playbin3, but not in playbin2; is it needed?
flags |= GST_PLAY_FLAG_BUFFERING; // set by default in playbin3, but not in playbin2; is it needed?
g_object_set(renderer_type[i]->pipeline, "flags", flags, NULL);
g_object_set (G_OBJECT (renderer_type[i]->pipeline), "uri", uri, NULL);
g_object_set (G_OBJECT (renderer_type[i]->pipeline), "uri", uri, NULL);
} else {
bool jpeg_pipeline = false;
switch (i) {
case 0:
if (i == type_264) {
renderer_type[i]->codec = h264;
caps = gst_caps_from_string(h264_caps);
} else if (i == type_265) {
renderer_type[i]->codec = h265;
caps = gst_caps_from_string(h265_caps);
} else if (i == type_jpeg) {
jpeg_pipeline = true;
renderer_type[i]->codec = jpeg;
caps = gst_caps_from_string(jpeg_caps);
break;
case 1:
renderer_type[i]->codec = h264;
caps = gst_caps_from_string(h264_caps);
break;
case 2:
renderer_type[i]->codec = h265;
caps = gst_caps_from_string(h265_caps);
break;
default:
} else {
g_assert(0);
}
GString *launch = g_string_new("appsrc name=video_source ! ");
if (jpeg_pipeline) {
if (jpeg_pipeline) {
g_string_append(launch, "jpegdec ");
} else {
} else {
g_string_append(launch, "queue ! ");
g_string_append(launch, parser);
g_string_append(launch, " ! ");
g_string_append(launch, decoder);
if (!rtp) {
g_string_append(launch, decoder);
} else {
g_string_append(launch, "rtph264pay ");
g_string_append(launch, rtp_pipeline);
}
}
g_string_append(launch, " ! ");
append_videoflip(launch, &videoflip[0], &videoflip[1]);
g_string_append(launch, converter);
g_string_append(launch, " ! ");
g_string_append(launch, "videoscale ! ");
if (jpeg_pipeline) {
g_string_append(launch, " imagefreeze allow-replace=TRUE ! ");
if (!rtp || jpeg_pipeline) {
g_string_append(launch, " ! ");
append_videoflip(launch, &videoflip[0], &videoflip[1]);
g_string_append(launch, converter);
g_string_append(launch, " ! ");
g_string_append(launch, "videoscale ! ");
if (jpeg_pipeline) {
g_string_append(launch, " imagefreeze allow-replace=TRUE ! ");
}
g_string_append(launch, videosink);
g_string_append(launch, " name=");
g_string_append(launch, videosink);
g_string_append(launch, "_");
g_string_append(launch, renderer_type[i]->codec);
g_string_append(launch, videosink_options);
if (video_sync && !jpeg_pipeline) {
g_string_append(launch, " sync=true");
sync = true;
} else {
g_string_append(launch, " sync=false");
sync = false;
}
}
g_string_append(launch, videosink);
g_string_append(launch, " name=");
g_string_append(launch, videosink);
g_string_append(launch, "_");
g_string_append(launch, renderer_type[i]->codec);
g_string_append(launch, videosink_options);
if (video_sync && !jpeg_pipeline) {
g_string_append(launch, " sync=true");
sync = true;
} else {
g_string_append(launch, " sync=false");
sync = false;
}
if (!strcmp(renderer_type[i]->codec, h264)) {
char *pos = launch->str;
while ((pos = strstr(pos,h265))){
@@ -370,14 +383,13 @@ void video_renderer_init(logger_t *render_logger, const char *server_name, vide
logger_log(logger, LOGGER_ERR, "GStreamer gst_parse_launch failed to create video pipeline %d\n"
"*** error message from gst_parse_launch was:\n%s\n"
"launch string parsed was \n[%s]", i + 1, error->message, launch->str);
if (strstr(error->message, "no element")) {
if (strstr(error->message, "no element")) {
logger_log(logger, LOGGER_ERR, "This error usually means that a uxplay option was mistyped\n"
" or some requested part of GStreamer is not installed\n");
}
g_clear_error (&error);
}
g_assert (renderer_type[i]->pipeline);
GstClock *clock = gst_system_clock_obtain();
g_object_set(clock, "clock-type", GST_CLOCK_TYPE_REALTIME, NULL);
gst_pipeline_use_clock(GST_PIPELINE_CAST(renderer_type[i]->pipeline), clock);
@@ -386,7 +398,7 @@ void video_renderer_init(logger_t *render_logger, const char *server_name, vide
g_object_set(renderer_type[i]->appsrc, "caps", caps, "stream-type", 0, "is-live", TRUE, "format", GST_FORMAT_TIME, NULL);
g_string_free(launch, TRUE);
gst_caps_unref(caps);
gst_object_unref(clock);
gst_object_unref(clock);
}
#ifdef X_DISPLAY_FIX
use_x11 = (strstr(videosink, "xvimagesink") || strstr(videosink, "ximagesink") || auto_videosink);
@@ -395,9 +407,9 @@ void video_renderer_init(logger_t *render_logger, const char *server_name, vide
renderer_type[i]->gst_window = NULL;
renderer_type[i]->use_x11 = false;
X11_search_attempts = 0;
/* setting char *x11_display_name to NULL means the value is taken from $DISPLAY in the environment
/* setting char *x11_display_name to NULL means the value is taken from $DISPLAY in the environment
* (a uxplay option to specify a different value is possible) */
char *x11_display_name = NULL;
char *x11_display_name = NULL;
if (use_x11) {
if (i == 0) {
renderer_type[0]->gst_window = (X11_Window_t *) calloc(1, sizeof(X11_Window_t));
@@ -417,19 +429,22 @@ void video_renderer_init(logger_t *render_logger, const char *server_name, vide
}
}
#endif
renderer_type[i]->bus = gst_element_get_bus(renderer_type[i]->pipeline);
gst_element_set_state (renderer_type[i]->pipeline, GST_STATE_READY);
GstState state;
if (gst_element_get_state (renderer_type[i]->pipeline, &state, NULL, 100 * GST_MSECOND)) {
if (state == GST_STATE_READY) {
logger_log(logger, LOGGER_DEBUG, "Initialized GStreamer video renderer %d", i + 1);
if (hls_video && i == 0) {
renderer = renderer_type[i];
}
} else {
logger_log(logger, LOGGER_ERR, "Failed to initialize GStreamer video renderer %d", i + 1);
GstStateChangeReturn ret = gst_element_get_state (renderer_type[i]->pipeline, &state, NULL, 100 * GST_MSECOND);
if (ret == GST_STATE_CHANGE_SUCCESS) {
logger_log(logger, LOGGER_DEBUG, "Initialized GStreamer video renderer %d", i + 1);
if (hls_video && i == 0) {
renderer = renderer_type[i];
}
} else {
logger_log(logger, LOGGER_ERR, "Failed to initialize GStreamer video renderer %d", i + 1);
logger_log(logger, LOGGER_INFO, "\nPerhaps your GStreamer installation is missing some required plugins,"
"\nor your choices of video options (-vs -vd -vc -fs etc.) are incompatible on"
"\nthis computer architecture. (An example: kmssink with fullscreen option -fs"
"\nmay work on some systems, but fail on others)");
exit(1);
}
}
}
@@ -461,20 +476,18 @@ void video_renderer_start() {
GstState state;
const gchar *state_name;
if (hls_video) {
renderer->bus = gst_element_get_bus(renderer->pipeline);
gst_element_set_state (renderer->pipeline, GST_STATE_PAUSED);
gst_element_get_state(renderer->pipeline, &state, NULL, 1000 * GST_MSECOND);
state_name= gst_element_state_get_name(state);
state_name = gst_element_state_get_name(state);
logger_log(logger, LOGGER_DEBUG, "video renderer_start: state %s", state_name);
return;
}
/* when not hls, start both h264 and h265 pipelines; will shut down the "wrong" one when we know the codec */
/* when not hls, start both h264 and h265 pipelines; will shut down the "wrong" one when we know the codec */
for (int i = 0; i < n_renderers; i++) {
renderer_type[i]->bus = gst_element_get_bus(renderer_type[i]->pipeline);
gst_element_set_state (renderer_type[i]->pipeline, GST_STATE_PAUSED);
gst_element_get_state(renderer_type[i]->pipeline, &state, NULL, 1000 * GST_MSECOND);
state_name= gst_element_state_get_name(state);
logger_log(logger, LOGGER_DEBUG, "video renderer_start: renderer %d state %s", i, state_name);
gst_element_get_state(renderer_type[i]->pipeline, &state, NULL, 1000 * GST_MSECOND);
state_name = gst_element_state_get_name(state);
logger_log(logger, LOGGER_DEBUG, "video renderer_start: renderer %d %p state %s", i, renderer_type[i], state_name);
}
renderer = NULL;
first_packet = true;
@@ -503,8 +516,53 @@ bool waiting_for_x11_window() {
return false;
}
/* use this to cycle the jpeg renderer to remove expired coverart when no new coverart has replaced it */
int video_renderer_cycle() {
if (!renderer || !strstr(renderer->codec, jpeg)) {
return -1;
}
GstState state, pending_state, target_state;
GstStateChangeReturn ret;
gst_element_get_state(renderer->pipeline, &state, NULL, 0);
logger_log(logger, LOGGER_DEBUG, "renderer_cycle renderer %p: initial pipeline state is %s", renderer,
gst_element_state_get_name(state));
for (int i = 0 ; i < 2; i++) {
int count = 0;
if (i == 0 ) {
target_state = GST_STATE_NULL;
video_renderer_stop();
} else {
target_state = GST_STATE_PLAYING;
gst_element_set_state (renderer->pipeline, target_state);
}
while (state != target_state) {
ret = gst_element_get_state(renderer->pipeline, &state, &pending_state, 1000 * GST_MSECOND);
if (ret == GST_STATE_CHANGE_SUCCESS) {
logger_log(logger, LOGGER_DEBUG, "current pipeline state is %s", gst_element_state_get_name(state));
if (pending_state != GST_STATE_VOID_PENDING) {
logger_log(logger, LOGGER_DEBUG, "pending pipeline state is %s", gst_element_state_get_name(pending_state));
}
} else if (ret == GST_STATE_CHANGE_FAILURE) {
logger_log(logger, LOGGER_ERR, "pipeline %s: state change to %s failed", renderer->codec, gst_element_state_get_name(target_state));
count++;
if (count > 10) {
return -1;
}
} else if (ret == GST_STATE_CHANGE_ASYNC) {
logger_log(logger, LOGGER_DEBUG, "state change to %s is asynchronous, waiting for completion ...",
gst_element_state_get_name(target_state));
}
}
}
return 0;
}
void video_renderer_display_jpeg(const void *data, int *data_len) {
GstBuffer *buffer;
if (type_jpeg == -1) {
return;
}
if (renderer && !strcmp(renderer->codec, jpeg)) {
buffer = gst_buffer_new_allocate(NULL, *data_len, NULL);
g_assert(buffer != NULL);
@@ -539,6 +597,10 @@ uint64_t video_renderer_render_buffer(unsigned char* data, int *data_len, int *n
logger_log(logger, LOGGER_INFO, "Begin streaming to GStreamer video pipeline");
first_packet = false;
}
if (!renderer || !(renderer->appsrc)) {
logger_log(logger, LOGGER_DEBUG, "*** no video renderer found");
return 0;
}
buffer = gst_buffer_new_allocate(NULL, *data_len, NULL);
g_assert(buffer != NULL);
//g_print("video latency %8.6f\n", (double) latency / SECOND_IN_NSECS);
@@ -580,25 +642,27 @@ void video_renderer_stop() {
static void video_renderer_destroy_instance(video_renderer_t *renderer) {
if (renderer) {
logger_log(logger, LOGGER_DEBUG,"destroying renderer instance %p", renderer);
logger_log(logger, LOGGER_DEBUG,"destroying renderer instance %p codec=%s ", renderer, renderer->codec);
GstState state;
GstStateChangeReturn ret;
GstStateChangeReturn ret;
gst_element_get_state(renderer->pipeline, &state, NULL, 100 * GST_MSECOND);
logger_log(logger, LOGGER_DEBUG,"pipeline state is %s", gst_element_state_get_name(state));
logger_log(logger, LOGGER_DEBUG,"pipeline state is %s", gst_element_state_get_name(state));
if (state != GST_STATE_NULL) {
if (!hls_video) {
gst_app_src_end_of_stream (GST_APP_SRC(renderer->appsrc));
}
ret = gst_element_set_state (renderer->pipeline, GST_STATE_NULL);
logger_log(logger, LOGGER_DEBUG,"pipeline_state_change_return: %s",
gst_element_state_change_return_get_name(ret));
gst_element_get_state(renderer->pipeline, NULL, NULL, 1000 * GST_MSECOND);
logger_log(logger, LOGGER_DEBUG,"pipeline_state_change_return: %s",
gst_element_state_change_return_get_name(ret));
gst_element_get_state(renderer->pipeline, &state, NULL, 1000 * GST_MSECOND);
logger_log(logger, LOGGER_DEBUG,"pipeline state is %s", gst_element_state_get_name(state));
}
if (renderer->appsrc) {
gst_object_unref (renderer->appsrc);
renderer->appsrc = NULL;
}
gst_object_unref(renderer->bus);
if (renderer->appsrc) {
gst_object_unref (renderer->appsrc);
}
gst_object_unref (renderer->pipeline);
gst_object_unref(renderer->pipeline);
#ifdef X_DISPLAY_FIX
if (renderer->gst_window) {
free(renderer->gst_window);
@@ -607,6 +671,7 @@ static void video_renderer_destroy_instance(video_renderer_t *renderer) {
#endif
free (renderer);
renderer = NULL;
logger_log(logger, LOGGER_DEBUG,"renderer destroyed\n");
}
}
@@ -619,35 +684,35 @@ void video_renderer_destroy() {
}
static void get_stream_status_name(GstStreamStatusType type, char *name, size_t len) {
switch (type) {
case GST_STREAM_STATUS_TYPE_CREATE:
strncpy(name, "CREATE", len);
return;
case GST_STREAM_STATUS_TYPE_ENTER:
strncpy(name, "ENTER", len);
return;
case GST_STREAM_STATUS_TYPE_LEAVE:
strncpy(name, "LEAVE", len);
return;
case GST_STREAM_STATUS_TYPE_DESTROY:
strncpy(name, "DESTROY", len);
return;
case GST_STREAM_STATUS_TYPE_START:
strncpy(name, "START", len);
return;
case GST_STREAM_STATUS_TYPE_PAUSE:
strncpy(name, "PAUSE", len);
return;
case GST_STREAM_STATUS_TYPE_STOP:
strncpy(name, "STOP", len);
return;
default:
strncpy(name, "", len);
return;
}
switch (type) {
case GST_STREAM_STATUS_TYPE_CREATE:
strncpy(name, "CREATE", len);
return;
case GST_STREAM_STATUS_TYPE_ENTER:
strncpy(name, "ENTER", len);
return;
case GST_STREAM_STATUS_TYPE_LEAVE:
strncpy(name, "LEAVE", len);
return;
case GST_STREAM_STATUS_TYPE_DESTROY:
strncpy(name, "DESTROY", len);
return;
case GST_STREAM_STATUS_TYPE_START:
strncpy(name, "START", len);
return;
case GST_STREAM_STATUS_TYPE_PAUSE:
strncpy(name, "PAUSE", len);
return;
case GST_STREAM_STATUS_TYPE_STOP:
strncpy(name, "STOP", len);
return;
default:
strncpy(name, "", len);
return;
}
}
gboolean gstreamer_pipeline_bus_callback(GstBus *bus, GstMessage *message, void *loop) {
static gboolean gstreamer_video_pipeline_bus_callback(GstBus *bus, GstMessage *message, void *loop) {
GstState old_state, new_state;
const gchar no_state[] = "";
const gchar *old_state_name = no_state, *new_state_name = no_state;
@@ -678,7 +743,7 @@ gboolean gstreamer_pipeline_bus_callback(GstBus *bus, GstMessage *message, void
if (logger_debug) {
gchar *name = NULL;
GstElement *element = NULL;
GstElement *element = NULL;
gchar type_name[8] = { 0 };
if (GST_MESSAGE_TYPE(message) == GST_MESSAGE_STREAM_STATUS) {
GstStreamStatusType type;
@@ -699,9 +764,9 @@ gboolean gstreamer_pipeline_bus_callback(GstBus *bus, GstMessage *message, void
g_print("GStreamer %s bus message %s %s %s %s\n", renderer_type[type]->codec,
GST_MESSAGE_SRC_NAME(message), GST_MESSAGE_TYPE_NAME(message), old_state_name, new_state_name);
}
if (name) {
g_free(name);
}
if (name) {
g_free(name);
}
}
/* monitor hls video position until seek to hls_start_position is achieved */
@@ -711,21 +776,21 @@ gboolean gstreamer_pipeline_bus_callback(GstBus *bus, GstMessage *message, void
if (!GST_CLOCK_TIME_IS_VALID(hls_duration)) {
gst_element_query_duration (renderer->pipeline, GST_FORMAT_TIME, &hls_duration);
}
gst_element_query_position (renderer_type[type]->pipeline, GST_FORMAT_TIME, &pos);
gst_element_query_position (renderer_type[type]->pipeline, GST_FORMAT_TIME, &pos);
//g_print("HLS position %" GST_TIME_FORMAT " requested_start_position %" GST_TIME_FORMAT " duration %" GST_TIME_FORMAT " %s\n",
// GST_TIME_ARGS(pos), GST_TIME_ARGS(hls_requested_start_position), GST_TIME_ARGS(hls_duration),
// (hls_seek_enabled ? "seek enabled" : "seek not enabled"));
if (pos > hls_requested_start_position) {
hls_requested_start_position = 0;
}
if ( hls_requested_start_position && pos < hls_requested_start_position && hls_seek_enabled) {
if ( hls_requested_start_position && pos < hls_requested_start_position && hls_seek_enabled) {
g_print("***************** seek to hls_requested_start_position %" GST_TIME_FORMAT "\n", GST_TIME_ARGS(hls_requested_start_position));
if (gst_element_seek_simple (renderer_type[type]->pipeline, GST_FORMAT_TIME,
GST_SEEK_FLAG_FLUSH | GST_SEEK_FLAG_KEY_UNIT, hls_requested_start_position)) {
hls_requested_start_position = 0;
}
}
}
}
}
}
switch (GST_MESSAGE_TYPE (message)) {
@@ -737,8 +802,8 @@ gboolean gstreamer_pipeline_bus_callback(GstBus *bus, GstMessage *message, void
gint percent = -1;
gst_message_parse_buffering(message, &percent);
hls_buffer_empty = TRUE;
hls_buffer_full = FALSE;
if (percent > 0) {
hls_buffer_full = FALSE;
if (percent > 0) {
hls_buffer_empty = FALSE;
renderer_type[type]->buffering_level = percent;
logger_log(logger, LOGGER_DEBUG, "Buffering :%d percent done", percent);
@@ -756,7 +821,7 @@ gboolean gstreamer_pipeline_bus_callback(GstBus *bus, GstMessage *message, void
gchar *debug;
gboolean flushing;
gst_message_parse_error (message, &err, &debug);
logger_log(logger, LOGGER_INFO, "GStreamer error: %s %s", GST_MESSAGE_SRC_NAME(message),err->message);
logger_log(logger, LOGGER_INFO, "GStreamer error (video): %s %s", GST_MESSAGE_SRC_NAME(message),err->message);
if (!hls_video && strstr(err->message,"Internal data stream error")) {
logger_log(logger, LOGGER_INFO,
"*** This is a generic GStreamer error that usually means that GStreamer\n"
@@ -767,11 +832,11 @@ gboolean gstreamer_pipeline_bus_callback(GstBus *bus, GstMessage *message, void
"*** to select a videosink of your choice (see \"man uxplay\").\n\n"
"*** Raspberry Pi models 4B and earlier using Video4Linux2 may need \"-bt709\" uxplay option");
}
g_error_free (err);
g_error_free (err);
g_free (debug);
if (renderer_type[type]->appsrc) {
if (renderer_type[type]->appsrc) {
gst_app_src_end_of_stream (GST_APP_SRC(renderer_type[type]->appsrc));
}
}
gst_bus_set_flushing(bus, TRUE);
gst_element_set_state (renderer_type[type]->pipeline, GST_STATE_READY);
renderer_type[type]->terminate = TRUE;
@@ -779,12 +844,12 @@ gboolean gstreamer_pipeline_bus_callback(GstBus *bus, GstMessage *message, void
break;
}
case GST_MESSAGE_EOS:
/* end-of-stream */
logger_log(logger, LOGGER_INFO, "GStreamer: End-Of-Stream");
if (hls_video) {
/* end-of-stream */
logger_log(logger, LOGGER_INFO, "GStreamer: End-Of-Stream (video)");
if (hls_video) {
gst_bus_set_flushing(bus, TRUE);
gst_element_set_state (renderer_type[type]->pipeline, GST_STATE_READY);
renderer_type[type]->terminate = TRUE;
renderer_type[type]->terminate = TRUE;
g_main_loop_quit( (GMainLoop *) loop);
}
break;
@@ -819,14 +884,14 @@ gboolean gstreamer_pipeline_bus_callback(GstBus *bus, GstMessage *message, void
char *sink = strstr(GST_MESSAGE_SRC_NAME(message), "-actual-sink-");
if (sink) {
sink += strlen("-actual-sink-");
if (strstr(GST_MESSAGE_SRC_NAME(message), renderer_type[type]->codec)) {
if (strstr(GST_MESSAGE_SRC_NAME(message), renderer_type[type]->codec)) {
logger_log(logger, LOGGER_DEBUG, "GStreamer: automatically-selected videosink"
" (renderer %d: %s) is \"%ssink\"", renderer_type[type]->id + 1,
renderer_type[type]->codec, sink);
#ifdef X_DISPLAY_FIX
renderer_type[type]->use_x11 = (strstr(sink, "ximage") || strstr(sink, "xvimage"));
#endif
renderer_type[type]->autovideo = false;
renderer_type[type]->autovideo = false;
}
}
}
@@ -879,17 +944,21 @@ int video_renderer_choose_codec (bool video_is_jpeg, bool video_is_h265) {
video_renderer_t *renderer_used = NULL;
g_assert(!hls_video);
if (video_is_jpeg) {
renderer_used = renderer_type[0];
} else if (n_renderers == 2) {
if (video_is_h265) {
logger_log(logger, LOGGER_ERR, "video is h265 but the -h265 option was not used");
return -1;
}
renderer_used = renderer_type[1];
g_assert(type_jpeg != -1);
renderer_used = renderer_type[type_jpeg];
} else {
renderer_used = video_is_h265 ? renderer_type[2] : renderer_type[1];
if (video_is_h265) {
if (type_265 == -1) {
logger_log(logger, LOGGER_ERR, "video is h265 but the -h265 option was not used");
return -1;
}
renderer_used = renderer_type[type_265];
} else {
g_assert(type_264 != -1);
renderer_used = renderer_type[type_264];
}
}
if (renderer_used == NULL) {
if (renderer_used == NULL) {
return -1;
} else if (renderer_used == renderer) {
return 0;
@@ -901,16 +970,16 @@ int video_renderer_choose_codec (bool video_is_jpeg, bool video_is_h265) {
GstState old_state, new_state;
if (gst_element_get_state(renderer->pipeline, &old_state, &new_state, 100 * GST_MSECOND) == GST_STATE_CHANGE_FAILURE) {
g_error("video pipeline failed to go into playing state");
return -1;
return -1;
}
logger_log(logger, LOGGER_DEBUG, "video_pipeline state change from %s to %s\n",
gst_element_state_get_name (old_state),gst_element_state_get_name (new_state));
gst_video_pipeline_base_time = gst_element_get_base_time(renderer->appsrc);
if (n_renderers > 2 && renderer == renderer_type[2]) {
if (strstr(renderer->codec, h265)) {
logger_log(logger, LOGGER_INFO, "*** video format is h265 high definition (HD/4K) video %dx%d", width, height);
}
/* destroy unused renderers */
for (int i = 1; i < n_renderers; i++) {
for (int i = 0; i < n_renderers; i++) {
if (renderer_type[i] == renderer) {
continue;
}
@@ -927,12 +996,12 @@ unsigned int video_reset_callback(void * loop) {
if (video_terminate) {
video_terminate = false;
if (renderer->appsrc) {
gst_app_src_end_of_stream (GST_APP_SRC(renderer->appsrc));
gst_app_src_end_of_stream (GST_APP_SRC(renderer->appsrc));
}
gboolean flushing = TRUE;
gst_bus_set_flushing(renderer->bus, flushing);
gst_element_set_state (renderer->pipeline, GST_STATE_NULL);
g_main_loop_quit( (GMainLoop *) loop);
gst_element_set_state (renderer->pipeline, GST_STATE_NULL);
g_main_loop_quit( (GMainLoop *) loop);
}
return (unsigned int) TRUE;
}
@@ -1007,5 +1076,5 @@ void video_renderer_seek(float position) {
unsigned int video_renderer_listen(void *loop, int id) {
g_assert(id >= 0 && id < n_renderers);
return (unsigned int) gst_bus_add_watch(renderer_type[id]->bus,(GstBusFunc)
gstreamer_pipeline_bus_callback, (gpointer) loop);
gstreamer_video_pipeline_bus_callback, (gpointer) loop);
}

View File

@@ -47,15 +47,17 @@ typedef enum videoflip_e {
typedef struct video_renderer_s video_renderer_t;
void video_renderer_init (logger_t *logger, const char *server_name, videoflip_t videoflip[2], const char *parser,
void video_renderer_init (logger_t *logger, const char *server_name, videoflip_t videoflip[2], const char *parser, const char *rtp_pipeline,
const char *decoder, const char *converter, const char *videosink, const char *videosink_options,
bool initial_fullscreen, bool video_sync, bool h265_support, guint playbin_version, const char *uri);
bool initial_fullscreen, bool video_sync, bool h265_support, bool coverart_support,
guint playbin_version, const char *uri);
void video_renderer_start ();
void video_renderer_stop ();
void video_renderer_pause ();
void video_renderer_seek(float position);
void video_renderer_set_start(float position);
void video_renderer_resume ();
int video_renderer_cycle ();
bool video_renderer_is_paused();
uint64_t video_renderer_render_buffer (unsigned char* data, int *data_len, int *nal_count, uint64_t *ntp_time);
void video_renderer_display_jpeg(const void *data, int *data_len);

View File

@@ -1,4 +1,4 @@
.TH UXPLAY "1" "May 2025" "1.72" "User Commands"
.TH UXPLAY "1" "2025-10-26" "UxPlay 1.72" "User Commands"
.SH NAME
uxplay \- start AirPlay server
.SH SYNOPSIS
@@ -9,7 +9,7 @@ UxPlay 1.72: An open\-source AirPlay mirroring (+ audio streaming) server:
.SH OPTIONS
.TP
.B
\fB\-n\fR name Specify the network name of the AirPlay server
\fB\-n\fR name Specify the network name of the AirPlay server (UTF-8/ascii)
.TP
\fB\-nh\fR Do \fBNOT\fR append "@\fIhostname\fR" at end of AirPlay server name
.TP
@@ -19,6 +19,8 @@ UxPlay 1.72: An open\-source AirPlay mirroring (+ audio streaming) server:
.IP
v = 2 or 3 (default 3) optionally selects video player version
.TP
\fB\-scrsv\fI n\fR Screensaver override \fIn\fR:0=off 1=on during activity 2=always on.
.TP
\fB\-pin\fI[xxxx]\fRUse a 4-digit pin code to control client access (default: no)
.IP
without option, pin is random: optionally use fixed pin xxxx.
@@ -89,11 +91,20 @@ UxPlay 1.72: An open\-source AirPlay mirroring (+ audio streaming) server:
.IP
choices: ximagesink,xvimagesink,vaapisink,glimagesink,
.IP
gtksink,waylandsink,osxvideosink,kmssink,d3d11videosink,...
gtksink,waylandsink,kmssink,fbdevsink,osxvideosink,
.IP
d3d11videosink,d3d12videosink ...
.PP
.TP
\fB\-vs\fR 0 Streamed audio only, with no video display window.
.TP
\fB\-vrtp\fI pl\fR Use rtph26[4,5]pay to send decoded video elsewhere: "pl"
.IP
is the remaining pipeline, starting with rtph26*pay options:
.IP
e.g. "config-interval=1 ! udpsink host=127.0.0.1 port=5000"
.PP
.TP
\fB\-v4l2\fR Use Video4Linux2 for GPU hardware h264 video decoding.
.TP
\fB\-bt709\fR Sometimes needed for Raspberry Pi models using Video4Linux2.
@@ -180,6 +191,10 @@ UxPlay 1.72: An open\-source AirPlay mirroring (+ audio streaming) server:
audio packets are dumped. "aud"= unknown format.
.PP
.TP
\fB\-ble\fI [fn]\fR For BluetoothLE beacon: write data to default file ~/.uxplay.ble
.IP
optional: write to file "fn" ("fn" = "off" to cancel)
.TP
\fB\-d [n]\fR Enable debug logging; optional: n=1 to skip normal packet data.
.TP
\fB\-v\fR Displays version information

File diff suppressed because it is too large Load Diff

View File

@@ -129,6 +129,7 @@ cd build
%{_docdir}/%{name}/README.txt
%{_docdir}/%{name}/README.html
%{_docdir}/%{name}/README.md
%{_docdir}/%{name}/systemd/uxplay.service
%license
%{_docdir}/%{name}/LICENSE