AI AGENT SKILLS

RenderIO ffmpeg

一个面向 Data & APIs 场景的 Agent 技能。原始说明:Use the RenderIO FFmpeg-as-a-Service API to process video, audio, and images in the cloud. Covers submitting commands, polling results, uploading files, chai...

SKILL.md

skill.md


name: renderio-api
description: Use the RenderIO FFmpeg-as-a-Service API to process video, audio, and images in the cloud. Covers submitting commands, polling results, uploading files, chained workflows, and webhook delivery. Use this skill whenever the user wants to convert, resize, compress, trim, watermark, extract audio from, or otherwise process media via the RenderIO API.
allowed-tools: Read, Write, Edit, Bash, WebFetch


RenderIO API Skill

RenderIO is an FFmpeg-as-a-Service REST API. You send an FFmpeg command over HTTP; RenderIO runs it in a secure cloud sandbox, stores outputs automatically, and returns signed download URLs.

Setup

# Store API key as environment variable
export RENDERIO_API_KEY="ffsk_your_api_key_here"

Get a free API key at renderio.dev/get-api-key.

The three rules that must never be broken

  1. Double braces for placeholders — use {{in_video}} not {in_video}
  2. Key prefixes — input keys start with in_, output keys start with out_
  3. Every key used in the command must be declared, and every declared key must appear in the command
// CORRECT
{
  "ffmpeg_command": "-i {{in_video}} -c:v libx264 {{out_video}}",
  "input_files": { "in_video": "https://example.com/video.mp4" },
  "output_files": { "out_video": "result.mp4" }
}

// WRONG — single braces, missing out_ prefix
{
  "ffmpeg_command": "-i {video} result.mp4",
  "input_files": { "video": "https://example.com/video.mp4" }
}

Core workflow

1. Submit a command

curl -X POST https://renderio.dev/api/v1/run-ffmpeg-command \
  -H "X-API-KEY: $RENDERIO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "ffmpeg_command": "-i {{in_video}} -vf scale=1280:720 -c:v libx264 -crf 23 {{out_video}}",
    "input_files": { "in_video": "https://example.com/input.mp4" },
    "output_files": { "out_video": "output-720p.mp4" }
  }'

Response: { "command_id": "a1b2c3d4-..." }

2. Poll for completion

curl https://renderio.dev/api/v1/commands/$COMMAND_ID \
  -H "X-API-KEY: $RENDERIO_API_KEY"

Status values: QUEUEDPROCESSINGSUCCESS or FAILED (always uppercase).

3. Get output URL

result.output_files.out_video.storage_url

TypeScript implementation

const API_KEY = process.env.RENDERIO_API_KEY!;
const BASE = "https://api.renderio.dev";

interface CommandResult {
  command_id: string;
  status: "QUEUED" | "PROCESSING" | "SUCCESS" | "FAILED";
  output_files: Record<string, {
    storage_url: string;
    filename: string;
    size_mbytes: number;
    duration?: number;
    codec?: string;
    width?: number;
    height?: number;
  }>;
  total_processing_seconds?: number;
  error?: string;
}

async function runFFmpeg(
  command: string,
  inputFiles: Record<string, string>,
  outputFiles: Record<string, string>,
): Promise<CommandResult> {
  const submitRes = await fetch(`${BASE}/api/v1/run-ffmpeg-command`, {
    method: "POST",
    headers: { "X-API-KEY": API_KEY, "Content-Type": "application/json" },
    body: JSON.stringify({
      ffmpeg_command: command,
      input_files: inputFiles,
      output_files: outputFiles,
    }),
  });

  if (!submitRes.ok) {
    const err = await submitRes.json();
    throw new Error(`Submit failed: ${err.message}`);
  }

  const { command_id } = await submitRes.json();

  // Poll with 2s interval
  while (true) {
    await new Promise((r) => setTimeout(r, 2000));

    const pollRes = await fetch(`${BASE}/api/v1/commands/${command_id}`, {
      headers: { "X-API-KEY": API_KEY },
    });
    const result: CommandResult = await pollRes.json();

    if (result.status === "SUCCESS") return result;
    if (result.status === "FAILED") {
      throw new Error(`Processing failed: ${result.error ?? "unknown"}`);
    }
  }
}

// Usage
const result = await runFFmpeg(
  "-i {{in_video}} -vf scale=1280:720 -c:v libx264 -crf 23 {{out_video}}",
  { in_video: "https://example.com/input.mp4" },
  { out_video: "output-720p.mp4" },
);

console.log(result.output_files.out_video.storage_url);

Python implementation

import os
import time
import requests
from typing import Any

API_KEY = os.environ["RENDERIO_API_KEY"]
BASE = "https://api.renderio.dev"

def run_ffmpeg(
    command: str,
    input_files: dict[str, str],
    output_files: dict[str, str],
) -> dict[str, Any]:
    res = requests.post(
        f"{BASE}/api/v1/run-ffmpeg-command",
        headers={"X-API-KEY": API_KEY},
        json={
            "ffmpeg_command": command,
            "input_files": input_files,
            "output_files": output_files,
        },
    )
    res.raise_for_status()
    command_id = res.json()["command_id"]

    while True:
        time.sleep(2)
        result = requests.get(
            f"{BASE}/api/v1/commands/{command_id}",
            headers={"X-API-KEY": API_KEY},
        ).json()

        if result["status"] == "SUCCESS":
            return result
        if result["status"] == "FAILED":
            raise RuntimeError(f"Processing failed: {result.get('error', 'unknown')}")

# Usage
result = run_ffmpeg(
    "-i {{in_video}} -vf scale=1280:720 -c:v libx264 -crf 23 {{out_video}}",
    {"in_video": "https://example.com/input.mp4"},
    {"out_video": "output-720p.mp4"},
)
print(result["output_files"]["out_video"]["storage_url"])

Common FFmpeg recipes

Copy-paste ready. Replace URLs and filenames.

Convert MP4 to WebM

{
  "ffmpeg_command": "-i {{in_video}} -c:v libvpx-vp9 -crf 30 -b:v 0 {{out_video}}",
  "input_files": { "in_video": "https://example.com/input.mp4" },
  "output_files": { "out_video": "output.webm" }
}

Resize video to 720p

{
  "ffmpeg_command": "-i {{in_video}} -vf scale=1280:720 -c:v libx264 -crf 23 {{out_video}}",
  "input_files": { "in_video": "https://example.com/input.mp4" },
  "output_files": { "out_video": "720p.mp4" }
}

Extract audio as MP3

{
  "ffmpeg_command": "-i {{in_video}} -vn -acodec libmp3lame -ab 192k {{out_audio}}",
  "input_files": { "in_video": "https://example.com/input.mp4" },
  "output_files": { "out_audio": "audio.mp3" }
}

Compress video (reduce file size)

{
  "ffmpeg_command": "-i {{in_video}} -c:v libx264 -crf 28 -preset slow {{out_video}}",
  "input_files": { "in_video": "https://example.com/input.mp4" },
  "output_files": { "out_video": "compressed.mp4" }
}

Generate thumbnail at 5 seconds

{
  "ffmpeg_command": "-i {{in_video}} -ss 5 -vframes 1 {{out_thumb}}",
  "input_files": { "in_video": "https://example.com/input.mp4" },
  "output_files": { "out_thumb": "thumbnail.jpg" }
}

Trim video (10s to 30s)

{
  "ffmpeg_command": "-i {{in_video}} -ss 10 -to 30 -c copy {{out_video}}",
  "input_files": { "in_video": "https://example.com/input.mp4" },
  "output_files": { "out_video": "trimmed.mp4" }
}

Add image watermark

{
  "ffmpeg_command": "-i {{in_video}} -i {{in_logo}} -filter_complex \"overlay=10:10\" {{out_video}}",
  "input_files": {
    "in_video": "https://example.com/input.mp4",
    "in_logo": "https://example.com/logo.png"
  },
  "output_files": { "out_video": "watermarked.mp4" }
}

Convert to GIF (480px wide, 12fps)

{
  "ffmpeg_command": "-i {{in_video}} -vf \"fps=12,scale=480:-1:flags=lanczos\" {{out_gif}}",
  "input_files": { "in_video": "https://example.com/input.mp4" },
  "output_files": { "out_gif": "output.gif" }
}

Mute video (remove audio track)

{
  "ffmpeg_command": "-i {{in_video}} -an -c:v copy {{out_video}}",
  "input_files": { "in_video": "https://example.com/input.mp4" },
  "output_files": { "out_video": "muted.mp4" }
}

Stack two videos side by side

{
  "ffmpeg_command": "-i {{in_left}} -i {{in_right}} -filter_complex \"[0:v][1:v]hstack=inputs=2\" {{out_video}}",
  "input_files": {
    "in_left": "https://example.com/left.mp4",
    "in_right": "https://example.com/right.mp4"
  },
  "output_files": { "out_video": "side-by-side.mp4" }
}

Upload a local file first

When the user has a local file (not a URL), upload it first:

async function uploadFile(filePath: string): Promise<string> {
  const form = new FormData();
  form.append("file", new Blob([await fs.readFile(filePath)]), path.basename(filePath));

  const res = await fetch("https://renderio.dev/api/v1/files/upload", {
    method: "POST",
    headers: { "X-API-KEY": API_KEY },
    body: form,
  });

  const data = await res.json();
  return data.storage_url; // use this as the input_files value
}

Chained commands (sequential pipeline)

Use the output of one step as the input of the next. Reference previous outputs with {{out_key}} in the next step's input_files.

POST /api/v1/run-chained-ffmpeg-commands
{
  "commands": [
    {
      "ffmpeg_command": "-i {{in_video}} -vf scale=1280:720 {{out_resized}}",
      "input_files": { "in_video": "https://example.com/input.mp4" },
      "output_files": { "out_resized": "resized.mp4" }
    },
    {
      "ffmpeg_command": "-i {{in_resized}} -c:v libx264 -crf 28 {{out_final}}",
      "input_files": { "in_resized": "{{out_resized}}" },
      "output_files": { "out_final": "final.mp4" }
    }
  ]
}

Parallel commands (multiple independent operations)

POST /api/v1/run-multiple-ffmpeg-commands
{
  "commands": [
    {
      "ffmpeg_command": "-i {{in_video}} -vf scale=1920:1080 {{out_1080p}}",
      "input_files": { "in_video": "https://example.com/input.mp4" },
      "output_files": { "out_1080p": "1080p.mp4" }
    },
    {
      "ffmpeg_command": "-i {{in_video}} -vf scale=1280:720 {{out_720p}}",
      "input_files": { "in_video": "https://example.com/input.mp4" },
      "output_files": { "out_720p": "720p.mp4" }
    }
  ]
}

Webhook setup (alternative to polling)

# Configure once
curl -X PUT https://renderio.dev/api/v1/webhook-config \
  -H "X-API-KEY: $RENDERIO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"url": "https://your-server.com/renderio-webhook"}'

# Payload your endpoint receives on completion:
# {
#   "data": {
#     "command_id": "...",
#     "status": "SUCCESS",
#     "output_files": { "out_video": { "storage_url": "...", ... } }
#   },
#   "timestamp": 1712345678000
# }

Error reference

| Code | Meaning | Fix |
|---|---|---|
| 400 | Bad request | Check placeholder syntax and key prefixes |
| 401 | Unauthorized | Check X-API-KEY header and key validity |
| 429 | Rate limited | Wait Retry-After seconds and retry |
| 404 | Not found | Check command_id or file_id |
| 500 | Server error | Safe to retry after a short delay |

Checklist when generating code for users

  • [ ] RENDERIO_API_KEY stored as environment variable, never hardcoded
  • [ ] Placeholder syntax uses {{double_braces}}
  • [ ] Input keys start with in_, output keys start with out_
  • [ ] Poll loop handles QUEUED, PROCESSING, SUCCESS, FAILED
  • [ ] Output URL accessed via output_files.out_key.storage_url
  • [ ] Error handling for both HTTP errors and FAILED status
  • [ ] Domain is api.renderio.dev (not any other domain)