271 lines
11 KiB
Python
271 lines
11 KiB
Python
from django.http import JsonResponse, HttpResponse, Http404
|
|
from django.views.decorators.csrf import csrf_exempt
|
|
from django.views.decorators.http import require_http_methods
|
|
from django.shortcuts import get_object_or_404
|
|
from django.conf import settings
|
|
from .models import VideoStream, StreamStatus
|
|
from .source_adapters import SourceAdapterFactory
|
|
from ai_processing.processors.video_analyzer import VideoAnalyzer
|
|
import json
|
|
import uuid
|
|
import logging
|
|
import os
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
@csrf_exempt
|
|
@require_http_methods(["POST"])
|
|
def create_stream(request):
|
|
"""Create or update RTMP stream (single stream pattern like webcam)"""
|
|
try:
|
|
data = json.loads(request.body)
|
|
source_type = data.get('source_type', 'rtmp')
|
|
|
|
# Look for existing stream of this type first
|
|
existing_stream = VideoStream.objects.filter(source_type=source_type).first()
|
|
|
|
if existing_stream:
|
|
# Update existing stream
|
|
existing_stream.name = data['name']
|
|
existing_stream.processing_mode = data.get('processing_mode', 'live')
|
|
existing_stream.save()
|
|
stream = existing_stream
|
|
logger.info(f"Updated existing {source_type} stream: {stream.id}")
|
|
else:
|
|
# Create new stream
|
|
stream = VideoStream.objects.create(
|
|
name=data['name'],
|
|
source_type=source_type,
|
|
processing_mode=data.get('processing_mode', 'live'),
|
|
stream_key=str(uuid.uuid4())
|
|
)
|
|
logger.info(f"Created new {source_type} stream: {stream.id}")
|
|
|
|
return JsonResponse({
|
|
'id': stream.id,
|
|
'name': stream.name,
|
|
'source_type': stream.source_type,
|
|
'processing_mode': stream.processing_mode,
|
|
'stream_key': stream.stream_key,
|
|
'status': stream.status,
|
|
'hls_playlist_url': f"{settings.HLS_BASE_URL}{settings.HLS_ENDPOINT_PATH}{stream.stream_key}.m3u8" if stream.status == 'active' else None,
|
|
'rtmp_ingest_url': f"rtmp://{request.get_host().split(':')[0]}:{settings.RTMP_PORT}/live",
|
|
'created_at': stream.created_at.isoformat()
|
|
})
|
|
|
|
except Exception as e:
|
|
return JsonResponse({'error': str(e)}, status=500)
|
|
|
|
|
|
def list_streams(request):
|
|
"""List all streams"""
|
|
streams = VideoStream.objects.all()
|
|
return JsonResponse({
|
|
'streams': [{
|
|
'id': s.id,
|
|
'name': s.name,
|
|
'source_type': s.source_type,
|
|
'processing_mode': s.processing_mode,
|
|
'stream_key': s.stream_key,
|
|
'status': s.status,
|
|
'hls_playlist_url': f"{settings.HLS_BASE_URL}{settings.HLS_ENDPOINT_PATH}{s.stream_key}.m3u8" if s.status == 'active' else None,
|
|
'rtmp_ingest_url': f"rtmp://{request.get_host().split(':')[0]}:{settings.RTMP_PORT}/live",
|
|
'created_at': s.created_at.isoformat()
|
|
} for s in streams]
|
|
})
|
|
|
|
|
|
@csrf_exempt
|
|
@require_http_methods(["POST"])
|
|
def start_stream(request, stream_key):
|
|
"""Start stream processing"""
|
|
stream = get_object_or_404(VideoStream, stream_key=stream_key)
|
|
|
|
try:
|
|
adapter = SourceAdapterFactory.create_adapter(stream)
|
|
success = adapter.start_processing()
|
|
|
|
if success:
|
|
return JsonResponse({
|
|
'message': 'Stream started successfully',
|
|
'hls_playlist_url': f"{settings.HLS_BASE_URL}{settings.HLS_ENDPOINT_PATH}{stream.stream_key}.m3u8"
|
|
})
|
|
else:
|
|
return JsonResponse({'error': 'Failed to start stream'}, status=500)
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error starting stream {stream_key}: {e}")
|
|
return JsonResponse({'error': str(e)}, status=500)
|
|
|
|
|
|
@csrf_exempt
|
|
@require_http_methods(["POST"])
|
|
def stop_stream(request, stream_key):
|
|
"""Stop stream processing"""
|
|
stream = get_object_or_404(VideoStream, stream_key=stream_key)
|
|
|
|
try:
|
|
adapter = SourceAdapterFactory.create_adapter(stream)
|
|
success = adapter.stop_processing()
|
|
|
|
if success:
|
|
return JsonResponse({'message': 'Stream stopped successfully'})
|
|
else:
|
|
return JsonResponse({'error': 'Failed to stop stream'}, status=500)
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error stopping stream {stream_key}: {e}")
|
|
return JsonResponse({'error': str(e)}, status=500)
|
|
|
|
|
|
def serve_hls_file(request, filename):
|
|
"""Serve HLS files with proper headers"""
|
|
# Files are stored in project media directory
|
|
media_dir = settings.MEDIA_ROOT
|
|
file_path = os.path.join(media_dir, filename)
|
|
|
|
# Check if file exists
|
|
if not os.path.exists(file_path):
|
|
raise Http404("HLS file not found")
|
|
|
|
# Trigger analysis for new .ts segments
|
|
if filename.endswith('.ts'):
|
|
logger.info(f"Processing .ts file request: {filename}")
|
|
try:
|
|
# Extract stream_key from filename: "stream_key-segment_number.ts" -> "stream_key"
|
|
# Example: "69f79422-5816-4cf0-9f44-0ac1421b8b8e-123.ts" -> "69f79422-5816-4cf0-9f44-0ac1421b8b8e"
|
|
base_name = filename.rsplit('.', 1)[0] # Remove .ts extension
|
|
stream_key = base_name.rsplit('-', 1)[0] # Remove last segment: "-123"
|
|
logger.info(f"Parsed stream_key: {stream_key} from filename: {filename}")
|
|
|
|
if stream_key:
|
|
# Queue for analysis
|
|
logger.info(f"Attempting to queue analysis for {filename}")
|
|
analyzer = VideoAnalyzer()
|
|
analyzer.queue_segment_analysis(stream_key, file_path)
|
|
logger.info(f"Queued segment for analysis: {filename} (stream: {stream_key})")
|
|
else:
|
|
logger.warning(f"No stream_key extracted from {filename}")
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error queuing analysis for {filename}: {e}")
|
|
import traceback
|
|
logger.error(f"Traceback: {traceback.format_exc()}")
|
|
|
|
# Determine content type
|
|
if filename.endswith('.m3u8'):
|
|
content_type = 'application/vnd.apple.mpegurl'
|
|
elif filename.endswith('.ts'):
|
|
content_type = 'video/mp2t'
|
|
else:
|
|
content_type = 'application/octet-stream'
|
|
|
|
# Read and serve file
|
|
with open(file_path, 'rb') as f:
|
|
response = HttpResponse(f.read(), content_type=content_type)
|
|
response['Cache-Control'] = 'no-cache'
|
|
return response
|
|
|
|
|
|
@csrf_exempt
|
|
@require_http_methods(["POST"])
|
|
def trigger_analysis(request, stream_key):
|
|
"""Manually trigger analysis for testing"""
|
|
try:
|
|
data = json.loads(request.body) if request.body else {}
|
|
segment_path = data.get('segment_path')
|
|
|
|
if not segment_path:
|
|
# Find latest segment in media directory
|
|
media_dir = settings.MEDIA_ROOT
|
|
ts_files = [f for f in os.listdir(media_dir) if f.endswith('.ts')]
|
|
if ts_files:
|
|
# Sort by filename to get the latest segment
|
|
ts_files.sort()
|
|
segment_path = os.path.join(media_dir, ts_files[-1])
|
|
else:
|
|
return JsonResponse({'error': 'No segments found'}, status=404)
|
|
|
|
analyzer = VideoAnalyzer()
|
|
success = analyzer.queue_segment_analysis(stream_key, segment_path)
|
|
|
|
if success:
|
|
return JsonResponse({'message': 'Analysis triggered', 'segment': segment_path})
|
|
else:
|
|
return JsonResponse({'error': 'Failed to queue analysis'}, status=500)
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error triggering analysis: {e}")
|
|
return JsonResponse({'error': str(e)}, status=500)
|
|
|
|
|
|
@csrf_exempt
|
|
@require_http_methods(["POST"])
|
|
def start_webcam_stream(request):
|
|
"""Start or reuse existing webcam stream"""
|
|
try:
|
|
# Look for existing webcam stream first
|
|
webcam_stream = VideoStream.objects.filter(source_type='webcam').first()
|
|
|
|
if not webcam_stream:
|
|
# Create new webcam stream
|
|
webcam_stream = VideoStream.objects.create(
|
|
name='Webcam Stream',
|
|
source_type='webcam',
|
|
processing_mode='live',
|
|
stream_key=str(uuid.uuid4())
|
|
)
|
|
logger.info(f"Created new webcam stream: {webcam_stream.id}")
|
|
|
|
# Check if another stream is active
|
|
active_streams = VideoStream.objects.filter(status=StreamStatus.ACTIVE).exclude(id=webcam_stream.id)
|
|
if active_streams.exists():
|
|
other = active_streams.first()
|
|
return JsonResponse({
|
|
'error': f'Another stream is active: {other.name}',
|
|
'active_stream_key': other.stream_key,
|
|
'active_stream_name': other.name
|
|
}, status=409)
|
|
|
|
# Start the webcam stream if not already active
|
|
if webcam_stream.status != StreamStatus.ACTIVE:
|
|
adapter = SourceAdapterFactory.create_adapter(webcam_stream)
|
|
success = adapter.start_processing()
|
|
|
|
if not success:
|
|
return JsonResponse({'error': 'Failed to start webcam'}, status=500)
|
|
|
|
# Wait for HLS playlist to be ready before returning
|
|
import time
|
|
playlist_path = os.path.join(settings.MEDIA_ROOT, f"{webcam_stream.stream_key}.m3u8")
|
|
max_wait_time = 10 # seconds
|
|
wait_interval = 0.5 # seconds
|
|
elapsed_time = 0
|
|
|
|
logger.info(f"Waiting for HLS playlist to be ready: {playlist_path}")
|
|
while elapsed_time < max_wait_time:
|
|
if os.path.exists(playlist_path) and os.path.getsize(playlist_path) > 0:
|
|
logger.info(f"HLS playlist ready after {elapsed_time:.1f}s")
|
|
break
|
|
time.sleep(wait_interval)
|
|
elapsed_time += wait_interval
|
|
|
|
if not os.path.exists(playlist_path):
|
|
logger.warning(f"HLS playlist not ready after {max_wait_time}s, returning anyway")
|
|
|
|
return JsonResponse({
|
|
'id': webcam_stream.id,
|
|
'name': webcam_stream.name,
|
|
'source_type': webcam_stream.source_type,
|
|
'processing_mode': webcam_stream.processing_mode,
|
|
'stream_key': webcam_stream.stream_key,
|
|
'status': webcam_stream.status,
|
|
'hls_playlist_url': f"{settings.HLS_BASE_URL}{settings.HLS_ENDPOINT_PATH}{webcam_stream.stream_key}.m3u8",
|
|
'created_at': webcam_stream.created_at.isoformat()
|
|
})
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error starting webcam stream: {e}")
|
|
return JsonResponse({'error': str(e)}, status=500)
|