-
Notifications
You must be signed in to change notification settings - Fork 107
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
7 changed files
with
451 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
135 changes: 135 additions & 0 deletions
135
Cloudinary/Classes/ios/Uploader/CLDVideoPreprocessChain.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,135 @@ | ||
// | ||
// CLDVideoPreprocessChain.swift | ||
// | ||
// Copyright (c) 2017 Cloudinary (http://cloudinary.com) | ||
// | ||
// Permission is hereby granted, free of charge, to any person obtaining a copy | ||
// of this software and associated documentation files (the "Software"), to deal | ||
// in the Software without restriction, including without limitation the rights | ||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | ||
// copies of the Software, and to permit persons to whom the Software is | ||
// furnished to do so, subject to the following conditions: | ||
// | ||
// The above copyright notice and this permission notice shall be included in all | ||
// copies or substantial portions of the Software. | ||
// | ||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | ||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | ||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | ||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | ||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | ||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | ||
// SOFTWARE. | ||
// | ||
|
||
import Foundation | ||
import AVKit | ||
|
||
public typealias CLDVideoPreprocessStep = (CLDVideoTranscode) throws -> CLDVideoTranscode | ||
|
||
public class CLDVideoPreprocessChain: CLDPreprocessChain<CLDVideoTranscode> { | ||
private var outputURL: URL? | ||
|
||
public override init() { | ||
super.init() | ||
} | ||
|
||
internal override func decodeResource(_ resourceData: Any) throws -> CLDVideoTranscode? { | ||
if let url = resourceData as? URL { | ||
return CLDVideoTranscode(sourceURL: url) | ||
} else if let data = resourceData as? Data { | ||
return try handleLargeVideoData(data) | ||
} else { | ||
throw CLDError.error(code: CLDError.CloudinaryErrorCode.preprocessingError, message: "Resource type should be URL or Data only!") | ||
} | ||
} | ||
|
||
private func handleLargeVideoData(_ data: Data) throws -> CLDVideoTranscode? { | ||
var tempDirectory: URL! | ||
if #available(iOS 10.0, *) { | ||
tempDirectory = FileManager.default.temporaryDirectory | ||
} else { | ||
tempDirectory = URL(fileURLWithPath: NSTemporaryDirectory()) | ||
} | ||
let tempURL = tempDirectory.appendingPathComponent(UUID().uuidString).appendingPathExtension("mp4") | ||
do { | ||
try data.write(to: tempURL) | ||
return CLDVideoTranscode(sourceURL: tempURL) | ||
} catch { | ||
throw CLDError.error(code: CLDError.CloudinaryErrorCode.preprocessingError, message: "Failed to write data to temporary file: \(error.localizedDescription)") | ||
} | ||
} | ||
|
||
public func setOutputFormat(format: AVFileType) -> Self { | ||
addStep { videoTranscode in | ||
try videoTranscode.setOutputFormat(format: format) | ||
return videoTranscode | ||
} | ||
return self | ||
} | ||
|
||
public func setOutputDimensions(dimensions: CGSize) -> Self { | ||
addStep { videoTranscode in | ||
videoTranscode.setOutputDimensions(dimensions: dimensions) | ||
return videoTranscode | ||
} | ||
return self | ||
} | ||
|
||
public func setCompressionPreset(preset: String) -> Self { | ||
addStep { videoTranscode in | ||
videoTranscode.setCompressionPreset(preset: preset) | ||
return videoTranscode | ||
} | ||
return self | ||
} | ||
|
||
internal override func execute(resourceData: Any) throws -> URL { | ||
try verifyEncoder() | ||
|
||
guard let videoTranscode = try decodeResource(resourceData) else { | ||
throw CLDError.error(code: CLDError.CloudinaryErrorCode.preprocessingError, message: "Error decoding resource") | ||
} | ||
|
||
let dispatchGroup = DispatchGroup() | ||
var resultURL: URL? | ||
var resultError: Error? | ||
|
||
dispatchGroup.enter() | ||
|
||
DispatchQueue.global(qos: .background).async { | ||
do { | ||
var processedTranscode = videoTranscode | ||
for preprocess in self.chain { | ||
processedTranscode = try preprocess(processedTranscode) | ||
} | ||
|
||
processedTranscode.transcode { success, error in | ||
if success, let outputURL = processedTranscode.outputURL { | ||
resultURL = outputURL | ||
} else { | ||
resultError = error ?? CLDError.error(code: CLDError.CloudinaryErrorCode.preprocessingError, message: "Error transcoding video") | ||
} | ||
dispatchGroup.leave() | ||
} | ||
} catch { | ||
resultError = error | ||
dispatchGroup.leave() | ||
} | ||
} | ||
|
||
dispatchGroup.wait() | ||
|
||
if let finalOutputURL = resultURL { | ||
return finalOutputURL | ||
} else { | ||
throw resultError ?? CLDError.error(code: CLDError.CloudinaryErrorCode.preprocessingError, message: "Unknown error") | ||
} | ||
} | ||
|
||
internal override func verifyEncoder() throws { | ||
if encoder == nil { | ||
encoder = { _ in self.outputURL } | ||
} | ||
} | ||
} |
52 changes: 52 additions & 0 deletions
52
Cloudinary/Classes/ios/Uploader/CLDVideoPreprocessHelpers.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,52 @@ | ||
import Foundation | ||
import AVKit | ||
|
||
public class CLDVideoPreprocessHelpers { | ||
|
||
private var steps: [(CLDVideoTranscode) throws -> CLDVideoTranscode] = [] | ||
|
||
public func addStep(_ step: @escaping (CLDVideoTranscode) throws -> CLDVideoTranscode) { | ||
steps.append(step) | ||
} | ||
|
||
public static func setOutputFormat(format: AVFileType) -> CLDVideoPreprocessStep { | ||
return { videoTranscode in | ||
do { | ||
try videoTranscode.setOutputFormat(format: format) | ||
} catch { | ||
throw error | ||
} | ||
return videoTranscode | ||
} | ||
} | ||
|
||
public static func setOutputDimensions(dimensions: CGSize) -> CLDVideoPreprocessStep { | ||
return { videoTranscode in | ||
videoTranscode.setOutputDimensions(dimensions: dimensions) | ||
return videoTranscode | ||
} | ||
} | ||
|
||
public static func setCompressionPreset(preset: String) -> CLDVideoPreprocessStep { | ||
return { videoTranscode in | ||
videoTranscode.setCompressionPreset(preset: preset) | ||
return videoTranscode | ||
} | ||
} | ||
|
||
public static func dimensionsValidator(minWidth: CGFloat, maxWidth: CGFloat, minHeight: CGFloat, maxHeight: CGFloat) -> CLDVideoPreprocessStep { | ||
return { videoTranscode in | ||
guard let dimensions = videoTranscode.outputDimensions else { | ||
throw NSError(domain: "CLDVideoPreprocessHelpers", code: -1, userInfo: [NSLocalizedDescriptionKey: "Dimensions not set"]) | ||
} | ||
if dimensions.width < minWidth || dimensions.width > maxWidth || dimensions.height < minHeight || dimensions.height > maxHeight { | ||
throw VideoPreprocessError.dimensionsOutOfRange | ||
} | ||
return videoTranscode | ||
} | ||
} | ||
} | ||
|
||
enum VideoPreprocessError: Error { | ||
case dimensionsOutOfRange | ||
} |
131 changes: 131 additions & 0 deletions
131
Cloudinary/Classes/ios/Uploader/CLDVideoTranscode.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,131 @@ | ||
import Foundation | ||
import AVFoundation | ||
|
||
public class CLDVideoTranscode { | ||
let sourceURL: URL | ||
var outputURL: URL? | ||
var outputFormat: AVFileType = .mov | ||
var outputDimensions: CGSize? | ||
var compressionPreset: String = AVAssetExportPresetPassthrough // Default to passthrough | ||
|
||
init(sourceURL: URL) { | ||
self.sourceURL = sourceURL | ||
} | ||
|
||
func setOutputFormat(format: AVFileType) throws { | ||
guard FileManager.default.fileExists(atPath: sourceURL.path) else { | ||
throw NSError(domain: "CLDVideoPreprocessHelpers", code: -1, userInfo: [NSLocalizedDescriptionKey: "Invalid source URL"]) | ||
} | ||
|
||
let asset = AVAsset(url: sourceURL) | ||
guard asset.tracks(withMediaType: .video).first != nil else { | ||
throw NSError(domain: "CLDVideoPreprocessHelpers", code: -1, userInfo: [NSLocalizedDescriptionKey: "No video track found"]) | ||
} | ||
|
||
self.outputFormat = format | ||
} | ||
|
||
func setOutputDimensions(dimensions: CGSize) { | ||
self.outputDimensions = dimensions | ||
} | ||
|
||
func setCompressionPreset(preset: String) { | ||
self.compressionPreset = preset | ||
} | ||
|
||
var hasVideoTrack: Bool { | ||
let asset = AVAsset(url: sourceURL) | ||
return asset.tracks(withMediaType: .video).first != nil | ||
} | ||
|
||
func transcode(completion: @escaping (Bool, Error?) -> Void) { | ||
let asset = AVAsset(url: sourceURL) | ||
let outputURL = generateOutputURL() | ||
|
||
do { | ||
// Create asset reader | ||
let assetReader = try AVAssetReader(asset: asset) | ||
guard let videoTrack = asset.tracks(withMediaType: .video).first else { | ||
throw NSError(domain: "CLDVideoTranscode", code: -1, userInfo: [NSLocalizedDescriptionKey: "No video track found"]) | ||
} | ||
|
||
// Configure output settings for AVAssetReader | ||
let assetReaderOutputSettings = [ | ||
kCVPixelBufferPixelFormatTypeKey as String: kCVPixelFormatType_32ARGB | ||
] | ||
|
||
let assetReaderOutput = AVAssetReaderTrackOutput(track: videoTrack, outputSettings: assetReaderOutputSettings) | ||
|
||
// Add the output to the reader | ||
assetReader.add(assetReaderOutput) | ||
|
||
// Create asset writer | ||
let assetWriter = try AVAssetWriter(outputURL: outputURL, fileType: outputFormat) | ||
let videoSettings: [String: Any] = [ | ||
AVVideoWidthKey: outputDimensions?.width ?? videoTrack.naturalSize.width, | ||
AVVideoHeightKey: outputDimensions?.height ?? videoTrack.naturalSize.height | ||
] | ||
|
||
let assetWriterInput = AVAssetWriterInput(mediaType: .video, outputSettings: videoSettings) | ||
assetWriterInput.expectsMediaDataInRealTime = true | ||
assetWriter.add(assetWriterInput) | ||
|
||
// Start reading and writing | ||
assetReader.startReading() | ||
assetWriter.startWriting() | ||
assetWriter.startSession(atSourceTime: .zero) | ||
|
||
assetWriterInput.requestMediaDataWhenReady(on: DispatchQueue(label: "assetWriterQueue")) { | ||
while assetWriterInput.isReadyForMoreMediaData { | ||
guard assetReader.status == .reading else { | ||
let error = assetReader.error ?? NSError(domain: "CLDVideoTranscode", code: -1, userInfo: [NSLocalizedDescriptionKey: "Asset reader status is not reading"]) | ||
completion(false, error) | ||
return | ||
} | ||
|
||
if let sampleBuffer = assetReaderOutput.copyNextSampleBuffer() { | ||
assetWriterInput.append(sampleBuffer) | ||
} else { | ||
assetWriterInput.markAsFinished() | ||
assetWriter.finishWriting { | ||
defer { | ||
if self.sourceURL.isFileURL { | ||
try? FileManager.default.removeItem(at: self.sourceURL) | ||
} | ||
} | ||
|
||
if assetWriter.status == .completed { | ||
self.outputURL = assetWriter.outputURL | ||
completion(true, nil) | ||
} else { | ||
completion(false, assetWriter.error) | ||
} | ||
} | ||
break | ||
} | ||
} | ||
} | ||
} catch { | ||
completion(false, error) | ||
} | ||
} | ||
|
||
private func generateOutputURL() -> URL { | ||
return URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent(UUID().uuidString).appendingPathExtension(outputFormat.fileExtension) | ||
} | ||
} | ||
|
||
private extension AVFileType { | ||
var fileExtension: String { | ||
switch self { | ||
case .mov: | ||
return "mov" | ||
case .mp4: | ||
return "mp4" | ||
case .m4v: | ||
return "m4v" | ||
default: | ||
return "mov" | ||
} | ||
} | ||
} |
Oops, something went wrong.