Skip to content

Commit 35f9895

Browse files
committed
现在可以在媒体库页面添加nipaplay远程访问。现在远程访问可以扫描局域网直接发现。修复了C键发送弹幕不会暂停视频的bug。2025.1220
1 parent 6df9cce commit 35f9895

11 files changed

+1203
-238
lines changed

Documentation/server-integration.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515

1616
**设置步骤**
1717
1. **PC端开启共享**:在设置 → 远程访问 → 启用web服务器
18-
2. **移动端连接**输入PC端的局域网地址(如:192.168.1.100:8080
18+
2. **移动端连接**在“共享媒体库”中点击“扫描局域网”选择设备;也可手动输入PC端局域网地址(如:192.168.1.100,默认端口 1180
1919
3. **开始观看**:媒体库中会显示PC端的视频,可直接播放
2020

2121
**优势**

lib/pages/media_library_page.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -366,7 +366,7 @@ class _MediaLibraryPageState extends State<MediaLibraryPage> {
366366
LoginField(
367367
key: 'baseUrl',
368368
label: '访问地址',
369-
hint: '例如:http://192.168.1.100:1180',
369+
hint: '例如:192.168.1.100(默认1180)或 192.168.1.100:2345',
370370
),
371371
],
372372
loginButtonText: '添加',

lib/providers/shared_remote_library_provider.dart

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -550,12 +550,35 @@ class SharedRemoteLibraryProvider extends ChangeNotifier {
550550

551551
String _normalizeBaseUrl(String url) {
552552
var normalized = url.trim();
553-
if (!normalized.startsWith('http://') && !normalized.startsWith('https://')) {
553+
if (normalized.isEmpty) return normalized;
554+
555+
final hasScheme = normalized.contains('://');
556+
if (!hasScheme) {
554557
normalized = 'http://$normalized';
555558
}
559+
560+
// 先去掉末尾斜杠,避免 Uri.parse 解析 path 导致的歧义。
556561
if (normalized.endsWith('/')) {
557562
normalized = normalized.substring(0, normalized.length - 1);
558563
}
564+
565+
try {
566+
final uri = Uri.parse(normalized);
567+
568+
// 用户未显式指定端口时:若是局域网/本机地址,则默认走 NipaPlay 远程访问默认端口 1180。
569+
if (!uri.hasPort &&
570+
uri.scheme == 'http' &&
571+
_shouldBypassProxy(uri.host)) {
572+
normalized = uri.replace(port: 1180).toString();
573+
}
574+
} catch (_) {
575+
// 若解析失败,保留原始输入(上层会在请求时给出错误提示)
576+
}
577+
578+
if (normalized.endsWith('/')) {
579+
normalized = normalized.substring(0, normalized.length - 1);
580+
}
581+
559582
return normalized;
560583
}
561584

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
import 'dart:convert';
2+
import 'dart:io';
3+
4+
import 'package:flutter/foundation.dart';
5+
6+
const int nipaplayLanDiscoveryPort = 32123;
7+
8+
class NipaPlayLanDiscoveredServer {
9+
const NipaPlayLanDiscoveredServer({
10+
required this.ip,
11+
required this.port,
12+
this.hostname,
13+
});
14+
15+
final String ip;
16+
final int port;
17+
final String? hostname;
18+
19+
String get baseUrl => 'http://$ip:$port';
20+
}
21+
22+
class NipaPlayLanDiscoveryProtocol {
23+
static const int protocolVersion = 1;
24+
25+
static const String requestType = 'nipaplay_discover';
26+
static const String responseType = 'nipaplay_discover_response';
27+
28+
static Uint8List buildRequestBytes() {
29+
return Uint8List.fromList(utf8.encode(json.encode(<String, dynamic>{
30+
'type': requestType,
31+
'v': protocolVersion,
32+
})));
33+
}
34+
35+
static Uint8List buildResponseBytes({required int webPort}) {
36+
return Uint8List.fromList(utf8.encode(json.encode(<String, dynamic>{
37+
'type': responseType,
38+
'v': protocolVersion,
39+
'app': 'NipaPlay',
40+
'hostname': Platform.localHostname,
41+
'os': Platform.operatingSystem,
42+
'port': webPort,
43+
})));
44+
}
45+
46+
static bool isValidRequestPayload(Map<String, dynamic> payload) {
47+
return payload['type'] == requestType && payload['v'] == protocolVersion;
48+
}
49+
50+
static NipaPlayLanDiscoveredServer? tryParseResponse(Datagram datagram) {
51+
final payload = _tryDecodeJson(datagram.data);
52+
if (payload == null) return null;
53+
if (payload['type'] != responseType) return null;
54+
if (payload['v'] != protocolVersion) return null;
55+
if (payload['app'] != 'NipaPlay') return null;
56+
57+
final rawPort = payload['port'];
58+
final port = rawPort is int ? rawPort : int.tryParse(rawPort?.toString() ?? '');
59+
if (port == null || port <= 0 || port > 65535) return null;
60+
61+
final hostname = payload['hostname'] is String ? payload['hostname'] as String : null;
62+
return NipaPlayLanDiscoveredServer(
63+
ip: datagram.address.address,
64+
port: port,
65+
hostname: hostname,
66+
);
67+
}
68+
69+
static Map<String, dynamic>? _tryDecodeJson(Uint8List data) {
70+
try {
71+
final decoded = json.decode(utf8.decode(data));
72+
return decoded is Map<String, dynamic> ? decoded : null;
73+
} catch (_) {
74+
return null;
75+
}
76+
}
77+
}
78+
79+
class NipaPlayLanDiscoveryResponder {
80+
RawDatagramSocket? _socket;
81+
int _webPort = 1180;
82+
83+
bool get isRunning => _socket != null;
84+
85+
Future<void> start({required int webPort}) async {
86+
_webPort = webPort;
87+
if (_socket != null) {
88+
return;
89+
}
90+
91+
try {
92+
final socket = await RawDatagramSocket.bind(
93+
InternetAddress.anyIPv4,
94+
nipaplayLanDiscoveryPort,
95+
reuseAddress: true,
96+
);
97+
98+
socket.listen((event) {
99+
if (event != RawSocketEvent.read) return;
100+
Datagram? datagram;
101+
while ((datagram = socket.receive()) != null) {
102+
final payload = _tryDecodeJson(datagram!.data);
103+
if (payload == null) continue;
104+
if (!NipaPlayLanDiscoveryProtocol.isValidRequestPayload(payload)) continue;
105+
106+
final response = NipaPlayLanDiscoveryProtocol.buildResponseBytes(webPort: _webPort);
107+
try {
108+
socket.send(response, datagram.address, datagram.port);
109+
} catch (_) {
110+
// ignore send failures
111+
}
112+
}
113+
});
114+
115+
_socket = socket;
116+
debugPrint('NipaPlayLanDiscoveryResponder: started on UDP $nipaplayLanDiscoveryPort');
117+
} catch (e) {
118+
debugPrint('NipaPlayLanDiscoveryResponder: start failed: $e');
119+
}
120+
}
121+
122+
Future<void> stop() async {
123+
try {
124+
_socket?.close();
125+
} catch (_) {
126+
// ignore
127+
} finally {
128+
_socket = null;
129+
}
130+
}
131+
132+
static Map<String, dynamic>? _tryDecodeJson(Uint8List data) {
133+
try {
134+
final decoded = json.decode(utf8.decode(data));
135+
return decoded is Map<String, dynamic> ? decoded : null;
136+
} catch (_) {
137+
return null;
138+
}
139+
}
140+
}

0 commit comments

Comments
 (0)