#!/usr/bin/env python3
"""
Video Feeder Process - Standalone process for reading and feeding video frames

This process is controlled by the main application via stdin (JSON commands)
and outputs raw RGB frames to stdout (piped to FFmpeg).

Communication Protocol:
- stdin: JSON commands for control
- stdout: Raw RGB24 frames (binary)
- stderr: JSON status messages

Commands (stdin JSON):
    {"command": "play", "video": "/path/to/video.mp4", "loop": true}
    {"command": "pause"}
    {"command": "resume"}
    {"command": "stop"}
    {"command": "quit"}

Status (stderr JSON):
    {"status": "playing", "video": "/path/to/video.mp4", "fps": 30.0}
    {"status": "paused"}
    {"status": "ended", "video": "/path/to/video.mp4"}
    {"status": "error", "message": "Failed to open video"}

FRAME QUEUE BUFFER:
- Reader reads as fast as possible to fill the queue
- Reader blocks when queue is full (backpressure)
- Writer outputs at the correct frame rate
- Queue absorbs variable decode times for smooth playback
"""

import sys
import os
import json
import time
import threading
import select
from collections import deque

# Ensure stdout/stderr are not buffered
sys.stdout = os.fdopen(sys.stdout.fileno(), 'wb', buffering=0)
sys.stderr = os.fdopen(sys.stderr.fileno(), 'w', buffering=1)

try:
    import cv2
    CV2_AVAILABLE = True
except ImportError:
    CV2_AVAILABLE = False
    sys.stderr.write(json.dumps({"status": "error", "message": "OpenCV not available"}) + "\n")
    sys.stderr.flush()
    sys.exit(1)

try:
    import numpy as np
    NUMPY_AVAILABLE = True
except ImportError:
    NUMPY_AVAILABLE = False
    sys.stderr.write(json.dumps({"status": "error", "message": "NumPy not available"}) + "\n")
    sys.stderr.flush()
    sys.exit(1)

# Configuration - OPTIMIZED for streaming
FRAME_QUEUE_SIZE = 75  # 3 seconds of buffered frames at 25fps
PRE_BUFFER_FRAMES = 25  # Pre-buffer 1 second before starting output
DEFAULT_FPS = 25.0  # OPTIMIZED: 25 fps instead of 30

# FIXED OUTPUT RESOLUTION - Must match FFmpeg input configuration
# All frames are resized to this resolution regardless of source video
OUTPUT_WIDTH = 1920
OUTPUT_HEIGHT = 1080

# Health monitoring configuration
HEARTBEAT_INTERVAL = 5.0  # Send heartbeat every 5 seconds
STALLED_THRESHOLD = 15.0  # If no frames written for 15 seconds, consider stalled
QUEUE_EMPTY_THRESHOLD = 30.0  # If queue empty for 30 seconds, consider stalled

# Debug mode - enabled via environment variable
DEBUG_MODE = os.environ.get('MBETTERC_DEBUG_VIDEO', '').lower() in ('1', 'true', 'yes')
DEBUG_FRAME_INTERVAL = int(os.environ.get('MBETTERC_DEBUG_FRAME_INTERVAL', '100'))  # Log every N frames


def send_status(status_dict):
    """Send status message to stderr as JSON"""
    try:
        # Add timestamp to all status messages
        status_dict["timestamp"] = time.time()
        sys.stderr.write(json.dumps(status_dict) + "\n")
        sys.stderr.flush()
    except Exception as e:
        pass


def debug_log(message, **kwargs):
    """Send debug log message when DEBUG_MODE is enabled"""
    if DEBUG_MODE:
        status_dict = {"status": "debug", "message": message}
        status_dict.update(kwargs)
        send_status(status_dict)


class FrameQueue:
    """Thread-safe frame queue with blocking put for backpressure"""
    
    def __init__(self, maxsize=FRAME_QUEUE_SIZE):
        self.queue = deque(maxlen=maxsize)
        self.lock = threading.Lock()
        self.not_empty = threading.Condition(self.lock)
        self.not_full = threading.Condition(self.lock)
        self.maxsize = maxsize
        self._closed = False
        
        # Statistics
        self.frames_added = 0
        self.frames_removed = 0
        self.underflows = 0
        self.overflows = 0
    
    def put(self, frame, block=True, timeout=None):
        """Add a frame to the queue
        
        Args:
            frame: Frame to add
            block: If True, block until space available
            timeout: Timeout for blocking (None = wait forever)
        
        Returns:
            True if frame was added, False if timed out or closed
        """
        with self.not_full:
            # Wait until there's space or queue is closed
            while len(self.queue) >= self.maxsize and not self._closed:
                if not block:
                    # Non-blocking: drop oldest frame
                    self.queue.popleft()
                    self.overflows += 1
                    break
                if timeout:
                    if not self.not_full.wait(timeout):
                        # Timeout - drop oldest frame
                        if len(self.queue) >= self.maxsize:
                            self.queue.popleft()
                            self.overflows += 1
                        else:
                            break
                else:
                    # Wait forever for space
                    self.not_full.wait()
            
            if self._closed:
                return False
            
            self.queue.append(frame)
            self.frames_added += 1
            self.not_empty.notify()
            return True
    
    def get(self, timeout=None):
        """Get a frame from the queue"""
        with self.not_empty:
            if not self.queue and not self._closed:
                if timeout:
                    self.not_empty.wait(timeout)
                if not self.queue:
                    self.underflows += 1
                    return None
            
            if not self.queue:
                return None
            
            frame = self.queue.popleft()
            self.frames_removed += 1
            self.not_full.notify()
            return frame
    
    def qsize(self):
        """Get current queue size"""
        with self.lock:
            return len(self.queue)
    
    def full(self):
        """Check if queue is full"""
        with self.lock:
            return len(self.queue) >= self.maxsize
    
    def close(self):
        """Close the queue"""
        with self.lock:
            self._closed = True
            self.not_empty.notify_all()
            self.not_full.notify_all()
    
    def is_closed(self):
        """Check if queue is closed"""
        with self.lock:
            return self._closed
    
    def get_stats(self):
        """Get queue statistics"""
        with self.lock:
            return {
                "size": len(self.queue),
                "maxsize": self.maxsize,
                "frames_added": self.frames_added,
                "frames_removed": self.frames_removed,
                "underflows": self.underflows,
                "overflows": self.overflows
            }


class VideoFeeder:
    """Video feeder with frame queue buffer for smooth playback
    
    Architecture:
    - Frame Reader Thread: Reads frames as fast as possible, blocks when queue full
    - Frame Writer Thread: Takes frames from queue, writes at output frame rate
    - Command Handler Thread: Reads commands from stdin
    
    Key insight:
    - Reader runs as fast as possible to keep queue full
    - Blocking put() provides backpressure when queue is full
    - Writer maintains output frame rate
    - Queue absorbs variable decode times for smooth playback
    """
    
    def __init__(self):
        self.running = True
        self.paused = False
        self.current_video_path = None
        self.current_video_capture = None
        self.loop_current_video = False
        self.frame_rate = DEFAULT_FPS
        # FIXED output size - all frames are resized to this resolution
        self.output_size = (OUTPUT_WIDTH, OUTPUT_HEIGHT)
        self.frame_interval = 1.0 / self.frame_rate
        self.black_frame = None
        
        # Frame queue buffer
        self.frame_queue = FrameQueue(maxsize=FRAME_QUEUE_SIZE)
        
        # Threading control
        self.reader_running = False
        self.writer_running = False
        self.reader_thread = None
        self.writer_thread = None
        
        # Pre-buffer control
        self.pre_buffering = True
        self.pre_buffer_event = threading.Event()
        
        # Video ended flag
        self.video_ended = False
        
        # Create default black frame at fixed output resolution
        self._create_black_frame()
        
        # Start command handler thread
        self.command_thread = threading.Thread(target=self._read_commands, daemon=True)
        self.command_thread.start()
        
        send_status({"status": "initialized", "queue_size": FRAME_QUEUE_SIZE})
    
    def _create_black_frame(self):
        """Create a black frame at the fixed output resolution"""
        self.black_frame = np.zeros((OUTPUT_HEIGHT, OUTPUT_WIDTH, 3), dtype=np.uint8)
    
    def _read_commands(self):
        """Thread to read commands from stdin"""
        while self.running:
            try:
                # Check if there's data available to read
                if select.select([sys.stdin], [], [], 0.1)[0]:
                    line = sys.stdin.readline()
                    if not line:
                        continue
                    
                    # Parse JSON command
                    try:
                        # Handle both string and binary stdin
                        if isinstance(line, bytes):
                            line = line.decode('utf-8')
                        line = line.strip()
                        if not line:
                            continue
                        
                        command = json.loads(line)
                        self._handle_command(command)
                    except json.JSONDecodeError as e:
                        send_status({"status": "error", "message": f"Invalid JSON: {e}"})
                
            except Exception as e:
                send_status({"status": "error", "message": f"Stdin read error: {e}"})
                time.sleep(0.1)
    
    def _handle_command(self, command):
        """Handle a command from stdin"""
        cmd = command.get("command", "").lower()
        
        if cmd == "play":
            video_path = command.get("video")
            loop = command.get("loop", False)
            self._play_video(video_path, loop)
        
        elif cmd == "pause":
            self._pause()
        
        elif cmd == "resume":
            self._resume()
        
        elif cmd == "stop":
            self._stop()
        
        elif cmd == "quit":
            self._quit()
        
        elif cmd == "stats":
            # Return queue statistics
            stats = self.frame_queue.get_stats()
            send_status({"status": "stats", **stats})
        
        else:
            send_status({"status": "error", "message": f"Unknown command: {cmd}"})
    
    def _play_video(self, video_path, loop=False):
        """Start playing a video file"""
        try:
            # Stop any current playback
            self._stop_reader_writer()
            
            # Clear the queue
            with self.frame_queue.lock:
                self.frame_queue.queue.clear()
            
            # Open new video
            cap = cv2.VideoCapture(video_path)
            if not cap.isOpened():
                send_status({"status": "error", "message": f"Failed to open video: {video_path}"})
                return
            
            # Get video properties (for logging only)
            source_fps = cap.get(cv2.CAP_PROP_FPS) or DEFAULT_FPS
            source_width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH)) or OUTPUT_WIDTH
            source_height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT)) or OUTPUT_HEIGHT
            
            # Use fixed output frame rate
            self.frame_rate = DEFAULT_FPS
            self.frame_interval = 1.0 / self.frame_rate
            
            self.current_video_capture = cap
            self.current_video_path = video_path
            self.loop_current_video = loop
            self.paused = False
            self.video_ended = False
            
            # Reset pre-buffering
            self.pre_buffering = True
            self.pre_buffer_event.clear()
            
            # Start reader thread
            self.reader_running = True
            self.reader_thread = threading.Thread(target=self._frame_reader_loop, daemon=True)
            self.reader_thread.start()
            
            # Start writer thread
            self.writer_running = True
            self.writer_thread = threading.Thread(target=self._frame_writer_loop, daemon=True)
            self.writer_thread.start()
            
            send_status({
                "status": "playing",
                "video": video_path,
                "source_fps": source_fps,
                "source_width": source_width,
                "source_height": source_height,
                "output_fps": self.frame_rate,
                "output_width": OUTPUT_WIDTH,
                "output_height": OUTPUT_HEIGHT,
                "loop": loop,
                "queue_size": FRAME_QUEUE_SIZE,
                "pre_buffer": PRE_BUFFER_FRAMES
            })
            
        except Exception as e:
            send_status({"status": "error", "message": f"Failed to play video: {e}"})
    
    def _pause(self):
        """Pause video playback"""
        self.paused = True
        send_status({"status": "paused"})
    
    def _resume(self):
        """Resume video playback"""
        self.paused = False
        send_status({"status": "resumed"})
    
    def _stop(self):
        """Stop video playback"""
        self._stop_reader_writer()
        self.current_video_path = None
        self.loop_current_video = False
        self.paused = False
        send_status({"status": "stopped"})
    
    def _quit(self):
        """Quit the video feeder process"""
        self.running = False
        self._stop_reader_writer()
        self.frame_queue.close()
        send_status({"status": "quitting"})
    
    def _stop_reader_writer(self):
        """Stop reader and writer threads"""
        # Stop reader
        self.reader_running = False
        if self.reader_thread:
            self.reader_thread.join(timeout=2)
            self.reader_thread = None
        
        # Stop writer
        self.writer_running = False
        if self.writer_thread:
            self.writer_thread.join(timeout=2)
            self.writer_thread = None
        
        # Release video capture
        if self.current_video_capture is not None:
            self.current_video_capture.release()
            self.current_video_capture = None
    
    def _frame_reader_loop(self):
        """Thread that reads frames as fast as possible
        
        Reader runs at maximum speed to keep the queue full.
        When queue is full, put() blocks, providing backpressure.
        This ensures:
        - Queue is always as full as possible
        - Variable decode times are absorbed by the queue
        - CPU is only used when there's space in the queue
        """
        send_status({"status": "reader_started"})
        debug_log("Reader thread started", video=self.current_video_path)
        
        frames_read = 0
        last_status_time = time.time()
        last_frame_time = time.time()
        read_errors = 0
        consecutive_errors = 0
        
        while self.reader_running and self.running:
            try:
                # Skip if paused
                if self.paused:
                    time.sleep(0.01)
                    continue
                
                # Check if we have a video capture
                if self.current_video_capture is None:
                    time.sleep(0.01)
                    continue
                
                # Read frame (as fast as possible)
                read_start = time.time()
                ret, frame = self.current_video_capture.read()
                read_time = time.time() - read_start
                
                if not ret:
                    read_errors += 1
                    consecutive_errors += 1
                    
                    # Debug: Log read failure
                    debug_log("Frame read failed",
                             read_errors=read_errors,
                             consecutive_errors=consecutive_errors,
                             frame_number=frames_read)
                    
                    # End of video
                    if self.loop_current_video:
                        # Loop: restart video
                        debug_log("Restarting video loop", video=self.current_video_path)
                        self.current_video_capture.set(cv2.CAP_PROP_POS_FRAMES, 0)
                        ret, frame = self.current_video_capture.read()
                        if not ret:
                            send_status({"status": "error", "message": "Failed to restart loop"})
                            debug_log("Failed to restart loop", error="read_failed_after_seek")
                            break
                        consecutive_errors = 0
                        send_status({"status": "looped", "video": self.current_video_path})
                    else:
                        # Non-looping video ended
                        self.video_ended = True
                        send_status({"status": "ended", "video": self.current_video_path})
                        debug_log("Video ended naturally",
                                 video=self.current_video_path,
                                 total_frames=frames_read,
                                 total_errors=read_errors)
                        break
                else:
                    consecutive_errors = 0  # Reset consecutive errors on success
                
                # ALWAYS resize to fixed output resolution
                # This ensures all videos output at the same resolution
                resize_time = 0
                if frame.shape[1] != OUTPUT_WIDTH or frame.shape[0] != OUTPUT_HEIGHT:
                    resize_start = time.time()
                    frame = cv2.resize(frame, (OUTPUT_WIDTH, OUTPUT_HEIGHT), interpolation=cv2.INTER_LINEAR)
                    resize_time = time.time() - resize_start
                
                # Add to queue - BLOCKING when queue is full
                # This provides backpressure and prevents CPU overload
                # Reader will wait here until writer makes space
                queue_start = time.time()
                if not self.frame_queue.put(frame, block=True):
                    # Queue was closed
                    debug_log("Queue closed, stopping reader")
                    break
                queue_time = time.time() - queue_start
                
                frames_read += 1
                
                # Debug: Log frame details periodically
                if DEBUG_MODE and frames_read % DEBUG_FRAME_INTERVAL == 0:
                    frame_time = time.time() - last_frame_time
                    debug_log("Frame read",
                             frame_number=frames_read,
                             read_time_ms=round(read_time * 1000, 2),
                             resize_time_ms=round(resize_time * 1000, 2),
                             queue_time_ms=round(queue_time * 1000, 2),
                             frame_interval_ms=round(frame_time * 1000, 2),
                             queue_size=self.frame_queue.qsize(),
                             queue_max=FRAME_QUEUE_SIZE)
                    last_frame_time = time.time()
                
                # Signal when pre-buffer is complete
                if self.pre_buffering and self.frame_queue.qsize() >= PRE_BUFFER_FRAMES:
                    self.pre_buffering = False
                    self.pre_buffer_event.set()
                    send_status({"status": "pre_buffer_complete", "frames": self.frame_queue.qsize()})
                    debug_log("Pre-buffer complete",
                             frames=self.frame_queue.qsize(),
                             target=PRE_BUFFER_FRAMES)
                
                # Send periodic status
                current_time = time.time()
                if current_time - last_status_time >= 5.0:
                    stats = self.frame_queue.get_stats()
                    send_status({
                        "status": "reading",
                        "frames_read": frames_read,
                        "queue_size": stats["size"],
                        "underflows": stats["underflows"],
                        "overflows": stats["overflows"],
                        "read_errors": read_errors
                    })
                    last_status_time = current_time
                
            except Exception as e:
                send_status({"status": "error", "message": f"Reader error: {e}"})
                debug_log("Reader exception", error=str(e), frame_number=frames_read)
                time.sleep(0.01)
        
        send_status({"status": "reader_stopped", "frames_read": frames_read})
        debug_log("Reader thread stopped", frames_read=frames_read, read_errors=read_errors)
    
    def _frame_writer_loop(self):
        """Thread that outputs frames at the correct frame rate
        
        Writer maintains the output frame rate by:
        - Taking frames from the queue
        - Writing them to stdout at precise intervals
        - Outputting black frames on underflow
        - Monitoring health and reporting stalls
        """
        send_status({"status": "writer_started"})
        debug_log("Writer thread started",
                 fps=self.frame_rate,
                 frame_interval_ms=round(self.frame_interval * 1000, 2))
        
        frames_written = 0
        black_frames_written = 0
        last_frame_time = time.time()
        last_status_time = time.time()
        last_successful_write = time.time()  # Track last successful frame write
        underflow_count = 0
        total_sleep_time = 0
        total_write_time = 0
        consecutive_black_frames = 0
        queue_empty_start = None  # Track when queue started being empty
        
        # Wait for pre-buffer to complete (with timeout)
        if self.pre_buffering:
            send_status({"status": "pre_buffering", "target": PRE_BUFFER_FRAMES})
            debug_log("Waiting for pre-buffer", target=PRE_BUFFER_FRAMES)
            pre_buffer_start = time.time()
            self.pre_buffer_event.wait(timeout=5.0)
            pre_buffer_time = time.time() - pre_buffer_start
            debug_log("Pre-buffer wait complete",
                     wait_time_ms=round(pre_buffer_time * 1000, 2),
                     queue_size=self.frame_queue.qsize())
        
        while self.writer_running and self.running:
            try:
                frame_start = time.time()
                
                # Get frame from queue (non-blocking with timeout)
                get_start = time.time()
                frame = self.frame_queue.get(timeout=0.1)
                get_time = time.time() - get_start
                
                is_black_frame = False
                if frame is None:
                    # Queue empty - output black frame
                    frame = self.black_frame
                    is_black_frame = True
                    black_frames_written += 1
                    consecutive_black_frames += 1
                    
                    # Track when queue started being empty
                    if queue_empty_start is None:
                        queue_empty_start = time.time()
                    queue_empty_duration = time.time() - queue_empty_start
                    
                    if not self.video_ended:
                        underflow_count += 1
                        send_status({"status": "underflow", "count": underflow_count})
                        debug_log("Queue underflow",
                                 underflow_count=underflow_count,
                                 frames_written=frames_written,
                                 queue_size=self.frame_queue.qsize(),
                                 consecutive_black_frames=consecutive_black_frames,
                                 queue_empty_duration=round(queue_empty_duration, 1))
                        
                        # Check if queue has been empty too long (stalled reader)
                        if queue_empty_duration > QUEUE_EMPTY_THRESHOLD:
                            send_status({
                                "status": "stalled",
                                "reason": "queue_empty_timeout",
                                "queue_empty_duration": round(queue_empty_duration, 1),
                                "consecutive_black_frames": consecutive_black_frames,
                                "frames_written": frames_written
                            })
                            debug_log("STALLED: Queue empty for too long",
                                     queue_empty_duration=round(queue_empty_duration, 1),
                                     threshold=QUEUE_EMPTY_THRESHOLD)
                else:
                    # Got a real frame - reset tracking
                    consecutive_black_frames = 0
                    queue_empty_start = None
                    last_successful_write = time.time()
                
                # Write frame to stdout
                write_start = time.time()
                write_success = self._write_frame(frame)
                write_time = time.time() - write_start
                total_write_time += write_time
                
                if not write_success:
                    # Write failed - FFmpeg may be stuck or dead
                    debug_log("WRITE_FAILED", "Frame write failed, FFmpeg may be stuck")
                    # Continue trying but the stalled detection will handle recovery
                    consecutive_black_frames += 1
                    continue
                
                frames_written += 1
                
                # Maintain output frame rate
                current_time = time.time()
                elapsed = current_time - last_frame_time
                sleep_time = 0
                if elapsed < self.frame_interval:
                    sleep_time = self.frame_interval - elapsed
                    time.sleep(sleep_time)
                    total_sleep_time += sleep_time
                last_frame_time = time.time()
                
                # Debug: Log frame details periodically
                if DEBUG_MODE and frames_written % DEBUG_FRAME_INTERVAL == 0:
                    frame_time = time.time() - frame_start
                    debug_log("Frame write",
                             frame_number=frames_written,
                             is_black_frame=is_black_frame,
                             get_time_ms=round(get_time * 1000, 2),
                             write_time_ms=round(write_time * 1000, 2),
                             sleep_time_ms=round(sleep_time * 1000, 2),
                             total_frame_time_ms=round(frame_time * 1000, 2),
                             queue_size=self.frame_queue.qsize(),
                             underflows=underflow_count,
                             consecutive_black_frames=consecutive_black_frames)
                
                # Send periodic status with health info
                current_time = time.time()
                if current_time - last_status_time >= HEARTBEAT_INTERVAL:
                    stats = self.frame_queue.get_stats()
                    time_since_last_write = current_time - last_successful_write
                    
                    # Check for stalled condition
                    is_stalled = time_since_last_write > STALLED_THRESHOLD
                    
                    send_status({
                        "status": "stalled" if is_stalled else "writing",
                        "frames_written": frames_written,
                        "black_frames": black_frames_written,
                        "consecutive_black_frames": consecutive_black_frames,
                        "queue_size": stats["size"],
                        "underflows": stats["underflows"],
                        "avg_write_time_ms": round(total_write_time / frames_written * 1000, 2) if frames_written > 0 else 0,
                        "time_since_last_frame": round(time_since_last_write, 1),
                        "is_healthy": not is_stalled and consecutive_black_frames < 100
                    })
                    last_status_time = current_time
                    
                    # Log health status in debug mode
                    if DEBUG_MODE:
                        debug_log("Health check",
                                 is_stalled=is_stalled,
                                 time_since_last_frame=round(time_since_last_write, 1),
                                 consecutive_black_frames=consecutive_black_frames,
                                 queue_size=stats["size"])
                
            except Exception as e:
                send_status({"status": "error", "message": f"Writer error: {e}"})
                debug_log("Writer exception", error=str(e), frame_number=frames_written)
                time.sleep(0.01)
        
        send_status({"status": "writer_stopped", "frames_written": frames_written})
        debug_log("Writer thread stopped",
                 frames_written=frames_written,
                 black_frames=black_frames_written,
                 underflows=underflow_count,
                 avg_write_time_ms=round(total_write_time / frames_written * 1000, 2) if frames_written > 0 else 0)
    
    def _write_frame(self, frame):
        """Write a frame to stdout with blocking detection"""
        try:
            # Convert BGR to RGB
            frame_rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
            frame_bytes = frame_rgb.tobytes()
            
            # Check if stdout is ready to receive data (non-blocking check)
            # Use select to check if the pipe is writable
            import select
            write_start = time.time()
            
            # Try to write with timeout detection
            try:
                written = sys.stdout.write(frame_bytes)
                sys.stdout.flush()
                
                write_time = time.time() - write_start
                if write_time > 0.1:  # If write took more than 100ms
                    debug_log("SLOW_WRITE",
                             write_time_ms=round(write_time * 1000, 1),
                             frame_size_kb=len(frame_bytes) // 1024)
                    
                if write_time > 1.0:  # If write took more than 1 second
                    send_status({
                        "status": "stalled",
                        "reason": "slow_write",
                        "write_time": round(write_time, 2)
                    })
                    
            except BlockingIOError:
                # Pipe buffer is full - FFmpeg is not consuming fast enough
                debug_log("BLOCKED_WRITE", "Pipe buffer full - FFmpeg not consuming")
                send_status({
                    "status": "stalled",
                    "reason": "pipe_full",
                    "message": "FFmpeg not consuming frames fast enough"
                })
                return False
                
        except BrokenPipeError:
            # FFmpeg has closed the pipe - it's gone
            send_status({
                "status": "error",
                "message": "FFmpeg pipe closed - process may have died"
            })
            return False
            
        except Exception as e:
            send_status({"status": "error", "message": f"Failed to write frame: {e}"})
            return False
            
        return True
    
    def run(self):
        """Main loop - wait for commands"""
        send_status({"status": "ready"})
        
        while self.running:
            time.sleep(0.1)
        
        # Cleanup
        self._stop_reader_writer()
        self.frame_queue.close()
        
        send_status({"status": "shutdown"})


def main():
    """Main entry point"""
    feeder = VideoFeeder()
    feeder.run()


if __name__ == "__main__":
    main()