#!/usr/bin/env python3 import subprocess import json import os import sys import argparse def get_best_hwaccel(): """ Checks for available FFmpeg hardware acceleration methods and returns the best one. Priority is defined as: cuda > qsv > dxva2. Returns the name of the best available method, or None if none are found. """ priority = ['cuda', 'qsv', 'dxva2'] print("Checking for available hardware acceleration...") try: # Run ffmpeg to list available hardware acceleration methods result = subprocess.run( ['ffmpeg', '-hide_banner', '-hwaccels'], capture_output=True, text=True, encoding='utf-8', check=False ) if result.returncode != 0: print("Warning: Could not query FFmpeg for hardware acceleration. Using software decoding.") return None # Parse the output to get a list of available methods available_methods = result.stdout.strip().split('\n') if len(available_methods) > 1: available_methods = available_methods[1:] # Check for our preferred methods in order of priority for method in priority: if method in available_methods: print(f"Found best available hardware acceleration: {method}") return method print("No high-priority hardware acceleration (cuda, qsv, dxva2) found. Using software decoding.") return None except FileNotFoundError: # This will be handled by the main ffmpeg call later, so we just return None. return None except Exception as e: print(f"Warning: An error occurred while checking for hwaccel: {e}. Using software decoding.") return None def detect_scenes(video_path, hwaccel=None, threshold=0.4): """ Uses FFmpeg to detect scene changes in a video and saves the timestamps to a JSON file. """ print(f"Starting scene detection for: {os.path.basename(video_path)}") # Define the output JSON file path based on the input video name base_name = os.path.splitext(video_path)[0] json_output_path = f"{base_name}.scenes.json" # Base FFmpeg command command = ['ffmpeg', '-hide_banner'] # If hardware acceleration is specified, add the relevant flags if hwaccel: print(f"Attempting to use hardware acceleration: {hwaccel}") command.extend(['-hwaccel', hwaccel]) # Add the rest of the command arguments # The filter string is now built dynamically with the specified threshold filter_string = f"select='gt(scene,{threshold})',showinfo" command.extend([ '-i', video_path, '-vf', filter_string, '-f', 'null', '-' ]) # Execute the FFmpeg command try: # Use Popen to start the process and read its output in real-time process = subprocess.Popen(command, stderr=subprocess.PIPE, text=True, encoding='utf-8') scene_timestamps = [] # Read stderr line by line as it's produced for line in iter(process.stderr.readline, ''): # Check for scene detection output (which contains pts_time) if 'pts_time:' in line: timestamp_str = line.split('pts_time:')[1].split()[0] scene_timestamps.append(float(timestamp_str)) # Check for ffmpeg's progress line (which starts with 'frame=') # and print it to the actual stderr to show progress elif line.strip().startswith('frame='): # Use sys.stderr to avoid interfering with stdout prints # Use '\r' to have the line overwrite itself, mimicking ffmpeg's behavior sys.stderr.write(f'\r{line.strip()}') sys.stderr.flush() # Wait for the process to finish and get the return code process.wait() # Print a newline to move past the progress bar sys.stderr.write('\n') sys.stderr.flush() if process.returncode != 0: print(f"\nWarning: FFmpeg may have encountered an error (exit code: {process.returncode}).") if not scene_timestamps: print("Warning: No scenes were detected. The output file will be empty.") # Save the detected scenes to a JSON file with open(json_output_path, 'w') as json_file: json.dump(scene_timestamps, json_file, indent=4) print(f"Scene detection completed. {len(scene_timestamps)} scenes found.") print(f"Results saved to: {json_output_path}") except FileNotFoundError: print("Error: 'ffmpeg' command not found. Is it installed and in your system's PATH?") except Exception as e: print(f"An error occurred: {e}") def main(): """ Main function to handle command-line arguments. """ parser = argparse.ArgumentParser(description="Detect scene changes in a video using FFmpeg.") parser.add_argument("video_path", help="Path to the video file.") parser.add_argument( "-t", "--threshold", type=float, default=0.4, help="Set the scene detection threshold (0.0 to 1.0). Lower is more sensitive. Default: 0.4" ) args = parser.parse_args() if not os.path.isfile(args.video_path): print(f"Error: Input file not found: '{args.video_path}'") sys.exit(1) # Automatically determine the best hardware acceleration method hwaccel_method = get_best_hwaccel() detect_scenes(args.video_path, hwaccel_method, args.threshold) if __name__ == "__main__": main()