Skip to main content

windows_capture/
encoder.rs

1use std::fs::{self, File};
2use std::path::Path;
3use std::sync::atomic::{self, AtomicBool};
4use std::sync::{Arc, mpsc};
5use std::thread::{self, JoinHandle};
6use std::time::Duration;
7
8use parking_lot::Mutex;
9use windows::Foundation::{TimeSpan, TypedEventHandler};
10use windows::Graphics::DirectX::Direct3D11::IDirect3DSurface;
11use windows::Graphics::Imaging::{BitmapAlphaMode, BitmapEncoder, BitmapPixelFormat};
12use windows::Media::Core::{
13    AudioStreamDescriptor, MediaStreamSample, MediaStreamSource, MediaStreamSourceSampleRequestedEventArgs,
14    MediaStreamSourceStartingEventArgs, VideoStreamDescriptor,
15};
16use windows::Media::MediaProperties::{
17    AudioEncodingProperties, ContainerEncodingProperties, MediaEncodingProfile, MediaEncodingSubtypes,
18    VideoEncodingProperties,
19};
20use windows::Media::Transcoding::MediaTranscoder;
21use windows::Security::Cryptography::CryptographicBuffer;
22use windows::Storage::Streams::{DataReader, IRandomAccessStream, InMemoryRandomAccessStream};
23use windows::Storage::{FileAccessMode, StorageFile};
24use windows::System::Threading::{ThreadPool, WorkItemHandler, WorkItemOptions, WorkItemPriority};
25use windows::Win32::Graphics::Direct3D11::{
26    D3D11_BIND_RENDER_TARGET, D3D11_BIND_SHADER_RESOURCE, D3D11_BOX, D3D11_TEXTURE2D_DESC, D3D11_USAGE_DEFAULT,
27    ID3D11Device, ID3D11RenderTargetView, ID3D11Texture2D,
28};
29use windows::Win32::Graphics::Dxgi::Common::{DXGI_FORMAT, DXGI_SAMPLE_DESC};
30use windows::Win32::Graphics::Dxgi::IDXGISurface;
31use windows::Win32::System::WinRT::Direct3D11::CreateDirect3D11SurfaceFromDXGISurface;
32use windows::core::{HSTRING, Interface};
33
34use crate::d3d11::SendDirectX;
35use crate::frame::Frame;
36use crate::settings::ColorFormat;
37
38type VideoFrameReceiver = Arc<Mutex<mpsc::Receiver<Option<(VideoEncoderSource, TimeSpan)>>>>;
39type AudioFrameReceiver = Arc<Mutex<mpsc::Receiver<Option<(AudioEncoderSource, TimeSpan)>>>>;
40
41#[derive(thiserror::Error, Debug)]
42/// Errors that can occur when encoding raw buffers to images via [`ImageEncoder`].
43pub enum ImageEncoderError {
44    /// The provided source pixel format is not supported for image encoding.
45    ///
46    /// This occurs for formats such as [`crate::settings::ColorFormat::Rgba16F`].
47    #[error("This color format is not supported for saving as an image")]
48    UnsupportedFormat,
49    /// An I/O error occurred while writing the image to disk.
50    ///
51    /// Wraps [`std::io::Error`].
52    #[error("I/O error: {0}")]
53    IoError(#[from] std::io::Error),
54    /// An integer conversion failed during buffer sizing or Windows API calls.
55    ///
56    /// Wraps [`std::num::TryFromIntError`].
57    #[error("Integer conversion error: {0}")]
58    IntConversionError(#[from] std::num::TryFromIntError),
59    /// A Windows Runtime/Win32 API call failed.
60    ///
61    /// Wraps [`windows::core::Error`].
62    #[error("Windows API error: {0}")]
63    WindowsError(#[from] windows::core::Error),
64}
65
66#[derive(Eq, PartialEq, Clone, Copy, Debug)]
67/// Supported output image formats for [`crate::encoder::ImageEncoder`].
68pub enum ImageFormat {
69    /// JPEG (lossy).
70    Jpeg,
71    /// PNG (lossless).
72    Png,
73    /// GIF (palette-based).
74    Gif,
75    /// TIFF (Tagged Image File Format).
76    Tiff,
77    /// BMP (Bitmap).
78    Bmp,
79    /// JPEG XR (HD Photo).
80    JpegXr,
81}
82
83/// Pixel formats supported by the Windows API for image encoding.
84#[derive(Eq, PartialEq, Clone, Copy, Debug)]
85pub enum ImageEncoderPixelFormat {
86    /// 16-bit floating-point RGBA format.
87    Rgb16F,
88    /// 8-bit unsigned integer BGRA format.
89    Bgra8,
90    /// 8-bit unsigned integer RGBA format.
91    Rgba8,
92}
93
94/// Encodes raw image buffers into encoded bytes for common formats.
95///
96/// Supports saving as PNG, JPEG, GIF, TIFF, BMP, and JPEG XR when the input
97/// color format is compatible.
98///
99/// # Example
100/// ```no_run
101/// use windows_capture::encoder::{ImageEncoder, ImageEncoderPixelFormat, ImageFormat};
102///
103/// let width = 320u32;
104/// let height = 240u32;
105/// // BGRA8 buffer (e.g., from a frame)
106/// let bgra = vec![0u8; (width * height * 4) as usize];
107///
108/// let png_bytes = ImageEncoder::new(ImageFormat::Png, ImageEncoderPixelFormat::Bgra8)
109///     .unwrap()
110///     .encode(&bgra, width, height)
111///     .unwrap();
112///
113/// std::fs::write("example.png", png_bytes).unwrap();
114/// ```
115pub struct ImageEncoder {
116    encoder: windows::core::GUID,
117    pixel_format: BitmapPixelFormat,
118}
119
120impl ImageEncoder {
121    /// Constructs a new [`ImageEncoder`].
122    #[inline]
123    pub fn new(format: ImageFormat, pixel_format: ImageEncoderPixelFormat) -> Result<Self, ImageEncoderError> {
124        let encoder = match format {
125            ImageFormat::Jpeg => BitmapEncoder::JpegEncoderId()?,
126            ImageFormat::Png => BitmapEncoder::PngEncoderId()?,
127            ImageFormat::Gif => BitmapEncoder::GifEncoderId()?,
128            ImageFormat::Tiff => BitmapEncoder::TiffEncoderId()?,
129            ImageFormat::Bmp => BitmapEncoder::BmpEncoderId()?,
130            ImageFormat::JpegXr => BitmapEncoder::JpegXREncoderId()?,
131        };
132
133        let pixel_format = match pixel_format {
134            ImageEncoderPixelFormat::Bgra8 => BitmapPixelFormat::Bgra8,
135            ImageEncoderPixelFormat::Rgba8 => BitmapPixelFormat::Rgba8,
136            ImageEncoderPixelFormat::Rgb16F => BitmapPixelFormat::Rgba16,
137        };
138
139        Ok(Self { pixel_format, encoder })
140    }
141
142    /// Encodes the provided pixel buffer into the configured output [`ImageFormat`].
143    ///
144    /// The input buffer must match the specified source [`crate::settings::ColorFormat`]
145    /// and dimensions. For packed 8-bit formats (e.g., [`crate::settings::ColorFormat::Bgra8`]),
146    /// the buffer length should be `width * height * 4`.
147    ///
148    /// # Errors
149    ///
150    /// - [`ImageEncoderError::UnsupportedFormat`] when the source format is unsupported for images
151    ///   (e.g., [`crate::settings::ColorFormat::Rgba16F`])
152    /// - [`ImageEncoderError::WindowsError`] when Windows Imaging API calls fail
153    /// - [`ImageEncoderError::IntConversionError`] on integer conversion failures
154    #[inline]
155    pub fn encode(&self, image_buffer: &[u8], width: u32, height: u32) -> Result<Vec<u8>, ImageEncoderError> {
156        let stream = InMemoryRandomAccessStream::new()?;
157
158        let encoder = BitmapEncoder::CreateAsync(self.encoder, &stream)?.join()?;
159
160        encoder.SetPixelData(
161            self.pixel_format,
162            BitmapAlphaMode::Premultiplied,
163            width,
164            height,
165            1.0,
166            1.0,
167            image_buffer,
168        )?;
169        encoder.FlushAsync()?.join()?;
170
171        let size = stream.Size()?;
172        let input = stream.GetInputStreamAt(0)?;
173        let reader = DataReader::CreateDataReader(&input)?;
174        reader.LoadAsync(size as u32)?.join()?;
175
176        let mut bytes = vec![0u8; size as usize];
177        reader.ReadBytes(&mut bytes)?;
178
179        Ok(bytes)
180    }
181}
182
183#[derive(thiserror::Error, Debug)]
184/// Errors emitted by [`VideoEncoder`] during configuration, streaming, or finalization.
185pub enum VideoEncoderError {
186    /// A Windows Runtime/Win32 API call failed.
187    ///
188    /// Wraps [`windows::core::Error`].
189    #[error("Windows API error: {0}")]
190    WindowsError(#[from] windows::core::Error),
191    /// Failed to send a video sample into the internal pipeline.
192    ///
193    /// Typically indicates the internal channel is closed.
194    #[error("Failed to send frame: {0}")]
195    FrameSendError(#[from] mpsc::SendError<Option<(VideoEncoderSource, TimeSpan)>>),
196    /// Failed to send an audio sample into the internal pipeline.
197    ///
198    /// Typically indicates the internal channel is closed.
199    #[error("Failed to send audio: {0}")]
200    AudioSendError(#[from] mpsc::SendError<Option<(AudioEncoderSource, TimeSpan)>>),
201    /// Video encoding was disabled via [`VideoSettingsBuilder::disabled`].
202    #[error("Video encoding is disabled")]
203    VideoDisabled,
204    /// Audio encoding was disabled via [`AudioSettingsBuilder::disabled`].
205    #[error("Audio encoding is disabled")]
206    AudioDisabled,
207    /// An I/O error occurred during file creation or writing.
208    ///
209    /// Wraps [`std::io::Error`].
210    #[error("I/O error: {0}")]
211    IoError(#[from] std::io::Error),
212    /// The provided frame color format is unsupported by the encoder path.
213    ///
214    /// See [`crate::settings::ColorFormat`].
215    #[error("Unsupported frame color format: {0:?}")]
216    UnsupportedFrameFormat(ColorFormat),
217}
218
219unsafe impl Send for VideoEncoderError {}
220unsafe impl Sync for VideoEncoderError {}
221
222/// Video sources used by [`VideoEncoder`].
223///
224/// - For [`VideoEncoderSource::DirectX`], the COM surface pointer is ref-counted; holding the
225///   pointer is sufficient.
226/// - For [`VideoEncoderSource::Buffer`], the encoder takes ownership of the bytes, allowing callers
227///   to return immediately.
228pub enum VideoEncoderSource {
229    /// A Direct3D surface sample.
230    DirectX(SendDirectX<IDirect3DSurface>),
231    /// A raw BGRA sample buffer.
232    Buffer(Vec<u8>),
233}
234
235/// Audio sources used by [`VideoEncoder`]. The encoder takes ownership of the bytes.
236pub enum AudioEncoderSource {
237    /// Interleaved PCM bytes.
238    Buffer(Vec<u8>),
239}
240
241struct CachedSurface {
242    width: u32,
243    height: u32,
244    format: ColorFormat,
245    texture: SendDirectX<ID3D11Texture2D>,
246    surface: SendDirectX<IDirect3DSurface>,
247    render_target_view: Option<SendDirectX<ID3D11RenderTargetView>>,
248}
249
250/// Builder for configuring video encoder settings.
251pub struct VideoSettingsBuilder {
252    sub_type: VideoSettingsSubType,
253    bitrate: u32,
254    width: u32,
255    height: u32,
256    frame_rate: u32,
257    pixel_aspect_ratio: (u32, u32),
258    disabled: bool,
259}
260
261impl VideoSettingsBuilder {
262    /// Constructs a new [`VideoSettingsBuilder`] with required geometry.
263    ///
264    /// Defaults:
265    /// - Subtype: [`VideoSettingsSubType::HEVC`]
266    /// - Bitrate: 15 Mbps
267    /// - Frame rate: 60 fps
268    /// - Pixel aspect ratio: 1:1
269    /// - Disabled: false
270    pub const fn new(width: u32, height: u32) -> Self {
271        Self {
272            bitrate: 15_000_000,
273            frame_rate: 60,
274            pixel_aspect_ratio: (1, 1),
275            sub_type: VideoSettingsSubType::HEVC,
276            width,
277            height,
278            disabled: false,
279        }
280    }
281
282    /// Sets the video codec/subtype (e.g., [`VideoSettingsSubType::HEVC`]).
283    pub const fn sub_type(mut self, sub_type: VideoSettingsSubType) -> Self {
284        self.sub_type = sub_type;
285        self
286    }
287    /// Sets target bitrate in bits per second.
288    pub const fn bitrate(mut self, bitrate: u32) -> Self {
289        self.bitrate = bitrate;
290        self
291    }
292    /// Sets target frame width in pixels.
293    pub const fn width(mut self, width: u32) -> Self {
294        self.width = width;
295        self
296    }
297    /// Sets target frame height in pixels.
298    pub const fn height(mut self, height: u32) -> Self {
299        self.height = height;
300        self
301    }
302    /// Sets target frame rate (numerator; denominator is fixed to 1).
303    pub const fn frame_rate(mut self, frame_rate: u32) -> Self {
304        self.frame_rate = frame_rate;
305        self
306    }
307    /// Sets pixel aspect ratio as (numerator, denominator).
308    pub const fn pixel_aspect_ratio(mut self, par: (u32, u32)) -> Self {
309        self.pixel_aspect_ratio = par;
310        self
311    }
312    /// Disables or enables video encoding.
313    ///
314    /// When `true`, calls to send frames still succeed but produce no video samples.
315    pub const fn disabled(mut self, disabled: bool) -> Self {
316        self.disabled = disabled;
317        self
318    }
319
320    fn build(self) -> Result<(VideoEncodingProperties, bool), VideoEncoderError> {
321        let properties = VideoEncodingProperties::new()?;
322        properties.SetSubtype(&self.sub_type.to_hstring())?;
323        properties.SetBitrate(self.bitrate)?;
324        properties.SetWidth(self.width)?;
325        properties.SetHeight(self.height)?;
326        properties.FrameRate()?.SetNumerator(self.frame_rate)?;
327        properties.FrameRate()?.SetDenominator(1)?;
328        properties.PixelAspectRatio()?.SetNumerator(self.pixel_aspect_ratio.0)?;
329        properties.PixelAspectRatio()?.SetDenominator(self.pixel_aspect_ratio.1)?;
330        Ok((properties, self.disabled))
331    }
332}
333
334/// Builder for configuring audio encoder settings.
335pub struct AudioSettingsBuilder {
336    bitrate: u32,
337    channel_count: u32,
338    sample_rate: u32,
339    bit_per_sample: u32,
340    sub_type: AudioSettingsSubType,
341    disabled: bool,
342}
343
344impl AudioSettingsBuilder {
345    /// Constructs a new [`AudioSettingsBuilder`] with common defaults.
346    ///
347    /// Defaults:
348    /// - Bitrate: 192 kbps
349    /// - Channels: 2
350    /// - Sample rate: 48 kHz
351    /// - Bits per sample: 16
352    /// - Subtype: [`AudioSettingsSubType::AAC`]
353    /// - Disabled: false
354    pub const fn new() -> Self {
355        Self {
356            bitrate: 192_000,
357            channel_count: 2,
358            sample_rate: 48_000,
359            bit_per_sample: 16,
360            sub_type: AudioSettingsSubType::AAC,
361            disabled: false,
362        }
363    }
364    /// Sets audio bitrate in bits per second.
365    pub const fn bitrate(mut self, bitrate: u32) -> Self {
366        self.bitrate = bitrate;
367        self
368    }
369    /// Sets number of interleaved channels.
370    pub const fn channel_count(mut self, channel_count: u32) -> Self {
371        self.channel_count = channel_count;
372        self
373    }
374    /// Sets sample rate in Hz.
375    pub const fn sample_rate(mut self, sample_rate: u32) -> Self {
376        self.sample_rate = sample_rate;
377        self
378    }
379    /// Sets bits per sample.
380    pub const fn bit_per_sample(mut self, bit_per_sample: u32) -> Self {
381        self.bit_per_sample = bit_per_sample;
382        self
383    }
384    /// Sets audio codec/subtype (e.g., [`AudioSettingsSubType::AAC`]).
385    pub const fn sub_type(mut self, sub_type: AudioSettingsSubType) -> Self {
386        self.sub_type = sub_type;
387        self
388    }
389    /// Disables or enables audio encoding.
390    pub const fn disabled(mut self, disabled: bool) -> Self {
391        self.disabled = disabled;
392        self
393    }
394
395    fn build(self) -> Result<(AudioEncodingProperties, bool), VideoEncoderError> {
396        let properties = AudioEncodingProperties::new()?;
397        properties.SetBitrate(self.bitrate)?;
398        properties.SetChannelCount(self.channel_count)?;
399        properties.SetSampleRate(self.sample_rate)?;
400        properties.SetBitsPerSample(self.bit_per_sample)?;
401        properties.SetSubtype(&self.sub_type.to_hstring())?;
402        Ok((properties, self.disabled))
403    }
404}
405
406impl Default for AudioSettingsBuilder {
407    fn default() -> Self {
408        Self::new()
409    }
410}
411
412/// Builder for configuring container settings.
413pub struct ContainerSettingsBuilder {
414    sub_type: ContainerSettingsSubType,
415}
416impl ContainerSettingsBuilder {
417    /// Constructs a new [`ContainerSettingsBuilder`].
418    ///
419    /// Default subtype: [`ContainerSettingsSubType::MPEG4`].
420    pub const fn new() -> Self {
421        Self { sub_type: ContainerSettingsSubType::MPEG4 }
422    }
423    /// Sets the container subtype (e.g., [`ContainerSettingsSubType::MPEG4`]).
424    pub const fn sub_type(mut self, sub_type: ContainerSettingsSubType) -> Self {
425        self.sub_type = sub_type;
426        self
427    }
428    fn build(self) -> Result<ContainerEncodingProperties, VideoEncoderError> {
429        let properties = ContainerEncodingProperties::new()?;
430        properties.SetSubtype(&self.sub_type.to_hstring())?;
431        Ok(properties)
432    }
433}
434impl Default for ContainerSettingsBuilder {
435    fn default() -> Self {
436        Self::new()
437    }
438}
439
440/// Video encoder subtypes.
441#[derive(Eq, PartialEq, Clone, Copy, Debug)]
442pub enum VideoSettingsSubType {
443    /// Uncompressed 32-bit ARGB (8:8:8:8).
444    ARGB32,
445    /// Uncompressed 32-bit BGRA (8:8:8:8).
446    BGRA8,
447    /// 16-bit depth format.
448    D16,
449    /// H.263 video.
450    H263,
451    /// H.264/AVC video.
452    H264,
453    /// H.264 elementary stream.
454    H264ES,
455    /// H.265/HEVC video.
456    HEVC,
457    /// H.265/HEVC elementary stream.
458    HEVCES,
459    /// Planar YUV 4:2:0 (IYUV).
460    IYUV,
461    /// 8-bit luminance (grayscale).
462    L8,
463    /// 16-bit luminance (grayscale).
464    L16,
465    /// Motion JPEG.
466    MJPG,
467    /// NV12 YUV 4:2:0 (semi-planar).
468    NV12,
469    /// MPEG-1 video.
470    MPEG1,
471    /// MPEG-2 video.
472    MPEG2,
473    /// 24-bit RGB.
474    RGB24,
475    /// 32-bit RGB.
476    RGB32,
477    /// Windows Media Video 9 (WMV3).
478    WMV3,
479    /// Windows Media Video Advanced Profile (VC-1).
480    WVC1,
481    /// VP9 video.
482    VP9,
483    /// Packed YUY2 4:2:2.
484    YUY2,
485    /// Planar YV12 4:2:0.
486    YV12,
487}
488impl VideoSettingsSubType {
489    /// Returns the Windows Media subtype identifier string for this [`VideoSettingsSubType`].
490    pub fn to_hstring(&self) -> HSTRING {
491        let s = match self {
492            Self::ARGB32 => "ARGB32",
493            Self::BGRA8 => "BGRA8",
494            Self::D16 => "D16",
495            Self::H263 => "H263",
496            Self::H264 => "H264",
497            Self::H264ES => "H264ES",
498            Self::HEVC => "HEVC",
499            Self::HEVCES => "HEVCES",
500            Self::IYUV => "IYUV",
501            Self::L8 => "L8",
502            Self::L16 => "L16",
503            Self::MJPG => "MJPG",
504            Self::NV12 => "NV12",
505            Self::MPEG1 => "MPEG1",
506            Self::MPEG2 => "MPEG2",
507            Self::RGB24 => "RGB24",
508            Self::RGB32 => "RGB32",
509            Self::WMV3 => "WMV3",
510            Self::WVC1 => "WVC1",
511            Self::VP9 => "VP9",
512            Self::YUY2 => "YUY2",
513            Self::YV12 => "YV12",
514        };
515        HSTRING::from(s)
516    }
517}
518
519/// Audio encoder subtypes.
520#[derive(Eq, PartialEq, Clone, Copy, Debug)]
521pub enum AudioSettingsSubType {
522    /// Advanced Audio Coding (AAC).
523    AAC,
524    /// Dolby Digital (AC-3).
525    AC3,
526    /// AAC framed with ADTS headers.
527    AACADTS,
528    /// AAC with HDCP protection.
529    AACHDCP,
530    /// AC-3 over S/PDIF.
531    AC3SPDIF,
532    /// AC-3 with HDCP protection.
533    AC3HDCP,
534    /// ADTS (Audio Data Transport Stream).
535    ADTS,
536    /// Apple Lossless Audio Codec (ALAC).
537    ALAC,
538    /// Adaptive Multi-Rate Narrowband (AMR-NB).
539    AMRNB,
540    /// Adaptive Multi-Rate Wideband (AMR-WB).
541    AWRWB,
542    /// DTS audio.
543    DTS,
544    /// Enhanced AC-3 (E-AC-3).
545    EAC3,
546    /// Free Lossless Audio Codec (FLAC).
547    FLAC,
548    /// 32-bit floating-point PCM.
549    Float,
550    /// MPEG-1/2 Layer III (MP3).
551    MP3,
552    /// Generic MPEG audio.
553    MPEG,
554    /// Opus audio.
555    OPUS,
556    /// Pulse-code modulation (PCM).
557    PCM,
558    /// Windows Media Audio 8.
559    WMA8,
560    /// Windows Media Audio 9.
561    WMA9,
562    /// Vorbis audio.
563    Vorbis,
564}
565impl AudioSettingsSubType {
566    /// Returns the Windows Media subtype identifier string for this [`AudioSettingsSubType`].
567    pub fn to_hstring(&self) -> HSTRING {
568        let s = match self {
569            Self::AAC => "AAC",
570            Self::AC3 => "AC3",
571            Self::AACADTS => "AACADTS",
572            Self::AACHDCP => "AACHDCP",
573            Self::AC3SPDIF => "AC3SPDIF",
574            Self::AC3HDCP => "AC3HDCP",
575            Self::ADTS => "ADTS",
576            Self::ALAC => "ALAC",
577            Self::AMRNB => "AMRNB",
578            Self::AWRWB => "AWRWB",
579            Self::DTS => "DTS",
580            Self::EAC3 => "EAC3",
581            Self::FLAC => "FLAC",
582            Self::Float => "Float",
583            Self::MP3 => "MP3",
584            Self::MPEG => "MPEG",
585            Self::OPUS => "OPUS",
586            Self::PCM => "PCM",
587            Self::WMA8 => "WMA8",
588            Self::WMA9 => "WMA9",
589            Self::Vorbis => "Vorbis",
590        };
591        HSTRING::from(s)
592    }
593}
594
595/// Container subtypes.
596#[derive(Eq, PartialEq, Clone, Copy, Debug)]
597pub enum ContainerSettingsSubType {
598    /// Advanced Systems Format (ASF).
599    ASF,
600    /// Raw MP3 container.
601    MP3,
602    /// MPEG-4 container (e.g., MP4).
603    MPEG4,
604    /// Audio Video Interleave (AVI).
605    AVI,
606    /// MPEG-2 container.
607    MPEG2,
608    /// WAVE (WAV) container.
609    WAVE,
610    /// AAC ADTS stream.
611    AACADTS,
612    /// ADTS container.
613    ADTS,
614    /// 3GP container.
615    GP3,
616    /// AMR container.
617    AMR,
618    /// FLAC container.
619    FLAC,
620}
621impl ContainerSettingsSubType {
622    /// Returns the Windows Media container subtype identifier string for this
623    /// [`ContainerSettingsSubType`].
624    pub fn to_hstring(&self) -> HSTRING {
625        match self {
626            Self::ASF => HSTRING::from("ASF"),
627            Self::MP3 => HSTRING::from("MP3"),
628            Self::MPEG4 => HSTRING::from("MPEG4"),
629            Self::AVI => HSTRING::from("AVI"),
630            Self::MPEG2 => HSTRING::from("MPEG2"),
631            Self::WAVE => HSTRING::from("WAVE"),
632            Self::AACADTS => HSTRING::from("AACADTS"),
633            Self::ADTS => HSTRING::from("ADTS"),
634            Self::GP3 => HSTRING::from("3GP"),
635            Self::AMR => HSTRING::from("AMR"),
636            Self::FLAC => HSTRING::from("FLAC"),
637        }
638    }
639}
640
641/// Encodes video frames (and optional audio) and writes them to a file or stream.
642///
643/// Frames are provided as Direct3D surfaces or raw BGRA buffers. Audio can be pushed
644/// as interleaved PCM bytes.
645///
646/// - Use [`VideoEncoder::new`] for file output or [`VideoEncoder::new_from_stream`] for stream
647///   output.
648/// - Push frames with [`VideoEncoder::send_frame`] or [`VideoEncoder::send_frame_buffer`].
649/// - Optionally push audio with [`VideoEncoder::send_audio_buffer`] or use
650///   [`VideoEncoder::send_frame_with_audio`].
651/// - Call [`VideoEncoder::finish`] to finalize the container.
652///
653/// # Example
654/// ```no_run
655/// use windows_capture::encoder::{
656///     AudioSettingsBuilder, ContainerSettingsBuilder, VideoEncoder, VideoSettingsBuilder,
657/// };
658///
659/// // Create an encoder that outputs H.265 in an MP4 container
660/// let mut encoder = VideoEncoder::new(
661///     VideoSettingsBuilder::new(1920, 1080),
662///     AudioSettingsBuilder::new().disabled(true),
663///     ContainerSettingsBuilder::new(),
664///     "capture.mp4",
665/// )
666/// .unwrap();
667///
668/// // In your capture loop, push frames:
669/// // encoder.send_frame(&frame).unwrap();
670///
671/// // When done:
672/// // encoder.finish().unwrap();
673/// ```
674pub struct VideoEncoder {
675    // Video timing
676    first_timestamp: Option<TimeSpan>,
677
678    // Channels
679    frame_sender: mpsc::Sender<Option<(VideoEncoderSource, TimeSpan)>>,
680    audio_sender: mpsc::Sender<Option<(AudioEncoderSource, TimeSpan)>>,
681
682    // MSS event tokens
683    sample_requested: i64,
684    media_stream_source: MediaStreamSource,
685    starting: i64,
686
687    // Transcode worker
688    transcode_thread: Option<JoinHandle<Result<(), VideoEncoderError>>>,
689    error_notify: Arc<AtomicBool>,
690
691    // Feature toggles
692    is_video_disabled: bool,
693    is_audio_disabled: bool,
694
695    // --- NEW: audio clock & format bookkeeping (monotonic timing) ---
696    audio_sample_rate: u32,  // Hz (frames per second)
697    audio_block_align: u32,  // bytes per interleaved sample frame (channels * (bits/8))
698    audio_samples_sent: u64, // number of sample frames (not bytes) emitted so far
699
700    // Video sizing constraints
701    target_width: u32,
702    target_height: u32,
703    target_color_format: ColorFormat,
704
705    cached_surface: Option<CachedSurface>,
706}
707
708impl VideoEncoder {
709    fn create_cached_surface(
710        device: &ID3D11Device,
711        width: u32,
712        height: u32,
713        format: ColorFormat,
714    ) -> Result<CachedSurface, VideoEncoderError> {
715        let texture_desc = D3D11_TEXTURE2D_DESC {
716            Width: width,
717            Height: height,
718            MipLevels: 1,
719            ArraySize: 1,
720            Format: DXGI_FORMAT(format as i32),
721            SampleDesc: DXGI_SAMPLE_DESC { Count: 1, Quality: 0 },
722            Usage: D3D11_USAGE_DEFAULT,
723            BindFlags: (D3D11_BIND_RENDER_TARGET.0 | D3D11_BIND_SHADER_RESOURCE.0) as u32,
724            CPUAccessFlags: 0,
725            MiscFlags: 0,
726        };
727
728        let mut texture = None;
729        unsafe {
730            device.CreateTexture2D(&texture_desc, None, Some(&mut texture))?;
731        }
732        let texture = texture.expect("CreateTexture2D returned None");
733
734        let mut render_target = None;
735        unsafe {
736            device.CreateRenderTargetView(&texture, None, Some(&mut render_target))?;
737        }
738        let render_target_view = render_target.map(SendDirectX::new);
739
740        let dxgi_surface: IDXGISurface = texture.cast()?;
741        let inspectable = unsafe { CreateDirect3D11SurfaceFromDXGISurface(&dxgi_surface)? };
742        let surface: IDirect3DSurface = inspectable.cast()?;
743
744        Ok(CachedSurface {
745            width,
746            height,
747            format,
748            texture: SendDirectX::new(texture),
749            surface: SendDirectX::new(surface),
750            render_target_view,
751        })
752    }
753
754    fn attach_sample_requested_handlers(
755        media_stream_source: &MediaStreamSource,
756        is_video_disabled: bool,
757        is_audio_disabled: bool,
758        frame_receiver: VideoFrameReceiver,
759        audio_receiver: AudioFrameReceiver,
760        audio_block_align: u32,
761        audio_sample_rate: u32,
762    ) -> Result<i64, VideoEncoderError> {
763        let token = media_stream_source.SampleRequested(&TypedEventHandler::<
764            MediaStreamSource,
765            MediaStreamSourceSampleRequestedEventArgs,
766        >::new(move |_, sample_requested| {
767            let sample_requested = sample_requested
768                .as_ref()
769                .expect("MediaStreamSource SampleRequested parameter was None. This should not happen.");
770
771            let request = sample_requested.Request()?;
772            let is_audio = request.StreamDescriptor()?.cast::<AudioStreamDescriptor>().is_ok();
773
774            // Always offload blocking work to the thread pool; never block the MSS event
775            // thread.
776            let deferral = request.GetDeferral()?;
777
778            if is_audio {
779                if is_audio_disabled {
780                    request.SetSample(None)?;
781                    deferral.Complete()?;
782                } else {
783                    let request_clone = request;
784                    let audio_receiver = audio_receiver.clone();
785                    ThreadPool::RunWithPriorityAndOptionsAsync(
786                        &WorkItemHandler::new(move |_| {
787                            let value = audio_receiver.lock().recv();
788                            match value {
789                                Ok(Some((source, timestamp))) => {
790                                    let sample = match source {
791                                        AudioEncoderSource::Buffer(bytes) => {
792                                            let buf = CryptographicBuffer::CreateFromByteArray(&bytes)?;
793                                            let sample = MediaStreamSample::CreateFromBuffer(&buf, timestamp)?;
794                                            // Duration = (frames / sample_rate) in 100ns ticks
795                                            // frames = bytes / block_align
796                                            let frames = (bytes.len() as u32) / audio_block_align;
797                                            let duration_ticks =
798                                                (frames as i64) * 10_000_000i64 / (audio_sample_rate as i64);
799                                            sample.SetDuration(TimeSpan { Duration: duration_ticks })?;
800                                            sample
801                                        }
802                                    };
803                                    request_clone.SetSample(&sample)?;
804                                }
805                                Ok(None) | Err(_) => {
806                                    request_clone.SetSample(None)?;
807                                }
808                            }
809                            deferral.Complete()?;
810                            Ok(())
811                        }),
812                        WorkItemPriority::Normal,
813                        WorkItemOptions::None,
814                    )?;
815                }
816            } else if is_video_disabled {
817                request.SetSample(None)?;
818                deferral.Complete()?;
819            } else {
820                let request_clone = request;
821                let frame_receiver = frame_receiver.clone();
822                ThreadPool::RunWithPriorityAndOptionsAsync(
823                    &WorkItemHandler::new(move |_| {
824                        let value = frame_receiver.lock().recv();
825                        match value {
826                            Ok(Some((source, timestamp))) => {
827                                let sample = match source {
828                                    VideoEncoderSource::DirectX(surface) => {
829                                        MediaStreamSample::CreateFromDirect3D11Surface(&surface.0, timestamp)?
830                                    }
831                                    VideoEncoderSource::Buffer(bytes) => {
832                                        let buf = CryptographicBuffer::CreateFromByteArray(&bytes)?;
833                                        MediaStreamSample::CreateFromBuffer(&buf, timestamp)?
834                                    }
835                                };
836                                request_clone.SetSample(&sample)?;
837                            }
838                            Ok(None) | Err(_) => {
839                                request_clone.SetSample(None)?;
840                            }
841                        }
842                        deferral.Complete()?;
843                        Ok(())
844                    }),
845                    WorkItemPriority::Normal,
846                    WorkItemOptions::None,
847                )?;
848            }
849
850            Ok(())
851        }))?;
852        Ok(token)
853    }
854
855    /// Constructs a new `VideoEncoder` that writes to a file path.
856    #[inline]
857    pub fn new<P: AsRef<Path>>(
858        video_settings: VideoSettingsBuilder,
859        audio_settings: AudioSettingsBuilder,
860        container_settings: ContainerSettingsBuilder,
861        path: P,
862    ) -> Result<Self, VideoEncoderError> {
863        let path = path.as_ref();
864        let media_encoding_profile = MediaEncodingProfile::new()?;
865
866        let (video_encoding_properties_cfg, is_video_disabled) = video_settings.build()?;
867        media_encoding_profile.SetVideo(&video_encoding_properties_cfg)?;
868        let (audio_encoding_properties_cfg, is_audio_disabled) = audio_settings.build()?;
869        media_encoding_profile.SetAudio(&audio_encoding_properties_cfg)?;
870        let container_encoding_properties = container_settings.build()?;
871        media_encoding_profile.SetContainer(&container_encoding_properties)?;
872
873        let target_width = video_encoding_properties_cfg.Width()?;
874        let target_height = video_encoding_properties_cfg.Height()?;
875        let target_color_format = ColorFormat::Bgra8;
876
877        let video_encoding_properties = VideoEncodingProperties::CreateUncompressed(
878            &MediaEncodingSubtypes::Bgra8()?,
879            video_encoding_properties_cfg.Width()?,
880            video_encoding_properties_cfg.Height()?,
881        )?;
882        let video_stream_descriptor = VideoStreamDescriptor::Create(&video_encoding_properties)?;
883
884        // Stream descriptor uses PCM; the profile still encodes to AAC/OPUS/etc.
885        let audio_desc_props = AudioEncodingProperties::CreatePcm(
886            audio_encoding_properties_cfg.SampleRate()?,
887            audio_encoding_properties_cfg.ChannelCount()?,
888            audio_encoding_properties_cfg.BitsPerSample()?,
889        )?;
890        let audio_stream_descriptor = AudioStreamDescriptor::Create(&audio_desc_props)?;
891
892        // Compute audio block align/sample-rate for monotonic clock
893        let audio_sr = audio_desc_props.SampleRate()?;
894        let audio_ch = audio_desc_props.ChannelCount()?;
895        let audio_bps = audio_desc_props.BitsPerSample()?;
896        let audio_block_align = (audio_bps / 8) * audio_ch;
897
898        let media_stream_source =
899            MediaStreamSource::CreateFromDescriptors(&video_stream_descriptor, &audio_stream_descriptor)?;
900        // Keep a modest buffer (30ms)
901        media_stream_source.SetBufferTime(Duration::from_millis(30).into())?;
902
903        let starting = media_stream_source.Starting(&TypedEventHandler::<
904            MediaStreamSource,
905            MediaStreamSourceStartingEventArgs,
906        >::new(move |_, stream_start| {
907            let stream_start =
908                stream_start.as_ref().expect("MediaStreamSource Starting parameter was None. This should not happen.");
909            stream_start.Request()?.SetActualStartPosition(TimeSpan { Duration: 0 })?;
910            Ok(())
911        }))?;
912
913        let (frame_sender, frame_receiver_raw) = mpsc::channel::<Option<(VideoEncoderSource, TimeSpan)>>();
914        let (audio_sender, audio_receiver_raw) = mpsc::channel::<Option<(AudioEncoderSource, TimeSpan)>>();
915
916        let frame_receiver = Arc::new(Mutex::new(frame_receiver_raw));
917        let audio_receiver = Arc::new(Mutex::new(audio_receiver_raw));
918
919        let sample_requested = Self::attach_sample_requested_handlers(
920            &media_stream_source,
921            is_video_disabled,
922            is_audio_disabled,
923            frame_receiver,
924            audio_receiver,
925            audio_block_align,
926            audio_sr,
927        )?;
928
929        let media_transcoder = MediaTranscoder::new()?;
930        media_transcoder.SetHardwareAccelerationEnabled(true)?;
931
932        File::create(path)?;
933        let path = fs::canonicalize(path)?.to_string_lossy()[4..].to_string();
934        let path = Path::new(&path);
935        let path = &HSTRING::from(path.as_os_str().to_os_string());
936
937        let file = StorageFile::GetFileFromPathAsync(path)?.join()?;
938        let media_stream_output = file.OpenAsync(FileAccessMode::ReadWrite)?.join()?;
939
940        let transcode = media_transcoder
941            .PrepareMediaStreamSourceTranscodeAsync(
942                &media_stream_source,
943                &media_stream_output,
944                &media_encoding_profile,
945            )?
946            .join()?;
947
948        let error_notify = Arc::new(AtomicBool::new(false));
949        let transcode_thread = thread::spawn({
950            let error_notify = error_notify.clone();
951            move || -> Result<(), VideoEncoderError> {
952                let result = transcode.TranscodeAsync();
953                if result.is_err() {
954                    error_notify.store(true, atomic::Ordering::Relaxed);
955                }
956                result?.join()?;
957                drop(media_transcoder);
958                Ok(())
959            }
960        });
961
962        Ok(Self {
963            first_timestamp: None,
964            frame_sender,
965            audio_sender,
966            sample_requested,
967            media_stream_source,
968            starting,
969            transcode_thread: Some(transcode_thread),
970            error_notify,
971            is_video_disabled,
972            is_audio_disabled,
973            audio_sample_rate: audio_sr,
974            audio_block_align,
975            audio_samples_sent: 0,
976            target_width,
977            target_height,
978            target_color_format,
979            cached_surface: None,
980        })
981    }
982
983    /// Constructs a new `VideoEncoder` that writes to the given stream.
984    ///
985    /// Unlike [`VideoEncoder::new`], which writes directly to a file, this constructor writes
986    /// encoded output into any [`IRandomAccessStream`]. Use [`InMemoryRandomAccessStream`] to
987    /// keep the encoded video in memory (e.g., for network streaming or further processing).
988    ///
989    /// # Example
990    /// ```no_run
991    /// use windows::Storage::Streams::InMemoryRandomAccessStream;
992    /// use windows::core::Interface;
993    /// use windows_capture::encoder::{
994    ///     AudioSettingsBuilder, ContainerSettingsBuilder, VideoEncoder, VideoSettingsBuilder,
995    /// };
996    ///
997    /// let stream = InMemoryRandomAccessStream::new().unwrap();
998    ///
999    /// let encoder = VideoEncoder::new_from_stream(
1000    ///     VideoSettingsBuilder::new(1920, 1080),
1001    ///     AudioSettingsBuilder::new().disabled(true),
1002    ///     ContainerSettingsBuilder::new(),
1003    ///     stream.cast().unwrap(),
1004    /// )
1005    /// .unwrap();
1006    /// ```
1007    #[inline]
1008    pub fn new_from_stream(
1009        video_settings: VideoSettingsBuilder,
1010        audio_settings: AudioSettingsBuilder,
1011        container_settings: ContainerSettingsBuilder,
1012        stream: IRandomAccessStream,
1013    ) -> Result<Self, VideoEncoderError> {
1014        let media_encoding_profile = MediaEncodingProfile::new()?;
1015
1016        let (video_encoding_properties_cfg, is_video_disabled) = video_settings.build()?;
1017        media_encoding_profile.SetVideo(&video_encoding_properties_cfg)?;
1018        let (audio_encoding_properties_cfg, is_audio_disabled) = audio_settings.build()?;
1019        media_encoding_profile.SetAudio(&audio_encoding_properties_cfg)?;
1020        let container_encoding_properties = container_settings.build()?;
1021        media_encoding_profile.SetContainer(&container_encoding_properties)?;
1022
1023        let target_width = video_encoding_properties_cfg.Width()?;
1024        let target_height = video_encoding_properties_cfg.Height()?;
1025        let target_color_format = ColorFormat::Bgra8;
1026
1027        let video_encoding_properties = VideoEncodingProperties::CreateUncompressed(
1028            &MediaEncodingSubtypes::Bgra8()?,
1029            video_encoding_properties_cfg.Width()?,
1030            video_encoding_properties_cfg.Height()?,
1031        )?;
1032        let video_stream_descriptor = VideoStreamDescriptor::Create(&video_encoding_properties)?;
1033
1034        let audio_desc_props = AudioEncodingProperties::CreatePcm(
1035            audio_encoding_properties_cfg.SampleRate()?,
1036            audio_encoding_properties_cfg.ChannelCount()?,
1037            audio_encoding_properties_cfg.BitsPerSample()?,
1038        )?;
1039        let audio_stream_descriptor = AudioStreamDescriptor::Create(&audio_desc_props)?;
1040
1041        // Monotonic audio timing parameters
1042        let audio_sr = audio_desc_props.SampleRate()?;
1043        let audio_ch = audio_desc_props.ChannelCount()?;
1044        let audio_bps = audio_desc_props.BitsPerSample()?;
1045        let audio_block_align = (audio_bps / 8) * audio_ch;
1046
1047        let media_stream_source =
1048            MediaStreamSource::CreateFromDescriptors(&video_stream_descriptor, &audio_stream_descriptor)?;
1049        // CHANGED: use 30ms buffer (was 0)
1050        media_stream_source.SetBufferTime(Duration::from_millis(30).into())?;
1051
1052        let starting = media_stream_source.Starting(&TypedEventHandler::<
1053            MediaStreamSource,
1054            MediaStreamSourceStartingEventArgs,
1055        >::new(move |_, stream_start| {
1056            let stream_start =
1057                stream_start.as_ref().expect("MediaStreamSource Starting parameter was None. This should not happen.");
1058            stream_start.Request()?.SetActualStartPosition(TimeSpan { Duration: 0 })?;
1059            Ok(())
1060        }))?;
1061
1062        let (frame_sender, frame_receiver_raw) = mpsc::channel::<Option<(VideoEncoderSource, TimeSpan)>>();
1063        let (audio_sender, audio_receiver_raw) = mpsc::channel::<Option<(AudioEncoderSource, TimeSpan)>>();
1064
1065        let frame_receiver = Arc::new(Mutex::new(frame_receiver_raw));
1066        let audio_receiver = Arc::new(Mutex::new(audio_receiver_raw));
1067
1068        let sample_requested = Self::attach_sample_requested_handlers(
1069            &media_stream_source,
1070            is_video_disabled,
1071            is_audio_disabled,
1072            frame_receiver,
1073            audio_receiver,
1074            audio_block_align,
1075            audio_sr,
1076        )?;
1077
1078        let media_transcoder = MediaTranscoder::new()?;
1079        media_transcoder.SetHardwareAccelerationEnabled(true)?;
1080
1081        let transcode = media_transcoder
1082            .PrepareMediaStreamSourceTranscodeAsync(&media_stream_source, &stream, &media_encoding_profile)?
1083            .join()?;
1084
1085        let error_notify = Arc::new(AtomicBool::new(false));
1086        let transcode_thread = thread::spawn({
1087            let error_notify = error_notify.clone();
1088            move || -> Result<(), VideoEncoderError> {
1089                let result = transcode.TranscodeAsync();
1090                if result.is_err() {
1091                    error_notify.store(true, atomic::Ordering::Relaxed);
1092                }
1093                result?.join()?;
1094                drop(media_transcoder);
1095                Ok(())
1096            }
1097        });
1098
1099        Ok(Self {
1100            first_timestamp: None,
1101            frame_sender,
1102            audio_sender,
1103            sample_requested,
1104            media_stream_source,
1105            starting,
1106            transcode_thread: Some(transcode_thread),
1107            error_notify,
1108            is_video_disabled,
1109            is_audio_disabled,
1110            audio_sample_rate: audio_sr,
1111            audio_block_align,
1112            audio_samples_sent: 0,
1113            target_width,
1114            target_height,
1115            target_color_format,
1116            cached_surface: None,
1117        })
1118    }
1119
1120    fn build_padded_surface(&mut self, frame: &Frame) -> Result<SendDirectX<IDirect3DSurface>, VideoEncoderError> {
1121        let frame_format = frame.color_format();
1122        let needs_recreate = self.cached_surface.as_ref().is_none_or(|cache| {
1123            cache.format != frame_format || cache.width != self.target_width || cache.height != self.target_height
1124        });
1125
1126        if needs_recreate {
1127            let surface =
1128                Self::create_cached_surface(frame.device(), self.target_width, self.target_height, frame_format)?;
1129            self.cached_surface = Some(surface);
1130            self.target_color_format = frame_format;
1131        }
1132
1133        let cache = self.cached_surface.as_mut().expect("cached_surface must be populated before use");
1134        let context = frame.device_context();
1135
1136        if let Some(rtv) = &cache.render_target_view {
1137            let clear_color = [0.0f32, 0.0, 0.0, 1.0];
1138            unsafe {
1139                context.ClearRenderTargetView(&rtv.0, &clear_color);
1140            }
1141        }
1142
1143        let copy_width = self.target_width.min(frame.width());
1144        let copy_height = self.target_height.min(frame.height());
1145
1146        if copy_width > 0 && copy_height > 0 {
1147            let source_box = D3D11_BOX { left: 0, top: 0, front: 0, right: copy_width, bottom: copy_height, back: 1 };
1148            unsafe {
1149                context.CopySubresourceRegion(
1150                    &cache.texture.0,
1151                    0,
1152                    0,
1153                    0,
1154                    0,
1155                    frame.as_raw_texture(),
1156                    0,
1157                    Some(&source_box),
1158                );
1159            }
1160        }
1161
1162        unsafe {
1163            context.Flush();
1164        }
1165
1166        Ok(SendDirectX::new(cache.surface.0.clone()))
1167    }
1168
1169    /// Sends a video frame (DirectX). Returns immediately.
1170    #[inline]
1171    pub fn send_frame(&mut self, frame: &Frame) -> Result<(), VideoEncoderError> {
1172        if self.is_video_disabled {
1173            return Err(VideoEncoderError::VideoDisabled);
1174        }
1175
1176        let timestamp = match self.first_timestamp {
1177            Some(t0) => TimeSpan { Duration: frame.timestamp()?.Duration - t0.Duration },
1178            None => {
1179                let ts = frame.timestamp()?;
1180                self.first_timestamp = Some(ts);
1181                TimeSpan { Duration: 0 }
1182            }
1183        };
1184
1185        let surface = if frame.width() == self.target_width && frame.height() == self.target_height {
1186            SendDirectX::new(frame.as_raw_surface().clone())
1187        } else {
1188            self.build_padded_surface(frame)?
1189        };
1190
1191        self.frame_sender.send(Some((VideoEncoderSource::DirectX(surface), timestamp)))?;
1192
1193        if self.error_notify.load(atomic::Ordering::Relaxed)
1194            && let Some(t) = self.transcode_thread.take()
1195        {
1196            t.join().expect("Failed to join transcode thread")?;
1197        }
1198
1199        Ok(())
1200    }
1201
1202    /// Sends a video frame and an audio buffer (owned). Returns immediately.
1203    /// Audio timestamp is derived from total samples sent so far (monotonic).
1204    #[inline]
1205    pub fn send_frame_with_audio(&mut self, frame: &mut Frame, audio_buffer: &[u8]) -> Result<(), VideoEncoderError> {
1206        if self.is_video_disabled {
1207            return Err(VideoEncoderError::VideoDisabled);
1208        }
1209        if self.is_audio_disabled {
1210            return Err(VideoEncoderError::AudioDisabled);
1211        }
1212
1213        // Video timestamp based on capture timestamps (as before)
1214        let video_ts = match self.first_timestamp {
1215            Some(t0) => TimeSpan { Duration: frame.timestamp()?.Duration - t0.Duration },
1216            None => {
1217                let ts = frame.timestamp()?;
1218                self.first_timestamp = Some(ts);
1219                TimeSpan { Duration: 0 }
1220            }
1221        };
1222
1223        let surface = if frame.width() == self.target_width && frame.height() == self.target_height {
1224            SendDirectX::new(frame.as_raw_surface().clone())
1225        } else {
1226            self.build_padded_surface(frame)?
1227        };
1228
1229        self.frame_sender.send(Some((VideoEncoderSource::DirectX(surface), video_ts)))?;
1230
1231        // Audio timestamp from running sample count
1232        let frames_in_buf = (audio_buffer.len() as u32) / self.audio_block_align;
1233        let audio_ts_ticks = ((self.audio_samples_sent as i128) * 10_000_000i128) / (self.audio_sample_rate as i128);
1234        let audio_ts = TimeSpan { Duration: audio_ts_ticks as i64 };
1235
1236        self.audio_sender.send(Some((AudioEncoderSource::Buffer(audio_buffer.to_vec()), audio_ts)))?;
1237
1238        // Advance counter after stamping
1239        self.audio_samples_sent = self.audio_samples_sent.saturating_add(frames_in_buf as u64);
1240
1241        if self.error_notify.load(atomic::Ordering::Relaxed)
1242            && let Some(t) = self.transcode_thread.take()
1243        {
1244            t.join().expect("Failed to join transcode thread")?;
1245        }
1246
1247        Ok(())
1248    }
1249
1250    /// Sends a raw frame buffer (owned inside). Returns immediately.
1251    /// Windows expects BGRA and bottom-to-top layout for this path.
1252    #[inline]
1253    pub fn send_frame_buffer(&mut self, buffer: &[u8], timestamp: i64) -> Result<(), VideoEncoderError> {
1254        if self.is_video_disabled {
1255            return Err(VideoEncoderError::VideoDisabled);
1256        }
1257
1258        let frame_timestamp = timestamp;
1259        let timestamp = match self.first_timestamp {
1260            Some(t0) => TimeSpan { Duration: frame_timestamp - t0.Duration },
1261            None => {
1262                self.first_timestamp = Some(TimeSpan { Duration: frame_timestamp });
1263                TimeSpan { Duration: 0 }
1264            }
1265        };
1266
1267        self.frame_sender.send(Some((VideoEncoderSource::Buffer(buffer.to_vec()), timestamp)))?;
1268
1269        if self.error_notify.load(atomic::Ordering::Relaxed)
1270            && let Some(t) = self.transcode_thread.take()
1271        {
1272            t.join().expect("Failed to join transcode thread")?;
1273        }
1274
1275        Ok(())
1276    }
1277
1278    /// Sends an audio buffer (owned inside). Returns immediately.
1279    /// NOTE: The provided `timestamp` is ignored; we use a monotonic audio clock.
1280    #[inline]
1281    pub fn send_audio_buffer(
1282        &mut self,
1283        buffer: &[u8],
1284        _timestamp: i64, // ignored to guarantee monotonic audio timing
1285    ) -> Result<(), VideoEncoderError> {
1286        if self.is_audio_disabled {
1287            return Err(VideoEncoderError::AudioDisabled);
1288        }
1289
1290        let frames_in_buf = (buffer.len() as u32) / self.audio_block_align;
1291        let audio_ts_ticks = ((self.audio_samples_sent as i128) * 10_000_000i128) / (self.audio_sample_rate as i128);
1292        let timestamp = TimeSpan { Duration: audio_ts_ticks as i64 };
1293
1294        self.audio_sender.send(Some((AudioEncoderSource::Buffer(buffer.to_vec()), timestamp)))?;
1295
1296        self.audio_samples_sent = self.audio_samples_sent.saturating_add(frames_in_buf as u64);
1297
1298        if self.error_notify.load(atomic::Ordering::Relaxed)
1299            && let Some(t) = self.transcode_thread.take()
1300        {
1301            t.join().expect("Failed to join transcode thread")?;
1302        }
1303
1304        Ok(())
1305    }
1306
1307    /// Finishes the encoding and performs any necessary cleanup.
1308    #[inline]
1309    pub fn finish(mut self) -> Result<(), VideoEncoderError> {
1310        // 1) Signal EOS on both streams.
1311        let _ = self.frame_sender.send(None);
1312        let _ = self.audio_sender.send(None);
1313
1314        // 2) **Close the channels** so any further recv() returns Err immediately. We replace the fields
1315        //    with dummy senders and drop the originals now.
1316        {
1317            let (dummy_tx_v, _dummy_rx_v) = mpsc::channel::<Option<(VideoEncoderSource, TimeSpan)>>();
1318            let (dummy_tx_a, _dummy_rx_a) = mpsc::channel::<Option<(AudioEncoderSource, TimeSpan)>>();
1319
1320            let old_v = std::mem::replace(&mut self.frame_sender, dummy_tx_v);
1321            let old_a = std::mem::replace(&mut self.audio_sender, dummy_tx_a);
1322            drop(old_v);
1323            drop(old_a);
1324        }
1325
1326        // 3) Wait for the transcoder to flush and finalize.
1327        if let Some(transcode_thread) = self.transcode_thread.take() {
1328            transcode_thread.join().expect("Failed to join transcode thread")?;
1329        }
1330
1331        // 4) Unhook events after pipeline has completed.
1332        self.media_stream_source.RemoveStarting(self.starting)?;
1333        self.media_stream_source.RemoveSampleRequested(self.sample_requested)?;
1334
1335        Ok(())
1336    }
1337}
1338
1339impl Drop for VideoEncoder {
1340    #[inline]
1341    fn drop(&mut self) {
1342        // Try to signal EOS, then **close** the channels before waiting.
1343        let _ = self.frame_sender.send(None);
1344        let _ = self.audio_sender.send(None);
1345
1346        // Close channels early in Drop too (same trick as in finish()).
1347        let (dummy_tx_v, _dummy_rx_v) = mpsc::channel::<Option<(VideoEncoderSource, TimeSpan)>>();
1348        let (dummy_tx_a, _dummy_rx_a) = mpsc::channel::<Option<(AudioEncoderSource, TimeSpan)>>();
1349
1350        let old_v = std::mem::replace(&mut self.frame_sender, dummy_tx_v);
1351        let old_a = std::mem::replace(&mut self.audio_sender, dummy_tx_a);
1352        drop(old_v);
1353        drop(old_a);
1354
1355        if let Some(transcode_thread) = self.transcode_thread.take() {
1356            let _ = transcode_thread.join();
1357        }
1358
1359        let _ = self.media_stream_source.RemoveStarting(self.starting);
1360        let _ = self.media_stream_source.RemoveSampleRequested(self.sample_requested);
1361    }
1362}
1363
1364#[allow(clippy::non_send_fields_in_send_ty)]
1365unsafe impl Send for VideoEncoder {}