crystal-streamio v0.1.0

crystal-streamio

Crystal port of the streamio-ffmpeg gem: a simple yet powerful wrapper around the ffmpeg and ffprobe commands for reading movie metadata and transcoding.

On top of the original gem's feature set, every external process call — ffprobe included — is supervised with a hard timeout that kills and reaps the child process. A corrupted or malformed input file that makes ffprobe hang can never silently eat a queue worker slot.

Requirements

  • Crystal >= 1.20.2
  • ffmpeg and ffprobe on PATH (on macOS: brew install ffmpeg)

Installation

  1. Add the dependency to your shard.yml:

    dependencies:
      crystal-streamio:
        github: MatheusBasso99/crystal-streamio
    
  2. Run shards install

Usage

require "crystal-streamio"

Reading Metadata

movie = FFmpeg::Movie.new("path/to/movie.mov")

movie.duration # => 7.56 (duration of the movie in seconds)
movie.bitrate  # => 481836 (bitrate in bit/s)
movie.size     # => 455546 (filesize in bytes)

movie.video_stream # => "h264 (Main) (avc1 / 0x31637661), yuv420p, 640x480 [SAR 1:1 DAR 4:3]"
movie.video_codec  # => "h264"
movie.colorspace   # => "yuv420p"
movie.resolution   # => "640x480"
movie.width        # => 640 (width of the movie in pixels)
movie.height       # => 480 (height of the movie in pixels)
movie.frame_rate   # => 16.746... (frames per second)

movie.audio_stream      # => "aac (mp4a / 0x6134706d), 44100 Hz, stereo, fltp, 75832 bit/s"
movie.audio_codec       # => "aac"
movie.audio_sample_rate # => 44100
movie.audio_channels    # => 2

# Multiple audio streams
movie.audio_streams[0].codec_name # => "aac"

movie.valid? # => true (false when ffmpeg fails to read the movie)

Movies can also be read from HTTP/HTTPS URLs; redirects are followed up to FFmpeg.max_http_redirect_attempts (default 10):

movie = FFmpeg::Movie.new("http://example.com/movie.mov")
movie.remote? # => true
movie.size    # => Content-Length of the HEAD response

Transcoding

The first argument is the output file path.

movie.transcode("tmp/movie.mp4") # Default ffmpeg settings for mp4 format

Keep track of progress with an optional block.

movie.transcode("movie.mp4") { |progress| puts progress } # 0.2 ... 0.5 ... 1.0

Give custom command line options with an argv array.

movie.transcode("movie.mp4", %w(-acodec aac -vcodec libx264 -ac 2))

Use FFmpeg::EncodingOptions for humanly readable transcoding options. Note that custom is an array so that it can hold repeatable ffmpeg options like -map:

options = FFmpeg::EncodingOptions.new(
  video_codec: "libx264", frame_rate: 10, resolution: "320x240", video_bitrate: 300,
  video_bitrate_tolerance: 100, aspect: 1.333333, keyframe_interval: 90,
  x264_vprofile: "high", x264_preset: "slow",
  audio_codec: "aac", audio_bitrate: 32, audio_sample_rate: 22050, audio_channels: 1,
  threads: 2, custom: %w(-vf crop=60:60:10:10 -map 0:0 -map 0:1))

movie.transcode("movie.mp4", options)

The transcode method returns a FFmpeg::Movie for the encoded file.

transcoded_movie = movie.transcode("tmp/movie.flv")

transcoded_movie.try(&.video_codec) # => "flv"
transcoded_movie.try(&.audio_codec) # => "mp3"

An aspect ratio is added to the encoding options automatically when none is specified (resolution: "320x180" implies -aspect 1.7777777777777777).

Preserve aspect ratio on width or height with the preserve_aspect_ratio transcoder option:

widescreen_movie = FFmpeg::Movie.new("path/to/widescreen_movie.mov")
options = FFmpeg::EncodingOptions.new(resolution: "320x240")

widescreen_movie.transcode("movie.mp4", options,
  FFmpeg::TranscoderOptions.new(preserve_aspect_ratio: :width)) # => 320x180

widescreen_movie.transcode("movie.mp4", options,
  FFmpeg::TranscoderOptions.new(preserve_aspect_ratio: :height)) # => 426x240

For constant bitrate encoding use video_min_bitrate and video_max_bitrate with buffer_size:

options = FFmpeg::EncodingOptions.new(
  video_min_bitrate: 600, video_max_bitrate: 600, buffer_size: 2000)
movie.transcode("movie.flv", options)

Specifying Input Options

Options that must apply to the input (such as its framerate) go into input_options:

transcoder_options = FFmpeg::TranscoderOptions.new(input_options: {"framerate" => "1/5"})
movie.transcode("movie.mp4", FFmpeg::EncodingOptions.new, transcoder_options)
# ffmpeg -y -framerate 1/5 -i path/to/movie.mov movie.mp4

Overriding the Input Path

When ffmpeg should read a sequence of files instead of the probed movie, set input:

transcoder_options = FFmpeg::TranscoderOptions.new(input: "img_%03d.png")
movie.transcode("movie.mp4", FFmpeg::EncodingOptions.new, transcoder_options)
# ffmpeg -y -i img_%03d.png movie.mp4

Watermarking

Add a watermark image on the video, e.g. at the right top corner with 10px padding:

options = FFmpeg::EncodingOptions.new(
  watermark: "full_path_of_watermark.png", resolution: "640x360",
  watermark_filter: FFmpeg::WatermarkFilter.new(position: :rt, padding_x: 10, padding_y: 10))

Positions are LT (Left Top), RT (Right Top), LB (Left Bottom) and RB (Right Bottom); padding_x/padding_y default to 10.

Taking Screenshots

movie.screenshot("screenshot.jpg")
movie.screenshot("screenshot.bmp", FFmpeg::EncodingOptions.new(seek_time: 5, resolution: "320x240"))

To generate multiple screenshots in a single pass, specify vframes and a wildcard filename, and disable output file validation:

movie.screenshot("screenshot_%d.jpg",
  FFmpeg::EncodingOptions.new(vframes: 20, frame_rate: "1/6"),
  FFmpeg::TranscoderOptions.new(validate: false))

Use quality (ffmpeg -q:v, 1..31, lower is better) for compressed formats, and preserve aspect ratio the same way as when transcoding:

movie.screenshot("screenshot.png",
  FFmpeg::EncodingOptions.new(seek_time: 2, resolution: "200x120"),
  FFmpeg::TranscoderOptions.new(preserve_aspect_ratio: :width))

Creating a Slideshow from Stills

There is no movie to probe, so use FFmpeg::Transcoder directly with input and input_options:

slideshow = FFmpeg::Transcoder.new(nil, "slideshow.mp4",
  FFmpeg::EncodingOptions.new(resolution: "320x240"),
  FFmpeg::TranscoderOptions.new(
    input: "img_%03d.jpeg",
    input_options: {"framerate" => "1/5"}))
slideshow.run

Configuration

FFmpeg.ffmpeg_binary = "/usr/local/bin/ffmpeg"   # default: found on PATH
FFmpeg.ffprobe_binary = "/usr/local/bin/ffprobe" # default: found on PATH
FFmpeg.max_http_redirect_attempts = 5            # default: 10
FFmpeg.logger                                    # stdlib Log for "ffmpeg"

Timeouts (hang protection)

Reading metadata is capped by a total wall-clock timeout — ffprobe emits nothing until it finishes, so this is the only guard that catches a hang on a corrupted file:

FFmpeg.ffprobe_timeout = 30.seconds # default; nil disables (not recommended)

Transcoding uses an inactivity timeout that resets on every ffmpeg progress line and trips only after that much silence:

FFmpeg::Transcoder.timeout = 10.seconds # default 30 seconds; nil disables

On expiry the child process is killed with SIGKILL, reaped (no zombies) and an FFmpeg::TimeoutError / FFmpeg::Error is raised.

Development

shards install                 # install dev dependencies (ameba)
crystal spec                   # run the test suite
crystal tool format            # format the code
bin/ameba                      # lint

# 100% line coverage gate (requires kcov):
crystal build --debug spec/support/coverage_entrypoint.cr -o bin/spec_runner
kcov --clean --include-path="$(pwd)/src" coverage ./bin/spec_runner

Contributing

  1. Fork it (https://github.com/MatheusBasso99/crystal-streamio/fork)
  2. Create your feature branch (git checkout -b my-new-feature)
  3. Commit your changes (git commit -am 'Add some feature')
  4. Push to the branch (git push origin my-new-feature)
  5. Create a new Pull Request

Contributors

Acknowledgments

This shard is a Crystal port of, and was entirely inspired by, the excellent streamio-ffmpeg Ruby gem. Huge thanks to its authors and contributors — the public API, option set, and test fixtures here all follow their design. Please give the original project a look and a star: https://github.com/streamio/streamio-ffmpeg.

Repository

crystal-streamio

Owner
Statistic
  • 0
  • 0
  • 0
  • 0
  • 1
  • about 8 hours ago
  • July 9, 2026
License

MIT License

Links
Synced at

Thu, 09 Jul 2026 03:58:28 GMT

Languages