// // AttachmentViewModel+Compress.swift // // // Created by MainasuK on 2022/11/11. // import UIKit import AVKit import MastodonCore import SessionExporter import Nuke extension AttachmentViewModel { func compressVideo(url: URL) async throws -> URL? { let urlAsset = AVURLAsset(url: url) guard let track = urlAsset.tracks(withMediaType: .video).first else { return nil } let exporter = NextLevelSessionExporter(withAsset: urlAsset) exporter.outputFileType = .mp4 let preferredSize = try await preferredSizeFor( track: track, maxLongestSide: 1280 ) let outputURL = try FileManager.default.createTemporaryFileURL( filename: UUID().uuidString, pathExtension: url.pathExtension ) exporter.outputURL = outputURL let compressionDict: [String: Any] = [ AVVideoAverageBitRateKey: NSNumber(integerLiteral: 3000000), // 3000k AVVideoProfileLevelKey: AVVideoProfileLevelH264HighAutoLevel as String, AVVideoAverageNonDroppableFrameRateKey: NSNumber(floatLiteral: 30), // 30 FPS ] exporter.videoOutputConfiguration = [ AVVideoCodecKey: AVVideoCodecType.h264, AVVideoWidthKey: NSNumber(floatLiteral: preferredSize.width), AVVideoHeightKey: NSNumber(floatLiteral: preferredSize.height), AVVideoScalingModeKey: AVVideoScalingModeResizeAspectFill, AVVideoCompressionPropertiesKey: compressionDict ] exporter.audioOutputConfiguration = [ AVFormatIDKey: kAudioFormatMPEG4AAC, AVEncoderBitRateKey: NSNumber(integerLiteral: 128000), // 128k AVNumberOfChannelsKey: NSNumber(integerLiteral: 2), AVSampleRateKey: NSNumber(value: Float(44100)) ] // needs set to LOW priority to prevent priority inverse issue let task = Task(priority: .utility) { _ = try await exportVideo(by: exporter) } _ = try await task.value return outputURL } private func preferredSizeFor(track: AVAssetTrack, maxLongestSide: CGFloat) async throws -> CGSize { let trackSize = try await track.load(.naturalSize).applying(track.preferredTransform) let actualSize = CGSize(width: abs(trackSize.width), height: abs(trackSize.height)) let isLandscape = actualSize.width >= actualSize.height switch isLandscape { case false: // portrait mode, needs height altered eventually if actualSize.height > maxLongestSide { // reduce height, keep aspect ratio return CGSize(width: (maxLongestSide / (actualSize.height/actualSize.width)), height: maxLongestSide) } return actualSize case true: // landscape mode, needs width altered eventually if actualSize.width > maxLongestSide { // reduce width, keep aspect ratio return CGSize(width: maxLongestSide, height: (maxLongestSide * (actualSize.height/actualSize.width))) } return actualSize } } private func exportVideo(by exporter: NextLevelSessionExporter) async throws -> URL { guard let outputURL = exporter.outputURL else { throw AppError.badRequest } return try await withCheckedThrowingContinuation { continuation in exporter.export(progressHandler: { progress in DispatchQueue.main.async { [weak self] in guard let self = self else { return } self.videoCompressProgress = Double(progress) } }, completionHandler: { result in switch result { case .success(let status): switch status { case .completed: print("NextLevelSessionExporter, export completed, \(exporter.outputURL?.description ?? "")") continuation.resume(with: .success(outputURL)) default: if Task.isCancelled { exporter.cancelExport() } print("NextLevelSessionExporter, did not complete") } case .failure(let error): continuation.resume(with: .failure(error)) } }) } } // end func } extension AttachmentViewModel { @AttachmentViewModelActor func compressImage(data: Data, sizeLimit: SizeLimit) throws -> Output { let maxPayloadSizeInBytes = max((sizeLimit.image ?? 10 * 1024 * 1024), 1 * 1024 * 1024) guard let image = UIImage(data: data)?.normalized(), var imageData = image.pngData() else { throw AttachmentError.invalidAttachmentType } repeat { guard let image = UIImage(data: imageData) else { throw AttachmentError.invalidAttachmentType } if AssetType(imageData) == .png { // A. png image if imageData.count > maxPayloadSizeInBytes { guard let compressedJpegData = image.jpegData(compressionQuality: 0.8) else { throw AttachmentError.invalidAttachmentType } imageData = compressedJpegData } else { break } } else { // B. other image if imageData.count > maxPayloadSizeInBytes { let targetSize = CGSize(width: image.size.width * 0.8, height: image.size.height * 0.8) let scaledImage = image.resized(size: targetSize) guard let compressedJpegData = scaledImage.jpegData(compressionQuality: 0.8) else { throw AttachmentError.invalidAttachmentType } imageData = compressedJpegData } else { break } } } while (imageData.count > maxPayloadSizeInBytes) return .image(imageData, imageKind: AssetType(imageData) == .png ? .png : .jpg) } } @globalActor actor AttachmentViewModelActor { static var shared = AttachmentViewModelActor() }