16

The Google Pixel 2 and probably other phones since have the capability to cover "Motion Photos". These are saved as MVIMG and comparatively big.

I’m looking for a way to remove/extract the video.

So far I found a promising exif tag

$ exiftool -xmp:all MVIMG_123.jpg
XMP Toolkit                     : Adobe XMP Core 5.1.0-jc003
Micro Video                     : 1
Micro Video Version             : 1
Micro Video Offset              : 4032524

I thought the video might be present at the specified offset, but this doesn’t work:

$ dd if=MVIMG_123.jpg of=video.mp4 bs=4032524 skip=1
$ file video.mp4
video.mp4: data

Are there any resources that document the embedding? Are there even any tools to remove/extract the video?

rumpel
  • 7,870
  • 2
  • 38
  • 39

4 Answers4

17

I did find https://github.com/cliveontoast/GoMoPho which scans for the mp4 header and then dumps the video.

We can do the same, scanning for ftypmp4 from the MP4 header (actual file starts 4 bytes earlier):

Thus to extract videos:

for i in MVIMG*.jpg; do \
  ofs=$(grep -F --byte-offset --only-matching --text ftypmp4 "$i"); \
  ofs=${ofs%:*}; \
  [[ $ofs ]] && dd "if=$i" "of=${i%.jpg}.mp4" bs=$((ofs-4)) skip=1; \
done

And to remove videos:

for i in MVIMG*.jpg; do \
  ofs=$(grep -F --byte-offset --only-matching --text ftypmp4 "$i"); \
  ofs=${ofs%:*}; \
  [[ $ofs ]] && truncate -s $((ofs-4)) "$i"; \
done
rumpel
  • 7,870
  • 2
  • 38
  • 39
  • 1
    Nice use of `grep` and `dd` :-) – Mark Setchell Nov 01 '18 at 16:24
  • I've tried the GoMoPho open-source console app mentioned above and it works perfectly fine with moving images captured by Google Pixel 2 XL. Thanks! – Adrian Ciura Mar 24 '19 at 10:47
  • 5
    Looks like this changed in Android 12. My Pixel 4a produces motion images where the MP4 is at offset `ftypisom`. – aorth Sep 30 '21 at 18:05
  • 4
    For compatibility with both, use `for i in PXL*.MP.jpg MVIMG*.jpg;` and `grep --byte-offset --only-matching --text "ftypmp4\|ftypisom" "$i"` (no `-F` because of the `or` pattern) – Perseids Oct 10 '22 at 22:28
6

The EXIF tag is useful, but the offset is with the respect to the end of the file. The mp4 file is embedded at:

[file_size-micro_video_offset, file_size)

For example:

$ exiftool -xmp:all MVIMG_123.jpg
XMP Toolkit                     : Adobe XMP Core 5.1.0-jc003
Micro Video                     : 1
Micro Video Version             : 1
Micro Video Offset              : 2107172
Micro Video Presentation Timestamp Us: 966280
$ python -c 'import os; print os.path.getsize("MVIMG_123.jpg") - 2107172'
3322791
$ dd if=MVIMG_123.jpg of=video.mp4 bs=3322791 skip=1
$ file video.mp4 
video.mp4: ISO Media, MP4 v2 [ISO 14496-14]
mitchle
  • 61
  • 1
6

The non-perl shell scripts at the top of the post worked on my Linux system. I merged them into a single shell script that preserves the input file (like MVIMG_20191216_153039.jpg) and creates two output files (like IMG_20191216_153039.jpg and IMG_20191216_153039.mp4):

#!/bin/bash
# extract-mvimg: Extract .mp4 video and .jpg still image from a Pixel phone
# camera "motion video" file with a name like MVIMG_20191216_153039.jpg
# to make files like IMG_20191216_153039.jpg and IMG_20191216_153039.mp4
#
# Usage: extract-mvimg MVIMG*.jpg [MVIMG*.jpg...]

for srcfile
do
  case "$srcfile" in
  MVIMG_*_*.jpg) ;;
  *)
    echo "extract-mvimg: skipping '$srcfile': not an MVIMG*.jpg file?" 2>&1
    continue
    ;;
  esac

  # Get base filename: strip leading MV and trailing .jpg
  # Example: MVIMG_20191216_153039.jpg becomes IMG_20191216_153039
  basefile=${srcfile#MV}
  basefile=${basefile%.jpg}

  # Get byte offset. Example output: 2983617:ftypmp4
  offset=$(grep -F --byte-offset --only-matching --text ftypmp4 "$srcfile")
  # Strip trailing text. Example output: 2983617
  offset=${offset%:*}

  # If $offset isn't an empty string, create .mp4 file and
  # truncate a copy of input file to make .jpg file.
  if [[ $offset ]]
  then
    dd status=none "if=$srcfile" "of=${basefile}.mp4" bs=$((offset-4)) skip=1
    cp -ip "$srcfile" "${basefile}.jpg" || exit 1
    truncate -s $((offset-4)) "${basefile}.jpg"
  else
    echo "extract-mvimg: can't find ftypmp4 in $srcfile; skipping..." 2>&1
  fi
done

The status=none suppresses the "1+1 records in" and "1+1 records out" status output from dd. If your dd doesn't understand, you can remove that.

Jerry Peek
  • 61
  • 1
  • 2
  • 1
    For Android 12 change `ftypmp4` to `ftypisom`. Or just use `ftyp` for maximum compatibility. – pucky124 Nov 11 '21 at 20:37
  • Interestingly, I was checking some MVIMG files from Android 9 and the header is `ftypiso6`. Perhaps good advice to use `ftyp`. – aorth Dec 26 '21 at 16:10
4

The above suggestion using grep -F --byte-offset ... and dd does not work for me on macOS High Sierra as /usr/bin/grep outputs a wrong offset — I guess it yields the offset of the "line" which contains the word ftypmp4, i.e. the position of the previous LF character plus one. I might guess wrong, but anyway, this is my solution:

for i in MVIMG*.jpg; do \
    perl -0777 -ne 's/^.*(....ftypmp4.*)$/$1/s && print' "$i" >"${i%.jpg}.mp4"; \
done

This uses the ability of perl to slurp in a whole file at once and treat it as one big string. If no ftypmp4 with at least four leading bytes is present, an empty file is created, if multiple are present, the last one is extracted.

Similarly, to remove the video from all the files:

for i in MVIMG*.jpg; do \
    perl -0777 -pi -e 's/^(.*?)....ftypmp4.*$/$1/s' "$i"; \
done

This uses the in-place editing feature of perl. Everything after the first occurrence of ftypmp4 with its four leading bytes is cut off. If there are no occurrences, the file is rewritten with its contents unchanged.

(One might or might not need to set PERLIO=raw in the environment and/or unset the locale related variables to avoid UTF-8 interpretation, which could fail for binary files which happen to include byte sequences that violate the UTF-8 composition rules. In my tests with various MVIMG files no such problems occurred though.)

Stacko
  • 41
  • 4
  • I had the same problem with the `grep` suggestions on my Vagrant Ubuntu Buster box. This answer worked for me, using `perl` commands. Thank you! – ChrisPrime May 09 '21 at 17:28