Timelapse does compile
This commit is contained in:
parent
225728d624
commit
e0d50cd496
1 changed files with 70 additions and 65 deletions
|
|
@ -1,7 +1,9 @@
|
||||||
import os
|
import os
|
||||||
import subprocess
|
import subprocess
|
||||||
import logging
|
import logging
|
||||||
from typing import Callable, List
|
import tempfile
|
||||||
|
import shutil
|
||||||
|
from typing import Callable
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
@ -16,86 +18,89 @@ def compile_timelapse(
|
||||||
Compile a timelapse video from a folder of images using ffmpeg.
|
Compile a timelapse video from a folder of images using ffmpeg.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
image_folder: Path to folder containing timestamped JPEG images
|
image_folder: Path to folder containing timestamped JPEG images.
|
||||||
output_path: Path where the output video should be saved
|
output_path: Path where the output video should be saved.
|
||||||
framerate: Desired frames per second
|
framerate: Desired frames per second.
|
||||||
update_progress: Callback function to report progress (current: int, total: int)
|
update_progress: Callback function to report progress (current: int, total: int).
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
True if successful, False otherwise
|
True if successful, False otherwise.
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
# Get list of image files and sort by timestamp
|
# List and sort JPEG image files in the folder.
|
||||||
image_files = sorted([
|
image_files = sorted([
|
||||||
f for f in os.listdir(image_folder)
|
f for f in os.listdir(image_folder)
|
||||||
if f.lower().endswith(('.jpg', '.jpeg'))
|
if f.lower().endswith(('.jpg', '.jpeg'))
|
||||||
])
|
])
|
||||||
|
|
||||||
if not image_files:
|
total_frames = len(image_files)
|
||||||
|
if total_frames == 0:
|
||||||
logger.error("No image files found in folder")
|
logger.error("No image files found in folder")
|
||||||
update_progress(0, 0)
|
update_progress(0, 0)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
total_frames = len(image_files)
|
|
||||||
logger.info(f"Found {total_frames} images to process")
|
logger.info(f"Found {total_frames} images to process")
|
||||||
|
|
||||||
# Create ffmpeg input file list
|
# Create a temporary directory to hold sequential files.
|
||||||
input_file = os.path.join(image_folder, "input.txt")
|
temp_dir = tempfile.mkdtemp()
|
||||||
frame_duration = 1.0 / framerate # Calculate duration based on requested framerate
|
logger.info(f"Created temporary directory: {temp_dir}")
|
||||||
|
|
||||||
with open(input_file, "w") as f:
|
try:
|
||||||
for img in image_files:
|
for idx, img in enumerate(image_files):
|
||||||
f.write(f"file '{os.path.join(image_folder, img)}'\n")
|
src = os.path.join(image_folder, img)
|
||||||
f.write(f"duration {frame_duration}\n")
|
dst = os.path.join(temp_dir, f"{idx:06d}.jpg")
|
||||||
|
|
||||||
# Build ffmpeg command
|
|
||||||
cmd = [
|
|
||||||
"ffmpeg",
|
|
||||||
"-f", "concat",
|
|
||||||
"-safe", "0",
|
|
||||||
"-i", input_file,
|
|
||||||
"-framerate", str(framerate),
|
|
||||||
"-c:v", "libx264",
|
|
||||||
"-pix_fmt", "yuv420p",
|
|
||||||
"-y", # Overwrite output file if it exists
|
|
||||||
output_path
|
|
||||||
]
|
|
||||||
|
|
||||||
# Run ffmpeg with progress reporting
|
|
||||||
process = subprocess.Popen(
|
|
||||||
cmd,
|
|
||||||
stdout=subprocess.PIPE,
|
|
||||||
stderr=subprocess.PIPE,
|
|
||||||
universal_newlines=True
|
|
||||||
)
|
|
||||||
|
|
||||||
# Monitor progress
|
|
||||||
frame_count = 0
|
|
||||||
for line in process.stderr:
|
|
||||||
if "frame=" in line:
|
|
||||||
try:
|
try:
|
||||||
# Extract frame number from ffmpeg output
|
os.symlink(src, dst)
|
||||||
frame = int(line.split("frame=")[1].split()[0])
|
except (AttributeError, NotImplementedError, OSError):
|
||||||
frame_count = min(frame, total_frames) # Ensure we don't exceed total
|
# Fallback to copying if symlinking is not supported.
|
||||||
update_progress(frame_count, total_frames)
|
shutil.copy2(src, dst)
|
||||||
except (IndexError, ValueError):
|
|
||||||
continue
|
|
||||||
|
|
||||||
# Wait for process to complete
|
|
||||||
process.wait()
|
|
||||||
|
|
||||||
# Clean up temporary file
|
|
||||||
os.remove(input_file)
|
|
||||||
|
|
||||||
if process.returncode == 0:
|
|
||||||
update_progress(total_frames, total_frames)
|
|
||||||
return True
|
|
||||||
else:
|
|
||||||
logger.error(f"ffmpeg failed with return code {process.returncode}")
|
|
||||||
update_progress(0, total_frames)
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
input_pattern = os.path.join(temp_dir, "%06d.jpg")
|
||||||
|
cmd = [
|
||||||
|
"ffmpeg",
|
||||||
|
"-framerate", str(framerate),
|
||||||
|
"-i", input_pattern,
|
||||||
|
"-c:v", "libx264",
|
||||||
|
"-pix_fmt", "yuv420p",
|
||||||
|
"-y", # Overwrite output file if it exists
|
||||||
|
output_path
|
||||||
|
]
|
||||||
|
logger.info("Running ffmpeg command: " + " ".join(cmd))
|
||||||
|
|
||||||
|
# Run ffmpeg and capture the stderr for progress updates.
|
||||||
|
process = subprocess.Popen(
|
||||||
|
cmd,
|
||||||
|
stdout=subprocess.PIPE,
|
||||||
|
stderr=subprocess.PIPE,
|
||||||
|
universal_newlines=True
|
||||||
|
)
|
||||||
|
|
||||||
|
frame_count = 0
|
||||||
|
for line in process.stderr:
|
||||||
|
if "frame=" in line:
|
||||||
|
try:
|
||||||
|
frame = int(line.split("frame=")[1].split()[0])
|
||||||
|
frame_count = min(frame, total_frames)
|
||||||
|
update_progress(frame_count, total_frames)
|
||||||
|
except (IndexError, ValueError):
|
||||||
|
continue
|
||||||
|
|
||||||
|
process.wait()
|
||||||
|
|
||||||
|
if process.returncode == 0:
|
||||||
|
update_progress(total_frames, total_frames)
|
||||||
|
logger.info("Timelapse video created successfully")
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
logger.error(f"ffmpeg failed with return code {process.returncode}")
|
||||||
|
update_progress(0, total_frames)
|
||||||
|
return False
|
||||||
|
|
||||||
|
finally:
|
||||||
|
shutil.rmtree(temp_dir)
|
||||||
|
logger.info(f"Removed temporary directory: {temp_dir}")
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error compiling timelapse: {str(e)}")
|
logger.error(f"Error compiling timelapse: {str(e)}")
|
||||||
update_progress(0, total_frames if 'total_frames' in locals() else 0)
|
update_progress(0, total_frames if 'total_frames' in locals() else 0)
|
||||||
return False
|
return False
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue