Class: SmartVideoPosterExtractionService

Inherits:
Object
  • Object
show all
Defined in:
app/services/smart_video_poster_extraction_service.rb

Overview

Heuristic ("smart") poster extraction using FFmpeg scene detection.

Strategy

  • Use FFmpeg to analyze the video input and find the first significant scene change
    after a minimum start time (to avoid intros/slates).
  • For Cloudflare videos, we detect the best timestamp using FFmpeg over the HLS
    manifest URL, then delegate the actual poster creation to
    VideoPosterExtractionService with that timestamp (which uses Cloudflare's
    thumbnail endpoint at ?time=s).
  • For self-hosted videos, we likewise detect the timestamp and delegate to
    VideoPosterExtractionService which extracts with FFmpeg at that timestamp.

Options

  • min_start_seconds: skip the first N seconds before detecting scenes (default: 5.0)
  • scene_threshold: FFmpeg scene threshold for cuts (default: 0.40)
  • fallback_seconds: if detection fails, use this timestamp (default: 10.0)

Instance Method Summary collapse

Constructor Details

#initialize(video, min_start_seconds: 5.0, scene_threshold: 0.40, fallback_seconds: 10.0) ⇒ SmartVideoPosterExtractionService

Returns a new instance of SmartVideoPosterExtractionService.



21
22
23
24
25
26
# File 'app/services/smart_video_poster_extraction_service.rb', line 21

def initialize(video, min_start_seconds: 5.0, scene_threshold: 0.40, fallback_seconds: 10.0)
  @video = video
  @min_start_seconds = min_start_seconds
  @scene_threshold = scene_threshold
  @fallback_seconds = fallback_seconds
end

Instance Method Details

#extract_posterObject

Raises VideoPosterExtractionService::ExtractionError on failure so callers
see the real root cause.



30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
# File 'app/services/smart_video_poster_extraction_service.rb', line 30

def extract_poster
  input = input_source
  unless input
    raise VideoPosterExtractionService::ExtractionError,
          "Video #{@video.id} has no valid input source (no Cloudflare UID or attachment)"
  end

  Rails.logger.info "[SmartPoster] Detecting scene timestamp for video #{@video.id} (min_start=#{@min_start_seconds}s, threshold=#{@scene_threshold})"

  timestamp = detect_scene_change_timestamp(input)
  if timestamp.nil?
    [0.35, 0.30, 0.25, 0.20, 0.15, 0.10].each do |th|
      next if th >= @scene_threshold

      Rails.logger.info "[SmartPoster] Retrying scene detection with threshold=#{th}"
      timestamp = detect_scene_change_timestamp(input, threshold_override: th)
      break if timestamp
    end
  end

  if timestamp.nil?
    Rails.logger.info '[SmartPoster] Attempting thumbnail heuristic'
    timestamp = detect_thumbnail_representative_timestamp(input)
  end

  timestamp ||= @fallback_seconds

  Rails.logger.info "[SmartPoster] Using timestamp #{timestamp.round(3)}s for video #{@video.id}"

  ms = (timestamp.to_f * 1000).to_i
  @video.update_column(:poster_offset, ms)
  VideoPosterExtractionService.new(@video, timestamp).extract_poster
end