Add UDP traffic analysis tools for GStreamer video debugging

This commit is contained in:
yair 2025-11-14 19:52:11 +02:00
parent 3a799c0a65
commit bdb89b2632
3 changed files with 383 additions and 0 deletions

View File

@ -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

View 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()

View 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()