How Android Dynamic Photos Work: XMP Metadata, Formats, and Kotlin Extraction
This article explores the technical architecture of Android dynamic photos, detailing the three‑layer file structure, XMP metadata specifications, and differences among Xiaomi Micro Video, Google Motion Photo, and OPPO O Live Photo, and provides a unified Kotlin solution for detection, parsing, and playback.
Overview
Dynamic photos combine a static image, an auxiliary video, and metadata using a "main static file + additional video + metadata" model, providing richer visual experience while maintaining compatibility.
Different manufacturers implement different formats: Xiaomi uses Micro Video with custom EXIF fields, Google uses standard Motion Photo with XMP, and OPPO uses O Live Photo with additional features.
Core Structure
Three‑layer architecture
Static image file (JPEG/HEIC/AVIF), optional video file (1‑3 seconds), and metadata system.
Main static image file
Purpose: visual main image.
Supported formats: JPEG, HEIC, AVIF.
Characteristics: may include gain map for HDR.
Secondary video file
Purpose: dynamic effect.
Content: short video clip.
Storage: appended to static file.
Metadata system
Camera XMP: defines display rules, e.g., Camera:MotionPhoto (0 = static, 1 = dynamic).
Container XMP: points to video location, e.g., Length field.
Official XMP Metadata Specification
Namespace URI: http://ns.google.com/photos/1.0/camera/ (prefix Camera). Camera:MotionPhoto (Integer) – 0 or 1. Camera:MotionPhotoVersion (Integer) – current version "1". Camera:MotionPhotoPresentationTimestampUs (Long) – timestamp of video frame, –1 if unset.
File Naming Convention
Official regex:
^([^\s\/\\][^\/\\]*MP)\.(JPG|jpg|JPEG|jpeg|HEIC|heic|AVIF|avif). The "MP" suffix marks a dynamic photo, but manufacturers often ignore it.
Case Analyses
Xiaomi Micro Video
Uses custom EXIF fields under the Google Camera namespace. Example metadata extracted with exiftool shows Micro Video flag, offset, and presentation timestamp. Video size is derived from file size minus offset.
exiftool /Users/allenzhang/Downloads/1752549853110.jpg File Name : 1752549853110.jpg
File Size : 5.5 MB
Make : (小米手机型号)
Micro Video Version : 1
Micro Video : 1
Micro Video Offset : 1735850
Micro Video Presentation Timestamp Us: 761955
Image Width : 3072
Image Height : 4096Google Motion Photo
Standard XMP container with Directory array distinguishing Primary image and MotionPhoto video. Video size is given by Item:Length. Supports extended XMP for large metadata.
exiftool /Users/allenzhang/Downloads/PXL_20250722_065151464.MP.jpg File Name : PXL_20250722_065151464.MP.jpg
File Size : 4.2 MB
Make : Google
Camera Model Name : Pixel 4
Motion Photo : 1
Motion Photo Version : 1
Motion Photo Presentation Timestamp Us: 411003
Directory Item Mime : image/jpeg, video/mp4
Directory Item Semantic : Primary, MotionPhoto
Directory Item Length : 0, 870399OPPO O Live Photo
Combines Primary, GainMap (HDR), and MotionPhoto items in a three‑layer container. Uses proprietary namespace http://ns.oplus.com/photos/1.0/camera/ for additional fields such as MotionPhotoOwner and VideoLength. Supports HDR gain map and dual timestamps.
exiftool /Users/allenzhang/Downloads/IMG20250722163505.jpg File Name : IMG20250722163505.jpg
File Size : 7.0 MB
Make : OPPO
Camera Model Name : OPPO Find X8
Motion Photo : 1
Motion Photo Version : 1
Motion Photo Presentation Timestamp Us: 266704
Motion Photo Owner : oplus
O Live Photo Version : 2
Video Length : 3334498
Directory Item Mime : image/jpeg, image/jpeg, video/mp4
Directory Item Semantic : Primary, GainMap, MotionPhoto
Directory Item Length : 0, 474937, 3334834Comparison (summarized):
Format identifier: Xiaomi Micro Video = 1, Google Motion Photo = 1, OPPO O Live Photo = 1 + O Live Photo Version 2.
Storage: Xiaomi custom EXIF, Google standard XMP, OPPO XMP + proprietary.
Video size proportion: Xiaomi ≈ 70 %, Google ≈ 20 %, OPPO ≈ 47 %.
HDR support: Xiaomi none, Google yes, OPPO yes.
Audio: Xiaomi none, Google partial, OPPO full.
Kotlin Unified Implementation
The article provides a Kotlin class UnifiedMotionPhotoExtractor that:
Extracts XMP from JPEG APP1 segment.
Detects vendor‑specific tags (MicroVideo, MotionPhoto, O Live Photo).
Finds MP4 header offsets by scanning the file tail.
Extracts video data to a separate file with progress callbacks.
Validates MP4 files.
Key functions include extractXMPFromJPEG, isXMPSegment, parseXMPFromSegment, checkOppoLivePhotoInXMP, findMp4HeaderOffset, and extractVideoData.
class UnifiedMotionPhotoExtractor {
companion object {
const val TAG = "MotionPhotoExtractor"
private const val BUFFER_SIZE = 8192
private val FTYP_SIGNATURE = byteArrayOf('f'.code.toByte(), 't'.code.toByte(), 'y'.code.toByte(), 'p'.code.toByte())
// ... other signatures omitted for brevity ...
}
private fun extractXMPFromJPEG(filePath: String): XMPMeta? {
return try {
RandomAccessFile(filePath, "r").use { raf ->
if (raf.readUnsignedShort() != JPEG_SOI) return null
while (true) {
val marker = raf.readUnsignedShort()
if (marker == JPEG_SOS) break
if (marker == JPEG_APP1) {
val segmentLength = raf.readUnsignedShort()
val segmentData = ByteArray(segmentLength - 2)
raf.readFully(segmentData)
if (isXMPSegment(segmentData)) {
return parseXMPFromSegment(segmentData)
}
} else {
val segmentLength = raf.readUnsignedShort()
raf.skipBytes(segmentLength - 2)
}
}
null
}
} catch (e: Exception) {
null
}
}
// ... other methods omitted for brevity ...
}Playback Component
A custom MotionPhotoView view combines an ImageView for the static picture and a VideoView for the video, handling start, pause, stop, and looping based on the presentation timestamp.
class MotionPhotoView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : FrameLayout(context, attrs, defStyleAttr) {
private val imageView: ImageView
private val videoView: VideoView
private var motionPhotoResult: MotionPhotoResult? = null
fun setMotionPhoto(imagePath: String, result: MotionPhotoResult) {
this.originalImagePath = imagePath
this.motionPhotoResult = result
loadStaticImage(imagePath)
}
fun playVideo() {
try {
videoView.setVideoPath(videoPath)
videoView.isVisible = true
imageView.isVisible = false
videoView.start()
} catch (e: Exception) {
showStaticImage()
}
}
// ... other methods omitted for brevity ...
}Conclusion and Outlook
Android dynamic photos rely on carefully designed file structures and XMP metadata, but vendor differences cause compatibility challenges. Understanding the specifications and using a unified extraction approach enables developers to build robust cross‑vendor support and improve user experience.
Sohu Tech Products
A knowledge-sharing platform for Sohu's technology products. As a leading Chinese internet brand with media, video, search, and gaming services and over 700 million users, Sohu continuously drives tech innovation and practice. We’ll share practical insights and tech news here.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
