import json import os import asyncio import tempfile segments = json.loads(params_kw.get('segments', '[]')) transitions = params_kw.get('transitions', 'fade') output_name = params_kw.get('output_name', 'merged_output.mp4') output_dir = os.path.join('/tmp/ffmpeg_output', output_name) os.makedirs(output_dir, exist_ok=True) output_path = os.path.join(output_dir, 'output.mp4') try: if not segments or len(segments) == 0: result = json.dumps({"status": "error", "error": "No segments provided"}) elif len(segments) == 1: # Single segment, just copy seg = segments[0] proc = await asyncio.create_subprocess_exec( 'ffmpeg', '-y', '-i', seg['path'], '-c', 'copy', output_path, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE ) stdout, stderr = await proc.communicate() if proc.returncode == 0: result = json.dumps({"status": "success", "output_path": output_path}) else: result = json.dumps({"status": "error", "error": stderr.decode()}) elif transitions == 'none' or len(segments) == 1: # Simple concat demuxer (no transitions) concat_list = os.path.join(output_dir, 'concat_list.txt') with open(concat_list, 'w') as f: for seg in segments: f.write(f"file '{seg['path']}'\n") proc = await asyncio.create_subprocess_exec( 'ffmpeg', '-y', '-f', 'concat', '-safe', '0', '-i', concat_list, '-c', 'copy', output_path, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE ) stdout, stderr = await proc.communicate() if proc.returncode == 0: result = json.dumps({"status": "success", "output_path": output_path}) else: result = json.dumps({"status": "error", "error": stderr.decode()}) else: # Crossfade transitions using xfade filter # Build complex xfade filter chain n = len(segments) fade_duration = 1.0 # 1 second crossfade inputs = [] for seg in segments: inputs.extend(['-i', seg['path']]) # Build xfade filter chain filter_parts = [] # Calculate offsets: each offset is cumulative duration minus fade overlaps offsets = [] cumulative = float(segments[0]['duration']) for i in range(1, n): offset = cumulative - fade_duration offsets.append(offset) cumulative = offset + float(segments[i]['duration']) if n == 2: filter_str = f'[0:v][1:v]xfade=transition=fade:duration={fade_duration}:offset={offsets[0]}[vout];[0:a][1:a]acrossfade=d={fade_duration}[aout]' else: # Chain xfade filters for multiple segments prev_label = '0:v' audio_prev = '0:a' vfilters = [] afilters = [] for i in range(1, n): out_label = f'v{i}' if i < n - 1 else 'vout' audio_out = f'a{i}' if i < n - 1 else 'aout' vfilters.append(f'[{prev_label}][{i}:v]xfade=transition=fade:duration={fade_duration}:offset={offsets[i-1]}[{out_label}]') afilters.append(f'[{audio_prev}][{i}:a]acrossfade=d={fade_duration}[{audio_out}]') prev_label = out_label audio_prev = audio_out filter_str = ';'.join(vfilters + afilters) cmd = ['ffmpeg', '-y'] + inputs + [ '-filter_complex', filter_str, '-map', '[vout]', '-map', '[aout]', '-c:v', 'libx264', '-c:a', 'aac', output_path ] proc = await asyncio.create_subprocess_exec( *cmd, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE ) stdout, stderr = await proc.communicate() if proc.returncode == 0: result = json.dumps({"status": "success", "output_path": output_path}) else: result = json.dumps({"status": "error", "error": stderr.decode()}) except Exception as e: result = json.dumps({"status": "error", "error": str(e)}) return result