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