147 lines
5.5 KiB
Python
147 lines
5.5 KiB
Python
#!/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() |