Module: HLS::Testing

Defined in:
lib/hls/testing.rb

Overview

Public test helpers for verifying that an HLS profile actually produces a correct bundle when run against ffmpeg. Users of the gem can include this module in their own spec suites to write integration tests against their ‘app/videos/*.rb` profile classes:

require "hls/testing"

RSpec.describe CourseVideo do
  include HLS::Testing

  it "encodes a valid HLS bundle" do
    video = generate_test_video(duration: 12)
    output = Pathname.new(Dir.mktmpdir)

    profile = CourseVideo.new(input: HLS::Input.new(video), output: output)
    silence_ffmpeg do
      profile.encode!
      profile.poster!
    end

    expect(output).to be_a_valid_hls_bundle.with_variants(3)
  end
end

All helpers shell out to ffmpeg/ffprobe (already a hard dep of the gem), so any environment that can run the gem can run these helpers.

Instance Method Summary collapse

Instance Method Details

#generate_test_video(path: nil, duration: 12, width: 640, height: 360, framerate: 30, frequency: 440) ⇒ Object

Generates a deterministic test video at ‘path` (or a tmpdir-backed path if not given). Returns the path. The video uses ffmpeg’s ‘testsrc` filter for video and `sine` for audio — fully self-contained, no external assets.



42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
# File 'lib/hls/testing.rb', line 42

def generate_test_video(path: nil, duration: 12, width: 640, height: 360, framerate: 30, frequency: 440)
  path = Pathname.new(path || Dir::Tmpname.create(["hls-fixture", ".mp4"]) {})

  cmd = [
    "ffmpeg", "-y", "-loglevel", "error",
    "-f", "lavfi", "-i", "testsrc=duration=#{duration}:size=#{width}x#{height}:rate=#{framerate}",
    "-f", "lavfi", "-i", "sine=frequency=#{frequency}:duration=#{duration}",
    "-c:v", "libx264", "-preset", "ultrafast", "-pix_fmt", "yuv420p",
    "-c:a", "aac", "-b:a", "64k",
    "-shortest",
    path.to_s
  ]
  unless system(*cmd, out: File::NULL, err: File::NULL)
    raise HLS::Error, "ffmpeg failed to generate test fixture at #{path}"
  end

  path
end

#parse_playlist(path) ⇒ Object

Parses an m3u8 file and returns the M3u8::Playlist.



82
83
84
# File 'lib/hls/testing.rb', line 82

def parse_playlist(path)
  M3u8::Reader.new.read(File.read(path))
end

#probe(path) ⇒ Object

Probes a media file with ffprobe and returns its parsed metadata.

Raises:



62
63
64
65
66
67
68
69
70
71
72
# File 'lib/hls/testing.rb', line 62

def probe(path)
  stdout, _stderr, status = Open3.capture3(
    "ffprobe", "-v", "error",
    "-select_streams", "v:0",
    "-show_entries", "stream=width,height,codec_name",
    "-of", "json",
    path.to_s
  )
  raise HLS::Error, "ffprobe failed for #{path}" unless status.success?
  JSON.parse(stdout)
end

#probe_dimensions(path) ⇒ Object

Returns [width, height] for an image or video file.



75
76
77
78
79
# File 'lib/hls/testing.rb', line 75

def probe_dimensions(path)
  stream = probe(path).fetch("streams").first or
    raise HLS::Error, "no video stream in #{path}"
  [stream["width"], stream["height"]]
end

#silence_ffmpegObject

Silences stdout/stderr inside the block. Useful for hiding ffmpeg’s noisy progress output during test runs. Restores streams even on exception.



89
90
91
92
93
94
95
96
97
98
# File 'lib/hls/testing.rb', line 89

def silence_ffmpeg
  original_stdout = $stdout.dup
  original_stderr = $stderr.dup
  $stdout.reopen(File::NULL, "w")
  $stderr.reopen(File::NULL, "w")
  yield
ensure
  $stdout.reopen(original_stdout) if original_stdout
  $stderr.reopen(original_stderr) if original_stderr
end