Add UDP traffic analysis tools for GStreamer video debugging
This commit is contained in:
parent
3a799c0a65
commit
bdb89b2632
@ -46,6 +46,57 @@ uv run .\scripts\recv_raw_rolling.py --no-display
|
|||||||
|
|
||||||
See [`scripts/recv_raw_rolling.py`](scripts/recv_raw_rolling.py) for the Python implementation with debug options.
|
See [`scripts/recv_raw_rolling.py`](scripts/recv_raw_rolling.py) for the Python implementation with debug options.
|
||||||
|
|
||||||
|
### UDP Traffic Analysis & Debugging
|
||||||
|
|
||||||
|
To inspect and analyze the raw UDP packets being transmitted:
|
||||||
|
|
||||||
|
```pwsh
|
||||||
|
# Detailed payload analyzer - shows format, dimensions, pixel statistics
|
||||||
|
uv run .\scripts\udp_payload_analyzer.py
|
||||||
|
```
|
||||||
|
|
||||||
|
**Example Output:**
|
||||||
|
```
|
||||||
|
================================================================================
|
||||||
|
PACKET #1 @ 17:45:23.456
|
||||||
|
================================================================================
|
||||||
|
Source: 127.0.0.1:52341
|
||||||
|
Total Size: 7368 bytes
|
||||||
|
|
||||||
|
PROTOCOL ANALYSIS:
|
||||||
|
--------------------------------------------------------------------------------
|
||||||
|
protocol : RAW
|
||||||
|
header_size : 0
|
||||||
|
payload_size : 7368
|
||||||
|
|
||||||
|
VIDEO PAYLOAD ANALYSIS:
|
||||||
|
--------------------------------------------------------------------------------
|
||||||
|
📹 Real camera data - Single line 2456x1 BGR
|
||||||
|
Format: BGR
|
||||||
|
Dimensions: 2456x1
|
||||||
|
Channels: 3
|
||||||
|
|
||||||
|
PIXEL STATISTICS:
|
||||||
|
--------------------------------------------------------------------------------
|
||||||
|
Channel 0 (B/R) : min= 0, max=110, mean= 28.63, std= 16.16
|
||||||
|
Channel 1 (G) : min= 17, max=233, mean= 62.39, std= 36.93
|
||||||
|
Channel 2 (R/B) : min= 25, max=255, mean= 99.76, std= 49.81
|
||||||
|
|
||||||
|
HEX PREVIEW (first 32 bytes):
|
||||||
|
--------------------------------------------------------------------------------
|
||||||
|
19 2e 4a 12 30 41 0a 2f 3f 01 32 3e 00 32 40 00 31 45 18 2d 4c 1e 2d...
|
||||||
|
|
||||||
|
SESSION SUMMARY:
|
||||||
|
Total Packets: 235
|
||||||
|
Total Bytes: 1,731,480 (7368 bytes/packet)
|
||||||
|
```
|
||||||
|
|
||||||
|
The analyzer automatically detects the format, shows pixel statistics per color channel, and provides a hex preview for debugging. Perfect for verifying data transmission and diagnosing issues.
|
||||||
|
|
||||||
|
```pwsh
|
||||||
|
# Simple packet receiver (no analysis, just basic info)
|
||||||
|
uv run .\scripts\udp_sniffer_raw.py
|
||||||
|
```
|
||||||
|
|
||||||
## Configuration Notes
|
## Configuration Notes
|
||||||
|
|
||||||
|
|||||||
265
scripts/udp_payload_analyzer.py
Normal file
265
scripts/udp_payload_analyzer.py
Normal file
@ -0,0 +1,265 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# /// script
|
||||||
|
# requires-python = ">=3.8"
|
||||||
|
# dependencies = [
|
||||||
|
# "numpy",
|
||||||
|
# ]
|
||||||
|
# ///
|
||||||
|
|
||||||
|
"""
|
||||||
|
UDP Payload Analyzer for GStreamer Raw Video
|
||||||
|
Analyzes UDP packets on port 5000 and reports on payload structure
|
||||||
|
|
||||||
|
Based on network_guide.md:
|
||||||
|
- Real data: 2456x1 BGR (7368 bytes per line)
|
||||||
|
- Demo data: 1x640 RGB (1920 bytes per frame)
|
||||||
|
|
||||||
|
Usage: uv run scripts/udp_payload_analyzer.py
|
||||||
|
"""
|
||||||
|
|
||||||
|
import socket
|
||||||
|
import sys
|
||||||
|
import struct
|
||||||
|
import numpy as np
|
||||||
|
from datetime import datetime
|
||||||
|
from collections import defaultdict
|
||||||
|
|
||||||
|
class PayloadAnalyzer:
|
||||||
|
def __init__(self):
|
||||||
|
self.packet_sizes = defaultdict(int)
|
||||||
|
self.total_packets = 0
|
||||||
|
self.total_bytes = 0
|
||||||
|
|
||||||
|
def analyze_gstreamer_header(self, data):
|
||||||
|
"""Try to detect and parse GStreamer RTP/UDP headers"""
|
||||||
|
info = {}
|
||||||
|
|
||||||
|
# Check if it's RTP (GStreamer sometimes uses RTP)
|
||||||
|
if len(data) >= 12:
|
||||||
|
# RTP header format
|
||||||
|
byte0 = data[0]
|
||||||
|
version = (byte0 >> 6) & 0x03
|
||||||
|
padding = (byte0 >> 5) & 0x01
|
||||||
|
extension = (byte0 >> 4) & 0x01
|
||||||
|
csrc_count = byte0 & 0x0F
|
||||||
|
|
||||||
|
if version == 2: # RTP version 2
|
||||||
|
info['protocol'] = 'RTP'
|
||||||
|
info['version'] = version
|
||||||
|
info['padding'] = bool(padding)
|
||||||
|
info['extension'] = bool(extension)
|
||||||
|
|
||||||
|
byte1 = data[1]
|
||||||
|
info['marker'] = bool(byte1 >> 7)
|
||||||
|
info['payload_type'] = byte1 & 0x7F
|
||||||
|
|
||||||
|
info['sequence'] = struct.unpack('!H', data[2:4])[0]
|
||||||
|
info['timestamp'] = struct.unpack('!I', data[4:8])[0]
|
||||||
|
info['ssrc'] = struct.unpack('!I', data[8:12])[0]
|
||||||
|
|
||||||
|
payload_offset = 12 + (csrc_count * 4)
|
||||||
|
info['header_size'] = payload_offset
|
||||||
|
info['payload_size'] = len(data) - payload_offset
|
||||||
|
|
||||||
|
return info, payload_offset
|
||||||
|
|
||||||
|
# Raw video data (no RTP)
|
||||||
|
info['protocol'] = 'RAW'
|
||||||
|
info['header_size'] = 0
|
||||||
|
info['payload_size'] = len(data)
|
||||||
|
return info, 0
|
||||||
|
|
||||||
|
def analyze_video_payload(self, data, offset=0):
|
||||||
|
"""Analyze raw video data"""
|
||||||
|
payload = data[offset:]
|
||||||
|
size = len(payload)
|
||||||
|
|
||||||
|
analysis = {
|
||||||
|
'size': size,
|
||||||
|
'format': 'unknown'
|
||||||
|
}
|
||||||
|
|
||||||
|
# Check for known video formats from network_guide.md
|
||||||
|
if size == 7368: # 2456 × 1 × 3 (BGR)
|
||||||
|
analysis['format'] = 'BGR'
|
||||||
|
analysis['width'] = 2456
|
||||||
|
analysis['height'] = 1
|
||||||
|
analysis['channels'] = 3
|
||||||
|
analysis['description'] = 'Real camera data - Single line 2456x1 BGR'
|
||||||
|
|
||||||
|
elif size == 1920: # 1 × 640 × 3 (RGB)
|
||||||
|
analysis['format'] = 'RGB'
|
||||||
|
analysis['width'] = 1
|
||||||
|
analysis['height'] = 640
|
||||||
|
analysis['channels'] = 3
|
||||||
|
analysis['description'] = 'Demo data - Single column 1x640 RGB'
|
||||||
|
|
||||||
|
else:
|
||||||
|
# Try to guess format
|
||||||
|
# Common raw video sizes
|
||||||
|
possible_formats = []
|
||||||
|
|
||||||
|
# Try BGR/RGB (3 channels)
|
||||||
|
if size % 3 == 0:
|
||||||
|
pixels = size // 3
|
||||||
|
possible_formats.append(f'{pixels} pixels @ 3 channels (BGR/RGB)')
|
||||||
|
|
||||||
|
# Try GRAY (1 channel)
|
||||||
|
possible_formats.append(f'{size} pixels @ 1 channel (GRAY)')
|
||||||
|
|
||||||
|
# Try RGBA (4 channels)
|
||||||
|
if size % 4 == 0:
|
||||||
|
pixels = size // 4
|
||||||
|
possible_formats.append(f'{pixels} pixels @ 4 channels (RGBA)')
|
||||||
|
|
||||||
|
analysis['possible_formats'] = possible_formats
|
||||||
|
|
||||||
|
# Pixel statistics (if manageable size)
|
||||||
|
if size <= 100000: # Only analyze if < 100KB
|
||||||
|
try:
|
||||||
|
if 'channels' in analysis and analysis['channels'] == 3:
|
||||||
|
# Reshape as color image
|
||||||
|
pixels = size // 3
|
||||||
|
arr = np.frombuffer(payload, dtype=np.uint8).reshape(-1, 3)
|
||||||
|
|
||||||
|
analysis['pixel_stats'] = {
|
||||||
|
'min': [int(arr[:, i].min()) for i in range(3)],
|
||||||
|
'max': [int(arr[:, i].max()) for i in range(3)],
|
||||||
|
'mean': [float(arr[:, i].mean()) for i in range(3)],
|
||||||
|
'std': [float(arr[:, i].std()) for i in range(3)]
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
# Treat as grayscale
|
||||||
|
arr = np.frombuffer(payload, dtype=np.uint8)
|
||||||
|
analysis['pixel_stats'] = {
|
||||||
|
'min': int(arr.min()),
|
||||||
|
'max': int(arr.max()),
|
||||||
|
'mean': float(arr.mean()),
|
||||||
|
'std': float(arr.std())
|
||||||
|
}
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# First 32 bytes in hex
|
||||||
|
hex_preview = ' '.join(f'{b:02x}' for b in payload[:32])
|
||||||
|
analysis['hex_preview'] = hex_preview + ('...' if size > 32 else '')
|
||||||
|
|
||||||
|
return analysis
|
||||||
|
|
||||||
|
def print_report(self, packet_num, addr, data):
|
||||||
|
"""Print detailed analysis report"""
|
||||||
|
self.total_packets += 1
|
||||||
|
self.total_bytes += len(data)
|
||||||
|
self.packet_sizes[len(data)] += 1
|
||||||
|
|
||||||
|
timestamp = datetime.now().strftime("%H:%M:%S.%f")[:-3]
|
||||||
|
|
||||||
|
print("=" * 80)
|
||||||
|
print(f"PACKET #{packet_num} @ {timestamp}")
|
||||||
|
print("=" * 80)
|
||||||
|
print(f"Source: {addr[0]}:{addr[1]}")
|
||||||
|
print(f"Total Size: {len(data)} bytes")
|
||||||
|
print()
|
||||||
|
|
||||||
|
# Analyze header
|
||||||
|
header_info, payload_offset = self.analyze_gstreamer_header(data)
|
||||||
|
|
||||||
|
print("PROTOCOL ANALYSIS:")
|
||||||
|
print("-" * 80)
|
||||||
|
for key, value in header_info.items():
|
||||||
|
print(f" {key:20s}: {value}")
|
||||||
|
print()
|
||||||
|
|
||||||
|
# Analyze video payload
|
||||||
|
video_info = self.analyze_video_payload(data, payload_offset)
|
||||||
|
|
||||||
|
print("VIDEO PAYLOAD ANALYSIS:")
|
||||||
|
print("-" * 80)
|
||||||
|
|
||||||
|
if 'description' in video_info:
|
||||||
|
print(f" 📹 {video_info['description']}")
|
||||||
|
print(f" Format: {video_info['format']}")
|
||||||
|
print(f" Dimensions: {video_info['width']}x{video_info['height']}")
|
||||||
|
print(f" Channels: {video_info['channels']}")
|
||||||
|
else:
|
||||||
|
print(f" Size: {video_info['size']} bytes")
|
||||||
|
if 'possible_formats' in video_info:
|
||||||
|
print(f" Possible formats:")
|
||||||
|
for fmt in video_info['possible_formats']:
|
||||||
|
print(f" - {fmt}")
|
||||||
|
|
||||||
|
print()
|
||||||
|
|
||||||
|
if 'pixel_stats' in video_info:
|
||||||
|
print("PIXEL STATISTICS:")
|
||||||
|
print("-" * 80)
|
||||||
|
stats = video_info['pixel_stats']
|
||||||
|
if isinstance(stats['min'], list):
|
||||||
|
# Color image (BGR/RGB)
|
||||||
|
channels = ['Channel 0 (B/R)', 'Channel 1 (G)', 'Channel 2 (R/B)']
|
||||||
|
for i, ch in enumerate(channels):
|
||||||
|
print(f" {ch:20s}: min={stats['min'][i]:3d}, max={stats['max'][i]:3d}, mean={stats['mean'][i]:6.2f}, std={stats['std'][i]:6.2f}")
|
||||||
|
else:
|
||||||
|
# Grayscale
|
||||||
|
print(f" Grayscale: min={stats['min']}, max={stats['max']}, mean={stats['mean']:.2f}, std={stats['std']:.2f}")
|
||||||
|
print()
|
||||||
|
|
||||||
|
print("HEX PREVIEW (first 32 bytes):")
|
||||||
|
print("-" * 80)
|
||||||
|
print(f" {video_info['hex_preview']}")
|
||||||
|
print()
|
||||||
|
|
||||||
|
def print_summary(self):
|
||||||
|
"""Print statistics summary"""
|
||||||
|
print("\n" + "=" * 80)
|
||||||
|
print("SESSION SUMMARY")
|
||||||
|
print("=" * 80)
|
||||||
|
print(f"Total Packets: {self.total_packets}")
|
||||||
|
print(f"Total Bytes: {self.total_bytes:,}")
|
||||||
|
print(f"Average Packet Size: {self.total_bytes / max(self.total_packets, 1):.2f} bytes")
|
||||||
|
print()
|
||||||
|
print("Packet Size Distribution:")
|
||||||
|
for size in sorted(self.packet_sizes.keys()):
|
||||||
|
count = self.packet_sizes[size]
|
||||||
|
print(f" {size:6d} bytes: {count:4d} packets")
|
||||||
|
print()
|
||||||
|
|
||||||
|
def main():
|
||||||
|
print("=" * 80)
|
||||||
|
print("UDP PAYLOAD ANALYZER - Port 5000, 127.0.0.1")
|
||||||
|
print("Specialized for GStreamer Raw Video Analysis")
|
||||||
|
print("=" * 80)
|
||||||
|
print("Press Ctrl+C to stop and see summary\n")
|
||||||
|
|
||||||
|
analyzer = PayloadAnalyzer()
|
||||||
|
|
||||||
|
# Create UDP socket
|
||||||
|
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||||
|
|
||||||
|
try:
|
||||||
|
sock.bind(("127.0.0.1", 5000))
|
||||||
|
print(f"Listening on 127.0.0.1:5000...\n")
|
||||||
|
print("Waiting for packets...\n")
|
||||||
|
|
||||||
|
packet_count = 0
|
||||||
|
while True:
|
||||||
|
data, addr = sock.recvfrom(65535)
|
||||||
|
packet_count += 1
|
||||||
|
|
||||||
|
analyzer.print_report(packet_count, addr, data)
|
||||||
|
|
||||||
|
# Print summary every 100 packets
|
||||||
|
if packet_count % 100 == 0:
|
||||||
|
print(f"\n[Received {packet_count} packets so far... continuing capture]\n")
|
||||||
|
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
print("\n\nCapture stopped by user.")
|
||||||
|
analyzer.print_summary()
|
||||||
|
except Exception as e:
|
||||||
|
print(f"\n[ERROR] {e}")
|
||||||
|
sys.exit(1)
|
||||||
|
finally:
|
||||||
|
sock.close()
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
67
scripts/udp_sniffer_raw.py
Normal file
67
scripts/udp_sniffer_raw.py
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# /// script
|
||||||
|
# requires-python = ">=3.8"
|
||||||
|
# dependencies = []
|
||||||
|
# ///
|
||||||
|
|
||||||
|
"""
|
||||||
|
Simple UDP Receiver for port 5000 on 127.0.0.1
|
||||||
|
This uses raw sockets (built-in) - no external dependencies needed
|
||||||
|
Usage: uv run scripts/udp_sniffer_raw.py
|
||||||
|
|
||||||
|
Note: This RECEIVES UDP packets (not sniffing like pcap/scapy)
|
||||||
|
"""
|
||||||
|
|
||||||
|
import socket
|
||||||
|
import sys
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
def main():
|
||||||
|
print("=" * 70)
|
||||||
|
print("UDP Receiver - Port 5000, 127.0.0.1")
|
||||||
|
print("=" * 70)
|
||||||
|
print("Press Ctrl+C to stop\n")
|
||||||
|
|
||||||
|
# Create UDP socket
|
||||||
|
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Bind to localhost port 5000
|
||||||
|
sock.bind(("127.0.0.1", 5000))
|
||||||
|
print(f"Listening on 127.0.0.1:5000...\n")
|
||||||
|
|
||||||
|
packet_count = 0
|
||||||
|
while True:
|
||||||
|
# Receive data
|
||||||
|
data, addr = sock.recvfrom(65535) # Max UDP packet size
|
||||||
|
packet_count += 1
|
||||||
|
|
||||||
|
timestamp = datetime.now().strftime("%H:%M:%S.%f")[:-3]
|
||||||
|
print(f"[{timestamp}] Packet #{packet_count}")
|
||||||
|
print(f" From: {addr[0]}:{addr[1]}")
|
||||||
|
print(f" Size: {len(data)} bytes")
|
||||||
|
|
||||||
|
# Print first 64 bytes in hex
|
||||||
|
hex_str = ' '.join(f'{b:02x}' for b in data[:64])
|
||||||
|
print(f" Data: {hex_str}{'...' if len(data) > 64 else ''}")
|
||||||
|
|
||||||
|
# Try to decode as ASCII (for text data)
|
||||||
|
try:
|
||||||
|
text = data[:100].decode('ascii', errors='ignore').strip()
|
||||||
|
if text and text.isprintable():
|
||||||
|
print(f" Text: {text[:80]}{'...' if len(text) > 80 else ''}")
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
print()
|
||||||
|
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
print(f"\n\nReceived {packet_count} packets. Stopped by user.")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"\n[ERROR] {e}")
|
||||||
|
sys.exit(1)
|
||||||
|
finally:
|
||||||
|
sock.close()
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
Loading…
x
Reference in New Issue
Block a user