Skip to main content

pixelflux/encoders/
hevc.rs

1/*
2 * This Source Code Form is subject to the terms of the Mozilla Public
3 * License, v. 2.0. If a copy of the MPL was not distributed with this
4 * file, You can obtain one at https://mozilla.org/MPL/2.0/.
5 */
6
7//! Software HEVC: x265 with the `gpl` feature, kvazaar without it, each at the fastest setting
8//! its library offers in real time (ultrafast, wavefront threads, no lookahead, no reordering)
9//! with the parameter sets repeated on every key frame. x265 carries 4:4:4 at full range like
10//! x264; kvazaar encodes 4:2:0. Neither library lets a session steer what a frame predicts
11//! from, so the session names no reference and refuses an invalidation, and the caller codes
12//! a key frame instead.
13
14use std::ffi::CString;
15
16use super::codec::{Codec, VIDEO_HEADER_LEN, h265_frame_type, push_video_header};
17use super::reference::Reference;
18use super::session::{Pending, Planes, Quality, RateSettings, encode_threads};
19use crate::RustCaptureSettings;
20
21/// One software HEVC session for one capture.
22pub struct HevcEncoder {
23    backend: Backend,
24    planes: Planes,
25    threads: i32,
26    quality: Quality,
27    rate: RateSettings,
28    omit_headers: bool,
29    /// Whether the next frame is the first after an open, which the encoder codes as a key
30    /// frame on its own.
31    fresh: bool,
32    next_pts: u64,
33    pending: Pending,
34    /// The quantizer the next frame is held at whatever the rate control (`hold_quantizer`).
35    held: Option<u32>,
36    /// The quantizer the rate control last coded a frame at, held frames aside.
37    last_quality: Option<u32>,
38    /// The bytes of that frame.
39    last_bytes: Option<usize>,
40}
41
42unsafe impl Send for HevcEncoder {}
43
44/// Whether the build's software HEVC encoder codes 10 bits: x265 where it was built with its
45/// 10-bit encoder linked in.
46pub fn ten_bit() -> bool {
47    Backend::ten_bit()
48}
49
50/// What a backend answered for one submitted picture, with the quantizer it was coded at.
51struct Coded {
52    bytes: Vec<u8>,
53    pts: u64,
54    qp: u32,
55}
56
57impl HevcEncoder {
58    /// Open a session on host frames; `rgba` names the byte order they arrive in.
59    pub fn new(settings: &RustCaptureSettings, rgba: bool) -> Result<Self, String> {
60        let _ = rgba;
61        let fullcolor = settings.video_fullcolor && Backend::FULLCOLOR;
62        let bit_depth = if settings.video_bit_depth >= 10 && Backend::ten_bit() {
63            10
64        } else {
65            8
66        };
67        let threads = encode_threads();
68        let rate = RateSettings::new(settings);
69        let quality = Quality::new(Codec::H265.quantizer(settings.video_crf));
70        let width = settings.width.max(1) as usize;
71        let height = settings.height.max(1) as usize;
72        let backend = Backend::open(
73            width,
74            height,
75            threads,
76            fullcolor,
77            bit_depth,
78            rate,
79            quality.current,
80        )?;
81        Ok(Self {
82            backend,
83            planes: Planes::new(width, height, fullcolor, bit_depth),
84            threads,
85            quality,
86            rate,
87            omit_headers: settings.omit_stripe_headers,
88            fresh: true,
89            next_pts: 0,
90            pending: Pending::default(),
91            held: None,
92            last_quality: None,
93            last_bytes: None,
94        })
95    }
96
97    pub fn codec(&self) -> Codec {
98        Codec::H265
99    }
100
101    /// The library behind the session, as the logs name it.
102    pub fn library(&self) -> &'static str {
103        Backend::LIBRARY
104    }
105
106    pub fn is_fullcolor(&self) -> bool {
107        self.planes.i444
108    }
109
110    /// The bits per sample the session codes.
111    pub fn bit_depth(&self) -> u32 {
112        self.planes.bit_depth
113    }
114
115    /// The software 4:4:4 of x264's kind signals full range.
116    pub fn is_full_range(&self) -> bool {
117        self.planes.i444
118    }
119
120    pub fn last_reference(&self) -> Reference {
121        Reference::Untracked
122    }
123
124    /// Whether `hold_quantizer` holds a frame at its quantizer: x265 at a constant rate factor,
125    /// kvazaar never. At a constant rate x265's rate control moves a forced quantizer back to
126    /// what its buffer affords (a key frame held at 18 came out 23.2 kB at 26.6 dB at 8 Mbit/s
127    /// 1080p), and a fresh session whose buffer is the key frame's budget plans it from a
128    /// fraction of that buffer (102.5 kB at 34 dB where the budget was 250 kB and the picture
129    /// before it 53.9 dB), so a constant-rate session holds none.
130    pub fn holds_quantizer(&self) -> bool {
131        Backend::HOLDS_QUANTIZER && !self.rate.cbr
132    }
133
134    /// The quality index (HEVC's quantizer) the rate control last coded a frame at, held frames
135    /// aside.
136    pub fn last_quality(&self) -> Option<u32> {
137        self.last_quality
138    }
139
140    /// The bytes of the last frame the rate control coded, held frames aside.
141    pub fn last_size(&self) -> Option<usize> {
142        self.last_bytes
143    }
144
145    /// The library keeps its reference lists to itself: refused, so the caller codes a key frame.
146    pub fn invalidate_reference(&mut self, _frame_id: u16) -> bool {
147        false
148    }
149
150    /// Open a fresh encoder with the live settings, whose first frame is a key frame.
151    fn reopen(&mut self) -> Result<(), String> {
152        self.backend = Backend::open(
153            self.planes.width,
154            self.planes.height,
155            self.threads,
156            self.planes.i444,
157            self.planes.bit_depth,
158            self.rate,
159            self.quality.current,
160        )?;
161        self.fresh = true;
162        Ok(())
163    }
164
165    /// Encode the next frame at the quantizer the quality index `crf` selects, leaving the
166    /// session's own quality for the frame after: the cleanup of a still screen, at a constant
167    /// rate factor, where x265 takes it as the frame's forced quantizer. A constant-rate session
168    /// codes the frame under its rate control (`holds_quantizer`). kvazaar takes no quantizer
169    /// but the one it opens at, and a re-open for the frame would need another to come back,
170    /// each a key frame, so it codes the frame at its own.
171    pub fn hold_quantizer(&mut self, crf: u32) {
172        if !self.rate.cbr {
173            self.held = Some(Codec::H265.quantizer(crf as i32));
174        }
175    }
176
177    /// Re-open the encoder when a rate or frame-rate setting changed: neither library's rate
178    /// control takes a new target while it runs.
179    pub fn reconfigure_rate(&mut self, settings: &RustCaptureSettings) -> Result<(), String> {
180        let Some(rate) = self.rate.changed(settings) else {
181            return Ok(());
182        };
183        self.rate = rate;
184        self.reopen()
185    }
186
187    /// Encode one packed host frame at the quality index `crf`, as a key frame when `force_idr`.
188    pub fn encode_host(
189        &mut self,
190        pixels: &[u8],
191        stride: usize,
192        rgba: bool,
193        frame_number: u64,
194        crf: u32,
195        force_idr: bool,
196    ) -> Result<Vec<u8>, String> {
197        if !self.rate.cbr
198            && self
199                .quality
200                .update(Codec::H265.quantizer(crf as i32))
201                .is_some()
202        {
203            self.reopen()?;
204        }
205        let held = self.held.take();
206        if force_idr && !self.fresh && !Backend::KEY_ON_REQUEST {
207            self.reopen()?;
208        }
209        let key = force_idr || self.fresh;
210        let full_range = self.is_full_range();
211        let pts = self.next_pts;
212        let coded = self.backend.encode(
213            &mut self.planes,
214            pixels,
215            stride,
216            rgba,
217            full_range,
218            self.threads as usize,
219            pts,
220            key,
221            held,
222        )?;
223        self.next_pts += 1;
224        self.fresh = false;
225        self.pending.push(pts, frame_number as u16);
226        let Some(coded) = coded else {
227            return Ok(Vec::new());
228        };
229        if held.is_none() {
230            self.last_quality = Some(coded.qp);
231            self.last_bytes = Some(coded.bytes.len());
232        }
233        let id = self.pending.take(coded.pts).unwrap_or(frame_number as u16);
234        let mut output = Vec::with_capacity(VIDEO_HEADER_LEN + coded.bytes.len());
235        if !self.omit_headers {
236            push_video_header(
237                &mut output,
238                Codec::H265,
239                h265_frame_type(&coded.bytes),
240                id,
241                0,
242                self.planes.width as u16,
243                self.planes.height as u16,
244                Reference::Untracked,
245            );
246        }
247        output.extend_from_slice(&coded.bytes);
248        Ok(output)
249    }
250}
251
252/// The CBR quantizer bounds in HEVC's domain, zero where the settings name none.
253fn qp_bounds(rate: RateSettings) -> (u32, u32) {
254    (
255        Codec::H265.quantizer_bound(rate.min_qp),
256        Codec::H265.quantizer_bound(rate.max_qp),
257    )
258}
259
260#[cfg(not(feature = "gpl"))]
261use kvazaar::Backend;
262#[cfg(feature = "gpl")]
263use x265::Backend;
264
265#[cfg(feature = "gpl")]
266mod x265 {
267    //! x265 through its versioned API table, configured by name so the session reads no field
268    //! of a parameter structure that moves between releases.
269    use std::ffi::c_int;
270    use std::ptr;
271
272    use codec_sys::x265::*;
273
274    use super::*;
275
276    pub struct Backend {
277        api: *const x265_api,
278        params: *mut x265_param,
279        encoder: *mut x265_encoder,
280    }
281
282    impl Drop for Backend {
283        fn drop(&mut self) {
284            unsafe {
285                let api = &*self.api;
286                if !self.encoder.is_null() {
287                    (api.encoder_close.unwrap())(self.encoder);
288                }
289                if !self.params.is_null() {
290                    (api.param_free.unwrap())(self.params);
291                }
292            }
293        }
294    }
295
296    impl Backend {
297        pub const LIBRARY: &'static str = "x265";
298        pub const FULLCOLOR: bool = true;
299
300        /// Whether the linked x265 carries a 10-bit encoder beside its 8-bit one, which is
301        /// a property of how the library was built.
302        pub fn ten_bit() -> bool {
303            !unsafe { x265_api_get(10) }.is_null()
304        }
305
306        pub const KEY_ON_REQUEST: bool = true;
307        pub const HOLDS_QUANTIZER: bool = true;
308
309        /// The frame rate the library was configured at, as `(num, den)`.
310        #[cfg(test)]
311        pub fn frame_rate(&self) -> (u32, u32) {
312            unsafe { ((*self.params).fpsNum, (*self.params).fpsDenom) }
313        }
314
315        fn set(&self, name: &str, value: &str) -> Result<(), String> {
316            let (n, v) = (CString::new(name).unwrap(), CString::new(value).unwrap());
317            match unsafe { ((*self.api).param_parse.unwrap())(self.params, n.as_ptr(), v.as_ptr()) }
318            {
319                0 => Ok(()),
320                X265_PARAM_BAD_NAME => Err(format!("x265 knows no option {name}")),
321                _ => Err(format!("x265 refused {name}={value}")),
322            }
323        }
324
325        /// The rate-control options of `rate` at quantizer `q`: a constant rate held to its
326        /// VBV with the quantizer capped at 51, since the library's default ceiling admits
327        /// out-of-spec quantizers that only force skips on a VBV underflow; else a constant
328        /// rate factor.
329        fn rate_options(rate: RateSettings, q: u32) -> Vec<(&'static str, String)> {
330            let mut options = Vec::new();
331            if rate.cbr {
332                let kbps = (rate.bps() / 1000).max(1);
333                let vbv_kbit = (rate.vbv() / 1000).max(1);
334                options.push(("bitrate", kbps.to_string()));
335                options.push(("vbv-maxrate", kbps.to_string()));
336                options.push(("vbv-bufsize", vbv_kbit.to_string()));
337                if rate.vbv() > 1000 {
338                    options.push(("vbv-init", vbv_kbit.to_string()));
339                }
340                options.push(("strict-cbr", "1".into()));
341                let (lo, hi) = qp_bounds(rate);
342                if lo > 0 {
343                    options.push(("qpmin", lo.to_string()));
344                }
345                options.push(("qpmax", if hi > 0 { hi } else { 51 }.to_string()));
346            } else {
347                options.push(("crf", q.to_string()));
348            }
349            options
350        }
351
352        pub fn open(
353            width: usize,
354            height: usize,
355            threads: i32,
356            i444: bool,
357            bit_depth: u32,
358            rate: RateSettings,
359            q: u32,
360        ) -> Result<Self, String> {
361            let api = unsafe { x265_api_get(bit_depth as c_int) };
362            if api.is_null() {
363                return Err(format!("x265 carries no {bit_depth}-bit encoder"));
364            }
365            let params = unsafe { ((*api).param_alloc.unwrap())() };
366            if params.is_null() {
367                return Err("x265 allocated no parameters".into());
368            }
369            let mut me = Self {
370                api,
371                params,
372                encoder: ptr::null_mut(),
373            };
374            let (preset, tune) = (c"ultrafast", c"zerolatency");
375            if unsafe {
376                ((*api).param_default_preset.unwrap())(params, preset.as_ptr(), tune.as_ptr())
377            } < 0
378            {
379                return Err("x265 refused the ultrafast zerolatency preset".into());
380            }
381            if width < 16 || height < 16 {
382                return Err(format!(
383                    "x265 encodes no picture as small as {width}x{height}"
384                ));
385            }
386            let mut options: Vec<(&str, String)> = vec![
387                ("log-level", "none".into()),
388                ("input-res", format!("{width}x{height}")),
389                ("input-csp", if i444 { "i444" } else { "i420" }.into()),
390                ("fps", format!("{}/{}", rate.fps.num, rate.fps.den)),
391                ("range", if i444 { "full" } else { "limited" }.into()),
392                ("colorprim", "bt709".into()),
393                ("transfer", "bt709".into()),
394                ("colormatrix", "bt709".into()),
395                ("keyint", "-1".into()),
396                ("scenecut", "0".into()),
397                ("repeat-headers", "1".into()),
398                ("annexb", "1".into()),
399                ("aud", "0".into()),
400                ("rc-lookahead", "0".into()),
401                ("bframes", "0".into()),
402                ("frame-threads", "1".into()),
403                ("pools", threads.to_string()),
404                ("wpp", "1".into()),
405            ];
406            if width < 32 || height < 32 {
407                options.push(("ctu", "16".into()));
408            } else if width < 64 || height < 64 {
409                options.push(("ctu", "32".into()));
410            }
411            options.extend(Self::rate_options(rate, q));
412            for (name, value) in &options {
413                me.set(name, value)?;
414            }
415            let profile = match (i444, bit_depth) {
416                (true, 10) => Some(c"main444-10"),
417                (true, _) => Some(c"main444-8"),
418                (false, 10) => Some(c"main10"),
419                (false, _) => None,
420            };
421            if let Some(profile) = profile
422                && unsafe { ((*api).param_apply_profile.unwrap())(params, profile.as_ptr()) } < 0
423            {
424                return Err(format!(
425                    "x265 refused the {} profile",
426                    profile.to_string_lossy()
427                ));
428            }
429            me.encoder = unsafe { ((*api).encoder_open.unwrap())(params) };
430            if me.encoder.is_null() {
431                return Err("x265 refused the session".into());
432            }
433            Ok(me)
434        }
435
436        #[allow(clippy::too_many_arguments)]
437        pub fn encode(
438            &mut self,
439            planes: &mut Planes,
440            pixels: &[u8],
441            stride: usize,
442            rgba: bool,
443            full_range: bool,
444            threads: usize,
445            pts: u64,
446            key: bool,
447            held: Option<u32>,
448        ) -> Result<Option<Coded>, String> {
449            planes.convert(pixels, stride, rgba, full_range, false, threads)?;
450            let api = unsafe { &*self.api };
451            let mut pic: x265_picture = unsafe { std::mem::zeroed() };
452            let mut out: [x265_picture; 2] = unsafe { std::mem::zeroed() };
453            unsafe {
454                (api.picture_init.unwrap())(self.params, &mut pic);
455                for o in &mut out {
456                    (api.picture_init.unwrap())(self.params, o);
457                }
458            }
459            // Written per index: x265 4 declares a fourth plane for alpha, x265 3 does not.
460            let [y, u, v] = planes.pointers();
461            pic.planes[0] = y.cast();
462            pic.planes[1] = u.cast();
463            pic.planes[2] = v.cast();
464            let bytes = planes.sample_bytes();
465            pic.stride[0] = (planes.width * bytes) as c_int;
466            pic.stride[1] = (planes.chroma_width() * bytes) as c_int;
467            pic.stride[2] = (planes.chroma_width() * bytes) as c_int;
468            pic.pts = pts as i64;
469            pic.bitDepth = planes.bit_depth as c_int;
470            pic.sliceType = if key {
471                X265_TYPE_IDR as c_int
472            } else {
473                X265_TYPE_AUTO as c_int
474            };
475            pic.forceqp = held.map_or(0, |q| q as c_int + 1);
476            let mut nals: *mut x265_nal = ptr::null_mut();
477            let mut count: u32 = 0;
478            let ret = unsafe {
479                encoder_encode(
480                    api,
481                    self.encoder,
482                    &mut nals,
483                    &mut count,
484                    &mut pic,
485                    out.as_mut_ptr(),
486                )
487            };
488            if ret < 0 {
489                return Err("x265 refused the frame".into());
490            }
491            if count == 0 {
492                return Ok(None);
493            }
494            let mut bytes = Vec::new();
495            for nal in unsafe { std::slice::from_raw_parts(nals, count as usize) } {
496                bytes.extend_from_slice(unsafe {
497                    std::slice::from_raw_parts(nal.payload, nal.sizeBytes as usize)
498                });
499            }
500            Ok(Some(Coded {
501                bytes,
502                pts: out[0].pts.max(0) as u64,
503                qp: out[0].frameData.qp.round().clamp(0.0, 51.0) as u32,
504            }))
505        }
506    }
507}
508
509#[cfg(not(feature = "gpl"))]
510mod kvazaar {
511    //! kvazaar through its API table. The library takes no per-frame key-frame request and no
512    //! live rate change, so both re-open the encoder.
513    use std::ptr;
514
515    use codec_sys::kvazaar::*;
516
517    use super::*;
518
519    pub struct Backend {
520        api: *const kvz_api,
521        config: *mut kvz_config,
522        encoder: *mut kvz_encoder,
523    }
524
525    impl Drop for Backend {
526        fn drop(&mut self) {
527            unsafe {
528                let api = &*self.api;
529                if !self.encoder.is_null() {
530                    (api.encoder_close.unwrap())(self.encoder);
531                }
532                if !self.config.is_null() {
533                    (api.config_destroy.unwrap())(self.config);
534                }
535            }
536        }
537    }
538
539    impl Backend {
540        pub const LIBRARY: &'static str = "kvazaar";
541        pub const FULLCOLOR: bool = false;
542
543        /// kvazaar's sample depth is fixed when it is built, and the build here is 8-bit.
544        pub fn ten_bit() -> bool {
545            false
546        }
547        pub const KEY_ON_REQUEST: bool = false;
548        pub const HOLDS_QUANTIZER: bool = false;
549
550        /// The frame rate the library was configured at, as `(num, den)`.
551        #[cfg(test)]
552        pub fn frame_rate(&self) -> (u32, u32) {
553            unsafe {
554                (
555                    (*self.config).framerate_num as u32,
556                    (*self.config).framerate_denom as u32,
557                )
558            }
559        }
560
561        fn set(&self, name: &str, value: &str) -> Result<(), String> {
562            let (n, v) = (CString::new(name).unwrap(), CString::new(value).unwrap());
563            if unsafe { ((*self.api).config_parse.unwrap())(self.config, n.as_ptr(), v.as_ptr()) }
564                == 0
565            {
566                return Err(format!("kvazaar refused {name}={value}"));
567            }
568            Ok(())
569        }
570
571        pub fn open(
572            width: usize,
573            height: usize,
574            threads: i32,
575            i444: bool,
576            bit_depth: u32,
577            rate: RateSettings,
578            q: u32,
579        ) -> Result<Self, String> {
580            let _ = (i444, bit_depth);
581            if !width.is_multiple_of(8) || !height.is_multiple_of(8) {
582                return Err(format!(
583                    "kvazaar takes only pictures a multiple of eight, not {width}x{height}"
584                ));
585            }
586            let api = unsafe { kvz_api_get(8) };
587            if api.is_null() {
588                return Err("kvazaar carries no 8-bit encoder".into());
589            }
590            let config = unsafe { ((*api).config_alloc.unwrap())() };
591            if config.is_null() {
592                return Err("kvazaar allocated no configuration".into());
593            }
594            let me = Self {
595                api,
596                config,
597                encoder: ptr::null_mut(),
598            };
599            unsafe {
600                if ((*api).config_init.unwrap())(config) == 0 {
601                    return Err("kvazaar refused to initialize its configuration".into());
602                }
603                let cfg = &mut *config;
604                cfg.width = width as i32;
605                cfg.height = height as i32;
606                cfg.framerate_num = rate.fps.num as i32;
607                cfg.framerate_denom = rate.fps.den as i32;
608                cfg.vui.fullrange = 0;
609                cfg.vui.colorprim = 1;
610                cfg.vui.transfer = 1;
611                cfg.vui.colormatrix = 1;
612            }
613            let mut options: Vec<(&str, String)> = vec![
614                ("preset", "ultrafast".into()),
615                ("gop", "0".into()),
616                ("period", "0".into()),
617                ("vps-period", "0".into()),
618                ("threads", threads.to_string()),
619                ("owf", "0".into()),
620                ("wpp", "1".into()),
621            ];
622            if rate.cbr {
623                options.push(("bitrate", rate.bps().to_string()));
624                options.push(("rc-algorithm", "oba".into()));
625            } else {
626                options.push(("qp", q.to_string()));
627            }
628            for (name, value) in &options {
629                me.set(name, value)?;
630            }
631            let mut me = me;
632            me.encoder = unsafe { ((*api).encoder_open.unwrap())(config) };
633            if me.encoder.is_null() {
634                return Err("kvazaar refused the session".into());
635            }
636            Ok(me)
637        }
638
639        #[allow(clippy::too_many_arguments)]
640        pub fn encode(
641            &mut self,
642            planes: &mut Planes,
643            pixels: &[u8],
644            stride: usize,
645            rgba: bool,
646            full_range: bool,
647            threads: usize,
648            pts: u64,
649            _key: bool,
650            _held: Option<u32>,
651        ) -> Result<Option<Coded>, String> {
652            let api = unsafe { &*self.api };
653            let pic =
654                unsafe { (api.picture_alloc.unwrap())(planes.width as i32, planes.height as i32) };
655            if pic.is_null() {
656                return Err("kvazaar allocated no picture".into());
657            }
658            let result = unsafe {
659                let p = &mut *pic;
660                let luma_stride = p.stride as usize;
661                let chroma_stride = luma_stride / 2;
662                let (w, h) = (planes.width, planes.height);
663                let y = std::slice::from_raw_parts_mut(p.y, luma_stride * h);
664                let u = std::slice::from_raw_parts_mut(p.u, chroma_stride * h.div_ceil(2));
665                let v = std::slice::from_raw_parts_mut(p.v, chroma_stride * h.div_ceil(2));
666                p.pts = pts as i64;
667                super::super::session::convert_into(
668                    pixels,
669                    stride,
670                    w,
671                    h,
672                    rgba,
673                    false,
674                    full_range,
675                    false,
676                    threads,
677                    y,
678                    u,
679                    v,
680                    (luma_stride, chroma_stride),
681                )
682            };
683            if let Err(e) = result {
684                unsafe { (api.picture_free.unwrap())(pic) };
685                return Err(e);
686            }
687            let mut data: *mut kvz_data_chunk = ptr::null_mut();
688            let mut len: u32 = 0;
689            let mut recon: *mut kvz_picture = ptr::null_mut();
690            let mut info: kvz_frame_info = unsafe { std::mem::zeroed() };
691            let ok = unsafe {
692                (api.encoder_encode.unwrap())(
693                    self.encoder,
694                    pic,
695                    &mut data,
696                    &mut len,
697                    &mut recon,
698                    ptr::null_mut(),
699                    &mut info,
700                )
701            };
702            let mut coded = None;
703            if ok != 0 && !data.is_null() {
704                let mut bytes = Vec::with_capacity(len as usize);
705                let mut chunk = data;
706                while !chunk.is_null() {
707                    let c = unsafe { &*chunk };
708                    bytes.extend_from_slice(&c.data[..c.len as usize]);
709                    chunk = c.next;
710                }
711                let out_pts = if recon.is_null() {
712                    pts
713                } else {
714                    unsafe { (*recon).pts.max(0) as u64 }
715                };
716                coded = Some(Coded {
717                    bytes,
718                    pts: out_pts,
719                    qp: info.qp.clamp(0, 51) as u32,
720                });
721            }
722            unsafe {
723                (api.picture_free.unwrap())(pic);
724                (api.picture_free.unwrap())(recon);
725                (api.chunk_free.unwrap())(data);
726            }
727            if ok == 0 {
728                return Err("kvazaar refused the frame".into());
729            }
730            Ok(coded)
731        }
732    }
733}
734
735#[cfg(test)]
736mod tests {
737    use super::*;
738
739    /// A session's first frame is a key frame, a forced one mid-stream is too, and the ones
740    /// between are deltas that name no reference.
741    #[test]
742    fn key_frames_come_on_request_and_deltas_name_no_reference() {
743        let settings = RustCaptureSettings {
744            width: 64,
745            height: 48,
746            target_fps: 30.0,
747            codec: Codec::H265,
748            video_crf: 25,
749            ..Default::default()
750        };
751        let mut enc = HevcEncoder::new(&settings, false).expect("session");
752        assert_eq!(enc.library(), Backend::LIBRARY);
753        let bgra = vec![128u8; 64 * 48 * 4];
754        let mut kinds = Vec::new();
755        for t in 0..4u64 {
756            let out = enc
757                .encode_host(&bgra, 64 * 4, false, t, 25, t == 2)
758                .expect("encode");
759            kinds.push(super::super::codec::parse_video_type(out[1]).map(|(_, k)| k));
760            assert_eq!(enc.last_reference(), Reference::Untracked);
761        }
762        assert_eq!(
763            kinds,
764            [
765                Some(super::super::codec::FRAME_KEY),
766                Some(super::super::codec::FRAME_DELTA),
767                Some(super::super::codec::FRAME_KEY),
768                Some(super::super::codec::FRAME_DELTA)
769            ]
770        );
771        assert!(!enc.invalidate_reference(1));
772    }
773
774    /// The library is configured at the capture's rate as the fraction it names.
775    #[test]
776    fn the_frame_rate_reaches_the_library_as_its_fraction() {
777        for (num, den) in [(60000u32, 1001u32), (120000, 1001), (144000, 1001), (60, 1)] {
778            let settings = RustCaptureSettings {
779                width: 64,
780                height: 48,
781                target_fps: num as f64 / den as f64,
782                codec: Codec::H265,
783                video_cbr_mode: true,
784                video_bitrate_kbps: 2000,
785                ..Default::default()
786            };
787            let enc = HevcEncoder::new(&settings, false).expect("session");
788            assert_eq!(enc.backend.frame_rate(), (num, den), "{}", Backend::LIBRARY);
789        }
790    }
791}