Class: FFMPEG::KeyframeExtractor

Inherits:
Object
  • Object
show all
Defined in:
lib/ffmpeg/keyframe_extractor.rb

Overview

Extracts keyframes/thumbnails from video files

Examples:

Extract at intervals

extractor = KeyframeExtractor.new(media)
frames = extractor.extract_at_intervals(interval: 5.0, output_dir: "/tmp/frames")
# => ["/tmp/frames/frame_0000.jpg", "/tmp/frames/frame_0005.jpg", ...]

Extract specific count

frames = extractor.extract_count(10, output_dir: "/tmp/frames")
# => ["/tmp/frames/frame_0000.jpg", ..., "/tmp/frames/frame_0009.jpg"]

Extract at specific timestamps

frames = extractor.extract_at_timestamps([0, 30, 60, 90], output_dir: "/tmp/frames")

Constant Summary collapse

FORMATS =

Supported output formats

%w[jpg jpeg png bmp webp].freeze

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(media) ⇒ KeyframeExtractor

Create a new KeyframeExtractor

Parameters:

  • media (Media)

    source media



29
30
31
32
# File 'lib/ffmpeg/keyframe_extractor.rb', line 29

def initialize(media)
  @media = media
  validate_media!
end

Instance Attribute Details

#mediaMedia (readonly)

Returns source media.

Returns:

  • (Media)

    source media



25
26
27
# File 'lib/ffmpeg/keyframe_extractor.rb', line 25

def media
  @media
end

Instance Method Details

#create_sprite(columns: 10, rows: 10, width: 160, output_path:) ⇒ Hash

Create a thumbnail sprite sheet (useful for video scrubbing)

Parameters:

  • columns (Integer) (defaults to: 10)

    number of columns in sprite

  • rows (Integer) (defaults to: 10)

    number of rows in sprite

  • width (Integer) (defaults to: 160)

    thumbnail width

  • output_path (String)

    output sprite path

Returns:

  • (Hash)

    sprite info with :path, :columns, :rows, :thumbnail_width, :thumbnail_height



130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
# File 'lib/ffmpeg/keyframe_extractor.rb', line 130

def create_sprite(columns: 10, rows: 10, width: 160, output_path:)
  total_frames = columns * rows
  interval = (media.duration || 0) / total_frames

  # Calculate thumbnail height maintaining aspect ratio
  aspect_ratio = (media.width.to_f / media.height) rescue 16.0/9
  height = (width / aspect_ratio).round

  tile = "#{columns}x#{rows}"
  scale = "scale=#{width}:#{height}"

  cmd = [
    FFMPEG.ffmpeg_binary,
    "-i", media.path,
    "-vf", "fps=1/#{interval},#{scale},tile=#{tile}",
    "-frames:v", "1",
    "-y",
    output_path
  ]

  Command.run!(*cmd)

  {
    path: output_path,
    columns: columns,
    rows: rows,
    thumbnail_width: width,
    thumbnail_height: height,
    interval: interval
  }
end

#extract_at_intervals(interval: 5.0, output_dir:, format: "jpg", quality: 2, resolution: nil) ⇒ Array<String>

Extract frames at regular intervals

Parameters:

  • interval (Float) (defaults to: 5.0)

    interval between frames in seconds

  • output_dir (String)

    directory for output frames

  • format (String) (defaults to: "jpg")

    output image format

  • quality (Integer) (defaults to: 2)

    JPEG quality (1-31 for ffmpeg, lower is better)

  • resolution (String, nil) (defaults to: nil)

    output resolution (e.g., “640x480”)

Returns:

  • (Array<String>)

    paths to extracted frames



41
42
43
44
45
46
47
48
49
50
51
52
53
54
# File 'lib/ffmpeg/keyframe_extractor.rb', line 41

def extract_at_intervals(interval: 5.0, output_dir:, format: "jpg", quality: 2, resolution: nil)
  ensure_output_dir!(output_dir)
  validate_format!(format)

  # Calculate timestamps
  timestamps = []
  current = 0.0
  while current < media.duration
    timestamps << current
    current += interval
  end

  extract_frames(timestamps, output_dir, format, quality, resolution)
end

#extract_at_timestamps(timestamps, output_dir:, format: "jpg", quality: 2, resolution: nil) ⇒ Array<String>

Extract frames at specific timestamps

Parameters:

  • timestamps (Array<Float>)

    timestamps in seconds

  • output_dir (String)

    directory for output frames

  • format (String) (defaults to: "jpg")

    output image format

  • quality (Integer) (defaults to: 2)

    JPEG quality

  • resolution (String, nil) (defaults to: nil)

    output resolution

Returns:

  • (Array<String>)

    paths to extracted frames



88
89
90
91
92
93
# File 'lib/ffmpeg/keyframe_extractor.rb', line 88

def extract_at_timestamps(timestamps, output_dir:, format: "jpg", quality: 2, resolution: nil)
  ensure_output_dir!(output_dir)
  validate_format!(format)

  extract_frames(timestamps.sort, output_dir, format, quality, resolution)
end

#extract_count(count, output_dir:, format: "jpg", quality: 2, resolution: nil) ⇒ Array<String>

Extract a specific number of frames evenly distributed

Parameters:

  • count (Integer)

    number of frames to extract

  • output_dir (String)

    directory for output frames

  • format (String) (defaults to: "jpg")

    output image format

  • quality (Integer) (defaults to: 2)

    JPEG quality

  • resolution (String, nil) (defaults to: nil)

    output resolution

Returns:

  • (Array<String>)

    paths to extracted frames



63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
# File 'lib/ffmpeg/keyframe_extractor.rb', line 63

def extract_count(count, output_dir:, format: "jpg", quality: 2, resolution: nil)
  ensure_output_dir!(output_dir)
  validate_format!(format)

  return [] if count <= 0

  # Calculate evenly distributed timestamps
  duration = media.duration || 0
  timestamps = if count == 1
                 [0.0]
               else
                 interval = duration / (count - 1)
                 (0...count).map { |i| i * interval }
               end

  extract_frames(timestamps, output_dir, format, quality, resolution)
end

#extract_iframes(output_dir:, format: "jpg", quality: 2, max_frames: nil) ⇒ Array<String>

Extract actual I-frames (keyframes) from the video

Parameters:

  • output_dir (String)

    directory for output frames

  • format (String) (defaults to: "jpg")

    output image format

  • quality (Integer) (defaults to: 2)

    JPEG quality

  • max_frames (Integer, nil) (defaults to: nil)

    maximum number of frames

Returns:

  • (Array<String>)

    paths to extracted frames



101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
# File 'lib/ffmpeg/keyframe_extractor.rb', line 101

def extract_iframes(output_dir:, format: "jpg", quality: 2, max_frames: nil)
  ensure_output_dir!(output_dir)
  validate_format!(format)

  output_pattern = File.join(output_dir, "iframe_%04d.#{format}")

  cmd = [
    FFMPEG.ffmpeg_binary,
    "-i", media.path,
    "-vf", "select='eq(pict_type,I)'",
    "-vsync", "vfr",
    "-q:v", quality.to_s
  ]

  cmd += ["-frames:v", max_frames.to_s] if max_frames
  cmd += ["-y", output_pattern]

  result = Command.run!(*cmd)

  # Return list of created files
  Dir.glob(File.join(output_dir, "iframe_*.#{format}")).sort
end