""" 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}")