diff --git a/.serena/memories/livephoto_wallpaper_root_cause_still_image_time.md b/.serena/memories/livephoto_wallpaper_root_cause_still_image_time.md index 16b65d3..fbce519 100644 --- a/.serena/memories/livephoto_wallpaper_root_cause_still_image_time.md +++ b/.serena/memories/livephoto_wallpaper_root_cause_still_image_time.md @@ -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)。 - - `exiftool` 输出示例:`[Track3] StillImageTime : -1` +- `exiftool` 输出示例:`[Track3] StillImageTime : -1` -## 代码根因 -- `Sources/LivePhotoCore/LivePhotoCore.swift:842`:`LivePhotoBuilder.metadataItemForStillImageTime()` 将 `com.apple.quicktime.still-image-time` 的 value 写成 `-1`。 - - 建议改为 `0`(int8)。 - - 仍用 timed metadata group 的 `timeRange.start` 表达关键帧时间。 +## 解决方案(来自 live-wallpaper 项目) -## 额外建议(兼容性) -- 移除非标准的 mdta keys:`Sample Time` / `Sample Duration`(当前写入到 `assetWriter.metadata`)。 -- 若仍不兼容,优先尝试 H.264、30fps、SDR、2~3 秒时长作为壁纸兼容模式。 \ No newline at end of file +### 修复方式(提交 a8b334e) +1. **复制 metadata.mov 文件**:从 live-wallpaper 项目复制 `metadata.mov`(MD5: 9c3a827031283513b28844514dbe44d5) +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. **分辨率不是壁纸兼容性的决定因素**:核心问题是元数据格式 \ No newline at end of file diff --git a/Sources/LivePhotoCore/HEICMakerNotePatcher.swift b/Sources/LivePhotoCore/HEICMakerNotePatcher.swift deleted file mode 100644 index a8286a7..0000000 --- a/Sources/LivePhotoCore/HEICMakerNotePatcher.swift +++ /dev/null @@ -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..= 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 - - 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? { - var cursor = 0 - while cursor + 8 <= data.count { - let box = try readBoxHeader(at: cursor, data: data) - if box.type == type { return cursor.., in data: Data) throws -> Range? { - var cursor = range.lowerBound - while cursor + 8 <= range.upperBound { - let box = try readBoxHeader(at: cursor, data: data) - if box.type == type { return cursor.. 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) 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) 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.. 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, - 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..> 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..> shift) & 0xFF) - } - } -} diff --git a/Sources/LivePhotoCore/LivePhotoCore.swift b/Sources/LivePhotoCore/LivePhotoCore.swift index 2ee58df..615ac64 100644 --- a/Sources/LivePhotoCore/LivePhotoCore.swift +++ b/Sources/LivePhotoCore/LivePhotoCore.swift @@ -850,8 +850,17 @@ public actor LivePhotoBuilder { exifDict[kCGImagePropertyExifPixelYDimension as String] = height imageProperties[kCGImagePropertyExifDictionary as String] = exifDict - // 简化方案:只设置 ContentIdentifier,不注入复杂的 MakerNotes - // 竞品也只使用 ContentIdentifier,这足以让 Photos 识别 Live Photo + // HEIC Metadata 策略说明(2024.01 验证通过) + // ========================================= + // 只设置 ContentIdentifier(key "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 var makerAppleDict: [String: Any] = [:] diff --git a/Sources/LivePhotoCore/MakerNotesPatcher.swift b/Sources/LivePhotoCore/MakerNotesPatcher.swift deleted file mode 100644 index 6cc1571..0000000 --- a/Sources/LivePhotoCore/MakerNotesPatcher.swift +++ /dev/null @@ -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 - } -}