use screencapturekit::cm::CMSampleBufferExt;
use screencapturekit::metal::{
IOSurfaceMetalExt, MTLPixelFormat, MetalDevice, MetalRenderPipelineDescriptor,
MetalRenderPipelineState, Uniforms, SHADER_SOURCE,
};
use screencapturekit::prelude::*;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::Arc;
struct Renderer {
device: MetalDevice,
pipeline_textured: MetalRenderPipelineState,
pipeline_ycbcr: MetalRenderPipelineState,
}
impl Renderer {
fn new() -> Result<Self, String> {
let device = MetalDevice::system_default().ok_or("No Metal device")?;
let library = device
.create_library_with_source(SHADER_SOURCE)
.map_err(|e| format!("Shader compile error: {e}"))?;
let pipeline_textured = {
let vert = library
.get_function("vertex_fullscreen")
.ok_or("vertex_fullscreen not found")?;
let frag = library
.get_function("fragment_textured")
.ok_or("fragment_textured not found")?;
let desc = MetalRenderPipelineDescriptor::new();
desc.set_vertex_function(&vert);
desc.set_fragment_function(&frag);
desc.set_color_attachment_pixel_format(0, MTLPixelFormat::BGRA8Unorm)
.map_err(|error| error.to_string())?;
device
.create_render_pipeline_state(&desc)
.ok_or("Failed to create textured pipeline")?
};
let pipeline_ycbcr = {
let vert = library
.get_function("vertex_fullscreen")
.ok_or("vertex_fullscreen not found")?;
let frag = library
.get_function("fragment_ycbcr")
.ok_or("fragment_ycbcr not found")?;
let desc = MetalRenderPipelineDescriptor::new();
desc.set_vertex_function(&vert);
desc.set_fragment_function(&frag);
desc.set_color_attachment_pixel_format(0, MTLPixelFormat::BGRA8Unorm)
.map_err(|error| error.to_string())?;
device
.create_render_pipeline_state(&desc)
.ok_or("Failed to create ycbcr pipeline")?
};
Ok(Self {
device,
pipeline_textured,
pipeline_ycbcr,
})
}
}
struct Handler {
renderer: Arc<Renderer>,
count: Arc<AtomicUsize>,
}
impl SCStreamOutputTrait for Handler {
fn did_output_sample_buffer(&self, sample: CMSampleBuffer, output_type: SCStreamOutputType) {
if !matches!(output_type, SCStreamOutputType::Screen) {
return;
}
let n = self.count.fetch_add(1, Ordering::Relaxed);
if n % 60 != 0 {
return;
}
let Some(pixel_buffer) = sample.pixel_buffer() else {
return;
};
let Some(surface) = pixel_buffer.io_surface() else {
println!("⚠️ Frame {n} - Not IOSurface-backed");
return;
};
let Some(textures) = surface.create_metal_textures(&self.renderer.device) else {
println!("⚠️ Frame {n} - Failed to create textures");
return;
};
println!("\n📹 Frame {n}");
println!(" Dimensions: {}x{}", textures.width, textures.height);
let pipeline = if textures.is_ycbcr() {
println!(" Format: {} (YCbCr biplanar)", textures.pixel_format);
println!(
" Plane 0 (Y): {}x{}",
textures.plane0.width(),
textures.plane0.height()
);
if let Some(ref plane1) = textures.plane1 {
println!(" Plane 1 (CbCr): {}x{}", plane1.width(), plane1.height());
}
println!(" Pipeline: fragment_ycbcr");
&self.renderer.pipeline_ycbcr
} else {
println!(" Format: {} (single-plane)", textures.pixel_format);
println!(
" Texture: {}x{}",
textures.plane0.width(),
textures.plane0.height()
);
println!(" Pipeline: fragment_textured");
&self.renderer.pipeline_textured
};
let viewport_width = 1920.0;
let viewport_height = 1080.0;
let uniforms = Uniforms::from_captured_textures(viewport_width, viewport_height, &textures);
let uniform_bytes = uniforms.to_bytes();
if let Some(uniform_buffer) = self
.renderer
.device
.create_buffer_with_bytes(&uniform_bytes)
{
println!(
" Uniforms: viewport={:?}, texture={:?}",
uniforms.viewport_size, uniforms.texture_size
);
println!(" Uniform buffer: {} bytes", uniform_buffer.length());
}
println!(" ✅ Ready to render (pipeline={:p})", pipeline.as_ptr());
}
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
println!("🎨 Metal Texture Creation and Rendering\n");
let renderer = Arc::new(Renderer::new()?);
println!("Metal device: {}", renderer.device.name());
println!("Pipelines created:");
println!(" - fragment_textured (for BGRA, L10R)");
println!(" - fragment_ycbcr (for 420v, 420f)");
let content = SCShareableContent::get()?;
let display = content
.displays()
.into_iter()
.next()
.ok_or("No displays found")?;
let filter = SCContentFilter::create()
.with_display(&display)
.with_excluding_windows(&[])
.build();
let config = SCStreamConfiguration::new()
.with_width(1920)
.with_height(1080)
.with_pixel_format(PixelFormat::YCbCr_420v);
let handler = Handler {
renderer,
count: Arc::new(AtomicUsize::new(0)),
};
let mut stream = SCStream::new(&filter, &config);
stream.add_output_handler(handler, SCStreamOutputType::Screen);
println!("\nStarting capture (YCbCr 420v format)...\n");
stream.start_capture()?;
std::thread::sleep(std::time::Duration::from_secs(5));
stream.stop_capture()?;
println!("\n✅ Done");
Ok(())
}