]> git.nihav.org Git - nihav.git/commitdiff
RPZA (very simple) encoder
authorKostya Shishkov <kostya.shishkov@gmail.com>
Thu, 30 Apr 2026 19:10:40 +0000 (21:10 +0200)
committerKostya Shishkov <kostya.shishkov@gmail.com>
Thu, 30 Apr 2026 19:10:40 +0000 (21:10 +0200)
nihav-qt/Cargo.toml
nihav-qt/src/codecs/mod.rs
nihav-qt/src/codecs/rpzaenc.rs [new file with mode: 0644]

index 25ee0da32c6953cde8924c89dcef7bfafb77b13f..af9a82bdd25ced38f47075a8e3f1623b4896bbed 100644 (file)
@@ -45,8 +45,9 @@ demuxer_warhol = ["demuxers"]
 all_encoders = ["all_video_encoders", "all_audio_encoders"]
 encoders = []
 
-all_video_encoders = ["encoder_rawvid", "encoder_rle"]
+all_video_encoders = ["encoder_rawvid", "encoder_rle", "encoder_rpza"]
 encoder_rawvid = ["encoders"]
 encoder_rle = ["encoders"]
+encoder_rpza = ["encoders"]
 
 all_audio_encoders = []
index bbec675f0953758a4f8a6a429af52aa4cf24ce8e..739ffe8ea452080bb128fa2208d933dc57d1286b 100644 (file)
@@ -115,6 +115,8 @@ pub fn qt_register_all_decoders(rd: &mut RegisteredDecoders) {
 mod rawvidenc;
 #[cfg(feature="encoder_rle")]
 mod rleenc;
+#[cfg(feature="encoder_rpza")]
+mod rpzaenc;
 
 #[cfg(feature="encoders")]
 const QT_ENCODERS: &[EncoderInfo] = &[
@@ -124,6 +126,8 @@ const QT_ENCODERS: &[EncoderInfo] = &[
     EncoderInfo { name: "qt-yuv4", get_encoder: rawvidenc::get_encoder_yuv4 },
 #[cfg(feature="encoder_rle")]
     EncoderInfo { name: "qt-rle", get_encoder: rleenc::get_encoder },
+#[cfg(feature="encoder_rpza")]
+    EncoderInfo { name: "apple-video", get_encoder: rpzaenc::get_encoder },
 ];
 
 /// Registers all available encoders provided by this crate.
diff --git a/nihav-qt/src/codecs/rpzaenc.rs b/nihav-qt/src/codecs/rpzaenc.rs
new file mode 100644 (file)
index 0000000..0f42eda
--- /dev/null
@@ -0,0 +1,638 @@
+use nihav_core::codecs::*;
+use nihav_core::io::byteio::*;
+use std::ops::{Add, AddAssign, Sub, Div, DivAssign};
+
+const RGB555BE_FORMAT: NAPixelFormaton = NAPixelFormaton::make_rgb16_fmt(5, 5, 5, true, true);
+
+#[derive(Clone,Copy,Debug,Default,PartialEq)]
+struct Pixel([u16; 3]);
+
+impl Pixel {
+    fn interpolate(&self, p1: Pixel) -> Pixel {
+        let mut ret = *self;
+        for (dst, &comp1) in ret.0.iter_mut().zip(p1.0.iter()) {
+            *dst = (*dst * 21 + comp1 * 11) >> 5;
+        }
+        ret
+    }
+    fn min(&self, p1: Pixel) -> Pixel {
+        let mut ret = *self;
+        for (c0, &c1) in ret.0.iter_mut().zip(p1.0.iter()) {
+            *c0 = (*c0).min(c1);
+        }
+        ret
+    }
+    fn max(&self, p1: Pixel) -> Pixel {
+        let mut ret = *self;
+        for (c0, &c1) in ret.0.iter_mut().zip(p1.0.iter()) {
+            *c0 = (*c0).max(c1);
+        }
+        ret
+    }
+    fn dist(&self, other: &Pixel) -> u32 {
+        self.0.iter().zip(other.0.iter()).fold(0u32,
+            |acc, (&a, &b)| acc + u32::from(a.abs_diff(b)) * u32::from(a.abs_diff(b)))
+    }
+}
+
+impl From<u16> for Pixel {
+    fn from(pix: u16) -> Pixel {
+        Pixel([(pix >> 10) & 0x1F, (pix >> 5) & 0x1F, pix & 0x1F])
+    }
+}
+
+impl From<Pixel> for u16 {
+    fn from(pix: Pixel) -> u16 {
+        (pix.0[0] << 10) | (pix.0[1] << 5) | pix.0[2]
+    }
+}
+
+impl Sub for Pixel {
+    type Output = Self;
+    fn sub(self, rhs: Pixel) -> Self::Output {
+        let mut ret = self;
+        for (c0, &c1) in ret.0.iter_mut().zip(rhs.0.iter()) {
+            *c0 = c0.abs_diff(c1);
+        }
+        ret
+    }
+}
+
+impl Add for Pixel {
+    type Output = Self;
+    fn add(self, rhs: Pixel) -> Self::Output {
+        let mut ret = self;
+        for (c0, &c1) in ret.0.iter_mut().zip(rhs.0.iter()) {
+            *c0 += c1;
+        }
+        ret
+    }
+}
+
+impl AddAssign for Pixel {
+    fn add_assign(&mut self, other: Pixel) {
+        for (dst, &src) in self.0.iter_mut().zip(other.0.iter()) {
+            *dst += src;
+        }
+    }
+}
+
+impl Div<u8> for Pixel {
+    type Output = Self;
+    fn div(self, val: u8) -> Pixel {
+        let mut ret = self;
+        for comp in ret.0.iter_mut() {
+            *comp /= u16::from(val);
+        }
+        ret
+    }
+}
+
+impl DivAssign<u8> for Pixel {
+    fn div_assign(&mut self, val: u8) {
+        for comp in self.0.iter_mut() {
+            *comp /= u16::from(val);
+        }
+    }
+}
+
+impl std::fmt::Display for Pixel {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
+        write!(f, "{:02X},{:02X},{:02X}", self.0[0], self.0[1], self.0[2])
+    }
+}
+
+fn interpolate_u16(c0: u16, c1: u16) -> u16 {
+    Pixel::from(c0).interpolate(c1.into()).into()
+}
+
+#[derive(Clone,Copy,Debug,PartialEq)]
+enum EncState {
+    None,
+    Skip(usize),
+    Fill(u16, usize),
+    Four(u16, u16, [u8; 4]),
+    Raw,
+}
+
+impl EncState {
+    fn write(&self, dst: &mut GrowableMemoryWriter<'_>, patterns: &mut Vec<[u8; 4]>) {
+        match *self {
+            EncState::None => {},
+            EncState::Skip(skip) => {
+                for _ in 0..(skip / 32) {
+                    let _ = dst.write_byte(0x80 | 0x1F);
+                }
+                if (skip & 0x1F) > 0 {
+                    let _ = dst.write_byte(0x80 | ((skip & 0x1F) as u8 - 1));
+                }
+            },
+            EncState::Fill(clr, nblocks) => {
+                for _ in 0..(nblocks / 32) {
+                    let _ = dst.write_byte(0xA0 | 0x1F);
+                    let _ = dst.write_u16be(clr);
+                }
+                if (nblocks & 0x1F) > 0 {
+                    let _ = dst.write_byte(0xA0 | ((nblocks & 0x1F) as u8 - 1));
+                    let _ = dst.write_u16be(clr);
+                }
+            },
+            EncState::Four(clr0, clr1, _) => {
+                if patterns.len() == 1 {
+                    let _ = dst.write_u16be(clr0);
+                    let _ = dst.write_u16be(clr1 | 0x8000);
+                    let _ = dst.write_buf(&patterns[0]);
+                } else {
+                    for pats in patterns.chunks(32) {
+                        let _ = dst.write_byte(0xC0 | (pats.len() as u8 - 1));
+                        let _ = dst.write_u16be(clr0);
+                        let _ = dst.write_u16be(clr1);
+                        for pat in pats.iter() {
+                            let _ = dst.write_buf(pat);
+                        }
+                    }
+                }
+                patterns.clear()
+            },
+            EncState::Raw => {},
+        }
+    }
+}
+
+#[derive(Clone,Copy,Debug,Default,PartialEq)]
+enum Strategy {
+    Lossless,
+    #[default]
+    Normal,
+    Refined,
+}
+
+fn find_nearest(cc: &[Pixel; 4], pix: &Pixel) -> (usize, u32) {
+    let mut best_idx = 0;
+    let mut best_dist = u32::MAX;
+    for (i, cand) in cc.iter().enumerate() {
+        let dist = cand.dist(pix);
+        if dist == 0 {
+            return (i, 0);
+        }
+        if dist < best_dist {
+            best_dist = dist;
+            best_idx  = i;
+        }
+    }
+    (best_idx, best_dist)
+}
+
+struct RoadPizzaEncoder {
+    stream:     Option<NAStreamRef>,
+    pkt:        Option<NAPacket>,
+    key_int:    u8,
+    frm_no:     u8,
+    last_frm:   Vec<u16>,
+    vinfo:      NAVideoInfo,
+    mode:       Strategy,
+}
+
+impl RoadPizzaEncoder {
+    fn new() -> Self {
+        Self {
+            stream:     None,
+            pkt:        None,
+            key_int:    25,
+            frm_no:     254,
+            last_frm:   Vec::new(),
+            vinfo:      NAVideoInfo::new(0, 0, false, RGB555BE_FORMAT),
+            mode:       Strategy::default(),
+        }
+    }
+    fn paint_4clr(dst: &mut [u16], stride: usize, x: usize, c3: u16, c0: u16, pat: [u8; 4]) {
+        let clrs = [c0, interpolate_u16(c0, c3), interpolate_u16(c3, c0), c3];
+        for (line, &pp) in dst.chunks_exact_mut(stride).zip(pat.iter()) {
+            let mut idx = usize::from(pp);
+            for pix in line[x..][..4].iter_mut() {
+                *pix = clrs[(idx >> 6) & 3];
+                idx <<= 2;
+            }
+        }
+    }
+    fn decide_mode(mode: Strategy, _prev_state: EncState, skip_dist: u32, pixels: &[Pixel; 16]) -> EncState {
+        let skip_thr = if mode == Strategy::Lossless { 0 } else { 32 };
+        if skip_dist.saturating_sub(skip_thr) == 0 {
+            return EncState::Skip(1);
+        }
+        match mode {
+            Strategy::Lossless => {
+                if pixels == &[pixels[0]; 16] {
+                    EncState::Fill(pixels[0].into(), 1)
+                } else {
+                    EncState::Raw
+                }
+            },
+            Strategy::Normal | Strategy::Refined => {
+                let mut min_pix = Pixel([0x1F; 3]);
+                let mut max_pix = Pixel([0x00; 3]);
+                for pix in pixels.iter() {
+                    min_pix = min_pix.min(*pix);
+                    max_pix = max_pix.max(*pix);
+                }
+                let range = max_pix - min_pix;
+                let ridx = if range.0[1] >= range.0[0] && range.0[1] >= range.0[2] {
+                        1
+                    } else if range.0[0] >= range.0[1] && range.0[0] >= range.0[2] {
+                        0
+                    } else if range.0[2] >= range.0[0] && range.0[2] >= range.0[1] {
+                        2
+                    } else {
+                        1
+                    };
+                if range.0[ridx] < 2 {
+                    let fill: Pixel = pixels.iter().fold(Pixel::default(), |acc, &p| acc + p) / 16;
+                    return EncState::Fill(u16::from(fill), 1);
+                }
+                let mut cnt = [0u8; 2];
+                let mut sum = [Pixel::default(); 2];
+                for pix in pixels.iter() {
+                    if pix.0[ridx].abs_diff(min_pix.0[ridx]) < 2 {
+                        cnt[0] += 1;
+                        sum[0] += *pix;
+                    }
+                    if pix.0[ridx].abs_diff(max_pix.0[ridx]) < 2 {
+                        cnt[1] += 1;
+                        sum[1] += *pix;
+                    }
+                }
+                sum[0] /= cnt[0];
+                sum[1] /= cnt[1];
+
+                let mut centroids = [sum[0], sum[0].interpolate(sum[1]), sum[1].interpolate(sum[0]), sum[1]];
+                if mode == Strategy::Refined {
+                    let mut cur_dist = pixels.iter().fold(0u32, |acc, pix| acc + find_nearest(&centroids, pix).1);
+                    loop {
+                        let mut sum = [Pixel::default(); 4];
+                        let mut cnt = [0; 4];
+                        for pix in pixels.iter() {
+                            let (idx, _dist) = find_nearest(&centroids, pix);
+                            sum[idx] += *pix;
+                            cnt[idx] += 1;
+                        }
+                        for (s, &cc) in sum.iter_mut().zip(cnt.iter()) {
+                            *s /= cc.max(1);
+                        }
+                        let new_dist = pixels.iter().fold(0u32, |acc, pix| acc + find_nearest(&sum, pix).1);
+                        if new_dist < cur_dist {
+                            centroids[0] = sum[0];
+                            centroids[3] = sum[3];
+                            centroids[1] = centroids[0].interpolate(centroids[3]);
+                            centroids[2] = centroids[3].interpolate(centroids[0]);
+                            if new_dist >= cur_dist - cur_dist / 8 {
+                                break;
+                            }
+                            cur_dist = new_dist;
+                        } else {
+                            break;
+                        }
+                    }
+                }
+                if u16::from(centroids[0]) > u16::from(centroids[3]) {
+                    centroids.swap(0, 3);
+                    centroids.swap(1, 2);
+                }
+                let mut pat = [0; 4];
+                for (pp, row) in pat.iter_mut().zip(pixels.chunks_exact(4)) {
+                    for pix in row.iter() {
+                        let (idx, _) = find_nearest(&centroids, pix);
+                        *pp <<= 2;
+                        *pp |= idx as u8;
+                    }
+                }
+                EncState::Four(centroids[3].into(), centroids[0].into(), pat)
+            },
+        }
+    }
+    fn encode(&mut self, dbuf: &mut Vec<u8>, src: &[u16], sstride: usize, force_intra: bool) -> bool {
+        let mut is_intra = true;
+        let mut pixels = [Pixel::default(); 16];
+
+        let mut state = EncState::None;
+        let mut patterns: Vec<[u8; 4]> = Vec::new();
+        let pstride = self.vinfo.width;
+        let mut gw = GrowableMemoryWriter::new_write(dbuf);
+        let _ = gw.seek(SeekFrom::End(0));
+        for (sstrip, pstrip) in src.chunks_exact(sstride * 4)
+                .zip(self.last_frm.chunks_exact_mut(pstride * 4)) {
+            for x in (0..self.vinfo.width).step_by(4) {
+                for (row, line) in pixels.chunks_exact_mut(4).zip(sstrip.chunks_exact(sstride)) {
+                    for (dst, &src) in row.iter_mut().zip(line[x..][..4].iter()) {
+                        *dst = Pixel::from(src);
+                    }
+                }
+                let mut skip_dist = if force_intra { u32::MAX } else { 0 };
+                if !force_intra {
+                    if self.mode == Strategy::Lossless {
+                        for (line0, line1) in sstrip.chunks_exact(sstride)
+                                .zip(pstrip.chunks_exact(pstride)) {
+                            if line0[x..][..4] != line1[x..][..4] {
+                                skip_dist = u32::MAX;
+                                break;
+                            }
+                        }
+                    } else {
+                        for (line, prow) in pstrip.chunks_exact(pstride).zip(pixels.chunks_exact(4)) {
+                            for (&ppix, npix) in line[x..][..4].iter().zip(prow.iter()) {
+                                skip_dist += npix.dist(&ppix.into());
+                            }
+                        }
+                    }
+                }
+
+                let new_state = Self::decide_mode(self.mode, state, skip_dist, &pixels);
+                state = match (state, new_state) {
+                        (_, EncState::None) => unreachable!(),
+                        (EncState::Skip(skip), EncState::Skip(_)) => {
+                            EncState::Skip(skip + 1)
+                        },
+                        (_, EncState::Skip(_)) => {
+                            state.write(&mut gw, &mut patterns);
+                            is_intra = false;
+                            new_state
+                        },
+                        (EncState::Fill(old_clr, old_cnt), EncState::Fill(new_clr, _)) => {
+                            for pline in pstrip.chunks_exact_mut(pstride) {
+                                for pix in pline[x..][..4].iter_mut() {
+                                    *pix = new_clr;
+                                }
+                            }
+                            if old_clr == new_clr {
+                                EncState::Fill(old_clr, old_cnt + 1)
+                            } else {
+                                state.write(&mut gw, &mut patterns);
+                                new_state
+                            }
+                        },
+                        (_, EncState::Fill(fill_clr, _)) => {
+                            for pline in pstrip.chunks_exact_mut(pstride) {
+                                for pix in pline[x..][..4].iter_mut() {
+                                    *pix = fill_clr;
+                                }
+                            }
+                            state.write(&mut gw, &mut patterns);
+                            new_state
+                        },
+                        (EncState::Four(oc0, oc3, _), EncState::Four(c0, c3, cpat)) => {
+                            Self::paint_4clr(pstrip, pstride, x, c0, c3, cpat);
+                            if oc0 != c0 || oc3 != c3 {
+                                state.write(&mut gw, &mut patterns);
+                            }
+                            patterns.push(cpat);
+                            new_state
+                        },
+                        (_, EncState::Four(c0, c3, cpat)) => {
+                            Self::paint_4clr(pstrip, pstride, x, c0, c3, cpat);
+                            state.write(&mut gw, &mut patterns);
+                            patterns.push(cpat);
+                            new_state
+                        },
+                        (_, EncState::Raw) => {
+                            for (line, pline) in sstrip.chunks_exact(sstride)
+                                    .zip(pstrip.chunks_exact_mut(pstride)) {
+                                pline[x..][..4].copy_from_slice(&line[x..][..4]);
+                                let _ = gw.write_u16be_arr(&line[x..][..4]);
+                            }
+                            state.write(&mut gw, &mut patterns);
+                            EncState::None
+                        },
+                    };
+            }
+        }
+        state.write(&mut gw, &mut patterns);
+        let _ = gw.flush();
+        is_intra
+    }
+}
+
+impl NAEncoder for RoadPizzaEncoder {
+    fn negotiate_format(&self, encinfo: &EncodeParameters) -> EncoderResult<EncodeParameters> {
+        match encinfo.format {
+            NACodecTypeInfo::None => {
+                Ok(EncodeParameters {
+                    format: NACodecTypeInfo::Video(NAVideoInfo::new(0, 0, false, RGB555BE_FORMAT)),
+                    ..Default::default()
+                })
+            },
+            NACodecTypeInfo::Video(_vinfo) => {
+                let mut info = *encinfo;
+                if let NACodecTypeInfo::Video(ref mut vinfo) = info.format {
+                    vinfo.format  = RGB555BE_FORMAT;
+                    vinfo.width   = (vinfo.width  + 3) & !3;
+                    vinfo.height  = (vinfo.height + 3) & !3;
+                    vinfo.bits    = 16;
+                    vinfo.flipped = false;
+                } else {
+                    return Err(EncoderError::FormatError);
+                }
+                Ok(info)
+            },
+            NACodecTypeInfo::Audio(_) => Err(EncoderError::FormatError),
+        }
+    }
+    fn get_capabilities(&self) -> u64 { ENC_CAPS_SKIPFRAME }
+    fn init(&mut self, stream_id: u32, encinfo: EncodeParameters) -> EncoderResult<NAStreamRef> {
+        match encinfo.format {
+            NACodecTypeInfo::None => Err(EncoderError::FormatError),
+            NACodecTypeInfo::Audio(_) => Err(EncoderError::FormatError),
+            NACodecTypeInfo::Video(vinfo) => {
+                self.vinfo = vinfo;
+                if vinfo.format != RGB555BE_FORMAT || ((vinfo.width | vinfo.height) & 3) != 0 {
+                    return Err(EncoderError::FormatError);
+                }
+                let info = NACodecInfo::new("apple-video", encinfo.format, None);
+                let mut stream = NAStream::new(StreamType::Video, stream_id, info, encinfo.tb_num, encinfo.tb_den, 0);
+                stream.set_num(stream_id as usize);
+                let stream = stream.into_ref();
+                self.stream = Some(stream.clone());
+                Ok(stream)
+            }
+        }
+    }
+    fn encode(&mut self, frm: &NAFrame) -> EncoderResult<()> {
+        let buf = frm.get_buffer();
+        if let Some(vinfo) = buf.get_video_info() {
+            if vinfo != self.vinfo {
+                println!("Input format differs from the initial one");
+                return Err(EncoderError::FormatError);
+            }
+        }
+        self.frm_no += 1;
+        let force_intra = self.frm_no >= self.key_int;
+
+        let mut dbuf = vec![0xE1, 0, 0, 0]; // ID + 24-bit frame size
+        let is_intra = if let Some(vbuf16) = buf.get_vbuf16() {
+                let src = vbuf16.get_data();
+                let stride = vbuf16.get_stride(0);
+                self.last_frm.resize(self.vinfo.width * self.vinfo.height, 0);
+
+                self.encode(&mut dbuf, src, stride, force_intra)
+            } else if matches!(buf, NABufferType::None) {
+                if force_intra {
+                    if self.last_frm.is_empty() {
+                        return Err(EncoderError::InvalidParameters);
+                    }
+                    let last_copy = self.last_frm.clone();
+                    self.encode(&mut dbuf, &last_copy, self.vinfo.width, force_intra);
+                    true
+                } else {
+                    let state = EncState::Skip((self.vinfo.width / 4) * (self.vinfo.height / 4));
+                    let mut gw = GrowableMemoryWriter::new_write(&mut dbuf);
+                    let _ = gw.seek(SeekFrom::End(0));
+                    let mut pats = Vec::new();
+                    state.write(&mut gw, &mut pats);
+                    let _ = gw.flush();
+                    false
+                }
+            } else {
+                return Err(EncoderError::FormatError);
+            };
+
+        if is_intra {
+            self.frm_no = 0;
+        }
+
+        let size = dbuf.len() as u32;
+        let _ = write_u24be(&mut dbuf[1..], size);
+
+        self.pkt = Some(NAPacket::new(self.stream.clone().unwrap(), frm.ts, is_intra, dbuf));
+        Ok(())
+    }
+    fn get_packet(&mut self) -> EncoderResult<Option<NAPacket>> {
+        let mut npkt = None;
+        std::mem::swap(&mut self.pkt, &mut npkt);
+        Ok(npkt)
+    }
+    fn flush(&mut self) -> EncoderResult<()> {
+        Ok(())
+    }
+}
+
+const MODE_OPTION: &str = "mode";
+
+const ENCODER_OPTS: &[NAOptionDefinition] = &[
+    NAOptionDefinition {
+        name: KEYFRAME_OPTION, description: KEYFRAME_OPTION_DESC,
+        opt_type: NAOptionDefinitionType::Int(Some(0), Some(128)) },
+    NAOptionDefinition {
+        name: MODE_OPTION, description: "encoding strategy",
+        opt_type: NAOptionDefinitionType::String(Some(&["lossless", "normal", "refined"])) },
+];
+
+impl NAOptionHandler for RoadPizzaEncoder {
+    fn get_supported_options(&self) -> &[NAOptionDefinition] { ENCODER_OPTS }
+    fn set_options(&mut self, options: &[NAOption]) {
+        for option in options.iter() {
+            for opt_def in ENCODER_OPTS.iter() {
+                if opt_def.check(option).is_ok() {
+                    match option.name {
+                        KEYFRAME_OPTION => {
+                            if let NAValue::Int(intval) = option.value {
+                                self.key_int = intval as u8;
+                            }
+                        },
+                        MODE_OPTION => {
+                            if let NAValue::String(ref strval) = option.value {
+                                match strval.as_str() {
+                                    "lossless"  => { self.mode = Strategy::Lossless; },
+                                    "normal"    => { self.mode = Strategy::Normal; },
+                                    "refined"   => { self.mode = Strategy::Refined; },
+                                    _ => {},
+                                }
+                            }
+                        },
+                        _ => {},
+                    }
+                }
+            }
+        }
+    }
+    fn query_option_value(&self, name: &str) -> Option<NAValue> {
+        match name {
+            KEYFRAME_OPTION => Some(NAValue::Int(i64::from(self.key_int))),
+            MODE_OPTION => {
+                match self.mode {
+                    Strategy::Lossless  => Some(NAValue::String("lossless".to_string())),
+                    Strategy::Normal    => Some(NAValue::String("normal".to_string())),
+                    Strategy::Refined   => Some(NAValue::String("refined".to_string())),
+                }
+            },
+            _ => None,
+        }
+    }
+}
+
+pub fn get_encoder() -> Box<dyn NAEncoder + Send> {
+    Box::new(RoadPizzaEncoder::new())
+}
+
+#[cfg(test)]
+mod test {
+    use nihav_core::codecs::*;
+    use nihav_core::demuxers::*;
+    use nihav_core::muxers::*;
+    use nihav_commonfmt::*;
+    use nihav_codec_support::test::enc_video::*;
+    use crate::*;
+
+    // samples from https://samples.mplayerhq.hu/V-codecs/QTRLE
+    fn test_core(in_name: &'static str, format: NAPixelFormaton, enc_options: &[NAOption], hash: &[u32; 4]) {
+        let mut dmx_reg = RegisteredDemuxers::new();
+        generic_register_all_demuxers(&mut dmx_reg);
+        let mut dec_reg = RegisteredDecoders::new();
+        qt_register_all_decoders(&mut dec_reg);
+        let mut mux_reg = RegisteredMuxers::new();
+        generic_register_all_muxers(&mut mux_reg);
+        let mut enc_reg = RegisteredEncoders::new();
+        qt_register_all_encoders(&mut enc_reg);
+
+        let dec_config = DecoderTestParams {
+                demuxer:        "mov",
+                in_name,
+                stream_type:    StreamType::Video,
+                limit:          Some(4),
+                dmx_reg, dec_reg,
+            };
+        let enc_config = EncoderTestParams {
+                muxer:          "mov",
+                enc_name:       "apple-video",
+                out_name:       "rpza.mov",
+                mux_reg, enc_reg,
+            };
+        let dst_vinfo = NAVideoInfo {
+                width:   0,
+                height:  0,
+                format,
+                flipped: false,
+                bits:    format.get_total_depth(),
+            };
+        let enc_params = EncodeParameters {
+                format:  NACodecTypeInfo::Video(dst_vinfo),
+                quality: 0,
+                bitrate: 0,
+                tb_num:  0,
+                tb_den:  0,
+                flags:   0,
+            };
+        //test_encoding_to_file(&dec_config, &enc_config, enc_params, enc_options);
+        test_encoding_md5(&dec_config, &enc_config, enc_params, enc_options, hash);
+    }
+
+    #[test]
+    fn test_rpza_lossless() {
+        test_core("assets/QT/Animation-Truecolour.mov", super::RGB555BE_FORMAT,
+                  &[NAOption{ name: "mode", value: NAValue::String("lossless".to_owned()) }],
+                  &[0x420025d3, 0x33b792c4, 0x8f211b13, 0xa1f6f8e6]);
+    }
+    #[test]
+    fn test_rpza_normal() {
+        test_core("assets/QT/Animation-Truecolour.mov", super::RGB555BE_FORMAT,
+                  &[NAOption{ name: "mode", value: NAValue::String("normal".to_owned()) }],
+                  &[0x42ca0fce, 0xe49fd607, 0xac393dd9, 0xd9d503cf]);
+    }
+}