Fix #3615: Unsupported video codecs.
Don't allow uploading videos with unsupported video codecs. The only video codecs we allow for MP4 files are H.264 and VP9. Other codecs, including H.265 (aka HEVC), MPEG-4 part 2, and AV1, are disallowed because they're not universally supported by browsers. Firefox doesn't support H.265 or MPEG-4 part 2, and Safari doesn't support AV1. Additionally, don't allow videos with multiple video tracks, multiple audio tracks, or no video tracks. Multiple video and audio tracks are disallowed because they're rare and for moderation purposes, we don't want people hiding content in extra tracks. These restrictions really only apply to MP4 videos, since WebM files don't support multiple video or audio tracks and only support a limited number of codecs (VP8 and VP9 for videos, Vorbis and Opus for audio). There are currently 22 posts with unsupported video codecs: * https://danbooru.donmai.us/posts?tags=video+is:mp4+-exif:Track1:CompressorID=avc1+-exif:Track2:CompressorID=avc1+-exif:Track1:CompressorID=vp09+-exif:Track2:CompressorID=vp09 # AVC1 is H.264 There is one post that has multiple audio tracks: * https://danbooru.donmai.us/posts/2382057
This commit is contained in:
@@ -46,11 +46,11 @@ class FFmpeg
|
||||
end
|
||||
|
||||
def width
|
||||
video_streams.first[:width]
|
||||
video_stream[:width]
|
||||
end
|
||||
|
||||
def height
|
||||
video_streams.first[:height]
|
||||
video_stream[:height]
|
||||
end
|
||||
|
||||
# @see https://trac.ffmpeg.org/wiki/FFprobeTips#Duration
|
||||
@@ -68,8 +68,8 @@ class FFmpeg
|
||||
|
||||
# @return [Integer, nil] The number of frames in the video or animation, or nil if unknown.
|
||||
def frame_count
|
||||
if video_streams.first&.has_key?(:nb_frames)
|
||||
video_streams.first[:nb_frames].to_i
|
||||
if video_stream.has_key?(:nb_frames)
|
||||
video_stream[:nb_frames].to_i
|
||||
elsif playback_info.has_key?(:frame)
|
||||
playback_info[:frame].to_i
|
||||
else
|
||||
@@ -83,6 +83,14 @@ class FFmpeg
|
||||
frame_count / duration
|
||||
end
|
||||
|
||||
def video_codec
|
||||
video_stream[:codec_name]
|
||||
end
|
||||
|
||||
def video_stream
|
||||
video_streams.first || {}
|
||||
end
|
||||
|
||||
def video_streams
|
||||
metadata[:streams].to_a.select { |stream| stream[:codec_type] == "video" }
|
||||
end
|
||||
|
||||
@@ -156,6 +156,16 @@ class MediaFile
|
||||
file_ext.in?([:webm, :mp4])
|
||||
end
|
||||
|
||||
# @return [Boolean] True if the file is a MP4.
|
||||
def is_mp4?
|
||||
file_ext == :mp4
|
||||
end
|
||||
|
||||
# @return [Boolean] True if the file is a WebM.
|
||||
def is_webm?
|
||||
file_ext == :webm
|
||||
end
|
||||
|
||||
# @return [Boolean] true if the file is a Pixiv ugoira
|
||||
def is_ugoira?
|
||||
file_ext == :zip
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
#
|
||||
# @see https://github.com/streamio/streamio-ffmpeg
|
||||
class MediaFile::Video < MediaFile
|
||||
delegate :duration, :frame_count, :frame_rate, :has_audio?, to: :video
|
||||
delegate :duration, :frame_count, :frame_rate, :has_audio?, :video_codec, :video_stream, :video_streams, :audio_streams, to: :video
|
||||
|
||||
def dimensions
|
||||
[video.width, video.height]
|
||||
@@ -16,14 +16,12 @@ class MediaFile::Video < MediaFile
|
||||
end
|
||||
|
||||
def is_supported?
|
||||
case file_ext
|
||||
when :webm
|
||||
metadata["Matroska:DocType"] == "webm"
|
||||
when :mp4
|
||||
true
|
||||
else
|
||||
false
|
||||
end
|
||||
return false if video_streams.size != 1
|
||||
return false if audio_streams.size > 1
|
||||
return false if is_webm? && metadata["Matroska:DocType"] != "webm"
|
||||
return false if is_mp4? && !video_codec.in?(["h264", "vp9"])
|
||||
|
||||
true
|
||||
end
|
||||
|
||||
# True if decoding the video fails.
|
||||
|
||||
@@ -31,7 +31,7 @@ class PostPreviewComponentTest < ViewComponent::TestCase
|
||||
|
||||
context "for a video post with sound" do
|
||||
should "render" do
|
||||
@post = create(:post_with_file, tag_string: "sound", filename: "test-audio.mp4").reload
|
||||
@post = create(:post_with_file, tag_string: "sound", filename: "mp4/test-audio.mp4").reload
|
||||
node = render_preview(@post, current_user: User.anonymous)
|
||||
|
||||
assert_equal(post_path(@post), node.css("article a").attr("href").value)
|
||||
|
||||
BIN
test/files/mp4/test-300x300.h265.mp4
Normal file
BIN
test/files/mp4/test-300x300.h265.mp4
Normal file
Binary file not shown.
@@ -241,6 +241,16 @@ class UploadsControllerTest < ActionDispatch::IntegrationTest
|
||||
create_upload!("test/files/webm/test-512x512.mkv", user: @user)
|
||||
assert_match("File type is not supported", Upload.last.error)
|
||||
end
|
||||
|
||||
should "fail for a .mp4 file encoded with h265" do
|
||||
create_upload!("test/files/mp4/test-300x300.h265.mp4", user: @user)
|
||||
assert_match("File type is not supported", Upload.last.error)
|
||||
end
|
||||
|
||||
should "fail for a .mp4 file encoded with av1" do
|
||||
create_upload!("test/files/mp4/test-300x300.av1.mp4", user: @user)
|
||||
assert_match("File type is not supported", Upload.last.error)
|
||||
end
|
||||
end
|
||||
|
||||
context "for a video longer than the video length limit" do
|
||||
@@ -334,10 +344,11 @@ class UploadsControllerTest < ActionDispatch::IntegrationTest
|
||||
should_upload_successfully("test/files/test.png")
|
||||
should_upload_successfully("test/files/test-static-32x32.gif")
|
||||
should_upload_successfully("test/files/test-animated-86x52.gif")
|
||||
should_upload_successfully("test/files/test-300x300.mp4")
|
||||
should_upload_successfully("test/files/test-audio.mp4")
|
||||
should_upload_successfully("test/files/mp4/test-300x300.mp4")
|
||||
should_upload_successfully("test/files/mp4/test-300x300.vp9.mp4")
|
||||
should_upload_successfully("test/files/mp4/test-audio.mp4")
|
||||
should_upload_successfully("test/files/mp4/test-audio.m4v")
|
||||
should_upload_successfully("test/files/webm/test-512x512.webm")
|
||||
should_upload_successfully("test/files/test-audio.m4v")
|
||||
# should_upload_successfully("test/files/compressed.swf")
|
||||
|
||||
should_upload_successfully("test/files/avif/fox.profile0.8bpc.yuv420.monochrome.avif")
|
||||
|
||||
@@ -36,7 +36,7 @@ class MediaFileTest < ActiveSupport::TestCase
|
||||
|
||||
should "determine the correct dimensions for a mp4 file" do
|
||||
skip unless MediaFile.videos_enabled?
|
||||
assert_equal([300, 300], MediaFile.open("test/files/test-300x300.mp4").dimensions)
|
||||
assert_equal([300, 300], MediaFile.open("test/files/mp4/test-300x300.mp4").dimensions)
|
||||
end
|
||||
|
||||
should "determine the correct dimensions for a ugoira file" do
|
||||
@@ -110,15 +110,15 @@ class MediaFileTest < ActiveSupport::TestCase
|
||||
end
|
||||
|
||||
should "determine the correct extension for a mp4 file" do
|
||||
assert_equal(:mp4, MediaFile.open("test/files/test-300x300.mp4").file_ext)
|
||||
assert_equal(:mp4, MediaFile.open("test/files/mp4/test-300x300.mp4").file_ext)
|
||||
end
|
||||
|
||||
should "determine the correct extension for a m4v file" do
|
||||
assert_equal(:mp4, MediaFile.open("test/files/test-audio.m4v").file_ext)
|
||||
assert_equal(:mp4, MediaFile.open("test/files/mp4/test-audio.m4v").file_ext)
|
||||
end
|
||||
|
||||
should "determine the correct extension for an iso5 mp4 file" do
|
||||
assert_equal(:mp4, MediaFile.open("test/files/test-iso5.mp4").file_ext)
|
||||
assert_equal(:mp4, MediaFile.open("test/files/mp4/test-iso5.mp4").file_ext)
|
||||
end
|
||||
|
||||
should "determine the correct extension for a ugoira file" do
|
||||
@@ -162,7 +162,7 @@ class MediaFileTest < ActiveSupport::TestCase
|
||||
should "generate a preview image for a video" do
|
||||
skip unless MediaFile.videos_enabled?
|
||||
assert_equal([150, 150], MediaFile.open("test/files/webm/test-512x512.webm").preview(150, 150).dimensions)
|
||||
assert_equal([150, 150], MediaFile.open("test/files/test-300x300.mp4").preview(150, 150).dimensions)
|
||||
assert_equal([150, 150], MediaFile.open("test/files/mp4/test-300x300.mp4").preview(150, 150).dimensions)
|
||||
end
|
||||
|
||||
should "be able to fit to width only" do
|
||||
@@ -196,18 +196,18 @@ class MediaFileTest < ActiveSupport::TestCase
|
||||
|
||||
context "for an mp4 file " do
|
||||
should "detect videos with audio" do
|
||||
assert_equal(true, MediaFile.open("test/files/test-audio.mp4").has_audio?)
|
||||
assert_equal(false, MediaFile.open("test/files/test-300x300.mp4").has_audio?)
|
||||
assert_equal(true, MediaFile.open("test/files/mp4/test-audio.mp4").has_audio?)
|
||||
assert_equal(false, MediaFile.open("test/files/mp4/test-300x300.mp4").has_audio?)
|
||||
end
|
||||
|
||||
should "determine the duration of the video" do
|
||||
file = MediaFile.open("test/files/test-audio.mp4")
|
||||
file = MediaFile.open("test/files/mp4/test-audio.mp4")
|
||||
assert_equal(false, file.is_corrupt?)
|
||||
assert_equal(1.002667, file.duration)
|
||||
assert_equal(10/1.002667, file.frame_rate)
|
||||
assert_equal(10, file.frame_count)
|
||||
|
||||
file = MediaFile.open("test/files/test-300x300.mp4")
|
||||
file = MediaFile.open("test/files/mp4/test-300x300.mp4")
|
||||
assert_equal(false, file.is_corrupt?)
|
||||
assert_equal(5.7, file.duration)
|
||||
assert_equal(1.75, file.frame_rate.round(2))
|
||||
@@ -215,9 +215,15 @@ class MediaFileTest < ActiveSupport::TestCase
|
||||
end
|
||||
|
||||
should "detect corrupt videos" do
|
||||
file = MediaFile.open("test/files/mp4/test-corrupt.mp4")
|
||||
assert_equal(true, MediaFile.open("test/files/mp4/test-corrupt.mp4").is_corrupt?)
|
||||
end
|
||||
|
||||
assert_equal(true, file.is_corrupt?)
|
||||
should "detect supported files" do
|
||||
assert_equal(true, MediaFile.open("test/files/mp4/test-300x300.mp4").is_supported?)
|
||||
assert_equal(true, MediaFile.open("test/files/mp4/test-300x300.vp9.mp4").is_supported?)
|
||||
|
||||
assert_equal(false, MediaFile.open("test/files/mp4/test-300x300.h265.mp4").is_supported?)
|
||||
assert_equal(false, MediaFile.open("test/files/mp4/test-300x300.av1.mp4").is_supported?)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
Reference in New Issue
Block a user