first commit
This commit is contained in:
commit
d3657cfd2b
2 changed files with 721 additions and 0 deletions
56
README.md
Normal file
56
README.md
Normal 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
665
dlna_receiver.py
Normal 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)
|
||||||
Loading…
Reference in a new issue