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:
evazion
2022-10-27 00:44:36 -05:00
parent 48ecb80d6b
commit a9d586e93a
11 changed files with 61 additions and 28 deletions

View File

@@ -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

View File

@@ -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

View File

@@ -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.

View File

@@ -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)

Binary file not shown.

View File

@@ -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")

View File

@@ -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