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:
Lucas RayanandClaude Sonnet 5.5 committed 2026-10-19 00:06:25 -03:00
1 parent 8fdb931c49
commit df194edb2f
8 files changed
+1606 -5

No files matched your search

+1
View File
@@ -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)
+1
View File
@@ -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
+42
View File
@@ -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 {
+66 -5
View File
@@ -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 {
+74
View File
@@ -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![],
}
}
}