149 lines
4.6 KiB
Python
149 lines
4.6 KiB
Python
"""
|
|
AWS Lambda handler for media transcoding.
|
|
|
|
Receives a job payload from Step Functions, downloads source from S3,
|
|
runs FFmpeg, uploads result to S3, and calls back to the API.
|
|
|
|
Uses the same core/ffmpeg module as the local Celery worker.
|
|
"""
|
|
|
|
import json
|
|
import logging
|
|
import os
|
|
import tempfile
|
|
from pathlib import Path
|
|
|
|
import boto3
|
|
import requests
|
|
|
|
logger = logging.getLogger()
|
|
logger.setLevel(logging.INFO)
|
|
|
|
# S3 config
|
|
S3_BUCKET_IN = os.environ.get("S3_BUCKET_IN", "mpr-media-in")
|
|
S3_BUCKET_OUT = os.environ.get("S3_BUCKET_OUT", "mpr-media-out")
|
|
AWS_REGION = os.environ.get("AWS_REGION", "us-east-1")
|
|
|
|
s3 = boto3.client("s3", region_name=AWS_REGION)
|
|
|
|
|
|
def handler(event, context):
|
|
"""
|
|
Lambda entry point.
|
|
|
|
Event payload (from Step Functions):
|
|
{
|
|
"job_id": "uuid",
|
|
"source_key": "path/to/source.mp4",
|
|
"output_key": "output_filename.mp4",
|
|
"preset": {...} or null,
|
|
"trim_start": float or null,
|
|
"trim_end": float or null,
|
|
"duration": float or null,
|
|
"callback_url": "https://mpr.mcrn.ar/api",
|
|
"api_key": "secret"
|
|
}
|
|
"""
|
|
job_id = event["job_id"]
|
|
source_key = event["source_key"]
|
|
output_key = event["output_key"]
|
|
preset = event.get("preset")
|
|
trim_start = event.get("trim_start")
|
|
trim_end = event.get("trim_end")
|
|
duration = event.get("duration")
|
|
callback_url = event.get("callback_url", "")
|
|
api_key = event.get("api_key", "")
|
|
|
|
logger.info(f"Starting job {job_id}: {source_key} -> {output_key}")
|
|
|
|
# Download source from S3
|
|
ext_in = Path(source_key).suffix or ".mp4"
|
|
tmp_source = tempfile.mktemp(suffix=ext_in, dir="/tmp")
|
|
logger.info(f"Downloading s3://{S3_BUCKET_IN}/{source_key}")
|
|
s3.download_file(S3_BUCKET_IN, source_key, tmp_source)
|
|
|
|
# Prepare output temp file
|
|
ext_out = Path(output_key).suffix or ".mp4"
|
|
tmp_output = tempfile.mktemp(suffix=ext_out, dir="/tmp")
|
|
|
|
try:
|
|
# Import ffmpeg module (bundled in container)
|
|
from core.ffmpeg.transcode import TranscodeConfig, transcode
|
|
|
|
if preset:
|
|
config = TranscodeConfig(
|
|
input_path=tmp_source,
|
|
output_path=tmp_output,
|
|
video_codec=preset.get("video_codec", "libx264"),
|
|
video_bitrate=preset.get("video_bitrate"),
|
|
video_crf=preset.get("video_crf"),
|
|
video_preset=preset.get("video_preset"),
|
|
resolution=preset.get("resolution"),
|
|
framerate=preset.get("framerate"),
|
|
audio_codec=preset.get("audio_codec", "aac"),
|
|
audio_bitrate=preset.get("audio_bitrate"),
|
|
audio_channels=preset.get("audio_channels"),
|
|
audio_samplerate=preset.get("audio_samplerate"),
|
|
container=preset.get("container", "mp4"),
|
|
extra_args=preset.get("extra_args", []),
|
|
trim_start=trim_start,
|
|
trim_end=trim_end,
|
|
)
|
|
else:
|
|
config = TranscodeConfig(
|
|
input_path=tmp_source,
|
|
output_path=tmp_output,
|
|
video_codec="copy",
|
|
audio_codec="copy",
|
|
trim_start=trim_start,
|
|
trim_end=trim_end,
|
|
)
|
|
|
|
success = transcode(config, duration=duration)
|
|
|
|
if not success:
|
|
raise RuntimeError("Transcode returned False")
|
|
|
|
# Upload result to S3
|
|
logger.info(f"Uploading s3://{S3_BUCKET_OUT}/{output_key}")
|
|
s3.upload_file(tmp_output, S3_BUCKET_OUT, output_key)
|
|
|
|
result = {"status": "completed", "job_id": job_id, "output_key": output_key}
|
|
|
|
# Callback to API
|
|
_callback(callback_url, job_id, api_key, {"status": "completed"})
|
|
|
|
return result
|
|
|
|
except Exception as e:
|
|
logger.exception(f"Job {job_id} failed: {e}")
|
|
|
|
_callback(callback_url, job_id, api_key, {
|
|
"status": "failed",
|
|
"error": str(e),
|
|
})
|
|
|
|
return {"status": "failed", "job_id": job_id, "error": str(e)}
|
|
|
|
finally:
|
|
for f in [tmp_source, tmp_output]:
|
|
try:
|
|
os.unlink(f)
|
|
except OSError:
|
|
pass
|
|
|
|
|
|
def _callback(callback_url, job_id, api_key, payload):
|
|
"""Call back to API with job result."""
|
|
if not callback_url:
|
|
return
|
|
try:
|
|
url = f"{callback_url}/jobs/{job_id}/callback"
|
|
headers = {}
|
|
if api_key:
|
|
headers["X-API-Key"] = api_key
|
|
resp = requests.post(url, json=payload, headers=headers, timeout=10)
|
|
logger.info(f"Callback response: {resp.status_code}")
|
|
except Exception as e:
|
|
logger.warning(f"Callback failed: {e}")
|