|
|
@@ -0,0 +1,346 @@
|
|
|
+//
|
|
|
+// AliCameraViewFactory.swift
|
|
|
+// AliyunOSSiOS
|
|
|
+//
|
|
|
+// Created by weihong huang on 2020/8/20.
|
|
|
+//
|
|
|
+
|
|
|
+import Flutter
|
|
|
+import Foundation
|
|
|
+import AliyunVideoSDKPro
|
|
|
+
|
|
|
+public class AliCameraViewFactory : NSObject, FlutterPlatformViewFactory {
|
|
|
+ var messeneger: FlutterBinaryMessenger!
|
|
|
+
|
|
|
+ public func create(withFrame frame: CGRect, viewIdentifier viewId: Int64, arguments args: Any?) -> FlutterPlatformView {
|
|
|
+ return AliCameraView(frame: frame, viewIdentifier: viewId, arguments: args, binaryMessenger: self.messeneger)
|
|
|
+ }
|
|
|
+
|
|
|
+ init(messeneger: FlutterBinaryMessenger) {
|
|
|
+ super.init()
|
|
|
+ self.messeneger = messeneger
|
|
|
+ }
|
|
|
+
|
|
|
+ public func createArgsCodec() -> FlutterMessageCodec & NSObjectProtocol {
|
|
|
+ return FlutterStandardMessageCodec.sharedInstance()
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+public class AliCameraView : NSObject, FlutterPlatformView, AliyunIRecorderDelegate, AliyunIExporterCallback {
|
|
|
+ fileprivate var uiView: UIView = UIView()
|
|
|
+ fileprivate var recorder: AliyunIRecorder!
|
|
|
+ fileprivate var methodChannel: FlutterMethodChannel!
|
|
|
+ fileprivate var taskPath: String!
|
|
|
+ fileprivate var aliyunEditor: AliyunEditor!
|
|
|
+ fileprivate var exportResult: FlutterResult!
|
|
|
+
|
|
|
+ init(frame: CGRect, viewIdentifier viewId: Int64, arguments args: Any?, binaryMessenger: FlutterBinaryMessenger) {
|
|
|
+ super.init()
|
|
|
+ // init view size
|
|
|
+ let dict = args as! [String: Any]
|
|
|
+ self.uiView.frame = CGRect.init(x: dict["x"] as? Float64 ?? 0,
|
|
|
+ y: dict["y"] as? Float64 ?? 0,
|
|
|
+ width: dict["width"] as? Float64 ?? Float64(UIScreen.main.bounds.width),
|
|
|
+ height: dict["height"] as? Float64 ?? Float64(UIScreen.main.bounds.height))
|
|
|
+ // init method channel
|
|
|
+ self.methodChannel = FlutterMethodChannel(name: "com.i2edu.cameraLib/camera_view_\(viewId)", binaryMessenger: binaryMessenger)
|
|
|
+ self.methodChannel?.setMethodCallHandler ({
|
|
|
+ [weak self]
|
|
|
+ (call: FlutterMethodCall, result: @escaping FlutterResult) -> Void in
|
|
|
+ if let this = self {
|
|
|
+ this.onMethonCall(call: call, result: result)
|
|
|
+ }
|
|
|
+ })
|
|
|
+ }
|
|
|
+
|
|
|
+ public func view() -> UIView {
|
|
|
+ return self.uiView
|
|
|
+ }
|
|
|
+
|
|
|
+ func onMethonCall(call: FlutterMethodCall, result: @escaping FlutterResult) {
|
|
|
+ let args = call.arguments as? [String: Any]
|
|
|
+ switch call.method {
|
|
|
+ case "create":
|
|
|
+ let recordOptions = args?["recordOption"] as? [String: Any]
|
|
|
+ let taskPath = args?["taskPath"] as! String
|
|
|
+ do {
|
|
|
+ try setUpCameraView(recordOption: recordOptions!, taskPath: taskPath)
|
|
|
+ result(true)
|
|
|
+ } catch {
|
|
|
+ result(false)
|
|
|
+ }
|
|
|
+ break
|
|
|
+ case "startPreview":
|
|
|
+ do {
|
|
|
+ try startPreview()
|
|
|
+ result(true)
|
|
|
+ } catch {
|
|
|
+ result(false)
|
|
|
+ }
|
|
|
+ break;
|
|
|
+ case "stopPreview":
|
|
|
+ do {
|
|
|
+ try stopPreview()
|
|
|
+ result(true)
|
|
|
+ } catch {
|
|
|
+ result(false)
|
|
|
+ }
|
|
|
+ break;
|
|
|
+ case "switchCamera":
|
|
|
+ do {
|
|
|
+ try switchCamera()
|
|
|
+ result(true)
|
|
|
+ } catch {
|
|
|
+ result(false)
|
|
|
+ }
|
|
|
+ break;
|
|
|
+ case "setBeauty":
|
|
|
+ do {
|
|
|
+ try setBeauty(level: args?["level"] as? Int32 ?? 0)
|
|
|
+ result(true)
|
|
|
+ } catch {
|
|
|
+ result(false)
|
|
|
+ }
|
|
|
+ break;
|
|
|
+ case "setFilter":
|
|
|
+ do {
|
|
|
+ try setFilter(path: args?["path"] as? String)
|
|
|
+ result(true)
|
|
|
+ } catch {
|
|
|
+ result(false)
|
|
|
+ }
|
|
|
+ break;
|
|
|
+ case "startRecord":
|
|
|
+ do {
|
|
|
+ try startRecord(
|
|
|
+ maxDuration: args?["max"] as! Int32,
|
|
|
+ recordPath: args?["recordPath"] as! String)
|
|
|
+ result(true)
|
|
|
+ } catch {
|
|
|
+ result(FlutterError.init(code: "1001", message: error.localizedDescription, details: nil))
|
|
|
+ }
|
|
|
+ break
|
|
|
+ case "startCompose":
|
|
|
+ do {
|
|
|
+ self.exportResult = result
|
|
|
+ let composeOption = args?["composeOption"] as! [String: Any]
|
|
|
+ try startCompose(
|
|
|
+ outputPath: args?["outputPath"] as! String,
|
|
|
+ bgmPath: args?["bgmPath"] as? String,
|
|
|
+ paths: args?["paths"] as! [String],
|
|
|
+ durations: args?["durations"] as! [Int32],
|
|
|
+ composeOption: composeOption
|
|
|
+ )
|
|
|
+ } catch {
|
|
|
+ result(FlutterError.init(code: "1002", message: error.localizedDescription, details: nil))
|
|
|
+ }
|
|
|
+ break
|
|
|
+ case "onDestroy":
|
|
|
+ do {
|
|
|
+ try onDestroy()
|
|
|
+ result(true)
|
|
|
+ } catch {
|
|
|
+ result(false)
|
|
|
+ }
|
|
|
+ break
|
|
|
+ default:
|
|
|
+ result(FlutterMethodNotImplemented)
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ func setUpCameraView(recordOption: [String: Any], taskPath: String) throws {
|
|
|
+ self.taskPath = taskPath
|
|
|
+
|
|
|
+ self.recorder = AliyunIRecorder.init(delegate: self, videoSize: CGSize(width: recordOption["videoWidth"] as! Int, height: recordOption["videoHeight"] as! Int))
|
|
|
+ let keys = recordOption.keys
|
|
|
+ if (keys.contains("fps")) {
|
|
|
+ self.recorder.recordFps = recordOption["fps"] as! Int32
|
|
|
+ }
|
|
|
+ if (keys.contains("videoCodecs")) {
|
|
|
+ if (recordOption["videoCodecs"] as! Int == 0) {
|
|
|
+ self.recorder.encodeMode = 0
|
|
|
+ } else {
|
|
|
+ self.recorder.encodeMode = 1
|
|
|
+ }
|
|
|
+ }
|
|
|
+ if (keys.contains("quality")) {
|
|
|
+ self.recorder.videoQuality = AliyunVideoQuality.init(rawValue: recordOption["quality"] as! Int) ?? AliyunVideoQuality.medium
|
|
|
+ }
|
|
|
+ if (keys.contains("videoBitrate")) {
|
|
|
+ self.recorder.bitrate = recordOption["videoBitrate"] as! Int32
|
|
|
+ }
|
|
|
+ if (keys.contains("gop")) {
|
|
|
+ self.recorder.gop = recordOption["gop"] as! Int32
|
|
|
+ }
|
|
|
+
|
|
|
+ self.recorder?.beautifyStatus = false
|
|
|
+ self.recorder?.beautifyValue = 0
|
|
|
+ self.recorder?.preview = self.uiView
|
|
|
+ }
|
|
|
+
|
|
|
+ func startPreview() throws {
|
|
|
+ self.recorder?.startPreview(withPositon: AliyunIRecorderCameraPosition.front)
|
|
|
+ }
|
|
|
+
|
|
|
+ func stopPreview() throws {
|
|
|
+ self.recorder?.stopPreview()
|
|
|
+ }
|
|
|
+
|
|
|
+ func switchCamera() throws {
|
|
|
+ self.recorder?.switchCameraPosition()
|
|
|
+ }
|
|
|
+
|
|
|
+ func setBeauty(level: Int32) throws {
|
|
|
+ self.recorder?.beautifyStatus = level != 0
|
|
|
+ self.recorder?.beautifyValue = level
|
|
|
+ }
|
|
|
+
|
|
|
+ func setFilter(path: String?) throws {
|
|
|
+ if path == nil || path?.count == 0 {
|
|
|
+ recorder?.deleteFilter()
|
|
|
+ } else {
|
|
|
+ self.recorder?.apply(AliyunEffectFilter.init(file: path))
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ func startRecord(maxDuration: Int32, recordPath: String) throws {
|
|
|
+ recorder?.taskPath = taskPath
|
|
|
+ recorder?.clipManager?.deleteAllPart()
|
|
|
+ recorder?.outputPath = recordPath
|
|
|
+ recorder?.clipManager?.maxDuration = CGFloat(Float(maxDuration) / 1000.0)
|
|
|
+ recorder?.startRecording()
|
|
|
+ }
|
|
|
+
|
|
|
+ func startCompose(outputPath: String, bgmPath: String?, paths: [String], durations: [Int32], composeOption: [String: Any]) throws {
|
|
|
+ // set up AliyunImporter
|
|
|
+ let importer = AliyunImporter.init(
|
|
|
+ path: self.taskPath,
|
|
|
+ outputSize: CGSize(width: composeOption["outputWidth"] as! CGFloat, height: composeOption["outputHeight"] as! CGFloat))
|
|
|
+ let composeParam = AliyunVideoParam.init()
|
|
|
+ let keys = composeOption.keys
|
|
|
+ if keys.contains("gop") {
|
|
|
+ composeParam.gop = composeOption["gop"] as! Int32
|
|
|
+ }
|
|
|
+ if keys.contains("bitrate") {
|
|
|
+ composeParam.bitrate = composeOption["bitrate"] as! Int32
|
|
|
+ }
|
|
|
+ if keys.contains("frameRate") {
|
|
|
+ composeParam.fps = composeOption["frameRate"] as! Int32
|
|
|
+ }
|
|
|
+ if keys.contains("videoCodecs") {
|
|
|
+ var codecType:AliyunVideoCodecType
|
|
|
+ let type = composeOption["videoCodecs"] as! Int32
|
|
|
+ if type == 0 {
|
|
|
+ codecType = AliyunVideoCodecType.hardware
|
|
|
+ } else if type == 1 {
|
|
|
+ codecType = AliyunVideoCodecType.openh264
|
|
|
+ } else if type == 2 {
|
|
|
+ codecType = AliyunVideoCodecType.fFmpeg
|
|
|
+ } else {
|
|
|
+ codecType = AliyunVideoCodecType.typeAuto
|
|
|
+ }
|
|
|
+ composeParam.codecType = codecType
|
|
|
+ }
|
|
|
+ if keys.contains("quality") {
|
|
|
+ composeParam.videoQuality =
|
|
|
+ AliyunVideoQuality.init(rawValue: composeOption["quality"] as! Int) ?? AliyunVideoQuality.medium
|
|
|
+ }
|
|
|
+ if keys.contains("scaleMode") {
|
|
|
+ composeParam.scaleMode =
|
|
|
+ AliyunScaleMode.init(rawValue: composeOption["scaleMode"] as! Int) ?? AliyunScaleMode.fit
|
|
|
+ }
|
|
|
+ for index in 0..<paths.count {
|
|
|
+ importer?.addMediaClip(AliyunClip.init(videoPath: paths[index], animDuration: 0))
|
|
|
+ }
|
|
|
+ importer?.setVideoParam(composeParam)
|
|
|
+ importer?.generateProjectConfigure()
|
|
|
+ // set up AliyunEditor
|
|
|
+ self.aliyunEditor = AliyunEditor.init(path: self.taskPath, preview: nil)
|
|
|
+ if bgmPath != nil && bgmPath?.count != 0 {
|
|
|
+ self.aliyunEditor.apply(AliyunEffectMusic.init(file: bgmPath))
|
|
|
+ }
|
|
|
+ self.aliyunEditor.delegate = self
|
|
|
+ self.aliyunEditor.startEdit()
|
|
|
+
|
|
|
+ let exporter = self.aliyunEditor.getExporter()
|
|
|
+ exporter?.startExport(outputPath)
|
|
|
+ }
|
|
|
+
|
|
|
+ func onDestroy() throws {
|
|
|
+ try stopPreview()
|
|
|
+ recorder?.stopRecording()
|
|
|
+ recorder?.destroy()
|
|
|
+ recorder = nil
|
|
|
+ }
|
|
|
+
|
|
|
+ // ----- record delegate
|
|
|
+
|
|
|
+ public func recorderDeviceAuthorization(_ status: AliyunIRecorderDeviceAuthor) {
|
|
|
+ if status == AliyunIRecorderDeviceAuthor.enabled {
|
|
|
+ print("sdk enabled")
|
|
|
+ } else {
|
|
|
+ print("sdk disabled")
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ public func recorderVideoDuration(_ duration: CGFloat) {
|
|
|
+ print("record update \(duration)")
|
|
|
+ methodChannel.invokeMethod("recordUpdate", arguments: ["progress": Int(duration * 1000)])
|
|
|
+ }
|
|
|
+
|
|
|
+ public func recorderDidStopWithMaxDuration() {
|
|
|
+ print("record stop with max duration")
|
|
|
+ recorder?.finishRecording()
|
|
|
+ }
|
|
|
+
|
|
|
+ public func recorderDidFinishRecording() {
|
|
|
+ print("record completed")
|
|
|
+ methodChannel.invokeMethod("recordUpdate", arguments: ["progress": Int((recorder?.clipManager?.maxDuration ?? 0) * 1000), "path": recorder?.outputPath ?? ""])
|
|
|
+ }
|
|
|
+
|
|
|
+ public func recoderError(_ error: Error!) {
|
|
|
+ print("record error \(error.localizedDescription)")
|
|
|
+ recorder?.finishRecording()
|
|
|
+ }
|
|
|
+
|
|
|
+ // ----- export delegate
|
|
|
+ public func exporterDidStart() {
|
|
|
+ print("export did start")
|
|
|
+ }
|
|
|
+
|
|
|
+ public func exportProgress(_ progress: Float) {
|
|
|
+ print("export progress \(progress)")
|
|
|
+ }
|
|
|
+
|
|
|
+ public func exportError(_ errorCode: Int32) {
|
|
|
+ print("export error \(errorCode)")
|
|
|
+ exportResult(false)
|
|
|
+ }
|
|
|
+
|
|
|
+ public func exporterDidEnd(_ outputPath: String!) {
|
|
|
+ print("export did end \(outputPath ?? "")")
|
|
|
+ exportResult(true)
|
|
|
+ }
|
|
|
+
|
|
|
+ public func exporterDidCancel() {
|
|
|
+ print("export did cancel")
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+
|
|
|
+extension AliCameraView : AliyunIPlayerCallback, AliyunIRenderCallback{
|
|
|
+ public func playerDidEnd() {
|
|
|
+ }
|
|
|
+
|
|
|
+ public func playProgress(_ playSec: Double, streamProgress streamSec: Double) {
|
|
|
+ }
|
|
|
+
|
|
|
+ public func playError(_ errorCode: Int32) {
|
|
|
+ }
|
|
|
+
|
|
|
+ public func seekDidEnd() {
|
|
|
+ }
|
|
|
+
|
|
|
+ public func playerDidStart() {
|
|
|
+ }
|
|
|
+}
|