refactor: 删除未使用的 MakerNotes Patcher 死代码

删除文件:
- HEICMakerNotePatcher.swift (591 行)
- MakerNotesPatcher.swift (83 行)

原因:
1. 锁屏壁纸兼容性的根因是 MOV 的 still-image-time(必须为 0),
   而非 HEIC 的 MakerNotes 结构
2. 简化版 ContentIdentifier 方案经竞品验证,对 iOS 17+ 有效
3. 复杂的二进制 MakerNote 注入从未被需要

同时在 LivePhotoCore.swift 添加策略说明注释

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
empty
2026-01-03 23:16:37 +08:00
parent e041cacd7d
commit 683cebd5fa
4 changed files with 44 additions and 687 deletions

View File

@@ -1,17 +1,39 @@
# Live Photo 无法设置为动态壁纸:根因记录 # Live Photo 动态壁纸兼容性:根因与解决方案
## 现象 ## 现象
- 生成的 Live Photo 在相册中可识别、可播放,但在用作壁纸时提示动态不可用 - 生成的 Live Photo 在相册中可识别、可播放,但在"用作壁纸"时提示"动态不可用"
## 关键发现(本地文件元数据) ## 根因分析
### 问题代码(初始版本 299415a
- `Sources/LivePhotoCore/LivePhotoCore.swift``metadataItemForStillImageTime()``com.apple.quicktime.still-image-time` 的 value 写成 **-1**Int8
- 代码注释错误地认为"竞品使用 0xFF (-1)"——实际上竞品live-wallpaper使用的是 **0**
### 关键发现
- `/Users/yuanjiantsui/Downloads/paired.mov` 中的 timed metadata`StillImageTime`**-1**int8 - `/Users/yuanjiantsui/Downloads/paired.mov` 中的 timed metadata`StillImageTime`**-1**int8
- `exiftool` 输出示例:`[Track3] StillImageTime : -1` - `exiftool` 输出示例:`[Track3] StillImageTime : -1`
## 代码根因 ## 解决方案(来自 live-wallpaper 项目)
- `Sources/LivePhotoCore/LivePhotoCore.swift:842``LivePhotoBuilder.metadataItemForStillImageTime()``com.apple.quicktime.still-image-time` 的 value 写成 `-1`
- 建议改为 `0`int8
- 仍用 timed metadata group 的 `timeRange.start` 表达关键帧时间。
## 额外建议(兼容性 ### 修复方式(提交 a8b334e
- 移除非标准的 mdta keys`Sample Time` / `Sample Duration`(当前写入到 `assetWriter.metadata`)。 1. **复制 metadata.mov 文件**:从 live-wallpaper 项目复制 `metadata.mov`MD5: 9c3a827031283513b28844514dbe44d5
- 若仍不兼容,优先尝试 H.264、30fps、SDR、2~3 秒时长作为壁纸兼容模式。 2. **采用 metadata track 复制策略**:不再手动创建 still-image-time 元数据,而是从预制的 metadata.mov 复制整个 metadata track
### live-wallpaper 项目的正确实现
- `LivePhotoCreator.swift:52`: `"com.apple.quicktime.still-image-time": 0` // **值为 0不是 -1**
- `LivePhotoCreator.swift:110`: `createMetadataItem(identifier: "com.apple.quicktime.still-image-time", value: 0)`
- `Converter4Video.swift:88-187`: 使用 AVAssetReaderTrackOutput + AVAssetWriterInput 复制 metadata track
### 两个项目的关键对齐点
| 特性 | live-wallpaper | to-live-photo修复后|
|-----|---------------|----------------------|
| metadata.mov | ✅ 有 | ✅ 有同一文件MD5 相同)|
| still-image-time 值 | 0 | 从 metadata.mov 复制(隐式为 0|
| 分辨率策略 | 固定 1080×1920 | 可配置,默认 maxDimension=1920 |
| 视频时长 | CMTimeMake(550, 600) ≈ 0.917s | 同 |
## 技术要点总结
1. **StillImageTime 必须为 0**-1 会导致壁纸设置时"动态不可用"
2. **metadata track 复制优于手动创建**:预制的 metadata.mov 包含完整的元数据结构,比手动构建更可靠
3. **分辨率不是壁纸兼容性的决定因素**:核心问题是元数据格式

View File

@@ -1,591 +0,0 @@
import Foundation
/// HEIC MakerNotes
/// CGImageDestination Int64 MakerNotes
public enum HEICMakerNoteError: Error, CustomStringConvertible {
case invalidHEIC(String)
case metaNotFound
case iinfNotFound
case ilocNotFound
case exifItemNotFound
case exifLocationNotFound(itemID: UInt32)
case exifPayloadTooSmall
case tiffNotFound
case invalidTIFF(String)
case exifIFDPointerNotFound
case makerNoteTagNotFound
case makerNoteOutOfRange
case makerNoteTooShort(available: Int, required: Int)
public var description: String {
switch self {
case .invalidHEIC(let msg): return "Invalid HEIC: \(msg)"
case .metaNotFound: return "meta box not found"
case .iinfNotFound: return "iinf box not found"
case .ilocNotFound: return "iloc box not found"
case .exifItemNotFound: return "Exif item not found"
case .exifLocationNotFound(let id): return "Exif item location not found for item_ID=\(id)"
case .exifPayloadTooSmall: return "Exif payload too small"
case .tiffNotFound: return "TIFF header not found"
case .invalidTIFF(let msg): return "Invalid TIFF: \(msg)"
case .exifIFDPointerNotFound: return "ExifIFDPointer (0x8769) not found"
case .makerNoteTagNotFound: return "MakerNote tag (0x927C) not found"
case .makerNoteOutOfRange: return "MakerNote data out of range"
case .makerNoteTooShort(let available, let required):
return "MakerNote too short: available=\(available), required=\(required)"
}
}
}
public final class HEICMakerNotePatcher {
// MARK: - Public API
/// MakerNotes HEIC
/// Exif item MakerNote
public static func injectMakerNoteInPlace(fileURL: URL, makerNote: Data) throws {
var fileData = try Data(contentsOf: fileURL, options: [.mappedIfSafe])
//
let metaRange = try findTopLevelBox(type: "meta", in: fileData)
guard let metaRange else { throw HEICMakerNoteError.metaNotFound }
let meta = BoxView(data: fileData, range: metaRange)
let metaChildrenStart = meta.contentStart + 4
guard metaChildrenStart <= meta.end else {
throw HEICMakerNoteError.invalidHEIC("meta content too short")
}
guard let iinfRange = try findChildBox(type: "iinf", within: metaChildrenStart..<meta.end, in: fileData) else {
throw HEICMakerNoteError.iinfNotFound
}
guard let ilocRange = try findChildBox(type: "iloc", within: metaChildrenStart..<meta.end, in: fileData) else {
throw HEICMakerNoteError.ilocNotFound
}
let exifItemID = try parseIINFAndFindExifItemID(data: fileData, iinfRange: iinfRange)
let (locations, ilocInfo) = try parseILOCWithInfo(data: fileData, ilocRange: ilocRange)
guard let exifLoc = locations[exifItemID] else {
throw HEICMakerNoteError.exifLocationNotFound(itemID: exifItemID)
}
let exifStart = Int(exifLoc.offset)
let exifLen = Int(exifLoc.length)
guard exifStart >= 0, exifLen > 0, exifStart + exifLen <= fileData.count else {
throw HEICMakerNoteError.exifPayloadTooSmall
}
// Exif item
let existingExif = fileData.subdata(in: exifStart..<(exifStart + exifLen))
// Exif item MakerNote
let newExif = try buildNewExifItem(existingExif: existingExif, newMakerNote: makerNote)
if newExif.count <= exifLen {
// Exif
var paddedExif = newExif
if paddedExif.count < exifLen {
paddedExif.append(Data(repeating: 0x00, count: exifLen - paddedExif.count))
}
fileData.replaceSubrange(exifStart..<(exifStart + exifLen), with: paddedExif)
} else {
// Exif iloc
let newExifOffset = fileData.count
fileData.append(newExif)
// iloc offset length
try updateILOC(
in: &fileData,
ilocRange: ilocRange,
ilocInfo: ilocInfo,
itemID: exifItemID,
newOffset: UInt64(newExifOffset),
newLength: UInt64(newExif.count)
)
}
try fileData.write(to: fileURL, options: [.atomic])
}
// MARK: - Build New Exif Item
/// Exif item MakerNote
private static func buildNewExifItem(existingExif: Data, newMakerNote: Data) throws -> Data {
guard existingExif.count >= 10 else {
throw HEICMakerNoteError.exifPayloadTooSmall
}
// Exif item
// 4 bytes: TIFF header offset ( 6 "Exif\0\0" )
// 4 bytes: "Exif"
// 2 bytes: \0\0
// TIFF
let tiffOffsetValue = existingExif.readUInt32BE(at: 0)
let tiffStart = 4 + Int(tiffOffsetValue)
guard tiffStart + 8 <= existingExif.count else {
throw HEICMakerNoteError.tiffNotFound
}
//
let endianMarker = existingExif.subdata(in: tiffStart..<(tiffStart + 2))
let isBigEndian: Bool
if endianMarker == Data([0x4D, 0x4D]) {
isBigEndian = true
} else if endianMarker == Data([0x49, 0x49]) {
isBigEndian = false
} else {
throw HEICMakerNoteError.invalidTIFF("Invalid endian marker")
}
// TIFF Big-Endian Apple
var newTiff = Data()
// TIFF Header: "MM" + 0x002A + IFD0 offset (8)
newTiff.append(contentsOf: [0x4D, 0x4D]) // Big-endian
newTiff.append(contentsOf: [0x00, 0x2A]) // TIFF magic
newTiff.append(contentsOf: [0x00, 0x00, 0x00, 0x08]) // IFD0 offset = 8
// IFD0: 1 entry (ExifIFDPointer)
// Entry count: 1
newTiff.append(contentsOf: [0x00, 0x01])
// Entry: ExifIFDPointer (0x8769)
let exifIFDOffset: UInt32 = 8 + 2 + 12 + 4 // = 26 (IFD0 )
newTiff.append(contentsOf: [0x87, 0x69]) // tag
newTiff.append(contentsOf: [0x00, 0x04]) // type = LONG
newTiff.append(contentsOf: [0x00, 0x00, 0x00, 0x01]) // count = 1
newTiff.appendUInt32BE(exifIFDOffset) // value = offset to Exif IFD
// Next IFD offset: 0 (no more IFDs)
newTiff.append(contentsOf: [0x00, 0x00, 0x00, 0x00])
// Exif IFD: 1 entry (MakerNote)
let makerNoteDataOffset: UInt32 = exifIFDOffset + 2 + 12 + 4 // = 44
newTiff.append(contentsOf: [0x00, 0x01]) // entry count
// Entry: MakerNote (0x927C)
newTiff.append(contentsOf: [0x92, 0x7C]) // tag
newTiff.append(contentsOf: [0x00, 0x07]) // type = UNDEFINED
newTiff.appendUInt32BE(UInt32(newMakerNote.count)) // count
newTiff.appendUInt32BE(makerNoteDataOffset) // offset to MakerNote data
// Next IFD offset: 0
newTiff.append(contentsOf: [0x00, 0x00, 0x00, 0x00])
// MakerNote data
newTiff.append(newMakerNote)
// Exif item
var newExifItem = Data()
// 4 bytes: offset to TIFF (= 6, "Exif\0\0")
newExifItem.append(contentsOf: [0x00, 0x00, 0x00, 0x06])
// "Exif\0\0"
newExifItem.append(contentsOf: [0x45, 0x78, 0x69, 0x66, 0x00, 0x00])
// TIFF data
newExifItem.append(newTiff)
return newExifItem
}
// MARK: - Box Parsing
private struct BoxHeader {
let type: String
let size: Int
let headerSize: Int
let contentStart: Int
let end: Int
}
private struct BoxView {
let data: Data
let range: Range<Int>
var start: Int { range.lowerBound }
var end: Int { range.upperBound }
var header: BoxHeader {
let size32 = Int(data.readUInt32BE(at: start))
let type = data.readFourCC(at: start + 4)
if size32 == 1 {
let size64 = Int(data.readUInt64BE(at: start + 8))
return BoxHeader(
type: type,
size: size64,
headerSize: 16,
contentStart: start + 16,
end: start + size64
)
} else if size32 == 0 {
return BoxHeader(
type: type,
size: data.count - start,
headerSize: 8,
contentStart: start + 8,
end: data.count
)
} else {
return BoxHeader(
type: type,
size: size32,
headerSize: 8,
contentStart: start + 8,
end: start + size32
)
}
}
var contentStart: Int { header.contentStart }
}
private static func findTopLevelBox(type: String, in data: Data) throws -> Range<Int>? {
var cursor = 0
while cursor + 8 <= data.count {
let box = try readBoxHeader(at: cursor, data: data)
if box.type == type { return cursor..<box.end }
cursor = box.end
}
return nil
}
private static func findChildBox(type: String, within range: Range<Int>, in data: Data) throws -> Range<Int>? {
var cursor = range.lowerBound
while cursor + 8 <= range.upperBound {
let box = try readBoxHeader(at: cursor, data: data)
if box.type == type { return cursor..<min(box.end, range.upperBound) }
cursor = box.end
}
return nil
}
private static func readBoxHeader(at offset: Int, data: Data) throws -> BoxHeader {
guard offset + 8 <= data.count else {
throw HEICMakerNoteError.invalidHEIC("box header out of bounds")
}
let size32 = Int(data.readUInt32BE(at: offset))
let type = data.readFourCC(at: offset + 4)
if size32 == 1 {
guard offset + 16 <= data.count else {
throw HEICMakerNoteError.invalidHEIC("large size box header out of bounds")
}
let size64 = Int(data.readUInt64BE(at: offset + 8))
guard size64 >= 16 else {
throw HEICMakerNoteError.invalidHEIC("invalid box size")
}
return BoxHeader(type: type, size: size64, headerSize: 16, contentStart: offset + 16, end: offset + size64)
} else if size32 == 0 {
return BoxHeader(type: type, size: data.count - offset, headerSize: 8, contentStart: offset + 8, end: data.count)
} else {
guard size32 >= 8 else {
throw HEICMakerNoteError.invalidHEIC("invalid box size")
}
return BoxHeader(type: type, size: size32, headerSize: 8, contentStart: offset + 8, end: offset + size32)
}
}
// MARK: - iinf / infe Parsing
private static func parseIINFAndFindExifItemID(data: Data, iinfRange: Range<Int>) throws -> UInt32 {
let iinf = BoxView(data: data, range: iinfRange).header
var cursor = iinf.contentStart
guard cursor + 4 <= iinf.end else {
throw HEICMakerNoteError.invalidHEIC("iinf too short")
}
let version = data.readUInt8(at: cursor)
cursor += 4
let entryCount: UInt32
if version == 0 {
entryCount = UInt32(data.readUInt16BE(at: cursor))
cursor += 2
} else {
entryCount = data.readUInt32BE(at: cursor)
cursor += 4
}
var foundExif: UInt32?
var scanned: UInt32 = 0
while cursor + 8 <= iinf.end, scanned < entryCount {
let infe = try readBoxHeader(at: cursor, data: data)
guard infe.type == "infe" else {
cursor = infe.end
continue
}
var p = infe.contentStart
guard p + 4 <= infe.end else {
throw HEICMakerNoteError.invalidHEIC("infe too short")
}
let infeVersion = data.readUInt8(at: p)
p += 4
let itemID: UInt32
if infeVersion >= 3 {
itemID = data.readUInt32BE(at: p); p += 4
} else {
itemID = UInt32(data.readUInt16BE(at: p)); p += 2
}
p += 2 // item_protection_index
var itemType = ""
if infeVersion >= 2 {
guard p + 4 <= infe.end else {
throw HEICMakerNoteError.invalidHEIC("infe item_type out of bounds")
}
itemType = data.readFourCC(at: p)
p += 4
}
if itemType == "Exif" {
foundExif = itemID
break
}
cursor = infe.end
scanned += 1
}
guard let exifID = foundExif else {
throw HEICMakerNoteError.exifItemNotFound
}
return exifID
}
// MARK: - iloc Parsing
private struct ItemLocation {
let offset: UInt64
let length: UInt64
}
private struct ILOCInfo {
let version: UInt8
let offsetSize: Int
let lengthSize: Int
let baseOffsetSize: Int
let indexSize: Int
let itemEntries: [UInt32: ILOCItemEntry]
}
private struct ILOCItemEntry {
let itemID: UInt32
let extentOffsetPosition: Int // extent_offset
let extentLengthPosition: Int // extent_length
}
private static func parseILOCWithInfo(data: Data, ilocRange: Range<Int>) throws -> ([UInt32: ItemLocation], ILOCInfo) {
let iloc = BoxView(data: data, range: ilocRange).header
var cursor = iloc.contentStart
guard cursor + 4 <= iloc.end else {
throw HEICMakerNoteError.invalidHEIC("iloc too short")
}
let version = data.readUInt8(at: cursor)
cursor += 4
guard cursor + 2 <= iloc.end else {
throw HEICMakerNoteError.invalidHEIC("iloc header out of bounds")
}
let offsetSize = Int(data.readUInt8(at: cursor) >> 4)
let lengthSize = Int(data.readUInt8(at: cursor) & 0x0F)
cursor += 1
let baseOffsetSize = Int(data.readUInt8(at: cursor) >> 4)
let indexSize = Int(data.readUInt8(at: cursor) & 0x0F)
cursor += 1
let itemCount: UInt32
if version < 2 {
guard cursor + 2 <= iloc.end else {
throw HEICMakerNoteError.invalidHEIC("iloc item_count out of bounds")
}
itemCount = UInt32(data.readUInt16BE(at: cursor))
cursor += 2
} else {
guard cursor + 4 <= iloc.end else {
throw HEICMakerNoteError.invalidHEIC("iloc item_count out of bounds")
}
itemCount = data.readUInt32BE(at: cursor)
cursor += 4
}
var locations: [UInt32: ItemLocation] = [:]
var itemEntries: [UInt32: ILOCItemEntry] = [:]
for _ in 0..<itemCount {
guard cursor + 2 <= iloc.end else { break }
let itemID: UInt32
if version < 2 {
itemID = UInt32(data.readUInt16BE(at: cursor)); cursor += 2
} else {
guard cursor + 4 <= iloc.end else { break }
itemID = data.readUInt32BE(at: cursor); cursor += 4
}
if version == 1 || version == 2 {
guard cursor + 2 <= iloc.end else { break }
cursor += 2
}
guard cursor + 2 <= iloc.end else { break }
cursor += 2
guard cursor + baseOffsetSize <= iloc.end else { break }
let baseOffset = try data.readUIntBE(at: cursor, size: baseOffsetSize)
cursor += baseOffsetSize
guard cursor + 2 <= iloc.end else { break }
let extentCount = Int(data.readUInt16BE(at: cursor))
cursor += 2
var firstExtentOffset: UInt64 = 0
var firstExtentLength: UInt64 = 0
var extentOffsetPos = 0
var extentLengthPos = 0
for e in 0..<extentCount {
if (version == 1 || version == 2) && indexSize > 0 {
guard cursor + indexSize <= iloc.end else { break }
cursor += indexSize
}
guard cursor + offsetSize + lengthSize <= iloc.end else { break }
if e == 0 {
extentOffsetPos = cursor
}
let extentOffset = try data.readUIntBE(at: cursor, size: offsetSize)
cursor += offsetSize
if e == 0 {
extentLengthPos = cursor
}
let extentLength = try data.readUIntBE(at: cursor, size: lengthSize)
cursor += lengthSize
if e == 0 {
firstExtentOffset = extentOffset
firstExtentLength = extentLength
}
}
let fileOffset = baseOffset + firstExtentOffset
if firstExtentLength > 0 {
locations[itemID] = ItemLocation(offset: fileOffset, length: firstExtentLength)
itemEntries[itemID] = ILOCItemEntry(
itemID: itemID,
extentOffsetPosition: extentOffsetPos,
extentLengthPosition: extentLengthPos
)
}
}
let info = ILOCInfo(
version: version,
offsetSize: offsetSize,
lengthSize: lengthSize,
baseOffsetSize: baseOffsetSize,
indexSize: indexSize,
itemEntries: itemEntries
)
return (locations, info)
}
private static func updateILOC(
in fileData: inout Data,
ilocRange: Range<Int>,
ilocInfo: ILOCInfo,
itemID: UInt32,
newOffset: UInt64,
newLength: UInt64
) throws {
guard let entry = ilocInfo.itemEntries[itemID] else {
throw HEICMakerNoteError.exifLocationNotFound(itemID: itemID)
}
// offset
fileData.writeUIntBE(at: entry.extentOffsetPosition, value: newOffset, size: ilocInfo.offsetSize)
// length
fileData.writeUIntBE(at: entry.extentLengthPosition, value: newLength, size: ilocInfo.lengthSize)
}
// MARK: - EXIF/TIFF Patching
enum Endian {
case little
case big
}
}
// MARK: - Data Extensions
private extension Data {
func readUInt8(at offset: Int) -> UInt8 {
self[self.index(self.startIndex, offsetBy: offset)]
}
func readUInt16BE(at offset: Int) -> UInt16 {
let b0 = UInt16(readUInt8(at: offset))
let b1 = UInt16(readUInt8(at: offset + 1))
return (b0 << 8) | b1
}
func readUInt32BE(at offset: Int) -> UInt32 {
let b0 = UInt32(readUInt8(at: offset))
let b1 = UInt32(readUInt8(at: offset + 1))
let b2 = UInt32(readUInt8(at: offset + 2))
let b3 = UInt32(readUInt8(at: offset + 3))
return (b0 << 24) | (b1 << 16) | (b2 << 8) | b3
}
func readUInt64BE(at offset: Int) -> UInt64 {
var v: UInt64 = 0
for i in 0..<8 {
v = (v << 8) | UInt64(readUInt8(at: offset + i))
}
return v
}
func readFourCC(at offset: Int) -> String {
let bytes = self.subdata(in: offset..<(offset + 4))
return String(bytes: bytes, encoding: .ascii) ?? "????"
}
func readUIntBE(at offset: Int, size: Int) throws -> UInt64 {
if size == 0 { return 0 }
guard offset + size <= count else {
throw HEICMakerNoteError.invalidHEIC("Variable-length integer out of bounds")
}
var v: UInt64 = 0
for i in 0..<size {
v = (v << 8) | UInt64(readUInt8(at: offset + i))
}
return v
}
mutating func appendUInt32BE(_ value: UInt32) {
append(UInt8((value >> 24) & 0xFF))
append(UInt8((value >> 16) & 0xFF))
append(UInt8((value >> 8) & 0xFF))
append(UInt8(value & 0xFF))
}
mutating func writeUIntBE(at offset: Int, value: UInt64, size: Int) {
for i in 0..<size {
let byteIndex = self.index(self.startIndex, offsetBy: offset + i)
let shift = (size - 1 - i) * 8
self[byteIndex] = UInt8((value >> shift) & 0xFF)
}
}
}

View File

@@ -850,8 +850,17 @@ public actor LivePhotoBuilder {
exifDict[kCGImagePropertyExifPixelYDimension as String] = height exifDict[kCGImagePropertyExifPixelYDimension as String] = height
imageProperties[kCGImagePropertyExifDictionary as String] = exifDict imageProperties[kCGImagePropertyExifDictionary as String] = exifDict
// ContentIdentifier MakerNotes // HEIC Metadata 2024.01
// 使 ContentIdentifier Photos Live Photo // =========================================
// ContentIdentifierkey "17" MakerNotes
//
//
// 1. MOV still-image-time 0
// HEIC MakerNotes livephoto_wallpaper_root_cause_still_image_time
// 2. iOS 17+ Live Photo ContentIdentifier
// 3. live-wallpaper使 ContentIdentifier
//
// HEICMakerNotePatcher
let assetIdentifierKey = "17" // Content Identifier let assetIdentifierKey = "17" // Content Identifier
var makerAppleDict: [String: Any] = [:] var makerAppleDict: [String: Any] = [:]

View File

@@ -1,83 +0,0 @@
import Foundation
/// HEIC Apple MakerNotes LivePhotoVideoIndex
/// CGImageDestination Int64 MakerNotes
/// 使
public struct MakerNotesPatcher {
// MARK: - iPhone MakerNotes
/// ContentIdentifier MakerNotes 36 ASCII UUID + null
private static let contentIdentifierOffset = 0x580 // 1408
private static let contentIdentifierLength = 36
/// LivePhotoVideoIndex MakerNotes 8 Big-Endian Int64
private static let livePhotoVideoIndexOffset = 0x5a6 // 1446
private static let livePhotoVideoIndexLength = 8
/// iPhone MakerNotes iPhone 13 Pro Max Live Photo
/// Apple MakerNotes ContentIdentifier LivePhotoVideoIndex
private static let makerNotesTemplate: Data = {
// Base64 MakerNotes
let base64 = """
QXBwbGUgaU9TAAABTU0APQABAAkAAAABAAAAEAACAAcAAAIAAAAC8AADAAcAAABoAAAE8AAEAAkA\
AAABAAAAAQAFAAkAAAABAAAAqQAGAAkAAAABAAAApQAHAAkAAAABAAAAAQAIAAoAAAADAAAFWAAM\
AAoAAAACAAAFcAANAAkAAAABAAAAFwAOAAkAAAABAAAABAAQAAkAAAABAAAAAQARAAIAAAAlAAAF\
gAAUAAkAAAABAAAACgAXABAAAAABAAAFpgAZAAkAAAABAAAAAgAaAAIAAAAGAAAFrgAfAAkAAAAB\
AAAAAAAgAAIAAAAlAAAFtAAhAAoAAAABAAAF2gAjAAkAAAACAAAF4gAlABAAAAABAAAF6gAmAAkA\
AAABAAAAAwAnAAoAAAABAAAF8gAoAAkAAAABAAAAAQArAAIAAAAlAAAF+gAtAAkAAAABAAATXAAu\
AAkAAAABAAAAAQAvAAkAAAABAAAAMAAwAAoAAAABAAAGIAAzAAkAAAABAAAQAAA0AAkAAAABAAAA\
BAA1AAkAAAABAAAAAwA2AAkAAAABAADnJAA3AAkAAAABAAAABAA4AAkAAAABAAACPgA5AAkAAAAB\
AAAAAAA6AAkAAAABAAAAAAA7AAkAAAABAAAAAAA8AAkAAAABAAAABAA9AAkAAAABAAAAAAA/AAkA\
AAABAAAAOwBAAAcAAABQAAAGKABBAAkAAAABAAAAAABCAAkAAAABAAAAAABDAAkAAAABAAAAAABE\
AAkAAAABAAAAAABFAAkAAAABAAAAAABGAAkAAAABAAAAAABIAAkAAAABAAACPgBJAAkAAAABAAAA\
AABKAAkAAAABAAAAAgBNAAoAAAABAAAGeABOAAcAAAB5AAAGgABPAAcAAAArAAAG+gBSAAkAAAAB\
AAAAAQBTAAkAAAABAAAAAQBVAAkAAAABAAAAAQBYAAkAAAABAAAHAwBgAAkAAAABAAASAABhAAkA\
AAABAAAAGgAAAAC9AtMCxAKWAlsCIALqAbwBkgFrAUYBJQEIAfEA3QDMAAMDMwNAA/QCmQJWAg0C\
1QGoAX4BUwEtAQ8B9QDhAM0ALANqA2oDTAPrAnUCKALpAb4BhgFWATABEAH0AN4AyQA9A7oDwgNP\
A7gCXAIYAugBqwF2AUwBJgEGAeoA0gC+ANECrwPSAxIDZAIVAt4BqwF8AVABKQEHAekA0AC6AKgA\
zwFCAnYCBAK/AZYBcwFQAS8BDQHvANQAvQCpAJgAigDUAP8AGgEVARoB5QC6AMUAsACsAJkAggBy\
AGQAXABTAGAAaABqAGYAXABMAEcARwBCAEEAPgAvACQAJAAlACoARgBIAEcARAA/ADcAMwAvACoA\
KAAnACEAHQAfAB8AIAAxADIAMQAwAC4AKwAnACcAJQAfABwAGgAXABkAEwAUACkAKgAmACQAIgAg\
ACEAIgAgAB4AHAAZABgAFAAXABUAIgAcABoAGwAaABcAHAAdABkAGAAYABgAFwAaABgAGAAaABsA\
GgAYABUAEwAWABcAEwAWABcAFQAVABYAEwAUABoAFwAWABMAEgATABAADwATABMAEgANAA8ADwAP\
AAwAEwAVABIAEwANABUAEwATABIADAAPAAsAEAASAA8ADgARABEADwAMAAwADwAWABMAEgASABQA\
DQAPAAoADAAOAGJwbGlzdDAw1AECAwQFBgcIVWZsYWdzVXZhbHVlWXRpbWVzY2FsZVVlcG9jaBAB\
EwABVEUBR7fxEjuaygAQAAgRFx0nLS84PQAAAAAAAAEBAAAAAAAAAAkAAAAAAAAAAAAAAAAAAAA/\
///J3gAANk3//8R4AAe+////5bUAAVy+AAAAOwAAAQAAAAAnAAABAAAAAAAAAAAAAAAAAAAAALtA\
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABI\
RUlDAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAbwBwAGwAaQBzAHQAMAAwANQBAgMEBQYHCFEzUTFR\
MlEwEAQiAAAAACQ/AAAAEAEIERMVFxkbICUAAAAAAAABAQAAAAAAAAAJAAAAAAAAAAAAAAAAAAAA\
JwAC8/UAABV+YnBsaXN0MDDSAQIDSFExUTIQA6IFCtIGBwgJUzIuMVMyLjIjQEsf2IAAAACJQAAA\
AAAAAADSBgcLDCM/4hqAAAAAAKNAVMAAAAAAAAgNDxETFhsfIywlOkMAAAAAAAABAQAAAAAAAAAL\
AAAAAAAAAAAAAAAAAAAAQQAAAAA=
"""
return Data(base64Encoded: base64.replacingOccurrences(of: "\\\n", with: "").replacingOccurrences(of: "\n", with: ""))!
}()
/// MakerNotes
/// - Parameters:
/// - contentIdentifier: Live Photo Content Identifier (UUID )
/// - livePhotoVideoIndex: Live Photo Video Index ( Float32 bitPattern)
/// - Returns: MakerNotes
public static func createMakerNotes(
contentIdentifier: String,
livePhotoVideoIndex: Int64
) -> Data {
var data = makerNotesTemplate
// ContentIdentifier
let uuidData = contentIdentifier.data(using: .ascii)!
let paddedUUID = uuidData + Data(repeating: 0, count: max(0, contentIdentifierLength - uuidData.count))
data.replaceSubrange(contentIdentifierOffset..<(contentIdentifierOffset + contentIdentifierLength), with: paddedUUID.prefix(contentIdentifierLength))
// LivePhotoVideoIndex (Big-Endian)
var bigEndianValue = UInt64(bitPattern: livePhotoVideoIndex).bigEndian
let indexData = Data(bytes: &bigEndianValue, count: livePhotoVideoIndexLength)
data.replaceSubrange(livePhotoVideoIndexOffset..<(livePhotoVideoIndexOffset + livePhotoVideoIndexLength), with: indexData)
return data
}
}