Helios::Videos
Video upload, processing, and streaming for Rails. Upload videos via ActiveStorage direct upload to S3, ingest into Mux or Cloudflare Stream for processing, and serve streaming video.
Installation
Add to your Gemfile:
gem "helios-videos"
Then:
bundle install
bin/rails helios_videos:install:migrations
bin/rails db:migrate
Prerequisites: Your host app must have ActiveStorage installed (bin/rails active_storage:install).
Configuration
Create config/initializers/helios_videos.rb:
Helios::Videos.configure do |config|
# Choose your video processor
config.processor = :cloudflare # or :mux
# Parent controller for admin views (must provide authentication)
config.admin_parent_controller = "Admin::BaseController"
# Use an existing model instead of Helios::Videos::Video (optional)
# Your model must include Helios::Videos::VideoConcern
# and have the required columns: key, playback_urls (jsonb),
# requires_signed_urls (boolean), provider (string)
config.video_model = "Video" # default: "Helios::Videos::Video"
# Cloudflare Stream settings
config.cloudflare_account_id = ENV["CLOUDFLARE_ACCOUNT_ID"]
config.cloudflare_api_token = ENV["CLOUDFLARE_API_TOKEN"]
config.cloudflare_customer_subdomain = ENV["CLOUDFLARE_CUSTOMER_SUBDOMAIN"]
config.require_signed_urls = true
# OR Mux settings
# config.processor = :mux
# config.mux_token_id = ENV["MUX_TOKEN_ID"]
# config.mux_token_secret = ENV["MUX_TOKEN_SECRET"]
end
Using an existing model
If your app already has a Video model, you can include the gem's functionality via a concern instead of using Helios::Videos::Video:
- Add the required columns to your existing table:
add_column :videos, :key, :string # service identifier (Mux asset ID or Cloudflare UID)
add_column :videos, :playback_urls, :jsonb
add_column :videos, :requires_signed_urls, :boolean, default: false, null: false
add_column :videos, :provider, :string # "cloudflare" or "mux"
- Include the concern in your model:
class Video < ApplicationRecord
include Helios::Videos::VideoConcern
end
- Set the model name in the initializer:
config.video_model = "Video"
The concern adds: video_file and thumbnail_image ActiveStorage attachments, provider enum, automatic ingestion via background job on create, per-video processor routing, signed URL support, and thumbnail downloads.
Upgrading from pre-0.2.0 (adding the provider column)
If you installed helios-videos before the provider column was added, you need to add it to your existing table. For apps using the gem's own table:
bin/rails helios_videos:install:migrations
bin/rails db:migrate
For apps using a custom video model with their own table, create a migration:
class AddProviderToVideos < ActiveRecord::Migration[8.0]
def change
add_column :videos, :provider, :string
add_index :videos, :provider
# Backfill existing videos with their current provider
reversible do |dir|
dir.up do
Video.where(provider: nil).update_all(provider: 'cloudflare') # or 'mux'
end
end
end
end
Routes
Mount the engine:
mount Helios::Videos::Engine, at: "/videos"
Usage
Creating a video
video = Helios::Videos.video_class.new(name: "My Video")
video.video_file.attach(params[:video_file])
video.save!
# CheckVideoJob will automatically ingest the video into your configured processor
Displaying a video
<%= video.player_component %>
Each video renders using the correct player for its provider — Cloudflare videos use video.js with HLS, Mux videos use the mux-player element. This means both providers can coexist in the same app during and after a migration.
Programmatic access
video.playback_url # unsigned HLS URL
video.playback_url(signed: true) # signed URL (Cloudflare)
video.signed_url(expiration: 2.hours) # shorthand
video.download_url # downloadable MP4 URL
video.download_and_store_thumbnail! # fetch and attach thumbnail
video.check_for_processing! # manually trigger ingestion
video.effective_processor # the processor for this video's provider
With helios-press
When both gems are loaded, video blocks are automatically available in the block editor. Videos can be dragged into block placeholders for direct upload and processing.
Migrating between providers
helios-videos includes built-in tooling for migrating videos between Cloudflare Stream and Mux. Migration happens record-by-record in background jobs so long ingestions don't block each other.
How it works
The migration pipeline has three stages:
- MigrateVideosJob (orchestrator) — finds all videos on the source provider, enqueues a conversion job for each one.
- ConvertVideoJob (per-video) — gets a download URL from the source, submits it to the destination for ingestion.
- For Mux destinations: playback IDs come back immediately, so the provider is flipped right away.
- For Cloudflare destinations: processing takes time, so a check job is enqueued.
- CheckIngestionJob (Cloudflare destination only) — polls Cloudflare every 30 seconds until the video is ready, then flips the provider.
Running a migration
Make sure both providers are configured in your initializer (you need credentials for both), then enqueue the orchestrator:
# Migrate all Cloudflare videos to Mux
Helios::Videos::Migration::MigrateVideosJob.perform_later(
from: "cloudflare",
to: "mux"
)
# Or migrate in batches
Helios::Videos::Migration::MigrateVideosJob.perform_later(
from: "cloudflare",
to: "mux",
batch_size: 10
)
During migration, both providers are served concurrently — each video's provider column determines which processor handles playback. Videos that haven't been migrated yet continue to work on the original provider.
Migrate a single video
Helios::Videos::Migration::ConvertVideoJob.perform_later(
video_id: 42,
from: "cloudflare",
to: "mux"
)
JavaScript
If your host app needs the video block Stimulus controller:
import { HeliosVideoBlockController } from "helios/videos"
application.register("video-block", HeliosVideoBlockController)
Vite
If your host app uses Vite, add an alias so Vite can resolve the gem's JavaScript:
// vite.config.mts
resolve: {
alias: {
'helios/videos': resolve(__dirname, '/path/to/helios-videos/app/javascript/helios/videos'),
},
},
License
Proprietary. All rights reserved.