mirror of
https://github.com/fluxerapp/fluxer
synced 2026-10-17 21:15:32 +09:00
fix(media): stream large files and honour size settings (#3387)
This commit is contained in:
1 parent
6acd7f3d22
commit
45b78a13c2
97 files changed
+2547
-409
No files matched your search
@@ -120,9 +120,6 @@ pub fn probe_av_metadata(
|
||||
if input.is_empty() {
|
||||
return Err(MediaError::MediaDecodeFailed);
|
||||
}
|
||||
if input.len() > media_limits.max_media_proxy_bytes() {
|
||||
return Err(MediaError::StreamTooLong);
|
||||
}
|
||||
if preview_max_dimension > NSFW_PREVIEW_MAX_DIMENSION {
|
||||
return Err(MediaError::InvalidImageDimensions);
|
||||
}
|
||||
|
||||
@@ -137,15 +137,15 @@ fn metadata_blocking(request: MetadataBlockingRequest<'_>) -> Result<PreparedMet
|
||||
input,
|
||||
options,
|
||||
} = request;
|
||||
if input.len() > constants::MAX_MEDIA_PROXY_BYTES {
|
||||
return Err(MediaError::StreamTooLong);
|
||||
}
|
||||
let sniffed = mime::sniff(input);
|
||||
if !mime::is_supported_media_mime(sniffed.mime) {
|
||||
return Err(MediaError::UnsupportedMediaType);
|
||||
}
|
||||
let initial_category = mime::category(sniffed.mime).ok_or(MediaError::UnsupportedMediaType)?;
|
||||
let is_image = initial_category == mime::Category::Image;
|
||||
if is_image && input.len() > constants::MAX_MEDIA_PROXY_BYTES {
|
||||
return Err(MediaError::StreamTooLong);
|
||||
}
|
||||
let dims = if is_image {
|
||||
let dims = probe_image_dims(media_limits, input)?;
|
||||
validate_jxl_decode_pixels(sniffed.mime, dims)?;
|
||||
|
||||
@@ -37,7 +37,7 @@ pub use metadata::{
|
||||
MetadataOptions, PreparedMetadata, finish_metadata, metadata_json_with_options,
|
||||
prepare_metadata,
|
||||
};
|
||||
pub use native_cost::{AV_NATIVE_COST_BYTES, image_transform_cost, metadata_cost};
|
||||
pub use native_cost::{AV_NATIVE_COST_BYTES, av_native_cost, image_transform_cost, metadata_cost};
|
||||
pub use nsfw_processing::encode_static_image_for_nsfw;
|
||||
pub use transform::transform_image;
|
||||
pub use video_thumbnail::{
|
||||
|
||||
@@ -5,8 +5,8 @@ use super::image_probe::probe_image_dims;
|
||||
use super::loaded_image::static_output_pixels;
|
||||
use super::native_runtime::ensure_vips_init;
|
||||
use crate::{
|
||||
image_quality::ImageQuality, image_transform::ImageOptions, media_limits::MediaLimits, mime,
|
||||
output_format::OutputFormat,
|
||||
constants, image_quality::ImageQuality, image_transform::ImageOptions,
|
||||
media_limits::MediaLimits, mime, output_format::OutputFormat,
|
||||
};
|
||||
|
||||
pub const AV_NATIVE_COST_BYTES: usize = 256 << 20;
|
||||
@@ -53,6 +53,12 @@ pub fn image_transform_cost(
|
||||
Ok(input.len().saturating_add(pixel_cost))
|
||||
}
|
||||
|
||||
pub fn av_native_cost(input_len: usize) -> usize {
|
||||
input_len
|
||||
.min(constants::MAX_MEDIA_PROXY_BYTES)
|
||||
.saturating_add(AV_NATIVE_COST_BYTES)
|
||||
}
|
||||
|
||||
pub fn metadata_cost(input: &[u8], media_limits: &MediaLimits) -> Result<usize, MediaError> {
|
||||
let sniffed_mime = mime::sniff(input).mime;
|
||||
match mime::category(sniffed_mime) {
|
||||
@@ -64,9 +70,7 @@ pub fn metadata_cost(input: &[u8], media_limits: &MediaLimits) -> Result<usize,
|
||||
.len()
|
||||
.saturating_add(source_pixels * decode_bytes_per_pixel(sniffed_mime)))
|
||||
}
|
||||
Some(mime::Category::Video | mime::Category::Audio) => {
|
||||
Ok(input.len().saturating_add(AV_NATIVE_COST_BYTES))
|
||||
}
|
||||
Some(mime::Category::Video | mime::Category::Audio) => Ok(av_native_cost(input.len())),
|
||||
_ => Ok(input.len()),
|
||||
}
|
||||
}
|
||||
@@ -111,7 +111,6 @@ pub(super) async fn classify_nsfw_buffers(
|
||||
|
||||
fn nsfw_video_scan_buffers(source: &NSFWScanSource<'_>) -> Result<Vec<Vec<u8>>, MediaError> {
|
||||
extract_video_frames_for_nsfw(VideoNSFWFramesRequest {
|
||||
media_limits: source.media_limits,
|
||||
input: source.input,
|
||||
duration_seconds: source.duration_seconds,
|
||||
deadline_ms: source.deadline_ms,
|
||||
@@ -363,7 +362,6 @@ pub(super) fn nsfw_frame_seed(input: &[u8]) -> u64 {
|
||||
}
|
||||
|
||||
pub(super) struct VideoNSFWFramesRequest<'a> {
|
||||
pub(super) media_limits: &'a MediaLimits,
|
||||
pub(super) input: &'a [u8],
|
||||
pub(super) duration_seconds: Option<f64>,
|
||||
pub(super) deadline_ms: Option<i64>,
|
||||
@@ -374,13 +372,12 @@ pub(super) fn extract_video_frames_for_nsfw(
|
||||
) -> Result<Vec<Vec<u8>>, MediaError> {
|
||||
use rand::SeedableRng as _;
|
||||
let VideoNSFWFramesRequest {
|
||||
media_limits,
|
||||
input,
|
||||
duration_seconds,
|
||||
deadline_ms,
|
||||
} = request;
|
||||
if input.is_empty() || input.len() > media_limits.max_media_proxy_bytes() {
|
||||
return Err(MediaError::StreamTooLong);
|
||||
if input.is_empty() {
|
||||
return Err(MediaError::MediaDecodeFailed);
|
||||
}
|
||||
let runtime = vips_runtime()?;
|
||||
let seed = nsfw_frame_seed(input);
|
||||
|
||||
@@ -56,7 +56,6 @@ fn video_nsfw_frames_are_downscaled_to_the_frame_cap() {
|
||||
.probe
|
||||
.duration_seconds;
|
||||
let frames = extract_video_frames_for_nsfw(VideoNSFWFramesRequest {
|
||||
media_limits: &media_limits,
|
||||
input: &mp4,
|
||||
duration_seconds: duration,
|
||||
deadline_ms: None,
|
||||
@@ -215,7 +214,6 @@ fn a_sample_beyond_the_packet_budget_keeps_the_frames_that_decoded() {
|
||||
};
|
||||
let media_limits = limits();
|
||||
let frames = extract_video_frames_for_nsfw(VideoNSFWFramesRequest {
|
||||
media_limits: &media_limits,
|
||||
input: &mp4,
|
||||
duration_seconds: Some(12.0),
|
||||
deadline_ms: None,
|
||||
|
||||
@@ -1,22 +1,33 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
use super::super::nsfw_processing::{VideoNSFWFramesRequest, extract_video_frames_for_nsfw};
|
||||
use super::super::placeholder::{encode_thumbhash, optional_thumbhash};
|
||||
use super::super::{
|
||||
MediaError, MetadataOptions, NSFW_PREVIEW_MAX_DIMENSION, metadata_json_with_options,
|
||||
probe_av_metadata,
|
||||
AV_NATIVE_COST_BYTES, MediaError, MetadataOptions, NSFW_PREVIEW_MAX_DIMENSION,
|
||||
extract_video_thumbnail, metadata_cost, metadata_json_with_options, probe_av_metadata,
|
||||
};
|
||||
use super::fixtures::{metadata_value, test_media_limits};
|
||||
use crate::{
|
||||
constants,
|
||||
metrics::transform::TransformMetrics,
|
||||
mime,
|
||||
nsfw::{NSFWClient, NSFWPolicy},
|
||||
output_format::OutputFormat,
|
||||
test_fixtures::{
|
||||
fixture_audio_mp3_with_png_cover_art, fixture_audio_mp4_with_attached_picture,
|
||||
fixture_audio_only_mp4, fixture_h264_mp4, fixture_mkv_with_png_video_stream,
|
||||
fixture_mp4_with_undecodable_video, synthetic_png, synthetic_wav,
|
||||
},
|
||||
};
|
||||
use std::sync::Arc;
|
||||
use std::{io::Write as _, sync::Arc};
|
||||
|
||||
fn mapped_past_the_buffer_bound(head: &[u8]) -> memmap2::Mmap {
|
||||
let mut file = tempfile::tempfile().expect("temp file");
|
||||
file.write_all(head).expect("written head");
|
||||
file.set_len(constants::MAX_MEDIA_PROXY_BYTES as u64 + 1)
|
||||
.expect("sparse tail");
|
||||
unsafe { memmap2::Mmap::map(&file).expect("mapped file") }
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn metadata_json_includes_dimensions_and_placeholder() {
|
||||
@@ -258,3 +269,54 @@ fn metadata_refuses_jxl_above_the_decode_pixel_cap() {
|
||||
});
|
||||
assert_eq!(MediaError::InvalidImageDimensions, err);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn metadata_refuses_an_image_past_the_buffer_bound() {
|
||||
let png = mapped_past_the_buffer_bound(&synthetic_png(16, 16));
|
||||
assert_eq!("image/png", mime::sniff(&png).mime);
|
||||
let err = tokio::runtime::Builder::new_current_thread()
|
||||
.build()
|
||||
.unwrap()
|
||||
.block_on(async {
|
||||
metadata_json_with_options(
|
||||
&png,
|
||||
"huge.png",
|
||||
MetadataOptions::default(),
|
||||
&test_media_limits(),
|
||||
&NSFWClient::disabled(),
|
||||
&TransformMetrics::new(),
|
||||
)
|
||||
.await
|
||||
.unwrap_err()
|
||||
});
|
||||
assert_eq!(MediaError::StreamTooLong, err);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn native_cost_of_a_video_past_the_buffer_bound_leaves_out_the_mapped_tail() {
|
||||
let mp4 = mapped_past_the_buffer_bound(&fixture_h264_mp4());
|
||||
assert_eq!(
|
||||
constants::MAX_MEDIA_PROXY_BYTES + AV_NATIVE_COST_BYTES,
|
||||
metadata_cost(&mp4, &test_media_limits()).expect("video cost")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn video_reads_accept_an_input_past_the_buffer_bound() {
|
||||
let mp4 = mapped_past_the_buffer_bound(&fixture_h264_mp4());
|
||||
let limits = test_media_limits();
|
||||
|
||||
let probe = probe_av_metadata(&mp4, NSFW_PREVIEW_MAX_DIMENSION, &limits, None)
|
||||
.expect("probed video")
|
||||
.probe;
|
||||
assert!(probe.has_video);
|
||||
let poster = extract_video_thumbnail(&mp4, OutputFormat::WebP, &limits).expect("poster frame");
|
||||
assert_eq!("image/webp", mime::sniff(&poster.bytes).mime);
|
||||
let frames = extract_video_frames_for_nsfw(VideoNSFWFramesRequest {
|
||||
input: &mp4,
|
||||
duration_seconds: probe.duration_seconds,
|
||||
deadline_ms: None,
|
||||
})
|
||||
.expect("sampled frames");
|
||||
assert!(!frames.is_empty());
|
||||
}
|
||||
@@ -4,7 +4,7 @@ use super::super::nsfw_processing::{
|
||||
VideoNSFWFramesRequest, compute_frame_sample_timestamps, extract_video_frames_for_nsfw,
|
||||
nsfw_frame_seed,
|
||||
};
|
||||
use super::super::probe_av_metadata;
|
||||
use super::super::{MediaError, probe_av_metadata};
|
||||
use super::fixtures::test_media_limits;
|
||||
|
||||
#[test]
|
||||
@@ -58,7 +58,6 @@ fn extract_video_frames_for_nsfw_returns_multiple_frames() {
|
||||
.probe
|
||||
.duration_seconds;
|
||||
let frames = extract_video_frames_for_nsfw(VideoNSFWFramesRequest {
|
||||
media_limits: &limits,
|
||||
input: &bytes,
|
||||
duration_seconds: duration,
|
||||
deadline_ms: None,
|
||||
@@ -74,3 +73,17 @@ fn extract_video_frames_for_nsfw_returns_multiple_frames() {
|
||||
assert_eq!(&f[..2], &[0xFF, 0xD8], "not a JPEG");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_empty_input_fails_to_decode_for_the_probe_and_the_frame_sampler() {
|
||||
let frames = extract_video_frames_for_nsfw(VideoNSFWFramesRequest {
|
||||
input: &[],
|
||||
duration_seconds: None,
|
||||
deadline_ms: None,
|
||||
});
|
||||
assert_eq!(Some(MediaError::MediaDecodeFailed), frames.err());
|
||||
assert_eq!(
|
||||
Some(MediaError::MediaDecodeFailed),
|
||||
probe_av_metadata(&[], 0, &test_media_limits(), None).err()
|
||||
);
|
||||
}
|
||||
@@ -45,9 +45,6 @@ pub fn extract_video_thumbnail_with_options(
|
||||
options: VideoThumbnailOptions,
|
||||
media_limits: &MediaLimits,
|
||||
) -> Result<ProcessedMedia, MediaError> {
|
||||
if input.len() > media_limits.max_media_proxy_bytes() {
|
||||
return Err(MediaError::StreamTooLong);
|
||||
}
|
||||
let VideoThumbnailOptions {
|
||||
format,
|
||||
width,
|
||||
|
||||
Reference in new issue
Block a user