Skip to content

Commit e5f9d6b

Browse files
authored
Merge pull request #8 from g0ddest/HEIC-SG22
#7 change searching the mp4 method to handle newer Samsung devices HE…
2 parents ba1a1fe + b390eea commit e5f9d6b

File tree

4 files changed

+96
-17
lines changed

4 files changed

+96
-17
lines changed

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "sm_motion_photo"
3-
version = "0.1.5"
3+
version = "0.1.6"
44
authors = ["Vitaliy Velikodniy <[email protected]>"]
55
edition = "2018"
66
repository = "https://github.com/g0ddest/sm_motion_photo"

src/lib.rs

Lines changed: 71 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ impl<'a> BMByteSearchable for Bytes<'a> {
2323
fn value_at(&self, index: usize) -> u8 {
2424
self.bytes[index]
2525
}
26-
fn iter(&self) -> Iter<u8> {
26+
fn iter(&self) -> Iter<'_, u8> {
2727
self.bytes.iter()
2828
}
2929
}
@@ -101,11 +101,79 @@ impl SmMotion {
101101
let bmb = BMByte::from(&indicator).unwrap();
102102
let bytes = Bytes::new(&self.mmap[..]);
103103
// Using the first entry because it is quite unique
104-
self.video_index = match bmb.find_first_in(bytes) {
105-
// Move index on the length of indicator
104+
let base_index = match bmb.find_first_in(bytes) {
105+
// Move index on the length of indicator (right after the marker)
106106
Some(index) => Some(index + 16),
107107
None => None,
108108
};
109+
110+
// On newer Samsung devices (e.g., SG22) there may be extra bytes after the marker
111+
// before the actual MP4 begins. The MP4 typically starts with an 'ftyp' box,
112+
// which appears 4 bytes after the start of the file/box (after the 32-bit size).
113+
// To make detection robust, scan forward from the marker for 'ftyp' and, if found,
114+
// shift the starting index back by 4 bytes to the true MP4 start.
115+
if let Some(start_after_marker) = base_index {
116+
// Limit the scan window to avoid scanning the entire image. 64 KiB should be plenty.
117+
let scan_start = start_after_marker;
118+
let scan_end = (scan_start + 65536).min(self.mmap.len());
119+
let scan_slice = &self.mmap[scan_start..scan_end];
120+
121+
// Search for 'ftyp' inside the scan window using the same Boyer-Moore engine
122+
let ftyp: Vec<u8> = b"ftyp".to_vec();
123+
let bmb_ftyp = BMByte::from(&ftyp).unwrap();
124+
let bytes_ftyp = Bytes::new(scan_slice);
125+
let adjusted = match bmb_ftyp.find_first_in(bytes_ftyp) {
126+
Some(rel_pos) => {
127+
// Ensure we don't underflow when backing up 4 bytes for the size field
128+
if rel_pos >= 4 {
129+
Some(scan_start + rel_pos - 4)
130+
} else {
131+
Some(scan_start)
132+
}
133+
}
134+
None => None,
135+
};
136+
// If we didn't find 'ftyp' right after the marker (new HEIC layout),
137+
// try to locate the MP4 'ftyp' elsewhere in the file. Prefer the last
138+
// occurrence before the marker to avoid the HEIC's own 'ftyp' at the start.
139+
if let Some(idx) = adjusted {
140+
self.video_index = Some(idx);
141+
} else {
142+
// Define a small helper to test major brand after 'ftyp'
143+
let majors: [&[u8]; 5] = [b"isom", b"mp42", b"mp41", b"iso4", b"avc1"];
144+
let mut search_pos = 0usize;
145+
let mmap = &self.mmap;
146+
let mut chosen: Option<usize> = None;
147+
while let Some(pos) = mmap[search_pos..].windows(4).position(|w| w == b"ftyp") {
148+
let abs_pos = search_pos + pos;
149+
// Check we have enough bytes to read major brand
150+
if abs_pos + 8 < mmap.len() {
151+
let major = &mmap[abs_pos + 4..abs_pos + 8];
152+
// Filter out HEIC/HEIF brands and keep MP4-like ones
153+
let is_mp4_brand = majors.iter().any(|m| *m == major);
154+
if is_mp4_brand {
155+
// Only consider positions before the SEF footer marker to avoid false positives
156+
if abs_pos < start_after_marker {
157+
chosen = Some(abs_pos);
158+
}
159+
}
160+
}
161+
// Advance search position
162+
search_pos = abs_pos + 4;
163+
if search_pos >= mmap.len() { break; }
164+
}
165+
if let Some(ftyp_pos) = chosen {
166+
// Back up 4 bytes for size field if possible
167+
let start = if ftyp_pos >= 4 { ftyp_pos - 4 } else { ftyp_pos };
168+
self.video_index = Some(start);
169+
} else {
170+
// Fallback: keep original behavior (start right after marker)
171+
self.video_index = Some(start_after_marker);
172+
}
173+
}
174+
} else {
175+
self.video_index = None;
176+
}
109177
Ok(self.video_index)
110178
}
111179

tests/data/photo-sg22-ultra.heic

2.72 MB
Binary file not shown.

tests/lib.rs

Lines changed: 24 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,15 @@ mod tests {
1818
File::open(format!("{}/{}", dir, "tests/data/photo.jpg")).unwrap()
1919
}
2020

21-
fn get_photo_file_heic() -> File {
21+
fn get_photo_file_heic(model: &str) -> File {
22+
23+
let ext = match model {
24+
"sg22-ultra" => "-sg22-ultra",
25+
_ => ""
26+
};
27+
2228
let dir = env::var("CARGO_MANIFEST_DIR").unwrap();
23-
File::open(format!("{}/{}", dir, "tests/data/photo.heic")).unwrap()
29+
File::open(format! ("{}/{}{}.heic", dir, "tests/data/photo", ext)).unwrap()
2430
}
2531

2632
fn get_wrong_photo_file() -> File {
@@ -58,7 +64,7 @@ mod tests {
5864

5965
#[test]
6066
fn test_search_index_heic() {
61-
let mut sm_motion = match SmMotion::with(&get_photo_file_heic()) {
67+
let mut sm_motion = match SmMotion::with(&get_photo_file_heic("sgs20")) {
6268
Some(sm) => sm,
6369
None => panic!("Not created motion"),
6470
};
@@ -90,16 +96,21 @@ mod tests {
9096

9197
#[test]
9298
fn test_dump_video_heic() {
93-
let sm_motion = match SmMotion::with(&get_photo_file_heic()) {
94-
Some(sm) => sm,
95-
None => panic!("Not created motion"),
96-
};
97-
let mut file = create_video_file(TMP_VIDEO_HEIC);
98-
let _ = sm_motion.dump_video_file(&mut file);
99-
let mut open_file = File::open(TMP_VIDEO_HEIC).unwrap();
100-
let mut context = mp4parse::MediaContext::new();
101-
let _ = mp4parse::read_mp4(&mut open_file, &mut context);
102-
assert_ne!(context.tracks.len(), 0);
99+
100+
let models = vec!["sgs20", "sg22-ultra"];
101+
102+
models.iter().for_each(|model| {
103+
let sm_motion = match SmMotion::with(&get_photo_file_heic(model)) {
104+
Some(sm) => sm,
105+
None => panic!("Not created motion"),
106+
};
107+
let mut file = create_video_file(TMP_VIDEO_HEIC);
108+
let _ = sm_motion.dump_video_file(&mut file);
109+
let mut open_file = File::open(TMP_VIDEO_HEIC).unwrap();
110+
let mut context = mp4parse::MediaContext::new();
111+
let _ = mp4parse::read_mp4(&mut open_file, &mut context);
112+
assert_ne!(context.tracks.len(), 0);
113+
});
103114
}
104115

105116
#[test]

0 commit comments

Comments
 (0)