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