Python DNLA模拟接收与推送机顶盒
·

由于家里的安卓机器人可以投放DLNA网上找了一圈没有合适的推送与接收,导致自己看除能投屏的APP以外想投送到安卓机器人上面不友好,配合猫爪浏览器插件,简直不要太爽
特此写了下面的代码
1、搜索局域网支持DLNA的设备,可以进行投放

2、虚拟一个机顶盒,APP手机端可以投放到此软件上,然后接收到投放的视频地址,然后可以下载,以及推送到其他设备上。

下面是完整的代码
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
DLNA 双向工具(调试增强版):
Tab1: 推送到真实设备
Tab2: 启动虚拟 DLNA 接收器,捕获手机推送的视频地址
"""
import tkinter as tk
from tkinter import ttk, messagebox
import threading
import socket
import struct
import re
import xml.etree.ElementTree as ET
from http.server import BaseHTTPRequestHandler, HTTPServer
import uuid
import time
import urllib.request
# ========================
# 调试开关
# ========================
DEBUG_SSDP = True # 启用 SSDP 调试日志
# ========================
# 全局变量
# ========================
virtual_dlna_server = None
virtual_dlna_thread = None
ssdp_responder_thread = None
stop_ssdp = threading.Event()
received_urls = []
def get_local_ip():
try:
with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as s:
s.connect(("10.255.255.250", 80))
ip = s.getsockname()[0]
if not ip.startswith(('127.', '169.254.')):
return ip
except:
pass
try:
interfaces = socket.getaddrinfo(socket.gethostname(), None)
for addr_info in interfaces:
ip = addr_info[4][0]
if ip and not ip.startswith(('127.', '169.254.')) and '.' in ip:
return ip
except:
pass
return "127.0.0.1"
LOCAL_IP = get_local_ip()
HTTP_PORT = 18000
DEVICE_UUID = str(uuid.uuid4())
FRIENDLY_NAME = "PyDLNA"
# ========================
# 设备描述 XML
# ========================
DEVICE_XML = f"""<?xml version="1.0" encoding="UTF-8"?>
<root xmlns="urn:schemas-upnp-org:device-1-0"
xmlns:dlna="urn:schemas-dlna-org:device-1-0">
<specVersion>
<major>1</major>
<minor>0</minor>
</specVersion>
<URLBase>http://{LOCAL_IP}:{HTTP_PORT}/</URLBase>
<device>
<deviceType>urn:schemas-upnp-org:device:MediaRenderer:1</deviceType>
<friendlyName>{FRIENDLY_NAME}</friendlyName>
<manufacturer>Python DLNA Project</manufacturer>
<manufacturerURL>https://python.org</manufacturerURL>
<modelDescription>Virtual DLNA Media Renderer</modelDescription>
<modelName>Python DLNA Receiver</modelName>
<modelNumber>1.0</modelNumber>
<modelURL>https://python.org</modelURL>
<serialNumber>{DEVICE_UUID[:12].upper()}</serialNumber>
<UDN>uuid:{DEVICE_UUID}</UDN>
<presentationURL>http://{LOCAL_IP}:{HTTP_PORT}/</presentationURL>
<iconList>
<icon>
<mimetype>image/png</mimetype>
<width>48</width>
<height>48</height>
<depth>24</depth>
<url>/icon.png</url>
</icon>
</iconList>
<serviceList>
<service>
<serviceType>urn:schemas-upnp-org:service:AVTransport:1</serviceType>
<serviceId>urn:upnp-org:serviceId:AVTransport</serviceId>
<controlURL>/ctl/AVTransport</controlURL>
<eventSubURL>/evt/AVTransport</eventSubURL>
<SCPDURL>/AVTransport.xml</SCPDURL>
</service>
<service>
<serviceType>urn:schemas-upnp-org:service:ConnectionManager:1</serviceType>
<serviceId>urn:upnp-org:serviceId:ConnectionManager</serviceId>
<controlURL>/ctl/ConnectionManager</controlURL>
<eventSubURL>/evt/ConnectionManager</eventSubURL>
<SCPDURL>/ConnectionManager.xml</SCPDURL>
</service>
<service>
<serviceType>urn:schemas-upnp-org:service:RenderingControl:1</serviceType>
<serviceId>urn:upnp-org:serviceId:RenderingControl</serviceId>
<controlURL>/ctl/RenderingControl</controlURL>
<eventSubURL>/evt/RenderingControl</eventSubURL>
<SCPDURL>/RenderingControl.xml</SCPDURL>
</service>
</serviceList>
<dlna:X_DLNADOC xmlns:dlna="urn:schemas-dlna-org:device-1-0">DMS-1.50</dlna:X_DLNADOC>
<dlna:X_DLNACAP xmlns:dlna="urn:schemas-dlna-org:device-1-0">playcontainer-0-1</dlna:X_DLNACAP>
</device>
</root>"""
AVT_SCPD = """<?xml version="1.0"?>
<scpd xmlns="urn:schemas-upnp-org:service-1-0">
<actionList>
<action><name>SetAVTransportURI</name>
<argumentList>
<argument><name>InstanceID</name><direction>in</direction><relatedStateVariable>A_ARG_TYPE_InstanceID</relatedStateVariable></argument>
<argument><name>CurrentURI</name><direction>in</direction><relatedStateVariable>AVTransportURI</relatedStateVariable></argument>
<argument><name>CurrentURIMetaData</name><direction>in</direction><relatedStateVariable>AVTransportURIMetaData</relatedStateVariable></argument>
</argumentList>
</action>
<action><name>Play</name>
<argumentList>
<argument><name>InstanceID</name><direction>in</direction><relatedStateVariable>A_ARG_TYPE_InstanceID</relatedStateVariable></argument>
<argument><name>Speed</name><direction>in</direction><relatedStateVariable>TransportPlaySpeed</relatedStateVariable></argument>
</argumentList>
</action>
</actionList>
<serviceStateTable>
<stateVariable sendEvents="no"><name>A_ARG_TYPE_InstanceID</name><dataType>ui4</dataType></stateVariable>
<stateVariable sendEvents="no"><name>AVTransportURI</name><dataType>string</dataType></stateVariable>
<stateVariable sendEvents="no"><name>AVTransportURIMetaData</name><dataType>string</dataType></stateVariable>
<stateVariable sendEvents="no"><name>TransportPlaySpeed</name><dataType>string</dataType></stateVariable>
</serviceStateTable>
</scpd>"""
RENDERING_SCPD = """<?xml version="1.0"?>
<scpd xmlns="urn:schemas-upnp-org:service-1-0">
<actionList>
<action><name>GetVolume</name></action>
<action><name>SetVolume</name></action>
</actionList>
<serviceStateTable>
<stateVariable sendEvents="no"><name>Volume</name><dataType>ui2</dataType></stateVariable>
</serviceStateTable>
</scpd>"""
CONNECTION_MANAGER_SCPD = """<?xml version="1.0"?>
<scpd xmlns="urn:schemas-upnp-org:service-1-0">
<actionList>
<action>
<name>GetProtocolInfo</name>
<argumentList>
<argument>
<name>Source</name>
<direction>out</direction>
<relatedStateVariable>SourceProtocolInfo</relatedStateVariable>
</argument>
<argument>
<name>Sink</name>
<direction>out</direction>
<relatedStateVariable>SinkProtocolInfo</relatedStateVariable>
</argument>
</argumentList>
</action>
<action>
<name>GetCurrentConnectionIDs</name>
<argumentList>
<argument>
<name>ConnectionIDs</name>
<direction>out</direction>
<relatedStateVariable>CurrentConnectionIDs</relatedStateVariable>
</argument>
</argumentList>
</action>
</actionList>
<serviceStateTable>
<stateVariable sendEvents="no">
<name>SourceProtocolInfo</name>
<dataType>string</dataType>
<defaultValue></defaultValue>
</stateVariable>
<stateVariable sendEvents="no">
<name>SinkProtocolInfo</name>
<dataType>string</dataType>
<defaultValue>http-get:*:video/*:*</defaultValue>
</stateVariable>
<stateVariable sendEvents="no">
<name>CurrentConnectionIDs</name>
<dataType>string</dataType>
<defaultValue>0</defaultValue>
</stateVariable>
</serviceStateTable>
</scpd>"""
# ========================
# HTTP 服务器
# ========================
class VirtualDLNARenderer(BaseHTTPRequestHandler):
def do_GET(self):
if self.path == "/device.xml":
self.send_response(200)
self.send_header("Content-Type", "text/xml; charset=utf-8")
self.end_headers()
self.wfile.write(DEVICE_XML.encode())
elif self.path == "/AVTransport.xml":
self.send_response(200)
self.send_header("Content-Type", "text/xml; charset=utf-8")
self.end_headers()
self.wfile.write(AVT_SCPD.encode())
elif self.path == "/RenderingControl.xml":
self.send_response(200)
self.send_header("Content-Type", "text/xml; charset=utf-8")
self.end_headers()
self.wfile.write(RENDERING_SCPD.encode())
elif self.path == "/ConnectionManager.xml":
self.send_response(200)
self.send_header("Content-Type", "text/xml; charset=utf-8")
self.end_headers()
self.wfile.write(CONNECTION_MANAGER_SCPD.encode())
elif self.path == "/icon.png":
# 1x1 透明 PNG
png_data = b'\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00\x01\x00\x00\x00\x01\x08\x06\x00\x00\x00\x1f\x15\xc4\x89\x00\x00\x00\nIDATx\x9cc\xf8\x0f\x00\x00\x01\x00\x01}\x0d\x95v\x00\x00\x00\x00IEND\xaeB`\x82'
self.send_response(200)
self.send_header("Content-Type", "image/png")
self.end_headers()
self.wfile.write(png_data)
else:
self.send_error(404)
def do_POST(self):
if self.path == "/ctl/AVTransport":
content_length = int(self.headers['Content-Length'])
post_data = self.rfile.read(content_length).decode('utf-8')
if "<u:SetAVTransportURI" in post_data:
uri_match = re.search(r"<CurrentURI>(.*?)</CurrentURI>", post_data)
if uri_match:
video_url = uri_match.group(1)
print(f"[📱 手机投屏] 收到视频地址: {video_url}")
received_urls.append(video_url)
if hasattr(self.server, 'gui_callback'):
self.server.gui_callback(video_url)
self.send_response(200)
self.send_header("Content-Type", "text/xml; charset=utf-8")
self.end_headers()
self.wfile.write(b'''<?xml version="1.0"?>
<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/" s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">
<s:Body><u:SetAVTransportURIResponse xmlns:u="urn:schemas-upnp-org:service:AVTransport:1"></u:SetAVTransportURIResponse></s:Body>
</s:Envelope>''')
elif self.path in ("/ctl/RenderingControl", "/ctl/ConnectionManager"):
self.send_response(200)
self.send_header("Content-Type", "text/xml; charset=utf-8")
self.end_headers()
self.wfile.write(b'<?xml version="1.0"?><s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/"/>')
else:
self.send_error(404)
def log_message(self, format, *args):
if DEBUG_SSDP:
print(f"[HTTP] {format%args}")
# ========================
# SSDP 服务器(终极兼容版)
# ========================
def ssdp_responder():
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP)
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
sock.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_TTL, 2)
sock.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_LOOP, 1)
try:
# 绑定到所有接口
sock.bind(("", 1900))
# 加入多播组
mreq = struct.pack("4sl", socket.inet_aton("239.255.255.250"), socket.INADDR_ANY)
sock.setsockopt(socket.IPPROTO_IP, socket.IP_ADD_MEMBERSHIP, mreq)
except Exception as e:
print(f"❌ SSDP绑定失败: {e}")
return
print(f"[📡 SSDP] 服务启动于 {LOCAL_IP}:1900")
print(f"[📡 SSDP] 设备名称: {FRIENDLY_NAME}")
while not stop_ssdp.is_set():
try:
data, addr = sock.recvfrom(65507)
if b'M-SEARCH' in data:
# 解析请求
st_match = re.search(rb'ST: (.*?)\r\n', data, re.IGNORECASE)
st = st_match.group(1).decode().strip() if st_match else "ssdp:all"
mx_match = re.search(rb'MX: (\d+)', data, re.IGNORECASE)
mx = int(mx_match.group(1)) if mx_match else 1
if DEBUG_SSDP:
print(f"[📡 SSDP] 收到搜索请求 from {addr}, ST={st}, MX={mx}")
# 构建响应
response = (
'HTTP/1.1 200 OK\r\n'
f'CACHE-CONTROL: max-age=1800\r\n'
f'LOCATION: http://{LOCAL_IP}:{HTTP_PORT}/device.xml\r\n'
f'SERVER: Linux/5.4.0, UPnP/1.0, DLNADOC/1.50\r\n' # 模拟常见设备的SERVER字段
f'ST: {st}\r\n' # 直接使用搜索请求中的ST字段,不固定写死
f'USN: uuid:{DEVICE_UUID}::{st}\r\n'
'EXT:\r\n'
'\r\n'
).encode('utf-8')
# 延迟响应(模拟真实设备行为,提高兼容性)
time.sleep(0.1)
sock.sendto(response, addr)
# 如果搜索的是 all 或 rootdevice,也响应
if 'ssdp:all' in st.lower() or 'rootdevice' in st.lower():
response_all = (
'HTTP/1.1 200 OK\r\n'
f'CACHE-CONTROL: max-age=1800\r\n'
f'LOCATION: http://{LOCAL_IP}:{HTTP_PORT}/device.xml\r\n'
f'SERVER: UPnP/1.0 DLNADOC/1.50\r\n'
f'ST: ssdp:all\r\n'
f'USN: uuid:{DEVICE_UUID}::ssdp:all\r\n'
'EXT:\r\n'
'\r\n'
).encode('utf-8')
sock.sendto(response_all, addr)
except OSError as e:
if not stop_ssdp.is_set():
print(f"[SSDP错误] {e}")
break
except Exception as e:
print(f"[SSDP未知错误] {e}")
break
sock.close()
# ========================
# 启动/停止虚拟 DLNA 接收器
# ========================
def start_virtual_dlna(gui_callback=None):
global virtual_dlna_server, virtual_dlna_thread, ssdp_responder_thread
if virtual_dlna_server:
return
virtual_dlna_server = HTTPServer((LOCAL_IP, HTTP_PORT), VirtualDLNARenderer)
if gui_callback:
virtual_dlna_server.gui_callback = gui_callback
virtual_dlna_thread = threading.Thread(target=virtual_dlna_server.serve_forever, daemon=True)
virtual_dlna_thread.start()
stop_ssdp.clear()
ssdp_responder_thread = threading.Thread(target=ssdp_responder, daemon=True)
ssdp_responder_thread.start()
print(f"✅ 虚拟DLNA接收器已启动: {FRIENDLY_NAME} @ {LOCAL_IP}:{HTTP_PORT}")
def stop_virtual_dlna():
global virtual_dlna_server, ssdp_responder_thread
if virtual_dlna_server:
virtual_dlna_server.shutdown()
virtual_dlna_server = None
stop_ssdp.set()
if ssdp_responder_thread:
ssdp_responder_thread.join(timeout=1)
# ========================
# SSDP 发现函数
# ========================
def discover_dlna_devices(timeout=3):
devices = set()
msearch_msg = (
'M-SEARCH * HTTP/1.1\r\n'
'HOST: 239.255.255.250:1900\r\n'
'MAN: "ssdp:discover"\r\n'
'MX: 1\r\n'
'ST: urn:schemas-upnp-org:device:MediaRenderer:1\r\n'
'\r\n'
)
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP)
sock.settimeout(timeout)
sock.sendto(msearch_msg.encode(), ("239.255.255.250", 1900))
while True:
try:
data, addr = sock.recvfrom(65507)
response = data.decode('utf-8', errors='ignore')
headers = {}
for line in response.split('\r\n')[1:]:
if ':' in line:
k, v = line.split(':', 1)
headers[k.strip().lower()] = v.strip()
location = headers.get('location')
if not location or location in [d[1] for d in devices]:
continue
ip = addr[0]
try:
with urllib.request.urlopen(location, timeout=3) as f:
xml_data = f.read()
root = ET.fromstring(xml_data)
ns = {'root': 'urn:schemas-upnp-org:device-1-0'}
friendly_name_elem = root.find('.//root:friendlyName', ns)
friendly_name = friendly_name_elem.text if friendly_name_elem is not None else f"Unknown ({ip})"
devices.add((friendly_name, location, ip))
except Exception as e:
print(f"[解析设备XML失败] {location}: {e}")
except socket.timeout:
break
except Exception as e:
print(f"[SSDP接收异常]: {e}")
break
sock.close()
return list(devices)
# ========================
# DLNA 推送函数
# ========================
def send_dlna_set_uri(control_url, instance_id=0, media_url="", meta_data=""):
soap_body = f"""<?xml version="1.0" encoding="utf-8"?>
<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/" s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">
<s:Body>
<u:SetAVTransportURI xmlns:u="urn:schemas-upnp-org:service:AVTransport:1">
<InstanceID>{instance_id}</InstanceID>
<CurrentURI>{media_url}</CurrentURI>
<CurrentURIMetaData>{meta_data}</CurrentURIMetaData>
</u:SetAVTransportURI>
</s:Body>
</s:Envelope>"""
headers = {
'Content-Type': 'text/xml; charset="utf-8"',
'SOAPACTION': '"urn:schemas-upnp-org:service:AVTransport:1#SetAVTransportURI"',
'User-Agent': 'Python DLNA Client',
'Connection': 'close'
}
try:
req = urllib.request.Request(control_url, data=soap_body.encode('utf-8'), headers=headers)
with urllib.request.urlopen(req, timeout=5) as response:
if response.status == 200:
return True
except Exception as e:
print(f"[推送失败] {e}")
return False
return False
def send_dlna_play(control_url, instance_id=0):
soap_body = f"""<?xml version="1.0" encoding="utf-8"?>
<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/" s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">
<s:Body>
<u:Play xmlns:u="urn:schemas-upnp-org:service:AVTransport:1">
<InstanceID>{instance_id}</InstanceID>
<Speed>1</Speed>
</u:Play>
</s:Body>
</s:Envelope>"""
headers = {
'Content-Type': 'text/xml; charset="utf-8"',
'SOAPACTION': '"urn:schemas-upnp-org:service:AVTransport:1#Play"',
'User-Agent': 'Python DLNA Client',
'Connection': 'close'
}
try:
req = urllib.request.Request(control_url, data=soap_body.encode('utf-8'), headers=headers)
with urllib.request.urlopen(req, timeout=5) as response:
if response.status == 200:
return True
except Exception as e:
print(f"[播放失败] {e}")
return False
return False
# ========================
# GUI 主类
# ========================
class DLNAGUI:
def __init__(self, root):
self.root = root
self.root.title("DLNA 双向工具(调试版)")
self.root.geometry("700x550")
self.discovered_real_devices = []
notebook = ttk.Notebook(root)
notebook.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
self.tab1 = ttk.Frame(notebook)
notebook.add(self.tab1, text="推送视频到机顶盒")
self.create_tab1()
self.tab2 = ttk.Frame(notebook)
notebook.add(self.tab2, text="接收手机投屏 (DLNA)")
self.create_tab2()
def create_tab1(self):
frame = ttk.Frame(self.tab1)
frame.pack(fill=tk.BOTH, expand=True, padx=20, pady=20)
ttk.Label(frame, text="设备列表:", font=("Arial", 10, "bold")).pack(anchor=tk.W, pady=(0, 5))
self.device_listbox = tk.Listbox(frame, height=8)
self.device_listbox.pack(fill=tk.BOTH, expand=True, pady=(0, 10))
ttk.Button(frame, text="🔍 刷新设备", command=self.refresh_devices).pack(pady=5)
ttk.Label(frame, text="视频地址:", font=("Arial", 10, "bold")).pack(anchor=tk.W, pady=(15, 5))
self.video_url_entry = ttk.Entry(frame)
self.video_url_entry.pack(fill=tk.X, pady=(0, 10))
self.video_url_entry.insert(0, "https://example.com/video.mp4") # 示例
ttk.Button(frame, text="🚀 推送视频", command=self.push_video).pack(pady=5)
def create_tab2(self):
info = f"📱 操作步骤:\n1. 点击下方【启动接收器】\n2. 在手机上打开视频App(如抖音、腾讯视频)\n3. 点击「投屏」或「TV」按钮\n4. 选择设备:「{FRIENDLY_NAME}」\n5. 视频地址将自动显示在下方\n\n📋 调试信息见控制台输出"
ttk.Label(self.tab2, text=info, justify=tk.LEFT).pack(padx=20, pady=(10, 20))
self.btn_toggle = ttk.Button(self.tab2, text="▶ 启动接收器", command=self.toggle_receiver)
self.btn_toggle.pack(pady=5)
ttk.Label(self.tab2, text="接收到的视频地址:", font=("Arial", 10, "bold")).pack(anchor=tk.W, padx=20, pady=(10, 0))
self.url_listbox = tk.Listbox(self.tab2, height=8)
self.url_listbox.pack(fill=tk.BOTH, expand=True, padx=20, pady=10)
btn_frame = ttk.Frame(self.tab2)
btn_frame.pack(pady=5)
ttk.Button(btn_frame, text="🗑 清空", command=lambda: self.url_listbox.delete(0, tk.END)).pack(side=tk.LEFT, padx=5)
ttk.Button(btn_frame, text="📋 复制选中", command=self.copy_selected).pack(side=tk.LEFT, padx=5)
def toggle_receiver(self):
if virtual_dlna_server is None:
start_virtual_dlna(gui_callback=self.on_url_received)
self.btn_toggle.config(text="⏹ 停止接收器")
messagebox.showinfo("提示", f"已启动!\n设备名称: {FRIENDLY_NAME}\nIP: {LOCAL_IP}\n\n请在手机投屏列表中选择该设备")
else:
stop_virtual_dlna()
self.btn_toggle.config(text="▶ 启动接收器")
def on_url_received(self, url):
self.root.after(0, lambda: self._update_url_list(url))
def _update_url_list(self, url):
self.url_listbox.insert(tk.END, url)
self.url_listbox.see(tk.END)
def copy_selected(self):
try:
selected = self.url_listbox.get(self.url_listbox.curselection())
self.root.clipboard_clear()
self.root.clipboard_append(selected)
messagebox.showinfo("提示", "已复制到剪贴板")
except tk.TclError:
messagebox.showwarning("警告", "请先选择一个地址")
def refresh_devices(self):
self.device_listbox.delete(0, tk.END)
self.device_listbox.insert(tk.END, "搜索中...")
self.root.update_idletasks()
threading.Thread(target=self._do_discover, daemon=True).start()
def _do_discover(self):
try:
devices = discover_dlna_devices(timeout=3)
self.root.after(0, self._update_device_list, devices)
except Exception as e:
print(f"[设备发现错误]: {e}")
self.root.after(0, lambda: self.device_listbox.delete(0, tk.END))
self.root.after(0, lambda: self.device_listbox.insert(tk.END, "发现失败,请重试"))
def _update_device_list(self, devices):
self.device_listbox.delete(0, tk.END)
self.discovered_real_devices = devices
if not devices:
self.device_listbox.insert(tk.END, "未找到 DLNA 投屏设备")
else:
for name, _, ip in devices:
self.device_listbox.insert(tk.END, f"{name} ({ip})")
def push_video(self):
url = self.video_url_entry.get().strip()
if not url:
messagebox.showwarning("警告", "请输入视频地址")
return
try:
index = self.device_listbox.curselection()[0]
device = self.discovered_real_devices[index]
name, location, ip = device
try:
with urllib.request.urlopen(location, timeout=3) as f:
xml_data = f.read()
root = ET.fromstring(xml_data)
ns = {'root': 'urn:schemas-upnp-org:device-1-0'}
service = root.find('.//root:service[root:serviceType="urn:schemas-upnp-org:service:AVTransport:1"]', ns)
if service is None:
messagebox.showerror("错误", "该设备不支持 AVTransport 服务")
return
control_url_elem = service.find('root:controlURL', ns)
if control_url_elem is None or not control_url_elem.text:
messagebox.showerror("错误", "无法获取 controlURL")
return
from urllib.parse import urljoin
full_control_url = urljoin(location, control_url_elem.text)
success_set = send_dlna_set_uri(full_control_url, media_url=url)
if success_set:
send_dlna_play(full_control_url)
messagebox.showinfo("成功", f"✅ 视频已推送到:\n{name}\n\n设备将开始加载播放。")
else:
messagebox.showerror("失败", "推送失败,请检查网络或设备是否支持。")
except Exception as e:
messagebox.showerror("错误", f"获取设备信息失败:{str(e)}")
except (IndexError, tk.TclError):
messagebox.showwarning("警告", "请先选择一个设备")
def on_closing(self):
stop_virtual_dlna()
self.root.destroy()
# ========================
# 主程序
# ========================
if __name__ == "__main__":
print(f"[🔧 调试信息]")
print(f"[🔧 本机IP: {LOCAL_IP}")
print(f"[🔧 HTTP端口: {HTTP_PORT}")
print(f"[🔧 设备名称: {FRIENDLY_NAME}")
print(f"[🔧 UUID: {DEVICE_UUID}")
print(f"[🔧 确保手机和电脑在同一WiFi网络]")
print(f"[🔧 运行后请查看控制台SSDP日志]")
print(f"[🔧 如果仍然搜索不到,请检查防火墙设置]")
root = tk.Tk()
app = DLNAGUI(root)
root.protocol("WM_DELETE_WINDOW", app.on_closing)
root.mainloop()
更多推荐

所有评论(0)