10

I can download the JPGs from my DCIM folder in my Pixel 2 and I know that the video is encoded inside the JPG, but I don't know what image viewer I can use to see the motion.

I usually use Irfanview, but I haven't yet been able to find out how to make it show the motion. And I haven't found anyone talking about how to do it.

Does anyone know?

9 Answers 9

8

I did some experimenting based on the answer by James Henstridge and came up with a simple PHP script that successfully split every Google Camera's motion photo I threw at it.

2022-01-24 - Code has been updated. See the edit at the bottom of this post. I'm leaving the old code here to avoid any confusion.

<?php
  
  $src_arg = $argv[1];
  $src_dir = realpath(pathinfo($src_arg, PATHINFO_DIRNAME)); 
  
  echo "Scanning for files...\n";
  
  foreach (glob($src_arg) as $src) {
    
    $file = realpath($src);
    
    if (!is_dir($file) && in_array(strtoupper(pathinfo($file, PATHINFO_EXTENSION)), ["JPEG", "JPG"])) {
      
      echo "\tProcessing: " . $file . "\n";

      $filesize = filesize($file);
      echo "\t\tFile size: " . $filesize . "\n";
      
      $handle = fopen($file, "rb");
      $data = fread($handle, $filesize);
      fclose($handle);
     
      $eoi_pos = strpos($data, "\xFF\xD9\x00\x00\x00\x18");
      echo "\t\tEOI segment position: " . $eoi_pos . "\n";
        
      if ($eoi_pos !== FALSE) {
        $output_base = $src_dir . DIRECTORY_SEPARATOR . pathinfo($file, PATHINFO_FILENAME);
        echo "\t\tSaving photo...\n";
        file_put_contents($output_base . "_photo.jpg", substr($data, 0, $eoi_pos + 2));
        echo "\t\tSaving video...\n";
        file_put_contents($output_base . "_video.mp4", substr($data, $eoi_pos + 2));
      } else {
        echo "\t\tSKIPPING - File does not appear to be a Google motion photo.\n";
      }
          
    }
    
  }
  
  echo "Done.\n";
  
?>

It should work on Windows and Linux. You just pass a path as the first argument and it will split any files that it believes are motion photos. You can use wildcards. It's non-destructive - the source file(s) are not deleted.

Some example uses:

php google_motion_photo_splitter.php c:\test\file.jpg
php google_motion_photo_splitter.php c:\test\*.jpg
php google_motion_photo_splitter.php c:\test\*

2022-01-24 As j3App mentioned in their answer, at some point, Google decided to add debug data between the JPG and MP4 in some cases. At the end of 2021, I also began seeing other variations (MP4s with varying start bytes - ex: 0000001C instead of 00000018, extra JPG EOI segments earlier in the image near XMP tags, etc). Anyway, I modified the code to search for the MP4 header first and then work backward to find the JPG EOI segment. It's worked on all variations I encountered. Here is the updated code:

<?php

$src_arg = $argv[1];
$src_dir = realpath(pathinfo($src_arg, PATHINFO_DIRNAME)); 

echo "Scanning for files...\n";

foreach (glob($src_arg) as $src) {
  
  $file = realpath($src);
  
  if (!is_dir($file) && in_array(strtoupper(pathinfo($file, PATHINFO_EXTENSION)), ["JPEG", "JPG"])) {
    
    echo "\tProcessing: " . $file . "\n";

    $filesize = filesize($file);
    echo "\t\tFile size: " . $filesize . "\n";
    
    $handle = fopen($file, "rb");
    $data = fread($handle, $filesize);
    fclose($handle);
        
    $mp4_start_pos = strpos($data, "ftyp");
    
    if ($mp4_start_pos !== FALSE) {
      $mp4_start_pos -= 4; # the real beginning of the mp4 starts 4 bytes before "ftyp"
      
      $jpg_end_pos = strrpos(substr($data, 0, $mp4_start_pos), "\xFF\xD9");
      
      if ($jpg_end_pos !== FALSE) {
        $jpg_end_pos += 2; # account for the length of the search string

        $output_base = $src_dir . DIRECTORY_SEPARATOR . pathinfo($file, PATHINFO_FILENAME);
        echo "\t\tSaving photo...\n";
        file_put_contents($output_base . "_photo.jpg", substr($data, 0, $jpg_end_pos));
        echo "\t\tSaving video...\n";
        file_put_contents($output_base . "_video.mp4", substr($data, $mp4_start_pos));
        
      } else {
        echo "\t\tSKIPPING - File appears to contain an MP4 but the no valid JPG EOI segment could be found.\n";
      }
      
    } else {
      echo "\t\tSKIPPING - File does not appear to be a Google motion photo.\n";
    }
        
  }
  
}

echo "Done.\n";
  
?>
1
  • Thanks for a good solution. Since my Pixel 4a 5g addes debug info between the photo and the video, I had to make some small adjustments to your solution (see my answer below)
    – j3App
    Commented May 26, 2021 at 10:34
6

I haven't seen any software to view them other than on the Google Photos website. I was curious about this so started pulling apart one of the photos from my phone, and here's what I found:

  1. The image file appears to be a standard JPEG image, but continues on after the End of Image segment (0xFF 0xD9).

  2. exiftool reports unrecognised MakerNotes. I suspect this custom metadata identifies the file as a motion photo.

  3. If transfer all the data after the EOI segment to a separate file, you'll have a standard MPEG-4 container. I got a GStreamer crash when trying to play it, but ffmpeg seems to be able to handle it and displayed the following metadata:

    Input #0, mov,mp4,m4a,3gp,3g2,mj2, from 'foo.mp4':
      Metadata:
        major_brand     : mp42
        minor_version   : 0
        compatible_brands: isommp42
        creation_time   : 2018-07-07T20:37:57.000000Z
        com.android.version: 8.1.0
      Duration: 00:00:01.87, start: 0.000000, bitrate: 20283 kb/s
        Stream #0:0(eng): Video: h264 (High) (avc1 / 0x31637661), yuvj420p(pc, smpte170m/smpte170m/unknown), 1024x768, 20161 kb/s, SAR 1:1 DAR 4:3, 30.01 fps, 30 tbr, 90k tbn, 180k tbc (default)
        Metadata:
          creation_time   : 2018-07-07T20:37:57.000000Z
          handler_name    : VideoHandle
        Stream #0:1(eng): Data: none (mett / 0x7474656D), 108 kb/s (default)
        Metadata:
          creation_time   : 2018-07-07T20:37:57.000000Z
          handler_name    : MetadHandle
        Stream #0:2(eng): Data: none (mett / 0x7474656D), 0 kb/s (default)
        Metadata:
          creation_time   : 2018-07-07T20:37:57.000000Z
          handler_name    : MetadHandle
    Unsupported codec with id 0 for input stream 1
    Unsupported codec with id 0 for input stream 2
    

    So, that's a 1.87 second H.264 video with resolution 1024x768, which seems to roughly match up with what I see from Google's apps/website (a drop in resolution and change in aspect ratio).

I know it isn't a complete solution, but it might be enough to get started on a tool to extract the videos.

2

Although the question is for Windows, I think it's appropriate to post my script to play it from command line on Linux here:

https://gist.github.com/vi/5de17bb8d4ea91b8c28e79e0bac6c3cb

#!/bin/bash

if [[ -z "$1"  || "$1" == --help || "$1" == "-?" ]]; then
    echo "Usage: mvimg_play MVIMG_20190806_183324.jpg [other files]"
    echo "Plays Google's Motion Photo using mpv. Depends on exiftool, mktemp, bash and mpv."
    exit 0
fi

FOUND=0
ARGS=()

TORM=()
TOKILL=()

function cleanup() {
    for i in "${TORM[@]}"; do
        rm -f "$i"
    done
    for p in ${TOKILL[@]}; do
        wait $p
    done
}

trap "cleanup" EXIT

for i in "$@"; do
    O=$(exiftool -t $i  | grep -F 'Micro Video Offset' | cut -f 2-2)
    if [[ -z "$O" ]]; then
        # wrong file? Just appending to playlist as is
        ARGS+=($i)
    else
        FOUND=1
        S=$(find $i -printf '%s')
        T=`mktemp`

        ARGS+=("$T")
        dd if="$i" skip=$((S-O)) iflag=skip_bytes of="$T" 2> /dev/null &
        TOKILL+=($!)
        TORM+=("$T")
    fi
done

if [[ $FOUND == 0 ]]; then
    echo "EXIF tag wasn't detected in specified files. Maybe exiftool does not work?" >&2
fi

mpv "${ARGS[@]}"

Maybe it can be used on Windows as well, with enough Mingw/Cygwin hackery.

2

There's also Motion-Photo-Viewer. Can either be self hosted or used online to play a single *.MP.jpg (and extract the mp4 through right-click-save-as.

You do not need a Web Server, so cloning the project and opening index.html will suffice.

1

I tried out the answer from Kory - thanks Kory. Since my Pixel 4a 5G adds a significant amount of debug info between the jpg and the mp4, my extracted mp4's didn't play.

So I had to add a little bit of extra functionality to Kory's solution

<?php

/** execute like > php motionPhotoSplitter.php "Camera Roll/*.MP.jpg" < */
const MP4_TYPES = ["avc1", "iso2", "isom", "mmp4", "mp41", "mp42", "mp71", "msnv", "ndas", "ndsc", "ndsh", "ndsm", "ndsp", "ndss", "ndxc", "ndxh", "ndxm", "ndxp", "ndxs"];
$src_arg = $argv[1];
$src_dir = realpath(pathinfo($src_arg, PATHINFO_DIRNAME));

echo "Scanning for files...\n";

foreach (glob($src_arg) as $src) {

    $file = realpath($src);

    if (!is_dir($file) && in_array(strtoupper(pathinfo($file, PATHINFO_EXTENSION)), ["JPEG", "JPG"])) {

        echo "\tProcessing: " . $file . "\n";

        $filesize = filesize($file);
        echo "\t\tFile size: " . $filesize . "\n";

        $handle = fopen($file, "rb"); // binary read
        $data = fread($handle, $filesize);
        fclose($handle);

        $eoi_pos = strpos($data, "\xFF\xD9"); // end of image in a jpeg file: #FFD9
        echo "\t\tEOI segment position: " . $eoi_pos . "\n";

        if ($eoi_pos !== FALSE) {
            $output_base = $src_dir . DIRECTORY_SEPARATOR . pathinfo($file, PATHINFO_FILENAME);
            echo "\t\tSaving photo...\n";
            file_put_contents($output_base . "_photo.jpg", substr($data, 0, $eoi_pos + 2));

/** an mp4 block starts with 4 bytes for length, then "ftyp", then a valid mp4-type as defined in the array MP4_TYPES */

            $mp4Pos = strpos($data, "ftyp", $eoi_pos);
            if ($mp4Pos && in_array(substr($data, $mp4Pos + 4, 4), MP4_TYPES)) {
                echo "\t\tSaving video...\n";
                file_put_contents($output_base . "_video.mp4", substr($data, $mp4Pos - 4));
            }
        } else {
            echo "\t\tSKIPPING - File does not appear to be a Google motion photo.\n";
        }
    }
}
echo "Done.\n";
?>
1
  • Thanks for posting this. I've also seen this as well as some other new variations. I updated my original post to work in a way that will hopefully be able to continue working with any future changes Google decides to make.
    – Kory
    Commented Jan 25, 2022 at 0:40
0

If you have already downloaded the jpg and MP files to your PC, all you need to do is edit the extension. Change it from filename.MP to filename.MP4 or .MPG and play with any media player like VLC, or import into any video editor like Premiere or EDIUS

0

I am not a fan of the solutions I have found so far, so I created my own Motion Photo Utility /exporter. https://www.3d-i.com/motionphotoutility/ it is free, closed source (for now), in alpha, and works on most platforms. It does a lot more validation, meta reading, update meta tags and detects some malformations… See website for more.

All the solutions I have looked at so far seem to follow the pattern of read whole motion photo jpeg file ~3,000,000 bytes long and look for EOI marker of jpeg anywhere in the file aka 0xffd9. Take a hacksaw and chop it there, and assume all is okay. You might get lucky and the corrupt info can be read by some robust software.

Warning the following is a verbose explanation, it may put some of you to sleep!

These new jpeg files are complex. Old jpeg version has 11 tags describe everything: https://docs.fileformat.com/image/jpeg/

The simplest jpeg parsers generally read to tag/marker, read var = bytes[length] associated with tag. Then consume the next var length of bytes in some defined manner. At the end of that data block a tag is EXPECTED 0xFFXX, if it is not there then throw an exception…. But that is also not enough.

Today jpeg file format continues to undergo changes adding meta data Exif, XMP… In the last few decades, every camera company has had some say in putting all kinds of interesting data into these files. From shoving thumbnails, videos, gps , orientation …, often for good reason. Exif meta data has several hundred tags, when that was not enough XMP was added, which has unrestricted length for describing imbedded data.

Some of these programs completely violate the rules, forgetting to represent tags etc, or have incorrect information.

Those poor software engineers that write your media players etc (vlc/browser), over the last decades, have to navigate around the changes and display what they can. The problem is they don’t know what the intent for a lot of the imbedded meta/data, and it is often just dropped. Case in point, open up a copy of an android motion photo on windows, click the favorite icon (heart). Observe the file size before and after, it is about half the size. Windows will delete content (including video), add some of its own meta tags and largely keep existing meta that describe the very data that was deleted. Aka it makes a mess. In my test VLC player (which is incredibly robust) will play motion photo as a video, but only render a still image.

1
  • An updated Motion Photo Utility to support newer android Motion Photos. The previous version over cautiously did not correctly recognize newer motion photos.
    – Dev3d
    Commented Jul 23, 2023 at 23:35
0

I'm developing a motion photo viewer for Windows 10/11, compatible with various motion photo formats from Google and Samsung, including JPG and HEIC.

It also allows exporting of video as MP4 and still frames as JPG.

Install from Windows Store: https://www.microsoft.com/store/apps/9NMBBHZ33KLM

Or download MSIX installer: https://phomo.cam

Please let me know if you have any issues. Thanks!

0

I studied Kory's approach, which I found very educational. However, I was looking for a plain shell solution on Linux that doesn't require the installation of additional tools. So I translated the PHP script to a bash equivalent. I did lazily assume that the file extension will always end in .jpg, as I have not encountered Motion Photos with .jpeg extensions. The only additional "feature" I added was to force the file timestamps to match that of the original file, just because I find it useful for other purposes.

I have successfully used this on files produced by my Pixel 7. I'm a bit concerned about the reliance on such short demarcation sequences, {0xFF, 0xD9} and "ftyp", as it seems fragile, but it has been working fine. I don't know enough about the file format to confidently suggest improvements for robustness.

Here it is my implementation for those who are interested:

#!/bin/bash

# Splits PXL_*.MP.jpg files (Motion Photos) into jpeg and mp4 pieces
#
# Based on https://android.stackexchange.com/a/203898/44325

if [ $# -lt 1 ]; then
    echo "Usage: $0 FILE1.jpg [FILE2.jpg] ..." >&2
    echo "Note that the filenames are assumed to end in .jpg" >&2
    exit 1
fi

for MP in $*; do
    echo "Processing: $MP"

    # The string "ftyp" will appear at the start of the MP4
    printf "\tFinding start of MP4 data...\n"
    FTYP_LOC=$(grep -oba "ftyp" $MP | awk -F: '{print $1}')

    if [ -z "$FTYP_LOC" ]; then
        printf "\tNo MP4 data found\n" 1>&2
        # Skip to next file
        continue
    fi

    # MP4 starts 4 bytes before the "ftyp" string
    MP4_START=$(($FTYP_LOC - 4))

    # The End of Image is demarked with the byte sequence
    # "0xff 0xd9", which is non-printable
    EOI=$(echo -n ffd9 | xxd -r -p)

    # Find the last match for EOI before the start of
    # the MP4 segment.
    EOI_LOC=$(dd if=$MP bs=1 count=$MP4_START 2>/dev/null | \
                  grep -oba $EOI                          | \
                  tail -1                                 | \
                  awk -F: '{print $1}')

    # Include the EOI bytes
    JPG_END=$((EOI_LOC + 2))

    # Names for output files. Assumes input files named *.jpg
    JPG_FILE=${MP%.jpg}_photo.jpg
    MP4_FILE=${MP%.jpg}_video.mp4

    printf "\tCreating $JPG_FILE...\n"
    dd if=$MP bs=1 count=$JPG_END of=$JPG_FILE 2>/dev/null
    touch -r $MP $JPG_FILE

    printf "\tCreating $MP4_FILE...\n"
    dd if=$MP bs=1 skip=$MP4_START of=$MP4_FILE 2>/dev/null
    touch -r $MP $MP4_FILE

done

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .