$ cat node-template.py

Video Add Video Overlay

// Overlays a video onto another video with positioning, scaling, optional cropping, and optional chromakey (green-screen removal).

Process
Video
template.py
1import os2import sys3import json4import subprocess5import traceback67INPUT_DIR = "/data/input"8OUTPUT_DIR = "/data/output"910FFMPEG_IMAGE = "jrottenberg/ffmpeg:7-ubuntu"111213def get_overlay_duration(host_input, overlay_filename):14    """Use ffprobe to get the duration of the overlay video."""15    cmd = [16        "docker", "run", "--rm",17        "--network", "none",18        "--memory", "512m",19        "--cpus", "1.0",20        "-v", f"{host_input}:/data/input:ro",21        FFMPEG_IMAGE,22        "-v", "error",23        "-show_entries", "format=duration",24        "-of", "default=noprint_wrappers=1:nokey=1",25        "/data/input/" + overlay_filename,26    ]27    # Replace the entrypoint with ffprobe28    cmd.insert(4, "--entrypoint")29    cmd.insert(5, "ffprobe")30    # Remove the FFMPEG_IMAGE from its current position and reinsert after entrypoint args31    # Rebuild command properly32    cmd = [33        "docker", "run", "--rm",34        "--network", "none",35        "--memory", "512m",36        "--cpus", "1.0",37        "--entrypoint", "ffprobe",38        "-v", f"{host_input}:/data/input:ro",39        FFMPEG_IMAGE,40        "-v", "error",41        "-show_entries", "format=duration",42        "-of", "default=noprint_wrappers=1:nokey=1",43        "/data/input/" + overlay_filename,44    ]45    result = subprocess.run(cmd, capture_output=True, text=True)46    if result.returncode != 0:47        raise RuntimeError(f"ffprobe failed: {result.stderr}")48    return float(result.stdout.strip())495051def main():52    execution_input = json.loads(sys.stdin.read())53    inputs = execution_input.get("inputs", {})5455    # Required inputs56    video = inputs["video"]57    overlay_video = inputs["overlay_video"]5859    # Position and scale60    x = inputs.get("x", 0)61    y = inputs.get("y", 0)62    video_scale = inputs.get("video_scale", 200)6364    # Duration and audio65    output_duration = inputs.get("output_duration", "video_1")66    audio_source = inputs.get("audio_source", "video_1")6768    # Crop options69    crop = inputs.get("crop", "no")70    crop_width = inputs.get("crop_width", 640)71    crop_height = inputs.get("crop_height", 360)7273    # Chromakey options74    chromakey = inputs.get("chromakey", "no")75    chromakey_color = inputs.get("chromakey_color", "00FF00")76    chromakey_similarity = inputs.get("chromakey_similarity", 0.3)77    chromakey_blend = inputs.get("chromakey_blend", 0.15)7879    os.makedirs(OUTPUT_DIR, exist_ok=True)8081    # Host staging paths for sibling Docker container82    host_input = os.environ.get("HOST_STAGING_INPUT", INPUT_DIR)83    host_output = os.environ.get("HOST_STAGING_OUTPUT", OUTPUT_DIR)8485    # Build filter chain for overlay video [1:v]86    overlay_filters = []8788    if chromakey == "yes":89        overlay_filters.append(90            f"colorkey=0x{chromakey_color}:{chromakey_similarity}:{chromakey_blend}"91        )9293    if crop == "yes":94        overlay_filters.append(95            f"crop={crop_width}:{crop_height}:(in_w-{crop_width})/2:(in_h-{crop_height})/2"96        )9798    overlay_filters.append(f"scale=trunc({video_scale}/2)*2:-2")99100    # Two-stage shell script approach to avoid heap corruption in ffmpeg 7's101    # overlay filter with simultaneous dual video decoders in Docker.102    # Stage 1 normalizes the overlay, Stage 2 composites onto the base.103    video_path = f"/data/input/{video}"104    overlay_path = f"/data/input/{overlay_video}"105    scale_filter = ",".join(overlay_filters)106107    # Stage 1: normalize overlay video (apply crop/chromakey/scale to temp file)108    stage1 = (109        f"ffmpeg -y -threads 4 -i '{overlay_path}' "110        f"-vf '{scale_filter}' "111        f"-c:v libx264 -preset veryfast -crf 18 -pix_fmt yuv420p "112        f"-c:a aac -ar 44100 -ac 2 "113        f"/tmp/_overlay_scaled.mp4"114    )115116    # Stage 2: overlay scaled video onto base117    if audio_source == "mix":118        overlay_fc = f"[0:v][1:v]overlay={x}:{y}[vout];[0:a][1:a]amix=inputs=2:duration=longest[aout]"119        audio_cmd = "-map '[vout]' -map '[aout]'"120    elif audio_source == "video_2":121        overlay_fc = f"[0:v][1:v]overlay={x}:{y}[vout]"122        audio_cmd = "-map '[vout]' -map 1:a -codec:a copy"123    else:124        overlay_fc = f"[0:v][1:v]overlay={x}:{y}[vout]"125        audio_cmd = "-map '[vout]' -map 0:a -codec:a copy"126127    duration_cmd = ""128    if output_duration == "video_2":129        dur = get_overlay_duration(host_input, overlay_video)130        duration_cmd = f"-t {dur}"131132    stage2 = (133        f"ffmpeg -y -threads 4 -i '{video_path}' -i /tmp/_overlay_scaled.mp4 "134        f"-filter_complex '{overlay_fc}' "135        f"{audio_cmd} "136        f"-c:v libx264 -preset veryfast -crf 23 -pix_fmt yuv420p "137        f"-movflags +faststart "138        f"{duration_cmd} "139        f"/data/output/output.mp4"140    )141142    shell_script = f"set -e\n{stage1}\n{stage2}\n"143144    cmd = [145        "docker", "run", "--rm",146        "--network", "none",147        "--memory", "4g",148        "--cpus", "4.0",149        "--cpuset-cpus", "0-3",150        "-v", f"{host_input}:/data/input:ro",151        "-v", f"{host_output}:/data/output:rw",152        "--entrypoint", "sh",153        FFMPEG_IMAGE,154        "-c", shell_script,155    ]156157    result = subprocess.run(cmd, capture_output=True, text=True, timeout=1800)158    if result.returncode != 0:159        raise RuntimeError(f"ffmpeg failed (exit {result.returncode}): {result.stderr[-2000:]}")160161    print(json.dumps({"video": "output.mp4"}, indent=2))162163164if __name__ == "__main__":165    try:166        main()167    except Exception as e:168        print(json.dumps({169            "error": str(e),170            "errorType": type(e).__name__,171            "traceback": traceback.format_exc(),172        }), file=sys.stderr)173        sys.exit(1)