110 lines
4.1 KiB
Plaintext
110 lines
4.1 KiB
Plaintext
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
|