|
|
@@ -0,0 +1,317 @@
|
|
|
+import Foundation
|
|
|
+import Flutter
|
|
|
+import UIKit
|
|
|
+import AliyunVideoSDKPro
|
|
|
+public class CameraViewFactory: NSObject, FlutterPlatformViewFactory {
|
|
|
+ var messenger: FlutterBinaryMessenger!
|
|
|
+ public func create(withFrame frame: CGRect, viewIdentifier viewId: Int64, arguments args: Any?) -> FlutterPlatformView {
|
|
|
+ return CameraView(withFrame: frame, viewIdentifier: viewId, arguments: args, binaryMessenger: messenger)
|
|
|
+ }
|
|
|
+ @objc public init(messenger: (NSObject & FlutterBinaryMessenger)?) {
|
|
|
+ super.init()
|
|
|
+ self.messenger = messenger
|
|
|
+ }
|
|
|
+
|
|
|
+ public func createArgsCodec() -> FlutterMessageCodec & NSObjectProtocol {
|
|
|
+ return FlutterStandardMessageCodec.sharedInstance()
|
|
|
+ }
|
|
|
+
|
|
|
+}
|
|
|
+public class CameraView: NSObject, FlutterPlatformView, AliyunIRecorderDelegate, AliyunIPlayerCallback, AliyunIExporterCallback, AliyunIRenderCallback {
|
|
|
+ public func playerDidEnd() {
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ public func playProgress(_ playSec: Double, streamProgress streamSec: Double) {
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ public func playError(_ errorCode: Int32) {
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ public func seekDidEnd() {
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ public func playerDidStart() {
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+
|
|
|
+ fileprivate var viewId: Int64!
|
|
|
+ fileprivate var cameraView: UIView!
|
|
|
+ fileprivate var channel: FlutterMethodChannel!
|
|
|
+ fileprivate var recordPath: String?
|
|
|
+ fileprivate var taskPath: String?
|
|
|
+ fileprivate var recorder: AliyunIRecorder!
|
|
|
+ fileprivate var composeResult: FlutterResult?
|
|
|
+ public init(withFrame frame: CGRect, viewIdentifier viewId: Int64, arguments args: Any?, binaryMessenger: FlutterBinaryMessenger) {
|
|
|
+ super.init()
|
|
|
+
|
|
|
+ self.viewId = viewId
|
|
|
+
|
|
|
+ self.cameraView = UIView()
|
|
|
+ self.cameraView.frame = UIScreen.main.bounds
|
|
|
+ self.channel = FlutterMethodChannel(name: "flutter_ali_camera_\(viewId)", binaryMessenger: binaryMessenger)
|
|
|
+ self.channel.setMethodCallHandler({
|
|
|
+ [weak self]
|
|
|
+ (call: FlutterMethodCall, result: @escaping FlutterResult) -> Void in
|
|
|
+ if let this = self {
|
|
|
+ this.onMethodCall(call: call, result: result)
|
|
|
+ }
|
|
|
+ })
|
|
|
+ }
|
|
|
+
|
|
|
+
|
|
|
+ public func view() -> UIView {
|
|
|
+ return self.cameraView
|
|
|
+ }
|
|
|
+ func onMethodCall(call: FlutterMethodCall, result: @escaping FlutterResult) {
|
|
|
+ let args = call.arguments as? [String: Any]
|
|
|
+ let method = call.method
|
|
|
+ if call.method == "initializeSdk" {
|
|
|
+// initSdk(result)
|
|
|
+ result(true)
|
|
|
+ } else if call.method == "cleanAudioData" {
|
|
|
+ let videoId = args!["videoId"] as! String
|
|
|
+ let pathAudio = args!["pathAudio"] as! String
|
|
|
+ deleteFileWithPrefix(folderPath: pathAudio, prefix: "\(videoId)_")
|
|
|
+ result(true)
|
|
|
+ } else if call.method == "getFilterParentPath" {
|
|
|
+ print("path \(Bundle.main.path(forResource: "icon", ofType: "png"))")
|
|
|
+ let url = Bundle.main.bundlePath
|
|
|
+ result("\(url)/filter/")
|
|
|
+ } else if method == "startPreview" {
|
|
|
+ initCameraView()
|
|
|
+ recorder?.clipManager?.deleteAllPart()
|
|
|
+ recorder?.startPreview(withPositon: AliyunIRecorderCameraPosition.front)
|
|
|
+ result(true)
|
|
|
+ } else
|
|
|
+ if method == "startPreview" {
|
|
|
+ initCameraView()
|
|
|
+ recorder?.clipManager?.deleteAllPart()
|
|
|
+ recorder?.startPreview(withPositon: AliyunIRecorderCameraPosition.front)
|
|
|
+ result(true)
|
|
|
+ } else if method == "setRecordPath" {
|
|
|
+ self.recordPath = args!["path"] as? String
|
|
|
+ self.taskPath = args!["taskPath"] as? String
|
|
|
+ result(true)
|
|
|
+ } else if method == "onStop" {
|
|
|
+ onStop()
|
|
|
+ result(true)
|
|
|
+ } else if method == "onResume" {
|
|
|
+ recorder?.startPreview(withPositon: AliyunIRecorderCameraPosition.front)
|
|
|
+ result(true)
|
|
|
+ } else if method == "onDestory" {
|
|
|
+ onStop()
|
|
|
+ onDestory()
|
|
|
+ result(true)
|
|
|
+ } else if method == "switchCamera" {
|
|
|
+ recorder.switchCameraPosition()
|
|
|
+ result(true)
|
|
|
+ } else if method == "setBeautyLevel" {
|
|
|
+ let level = args!["level"] as! Int
|
|
|
+ recorder.beautifyStatus = (level == 0)
|
|
|
+ recorder.beautifyValue = Int32(level)
|
|
|
+ result(true)
|
|
|
+ } else if method == "startRecord" {
|
|
|
+ let maxDuration = args!["maxDuration"] as! Int
|
|
|
+ let videoId = args!["videoId"] as! String
|
|
|
+ let index = args!["index"] as! Int
|
|
|
+ let fileName = args!["fileName"] as! String
|
|
|
+ if recorder?.clipManager?.videoAbsolutePaths != nil && recorder?.clipManager?.videoAbsolutePaths.count != 0 {
|
|
|
+ recorder?.clipManager?.deletePart()
|
|
|
+ }
|
|
|
+ clearCacheVideo(videoId: videoId, index: index)
|
|
|
+ recorder?.outputPath = self.recordPath! + "\(fileName).mp4"
|
|
|
+ recorder?.clipManager?.maxDuration = CGFloat(Float(maxDuration) / 1000.0)
|
|
|
+ recorder?.startRecording()
|
|
|
+ result(true)
|
|
|
+ } else if method == "stopRecord" {
|
|
|
+ recorder?.stopRecording()
|
|
|
+ result(true)
|
|
|
+ } else if method == "finishRecord" {
|
|
|
+ recorder?.finishRecording()
|
|
|
+ result(true)
|
|
|
+ } else if method == "setFilter" {
|
|
|
+ let path: String? = args?["path"] as? String
|
|
|
+ if path == nil || path?.count == 0 {
|
|
|
+ // delete filter
|
|
|
+ recorder?.deleteFilter()
|
|
|
+ return
|
|
|
+ }
|
|
|
+ recorder?.apply(AliyunEffectFilter.init(file: path))
|
|
|
+ } else if method == "startCompose" {
|
|
|
+ let outputPath = args!["outputPath"] as! String
|
|
|
+ let bgmPath = args!["bgmPath"] as! String
|
|
|
+ let paths = args!["paths"] as! [String]
|
|
|
+ let durations = args!["durations"] as! [Int]
|
|
|
+ composeResult = result
|
|
|
+ startCompose(outputPath: outputPath, bgmPath: bgmPath, paths: paths, durations: durations)
|
|
|
+ } else if method == "create"{
|
|
|
+ result(FlutterMethodNotImplemented)
|
|
|
+ }
|
|
|
+ }
|
|
|
+ private func initCameraView() {
|
|
|
+ recorder = AliyunIRecorder.init(delegate: self, videoSize: CGSize(width: 720, height: 1280))
|
|
|
+ recorder?.preview = self.cameraView
|
|
|
+ recorder?.taskPath = self.taskPath
|
|
|
+ recorder?.clipManager?.maxDuration = 0
|
|
|
+ }
|
|
|
+
|
|
|
+ private func onStop() {
|
|
|
+ recorder?.stopRecording()
|
|
|
+ recorder?.stopPreview()
|
|
|
+ }
|
|
|
+
|
|
|
+ private func onDestory() {
|
|
|
+ recorder?.destroy()
|
|
|
+ recorder = nil
|
|
|
+ cameraView = nil
|
|
|
+ }
|
|
|
+
|
|
|
+ var editor: AliyunEditor?
|
|
|
+ private func startCompose(outputPath: String, bgmPath: String, paths: [String], durations: [Int]) {
|
|
|
+ AliyunVideoSDKInfo.print()
|
|
|
+ if durations.count != paths.count {
|
|
|
+ return
|
|
|
+ }
|
|
|
+// AliyunVideoSDKInfo.setLogLevel(AlivcLogLevel.debug)
|
|
|
+
|
|
|
+ let importer = AliyunImporter.init(path: self.taskPath, outputSize: CGSize(width: 480, height: 720))
|
|
|
+ let param = AliyunVideoParam.init()
|
|
|
+ param.fps = 30
|
|
|
+ param.gop = 30
|
|
|
+ param.videoQuality = AliyunVideoQuality.medium
|
|
|
+ param.scaleMode = AliyunScaleMode.fill
|
|
|
+ // 编码模式
|
|
|
+ param.codecType = AliyunVideoCodecType.hardware
|
|
|
+ importer?.setVideoParam(param)
|
|
|
+ for index in 0..<paths.count {
|
|
|
+ print("paths: \(paths[index])")
|
|
|
+ // 取出有效的document地址
|
|
|
+ let documentRance: Range = paths[index].range(of: "/Documents/")!
|
|
|
+ let distance: Int = paths[index].distance(from: paths[index].startIndex, to: documentRance.upperBound)
|
|
|
+ let truePath = NSSearchPathForDirectoriesInDomains(FileManager.SearchPathDirectory.documentDirectory, FileManager.SearchPathDomainMask.userDomainMask, true)[0] + paths[index].subString(from: distance - 1)
|
|
|
+ print("true path: \(truePath)")
|
|
|
+
|
|
|
+ importer?.addMediaClip(AliyunClip.init(videoPath: truePath, animDuration: 0))
|
|
|
+ }
|
|
|
+ importer?.generateProjectConfigure()
|
|
|
+ editor = AliyunEditor.init(path: self.taskPath, preview: nil)
|
|
|
+ editor?.apply(AliyunEffectMusic.init(file: bgmPath))
|
|
|
+ editor?.delegate = self
|
|
|
+ editor?.startEdit()
|
|
|
+
|
|
|
+ let exporter = editor?.getExporter()
|
|
|
+ exporter?.startExport(outputPath)
|
|
|
+ }
|
|
|
+
|
|
|
+ private func clearCacheVideo(videoId: String, index: Int) {
|
|
|
+ deleteFileWithPrefix(folderPath: self.recordPath!, prefix: "\(videoId)_\(index)_")
|
|
|
+ }
|
|
|
+
|
|
|
+ private func deleteFileWithPrefix(folderPath: String, prefix: String) {
|
|
|
+ let manager = FileManager.default
|
|
|
+ let files:[String] = try! manager.contentsOfDirectory(atPath: folderPath)
|
|
|
+
|
|
|
+ for file in files{
|
|
|
+ do {
|
|
|
+ if file.hasPrefix(prefix) {
|
|
|
+ try manager.removeItem(atPath: folderPath + "/\(file)")
|
|
|
+ }
|
|
|
+ } catch {
|
|
|
+ print("faild to delete")
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ public func recorderDeviceAuthorization(_ status: AliyunIRecorderDeviceAuthor) {
|
|
|
+ if status == AliyunIRecorderDeviceAuthor.enabled {
|
|
|
+ print("sdk enabled")
|
|
|
+ } else {
|
|
|
+ print("sdk disabled")
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ public func recorderVideoDuration(_ duration: CGFloat) {
|
|
|
+ print("recorderVideoDuration:\(duration)")
|
|
|
+ channel.invokeMethod("recordProgress", arguments: ["progress": Int(duration * 1000)])
|
|
|
+ }
|
|
|
+
|
|
|
+ public func recoderError(_ error: Error!) {
|
|
|
+ print("recoderError")
|
|
|
+ recorder?.finishRecording()
|
|
|
+ }
|
|
|
+
|
|
|
+ public func recorderDidStopWithMaxDuration() {
|
|
|
+ print("recorderDidStopWithMaxDuration")
|
|
|
+ recorder?.finishRecording()
|
|
|
+ }
|
|
|
+
|
|
|
+ public func recorderDidFinishRecording() {
|
|
|
+ channel.invokeMethod("recordProgress", arguments: ["progress": Int((recorder?.clipManager?.maxDuration ?? 0) * 1000), "path": recorder?.outputPath])
|
|
|
+ }
|
|
|
+ public func exporterDidCancel() {
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ public func exportProgress(_ progress: Float) {
|
|
|
+ print("exportProgress")
|
|
|
+ }
|
|
|
+
|
|
|
+ public func exportError(_ errorCode: Int32) {
|
|
|
+ print("export error : \(errorCode)")
|
|
|
+ composeResult?(false)
|
|
|
+ }
|
|
|
+
|
|
|
+ public func exporterDidStart() {
|
|
|
+ print("exporterDidStart")
|
|
|
+ }
|
|
|
+
|
|
|
+ public func exporterDidEnd(_ outputPath: String!) {
|
|
|
+ composeResult?(true)
|
|
|
+ }
|
|
|
+}
|
|
|
+extension String {
|
|
|
+ func toInt() -> Int? {
|
|
|
+ return Int(self)
|
|
|
+ }
|
|
|
+ func toFloat() -> Float? {
|
|
|
+ return Float(self)
|
|
|
+ }
|
|
|
+ func toDouble() -> Double? {
|
|
|
+ return Double(self)
|
|
|
+ }
|
|
|
+ //MARK:- 去除字符串两端的空白字符
|
|
|
+ func trim() -> String {
|
|
|
+ return self.trimmingCharacters(in: CharacterSet.whitespaces)
|
|
|
+ }
|
|
|
+ func subString(to: Int) -> String {
|
|
|
+ var to = to
|
|
|
+ if to > self.count {
|
|
|
+ to = self.count
|
|
|
+ }
|
|
|
+ return String(self.prefix(to))
|
|
|
+}
|
|
|
+ func subString(from: Int) -> String {
|
|
|
+ if from >= self.count {
|
|
|
+ return ""
|
|
|
+ }
|
|
|
+ let startIndex = self.index(self.startIndex, offsetBy: from)
|
|
|
+ let endIndex = self.endIndex
|
|
|
+ return String(self[startIndex..<endIndex])
|
|
|
+}
|
|
|
+ func subString(start: Int, end: Int) -> String {
|
|
|
+ if start < end {
|
|
|
+ let startIndex = self.index(self.startIndex, offsetBy: start)
|
|
|
+ let endIndex = self.index(self.startIndex, offsetBy: end)
|
|
|
+
|
|
|
+ return String(self[startIndex..<endIndex])
|
|
|
+ }
|
|
|
+ return ""
|
|
|
+ }
|
|
|
+
|
|
|
+
|
|
|
+}
|