浏览代码

卡点模块组件化

jsonwang 4 年之前
父节点
当前提交
0278c46605
共有 100 个文件被更改,包括 11612 次插入5 次删除
  1. 31 5
      BFFramework.podspec
  2. 二进制
      BFFramework/Assets/Lark20210513-102008.png
  3. 19 0
      BFFramework/Classes/BFFramework-Bridging-Header.h
  4. 179 0
      BFFramework/Classes/Base/Controller/PQBaseViewController.swift
  5. 190 0
      BFFramework/Classes/Base/Controller/PQBaseWebViewController.swift
  6. 46 0
      BFFramework/Classes/Base/Controller/PQNavigatinController.swift
  7. 267 0
      BFFramework/Classes/Base/Controller/PQPhotoAlbumController.swift
  8. 558 0
      BFFramework/Classes/Base/Controller/PQPhotoMaterialController.swift
  9. 189 0
      BFFramework/Classes/Base/Model/PQBaseModel.swift
  10. 48 0
      BFFramework/Classes/Base/View/PQActivityIndicatorView.swift
  11. 103 0
      BFFramework/Classes/Base/View/PQAssetCategoryCell.swift
  12. 81 0
      BFFramework/Classes/Base/View/PQBaseVideoInfoView.swift
  13. 252 0
      BFFramework/Classes/Base/View/PQChoseMaterialCell.swift
  14. 121 0
      BFFramework/Classes/Base/View/PQFollowButton.swift
  15. 41 0
      BFFramework/Classes/Base/View/PQGIFImageView.swift
  16. 53 0
      BFFramework/Classes/Base/View/PQHeartAnimation.swift
  17. 101 0
      BFFramework/Classes/Base/View/PQLoadingHUB.swift
  18. 174 0
      BFFramework/Classes/Base/View/PQRemindView.swift
  19. 103 0
      BFFramework/Classes/Base/View/PQSectionHeadView.swift
  20. 113 0
      BFFramework/Classes/Base/View/PQSelectedOprationView.swift
  21. 27 0
      BFFramework/Classes/Base/View/PQTabBar.swift
  22. 104 0
      BFFramework/Classes/Base/View/PQTextView.swift
  23. 26 0
      BFFramework/Classes/Base/ViewModel/Extensions/OperationQueue+Ext.swift
  24. 39 0
      BFFramework/Classes/Base/ViewModel/Extensions/Task+Ext.swift
  25. 647 0
      BFFramework/Classes/Base/ViewModel/PQBaseViewModel.swift
  26. 75 0
      BFFramework/Classes/Base/ViewModel/PQDownloadFileManager.swift
  27. 225 0
      BFFramework/Classes/Base/ViewModel/PQDownloadManager.swift
  28. 188 0
      BFFramework/Classes/Base/ViewModel/PQSessionManager.swift
  29. 121 0
      BFFramework/Classes/Base/ViewModel/PQUploadViewModel.swift
  30. 109 0
      BFFramework/Classes/Categorys/Int+Ext.swift
  31. 169 0
      BFFramework/Classes/Categorys/String+Ext.swift
  32. 63 0
      BFFramework/Classes/Categorys/UIButton+ext.swift
  33. 46 0
      BFFramework/Classes/Categorys/UIColor+Ext.swift
  34. 210 0
      BFFramework/Classes/Categorys/UIImage+Ext.swift
  35. 484 0
      BFFramework/Classes/Categorys/UIView+Ext.swift
  36. 727 0
      BFFramework/Classes/Enums/Enums.swift
  37. 319 0
      BFFramework/Classes/EventTrack/Model/PQVideoMakeEventTrackModel.swift
  38. 524 0
      BFFramework/Classes/EventTrack/ViewModel/PQEventTrackViewModel.swift
  39. 100 0
      BFFramework/Classes/PModels/PQDownloadModel.swift
  40. 178 0
      BFFramework/Classes/PModels/PQLoginUserInfo.swift
  41. 75 0
      BFFramework/Classes/PModels/PQReCreateModel.swift
  42. 31 0
      BFFramework/Classes/PModels/PQUploadModel.swift
  43. 174 0
      BFFramework/Classes/PModels/PQUserInfoModel.swift
  44. 352 0
      BFFramework/Classes/PModels/PQVideoListModel.swift
  45. 77 0
      BFFramework/Classes/PModels/editDarftModels/PQEditAudioTrackMaterialModel.swift
  46. 63 0
      BFFramework/Classes/PModels/editDarftModels/PQEditAudioTrackModel.swift
  47. 55 0
      BFFramework/Classes/PModels/editDarftModels/PQEditBaseModel.swift
  48. 29 0
      BFFramework/Classes/PModels/editDarftModels/PQEditBgmInfoModel.swift
  49. 33 0
      BFFramework/Classes/PModels/editDarftModels/PQEditFileMergeTable.swift
  50. 31 0
      BFFramework/Classes/PModels/editDarftModels/PQEditMaterialDurationFitModel.swift
  51. 29 0
      BFFramework/Classes/PModels/editDarftModels/PQEditMaterialEffectModel.swift
  52. 27 0
      BFFramework/Classes/PModels/editDarftModels/PQEditMaterialLayerModel.swift
  53. 29 0
      BFFramework/Classes/PModels/editDarftModels/PQEditMaterialPositionModel.swift
  54. 32 0
      BFFramework/Classes/PModels/editDarftModels/PQEditMaterialSizeClipModel.swift
  55. 63 0
      BFFramework/Classes/PModels/editDarftModels/PQEditProduceVoiceConfigModel.swift
  56. 78 0
      BFFramework/Classes/PModels/editDarftModels/PQEditProjectModel.swift
  57. 158 0
      BFFramework/Classes/PModels/editDarftModels/PQEditSdataModel.swift
  58. 31 0
      BFFramework/Classes/PModels/editDarftModels/PQEditSectionExtDataModel.swift
  59. 287 0
      BFFramework/Classes/PModels/editDarftModels/PQEditSectionModel.swift
  60. 71 0
      BFFramework/Classes/PModels/editDarftModels/PQEditSectionTimelineModel.swift
  61. 44 0
      BFFramework/Classes/PModels/editDarftModels/PQEditSubTitleModel.swift
  62. 47 0
      BFFramework/Classes/PModels/editDarftModels/PQEditSubtitleInfoModel.swift
  63. 34 0
      BFFramework/Classes/PModels/editDarftModels/PQEditSystemParamModel.swift
  64. 46 0
      BFFramework/Classes/PModels/editDarftModels/PQEditVideoMetaDataModel.swift
  65. 265 0
      BFFramework/Classes/PModels/editDarftModels/PQEditVisionTrackMaterialsModel.swift
  66. 114 0
      BFFramework/Classes/PModels/editDarftModels/PQEditVisionTrackModel.swift
  67. 217 0
      BFFramework/Classes/PQGPUImage/Source/BasicOperation.swift
  68. 39 0
      BFFramework/Classes/PQGPUImage/Source/CameraConversion.swift
  69. 20 0
      BFFramework/Classes/PQGPUImage/Source/Color.swift
  70. 8 0
      BFFramework/Classes/PQGPUImage/Source/ConvertedShaders_GLES.swift
  71. 77 0
      BFFramework/Classes/PQGPUImage/Source/FillMode.swift
  72. 277 0
      BFFramework/Classes/PQGPUImage/Source/Framebuffer.swift
  73. 64 0
      BFFramework/Classes/PQGPUImage/Source/FramebufferCache.swift
  74. 16 0
      BFFramework/Classes/PQGPUImage/Source/GPUImage-Bridging-Header.h
  75. 27 0
      BFFramework/Classes/PQGPUImage/Source/ImageGenerator.swift
  76. 42 0
      BFFramework/Classes/PQGPUImage/Source/ImageOrientation.swift
  77. 124 0
      BFFramework/Classes/PQGPUImage/Source/Matrix.swift
  78. 14 0
      BFFramework/Classes/PQGPUImage/Source/NSObject+Exception.h
  79. 24 0
      BFFramework/Classes/PQGPUImage/Source/NSObject+Exception.m
  80. 66 0
      BFFramework/Classes/PQGPUImage/Source/NXAVUtil.h
  81. 430 0
      BFFramework/Classes/PQGPUImage/Source/NXAVUtil.m
  82. 106 0
      BFFramework/Classes/PQGPUImage/Source/OpenGLContext_Shared.swift
  83. 281 0
      BFFramework/Classes/PQGPUImage/Source/OpenGLRendering.swift
  84. 26 0
      BFFramework/Classes/PQGPUImage/Source/OperationGroup.swift
  85. 17 0
      BFFramework/Classes/PQGPUImage/Source/Operations/AdaptiveThreshold.swift
  86. 5 0
      BFFramework/Classes/PQGPUImage/Source/Operations/AddBlend.swift
  87. 9 0
      BFFramework/Classes/PQGPUImage/Source/Operations/AlphaBlend.swift
  88. 22 0
      BFFramework/Classes/PQGPUImage/Source/Operations/AmatorkaFilter.swift
  89. 74 0
      BFFramework/Classes/PQGPUImage/Source/Operations/AverageColorExtractor.swift
  90. 49 0
      BFFramework/Classes/PQGPUImage/Source/Operations/AverageLuminanceExtractor.swift
  91. 19 0
      BFFramework/Classes/PQGPUImage/Source/Operations/AverageLuminanceThreshold.swift
  92. 11 0
      BFFramework/Classes/PQGPUImage/Source/Operations/BilateralBlur.swift
  93. 82 0
      BFFramework/Classes/PQGPUImage/Source/Operations/BoxBlur.swift
  94. 9 0
      BFFramework/Classes/PQGPUImage/Source/Operations/BrightnessAdjustment.swift
  95. 13 0
      BFFramework/Classes/PQGPUImage/Source/Operations/BulgeDistortion.swift
  96. 5 0
      BFFramework/Classes/PQGPUImage/Source/Operations/CGAColorspaceFilter.swift
  97. 37 0
      BFFramework/Classes/PQGPUImage/Source/Operations/CannyEdgeDetection.swift
  98. 13 0
      BFFramework/Classes/PQGPUImage/Source/Operations/ChromaKeyBlend.swift
  99. 13 0
      BFFramework/Classes/PQGPUImage/Source/Operations/ChromaKeying.swift
  100. 51 0
      BFFramework/Classes/PQGPUImage/Source/Operations/CircleGenerator.swift

+ 31 - 5
BFFramework.podspec

@@ -25,20 +25,46 @@ TODO: Add long description of the pod here.
   # s.screenshots     = 'www.example.com/screenshots_1', 'www.example.com/screenshots_2'
   s.license          = { :type => 'MIT', :file => 'LICENSE' }
   s.author           = { '287971051@qq.com' => '287971051@qq.com' }
-  s.source           = { :git => 'https://git.yishihui.comiOS/BFFramework.git', :tag => s.version.to_s }
+  s.source           = { :git => 'https://git.yishihui.comiOS/BFFramework.git', :git => 'https://github.com/jsonwang/NXFramework-Swift.git' , :tag => s.version.to_s   }
   # s.social_media_url = 'https://twitter.com/<TWITTER_USERNAME>'
 
-  s.ios.deployment_target = '9.0'
+  s.ios.deployment_target = '10.0'
 
   s.source_files = 'BFFramework/Classes/**/*'
 
    s.resource_bundles = {
      'BFFramework' => ['BFFramework/Assets/*.png']
    }
+   s.static_framework = true
 
-  # s.public_header_files = 'Pod/Classes/**/*.h'
-  # s.frameworks = 'UIKit', 'MapKit'
-#    s.dependency 'Alamofire','4.9.1' # 网络请求库
+   s.xcconfig = { 'CLANG_MODULES_AUTOLINK' => 'YES',
+                         'OTHER_SWIFT_FLAGS' => "$(inherited) -DGLES"}
+
+   s.public_header_files = 'BFFramework/Classes/**/*.h'
+   s.frameworks = 'UIKit', 'AVFoundation','OpenGLES', 'CoreMedia', 'QuartzCore'
+    # 排除非 ios 平台的文件
+    # s.ios.exclude_files = 'framework/Source/Mac', 'framework/Source/Linux', 'framework/Source/Operations/Shaders/ConvertedShaders_GL.swift'
+
+    s.dependency 'Alamofire','4.9.1' # 网络请求库
     s.dependency 'SnapKit','4.2.0' # 布局库
     s.dependency 'Kingfisher','4.10.1' # 图片加载库
+    s.dependency 'RealmSwift','10.7.2' # Realm数据库
+    s.dependency 'ObjectMapper','4.2.0' # json转model库
+    s.dependency 'KeychainAccess','4.2.2' # 钥匙串库
+    s.dependency 'Toast-Swift','5.0.1' # Toast提示组件
+    s.dependency 'AliyunOSSiOS','2.10.8' # 阿里云组件
+    s.dependency 'WechatOpenSDK-Swift','1.8.7.1' # 微信组件
+    s.dependency 'MJRefresh','3.5.0' # 刷新组件
+    s.dependency 'FDFullscreenPopGesture'
+    s.dependency 'LMJHorizontalScrollText'
+
+    # s.dependency 'UMCommon','7.2.5' # 友盟基础组件
+    # s.dependency 'UMDevice','1.1.0' # 友盟基础组件
+    # s.dependency 'UMCSecurityPlugins','1.0.6' # 友盟安全组件
+    # s.dependency 'UMAPM','1.1.1' # 友盟统计分析组件
+    # s.dependency 'UMPush','3.3.1' # 友盟推送组件
+    # s.dependency 'Bugly','2.5.71' # 奔溃分析组件
+
+    s.dependency "NXFramework-Swift"
+    # s.dependency 'KingfisherWebP','0.4.2' # 加载WebP格式图片库 使用https://github.com/webmproject/libwebp.git地址可以不翻
 end

二进制
BFFramework/Assets/Lark20210513-102008.png


+ 19 - 0
BFFramework/Classes/BFFramework-Bridging-Header.h

@@ -0,0 +1,19 @@
+//
+//  Use this file to import your target's public headers that you would like to expose to Swift.
+//
+
+#import "AliyunOSSiOS.h"
+ 
+//#import "WXApi.h"
+//#import "WechatAuthSDK.h"
+//#import <UMCommon/UMCommon.h>
+//#import <UMCommon/MobClick.h>
+//#import <UMPush/UMessage.h>
+
+#import "PQBridgeObject.h"
+#import "MJRefresh.h"
+#import "UINavigationController+FDFullscreenPopGesture.h"
+#import "DES3Util.h"
+#import "LMJHorizontalScrollText.h"
+#import "FBShimmeringView.h"
+#import "UIControl+NXCategory.h"

+ 179 - 0
BFFramework/Classes/Base/Controller/PQBaseViewController.swift

@@ -0,0 +1,179 @@
+//
+//  PQBaseViewController.swift
+//  PQSpeed
+//
+//  Created by SanW on 2020/5/25.
+//  Copyright © 2020 BytesFlow. All rights reserved.
+//
+
+// import MediaPlayer
+import Alamofire
+import UIKit
+import NXFramework_Swift
+class PQBaseViewController: UIViewController, UIGestureRecognizerDelegate {
+    // 侧滑拦截返回
+    var popGestureHandle: (() -> Void)?
+    var naviTitle: String? // 标题
+    var rightButton: UIButton? // 右边按钮
+    var backButton: UIButton? // 左边按钮
+    var navTitleLabel: UILabel? // 标题
+    var navHeadImageView: UIImageView? // 导航条
+    var lineView: UIView? // 导航分隔线
+    var isHiddenStatus: Bool = false { // 更新状态栏
+        didSet {
+            setNeedsStatusBarAppearanceUpdate()
+        }
+    }
+
+    lazy var manager: NetworkReachabilityManager? = {
+        let manager = NetworkReachabilityManager(host: "www.baidu.com")
+        manager?.listener = { status in
+            if status == .reachable(.wwan) || status == .reachable(.ethernetOrWiFi) {
+           
+            }
+        }
+        return manager
+    }()
+
+    override func viewDidLoad() {
+        super.viewDidLoad()
+        navigationController?.isNavigationBarHidden = true
+        view.backgroundColor = UIColor.black
+
+        navHeadImageView = UIImageView(image: UIImage())
+        navHeadImageView?.isUserInteractionEnabled = true
+        navHeadImageView?.backgroundColor = UIColor.black
+        navHeadImageView?.frame = CGRect(x: 0, y: 0, width: cScreenWidth, height: cDevice_iPhoneNavBarAndStatusBarHei)
+        view.addSubview(navHeadImageView!)
+
+        lineView = UIView(frame: CGRect(x: 0, y: (navHeadImageView?.frame.maxY ?? cDevice_iPhoneNavBarAndStatusBarHei) - 0.5, width: cScreenWidth, height: 0.5))
+        lineView?.backgroundColor = UIColor.black
+        view.addSubview(lineView!)
+        UINavigationBar.appearance().setBackgroundImage(UIImage(), for: .default)
+        automaticallyAdjustsScrollViewInsets = false
+        navigationController?.interactivePopGestureRecognizer?.delegate = self
+        fd_prefersNavigationBarHidden = true
+    }
+
+    func hiddenNavigation() {
+        navHeadImageView?.isHidden = true
+        lineView?.isHidden = true
+    }
+
+    func showNavigation() {
+        if navHeadImageView != nil {
+            navHeadImageView?.isHidden = false
+            lineView?.isHidden = false
+            view.bringSubviewToFront(navHeadImageView!)
+        }
+    }
+
+    func leftBackButton() {
+        leftButton(image: "icon_detail_back")
+    }
+
+    func leftButton(image: String?) {
+        let leftButton = UIButton(type: .custom)
+        leftButton.frame = CGRect(x: 0, y: cDevice_iPhoneStatusBarHei, width: cDefaultMargin * 4, height: cDefaultMargin * 4)
+        leftButton.imageEdgeInsets = UIEdgeInsets(top: 0, left: 0, bottom: -5, right: 0)
+        leftButton.setImage(UIImage(named: image ?? "icon_detail_back"), for: .normal)
+        leftButton.addTarget(self, action: #selector(backBtnClick), for: .touchUpInside)
+        navHeadImageView?.addSubview(leftButton)
+        backButton = leftButton
+    }
+
+    func rightButtonItem(image: String?, title: String?) {
+        let rightButtonItem = UIButton(type: .custom)
+        var rightW: CGFloat = cDefaultMargin
+        if title != nil, title?.count ?? 0 > 0 {
+            rightW = rightW + sizeWithText(text: title ?? "", font: UIFont.systemFont(ofSize: 16), size: CGSize(width: CGFloat.greatestFiniteMagnitude, height: cDefaultMargin * 4)).width
+            rightButtonItem.setTitle(title, for: .normal)
+            rightButtonItem.setTitleColor(UIColor.hexColor(hexadecimal: "#242F44"), for: .normal)
+            rightButtonItem.titleLabel?.font = UIFont.systemFont(ofSize: 16)
+        }
+        if image != nil, image?.count ?? 0 > 0 {
+            rightW = rightW + cDefaultMargin * 4
+            rightButtonItem.setImage(UIImage(named: image ?? ""), for: .normal)
+            rightButtonItem.imageEdgeInsets = UIEdgeInsets(top: 0, left: 0, bottom: (title != nil && title?.count ?? 0 > 0) ? 0 : -5, right: 0)
+        }
+        rightButtonItem.adjustsImageWhenHighlighted = false
+        rightButtonItem.frame = CGRect(x: cScreenWidth - rightW, y: cDevice_iPhoneStatusBarHei, width: rightW, height: cDefaultMargin * 4)
+        rightButtonItem.addTarget(self, action: #selector(rightBtnClick(sender:)), for: .touchUpInside)
+        rightButtonItem.contentHorizontalAlignment = .center
+        navHeadImageView?.addSubview(rightButtonItem)
+        rightButton = rightButtonItem
+    }
+
+    func setTitle(title: String?, color: UIColor = UIColor.white) {
+        naviTitle = title
+        if navTitleLabel == nil {
+            let titleLabel = UILabel(frame: CGRect(x: cDefaultMargin * 5, y: cDevice_iPhoneStatusBarHei, width: cScreenWidth - 100, height: cDefaultMargin * 4))
+            titleLabel.textColor = color
+            titleLabel.textAlignment = .center
+            navTitleLabel = titleLabel
+            navHeadImageView?.addSubview(titleLabel)
+        }
+        navTitleLabel?.text = title
+    }
+
+    @objc func rightBtnClick(sender _: UIButton) {}
+
+    @objc func backBtnClick() {
+        navigationController?.popViewController(animated: true)
+    }
+
+    override func viewDidAppear(_ animated: Bool) {
+        super.viewDidAppear(animated)
+      
+    }
+
+    override func viewWillDisappear(_ animated: Bool) {
+        super.viewWillDisappear(animated)
+        PQLoadingHUB.shared.dismissHUB()
+    }
+
+    override func viewDidDisappear(_ animated: Bool) {
+        super.viewDidDisappear(animated)
+       
+        if view.viewWithTag(cGuideTag) != nil {
+            view.viewWithTag(cGuideTag)?.removeFromSuperview()
+        }
+    }
+
+    deinit {
+        PQNotification.removeObserver(self)
+        PQLog(message: "\(String(describing: type(of: self)))被销毁")
+    }
+
+    override var preferredStatusBarStyle: UIStatusBarStyle {
+        return .lightContent
+    }
+
+    override var prefersStatusBarHidden: Bool {
+        return isHiddenStatus
+    }
+
+    /// 禁止滑动返回
+    /// - Returns: <#description#>
+    func disablePopGesture() -> PQBaseViewController {
+        let traget = navigationController?.interactivePopGestureRecognizer?.delegate
+        let pan = UIPanGestureRecognizer(target: traget, action: #selector(popGesture(panGes:)))
+        view.addGestureRecognizer(pan)
+        return self
+    }
+
+    /// 拦截侧滑手势
+    /// - Returns: <#description#>
+    @objc private func popGesture(panGes: UIPanGestureRecognizer) {
+        if panGes.state == .ended, popGestureHandle != nil {
+            popGestureHandle!()
+        }
+    }
+
+    func gestureRecognizer(_: UIGestureRecognizer, shouldReceive touch: UITouch) -> Bool {
+        if touch.view is UISlider {
+            return false
+        }
+        return true
+    }
+}

+ 190 - 0
BFFramework/Classes/Base/Controller/PQBaseWebViewController.swift

@@ -0,0 +1,190 @@
+
+//
+//  PQBaseWebViewController.swift
+//  PQSpeed
+//
+//  Created by SanW on 2020/5/27.
+//  Copyright © 2020 BytesFlow. All rights reserved.
+//
+
+import UIKit
+import WebKit
+
+class PQBaseWebViewController: PQBaseViewController {
+    var emptyData: PQEmptyModel? = {
+        let emptyData = PQEmptyModel()
+        emptyData.title = "网页加载失败,请重试~"
+        emptyData.emptyImage = "pic_network"
+        return emptyData
+    }()
+ 
+    lazy var webView: WKWebView = {
+        let config: WKWebViewConfiguration = WKWebViewConfiguration()
+        config.allowsInlineMediaPlayback = true
+        let webView: WKWebView = WKWebView(frame: CGRect(x: 0, y: cDevice_iPhoneNavBarAndStatusBarHei, width: cScreenWidth, height: cScreenHeigth - cDevice_iPhoneNavBarAndStatusBarHei), configuration: config)
+        webView.backgroundColor = UIColor.white
+        if #available(iOS 11.0, *) {
+            webView.scrollView.contentInsetAdjustmentBehavior = .never
+        } else {
+            automaticallyAdjustsScrollViewInsets = false
+        }
+        webView.navigationDelegate = self
+        return webView
+    }()
+
+    lazy var progresslayer: CALayer = {
+        let progresslayer = CALayer()
+        progresslayer.frame = CGRect(x: 0, y: cDevice_iPhoneNavBarAndStatusBarHei, width: cScreenWidth * 0.1, height: 2)
+        progresslayer.backgroundColor = UIColor.hexColor(hexadecimal: "#FF9500").cgColor
+        return progresslayer
+    }()
+
+    @objc var baseUrl: String? {
+        didSet {
+            if baseUrl != nil, baseUrl?.count ?? 0 > 0 {
+                webView.load(URLRequest(url: NSURL(string: baseUrl ?? "")! as URL, cachePolicy: .reloadIgnoringCacheData))
+                // 添加属性监听
+                webView.addObserver(self, forKeyPath: "estimatedProgress", options: .new, context: nil)
+                isAddObserve = true
+                view.layer.addSublayer(progresslayer)
+            }
+        }
+    }
+
+    @objc var baseTitle: String?
+    var evaluateJavaScript: String? // 交互
+
+    var isAddObserve: Bool = false
+
+    override func viewDidLoad() {
+        super.viewDidLoad()
+        // Do any additional setup after loading the view.
+        view.addSubview(webView)
+        leftButton(image: "icon_blanc_back")
+        navHeadImageView?.backgroundColor = UIColor.white
+    }
+
+    override func viewWillAppear(_ animated: Bool) {
+        super.viewWillAppear(animated)
+        if (UIApplication.shared.keyWindow?.viewWithTag(cProtocalViewTag)) != nil {
+            (UIApplication.shared.keyWindow?.viewWithTag(cProtocalViewTag))?.isHidden = true
+        }
+    }
+
+    override func viewWillDisappear(_ animated: Bool) {
+        super.viewWillDisappear(animated)
+        if (UIApplication.shared.keyWindow?.viewWithTag(cProtocalViewTag)) != nil {
+            (UIApplication.shared.keyWindow?.viewWithTag(cProtocalViewTag))?.isHidden = false
+        }
+    }
+
+    deinit {
+        if isAddObserve {
+            webView.removeObserver(self, forKeyPath: "estimatedProgress")
+        }
+    }
+}
+
+extension PQBaseWebViewController: WKNavigationDelegate {
+    func refreshClick() {
+        if baseUrl != nil, baseUrl?.count ?? 0 > 0 {
+            webView.load(URLRequest(url: NSURL(string: baseUrl ?? "")! as URL, cachePolicy: .useProtocolCachePolicy))
+        }
+    }
+
+    @objc func back() {
+        if webView.canGoBack {
+            webView.goBack()
+        } else if navigationController != nil {
+            navigationController?.popViewController(animated: true)
+        } else {
+            dismiss(animated: true, completion: nil)
+        }
+    }
+
+    override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey: Any]?, context: UnsafeMutableRawPointer?) {
+        if keyPath == "estimatedProgress" {
+            progresslayer.opacity = 1
+            let float = (change?[NSKeyValueChangeKey.newKey] as! NSNumber).floatValue
+            progresslayer.frame = CGRect(x: 0, y: cDevice_iPhoneNavBarAndStatusBarHei, width: cScreenWidth * CGFloat(float), height: 3)
+            if float == 1 {
+                weak var weakself = self
+                DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.2) {
+                    weakself?.progresslayer.opacity = 0
+                }
+                DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.8) {
+                    weakself?.progresslayer.frame = CGRect(x: 0, y: cDevice_iPhoneNavBarAndStatusBarHei, width: 0, height: 3)
+                }
+            }
+        } else {
+            super.observeValue(forKeyPath: keyPath, of: object, change: change, context: context)
+        }
+    }
+
+    func webView(_ webView: WKWebView, didFinish _: WKNavigation!) {
+        if baseTitle == nil || baseTitle?.count ?? 0 <= 0 {
+            webView.evaluateJavaScript("document.title") { [weak self] (any, _) -> Void in
+                self?.setTitle(title: any as? String)
+                self?.baseTitle = any as? String
+            }
+        }
+        if evaluateJavaScript != nil, (evaluateJavaScript?.count ?? 0) > 0 {
+            webView.evaluateJavaScript(evaluateJavaScript!) { _, _ in
+            }
+        }
+     
+    }
+
+    func webView(_ webView: WKWebView, didFail _: WKNavigation!, withError error: Error) {
+        PQLog(message: error)
+        if baseTitle == nil || baseTitle?.count ?? 0 <= 0 {
+            webView.evaluateJavaScript("document.title") { [weak self] (any, _) -> Void in
+                self?.setTitle(title: any as? String)
+                self?.baseTitle = any as? String
+            }
+        }
+     
+    }
+
+    func webView(_: WKWebView, didFailProvisionalNavigation _: WKNavigation!, withError _: Error) {
+   
+    }
+
+    func webView(_: WKWebView, decidePolicyFor navigationResponse: WKNavigationResponse, decisionHandler: @escaping (WKNavigationResponsePolicy) -> Void) {
+        PQLog(message: "navigationResponse:\(String(describing: navigationResponse))")
+        decisionHandler(.allow)
+    }
+
+    func webView(_: WKWebView, didStartProvisionalNavigation navigation: WKNavigation!) {
+        PQLog(message: "didStartProvisionalNavigation:\(String(describing: navigation))")
+    }
+
+    func webView(_: WKWebView, didReceiveServerRedirectForProvisionalNavigation navigation: WKNavigation!) {
+        PQLog(message: "didReceiveServerRedirectForProvisionalNavigation:\(String(describing: navigation))")
+    }
+
+    func webView(_: WKWebView, didCommit navigation: WKNavigation!) {
+        PQLog(message: "\(String(describing: navigation))")
+    }
+
+//    func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) {
+//        let url : String = navigationAction.request.url?.absoluteString ?? "";
+//        if(url.count == 0 || url == "about:blank"){
+//            decisionHandler(.cancel)
+//            return
+//        }
+//        let vc = PQBaseWebViewController.init()
+//        vc.baseUrl = url
+//        navigationController?.pushViewController(vc, animated: true)
+//        PQLog(message: "decidePolicyFor \(String(describing: navigationAction))")
+//        decisionHandler(.allow)
+//    }
+    override var preferredStatusBarStyle: UIStatusBarStyle {
+        if #available(iOS 13.0, *) {
+            return .darkContent
+        } else {
+            // Fallback on earlier versions
+            return .default
+        }
+    }
+}

+ 46 - 0
BFFramework/Classes/Base/Controller/PQNavigatinController.swift

@@ -0,0 +1,46 @@
+//
+//  PQNavigatinController.swift
+//  PQSpeed
+//
+//  Created by SanW on 2020/5/25.
+//  Copyright © 2020 BytesFlow. All rights reserved.
+//
+
+import UIKit
+
+class PQNavigatinController: UINavigationController, UIGestureRecognizerDelegate {
+    var isPop: Bool = false
+
+    override func viewDidLoad() {
+        super.viewDidLoad()
+
+        // Do any additional setup after loading the view.
+    }
+
+    override func pushViewController(_ viewController: UIViewController, animated _: Bool) {
+        if viewControllers.count > 0 {
+            viewController.hidesBottomBarWhenPushed = true
+        }
+        super.pushViewController(viewController, animated: true)
+    }
+
+    func supportedInterfaceOrientations() -> UIInterfaceOrientationMask {
+        return topViewController!.supportedInterfaceOrientations
+    }
+
+    func preferredInterfaceOrientationForPresentation() -> UIInterfaceOrientation {
+        return topViewController!.preferredInterfaceOrientationForPresentation
+    }
+
+    override var childForStatusBarStyle: UIViewController? {
+        return topViewController
+    }
+
+    override var childForStatusBarHidden: UIViewController? {
+        return topViewController
+    }
+
+    func gestureRecognizerShouldBegin(_: UIGestureRecognizer) -> Bool {
+        return viewControllers.count > 1
+    }
+}

+ 267 - 0
BFFramework/Classes/Base/Controller/PQPhotoAlbumController.swift

@@ -0,0 +1,267 @@
+//
+//  PQPhotoAlbumController.swift
+//  PQSpeed
+//
+//  Created by SanW on 2020/8/31.
+//  Copyright © 2020 BytesFlow. All rights reserved.
+//
+
+import Photos
+import UIKit
+/// 相册集
+class PQPhotoAlbumController: PQBaseViewController {
+    var selectedHandle: ((_ selectedData: PQUploadModel?) -> Void)? // 选中/取消选中的回调
+    var isTopShow : Bool = false // 是否显示在顶部
+    
+    var categoryH: CGFloat = cDefaultMargin * 26 // 相簿高度
+    var albaumsData: [PQUploadModel] = Array<PQUploadModel>.init()
+    let itemSize = CGSize(width: ((cScreenWidth - cDefaultMargin) / 3) * UIScreen.main.scale, height: ((cScreenWidth - cDefaultMargin) / 3) * UIScreen.main.scale)
+    var catagerySelectedIndex: IndexPath = IndexPath(item: 0, section: 0) // 更多图库选择
+    lazy var imageManager: PHCachingImageManager = {
+        PHCachingImageManager()
+    }()
+
+    lazy var albaumCollectionView: UICollectionView = {
+        let layout = UICollectionViewFlowLayout()
+        layout.sectionInset = UIEdgeInsets.zero
+        layout.itemSize = CGSize(width: view.frame.width, height: cDefaultMargin * 8)
+        layout.minimumLineSpacing = 0
+        layout.minimumInteritemSpacing = 0
+        let albaumCollectionView = UICollectionView(frame: CGRect(x: 0, y: isTopShow ? 0 : (albaumView.frame.height - categoryH), width: albaumView.frame.width, height: isTopShow ? 0 : categoryH), collectionViewLayout: layout)
+        albaumCollectionView.showsVerticalScrollIndicator = false
+        albaumCollectionView.register(PQAssetCategoryCell.self, forCellWithReuseIdentifier: "PQAssetCategoryCell")
+        albaumCollectionView.delegate = self
+        albaumCollectionView.dataSource = self
+        if #available(iOS 11.0, *) {
+            albaumCollectionView.contentInsetAdjustmentBehavior = .never
+        } else {
+            automaticallyAdjustsScrollViewInsets = false
+        }
+        albaumCollectionView.backgroundColor = UIColor.hexColor(hexadecimal: "#111111")
+        return albaumCollectionView
+    }()
+
+    lazy var emptyRemindView: PQEmptyRemindView = {
+        let emptyRemindView = PQEmptyRemindView(frame: albaumCollectionView.bounds)
+        emptyRemindView.isHidden = true
+        albaumCollectionView.addSubview(emptyRemindView)
+        emptyRemindView.backgroundColor = UIColor.hexColor(hexadecimal: "#242424")
+        emptyRemindView.fullRefreshBloc = { [weak self] _, _ in
+            if emptyRemindView.refreshBtn.currentTitle == "授予权限" {
+                openAppSetting()
+            } else if emptyRemindView.refreshBtn.currentTitle == "刷新" {
+                self?.albaumsData.removeAll()
+                self?.loadPhotoData()
+            }
+        }
+        emptyRemindView.remindLab.textColor = UIColor.hexColor(hexadecimal: "#999999")
+        emptyRemindView.refreshBtn.setTitle("授予权限", for: .normal)
+        let anthorEmptyData = PQEmptyModel()
+        anthorEmptyData.title = "挑选相册素材"
+        anthorEmptyData.summary = "要挑选相册素材,请先授予相册使用权限"
+        anthorEmptyData.emptyImage = "icon_authorError"
+        anthorEmptyData.isRefreshHidden = false
+        emptyRemindView.emptyData = anthorEmptyData
+        return emptyRemindView
+    }()
+
+    lazy var albaumView: UIView = {
+        let albaumView = UIView(frame: CGRect(x: 0, y: 0, width: view.frame.width, height: view.frame.height))
+        let ges = UITapGestureRecognizer(target: self, action: #selector(dismissCategoryView))
+        albaumView.addGestureRecognizer(ges)
+        return albaumView
+    }()
+
+    override func viewDidLoad() {
+        super.viewDidLoad()
+
+        // Do any additional setup after loading the view.
+        view.backgroundColor = UIColor(red: 0, green: 0, blue: 0, alpha: 0.6)
+        hiddenNavigation()
+        loadPhotoData()
+    }
+
+    override func viewWillAppear(_ animated: Bool) {
+        super.viewWillAppear(animated)
+        PHPhotoLibrary.shared().register(self)
+    }
+
+    override func viewWillDisappear(_ animated: Bool) {
+        super.viewWillDisappear(animated)
+        PHPhotoLibrary.shared().unregisterChangeObserver(self)
+    }
+}
+
+extension PQPhotoAlbumController {
+    /// 修改view frame
+    /// - Parameter frame: <#frame description#>
+    /// - Returns: <#description#>
+    func updateViewFrame(frame: CGRect) {
+        view.frame = frame
+        view.isHidden = true
+        view.addSubview(albaumView)
+        view.addSubview(albaumCollectionView)
+    }
+
+    func loadPhotoData() {
+        DispatchQueue.global().async { [weak self] in
+            let allPhotos = PHAsset.fetchAssets(with: creaFetchOptions)
+            if allPhotos.count > 0 {
+                let tempData = PQUploadModel()
+                tempData.title = "全部"
+                tempData.categoryList = allPhotos
+                self?.albaumsData.insert(tempData, at: 0)
+                self?.updateItems(indexPath: IndexPath(item: 0, section: 0))
+            }
+            DispatchQueue.main.async { [weak self] in
+                self?.emptyRemindView.isHidden = allPhotos.count > 0
+            }
+            let smartAlbums = PHAssetCollection.fetchAssetCollections(with: .smartAlbum, subtype: .albumRegular, options: nil)
+            smartAlbums.enumerateObjects { [weak self] assCollection, _, _ in
+                if assCollection.localizedTitle != "最近删除" {
+                    self?.convertCollection(collection: assCollection)
+                }
+            }
+            let userCollections = PHCollectionList.fetchTopLevelUserCollections(with: nil)
+            userCollections.enumerateObjects { [weak self] assCollection, index, point in
+                PQLog(message: "userCollections == \(assCollection),index = \(index),point = \(point)")
+                if assCollection is PHAssetCollection {
+                    if assCollection.localizedTitle != "最近删除" {
+                        self?.convertCollection(collection: assCollection as? PHAssetCollection)
+                    }
+                }
+            }
+        }
+    }
+
+    // 转化处理获取到的相簿
+    func convertCollection(collection: PHAssetCollection?) {
+        if collection == nil {
+            return
+        }
+        DispatchQueue.global().async { [weak self] in
+            var options: PHFetchOptions = creaFetchOptions
+            if collection?.localizedTitle == "最近项目" || collection?.localizedTitle == "最近添加" {
+                options = modiFetchOptions
+            }
+            let assetsFetchResult = PHAsset.fetchAssets(in: collection!, options: options)
+            if assetsFetchResult.count > 0 {
+                let tempData = PQUploadModel()
+                tempData.assetCollection = collection
+                tempData.title = collection?.localizedTitle
+                tempData.categoryList = assetsFetchResult
+                if tempData.categoryList.count > 0 {
+                    if tempData.title == "视频" {
+                        self?.albaumsData.insert(tempData, at: 1)
+                        self?.updateItems(indexPath: IndexPath(item: 1, section: 0))
+                    } else {
+                        self?.albaumsData.append(tempData)
+                        self?.updateItems(indexPath: IndexPath(item: (self?.albaumsData.count ?? 1) - 1, section: 0))
+                    }
+                }
+                PQLog(message: "assetsFetchResult = \(assetsFetchResult)")
+            }
+        }
+    }
+
+    /// 更新数据源
+    /// - Parameter indexPath: <#indexPath description#>
+    /// - Returns: <#description#>
+    func updateItems(indexPath _: IndexPath) {
+        DispatchQueue.main.async { [weak self] in
+            self?.albaumCollectionView.reloadData()
+//            UIView.performWithoutAnimation {[weak self] in
+//                self?.albaumCollectionView.performBatchUpdates({
+//                    self?.albaumCollectionView.insertItems(at: [indexPath])
+//                }) {(isSuccess) in
+//
+//                }
+//            }
+        }
+    }
+
+    @objc func dismissCategoryView() {
+        if isTopShow {
+            UIView.animate(withDuration: 0.3, animations: {
+                self.albaumCollectionView.frame = CGRect(x: 0, y: 0, width: cScreenWidth, height: 0)
+                self.albaumView.alpha = 0
+            }) { _ in
+                self.albaumView.isHidden = true
+                self.view.isHidden = true
+            }
+            
+        }else{
+            albaumCollectionView.dismissViewAnimate { [weak self] _ in
+                self?.view.isHidden = true
+            }
+        }
+    }
+
+    @objc func showCategoryView() {
+        if isTopShow {
+            view.isHidden = false
+            albaumView.isHidden = false
+            albaumView.alpha = 0
+//            view.bringSubviewToFront(albaumView)
+            UIView.animate(withDuration: 0.3, animations: {
+                self.albaumCollectionView.frame = CGRect(x: 0, y: 0, width: cScreenWidth, height: self.categoryH)
+                self.albaumView.alpha = 1
+            }) { _ in
+            }
+           albaumCollectionView.reloadData()
+        }else{
+            view.isHidden = false
+            albaumCollectionView.showViewAnimate()
+        }
+    }
+}
+
+extension PQPhotoAlbumController: UICollectionViewDelegate, UICollectionViewDataSource, UIScrollViewDelegate {
+    func collectionView(_: UICollectionView, numberOfItemsInSection _: Int) -> Int {
+        return albaumsData.count
+    }
+
+    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
+        let cell = PQAssetCategoryCell.assetCategoryCell(collectionView: collectionView, indexPath: indexPath)
+        let itemData = albaumsData[indexPath.item]
+        let asset = itemData.categoryList.object(at: 0)
+        cell.representedAssetIdentifier = asset.localIdentifier
+        if itemData.image == nil {
+            imageManager.requestImage(for: asset, targetSize: itemSize, contentMode: .aspectFill, options: nil) { image, info in
+                if info?.keys.contains("PHImageResultIsDegradedKey") ?? false, "\(info?["PHImageResultIsDegradedKey"] ?? "0")" == "0", cell.representedAssetIdentifier == asset.localIdentifier {
+                    itemData.image = image
+                    cell.uploadData = itemData
+                }
+            }
+        } else {
+            cell.uploadData = itemData
+        }
+        cell.isSelected = indexPath == catagerySelectedIndex
+        return cell
+    }
+
+    func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
+        if selectedHandle != nil {
+            selectedHandle!(albaumsData[indexPath.item])
+        }
+        catagerySelectedIndex = indexPath
+        collectionView.reloadData()
+        dismissCategoryView()
+    }
+
+    func scrollViewDidScroll(_ scrollView: UIScrollView) {
+        if (!isTopShow && (scrollView.contentOffset.y < -cDefaultMargin * 7)) || (isTopShow && scrollView.contentOffset.y > ((scrollView.contentSize.height - scrollView.frame.height) + cDefaultMargin * 10)) {
+            dismissCategoryView()
+        }
+    }
+}
+
+extension PQPhotoAlbumController: PHPhotoLibraryChangeObserver {
+    func photoLibraryDidChange(_: PHChange) {
+        DispatchQueue.main.sync {
+            // Check each of the three top-level fetches for changes.
+            albaumsData.removeAll()
+            loadPhotoData()
+        }
+    }
+}

+ 558 - 0
BFFramework/Classes/Base/Controller/PQPhotoMaterialController.swift

@@ -0,0 +1,558 @@
+//
+//  PQPhotoMaterialController.swift
+//  PQSpeed
+//
+//  Created by SanW on 2021/4/26.
+//  Copyright © 2021 BytesFlow. All rights reserved.
+//
+
+import Photos
+import UIKit
+
+class PQPhotoMaterialController: PQBaseViewController {
+    // 是否显示素材标识
+    var isShowMediaTag: Bool = true
+    // 是否是已经添加过的素材
+    var isAdded: Bool = false
+    // 默认图片时长
+    var imageDuration: Float64 = 2.0
+    // 已选图片时长
+    var selectedImageDataCount: Int = 0
+    var itemSpacing: CGFloat = cDefaultMargin / 2 // 间隔
+    var itemSize = CGSize(width: (cScreenWidth - cDefaultMargin) / 3, height: (cScreenWidth - cDefaultMargin) / 3) // cell 大小
+    var previousPreheatRect = CGRect.zero
+
+    var allPhotos: PHFetchResult<PHAsset> = PHFetchResult<PHAsset>.init() // 所有的图片
+    var photoData: [PQEditVisionTrackMaterialsModel] = Array<PQEditVisionTrackMaterialsModel>.init() // 相册数据
+    // 选中的相册数据
+    var selectedPhotoData: [PQEditVisionTrackMaterialsModel] = Array<PQEditVisionTrackMaterialsModel>.init()
+    // 选中的总时长
+    var selectedTotalDuration: Float64 = 0
+
+    var selectedMaterialHandle: ((_ currentMaterialData: PQEditVisionTrackMaterialsModel?, _ selectedPhotoData: [PQEditVisionTrackMaterialsModel], _ selectedTotalDuration: Float64, _ imageCount: Int) -> Void)? // 选中/取消选中的回调
+    var detailMaterialHandle: ((_ indexPath: IndexPath, _ currentMaterialData: PQEditVisionTrackMaterialsModel?) -> Void)? // 点击详情
+    /// 滑动的回调
+    var scrollViewDidScroll: ((_ contentOffset: CGPoint) -> Void)?
+    /// 空白页面点击的回调
+    var emptyRefreshHandle: ((_ msgType: material_msgType) -> Void)?
+    var msgType: material_msgType = .all { // 默认类型
+        didSet {
+            photoData.removeAll()
+            allPhotos = PHFetchResult<PHAsset>.init()
+            photoCollectionView.setContentOffset(CGPoint.zero, animated: false)
+            photoCollectionView.reloadData()
+            loadLocalData()
+        }
+    }
+
+    var assetCollection: PHAssetCollection? // 相簿
+    lazy var imageManager: PHCachingImageManager = { // 图库缓存管理
+        (PHCachingImageManager.default() as? PHCachingImageManager) ?? PHCachingImageManager()
+    }()
+
+    lazy var photoFlowLayout: UICollectionViewFlowLayout = {
+        let photoFlowLayout = UICollectionViewFlowLayout()
+        photoFlowLayout.sectionInset = UIEdgeInsets.zero
+        photoFlowLayout.minimumLineSpacing = itemSpacing
+        photoFlowLayout.minimumInteritemSpacing = itemSpacing
+        photoFlowLayout.scrollDirection = .vertical
+        photoFlowLayout.itemSize = itemSize
+        return photoFlowLayout
+    }()
+
+    lazy var photoCollectionView: UICollectionView = {
+        let photoCollectionView = UICollectionView(frame: CGRect(x: itemSpacing, y: 0, width: view.frame.width, height: view.frame.height), collectionViewLayout: photoFlowLayout)
+        photoCollectionView.register(PQChoseMaterialCell.self, forCellWithReuseIdentifier: String(describing: PQChoseMaterialCell.self))
+        photoCollectionView.showsVerticalScrollIndicator = false
+        photoCollectionView.showsHorizontalScrollIndicator = false
+        photoCollectionView.delegate = self
+        photoCollectionView.dataSource = self
+        photoCollectionView.backgroundColor = UIColor.clear
+        if #available(iOS 11.0, *) {
+            photoCollectionView.contentInsetAdjustmentBehavior = .never
+        } else {
+            automaticallyAdjustsScrollViewInsets = false
+        }
+        return photoCollectionView
+    }()
+
+    var anthorEmptyData: PQEmptyModel = {
+        let anthorEmptyData = PQEmptyModel()
+        anthorEmptyData.title = "挑选相册素材"
+        anthorEmptyData.summary = "要挑选相册素材,请先授予相册使用权限"
+        anthorEmptyData.emptyImage = "icon_authorError"
+        return anthorEmptyData
+    }()
+
+    var emptyData: PQEmptyModel = {
+        let emptyData = PQEmptyModel()
+        emptyData.title = "此相册中什么都没有"
+        emptyData.isRefreshHidden = false
+        emptyData.refreshTitle = NSMutableAttributedString(string: "刷新")
+        emptyData.emptyImage = "material_empty"
+        return emptyData
+    }()
+
+    lazy var emptyRemindView: PQEmptyRemindView = {
+        let emptyRemindView = PQEmptyRemindView(frame: CGRect(x: 0, y: 0, width: view.frame.width, height: view.frame.height))
+        emptyRemindView.isHidden = true
+        photoCollectionView.addSubview(emptyRemindView)
+        emptyRemindView.backgroundColor = UIColor.hexColor(hexadecimal: "#242424")
+        emptyRemindView.fullRefreshBloc = { [weak self] _, _ in
+            if emptyRemindView.refreshBtn.currentTitle == "授予权限" {
+                openAppSetting()
+            } else if emptyRemindView.refreshBtn.currentAttributedTitle == NSMutableAttributedString(string: "刷新") {
+                self?.loadLocalData()
+            } else if self?.emptyRefreshHandle != nil {
+                self?.emptyRefreshHandle!(self?.msgType ?? .all)
+            }
+        }
+        emptyRemindView.remindLab.textColor = UIColor.hexColor(hexadecimal: "#999999")
+        emptyRemindView.refreshBtn.addCorner(corner: 4)
+        return emptyRemindView
+    }()
+
+    override func viewDidLoad() {
+        super.viewDidLoad()
+        view.addSubview(photoCollectionView)
+    }
+
+    /// <#Description#>
+    /// - Returns: <#description#>
+    func loadLocalData() {
+        let authStatus = PHPhotoLibrary.authorizationStatus()
+        if authStatus == .notDetermined {
+            // 第一次触发授权 alert
+            PHPhotoLibrary.requestAuthorization { [weak self] (status: PHAuthorizationStatus) -> Void in
+                if status == .authorized {
+                    if (self?.allPhotos == nil) || (self?.allPhotos.count ?? 0) <= 0 {
+                        self?.loadPhotoData()
+                    }
+                }
+                DispatchQueue.main.async { [weak self] in
+                    self?.showEmpthView()
+                }
+            }
+        } else if authStatus == .authorized {
+            PQLog(message: "授权成功,开始请求相册数据-\(allPhotos)")
+//            if allPhotos.count <= 0 {
+            loadPhotoData()
+//            }
+        } else {
+            showEmpthView()
+        }
+        PHPhotoLibrary.shared().register(self)
+    }
+
+    /// 加载图库数据
+    /// - Returns: <#description#>
+    func loadPhotoData() {
+        DispatchQueue.main.async { [weak self] in
+            PQLoadingHUB.shared.showHUB(superView: self!.view, isVerticality: false)
+        }
+        DispatchQueue.global().async { [weak self] in
+            self?.allPhotos = self?.assetCollection != nil ? PHAsset.fetchAssets(in: (self?.assetCollection)!, options: self?.fetchOptions()) : PHAsset.fetchAssets(with: self?.fetchOptions())
+            DispatchQueue.main.async { [weak self] in
+                if self?.view != nil {
+                    PQLoadingHUB.shared.dismissHUB(superView: self!.view)
+                }
+                self?.photoCollectionView.reloadData()
+                self?.showEmpthView()
+//                self?.updateCachedAssets()
+            }
+        }
+    }
+
+    /// 获取option
+    /// - Returns: <#description#>
+    func fetchOptions() -> PHFetchOptions {
+        if msgType != .all {
+            let fetchOptions = PHFetchOptions()
+            fetchOptions.sortDescriptors = [NSSortDescriptor(key: "creationDate", ascending: false)]
+            fetchOptions.predicate = NSPredicate(format: "mediaType = %d", msgType == .video ? PHAssetMediaType.video.rawValue : PHAssetMediaType.image.rawValue)
+            return fetchOptions
+        } else {
+            return creaFetchOptions
+        }
+    }
+
+    /// 展示空页面
+    func showEmpthView() {
+        emptyRemindView.isHidden = allPhotos.count > 0
+        if !emptyRemindView.isHidden {
+            let authStatus = PHPhotoLibrary.authorizationStatus()
+            if authStatus == .denied {
+                emptyRemindView.emptyData = anthorEmptyData
+                emptyRemindView.refreshBtn.setTitle("授予权限", for: .normal)
+            } else {
+                switch msgType {
+                case .video:
+                    emptyData.title = "此相册中没有视频"
+                    emptyData.emptyImage = "stuckPoint_video_empty"
+                    emptyData.refreshTitle = NSMutableAttributedString(string: "去选照片")
+                case .image:
+                    emptyData.title = "此相册中没有照片"
+                    emptyData.emptyImage = "stuckPoint_image_empty"
+                    emptyData.refreshTitle = NSMutableAttributedString(string: "去选视频")
+                default:
+                    emptyData.title = "此相册中什么都没有"
+                    emptyData.emptyImage = "material_empty"
+                    emptyData.refreshTitle = NSMutableAttributedString(string: "刷新")
+                }
+                emptyData.summary = nil
+                emptyRemindView.emptyData = emptyData
+            }
+        }
+    }
+
+    /// 更新frame
+    /// - Parameter newFrame: <#newFrame description#>
+    /// - Returns: <#description#>
+    func updateFrame(newFrame: CGRect, isAnimate: Bool = false, isScroll _: Bool = false) {
+        if isAnimate {
+            UIView.animate(withDuration: 0.5, delay: 0, options: .allowUserInteraction) { [weak self] in
+                // 调整位置
+                self?.view.frame = newFrame
+                self?.photoCollectionView.frame = self?.view.bounds ?? CGRect.zero
+                self?.emptyRemindView.frame = self?.photoCollectionView.bounds ?? CGRect.zero
+            } completion: { _ in
+            }
+        } else {
+            view.frame = newFrame
+            photoCollectionView.frame = view.bounds
+            emptyRemindView.frame = photoCollectionView.bounds
+        }
+    }
+}
+
+extension PQPhotoMaterialController: UICollectionViewDelegate, UICollectionViewDataSource, UIScrollViewDelegate {
+    func collectionView(_: UICollectionView, numberOfItemsInSection _: Int) -> Int {
+        return allPhotos.count
+    }
+
+    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
+        let cell = PQChoseMaterialCell.choseMaterialCell(collectionView: collectionView, indexPath: indexPath)
+        cell.isShowMediaTag = isShowMediaTag
+        cell.isAdded = isAdded
+        if photoData.count <= indexPath.item {
+            let itemData: PQEditVisionTrackMaterialsModel!
+            let asset = allPhotos.object(at: indexPath.item)
+            let selectedItem = selectedPhotoData.first { (item) -> Bool in
+                item.asset == asset
+            }
+            if selectedItem != nil {
+                itemData = selectedItem
+            } else {
+                itemData = PQEditVisionTrackMaterialsModel()
+                itemData.downloadState = .compelte
+                itemData.asset = asset
+                itemData.height = Float(itemData.asset?.pixelHeight ?? 0)
+                itemData.width = Float(itemData.asset?.pixelWidth ?? 0)
+                itemData.type = asset.mediaType == .image ? StickerType.IMAGE.rawValue : StickerType.VIDEO.rawValue
+                itemData.volumeGain = asset.mediaType == .image ? 0 : 100
+                itemData.duration = asset.mediaType == .image ? imageDuration : asset.duration
+                itemData.out = itemData.duration
+            }
+            photoData.append(itemData)
+        }
+        if photoData.count > indexPath.item {
+            let itemData = photoData[indexPath.item]
+            cell.materialData = itemData
+            cell.representedAssetIdentifier = itemData.asset?.localIdentifier
+            if itemData.coverImageUI == nil, itemData.asset != nil {
+                imageManager.requestImage(for: itemData.asset!, targetSize: itemSize, contentMode: .aspectFill, options: nil) { image, info in
+                    if info?.keys.contains("PHImageResultIsDegradedKey") ?? false, "\(info?["PHImageResultIsDegradedKey"] ?? "0")" == "0", cell.representedAssetIdentifier == itemData.asset?.localIdentifier {
+                        if image != nil {
+                            itemData.coverImageUI = image
+                            cell.materialImageView.image = image
+                        } else if image == nil, info?.keys.contains("PHImageResultIsInCloudKey") ?? false {
+                            let option = PHImageRequestOptions()
+                            option.isNetworkAccessAllowed = true
+                            option.resizeMode = .fast
+                            self.imageManager.requestImageData(for: itemData.asset!, options: option) { data, _, _, _ in
+                                if data != nil {
+                                    let image = UIImage(data: data!)
+                                    itemData.coverImageUI = image
+                                    cell.materialImageView.image = image
+                                }
+                            }
+                        }
+                    }
+                }
+            }
+        } else {
+            cell.materialData = PQEditVisionTrackMaterialsModel()
+        }
+        cell.materialClicHandle = { [weak self] _, materialData in
+            if (self?.photoData.count ?? 0) > indexPath.item {
+                // 处理相册选择
+                self?.dealWithSelectedMaterial(indexPath: indexPath, materialData: materialData)
+            } else {
+//                collectionView.reloadData()
+            }
+        }
+        return cell
+    }
+
+    func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
+        if photoData.count <= indexPath.item {
+//            collectionView.reloadData()
+            return
+        }
+        if detailMaterialHandle != nil {
+            detailMaterialHandle!(indexPath, photoData[indexPath.item])
+        }
+    }
+    func collectionView(_ collectionView: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {
+        if !isAdded && !isShowMediaTag {
+//            // 卡点音乐素材选择曝光上报
+//            PQEventTrackViewModel.baseReportUpload(businessType: .bt_windowView, objectType: .ot_view_selectSyncedUpMaterial, pageSource: .sp_stuck_selectMaterial, extParams: nil, remindmsg: "卡点音乐素材选择曝光上报")
+        }
+    }
+    /// 点击选择的回调
+    /// - Parameter materialData: <#materialData description#>
+    /// - Returns: description
+    func dealWithSelectedMaterial(indexPath: IndexPath?, materialData: PQEditVisionTrackMaterialsModel?) {
+        let ratio = (materialData?.width ?? 0) / (materialData?.height ?? 1)
+        if ratio < 0.4 || ratio > 4.2 {
+            cShowHUB(superView: nil, msg: "暂不支持该比例的素材")
+            return
+        }
+        if materialData != nil, indexPath != nil {
+            materialData?.isSelected = !(materialData?.isSelected ?? false)
+            if materialData?.isSelected ?? false {
+                selectedPhotoData.append(materialData!)
+                if materialData?.type == StickerType.IMAGE.rawValue {
+                    selectedImageDataCount = selectedImageDataCount + 1
+                }
+                materialData?.selectedIndex = selectedPhotoData.count
+                if materialData?.asset != nil && materialData?.asset?.mediaType != .video && (materialData?.coverImageUI == nil || (materialData?.locationPath.count ?? 0) <= 0) {
+                    PQPHAssetVideoParaseUtil.requestAssetOringinImage(asset: (materialData?.asset)!) { [weak self] _, data, image, _ in
+                        if image == nil {
+                            PQLog(message: "图片数据为空!!!!!")
+                        }
+                        // 1,图片,gif的原文件数据,和视频的封面
+                        var newImage: UIImage?
+                        if image != nil {
+                            newImage = UIImage.nx_fixOrientation(image, isFront: false).nx_scaleWithMaxLength(maxLength: 1920)
+                        }
+                        materialData?.coverImageUI = newImage
+                        materialData?.originalData = data
+                        let timeInterval: TimeInterval = Date().timeIntervalSince1970
+                        let imageCacheName = "images_\(timeInterval)"
+                        let imageCachePath = downloadImagesDirectory + imageCacheName
+                        if !directoryIsExists(dicPath: downloadImagesDirectory) {
+                            PQLog(message: "文件夹不存在 \(downloadImagesDirectory)")
+                            createDirectory(path: downloadImagesDirectory)
+                        }
+                        // 创建目录
+                        if newImage != nil {
+                            try? newImage?.pngData()?.write(to: URL(fileURLWithPath: imageCachePath))
+                        } else {
+                            try? data?.write(to: URL(fileURLWithPath: imageCachePath))
+                        }
+                        if materialData?.asset?.mediaType != .video, newImage != nil || data != nil {
+                            materialData?.locationPath = imageCachePath.replacingOccurrences(of: documensDirectory, with: "")
+                        }
+                        materialData?.width = Float(materialData?.asset?.pixelWidth ?? 0)
+                        materialData?.height = Float(materialData?.asset?.pixelHeight ?? 0)
+                        if self?.selectedMaterialHandle != nil {
+                            self?.selectedTotalDuration = (self?.selectedTotalDuration ?? 0) + (materialData?.duration ?? 0.0)
+                            if indexPath != nil {
+                                self?.photoCollectionView.reloadItems(at: [indexPath!])
+                            }
+                            // 处理已选择的数据
+                            self?.dealWithSelectedData(materialData: materialData)
+                        }
+                    }
+                } else {
+                    selectedTotalDuration = selectedTotalDuration + (materialData?.duration ?? 0.0)
+                    if indexPath != nil {
+                        photoCollectionView.reloadItems(at: [indexPath!])
+                    }
+                    // 处理已选择的数据
+                    dealWithSelectedData(materialData: materialData)
+                }
+            } else {
+                // 处理取消选择数据
+                deSeletedMaterialData(materialData: materialData)
+            }
+        }
+    }
+
+    /// 处理取消选择数据
+    /// - Parameter materialData: <#materialData description#>
+    /// - Returns: <#description#>
+    func deSeletedMaterialData(materialData: PQEditVisionTrackMaterialsModel?) {
+        selectedPhotoData.removeAll(where: { (item) -> Bool in
+            item.asset == materialData?.asset
+        })
+        for item in photoData {
+            if item.isSelected, item.selectedIndex > (materialData?.selectedIndex ?? 1) {
+                item.selectedIndex = item.selectedIndex - 1
+            }
+        }
+        if materialData?.type == StickerType.IMAGE.rawValue {
+            selectedImageDataCount = selectedImageDataCount - 1
+        }
+        selectedTotalDuration = selectedTotalDuration - (materialData?.duration ?? 0.0)
+        photoCollectionView.reloadData()
+        // 处理已选择的数据
+        dealWithSelectedData(materialData: materialData)
+    }
+
+    /// 处理已选择的数据
+    /// - Parameters:
+    ///   - indexPath: <#indexPath description#>
+    ///   - materialData: <#materialData description#>
+    /// - Returns: <#description#>
+    func dealWithSelectedData(materialData: PQEditVisionTrackMaterialsModel?) {
+//        if materialData?.asset != nil && materialData?.asset?.mediaType == .video && (materialData?.locationPath == nil || (materialData?.locationPath.count ?? 0) <= 0) {
+//            PQPHAssetVideoParaseUtil.parasToAVAsset(phAsset: (materialData?.asset)!) { [weak self] avAsset, _, _, _ in
+//                if avAsset is AVURLAsset {
+//                    materialData?.locationPath = (avAsset as? AVURLAsset)?.url.absoluteString.replacingOccurrences(of: "file:///", with: "") ?? ""
+//                    if self?.selectedMaterialHandle != nil {
+//                        DispatchQueue.main.async {
+//                            self?.selectedMaterialHandle!(materialData, self?.selectedPhotoData ?? [], self?.selectedTotalDuration ?? 0)
+//                        }
+//                    }
+//                } else if self?.selectedMaterialHandle != nil {
+//                    DispatchQueue.main.async {
+//                        self?.selectedMaterialHandle!(materialData, self?.selectedPhotoData ?? [], self?.selectedTotalDuration ?? 0)
+//                    }
+//                }
+//            }
+//        } else
+        if selectedMaterialHandle != nil {
+            selectedMaterialHandle!(materialData, selectedPhotoData, selectedTotalDuration, selectedImageDataCount)
+        }
+    }
+
+    /// <#Description#>
+    /// - Parameter scrollView: <#scrollView description#>
+    func scrollViewDidScroll(_ scrollView: UIScrollView) {
+        if scrollViewDidScroll != nil {
+            scrollViewDidScroll!(scrollView.contentOffset)
+        }
+    }
+
+    /// 更新数据
+    /// - Parameter isMaterialSelected:
+    /// - Parameter materialData: <#materialData description#>
+    /// - Returns: <#description#>
+    func updateMaterials(isSelected: Bool, materialData: PQEditVisionTrackMaterialsModel?) {
+        let photoIndexPath = selectedPhotoData.firstIndex { (item) -> Bool in
+            materialData?.asset == item.asset
+        }
+        materialData?.isSelected = isSelected
+        if isSelected {
+            if photoIndexPath == nil {
+                selectedPhotoData.append(materialData!)
+            }
+            materialData?.selectedIndex = selectedPhotoData.count
+            selectedTotalDuration = selectedTotalDuration + (materialData?.duration ?? 0.0)
+        } else if photoIndexPath != nil {
+            selectedPhotoData.remove(at: photoIndexPath ?? 0)
+            for item in selectedPhotoData {
+                if item.isSelected, item.selectedIndex > (materialData?.selectedIndex ?? 1) {
+                    item.selectedIndex = item.selectedIndex - 1
+                }
+            }
+            selectedTotalDuration = selectedTotalDuration - (materialData?.duration ?? 0.0)
+        }
+        photoCollectionView.reloadData()
+        if selectedMaterialHandle != nil {
+            selectedMaterialHandle!(materialData, selectedPhotoData, selectedTotalDuration, selectedImageDataCount)
+        }
+    }
+}
+
+extension PQPhotoMaterialController: PHPhotoLibraryChangeObserver {
+    func photoLibraryDidChange(_ changeInstance: PHChange) {
+        DispatchQueue.main.sync {
+            // Check each of the three top-level fetches for changes.
+            guard let collectionChanges = changeInstance.changeDetails(for: allPhotos)
+            else {
+                return
+            }
+            if !collectionChanges.hasIncrementalChanges || collectionChanges.hasMoves {
+                return
+            }
+            // Update the cached fetch result.
+            allPhotos = collectionChanges.fetchResultAfterChanges
+            photoData.removeAll()
+            photoCollectionView.reloadData()
+//            photoCollectionView.setContentOffset(CGPoint.zero, animated: false)
+        }
+    }
+
+    private func resetCachedAssets() {
+        imageManager.stopCachingImagesForAllAssets()
+        previousPreheatRect = .zero
+    }
+
+    private func updateCachedAssets() {
+        if allPhotos.count <= 0 {
+            return
+        }
+        guard isViewLoaded, view.window != nil else { return }
+        let visibleRect = CGRect(origin: photoCollectionView.contentOffset, size: photoCollectionView.bounds.size)
+        let preheatRect = visibleRect.insetBy(dx: 0, dy: -0.5 * visibleRect.height)
+        let delta = abs(preheatRect.midY - previousPreheatRect.midY)
+        guard delta > view.bounds.height / 3 else { return }
+        let (addedRects, removedRects) = differencesBetweenRects(previousPreheatRect, preheatRect)
+        let addedAssets: [PHAsset]? = addedRects
+            .flatMap { rect in photoCollectionView.indexPathsForElements(in: rect) }
+            .map { indexPath in
+                if indexPath.item < allPhotos.count {
+                    return allPhotos.object(at: indexPath.item)
+                } else {
+                    return PHAsset()
+                }
+            }
+        let removedAssets: [PHAsset]? = removedRects
+            .flatMap { rect in photoCollectionView.indexPathsForElements(in: rect) }
+            .map { indexPath in
+                if indexPath.item < allPhotos.count {
+                    return allPhotos.object(at: indexPath.item)
+                } else {
+                    return PHAsset()
+                }
+            }
+        if addedAssets != nil, (addedAssets?.count ?? 0) > 0 {
+            imageManager.startCachingImages(for: addedAssets!,
+                                            targetSize: itemSize, contentMode: .aspectFill, options: nil)
+        }
+        if removedAssets != nil, (removedAssets?.count ?? 0) > 0 {
+            imageManager.stopCachingImages(for: removedAssets!,
+                                           targetSize: itemSize, contentMode: .aspectFill, options: nil)
+        }
+        previousPreheatRect = preheatRect
+    }
+
+    private func differencesBetweenRects(_ old: CGRect, _ new: CGRect) -> (added: [CGRect], removed: [CGRect]) {
+        if old.intersects(new) {
+            var added = [CGRect]()
+            if new.maxY > old.maxY {
+                added += [CGRect(x: new.origin.x, y: old.maxY,
+                                 width: new.width, height: new.maxY - old.maxY)]
+            }
+            if old.minY > new.minY {
+                added += [CGRect(x: new.origin.x, y: new.minY,
+                                 width: new.width, height: old.minY - new.minY)]
+            }
+            var removed = [CGRect]()
+            if new.maxY < old.maxY {
+                removed += [CGRect(x: new.origin.x, y: new.maxY,
+                                   width: new.width, height: old.maxY - new.maxY)]
+            }
+            if old.minY < new.minY {
+                removed += [CGRect(x: new.origin.x, y: old.minY,
+                                   width: new.width, height: new.minY - old.minY)]
+            }
+            return (added, removed)
+        } else {
+            return ([new], [old])
+        }
+    }
+}

+ 189 - 0
BFFramework/Classes/Base/Model/PQBaseModel.swift

@@ -0,0 +1,189 @@
+//
+//  PQBaseModel.swift
+//  PQSpeed
+//
+//  Created by SanW on 2020/5/25.
+//  Copyright © 2020 BytesFlow. All rights reserved.
+//
+
+import RealmSwift
+import UIKit
+
+class PQBaseModel: Object {
+    @objc dynamic var uniqueId: String? // 唯一ID
+    @objc dynamic var videoId: Int = 0 // 视频ID
+    @objc dynamic var eventId: String? // 事件ID
+    @objc dynamic var title: String? // 标题
+    @objc dynamic var attributedTitle: NSMutableAttributedString? // 富文本标题
+    @objc dynamic var summary: String? // 描述
+    @objc dynamic var imageUrl: String = "" // 图片地址
+    @objc dynamic var selectedImage: String = "" // 图片地址
+    @objc dynamic var isSelected: Bool = false
+    @objc dynamic var recommendLogVO: String? // 推荐日志对象
+    @objc dynamic var abInfoData: String? // AB
+    @objc dynamic var pageCategoryId: Int = 0 // 页面分类ID
+    @objc dynamic var version: String = versionName // 版本号
+    @objc dynamic var mid = getMachineCode() // 设备ID
+    @objc dynamic var date: Int = 0 // 当前时间戳  CGFloat(Date.init().timeIntervalSince1970) * 1000
+    @objc dynamic var itemWidth: Float = 0 // cell宽
+    @objc dynamic var primaryKeys: String? // 区分存储唯一值
+    override class func primaryKey() -> String? {
+        return "uniqueId"
+    }
+
+    override required init() {
+        super.init()
+        uniqueId = getUniqueId(desc: "uniqueId")
+    }
+
+    override class func ignoredProperties() -> [String] {
+        return ["attributedTitle"]
+    }
+
+    @objc func toString() -> String {
+        var json: [String: Any] = [
+            "version": version,
+            "mid": mid,
+            "pageCategoryId": pageCategoryId,
+            "selectedImage": selectedImage,
+            "isSelected": isSelected,
+            "imageUrl": imageUrl,
+        ]
+        if uniqueId != nil {
+            json["uniqueId"] = uniqueId
+            json["videoId"] = videoId
+        }
+        if eventId != nil {
+            json["eventId"] = eventId
+        }
+        if title != nil {
+            json["title"] = title
+        }
+        if summary != nil {
+            json["summary"] = summary
+        }
+        if recommendLogVO != nil {
+            json["recommendLogVO"] = recommendLogVO
+        }
+        return dictionaryToJsonString(json) ?? ""
+    }
+
+    init(jsonDict: [String: Any]) {
+        super.init()
+        if jsonDict.keys.contains("id") {
+            uniqueId = "\(jsonDict["id"] ?? "")"
+            videoId = Int(uniqueId ?? "0") ?? 0
+        }
+        if jsonDict.keys.contains("uniqueId") {
+            uniqueId = "\(jsonDict["uniqueId"] ?? "")"
+            videoId = Int(uniqueId ?? "0") ?? 0
+        }
+        if jsonDict.keys.contains("eventId") {
+            eventId = "\(jsonDict["eventId"] ?? "")"
+        }
+        if jsonDict.keys.contains("title") {
+            title = "\(jsonDict["title"] ?? "")"
+        }
+        if jsonDict.keys.contains("summary") {
+            summary = "\(jsonDict["summary"] ?? "")"
+        }
+        if jsonDict.keys.contains("imageUrl") {
+            imageUrl = "\(jsonDict["imageUrl"] ?? "")"
+        }
+        if jsonDict.keys.contains("selectedImage") {
+            selectedImage = "\(jsonDict["selectedImage"] ?? "")"
+        }
+        if jsonDict.keys.contains("isSelected") {
+            isSelected = jsonDict["isSelected"] as! Bool
+        }
+        if jsonDict.keys.contains("recommendLogVO") {
+            recommendLogVO = "\(jsonDict["recommendLogVO"] ?? "")"
+        }
+        if jsonDict.keys.contains("pageCategoryId") {
+            pageCategoryId = Int("\(jsonDict["pageCategoryId"] ?? "0")") ?? 0
+        }
+        if jsonDict.keys.contains("version") {
+            version = "\(jsonDict["version"] ?? "")"
+        }
+        if jsonDict.keys.contains("mid") {
+            mid = "\(jsonDict["mid"] ?? "")"
+        }
+    }
+}
+
+// MARK: - 当前应用本地存储的model
+
+/// 当前应用本地存储的model
+class PQLocalStoreModel: PQBaseModel {
+    @objc dynamic var currentDate: String?
+    @objc required init() {
+        super.init()
+        currentDate = systemCurrentDate()
+    }
+}
+
+// MARK: - oss上传model
+
+/// oss上传model
+class PQOssUploadModel: NSObject {
+    var accessKeyId: String?
+    var secretKeyId: String?
+    var securityToken: String?
+    var endpoint: String?
+    var endpoints: [String]?
+    var bucketName: String?
+    var fileName: String?
+    var uploadID: String?
+    var expiration: String? // 过期时间
+    init(jsonDict: [String: Any]) {
+        super.init()
+
+        if jsonDict.keys.contains("AccessKeyId") {
+            accessKeyId = "\(jsonDict["AccessKeyId"] ?? "")"
+        }
+        if jsonDict.keys.contains("AccessKeySecret") {
+            secretKeyId = "\(jsonDict["AccessKeySecret"] ?? "")"
+        }
+        if jsonDict.keys.contains("SecurityToken") {
+            securityToken = "\(jsonDict["SecurityToken"] ?? "")"
+        }
+        if jsonDict.keys.contains("Hosts") {
+            endpoints = jsonDict["Hosts"] as? [String]
+        }
+        if jsonDict.keys.contains("Host") {
+            endpoint = "\(jsonDict["Host"] ?? "")"
+            if endpoint != nil {
+                if endpoints == nil {
+                    endpoints = [endpoint!]
+                } else {
+                    endpoints?.append(endpoint!)
+                }
+            }
+        }
+        if jsonDict.keys.contains("Bucket") {
+            bucketName = "\(jsonDict["Bucket"] ?? "")"
+        }
+        if jsonDict.keys.contains("FileName") {
+            fileName = "\(jsonDict["FileName"] ?? "")"
+        }
+        if jsonDict.keys.contains("Upload") {
+            uploadID = "\(jsonDict["Upload"] ?? "")"
+        }
+        if jsonDict.keys.contains("Expiration") {
+            expiration = "\(jsonDict["Expiration"] ?? "")"
+        }
+    }
+}
+
+// MARK: - 空白页面model
+
+/// 空白页面model
+class PQEmptyModel: NSObject {
+    var title: String? // 标题
+    var summary: String? // 描述
+    var emptyImage: String? // 空白提示图
+    var isRefreshHidden: Bool = true // 是否隐藏刷新按钮
+    var refreshImage: String? // 刷新按钮图片
+    var refreshTitle: NSMutableAttributedString? // 刷新按钮文字
+    var refreshBgColor: UIColor? // 刷新按钮背景颜色
+}

+ 48 - 0
BFFramework/Classes/Base/View/PQActivityIndicatorView.swift

@@ -0,0 +1,48 @@
+//
+//  PQActivityIndicatorView.swift
+//  PQSpeed
+//
+//  Created by ak on 2020/9/23.
+//  Copyright © 2020 BytesFlow. All rights reserved.
+//  功能: 显示 UIActivityIndicatorView
+
+import UIKit
+
+class PQActivityIndicatorView: UIView {
+    func showHud(isCovered: Bool = false) {
+        hideHud()
+        let frame = CGRect(x: 0, y: 0, width: 78, height: 78)
+
+        let backVFrame = isCovered == false ? frame : CGRect(x: 0, y: 0, width: cScreenWidth, height: cScreenHeigth)
+        self.frame = backVFrame
+        let backV = UIView(frame: backVFrame)
+        backV.center = center
+        backV.tag = 8421
+        addSubview(backV)
+        backgroundColor = UIColor.clear
+
+        let hudV = UIView(frame: frame)
+        hudV.center = CGPoint(x: backV.frame.width / 2, y: backV.frame.height / 2)
+        hudV.layer.cornerRadius = 12
+        hudV.backgroundColor = UIColor(red: 0, green: 0, blue: 0, alpha: 0.8)
+//        hudV.backgroundColor = .green
+        backV.addSubview(hudV)
+
+        let indicatorView = UIActivityIndicatorView(style: .whiteLarge)
+        indicatorView.frame = CGRect(x: 21, y: 21, width: 36, height: 36)
+        indicatorView.startAnimating()
+        hudV.addSubview(indicatorView)
+
+        hudV.alpha = 0.0
+        UIView.animate(withDuration: 0.2, animations: {
+            hudV.alpha = 1
+        })
+    }
+
+    func hideHud() {
+        let backV = viewWithTag(8421)
+        guard let backv = backV else { return }
+        backv.removeFromSuperview()
+        removeFromSuperview()
+    }
+}

+ 103 - 0
BFFramework/Classes/Base/View/PQAssetCategoryCell.swift

@@ -0,0 +1,103 @@
+//
+//  PQAssetCategoryCell.swift
+//  PQSpeed
+//
+//  Created by SanW on 2020/8/4.
+//  Copyright © 2020 BytesFlow. All rights reserved.
+//
+
+import UIKit
+
+class PQAssetCategoryCell: UICollectionViewCell {
+    var representedAssetIdentifier: String!
+    lazy var videoImageView: UIImageView = {
+        let videoImageView = UIImageView()
+        videoImageView.addCorner(corner: 4)
+        videoImageView.contentMode = .scaleAspectFill
+        return videoImageView
+    }()
+
+    lazy var categoryNameLab: UILabel = {
+        let categoryNameLab = UILabel()
+        categoryNameLab.font = UIFont.systemFont(ofSize: 14)
+        categoryNameLab.textColor = UIColor.white
+        return categoryNameLab
+    }()
+
+    lazy var countLab: UILabel = {
+        let countLab = UILabel()
+        countLab.font = UIFont.systemFont(ofSize: 14)
+        countLab.textColor = UIColor.white
+        return countLab
+    }()
+
+    lazy var seleImage: UIImageView = {
+        let seleImage = UIImageView(image: UIImage(named: "icon_uploadVideo_do"))
+        seleImage.isHidden = true
+        return seleImage
+    }()
+
+    @objc class func assetCategoryCell(collectionView: UICollectionView, indexPath: IndexPath) -> PQAssetCategoryCell {
+        let cell: PQAssetCategoryCell = collectionView.dequeueReusableCell(withReuseIdentifier: "PQAssetCategoryCell", for: indexPath) as! PQAssetCategoryCell
+        return cell
+    }
+
+    override init(frame: CGRect) {
+        super.init(frame: frame)
+        contentView.addSubview(videoImageView)
+        contentView.addSubview(categoryNameLab)
+        contentView.addSubview(countLab)
+        contentView.addSubview(seleImage)
+    }
+
+    override var isSelected: Bool {
+        didSet {
+            seleImage.isHidden = !isSelected
+        }
+    }
+
+    required init?(coder _: NSCoder) {
+        fatalError("init(coder:) has not been implemented")
+    }
+
+    var uploadData: PQUploadModel? {
+        didSet {
+            addData()
+            addLayout()
+        }
+    }
+
+    func addData() {
+        videoImageView.image = uploadData?.image
+        categoryNameLab.text = "\(uploadData?.title ?? "")"
+        countLab.text = "(\(uploadData?.categoryList.count ?? 0))"
+    }
+
+    func addLayout() {
+        let margin: CGFloat = 12
+        let imageW: CGFloat = 66
+        let countW: CGFloat = sizeWithText(text: countLab.text ?? "", font: UIFont.systemFont(ofSize: 14), size: CGSize(width: cScreenWidth - imageW - margin * 6, height: cDefaultMargin * 2)).width + cDefaultMargin
+        let maxW: CGFloat = cScreenWidth - margin - imageW - margin - countW - margin - 13 - margin
+        videoImageView.snp.makeConstraints { make in
+            make.left.equalToSuperview().offset(margin)
+            make.width.height.equalTo(imageW)
+            make.centerY.equalToSuperview()
+        }
+        categoryNameLab.snp.makeConstraints { make in
+            make.left.equalTo(videoImageView.snp_right).offset(margin)
+            make.width.lessThanOrEqualTo(maxW)
+            make.centerY.equalToSuperview()
+        }
+        countLab.snp.remakeConstraints { make in
+            make.left.equalTo(categoryNameLab.snp_right)
+            make.centerY.equalToSuperview()
+            make.width.equalTo(countW)
+        }
+        seleImage.snp.makeConstraints { make in
+            make.centerY.equalToSuperview()
+            make.right.equalToSuperview().offset(-margin)
+            make.width.equalTo(13)
+            make.height.equalTo(10)
+        }
+    }
+}

+ 81 - 0
BFFramework/Classes/Base/View/PQBaseVideoInfoView.swift

@@ -0,0 +1,81 @@
+//
+//  PQMessageVideoInfoView.swift
+//  PQSpeed
+//
+//  Created by SanW on 2020/11/12.
+//  Copyright © 2020 BytesFlow. All rights reserved.
+//
+
+import UIKit
+
+class PQBaseVideoInfoView: UIView {
+    lazy var imageView: UIImageView = {
+        let imageView = UIImageView(image: UIImage(named: "msg_default"))
+        imageView.addCorner(corner: 4)
+        imageView.contentMode = .scaleAspectFill
+        return imageView
+    }()
+
+    lazy var videoTagView: UIImageView = {
+        let videoTagView = UIImageView(image: UIImage(named: "msg_video_tag"))
+        return videoTagView
+    }()
+
+    lazy var titleLab: UILabel = {
+        let titleLab = UILabel()
+        titleLab.textColor = UIColor.hexColor(hexadecimal: "#CCCCCC")
+        titleLab.numberOfLines = 3
+        titleLab.font = UIFont(name: "PingFangSC", size: 13)
+        return titleLab
+    }()
+
+    override init(frame: CGRect) {
+        super.init(frame: frame)
+        addSubview(imageView)
+        addSubview(titleLab)
+        imageView.addSubview(videoTagView)
+        backgroundColor = UIColor.hexColor(hexadecimal: "#171718")
+    }
+
+    required init?(coder _: NSCoder) {
+        fatalError("init(coder:) has not been implemented")
+    }
+
+    var videoData: PQVideoListModel? {
+        didSet {
+            addData()
+            addLayout()
+        }
+    }
+
+    func addData() {
+        // 这里会crash
+        let coverImg = (videoData?.videoCoverSnapshotPath != nil && (videoData?.videoCoverSnapshotPath?.count ?? 0) > 0) ? videoData?.videoCoverSnapshotPath ?? "" : (videoData?.coverImg?["coverImgPath"] as? String ?? "")
+        imageView.setNetImage(url: coverImg, placeholder: UIImage(named: "msg_default")!)
+        titleLab.text = videoData?.title
+    }
+
+    func addLayout() {
+        let margin: CGFloat = 12
+        let imageH: CGFloat = 57
+        let imageW: CGFloat = imageH * (100.0 / 57.0)
+        let tagW: CGFloat = 21
+        let tagH: CGFloat = 23
+        imageView.snp.makeConstraints { make in
+            make.left.equalToSuperview().offset(margin)
+            make.width.equalTo(imageW)
+            make.height.equalTo(imageH)
+            make.centerY.equalToSuperview()
+        }
+        videoTagView.snp.makeConstraints { make in
+            make.right.bottom.equalToSuperview().offset(-3)
+            make.width.equalTo(tagW)
+            make.height.equalTo(tagH)
+        }
+        titleLab.snp.makeConstraints { make in
+            make.top.equalTo(imageView)
+            make.left.equalTo(imageView.snp_right).offset(margin)
+            make.right.equalToSuperview().offset(-margin)
+        }
+    }
+}

+ 252 - 0
BFFramework/Classes/Base/View/PQChoseMaterialCell.swift

@@ -0,0 +1,252 @@
+//
+//  PQPQChoseMaterialCell.swift
+//  PQSpeed
+//
+//  Created by SanW on 2020/8/28.
+//  Copyright © 2020 BytesFlow. All rights reserved.
+//
+
+import Kingfisher
+import UIKit
+import Photos
+
+class PQChoseMaterialCell: UICollectionViewCell {
+    // 是否显示素材标识
+    var isShowMediaTag: Bool = true
+    // 是否是已经添加过的
+    var isAdded: Bool = false
+    var representedAssetIdentifier: String!
+    var materialClicHandle: ((_ sender: UIButton, _ materialData: PQEditVisionTrackMaterialsModel?) -> Void)?
+
+    lazy var materialImageView: AnimatedImageView = {
+        let materialImageView = AnimatedImageView()
+        materialImageView.contentMode = .scaleAspectFill
+        materialImageView.isUserInteractionEnabled = true
+        materialImageView.clipsToBounds = true
+        return materialImageView
+    }()
+
+    lazy var borderView: UIView = {
+        let borderView = UIView()
+        borderView.layer.borderColor = UIColor.hexColor(hexadecimal: "#333333").cgColor
+        borderView.layer.borderWidth = 1.5
+        return borderView
+    }()
+
+    lazy var statusLab: UILabel = {
+        let statusLab = UILabel()
+        statusLab.textColor = UIColor.white
+        statusLab.backgroundColor = UIColor(red: 0, green: 0, blue: 0, alpha: 0.7)
+        statusLab.font = UIFont.systemFont(ofSize: 11)
+        statusLab.text = "GIF"
+        statusLab.addCorner(corner: 3)
+        return statusLab
+    }()
+
+    lazy var videoStatuImageView: UIImageView = {
+        let videoStatuImageView = UIImageView(image: UIImage(named: "allPreview"))
+        return videoStatuImageView
+    }()
+
+    lazy var videoLoadingView: AnimatedImageView = {
+        let videoLoadingView = AnimatedImageView()
+        videoLoadingView.kf.setImage(with: URL(fileURLWithPath: Bundle.main.path(forResource: "material_loading", ofType: ".gif")!))
+        videoLoadingView.stopAnimating()
+        return videoLoadingView
+    }()
+
+    lazy var choseContentView: UIView = {
+        let choseContentView = UIView()
+        let ges = UITapGestureRecognizer(target: self, action: #selector(choseTap(ges:)))
+        choseContentView.addGestureRecognizer(ges)
+//        choseContentView.backgroundColor = UIColor.hexColor(hexadecimal: "#FF0000")
+        return choseContentView
+    }()
+
+    lazy var choseBtn: UIButton = {
+        let choseBtn = UIButton(type: .custom)
+        choseBtn.setBackgroundImage(UIImage(named: "videomk_chose_nomal"), for: .normal)
+        choseBtn.setBackgroundImage(UIImage(named: "videomk_chose_selected"), for: .selected)
+        choseBtn.setTitleColor(UIColor.white, for: .normal)
+        choseBtn.titleLabel?.font = UIFont.systemFont(ofSize: 12)
+        choseBtn.tag = 1
+        choseBtn.addCorner(corner: 15)
+        choseBtn.isUserInteractionEnabled = false
+        return choseBtn
+    }()
+
+    /// 删除按钮
+    lazy var deleteBtn: UIButton = {
+        let deleteBtn = UIButton(type: .custom)
+        deleteBtn.setImage(UIImage(named: "icon_search_delete"), for: .normal)
+        deleteBtn.tag = 2
+        deleteBtn.addTarget(self, action: #selector(btnClick(sender:)), for: .touchUpInside)
+        deleteBtn.isHidden = !isAdded
+        return deleteBtn
+    }()
+
+    @objc class func choseMaterialCell(collectionView: UICollectionView, indexPath: IndexPath) -> PQChoseMaterialCell {
+        let cell: PQChoseMaterialCell = collectionView.dequeueReusableCell(withReuseIdentifier: String(describing: PQChoseMaterialCell.self), for: indexPath) as! PQChoseMaterialCell
+        return cell
+    }
+
+    override init(frame: CGRect) {
+        super.init(frame: frame)
+        contentView.addSubview(borderView)
+        contentView.addSubview(materialImageView)
+        contentView.addSubview(deleteBtn)
+        materialImageView.addSubview(videoStatuImageView)
+        materialImageView.addSubview(choseContentView)
+        choseContentView.addSubview(choseBtn)
+        materialImageView.addSubview(statusLab)
+        materialImageView.addSubview(videoLoadingView)
+    }
+
+    required init?(coder _: NSCoder) {
+        fatalError("init(coder:) has not been implemented")
+    }
+
+    var materialData: PQEditVisionTrackMaterialsModel? {
+        didSet {
+            addData()
+            addLayout()
+        }
+    }
+
+    func addData() {
+        if materialData?.coverImageUI != nil {
+            materialImageView.image = materialData?.coverImageUI
+        } else if materialData?.asset != nil{
+//            DispatchQueue.global().async {[weak self] in
+//                (PHCachingImageManager.default() as? PHCachingImageManager)?.requestImage(for: (self?.materialData?.asset)!, targetSize: photoItemSize, contentMode: .aspectFill, options: nil) {[weak self] image, info in
+//                    if info?.keys.contains("PHImageResultIsDegradedKey") ?? false, "\(info?["PHImageResultIsDegradedKey"] ?? "0")" == "0", self?.representedAssetIdentifier == self?.materialData?.asset?.localIdentifier {
+//                        if image != nil {
+//                            self?.materialData?.coverImageUI = image
+//                            DispatchQueue.main.async {[weak self] in
+//                                self?.materialImageView.image = image
+//                            }
+//                        } else if image == nil, info?.keys.contains("PHImageResultIsInCloudKey") ?? false {
+//                            let option = PHImageRequestOptions()
+//                            option.isNetworkAccessAllowed = true
+//                            option.resizeMode = .fast
+//                            (PHCachingImageManager.default() as? PHCachingImageManager)?.requestImageData(for: (self?.materialData?.asset)!, options: option) { data, _, _, _ in
+//                                if data != nil, self?.representedAssetIdentifier == self?.materialData?.asset?.localIdentifier {
+//                                    let image = UIImage(data: data!)
+//                                    self?.materialData?.coverImageUI = image
+//                                    DispatchQueue.main.async {[weak self] in
+//                                        self?.materialImageView.image = image
+//                                    }
+//                                }
+//                            }
+//                        }
+//                    }
+//                }
+//            }
+        }else if(materialData?.netResCoverImageURL != nil){
+            materialImageView.setNetImage(url: materialData?.netResCoverImageURL ?? "")
+        }
+
+        if materialData?.type == StickerType.VIDEO.rawValue {
+            var duration = (materialData?.duration ?? 0.0)
+            if duration < 1 {
+                duration = 1
+            }
+            statusLab.text = "  \(Float64(duration).formatDurationToHMS())  "
+            statusLab.isHidden = false
+        } else if materialData?.type == StickerType.GIF.rawValue {
+            statusLab.text = "  GIF  "
+            statusLab.isHidden = false
+        } else {
+            statusLab.text = nil
+            statusLab.isHidden = true
+        }
+        videoStatuImageView.isHidden = !isShowMediaTag || materialData?.type != StickerType.VIDEO.rawValue
+        choseContentView.isHidden = isAdded
+        deleteBtn.isHidden = !isAdded
+        if !isAdded {
+            choseBtn.isSelected = materialData?.isSelected ?? false
+            if materialData?.isSelected ?? false {
+                choseBtn.setTitle("\(materialData?.selectedIndex ?? 1)", for: .normal)
+            } else {
+                choseBtn.setTitle(nil, for: .normal)
+            }
+        }else{
+            materialImageView.contentMode = .scaleAspectFit
+            materialImageView.backgroundColor = UIColor.hexColor(hexadecimal: "#0F0F14")
+        }
+        videoLoadingView.isHidden = !(!isAdded && materialData?.type == StickerType.VIDEO.rawValue && materialData?.isSelected == true && materialData?.downloadState == .downloading)
+        if videoLoadingView.isHidden {
+            videoLoadingView.stopAnimating()
+        } else {
+            videoLoadingView.startAnimating()
+        }
+        PQLog(message: "导出视频 = \(videoLoadingView.isHidden),asset = \(String(describing: materialData?.asset)),downloadState = \(String(describing: materialData?.downloadState))")
+//        if videoLoadingView.isHidden {
+//            videoLoadingView.stopAnimating()
+//            videoLoadingView.layer.removeAllAnimations()
+//        } else {
+//            videoLoadingView.stopAnimating()
+//            videoLoadingView.layer.removeAllAnimations()
+//            videoLoadingView.showLoadingAnimation()
+//        }
+    }
+
+    func addLayout() {
+        if isAdded {
+            borderView.snp.makeConstraints { make in
+                make.size.equalTo(CGSize.zero)
+            }
+            deleteBtn.snp.makeConstraints { make in
+                make.right.equalToSuperview()
+                make.top.equalToSuperview()
+                make.width.height.equalTo(cDefaultMargin * 2)
+            }
+            materialImageView.snp.makeConstraints { make in
+                make.left.equalToSuperview()
+                make.right.equalTo(deleteBtn.snp_centerX)
+                make.top.equalTo(deleteBtn.snp_centerY)
+                make.width.equalTo(materialImageView.snp_height)
+            }
+        } else {
+            borderView.snp.makeConstraints { make in
+                make.size.equalToSuperview()
+            }
+            materialImageView.snp.makeConstraints { make in
+                make.size.equalToSuperview()
+            }
+            choseContentView.snp.makeConstraints { make in
+                make.top.right.equalToSuperview()
+                make.width.height.equalTo(cDefaultMargin * 5)
+            }
+            choseBtn.snp.makeConstraints { make in
+                make.top.right.equalToSuperview()
+                make.width.height.equalTo(cDefaultMargin * 3)
+            }
+        }
+        statusLab.snp.makeConstraints { make in
+            make.bottom.right.equalToSuperview().offset(-4)
+            make.height.equalTo(18)
+        }
+        videoStatuImageView.snp.makeConstraints { make in
+            make.center.equalToSuperview()
+        }
+        videoLoadingView.snp.makeConstraints { make in
+            make.center.equalToSuperview()
+            make.width.height.equalTo(cDefaultMargin * 4)
+        }
+    }
+
+    @objc func btnClick(sender: UIButton) {
+        if materialClicHandle != nil {
+            materialClicHandle!(sender, materialData)
+        }
+    }
+
+    @objc func choseTap(ges _: UITapGestureRecognizer) {
+        btnClick(sender: choseBtn)
+    }
+
+    deinit {
+        videoLoadingView.stopAnimating()
+    }
+}

+ 121 - 0
BFFramework/Classes/Base/View/PQFollowButton.swift

@@ -0,0 +1,121 @@
+//
+//  PQFollowButton.swift
+//  PQSpeed
+//
+//  Created by lieyunye on 2020/6/18.
+//  Copyright © 2020 BytesFlow. All rights reserved.
+//
+
+import Foundation
+
+class PQFollowButton: UIButton {
+    let bgLayer = CAShapeLayer()
+
+    var attenBtn: UIButton = {
+        let attenBtn = UIButton(type: .custom)
+        attenBtn.isUserInteractionEnabled = false
+        attenBtn.setTitle("", for: .selected)
+        attenBtn.setImage(UIImage(named: "icon_oder"), for: .selected)
+        attenBtn.setTitle("+", for: .normal)
+        attenBtn.setImage(nil, for: .normal)
+        attenBtn.setTitleColor(UIColor.white, for: .normal)
+        attenBtn.titleLabel?.font = UIFont.systemFont(ofSize: 20, weight: .bold)
+        attenBtn.layer.cornerRadius = 10
+//        attenBtn.layer.masksToBounds = true
+        attenBtn.backgroundColor = UIColor.hexColor(hexadecimal: "#EE0051")
+        attenBtn.tag = 2
+        attenBtn.titleEdgeInsets = UIEdgeInsets(top: -3, left: 0, bottom: 0, right: 0)
+        return attenBtn
+    }()
+
+    override init(frame: CGRect) {
+        super.init(frame: frame)
+        addSubview(attenBtn)
+    }
+
+    required init?(coder _: NSCoder) {
+        fatalError("init(coder:) has not been implemented")
+    }
+
+    override func layoutSubviews() {
+        super.layoutSubviews()
+        attenBtn.snp.makeConstraints { make in
+            make.edges.equalTo(self).inset(UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0))
+        }
+    }
+
+    func reset() {
+        attenBtn.isHidden = false
+        attenBtn.layer.removeAllAnimations()
+        bgLayer.removeAllAnimations()
+        bgLayer.sublayers?.forEach {
+            $0.removeAllAnimations()
+            $0.removeFromSuperlayer()
+        }
+        bgLayer.removeFromSuperlayer()
+    }
+
+    func start() {
+        let bounds = self.bounds
+
+        bgLayer.frame = bounds
+        bgLayer.cornerRadius = bounds.height / 2.0
+        bgLayer.masksToBounds = true
+        layer.addSublayer(bgLayer)
+
+        let bgScale = CABasicAnimation(keyPath: "transform.scale")
+        bgScale.fromValue = 1
+        bgScale.toValue = 1.2
+        bgScale.duration = 0.2
+        attenBtn.layer.add(bgScale, forKey: nil)
+
+        DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.2) {
+            self.attenBtn.isHidden = true
+            let opacity = CAKeyframeAnimation(keyPath: "backgroundColor")
+            opacity.values = [UIColor.red.cgColor, UIColor.white.cgColor]
+            opacity.duration = 0.5
+            opacity.fillMode = .forwards
+            opacity.isRemovedOnCompletion = false
+            self.bgLayer.add(opacity, forKey: nil)
+
+            let bgLayer1 = CAShapeLayer()
+            let frame = CGRect(x: (bounds.size.width - bounds.size.height) / 2.0, y: 0, width: bounds.size.height, height: bounds.size.height)
+            bgLayer1.frame = frame
+            self.bgLayer.addSublayer(bgLayer1)
+
+            let linePath = UIBezierPath()
+
+            let radius: CGFloat = frame.size.width / 2.0
+            linePath.move(to: CGPoint(x: radius * 0.45, y: radius * 1.0))
+            linePath.addLine(to: CGPoint(x: radius * 0.84, y: radius * 1.32))
+            linePath.addLine(to: CGPoint(x: radius * 1.48, y: radius * 0.68))
+
+            let layer = CAShapeLayer()
+
+            layer.path = linePath.cgPath
+            layer.fillColor = UIColor.clear.cgColor
+            layer.strokeColor = UIColor.hexColor(hexadecimal: "#EE0051").cgColor
+            layer.lineWidth = 2.5
+            bgLayer1.addSublayer(layer)
+
+            let animation = CABasicAnimation()
+            animation.duration = 0.5
+            animation.keyPath = "strokeEnd"
+            animation.fromValue = 0
+            animation.toValue = 1
+            animation.fillMode = .forwards
+            animation.isRemovedOnCompletion = false
+
+            layer.add(animation, forKey: "strokeEnd")
+
+            let scale = CABasicAnimation(keyPath: "transform.scale")
+            scale.fromValue = 1
+            scale.toValue = 0
+            scale.duration = 0.2
+            scale.beginTime = CACurrentMediaTime() + 1.5
+            scale.fillMode = .forwards
+            scale.isRemovedOnCompletion = false
+            self.bgLayer.add(scale, forKey: nil)
+        }
+    }
+}

+ 41 - 0
BFFramework/Classes/Base/View/PQGIFImageView.swift

@@ -0,0 +1,41 @@
+//
+//  PQGIFImageView.swift
+//  PQSpeed
+//
+//  Created by SanW on 2020/9/2.
+//  Copyright © 2020 BytesFlow. All rights reserved.
+//
+
+import UIKit
+
+class PQGIFImageView: UIImageView {
+    var imagesDara: [String]? {
+        didSet {
+            generateImages()
+            displayGIF(1, Int.max)
+        }
+    }
+
+    var images: [UIImage] = Array<UIImage>.init()
+    func generateImages() {
+        if imagesDara != nil, (imagesDara?.count ?? 0) > 0 {
+            for item in imagesDara! {
+                let image = UIImage(named: item)!
+                images.append(image)
+            }
+        }
+    }
+
+    func displayGIF(_ duration: TimeInterval, _ repeatCount: Int) {
+        if !isAnimating {
+            layer.removeAllAnimations()
+            if images.count <= 0 {
+                return
+            }
+            animationImages = images
+            animationDuration = duration
+            animationRepeatCount = repeatCount
+            startAnimating()
+        }
+    }
+}

+ 53 - 0
BFFramework/Classes/Base/View/PQHeartAnimation.swift

@@ -0,0 +1,53 @@
+//
+//  PQHeartAnimation.swift
+//  PQSpeed
+//
+//  Created by SanW on 2020/6/11.
+//  Copyright © 2020 BytesFlow. All rights reserved.
+//
+
+import UIKit
+
+class PQHeartAnimation: NSObject {
+    static let angleArr: [CGFloat] = [CGFloat.pi / 4.0, -CGFloat.pi / 4.0, 0.0]
+    var isRepeat: Bool = false
+
+    static func showAnimation(isRepeat: Bool = false, point: CGPoint, size: CGFloat = 80.0, baseView: UIView, completeHander: @escaping (_ isFinised: Bool) -> Void) {
+        if isRepeat, baseView.viewWithTag(cHeartTag) != nil {
+            return
+        }
+        let imgV = UIImageView(frame: CGRect(x: point.x - size / 2.0, y: point.y - size / 2.0, width: size, height: size))
+        imgV.tag = cHeartTag
+        imgV.image = UIImage(named: "ic_heart")
+        imgV.contentMode = .scaleAspectFill
+        baseView.addSubview(imgV)
+
+        // 偏移角度
+        var num = 2
+        if !isRepeat {
+            num = Int(arc4random_uniform(3))
+        }
+//      PQLog(message: "num = \(num)")
+        imgV.transform = CGAffineTransform(rotationAngle: angleArr[num])
+        // 放大动画
+        let animation = CAKeyframeAnimation(keyPath: "transform.scale")
+        animation.duration = 0.5
+        animation.calculationMode = CAAnimationCalculationMode.cubic
+        animation.values = [1.3, 0.8, 1.0]
+        imgV.layer.add(animation, forKey: "transform.scale")
+
+        UIView.animate(withDuration: 1, delay: 0.5, options: .layoutSubviews, animations: {
+            imgV.alpha = 0.0
+            var newFrame = imgV.frame
+            newFrame.origin.x -= (isRepeat ? -cDefaultMargin * 2 : cDefaultMargin)
+            newFrame.origin.y -= 45.0
+            newFrame.size.height += 10.0
+            newFrame.size.width += 10.0
+            imgV.frame = newFrame
+        }) { isOK in
+            imgV.layer.removeAllAnimations()
+            imgV.removeFromSuperview()
+            completeHander(isOK)
+        }
+    }
+}

+ 101 - 0
BFFramework/Classes/Base/View/PQLoadingHUB.swift

@@ -0,0 +1,101 @@
+//
+//  PQLoadingHUB.swift
+//  PQSpeed
+//
+//  Created by SanW on 2020/6/5.
+//  Copyright © 2020 BytesFlow. All rights reserved.
+//
+
+import UIKit
+
+class PQLoadingHUBView: UIView {
+    lazy var loadingImage: UIImageView = {
+        let loadingImage = UIImageView()
+        loadingImage.kf.setImage(with: URL(fileURLWithPath: Bundle.main.path(forResource: "loading_pq", ofType: ".gif")!))
+        return loadingImage
+    }()
+
+    override init(frame: CGRect) {
+        super.init(frame: frame)
+        addSubview(loadingImage)
+        isUserInteractionEnabled = false
+    }
+
+    required init?(coder _: NSCoder) {
+        fatalError("init(coder:) has not been implemented")
+    }
+
+    override func layoutSubviews() {
+        super.layoutSubviews()
+        // 334 * 307
+        let imageW: CGFloat = 67
+        let imageH: CGFloat = 62
+        loadingImage.frame = CGRect(x: (frame.width - imageW) / 2, y: (frame.height - imageW) / 2, width: imageW, height: imageH)
+    }
+}
+
+class PQLoadingHUB: NSObject {
+    static let shared = PQLoadingHUB()
+    let viewTag = 11111
+    var isLoading: Bool = false
+
+    func showHUB() {
+        DispatchQueue.main.async { [weak self] in
+            let window = UIApplication.shared.keyWindow
+            if (window?.viewWithTag(self!.viewTag)) == nil {
+                let loadingHUB: PQLoadingHUBView = PQLoadingHUBView(frame: CGRect(x: 0, y: 0, width: 100, height: 100))
+                loadingHUB.tag = self!.viewTag
+                window?.addSubview(loadingHUB)
+                loadingHUB.center = window?.center as! CGPoint
+                self?.isLoading = true
+            }
+        }
+    }
+
+    func dismissHUB() {
+        DispatchQueue.main.async { [weak self] in
+            let window = UIApplication.shared.keyWindow
+            if (window?.viewWithTag(self!.viewTag)) != nil {
+                window?.viewWithTag(self!.viewTag)?.removeFromSuperview()
+                self?.isLoading = false
+            }
+        }
+    }
+
+    func showHUB(superView: UIView, isVerticality: Bool = false) {
+        DispatchQueue.main.async { [weak self] in
+            if superView.viewWithTag(self!.viewTag) == nil {
+                let hubW: CGFloat = 100
+                let supW: CGFloat = superView.frame.width
+                let supH: CGFloat = superView.frame.height
+                let hubY: CGFloat = isVerticality ? ((supW - hubW) / 2) : ((supH - hubW) / 2)
+                let hubX: CGFloat = isVerticality ? ((supH - hubW) / 2) : ((supW - hubW) / 2)
+                let loadingHUB: PQLoadingHUBView = PQLoadingHUBView(frame: CGRect(x: hubX, y: hubY, width: 100, height: 100))
+                loadingHUB.tag = self!.viewTag
+                superView.addSubview(loadingHUB)
+                self?.isLoading = true
+            }
+        }
+    }
+
+    func dismissHUB(superView: UIView) {
+        DispatchQueue.main.async { [weak self] in
+            if superView.viewWithTag(self!.viewTag) != nil {
+                superView.viewWithTag(self!.viewTag)?.removeFromSuperview()
+                self?.isLoading = false
+            }
+        }
+    }
+
+    override private init() {
+        super.init()
+    }
+
+    override func copy() -> Any {
+        return self
+    }
+
+    override func mutableCopy() -> Any {
+        return self
+    }
+}

+ 174 - 0
BFFramework/Classes/Base/View/PQRemindView.swift

@@ -0,0 +1,174 @@
+// MARK: 空白提示页
+
+/// 空白提示页
+class PQEmptyRemindView: UIView {
+    // 回调
+    var fullRefreshBloc: ((_ isNetConnected: Bool, _ emptyData: PQEmptyModel?) -> Void)?
+
+    lazy var imageView: UIImageView = {
+        let imageView = UIImageView()
+        imageView.backgroundColor = UIColor.clear
+        imageView.contentMode = .scaleAspectFit
+        return imageView
+    }()
+
+    lazy var remindLab: UILabel = {
+        let remindLab = UILabel()
+        remindLab.font = UIFont.systemFont(ofSize: 16)
+        remindLab.numberOfLines = 1
+        remindLab.textAlignment = NSTextAlignment.center
+        remindLab.textColor = UIColor.white
+        return remindLab
+    }()
+
+    lazy var shimmeringView: FBShimmeringView = {
+        let shimmeringView = FBShimmeringView()
+        shimmeringView.isShimmering = false
+        shimmeringView.shimmeringBeginFadeDuration = 0.3
+        shimmeringView.shimmeringEndFadeDuration = 0.1
+        shimmeringView.shimmeringOpacity = 0.2
+        shimmeringView.shimmeringSpeed = 300
+        shimmeringView.contentView = remindLab
+        return shimmeringView
+    }()
+
+    lazy var remindSubLab: UILabel = {
+        let remindSubLab = UILabel()
+        remindSubLab.font = UIFont.systemFont(ofSize: 14)
+        remindSubLab.numberOfLines = 1
+        remindSubLab.textAlignment = NSTextAlignment.center
+        remindSubLab.textColor = UIColor.hexColor(hexadecimal: "#999999")
+        return remindSubLab
+    }()
+
+    lazy var refreshBtn: UIButton = {
+        let refreshBtn = UIButton(type: .custom)
+        refreshBtn.backgroundColor = UIColor.hexColor(hexadecimal: "#EE0051")
+        refreshBtn.titleLabel?.font = UIFont.systemFont(ofSize: 15)
+        refreshBtn.setTitleColor(UIColor.white, for: .normal)
+        refreshBtn.setTitle("刷新", for: .normal)
+        refreshBtn.setTitle("重新连接网络", for: .selected)
+        refreshBtn.addCorner(corner: cDefaultMargin * 2)
+        refreshBtn.isHidden = true
+        refreshBtn.addTarget(self, action: #selector(fullRefresh), for: .touchUpInside)
+        return refreshBtn
+    }()
+
+    override init(frame: CGRect) {
+        super.init(frame: frame)
+        addSubview(imageView)
+        addSubview(shimmeringView)
+        addSubview(remindSubLab)
+        addSubview(refreshBtn)
+        let ges = UITapGestureRecognizer(target: self, action: #selector(fullRefresh))
+        addGestureRecognizer(ges)
+        backgroundColor = UIColor.black
+    }
+
+    required init?(coder _: NSCoder) {
+        fatalError("init(coder:) has not been implemented")
+    }
+
+    @objc var emptyData: PQEmptyModel? {
+        didSet {
+            addData()
+            addLayout()
+        }
+    }
+}
+
+extension PQEmptyRemindView {
+    func addData() {
+        if !isNetConnected() {
+            remindLab.text = "网络连接失败,请检查网络后重试"
+            remindSubLab.isHidden = true
+            refreshBtn.isHidden = false
+            refreshBtn.backgroundColor = UIColor.hexColor(hexadecimal: "#EE0051")
+            refreshBtn.setTitleColor(UIColor.white, for: .normal)
+            refreshBtn.setTitle("刷新", for: .normal)
+            imageView.image = UIImage(named: "pic_network")
+        } else {
+            if emptyData?.emptyImage != nil, emptyData?.emptyImage?.count ?? 0 > 0 {
+                imageView.image = UIImage(named: emptyData?.emptyImage ?? "")
+            } else {
+                imageView.image = nil
+            }
+            remindLab.text = emptyData?.title
+            remindSubLab.text = emptyData?.summary
+            refreshBtn.isHidden = emptyData?.isRefreshHidden ?? true
+            if emptyData?.refreshImage != nil, (emptyData?.refreshImage?.count ?? 0) > 0 {
+                refreshBtn.setImage(UIImage(named: emptyData?.refreshImage ?? ""), for: .normal)
+            } else {
+                refreshBtn.setImage(nil, for: .normal)
+            }
+            refreshBtn.setAttributedTitle(emptyData?.refreshTitle, for: .normal)
+            if emptyData?.refreshBgColor != nil {
+                refreshBtn.backgroundColor = emptyData?.refreshBgColor
+            }
+        }
+    }
+
+    override var isHidden: Bool {
+        didSet {
+            addData()
+            addLayout()
+        }
+    }
+
+    func addLayout() {
+        var imageH: CGFloat = cDefaultMargin * 7
+        var contentH: CGFloat = 0
+
+        if (emptyData?.emptyImage != nil &&  emptyData?.emptyImage?.count ?? 0 > 0) || !isNetConnected() {
+            contentH = contentH + imageH + cDefaultMargin
+        } else {
+            imageH = 0
+        }
+        if emptyData?.title != nil, emptyData?.title?.count ?? 0 > 0 {
+            contentH = contentH + cDefaultMargin * 2 + cDefaultMargin
+        }
+        if emptyData?.summary != nil, emptyData?.summary?.count ?? 0 > 0 {
+            contentH = contentH + cDefaultMargin * 2 + cDefaultMargin
+        }
+        if !refreshBtn.isHidden {
+            contentH = contentH + cDefaultMargin + cDefaultMargin * 4
+        }
+        let topY = (frame.height - contentH) / 2
+        imageView.snp.remakeConstraints { make in
+            make.top.equalTo(topY)
+            make.centerX.equalTo(self)
+            make.height.equalTo(imageH)
+        }
+        shimmeringView.snp.makeConstraints { make in
+            make.left.right.equalTo(self)
+            make.top.equalTo(imageView.snp_bottom).offset(cDefaultMargin)
+        }
+        remindLab.snp.remakeConstraints { make in
+            make.size.equalToSuperview()
+        }
+        remindSubLab.snp.makeConstraints { make in
+            make.left.right.equalTo(self)
+            make.top.equalTo(remindLab.snp_bottom).offset(cDefaultMargin)
+        }
+        refreshBtn.snp.makeConstraints { make in
+            make.width.equalTo(cDefaultMargin * 13)
+            make.height.equalTo(cDefaultMargin * 4)
+            make.top.equalTo(remindSubLab.snp_bottom).offset(cDefaultMargin)
+            make.centerX.equalToSuperview()
+        }
+    }
+
+    @objc func fullRefresh() {
+        let isConnected: Bool = isNetConnected()
+        if !isConnected {
+            cShowHUB(superView: nil, msg: "网络不给力")
+        }
+        if fullRefreshBloc != nil {
+            fullRefreshBloc!(isConnected, emptyData)
+        }
+    }
+
+    func addShimmeringView() {}
+
+    func removeShimmeringView() {}
+}

+ 103 - 0
BFFramework/Classes/Base/View/PQSectionHeadView.swift

@@ -0,0 +1,103 @@
+//
+//  PQShareSpaceHeadView.swift
+//  PQSpeed
+//
+//  Created by SanW on 2020/11/12.
+//  Copyright © 2020 BytesFlow. All rights reserved.
+//
+
+import UIKit
+
+class PQSectionHeadView: UIView {
+    // 清除按钮回调
+    var btnClickHandle: ((_ sender: UIButton) -> Void)?
+    // 是否隐藏清除按钮
+    var isHiddenClearBtn: Bool = true {
+        didSet {
+            clearBtn.isHidden = isHiddenClearBtn
+        }
+    }
+
+    var sectionTitle: String? {
+        didSet {
+            addData()
+            addLayout()
+        }
+    }
+
+    lazy var lineView: UIView = {
+        let lineView = UIView()
+        lineView.backgroundColor = UIColor.hexColor(hexadecimal: "#EE0051")
+        lineView.addCorner(corner: 1.5)
+        return lineView
+    }()
+
+    lazy var titleLab: UILabel = {
+        let titleLab = UILabel()
+        titleLab.textColor = UIColor.white
+        titleLab.font = UIFont(name: "PingFangSC", size: 16)
+        return titleLab
+    }()
+
+    lazy var clearBtn: UIButton = {
+        let clearBtn = UIButton(type: .custom)
+        clearBtn.setTitle("全部已读 ", for: .normal)
+        clearBtn.setImage(UIImage(named: "msg_clear_noreaded"), for: .normal)
+        clearBtn.setTitleColor(UIColor.hexColor(hexadecimal: "#666666"), for: .normal)
+        clearBtn.titleLabel?.font = UIFont(name: "PingFangSC", size: 13)
+        clearBtn.addTarget(self, action: #selector(btnClick(sender:)), for: .touchUpInside)
+        clearBtn.isHidden = true
+        return clearBtn
+    }()
+
+    override init(frame: CGRect) {
+        super.init(frame: frame)
+        addSubviews()
+    }
+
+    override func layoutSubviews() {
+        super.layoutSubviews()
+//        addData()
+//        addLayout()
+    }
+
+    required init?(coder _: NSCoder) {
+        fatalError("init(coder:) has not been implemented")
+    }
+
+    func addSubviews() {
+        addSubview(lineView)
+        addSubview(titleLab)
+        addSubview(clearBtn)
+    }
+
+    func addData() {
+        titleLab.text = sectionTitle
+    }
+
+    func addLayout() {
+        let lineW: CGFloat = 3
+        let lineH: CGFloat = cDefaultMargin * 2
+
+        lineView.snp.makeConstraints { make in
+            make.width.equalTo(lineW)
+            make.height.equalTo(lineH)
+            make.centerY.equalToSuperview()
+        }
+        titleLab.snp.makeConstraints { make in
+            make.left.equalTo(lineView.snp_right).offset(lineW * 4)
+            make.centerY.equalToSuperview()
+        }
+        clearBtn.snp.makeConstraints { make in
+            make.centerY.equalToSuperview()
+            make.right.equalToSuperview().offset(-cDefaultMargin)
+        }
+        clearBtn.imagePosition(at: .right, space: 0)
+    }
+
+    @objc func btnClick(sender: UIButton) {
+        if btnClickHandle != nil {
+            btnClickHandle!(sender)
+        }
+    }
+}

+ 113 - 0
BFFramework/Classes/Base/View/PQSelectedOprationView.swift

@@ -0,0 +1,113 @@
+//
+//  PQSelectedOprationView.swift
+//  PQSpeed
+//
+//  Created by SanW on 2020/12/11.
+//  Copyright © 2020 BytesFlow. All rights reserved.
+//
+
+import UIKit
+
+// MARK: - 选择操作view
+
+/// 选择操作view
+class PQSelectedOprationView: UIView {
+    let itemH: CGFloat = cDefaultMargin * 6
+    let lineH: CGFloat = 1
+    let margin: CGFloat = 8
+    var contentH: CGFloat = 0
+    var itemList: [String]? {
+        didSet {
+            if (itemList?.count ?? 0) > 0 {
+                contentH = itemH + margin + cSafeAreaHeight + CGFloat(itemList?.count ?? 0) * (itemH + 1)
+                contentView.frame = CGRect(x: 0, y: cScreenHeigth, width: cScreenWidth, height: contentH)
+                contentView.addCorner(roundingCorners: [.topLeft, .topRight], corner: 11)
+                cancelBtn.frame = CGRect(x: 0, y: contentH - itemH - cSafeAreaHeight, width: cScreenWidth, height: itemH + cSafeAreaHeight)
+            }
+        }
+    }
+
+    var completeHander: ((_ sender: UIButton) -> Void)?
+    lazy var contentView: UIView = {
+        let contentView = UIView(frame: CGRect(x: 0, y: cScreenHeigth, width: cScreenWidth, height: itemH + margin))
+        contentView.backgroundColor = UIColor.black
+        contentView.addCorner(roundingCorners: [.topLeft, .topRight], corner: 11)
+        return contentView
+    }()
+
+    lazy var cancelBtn: UIButton = {
+        let cancelBtn = UIButton(type: .custom)
+        cancelBtn.setTitle("取消", for: .normal)
+        cancelBtn.setTitleColor(UIColor.hexColor(hexadecimal: "#BDBDBD"), for: .normal)
+        cancelBtn.backgroundColor = UIColor.hexColor(hexadecimal: "#2C2C2C")
+        cancelBtn.addTarget(self, action: #selector(btnClick(sender:)), for: .touchUpInside)
+        cancelBtn.titleLabel?.font = UIFont.systemFont(ofSize: 17)
+        return cancelBtn
+    }()
+
+    override init(frame: CGRect) {
+        super.init(frame: frame)
+        backgroundColor = cShadowColor
+        addSubview(contentView)
+        contentView.addSubview(cancelBtn)
+        let ges = UITapGestureRecognizer(target: self, action: #selector(removeView))
+        addGestureRecognizer(ges)
+    }
+
+    required init?(coder _: NSCoder) {
+        fatalError("init(coder:) has not been implemented")
+    }
+
+    override func layoutSubviews() {
+        super.layoutSubviews()
+        addSubViews()
+    }
+
+    func addSubViews() {
+        if (itemList?.count ?? 0) > 0 {
+            for (index, item) in itemList!.enumerated() {
+                let normalBtn = UIButton(type: .custom)
+                normalBtn.frame = CGRect(x: 0, y: (itemH + 1) * CGFloat(index), width: frame.width, height: itemH)
+                normalBtn.backgroundColor = UIColor.hexColor(hexadecimal: "#2C2C2C")
+                normalBtn.setTitle(item, for: .normal)
+                normalBtn.setTitleColor(UIColor.hexColor(hexadecimal: "#BDBDBD"), for: .normal)
+                normalBtn.tag = index + 1
+                normalBtn.titleLabel?.font = UIFont.systemFont(ofSize: 17)
+                normalBtn.addTarget(self, action: #selector(btnClick(sender:)), for: .touchUpInside)
+                contentView.addSubview(normalBtn)
+
+                let lineView = UIView(frame: CGRect(x: 0, y: normalBtn.frame.maxY, width: frame.width, height: lineH))
+                lineView.backgroundColor = UIColor.hexColor(hexadecimal: "#212223")
+                contentView.addSubview(lineView)
+                contentH = contentH + (itemH + 1)
+            }
+        }
+    }
+
+    @objc func removeView() {
+        UIView.animate(withDuration: 0.3, animations: { [weak self] in
+            self?.contentView.frame = CGRect(x: 0, y: cScreenHeigth, width: cScreenWidth, height: self!.contentH)
+        }) { [weak self] _ in
+            self?.removeFromSuperview()
+        }
+    }
+
+    class func showSelectedOprationView(itemList: [String], completeHander: @escaping ((_ sender: UIButton) -> Void)) {
+        let selectedOprationView: PQSelectedOprationView = PQSelectedOprationView(frame: CGRect(x: 0, y: 0, width: cScreenWidth, height: cScreenHeigth))
+        selectedOprationView.completeHander = completeHander
+        selectedOprationView.itemList = itemList
+        UIApplication.shared.keyWindow?.addSubview(selectedOprationView)
+        let contentH: CGFloat = 68.0 + cSafeAreaHeight + CGFloat(itemList.count) * 61.0
+        UIView.animate(withDuration: 0.3, animations: {
+            selectedOprationView.contentView.frame = CGRect(x: 0, y: cScreenHeigth - contentH, width: cScreenWidth, height: contentH)
+        }) { _ in
+        }
+    }
+
+    @objc func btnClick(sender: UIButton) {
+        if completeHander != nil {
+            completeHander!(sender)
+        }
+        removeView()
+    }
+}

+ 27 - 0
BFFramework/Classes/Base/View/PQTabBar.swift

@@ -0,0 +1,27 @@
+//
+//  PQTabBar.swift
+//  PQSpeed
+//
+//  Created by SanW on 2021/1/18.
+//  Copyright © 2021 BytesFlow. All rights reserved.
+//
+
+import UIKit
+
+class PQTabBar: UITabBar {
+    override open func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
+        if isHidden { // 如果隐藏的话不处理事件
+            return super.hitTest(point, with: event)
+        }
+        let view = super.hitTest(point, with: event)
+        if view == nil {
+            for subView in subviews {
+                let myPoint = subView.convert(point, from: self)
+                if subView.bounds.contains(myPoint) {
+                    return subView
+                }
+            }
+        }
+        return view
+    }
+}

+ 104 - 0
BFFramework/Classes/Base/View/PQTextView.swift

@@ -0,0 +1,104 @@
+//
+//  PQTextView.swift
+//  PQSpeed
+//
+//  Created by SanW on 2020/6/11.
+//  Copyright © 2020 BytesFlow. All rights reserved.
+//
+
+import UIKit
+
+class PQTextView: UITextView {
+    private let textValuesQueue = DispatchQueue(label: "com.BytesFlow.textValuesQueue", qos: .default)
+
+    var textDidChangedHandle: ((_ textView: UITextView) -> Void)?
+    // 最多输入的个数
+    var maxTextLength : Int?
+    // 输入个数超过时提示
+    var maxTextLengthRemind : String?
+    /// setNeedsDisplay调用drawRect
+    var placeHolder: String = "" {
+        didSet {
+            setNeedsDisplay()
+        }
+    }
+
+    /// placeHolder是否居中
+    var isCenter: Bool = false {
+        didSet {
+            setNeedsDisplay()
+        }
+    }
+
+    var placeHolderColor: UIColor = UIColor.gray {
+        didSet {
+            setNeedsDisplay()
+        }
+    }
+
+    override var font: UIFont? {
+        didSet {
+            setNeedsDisplay()
+        }
+    }
+
+    override var text: String! {
+        didSet{
+           setNeedsLayout()
+        }
+    }
+
+    override var attributedText: NSAttributedString! {
+        didSet {
+            setNeedsDisplay()
+        }
+    }
+
+    override init(frame: CGRect, textContainer: NSTextContainer?) {
+        super.init(frame: frame, textContainer: textContainer)
+        /// default字号
+        PQNotification.addObserver(self, selector: #selector(textDidChanged(noti:)), name: UITextView.textDidChangeNotification, object: self)
+    }
+
+    required init?(coder _: NSCoder) {
+        fatalError("init(coder:) has not been implemented")
+    }
+
+    @objc func textDidChanged(noti _: NSNotification) {
+        if maxTextLength != nil && text.count > maxTextLength! {
+            text = String(text.prefix(maxTextLength!))
+            cShowHUB(superView: nil, msg: (maxTextLengthRemind != nil && (maxTextLengthRemind?.count ?? 0) > 0) ? maxTextLengthRemind : "最多输入\( maxTextLength!)个字符")
+        }
+        setNeedsDisplay()
+        if textDidChangedHandle != nil {
+            textDidChangedHandle!(self)
+        }
+    }
+
+    override func draw(_ rect: CGRect) {
+        if hasText {
+            return
+        }
+        let size = sizeWithText(text: placeHolder, font: font ?? UIFont.systemFont(ofSize: 14), size: rect.size)
+        var newRect = CGRect()
+        newRect.size.width = size.width
+        newRect.size.height = size.height
+        if isCenter {
+            newRect.origin.x = (rect.width - size.width) / 2
+            newRect.origin.y = (rect.height - size.height) / 2
+        } else {
+            newRect.origin.x = placeHolder.contains("未识别到文字") ? 0 : 5
+            newRect.origin.y = placeHolder.contains("未识别到文字") ? 0 : 7
+        }
+        (placeHolder as NSString).draw(in: newRect, withAttributes: [NSAttributedString.Key.font: font ?? UIFont.systemFont(ofSize: 14), NSAttributedString.Key.foregroundColor: placeHolderColor])
+    }
+
+    override func layoutSubviews() {
+        super.layoutSubviews()
+        setNeedsDisplay()
+    }
+
+    deinit {
+        PQNotification.removeObserver(self, name: UITextView.textDidChangeNotification, object: self)
+    }
+}

+ 26 - 0
BFFramework/Classes/Base/ViewModel/Extensions/OperationQueue+Ext.swift

@@ -0,0 +1,26 @@
+//
+//  OperationQueue+Ext.swift
+//  PQSpeed
+//
+//  Created by SanW on 2020/12/8.
+//  Copyright © 2020 BytesFlow. All rights reserved.
+//
+
+import Foundation
+
+// MARK: - 操作队列扩展
+
+/// 操作队列扩展
+extension OperationQueue {
+    convenience init(qualityOfService: QualityOfService = .default,
+                     maxConcurrentOperationCount: Int = OperationQueue.defaultMaxConcurrentOperationCount,
+                     underlyingQueue: DispatchQueue? = nil,
+                     name: String? = nil)
+    {
+        self.init()
+        self.qualityOfService = qualityOfService
+        self.maxConcurrentOperationCount = maxConcurrentOperationCount
+        self.underlyingQueue = underlyingQueue
+        self.name = name
+    }
+}

+ 39 - 0
BFFramework/Classes/Base/ViewModel/Extensions/Task+Ext.swift

@@ -0,0 +1,39 @@
+//
+//  Task+Ext.swift
+//  PQSpeed
+//
+//  Created by SanW on 2020/12/8.
+//  Copyright © 2020 BytesFlow. All rights reserved.
+//
+
+import Foundation
+
+// MARK: - URLSessionDataTask添加url跟taskId
+
+/// URLSessionDataTask添加url跟taskId
+
+private var taskId_key: Void?
+private var url_key: Void?
+private var pathExtension_key: Void?
+
+extension URLSessionTask {
+    /// 任务url
+    var taskUrl: String {
+        set {
+            objc_setAssociatedObject(self, &url_key, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
+        }
+        get {
+            return objc_getAssociatedObject(self, &url_key) as? String ?? ""
+        }
+    }
+
+    /// 任务唯一标示
+    var taskId: String {
+        set {
+            objc_setAssociatedObject(self, &taskId_key, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
+        }
+        get {
+            return objc_getAssociatedObject(self, &taskId_key) as? String ?? ""
+        }
+    }
+}

+ 647 - 0
BFFramework/Classes/Base/ViewModel/PQBaseViewModel.swift

@@ -0,0 +1,647 @@
+//
+//  PQBaseViewModel.swift
+//  PQSpeed
+//
+//  Created by SanW on 2020/5/25.
+//  Copyright © 2020 BytesFlow. All rights reserved.
+//
+
+import Alamofire
+import UIKit
+import ObjectMapper
+import RealmSwift
+
+class PQBaseViewModel: NSObject {
+    
+    typealias completeHander = (_ userInfo: [String: Any]?, _ msg: String?) -> Void
+    
+    /// 搜索背景音乐
+    /// - Parameters:
+    ///   - keyWord: 搜索key
+    ///   - pageNum: 当前页
+    ///   - pageSize: 每页个数 默认 :30
+    ///   - videoCount: 卡点音乐数据搜索-视频素材个数
+    ///   - imageCount: 卡点音乐数据搜索-图片素材个数
+    ///   - totalDuration: 卡点音乐数据搜索-素材总时长
+    ///   - completeHander: completeHander description
+    /// - Returns: <#description#>
+    class func searchBGMListData(_ keyWord: String?, _ pageNum: Int = 1, _ pageSize: Int = 30,videoCount: Int = 0, imageCount: Int = 0, totalDuration: Float64 = 0, completeHander: @escaping (_ bgmList: [PQVoiceModel], _ msg: String?) -> Void) {
+        SWNetRequest.postRequestData(url: PQENVUtil.shared.materialSearchApi + searchBGMMaterialUrl, parames: ["keyWord": keyWord ?? "", "pageNo": pageNum, "pageSize": pageSize], encoding: JSONEncoding.default) { response, _, error, _ in
+            DispatchQueue.global().async {
+                var bgmList = Array<PQVoiceModel>.init()
+                if response is NSNull || response == nil {
+                    DispatchQueue.main.async {
+                        completeHander(bgmList, error?.msg)
+                    }
+                    return
+                }
+                let tempArr = (response as? [String: Any])?["entityList"] as? [[String: Any]]
+                if tempArr != nil {
+                    for item in tempArr! {
+                        let tempModel = PQVoiceModel(jsonDict: item)
+                        tempModel.volume = 30
+                        tempModel.voiceType = VOICETYPT.BGM.rawValue
+                        if tempModel.rhythmSdata.count > 0 && (videoCount > 0 || imageCount > 0 ||  totalDuration > 0)  {
+                            tempModel.endTime = tempModel.startTime + tempModel.stuckPointCuttingTime(videoCount: videoCount, imageCount: imageCount, totalDuration: totalDuration)
+                        }
+                        bgmList.append(tempModel)
+                    }
+                }
+                DispatchQueue.main.async {
+                    completeHander(bgmList, nil)
+                }
+            }
+        }
+    }
+    /// 请求系统配置
+    /// - Parameter completeHander: <#completeHander description#>
+    /// - Returns: <#description#>
+    class func systemConfig(completeHander: @escaping (_ isSuccess: Bool) -> Void) {
+        if PQSingletoMemoryUtil.shared.isFinishedCoging {
+            completeHander(true)
+            return
+        }
+        SWNetRequest.postRequestData(url: PQENVUtil.shared.longvideoapi + systemAppConfigUrl, parames: nil) { response, _, _, _ in
+            if response != nil, !(response is NSNull), (response as! [String: Any]).keys.contains("needLogin") {
+                let needLogin: String = "\((response as! [String: Any])["needLogin"] ?? "0")"
+                saveUserDefaults(key: cNeedLoginKey, value: needLogin)
+                PQSingletoMemoryUtil.shared.needLogin = needLogin == "1"
+                PQSingletoMemoryUtil.shared.isFinishedCoging = true
+                completeHander(true)
+            } else {
+                completeHander(false)
+            }
+        }
+    }
+
+    /// 系统设置
+    /// - Parameter completeHander: <#completeHander description#>
+    /// - Returns: <#description#>
+    class func systemBaseConfig(completeHander: @escaping (_ isSuccess: Bool) -> Void) {
+        SWNetRequest.postRequestData(url: PQENVUtil.shared.longvideoapi + systemConfigUrl, parames: nil) { _, _, _, _ in
+            completeHander(true)
+        }
+    }
+  
+ 
+
+    /// 删除某个视频
+    /// - Parameters:
+    ///   - videoId: <#videoId description#>
+    ///   - completeHander: <#completeHander description#>
+    /// - Returns: <#description#>
+    class func deleteVideo(videoId: Int, completeHander: @escaping (_ isSuccess: Bool, _ msg: String?) -> Void) {
+        SWNetRequest.postRequestData(url: PQENVUtil.shared.longvideoapi + deleteVideoUrl, parames: ["videoId": videoId]) { response, _, error, _ in
+            if response != nil {
+                postNotification(name: cDeleteVideoSuccessKey, userInfo: ["videoId": "\(videoId)"])
+                completeHander(true, nil)
+            } else {
+                completeHander(false, error?.msg)
+            }
+        }
+    }
+
+    /// 不感兴趣某个视频b
+    /// - Parameters:
+    ///   - videoId: 视频Id
+    ///   - completeHander: <#completeHander description#>
+    /// - Returns: <#description#>
+    class func hateVideo(videoId: Int, completeHander: @escaping (_ isSuccess: Bool, _ msg: String?) -> Void) {
+        SWNetRequest.postRequestData(url: PQENVUtil.shared.longvideoapi + hateVideoUrl, parames: ["videoId": videoId]) { response, _, error, _ in
+            if response != nil {
+                completeHander(true, nil)
+            } else {
+                completeHander(false, error?.msg)
+            }
+        }
+    }
+
+    /// 获取用户发布最新/最热视频
+    /// - Parameters:
+    ///   - type: 1-最新列表 2-最热列表
+    ///   - targetUid: 目标用户
+    ///   - pageSize: 页面大小
+    ///   - pageNo: 当前页
+    ///   - currentVideoId: 当前视频ID
+    ///   - sortField: 排序方式 1:时间,2:热度
+    ///   - layoutType: 页面排版方式 1:单列,2:双列
+    ///   - lastTimestamp: 最后一条记录的时间戳
+    /// - Returns: <#description#>
+    class func userInfoVideoList(type: Int = 1, targetUid: Int, pageSize: Int = 10, pageNo: Int, currentVideoId _: Int = 0, sortField _: Int = 1, layoutType _: Int = 2, lastTimestamp: Int, completeHander: @escaping (_ listData: [PQVideoListModel]?, _ videoList: [[PQVideoListModel]]?, _ msg: String?) -> Void) {
+        var url: String = PQENVUtil.shared.longvideoapi
+        if type == 1 {
+            url = url + latelyByCollectionIdUrl
+        } else {
+            url = url + hotByCollectionIdUrl
+        }
+        var params: [String: Any] = ["targetUid": targetUid, "pageSize": pageSize, "sortField": type]
+        if type == 1 {
+            params["lastTimestamp"] = lastTimestamp
+        } else {
+            params["pageNo"] = pageNo
+        }
+        SWNetRequest.postRequestData(url: url, parames: params) { response, _, error, _ in
+            PQLog(message: "当前线程:\(Thread.isMainThread) ")
+            DispatchQueue.global().async {
+                PQLog(message: "当前线程 global:\(Thread.isMainThread) ")
+                var listData = Array<PQVideoListModel>.init()
+                var videoList = Array<[PQVideoListModel]>.init()
+
+                if !(response is NSNull), response != nil {
+                    let tempArr = response as! [[String: Any]]
+                    for item in tempArr {
+                        let tempModel = PQVideoListModel(jsonDict: item)
+                        if targetUid == Int(PQLoginUserInfo.shared.uid) {
+                            tempModel.pageSource = .sp_videoDetail_upload
+                        } else {
+                            tempModel.pageSource = .sp_videoDetail_userHomePage
+                        }
+                        listData.append(tempModel)
+                        if tempModel.auditStatus == 5, tempModel.transcodeStatus == 3 {
+                            videoList.append([tempModel])
+                        }
+                    }
+                    DispatchQueue.main.async {
+                        PQLog(message: "当前线程 main:\(Thread.isMainThread) ")
+                        completeHander(listData, videoList, nil)
+                    }
+                } else {
+                    DispatchQueue.main.async {
+                        PQLog(message: "当前线程main:\(Thread.isMainThread) ")
+                        completeHander(listData, videoList, error?.msg)
+                    }
+                }
+            }
+        }
+    }
+
+    /// 请求举报原因列表
+    /// - Parameters:
+    ///   - isReportUser: 是否举报用户
+    ///   - groupId: 组id
+    ///   - completeHander: <#completeHander description#>
+    /// - Returns: <#description#>
+    class func reportList(isReportUser: Bool, groupId _: String?, completeHander: @escaping (_ listData: [PQBaseModel]?) -> Void) {
+        var url: String = PQENVUtil.shared.longvideoapi
+        if isReportUser {
+            url = url + reportUserListUrl
+        } else {
+            url = url + reportVideoListUrl
+        }
+        SWNetRequest.postRequestData(url: url, parames: nil) { response, _, _, _ in
+            var reportList = Array<PQBaseModel>.init()
+            if response is NSNull || response == nil {
+                completeHander(reportList)
+                return
+            }
+            if isReportUser {
+                for item in response as! [String] {
+                    let tempModel = PQBaseModel()
+                    tempModel.title = item
+                    reportList.append(tempModel)
+                }
+            } else {
+                let tempArr = response as! [[String: Any]]
+                for item in tempArr {
+                    let reasonsArr = item["reasons"] as! [String]
+                    for reasonsItem in reasonsArr {
+                        let tempModel = PQBaseModel()
+                        tempModel.title = reasonsItem
+                        reportList.append(tempModel)
+                    }
+                }
+            }
+            completeHander(reportList)
+        }
+    }
+
+    /// 举报视频/用户
+    /// - Parameters:
+    ///   - isReportUser: 是否是举报用户
+    ///   - videoId: 视频ID
+    ///   - reason: 举报原因
+    ///   - completeHander: <#completeHander description#>
+    /// - Returns: <#description#>
+    class func reportOprate(isReportUser: Bool, uniqueId: Int, reason: String?, completeHander: @escaping (_ isSuccess: Bool) -> Void) {
+        var params: [String: Any] = ["reason": reason ?? ""]
+        var url: String = PQENVUtil.shared.longvideoapi
+        if isReportUser {
+            url = url + reportUserUrl
+            params["reportUid"] = uniqueId
+        } else {
+            url = url + reportVideoUrl
+            params["videoId"] = uniqueId
+        }
+        SWNetRequest.postRequestData(url: url, parames: params) { _, _, _, _ in
+            completeHander(true)
+        }
+    }
+
+    /// 拉黑/移除某个用户
+    /// - Parameters:
+    ///   - targetUid: <#targetUid description#>
+    ///   - completeHander: <#completeHander description#>
+    /// - Returns: <#description#>
+    class func bannedUser(isBanned: Bool, targetUid: Int, completeHander: @escaping (_ isSuccess: Bool, _ isBanned: Bool) -> Void) {
+        var url: String = PQENVUtil.shared.longvideoapi
+        if isBanned {
+            url = url + bannedUserUrl
+        } else {
+            url = url + unBannedUserUrl
+        }
+        SWNetRequest.postRequestData(url: url, parames: ["targetUid": targetUid]) { _, _, _, _ in
+            postNotification(name: cBannedNotiKey, userInfo: ["userId": targetUid, "isBanned": isBanned ? 1 : 0])
+            completeHander(true, isBanned)
+        }
+    }
+
+    /// 某个用户是否被拉黑
+    /// - Parameters:
+    ///   - targetUid: <#targetUid description#>
+    ///   - completeHander: <#completeHander description#>
+    /// - Returns: <#description#>
+    class func isBannedUser(targetUid: Int, completeHander: @escaping (_ isSuccess: Bool, _ isBanned: Bool) -> Void) {
+        SWNetRequest.postRequestData(url: PQENVUtil.shared.longvideoapi + isBannedUserUrl, parames: ["targetUid": targetUid]) { response, _, _, _ in
+            if response == nil {
+                completeHander(false, false)
+            } else {
+                completeHander(true, (response as! Int) == 1)
+            }
+        }
+    }
+
+    /// 获取用户管理列表
+    /// - Parameter completeHander: <#completeHander description#>
+    /// - Returns: <#description#>
+    class func bannedUserList(completeHander: @escaping (_ listData: [PQUserInfoModel]?) -> Void) {
+        SWNetRequest.postRequestData(url: PQENVUtil.shared.longvideoapi + bannedUserListUrl, parames: nil) { response, _, _, _ in
+            var listData = Array<PQUserInfoModel>.init()
+            if response is NSNull || response == nil {
+                completeHander(listData)
+                return
+            }
+            let responseArr: [[String: Any]] = response as! [[String: Any]]
+            for dictItem in responseArr {
+                let tempModel = PQUserInfoModel(jsonDict: dictItem)
+                tempModel.isBanned = true
+                listData.append(tempModel)
+            }
+            completeHander(listData)
+        }
+    }
+
+    /// 获取视频详情数据
+    /// - Parameters:
+    ///   - isBatch: 是否批量获取
+    ///   - videoId: 视频id ,isBatch = true 时,用英文,隔开
+    ///   - completeHander: <#completeHander description#>
+    /// - Returns: <#description#>
+    class func videoDetailInfo(isBatch: Bool = false, videoId: String, completeHander: @escaping (_ videoDatas: [[PQVideoListModel]]?, _ code: Int?, _ mag: String?) -> Void) {
+        var url: String = PQENVUtil.shared.longvideoapi
+        var params: [String: Any] = [:]
+
+        if isBatch {
+            url = url + videosDetailUrl
+            params = ["videoIds": videoId]
+        } else {
+            url = url + videoDetailUrl
+            params = ["videoId": videoId]
+        }
+        SWNetRequest.postRequestData(url: url, parames: params) { response, _, error, _ in
+            if response is NSNull || response == nil {
+                completeHander(nil, error?.code, error?.msg)
+                return
+            }
+            var videoDatasList = Array<[PQVideoListModel]>.init()
+            if !isBatch {
+                let tempModel = PQVideoListModel(jsonDict: response as! [String: Any])
+                videoDatasList.append([tempModel])
+            } else {
+                if response is [[String: Any]] {
+                    let tempArr = response as! [[String: Any]]
+                    for item in tempArr {
+                        let tempModel = PQVideoListModel(jsonDict: item)
+                        tempModel.tab_pageType = .TAB_PAGETYPE_RECOMM
+                        tempModel.pageSource = .sp_category
+                        videoDatasList.append([tempModel])
+                    }
+                }
+            }
+            completeHander(videoDatasList, 0, nil)
+        }
+    }
+
+    class func h5ShareLinkInfo(videoId: String, pageSource: PAGESOURCE, completeHander: @escaping (_ shareLinkPath: String?, _ mag: String?) -> Void) {
+        SWNetRequest.postRequestData(url: PQENVUtil.shared.longvideoapi + h5ShareLinkUrl, parames: ["videoId": videoId, "pageSource": pageSource.rawValue, "h5WxRootPageSource": pageSource.rawValue]) { response, _, _, _ in
+            if response is NSNull || response == nil {
+                completeHander(nil, "获取分享地址失败")
+                return
+            }
+            completeHander(response as? String, nil)
+        }
+    }
+
+    class func wxFriendShareInfo(videoId: String, completeHander: @escaping (_ shareImagePath: String?, _ shareTitle: String?, _ shareWeappRawId: String?, _ mag: String?) -> Void) {
+        SWNetRequest.postRequestData(url: PQENVUtil.shared.longvideoapi + wxFriendUrl, parames: ["videoId": videoId]) { response, _, _, _ in
+            if response is NSNull || response == nil {
+                completeHander(nil, nil, nil, "获取分享好友数据失败")
+                return
+            }
+            let responseDic = response as! [String: String]
+            completeHander(responseDic["shareImgPath"], responseDic["shareTitle"], responseDic["shareWeappRawId"], nil)
+        }
+    }
+
+    /// 获取白名单设置
+    /// - Returns: <#description#>
+    class func datashowAllowData(completeHander: @escaping (_ isShowInfo: Bool, _ msg: String?) -> Void) {
+        SWNetRequest.postRequestData(url: PQENVUtil.shared.longvideoapi + datashowAllowUrl, parames: ["mid": getMachineCode()], encoding: JSONEncoding.default) { response, _, _, _ in
+            if response is NSNull || response == nil {
+                completeHander(false, "获取白名单数据失败")
+                return
+            }
+            completeHander((response as? Int) == 1, nil)
+        }
+    }
+    
+    
+    /// add by ak 取 STS token
+    /// - Parameter completeHander: completeHander description
+    class func getStsToken(completeHander: @escaping completeHander) {
+        SWNetRequest.postRequestData(url: PQENVUtil.shared.longvideoapi + getStsTokenUrl, parames: ["fileType": "2", "type": 1]) { response, _, error, _ in
+            if error != nil {
+                completeHander(nil, error?.msg)
+                return
+            }
+            completeHander(response as? [String: Any], nil)
+        }
+    }
+    
+    /// 获取OSS
+    /// - Parameter completeHander: <#completeHander description#>
+    /// - Returns: <#description#>
+    class func ossTempToken(completeHander: @escaping completeHander) {
+        SWNetRequest.postRequestData(url: PQENVUtil.shared.longvideoapi + ossTempTokenUrl, parames: ["type": "2", "fileType": "1"]) { response, _, _, _ in
+            completeHander(response as? [String: Any], nil)
+        }
+    }
+
+}
+
+// MARK: - 草稿箱相关
+
+/// 草稿箱相关
+extension PQBaseViewModel {
+    /// 保存草稿-进入创作工具调用
+    /// - Parameters:
+    ///   - draftboxId: 草稿ID
+    ///   - title: 草稿标题
+    ///   - coverUrl: 封面url
+    ///   - sdata: 结构化数据
+    ///   - videoFromScene:上传场景 1:普通上传 2:创作工具,3:普通上传转创作工具,4:后台转换加工,5:卡点视频制作
+    ///   - copyType:卡点视频制作再创作传 - 传3
+    ///   - originProjectId:卡点视频制作再创作传-源项目ID从那个项目做同款
+    ///   - completeHander: <#completeHander description#>
+    /// - Returns: <#description#>
+    class func saveDraftbox(draftboxId: String?, title: String?, coverUrl: String?, sdata: String, videoFromScene: videoFromScene,copyType:Int? = nil,originProjectId:String? = nil, completeHander: @escaping (_ draftboxInfo: [String: Any]?, _ msg: String?) -> Void) {
+        var parames: [String: Any] = ["sdata": sdata, "fromScene": videoFromScene.rawValue]
+        if draftboxId != nil {
+            parames["draftboxId"] = draftboxId
+        }
+        if copyType != nil && videoFromScene == .stuckPoint{
+            parames["copyType"] = 3
+        }
+        if originProjectId != nil && videoFromScene == .stuckPoint{
+            parames["originProjectId"] = originProjectId
+        }
+        if title != nil {
+            parames["title"] = title
+        }
+        if coverUrl != nil {
+            parames["coverUrl"] = coverUrl
+        }
+        PQLog(message: "保存草稿参数为:\(parames) \n\n sdata is:\n \(sdata) \n")
+        /* 返回数据格式
+         "coverUrl": "string",
+         "dataVersionCode": 0,
+         "draftboxId": "string",
+         "duration": 0,
+         "projectId": "string",
+         "title": "string",
+         "updateTimestamp": 0
+         */
+        SWNetRequest.postRequestData(url: PQENVUtil.shared.clipapiapi + saveDraftboxUrl, parames: parames) { response, _, error, _ in
+            if error != nil {
+                completeHander(nil, error?.msg)
+                return
+            }
+            PQLog(message: "保存草稿返回数据 :\(String(describing: response)))")
+            completeHander(response as? [String: Any], nil)
+        }
+    }
+
+    /// 保存项目-合成的视频上传完成后发布视频send之前调用
+    /// - Parameters:
+    ///   - projectId: <#projectId description#>
+    ///   - ossObjectKey: <#ossObjectKey description#>
+    ///   - draftboxId: <#draftboxId description#>
+    ///   - videoId: <#videoId description#>
+    ///   - sdata: <#sdata description#>
+    ///   - videoFromScene:上传场景 1:普通上传 2:创作工具,3:普通上传转创作工具,4:后台转换加工,5:卡点视频制作
+    ///   - completeHander: <#completeHander description#>
+    /// - Returns: <#description#>
+    class func saveProject(draftboxId: String?, sdata: String, videoFromScene: videoFromScene, completeHander: @escaping (_ projectId: String?, _ msg: String?) -> Void) {
+        var parames: [String: Any] = ["sdata": sdata,"fromScene": videoFromScene.rawValue]
+        if draftboxId != nil {
+            parames["draftboxId"] = draftboxId
+        }
+        SWNetRequest.postRequestData(url: PQENVUtil.shared.clipapiapi + saveProjectUrl, parames: parames) { response, _, error, _ in
+            if error != nil {
+                completeHander(nil, error?.msg)
+                return
+            }
+            PQLog(message: "生成的项目id :\(String(describing: response))")
+            completeHander(response as? String, nil)
+        }
+    }
+
+    /// 更新项目
+    /// - Parameter projectId: 项目 ID
+    /// - Parameter produceStatus: 合成状态 必传-合成状态(5:合成成功,99:合成失败)
+    /// - Parameter completeHander: 回调
+    class func updateProject(projectId: String?, produceStatus: String, completeHander: @escaping (_ projectId: String?, _ msg: String?) -> Void) {
+        var parames: [String: String] = ["produceStatus": produceStatus]
+
+        if projectId != nil {
+            parames["projectId"] = projectId
+        }
+        SWNetRequest.postRequestData(url: PQENVUtil.shared.clipapiapi + updateProjectUrl, parames: parames) { response, _, error, _ in
+            if error != nil {
+                completeHander(nil, error?.msg)
+                return
+            }
+            PQLog(message: "生成的项目id :\(String(describing: response))")
+            completeHander(response as? String, nil)
+        }
+    }
+
+    /// 发布视频后上报
+    /// - Parameters:
+    ///   - projectId: 项目ID
+    ///   - videoId: 视频ID
+    ///   - videoFromScene:上传场景 1:普通上传 2:创作工具,3:普通上传转创作工具,4:后台转换加工,5:卡点视频制作
+    ///   - completeHander: <#completeHander description#>
+    /// - Returns: <#description#>
+    class func reportSendVideo(_ projectId: String, _ videoId: String, videoFromScene: videoFromScene, completeHander: @escaping (_ isSeccess: Bool, _ msg: String?) -> Void) {
+        SWNetRequest.postRequestData(url: PQENVUtil.shared.clipapiapi + reportSendVideoUrl, parames: ["projectId": projectId, "videoId": videoId, "fromScene": videoFromScene.rawValue]) { _, _, error, _ in
+
+            PQLog(message: "发布视频后上报:projectId = \(projectId),videoId = \(videoId)")
+            if error != nil {
+                completeHander(false, error?.msg)
+                return
+            }
+            completeHander(true, nil)
+        }
+    }
+
+    /// 获取用户项目草稿箱数据
+    /// - Parameters:
+    ///   - lastTimestamp: 最后一条时间戳
+    ///   - pageSize: 每页大小
+    ///   - isSelected: 是否已选
+    ///   - completeHander: <#completeHander description#>
+    class func listUserDraftbox(lastTimestamp: Int, pageSize: Int = 10, isSelected: Bool = false, completeHander: @escaping (_ projectList: [PQEditProjectModel]?, _ msg: String?) -> Void) {
+        let params: [String: Any] = ["pageSize": pageSize, "lastTimestamp": lastTimestamp]
+        SWNetRequest.postRequestData(url: PQENVUtil.shared.clipapiapi + listUserDraftboxUrl, parames: params) { response, _, error, _ in
+            if error?.code == -1009 || error?.code == -1001 {
+                cShowHUB(superView: nil, msg: "网络不可用")
+            }
+            if response is NSNull || response == nil {
+                completeHander(nil, error?.msg)
+                return
+            }
+            var projectList = Array<PQEditProjectModel>.init()
+            let tempList = (response as? [[String: Any]])
+            if tempList != nil, (tempList?.count ?? 0) > 0 {
+                let draftDB: Realm = PQSingletoRealmUtil.shared.getDraftDB(uid: PQLoginUserInfo.shared.uid)
+                for item in tempList! {
+                    let tempModel: PQEditProjectModel? = Mapper<PQEditProjectModel>().map(JSON: item)
+                    tempModel?.isSelected = isSelected
+                    if tempModel != nil {
+                        let localData = PQSingletoRealmUtil.shared.reamlQueryObjects(realm: draftDB, PQEditProjectModel.self, filter: "draftboxId == '\(tempModel?.draftboxId ?? "")'")
+                        if localData != nil, (localData?.count ?? 0) > 0 {
+                            tempModel?.cacheDataVersionCode = (localData?.first as? PQEditProjectModel)?.dataVersionCode ?? 0
+                        }
+                        projectList.append(tempModel!)
+                    }
+                }
+            }
+            completeHander(projectList, "请求成功")
+        }
+    }
+
+    /// 获取草稿箱数量
+    /// - Parameter completeHander: <#completeHander description#>
+    /// - Returns: <#description#>
+    class func draftboxUserCount(completeHander: @escaping (_ draftboxCount: Int, _ msg: String?) -> Void) {
+        SWNetRequest.postRequestData(url: PQENVUtil.shared.clipapiapi + draftboxUserCountUrl, parames: nil) { response, _, error, _ in
+            if response is NSNull || response == nil {
+                completeHander(0, error?.msg)
+                return
+            }
+            completeHander((response as? Int) ?? 0, nil)
+        }
+    }
+
+    /// 删除草稿箱
+    /// - Parameters:
+    ///   - isBatch: 是否批量删除(多个用英文逗号分隔)
+    ///   - draftboxIds: 草稿id
+    ///   - completeHander: <#completeHander description#>
+    class func deleteDraftbox(isBatch: Bool = false, draftboxIds: String?, completeHander: @escaping (_ isSuccess: Bool, _ msg: String?) -> Void) {
+        var url: String = PQENVUtil.shared.clipapiapi
+        var params: [String: Any] = Dictionary<String, Any>.init()
+        if isBatch {
+            url = url + deleteMultiDraftboxUrl
+            params["draftboxIds"] = draftboxIds ?? ""
+        } else {
+            url = url + deleteDraftboxUrl
+            params["draftboxId"] = draftboxIds ?? ""
+        }
+        SWNetRequest.postRequestData(url: url, parames: params) { _, _, error, _ in
+            completeHander(error == nil, error?.msg)
+        }
+    }
+
+    /// 复制草稿箱
+    /// - Parameters:
+    ///   - draftboxId: 草稿箱id
+    ///   - title: 草稿箱标题
+    ///   - copyType: 复制类型(1:复制自己的项目,2:创建副本(复制别人的项目) 3:再创作)
+    ///   - completeHander: <#completeHander description#>
+    class func copyDraftbox(draftboxId: String?, title: String, copyType: Int, completeHander: @escaping (_ newDraftboxId: String?, _ msg: String?) -> Void) {
+        SWNetRequest.postRequestData(url: PQENVUtil.shared.clipapiapi + copyDraftboxUrl, parames: ["draftboxId": draftboxId ?? "", "title": title, "copyType": copyType]) { response, _, error, _ in
+            if response is NSNull || response == nil {
+                completeHander(nil, error?.msg)
+                return
+            }
+            completeHander(response as? String, nil)
+        }
+    }
+
+    /// 更新草稿名称
+    /// - Parameters:
+    ///   - draftboxId: 草稿id
+    ///   - title: 标题
+    ///   - completeHander: <#completeHander description#>
+    class func updateDraftBoxTitle(draftboxId: String?, title: String, completeHander: @escaping (_ newDraftData: PQEditProjectModel?, _ msg: String?) -> Void) {
+        SWNetRequest.postRequestData(url: PQENVUtil.shared.clipapiapi + updateDraftboxTitleUrl, parames: ["draftboxId": draftboxId ?? "", "title": title]) { response, _, error, _ in
+            if response is NSNull || response == nil {
+                completeHander(nil, error?.msg)
+                return
+            }
+            completeHander(Mapper<PQEditProjectModel>().map(JSON: response as! [String: Any]), nil)
+        }
+    }
+
+    /// 获取草稿箱结构化数据
+    /// - Parameters:
+    ///   - : <# description#>
+    ///   - completeHander: <#completeHander description#>
+    class func draftboxGetSdata(draftboxId: String?, completeHander: @escaping (_ projectModel: PQEditSdataModel?, _ msg: String?) -> Void) {
+        let params: [String: Any] = ["draftboxId": draftboxId ?? ""]
+        SWNetRequest.postRequestData(url: PQENVUtil.shared.clipapiapi + draftboxGetSdataUrl, parames: params) { response, _, error, _ in
+
+            if response is NSNull || response == nil {
+                completeHander(nil, error?.msg)
+                return
+            }
+            PQLog(message: "draftboxGetSdata response is \(String(describing: response))")
+
+            let oneProjectModel = Mapper<PQEditSdataModel>().map(JSONString: response as! String)
+
+            completeHander(oneProjectModel, "请求成功")
+        }
+    }
+
+    /// 素材上报扩展字段
+    /// - Parameter materialData: <#materialData description#>
+    /// - Returns: <#description#>
+    class func uploadReportExParams(isDownload: Bool, materialData: PQEditVisionTrackMaterialsModel?) -> [String: Any] {
+        var params: [String: Any] = ["draftboxId": PQSingletoMemoryUtil.shared.draftboxId ?? "", "materialType": materialData?.type ?? ""]
+        if !isDownload {
+            params["materialSource"] = (materialData?.localSearchId != nil && (materialData?.localSearchId ?? "").count > 0) ? "netMaterial" : "localMaterial"
+        }
+        if materialData?.id != nil, (materialData?.id ?? 0) > 0 {
+            params["materialId"] = materialData?.id ?? 0
+        }
+        if materialData?.materialUrl != nil, (materialData?.materialUrl.count ?? 0) > 0 {
+            params["materialUrl"] = materialData?.materialUrl ?? ""
+        }
+        if materialData?.locationPath != nil, materialData?.locationPath.count ?? 0 > 0 {
+            params["materialMD5"] = (contentMD5(path: documensDirectory + (materialData?.locationPath ?? ""), data: nil) ?? "")
+        }
+        PQLog(message: "素材上报扩展字段 = \(params),isDownload = \(isDownload)")
+        return params
+    }
+}

+ 75 - 0
BFFramework/Classes/Base/ViewModel/PQDownloadFileManager.swift

@@ -0,0 +1,75 @@
+//
+//  PQDownloadFileManager.swift
+//  PQSpeed
+//
+//  Created by SanW on 2020/12/9.
+//  Copyright © 2020 BytesFlow. All rights reserved.
+//
+
+import UIKit
+
+class PQDownloadFileManager: NSObject {
+    /// 创建文件
+    /// - Parameter url: 原地址
+    /// - Returns: <#description#>
+    class func createDownloadFilePath(url: String, fileExtensionType: FileExtensionType?) -> String {
+        let fileManager = FileManager.default
+        let filePath = downloadFileLocalPath(url: url, fileExtensionType: fileExtensionType)
+        if !fileManager.fileExists(atPath: filePath) {
+            let isFinished = fileManager.createFile(atPath: filePath, contents: nil, attributes: nil)
+            PQLog(message: "生成本地地址:\(url),localPath = \(filePath),isFinished:\(isFinished)")
+        } else {
+            PQLog(message: "已存在本地地址:\(url),localPath = \(filePath)")
+        }
+        return filePath
+    }
+
+    /// 获取文件本地存储地址
+    /// - Parameter url: 原地址
+    /// - Returns: <#description#>
+    class func downloadFileLocalPath(url: String, fileExtensionType: FileExtensionType?) -> String {
+        if url.hasPrefix(downloadDirectory) {
+            return url
+        }
+        let type: String = fileExtensionType?.rawValue ?? url.pathExtension
+        PQLog(message: "localPath : \(downloadDirectory + url.kf.md5 + (type.count > 0 ? ".\(type)" : ""))")
+        return downloadDirectory + url.kf.md5 + (type.count > 0 ? ".\(type)" : "")
+    }
+
+    /// 获取已缓存大小
+    /// - Parameter url: <#url description#>
+    /// - Returns: <#description#>
+    class func downloadFileLength(url: String, fileExtensionType: FileExtensionType?) -> Int64 {
+        let fileManager = FileManager.default
+        let filePath = downloadFileLocalPath(url: url, fileExtensionType: fileExtensionType)
+        if fileManager.fileExists(atPath: filePath) {
+            let att = try? fileManager.attributesOfItem(atPath: filePath)
+            return Int64((att?[FileAttributeKey.size] as? UInt64) ?? 0)
+        }
+        return 0
+    }
+
+    /// 移除已下载文件
+    /// - Parameter url: <#url description#>
+    /// - Returns: <#description#>
+    class func removeDownloadFile(url: String, fileExtensionType: FileExtensionType?) {
+        let fileManager = FileManager.default
+        var path = url
+        if !path.hasPrefix(downloadDirectory) {
+            path = downloadFileLocalPath(url: url, fileExtensionType: fileExtensionType)
+        }
+        if fileManager.fileExists(atPath: path) {
+            PQLog(message: "删除本地文件 == \(path)")
+            try? fileManager.removeItem(atPath: path)
+        }
+    }
+
+    /// 获取下载的总数量
+    /// - Returns: <#description#>
+    class func downloadTotalFile() -> [String]? {
+        let fileManager = FileManager.default
+        let subpaths = fileManager.subpaths(atPath: downloadDirectory)
+        PQLog(message: "已下载的总文件 == \(subpaths ?? [])")
+        return subpaths
+    }
+}

+ 225 - 0
BFFramework/Classes/Base/ViewModel/PQDownloadManager.swift

@@ -0,0 +1,225 @@
+//
+//
+//  PQDownloadManager.swift
+//  PQSpeed
+//
+//  Created by SanW on 2020/8/28.
+//  Copyright © 2020 BytesFlow. All rights reserved.
+//
+
+import UIKit
+
+// MARK: - 下载管理
+
+/// 下载管理
+
+class PQDownloadManager: NSObject {
+    static let shared = PQDownloadManager()
+    let maxDownloadCount: Int = 1000 // 最大的下载数量
+    var batchDownloadData: [String: Any] = Dictionary<String, Any>.init() // 批量下载数据
+    lazy var sessionManager: PQSessionManager = {
+        let sessionManager = PQSessionManager("downloadConfiguration")
+        return sessionManager
+    }()
+
+    /// <#Description#>
+    /// - Parameters:
+    ///   - url: <#url description#>
+    ///   - name: <#name description#>
+    ///   - pathExtension: <#name description#>
+    ///   - imageURL: <#imageURL description#>
+    ///   - progressHandle: <#progressHandle description#>
+    ///   - stateHandle: <#stateHandle description#>
+    /// - Returns: <#description#>
+    func download(url: String, name: String? = nil, fileExtensionType: FileExtensionType?, imageURL: String? = nil, progressHandle: @escaping ProgressHandle, stateHandle: @escaping StateHandle) {
+        PQLog(message: "开始下载文件:\(url)")
+        let subfile = PQDownloadFileManager.downloadTotalFile()
+        let newFileExtensionType: FileExtensionType = fileExtensionType ?? (FileExtensionType(rawValue: url.pathExtension) ?? FileExtensionType.normal)
+        if (subfile?.count ?? 0) > 0 && (subfile?.count ?? 0) > maxDownloadCount {
+            PQDownloadFileManager.removeDownloadFile(url: downloadDirectory + (subfile?.first)!, fileExtensionType: newFileExtensionType)
+        }
+        if !isValidURL(url: url) {
+            PQLog(message: "文件地址为空:\(url)")
+            return
+        }
+        let taskId = url.kf.md5
+        let absolutePath = url + ".\(newFileExtensionType.rawValue)"
+        let localPath = PQDownloadFileManager.downloadFileLocalPath(url: absolutePath, fileExtensionType: newFileExtensionType)
+        let downloadLenght: Int64 = PQDownloadFileManager.downloadFileLength(url: absolutePath, fileExtensionType: newFileExtensionType)
+        let downloadingTask: PQDownloadModel? = sessionManager.downloadTaskDatas.keys.contains(taskId) ? sessionManager.downloadTaskDatas[taskId] : nil
+        var totalLength: Int64 = 0
+        if downloadingTask != nil {
+            totalLength = downloadingTask?.totalLength ?? 0
+        }
+        if downloadLenght > 0, (downloadingTask != nil && downloadingTask?.state == .compelte) || (totalLength > 0 && totalLength > downloadLenght) {
+            progressHandle(1, downloadLenght, totalLength)
+            PQLog(message: "文件已下载完成:\(url),downloadLenght = \(downloadLenght),totalLength = \(totalLength)")
+            downloadingTask?.state = .compelte
+            downloadingTask?.progress = 1
+            postNotification(name: cDownloadMatrialSuccessKey, userInfo: ["code": "1", "url": url, "localPath": localPath, "fileExtensionType": newFileExtensionType])
+            stateHandle(.compelte, url, localPath, nil)
+            return
+        }
+        if downloadingTask != nil {
+            var request = URLRequest(url: URL(string: url)!)
+            request.setValue("bytes=%lld-\(downloadLenght)", forHTTPHeaderField: "Range")
+            request.setValue("Accept-Encoding", forHTTPHeaderField: "identity")
+            let task = sessionManager.session?.dataTask(with: request)
+            task?.taskUrl = url
+            task?.taskId = taskId
+            downloadingTask?.task = task
+            downloadingTask?.task?.resume()
+            PQLog(message: "下载任务已存在继续下载:\(url),localPath = \(localPath)")
+        } else {
+            createDirectory(path: downloadDirectory)
+            PQDownloadFileManager.removeDownloadFile(url: absolutePath, fileExtensionType: newFileExtensionType)
+            PQLog(message: "URL(string: url)! ==\(URL(string: url)!)")
+            var request = URLRequest(url: URL(string: url)!)
+//            request.setValue("bytes=%lld-\(downloadLenght)", forHTTPHeaderField: "Range")
+            request.setValue("Accept-Encoding", forHTTPHeaderField: "identity")
+            let task = sessionManager.session?.dataTask(with: request)
+            task?.taskUrl = url
+            task?.taskId = taskId
+            task?.resume()
+
+            let tempModel = PQDownloadModel()
+            tempModel.sourceURL = url
+            tempModel.fileExtensionType = newFileExtensionType
+            tempModel.progress = 0
+            tempModel.state = .downloading
+            tempModel.name = name
+            tempModel.imageURL = imageURL
+            tempModel.progressHandle = progressHandle
+            tempModel.stateHandle = stateHandle
+            tempModel.task = task
+            PQLog(message: "新建下载任务:\(url),localPath = \(PQDownloadFileManager.downloadFileLocalPath(url: url, fileExtensionType: newFileExtensionType)),taskID:\(taskId)  tempModel\(String(describing: tempModel.sourceURL))")
+            if !sessionManager.downloadTaskDatas.keys.contains(taskId) {
+                sessionManager.downloadTaskDatas[taskId] = tempModel
+            }
+        }
+    }
+
+    /// 批量下载
+    /// - Parameters:
+    ///   - uniqueId: 每个批量下载唯一id
+    ///   - urls: 需要下载urls
+    ///   - downloadHandle: <#downloadHandle description#>
+    /// - Returns: <#description#>
+    func batchDownload(uniqueId: String, urls: [PQDownloadModel], downloadHandle: @escaping (_ isSuccess: downloadState, _ msg: String?, _ data: [String: Any]?) -> Void) {
+        PQLog(message: "urls count is \(urls.count)")
+        if urls.count <= 0 {
+            return
+        }
+        if batchDownloadData.keys.contains(uniqueId) {
+            PQLog(message: "这组任务已经在下载中\(uniqueId)")
+            downloadHandle(.downloading, nil, nil)
+            return
+        }
+        let dispatchGroup = DispatchGroup()
+        var downloadInfo: [String: Any] = ["dispatchGroup": dispatchGroup]
+        for downloadUrl in urls {
+            if isValidURL(url: downloadUrl.sourceURL) {
+                DispatchQueue.global().async(group: dispatchGroup, execute: DispatchWorkItem(block: {
+                    dispatchGroup.enter()
+                    PQDownloadManager.shared.download(url: downloadUrl.sourceURL ?? "", fileExtensionType: downloadUrl.fileExtensionType) { _, _, _ in
+
+                    } stateHandle: { _, _, _, _ in
+                    }
+                }))
+            }
+        }
+        downloadInfo["urls"] = urls
+        downloadInfo["count"] = urls.count
+        PQDownloadManager.shared.batchDownloadData[uniqueId] = downloadInfo
+        dispatchGroup.notify(queue: DispatchQueue.main) {
+            PQLog(message: "所有的已请求完成,tempArr = \(PQDownloadManager.shared.batchDownloadData[uniqueId] ?? [])")
+            postNotification(name: cBatchDownloadMatrialSuccessKey, userInfo: ["uniqueId": uniqueId, "urls": PQDownloadManager.shared.batchDownloadData[uniqueId] ?? []])
+
+            PQDownloadManager.shared.batchDownloadData.removeValue(forKey: uniqueId)
+        }
+    }
+
+    /// 下载素材成功的通知
+    /// - Parameter notification: <#notification description#>
+    /// - Returns: <#description#>
+    @objc func downloadMarial(notification: Notification) {
+        let userInfo = notification.userInfo
+        let url = userInfo?["url"] as? String
+        let localPath = userInfo?["localPath"] as? String
+        let code = userInfo?["code"] as? String
+        PQLog(message: "目前下载的任务组数 \(PQDownloadManager.shared.batchDownloadData.keys)")
+        PQDownloadManager.shared.batchDownloadData.forEach { _, value in
+            var downloadInfo: [String: Any]? = value as? [String: Any]
+            let urlsArr: [PQDownloadModel]? = downloadInfo?["urls"] as? [PQDownloadModel]
+            let dispatchGroup: DispatchGroup? = downloadInfo?["dispatchGroup"] as? DispatchGroup
+            var count: Int = downloadInfo?["count"] as? Int ?? 0
+            if (urlsArr?.count ?? 0) > 0 {
+                urlsArr?.forEach { downloadModel in
+                    if downloadModel.sourceURL == url {
+                        if code == "1" {
+                            downloadModel.filePath = localPath
+                        }
+                        PQLog(message: "count = \(count)")
+                        if count > 0 {
+                            PQLog(message: "leave = \(count)")
+                            count = count - 1
+                            downloadInfo?["count"] = count
+                            let dispatchGroupCount = dispatchGroup.debugDescription.components(separatedBy: ",").filter { $0.contains("count") }.first?.components(separatedBy: CharacterSet.decimalDigits.inverted).compactMap { Int($0) }.first
+                            PQLog(message: "dispatchGroup count is \(String(describing: dispatchGroupCount))")
+                            dispatchGroup?.leave()
+                        }
+                    }
+                }
+            }
+        }
+    }
+
+    /// 下载背景音乐
+    /// - Parameters:
+    ///   - url: <#url description#>
+    ///   - completionHandler: <#completionHandler description#>
+    /// - Returns: <#description#>
+    class func downLoadFile(url: String, completionHandler: @escaping (_ filePath: String?, _ error: Error?) -> Void) {
+        // 创建目录
+        createDirectory(path: bgMusicDirectory)
+        let filePath = bgMusicDirectory + url.kf.md5 + ".mp3"
+        let data = try? Data(contentsOf: NSURL.fileURL(withPath: filePath))
+        if FileManager.default.fileExists(atPath: filePath) && (data?.count ?? 0) > 0 {
+            DispatchQueue.main.async {
+                completionHandler(filePath, nil)
+            }
+        } else {
+            let session = URLSession.shared
+            let request = URLRequest(url: URL(string: url)!, cachePolicy: .reloadIgnoringCacheData, timeoutInterval: 30.0)
+            let dataTask = session.downloadTask(with: request) { url, _, error in
+                if url != nil {
+                    if FileManager.default.fileExists(atPath: filePath) {
+                        try? FileManager.default.removeItem(atPath: filePath)
+                    }
+                    try? FileManager.default.moveItem(at: url!, to: URL(fileURLWithPath: filePath))
+                    DispatchQueue.main.async {
+                        completionHandler(filePath, nil)
+                    }
+                } else {
+                    DispatchQueue.main.async {
+                        completionHandler(nil, error)
+                    }
+                }
+            }
+            dataTask.resume()
+        }
+    }
+
+    override private init() {
+        super.init()
+        addNotification(self, selector: #selector(downloadMarial(notification:)), name: cDownloadMatrialSuccessKey, object: nil)
+    }
+
+    override func copy() -> Any {
+        return self
+    }
+
+    override func mutableCopy() -> Any {
+        return self
+    }
+}

+ 188 - 0
BFFramework/Classes/Base/ViewModel/PQSessionManager.swift

@@ -0,0 +1,188 @@
+//
+//  PQSessionManager.swift
+//  PQSpeed
+//
+//  Created by SanW on 2020/12/8.
+//  Copyright © 2020 BytesFlow. All rights reserved.
+//
+
+import UIKit
+
+class PQSessionManager: NSObject {
+    @Atomic var downloadTaskDatas: [String: PQDownloadModel] = Dictionary<String, PQDownloadModel>.init()
+    public var dispatchQueue: DispatchQueue! // 线程
+    public var session: URLSession? // sesstion
+    public var identifier: String = "" // 标示
+    public var completionHandler: (() -> Void)? // 完成回调
+    public lazy var configuration: URLSessionConfiguration = { // sesstion 配置
+        //        let configuration = URLSessionConfiguration.background(withIdentifier: identifier)
+        let configuration = URLSessionConfiguration.default
+        configuration.isDiscretionary = false
+        configuration.timeoutIntervalForRequest = 30 // 请求超时时长,默认是60s
+        configuration.timeoutIntervalForResource = 7 * 24 * 60 * 60 // 资源超时时长,默认是7天,也就是说资源要在7天内到达
+        configuration.httpMaximumConnectionsPerHost = 20
+        configuration.allowsCellularAccess = true // 是否支持蜂窝网络下载
+        if #available(iOS 13, *) {
+            configuration.allowsConstrainedNetworkAccess = true // 是否使用受限制的网络
+            configuration.allowsExpensiveNetworkAccess = true // 是否使用昂贵的网络
+        }
+        return configuration
+    }()
+
+    public init(_ identifier: String,
+                configuration: URLSessionConfiguration? = nil,
+                dispatchQueue: DispatchQueue = DispatchQueue(label: "com.piaoquan.pqspeed.dispatchQueue"))
+    {
+        super.init()
+        let bundleIdentifier = Bundle.main.bundleIdentifier ?? "com.piaoquan.pqspeed"
+        if configuration != nil {
+            self.configuration = configuration!
+        }
+        self.identifier = "\(bundleIdentifier).\(identifier)"
+        self.dispatchQueue = dispatchQueue
+        createSession()
+    }
+
+    func createSession() {
+        let delegateQueue = OperationQueue(maxConcurrentOperationCount: 4, underlyingQueue: dispatchQueue, name: "com.piaoquan.pqspeed.delegateQueue")
+        session = URLSession(configuration: configuration, delegate: self, delegateQueue: delegateQueue)
+    }
+}
+
+extension PQSessionManager: URLSessionDataDelegate {
+    /// 收到请求响应
+    /// - Parameters:
+    ///   - session: <#session description#>
+    ///   - dataTask: <#dataTask description#>
+    ///   - response: <#response description#>
+    ///   - completionHandler: <#completionHandler description#>
+    func urlSession(_: URLSession, dataTask: URLSessionDataTask, didReceive response: URLResponse, completionHandler: @escaping (URLSession.ResponseDisposition) -> Void) {
+        let taskId = dataTask.taskId
+        let taskUrl = dataTask.taskUrl
+        let downloadData = downloadTaskDatas[taskId]
+        let fileExtensionType: FileExtensionType = downloadData?.fileExtensionType ?? (FileExtensionType(rawValue: taskUrl.pathExtension) ?? FileExtensionType.normal)
+        let absolutePath = taskUrl + ".\(fileExtensionType.rawValue)"
+        let expectedLength: Int64 = response.expectedContentLength
+        let downloadedLength: Int64 = PQDownloadFileManager.downloadFileLength(url: absolutePath, fileExtensionType: fileExtensionType)
+        let totalLength: Int64 = expectedLength
+        if totalLength == 0 {
+            if downloadData?.progressHandle != nil {
+                downloadData?.progressHandle!(0, downloadedLength, totalLength)
+            }
+            if downloadData?.stateHandle != nil {
+                downloadData?.stateHandle!(.error, taskUrl, PQDownloadFileManager.downloadFileLocalPath(url: absolutePath, fileExtensionType: fileExtensionType), PQError(msg: "下载文件为空", code: 0))
+            }
+            PQLog(message: "收到下载请求-下载文件为空:\(taskUrl),localPath = \(PQDownloadFileManager.downloadFileLocalPath(url: absolutePath, fileExtensionType: fileExtensionType))")
+            return
+        }
+        // 创建文件地址
+        let filePath = PQDownloadFileManager.createDownloadFilePath(url: absolutePath, fileExtensionType: fileExtensionType)
+        let fileHandle: FileHandle? = FileHandle(forWritingAtPath: filePath)
+        downloadData?.downloadLength = downloadedLength
+        downloadData?.totalLength = totalLength
+        downloadData?.fileHandle = fileHandle
+        downloadData?.filePath = filePath
+        downloadData?.mimeType = response.mimeType
+        if downloadData?.name == nil {
+            downloadData?.name = response.suggestedFilename
+        }
+        PQLog(message: "收到下载请求-下载文件:downloadedLength = \(downloadedLength),localPath = \(filePath),totalLength = \(totalLength),expectedLength = \(expectedLength)")
+        completionHandler(.allow)
+    }
+
+    /// 收到数据
+    /// - Parameters:
+    ///   - session: <#session description#>
+    ///   - dataTask: <#dataTask description#>
+    ///   - data: <#data description#>
+    func urlSession(_: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) {
+        let taskId = dataTask.taskId
+        let taskUrl = dataTask.taskUrl
+        let downloadData = downloadTaskDatas[taskId]
+        let fileExtensionType: FileExtensionType = downloadData?.fileExtensionType ?? (FileExtensionType(rawValue: taskUrl.pathExtension) ?? FileExtensionType.normal)
+        let absolutePath = taskUrl + ".\(fileExtensionType.rawValue)"
+        PQLog(message: "收到下载请求-收到data taskId = \(taskId),data = \(data),totalBytes = \(downloadData?.totalLength ?? 0),download = \(PQDownloadFileManager.downloadFileLength(url: absolutePath, fileExtensionType: fileExtensionType))")
+
+        if downloadData != nil {
+            downloadData?.fileHandle?.seekToEndOfFile()
+            downloadData?.fileHandle?.write(data)
+            downloadData?.downloadLength = (downloadData?.downloadLength ?? 0) + Int64(data.count)
+            let progress = Float(downloadData?.downloadLength ?? 0) / Float(downloadData?.totalLength ?? 1)
+            downloadData?.progress = progress
+            downloadData?.state = .downloading
+            if downloadData?.progressHandle != nil {
+                downloadData?.progressHandle!(progress, downloadData?.downloadLength, downloadData?.totalLength)
+            }
+            if downloadData?.stateHandle != nil {
+                downloadData?.stateHandle!(.downloading, taskUrl, PQDownloadFileManager.downloadFileLocalPath(url: absolutePath, fileExtensionType: fileExtensionType), nil)
+            }
+        }
+    }
+
+    /// 下载完成/终止
+    /// - Parameters:
+    ///   - session: <#session description#>
+    ///   - task: <#task description#>
+    ///   - error: <#error description#>
+    func urlSession(_: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
+        let taskId = task.taskId
+        let taskUrl = task.taskUrl
+        let downloadData = downloadTaskDatas[taskId]
+        var fileExtensionType: FileExtensionType = downloadData?.fileExtensionType ?? (FileExtensionType(rawValue: taskUrl.pathExtension) ?? FileExtensionType.normal)
+        let absolutePath = taskUrl + ".\(fileExtensionType.rawValue)"
+        downloadData?.fileHandle?.closeFile()
+        PQLog(message: "收到下载请求-下载完成 taskId = \(taskId),error = \(error ?? PQError(msg: ""))")
+        downloadData?.fileHandle = nil
+        if downloadData?.realFileExtensionType != nil, downloadData?.fileExtensionType != nil, downloadData?.realFileExtensionType != downloadData?.fileExtensionType {
+            try? FileManager.default.moveItem(atPath: PQDownloadFileManager.downloadFileLocalPath(url: absolutePath, fileExtensionType: fileExtensionType), toPath: PQDownloadFileManager.downloadFileLocalPath(url: absolutePath, fileExtensionType: downloadData?.realFileExtensionType))
+            fileExtensionType = (downloadData?.realFileExtensionType)!
+        }
+
+        // mdf by ak 当下载的视频 FPS 不是30帧时 要处理到30 FPS
+        if downloadData?.fileExtensionType == .mp4, AVURLAsset(url: URL(fileURLWithPath: PQDownloadFileManager.downloadFileLocalPath(url: absolutePath, fileExtensionType: fileExtensionType)), options: avAssertOptions).tracks(withMediaType: .video).first?.nominalFrameRate != 30 {
+            let oldVideoPath = PQDownloadFileManager.downloadFileLocalPath(url: absolutePath, fileExtensionType: fileExtensionType)
+
+            let outVideoPath = downloadDirectory + "export_temp" + oldVideoPath.replacingOccurrences(of: downloadDirectory, with: "")
+
+            PQLog(message: "下载视频 FPS 不是30帧 要处理 oldVideoPath is \(oldVideoPath) \n outVideoPath is\(outVideoPath)")
+
+            NXAVAssetExportSession().exportAsynchronouslyWithCompletionHandler(inFilePath: oldVideoPath, outFilePath: outVideoPath, frameDuration: CMTime(value: 1, timescale: 30)) { _ in
+
+                do {
+                    // 1,delete old file
+                    try FileManager.default.removeItem(at: URL(fileURLWithPath: oldVideoPath))
+                    // 2,用新文件覆盖老文件路径
+                    try FileManager.default.moveItem(atPath: outVideoPath, toPath: oldVideoPath)
+
+                } catch {
+                    // No-op
+                }
+
+                PQLog(message: "clear data movie outFilePath is \(String(describing: oldVideoPath))")
+
+                postNotification(name: cDownloadMatrialSuccessKey, userInfo: ["code": error != nil ? "0" : "1", "url": task.taskUrl, "localPath": error != nil ? "" : PQDownloadFileManager.downloadFileLocalPath(url: absolutePath, fileExtensionType: fileExtensionType), "fileExtensionType": fileExtensionType])
+                if downloadData?.stateHandle != nil {
+                    if error != nil {
+                        downloadData?.state = .error
+                        downloadData?.stateHandle!(.error, taskUrl, PQDownloadFileManager.downloadFileLocalPath(url: absolutePath, fileExtensionType: fileExtensionType), PQError(msg: error?.localizedDescription, code: 0))
+                    } else {
+                        downloadData?.state = .compelte
+                        downloadData?.stateHandle!(.compelte, taskUrl, PQDownloadFileManager.downloadFileLocalPath(url: absolutePath, fileExtensionType: fileExtensionType), nil)
+                    }
+                }
+            }
+
+        } else {
+            postNotification(name: cDownloadMatrialSuccessKey, userInfo: ["code": error != nil ? "0" : "1", "url": task.taskUrl, "localPath": error != nil ? "" : PQDownloadFileManager.downloadFileLocalPath(url: absolutePath, fileExtensionType: fileExtensionType), "fileExtensionType": fileExtensionType])
+            if downloadData?.stateHandle != nil {
+                if error != nil {
+                    downloadData?.state = .error
+                    downloadData?.stateHandle!(.error, taskUrl, PQDownloadFileManager.downloadFileLocalPath(url: absolutePath, fileExtensionType: fileExtensionType), PQError(msg: error?.localizedDescription, code: 0))
+                } else {
+                    downloadData?.state = .compelte
+                    downloadData?.stateHandle!(.compelte, taskUrl, PQDownloadFileManager.downloadFileLocalPath(url: absolutePath, fileExtensionType: fileExtensionType), nil)
+                }
+            }
+        }
+    }
+}

+ 121 - 0
BFFramework/Classes/Base/ViewModel/PQUploadViewModel.swift

@@ -0,0 +1,121 @@
+//
+//  PQUploadViewModel.swift
+//  PQSpeed
+//
+//  Created by SanW on 2020/8/4.
+//  Copyright © 2020 BytesFlow. All rights reserved.
+//
+
+import UIKit
+
+class PQUploadViewModel: NSObject {
+    /// 发布视频
+    /// - Parameters:
+    ///   - projectId 项目ID-发布创作的视频时必传,会在进入创作工具页时生成,以app_no_projectdata为前缀
+    ///   - fileExtensions 视频封装格式 -必传
+    ///   - title: 标题
+    ///   - videoPath: 视频地址
+    ///   - coverImgPath: 封面地址
+    ///   - descr: 描述
+    ///   - videoFromScene 视频来源场景 1:普通上传 2:创作工具,3:普通上传转创作工具,4:后台转换加工,5:卡点视频制作
+    /// - Returns: <#description#>
+    class func publishVideo(projectId: String?, fileExtensions: String?, title: String, videoPath: String, coverImgPath: String, descr: String, videoFromScene: videoFromScene, reCreateData: PQReCreateModel?, eventTrackData: PQVideoMakeEventTrackModel?, completeHander: @escaping (_ videoData: PQVideoListModel?, _ jsonDict: [String: Any]?, _ msg: String?) -> Void) {
+        PQLog(message: "AKAKAAKprojectId is\(String(describing: projectId)) videoFromScene is \(videoFromScene)")
+        SWNetRequest.postRequestData(url: PQENVUtil.shared.longvideoapi + videoSendUrl, parames: ["title": title, "fileExtensions": fileExtensions ?? "application/octet-stream", "videoPath": videoPath, "coverImgPath": coverImgPath, "descr": descr, "viewStatus": 1, "produceProjectId": (projectId ?? "") as String, "videoFromScene": videoFromScene.rawValue]) { response, _, error, _ in
+            if error != nil {
+                completeHander(nil, nil, error?.msg)
+                return
+            }
+            var jsonDict = (response as! [String: Any])
+            jsonDict["title"] = title
+            jsonDict["auditStatus"] = 1
+            jsonDict["uid"] = PQLoginUserInfo.shared.uid
+            let tempModel = PQVideoListModel(jsonDict: jsonDict)
+            tempModel.uid = Int(PQLoginUserInfo.shared.uid) ?? 0
+            tempModel.auditStatus = 1
+            completeHander(tempModel, jsonDict, nil)
+            // 发布成功
+            var extParams: [String: Any] = ["source": (projectId != nil && (projectId?.count ?? 0) > 0) ? "videoCompose" : "videoUpload", "projectId": projectId ?? ""]
+            if reCreateData != nil {
+                extParams["projectId"] = reCreateData?.projectId ?? ""
+                extParams["parentVideoId"] = reCreateData?.videoId ?? ""
+                extParams["parentProjectId"] = reCreateData?.parentProjectId ?? ""
+                extParams["rootProjectId"] = reCreateData?.rootProjectId ?? ""
+            }
+            PQEventTrackViewModel.baseReportUpload(businessType: .bt_up_process, objectType: .ot_up_publishSuccess, pageSource: nil, extParams: extParams, remindmsg: "上传相关")
+            if projectId != nil {
+                PQBaseViewModel.reportSendVideo(projectId!, tempModel.uniqueId ?? "",videoFromScene: videoFromScene) { isSuccess, _ in
+                    if !isSuccess {
+                        PQBaseViewModel.reportSendVideo(projectId!, tempModel.uniqueId ?? "",videoFromScene: videoFromScene) { isSuccess, _ in
+                            if !isSuccess {
+                                PQBaseViewModel.reportSendVideo(projectId!, tempModel.uniqueId ?? "",videoFromScene: videoFromScene) { isSuccess, _ in
+                                    if !isSuccess {}
+                                }
+                            }
+                        }
+                    }
+                }
+            }
+            if eventTrackData != nil {
+                eventTrackData?.title = title
+                eventTrackData?.videoDes = descr
+                eventTrackData?.videoId = tempModel.uniqueId
+                eventTrackData?.coverUrl = coverImgPath
+                PQEventTrackViewModel.baseReportUpload(logType: .st_log_type_videoProduction, businessType: nil, objectType: nil, pageSource: nil, eventData: eventTrackData?.toParams(), remindmsg: "创作工具埋点上报")
+            }
+        }
+    }
+
+    /// 修改视频
+    /// - Parameters:
+    ///   - title: 标题
+    ///   - videoId: 视频id
+    ///   - coverImgPath: 图片地址
+    ///   - descr: 描述
+    ///   - completeHander: <#completeHander description#>
+    /// - Returns: <#description#>
+    class func updateVideo(title: String, videoId: String, coverImgPath: String, descr: String, completeHander: @escaping (_ videoData: PQVideoListModel?, _ msg: String?) -> Void) {
+        SWNetRequest.postRequestData(url: PQENVUtil.shared.longvideoapi + updateVideoUrl, parames: ["title": title, "videoId": videoId, "coverImgPath": coverImgPath, "descr": descr, "viewStatus": 1, "barrageSwitch": 1]) { response, _, error, _ in
+            if error != nil {
+                completeHander(nil, error?.msg)
+                return
+            }
+            // 1015
+            let tempModel = PQVideoListModel(jsonDict: response as! [String: Any])
+            tempModel.auditStatus = 1
+            completeHander(tempModel, nil)
+        }
+    }
+
+    /// 获取视频封面
+    /// - Parameters:
+    ///   - videoId: 视频ID
+    ///   - videoPath: 视频地址
+    ///   - totalTime: 总时长
+    ///   - completeHander: <#completeHander description#>
+    /// - Returns: <#description#>
+    class func vodeoCoverImageList(videoId: String, videoPath: String, totalTime: Int, videoHeight: CGFloat, videoWidth: CGFloat, completeHander: @escaping (_ coverImages: [PQUploadModel]?, _ msg: String?) -> Void) {
+        SWNetRequest.postRequestData(url: PQENVUtil.shared.longvideoapi + vodeoCoverImageUrl, parames: ["id": videoId, "videoPath": videoPath, "totalTime": totalTime]) { response, _, error, _ in
+            if error != nil {
+                completeHander(nil, error?.msg)
+                return
+            }
+            var coverImages: [PQUploadModel] = Array<PQUploadModel>.init()
+            let data: [String: Any]? = response as? [String: Any]
+            if data != nil, data?.keys.contains("videoCoverImages") ?? false {
+                let videoCoverImages: [String]? = data?["videoCoverImages"] as? [String]
+                if videoCoverImages != nil, (videoCoverImages?.count ?? 0) > 0 {
+                    for item in videoCoverImages! {
+                        let tempItem = PQUploadModel()
+                        tempItem.contentMode = .scaleAspectFit
+                        tempItem.videoHeight = videoHeight
+                        tempItem.videoWidth = videoWidth
+                        tempItem.imageUrl = item
+                        coverImages.append(tempItem)
+                    }
+                }
+            }
+            completeHander(coverImages, nil)
+        }
+    }
+}

+ 109 - 0
BFFramework/Classes/Categorys/Int+Ext.swift

@@ -0,0 +1,109 @@
+//
+//  Int+Ext.swift
+//  PQSpeed
+//
+//  Created by SanW on 2020/7/20.
+//  Copyright © 2020 BytesFlow. All rights reserved.
+//
+
+import Foundation
+
+extension Int {
+    /// 分数字格式化如413200->4,132.09
+
+    /// 是否保留2位小数
+    /// - Parameter isDecimal: <#isDecimal description#>
+    /// - Returns: <#description#>
+    func paraseDecimalFormatterValue(isDecimal: Bool = false) -> String? {
+        let decimal = self % 100
+        let nonDecimal = self / 100
+        let formatter = NumberFormatter()
+        formatter.numberStyle = .decimal
+        if let title = formatter.string(from: NSNumber(value: nonDecimal)) {
+            if isDecimal {
+                return title + String(format: ".%02d", decimal)
+            } else {
+                return title
+            }
+        }
+        return nil
+    }
+
+    /// 改变单位为万
+    /// @param originUnit <#originUnit description#>
+    func changeUnit() -> String {
+        var unitStr: String = ""
+        if self < 10000 {
+            unitStr = "\(self)"
+        } else if self <= 1_000_000 {
+            let divisor = pow(10.0, Double(1))
+            let decimal = ((Double(self) / 10000) * divisor).rounded() / divisor
+            unitStr = "\(decimal)万"
+        } else {
+            unitStr = "\(self / 10000)万"
+        }
+        PQLog(message: "转化单位:\(self) = \(unitStr)")
+        return unitStr
+    }
+}
+
+// MARK: - Float64 double类型扩展
+
+/// Float64 double类型扩展
+
+extension Float64 {
+    /// 时长转化为分秒 62'52"
+    /// - Returns: <#description#>
+    func formatDurationToMS() -> String {
+        let duration = lround(self)
+        var text = ""
+        let min = duration / 60
+        let second = duration % 60
+        if min > 0, second > 0 {
+            text = "\(min)" + "\'" + "\(second)" + "\""
+        } else if min > 0 {
+            text = "\(min)" + "\'" + "0\""
+        } else {
+            text = "\(second)" + "\""
+        }
+        return text
+    }
+
+    /// 时长转化成时分秒 01:02:52
+    /// - Parameter value: <#value description#>
+    /// - Returns: <#description#>
+    func formatDurationToHMS() -> String {
+        var theTime = lround(self)
+        var theTime1 = 0 // 分
+        var theTime2 = 0 // 小时
+        if theTime < 60 {
+            if theTime < 10 {
+                return "00:0\(theTime)"
+            }
+            return "00:\(theTime)"
+        }
+        theTime1 = theTime / 60
+        theTime = theTime % 60
+        if theTime1 > 60 {
+            theTime2 = theTime1 / 60
+            theTime1 = theTime1 % 60
+        }
+        var result = "\(theTime)"
+        if theTime < 10 {
+            result = "0" + result
+        }
+        if theTime1 > 0 {
+            result = "\(theTime1):" + result
+            if theTime1 < 10 {
+                result = "0" + result
+            }
+        }
+        if theTime2 > 0 {
+            result = "\(theTime2):" + result
+            if theTime2 < 10 {
+                result = "0" + result
+            }
+        }
+        return result
+    }
+}

+ 169 - 0
BFFramework/Classes/Categorys/String+Ext.swift

@@ -0,0 +1,169 @@
+//
+//  String+Ext.swift
+//  PQSpeed
+//
+//  Created by SanW on 2020/7/22.
+//  Copyright © 2020 BytesFlow. All rights reserved.
+//
+
+import Foundation
+import MobileCoreServices
+
+extension String {
+    // 文件后缀名
+    var pathExtension: String {
+        return (self as NSString).pathExtension
+    }
+
+    func enumerateSearchText(searchText: String?, complate: (_ idx: Int, _ range: NSRange) -> Void) {
+        if searchText == nil || (searchText?.count ?? 0) <= 0 || !contains(searchText!) {
+            return
+        }
+        let len: Int = searchText!.count
+        var loc: Int = 0
+        let subStrings: [String] = NSString(string: self).components(separatedBy: searchText!)
+        for (idx, subText) in subStrings.enumerated() {
+            loc = loc + subText.count
+            let range = NSRange(location: loc, length: len)
+            complate(idx, range)
+            loc = loc + len
+            if idx == subStrings.count - 2 {
+                break
+            }
+        }
+    }
+
+    func attributedTextWithSearchText(searchText: String?, textColor: UIColor, textFont: UIFont, searchTextColor: UIColor, searchTextFont: UIFont) -> NSMutableAttributedString {
+        let attbText = NSMutableAttributedString(string: self, attributes: [NSAttributedString.Key.font: textFont, NSAttributedString.Key.foregroundColor: textColor])
+        if count <= 0 || searchText == nil || (searchText?.count ?? 0) <= 0 {
+            return attbText
+        }
+        for tempStr in searchText! {
+            PQLog(message: "self = \(self),searchText = \(searchText ?? ""),tempStr = \(tempStr)")
+            enumerateSearchText(searchText: "\(tempStr)") { _, range in
+                attbText.setAttributes([NSAttributedString.Key.font: searchTextFont, NSAttributedString.Key.foregroundColor: searchTextColor], range: range)
+            }
+        }
+        return attbText
+    }
+
+    // 判断是否为空
+    var isSpace: Bool {
+        return allSatisfy { $0.isWhitespace }
+    }
+
+    /// 通过 文件路径/文件名/文件后缀 获取mimeType(文件媒体类型)
+    /// - Parameter pathExtension: <#pathExtension description#>
+    /// - Returns: <#description#>
+    func mimeType() -> String {
+        if let uti = UTTypeCreatePreferredIdentifierForTag(kUTTagClassFilenameExtension, (self as NSString).pathExtension as CFString, nil)?.takeRetainedValue() {
+            if let mimetype = UTTypeCopyPreferredTagWithClass(uti, kUTTagClassMIMEType)?
+                .takeRetainedValue()
+            {
+                return mimetype as String
+            }
+        }
+        return "application/octet-stream"
+    }
+
+    // 将原始的url编码为合法的url
+    func urlEncoded() -> String {
+        let encodeUrlString = addingPercentEncoding(withAllowedCharacters:
+            .urlQueryAllowed)
+        return encodeUrlString ?? ""
+    }
+
+    // 将编码后的url转换回原始的url
+    func urlDecoded() -> String {
+        return removingPercentEncoding ?? ""
+    }
+
+    // 判断是否包含Emoji表情
+    func isEmoji() -> Bool {
+        let numbers = ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9"]
+        guard !numbers.contains(self) else {
+            return false
+        }
+        let tempEmoji: [Unicode.Scalar] = ["\u{26F8}", "\u{26F7}", "\u{263A}", "\u{2639}", "\u{2620}", "\u{270C}", "\u{261D}", "\u{0001F441}", "\u{0001F5E3}", "\u{0001F575}", "\u{0001F574}", "\u{26D1}", "\u{0001F576}", "\u{0001F577}", "\u{0001F54A}", "\u{0001F43F}", "\u{2618}", "\u{0001F32A}", "\u{2600}", "\u{0001F324}", "\u{0001F325}", "\u{2601}", "\u{0001F326}", "\u{0001F327}", "\u{26C8}", "\u{0001F329}", "\u{0001F328}", "\u{2744}", "\u{2603}", "\u{0001F32C}", "\u{2602}", "\u{0001F32B}", "\u{0001F336}", "\u{0001F37D}"]
+        let scalars = unicodeScalars.map { $0.value }
+        for element in scalars {
+            if let scalar = Unicode.Scalar(element) {
+                if #available(iOS 10.2, *) {
+                    if (scalar.properties.isEmoji && scalar.properties.isEmojiPresentation) || tempEmoji.contains(scalar) {
+                        return true
+                    } else {
+                        PQLog(message: "是表情==\(element),\(scalar)")
+                    }
+                }
+            }
+        }
+        return false
+    }
+
+    var isContainsEmoji: Bool {
+        for scalar in unicodeScalars {
+            switch scalar.value {
+            case 0x1F600...0x1F64F, // Emoticons
+                 0x1F300...0x1F5FF, // Misc Symbols and Pictographs
+                 0x1F680...0x1F6FF, // Transport and Map
+                 0x2600...0x26FF, // Misc symbols
+                 0x2700...0x27BF, // Dingbats
+                 0xFE00...0xFE0F: // Variation Selectors
+                return true
+            default:
+                continue
+            }
+        }
+        return false
+    }
+
+    // 是否包含表情
+    var containsEmoji: Bool {
+        for scalar in unicodeScalars {
+            switch scalar.value {
+            case
+                0x00A0...0x00AF,
+                0x2030...0x204F,
+                0x2120...0x213F,
+                0x2190...0x21AF,
+                0x2310...0x329F,
+                0x1F000...0x1F9CF:
+                return true
+            default:
+                continue
+            }
+        }
+        return false
+    }
+
+    /**
+     * 字母、数字、中文正则判断(不包括空格)
+     *注意: 因为考虑到输入习惯,许多人习惯使用九宫格,这里在正常选择全键盘输入错误的时候,进行九宫格判断,九宫格对应的是下面➋➌➍➎➏➐➑➒的字符
+     */
+    static func isInputRuleNotBlank(str: String) -> Bool {
+        let pattern = "^[a-zA-Z\\u4E00-\\u9FA5\\d]*$"
+        let pred = NSPredicate(format: "SELF MATCHES %@", pattern)
+        let isMatch = pred.evaluate(with: str)
+        if !isMatch {
+            let other = "➋➌➍➎➏➐➑➒"
+            let len = str.count
+            for i in 0..<len {
+                let tmpStr = str as NSString
+                let tmpOther = other as NSString
+                let c = tmpStr.character(at: i)
+
+                if !(isalpha(Int32(c)) > 0 || isalnum(Int32(c)) > 0 || (Int(c) == "_".hashValue) || (Int(c) == "-".hashValue) || (c >= 0x4E00 && c <= 0x9FA6) || (tmpOther.range(of: str).location != NSNotFound)) {
+                    return false
+                }
+                return true
+            }
+        }
+        return isMatch
+    }
+}
+
+extension Optional where Wrapped == String {
+    var isSpace: Bool {
+        return self?.isSpace ?? true
+    }
+}

+ 63 - 0
BFFramework/Classes/Categorys/UIButton+ext.swift

@@ -0,0 +1,63 @@
+//
+//  UIButton+ext.swift
+//  PQSpeed
+//
+//  Created by ak on 2020/8/14.
+//  Copyright © 2020 BytesFlow. All rights reserved.
+//
+/**
+ UIButton图像文字同时存在时---图像相对于文字的位置
+
+ - top:    图像在上
+ - left:   图像在左
+ - right:  图像在右
+ - bottom: 图像在下
+ */
+enum PQButtonImageEdgeInsetsStyle {
+    case top, left, right, bottom
+}
+
+import Foundation
+extension UIButton {
+    func imagePosition(at style: PQButtonImageEdgeInsetsStyle, space: CGFloat) {
+        guard let imageV = imageView else { return }
+        guard let titleL = titleLabel else { return }
+        // 获取图像的宽和高
+        let imageWidth = imageV.frame.size.width
+        let imageHeight = imageV.frame.size.height
+        // 获取文字的宽和高
+        let labelWidth = titleL.frame.size.width
+        let labelHeight = titleL.frame.size.height
+
+        var imageEdgeInsets = UIEdgeInsets.zero
+        var labelEdgeInsets = UIEdgeInsets.zero
+        // UIButton同时有图像和文字的正常状态---左图像右文字,间距为0
+        switch style {
+        case .left:
+            // 正常状态--只不过加了个间距
+            imageEdgeInsets = UIEdgeInsets(top: 0, left: -space * 0.5, bottom: 0, right: space * 0.5)
+            labelEdgeInsets = UIEdgeInsets(top: 0, left: space * 0.5, bottom: 0, right: -space * 0.5)
+        case .right:
+            // 切换位置--左文字右图像
+            // 图像:UIEdgeInsets的left是相对于UIButton的左边移动了labelWidth + space * 0.5,right相对于label的左边移动了-labelWidth - space * 0.5
+            imageEdgeInsets = UIEdgeInsets(top: 0, left: labelWidth + space * 0.5, bottom: 0, right: -labelWidth - space * 0.5)
+            labelEdgeInsets = UIEdgeInsets(top: 0, left: -imageWidth - space * 0.5, bottom: 0, right: imageWidth + space * 0.5)
+        case .top:
+            // 切换位置--上图像下文字
+            /** 图像的中心位置向右移动了labelWidth * 0.5,向上移动了-imageHeight * 0.5 - space * 0.5
+              *文字的中心位置向左移动了imageWidth * 0.5,向下移动了labelHeight*0.5+space*0.5
+             */
+            imageEdgeInsets = UIEdgeInsets(top: -(imageHeight * 0.5 - space), left: labelWidth * 0.5, bottom: imageHeight * 0.5 - space, right: -labelWidth * 0.5)
+            labelEdgeInsets = UIEdgeInsets(top: labelHeight * 0.5 + space * 2, left: -imageWidth * 0.5, bottom: -(labelHeight * 0.5 + space * 2), right: imageWidth * 0.5)
+        case .bottom:
+            // 切换位置--下图像上文字
+            /** 图像的中心位置向右移动了labelWidth * 0.5,向下移动了imageHeight * 0.5 + space * 0.5
+             *文字的中心位置向左移动了imageWidth * 0.5,向上移动了labelHeight*0.5+space*0.5
+             */
+            imageEdgeInsets = UIEdgeInsets(top: imageHeight * 0.5 + space * 0.5, left: labelWidth * 0.5, bottom: -imageHeight * 0.5 - space * 0.5, right: -labelWidth * 0.5)
+            labelEdgeInsets = UIEdgeInsets(top: -labelHeight * 0.5 - space * 0.5, left: -imageWidth * 0.5, bottom: labelHeight * 0.5 + space * 0.5, right: imageWidth * 0.5)
+        }
+        titleEdgeInsets = labelEdgeInsets
+        self.imageEdgeInsets = imageEdgeInsets
+    }
+}

+ 46 - 0
BFFramework/Classes/Categorys/UIColor+Ext.swift

@@ -0,0 +1,46 @@
+//
+//  UIColor+Ext.swift
+//  PQSpeed
+//
+//  Created by SanW on 2020/5/25.
+//  Copyright © 2020 BytesFlow. All rights reserved.
+//
+
+import Foundation
+import UIKit
+
+extension UIColor {
+    class func hexColor(hexadecimal: String) -> UIColor {
+        var cstr = hexadecimal.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines).uppercased() as NSString
+        if cstr.length < 6 {
+            return UIColor.clear
+        }
+        if cstr.hasPrefix("0X") {
+            cstr = cstr.substring(from: 2) as NSString
+        }
+        if cstr.hasPrefix("#") {
+            cstr = cstr.substring(from: 1) as NSString
+        }
+        if cstr.length != 6 {
+            return UIColor.clear
+        }
+        var range = NSRange()
+        range.location = 0
+        range.length = 2
+        // r
+        let rStr = cstr.substring(with: range)
+        // g
+        range.location = 2
+        let gStr = cstr.substring(with: range)
+        // b
+        range.location = 4
+        let bStr = cstr.substring(with: range)
+        var r: UInt32 = 0x0
+        var g: UInt32 = 0x0
+        var b: UInt32 = 0x0
+        Scanner(string: rStr).scanHexInt32(&r)
+        Scanner(string: gStr).scanHexInt32(&g)
+        Scanner(string: bStr).scanHexInt32(&b)
+        return UIColor(red: CGFloat(r) / 255.0, green: CGFloat(g) / 255.0, blue: CGFloat(b) / 255.0, alpha: 1)
+    }
+}

+ 210 - 0
BFFramework/Classes/Categorys/UIImage+Ext.swift

@@ -0,0 +1,210 @@
+//
+//  UIImage+Ext.swift
+//  PQSpeed
+//
+//  Created by SanW on 2020/6/19.
+//  Copyright © 2020 BytesFlow. All rights reserved.
+//
+
+import Foundation
+extension UIImage {
+    func cropImage(ratio: CGFloat) -> UIImage {
+        // 计算最终尺寸
+        let newSize: CGSize = CGSize(width: size.width, height: size.width * ratio)
+        // 图片绘制区域
+        var rect = CGRect.zero
+        rect.size.width = size.width
+        rect.size.height = size.height
+        rect.origin.x = (newSize.width - size.width) / 2.0
+        rect.origin.y = (newSize.height - size.height) / 2.0
+
+        UIGraphicsBeginImageContext(newSize)
+        draw(in: rect)
+        let scaledImage = UIGraphicsGetImageFromCurrentImageContext()
+        UIGraphicsEndImageContext()
+
+        return scaledImage!
+    }
+
+    func cropImage(newSize: CGSize) -> UIImage {
+        //// 图片绘制区域
+        var rect = CGRect.zero
+        rect.size.width = newSize.width
+        rect.size.height = newSize.width * (size.height / size.width)
+        // 绘制并获取最终图片
+        UIGraphicsBeginImageContext(newSize)
+        draw(in: rect)
+        let scaledImage = UIGraphicsGetImageFromCurrentImageContext()
+        UIGraphicsEndImageContext()
+
+        return scaledImage!
+    }
+
+    func imageWithImage(scaledToSize newSize: CGSize) -> UIImage {
+        UIGraphicsBeginImageContextWithOptions(newSize, false, 0.0)
+        draw(in: CGRect(origin: CGPoint.zero, size: newSize))
+        let newImage = UIGraphicsGetImageFromCurrentImageContext() ?? self
+        UIGraphicsEndImageContext()
+        return newImage
+    }
+
+    /// 旋转角度
+    /// - Parameter image: <#image description#>
+    /// - Returns: <#description#>
+    func rotateImage(rotate: Int, originWidth: CGFloat, originHeight: CGFloat) -> UIImage {
+        let rotate: CGFloat = CGFloat(3 * Double.pi / 2)
+        let rect = CGRect(x: 0, y: 0, width: originWidth, height: originHeight)
+        let translateX: CGFloat = -rect.size.height
+        let translateY: CGFloat = 0
+        let scaleY = rect.size.width / rect.size.height
+        let scaleX = rect.size.height / rect.size.width
+        UIGraphicsBeginImageContext(rect.size)
+        let context = UIGraphicsGetCurrentContext()
+        //        context!.translateBy(x: 0.0, y: rect.size.height)
+        //        context!.scaleBy(x: 1.0, y: -1.0)
+        context!.rotate(by: rotate)
+        context!.translateBy(x: translateX, y: translateY)
+        context!.scaleBy(x: scaleX, y: scaleY)
+        draw(in: CGRect(x: 0, y: 0, width: rect.size.width, height: rect.size.height))
+        return UIGraphicsGetImageFromCurrentImageContext()!
+    }
+
+    /// 生成三角图
+    /// - Parameters:
+    ///   - size: <#size description#>
+    ///   - tintColor: <#tintColor description#>
+    ///   - convert:是否倒置
+    /// - Returns: <#description#>
+    class func triangleImage(size: CGSize, tintColor: UIColor, direction: moveDirection = .moveDirectionDown) -> UIImage {
+        var startPoint: CGPoint = CGPoint.zero
+        var middlePoint: CGPoint = CGPoint.zero
+        var endPoint: CGPoint = CGPoint.zero
+        switch direction {
+        case .moveDirectionLeft:
+            startPoint = CGPoint(x: size.width, y: 0)
+            middlePoint = CGPoint(x: 0, y: size.height / 2.0)
+            endPoint = CGPoint(x: size.width, y: size.height)
+        case .moveDirectionRight:
+            startPoint = CGPoint(x: 0, y: 0)
+            middlePoint = CGPoint(x: size.width, y: size.height / 2.0)
+            endPoint = CGPoint(x: 0, y: size.height)
+        case .moveDirectionUp:
+            startPoint = CGPoint(x: 0, y: size.height)
+            middlePoint = CGPoint(x: size.width / 2.0, y: 0)
+            endPoint = CGPoint(x: size.width, y: size.height)
+        default:
+            startPoint = CGPoint(x: 0, y: 0)
+            middlePoint = CGPoint(x: size.width / 2.0, y: size.height)
+            endPoint = CGPoint(x: size.width, y: 0)
+        }
+        UIGraphicsBeginImageContextWithOptions(size, false, 0)
+        let ctx = UIGraphicsGetCurrentContext()
+        let path = UIBezierPath()
+        path.move(to: startPoint)
+        path.addLine(to: middlePoint)
+        path.addLine(to: endPoint)
+        path.close()
+        ctx?.setFillColor(tintColor.cgColor)
+        path.fill()
+        let image = UIGraphicsGetImageFromCurrentImageContext()!
+        UIGraphicsEndImageContext()
+        return image
+    }
+
+    /// 按照最短边缩放  add by ak
+    /// - Parameter maxLength: 边长最大值
+    func nx_scaleWithMaxLength(maxLength: CGFloat) -> UIImage {
+        if size.width > maxLength || size.height > maxLength {
+            var maxWidth: CGFloat = maxLength
+            var maxHeight: CGFloat = maxLength
+
+            if size.width != size.height {
+                if size.width > size.height {
+                    // 按照宽 来缩放
+                    let imageScale: CGFloat = maxLength / size.width
+
+                    maxHeight = size.height * imageScale
+                } else if size.width < size.height {
+                    let imageScale: CGFloat = maxLength / size.height
+
+                    maxWidth = size.width * imageScale
+                }
+            }
+            // 返回新的改变大小后的图片
+            return nx_scaleToSize(size: CGSize(width: maxWidth, height: maxHeight))
+        }
+
+        return self
+    }
+
+    /// 缩放到指定大小 add by ak
+    /// - Parameter size: 新的大小
+    func nx_scaleToSize(size: CGSize) -> UIImage {
+        var width: CGFloat = CGFloat(cgImage!.width)
+        var height: CGFloat = CGFloat(cgImage!.height)
+
+        let verticalRadio: CGFloat = size.height * 1.0 / height
+        let horizontalRadio: CGFloat = size.width * 1.0 / width
+
+        var radio: CGFloat = 1
+        if verticalRadio > 1, horizontalRadio > 1 {
+            radio = verticalRadio > horizontalRadio ? horizontalRadio : verticalRadio
+        } else {
+            radio = verticalRadio < horizontalRadio ? verticalRadio : horizontalRadio
+        }
+
+        width = width * radio
+        height = height * radio
+
+        let xPos: CGFloat = (size.width - width) / 2
+        let yPos: CGFloat = (size.height - height) / 2
+
+        // 创建一个bitmap的context
+        // 并把它设置成为当前正在使用的context
+        UIGraphicsBeginImageContext(size)
+
+        // 绘制改变大小的图片
+        var rect = CGRect.zero
+        rect.size.width = width
+        rect.size.height = height
+        rect.origin.x = xPos
+        rect.origin.y = yPos
+
+        draw(in: rect)
+
+        // 从当前context中创建一个改变大小后的图片
+        let scaledImage: UIImage = UIGraphicsGetImageFromCurrentImageContext()!
+
+        // 使当前的context出堆栈
+        UIGraphicsEndImageContext()
+
+        // 返回新的改变大小后的图片
+        return scaledImage
+    }
+
+    // 将图片裁剪成指定比例(多余部分自动删除)let image3 = image.crop(ratio: 1) /将图片转成 1:1 比例(正方形)
+    func nxcrop(ratio: CGFloat) -> UIImage {
+        // 计算最终尺寸
+        var newSize: CGSize!
+        if size.width / size.height > ratio {
+            newSize = CGSize(width: size.height * ratio, height: size.height)
+        } else {
+            newSize = CGSize(width: size.width, height: size.width / ratio)
+        }
+
+        ////图片绘制区域
+        var rect = CGRect.zero
+        rect.size.width = size.width
+        rect.size.height = size.height
+        rect.origin.x = (newSize.width - size.width) / 2.0
+        rect.origin.y = (newSize.height - size.height) / 2.0
+
+        // 绘制并获取最终图片
+        UIGraphicsBeginImageContext(newSize)
+        draw(in: rect)
+        let scaledImage = UIGraphicsGetImageFromCurrentImageContext()
+        UIGraphicsEndImageContext()
+
+        return scaledImage!
+    }
+}

+ 484 - 0
BFFramework/Classes/Categorys/UIView+Ext.swift

@@ -0,0 +1,484 @@
+//
+//  UICollectionView+Ext.swift
+//  PQSpeed
+//
+//  Created by SanW on 2020/6/6.
+//  Copyright © 2020 BytesFlow. All rights reserved.
+//
+
+//import KingfisherWebP
+import UIKit
+
+// MARK: - UIView的分类扩展
+
+/// UIView的分类扩展
+extension UIView {
+    func addCorner(roundingCorners: UIRectCorner = .allCorners, corner: CGFloat = cDefaultMargin) {
+        if roundingCorners == .allCorners {
+            layer.cornerRadius = corner
+            layer.masksToBounds = true
+        } else {
+            let path = UIBezierPath(roundedRect: bounds, byRoundingCorners: roundingCorners, cornerRadii: CGSize(width: corner, height: corner))
+            let cornerLayer = CAShapeLayer()
+            cornerLayer.frame = bounds
+            cornerLayer.path = path.cgPath
+            layer.mask = cornerLayer
+        }
+    }
+
+    /// 添加阴影
+    /// - Parameters:
+    ///   - color: <#color description#>
+    ///   - offset: <#offset description#>
+    /// - Returns: <#description#>
+    func addShadowLayer(isAll: Bool = false, color: UIColor = UIColor(red: 0, green: 0, blue: 0, alpha: 0.5), offset: CGSize = CGSize(width: 1, height: 1)) {
+        layer.shadowColor = color.cgColor
+        layer.shadowOffset = offset
+        layer.shadowRadius = 0.5
+        layer.shadowOpacity = 1.0
+        if isAll {
+            layer.shadowColor = color.cgColor
+            layer.shadowOffset = CGSize.zero
+            // 设置偏移量为0,四周都有阴影
+            layer.shadowRadius = 0.5 // 阴影半径
+            layer.shadowOpacity = 0.3 // 阴影透明度
+            layer.masksToBounds = false
+            layer.shadowPath = UIBezierPath(roundedRect: bounds, cornerRadius: layer.cornerRadius).cgPath
+        }
+    }
+
+    /// 添加虚线条
+    func addBorderToLayer(frame: CGRect? = nil) {
+        // 线条颜色
+        let borderLayer: CAShapeLayer = CAShapeLayer()
+        borderLayer.strokeColor = UIColor.hexColor(hexadecimal: "#FFFFFF").cgColor
+        borderLayer.fillColor = nil
+        borderLayer.path = UIBezierPath(rect: frame == nil ? bounds : frame!).cgPath
+        borderLayer.frame = bounds
+        borderLayer.lineWidth = 2.0
+        borderLayer.lineCap = .round
+        // 第一位是 线条长度   第二位是间距 nil时为实线
+        borderLayer.lineDashPattern = [5, 5]
+        layer.addSublayer(borderLayer)
+    }
+
+    func animateZoom() {
+        transform = CGAffineTransform(scaleX: 0.4, y: 0.4)
+        UIView.animate(withDuration: 0.5, animations: {
+            self.transform = CGAffineTransform(scaleX: 1.2, y: 1.2)
+        }) { _ in
+            UIView.animate(withDuration: 0.5, animations: {
+                self.transform = .identity
+            }) { _ in
+            }
+        }
+    }
+
+    /// 添加抖动功能
+    /// - Parameters:
+    ///   - fromValue: <#fromValue description#>
+    ///   - toValue: <#toValue description#>
+    ///   - duration: <#duration description#>
+    ///   - repeatCount: <#repeatCount description#>
+    /// - Returns: <#description#>
+    func shakeAnimation(_ fromValue: Float, _ toValue: Float, _ duration: Float, _: Float) {
+        layer.removeAllAnimations()
+        let shake = CABasicAnimation(keyPath: "transform.rotation.z")
+        shake.fromValue = fromValue
+        shake.toValue = toValue
+        shake.duration = CFTimeInterval(duration)
+        shake.autoreverses = true
+        shake.repeatCount = Float(CGFloat.greatestFiniteMagnitude)
+        shake.isRemovedOnCompletion = false
+        layer.add(shake, forKey: "imageView")
+        // 增加锚点
+        layer.anchorPoint = CGPoint(x: 0.5, y: 1)
+    }
+
+    /// 添加心跳动画
+    /// - Parameters:
+    ///   - duration: <#duration description#>
+    ///   - isRepeat: <#isRepeat description#>
+    ///   - multiple: <#multiple description#>
+    /// - Returns: <#description#>
+    func heartbeatAnimate(duration: TimeInterval, isRepeat: Bool, multiple: CGFloat) {
+        UIView.animateKeyframes(withDuration: duration, delay: 0, options: .allowUserInteraction, animations: {
+            self.transform = CGAffineTransform(scaleX: 1.0 + multiple, y: 1.0 + multiple)
+        }) { _ in
+            UIView.animateKeyframes(withDuration: duration, delay: 0, options: .allowUserInteraction, animations: {
+                self.transform = .identity
+            }) { _ in
+                UIView.animateKeyframes(withDuration: duration, delay: 0, options: .allowUserInteraction, animations: {
+                    self.transform = CGAffineTransform(scaleX: 1.0 + multiple * 2, y: 1.0 + multiple * 2)
+                }) { _ in
+                    UIView.animateKeyframes(withDuration: duration, delay: 0, options: .allowUserInteraction, animations: {
+                        self.transform = .identity
+                    }) { _ in
+                        if isRepeat {
+                            DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 2.3) {
+                                self.heartbeatAnimate(duration: duration, isRepeat: isRepeat, multiple: multiple)
+                            }
+                        }
+                    }
+                }
+            }
+        }
+    }
+
+    /// 活动心跳动画
+    /// - Returns: <#description#>
+    func activityHeartbeatAnimate() {
+        layer.removeAllAnimations()
+        UIView.animateKeyframes(withDuration: 0.45, delay: 0, options: .allowUserInteraction, animations: {
+            self.transform = CGAffineTransform(scaleX: 0.9, y: 0.9)
+        }) { _ in
+            UIView.animateKeyframes(withDuration: 0.45, delay: 0, options: .allowUserInteraction, animations: {
+                self.transform = .identity
+            }) { _ in
+                self.activityHeartbeatAnimate()
+            }
+        }
+    }
+
+    /// 活动心跳动画
+    /// - Returns: <#description#>
+    func heartbeatAnimate() {
+        layer.removeAllAnimations()
+        UIView.animateKeyframes(withDuration: 1, delay: 0, options: .allowUserInteraction, animations: {
+            self.transform = CGAffineTransform(scaleX: 1.3, y: 1.3)
+        }) { isFinished in
+            UIView.animateKeyframes(withDuration: 1, delay: 0, options: .allowUserInteraction, animations: {
+                self.transform = .identity
+            }) { isFinished in
+                if isFinished {
+                    self.heartbeatAnimate()
+                }
+            }
+        }
+    }
+
+    func addScaleBasicAnimation() {
+        let animation = CABasicAnimation(keyPath: "transform.scale")
+        animation.timingFunction = CAMediaTimingFunction(name: .easeOut)
+        animation.duration = 0.5
+        animation.repeatCount = 1000
+        animation.autoreverses = true
+        animation.fromValue = 0.8
+        animation.toValue = 1.1
+        layer.add(animation, forKey: nil)
+    }
+
+    func addScaleYBasicAnimation() {
+        let animation = CAKeyframeAnimation(keyPath: "transform.translation.y")
+        animation.duration = 0.5
+        animation.repeatCount = 100
+        animation.isRemovedOnCompletion = true
+        //        animation.
+        animation.timingFunction = CAMediaTimingFunction(name: .easeOut)
+        layer.add(animation, forKey: nil)
+        //            CAKeyframeAnimation *animation = [CAKeyframeAnimation animationWithKeyPath:@"transform.translation.y"];
+        //            CGFloat duration = 1.f;
+        //            CGFloat height = 7.f;
+        //            CGFloat currentY = self.animationView.transform.ty;
+        //            animation.duration = duration;
+        //            animation.values = @[@(currentY),@(currentY - height/4),@(currentY - height/4*2),@(currentY - height/4*3),@(currentY - height),@(currentY - height/ 4*3),@(currentY - height/4*2),@(currentY - height/4),@(currentY)];
+        //            animation.keyTimes = @[ @(0), @(0.025), @(0.085), @(0.2), @(0.5), @(0.8), @(0.915), @(0.975), @(1) ];
+        //            animation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
+        //            animation.repeatCount = HUGE_VALF;
+        //            [self.animationView.layer addAnimation:animation forKey:@"kViewShakerAnimationKey"];
+        //        }
+    }
+
+    /// 将view生成一张图片
+    /// - Returns: <#description#>
+    func graphicsGetImage() -> UIImage? {
+        UIGraphicsBeginImageContextWithOptions(frame.size, true, 0.0)
+        layer.render(in: UIGraphicsGetCurrentContext()!)
+        let newImage = UIGraphicsGetImageFromCurrentImageContext()
+        UIGraphicsEndImageContext()
+        return newImage
+    }
+
+    /// 动画显示View
+    /// - Returns: <#description#>
+    func showViewAnimate(duration: TimeInterval = 0.3, completion: ((Bool) -> Void)? = nil) {
+        UIView.animate(withDuration: duration, animations: { [weak self] in
+            self?.frame = CGRect(x: 0, y: cScreenHeigth - self!.frame.height, width: self!.frame.width, height: self!.frame.height)
+        }) { isFinished in
+            if completion != nil {
+                completion!(isFinished)
+            }
+        }
+    }
+
+    /// 动画隐藏view
+    /// - Returns: <#description#>
+    func dismissViewAnimate(duration: TimeInterval = 0.3, completion: ((Bool) -> Void)? = nil) {
+        UIView.animate(withDuration: duration, animations: { [weak self] in
+            self?.frame = CGRect(x: 0, y: cScreenHeigth, width: self!.frame.width, height: self!.frame.height)
+        }) { isFinished in
+            if completion != nil {
+                completion!(isFinished)
+            }
+        }
+    }
+
+    /// add  by ak 添加虚线框
+    /// - Parameter color: 框色
+    /// - Parameter lineWidth: 框宽
+    func addBorderToLayer(color: CGColor, lineWidth: CGFloat) {
+        let border = CAShapeLayer()
+
+        //  线条颜色
+        border.strokeColor = color
+        border.fillColor = nil
+
+        border.path = UIBezierPath(rect: bounds).cgPath
+
+        border.frame = bounds
+        border.lineWidth = lineWidth
+        border.lineCap = .square
+
+        //  第一位是 线条长度   第二位是间距 nil时为实线
+        border.lineDashPattern = [9, 4]
+        layer.addSublayer(border)
+    }
+}
+
+// MARK: - UICollectionView的分类扩展
+
+/// UICollectionView的分类扩展
+extension UICollectionView {
+    /// 获取当前cell
+    /// - Returns: <#description#>
+    func visibleCell() -> UICollectionViewCell? {
+        let visibleRect = CGRect(origin: contentOffset, size: bounds.size)
+        let visiblePoint = CGPoint(x: visibleRect.midX, y: visibleRect.midY)
+        guard let visibleIndexPath = indexPathForItem(at: visiblePoint) else { return nil }
+        return cellForItem(at: visibleIndexPath)
+    }
+
+    /// 添加刷新组件
+    /// - Parameters:
+    ///   - scroller: <#scroller description#>
+    ///   - type: 1-头部跟尾部 2-头部 3-尾部
+    func addRefreshView(type: REFRESH_TYPE = .REFRESH_TYPE_ALL, refreshHandle: ((_ isHeader: Bool) -> Void)?) {
+        if type == .REFRESH_TYPE_ALL || type == .REFRESH_TYPE_HEADER {
+            let header = MJRefreshNormalHeader.init {
+                if refreshHandle != nil {
+                    refreshHandle!(true)
+                }
+            }
+            header.setTitle("下拉刷新", for: .willRefresh)
+            header.setTitle("正在刷新...", for: .refreshing)
+            header.setTitle("松开刷新", for: .pulling)
+            header.setTitle("下拉刷新", for: .idle)
+            header.lastUpdatedTimeLabel?.isHidden = true
+            mj_header = header
+        }
+        if type == .REFRESH_TYPE_ALL || type == .REFRESH_TYPE_FOOTER {
+            // MJRefreshBackNormalFooter 不会附在上面
+            // MJRefreshAutoFooter 不会便宜
+            let footer = MJRefreshBackNormalFooter.init {
+                if refreshHandle != nil {
+                    refreshHandle!(false)
+                }
+            }
+            footer.setTitle("暂时没有更多了", for: .noMoreData)
+            footer.setTitle("精彩内容正在加载中...", for: .refreshing)
+            mj_footer = footer
+        }
+    }
+
+    func indexPathsForElements(in rect: CGRect) -> [IndexPath] {
+        let allLayoutAttributes = collectionViewLayout.layoutAttributesForElements(in: rect)!
+        return allLayoutAttributes.map { $0.indexPath }
+    }
+}
+
+// MARK: - UITabBar的分类扩展
+
+/// UITabBar的分类扩展
+extension UITabBar {
+    /// 展示小红点
+    /// - Parameter index: <#index description#>
+    /// - Returns: <#description#>
+    func showPoint(index: Int) {
+        let pointW: CGFloat = 8
+        let pointView = UIView()
+        pointView.tag = 11111 + index
+        pointView.layer.cornerRadius = pointW / 2
+        pointView.backgroundColor = UIColor.hexColor(hexadecimal: "#EE0051")
+        let percentX: CGFloat = CGFloat(Double(index) + 0.7) / CGFloat(items?.count ?? 1)
+        let pointX = ceil(percentX * frame.width)
+        let pointY = ceil(0.1 * frame.height)
+        pointView.frame = CGRect(x: pointX, y: pointY, width: pointW, height: pointW)
+        addSubview(pointView)
+    }
+
+    /// 移除小红点
+    /// - Parameter index: <#index description#>
+    /// - Returns: <#description#>
+    func removePoint(index: Int) {
+        for item in subviews {
+            if item.tag == 11111 + index {
+                item.removeFromSuperview()
+            }
+        }
+    }
+
+    /// 展示创作视频引导
+    /// - Parameter index: <#index description#>
+    /// - Returns: <#description#>
+    func showVideoMakeRemindView() {
+        let isOldUploadClick: String? = getUserDefaults(key: cIsUploadClick) as? String
+        let isUploadClick: String? = getUserDefaultsForJson(key: cIsUploadClick) as? String
+        let isVerticalSlip: String? = getUserDefaults(key: cIsVerticalSlip) as? String
+        if isOldUploadClick == nil && isVerticalSlip != nil && isVerticalSlip == "1", isUploadClick == nil || isUploadClick?.count ?? 0 <= 0 || isUploadClick != "2" {
+            let width: CGFloat = 275 // 275
+            let height: CGFloat = 107 // 107
+            let videoMakeRemindBtn = UIButton(frame: CGRect(x: 0, y: -height + 5, width: width, height: height))
+            videoMakeRemindBtn.tag = cVideoMakeRemindTag
+            videoMakeRemindBtn.setBackgroundImage(UIImage(named: "videomk_guide"), for: .normal)
+            addSubview(videoMakeRemindBtn)
+            videoMakeRemindBtn.center.x = center.x
+            videoMakeRemindBtn.addTarget(self, action: #selector(dismiss), for: .touchUpInside)
+            videoMakeRemindBtn.addScaleBasicAnimation()
+            if isUploadClick == "1" {
+                saveUserDefaultsToJson(key: cIsUploadClick, value: "2")
+            } else {
+                saveUserDefaultsToJson(key: cIsUploadClick, value: "1")
+            }
+            DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 5) { [weak self] in
+                self?.removeVideoMakeRemindView()
+            }
+        }
+    }
+
+    @objc func dismiss() {
+  
+    }
+
+    /// 移除创作视频引导
+    /// - Returns: <#description#>
+    func removeVideoMakeRemindView() {
+        viewWithTag(cVideoMakeRemindTag)?.removeFromSuperview()
+    }
+}
+
+// MARK: - UILabel的分类扩展
+
+/// UILabel的分类扩展
+extension UILabel {
+    var isTruncated: Bool {
+        guard let labelText = text else {
+            return false
+        }
+
+        // 计算理论上显示所有文字需要的尺寸
+        let rect = CGSize(width: bounds.width, height: CGFloat.greatestFiniteMagnitude)
+        let labelTextSize = (labelText as NSString)
+            .boundingRect(with: rect, options: .usesLineFragmentOrigin,
+                          attributes: [NSAttributedString.Key.font: font as Any], context: nil)
+
+        // 计算理论上需要的行数
+        let labelTextLines = Int(ceil(CGFloat(labelTextSize.height) / font.lineHeight))
+
+        // 实际可显示的行数
+        var labelShowLines = Int(floor(CGFloat(bounds.size.height) / font.lineHeight))
+        if numberOfLines != 0 {
+            labelShowLines = min(labelShowLines, numberOfLines)
+        }
+
+        // 比较两个行数来判断是否需要截断
+        return labelTextLines > labelShowLines
+    }
+
+    /// 添加阴影
+    /// - Parameters:
+    ///   - color: <#color description#>
+    ///   - offset: <#offset description#>
+    /// - Returns: <#description#>
+    func addShadow(color: UIColor = UIColor(red: 0, green: 0, blue: 0, alpha: 0.5), offset: CGSize = CGSize(width: 1, height: 1)) {
+        shadowColor = color
+        shadowOffset = offset
+    }
+}
+
+extension UIImageView {
+    /// imageView加载网络图片
+    /// - Parameters:
+    ///   - url: 网络url
+    func setNetImage(url: String?, placeholder: UIImage = UIImage(named: "placehold_image")!) {
+        if url == nil || (url?.count ?? 0) <= 0 {
+            PQLog(message: "设置按钮网络图片地址为空")
+            return
+        }
+//        kf.setImage(with: URL(string: url!), placeholder: placeholder, options: url?.suffix(5) == ".webp" ? [.processor(WebPProcessor.default), .cacheSerializer(WebPSerializer.default)] : nil, progressBlock: { _, _ in
+//
+//        }) { _, _, _, _ in
+//        }
+    }
+
+    /// 展示加载中动画
+    /// - Returns: <#description#>
+    func showLoadingAnimation(duration: Double = 1) {
+        let rotationAnim = CABasicAnimation(keyPath: "transform.rotation.z")
+        rotationAnim.fromValue = 0
+        rotationAnim.toValue = Double.pi * 2
+        rotationAnim.repeatCount = MAXFLOAT
+        rotationAnim.duration = duration
+        rotationAnim.isRemovedOnCompletion = false
+        layer.add(rotationAnim, forKey: nil)
+    }
+
+    /// 播放GIF
+    /// - Parameters:
+    ///   - data : 图片二进制数据
+    ///   - images: 图片
+    ///   - repeatCount: 循环次数
+    ///   - duration: 时长
+    /// - Returns: <#description#>
+    func displayGIF(data: Data? = nil, images: [UIImage]? = nil, repeatCount: Int = Int.max, duration: Double = 1) {
+        if images != nil, (images?.count ?? 0) > 0, !isAnimating {
+            layer.removeAllAnimations()
+            stopAnimating()
+            animationImages = images
+            animationDuration = duration
+            animationRepeatCount = repeatCount
+            startAnimating()
+        } else if images == nil && data != nil {
+            PQPHAssetVideoParaseUtil.parasGIFImage(data: data!) { [weak self] _, images, duration in
+                if images != nil, (images?.count ?? 0) > 0 {
+                    self?.displayGIF(images: images!, repeatCount: repeatCount, duration: duration ?? 1)
+                }
+            }
+        }
+    }
+}
+
+extension UIButton {
+    /// UIButton加载网络图片
+    /// - Parameters:
+    ///   - url: 网络url
+    func setNetImage(url: String?, placeholder: UIImage = UIImage(named: "placehold_image")!) {
+        if url == nil || (url?.count ?? 0) <= 0 {
+            PQLog(message: "设置按钮网络图片地址为空")
+            return
+        }
+//        kf.setImage(with: URL(string: url!), for: .normal, placeholder: placeholder, options: url?.suffix(5) == ".webp" ? [.processor(WebPProcessor.default), .cacheSerializer(WebPSerializer.default)] : nil, progressBlock: { _, _ in
+//
+//        }) { _, _, _, _ in
+//        }
+    }
+
+    /// UIButton加载网络背景图片
+    /// - Parameters:
+    ///   - url: 网络url
+    func setNetBackgroundImage(url: String, placeholder: UIImage = UIImage(named: "placehold_image")!) {
+//        kf.setBackgroundImage(with: URL(string: url), for: .normal, placeholder: placeholder, options: url.suffix(5) == ".webp" ? [.processor(WebPProcessor.default), .cacheSerializer(WebPSerializer.default)] : nil, progressBlock: { _, _ in
+//
+//        }) { _, _, _, _ in
+//        }
+    }
+}

+ 727 - 0
BFFramework/Classes/Enums/Enums.swift

@@ -0,0 +1,727 @@
+//
+//  Enums.swift
+//  PQSpeed
+//
+//  Created by lieyunye on 2020/5/29.
+//  Copyright © 2020 BytesFlow. All rights reserved.
+//
+
+import Foundation
+
+// MARK: - 视频播放页面类型
+
+/// 视频播放页面类型
+enum PQVIDEOPAGETYPE {
+    case PQVIDEOPAGETYPE_Normal // 默认-单个视频详情
+    case PQVIDEOPAGETYPE_RECOMM // 默认-推荐
+    case PQVIDEOPAGETYPE_ATTEN // 关注
+    case PQVIDEOPAGETYPE_DETAIL // 用户发布视频详情
+    case PQVIDEOPAGETYPE_LIKEDETAIL // 我的喜欢视频详情
+    case PQVIDEOPAGETYPE_ATTUSERDETAIL // 关注用户详情
+    case PQVIDEOPAGETYPE_SEARCHDETAIL // 搜索详情
+    case PQVIDEOPAGETYPE_HOTVIDEODETAIL // 热门视频详情
+    case PQVIDEOPAGETYPE_GUIDEVIDEODETAIL // 引导视频详情
+    case PQVIDEOPAGETYPE_SINGLEVIDEODETAIL // 单个视频详情
+    case PQVIDEOPAGETYPE_RECREATDETAIL // 再创作详情页
+}
+
+// MARK: - 视频播放状态
+
+/// 视频播放状态
+enum PQVIDEO_PLAY_STATUS {
+    case PQVIDEO_PLAY_STATUS_LOADING // 加载中
+    case PQVIDEO_PLAY_STATUS_BEGIN // 开始播放
+    case PQVIDEO_PLAY_STATUS_END // 播放结束
+    case PQVIDEO_PLAY_STATUS_DISCONNECT // 重连失败
+    case PQVIDEO_PLAY_STATUS_NOT_FOUND // 播放文件不存在
+    case PQVIDEO_PLAY_STATUS_RECONNECT // 重连连接
+}
+
+// MARK: - 页面场景
+
+/// 页面场景
+enum PAGESOURCE: String {
+    /*************** 视频相关pageSource ***************/
+    case sp_category = "speedApp-category" //   首页-单列
+    case sp_category_recommend = "speedApp-category_recommend" // 由首页单列右划
+    case sp_categoryDouble = "speedApp-categoryDouble" // 首页-双列
+    case sp_videoDetail = "speedApp-videoDetail" // 视频详情页
+    case sp_videoDetail_search = "speedApp-videoDetail_search" //    由搜索结果列表进入
+    case sp_videoDetail_search_recommend = "speedApp-videoDetail_search_recommend" // 由搜索结果列表进入
+    case sp_videoDetail_upload = "speedApp-videoDetail_upload" //     由我的tab中“作品”列表进入
+    case sp_videoDetail_upload_recommend = "speedApp-videoDetail_upload_recommend" // 、由我的tab中“作品”列表进入
+    case sp_videoDetail_favorite = "speedApp-videoDetail_favorite" // 由我的喜欢列表进入
+    case sp_videoDetail_share = "speedApp-videoDetail_share" // 由我的分享列表进入
+    case sp_videoDetail_favorite_recommend = "speedApp-videoDetail_favorite_recommend" // 由我的喜欢列表推荐
+    case sp_userHomePage = "speedApp-userHomePage" // 用户个人主页
+    case sp_videoDetail_userHomePage = "speedApp-videoDetail_userHomePage" // 由用户个人主页进入
+    case sp_videoDetail_userHomePage_recommend = "speedApp-videoDetail_userHomePage_recommend" // 由用户个人主页进入
+    case sp_recommendBottom = "speedApp-recommendBottom" //    底部展示的3个相关推荐视频
+    case sp_follow = "speedApp-follow" // 关注tab
+    case sp_follow_recommend = "speedApp-follow_recommend" // 由关注列表右划
+    case sp_followSingle = "speedApp-followSingle" // 关注tab点击顶部个人头像进入某个人的关注
+    case sp_followSingle_recommend = "speedApp-followSingle_recommend" // 关注tab点击顶部个人头像进入某个人的关注
+    case sp_mine = "speedApp-mine" // 我的tab
+    case sp_search = "speedApp-search" // 搜索页
+    case sp_uploadVideo = "speedApp-uploadVideo" // 视频制作入口
+
+    /*************** 活动相关pageSource ***************/
+    case sp_activity_entranceButton = "speedApp_playActivity_entranceButton" // 活动活动入口按键
+    case sp_activity_invite = "speedApp_playActivity_invite" // H5页面内邀请好友按键
+    case sp_activity_threeDot = "speedApp_playActivity_threeDot" // APP右上角…按键
+    case sp_activity_shareFriend = "speedApp_playActivity_shareFriend" // 进一步点击分享好友
+    case sp_activity_shareMoment = "speedApp_playActivity_shareMoment" // 进一步点击分享朋友圈
+    case sp_activity_openH5 = "speedApp_playActivity_completeOpenH5" // 当天完成活动的弹窗->去查看
+    case sp_activity_openH5AndPayment = "speedApp_playActivity_completeOpenH5AndPayment" // 当天完成活动的弹窗->报名明日
+    case sp_activity_failOpenH5 = "speedApp_playActivity_missionFailWindowOpenH5" // 未成功弹窗->继续活动
+    case sp_activity_success_share = "speedApp_playActivity_missionSuccessWindow_share" // 成功弹窗->分享给好友炫耀下
+    case sp_activity_entrance_close = "speedApp_playActivity_entrance_close" // 活动入口弹窗->关闭按钮
+    case sp_activity_entranceWindow = "speedApp_playActivity_entranceWindow" // 活动入口弹窗
+    case sp_activity_successWindow = "speedApp_playActivity_missionSuccessWindow" // 成功弹窗
+    case sp_activity_failWindow = "speedApp_playActivity_missionFailWindow" // 未成功弹窗
+    case sp_activity_completeWindow = "speedApp_playActivity_missionCompleteWindow" //  当天完成活动的弹窗
+
+    /*************** 上传相关pageSource ***************/
+    case sp_upload_videoSelect = "speedApp-upload_videoSelect" //  选视频-进入页面事件
+    case sp_upload_coverSelect = "speedApp-upload_coverSelect" //  选封面-进入页面事件
+    case sp_upload_videoPublish = "speedApp-upload_videoPublish" //  发布-进入页面事件
+
+    /*************** tab点击相关pageSource ***************/
+    case sp_categoryTabButton = "speedApp-categoryTabButton" //  首页tab
+    case sp_followTabButton = "speedApp-followTabButton" //  关注tab
+    case sp_uploadTabButton = "speedApp-uploadTabButton" //  上传tab
+    case sp_mineTabButton = "speedApp-mineTabButton" //  我的tab
+    case sp_msg_tabBtn = "speedApp_msgTabButton" //  消息tab
+
+    /*************** 发布pageSource ***************/
+    case sp_videoMaking = "speedApp-videoMaking" // 视频创作
+    case sp_videoCompose_guid = "speedApp-videoCompose_guid" // 预览页面
+    case sp_videoCompose_edit = "speedApp-videoCompose_edit" // 编辑页面
+    case sp_videoCompose_overview = "speedApp-videoCompose_overview" // 总览页面
+    case sp_videoCompose_composition = "speedApp-videoCompose_composition" // 合成页面
+    case sp_material_search = "vlog-pages/user-videos-share" // 素材搜索上报
+
+    /*************** 消息pageSource ***************/
+    case sp_msg_shareSpace = "speedApp-message" // 分享空间
+    case sp_msg_shareSpace_detail = "speedApp-message_shareSpace" // 分享空间详情
+    case sp_msg_share = "speedApp-message_share" // 分享
+    case sp_msg_like = "speedApp-message_like" // 喜欢
+    case sp_msg_fans = "speedApp-message_fans" // 粉丝
+    case sp_msg_push = "speedApp-message_push" // 通知
+
+    /*************** 草稿箱相关pageSource ***************/
+    case sp_draft_projectList = "speedApp-projectList" // 草稿箱列表
+    case sp_reproduce_childList = "speedApp-reproduceCollection_child" // 再制作子列表页面
+    case sp_reproduce_fatherList = "speedApp-reproduceCollection_father" // 再制作父列表页面
+    case sp_reproduce_chilDetailList = "speedApp-videoDetail_reproduceCollection_child" // 再制作子列表页面
+    case sp_reproduce_fatherDetailList = "speedApp-videoDetail_reproduceCollection_father" // 再制作父列表页面
+    // add by ak
+    case sp_speedApp_upload2Compose = "speedApp-upload2Compose" // 上传转创作
+    
+    /*************** 卡点视频相关pageSource ***************/
+    case sp_stuck_selectMaterial = "speedApp-selectSyncedUpMaterial" // 卡点视频素材选择页
+    case sp_stuck_selectSynceedUpMusic = "speedApp-selectSynceedUpMusic" // 卡点视频音乐选择页
+    case sp_stuck_previewSyncedUp = "speedApp_previewSyncedUp" // 预览页面曝光上报
+    case sp_stuck_searchSyncedUpMusic = "speedApp_searchSyncedUpMusic" // 音乐素材搜索页
+    case sp_stuck_publishSyncedUp = "speeddApp_publishSyncedUp" // 合成发布页
+}
+
+// MARK: - objectType
+
+/// objectType
+enum objectType: String {
+    /*************** tab点击相关pageSource ***************/
+
+    case ot_home_tabBtn = "speedApp-categoryTabButton" //  首页tab
+    case ot_follow_tabBtn = "speedApp-followTabButton" //  关注tab
+    case ot_up_tabBtn = "speedApp-uploadTabButton" //  上传tab
+    case ot_mine_tabBtn = "speedApp-mineTabButton" //  我的tab
+    case ot_public_tabBtn = "speedApp-publicTabButton" //  发布tab
+    case ot_msg_tabBtn = "speedApp_msgTabButton" //  消息tab
+
+    /*************** 上传相关objectType ***************/
+    case ot_up_backBtn = "speedApp-uploadBackButton" // 点击左下角叉子
+    case ot_up_nextBtn = "speedApp-uploadNextButton" // 点击右下角下一步
+    case ot_up_coverBtn = "speedApp-uploadCoverButton" // 点击上传封面
+    case ot_up_pickCoverBtn = "speedApp-pickCoverButton" // 点击截取封面
+    case ot_up_publishBtn = "speedApp-videoPublishButton" // 点击发布
+    case ot_up_changeCoverBtn = "speedApp-changeCoverButton" // 点击选封面返回
+    case ot_up_start = "speedApp-uploadStart" // 上传开始事件
+    case ot_up_fail = "speedApp-uploadFail" // 上传中断/失败事件
+    case ot_up_restart = "speedApp-uploadRestart" // 上传重试事件
+    case ot_up_success = "speedApp-uploadSuccess" // 上传完成
+    case ot_up_publishSuccess = "speedApp-videoPublishSuccess" // 发布完成
+    case ot_up_viewPopup_guideUsersToShare = "speedApp_viewPopup_guideUsersToShare_pubishVideo" // 发布成功弹出分享界面
+    case ot_up_clickWechatMoments_guideUsersToShare = "speedApp_clickWechatMoments_guideUsersToShare_pubishVideo" // 分享介面点击微信朋友圈
+    case ot_up_clickWechat_guideUsersToShare = "speedApp_clickWechat_guideUsersToShare_pubishVideo"
+    case ot_selectVideoProductionMode = "speedApp_viewWindow_selectVideoProductionMode"
+    // 分享界面点击微信好友
+    /*************** 创作工具相关objectType ***************/
+    case ot_makevideo_video = "video" // 视频
+    case ot_makevideo_gif = "gif" // 动态图
+    case ot_makevideo_jpg = "jpg" // 图片
+    case ot_enterComposeToolButton = "speedApp_enterComposeToolButton" // 点击上传tab后-点击发布视频 add by ak & 视频合成入口
+    case ot_enterVideoUploadButton = "speedApp_enterVideoUploadButton" // 点击上传tab后-点击上传视频
+    case ot_videoCompose_overviewButton = "speedApp_videoCompose_overviewButton" // 点击总览
+    case ot_videoCompose_videoCompositeButton = "speedApp_videoCompose_videoCompositeButton" // 点击去发布
+    case ot_videoCompose_videoPublish = "speedApp_videoPublishButton" // 发布视频
+    case ot_speedApp_searchButton = "speedApp_searchButton" // 发布视频
+    case ot_pageView = "pageView" // 页面访问
+    //
+    // 图文入口
+    case ot_speedApp_clickButton_imageAndTextGenerateVideo = "speedApp_clickButton_imageAndTextGenerateVideo"
+    // 电子相册
+    case ot_speedApp_clickButton_electronicAlbum = "speedApp_clickButton_electronicAlbum"
+    /*************** 消息相关objectType ***************/
+    case ot_msg_fansMsgButton = "speedApp_msgTab_fansMsgButton" //  粉丝消息入口
+    case ot_msg_likeMsgButton = "speedApp_msgTab_likeMsgButton" //  喜欢消息入口
+    case ot_msg_shareMsgButton = "speedApp_msgTab_shareMsgButton" //  分享消息入口
+    case ot_msg_commentMsgButton = "speedApp_msgTab_commentMsgButton" //  评论消息入口
+    case ot_msg_systemMsgButton = "speedApp_msgTab_systemMsgButton" //  通知消息入口
+
+    /*************** 消息详情相关objectType ***************/
+    case ot_msg_shareSpaceViewTab = "speedApp_message_shareSpace_viewTab" // 分享空间详情页观看
+    case ot_msg_shareSpaceLikeTab = "speedApp_message_shareSpace_likeTab" // 分享空间详情页喜欢
+    case ot_msg_shareSpaceCommentTab = "speedApp_message_shareSpace_commentTab" // 分享空间详情页评论
+    case ot_msg_shareSpaceShareTab = "speedApp_message_shareSpace_shareTab" // 分享空间详情页分享
+
+    /*************** 草稿箱相关objectType ***************/
+    case ot_draft_clicktButton = "speedApp_clickOpenProjectListButton" // 草稿箱点击入口
+    case ot_draft_editProject = "speedApp_editProject" // - 点击 Project Item(草稿箱中的每个项目)
+    case ot_draft_viewProject = "speedApp_viewProject" // - 看到 Project Item(草稿箱中的每个项目)
+    case ot_draft_clickEditProject = "speedApp_clickEditProject" // - 点击 Project Item 弹出菜单的「编辑」按钮
+    case ot_draft_clickPublishProject = "speedApp_clickPublishProject" // - 点击 Project Item 弹出菜单的「去发布」按钮
+    case ot_draft_clickRenameProject = "speedApp_clickRenameProject" // - 点击 Project Item 弹出菜单的「重命名」按钮
+    case ot_draft_clickCopyProject = "speedApp_clickCopyProject" // - 点击 Project Item 弹出菜单的「复制」按钮
+    case ot_draft_clickDeleteProject = "speedApp_clickDeleteProject" // - 点击 Project Item 弹出菜单的「删除」按钮
+    case ot_draft_uploadMaterial = "speedApp_uploadMaterial" // - 素材开始上传事件
+    case ot_draft_uploadMatrialSuccess = "speedApp_uploadMatrialSuccess" // - 素材上传成功事件
+    case ot_draft_downloadMaterial = "speedApp_downloadMaterial" // - 素材开始下载事件
+    case ot_draft_downloadMaterialSuccess = "speedApp_downloadMaterialSuccess" // - 素材下载成功事件
+    case ot_videoCompose_mux_complete = "speedApp_videoCompose_mux_complete" // 创作工具「合成成功」添加上报参数
+
+    case ot_reproduce_collectionClicButton = "speedApp_clickReproduceButton_collection" // - 再创作集合页的「再创作按钮」
+    case ot_reproduce_clickButton = "speedApp_clickReProduceButton" // 再创作按钮(右上角)点击上报
+    case ot_reproduce_collectionBar = "speedApp_clickReproduceCollectionBar" // 再创作按钮(左下角)点击上报
+    case ot_reproduce_collectionVideo = "speedApp_clickReproduceCollectionVideo" // 再创作集合页的「视频」点击上报
+    case ot_reproduce_sameSourceButton = "speedApp_viewSameSourceButton" // 再创作查看同款来源按钮
+    case ot_reproduce_saveProjectToDraftBox = "speedApp-saveProjectToDraftBox" // 创作工具「保存项目成功」添加上报参数
+    // add by ak
+    case speedApp_viewWindow_upload2Compose // 「上传转创作」:窗口曝光
+    case speedApp_clickButton_upload2Compose_addMusic // 「上传转创作」:加音乐 - 按钮点击
+    case speedApp_clickButton_upload2Compose_addText // 「上传转创作」:加语音 - 按钮点击
+    case speedApp_clickButton_upload2Compose_addVoice // 「上传转创作」:加语音 - 按钮点击
+    case speedApp_clickButton_upload2Compose_addCompose // 上传转创作」:加多段拼接 - 按钮点击
+    case speedApp_clickButton_upload2Compose_processingVideo // 「上传转创作」:加工视频 - 按钮点击
+    case speedApp_clickButton_upload2Compose_publish // 「上传转创作」:直接发布 - 按钮点击
+    case speedApp_viewButton_uploadCoverButton // 「发布页」:修改封面 - 按钮曝光
+    case speedApp_viewButton_uploadCoverTipButton // 修改封面提示 - 按钮曝光
+    case speedApp_clickButton_uploadCoverButton // 「发布页」:修改封面 - 按钮点击
+    case speedApp_clickButton_uploadCoverTipButton
+
+    case speedApp_viewButton_addMusic // 「创作工具页」:添加音乐 - 按钮曝光
+    case speedApp_clickButton_addMusic // 「创作工具页」:添加音乐 - 按钮点击
+    case speedApp_viewButton_addMusicTip // 创作工具页」:添加音乐提示 - 按钮曝光
+    case speedApp_clickButton_addMusicTip // 「创作工具页」:添加音乐提示 - 按钮点击
+    
+    /*************** 卡点视频相关objectType ***************/
+    case ot_click_syncedUpMusic = "speedApp_clickButton_syncedUpMusic" // 弹出面板中点击「卡点视频」按键
+    case ot_view_selectSyncedUpMaterial = "speedApp_viewWindow_selectSyncedUpMaterial" // 曝光上报:卡点视频素材选择页
+    case ot_click_confirmMaterial = "speedApp_clickButton_confirmSyncedUpMaterial" // 点击上报:卡点视频素材确认按钮
+    case ot_click_back = "speedApp_clickButton_back" // 点击上报:返回按钮
+    case ot_view_selectSyncedUpMusic = "speedApp_viewWindow_selectSyncedUpMusic" // 曝光上报:卡点视频音乐选择页
+    case ot_view_syncedUpMusic = "speedApp_viewButton_syncedUpMusic" // 曝光上报:音乐素材曝光
+    case ot_click_auditionMusic = "speedApp_clickButton_auditionMusic" // 点击上报:音乐素材试听
+    case ot_click_chooseMusic = "speedApp_clickButton_chooseMusic" // 点击上报:选择音乐素材
+    case ot_click_chooseMusicCategory = "speedApp_clickButton_chooseMusicCategory" // 点击上报:选择音乐分类
+    case ot_click_chooseMusicCategoryTag = "speedApp_clickButton_chooseMusicCategoryTag" // 点击上报:选择音乐分类下的 TAG
+    case ot_view_previewSyncedUp = "speedApp_viewWindow_previewSyncedUp" // 曝光上报:预览页面曝光上报
+    case ot_click_selectMusic = "speedApp_clickButton_selectMusic" // 点击上报:重新选择音乐
+    case ot_click_selectRhythm = "speedApp_clickButton_selectRhythm" // 点击上报:选择节奏
+    case ot_click_dragFront = "speedApp_clickButton_dragFront" // 点击上报:拖动拖拽条(左部分)
+    case ot_click_dragBehind = "speedApp_clickButton_dragBehind" // 点击上报:拖动拖拽条(右部分)
+    case ot_click_commit = "sppedApp_clickButton_commit" // 点击上报:去合成
+    case ot_view_searchSyncedUpMusic = "speedApp_viewWindow_searchSyncedUpMusic" // 曝光上报:音乐素材搜索页
+    case ot_click_searchSyncedUpMusic = "speedApp_clickButton_searchSyncedUpMusic" // 点击上报:用户在搜索框输入文字然后按回车
+    case ot_view_searchMusic = "speedApp_viewButton_searchMusic" // 曝光上报:搜索结果音乐素材曝光
+    case ot_click_auditionSearchMusic = "speedApp_clickButton_auditionSearchMusic" // 点击上报:试听音乐素材
+    case ot_click_chooseSearchMusic = "speedApp_clickButton_chooseSearchMusic" // 点击上报:选择音乐素材
+    case ot_view_publishSyncedUp = "speedApp_viewWindow_publishSyncedUp" // 曝光上报:窗口曝光
+    case ot_click_shareWechat = "speedApp_clickButton_shareWechat" // 点击上报:分享微信
+    case ot_click_shareWechatMoment = "speedApp_clickButton_shareWechatMoment" // 点击上报:分享朋友圈
+    case ot_click_finished = "speedApp_clickButton_finished" // 点击上报:完成
+
+}
+
+// MARK: - 视频上报类型
+
+/// 视频上报类型
+enum businessType: String {
+    /*************** 视频相关businessType ***************/
+    case bt_videoView = "videoView" //  视频展示到屏幕,在此APP中 首页中:视频展示但未播放时,用于后端算法相关逻辑 相关推荐:在播放中下部展示出相关推荐的3个视频时,上报这3个视频的videoView。同一次播放下不重复上报3个推荐视频的videoView
+    case bt_videoPreView = "videoPreView"
+    case bt_videoPlaySlow = "videoPlaySlow"
+    case bt_videoPlayError = "videoPlayError" // 播放失败
+    case bt_aliasBindingError = "aliasBindingError" // 绑定别名失败
+    case bt_videoPlaySuccessTime = "videoPlaySuccessTime" // 加载时长
+    case bt_videoPlayException = "videoPlayException"
+    case bt_videoPlay_normal = "videoPlay"
+    enum bt_videoPlay: String {
+        case bt_videoPlay_userSlideSingle = "userSlideSingle" // 用户在沉浸态下滑动切换视频(极速版、爱电影)
+        case bt_videoPlay_userClickCover = "userClickCover" // 用户点击封面播放
+        case bt_videoPlay_userSlideList = "userSlideList" // 用户在列表中滑动停止导致的播放
+        case bt_videoPlay_autoNext = "autoNext" // 自动播放下一个视频
+        case bt_videoPlay_autoStart = "autoStart" // 1、启动后自动播第一个视频 2、播放器出问题导致重新播放 3、别的页面返回之前的页面播放
+    }
+
+    case bt_videoShareH5 = "videoShareH5" // 点击分享朋友圈按键
+    case bt_videoShareFriend = "videoShareFriend" // 点击分享好友按键
+    case bt_videoSemiRealPlay = "videoSemiRealPlay" // 视频播放到10s时上报
+    case bt_videoRealPlay = "videoRealPlay" // 视频播放到20s或播放到总时长30%,哪个先到为准
+    case bt_videoPlaySuccess = "videoPlaySuccess" // 视频缓冲完成开始播放
+    case bt_videoPlayEnd = "videoPlayEnd" // 视频播放到总时长100%
+    case bt_videoFavorite = "videoFavorite" // 视频喜欢的上报
+    case bt_videoEnterUser = "videoEnterUserHomepage" // 在视频页点击 Up 主头像进入个人主页
+    case bt_videoSwipeRight = "videoSwipeRight" // 右滑查看视频的相关推荐
+    case bt_videoEnterRecom = "videoEnterRecomendation" // 点击视频的相关推荐
+
+    /*************** 活动相关businessType ***************/
+    case bt_buttonClick = "buttonClick" // 按键点击
+    case bt_buttonView = "buttonView" // 按键曝光
+    case bt_windowView = "windowView" // 弹窗曝光
+    case bt_autoJump = "autoJump" // 自动跳转
+    case bt_pageView = "pageView" // 页面访问
+
+    /*************** 供给稳定性相关businessType ***************/
+    case bt_fetchSlow = "FetchSlow" // 拉取卡顿率
+    case bt_fetchFail = "FetchFail" // 拉取失败率
+    case bt_fetchDuration = "FetchDuration" // 拉取时长
+    case bt_dnsParseCostTime = "dnsParseCostTime" // DNS上报 "speed.piaoquantv.com", "rescdn.yishihui.com"
+
+    /*************** 上传相关businessType ***************/
+    case bt_up_process = "videoUploadProcess" // 上传开始事件
+    case bt_publish_error = "publishError" // 发布失败
+    case bt_publish_success = "publishSuccess" // 发布成功
+
+    /*************** 创作工具相关businessType ***************/
+    case bt_makeVideos = "makeVideos" // 创作视频
+    case bt_materialView = "materialView" // 素材曝光
+    case bt_materialUse = "materialUse" // 用户选取
+    case bt_materialCompose = "materialCompose" // 素材合成
+
+    /*************** 消息相关businessType ***************/
+    case bt_draft_downloadMaterialSuccess = "downloadMaterialSuccess" // 素材下载成功事件
+    case bt_draft_downloadMaterial = "downloadMaterial" // - 素材开始下载事件
+    case bt_draft_uploadMaterialSuccess = "uploadMaterialSuccess" // - 素材上传成功事件
+    case bt_draft_uploadMaterial = "uploadMaterial" // - 素材开始上传事件
+    case bt_videoCompose_muxAction = "muxAction" // 创作工具「合成成功」添加上报参数
+}
+
+// MARK: - autoType 自动动作的类型
+
+/// autoType 自动动作的类型
+enum autoType: Int {
+    case AUTO_TYPE_SCROLL_VERTICAL_DOWN = 11 // 用户手动下划触发播放(播上一个视频)
+    case AUTO_TYPE_SCROLL_VERTICAL_UP = 12 // 用户手动上划触发播放(播下一个视频)
+    case AUTO_TYPE_SCROLL_HORIZON_RIGHT = 13 // 用户手动右划触发播放
+    case AUTO_TYPE_SCROLL_HORIZON_LEFT = 14 // 用户手动左划触发播放
+    case AUTO_TYPE_ENTER_DETAIL = 15 // 用户手动点击(进详情页)触发播放
+    case AUTO_TYPE_ENTER_APP = 21 // 打开APP自动播放的第一个
+    case AUTO_TYPE_NEXT_AFTER_COMPLETE = 22 // 用户播放完一个视频后自动触发播放相关推荐视频
+    case AUTO_TYPE_BACKEND_RESUME = 23 // 后台唤醒app,如果播放器挂了,重新播放、在别的页面播放过视频再回来之前的页面播放
+    case AUTO_TYPE_REFRESH_LIST = 24 // 首页下拉刷新,自动播的第一个
+    case AUTO_TYPE_CLICK_RECOMMEND = 25 // 点击推荐视频播放
+    case AUTO_TYPE_CLICK_DOUBLE = 26 // 双列点击视频进入单列播放
+}
+
+// MARK: - 消息动作:表示该条日志属于某条消息生命周期的哪个漏斗环节
+
+/// 消息动作:表示该条日志属于某条消息生命周期的哪个漏斗环节
+enum actionType: String {
+    case at_msg_backendReturn = "backendReturn" // 后端将消息返回给客户端
+    case at_msg_frontendPull = "frontendPull" // 客户端获取到
+    case at_msg_view = "view" // 在客户端消息被滑动展示在屏幕上
+    case at_msg_click = "click" // 用户点击消息
+}
+
+// MARK: - 消息类型
+
+/// 消息类型
+enum messageType: Int {
+    case mt_nomal = 0 // 未知消息
+    case mt_fans = 1 // 粉丝消息
+    case mt_like = 2 // 喜欢/赞
+    case mt_share = 3 // 分享消息
+    case mt_comment = 4 // 评论消息
+    case mt_notification = 5 // 通知消息
+    case mt_share_dynamics = 6 // 分享动态消息
+    /*** 自定义添加不涉及正式消息类型 ***/
+    case mt_badgeNumber = 1001 // 显示消息个数cell
+}
+
+/// 分享空间二级界面数据请求类型  1,观看 2,分享,3,喜欢 4, 评论
+enum sharePageType: Int {
+    case share_page_play = 1 // 分享空间-播放列表
+    case share_page_share = 2 // 分享空间-分享列表
+    case share_page_favorite = 3 // 分享空间-收藏列表
+    case share_page_commnet = 4 // 分享空间-评论列表
+}
+
+// MARK: - 消息子类型
+
+/// 消息子类型
+enum messageSubType: Int {
+    case mtsub_nomal = 0 // 未知消息
+    case mtsub_fansAtt = 101 // 粉丝-关注
+    case mtsub_fansSbs = 102 // 粉丝-订阅
+    case mtsub_likeColl = 201 // 喜欢-收藏视频
+    case mtsube_likePrai = 202 // 喜欢-评论点赞
+    case mtsub_shareWechat = 301 // 分享-微信会话
+    case mtsub_shareFriend = 302 // 分享-微信朋友圈
+    case mtsub_comment = 401 // 评论一级评论
+    case mtsub_commentSub = 402 // 评论二级评论
+    case mtsub_pushSucc = 501 // 通知-发布成功
+    case mtsub_pushNoPass = 502 // 通知-审核不通过
+    case mtsub_pushFixing = 503 // 通知-审核待修改
+    case mtsub_pushTransFail = 504 // 通知-转码失败
+    case mtsub_pushVideoRecom = 505 // 通知视频推荐
+    case mtsub_pushAccForbi = 506 // 通知-账号封禁
+    case mtsub_pushAccUnban = 507 // 通知-账号解封
+    case mtsub_pushIncStart = 508 // 通知-激励开始
+    case mtsub_pushIncEnd = 509 // 通知-激励结束
+    case mtsub_pushIncRupt = 510 // 通知-激励中断
+    case mtsub_shareSpace = 601 // 分享空间-分享
+    case mtsub_shareSpLike = 602 // 分享空间-喜欢
+    case mtsub_shareSpComm = 603 // 分享空间-评论
+    case mtsub_shareSpPlay = 604 // 分享空间-播放
+    case mtsub_shareSpMerge = 605 // 分享空间-外层合并消息(与业务逻辑无关,埋点上报使用)
+}
+
+// MARK: - 埋点上报消息类型(暂未统一)
+
+/// 埋点上报消息类型(暂未统一)
+enum messagePointType: Int {
+    case mt_point_nomal = 0 // 未知消息
+    case mt_point_fans = 1 // 粉丝消息
+    case mt_point_praise = 2 // 赞
+    case mt_point_barrage = 3 // 弹幕消息
+    case mt_point_comment = 4 // 评论消息
+    case mt_point_share = 5 // 分享消息
+    case mt_point_collect = 6 // 收藏消息
+    case mt_point_notification = 7 // 通知消息
+    case mt_point_operative = 8 // 运营消息
+    case mt_point_dynamics = 1000 // 分享动态消息
+}
+
+// MARK: - 日志库类型
+
+/// 日志库类型
+enum statisticsLogType: Int {
+    case st_log_type_abtestinfo = 10 // abtestinfo-Log ABTEST的 dlog Store
+    case st_log_type_operation = 20 // operation-Log 漏斗模型的相关上报
+    case st_log_type_simpleevent = 30 // simpleevent-Log 独立事件的相关上报
+    case st_log_type_frontend = 40 // frontend-Log 前端调试日志
+    case st_log_type_pLayaction = 50 // pLayaction-Log 播放行为日志上报
+    case st_log_type_useractive = 60 // useractive-Log 用户活跃日志上报
+    case st_log_type_videoPlayTracking = 70 // video-play-tracking 视频播放行为跟踪
+    case st_log_type_proauceSearch = 80 // proauce-search-log 素材搜索日志上报
+    case st_log_type_videoCompose = 90 // video-compose-log 创作工具素材搜索日志上报
+    case st_log_type_location = 100 // user-location-log 位置信息日志上报
+    case st_log_type_videoProduction = 110 // video-production-log 创作工具埋点日志上报
+}
+
+// MARK: - 冷启动方式
+
+/// coldLaunchType // 冷启动方式。若为热启动,则不用上报该字段
+enum coldLaunchType: String {
+    case coldLaunchType_userActiveOpen = "userActiveOpen" // 用户主动打开
+    case coldLaunchType_appRecall = "appRecall" // 其它APP唤起,包括H5应用宝唤起
+    case coldLaunchType_pushRecall = "pushRecall" // 推送唤起
+}
+
+// MARK: - 上报日志类型
+
+/// 上报日志类型
+enum reportLogType {
+    case reportLogType_view // 显示页面
+    case reportLogType_realPlay // 真实播放 视频播放到20s或播放到总时长30%,哪个先到为准
+    case reportLogType_play // 记录播放的视频
+    case reportLogType_Action // 上报视频动作记录
+    case reportLogType_Frontend // 通用上报
+}
+
+// MARK: - 底部tab
+
+/// 底部tab
+enum TAB_PAGETYPE: String {
+    case TAB_PAGETYPE_NORMAL = "nomalTab" // 默认
+    case TAB_PAGETYPE_RECOMM = "categoryTab" // 推荐
+    case TAB_PAGETYPE_ATTEN = "followTab" // 关注
+    case TAB_PAGETYPE_PUBLISH = "publishTab" // 发布
+    case TAB_PAGETYPE_MESSAGE = "messageTab" // 消息
+    case TAB_PAGETYPE_MINE = "mineTab" // 我的
+}
+
+// MARK: - 刷新控件类型
+
+/// 刷新控件类型
+enum REFRESH_TYPE {
+    case REFRESH_TYPE_ALL // 推荐
+    case REFRESH_TYPE_HEADER // 头部
+    case REFRESH_TYPE_FOOTER // 尾部
+}
+
+// MARK: - 刷新控件类型
+
+/// 刷新控件类型
+enum moveDirection {
+    case moveDirectionNormal
+    case moveDirectionUp
+    case moveDirectionDown
+    case moveDirectionRight
+    case moveDirectionLeft
+}
+
+// MARK: - 关注跟粉丝cell类型
+
+/// 关注跟粉丝cell类型
+enum atttendAndFansCellType {
+    case cellType_attend // 关注
+    case cellType_fans // 粉丝
+    case cellType_banned // 黑名单
+}
+
+// MARK: - 视频全屏播放操作类型
+
+/// 视频全屏播放操作类型
+enum fullScreenActionType {
+    case volume // 声音
+    case brightness // 亮度
+    case progress // 进度
+}
+
+// MARK: - 活动提示页类型
+
+/// 活动提示页类型
+enum activityRemindType {
+    case nomal //
+    case newUser // 新用户专享
+    case yesterDay_finish // 昨日已完成
+    case yesterDay_unfinish // 昨日未完成
+    case today_finish // 今日任务已完成
+}
+
+// MARK: - 制作工具素材搜索msgType
+
+/// 制作工具素材搜索msgType
+enum material_msgType: String {
+    case all = "ALL_SEARCH_EVENT" // 搜索所有
+    case image = "IMAGE_SEARCH_EVENT" // 图片搜索
+    case video = "VIDEO_SEARCH_EVENT" // 视频搜索
+    case gif = "GIF_SEARCH_EVENT" // 动图搜索
+    case recommend_image = "IMAGE_RECOMMEND_EVENT" // 图片推荐搜索
+    case recommend_video = "VIDEO_RECOMMEND_EVENT" // 视频推荐搜索
+    case recommend_gif = "GIF_RECOMMEND_EVENT" // 动图推荐搜索
+}
+
+// 画布类型
+enum videoCanvasType: Int {
+    case origin = 1 // 原始
+    case nineToSixteen = 2 // 9:16
+    case oneToOne = 3 // 1:1
+    case sixteenToNine = 4 // 16:9
+}
+
+/// 贴纸类型
+enum StickerType: String {
+    case UNKONW = "unknow"
+    case IMAGE = "image" // 图片
+    case VIDEO = "video" // 视频
+    case GIF = "gif" // gif动图
+    case VOICE = "voice" // 声音
+    case FILE = "file" // 文件
+    case SUBTITLE = "subtitle" // 字幕
+    /// 获取index
+    /// - Returns: description
+    func index() -> Int {
+        var fileType: Int = 0
+        switch self {
+        case .IMAGE:
+            fileType = 1
+        case .VIDEO:
+            fileType = 2
+        case .VOICE:
+            fileType = 3
+        case .FILE:
+            fileType = 4
+        case .GIF:
+            fileType = 5
+        case .SUBTITLE:
+            fileType = 6
+        default:
+            fileType = 0
+        }
+        return fileType
+    }
+
+    /// 媒体类型
+    /// - Returns: <#description#>
+    func mimeType() -> String {
+        var mimeType: String = "application/octet-stream"
+        switch self {
+        case .IMAGE:
+            mimeType = "image/jpeg"
+        case .VIDEO:
+            mimeType = "video/mpeg"
+        case .VOICE:
+            mimeType = "audio/mpeg"
+        case .FILE:
+            mimeType = "application/octet-stream"
+        case .GIF:
+            mimeType = "image/gif"
+        default:
+            mimeType = "application/octet-stream"
+        }
+        return mimeType
+    }
+
+    func pathExtension() -> String {
+        var pathExtension: String = ""
+        switch self {
+        case .IMAGE:
+            pathExtension = "png"
+        case .VIDEO:
+            pathExtension = "mp4"
+        case .VOICE:
+            pathExtension = "mp3"
+        case .FILE:
+            pathExtension = ""
+        case .GIF:
+            pathExtension = "gif"
+        default:
+            break
+        }
+        return pathExtension
+    }
+}
+
+// MARK: - 贴纸裁剪方式
+
+/// 贴纸裁剪方式
+enum stickerContentModeDef: Int {
+    case aspectFit = 0 // 自适应
+    case aspectFill = 1 // 铺满
+}
+
+/// 贴纸裁剪方式 add by ak v2
+enum stickerContentMode: String {
+    case aspectFitStr = "complete" // 完整显示(有黑边)
+    case aspectFillStr = "full" // 铺满
+}
+
+// MARK: - 适配模式
+
+/// 适配模式
+enum adapterModeDef: Int {
+    case speedyAuto = 0 // 快速自适应
+    case loopAuto = 1 // 自动循环
+    case crop = 2 // 定帧/裁剪
+}
+
+/// 适配模式 add by ak  v2
+enum adapterMode: String {
+    case multiple // 快速自适应
+    case loopAuto = "loop" // 自动循环
+    case staticFrame // 定帧/裁剪
+}
+
+// MARK: - 上传视频类型
+
+/// 上传视频类型
+enum videoUploadSourceType: String {
+    case videoUpload // 上传
+    case videoCompose // 合成制作
+    case videoUploadToCompose // 上传转合成制作
+}
+
+// MARK: - 进入创作工具入口
+
+/// 素材上传、保存、收藏相关
+enum videoMakeEntranceType: String {
+    case entranceMineTabDraft = "draft_metab" // 我的Tab -> 草稿箱列表 -> 创作工具
+    case entrancePublicTabDraft = "draft_uploadpopup" // 发布Tab -> 草稿箱列表 -> 创作工具
+    case entrancePublicTabCompose = "composeVideo" // 发布Tab -> 视频合成 -> 创作工具
+    case entrancePublicTabImageText = "imageTextGenerate" // 发布Tab -> 图文生成视频 -> 创作工具
+    case entrancePublicTabAlbum = "electronicAlbum" // 发布Tab -> 电子相册 -> 创作工具
+    case entranceReproduction = "reproduction" // 视频详情 -> 做同款 -> 创作工具
+    case entrancePublicTabUpload = "upload" // 发布Tab -> 上传视频 -> 直接发布
+    // add by ak
+    case entranceUpload2compose = "upload2compose" // 发布Tab -> 上传视频 -> 加工视频 -> 创作工具
+    case entranceStuckPointPublic = "syncedUpVideo" // 卡点视频发布
+
+    // * 新添加-自定义
+    case entrancePublicTabAddMusic = "addMusic" // 发布Tab -> 上传视频 -> 加音乐 -> 创作工具
+    case entrancePublicTabAddSubtitle = "addSubtitle" // 发布Tab -> 上传视频 -> 加字幕 -> 创作工具
+    case entrancePublicTabAddVoice = "addVoice" // 发布Tab -> 上传视频 -> 加语音 -> 创作工具
+    case entrancePublicTabAddSection = "addSection" // 发布Tab -> 上传视频 -> 多段拼接 -> 创作工具
+}
+
+// MARK: - 段落类型
+
+/// 段落类型
+enum sectionType: String {
+    case normal // 普通段
+    case global // 全局段
+}
+
+// MARK: - 音乐类型
+
+/// 音乐类型
+enum VOICETYPT: String {
+    case PRODUCE = "produce" // 合成语音
+    case BGM = "bgm" // 背景音乐
+    case SPEECH = "speech" // 录音
+    case LOCAL = "local" // 导入文件
+}
+
+// MARK: - 输入框状态B
+
+/// 输入框状态
+enum inputStatus {
+    case normal // 写故事,可智能配音,自动生成字幕
+    case inputing // 输入中
+    case recording // 语音识别成文字中…
+    case recordEmpty // 录音未识别到文字,点此输入
+    case recordError // 获取录音文字失败,请重试
+    case recordSuccess // 获取录音文字成功
+}
+
+// MARK: - 画面比例
+
+/// 画面比例
+enum aspectRatio {
+    case origin(width: CGFloat, height: CGFloat) // 原始
+    case oneToOne // 1:1
+    case sixteenToNine // 16:9
+    case nineToSixteen // 9:16
+}
+
+// MARK: - 卡点视频音乐页面类型
+
+/// 卡点视频音乐页面类型
+enum stuckPointMusicContentType {
+    case catagery
+    case serach
+    case page
+}
+
+// 视频发布来源场景 1:普通上传 2:创作工具,3:普通上传转创作工具,4:后台转换加工,5:卡点视频制作
+enum videoFromScene: Int {
+    case UploadNormal = 1 // 普通上传
+    case UploadMakeVideo = 2 // 创作工具
+    case UploadNormalToMakeVideo = 3 // 普通上传转创作工具
+    case server = 4 // 后台转换加工
+    case stuckPoint = 5 // 卡点视频制作
+}
+
+

+ 319 - 0
BFFramework/Classes/EventTrack/Model/PQVideoMakeEventTrackModel.swift

@@ -0,0 +1,319 @@
+//
+//  PQVideoMakeEventTrackModel.swift
+//  PQSpeed
+//
+//  Created by SanW on 2021/3/3.
+//  Copyright © 2021 BytesFlow. All rights reserved.
+//
+
+import UIKit
+
+class PQVideoMakeEventTrackModel: NSObject {
+    // 进入创作工具的入口
+    var entrance: videoMakeEntranceType = .entrancePublicTabCompose {
+        didSet {
+            isPureUploadVideo = entrance == .entrancePublicTabUpload
+            isReproduction = entrance == .entranceReproduction
+        }
+    }
+
+    // 草稿 Id
+    var draftId: String?
+    // 项目 Id
+    var projectId: String?
+    // 再创作视频的父 projectId - 仅「再创作」视频存在
+    var fatherProjectId: String?
+    // 再创作视频的根 projectId - 仅「再创作」视频存在
+    var rootProjectId: String?
+    // 再编辑视频的父 draftId - 仅「再编辑」视频存在
+    var fatherDraftId: String?
+
+    // 发布标题
+    var title: String?
+    // 发布描述
+    var videoDes: String?
+    // 发布封面的 URL
+    var coverUrl: String?
+    // 发布的视频 Id
+    var videoId: String?
+
+    /**
+      用户创作视频所用的时间成本,单位:毫秒(ms)
+      (仅包含合成前的时间段,不包含合成后选择封面等时间消耗)
+     如果是草稿箱项目,不包含之前累计的时间消耗,仅记录发布这一次的时间消耗
+     如果是再创作项目,不包含别人创作的时间消耗,仅记录发布这一次的时间消耗
+     如果是多次发布的项目,不包含之前累计的时间消耗,仅记录发布这一次的时间消耗
+     */
+    var editTimeCost: Float64 = 0
+    // 合成视频所用的时间成本,单位:毫秒(ms)
+    var composeTimeCost: Float64 = 0
+    // 上传视频所用的时间成本,单位:毫秒(ms)
+    var uploadTimeCost: Float64 = 0
+
+    // 是否为纯上传视频 纯上传视频:true 加工工具视频:false
+    var isPureUploadVideo: Bool = false
+    // 是否为再创作视频 再创作视频:true 非再创作视频:false
+    var isReproduction: Bool = false
+    // 是否为再编辑视频 再编辑视频:true 非再编辑视频:false
+    var isCopyVideo: Bool = false
+    // 段落相关-视频中存在的段落个数:number
+    var sectionNum: Int = 1
+    // 文字相关 段落中的文字长度信息:number[] [​ section1-text-length, section2-text-length, ... ]
+    var secTextLength: [Int] = Array<Int>.init()
+    // 图片 / 视频素材相关 段落中的(本地素材)图片数量:number[] [ section1-localImage-num, section2-localImage-num, ... ]
+    var secLocalImageNum: [Int] = Array<Int>.init()
+    // 图片 / 视频素材相关 段落中的(本地素材)动图数量:number[] [ section1-localGif-num, section2-localGif-num, ... ]
+    var secLocalGifNum: [Int] = Array<Int>.init()
+    // 图片 / 视频素材相关 段落中的(本地素材)视频数量:number[] [ section1-localVideo-num, section2-localVideo-num, ... ]
+    var secLocalVideoNum: [Int] = Array<Int>.init()
+    // 图片/视频素材相关 -段落中的(网络素材)图片数量
+    var secCloudImageNum: [Int] = Array<Int>.init()
+    // 图片/视频素材相关 -段落中(网络素材)动图数量
+    var secCloudGifNum: [Int] = Array<Int>.init()
+    // 图片/视频素材相关 -段落中(网络素材)视频数量
+    var secCloudVideoNum: [Int] = Array<Int>.init()
+    // 文字转语音相关 -段落中使用的语音素材名称,若段落中没有使用则报""
+    var secTextToSpeechMaterial: [String] = Array<String>.init()
+    // 文字转语音相关 -段落中文字转语音的毫秒数
+    var secTextToSpeechTime: [Int64] = Array<Int64>.init()
+    // 语音转文字相关 -每个段落中使用录音的毫秒数
+    var secSpeechToTextTime: [Int64] = Array<Int64>.init()
+
+    // 开始上传时间
+    var startUploadDate: Float64 = 0
+    // 结束上传时间
+    var endUploadDate: Float64 = 0 {
+        didSet {
+            uploadTimeCost = (endUploadDate - startUploadDate) * 1000
+        }
+    }
+
+    // 使用音乐的名称(未使用音乐默认为 "")
+    var musicName: String = ""
+    // 使用音乐Id
+    var musicId: String = ""
+    // 使用音乐的地址(未使用音乐默认为 "")
+    var musicUrl: String = ""
+    // 音乐的类型(未使用音乐默认为 "") - original - 原唱 - accompaniment - 伴奏
+    var musicType: String = ""
+    // 音乐是否为片段(未使用音乐默认为 "")- true - 音乐片段 - false - 完整音乐
+    var isMusicClip: Bool = false
+    // 画布比例
+    var canvasRatio: String = "original"
+    // 卡点视频 使用视频素材数量
+    var syncedUpVideoNumber: Int = 0
+    // 卡点视频 使用图片素材数量
+    var syncedUpImageNumber: Int = 0
+    // 卡点视频 使用音乐Id
+    var syncedUpMusicId: String = ""
+    // 卡点视频 使用音乐名称
+    var syncedUpMusicName: String = ""
+    // 卡点视频 合成后视频长度(单位:毫秒)
+    var syncedUpVideoDuration: Float64 = 0
+    // 卡点视频 原素材总时长(单位:毫秒)视频:报视频时长 图片:一张图报 1000ms
+    var syncedUpOriginalMaterialDuration: Float64 = 0
+    // 卡点视频 视频选用节奏名称(快节奏 1、适中 2、慢节奏 3))
+    var syncedUpRhythmNumber: Int = 2
+    override init() {
+        super.init()
+    }
+
+    /// 初始化
+    /// - Parameter projectModel: <#projectModel description#>
+    init(projectModel: PQEditProjectModel?, reCreateData: PQReCreateModel?) {
+        super.init()
+        if projectModel != nil {
+            draftId = projectModel?.draftboxId
+            projectId = projectModel?.projectId
+            sectionNum = projectModel?.sData?.sections.count ?? 1
+            if projectModel?.sData?.sections != nil, (projectModel?.sData?.sections.count ?? 0) > 0 {
+                for section in (projectModel?.sData?.sections)! {
+                    if section.sectionType == "normal" {
+                        // 段落中的文字长度信息
+                        let voiceType: VOICETYPT? = VOICETYPT(rawValue: section.sectionTimeline?.audioTrack?.audioTrackMaterials.first?.voiceType ?? "")
+                        if voiceType == .SPEECH || voiceType == .LOCAL {
+                            secTextLength.append(section.getInputSubtitle().count)
+                        } else {
+                            secTextLength.append(section.sectionText.count)
+                        }
+                        // 段落中使用的语音素材名称
+                        secTextToSpeechMaterial.append(section.sectionTimeline?.audioTrack?.audioTrackMaterials.first?.produceVoiceConfig?.voice ?? "")
+                        // 段落中文字转语音的毫秒数
+                        var duration: Float64 = 0
+                        if voiceType != nil && section.audioFilePath.count > 0 {
+                            let audioAsset = AVURLAsset(url: URL(fileURLWithPath: documensDirectory + section.audioFilePath), options: avAssertOptions)
+                            duration = Float64(audioAsset.duration.seconds * 1000)
+                        }
+                        if voiceType == .SPEECH || voiceType == .LOCAL {
+                            secSpeechToTextTime.append(Int64(duration))
+                            secTextToSpeechTime.append(0)
+                        } else {
+                            secTextToSpeechTime.append(Int64(duration))
+                            secSpeechToTextTime.append(0)
+                        }
+                        // 本地图片数量
+                        var localImageCount: Int = 0
+                        // 本地gif数量
+                        var localGifCount: Int = 0
+                        // 本地视频数量
+                        var localVideoCount: Int = 0
+                        // 网络图片数量
+                        var netImageCount: Int = 0
+                        // 网络gif数量
+                        var netGifCount: Int = 0
+                        // 网络视频数量
+                        var netVideoCount: Int = 0
+                        if section.sectionTimeline?.visionTrack?.visionTrackMaterials != nil, (section.sectionTimeline?.visionTrack?.visionTrackMaterials.count ?? 0) > 0 {
+                            for visionMaterial in (section.sectionTimeline?.visionTrack?.visionTrackMaterials)! {
+                                switch visionMaterial.type {
+                                case "image":
+                                    if visionMaterial.netResCoverImageURL != nil, (visionMaterial.netResCoverImageURL?.count ?? 0) > 0 {
+                                        netImageCount += 1
+                                    } else {
+                                        localImageCount += 1
+                                    }
+                                case "gif":
+                                    if visionMaterial.netResCoverImageURL != nil, (visionMaterial.netResCoverImageURL?.count ?? 0) > 0 {
+                                        netGifCount += 1
+                                    } else {
+                                        localGifCount += 1
+                                    }
+                                case "video":
+                                    if visionMaterial.netResUrl.count > 0 {
+                                        netVideoCount += 1
+                                    } else {
+                                        localVideoCount += 1
+                                    }
+                                default:
+                                    break
+                                }
+                            }
+                        }
+                        // 本地素材图片数量
+                        secLocalImageNum.append(localImageCount)
+                        // 本地素材动图数量
+                        secLocalGifNum.append(localGifCount)
+                        // 本地素材视频数量
+                        secLocalVideoNum.append(localVideoCount)
+                        // 网络素材图片数量
+                        secCloudImageNum.append(netImageCount)
+                        // 网络素材动图数量
+                        secCloudGifNum.append(netGifCount)
+                        // 网络素材视频数量
+                        secCloudVideoNum.append(netVideoCount)
+                    } else {
+                        sectionNum = sectionNum - 1
+                        if sectionNum <= 0 {
+                            sectionNum = 1
+                        }
+                    }
+                }
+            }
+        }
+        if reCreateData != nil {
+            fatherProjectId = reCreateData?.projectId
+            rootProjectId = reCreateData?.rootProjectId ?? reCreateData?.projectId
+            fatherDraftId = reCreateData?.draftboxId
+        }
+    }
+
+    /// 转换为字典
+    /// - Returns: <#description#>
+    func toParams() -> [String: Any] {
+        var eventTrackDic = Dictionary<String, Any>.init()
+        // 进入创作工具的入口
+        eventTrackDic["entrance"] = entrance.rawValue
+        // 发布的视频 Id
+        eventTrackDic["videoId"] = videoId ?? ""
+        // 草稿 Id
+        eventTrackDic["draftId"] = draftId ?? ""
+        // 项目 Id
+        eventTrackDic["projectId"] = projectId ?? ""
+        // 再创作视频的父 projectId - 仅「再创作」视频存在
+        eventTrackDic["fatherProjectId"] = fatherProjectId ?? ""
+        // 再创作视频的根 projectId - 仅「再创作」视频存在
+        eventTrackDic["rootProjectId"] = rootProjectId ?? ""
+        // 再编辑视频的父 draftId - 仅「再编辑」视频存在
+        eventTrackDic["fatherDraftId"] = fatherDraftId ?? ""
+        // 发布标题
+        eventTrackDic["title"] = title ?? ""
+        // 发布描述
+        eventTrackDic["description"] = videoDes ?? ""
+        // 发布封面的 URL
+        eventTrackDic["coverUrl"] = coverUrl ?? ""
+        /**
+          用户创作视频所用的时间成本,单位:毫秒(ms)
+          (仅包含合成前的时间段,不包含合成后选择封面等时间消耗)
+         如果是草稿箱项目,不包含之前累计的时间消耗,仅记录发布这一次的时间消耗
+         如果是再创作项目,不包含别人创作的时间消耗,仅记录发布这一次的时间消耗
+         如果是多次发布的项目,不包含之前累计的时间消耗,仅记录发布这一次的时间消耗
+         */
+        eventTrackDic["editTimeCost"] = Int64(editTimeCost)
+        // 合成视频所用的时间成本,单位:毫秒(ms)
+        eventTrackDic["composeTimeCost"] = Int64(composeTimeCost)
+        // 上传视频所用的时间成本,单位:毫秒(ms)
+        eventTrackDic["uploadTimeCost"] = Int64(uploadTimeCost)
+        // 是否为纯上传视频 纯上传视频:true 加工工具视频:false
+        eventTrackDic["isPureUploadVideo"] = isPureUploadVideo
+        // 是否为再创作视频 再创作视频:true 非再创作视频:false
+        eventTrackDic["isReproduction"] = isReproduction
+        // 是否为再编辑视频 再编辑视频:true 非再编辑视频:false
+        eventTrackDic["isCopyVideo"] = isCopyVideo
+        // 段落相关-视频中存在的段落个数:number
+        eventTrackDic["sectionNum"] = entrance == .entrancePublicTabUpload ? 0 : sectionNum
+        // 文字相关 -段落中的文字长度信息
+        eventTrackDic["secTextLength"] = secTextLength
+        // 图片/视频素材相关 -段落中的(本地素材)图片数量
+        eventTrackDic["secLocalImageNum"] = secLocalImageNum
+        // 图片/视频素材相关 -段落中的(本地素材)动图数量
+        eventTrackDic["secLocalGifNum"] = secLocalGifNum
+        // 图片/视频素材相关 -段落中的(本地素材)视频数量
+        eventTrackDic["secLocalVideoNum"] = secLocalVideoNum
+        // 图片/视频素材相关 -段落中的(网络素材)图片数量
+        eventTrackDic["secCloudImageNum"] = secCloudImageNum
+        // 图片/视频素材相关 -段落中(网络素材)动图数量
+        eventTrackDic["secCloudGifNum"] = secCloudGifNum
+        // 图片/视频素材相关 -段落中(网络素材)视频数量
+        eventTrackDic["secCloudVideoNum"] = secCloudVideoNum
+        // 文字转语音相关 -段落中使用的语音素材名称,若段落中没有使用则报""
+        eventTrackDic["secTextToSpeechMaterial"] = secTextToSpeechMaterial
+        // 文字转语音相关 -段落中文字转语音的毫秒数
+        eventTrackDic["secTextToSpeechTime"] = secTextToSpeechTime
+        // 语音转文字相关 -每个段落中使用录音的毫秒数
+        eventTrackDic["secSpeechToTextTime"] = secSpeechToTextTime
+        // 实验数据abInfoData
+        eventTrackDic["abInfoData"] = dictionaryToJsonString(PQSingletoMemoryUtil.shared.abInfoData) ?? ""
+        // 使用音乐的名称(未使用音乐默认为 "")
+        eventTrackDic["musicName"] = musicName
+        // 使用音乐Id
+        eventTrackDic["musicId"] = musicId
+        // 使用音乐的地址(未使用音乐默认为 "")
+        eventTrackDic["musicUrl"] = musicUrl
+        // 音乐的类型(未使用音乐默认为 "") - original - 原唱 - accompaniment - 伴奏
+        eventTrackDic["musicType"] = musicType
+        // 音乐是否为片段(未使用音乐默认为 "")- true - 音乐片段 - false - 完整音乐
+        if musicType.count > 0 {
+            eventTrackDic["isMusicClip"] = isMusicClip
+        } else {
+            eventTrackDic["isMusicClip"] = ""
+        }
+        // 画布比例
+        eventTrackDic["canvasRatio"] = canvasRatio
+        // 卡点视频 使用视频素材数量
+        eventTrackDic["syncedUpVideoNumber"] = syncedUpVideoNumber
+        // 卡点视频 使用图片素材数量
+        eventTrackDic["syncedUpImageNumber"] = syncedUpImageNumber
+        // 卡点视频 使用音乐Id
+        eventTrackDic["syncedUpMusicId"] = syncedUpMusicId
+        // 卡点视频 使用音乐名称
+        eventTrackDic["syncedUpMusicName"] = syncedUpMusicName
+        // 卡点视频 合成后视频长度(单位:毫秒)
+        eventTrackDic["syncedUpVideoDuration"] = syncedUpVideoDuration
+        // 卡点视频 原素材总时长(单位:毫秒)视频:报视频时长 图片:一张图报 1000ms
+        eventTrackDic["syncedUpOriginalMaterialDuration"] = syncedUpOriginalMaterialDuration
+        // 卡点视频 视频选用节奏名称(快节奏 1、适中 2、慢节奏 3))
+        eventTrackDic["syncedUpRhythmNumber"] = syncedUpRhythmNumber
+        PQLog(message: "创作工具埋点信息数据-\(eventTrackDic)")
+        return eventTrackDic
+    }
+}

+ 524 - 0
BFFramework/Classes/EventTrack/ViewModel/PQEventTrackViewModel.swift

@@ -0,0 +1,524 @@
+//
+//  PQEventTrackViewModel.swift
+//  PQSpeed
+//
+//  Created by SanW on 2020/11/3.
+//  Copyright © 2020 BytesFlow. All rights reserved.
+//
+
+import UIKit
+
+// MARK: - 埋点数据上报
+
+/// 埋点数据上报
+class PQEventTrackViewModel: NSObject {
+    /// 基础埋点上报
+    /// - Parameters:
+    ///   - logType: 数据库类型
+    ///   - businessType: businessType
+    ///   - objectType: objectType
+    ///   - eventData: eventData
+    ///   - pageSource: 页面场景
+    ///   - extParams: extParams 扩展字段,为json对象
+    ///   - remindmsg: remindmsg 打印提示信息
+    /// - Returns: <#description#>
+    class func baseReportUpload(logType: statisticsLogType = .st_log_type_simpleevent, businessType: businessType?, objectType: objectType?, pageSource: PAGESOURCE?, params: [String: Any]? = nil, eventData: [String: Any]? = nil, extParams: [String: Any]? = nil, remindmsg: String? = "基础") {
+        DispatchQueue.global().async {
+            // LogType
+            var tempParams: [String: Any] = params ?? [:]
+            tempParams["LogType"] = logType.rawValue
+            // pageSource
+            if pageSource != nil {
+                tempParams["pageSource"] = pageSource?.rawValue
+            }
+            // eventData
+            var tempEventData: [String: Any] = eventData ?? [:]
+            if objectType != nil {
+                tempEventData["objectType"] = objectType?.rawValue
+            }
+            if businessType != nil {
+                tempEventData["businessType"] = businessType?.rawValue
+            }
+            if tempEventData.keys.count > 0 {
+                tempParams["eventData"] = dictionaryToJsonString(tempEventData)
+            }
+            // extParams
+            if extParams != nil, (extParams?.keys.count ?? 0) > 0 {
+                tempParams["extParams"] = dictionaryToJsonString(extParams!)
+            }
+            SWNetRequest.postRequestData(url: PQENVUtil.shared.commonapi + staticsFrontendReportUrl, parames: tempParams) { _, _, _, _ in
+                PQLog(message: "\(remindmsg ?? "基础")埋点数据上报:\(tempParams)")
+            }
+        }
+    }
+
+    /// 视频相关上报
+    /// - Parameters:
+    ///   - reportLogType: 上报库类型
+    ///   - videoData: 视频数据
+    ///   - pageSource: 页面
+    ///   - businessType: <#businessType description#>
+    ///   - objectType: <#objectType description#>
+    ///   - extParams: <#extParams description#>
+    ///   - shareId: <#shareId description#>
+    ///   - videoIds: <#videoIds description#>
+    ///   - playId: <#playId description#>
+    ///   - headVideoId: <#headVideoId description#>
+    class func videoRelationReportUpload(reportLogType: reportLogType, videoData: PQVideoListModel?, pageSource: PAGESOURCE? = nil, businessType: businessType?, objectType: objectType? = nil, extParams: [String: Any]? = nil, shareId: String? = nil, videoIds: String? = nil, playId: String? = nil, headVideoId: String? = nil) {
+        var tempExtParams: [String: Any] = extParams ?? [:]
+        if videoData?.reCreateVideoData != nil {
+            tempExtParams["projectId"] = videoData?.reCreateVideoData?.projectId ?? ""
+            tempExtParams["parentProjectId"] = videoData?.reCreateVideoData?.parentProjectId ?? ""
+            tempExtParams["rootProjectId"] = videoData?.reCreateVideoData?.rootProjectId ?? ""
+            tempExtParams["canProduce"] = videoData?.reCreateVideoData?.canReproduce ?? 0
+            if !tempExtParams.keys.contains("clickedVideoId") {
+                tempExtParams["videoId"] = videoData?.uniqueId ?? "0"
+            }
+            if videoData?.reCreateVideoData?.parentVideoId != nil {
+                tempExtParams["parentVideoId"] = videoData?.reCreateVideoData?.parentVideoId ?? ""
+            }
+        }
+        if objectType == .ot_reproduce_clickButton || objectType == .ot_reproduce_collectionBar || objectType == .ot_reproduce_collectionClicButton || objectType == .ot_reproduce_sameSourceButton {
+            PQEventTrackViewModel.baseReportUpload(businessType: businessType, objectType: objectType, pageSource: pageSource != nil ? pageSource! : (videoData?.pageSource ?? .sp_category), extParams: tempExtParams, remindmsg: "再创作上报")
+        } else {
+            PQEventTrackViewModel.reportVideoPlayUpload(reportLogType: reportLogType, videoId: videoData?.uniqueId ?? "0", headVideoId: headVideoId ?? videoData?.headVideoId, videoIds: videoIds, pageSource: pageSource != nil ? pageSource! : (videoData?.pageSource ?? .sp_category), playId: playId ?? "", recommendId: videoData?.recommendId, recommendLogVO: videoData?.recommendLogVO, abInfoData: videoData?.abInfoData, measureType: videoData?.measureType, measureId: videoData?.measureId, businessType: businessType, shareId: shareId ?? "", extParams: tempExtParams, objectType: objectType)
+        }
+    }
+
+    /// 播放相关数据上报
+    /// - Parameters:
+    ///   - reportLogType: 日志类型
+    ///   - videoId: 视频Id
+    ///   - headVideoId: 当前的相关推荐视频是属于哪个视频的相关推荐,值为那个头部视频的videoId
+    ///   - videoIds: reportLogType_view时传
+    ///   - pageSource: 页面
+    ///   - playId: 播放ID 对于每一次播放操作,生成唯一playid,标示唯一一次播放操作,视频播放中暂停,再继续播放时,不算一次新的播放,不需要生成新的playid。重播视频算一次新的播放,即需要生成新的playid。
+    ///   - recommendId: 推荐链路ID 列表返回
+    ///   - recommendLogVO: 推荐日志对象 列表返回
+    ///   - abInfoData: AB信息 列表返回
+    ///   - measureType:
+    ///   - measureId:
+    ///   - businessType: 操作类型
+    ///   - targetUid: 视频用户ID
+    /// - Returns: <#description#>
+    class func reportVideoPlayUpload(reportLogType: reportLogType, videoId: String, headVideoId: String?, videoIds: String?, pageSource: PAGESOURCE, playId: String, recommendId: String?, recommendLogVO: String?, abInfoData: String?, measureType: Int?, measureId: Int?, businessType: businessType?, targetUid: Int = 0, shareId: String = "", extParams: [String: Any]? = nil, objectType: objectType? = nil) {
+        DispatchQueue.global().async {
+            var params: [String: Any] = ["videoId": videoId, "pageSource": pageSource.rawValue, "playId": playId, "targetUid": targetUid]
+            if measureType != nil {
+                params["measureType"] = measureType
+            }
+            if measureId != nil {
+                params["measureId"] = measureId
+            }
+            if recommendId != nil, !(recommendId?.isEmpty ?? true) {
+                params["recommendId"] = recommendId
+            }
+            if recommendLogVO != nil, !(recommendLogVO?.isEmpty ?? true) {
+                params["recommendLogVO"] = recommendLogVO
+            }
+            if abInfoData != nil, !(abInfoData?.isEmpty ?? true) {
+                params["abInfoData"] = abInfoData
+            }
+            if pageSource.rawValue.contains("speedApp-category") {
+                params["pageCategoryId"] = 55
+            }
+            // eventData
+            var tempEventData: [String: Any] = Dictionary<String, Any>.init()
+            if objectType != nil {
+                tempEventData["objectType"] = objectType?.rawValue
+            }
+            if businessType != nil {
+                tempEventData["businessType"] = businessType?.rawValue
+            }
+            // extParams
+            var tempExtParams: [String: Any] = extParams ?? [:]
+            if headVideoId != nil, (headVideoId?.count ?? 0) > 0 {
+                tempExtParams["headVideoId"] = (headVideoId ?? "0")
+            }
+            var url: String = PQENVUtil.shared.longvideoapi
+            switch reportLogType {
+            case .reportLogType_view:
+                url = url + videoViewReportUrl
+                if videoIds != nil, !videoIds!.isEmpty {
+                    params["videoIds"] = videoIds
+                } else {
+                    params["videoIds"] = videoId
+                }
+                params["viewId"] = getUniqueId(desc: "\(videoId)viewId")
+            case .reportLogType_realPlay:
+                url = url + videoRealPlayReportUrl
+                if businessType != nil {
+                    params["actionTriggerType"] = businessType?.rawValue
+                }
+            case .reportLogType_play:
+                url = url + videoPlayReportUrl
+            case .reportLogType_Action:
+                url = url + videoActionReportUrl
+                if businessType != nil, businessType == .bt_videoShareH5 || businessType == .bt_videoShareFriend {
+                    params["shareId"] = shareId
+                    params["rootLaunchShareId"] = shareId
+                    params["parentShareId"] = shareId
+                    params["shareDepth"] = "0"
+                }
+                if businessType != nil {
+                    params["businessType"] = businessType?.rawValue
+                }
+            case .reportLogType_Frontend:
+                url = PQENVUtil.shared.commonapi + staticsFrontendReportUrl
+                params["LogType"] = 30
+                params["eventData"] = dictionaryToJsonString(["tabIndex": PQSingletoMemoryUtil.shared.selectedTabIndex ?? "categoryTab", "businessType": businessType?.rawValue ?? ""])
+            }
+            if tempEventData.keys.count > 0 {
+                params["eventData"] = dictionaryToJsonString(tempEventData)
+            }
+            if tempExtParams.keys.count > 0 {
+                params["extParams"] = dictionaryToJsonString(tempExtParams)
+            }
+            SWNetRequest.postRequestData(url: url, parames: params) { response, _, error, _ in
+                PQLog(message: "播放相关数据上报:\(String(describing: error)),\(response ?? [:])")
+            }
+        }
+    }
+
+    /// 分享上报
+    /// - Parameters:
+    ///   - isShareVideo: 是否是分享视频
+    ///   - screenType: 分享场景 1-分享视频/用户 2-分享视频到朋友圈 3-分享视频到好友
+    ///   - videoId: 视频Id
+    ///   - pageSource: 页面枚举
+    ///   - recommendId: <#recommendId description#>
+    ///   - recommendLogVO: <#recommendLogVO description#>
+    ///   - abInfoData: <#abInfoData description#>
+    ///   - measureType: <#measureType description#>
+    ///   - measureId: <#measureId description#>
+    ///   - businessType: <#businessType description#>
+    ///   - targetUid: <#targetUid description#>
+    ///   - shareId: <#shareId description#>
+//    class func shareReportUpload(isShareVideo: Bool = true, screenType: Int = 1, videoId: String, pageSource: PAGESOURCE, recommendId: String?, recommendLogVO: String?, abInfoData: String?, measureType: Int?, measureId: Int?, businessType: businessType?, targetUid: Int?, shareId: String = "") {
+//        DispatchQueue.global().async {
+//            var url: String = PQENVUtil.shared.longvideoapi
+//            switch screenType {
+//            case 1:
+//                url = url + userShareReportUrl
+//            case 2:
+//                url = url + userShareH5ReportUrl
+//            case 3:
+//                url = url + userShareFriendReportUrl
+//            default:
+//                break
+//            }
+//            var params: [String: Any] = ["type": isShareVideo ? "1" : "2", "videoId": videoId, "pageSource": pageSource.rawValue, "playId": PQSingletoVideoPlayer.shared.playId, "targetUid": targetUid ?? 0, "shareDepth": "0"]
+//            if measureType != nil {
+//                params["measureType"] = measureType
+//            }
+//            if measureId != nil {
+//                params["measureId"] = measureId
+//            }
+//            params["shareId"] = shareId
+//            params["rootLaunchShareId"] = shareId
+//            params["parentShareId"] = shareId
+//            params["rootShareId"] = shareId
+//            if !PQLoginUserInfo.shared.openId.isEmpty {
+//                params["shareUi"] = PQLoginUserInfo.shared.openId
+//            }
+//            if pageSource.rawValue.contains("speedApp-category") {
+//                params["pageCategoryId"] = 55
+//            }
+//            if isShareVideo {
+//                params["shareObjectId"] = videoId
+//            } else {
+//                params["shareObjectId"] = targetUid
+//            }
+//            if businessType != nil {
+//                params["businessType"] = businessType?.rawValue
+//            }
+//            if recommendId != nil, !(recommendId?.isEmpty ?? true) {
+//                params["recommendId"] = recommendId
+//            }
+//            if recommendLogVO != nil, !(recommendLogVO?.isEmpty ?? true) {
+//                params["recommendLogVO"] = recommendLogVO
+//            }
+//            if abInfoData != nil, !(abInfoData?.isEmpty ?? true) {
+//                params["abInfoData"] = abInfoData
+//            }
+//            SWNetRequest.postRequestData(url: url, parames: params) { response, _, error, _ in
+//                PQLog(message: "用户点击分享数据上报:\(String(describing: error)),\(response ?? [:])")
+//            }
+//        }
+//    }
+
+    /// DNS上报
+    /// - Returns: <#description#>
+    class func dnsReportUpload() {
+        DispatchQueue.global().async {
+            let speedExtParams = parseDNS(hostUrl: "speed.piaoquantv.com")
+            let rescdnExtParams = parseDNS(hostUrl: "rescdn.yishihui.com")
+            if speedExtParams != nil {
+                PQEventTrackViewModel.baseReportUpload(businessType: .bt_dnsParseCostTime, objectType: nil, pageSource: nil, eventData: ["tabIndex": PQSingletoMemoryUtil.shared.selectedTabIndex ?? "categoryTab"], extParams: speedExtParams, remindmsg: "dnsParse")
+            }
+            if rescdnExtParams != nil {
+                PQEventTrackViewModel.baseReportUpload(businessType: .bt_dnsParseCostTime, objectType: nil, pageSource: nil, eventData: ["tabIndex": PQSingletoMemoryUtil.shared.selectedTabIndex ?? "categoryTab"], extParams: rescdnExtParams, remindmsg: "dnsParse")
+            }
+        }
+    }
+
+    /// 冷热启动数据上报
+    /// - Parameters:
+    ///   - isHotLaunch 是否是热启动
+    ///   - logType: <#logType description#>
+    ///   - eventId: <#eventId description#>
+    ///   - eventData: <#eventData description#>
+    ///   - extParams: <#extParams description#>
+    ///   - pageSource: <#pageSource description#>
+    /// - Returns: <#description#>
+    class func reportStatisticsUpload(isHotLaunch: Bool = false, logType: statisticsLogType, coldLaunchType _: coldLaunchType = .coldLaunchType_userActiveOpen, eventId _: String?, eventData _: String?, pageSource: PAGESOURCE?) {
+        DispatchQueue.global().async {
+            var params: [String: Any] = ["LogType": logType.rawValue]
+            if PQSingletoMemoryUtil.shared.isColdLaunch {
+                // 1-请求中 2-请求成功 3-请求失败
+                if PQSingletoMemoryUtil.shared.coldLaunchStatus != 2 {
+                    PQSingletoMemoryUtil.shared.coldLaunchStatus = 1
+                } else {
+                    return
+                }
+            }
+            params["eventData"] = dictionaryToJsonString(["tabIndex": PQSingletoMemoryUtil.shared.selectedTabIndex ?? "categoryTab"])
+            // 参数
+            var extParams: [String: Any] = Dictionary<String, Any>.init()
+            extParams["downloadChannel"] = channelID
+            extParams["launchParams"] = PQSingletoMemoryUtil.shared.commandLaunchParams
+            if pageSource?.rawValue.contains("speedApp-category") ?? false {
+                params["pageCategoryId"] = 55
+            }
+            if PQSingletoMemoryUtil.shared.commandReportParams != nil {
+                for (key, value) in PQSingletoMemoryUtil.shared.commandReportParams!.reversed() {
+                    extParams[key] = value
+                }
+            }
+            if !isHotLaunch {
+                extParams["coldLaunchType"] = (PQSingletoMemoryUtil.shared.coldLaunchType ?? .coldLaunchType_userActiveOpen).rawValue
+            } else {
+                extParams["hotLaunchType"] = (PQSingletoMemoryUtil.shared.coldLaunchType ?? .coldLaunchType_userActiveOpen).rawValue
+            }
+            if pageSource != nil {
+                params["pageSource"] = pageSource!.rawValue
+            }
+            // 是否第一次安装
+            let firstInstall: String? = getUserDefaults(key: cFirstInstall) as? String
+            if firstInstall == nil || (firstInstall?.count ?? 0 <= 0) || firstInstall != "1" {
+                extParams["isFirstLaunch"] = 1
+            } else {
+                extParams["isFirstLaunch"] = 0
+            }
+            params["extParams"] = dictionaryToJsonString(extParams)
+            // 是否第一次安装
+            let firstParams: String? = getUserDefaults(key: cFirstParams) as? String
+            if (firstInstall == nil || firstInstall?.count ?? 0 <= 0 || firstInstall != "1") && (firstParams != nil && ((firstParams?.count ?? 0) > 0)) {
+                params = jsonStringToDictionary(firstParams!) ?? Dictionary<String, Any>.init()
+            }
+            if firstParams == nil || ((firstParams?.count ?? 0) <= 0) {
+                saveUserDefaults(key: cFirstParams, value: dictionaryToJsonString(params) ?? "")
+            }
+            SWNetRequest.postRequestData(url: PQENVUtil.shared.commonapi + staticsFrontendReportUrl, parames: params) { response, _, error, _ in
+                PQLog(message: "冷热启动上报:\(String(describing: error)),\(response ?? [:]),params = \(params)")
+                if PQSingletoMemoryUtil.shared.isColdLaunch {
+                    PQSingletoMemoryUtil.shared.coldLaunchStatus = error == nil ? 2 : 3
+                }
+                // 清空启动数据
+                PQSingletoMemoryUtil.shared.coldLaunchType = nil
+                if error == nil, firstInstall == nil || firstInstall?.count ?? 0 <= 0 || firstInstall != "1" {
+                    saveUserDefaults(key: cFirstInstall, value: "1")
+                }
+                saveUserDefaults(key: cSelectedTabIndex, value: "categoryTab")
+            }
+        }
+    }
+
+    /// 推送点击数据上报
+    /// - Parameters:
+    ///   - pushId: 推送Id
+    ///   - pushTargetType: 1-推送单个视频 2-整体关注有更新 3-关注单个up主有更新(暂废弃) 4-订阅某人有更新
+    ///   - pushBrand: 推送平台 APPLE_TYPE
+    ///   - pushTargetId: pushTargetType == 1 视频ID,pushTargetType == 4 用户ID
+    ///   - bizParam 扩展参数
+    /// - Returns: <#description#>
+    class func reportPushActionUpload(pushId: String, pushTargetType: Int, pushBrand: String = cPushChannel, pushTargetId: String?,bizParam : [String: Any]? = nil) {
+        DispatchQueue.global().async {
+            var params: [String: Any] = bizParam ?? Dictionary<String,Any>.init()
+            if pushTargetId != nil {
+                params["pushTargetId"] = pushTargetId
+            }
+            if pushId.count > 0 {
+                params["pushId"] = pushId
+            }
+            params["pushTargetType"] = pushTargetType
+            params["pushBrand"] = pushBrand
+            params["pushReportType"] = "click"
+            if params.keys.contains("aps"){
+                params.removeValue(forKey: "aps")
+            }
+            SWNetRequest.postRequestData(url: PQENVUtil.shared.longvideoapi + pushActionReportUrl, parames: params) { response, _, error, _ in
+                PQLog(message: "推送点击数据上报:\(String(describing: error)),\(response ?? [:])")
+            }
+        }
+    }
+
+    /// 上报deviceToken
+    /// - Parameter registerId: 设备id
+    /// - Parameter deviceToken: <#deviceToken description#>
+    /// - Returns: <#description#>
+    class func reportPushDeviceTokenUpload(registerId: String, deviceToken: String, completeHander: @escaping (_ isSuccess: Bool) -> Void) {
+        DispatchQueue.global().async {
+            SWNetRequest.postRequestData(url: PQENVUtil.shared.longvideoapi + pushDeviceTokenReportUrl, parames: ["registerId": registerId, "deviceToken": deviceToken, "brand": cPushChannel]) { response, _, error, _ in
+                completeHander(error == nil ? true : false)
+                PQLog(message: "deviceToken数据上报:\(String(describing: error)),\(response ?? [:])")
+            }
+        }
+    }
+
+    /// 搜索上报
+    /// - Parameters:
+    ///   - keyWord: 搜索词
+    ///   - searchType: 1 热搜词搜索 2 历史记录 3 普通搜索
+    ///   - searchNumber: 数量
+    ///   - reportType: 1 into 2 click 3 show
+    /// - Returns: <#description#>
+    class func searchReportUpload(keyWord: String, searchType: Int, searchNumber: Int = 10, reportType: Int = 2) {
+        DispatchQueue.global().async {
+            let params: [String: Any] = ["keyWord": keyWord, "searchType": searchType, "searchNumber": searchNumber, "reportType": reportType]
+            SWNetRequest.postRequestData(url: PQENVUtil.shared.longvideoapi + searchReportUrl, parames: params) { response, _, error, _ in
+                PQLog(message: "搜索数据上报:\(String(describing: error)),\(response ?? [:])")
+            }
+        }
+    }
+
+    /// 发布视频的上报
+    /// - Parameters:
+    ///   - projectId:项目ID-发布创作的视频时必传,会在进入创作工具页时生成,以app_no_projectdata为前缀
+    ///   - businessType: <#businessType description#>
+    ///   - ossInfo: <#ossInfo description#>
+    ///   - params: <#params description#>
+    /// - Returns: <#description#>
+    class func publishReportUpload(projectId: String?, businessType: businessType, ossInfo: [String: Any], params: [String: Any]) {
+        DispatchQueue.global().async {
+            var extParams: [String: Any] = ["ossInfo": dictionaryToJsonString(ossInfo) ?? "", "params": dictionaryToJsonString(params) ?? ""]
+            if projectId != nil {
+                extParams["projectId"] = projectId
+            }
+            if !extParams.keys.contains("source") {
+                extParams["source"] = projectId != nil ? "videoCompose" : "videoUpload"
+            }
+            PQEventTrackViewModel.baseReportUpload(businessType: businessType, objectType: nil, pageSource: nil, extParams: extParams, remindmsg: "发布视频")
+        }
+    }
+
+    /// 处理视频创作素材搜索上报
+    /// - Parameters:
+    ///   - businessType: <#businessType description#>
+    ///   - materialList: <#materialList description#>
+    /// - Returns: <#description#>
+    class func dealWithMaterialSearchReportUpload(businessType: businessType, materialList: [PQEditVisionTrackMaterialsModel]) {
+        if materialList.count > 0 {
+            DispatchQueue.global().async {
+                for item in materialList {
+                    materialReportUpload(material: item, businessType: businessType)
+                }
+            }
+        }
+    }
+
+    /// 视频创作素材搜索上报
+    /// - Parameters:
+    ///   - material:搜索素材
+    ///   - searchId:搜索ID
+    ///   - businessType: <#businessType description#>
+    ///   - pageSource: <#pageSource description#>
+    /// - Returns: <#description#>
+    class func materialReportUpload(material: PQEditVisionTrackMaterialsModel?, businessType: businessType?, objectType: objectType? = nil) {
+        DispatchQueue.global().async {
+            var params: [String: Any] = [:]
+            var eventData: [String: Any] = [:]
+            var tempObjectType: objectType? = objectType
+            if tempObjectType == nil, material != nil {
+                if material?.type == StickerType.GIF.rawValue {
+                    tempObjectType = .ot_makevideo_gif
+                } else if material?.type == StickerType.VIDEO.rawValue {
+                    tempObjectType = .ot_makevideo_video
+                } else if material?.type == StickerType.IMAGE.rawValue {
+                    tempObjectType = .ot_makevideo_jpg
+                }
+            }
+            if material?.searchId != nil {
+                params["searchId"] = material?.searchId
+                eventData["searchId"] = material?.searchId
+            }
+            if material?.localSearchId != nil {
+                params["localSearchId"] = material?.localSearchId
+                eventData["localSearchId"] = material?.localSearchId
+            }
+            if material?.sliceId != nil {
+                params["sliceId"] = material?.sliceId
+                params["videoId"] = material?.sliceId
+            }
+            if material?.sourceType != nil {
+                params["sourceType"] = material?.sourceType
+            }
+            PQEventTrackViewModel.baseReportUpload(logType: .st_log_type_videoCompose, businessType: businessType, objectType: tempObjectType, pageSource: .sp_material_search, params: params, eventData: eventData, remindmsg: "视频创作素材搜索")
+        }
+    }
+
+    /// 站内消息埋点上报
+    /// - Parameters:
+    ///   - messageIds: 消息Id,多个用逗号分隔
+    ///   - clickId: 子入口点击ID,标识一次子入口点击动作。子入口内消息列表中的消息点击行为都带有此字段,分享空间消息除外
+    ///   - messageType: 消息类型
+    ///   - messageSubType: 消息子类型
+    ///   - actionType: 动作类型(backendCreate:后端构建;backendReturn:后端返回;frontendPull:前端拉取;view:曝光;click:点击)
+    ///   - objectType: <#objectType description#>
+    ///   - pageSource: <#pageSource description#>
+    ///   - readStatus: 已读状态:1:页面上显示未读 2:页面上显示已读
+    ///   - eventData: 扩展数据,json格式,日志系统里会展开存储
+    ///   - extParams: 扩展字段 json格式
+    ///   - remindmsg: 打印提示信息
+    /// - Returns: <#description#>
+    class func messageReportUpload(messageIds: String?, clickId: String?, messageType: messageType?, messageSubType: messageSubType?, actionType: actionType?, objectType: objectType?, pageSource: PAGESOURCE?, readStatus: Int = 1, eventData: [String: Any]? = nil, extParams: [String: Any]? = nil, remindmsg: String? = "基础") {
+        DispatchQueue.global().async {
+            var tempParams: [String: Any] = extParams ?? [:]
+            if messageType != nil, messageType != .mt_nomal {
+                tempParams["messageType"] = messageType?.rawValue
+            }
+            if messageSubType != nil, messageSubType != .mtsub_nomal {
+                tempParams["messageSubType"] = messageSubType?.rawValue
+            }
+            if actionType != nil {
+                tempParams["actionType"] = actionType?.rawValue
+            }
+            if messageIds != nil {
+                tempParams["messageIds"] = messageIds
+            }
+            tempParams["readStatus"] = readStatus + 1
+            if pageSource != nil {
+                tempParams["pageSource"] = pageSource?.rawValue
+            }
+            // eventData
+            var tempEventData: [String: Any] = eventData ?? [:]
+            if objectType != nil {
+                tempEventData["objectType"] = objectType?.rawValue
+            }
+            if tempEventData.keys.count > 0 {
+                tempParams["eventData"] = dictionaryToJsonString(tempEventData)
+            }
+            // extParams
+            var tempExtParams: [String: Any] = extParams ?? [:]
+            if clickId != nil {
+                tempExtParams["clickId"] = clickId
+            }
+            if tempExtParams.keys.count > 0 {
+                tempParams["extParams"] = dictionaryToJsonString(tempExtParams)
+            }
+            SWNetRequest.postRequestData(url: PQENVUtil.shared.longvideoapi + messagePeportUrl, parames: tempParams) { _, _, _, _ in
+                PQLog(message: "\(remindmsg ?? "基础")埋点数据上报:\(tempParams)")
+            }
+        }
+    }
+}

+ 100 - 0
BFFramework/Classes/PModels/PQDownloadModel.swift

@@ -0,0 +1,100 @@
+//
+//  PQDownloadModel.swift
+//  PQSpeed
+//
+//  Created by SanW on 2020/9/10.
+//  Copyright © 2020 BytesFlow. All rights reserved.
+//
+
+import UIKit
+
+// MARK: - 文件扩展名
+
+/// 文件扩展名
+enum FileExtensionType: String {
+    case normal = "" // 无后缀名
+    case png // png
+    case jpg // jpg
+    case gif // gif
+    case pdf // pdf
+    case mp4 // mp4
+    case mp3 // mp3
+    case m4a // m4a
+    case txt // txt
+    case zip // zip
+    /// 文件类型
+    /// - Returns: <#description#>
+    func fileType() -> String {
+        var fileType: String = "unknow"
+        switch self {
+        case .png, .jpg:
+            fileType = "image"
+        case .gif:
+            fileType = "gif"
+        case .mp3, .m4a:
+            fileType = "voice"
+        case .mp4:
+            fileType = "video"
+        default:
+            fileType = "unknow"
+        }
+        return fileType
+    }
+}
+
+// MARK: - 下载model
+
+/// 下载model
+class PQDownloadModel: NSObject {
+    var name: String? // 资源名
+    var fileExtensionType: FileExtensionType? // 文件后缀类型
+    var realFileExtensionType: FileExtensionType? // 真实文件后缀类型
+    var mimeType: String? { // 媒体类型
+        didSet {
+//            if mimeType?.contains("jpeg") ?? false { // image/jpeg
+//                realFileExtensionType = .jpg
+//            }else if mimeType?.contains("gif") ?? false { // image/gif
+//                realFileExtensionType = .gif
+//            }else if mimeType?.contains("pdf") ?? false{ // application/pdf
+//                realFileExtensionType = .pdf
+//            }else if mimeType?.contains("mp4") ?? false{ // video/mp4
+//                realFileExtensionType = .mp4
+//            }else if mimeType?.contains("mp3") ?? false{ // audio/mpeg
+//                realFileExtensionType = .mp3
+//            }else if mimeType?.contains("m4a") ?? false { // audio/x-m4a/audio/m4a
+//                realFileExtensionType = .m4a
+//            }
+            if mimeType?.contains("m4a") ?? false { // audio/x-m4a,audio/m4a
+                realFileExtensionType = .m4a
+            }
+        }
+    }
+
+    var sourceURL: String? // 源地址
+    var filePath: String? // 下载到本地地址
+    var imageURL: String? // 图片地址
+    var totalLength: Int64? // 总大小
+    var downloadLength: Int64? // 已下载大小
+    var task: URLSessionDataTask? // 下载任务
+    var progress: Float? // 下载进度
+    var state: downloadState? // 下载状态
+    var progressHandle: ProgressHandle? // 进度的回调
+    var stateHandle: StateHandle? // 状态回调
+    var fileHandle: FileHandle? // 文件句柄
+}
+
+// MARK: - 下载状态
+
+/// 下载状态
+enum downloadState: Int {
+    case downloading = 0 // 下载中
+    case compelte = 1 // 下载完成
+    case error = 2 // 下载失败
+    case pause = 3 // 暂停下载
+    case cancel = 4 // 取消下载
+}
+
+// 进度的回调
+typealias ProgressHandle = (_ progress: Float, _ downloadLength: Int64?, _ totalLength: Int64?) -> Void
+// 状态回调
+typealias StateHandle = (_ state: downloadState, _ url: String, _ localPath: String?, _ error: PQError?) -> Void

+ 178 - 0
BFFramework/Classes/PModels/PQLoginUserInfo.swift

@@ -0,0 +1,178 @@
+//
+//  PQLoginUserInfo.swift
+//  PQSpeed
+//
+//  Created by SanW on 2020/5/27.
+//  Copyright © 2020 BytesFlow. All rights reserved.
+//
+
+import UIKit
+
+// MARK: - 登录用户信息
+
+/// 登录用户信息
+class PQLoginUserInfo: NSObject {
+    static let shared = PQLoginUserInfo()
+    var uid: String = "" // 账号
+    var userCode: String = "" // 账号
+    var accessToken: String = "" // token
+    var avatarUrl: String = "" // 头像
+    var city: String = "" // 城市
+    var province: String = "" // 省市
+    var country: String = "" // 国家
+    var phoneNumber: String = "" // 电话
+    var openId: String = "" // 微信openId
+    var nickName: String = "" // 昵称
+    var gender: String = "" // 性别
+    var expiredTime: String = "" // 过期时间
+    var videos: String = "0" // 视频数
+    var idols: String = "0" // 关注数
+    var fans: String = "0" // 粉丝数
+    var otherSubscribes: String = "0" // 别人订阅我的数量
+    var userStatus: String = "1" // 1有效,2 已删除,3 已屏蔽,4 敏感
+    var isVirtualUser: Bool = false // 是否是虚拟账号
+    var mid = getMachineCode() // 设备ID
+
+    @objc func toString() -> String {
+        let json: [String: Any] = [
+            "accessToken": accessToken,
+            "avatarUrl": avatarUrl,
+            "city": city,
+            "country": country,
+            "gender": gender,
+            "nickName": nickName,
+            "openId": openId,
+            "phoneNumber": phoneNumber,
+            "province": province,
+            "uid": uid,
+            "userCode": userCode,
+            "expiredTime": expiredTime,
+            "videos": videos,
+            "idols": idols,
+            "fans": fans,
+            "userStatus": userStatus,
+            "isVirtualUser": isVirtualUser,
+            "mid": mid,
+        ]
+        return dictionaryToJsonString(json) ?? ""
+    }
+
+    override init() {
+        super.init()
+        resetData(isClear: false)
+        if getUserDefaults(key: cMineVideos) != nil {
+            videos = getUserDefaults(key: cMineVideos) as! String
+        }
+        if getUserDefaults(key: cMineIdols) != nil {
+            idols = getUserDefaults(key: cMineIdols) as! String
+        }
+        if getUserDefaults(key: cMineFans) != nil {
+            fans = getUserDefaults(key: cMineFans) as! String
+        }
+        if getUserDefaults(key: cOtherSubscribes) != nil {
+            otherSubscribes = getUserDefaults(key: cOtherSubscribes) as! String
+        }
+        if getUserDefaults(key: cAvatarUrl) != nil {
+            avatarUrl = getUserDefaults(key: cAvatarUrl) as! String
+        }
+        if getUserDefaults(key: cUpdatePhone) != nil {
+            phoneNumber = getUserDefaults(key: cUpdatePhone) as! String
+        }
+    }
+
+    @objc func resetData(isClear: Bool) {
+        if isClear {
+            UserDefaults.standard.removeObject(forKey: cUserInfoStorageKey)
+            UserDefaults.standard.removeObject(forKey: cMineVideos)
+            UserDefaults.standard.removeObject(forKey: cMineFans)
+            UserDefaults.standard.removeObject(forKey: cOtherSubscribes)
+            UserDefaults.standard.removeObject(forKey: cMineIdols)
+            UserDefaults.standard.removeObject(forKey: cAvatarUrl)
+            UserDefaults.standard.removeObject(forKey: cUpdatePhone)
+            UserDefaults.standard.synchronize()
+
+            accessToken = ""
+            avatarUrl = ""
+            city = ""
+            country = ""
+            gender = ""
+            nickName = ""
+            openId = ""
+            phoneNumber = ""
+            province = ""
+            uid = ""
+            userCode = ""
+            expiredTime = ""
+            videos = "0"
+            idols = "0"
+            fans = "0"
+            userStatus = "1"
+            isVirtualUser = false
+            mid = ""
+            return
+        }
+        let userInfo: [String: Any] = jsonStringToDictionary(UserDefaults.standard.string(forKey: cUserInfoStorageKey) ?? "") ?? [:]
+        updateData(userInfo: userInfo)
+    }
+
+    func updateData(userInfo: [String: Any]?) {
+        if userInfo != nil, userInfo?.count ?? 0 > 0 {
+            accessToken = "\(userInfo?["accessToken"] ?? "")"
+            if userInfo?.keys.contains("token") ?? false {
+                accessToken = "\(userInfo?["token"] ?? "")"
+            }
+            avatarUrl = "\(userInfo?["avatarUrl"] ?? "")"
+            city = "\(userInfo?["city"] ?? "")"
+            country = "\(userInfo?["country"] ?? "")"
+            gender = "\(userInfo?["gender"] ?? "")"
+            nickName = "\(userInfo?["nickName"] ?? "")"
+            openId = "\(userInfo?["openId"] ?? "")"
+            if userInfo?.keys.contains("phoneNumber") ?? false, !(userInfo?["phoneNumber"] is NSNull) {
+                phoneNumber = "\(userInfo?["phoneNumber"] ?? "")"
+            }
+            if userInfo?.keys.contains("userStatus") ?? false, !(userInfo?["userStatus"] is NSNull) {
+                userStatus = "\(userInfo?["userStatus"] ?? "")"
+            }
+            if userInfo?.keys.contains("mid") ?? false, !(userInfo?["mid"] is NSNull) {
+                mid = "\(userInfo?["mid"] ?? "")"
+            }
+            if userInfo?.keys.contains("isVirtualUser") ?? false, !(userInfo?["isVirtualUser"] is NSNull) {
+                isVirtualUser = (userInfo?["isVirtualUser"] as? Bool) ?? false
+            }
+            province = "\(userInfo?["province"] ?? "")"
+            uid = "\(userInfo?["uid"] ?? "")"
+            userCode = "\(userInfo?["userCode"] ?? "")"
+            expiredTime = "\(userInfo?["expiredTime"] ?? "")"
+            if getUserDefaults(key: cMineVideos) != nil {
+                videos = getUserDefaults(key: cMineVideos) as! String
+            }
+            if getUserDefaults(key: cAvatarUrl) != nil {
+                avatarUrl = getUserDefaults(key: cAvatarUrl) as! String
+            }
+            if getUserDefaults(key: cUpdatePhone) != nil {
+                phoneNumber = getUserDefaults(key: cUpdatePhone) as! String
+            }
+            if getUserDefaults(key: cMineIdols) != nil {
+                idols = getUserDefaults(key: cMineIdols) as! String
+            }
+            if getUserDefaults(key: cMineFans) != nil {
+                fans = getUserDefaults(key: cMineFans) as! String
+            }
+            if getUserDefaults(key: cOtherSubscribes) != nil {
+                otherSubscribes = getUserDefaults(key: cOtherSubscribes) as! String
+            }
+        }
+    }
+
+    func isLogin() -> Bool {
+        return accessToken.count > 0
+    }
+
+    override func copy() -> Any {
+        return self
+    }
+
+    override func mutableCopy() -> Any {
+        return self
+    }
+}

+ 75 - 0
BFFramework/Classes/PModels/PQReCreateModel.swift

@@ -0,0 +1,75 @@
+//
+//  PQReCreateModel.swift
+//  PQSpeed
+//
+//  Created by SanW on 2020/12/28.
+//  Copyright © 2020 BytesFlow. All rights reserved.
+//
+
+import RealmSwift
+import UIKit
+
+class PQReCreateModel: Object {
+    @objc dynamic var canReproduce: Int = 0 // 是否可以被再创作,1:是,0:否
+    @objc dynamic var draftboxId: String? // 草稿ID
+    @objc dynamic var parentProjectId: String? // 父项目ID
+    @objc dynamic var rootProjectId: String? // 根项目ID
+    @objc dynamic var projectId: String? // 项目ID
+    @objc dynamic var projectLinkUrl: String? // 项目链接
+    @objc dynamic var reProduceCount: Int = 0 // 被再创作次数
+    @objc dynamic var reProduceVideoFlag: Int = 0 //  再创作视频标记,1:是,0:否
+    @objc dynamic var videoId: String? // 视频id
+    @objc dynamic var parentVideoId: String? // 父视频id
+    @objc dynamic var rootVideoId: String? // 根视频id
+    @objc dynamic var rhythmMusicFlag: Int = 0 // 是否有卡点音乐标记 1:是,0:否
+    @objc dynamic var rhythmMusicName: String? // 卡点音乐歌名
+    var rhythmMusicNameWidth: CGFloat = 0 // 卡点音乐显示宽度
+
+    override required init() {
+        super.init()
+    }
+
+    init(jsonDict: [String: Any]) {
+        super.init()
+        if jsonDict.keys.contains("canReproduce") {
+            canReproduce = Int("\(jsonDict["canReproduce"] ?? "0")") ?? 0
+        }
+        if jsonDict.keys.contains("draftboxId"), "\(jsonDict["draftboxId"] ?? "")" != "<null>" {
+            draftboxId = "\(jsonDict["draftboxId"] ?? "")"
+        }
+        if jsonDict.keys.contains("parentProjectId"), "\(jsonDict["parentProjectId"] ?? "")" != "<null>" {
+            parentProjectId = "\(jsonDict["parentProjectId"] ?? "")"
+        }
+        if jsonDict.keys.contains("rootProjectId"), "\(jsonDict["rootProjectId"] ?? "")" != "<null>" {
+            rootProjectId = "\(jsonDict["rootProjectId"] ?? "")"
+        }
+        if jsonDict.keys.contains("projectId"), "\(jsonDict["projectId"] ?? "")" != "<null>" {
+            projectId = "\(jsonDict["projectId"] ?? "")"
+        }
+        if jsonDict.keys.contains("projectLinkUrl") {
+            projectLinkUrl = "\(jsonDict["projectLinkUrl"] ?? "")"
+        }
+        if jsonDict.keys.contains("reProduceCount") {
+            reProduceCount = Int("\(jsonDict["reProduceCount"] ?? "0")") ?? 0
+        }
+        if jsonDict.keys.contains("reProduceVideoFlag") {
+            reProduceVideoFlag = Int("\(jsonDict["reProduceVideoFlag"] ?? "0")") ?? 0
+        }
+        if jsonDict.keys.contains("parentVideoId"), "\(jsonDict["parentVideoId"] ?? "")" != "<null>" {
+            parentVideoId = "\(jsonDict["parentVideoId"] ?? "")"
+        }
+        if jsonDict.keys.contains("rhythmMusicFlag") {
+            rhythmMusicFlag = Int("\(jsonDict["rhythmMusicFlag"] ?? "0")") ?? 0
+        }
+        if jsonDict.keys.contains("rhythmMusicName"), "\(jsonDict["rhythmMusicName"] ?? "")" != "<null>" {
+            rhythmMusicName = "\(jsonDict["rhythmMusicName"] ?? "")"
+            if rhythmMusicFlag == 1 {
+                rhythmMusicNameWidth = sizeWithText(text: rhythmMusicName ?? "", font: UIFont.systemFont(ofSize: 13), size: CGSize(width: cScreenWidth - cDefaultMargin * 10 - 32 - 40, height: cDefaultMargin * 3)).width
+                if rhythmMusicNameWidth < cDefaultMargin * 4 {
+                    rhythmMusicNameWidth = cDefaultMargin * 4
+                }
+                rhythmMusicNameWidth = rhythmMusicNameWidth + 32 + 40
+            }
+        }
+    }
+}

+ 31 - 0
BFFramework/Classes/PModels/PQUploadModel.swift

@@ -0,0 +1,31 @@
+
+//
+//  PQUploadModel.swift
+//  PQSpeed
+//
+//  Created by SanW on 2020/8/1.
+//  Copyright © 2020 BytesFlow. All rights reserved.
+//
+
+import Photos
+import UIKit
+
+class PQUploadModel: PQBaseModel {
+    var image: UIImage? // 图片
+    var localPath: String? // 地址
+    var duration: TimeInterval = 0 // 时间
+    var asset: PHAsset? // 视频资源
+    var videoBucketKey: String? // 上传视频功能
+    var imageBucketKey: String? // 上传图片地址
+    var assetCollection: PHAssetCollection? // 相簿
+    var categoryList: PHFetchResult<PHAsset> = PHFetchResult<PHAsset>.init() // 子相册集合
+    var assetList: [PHAsset] = Array<PHAsset>.init() // 子相册集合
+    var uploadID: String? // 上传ID
+    var videoWidth: CGFloat = 0 // 视频宽
+    var videoHeight: CGFloat = 0 // 视频高
+    var contentMode: UIView.ContentMode = .scaleAspectFill
+    var stsToken: [String: Any]? // 上传信息
+
+    // add by ak 上传来源类型
+    var videoFromScene: videoFromScene = .UploadNormal
+}

+ 174 - 0
BFFramework/Classes/PModels/PQUserInfoModel.swift

@@ -0,0 +1,174 @@
+//
+//  PQUserInfoModel.swift
+//  PQSpeed
+//
+//  Created by SanW on 2020/5/27.
+//  Copyright © 2020 BytesFlow. All rights reserved.
+//
+
+import UIKit
+
+class PQUserInfoModel: PQBaseModel {
+    var avatarUrl: String? // 头像地址
+    var backgroundImage: String? // 背景图
+
+    var bothFollow: Bool = false // 是否相互关注
+    var fans: Int = 0 // 粉丝数
+    var followed: Int = 0 // 是否关注
+    var idols: Int = 0 // 关注的人数
+    var introduction: String?
+    var nickName: String? // 昵称
+
+    var otherVideoShowCount: Int = 0
+    var playCountTotal: Int = 0 // 用户视频总播放数,按人去重 ,
+    var playCountFormatStr: String? // 用户视频总播放数,格式化后的值,前端直接显示 ,
+    var positionType: Int = 0
+    var sensitiveStatus: Int = 0
+    var subscribeStatus: Int = 0 // 0:未订阅,1:已订阅
+    var mySubscribes: Int = 0 // 我的订阅数
+    var otherSubscribes: Int = 0 // 别人订阅我的数量
+    var uid: Int = 0
+    var uploadDate: String?
+    var userType: Int = 0
+    var videos: Int = 0 // 视频数
+    var videosDescr: String = "0" // 视频数
+    var vipStatus: Int = 0 // vip状态,0:不是vip,1:是vip
+    var vipDesc: String? // vip身份描述
+    var tagList: [String]? // 标签
+    var picPathList: [[String: Any]]? // 推荐列表
+    var isLoginUser: Bool = false // 是否是登录用户
+    var gmtCreateTimestamp: Int = 0
+    var intimacy: Int = 0
+    var isBothFollow: Int = 0 // 是否相互关注 ,
+    var isFollowed: Bool = false // 是否关注
+    var lastTimestamp: Int = 0
+    var latestSendvideoId: Int = 0
+    var updated: Int = 0
+    var favoriteCount: Int = 0 // 喜欢的视频数
+    var shareCount: Int = 0 // 分享的视频数
+    var isBanned: Bool = false // 是否被拉黑
+    var tab_pageType: TAB_PAGETYPE = .TAB_PAGETYPE_NORMAL // 0-推荐 1-关注
+
+    required init() {
+        super.init()
+    }
+
+    override init(jsonDict: [String: Any]) {
+        super.init()
+        if jsonDict.keys.contains("avatarUrl") {
+            avatarUrl = "\(jsonDict["avatarUrl"] ?? "")"
+        }
+        if jsonDict.keys.contains("bothFollow") {
+            bothFollow = Bool("\(jsonDict["bothFollow"] ?? "")") ?? false
+        }
+        if jsonDict.keys.contains("fans") {
+            fans = Int("\(jsonDict["fans"] ?? "0")") ?? 0
+        }
+        if jsonDict.keys.contains("followed") {
+            followed = Int("\(jsonDict["followed"] ?? "0")") ?? 0
+        }
+        if jsonDict.keys.contains("idols") {
+            idols = Int("\(jsonDict["idols"] ?? "0")") ?? 0
+        }
+        if jsonDict.keys.contains("introduction") {
+            introduction = "\(jsonDict["introduction"] ?? "0")"
+        }
+        if jsonDict.keys.contains("nickName") {
+            nickName = "\(jsonDict["nickName"] ?? "0")"
+        }
+        if jsonDict.keys.contains("otherVideoShowCount") {
+            otherVideoShowCount = Int("\(jsonDict["otherVideoShowCount"] ?? "0")") ?? 0
+        }
+        if jsonDict.keys.contains("playCountTotal") {
+            playCountTotal = Int("\(jsonDict["playCountTotal"] ?? "0")") ?? 0
+        }
+        if jsonDict.keys.contains("playCountFormatStr") {
+            playCountFormatStr = "\(jsonDict["playCountFormatStr"] ?? "0")"
+        }
+        if jsonDict.keys.contains("positionType") {
+            positionType = Int("\(jsonDict["positionType"] ?? "0")") ?? 0
+        }
+        if jsonDict.keys.contains("favoriteCount") {
+            favoriteCount = Int("\(jsonDict["favoriteCount"] ?? "0")") ?? 0
+        }
+        if jsonDict.keys.contains("shareCount") {
+            shareCount = Int("\(jsonDict["shareCount"] ?? "0")") ?? 0
+        }
+        if jsonDict.keys.contains("sensitiveStatus") {
+            sensitiveStatus = Int("\(jsonDict["sensitiveStatus"] ?? "0")") ?? 0
+        }
+        if jsonDict.keys.contains("subscribeStatus") {
+            subscribeStatus = Int("\(jsonDict["subscribeStatus"] ?? "0")") ?? 0
+        }
+        if jsonDict.keys.contains("mySubscribes") {
+            mySubscribes = Int("\(jsonDict["mySubscribes"] ?? "0")") ?? 0
+        }
+        if jsonDict.keys.contains("otherSubscribes") {
+            otherSubscribes = Int("\(jsonDict["otherSubscribes"] ?? "0")") ?? 0
+        }
+        if jsonDict.keys.contains("uid") {
+            uid = Int("\(jsonDict["uid"] ?? "0")") ?? 0
+        }
+        if jsonDict.keys.contains("uploadDate") {
+            uploadDate = "\(jsonDict["uploadDate"] ?? "0")"
+        }
+        if jsonDict.keys.contains("userType") {
+            userType = Int("\(jsonDict["userType"] ?? "0")") ?? 0
+        }
+        if jsonDict.keys.contains("videos") {
+            videos = Int("\(jsonDict["videos"] ?? "0")") ?? 0
+        }
+        if jsonDict.keys.contains("videosDescr") {
+            videosDescr = "\(jsonDict["videosDescr"] ?? "")"
+        }
+        if jsonDict.keys.contains("vipStatus") {
+            vipStatus = Int("\(jsonDict["vipStatus"] ?? "0")") ?? 0
+        }
+        if jsonDict.keys.contains("tagList") {
+            tagList = jsonDict["tagList"] as? [String]
+        }
+        if jsonDict.keys.contains("picPathList") {
+            picPathList = jsonDict["picPathList"] as? [[String: Any]]
+        }
+        if jsonDict.keys.contains("vipDesc") {
+            vipDesc = "\(jsonDict["vipDesc"] ?? "")"
+        }
+        if jsonDict.keys.contains("isLoginUser") {
+            isLoginUser = Bool("\(jsonDict["isLoginUser"] ?? "")") ?? false
+        }
+        if jsonDict.keys.contains("gmtCreateTimestamp") {
+            gmtCreateTimestamp = Int("\(jsonDict["gmtCreateTimestamp"] ?? "0")") ?? 0
+        }
+        if jsonDict.keys.contains("intimacy") {
+            intimacy = Int("\(jsonDict["intimacy"] ?? "0")") ?? 0
+        }
+        if jsonDict.keys.contains("isBothFollow") {
+            isBothFollow = Int("\(jsonDict["isBothFollow"] ?? "0")") ?? 0
+        }
+        if jsonDict.keys.contains("isFollowed") {
+            isFollowed = jsonDict["isFollowed"] as! Bool
+        }
+        if jsonDict.keys.contains("lastTimestamp") {
+            lastTimestamp = Int("\(jsonDict["lastTimestamp"] ?? "0")") ?? 0
+        }
+        if jsonDict.keys.contains("latestSendvideoId") {
+            latestSendvideoId = Int("\(jsonDict["latestSendvideoId"] ?? "0")") ?? 0
+        }
+        if jsonDict.keys.contains("updated") {
+            updated = Int("\(jsonDict["updated"] ?? "0")") ?? 0
+        }
+    }
+
+    /// 创建虚拟用户数据
+    /// - Parameter virtual: <#virtual description#>
+    init(avatarIcon: String?, userName: String?) {
+        super.init()
+        avatarUrl = avatarIcon
+        nickName = userName
+        fans = Int(arc4random() % 10)
+        followed = Int(arc4random() % 10)
+        idols = Int(arc4random() % 10)
+        mySubscribes = Int(arc4random() % 10)
+        otherSubscribes = Int(arc4random() % 10)
+    }
+}

+ 352 - 0
BFFramework/Classes/PModels/PQVideoListModel.swift

@@ -0,0 +1,352 @@
+//
+//  PQRecommVideoModel.swift
+//  PQSpeed
+//
+//  Created by SanW on 2020/5/26.
+//  Copyright © 2020 BytesFlow. All rights reserved.
+//
+
+import UIKit
+
+class PQVideoListModel: PQBaseModel {
+    @objc required init() {
+        super.init()
+    }
+
+    var headVideoId: String? // 当前的相关推荐视频是属于哪个视频的相关推荐,值为那个头部视频的videoId
+    var auditStatus: Int = 0 // 审核状态 1 审核中,2 不通过 3 待修改,4 自己可见 5 通过 ,
+    var barrageCount: Int = 0 // 弹幕数量
+    var barrageSwitch: Int = 0 // 是否打开弹幕 1打开 -1关闭 ,
+    var auditReason: String? // 审核不通过或者待修改的原因
+    var barrage: Any? // 弹幕集合
+    var chargeDetail: [String: Any]? // 收费的相关信息
+    var commentCount: Int = 0 // 评论数量
+    var coverImg: [String: Any]? // 封面对象 ,
+    var cutVoStr: String? // h5剪切板内容 ,
+    var descr: String? // 视频简介 ,
+    var encryption: Int = 0 // 是否加密视频:0是1不是 ,
+    var favorited: Bool = false // 是否收藏 ,
+    var favoriteds: Int = 0 //  收藏数 ,
+    var fileExtensions: String? // 视频后缀 ,
+    var firstPicture: Bool = false // 封面是否是第一帧,false不是true是 ,
+    var gmtCreate: String? // 创建时间 ,
+    var gmtCreateDescr: String? // 发视频时间描述 ,
+    var gmtCreateTimestamp: Int = 0 // 创建时间戳 ,
+    var gmtModifie: String? // 修改时间 ,
+    var gmtModifiedTimestamp: Int = 0 // 修改时间戳 ,
+    var h5ShareImgPath: String? // h5分享图URL ,
+    var hasShareSpaceData: Bool = false // 是否有分享空间数据,
+    var height: CGFloat = 0 // 视频高 ,
+    var isRecommendShare: Int = 0 // 是否有分发推荐的封面和标题 1 有 0 无 传空或者不传默认为0,
+    var lastTimestamp: Int = 0 // 时间戳 ,
+    //  liteVideoData (LiteVideoDataVO, optional): lite数据,
+    var measure: Int = 0
+    var measureId: Int = 0
+    var measureType: Int = 0 // 0 非流量池 1曝光池2普通推荐测试池3待推荐测试池 ,
+    var playBeforeDay: Int = 0 // 播放时间距离今天的天数 ,
+    var playCount: Int = 0 // 播放次数 ,
+    var playCountFormatStr: String? //  用户视频总播放数,格式化后的值,前端直接显示 ,
+    var playCountTotal: Int = 0 // 总播放次数 ,
+    var playTime: Int = 0 // 播放时间 ,
+    var processShareHeadLab: [String: Any]? // 视频分享片尾数据 ,
+    var processShareTailLab: [String: Any]? // 视频分享片尾数据 ,
+    var pwd: String? // 视频密码 ,
+    var recommendId: String? // 推荐链路ID ,
+    var recommendSource: Int = 0 // 0 默认 1 第四范式
+    var recommendStatus: Int = 0 // 推荐状态 ,
+    var rotate: Int = 0 // 旋转角度 ,
+    var sampleJobId: String?
+    var sampleRequestId: String?
+    var sampleTotalTime: Int = 0
+    var sampleTranscodeStatus: Int = 0
+    var sampleTransedVideoPath: String?
+    var sendBeforeDay: Int = 0 // 发视频距离今天的天数 ,
+    var sensitiveMsg: String? // 敏感提示信息 ,
+    var sensitiveStatus: Int = 0 //  内容敏感状态(0:未检验,1:不敏感,2:敏感,3:敏感已审) ,
+    var shareCount: Int = 0 // 分享到朋友圈次数 ,
+    // add by ak 个人中心里我分享的视频列表返回的参数 e.g. 78C10B44-6892-42A8-AE69-F1B35F9E676F-534530
+    var shareId: String? // 分享的 ID
+    var shareCountFriend: Int = 0 // 分享到微信好友 ,
+    var shareImgPath: String? // 分享图URL ,
+    var shareLinkType: Int = 0 // 分享到微信好友的图片的链接的类型 ,
+    var sharePageType: Int = 0 // 0 综合模块 1 feed流 ,
+    var shareTitle: String? // 分享到微信好友的图片的title ,
+    var showHotRecommend: Bool = false // 是否需要显示热门推荐 ,
+    var size: Int = 0 //  大小 ,
+    var status: Int = 0 // 数据状态,1 有效,2 已删除,3 已屏蔽,4 关注可见,5 分享可见 6 自己可见 ,
+    var tabShareImgPath: String? // 转发分享图URL ,
+    var thumbnailImagePath: String? // 缩略图URL ,
+    var totalTime: Int = 0 // 视频时长 ,
+    var totalTimeParas: String? // 视频时长十分秒 ,
+    var transcodeStatus: Int = 0 // 转码状态:1-不需转码 2-转码中 3-转码完成 4-转码失败 ,
+    var transcodeVOList: [Any]? // 多码率数据 ,
+    var uid: Int = 0 // 视频的用户ID ,
+    var user: [String: Any]? //  用户对象 ,
+    var userInfo: PQUserInfoModel? //  用户对象 ,
+    var videoCollectionId: Int = 0 // 视频所在的视频集ID ,
+    var videoCoverSnapshotPath: String? // 原始封面图片 ,
+    var videoPath: String? // 视频地址 ,
+    var videoURL: String? // 视频地址 ,
+    var videoReportMeta: String? // 视频上报数据,上报时原样返回 ,
+    var videoShareJumpModel: [String: Any]? // 分享页跳转的信息 ,
+    var width: CGFloat = 0 // 视频宽
+    var itemHeight: CGFloat = 0 // 个人中心cell高
+    var originImageH: CGFloat = 0 // 原始图片的宽
+    var originImageW: CGFloat = 0 // 原始图片的高
+    var imageH: CGFloat = 0 // 图片的高
+    var imageW: CGFloat = 0 // 图片的宽
+    var titleH: CGFloat = 0 // 标题的高
+    var descH: CGFloat = 0 // 描述的高
+    var titleFontSize: CGFloat = 0 // add by ak 标题字号
+    var usnameW: CGFloat = 0 // add by ak 用户名宽度
+    var rotationH: CGFloat = 0 // add by ak 三个推荐视频高度包括标题
+    var watchInfoH: CGFloat = 0 // add by ak watch info 高度
+    var watchInfoY: CGFloat = 0 // add by ak watch info Y 值
+    var relationData: [PQVideoListModel]?
+    var playProgress: CGFloat = 0 // 已播放时长
+    var duration: CGFloat = 0 // 视频总时长
+    var tab_pageType: TAB_PAGETYPE = .TAB_PAGETYPE_NORMAL // 0-推荐 1-关注
+    var pageSource: PAGESOURCE = .sp_category
+    var isVerticality: Bool = false
+    var isShareList: Bool = false // 是否是分享列表
+
+    var funcH: CGFloat = cDefaultMargin * 33
+    let funcW: CGFloat = cDefaultMargin * 5
+    var uplpadImage: UIImage? // 上传的图片封面
+    var uplpadBucketKey: String? // 上传视频地址
+    var uplpadStatus: Int = 0 // 上传视频状态  1-上传中 2-上传完成 3-上传失败 4-发布中 4-发布完成
+    var uplpadRequest: OSSMultipartUploadRequest?
+    var stsToken: [String: Any]? // 上传信息
+    var localPath: String? // 地址
+    var progress: Float = 0
+    var projectId: String? // 项目ID-发布创作的视频时必传,会在进入创作工具页时生成,以app_no_projectdata为前缀
+    var reCreateVideoData: PQReCreateModel? // 再创作数据
+    // 视频创作埋点数据
+    var eventTrackData: PQVideoMakeEventTrackModel?
+    var autoType: autoType? // autoType 自动动作的类型
+    // add by ak 发布视频来源类型
+    var videoFromScene: videoFromScene = .UploadNormal
+
+    override init(jsonDict: [String: Any]) {
+        super.init(jsonDict: jsonDict)
+
+        if jsonDict.keys.contains("videoPath") {
+            videoPath = "\(jsonDict["videoPath"] ?? "")"
+            videoURL = videoPath
+            if videoPath?.contains(".m3u8") ?? false {
+                videoURL = videoPath?.components(separatedBy: "?").first
+            }
+        }
+        if jsonDict.keys.contains("videoCoverSnapshotPath") {
+            videoCoverSnapshotPath = "\(jsonDict["videoCoverSnapshotPath"] ?? "")"
+            if (videoCoverSnapshotPath?.count ?? 0) > 0, !(videoCoverSnapshotPath?.contains("http") ?? false) {
+                videoCoverSnapshotPath = "https://rescdn.yishihui.com/" + videoCoverSnapshotPath!
+            }
+        }
+        if jsonDict.keys.contains("thumbnailImagePath") {
+            thumbnailImagePath = "\(jsonDict["thumbnailImagePath"] ?? "")"
+        }
+        if jsonDict.keys.contains("rotate") {
+            rotate = Int("\(jsonDict["rotate"] ?? "0")") ?? 0
+        }
+        if jsonDict.keys.contains("descr") {
+            descr = "\(jsonDict["descr"] ?? "")"
+            if descr != nil, (descr?.count ?? 0) > 2, descr?.hasSuffix("\n") ?? false {
+                descr = String(descr?.prefix((descr?.count ?? 2) - 2) ?? "")
+            }
+        }
+        if jsonDict.keys.contains("favorited") {
+            favorited = jsonDict["favorited"] as! Bool
+        }
+        if jsonDict.keys.contains("firstPicture") {
+            firstPicture = jsonDict["firstPicture"] as! Bool
+        }
+        if jsonDict.keys.contains("shareImgPath") {
+            shareImgPath = "\(jsonDict["shareImgPath"] ?? "")"
+        }
+        if jsonDict.keys.contains("coverImg") {
+            coverImg = jsonDict["coverImg"] as? [String: Any]
+        }
+        if jsonDict.keys.contains("user") {
+            user = jsonDict["user"] as? [String: Any]
+            userInfo = PQUserInfoModel(jsonDict: user!)
+        }
+        if jsonDict.keys.contains("shareCountFriend") {
+            shareCountFriend = Int("\(jsonDict["shareCountFriend"] ?? "0")") ?? 0
+        }
+        if jsonDict.keys.contains("shareId") {
+            shareId = "\(jsonDict["shareId"] ?? "")"
+        }
+        if jsonDict.keys.contains("shareCount") {
+            shareCount = Int("\(jsonDict["shareCount"] ?? "0")") ?? 0
+        }
+        if jsonDict.keys.contains("favoriteds") {
+            favoriteds = Int("\(jsonDict["favoriteds"] ?? "0")") ?? 0
+        }
+        if jsonDict.keys.contains("uid") {
+            uid = Int("\(jsonDict["uid"] ?? "0")") ?? 0
+        }
+        if jsonDict.keys.contains("lastTimestamp") {
+            lastTimestamp = Int("\(jsonDict["lastTimestamp"] ?? "0")") ?? 0
+        }
+        if jsonDict.keys.contains("auditStatus") {
+            auditStatus = Int("\(jsonDict["auditStatus"] ?? "0")") ?? 0
+        }
+        if jsonDict.keys.contains("status") {
+            status = Int("\(jsonDict["status"] ?? "0")") ?? 0
+        }
+        if jsonDict.keys.contains("auditReason") {
+            auditReason = "\(jsonDict["auditReason"] ?? "")"
+        }
+        if jsonDict.keys.contains("transcodeStatus") {
+            transcodeStatus = Int("\(jsonDict["transcodeStatus"] ?? "0")") ?? 0
+        }
+        if jsonDict.keys.contains("playCountTotal") {
+            playCountTotal = Int("\(jsonDict["playCountTotal"] ?? "0")") ?? 0
+        }
+        if jsonDict.keys.contains("measure") {
+            measure = Int("\(jsonDict["measure"] ?? "0")") ?? 0
+        }
+        if jsonDict.keys.contains("measureId") {
+            measureId = Int("\(jsonDict["measureId"] ?? "0")") ?? 0
+        }
+        if jsonDict.keys.contains("measureType") {
+            measureType = Int("\(jsonDict["measureType"] ?? "0")") ?? 0
+        }
+        if jsonDict.keys.contains("playCountFormatStr") {
+            playCountFormatStr = "\(jsonDict["playCountFormatStr"] ?? "")"
+        }
+        if jsonDict.keys.contains("recommendId") {
+            recommendId = "\(jsonDict["recommendId"] ?? "")"
+        }
+        if jsonDict.keys.contains("abInfoData") {
+            abInfoData = "\(jsonDict["abInfoData"] ?? "")"
+        }
+        if jsonDict.keys.contains("totalTime") {
+            totalTime = Int("\(jsonDict["totalTime"] ?? "0")") ?? 0
+            totalTimeParas = Float64(totalTime).formatDurationToHMS()
+        }
+        if jsonDict.keys.contains("width") {
+            width = CGFloat(Int("\(jsonDict["width"] ?? 0)") ?? 0)
+            if rotate > 0 {
+                originImageH = CGFloat(Int("\(jsonDict["width"] ?? 0)") ?? 0)
+            } else {
+                originImageW = CGFloat(Int("\(jsonDict["width"] ?? 0)") ?? 0)
+            }
+        }
+        if jsonDict.keys.contains("height") {
+            height = CGFloat(Int("\(jsonDict["height"] ?? 0)") ?? 0)
+            if rotate > 0 {
+                originImageW = CGFloat(Int("\(jsonDict["height"] ?? 0)") ?? 0)
+            } else {
+                originImageH = CGFloat(Int("\(jsonDict["height"] ?? 0)") ?? 0)
+            }
+        }
+        if jsonDict.keys.contains("produceProjectDataV2"), ((jsonDict["produceProjectDataV2"] as? [String: Any])?.keys.count ?? 0) > 0 {
+            reCreateVideoData = PQReCreateModel(jsonDict: jsonDict["produceProjectDataV2"] as! [String: Any])
+            if reCreateVideoData?.canReproduce == 1 {
+                funcH = funcH + cDefaultMargin * 7
+            }
+            reCreateVideoData?.videoId = uniqueId
+        }
+        if coverImg != nil, coverImg?.count ?? 0 > 0 {
+            imageW = cScreenWidth
+            imageH = imageW * originImageH / originImageW
+            isVerticality = originImageH > originImageW
+            if imageH > cScreenHeigth {
+                imageH = cScreenHeigth
+                imageW = imageH * originImageW / originImageH
+            }
+        }
+        // 计算个人中心高度
+        var tempTitleH: CGFloat = -cDefaultMargin
+        if title != nil, (title?.count ?? 0) > 0 {
+            tempTitleH = sizeWithText(text: title ?? "", font: UIFont.systemFont(ofSize: 16), size: CGSize(width: (cScreenWidth - cDefaultMargin * 3) / 2, height: cDefaultMargin * 4)).height
+        }
+        itemHeight = (cScreenWidth - cDefaultMargin * 3) / 2 * originImageH / originImageW + tempTitleH + cDefaultMargin * 4.5
+
+        if title != nil, (title?.count ?? 0) > 0 {
+            titleH = sizeWithText(text: title ?? "", font: UIFont.systemFont(ofSize: 26, weight: .medium), size: CGSize(width: cScreenWidth - cDefaultMargin * 2, height: CGFloat.greatestFiniteMagnitude)).height + cDefaultMargin
+        }
+        if titleH > 70 {
+            titleH = 70
+            titleFontSize = cScreenWidth <= 320 ? 19 : 21
+        } else {
+            titleFontSize = 25
+        }
+
+        PQLog(message: "title \(String(describing: title))   titleH :\(titleH)  titleFontSize :\(titleFontSize)")
+
+        var isFollowed: Bool = false
+
+        if tab_pageType == .TAB_PAGETYPE_RECOMM {
+            isFollowed = userInfo?.followed != 1
+        } else {
+            isFollowed = true
+        }
+
+        let attM: CGFloat = cDefaultMargin * 4.5
+        usnameW = sizeWithText(text: userInfo?.nickName ?? "", font: UIFont.systemFont(ofSize: 16, weight: .medium), size: CGSize(width: !isFollowed ? (cScreenWidth - cDefaultMargin * 8) : (cScreenWidth - cDefaultMargin * 9 - attM), height: cDefaultMargin * 2)).width + cDefaultMargin * 2
+
+        PQLog(message: "nickname is \(userInfo?.nickName ?? "") '    'usnameW is \(usnameW) isFollowed is \(isFollowed)")
+
+        updateReommendAgent()
+
+        watchInfoY = -cDefaultMargin
+
+        PQLog(message: "watchInfoY11111 is \(watchInfoY)")
+    }
+
+    func updateReommendAgent() {
+        // 计算相关推荐高度
+        var itemH: CGFloat = 0
+        var verticality: Bool = false
+        var haveTitle: Bool = false
+        if (relationData?.count ?? 0) > 0 {
+            for item in (relationData)! {
+                if item.imageH > item.imageW {
+                    verticality = true
+                    break
+                }
+                PQLog(message: "item.title  \(String(describing: item.title))")
+                if item.title != nil, (item.title?.count ?? 0) > 0 {
+                    haveTitle = true
+                    break
+                }
+            }
+        }
+        itemH = verticality ? 130 : (haveTitle ? 90 : 55)
+        itemH = (relationData?.count ?? 0) > 0 ? itemH : 0
+
+        PQLog(message: "itemH is: \(itemH) sss \(String(describing: relationData?.count))")
+
+        rotationH = itemH
+        // 描述部分
+        if (relationData?.count ?? 0) > 0, tab_pageType != .TAB_PAGETYPE_ATTEN {
+            watchInfoY = (isVerticality ? -cDefaultMargin : -(cDefaultMargin * 2 + rotationH))
+        } else {
+            watchInfoY = -cDefaultMargin
+        }
+
+        PQLog(message: "watchInfoY2222 is \(watchInfoY)")
+
+        let nomalH: CGFloat = cDefaultMargin * 1.5
+//        let likeH: CGFloat = favoriteds <= 0 ? 0 : nomalH
+        let likeH: CGFloat = 0
+        watchInfoH = cDefaultMargin * 2 + nomalH + likeH + cDefaultMargin
+        if reCreateVideoData != nil, reCreateVideoData?.canReproduce == 1 {
+            watchInfoH = watchInfoH + ((reCreateVideoData?.rhythmMusicFlag != 1 && (reCreateVideoData?.parentVideoId != nil)) ? cDefaultMargin * 8 : 43)
+        }
+        if descr != nil, !(descr?.isEmpty ?? true) {
+            if Thread.isMainThread {
+                descH = sizeTextFits(attributedText: NSMutableAttributedString(string: descr ?? ""), text: nil, numberOfLines: 5, font: UIFont.systemFont(ofSize: 14), maxSize: CGSize(width: cScreenWidth - funcW - cDefaultMargin * 3, height: CGFloat.greatestFiniteMagnitude)).height
+                watchInfoH = watchInfoH + descH
+            } else {
+                DispatchQueue.main.async { [weak self] in
+                    self?.descH = sizeTextFits(attributedText: NSMutableAttributedString(string: (self?.descr)!), text: nil, numberOfLines: 5, font: UIFont.systemFont(ofSize: 14), maxSize: CGSize(width: cScreenWidth - (self?.funcW ?? 0) - cDefaultMargin * 3, height: CGFloat.greatestFiniteMagnitude)).height
+                    self?.watchInfoH = (self?.watchInfoH ?? 0) + (self?.descH ?? 0)
+                }
+            }
+        }
+    }
+}

+ 77 - 0
BFFramework/Classes/PModels/editDarftModels/PQEditAudioTrackMaterialModel.swift

@@ -0,0 +1,77 @@
+//
+//  PQAudioTrackMaterialModel.swift
+//  PQSpeed
+//
+//  Created by ak on 2020/12/5.
+//  Copyright © 2020 BytesFlow. All rights reserved.
+//
+
+import Foundation
+import ObjectMapper
+import RealmSwift
+
+class PQEditAudioTrackMaterialModel: PQEditBaseModel {
+    /**
+     @objc dynamic var id: Int64 = 0
+     @objc dynamic var materialUrl: String = ""
+     @objc dynamic var type: String = ""
+     @objc dynamic var duration: Float64 = 0
+     @objc dynamic var timelineIn: Float64 = 0
+     @objc dynamic var timelineOut: Float64 = 0
+     @objc dynamic var model_in: Float64 = 0
+     @objc dynamic var out: Float64 = 0
+     @objc dynamic var volumeGain: Float64 = 0
+     @objc dynamic var materialDurationFit: PQEditmaterialDurationFitModel?
+     @objc dynamic var locationPath: String = "" {
+         didSet {
+             PQLog(message: "音频的:locationPath\(locationPath)")
+         }
+     }
+     **/
+    @objc dynamic var id: Int64 = 0
+    @objc dynamic var type: String = ""
+    // 音频类型(bgm:背景音乐,produce:合成语音,speech:录音,local : 本地文件)
+    @objc dynamic var voiceType: String = ""
+    @objc dynamic var duration: Float64 = 0
+    @objc dynamic var timelineIn: Float64 = 0
+    @objc dynamic var timelineOut: Float64 = 0
+    @objc dynamic var model_in: Float64 = 0
+    @objc dynamic var out: Float64 = 0
+    // 音量增益,保留一位小数,取值范围 0.0 - 10.0,大于1,表示音量增强。小于1,表示音量减小。0.0 表示静音
+    @objc dynamic var volumeGain: Float64 = 0
+    @objc dynamic var materialDurationFit: PQEditmaterialDurationFitModel?
+    @objc dynamic var produceVoiceConfig: PQEditProduceVoiceConfigModel?
+    @objc dynamic var bgmInfo: PQEditBgmInfoModel?
+    @objc dynamic var materialUrl: String = ""
+    var status: inputStatus = .normal
+    // add by ak 业务逻辑添加的属性 声音文件的本地址
+    // 音频文件本地地址 URI
+    @objc dynamic var locationPath: String = "" {
+        didSet {
+            PQLog(message: "音频的:locationPath\(locationPath)")
+        }
+    }
+
+    required convenience init?(map _: Map) {
+        self.init()
+    }
+
+    override func mapping(map: Map) {
+        id <- map["id"]
+        type <- map["type"]
+        voiceType <- map["voiceType"]
+        duration <- (map["duration"], timeTransform)
+        timelineIn <- (map["timelineIn"], timeTransform)
+        timelineOut <- (map["timelineOut"], timeTransform)
+        model_in <- (map["in"], timeTransform)
+        out <- (map["out"], timeTransform)
+
+        volumeGain <- (map["volumeGain"], volumeGainTransform)
+        materialDurationFit <- map["materialDurationFit"]
+        produceVoiceConfig <- map["produceVoiceConfig"]
+        bgmInfo <- map["bgmInfo"]
+
+        locationPath <- map["locationPath"]
+        materialUrl <- map["materialUrl"]
+    }
+}

+ 63 - 0
BFFramework/Classes/PModels/editDarftModels/PQEditAudioTrackModel.swift

@@ -0,0 +1,63 @@
+/*
+ Copyright (c) 2020 Swift Models Generated from JSON powered by http://www.json4swift.com
+
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
+
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+ For support, please feel free to contact me at https://www.linkedin.com/in/syedabsar
+
+ */
+
+import Foundation
+
+import ObjectMapper
+import RealmSwift
+class PQEditAudioTrackModel: PQEditBaseModel {
+    var audioTrackMaterials: List<PQEditAudioTrackMaterialModel> = List<PQEditAudioTrackMaterialModel>.init()
+    @objc dynamic var count: Int = 0
+    @objc dynamic var duration: Float64 = 0
+
+    required convenience init?(map _: Map) {
+        self.init()
+    }
+
+    required init() {
+        super.init()
+    }
+
+    override func mapping(map: Map) {
+        audioTrackMaterials <- (map["audioTrackMaterials"], PQListTransform<PQEditAudioTrackMaterialModel>())
+        count <- map["count"]
+        duration <- (map["duration"], timeTransform)
+    }
+
+    /// 根据 VoiceType 取音轨信息
+    /// - Parameter voiceType: voiceType  音频类型(bgm:背景音乐,produce:合成语音,speech:录音,local : 本地文件)
+    /// - Returns: PQEditAudioTrackMaterialModel
+    func getAudioTrackModel(voiceType: String) -> PQEditAudioTrackMaterialModel? {
+        PQLog(message: "查询 voiceType is \(voiceType)")
+        var audioTrackMaterialModel: PQEditAudioTrackMaterialModel?
+        for materialModel in audioTrackMaterials {
+            if materialModel.voiceType == voiceType {
+                audioTrackMaterialModel = materialModel
+            }
+        }
+        return audioTrackMaterialModel
+    }
+
+    // 添加录音文件 如果原来有先删除旧数据
+    func addSpeechAudioTrackMaterialModel(_ audioTrackMaterialModel: PQEditAudioTrackMaterialModel) {
+        // 1 找出声音转文字的对应段落
+        let sesscionIdex = audioTrackMaterials.firstIndex(where: { (audioTrack) -> Bool in
+            audioTrack.voiceType == VOICETYPT.SPEECH.rawValue
+        })
+        if audioTrackMaterials.count > sesscionIdex ?? 0 {
+            PQLog(message: "原来有录音 \(String(describing: sesscionIdex))")
+            audioTrackMaterials.remove(at: sesscionIdex ?? 0)
+        }
+        audioTrackMaterials.append(audioTrackMaterialModel)
+    }
+}

+ 55 - 0
BFFramework/Classes/PModels/editDarftModels/PQEditBaseModel.swift

@@ -0,0 +1,55 @@
+//
+//  PQEditBaseModel.swift
+//  PQSpeed
+//
+//  Created by ak on 2020/12/19.
+//  Copyright © 2020 BytesFlow. All rights reserved.
+//
+
+import ObjectMapper
+import RealmSwift
+
+// 自定义时长转换协议,服务单位为微秒,我们使用 S
+let timeTransform = TransformOf<Float64, Float64>(fromJSON: { (value: Float64?) -> Float64? in
+    // transform value from String? to Int?
+    (value ?? 0) / 1_000_000
+}, toJSON: { (value: Float64?) -> Float64? in
+    // transform value from Int? to String?
+    if let value = value {
+        return value * 1_000_000
+    }
+    return nil
+})
+
+// 自定义音量转换协议,服务器单位0.0-1.0,我们使用 0-100
+let volumeGainTransform = TransformOf<Float64, Float64>(fromJSON: { (value: Float64?) -> Float64? in
+    PQLog(message: "value is \(value)")
+    return (value ?? 0.0) * 100.0
+}, toJSON: { (value: Float64?) -> Float64? in
+    if let value = value {
+        return value / 100.0
+    }
+    return nil
+})
+// 自定义 sectionIndex 转换协议服务器是从 1 开始,我们是从0开始
+let sectionIndexTransform = TransformOf<Int, Int>(fromJSON: { (value: Int?) -> Int? in
+    (value ?? 0) - 1
+}, toJSON: { (value: Int?) -> Int? in
+    if let value = value {
+        return value + 1
+    }
+    return nil
+})
+
+class PQEditBaseModel: Object, Mappable {
+    @objc dynamic var uniqueId: String = getUniqueId(desc: "uniqueId")
+    func mapping(map _: Map) {}
+
+    required convenience init?(map _: Map) {
+        self.init()
+    }
+
+    override required init() {
+        super.init()
+    }
+}

+ 29 - 0
BFFramework/Classes/PModels/editDarftModels/PQEditBgmInfoModel.swift

@@ -0,0 +1,29 @@
+//
+//  PQBgmInfoModel.swift
+//  PQSpeed
+//
+//  Created by ak on 2020/12/5.
+//  Copyright © 2020 BytesFlow. All rights reserved.
+//
+
+import ObjectMapper
+import RealmSwift
+class PQEditBgmInfoModel: PQEditBaseModel {
+    @objc dynamic var musicId: String = ""
+    @objc dynamic var originType: Int = 0
+    // 选择的声音类型,1:原声 ,2:背景声
+    @objc dynamic var selectVoiceType: Int = 1
+    // 卡点音乐节奏速度(1:快节奏,2:适中,3:慢节奏)
+    @objc dynamic var rhythmMusicSpeed: Int = 1
+
+    required convenience init?(map _: Map) {
+        self.init()
+    }
+
+    override func mapping(map: Map) {
+        musicId <- map["musicId"]
+        originType <- map["originType"]
+        selectVoiceType <- map["selectVoiceType"]
+        rhythmMusicSpeed <- map["rhythmMusicSpeed"]
+    }
+}

+ 33 - 0
BFFramework/Classes/PModels/editDarftModels/PQEditFileMergeTable.swift

@@ -0,0 +1,33 @@
+//
+//  PQEditFileMergeTable.swift
+//  PQSpeed
+//
+//  Created by ak on 2020/12/12.
+//  Copyright © 2020 BytesFlow. All rights reserved.
+// 草稿箱业务 本地文件和网络文件的对应关系表
+
+import RealmSwift
+import UIKit
+class PQEditFileMergeTable: Object {
+    // 草稿箱ID 服务端生成
+    @objc dynamic var draftboxId: String = ""
+    // 上传成功后,服务器返回的素材 ID
+    @objc dynamic var id: Int64 = 0
+
+    // 唯一值
+    @objc dynamic var unid: String = ""
+
+    // 素材的本地路径 为 URI
+    @objc dynamic var fileLocalPath: String = ""
+
+    // 素材的外网 URL 我方服务器的外网地址 就是上传后的 URL
+    @objc dynamic var materialUrl: String = ""
+    override required init() {
+        super.init()
+        unid = getUniqueId(desc: "PQEditFileMergeTable")
+    }
+
+    override static func primaryKey() -> String? {
+        return "unid"
+    }
+}

+ 31 - 0
BFFramework/Classes/PModels/editDarftModels/PQEditMaterialDurationFitModel.swift

@@ -0,0 +1,31 @@
+//
+//  PQEditmaterialDurationFit?Model.swift
+//  PQSpeed
+//
+//  Created by ak on 2020/12/5.
+//  Copyright © 2020 BytesFlow. All rights reserved.
+//
+
+import Foundation
+import ObjectMapper
+import RealmSwift
+
+class PQEditmaterialDurationFitModel: PQEditBaseModel {
+    @objc dynamic var fitType: String = ""
+    @objc dynamic var multipleValue: Float = 0.0
+    @objc dynamic var loopValue: Int = 0
+
+    required convenience init?(map _: Map) {
+        self.init()
+    }
+
+    required init() {
+        super.init()
+    }
+
+    override func mapping(map: Map) {
+        fitType <- map["fitType"]
+        multipleValue <- map["multipleValue"]
+        loopValue <- map["loopValue"]
+    }
+}

+ 29 - 0
BFFramework/Classes/PModels/editDarftModels/PQEditMaterialEffectModel.swift

@@ -0,0 +1,29 @@
+/*
+ Copyright (c) 2020 Swift Models Generated from JSON powered by http://www.json4swift.com
+
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
+
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+ For support, please feel free to contact me at https://www.linkedin.com/in/syedabsar
+
+ */
+
+import Foundation
+import ObjectMapper
+import RealmSwift
+class PQEditMaterialEffectModel: PQEditBaseModel {
+    @objc dynamic var type: String = ""
+    @objc dynamic var params: String = ""
+
+    required convenience init?(map _: Map) {
+        self.init()
+    }
+
+    override func mapping(map: Map) {
+        type <- map["type"]
+        params <- map["params"]
+    }
+}

+ 27 - 0
BFFramework/Classes/PModels/editDarftModels/PQEditMaterialLayerModel.swift

@@ -0,0 +1,27 @@
+/*
+ Copyright (c) 2020 Swift Models Generated from JSON powered by http://www.json4swift.com
+
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
+
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+ For support, please feel free to contact me at https://www.linkedin.com/in/syedabsar
+
+ */
+
+import Foundation
+import ObjectMapper
+import RealmSwift
+
+class PQEditMaterialLayerModel: PQEditBaseModel {
+    @objc dynamic var layer: Int = 0
+    required convenience init?(map _: Map) {
+        self.init()
+    }
+
+    override func mapping(map: Map) {
+        layer <- map["layer"]
+    }
+}

+ 29 - 0
BFFramework/Classes/PModels/editDarftModels/PQEditMaterialPositionModel.swift

@@ -0,0 +1,29 @@
+//
+//  PQEditMaterialPositionModel.swift
+//  PQSpeed
+//
+//  Created by ak on 2020/12/5.
+//  Copyright © 2020 BytesFlow. All rights reserved.
+//
+
+import ObjectMapper
+import RealmSwift
+import UIKit
+
+class PQEditMaterialPositionModel: PQEditBaseModel {
+    @objc dynamic var x: Int = 0
+    @objc dynamic var y: Int = 0
+    @objc dynamic var width: Int = 0
+    @objc dynamic var height: Int = 0
+
+    required convenience init?(map _: Map) {
+        self.init()
+    }
+
+    override func mapping(map: Map) {
+        x <- map["x"]
+        y <- map["y"]
+        width <- map["width"]
+        height <- map["height"]
+    }
+}

+ 32 - 0
BFFramework/Classes/PModels/editDarftModels/PQEditMaterialSizeClipModel.swift

@@ -0,0 +1,32 @@
+//
+//  PQEditMaterialSizeClipModel.swift
+//  PQSpeed
+//
+//  Created by ak on 2020/12/5.
+//  Copyright © 2020 BytesFlow. All rights reserved.
+//
+
+import ObjectMapper
+import RealmSwift
+
+class PQEditMaterialSizeClipModel: PQEditBaseModel {
+    @objc dynamic var x: Int = 0
+    @objc dynamic var y: Int = 0
+    @objc dynamic var width: Int = 0
+    @objc dynamic var height: Int = 0
+    @objc dynamic var scale: Int = 0
+    @objc dynamic var rotate: Int = 0
+
+    required convenience init?(map _: Map) {
+        self.init()
+    }
+
+    override func mapping(map: Map) {
+        x <- map["x"]
+        y <- map["y"]
+        width <- map["width"]
+        height <- map["height"]
+        scale <- map["scale"]
+        rotate <- map["rotate"]
+    }
+}

+ 63 - 0
BFFramework/Classes/PModels/editDarftModels/PQEditProduceVoiceConfigModel.swift

@@ -0,0 +1,63 @@
+//
+//  PQEditProduceVoiceConfigModel.swift
+//  PQSpeed
+//
+//  Created by ak on 2020/12/5.
+//  Copyright © 2020 BytesFlow. All rights reserved.
+//
+
+import Foundation
+import ObjectMapper
+import RealmSwift
+import UIKit
+
+class PQEditProduceVoiceConfigModel: PQEditBaseModel {
+    //阿里云参数
+    @objc dynamic var channel: String = ""
+    @objc dynamic var voice: String = ""
+    @objc dynamic var volume: Int = 100
+    @objc dynamic var speechRate: Int = 0
+    @objc dynamic var pitchRate: Int = 0
+    
+    //深音科技
+    @objc dynamic var deepsoundVolume: String = "" // deepsound音量(取值范围0.0-1.0,默认1.0表示最大音量)
+    @objc dynamic var deepsoundSpeechRate: String = ""
+    @objc dynamic var deepsoundPitchRate: String = ""
+    
+    //微软语音参数
+    // 音量,以从 0.0 到 150.0(从最安静到最大声), 默认值为 100.0。
+    @objc dynamic var azureVolume:String = "100"
+    // 语速,取值为0.00-3.00,默认为1.00,如果值为 1,则速率不会变化。 如果值为 0.5,则速率会减慢一半。 如果值为 3,则速率为三倍。
+    @objc dynamic var azureSpeechRate:String = "1.00"
+    // 语调,取值为-50%-50%,默认为0%
+    @objc dynamic var azurePitchRate:String = "0%"
+    // 语音风格,默认为 general
+    @objc dynamic var azureStyle:String = "general"
+    // 句末停顿时间,默认500ms
+    @objc dynamic var azureEndBreakTime:String = "0ms"
+    
+
+    required convenience init?(map _: Map) {
+        self.init()
+    }
+
+    override func mapping(map: Map) {
+        channel <- map["channel"]
+        voice <- map["voice"]
+        volume <- map["volume"]
+        speechRate <- map["speechRate"]
+        pitchRate <- map["pitchRate"]
+        
+        deepsoundVolume <- map["deepsoundVolume"]
+        deepsoundSpeechRate <- map["deepsoundSpeechRate"]
+        deepsoundPitchRate <- map["deepsoundPitchRate"]
+        
+        azureVolume <- map["azureVolume"]
+        azureSpeechRate <- map["azureSpeechRate"]
+        azurePitchRate <- map["azurePitchRate"]
+        azureStyle <- map["azureStyle"]
+        azureEndBreakTime <- map["azureEndBreakTime"]
+        
+        
+    }
+}

+ 78 - 0
BFFramework/Classes/PModels/editDarftModels/PQEditProjectModel.swift

@@ -0,0 +1,78 @@
+/*
+ Copyright (c) 2020 Swift Models Generated from JSON powered by http://www.json4swift.com
+
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
+
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+ For support, please feel free to contact me at https://www.linkedin.com/in/syedabsar
+
+ */
+
+// 这个是一个项目所有段的数据,进入到编辑界面的时候传的这个就是这个 MODLE , 合成和预览都是
+/* XXXXX
+
+ 注意:使用 ObjectMappers 的 toJSON 函数来生成 JSON 字符串只在 Realm 的写事务中有效(write transaction)。这是因为 ObjectMapper 在解析和生成时在映射函数( <-< code=""> )中使用  inout 作为标记( flag )。Realm 会检测到标记并且强制要求 toJSON 函数只能在一个写的事务中调用,即使这个对象并没有被修改。
+ 参考:
+ https://github.com/jakenberg/ObjectMapper-Realm
+ https://github.com/alibaba/handyjson
+ https://juejin.cn/post/6844903450308771847
+ https://juejin.cn/post/6844904117442215944
+
+ */
+
+import Foundation
+import ObjectMapper
+import RealmSwift
+class PQEditProjectModel: PQEditBaseModel {
+    // 大 JSON 结构化数据
+    @objc dynamic var sData: PQEditSdataModel?
+    @objc dynamic var isSelected: Bool = false // 是否选中
+    @objc var coverUrl: String? // 图片
+    @objc dynamic var dataVersionCode: Int = 0 // 数据版本号
+    var cacheDataVersionCode: Int = -1 // 缓存版本号
+    @objc var draftboxId: String = "" // 草稿id
+    @objc dynamic var projectId: String = "" // 项目id
+    @objc dynamic var title: String = "" // 标题
+    @objc dynamic var updateTimestamp: Int = 0 { // 更新时间
+        didSet {
+            updateTimestampDes = timeIntervalToDateString(timeInterval: TimeInterval(updateTimestamp / 1000))
+        }
+    }
+
+    var updateTimestampDes: String? // 更新时间描述
+    @objc var duration: Float64 = 0 { // 时长
+        didSet {
+            durationDes = duration.formatDurationToHMS()
+        }
+    }
+
+    var durationDes: String = "00:00" // 时长描述
+    required convenience init?(map _: Map) {
+        self.init()
+    }
+
+    required init() {
+        super.init()
+        sData = PQEditSdataModel()
+    }
+
+    override static func primaryKey() -> String? {
+        return "draftboxId"
+    }
+
+    override func mapping(map: Map) {
+        sData <- map["sData"]
+        coverUrl <- map["coverUrl"]
+        dataVersionCode <- map["dataVersionCode"]
+        draftboxId <- map["draftboxId"]
+        projectId <- map["projectId"]
+        title <- map["title"]
+        updateTimestamp <- map["updateTimestamp"]
+        duration <- (map["duration"], timeTransform)
+        durationDes <- map["durationDes"]
+        updateTimestampDes <- map["updateTimestampDes"]
+    }
+}

+ 158 - 0
BFFramework/Classes/PModels/editDarftModels/PQEditSdataModel.swift

@@ -0,0 +1,158 @@
+//
+//  PQEditSdataModel.swift
+//  PQSpeed
+//
+//  Created by ak on 2020/12/16.
+//  Copyright © 2020 BytesFlow. All rights reserved.
+//
+
+import ObjectMapper
+import RealmSwift
+import UIKit
+class PQEditSdataModel: PQEditBaseModel {
+    @objc dynamic var systemParam: PQEditSystemParamModel?
+    @objc dynamic var videoMetaData: PQEditVideoMetaDataModel?
+    var sections: List<PQEditSectionModel> = List()
+
+    required convenience init?(map _: Map) {
+        self.init()
+    }
+
+    required init() {
+        super.init()
+        systemParam = PQEditSystemParamModel()
+        videoMetaData = PQEditVideoMetaDataModel()
+    }
+
+    override func mapping(map: Map) {
+        systemParam <- map["systemParam"]
+        videoMetaData <- map["videoMetaData"]
+        sections <- (map["sections"], PQListTransform<PQEditSectionModel>())
+    }
+
+    func deleteBGM() {
+        var selectBGMIndex: Int = -1
+        for (i, section) in sections.enumerated() {
+            if section.sectionType == "global" {
+                selectBGMIndex = i
+            }
+        }
+        if selectBGMIndex != -1 {
+            PQLog(message: "DELETE BGM INDEX\(selectBGMIndex)")
+            sections.remove(at: selectBGMIndex)
+        }
+    }
+
+    // 添加背景音乐
+    func addBGM(audioMix: PQVoiceModel) {
+        // 1,如果已经选择过就先删除一次
+        deleteBGM()
+
+        // 所有段落的时长
+        var sectionDuration: Float64 = 0
+        for section in sections {
+            if section.sectionType == "normal" {
+                sectionDuration = sectionDuration + section.sectionDuration
+            }
+        }
+
+        // 拼接数据
+        let bgmSection = PQEditSectionModel()
+        bgmSection.sectionType = "global"
+        bgmSection.sectionIndex = 0
+        bgmSection.projectTimelineIn = 0
+        bgmSection.projectTimelineOut = sectionDuration
+
+        bgmSection.sectionDuration = Float64(audioMix.duration ?? "0")!
+        let audioTrackModel = PQEditAudioTrackModel()
+        let audioTrackMaterial = PQEditAudioTrackMaterialModel()
+        audioTrackMaterial.id = 0
+        audioTrackMaterial.type = "voice"
+        audioTrackMaterial.voiceType = VOICETYPT.BGM.rawValue
+        // 背景音乐的时长
+        audioTrackMaterial.duration = Float64(audioMix.duration ?? "0")!
+        audioTrackMaterial.timelineIn = 0
+        // 所有段落的总时长
+        audioTrackMaterial.timelineOut = sectionDuration
+        // 裁剪位置
+        audioTrackMaterial.model_in = audioMix.startTime
+        audioTrackMaterial.out = (audioMix.endTime == 0 ? Float64(audioMix.duration ?? "0") : audioMix.endTime)!
+
+        audioTrackMaterial.volumeGain = Float64(audioMix.volume)
+
+        let materialDurationFit = PQEditmaterialDurationFitModel()
+        materialDurationFit.fitType = "loop"
+        materialDurationFit.loopValue = 1
+
+        audioTrackMaterial.materialDurationFit = materialDurationFit
+
+        let bgmInfo = PQEditBgmInfoModel()
+        bgmInfo.musicId = audioMix.musicId ?? ""
+        bgmInfo.originType = audioMix.originType
+        bgmInfo.selectVoiceType = audioMix.selectVoiceType
+        bgmInfo.rhythmMusicSpeed = audioMix.speed
+        audioTrackMaterial.bgmInfo = bgmInfo
+        audioTrackModel.audioTrackMaterials.append(audioTrackMaterial)
+        bgmSection.sectionTimeline?.audioTrack = audioTrackModel
+
+        sections.append(bgmSection)
+    }
+
+    // 取背景音乐的 session
+    func getBGMSession() -> PQEditSectionModel? {
+        var BGMSession: PQEditSectionModel?
+        if sections.count > 0 {
+            for section in sections {
+                if section.sectionType == "global" {
+                    BGMSession = section
+                }
+            }
+        }
+
+        return BGMSession
+    }
+
+    // 取有效素材的段落剔除 global BGM 的 session  , type is normal
+    func getVoisonSessions() -> List<PQEditSectionModel> {
+        let voisonArray = List<PQEditSectionModel>.init()
+        for section in sections {
+            if section.sectionType == "normal" {
+                voisonArray.append(section)
+            } else { PQLog(message: "有音频bgm session") }
+        }
+        PQLog(message: "有效视觉段落数 \(voisonArray.count)")
+        return voisonArray
+    }
+
+    ///
+    func deleteDisableSaveMaterials() {}
+
+    // 删除无效的素材
+    /// - Parameter isSaveDraft: 只能在保存项目API 调用前使用COPY 的 sdata 设TRUE ,恢复项目时设置 false
+    func deleteDisableMaterials(isSaveDraft: Bool = false) {
+        for section in sections {
+            // 视觉素材sr
+            let visionTrackMaterialsTemp = List<PQEditVisionTrackMaterialsModel>.init()
+            for visionTrackMaterialsModel in section.sectionTimeline?.visionTrack?.visionTrackMaterials ?? List<PQEditVisionTrackMaterialsModel>.init() {
+                if visionTrackMaterialsModel.type == "subtitle" {
+                    visionTrackMaterialsTemp.append(visionTrackMaterialsModel)
+
+                } else if (isSaveDraft && visionTrackMaterialsModel.materialUrl.count > 0) || (!isSaveDraft && (visionTrackMaterialsModel.locationPath.count > 0 || visionTrackMaterialsModel.materialUrl.count > 0)) {
+                    visionTrackMaterialsTemp.append(visionTrackMaterialsModel)
+
+                } else { PQLog(message: "visionTrackMaterialsModel 这个无效") }
+            }
+            section.sectionTimeline?.visionTrack?.visionTrackMaterials = visionTrackMaterialsTemp
+
+            // 音频素材
+            let audioTrackMaterialModelTemp = List<PQEditAudioTrackMaterialModel>.init()
+            for audioTrackMaterialModel in section.sectionTimeline?.audioTrack?.audioTrackMaterials ?? List<PQEditAudioTrackMaterialModel>.init() {
+                if audioTrackMaterialModel.voiceType == VOICETYPT.PRODUCE.rawValue || audioTrackMaterialModel.voiceType == VOICETYPT.BGM.rawValue || (isSaveDraft && audioTrackMaterialModel.materialUrl.count > 0) || (!isSaveDraft && (audioTrackMaterialModel.locationPath.count > 0 || audioTrackMaterialModel.materialUrl.count > 0)) {
+                    audioTrackMaterialModelTemp.append(audioTrackMaterialModel)
+
+                } else { PQLog(message: "audioTrackMaterialModel 这个无效") }
+            }
+            section.sectionTimeline?.audioTrack?.audioTrackMaterials = audioTrackMaterialModelTemp
+        }
+    }
+}

+ 31 - 0
BFFramework/Classes/PModels/editDarftModels/PQEditSectionExtDataModel.swift

@@ -0,0 +1,31 @@
+/*
+ Copyright (c) 2020 Swift Models Generated from JSON powered by http://www.json4swift.com
+
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
+
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+ For support, please feel free to contact me at https://www.linkedin.com/in/syedabsar
+
+ */
+
+import Foundation
+import ObjectMapper
+import RealmSwift
+class PQEditSectionExtDataModel: PQEditBaseModel {
+    @objc dynamic var audioDuration: Float64 = 0
+    @objc dynamic var coverUrl: String = ""
+    @objc dynamic var isSpeech: Bool = false
+
+    required convenience init?(map _: Map) {
+        self.init()
+    }
+
+    override func mapping(map: Map) {
+        audioDuration <- (map["audioDuration"], timeTransform)
+        coverUrl <- map["coverUrl"]
+        isSpeech <- map["isSpeech"]
+    }
+}

+ 287 - 0
BFFramework/Classes/PModels/editDarftModels/PQEditSectionModel.swift

@@ -0,0 +1,287 @@
+/*
+ Copyright (c) 2020 Swift Models Generated from JSON powered by http://www.json4swift.com
+
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
+
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+ For support, please feel free to contact me at https://www.linkedin.com/in/syedabsar
+
+ */
+
+import Foundation
+import AVFoundation
+import ObjectMapper
+import RealmSwift
+class PQEditSectionModel: PQEditBaseModel {
+    @objc dynamic var addAutoEffect: Int = 0
+    @objc dynamic var sectionDuration: Float64 = 0 {
+        didSet {
+            PQLog(message: "sectionDuration == \(sectionDuration)")
+        }
+    }
+
+    @objc dynamic var projectTimelineIn: Float64 = 0
+    @objc dynamic var projectTimelineOut: Float64 = 0
+    // 段落序号globle 全是0  nomoal 从1开始++ XXXXXX(已经通过TransformOf处理过,代码中使用时都是从0开始就 OK )
+    @objc dynamic var sectionIndex: Int = 0 {
+        didSet {
+            PQLog(message: "sectionIndex is \(sectionIndex)")
+        }
+    }
+
+    @objc dynamic var sectionText: String = "" {
+        didSet {
+          PQLog(message: "文字发生了改变")
+  
+        }
+    }
+
+    @objc dynamic var sectionTimeline: PQEditSectionTimelineModel? {
+        didSet {}
+    }
+
+    @objc dynamic var sectionType: String = "normal"
+
+    // pc
+    @objc dynamic var sectionExtData: PQEditSectionExtDataModel?
+
+    // add by ak 业务逻辑层要使用的属性 -----------
+    var textIsChane: Bool = false
+
+    // 是否为选中状态 选中状态有三种,0未选中状态 1 设置状态 2,播放状态
+    var isSelected: Int = 0
+    // 是否为可设置状态
+    var enabledSetting: Bool = true
+    // 是否可添加段
+    var enableAdd: Bool = true
+    // 当前段选择的发音人数据
+    var selectVoice: PQVoiceModel?
+
+    // 每一段的封面
+    var coverImage: UIImage?
+    // 声音文件地址
+    var audioFilePath: String = "" {
+        didSet {
+            if (sectionTimeline?.visionTrack?.getEnableVisionTrackMaterials().count ?? 0) > 0 {
+                sectionDuration = allStickerAptDuration()
+            } else {
+                if audioFilePath.count > 0 {
+                    let audioAsset = AVURLAsset(url: URL(fileURLWithPath: documensDirectory + audioFilePath), options: avAssertOptions)
+                    sectionDuration = Float64(audioAsset.duration.seconds)
+                }
+            }
+        }
+    }
+
+    // 谁长用谁功能,如果视觉长会拼接空音频文件,这个是拼接后的地址
+    var mixEmptyAuidoFilePath: String = ""
+
+    var subTitles = List<PQEditSubTitleModel>()
+
+    // 背景音乐数据(可能每一段都会有背景音)
+    public var bgmData: PQVoiceModel?
+
+    // 录音人头像,在恢复项目时会有值
+    var audioAvatarUrl: String = ""
+
+    // MARK: - 录音相关属性,不保存sadata 不入库
+
+    // 保存每一个段落所有分段的录音记录
+    var cacheRecorderFiles: [URL] = Array()
+    // 录制的小段数
+    var cacheRecorderCount: Int = 0
+    // 合并后的地址 并已经转成 MP3  mp3 file path
+    var compliteMP3AudioFile: String = ""
+    // 录音声音分贝值
+    var audioPowers: [Int] = Array()
+    
+    //是否正在文字转换中。。
+    var isConverding: Bool = false
+
+    required convenience init?(map _: Map) {
+        self.init()
+    }
+
+    required init() {
+        super.init()
+
+        sectionTimeline = PQEditSectionTimelineModel()
+    }
+
+    override func mapping(map: Map) {
+        addAutoEffect <- map["addAutoEffect"]
+        sectionDuration <- (map["duration"], timeTransform)
+        projectTimelineIn <- (map["projectTimelineIn"], timeTransform)
+        projectTimelineOut <- (map["projectTimelineOut"], timeTransform)
+
+        sectionText <- map["sectionText"]
+        sectionTimeline <- map["sectionTimeline"]
+        sectionType <- map["sectionType"]
+
+        if sectionType != "global" {
+            sectionIndex <- (map["sectionIndex"], sectionIndexTransform)
+        } else {
+            sectionIndex <- map["sectionIndex"]
+        }
+
+        sectionExtData <- map["sectionExtData"]
+
+        // 业务逻辑属性
+        audioFilePath <- map["audioFilePath"]
+        mixEmptyAuidoFilePath <- map["mixEmptyAuidoFilePath"]
+        subTitles <- (map["subTitles"], PQListTransform<PQEditSubTitleModel>())
+        uniqueId <- map["uniqueId"]
+    }
+
+    // 计算所有贴纸的时间 aptDuration 已经是根据有无配音计算好的 可直接使用
+    func allStickerAptDuration() -> Float64 {
+        var stickerAptDuration: Float64 = 0
+        if sectionTimeline?.visionTrack?.getEnableVisionTrackMaterials().count ?? 0 > 0 {
+            for sticker in (sectionTimeline!.visionTrack?.getEnableVisionTrackMaterials())! {
+                stickerAptDuration = stickerAptDuration + Double(lround(sticker.aptDuration))
+            }
+        }
+        return stickerAptDuration
+    }
+
+    // 所有内贴纸的时长未4舍5入的
+    func allStickerAptDurationNoRound() -> Float64 {
+        var stickerAptDuration: Float64 = 0
+        if sectionTimeline?.visionTrack?.getEnableVisionTrackMaterials().count ?? 0 > 0 {
+            for sticker in (sectionTimeline!.visionTrack?.getEnableVisionTrackMaterials())! {
+                stickerAptDuration = stickerAptDuration + Double(sticker.aptDuration)
+            }
+        }
+        return stickerAptDuration
+    }
+
+    /// 是否已经设置过一个发音人,voice 会在选择时给值,取消选择时清空 init
+    func haveSelectVoice() -> Bool {
+        PQLog(message: "voice?.avatarUrl \(String(describing: selectVoice?.avatarUrl))")
+        return selectVoice?.avatarUrl.count ?? 0 > 0
+    }
+
+    /// 判断素材是否下载完成,如果本地址没有,就应该是有问题或没有下载完成
+    /// - Returns: <#description#>
+    func matrialIsDownloaded() -> Bool {
+        var isDownloaded: Bool = true // 素材是否下载完成
+        for sticker in (sectionTimeline!.visionTrack?.getEnableVisionTrackMaterials())! {
+            if sticker.locationPath.count == 0 {
+                isDownloaded = false
+                break
+            }
+        }
+        return isDownloaded
+    }
+
+    //  添加字幕信息
+    func addSubtitleMatraislInfo(subtitls: List<PQEditVisionTrackMaterialsModel>) {
+        deleteSubtitleMatraislInfo()
+
+        // 2,添加新的一组字幕
+        sectionTimeline!.visionTrack?.visionTrackMaterials.append(objectsIn: subtitls)
+    }
+
+    // 删除字幕
+    func deleteSubtitleMatraislInfo() {
+        // 1,确保每一个段落只有一组字幕 先移除老的一组字幕如果有, 只设置有效的素材
+        let otherEnableVision = sectionTimeline!.visionTrack?.getEnableVisionTrackMaterials()
+        sectionTimeline!.visionTrack?.visionTrackMaterials = otherEnableVision ?? List<PQEditVisionTrackMaterialsModel>.init()
+    }
+
+    // 判断当前段落是否有有效素材,1, 有视觉素材,2,有声音数据1)录音2)配音-- audioFilePath (如果是空的声音不算) 3,字幕 1)输入的的 2)录音转的
+    func haveRes() -> Bool {
+        var have: Bool = false
+        if subTitles.count > 0 || (sectionTimeline?.visionTrack?.getEnableVisionTrackMaterials().count ?? 0) > 0 || (audioFilePath.count > 0 && !audioFilePath.contains("empty")) || sectionTimeline?.visionTrack?.getSubtitleMatraislInfo().count ?? 0 > 0 || sectionText.count > 0 {
+            have = true
+        }
+
+        return have
+    }
+
+    /* 有一个特殊场景 根据当前段的视觉素材生成空声音文件
+     1)所有段没有文字
+     2)p1有视觉 p2 有
+     3)p1没有预览 ,P2直接点了全览
+     这个情况 P1 还没有生成空的文件会导致所有时长不对,贴纸在选择加入的时候已经处理,这里只处理这种 PART 的空声音文件
+
+     */
+    func generateEmptyAuido() {
+        var stickerTotalDuration: Float64 = 0
+        // 没有选择发声音人,只有纯视觉的情况
+        if sectionText.count == 0, audioFilePath.count == 0, mixEmptyAuidoFilePath.count == 0 {
+            PQLog(message: "section index is \(sectionIndex)")
+            if !isEmptyObject(object: sectionTimeline?.visionTrack?.getEnableVisionTrackMaterials()) {
+                for sticker in sectionTimeline!.visionTrack!.getEnableVisionTrackMaterials() {
+                    if sticker.timelineOut == 0, sticker.timelineIn == 0 {
+                        stickerTotalDuration = stickerTotalDuration + (sticker.out - sticker.model_in)
+                    } else {
+                        stickerTotalDuration = stickerTotalDuration + (sticker.timelineOut - sticker.timelineIn)
+                    }
+                }
+            }
+
+            // 创建空的数据
+            let tool = PQCreateEmptyWAV(sampleRate: 44100,
+                                        channel: 1,
+                                        duration: stickerTotalDuration,
+                                        bit: 16)
+            let timeInterval: TimeInterval = Date().timeIntervalSince1970
+            var documentPath = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true).first! as String
+            documentPath.append("/\(timeInterval).wav")
+            tool.createEmptyWAVFile(url: URL(fileURLWithPath: documentPath))
+           
+             audioFilePath = documentPath
+             sectionDuration = CMTimeGetSeconds(AVURLAsset(url: URL(fileURLWithPath: documentPath), options: avAssertOptions).duration)
+            PQLog(message: " 生成的空声音 \(String(describing: documentPath))  时长 \(String(describing: sectionDuration))")
+            
+        }
+    }
+
+    // 生成输入状态的可使用的文字信息
+    func getInputSubtitle() -> String {
+        var text: String = ""
+        if sectionTimeline?.visionTrack?.getSubtitleMatraislInfo() != nil {
+            for subTitleModel in sectionTimeline!.visionTrack!.getSubtitleMatraislInfo() {
+                if subTitleModel.subtitleInfo?.text.count ?? 0 > 0 {
+                    text = text + (subTitleModel.subtitleInfo?.text ?? "") + "\n"
+                }
+            }
+        }
+        return text
+    }
+
+    /// 删除录音的缓存 文件,在删除段落时调用
+    func deleteCacheAudioFiles(isDeleteMergeFile: Bool = false) {
+        let fileManger = FileManager.default
+        // 删除临时音频文件数据
+        // file:///var/mobile/Containers/Data/Application/176BE83D-F514-49EA-8710-8A077CFCFA72/Documents/Resource/ExportAudios/recorder_1614170151.815164_noise.wav
+        for url in cacheRecorderFiles {
+            if fileManger.fileExists(atPath: url.relativePath) {
+                do {
+                    try fileManger.removeItem(atPath: url.relativePath)
+                    print("\(url.relativePath)Success to remove file.")
+                } catch {
+                    print("Failed to remove file.")
+                }
+            }
+        }
+        cacheRecorderFiles.removeAll()
+        // 2 删除合并后的文件
+        if isDeleteMergeFile {
+            if fileManger.fileExists(atPath: compliteMP3AudioFile) {
+                do {
+                    try fileManger.removeItem(atPath: compliteMP3AudioFile)
+                    print("\(compliteMP3AudioFile)Success to remove file.")
+                } catch {
+                    print("Failed to remove file.")
+                }
+            }
+        }
+        compliteMP3AudioFile = ""
+        audioPowers = []
+    }
+}

+ 71 - 0
BFFramework/Classes/PModels/editDarftModels/PQEditSectionTimelineModel.swift

@@ -0,0 +1,71 @@
+/*
+ Copyright (c) 2020 Swift Models Generated from JSON powered by http://www.json4swift.com
+
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
+
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+ For support, please feel free to contact me at https://www.linkedin.com/in/syedabsar
+
+ */
+
+import Foundation
+import ObjectMapper
+import RealmSwift
+
+class PQEditSectionTimelineModel: PQEditBaseModel {
+    @objc dynamic var audioTrack: PQEditAudioTrackModel?
+    @objc dynamic var visionTrack: PQEditVisionTrackModel?
+
+    required convenience init?(map _: Map) {
+        self.init()
+    }
+
+    required init() {
+        super.init()
+        audioTrack = PQEditAudioTrackModel()
+        visionTrack = PQEditVisionTrackModel()
+    }
+
+    override func mapping(map: Map) {
+        audioTrack <- map["audioTrack"]
+        visionTrack <- map["visionTrack"]
+    }
+
+    // 添加发声音人数据
+    func addVoice(audioMix: PQVoiceModel) {
+        deleteVoice()
+        PQLog(message: "添加发声音人数据 \(audioMix.voice)")
+        let audioTrackMaterial = PQEditAudioTrackMaterialModel()
+
+        audioTrackMaterial.voiceType = VOICETYPT.PRODUCE.rawValue
+        audioTrackMaterial.type = "voice"
+        audioTrackMaterial.volumeGain = Float64(audioMix.volume)
+        audioTrackMaterial.duration = audioMix.wavfileDuration
+
+        let produceVoiceConfig = PQEditProduceVoiceConfigModel()
+        produceVoiceConfig.channel = audioMix.channel
+        //这里区分不同渠道 设置不同的参数语速和语调
+        if produceVoiceConfig.channel == "aliyun" {
+            produceVoiceConfig.pitchRate = Int(audioMix.pitchRate)
+            produceVoiceConfig.speechRate = Int(audioMix.speedRate)
+        } else {
+            produceVoiceConfig.azurePitchRate = "\(audioMix.pitchRate)%"
+            produceVoiceConfig.azureSpeechRate = "\(audioMix.speedRate)"
+            produceVoiceConfig.azureStyle = audioMix.azureStyle
+        }
+        produceVoiceConfig.voice = audioMix.voice
+        produceVoiceConfig.volume = audioMix.volume
+        audioTrackMaterial.produceVoiceConfig = produceVoiceConfig
+
+        audioTrack?.audioTrackMaterials.append(audioTrackMaterial)
+    }
+
+    // 删除发声音人,目前只有一个,配音
+    func deleteVoice() {
+        PQLog(message: "删除一个发声音人")
+        audioTrack?.audioTrackMaterials.removeAll()
+    }
+}

+ 44 - 0
BFFramework/Classes/PModels/editDarftModels/PQEditSubTitleModel.swift

@@ -0,0 +1,44 @@
+//
+//  PQEditSubtitleModel.swift
+//  PQSpeed
+//
+//  Created by ak on 2020/12/10.
+//  Copyright © 2020 BytesFlow. All rights reserved.
+//
+// 字幕信息以每一段为单位,都会有一个这样的信息
+import ObjectMapper
+import RealmSwift
+class PQEditSubTitleModel: PQEditBaseModel {
+    // 开始时间
+    var timelineIn: Float64 = 0
+    // 结束时间
+    var timelineOut: Float64 = 0
+    // 显示每一句字幕
+    var text: String = ""
+    required init() {
+        super.init()
+    }
+
+    required convenience init?(map _: Map) {
+        self.init()
+    }
+
+    override func mapping(map: Map) {
+        timelineIn <- map["timelineIn"]
+        timelineOut <- map["timelineOut"]
+        text <- map["text"]
+    }
+
+    init(jsonDict: [String: Any]) {
+        super.init()
+        if jsonDict.keys.contains("beginTime") {
+            timelineIn = Float64("\(jsonDict["beginTime"] ?? "0")") ?? 0
+        }
+        if jsonDict.keys.contains("endTime") {
+            timelineOut = Float64("\(jsonDict["endTime"] ?? "0")") ?? 0
+        }
+        if jsonDict.keys.contains("text") {
+            text = "\(jsonDict["text"] ?? "")"
+        }
+    }
+}

+ 47 - 0
BFFramework/Classes/PModels/editDarftModels/PQEditSubtitleInfoModel.swift

@@ -0,0 +1,47 @@
+//
+//  PQEditSubtitleInfoModel.swift
+//  PQSpeed
+//
+//  Created by ak on 2020/12/5.
+//  Copyright © 2020 BytesFlow. All rights reserved.
+//
+
+import ObjectMapper
+import RealmSwift
+import UIKit
+class PQEditSubtitleInfoModel: PQEditBaseModel {
+    @objc dynamic var layoutType: String = ""
+    @objc dynamic var text: String = "" {
+        didSet {
+            itemHeigth = sizeWithText(text: text, font: UIFont.systemFont(ofSize: 17), size: CGSize(width: cScreenWidth - cDefaultMargin * 9, height: CGFloat.greatestFiniteMagnitude)).height
+            if itemHeigth < cDefaultMargin * 2 {
+                itemHeigth = cDefaultMargin * 2
+            }
+            PQLog(message: "text== \(text),itemHeigth = \(itemHeigth)")
+        }
+    }
+
+    @objc dynamic var font: String = ""
+    @objc dynamic var fontColor: String = ""
+    @objc dynamic var bgColor: String = ""
+    @objc dynamic var fontSize: Int = 0
+    // cell高度
+    var itemHeigth: CGFloat = 20
+    required convenience init?(map _: Map) {
+        self.init()
+    }
+
+    // 过滤itemHeigth
+    override class func ignoredProperties() -> [String] {
+        return ["itemHeigth"]
+    }
+
+    override func mapping(map: Map) {
+        layoutType <- map["layoutType"]
+        text <- map["text"]
+        font <- map["font"]
+        fontColor <- map["fontColor"]
+        bgColor <- map["bgColor"]
+        fontSize <- map["fontSize"]
+    }
+}

+ 34 - 0
BFFramework/Classes/PModels/editDarftModels/PQEditSystemParamModel.swift

@@ -0,0 +1,34 @@
+/*
+ Copyright (c) 2020 Swift Models Generated from JSON powered by http://www.json4swift.com
+
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
+
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+ For support, please feel free to contact me at https://www.linkedin.com/in/syedabsar
+
+ */
+
+import Foundation
+import ObjectMapper
+import RealmSwift
+class PQEditSystemParamModel: PQEditBaseModel {
+    // 版本号,注意:这里是视频结构化数据格式版本,当前版本 2
+    @objc dynamic var versionCode: Int = 2
+    // 平台:ios/android/pc
+    @objc dynamic var platform: String = "ios"
+    // 产品代号
+    @objc dynamic var appType: Int = 13
+
+    required convenience init?(map _: Map) {
+        self.init()
+    }
+
+    override func mapping(map: Map) {
+        versionCode <- map["versionCode"]
+        platform <- map["platform"]
+        appType <- map["appType"]
+    }
+}

+ 46 - 0
BFFramework/Classes/PModels/editDarftModels/PQEditVideoMetaDataModel.swift

@@ -0,0 +1,46 @@
+/*
+ Copyright (c) 2020 Swift Models Generated from JSON powered by http://www.json4swift.com
+
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
+
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+ For support, please feel free to contact me at https://www.linkedin.com/in/syedabsar
+
+ */
+
+import Foundation
+import ObjectMapper
+import RealmSwift
+class PQEditVideoMetaDataModel: PQEditBaseModel {
+    // 是否添加片尾(1:是,0:否)
+    @objc dynamic var appendTailStatus: Int = 0
+    @objc dynamic var coverUrl: String = ""
+    @objc dynamic var duration: Float64 = 0
+    @objc dynamic var title: String = ""
+    // 输出视频的最终画布宽高,和canvasType 保持一致,只有在用户操作时设置时,其它地方都是只读
+    @objc dynamic var videoHeight: Int = 1080
+    @objc dynamic var videoWidth: Int = 1080
+    // 画布类型(1:原始,2:9比16,3:1比1,4:16比9)
+    @objc dynamic var canvasType: Int = 1
+
+    // 素材文件总大小
+    @objc dynamic var materialTotalSize: Float64 = 0
+
+    required convenience init?(map _: Map) {
+        self.init()
+    }
+
+    override func mapping(map: Map) {
+        appendTailStatus <- map["appendTailSftatus"]
+        coverUrl <- map["coverUrl"]
+        duration <- (map["duration"], timeTransform)
+        title <- map["title"]
+        videoHeight <- map["videoHeight"]
+        videoWidth <- map["videoWidth"]
+        canvasType <- map["canvasType"]
+        materialTotalSize <- map["materialTotalSize"]
+    }
+}

+ 265 - 0
BFFramework/Classes/PModels/editDarftModels/PQEditVisionTrackMaterialsModel.swift

@@ -0,0 +1,265 @@
+/*
+ Copyright (c) 2020 Swift Models Generated from JSON powered by http://www.json4swift.com
+
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
+
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+ For support, please feel free to contact me at https://www.linkedin.com/in/syedabsar
+
+ */
+
+import Foundation
+import Kingfisher
+//import KingfisherWebP
+import Photos
+
+import ObjectMapper
+import RealmSwift
+
+class PQEditVisionTrackMaterialsModel: PQEditBaseModel {
+    @objc dynamic var width: Float = 0
+    @objc dynamic var height: Float = 0
+    @objc var itemWidth: Float = 0
+    @objc var itemHeight: Float = 0
+    // 搜索素材来源
+    @objc dynamic var sourceType: Int = 0
+    @objc dynamic var type: String = ""
+    @objc dynamic var canvasFillType: String = ""
+
+    @objc dynamic var materialType: String = ""
+
+    // 选择素材的时候会给值,这里是素材的真实时长
+    @objc dynamic var duration: Float64 = 0
+    // 公式计算出来的逻辑显示时长
+    @objc var aptDuration: Float64 = 0
+
+    @objc dynamic var id: Int64 = 0
+    @objc dynamic var materialLayer: PQEditMaterialLayerModel?
+    @objc dynamic var materialEffect: PQEditMaterialEffectModel?
+    
+    //整体时间线中显示的开始和结束时间 (showtime) e.g. 10s - 20s
+    @objc dynamic var timelineIn: Float64 = 0
+    @objc dynamic var timelineOut: Float64 = 0
+    //取原视频素材的截取时间(cliptime) e.g.  5s - 10s
+    @objc dynamic var model_in: Float64 = 0
+    @objc dynamic var out: Float64 = 0
+    
+    @objc dynamic var volumeGain: Float64 = 0
+    @objc dynamic var subtitleInfo: PQEditSubtitleInfoModel?
+    @objc dynamic var materialDurationFit: PQEditmaterialDurationFitModel?
+    @objc dynamic var materialSizeClip: PQEditMaterialSizeClipModel?
+    @objc dynamic var materialPosition: PQEditMaterialPositionModel?
+
+    // -----------------------地址相关属性 本地地址都是 URI
+    // 素材的外网 URL 我方服务器的外网地址 ,客户端不会赋值 会在 getDraftInfo API 中返回,是服务器自动拼接生成的。XXXXXX 素材封面都是根据这个地址现取的
+    @objc dynamic var materialUrl: String = ""
+
+    // 这个是给网络素材库用的下载地址,其它地方都使用 materialUrl
+    var netResUrl: String = ""
+    var netResCoverImageURL: String?
+
+    // 文件本地地址 URI
+    @objc dynamic var locationPath: String = "" {
+        didSet {
+            PQLog(message: "如果是全路径就有问题XXXX 设置了新值为:locationPath\(locationPath)")
+        }
+    }
+
+    // --------------------------
+
+    // add by ak 业务逻辑扩展字段
+    // 是否为网络素材本地
+    var isNetworkMaterial: Bool = false
+    var isSelected: Bool = false // 是否被选中
+
+    // 这里不会给值了
+    var asset: PHAsset? // 视频资源
+    var PHImageRequestID : Int32?
+    var selectedIndex: Int = 1 // 选中的index
+    // 图片 和GIF 原数据 这里可能不在使用了
+    var originalData: Data?
+    // 封面 仅本地相册显示封面使用
+    var coverImageUI: UIImage?
+    var downloadState: downloadState = .downloading
+    var localSearchId: String? // 本地每次搜索ID
+    var sliceId: String? // 搜索素材视频id
+    var searchId: String? // 每次搜索ID
+    var searchResourceId: String? // 搜索资源ID
+    var outSideChannel: String = ""
+    var outSideVideoId: String = ""
+    var status: inputStatus = .recordSuccess
+
+    required init() {
+        super.init()
+        materialDurationFit = PQEditmaterialDurationFitModel()
+        materialSizeClip = PQEditMaterialSizeClipModel()
+        materialPosition = PQEditMaterialPositionModel()
+    }
+
+    required convenience init?(map _: Map) {
+        self.init()
+    }
+
+    override func mapping(map: Map) {
+        width <- map["width"]
+        height <- map["height"]
+        sourceType <- map["sourceType"]
+        type <- map["type"]
+        canvasFillType <- map["canvasFillType"]
+        materialUrl <- map["materialUrl"]
+
+        materialType <- map["materialType"]
+        duration <- (map["duration"], timeTransform)
+        id <- map["id"]
+        materialLayer <- map["materialLayer"]
+        materialEffect <- map["materialEffect"]
+        timelineIn <- (map["timelineIn"], timeTransform)
+
+        timelineOut <- (map["timelineOut"], timeTransform)
+
+        model_in <- (map["in"], timeTransform)
+        out <- (map["out"], timeTransform)
+        volumeGain <- (map["volumeGain"], volumeGainTransform)
+        subtitleInfo <- map["subtitleInfo"]
+        materialDurationFit <- map["materialDurationFit"]
+        materialSizeClip <- map["materialSizeClip"]
+        materialPosition <- map["materialPosition"]
+//        netResUrl <- map["netResUrl"]
+//        netResCoverImageURL <- map["netResCoverImageURL"]
+        locationPath <- map["locationPath"]
+
+        aptDuration <- map["aptDuration"]
+        outSideVideoId <- map["outSideVideoId"]
+        outSideChannel <- map["outSideChannel"]
+    }
+
+    // 视频素材有无裁剪过,时长计算就不一样了
+    func videoIsCrop() -> Bool {
+        return (type == StickerType.VIDEO.rawValue && (model_in != 0 || out != 0))
+    }
+
+    // 生成默认值
+    func generateDefaultValues() {
+        if materialDurationFit?.fitType.count == 0 {
+            materialDurationFit?.fitType = adapterMode.loopAuto.rawValue
+        }
+
+        if canvasFillType.count == 0 {
+            canvasFillType = stickerContentMode.aspectFitStr.rawValue
+        }
+    }
+
+    /// 初始化
+    /// - Parameter jsonDict: <#jsonDict description#>
+    convenience init(jsonDict: [String: Any]) {
+        self.init()
+        if jsonDict.keys.contains("width") {
+            width = Float(CGFloat(Double("\(jsonDict["width"] ?? "")") ?? 0))
+        }
+        if jsonDict.keys.contains("height") {
+            height = Float(CGFloat(Double("\(jsonDict["height"] ?? "")") ?? 0))
+        }
+        if jsonDict.keys.contains("imageUrl"), !(jsonDict["imageUrl"] is NSNull), "\(jsonDict["imageUrl"] ?? "")" != "<null>" {
+            netResCoverImageURL = "\(jsonDict["imageUrl"] ?? "")"
+        }
+        if jsonDict.keys.contains("gifUrl"), !(jsonDict["gifUrl"] is NSNull), "\(jsonDict["gifUrl"] ?? "")" != "<null>" {
+            netResCoverImageURL = "\(jsonDict["gifUrl"] ?? "")"
+        }
+        if jsonDict.keys.contains("coversImageUrl"), !(jsonDict["coversImageUrl"] is NSNull), "\(jsonDict["coversImageUrl"] ?? "")" != "<null>" {
+            netResCoverImageURL = "\(jsonDict["coversImageUrl"] ?? "")"
+        }
+        if jsonDict.keys.contains("videoUrl") {
+            netResUrl = "\(jsonDict["videoUrl"] ?? "")"
+        }
+        if jsonDict.keys.contains("duration") {
+            duration = Double((Int("\(jsonDict["duration"] ?? "0")") ?? 0) / 1_000_000)
+            out = duration
+        }
+        if jsonDict.keys.contains("id") {
+            searchResourceId = "\(jsonDict["id"] ?? "")"
+        }
+        if jsonDict.keys.contains("searchId") {
+            searchId = "\(jsonDict["searchId"] ?? "")"
+        }
+        if jsonDict.keys.contains("sliceId") {
+            sliceId = "\(jsonDict["sliceId"] ?? "")"
+        }
+        if jsonDict.keys.contains("sourceType") {
+            sourceType = Int("\(jsonDict["sourceType"] ?? "1")") ?? 0
+        }
+
+        if jsonDict.keys.contains("outSideVideoId"), "\(jsonDict["outSideVideoId"] ?? "")" != "<null>" {
+            outSideVideoId = "\(jsonDict["outSideVideoId"] ?? "")"
+        }
+
+        if jsonDict.keys.contains("outSideChannel"), "\(jsonDict["outSideChannel"] ?? "")" != "<null>" {
+            outSideChannel = "\(jsonDict["outSideChannel"] ?? "")"
+        }
+
+//        itemWidth = Float((cScreenWidth - PQChoseMaterialController.itemSpacing * 4) / 3)
+        itemHeight = itemWidth * (height / (width == 0 ? 1 : width))
+    }
+
+    /// 素材是否相等
+    /// - Parameter newMaterial: <#newMaterial description#>
+    /// - Returns: <#description#>
+    func isEqualMaterial(newMaterial: PQEditVisionTrackMaterialsModel?) -> Bool {
+        if newMaterial == nil {
+            PQLog(message: "素材对比为空:material = \(String(describing: newMaterial))")
+            return false
+        }
+        if asset != nil && newMaterial?.asset != nil && asset == newMaterial?.asset {
+            PQLog(message: "素材对比相等:asset = \(asset!)--\(String(describing: newMaterial?.asset))")
+            return true
+        }
+        if (netResCoverImageURL != nil && (netResCoverImageURL?.count ?? 0) > 0) && (newMaterial?.netResCoverImageURL != nil && (newMaterial?.netResCoverImageURL?.count ?? 0) > 0) && netResCoverImageURL == newMaterial?.netResCoverImageURL {
+            PQLog(message: "素材对比相等:netResCoverImageURL = \(netResCoverImageURL!)--\(String(describing: newMaterial?.netResCoverImageURL))")
+            return true
+        }
+        if (materialUrl.count > 0) && ((newMaterial?.materialUrl.count ?? 0) > 0) && materialUrl == newMaterial?.materialUrl {
+            PQLog(message: "素材对比相等:materialUrl = \(materialUrl)--\(String(describing: newMaterial?.materialUrl))")
+            return true
+        }
+        PQLog(message: "素材对比不相等:asset = \(String(describing: asset))--\(String(describing: newMaterial?.asset)),netResCoverImageURL = \(String(describing: netResCoverImageURL))--\(String(describing: newMaterial?.netResCoverImageURL)),materialUrl = \(materialUrl)--\(String(describing: newMaterial?.materialUrl))")
+        return false
+    }
+
+    func getCoverImage() -> UIImage? {
+        if coverImageUI != nil {
+            PQLog(message: "已经有封面了")
+            return coverImageUI
+        }
+        if locationPath.count == 0 {
+            PQLog(message: "本地地址为空可能没有下载完成!!")
+            return nil
+        }
+        PQLog(message: " locationPath is\(documensDirectory + locationPath)")
+        if type != StickerType.VIDEO.rawValue {
+            var coverImage = UIImage(contentsOfFile: documensDirectory + locationPath)
+
+            if coverImage == nil {
+                // 有可能是 WEBP
+                var fileData: Data?
+                if fileIsExists(filePath: documensDirectory + locationPath) {
+                    fileData = try! Data(contentsOf: URL(fileURLWithPath: documensDirectory + locationPath))
+                }
+//                if fileData != nil && (fileData?.count ?? 0) > 0 && fileData?.isWebPFormat ?? false {
+//                    PQLog(message: "这个资源为web!")
+//                    coverImage = WebPProcessor.default.process(item: ImageProcessItem.data(fileData!), options: [.onlyLoadFirstFrame, .scaleFactor(1)])
+//                }
+            }
+
+            return coverImage
+        } else {
+            if coverImageUI != nil {
+                PQLog(message: "已经有封面了")
+                return coverImageUI
+            } else {
+                return PQVideoSnapshotUtil.videoSnapshot(videoURL: URL(fileURLWithPath: documensDirectory + locationPath), time: 0)
+            }
+        }
+    }
+}

+ 114 - 0
BFFramework/Classes/PModels/editDarftModels/PQEditVisionTrackModel.swift

@@ -0,0 +1,114 @@
+/*
+ Copyright (c) 2020 Swift Models Generated from JSON powered by http://www.json4swift.com
+
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
+
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+ For support, please feel free to contact me at https://www.linkedin.com/in/syedabsar
+
+ */
+
+import Foundation
+import ObjectMapper
+import RealmSwift
+class PQEditVisionTrackModel: PQEditBaseModel {
+    @objc dynamic var count: Int = 0
+    @objc dynamic var duration: Float64 = 0
+    var visionTrackMaterials = List<PQEditVisionTrackMaterialsModel>.init()
+
+    required convenience init?(map _: Map) {
+        self.init()
+    }
+
+    override func mapping(map: Map) {
+        count <- map["count"]
+        duration <- (map["duration"], timeTransform)
+        visionTrackMaterials <- (map["visionTrackMaterials"], PQListTransform<PQEditVisionTrackMaterialsModel>())
+    }
+
+    // 取到指定类型素材 "image" "video" "gif"
+    func getEnableVisionTrackMaterials(type:String) -> List<PQEditVisionTrackMaterialsModel> {
+        let visionTrackMaterialsTemp = List<PQEditVisionTrackMaterialsModel>.init()
+
+
+        for visionTrackMaterialsModel in visionTrackMaterials {
+            if visionTrackMaterialsModel.type == type   {
+                visionTrackMaterialsTemp.append(visionTrackMaterialsModel)
+            }
+        }
+        return visionTrackMaterialsTemp
+    }
+    
+    // 取到有效的素材 image video gif
+    func getEnableVisionTrackMaterials() -> List<PQEditVisionTrackMaterialsModel> {
+        let visionTrackMaterialsTemp = List<PQEditVisionTrackMaterialsModel>.init()
+
+//        PQLog(message: "visionTrackMaterials 总数\(visionTrackMaterials.count)")
+        for visionTrackMaterialsModel in visionTrackMaterials {
+            if visionTrackMaterialsModel.type == "image" || visionTrackMaterialsModel.type == "video" || visionTrackMaterialsModel.type == "gif" {
+                visionTrackMaterialsTemp.append(visionTrackMaterialsModel)
+            }
+        }
+//        PQLog(message: "visionTrackMaterials 有效素材总数\(visionTrackMaterialsTemp.count)")
+        return visionTrackMaterialsTemp
+    }
+
+//    (video,image, gif)
+//    (video,image, gif,subtitle)
+    /// 交换有效视觉位置 不包括字幕
+    /// - Parameters:
+    ///   - at: 原位置
+    ///   - to: 目标位
+    func updateVisionTrackMaterialsIndex(at: Int, to: Int) {
+        // 如果有字幕缓存字幕,交换位置后在恢复原数组
+        let subtitlesTemp = List<PQEditVisionTrackMaterialsModel>.init()
+        for subtitle in getSubtitleMatraislInfo() {
+            subtitlesTemp.append(subtitle)
+        }
+
+        visionTrackMaterials = getEnableVisionTrackMaterials()
+
+        // 交换位置
+        let atItem = getEnableVisionTrackMaterials()[at]
+        visionTrackMaterials.remove(at: at)
+        visionTrackMaterials.insert(atItem, at: to)
+
+        // 恢复字幕
+        for subtitle in subtitlesTemp {
+            visionTrackMaterials.append(subtitle)
+        }
+    }
+
+    /// 取录音有效字幕数据
+    /// - Returns:
+    func getSubtitleMatraislInfo() -> List<PQEditVisionTrackMaterialsModel> {
+        let visionTrackMaterialsTemp = List<PQEditVisionTrackMaterialsModel>.init()
+
+        PQLog(message: "visionTrackMaterials 总数\(visionTrackMaterials.count)")
+        for visionTrackMaterialsModel in visionTrackMaterials {
+            if visionTrackMaterialsModel.type == "subtitle" {
+                visionTrackMaterialsTemp.append(visionTrackMaterialsModel)
+            }
+        }
+        PQLog(message: "visionTrackMaterials 本段有效字幕总数\(visionTrackMaterialsTemp.count)")
+        return visionTrackMaterialsTemp
+    }
+
+    /// 删除某个视觉素材,不包括字幕
+    /// - Parameter materialsModel: GIF . IMAGE, video 类型的
+    func deleteMatraislInfo(materialsModel: PQEditVisionTrackMaterialsModel?) {
+        if materialsModel == nil {
+            PQLog(message: "要删除的素材为空 操作不成功!")
+            return
+        }
+        let index = visionTrackMaterials.firstIndex(where: { (item) -> Bool in
+            item.isEqualMaterial(newMaterial: materialsModel)
+        })
+        if index != nil, visionTrackMaterials.count > (index ?? 0) {
+            visionTrackMaterials.remove(at: index!)
+        }
+    }
+}

+ 217 - 0
BFFramework/Classes/PQGPUImage/Source/BasicOperation.swift

@@ -0,0 +1,217 @@
+import Foundation
+
+public func defaultVertexShaderForInputs(_ inputCount: UInt) -> String {
+    switch inputCount {
+    case 1: return OneInputVertexShader
+    case 2: return TwoInputVertexShader
+    case 3: return ThreeInputVertexShader
+    case 4: return FourInputVertexShader
+    case 5: return FiveInputVertexShader
+    default: return OneInputVertexShader
+    }
+}
+
+open class BasicOperation: ImageProcessingOperation {
+    public let maximumInputs: UInt
+    public var overriddenOutputSize: Size?
+    public var overriddenOutputRotation: Rotation?
+    public var backgroundColor = Color.black
+    public var drawUnmodifiedImageOutsideOfMask: Bool = true
+    public var mask: ImageSource? {
+        didSet {
+            if let mask = mask {
+                maskImageRelay.newImageCallback = { [weak self] framebuffer in
+                    self?.maskFramebuffer?.unlock()
+                    framebuffer.lock()
+                    self?.maskFramebuffer = framebuffer
+                }
+                mask.addTarget(maskImageRelay)
+            } else {
+                maskFramebuffer?.unlock()
+                maskImageRelay.removeAllSources()
+                maskFramebuffer = nil
+            }
+        }
+    }
+
+    public var activatePassthroughOnNextFrame: Bool = false
+    public var uniformSettings = ShaderUniformSettings()
+
+    // MARK: -
+
+    // MARK: Internal
+
+    public let targets = TargetContainer()
+    public let sources = SourceContainer()
+    var shader: ShaderProgram
+    public var inputFramebuffers = [UInt: Framebuffer]()
+    var renderFramebuffer: Framebuffer!
+    var outputFramebuffer: Framebuffer { return renderFramebuffer }
+    let usesAspectRatio: Bool
+    let maskImageRelay = ImageRelay()
+    var maskFramebuffer: Framebuffer?
+
+    public private(set) var userInfo: [AnyHashable: Any]?
+
+    // MARK: -
+
+    // MARK: Initialization and teardown
+
+    public init(shader: ShaderProgram, numberOfInputs: UInt = 1) {
+        maximumInputs = numberOfInputs
+        self.shader = shader
+        usesAspectRatio = shader.uniformIndex("aspectRatio") != nil
+    }
+
+    public init(vertexShader: String? = nil, fragmentShader: String, numberOfInputs: UInt = 1, operationName: String = #file) {
+        let compiledShader = crashOnShaderCompileFailure(operationName) { try sharedImageProcessingContext.programForVertexShader(vertexShader ?? defaultVertexShaderForInputs(numberOfInputs), fragmentShader: fragmentShader) }
+        maximumInputs = numberOfInputs
+        shader = compiledShader
+        usesAspectRatio = shader.uniformIndex("aspectRatio") != nil
+    }
+
+    public init(vertexShaderFile: URL? = nil, fragmentShaderFile: URL, numberOfInputs: UInt = 1, operationName: String = #file) throws {
+        let compiledShader: ShaderProgram
+        if let vertexShaderFile = vertexShaderFile {
+            compiledShader = crashOnShaderCompileFailure(operationName) { try sharedImageProcessingContext.programForVertexShader(vertexShaderFile, fragmentShader: fragmentShaderFile) }
+        } else {
+            compiledShader = crashOnShaderCompileFailure(operationName) { try sharedImageProcessingContext.programForVertexShader(defaultVertexShaderForInputs(numberOfInputs), fragmentShader: fragmentShaderFile) }
+        }
+        maximumInputs = numberOfInputs
+        shader = compiledShader
+        usesAspectRatio = shader.uniformIndex("aspectRatio") != nil
+    }
+
+    deinit {
+        // debugPrint("Deallocating operation: \(self)")
+    }
+
+    // MARK: -
+
+    // MARK: Rendering
+
+    public func newFramebufferAvailable(_ framebuffer: Framebuffer, fromSourceIndex: UInt) {
+        if let previousFramebuffer = inputFramebuffers[fromSourceIndex] {
+            previousFramebuffer.unlock()
+        }
+        inputFramebuffers[fromSourceIndex] = framebuffer
+
+        guard !activatePassthroughOnNextFrame else { // Use this to allow a bootstrap of cyclical processing, like with a low pass filter
+            activatePassthroughOnNextFrame = false
+            updateTargetsWithFramebuffer(framebuffer)
+            return
+        }
+
+        if UInt(inputFramebuffers.count) >= maximumInputs {
+            renderFrame()
+
+            // add by ak 设置 timingStyle 到每一帧
+            outputFramebuffer.timingStyle = framebuffer.timingStyle
+
+            let currTime1 = CMTimeGetSeconds(CMTime(value: framebuffer.timingStyle.timestamp!.value, timescale: framebuffer.timingStyle.timestamp!.timescale))
+//            PQLog(message: "baseicOperation  framebuffer 当前时间: \(currTime1)")
+
+            let currTime = CMTimeGetSeconds(CMTime(value: outputFramebuffer.timingStyle.timestamp!.value, timescale: outputFramebuffer.timingStyle.timestamp!.timescale))
+//            PQLog(message: "baseicOperation 当前时间: \(currTime)")
+
+            updateTargetsWithFramebuffer(outputFramebuffer)
+        }
+    }
+
+    open func renderFrame() {
+        renderFramebuffer = sharedImageProcessingContext.framebufferCache.requestFramebufferWithProperties(orientation: .portrait, size: sizeOfInitialStageBasedOnFramebuffer(inputFramebuffers[0]!), stencil: mask != nil)
+
+        let textureProperties = initialTextureProperties()
+        configureFramebufferSpecificUniforms(inputFramebuffers[0]!)
+
+        renderFramebuffer.activateFramebufferForRendering()
+        clearFramebufferWithColor(backgroundColor)
+        if let maskFramebuffer = maskFramebuffer {
+            if drawUnmodifiedImageOutsideOfMask {
+                renderQuadWithShader(sharedImageProcessingContext.passthroughShader, uniformSettings: nil, vertexBufferObject: sharedImageProcessingContext.standardImageVBO, inputTextures: textureProperties)
+            }
+            renderStencilMaskFromFramebuffer(maskFramebuffer)
+            internalRenderFunction(inputFramebuffers[0]!, textureProperties: textureProperties)
+            disableStencil()
+        } else {
+            internalRenderFunction(inputFramebuffers[0]!, textureProperties: textureProperties)
+        }
+    }
+
+    func internalRenderFunction(_: Framebuffer, textureProperties: [InputTextureProperties]) {
+        renderQuadWithShader(shader, uniformSettings: uniformSettings, vertexBufferObject: sharedImageProcessingContext.standardImageVBO, inputTextures: textureProperties)
+        releaseIncomingFramebuffers()
+    }
+
+    func releaseIncomingFramebuffers() {
+        var remainingFramebuffers = [UInt: Framebuffer]()
+        // If all inputs are still images, have this output behave as one
+        renderFramebuffer.timingStyle = .stillImage
+
+        var foundUserInfo: [AnyHashable: Any]?
+
+        var latestTimestamp: Timestamp?
+        for (key, framebuffer) in inputFramebuffers {
+            // When there are multiple transient input sources, use the latest timestamp as the value to pass along
+            if let timestamp = framebuffer.timingStyle.timestamp {
+                if !(timestamp < (latestTimestamp ?? timestamp)) {
+                    latestTimestamp = timestamp
+                    renderFramebuffer.timingStyle = .videoFrame(timestamp: timestamp)
+                }
+
+                framebuffer.unlock()
+            } else {
+                remainingFramebuffers[key] = framebuffer
+            }
+
+            // Pick userInfo from whichever input buffer has it
+            if let framebufferUserInfo = framebuffer.userInfo {
+                foundUserInfo = framebufferUserInfo
+            }
+        }
+
+        userInfo = foundUserInfo
+        // Pass onto the output buffer
+        renderFramebuffer.userInfo = foundUserInfo
+
+        inputFramebuffers = remainingFramebuffers
+    }
+
+    func sizeOfInitialStageBasedOnFramebuffer(_ inputFramebuffer: Framebuffer) -> GLSize {
+        if let outputSize = overriddenOutputSize {
+            return GLSize(outputSize)
+        } else {
+            return inputFramebuffer.sizeForTargetOrientation(.portrait)
+        }
+    }
+
+    func initialTextureProperties() -> [InputTextureProperties] {
+        var inputTextureProperties = [InputTextureProperties]()
+
+        if let outputRotation = overriddenOutputRotation {
+            for framebufferIndex in 0..<inputFramebuffers.count {
+                inputTextureProperties.append(inputFramebuffers[UInt(framebufferIndex)]!.texturePropertiesForOutputRotation(outputRotation))
+            }
+        } else {
+            for framebufferIndex in 0..<inputFramebuffers.count {
+                inputTextureProperties.append(inputFramebuffers[UInt(framebufferIndex)]!.texturePropertiesForTargetOrientation(.portrait))
+            }
+        }
+
+        return inputTextureProperties
+    }
+
+    open func configureFramebufferSpecificUniforms(_ inputFramebuffer: Framebuffer) {
+        if usesAspectRatio {
+            let outputRotation = overriddenOutputRotation ?? inputFramebuffer.orientation.rotationNeededForOrientation(.portrait)
+            uniformSettings["aspectRatio"] = inputFramebuffer.aspectRatioForRotation(outputRotation)
+        }
+    }
+
+    public func transmitPreviousImage(to _: ImageConsumer, atIndex _: UInt) {
+        // guard let renderFramebuffer = self.renderFramebuffer, (!renderFramebuffer.timingStyle.isTransient()) else { return }
+
+        // renderFramebuffer.lock()
+        // target.newFramebufferAvailable(renderFramebuffer, fromSourceIndex:atIndex)
+    }
+}

+ 39 - 0
BFFramework/Classes/PQGPUImage/Source/CameraConversion.swift

@@ -0,0 +1,39 @@
+// Note: the original name of YUVToRGBConversion.swift for this file chokes the compiler on Linux for some reason
+
+// BT.601, which is the standard for SDTV.
+public let colorConversionMatrix601Default = Matrix3x3(rowMajorValues: [
+    1.164, 1.164, 1.164,
+    0.0, -0.392, 2.017,
+    1.596, -0.813, 0.0,
+])
+
+// BT.601 full range (ref: http://www.equasys.de/colorconversion.html)
+public let colorConversionMatrix601FullRangeDefault = Matrix3x3(rowMajorValues: [
+    1.0, 1.0, 1.0,
+    0.0, -0.343, 1.765,
+    1.4, -0.711, 0.0,
+])
+
+// BT.709, which is the standard for HDTV.
+public let colorConversionMatrix709Default = Matrix3x3(rowMajorValues: [
+    1.164, 1.164, 1.164,
+    0.0, -0.213, 2.112,
+    1.793, -0.533, 0.0,
+])
+
+public func convertYUVToRGB(shader: ShaderProgram, luminanceFramebuffer: Framebuffer, chrominanceFramebuffer: Framebuffer, secondChrominanceFramebuffer: Framebuffer? = nil, resultFramebuffer: Framebuffer, colorConversionMatrix: Matrix3x3) {
+    let textureProperties: [InputTextureProperties]
+    if let secondChrominanceFramebuffer = secondChrominanceFramebuffer {
+        textureProperties = [luminanceFramebuffer.texturePropertiesForTargetOrientation(resultFramebuffer.orientation), chrominanceFramebuffer.texturePropertiesForTargetOrientation(resultFramebuffer.orientation), secondChrominanceFramebuffer.texturePropertiesForTargetOrientation(resultFramebuffer.orientation)]
+    } else {
+        textureProperties = [luminanceFramebuffer.texturePropertiesForTargetOrientation(resultFramebuffer.orientation), chrominanceFramebuffer.texturePropertiesForTargetOrientation(resultFramebuffer.orientation)]
+    }
+    resultFramebuffer.activateFramebufferForRendering()
+    clearFramebufferWithColor(Color.black)
+    let uniformSettings = ShaderUniformSettings()
+    uniformSettings["colorConversionMatrix"] = colorConversionMatrix
+    renderQuadWithShader(shader, uniformSettings: uniformSettings, vertexBufferObject: sharedImageProcessingContext.standardImageVBO, inputTextures: textureProperties)
+    luminanceFramebuffer.unlock()
+    chrominanceFramebuffer.unlock()
+    secondChrominanceFramebuffer?.unlock()
+}

+ 20 - 0
BFFramework/Classes/PQGPUImage/Source/Color.swift

@@ -0,0 +1,20 @@
+public struct Color {
+    public let redComponent: Float
+    public let greenComponent: Float
+    public let blueComponent: Float
+    public let alphaComponent: Float
+
+    public init(red: Float, green: Float, blue: Float, alpha: Float = 1.0) {
+        redComponent = red
+        greenComponent = green
+        blueComponent = blue
+        alphaComponent = alpha
+    }
+
+    public static let black = Color(red: 0.0, green: 0.0, blue: 0.0, alpha: 1.0)
+    public static let white = Color(red: 1.0, green: 1.0, blue: 1.0, alpha: 1.0)
+    public static let red = Color(red: 1.0, green: 0.0, blue: 0.0, alpha: 1.0)
+    public static let green = Color(red: 0.0, green: 1.0, blue: 0.0, alpha: 1.0)
+    public static let blue = Color(red: 0.0, green: 0.0, blue: 1.0, alpha: 1.0)
+    public static let transparent = Color(red: 0.0, green: 0.0, blue: 0.0, alpha: 0.0)
+}

文件差异内容过多而无法显示
+ 8 - 0
BFFramework/Classes/PQGPUImage/Source/ConvertedShaders_GLES.swift


+ 77 - 0
BFFramework/Classes/PQGPUImage/Source/FillMode.swift

@@ -0,0 +1,77 @@
+#if os(Linux)
+    import Glibc
+    #if GLES
+        import COpenGLES.gles2
+    #else
+        import COpenGL
+    #endif
+#else
+    #if GLES
+        import OpenGLES
+    #else
+        import OpenGL.GL3
+    #endif
+#endif
+
+public enum FillMode {
+    case stretch
+    case preserveAspectRatio
+    case preserveAspectRatioAndFill
+
+    func transformVertices(_ vertices: [GLfloat], fromInputSize: GLSize, toFitSize: GLSize) -> [GLfloat] {
+        guard vertices.count == 8 else { fatalError("Attempted to transform a non-quad to account for fill mode.") }
+
+        let aspectRatio = GLfloat(fromInputSize.height) / GLfloat(fromInputSize.width)
+        let targetAspectRatio = GLfloat(toFitSize.height) / GLfloat(toFitSize.width)
+
+        let yRatio: GLfloat
+        let xRatio: GLfloat
+        switch self {
+        case .stretch: return vertices
+        case .preserveAspectRatio:
+            if aspectRatio > targetAspectRatio {
+                yRatio = 1.0
+//                    xRatio = (GLfloat(toFitSize.height) / GLfloat(fromInputSize.height)) * (GLfloat(fromInputSize.width) / GLfloat(toFitSize.width))
+                xRatio = (GLfloat(fromInputSize.width) / GLfloat(toFitSize.width)) * (GLfloat(toFitSize.height) / GLfloat(fromInputSize.height))
+            } else {
+                xRatio = 1.0
+                yRatio = (GLfloat(fromInputSize.height) / GLfloat(toFitSize.height)) * (GLfloat(toFitSize.width) / GLfloat(fromInputSize.width))
+            }
+        case .preserveAspectRatioAndFill:
+            if aspectRatio > targetAspectRatio {
+                xRatio = 1.0
+                yRatio = (GLfloat(fromInputSize.height) / GLfloat(toFitSize.height)) * (GLfloat(toFitSize.width) / GLfloat(fromInputSize.width))
+            } else {
+                yRatio = 1.0
+                xRatio = (GLfloat(toFitSize.height) / GLfloat(fromInputSize.height)) * (GLfloat(fromInputSize.width) / GLfloat(toFitSize.width))
+            }
+        }
+        // Pixel-align to output dimensions
+//        return [vertices[0] * xRatio, vertices[1] * yRatio, vertices[2] * xRatio, vertices[3] * yRatio, vertices[4] * xRatio, vertices[5] * yRatio, vertices[6] * xRatio, vertices[7] * yRatio]
+        // TODO: Determine if this is misaligning things
+
+        let xConversionRatio: GLfloat = xRatio * GLfloat(toFitSize.width) / 2.0
+        let xConversionDivisor: GLfloat = GLfloat(toFitSize.width) / 2.0
+        let yConversionRatio: GLfloat = yRatio * GLfloat(toFitSize.height) / 2.0
+        let yConversionDivisor: GLfloat = GLfloat(toFitSize.height) / 2.0
+
+        // The Double casting here is required by Linux
+
+        let value1: GLfloat = GLfloat(round(Double(vertices[0] * xConversionRatio))) / xConversionDivisor
+        let value2: GLfloat = GLfloat(round(Double(vertices[1] * yConversionRatio))) / yConversionDivisor
+        let value3: GLfloat = GLfloat(round(Double(vertices[2] * xConversionRatio))) / xConversionDivisor
+        let value4: GLfloat = GLfloat(round(Double(vertices[3] * yConversionRatio))) / yConversionDivisor
+        let value5: GLfloat = GLfloat(round(Double(vertices[4] * xConversionRatio))) / xConversionDivisor
+        let value6: GLfloat = GLfloat(round(Double(vertices[5] * yConversionRatio))) / yConversionDivisor
+        let value7: GLfloat = GLfloat(round(Double(vertices[6] * xConversionRatio))) / xConversionDivisor
+        let value8: GLfloat = GLfloat(round(Double(vertices[7] * yConversionRatio))) / yConversionDivisor
+
+        return [value1, value2, value3, value4, value5, value6, value7, value8]
+
+        // This expression chokes the compiler in Xcode 8.0, Swift 3
+//        return [GLfloat(round(Double(vertices[0] * xConversionRatio))) / xConversionDivisor, GLfloat(round(Double(vertices[1] * yConversionRatio))) / yConversionDivisor,
+//                GLfloat(round(Double(vertices[2] * xConversionRatio))) / xConversionDivisor, GLfloat(round(Double(vertices[3] * yConversionRatio))) / yConversionDivisor,
+//                GLfloat(round(Double(vertices[4] * xConversionRatio))) / xConversionDivisor, GLfloat(round(Double(vertices[5] * yConversionRatio))) / yConversionDivisor,
+//                GLfloat(round(Double(vertices[6] * xConversionRatio))) / xConversionDivisor, GLfloat(round(Double(vertices[7] * yConversionRatio))) / yConversionDivisor]
+    }
+}

+ 277 - 0
BFFramework/Classes/PQGPUImage/Source/Framebuffer.swift

@@ -0,0 +1,277 @@
+#if os(Linux)
+    import Glibc
+    #if GLES
+        import COpenGLES.gles2
+        let GL_BGRA = GL_RGBA // A hack: Raspberry Pi needs this or framebuffer creation fails
+    #else
+        import COpenGL
+    #endif
+#else
+    #if GLES
+        import OpenGLES
+    #else
+        import OpenGL.GL3
+    #endif
+#endif
+
+import AVFoundation
+import Foundation
+
+struct FramebufferCreationError: Error, CustomStringConvertible {
+    var description: String {
+        return "FramebufferCreationError(errorCode: \(errorCode), errorCodeDescription: \(errorCodeDescription))"
+    }
+
+    let errorCode: GLenum
+
+    var errorCodeDescription: String {
+        // Source --> https://www.khronos.org/registry/OpenGL-Refpages/es3.0/html/glCheckFramebufferStatus.xhtml
+        switch errorCode {
+        case GLenum(GL_FRAMEBUFFER_COMPLETE):
+            return "GL_FRAMEBUFFER_UNDEFINED"
+        case GLenum(GL_FRAMEBUFFER_INCOMPLETE_ATTACHMENT):
+            return "GL_FRAMEBUFFER_INCOMPLETE_ATTACHMENT"
+        case GLenum(GL_FRAMEBUFFER_INCOMPLETE_MISSING_ATTACHMENT):
+            return "GL_FRAMEBUFFER_INCOMPLETE_MISSING_ATTACHMENT"
+        case GLenum(GL_FRAMEBUFFER_UNSUPPORTED):
+            return "GL_FRAMEBUFFER_UNSUPPORTED"
+        case GLenum(GL_FRAMEBUFFER_INCOMPLETE_MULTISAMPLE):
+            return "GL_FRAMEBUFFER_INCOMPLETE_MULTISAMPLE"
+        case GLenum(GL_INVALID_ENUM):
+            return "GL_INVALID_ENUM"
+        default:
+            return "UNKNOWN"
+        }
+    }
+}
+
+public enum FramebufferTimingStyle {
+    case stillImage
+    case videoFrame(timestamp: Timestamp)
+
+    func isTransient() -> Bool {
+        switch self {
+        case .stillImage: return false
+        case .videoFrame: return true
+        }
+    }
+
+    public var timestamp: Timestamp? {
+        switch self {
+        case .stillImage: return nil
+        case let .videoFrame(timestamp): return timestamp
+        }
+    }
+}
+
+public class Framebuffer {
+    public var timingStyle: FramebufferTimingStyle = .stillImage
+    public var orientation: ImageOrientation
+    public var userInfo: [AnyHashable: Any]?
+
+    public let texture: GLuint
+    let framebuffer: GLuint?
+    let stencilBuffer: GLuint?
+    public let size: GLSize
+    let internalFormat: Int32
+    let format: Int32
+    let type: Int32
+
+    let hash: Int64
+    let textureOverride: Bool
+
+    unowned var context: OpenGLContext
+
+    public init(context: OpenGLContext, orientation: ImageOrientation, size: GLSize, textureOnly: Bool = false, minFilter: Int32 = GL_LINEAR, magFilter: Int32 = GL_LINEAR, wrapS: Int32 = GL_CLAMP_TO_EDGE, wrapT: Int32 = GL_CLAMP_TO_EDGE, internalFormat: Int32 = GL_RGBA, format: Int32 = GL_BGRA, type: Int32 = GL_UNSIGNED_BYTE, stencil: Bool = false, overriddenTexture: GLuint? = nil) throws {
+        self.context = context
+        self.size = size
+        self.orientation = orientation
+        self.internalFormat = internalFormat
+        self.format = format
+        self.type = type
+
+        hash = hashForFramebufferWithProperties(orientation: orientation, size: size, textureOnly: textureOnly, minFilter: minFilter, magFilter: magFilter, wrapS: wrapS, wrapT: wrapT, internalFormat: internalFormat, format: format, type: type, stencil: stencil)
+
+        if let newTexture = overriddenTexture {
+            textureOverride = true
+            texture = newTexture
+        } else {
+            textureOverride = false
+            texture = generateTexture(minFilter: minFilter, magFilter: magFilter, wrapS: wrapS, wrapT: wrapT)
+        }
+
+        if !textureOnly {
+            do {
+                let (createdFrameBuffer, createdStencil) = try generateFramebufferForTexture(texture, width: size.width, height: size.height, internalFormat: internalFormat, format: format, type: type, stencil: stencil)
+                framebuffer = createdFrameBuffer
+                stencilBuffer = createdStencil
+            } catch {
+                stencilBuffer = nil
+                framebuffer = nil
+                throw error
+            }
+        } else {
+            stencilBuffer = nil
+            framebuffer = nil
+        }
+    }
+
+    deinit {
+        if !textureOverride {
+            var mutableTexture = texture
+            context.runOperationAsynchronously {
+                glDeleteTextures(1, &mutableTexture)
+            }
+            // debugPrint("Delete texture at size: \(size)")
+        }
+
+        if let framebuffer = framebuffer {
+            var mutableFramebuffer = framebuffer
+            context.runOperationAsynchronously {
+                glDeleteFramebuffers(1, &mutableFramebuffer)
+            }
+        }
+
+        if let stencilBuffer = stencilBuffer {
+            var mutableStencil = stencilBuffer
+            context.runOperationAsynchronously {
+                glDeleteRenderbuffers(1, &mutableStencil)
+            }
+        }
+    }
+
+    public func sizeForTargetOrientation(_ targetOrientation: ImageOrientation) -> GLSize {
+        if orientation.rotationNeededForOrientation(targetOrientation).flipsDimensions() {
+            return GLSize(width: size.height, height: size.width)
+        } else {
+            return size
+        }
+    }
+
+    public func aspectRatioForRotation(_ rotation: Rotation) -> Float {
+        if rotation.flipsDimensions() {
+            return Float(size.width) / Float(size.height)
+        } else {
+            return Float(size.height) / Float(size.width)
+        }
+    }
+
+    public func texelSize(for rotation: Rotation) -> Size {
+        if rotation.flipsDimensions() {
+            return Size(width: 1.0 / Float(size.height), height: 1.0 / Float(size.width))
+        } else {
+            return Size(width: 1.0 / Float(size.width), height: 1.0 / Float(size.height))
+        }
+    }
+
+    func initialStageTexelSize(for rotation: Rotation) -> Size {
+        if rotation.flipsDimensions() {
+            return Size(width: 1.0 / Float(size.height), height: 0.0)
+        } else {
+            return Size(width: 0.0, height: 1.0 / Float(size.height))
+        }
+    }
+
+    public func texturePropertiesForOutputRotation(_ rotation: Rotation) -> InputTextureProperties {
+        return InputTextureProperties(textureVBO: context.textureVBO(for: rotation), texture: texture)
+    }
+
+    public func texturePropertiesForTargetOrientation(_ targetOrientation: ImageOrientation) -> InputTextureProperties {
+        return texturePropertiesForOutputRotation(orientation.rotationNeededForOrientation(targetOrientation))
+    }
+
+    public func activateFramebufferForRendering() {
+        guard let framebuffer = framebuffer else { fatalError("ERROR: Attempted to activate a framebuffer that has not been initialized") }
+        glBindFramebuffer(GLenum(GL_FRAMEBUFFER), framebuffer)
+        glViewport(0, 0, size.width, size.height)
+    }
+
+    // MARK: -
+
+    // MARK: Framebuffer cache
+
+    weak var cache: FramebufferCache?
+    var framebufferRetainCount = 0
+    public func lock() {
+        framebufferRetainCount += 1
+    }
+
+    func resetRetainCount() {
+        framebufferRetainCount = 0
+    }
+
+    public func unlock() {
+        framebufferRetainCount -= 1
+        if framebufferRetainCount < 1 {
+            if framebufferRetainCount < 0, cache != nil {
+                debugPrint("WARNING: Tried to overrelease a framebuffer")
+            }
+            framebufferRetainCount = 0
+            cache?.returnToCache(self)
+        }
+    }
+}
+
+func hashForFramebufferWithProperties(orientation _: ImageOrientation, size: GLSize, textureOnly: Bool = false, minFilter _: Int32 = GL_LINEAR, magFilter _: Int32 = GL_LINEAR, wrapS _: Int32 = GL_CLAMP_TO_EDGE, wrapT _: Int32 = GL_CLAMP_TO_EDGE, internalFormat: Int32 = GL_RGBA, format: Int32 = GL_BGRA, type: Int32 = GL_UNSIGNED_BYTE, stencil: Bool = false) -> Int64 {
+    var result: Int64 = 1
+    let prime: Int64 = 31
+    let yesPrime: Int64 = 1231
+    let noPrime: Int64 = 1237
+
+    // TODO: Complete the rest of this
+    result = prime * result + Int64(size.width)
+    result = prime * result + Int64(size.height)
+    result = prime * result + Int64(internalFormat)
+    result = prime * result + Int64(format)
+    result = prime * result + Int64(type)
+    result = prime * result + (textureOnly ? yesPrime : noPrime)
+    result = prime * result + (stencil ? yesPrime : noPrime)
+    return result
+}
+
+// MARK: -
+
+// MARK: Framebuffer-related extensions
+
+extension Rotation {
+    func textureCoordinates() -> [GLfloat] {
+        switch self {
+        case .noRotation: return [0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 1.0, 1.0]
+        case .rotateCounterclockwise: return [0.0, 1.0, 0.0, 0.0, 1.0, 1.0, 1.0, 0.0]
+        case .rotateClockwise: return [1.0, 0.0, 1.0, 1.0, 0.0, 0.0, 0.0, 1.0]
+        case .rotate180: return [1.0, 1.0, 0.0, 1.0, 1.0, 0.0, 0.0, 0.0]
+        case .flipHorizontally: return [1.0, 0.0, 0.0, 0.0, 1.0, 1.0, 0.0, 1.0]
+        case .flipVertically: return [0.0, 1.0, 1.0, 1.0, 0.0, 0.0, 1.0, 0.0]
+        case .rotateClockwiseAndFlipVertically: return [0.0, 0.0, 0.0, 1.0, 1.0, 0.0, 1.0, 1.0]
+        case .rotateClockwiseAndFlipHorizontally: return [1.0, 1.0, 1.0, 0.0, 0.0, 1.0, 0.0, 0.0]
+        }
+    }
+
+    func croppedTextureCoordinates(offsetFromOrigin _: Position, cropSize _: Size) -> [GLfloat] {
+        let minX = GLfloat(0)
+        let minY = GLfloat(0)
+        let maxX = GLfloat(0.5)
+        let maxY = GLfloat(0.5)
+
+        switch self {
+        case .noRotation: return [minX, minY, maxX, minY, minX, maxY, maxX, maxY]
+        case .rotateCounterclockwise: return [minX, maxY, minX, minY, maxX, maxY, maxX, minY]
+        case .rotateClockwise: return [maxX, minY, maxX, maxY, minX, minY, minX, maxY]
+        case .rotate180: return [maxX, maxY, minX, maxY, maxX, minY, minX, minY]
+        case .flipHorizontally: return [maxX, minY, minX, minY, maxX, maxY, minX, maxY]
+        case .flipVertically: return [minX, maxY, maxX, maxY, minX, minY, maxX, minY]
+        case .rotateClockwiseAndFlipVertically: return [minX, minY, minX, maxY, maxX, minY, maxX, maxY]
+        case .rotateClockwiseAndFlipHorizontally: return [maxX, maxY, maxX, minY, minX, maxY, minX, minY]
+        }
+    }
+}
+
+public extension Size {
+    func glWidth() -> GLint {
+        return GLint(round(Double(width)))
+    }
+
+    func glHeight() -> GLint {
+        return GLint(round(Double(height)))
+    }
+}

+ 64 - 0
BFFramework/Classes/PQGPUImage/Source/FramebufferCache.swift

@@ -0,0 +1,64 @@
+#if os(Linux)
+    #if GLES
+        import COpenGLES.gles2
+    #else
+        import COpenGL
+    #endif
+#else
+    #if GLES
+        import OpenGLES
+    #else
+        import OpenGL.GL3
+    #endif
+#endif
+
+// TODO: Add mechanism to purge framebuffers on low memory
+
+public class FramebufferCache {
+    var framebufferCache = [Int64: [Framebuffer]]()
+    let context: OpenGLContext
+
+    init(context: OpenGLContext) {
+        self.context = context
+    }
+
+    public func requestFramebufferWithProperties(orientation: ImageOrientation, size: GLSize, textureOnly: Bool = false, minFilter: Int32 = GL_LINEAR, magFilter: Int32 = GL_LINEAR, wrapS: Int32 = GL_CLAMP_TO_EDGE, wrapT: Int32 = GL_CLAMP_TO_EDGE, internalFormat: Int32 = GL_RGBA, format: Int32 = GL_BGRA, type: Int32 = GL_UNSIGNED_BYTE, stencil: Bool = false) -> Framebuffer {
+        let hash = hashForFramebufferWithProperties(orientation: orientation, size: size, textureOnly: textureOnly, minFilter: minFilter, magFilter: magFilter, wrapS: wrapS, wrapT: wrapT, internalFormat: internalFormat, format: format, type: type, stencil: stencil)
+        let framebuffer: Framebuffer
+
+        if framebufferCache.count > 20 {
+            debugPrint("WARNING: Runaway framebuffer cache with size: \(framebufferCache.count)")
+        }
+
+        if (framebufferCache[hash]?.count ?? -1) > 0 {
+            // debugPrint("Restoring previous framebuffer")
+            framebuffer = framebufferCache[hash]!.removeLast()
+            framebuffer.orientation = orientation
+        } else {
+            do {
+                // debugPrint("Generating new framebuffer at size: \(size)")
+
+                framebuffer = try Framebuffer(context: context, orientation: orientation, size: size, textureOnly: textureOnly, minFilter: minFilter, magFilter: magFilter, wrapS: wrapS, wrapT: wrapT, internalFormat: internalFormat, format: format, type: type, stencil: stencil)
+                framebuffer.cache = self
+            } catch {
+                fatalError("Could not create a framebuffer of the size (\(size.width), \(size.height)), error: \(error)")
+            }
+        }
+        return framebuffer
+    }
+
+    public func purgeAllUnassignedFramebuffers() {
+        framebufferCache.removeAll()
+    }
+
+    func returnToCache(_ framebuffer: Framebuffer) {
+        // sprint("Returning to cache: \(framebuffer)")
+        context.runOperationSynchronously {
+            if self.framebufferCache[framebuffer.hash] != nil {
+                self.framebufferCache[framebuffer.hash]!.append(framebuffer)
+            } else {
+                self.framebufferCache[framebuffer.hash] = [framebuffer]
+            }
+        }
+    }
+}

+ 16 - 0
BFFramework/Classes/PQGPUImage/Source/GPUImage-Bridging-Header.h

@@ -0,0 +1,16 @@
+//
+//  GPUImage-Bridging-Header.h
+//  GPUImage
+//
+//  Created by Josh Bernfeld on 12/7/17.
+//  Copyright © 2017 Sunset Lake Software LLC. All rights reserved.
+//
+
+#ifndef GPUImage_Bridging_Header_h
+#define GPUImage_Bridging_Header_h
+
+#import "NSObject+Exception.h"
+#import "TPCircularBuffer.h"
+#import "NXAVUtil.h"
+
+#endif /* GPUImage_Bridging_Header_h */

+ 27 - 0
BFFramework/Classes/PQGPUImage/Source/ImageGenerator.swift

@@ -0,0 +1,27 @@
+public class ImageGenerator: ImageSource {
+    public var size: Size
+
+    public let targets = TargetContainer()
+    var imageFramebuffer: Framebuffer!
+
+    public init(size: Size) {
+        self.size = size
+
+        sharedImageProcessingContext.runOperationSynchronously {
+            do {
+                imageFramebuffer = try Framebuffer(context: sharedImageProcessingContext, orientation: .portrait, size: GLSize(size))
+            } catch {
+                fatalError("Could not construct framebuffer of size: \(size), error:\(error)")
+            }
+        }
+    }
+
+    public func transmitPreviousImage(to target: ImageConsumer, atIndex: UInt) {
+        imageFramebuffer.lock()
+        target.newFramebufferAvailable(imageFramebuffer, fromSourceIndex: atIndex)
+    }
+
+    func notifyTargets() {
+        updateTargetsWithFramebuffer(imageFramebuffer)
+    }
+}

+ 42 - 0
BFFramework/Classes/PQGPUImage/Source/ImageOrientation.swift

@@ -0,0 +1,42 @@
+public enum ImageOrientation {
+    case portrait
+    case portraitUpsideDown
+    case landscapeLeft
+    case landscapeRight
+
+    public func rotationNeededForOrientation(_ targetOrientation: ImageOrientation) -> Rotation {
+        switch (self, targetOrientation) {
+        case (.portrait, .portrait), (.portraitUpsideDown, .portraitUpsideDown), (.landscapeLeft, .landscapeLeft), (.landscapeRight, .landscapeRight): return .noRotation
+        case (.portrait, .portraitUpsideDown): return .rotate180
+        case (.portraitUpsideDown, .portrait): return .rotate180
+        case (.portrait, .landscapeLeft): return .rotateCounterclockwise
+        case (.landscapeLeft, .portrait): return .rotateClockwise
+        case (.portrait, .landscapeRight): return .rotateClockwise
+        case (.landscapeRight, .portrait): return .rotateCounterclockwise
+        case (.landscapeLeft, .landscapeRight): return .rotate180
+        case (.landscapeRight, .landscapeLeft): return .rotate180
+        case (.portraitUpsideDown, .landscapeLeft): return .rotateClockwise
+        case (.landscapeLeft, .portraitUpsideDown): return .rotateCounterclockwise
+        case (.portraitUpsideDown, .landscapeRight): return .rotateCounterclockwise
+        case (.landscapeRight, .portraitUpsideDown): return .rotateClockwise
+        }
+    }
+}
+
+public enum Rotation {
+    case noRotation
+    case rotateCounterclockwise
+    case rotateClockwise
+    case rotate180
+    case flipHorizontally
+    case flipVertically
+    case rotateClockwiseAndFlipVertically
+    case rotateClockwiseAndFlipHorizontally
+
+    public func flipsDimensions() -> Bool {
+        switch self {
+        case .noRotation, .rotate180, .flipHorizontally, .flipVertically: return false
+        case .rotateCounterclockwise, .rotateClockwise, .rotateClockwiseAndFlipVertically, .rotateClockwiseAndFlipHorizontally: return true
+        }
+    }
+}

+ 124 - 0
BFFramework/Classes/PQGPUImage/Source/Matrix.swift

@@ -0,0 +1,124 @@
+#if !os(Linux)
+    import QuartzCore
+#endif
+
+public struct Matrix4x4 {
+    public let m11: Float, m12: Float, m13: Float, m14: Float
+    public let m21: Float, m22: Float, m23: Float, m24: Float
+    public let m31: Float, m32: Float, m33: Float, m34: Float
+    public let m41: Float, m42: Float, m43: Float, m44: Float
+
+    public init(rowMajorValues: [Float]) {
+        guard rowMajorValues.count > 15 else { fatalError("Tried to initialize a 4x4 matrix with fewer than 16 values") }
+
+        m11 = rowMajorValues[0]
+        m12 = rowMajorValues[1]
+        m13 = rowMajorValues[2]
+        m14 = rowMajorValues[3]
+
+        m21 = rowMajorValues[4]
+        m22 = rowMajorValues[5]
+        m23 = rowMajorValues[6]
+        m24 = rowMajorValues[7]
+
+        m31 = rowMajorValues[8]
+        m32 = rowMajorValues[9]
+        m33 = rowMajorValues[10]
+        m34 = rowMajorValues[11]
+
+        m41 = rowMajorValues[12]
+        m42 = rowMajorValues[13]
+        m43 = rowMajorValues[14]
+        m44 = rowMajorValues[15]
+    }
+
+    public static let identity = Matrix4x4(rowMajorValues: [1.0, 0.0, 0.0, 0.0,
+                                                            0.0, 1.0, 0.0, 0.0,
+                                                            0.0, 0.0, 1.0, 0.0,
+                                                            0.0, 0.0, 0.0, 1.0])
+}
+
+public struct Matrix3x3 {
+    public let m11: Float, m12: Float, m13: Float
+    public let m21: Float, m22: Float, m23: Float
+    public let m31: Float, m32: Float, m33: Float
+
+    public init(rowMajorValues: [Float]) {
+        guard rowMajorValues.count > 8 else { fatalError("Tried to initialize a 3x3 matrix with fewer than 9 values") }
+
+        m11 = rowMajorValues[0]
+        m12 = rowMajorValues[1]
+        m13 = rowMajorValues[2]
+
+        m21 = rowMajorValues[3]
+        m22 = rowMajorValues[4]
+        m23 = rowMajorValues[5]
+
+        m31 = rowMajorValues[6]
+        m32 = rowMajorValues[7]
+        m33 = rowMajorValues[8]
+    }
+
+    public static let identity = Matrix3x3(rowMajorValues: [1.0, 0.0, 0.0,
+                                                            0.0, 1.0, 0.0,
+                                                            0.0, 0.0, 1.0])
+
+    public static let centerOnly = Matrix3x3(rowMajorValues: [0.0, 0.0, 0.0,
+                                                              0.0, 1.0, 0.0,
+                                                              0.0, 0.0, 0.0])
+}
+
+func orthographicMatrix(_ left: Float, right: Float, bottom: Float, top: Float, near: Float, far: Float, anchorTopLeft: Bool = false) -> Matrix4x4 {
+    let r_l = right - left
+    let t_b = top - bottom
+    let f_n = far - near
+    var tx = -(right + left) / (right - left)
+    var ty = -(top + bottom) / (top - bottom)
+    let tz = -(far + near) / (far - near)
+
+    let scale: Float
+    if anchorTopLeft {
+        scale = 4.0
+        tx = -1.0
+        ty = -1.0
+    } else {
+        scale = 2.0
+    }
+
+    return Matrix4x4(rowMajorValues: [
+        scale / r_l, 0.0, 0.0, tx,
+        0.0, scale / t_b, 0.0, ty,
+        0.0, 0.0, scale / f_n, tz,
+        0.0, 0.0, 0.0, 1.0,
+    ])
+}
+
+#if !os(Linux)
+    public extension Matrix4x4 {
+        init(_ transform3D: CATransform3D) {
+            m11 = Float(transform3D.m11)
+            m12 = Float(transform3D.m12)
+            m13 = Float(transform3D.m13)
+            m14 = Float(transform3D.m14)
+
+            m21 = Float(transform3D.m21)
+            m22 = Float(transform3D.m22)
+            m23 = Float(transform3D.m23)
+            m24 = Float(transform3D.m24)
+
+            m31 = Float(transform3D.m31)
+            m32 = Float(transform3D.m32)
+            m33 = Float(transform3D.m33)
+            m34 = Float(transform3D.m34)
+
+            m41 = Float(transform3D.m41)
+            m42 = Float(transform3D.m42)
+            m43 = Float(transform3D.m43)
+            m44 = Float(transform3D.m44)
+        }
+
+        init(_ transform: CGAffineTransform) {
+            self.init(CATransform3DMakeAffineTransform(transform))
+        }
+    }
+#endif

+ 14 - 0
BFFramework/Classes/PQGPUImage/Source/NSObject+Exception.h

@@ -0,0 +1,14 @@
+//
+//  NSObject+Exception.h
+//  GPUImage2
+//
+//  Created by Josh Bernfeld on 11/23/17.
+//
+
+#import <Foundation/Foundation.h>
+
+@interface NSObject (Exception)
+
++ (BOOL)catchException:(void(^)(void))tryBlock error:(__autoreleasing NSError **)error;
+
+@end

+ 24 - 0
BFFramework/Classes/PQGPUImage/Source/NSObject+Exception.m

@@ -0,0 +1,24 @@
+//
+//  NSObject+Exception.m
+//  GPUImage2
+//
+//  Created by Josh Bernfeld on 11/23/17.
+//
+//  Source: https://stackoverflow.com/a/36454808/1275014
+
+#import "NSObject+Exception.h"
+
+@implementation NSObject (Exception)
+
++ (BOOL)catchException:(void(^)(void))tryBlock error:(__autoreleasing NSError **)error {
+    @try {
+        tryBlock();
+        return YES;
+    }
+    @catch (NSException *exception) {
+        *error = [[NSError alloc] initWithDomain:exception.name code:0 userInfo:exception.userInfo];
+        return NO;
+    }
+}
+
+@end

+ 66 - 0
BFFramework/Classes/PQGPUImage/Source/NXAVUtil.h

@@ -0,0 +1,66 @@
+//
+//  NXAVUtil.h
+//  OpenGLVideoMerge
+//
+//  Created by AK on 2017/4/17.
+//  Copyright © 2017年 Tuo. All rights reserved.
+//
+
+#import <UIKit/UIKit.h>
+#import <AVFoundation/AVFoundation.h>
+@interface NXAVUtil : NSObject
+{
+
+}
+
+typedef enum { kGPUImageNoRotation, kGPUImageRotateLeft, kGPUImageRotateRight, kGPUImageFlipVertical, kGPUImageFlipHorizonal, kGPUImageRotateRightFlipVertical, kGPUImageRotateRightFlipHorizontal, kGPUImageRotate180 } GPUImageRotationMode;
+
+/**
+  把指定 size 统一到16的位数
+
+ @param size 原始大小
+ @return 处理后的大小
+ */
++ (CGSize)aptSize:(CGSize)size;
+/**
+ 
+ CGImageRef to CVPixelBufferRef
+ @param image CGImageRef
+ @param imageSize 指定大小
+ @return 返回 CVPixelBufferRef 注意在返回的 buffer 不在使用的时候 调用 CVPixelBufferRef CVBufferRelease 析构
+ */
++ (CVPixelBufferRef)pixelBufferFromImage:(UIImage *)image
+                                      size:(CGSize)imageSize;
+
+
+/**
+ 两个图片的过渡效果
+
+ @param baseImage 原图片
+ @param fadeInImage 过渡图片
+ @param imageSize 图片大小
+ @param alpha 透明度
+ @return 返回合成后 CVPixelBufferRef 注意在返回的 buffer 不在使用的时候 调用 CVPixelBufferRef CVBufferRelease 析构
+ */
++ (CVPixelBufferRef)crossFadeImage:(UIImage *)baseImage
+                           toImage:(UIImage *)fadeInImage
+                            atSize:(CGSize)imageSize
+                         withAlpha:(CGFloat)alpha;
+
+/**
+ cvpixel buffer ref to uiimage
+
+ @param pixelBuffer pix buff
+ @return image obj
+ */
++ (UIImage *)imageFromCVPixelBufferRef:(CVPixelBufferRef)pixelBuffer;
+
++ (CMSampleBufferRef *)GPUImageCreateResizedSampleBuffer:(CVPixelBufferRef) cameraFrame finalSize:(CGSize)finalSize;
+
++ (CVPixelBufferRef)rotateBuffer:(CMSampleBufferRef)sampleBuffer withConstant:(uint8_t)rotationConstant;
+
++ (CVPixelBufferRef)correctBufferOrientation:(CMSampleBufferRef)sampleBuffer;
+
++ (const GLfloat *)textureCoordinatesForRotation:(GPUImageRotationMode)rotationMode;
+@end
+

+ 430 - 0
BFFramework/Classes/PQGPUImage/Source/NXAVUtil.m

@@ -0,0 +1,430 @@
+//
+//  NXAVUtil.m
+//  OpenGLVideoMerge
+//
+//  Created by AK on 2017/4/17.
+//  Copyright © 2017年 Tuo. All rights reserved.
+//
+
+#import "NXAVUtil.h"
+#import "UIImage+NXCategory.h"
+#import <AVFoundation/AVFoundation.h>
+#import <Accelerate/Accelerate.h>
+@implementation NXAVUtil
+
+
+
+
++ (CGSize)aptSize:(CGSize)size
+{
+    int ft = (int)size.width%16;
+    if (ft!=0)
+    {
+        int num = (int)size.width/16;
+        double w = num * 16;
+        double h = num*16*size.height/size.width;
+        h = ((int)h)%2 == 0? (int)h:(int)h-1;
+        return CGSizeMake(w,h);
+        
+    }
+    //宽度是 16的倍数时, 处理一下高度的 奇数问题
+    size.height = ((int)size.height)%2 == 0? (int)size.height:(int)size.height-1;
+    
+    return size;
+
+}
+
++ (CVPixelBufferRef)pixelBufferFromImage:(UIImage *)image
+                                      size:(CGSize)imageSize
+{
+    //XXXXX 如果原图宽度不是16的位数,则缩放原图到16的倍数,
+   
+    //1,图片的归到16的倍数
+    CGSize aptImageSize = [NXAVUtil aptSize:image.size];
+    if (!CGSizeEqualToSize(image.size, aptImageSize))
+    {
+        NSLog(@"原图大小宽度不是16的倍数 %@",NSStringFromCGSize(image.size));
+        
+        image = [image nx_scaleToSize:aptImageSize];
+    
+        NSLog(@"归16后大小 %@",NSStringFromCGSize(image.size));
+    }
+    //2,指定输出的大小归到16的倍数
+    imageSize = [NXAVUtil aptSize:imageSize];
+
+   
+    CGImageRef  imageRef = image.CGImage;
+    NSDictionary *options = @{(id)kCVPixelBufferCGImageCompatibilityKey: @YES,
+                              (id)kCVPixelBufferCGBitmapContextCompatibilityKey: @YES};
+    CVPixelBufferRef pxbuffer = NULL;
+    CVReturn status = CVPixelBufferCreate(kCFAllocatorDefault, imageSize.width,
+                                          imageSize.height, kCVPixelFormatType_32ARGB, (__bridge CFDictionaryRef) options,
+                                          &pxbuffer);
+    NSParameterAssert(status == kCVReturnSuccess && pxbuffer != NULL);
+    
+    CVPixelBufferLockBaseAddress(pxbuffer, 0);
+    void *pxdata = CVPixelBufferGetBaseAddress(pxbuffer);
+    NSParameterAssert(pxdata != NULL);
+    
+    //@see 图片解压缩对比 http://blog.leichunfeng.com/blog/2017/02/20/talking-about-the-decompression-of-the-image-in-ios/
+    //@see http://blog.csdn.net/jymn_chen/article/details/18645203
+    //创建颜色空间
+    CGColorSpaceRef rgbColorSpace = CGColorSpaceCreateDeviceRGB();
+    
+    /*
+     data   指向要渲染的绘制内存的地址。这个内存块的大小至少是(bytesPerRow*height)个字节
+     width   bitmap的宽度,单位为像素
+     height  bitmap的高度,单位为像素
+     bitsPerComponent        内存中像素的每个组件的位数.例如,对于32位像素格式和RGB 颜色空间,你应该将这个值设为8.
+     bytesPerRow     bitmap的每一行在内存所占的比特数
+     colorspace              bitmap上下文使用的颜色空间。
+     bitmapInfo       指定bitmap是否包含alpha通道,像素中alpha通道的相对位置,像素组件是整形还是浮点型等信息的字符
+     */
+    CGContextRef context = CGBitmapContextCreate(pxdata, imageSize.width,
+                                                 imageSize.height, 8, 4*imageSize.width, rgbColorSpace,
+                                                 kCGImageAlphaNoneSkipFirst);
+    NSParameterAssert(context);
+    
+    CGRect rect = CGRectMake((imageSize.width - CGImageGetWidth(imageRef))/2.,
+                            (imageSize.height - CGImageGetHeight(imageRef))/2.,
+                            CGImageGetWidth(imageRef),
+                            CGImageGetHeight(imageRef));
+    
+    CGContextDrawImage(context, rect, imageRef);
+    
+    //release C对象 它是不受ARC管理可以用CFGetRetainCount来查看当前引用计数  如,CFGetRetainCount(rgbColorSpace)
+    CGColorSpaceRelease(rgbColorSpace);
+    CGContextRelease(context);
+    
+    CVPixelBufferUnlockBaseAddress(pxbuffer, 0);
+    
+    return pxbuffer;
+}
+
++ (CVPixelBufferRef)crossFadeImage:(UIImage *)baseImage
+                           toImage:(UIImage *)fadeInImage
+                            atSize:(CGSize)imageSize
+                         withAlpha:(CGFloat)alpha
+{
+    
+    //1,图片1的归到16的倍数
+    CGSize aptBaseImageSize = [NXAVUtil aptSize:baseImage.size];
+    if (!CGSizeEqualToSize(baseImage.size, aptBaseImageSize))
+    {
+        baseImage = [baseImage nx_scaleToSize:aptBaseImageSize];
+    }
+    //2,图片2的归到16的倍数
+    CGSize aptFadeImageSize = [NXAVUtil aptSize:baseImage.size];
+    if (!CGSizeEqualToSize(baseImage.size, aptFadeImageSize))
+    {
+        fadeInImage = [fadeInImage nx_scaleToSize:aptFadeImageSize];
+    }
+    //3,指定输出的大小归到16的倍数
+    imageSize = [NXAVUtil aptSize:imageSize];
+    
+    
+    NSDictionary *options = @{(id)kCVPixelBufferCGImageCompatibilityKey: @YES,
+                              (id)kCVPixelBufferCGBitmapContextCompatibilityKey: @YES};
+    CVPixelBufferRef pxbuffer = NULL;
+    CVReturn status = CVPixelBufferCreate(kCFAllocatorDefault, imageSize.width,
+                                          imageSize.height, kCVPixelFormatType_32ARGB, (__bridge CFDictionaryRef) options,
+                                          &pxbuffer);
+    NSParameterAssert(status == kCVReturnSuccess && pxbuffer != NULL);
+    
+    CVPixelBufferLockBaseAddress(pxbuffer, 0);
+    void *pxdata = CVPixelBufferGetBaseAddress(pxbuffer);
+    NSParameterAssert(pxdata != NULL);
+    
+    CGColorSpaceRef rgbColorSpace = CGColorSpaceCreateDeviceRGB();
+    CGContextRef context = CGBitmapContextCreate(pxdata, imageSize.width,
+                                                 imageSize.height, 8, 4*imageSize.width, rgbColorSpace,
+                                                 kCGImageAlphaNoneSkipFirst);
+    NSParameterAssert(context);
+    
+    CGRect drawRect = CGRectMake(0 + (imageSize.width-CGImageGetWidth(baseImage.CGImage))/2,
+                                 (imageSize.height-CGImageGetHeight(baseImage.CGImage))/2,
+                                 CGImageGetWidth(baseImage.CGImage),
+                                 CGImageGetHeight(baseImage.CGImage));
+    
+    CGContextDrawImage(context, drawRect, baseImage.CGImage);
+    
+    CGContextBeginTransparencyLayer(context, nil);
+    CGContextSetAlpha( context, alpha );
+    CGContextDrawImage(context, drawRect, fadeInImage.CGImage);
+    CGContextEndTransparencyLayer(context);
+    
+    CGColorSpaceRelease(rgbColorSpace);
+    CGContextRelease(context);
+    
+    CVPixelBufferUnlockBaseAddress(pxbuffer, 0);
+    
+    return pxbuffer;
+}
+
++ (UIImage *)imageFromCVPixelBufferRef:(CVPixelBufferRef)pixelBuffer
+{
+    CIImage *ciImage = [CIImage imageWithCVPixelBuffer:pixelBuffer];
+    
+    CIContext *temporaryContext = [CIContext contextWithOptions:nil];
+    CGImageRef videoImage = [temporaryContext
+                             createCGImage:ciImage
+                             fromRect:CGRectMake(0, 0,
+                                                 CVPixelBufferGetWidth(pixelBuffer),
+                                                 CVPixelBufferGetHeight(pixelBuffer))];
+    
+    UIImage *image = [UIImage imageWithCGImage:videoImage];
+    CGImageRelease(videoImage);
+    return image;
+}
+
+
++ (CMSampleBufferRef *)GPUImageCreateResizedSampleBuffer:(CVPixelBufferRef) cameraFrame finalSize:(CGSize)finalSize
+{
+    // CVPixelBufferCreateWithPlanarBytes for YUV input
+//CMSampleBuffer
+    CMSampleBufferRef * output = NULL;
+
+     CGSize originalSize = CGSizeMake(CVPixelBufferGetWidth(cameraFrame), CVPixelBufferGetHeight(cameraFrame));
+
+     CVPixelBufferLockBaseAddress(cameraFrame, 0);
+     GLubyte *sourceImageBytes =  CVPixelBufferGetBaseAddress(cameraFrame);
+     CGDataProviderRef dataProvider = CGDataProviderCreateWithData(NULL, sourceImageBytes, CVPixelBufferGetBytesPerRow(cameraFrame) * originalSize.height, NULL);
+     CGColorSpaceRef genericRGBColorspace = CGColorSpaceCreateDeviceRGB();
+     CGImageRef cgImageFromBytes = CGImageCreate((int)originalSize.width, (int)originalSize.height, 8, 32, CVPixelBufferGetBytesPerRow(cameraFrame), genericRGBColorspace, kCGBitmapByteOrder32Little | kCGImageAlphaPremultipliedFirst, dataProvider, NULL, NO, kCGRenderingIntentDefault);
+
+     GLubyte *imageData = (GLubyte *) calloc(1, (int)finalSize.width * (int)finalSize.height * 4);
+
+     CGContextRef imageContext = CGBitmapContextCreate(imageData, (int)finalSize.width, (int)finalSize.height, 8, (int)finalSize.width * 4, genericRGBColorspace,  kCGBitmapByteOrder32Little | kCGImageAlphaPremultipliedFirst);
+     CGContextDrawImage(imageContext, CGRectMake(0.0, 0.0, finalSize.width, finalSize.height), cgImageFromBytes);
+     CGImageRelease(cgImageFromBytes);
+     CGContextRelease(imageContext);
+     CGColorSpaceRelease(genericRGBColorspace);
+     CGDataProviderRelease(dataProvider);
+
+     CVPixelBufferRef pixel_buffer = NULL;
+     CVPixelBufferCreateWithBytes(kCFAllocatorDefault, finalSize.width, finalSize.height, kCVPixelFormatType_32BGRA, imageData, finalSize.width * 4, NULL, NULL, NULL, &pixel_buffer);
+     CMVideoFormatDescriptionRef videoInfo = NULL;
+     CMVideoFormatDescriptionCreateForImageBuffer(NULL, pixel_buffer, &videoInfo);
+
+     CMTime frameTime = CMTimeMake(1, 30);
+     CMSampleTimingInfo timing = {frameTime, frameTime, kCMTimeInvalid};
+
+     CMSampleBufferCreateForImageBuffer(kCFAllocatorDefault, pixel_buffer, YES, NULL, NULL, videoInfo, &timing, output);
+     CFRelease(videoInfo);
+     CVPixelBufferRelease(pixel_buffer);
+    
+    return output;
+}
+
+
++ (CVPixelBufferRef)imageToYUVPixelBuffer:(UIImage *)image {
+    CGSize frameSize = CGSizeMake(CGImageGetWidth(image.CGImage), CGImageGetHeight(image.CGImage));
+    NSDictionary *options = [NSDictionary dictionaryWithObjectsAndKeys:
+                             [NSNumber numberWithBool:YES],kCVPixelBufferCGImageCompatibilityKey,
+                             [NSNumber numberWithBool:YES],kCVPixelBufferCGBitmapContextCompatibilityKey,nil];
+    CVPixelBufferRef pxbuffer = NULL;
+    CVPixelBufferCreate(kCFAllocatorDefault, frameSize.width, frameSize.height,kCVPixelFormatType_420YpCbCr8BiPlanarFullRange, (__bridge CFDictionaryRef)options,&pxbuffer);
+    CVPixelBufferLockBaseAddress(pxbuffer, 0);
+    void *pxdata = CVPixelBufferGetBaseAddressOfPlane(pxbuffer,0);
+    CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceGray();
+    CGContextRef context = CGBitmapContextCreate(pxdata, frameSize.width, frameSize.height,8,CVPixelBufferGetBytesPerRowOfPlane(pxbuffer, 0),colorSpace,kCGImageAlphaNone);
+    CGContextDrawImage(context, CGRectMake(0, 0, CGImageGetWidth(image.CGImage),CGImageGetHeight(image.CGImage)), image.CGImage);
+    CGColorSpaceRelease(colorSpace);
+    CGContextRelease(context);
+    CVPixelBufferUnlockBaseAddress(pxbuffer, 0);
+    return pxbuffer;
+}
+
+/* rotationConstant:
+ *  0 -- rotate 0 degrees (simply copy the data from src to dest)
+ *  1 -- rotate 90 degrees counterclockwise
+ *  2 -- rotate 180 degress
+ *  3 -- rotate 270 degrees counterclockwise
+ */
+
++ (CVPixelBufferRef)rotateBuffer:(CMSampleBufferRef)sampleBuffer withConstant:(uint8_t)rotationConstant
+{
+    [NXAVUtil correctBufferOrientation:sampleBuffer];
+    CVImageBufferRef imageBuffer        = CMSampleBufferGetImageBuffer(sampleBuffer);
+    CVPixelBufferLockBaseAddress(imageBuffer, 0);
+
+    OSType pixelFormatType              = CVPixelBufferGetPixelFormatType(imageBuffer);
+    NSLog(@"pixelFormatType",pixelFormatType);
+    NSAssert(pixelFormatType == kCVPixelFormatType_32ARGB, @"Code works only with 32ARGB format. Test/adapt for other formats!");
+
+    const size_t kAlignment_32ARGB      = 32;
+    const size_t kBytesPerPixel_32ARGB  = 4;
+
+    size_t bytesPerRow                  = CVPixelBufferGetBytesPerRow(imageBuffer);
+    size_t width                        = CVPixelBufferGetWidth(imageBuffer);
+    size_t height                       = CVPixelBufferGetHeight(imageBuffer);
+
+    BOOL rotatePerpendicular            = (rotationConstant == 1) || (rotationConstant == 3); // Use enumeration values here
+    const size_t outWidth               = rotatePerpendicular ? height : width;
+    const size_t outHeight              = rotatePerpendicular ? width  : height;
+
+    size_t bytesPerRowOut               = kBytesPerPixel_32ARGB * ceil(outWidth * 1.0 / kAlignment_32ARGB) * kAlignment_32ARGB;
+
+    const size_t dstSize                = bytesPerRowOut * outHeight * sizeof(unsigned char);
+
+    void *srcBuff                       = CVPixelBufferGetBaseAddress(imageBuffer);
+
+    unsigned char *dstBuff              = (unsigned char *)malloc(dstSize);
+
+    vImage_Buffer inbuff                = {srcBuff, height, width, bytesPerRow};
+    vImage_Buffer outbuff               = {dstBuff, outHeight, outWidth, bytesPerRowOut};
+
+    uint8_t bgColor[4]                  = {0, 0, 0, 0};
+
+    vImage_Error err                    = vImageRotate90_ARGB8888(&inbuff, &outbuff, rotationConstant, bgColor, 0);
+    if (err != kvImageNoError)
+    {
+        NSLog(@"%ld", err);
+    }
+
+    CVPixelBufferUnlockBaseAddress(imageBuffer, 0);
+
+    CVPixelBufferRef rotatedBuffer      = NULL;
+    CVPixelBufferCreateWithBytes(NULL,
+                                 outWidth,
+                                 outHeight,
+                                 pixelFormatType,
+                                 outbuff.data,
+                                 bytesPerRowOut,
+                                 freePixelBufferDataAfterRelease,
+                                 NULL,
+                                 NULL,
+                                 &rotatedBuffer);
+
+    return rotatedBuffer;
+}
+
+ 
++ (CVPixelBufferRef)correctBufferOrientation:(CMSampleBufferRef)sampleBuffer
+{
+    CVImageBufferRef imageBuffer        = CMSampleBufferGetImageBuffer(sampleBuffer);
+    CVPixelBufferLockBaseAddress(imageBuffer, 0);
+
+    size_t bytesPerRow                  = CVPixelBufferGetBytesPerRow(imageBuffer);
+    size_t width                        = CVPixelBufferGetWidth(imageBuffer);
+    size_t height                       = CVPixelBufferGetHeight(imageBuffer);
+    size_t currSize                     = bytesPerRow * height * sizeof(unsigned char);
+    size_t bytesPerRowOut               = 4 * height * sizeof(unsigned char);
+
+    void *srcBuff                       = CVPixelBufferGetBaseAddress(imageBuffer);
+
+    /* rotationConstant:
+     *  0 -- rotate 0 degrees (simply copy the data from src to dest)
+     *  1 -- rotate 90 degrees counterclockwise
+     *  2 -- rotate 180 degress
+     *  3 -- rotate 270 degrees counterclockwise
+     */
+    uint8_t rotationConstant            = 3;
+
+    unsigned char *dstBuff              = (unsigned char *)malloc(currSize);
+
+    vImage_Buffer inbuff                = {srcBuff, height, width, bytesPerRow};
+    vImage_Buffer outbuff               = {dstBuff, width, height, bytesPerRowOut};
+
+    uint8_t bgColor[4]                  = {0, 0, 0, 0};
+
+    vImage_Error err                    = vImageRotate90_Planar16U(&inbuff, &outbuff, rotationConstant, bgColor, 0);
+    if (err != kvImageNoError) NSLog(@"%ld", err);
+
+    CVPixelBufferUnlockBaseAddress(imageBuffer, 0);
+
+    CVPixelBufferRef rotatedBuffer      = NULL;
+    CVPixelBufferCreateWithBytes(NULL,
+                                 height,
+                                 width,
+                                 kCVPixelFormatType_32BGRA,
+                                 outbuff.data,
+                                 bytesPerRowOut,
+                                 freePixelBufferDataAfterRelease,
+                                 NULL,
+                                 NULL,
+                                 &rotatedBuffer);
+
+    return rotatedBuffer;
+}
+
+void freePixelBufferDataAfterRelease(void *releaseRefCon, const void *baseAddress)
+{
+    // Free the memory we malloced for the vImage rotation
+    free((void *)baseAddress);
+}
+
+
+// 根据旋转方向获取纹理坐标
++ (const GLfloat *)textureCoordinatesForRotation:(GPUImageRotationMode)rotationMode;
+{
+    static const GLfloat noRotationTextureCoordinates[] = {
+        0.0f, 0.0f,
+        1.0f, 0.0f,
+        0.0f, 1.0f,
+        1.0f, 1.0f,
+    };
+    
+    static const GLfloat rotateLeftTextureCoordinates[] = {
+        1.0f, 0.0f,
+        1.0f, 1.0f,
+        0.0f, 0.0f,
+        0.0f, 1.0f,
+    };
+    
+    static const GLfloat rotateRightTextureCoordinates[] = {
+        0.0f, 1.0f,
+        0.0f, 0.0f,
+        1.0f, 1.0f,
+        1.0f, 0.0f,
+    };
+    
+    static const GLfloat verticalFlipTextureCoordinates[] = {
+        0.0f, 1.0f,
+        1.0f, 1.0f,
+        0.0f,  0.0f,
+        1.0f,  0.0f,
+    };
+    
+    static const GLfloat horizontalFlipTextureCoordinates[] = {
+        1.0f, 0.0f,
+        0.0f, 0.0f,
+        1.0f,  1.0f,
+        0.0f,  1.0f,
+    };
+    
+    static const GLfloat rotateRightVerticalFlipTextureCoordinates[] = {
+        0.0f, 0.0f,
+        0.0f, 1.0f,
+        1.0f, 0.0f,
+        1.0f, 1.0f,
+    };
+
+    static const GLfloat rotateRightHorizontalFlipTextureCoordinates[] = {
+        1.0f, 1.0f,
+        1.0f, 0.0f,
+        0.0f, 1.0f,
+        0.0f, 0.0f,
+    };
+
+    static const GLfloat rotate180TextureCoordinates[] = {
+        1.0f, 1.0f,
+        0.0f, 1.0f,
+        1.0f, 0.0f,
+        0.0f, 0.0f,
+    };
+
+    switch(rotationMode)
+    {
+        case kGPUImageNoRotation: return noRotationTextureCoordinates;
+        case kGPUImageRotateLeft: return rotateLeftTextureCoordinates;
+        case kGPUImageRotateRight: return rotateRightTextureCoordinates;
+        case kGPUImageFlipVertical: return verticalFlipTextureCoordinates;
+        case kGPUImageFlipHorizonal: return horizontalFlipTextureCoordinates;
+        case kGPUImageRotateRightFlipVertical: return rotateRightVerticalFlipTextureCoordinates;
+        case kGPUImageRotateRightFlipHorizontal: return rotateRightHorizontalFlipTextureCoordinates;
+        case kGPUImageRotate180: return rotate180TextureCoordinates;
+    }
+}
+
+@end

+ 106 - 0
BFFramework/Classes/PQGPUImage/Source/OpenGLContext_Shared.swift

@@ -0,0 +1,106 @@
+
+#if os(Linux)
+    #if GLES
+        import COpenGLES.gles2
+    #else
+        import COpenGL
+    #endif
+#else
+    #if GLES
+        import OpenGLES
+    #else
+        import OpenGL.GL3
+    #endif
+#endif
+
+import Foundation
+
+public let sharedImageProcessingContext = OpenGLContext()
+
+public extension OpenGLContext {
+    func programForVertexShader(_ vertexShader: String, fragmentShader: String) throws -> ShaderProgram {
+        let lookupKeyForShaderProgram = "V: \(vertexShader) - F: \(fragmentShader)"
+        if let shaderFromCache = shaderCache[lookupKeyForShaderProgram] {
+            return shaderFromCache
+        } else {
+            return try runOperationSynchronously {
+                let program = try ShaderProgram(vertexShader: vertexShader, fragmentShader: fragmentShader)
+                self.shaderCache[lookupKeyForShaderProgram] = program
+                return program
+            }
+        }
+    }
+
+    func programForVertexShader(_ vertexShader: String, fragmentShader: URL) throws -> ShaderProgram {
+        return try programForVertexShader(vertexShader, fragmentShader: try shaderFromFile(fragmentShader))
+    }
+
+    func programForVertexShader(_ vertexShader: URL, fragmentShader: URL) throws -> ShaderProgram {
+        return try programForVertexShader(try shaderFromFile(vertexShader), fragmentShader: try shaderFromFile(fragmentShader))
+    }
+
+    func openGLDeviceSettingForOption(_ option: Int32) -> GLint {
+        return runOperationSynchronously { () -> GLint in
+            self.makeCurrentContext()
+            var openGLValue: GLint = 0
+            glGetIntegerv(GLenum(option), &openGLValue)
+            return openGLValue
+        }
+    }
+
+    func deviceSupportsExtension(_ openGLExtension: String) -> Bool {
+        #if os(Linux)
+            return false
+        #else
+            return extensionString.contains(openGLExtension)
+        #endif
+    }
+
+    // http://www.khronos.org/registry/gles/extensions/EXT/EXT_texture_rg.txt
+
+    func deviceSupportsRedTextures() -> Bool {
+        return deviceSupportsExtension("GL_EXT_texture_rg")
+    }
+
+    func deviceSupportsFramebufferReads() -> Bool {
+        return deviceSupportsExtension("GL_EXT_shader_framebuffer_fetch")
+    }
+
+    func sizeThatFitsWithinATextureForSize(_ size: Size) -> Size {
+        let maxTextureSize = Float(maximumTextureSizeForThisDevice)
+        if size.width < maxTextureSize, size.height < maxTextureSize {
+            return size
+        }
+
+        let adjustedSize: Size
+        if size.width > size.height {
+            adjustedSize = Size(width: maxTextureSize, height: (maxTextureSize / size.width) * size.height)
+        } else {
+            adjustedSize = Size(width: (maxTextureSize / size.height) * size.width, height: maxTextureSize)
+        }
+
+        return adjustedSize
+    }
+
+    internal func generateTextureVBOs() {
+        textureVBOs[.noRotation] = generateVBO(for: Rotation.noRotation.textureCoordinates())
+        textureVBOs[.rotateCounterclockwise] = generateVBO(for: Rotation.rotateCounterclockwise.textureCoordinates())
+        textureVBOs[.rotateClockwise] = generateVBO(for: Rotation.rotateClockwise.textureCoordinates())
+        textureVBOs[.rotate180] = generateVBO(for: Rotation.rotate180.textureCoordinates())
+        textureVBOs[.flipHorizontally] = generateVBO(for: Rotation.flipHorizontally.textureCoordinates())
+        textureVBOs[.flipVertically] = generateVBO(for: Rotation.flipVertically.textureCoordinates())
+        textureVBOs[.rotateClockwiseAndFlipVertically] = generateVBO(for: Rotation.rotateClockwiseAndFlipVertically.textureCoordinates())
+        textureVBOs[.rotateClockwiseAndFlipHorizontally] = generateVBO(for: Rotation.rotateClockwiseAndFlipHorizontally.textureCoordinates())
+    }
+
+    func textureVBO(for rotation: Rotation) -> GLuint {
+        guard let textureVBO = textureVBOs[rotation] else { fatalError("GPUImage doesn't have a texture VBO set for the rotation \(rotation)") }
+        return textureVBO
+    }
+}
+
+@_semantics("sil.optimize.never") public func debugPrint(_ stringToPrint: String, file: StaticString = #file, line: UInt = #line, function: StaticString = #function) {
+    #if DEBUG
+        print("[GPUImage2] \(stringToPrint) --> \((String(describing: file) as NSString).lastPathComponent): \(function): \(line)")
+    #endif
+}

+ 281 - 0
BFFramework/Classes/PQGPUImage/Source/OpenGLRendering.swift

@@ -0,0 +1,281 @@
+#if os(Linux)
+    #if GLES
+        import COpenGLES.gles2
+        let GL_DEPTH24_STENCIL8 = GL_DEPTH24_STENCIL8_OES
+        let GL_TRUE = GLboolean(1)
+        let GL_FALSE = GLboolean(0)
+    #else
+        import COpenGL
+    #endif
+#else
+    #if GLES
+        import OpenGLES
+    #else
+        import OpenGL.GL3
+    #endif
+#endif
+
+import Foundation
+
+public enum InputTextureStorageFormat {
+    case textureCoordinates([GLfloat])
+    case textureVBO(GLuint)
+}
+
+public struct InputTextureProperties {
+    public let textureStorage: InputTextureStorageFormat
+    public let texture: GLuint
+
+    public init(textureCoordinates: [GLfloat]? = nil, textureVBO: GLuint? = nil, texture: GLuint) {
+        self.texture = texture
+        switch (textureCoordinates, textureVBO) {
+        case let (.some(coordinates), .none): self.textureStorage = .textureCoordinates(coordinates)
+        case let (.none, .some(vbo)): textureStorage = .textureVBO(vbo)
+        case (.none, .none): fatalError("Need to specify either texture coordinates or a VBO to InputTextureProperties")
+        case (.some, .some): fatalError("Can't specify both texture coordinates and a VBO to InputTextureProperties")
+        }
+    }
+}
+
+public struct GLSize {
+    public let width: GLint
+    public let height: GLint
+
+    public init(width: GLint, height: GLint) {
+        self.width = width
+        self.height = height
+    }
+
+    public init(_ size: Size) {
+        width = size.glWidth()
+        height = size.glHeight()
+    }
+}
+
+extension Size {
+    init(_ size: GLSize) {
+        width = Float(size.width)
+        height = Float(size.height)
+    }
+}
+
+public let standardImageVertices: [GLfloat] = [-1.0, -1.0, 1.0, -1.0, -1.0, 1.0, 1.0, 1.0]
+public let verticallyInvertedImageVertices: [GLfloat] = [-1.0, 1.0, 1.0, 1.0, -1.0, -1.0, 1.0, -1.0]
+
+// "position" and "inputTextureCoordinate", "inputTextureCoordinate2" attribute naming follows the convention of the old GPUImage
+public func renderQuadWithShader(_ shader: ShaderProgram, uniformSettings: ShaderUniformSettings? = nil, vertices: [GLfloat]? = nil, vertexBufferObject: GLuint? = nil, inputTextures: [InputTextureProperties], context: OpenGLContext = sharedImageProcessingContext) {
+    switch (vertices, vertexBufferObject) {
+    case (.none, .some): break
+    case (.some, .none): break
+    case (.some, .some): fatalError("Can't specify both vertices and a VBO in renderQuadWithShader()")
+    case (.none, .none): fatalError("Can't specify both vertices and a VBO in renderQuadWithShader()")
+    }
+
+    context.makeCurrentContext()
+    shader.use()
+    uniformSettings?.restoreShaderSettings(shader)
+
+    guard let positionAttribute = shader.attributeIndex("position") else { fatalError("A position attribute was missing from the shader program during rendering.") }
+
+    if let boundVBO = vertexBufferObject {
+        glBindBuffer(GLenum(GL_ARRAY_BUFFER), boundVBO)
+        glVertexAttribPointer(positionAttribute, 2, GLenum(GL_FLOAT), 0, 0, nil)
+        glBindBuffer(GLenum(GL_ARRAY_BUFFER), 0)
+    } else {
+        glVertexAttribPointer(positionAttribute, 2, GLenum(GL_FLOAT), 0, 0, vertices!)
+    }
+
+    for (index, inputTexture) in inputTextures.enumerated() {
+        if let textureCoordinateAttribute = shader.attributeIndex("inputTextureCoordinate".withNonZeroSuffix(index)) {
+            switch inputTexture.textureStorage {
+            case let .textureCoordinates(textureCoordinates):
+                glVertexAttribPointer(textureCoordinateAttribute, 2, GLenum(GL_FLOAT), 0, 0, textureCoordinates)
+            case let .textureVBO(textureVBO):
+                glBindBuffer(GLenum(GL_ARRAY_BUFFER), textureVBO)
+                glVertexAttribPointer(textureCoordinateAttribute, 2, GLenum(GL_FLOAT), 0, 0, nil)
+                glBindBuffer(GLenum(GL_ARRAY_BUFFER), 0)
+            }
+        } else if index == 0 {
+            fatalError("The required attribute named inputTextureCoordinate was missing from the shader program during rendering.")
+        }
+
+        glActiveTexture(textureUnitForIndex(index))
+        glBindTexture(GLenum(GL_TEXTURE_2D), inputTexture.texture)
+
+        shader.setValue(GLint(index), forUniform: "inputImageTexture".withNonZeroSuffix(index))
+    }
+
+    glDrawArrays(GLenum(GL_TRIANGLE_STRIP), 0, 4)
+
+    if vertexBufferObject != nil {
+        glBindBuffer(GLenum(GL_ARRAY_BUFFER), 0)
+    }
+
+    for (index, _) in inputTextures.enumerated() {
+        glActiveTexture(textureUnitForIndex(index))
+        glBindTexture(GLenum(GL_TEXTURE_2D), 0)
+    }
+}
+
+public func clearFramebufferWithColor(_ color: Color) {
+    glClearColor(GLfloat(color.redComponent), GLfloat(color.greenComponent), GLfloat(color.blueComponent), GLfloat(color.alphaComponent))
+    glClear(GLenum(GL_COLOR_BUFFER_BIT))
+}
+
+func renderStencilMaskFromFramebuffer(_ framebuffer: Framebuffer) {
+    let inputTextureProperties = framebuffer.texturePropertiesForOutputRotation(.noRotation)
+    glEnable(GLenum(GL_STENCIL_TEST))
+    glClearStencil(0)
+    glClear(GLenum(GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT))
+    glColorMask(GLboolean(GL_FALSE), GLboolean(GL_FALSE), GLboolean(GL_FALSE), GLboolean(GL_FALSE))
+    glStencilFunc(GLenum(GL_ALWAYS), 1, 1)
+    glStencilOp(GLenum(GL_KEEP), GLenum(GL_KEEP), GLenum(GL_REPLACE))
+
+    #if GL
+        glEnable(GLenum(GL_ALPHA_TEST))
+        glAlphaFunc(GLenum(GL_NOTEQUAL), 0.0)
+        renderQuadWithShader(sharedImageProcessingContext.passthroughShader, vertices: standardImageVertices, inputTextures: [inputTextureProperties])
+    #else
+        let alphaTestShader = crashOnShaderCompileFailure("Stencil") { try sharedImageProcessingContext.programForVertexShader(OneInputVertexShader, fragmentShader: AlphaTestFragmentShader) }
+        renderQuadWithShader(alphaTestShader, vertices: standardImageVertices, inputTextures: [inputTextureProperties])
+    #endif
+
+    glColorMask(GLboolean(GL_TRUE), GLboolean(GL_TRUE), GLboolean(GL_TRUE), GLboolean(GL_TRUE))
+
+    glStencilFunc(GLenum(GL_EQUAL), 1, 1)
+    glStencilOp(GLenum(GL_KEEP), GLenum(GL_KEEP), GLenum(GL_KEEP))
+
+    #if GL
+        glDisable(GLenum(GL_ALPHA_TEST))
+    #endif
+}
+
+func disableStencil() {
+    glDisable(GLenum(GL_STENCIL_TEST))
+}
+
+func textureUnitForIndex(_ index: Int) -> GLenum {
+    switch index {
+    case 0: return GLenum(GL_TEXTURE0)
+    case 1: return GLenum(GL_TEXTURE1)
+    case 2: return GLenum(GL_TEXTURE2)
+    case 3: return GLenum(GL_TEXTURE3)
+    case 4: return GLenum(GL_TEXTURE4)
+    case 5: return GLenum(GL_TEXTURE5)
+    case 6: return GLenum(GL_TEXTURE6)
+    case 7: return GLenum(GL_TEXTURE7)
+    case 8: return GLenum(GL_TEXTURE8)
+    default: fatalError("Attempted to address too high a texture unit")
+    }
+}
+
+public func generateTexture(minFilter: Int32, magFilter: Int32, wrapS: Int32, wrapT: Int32) -> GLuint {
+    var texture: GLuint = 0
+
+    glActiveTexture(GLenum(GL_TEXTURE1))
+    glGenTextures(1, &texture)
+    glBindTexture(GLenum(GL_TEXTURE_2D), texture)
+    glTexParameteri(GLenum(GL_TEXTURE_2D), GLenum(GL_TEXTURE_MIN_FILTER), minFilter)
+    glTexParameteri(GLenum(GL_TEXTURE_2D), GLenum(GL_TEXTURE_MAG_FILTER), magFilter)
+    glTexParameteri(GLenum(GL_TEXTURE_2D), GLenum(GL_TEXTURE_WRAP_S), wrapS)
+    glTexParameteri(GLenum(GL_TEXTURE_2D), GLenum(GL_TEXTURE_WRAP_T), wrapT)
+
+    glBindTexture(GLenum(GL_TEXTURE_2D), 0)
+
+    return texture
+}
+
+public func uploadLocalArray(data: [GLfloat], into texture: GLuint, size: GLSize) {
+    glActiveTexture(GLenum(GL_TEXTURE1))
+    glBindTexture(GLenum(GL_TEXTURE_2D), texture)
+    glTexImage2D(GLenum(GL_TEXTURE_2D), 0, GL_RGBA, size.width, size.height, 0, GLenum(GL_RGBA), GLenum(GL_FLOAT), data)
+    glBindTexture(GLenum(GL_TEXTURE_2D), 0)
+}
+
+func generateFramebufferForTexture(_ texture: GLuint, width: GLint, height: GLint, internalFormat: Int32, format: Int32, type: Int32, stencil: Bool) throws -> (GLuint, GLuint?) {
+    var framebuffer: GLuint = 0
+    glActiveTexture(GLenum(GL_TEXTURE1))
+
+    glGenFramebuffers(1, &framebuffer)
+    glBindFramebuffer(GLenum(GL_FRAMEBUFFER), framebuffer)
+    glBindTexture(GLenum(GL_TEXTURE_2D), texture)
+
+    glTexImage2D(GLenum(GL_TEXTURE_2D), 0, internalFormat, width, height, 0, GLenum(format), GLenum(type), nil)
+    glFramebufferTexture2D(GLenum(GL_FRAMEBUFFER), GLenum(GL_COLOR_ATTACHMENT0), GLenum(GL_TEXTURE_2D), texture, 0)
+
+    let status = glCheckFramebufferStatus(GLenum(GL_FRAMEBUFFER))
+    if status != GLenum(GL_FRAMEBUFFER_COMPLETE) {
+        throw FramebufferCreationError(errorCode: status)
+    }
+
+    let stencilBuffer: GLuint?
+    if stencil {
+        stencilBuffer = try attachStencilBuffer(width: width, height: height)
+    } else {
+        stencilBuffer = nil
+    }
+
+    glBindTexture(GLenum(GL_TEXTURE_2D), 0)
+    glBindFramebuffer(GLenum(GL_FRAMEBUFFER), 0)
+    return (framebuffer, stencilBuffer)
+}
+
+func attachStencilBuffer(width: GLint, height: GLint) throws -> GLuint {
+    var stencilBuffer: GLuint = 0
+    glGenRenderbuffers(1, &stencilBuffer)
+    glBindRenderbuffer(GLenum(GL_RENDERBUFFER), stencilBuffer)
+    glRenderbufferStorage(GLenum(GL_RENDERBUFFER), GLenum(GL_DEPTH24_STENCIL8), width, height) // iOS seems to only support combination depth + stencil, from references
+    #if os(iOS)
+        glFramebufferRenderbuffer(GLenum(GL_FRAMEBUFFER), GLenum(GL_DEPTH_ATTACHMENT), GLenum(GL_RENDERBUFFER), stencilBuffer)
+    #endif
+    glFramebufferRenderbuffer(GLenum(GL_FRAMEBUFFER), GLenum(GL_STENCIL_ATTACHMENT), GLenum(GL_RENDERBUFFER), stencilBuffer)
+
+    glBindRenderbuffer(GLenum(GL_RENDERBUFFER), 0)
+
+    let status = glCheckFramebufferStatus(GLenum(GL_FRAMEBUFFER))
+    if status != GLenum(GL_FRAMEBUFFER_COMPLETE) {
+        throw FramebufferCreationError(errorCode: status)
+    }
+
+    return stencilBuffer
+}
+
+public func enableAdditiveBlending() {
+    glBlendEquation(GLenum(GL_FUNC_ADD))
+    glBlendFunc(GLenum(GL_ONE), GLenum(GL_ONE))
+    glEnable(GLenum(GL_BLEND))
+}
+
+public func disableBlending() {
+    glDisable(GLenum(GL_BLEND))
+}
+
+public func generateVBO(for vertices: [GLfloat]) -> GLuint {
+    var newBuffer: GLuint = 0
+    glGenBuffers(1, &newBuffer)
+    glBindBuffer(GLenum(GL_ARRAY_BUFFER), newBuffer)
+    glBufferData(GLenum(GL_ARRAY_BUFFER), MemoryLayout<GLfloat>.size * vertices.count, vertices, GLenum(GL_STATIC_DRAW))
+    glBindBuffer(GLenum(GL_ARRAY_BUFFER), 0)
+    return newBuffer
+}
+
+public func deleteVBO(_ vbo: GLuint) {
+    var deletedVBO = vbo
+    glDeleteBuffers(1, &deletedVBO)
+}
+
+extension String {
+    func withNonZeroSuffix(_ suffix: Int) -> String {
+        if suffix == 0 {
+            return self
+        } else {
+            return "\(self)\(suffix + 1)"
+        }
+    }
+
+    func withGLChar(_ operation: (UnsafePointer<GLchar>) -> Void) {
+        withCString { pointer in
+            operation(UnsafePointer<GLchar>(pointer))
+        }
+    }
+}

+ 26 - 0
BFFramework/Classes/PQGPUImage/Source/OperationGroup.swift

@@ -0,0 +1,26 @@
+open class OperationGroup: ImageProcessingOperation {
+    public let inputImageRelay = ImageRelay()
+    public let outputImageRelay = ImageRelay()
+
+    public var sources: SourceContainer { return inputImageRelay.sources }
+    public var targets: TargetContainer { return outputImageRelay.targets }
+    public let maximumInputs: UInt = 1
+
+    public private(set) var userInfo: [AnyHashable: Any]?
+
+    public init() {}
+
+    public func newFramebufferAvailable(_ framebuffer: Framebuffer, fromSourceIndex: UInt) {
+        userInfo = framebuffer.userInfo
+
+        inputImageRelay.newFramebufferAvailable(framebuffer, fromSourceIndex: fromSourceIndex)
+    }
+
+    public func configureGroup(_ configurationOperation: (_ input: ImageRelay, _ output: ImageRelay) -> Void) {
+        configurationOperation(inputImageRelay, outputImageRelay)
+    }
+
+    public func transmitPreviousImage(to target: ImageConsumer, atIndex: UInt) {
+        outputImageRelay.transmitPreviousImage(to: target, atIndex: atIndex)
+    }
+}

+ 17 - 0
BFFramework/Classes/PQGPUImage/Source/Operations/AdaptiveThreshold.swift

@@ -0,0 +1,17 @@
+public class AdaptiveThreshold: OperationGroup {
+    public var blurRadiusInPixels: Float { didSet { boxBlur.blurRadiusInPixels = blurRadiusInPixels } }
+
+    let luminance = Luminance()
+    let boxBlur = BoxBlur()
+    let adaptiveThreshold = BasicOperation(fragmentShader: AdaptiveThresholdFragmentShader, numberOfInputs: 2)
+
+    override public init() {
+        blurRadiusInPixels = 4.0
+        super.init()
+
+        configureGroup { input, output in
+            input --> self.luminance --> self.boxBlur --> self.adaptiveThreshold --> output
+            self.luminance --> self.adaptiveThreshold
+        }
+    }
+}

+ 5 - 0
BFFramework/Classes/PQGPUImage/Source/Operations/AddBlend.swift

@@ -0,0 +1,5 @@
+public class AddBlend: BasicOperation {
+    public init() {
+        super.init(fragmentShader: AddBlendFragmentShader, numberOfInputs: 2)
+    }
+}

+ 9 - 0
BFFramework/Classes/PQGPUImage/Source/Operations/AlphaBlend.swift

@@ -0,0 +1,9 @@
+public class AlphaBlend: BasicOperation {
+    public var mix: Float = 0.5 { didSet { uniformSettings["mixturePercent"] = mix } }
+
+    public init() {
+        super.init(fragmentShader: AlphaBlendFragmentShader, numberOfInputs: 2)
+
+        ({ mix = 0.5 })()
+    }
+}

+ 22 - 0
BFFramework/Classes/PQGPUImage/Source/Operations/AmatorkaFilter.swift

@@ -0,0 +1,22 @@
+/** A photo filter based on Photoshop action by Amatorka
+ http://amatorka.deviantart.com/art/Amatorka-Action-2-121069631
+ */
+
+// Note: If you want to use this effect you have to add lookup_amatorka.png
+//       from Resources folder to your application bundle.
+
+#if !os(Linux)
+
+    public class AmatorkaFilter: LookupFilter {
+        override public init() {
+            super.init()
+
+            do {
+                try ({ lookupImage = try PictureInput(imageName: "lookup_amatorka.png") })()
+            } catch {
+                print("ERROR: Unable to create PictureInput \(error)")
+            }
+            ({ intensity = 1.0 })()
+        }
+    }
+#endif

+ 74 - 0
BFFramework/Classes/PQGPUImage/Source/Operations/AverageColorExtractor.swift

@@ -0,0 +1,74 @@
+#if os(Linux)
+    import Glibc
+    #if GLES
+        import COpenGLES.gles2
+    #else
+        import COpenGL
+    #endif
+#else
+    #if GLES
+        import OpenGLES
+    #else
+        import OpenGL.GL3
+    #endif
+#endif
+
+import Foundation
+
+public class AverageColorExtractor: BasicOperation {
+    public var extractedColorCallback: ((Color) -> Void)?
+
+    public init() {
+        super.init(vertexShader: AverageColorVertexShader, fragmentShader: AverageColorFragmentShader)
+    }
+
+    override open func renderFrame() {
+        averageColorBySequentialReduction(inputFramebuffer: inputFramebuffers[0]!, shader: shader, extractAverageOperation: extractAverageColorFromFramebuffer)
+        releaseIncomingFramebuffers()
+    }
+
+    func extractAverageColorFromFramebuffer(_ framebuffer: Framebuffer) {
+        var data = [UInt8](repeating: 0, count: Int(framebuffer.size.width * framebuffer.size.height * 4))
+        glReadPixels(0, 0, framebuffer.size.width, framebuffer.size.height, GLenum(GL_RGBA), GLenum(GL_UNSIGNED_BYTE), &data)
+        renderFramebuffer = framebuffer
+        framebuffer.resetRetainCount()
+
+        let totalNumberOfPixels = Int(framebuffer.size.width * framebuffer.size.height)
+
+        var redTotal = 0, greenTotal = 0, blueTotal = 0, alphaTotal = 0
+        for currentPixel in 0..<totalNumberOfPixels {
+            redTotal += Int(data[currentPixel * 4])
+            greenTotal += Int(data[(currentPixel * 4) + 1])
+            blueTotal += Int(data[(currentPixel * 4) + 2])
+            alphaTotal += Int(data[(currentPixel * 4) + 3])
+        }
+
+        let returnColor = Color(red: Float(redTotal) / Float(totalNumberOfPixels) / 255.0, green: Float(greenTotal) / Float(totalNumberOfPixels) / 255.0, blue: Float(blueTotal) / Float(totalNumberOfPixels) / 255.0, alpha: Float(alphaTotal) / Float(totalNumberOfPixels) / 255.0)
+
+        extractedColorCallback?(returnColor)
+    }
+}
+
+func averageColorBySequentialReduction(inputFramebuffer: Framebuffer, shader: ShaderProgram, extractAverageOperation: (Framebuffer) -> Void) {
+    let uniformSettings = ShaderUniformSettings()
+    let inputSize = Size(inputFramebuffer.size)
+    let numberOfReductionsInX = floor(log(Double(inputSize.width)) / log(4.0))
+    let numberOfReductionsInY = floor(log(Double(inputSize.height)) / log(4.0))
+    let reductionsToHitSideLimit = Int(floor(min(numberOfReductionsInX, numberOfReductionsInY)))
+    inputFramebuffer.lock()
+    var previousFramebuffer = inputFramebuffer
+    for currentReduction in 0..<reductionsToHitSideLimit {
+        let currentStageSize = Size(width: Float(floor(Double(inputSize.width) / pow(4.0, Double(currentReduction) + 1.0))), height: Float(floor(Double(inputSize.height) / pow(4.0, Double(currentReduction) + 1.0))))
+        let currentFramebuffer = sharedImageProcessingContext.framebufferCache.requestFramebufferWithProperties(orientation: previousFramebuffer.orientation, size: GLSize(currentStageSize))
+        currentFramebuffer.lock()
+        uniformSettings["texelWidth"] = 0.25 / currentStageSize.width
+        uniformSettings["texelHeight"] = 0.25 / currentStageSize.height
+
+        currentFramebuffer.activateFramebufferForRendering()
+        renderQuadWithShader(shader, uniformSettings: uniformSettings, vertexBufferObject: sharedImageProcessingContext.standardImageVBO, inputTextures: [previousFramebuffer.texturePropertiesForTargetOrientation(currentFramebuffer.orientation)])
+        previousFramebuffer.unlock()
+        previousFramebuffer = currentFramebuffer
+    }
+
+    extractAverageOperation(previousFramebuffer)
+}

+ 49 - 0
BFFramework/Classes/PQGPUImage/Source/Operations/AverageLuminanceExtractor.swift

@@ -0,0 +1,49 @@
+#if os(Linux)
+    #if GLES
+        import COpenGLES.gles2
+    #else
+        import COpenGL
+    #endif
+#else
+    #if GLES
+        import OpenGLES
+    #else
+        import OpenGL.GL3
+    #endif
+#endif
+
+public class AverageLuminanceExtractor: BasicOperation {
+    public var extractedLuminanceCallback: ((Float) -> Void)?
+
+    public init() {
+        super.init(vertexShader: AverageColorVertexShader, fragmentShader: AverageLuminanceFragmentShader)
+    }
+
+    override open func renderFrame() {
+        // Reduce to luminance before passing into the downsampling
+        // TODO: Combine this with the first stage of the downsampling by doing reduction here
+        let luminancePassShader = crashOnShaderCompileFailure("AverageLuminance") { try sharedImageProcessingContext.programForVertexShader(defaultVertexShaderForInputs(1), fragmentShader: LuminanceFragmentShader) }
+        let luminancePassFramebuffer = sharedImageProcessingContext.framebufferCache.requestFramebufferWithProperties(orientation: inputFramebuffers[0]!.orientation, size: inputFramebuffers[0]!.size)
+        luminancePassFramebuffer.activateFramebufferForRendering()
+        renderQuadWithShader(luminancePassShader, vertexBufferObject: sharedImageProcessingContext.standardImageVBO, inputTextures: [inputFramebuffers[0]!.texturePropertiesForTargetOrientation(luminancePassFramebuffer.orientation)])
+
+        averageColorBySequentialReduction(inputFramebuffer: luminancePassFramebuffer, shader: shader, extractAverageOperation: extractAverageLuminanceFromFramebuffer)
+        releaseIncomingFramebuffers()
+    }
+
+    func extractAverageLuminanceFromFramebuffer(_ framebuffer: Framebuffer) {
+        var data = [UInt8](repeating: 0, count: Int(framebuffer.size.width * framebuffer.size.height * 4))
+        glReadPixels(0, 0, framebuffer.size.width, framebuffer.size.height, GLenum(GL_BGRA), GLenum(GL_UNSIGNED_BYTE), &data)
+        renderFramebuffer = framebuffer
+        framebuffer.resetRetainCount()
+
+        let totalNumberOfPixels = Int(framebuffer.size.width * framebuffer.size.height)
+
+        var redTotal = 0
+        for currentPixel in 0..<totalNumberOfPixels {
+            redTotal += Int(data[currentPixel * 4])
+        }
+
+        extractedLuminanceCallback?(Float(redTotal) / Float(totalNumberOfPixels) / 255.0)
+    }
+}

+ 19 - 0
BFFramework/Classes/PQGPUImage/Source/Operations/AverageLuminanceThreshold.swift

@@ -0,0 +1,19 @@
+public class AverageLuminanceThreshold: OperationGroup {
+    public var thresholdMultiplier: Float = 1.0
+
+    let averageLuminance = AverageLuminanceExtractor()
+    let luminanceThreshold = LuminanceThreshold()
+
+    override public init() {
+        super.init()
+
+        averageLuminance.extractedLuminanceCallback = { [weak self] luminance in
+            self?.luminanceThreshold.threshold = (self?.thresholdMultiplier ?? 1.0) * luminance
+        }
+
+        configureGroup { input, output in
+            input --> self.averageLuminance
+            input --> self.luminanceThreshold --> output
+        }
+    }
+}

+ 11 - 0
BFFramework/Classes/PQGPUImage/Source/Operations/BilateralBlur.swift

@@ -0,0 +1,11 @@
+// TODO: auto-generate shaders for this, per the Gaussian blur method
+
+public class BilateralBlur: TwoStageOperation {
+    public var distanceNormalizationFactor: Float = 8.0 { didSet { uniformSettings["distanceNormalizationFactor"] = distanceNormalizationFactor } }
+
+    public init() {
+        super.init(vertexShader: BilateralBlurVertexShader, fragmentShader: BilateralBlurFragmentShader)
+
+        ({ distanceNormalizationFactor = 1.0 })()
+    }
+}

+ 82 - 0
BFFramework/Classes/PQGPUImage/Source/Operations/BoxBlur.swift

@@ -0,0 +1,82 @@
+#if os(Linux)
+    import Glibc
+#endif
+
+import Foundation
+
+public class BoxBlur: TwoStageOperation {
+    public var blurRadiusInPixels: Float {
+        didSet {
+            let (sigma, downsamplingFactor) = sigmaAndDownsamplingForBlurRadius(blurRadiusInPixels, limit: 8.0, override: overrideDownsamplingOptimization)
+            sharedImageProcessingContext.runOperationAsynchronously {
+                self.downsamplingFactor = downsamplingFactor
+                let pixelRadius = pixelRadiusForBlurSigma(Double(sigma))
+                self.shader = crashOnShaderCompileFailure("BoxBlur") { try sharedImageProcessingContext.programForVertexShader(vertexShaderForOptimizedBoxBlurOfRadius(pixelRadius), fragmentShader: fragmentShaderForOptimizedBoxBlurOfRadius(pixelRadius)) }
+            }
+        }
+    }
+
+    public init() {
+        blurRadiusInPixels = 2.0
+        let pixelRadius = UInt(round(round(Double(blurRadiusInPixels) / 2.0) * 2.0))
+        let initialShader = crashOnShaderCompileFailure("BoxBlur") { try sharedImageProcessingContext.programForVertexShader(vertexShaderForOptimizedBoxBlurOfRadius(pixelRadius), fragmentShader: fragmentShaderForOptimizedBoxBlurOfRadius(pixelRadius)) }
+        super.init(shader: initialShader, numberOfInputs: 1)
+    }
+}
+
+func vertexShaderForOptimizedBoxBlurOfRadius(_ radius: UInt) -> String {
+    guard radius > 0 else { return OneInputVertexShader }
+
+    let numberOfOptimizedOffsets = min(radius / 2 + (radius % 2), 7)
+    var shaderString = "attribute vec4 position;\n attribute vec4 inputTextureCoordinate;\n \n uniform float texelWidth;\n uniform float texelHeight;\n \n varying vec2 blurCoordinates[\(1 + (numberOfOptimizedOffsets * 2))];\n \n void main()\n {\n gl_Position = position;\n \n vec2 singleStepOffset = vec2(texelWidth, texelHeight);\n"
+    shaderString += "blurCoordinates[0] = inputTextureCoordinate.xy;\n"
+    for currentOptimizedOffset in 0..<numberOfOptimizedOffsets {
+        let optimizedOffset = Float(currentOptimizedOffset * 2) + 1.5
+        shaderString += "blurCoordinates[\((currentOptimizedOffset * 2) + 1)] = inputTextureCoordinate.xy - singleStepOffset * \(optimizedOffset);\n"
+        shaderString += "blurCoordinates[\((currentOptimizedOffset * 2) + 2)] = inputTextureCoordinate.xy + singleStepOffset * \(optimizedOffset);\n"
+    }
+
+    shaderString += "}\n"
+    return shaderString
+}
+
+func fragmentShaderForOptimizedBoxBlurOfRadius(_ radius: UInt) -> String {
+    guard radius > 0 else { return PassthroughFragmentShader }
+
+    let numberOfOptimizedOffsets = min(radius / 2 + (radius % 2), 7)
+    let trueNumberOfOptimizedOffsets = radius / 2 + (radius % 2)
+
+    // Header
+    #if GLES
+        var shaderString = "uniform sampler2D inputImageTexture;\n uniform highp float texelWidth;\n uniform highp float texelHeight;\n \n varying highp vec2 blurCoordinates[\(1 + (numberOfOptimizedOffsets * 2))];\n \n void main()\n {\n lowp vec4 sum = vec4(0.0);\n"
+    #else
+        var shaderString = "uniform sampler2D inputImageTexture;\n uniform float texelWidth;\n uniform float texelHeight;\n \n varying vec2 blurCoordinates[\(1 + (numberOfOptimizedOffsets * 2))];\n \n void main()\n {\n vec4 sum = vec4(0.0);\n"
+    #endif
+
+    // Inner texture loop
+    let boxWeight = 1.0 / Float((radius * 2) + 1)
+    shaderString += "sum += texture2D(inputImageTexture, blurCoordinates[0]) * \(boxWeight);\n"
+    for currentBlurCoordinateIndex in 0..<numberOfOptimizedOffsets {
+        shaderString += "sum += texture2D(inputImageTexture, blurCoordinates[\((currentBlurCoordinateIndex * 2) + 1)]) * \(boxWeight * 2.0);\n"
+        shaderString += "sum += texture2D(inputImageTexture, blurCoordinates[\((currentBlurCoordinateIndex * 2) + 2)]) * \(boxWeight * 2.0);\n"
+    }
+
+    // If the number of required samples exceeds the amount we can pass in via varyings, we have to do dependent texture reads in the fragment shader
+    if trueNumberOfOptimizedOffsets > numberOfOptimizedOffsets {
+        #if GLES
+            shaderString += "highp vec2 singleStepOffset = vec2(texelWidth, texelHeight);\n"
+        #else
+            shaderString += "vec2 singleStepOffset = vec2(texelWidth, texelHeight);\n"
+        #endif
+        for currentOverlowTextureRead in numberOfOptimizedOffsets..<trueNumberOfOptimizedOffsets {
+            let optimizedOffset = Float(currentOverlowTextureRead * 2) + 1.5
+
+            shaderString += "sum += texture2D(inputImageTexture, blurCoordinates[0] + singleStepOffset * \(optimizedOffset)) * \(boxWeight * 2.0);\n"
+            shaderString += "sum += texture2D(inputImageTexture, blurCoordinates[0] - singleStepOffset * \(optimizedOffset)) * \(boxWeight * 2.0);\n"
+        }
+    }
+
+    // Footer
+    shaderString += "gl_FragColor = sum;\n }\n"
+    return shaderString
+}

+ 9 - 0
BFFramework/Classes/PQGPUImage/Source/Operations/BrightnessAdjustment.swift

@@ -0,0 +1,9 @@
+public class BrightnessAdjustment: BasicOperation {
+    public var brightness: Float = 0.0 { didSet { uniformSettings["brightness"] = brightness } }
+
+    public init() {
+        super.init(fragmentShader: BrightnessFragmentShader, numberOfInputs: 1)
+
+        ({ brightness = 1.0 })()
+    }
+}

+ 13 - 0
BFFramework/Classes/PQGPUImage/Source/Operations/BulgeDistortion.swift

@@ -0,0 +1,13 @@
+public class BulgeDistortion: BasicOperation {
+    public var radius: Float = 0.25 { didSet { uniformSettings["radius"] = radius } }
+    public var scale: Float = 0.5 { didSet { uniformSettings["scale"] = scale } }
+    public var center: Position = Position.center { didSet { uniformSettings["center"] = center } }
+
+    public init() {
+        super.init(fragmentShader: BulgeDistortionFragmentShader, numberOfInputs: 1)
+
+        ({ radius = 0.25 })()
+        ({ scale = 0.5 })()
+        ({ center = Position.center })()
+    }
+}

+ 5 - 0
BFFramework/Classes/PQGPUImage/Source/Operations/CGAColorspaceFilter.swift

@@ -0,0 +1,5 @@
+public class CGAColorspaceFilter: BasicOperation {
+    public init() {
+        super.init(fragmentShader: CGAColorspaceFragmentShader, numberOfInputs: 1)
+    }
+}

+ 37 - 0
BFFramework/Classes/PQGPUImage/Source/Operations/CannyEdgeDetection.swift

@@ -0,0 +1,37 @@
+/** This applies the edge detection process described by John Canny in
+
+ Canny, J., A Computational Approach To Edge Detection, IEEE Trans. Pattern Analysis and Machine Intelligence, 8(6):679–698, 1986.
+
+ and implemented in OpenGL ES by
+
+ A. Ensor, S. Hall. GPU-based Image Analysis on Mobile Devices. Proceedings of Image and Vision Computing New Zealand 2011.
+
+ It starts with a conversion to luminance, followed by an accelerated 9-hit Gaussian blur. A Sobel operator is applied to obtain the overall
+ gradient strength in the blurred image, as well as the direction (in texture sampling steps) of the gradient. A non-maximum suppression filter
+ acts along the direction of the gradient, highlighting strong edges that pass the threshold and completely removing those that fail the lower
+ threshold. Finally, pixels from in-between these thresholds are either included in edges or rejected based on neighboring pixels.
+ */
+
+public class CannyEdgeDetection: OperationGroup {
+    public var blurRadiusInPixels: Float = 2.0 { didSet { gaussianBlur.blurRadiusInPixels = blurRadiusInPixels } }
+    public var upperThreshold: Float = 0.4 { didSet { directionalNonMaximumSuppression.uniformSettings["upperThreshold"] = upperThreshold } }
+    public var lowerThreshold: Float = 0.1 { didSet { directionalNonMaximumSuppression.uniformSettings["lowerThreshold"] = lowerThreshold } }
+
+    let luminance = Luminance()
+    let gaussianBlur = SingleComponentGaussianBlur()
+    let directionalSobel = TextureSamplingOperation(fragmentShader: DirectionalSobelEdgeDetectionFragmentShader)
+    let directionalNonMaximumSuppression = TextureSamplingOperation(vertexShader: OneInputVertexShader, fragmentShader: DirectionalNonMaximumSuppressionFragmentShader)
+    let weakPixelInclusion = TextureSamplingOperation(fragmentShader: WeakPixelInclusionFragmentShader)
+
+    override public init() {
+        super.init()
+
+        ({ blurRadiusInPixels = 2.0 })()
+        ({ upperThreshold = 0.4 })()
+        ({ lowerThreshold = 0.1 })()
+
+        configureGroup { input, output in
+            input --> self.luminance --> self.gaussianBlur --> self.directionalSobel --> self.directionalNonMaximumSuppression --> self.weakPixelInclusion --> output
+        }
+    }
+}

+ 13 - 0
BFFramework/Classes/PQGPUImage/Source/Operations/ChromaKeyBlend.swift

@@ -0,0 +1,13 @@
+public class ChromaKeyBlend: BasicOperation {
+    public var thresholdSensitivity: Float = 0.4 { didSet { uniformSettings["thresholdSensitivity"] = thresholdSensitivity } }
+    public var smoothing: Float = 0.1 { didSet { uniformSettings["smoothing"] = smoothing } }
+    public var colorToReplace: Color = Color.green { didSet { uniformSettings["colorToReplace"] = colorToReplace } }
+
+    public init() {
+        super.init(fragmentShader: ChromaKeyBlendFragmentShader, numberOfInputs: 2)
+
+        ({ thresholdSensitivity = 0.4 })()
+        ({ smoothing = 0.1 })()
+        ({ colorToReplace = Color.green })()
+    }
+}

+ 13 - 0
BFFramework/Classes/PQGPUImage/Source/Operations/ChromaKeying.swift

@@ -0,0 +1,13 @@
+public class ChromaKeying: BasicOperation {
+    public var thresholdSensitivity: Float = 0.4 { didSet { uniformSettings["thresholdSensitivity"] = thresholdSensitivity } }
+    public var smoothing: Float = 0.1 { didSet { uniformSettings["smoothing"] = smoothing } }
+    public var colorToReplace: Color = Color.green { didSet { uniformSettings["colorToReplace"] = colorToReplace } }
+
+    public init() {
+        super.init(fragmentShader: ChromaKeyFragmentShader, numberOfInputs: 1)
+
+        ({ thresholdSensitivity = 0.4 })()
+        ({ smoothing = 0.1 })()
+        ({ colorToReplace = Color.green })()
+    }
+}

+ 51 - 0
BFFramework/Classes/PQGPUImage/Source/Operations/CircleGenerator.swift

@@ -0,0 +1,51 @@
+#if os(Linux)
+    #if GLES
+        import COpenGLES.gles2
+    #else
+        import COpenGL
+    #endif
+#else
+    #if GLES
+        import OpenGLES
+    #else
+        import OpenGL.GL
+    #endif
+#endif
+
+public class CircleGenerator: ImageGenerator {
+    let circleShader: ShaderProgram
+
+    override public init(size: Size) {
+        circleShader = crashOnShaderCompileFailure("CircleGenerator") { try sharedImageProcessingContext.programForVertexShader(CircleVertexShader, fragmentShader: CircleFragmentShader) }
+        circleShader.colorUniformsUseFourComponents = true
+        super.init(size: size)
+    }
+
+    public func renderCircleOfRadius(_ radius: Float, center: Position, circleColor: Color = Color.white, backgroundColor: Color = Color.black) {
+        let scaledRadius = radius * 2.0
+        imageFramebuffer.activateFramebufferForRendering()
+        let uniformSettings = ShaderUniformSettings()
+        uniformSettings["circleColor"] = circleColor
+        uniformSettings["backgroundColor"] = backgroundColor
+        uniformSettings["radius"] = scaledRadius
+        uniformSettings["aspectRatio"] = imageFramebuffer.aspectRatioForRotation(.noRotation)
+
+        let convertedCenterX = (Float(center.x) * 2.0) - 1.0
+        let convertedCenterY = (Float(center.y) * 2.0) - 1.0
+        let scaledYRadius = scaledRadius / imageFramebuffer.aspectRatioForRotation(.noRotation)
+
+        uniformSettings["center"] = Position(convertedCenterX, convertedCenterY)
+        let circleVertices: [GLfloat] = [GLfloat(convertedCenterX - scaledRadius), GLfloat(convertedCenterY - scaledYRadius), GLfloat(convertedCenterX + scaledRadius), GLfloat(convertedCenterY - scaledYRadius), GLfloat(convertedCenterX - scaledRadius), GLfloat(convertedCenterY + scaledYRadius), GLfloat(convertedCenterX + scaledRadius), GLfloat(convertedCenterY + scaledYRadius)]
+
+        clearFramebufferWithColor(backgroundColor)
+        circleShader.use()
+        uniformSettings.restoreShaderSettings(circleShader)
+
+        guard let positionAttribute = circleShader.attributeIndex("position") else { fatalError("A position attribute was missing from the shader program during rendering.") }
+        glVertexAttribPointer(positionAttribute, 2, GLenum(GL_FLOAT), 0, 0, circleVertices)
+
+        glDrawArrays(GLenum(GL_TRIANGLE_STRIP), 0, 4)
+
+        notifyTargets()
+    }
+}

部分文件因为文件数量过多而无法显示