Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 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 .claude/settings.local.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,8 @@
"Bash(rustfmt:*)",
"Bash(cargo tree:*)",
"WebFetch(domain:github.com)",
"WebFetch(domain:docs.rs)"
"WebFetch(domain:docs.rs)",
"WebFetch(domain:gix.github.io)"
],
"deny": [],
"ask": []
Expand Down
2 changes: 2 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions crates/camera-ffmpeg/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ license = "MIT"
[dependencies]
ffmpeg = { workspace = true }
thiserror.workspace = true
tracing.workspace = true
cap-camera = { path = "../camera" }
workspace-hack = { version = "0.1", path = "../workspace-hack" }

Expand Down
155 changes: 154 additions & 1 deletion crates/camera-ffmpeg/src/macos.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
use cap_camera::CapturedFrame;
use cap_camera_avfoundation::ImageBufExt;
use cidre::*;
use ffmpeg::{format::Pixel, software::scaling};
use std::sync::atomic::{AtomicBool, Ordering};

use crate::CapturedFrameExt;

Expand All @@ -14,10 +16,41 @@ pub enum AsFFmpegError {
expected: usize,
found: usize,
},
#[error("Swscale fallback failed for format '{format}': {reason}")]
SwscaleFallbackFailed { format: String, reason: String },
#[error("{0}")]
Native(#[from] cidre::os::Error),
}

struct FourccInfo {
pixel: Pixel,
bytes_per_pixel: usize,
}

fn fourcc_to_pixel_format(fourcc: &str) -> Option<FourccInfo> {
match fourcc {
"ABGR" => Some(FourccInfo {
pixel: Pixel::ABGR,
bytes_per_pixel: 4,
}),
"b64a" => Some(FourccInfo {
pixel: Pixel::RGBA64BE,
bytes_per_pixel: 8,
}),
"b48r" => Some(FourccInfo {
pixel: Pixel::RGB48BE,
bytes_per_pixel: 6,
}),
"L016" => Some(FourccInfo {
pixel: Pixel::GRAY16LE,
bytes_per_pixel: 2,
}),
_ => None,
}
}

static FALLBACK_WARNING_LOGGED: AtomicBool = AtomicBool::new(false);

impl CapturedFrameExt for CapturedFrame {
fn as_ffmpeg(&self) -> Result<ffmpeg::frame::Video, AsFFmpegError> {
let native = self.native().clone();
Expand Down Expand Up @@ -162,6 +195,29 @@ impl CapturedFrameExt for CapturedFrame {

ff_frame
}
"RGBA" => {
let mut ff_frame = ffmpeg::frame::Video::new(
ffmpeg::format::Pixel::RGBA,
width as u32,
height as u32,
);

let src_stride = native.image_buf().plane_bytes_per_row(0);
let dest_stride = ff_frame.stride(0);

let src_bytes = bytes_lock.plane_data(0);
let dest_bytes = &mut ff_frame.data_mut(0);

for y in 0..height {
let row_width = width * 4;
let src_row = &src_bytes[y * src_stride..y * src_stride + row_width];
let dest_row = &mut dest_bytes[y * dest_stride..y * dest_stride + row_width];

dest_row.copy_from_slice(src_row);
}

ff_frame
}
"24BG" => {
let mut ff_frame = ffmpeg::frame::Video::new(
ffmpeg::format::Pixel::BGR24,
Expand All @@ -185,6 +241,29 @@ impl CapturedFrameExt for CapturedFrame {

ff_frame
}
"24RG" => {
let mut ff_frame = ffmpeg::frame::Video::new(
ffmpeg::format::Pixel::RGB24,
width as u32,
height as u32,
);

let src_stride = native.image_buf().plane_bytes_per_row(0);
let dest_stride = ff_frame.stride(0);

let src_bytes = bytes_lock.plane_data(0);
let dest_bytes = &mut ff_frame.data_mut(0);

for y in 0..height {
let row_width = width * 3;
let src_row = &src_bytes[y * src_stride..y * src_stride + row_width];
let dest_row = &mut dest_bytes[y * dest_stride..y * dest_stride + row_width];

dest_row.copy_from_slice(src_row);
}

ff_frame
}
"y420" => {
let plane_count = native.image_buf().plane_count();
if plane_count < 3 {
Expand Down Expand Up @@ -220,8 +299,82 @@ impl CapturedFrameExt for CapturedFrame {

ff_frame
}
"L008" | "GRAY" => {
let mut ff_frame = ffmpeg::frame::Video::new(
ffmpeg::format::Pixel::GRAY8,
width as u32,
height as u32,
);

let src_stride = native.image_buf().plane_bytes_per_row(0);
let dest_stride = ff_frame.stride(0);

let src_bytes = bytes_lock.plane_data(0);
let dest_bytes = &mut ff_frame.data_mut(0);

for y in 0..height {
let row_width = width;
let src_row = &src_bytes[y * src_stride..y * src_stride + row_width];
let dest_row = &mut dest_bytes[y * dest_stride..y * dest_stride + row_width];

dest_row.copy_from_slice(src_row);
}

ff_frame
}
format => {
return Err(AsFFmpegError::UnsupportedSubType(format.to_string()));
if let Some(info) = fourcc_to_pixel_format(format) {
if !FALLBACK_WARNING_LOGGED.swap(true, Ordering::Relaxed) {
tracing::warn!(
"Using swscale fallback for camera format '{}' - this may impact performance",
format
);
}

let mut src_frame =
ffmpeg::frame::Video::new(info.pixel, width as u32, height as u32);

let src_stride = native.image_buf().plane_bytes_per_row(0);
let dest_stride = src_frame.stride(0);
let src_bytes = bytes_lock.plane_data(0);
let dest_bytes = &mut src_frame.data_mut(0);

let row_width = width * info.bytes_per_pixel;
for y in 0..height {
let src_row = &src_bytes[y * src_stride..y * src_stride + row_width];
let dest_row =
&mut dest_bytes[y * dest_stride..y * dest_stride + row_width];
dest_row.copy_from_slice(src_row);
}

let mut scaler = scaling::Context::get(
info.pixel,
width as u32,
height as u32,
Pixel::RGBA,
width as u32,
height as u32,
scaling::flag::Flags::FAST_BILINEAR,
)
.map_err(|e| AsFFmpegError::SwscaleFallbackFailed {
format: format.to_string(),
reason: format!("Failed to create scaler: {e}"),
})?;

let mut output_frame =
ffmpeg::frame::Video::new(Pixel::RGBA, width as u32, height as u32);

scaler.run(&src_frame, &mut output_frame).map_err(|e| {
AsFFmpegError::SwscaleFallbackFailed {
format: format.to_string(),
reason: format!("Conversion failed: {e}"),
}
})?;

output_frame
} else {
return Err(AsFFmpegError::UnsupportedSubType(format.to_string()));
}
}
};

Expand Down
Loading
Loading