v0.15: Live Photo 生产规则 — 平台限制、拼图版式、长视频处理、发布提醒、可见文案规则

新增 Live Photo 正式能力:单视频、二宫格、三宫格、四宫格、三连拼图、
长视频低成本诊断。增加 references/live-photo-production.md 生产规范、
scripts/ 工具链(contact sheet / .pvt 打包 / Apple metadata)、
validator R8 visual bounds + R9 title gap、文档约束测试。

由 Codex 完成主体实现。
This commit is contained in:
郭浩 committed 2026-07-01 16:11:34 +08:00
1 parent 032782ff67
commit f492caeb8b
20 files changed
+1982 -22

No files matched your search

+140
View File
@@ -0,0 +1,140 @@
import Foundation
import AVFoundation
import CoreMedia
guard CommandLine.arguments.count == 4 else {
fputs("Usage: swift add-livephoto-mov-metadata.swift <input.mov> <output.mov> <asset-id>\n", stderr)
exit(2)
}
let inputURL = URL(fileURLWithPath: CommandLine.arguments[1])
let outputURL = URL(fileURLWithPath: CommandLine.arguments[2])
let assetIdentifier = CommandLine.arguments[3]
try? FileManager.default.removeItem(at: outputURL)
func quickTimeMetadataItem(key: String, value: NSCopying & NSObjectProtocol, dataType: String) -> AVMutableMetadataItem {
let item = AVMutableMetadataItem()
item.keySpace = .quickTimeMetadata
item.key = key as NSString
item.value = value
item.dataType = dataType
return item
}
let asset = AVURLAsset(url: inputURL)
let semaphore = DispatchSemaphore(value: 0)
var didFail = false
Task {
do {
let tracks = try await asset.loadTracks(withMediaType: .video)
guard let videoTrack = tracks.first else {
throw NSError(domain: "LivePhotoMetadata", code: 1, userInfo: [NSLocalizedDescriptionKey: "No video track"])
}
let reader = try AVAssetReader(asset: asset)
let readerOutput = AVAssetReaderTrackOutput(track: videoTrack, outputSettings: nil)
readerOutput.alwaysCopiesSampleData = false
guard reader.canAdd(readerOutput) else {
throw NSError(domain: "LivePhotoMetadata", code: 2, userInfo: [NSLocalizedDescriptionKey: "Cannot add reader output"])
}
reader.add(readerOutput)
let writer = try AVAssetWriter(outputURL: outputURL, fileType: .mov)
let formatDescriptions = try await videoTrack.load(.formatDescriptions)
let sourceFormatHint = formatDescriptions.first
let writerInput = AVAssetWriterInput(mediaType: .video, outputSettings: nil, sourceFormatHint: sourceFormatHint)
writerInput.expectsMediaDataInRealTime = false
guard writer.canAdd(writerInput) else {
throw NSError(domain: "LivePhotoMetadata", code: 3, userInfo: [NSLocalizedDescriptionKey: "Cannot add writer input"])
}
writer.add(writerInput)
let contentIdentifier = quickTimeMetadataItem(
key: "com.apple.quicktime.content.identifier",
value: assetIdentifier as NSString,
dataType: "com.apple.metadata.datatype.UTF-8"
)
writer.metadata = [contentIdentifier]
let metadataSpec: [String: Any] = [
kCMMetadataFormatDescriptionMetadataSpecificationKey_Identifier as String: "mdta/com.apple.quicktime.still-image-time",
kCMMetadataFormatDescriptionMetadataSpecificationKey_DataType as String: "com.apple.metadata.datatype.int8"
]
var metadataDescription: CMFormatDescription?
let status = CMMetadataFormatDescriptionCreateWithMetadataSpecifications(
allocator: kCFAllocatorDefault,
metadataType: kCMMetadataFormatType_Boxed,
metadataSpecifications: [metadataSpec] as CFArray,
formatDescriptionOut: &metadataDescription
)
guard status == noErr, let metadataDescription else {
throw NSError(domain: "LivePhotoMetadata", code: 4, userInfo: [NSLocalizedDescriptionKey: "Cannot create metadata description"])
}
let metadataInput = AVAssetWriterInput(mediaType: .metadata, outputSettings: nil, sourceFormatHint: metadataDescription)
let metadataAdaptor = AVAssetWriterInputMetadataAdaptor(assetWriterInput: metadataInput)
guard writer.canAdd(metadataInput) else {
throw NSError(domain: "LivePhotoMetadata", code: 5, userInfo: [NSLocalizedDescriptionKey: "Cannot add metadata input"])
}
writer.add(metadataInput)
guard writer.startWriting() else {
throw writer.error ?? NSError(domain: "LivePhotoMetadata", code: 6)
}
guard reader.startReading() else {
throw reader.error ?? NSError(domain: "LivePhotoMetadata", code: 7)
}
writer.startSession(atSourceTime: .zero)
let stillImageTime = quickTimeMetadataItem(
key: "com.apple.quicktime.still-image-time",
value: NSNumber(value: Int8(-1)),
dataType: "com.apple.metadata.datatype.int8"
)
let stillGroup = AVTimedMetadataGroup(
items: [stillImageTime],
timeRange: CMTimeRange(start: .zero, duration: CMTime(value: 1, timescale: 600))
)
guard metadataAdaptor.append(stillGroup) else {
throw writer.error ?? NSError(domain: "LivePhotoMetadata", code: 8, userInfo: [NSLocalizedDescriptionKey: "Could not append still image time metadata"])
}
metadataInput.markAsFinished()
let queue = DispatchQueue(label: "livephoto.video.writer")
writerInput.requestMediaDataWhenReady(on: queue) {
while writerInput.isReadyForMoreMediaData {
if let sample = readerOutput.copyNextSampleBuffer() {
if !writerInput.append(sample) {
didFail = true
reader.cancelReading()
writerInput.markAsFinished()
writer.cancelWriting()
semaphore.signal()
return
}
} else {
writerInput.markAsFinished()
writer.finishWriting {
semaphore.signal()
}
return
}
}
}
} catch {
didFail = true
fputs("Error: \(error.localizedDescription)\n", stderr)
semaphore.signal()
}
}
semaphore.wait()
if didFail {
exit(1)
}
print("Wrote QuickTime Live Photo metadata: \(assetIdentifier)")