an eink camera running on an rpi zero 2 w
1#!/usr/bin/env -S nix shell nixpkgs#parallel nixpkgs#ffmpeg nixpkgs#imagemagick --command bash
2
3# Define temporary directory variables globally so trap can access them
4FRAMES_IN=""
5FRAMES_OUT=""
6
7# Function to clean up and exit
8cleanup_and_exit() {
9 echo -e "\nInterrupted. Cleaning up temporary files..."
10 # Only remove if directories were actually created
11 [ -n "$FRAMES_IN" ] && [ -d "$FRAMES_IN" ] && rm -rf "$FRAMES_IN"
12 [ -n "$FRAMES_OUT" ] && [ -d "$FRAMES_OUT" ] && rm -rf "$FRAMES_OUT"
13 exit 1
14}
15
16# Set up trap for Ctrl+C (SIGINT)
17trap cleanup_and_exit INT
18
19# Check if input and output parameters are provided
20if [ $# -lt 2 ]; then
21 echo "Usage: $0 input_video output_video [num_threads]"
22 exit 1
23fi
24
25INPUT_VIDEO=$1
26OUTPUT_VIDEO=$2
27# Default to number of CPU cores if threads not specified
28NUM_THREADS=${3:-$(nproc)}
29
30# Determine and create temporary directories, preferring RAM disk
31TEMP_BASE=""
32if [ -d "/dev/shm" ] && [ -w "/dev/shm" ]; then
33 TEMP_BASE="/dev/shm"
34 echo "Using RAM disk (/dev/shm) for temporary files."
35elif [ -d "/tmp" ] && [ -w "/tmp" ] && mount | grep -q 'on /tmp type tmpfs'; then
36 TEMP_BASE="/tmp"
37 echo "Using RAM disk (/tmp) for temporary files."
38else
39 TEMP_BASE="."
40 echo "RAM disk not available or not writable, using current directory for temporary files."
41fi
42
43# Create unique temporary directory names using process ID
44# Add a random suffix for extra safety in case PID wraps or script runs concurrently
45RAND_SUFFIX=$(head /dev/urandom | tr -dc A-Za-z0-9 | head -c 8)
46FRAMES_IN="${TEMP_BASE}/inkify_frames_in_${$}_${RAND_SUFFIX}"
47FRAMES_OUT="${TEMP_BASE}/inkify_frames_out_${$}_${RAND_SUFFIX}"
48
49# Check if magick command exists
50if ! command -v magick &>/dev/null; then
51 echo "Error: ImageMagick (magick command) not found. Please install it."
52 # Attempt cleanup before exiting
53 [ -d "$FRAMES_IN" ] && rm -rf "$FRAMES_IN"
54 [ -d "$FRAMES_OUT" ] && rm -rf "$FRAMES_OUT"
55 exit 1
56fi
57
58# Check if eink-2color.png exists
59if [ ! -f "eink-2color.png" ]; then
60 echo "Error: Colormap file 'eink-2color.png' not found in the current directory."
61 # Attempt cleanup before exiting
62 [ -d "$FRAMES_IN" ] && rm -rf "$FRAMES_IN"
63 [ -d "$FRAMES_OUT" ] && rm -rf "$FRAMES_OUT"
64 exit 1
65fi
66
67# Create temporary directories only after checks pass
68mkdir -p "$FRAMES_IN"
69if [ $? -ne 0 ]; then echo "Error: Could not create temporary directory $FRAMES_IN"; exit 1; fi
70mkdir -p "$FRAMES_OUT"
71if [ $? -ne 0 ]; then echo "Error: Could not create temporary directory $FRAMES_OUT"; rm -rf "$FRAMES_IN"; exit 1; fi
72
73
74# Extract frames from video and detect framerate
75FRAMERATE=$(ffprobe -v error -select_streams v -of default=noprint_wrappers=1:nokey=1 -show_entries stream=r_frame_rate "$INPUT_VIDEO" 2>/dev/null | bc -l | awk '{printf "%.2f", $0}')
76if [ -z "$FRAMERATE" ] || [ "$FRAMERATE" = "0.00" ]; then
77 echo "Error: Could not detect framerate for $INPUT_VIDEO."
78 cleanup_and_exit # Use cleanup function
79fi
80echo "Detected framerate: $FRAMERATE fps"
81
82# Extract frames from video (use -threads for faster extraction)
83echo "Extracting frames to $FRAMES_IN..."
84if ! ffmpeg -threads "$NUM_THREADS" -i "$INPUT_VIDEO" "$FRAMES_IN/frame_%04d.png" >/dev/null 2>&1; then
85 echo "Error: ffmpeg failed to extract frames from $INPUT_VIDEO."
86 cleanup_and_exit # Use cleanup function
87fi
88
89# Check if frames were extracted
90if ! ls "$FRAMES_IN"/frame_*.png > /dev/null 2>&1; then
91 echo "Error: No frames were extracted to $FRAMES_IN. Check input video and permissions."
92 cleanup_and_exit
93fi
94
95
96# Process frames in parallel using GNU Parallel
97if command -v parallel &>/dev/null; then
98 echo "Processing frames in parallel with $NUM_THREADS threads..."
99 # Need to export FRAMES_OUT for parallel to see it in subshells
100 export FRAMES_OUT
101 # Use parallel's built-in path manipulation {/} for basename
102 if ! find "$FRAMES_IN" -name "frame_*.png" | parallel --progress -j "$NUM_THREADS" \
103 "magick {} -dither FloydSteinberg -define dither:diffusion-amount=100% -remap eink-2color.png \"$FRAMES_OUT/{/}\""; then
104 echo "Error: Parallel processing with magick failed."
105 cleanup_and_exit
106 fi
107else
108 echo "GNU Parallel not found. Processing frames sequentially..."
109 # Process each frame with the dithering effect
110 processed_count=0
111 for frame in "$FRAMES_IN"/frame_*.png; do
112 # Check if the file exists before processing
113 if [ -f "$frame" ]; then
114 # Construct output path correctly using basename
115 output_frame="$FRAMES_OUT/$(basename "$frame")"
116 if ! magick "$frame" -dither FloydSteinberg -define dither:diffusion-amount=100% -remap eink-2color.png "$output_frame"; then
117 echo "Error: magick failed to process $frame."
118 # Decide if you want to stop on first error or continue
119 cleanup_and_exit # Stop on first error
120 # continue # Or uncomment to continue processing other frames
121 fi
122 # echo "Processed: $frame" # Can be too verbose
123 processed_count=$((processed_count + 1))
124 fi
125 done
126 echo "Processed $processed_count frames sequentially."
127 if [ $processed_count -eq 0 ]; then
128 echo "Error: No frames found to process sequentially in $FRAMES_IN"
129 cleanup_and_exit
130 fi
131fi
132
133# Check if output frames exist before attempting recombination
134if ! ls "$FRAMES_OUT"/frame_*.png > /dev/null 2>&1; then
135 echo "Error: No processed frames found in $FRAMES_OUT. Image processing likely failed."
136 cleanup_and_exit
137fi
138
139
140# Recombine frames into video with detected framerate (use -threads for faster encoding)
141echo "Recombining frames from $FRAMES_OUT..."
142# Allow ffmpeg to prompt for overwrite by removing output redirection
143if ! ffmpeg -framerate "$FRAMERATE" -threads "$NUM_THREADS" -i "$FRAMES_OUT/frame_%04d.png" -c:v libx264 -preset faster -pix_fmt yuv420p "$OUTPUT_VIDEO"; then
144 echo "Error: ffmpeg failed to recombine frames into $OUTPUT_VIDEO (or user cancelled overwrite)."
145 cleanup_and_exit # Use cleanup function
146fi
147
148# Use the cleanup function for consistency
149cleanup_and_exit() {
150 echo -e "\nCleaning up temporary files..."
151 # Only remove if directories were actually created
152 [ -n "$FRAMES_IN" ] && [ -d "$FRAMES_IN" ] && rm -rf "$FRAMES_IN"
153 [ -n "$FRAMES_OUT" ] && [ -d "$FRAMES_OUT" ] && rm -rf "$FRAMES_OUT"
154}
155# Call cleanup explicitly at the end (trap handles interruptions)
156cleanup_and_exit
157# Reset exit code to 0 for successful completion
158exit 0