diff --git a/app/logical/ffmpeg.rb b/app/logical/ffmpeg.rb index a89984bd8..35d0b62f0 100644 --- a/app/logical/ffmpeg.rb +++ b/app/logical/ffmpeg.rb @@ -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 diff --git a/app/logical/media_file.rb b/app/logical/media_file.rb index ebb145378..9b74ef336 100644 --- a/app/logical/media_file.rb +++ b/app/logical/media_file.rb @@ -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 diff --git a/app/logical/media_file/video.rb b/app/logical/media_file/video.rb index badab7be6..ac371ced5 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?, 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. diff --git a/test/components/post_preview_component_test.rb b/test/components/post_preview_component_test.rb index 06003769a..e80b2dfbd 100644 --- a/test/components/post_preview_component_test.rb +++ b/test/components/post_preview_component_test.rb @@ -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) diff --git a/test/files/mp4/test-300x300.h265.mp4 b/test/files/mp4/test-300x300.h265.mp4 new file mode 100644 index 000000000..c8df94aa8 Binary files /dev/null and b/test/files/mp4/test-300x300.h265.mp4 differ diff --git a/test/files/test-300x300.mp4 b/test/files/mp4/test-300x300.mp4 similarity index 100% rename from test/files/test-300x300.mp4 rename to test/files/mp4/test-300x300.mp4 diff --git a/test/files/test-audio.m4v b/test/files/mp4/test-audio.m4v similarity index 100% rename from test/files/test-audio.m4v rename to test/files/mp4/test-audio.m4v diff --git a/test/files/test-audio.mp4 b/test/files/mp4/test-audio.mp4 similarity index 100% rename from test/files/test-audio.mp4 rename to test/files/mp4/test-audio.mp4 diff --git a/test/files/test-iso5.mp4 b/test/files/mp4/test-iso5.mp4 similarity index 100% rename from test/files/test-iso5.mp4 rename to test/files/mp4/test-iso5.mp4 diff --git a/test/functional/uploads_controller_test.rb b/test/functional/uploads_controller_test.rb index 82aa5efd5..7babf6f11 100644 --- a/test/functional/uploads_controller_test.rb +++ b/test/functional/uploads_controller_test.rb @@ -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") diff --git a/test/unit/media_file_test.rb b/test/unit/media_file_test.rb index 2e2e2c604..0e316409c 100644 --- a/test/unit/media_file_test.rb +++ b/test/unit/media_file_test.rb @@ -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