mp4_merge/
lib.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2// Copyright © 2022 Adrian <adrian.eddy at gmail>
3
4use std::io::{ Read, Seek, Write, Result };
5use std::path::*;
6use byteorder::{ BigEndian, LittleEndian, ReadBytesExt, WriteBytesExt };
7use std::time::Instant;
8
9mod desc_reader;
10mod progress_stream;
11mod writer;
12mod insta360;
13use progress_stream::*;
14
15// We need to:
16// - Merge mdat boxes
17// - Sum         moov/mvhd/duration
18// - Sum         moov/trak/tkhd/duration
19// - Sum         moov/trak/mdia/mdhd/duration
20// - Sum         moov/trak/edts/elst/segment duration
21// - Merge lists moov/trak/mdia/minf/stbl/stts
22// - Merge lists moov/trak/mdia/minf/stbl/stsz
23// - Merge lists moov/trak/mdia/minf/stbl/stss
24// - Merge lists moov/trak/mdia/minf/stbl/stco and co64
25// - Rewrite stco to co64
26
27const fn fourcc(s: &str) -> u32 {
28    let s = s.as_bytes();
29    (s[3] as u32) | ((s[2] as u32) << 8) | ((s[1] as u32) << 16) | ((s[0] as u32) << 24)
30}
31const fn has_children(typ: u32, is_read: bool) -> bool {
32    typ == fourcc("moov") || typ == fourcc("trak") || typ == fourcc("edts") ||
33    typ == fourcc("mdia") || typ == fourcc("minf") || typ == fourcc("stbl") ||
34    (typ == fourcc("stsd") && is_read)
35}
36fn typ_to_str(typ: u32) -> String {
37    match String::from_utf8(vec![(typ >> 24) as u8, (typ >> 16) as u8, (typ >> 8) as u8, typ as u8 ]) {
38        Ok(x) => x,
39        Err(_) => format!("{:08X}", typ)
40    }
41}
42
43pub fn read_box<R: Read + Seek>(reader: &mut R) -> Result<(u32, u64, u64, i64)> {
44    let pos = reader.stream_position()?;
45    let size = reader.read_u32::<BigEndian>()?;
46    let typ = reader.read_u32::<BigEndian>()?;
47    if size == 1 {
48        let largesize = reader.read_u64::<BigEndian>()?;
49        Ok((typ, pos, largesize, 16))
50    } else {
51        Ok((typ, pos, size as u64, 8))
52    }
53}
54
55pub fn join_files<P: AsRef<Path>, F: Fn(f64)>(files: &[P], output_file: &P, progress_cb: F) -> Result<()> {
56    let mut open_files = Vec::with_capacity(files.len());
57    for x in files {
58        let f = std::fs::File::open(x)?;
59        let size = f.metadata()?.len() as usize;
60        open_files.push((f, size));
61    }
62    join_file_streams(&mut open_files, std::fs::File::create(output_file)?, progress_cb)
63}
64
65pub fn join_file_streams<F: Fn(f64), I: Read + Seek, O: Read + Write + Seek>(files: &mut [(I, usize)], output_file: O, progress_cb: F) -> Result<()> {
66    // Get the merged description from all source files
67    let mut desc = desc_reader::Desc::default();
68    desc.moov_tracks.resize(10, Default::default());
69    let mut total_size = 0;
70    let num_files = files.len() as f64;
71    let mut insta360_max_read = None;
72    for (i, fs) in files.iter_mut().enumerate() {
73        let filesize = fs.1;
74        let mut fs = std::io::BufReader::with_capacity(16*1024, &mut fs.0);
75        total_size += filesize;
76
77        { // Find mdat first
78            while let Ok((typ, offs, size, header_size)) = read_box(&mut fs) {
79                let org_pos = fs.stream_position()?;
80                if typ == fourcc("mdat") {
81                    log::debug!("Reading {}, offset: {}, size: {size}, header_size: {header_size}", typ_to_str(typ), offs);
82                    desc.mdat_position.push((None, org_pos, size - header_size as u64));
83                    desc.mdat_final_position = org_pos;
84                    break;
85                }
86                fs.seek(std::io::SeekFrom::Start(org_pos + size - header_size as u64))?;
87            }
88
89            if insta360_max_read.is_none() {
90                fs.seek(std::io::SeekFrom::End(-40))?;
91                let mut buf = vec![0u8; 40];
92                fs.read_exact(&mut buf)?;
93                // Check if it's Insta360
94                if &buf[8..] == insta360::MAGIC {
95                    insta360_max_read = Some(filesize as u64 - (&buf[..]).read_u32::<LittleEndian>()? as u64);
96                }
97            }
98
99            fs.seek(std::io::SeekFrom::Start(0))?;
100        }
101
102        desc_reader::read_desc(&mut fs, &mut desc, 0, u64::MAX, i)?;
103
104        if let Some(mdat) = desc.mdat_position.last_mut() {
105            mdat.0 = Some(i);
106            desc.mdat_offset += mdat.2;
107            for t in &mut desc.moov_tracks {
108                t.sample_offset = t.stsz_count;
109                t.chunk_offset = t.stco.len() as u32;
110            }
111        }
112
113        progress_cb(((i as f64 + 1.0) / num_files) * 0.1);
114    }
115
116    // Write it to the file
117    let mut debounce = Instant::now();
118    let f_out = ProgressStream::new(output_file, |total| {
119        if (Instant::now() - debounce).as_millis() > 100 {
120            progress_cb((0.1 + ((total as f64 / total_size as f64) * 0.9)).min(0.9999));
121            debounce = Instant::now();
122        }
123    });
124    let mut f_out = std::io::BufWriter::with_capacity(64*1024, f_out);
125
126    writer::get_first(files).seek(std::io::SeekFrom::Start(0))?;
127    writer::rewrite_from_desc(files, &mut f_out, &mut desc, 0, insta360_max_read.unwrap_or(u64::MAX))?;
128
129    // Patch final mdat positions
130    for track in &desc.moov_tracks {
131        f_out.seek(std::io::SeekFrom::Start(track.co64_final_position))?;
132        for x in &track.stco {
133            f_out.write_u64::<BigEndian>(*x + desc.mdat_final_position)?;
134        }
135    }
136
137    if insta360_max_read.is_some() {
138        // Merge Insta360 metadata
139        f_out.seek(std::io::SeekFrom::End(0))?;
140        let offsets = insta360::get_insta360_offsets(files)?;
141        insta360::merge_metadata(files, &offsets, f_out)?;
142    }
143
144    progress_cb(1.0);
145
146    Ok(())
147}
148
149pub fn update_file_times(input_path: &PathBuf, output_path: &PathBuf) {
150    if let Err(e) = || -> std::io::Result<()> {
151        let org_time = filetime_creation::FileTime::from_creation_time(&std::fs::metadata(&input_path)?).ok_or(std::io::ErrorKind::Other)?;
152        if cfg!(target_os = "windows") {
153            ::log::debug!("Updating creation time of {} to {}", output_path.display(), org_time.to_string());
154            filetime_creation::set_file_ctime(output_path, org_time)?;
155        } else {
156            ::log::debug!("Updating modification time of {} to {}", output_path.display(), org_time.to_string());
157            filetime_creation::set_file_mtime(output_path, org_time)?;
158        }
159        Ok(())
160    }() {
161        ::log::warn!("Failed to update file times: {e:?}");
162    }
163}