diff --git a/ios/Classes/AvController.swift b/ios/Classes/AvController.swift deleted file mode 100644 index 04ea868..0000000 --- a/ios/Classes/AvController.swift +++ /dev/null @@ -1,53 +0,0 @@ - -import AVFoundation -import MobileCoreServices - -class AvController: NSObject { - public func getVideoAsset(_ url:URL)->AVURLAsset { - return AVURLAsset(url: url) - } - - public func getTrack(_ asset: AVURLAsset)->AVAssetTrack? { - var track : AVAssetTrack? = nil - let group = DispatchGroup() - group.enter() - asset.loadValuesAsynchronously(forKeys: ["tracks"], completionHandler: { - var error: NSError? = nil; - let status = asset.statusOfValue(forKey: "tracks", error: &error) - if (status == .loaded) { - track = asset.tracks(withMediaType: AVMediaType.video).first - } - group.leave() - }) - group.wait() - return track - } - - public func getVideoOrientation(_ path:String)-> Int? { - let url = Utility.getPathUrl(path) - let asset = getVideoAsset(url) - guard let track = getTrack(asset) else { - return nil - } - let size = track.naturalSize - let txf = track.preferredTransform - if size.width == txf.tx && size.height == txf.ty { - return 0 - } else if txf.tx == 0 && txf.ty == 0 { - return 90 - } else if txf.tx == 0 && txf.ty == size.width { - return 180 - } else { - return 270 - } - } - - public func getMetaDataByTag(_ asset:AVAsset,key:String)->String { - for item in asset.commonMetadata { - if item.commonKey?.rawValue == key { - return item.stringValue ?? ""; - } - } - return "" - } -} diff --git a/ios/Classes/SwiftVideoCompressPlugin.swift b/ios/Classes/SwiftVideoCompressPlugin.swift deleted file mode 100644 index 11f2c28..0000000 --- a/ios/Classes/SwiftVideoCompressPlugin.swift +++ /dev/null @@ -1,251 +0,0 @@ -import Flutter -import AVFoundation - -public class SwiftVideoCompressPlugin: NSObject, FlutterPlugin { - private let channelName = "video_compress" - private var exporter: AVAssetExportSession? = nil - private var stopCommand = false - private let channel: FlutterMethodChannel - private let avController = AvController() - - init(channel: FlutterMethodChannel) { - self.channel = channel - } - - public static func register(with registrar: FlutterPluginRegistrar) { - let channel = FlutterMethodChannel(name: "video_compress", binaryMessenger: registrar.messenger()) - let instance = SwiftVideoCompressPlugin(channel: channel) - registrar.addMethodCallDelegate(instance, channel: channel) - } - - public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { - let args = call.arguments as? Dictionary - switch call.method { - case "getByteThumbnail": - let path = args!["path"] as! String - let quality = args!["quality"] as! NSNumber - let position = args!["position"] as! NSNumber - getByteThumbnail(path, quality, position, result) - case "getFileThumbnail": - let path = args!["path"] as! String - let quality = args!["quality"] as! NSNumber - let position = args!["position"] as! NSNumber - getFileThumbnail(path, quality, position, result) - case "getMediaInfo": - let path = args!["path"] as! String - getMediaInfo(path, result) - case "compressVideo": - let path = args!["path"] as! String - let quality = args!["quality"] as! NSNumber - let deleteOrigin = args!["deleteOrigin"] as! Bool - let startTime = args!["startTime"] as? Double - let duration = args!["duration"] as? Double - let includeAudio = args!["includeAudio"] as? Bool - let frameRate = args!["frameRate"] as? Int - compressVideo(path, quality, deleteOrigin, startTime, duration, includeAudio, - frameRate, result) - case "cancelCompression": - cancelCompression(result) - case "deleteAllCache": - Utility.deleteFile(Utility.basePath(), clear: true) - result(true) - default: - result(FlutterMethodNotImplemented) - } - } - - private func getBitMap(_ path: String,_ quality: NSNumber,_ position: NSNumber,_ result: FlutterResult)-> Data? { - let url = Utility.getPathUrl(path) - let asset = avController.getVideoAsset(url) - guard let track = avController.getTrack(asset) else { return nil } - - let assetImgGenerate = AVAssetImageGenerator(asset: asset) - assetImgGenerate.appliesPreferredTrackTransform = true - - let timeScale = CMTimeScale(track.nominalFrameRate) - let time = CMTimeMakeWithSeconds(Float64(truncating: position),preferredTimescale: timeScale) - guard let img = try? assetImgGenerate.copyCGImage(at:time, actualTime: nil) else { - return nil - } - let thumbnail = UIImage(cgImage: img) - let compressionQuality = CGFloat(0.01 * Double(truncating: quality)) - return thumbnail.jpegData(compressionQuality: compressionQuality) - } - - private func getByteThumbnail(_ path: String,_ quality: NSNumber,_ position: NSNumber,_ result: FlutterResult) { - if let bitmap = getBitMap(path,quality,position,result) { - result(bitmap) - } - } - - private func getFileThumbnail(_ path: String,_ quality: NSNumber,_ position: NSNumber,_ result: FlutterResult) { - let fileName = Utility.getFileName(path) - let url = Utility.getPathUrl("\(Utility.basePath())/\(fileName).jpg") - Utility.deleteFile(path) - if let bitmap = getBitMap(path,quality,position,result) { - guard (try? bitmap.write(to: url)) != nil else { - return result(FlutterError(code: channelName,message: "getFileThumbnail error",details: "getFileThumbnail error")) - } - result(Utility.excludeFileProtocol(url.absoluteString)) - } - } - - public func getMediaInfoJson(_ path: String)->[String : Any?] { - let url = Utility.getPathUrl(path) - let asset = avController.getVideoAsset(url) - guard let track = avController.getTrack(asset) else { return [:] } - - let playerItem = AVPlayerItem(url: url) - let metadataAsset = playerItem.asset - - let orientation = avController.getVideoOrientation(path) - - let title = avController.getMetaDataByTag(metadataAsset,key: "title") - let author = avController.getMetaDataByTag(metadataAsset,key: "author") - - let duration = asset.duration.seconds * 1000 - let filesize = track.totalSampleDataLength - - let size = track.naturalSize.applying(track.preferredTransform) - - let width = abs(size.width) - let height = abs(size.height) - - let dictionary = [ - "path":Utility.excludeFileProtocol(path), - "title":title, - "author":author, - "width":width, - "height":height, - "duration":duration, - "filesize":filesize, - "orientation":orientation - ] as [String : Any?] - return dictionary - } - - private func getMediaInfo(_ path: String,_ result: FlutterResult) { - let json = getMediaInfoJson(path) - let string = Utility.keyValueToJson(json) - result(string) - } - - - @objc private func updateProgress(timer:Timer) { - let asset = timer.userInfo as! AVAssetExportSession - if(!stopCommand) { - channel.invokeMethod("updateProgress", arguments: "\(String(describing: asset.progress * 100))") - } - } - - private func getExportPreset(_ quality: NSNumber)->String { - switch(quality) { - case 1: - return AVAssetExportPresetLowQuality - case 2: - return AVAssetExportPresetMediumQuality - case 3: - return AVAssetExportPresetHighestQuality - default: - return AVAssetExportPresetMediumQuality - } - } - - private func getComposition(_ isIncludeAudio: Bool,_ timeRange: CMTimeRange, _ sourceVideoTrack: AVAssetTrack)->AVAsset { - let composition = AVMutableComposition() - if !isIncludeAudio { - let compressionVideoTrack = composition.addMutableTrack(withMediaType: AVMediaType.video, preferredTrackID: kCMPersistentTrackID_Invalid) - compressionVideoTrack!.preferredTransform = sourceVideoTrack.preferredTransform - try? compressionVideoTrack!.insertTimeRange(timeRange, of: sourceVideoTrack, at: CMTime.zero) - } else { - return sourceVideoTrack.asset! - } - - return composition - } - - private func compressVideo(_ path: String,_ quality: NSNumber,_ deleteOrigin: Bool,_ startTime: Double?, - _ duration: Double?,_ includeAudio: Bool?,_ frameRate: Int?, - _ result: @escaping FlutterResult) { - let sourceVideoUrl = Utility.getPathUrl(path) - let sourceVideoType = "mp4" - - let sourceVideoAsset = avController.getVideoAsset(sourceVideoUrl) - let sourceVideoTrack = avController.getTrack(sourceVideoAsset) - - let compressionUrl = - Utility.getPathUrl("\(Utility.basePath())/\(Utility.getFileName(path)).\(sourceVideoType)") - - let timescale = sourceVideoAsset.duration.timescale - let minStartTime = Double(startTime ?? 0) - - let videoDuration = sourceVideoAsset.duration.seconds - let minDuration = Double(duration ?? videoDuration) - let maxDurationTime = minStartTime + minDuration < videoDuration ? minDuration : videoDuration - - let cmStartTime = CMTimeMakeWithSeconds(minStartTime, preferredTimescale: timescale) - let cmDurationTime = CMTimeMakeWithSeconds(maxDurationTime, preferredTimescale: timescale) - let timeRange: CMTimeRange = CMTimeRangeMake(start: cmStartTime, duration: cmDurationTime) - - let isIncludeAudio = includeAudio != nil ? includeAudio! : true - - let session = getComposition(isIncludeAudio, timeRange, sourceVideoTrack!) - - let exporter = AVAssetExportSession(asset: session, presetName: getExportPreset(quality))! - - exporter.outputURL = compressionUrl - exporter.outputFileType = AVFileType.mp4 - exporter.shouldOptimizeForNetworkUse = true - - if frameRate != nil { - let videoComposition = AVMutableVideoComposition(propertiesOf: sourceVideoAsset) - videoComposition.frameDuration = CMTimeMake(value: 1, timescale: Int32(frameRate!)) - exporter.videoComposition = videoComposition - } - - if !isIncludeAudio { - exporter.timeRange = timeRange - } - - Utility.deleteFile(compressionUrl.absoluteString) - - let timer = Timer.scheduledTimer(timeInterval: 0.1, target: self, selector: #selector(self.updateProgress), - userInfo: exporter, repeats: true) - - exporter.exportAsynchronously(completionHandler: { - if(self.stopCommand) { - timer.invalidate() - self.stopCommand = false - var json = self.getMediaInfoJson(path) - json["isCancel"] = true - let jsonString = Utility.keyValueToJson(json) - return result(jsonString) - } - if deleteOrigin { - timer.invalidate() - let fileManager = FileManager.default - do { - if fileManager.fileExists(atPath: path) { - try fileManager.removeItem(atPath: path) - } - self.exporter = nil - self.stopCommand = false - } - catch let error as NSError { - print(error) - } - } - var json = self.getMediaInfoJson(compressionUrl.absoluteString) - json["isCancel"] = false - let jsonString = Utility.keyValueToJson(json) - result(jsonString) - }) - } - - private func cancelCompression(_ result: FlutterResult) { - exporter?.cancelExport() - stopCommand = true - result("") - } - -} diff --git a/ios/Classes/Utility.h b/ios/Classes/Utility.h new file mode 100644 index 0000000..87af079 --- /dev/null +++ b/ios/Classes/Utility.h @@ -0,0 +1,21 @@ +NS_ASSUME_NONNULL_BEGIN + +@interface Utility : NSObject + ++ (NSString *)basePath; + ++ (NSString *)excludeFileProtocol:(NSString *)path; + ++ (NSURL *)getPathUrl:(NSString *)path; + ++ (NSString *)getFileName:(NSString *)path; + ++ (void)deleteFile:(NSString *)path; + ++ (NSString *)keyValueToJson:(NSDictionary *)keyAndValue; + + +@end + +NS_ASSUME_NONNULL_END + diff --git a/ios/Classes/Utility.m b/ios/Classes/Utility.m new file mode 100644 index 0000000..0803205 --- /dev/null +++ b/ios/Classes/Utility.m @@ -0,0 +1,40 @@ +#import "Utility.h" + +@implementation Utility + ++ (NSString *)basePath { + NSFileManager *fileManager = [NSFileManager defaultManager]; + + NSString *tempPath = NSTemporaryDirectory(); + NSString *path = [tempPath stringByAppendingString:@"video_compress"]; + + if (![fileManager fileExistsAtPath:path]){ + [fileManager createDirectoryAtPath:path withIntermediateDirectories:true attributes:nil error:nil]; + } + return path; +} + ++ (NSString *)excludeFileProtocol:(NSString *)path { + return [path stringByReplacingOccurrencesOfString:@"file://" withString:@""]; +} + ++ (NSURL *)getPathUrl:(NSString *)path { + return [NSURL fileURLWithPath:[self excludeFileProtocol:path]]; +} + ++ (NSString *)getFileName:(NSString *)path { + return [[path lastPathComponent] stringByDeletingPathExtension]; +} + ++ (void)deleteFile:(NSString *)path { + NSFileManager *fileManager = [NSFileManager defaultManager]; + NSURL *url = [self getPathUrl:path]; + [fileManager removeItemAtURL:url error:nil]; +} + ++ (NSString *)keyValueToJson:(NSDictionary *)keyAndValue { + NSData *data = [NSJSONSerialization dataWithJSONObject:keyAndValue options:NSJSONWritingFragmentsAllowed error:nil]; + return [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]; +} + +@end diff --git a/ios/Classes/Utility.swift b/ios/Classes/Utility.swift deleted file mode 100644 index 05ee119..0000000 --- a/ios/Classes/Utility.swift +++ /dev/null @@ -1,52 +0,0 @@ - -class Utility: NSObject { - static let fileManager = FileManager.default - - static func basePath()->String { - let path = "\(NSTemporaryDirectory())video_compress" - do { - if !fileManager.fileExists(atPath: path) { - try! fileManager.createDirectory(atPath: path, - withIntermediateDirectories: true, attributes: nil) - } - } - return path - } - - static func stripFileExtension(_ fileName:String)->String { - var components = fileName.components(separatedBy: ".") - if components.count > 1 { - components.removeLast() - return components.joined(separator: ".") - } else { - return fileName - } - } - static func getFileName(_ path: String)->String { - return stripFileExtension((path as NSString).lastPathComponent) - } - - static func getPathUrl(_ path: String)->URL { - return URL(fileURLWithPath: excludeFileProtocol(path)) - } - - static func excludeFileProtocol(_ path: String)->String { - return path.replacingOccurrences(of: "file://", with: "") - } - - static func keyValueToJson(_ keyAndValue: [String : Any?])->String { - let data = try! JSONSerialization.data(withJSONObject: keyAndValue as NSDictionary, options: []) - let jsonString = NSString(data:data as Data,encoding: String.Encoding.utf8.rawValue) - return jsonString! as String - } - - static func deleteFile(_ path: String, clear: Bool = false) { - let url = getPathUrl(path) - if fileManager.fileExists(atPath: url.absoluteString) { - try? fileManager.removeItem(at: url) - } - if clear { - try? fileManager.removeItem(at: url) - } - } -} diff --git a/ios/Classes/VideoCompressPlugin.m b/ios/Classes/VideoCompressPlugin.m index 10ef50d..8945b8b 100644 --- a/ios/Classes/VideoCompressPlugin.m +++ b/ios/Classes/VideoCompressPlugin.m @@ -1,8 +1,100 @@ #import "VideoCompressPlugin.h" -#import +#import "Utility.h" +#import + +@interface VideoCompressPlugin () + +- (void)compressVideo:(NSString *)path quality:(NSNumber *)quality result:(FlutterResult)result; +- (void)cancelCompression:(FlutterResult)result; +- (NSDictionary *)getMediaInfoJson:(NSString *)path; +- (NSString *)getExportPreset:(NSNumber *)quality; + +@end + +@implementation VideoCompressPlugin { + AVAssetExportSession *exporter; + bool stopCommand; +} -@implementation VideoCompressPlugin + (void)registerWithRegistrar:(NSObject*)registrar { - [SwiftVideoCompressPlugin registerWithRegistrar:registrar]; + FlutterMethodChannel *channel = + [FlutterMethodChannel methodChannelWithName: @"video_compress" + binaryMessenger:[registrar messenger]]; + VideoCompressPlugin *instance = [VideoCompressPlugin new]; + [registrar addMethodCallDelegate:instance channel:channel]; +} + +- (void)handleMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result { + NSDictionary *args = call.arguments; + if ([@"compressVideo" isEqualToString:call.method]) { + NSString *path = args[@"path"]; + NSNumber *quality = args[@"quality"]; + [self compressVideo:path quality:quality result:result]; + } else if ([@"cancelCompression" isEqualToString:call.method]) { + [self cancelCompression:result]; + } else if ([@"deleteAllCache" isEqualToString:call.method]) { + [Utility deleteFile:[Utility basePath]]; + result(@YES); + } else { + result(FlutterMethodNotImplemented); + } +} + +- (NSDictionary *)getMediaInfoJson:(NSString *)path { + NSDictionary* dictionary = @{ @"path": [Utility excludeFileProtocol:path] }; + return dictionary; +} + +- (NSString *)getExportPreset:(NSNumber *)quality { + switch ([quality intValue]) { + case 1: + return AVAssetExportPresetLowQuality; + case 2: + return AVAssetExportPresetMediumQuality; + case 3: + return AVAssetExportPresetHighestQuality; + default: + return AVAssetExportPresetMediumQuality; + } +} + +- (void)compressVideo:(NSString *)path quality:(NSNumber *)quality result:(FlutterResult)result { + NSURL *sourceVideoUrl = [Utility getPathUrl:path]; + AVURLAsset *sourceVideoAsset = [AVURLAsset URLAssetWithURL:sourceVideoUrl options:nil]; + + NSURL *compressionUrl = [Utility getPathUrl:[NSString stringWithFormat:@"%@/%@.mp4", [Utility basePath], [Utility getFileName:path]]]; + + stopCommand = false; + exporter = [AVAssetExportSession exportSessionWithAsset:sourceVideoAsset presetName: [self getExportPreset:quality]]; + exporter.outputURL = compressionUrl; + exporter.outputFileType = AVFileTypeMPEG4; + exporter.shouldOptimizeForNetworkUse = true; + + [Utility deleteFile:[compressionUrl absoluteString]]; + + [exporter exportAsynchronouslyWithCompletionHandler: ^{ + self->exporter = nil; + if (self->stopCommand){ + self->stopCommand = false; + NSMutableDictionary *json = [[self getMediaInfoJson:path] mutableCopy]; + [json setValue:@YES forKey:@"isCancel"]; + NSString *jsonString = [Utility keyValueToJson:json]; + return result(jsonString); + } + NSMutableDictionary *json = [[self getMediaInfoJson:[compressionUrl absoluteString]] mutableCopy]; + [json setValue:@NO forKey:@"isCancel"]; + NSString *jsonString = [Utility keyValueToJson:json]; + return result(jsonString); + }]; } + +- (void)cancelCompression:(FlutterResult)result { + if (exporter != nil) { + [exporter cancelExport]; + stopCommand = true; + } + result(@""); +} + @end +