Files
danbooru/app/logical/media_file/ugoira.rb
evazion 99221af855 ugoiras: fix regression in 7031fd13d.
Fix `Cannot write log file 'ffmpeg2pass-0.log' for pass-1 encoding: Permission denied` error
when uploading ugoira files. Caused by the fact that 2-pass encoding tries to write a log file in
the current directory by default, which fails in production because the default working directory in
the Docker image is /danbooru, which is read-only.
2022-03-01 00:16:55 -06:00

113 lines
3.6 KiB
Ruby

# frozen_string_literal: true
# A MediaFile for a Pixiv ugoira file.
#
# A Pixiv ugoira is an animation format that consists of a zip file containing
# JPEG or PNG images, one per frame, plus a JSON object containing the
# inter-frame delay timings. Each frame can have a different delay, therefore
# ugoiras can have a variable framerate. The frame data isn't stored inside the
# zip file, so it must be passed around separately.
class MediaFile::Ugoira < MediaFile
class Error < StandardError; end
attr_accessor :frame_data
def initialize(file, frame_data: {}, **options)
super(file, **options)
@frame_data = frame_data
end
def close
file.close
zipfile.close
preview_frame.close
end
def dimensions
preview_frame.dimensions
end
def preview(width, height, **options)
preview_frame.preview(width, height, **options)
end
def duration
(frame_delays.sum / 1000.0)
end
def frame_count
frame_data.count
end
def frame_rate
frame_count / duration
end
def frame_delays
frame_data.map { |frame| frame["delay"] }
end
# Convert a ugoira to a webm.
# XXX should take width and height and resize image
def convert
raise NotImplementedError, "can't convert ugoira to webm: ffmpeg or mkvmerge not installed" unless self.class.videos_enabled?
raise RuntimeError, "can't convert ugoira to webm: no ugoira frame data was provided" unless frame_data.present?
Dir.mktmpdir("ugoira-#{md5}") do |tmpdir|
output_file = Tempfile.new(["ugoira-conversion", ".webm"], binmode: true)
FileUtils.mkdir_p("#{tmpdir}/images")
zipfile.each do |entry|
path = File.join(tmpdir, "images", entry.name)
entry.extract(path)
end
# Duplicate last frame to avoid it being displayed only for a very short amount of time.
last_file_name = zipfile.entries.last.name
last_file_name =~ /\A(\d{6})(\.\w{,4})\Z/
new_last_index = $1.to_i + 1
file_ext = $2
new_last_filename = ("%06d" % new_last_index) + file_ext
path_from = File.join(tmpdir, "images", last_file_name)
path_to = File.join(tmpdir, "images", new_last_filename)
FileUtils.cp(path_from, path_to)
delay_sum = 0
timecodes_path = File.join(tmpdir, "timecodes.tc")
File.open(timecodes_path, "w+") do |f|
f.write("# timecode format v2\n")
frame_data.each do |img|
f.write("#{delay_sum}\n")
delay_sum += (img["delay"] || img["delay_msec"])
end
f.write("#{delay_sum}\n")
f.write("#{delay_sum}\n")
end
ext = zipfile.first.name.match(/\.(\w{,4})$/)[1]
ffmpeg_out, status = Open3.capture2e("ffmpeg -i #{tmpdir}/images/%06d.#{ext} -codec:v libvpx-vp9 -crf 12 -b:v 0 -an -threads 8 -tile-columns 2 -tile-rows 1 -row-mt 1 -pass 1 -passlogfile #{tmpdir}/ffmpeg2pass -f null /dev/null")
raise Error, "ffmpeg failed: #{ffmpeg_out}" unless status.success?
ffmpeg_out, status = Open3.capture2e("ffmpeg -i #{tmpdir}/images/%06d.#{ext} -codec:v libvpx-vp9 -crf 12 -b:v 0 -an -threads 8 -tile-columns 2 -tile-rows 1 -row-mt 1 -pass 2 -passlogfile #{tmpdir}/ffmpeg2pass #{tmpdir}/tmp.webm")
raise Error, "ffmpeg failed: #{ffmpeg_out}" unless status.success?
mkvmerge_out, status = Open3.capture2e("mkvmerge -o #{output_file.path} --webm --timecodes 0:#{tmpdir}/timecodes.tc #{tmpdir}/tmp.webm")
raise Error, "mkvmerge failed: #{mkvmerge_out}" unless status.success?
MediaFile.open(output_file)
end
end
private
def zipfile
Zip::File.new(file.path)
end
def preview_frame
FFmpeg.new(convert).smart_video_preview
end
memoize :zipfile, :preview_frame, :dimensions, :convert
end