偷偷摘套内射激情视频,久久精品99国产国产精,中文字幕无线乱码人妻,中文在线中文a,性爽19p

Android動態(tài)圖片技術深度解析

移動開發(fā) Android
Android 動態(tài)照片技術借助精心設計的文件結(jié)構、元數(shù)據(jù)系統(tǒng)及容器規(guī)范,實現(xiàn)了靜態(tài)與動態(tài)內(nèi)容的無縫融合。然而,不同廠商的技術實現(xiàn)存在差異,這不可避免地導致了兼容性問題 —— 各廠商的動態(tài)照片格式往往難以互通。

1.概述

動態(tài)照片是一種融合靜態(tài)圖片與動態(tài)視頻的多媒體格式,其核心設計采用"主靜態(tài)文件 + 附加視頻 + 元數(shù)據(jù)"的組合模式,既能實現(xiàn)靜態(tài)展示,又可支持動態(tài)播放,為用戶帶來更豐富的視覺體驗,同時保持了較好的兼容性。

不過,由于不同手機廠商對動態(tài)照片的技術實現(xiàn)存在差異(如封裝格式、元數(shù)據(jù)標簽定義等),導致顯示和提取方式不同。目前測試了三個主流廠商,其特點如下:

  • 小米: 使用 Micro Video 格式,通過自定義 EXIF 字段存儲元數(shù)據(jù)
  • Google: 采用標準 Motion Photo 格式,基于 XMP 元數(shù)據(jù)系統(tǒng)
  • OPPO: 實現(xiàn) O Live Photo 格式,支持 HDR GainMap 等高級特性

本文將深入解析 Android 動態(tài)照片的技術原理,重點分析 XMP 元數(shù)據(jù)系統(tǒng),并通過小米 Micro Video、Google Motion Photo 和 OPPO O Live Photo 三個真實案例,展示不同廠商的技術實現(xiàn)細節(jié),探尋一套統(tǒng)一的檢測和處理解決方案。

2.動態(tài)照片的核心結(jié)構

2.1 三層架構設計

動態(tài)照片的基礎框架由三部分構成:

主要靜態(tài)圖片文件

  • 作用:作為視覺主體,是用戶直觀看到的 "照片" 部分
  • 格式支持:JPEG、HEIC(高效圖像格式)、AVIF(新一代開放格式)
  • 內(nèi)容特點:通常為拍攝瞬間的靜態(tài)畫面,可能包含增益圖以支持 HDR 效果

次要視頻文件

  • 作用:作為動態(tài)補充,提供動態(tài)效果
  • 內(nèi)容特點:通常為拍攝前后 1-3 秒的短視頻片段
  • 存儲方式:附加在靜態(tài)文件中,用于呈現(xiàn)微小動作、聲音等動態(tài)元素

元數(shù)據(jù)系統(tǒng)

  • Camera XMP:定義靜態(tài)圖片與視頻的顯示規(guī)則,例如Camera:MotionPhoto屬性定義了是否是動態(tài)照片,0 是非動態(tài)照片;1 是動態(tài)照片。
  • Container XMP:指引設備定位并讀取附加的視頻文件,例如Length字段定義了次要媒體內(nèi)容的字節(jié)長度。

2.2 增益圖特性(可選)

動態(tài)照片的主要靜態(tài)文件可能包含 "增益圖"(Gain Map),這一設計與 Ultra HDR JPEG 的增益圖邏輯一致:

  • 基礎原理:通過 "基礎圖片 + 增益圖" 的組合實現(xiàn)高動態(tài)范圍(HDR)效果
  • 兼容性設計:支持設備渲染 HDR 效果,不支持的設備顯示基礎圖片

3.Android 官方動態(tài)照片 XMP 元數(shù)據(jù)系統(tǒng)詳解

3.1 Camera XMP 元數(shù)據(jù)

命名空間 URIhttp://ns.google.com/photos/1.0/camera/默認前綴Camera

核心屬性

屬性名

類型

說明

Camera:MotionPhoto

Integer

0:非動態(tài)照片;1:動態(tài)照片;其他值視為 0

Camera:MotionPhotoVersion

Integer

動態(tài)照片格式版本,當前規(guī)范為"1"

Camera:MotionPhotoPresentationTimestampUs

Long

與靜態(tài)圖片對應的視頻幀時間戳(微秒),-1 表示未設置

3.2 Container XMP 元數(shù)據(jù)

命名空間 URIhttp://ns.google.com/photos/1.0/container/默認前綴Container

目錄結(jié)構

Directory: 有序結(jié)構數(shù)組
  ├── Container:Item (主圖片 - 必須是第一項)
  ├── Container:Item (增益圖 - Ultra HDR時)
  └── Container:Item (視頻文件 - 必須是最后一項)

必需屬性

屬性名

類型

說明

Mime

String

媒體內(nèi)容的 MIME 類型

Semantic

String

媒體內(nèi)容的語義含義

Length

Integer

次要媒體內(nèi)容的字節(jié)長度

可選屬性

屬性名

類型

說明

Padding

Integer

主圖片結(jié)尾到下一媒體項的間隔字節(jié)數(shù)

Semantic 的可能的值

語義值

說明

Primary

主顯示圖片(必須有且僅有一個)

MotionPhoto

視頻容器(必須有且僅有一個,位于文件末尾)

GainMap

增益圖(Ultra HDR 時必需,位于視頻項之前)

3.3 元數(shù)據(jù)格式的解釋

命名空間的概念可以結(jié)合 Android XML 文件來理解。例如,xmlns:app="http://schemas.android.com/apk/res-auto" 定義了 app 命名空間,有了這個定義,像 MotionPhotoView 這樣的自定義視圖才能通過 app:layout_constraintLeft_toLeftOf 這類帶 app 前綴的屬性來設置值。

由于不同手機廠商可能會定義同名的元數(shù)據(jù)屬性,命名空間的核心作用就是通過前綴區(qū)分這些屬性,避免因名稱重復導致的沖突。

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@color/Blk_12"
    xmlns:app="http://schemas.android.com/apk/res-auto">
    <hy.sohu.com.app.ugc.share.view.widget.MotionPhotoView
        android:id="@+id/motion_photo_view"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

以下是一個根據(jù)官方文檔生成的 XMP 數(shù)據(jù)格式,可能不準確,大概結(jié)構是這樣的,通過下面的示例,我們可以更好的理解元數(shù)據(jù)是怎么存儲的。

<x:xmpmeta xmlns:x="adobe:ns:meta/">
    <rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
        <rdf:Description rdf:about=""
            xmlns:Camera="http://ns.google.com/photos/1.0/camera/"
            xmlns:Container="http://ns.google.com/photos/1.0/container/">

            
            <Camera:MotionPhoto>1</Camera:MotionPhoto>
            <Camera:MotionPhotoVersion>1</Camera:MotionPhotoVersion>
            <Camera:MotionPhotoPresentationTimestampUs>1500000</Camera:MotionPhotoPresentationTimestampUs>

            
            <Container:Directory>
                <rdf:Seq>
                    <rdf:li rdf:parseType="Resource">
                        <Container:Item:Mime>image/jpeg</Container:Item:Mime>
                        <Container:Item:Semantic>Primary</Container:Item:Semantic>
                    </rdf:li>
                </rdf:Seq>
            </Container:Directory>
        </rdf:Description>
    </rdf:RDF>
</x:xmpmeta>

4.Android 官方動態(tài)照片文件名模式規(guī)范

4.1 正則表達式規(guī)范

動態(tài)照片的文件名需遵循特定正則表達式命名規(guī)則,這是官方推薦的命名規(guī)范,目的是通過文件名快速識別動態(tài)照片。但實際情況中,各手機廠商并未統(tǒng)一遵循這一規(guī)則,導致該方法的識別效果大打折扣。

^([^\\s\\/\\\\][^\\/\\\\]*MP)\\.(JPG|jpg|JPEG|jpeg|HEIC|heic|AVIF|avif)

4.2 命名規(guī)則解析

前綴部分:[^\\s\\/\\\\][^\\/\\\\]*

  • 第一個字符不能是空格、斜杠或反斜杠
  • 后續(xù)字符可以是除斜杠和反斜杠之外的任意字符

標識部分:MP

  • 作用:作為動態(tài)照片文件的標志性標識
  • 位置:必須位于文件名末尾(擴展名之前)
  • 示例IMG_1234MP.jpg、DSC0056MP.heic

后綴部分:支持的擴展名

  • JPEG.jpg、.jpeg
  • HEIC.heic
  • AVIF.avif

5.Android 官方動態(tài)照片視頻容器內(nèi)容規(guī)范

以下是 android 官方定義的動態(tài)照片中視頻部分的編碼方式、軌道結(jié)構、同步機制和播放行為,這些規(guī)范保證了用戶在查看動態(tài)照片時能夠獲得流暢、一致的體驗,但不同手機廠商實現(xiàn)方式可能會不同。

5.1 軌道結(jié)構

主視頻軌道(必需)

  • 編碼格式:AVC(H.264)、HEVC(H.265)或 AV1
  • 分辨率:無強制限制
  • 色彩支持

SDR:8 位,BT.709 色彩空間,sRGB 轉(zhuǎn)換

HDR:10 位,BT.2100 色彩空間,HLG/PQ 轉(zhuǎn)換

次要視頻軌道(可選)

  • 作用:高分辨率縮略圖或替代畫面
  • 編碼格式:同主視頻軌道
  • 幀率:通常較低(1-5fps)
  • 幀關聯(lián):與主軌道幀一一對應,時間戳完全相同

音頻軌道(可選)

  • 編碼格式:AAC
  • 參數(shù):16 位單聲道或立體聲
  • 采樣率:44kHz、48kHz 或 96kHz
  • 播放規(guī)則:與主視頻軌道同步

5.2 軌道排序規(guī)則

  1. 主視頻軌道:索引最小,必須是第一個視頻軌道
  2. 次要視頻軌道:索引大于主軌道,位于主軌道之后
  3. 音頻軌道:無強制順序,但需時間同步

6.不同動態(tài)照片實現(xiàn)案例分析

6.1 動態(tài)照片技術特點與檢測方法

Android 的ExifInterface類主要支持標準 EXIF 標簽,對 XMP 命名空間的支持非常有限,例如小米的 MicroVideo 標簽屬于自定義 XMP 命名空間,因此 ExifInterface 無法解析。實際開發(fā)中需要使用二進制解析或 Adobe XMP SDK 進行處理。

小米官方提供了專用 SDK,可以展示、判斷和制作動態(tài)照片,為開發(fā)者提供了完整的解決方案。

6.2 小米 Micro Video 案例分析

小米手機使用自有的 Micro Video 格式,通過自定義 EXIF 字段存儲動態(tài)照片元數(shù)據(jù):

exiftool /Users/allenzhang/Downloads/1752549853110.jpg

關鍵元數(shù)據(jù)信息:

File Name                       : 1752549853110.jpg
File Size                       : 5.5 MB
Make                            : (小米手機型號)
XMP Toolkit                     : Adobe XMP Core 5.1.0-jc003
Micro Video Version             : 1
Micro Video                     : 1
Micro Video Offset              : 1735850
Micro Video Presentation Timestamp Us: 761955
Image Width                     : 3072
Image Height                    : 4096

XMP SDK 解析結(jié)果

通過 Adobe XMP SDK 的 xmpMeta.dumpObject() 方法可以獲取小米動態(tài)照片的完整 XMP 結(jié)構:

ROOT NODE
 http://ns.google.com/photos/1.0/camera/ = "GCamera:" (0x80000000 : SCHEMA_NODE)
  GCamera:MicroVideo = "1"
  GCamera:MicroVideoVersion = "1"
  GCamera:MicroVideoOffset = "1757635"
  GCamera:MicroVideoPresentationTimestampUs = "333227"
 http://ns.adobe.com/exif/1.0/ = "exif:" (0x80000000 : SCHEMA_NODE)
  exif:ImageWidth = "4096"
  exif:ImageLength = "3072"
  exif:Make = "Xiaomi"
  exif:Model = "XIAOMI Device"
 http://ns.adobe.com/xmp/note/ = "xmpNote:" (0x80000000 : SCHEMA_NODE)
  xmpNote:HasExtendedXMP = ""

技術特點分析

  • 小米復用了 Google 的 Camera 命名空間,并在其下擴展了 MicroVideo 等自定義屬性(非標準屬性),這可能導致與其他廠商的標準實現(xiàn)產(chǎn)生沖突
  • 通過 MicroVideoOffset 字段指示視頻數(shù)據(jù)位置
  • 視頻大小可能通過 文件總大小 - MicroVideoOffset 計算得出
  • 相比 Google 和 OPPO,小米的 XMP 結(jié)構相對簡單

6.3 Google Pixel 4 動態(tài)照片案例分析

Google Pixel 手機使用標準的 Motion Photo 格式,以下是 Pixel 4 拍攝的動態(tài)照片元數(shù)據(jù):

exiftool /Users/allenzhang/Downloads/PXL_20250722_065151464.MP.jpg

關鍵元數(shù)據(jù)信息:

File Name                       : PXL_20250722_065151464.MP.jpg
File Size                       : 4.2 MB
Make                            : Google
Camera Model Name               : Pixel 4
XMP Toolkit                     : Adobe XMP Core 5.1.0-jc003
Motion Photo                    : 1
Motion Photo Version            : 1
Motion Photo Presentation Timestamp Us: 411003
Has Extended XMP                : 4938039014578563D928899A05F0B30F
Directory Item Mime             : image/jpeg, video/mp4
Directory Item Semantic         : Primary, MotionPhoto
Directory Item Length           : 0, 870399
Directory Item Padding          : 0, 0
Motion Photo Video              : (Binary data 870399 bytes, use -b option to extract)

XMP SDK 解析結(jié)果

通過 Adobe XMP SDK 的 xmpMeta.dumpObject() 方法可以獲取 Google Motion Photo 的完整 XMP 結(jié)構:

ROOT NODE
 http://ns.google.com/photos/1.0/container/ = "Container:" (0x80000000 : SCHEMA_NODE)
  Container:Directory (0x600 : ARRAY | ARRAY_ORDERED)
   [1] (0x100 : STRUCT)
    Container:Item (0x100 : STRUCT)
     Item:Length = "0"
     Item:Mime = "image/jpeg"
     Item:Padding = "0"
     Item:Semantic = "Primary"
   [2] (0x100 : STRUCT)
    Container:Item (0x100 : STRUCT)
     Item:Length = "870399"
     Item:Mime = "video/mp4"
     Item:Padding = "0"
     Item:Semantic = "MotionPhoto"
 http://ns.google.com/photos/1.0/camera/ = "GCamera:" (0x80000000 : SCHEMA_NODE)
  GCamera:MotionPhoto = "1"
  GCamera:MotionPhotoPresentationTimestampUs = "411003"
  GCamera:MotionPhotoVersion = "1"
 http://ns.adobe.com/xmp/note/ = "xmpNote:" (0x80000000 : SCHEMA_NODE)
  xmpNote:HasExtendedXMP = "4938039014578563D928899A05F0B30F"

技術特點分析

  • Google 使用標準 Container 命名空間定義復雜的容器結(jié)構
  • 通過 Directory 數(shù)組清晰區(qū)分 Primary 圖片和 MotionPhoto 視頻
  • Item:Length 字段直接提供視頻大小信息(870,399 字節(jié))
  • HasExtendedXMP 表示使用了擴展 XMP 存儲大型元數(shù)據(jù)
  • 完全符合 Adobe XMP 和 Google Motion Photo 標準

6.4 OPPO Find X8 動態(tài)照片案例分析

OPPO 手機實現(xiàn)了自己的動態(tài)照片格式,稱為 "O Live Photo",同時也支持標準 Motion Photo:

exiftool /Users/allenzhang/Downloads/IMG20250722163505.jpg

關鍵元數(shù)據(jù)信息:

File Name                       : IMG20250722163505.jpg
File Size                       : 7.0 MB
Make                            : OPPO
Camera Model Name               : OPPO Find X8
XMP Toolkit                     : Adobe XMP Core 5.1.0-jc003
Version                         : 1.0
Motion Photo                    : 1
Motion Photo Version            : 1
Motion Photo Presentation Timestamp Us: 266704
Motion Photo Primary 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, 3334834
Directory Item Padding          : 0, 0
Gain Map Image                  : (Binary data 3334834 bytes, use -b option to extract)
Motion Photo Video              : (Binary data 3334834 bytes, use -b option to extract)

XMP SDK 解析結(jié)果

通過 Adobe XMP SDK 的 xmpMeta.dumpObject() 方法可以獲取 OPPO O Live Photo 的完整 XMP 結(jié)構:

ROOT NODE
 http://ns.google.com/photos/1.0/container/ = "Container:" (0x80000000 : SCHEMA_NODE)
  Container:Directory (0x600 : ARRAY | ARRAY_ORDERED)
   [1] (0x100 : STRUCT)
    Container:Item (0x100 : STRUCT)
     Item:Length = "0"
     Item:Mime = "image/jpeg"
     Item:Padding = "0"
     Item:Semantic = "Primary"
   [2] (0x100 : STRUCT)
    Container:Item (0x100 : STRUCT)
     Item:Length = "474937"
     Item:Mime = "image/jpeg"
     Item:Padding = "0"
     Item:Semantic = "GainMap"
   [3] (0x100 : STRUCT)
    Container:Item (0x100 : STRUCT)
     Item:Length = "3334834"
     Item:Mime = "video/mp4"
     Item:Semantic = "MotionPhoto"
 http://ns.google.com/photos/1.0/camera/ = "GCamera:" (0x80000000 : SCHEMA_NODE)
  GCamera:MotionPhoto = "1"
  GCamera:MotionPhotoPresentationTimestampUs = "266704"
  GCamera:MotionPhotoVersion = "1"
 http://ns.oplus.com/photos/1.0/camera/ = "OpCamera:" (0x80000000 : SCHEMA_NODE)
  OpCamera:MotionPhotoOwner = "oplus"
  OpCamera:MotionPhotoPrimaryPresentationTimestampUs = "266704"
  OpCamera:OLivePhotoVersion = "2"
  OpCamera:VideoLength = "3334498"
 http://ns.adobe.com/hdr-gain-map/1.0/ = "hdrgm:" (0x80000000 : SCHEMA_NODE)
  hdrgm:Version = "1.0"

技術特點分析

  • OPPO 中的這個例子包含三層容器結(jié)構:Primary + GainMap + MotionPhoto
  • 使用專有命名空間 http://ns.oplus.com/photos/1.0/camera/ 存儲 O Live Photo 擴展信息
  • 支持 HDR 增益圖(GainMap),文件大小 474,937 字節(jié)
  • 兩個視頻大小字段:Container 中的 3,334,834 字節(jié)和 OPPO 專有的 3,334,498 字節(jié)(OPPO 視頻項未顯式定義 Padding 字段,推測其 Length 包含的額外字節(jié)(336 字節(jié))為隱式 Padding,可能與廠商編碼邏輯有關)
  • 集成 Adobe HDR-Gain-Map 標準,版本 1.0
  • 雙時間戳機制:標準時間戳和 Primary 專用時間戳
  • 向后兼容 Google Motion Photo 標準的同時提供 OPPO 增強功能

6.5 三大廠商動態(tài)照片格式對比分析

特征項

小米 Micro Video

Google Motion Photo

OPPO O Live Photo

格式標識

Micro Video: 1

Motion Photo: 1

Motion Photo: 1

 + O Live Photo Version: 2

存儲位置

EXIF 自定義字段

標準 XMP 元數(shù)據(jù)

標準 XMP 元數(shù)據(jù) + OPPO 擴展

視頻標識

Micro Video Offset

Directory Item Length

Video Length

 + Directory Item Length

時間戳字段

Micro Video Presentation Timestamp Us

Motion Photo Presentation Timestamp Us

雙時間戳支持

容器結(jié)構

簡單二進制附加

符合 Google 容器規(guī)范

支持 GainMap (HDR)

文件大小占比

視頻約 70%

視頻約 20%

視頻約 47%

兼容性

小米專有

標準格式

OPPO 擴展 + 標準兼容

特性

不支持增益圖、HDR

支持增益圖、HDR

支持增益圖、HDR

音頻支持

不支持

部分支持

完全支持

XMP 命名空間

com.mi

com.google

com.oppo

詳細分析:

1)小米 Micro Video(傳統(tǒng)格式)

文件總大小: 5.5 MB
├── 靜態(tài)圖片: 1.74 MB (約 31%)
└── 視頻數(shù)據(jù): 4.03 MB (約 69%)
  • 使用自定義 EXIF 字段存儲元數(shù)據(jù)
  • 視頻直接附加在靜態(tài)圖片后
  • 格式簡單但兼容性有限

2)Google Motion Photo(標準格式)

文件總大小: 4.2 MB
├── 靜態(tài)圖片: 約 3.3 MB (約 79%)
├── 視頻數(shù)據(jù): 870 KB (約 20%)
└── Extended XMP: 少量元數(shù)據(jù)
  • 完全符合 Google Motion Photo 標準
  • 使用 Extended XMP 處理大型元數(shù)據(jù)
  • 容器結(jié)構清晰,支持多種媒體類型

3)OPPO O Live Photo(混合格式)

文件總大小: 7.0 MB
├── 靜態(tài)圖片: 約 3.2 MB (約 46%)
├── GainMap (HDR): 475 KB (約 7%)
└── 視頻數(shù)據(jù): 3.3 MB (約 47%)
  • 支持 HDR 增益圖 (GainMap)
  • 雙時間戳機制提供更精確的同步
  • 向后兼容標準 Motion Photo 格式

7.處理動態(tài)照片的 Kotlin 實現(xiàn)

7.1 依賴配置

首先在 build.gradle 中添加必要的依賴:

dependencies {
    // Adobe XMP SDK
    implementation 'com.adobe.xmp:xmpcore:6.1.11'

    // 協(xié)程支持
    implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3'

    // 文件處理
    implementation 'androidx.core:core-ktx:1.12.0'
}

7.2 動態(tài)照片檢測方法和解析方法

基于以上對三大廠商動態(tài)照片格式的分析,我嘗試設計了一套統(tǒng)一的動態(tài)照片檢測解決方案。該方案采用雙重檢測策略:首先通過 extractXMPFromJPEG 方法解析 XMP 元數(shù)據(jù)來識別動態(tài)照片類型和視頻信息;當 XMP 元數(shù)據(jù)不完整或缺失時,則使用 findMp4HeaderOffset 方法直接從文件二進制數(shù)據(jù)中定位并提取視頻信息。這種可以提供更好的兼容性和可靠性。

下面以 OPPO 動態(tài)照片為例,展示完整的元數(shù)據(jù)解析和視頻提取實現(xiàn)。小米和 Google 動態(tài)照片的處理邏輯與此類似,遵循相同的接口設計模式。

class UnifiedMotionPhotoExtractor {
    companionobject {
        constval TAG = "MotionPhotoExtractor"
        privateconstval BUFFER_SIZE = 8192

        // MP4文件頭標識
        privateval FTYP_SIGNATURE = byteArrayOf('f'.toByte(), 't'.toByte(), 'y'.toByte(), 'p'.toByte())
        privateval FTYPMP4_SIGNATURE = byteArrayOf('f'.toByte(), 't'.toByte(), 'y'.toByte(), 'p'.toByte(),
                                               'm'.toByte(), 'p'.toByte(), '4'.toByte())
        privateval FTYPMP42_SIGNATURE = byteArrayOf('f'.toByte(), 't'.toByte(), 'y'.toByte(), 'p'.toByte(),
                                                'm'.toByte(), 'p'.toByte(), '4'.toByte(), '2'.toByte())
        privateval FTYPISOM_SIGNATURE = byteArrayOf('f'.toByte(), 't'.toByte(), 'y'.toByte(), 'p'.toByte(),
                                                'i'.toByte(), 's'.toByte(), 'o'.toByte(), 'm'.toByte())

        // 元數(shù)據(jù)標簽
        privateconstval XIAOMI_MICRO_VIDEO = "MicroVideo"
        privateconstval XIAOMI_MICRO_VIDEO_OFFSET = "MicroVideoOffset"
        privateconstval GCAMERA_MICRO_VIDEO = "GCamera:MicroVideo"
        privateconstval GCAMERA_MICRO_VIDEO_OFFSET = "GCamera:MicroVideoOffset"
        privateconstval GOOGLE_MOTION_PHOTO = "GCamera:MotionPhoto"
        privateconstval OPPO_LIVE_PHOTO = "OpCamera:OLivePhotoVersion"

        // 進度回調(diào)間隔(字節(jié)數(shù))
        privateconstval PROGRESS_INTERVAL = 100000

        // JPEG 段標記
        privateconstval JPEG_SOI = 0xFFD8
        privateconstval JPEG_APP1 = 0xFFE1
        privateconstval JPEG_SOS = 0xFFDA

        // XMP 頭標識常量
        privateval XMP_HEADER = "http://ns.adobe.com/xap/1.0/".toByteArray()
    }


    /**
     * 從 JPEG 文件中提取 XMP 元數(shù)據(jù)
     */
    privatefun extractXMPFromJPEG(filePath: String): XMPMeta? {
        returntry {
            RandomAccessFile(filePath, "r").use { raf ->
                // 驗證 JPEG 文件頭
                if (raf.readUnsignedShort() != JPEG_SOI) {
                    returnnull
                }

                while (true) {
                    // 讀取段標記
                    val marker = raf.readUnsignedShort()

                    // 如果到達圖像數(shù)據(jù)段,停止解析
                    if (marker == JPEG_SOS) {
                        break
                    }

                    // 只處理 APP1 段
                    if (marker == JPEG_APP1) {
                        val segmentLength = raf.readUnsignedShort()
                        val segmentData = ByteArray(segmentLength - 2)
                        raf.readFully(segmentData)

                        // 檢查是否為 XMP 數(shù)據(jù)
                        if (isXMPSegment(segmentData)) {
                            Log.d("chao"," 發(fā)現(xiàn) XMP 數(shù)據(jù)段,文件: ${filePath}")
                            return parseXMPFromSegment(segmentData)
                        }
                    } else {
                        // 跳過其他段
                        val segmentLength = raf.readUnsignedShort()
                        raf.skipBytes(segmentLength - 2)
                    }
                }
            }
            null
        } catch (e: Exception) {
            Log.e(TAG, "XMP 提取失敗: ${e.message}", e)
            null
        }
    }

    /**
     * 檢查是否為 XMP 段
     */
    privatefun isXMPSegment(data: ByteArray): Boolean {
        if (data.size < XMP_HEADER.size) returnfalse
        for (i in XMP_HEADER.indices) {
            if (data[i] != XMP_HEADER[i]) returnfalse
        }
        returntrue
    }

    /**
     * 從段數(shù)據(jù)中解析 XMP
     */
    privatefun parseXMPFromSegment(segmentData: ByteArray): XMPMeta? {
        returntry {
            val xmpDataStart = XMP_HEADER.size + 1// +1 for null terminator
            val xmpData = segmentData.copyOfRange(xmpDataStart, segmentData.size)
            XMPMetaFactory.parseFromBuffer(xmpData)
        } catch (e: XMPException) {
            Log.e(TAG, "XMP 解析失敗: ${e.message}", e)
            null
        }
    }


    // 這里是oppo手機提取xmp方法,其它手機的類似
    privatefun checkOppoLivePhotoInXMP(xmpMeta: XMPMeta): MotionPhotoResult? {
        returntry {
            val cameraNamespace = "http://ns.google.com/photos/1.0/camera/"
            val oppoNamespace = "http://ns.oplus.com/photos/1.0/camera/"http:// OPPO專有命名空間
            val containerNamespace = "http://ns.google.com/photos/1.0/container/"
            val hdrNamespace = "http://ns.adobe.com/hdr-gain-map/1.0/"

            // 首先檢查OPPO專有命名空間
            val oppoMotionPhotoOwner = try {
                xmpMeta.getPropertyString(oppoNamespace, "MotionPhotoOwner")
            } catch (e: Exception) { null }

            val oppoOLivePhotoVersion = try {
                xmpMeta.getPropertyString(oppoNamespace, "OLivePhotoVersion")
            } catch (e: Exception) { null }

            val oppoVideoLength = try {
                xmpMeta.getPropertyString(oppoNamespace, "VideoLength")?.toLongOrNull()
            } catch (e: Exception) { null }

            // 檢查標準命名空間中的Motion Photo標識
            val motionPhoto = try {
                xmpMeta.getPropertyInteger(cameraNamespace, "MotionPhoto")
            } catch (e: Exception) { null }

            // 如果是OPPO格式 (有OPPO專有標識或VideoLength字段)
            val isOppoFormat = oppoMotionPhotoOwner == "oplus" ||
                    oppoOLivePhotoVersion != null ||
                    oppoVideoLength != null

            if (motionPhoto == 1 && isOppoFormat) {
                Log.d("XMP", "檢測到OPPO O Live Photo格式")

                val version = try {
                    // 優(yōu)先使用OPPO版本,回退到標準版本
                    oppoOLivePhotoVersion?.toIntOrNull()
                        ?: xmpMeta.getPropertyInteger(cameraNamespace, "MotionPhotoVersion")
                } catch (e: Exception) { 1 }

                val timestamp = try {
                    // 優(yōu)先使用OPPO的Primary時間戳
                    xmpMeta.getPropertyLong(oppoNamespace, "MotionPhotoPrimaryPresentationTimestampUs")
                } catch (e: Exception) {
                    try {
                        // 回退到標準時間戳
                        xmpMeta.getPropertyLong(cameraNamespace, "MotionPhotoPresentationTimestampUs")
                    } catch (e2: Exception) { -1L }
                }

                // 解析Container結(jié)構獲取視頻大小和GainMap信息
                val containerInfo = parseOppoContainerInfo(xmpMeta, containerNamespace)

                // 優(yōu)先使用Container中的視頻大小,回退到OPPO的VideoLength字段
                val videoSize = containerInfo.videoSize.takeIf { it > 0 }
                    ?: oppoVideoLength ?: -1L

                // 檢查是否有HDR GainMap
                val hasHdrGainMap = try {
                    val hdrVersion = xmpMeta.getPropertyString(hdrNamespace, "Version")
                    hdrVersion != null && containerInfo.hasGainMap
                } catch (e: Exception) {
                    containerInfo.hasGainMap
                }

                Log.d("XMP", "OPPO檢測結(jié)果 - VideoSize: $videoSize, HasGainMap: $hasHdrGainMap, Version: $version")

                return MotionPhotoResult(
                    type = MotionPhotoType.OPPO_LIVE_PHOTO,
                    vendor = "OPPO",
                    version = version,
                    videoSize = videoSize,
                    presentationTimestamp = timestamp,
                    detectionMethod = "XMP SDK",
                    hasGainMap = hasHdrGainMap
                )
            }

            null
        } catch (e: Exception) {
            Log.e("XMP", "OPPO檢測失敗: ${e.message}", e)
            null
        }
    }



    /**
     * 提取OPPO動態(tài)照片中的視頻
     */
    privatefun extractVideo(
        inputFile: File,
        outputFile: File,
        videoSize: Long = 0L,
        videoOffset: Long = 0L,
        progressCallback: ((Long, Long) -> Unit)?
    ): Boolean {
        // OPPO動態(tài)照片可能包含增益圖,需要特殊處理
        // 這里使用簡化方法,直接搜索MP4文件頭
        val offset = findMp4HeaderOffset(inputFile.path)?:0

        Log.d(TAG,"找到視頻數(shù)據(jù),偏移量: $offset")

        // 嘗試多個可能的偏移量
        val possibleOffsets = listOf(
            videoOffset,
            offset
        )

        for (tryOffset in possibleOffsets) {
            if (tryOffset < 0) continue

            val tempFile = File.createTempFile("motion_video_", ".mp4")
            extractVideoData(inputFile, tempFile, tryOffset, progressCallback)

            if (isValidMp4(tempFile)) {
                Log.d(TAG,"? 偏移量 $tryOffset 提取的文件是有效的MP4格式")
                tempFile.copyTo(outputFile, overwrite = true)
                tempFile.delete()
                returntrue
            }

            tempFile.delete()
        }

        return isValidMp4(outputFile)
    }

    /**
     * 查找MP4文件頭的偏移量
     */
    privatefun findMp4HeaderOffset(filePath: String): Long? {
        val file = File(filePath)
        if (!file.exists() || file.length() < 8) {
            returnnull
        }

        // 只搜索文件的后半部分,因為視頻通常在文件末尾
        val fileSize = file.length()
        val startOffset = maxOf(0, fileSize / 2)

        RandomAccessFile(filePath, "r").use { raf ->
            raf.seek(startOffset)

            val buffer = ByteArray(BUFFER_SIZE)
            val window = ByteArray(8) // 滑動窗口,用于查找簽名
            var bytesRead: Int
            var currentOffset = startOffset

            while (raf.read(buffer).also { bytesRead = it } > 0) {
                for (i in0 until bytesRead) {
                    // 更新滑動窗口
                    System.arraycopy(window, 1, window, 0, window.size - 1)
                    window[window.size - 1] = buffer[i]

                    // 檢查當前窗口是否包含任何一種MP4文件頭標識
                    val signatureOffset = containsSignature(window)
                    if (signatureOffset != -1) {
                        // 找到了MP4文件頭,返回文件中的實際偏移量
                        // 減去4是因為MP4的box大小字段在標識符之前
                        return currentOffset + i - (window.size - signatureOffset) + 1 - 4
                    }
                }
                currentOffset += bytesRead
            }
        }

        returnnull
    }

    /**
     * 檢查給定的字節(jié)數(shù)組是否包含任何一種MP4文件頭標識
     *
     * @return 如果包含,返回標識符在數(shù)組中的起始位置;否則返回-1
     */
    privatefun containsSignature(data: ByteArray): Int {
        // 按優(yōu)先級順序檢查各種MP4文件頭標識
        val signatures = listOf(FTYPMP42_SIGNATURE, FTYPMP4_SIGNATURE, FTYPISOM_SIGNATURE, FTYP_SIGNATURE)

        for (signature in signatures) {
            for (i in0..data.size - signature.size) {
                var found = true
                for (j in signature.indices) {
                    if (data[i + j] != signature[j]) {
                        found = false
                        break
                    }
                }
                if (found) return i
            }
        }

        return -1
    }

    /**
     * 從輸入文件的指定偏移量開始提取視頻數(shù)據(jù)到輸出文件
     */
    privatefun extractVideoData(
        inputFile: File,
        outputFile: File,
        offset: Long,
        progressCallback: ((Long, Long) -> Unit)?
    ) {
        val fileSize = inputFile.length()
        val videoSize = fileSize - offset

        FileInputStream(inputFile).use { input ->
            FileOutputStream(outputFile).use { output ->
                // 跳過偏移量之前的數(shù)據(jù)
                input.skip(offset)

                val buffer = ByteArray(BUFFER_SIZE)
                var bytesRead: Int
                var totalBytesRead: Long = 0
                var lastProgressUpdate: Long = 0

                while (input.read(buffer).also { bytesRead = it } > 0) {
                    output.write(buffer, 0, bytesRead)

                    totalBytesRead += bytesRead

                    // 更新進度
                    if (progressCallback != null && totalBytesRead - lastProgressUpdate > PROGRESS_INTERVAL) {
                        progressCallback(totalBytesRead, videoSize)
                        lastProgressUpdate = totalBytesRead
                    }
                }

                // 最終進度更新
                progressCallback?.invoke(totalBytesRead, videoSize)
            }
        }
    }

    /**
     * 檢查文件是否為有效的MP4格式
     */
    privatefun isValidMp4(file: File): Boolean {
        if (!file.exists() || file.length() < 8) returnfalse

        FileInputStream(file).use { input ->
            val header = ByteArray(8)
            if (input.read(header) != header.size) returnfalse

            // 檢查文件頭是否包含ftyp標識
            val headerStr = String(header, StandardCharsets.UTF_8)
            return headerStr.contains("ftyp")
        }
    }
}

7.3 動態(tài)照片播放組件

為了更好地展示動態(tài)照片,我們可以創(chuàng)建一個專用的 MotionPhotoView 組件,ImageView用來顯示靜態(tài)圖片,VideoView用來顯示視頻,主要方法如下:

/**
 * 動態(tài)照片播放組件
 * 支持靜態(tài)圖片顯示和視頻播放切換
 */
class MotionPhotoView @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null,
    defStyleAttr: Int = 0
) : FrameLayout(context, attrs, defStyleAttr) {

    privateval imageView: ImageView
    privateval videoView: VideoView
    privatevar motionPhotoResult: MotionPhotoResult? = null

    /**
     * 設置動態(tài)照片數(shù)據(jù)
     */
    fun setMotionPhoto(imagePath: String, result: MotionPhotoResult) {
        this.originalImagePath = imagePath
        this.motionPhotoResult = result
        // 顯示靜態(tài)圖片
        loadStaticImage(imagePath)
    }

    /**
     * 播放視頻
     */
    fun playVideo() {
        try {
            Log.d("MotionPhotoView", "開始播放視頻: $videoPath")
            videoView.setVideoPath(videoPath)
            videoView.isVisible = true
            imageView.isVisible = false
            videoView.start()
            isVideoPlaying = true

        } catch (e: Exception) {
            Log.e("MotionPhotoView", "播放視頻異常: ${e.message}", e)
            onError?.invoke("視頻播放失敗: ${e.message}")
            showStaticImage()
        }
    }

    /**
     * 停止視頻播放
     */
    fun stopVideo() {
        if (videoView.isPlaying) {
            videoView.stopPlayback()
        }
        showStaticImage()
    }

    /**
     * 暫停視頻
     */
    fun pauseVideo() {
        if (videoView.isPlaying) {
            videoView.pause()
        }
    }

    /**
     * 設置VideoView回調(diào)
     */
    privatefun setupVideoCallbacks() {
        videoView.setOnPreparedListener { mediaPlayer ->
            Log.d("MotionPhotoView", "視頻準備完成")

            // 設置循環(huán)播放
            if (autoLoop) {
                mediaPlayer.isLooping = true
            }
            // 根據(jù)時間戳定位播放位置
            motionPhotoResult?.presentationTimestamp?.let { timestamp ->
                if (timestamp > 0) {
                    val seekPosition = (timestamp / 1000).toInt() // 轉(zhuǎn)換為毫秒
                    Log.d("MotionPhotoView", "定位到時間戳: ${seekPosition}ms (原始: ${timestamp}μs)")
                    mediaPlayer.seekTo(seekPosition)
                }
            }
        }

        videoView.setOnCompletionListener {
            Log.d("MotionPhotoView", "視頻播放完成")
            if (!autoLoop) {
                showStaticImage()
            }
        }

        videoView.setOnErrorListener { _, what, extra ->
            showStaticImage()
            true
        }
    }
}

08總結(jié)與展望

Android 動態(tài)照片技術借助精心設計的文件結(jié)構、元數(shù)據(jù)系統(tǒng)及容器規(guī)范,實現(xiàn)了靜態(tài)與動態(tài)內(nèi)容的無縫融合。然而,不同廠商的技術實現(xiàn)存在差異,這不可避免地導致了兼容性問題 —— 各廠商的動態(tài)照片格式往往難以互通。

本文通過深入解析 XMP 元數(shù)據(jù)系統(tǒng),并結(jié)合小米 Micro Video、Google Motion Photo 和 OPPO O Live Photo 的真實案例,詳細展示了動態(tài)照片的實現(xiàn)原理。對于開發(fā)者而言,理解這些技術細節(jié)不僅有助于構建更好的應用體驗,也為跨平臺兼容性處理提供了重要參考。

責任編輯:武曉燕 來源: 搜狐技術產(chǎn)品
相關推薦

2009-02-05 17:09:02

動態(tài)圖片JSPTomcat

2012-05-24 15:41:38

PHP

2021-05-13 15:23:31

人工智能深度學習

2012-11-20 10:23:47

云計算效用計算網(wǎng)格計算

2009-12-08 11:16:07

PHP動態(tài)圖像創(chuàng)建

2011-06-02 11:13:10

Android Activity

2021-10-12 11:07:33

動畫深度Android

2011-06-21 18:02:14

Qt 動態(tài) 鏈接庫

2024-09-29 08:00:00

動態(tài)代理RPC架構微服務架構

2023-06-09 15:34:32

數(shù)字孿生物聯(lián)網(wǎng)

2025-09-15 06:25:00

2021-04-18 20:49:03

Pyecharts圖表 組件

2009-08-11 13:27:09

C#動態(tài)圖像按鈕

2009-05-13 09:10:59

Facebook存儲基礎架構照片應用程序

2011-05-27 17:28:01

Android

2024-09-19 08:08:25

2012-05-23 11:17:58

2024-09-19 08:49:13

2011-05-05 14:28:47

散熱投影機

2023-06-13 09:53:59

智能汽車
點贊
收藏

51CTO技術棧公眾號