first commit

This commit is contained in:
FabioMich66 2026-06-01 12:53:22 +02:00
commit d3657cfd2b
2 changed files with 721 additions and 0 deletions

56
README.md Normal file
View file

@ -0,0 +1,56 @@
🔥 COME ABBIAMO SBLOCCATO MACOS PER PERMETTERE IL MULTICAST SSDP
macOS blocca il multicast UDP in uscita per processi non autorizzati,
anche se il firewall è disattivato.
Per questo il tuo Python diceva “SSDP sent”, ma non usciva nulla sulla rete.
✔️ 1. Abbiamo autorizzato Python nel firewall di macOS
Se usi Python di sistema:
Codice
sudo /usr/libexec/ApplicationFirewall/socketfilterfw --add /usr/bin/python3
sudo /usr/libexec/ApplicationFirewall/socketfilterfw --unblockapp /usr/bin/python3
Questo è il passaggio fondamentale:
senza questa autorizzazione, macOS non invia multicast.
✔️ 2. Abbiamo abilitato le app firmate a inviare multicast
macOS ha due flag nascosti che, se disattivati, bloccano il multicast:
Codice
sudo /usr/libexec/ApplicationFirewall/socketfilterfw --setallowsigned on
sudo /usr/libexec/ApplicationFirewall/socketfilterfw --setallowsignedapp on
Questi due comandi dicono al firewall:
“Lascia passare il traffico multicast delle app firmate (come Python).”
✔️ 3. Abbiamo verificato con tcpdump
Dopo la patch:
Codice
sudo tcpdump -i en0 udp port 1900
E finalmente hai visto:
Codice
IP 192.168.1.2 > 239.255.255.250:1900 UDP
→ Conferma che macOS ora lascia uscire i pacchetti SSDP.
DIPENDENZE minime
pip install flask netifaces
DIPENDENZE per altri test DLNA
lxml → per fare parsing XML più robusto (prima di passare a parsing manuale)
requests → per testare SOAP verso TV reali
flask → già incluso, ma reinstallato
pip install flask lxml requests
COME AVVIARE
python3 dlna_receiver.py
ma va avviato anche VLC in modalità player DLNA
/Applications/VLC.app/Contents/MacOS/VLC --extraintf rc --rc-host localhost:9999

665
dlna_receiver.py Normal file
View file

@ -0,0 +1,665 @@
import socket
import struct
import threading
import time
import subprocess
import netifaces
from flask import Flask, request, Response
# =========================================================
# LG webOS TV OLED65C1 — DLNA MediaRenderer (PARTE 1/3)
# =========================================================
DEVICE_NAME = "LG webOS TV OLED65C1"
HTTP_PORT = 8200
SSDP_PORT = 1900
UUID = "uuid:lg-webos-oled65c1-1234"
# ---------------------------------------------------------
# IP DETECTION
# ---------------------------------------------------------
def get_ip():
for iface in netifaces.interfaces():
addrs = netifaces.ifaddresses(iface)
if netifaces.AF_INET in addrs:
for addr in addrs[netifaces.AF_INET]:
ip = addr['addr']
if not ip.startswith("127."):
return ip
return "127.0.0.1"
LOCAL_IP = get_ip()
print("IP rilevato:", LOCAL_IP)
# ---------------------------------------------------------
# SSDP ANNOUNCE (LG webOS style)
# ---------------------------------------------------------
def ssdp_announce():
NOTIFY_TEMPLATES = [
# rootdevice
(
"NOTIFY * HTTP/1.1\r\n"
"HOST: 239.255.255.250:1900\r\n"
"CACHE-CONTROL: max-age=1800\r\n"
f"LOCATION: http://{LOCAL_IP}:{HTTP_PORT}/device.xml\r\n"
"NT: upnp:rootdevice\r\n"
"NTS: ssdp:alive\r\n"
"SERVER: webOS/4.0 UPnP/1.0 LGDLNA/1.0\r\n"
f"USN: {UUID}::upnp:rootdevice\r\n"
"\r\n"
),
# MediaRenderer
(
"NOTIFY * HTTP/1.1\r\n"
"HOST: 239.255.255.250:1900\r\n"
"CACHE-CONTROL: max-age=1800\r\n"
f"LOCATION: http://{LOCAL_IP}:{HTTP_PORT}/device.xml\r\n"
"NT: urn:schemas-upnp-org:device:MediaRenderer:1\r\n"
"NTS: ssdp:alive\r\n"
"SERVER: webOS/4.0 UPnP/1.0 LGDLNA/1.0\r\n"
f"USN: {UUID}::urn:schemas-upnp-org:device:MediaRenderer:1\r\n"
"\r\n"
),
# AVTransport
(
"NOTIFY * HTTP/1.1\r\n"
"HOST: 239.255.255.250:1900\r\n"
"CACHE-CONTROL: max-age=1800\r\n"
f"LOCATION: http://{LOCAL_IP}:{HTTP_PORT}/device.xml\r\n"
"NT: urn:schemas-upnp-org:service:AVTransport:1\r\n"
"NTS: ssdp:alive\r\n"
"SERVER: webOS/4.0 UPnP/1.0 LGDLNA/1.0\r\n"
f"USN: {UUID}::urn:schemas-upnp-org:service:AVTransport:1\r\n"
"\r\n"
),
# RenderingControl
(
"NOTIFY * HTTP/1.1\r\n"
"HOST: 239.255.255.250:1900\r\n"
"CACHE-CONTROL: max-age=1800\r\n"
f"LOCATION: http://{LOCAL_IP}:{HTTP_PORT}/device.xml\r\n"
"NT: urn:schemas-upnp-org:service:RenderingControl:1\r\n"
"NTS: ssdp:alive\r\n"
"SERVER: webOS/4.0 UPnP/1.0 LGDLNA/1.0\r\n"
f"USN: {UUID}::urn:schemas-upnp-org:service:RenderingControl:1\r\n"
"\r\n"
),
# ⭐ ConnectionManager
(
"NOTIFY * HTTP/1.1\r\n"
"HOST: 239.255.255.250:1900\r\n"
"CACHE-CONTROL: max-age=1800\r\n"
f"LOCATION: http://{LOCAL_IP}:{HTTP_PORT}/device.xml\r\n"
"NT: urn:schemas-upnp-org:service:ConnectionManager:1\r\n"
"NTS: ssdp:alive\r\n"
"SERVER: webOS/4.0 UPnP/1.0 LGDLNA/1.0\r\n"
f"USN: {UUID}::urn:schemas-upnp-org:service:ConnectionManager:1\r\n"
"\r\n"
),
]
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP)
sock.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_TTL, 4)
sock.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_IF, socket.inet_aton(LOCAL_IP))
sock.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_LOOP, 1)
while True:
for msg in NOTIFY_TEMPLATES:
sock.sendto(msg.encode("utf-8"), ("239.255.255.250", SSDP_PORT))
print("SSDP NOTIFY sent (LG webOS style)")
time.sleep(3)
# ---------------------------------------------------------
# SSDP LISTENER
# ---------------------------------------------------------
def ssdp_listener():
MCAST_GRP = "239.255.255.250"
MCAST_PORT = 1900
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP)
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
try:
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1)
except:
pass
sock.bind(("", MCAST_PORT))
mreq = struct.pack("4s4s", socket.inet_aton(MCAST_GRP), socket.inet_aton(LOCAL_IP))
sock.setsockopt(socket.IPPROTO_IP, socket.IP_ADD_MEMBERSHIP, mreq)
print("SSDP listener attivo su 239.255.255.250:1900")
while True:
data, addr = sock.recvfrom(2048)
text = data.decode(errors="ignore").upper()
if "M-SEARCH" in text:
if "MEDIAR" in text or "ROOTDEVICE" in text or "AVTRANSPORT" in text or "RENDERINGCONTROL" in text or "CONNECTIONMANAGER" in text:
print("M-SEARCH ricevuto da", addr)
responses = [
# rootdevice
(
"HTTP/1.1 200 OK\r\n"
f"LOCATION: http://{LOCAL_IP}:{HTTP_PORT}/device.xml\r\n"
"EXT:\r\n"
"ST: upnp:rootdevice\r\n"
"SERVER: webOS/4.0 UPnP/1.0 LGDLNA/1.0\r\n"
f"USN: {UUID}::upnp:rootdevice\r\n"
"\r\n"
),
# MediaRenderer
(
"HTTP/1.1 200 OK\r\n"
f"LOCATION: http://{LOCAL_IP}:{HTTP_PORT}/device.xml\r\n"
"EXT:\r\n"
"ST: urn:schemas-upnp-org:device:MediaRenderer:1\r\n"
"SERVER: webOS/4.0 UPnP/1.0 LGDLNA/1.0\r\n"
f"USN: {UUID}::urn:schemas-upnp-org:device:MediaRenderer:1\r\n"
"\r\n"
),
]
for r in responses:
sock.sendto(r.encode("utf-8"), addr)
print("Risposte M-SEARCH inviate a", addr)
# ---------------------------------------------------------
# FLASK SERVER (device.xml + SCPD)
# ---------------------------------------------------------
app = Flask(__name__)
@app.route("/device.xml")
def device_desc():
xml = f"""<?xml version="1.0"?>
<root xmlns="urn:schemas-upnp-org:device-1-0">
<specVersion><major>1</major><minor>0</minor></specVersion>
<device>
<deviceType>urn:schemas-upnp-org:device:MediaRenderer:1</deviceType>
<friendlyName>{DEVICE_NAME}</friendlyName>
<manufacturer>LG Electronics</manufacturer>
<modelName>OLED65C1</modelName>
<UDN>{UUID}</UDN>
<serviceList>
<service>
<serviceType>urn:schemas-upnp-org:service:AVTransport:1</serviceType>
<serviceId>urn:upnp-org:serviceId:AVTransport</serviceId>
<controlURL>/upnp/control/avtransport</controlURL>
<eventSubURL>/upnp/event/avtransport</eventSubURL>
<SCPDURL>/avtransport.xml</SCPDURL>
</service>
<service>
<serviceType>urn:schemas-upnp-org:service:RenderingControl:1</serviceType>
<serviceId>urn:upnp-org:serviceId:RenderingControl</serviceId>
<controlURL>/upnp/control/renderingcontrol</controlURL>
<eventSubURL>/upnp/event/renderingcontrol</eventSubURL>
<SCPDURL>/renderingcontrol.xml</SCPDURL>
</service>
<!-- ConnectionManager -->
<service>
<serviceType>urn:schemas-upnp-org:service:ConnectionManager:1</serviceType>
<serviceId>urn:upnp-org:serviceId:ConnectionManager</serviceId>
<controlURL>/upnp/control/connectionmanager</controlURL>
<eventSubURL>/upnp/event/connectionmanager</eventSubURL>
<SCPDURL>/connectionmanager.xml</SCPDURL>
</service>
</serviceList>
</device>
</root>
"""
return Response(xml, mimetype="text/xml")
@app.route("/avtransport.xml")
def avtransport_scpd():
xml = """<?xml version="1.0"?>
<scpd xmlns="urn:schemas-upnp-org:service-1-0">
<specVersion><major>1</major><minor>0</minor></specVersion>
<actionList>
<action><name>SetAVTransportURI</name></action>
<action><name>Play</name></action>
<action><name>Pause</name></action>
<action><name>Stop</name></action>
<action><name>Seek</name></action>
<action><name>GetPositionInfo</name></action>
<action><name>GetTransportInfo</name></action>
<action><name>GetMediaInfo</name></action>
</actionList>
</scpd>
"""
return Response(xml, mimetype="text/xml")
@app.route("/renderingcontrol.xml")
def rendering_scpd():
xml = """<?xml version="1.0"?>
<scpd xmlns="urn:schemas-upnp-org:service-1-0">
<specVersion><major>1</major><minor>0</minor></specVersion>
<actionList>
<action><name>GetVolume</name></action>
<action><name>SetVolume</name></action>
<action><name>GetMute</name></action>
<action><name>SetMute</name></action>
</actionList>
</scpd>
"""
return Response(xml, mimetype="text/xml")
# ⭐ ConnectionManager SCPD
@app.route("/connectionmanager.xml")
def connectionmanager_scpd():
xml = """<?xml version="1.0"?>
<scpd xmlns="urn:schemas-upnp-org:service-1-0">
<specVersion><major>1</major><minor>0</minor></specVersion>
<actionList>
<action><name>GetProtocolInfo</name></action>
</actionList>
<serviceStateTable>
<stateVariable sendEvents="no">
<name>SourceProtocolInfo</name>
<dataType>string</dataType>
</stateVariable>
<stateVariable sendEvents="no">
<name>SinkProtocolInfo</name>
<dataType>string</dataType>
</stateVariable>
<stateVariable sendEvents="no">
<name>CurrentConnectionIDs</name>
<dataType>string</dataType>
</stateVariable>
</serviceStateTable>
</scpd>
"""
return Response(xml, mimetype="text/xml")
# =========================================================
# LG webOS TV OLED65C1 — DLNA MediaRenderer (PARTE 2/3)
# =========================================================
# ---------------------------------------------------------
# VLC RC CONTROL (localhost:9999)
# ---------------------------------------------------------
VLC_RC_HOST = "127.0.0.1"
VLC_RC_PORT = 9999
def vlc_rc_send(cmd, expect_reply=False):
try:
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.settimeout(1.0)
s.connect((VLC_RC_HOST, VLC_RC_PORT))
# consume banner
try:
s.recv(4096)
except:
pass
s.sendall((cmd + "\n").encode("utf-8"))
if expect_reply:
data = s.recv(4096).decode("utf-8", errors="ignore")
s.close()
return data
s.close()
except Exception as e:
print("VLC RC error:", e)
return ""
def vlc_play_uri(uri):
print("VLC PLAY URI:", uri)
vlc_rc_send("clear")
vlc_rc_send(f"add {uri}")
def vlc_stop():
print("VLC STOP")
vlc_rc_send("stop")
def vlc_pause():
print("VLC PAUSE")
vlc_rc_send("pause")
def vlc_seek_abs(seconds):
print("VLC SEEK:", seconds)
vlc_rc_send(f"seek {int(seconds)}")
def vlc_set_volume(vol):
v = max(0, min(100, int(vol)))
vlc_rc_send(f"volume {v}")
def vlc_get_time():
out = vlc_rc_send("get_time", expect_reply=True)
try:
for line in out.splitlines():
if line.strip().isdigit():
return int(line.strip())
except:
pass
return 0
def vlc_get_length():
out = vlc_rc_send("get_length", expect_reply=True)
try:
for line in out.splitlines():
if line.strip().isdigit():
return int(line.strip())
except:
pass
return 0
# ---------------------------------------------------------
# DLNA STATE
# ---------------------------------------------------------
current_uri = ""
current_metadata = ""
current_volume = 50
current_mute = False
# ---------------------------------------------------------
# SOAP ENVELOPE
# ---------------------------------------------------------
def soap_envelope(body_xml):
return f"""<?xml version="1.0"?>
<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/">
<s:Body>
{body_xml}
</s:Body>
</s:Envelope>
"""
# ---------------------------------------------------------
# AVTRANSPORT HANDLER
# ---------------------------------------------------------
def handle_avtransport(body):
global current_uri, current_metadata
b = body
# -----------------------------
# SetAVTransportURI
# -----------------------------
if "SetAVTransportURI" in b:
import re
uri = ""
meta = ""
m = re.search(r"<CurrentURI>(.*?)</CurrentURI>", b, re.DOTALL)
if m:
uri = m.group(1)
m2 = re.search(r"<CurrentURIMetaData>(.*?)</CurrentURIMetaData>", b, re.DOTALL)
if m2:
meta = m2.group(1)
current_uri = uri
current_metadata = meta
print("SetAVTransportURI:", current_uri)
if current_uri:
vlc_play_uri(current_uri)
return soap_envelope(
'<u:SetAVTransportURIResponse xmlns:u="urn:schemas-upnp-org:service:AVTransport:1"/>'
)
# -----------------------------
# Play
# -----------------------------
if "Play" in b:
print("AVTransport Play")
vlc_rc_send("play")
return soap_envelope(
'<u:PlayResponse xmlns:u="urn:schemas-upnp-org:service:AVTransport:1"/>'
)
# -----------------------------
# Pause
# -----------------------------
if "Pause" in b:
print("AVTransport Pause")
vlc_pause()
return soap_envelope(
'<u:PauseResponse xmlns:u="urn:schemas-upnp-org:service:AVTransport:1"/>'
)
# -----------------------------
# Stop
# -----------------------------
if "Stop" in b:
print("AVTransport Stop")
vlc_stop()
return soap_envelope(
'<u:StopResponse xmlns:u="urn:schemas-upnp-org:service:AVTransport:1"/>'
)
# -----------------------------
# Seek
# -----------------------------
if "Seek" in b:
import re
m = re.search(r"<Target>(.*?)</Target>", b)
if m:
target = m.group(1)
parts = target.split(":")
if len(parts) == 3:
h, m_, s = parts
seconds = int(h) * 3600 + int(m_) * 60 + int(s)
vlc_seek_abs(seconds)
return soap_envelope(
'<u:SeekResponse xmlns:u="urn:schemas-upnp-org:service:AVTransport:1"/>'
)
# -----------------------------
# GetPositionInfo
# -----------------------------
if "GetPositionInfo" in b:
rel_time = vlc_get_time()
dur = vlc_get_length()
def fmt(t):
h = t // 3600
m_ = (t % 3600) // 60
s = t % 60
return f"{h:02d}:{m_:02d}:{s:02d}"
rel_str = fmt(rel_time)
dur_str = fmt(dur)
body_xml = f"""
<u:GetPositionInfoResponse xmlns:u="urn:schemas-upnp-org:service:AVTransport:1">
<Track>1</Track>
<TrackDuration>{dur_str}</TrackDuration>
<TrackMetaData>{current_metadata}</TrackMetaData>
<TrackURI>{current_uri}</TrackURI>
<RelTime>{rel_str}</RelTime>
<AbsTime>{rel_str}</AbsTime>
<RelCount>0</RelCount>
<AbsCount>0</AbsCount>
</u:GetPositionInfoResponse>
"""
return soap_envelope(body_xml)
# -----------------------------
# GetTransportInfo
# -----------------------------
if "GetTransportInfo" in b:
state = "STOPPED"
if vlc_get_time() > 0:
state = "PLAYING"
body_xml = f"""
<u:GetTransportInfoResponse xmlns:u="urn:schemas-upnp-org:service:AVTransport:1">
<CurrentTransportState>{state}</CurrentTransportState>
<CurrentTransportStatus>OK</CurrentTransportStatus>
<CurrentSpeed>1</CurrentSpeed>
</u:GetTransportInfoResponse>
"""
return soap_envelope(body_xml)
# -----------------------------
# GetMediaInfo
# -----------------------------
if "GetMediaInfo" in b:
dur = vlc_get_length()
h = dur // 3600
m_ = (dur % 3600) // 60
s = dur % 60
dur_str = f"{h:02d}:{m_:02d}:{s:02d}"
body_xml = f"""
<u:GetMediaInfoResponse xmlns:u="urn:schemas-upnp-org:service:AVTransport:1">
<NrTracks>1</NrTracks>
<MediaDuration>{dur_str}</MediaDuration>
<CurrentURI>{current_uri}</CurrentURI>
<CurrentURIMetaData>{current_metadata}</CurrentURIMetaData>
<NextURI></NextURI>
<NextURIMetaData></NextURIMetaData>
<PlayMedium>NETWORK</PlayMedium>
<RecordMedium>NOT_IMPLEMENTED</RecordMedium>
<WriteStatus>NOT_IMPLEMENTED</WriteStatus>
</u:GetMediaInfoResponse>
"""
return soap_envelope(body_xml)
# -----------------------------
# Default
# -----------------------------
return soap_envelope(
'<u:GenericResponse xmlns:u="urn:schemas-upnp-org:service:AVTransport:1"/>'
)
# =========================================================
# LG webOS TV OLED65C1 — DLNA MediaRenderer (PARTE 3/3)
# =========================================================
# ---------------------------------------------------------
# RENDERINGCONTROL HANDLER
# ---------------------------------------------------------
def handle_rendering_control(body):
global current_volume, current_mute
b = body
# -----------------------------
# SetVolume
# -----------------------------
if "SetVolume" in b:
import re
m = re.search(r"<DesiredVolume>(.*?)</DesiredVolume>", b)
if m:
try:
v = int(m.group(1))
current_volume = max(0, min(100, v))
print("SetVolume:", current_volume)
if not current_mute:
vlc_set_volume(current_volume)
except:
pass
return soap_envelope(
'<u:SetVolumeResponse xmlns:u="urn:schemas-upnp-org:service:RenderingControl:1"/>'
)
# -----------------------------
# GetVolume
# -----------------------------
if "GetVolume" in b:
body_xml = f"""
<u:GetVolumeResponse xmlns:u="urn:schemas-upnp-org:service:RenderingControl:1">
<CurrentVolume>{current_volume}</CurrentVolume>
</u:GetVolumeResponse>
"""
return soap_envelope(body_xml)
# -----------------------------
# SetMute
# -----------------------------
if "SetMute" in b:
current_mute = "true" in b.lower()
print("SetMute:", current_mute)
if current_mute:
vlc_set_volume(0)
else:
vlc_set_volume(current_volume)
return soap_envelope(
'<u:SetMuteResponse xmlns:u="urn:schemas-upnp-org:service:RenderingControl:1"/>'
)
# -----------------------------
# GetMute
# -----------------------------
if "GetMute" in b:
val = "1" if current_mute else "0"
body_xml = f"""
<u:GetMuteResponse xmlns:u="urn:schemas-upnp-org:service:RenderingControl:1">
<CurrentMute>{val}</CurrentMute>
</u:GetMuteResponse>
"""
return soap_envelope(body_xml)
# -----------------------------
# Default
# -----------------------------
return soap_envelope(
'<u:RenderingControlResponse xmlns:u="urn:schemas-upnp-org:service:RenderingControl:1"/>'
)
# ---------------------------------------------------------
# CONNECTIONMANAGER HANDLER
# ---------------------------------------------------------
def handle_connection_manager(body):
# Risposta minimale ma valida per AVES
sink = ",".join([
"http-get:*:video/mp4:*",
"http-get:*:video/x-matroska:*",
"http-get:*:image/jpeg:*",
"http-get:*:image/png:*",
])
body_xml = f"""
<u:GetProtocolInfoResponse xmlns:u="urn:schemas-upnp-org:service:ConnectionManager:1">
<SourceProtocolInfo></SourceProtocolInfo>
<SinkProtocolInfo>{sink}</SinkProtocolInfo>
<CurrentConnectionIDs>0</CurrentConnectionIDs>
</u:GetProtocolInfoResponse>
"""
return soap_envelope(body_xml)
# ---------------------------------------------------------
# FLASK ROUTES FINALI
# ---------------------------------------------------------
@app.route("/upnp/control/avtransport", methods=["POST"])
def avtransport_control():
body = request.data.decode()
resp = handle_avtransport(body)
return Response(resp, mimetype="text/xml")
@app.route("/upnp/control/renderingcontrol", methods=["POST"])
def rendering_control():
body = request.data.decode()
resp = handle_rendering_control(body)
return Response(resp, mimetype="text/xml")
@app.route("/upnp/control/connectionmanager", methods=["POST"])
def connectionmanager_control():
body = request.data.decode()
resp = handle_connection_manager(body)
return Response(resp, mimetype="text/xml")
# ---------------------------------------------------------
# MAIN
# ---------------------------------------------------------
if __name__ == "__main__":
threading.Thread(target=ssdp_announce, daemon=True).start()
threading.Thread(target=ssdp_listener, daemon=True).start()
print("LG webOS TV OLED65C1 — DLNA Renderer attivo su:", LOCAL_IP)
print("Assicurati che VLC sia avviato con:")
print("/Applications/VLC.app/Contents/MacOS/VLC --extraintf rc --rc-host localhost:9999")
app.run(host="0.0.0.0", port=HTTP_PORT)