Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .env.example
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@

RTSP_URL=rtsp://username:password@192.168.1.3:554/stream1
STREAM_URL=rtsp://username:password@192.168.1.3:554/stream1

CAMERA_HOST=192.168.1.100
CAMERA_USERNAME=your_tapo_email@example.com
Expand Down
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,7 @@ cython_debug/
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
# and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
#.idea/
.idea/

# Abstra
# Abstra is an AI-powered process automation framework.
Expand Down
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -84,4 +84,4 @@ HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
CMD curl -f http://localhost:8847/ || exit 1

# Default command - run Flask in production mode with Gunicorn
CMD ["gunicorn", "--bind", "0.0.0.0:8847", "--timeout", "120", "app:app"]
CMD ["gunicorn", "--bind", "0.0.0.0:8847", "--workers", "1", "--threads", "4", "--worker-class", "gthread", "--timeout", "120", "app:app"]
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@

### 🔄 **Smart Connectivity**
- **RTSP Stream Support**: Compatible with IP cameras supporting RTSP protocol
- **MJPEG Stream Support**: Compatible with variety of devices that stream this protocol
- **Auto-Reconnection**: Intelligent reconnection on network interruptions
- **Buffering Optimization**: Low-latency streaming with minimal delay
- **Connection Status**: Visual indicators for stream health
Expand Down
10 changes: 7 additions & 3 deletions api/camera_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,18 +17,22 @@
def get_camera_info():
"""Get camera basic information, connection status, and presets."""
try:
# Get camera status
# Get camera status (now handles background init and caching)
status = camera_service.get_status()

# Get camera presets
# Get camera presets (now handles background init and caching)
presets = camera_service.get_presets()

# We return success: True if we at least got a valid JSON response from the service
# even if the camera itself is offline or initializing.
return jsonify({
'success': status['available'],
'success': True,
'is_available': status['available'],
'device_model': status['device_model'],
'privacy_mode': status['privacy_mode'],
'connection_status': status['connection_status'],
'presets': presets,
'reason': status.get('reason'),
'error': status.get('error') if not status['available'] else None
})
except Exception as e:
Expand Down
2 changes: 1 addition & 1 deletion api/metrics_route.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ def get_metrics():
# Return basic metrics when streaming is not available
import psutil
return jsonify({
'cpu': psutil.cpu_percent(interval=0.1),
'cpu': psutil.cpu_percent(interval=None),
'memory': psutil.virtual_memory().percent,
'network': 0,
'detection_rate': 0,
Expand Down
36 changes: 36 additions & 0 deletions api/stream_route.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
"""
MJPEG Streaming Routes
"""
from flask import Blueprint, Response
from flask_login import login_required
from services.streaming.streaming_service import get_streaming_service
import cv2
import time

stream_bp = Blueprint('stream', __name__)

def gen_frames():
"""Frame generator for MJPEG stream"""
streaming_service = get_streaming_service()
if not streaming_service:
return

while True:
if streaming_service.ai_streamer:
frame = streaming_service.ai_streamer.get_latest_frame()
if frame is not None:
# Use slightly lower quality for MJPEG to save bandwidth
ret, buffer = cv2.imencode('.jpg', frame, [cv2.IMWRITE_JPEG_QUALITY, 70])
if ret:
yield (b'--frame\r\n'
b'Content-Type: image/jpeg\r\n\r\n' + buffer.tobytes() + b'\r\n')

# Limit to ~20 FPS for the web preview
time.sleep(0.05)

@stream_bp.route('/video_feed')
@login_required
def video_feed():
"""Video streaming route. Put this in the src attribute of an img tag."""
return Response(gen_frames(),
mimetype='multipart/x-mixed-replace; boundary=frame')
2 changes: 2 additions & 0 deletions app.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
from api.error_handlers import errors_bp
from api.websocket_handlers import register_socketio_events
from api.camera_api import camera_info_bp, camera_control_bp
from api.stream_route import stream_bp
from services.streaming.streaming_service import initialize_streaming_service

app = Flask(__name__)
Expand Down Expand Up @@ -51,6 +52,7 @@ def load_user(user_id):
# Import and register active users blueprint
from api.active_users_route import active_users_bp
app.register_blueprint(active_users_bp)
app.register_blueprint(stream_bp)

# Initialize databases (notification manager first, then auth)
notification_manager.init_app(app)
Expand Down
7 changes: 4 additions & 3 deletions config/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,9 @@
class BabyMonitorSettings:
"""Main configuration class for RTSP Recorder"""

# ==================== RTSP Settings ====================
RTSP_URL = os.getenv("RTSP_URL")
# ==================== Video Input Settings ====================
STREAM_URL = os.getenv("STREAM_URL", os.getenv("RTSP_URL")) # Can be RTSP URI or HTTP/HTTPS MJPEG stream URL
STREAM_SELF_SIGNED_CERT = os.getenv("STREAM_SELF_SIGNED_CERT", "false").lower() == "true"
RTSP_TIMEOUT = int(os.getenv("RTSP_TIMEOUT", 10))
YOLO_MODEL_NAME = os.getenv("MODEL_NAME", "yolov8n.pt")

Expand All @@ -33,7 +34,7 @@ class BabyMonitorSettings:
# Use Docker-compatible paths if running in container

CONFIDENCE_THRESHOLD = 0.4 # detection confidence
TARGET_FPS = 30.0 # reduced fps for CPU processing
TARGET_FPS = 30.0 # reduced fps for CPU processing. Higher values starve the web server.
DEBUG_VIDEO = True # enable extra video debugging output

# GPU usage flag
Expand Down
21 changes: 15 additions & 6 deletions docker-compose.yml
Original file line number Diff line number Diff line change
@@ -1,26 +1,35 @@
services:
ai-baby-monitor:
build:
build:
context: .
dockerfile: Dockerfile
container_name: AI-Baby-Monitor
restart: unless-stopped
ports:
- "8847:8847"

# Environment variables
environment:
- RTSP_URL=${RTSP_URL}
- STREAM_URL=${STREAM_URL}
- STREAM_SELF_SIGNED_CERT=${STREAM_SELF_SIGNED_CERT}
- FLASK_ENV=production
- PYTHONUNBUFFERED=1

# Volume mappings
volumes:
# Map recordings directory (MONITOR_RECORDINGS_DIR)
- ${HOME}/baby-monitor:/app/baby-monitor
# Health check override
# Mount source code for development/debugging
#- ./services:/app/services
#- ./templates:/app/templates
#- ./static:/app/static
#- ./api:/app/api
#- ./config:/app/config
#- ./utils:/app/utils
#- ./app.py:/app/app.py
# Health check override
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8847/health"]
test: [ "CMD", "curl", "-f", "http://localhost:8847/health" ]
interval: 30s
timeout: 10s
retries: 3
Expand Down
4 changes: 3 additions & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,6 @@ flask-wtf==1.2.2
wtforms==3.2.1
werkzeug==3.1.3
bcrypt==4.3.0
pytapo==3.3.49
pytapo==3.3.49
requests==2.32.5
email-validator==2.3.0
Loading