AVAssetWriter pipeline generating 3s MOV at 30fps with Apple content identifier metadata and still-image-time. HEIC generation with maker note pairing. PHAssetCreation with photo + pairedVideo resources. CIFilter-based effects for Ken Burns, Color Shift, Vignette, and Glitch.
83 lines
2.8 KiB
Swift
83 lines
2.8 KiB
Swift
import ExpoModulesCore
|
|
import Photos
|
|
import PhotosUI
|
|
|
|
public class LivePhotoModule: Module {
|
|
public func definition() -> ModuleDefinition {
|
|
Name("LivePhoto")
|
|
|
|
/// Generate a Live Photo from an image + animation config and save to photo library.
|
|
/// Returns the local identifier of the saved PHAsset.
|
|
AsyncFunction("exportLivePhoto") { (configJson: String, promise: Promise) in
|
|
guard let data = configJson.data(using: .utf8),
|
|
let config = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
|
|
let sourceUri = config["sourceUri"] as? String else {
|
|
promise.reject("INVALID_CONFIG", "Invalid wallpaper config JSON")
|
|
return
|
|
}
|
|
|
|
let shaderId = config["animation"] as? String ?? "ken-burns"
|
|
let uniforms = config["uniforms"] as? [String: Double] ?? [:]
|
|
let intensity = Float(uniforms["intensity"] ?? 0.5)
|
|
let speed = Float(uniforms["speed"] ?? 0.3)
|
|
let direction = Float(uniforms["direction"] ?? 0.0)
|
|
|
|
// Resolve image path
|
|
let imagePath: String
|
|
if sourceUri.hasPrefix("file://") {
|
|
imagePath = String(sourceUri.dropFirst(7))
|
|
} else {
|
|
imagePath = sourceUri
|
|
}
|
|
|
|
guard let image = UIImage(contentsOfFile: imagePath) else {
|
|
promise.reject("IMAGE_NOT_FOUND", "Could not load image at \(imagePath)")
|
|
return
|
|
}
|
|
|
|
let screenSize = UIScreen.main.bounds.size
|
|
let scale = UIScreen.main.scale
|
|
let targetSize = CGSize(width: screenSize.width * scale, height: screenSize.height * scale)
|
|
|
|
Task {
|
|
do {
|
|
let exporter = LivePhotoExporter()
|
|
let assetId = try await exporter.export(
|
|
image: image,
|
|
targetSize: targetSize,
|
|
shaderId: shaderId,
|
|
intensity: intensity,
|
|
speed: speed,
|
|
direction: direction
|
|
)
|
|
promise.resolve(assetId)
|
|
} catch {
|
|
promise.reject("EXPORT_FAILED", error.localizedDescription)
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Check if the photo library is accessible.
|
|
AsyncFunction("checkPermission") { () -> String in
|
|
let status = PHPhotoLibrary.authorizationStatus(for: .readWrite)
|
|
switch status {
|
|
case .authorized, .limited: return "granted"
|
|
case .denied, .restricted: return "denied"
|
|
case .notDetermined: return "undetermined"
|
|
@unknown default: return "unknown"
|
|
}
|
|
}
|
|
|
|
/// Request photo library write permission.
|
|
AsyncFunction("requestPermission") { (promise: Promise) in
|
|
PHPhotoLibrary.requestAuthorization(for: .readWrite) { status in
|
|
switch status {
|
|
case .authorized, .limited: promise.resolve("granted")
|
|
case .denied, .restricted: promise.resolve("denied")
|
|
default: promise.resolve("denied")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|