diff --git a/app/logical/ffmpeg.rb b/app/logical/ffmpeg.rb index 35d0b62f0..fc4612bb2 100644 --- a/app/logical/ffmpeg.rb +++ b/app/logical/ffmpeg.rb @@ -83,6 +83,10 @@ class FFmpeg frame_count / duration end + def pix_fmt + video_stream[:pix_fmt] + end + def video_codec video_stream[:codec_name] end diff --git a/app/logical/media_file/video.rb b/app/logical/media_file/video.rb index ac371ced5..0986ed615 100644 --- a/app/logical/media_file/video.rb +++ b/app/logical/media_file/video.rb @@ -5,7 +5,7 @@ # # @see https://github.com/streamio/streamio-ffmpeg class MediaFile::Video < MediaFile - delegate :duration, :frame_count, :frame_rate, :has_audio?, :video_codec, :video_stream, :video_streams, :audio_streams, to: :video + delegate :duration, :frame_count, :frame_rate, :has_audio?, :pix_fmt, :video_codec, :video_stream, :video_streams, :audio_streams, to: :video def dimensions [video.width, video.height] @@ -21,6 +21,17 @@ class MediaFile::Video < MediaFile return false if is_webm? && metadata["Matroska:DocType"] != "webm" return false if is_mp4? && !video_codec.in?(["h264", "vp9"]) + # Only allow pixel formats supported by most browsers. Don't allow 10-bit video or 4:4:4 subsampling (neither are supported by Firefox). + # + # yuv420p: 8-bit YUV, 4:2:0 subsampling. The vast majority of videos use this format. + # yuvj420p: 8-bit YUV, 4:2:0 subsampling, color range restricted to 16-235. Uncommon, but widely supported. + # yuv444p: 8-bit YUV, 4:4:4 subsampling (i.e. no subsampling). Uncommon, not supported by Firefox. + # yuv420p10le: 10-bit YUV, 4:2:0 subsampling (i.e. 10-bit video). Uncommon, not supported by Firefox. + # gbrp: 8-bit RGB (used by VP9). Uncommon, but widely supported. + # + # https://github.com/FFmpeg/FFmpeg/blob/master/libavutil/pixfmt.h + return false if !pix_fmt.in?(%w[yuv420p yuvj420p gbrp]) + true end diff --git a/test/files/mp4/README.md b/test/files/mp4/README.md new file mode 100644 index 000000000..4a47f4b70 --- /dev/null +++ b/test/files/mp4/README.md @@ -0,0 +1,6 @@ +Test file sources: + +* https://danbooru.donmai.us/posts/2878908 (https://twitter.com/chan_co/status/913025965604749314) +* https://danbooru.donmai.us/posts/4270949 (https://twitter.com/chrone_co/status/1342709028401643520) +* https://danbooru.donmai.us/posts/5152189 (https://twitter.com/001_31_/status/1491405055563792386) +* https://github.com/jursonovicst/gradient/blob/master/player_validation_sequences/test_yuv420p10le_x265.mp4 diff --git a/test/files/mp4/test-300x300-av1.mp4 b/test/files/mp4/test-300x300-av1.mp4 new file mode 100644 index 000000000..17da034b5 Binary files /dev/null and b/test/files/mp4/test-300x300-av1.mp4 differ diff --git a/test/files/mp4/test-300x300.h265.mp4 b/test/files/mp4/test-300x300-h265.mp4 similarity index 100% rename from test/files/mp4/test-300x300.h265.mp4 rename to test/files/mp4/test-300x300-h265.mp4 diff --git a/test/files/mp4/test-300x300-vp9.mp4 b/test/files/mp4/test-300x300-vp9.mp4 new file mode 100644 index 000000000..adc8a2d2c Binary files /dev/null and b/test/files/mp4/test-300x300-vp9.mp4 differ diff --git a/test/files/mp4/test-300x300-yuv444p-h264.mp4 b/test/files/mp4/test-300x300-yuv444p-h264.mp4 new file mode 100644 index 000000000..76ab8e3da Binary files /dev/null and b/test/files/mp4/test-300x300-yuv444p-h264.mp4 differ diff --git a/test/files/mp4/test-300x300-yuvj420p-h264.mp4 b/test/files/mp4/test-300x300-yuvj420p-h264.mp4 new file mode 100644 index 000000000..d1f11d608 Binary files /dev/null and b/test/files/mp4/test-300x300-yuvj420p-h264.mp4 differ diff --git a/test/files/mp4/test-yuv420p10le-av1.mp4 b/test/files/mp4/test-yuv420p10le-av1.mp4 new file mode 100644 index 000000000..84bcbc297 Binary files /dev/null and b/test/files/mp4/test-yuv420p10le-av1.mp4 differ diff --git a/test/files/mp4/test-yuv420p10le-h264.mp4 b/test/files/mp4/test-yuv420p10le-h264.mp4 new file mode 100644 index 000000000..18ef037da Binary files /dev/null and b/test/files/mp4/test-yuv420p10le-h264.mp4 differ diff --git a/test/files/mp4/test-yuv420p10le-vp9.mp4 b/test/files/mp4/test-yuv420p10le-vp9.mp4 new file mode 100644 index 000000000..0f223f959 Binary files /dev/null and b/test/files/mp4/test-yuv420p10le-vp9.mp4 differ diff --git a/test/files/webm/test-gbrp-vp9.webm b/test/files/webm/test-gbrp-vp9.webm new file mode 100644 index 000000000..8388890a8 Binary files /dev/null and b/test/files/webm/test-gbrp-vp9.webm differ diff --git a/test/files/webm/test-yuv420p10le-vp9.webm b/test/files/webm/test-yuv420p10le-vp9.webm new file mode 100644 index 000000000..17e5b4de4 Binary files /dev/null and b/test/files/webm/test-yuv420p10le-vp9.webm differ diff --git a/test/functional/uploads_controller_test.rb b/test/functional/uploads_controller_test.rb index 7babf6f11..d480ec790 100644 --- a/test/functional/uploads_controller_test.rb +++ b/test/functional/uploads_controller_test.rb @@ -243,12 +243,37 @@ class UploadsControllerTest < ActionDispatch::IntegrationTest end should "fail for a .mp4 file encoded with h265" do - create_upload!("test/files/mp4/test-300x300.h265.mp4", user: @user) + 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) + create_upload!("test/files/mp4/test-300x300-av1.mp4", user: @user) + assert_match("File type is not supported", Upload.last.error) + end + + should "fail for a 10-bit color .mp4 file encoded with av1" do + create_upload!("test/files/mp4/test-yuv420p10le-av1.mp4", user: @user) + assert_match("File type is not supported", Upload.last.error) + end + + should "fail for a 10-bit color .mp4 file encoded with h264" do + create_upload!("test/files/mp4/test-yuv420p10le-h264.mp4", user: @user) + assert_match("File type is not supported", Upload.last.error) + end + + should "fail for a 10-bit color .mp4 file encoded with vp9" do + create_upload!("test/files/mp4/test-yuv420p10le-vp9.mp4", user: @user) + assert_match("File type is not supported", Upload.last.error) + end + + should "fail for a 4:4:4 subsampled .mp4 file" do + create_upload!("test/files/mp4/test-300x300-yuv444p-h264.mp4", user: @user) + assert_match("File type is not supported", Upload.last.error) + end + + should "fail for a 10-bit color .webm file encoded with vp9" do + create_upload!("test/files/webm/test-yuv420p10le-vp9.webm", user: @user) assert_match("File type is not supported", Upload.last.error) end end @@ -335,7 +360,7 @@ class UploadsControllerTest < ActionDispatch::IntegrationTest assert_equal([550, 368], full_variant.dimensions) assert_equal(:jpg, full_variant.file_ext) - assert_equal(nil, media_asset.variant(:sample)) + assert_nil(media_asset.variant(:sample)) end end @@ -345,10 +370,12 @@ class UploadsControllerTest < ActionDispatch::IntegrationTest should_upload_successfully("test/files/test-static-32x32.gif") should_upload_successfully("test/files/test-animated-86x52.gif") 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-300x300-vp9.mp4") + should_upload_successfully("test/files/mp4/test-300x300-yuvj420p-h264.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/webm/test-gbrp-vp9.webm") # should_upload_successfully("test/files/compressed.swf") should_upload_successfully("test/files/avif/fox.profile0.8bpc.yuv420.monochrome.avif") diff --git a/test/unit/media_file_test.rb b/test/unit/media_file_test.rb index 0e316409c..e79e5d2e3 100644 --- a/test/unit/media_file_test.rb +++ b/test/unit/media_file_test.rb @@ -214,16 +214,36 @@ class MediaFileTest < ActiveSupport::TestCase assert_equal(10, file.frame_count) end + should "determine the pixel format of the video" do + assert_equal("yuv420p", MediaFile.open("test/files/mp4/test-300x300-av1.mp4").pix_fmt) + assert_equal("yuv420p", MediaFile.open("test/files/mp4/test-300x300-h265.mp4").pix_fmt) + assert_equal("yuv420p", MediaFile.open("test/files/mp4/test-300x300-vp9.mp4").pix_fmt) + assert_equal("yuv420p", MediaFile.open("test/files/mp4/test-300x300.mp4").pix_fmt) + assert_equal("yuv420p", MediaFile.open("test/files/mp4/test-audio.m4v").pix_fmt) + assert_equal("yuv420p", MediaFile.open("test/files/mp4/test-audio.mp4").pix_fmt) + assert_equal("yuv420p", MediaFile.open("test/files/mp4/test-iso5.mp4").pix_fmt) + assert_equal("yuv444p", MediaFile.open("test/files/mp4/test-300x300-yuv444p-h264.mp4").pix_fmt) + assert_equal("yuvj420p", MediaFile.open("test/files/mp4/test-300x300-yuvj420p-h264.mp4").pix_fmt) + assert_equal("yuv420p10le", MediaFile.open("test/files/mp4/test-yuv420p10le-av1.mp4").pix_fmt) + assert_equal("yuv420p10le", MediaFile.open("test/files/mp4/test-yuv420p10le-h264.mp4").pix_fmt) + assert_equal("yuv420p10le", MediaFile.open("test/files/mp4/test-yuv420p10le-vp9.mp4").pix_fmt) + end + should "detect corrupt videos" do assert_equal(true, MediaFile.open("test/files/mp4/test-corrupt.mp4").is_corrupt?) end 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(true, MediaFile.open("test/files/mp4/test-300x300-vp9.mp4").is_supported?) + assert_equal(true, MediaFile.open("test/files/mp4/test-300x300-yuvj420p-h264.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?) + 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?) + assert_equal(false, MediaFile.open("test/files/mp4/test-300x300-yuv444p-h264.mp4").is_supported?) + assert_equal(false, MediaFile.open("test/files/mp4/test-yuv420p10le-av1.mp4").is_supported?) + assert_equal(false, MediaFile.open("test/files/mp4/test-yuv420p10le-h264.mp4").is_supported?) + assert_equal(false, MediaFile.open("test/files/mp4/test-yuv420p10le-vp9.mp4").is_supported?) end end @@ -235,10 +255,12 @@ class MediaFileTest < ActiveSupport::TestCase assert_equal(10, file.frame_count) end - should "not detect .mkv files as .webm" do - file = MediaFile.open("test/files/webm/test-512x512.mkv") + should "detect supported files" do + assert_equal(true, MediaFile.open("test/files/webm/test-512x512.webm").is_supported?) + assert_equal(true, MediaFile.open("test/files/webm/test-gbrp-vp9.webm").is_supported?) - assert_equal(false, file.is_supported?) + assert_equal(false, MediaFile.open("test/files/webm/test-512x512.mkv").is_supported?) + assert_equal(false, MediaFile.open("test/files/webm/test-yuv420p10le-vp9.webm").is_supported?) end end