From ff6a914f5b07a9c62d7624fb6dd9f1fc093e82ca Mon Sep 17 00:00:00 2001 From: Kostya Shishkov Date: Sat, 15 Oct 2022 10:39:24 +0200 Subject: [PATCH] add Highlander FMV support --- nihav-game/Cargo.toml | 6 +- nihav-game/src/codecs/hl_fmv.rs | 516 ++++++++++++++++++++++++++++++ nihav-game/src/codecs/mod.rs | 4 + nihav-game/src/demuxers/hl_fmv.rs | 115 +++++++ nihav-game/src/demuxers/mod.rs | 4 + nihav-registry/src/detect.rs | 6 + nihav-registry/src/register.rs | 1 + 7 files changed, 650 insertions(+), 2 deletions(-) create mode 100644 nihav-game/src/codecs/hl_fmv.rs create mode 100644 nihav-game/src/demuxers/hl_fmv.rs diff --git a/nihav-game/Cargo.toml b/nihav-game/Cargo.toml index 3811364..eb0f1fd 100644 --- a/nihav-game/Cargo.toml +++ b/nihav-game/Cargo.toml @@ -18,12 +18,13 @@ nihav_commonfmt = { path = "../nihav-commonfmt", default-features=false, feature [features] default = ["all_decoders", "all_demuxers"] demuxers = [] -all_demuxers = ["demuxer_bmv", "demuxer_bmv3", "demuxer_fcmp", "demuxer_fst", "demuxer_gdv", "demuxer_imax", "demuxer_q", "demuxer_smush", "demuxer_vmd", "demuxer_vx"] +all_demuxers = ["demuxer_bmv", "demuxer_bmv3", "demuxer_fcmp", "demuxer_fst", "demuxer_gdv", "demuxer_hl_fmv", "demuxer_imax", "demuxer_q", "demuxer_smush", "demuxer_vmd", "demuxer_vx"] demuxer_bmv = ["demuxers"] demuxer_bmv3 = ["demuxers"] demuxer_fcmp = ["demuxers"] demuxer_fst = ["demuxers"] demuxer_gdv = ["demuxers"] +demuxer_hl_fmv = ["demuxers"] demuxer_imax = ["demuxers"] demuxer_q = ["demuxers"] demuxer_smush = ["demuxers"] @@ -33,11 +34,12 @@ demuxer_vx = ["demuxers"] all_decoders = ["all_video_decoders", "all_audio_decoders"] decoders = [] -all_video_decoders = ["decoder_bmv", "decoder_bmv3", "decoder_fstvid", "decoder_gdvvid", "decoder_imax", "decoder_ipma", "decoder_midivid", "decoder_midivid3", "decoder_q", "decoder_smush_video", "decoder_vmd", "decoder_vx"] +all_video_decoders = ["decoder_bmv", "decoder_bmv3", "decoder_fstvid", "decoder_gdvvid", "decoder_hl_fmv", "decoder_imax", "decoder_ipma", "decoder_midivid", "decoder_midivid3", "decoder_q", "decoder_smush_video", "decoder_vmd", "decoder_vx"] decoder_bmv = ["decoders"] decoder_bmv3 = ["decoders"] decoder_fstvid = ["decoders"] decoder_gdvvid = ["decoders"] +decoder_hl_fmv = ["decoders"] decoder_imax = ["decoders"] decoder_ipma = ["decoders"] decoder_midivid = ["decoders"] diff --git a/nihav-game/src/codecs/hl_fmv.rs b/nihav-game/src/codecs/hl_fmv.rs new file mode 100644 index 0000000..426d144 --- /dev/null +++ b/nihav-game/src/codecs/hl_fmv.rs @@ -0,0 +1,516 @@ +use nihav_core::codecs::*; +use nihav_core::io::byteio::*; + +const FRAME_W: usize = 320; +const FRAME_H: usize = 240; +const HIST_SIZE: usize = 32768; + +struct Pattern { + len: u8, + pattern: [u8; 16], +} + +struct HighlanderDecoder { + info: NACodecInfoRef, + hist: [u8; HIST_SIZE], + tmp1: [u8; FRAME_W * FRAME_H], + tmp2: [u8; FRAME_W * FRAME_H * 17 / 16], +} + +fn unpack(src: &[u8], dst: &mut [u8], hist: &mut [u8; HIST_SIZE]) -> DecoderResult { + let mut mr = MemoryReader::new_read(src); + let mut br = ByteReader::new(&mut mr); + + let mut mw = MemoryWriter::new_write(dst); + let mut bw = ByteWriter::new(&mut mw); + + *hist = [0; HIST_SIZE]; + + let mut pprev = 0; + let mut prev = 0; + while br.left() > 0 { + let mut flags = br.read_byte()?; + for _ in 0..8 { + let idx = (usize::from(pprev) << 7) ^ usize::from(prev); + if (flags & 1) == 0 { + if br.left() == 0 { + break; + } + hist[idx] = br.read_byte()?; + } + let val = hist[idx]; + bw.write_byte(val)?; + + flags >>= 1; + pprev = prev; + prev = val; + } + } + + Ok(bw.tell() as usize) +} + +fn paint_frame(dst: &mut [u8], stride: usize, src: &[u8]) -> DecoderResult<()> { + let mut mr = MemoryReader::new_read(src); + let mut br = ByteReader::new(&mut mr); + + let mut blk_offs = [0; 16]; + for (y, offs) in blk_offs.chunks_mut(4).enumerate() { + offs[0] = stride * y; + offs[1] = stride * y + 1; + offs[2] = stride * y + 2; + offs[3] = stride * y + 3; + } + + for row in dst.chunks_mut(stride * 4).take(FRAME_H / 4) { + for xoff in (0..FRAME_W).step_by(4) { + let idx = br.read_byte()? as usize; + validate!(idx < PAINT_MODE.len()); + let mode = &PAINT_MODE[idx]; + validate!(i64::from(mode.len) <= br.left()); + + for (&blk_off, &idx) in blk_offs.iter().zip(mode.pattern.iter()) { + if idx == 0xFF { + row[xoff + blk_off] = br.read_byte()?; + } + } + for (&blk_off, &idx) in blk_offs.iter().zip(mode.pattern.iter()) { + if idx != 0xFF { + row[xoff + blk_off] = row[xoff + blk_offs[idx as usize]]; + } + } + } + } + Ok(()) +} + +impl HighlanderDecoder { + fn new() -> Self { + Self { + info: NACodecInfoRef::default(), + hist: [0; HIST_SIZE], + tmp1: [0; FRAME_W * FRAME_H], + tmp2: [0; FRAME_W * FRAME_H * 17 / 16], + } + } +} + +impl NADecoder for HighlanderDecoder { + fn init(&mut self, _supp: &mut NADecoderSupport, info: NACodecInfoRef) -> DecoderResult<()> { + if let NACodecTypeInfo::Video(_vinfo) = info.get_properties() { + let myinfo = NACodecTypeInfo::Video(NAVideoInfo::new(FRAME_W, FRAME_H, 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 { + let src = pkt.get_buffer(); + validate!(src.len() > 4); + + let bufinfo = alloc_video_buffer(self.info.get_properties().get_video_info().unwrap(), 0)?; + let bufo = bufinfo.get_vbuf(); + let mut buf = bufo.unwrap(); + let paloff = buf.get_offset(1); + let stride = buf.get_stride(0); + let data = buf.get_data_mut().unwrap(); + let dst = data.as_mut_slice(); + + validate!(src.len() > 4); + + let size = read_u32le(&src)? as usize; + validate!(size <= src.len() - 4); + + let size2 = unpack(&src[4..][..size], &mut self.tmp1, &mut self.hist)?; + let size3 = unpack(&self.tmp1[..size2], &mut self.tmp2, &mut self.hist)?; + paint_frame(dst, stride, &self.tmp2[..size3])?; + + let dpal = &mut dst[paloff..][..768]; + for (dst, &src) in dpal.iter_mut().zip(DEFAULT_PAL.iter()) { + *dst = (src << 2) | (src >> 4); + } + + let mut frm = NAFrame::new_from_pkt(pkt, self.info.clone(), bufinfo); + frm.set_keyframe(true); + frm.set_frame_type(FrameType::I); + Ok(frm.into_ref()) + } + fn flush(&mut self) { + } +} + +impl NAOptionHandler for HighlanderDecoder { + fn get_supported_options(&self) -> &[NAOptionDefinition] { &[] } + fn set_options(&mut self, _options: &[NAOption]) { } + fn query_option_value(&self, _name: &str) -> Option { None } +} + + +pub fn get_decoder() -> Box { + Box::new(HighlanderDecoder::new()) +} + +#[cfg(test)] +mod test { + use nihav_core::codecs::RegisteredDecoders; + use nihav_core::demuxers::RegisteredDemuxers; + use nihav_codec_support::test::dec_video::*; + use crate::game_register_all_decoders; + use crate::game_register_all_demuxers; + // sample extracted from Highlander: The Last of the MacLeods unpublished game + #[test] + fn test_hl_fmv_video() { + let mut dmx_reg = RegisteredDemuxers::new(); + game_register_all_demuxers(&mut dmx_reg); + let mut dec_reg = RegisteredDecoders::new(); + game_register_all_decoders(&mut dec_reg); + + test_decoding("hl-fmv", "hl-fmv-video", "assets/Game/0260.fmv", Some(10), &dmx_reg, &dec_reg, + ExpectedTestResult::MD5([0x369659f0, 0x417ad3a7, 0xc62dfc6f, 0x6e5fe871])); + } +} + +const PAINT_MODE: [Pattern; 9] = [ + Pattern { + len: 1, + pattern: [ + 0xFF, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00 + ], + }, + Pattern { + len: 2, + pattern: [ + 0x05, 0x09, 0x05, 0x09, + 0x09, 0xFF, 0x09, 0x05, + 0x05, 0xFF, 0x05, 0x09, + 0x09, 0x05, 0x09, 0x05 + ], + }, + Pattern { + len: 2, + pattern: [ + 0xFF, 0xFF, 0x00, 0x01, + 0x01, 0x01, 0x01, 0x01, + 0x00, 0x01, 0x00, 0x01, + 0x01, 0x01, 0x01, 0x01 + ] + }, + Pattern { + len: 2, + pattern: [ + 0xFF, 0xFF, 0x00, 0x01, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x01, 0x00, 0x01, + 0x00, 0x00, 0x00, 0x00 + ], + }, + Pattern { + len: 2, + pattern: [ + 0x0E, 0x0E, 0x0E, 0x0E, + 0x0E, 0x0F, 0x0E, 0x0F, + 0x0E, 0x0E, 0x0E, 0x0E, + 0x0E, 0x0F, 0xFF, 0xFF + ], + }, + Pattern { + len: 2, + pattern: [ + 0x0F, 0x0F, 0x0F, 0x0F, + 0x0E, 0x0F, 0x0E, 0x0F, + 0x0F, 0x0F, 0x0F, 0x0F, + 0x0E, 0x0F, 0xFF, 0xFF + ], + }, + Pattern { + len: 5, + pattern: [ + 0xFF, 0xFF, 0x00, 0xFF, + 0x01, 0xFF, 0x01, 0xFF, + 0x00, 0x01, 0x00, 0x03, + 0x03, 0x07, 0x03, 0x07 + ], + }, + Pattern { + len: 8, + pattern: [ + 0xFF, 0xFF, 0xFF, 0xFF, + 0x01, 0x00, 0x03, 0x02, + 0xFF, 0xFF, 0xFF, 0xFF, + 0x09, 0x08, 0x0B, 0x0A + ], + }, + Pattern { + len: 16, + pattern: [ + 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF + ], + } +]; + +const DEFAULT_PAL: [u8; 768] = [ + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x20, + 0x00, 0x20, 0x00, + 0x00, 0x20, 0x20, + 0x20, 0x00, 0x00, + 0x20, 0x00, 0x20, + 0x20, 0x20, 0x00, + 0x30, 0x30, 0x30, + 0x30, 0x37, 0x30, + 0x3C, 0x32, 0x29, + 0x01, 0x01, 0x01, + 0x02, 0x02, 0x02, + 0x03, 0x03, 0x03, + 0x04, 0x04, 0x04, + 0x05, 0x05, 0x05, + 0x07, 0x07, 0x07, + 0x08, 0x08, 0x08, + 0x0A, 0x0A, 0x0A, + 0x15, 0x15, 0x15, + 0x13, 0x13, 0x13, + 0x10, 0x10, 0x10, + 0x0E, 0x0E, 0x0E, + 0x20, 0x20, 0x20, + 0x00, 0x00, 0x20, + 0x00, 0x20, 0x00, + 0x00, 0x20, 0x20, + 0x20, 0x00, 0x00, + 0x20, 0x00, 0x20, + 0x20, 0x20, 0x00, + 0x00, 0x00, 0x0C, + 0x00, 0x00, 0x19, + 0x00, 0x00, 0x26, + 0x00, 0x00, 0x33, + 0x00, 0x0C, 0x00, + 0x00, 0x0C, 0x0C, + 0x00, 0x0C, 0x19, + 0x00, 0x0C, 0x26, + 0x00, 0x0C, 0x33, + 0x00, 0x0C, 0x3F, + 0x00, 0x19, 0x00, + 0x00, 0x19, 0x0C, + 0x00, 0x19, 0x19, + 0x00, 0x19, 0x26, + 0x00, 0x19, 0x33, + 0x00, 0x19, 0x3F, + 0x00, 0x26, 0x00, + 0x00, 0x26, 0x0C, + 0x00, 0x26, 0x19, + 0x00, 0x26, 0x26, + 0x00, 0x26, 0x33, + 0x00, 0x26, 0x3F, + 0x00, 0x33, 0x00, + 0x00, 0x33, 0x0C, + 0x00, 0x33, 0x19, + 0x00, 0x33, 0x26, + 0x00, 0x33, 0x33, + 0x00, 0x33, 0x3F, + 0x00, 0x3F, 0x19, + 0x00, 0x3F, 0x26, + 0x00, 0x3F, 0x33, + 0x0C, 0x00, 0x00, + 0x0C, 0x00, 0x0C, + 0x0C, 0x00, 0x19, + 0x0C, 0x00, 0x26, + 0x0C, 0x00, 0x33, + 0x0C, 0x00, 0x3F, + 0x0C, 0x0C, 0x00, + 0x0C, 0x0C, 0x0C, + 0x0C, 0x0C, 0x19, + 0x0C, 0x0C, 0x26, + 0x0C, 0x0C, 0x33, + 0x0C, 0x0C, 0x3F, + 0x0C, 0x19, 0x00, + 0x0C, 0x19, 0x0C, + 0x0C, 0x19, 0x19, + 0x0C, 0x19, 0x26, + 0x0C, 0x19, 0x33, + 0x0C, 0x19, 0x3F, + 0x0C, 0x26, 0x00, + 0x0C, 0x26, 0x0C, + 0x0C, 0x26, 0x19, + 0x0C, 0x26, 0x26, + 0x0C, 0x26, 0x33, + 0x0C, 0x26, 0x3F, + 0x0C, 0x33, 0x00, + 0x0C, 0x33, 0x0C, + 0x0C, 0x33, 0x19, + 0x0C, 0x33, 0x26, + 0x0C, 0x33, 0x33, + 0x0C, 0x33, 0x3F, + 0x0C, 0x3F, 0x0C, + 0x0C, 0x3F, 0x19, + 0x0C, 0x3F, 0x26, + 0x0C, 0x3F, 0x33, + 0x0C, 0x3F, 0x3F, + 0x19, 0x00, 0x00, + 0x19, 0x00, 0x0C, + 0x19, 0x00, 0x19, + 0x19, 0x00, 0x26, + 0x19, 0x00, 0x33, + 0x19, 0x00, 0x3F, + 0x19, 0x0C, 0x00, + 0x19, 0x0C, 0x0C, + 0x19, 0x0C, 0x19, + 0x19, 0x0C, 0x26, + 0x19, 0x0C, 0x33, + 0x19, 0x0C, 0x3F, + 0x19, 0x19, 0x00, + 0x19, 0x19, 0x0C, + 0x19, 0x19, 0x19, + 0x19, 0x19, 0x26, + 0x19, 0x19, 0x33, + 0x19, 0x26, 0x00, + 0x19, 0x26, 0x0C, + 0x19, 0x26, 0x19, + 0x19, 0x26, 0x26, + 0x19, 0x26, 0x33, + 0x19, 0x26, 0x3F, + 0x19, 0x33, 0x00, + 0x19, 0x33, 0x0C, + 0x19, 0x33, 0x26, + 0x19, 0x33, 0x33, + 0x19, 0x33, 0x3F, + 0x19, 0x3F, 0x00, + 0x19, 0x3F, 0x0C, + 0x19, 0x3F, 0x26, + 0x19, 0x3F, 0x33, + 0x33, 0x00, 0x3F, + 0x3F, 0x00, 0x33, + 0x26, 0x26, 0x00, + 0x26, 0x0C, 0x26, + 0x26, 0x00, 0x26, + 0x26, 0x00, 0x33, + 0x26, 0x00, 0x00, + 0x26, 0x0C, 0x0C, + 0x26, 0x00, 0x19, + 0x26, 0x0C, 0x33, + 0x26, 0x00, 0x3F, + 0x26, 0x19, 0x00, + 0x26, 0x19, 0x0C, + 0x26, 0x0C, 0x19, + 0x26, 0x19, 0x26, + 0x26, 0x19, 0x33, + 0x26, 0x0C, 0x3F, + 0x26, 0x26, 0x0C, + 0x26, 0x26, 0x19, + 0x26, 0x26, 0x26, + 0x26, 0x26, 0x33, + 0x26, 0x26, 0x3F, + 0x26, 0x33, 0x00, + 0x26, 0x33, 0x0C, + 0x19, 0x33, 0x19, + 0x26, 0x33, 0x26, + 0x26, 0x33, 0x33, + 0x26, 0x33, 0x3F, + 0x26, 0x3F, 0x00, + 0x26, 0x3F, 0x0C, + 0x26, 0x33, 0x19, + 0x26, 0x3F, 0x26, + 0x26, 0x3F, 0x33, + 0x26, 0x3F, 0x3F, + 0x33, 0x00, 0x00, + 0x26, 0x00, 0x0C, + 0x33, 0x00, 0x19, + 0x33, 0x00, 0x26, + 0x33, 0x00, 0x33, + 0x26, 0x0C, 0x00, + 0x33, 0x0C, 0x0C, + 0x33, 0x0C, 0x19, + 0x33, 0x0C, 0x26, + 0x33, 0x0C, 0x33, + 0x33, 0x0C, 0x3F, + 0x33, 0x19, 0x00, + 0x33, 0x19, 0x0C, + 0x26, 0x19, 0x19, + 0x33, 0x19, 0x26, + 0x33, 0x19, 0x33, + 0x26, 0x19, 0x3F, + 0x33, 0x26, 0x00, + 0x33, 0x26, 0x0C, + 0x33, 0x26, 0x19, + 0x33, 0x26, 0x26, + 0x33, 0x26, 0x33, + 0x33, 0x26, 0x3F, + 0x33, 0x33, 0x00, + 0x33, 0x33, 0x0C, + 0x33, 0x33, 0x19, + 0x33, 0x33, 0x26, + 0x33, 0x33, 0x33, + 0x33, 0x33, 0x3F, + 0x33, 0x3F, 0x00, + 0x33, 0x3F, 0x0C, + 0x26, 0x3F, 0x19, + 0x33, 0x3F, 0x26, + 0x33, 0x3F, 0x33, + 0x33, 0x3F, 0x3F, + 0x33, 0x00, 0x0C, + 0x3F, 0x00, 0x19, + 0x3F, 0x00, 0x26, + 0x33, 0x0C, 0x00, + 0x3F, 0x0C, 0x0C, + 0x3F, 0x0C, 0x19, + 0x3F, 0x0C, 0x26, + 0x3F, 0x0C, 0x33, + 0x3F, 0x0C, 0x3F, + 0x3F, 0x19, 0x00, + 0x3F, 0x19, 0x0C, + 0x33, 0x19, 0x19, + 0x3F, 0x19, 0x26, + 0x3F, 0x19, 0x33, + 0x33, 0x19, 0x3F, + 0x3F, 0x26, 0x00, + 0x3F, 0x26, 0x0C, + 0x3F, 0x26, 0x19, + 0x3F, 0x26, 0x26, + 0x3F, 0x26, 0x33, + 0x3F, 0x26, 0x3F, + 0x3F, 0x33, 0x00, + 0x3F, 0x33, 0x0C, + 0x3F, 0x33, 0x19, + 0x3F, 0x33, 0x26, + 0x3F, 0x33, 0x33, + 0x3F, 0x33, 0x3F, + 0x3F, 0x3F, 0x0C, + 0x33, 0x3F, 0x19, + 0x3F, 0x3F, 0x26, + 0x3F, 0x3F, 0x33, + 0x19, 0x19, 0x3F, + 0x19, 0x3F, 0x19, + 0x19, 0x3F, 0x3F, + 0x3F, 0x19, 0x19, + 0x3F, 0x19, 0x3F, + 0x3F, 0x3F, 0x19, + 0x30, 0x30, 0x30, + 0x17, 0x17, 0x17, + 0x1D, 0x1D, 0x1D, + 0x21, 0x21, 0x21, + 0x25, 0x25, 0x25, + 0x32, 0x32, 0x32, + 0x2C, 0x2C, 0x2C, + 0x35, 0x35, 0x35, + 0x37, 0x37, 0x37, + 0x38, 0x38, 0x38, + 0x3A, 0x3A, 0x3A, + 0x3C, 0x3C, 0x3C, + 0x3E, 0x3E, 0x3E, + 0x3C, 0x3E, 0x3F, + 0x29, 0x28, 0x28, + 0x20, 0x20, 0x20, + 0x00, 0x00, 0x3F, + 0x00, 0x3F, 0x00, + 0x00, 0x3F, 0x3F, + 0x3F, 0x00, 0x00, + 0x3F, 0x00, 0x3F, + 0x3F, 0x3F, 0x00, + 0x3F, 0x3F, 0x3F +]; diff --git a/nihav-game/src/codecs/mod.rs b/nihav-game/src/codecs/mod.rs index c75e263..ff87adf 100644 --- a/nihav-game/src/codecs/mod.rs +++ b/nihav-game/src/codecs/mod.rs @@ -13,6 +13,8 @@ pub mod bmv3; pub mod futurevision; #[cfg(feature="decoder_gdvvid")] pub mod gremlinvideo; +#[cfg(feature="decoder_hl_fmv")] +pub mod hl_fmv; #[cfg(feature="decoder_imax")] pub mod imax; #[cfg(feature="decoder_ipma")] @@ -51,6 +53,8 @@ const GAME_CODECS: &[DecoderInfo] = &[ DecoderInfo { name: "fst-audio", get_decoder: futurevision::get_decoder_audio }, #[cfg(feature="decoder_fstvid")] DecoderInfo { name: "fst-video", get_decoder: futurevision::get_decoder_video }, +#[cfg(feature="decoder_hl_fmv")] + DecoderInfo { name: "hl-fmv-video", get_decoder: hl_fmv::get_decoder }, #[cfg(feature="decoder_imax")] DecoderInfo { name: "fable-imax", get_decoder: imax::get_decoder }, #[cfg(feature="decoder_ipma")] diff --git a/nihav-game/src/demuxers/hl_fmv.rs b/nihav-game/src/demuxers/hl_fmv.rs new file mode 100644 index 0000000..b511bb9 --- /dev/null +++ b/nihav-game/src/demuxers/hl_fmv.rs @@ -0,0 +1,115 @@ +use nihav_core::frame::*; +use nihav_core::demuxers::*; + +#[allow(dead_code)] +struct HighlanderFMVDemuxer<'a> { + src: &'a mut ByteReader<'a>, + vpts: u64, + apts: u64, +} + +impl<'a> DemuxCore<'a> for HighlanderFMVDemuxer<'a> { + #[allow(unused_variables)] + fn open(&mut self, strmgr: &mut StreamManager, _seek_index: &mut SeekIndex) -> DemuxerResult<()> { + let src = &mut self.src; + + let tag = src.read_tag()?; + validate!(&tag == b"FMV*"); + let size = src.read_u32le()?; + validate!(size == 0); + + let vhdr = NAVideoInfo::new(320, 240, false, PAL8_FORMAT); + let vci = NACodecTypeInfo::Video(vhdr); + let vinfo = NACodecInfo::new("hl-fmv-video", vci, None); + if strmgr.add_stream(NAStream::new(StreamType::Video, 0, vinfo, 2, 25, 0)).is_none() { + return Err(DemuxerError::MemoryError); + } + let ahdr = NAAudioInfo::new(22050, 1, SND_U8_FORMAT, 1); + let ainfo = NACodecInfo::new("pcm", NACodecTypeInfo::Audio(ahdr), None); + if strmgr.add_stream(NAStream::new(StreamType::Audio, 1, ainfo, 1, 22050, 0)).is_none() { + return Err(DemuxerError::MemoryError); + } + + self.apts = 0; + self.vpts = 0; + Ok(()) + } + + fn get_frame(&mut self, strmgr: &mut StreamManager) -> DemuxerResult { + let tag = self.src.read_tag()?; + let size = self.src.read_u32le()? as usize; + match &tag { + b"AUD1" => { + let stream = strmgr.get_stream_by_id(1).unwrap(); + let (tb_num, tb_den) = stream.get_timebase(); + let ts = NATimeInfo::new(Some(self.apts), None, None, tb_num, tb_den); + self.apts += size as u64; + self.src.read_packet(stream, ts, true, size) + }, + b"VID3" => { + let stream = strmgr.get_stream_by_id(0).unwrap(); + let (tb_num, tb_den) = stream.get_timebase(); + let ts = NATimeInfo::new(Some(self.vpts), None, None, tb_num, tb_den); + self.vpts += 1; + self.src.read_packet(stream, ts, true, size) + }, + b"END*" => Err(DemuxerError::EOF), + _ => 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 HighlanderFMVDemuxer<'a> { + fn get_supported_options(&self) -> &[NAOptionDefinition] { &[] } + fn set_options(&mut self, _options: &[NAOption]) { } + fn query_option_value(&self, _name: &str) -> Option { None } +} +impl<'a> HighlanderFMVDemuxer<'a> { + fn new(io: &'a mut ByteReader<'a>) -> Self { + HighlanderFMVDemuxer { + src: io, + vpts: 0, + apts: 0, + } + } +} + +pub struct HighlanderFMVDemuxerCreator { } + +impl DemuxerCreator for HighlanderFMVDemuxerCreator { + fn new_demuxer<'a>(&self, br: &'a mut ByteReader<'a>) -> Box + 'a> { + Box::new(HighlanderFMVDemuxer::new(br)) + } + fn get_name(&self) -> &'static str { "hl-fmv" } +} + +#[cfg(test)] +mod test { + use super::*; + use std::fs::File; + + // sample extracted from Highlander: The Last of the MacLeods unpublished game + #[test] + fn test_highlander_fmv_demux() { + let mut file = File::open("assets/Game/0010.fmv").unwrap(); + let mut fr = FileReader::new_read(&mut file); + let mut br = ByteReader::new(&mut fr); + let mut dmx = HighlanderFMVDemuxer::new(&mut br); + let mut sm = StreamManager::new(); + let mut si = SeekIndex::new(); + dmx.open(&mut sm, &mut si).unwrap(); + loop { + let pktres = dmx.get_frame(&mut sm); + if let Err(e) = pktres { + if (e as i32) == (DemuxerError::EOF as i32) { break; } + panic!("error"); + } + let pkt = pktres.unwrap(); + println!("Got {}", pkt); + } + } +} diff --git a/nihav-game/src/demuxers/mod.rs b/nihav-game/src/demuxers/mod.rs index 943ca54..ca85790 100644 --- a/nihav-game/src/demuxers/mod.rs +++ b/nihav-game/src/demuxers/mod.rs @@ -11,6 +11,8 @@ mod bmv; mod fst; #[cfg(feature="demuxer_gdv")] mod gdv; +#[cfg(feature="demuxer_hl_fmv")] +mod hl_fmv; #[cfg(feature="demuxer_imax")] mod imax; #[cfg(feature="demuxer_q")] @@ -33,6 +35,8 @@ const GAME_DEMUXERS: &[&dyn DemuxerCreator] = &[ &fst::FSTDemuxerCreator {}, #[cfg(feature="demuxer_gdv")] &gdv::GDVDemuxerCreator {}, +#[cfg(feature="demuxer_hl_fmv")] + &hl_fmv::HighlanderFMVDemuxerCreator {}, #[cfg(feature="demuxer_imax")] &imax::IMAXDemuxerCreator {}, #[cfg(feature="demuxer_q")] diff --git a/nihav-registry/src/detect.rs b/nihav-registry/src/detect.rs index 3e1d26c..e73ae4b 100644 --- a/nihav-registry/src/detect.rs +++ b/nihav-registry/src/detect.rs @@ -274,6 +274,12 @@ const DETECTORS: &[DetectConditions] = &[ conditions: &[CheckItem{offs: 0, cond: &CC::Str(b"IMAX") }, CheckItem{offs: 10, cond: &CC::Eq(Arg::U16LE(0x102)) }], }, + DetectConditions { + demux_name: "hl-fmv", + extensions: ".fmv", + conditions: &[CheckItem{offs: 0, cond: &CC::Str(b"FMV*") }, + CheckItem{offs: 4, cond: &CC::Eq(Arg::U32LE(0)) }], + }, DetectConditions { demux_name: "legend-q", extensions: ".q", diff --git a/nihav-registry/src/register.rs b/nihav-registry/src/register.rs index 3a37892..21e6165 100644 --- a/nihav-registry/src/register.rs +++ b/nihav-registry/src/register.rs @@ -250,6 +250,7 @@ static CODEC_REGISTER: &[CodecDescription] = &[ desc!(video; "fable-imax", "Fable IMAX video"), desc!(video; "fst-video", "FutureVision video"), desc!(audio; "fst-audio", "FutureVision audio"), + desc!(video; "hl-fmv-video", "Highlander FMV video"), desc!(video-llp; "ipma", "Imagination Pilots Matte Animation"), desc!(video-llp; "ipma2", "Imagination Pilots Matte Animation v2"), desc!(video; "legend-q-video", "Legend Entertainment Q video"), -- 2.39.5