ff_encode/video/encoder.rs
1//! Video encoder public API.
2//!
3//! This module provides the public interface for video encoding operations.
4
5use crate::{EncodeError, EncoderBuilder};
6use ff_format::{AudioFrame, VideoFrame};
7use std::time::Instant;
8
9use super::encoder_inner::{VideoEncoderConfig, VideoEncoderInner, preset_to_string};
10
11/// Video encoder.
12///
13/// Encodes video frames to a file using FFmpeg.
14///
15/// # Examples
16///
17/// ```no_run
18/// use ff_encode::{VideoEncoder, VideoCodec};
19/// use ff_format::VideoFrame;
20///
21/// let mut encoder = VideoEncoder::create("output.mp4")
22/// .expect("Failed to create encoder")
23/// .video(1920, 1080, 30.0)
24/// .video_codec(VideoCodec::H264)
25/// .build()
26/// .expect("Failed to build encoder");
27///
28/// // Push frames
29/// let frames = vec![]; // Your video frames here
30/// for frame in frames {
31/// encoder.push_video(&frame).expect("Failed to push frame");
32/// }
33///
34/// encoder.finish().expect("Failed to finish encoding");
35/// ```
36pub struct VideoEncoder {
37 inner: Option<VideoEncoderInner>,
38 _config: VideoEncoderConfig,
39 start_time: Instant,
40 progress_callback: Option<Box<dyn crate::ProgressCallback>>,
41}
42
43impl VideoEncoder {
44 /// Create a new encoder builder with the given output path.
45 ///
46 /// # Arguments
47 ///
48 /// * `path` - Output file path
49 ///
50 /// # Returns
51 ///
52 /// Returns an [`EncoderBuilder`] for configuring the encoder.
53 ///
54 /// # Examples
55 ///
56 /// ```ignore
57 /// use ff_encode::VideoEncoder;
58 ///
59 /// let builder = VideoEncoder::create("output.mp4")?;
60 /// ```
61 pub fn create<P: AsRef<std::path::Path>>(path: P) -> Result<EncoderBuilder, EncodeError> {
62 EncoderBuilder::new(path.as_ref().to_path_buf())
63 }
64
65 /// Create an encoder from a builder.
66 ///
67 /// This is called by [`EncoderBuilder::build()`] and should not be called directly.
68 pub(crate) fn from_builder(builder: EncoderBuilder) -> Result<Self, EncodeError> {
69 let config = VideoEncoderConfig {
70 path: builder.path.clone(),
71 video_width: builder.video_width,
72 video_height: builder.video_height,
73 video_fps: builder.video_fps,
74 video_codec: builder.video_codec,
75 video_bitrate: builder.video_bitrate,
76 video_quality: builder.video_quality,
77 preset: preset_to_string(&builder.preset),
78 hardware_encoder: builder.hardware_encoder,
79 audio_sample_rate: builder.audio_sample_rate,
80 audio_channels: builder.audio_channels,
81 audio_codec: builder.audio_codec,
82 audio_bitrate: builder.audio_bitrate,
83 _progress_callback: builder.progress_callback.is_some(),
84 };
85
86 let inner = if config.video_width.is_some() {
87 Some(VideoEncoderInner::new(&config)?)
88 } else {
89 None
90 };
91
92 Ok(Self {
93 inner,
94 _config: config,
95 start_time: Instant::now(),
96 progress_callback: builder.progress_callback,
97 })
98 }
99
100 /// Get the actual video codec being used.
101 ///
102 /// Returns the name of the FFmpeg encoder (e.g., "h264_nvenc", "libx264").
103 #[must_use]
104 pub fn actual_video_codec(&self) -> &str {
105 self.inner
106 .as_ref()
107 .map_or("", |inner| inner.actual_video_codec.as_str())
108 }
109
110 /// Get the actual audio codec being used.
111 ///
112 /// Returns the name of the FFmpeg encoder (e.g., "aac", "libopus").
113 #[must_use]
114 pub fn actual_audio_codec(&self) -> &str {
115 self.inner
116 .as_ref()
117 .map_or("", |inner| inner.actual_audio_codec.as_str())
118 }
119
120 /// Get the hardware encoder actually being used.
121 ///
122 /// Returns the hardware encoder type that is actually being used for encoding.
123 /// This may differ from what was requested if the requested encoder is not available.
124 ///
125 /// # Examples
126 ///
127 /// ```ignore
128 /// use ff_encode::{VideoEncoder, VideoCodec, HardwareEncoder};
129 ///
130 /// let encoder = VideoEncoder::create("output.mp4")?
131 /// .video(1920, 1080, 30.0)
132 /// .video_codec(VideoCodec::H264)
133 /// .hardware_encoder(HardwareEncoder::Auto)
134 /// .build()?;
135 ///
136 /// println!("Using hardware encoder: {:?}", encoder.hardware_encoder());
137 /// ```
138 #[must_use]
139 pub fn hardware_encoder(&self) -> crate::HardwareEncoder {
140 let codec_name = self.actual_video_codec();
141
142 // Detect hardware encoder from codec name
143 if codec_name.contains("nvenc") {
144 crate::HardwareEncoder::Nvenc
145 } else if codec_name.contains("qsv") {
146 crate::HardwareEncoder::Qsv
147 } else if codec_name.contains("amf") {
148 crate::HardwareEncoder::Amf
149 } else if codec_name.contains("videotoolbox") {
150 crate::HardwareEncoder::VideoToolbox
151 } else if codec_name.contains("vaapi") {
152 crate::HardwareEncoder::Vaapi
153 } else {
154 crate::HardwareEncoder::None
155 }
156 }
157
158 /// Check if hardware encoding is being used.
159 ///
160 /// Returns `true` if the encoder is using hardware acceleration,
161 /// `false` if using software encoding.
162 ///
163 /// # Examples
164 ///
165 /// ```ignore
166 /// use ff_encode::{VideoEncoder, VideoCodec, HardwareEncoder};
167 ///
168 /// let encoder = VideoEncoder::create("output.mp4")?
169 /// .video(1920, 1080, 30.0)
170 /// .video_codec(VideoCodec::H264)
171 /// .hardware_encoder(HardwareEncoder::Auto)
172 /// .build()?;
173 ///
174 /// if encoder.is_hardware_encoding() {
175 /// println!("Using hardware encoder: {}", encoder.actual_video_codec());
176 /// } else {
177 /// println!("Using software encoder: {}", encoder.actual_video_codec());
178 /// }
179 /// ```
180 #[must_use]
181 pub fn is_hardware_encoding(&self) -> bool {
182 !matches!(self.hardware_encoder(), crate::HardwareEncoder::None)
183 }
184
185 /// Check if the actually selected video encoder is LGPL-compliant.
186 ///
187 /// Returns `true` if the encoder is safe for commercial use without licensing fees.
188 /// Returns `false` for GPL encoders that require licensing.
189 ///
190 /// # LGPL-Compatible Encoders (Commercial Use OK)
191 ///
192 /// - **Hardware encoders**: h264_nvenc, h264_qsv, h264_amf, h264_videotoolbox, h264_vaapi
193 /// - **Royalty-free codecs**: libvpx-vp9, libaom-av1, libsvtav1
194 /// - **Professional codecs**: prores_ks, dnxhd
195 ///
196 /// # GPL Encoders (Licensing Required)
197 ///
198 /// - **libx264**: Requires MPEG LA H.264 license for commercial distribution
199 /// - **libx265**: Requires MPEG LA H.265 license for commercial distribution
200 ///
201 /// # Examples
202 ///
203 /// ```ignore
204 /// use ff_encode::{VideoEncoder, VideoCodec, HardwareEncoder};
205 ///
206 /// // Default: Will use hardware encoder or VP9 fallback (LGPL-compliant)
207 /// let encoder = VideoEncoder::create("output.mp4")?
208 /// .video(1920, 1080, 30.0)
209 /// .video_codec(VideoCodec::H264)
210 /// .build()?;
211 ///
212 /// if encoder.is_lgpl_compliant() {
213 /// println!("✓ Safe for commercial use: {}", encoder.actual_video_codec());
214 /// } else {
215 /// println!("⚠ GPL encoder (requires licensing): {}", encoder.actual_video_codec());
216 /// }
217 /// ```
218 ///
219 /// # Note
220 ///
221 /// By default (without `gpl` feature), this will always return `true` because
222 /// the encoder automatically selects LGPL-compatible alternatives.
223 #[must_use]
224 pub fn is_lgpl_compliant(&self) -> bool {
225 let codec_name = self.actual_video_codec();
226
227 // Hardware encoders are LGPL-compatible
228 if codec_name.contains("nvenc")
229 || codec_name.contains("qsv")
230 || codec_name.contains("amf")
231 || codec_name.contains("videotoolbox")
232 || codec_name.contains("vaapi")
233 {
234 return true;
235 }
236
237 // LGPL-compatible software encoders
238 if codec_name.contains("vp9")
239 || codec_name.contains("av1")
240 || codec_name.contains("aom")
241 || codec_name.contains("svt")
242 || codec_name.contains("prores")
243 || codec_name == "mpeg4"
244 || codec_name == "dnxhd"
245 {
246 return true;
247 }
248
249 // GPL encoders
250 if codec_name == "libx264" || codec_name == "libx265" {
251 return false;
252 }
253
254 // Default to true for unknown encoders (conservative approach)
255 true
256 }
257
258 /// Push a video frame for encoding.
259 ///
260 /// # Arguments
261 ///
262 /// * `frame` - The video frame to encode
263 ///
264 /// # Errors
265 ///
266 /// Returns an error if encoding fails or the encoder is not initialized.
267 /// Returns `EncodeError::Cancelled` if the progress callback requested cancellation.
268 pub fn push_video(&mut self, frame: &VideoFrame) -> Result<(), EncodeError> {
269 // Check for cancellation before encoding
270 if let Some(ref callback) = self.progress_callback
271 && callback.should_cancel()
272 {
273 return Err(EncodeError::Cancelled);
274 }
275
276 let inner = self
277 .inner
278 .as_mut()
279 .ok_or_else(|| EncodeError::InvalidConfig {
280 reason: "Video encoder not initialized".to_string(),
281 })?;
282
283 // SAFETY: inner is properly initialized and we have exclusive access
284 unsafe { inner.push_video_frame(frame)? };
285
286 // Report progress after encoding
287 let progress = self.create_progress_info();
288 if let Some(ref mut callback) = self.progress_callback {
289 callback.on_progress(&progress);
290 }
291
292 Ok(())
293 }
294
295 /// Push an audio frame for encoding.
296 ///
297 /// # Arguments
298 ///
299 /// * `frame` - The audio frame to encode
300 ///
301 /// # Errors
302 ///
303 /// Returns an error if encoding fails or the encoder is not initialized.
304 /// Returns `EncodeError::Cancelled` if the progress callback requested cancellation.
305 ///
306 /// # Examples
307 ///
308 /// ```ignore
309 /// use ff_encode::{VideoEncoder, AudioCodec};
310 /// use ff_format::AudioFrame;
311 ///
312 /// let mut encoder = VideoEncoder::create("output.mp4")?
313 /// .video(1920, 1080, 30.0)
314 /// .audio(48000, 2)
315 /// .audio_codec(AudioCodec::Aac)
316 /// .build()?;
317 ///
318 /// // Push audio frames
319 /// let frame = AudioFrame::empty(1024, 2, 48000, ff_format::SampleFormat::F32)?;
320 /// encoder.push_audio(&frame)?;
321 /// ```
322 pub fn push_audio(&mut self, frame: &AudioFrame) -> Result<(), EncodeError> {
323 // Check for cancellation before encoding
324 if let Some(ref callback) = self.progress_callback
325 && callback.should_cancel()
326 {
327 return Err(EncodeError::Cancelled);
328 }
329
330 let inner = self
331 .inner
332 .as_mut()
333 .ok_or_else(|| EncodeError::InvalidConfig {
334 reason: "Audio encoder not initialized".to_string(),
335 })?;
336
337 // SAFETY: inner is properly initialized and we have exclusive access
338 unsafe { inner.push_audio_frame(frame)? };
339
340 // Report progress after encoding
341 let progress = self.create_progress_info();
342 if let Some(ref mut callback) = self.progress_callback {
343 callback.on_progress(&progress);
344 }
345
346 Ok(())
347 }
348
349 /// Finish encoding and write the file trailer.
350 ///
351 /// This method must be called to properly finalize the output file.
352 /// It flushes any remaining encoded frames and writes the file trailer.
353 ///
354 /// # Errors
355 ///
356 /// Returns an error if finalizing fails.
357 pub fn finish(mut self) -> Result<(), EncodeError> {
358 if let Some(mut inner) = self.inner.take() {
359 // SAFETY: inner is properly initialized and we have exclusive access
360 unsafe { inner.finish()? };
361 }
362 Ok(())
363 }
364
365 /// Create progress information from current encoder state.
366 fn create_progress_info(&self) -> crate::Progress {
367 let elapsed = self.start_time.elapsed();
368
369 let (frames_encoded, bytes_written) = self
370 .inner
371 .as_ref()
372 .map_or((0, 0), |inner| (inner.frame_count, inner.bytes_written));
373
374 // Calculate current FPS
375 #[allow(clippy::cast_precision_loss)]
376 let current_fps = if !elapsed.is_zero() {
377 frames_encoded as f64 / elapsed.as_secs_f64()
378 } else {
379 0.0
380 };
381
382 // Calculate current bitrate
383 #[allow(clippy::cast_precision_loss)]
384 let current_bitrate = if !elapsed.is_zero() {
385 let elapsed_secs = elapsed.as_secs();
386 if elapsed_secs > 0 {
387 (bytes_written * 8) / elapsed_secs
388 } else {
389 // Less than 1 second elapsed, use fractional seconds
390 ((bytes_written * 8) as f64 / elapsed.as_secs_f64()) as u64
391 }
392 } else {
393 0
394 };
395
396 // We don't know total frames without user input, so this is None for now
397 let total_frames = None;
398 let remaining = None;
399
400 crate::Progress {
401 frames_encoded,
402 total_frames,
403 bytes_written,
404 current_bitrate,
405 elapsed,
406 remaining,
407 current_fps,
408 }
409 }
410}
411
412impl Drop for VideoEncoder {
413 fn drop(&mut self) {
414 // VideoEncoderInner will handle cleanup in its Drop implementation
415 }
416}
417
418#[cfg(test)]
419mod tests {
420 use super::super::encoder_inner::{VideoEncoderConfig, VideoEncoderInner};
421 use super::*;
422 use crate::HardwareEncoder;
423
424 /// Helper function to create a mock encoder for testing.
425 fn create_mock_encoder(video_codec_name: &str, audio_codec_name: &str) -> VideoEncoder {
426 VideoEncoder {
427 inner: Some(VideoEncoderInner {
428 format_ctx: std::ptr::null_mut(),
429 video_codec_ctx: None,
430 audio_codec_ctx: None,
431 video_stream_index: -1,
432 audio_stream_index: -1,
433 sws_ctx: None,
434 swr_ctx: None,
435 frame_count: 0,
436 audio_sample_count: 0,
437 bytes_written: 0,
438 actual_video_codec: video_codec_name.to_string(),
439 actual_audio_codec: audio_codec_name.to_string(),
440 last_src_width: None,
441 last_src_height: None,
442 last_src_format: None,
443 }),
444 _config: VideoEncoderConfig {
445 path: "test.mp4".into(),
446 video_width: Some(1920),
447 video_height: Some(1080),
448 video_fps: Some(30.0),
449 video_codec: crate::VideoCodec::H264,
450 video_bitrate: None,
451 video_quality: None,
452 preset: "medium".to_string(),
453 hardware_encoder: HardwareEncoder::Auto,
454 audio_sample_rate: None,
455 audio_channels: None,
456 audio_codec: crate::AudioCodec::Aac,
457 audio_bitrate: None,
458 _progress_callback: false,
459 },
460 start_time: std::time::Instant::now(),
461 progress_callback: None,
462 }
463 }
464
465 #[test]
466 fn test_create_encoder_builder() {
467 let builder = VideoEncoder::create("output.mp4");
468 assert!(builder.is_ok());
469 }
470
471 #[test]
472 fn test_is_lgpl_compliant_hardware_encoders() {
473 // Test hardware encoder names
474 let test_cases = vec![
475 ("h264_nvenc", true),
476 ("h264_qsv", true),
477 ("h264_amf", true),
478 ("h264_videotoolbox", true),
479 ("hevc_nvenc", true),
480 ("hevc_qsv", true),
481 ("hevc_vaapi", true),
482 ];
483
484 for (codec_name, expected) in test_cases {
485 let encoder = create_mock_encoder(codec_name, "");
486
487 assert_eq!(
488 encoder.is_lgpl_compliant(),
489 expected,
490 "Failed for codec: {}",
491 codec_name
492 );
493 }
494 }
495
496 #[test]
497 fn test_is_lgpl_compliant_software_encoders() {
498 // Test software encoder names
499 let test_cases = vec![
500 ("libx264", false),
501 ("libx265", false),
502 ("libvpx-vp9", true),
503 ("libaom-av1", true),
504 ("libsvtav1", true),
505 ("prores_ks", true),
506 ("mpeg4", true),
507 ("dnxhd", true),
508 ];
509
510 for (codec_name, expected) in test_cases {
511 let encoder = create_mock_encoder(codec_name, "");
512
513 assert_eq!(
514 encoder.is_lgpl_compliant(),
515 expected,
516 "Failed for codec: {}",
517 codec_name
518 );
519 }
520 }
521
522 #[test]
523 fn test_hardware_encoder_detection() {
524 // Test hardware encoder detection from codec name
525 let test_cases = vec![
526 ("h264_nvenc", HardwareEncoder::Nvenc, true),
527 ("hevc_nvenc", HardwareEncoder::Nvenc, true),
528 ("h264_qsv", HardwareEncoder::Qsv, true),
529 ("hevc_qsv", HardwareEncoder::Qsv, true),
530 ("h264_amf", HardwareEncoder::Amf, true),
531 ("h264_videotoolbox", HardwareEncoder::VideoToolbox, true),
532 ("hevc_videotoolbox", HardwareEncoder::VideoToolbox, true),
533 ("h264_vaapi", HardwareEncoder::Vaapi, true),
534 ("hevc_vaapi", HardwareEncoder::Vaapi, true),
535 ("libx264", HardwareEncoder::None, false),
536 ("libx265", HardwareEncoder::None, false),
537 ("libvpx-vp9", HardwareEncoder::None, false),
538 ];
539
540 for (codec_name, expected_hw, expected_is_hw) in test_cases {
541 let encoder = create_mock_encoder(codec_name, "");
542
543 assert_eq!(
544 encoder.hardware_encoder(),
545 expected_hw,
546 "Failed for codec: {}",
547 codec_name
548 );
549 assert_eq!(
550 encoder.is_hardware_encoding(),
551 expected_is_hw,
552 "is_hardware_encoding failed for codec: {}",
553 codec_name
554 );
555 }
556 }
557}