python dlna 投屏
python dlna 投屏 python-dlna,python投屏,python模拟投屏设备,python获取投屏视源,python伪装投屏设备 投屏分2种,投屏端如手机,被投屏端如电视。 本代码模拟的是投屏中的电视设备,被手机app搜索投屏视频的过程 dlna协议是投屏协议,可分成投屏端,和被投屏端 被投屏端主要就做2件件事,1服务被发现,2被交互处理 upnp是DLNA的基础协议,用于发现新加入局域网的设备 我分成2个脚本了,1upnp.py,2rsp.py pycharm运行,python3,占用本机8889端口 http://localhost:8889/dlna/info.xml运行后能看到返回
1upnp.py向多播地址宣告本服务
import socket
import http.server
import socketserver
import time
import struct
import xml.etree.ElementTree as ET
port = 8889
# uuid = "27d6877e-{}-ea12-abdf-cf8d50e36d54".format(randint(1000, 9999))
NLS = "27d6877e-6677-ea12-abdf-cf8d50e36d54" # 我不知道什么东西,格式和uuid一样,但是不是一个东西
uuid = "27d6877e-6677-ea12-abdf-cf8d50e36d54"
def getLocalIp():
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
s.connect(("119.29.29.29", 80))
ip = s.getsockname()[0]
s.close()
return ip
class SSDPServer(socketserver.UDPServer): # 用于创建一个能够监听指定端口的 UDP 服务器。
allow_reuse_address = True
class SSDPHandler(socketserver.BaseRequestHandler):
def handle(self): # 用于处理接收到的 SSDP 消息,并打印出来。
data = self.request[0].strip()
print(f"Received SSDP message:\n{data.decode('utf-8')}")
def send_ssdp_broadcast(): # 生成并发送六条 SSDP 消息。这些消息遵循 UPnP 规范,用于设备的发现和声明。http://{}:{}/dlna/info.xml'
# "LOCATION: http://"+ip+":"+str(port)+"/description.xml\r\n"
print("----六条 SSDP 消息")
ssdp_message1 = (
"NOTIFY * HTTP/1.1\r\n"
"HOST: 239.255.255.250:1900\r\n"
"CACHE-CONTROL: max-age=66\r\n"
"LOCATION: http://"+ip+":"+str(port)+"/dlna/info.xml\r\n"
"OPT: \"http://schemas.upnp.org/upnp/1/0/\"; ns=01\r\n"
"01-NLS: "+NLS+"\r\n"
"NT: upnp:rootdevice\r\n"
"NTS: ssdp:alive\r\n"
"SERVER: Linux/4.19.157-perf-download HTTP/1.0\r\n"
"X-User-Agent: redsonic\r\n"
"USN: uuid:"+uuid+"::upnp:rootdevice\r\n"
)
ssdp_message2 = (
"NOTIFY * HTTP/1.1\r\n"
"HOST: 239.255.255.250:1900\r\n"
"CACHE-CONTROL: max-age=66\r\n"
"LOCATION: http://"+ip+":"+str(port)+"/description.xml\r\n"
"OPT: \"http://schemas.upnp.org/upnp/1/0/\"; ns=01\r\n"
"01-NLS: "+NLS+"\r\n"
"NT: uuid:"+uuid+"\r\n"
"NTS: ssdp:alive\r\n"
"SERVER: Linux/4.19.157-perf-download HTTP/1.0\r\n"
"X-User-Agent: redsonic\r\n"
"USN: uuid:"+uuid+"\r\n"
)
ssdp_message3 = (
"NOTIFY * HTTP/1.1\r\n"
"HOST: 239.255.255.250:1900\r\n"
"CACHE-CONTROL: max-age=66\r\n"
"LOCATION: http://"+ip+":"+str(port)+"/description.xml\r\n"
"OPT: \"http://schemas.upnp.org/upnp/1/0/\"; ns=01\r\n"
"01-NLS: "+NLS+"\r\n"
"NT: urn:schemas-upnp-org:device:MediaRenderer:1\r\n"
"NTS: ssdp:alive\r\n"
"SERVER: Linux/4.19.157-perf-download HTTP/1.0\r\n"
"X-User-Agent: redsonic\r\n"
"USN: uuid:"+uuid+"::urn:schemas-upnp-org:device:MediaRenderer:1\r\n"
)
ssdp_message4 = (
"NOTIFY * HTTP/1.1\r\n"
"HOST: 239.255.255.250:1900\r\n"
"CACHE-CONTROL: max-age=66\r\n"
"LOCATION: http://"+ip+":"+str(port)+"/description.xml\r\n"
"OPT: \"http://schemas.upnp.org/upnp/1/0/\"; ns=01\r\n"
"01-NLS: "+NLS+"\r\n"
"NT: urn:schemas-upnp-org:service:AVTransport:1\r\n"
"NTS: ssdp:alive\r\n"
"SERVER: Linux/4.19.157-perf-download HTTP/1.0\r\n"
"X-User-Agent: redsonic\r\n"
"USN: uuid:"+uuid+"::urn:schemas-upnp-org:service:AVTransport:1\r\n"
)
ssdp_message5 = (
"NOTIFY * HTTP/1.1\r\n"
"HOST: 239.255.255.250:1900\r\n"
"CACHE-CONTROL: max-age=66\r\n"
"LOCATION: http://"+ip+":"+str(port)+"/description.xml\r\n"
"OPT: \"http://schemas.upnp.org/upnp/1/0/\"; ns=01\r\n"
"01-NLS: "+NLS+"\r\n"
"NT: urn:schemas-upnp-org:service:RenderingControl:1\r\n"
"NTS: ssdp:alive\r\n"
"SERVER: Linux/4.19.157-perf-download HTTP/1.0\r\n"
"X-User-Agent: redsonic\r\n"
"USN: uuid:"+uuid+"::urn:schemas-upnp-org:service:RenderingControl:1\r\n"
)
ssdp_message6 = (
"NOTIFY * HTTP/1.1\r\n"
"HOST: 239.255.255.250:1900\r\n"
"CACHE-CONTROL: max-age=66\r\n"
"LOCATION: http://"+ip+":"+str(port)+"/description.xml\r\n"
"OPT: \"http://schemas.upnp.org/upnp/1/0/\"; ns=01\r\n"
"01-NLS: "+NLS+"\r\n"
"NT: urn:schemas-upnp-org:service:ConnectionManager:1\r\n"
"NTS: ssdp:alive\r\n"
"SERVER: Linux/4.19.157-perf-download HTTP/1.0\r\n"
"X-User-Agent: redsonic\r\n"
"USN: uuid:"+uuid+"::urn:schemas-upnp-org:service:ConnectionManager:1\r\n"
)
multicast_group = ("239.255.255.250", 1900)
# Create a UDP socket
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.bind((ip, 0)) # 绑定到指定的 IP 地址
# Set the time-to-live for the socket
ttl = struct.pack('b', 1)
sock.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_TTL, ttl)
# Send the SSDP broadcast
sock.sendto(ssdp_message1.encode("utf-8"), multicast_group)
sock.close()
time.sleep(1)
# Create a UDP socket
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.bind((ip, 0)) # 绑定到指定的 IP 地址
# Set the time-to-live for the socket
sock.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_TTL, ttl)
# Send the SSDP broadcast
sock.sendto(ssdp_message2.encode("utf-8"), multicast_group)
sock.close()
time.sleep(1)
# Create a UDP socket
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.bind((ip, 0)) # 绑定到指定的 IP 地址
# Set the time-to-live for the socket
sock.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_TTL, ttl)
# Send the SSDP broadcast
sock.sendto(ssdp_message3.encode("utf-8"), multicast_group)
sock.close()
time.sleep(1)
# Create a UDP socket
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.bind((ip, 0)) # 绑定到指定的 IP 地址
# Set the time-to-live for the socket
sock.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_TTL, ttl)
# Send the SSDP broadcast
sock.sendto(ssdp_message4.encode("utf-8"), multicast_group)
sock.close()
time.sleep(1)
# Create a UDP socket
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.bind((ip, 0)) # 绑定到指定的 IP 地址
# Set the time-to-live for the socket
sock.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_TTL, ttl)
# Send the SSDP broadcast
sock.sendto(ssdp_message5.encode("utf-8"), multicast_group)
sock.close()
time.sleep(1)
# Create a UDP socket
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.bind((ip, 0)) # 绑定到指定的 IP 地址
# Set the time-to-live for the socket
sock.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_TTL, ttl)
# Send the SSDP broadcast
sock.sendto(ssdp_message6.encode("utf-8"), multicast_group)
sock.close()
time.sleep(1)
class MyHandler(http.server.SimpleHTTPRequestHandler):
def log_message(self, format, *args):
# Override the log_message method to suppress log messages
pass
def do_POST(self):
content_length = int(self.headers['Content-Length'])
post_data = self.rfile.read(content_length).decode('utf-8')
data = ET.fromstring(post_data)
current_uri_element = data.find(".//CurrentURI")
if current_uri_element is not None:
current_uri_value = current_uri_element.text
print("Current URI:", current_uri_value)
self.send_response(200)
self.send_header('Content-type', 'text/html')
self.end_headers()
self.wfile.write(b'POST request received successfully')
def start_web_server():
handler = MyHandler # 它使用 MyHandler 来处理请求。
httpd = socketserver.TCPServer(("0.0.0.0", port), handler)
print(f"Web server is running on http://localhost:{port}")
try:
httpd.serve_forever()
except KeyboardInterrupt:
pass
finally:
httpd.server_close()
print('一次http结束')
if __name__ == "__main__":
ip = getLocalIp()
print(ip)
# Start the SSDP server
while True:
send_ssdp_broadcast()
2rsp.py对手机的请求做出返回,获取视频链接
#!/usr/bin/env python
# encoding: utf-8
import socket
import datetime
from random import randint
from urllib import parse
import traceback
import _thread
from socketserver import ThreadingMixIn
from http.server import HTTPServer, BaseHTTPRequestHandler
import json
import xml.etree.ElementTree as ET
class PlayStatus:
playing = None
stoped = True
url = ""
meta = ""
class xmlReqParser:
def __init__(self, data):
self.data = data
def CurrentURI(self):
root = ET.fromstring(self.data)
namespaces = {'s': 'http://schemas.xmlsoap.org/soap/envelope/'}
value = root.findtext('s:Body//CurrentURI', None, namespaces)
return value
def CurrentURIMetaData(self):
root = ET.fromstring(self.data)
namespaces = {'s': 'http://schemas.xmlsoap.org/soap/envelope/'}
value = root.findtext('s:Body//CurrentURIMetaData', None, namespaces)
return value
class Handler(BaseHTTPRequestHandler):
def do_GET(self):
try:
req = parse.urlparse(self.path)
path = req.path
print(path)
query = parse.parse_qs(req.query)
if path.startswith('/info'):
return self.info(query)
if path.startswith('/dlna/info.xml'):
return self.respdesc()
if path.startswith('/dlna/AVTransport_scpd.xml'):
return self.AVTransport_scpd()
if path.startswith('/dlna/RenderingControl_scpd.xml'):
return self.RenderingControl_scpd()
return self.index()
except Exception as e:
traceback.print_exc()
self.send_error(500, str(e), str(e))
def do_POST(self):
try:
req = parse.urlparse(self.path)
path = req.path
query = parse.parse_qs(req.query)
if path.startswith('/play'):
return self.play(query)
if path.startswith('/pause'):
return self.pause(query)
if path.startswith('/stop'):
return self.stop(query)
if path.startswith('/position'):
return self.position(query)
if path.startswith('/seek'):
return self.seek(query)
if path.startswith('/dlna/_urn:schemas-upnp-org:service:AVTransport_control'):
print('-----POST消息 视频链接数据')
body = self.rfile.read(int(self.headers['content-length']))
data1 = body.decode()
print('AVT数据', data1)
self.execPlay(data1) # 处理
return
return self.notfound()
except Exception as e:
traceback.print_exc()
self.send_error(500, str(e), str(e))
def do_SUBSCRIBE(self):
print(self.headers)
print('do_SUBSCRIBE')
self.send_response(200)
self.send_header('TIMEOUT', 'Second-3600')
self.send_header('SID', 'uuid:f392-a153-571c-e10b')
self.send_header('Content-Length', '0')
self.end_headers()
def respdesc(self):
data = xmlreplayer.desc()
self.send_response(200)
self.send_header('Content-type', 'text/xml')
self.send_header('Access-Control-Allow-Origin', '*')
self.end_headers()
self.wfile.write(data.encode())
def AVTransport_scpd(self):
data = xmlreplayer.AVTransport_scpd()
self.send_response(200)
self.send_header('Content-type', 'text/xml')
self.send_header('Access-Control-Allow-Origin', '*')
self.end_headers()
self.wfile.write(data.encode())
def RenderingControl_scpd(self):
data = xmlreplayer.RenderingControl_scpd()
self.send_response(200)
self.send_header('Content-type', 'text/xml')
self.send_header('Access-Control-Allow-Origin', '*')
self.end_headers()
self.wfile.write(data.encode())
def execPlay(self, data):
if 'GetTransportInfo' in data:
data = xmlreplayer.trans()
self.send_response(200)
self.send_header('Content-type', 'text/xml')
self.send_header('Access-Control-Allow-Origin', '*')
self.end_headers()
self.wfile.write(data.encode())
return
if 'u:Stop' in data:
if PlayStatus.playing:
PlayStatus.playing.kill()
PlayStatus.stoped = True
print('local stop ', PlayStatus.url)
data = xmlreplayer.stop()
self.send_response(200)
self.send_header('Content-type', 'text/xml')
self.send_header('Access-Control-Allow-Origin', '*')
self.end_headers()
self.wfile.write(data.encode())
return
if 'u:Pause' in data:
data = xmlreplayer.pause()
self.send_response(200)
self.send_header('Content-type', 'text/xml')
self.send_header('Access-Control-Allow-Origin', '*')
self.end_headers()
self.wfile.write(data.encode())
return
if 'u:Play' in data:
data = xmlreplayer.playresp()
self.send_response(200)
self.send_header('Content-type', 'text/xml')
self.send_header('Access-Control-Allow-Origin', '*')
self.end_headers()
self.wfile.write(data.encode())
return
if 'u:Seek' in data:
data = xmlreplayer.seekresp()
self.send_response(200)
self.send_header('Content-type', 'text/xml')
self.send_header('Access-Control-Allow-Origin', '*')
self.end_headers()
self.wfile.write(data.encode())
return
if 'u:GetMediaInfo' in data:
data = xmlreplayer.mediainfo()
self.send_response(200)
self.send_header('Content-type', 'text/xml')
self.send_header('Access-Control-Allow-Origin', '*')
self.end_headers()
self.wfile.write(data.encode())
return
if 'u:GetPositionInfo' in data:
data = xmlreplayer.postioninfo()
self.send_response(200)
self.send_header('Content-type', 'text/xml')
self.send_header('Access-Control-Allow-Origin', '*')
self.end_headers()
self.wfile.write(data.encode())
return
reqParser = xmlReqParser(data)
print('数据全', data)
url = reqParser.CurrentURI()
meta = reqParser.CurrentURIMetaData()
if url is None:
print(self.headers)
print(data)
self.notfound()
return
print('获取到视频链接', url)
# ret = subprocess.Popen('{} "{}"'.format(player, url), shell=True)
# print(ret)
if PlayStatus.playing:
PlayStatus.playing.kill()
# PlayStatus.playing = ret # type: ignore
PlayStatus.stoped = False
PlayStatus.url = url
PlayStatus.meta = meta or ""
data = xmlreplayer.setUriResp()
self.send_response(200)
self.send_header('Content-type', 'text/xml')
self.send_header('Access-Control-Allow-Origin', '*')
self.end_headers()
self.wfile.write(data.encode())
def index(self):
self.send_response(200)
self.send_header('Content-type', 'text/html')
self.send_header('Access-Control-Allow-Origin', '*')
self.end_headers()
with open("index.html", "rb") as f:
self.wfile.write(f.read())
def notfound(self):
self.send_response(404)
self.send_header('Content-type', 'text/plain')
self.send_header('Access-Control-Allow-Origin', '*')
self.end_headers()
self.wfile.write(b'404 not found')
def info(self, query):
self.send_response(200)
self.send_header('Content-type', 'application/json')
self.send_header('Access-Control-Allow-Origin', '*')
self.end_headers()
self.wfile.write(json.dumps(dlna.getInfos()).encode())
def play(self, query):
url = query.get('url')
if url is None:
return self.err('error params')
url = url[0]
device = dlna.getDevice(url)
if device is None:
return self.err('no device')
playUrl = query.get('playUrl')
if playUrl is None or playUrl[0] is None:
# recover play
ret = device.play()
self.ok(ret.decode())
return
playUrl = playUrl[0]
print('remote play ', playUrl)
ret = device.setPlayUrl(playUrl)
device.play()
return self.ok(ret.decode())
def pause(self, query):
url = query.get('url')
if url is None:
return self.err('error params')
url = url[0]
device = dlna.getDevice(url)
if device is None:
return self.err('no device')
ret = device.pause()
return self.ok(ret.decode())
def stop(self, query):
url = query.get('url')
if url is None:
return self.err('error params')
url = url[0]
device = dlna.getDevice(url)
if device is None:
return self.err('no device')
ret = device.stop()
return self.ok(ret.decode())
def position(self, query):
url = query.get('url')
if url is None:
return self.err('error params')
url = url[0]
device = dlna.getDevice(url)
if device is None:
return self.err('no device')
ret = device.getPosition()
return self.ok(ret.decode())
def seek(self, query):
url = query.get('url')
if url is None:
return self.err('error params')
url = url[0]
sk = query.get('seek')
if sk is None:
return self.err('error params')
sk = sk[0]
device = dlna.getDevice(url)
if device is None:
return self.err('no device')
ret = device.seek(sk)
return self.ok(ret.decode())
def err(self, err):
self.send_response(200)
self.send_header('Content-type', 'application/json')
self.send_header('Access-Control-Allow-Origin', '*')
self.end_headers()
self.wfile.write(json.dumps({'code': -1, 'msg': err}).encode())
def ok(self, msg):
self.send_response(200)
self.send_header('Content-type', 'application/json')
self.send_header('Access-Control-Allow-Origin', '*')
self.end_headers()
self.wfile.write(json.dumps({'code': 0, 'msg': msg}).encode())
class XmlReplay:
def __init__(self, ip, port, name):
self.ip = ip
self.port = port
self.name = name
def alive(self):
GMT_FORMAT = '%a, %d %b %Y %H:%M:%S GMT'
date = datetime.datetime.now(datetime.timezone.utc).strftime(GMT_FORMAT)
st = 'urn:schemas-upnp-org:device:MediaRenderer:1'
return '\r\n'.join(
[
'HTTP/1.1 200 OK',
'CACHE-CONTROL: max-age=60',
'EXT:',
'DATE: {}'.format(date),
'LOCATION: http://{}:{}/dlna/info.xml'.format(self.ip, self.port),
'SERVER: simple python dlna server',
'ST: {}'.format(st),
'USN: uuid:{}'.format(uuid),
'',
'',
]
)
def desc(self):
return '''<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>
<presentationURL>/</presentationURL>
<friendlyName>{}</friendlyName>
<manufacturer>python dlna server</manufacturer>
<manufacturerURL>https://github.com/suconghou/dlna-python</manufacturerURL>
<modelDescription>python dlna</modelDescription>
<modelName>python dlna</modelName>
<modelURL>https://github.com/suconghou/dlna-python</modelURL>
<UPC>000000000000</UPC>
<UDN>uuid:{}</UDN>
<serviceList>
<service>
<serviceType>urn:schemas-upnp-org:service:AVTransport:1</serviceType>
<serviceId>urn:upnp-org:serviceId:AVTransport</serviceId>
<SCPDURL>/dlna/AVTransport_scpd.xml</SCPDURL>
<controlURL>/dlna/_urn:schemas-upnp-org:service:AVTransport_control</controlURL>
<eventSubURL>/dlna/_urn:schemas-upnp-org:service:AVTransport_event</eventSubURL>
</service>
<service>
<serviceType>urn:schemas-upnp-org:service:RenderingControl:1</serviceType>
<serviceId>urn:upnp-org:serviceId:RenderingControl</serviceId>
<SCPDURL>/dlna/RenderingControl_scpd.xml</SCPDURL>
<controlURL>/dlna/_urn:schemas-upnp-org:service:RenderingControl_control</controlURL>
<eventSubURL>/dlna/_urn:schemas-upnp-org:service:RenderingControl_event</eventSubURL>
</service>
</serviceList>
</device>
<URLBase>http://{}:{}</URLBase>
</root>'''.format(
self.name, uuid, self.ip, self.port
)
def trans(self):
return '''<?xml version="1.0" encoding="UTF-8"?>
<s:Envelope s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/" xmlns:s="http://schemas.xmlsoap.org/soap/envelope/">
<s:Body>
<u:GetTransportInfoResponse xmlns:u="urn:schemas-upnp-org:service:AVTransport:1">
<CurrentTransportState>{}</CurrentTransportState>
<CurrentTransportStatus>OK</CurrentTransportStatus>
<CurrentSpeed>1</CurrentSpeed>
</u:GetTransportInfoResponse>
</s:Body>
</s:Envelope>'''.format(
'PLAYING'
if PlayStatus.stoped == False
else 'STOPED'
if PlayStatus.url
else 'NO_MEDIA_PRESENT'
)
def stop(self):
return '''<?xml version="1.0" encoding="UTF-8"?>
<s:Envelope s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/" xmlns:s="http://schemas.xmlsoap.org/soap/envelope/">
<s:Body>
<u:StopResponse xmlns:u="urn:schemas-upnp-org:service:AVTransport:1"/>
</s:Body>
</s:Envelope>'''
def pause(self):
return '''<?xml version="1.0" encoding="UTF-8"?>
<s:Envelope s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/" xmlns:s="http://schemas.xmlsoap.org/soap/envelope/">
<s:Body>
<u:PauseResponse xmlns:u="urn:schemas-upnp-org:service:AVTransport:1"/>
</s:Body>
</s:Envelope>'''
def mediainfo(self):
return '''<?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:GetMediaInfoResponse xmlns:u="urn:schemas-upnp-org:service:AVTransport:1">
<NrTracks>0</NrTracks>
<MediaDuration>02:00:00</MediaDuration>
<CurrentURI>{}</CurrentURI>
<CurrentURIMetaData>{}</CurrentURIMetaData>
<NextURI></NextURI>
<NextURIMetaData></NextURIMetaData>
<PlayMedium>NETWORK</PlayMedium>
<RecordMedium>NOT_IMPLEMENTED</RecordMedium>
<WriteStatus>NOT_IMPLEMENTED</WriteStatus>
</u:GetMediaInfoResponse>
</s:Body>
</s:Envelope>'''.format(
htmlEncode(PlayStatus.url), htmlEncode(PlayStatus.meta)
)
def postioninfo(self):
x = randint(1, 9)
return '''<?xml version="1.0" encoding="UTF-8"?>
<s:Envelope s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/" xmlns:s="http://schemas.xmlsoap.org/soap/envelope/">
<s:Body>
<u:GetPositionInfoResponse xmlns:u="urn:schemas-upnp-org:service:AVTransport:1">
<Track>0</Track>
<TrackDuration>02:00:00</TrackDuration>
<TrackMetaData></TrackMetaData>
<TrackURI>{}</TrackURI>
<RelTime>00:00:0{}</RelTime>
<AbsTime>00:00:0{}</AbsTime>
<RelCount>2147483647</RelCount>
<AbsCount>2147483647</AbsCount>
</u:GetPositionInfoResponse>
</s:Body>
</s:Envelope>'''.format(
htmlEncode(PlayStatus.url), x, x
)
def setUriResp(self):
return '''<?xml version="1.0" encoding="UTF-8"?>
<s:Envelope s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/" xmlns:s="http://schemas.xmlsoap.org/soap/envelope/">
<s:Body>
<u:SetAVTransportURIResponse xmlns:u="urn:schemas-upnp-org:service:AVTransport:1"/>
</s:Body>
</s:Envelope>'''
def playresp(self):
return '''<?xml version="1.0" encoding="UTF-8"?>
<s:Envelope s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/" xmlns:s="http://schemas.xmlsoap.org/soap/envelope/">
<s:Body>
<u:PlayResponse xmlns:u="urn:schemas-upnp-org:service:AVTransport:1"/>
</s:Body>
</s:Envelope>'''
def seekresp(self):
return "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<s:Envelope xmlns:s=\"http://schemas.xmlsoap.org/soap/envelope/\" s:encodingStyle=\"http://schemas.xmlsoap.org/soap/encoding/\"><s:Body><u:SeekResponse xmlns:u=\"urn:schemas-upnp-org:service:AVTransport:1\"></u:SeekResponse></s:Body></s:Envelope>"
def AVTransport_scpd(self):
text = '''<?xml version="1.0" encoding="utf-8"?>
<scpd xmlns="urn:schemas-upnp-org:service-1-0">
<specVersion>
<major>1</major>
<minor>0</minor>
</specVersion>
<actionList>
<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>
<action>
<name>Stop</name>
<argumentList>
<argument>
<name>InstanceID</name>
<direction>in</direction>
<relatedStateVariable>A_ARG_TYPE_InstanceID</relatedStateVariable>
</argument>
</argumentList>
</action>
<action>
<name>GetMediaInfo</name>
<argumentList>
<argument>
<name>InstanceID</name>
<direction>in</direction>
<relatedStateVariable>A_ARG_TYPE_InstanceID</relatedStateVariable>
</argument>
<argument>
<name>NrTracks</name>
<direction>out</direction>
<relatedStateVariable>NumberOfTracks</relatedStateVariable>
<defaultValue>0</defaultValue>
</argument>
<argument>
<name>MediaDuration</name>
<direction>out</direction>
<relatedStateVariable>CurrentMediaDuration</relatedStateVariable>
</argument>
<argument>
<name>CurrentURI</name>
<direction>out</direction>
<relatedStateVariable>AVTransportURI</relatedStateVariable>
</argument>
<argument>
<name>CurrentURIMetaData</name>
<direction>out</direction>
<relatedStateVariable>AVTransportURIMetaData</relatedStateVariable>
</argument>
<argument>
<name>NextURI</name>
<direction>out</direction>
<relatedStateVariable>NextAVTransportURI</relatedStateVariable>
</argument>
<argument>
<name>NextURIMetaData</name>
<direction>out</direction>
<relatedStateVariable>NextAVTransportURIMetaData</relatedStateVariable>
</argument>
<argument>
<name>PlayMedium</name>
<direction>out</direction>
<relatedStateVariable>PlaybackStorageMedium</relatedStateVariable>
</argument>
<argument>
<name>RecordMedium</name>
<direction>out</direction>
<relatedStateVariable>RecordStorageMedium</relatedStateVariable>
</argument>
<argument>
<name>WriteStatus</name>
<direction>out</direction>
<relatedStateVariable>RecordMediumWriteStatus</relatedStateVariable>
</argument>
</argumentList>
</action>
<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>GetTransportInfo</name>
<argumentList>
<argument>
<name>InstanceID</name>
<direction>in</direction>
<relatedStateVariable>A_ARG_TYPE_InstanceID</relatedStateVariable>
</argument>
<argument>
<name>CurrentTransportState</name>
<direction>out</direction>
<relatedStateVariable>TransportState</relatedStateVariable>
</argument>
<argument>
<name>CurrentTransportStatus</name>
<direction>out</direction>
<relatedStateVariable>TransportStatus</relatedStateVariable>
</argument>
<argument>
<name>CurrentSpeed</name>
<direction>out</direction>
<relatedStateVariable>TransportPlaySpeed</relatedStateVariable>
</argument>
</argumentList>
</action>
<action>
<name>Pause</name>
<argumentList>
<argument>
<name>InstanceID</name>
<direction>in</direction>
<relatedStateVariable>A_ARG_TYPE_InstanceID</relatedStateVariable>
</argument>
</argumentList>
</action>
<action>
<name>Seek</name>
<argumentList>
<argument>
<name>InstanceID</name>
<direction>in</direction>
<relatedStateVariable>A_ARG_TYPE_InstanceID</relatedStateVariable>
</argument>
<argument>
<name>Unit</name>
<direction>in</direction>
<relatedStateVariable>A_ARG_TYPE_SeekMode</relatedStateVariable>
</argument>
<argument>
<name>Target</name>
<direction>in</direction>
<relatedStateVariable>A_ARG_TYPE_SeekTarget</relatedStateVariable>
</argument>
</argumentList>
</action>
<action>
<name>GetPositionInfo</name>
<argumentList>
<argument>
<name>InstanceID</name>
<direction>in</direction>
<relatedStateVariable>A_ARG_TYPE_InstanceID</relatedStateVariable>
</argument>
<argument>
<name>Track</name>
<direction>out</direction>
<relatedStateVariable>CurrentTrack</relatedStateVariable>
</argument>
<argument>
<name>TrackDuration</name>
<direction>out</direction>
<relatedStateVariable>CurrentTrackDuration</relatedStateVariable>
</argument>
<argument>
<name>TrackMetaData</name>
<direction>out</direction>
<relatedStateVariable>CurrentTrackMetaData</relatedStateVariable>
</argument>
<argument>
<name>TrackURI</name>
<direction>out</direction>
<relatedStateVariable>CurrentTrackURI</relatedStateVariable>
</argument>
<argument>
<name>RelTime</name>
<direction>out</direction>
<relatedStateVariable>RelativeTimePosition</relatedStateVariable>
</argument>
<argument>
<name>AbsTime</name>
<direction>out</direction>
<relatedStateVariable>AbsoluteTimePosition</relatedStateVariable>
</argument>
<argument>
<name>RelCount</name>
<direction>out</direction>
<relatedStateVariable>RelativeCounterPosition</relatedStateVariable>
</argument>
<argument>
<name>AbsCount</name>
<direction>out</direction>
<relatedStateVariable>AbsoluteCounterPosition</relatedStateVariable>
</argument>
</argumentList>
</action>
</actionList>
<serviceStateTable>
<stateVariable sendEvents="no">
<name>TransportState</name>
<dataType>string</dataType>
<allowedValueList>
<allowedValue>STOPPED</allowedValue>
<allowedValue>PAUSED_PLAYBACK</allowedValue>
<allowedValue>PLAYING</allowedValue>
<allowedValue>TRANSITIONING</allowedValue>
<allowedValue>NO_MEDIA_PRESENT</allowedValue>
</allowedValueList>
<defaultValue>NO_MEDIA_PRESENT</defaultValue>
</stateVariable>
<stateVariable sendEvents="no">
<name>TransportStatus</name>
<dataType>string</dataType>
<allowedValueList>
<allowedValue>OK</allowedValue>
<allowedValue>ERROR_OCCURRED</allowedValue>
</allowedValueList>
<defaultValue>OK</defaultValue>
</stateVariable>
<stateVariable sendEvents="no">
<name>TransportPlaySpeed</name>
<dataType>string</dataType>
<defaultValue>1</defaultValue>
</stateVariable>
<stateVariable sendEvents="no">
<name>NumberOfTracks</name>
<dataType>ui4</dataType>
<allowedValueRange>
<minimum>0</minimum>
<maximum>4294967295</maximum>
</allowedValueRange>
<defaultValue>0</defaultValue>
</stateVariable>
<stateVariable sendEvents="no">
<name>CurrentMediaDuration</name>
<dataType>string</dataType>
<defaultValue>00:00:00</defaultValue>
</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>PlaybackStorageMedium</name>
<dataType>string</dataType>
<allowedValueList>
<allowedValue>NONE</allowedValue>
<allowedValue>NETWORK</allowedValue>
</allowedValueList>
<defaultValue>NONE</defaultValue>
</stateVariable>
<stateVariable sendEvents="no">
<name>CurrentTrack</name>
<dataType>ui4</dataType>
<allowedValueRange>
<minimum>0</minimum>
<maximum>4294967295</maximum>
<step>1</step>
</allowedValueRange>
<defaultValue>0</defaultValue>
</stateVariable>
<stateVariable sendEvents="no">
<name>CurrentTrackDuration</name>
<dataType>string</dataType>
<defaultValue>00:00:00</defaultValue>
</stateVariable>
<stateVariable sendEvents="no">
<name>CurrentTrackMetaData</name>
<dataType>string</dataType>
</stateVariable>
<stateVariable sendEvents="no">
<name>CurrentTrackURI</name>
<dataType>string</dataType>
</stateVariable>
<stateVariable sendEvents="no">
<name>RelativeTimePosition</name>
<dataType>string</dataType>
<defaultValue>00:00:00</defaultValue>
</stateVariable>
<stateVariable sendEvents="no">
<name>AbsoluteTimePosition</name>
<dataType>string</dataType>
<defaultValue>00:00:00</defaultValue>
</stateVariable>
<stateVariable sendEvents="no">
<name>NextAVTransportURI</name>
<dataType>string</dataType>
</stateVariable>
<stateVariable sendEvents="no">
<name>NextAVTransportURIMetaData</name>
<dataType>string</dataType>
</stateVariable>
<stateVariable sendEvents="no">
<name>CurrentTransportActions</name>
<dataType>string</dataType>
</stateVariable>
<stateVariable sendEvents="no">
<name>RecordStorageMedium</name>
<dataType>string</dataType>
<allowedValueList>
<allowedValue>NOT_IMPLEMENTED</allowedValue>
</allowedValueList>
<defaultValue>NOT_IMPLEMENTED</defaultValue>
</stateVariable>
<stateVariable sendEvents="no">
<name>RecordMediumWriteStatus</name>
<dataType>string</dataType>
<allowedValueList>
<allowedValue>NOT_IMPLEMENTED</allowedValue>
</allowedValueList>
<defaultValue>NOT_IMPLEMENTED</defaultValue>
</stateVariable>
<stateVariable sendEvents="no">
<name>RelativeCounterPosition</name>
<dataType>i4</dataType>
<defaultValue>2147483647</defaultValue>
</stateVariable>
<stateVariable sendEvents="no">
<name>AbsoluteCounterPosition</name>
<dataType>i4</dataType>
<defaultValue>2147483647</defaultValue>
</stateVariable>
<stateVariable sendEvents="yes">
<name>LastChange</name>
<dataType>string</dataType>
</stateVariable>
<stateVariable sendEvents="no">
<name>A_ARG_TYPE_InstanceID</name>
<dataType>ui4</dataType>
</stateVariable>
<stateVariable sendEvents="no">
<name>A_ARG_TYPE_SeekMode</name>
<dataType>string</dataType>
<allowedValueList>
<allowedValue>TRACK_NR</allowedValue>
<allowedValue>REL_TIME</allowedValue>
<allowedValue>ABS_TIME</allowedValue>
<allowedValue>ABS_COUNT</allowedValue>
<allowedValue>REL_COUNT</allowedValue>
<allowedValue>FRAME</allowedValue>
</allowedValueList>
<defaultValue>REL_TIME</defaultValue>
</stateVariable>
<stateVariable sendEvents="no">
<name>A_ARG_TYPE_SeekTarget</name>
<dataType>string</dataType>
</stateVariable>
</serviceStateTable>
</scpd>'''
return text
def RenderingControl_scpd(self):
text = '''<scpd xmlns="urn:schemas-upnp-org:service-1-0">
<specVersion>
<major>1</major>
<minor>0</minor>
</specVersion>
<actionList>
<action>
<name>GetMute</name>
<argumentList>
<argument>
<name>InstanceID</name>
<direction>in</direction>
<relatedStateVariable>A_ARG_TYPE_InstanceID</relatedStateVariable>
</argument>
<argument>
<name>Channel</name>
<direction>in</direction>
<relatedStateVariable>A_ARG_TYPE_Channel</relatedStateVariable>
</argument>
<argument>
<name>CurrentMute</name>
<direction>out</direction>
<relatedStateVariable>Mute</relatedStateVariable>
</argument>
</argumentList>
</action>
<action>
<name>GetVolume</name>
<argumentList>
<argument>
<name>InstanceID</name>
<direction>in</direction>
<relatedStateVariable>A_ARG_TYPE_InstanceID</relatedStateVariable>
</argument>
<argument>
<name>Channel</name>
<direction>in</direction>
<relatedStateVariable>A_ARG_TYPE_Channel</relatedStateVariable>
</argument>
<argument>
<name>CurrentVolume</name>
<direction>out</direction>
<relatedStateVariable>Volume</relatedStateVariable>
</argument>
</argumentList>
</action>
<action>
<name>GetVolumeDB</name>
<argumentList>
<argument>
<name>InstanceID</name>
<direction>in</direction>
<relatedStateVariable>A_ARG_TYPE_InstanceID</relatedStateVariable>
</argument>
<argument>
<name>Channel</name>
<direction>in</direction>
<relatedStateVariable>A_ARG_TYPE_Channel</relatedStateVariable>
</argument>
<argument>
<name>CurrentVolume</name>
<direction>out</direction>
<relatedStateVariable>VolumeDB</relatedStateVariable>
</argument>
</argumentList>
</action>
<action>
<name>GetVolumeDBRange</name>
<argumentList>
<argument>
<name>InstanceID</name>
<direction>in</direction>
<relatedStateVariable>A_ARG_TYPE_InstanceID</relatedStateVariable>
</argument>
<argument>
<name>Channel</name>
<direction>in</direction>
<relatedStateVariable>A_ARG_TYPE_Channel</relatedStateVariable>
</argument>
<argument>
<name>MinValue</name>
<direction>out</direction>
<relatedStateVariable>VolumeDB</relatedStateVariable>
</argument>
<argument>
<name>MaxValue</name>
<direction>out</direction>
<relatedStateVariable>VolumeDB</relatedStateVariable>
</argument>
</argumentList>
</action>
<action>
<name>ListPresets</name>
<argumentList>
<argument>
<name>InstanceID</name>
<direction>in</direction>
<relatedStateVariable>A_ARG_TYPE_InstanceID</relatedStateVariable>
</argument>
<argument>
<name>CurrentPresetNameList</name>
<direction>out</direction>
<relatedStateVariable>PresetNameList</relatedStateVariable>
</argument>
</argumentList>
</action>
<action>
<name>SelectPreset</name>
<argumentList>
<argument>
<name>InstanceID</name>
<direction>in</direction>
<relatedStateVariable>A_ARG_TYPE_InstanceID</relatedStateVariable>
</argument>
<argument>
<name>PresetName</name>
<direction>in</direction>
<relatedStateVariable>A_ARG_TYPE_PresetName</relatedStateVariable>
</argument>
</argumentList>
</action>
<action>
<name>SetMute</name>
<argumentList>
<argument>
<name>InstanceID</name>
<direction>in</direction>
<relatedStateVariable>A_ARG_TYPE_InstanceID</relatedStateVariable>
</argument>
<argument>
<name>Channel</name>
<direction>in</direction>
<relatedStateVariable>A_ARG_TYPE_Channel</relatedStateVariable>
</argument>
<argument>
<name>DesiredMute</name>
<direction>in</direction>
<relatedStateVariable>Mute</relatedStateVariable>
</argument>
</argumentList>
</action>
<action>
<name>SetVolume</name>
<argumentList>
<argument>
<name>InstanceID</name>
<direction>in</direction>
<relatedStateVariable>A_ARG_TYPE_InstanceID</relatedStateVariable>
</argument>
<argument>
<name>Channel</name>
<direction>in</direction>
<relatedStateVariable>A_ARG_TYPE_Channel</relatedStateVariable>
</argument>
<argument>
<name>DesiredVolume</name>
<direction>in</direction>
<relatedStateVariable>Volume</relatedStateVariable>
</argument>
</argumentList>
</action>
</actionList>
<serviceStateTable>
<stateVariable sendEvents="no">
<name>GreenVideoGain</name>
<dataType>ui2</dataType>
<allowedValueRange>
<minimum>0</minimum>
<maximum>100</maximum>
<step>1</step>
</allowedValueRange>
</stateVariable>
<stateVariable sendEvents="no">
<name>BlueVideoBlackLevel</name>
<dataType>ui2</dataType>
<allowedValueRange>
<minimum>0</minimum>
<maximum>100</maximum>
<step>1</step>
</allowedValueRange>
</stateVariable>
<stateVariable sendEvents="no">
<name>VerticalKeystone</name>
<dataType>i2</dataType>
<allowedValueRange>
<minimum>-32768</minimum>
<maximum>32767</maximum>
<step>1</step>
</allowedValueRange>
</stateVariable>
<stateVariable sendEvents="no">
<name>GreenVideoBlackLevel</name>
<dataType>ui2</dataType>
<allowedValueRange>
<minimum>0</minimum>
<maximum>100</maximum>
<step>1</step>
</allowedValueRange>
</stateVariable>
<stateVariable sendEvents="no">
<name>Volume</name>
<dataType>ui2</dataType>
<allowedValueRange>
<minimum>0</minimum>
<maximum>100</maximum>
<step>1</step>
</allowedValueRange>
</stateVariable>
<stateVariable sendEvents="no">
<name>Loudness</name>
<dataType>boolean</dataType>
</stateVariable>
<stateVariable sendEvents="no">
<name>A_ARG_TYPE_InstanceID</name>
<dataType>ui4</dataType>
</stateVariable>
<stateVariable sendEvents="no">
<name>RedVideoGain</name>
<dataType>ui2</dataType>
<allowedValueRange>
<minimum>0</minimum>
<maximum>100</maximum>
<step>1</step>
</allowedValueRange>
</stateVariable>
<stateVariable sendEvents="no">
<name>ColorTemperature</name>
<dataType>ui2</dataType>
<allowedValueRange>
<minimum>0</minimum>
<maximum>65535</maximum>
<step>1</step>
</allowedValueRange>
</stateVariable>
<stateVariable sendEvents="no">
<name>Sharpness</name>
<dataType>ui2</dataType>
<allowedValueRange>
<minimum>0</minimum>
<maximum>100</maximum>
<step>1</step>
</allowedValueRange>
</stateVariable>
<stateVariable sendEvents="no">
<name>A_ARG_TYPE_PresetName</name>
<dataType>string</dataType>
<allowedValueList>
<allowedValue>FactoryDefaults</allowedValue>
<allowedValue>InstallationDefaults</allowedValue>
<allowedValue>Vendor defined</allowedValue>
</allowedValueList>
</stateVariable>
<stateVariable sendEvents="no">
<name>RedVideoBlackLevel</name>
<dataType>ui2</dataType>
<allowedValueRange>
<minimum>0</minimum>
<maximum>100</maximum>
<step>1</step>
</allowedValueRange>
</stateVariable>
<stateVariable sendEvents="no">
<name>BlueVideoGain</name>
<dataType>ui2</dataType>
<allowedValueRange>
<minimum>0</minimum>
<maximum>100</maximum>
<step>1</step>
</allowedValueRange>
</stateVariable>
<stateVariable sendEvents="no">
<name>Mute</name>
<dataType>boolean</dataType>
</stateVariable>
<stateVariable sendEvents="yes">
<name>LastChange</name>
<dataType>string</dataType>
</stateVariable>
<stateVariable sendEvents="no">
<name>A_ARG_TYPE_Channel</name>
<dataType>string</dataType>
<allowedValueList>
<allowedValue>Master</allowedValue>
<allowedValue>LF</allowedValue>
<allowedValue>RF</allowedValue>
</allowedValueList>
</stateVariable>
<stateVariable sendEvents="no">
<name>HorizontalKeystone</name>
<dataType>i2</dataType>
<allowedValueRange>
<minimum>-32768</minimum>
<maximum>32767</maximum>
<step>1</step>
</allowedValueRange>
</stateVariable>
<stateVariable sendEvents="no">
<name>VolumeDB</name>
<dataType>i2</dataType>
<allowedValueRange>
<minimum>-32768</minimum>
<maximum>32767</maximum>
</allowedValueRange>
</stateVariable>
<stateVariable sendEvents="no">
<name>PresetNameList</name>
<dataType>string</dataType>
</stateVariable>
<stateVariable sendEvents="no">
<name>Contrast</name>
<dataType>ui2</dataType>
<allowedValueRange>
<minimum>0</minimum>
<maximum>100</maximum>
<step>1</step>
</allowedValueRange>
</stateVariable>
<stateVariable sendEvents="no">
<name>Brightness</name>
<dataType>ui2</dataType>
<allowedValueRange>
<minimum>0</minimum>
<maximum>100</maximum>
<step>1</step>
</allowedValueRange>
</stateVariable>
</serviceStateTable>
</scpd>
'''
return text
def getLocalIp():
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
s.connect(("119.29.29.29", 80))
ip = s.getsockname()[0]
s.close()
return ip
class Dlna:
def __init__(self):
self.devices = {}
self.infos = {}
def start(self):
# ListenWorker(self.onFound).start()
pass
def onFound(self, url, info, item):
self.infos[url] = info
self.devices[url] = item
def getInfos(self):
return self.infos
def getDevice(self, url):
return self.devices.get(url)
def htmlEncode(text):
d = {"&": "&", "<": "<", ">": ">", "'": "'", '"': '"'}
for k, v in d.items():
text = text.replace(k, v)
return text
class ThreadingSimpleServer(ThreadingMixIn, HTTPServer):
pass
# ===============================================================================
# uuid = "27d6877e-{}-ea12-abdf-cf8d50e36d54".format(randint(1000, 9999))
uuid = "27d6877e-6677-ea12-abdf-cf8d50e36d54"
localIp = getLocalIp()
host = (localIp, 8889)
dlna_name = "dlna({}:{})".format(localIp, host[1])
xmlreplayer = XmlReplay(localIp, host[1], dlna_name)
dlna = Dlna()
dlna.start()
server = ThreadingSimpleServer(host, Handler)
def local(ip, port):
ThreadingSimpleServer((ip, port), Handler).serve_forever()
_thread.start_new_thread(local, ('127.0.0.1', host[1]))
print("Starting server, listen at: %s:%s" % host)
server.serve_forever()
# http://localhost:8889/dlna/info.xml
运行结果
手机app投屏搜索到
代码运行输出里看视频链接
到此,获取到投屏视频的播放链接 获取到芒果TV投屏的播放链接: http://mobile-shadow.api.mgtv.com/v1/video/shadow.m3u8?tea=TRp4。。。efe35d&ftcx=ab2ea0demg_androidP1 获取到百度网盘投屏的播放链接: https://pan.baidu.com/api/streaming?check_blue=1&type=M3U8_AUTO_720&fsid=29855。。。qc69B7/GzlTXUYA=
播放链接
某种m3u8,某些工具可以播放这种链接,正在研究
代码参考
代码参考,如果你也在研究,点点star https://github.com/suconghou/dlna-python https://github.com/Hans155922/DLNA_URL https://github.com/jarryleo/watch_together https://github.com/VideoTogether/VideoTogether
2024-12-02
作者:黑音