]> git.nihav.org Git - nihav.git/commitdiff
The Complete Animation format support
authorKostya Shishkov <kostya.shishkov@gmail.com>
Sat, 12 Apr 2025 07:38:58 +0000 (09:38 +0200)
committerKostya Shishkov <kostya.shishkov@gmail.com>
Sat, 12 Apr 2025 07:38:58 +0000 (09:38 +0200)
nihav-acorn/Cargo.toml
nihav-acorn/src/codecs/euclid.rs [new file with mode: 0644]
nihav-acorn/src/codecs/mod.rs
nihav-acorn/src/demuxers/mod.rs
nihav-acorn/src/demuxers/tca.rs [new file with mode: 0644]
nihav-acorn/src/lib.rs
nihav-allstuff/src/lib.rs
nihav-registry/src/detect.rs
nihav-registry/src/register.rs

index e430a58faf7aec070a0bde3aa3a2248405e51121..56ac79d9fcfd5d755cdc3fad4bcb436eb23cb98b 100644 (file)
@@ -14,7 +14,7 @@ path = "../nihav-codec-support"
 default = ["all_decoders", "all_demuxers", "all_packetisers"]
 
 all_decoders = ["all_video_decoders", "all_audio_decoders"]
-all_video_decoders = ["decoder_movinglines", "decoder_movingblocks", "decoder_movingblockshq", "decoder_supermovingblocks", "decoder_linepack", "decoder_escape", "decoder_rawvideo"]
+all_video_decoders = ["decoder_movinglines", "decoder_movingblocks", "decoder_movingblockshq", "decoder_supermovingblocks", "decoder_linepack", "decoder_escape", "decoder_rawvideo", "decoder_euclid"]
 all_audio_decoders = ["decoder_rawaudio"]
 decoders = []
 
@@ -25,13 +25,15 @@ decoder_supermovingblocks = ["decoders"]
 decoder_linepack = ["decoders"]
 decoder_rawvideo = ["decoders"]
 decoder_escape = ["decoders"]
+decoder_euclid = ["decoders"]
 
 decoder_rawaudio = ["decoders"]
 
-all_demuxers = ["demuxer_armovie"]
+all_demuxers = ["demuxer_armovie", "demuxer_tca"]
 demuxers = []
 
 demuxer_armovie = ["demuxers"]
+demuxer_tca = ["demuxers"]
 
 all_packetisers = ["packetiser_cinepak", "packetiser_msvideo1"]
 packetisers = []
diff --git a/nihav-acorn/src/codecs/euclid.rs b/nihav-acorn/src/codecs/euclid.rs
new file mode 100644 (file)
index 0000000..3f7e13d
--- /dev/null
@@ -0,0 +1,325 @@
+use nihav_core::codecs::*;
+use nihav_core::io::bitreader::*;
+use nihav_core::io::byteio::*;
+use std::cmp::Ordering;
+
+const START_BITS: u8 = 9;
+const MAX_BITS:   u8 = 16;
+const DICT_SIZE: usize = 1 << MAX_BITS;
+const START_POS:   usize = 257;
+const INVALID_POS: usize = 65536;
+
+struct LZWState {
+    dict_sym:   [u8; DICT_SIZE],
+    dict_prev:  [u16; DICT_SIZE],
+    dict_pos:   usize,
+    dict_lim:   usize,
+    idx_bits:   u8,
+}
+
+impl LZWState {
+    fn new() -> Self {
+        Self {
+            dict_sym:   [0; DICT_SIZE],
+            dict_prev:  [0; DICT_SIZE],
+            dict_pos:   START_POS,
+            dict_lim:   1 << START_BITS,
+            idx_bits:   START_BITS,
+        }
+    }
+    fn reset(&mut self) {
+        self.dict_pos = START_POS;
+        self.dict_lim = 1 << START_BITS;
+        self.idx_bits = START_BITS;
+    }
+    fn add(&mut self, prev: usize, sym: u8) {
+        if self.dict_pos < self.dict_lim {
+            self.dict_sym [self.dict_pos] = sym;
+            self.dict_prev[self.dict_pos] = prev as u16;
+            self.dict_pos += 1;
+        }
+    }
+    fn decode_idx(&self, dst: &mut [u8], pos: usize, idx: usize) -> DecoderResult<usize> {
+        let mut tot_len = 1;
+        let mut tidx = idx;
+        while tidx > 256 {
+            tidx = self.dict_prev[tidx] as usize;
+            tot_len += 1;
+        }
+        validate!(pos + tot_len <= dst.len());
+
+        let mut end = pos + tot_len - 1;
+        let mut tidx = idx;
+        while tidx > 256 {
+            dst[end] = self.dict_sym[tidx];
+            end -= 1;
+            tidx = self.dict_prev[tidx] as usize;
+        }
+        dst[end] = tidx as u8;
+
+        Ok(tot_len)
+    }
+    fn decode(&mut self, br: &mut BitReader, dst: &mut [u8]) -> DecoderResult<()> {
+        self.reset();
+
+        let mut pos = 0;
+        let mut lastidx = INVALID_POS;
+        br.skip(9)?;
+        loop {
+            let idx         = br.read(self.idx_bits)? as usize;
+            if idx == 256 {
+                break;
+            }
+            match idx.cmp(&self.dict_pos) {
+                Ordering::Less => {
+                    let len = self.decode_idx(dst, pos, idx)?;
+                    if lastidx != INVALID_POS {
+                        self.add(lastidx, dst[pos]);
+                    }
+                    pos += len;
+                },
+                Ordering::Equal => {
+                    validate!(lastidx != INVALID_POS);
+                    let len = self.decode_idx(dst, pos, lastidx)?;
+                    let lastsym = dst[pos];
+                    pos += len;
+                    validate!(pos < dst.len());
+                    dst[pos] = lastsym;
+                    pos += 1;
+                    self.add(lastidx, lastsym);
+                },
+                Ordering::Greater => return Err(DecoderError::InvalidData),
+            }
+            lastidx = idx;
+            if self.dict_pos == self.dict_lim && self.idx_bits < MAX_BITS {
+                self.dict_lim <<= 1;
+                self.idx_bits += 1;
+            }
+        }
+        validate!(pos == dst.len());
+        Ok(())
+    }
+}
+
+struct EuclidDecoder {
+    info:           NACodecInfoRef,
+    width:          usize,
+    height:         usize,
+    lzw:            LZWState,
+    pal:            [u8; 768],
+    frame:          Vec<u8>,
+    mode:           u32,
+    update:         bool,
+    buf:            Vec<u8>,
+}
+
+impl EuclidDecoder {
+    fn new() -> Self {
+        Self {
+            info:       NACodecInfoRef::default(),
+            width:      0,
+            height:     0,
+            lzw:        LZWState::new(),
+            pal:        [0; 768],
+            frame:      Vec::new(),
+            mode:       0,
+            update:     false,
+            buf:        Vec::new(),
+        }
+    }
+    fn output_frame(&self, dst: &mut [u8], stride: usize, paloff: usize) {
+        match self.mode {
+            12 => {
+                for (dlines, sline) in dst.chunks_exact_mut(stride * 2)
+                        .zip(self.frame.chunks_exact(self.width / 2)).take(self.height / 2) {
+                    let (dline0, dline1) = dlines.split_at_mut(stride);
+                    for (pair, &b) in dline0.chunks_exact_mut(2).zip(sline.iter()) {
+                        pair[0] = b & 0xF;
+                        pair[1] = b >> 4;
+                    }
+                    dline1.copy_from_slice(dline0);
+                }
+            },
+            15 | 40 => {
+                for (dlines, sline) in dst.chunks_exact_mut(stride * 2)
+                        .zip(self.frame.chunks_exact(self.width)).take(self.height / 2) {
+                    let (dline0, dline1) = dlines.split_at_mut(stride);
+                    dline0[..sline.len()].copy_from_slice(sline);
+                    dline1[..sline.len()].copy_from_slice(sline);
+                }
+            },
+            27 => {
+                for (dline, sline) in dst.chunks_exact_mut(stride)
+                        .zip(self.frame.chunks_exact(self.width / 2)).take(self.height) {
+                    for (pair, &b) in dline.chunks_exact_mut(2).zip(sline.iter()) {
+                        pair[0] = b & 0xF;
+                        pair[1] = b >> 4;
+                    }
+                }
+            },
+            _ => {
+                for (dline, sline) in dst.chunks_exact_mut(stride)
+                        .zip(self.frame.chunks_exact(self.width)).take(self.height) {
+                    dline[..sline.len()].copy_from_slice(sline);
+                }
+            },
+        }
+
+        dst[paloff..][..768].copy_from_slice(&self.pal);
+    }
+}
+
+impl NADecoder for EuclidDecoder {
+    fn init(&mut self, _supp: &mut NADecoderSupport, info: NACodecInfoRef) -> DecoderResult<()> {
+        if let NACodecTypeInfo::Video(vinfo) = info.get_properties() {
+            self.width  = vinfo.get_width();
+            self.height = vinfo.get_height();
+            if let Some(edata) = info.get_extradata() {
+                validate!(edata.len() >= 0x34);
+                let hdr_size = read_u32le(&edata)? as usize;
+                validate!(hdr_size >= 0x34 && hdr_size <= edata.len());
+                self.mode = read_u32le(&edata[0x10..])?;
+
+                self.update = (read_u32le(&edata[0x18..])? & 1) != 0;
+
+                if edata.len() > hdr_size {
+                    let src_pal = &edata[hdr_size..];
+                    let copy_size = src_pal.len().min(self.pal.len());
+                    self.pal[..copy_size].copy_from_slice(&src_pal[..copy_size]);
+                } else {
+                    //self.pal[..DEFAULT_PAL.len()].copy_from_slice(&DEFAULT_PAL);
+                    for (i, clr) in self.pal.chunks_exact_mut(3).enumerate() {
+                        clr[0] = i as u8;
+                        clr[1] = i as u8;
+                        clr[2] = i as u8;
+                    }
+                }
+            } else {
+                return Err(DecoderError::InvalidData);
+            }
+            let frm_size = match self.mode {
+                    12 => {
+                        validate!((self.width & 1) == 0);
+                        validate!((self.height & 1) == 0);
+                        (self.width / 2) * (self.height / 2)
+                    },
+                    15 | 40 => {
+                        validate!((self.height & 1) == 0);
+                        self.width * (self.height / 2)
+                    },
+                    27 => {
+                        validate!((self.width & 1) == 0);
+                        (self.width / 2) * self.height
+                    },
+                    28 => {
+                        self.width * self.height
+                    },
+                    _ => return Err(DecoderError::NotImplemented),
+                };
+            self.frame = vec![0; frm_size];
+            self.buf = vec![0; frm_size];
+
+            let myinfo = NACodecTypeInfo::Video(NAVideoInfo::new(self.width, self.height, false, PAL8_FORMAT));
+            self.info = NACodecInfo::new_ref(info.get_name(), myinfo, info.get_extradata()).into_ref();
+
+            Ok(())
+        } else {
+            Err(DecoderError::InvalidData)
+        }
+    }
+    fn decode(&mut self, _supp: &mut NADecoderSupport, pkt: &NAPacket) -> DecoderResult<NAFrameRef> {
+        let src = pkt.get_buffer();
+        validate!(src.len() > 1);
+
+        let mut br = BitReader::new(&src, BitReaderMode::LE);
+
+        let bufinfo = alloc_video_buffer(self.info.get_properties().get_video_info().unwrap(), 0)?;
+
+        let mut buf = bufinfo.get_vbuf().unwrap();
+        let paloff = buf.get_offset(1);
+        let stride = buf.get_stride(0);
+        let data = buf.get_data_mut().unwrap();
+
+        self.lzw.decode(&mut br, if !self.update { &mut self.frame } else { &mut self.buf })?;
+        if self.update {
+            for (pix, &b) in self.frame.iter_mut().zip(self.buf.iter()) {
+                *pix ^= b;
+            }
+        }
+
+        self.output_frame(data, stride, paloff);
+
+        let mut frm = NAFrame::new_from_pkt(pkt, self.info.clone(), bufinfo);
+        frm.set_keyframe(self.update);
+        frm.set_frame_type(if !self.update { FrameType::I } else { FrameType::P });
+        Ok(frm.into_ref())
+    }
+    fn flush(&mut self) {}
+}
+
+impl NAOptionHandler for EuclidDecoder {
+    fn get_supported_options(&self) -> &[NAOptionDefinition] { &[] }
+    fn set_options(&mut self, _options: &[NAOption]) { }
+    fn query_option_value(&self, _name: &str) -> Option<NAValue> { None }
+}
+
+pub fn get_decoder() -> Box<dyn NADecoder + Send> {
+    Box::new(EuclidDecoder::new())
+}
+
+#[cfg(test)]
+mod test {
+    use nihav_core::codecs::RegisteredDecoders;
+    use nihav_core::demuxers::RegisteredDemuxers;
+    use nihav_codec_support::test::dec_video::*;
+    use crate::*;
+
+    #[test]
+    fn test_euclid() {
+        let mut dmx_reg = RegisteredDemuxers::new();
+        acorn_register_all_demuxers(&mut dmx_reg);
+        let mut dec_reg = RegisteredDecoders::new();
+        acorn_register_all_decoders(&mut dec_reg);
+
+        // a sample from RISC DISC 3
+        test_decoding("tca", "euclid", "assets/Acorn/JUMPER", Some(1),
+                          &dmx_reg, &dec_reg,
+                          ExpectedTestResult::MD5Frames(vec![
+                                [0x5ce22d08, 0x5f3cbd1b, 0x61a8b85e, 0x58197ba6],
+                                [0x1fc3cc2e, 0xdc107b42, 0xada9f7b5, 0x0882f3d6]]));
+    }
+    #[test]
+    fn test_euclid_inter() {
+        let mut dmx_reg = RegisteredDemuxers::new();
+        acorn_register_all_demuxers(&mut dmx_reg);
+        let mut dec_reg = RegisteredDecoders::new();
+        acorn_register_all_decoders(&mut dec_reg);
+
+        // a sample from RISC DISC 3
+        test_decoding("tca", "euclid", "assets/Acorn/WIGGLFACE", Some(1),
+                          &dmx_reg, &dec_reg,
+                          ExpectedTestResult::MD5Frames(vec![
+                                [0xd1362b20, 0x986be0ec, 0x59aae46e, 0x11123e70],
+                                [0xc07ab19c, 0xc518aca6, 0xc1f632b3, 0x9baa9cb5]]));
+    }
+}
+
+#[allow(dead_code)]
+const DEFAULT_PAL: [u8; 16 * 3] = [
+    0xFF, 0xFF, 0xFF,
+    0xDD, 0xDD, 0xDD,
+    0xBB, 0xBB, 0xBB,
+    0x99, 0x99, 0x99,
+    0x77, 0x77, 0x77,
+    0x55, 0x55, 0x55,
+    0x33, 0x33, 0x33,
+    0x00, 0x00, 0x00,
+    0x00, 0x44, 0x99,
+    0xEE, 0xEE, 0x00,
+    0x00, 0xCC, 0x00,
+    0xDD, 0x00, 0x00,
+    0xEE, 0xEE, 0xBB,
+    0x55, 0x88, 0x00,
+    0xFF, 0xBB, 0x00,
+    0x00, 0xBB, 0xFF
+];
index 48d8a3e16d2503f3263d922f3b16bf4ec7beae30..5503d1ae721106cca0146ceebff9633eb3abb63c 100644 (file)
@@ -38,6 +38,9 @@ mod linepack;
 #[cfg(feature="decoder_escape")]
 mod escape;
 
+#[cfg(feature="decoder_euclid")]
+mod euclid;
+
 #[cfg(feature="decoder_rawaudio")]
 mod rawaudio;
 
@@ -65,6 +68,9 @@ const ACORN_CODECS: &[DecoderInfo] = &[
 #[cfg(feature="decoder_escape")]
     DecoderInfo { name: "escape-adpcm", get_decoder: escape::get_decoder_audio },
 
+#[cfg(feature="decoder_euclid")]
+    DecoderInfo { name: "euclid", get_decoder: euclid::get_decoder },
+
 #[cfg(feature="decoder_rawaudio")]
     DecoderInfo { name: "arm_rawaudio", get_decoder: rawaudio::get_decoder },
 ];
index 2acc820dc0c6d1e976178d17d6760ca8a4a57354..69db3698d5a4eb806021ff2f517e95faf0b88264 100644 (file)
@@ -11,6 +11,14 @@ macro_rules! validate {
     ($a:expr) => { if !$a { return Err(DemuxerError::InvalidData); } };
 }
 
+#[cfg(feature="demuxer_tca")]
+mod tca;
+
+const DEMUXERS: &[&dyn DemuxerCreator] = &[
+#[cfg(feature="demuxer_tca")]
+    &tca::TCADemuxerCreator {},
+];
+
 #[cfg(feature="demuxer_armovie")]
 mod armovie;
 
@@ -19,6 +27,13 @@ const RAW_DEMUXERS: &[&dyn RawDemuxerCreator] = &[
     &armovie::ARMovieDemuxerCreator {},
 ];
 
+/// Registers all available demuxers provided by this crate.
+pub fn acorn_register_all_demuxers(rd: &mut RegisteredDemuxers) {
+    for demuxer in DEMUXERS.iter() {
+        rd.add_demuxer(*demuxer);
+    }
+}
+
 /// Registers all available demuxers provided by this crate.
 pub fn acorn_register_all_raw_demuxers(rd: &mut RegisteredRawDemuxers) {
     for demuxer in RAW_DEMUXERS.iter() {
diff --git a/nihav-acorn/src/demuxers/tca.rs b/nihav-acorn/src/demuxers/tca.rs
new file mode 100644 (file)
index 0000000..300b68a
--- /dev/null
@@ -0,0 +1,147 @@
+use nihav_core::demuxers::*;
+
+struct TCADemuxer<'a> {
+    src:        &'a mut ByteReader<'a>,
+    frameno:    u64,
+    data_end:   u64,
+}
+
+impl<'a> TCADemuxer<'a> {
+    fn new(src: &'a mut ByteReader<'a>) -> Self {
+        Self {
+            src,
+            frameno:    0,
+            data_end:   0,
+        }
+    }
+}
+
+impl<'a> DemuxCore<'a> for TCADemuxer<'a> {
+    fn open(&mut self, strmgr: &mut StreamManager, _seek_index: &mut SeekIndex) -> DemuxerResult<()> {
+        let tag                 = self.src.peek_tag()?;
+        let is_acef = &tag == b"ACEF";
+        let acef_size = if is_acef {
+                                      self.src.read_skip(4)?;
+                                      self.src.read_u32le()?
+            } else { 0 };
+        let size2                   = self.src.read_u32le()?;
+        if is_acef {
+            validate!(acef_size > 0x44 && size2 + 8 <= acef_size);
+        }
+        self.data_end = u64::from(size2 + 8);
+
+                                      self.src.read_skip(12)?;
+
+        const HDR_SIZE: usize = 0x30;
+
+        let mut hdr = vec![0; HDR_SIZE + 4];
+        write_u32le(&mut hdr, HDR_SIZE as u32 + 4)?;
+                                      self.src.read_buf(&mut hdr[4..])?;
+
+        let width  = read_u32le(&hdr[8..])? as usize;
+        let height = read_u32le(&hdr[12..])? as usize;
+        validate!(width > 0 && height > 0);
+        validate!((width | height) & 1 == 0);
+
+        if is_acef {
+            let data_start = self.src.tell();
+
+            // scan tail for palette and such
+            if self.src.seek(SeekFrom::Start(u64::from(acef_size))).is_ok() {
+                while let Ok(tag)   = self.src.read_tag() {
+                    let size        = self.src.read_u32le()? as usize;
+                    validate!(size >= 8);
+                    if &tag == b"PALE" {
+                        validate!((0x28..=0x428).contains(&size) && (size & 3) == 0);
+                                      self.src.read_skip(0x1C)?;
+                        let nclrs = (size - 0x24) / 4;
+                        hdr.resize(HDR_SIZE + 4 + 256 * 3, 0);
+                        for _ in 0..nclrs {
+                            let idx = usize::from(self.src.read_byte()?);
+                                      self.src.read_buf(&mut hdr[HDR_SIZE + 4 + idx * 3..][..3])?;
+                        }
+                    } else {
+                                      self.src.read_skip(size - 8)?;
+                    }
+                }
+            }
+                                      self.src.seek(SeekFrom::Start(data_start))?;
+        }
+
+        let vci = NACodecTypeInfo::Video(NAVideoInfo::new(width / 2, height / 2, false, PAL8_FORMAT));
+        let vinfo = NACodecInfo::new("euclid", vci, Some(hdr));
+        let ret = strmgr.add_stream(NAStream::new(StreamType::Video, 0, vinfo, 1, 10, 0));
+        if ret.is_none() {
+            return Err(DemuxerError::MemoryError);
+        }
+
+        Ok(())
+    }
+
+    fn get_frame(&mut self, strmgr: &mut StreamManager) -> DemuxerResult<NAPacket> {
+        if self.src.tell() >= self.data_end {
+            return Err(DemuxerError::EOF);
+        }
+        let fsize               = self.src.read_u32le()? as usize;
+        if fsize == 0 {
+            return Err(DemuxerError::EOF);
+        }
+        validate!((9..=1048576).contains(&fsize));
+        if let Some(stream) = strmgr.get_stream(0) {
+            let ts = stream.make_ts(Some(self.frameno), None, None);
+            self.frameno += 1;
+            // last word is the current packet size for backwards seeking so it can be omitted
+            self.src.read_packet(stream, ts, false, fsize - 4)
+        } else {
+            Err(DemuxerError::InvalidData)
+        }
+    }
+
+    fn seek(&mut self, _time: NATimePoint, _seek_index: &SeekIndex) -> DemuxerResult<()> {
+        Err(DemuxerError::NotPossible)
+    }
+    fn get_duration(&self) -> u64 { 0 }
+}
+
+impl<'a> NAOptionHandler for TCADemuxer<'a> {
+    fn get_supported_options(&self) -> &[NAOptionDefinition] { &[] }
+    fn set_options(&mut self, _options: &[NAOption]) { }
+    fn query_option_value(&self, _name: &str) -> Option<NAValue> { None }
+}
+
+pub struct TCADemuxerCreator { }
+
+impl DemuxerCreator for TCADemuxerCreator {
+    fn new_demuxer<'a>(&self, br: &'a mut ByteReader<'a>) -> Box<dyn DemuxCore<'a> + 'a> {
+        Box::new(TCADemuxer::new(br))
+    }
+    fn get_name(&self) -> &'static str { "tca" }
+}
+
+#[cfg(test)]
+mod test {
+    use super::*;
+    use std::fs::File;
+
+    #[test]
+    fn test_tca_demux() {
+        // a sample from RISC DISC 3
+        let mut file = File::open("assets/Acorn/JUMPER").unwrap();
+        let mut fr = FileReader::new_read(&mut file);
+        let mut br = ByteReader::new(&mut fr);
+        let mut dmx = TCADemuxer::new(&mut br);
+        let mut sm = StreamManager::new();
+        let mut si = SeekIndex::new();
+        dmx.open(&mut sm, &mut si).unwrap();
+
+        loop {
+            match dmx.get_frame(&mut sm) {
+                Ok(pkt) => {
+                    println!("Got {pkt}");
+                },
+                Err(DemuxerError::EOF) => return,
+                Err(_) => panic!("error"),
+            }
+        }
+    }
+}
index 0a99f67cae2500782de3f81e46769d87c91d4415..9666f8fa02dd9fa2a552553d4e6365bfd187586a 100644 (file)
@@ -8,4 +8,5 @@ mod demuxers;
 
 pub use crate::codecs::acorn_register_all_decoders;
 pub use crate::codecs::acorn_register_all_packetisers;
+pub use crate::demuxers::acorn_register_all_demuxers;
 pub use crate::demuxers::acorn_register_all_raw_demuxers;
\ No newline at end of file
index 89a608fde07fce9ddafcf8c50b95c79b30ca5ec0..08bca9303441d32744ab845e11137eb5612a9955 100644 (file)
@@ -56,6 +56,7 @@ pub fn nihav_register_all_packetisers(rp: &mut RegisteredPacketisers) {
 
 /// Registers all known demuxers.
 pub fn nihav_register_all_demuxers(rd: &mut RegisteredDemuxers) {
+    acorn_register_all_demuxers(rd);
     duck_register_all_demuxers(rd);
     generic_register_all_demuxers(rd);
     flash_register_all_demuxers(rd);
index fcfafefc1eb89debbf31c79dd77914b7f7cb94f0..be5b509d5b22a8071c49f593879e23d45e210ccf 100644 (file)
@@ -261,6 +261,12 @@ const DETECTORS: &[DetectConditions] = &[
         extensions: ".rpl",
         conditions: &[CheckItem{offs:  0, cond: &CC::Str(b"ARMovie\n") }],
     },
+    DetectConditions {
+        demux_name: "tca",
+        extensions: ".tca",
+        conditions: &[CheckItem{offs: 0x00, cond: &CC::Str(b"ACEF") },
+                      CheckItem{offs: 0x18, cond: &CC::Eq(Arg::U32LE(64))}],
+    },
     DetectConditions {
         demux_name: "flv",
         extensions: ".flv",
index 383ecd99df1ce45bd088a7b7608a9ffb865e7b38..eea28613ca4729d7e0d93e1e3e87de5174133886 100644 (file)
@@ -222,6 +222,7 @@ static CODEC_REGISTER: &[CodecDescription] = &[
     desc!(video;    "escape124",            "Eidos Escape 124"),
     desc!(video;    "escape130",            "Eidos Escape 130"),
     desc!(audio;    "escape-adpcm",         "Eidos Escape ADPCM"),
+    desc!(video-llp; "euclid",              "Iota Euclid / The Complete Animation"),
 
     desc!(video;    "truemotion1",   "TrueMotion 1"),
     desc!(video-im; "truemotionrt",  "TrueMotion RT"),