Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: fall back to chunked mode for unknown frame buffer sizes #62

Merged
merged 1 commit into from
Nov 6, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion src/command.rs
Original file line number Diff line number Diff line change
Expand Up @@ -542,7 +542,8 @@ impl FfmpegCommand {
//// Preset argument sets for common use cases.

/// Generate a procedural test video. Equivalent to `ffmpeg -f lavfi -i
/// testsrc=duration=10`.
/// testsrc=duration=10`. It also inherits defaults from the `testsrc` filter
/// in FFmpeg: `320x240` size and `25` fps.
///
/// [FFmpeg `testsrc` filter
/// documentation](https://ffmpeg.org/ffmpeg-filters.html#allrgb_002c-allyuv_002c-color_002c-colorchart_002c-colorspectrum_002c-haldclutsrc_002c-nullsrc_002c-pal75bars_002c-pal100bars_002c-rgbtestsrc_002c-smptebars_002c-smptehdbars_002c-testsrc_002c-testsrc2_002c-yuvtestsrc)
Expand Down
185 changes: 104 additions & 81 deletions src/iter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -198,83 +198,119 @@ pub fn spawn_stdout_thread(
.unwrap_or(false)
});

// Error on mixing rawvideo and non-rawvideo streams
// TODO: Maybe just revert to chunk mode if this happens?
let any_rawvideo = stdout_output_video_streams
.clone()
.any(|s| s.format == "rawvideo");
let any_non_rawvideo = stdout_output_video_streams
.clone()
.any(|s| s.format != "rawvideo");
if any_rawvideo && any_non_rawvideo {
panic!("Cannot mix rawvideo and non-rawvideo streams");
// Exit early if nothing is being sent to stdout
if stdout_output_video_streams.clone().count() == 0 {
return;
}

// Prepare buffers
let mut buffers = stdout_output_video_streams
// If the size of a frame can't be determined, it will be read in arbitrary chunks.
let mut chunked_mode = false;

// Calculate frame buffer sizes up front.
// Any sizes that cannot be calculated will trigger chunked mode.
let frame_buffer_sizes: Vec<usize> = stdout_output_video_streams
.clone()
.map(|video_stream| {
// Since we filtered for video_streams above, we can unwrap unconditionally.
let buf_size = match video_stream.format.as_str() {
"rawvideo" => {
let Some(video_data) = video_stream.video_data() else {
tx.send(FfmpegEvent::Error(
"Video stream doesn't have any video data".to_owned(),
))
.ok();
return Vec::new();
};

let Some(bytes_per_frame) = get_bytes_per_frame(video_data) else {
tx.send(FfmpegEvent::Error(
format!("Can't bytes per fraame for video data {video_data:?}").to_owned(),
))
.ok();
return Vec::new();
};

bytes_per_frame as usize
}
// Any non-rawvideo streams instantly enable chunked mode, since it's
// impossible to tell when one chunk ends and another begins.
if video_stream.format != "rawvideo" {
chunked_mode = true;
return 0;
}

// Arbitrary default buffer size for receiving indeterminate chunks
// of any encoder or container output, when frame boundaries are unknown
_ => 32_768, // ~= 32mb (plenty large enough for any chunk of video at reasonable bitrate)
// This is an unexpected error since we've already filtered for video streams.
let Some(video_data) = video_stream.video_data() else {
chunked_mode = true;
return 0;
};

// Catch unsupported pixel formats
assert!(
buf_size > 0,
"Unsupported pixel format with 0 bytes per pixel"
);
// This may trigger either on an unsupported pixel format, or
// framebuffers with non-byte-aligned sizes. FFmpeg will pad these with
// zeroes, but we can't predict the exact padding or end size on every format.
let Some(bytes_per_frame) = get_bytes_per_frame(video_data) else {
chunked_mode = true;
return 0;
};

vec![0u8; buf_size]
bytes_per_frame as usize
})
.collect::<Vec<Vec<u8>>>();
.collect();

// No buffers probably indicates that output is being sent to file or
// that an error occured.
if buffers.is_empty() {
return;
// Final check: FFmpeg supports multiple outputs interleaved on stdout,
// but we can only keep track of them if the framerates match. It's
// theoretically still possible to determine the expected frame order,
// but it's not currently supported.
let output_framerates: Vec<f32> = stdout_output_video_streams
.clone()
.filter(|s| s.format == "rawvideo")
.map(|video_stream| {
if let Some(video_data) = video_stream.video_data() {
return video_data.fps;
} else {
return -1.0;
}
})
.collect();
let any_mismatched_framerates = output_framerates
.iter()
.any(|&fps| fps != output_framerates[0] || fps == -1.0);
if any_mismatched_framerates {
// This edge case is probably not what the user was intending,
// so we'll notify with an error.
tx.send(FfmpegEvent::Error(
"Multiple output streams with different framerates are not supported when outputting to stdout. Falling back to chunked mode.".to_owned()
)).ok();
chunked_mode = true;
}

// Read into buffers
let num_buffers = buffers.len();
let mut buffer_index = (0..buffers.len()).cycle();
let mut reader = BufReader::new(stdout);
let mut frame_num = 0;
loop {
let i = buffer_index.next().unwrap();
let video_stream = &output_streams[i];
// Since we filtered for video_streams above, we can unwrap unconditionally.
let video_data = video_stream.video_data().unwrap();
let buffer = &mut buffers[i];
let output_frame_num = frame_num / num_buffers;
let timestamp = output_frame_num as f32 / video_data.fps;
frame_num += 1;

// Handle two scenarios:
match video_stream.format.as_str() {
// 1. `rawvideo` with exactly known pixel layout
"rawvideo" => match reader.read_exact(buffer.as_mut_slice()) {
if chunked_mode {
// Arbitrary default buffer size for receiving indeterminate chunks
// of any encoder or container output, when frame boundaries are unknown
let mut chunk_buffer = vec![0u8; 65_536];
loop {
match reader.read(chunk_buffer.as_mut_slice()) {
Ok(0) => break,
Ok(bytes_read) => {
let mut data = vec![0; bytes_read];
data.clone_from_slice(&chunk_buffer[..bytes_read]);
tx.send(FfmpegEvent::OutputChunk(data)).ok()
}
Err(e) => match e.kind() {
ErrorKind::UnexpectedEof => break,
e => tx.send(FfmpegEvent::Error(e.to_string())).ok(),
},
};
}
} else {
// Prepare frame buffers
let mut frame_buffers = frame_buffer_sizes
.iter()
.map(|&size| vec![0u8; size])
.collect::<Vec<Vec<u8>>>();

// Empty buffer array is unexpected at this point, since we've already ruled out
// both chunked mode and non-stdout streams.
if frame_buffers.is_empty() {
tx.send(FfmpegEvent::Error("No frame buffers found".to_owned()))
.ok();
return;
}

// Read into buffers
let num_frame_buffers = frame_buffers.len();
let mut frame_buffer_index = (0..frame_buffers.len()).cycle();
let mut frame_num = 0;
loop {
let i = frame_buffer_index.next().unwrap();
let video_stream = &output_streams[i];
let video_data = video_stream.video_data().unwrap();
let buffer = &mut frame_buffers[i];
let output_frame_num = frame_num / num_frame_buffers;
let timestamp = output_frame_num as f32 / video_data.fps;
frame_num += 1;

match reader.read_exact(buffer.as_mut_slice()) {
Ok(_) => tx
.send(FfmpegEvent::OutputFrame(OutputVideoFrame {
width: video_data.width,
Expand All @@ -290,23 +326,10 @@ pub fn spawn_stdout_thread(
ErrorKind::UnexpectedEof => break,
e => tx.send(FfmpegEvent::Error(e.to_string())).ok(),
},
},

// 2. Anything else, with unknown buffer size
_ => match reader.read(buffer.as_mut_slice()) {
Ok(0) => break,
Ok(bytes_read) => {
let mut data = vec![0; bytes_read];
data.clone_from_slice(&buffer[..bytes_read]);
tx.send(FfmpegEvent::OutputChunk(data)).ok()
}
Err(e) => match e.kind() {
ErrorKind::UnexpectedEof => break,
e => tx.send(FfmpegEvent::Error(e.to_string())).ok(),
},
},
};
};
}
}

tx.send(FfmpegEvent::Done).ok();
})
}
Expand Down
Loading
Loading