100 lines
3.5 KiB
Swift
100 lines
3.5 KiB
Swift
// Copyright 2024-present 650 Industries. All rights reserved.
|
||
|
||
import AVFoundation
|
||
import UniformTypeIdentifiers
|
||
import Photos
|
||
import ExpoModulesCore
|
||
|
||
internal struct VideoUtils {
|
||
static func tryCopyingVideo(at: URL, to: URL) throws {
|
||
do {
|
||
// we copy the file as `moveItem(at:,to:)` throws an error in iOS 13 due to missing permissions
|
||
try FileManager.default.copyItem(at: at, to: to)
|
||
} catch {
|
||
throw FailedToPickVideoException()
|
||
.causedBy(error)
|
||
}
|
||
}
|
||
|
||
/**
|
||
@returns duration in milliseconds
|
||
*/
|
||
static func readDurationFrom(url: URL) -> Double {
|
||
let asset = AVURLAsset(url: url)
|
||
return Double(asset.duration.value) / Double(asset.duration.timescale) * 1000
|
||
}
|
||
|
||
static func readSizeFrom(url: URL) -> CGSize? {
|
||
let asset = AVURLAsset(url: url)
|
||
guard let assetTrack = asset.tracks(withMediaType: .video).first else {
|
||
return nil
|
||
}
|
||
// The video could be rotated and the resulting transform can result in a negative width/height.
|
||
let size = assetTrack.naturalSize.applying(assetTrack.preferredTransform)
|
||
return CGSize(width: abs(size.width), height: abs(size.height))
|
||
}
|
||
|
||
static func readVideoUrlFrom(mediaInfo: MediaInfo) -> URL? {
|
||
return mediaInfo[.mediaURL] as? URL ?? mediaInfo[.referenceURL] as? URL
|
||
}
|
||
|
||
/**
|
||
Asynchronously transcodes asset provided as `sourceAssetUrl` according to `exportPreset`.
|
||
Result URL is returned to the `completion` closure.
|
||
Transcoded video is saved at `destinationUrl`, unless `exportPreset` is set to `passthrough`.
|
||
In this case, `sourceAssetUrl` is returned.
|
||
*/
|
||
static func transcodeVideoAsync(
|
||
sourceAssetUrl: URL,
|
||
destinationUrl: URL,
|
||
outputFileType: AVFileType,
|
||
exportPreset: VideoExportPreset
|
||
) async throws -> URL {
|
||
if case .passthrough = exportPreset {
|
||
return sourceAssetUrl
|
||
}
|
||
let asset = AVURLAsset(url: sourceAssetUrl)
|
||
let preset = exportPreset.toAVAssetExportPreset()
|
||
let canBeTranscoded = await AVAssetExportSession.compatibility(ofExportPreset: preset, with: asset, outputFileType: outputFileType)
|
||
|
||
guard canBeTranscoded else {
|
||
throw UnsupportedVideoExportPresetException(preset.description)
|
||
}
|
||
guard let exportSession = AVAssetExportSession(asset: asset, presetName: preset) else {
|
||
throw FailedToTranscodeVideoException()
|
||
}
|
||
|
||
exportSession.outputFileType = outputFileType
|
||
exportSession.outputURL = destinationUrl
|
||
|
||
await exportSession.export()
|
||
|
||
if case exportSession.status = .failed {
|
||
let error = exportSession.error
|
||
throw FailedToTranscodeVideoException().causedBy(error)
|
||
}
|
||
return destinationUrl
|
||
}
|
||
|
||
static func loadVideoRepresentation(provider: NSItemProvider, urlTransformer: @escaping (URL) throws -> URL) async throws -> URL {
|
||
return try await withCheckedThrowingContinuation { continuation in
|
||
let typeId = UTType.movie.identifier
|
||
|
||
provider.loadFileRepresentation(forTypeIdentifier: typeId) { url, error in
|
||
guard let url else {
|
||
return continuation.resume(throwing: FailedToReadVideoException().causedBy(error))
|
||
}
|
||
do {
|
||
// The provided URL is only temporary – the system deletes that file when the completion handler returns.
|
||
// Since we're using it asynchronously, we need to copy the video to another location.
|
||
let newUrl = try urlTransformer(url)
|
||
try VideoUtils.tryCopyingVideo(at: url, to: newUrl)
|
||
continuation.resume(returning: newUrl)
|
||
} catch {
|
||
continuation.resume(throwing: error)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|