Ruby SDK Guide

The MediaConvert.io Ruby SDK provides a convenient way to integrate media conversion into your Ruby applications. This guide covers installation, configuration, and common usage patterns.

Installation

Add the gem to your Gemfile:

ruby
gem 'mediaconvert', '~> 1.0'

Then run:

bash
bundle install

Or install directly:

bash
gem install mediaconvert

Quick Start

Basic Configuration

ruby
require 'mediaconvert'

# Configure the client
MediaConvert.configure do |config|
  config.api_token = ENV['MEDIACONVERT_API_TOKEN']
  config.api_base_url = 'https://mediaconvert.io/api/v1'
  config.timeout = 30
  config.retries = 3
end

# Alternative: Initialize client directly
client = MediaConvert::Client.new(
  api_token: ENV['MEDIACONVERT_API_TOKEN']
)

Your First Conversion

ruby
# Create a conversion job
job = MediaConvert::ConversionJob.create(
  job_type: 'video',
  input_url: 'https://bucket.s3.amazonaws.com/input.mp4?...',
  output_url: 'https://bucket.s3.amazonaws.com/output.webm?...',
  input_format: 'mp4',
  output_format: 'webm',
  input_size_bytes: 52428800,
  webhook_url: 'https://your-app.com/webhooks/mediaconvert'
)

puts "Job created: #{job.id}"
puts "Status: #{job.status}"
# New micro-precision API (recommended)
puts "Estimated cost: €#{job.estimated_cost_micros / 1_000_000.0}"
# Or using backward-compatible cents field in API response
puts "Estimated cost: €#{job.estimated_cost_cents / 100.0}"

Core Classes

MediaConvert::ConversionJob

The main class for managing conversion jobs:

ruby
# Create a job
job = MediaConvert::ConversionJob.create(
  job_type: 'video',
  input_url: input_presigned_url,
  output_url: output_presigned_url,
  input_format: 'mp4',
  output_format: 'webm',
  input_size_bytes: file_size_bytes
)

# Check job status
job.reload
puts job.status                 # => "processing"
puts job.progress_percentage    # => 45

# Wait for completion
job.wait_until_complete do |current_job|
  puts "Progress: #{current_job.progress_percentage}%"
end

# Access results
puts "Processing time: #{job.processing_time_seconds}s"
puts "Output size: #{job.output_size_bytes} bytes"
# New micro-precision API (recommended)
puts "Total cost: €#{job.cost_micros / 1_000_000.0}"
# Or using backward-compatible cents field in API response  
puts "Total cost: €#{job.cost_cents / 100.0}"

MediaConvert::Account

Manage your account information:

ruby
# Get account details
account = MediaConvert::Account.current
puts account.email
puts account.tier
puts "Credits: €#{account.credits_balance_cents / 100.0}"

# Check usage
usage = account.usage_summary
puts "Jobs this month: #{usage.jobs_count}"
puts "Data processed: #{usage.total_mb_processed} MB"
puts "Total spent: €#{usage.total_cost_micros / 1_000_000.0}"

Rails Integration

ActiveJob Integration

Integrate with Rails background jobs:

ruby
# app/jobs/media_conversion_job.rb
class MediaConversionJob < ApplicationJob
  queue_as :default

  retry_on MediaConvert::RateLimitError, wait: :exponentially_longer
  retry_on MediaConvert::APIError, attempts: 3

  def perform(user, input_url, output_url, options = {})
    job = MediaConvert::ConversionJob.create({
      input_url: input_url,
      output_url: output_url,
      webhook_url: webhook_url_for(user)
    }.merge(options))

    # Store job reference
    user.media_conversions.create!(
      external_id: job.id,
      status: job.status,
      input_url: input_url,
      output_url: output_url
    )
  end

  private

  def webhook_url_for(user)
    Rails.application.routes.url_helpers.webhooks_mediaconvert_url(
      host: Rails.application.config.action_mailer.default_url_options[:host]
    )
  end
end

# Usage
MediaConversionJob.perform_later(
  current_user,
  input_presigned_url,
  output_presigned_url,
  job_type: 'video',
  output_format: 'webm'
)

Model Integration

Integrate with your models:

ruby
# app/models/video.rb
class Video < ApplicationRecord
  belongs_to :user
  has_one_attached :original_file

  after_create :enqueue_conversion

  enum status: { 
    pending: 0, 
    processing: 1, 
    completed: 2, 
    failed: 3 
  }

  def enqueue_conversion
    return unless original_file.attached?

    input_url = generate_presigned_url(original_file, :get)
    output_key = "converted/#{id}/output.webm"
    output_url = generate_presigned_url_for_key(output_key, :put)

    MediaConversionJob.perform_later(
      user,
      input_url,
      output_url,
      job_type: 'video',
      output_format: 'webm'
    )
  end

  private

  def generate_presigned_url(attachment, method)
    attachment.service.send(
      :presigned_url,
      attachment.key,
      method: method,
      expires_in: 1.hour
    )
  end

  def generate_presigned_url_for_key(key, method)
    original_file.service.send(
      :presigned_url,
      key,
      method: method,
      expires_in: 1.hour
    )
  end
end

Webhook Integration

Handle webhook notifications in Rails:

ruby
# config/routes.rb
post '/webhooks/mediaconvert', to: 'webhooks#mediaconvert'

# app/controllers/webhooks_controller.rb
class WebhooksController < ApplicationController
  protect_from_forgery except: [:mediaconvert]

  def mediaconvert
    # Verify webhook signature
    unless verify_signature(request.raw_post, request.headers['X-MediaConvert-Signature'])
      head :unauthorized
      return
    end

    # Process the webhook
    case params[:status]
    when 'completed'
      handle_completed_job(params[:job_id])
    when 'failed'
      handle_failed_job(params[:job_id], params[:error_message])
    when 'processing'
      update_job_progress(params[:job_id], params[:progress_percentage])
    end

    head :ok
  end

  private

  def verify_signature(payload, signature)
    expected = OpenSSL::HMAC.hexdigest(
      'sha256',
      ENV['MEDIACONVERT_WEBHOOK_SECRET'],
      payload
    )

    Rack::Utils.secure_compare(signature, expected)
  end

  def handle_completed_job(job_id)
    # Process completed conversion
    job = MediaConvertJob.find_by(external_id: job_id)
    job.update(status: 'completed', completed_at: Time.current)

    # Notify user
    ConversionCompleteJob.perform_later(job.user, job)
  end
end

Error Handling

Robust error handling for production applications:

ruby
begin
  job = MediaConvert::ConversionJob.create(
    job_type: 'video',
    input_url: input_url,
    output_url: output_url,
    input_format: 'mp4',
    output_format: 'webm'
  )

  job.wait_until_complete

rescue MediaConvert::AuthenticationError
  puts "Authentication failed - check your API token"

rescue MediaConvert::InsufficientCreditsError => e
  puts "Need €#{e.required_credits_micros / 1_000_000.0} but only have €#{e.available_credits_micros / 1_000_000.0}"

rescue MediaConvert::ValidationError => e
  puts "Validation failed: #{e.errors.join(', ')}"

rescue MediaConvert::ConversionError => e
  puts "Conversion failed: #{e.message}"

rescue MediaConvert::RateLimitError => e
  puts "Rate limited. Retry after #{e.retry_after} seconds"
  sleep(e.retry_after)
  retry

rescue MediaConvert::APIError => e
  puts "API error: #{e.message} (#{e.http_status})"
end

Batch Processing

Process multiple files efficiently:

ruby
class BatchProcessor
  def initialize
    @jobs = []
  end

  def add_job(input_url, output_url, options = {})
    job = MediaConvert::ConversionJob.create({
      input_url: input_url,
      output_url: output_url
    }.merge(options))

    @jobs << job
    job
  end

  def wait_for_completion
    completed = []
    failed = []

    @jobs.each do |job|
      begin
        job.wait_until_complete
        completed << job
      rescue MediaConvert::ConversionError => e
        failed << { job: job, error: e }
      end
    end

    { completed: completed, failed: failed }
  end
end

# Usage
processor = BatchProcessor.new

# Add multiple jobs
processor.add_job(input_url_1, output_url_1, output_format: 'webm')
processor.add_job(input_url_2, output_url_2, output_format: 'mp4')
processor.add_job(input_url_3, output_url_3, output_format: 'webp')

# Process all jobs
results = processor.wait_for_completion
puts "#{results[:completed].size} completed, #{results[:failed].size} failed"

Testing

Test Helpers

ruby
# spec/support/mediaconvert_helpers.rb
module MediaConvertHelpers
  def stub_mediaconvert_job_creation
    WebMock.stub_request(:post, 'https://mediaconvert.io/api/v1/conversion_jobs')
      .to_return(
        status: 201,
        headers: { 'Content-Type' => 'application/json' },
        body: {
          id: 'job_test123',
          status: 'pending',
          job_type: 'video',
          estimated_cost_micros: 150_000  # €0.15
        }.to_json
      )
  end

  def stub_mediaconvert_job_status(job_id, status, progress = 0)
    WebMock.stub_request(:get, "https://mediaconvert.io/api/v1/conversion_jobs/#{job_id}")
      .to_return(
        status: 200,
        headers: { 'Content-Type' => 'application/json' },
        body: {
          id: job_id,
          status: status,
          progress_percentage: progress
        }.to_json
      )
  end
end

RSpec.configure do |config|
  config.include MediaConvertHelpers
end

Example Tests

ruby
# spec/jobs/media_conversion_job_spec.rb
RSpec.describe MediaConversionJob, type: :job do
  let(:user) { create(:user) }
  let(:input_url) { 'https://bucket.s3.amazonaws.com/input.mp4' }
  let(:output_url) { 'https://bucket.s3.amazonaws.com/output.webm' }

  before do
    stub_mediaconvert_job_creation
  end

  it 'creates a conversion job' do
    expect {
      described_class.perform_now(user, input_url, output_url)
    }.to change(user.media_conversions, :count).by(1)
  end

  it 'handles API errors gracefully' do
    WebMock.stub_request(:post, 'https://mediaconvert.io/api/v1/conversion_jobs')
      .to_return(status: 402, body: { error: 'insufficient_credits' }.to_json)

    expect {
      described_class.perform_now(user, input_url, output_url)
    }.to raise_error(MediaConvert::InsufficientCreditsError) do |error|
      expect(error.required_credits_micros).to eq(3_150_000)
      expect(error.available_credits_micros).to eq(500_000)
    end
  end
end

Configuration Options

Global Configuration

ruby
MediaConvert.configure do |config|
  # Required
  config.api_token = ENV['MEDIACONVERT_API_TOKEN']

  # Optional settings
  config.api_base_url = 'https://mediaconvert.io/api/v1'  # Default
  config.timeout = 30                                      # Default: 30 seconds
  config.open_timeout = 10                                 # Default: 10 seconds
  config.retries = 3                                       # Default: 3
  config.retry_delay = 1                                   # Default: 1 second
  config.user_agent = 'MyApp/1.0'                         # Custom user agent

  # Webhook settings
  config.webhook_secret = ENV['MEDIACONVERT_WEBHOOK_SECRET']

  # Logging
  config.logger = Rails.logger                             # Default: Logger.new(STDOUT)
  config.log_level = :info                                 # Default: :info
end

Common Patterns

S3 Presigned URL Generation

ruby
class S3UrlGenerator
  def initialize(bucket, region = 'us-east-1')
    @s3 = Aws::S3::Client.new(region: region)
    @bucket = bucket
  end

  def input_url(key, expires_in = 3600)
    @s3.presigned_url(:get_object,
      bucket: @bucket,
      key: key,
      expires_in: expires_in
    )
  end

  def output_url(key, expires_in = 3600)
    @s3.presigned_url(:put_object,
      bucket: @bucket,
      key: key,
      expires_in: expires_in
    )
  end
end

# Usage
generator = S3UrlGenerator.new('my-bucket')
input_url = generator.input_url('videos/input.mp4')
output_url = generator.output_url('videos/output.webm')

job = MediaConvert::ConversionJob.create(
  job_type: 'video',
  input_url: input_url,
  output_url: output_url,
  input_format: 'mp4',
  output_format: 'webm'
)

Progress Monitoring

ruby
class ConversionMonitor
  def initialize(job)
    @job = job
  end

  def monitor_progress
    loop do
      @job.reload

      puts "Status: #{@job.status}"
      puts "Progress: #{@job.progress_percentage}%" if @job.progress_percentage

      case @job.status
      when 'completed'
        puts "✅ Conversion completed!"
        puts "Processing time: #{@job.processing_time_seconds}s"
        puts "Cost: €#{@job.cost_micros / 1_000_000.0}"
        break
      when 'failed'
        puts "❌ Conversion failed: #{@job.error_message}"
        break
      when 'cancelled'
        puts "🚫 Conversion cancelled"
        break
      end

      sleep(5) # Check every 5 seconds
    end
  end
end

# Usage
job = MediaConvert::ConversionJob.create(conversion_params)
monitor = ConversionMonitor.new(job)
monitor.monitor_progress

### Webhook Signature Verification

```ruby
class MediaConvertWebhookVerifier
  def initialize(webhook_secret)
    @webhook_secret = webhook_secret
  end

  def verify_signature(payload, signature)
    expected = OpenSSL::HMAC.hexdigest('sha256', @webhook_secret, payload)
    expected_with_prefix = "sha256=#{expected}"

    Rack::Utils.secure_compare(signature, expected_with_prefix)
  end

  def process_webhook(request)
    payload = request.raw_post
    signature = request.headers['X-MediaConvert-Signature']

    unless verify_signature(payload, signature)
      raise MediaConvert::WebhookSignatureError, 'Invalid webhook signature'
    end

    JSON.parse(payload)
  end
end

# Usage in controller
class WebhooksController < ApplicationController
  before_action :verify_webhook_signature

  private

  def verify_webhook_signature
    verifier = MediaConvertWebhookVerifier.new(ENV['MEDIACONVERT_WEBHOOK_SECRET'])
    @webhook_data = verifier.process_webhook(request)
  rescue MediaConvert::WebhookSignatureError => e
    head :unauthorized
  end

  def mediaconvert
    case @webhook_data['status']
    when 'completed'
      handle_completed_job(@webhook_data)
    when 'failed'
      handle_failed_job(@webhook_data)
    when 'processing'
      update_job_progress(@webhook_data)
    end

    head :ok
  end
end

Advanced Configuration with Circuit Breaker

ruby
class MediaConvertCircuitBreaker
  def initialize(failure_threshold: 5, recovery_timeout: 60)
    @failure_threshold = failure_threshold
    @recovery_timeout = recovery_timeout
    @failure_count = 0
    @last_failure_time = nil
    @state = :closed # :closed, :open, :half_open
  end

  def call(&block)
    case @state
    when :open
      if Time.current - @last_failure_time > @recovery_timeout
        @state = :half_open
        attempt_request(&block)
      else
        raise MediaConvert::CircuitBreakerOpenError, 'Circuit breaker is open'
      end
    when :half_open
      attempt_request(&block)
    when :closed
      attempt_request(&block)
    end
  end

  private

  def attempt_request(&block)
    result = block.call
    reset_failure_count
    result
  rescue MediaConvert::APIError => e
    record_failure
    raise e
  end

  def record_failure
    @failure_count += 1
    @last_failure_time = Time.current

    if @failure_count >= @failure_threshold
      @state = :open
      Rails.logger.error "[MediaConvert] Circuit breaker opened after #{@failure_count} failures"
    end
  end

  def reset_failure_count
    @failure_count = 0
    @state = :closed
  end
end

# Usage with circuit breaker
class RobustMediaConvertService
  def initialize
    @circuit_breaker = MediaConvertCircuitBreaker.new
  end

  def convert_with_protection(options)
    @circuit_breaker.call do
      MediaConvert::ConversionJob.create(options)
    end
  rescue MediaConvert::CircuitBreakerOpenError => e
    Rails.logger.warn "[MediaConvert] Circuit breaker open, using fallback"
    # Implement fallback behavior (queue for later, use alternative service, etc.)
    queue_for_later_processing(options)
  end
end
text

## Performance Tips

### Connection Pooling

```ruby
# Use persistent connections for high-throughput applications
MediaConvert.configure do |config|
  config.api_token = ENV['MEDIACONVERT_API_TOKEN']
  config.adapter = :net_http_persistent  # Reuse connections
  config.pool_size = 10                   # Connection pool size
end

Async Processing

ruby
# Don't wait for completion in web requests
def convert_video
  job = MediaConvert::ConversionJob.create(
    job_type: 'video',
    input_url: params[:input_url],
    output_url: params[:output_url],
    webhook_url: webhook_url
  )

  # Return immediately
  render json: { job_id: job.id, status: job.status }
end

Next Steps

Support