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""" 1 {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)