Added Docker Files
This commit is contained in:
240
scripts/record_flight.py
Normal file
240
scripts/record_flight.py
Normal file
@@ -0,0 +1,240 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Record Drone Simulation with Flight
|
||||
====================================
|
||||
Runs the drone controller while recording the Gazebo simulation to video.
|
||||
|
||||
This script:
|
||||
1. Starts screen recording (ffmpeg)
|
||||
2. Runs the drone flight pattern
|
||||
3. Stops recording and saves the video
|
||||
|
||||
Usage:
|
||||
python scripts/record_flight.py --pattern square --duration 120
|
||||
python scripts/record_flight.py --pattern circle --output my_flight
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
import signal
|
||||
import argparse
|
||||
import subprocess
|
||||
import threading
|
||||
from datetime import datetime
|
||||
|
||||
# Add parent directory to path
|
||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
|
||||
from src.drone_controller import DroneController
|
||||
|
||||
|
||||
class SimulationRecorder:
|
||||
"""Records the simulation display using ffmpeg."""
|
||||
|
||||
def __init__(self, output_dir="recordings", fps=30, quality="medium"):
|
||||
self.output_dir = output_dir
|
||||
self.fps = fps
|
||||
self.quality = quality
|
||||
self.process = None
|
||||
self.recording = False
|
||||
self.output_file = None
|
||||
|
||||
# Quality presets (CRF values for libx264)
|
||||
self.quality_presets = {
|
||||
"low": (28, "faster"),
|
||||
"medium": (23, "medium"),
|
||||
"high": (18, "slow")
|
||||
}
|
||||
|
||||
os.makedirs(output_dir, exist_ok=True)
|
||||
|
||||
def start(self, output_name=None):
|
||||
"""Start recording."""
|
||||
if self.recording:
|
||||
print("[WARN] Already recording")
|
||||
return False
|
||||
|
||||
if not output_name:
|
||||
output_name = f"flight_{datetime.now().strftime('%Y%m%d_%H%M%S')}"
|
||||
|
||||
self.output_file = os.path.join(self.output_dir, f"{output_name}.mp4")
|
||||
|
||||
crf, preset = self.quality_presets.get(self.quality, (23, "medium"))
|
||||
display = os.environ.get("DISPLAY", ":0")
|
||||
|
||||
# FFmpeg command for screen recording
|
||||
cmd = [
|
||||
"ffmpeg", "-y",
|
||||
"-f", "x11grab",
|
||||
"-framerate", str(self.fps),
|
||||
"-i", display,
|
||||
"-c:v", "libx264",
|
||||
"-preset", preset,
|
||||
"-crf", str(crf),
|
||||
"-pix_fmt", "yuv420p",
|
||||
self.output_file
|
||||
]
|
||||
|
||||
print(f"[INFO] Starting recording: {self.output_file}")
|
||||
|
||||
try:
|
||||
self.process = subprocess.Popen(
|
||||
cmd,
|
||||
stdin=subprocess.PIPE,
|
||||
stdout=subprocess.DEVNULL,
|
||||
stderr=subprocess.DEVNULL
|
||||
)
|
||||
self.recording = True
|
||||
return True
|
||||
except FileNotFoundError:
|
||||
print("[ERROR] ffmpeg not found. Install with: sudo apt install ffmpeg")
|
||||
return False
|
||||
except Exception as e:
|
||||
print(f"[ERROR] Failed to start recording: {e}")
|
||||
return False
|
||||
|
||||
def stop(self):
|
||||
"""Stop recording."""
|
||||
if not self.recording or not self.process:
|
||||
return None
|
||||
|
||||
print("[INFO] Stopping recording...")
|
||||
|
||||
# Send 'q' to ffmpeg to stop gracefully
|
||||
try:
|
||||
self.process.communicate(input=b'q', timeout=5)
|
||||
except subprocess.TimeoutExpired:
|
||||
self.process.kill()
|
||||
|
||||
self.recording = False
|
||||
|
||||
if os.path.exists(self.output_file):
|
||||
size = os.path.getsize(self.output_file)
|
||||
print(f"[OK] Recording saved: {self.output_file} ({size / 1024 / 1024:.1f} MB)")
|
||||
return self.output_file
|
||||
else:
|
||||
print("[WARN] Recording file not found")
|
||||
return None
|
||||
|
||||
|
||||
def run_recorded_flight(pattern="square", altitude=5.0, size=5.0, output_name=None, quality="medium"):
|
||||
"""Run a drone flight while recording."""
|
||||
|
||||
print("=" * 50)
|
||||
print(" Recorded Drone Flight")
|
||||
print("=" * 50)
|
||||
print(f" Pattern: {pattern}")
|
||||
print(f" Altitude: {altitude}m")
|
||||
print(f" Size: {size}m")
|
||||
print(f" Recording Quality: {quality}")
|
||||
print("=" * 50)
|
||||
print()
|
||||
|
||||
# Start recorder
|
||||
recorder = SimulationRecorder(quality=quality)
|
||||
|
||||
# Initialize controller
|
||||
controller = DroneController()
|
||||
|
||||
if not controller.connect():
|
||||
print("[ERROR] Could not connect to SITL")
|
||||
return False
|
||||
|
||||
# Start recording
|
||||
recorder.start(output_name)
|
||||
time.sleep(2) # Give recording time to start
|
||||
|
||||
try:
|
||||
# Setup for GPS-denied flight
|
||||
print("\n--- SETUP ---")
|
||||
controller.setup_gps_denied()
|
||||
|
||||
if not controller.set_mode("GUIDED_NOGPS"):
|
||||
raise Exception("Could not set GUIDED_NOGPS mode")
|
||||
|
||||
if not controller.arm():
|
||||
raise Exception("Could not arm")
|
||||
|
||||
# Takeoff
|
||||
print("\n--- TAKEOFF ---")
|
||||
if not controller.takeoff(altitude):
|
||||
print("[WARN] Takeoff may have failed, continuing anyway...")
|
||||
|
||||
time.sleep(2)
|
||||
|
||||
# Fly pattern
|
||||
print("\n--- FLY PATTERN ---")
|
||||
if pattern == "square":
|
||||
controller.fly_square(size=size, altitude=altitude)
|
||||
elif pattern == "circle":
|
||||
controller.fly_circle(radius=size, altitude=altitude)
|
||||
else:
|
||||
# Hover
|
||||
print("[INFO] Hovering...")
|
||||
time.sleep(10)
|
||||
|
||||
# Land
|
||||
print("\n--- LANDING ---")
|
||||
controller.land()
|
||||
|
||||
# Wait for landing
|
||||
print("[INFO] Waiting for landing...")
|
||||
for i in range(100):
|
||||
controller.update_state()
|
||||
if controller.altitude < 0.5:
|
||||
print(f"[OK] Landed at altitude {controller.altitude:.1f}m")
|
||||
break
|
||||
time.sleep(0.1)
|
||||
|
||||
time.sleep(2)
|
||||
|
||||
except KeyboardInterrupt:
|
||||
print("\n[INFO] Interrupted - landing...")
|
||||
controller.land()
|
||||
time.sleep(3)
|
||||
except Exception as e:
|
||||
print(f"\n[ERROR] {e}")
|
||||
controller.land()
|
||||
finally:
|
||||
# Stop recording
|
||||
time.sleep(1)
|
||||
video_file = recorder.stop()
|
||||
|
||||
print()
|
||||
print("=" * 50)
|
||||
print(" Flight Complete")
|
||||
print("=" * 50)
|
||||
if video_file:
|
||||
print(f" Video: {video_file}")
|
||||
print()
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="Record Drone Flight Simulation")
|
||||
parser.add_argument("--pattern", "-p", choices=["square", "circle", "hover"],
|
||||
default="square", help="Flight pattern")
|
||||
parser.add_argument("--altitude", "-a", type=float, default=5.0,
|
||||
help="Flight altitude (meters)")
|
||||
parser.add_argument("--size", "-s", type=float, default=5.0,
|
||||
help="Pattern size (meters)")
|
||||
parser.add_argument("--output", "-o", type=str, default=None,
|
||||
help="Output video filename (without extension)")
|
||||
parser.add_argument("--quality", "-q", choices=["low", "medium", "high"],
|
||||
default="medium", help="Video quality")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
run_recorded_flight(
|
||||
pattern=args.pattern,
|
||||
altitude=args.altitude,
|
||||
size=args.size,
|
||||
output_name=args.output,
|
||||
quality=args.quality
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
235
scripts/record_simulation.sh
Executable file
235
scripts/record_simulation.sh
Executable file
@@ -0,0 +1,235 @@
|
||||
#!/bin/bash
|
||||
# =============================================================================
|
||||
# Record Gazebo Simulation to Video
|
||||
# =============================================================================
|
||||
# Records the Gazebo simulation window to a video file.
|
||||
#
|
||||
# Usage:
|
||||
# ./scripts/record_simulation.sh # Default: record for 60s
|
||||
# ./scripts/record_simulation.sh --duration 120 # Record for 120 seconds
|
||||
# ./scripts/record_simulation.sh --output my_video # Custom output name
|
||||
# ./scripts/record_simulation.sh --method gazebo # Use Gazebo's recorder
|
||||
#
|
||||
# Prerequisites:
|
||||
# - Gazebo must be running
|
||||
# - For ffmpeg method: sudo apt install ffmpeg
|
||||
# =============================================================================
|
||||
|
||||
set -e
|
||||
|
||||
# Default values
|
||||
DURATION=60
|
||||
OUTPUT_NAME="drone_simulation_$(date +%Y%m%d_%H%M%S)"
|
||||
OUTPUT_DIR="./recordings"
|
||||
METHOD="ffmpeg" # ffmpeg or gazebo
|
||||
FPS=30
|
||||
QUALITY="medium" # low, medium, high
|
||||
|
||||
# Parse arguments
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case $1 in
|
||||
-d|--duration)
|
||||
DURATION="$2"
|
||||
shift 2
|
||||
;;
|
||||
-o|--output)
|
||||
OUTPUT_NAME="$2"
|
||||
shift 2
|
||||
;;
|
||||
--dir)
|
||||
OUTPUT_DIR="$2"
|
||||
shift 2
|
||||
;;
|
||||
-m|--method)
|
||||
METHOD="$2"
|
||||
shift 2
|
||||
;;
|
||||
--fps)
|
||||
FPS="$2"
|
||||
shift 2
|
||||
;;
|
||||
-q|--quality)
|
||||
QUALITY="$2"
|
||||
shift 2
|
||||
;;
|
||||
-h|--help)
|
||||
echo "Usage: $0 [OPTIONS]"
|
||||
echo ""
|
||||
echo "Options:"
|
||||
echo " -d, --duration SEC Recording duration in seconds (default: 60)"
|
||||
echo " -o, --output NAME Output filename without extension"
|
||||
echo " --dir PATH Output directory (default: ./recordings)"
|
||||
echo " -m, --method METHOD Recording method: ffmpeg or gazebo"
|
||||
echo " --fps FPS Frames per second (default: 30)"
|
||||
echo " -q, --quality QUAL Quality: low, medium, high (default: medium)"
|
||||
echo " -h, --help Show this help"
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
echo "Unknown option: $1"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
# Create output directory
|
||||
mkdir -p "$OUTPUT_DIR"
|
||||
|
||||
# Quality presets
|
||||
case $QUALITY in
|
||||
low)
|
||||
CRF=28
|
||||
PRESET="faster"
|
||||
;;
|
||||
medium)
|
||||
CRF=23
|
||||
PRESET="medium"
|
||||
;;
|
||||
high)
|
||||
CRF=18
|
||||
PRESET="slow"
|
||||
;;
|
||||
esac
|
||||
|
||||
echo "=============================================="
|
||||
echo " Gazebo Simulation Recorder"
|
||||
echo "=============================================="
|
||||
echo " Duration: ${DURATION}s"
|
||||
echo " Output: ${OUTPUT_DIR}/${OUTPUT_NAME}.mp4"
|
||||
echo " Method: $METHOD"
|
||||
echo " FPS: $FPS"
|
||||
echo " Quality: $QUALITY"
|
||||
echo "=============================================="
|
||||
echo ""
|
||||
|
||||
if [ "$METHOD" = "gazebo" ]; then
|
||||
# ==========================================================================
|
||||
# METHOD 1: Gazebo's built-in video recording
|
||||
# ==========================================================================
|
||||
# Gazebo Harmonic can record video via the GUI or command line
|
||||
|
||||
OUTPUT_FILE="${OUTPUT_DIR}/${OUTPUT_NAME}.mp4"
|
||||
|
||||
echo "[INFO] Using Gazebo's built-in recorder..."
|
||||
echo "[INFO] In Gazebo GUI:"
|
||||
echo " 1. Click the hamburger menu (☰) in top-left"
|
||||
echo " 2. Select 'Video Recorder'"
|
||||
echo " 3. Click 'Record' to start, 'Stop' to stop"
|
||||
echo " 4. Video saves to ~/.gz/sim/recordings/"
|
||||
echo ""
|
||||
echo "[INFO] Or use gz service to record programmatically..."
|
||||
|
||||
# Check if Gazebo is running
|
||||
if ! pgrep -x "gz" > /dev/null && ! pgrep -x "ruby" > /dev/null; then
|
||||
echo "[ERROR] Gazebo doesn't appear to be running"
|
||||
echo "[TIP] Start Gazebo first: ./scripts/run_ardupilot_sim.sh runway"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Start recording via gz service (if available)
|
||||
echo "[INFO] Sending record command to Gazebo..."
|
||||
gz service -s /gui/record_video --reqtype gz.msgs.VideoRecord --reptype gz.msgs.Boolean \
|
||||
--timeout 5000 --req "start: true, format: \"mp4\", save_filename: \"${OUTPUT_FILE}\"" 2>/dev/null || {
|
||||
echo "[WARN] Could not start recording via service"
|
||||
echo "[TIP] Use the Gazebo GUI to record manually"
|
||||
}
|
||||
|
||||
echo "[INFO] Recording for ${DURATION} seconds..."
|
||||
sleep "$DURATION"
|
||||
|
||||
# Stop recording
|
||||
gz service -s /gui/record_video --reqtype gz.msgs.VideoRecord --reptype gz.msgs.Boolean \
|
||||
--timeout 5000 --req "start: false" 2>/dev/null || true
|
||||
|
||||
echo "[OK] Recording stopped"
|
||||
echo "[INFO] Check ~/.gz/sim/recordings/ for the video"
|
||||
|
||||
else
|
||||
# ==========================================================================
|
||||
# METHOD 2: FFmpeg screen recording
|
||||
# ==========================================================================
|
||||
# Records the Gazebo window using ffmpeg
|
||||
|
||||
OUTPUT_FILE="${OUTPUT_DIR}/${OUTPUT_NAME}.mp4"
|
||||
|
||||
# Check for ffmpeg
|
||||
if ! command -v ffmpeg &> /dev/null; then
|
||||
echo "[ERROR] ffmpeg not found"
|
||||
echo "[TIP] Install: sudo apt install ffmpeg"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Find the Gazebo window
|
||||
echo "[INFO] Looking for Gazebo window..."
|
||||
|
||||
# Try to find the window ID
|
||||
WINDOW_ID=""
|
||||
|
||||
# Try xdotool first
|
||||
if command -v xdotool &> /dev/null; then
|
||||
WINDOW_ID=$(xdotool search --name "Gazebo" 2>/dev/null | head -1) || true
|
||||
fi
|
||||
|
||||
# Fallback to wmctrl
|
||||
if [ -z "$WINDOW_ID" ] && command -v wmctrl &> /dev/null; then
|
||||
WINDOW_ID=$(wmctrl -l | grep -i "gazebo" | awk '{print $1}' | head -1) || true
|
||||
fi
|
||||
|
||||
if [ -n "$WINDOW_ID" ]; then
|
||||
# Get window geometry
|
||||
if command -v xdotool &> /dev/null; then
|
||||
eval $(xdotool getwindowgeometry --shell "$WINDOW_ID" 2>/dev/null) || true
|
||||
fi
|
||||
|
||||
if [ -n "$WIDTH" ] && [ -n "$HEIGHT" ] && [ -n "$X" ] && [ -n "$Y" ]; then
|
||||
echo "[INFO] Found Gazebo window: ${WIDTH}x${HEIGHT} at +${X}+${Y}"
|
||||
GRAB_REGION="-video_size ${WIDTH}x${HEIGHT} -grab_x $X -grab_y $Y"
|
||||
else
|
||||
echo "[WARN] Could not get window geometry, recording full screen"
|
||||
GRAB_REGION=""
|
||||
fi
|
||||
else
|
||||
echo "[WARN] Could not find Gazebo window, recording full screen"
|
||||
GRAB_REGION=""
|
||||
fi
|
||||
|
||||
echo "[INFO] Starting recording..."
|
||||
echo "[INFO] Press Ctrl+C to stop early"
|
||||
echo ""
|
||||
|
||||
# Record using ffmpeg
|
||||
# Using x11grab to capture the screen
|
||||
ffmpeg -y \
|
||||
-f x11grab \
|
||||
-framerate "$FPS" \
|
||||
$GRAB_REGION \
|
||||
-i "${DISPLAY:-:0}" \
|
||||
-t "$DURATION" \
|
||||
-c:v libx264 \
|
||||
-preset "$PRESET" \
|
||||
-crf "$CRF" \
|
||||
-pix_fmt yuv420p \
|
||||
"$OUTPUT_FILE" \
|
||||
2>&1 | while IFS= read -r line; do
|
||||
# Show progress
|
||||
if [[ "$line" =~ time=([0-9:\.]+) ]]; then
|
||||
echo -ne "\r[Recording] Time: ${BASH_REMATCH[1]} / ${DURATION}s"
|
||||
fi
|
||||
done
|
||||
|
||||
echo ""
|
||||
echo ""
|
||||
echo "[OK] Recording complete!"
|
||||
echo "[INFO] Output: $OUTPUT_FILE"
|
||||
|
||||
# Get file info
|
||||
if [ -f "$OUTPUT_FILE" ]; then
|
||||
FILE_SIZE=$(du -h "$OUTPUT_FILE" | cut -f1)
|
||||
echo "[INFO] Size: $FILE_SIZE"
|
||||
fi
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "=============================================="
|
||||
echo " Recording Complete"
|
||||
echo "=============================================="
|
||||
Reference in New Issue
Block a user