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"""
10
urn:schemas-upnp-org:device:MediaRenderer:1
{DEVICE_NAME}
LG Electronics
OLED65C1
{UUID}
urn:schemas-upnp-org:service:AVTransport:1
urn:upnp-org:serviceId:AVTransport
/upnp/control/avtransport
/upnp/event/avtransport
/avtransport.xml
urn:schemas-upnp-org:service:RenderingControl:1
urn:upnp-org:serviceId:RenderingControl
/upnp/control/renderingcontrol
/upnp/event/renderingcontrol
/renderingcontrol.xml
urn:schemas-upnp-org:service:ConnectionManager:1
urn:upnp-org:serviceId:ConnectionManager
/upnp/control/connectionmanager
/upnp/event/connectionmanager
/connectionmanager.xml
"""
return Response(xml, mimetype="text/xml")
@app.route("/avtransport.xml")
def avtransport_scpd():
xml = """
10
SetAVTransportURI
Play
Pause
Stop
Seek
GetPositionInfo
GetTransportInfo
GetMediaInfo
"""
return Response(xml, mimetype="text/xml")
@app.route("/renderingcontrol.xml")
def rendering_scpd():
xml = """
10
GetVolume
SetVolume
GetMute
SetMute
"""
return Response(xml, mimetype="text/xml")
# ⭐ ConnectionManager SCPD
@app.route("/connectionmanager.xml")
def connectionmanager_scpd():
xml = """
10
GetProtocolInfo
SourceProtocolInfo
string
SinkProtocolInfo
string
CurrentConnectionIDs
string
"""
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"""
{body_xml}
"""
# ---------------------------------------------------------
# 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"(.*?)", b, re.DOTALL)
if m:
uri = m.group(1)
m2 = re.search(r"(.*?)", 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(
''
)
# -----------------------------
# Play
# -----------------------------
if "Play" in b:
print("AVTransport Play")
vlc_rc_send("play")
return soap_envelope(
''
)
# -----------------------------
# Pause
# -----------------------------
if "Pause" in b:
print("AVTransport Pause")
vlc_pause()
return soap_envelope(
''
)
# -----------------------------
# Stop
# -----------------------------
if "Stop" in b:
print("AVTransport Stop")
vlc_stop()
return soap_envelope(
''
)
# -----------------------------
# Seek
# -----------------------------
if "Seek" in b:
import re
m = re.search(r"(.*?)", 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(
''
)
# -----------------------------
# 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"""
{dur_str}
{current_metadata}
{current_uri}
{rel_str}
{rel_str}
0
0
"""
return soap_envelope(body_xml)
# -----------------------------
# GetTransportInfo
# -----------------------------
if "GetTransportInfo" in b:
state = "STOPPED"
if vlc_get_time() > 0:
state = "PLAYING"
body_xml = f"""
{state}
OK
1
"""
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"""
1
{dur_str}
{current_uri}
{current_metadata}
NETWORK
NOT_IMPLEMENTED
NOT_IMPLEMENTED
"""
return soap_envelope(body_xml)
# -----------------------------
# Default
# -----------------------------
return soap_envelope(
''
)
# =========================================================
# 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"(.*?)", 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(
''
)
# -----------------------------
# GetVolume
# -----------------------------
if "GetVolume" in b:
body_xml = f"""
{current_volume}
"""
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(
''
)
# -----------------------------
# GetMute
# -----------------------------
if "GetMute" in b:
val = "1" if current_mute else "0"
body_xml = f"""
{val}
"""
return soap_envelope(body_xml)
# -----------------------------
# Default
# -----------------------------
return soap_envelope(
''
)
# ---------------------------------------------------------
# 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"""
{sink}
0
"""
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)