]> git.nihav.org Git - nihav.git/commitdiff
SMC encoder
authorKostya Shishkov <kostya.shishkov@gmail.com>
Sat, 16 May 2026 14:32:28 +0000 (16:32 +0200)
committerKostya Shishkov <kostya.shishkov@gmail.com>
Sat, 16 May 2026 14:32:28 +0000 (16:32 +0200)
nihav-qt/Cargo.toml
nihav-qt/src/codecs/mod.rs
nihav-qt/src/codecs/smcenc.rs [new file with mode: 0644]

index 9564a2cfbaf8826ebed4737c421ddd391c6b48ab..586347ee8c332a9051091dae9adb2f69676259b9 100644 (file)
@@ -10,7 +10,7 @@ features = []
 
 [dependencies.nihav_codec_support]
 path = "../nihav-codec-support"
-features = ["blockdsp", "fft", "qmf", "qt_pal"]
+features = ["blockdsp", "fft", "qmf", "qt_pal", "vq"]
 
 [dev-dependencies]
 nihav_commonfmt = { path = "../nihav-commonfmt", default-features=false, features = ["all_demuxers", "muxer_mov", "decoder_pcm"] }
@@ -46,10 +46,11 @@ demuxer_warhol = ["demuxers"]
 all_encoders = ["all_video_encoders", "all_audio_encoders"]
 encoders = []
 
-all_video_encoders = ["encoder_rawvid", "encoder_rle", "encoder_rpza"]
+all_video_encoders = ["encoder_rawvid", "encoder_rle", "encoder_rpza", "encoder_smc"]
 encoder_rawvid = ["encoders"]
 encoder_rle = ["encoders"]
 encoder_rpza = ["encoders"]
+encoder_smc = ["encoders"]
 
 all_audio_encoders = ["encoder_ima_adpcm_qt", "encoder_mace"]
 encoder_ima_adpcm_qt = ["encoders"]
index a058a3bb0fe5cf23612a3300eab52f380b19f138..7e2f485adb65ac2b8470152a3a05acbcfdd3505b 100644 (file)
@@ -124,6 +124,8 @@ mod rawvidenc;
 mod rleenc;
 #[cfg(feature="encoder_rpza")]
 mod rpzaenc;
+#[cfg(feature="encoder_smc")]
+mod smcenc;
 
 #[cfg(feature="encoder_ima_adpcm_qt")]
 mod imaadpcmenc;
@@ -140,6 +142,8 @@ const QT_ENCODERS: &[EncoderInfo] = &[
     EncoderInfo { name: "qt-rle", get_encoder: rleenc::get_encoder },
 #[cfg(feature="encoder_rpza")]
     EncoderInfo { name: "apple-video", get_encoder: rpzaenc::get_encoder },
+#[cfg(feature="encoder_smc")]
+    EncoderInfo { name: "qt-smc", get_encoder: smcenc::get_encoder },
 
 #[cfg(feature="encoder_ima_adpcm_qt")]
     EncoderInfo { name: "ima-adpcm-qt", get_encoder: imaadpcmenc::get_encoder },
diff --git a/nihav-qt/src/codecs/smcenc.rs b/nihav-qt/src/codecs/smcenc.rs
new file mode 100644 (file)
index 0000000..80c2ea5
--- /dev/null
@@ -0,0 +1,908 @@
+use std::convert::TryInto;
+use nihav_core::codecs::*;
+use nihav_core::io::byteio::*;
+use nihav_codec_support::vq::*;
+
+#[derive(Clone,Copy,Default,PartialEq)]
+enum Strategy {
+    #[default]
+    Lossless,
+    Max2Clr,
+    Max4Clr,
+    Max8Clr,
+}
+
+#[derive(Clone,PartialEq)]
+enum Opcode {
+    None,
+    Skip(usize),
+    Repeat(usize),
+    RepeatTwo(usize),
+    Fill(usize, u8),
+    TwoColours(usize, [u8; 2], [u16; 16]),
+    FourColours(usize, [u8; 4], [u32; 16]),
+    EightColours(usize, [u8; 8], [[u16; 3]; 16]),
+    Raw(usize),
+}
+
+impl Opcode {
+    fn is_skip(&self) -> bool { matches!(*self, Opcode::Skip(_)) }
+}
+
+struct EncoderHelper {
+    pairs:      [[u8; 2]; 256],
+    ppos:       usize,
+    pcount:     usize,
+    quads:      [[u8; 4]; 256],
+    qpos:       usize,
+    qcount:     usize,
+    octets:     [[u8; 8]; 256],
+    opos:       usize,
+    ocount:     usize,
+}
+
+impl Default for EncoderHelper {
+    fn default() -> Self {
+        let tmp = std::mem::MaybeUninit::<Self>::zeroed();
+        unsafe { tmp.assume_init() }
+    }
+}
+
+impl EncoderHelper {
+    fn reset(&mut self) {
+        *self = EncoderHelper::default();
+    }
+    fn write_token(&mut self, dbuf: &mut Vec<u8>, token: &Opcode, blocks: &[[u8; 16]], pos: usize) {
+        match *token {
+            Opcode::None => {},
+            Opcode::Skip(skip_len) => {
+                let mut to_write = skip_len;
+                while to_write > 16 {
+                    let len = to_write.min(256);
+                    dbuf.push(0x10);
+                    dbuf.push((len - 1) as u8);
+                    to_write -= len;
+                }
+                if to_write > 0 {
+                    dbuf.push((to_write - 1) as u8);
+                }
+            },
+            Opcode::Repeat(repeat_len) => {
+                let mut to_write = repeat_len;
+                while to_write > 16 {
+                    let len = to_write.min(256);
+                    dbuf.push(0x30);
+                    dbuf.push((len - 1) as u8);
+                    to_write -= len;
+                }
+                if to_write > 0 {
+                    dbuf.push(0x20 | ((to_write - 1) as u8));
+                }
+            },
+            Opcode::RepeatTwo(repeat_len) => {
+                let mut to_write = repeat_len;
+                while to_write > 16 {
+                    let len = to_write.min(256);
+                    dbuf.push(0x50);
+                    dbuf.push((len - 1) as u8);
+                    to_write -= len;
+                }
+                if to_write > 0 {
+                    dbuf.push(0x40 | ((to_write - 1) as u8));
+                }
+            },
+            Opcode::Fill(run_len, clr) => {
+                let mut to_write = run_len;
+                while to_write > 16 {
+                    let len = to_write.min(256);
+                    dbuf.push(0x70);
+                    dbuf.push((len - 1) as u8);
+                    dbuf.push(clr);
+                    to_write -= len;
+                }
+                if to_write > 0 {
+                    dbuf.push(0x60 | ((to_write - 1) as u8));
+                    dbuf.push(clr);
+                }
+            },
+            Opcode::TwoColours(count, pair, flags) => {
+                if let Some(idx) = self.pairs.iter().take(self.pcount).position(|pp| pp == &pair) {
+                    dbuf.push(0x90 | ((count - 1) as u8));
+                    dbuf.push(idx as u8);
+                } else {
+                    self.pairs[self.ppos] = pair;
+                    self.ppos = (self.ppos + 1) & 0xFF;
+                    if self.pcount < 256 {
+                        self.pcount += 1;
+                    }
+                    dbuf.push(0x80 | ((count - 1) as u8));
+                    dbuf.extend_from_slice(&pair);
+                }
+                let mut tmp = [0; 2];
+                for &flg in flags[..count].iter() {
+                    let _ = write_u16be(&mut tmp, flg);
+                    dbuf.extend_from_slice(&tmp);
+                }
+            },
+            Opcode::FourColours(count, quad, flags) => {
+                if let Some(idx) = self.quads.iter().take(self.qcount).position(|qq| qq == &quad) {
+                    dbuf.push(0xB0 | ((count - 1) as u8));
+                    dbuf.push(idx as u8);
+                } else {
+                    self.quads[self.qpos] = quad;
+                    self.qpos = (self.qpos + 1) & 0xFF;
+                    if self.qcount < 256 {
+                        self.qcount += 1;
+                    }
+                    dbuf.push(0xA0 | ((count - 1) as u8));
+                    dbuf.extend_from_slice(&quad);
+                }
+                let mut tmp = [0; 4];
+                for &flg in flags[..count].iter() {
+                    let _ = write_u32be(&mut tmp, flg);
+                    dbuf.extend_from_slice(&tmp);
+                }
+            },
+            Opcode::EightColours(count, oct, flags) => {
+                if let Some(idx) = self.octets.iter().take(self.ocount).position(|oo| oo == &oct) {
+                    dbuf.push(0xD0 | ((count - 1) as u8));
+                    dbuf.push(idx as u8);
+                } else {
+                    self.octets[self.opos] = oct;
+                    self.opos = (self.opos + 1) & 0xFF;
+                    if self.ocount < 256 {
+                        self.ocount += 1;
+                    }
+                    dbuf.push(0xC0 | ((count - 1) as u8));
+                    dbuf.extend_from_slice(&oct);
+                }
+                let mut tmp = [0; 6];
+                for flg in flags[..count].iter() {
+                    let _ = write_u16be(&mut tmp[..2], flg[0]);
+                    let _ = write_u16be(&mut tmp[2..], flg[1]);
+                    let _ = write_u16be(&mut tmp[4..], flg[2]);
+                    dbuf.extend_from_slice(&tmp);
+                }
+            },
+            Opcode::Raw(count) => {
+                let mut to_write = count;
+                let mut src_pos = pos - count;
+                while to_write > 0 {
+                    let len = to_write.min(16);
+                    dbuf.push(0xE0 | ((len - 1) as u8));
+                    for blk in blocks[src_pos..][..len].iter() {
+                        dbuf.extend_from_slice(blk);
+                    }
+                    to_write -= len;
+                    src_pos += len;
+                }
+            },
+        }
+    }
+}
+
+fn blk_2clr(block: &[u8; 16], clrs: &[u8; 2]) -> u16 {
+    let mut flags = 0;
+    for &pix in block.iter() {
+        flags <<= 1;
+        if pix == clrs[1] {
+            flags |= 1;
+        }
+    }
+    flags
+}
+
+fn blk_4clr(block: &[u8; 16], clrs: &[u8; 4]) -> u32 {
+    let mut flags = 0;
+    for pix in block.iter() {
+        flags <<= 2;
+        flags |= clrs.iter().position(|c| c == pix).unwrap_or(0) as u32;
+    }
+    flags
+}
+
+fn blk_8clr(block: &[u8; 16], clrs: &[u8; 8]) -> [u16; 3] {
+    let mut flags = [0; 4];
+    for (flag, row) in flags.iter_mut().zip(block.chunks_exact(4)) {
+        for pix in row.iter() {
+            *flag <<= 3;
+            *flag |= clrs.iter().position(|c| c == pix).unwrap_or(0) as u16;
+        }
+    }
+    [(flags[0] << 4) | (flags[3] >> 8),
+     (flags[1] << 4) | ((flags[3] >> 4) & 0xF),
+     (flags[2] << 4) | (flags[3] & 0xF)]
+}
+
+fn calc_clrs(clrs: &mut [u8; 24], mut ccount: usize, blk: &[u8; 16]) -> usize {
+    for clr in blk.iter() {
+        if !clrs[..ccount].contains(clr) {
+            clrs[ccount] = *clr;
+            ccount += 1;
+        }
+    }
+    if (2..=8).contains(&ccount) {
+        clrs[..ccount].sort();
+    }
+    ccount
+}
+
+#[derive(Clone,Copy,Default,PartialEq)]
+struct Colour([u8; 3]);
+
+impl VQElement for Colour {
+    fn dist(&self, rval: Self) -> u32 {
+        let rd = u32::from(self.0[0].abs_diff(rval.0[0]));
+        let gd = u32::from(self.0[1].abs_diff(rval.0[1]));
+        let bd = u32::from(self.0[2].abs_diff(rval.0[2]));
+        rd * rd + gd * gd + bd * bd
+    }
+    fn min_cw() -> Self { Colour([0x00; 3]) }
+    fn max_cw() -> Self { Colour([0xFF; 3]) }
+    fn min(&self, rval: Self) -> Self {
+        Colour([self.0[0].min(rval.0[0]),
+                self.0[1].min(rval.0[1]),
+                self.0[2].min(rval.0[2])])
+    }
+    fn max(&self, rval: Self) -> Self {
+        Colour([self.0[0].max(rval.0[0]),
+                self.0[1].max(rval.0[1]),
+                self.0[2].max(rval.0[2])])
+    }
+    fn num_components() -> usize { 3 }
+    fn sort_by_component(arr: &mut [Self], component: usize) {
+        arr.sort_unstable_by(|a, b| a.0[component].cmp(&b.0[component]));
+    }
+    fn max_dist_component(min: &Self, max: &Self) -> usize {
+        let rd = u32::from(min.0[0].abs_diff(max.0[0]));
+        let gd = u32::from(min.0[1].abs_diff(max.0[1]));
+        let bd = u32::from(min.0[2].abs_diff(max.0[2]));
+        if gd >= rd && gd >= bd {
+            1
+        } else if rd >= gd && rd >= bd {
+            0
+        } else if bd >= rd && bd >= gd {
+            2
+        } else {
+            1
+        }
+    }
+}
+
+#[derive(Default)]
+struct ColourSum {
+    clr:    [u16; 3],
+    tot:    u16,
+}
+
+impl VQElementSum<Colour> for ColourSum {
+    fn zero() -> Self { Self::default() }
+    fn add(&mut self, rval: Colour, count: u64) {
+        for (dst, &src) in self.clr.iter_mut().zip(rval.0.iter()) {
+            *dst += u16::from(src) * (count as u16);
+        }
+        self.tot += count as u16;
+    }
+    fn get_centroid(&self) -> Colour {
+        if self.tot > 0 {
+            Colour([(self.clr[0] / self.tot) as u8,
+                    (self.clr[1] / self.tot) as u8,
+                    (self.clr[2] / self.tot) as u8])
+        } else {
+            Colour::default()
+        }
+    }
+}
+
+fn pixel_dist(pix: &Colour, pal: &[u8]) -> u32 {
+    pal.iter().zip(pix.0.iter()).fold(0u32,
+        |acc, (&a, &b)| acc + u32::from(a.abs_diff(b)) * u32::from(a.abs_diff(b)))
+}
+
+fn lossy_block(blk: &[u8; 16], pal: &[u8; 1024], nclrs: usize, dst_clrs: &mut [u8; 8], dst_idx: &mut [u8; 16]) {
+    let mut pixels = [Colour::default(); 16];
+    for (dst, &src) in pixels.iter_mut().zip(blk.iter()) {
+        let clr = &pal[usize::from(src) * 4..][..3];
+        *dst = Colour([clr[0], clr[1], clr[2]]);
+    }
+    let mut ppal = [Colour::default(); 8];
+    quantise_median_cut::<Colour, ColourSum>(&pixels, &mut ppal[..nclrs]);
+    for (dst, src) in dst_clrs.iter_mut().zip(ppal.iter_mut()) {
+        let mut best_dist = u32::MAX;
+        let mut best_idx = 0;
+        for (i, palclr) in pal.chunks_exact(4).enumerate() {
+            let dist = pixel_dist(src, palclr);
+            if dist < best_dist {
+                best_dist = dist;
+                best_idx = i;
+                if dist == 0 {
+                    break;
+                }
+            }
+        }
+        *dst = best_idx as u8;
+        *src = Colour([pal[best_idx * 4], pal[best_idx * 4 + 1], pal[best_idx * 4 + 2]]);
+    }
+    for i in 0..nclrs {
+        for j in i + 1..nclrs {
+            if dst_clrs[i] > dst_clrs[j] {
+                dst_clrs.swap(i, j);
+                ppal.swap(i, j);
+            }
+        }
+    }
+
+    for (dst, pix) in dst_idx.iter_mut().zip(pixels.iter()) {
+        let mut best_dist = u32::MAX;
+        let mut best_idx = 0;
+        for (i, refclr) in ppal.iter().take(nclrs).enumerate() {
+            let dist = pixel_dist(pix, &refclr.0);
+            if dist < best_dist {
+                best_dist = dist;
+                best_idx = i;
+                if dist == 0 {
+                    break;
+                }
+            }
+        }
+        *dst = best_idx as u8;
+    }
+}
+
+struct SeanEncoder {
+    stream:     Option<NAStreamRef>,
+    pkt:        Option<NAPacket>,
+    key_int:    u8,
+    frm_no:     u8,
+    lpal:       Arc<[u8; 1024]>,
+    last_frm:   Vec<[u8; 16]>,
+    cur_frm:    Vec<[u8; 16]>,
+    vinfo:      NAVideoInfo,
+    mode:       Strategy,
+    helper:     EncoderHelper,
+}
+
+impl SeanEncoder {
+    fn new() -> Self {
+        Self {
+            stream:     None,
+            pkt:        None,
+            key_int:    25,
+            frm_no:     254,
+            lpal:       Arc::new([0; 1024]),
+            last_frm:   Vec::new(),
+            cur_frm:    Vec::new(),
+            vinfo:      NAVideoInfo::new(0, 0, false, PAL8_FORMAT),
+            mode:       Strategy::default(),
+            helper:     EncoderHelper::default(),
+        }
+    }
+    fn load_frame(&mut self, buf: NAVideoBufferRef<u8>) {
+        let (w, h) = buf.get_dimensions(0);
+        let stride = buf.get_stride(0);
+        let src = buf.get_data();
+
+        self.cur_frm.clear();
+        let mut blk = [0; 16];
+        for strip in src.chunks_exact(stride * 4).take(h / 4) {
+            for x in (0..w).step_by(4) {
+                for (row, line) in blk.chunks_exact_mut(4).zip(strip.chunks_exact(stride)) {
+                    row.copy_from_slice(&line[x..][..4]);
+                }
+                self.cur_frm.push(blk);
+            }
+        }
+    }
+    fn encode_frame(&mut self, dbuf: &mut Vec<u8>, force_intra: bool) -> bool {
+        let mut is_intra = true;
+
+        let mut opcode = Opcode::None;
+        self.helper.reset();
+        let mut clrs = [0; 24];
+        let mut clrs2 = [0; 24];
+        let mut lookahead = false;
+        let mut lossy_clrs = [0; 8];
+        let mut lossy_idx = [0; 16];
+        for (blk_no, (block, pblock)) in self.cur_frm.iter().zip(self.last_frm.iter_mut()).enumerate() {
+            if lookahead {
+                lookahead = false;
+                pblock.copy_from_slice(block);
+                continue;
+            }
+
+            if !force_intra && block == pblock {
+                if let Opcode::Skip(ref mut scount) = opcode {
+                    *scount += 1;
+                } else {
+                    self.helper.write_token(dbuf, &opcode, &self.cur_frm, blk_no);
+                    opcode = Opcode::Skip(1);
+                }
+                is_intra = false;
+                continue;
+            }
+
+            pblock.copy_from_slice(block);
+
+            let has_next_block = blk_no < self.cur_frm.len() - 1;
+
+            if let Opcode::Fill(ref mut count, fillclr) = opcode {
+                if block == &[fillclr; 16] {
+                    *count += 1;
+                    continue;
+                }
+            }
+
+            if !opcode.is_skip() && blk_no > 0 && block == &self.cur_frm[blk_no - 1] {
+                if let Opcode::Repeat(ref mut count) = opcode {
+                    *count += 1;
+                    continue;
+                }
+                self.helper.write_token(dbuf, &opcode, &self.cur_frm, blk_no);
+                opcode = Opcode::Repeat(1);
+                continue;
+            }
+
+            if !opcode.is_skip() && blk_no > 1 && has_next_block && block == &self.cur_frm[blk_no - 2] && self.cur_frm[blk_no - 1] == self.cur_frm[blk_no + 1] {
+                if let Opcode::RepeatTwo(ref mut count) = opcode {
+                    lookahead = true;
+                    *count += 1;
+                    continue;
+                }
+                lookahead = true;
+                self.helper.write_token(dbuf, &opcode, &self.cur_frm, blk_no);
+                opcode = Opcode::RepeatTwo(1);
+                continue;
+            }
+
+            let ccount = calc_clrs(&mut clrs, 0, block);
+            match ccount {
+                1 => {
+                    if let Opcode::Fill(ref mut count, ref clr) = opcode {
+                        if *clr == clrs[0] {
+                            *count += 1;
+                            continue;
+                        }
+                    }
+                    self.helper.write_token(dbuf, &opcode, &self.cur_frm, blk_no);
+                    opcode = Opcode::Fill(1, clrs[0]);
+                },
+                2 => {
+                    if let Opcode::TwoColours(ref mut count, ref twoclrs, ref mut flags) = opcode {
+                        if *count < 16 && twoclrs == &clrs[..2] {
+                            flags[*count] = blk_2clr(block, twoclrs);
+                            *count += 1;
+                            continue;
+                        }
+                    }
+                    self.helper.write_token(dbuf, &opcode, &self.cur_frm, blk_no);
+                    let mut flags = [0; 16];
+                    let twoclrs = [clrs[0], clrs[1]];
+                    flags[0] = blk_2clr(block, &twoclrs);
+                    opcode = Opcode::TwoColours(1, twoclrs, flags);
+                },
+                _ if self.mode == Strategy::Max2Clr => {
+                    lossy_block(block, &self.lpal, 2, &mut lossy_clrs, &mut lossy_idx);
+                    let mut blk_flags = 0;
+                    for (dst, &idx) in pblock.iter_mut().zip(lossy_idx.iter()) {
+                        *dst = lossy_clrs[usize::from(idx)];
+                        blk_flags = (blk_flags << 1) | u16::from(idx);
+                    }
+                    if let Opcode::TwoColours(ref mut count, ref twoclrs, ref mut flags) = opcode {
+                        if *count < 16 && twoclrs == &lossy_clrs[..2] {
+                            flags[*count] = blk_flags;
+                            *count += 1;
+                            continue;
+                        }
+                    }
+                    self.helper.write_token(dbuf, &opcode, &self.cur_frm, blk_no);
+                    let mut flags = [0; 16];
+                    let twoclrs = [lossy_clrs[0], lossy_clrs[1]];
+                    flags[0] = blk_flags;
+                    opcode = Opcode::TwoColours(1, twoclrs, flags);
+                },
+                3 => {
+                    if let Opcode::FourColours(ref mut count, ref fourclrs, ref mut flags) = opcode {
+                        if *count < 16 && fourclrs.contains(&clrs[0]) && fourclrs.contains(&clrs[1]) && fourclrs.contains(&clrs[2]) {
+                            flags[*count] = blk_4clr(block, fourclrs);
+                            *count += 1;
+                            continue;
+                        }
+                    }
+                    // check next blocks if possible to see if we can use merged 4-colour set
+                    let mut fourclrs = clrs[..4].try_into().unwrap();
+                    clrs2[..ccount].copy_from_slice(&clrs[..ccount]);
+                    let mut ccount2 = ccount;
+                    for next_blk in self.cur_frm.iter().skip(blk_no + 1) {
+                        ccount2 = calc_clrs(&mut clrs2, ccount2, next_blk);
+                        if ccount2 <= 4 {
+                            fourclrs = clrs2[..4].try_into().unwrap();
+                        } else {
+                            break;
+                        }
+                    }
+                    self.helper.write_token(dbuf, &opcode, &self.cur_frm, blk_no);
+                    let mut flags = [0; 16];
+                    flags[0] = blk_4clr(block, &fourclrs);
+                    opcode = Opcode::FourColours(1, fourclrs, flags);
+                },
+                4 => {
+                    if let Opcode::FourColours(ref mut count, ref fourclrs, ref mut flags) = opcode {
+                        if *count < 16 && fourclrs == &clrs[..4] {
+                            flags[*count] = blk_4clr(block, fourclrs);
+                            *count += 1;
+                            continue;
+                        }
+                    }
+                    self.helper.write_token(dbuf, &opcode, &self.cur_frm, blk_no);
+                    let mut flags = [0; 16];
+                    let fourclrs = [clrs[0], clrs[1], clrs[2], clrs[3]];
+                    flags[0] = blk_4clr(block, &fourclrs);
+                    opcode = Opcode::FourColours(1, fourclrs, flags);
+                },
+                _ if self.mode == Strategy::Max4Clr => {
+                    lossy_block(block, &self.lpal, 4, &mut lossy_clrs, &mut lossy_idx);
+                    let mut blk_flags = 0;
+                    for (dst, &idx) in pblock.iter_mut().zip(lossy_idx.iter()) {
+                        *dst = lossy_clrs[usize::from(idx)];
+                        blk_flags = (blk_flags << 2) | u32::from(idx);
+                    }
+                    if let Opcode::FourColours(ref mut count, ref fourclrs, ref mut flags) = opcode {
+                        if *count < 16 && fourclrs == &lossy_clrs[..4] {
+                            flags[*count] = blk_flags;
+                            *count += 1;
+                            continue;
+                        }
+                    }
+                    self.helper.write_token(dbuf, &opcode, &self.cur_frm, blk_no);
+                    let mut flags = [0; 16];
+                    let fourclrs = lossy_clrs[..4].try_into().unwrap();
+                    flags[0] = blk_flags;
+                    opcode = Opcode::FourColours(1, fourclrs, flags);
+                },
+                5..=7 => {
+                    if let Opcode::EightColours(ref mut count, ref eightclrs, ref mut flags) = opcode {
+                        let mut all_in = true;
+                        for clr in clrs[..ccount].iter() {
+                            if !eightclrs.contains(clr) {
+                                all_in = false;
+                                break;
+                            }
+                        }
+                        if *count < 16 && all_in {
+                            flags[*count] = blk_8clr(block, eightclrs);
+                            *count += 1;
+                            continue;
+                        }
+                    }
+                    // check next block if possible to see if we can use merged 8-colour set
+                    let mut eightclrs = clrs[..8].try_into().unwrap();
+                    clrs2[..ccount].copy_from_slice(&clrs[..ccount]);
+                    let mut ccount2 = ccount;
+                    for next_blk in self.cur_frm.iter().skip(blk_no + 1) {
+                        ccount2 = calc_clrs(&mut clrs2, ccount2, next_blk);
+                        if ccount2 <= 8 {
+                            eightclrs = clrs2[..8].try_into().unwrap();
+                        } else {
+                            break;
+                        }
+                    }
+                    self.helper.write_token(dbuf, &opcode, &self.cur_frm, blk_no);
+                    let mut flags = [[0; 3]; 16];
+                    flags[0] = blk_8clr(block, &eightclrs);
+                    opcode = Opcode::EightColours(1, eightclrs, flags);
+                },
+                8 => {
+                    if let Opcode::EightColours(ref mut count, ref eightclrs, ref mut flags) = opcode {
+                        if *count < 16 && eightclrs == &clrs[..8] {
+                            flags[*count] = blk_8clr(block, eightclrs);
+                            *count += 1;
+                            continue;
+                        }
+                    }
+                    self.helper.write_token(dbuf, &opcode, &self.cur_frm, blk_no);
+                    let mut flags = [[0; 3]; 16];
+                    let eightclrs = clrs[..8].try_into().unwrap();
+                    flags[0] = blk_8clr(block, &eightclrs);
+                    opcode = Opcode::EightColours(1, eightclrs, flags);
+                },
+                _ if self.mode == Strategy::Max8Clr => {
+                    lossy_block(block, &self.lpal, 8, &mut lossy_clrs, &mut lossy_idx);
+                    let mut row_flags = [0; 4];
+                    for (dst, &idx) in pblock.iter_mut().zip(lossy_idx.iter()) {
+                        *dst = lossy_clrs[usize::from(idx)];
+                    }
+                    for (flg, row) in row_flags.iter_mut().zip(lossy_idx.chunks_exact(4)) {
+                        for &idx in row.iter() {
+                            *flg = (*flg << 3) | u16::from(idx);
+                        }
+                    }
+                    let blk_flags = [(row_flags[0] << 4) | (row_flags[3] >> 8),
+                                     (row_flags[1] << 4) | ((row_flags[3] >> 4) & 0xF),
+                                     (row_flags[2] << 4) | (row_flags[3] & 0xF)];
+
+                    if let Opcode::EightColours(ref mut count, ref eightclrs, ref mut flags) = opcode {
+                        if *count < 16 && eightclrs == &lossy_clrs {
+                            flags[*count] = blk_flags;
+                            *count += 1;
+                            continue;
+                        }
+                    }
+                    self.helper.write_token(dbuf, &opcode, &self.cur_frm, blk_no);
+                    let mut flags = [[0; 3]; 16];
+                    let eightclrs = lossy_clrs;
+                    flags[0] = blk_flags;
+                    opcode = Opcode::EightColours(1, eightclrs, flags);
+                },
+                _ => {
+                    if let Opcode::Raw(ref mut count) = opcode {
+                        if *count < 16 {
+                            *count += 1;
+                            continue;
+                        }
+                    }
+                    self.helper.write_token(dbuf, &opcode, &self.cur_frm, blk_no);
+                    opcode = Opcode::Raw(1);
+                },
+            }
+        }
+        self.helper.write_token(dbuf, &opcode, &self.cur_frm, self.cur_frm.len());
+
+        is_intra
+    }
+    fn encode_skip_frame(&mut self, dbuf: &mut Vec<u8>) {
+        self.helper.write_token(dbuf, &Opcode::Skip(self.cur_frm.len()), &[], 0);
+    }
+}
+
+impl NAEncoder for SeanEncoder {
+    fn negotiate_format(&self, encinfo: &EncodeParameters) -> EncoderResult<EncodeParameters> {
+        match encinfo.format {
+            NACodecTypeInfo::None => {
+                Ok(EncodeParameters {
+                    format: NACodecTypeInfo::Video(NAVideoInfo::new(0, 0, false, PAL8_FORMAT)),
+                    ..Default::default()
+                })
+            },
+            NACodecTypeInfo::Video(_vinfo) => {
+                let mut info = *encinfo;
+                if let NACodecTypeInfo::Video(ref mut vinfo) = info.format {
+                    vinfo.format  = PAL8_FORMAT;
+                    vinfo.width   = (vinfo.width  + 3) & !3;
+                    vinfo.height  = (vinfo.height + 3) & !3;
+                    vinfo.bits    = 8;
+                    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 != PAL8_FORMAT || ((vinfo.width | vinfo.height) & 3) != 0 {
+                    return Err(EncoderError::FormatError);
+                }
+                let info = NACodecInfo::new("qt-smc", 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 side_data = if let Some(vbuf) = buf.get_vbuf() {
+                let mut npal = [0; 1024];
+                let src = vbuf.get_data();
+                for (dst, src) in npal.chunks_exact_mut(4).zip(src[vbuf.get_offset(1)..].chunks_exact(3)) {
+                    dst[..3].copy_from_slice(src);
+                }
+                let new_pal = npal != *self.lpal;
+                if new_pal {
+                    self.lpal = Arc::new(npal);
+                }
+                Some(NASideData::Palette(new_pal, Arc::clone(&self.lpal)))
+            } else { None };
+
+        let mut dbuf = vec![0x80, 0, 0, 0]; // ID and 24-bit frame size
+        let is_intra = if let Some(vbuf) = buf.get_vbuf() {
+                self.load_frame(vbuf);
+                if self.last_frm.is_empty() {
+                    self.last_frm.extend_from_slice(&self.cur_frm);
+                }
+
+                self.encode_frame(&mut dbuf, force_intra)
+            } else if matches!(buf, NABufferType::None) {
+                self.cur_frm.clear();
+                self.cur_frm.extend_from_slice(&self.last_frm);
+                if force_intra {
+                    if self.cur_frm.is_empty() {
+                        return Err(EncoderError::InvalidParameters);
+                    }
+                    self.encode_frame(&mut dbuf, force_intra);
+                    true
+                } else {
+                    self.encode_skip_frame(&mut dbuf);
+                    false
+                }
+            } else {
+                return Err(EncoderError::FormatError);
+            };
+        std::mem::swap(&mut self.last_frm, &mut self.cur_frm);
+
+        if is_intra {
+            self.frm_no = 0;
+        }
+
+        let size = dbuf.len() as u32;
+        let _ = write_u24be(&mut dbuf[1..], size);
+
+        let mut pkt = NAPacket::new(self.stream.clone().unwrap(), frm.ts, is_intra, dbuf);
+        if let Some(sdata) = side_data {
+            pkt.add_side_data(sdata);
+        }
+
+        self.pkt = Some(pkt);
+        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", "2clr", "4clr", "8clr"])) },
+];
+
+impl NAOptionHandler for SeanEncoder {
+    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; },
+                                    "2clr"     => { self.mode = Strategy::Max2Clr; },
+                                    "4clr"     => { self.mode = Strategy::Max4Clr; },
+                                    "8clr"     => { self.mode = Strategy::Max8Clr; },
+                                    _ => {},
+                                }
+                            }
+                        },
+                        _ => {},
+                    }
+                }
+            }
+        }
+    }
+    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::Max2Clr  => Some(NAValue::String("2clr".to_string())),
+                    Strategy::Max4Clr  => Some(NAValue::String("4clr".to_string())),
+                    Strategy::Max8Clr  => Some(NAValue::String("8clr".to_string())),
+                }
+            },
+            _ => None,
+        }
+    }
+}
+
+pub fn get_encoder() -> Box<dyn NAEncoder + Send> {
+    Box::new(SeanEncoder::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(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:        "assets/QT/Animation-256Greys.mov",
+                stream_type:    StreamType::Video,
+                limit:          Some(2),
+                dmx_reg, dec_reg,
+            };
+        let enc_config = EncoderTestParams {
+                muxer:          "mov",
+                enc_name:       "qt-smc",
+                out_name:       "qt_smc.mov",
+                mux_reg, enc_reg,
+            };
+        let dst_vinfo = NAVideoInfo {
+                width:   0,
+                height:  0,
+                format:  PAL8_FORMAT,
+                flipped: false,
+                bits:    8,
+            };
+        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_smc() {
+        test_core(&[],
+                  &[0xf53a5201, 0x7549e986, 0x680f929a, 0x92467b9e]);
+    }
+    #[test]
+    fn test_smc_2clrs() {
+        test_core(&[NAOption{ name: "mode", value: NAValue::String("2clr".to_string())}],
+                  &[0xbf7be071, 0x62d1e567, 0x3ae99dc3, 0xac0bbc82]);
+    }
+    #[test]
+    fn test_smc_4clrs() {
+        test_core(&[NAOption{ name: "mode", value: NAValue::String("4clr".to_string())}],
+                  &[0xc4b244ca, 0x1a4eebfd, 0x37b04213, 0x352a0105]);
+    }
+    #[test]
+    fn test_smc_8clrs() {
+        test_core(&[NAOption{ name: "mode", value: NAValue::String("8clr".to_string())}],
+                  &[0x2f9a1663, 0x09601374, 0x1a014ab8, 0xfc341a92]);
+    }
+}