I’m trying to set a Live Photo as a live wallpaper on iOS. I’ve saved the Live Photo to my Photos library, but when I attempt to set it as the wallpaper, the Live Photo effect option is grayed out.
I try all scenarios for correct video; 3second 1 second
I downloaded a video from a livewallapaper app and i use it because of i thnik my video is not correct. Still dont working.
import Foundation
import UIKit
import Photos
import React
import AVFoundation
import MobileCoreServices
@objc(LivePhotoModule)
class LivePhotoModule: NSObject {
// MARK: - React Native Bridge Method
@objc
func saveLivePhoto(_ videoUri: String, albumName: String, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
let fileURL = URL(fileURLWithPath: videoUri)
guard FileManager.default.fileExists(atPath: fileURL.path) else {
return reject("E_FILE", "Video dosyası bulunamadı", nil)
}
// İzin İste
PHPhotoLibrary.requestAuthorization { status in
guard status == .authorized || status == .limited else {
return reject("E_PERM", "Galeri izni verilmedi", nil)
}
// İşlemi Başlat
self.generateLivePhoto(from: fileURL) { (pairedImageURL, pairedVideoURL) in
guard let imageURL = pairedImageURL, let videoURL = pairedVideoURL else {
return reject("E_GEN", "Live Photo oluşturulamadı", nil)
}
// Galeriye Kaydet
self.saveToLibrary(imageURL: imageURL, videoURL: videoURL, albumName: albumName, resolve: resolve, reject: reject)
}
}
}
// MARK: - Generation Logic (Based on GitHub Reference)
private func generateLivePhoto(from videoURL: URL, completion: @escaping (URL?, URL?) -> Void) {
let assetIdentifier = UUID().uuidString
let cacheDirectory = FileManager.default.temporaryDirectory
// 1. Videodan Kapak Resmi Üret (Tam ortasından - 0.5)
guard let keyPhotoURL = self.generateKeyPhoto(from: videoURL, atPercent: 0.5) else {
print("Kapak resmi üretilemedi")
completion(nil, nil)
return
}
// 2. Resme Metadata Ekle
let finalImageURL = cacheDirectory.appendingPathComponent(assetIdentifier).appendingPathExtension("jpg")
guard let savedImageURL = self.addAssetID(assetIdentifier, toImage: keyPhotoURL, saveTo: finalImageURL) else {
completion(nil, nil)
return
}
// 3. Videoya Metadata ve Still Image Time Ekle
let finalVideoURL = cacheDirectory.appendingPathComponent(assetIdentifier).appendingPathExtension("mov")
self.addAssetID(assetIdentifier, toVideo: videoURL, saveTo: finalVideoURL) { (outputURL) in
completion(savedImageURL, outputURL)
}
}
// MARK: - Image Helpers
private func generateKeyPhoto(from videoURL: URL, atPercent percent: Float) -> URL? {
let asset = AVURLAsset(url: videoURL)
let imageGenerator = AVAssetImageGenerator(asset: asset)
imageGenerator.appliesPreferredTrackTransform = true
imageGenerator.requestedTimeToleranceBefore = .zero
imageGenerator.requestedTimeToleranceAfter = .zero
let time = asset.duration
let targetTimeValue = Int64(Float(time.value) * percent)
let targetTime = CMTimeMake(value: targetTimeValue, timescale: time.timescale)
do {
let cgImage = try imageGenerator.copyCGImage(at: targetTime, actualTime: nil)
let image = UIImage(cgImage: cgImage)
let tempURL = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString).appendingPathExtension("jpg")
if let data = image.jpegData(compressionQuality: 1.0) {
try data.write(to: tempURL)
return tempURL
}
} catch {
print("Frame yakalama hatası: \(error)")
}
return nil
}
private func addAssetID(_ assetIdentifier: String, toImage imageURL: URL, saveTo destinationURL: URL) -> URL? {
guard let imageDestination = CGImageDestinationCreateWithURL(destinationURL as CFURL, kUTTypeJPEG, 1, nil),
let imageSource = CGImageSourceCreateWithURL(imageURL as CFURL, nil),
var imageProperties = CGImageSourceCopyPropertiesAtIndex(imageSource, 0, nil) as? [AnyHashable : Any] else { return nil }
let makerNote = ["17": assetIdentifier]
imageProperties[kCGImagePropertyMakerAppleDictionary] = makerNote
CGImageDestinationAddImageFromSource(imageDestination, imageSource, 0, imageProperties as CFDictionary)
CGImageDestinationFinalize(imageDestination)
return destinationURL
}
// MARK: - Video Helpers (The Complex Part)
private func addAssetID(_ assetIdentifier: String, toVideo videoURL: URL, saveTo destinationURL: URL, completion: @escaping (URL?) -> Void) {
let videoAsset = AVURLAsset(url: videoURL)
guard let videoTrack = videoAsset.tracks(withMediaType: .video).first else { return completion(nil) }
do {
let assetWriter = try AVAssetWriter(outputURL: destinationURL, fileType: .mov)
let videoReader = try AVAssetReader(asset: videoAsset)
// Reader Output
let readerOutputSettings = [kCVPixelBufferPixelFormatTypeKey as String: NSNumber(value: kCVPixelFormatType_32BGRA)]
let videoReaderOutput = AVAssetReaderTrackOutput(track: videoTrack, outputSettings: readerOutputSettings)
videoReader.add(videoReaderOutput)
// Writer Input
let writerOutputSettings: [String: Any] = [
AVVideoCodecKey: AVVideoCodecType.h264,
AVVideoWidthKey: videoTrack.naturalSize.width,
AVVideoHeightKey: videoTrack.naturalSize.height,
]
let videoWriterInput = AVAssetWriterInput(mediaType: .video, outputSettings: writerOutputSettings)
videoWriterInput.transform = videoTrack.preferredTransform
videoWriterInput.expectsMediaDataInRealTime = true
assetWriter.add(videoWriterInput)
// Metadata: Asset ID
let idItem = AVMutableMetadataItem()
idItem.key = "com.apple.quicktime.content.identifier" as (NSCopying & NSObjectProtocol)
idItem.keySpace = AVMetadataKeySpace.quickTimeMetadata
idItem.value = assetIdentifier as (NSCopying & NSObjectProtocol)
idItem.dataType = "com.apple.metadata.datatype.UTF-8"
assetWriter.metadata = [idItem]
// Metadata: Still Image Time (KRİTİK KISIM)
let spec: NSDictionary = [
kCMMetadataFormatDescriptionMetadataSpecificationKey_Identifier as NSString: "mdta/com.apple.quicktime.still-image-time",
kCMMetadataFormatDescriptionMetadataSpecificationKey_DataType as NSString: "com.apple.metadata.datatype.int8"
]
var desc: CMFormatDescription? = nil
CMMetadataFormatDescriptionCreateWithMetadataSpecifications(allocator: kCFAllocatorDefault, metadataType: kCMMetadataFormatType_Boxed, metadataSpecifications: [spec] as CFArray, formatDescriptionOut: &desc)
let metaInput = AVAssetWriterInput(mediaType: .metadata, outputSettings: nil, sourceFormatHint: desc)
let adaptor = AVAssetWriterInputMetadataAdaptor(assetWriterInput: metaInput)
assetWriter.add(metaInput)
// Start
assetWriter.startWriting()
videoReader.startReading()
assetWriter.startSession(atSourceTime: .zero)
// Still Image Time Verisini Yaz
let stillTimePercent: Float = 0.5
let duration = videoAsset.duration
let frameCount = Int(CMTimeGetSeconds(duration) * Float64(videoTrack.nominalFrameRate))
let frameDuration = Int64(Float(duration.value) / Float(frameCount))
let targetTimeValue = Int64(Float(duration.value) * stillTimePercent)
let stillTime = CMTimeMake(value: targetTimeValue, timescale: duration.timescale)
let stillTimeRange = CMTimeRangeMake(start: stillTime, duration: CMTimeMake(value: frameDuration, timescale: duration.timescale))
let stillItem = AVMutableMetadataItem()
stillItem.key = "com.apple.quicktime.still-image-time" as (NSCopying & NSObjectProtocol)
stillItem.keySpace = AVMetadataKeySpace.quickTimeMetadata
stillItem.value = 0 as (NSCopying & NSObjectProtocol)
stillItem.dataType = "com.apple.metadata.datatype.int8"
let timedGroup = AVTimedMetadataGroup(items: [stillItem], timeRange: stillTimeRange)
adaptor.append(timedGroup)
// Video Yazma Döngüsü
let queue = DispatchQueue(label: "rwQueue")
videoWriterInput.requestMediaDataWhenReady(on: queue) {
while videoWriterInput.isReadyForMoreMediaData {
if let buffer = videoReaderOutput.copyNextSampleBuffer() {
videoWriterInput.append(buffer)
} else {
videoWriterInput.markAsFinished()
metaInput.markAsFinished()
assetWriter.finishWriting {
DispatchQueue.main.async {
if assetWriter.status == .completed {
completion(destinationURL)
} else {
print("Writer error: \(String(describing: assetWriter.error))")
completion(nil)
}
}
}
videoReader.cancelReading()
break
}
}
}
} catch {
print("Setup hatası: \(error)")
completion(nil)
}
}
// MARK: - Save to Library
private func saveToLibrary(imageURL: URL, videoURL: URL, albumName: String, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
PHPhotoLibrary.shared().performChanges({
let req = PHAssetCreationRequest.forAsset()
let opts = PHAssetResourceCreationOptions()
opts.shouldMoveFile = true
req.addResource(with: .photo, fileURL: imageURL, options: opts)
req.addResource(with: .pairedVideo, fileURL: videoURL, options: opts)
}) { success, error in
if success {
resolve("Live Photo Saved!")
} else {
reject("E_SAVE", error?.localizedDescription ?? "Bilinmeyen hata", nil)
}
}
}
}