Skip to main content

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}