// _____ _ // |_ _| |_ _ _ ___ ___ _ __ __ _ // | | | ' \| '_/ -_) -_) ' \/ _` |_ // |_| |_||_|_| \___\___|_|_|_\__,_(_) // // Threema iOS Client // Copyright (c) 2020 Threema GmbH // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License, version 3, // as published by the Free Software Foundation. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . import UIKit import CocoaLumberjackSwift import PromiseKit enum VideoURLSenderItemCreatorError : Error { case thumbnailCreationFailed case couldNotCreateExportSession case generalError } @objc protocol VideoConversionProgressDelegate { func videoExportSession(exportSession : SDAVAssetExportSession) } @objc public class VideoURLSenderItemCreator: NSObject { @objc public static let temporaryDirectory = "tmpVideoCreator" @objc var encodeProgressDelegate : VideoConversionProgressDelegate? func getThumbnail(asset : AVAsset) -> Promise { return Promise { seal in guard let thumbnail = MediaConverter.getThumbnailForVideo(asset) else { seal.reject(VideoURLSenderItemCreatorError.thumbnailCreationFailed) return } seal.resolve(thumbnail, nil) } } func getExportSession(asset : AVAsset) -> Promise { return Promise { seal in guard let outputURL = MediaConverter.getAssetOutputURL() else { seal.reject(VideoURLSenderItemCreatorError.couldNotCreateExportSession) return } guard let exportSession = MediaConverter.getAVAssetExportSession(from: asset, outputURL: outputURL) else { seal.reject(VideoURLSenderItemCreatorError.couldNotCreateExportSession) return } // exportSession.addObserver(self, forKeyPath: "progress", options: .new, context: nil) seal.fulfill(exportSession) } } public override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) { super.observeValue(forKeyPath: keyPath, of: object, change: change, context: context) if let exportSession = object as? SDAVAssetExportSession { self.encodeProgressDelegate?.videoExportSession(exportSession: exportSession) } } @objc public func getExportSession(for asset : AVAsset) -> SDAVAssetExportSession? { var exportSession : SDAVAssetExportSession? let sema = DispatchSemaphore(value: 0) DispatchQueue.global(qos: .userInitiated).async { self.getExportSession(asset: asset).done { session in exportSession = session sema.signal() }.catch { error in sema.signal() DDLogError("Encountered an error: \(error)") } } sema.wait() return exportSession } func convertVideo(asset : AVAsset) -> Promise { getExportSession(asset: asset).then { exportSession in return self.convertVideo(on: exportSession, asset: asset) } } func convertVideo(on exportSession : SDAVAssetExportSession, asset : AVAsset) -> Promise { return Promise { seal in MediaConverter.convertVideoAsset(asset, with: exportSession, onCompletion: { completionURL in guard let url = completionURL else { seal.reject(VideoURLSenderItemCreatorError.generalError) return } return seal.fulfill(url) }, onError: { completionError in guard let error = completionError else { seal.reject(VideoURLSenderItemCreatorError.generalError) return } seal.reject(error) }) } } public func senderItem(from asset : AVAsset) -> Promise { let bgq = DispatchQueue.global(qos: .userInitiated) return self.getExportSession(asset: asset).then(on: bgq) { exportSession in self.convertVideo(on: exportSession, asset: asset) }.compactMap(on: bgq) { url in URLSenderItem.init(url: url, type: kUTTypeMPEG4 as String, renderType: 1, sendAsFile: true) } } @objc public func senderItem(from videoUrl : URL) -> URLSenderItem? { guard let scheme = videoUrl.scheme else { return nil } if (scheme != "file"), !FileManager.default.fileExists(atPath: videoUrl.absoluteString) { return nil } let asset = AVURLAsset(url: videoUrl) if !asset.isExportable { return nil } return senderItem(fromAsset: asset) } @objc public func senderItem(fromAsset : AVAsset) -> URLSenderItem? { var senderItem : URLSenderItem? let sema = DispatchSemaphore(value: 0) let bgq = DispatchQueue.global(qos: .userInitiated) firstly { self.senderItem(from: fromAsset) }.done(on: bgq) { item in senderItem = item sema.signal() }.catch { error in DDLogError("Error: \(error)") } sema.wait() return senderItem } @objc func senderItem(from asset : AVAsset, on exportSession : SDAVAssetExportSession) -> URLSenderItem? { var senderItem : URLSenderItem? let sema = DispatchSemaphore(value: 0) firstly { self.convertVideo(on: exportSession, asset: asset) }.then { url in self.senderItem(from: asset) }.done { item in senderItem = item sema.signal() }.catch { error in sema.signal() DDLogError("Error: \(error)") } sema.wait() return senderItem } @objc static public func writeToTemporaryDirectory(data : Data) -> URL? { let fileManager = FileManager.default let tmpDirectory = fileManager.temporaryDirectory let tmpFolder = tmpDirectory.appendingPathComponent(VideoURLSenderItemCreator.temporaryDirectory) let fileName = SwiftUtils.pseudoRandomString(length: 10) let fileUrl = tmpFolder.appendingPathComponent(fileName).appendingPathExtension("mp4") do { try fileManager.createDirectory(at: tmpFolder, withIntermediateDirectories: true, attributes: nil) } catch { DDLogError("Could not create temporary directory \(error)") return nil } do { try data.write(to: fileUrl) } catch { DDLogError("Could not write file \(error)") return nil } return fileUrl } @objc static public func cleanTemporaryDirectory() -> Bool { let fileManager = FileManager.default let tmpDirectory = fileManager.temporaryDirectory let tmpFolder = tmpDirectory.appendingPathComponent(VideoURLSenderItemCreator.temporaryDirectory) do { try fileManager.removeItem(at: tmpFolder) } catch { DDLogError("Could not clean temporary directory \(error)") return false } return true } /// Returns a pseudorandom string /// - Parameter length: the length of the returned String /// - Returns: A pseudorandom string of the given length private static func pseudoRandomString(length: Int) -> String { let letters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" return String((0..