@apocaliss92/scrypted-reolink-native 0.1.1 → 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,135 +0,0 @@
1
- #!/usr/bin/env python3
2
- """Simple PCAP analyzer to compare neolink vs scrypted UDP flows"""
3
- import sys
4
- import struct
5
-
6
- def read_pcap_header(f):
7
- """Read PCAP file header (24 bytes)"""
8
- magic = f.read(4)
9
- if magic != b'\xd4\xc3\xb2\xa1': # pcap magic number (little endian)
10
- return None
11
- version_major, version_minor = struct.unpack('<HH', f.read(4))
12
- thiszone, sigfigs = struct.unpack('<II', f.read(8))
13
- snaplen, network = struct.unpack('<II', f.read(8))
14
- return {
15
- 'version_major': version_major,
16
- 'version_minor': version_minor,
17
- 'snaplen': snaplen,
18
- 'network': network
19
- }
20
-
21
- def read_packet_header(f):
22
- """Read PCAP packet header (16 bytes)"""
23
- data = f.read(16)
24
- if len(data) < 16:
25
- return None
26
- ts_sec, ts_usec, incl_len, orig_len = struct.unpack('<IIII', data)
27
- return {
28
- 'ts_sec': ts_sec,
29
- 'ts_usec': ts_usec,
30
- 'incl_len': incl_len,
31
- 'orig_len': orig_len
32
- }
33
-
34
- def analyze_pcap(filename):
35
- """Basic analysis of UDP packets in PCAP"""
36
- with open(filename, 'rb') as f:
37
- header = read_pcap_header(f)
38
- if not header:
39
- print(f"Error: {filename} is not a valid PCAP file")
40
- return
41
-
42
- packets = []
43
- packet_num = 0
44
-
45
- while True:
46
- pkt_header = read_packet_header(f)
47
- if not pkt_header:
48
- break
49
-
50
- packet_data = f.read(pkt_header['incl_len'])
51
- if len(packet_data) < pkt_header['incl_len']:
52
- break
53
-
54
- # Check if this is an Ethernet frame with IPv4 UDP
55
- if len(packet_data) >= 42: # Min Ethernet + IP + UDP headers
56
- # Skip Ethernet header (14 bytes), check IP
57
- ip_header = packet_data[14:34]
58
- if ip_header[0] == 0x45: # IPv4
59
- protocol = ip_header[9]
60
- if protocol == 17: # UDP
61
- src_port = struct.unpack('>H', ip_header[20:22])[0]
62
- dst_port = struct.unpack('>H', ip_header[22:24])[0]
63
- udp_data = packet_data[42:]
64
-
65
- # Check for BCUDP magic numbers
66
- if len(udp_data) >= 4:
67
- magic = udp_data[:4]
68
- if magic == b'\x3a\xcf\x87\x2a': # Discovery
69
- packets.append({
70
- 'num': packet_num,
71
- 'ts': pkt_header['ts_sec'] + pkt_header['ts_usec'] / 1000000,
72
- 'src_port': src_port,
73
- 'dst_port': dst_port,
74
- 'type': 'discovery',
75
- 'data': udp_data[:100] # First 100 bytes
76
- })
77
- elif magic == b'\x20\xcf\x87\x2a': # ACK
78
- packets.append({
79
- 'num': packet_num,
80
- 'ts': pkt_header['ts_sec'] + pkt_header['ts_usec'] / 1000000,
81
- 'src_port': src_port,
82
- 'dst_port': dst_port,
83
- 'type': 'ack',
84
- 'data': udp_data[:50]
85
- })
86
- elif magic == b'\x10\xcf\x87\x2a': # DATA
87
- packets.append({
88
- 'num': packet_num,
89
- 'ts': pkt_header['ts_sec'] + pkt_header['ts_usec'] / 1000000,
90
- 'src_port': src_port,
91
- 'dst_port': dst_port,
92
- 'type': 'data',
93
- 'data': udp_data[:50]
94
- })
95
-
96
- packet_num += 1
97
-
98
- print(f"\n=== {filename} ===")
99
- print(f"Total packets analyzed: {packet_num}")
100
- print(f"BCUDP packets found: {len(packets)}")
101
-
102
- # Group by type
103
- by_type = {}
104
- for pkt in packets:
105
- by_type.setdefault(pkt['type'], []).append(pkt)
106
-
107
- for pkt_type, pkts in by_type.items():
108
- print(f"\n{pkt_type.upper()}: {len(pkts)} packets")
109
- if pkts:
110
- print(f" First at {pkts[0]['ts']:.3f}s, last at {pkts[-1]['ts']:.3f}s")
111
- print(f" Ports: src={set(p['src_port'] for p in pkts)}, dst={set(p['dst_port'] for p in pkts)}")
112
-
113
- # Show first few discovery packets in detail
114
- discovery_pkts = [p for p in packets if p['type'] == 'discovery']
115
- if discovery_pkts:
116
- print(f"\nFirst 5 discovery packets:")
117
- for pkt in discovery_pkts[:5]:
118
- print(f" #{pkt['num']} at {pkt['ts']:.3f}s: {pkt['src_port']} -> {pkt['dst_port']}")
119
- # Look for XML-like content
120
- data_str = pkt['data'].decode('latin1', errors='ignore')
121
- if 'C2D' in data_str or 'D2C' in data_str:
122
- # Try to extract XML tag
123
- start = data_str.find('<')
124
- if start >= 0:
125
- end = data_str.find('>', start)
126
- if end > start:
127
- tag = data_str[start:end+1]
128
- print(f" Tag: {tag}")
129
-
130
- return packets
131
-
132
- if __name__ == '__main__':
133
- neolink_packets = analyze_pcap('neolink.pcapng')
134
- print("\n" + "="*60)
135
- scrypted_packets = analyze_pcap('scrypted.pcapng')
@@ -1,274 +0,0 @@
1
- #!/usr/bin/env python3
2
- """
3
- Compare neolink.pcapng vs scrypted.pcapng to find differences in UDP flow.
4
- This script parses pcapng files and extracts BCUDP packets for comparison.
5
- """
6
- import struct
7
- import sys
8
- from typing import List, Dict, Any
9
-
10
- BCUDP_MAGIC_DISCOVERY = 0x3acf872a
11
- BCUDP_MAGIC_DATA = 0x10cf872a
12
- BCUDP_MAGIC_ACK = 0x20cf872a
13
-
14
- def read_pcapng_block(f) -> tuple[bytes, int] | None:
15
- """Read a pcapng block. Returns (block_type, block_data) or None if EOF."""
16
- # Read block type (4 bytes)
17
- block_type_data = f.read(4)
18
- if len(block_type_data) < 4:
19
- return None
20
- block_type = struct.unpack('<I', block_type_data)[0]
21
-
22
- # Read block length (4 bytes)
23
- block_length_data = f.read(4)
24
- if len(block_length_data) < 4:
25
- return None
26
- block_length = struct.unpack('<I', block_length_data)[0]
27
-
28
- if block_length < 12: # Minimum block size (type + len + len)
29
- return None
30
-
31
- # Read block body (length - 8, since we already read type and length)
32
- block_body = f.read(block_length - 8)
33
- if len(block_body) < block_length - 8:
34
- return None
35
-
36
- # Read trailing length (should match)
37
- trailing_length_data = f.read(4)
38
- if len(trailing_length_data) < 4:
39
- return None
40
- trailing_length = struct.unpack('<I', trailing_length_data)[0]
41
-
42
- if trailing_length != block_length:
43
- print(f"Warning: block length mismatch: {block_length} != {trailing_length}")
44
-
45
- return (block_type, block_body)
46
-
47
- def parse_enhanced_packet_block(block_body: bytes) -> Dict[str, Any] | None:
48
- """Parse an Enhanced Packet Block (type 6) from pcapng."""
49
- if len(block_body) < 16:
50
- return None
51
-
52
- # Read interface ID, timestamp, and packet data length
53
- interface_id = struct.unpack('<I', block_body[0:4])[0]
54
- timestamp_high = struct.unpack('<I', block_body[4:8])[0]
55
- timestamp_low = struct.unpack('<I', block_body[8:12])[0]
56
- captured_len = struct.unpack('<I', block_body[12:16])[0]
57
-
58
- # Packet data starts at offset 16
59
- packet_data = block_body[16:16+captured_len]
60
-
61
- return {
62
- 'interface_id': interface_id,
63
- 'timestamp': (timestamp_high << 32) | timestamp_low,
64
- 'packet_data': packet_data
65
- }
66
-
67
- def extract_udp_packet(packet_data: bytes) -> Dict[str, Any] | None:
68
- """Extract UDP packet from Ethernet frame. Returns None if not UDP."""
69
- # Skip Ethernet header (14 bytes)
70
- if len(packet_data) < 42: # Min: 14 (Ethernet) + 20 (IP) + 8 (UDP)
71
- return None
72
-
73
- # Check for IPv4 (0x45 at offset 14)
74
- if packet_data[14] != 0x45:
75
- return None
76
-
77
- # Extract IP header fields
78
- ip_header = packet_data[14:34]
79
- src_ip = '.'.join(str(b) for b in ip_header[12:16])
80
- dst_ip = '.'.join(str(b) for b in ip_header[16:20])
81
- protocol = ip_header[9]
82
-
83
- if protocol != 17: # UDP
84
- return None
85
-
86
- # Extract UDP header
87
- udp_header = packet_data[34:42]
88
- src_port = struct.unpack('>H', udp_header[0:2])[0]
89
- dst_port = struct.unpack('>H', udp_header[2:4])[0]
90
- udp_length = struct.unpack('>H', udp_header[4:6])[0]
91
-
92
- # UDP payload
93
- udp_payload = packet_data[42:42+udp_length-8]
94
-
95
- return {
96
- 'src_ip': src_ip,
97
- 'dst_ip': dst_ip,
98
- 'src_port': src_port,
99
- 'dst_port': dst_port,
100
- 'payload': udp_payload
101
- }
102
-
103
- def parse_bcudp_packet(udp_payload: bytes) -> Dict[str, Any] | None:
104
- """Parse BCUDP packet from UDP payload."""
105
- if len(udp_payload) < 4:
106
- return None
107
-
108
- magic = struct.unpack('<I', udp_payload[0:4])[0]
109
-
110
- if magic == BCUDP_MAGIC_DISCOVERY:
111
- return {
112
- 'type': 'discovery',
113
- 'magic': hex(magic),
114
- 'payload': udp_payload
115
- }
116
- elif magic == BCUDP_MAGIC_DATA:
117
- if len(udp_payload) < 20:
118
- return None
119
- connection_id = struct.unpack('<i', udp_payload[4:8])[0]
120
- packet_id = struct.unpack('<I', udp_payload[12:16])[0]
121
- payload_len = struct.unpack('<I', udp_payload[16:20])[0]
122
- return {
123
- 'type': 'data',
124
- 'magic': hex(magic),
125
- 'connection_id': connection_id,
126
- 'packet_id': packet_id,
127
- 'payload_len': payload_len,
128
- 'payload': udp_payload
129
- }
130
- elif magic == BCUDP_MAGIC_ACK:
131
- if len(udp_payload) < 28:
132
- return None
133
- connection_id = struct.unpack('<i', udp_payload[4:8])[0]
134
- group_id = struct.unpack('<I', udp_payload[12:16])[0]
135
- packet_id = struct.unpack('<I', udp_payload[16:20])[0]
136
- return {
137
- 'type': 'ack',
138
- 'magic': hex(magic),
139
- 'connection_id': connection_id,
140
- 'group_id': group_id,
141
- 'packet_id': packet_id,
142
- 'payload': udp_payload
143
- }
144
-
145
- return None
146
-
147
- def analyze_pcapng(filename: str) -> List[Dict[str, Any]]:
148
- """Analyze pcapng file and extract BCUDP packets."""
149
- packets = []
150
-
151
- with open(filename, 'rb') as f:
152
- # Read Section Header Block (first block, type 0x0A0D0D0A)
153
- first_block = read_pcapng_block(f)
154
- if not first_block or first_block[0] != 0x0A0D0D0A:
155
- print(f"Error: {filename} is not a valid pcapng file")
156
- return packets
157
-
158
- packet_num = 0
159
- while True:
160
- block = read_pcapng_block(f)
161
- if not block:
162
- break
163
-
164
- block_type, block_body = block
165
-
166
- # Enhanced Packet Block (type 6)
167
- if block_type == 6:
168
- epb = parse_enhanced_packet_block(block_body)
169
- if epb:
170
- udp_pkt = extract_udp_packet(epb['packet_data'])
171
- if udp_pkt:
172
- bcudp_pkt = parse_bcudp_packet(udp_pkt['payload'])
173
- if bcudp_pkt:
174
- packets.append({
175
- 'num': packet_num,
176
- 'timestamp': epb['timestamp'],
177
- 'src_ip': udp_pkt['src_ip'],
178
- 'dst_ip': udp_pkt['dst_ip'],
179
- 'src_port': udp_pkt['src_port'],
180
- 'dst_port': udp_pkt['dst_port'],
181
- 'bcudp': bcudp_pkt,
182
- 'raw_payload': udp_pkt['payload'].hex()[:100] # First 50 bytes hex
183
- })
184
- packet_num += 1
185
-
186
- return packets
187
-
188
- def print_packet_summary(packets: List[Dict[str, Any]], name: str):
189
- """Print summary of packets."""
190
- print(f"\n{'='*80}")
191
- print(f"{name}: {len(packets)} BCUDP packets")
192
- print(f"{'='*80}")
193
-
194
- for i, pkt in enumerate(packets[:50]): # Show first 50 packets
195
- bcudp = pkt['bcudp']
196
- direction = '→' if '192.168' in pkt['dst_ip'] else '←'
197
- pkt_type = bcudp['type'].upper()
198
-
199
- info_parts = [f"#{pkt['num']}", direction, pkt_type]
200
-
201
- if pkt_type == 'DATA':
202
- info_parts.append(f"conn={bcudp.get('connection_id', '?')}")
203
- info_parts.append(f"pid={bcudp.get('packet_id', '?')}")
204
- info_parts.append(f"len={bcudp.get('payload_len', '?')}")
205
- elif pkt_type == 'ACK':
206
- info_parts.append(f"conn={bcudp.get('connection_id', '?')}")
207
- info_parts.append(f"pid={bcudp.get('packet_id', '?')}")
208
- info_parts.append(f"gid={bcudp.get('group_id', '?')}")
209
-
210
- info_parts.append(f"{pkt['src_port']}→{pkt['dst_port']}")
211
-
212
- print(f"{' '.join(info_parts)}")
213
- if i < 10: # Show hex for first 10 packets
214
- print(f" Hex: {pkt['raw_payload']}...")
215
-
216
- if len(packets) > 50:
217
- print(f"... and {len(packets) - 50} more packets")
218
-
219
- def compare_flows(neolink_packets: List[Dict[str, Any]], scrypted_packets: List[Dict[str, Any]]):
220
- """Compare the two flows and highlight differences."""
221
- print(f"\n{'='*80}")
222
- print("COMPARISON")
223
- print(f"{'='*80}")
224
-
225
- print(f"\nNeolink: {len(neolink_packets)} BCUDP packets")
226
- print(f"Scrypted: {len(scrypted_packets)} BCUDP packets")
227
-
228
- # Find first DATA packets
229
- neolink_first_data = next((p for p in neolink_packets if p['bcudp']['type'] == 'data'), None)
230
- scrypted_first_data = next((p for p in scrypted_packets if p['bcudp']['type'] == 'data'), None)
231
-
232
- if neolink_first_data and scrypted_first_data:
233
- print(f"\nFirst DATA packet:")
234
- print(f" Neolink: conn={neolink_first_data['bcudp'].get('connection_id')}, pid={neolink_first_data['bcudp'].get('packet_id')}, {neolink_first_data['src_port']}→{neolink_first_data['dst_port']}")
235
- print(f" Scrypted: conn={scrypted_first_data['bcudp'].get('connection_id')}, pid={scrypted_first_data['bcudp'].get('packet_id')}, {scrypted_first_data['src_port']}→{scrypted_first_data['dst_port']}")
236
- print(f" Hex Neolink: {neolink_first_data['raw_payload']}")
237
- print(f" Hex Scrypted: {scrypted_first_data['raw_payload']}")
238
-
239
- if neolink_first_data['raw_payload'] != scrypted_first_data['raw_payload']:
240
- print(f" ⚠️ DIFFERENT HEX!")
241
- else:
242
- print(f" ✓ Same hex")
243
-
244
- # Count DATA/ACK packets after discovery
245
- neolink_data_count = sum(1 for p in neolink_packets if p['bcudp']['type'] == 'data')
246
- neolink_ack_count = sum(1 for p in neolink_packets if p['bcudp']['type'] == 'ack')
247
- scrypted_data_count = sum(1 for p in scrypted_packets if p['bcudp']['type'] == 'data')
248
- scrypted_ack_count = sum(1 for p in scrypted_packets if p['bcudp']['type'] == 'ack')
249
-
250
- print(f"\nPacket counts:")
251
- print(f" Neolink: {neolink_data_count} DATA, {neolink_ack_count} ACK")
252
- print(f" Scrypted: {scrypted_data_count} DATA, {scrypted_ack_count} ACK")
253
-
254
- def main():
255
- if len(sys.argv) < 3:
256
- print("Usage: python3 compare_pcaps.py <neolink.pcapng> <scrypted.pcapng>")
257
- sys.exit(1)
258
-
259
- neolink_file = sys.argv[1]
260
- scrypted_file = sys.argv[2]
261
-
262
- print("Analyzing neolink.pcapng...")
263
- neolink_packets = analyze_pcapng(neolink_file)
264
-
265
- print("Analyzing scrypted.pcapng...")
266
- scrypted_packets = analyze_pcapng(scrypted_file)
267
-
268
- print_packet_summary(neolink_packets, "NEOLINK")
269
- print_packet_summary(scrypted_packets, "SCRYPTED")
270
- compare_flows(neolink_packets, scrypted_packets)
271
-
272
- if __name__ == '__main__':
273
- main()
274
-
@@ -1,222 +0,0 @@
1
- #!/usr/bin/env python3
2
- """
3
- Compare stream flow between neolink and scrypted after discovery/nonce request.
4
- Focus on DATA/ACK packets to identify why stream might stall.
5
- """
6
- import json
7
- import sys
8
- import struct
9
-
10
- BCUDP_MAGIC_DATA = 0x10cf872a
11
- BCUDP_MAGIC_ACK = 0x20cf872a
12
-
13
- def hex_to_bytes(hex_str: str) -> bytes:
14
- try:
15
- return bytes.fromhex(hex_str.replace(':', '').replace(' ', ''))
16
- except:
17
- return b''
18
-
19
- def parse_bcudp_data(data: bytes):
20
- """Parse BCUDP DATA packet."""
21
- if len(data) < 20:
22
- return None
23
- magic = struct.unpack('>I', data[0:4])[0]
24
- if magic != BCUDP_MAGIC_DATA:
25
- return None
26
- connection_id = int.from_bytes(data[4:8], 'little', signed=True)
27
- packet_id = int.from_bytes(data[12:16], 'little')
28
- payload_len = int.from_bytes(data[16:20], 'little')
29
- return {'type': 'data', 'connection_id': connection_id, 'packet_id': packet_id, 'payload_len': payload_len}
30
-
31
- def parse_bcudp_ack(data: bytes):
32
- """Parse BCUDP ACK packet."""
33
- if len(data) < 28:
34
- return None
35
- magic = struct.unpack('>I', data[0:4])[0]
36
- if magic != BCUDP_MAGIC_ACK:
37
- return None
38
- connection_id = int.from_bytes(data[4:8], 'little', signed=True)
39
- group_id = int.from_bytes(data[12:16], 'little')
40
- packet_id = int.from_bytes(data[16:20], 'little')
41
- return {'type': 'ack', 'connection_id': connection_id, 'group_id': group_id, 'packet_id': packet_id}
42
-
43
- def extract_stream_packets(json_file: str):
44
- """Extract DATA and ACK packets from stream."""
45
- packets = []
46
-
47
- with open(json_file, 'r') as f:
48
- data = json.load(f)
49
-
50
- for entry in data:
51
- source = entry.get('_source', {})
52
- layers = source.get('layers', {})
53
- frame = layers.get('frame', {})
54
- frame_num = int(frame.get('frame.number', '0'))
55
- frame_time = float(frame.get('frame.time_relative', '0'))
56
- udp = layers.get('udp', {})
57
- if not udp:
58
- continue
59
- src_port = int(udp.get('udp.srcport', '0'))
60
- dst_port = int(udp.get('udp.dstport', '0'))
61
- ip = layers.get('ip', {})
62
- src_ip = ip.get('ip.src', '')
63
- payload_hex = udp.get('udp.payload', '')
64
- if not payload_hex:
65
- continue
66
-
67
- bytes_data = hex_to_bytes(payload_hex)
68
- if len(bytes_data) < 4:
69
- continue
70
-
71
- # Try to parse as DATA or ACK
72
- pkt_data = parse_bcudp_data(bytes_data)
73
- if pkt_data:
74
- packets.append({
75
- 'frame_num': frame_num,
76
- 'time': frame_time,
77
- 'src_ip': src_ip,
78
- 'src_port': src_port,
79
- 'dst_port': dst_port,
80
- **pkt_data
81
- })
82
- continue
83
-
84
- pkt_ack = parse_bcudp_ack(bytes_data)
85
- if pkt_ack:
86
- packets.append({
87
- 'frame_num': frame_num,
88
- 'time': frame_time,
89
- 'src_ip': src_ip,
90
- 'src_port': src_port,
91
- 'dst_port': dst_port,
92
- **pkt_ack
93
- })
94
-
95
- return packets
96
-
97
- def analyze_stream_flow(packets, name: str):
98
- """Analyze stream flow statistics."""
99
- print(f"\n{'='*100}")
100
- print(f"{name}: Stream Flow Analysis")
101
- print(f"{'='*100}")
102
-
103
- data_packets = [p for p in packets if p['type'] == 'data']
104
- ack_packets = [p for p in packets if p['type'] == 'ack']
105
-
106
- print(f"\nTotal packets: {len(packets)} (DATA: {len(data_packets)}, ACK: {len(ack_packets)})")
107
-
108
- if not data_packets:
109
- print(" No DATA packets found!")
110
- return
111
-
112
- # Find direction: camera->client (DATA) vs client->camera (ACK)
113
- # Assuming camera IP is 192.168.x.x
114
- camera_data = [p for p in data_packets if '192.168' in p['src_ip']]
115
- client_data = [p for p in data_packets if '192.168' not in p['src_ip']]
116
-
117
- camera_ack = [p for p in ack_packets if '192.168' in p['src_ip']]
118
- client_ack = [p for p in ack_packets if '192.168' not in p['src_ip']]
119
-
120
- print(f"\nDirection breakdown:")
121
- print(f" Camera→Client DATA: {len(camera_data)}")
122
- print(f" Client→Camera DATA: {len(client_data)}")
123
- print(f" Camera→Client ACK: {len(camera_ack)}")
124
- print(f" Client→Camera ACK: {len(client_ack)}")
125
-
126
- if camera_data:
127
- print(f"\nCamera→Client DATA packets:")
128
- print(f" First: frame #{camera_data[0]['frame_num']}, time {camera_data[0]['time']:.3f}s, pid={camera_data[0]['packet_id']}")
129
- print(f" Last: frame #{camera_data[-1]['frame_num']}, time {camera_data[-1]['time']:.3f}s, pid={camera_data[-1]['packet_id']}")
130
- print(f" Duration: {camera_data[-1]['time'] - camera_data[0]['time']:.3f}s")
131
- print(f" Packet ID range: {camera_data[0]['packet_id']} → {camera_data[-1]['packet_id']}")
132
-
133
- # Check for gaps
134
- pids = sorted([p['packet_id'] for p in camera_data])
135
- gaps = []
136
- for i in range(len(pids) - 1):
137
- gap = pids[i+1] - pids[i]
138
- if gap > 1:
139
- gaps.append((pids[i], pids[i+1], gap))
140
- if gaps:
141
- print(f" ⚠️ Found {len(gaps)} gaps in packet IDs:")
142
- for start, end, gap in gaps[:10]:
143
- print(f" Missing: {start+1} to {end-1} (gap={gap-1})")
144
- else:
145
- print(f" ✓ No gaps in packet IDs")
146
-
147
- # Check packet rate
148
- if len(camera_data) > 1:
149
- duration = camera_data[-1]['time'] - camera_data[0]['time']
150
- rate = len(camera_data) / duration if duration > 0 else 0
151
- print(f" Packet rate: {rate:.2f} packets/sec")
152
-
153
- # Check ACK frequency from client
154
- if client_ack:
155
- print(f"\nClient→Camera ACK packets:")
156
- print(f" Total: {len(client_ack)}")
157
- if len(client_ack) > 1:
158
- duration = client_ack[-1]['time'] - client_ack[0]['time']
159
- rate = len(client_ack) / duration if duration > 0 else 0
160
- print(f" ACK rate: {rate:.2f} ACKs/sec")
161
- print(f" First ACK: time {client_ack[0]['time']:.3f}s, pid={client_ack[0]['packet_id']}")
162
- print(f" Last ACK: time {client_ack[-1]['time']:.3f}s, pid={client_ack[-1]['packet_id']}")
163
-
164
- def compare_streams(nl_packets, sc_packets):
165
- """Compare stream flows."""
166
- print(f"\n{'='*100}")
167
- print("COMPARISON")
168
- print(f"{'='*100}")
169
-
170
- nl_camera_data = [p for p in nl_packets if p['type'] == 'data' and '192.168' in p['src_ip']]
171
- sc_camera_data = [p for p in sc_packets if p['type'] == 'data' and '192.168' in p['src_ip']]
172
-
173
- nl_client_ack = [p for p in nl_packets if p['type'] == 'ack' and '192.168' not in p['src_ip']]
174
- sc_client_ack = [p for p in sc_packets if p['type'] == 'ack' and '192.168' not in p['src_ip']]
175
-
176
- print(f"\nCamera→Client DATA packets:")
177
- print(f" Neolink: {len(nl_camera_data)} packets")
178
- print(f" Scrypted: {len(sc_camera_data)} packets")
179
- print(f" Ratio: {len(sc_camera_data) / len(nl_camera_data) * 100 if nl_camera_data else 0:.1f}%")
180
-
181
- print(f"\nClient→Camera ACK packets:")
182
- print(f" Neolink: {len(nl_client_ack)} packets")
183
- print(f" Scrypted: {len(sc_client_ack)} packets")
184
- print(f" Ratio: {len(sc_client_ack) / len(nl_client_ack) * 100 if nl_client_ack else 0:.1f}%")
185
-
186
- if nl_camera_data and sc_camera_data:
187
- nl_duration = nl_camera_data[-1]['time'] - nl_camera_data[0]['time']
188
- sc_duration = sc_camera_data[-1]['time'] - sc_camera_data[0]['time']
189
- print(f"\nStream duration:")
190
- print(f" Neolink: {nl_duration:.2f}s")
191
- print(f" Scrypted: {sc_duration:.2f}s")
192
-
193
- nl_rate = len(nl_camera_data) / nl_duration if nl_duration > 0 else 0
194
- sc_rate = len(sc_camera_data) / sc_duration if sc_duration > 0 else 0
195
- print(f"\nPacket rate:")
196
- print(f" Neolink: {nl_rate:.2f} packets/sec")
197
- print(f" Scrypted: {sc_rate:.2f} packets/sec")
198
-
199
- if sc_rate < nl_rate * 0.5:
200
- print(f" ⚠️ Scrypted has much lower packet rate!")
201
-
202
- def main():
203
- if len(sys.argv) < 3:
204
- print("Usage: python3 compare_stream_flow.py <neolink.json> <scrypted.json>")
205
- sys.exit(1)
206
-
207
- neolink_file = sys.argv[1]
208
- scrypted_file = sys.argv[2]
209
-
210
- print("Loading neolink.json...")
211
- nl_packets = extract_stream_packets(neolink_file)
212
-
213
- print("Loading scrypted.json...")
214
- sc_packets = extract_stream_packets(scrypted_file)
215
-
216
- analyze_stream_flow(nl_packets, "NEOLINK")
217
- analyze_stream_flow(sc_packets, "SCRYPTED")
218
- compare_streams(nl_packets, sc_packets)
219
-
220
- if __name__ == '__main__':
221
- main()
222
-