Reconstrução de vídeo MP4/MOV sem moov (H.264/HEVC) com referência de câmera e arquivos montados em partes
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
8fdb931c49
commit
df194edb2f
8 files changed
+1606
-5
No files matched your search
@@ -34,3 +34,4 @@ windows-sys = { version = "0.59", features = ["Win32_Foundation", "Win32_Storage
|
||||
|
||||
[dev-dependencies]
|
||||
tempfile = "3"
|
||||
mp4 = "0.14"
|
||||
@@ -66,6 +66,7 @@ pub fn make_found(ctx: &Ctx, n: NewFound) -> Found {
|
||||
let mut note = n.note.map(String::from);
|
||||
let head: Vec<u8> = match &n.data {
|
||||
DataLoc::Resident { bytes } => bytes.iter().take(32).copied().collect(),
|
||||
DataLoc::Parts { .. } => vec![],
|
||||
DataLoc::Extents { extents } => extents
|
||||
.iter()
|
||||
.find(|e| !e.sparse)
|
||||
|
||||
@@ -20,6 +20,7 @@ pub mod image;
|
||||
pub mod jpegscan;
|
||||
pub mod lvm;
|
||||
pub mod meta;
|
||||
pub mod mp4rebuild;
|
||||
pub mod notify;
|
||||
pub mod owner;
|
||||
pub mod partition;
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -375,6 +375,29 @@ pub fn write_file(src: &dyn Source, f: &Found, path: &Path) -> Result<Hashes> {
|
||||
out.write_all(bytes)?;
|
||||
hasher.update(bytes);
|
||||
}
|
||||
DataLoc::Parts { parts } => {
|
||||
for part in parts {
|
||||
match part {
|
||||
Part::Bytes { bytes } => {
|
||||
out.write_all(bytes)?;
|
||||
hasher.update(bytes);
|
||||
}
|
||||
Part::Run { offset, len } => {
|
||||
let mut done = 0u64;
|
||||
while done < *len {
|
||||
let n = (*len - done).min(1 << 20) as usize;
|
||||
let got = read_vec(src, offset + done, n)?;
|
||||
if got.len() < n {
|
||||
return Err(Error::msg("leitura além do fim da origem"));
|
||||
}
|
||||
out.write_all(&got)?;
|
||||
hasher.update(&got);
|
||||
done += n as u64;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
DataLoc::Extents { extents } => {
|
||||
let mut left = f.size;
|
||||
let mut buf = vec![0u8; 1 << 20];
|
||||
@@ -425,6 +448,25 @@ pub fn read_found_at(
|
||||
buf[..n].copy_from_slice(&bytes[o..o + n]);
|
||||
Ok(n)
|
||||
}
|
||||
DataLoc::Parts { parts } => {
|
||||
let mut pos = 0u64;
|
||||
for p in parts {
|
||||
let plen = p.len();
|
||||
if offset < pos + plen {
|
||||
let within = offset - pos;
|
||||
let n = want.min((plen - within) as usize);
|
||||
return match p {
|
||||
Part::Bytes { bytes } => {
|
||||
buf[..n].copy_from_slice(&bytes[within as usize..within as usize + n]);
|
||||
Ok(n)
|
||||
}
|
||||
Part::Run { offset: o, .. } => src.read_at(o + within, &mut buf[..n]),
|
||||
};
|
||||
}
|
||||
pos += plen;
|
||||
}
|
||||
Ok(0)
|
||||
}
|
||||
DataLoc::Extents { extents } => {
|
||||
let mut pos = 0u64;
|
||||
for e in extents {
|
||||
|
||||
@@ -207,6 +207,15 @@ pub fn run(
|
||||
total,
|
||||
index: crate::defrag::ClusterIndex::new(src.size()),
|
||||
pending: vec![],
|
||||
intact_mp4: vec![],
|
||||
refs: opts
|
||||
.references
|
||||
.iter()
|
||||
.filter_map(|p| {
|
||||
let s = crate::source::open_path(std::path::Path::new(p)).ok()?;
|
||||
crate::mp4rebuild::config_from_mp4(s.as_ref(), 0, s.size())
|
||||
})
|
||||
.collect(),
|
||||
carved: vec![],
|
||||
};
|
||||
for ri in 0..cp.ranges.len() {
|
||||
@@ -227,10 +236,11 @@ pub fn run(
|
||||
Outcome::Completed
|
||||
}
|
||||
|
||||
/// JPEG que parece quebrado: tenta recompor depois de varrer o disco todo.
|
||||
struct PendingJpeg {
|
||||
/// Arquivo que parece quebrado: tenta recompor depois de varrer o disco todo.
|
||||
struct Pending {
|
||||
found: Found,
|
||||
hit_len: u64,
|
||||
mp4: bool,
|
||||
}
|
||||
|
||||
struct Handler<'a> {
|
||||
@@ -242,16 +252,58 @@ struct Handler<'a> {
|
||||
done: u64,
|
||||
total: u64,
|
||||
index: crate::defrag::ClusterIndex,
|
||||
pending: Vec<PendingJpeg>,
|
||||
pending: Vec<Pending>,
|
||||
/// Vídeos íntegros achados no carving (início, tamanho): fontes de SPS/PPS.
|
||||
intact_mp4: Vec<(u64, u64)>,
|
||||
refs: Vec<crate::mp4rebuild::MediaConfig>,
|
||||
/// Extents de arquivos carved já emitidos (não podem servir de fragmento para outro).
|
||||
carved: Vec<Extent>,
|
||||
}
|
||||
|
||||
impl Handler<'_> {
|
||||
fn rebuild_mp4(&mut self, p: Pending, off: u64) {
|
||||
// Referências: as informadas pelo usuário e os vídeos íntegros mais próximos no disco.
|
||||
let mut refs = self.refs.clone();
|
||||
let mut near = self.intact_mp4.clone();
|
||||
near.sort_by_key(|&(o, _)| o.abs_diff(off));
|
||||
for (o, l) in near.into_iter().take(6) {
|
||||
if let Some(c) = crate::mp4rebuild::config_from_mp4(self.src, o, l) {
|
||||
refs.push(c);
|
||||
}
|
||||
}
|
||||
let Some(r) = crate::mp4rebuild::rebuild(self.src, off, p.hit_len, &refs) else { return };
|
||||
let Ok(pre) = crate::source::read_vec(self.src, off, r.pre_len as usize) else { return };
|
||||
let payload_len = r.payload.1 - r.payload.0;
|
||||
let mut f = p.found;
|
||||
let hdr = r.mdat_header();
|
||||
f.size = pre.len() as u64 + hdr.len() as u64 + payload_len + r.moov.len() as u64;
|
||||
f.data = DataLoc::Parts {
|
||||
parts: vec![
|
||||
Part::Bytes { bytes: pre },
|
||||
Part::Bytes { bytes: hdr },
|
||||
Part::Run { offset: r.payload.0, len: payload_len },
|
||||
Part::Bytes { bytes: r.moov },
|
||||
],
|
||||
};
|
||||
f.ext = "mp4".into();
|
||||
f.name = format!("arquivo_{:06}.mp4", f.id);
|
||||
f.confidence = Confidence::Medium;
|
||||
f.note = Some(format!(
|
||||
"índice (moov) reconstruído: {} quadros {}x{} a {:.1} fps, sem áudio; ajuste de tempo aproximado",
|
||||
r.frames, r.width, r.height, r.fps
|
||||
));
|
||||
self.carved.push(Extent::new(r.payload.0, payload_len));
|
||||
self.sink.replace(f);
|
||||
}
|
||||
|
||||
fn rebuild_pending(&mut self) {
|
||||
let pend = std::mem::take(&mut self.pending);
|
||||
for p in pend {
|
||||
let off = p.found.first_offset().unwrap_or(0);
|
||||
if p.mp4 {
|
||||
self.rebuild_mp4(p, off);
|
||||
continue;
|
||||
}
|
||||
let mut used = self.fs_extents.clone();
|
||||
used.extend(self.carved.iter().copied());
|
||||
// O próprio começo do arquivo não conta como "ocupado" para ele mesmo.
|
||||
@@ -314,6 +366,15 @@ impl CarveEvents for Handler<'_> {
|
||||
crate::defrag::Analysis::Unsupported | crate::defrag::Analysis::Rebuilt { .. } => {}
|
||||
}
|
||||
}
|
||||
let mut pending_mp4 = false;
|
||||
if matches!(hit.ext, "mp4" | "mov" | "m4v" | "3gp") {
|
||||
if hit.confidence == Confidence::High {
|
||||
self.intact_mp4.push((off, hit.len));
|
||||
} else {
|
||||
pending_mp4 = true;
|
||||
pending_len = Some(hit.len);
|
||||
}
|
||||
}
|
||||
let hit = &hit;
|
||||
let id = self
|
||||
.next_id
|
||||
@@ -348,8 +409,8 @@ impl CarveEvents for Handler<'_> {
|
||||
self.carved.push(Extent::new(off, hit.len));
|
||||
if let Some(hl) = pending_len {
|
||||
let mut pf = f.clone();
|
||||
pf.note = Some("JPEG interrompido ou fragmentado".into());
|
||||
self.pending.push(PendingJpeg { found: pf, hit_len: hl });
|
||||
pf.note = Some(if pending_mp4 { "vídeo sem índice (moov)".into() } else { "JPEG interrompido ou fragmentado".into() });
|
||||
self.pending.push(Pending { found: pf, hit_len: hl, mp4: pending_mp4 });
|
||||
}
|
||||
self.sink.found(f);
|
||||
match skip_len {
|
||||
|
||||
@@ -106,6 +106,72 @@ pub enum DataLoc {
|
||||
Extents {
|
||||
extents: Vec<Extent>,
|
||||
},
|
||||
/// Arquivo montado: trechos em memória (cabeçalhos refeitos) e trechos do disco.
|
||||
Parts {
|
||||
parts: Vec<Part>,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(tag = "part", rename_all = "lowercase")]
|
||||
pub enum Part {
|
||||
Bytes {
|
||||
#[serde(with = "b64")]
|
||||
bytes: Vec<u8>,
|
||||
},
|
||||
/// Trecho contíguo da origem.
|
||||
Run { offset: u64, len: u64 },
|
||||
}
|
||||
|
||||
impl Part {
|
||||
pub fn len(&self) -> u64 {
|
||||
match self {
|
||||
Part::Bytes { bytes } => bytes.len() as u64,
|
||||
Part::Run { len, .. } => *len,
|
||||
}
|
||||
}
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.len() == 0
|
||||
}
|
||||
}
|
||||
|
||||
mod b64 {
|
||||
use serde::{Deserialize, Deserializer, Serializer};
|
||||
const T: &[u8; 64] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
||||
pub fn serialize<S: Serializer>(b: &Vec<u8>, s: S) -> Result<S::Ok, S::Error> {
|
||||
let mut out = String::with_capacity(b.len().div_ceil(3) * 4);
|
||||
for c in b.chunks(3) {
|
||||
let n = (c[0] as u32) << 16 | (*c.get(1).unwrap_or(&0) as u32) << 8 | *c.get(2).unwrap_or(&0) as u32;
|
||||
out.push(T[(n >> 18) as usize & 63] as char);
|
||||
out.push(T[(n >> 12) as usize & 63] as char);
|
||||
out.push(if c.len() > 1 { T[(n >> 6) as usize & 63] as char } else { '=' });
|
||||
out.push(if c.len() > 2 { T[n as usize & 63] as char } else { '=' });
|
||||
}
|
||||
s.serialize_str(&out)
|
||||
}
|
||||
pub fn deserialize<'de, D: Deserializer<'de>>(d: D) -> Result<Vec<u8>, D::Error> {
|
||||
let s = String::deserialize(d)?;
|
||||
let mut out = vec![];
|
||||
let (mut acc, mut bits) = (0u32, 0);
|
||||
for c in s.bytes() {
|
||||
let v = match c {
|
||||
b'A'..=b'Z' => c - b'A',
|
||||
b'a'..=b'z' => c - b'a' + 26,
|
||||
b'0'..=b'9' => c - b'0' + 52,
|
||||
b'+' => 62,
|
||||
b'/' => 63,
|
||||
_ => continue,
|
||||
} as u32;
|
||||
acc = (acc << 6) | v;
|
||||
bits += 6;
|
||||
if bits >= 8 {
|
||||
bits -= 8;
|
||||
out.push((acc >> bits) as u8);
|
||||
acc &= (1 << bits) - 1;
|
||||
}
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
@@ -188,6 +254,10 @@ impl Found {
|
||||
match &self.data {
|
||||
DataLoc::Extents { extents } => extents.iter().find(|e| !e.sparse).map(|e| e.offset),
|
||||
DataLoc::Resident { .. } => None,
|
||||
DataLoc::Parts { parts } => parts.iter().find_map(|p| match p {
|
||||
Part::Run { offset, .. } => Some(*offset),
|
||||
_ => None,
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -330,6 +400,9 @@ pub struct ScanOptions {
|
||||
/// O disco principal é só membro de RAID sem metadados: varre apenas os volumes virtuais.
|
||||
#[serde(default)]
|
||||
pub skip_primary: bool,
|
||||
/// Vídeos íntegros da mesma câmera/drone: fornecem SPS/PPS para remontar MP4 sem moov.
|
||||
#[serde(default)]
|
||||
pub references: Vec<String>,
|
||||
}
|
||||
|
||||
fn default_max_carve() -> u64 {
|
||||
@@ -347,6 +420,7 @@ impl Default for ScanOptions {
|
||||
extra_parts: vec![],
|
||||
covered: vec![],
|
||||
skip_primary: false,
|
||||
references: vec![],
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,309 @@
|
||||
//! Reconstrução do moov de MP4 H.264: arquivos sintéticos e, se existir, um vídeo real.
|
||||
|
||||
use guara_core::mp4rebuild;
|
||||
use guara_core::source::MemSource;
|
||||
|
||||
struct BitW {
|
||||
v: Vec<u8>,
|
||||
n: usize,
|
||||
}
|
||||
impl BitW {
|
||||
fn new() -> Self {
|
||||
Self { v: vec![], n: 0 }
|
||||
}
|
||||
fn bit(&mut self, b: u32) {
|
||||
if self.n % 8 == 0 {
|
||||
self.v.push(0);
|
||||
}
|
||||
if b != 0 {
|
||||
*self.v.last_mut().unwrap() |= 0x80 >> (self.n % 8);
|
||||
}
|
||||
self.n += 1;
|
||||
}
|
||||
fn u(&mut self, val: u32, bits: u32) {
|
||||
for i in (0..bits).rev() {
|
||||
self.bit((val >> i) & 1);
|
||||
}
|
||||
}
|
||||
fn ue(&mut self, v: u32) {
|
||||
let x = v + 1;
|
||||
let len = 32 - x.leading_zeros();
|
||||
for _ in 0..len - 1 {
|
||||
self.bit(0);
|
||||
}
|
||||
self.u(x, len);
|
||||
}
|
||||
}
|
||||
|
||||
const SPS: &str = "67f4000d90d9b28283f602d41818190000030001000003003c8f142996";
|
||||
const PPS: &str = "68ebec448440";
|
||||
|
||||
fn hex(s: &str) -> Vec<u8> {
|
||||
(0..s.len() / 2).map(|i| u8::from_str_radix(&s[i * 2..i * 2 + 2], 16).unwrap()).collect()
|
||||
}
|
||||
|
||||
/// Cabeçalho de fatia H.264 conforme o SPS acima (log2_max_frame_num e log2_max_poc_lsb lidos do SPS).
|
||||
fn slice(idr: bool, frame_num: u32, poc_lsb: u32, payload: usize, seed: u8, sps: &mguara::Sps) -> Vec<u8> {
|
||||
let mut b = BitW::new();
|
||||
b.ue(0); // first_mb_in_slice
|
||||
b.ue(if idr { 7 } else { 5 }); // slice_type
|
||||
b.ue(0); // pps
|
||||
b.u(frame_num, sps.log2_max_frame_num);
|
||||
if idr {
|
||||
b.ue(0);
|
||||
}
|
||||
b.u(poc_lsb, sps.log2_max_poc_lsb);
|
||||
b.u(0x55, 8);
|
||||
let mut nal = vec![if idr { 0x65 } else { 0x41 }];
|
||||
nal.extend_from_slice(&b.v);
|
||||
nal.extend((0..payload).map(|i| (i as u8).wrapping_mul(7).wrapping_add(seed) | 0x10));
|
||||
nal
|
||||
}
|
||||
|
||||
mod mguara {
|
||||
pub use guara_core::mp4rebuild::Sps264 as Sps;
|
||||
}
|
||||
|
||||
fn nal(out: &mut Vec<u8>, n: &[u8]) {
|
||||
out.extend_from_slice(&(n.len() as u32).to_be_bytes());
|
||||
out.extend_from_slice(n);
|
||||
}
|
||||
|
||||
/// Monta um MP4 sem moov: ftyp, free, mdat. Devolve (arquivo, tamanhos esperados, sincronos, ordem de exibição).
|
||||
fn synthetic(gops: usize) -> (Vec<u8>, Vec<usize>, Vec<bool>) {
|
||||
synthetic_opt(gops, true)
|
||||
}
|
||||
|
||||
fn synthetic_opt(gops: usize, inband: bool) -> (Vec<u8>, Vec<usize>, Vec<bool>) {
|
||||
let sps_nal = hex(SPS);
|
||||
let sps = guara_core::mp4rebuild::parse_sps264(&sps_nal).expect("SPS");
|
||||
assert_eq!((sps.width, sps.height), (320, 240));
|
||||
let mut mdat = vec![];
|
||||
let mut sizes = vec![];
|
||||
let mut sync = vec![];
|
||||
// GOP: I P B B P B B (ordem de decodificação) com POC de exibição 0 6 2 4 12 8 10 (em unidades de 2)
|
||||
let pocs = [0u32, 6, 2, 4, 12, 8, 10];
|
||||
let mut frame_num = 0;
|
||||
for g in 0..gops {
|
||||
for (i, &p) in pocs.iter().enumerate() {
|
||||
let start = mdat.len();
|
||||
nal(&mut mdat, &[0x09, 0x10]);
|
||||
if i == 0 && inband {
|
||||
nal(&mut mdat, &sps_nal);
|
||||
nal(&mut mdat, &hex(PPS));
|
||||
}
|
||||
if i == 0 {
|
||||
frame_num = 0;
|
||||
}
|
||||
let s = slice(i == 0, frame_num % 16, p % 64, 600 + i * 90 + g * 7, (g * 7 + i) as u8, &sps);
|
||||
nal(&mut mdat, &s);
|
||||
if p % 4 == 0 || i == 1 || i == 4 {
|
||||
frame_num += 1;
|
||||
}
|
||||
sizes.push(mdat.len() - start);
|
||||
sync.push(i == 0);
|
||||
}
|
||||
}
|
||||
let mut f = vec![];
|
||||
f.extend_from_slice(&[0, 0, 0, 24, b'f', b't', b'y', b'p', b'i', b's', b'o', b'm', 0, 0, 2, 0, b'i', b's', b'o', b'm', b'a', b'v', b'c', b'1']);
|
||||
f.extend_from_slice(&[0, 0, 0, 8, b'f', b'r', b'e', b'e']);
|
||||
f.extend_from_slice(&((8 + mdat.len()) as u32).to_be_bytes());
|
||||
f.extend_from_slice(b"mdat");
|
||||
f.extend_from_slice(&mdat);
|
||||
(f, sizes, sync)
|
||||
}
|
||||
|
||||
fn assemble(file: &[u8], r: &mp4rebuild::Rebuilt) -> Vec<u8> {
|
||||
let mut out = file[..r.pre_len as usize].to_vec();
|
||||
out.extend_from_slice(&r.mdat_header());
|
||||
out.extend_from_slice(&file[r.payload.0 as usize..r.payload.1 as usize]);
|
||||
out.extend_from_slice(&r.moov);
|
||||
out
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rebuilds_synthetic_stream_with_b_frames_and_garbage_tail() {
|
||||
let (mut file, sizes, sync) = synthetic(6);
|
||||
let real_len = file.len();
|
||||
// Lixo depois do fim real (outro arquivo no disco) e tamanho do mdat declarado como "até o fim".
|
||||
file.extend((0..20_000u32).map(|i| (i.wrapping_mul(2654435761) >> 11) as u8 | 0x80));
|
||||
let n = file.len() as u32 - 32;
|
||||
file[32..36].copy_from_slice(&n.to_be_bytes());
|
||||
let src = MemSource(file.clone());
|
||||
let r = mp4rebuild::rebuild(&src, 0, file.len() as u64, &[]).expect("reconstrução");
|
||||
assert_eq!(r.frames, sizes.len());
|
||||
assert_eq!((r.width, r.height), (320, 240));
|
||||
assert_eq!(r.payload.1 as usize, real_len, "o fim do vídeo foi detectado");
|
||||
let out = assemble(&file, &r);
|
||||
let size = out.len() as u64;
|
||||
let mut mp4 = mp4::Mp4Reader::read_header(std::io::Cursor::new(out), size).expect("moov válido");
|
||||
let track_id = *mp4.tracks().keys().next().unwrap();
|
||||
let t = &mp4.tracks()[&track_id];
|
||||
assert_eq!(t.sample_count() as usize, sizes.len());
|
||||
for i in 0..sizes.len() {
|
||||
let s = mp4.read_sample(track_id, i as u32 + 1).unwrap().unwrap();
|
||||
assert_eq!(s.bytes.len(), sizes[i], "tamanho da amostra {}", i + 1);
|
||||
assert_eq!(s.is_sync, sync[i], "sincronia da amostra {}", i + 1);
|
||||
}
|
||||
// Ordem de exibição: pts = dts + deslocamento deve crescer na ordem do POC.
|
||||
let mut pts: Vec<(i64, usize)> = vec![];
|
||||
for i in 0..sizes.len() {
|
||||
let s = mp4.read_sample(track_id, i as u32 + 1).unwrap().unwrap();
|
||||
pts.push((s.start_time as i64 + s.rendering_offset as i64, i));
|
||||
}
|
||||
let pocs = [0u32, 6, 2, 4, 12, 8, 10];
|
||||
for g in 0..6 {
|
||||
let mut order: Vec<usize> = (0..7).collect();
|
||||
order.sort_by_key(|&k| pts[g * 7 + k].0);
|
||||
let mut expect: Vec<usize> = (0..7).collect();
|
||||
expect.sort_by_key(|&k| pocs[k]);
|
||||
assert_eq!(order, expect, "ordem de exibição do GOP {g}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rebuilds_real_h264_file_like_the_original_index() {
|
||||
let Ok(path) = std::env::var("GUARA_MP4_SAMPLE") else {
|
||||
eprintln!("SKIP: defina GUARA_MP4_SAMPLE com um MP4 H.264 para comparar com o índice original");
|
||||
return;
|
||||
};
|
||||
let orig = std::fs::read(&path).unwrap();
|
||||
// Corta o moov original.
|
||||
let mut p = 0usize;
|
||||
let mut cut = orig.len();
|
||||
while p + 8 <= orig.len() {
|
||||
let sz = u32::from_be_bytes(orig[p..p + 4].try_into().unwrap()) as usize;
|
||||
if &orig[p + 4..p + 8] == b"moov" {
|
||||
cut = p;
|
||||
break;
|
||||
}
|
||||
p += sz;
|
||||
}
|
||||
let file = orig[..cut].to_vec();
|
||||
let src = MemSource(file.clone());
|
||||
let r = mp4rebuild::rebuild(&src, 0, file.len() as u64, &[]).expect("reconstrução");
|
||||
let out = assemble(&file, &r);
|
||||
let osize = orig.len() as u64;
|
||||
let mut a = mp4::Mp4Reader::read_header(std::io::Cursor::new(orig), osize).unwrap();
|
||||
let rsize = out.len() as u64;
|
||||
let mut b = mp4::Mp4Reader::read_header(std::io::Cursor::new(out), rsize).unwrap();
|
||||
let ta = *a.tracks().keys().next().unwrap();
|
||||
let tb = *b.tracks().keys().next().unwrap();
|
||||
assert_eq!(a.tracks()[&ta].sample_count(), b.tracks()[&tb].sample_count());
|
||||
for i in 1..=a.tracks()[&ta].sample_count() {
|
||||
let x = a.read_sample(ta, i).unwrap().unwrap();
|
||||
let y = b.read_sample(tb, i).unwrap().unwrap();
|
||||
assert_eq!(x.bytes, y.bytes, "amostra {i}");
|
||||
assert_eq!(x.is_sync, y.is_sync, "sincronia {i}");
|
||||
}
|
||||
eprintln!("{} quadros, {:.2} fps", r.frames, r.fps);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rebuilds_real_video_with_interleaved_audio() {
|
||||
// GUARA_MP4_BIG: vídeo real com áudio; o teste corta o moov e os primeiros 40 MB do mdat bastam.
|
||||
let Ok(path) = std::env::var("GUARA_MP4_BIG") else {
|
||||
return;
|
||||
};
|
||||
use std::io::Read;
|
||||
let mut f = std::fs::File::open(&path).unwrap();
|
||||
let total = f.metadata().unwrap().len();
|
||||
let mut head = vec![0u8; 1 << 20];
|
||||
f.read_exact(&mut head).unwrap();
|
||||
// átomos no começo
|
||||
let mut p = 0usize;
|
||||
let (mut ftyp_end, mut mdat_at) = (0usize, None);
|
||||
while p + 8 <= head.len() {
|
||||
let sz = u32::from_be_bytes(head[p..p + 4].try_into().unwrap()) as usize;
|
||||
match &head[p + 4..p + 8] {
|
||||
b"ftyp" => ftyp_end = p + sz,
|
||||
b"mdat" => {
|
||||
mdat_at = Some(p);
|
||||
break;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
if sz == 0 {
|
||||
break;
|
||||
}
|
||||
p += sz;
|
||||
}
|
||||
let Some(mp) = mdat_at else {
|
||||
eprintln!("SKIP: mdat fora do primeiro MiB (moov no começo é grande)");
|
||||
return;
|
||||
};
|
||||
// Reexecuta com a leitura completa do começo para montar: ftyp + mdat (40 MB).
|
||||
use std::io::Seek;
|
||||
let mut f = std::fs::File::open(&path).unwrap();
|
||||
f.seek(std::io::SeekFrom::Start(mp as u64)).unwrap();
|
||||
let mut mdat = vec![0u8; 40 << 20];
|
||||
f.read_exact(&mut mdat).unwrap();
|
||||
let mut file = head[..ftyp_end].to_vec();
|
||||
file.extend_from_slice(&mdat);
|
||||
// O tamanho do mdat declarado vai muito além dos 40 MB que temos (arquivo cortado).
|
||||
let src = MemSource(file.clone());
|
||||
// Sem SPS/PPS no mdat, é preciso um vídeo de referência da mesma câmera (aqui, o próprio original).
|
||||
let cfg = mp4rebuild::config_from_mp4(&MemSource(head.clone()), 0, head.len() as u64).expect("avcC do original");
|
||||
assert!(mp4rebuild::rebuild(&src, 0, file.len() as u64, &[]).is_none(), "sem referência não há como montar o avcC");
|
||||
let r = mp4rebuild::rebuild(&src, 0, file.len() as u64, &[cfg]).expect("reconstrução");
|
||||
let out = assemble(&file, &r);
|
||||
let osize = out.len() as u64;
|
||||
let mut b = mp4::Mp4Reader::read_header(std::io::Cursor::new(out), osize).unwrap();
|
||||
let tb = *b.tracks().keys().next().unwrap();
|
||||
// Original (índice completo) para comparar as primeiras amostras.
|
||||
let f = std::fs::File::open(&path).unwrap();
|
||||
let mut a = mp4::Mp4Reader::read_header(std::io::BufReader::new(f), total).unwrap();
|
||||
let vid = a.tracks().values().find(|t| t.track_type().map(|x| x == mp4::TrackType::Video).unwrap_or(false)).unwrap().track_id();
|
||||
eprintln!("reconstruído: {} quadros, {} bytes pulados (áudio), {}x{}", r.frames, r.skipped_bytes, r.width, r.height);
|
||||
let sizes_a: Vec<usize> = (1..=60).map(|i| a.read_sample(vid, i).unwrap().unwrap().bytes.len()).collect();
|
||||
let sizes_b: Vec<usize> = (1..=60).map(|i| b.read_sample(tb, i).unwrap().unwrap().bytes.len()).collect();
|
||||
eprintln!("ORIG {:?}\nMINE {:?}", &sizes_a[40..60], &sizes_b[40..60]);
|
||||
let check = (r.frames as u32 - 1).min(400);
|
||||
for i in 1..=check {
|
||||
let x = a.read_sample(vid, i).unwrap().unwrap();
|
||||
let y = b.read_sample(tb, i).unwrap().unwrap();
|
||||
assert_eq!(x.bytes.len(), y.bytes.len(), "tamanho da amostra {i}");
|
||||
assert_eq!(x.is_sync, y.is_sync, "sincronia {i}");
|
||||
}
|
||||
}
|
||||
|
||||
mod common;
|
||||
|
||||
#[test]
|
||||
fn deep_scan_rebuilds_a_video_without_moov_using_a_neighbour_as_reference() {
|
||||
use guara_core::{ScanMode, ScanOptions};
|
||||
common::init();
|
||||
// Arquivo íntegro (com moov) da "mesma câmera" e um segundo que perdeu o moov e não traz SPS/PPS.
|
||||
let (good_raw, _, _) = synthetic_opt(8, true);
|
||||
let r = mp4rebuild::rebuild(&MemSource(good_raw.clone()), 0, good_raw.len() as u64, &[]).unwrap();
|
||||
let good = assemble(&good_raw, &r);
|
||||
let (broken, sizes, _) = synthetic_opt(10, false);
|
||||
let mut disk = vec![0u8; 8192];
|
||||
let good_off = disk.len();
|
||||
disk.extend_from_slice(&good);
|
||||
disk.resize(disk.len().div_ceil(4096) * 4096 + 8192, 0);
|
||||
let broken_off = disk.len();
|
||||
disk.extend_from_slice(&broken);
|
||||
disk.extend((0..30_000u32).map(|i| (i.wrapping_mul(2654435761) >> 9) as u8 | 0x80));
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let img = dir.path().join("v.img");
|
||||
std::fs::write(&img, &disk).unwrap();
|
||||
let c = common::scan_path(&img, &ScanOptions { mode: ScanMode::Deep, ..Default::default() });
|
||||
let vids: Vec<_> = c.found.iter().filter(|f| f.ext == "mp4").collect();
|
||||
assert_eq!(vids.len(), 2, "{:?}", vids.iter().map(|f| (f.first_offset(), f.size, f.confidence, &f.note)).collect::<Vec<_>>());
|
||||
let fb = vids.iter().find(|f| matches!(f.data, guara_core::DataLoc::Parts { .. })).expect("vídeo quebrado");
|
||||
let _ = broken_off;
|
||||
assert!(fb.note.as_deref().unwrap_or("").contains("reconstruído"), "{:?}", fb.note);
|
||||
let _ = good_off;
|
||||
let out = dir.path().join("out");
|
||||
let rep = common::recover_all(&img, std::slice::from_ref(*fb), &out);
|
||||
assert!(rep.failed.is_empty());
|
||||
let p = std::path::PathBuf::from(&rep.recovered[0].path);
|
||||
let bytes = std::fs::read(if p.is_absolute() { p } else { out.join(p) }).unwrap();
|
||||
let len = bytes.len() as u64;
|
||||
let mut m = mp4::Mp4Reader::read_header(std::io::Cursor::new(bytes), len).expect("MP4 válido");
|
||||
let tid = *m.tracks().keys().next().unwrap();
|
||||
assert_eq!(m.tracks()[&tid].sample_count() as usize, sizes.len());
|
||||
let first = m.read_sample(tid, 1).unwrap().unwrap();
|
||||
assert_eq!(first.bytes.len(), sizes[0]);
|
||||
}
|
||||
Reference in new issue
Block a user