Adding "stream_type": "neolink" support requires updates across multiple layers:
Existing values in cameras.json:
"HLS" - Standard HLS (6+ cameras)"LL_HLS" - Low-latency HLS via MediaMTX (1 camera - REOLINK_OFFICE)"RTMP" - RTMP/FLV streaming (tested, not production)"mjpeg_proxy" - MJPEG proxy (1 camera - Eufy)null - No streamingNew value to add:
"NEOLINK" - Reolink cameras via Neolink bridge (Baichuan protocol)For Reolink cameras using Neolink, add/modify:
{
"REOLINK_OFFICE": {
"name": "CAM OFFICE",
"model": "RLC-410-5MP",
"type": "reolink",
"serial": "REOLINK_OFFICE",
"host": "192.168.10.88",
"mac": "ec:71:db:3e:93:f5",
"capabilities": ["streaming"],
"stream_type": "NEOLINK", // <-- Changed from "LL_HLS"
// New section for Neolink-specific config
"neolink": {
"baichuan_port": 9000,
"rtsp_path": "mainStream", // or "subStream"
"enabled": true
},
// Keep existing rtsp_input/rtsp_output for FFmpeg processing
"rtsp_input": {
"rtsp_transport": "tcp",
"timeout": 5000000,
// ... existing params
},
"rtsp_output": {
// ... existing HLS output params
},
// Player settings remain the same
"player_settings": {
"hls_js": {
"enableWorker": true,
"lowLatencyMode": true,
// ... existing params
}
}
}
}
Key points:
stream_type: "NEOLINK" tells system to route through Neolink bridgeneolink section contains Neolink-specific configNew file: 0_MAINTENANCE_SCRIPTS/generate_neolink_config.py
#!/usr/bin/env python3
"""
Generate neolink.toml from cameras.json
Filters for cameras with stream_type = "NEOLINK"
"""
import json
import sys
from pathlib import Path
def generate_neolink_config():
# Load cameras.json
cameras_file = Path(__file__).parent.parent / 'config' / 'cameras.json'
with open(cameras_file) as f:
data = json.load(f)
# Filter for Neolink cameras
neolink_cameras = []
for serial, config in data.get('devices', {}).items():
if config.get('stream_type') == 'NEOLINK' and config.get('type') == 'reolink':
neolink_cameras.append({
'serial': serial,
'name': config.get('name', serial),
'host': config.get('host'),
'neolink': config.get('neolink', {}),
'credentials': config.get('credentials', {})
})
if not neolink_cameras:
print("No cameras with stream_type='NEOLINK' found")
return
# Generate neolink.toml
output_file = Path(__file__).parent.parent / 'config' / 'neolink.toml'
with open(output_file, 'w') as f:
f.write("""################################################################################
# NEOLINK CONFIGURATION - AUTO-GENERATED
# Generated from cameras.json
# DO NOT EDIT MANUALLY - Use generate_neolink_config.py
################################################################################
bind = "0.0.0.0:8554"
log_level = "info"
""")
for cam in neolink_cameras:
baichuan_port = cam['neolink'].get('baichuan_port', 9000)
stream_path = cam['neolink'].get('rtsp_path', 'mainStream')
enabled = cam['neolink'].get('enabled', True)
# Get credentials (implement credential provider logic)
username = cam['credentials'].get('username', 'admin')
password = cam['credentials'].get('password', '')
f.write(f"""
################################################################################
# {cam['name']} ({cam['serial']})
################################################################################
[[cameras]]
name = "{cam['serial']}"
username = "{username}"
password = "{password}"
uid = ""
address = "{cam['host']}:{baichuan_port}"
stream = "{stream_path}"
enabled = {str(enabled).lower()}
""")
print(f"✓ Generated {output_file}")
print(f"✓ Configured {len(neolink_cameras)} camera(s)")
if __name__ == '__main__':
generate_neolink_config()
Usage:
cd ~/0_NVR
python3 0_MAINTENANCE_SCRIPTS/generate_neolink_config.py
File: streaming/handlers/reolink_stream_handler.py
Current behavior:
build_rtsp_url() connects directly to camera:554New behavior:
stream_type in camera config"NEOLINK", return Neolink bridge URL instead"HLS" or "LL_HLS", use direct camera URLdef build_rtsp_url(self, camera_config: Dict, stream_type: str = 'sub') -> str:
"""
Build RTSP URL - either direct camera or via Neolink bridge
"""
serial = camera_config.get('serial', 'UNKNOWN')
config_stream_type = camera_config.get('stream_type', 'HLS').upper()
# NEOLINK: Use bridge instead of direct camera connection
if config_stream_type == 'NEOLINK':
neolink_config = camera_config.get('neolink', {})
rtsp_path = neolink_config.get('rtsp_path', 'mainStream')
# Neolink runs in same container, use localhost
rtsp_url = f"rtsp://localhost:8554/{serial}/{rtsp_path}"
logger.info(f"Using Neolink bridge for {serial}: {rtsp_url}")
return rtsp_url
# STANDARD: Direct camera connection (existing logic)
else:
# ... existing direct camera RTSP URL logic ...
camera_ip = camera_config.get('host')
# ... rest of existing code ...
Location in file: Around line 40-60 in build_rtsp_url() method
File: stream_manager.py
Current code (line 253, 344):
st = (cam or {}).get('stream_type', 'HLS').upper()
protocol = camera.get('stream_type', 'HLS').upper()
Update needed: Add NEOLINK to valid stream types check:
# Around line 253
st = (cam or {}).get('stream_type', 'HLS').upper()
if st not in ['HLS', 'LL_HLS', 'MJPEG_PROXY', 'RTMP', 'NEOLINK']:
logger.warning(f"Unknown stream_type '{st}' for {serial}, defaulting to HLS")
st = 'HLS'
Behavior:
File: ffmpeg_params.py
Check line 228:
builder = FFmpegHLSParamBuilder(camera_name=camera_name, stream_type=stream_type, ...)
Likely no change needed - NEOLINK cameras still produce HLS output
File: stream.js
Current routing (line 296-305):
// Use streamType to determine which manager to use
if (streamType === 'mjpeg_proxy') {
success = await this.mjpegManager.startStream(serial, streamElement);
} else if (streamType === 'HLS' || streamType === 'LL_HLS' || streamType === 'NEOLINK' || streamType === 'NEOLINK_LL_HLS') {
success = await this.hlsManager.startStream(serial, streamElement, 'sub');
} else if (streamType === 'RTMP') {
success = await this.flvManager.startStream(serial, streamElement);
} else {
throw new Error(`Unknown stream type: ${streamType}`);
}
Updated routing:
// Use streamType to determine which manager to use
if (streamType === 'mjpeg_proxy') {
success = await this.mjpegManager.startStream(serial, streamElement);
} else if (streamType === 'HLS' || streamType === 'LL_HLS' || streamType === 'NEOLINK' || streamType === 'NEOLINK_LL_HLS') {
// NEOLINK cameras output HLS (via Neolink bridge)
success = await this.hlsManager.startStream(serial, streamElement, 'sub');
} else if (streamType === 'RTMP') {
success = await this.flvManager.startStream(serial, streamElement);
} else {
throw new Error(`Unknown stream type: ${streamType}`);
}
Also update health monitoring (line 321-329):
if ((streamType === 'HLS' || streamType === 'NEOLINK') && this.health) {
const hls = this.hlsManager?.hlsInstances?.get?.(serial) || null;
el._healthDetach = this.health.attachHls(serial, el, hls);
}
Key point: From frontend perspective, NEOLINK = HLS (browser doesn’t care about Baichuan)
Check line 240:
if (streamType === 'HLS' || streamType === 'LL_HLS' || streamType === 'NEOLINK' || streamType === 'NEOLINK_LL_HLS') {
Update to:
if (streamType === 'HLS' || streamType === 'LL_HLS' || streamType === 'NEOLINK' || streamType === 'NEOLINK_LL_HLS') {
Check data attributes:
<div class="stream-item"
data-serial="REOLINK_OFFICE"
data-stream-type="NEOLINK"> <!-- New value -->
No code changes needed - just ensure stream_type from cameras.json passes through correctly
Add Neolink binary to container:
# ... existing COPY commands ...
# Add Neolink binary and config
COPY neolink/target/release/neolink /usr/local/bin/neolink
COPY config/neolink.toml /app/config/neolink.toml
RUN chmod +x /usr/local/bin/neolink
# ... rest of Dockerfile ...
Expose Neolink RTSP port (internal only):
services:
unified-nvr:
# ... existing config ...
ports:
- "5000:5000" # Flask app
- "8554:8554" # Neolink RTSP server (NEW)
# ... rest of config ...
Note: Port 8554 is INTERNAL to container network - not exposed to host
If using supervisord to manage processes in container:
[program:neolink]
command=/usr/local/bin/neolink rtsp --config=/app/config/neolink.toml
autostart=true
autorestart=true
stdout_logfile=/dev/stdout
stdout_logfile_maxbytes=0
stderr_logfile=/dev/stderr
stderr_logfile_maxbytes=0
Or start in Dockerfile ENTRYPOINT:
CMD ["sh", "-c", "neolink rtsp --config=/app/config/neolink.toml & python3 app.py"]
Test camera config:
# Verify NEOLINK cameras detected
python3 0_MAINTENANCE_SCRIPTS/generate_neolink_config.py
# Check generated neolink.toml
cat config/neolink.toml
Test RTSP URL generation:
# In Python console
from streaming.handlers.reolink_stream_handler import ReolinkStreamHandler
config = {
'serial': 'REOLINK_OFFICE',
'stream_type': 'NEOLINK',
'host': '192.168.10.88',
'neolink': {
'rtsp_path': 'mainStream'
}
}
handler = ReolinkStreamHandler(None, {})
url = handler.build_rtsp_url(config)
print(url) # Should be: rtsp://localhost:8554/REOLINK_OFFICE/mainStream
Step 1: Test Neolink standalone
# Start Neolink manually
cd ~/0_NVR/neolink
./target/release/neolink rtsp --config=../config/neolink.toml
# Verify RTSP stream works
ffmpeg -rtsp_transport tcp -i rtsp://localhost:8554/REOLINK_OFFICE/mainStream -t 5 -f null -
Step 2: Test in Docker container
# Rebuild container with Neolink
docker compose build unified-nvr
docker compose up -d unified-nvr
# Check Neolink is running
docker compose exec unified-nvr ps aux | grep neolink
# Check RTSP port
docker compose exec unified-nvr netstat -tlnp | grep 8554
# Test stream from inside container
docker compose exec unified-nvr ffmpeg -rtsp_transport tcp -i rtsp://localhost:8554/REOLINK_OFFICE/mainStream -t 5 -f null -
Step 3: Test full pipeline
# Start stream via API
curl -X POST https://192.168.10.15/api/stream/start/REOLINK_OFFICE
# Check logs
docker compose logs -f unified-nvr | grep -i neolink
# Open browser: https://192.168.10.15/streams
# Camera should play with improved latency
Measure latency:
Test with:
# Use VLC or ffplay with timestamp overlay
ffplay -rtsp_transport tcp rtsp://localhost:8554/REOLINK_OFFICE/mainStream
Compare:
Camera .88 (OFFICE) - Guinea Pig:
stream_type: "NEOLINK" in cameras.jsonCamera .89 (TERRACE) - Second:
Other Reolink cameras - Batch:
If Neolink causes issues:
# 1. Revert cameras.json
git checkout cameras.json
# 2. Remove Neolink from container
docker compose exec unified-nvr pkill neolink
# 3. Restart container without Neolink
# (or rebuild from previous git commit)
docker compose down unified-nvr
docker compose up -d unified-nvr
Keep backups:
cameras.json.backup.{timestamp}Dockerfile.backup.{timestamp}docker-compose.yml.backup.{timestamp}0_MAINTENANCE_SCRIPTS/generate_neolink_config.py (NEW)streaming/handlers/reolink_stream_handler.py (MODIFY)stream_manager.py (MODIFY - add NEOLINK to valid types)ffmpeg_params.py (CHECK - likely no change needed)stream.js (MODIFY - add NEOLINK to HLS routing)cameras.json (MODIFY - add stream_type: “NEOLINK” + neolink section)config/neolink.toml (AUTO-GENERATED from cameras.json)Dockerfile (MODIFY - add Neolink binary)docker-compose.yml (MODIFY - expose port 8554)README_project_history.md (UPDATE - document Neolink integration)Step 1: ✅ Build Neolink binary (integration script Step 1) Step 2: ✅ Test Neolink standalone (integration script Steps 2-4) Step 3: ⏳ Implement backend updates (this document) Step 4: ⏳ Implement frontend updates (this document) Step 5: ⏳ Docker integration (integration script Steps 5-7) Step 6: ⏳ Testing and validation (integration script Step 8 + this doc Phase 5) Step 7: ⏳ Production deployment and monitoring
Created: October 23, 2025 Status: Ready for implementation after Neolink build completes