1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
use boyer_moore_magiclen::{BMByte, BMByteSearchable};
use core::slice::Iter;
use memmap::Mmap;
use mp4parse::MediaContext;
use std::fs::File;
use std::io::Write;

// Implementing Bytes struct to use boyer moore search in [u8]
pub struct Bytes<'a> {
    bytes: &'a [u8],
}

impl<'a> Bytes<'a> {
    fn new(bytes: &'a [u8]) -> Self {
        Bytes { bytes }
    }
}

impl<'a> BMByteSearchable for Bytes<'a> {
    fn len(&self) -> usize {
        self.bytes.len()
    }
    fn value_at(&self, index: usize) -> u8 {
        self.bytes[index]
    }
    fn iter(&self) -> Iter<u8> {
        self.bytes.iter()
    }
}

/// Simple extractor of Motion Photo taken on Samsung phone
/// (if it provides such feature and this feature is turned on) and saves it in MP4.
/// It is available on Galaxy S20, S20+, S20 Ultra, Z Flip, Note10, Note10+, S10e, S10, S10+,
/// Fold, Note9, S9, S9+, Note8, S8, S8+, S7, and S7 edge.
///
/// Example of usage:
/// ```
/// use std::fs::File;
/// use sm_motion_photo::SmMotion;
///
/// // open file
/// let photo_file = File::open("tests/data/photo.jpg").unwrap();
/// let mut sm = SmMotion::with(&photo_file).unwrap();
/// println!("JPEG file contains video? {:?}", sm.has_video());
/// let mut video_file = File::create("tests/tmp/video.mp4").unwrap();
/// // dump mp4 from jpeg
/// sm.dump_video_file(&mut video_file).unwrap();
/// // get video duration (no dump needed)
/// println!("{:?}", sm.get_video_file_duration());
/// // get MP4 file context
/// println!("{:?}", sm.find_video_context());
/// // You can also save index and use it afterwards
/// let mut sm_cached = SmMotion::with_precalculated(&photo_file, 3366251).unwrap();
/// println!("{:?}", sm_cached.get_video_file_duration());
/// ```
pub struct SmMotion {
    mmap: Mmap,
    /// Index where starts a video
    pub video_index: Option<usize>,
}

impl SmMotion {
    ///  First things first send here a file ref
    pub fn with(file: &File) -> Option<SmMotion> {
        let mut motion = SmMotion {
            video_index: None,
            // Don't place entire file in memory, using memory efficient memory mapping
            mmap: match unsafe { Mmap::map(&file) } {
                Ok(m) => m,
                _ => return None,
            },
        };

        let _ = motion.find_video_index();

        Some(motion)
    }

    /// Initialize SmMotion with a known video index.
    /// It's handful when you are caching the results of searching.
    pub fn with_precalculated(file: &File, index: usize) -> Option<SmMotion> {
        Some(SmMotion {
            video_index: Some(index),
            // Don't place entire file in memory, using memory efficient memory mapping
            mmap: match unsafe { Mmap::map(&file) } {
                Ok(m) => m,
                _ => return None,
            },
        })
    }

    /// Look for starting MP4 index in Samsung Motion Photo JPEG (or HEIC/HEIF) file
    pub fn find_video_index(&mut self) -> Result<Option<usize>, &'static str> {
        // This line is an indicator of ending JPEG (or HEIC/HEIF) file and starting MP4 file
        let indicator: Vec<u8> = vec![
            0x4D, 0x6F, 0x74, 0x69, 0x6F, 0x6E, 0x50, 0x68, 0x6F, 0x74, 0x6F, 0x5F, 0x44, 0x61,
            0x74, 0x61,
        ];

        // Using boyer moore for faster search of vec position in a file
        let bmb = BMByte::from(&indicator).unwrap();
        let bytes = Bytes::new(&self.mmap[..]);
        // Using the first entry because it is quite unique
        self.video_index = match bmb.find_first_in(bytes) {
            // Move index on the length of indicator
            Some(index) => Some(index + 16),
            None => None,
        };
        Ok(self.video_index)
    }

    /// Check if a photo has a Motion Photo feature
    pub fn has_video(&self) -> bool {
        match self.video_index {
            Some(_) => true,
            None => false,
        }
    }

    /// Get video context from mp4parse.
    pub fn find_video_context(&self) -> Option<MediaContext> {
        match self.video_index {
            Some(index) => {
                let mut video_content = &self.mmap[index..];
                let mut context = mp4parse::MediaContext::new();
                let _ = mp4parse::read_mp4(&mut video_content, &mut context);
                Some(context)
            }
            None => None,
        }
    }

    /// Gen length of video file in photo in milliseconds
    pub fn get_video_file_duration(&self) -> Option<u64> {
        let context = self.find_video_context()?;
        if context.tracks.len() != 1 {
            return None;
        }
        match context.tracks[0].tkhd.as_ref() {
            Some(tkhd) => Some(tkhd.duration),
            None => None,
        }
    }

    // Save video file from image
    pub fn dump_video_file(&self, file: &mut File) -> Result<(), &str> {
        match self.video_index {
            Some(index) => {
                let video_content = &self.mmap[index..];
                match file.write_all(&video_content) {
                    Ok(()) => Ok(()),
                    Err(_) => Err("Can't write to file"),
                }
            }
            None => Err("Video not found"),
        }
    }
}