process mp4 file thumbnailing
This commit is contained in:
@@ -23,8 +23,8 @@ class LoadedMedia
|
||||
)
|
||||
when %r{image/jpeg}, %r{image/jpg}, %r{image/png}, %r{image/bmp}
|
||||
LoadedMedia::StaticImage.new(media_path)
|
||||
when %r{video/webm}
|
||||
LoadedMedia::Webm.new(media_path)
|
||||
when %r{video/webm}, %r{video/mp4}
|
||||
LoadedMedia::WebmOrMp4.new(media_path)
|
||||
else
|
||||
return nil
|
||||
end
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# typed: strict
|
||||
# frozen_string_literal: true
|
||||
|
||||
class LoadedMedia::Webm < LoadedMedia
|
||||
class LoadedMedia::WebmOrMp4 < LoadedMedia
|
||||
include HasColorLogger
|
||||
|
||||
::FFMPEG.logger = Logger.new(nil)
|
||||
@@ -74,42 +74,33 @@ class LoadedMedia::Webm < LoadedMedia
|
||||
File.join(BlobFile::TMP_DIR, "webm-#{frame}-#{SecureRandom.uuid}.png")
|
||||
FileUtils.mkdir_p(File.dirname(tmp_path))
|
||||
|
||||
# @media.screenshot(tmp_path, { seek_time: frame_time })
|
||||
# Always seek from the beginning for simplicity and reliability
|
||||
# Add a small safety margin for the last frame to avoid seeking beyond video duration
|
||||
safe_seek_time = [frame_time, @duration - 0.01].min
|
||||
|
||||
# Determine if we should seek from start or end based on where we are in the file
|
||||
past_halfway = frame_time / @duration > 0.5
|
||||
|
||||
cmd = [FFMPEG_BIN, "-y", "-xerror", "-abort_on", "empty_output"] # Overwrite output files
|
||||
|
||||
if past_halfway
|
||||
# For frames in the second half of the file, seek from the end
|
||||
# Convert to a negative offset from the end
|
||||
end_offset = frame_time - @duration
|
||||
cmd.concat(["-sseof", end_offset.round(2).to_s])
|
||||
else
|
||||
# For frames in the first half, seek from the beginning
|
||||
cmd.concat(["-ss", frame_time.round(2).to_s])
|
||||
end
|
||||
|
||||
# Add input file and frame extraction options
|
||||
cmd.concat(
|
||||
[
|
||||
"-i",
|
||||
@media_path, # Input file
|
||||
"-vframes",
|
||||
"1", # Extract one frame
|
||||
"-f",
|
||||
"image2", # Force format to image2
|
||||
"-update",
|
||||
"1", # Update existing file
|
||||
tmp_path,
|
||||
],
|
||||
)
|
||||
cmd = [
|
||||
FFMPEG_BIN,
|
||||
"-y", # Overwrite output files
|
||||
"-xerror", # Exit on error
|
||||
"-abort_on",
|
||||
"empty_output", # Abort if output is empty
|
||||
"-ss",
|
||||
safe_seek_time.round(2).to_s,
|
||||
"-i",
|
||||
@media_path, # Input file
|
||||
"-vframes",
|
||||
"1", # Extract one frame
|
||||
"-f",
|
||||
"image2", # Force format to image2
|
||||
"-update",
|
||||
"1", # Update existing file
|
||||
tmp_path,
|
||||
]
|
||||
|
||||
_output, error, status = Open3.capture3(*cmd)
|
||||
unless status.exitstatus == 0
|
||||
$stderr.print error
|
||||
raise "Failed to extract frame with ffmpeg: #{error}"
|
||||
raise "Failed to extract frame with ffmpeg: #{cmd.join(" ")}: #{error}"
|
||||
end
|
||||
|
||||
# Use the original frame number in the error message
|
||||
@@ -1,11 +1,11 @@
|
||||
# typed: true
|
||||
|
||||
# DO NOT EDIT MANUALLY
|
||||
# This is an autogenerated file for dynamic methods in `LoadedMedia::Webm`.
|
||||
# Please instead update this file by running `bin/tapioca dsl LoadedMedia::Webm`.
|
||||
# This is an autogenerated file for dynamic methods in `LoadedMedia::WebmOrMp4`.
|
||||
# Please instead update this file by running `bin/tapioca dsl LoadedMedia::WebmOrMp4`.
|
||||
|
||||
|
||||
class LoadedMedia::Webm
|
||||
class LoadedMedia::WebmOrMp4
|
||||
sig { returns(ColorLogger) }
|
||||
def logger; end
|
||||
|
||||
@@ -21,6 +21,9 @@ RSpec.describe LoadedMedia do
|
||||
.join("test/fixtures/files/images/e621-5421402-animated.webm")
|
||||
.to_s
|
||||
end
|
||||
let(:mp4_fixture_path) do
|
||||
Rails.root.join("test/fixtures/files/images/bsky-3l6tnjkcgw72y.mp4").to_s
|
||||
end
|
||||
|
||||
let(:thumbnail_options) do
|
||||
LoadedMedia::ThumbnailOptions.new(
|
||||
@@ -53,14 +56,23 @@ RSpec.describe LoadedMedia do
|
||||
end
|
||||
|
||||
context "with a webm file" do
|
||||
it "creates a LoadedMedia::Webm instance" do
|
||||
it "creates a LoadedMedia::WebmOrMp4 instance" do
|
||||
media = LoadedMedia.from_file("video/webm", webm_fixture_path)
|
||||
|
||||
expect(media).to be_a(LoadedMedia::Webm)
|
||||
expect(media).to be_a(LoadedMedia::WebmOrMp4)
|
||||
expect(media.num_frames).to eq(908)
|
||||
end
|
||||
end
|
||||
|
||||
context "with an mp4 file" do
|
||||
it "creates a LoadedMedia::WebmOrMp4 instance" do
|
||||
media = LoadedMedia.from_file("video/mp4", mp4_fixture_path)
|
||||
|
||||
expect(media).to be_a(LoadedMedia::WebmOrMp4)
|
||||
expect(media.num_frames).to eq(22)
|
||||
end
|
||||
end
|
||||
|
||||
context "with an unsupported file type" do
|
||||
it "returns nil" do
|
||||
media = LoadedMedia.from_file("application/pdf", jpeg_fixture_path)
|
||||
@@ -155,5 +167,30 @@ RSpec.describe LoadedMedia do
|
||||
expect(FileUtils.compare_file(paths[1], paths[2])).to be false
|
||||
end
|
||||
end
|
||||
|
||||
context "with an mp4 file" do
|
||||
it "can extract a frame and save a thumbnail" do
|
||||
media = LoadedMedia.from_file("video/mp4", mp4_fixture_path)
|
||||
|
||||
output_path = make_output_path(0)
|
||||
media.write_frame_thumbnail(0, output_path, thumbnail_options)
|
||||
end
|
||||
|
||||
it "creates different frames" do
|
||||
media = LoadedMedia.from_file("video/mp4", mp4_fixture_path)
|
||||
|
||||
paths =
|
||||
[0, 10, media.num_frames - 1].map do |frame|
|
||||
output_path = make_output_path(frame)
|
||||
media.write_frame_thumbnail(frame, output_path, thumbnail_options)
|
||||
output_path
|
||||
end
|
||||
|
||||
expect(paths.uniq.length).to eq(3)
|
||||
expect(FileUtils.compare_file(paths[0], paths[1])).to be false
|
||||
expect(FileUtils.compare_file(paths[0], paths[2])).to be false
|
||||
expect(FileUtils.compare_file(paths[1], paths[2])).to be false
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
BIN
test/fixtures/files/images/bsky-3l6tnjkcgw72y.mp4
vendored
Normal file
BIN
test/fixtures/files/images/bsky-3l6tnjkcgw72y.mp4
vendored
Normal file
Binary file not shown.
Reference in New Issue
Block a user