Skip to content

Commit

Permalink
Add video transcode feature
Browse files Browse the repository at this point in the history
  • Loading branch information
adimiz1 authored Sep 17, 2024
1 parent d9a826a commit a0bd794
Show file tree
Hide file tree
Showing 7 changed files with 451 additions and 1 deletion.
12 changes: 12 additions & 0 deletions Cloudinary.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@
B694AAEC2B308AF100075041 /* CLDAnalytics.swift in Sources */ = {isa = PBXBuildFile; fileRef = B694AAEB2B308AF100075041 /* CLDAnalytics.swift */; };
B694AAEF2B308B8400075041 /* VideoAnalytics.swift in Sources */ = {isa = PBXBuildFile; fileRef = B694AAEE2B308B8400075041 /* VideoAnalytics.swift */; };
B694AAF12B308B9D00075041 /* VideoEventsManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = B694AAF02B308B9D00075041 /* VideoEventsManager.swift */; };
B6B4C49A2C560FEE00C9B604 /* CLDVideoPreprocessChain.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6B4C4992C560FEE00C9B604 /* CLDVideoPreprocessChain.swift */; };
B6B4C49C2C56100600C9B604 /* CLDVideoTranscode.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6B4C49B2C56100600C9B604 /* CLDVideoTranscode.swift */; };
B6B4C4A12C5615CF00C9B604 /* CLDVideoPreprocessHelpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6B4C4A02C5615CF00C9B604 /* CLDVideoPreprocessHelpers.swift */; };
B6C2D4862A724C4200AA0039 /* CLDVideoPlayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6C2D4852A724C4200AA0039 /* CLDVideoPlayer.swift */; };
OBJ_257 /* CLDNConvertible.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_10 /* CLDNConvertible.swift */; };
OBJ_258 /* CLDNError.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_11 /* CLDNError.swift */; };
Expand Down Expand Up @@ -170,6 +173,9 @@
B694AAEB2B308AF100075041 /* CLDAnalytics.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CLDAnalytics.swift; sourceTree = "<group>"; };
B694AAEE2B308B8400075041 /* VideoAnalytics.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoAnalytics.swift; sourceTree = "<group>"; };
B694AAF02B308B9D00075041 /* VideoEventsManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoEventsManager.swift; sourceTree = "<group>"; };
B6B4C4992C560FEE00C9B604 /* CLDVideoPreprocessChain.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CLDVideoPreprocessChain.swift; sourceTree = "<group>"; };
B6B4C49B2C56100600C9B604 /* CLDVideoTranscode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CLDVideoTranscode.swift; sourceTree = "<group>"; };
B6B4C4A02C5615CF00C9B604 /* CLDVideoPreprocessHelpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CLDVideoPreprocessHelpers.swift; sourceTree = "<group>"; };
B6C2D4852A724C4200AA0039 /* CLDVideoPlayer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CLDVideoPlayer.swift; sourceTree = "<group>"; };
"Cloudinary::Cloudinary::Product" /* Cloudinary.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = Cloudinary.framework; sourceTree = BUILT_PRODUCTS_DIR; };
OBJ_10 /* CLDNConvertible.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CLDNConvertible.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -688,6 +694,9 @@
children = (
OBJ_240 /* CLDImagePreprocessChain.swift */,
OBJ_241 /* CLDPreprocessHelpers.swift */,
B6B4C4992C560FEE00C9B604 /* CLDVideoPreprocessChain.swift */,
B6B4C49B2C56100600C9B604 /* CLDVideoTranscode.swift */,
B6B4C4A02C5615CF00C9B604 /* CLDVideoPreprocessHelpers.swift */,
);
path = Uploader;
sourceTree = "<group>";
Expand Down Expand Up @@ -975,6 +984,7 @@
OBJ_309 /* CLDOperators.swift in Sources */,
OBJ_310 /* CLDTransformation.swift in Sources */,
OBJ_311 /* CLDVariable.swift in Sources */,
B6B4C49C2C56100600C9B604 /* CLDVideoTranscode.swift in Sources */,
OBJ_312 /* CLDFetchLayer.swift in Sources */,
OBJ_313 /* CLDLayer.swift in Sources */,
OBJ_314 /* CLDSubtitlesLayer.swift in Sources */,
Expand Down Expand Up @@ -1002,6 +1012,7 @@
OBJ_335 /* CLDOcrResult.swift in Sources */,
OBJ_336 /* CLDOcrSymbolResult.swift in Sources */,
OBJ_337 /* CLDOcrTextAnnotationResult.swift in Sources */,
B6B4C49A2C560FEE00C9B604 /* CLDVideoPreprocessChain.swift in Sources */,
OBJ_338 /* CLDOcrWordResult.swift in Sources */,
OBJ_339 /* CLDQualityAnalysis.swift in Sources */,
OBJ_340 /* CLDRekognitionFace.swift in Sources */,
Expand Down Expand Up @@ -1076,6 +1087,7 @@
OBJ_406 /* CLDDefaultNetworkAdapter.swift in Sources */,
OBJ_407 /* CLDNetworkCoordinator.swift in Sources */,
OBJ_408 /* CLDAsyncNetworkUploadRequest.swift in Sources */,
B6B4C4A12C5615CF00C9B604 /* CLDVideoPreprocessHelpers.swift in Sources */,
OBJ_409 /* CLDErrorRequest.swift in Sources */,
OBJ_410 /* CLDGenericNetworkRequest.swift in Sources */,
OBJ_411 /* CLDNetworkDataRequestImpl.swift in Sources */,
Expand Down
135 changes: 135 additions & 0 deletions Cloudinary/Classes/ios/Uploader/CLDVideoPreprocessChain.swift
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 Cloudinary/Classes/ios/Uploader/CLDVideoPreprocessHelpers.swift
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 Cloudinary/Classes/ios/Uploader/CLDVideoTranscode.swift
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"
}
}
}
Loading

0 comments on commit a0bd794

Please sign in to comment.