mirror of
https://github.com/morgan9e/UxPlay
synced 2026-04-14 00:04:13 +09:00
Merge pull request #197 from FDH2/master
continuing development towards uxplay-1.73
This commit is contained in:
45
Bluetooth_LE_beacon/dbus/uxplay-beacon.1
Normal file
45
Bluetooth_LE_beacon/dbus/uxplay-beacon.1
Normal 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>
|
||||
477
Bluetooth_LE_beacon/dbus/uxplay-beacon.py
Normal file
477
Bluetooth_LE_beacon/dbus/uxplay-beacon.py
Normal 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)
|
||||
|
||||
39
Bluetooth_LE_beacon/winrt/uxplay-beacon.1
Normal file
39
Bluetooth_LE_beacon/winrt/uxplay-beacon.1
Normal 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>
|
||||
274
Bluetooth_LE_beacon/winrt/uxplay-beacon.py
Executable file
274
Bluetooth_LE_beacon/winrt/uxplay-beacon.py
Executable 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)
|
||||
|
||||
@@ -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(
|
||||
|
||||
222
README.html
222
README.html
@@ -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 >=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 <rest-of-pipeline></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 <n></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><filename></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 client’s drop-down
|
||||
done.</p>
|
||||
<p>If the UxPlay server is not seen by the iOS client’s drop-down
|
||||
“Screen Mirroring” panel, check that your DNS-SD server (usually
|
||||
avahi-daemon) is running: do this in a terminal window with
|
||||
<code>systemctl status avahi-daemon</code>. If this shows the
|
||||
@@ -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><videosink></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 <options>"</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 GStreamer’s "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>=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.</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 <config file></code> read beacon options from
|
||||
<code><config file></code> instead of
|
||||
<code>~/.uxplay.beacon</code>.</p></li>
|
||||
<li><p><code>--ipv4 <ipv4 address></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 <BLE data file></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 <BLE data file></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 <=
|
||||
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.</p></li>
|
||||
<li><p><code>--index x</code> (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).
|
||||
<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
190
README.md
@@ -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
|
||||
|
||||
240
README.txt
240
README.txt
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
35
lib/dnssd.c
35
lib/dnssd.c
@@ -310,16 +310,16 @@ dnssd_register_raop(dnssd_t *dnssd, unsigned short port)
|
||||
case 2:
|
||||
case 3:
|
||||
dnssd->TXTRecordSetValue(&dnssd->raop_record, "pw", strlen("true"), "true");
|
||||
dnssd->TXTRecordSetValue(&dnssd->raop_record, "sf", 4, "0x84");
|
||||
break;
|
||||
dnssd->TXTRecordSetValue(&dnssd->raop_record, "sf", 4, "0x84");
|
||||
break;
|
||||
case 1:
|
||||
dnssd->TXTRecordSetValue(&dnssd->raop_record, "pw", strlen("true"), "true");
|
||||
dnssd->TXTRecordSetValue(&dnssd->raop_record, "sf", 3, "0x8c");
|
||||
break;
|
||||
dnssd->TXTRecordSetValue(&dnssd->raop_record, "sf", 3, "0x8c");
|
||||
break;
|
||||
default:
|
||||
dnssd->TXTRecordSetValue(&dnssd->raop_record, "pw", strlen("false"), "false");
|
||||
dnssd->TXTRecordSetValue(&dnssd->raop_record, "sf", strlen(RAOP_SF), RAOP_SF);
|
||||
break;
|
||||
dnssd->TXTRecordSetValue(&dnssd->raop_record, "sf", strlen(RAOP_SF), RAOP_SF);
|
||||
break;
|
||||
}
|
||||
dnssd->TXTRecordSetValue(&dnssd->raop_record, "sr", strlen(RAOP_SR), RAOP_SR);
|
||||
dnssd->TXTRecordSetValue(&dnssd->raop_record, "ss", strlen(RAOP_SS), RAOP_SS);
|
||||
@@ -382,8 +382,8 @@ dnssd_register_airplay(dnssd_t *dnssd, unsigned short port)
|
||||
switch (dnssd->pin_pw) {
|
||||
case 1: // display onscreen pin
|
||||
dnssd->TXTRecordSetValue(&dnssd->airplay_record, "pw", strlen("true"), "true");
|
||||
dnssd->TXTRecordSetValue(&dnssd->airplay_record, "flags", 3, "0x4");
|
||||
break;
|
||||
dnssd->TXTRecordSetValue(&dnssd->airplay_record, "flags", 3, "0x4");
|
||||
break;
|
||||
case 2: // require password
|
||||
case 3:
|
||||
dnssd->TXTRecordSetValue(&dnssd->airplay_record, "pw", strlen("true"), "true");
|
||||
@@ -412,6 +412,13 @@ dnssd_register_airplay(dnssd_t *dnssd, unsigned short port)
|
||||
return (int) retval; /* error codes are listed in Apple's dns_sd.h */
|
||||
}
|
||||
|
||||
const char *
|
||||
dnssd_get_raop_txt(dnssd_t *dnssd, int *length)
|
||||
{
|
||||
*length = dnssd->TXTRecordGetLength(&dnssd->raop_record);
|
||||
return dnssd->TXTRecordGetBytesPtr(&dnssd->raop_record);
|
||||
}
|
||||
|
||||
const char *
|
||||
dnssd_get_airplay_txt(dnssd_t *dnssd, int *length)
|
||||
{
|
||||
@@ -476,13 +483,13 @@ dnssd_unregister_airplay(dnssd_t *dnssd)
|
||||
}
|
||||
|
||||
uint64_t dnssd_get_airplay_features(dnssd_t *dnssd) {
|
||||
uint64_t features = ((uint64_t) dnssd->features2) << 32;
|
||||
features += (uint64_t) dnssd->features1;
|
||||
return features;
|
||||
uint64_t features = ((uint64_t) dnssd->features2) << 32;
|
||||
features += (uint64_t) dnssd->features1;
|
||||
return features;
|
||||
}
|
||||
|
||||
void dnssd_set_pk(dnssd_t *dnssd, char * pk_str) {
|
||||
dnssd->pk = pk_str;
|
||||
dnssd->pk = pk_str;
|
||||
}
|
||||
|
||||
void dnssd_set_airplay_features(dnssd_t *dnssd, int bit, int val) {
|
||||
@@ -498,8 +505,8 @@ void dnssd_set_airplay_features(dnssd_t *dnssd, int bit, int val) {
|
||||
features = &(dnssd->features1);
|
||||
}
|
||||
if (val) {
|
||||
*features = *features | mask;
|
||||
*features = *features | mask;
|
||||
} else {
|
||||
*features = *features & ~mask;
|
||||
*features = *features & ~mask;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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++;
|
||||
}
|
||||
|
||||
88
lib/httpd.c
88
lib/httpd.c
@@ -201,23 +201,26 @@ httpd_destroy(httpd_t *httpd)
|
||||
static void
|
||||
httpd_remove_connection(httpd_t *httpd, http_connection_t *connection)
|
||||
{
|
||||
int socket_fd = connection->socket_fd;
|
||||
connection->socket_fd = 0;
|
||||
if (connection->request) {
|
||||
http_request_destroy(connection->request);
|
||||
connection->request = NULL;
|
||||
}
|
||||
logger_log(httpd->logger, LOGGER_DEBUG, "removing connection type %s socket %d conn %p", typename[connection->type],
|
||||
connection->socket_fd, connection->user_data);
|
||||
socket_fd, connection->user_data);
|
||||
if (connection->user_data) {
|
||||
httpd->callbacks.conn_destroy(connection->user_data);
|
||||
connection->user_data = NULL;
|
||||
}
|
||||
if (connection->socket_fd) {
|
||||
shutdown(connection->socket_fd, SHUT_WR);
|
||||
int ret = closesocket(connection->socket_fd);
|
||||
if (socket_fd) {
|
||||
shutdown(socket_fd, SHUT_WR);
|
||||
int ret = closesocket(socket_fd);
|
||||
if (ret == -1) {
|
||||
logger_log(httpd->logger, LOGGER_ERR, "httpd error in closesocket (close): %d %s", errno, strerror(errno));
|
||||
} else {
|
||||
logger_log(httpd->logger, LOGGER_INFO, "Connection closed on socket %d", socket_fd);
|
||||
}
|
||||
connection->socket_fd = 0;
|
||||
}
|
||||
if (connection->connected) {
|
||||
connection->connected = 0;
|
||||
@@ -265,6 +268,7 @@ httpd_accept_connection(httpd_t *httpd, int server_fd, int is_ipv6)
|
||||
struct sockaddr_storage local_saddr;
|
||||
socklen_t local_saddrlen;
|
||||
unsigned char *local, *remote;
|
||||
unsigned short port;
|
||||
unsigned int local_zone_id, remote_zone_id;
|
||||
int local_len, remote_len;
|
||||
int ret, fd;
|
||||
@@ -284,10 +288,16 @@ httpd_accept_connection(httpd_t *httpd, int server_fd, int is_ipv6)
|
||||
return 0;
|
||||
}
|
||||
|
||||
logger_log(httpd->logger, LOGGER_INFO, "Accepted %s client on socket %d",
|
||||
(is_ipv6 ? "IPv6" : "IPv4"), fd);
|
||||
local = netutils_get_address(&local_saddr, &local_len, &local_zone_id);
|
||||
remote = netutils_get_address(&remote_saddr, &remote_len, &remote_zone_id);
|
||||
local = netutils_get_address(&local_saddr, &local_len, &local_zone_id, &port);
|
||||
logger_log(httpd->logger, LOGGER_INFO, "Accepted %s client on socket %d, port %u",
|
||||
(is_ipv6 ? "IPv6" : "IPv4"), fd, port);
|
||||
remote = netutils_get_address(&remote_saddr, &remote_len, &remote_zone_id, NULL);
|
||||
|
||||
// is it correct that ipv6 link-local local and remote zone id should be the same, as asserted below?
|
||||
if (local_zone_id != remote_zone_id) {
|
||||
logger_log(httpd->logger, LOGGER_INFO, "ipv6 zone_id mismatch: local_zone_id = %u, remote_zone_id = %u",
|
||||
local_zone_id, remote_zone_id);
|
||||
}
|
||||
assert (local_zone_id == remote_zone_id);
|
||||
|
||||
ret = httpd_add_connection(httpd, fd, local, local_len, remote, remote_len, local_zone_id);
|
||||
@@ -342,7 +352,7 @@ httpd_thread(void *arg)
|
||||
struct timeval tv;
|
||||
int nfds=0;
|
||||
int ret;
|
||||
int new_request;
|
||||
int new_request;
|
||||
|
||||
MUTEX_LOCK(httpd->run_mutex);
|
||||
if (!httpd->running) {
|
||||
@@ -388,7 +398,9 @@ httpd_thread(void *arg)
|
||||
/* Timeout happened */
|
||||
continue;
|
||||
} else if (ret == -1) {
|
||||
logger_log(httpd->logger, LOGGER_ERR, "httpd error in select: %d %s", errno, strerror(errno));
|
||||
int sock_err = SOCKET_GET_ERROR();
|
||||
logger_log(httpd->logger, LOGGER_ERR,
|
||||
"httpd error in select: %d %s", sock_err, SOCKET_ERROR_STRING(sock_err));
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -430,11 +442,11 @@ httpd_thread(void *arg)
|
||||
if (connection->type == CONNECTION_TYPE_PTTH) {
|
||||
http_request_is_reverse(connection->request);
|
||||
}
|
||||
logger_log(httpd->logger, LOGGER_DEBUG, "new request, connection %d, socket %d type %s",
|
||||
logger_log(httpd->logger, LOGGER_DEBUG, "new request, connection %d, socket %d type %s",
|
||||
i, connection->socket_fd, typename [connection->type]);
|
||||
} else {
|
||||
new_request = 0;
|
||||
}
|
||||
}
|
||||
|
||||
logger_log(httpd->logger, LOGGER_DEBUG, "httpd receiving on socket %d, connection %d",
|
||||
connection->socket_fd, i);
|
||||
@@ -442,54 +454,64 @@ httpd_thread(void *arg)
|
||||
logger_log(httpd->logger, LOGGER_DEBUG,"\nhttpd: current connections:");
|
||||
for (int i = 0; i < httpd->max_connections; i++) {
|
||||
http_connection_t *connection = &httpd->connections[i];
|
||||
if(!connection->connected) {
|
||||
if (!connection->connected || !connection->socket_fd) {
|
||||
continue;
|
||||
}
|
||||
if (!FD_ISSET(connection->socket_fd, &rfds)) {
|
||||
logger_log(httpd->logger, LOGGER_DEBUG, "connection %d type %d socket %d conn %p %s", i,
|
||||
connection->type, connection->socket_fd,
|
||||
connection->user_data, typename [connection->type]);
|
||||
} else {
|
||||
logger_log(httpd->logger, LOGGER_DEBUG, "connection %d type %d socket %d conn %p %s ACTIVE CONNECTION",
|
||||
i, connection->type, connection->socket_fd, connection->user_data, typename [connection->type]);
|
||||
} else {
|
||||
logger_log(httpd->logger, LOGGER_DEBUG, "connection %d type %d socket %d conn %p %s ACTIVE CONNECTION",
|
||||
i, connection->type, connection->socket_fd, connection->user_data, typename [connection->type]);
|
||||
}
|
||||
}
|
||||
logger_log(httpd->logger, LOGGER_DEBUG, " ");
|
||||
}
|
||||
logger_log(httpd->logger, LOGGER_DEBUG, " ");
|
||||
}
|
||||
/* reverse-http responses from the client must not be sent to the llhttp parser:
|
||||
* such messages start with "HTTP/1.1" */
|
||||
if (new_request) {
|
||||
int readstart = 0;
|
||||
new_request = 0;
|
||||
while (readstart < 8) {
|
||||
if (!connection->socket_fd) {
|
||||
break;
|
||||
}
|
||||
ret = recv(connection->socket_fd, buffer + readstart, sizeof(buffer) - 1 - readstart, 0);
|
||||
if (ret == 0) {
|
||||
logger_log(httpd->logger, LOGGER_INFO, "Connection closed for socket %d",
|
||||
logger_log(httpd->logger, LOGGER_DEBUG, "client closed connection on socket %d",
|
||||
connection->socket_fd);
|
||||
break;
|
||||
} else if (ret == -1) {
|
||||
if (errno == EAGAIN) {
|
||||
continue;
|
||||
} else {
|
||||
int sock_err = SOCKET_GET_ERROR();
|
||||
logger_log(httpd->logger, LOGGER_ERR, "httpd: recv socket error %d:%s",
|
||||
sock_err, SOCKET_ERROR_STRING(sock_err));
|
||||
break;
|
||||
}
|
||||
if (errno == EAGAIN) {
|
||||
continue;
|
||||
} else {
|
||||
int sock_err = SOCKET_GET_ERROR();
|
||||
logger_log(httpd->logger, LOGGER_ERR, "httpd: recv error %d on socket %d: %s",
|
||||
sock_err, connection->socket_fd, SOCKET_ERROR_STRING(sock_err));
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
readstart += ret;
|
||||
ret = readstart;
|
||||
}
|
||||
}
|
||||
if (!connection->socket_fd) {
|
||||
/* connection was recently removed */
|
||||
continue;
|
||||
}
|
||||
if (!memcmp(buffer, http, 8)) {
|
||||
http_request_set_reverse(connection->request);
|
||||
}
|
||||
} else {
|
||||
ret = recv(connection->socket_fd, buffer, sizeof(buffer) - 1, 0);
|
||||
if (ret == 0) {
|
||||
logger_log(httpd->logger, LOGGER_INFO, "Connection closed for socket %d",
|
||||
connection->socket_fd);
|
||||
httpd_remove_connection(httpd, connection);
|
||||
if (connection->socket_fd) {
|
||||
ret = recv(connection->socket_fd, buffer, sizeof(buffer) - 1, 0);
|
||||
if (ret == 0) {
|
||||
httpd_remove_connection(httpd, connection);
|
||||
continue;
|
||||
}
|
||||
} else {
|
||||
/* connection was recently removed */
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
61
lib/raop.c
61
lib/raop.c
@@ -188,26 +188,35 @@ conn_request(void *ptr, http_request_t *request, http_response_t **response) {
|
||||
All requests arriving here have been parsed by llhttp to obtain
|
||||
method | url | protocol (RTSP/1.0 or HTTP/1.1)
|
||||
|
||||
There are three types of connections supplying these requests:
|
||||
There are four types of connections supplying these requests:
|
||||
Connections from the AirPlay client:
|
||||
(1) type RAOP connections with CSeq seqence header, and no X-Apple-Session-ID header
|
||||
(2) type AIRPLAY connection with an X-Apple-Sequence-ID header and no Cseq header
|
||||
Connections from localhost:
|
||||
(3) type HLS internal connections from the local HLS server (gstreamer) at localhost with neither
|
||||
of these headers, but a Host: localhost:[port] header. method = GET.
|
||||
(4) a special RAOP connection trigggered by a Bluetooth LE beacon: Protocol RTSP/1.0, method: GET
|
||||
url /info?txtAirPlay?txtRAOP, and no headers including CSeq
|
||||
*/
|
||||
|
||||
const char *method = http_request_get_method(request);
|
||||
const char *url = http_request_get_url(request);
|
||||
const char *protocol = http_request_get_protocol(request);
|
||||
|
||||
if (!method || !url) {
|
||||
if (!method || !url || !protocol) {
|
||||
return;
|
||||
}
|
||||
|
||||
/* this rejects messages from _airplay._tcp for video streaming protocol unless bool raop->hls_support is true*/
|
||||
/* ¨idenitfy if request is a response to a BLE beaconn */
|
||||
const char *cseq = http_request_get_header(request, "CSeq");
|
||||
const char *protocol = http_request_get_protocol(request);
|
||||
if (!cseq && !conn->raop->hls_support) {
|
||||
bool ble = false;
|
||||
if (!strcmp(protocol,"RTSP/1.0") && !cseq && (strstr(url, "txtAirPlay") || strstr(url, "txtRAOP") )) {
|
||||
logger_log(conn->raop->logger, LOGGER_INFO, "response to Bluetooth LE beacon advertisement received)");
|
||||
ble = true;
|
||||
}
|
||||
|
||||
/* this rejects messages from _airplay._tcp for video streaming protocol unless bool raop->hls_support is true*/
|
||||
if (!cseq && !conn->raop->hls_support && !ble) {
|
||||
logger_log(conn->raop->logger, LOGGER_INFO, "ignoring AirPlay video streaming request (use option -hls to activate HLS support)");
|
||||
return;
|
||||
}
|
||||
@@ -217,7 +226,7 @@ conn_request(void *ptr, http_request_t *request, http_response_t **response) {
|
||||
hls_request = (host && !cseq && !client_session_id);
|
||||
|
||||
if (conn->connection_type == CONNECTION_TYPE_UNKNOWN) {
|
||||
if (cseq) {
|
||||
if (cseq || ble) {
|
||||
if (httpd_count_connection_type(conn->raop->httpd, CONNECTION_TYPE_RAOP)) {
|
||||
char ipaddr[40];
|
||||
utils_ipaddress_to_string(conn->remotelen, conn->remote, conn->zone_id, ipaddr, (int) (sizeof(ipaddr)));
|
||||
@@ -225,8 +234,8 @@ conn_request(void *ptr, http_request_t *request, http_response_t **response) {
|
||||
logger_log(conn->raop->logger, LOGGER_INFO, "\"nohold\" feature: switch to new connection request from %s", ipaddr);
|
||||
if (conn->raop->callbacks.video_reset) {
|
||||
conn->raop->callbacks.video_reset(conn->raop->callbacks.cls);
|
||||
}
|
||||
httpd_remove_known_connections(conn->raop->httpd);
|
||||
}
|
||||
httpd_remove_known_connections(conn->raop->httpd);
|
||||
} else {
|
||||
logger_log(conn->raop->logger, LOGGER_WARNING, "rejecting new connection request from %s", ipaddr);
|
||||
*response = http_response_create();
|
||||
@@ -245,7 +254,7 @@ conn_request(void *ptr, http_request_t *request, http_response_t **response) {
|
||||
conn->client_session_id = (char *) malloc(len);
|
||||
strncpy(conn->client_session_id, client_session_id, len);
|
||||
/* airplay video has been requested: shut down any running RAOP udp services */
|
||||
raop_conn_t *raop_conn = (raop_conn_t *) httpd_get_connection_by_type(conn->raop->httpd, CONNECTION_TYPE_RAOP, 1);
|
||||
raop_conn_t *raop_conn = (raop_conn_t *) httpd_get_connection_by_type(conn->raop->httpd, CONNECTION_TYPE_RAOP, 1);
|
||||
if (raop_conn) {
|
||||
raop_rtp_mirror_t *raop_rtp_mirror = raop_conn->raop_rtp_mirror;
|
||||
if (raop_rtp_mirror) {
|
||||
@@ -273,7 +282,7 @@ conn_request(void *ptr, http_request_t *request, http_response_t **response) {
|
||||
httpd_set_connection_type(conn->raop->httpd, ptr, CONNECTION_TYPE_HLS);
|
||||
conn->connection_type = CONNECTION_TYPE_HLS;
|
||||
} else {
|
||||
logger_log(conn->raop->logger, LOGGER_WARNING, "connection from unknown connection type");
|
||||
logger_log(conn->raop->logger, LOGGER_WARNING, "connection from unknown connection type");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -310,9 +319,9 @@ conn_request(void *ptr, http_request_t *request, http_response_t **response) {
|
||||
if (request_data && logger_debug) {
|
||||
if (request_datalen > 0) {
|
||||
/* logger has a buffer limit of 4096 */
|
||||
if (data_is_plist) {
|
||||
plist_t req_root_node = NULL;
|
||||
plist_from_bin(request_data, request_datalen, &req_root_node);
|
||||
if (data_is_plist) {
|
||||
plist_t req_root_node = NULL;
|
||||
plist_from_bin(request_data, request_datalen, &req_root_node);
|
||||
char * plist_xml = NULL;
|
||||
char * stripped_xml = NULL;
|
||||
uint32_t plist_len;
|
||||
@@ -353,7 +362,7 @@ conn_request(void *ptr, http_request_t *request, http_response_t **response) {
|
||||
if (!strcmp(method, "POST")) {
|
||||
if (!strcmp(url, "/feedback")) {
|
||||
handler = &raop_handler_feedback;
|
||||
} else if (!strcmp(url, "/pair-pin-start")) {
|
||||
} else if (!strcmp(url, "/pair-pin-start")) {
|
||||
handler = &raop_handler_pairpinstart;
|
||||
} else if (!strcmp(url, "/pair-setup-pin")) {
|
||||
handler = &raop_handler_pairsetup_pin;
|
||||
@@ -363,13 +372,11 @@ conn_request(void *ptr, http_request_t *request, http_response_t **response) {
|
||||
handler = &raop_handler_pairverify;
|
||||
} else if (!strcmp(url, "/fp-setup")) {
|
||||
handler = &raop_handler_fpsetup;
|
||||
} else if (!strcmp(url, "/getProperty")) {
|
||||
handler = &http_handler_get_property;
|
||||
} else if (!strcmp(url, "/audioMode")) {
|
||||
//handler = &http_handler_audioMode;
|
||||
handler = &raop_handler_audiomode;
|
||||
}
|
||||
} else if (!strcmp(method, "GET")) {
|
||||
if (!strcmp(url, "/info")) {
|
||||
if (strstr(url, "/info")) {
|
||||
handler = &raop_handler_info;
|
||||
}
|
||||
} else if (!strcmp(method, "OPTIONS")) {
|
||||
@@ -388,7 +395,7 @@ conn_request(void *ptr, http_request_t *request, http_response_t **response) {
|
||||
handler = &raop_handler_teardown;
|
||||
} else {
|
||||
http_response_init(*response, protocol, 501, "Not Implemented");
|
||||
}
|
||||
}
|
||||
} else if (!hls_request && !strcmp(protocol, "HTTP/1.1")) {
|
||||
if (!strcmp(method, "POST")) {
|
||||
if (!strcmp(url, "/reverse")) {
|
||||
@@ -415,9 +422,9 @@ conn_request(void *ptr, http_request_t *request, http_response_t **response) {
|
||||
handler = &http_handler_playback_info;
|
||||
}
|
||||
} else if (!strcmp(method, "PUT")) {
|
||||
if (!strncmp (url, "/setProperty?", strlen("/setProperty?"))) {
|
||||
if (!strncmp (url, "/setProperty?", strlen("/setProperty?"))) {
|
||||
handler = &http_handler_set_property;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (hls_request) {
|
||||
handler = &http_handler_hls;
|
||||
@@ -426,8 +433,8 @@ conn_request(void *ptr, http_request_t *request, http_response_t **response) {
|
||||
if (handler != NULL) {
|
||||
handler(conn, request, *response, &response_data, &response_datalen);
|
||||
} else {
|
||||
logger_log(conn->raop->logger, LOGGER_INFO,
|
||||
"Unhandled Client Request: %s %s %s", method, url, protocol);
|
||||
logger_log(conn->raop->logger, LOGGER_INFO,
|
||||
"Unhandled Client Request: %s %s %s", method, url, protocol);
|
||||
}
|
||||
|
||||
finish:;
|
||||
@@ -435,7 +442,7 @@ conn_request(void *ptr, http_request_t *request, http_response_t **response) {
|
||||
http_response_add_header(*response, "Server", "AirTunes/"GLOBAL_VERSION);
|
||||
if (cseq) {
|
||||
http_response_add_header(*response, "CSeq", cseq);
|
||||
}
|
||||
}
|
||||
}
|
||||
http_response_finish(*response, response_data, response_datalen);
|
||||
|
||||
@@ -488,7 +495,7 @@ conn_request(void *ptr, http_request_t *request, http_response_t **response) {
|
||||
}
|
||||
if (response_data) {
|
||||
free(response_data);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -646,10 +653,10 @@ raop_destroy(raop_t *raop) {
|
||||
pairing_destroy(raop->pairing);
|
||||
httpd_destroy(raop->httpd);
|
||||
logger_destroy(raop->logger);
|
||||
if (raop->nonce) {
|
||||
if (raop->nonce) {
|
||||
free(raop->nonce);
|
||||
}
|
||||
if (raop->random_pw) {
|
||||
if (raop->random_pw) {
|
||||
free(raop->random_pw);
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
10
lib/utils.c
10
lib/utils.c
@@ -312,20 +312,20 @@ char *utils_strip_data_from_plist_xml(char *plist_xml) {
|
||||
nchars = eol + 1 - ptr1;
|
||||
memcpy(ptr2, ptr1, nchars);
|
||||
ptr2 += nchars;
|
||||
ptr1 += nchars;
|
||||
ptr1 += nchars;
|
||||
end = strstr(ptr1, "</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 {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
21
uxplay.1
21
uxplay.1
@@ -1,4 +1,4 @@
|
||||
.TH UXPLAY "1" "May 2025" "1.72" "User Commands"
|
||||
.TH UXPLAY "1" "2025-10-26" "UxPlay 1.72" "User Commands"
|
||||
.SH NAME
|
||||
uxplay \- start AirPlay server
|
||||
.SH SYNOPSIS
|
||||
@@ -9,7 +9,7 @@ UxPlay 1.72: An open\-source AirPlay mirroring (+ audio streaming) server:
|
||||
.SH OPTIONS
|
||||
.TP
|
||||
.B
|
||||
\fB\-n\fR name Specify the network name of the AirPlay server
|
||||
\fB\-n\fR name Specify the network name of the AirPlay server (UTF-8/ascii)
|
||||
.TP
|
||||
\fB\-nh\fR Do \fBNOT\fR append "@\fIhostname\fR" at end of AirPlay server name
|
||||
.TP
|
||||
@@ -19,6 +19,8 @@ UxPlay 1.72: An open\-source AirPlay mirroring (+ audio streaming) server:
|
||||
.IP
|
||||
v = 2 or 3 (default 3) optionally selects video player version
|
||||
.TP
|
||||
\fB\-scrsv\fI n\fR Screensaver override \fIn\fR:0=off 1=on during activity 2=always on.
|
||||
.TP
|
||||
\fB\-pin\fI[xxxx]\fRUse a 4-digit pin code to control client access (default: no)
|
||||
.IP
|
||||
without option, pin is random: optionally use fixed pin xxxx.
|
||||
@@ -89,11 +91,20 @@ UxPlay 1.72: An open\-source AirPlay mirroring (+ audio streaming) server:
|
||||
.IP
|
||||
choices: ximagesink,xvimagesink,vaapisink,glimagesink,
|
||||
.IP
|
||||
gtksink,waylandsink,osxvideosink,kmssink,d3d11videosink,...
|
||||
gtksink,waylandsink,kmssink,fbdevsink,osxvideosink,
|
||||
.IP
|
||||
d3d11videosink,d3d12videosink ...
|
||||
.PP
|
||||
.TP
|
||||
\fB\-vs\fR 0 Streamed audio only, with no video display window.
|
||||
.TP
|
||||
\fB\-vrtp\fI pl\fR Use rtph26[4,5]pay to send decoded video elsewhere: "pl"
|
||||
.IP
|
||||
is the remaining pipeline, starting with rtph26*pay options:
|
||||
.IP
|
||||
e.g. "config-interval=1 ! udpsink host=127.0.0.1 port=5000"
|
||||
.PP
|
||||
.TP
|
||||
\fB\-v4l2\fR Use Video4Linux2 for GPU hardware h264 video decoding.
|
||||
.TP
|
||||
\fB\-bt709\fR Sometimes needed for Raspberry Pi models using Video4Linux2.
|
||||
@@ -180,6 +191,10 @@ UxPlay 1.72: An open\-source AirPlay mirroring (+ audio streaming) server:
|
||||
audio packets are dumped. "aud"= unknown format.
|
||||
.PP
|
||||
.TP
|
||||
\fB\-ble\fI [fn]\fR For BluetoothLE beacon: write data to default file ~/.uxplay.ble
|
||||
.IP
|
||||
optional: write to file "fn" ("fn" = "off" to cancel)
|
||||
.TP
|
||||
\fB\-d [n]\fR Enable debug logging; optional: n=1 to skip normal packet data.
|
||||
.TP
|
||||
\fB\-v\fR Displays version information
|
||||
|
||||
841
uxplay.cpp
841
uxplay.cpp
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user