Search code examples
pythonvideoffmpeg

How to automate ffmpeg to split and merge parts of video, and keep the audio in sync?


I have a Python script that automates trimming a large video (2 hours) into smaller segments and then concatenating them without re-encoding, to keep the process fast. The script runs these ffmpeg commands:

import subprocess

# Extract chunks
segments = [(0, 300), (300, 600), (600, 900)]  # example segments in seconds
for i, (start, length) in enumerate(segments):
    subprocess.run([
        "ffmpeg", "-i", "input.mp4", "-ss", str(start), "-t", str(length),
        "-c", "copy", "-reset_timestamps", "1", "-y", f"chunk_{i}.mp4"
    ], check=True)

# Create concat list
with open("list.txt", "w") as f:
    for i in range(len(segments)):
        f.write(f"file 'chunk_{i}.mp4'\n")

# Concatenate
subprocess.run([
    "ffmpeg", "-f", "concat", "-safe", "0",
    "-i", "list.txt", "-c", "copy", "-y", "merged_output.mp4"
], check=True)

All chunks come from the same source video, with identical codecs, resolution, and bitrate. Despite this, the final merged_output.mp4 sometimes has audio out of sync—especially after the first chunk.

I’ve tried using -ss before -i to cut on keyframes, but the issue persists.

Question: How can I ensure correct A/V sync in the final concatenated video when programmatically segmenting and merging via ffmpeg without fully re-encoding? Is there a way to adjust the ffmpeg commands or process to avoid audio desynchronization?


Solution

  • Solution is to use mkvmerge, and then copy back to mp4

    import subprocess
    import os
    
    
    # Function to convert seconds to HH:MM:SS format
    def convert_to_hms(seconds):
        hours = seconds // 3600
        minutes = (seconds % 3600) // 60
        secs = seconds % 60
        return f"{hours:02}:{minutes:02}:{secs:02}"
    
    
    # Segments defined as (start, end) in seconds
    segments = [(0, 300), (300, 600), (600, 900)]  # Example segments in seconds
    input_file = "input.mp4"
    base_name = os.path.splitext(input_file)[0]
    
    # Directory for temporary chunks
    temp_dir = "./temp_chunks"
    os.makedirs(temp_dir, exist_ok=True)
    
    # Normalize time and prepare mkvmerge parts
    split_parts = []
    for start, end in segments:
        start_hms = convert_to_hms(start)
        end_hms = convert_to_hms(end)
        split_parts.append(f"{start_hms}-{end_hms}")
    
    split_parts_str = ",".join(split_parts)
    split_file = os.path.join(temp_dir, f"{base_name}_split.mkv")
    
    # Split the input file into chunks using mkvmerge
    print(f"Running mkvmerge to split into parts: {split_parts_str}")
    subprocess.run([
        "mkvmerge", "-o", split_file, "--split", f"parts:{split_parts_str}", input_file
    ], check=True)
    
    # Check if the split parts exist
    part_files = sorted([
        os.path.join(temp_dir, f)
        for f in os.listdir(temp_dir)
        if f.startswith(f"{base_name}_split-") and f.endswith(".mkv")
    ])
    
    if not part_files:
        print("No split parts found. Exiting.")
        exit(1)
    
    # Merge the parts using mkvmerge
    merged_file = f"{base_name}_merged.mkv"
    merge_cmd = ["mkvmerge", "-o", merged_file]
    merge_cmd.extend(part_files)
    
    print(f"Merging files: {part_files}")
    subprocess.run(merge_cmd, check=True)
    
    # Convert the merged MKV file to MP4 using ffmpeg
    output_file = f"{base_name}_processed.mp4"
    print(f"Converting merged file to MP4: {output_file}")
    subprocess.run([
        "ffmpeg", "-i", merged_file, "-c", "copy", "-y", output_file
    ], check=True)
    
    # Cleanup temporary files
    for f in part_files:
        os.remove(f)
    os.rmdir(temp_dir)
    
    print(f"Processed video saved to {output_file}")