Huanglei

I’m PADI No.1409AZ6094

0%

投屏

今天聊投送视频链接和投屏播放这两事:

1、发送投屏内容的可以是手机或者电脑的浏览器、各种app 投屏操作;

2、接受投屏内容端的可以是电视、平板、电脑;

下面的脚本已在我的iPad和Mac上正常运行好几天了,我的Pythonista是用巨魔商店安装的。

BTW:最近工作已经用上了不限 Token 的DeepSeek V4 Flash,跑在 VSCode 的 Continue 插件上,下面的脚本与工作没有半毛钱关系,几乎都是用 Kimi K3 一点一点试错出来的。

1、发起投送视频链接端

​ 除了各种 视频自带的APP都具备传统投屏功能以外,我用 EDGE 浏览器的ScriptCat脚本,构建了一个可以向 OK 影视投送正在浏览器打开的视频的链接,在 OK 影视播放的投送按钮脚本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
// ==UserScript==
// @name 视频链接推送助手(去广告+多设备版)
// @namespace https://maxlay.github.io
// @version 1.0.0
// @description 全文检索 mp4/mkv/m3u8 链接,去除页面广告,并同时 POST 到多台电视设备
// @author huanglei
// @match *://*/*
// @grant GM_xmlhttpRequest
// @connect 192.168.1.99
// @connect 192.168.1.100
// @connect 192.168.1.101
// @run-at document-idle
// ==/UserScript==

(function () {
'use strict';

// 🛑 核心修复:如果在 iframe 中运行则直接退出,避免在视频播放器内部生成第二个按钮
if (window.self !== window.top) return;

const CONFIG = {
targets: [
{ ip: '192.168.1.99', url: 'http://192.168.1.99:9978/action' },
{ ip: '192.168.1.100', url: 'http://192.168.1.100:9978/action' },
{ ip: '192.168.1.101', url: 'http://192.168.1.101:9978/action' }
],
btnText: '📺 推送到电视平板和电脑上播放',
btnColor: '#ff4757'
};

/* ==================== 去广告功能 ==================== */
const adSelectors = [
'.ps_25', // 侧边栏广告占位
'.ps_21', // 播放器下方广告
'.palyAdv', // 播放区广告容器
'[class*="ps_"]' // 兜底:匹配所有 ps_ 开头的广告类
];

function removeAds() {
adSelectors.forEach(selector => {
document.querySelectorAll(selector).forEach(el => el.remove());
});

// 清理 gc_video_content 内的空白广告节点
const videoContent = document.querySelector('.gc_video_content');
if (videoContent) {
videoContent.querySelectorAll('div').forEach(div => {
if (div.classList.length === 0 && div.children.length === 0 && div.innerHTML.trim() === '') {
div.remove();
}
});
}
}

function initAdBlocker() {
removeAds();
const observer = new MutationObserver(() => removeAds());
observer.observe(document.body, { childList: true, subtree: true });
setTimeout(() => observer.disconnect(), 5000); // 5秒后停止观察,避免性能损耗
}

/* ==================== 视频提取功能(保持原逻辑) ==================== */
function cleanUrl(raw) {
if (!raw) return '';
let url = raw.replace(/\\\//g, '/');
try {
url = JSON.parse(`"${url}"`);
} catch (_) {
url = url.replace(/\\u([0-9a-fA-F]{4})/gi, (_, hex) =>
String.fromCharCode(parseInt(hex, 16))
);
}
return url;
}

function extractVideoUrl() {
const html = document.documentElement.innerHTML;
const extensions = ['mp4', 'mkv', 'm3u8'];

for (const ext of extensions) {
const regex = new RegExp(`https?:\\\\?\\/\\\\?\\/[^"'\\s<>]+\\.${ext}`, 'gi');
const match = html.match(regex);
if (match && match.length > 0) {
const cleaned = cleanUrl(match[0]);
console.log(`[视频推送] ✅ 命中 ${ext.toUpperCase()} 链接:`, cleaned);
return cleaned;
}
}

console.warn('[视频推送] ❌ 未在页面源码中找到任何视频链接');
return null;
}

/* ==================== 多设备同时推送 ==================== */
function pushToServer(videoUrl, btnElement) {
const postData = `url=${encodeURIComponent(videoUrl)}&do=push`;
let completed = 0;
let successCount = 0;
const total = CONFIG.targets.length;

function checkComplete() {
completed++;
btnElement.textContent = `⏳ 推送中 (${completed}/${total})...`;
if (completed === total) {
if (successCount > 0) {
btnElement.textContent = `✅ 成功 ${successCount}/${total}`;
btnElement.style.backgroundColor = '#2ed573';
} else {
btnElement.textContent = '❌ 全部失败';
btnElement.style.backgroundColor = '#57606f';
alert('推送失败!请检查所有电视设备是否在线及网络配置。');
}
setTimeout(() => resetBtn(btnElement), 2500);
}
}

// 并行向 3 台设备发送请求
CONFIG.targets.forEach(target => {
GM_xmlhttpRequest({
method: 'POST',
url: target.url,
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
data: postData,
timeout: 5000, // 单设备 5 秒超时
onload: (res) => {
console.log(`[视频推送] ${target.ip} 响应:`, res.status, res.responseText);
if (res.status >= 200 && res.status < 300) successCount++;
checkComplete();
},
onerror: (err) => {
console.error(`[视频推送] ${target.ip} 请求失败:`, err);
checkComplete();
},
ontimeout: () => {
console.warn(`[视频推送] ${target.ip} 请求超时`);
checkComplete();
}
});
});
}

/* ==================== UI 按钮(保持原逻辑) ==================== */
function createFloatButton() {
if (document.getElementById('gm-video-push-btn')) return;

const btn = document.createElement('div');
btn.id = 'gm-video-push-btn';
Object.assign(btn.style, {
position: 'fixed',
right: '20px',
bottom: '80px',
zIndex: '99999',
padding: '12px 20px',
backgroundColor: CONFIG.btnColor,
color: '#fff',
borderRadius: '8px',
cursor: 'pointer',
fontSize: '14px',
fontWeight: 'bold',
boxShadow: '0 4px 12px rgba(0,0,0,0.3)',
transition: 'all 0.3s ease',
userSelect: 'none'
});
btn.textContent = CONFIG.btnText;

btn.addEventListener('mouseenter', () => btn.style.opacity = '0.85');
btn.addEventListener('mouseleave', () => btn.style.opacity = '1');

btn.addEventListener('click', () => {
const url = extractVideoUrl();
if (!url) {
alert('❌ 未找到视频链接!\n\n已按顺序检索:mp4 → mkv → m3u8\n均未在页面源码中命中。\n\n请打开 F12 控制台查看详细日志。');
return;
}

btn.textContent = '⏳ 准备推送...';
btn.style.pointerEvents = 'none';
pushToServer(url, btn);
});

document.body.appendChild(btn);
}

function resetBtn(btn) {
btn.textContent = CONFIG.btnText;
btn.style.backgroundColor = CONFIG.btnColor;
btn.style.pointerEvents = 'auto';
}

/* ==================== 初始化 ==================== */
initAdBlocker(); // 先执行去广告

if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', createFloatButton);
} else {
createFloatButton();
}
})();

2、接受投送和投屏内容端

2.1电视端:

​ 我推荐在电视端直接安装 OK 影视或者 TVBox 就能接受投送和投屏内容;

2.2Mac端:

2.2.1接受投送的方案(需要安装 iina 播放器)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
#!/usr/bin/env python3
"""
macOS IINA 视频推送接收器
监听 0.0.0.0:9978,接收 POST 请求并使用 IINA 播放视频
"""

import subprocess
import shutil
from flask import Flask, request, jsonify

app = Flask(__name__)

# ========== 配置区 ==========
HOST = "0.0.0.0" # 监听所有网卡,确保局域网可访问
PORT = 9978 # 与脚本中 CONFIG.targetUrl 端口一致
IINA_CLI = "iina-cli" # IINA 命令行工具,若未加入 PATH 请改为绝对路径
# ============================


def find_iina():
"""查找 iina-cli 的实际路径"""
path = shutil.which(IINA_CLI)
if path:
return path
# macOS 默认安装路径兜底
default_path = "/Applications/IINA.app/Contents/MacOS/iina-cli"
if shutil.which(default_path):
return default_path
return None


@app.route("/action", methods=["POST"])
def receive_video():
url = request.form.get("url", "").strip()
action = request.form.get("do", "").strip()

if not url or action != "push":
return jsonify({"code": 400, "msg": "缺少 url 参数或 do!=push"}), 400

iina_path = find_iina()
if not iina_path:
return jsonify({"code": 500, "msg": "未找到 iina-cli,请确认 IINA 已安装"}), 500

print(f"[✅ 收到推送] {url}")

try:
# --keep-running 防止 IINA 关闭后重复打开新窗口
subprocess.Popen(
[iina_path, "--keep-running", url],
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
)
return jsonify({"code": 200, "msg": "ok", "url": url})
except Exception as e:
print(f"[❌ 启动失败] {e}")
return jsonify({"code": 500, "msg": str(e)}), 500


@app.route("/health", methods=["GET"])
def health():
"""健康检查接口,方便调试"""
return jsonify({"status": "running", "port": PORT})


if __name__ == "__main__":
iina_path = find_iina()
status = f"✅ 已找到: {iina_path}" if iina_path else "❌ 未找到 iina-cli!"
print(f"[IINA 状态] {status}")
print(f"[服务启动] http://{HOST}:{PORT}/action")
app.run(host=HOST, port=PORT, debug=False)
2.2.2接受投屏的方案(需要安装 iina 播放器)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
DLNA MediaRenderer 接收服务端 (IINA 版)
"""

import socket
import struct
import threading
import time
import uuid
import os
import subprocess
import re
import html
from http.server import HTTPServer, BaseHTTPRequestHandler
import qrcode

# ==================== 配置 ====================
HTTP_PORT = 8080
SSDP_PORT = 1900
MULTICAST_ADDR = "239.255.255.250"
DEVICE_NAME = "MyDLNA-Mac-Book—Pro"
DEVICE_UUID = str(uuid.uuid5(uuid.NAMESPACE_DNS, "mydlna-receiver.local"))

LOCAL_IP = None

def get_local_ip():
global LOCAL_IP
if LOCAL_IP:
return LOCAL_IP
try:
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
s.connect(("8.8.8.8", 80))
LOCAL_IP = s.getsockname()[0]
s.close()
except Exception:
LOCAL_IP = "127.0.0.1"
return LOCAL_IP

BASE_URL = f"http://{get_local_ip()}:{HTTP_PORT}"
DEVICE_DESC_URL = f"{BASE_URL}/description.xml"

# ==================== IINA 播放器 ====================
class VideoPlayer:
def __init__(self):
self.current_url = None

def play(self, url):
# TVBox 通过 XML/SOAP 发送的 URL 中,& 会被编码为 &amp;
url = html.unescape(url)
self.current_url = url

# 先尝试关闭已有的 IINA(避免窗口堆积)
subprocess.run(["killall", "IINA"], capture_output=True)
time.sleep(0.3)

# 尝试多种方式启动 IINA
iina_cmds = [
["iina", url], # brew 安装的 iina 命令
["/Applications/IINA.app/Contents/MacOS/iina-cli", url], # iina-cli
["/Applications/IINA.app/Contents/MacOS/IINA", url], # 直接调用
["open", "-a", "IINA", url], # open 命令兜底
]

for cmd in iina_cmds:
try:
subprocess.Popen(
cmd,
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL
)
print(f"[播放器] IINA 开始播放: {url}")
return
except FileNotFoundError:
continue

print("[错误] 无法启动 IINA,请确认已安装: brew install --cask iina")

def stop(self):
subprocess.run(["killall", "IINA"], capture_output=True)
print("[播放器] 已停止")
self.current_url = None

player = VideoPlayer()

# ==================== SSDP 服务发现 ====================
class SSDPServer(threading.Thread):
def __init__(self):
super().__init__(daemon=True)
self.running = True
self.sock = None

def run(self):
self.sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP)
self.sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
self.sock.bind(("0.0.0.0", SSDP_PORT))
mreq = struct.pack("4sl", socket.inet_aton(MULTICAST_ADDR), socket.INADDR_ANY)
self.sock.setsockopt(socket.IPPROTO_IP, socket.IP_ADD_MEMBERSHIP, mreq)
print(f"[SSDP] 服务已启动,监听 {MULTICAST_ADDR}:{SSDP_PORT}")

notify_thread = threading.Thread(target=self._send_notify_loop, daemon=True)
notify_thread.start()

while self.running:
try:
self.sock.settimeout(1.0)
data, addr = self.sock.recvfrom(2048)
self._handle_request(data.decode("utf-8", errors="ignore"), addr)
except socket.timeout:
continue
except Exception as e:
if self.running:
print(f"[SSDP] 错误: {e}")

def _handle_request(self, data, addr):
if "M-SEARCH" not in data:
return
st_match = re.search(r"ST:\s*(.+)", data, re.IGNORECASE)
if not st_match:
return
st = st_match.group(1).strip()
valid_sts = [
"ssdp:all", "upnp:rootdevice",
"urn:schemas-upnp-org:device:MediaRenderer:1",
"urn:schemas-upnp-org:service:AVTransport:1",
"urn:schemas-upnp-org:service:ConnectionManager:1",
"urn:schemas-upnp-org:service:RenderingControl:1",
]
if any(valid in st for valid in valid_sts):
self._send_response(addr, st)

def _send_response(self, addr, st):
response = (
"HTTP/1.1 200 OK\r\n"
"CACHE-CONTROL: max-age=1800\r\n"
f"DATE: {time.strftime('%a, %d %b %Y %H:%M:%S GMT', time.gmtime())}\r\n"
f"LOCATION: {DEVICE_DESC_URL}\r\n"
"SERVER: Python/3.x UPnP/1.0 MyDLNA/1.0\r\n"
f"ST: {st}\r\n"
f"USN: uuid:{DEVICE_UUID}::{st}\r\n"
"\r\n"
)
try:
self.sock.sendto(response.encode(), addr)
print(f"[SSDP] 响应搜索请求 -> {addr[0]}:{addr[1]}")
except Exception as e:
print(f"[SSDP] 发送响应失败: {e}")

def _send_notify_loop(self):
while self.running:
self._send_notify()
time.sleep(30)

def _send_notify(self):
for nt in ["upnp:rootdevice", "urn:schemas-upnp-org:device:MediaRenderer:1", "urn:schemas-upnp-org:service:AVTransport:1"]:
msg = (
"NOTIFY * HTTP/1.1\r\n"
f"HOST: {MULTICAST_ADDR}:{SSDP_PORT}\r\n"
"CACHE-CONTROL: max-age=1800\r\n"
f"LOCATION: {DEVICE_DESC_URL}\r\n"
"NTS: ssdp:alive\r\n"
"SERVER: Python/3.x UPnP/1.0 MyDLNA/1.0\r\n"
f"NT: {nt}\r\n"
f"USN: uuid:{DEVICE_UUID}::{nt}\r\n"
"\r\n"
)
try:
self.sock.sendto(msg.encode(), (MULTICAST_ADDR, SSDP_PORT))
except Exception:
pass

def stop(self):
self.running = False
if self.sock:
self.sock.close()

# ==================== XML 描述文件 ====================
DEVICE_DESCRIPTION = 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>MyDLNA</manufacturer>
<manufacturerURL>https://github.com</manufacturerURL>
<modelDescription>Python DLNA MediaRenderer</modelDescription>
<modelName>MyDLNA Receiver</modelName>
<modelNumber>1.0</modelNumber>
<modelURL>https://github.com</modelURL>
<serialNumber>001</serialNumber>
<UDN>uuid:{DEVICE_UUID}</UDN>
<serviceList>
<service>
<serviceType>urn:schemas-upnp-org:service:AVTransport:1</serviceType>
<serviceId>urn:upnp-org:serviceId:AVTransport</serviceId>
<SCPDURL>/service/AVTransport/scpd</SCPDURL>
<controlURL>/service/AVTransport/control</controlURL>
<eventSubURL>/service/AVTransport/event</eventSubURL>
</service>
<service>
<serviceType>urn:schemas-upnp-org:service:ConnectionManager:1</serviceType>
<serviceId>urn:upnp-org:serviceId:ConnectionManager</serviceId>
<SCPDURL>/service/ConnectionManager/scpd</SCPDURL>
<controlURL>/service/ConnectionManager/control</controlURL>
<eventSubURL>/service/ConnectionManager/event</eventSubURL>
</service>
<service>
<serviceType>urn:schemas-upnp-org:service:RenderingControl:1</serviceType>
<serviceId>urn:upnp-org:serviceId:RenderingControl</serviceId>
<SCPDURL>/service/RenderingControl/scpd</SCPDURL>
<controlURL>/service/RenderingControl/control</controlURL>
<eventSubURL>/service/RenderingControl/event</eventSubURL>
</service>
</serviceList>
</device>
</root>"""

AVTRANSPORT_SCPD = """<?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>
<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>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></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>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>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>
<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>Pause</name>
<argumentList>
<argument><name>InstanceID</name><direction>in</direction><relatedStateVariable>A_ARG_TYPE_InstanceID</relatedStateVariable></argument>
</argumentList>
</action>
</actionList>
<serviceStateTable>
<stateVariable sendEvents="no"><name>TransportState</name><dataType>string</dataType><allowedValueList><allowedValue>STOPPED</allowedValue><allowedValue>PLAYING</allowedValue><allowedValue>PAUSED_PLAYBACK</allowedValue><allowedValue>TRANSITIONING</allowedValue></allowedValueList></stateVariable>
<stateVariable sendEvents="no"><name>TransportStatus</name><dataType>string</dataType><allowedValueList><allowedValue>OK</allowedValue><allowedValue>ERROR_OCCURRED</allowedValue></allowedValueList></stateVariable>
<stateVariable sendEvents="no"><name>TransportPlaySpeed</name><dataType>string</dataType><defaultValue>1</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>NextAVTransportURI</name><dataType>string</dataType></stateVariable>
<stateVariable sendEvents="no"><name>NextAVTransportURIMetaData</name><dataType>string</dataType></stateVariable>
<stateVariable sendEvents="no"><name>NumberOfTracks</name><dataType>ui4</dataType><defaultValue>0</defaultValue></stateVariable>
<stateVariable sendEvents="no"><name>CurrentMediaDuration</name><dataType>string</dataType></stateVariable>
<stateVariable sendEvents="no"><name>CurrentTrack</name><dataType>ui4</dataType><defaultValue>0</defaultValue></stateVariable>
<stateVariable sendEvents="no"><name>CurrentTrackDuration</name><dataType>string</dataType></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></stateVariable>
<stateVariable sendEvents="no"><name>AbsoluteTimePosition</name><dataType>string</dataType></stateVariable>
<stateVariable sendEvents="no"><name>RelativeCounterPosition</name><dataType>i4</dataType></stateVariable>
<stateVariable sendEvents="no"><name>AbsoluteCounterPosition</name><dataType>i4</dataType></stateVariable>
<stateVariable sendEvents="no"><name>PlaybackStorageMedium</name><dataType>string</dataType><allowedValueList><allowedValue>NONE</allowedValue><allowedValue>NETWORK</allowedValue></allowedValueList></stateVariable>
<stateVariable sendEvents="no"><name>RecordStorageMedium</name><dataType>string</dataType><allowedValueList><allowedValue>NOT_IMPLEMENTED</allowedValue></allowedValueList></stateVariable>
<stateVariable sendEvents="no"><name>RecordMediumWriteStatus</name><dataType>string</dataType><allowedValueList><allowedValue>NOT_IMPLEMENTED</allowedValue></allowedValueList></stateVariable>
<stateVariable sendEvents="no"><name>A_ARG_TYPE_InstanceID</name><dataType>ui4</dataType></stateVariable>
</serviceStateTable>
</scpd>"""

CONNECTION_MANAGER_SCPD = """<?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>
<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>
</actionList>
<serviceStateTable>
<stateVariable sendEvents="no"><name>SourceProtocolInfo</name><dataType>string</dataType></stateVariable>
<stateVariable sendEvents="no"><name>SinkProtocolInfo</name><dataType>string</dataType><defaultValue>http-get:*:video/mp4:*,http-get:*:video/x-matroska:*,http-get:*:video/x-msvideo:*,http-get:*:audio/mpeg:*,http-get:*:audio/mp4:*</defaultValue></stateVariable>
</serviceStateTable>
</scpd>"""

RENDERING_CONTROL_SCPD = """<?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>
<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>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>
<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>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>
</actionList>
<serviceStateTable>
<stateVariable sendEvents="no"><name>Volume</name><dataType>ui2</dataType><allowedValueRange><minimum>0</minimum><maximum>100</maximum></allowedValueRange></stateVariable>
<stateVariable sendEvents="no"><name>Mute</name><dataType>boolean</dataType></stateVariable>
<stateVariable sendEvents="no"><name>A_ARG_TYPE_InstanceID</name><dataType>ui4</dataType></stateVariable>
<stateVariable sendEvents="no"><name>A_ARG_TYPE_Channel</name><dataType>string</dataType><allowedValueList><allowedValue>Master</allowedValue></allowedValueList></stateVariable>
</serviceStateTable>
</scpd>"""

# ==================== HTTP 处理器 ====================
class DLNAHandler(BaseHTTPRequestHandler):
current_uri = None
transport_state = "STOPPED"
current_track_duration = "00:00:00"
current_position = "00:00:00"

def log_message(self, format, *args):
print(f"[HTTP] {self.address_string()} - {format % args}")

def do_GET(self):
path = self.path
if path == "/description.xml":
self._send_xml(DEVICE_DESCRIPTION)
elif path == "/service/AVTransport/scpd":
self._send_xml(AVTRANSPORT_SCPD)
elif path == "/service/ConnectionManager/scpd":
self._send_xml(CONNECTION_MANAGER_SCPD)
elif path == "/service/RenderingControl/scpd":
self._send_xml(RENDERING_CONTROL_SCPD)
elif path == "/qr.png":
self._send_qr_image()
else:
self.send_error(404, "Not Found")

def do_POST(self):
path = self.path
if "/control" in path:
content_length = int(self.headers.get("Content-Length", 0))
body = self.rfile.read(content_length).decode("utf-8")
action = self._parse_soap_action(body)
service_type = self._get_service_type(path)

if "SetAVTransportURI" in action:
self._handle_set_av_transport_uri(body, service_type)
elif "Play" in action:
self._handle_play(service_type)
elif "Stop" in action:
self._handle_stop(service_type)
elif "Pause" in action:
self._handle_pause(service_type)
elif "GetTransportInfo" in action:
self._handle_get_transport_info(service_type)
elif "GetMediaInfo" in action:
self._handle_get_media_info(service_type)
elif "GetPositionInfo" in action:
self._handle_get_position_info(service_type)
elif "GetProtocolInfo" in action:
self._handle_get_protocol_info(service_type)
elif "GetVolume" in action:
self._handle_get_volume(service_type)
elif "SetVolume" in action:
self._send_soap_response(action, "", service_type)
elif "GetMute" in action:
self._handle_get_mute(service_type)
elif "SetMute" in action:
self._send_soap_response(action, "", service_type)
else:
self._send_soap_response(action, "", service_type)
else:
self.send_error(404, "Not Found")

def do_SUBSCRIBE(self):
sid = f"uuid:{uuid.uuid4()}"
self.send_response(200)
self.send_header("SID", sid)
self.send_header("TIMEOUT", "Second-1800")
self.end_headers()
print(f"[事件] TVBox 接收成功 -> SID: {sid}")

def do_UNSUBSCRIBE(self):
self.send_response(200)
self.end_headers()
print("[事件] TVBox 继续等待下一个视频")

def _send_xml(self, content):
self.send_response(200)
self.send_header("Content-Type", "text/xml; charset=utf-8")
self.send_header("Content-Length", len(content))
self.end_headers()
self.wfile.write(content.encode())

def _send_qr_image(self):
qr_path = generate_qr_code()
if os.path.exists(qr_path):
with open(qr_path, "rb") as f:
data = f.read()
self.send_response(200)
self.send_header("Content-Type", "image/png")
self.send_header("Content-Length", len(data))
self.end_headers()
self.wfile.write(data)
else:
self.send_error(404, "QR Code not found")

def _parse_soap_action(self, body):
soap_action = self.headers.get("SOAPAction", "")
if soap_action:
return soap_action.strip('"').split("#")[-1]
match = re.search(r"<u:([A-Za-z]+)", body)
if match:
return match.group(1)
return ""

def _get_service_type(self, path):
if "AVTransport" in path:
return "urn:schemas-upnp-org:service:AVTransport:1"
elif "ConnectionManager" in path:
return "urn:schemas-upnp-org:service:ConnectionManager:1"
elif "RenderingControl" in path:
return "urn:schemas-upnp-org:service:RenderingControl:1"
return "urn:schemas-upnp-org:service:AVTransport:1"

def _extract_tag_value(self, body, tag_name):
patterns = [
f"<{tag_name}>([^<]+)</{tag_name}>",
f"<[^>]*:{tag_name}>([^<]+)</[^>]*:{tag_name}>",
]
for pattern in patterns:
match = re.search(pattern, body)
if match:
return match.group(1)
return ""

def _send_soap_response(self, action, body_content, service_type):
response = f"""<?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:{action}Response xmlns:u="{service_type}">
{body_content}
</u:{action}Response>
</s:Body>
</s:Envelope>"""
self.send_response(200)
self.send_header("Content-Type", "text/xml; charset=utf-8")
self.send_header("Content-Length", len(response))
self.end_headers()
self.wfile.write(response.encode())

def _handle_set_av_transport_uri(self, body, service_type):
uri = self._extract_tag_value(body, "CurrentURI")
if uri:
DLNAHandler.current_uri = uri
DLNAHandler.transport_state = "STOPPED"
print(f"[投屏] 接收到视频 URL: {uri}")
self._send_soap_response("SetAVTransportURI", "", service_type)

def _handle_play(self, service_type):
if DLNAHandler.current_uri:
DLNAHandler.transport_state = "PLAYING"
player.play(DLNAHandler.current_uri)
self._send_soap_response("Play", "", service_type)

def _handle_stop(self, service_type):
DLNAHandler.transport_state = "STOPPED"
player.stop()
self._send_soap_response("Stop", "", service_type)

def _handle_pause(self, service_type):
DLNAHandler.transport_state = "PAUSED_PLAYBACK"
self._send_soap_response("Pause", "", service_type)

def _handle_get_transport_info(self, service_type):
body = f"""<CurrentTransportState>{DLNAHandler.transport_state}</CurrentTransportState>
<CurrentTransportStatus>OK</CurrentTransportStatus>
<CurrentSpeed>1</CurrentSpeed>"""
self._send_soap_response("GetTransportInfo", body, service_type)

def _handle_get_media_info(self, service_type):
body = f"""<NrTracks>1</NrTracks>
<MediaDuration>{DLNAHandler.current_track_duration}</MediaDuration>
<CurrentURI>{DLNAHandler.current_uri or ""}</CurrentURI>
<CurrentURIMetaData></CurrentURIMetaData>
<NextURI></NextURI>
<NextURIMetaData></NextURIMetaData>
<PlayMedium>NETWORK</PlayMedium>
<RecordMedium>NOT_IMPLEMENTED</RecordMedium>
<WriteStatus>NOT_IMPLEMENTED</WriteStatus>"""
self._send_soap_response("GetMediaInfo", body, service_type)

def _handle_get_position_info(self, service_type):
body = f"""<Track>1</Track>
<TrackDuration>{DLNAHandler.current_track_duration}</TrackDuration>
<TrackMetaData></TrackMetaData>
<TrackURI>{DLNAHandler.current_uri or ""}</TrackURI>
<RelTime>{DLNAHandler.current_position}</RelTime>
<AbsTime>{DLNAHandler.current_position}</AbsTime>
<RelCount>0</RelCount>
<AbsCount>0</AbsCount>"""
self._send_soap_response("GetPositionInfo", body, service_type)

def _handle_get_protocol_info(self, service_type):
body = """<Source></Source>
<Sink>http-get:*:video/mp4:*,http-get:*:video/x-matroska:*,http-get:*:video/x-msvideo:*,http-get:*:audio/mpeg:*,http-get:*:audio/mp4:*</Sink>"""
self._send_soap_response("GetProtocolInfo", body, service_type)

def _handle_get_volume(self, service_type):
body = "<CurrentVolume>50</CurrentVolume>"
self._send_soap_response("GetVolume", body, service_type)

def _handle_get_mute(self, service_type):
body = "<CurrentMute>0</CurrentMute>"
self._send_soap_response("GetMute", body, service_type)

# ==================== 二维码生成 ====================
def generate_qr_code():
qr_path = os.path.join(os.getcwd(), "dlna_qr.png")
qr_data = DEVICE_DESC_URL
qr = qrcode.QRCode(
version=1,
error_correction=qrcode.constants.ERROR_CORRECT_H,
box_size=10,
border=4,
)
qr.add_data(qr_data)
qr.make(fit=True)
img = qr.make_image(fill_color="black", back_color="white")
img.save(qr_path)
print(f"[二维码] 已生成: {qr_path}")
print(f"[二维码] 内容: {qr_data}")
return qr_path

# ==================== 主程序 ====================
def main():
print("=" * 60)
print(" DLNA MediaRenderer 接收服务端 (IINA 版)")
print("=" * 60)
print(f"设备名称: {DEVICE_NAME}")
print(f"设备UUID: {DEVICE_UUID}")
print(f"本机IP: {get_local_ip()}")
print(f"HTTP端口: {HTTP_PORT}")
print(f"设备描述: {DEVICE_DESC_URL}")
print("=" * 60)

qr_path = generate_qr_code()
print(f"\n请用 TVBox 扫描二维码或访问: {BASE_URL}/qr.png")
print(f"二维码文件: {qr_path}")

ssdp = SSDPServer()
ssdp.start()

httpd = HTTPServer(("0.0.0.0", HTTP_PORT), DLNAHandler)
print(f"\n[HTTP] 服务已启动,监听 0.0.0.0:{HTTP_PORT}")
print("\n等待投屏... (按 Ctrl+C 停止)\n")

try:
httpd.serve_forever()
except KeyboardInterrupt:
print("\n\n正在停止服务...")
ssdp.stop()
httpd.shutdown()
player.stop()
print("服务已停止")

if __name__ == "__main__":
main()

2.3iPad端:

​ 我的 iPad 上装了 Pythonista,可以像OK 影视一样接受投屏,脚本如下:

2.3.1接受投送的方案
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
from http.server import BaseHTTPRequestHandler, HTTPServer
import urllib.parse
import webbrowser
import threading
import time

PORT = 9978

class VideoHandler(BaseHTTPRequestHandler):

def do_POST(self):
content_length = int(self.headers.get('Content-Length', 0))
body = self.rfile.read(content_length).decode("utf-8")

print("\n📥 收到 POST 数据:")
print(body)

# 解析表单数据
data = urllib.parse.parse_qs(body)
video_url = data.get("url", [""])[0]
action = data.get("do", [""])[0]

if video_url and action == "push":
print(f"▶️ 准备播放视频:\n{video_url}")

# 在新线程中打开浏览器,防止阻塞 HTTP 服务
threading.Thread(
target=self.open_browser,
args=(video_url,),
daemon=True
).start()

self.send_response(200)
self.send_header("Content-Type", "text/plain; charset=utf-8")
self.end_headers()
self.wfile.write(f"playing {video_url}".encode("utf-8"))
else:
self.send_response(400)
self.end_headers()
self.wfile.write(b"invalid request")

def open_browser(self, url):
# 稍微延迟,确保 HTTP 响应先返回
time.sleep(0.2)
webbrowser.open(url)

def log_message(self, format, *args):
# 简化日志
print(args)

def run():
server = HTTPServer(("0.0.0.0", PORT), VideoHandler)
print(f"🚀 视频接收服务已启动:http://0.0.0.0:{PORT}/action")
print("等待 iPad 发送视频链接...")
server.serve_forever()

if __name__ == "__main__":
run()

2.3.2接受投屏的方案
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
DLNA MediaRenderer 接收服务端 (Pythonista / iPadOS 终极兼容版)
"""

import socket
import struct
import threading
import time
import uuid
import re
import html
import webbrowser
import xml.etree.ElementTree as ET
from http.server import HTTPServer, BaseHTTPRequestHandler

# ==================== 配置 ====================
HTTP_PORT = 8080
SSDP_PORT = 1900
MULTICAST_ADDR = "239.255.255.250"
DEVICE_NAME = "MyDLNA-iPad-Pro"
DEVICE_UUID = str(uuid.uuid5(uuid.NAMESPACE_DNS, "ipad-dlna.local"))

LOCAL_IP = None

def get_local_ip():
global LOCAL_IP
if LOCAL_IP:
return LOCAL_IP
try:
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
s.connect(("8.8.8.8", 80))
LOCAL_IP = s.getsockname()[0]
s.close()
except Exception:
LOCAL_IP = "127.0.0.1"
return LOCAL_IP

BASE_URL = f"http://{get_local_ip()}:{HTTP_PORT}"
DEVICE_DESC_URL = f"{BASE_URL}/description.xml"

# ==================== Safari 播放器 ====================
class SafariPlayer:
def __init__(self):
self.current_url = None

def play(self, url):
url = html.unescape(url)
self.current_url = url
webbrowser.open(url)
print(f"[播放器] Safari 已打开: {url}")

def stop(self):
self.current_url = None
print("[播放器] 已停止")

player = SafariPlayer()

# ==================== 动态生成 XML (确保 100% 格式正确) ====================
def get_device_description():
"""用 ElementTree 生成设备描述 XML,避免手写格式错误"""
root = ET.Element("root", {
"xmlns": "urn:schemas-upnp-org:device-1-0"
})

specVersion = ET.SubElement(root, "specVersion")
ET.SubElement(specVersion, "major").text = "1"
ET.SubElement(specVersion, "minor").text = "0"

device = ET.SubElement(root, "device")
ET.SubElement(device, "deviceType").text = "urn:schemas-upnp-org:device:MediaRenderer:1"
ET.SubElement(device, "friendlyName").text = DEVICE_NAME
ET.SubElement(device, "manufacturer").text = "MyDLNA"
ET.SubElement(device, "manufacturerURL").text = "https://github.com"
ET.SubElement(device, "modelDescription").text = "Pythonista DLNA MediaRenderer"
ET.SubElement(device, "modelName").text = "MyDLNA Receiver"
ET.SubElement(device, "modelNumber").text = "1.0"
ET.SubElement(device, "modelURL").text = "https://github.com"
ET.SubElement(device, "serialNumber").text = "001"
ET.SubElement(device, "UDN").text = f"uuid:{DEVICE_UUID}"

serviceList = ET.SubElement(device, "serviceList")

services = [
("urn:schemas-upnp-org:service:AVTransport:1", "urn:upnp-org:serviceId:AVTransport", "/service/AVTransport"),
("urn:schemas-upnp-org:service:ConnectionManager:1", "urn:upnp-org:serviceId:ConnectionManager", "/service/ConnectionManager"),
("urn:schemas-upnp-org:service:RenderingControl:1", "urn:upnp-org:serviceId:RenderingControl", "/service/RenderingControl"),
]

for st, sid, base in services:
svc = ET.SubElement(serviceList, "service")
ET.SubElement(svc, "serviceType").text = st
ET.SubElement(svc, "serviceId").text = sid
ET.SubElement(svc, "SCPDURL").text = f"{base}/scpd"
ET.SubElement(svc, "controlURL").text = f"{base}/control"
ET.SubElement(svc, "eventSubURL").text = f"{base}/event"

return '<?xml version="1.0"?>\n' + ET.tostring(root, encoding="unicode")

def get_avtransport_scpd():
root = ET.Element("scpd", {"xmlns": "urn:schemas-upnp-org:service-1-0"})
spec = ET.SubElement(root, "specVersion")
ET.SubElement(spec, "major").text = "1"
ET.SubElement(spec, "minor").text = "0"

actions = ET.SubElement(root, "actionList")

for name, args in [
("SetAVTransportURI", [("InstanceID","in","A_ARG_TYPE_InstanceID"), ("CurrentURI","in","AVTransportURI"), ("CurrentURIMetaData","in","AVTransportURIMetaData")]),
("GetMediaInfo", [("InstanceID","in","A_ARG_TYPE_InstanceID"), ("NrTracks","out","NumberOfTracks"), ("MediaDuration","out","CurrentMediaDuration"), ("CurrentURI","out","AVTransportURI"), ("CurrentURIMetaData","out","AVTransportURIMetaData"), ("NextURI","out","NextAVTransportURI"), ("NextURIMetaData","out","NextAVTransportURIMetaData"), ("PlayMedium","out","PlaybackStorageMedium"), ("RecordMedium","out","RecordStorageMedium"), ("WriteStatus","out","RecordMediumWriteStatus")]),
("GetTransportInfo", [("InstanceID","in","A_ARG_TYPE_InstanceID"), ("CurrentTransportState","out","TransportState"), ("CurrentTransportStatus","out","TransportStatus"), ("CurrentSpeed","out","TransportPlaySpeed")]),
("GetPositionInfo", [("InstanceID","in","A_ARG_TYPE_InstanceID"), ("Track","out","CurrentTrack"), ("TrackDuration","out","CurrentTrackDuration"), ("TrackMetaData","out","CurrentTrackMetaData"), ("TrackURI","out","CurrentTrackURI"), ("RelTime","out","RelativeTimePosition"), ("AbsTime","out","AbsoluteTimePosition"), ("RelCount","out","RelativeCounterPosition"), ("AbsCount","out","AbsoluteCounterPosition")]),
("Play", [("InstanceID","in","A_ARG_TYPE_InstanceID"), ("Speed","in","TransportPlaySpeed")]),
("Stop", [("InstanceID","in","A_ARG_TYPE_InstanceID")]),
("Pause", [("InstanceID","in","A_ARG_TYPE_InstanceID")]),
]:
act = ET.SubElement(actions, "action")
ET.SubElement(act, "name").text = name
argList = ET.SubElement(act, "argumentList")
for n, d, r in args:
a = ET.SubElement(argList, "argument")
ET.SubElement(a, "name").text = n
ET.SubElement(a, "direction").text = d
ET.SubElement(a, "relatedStateVariable").text = r

stateTable = ET.SubElement(root, "serviceStateTable")
for name, dtype, default, allowed in [
("TransportState", "string", None, ["STOPPED","PLAYING","PAUSED_PLAYBACK"]),
("TransportStatus", "string", None, ["OK","ERROR_OCCURRED"]),
("TransportPlaySpeed", "string", "1", None),
("AVTransportURI", "string", None, None),
("AVTransportURIMetaData", "string", None, None),
("NextAVTransportURI", "string", None, None),
("NextAVTransportURIMetaData", "string", None, None),
("NumberOfTracks", "ui4", "0", None),
("CurrentMediaDuration", "string", None, None),
("CurrentTrack", "ui4", "0", None),
("CurrentTrackDuration", "string", None, None),
("CurrentTrackMetaData", "string", None, None),
("CurrentTrackURI", "string", None, None),
("RelativeTimePosition", "string", None, None),
("AbsoluteTimePosition", "string", None, None),
("RelativeCounterPosition", "i4", None, None),
("AbsoluteCounterPosition", "i4", None, None),
("PlaybackStorageMedium", "string", None, ["NONE","NETWORK"]),
("RecordStorageMedium", "string", None, ["NOT_IMPLEMENTED"]),
("RecordMediumWriteStatus", "string", None, ["NOT_IMPLEMENTED"]),
("A_ARG_TYPE_InstanceID", "ui4", None, None),
]:
sv = ET.SubElement(stateTable, "stateVariable", {"sendEvents": "no"})
ET.SubElement(sv, "name").text = name
ET.SubElement(sv, "dataType").text = dtype
if default:
ET.SubElement(sv, "defaultValue").text = default
if allowed:
avl = ET.SubElement(sv, "allowedValueList")
for v in allowed:
ET.SubElement(avl, "allowedValue").text = v

return '<?xml version="1.0"?>\n' + ET.tostring(root, encoding="unicode")

def get_connection_manager_scpd():
root = ET.Element("scpd", {"xmlns": "urn:schemas-upnp-org:service-1-0"})
spec = ET.SubElement(root, "specVersion")
ET.SubElement(spec, "major").text = "1"
ET.SubElement(spec, "minor").text = "0"
actions = ET.SubElement(root, "actionList")
act = ET.SubElement(actions, "action")
ET.SubElement(act, "name").text = "GetProtocolInfo"
argList = ET.SubElement(act, "argumentList")
for n, d, r in [("Source","out","SourceProtocolInfo"), ("Sink","out","SinkProtocolInfo")]:
a = ET.SubElement(argList, "argument")
ET.SubElement(a, "name").text = n
ET.SubElement(a, "direction").text = d
ET.SubElement(a, "relatedStateVariable").text = r
stateTable = ET.SubElement(root, "serviceStateTable")
for name, dtype, default in [
("SourceProtocolInfo", "string", None),
("SinkProtocolInfo", "string", "http-get:*:video/mp4:*,http-get:*:video/x-matroska:*,http-get:*:video/x-msvideo:*,http-get:*:audio/mpeg:*,http-get:*:audio/mp4:*"),
]:
sv = ET.SubElement(stateTable, "stateVariable", {"sendEvents": "no"})
ET.SubElement(sv, "name").text = name
ET.SubElement(sv, "dataType").text = dtype
if default:
ET.SubElement(sv, "defaultValue").text = default
return '<?xml version="1.0"?>\n' + ET.tostring(root, encoding="unicode")

def get_rendering_control_scpd():
root = ET.Element("scpd", {"xmlns": "urn:schemas-upnp-org:service-1-0"})
spec = ET.SubElement(root, "specVersion")
ET.SubElement(spec, "major").text = "1"
ET.SubElement(spec, "minor").text = "0"
actions = ET.SubElement(root, "actionList")
for name, args in [
("GetVolume", [("InstanceID","in","A_ARG_TYPE_InstanceID"), ("Channel","in","A_ARG_TYPE_Channel"), ("CurrentVolume","out","Volume")]),
("SetVolume", [("InstanceID","in","A_ARG_TYPE_InstanceID"), ("Channel","in","A_ARG_TYPE_Channel"), ("DesiredVolume","in","Volume")]),
("GetMute", [("InstanceID","in","A_ARG_TYPE_InstanceID"), ("Channel","in","A_ARG_TYPE_Channel"), ("CurrentMute","out","Mute")]),
("SetMute", [("InstanceID","in","A_ARG_TYPE_InstanceID"), ("Channel","in","A_ARG_TYPE_Channel"), ("DesiredMute","in","Mute")]),
]:
act = ET.SubElement(actions, "action")
ET.SubElement(act, "name").text = name
argList = ET.SubElement(act, "argumentList")
for n, d, r in args:
a = ET.SubElement(argList, "argument")
ET.SubElement(a, "name").text = n
ET.SubElement(a, "direction").text = d
ET.SubElement(a, "relatedStateVariable").text = r
stateTable = ET.SubElement(root, "serviceStateTable")
for name, dtype, default, allowed in [
("Volume", "ui2", None, None),
("Mute", "boolean", None, None),
("A_ARG_TYPE_InstanceID", "ui4", None, None),
("A_ARG_TYPE_Channel", "string", None, ["Master"]),
]:
sv = ET.SubElement(stateTable, "stateVariable", {"sendEvents": "no"})
ET.SubElement(sv, "name").text = name
ET.SubElement(sv, "dataType").text = dtype
if default:
ET.SubElement(sv, "defaultValue").text = default
if allowed:
avl = ET.SubElement(sv, "allowedValueList")
for v in allowed:
ET.SubElement(avl, "allowedValue").text = v
return '<?xml version="1.0"?>\n' + ET.tostring(root, encoding="unicode")

# 预生成 XML
DEVICE_DESCRIPTION = get_device_description()
AVTRANSPORT_SCPD = get_avtransport_scpd()
CONNECTION_MANAGER_SCPD = get_connection_manager_scpd()
RENDERING_CONTROL_SCPD = get_rendering_control_scpd()

# ==================== SSDP 服务发现 ====================
class SSDPServer(threading.Thread):
def __init__(self):
super().__init__(daemon=True)
self.running = True
self.sock = None

def run(self):
self.sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP)
self.sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
self.sock.bind(("0.0.0.0", SSDP_PORT))
mreq = struct.pack("4sl", socket.inet_aton(MULTICAST_ADDR), socket.INADDR_ANY)
self.sock.setsockopt(socket.IPPROTO_IP, socket.IP_ADD_MEMBERSHIP, mreq)
print(f"[SSDP] 服务已启动,监听 {MULTICAST_ADDR}:{SSDP_PORT}")

notify_thread = threading.Thread(target=self._send_notify_loop, daemon=True)
notify_thread.start()

while self.running:
try:
self.sock.settimeout(1.0)
data, addr = self.sock.recvfrom(2048)
self._handle_request(data.decode("utf-8", errors="ignore"), addr)
except socket.timeout:
continue
except Exception as e:
if self.running:
print(f"[SSDP] 错误: {e}")

def _handle_request(self, data, addr):
if "M-SEARCH" not in data:
return

st_match = re.search(r"ST:\s*(.+?)(?:\r|\n|$)", data, re.IGNORECASE)
if not st_match:
return

st = st_match.group(1).strip()
print(f"[SSDP] TVBox 搜索: {st} (来自 {addr[0]}:{addr[1]})")

# 响应所有可能的 DLNA 搜索目标
valid_keywords = [
"ssdp:all", "upnp:rootdevice", "MediaRenderer", "MediaRenderer:1",
"MediaRenderer:2", "MediaRenderer:3", "AVTransport", "ConnectionManager", "RenderingControl"
]
if not any(kw in st for kw in valid_keywords):
print(f"[SSDP] ST 不匹配,忽略")
return

self._send_response(addr, st)

def _send_response(self, addr, st):
"""发送 SSDP 响应 - 包含 EXT 头,兼容 cling 库"""
response = (
"HTTP/1.1 200 OK\r\n"
"CACHE-CONTROL: max-age=1800\r\n"
f"DATE: {time.strftime('%a, %d %b %Y %H:%M:%S GMT', time.gmtime())}\r\n"
"EXT:\r\n"
f"LOCATION: {DEVICE_DESC_URL}\r\n"
"SERVER: UPnP/1.0 Python/3.x iPadDLNA/1.0\r\n"
f"ST: {st}\r\n"
f"USN: uuid:{DEVICE_UUID}::{st}\r\n"
"\r\n"
)

try:
self.sock.sendto(response.encode(), addr)
print(f"[SSDP] ✅ 已响应 -> {addr[0]}:{addr[1]}")
except Exception as e:
print(f"[SSDP] 发送失败: {e}")

def _send_notify_loop(self):
while self.running:
self._send_notify()
time.sleep(15)

def _send_notify(self):
notifies = [
"upnp:rootdevice",
"urn:schemas-upnp-org:device:MediaRenderer:1",
"urn:schemas-upnp-org:device:MediaRenderer:2",
"urn:schemas-upnp-org:device:MediaRenderer:3",
"urn:schemas-upnp-org:service:AVTransport:1",
]

for nt in notifies:
msg = (
"NOTIFY * HTTP/1.1\r\n"
f"HOST: {MULTICAST_ADDR}:{SSDP_PORT}\r\n"
"CACHE-CONTROL: max-age=1800\r\n"
f"LOCATION: {DEVICE_DESC_URL}\r\n"
"NTS: ssdp:alive\r\n"
"EXT:\r\n"
"SERVER: UPnP/1.0 Python/3.x iPadDLNA/1.0\r\n"
f"NT: {nt}\r\n"
f"USN: uuid:{DEVICE_UUID}::{nt}\r\n"
"\r\n"
)
try:
self.sock.sendto(msg.encode(), (MULTICAST_ADDR, SSDP_PORT))
except Exception:
pass

def stop(self):
self.running = False
if self.sock:
self.sock.close()

# ==================== 强制 IPv4 的 HTTP 服务器 ====================
class HTTPServerV4(HTTPServer):
address_family = socket.AF_INET

class DLNAHandler(BaseHTTPRequestHandler):
current_uri = None
transport_state = "STOPPED"

def log_message(self, format, *args):
print(f"[HTTP] {self.address_string()} - {format % args}")

def do_GET(self):
path = self.path
print(f"[HTTP] GET {path}")

if path == "/":
# 诊断页面
self._send_diagnostic()
elif path == "/description.xml":
self._send_xml(DEVICE_DESCRIPTION)
elif path == "/service/AVTransport/scpd":
self._send_xml(AVTRANSPORT_SCPD)
elif path == "/service/ConnectionManager/scpd":
self._send_xml(CONNECTION_MANAGER_SCPD)
elif path == "/service/RenderingControl/scpd":
self._send_xml(RENDERING_CONTROL_SCPD)
else:
self.send_error(404, "Not Found")

def _send_diagnostic(self):
"""返回诊断页面,方便测试 HTTP 是否可达"""
html = f"""<html><body>
<h1>iPad DLNA 诊断页面</h1>
<p>设备名: {DEVICE_NAME}</p>
<p>IP: {LOCAL_IP}</p>
<p>HTTP 服务正常</p>
<p><a href="/description.xml">查看设备描述 XML</a></p>
</body></html>"""
self.send_response(200)
self.send_header("Content-Type", "text/html; charset=utf-8")
self.send_header("Content-Length", len(html.encode()))
self.end_headers()
self.wfile.write(html.encode())

def _send_xml(self, content):
data = content.encode("utf-8")
self.send_response(200)
self.send_header("Content-Type", "text/xml; charset=utf-8")
self.send_header("Content-Length", len(data))
self.end_headers()
self.wfile.write(data)

def do_POST(self):
path = self.path
if "/control" in path:
content_length = int(self.headers.get("Content-Length", 0))
body = self.rfile.read(content_length).decode("utf-8")
action = self._parse_soap_action(body)
service_type = self._get_service_type(path)

if "SetAVTransportURI" in action:
self._handle_set_av_transport_uri(body, service_type)
elif "Play" in action:
self._handle_play(service_type)
elif "Stop" in action:
self._handle_stop(service_type)
elif "Pause" in action:
self._handle_pause(service_type)
elif "GetTransportInfo" in action:
self._handle_get_transport_info(service_type)
elif "GetMediaInfo" in action:
self._handle_get_media_info(service_type)
elif "GetPositionInfo" in action:
self._handle_get_position_info(service_type)
elif "GetProtocolInfo" in action:
self._handle_get_protocol_info(service_type)
elif "GetVolume" in action:
self._handle_get_volume(service_type)
elif "SetVolume" in action:
self._send_soap_response(action, "", service_type)
elif "GetMute" in action:
self._handle_get_mute(service_type)
elif "SetMute" in action:
self._send_soap_response(action, "", service_type)
else:
self._send_soap_response(action, "", service_type)
else:
self.send_error(404, "Not Found")

def do_SUBSCRIBE(self):
sid = f"uuid:{uuid.uuid4()}"
self.send_response(200)
self.send_header("SID", sid)
self.send_header("TIMEOUT", "Second-1800")
self.end_headers()
print(f"[事件] TVBox 接收成功 -> SID: {sid}")

def do_UNSUBSCRIBE(self):
self.send_response(200)
self.end_headers()
print("[事件] TVBox 等待下一个视频")

def _parse_soap_action(self, body):
soap_action = self.headers.get("SOAPAction", "")
if soap_action:
return soap_action.strip('"').split("#")[-1]
match = re.search(r"<u:([A-Za-z]+)", body)
if match:
return match.group(1)
return ""

def _get_service_type(self, path):
if "AVTransport" in path:
return "urn:schemas-upnp-org:service:AVTransport:1"
elif "ConnectionManager" in path:
return "urn:schemas-upnp-org:service:ConnectionManager:1"
elif "RenderingControl" in path:
return "urn:schemas-upnp-org:service:RenderingControl:1"
return "urn:schemas-upnp-org:service:AVTransport:1"

def _extract_tag_value(self, body, tag_name):
patterns = [
f"<{tag_name}>([^<]+)</{tag_name}>",
f"<[^>]*:{tag_name}>([^<]+)</[^>]*:{tag_name}>",
]
for pattern in patterns:
match = re.search(pattern, body)
if match:
return match.group(1)
return ""

def _send_soap_response(self, action, body_content, service_type):
response = f"""<?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:{action}Response xmlns:u="{service_type}">
{body_content}
</u:{action}Response>
</s:Body>
</s:Envelope>"""
data = response.encode("utf-8")
self.send_response(200)
self.send_header("Content-Type", "text/xml; charset=utf-8")
self.send_header("Content-Length", len(data))
self.end_headers()
self.wfile.write(data)

def _handle_set_av_transport_uri(self, body, service_type):
uri = self._extract_tag_value(body, "CurrentURI")
if uri:
DLNAHandler.current_uri = uri
DLNAHandler.transport_state = "STOPPED"
print(f"[投屏] 接收到视频 URL: {uri}")
self._send_soap_response("SetAVTransportURI", "", service_type)

def _handle_play(self, service_type):
if DLNAHandler.current_uri:
DLNAHandler.transport_state = "PLAYING"
player.play(DLNAHandler.current_uri)
self._send_soap_response("Play", "", service_type)

def _handle_stop(self, service_type):
DLNAHandler.transport_state = "STOPPED"
player.stop()
self._send_soap_response("Stop", "", service_type)

def _handle_pause(self, service_type):
DLNAHandler.transport_state = "PAUSED_PLAYBACK"
self._send_soap_response("Pause", "", service_type)

def _handle_get_transport_info(self, service_type):
body = f"""<CurrentTransportState>{DLNAHandler.transport_state}</CurrentTransportState>
<CurrentTransportStatus>OK</CurrentTransportStatus>
<CurrentSpeed>1</CurrentSpeed>"""
self._send_soap_response("GetTransportInfo", body, service_type)

def _handle_get_media_info(self, service_type):
body = f"""<NrTracks>1</NrTracks>
<MediaDuration>00:00:00</MediaDuration>
<CurrentURI>{DLNAHandler.current_uri or ""}</CurrentURI>
<CurrentURIMetaData></CurrentURIMetaData>
<NextURI></NextURI>
<NextURIMetaData></NextURIMetaData>
<PlayMedium>NETWORK</PlayMedium>
<RecordMedium>NOT_IMPLEMENTED</RecordMedium>
<WriteStatus>NOT_IMPLEMENTED</WriteStatus>"""
self._send_soap_response("GetMediaInfo", body, service_type)

def _handle_get_position_info(self, service_type):
body = f"""<Track>1</Track>
<TrackDuration>00:00:00</TrackDuration>
<TrackMetaData></TrackMetaData>
<TrackURI>{DLNAHandler.current_uri or ""}</TrackURI>
<RelTime>00:00:00</RelTime>
<AbsTime>00:00:00</AbsTime>
<RelCount>0</RelCount>
<AbsCount>0</AbsCount>"""
self._send_soap_response("GetPositionInfo", body, service_type)

def _handle_get_protocol_info(self, service_type):
body = """<Source></Source>
<Sink>http-get:*:video/mp4:*,http-get:*:video/x-matroska:*,http-get:*:video/x-msvideo:*,http-get:*:audio/mpeg:*,http-get:*:audio/mp4:*</Sink>"""
self._send_soap_response("GetProtocolInfo", body, service_type)

def _handle_get_volume(self, service_type):
body = "<CurrentVolume>50</CurrentVolume>"
self._send_soap_response("GetVolume", body, service_type)

def _handle_get_mute(self, service_type):
body = "<CurrentMute>0</CurrentMute>"
self._send_soap_response("GetMute", body, service_type)

# ==================== 主程序 ====================
def main():
print("=" * 60)
print(" DLNA MediaRenderer 接收服务端 (Pythonista / iPadOS)")
print("=" * 60)
print(f"设备名称: {DEVICE_NAME}")
print(f"设备UUID: {DEVICE_UUID}")
print(f"本机IP: {get_local_ip()}")
print(f"HTTP端口: {HTTP_PORT}")
print(f"设备描述: {DEVICE_DESC_URL}")
print("=" * 60)
print("\n⚠️ 请保持 Pythonista 在前台运行")
print("⚠️ 请检查: 设置 -> 隐私 -> 本地网络 -> Pythonista (必须开启)")
print("=" * 60)
print(f"\n诊断页面: http://{LOCAL_IP}:{HTTP_PORT}/")
print("先在同一 Wi-Fi 的 Safari 中打开诊断页面,确认 HTTP 可达")

ssdp = SSDPServer()
ssdp.start()

# 强制使用 IPv4
httpd = HTTPServerV4(("0.0.0.0", HTTP_PORT), DLNAHandler)
print(f"\n[HTTP] 服务已启动,监听 0.0.0.0:{HTTP_PORT}")
print("\n等待投屏... (按 Ctrl+C 停止)\n")

try:
httpd.serve_forever()
except KeyboardInterrupt:
print("\n\n正在停止服务...")
ssdp.stop()
httpd.shutdown()
print("服务已停止")

if __name__ == "__main__":
main()
-------------本文结束 感谢阅读-------------
  • 本文作者: HuangLei
  • 本文链接: https://maxlay.github.io/2026/08/11/投屏/
  • 版权声明: 本博客所有原创文章(除文末特别标注转载或特殊声明外),均采用知识共享署名。非商业性使用 ,相同方式共享 4.0 国际许可协议 BY-NC-SA 进行许可。
    您可以:自由地分享(复制、分发、传播)和演绎(修改、改编、创作衍生作品)。
    须遵守署名(BY):必须给出适当的信用,提供指向本许可协议的链接,并指出是否进行了修改。转载请注明出处及原文链接。禁止整站搬运、禁止去除作者信息、禁止用于自媒体洗稿盈利。
    非商业性使用(NC):您不得将本作品用于商业目的。
    相同方式共享(SA):如果您再混合、转换或者基于本作品进行创作,您必须基于与原作品相同的许可协议分发您的贡献。

    商业授权:如需商业用途(包括但不限于出版、付费课程、企业内训等),请事先联系作者获取书面授权。任何形式的商用、二次分发、改编后商用均需授权。本网站所有技术分享严格遵守《中华人民共和国网络安全法》《数据安全法》《个人信息保护法》及相关法律法规。
    用途限制:站内涉及渗透测试、漏洞分析、逆向工程、脚本工具等技术内容,仅限用于合法的教育、学术研究及获得明确授权的安全测试场景。
    禁止行为:任何个人或组织不得利用本站提供的技术信息、代码或工具从事危害网络安全、侵入他人系统、干扰网络正常功能及防护措施等非法活动。
    责任自负:使用者应严格遵守法律法规,因滥用本站内容而导致的任何法律纠纷、经济损失或刑事责任,均由使用者自行承担,与本站及作者无关。作者不对任何因使用本站内容而产生的后果负责。

    技术具有时效性,本站部分内容可能随时间推移而过时或不准确。作者尽力保证内容的正确性,但不对其准确性、完整性、适用性或及时性做任何明示或暗示的保证。读者在参考本站内容进行实际操作(如修改配置、运行代码)前,请务必在测试环境中验证,并做好数据备份。尊重知识产权,保护网络安全,本站不提供任何系统、网站、设备的渗透目标,所有技术仅用于学习与防护。若您认为本站内容侵犯了您的合法权益,或发现本站存在违法违规内容,请通过左侧电子邮箱联系作者,作者将尽快核实并处理。

    本声明自2012年5月5日起生效