Video editing from Ruby on Rails, without FFmpeg on your machine.
Trim, resize, compress, convert, caption and transcribe by calling one HTTP endpoint per operation. Parameters are validated before anything renders, so a bad call fails as a typed error rather than a broken encode.
What running FFmpeg from Rails usually costs
- The gems that wrap FFmpeg shell out to a binary your host has to provide, so a managed platform means a custom buildpack or a Docker image you now maintain.
- A render inside a request blocks a worker for as long as the encode takes, which is measured in whole seconds rather than milliseconds.
- Moving it to a background job helps the request and not the box: the encode still competes with the app for the same CPU.
Trim a clip from a background job
Ruby on Rails
require 'net/http'
require 'json'
class KinopipeClient
BASE = URI('https://kinopipe.com/api/v1')
def initialize(key = ENV.fetch('KINOPIPE_API_KEY'))
@headers = { 'Authorization' => "Bearer #{key}", 'Content-Type' => 'application/json' }
end
def run(tool, inputs, options = {})
started = post("/tools/#{tool}", inputs: inputs, options: options)
loop do
job = get("/jobs/#{started.fetch('jobId')}")
return job if %w[succeeded failed].include?(job['status'])
sleep 2
end
end
private
def post(path, body)
request(Net::HTTP::Post.new(URI.join(BASE.to_s + '/', path.delete_prefix('/')), @headers).tap { |r| r.body = body.to_json })
end
def get(path)
request(Net::HTTP::Get.new(URI.join(BASE.to_s + '/', path.delete_prefix('/')), @headers))
end
def request(req)
response = Net::HTTP.start(req.uri.hostname, req.uri.port, use_ssl: true) { |http| http.request(req) }
JSON.parse(response.body)
end
end
class TrimClipJob < ApplicationJob
def perform(source_url)
job = KinopipeClient.new.run(
'trim-video',
[{ id: 'main', url: source_url }],
{ start: 12, end: 48 },
)
raise job['errorMessage'] || 'the job did not finish' if job['status'] == 'failed'
job.dig('result', 'output', 'downloadUrl')
end
end- Net::HTTP and JSON are in the standard library, so this adds no gem.
- An Active Storage blob needs a URL the service can fetch. A signed URL works; for a private file, upload it through /api/v1/uploads and use the media URL it returns.
- Send an Idempotency-Key header when you retry, and the retry returns the original job instead of paying for a second render.
FAQ
What about streamio-ffmpeg?
It is a clean wrapper when FFmpeg is installed and you are happy to keep it installed. This is for when providing the binary is the hard part, or when you do not want an encode sharing CPU with the app.
Does this fit Active Storage?
Yes, as a step after the upload. Take the blob URL, run the operation, and attach the result back, rather than transforming inside the request.
Do I still need a background job?
Only for the waiting. The submission returns immediately, so a job that polls, or a webhook, keeps the request path clear either way.