Files
lively/modules/livephoto-ios/ios/LivePhotoModule.swift
Mathis Pruvot 187d3391d0 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.
2026-05-28 11:50:28 +00:00

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")
}
}
}
}
}