webp_screenshot_rust/
lib.rs1#![allow(missing_docs)]
29#![cfg_attr(docsrs, feature(doc_cfg))]
30
31pub mod capture;
32pub mod encoder;
33pub mod error;
34pub mod memory_pool;
35pub mod pipeline;
36pub mod types;
37
38#[cfg(feature = "c-api")]
39pub mod ffi;
40
41pub use capture::{Capturer, ScreenCapture};
43pub use encoder::{WebPEncoder, EncoderOptions};
44pub use error::{CaptureError, CaptureResult, EncodingError, EncodingResult};
45pub use memory_pool::{MemoryPool, PooledBuffer};
46pub use pipeline::{StreamingPipeline, StreamingPipelineBuilder, ZeroCopyOptimizer};
47pub use types::{
48 CaptureConfig, CaptureMetadata, CaptureRegion, DisplayInfo, PerformanceStats, PixelFormat,
49 RawImage, Rectangle, Screenshot, WebPConfig,
50};
51
52use std::sync::Arc;
53use std::time::{Duration, Instant, SystemTime};
54
55pub struct WebPScreenshot {
57 capturer: Box<dyn ScreenCapture>,
58 encoder: WebPEncoder,
59 memory_pool: Arc<MemoryPool>,
60 config: CaptureConfig,
61 stats: PerformanceStats,
62 zero_copy: Option<ZeroCopyOptimizer>,
63 gpu_encoder: Option<encoder::gpu::GpuWebPEncoder>,
64}
65
66impl WebPScreenshot {
67 pub fn new() -> CaptureResult<Self> {
69 Self::with_config(CaptureConfig::default())
70 }
71
72 pub fn with_config(config: CaptureConfig) -> CaptureResult<Self> {
74 let zero_copy = if ZeroCopyOptimizer::is_supported() {
75 Some(ZeroCopyOptimizer::new())
76 } else {
77 None
78 };
79
80 let gpu_encoder = if cfg!(feature = "gpu") {
81 Some(encoder::gpu::GpuWebPEncoder::new())
82 } else {
83 None
84 };
85
86 Ok(Self {
87 capturer: Capturer::new()?,
88 encoder: WebPEncoder::new(),
89 memory_pool: memory_pool::global_pool(),
90 config,
91 stats: PerformanceStats::default(),
92 zero_copy,
93 gpu_encoder,
94 })
95 }
96
97 pub fn get_displays(&self) -> CaptureResult<Vec<DisplayInfo>> {
99 self.capturer.get_displays()
100 }
101
102 pub fn capture_display(&mut self, display_index: usize) -> CaptureResult<Screenshot> {
104 eprintln!("[LIB] ========================================");
105 eprintln!("[LIB] capture_display called with display_index: {}", display_index);
106 eprintln!("[LIB] Config region: {:?}", self.config.region);
107 eprintln!("[LIB] ========================================");
108
109 let start_time = Instant::now();
110 let timestamp = SystemTime::now();
111
112 let mut last_error = None;
114 for attempt in 0..=self.config.max_retries {
115 if attempt > 0 {
116 std::thread::sleep(self.config.retry_delay);
117 log::debug!("Retry attempt {} for display {}", attempt, display_index);
118 }
119
120 match self.capture_display_internal(display_index, timestamp, start_time) {
121 Ok(screenshot) => {
122 self.stats.successful_captures += 1;
123 self.stats.total_captures += 1;
124 return Ok(screenshot);
125 }
126 Err(e) if e.is_recoverable() && attempt < self.config.max_retries => {
127 last_error = Some(e);
128 continue;
129 }
130 Err(e) => {
131 self.stats.failed_captures += 1;
132 self.stats.total_captures += 1;
133 return Err(e);
134 }
135 }
136 }
137
138 self.stats.failed_captures += 1;
139 self.stats.total_captures += 1;
140 Err(last_error.unwrap_or_else(|| {
141 CaptureError::CaptureFailed("Max retries exceeded".to_string())
142 }))
143 }
144
145 fn capture_display_internal(
147 &mut self,
148 display_index: usize,
149 timestamp: SystemTime,
150 start_time: Instant,
151 ) -> CaptureResult<Screenshot> {
152 let capture_start = Instant::now();
154
155 let raw_image = if let Some(ref zero_copy) = self.zero_copy {
156 if zero_copy.is_enabled() && self.config.region.is_none() {
159 eprintln!("[LIB] Using ZERO-COPY optimization path (full screen)");
160 zero_copy.capture_zero_copy(&*self.capturer, display_index)?
161 } else {
162 if self.config.region.is_some() {
163 eprintln!("[LIB] Region set - disabling zero-copy, using normal GDI path");
164 } else {
165 eprintln!("[LIB] Zero-copy available but disabled, using normal path");
166 }
167 self.capture_normal(display_index)?
168 }
169 } else {
170 eprintln!("[LIB] No zero-copy, using normal capture path");
171 self.capture_normal(display_index)?
172 };
173
174 let capture_duration = capture_start.elapsed();
175
176 self.stats.total_bytes_captured += raw_image.size() as u64;
178 self.stats.total_capture_time += capture_duration;
179
180 let encoding_start = Instant::now();
182
183 let webp_data = if let Some(ref gpu_encoder) = self.gpu_encoder {
184 if gpu_encoder.is_available() && gpu_encoder.is_size_suitable(raw_image.width, raw_image.height) {
185 gpu_encoder.encode(&raw_image, &self.config.webp_config)?
186 } else {
187 self.encoder.encode(&raw_image, &self.config.webp_config)
188 .map_err(|e| CaptureError::Other(e.into()))?
189 }
190 } else {
191 self.encoder.encode(&raw_image, &self.config.webp_config)
192 .map_err(|e| CaptureError::Other(e.into()))?
193 };
194
195 let encoding_duration = encoding_start.elapsed();
196
197 self.stats.total_bytes_encoded += webp_data.len() as u64;
199 self.stats.total_encoding_time += encoding_duration;
200
201 let total_duration = start_time.elapsed();
203 if self.stats.fastest_capture == Duration::ZERO
204 || total_duration < self.stats.fastest_capture
205 {
206 self.stats.fastest_capture = total_duration;
207 }
208 if total_duration > self.stats.slowest_capture {
209 self.stats.slowest_capture = total_duration;
210 }
211
212 let metadata = CaptureMetadata {
214 timestamp,
215 capture_duration,
216 encoding_duration,
217 original_size: raw_image.size(),
218 compressed_size: webp_data.len(),
219 implementation: self.capturer.implementation_name(),
220 };
221
222 Ok(Screenshot {
223 data: webp_data,
224 width: raw_image.width,
225 height: raw_image.height,
226 display_index,
227 metadata,
228 })
229 }
230
231 fn capture_normal(&self, display_index: usize) -> CaptureResult<RawImage> {
233 if let Some(region) = self.config.region {
234 eprintln!("[LIB] capture_normal: Using region mode - {:?}", region);
235 eprintln!("[LIB] Calling capturer.capture_region()");
236 self.capturer.capture_region(region)
237 } else {
238 eprintln!("[LIB] capture_normal: Using display mode - display {}", display_index);
239 eprintln!("[LIB] Calling capturer.capture_display()");
240 self.capturer.capture_display(display_index)
241 }
242 }
243
244 pub fn capture_all_displays(&mut self) -> Vec<CaptureResult<Screenshot>> {
246 match self.get_displays() {
247 Ok(displays) => displays
248 .iter()
249 .enumerate()
250 .map(|(index, _)| self.capture_display(index))
251 .collect(),
252 Err(e) => vec![Err(e)],
253 }
254 }
255
256 pub fn capture_with_config(
258 &mut self,
259 display_index: usize,
260 webp_config: WebPConfig,
261 ) -> CaptureResult<Screenshot> {
262 let original_config = self.config.webp_config.clone();
263 self.config.webp_config = webp_config;
264 let result = self.capture_display(display_index);
265 self.config.webp_config = original_config;
266 result
267 }
268
269 pub fn create_streaming_pipeline(&self) -> StreamingPipelineBuilder {
271 StreamingPipelineBuilder::new()
272 }
273
274 pub fn set_config(&mut self, config: CaptureConfig) {
276 self.config = config;
277 }
278
279 pub fn config(&self) -> &CaptureConfig {
281 &self.config
282 }
283
284 pub fn stats(&self) -> &PerformanceStats {
286 &self.stats
287 }
288
289 pub fn reset_stats(&mut self) {
291 self.stats = PerformanceStats::default();
292 }
293
294 pub fn memory_stats(&self) -> memory_pool::PoolStats {
296 self.memory_pool.stats()
297 }
298
299 pub fn zero_copy_stats(&self) -> Option<pipeline::zero_copy::ZeroCopyStats> {
301 self.zero_copy.as_ref().map(|zc| zc.stats())
302 }
303
304 pub fn implementation_name(&self) -> String {
306 self.capturer.implementation_name()
307 }
308
309 pub fn is_hardware_accelerated(&self) -> bool {
311 self.capturer.is_hardware_accelerated()
312 }
313
314 pub fn gpu_info(&self) -> Option<String> {
316 self.gpu_encoder.as_ref().map(|gpu| {
317 format!(
318 "{} - {}",
319 gpu.backend_name(),
320 gpu.device_info().unwrap_or_else(|| "Unknown".to_string())
321 )
322 })
323 }
324}
325
326impl Default for WebPScreenshot {
327 fn default() -> Self {
328 Self::new().expect("Failed to initialize WebPScreenshot")
329 }
330}
331
332pub fn capture_primary_display() -> CaptureResult<Screenshot> {
334 let mut screenshot = WebPScreenshot::new()?;
335 screenshot.capture_display(0)
336}
337
338pub fn capture_with_quality(display_index: usize, quality: u8) -> CaptureResult<Screenshot> {
340 let config = CaptureConfig {
341 webp_config: WebPConfig {
342 quality,
343 ..Default::default()
344 },
345 ..Default::default()
346 };
347
348 let mut screenshot = WebPScreenshot::with_config(config)?;
349 screenshot.capture_display(display_index)
350}
351
352pub fn get_displays() -> CaptureResult<Vec<DisplayInfo>> {
354 let capturer = Capturer::new()?;
355 capturer.get_displays()
356}
357
358pub fn version() -> &'static str {
360 env!("CARGO_PKG_VERSION")
361}
362
363pub fn capabilities() -> String {
365 let mut caps = Vec::new();
366
367 #[cfg(target_os = "windows")]
369 caps.push("Windows");
370 #[cfg(target_os = "macos")]
371 caps.push("macOS");
372 #[cfg(target_os = "linux")]
373 caps.push("Linux");
374
375 let simd_caps = encoder::simd::global_simd_converter().capabilities();
377 if !simd_caps.is_empty() && simd_caps != "None (scalar)" {
378 caps.push(&simd_caps);
379 }
380
381 if ZeroCopyOptimizer::is_supported() {
383 caps.push("Zero-Copy");
384 }
385
386 #[cfg(feature = "gpu")]
387 caps.push("GPU");
388
389 #[cfg(feature = "parallel")]
390 caps.push("Parallel");
391
392 caps.join(", ")
393}
394
395#[cfg(test)]
396mod tests {
397 use super::*;
398
399 #[test]
400 fn test_version() {
401 let version = version();
402 assert!(!version.is_empty());
403 }
404
405 #[test]
406 fn test_capabilities() {
407 let caps = capabilities();
408 println!("Library capabilities: {}", caps);
409 assert!(!caps.is_empty());
410 }
411
412 #[test]
413 fn test_screenshot_creation() {
414 let result = WebPScreenshot::new();
416 drop(result);
418 }
419
420 #[test]
421 fn test_config_creation() {
422 let config = CaptureConfig::default();
423 assert_eq!(config.webp_config.quality, 80);
424 assert!(!config.include_cursor);
425 }
426}