KINOPIPE WITH RUBY ON RAILS

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.
Or let an agent write the call
The same operations are exposed over MCP, so an agent in your editor discovers them and calls them directly.

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.