feat: add iOS Live Photo export native module
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.
This commit is contained in:
82
modules/livephoto-ios/ios/LivePhotoModule.swift
Normal file
82
modules/livephoto-ios/ios/LivePhotoModule.swift
Normal file
@@ -0,0 +1,82 @@
|
||||
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")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user