wenweiwei 3 лет назад
Родитель
Сommit
c1555cbbb6
35 измененных файлов с 11890 добавлено и 7 удалено
  1. 1 0
      BFStuckPointKit.podspec
  2. 24 0
      BFStuckPointKit/Classes/BFConfig/BFStuckPointKitConfig.swift
  3. 333 0
      BFStuckPointKit/Classes/Controller/PQEditMusicSearchController.swift
  4. 2073 0
      BFStuckPointKit/Classes/Controller/PQStuckPointEditerController.swift
  5. 384 0
      BFStuckPointKit/Classes/Controller/PQStuckPointMaterialController.swift
  6. 321 0
      BFStuckPointKit/Classes/Controller/PQStuckPointMusicContentController.swift
  7. 705 0
      BFStuckPointKit/Classes/Controller/PQStuckPointMusicController.swift
  8. 102 0
      BFStuckPointKit/Classes/Controller/PQStuckPointMusicSearchController.swift
  9. 1947 0
      BFStuckPointKit/Classes/Controller/PQStuckPointPublicController.swift
  10. 48 0
      BFStuckPointKit/Classes/Model/PQStuckPointMusicTagsModel.swift
  11. 28 0
      BFStuckPointKit/Classes/Model/PQStuckPointTimesModel.swift
  12. 329 0
      BFStuckPointKit/Classes/Model/PQVoiceModel.swift
  13. 270 0
      BFStuckPointKit/Classes/View/PQCustomSpeedSettingView.swift
  14. 103 0
      BFStuckPointKit/Classes/View/PQCustomSwitchView.swift
  15. 42 0
      BFStuckPointKit/Classes/View/PQCuttingPointView.swift
  16. 203 0
      BFStuckPointKit/Classes/View/PQEditPublicCoverImageView.swift
  17. 306 0
      BFStuckPointKit/Classes/View/PQEditPublicTitleView.swift
  18. 726 0
      BFStuckPointKit/Classes/View/PQSelecteMusicView.swift
  19. 93 0
      BFStuckPointKit/Classes/View/PQSelectedMaterialListView.swift
  20. 346 0
      BFStuckPointKit/Classes/View/PQSpeedSettingView.swift
  21. 458 0
      BFStuckPointKit/Classes/View/PQStuckPointCuttingView.swift
  22. 102 0
      BFStuckPointKit/Classes/View/PQStuckPointLoadingView.swift
  23. 67 0
      BFStuckPointKit/Classes/View/PQStuckPointMaterialHeadView.swift
  24. 260 0
      BFStuckPointKit/Classes/View/PQStuckPointMusicContentCell.swift
  25. 89 0
      BFStuckPointKit/Classes/View/PQStuckPointMusicTagsCell.swift
  26. 62 0
      BFStuckPointKit/Classes/View/PQStuckPointMusicTagsContentCell.swift
  27. 115 0
      BFStuckPointKit/Classes/View/PQStuckPointSearchEmptyCell.swift
  28. 300 0
      BFStuckPointKit/Classes/View/PQVideoCutingOprateView.swift
  29. 802 0
      BFStuckPointKit/Classes/ViewModel/PQGPUImagePlayerView.swift
  30. 854 0
      BFStuckPointKit/Classes/ViewModel/PQPlayerViewModel.swift
  31. 30 0
      BFStuckPointKit/Classes/ViewModel/PQStuckPointMusciTagsFlowLayout.swift
  32. 163 0
      BFStuckPointKit/Classes/ViewModel/PQStuckPointViewModel.swift
  33. 57 3
      Example/BFStuckPointKit.xcodeproj/project.pbxproj
  34. 1 1
      Example/Podfile
  35. 146 3
      Example/Podfile.lock

+ 1 - 0
BFStuckPointKit.podspec

@@ -33,4 +33,5 @@ TODO: Add long description of the pod here.
   s.dependency 'BFNetRequestKit'
   s.dependency 'BFUIKit'
   s.dependency 'BFMaterialKit'
+  s.dependency 'BFMediaKit'
 end

+ 24 - 0
BFStuckPointKit/Classes/BFConfig/BFStuckPointKitConfig.swift

@@ -0,0 +1,24 @@
+//
+//  BFStuckPointKitConfig.swift
+//  BFStuckPointKit
+//
+//  Created by SanW on 2021/12/8.
+//
+
+import UIKit
+
+public class BFStuckPointKitConfig: NSObject {
+    public static let shared = BFStuckPointKitConfig()
+
+    override private init() {
+        super.init()
+    }
+
+    override public func copy() -> Any {
+        return self
+    }
+
+    override public func mutableCopy() -> Any {
+        return self
+    }
+}

+ 333 - 0
BFStuckPointKit/Classes/Controller/PQEditMusicSearchController.swift

@@ -0,0 +1,333 @@
+//
+//  PQEditMusicSearchController.swift
+//  BFFramework
+//
+//  Created by ak on 2021/8/7.
+//  功能:显示编辑界中搜索音乐界面
+
+import Foundation
+import BFUIKit
+
+class PQEditMusicSearchController: BFBaseViewController {
+    // 当前播放的音乐
+    var currentPlayData: PQVoiceModel?
+    // 当前播放的视频
+    var playerItem: AVPlayerItem?
+    // 左边距离
+    let leftMargin: CGFloat = 47
+    // 搜索拦高度
+    let searchTFH: CGFloat = 37
+    // 热搜数据
+    var hotList: [Any] = Array<Any>.init()
+    // 按钮点击的回调
+    var btnClickHandle: ((_ sender: UIButton, _ bgmData: Any?) -> Void)?
+    lazy var avPlayer: AVPlayer = {
+        let avPlayer = AVPlayer()
+        PQNotification.addObserver(forName: .AVPlayerItemDidPlayToEndTime, object: avPlayer.currentItem, queue: .main) { [weak self] notify in
+            BFLog(message: "AVPlayerItemDidPlayToEndTime = \(notify)")
+            avPlayer.seek(to: CMTime(value: CMTimeValue((self?.currentPlayData?.startTime ?? 0) * 1000), timescale: CMTimeScale(playerTimescale)))
+//            avPlayer.play()
+            self?.playStuckPointMusic(itemData: nil)
+        }
+//        PQNotification.addObserver(forName: .AVPlayerItemNewErrorLogEntry, object: avPlayer.currentItem, queue: .main) { notify in
+//            BFLog(message: "AVPlayerItemNewErrorLogEntry = \(notify)")
+//        }
+//        PQNotification.addObserver(forName: .AVPlayerItemFailedToPlayToEndTime, object: avPlayer.currentItem, queue: .main) { notify in
+//            BFLog(message: "AVPlayerItemFailedToPlayToEndTime = \(notify)")
+//        }
+//        PQNotification.addObserver(forName: .AVPlayerItemPlaybackStalled, object: avPlayer.currentItem, queue: .main) { notify in
+//            BFLog(message: "AVPlayerItemPlaybackStalled = \(notify)")
+//        }
+//        avPlayer.addPeriodicTimeObserver(forInterval: CMTime(value: 1, timescale: CMTimeScale(playerTimescale)), queue: .main) { [weak self] cmTime in
+//            BFLog(message: "addPeriodicTimeObserver = \(cmTime)")
+//        }
+        return avPlayer
+    }()
+    // 输入框清空按钮
+    lazy var clearBtn: UIButton = {
+        let clearBtn = UIButton(type: .custom)
+        clearBtn.setImage(UIImage.moduleImage(named: "icon_search_delete", moduleName: "BFFramework", isAssets: false), for: .normal)
+        clearBtn.frame = CGRect(x: 0, y: 0, width: 28, height: 32)
+        clearBtn.tag = 1
+        clearBtn.addTarget(self, action: #selector(btnClick(sender:)), for: .touchUpInside)
+        clearBtn.isHidden = true
+        return clearBtn
+    }()
+    // 搜索框
+    lazy var searchTF: UITextField = {
+        let searchTF = UITextField(frame: CGRect(x: leftMargin, y: cDevice_iPhoneStatusBarHei, width: cScreenWidth - leftMargin * 2, height: searchTFH))
+        searchTF.font = UIFont.systemFont(ofSize: 17)
+        searchTF.backgroundColor = BFConfig.shared.otherTintColor
+        searchTF.attributedPlaceholder = NSAttributedString(string: "搜索歌曲名称/歌手", attributes: [.foregroundColor: UIColor.hexColor(hexadecimal: "#BDBDBD"), .font: UIFont.systemFont(ofSize: 14)])
+        searchTF.textColor = BFConfig.shared.styleTitleColor
+        searchTF.addCorner(corner: searchTFH / 2)
+
+        searchTF.leftViewMode = .always
+        let leftView = UIView(frame: CGRect(x: 0, y: 0, width: 35, height: 32))
+        let imageView = UIImageView(image: UIImage.moduleImage(named: "icon_search_s", moduleName: "BFFramework", isAssets: false))
+        imageView.frame = CGRect(x: 15, y: 8, width: 16, height: 16)
+        leftView.addSubview(imageView)
+        searchTF.leftView = leftView
+        searchTF.delegate = self
+
+        searchTF.rightViewMode = .always
+        let rightView = UIView(frame: CGRect(x: 0, y: 0, width: 40, height: 32))
+        rightView.addSubview(clearBtn)
+        searchTF.rightView = rightView
+        searchTF.delegate = self
+        searchTF.returnKeyType = .search
+        searchTF.addTarget(self, action: #selector(editingChanged), for: .editingChanged)
+        return searchTF
+    }()
+
+    /// 搜索控制器
+    lazy var searchController: PQStuckPointMusicSearchController = {
+        let searchController = PQStuckPointMusicSearchController()
+        searchController.cellHight = cDefaultMargin * 8
+ 
+        searchController.updateViewFrame(newFrame: CGRect(x: 0, y: cDevice_iPhoneNavBarAndStatusBarHei + cDefaultMargin * 2, width: view.frame.width, height: view.frame.height - (cDevice_iPhoneNavBarAndStatusBarHei + cDefaultMargin * 2)))
+        searchController.didSelectedHandle = { [weak self] isTagsClick, _, _, itemData in
+            if !isTagsClick {
+                self?.view.endEditing(true)
+                if !(itemData is BFEmptyModel) {
+                    PQEventTrackViewModel.baseReportUpload(businessType: .bt_buttonClick, objectType: .ot_shanyinApp_musicVideoPreview_musicSearchAudition, pageSource: .sp_shanyinApp_main, extParams: ["musicName":(itemData as? PQVoiceModel)?.musicName ?? "" ,"musicId":(itemData as? PQVoiceModel)?.musicId ?? ""], remindmsg: "")
+                    
+                    self?.playStuckPointMusic(itemData: itemData as? PQVoiceModel)
+                }
+            }
+        }
+        searchController.btnClickHandle = { [weak self] btn, bgmData in
+            
+            PQEventTrackViewModel.baseReportUpload(businessType: .bt_buttonClick, objectType: .ot_shanyinApp_musicVideoPreview_musicSearchSelect, pageSource: .sp_shanyinApp_main, extParams: ["musicName":(bgmData as? PQVoiceModel)?.musicName ?? "" ,"musicId":(bgmData as? PQVoiceModel)?.musicId ?? ""], remindmsg: "")
+            
+            // 使用音乐
+            self?.backBtnClick()
+            if(self?.btnClickHandle != nil){
+                self?.btnClickHandle!(btn,bgmData)
+            }
+ 
+        }
+        searchController.scroDidScroHandle = { [weak self] in
+            self?.view.endEditing(true)
+        }
+        searchController.contentType = .serach
+        return searchController
+    }()
+    override func backBtnClick() {
+        super.backBtnClick()
+        avPlayer.pause()
+    }
+    deinit {
+        PQNotification.removeObserver(self)
+        avPlayer.currentItem?.removeObserver(self, forKeyPath: "status")
+        avPlayer.currentItem?.removeObserver(self, forKeyPath: "error")
+        avPlayer.pause()
+        avPlayer.replaceCurrentItem(with: nil)
+        playerItem = nil
+    }
+    override func viewWillAppear(_ animated: Bool) {
+        super.viewDidAppear(animated)
+        PQEventTrackViewModel.baseReportUpload(businessType: .bt_windowView, objectType: .ot_shanyinApp_musicVideoPreview_musicSearch, pageSource: .sp_shanyinApp_main, extParams: nil, remindmsg: "")
+
+
+    }
+    override func viewDidLoad() {
+        super.viewDidLoad()
+        view.addSubview(searchTF)
+        searchTF.becomeFirstResponder()
+        
+        addChild(searchController)
+        view.addSubview(searchController.view)
+        
+        leftButton(image: UIImage(named: "upload_delete"), tintColor: BFConfig.shared.styleTitleColor)
+        
+        //请求一下热门数据在没有搜索数据时显示
+        PQStuckPointViewModel.stuckPointMusicPageList(tagId: 425, parentTagId: 0, pageNum: 1, videoCount: 0, imageCount: 0, totalDuration: 0) { [weak self] musicInfo, _ in
+
+            if musicInfo.count > 0 {
+                self?.searchController.hotList = musicInfo
+                self?.searchController.configMusicListData(isRefresh: true, musicListData: musicInfo)
+
+            }
+       
+        }
+    }
+    
+    
+    /// 按钮点击事件
+    /// - Parameter sender: <#sender description#>
+    /// - Returns: <#description#>
+    @objc func btnClick(sender: UIButton) {
+        switch sender.tag {
+        case 1: // 清除
+            searchTF.text = nil
+//            emptyRemindView.isHidden = true
+            clearBtn.isHidden = true
+        default:
+            break
+        }
+    }
+    
+    var avPlayerTimeObserver:Any?
+    /// 播放音乐
+    /// - Parameter itemData: <#itemData description#>
+    func playStuckPointMusic(itemData: PQVoiceModel?, isClearCurrentMusic: Bool = false) {
+        if itemData != nil, currentPlayData != itemData {
+            if !isValidURL(url: itemData?.musicPath ?? "") {
+                cShowHUB(superView: nil, msg: "本歌曲暂无伴奏版本哦~")
+                return
+            }
+            avPlayer.pause()
+            playerItem?.removeObserver(self, forKeyPath: "status")
+            playerItem?.removeObserver(self, forKeyPath: "error")
+            if avPlayerTimeObserver != nil {
+                avPlayer.removeTimeObserver(avPlayerTimeObserver as Any)
+            }
+            
+            playerItem = AVPlayerItem(url: URL(string: itemData?.musicPath ?? "")!)
+            if (itemData?.endTime ?? 0) > 0, (itemData?.endTime ?? 0) > (itemData?.startTime ?? 0) {
+                playerItem?.forwardPlaybackEndTime = CMTime(value: CMTimeValue((itemData?.endTime ?? 0) * playerTimescale), timescale: CMTimeScale(playerTimescale))
+            }
+            avPlayer.replaceCurrentItem(with: playerItem)
+            playerItem?.addObserver(self, forKeyPath: "status", options: .new, context: nil)
+            playerItem?.addObserver(self, forKeyPath: "error", options: .new, context: nil)
+            avPlayer.seek(to: CMTime(value: CMTimeValue((itemData?.startTime ?? 0) * playerTimescale), timescale: CMTimeScale(playerTimescale)))
+            avPlayer.play()
+            
+            avPlayerTimeObserver = avPlayer.addPeriodicTimeObserver(forInterval: CMTime(value: 1, timescale: 10), queue: DispatchQueue.global()) {[weak self] time in
+                if fabs(CMTimeGetSeconds(time) - (itemData?.startTime ?? 0)) > 0.1 {
+                    self?.avPlayer.removeTimeObserver(self?.avPlayerTimeObserver as Any)
+                    self?.avPlayerTimeObserver = nil
+                    // 停止cell loading动画
+                    PQNotification.post(name: NSNotification.Name(rawValue: "MusicContentCellIconLoadingAnimationStop"), object: nil)
+                }
+            }
+//            let player = TXVodPlayer()
+//            let config = TXVodPlayConfig()
+//            config.cacheFolderPath = videoCacheDirectory
+//            config.maxCacheItems = 0
+//            player.config = config
+            ////            player.vodDelegate = self
+//            player.setRenderMode(.RENDER_MODE_FILL_EDGE)
+//            player.startPlay("https://clipres.yishihui.com/longvideo/material/voice/prod/20210512/MUSIC_QQ_000T1Ws32MWrUj")
+            currentPlayData = itemData
+        } else if itemData != nil, avPlayer.rate == 0.0 {
+            avPlayer.play()
+        } else {
+            avPlayer.pause()
+ 
+        }
+        if isClearCurrentMusic {
+            avPlayer.pause()
+            currentPlayData = nil
+        }
+    }
+}
+
+/// 音乐搜索相关
+extension PQEditMusicSearchController: UITextFieldDelegate {
+    /// 点击输入框
+    @objc func editingChanged() {
+        if searchTF.text != nil, (searchTF.text?.count ?? 0) > 0, searchTF.markedTextRange == nil {
+            if searchTF.text?.isSpace ?? false {
+                return
+            }
+//            loadSearchData()
+            clearBtn.isHidden = false
+        }
+    }
+
+    /// 搜索
+    @objc func loadSearchData() {
+        if searchTF.text != nil, (searchTF.text?.count ?? 0) > 0, searchTF.text?.isSpace ?? false {
+            cShowHUB(superView: nil, msg: "搜索内容不能为空")
+            return
+        }
+        if searchTF.text != nil, (searchTF.text?.count ?? 0) > 0 {
+            BFLog(message: "背景音乐--开始搜索背景音乐-1")
+            searchController.loadSearchData(keyword: searchTF.text)
+        }
+    }
+
+    /// 添加键盘监听
+    /// - Returns: <#description#>
+    func addKeyboardObserver() {
+        // 监听键盘的显示和隐藏
+        PQNotification.addObserver(self, selector: #selector(keyboardWillShow(notification:)), name: UIResponder.keyboardWillShowNotification, object: nil)
+        PQNotification.addObserver(self, selector: #selector(keyboardWillHidden), name: UIResponder.keyboardWillHideNotification, object: nil)
+    }
+
+    /// 键盘显示
+    /// - Parameter notification: <#notification description#>
+    @objc func keyboardWillShow(notification: Notification) {
+        let duration: TimeInterval = TimeInterval("\(notification.userInfo?["UIKeyboardAnimationDurationUserInfoKey"] ?? "1")") ?? 1
+        UIView.animate(withDuration: duration) { [weak self] in
+            self?.navTitleLabel?.alpha = 0
+            self?.searchTF.frame = CGRect(x: cDefaultMargin * 5, y: (cDevice_iPhoneNavBarHei - (self?.searchTFH ?? 37.0)) / 2 + cDevice_iPhoneStatusBarHei, width: cScreenWidth - cDefaultMargin * 7, height: self?.searchTFH ?? 37.0)
+        } completion: { _ in
+        }
+        searchController.view.isHidden = false
+        hotList.forEach { item in
+            if item is PQVoiceModel {
+                (item as? PQVoiceModel)?.isSelected = false
+                (item as? PQVoiceModel)?.isPlaying = false
+            }
+        }
+
+        searchController.hotList = hotList
+        playStuckPointMusic(itemData: nil)
+    
+    }
+
+    /// 键盘将要隐藏
+    @objc func keyboardWillHidden() {}
+
+    /// 隐藏搜索界面
+    /// - Returns: <#description#>
+    func hiddenSearchController() {
+        view.endEditing(true)
+        UIView.animate(withDuration: 0.3) { [weak self] in
+            self?.navTitleLabel?.alpha = 1
+            self?.searchTF.frame = CGRect(x: self?.leftMargin ?? 0, y: cDevice_iPhoneNavBarAndStatusBarHei, width: cScreenWidth - (self?.leftMargin ?? 0) * 2, height: self?.searchTFH ?? 0)
+        } completion: { _ in
+        }
+        clearBtn.isHidden = true
+        searchController.view.isHidden = true
+        playStuckPointMusic(itemData: nil)
+    }
+
+    override func touchesBegan(_: Set<UITouch>, with _: UIEvent?) {
+        view.endEditing(true)
+    }
+
+    func textFieldShouldReturn(_ textField: UITextField) -> Bool {
+        loadSearchData()
+        
+        PQEventTrackViewModel.baseReportUpload(businessType: .bt_buttonClick, objectType: .ot_shanyinApp_musicVideoPreview_musicSearch, pageSource: .sp_shanyinApp_main, extParams: nil, remindmsg: "")
+        view.endEditing(true)
+        if textField.text == nil || (textField.text?.count ?? 0) <= 0 {
+            cShowHUB(superView: nil, msg: "请先输入搜索内容")
+        }
+        return true
+    }
+}
+extension PQEditMusicSearchController {
+    override func observeValue(forKeyPath keyPath: String?, of object: Any?, change _: [NSKeyValueChangeKey: Any]?, context _: UnsafeMutableRawPointer?) {
+        if object is AVPlayerItem, keyPath == "status" {
+            BFLog(message: "AVPlayerItem - status = \((object as! AVPlayerItem).status.rawValue)")
+            switch (object as! AVPlayerItem).status {
+            case .unknown:
+                break
+            case .readyToPlay:
+                break
+            case .failed:
+                break
+            default:
+                break
+            }
+        } else if object is AVPlayerItem, keyPath == "error" {
+            BFLog(message: "AVPlayerItem - error = \(String(describing: (object as! AVPlayerItem).error))")
+        }
+    }
+}
+

+ 2073 - 0
BFStuckPointKit/Classes/Controller/PQStuckPointEditerController.swift

@@ -0,0 +1,2073 @@
+//
+//  PQStuckPointEditerController.swift
+//  PQSpeed
+//
+//  Created by ak on 2021/4/26.
+//  Copyright © 2021 BytesFlow. All rights reserved.
+//  功能:卡点音乐编辑界面
+
+// 创建不同玩法的类型 (1:跳跃卡点,2:快慢速,3:仅配乐)
+public enum createStickersModel: Int {
+    case createStickersModelPoint = 1 // 跳跃卡点
+    case createStickersModelSpeed = 2 // 快慢速
+    case createStickersModelOnlyMusic = 3 // 仅配乐
+}
+
+import BFCommonKit
+import Foundation
+import ObjectMapper
+import Photos
+import RealmSwift
+import UIKit
+
+class PQStuckPointEditerController: BFBaseViewController {
+    // 是否导出视频成功
+    var isExportVideosSuccess: Bool = false
+    // 是否请求卡点数据成功
+    var isStuckPointDataSuccess: Bool = false
+    // 是否同步音乐成功
+    var isSynchroMusicInfoSuccess: Bool = false
+    /// 当前所有的filter
+    var filters: Array = Array<ImageProcessingOperation>.init()
+    // 选中所有素材的的总时长 再进入编辑界面时已经不包括图片的时长
+    var selectedTotalDuration: Float64 = 0
+    // 选择的总数
+    var selectedDataCount: Int = 0
+    // 选择的图片总数
+    var selectedImageDataCount: Int = 0
+    // 选中的素材数据
+    var selectedPhotoData: [PHAsset]? {
+        didSet {
+            if selectedPhotoData != nil, (selectedPhotoData?.count ?? 0) > 0 {
+                selectedMetarialData = Array<PQEditVisionTrackMaterialsModel>.init()
+                selectedPhotoData?.forEach { phAsset in
+                    let metarialData = PQEditVisionTrackMaterialsModel()
+                    metarialData.asset = phAsset
+                    metarialData.width = Float(phAsset.pixelWidth)
+                    metarialData.itemWidth = Float(phAsset.pixelWidth)
+                    metarialData.height = Float(phAsset.pixelHeight)
+                    metarialData.itemHeight = Float(phAsset.pixelHeight)
+                    if phAsset.mediaType == .image {
+                        metarialData.type = "image"
+                    } else if phAsset.mediaType == .video {
+                        metarialData.type = "video"
+                        metarialData.duration = Float64(phAsset.duration)
+                    }
+                    metarialData.canvasFillType = phAsset.canvasFillType ?? ""
+                    metarialData.locationPath = phAsset.localPath ?? ""
+                    metarialData.selectedIndex = phAsset.selectedIndex ?? 1
+                    metarialData.originalData = phAsset.originalData
+                    metarialData.coverImageUI = phAsset.image
+                    selectedMetarialData?.append(metarialData)
+                }
+            }
+        }
+    }
+
+    var selectedMetarialData: [PQEditVisionTrackMaterialsModel]?
+    // 选中的音乐数据
+    var stuckPointMusicData: PQVoiceModel?
+    // 保存所有段的所有贴纸,音频信息,用于播放器的渲染使用
+    var projectModel: PQEditProjectModel = PQEditProjectModel()
+    // 从草稿箱进入的项目数据
+    var draftProjectModel: PQEditProjectModel?
+    var mStickers: [PQEditVisionTrackMaterialsModel]?
+    // 播放器的开始和结束时间,1,刚进界面使用推荐的开始结束时间,2,用户修改起结点时修改
+    var playeTimeRange: CMTimeRange = CMTimeRange()
+
+    // 首帧图片
+    var firstFrameImage: UIImage? {
+        didSet {
+            if firstFrameImage != nil {
+                playerView.layer.contents = firstFrameImage?.cgImage
+            }
+        }
+    }
+
+    // add by ak 是否是再创作模式
+    var isReCreate: Bool = false
+    public var reCreateVideoData: PQReCreateModel? // 再创作数据
+    // 最后一个选择的模式 BTN 用于还原选中状态
+    var lastEditModelBtn: UIButton?
+
+    // add by ak  最大、最小速度 有固定值和自定义,当快慢速下两个值都有效,当跳跃卡点只有maxSpeed有效
+    // 快慢速模式的 速度设置,快/慢速
+    var modelSpeed_maxSpeed: Float = 1.0
+    var modelSpeed_minSpeed: Float = 1.0
+    // 跳跃模式的速度
+    var modelPoint_speed: Float = 1.0
+
+    // 快慢速最后一次选择的速度位置
+    var lastSpeedSelectIndex: Int = 0
+    // 跳跃卡点最后一次选择的速度位置
+    var lastJumpSpeedSelectIndex: Int = 0
+    // 循环次数设置最后一次选择的位置
+    var lastCyclesSelectIndex: Int = -1
+
+    // 当前选择的玩法模式
+    var currentCreateStickersModel: createStickersModel = .createStickersModelSpeed
+
+    // 最终使用的卡点数据
+    var finallyStuckPoints: Array = Array<Float>.init()
+    var finallyStuckPointsInt64: Array = Array<Int64>.init()
+    // 最终使用的音频时长
+    var finallyUserAudioTime: Float = 0.0
+
+    // 注意推荐时间位置和后面最近的卡点时间与0.3的关系
+    // 保存丢卡点处理后的卡点信息推荐开始到最后倒数第二个
+    // 经过档位处理后的卡点信息
+    var stuckPointsTemp: Array = Array<Float>.init()
+    var stuckPointsTempInt64: Array = Array<Int64>.init()
+
+    // 是否点击了下一步去合成
+    var isClickNextBtn: Bool = false
+
+    // 下一步
+    lazy var nextBtn: UIButton = {
+        let nextBtn = UIButton(type: .custom)
+        nextBtn.frame = CGRect(x: cScreenWidth - 16 - cDefaultMargin * 6, y: cDevice_iPhoneStatusBarHei + (cDevice_iPhoneNavBarHei - cDefaultMargin * 3) / 2, width: cDefaultMargin * 6, height: cDefaultMargin * 3)
+        nextBtn.setTitle("合成", for: .normal)
+        nextBtn.titleLabel?.font = UIFont.systemFont(ofSize: 15, weight: .medium)
+        nextBtn.addTarget(self, action: #selector(nextBtnClick(sender:)), for: .touchUpInside)
+        nextBtn.backgroundColor = UIColor.hexColor(hexadecimal: BFConfig.shared.styleColor.rawValue)
+        nextBtn.setTitleColor(UIColor.hexColor(hexadecimal: "#FFFFFF"), for: .normal)
+        nextBtn.uxy_acceptEventInterval = 0.5
+        nextBtn.addCorner(corner: 3)
+        return nextBtn
+    }()
+
+    // 播放器显示 view
+    lazy var playerView: PQGPUImagePlayerView = {
+        let playerView = PQGPUImagePlayerView(frame: .zero)
+        playerView.backgroundColor = BFConfig.shared.styleBackGroundColor
+        playerView.isShowLine = false
+        playerView.showGaussianBlur = true
+        playerView.pause()
+        playerView.renderViewOnClickHandle = { [weak self] in
+            self?.musicEditBGView.pausePlayer()
+        }
+        playerView.playerEmptyView.isHidden = true
+        return playerView
+    }()
+
+    /// 节奏选择视图
+    lazy var sustomSwitchView: PQCustomSwitchView = {
+        let sustomSwitchView = PQCustomSwitchView(frame: CGRect(x: 16, y: 0, width: 180, height: 30), titles: ["慢节奏", "适中", "快节奏"], defaultIndex: stuckPointMusicData?.speed ?? 2)
+        sustomSwitchView.switchChangeHandle = { [weak self] sender in
+            // 改变速率,.只有快慢速且非只有图片素材时自动+1处理
+            self?.stuckPointMusicData?.speed = sender.tag
+            self?.musicEditBGView.pausePlayer()
+
+            self?.projectModel.sData?.getBGMSession()?.sectionTimeline?.audioTrack?.audioTrackMaterials.first?.bgmInfo?.rhythmMusicSpeed = sender.tag
+            // 播放前先暂停
+//            self?.playerView.stop()
+            // 开始播放
+            self?.settingPlayerView()
+
+            // 下面都是统计
+            if self?.currentCreateStickersModel == .createStickersModelPoint {
+                PQEventTrackViewModel.baseReportUpload(businessType: .bt_buttonClick, objectType: .ot_shanyinApp_musicVideoPreview_selectMusicVideoRhythm, pageSource: .sp_stuck_previewSyncedUp, extParams: nil, remindmsg: "点击上报:选择节奏")
+            } else if self?.currentCreateStickersModel == .createStickersModelSpeed {
+                PQEventTrackViewModel.baseReportUpload(businessType: .bt_buttonClick, objectType: .ot_shanyinApp_musicVideoPreview_selectSpeedRhythm, pageSource: .sp_stuck_previewSyncedUp, extParams: nil, remindmsg: "点击上报:选择节奏")
+            } else if self?.currentCreateStickersModel == .createStickersModelOnlyMusic {
+                PQEventTrackViewModel.baseReportUpload(businessType: .bt_buttonClick, objectType: .ot_shanyinApp_musicVideoPreview_selectMusicVideoRepeatRhythm, pageSource: .sp_stuck_previewSyncedUp, extParams: nil, remindmsg: "点击上报:选择节奏")
+            }
+        }
+        return sustomSwitchView
+    }()
+
+    /// 裁剪视图
+    lazy var stuckPointCuttingView: PQStuckPointCuttingView = {
+        let stuckPointCuttingView = PQStuckPointCuttingView(frame: CGRect(x: 0, y: optionlineView.frame.minY - 85 - 28, width: view.frame.width, height: 80), duration: CGFloat(Float(stuckPointMusicData?.duration ?? "0") ?? 0), suggestRhythmStartTime: CGFloat(stuckPointMusicData?.suggestRhythmStartTime ?? 0))
+        /// 裁剪进度回调
+        stuckPointCuttingView.videoDidBeginDrag = { [weak self] in
+            BFLog(message: "开始划动")
+            self?.playerView.pause()
+        }
+        /// 播放进度回调
+        stuckPointCuttingView.videoProgressDidChanged = { [weak self] progress in
+            BFLog(message: "进度更新返回--progress = \(progress) \(String(describing: self?.playerView.mPlayeTimeRange))")
+        }
+        /// 拖缀结束的回调 type - 1-拖动左边裁剪结束 2--拖动右边裁剪结束 3-进度条拖动结束 4-滑动结束
+        stuckPointCuttingView.videoDidEndDragging = { [weak self] type, startTime, endTime, progress in
+            BFLog(1, message: "拖拽结束返回--type = \(type),startTime = \(startTime),endTime = \(endTime),progress = \(progress)")
+            self?.playerView.pause()
+            self?.musicEditBGView.pausePlayer()
+
+            // 修改最新值
+            self?.stuckPointMusicData?.startTime = Float64(startTime)
+            self?.stuckPointMusicData?.endTime = Float64(endTime)
+            // 红的指针完成
+            if type == 3 {
+                if CMTimeGetSeconds(self?.playerView.mPlayeTimeRange?.end ?? .zero) == 0 {
+                    BFLog(message: "mPlayeTimeRange is error")
+                    return
+                }
+                let newBeginSconds = (Double(startTime) + (Double(endTime) - Double(startTime)) * Double(progress)) * 600
+                BFLog(message: " newBeginSconds is \(newBeginSconds)")
+                let seekTimeRange: CMTimeRange = CMTimeRange(start: CMTime(value: CMTimeValue(Int64(newBeginSconds)), timescale: 600), end:
+                    CMTime(value: CMTimeValue(Int64(endTime * 600)), timescale: 600))
+                BFLog(message: "修改的开始 \(CMTimeGetSeconds(seekTimeRange.start)) 结束  \(CMTimeGetSeconds(seekTimeRange.end))")
+                // 重新设置有效缓存
+                self?.playerView.configCache(beginTime: CMTimeGetSeconds(seekTimeRange.start))
+                self?.playerView.play(pauseFirstFrame: false, playeTimeRange: seekTimeRange)
+
+            } else {
+                // 更改素材开始时间及结束时间
+                self?.projectModel.sData?.getBGMSession()?.sectionTimeline?.audioTrack?.audioTrackMaterials.first?.out = Float64(endTime)
+                self?.projectModel.sData?.getBGMSession()?.sectionTimeline?.audioTrack?.audioTrackMaterials.first?.model_in = Float64(startTime)
+                self?.projectModel.sData?.getBGMSession()?.sectionTimeline?.audioTrack?.audioTrackMaterials.first?.timelineIn = Float64(startTime)
+                self?.projectModel.sData?.getBGMSession()?.sectionTimeline?.audioTrack?.audioTrackMaterials.first?.timelineOut = Float64(endTime)
+                BFLog(message: "调整后总时长: \(endTime - startTime) startTime:\(startTime) endTime:\(endTime)")
+                // 初始化音频的开始和结束时间
+                self?.playeTimeRange = CMTimeRange(start: CMTimeMakeWithSeconds(Float64(startTime), preferredTimescale: BASE_FILTER_TIMESCALE), end: CMTimeMakeWithSeconds(Float64(endTime), preferredTimescale: BASE_FILTER_TIMESCALE))
+
+                self?.dealParameter(model: self?.currentCreateStickersModel ?? .createStickersModelSpeed)
+                if (self?.finallyStuckPoints.count ?? 0) < 1 {
+                    BFLog(message: "finallyStuckPoints data is error!!!!")
+                    return
+                }
+
+                DispatchQueue.global().async { // 并行、异步
+                    let beginTime = Date()
+
+                    self?.mStickers = self?.createStickers(sections: self?.projectModel.sData?.sections ?? List(), inputSize: CGSize(width: CGFloat(self?.projectModel.sData?.videoMetaData?.videoWidth ?? 0), height: CGFloat(self?.projectModel.sData?.videoMetaData?.videoHeight ?? 0)), model: self?.currentCreateStickersModel ?? .createStickersModelSpeed)
+
+                    DispatchQueue.main.async { // 串行、异步
+                        self?.playerView.mStickers = self?.mStickers
+                        BFLog(message: "endTime is endTimeendTime \(Date().timeIntervalSince(beginTime))")
+                        self?.playerView.play(pauseFirstFrame: false, playeTimeRange: self!.playeTimeRange)
+
+                        // 更新一下时间条的UI总时间 及数据
+                        self?.stuckPointCuttingView.videoDuration = CGFloat(self?.finallyUserAudioTime ?? 0)
+
+                        self?.stuckPointCuttingView.stuckPointStartTime = CGFloat(CMTimeGetSeconds(self?.playeTimeRange.start ?? .zero))
+                        self?.stuckPointCuttingView.stuckPointEndTime = CGFloat(CMTimeGetSeconds(self?.playeTimeRange.end ?? .zero))
+                        self?.stuckPointCuttingView.tatalTimeLabel.text = "\(Float64(CMTimeGetSeconds(self?.playeTimeRange.end ?? .zero) - CMTimeGetSeconds(self?.playeTimeRange.start ?? .zero)).formatDurationToHMS())"
+                    }
+                }
+            }
+        }
+        return stuckPointCuttingView
+    }()
+
+    /// 卡点模式标题
+    lazy var pointEditRemindLab: UILabel = {
+        let pointEditRemindLab = UILabel()
+        pointEditRemindLab.backgroundColor = .clear
+        pointEditRemindLab.textAlignment = .left
+        pointEditRemindLab.font = UIFont.boldSystemFont(ofSize: 14)
+        pointEditRemindLab.textColor = BFConfig.shared.styleTitleColor
+        pointEditRemindLab.text = "卡点模式"
+        return pointEditRemindLab
+    }()
+
+    /// 卡点模式标题
+    lazy var speedTitleLab: UILabel = {
+        let speedTitleLab = UILabel()
+        speedTitleLab.backgroundColor = .clear
+        speedTitleLab.textAlignment = .left
+        speedTitleLab.font = UIFont.boldSystemFont(ofSize: 14)
+        speedTitleLab.textColor = BFConfig.shared.styleTitleColor
+        speedTitleLab.text = "节奏变化"
+        return speedTitleLab
+    }()
+
+    /// 卡点模式下方操作区背景
+    lazy var pointEditBGView: UIView = {
+        let pointEditBGView = UIView()
+        pointEditBGView.backgroundColor = .clear
+        return pointEditBGView
+    }()
+
+    /// 下方音乐编辑操作区背景
+    lazy var musicEditBGView: PQSelecteMusicView = {
+        let musicEditBGView = PQSelecteMusicView()
+        musicEditBGView.backgroundColor = .clear
+        musicEditBGView.isUserInteractionEnabled = true
+        musicEditBGView.isHidden = true
+        musicEditBGView.musicSearchBtn.addTarget(self, action: #selector(musicSearchBtnClick(sender:)), for: .touchUpInside)
+
+        musicEditBGView.didSelectItemHandle = { [weak self] status in
+            if status == .isSelected {
+                if self?.playerView.status != .playing {
+                    self?.playerView.RenderViewOnclick()
+                }
+            } else {
+                self?.playerView.pause()
+            }
+        }
+        musicEditBGView.btnClickHandle = { [weak self] _, bgmData in
+            // 使用音乐
+            self?.userstuckPointMusic(musicData: bgmData as? PQVoiceModel)
+        }
+
+        return musicEditBGView
+    }()
+
+    // 卡点编辑 btn
+    lazy var pointEditerBtn: UIButton = {
+        let pointEdterBtn = UIButton(type: .custom)
+
+        pointEdterBtn.setImage(UIImage.moduleImage(named: "pointEditerBtn_n", moduleName: "BFFramework", isAssets: false), for: .normal)
+
+        pointEdterBtn.setImage(UIImage.moduleImage(named: "pointEditerBtn_h", moduleName: "BFFramework", isAssets: false)?.withRenderingMode(.alwaysTemplate), for: .selected)
+        pointEdterBtn.tintColor = UIColor.hexColor(hexadecimal: BFConfig.shared.styleColor.rawValue)
+        pointEdterBtn.addTarget(self, action: #selector(pointEditerBtnClick(sender:)), for: .touchUpInside)
+        pointEdterBtn.isSelected = true
+        pointEdterBtn.adjustsImageWhenHighlighted = false
+        return pointEdterBtn
+    }()
+
+    // 音乐编辑 btn
+    lazy var musicEditerBtn: UIButton = {
+        let musicEditerBtn = UIButton(type: .custom)
+        musicEditerBtn.setImage(UIImage.moduleImage(named: "musicEditerBtn_n", moduleName: "BFFramework", isAssets: false), for: .normal)
+        musicEditerBtn.setImage(UIImage.moduleImage(named: "musicEditerBtn_h", moduleName: "BFFramework", isAssets: false)?.withRenderingMode(.alwaysTemplate), for: .selected)
+        musicEditerBtn.tintColor = UIColor.hexColor(hexadecimal: BFConfig.shared.styleColor.rawValue)
+        musicEditerBtn.addTarget(self, action: #selector(musicEditerBtnClick(sender:)), for: .touchUpInside)
+        musicEditerBtn.adjustsImageWhenHighlighted = false
+        return musicEditerBtn
+    }()
+
+    // 快慢速卡点模式 btn
+    lazy var speedStuckBtn: UIButton = {
+        let speedStuckBtn = UIButton(type: .custom)
+        speedStuckBtn.addTarget(self, action: #selector(editModelClick1(sender:)), for: .touchUpInside)
+        speedStuckBtn.setTitle("快慢速卡点", for: .normal)
+        speedStuckBtn.titleLabel?.font = UIFont.systemFont(ofSize: 13, weight: .regular)
+        jumpPointBtn.backgroundColor =  BFConfig.shared.pointEditNamalBackgroundColor
+        speedStuckBtn.setTitleColor(UIColor.hexColor(hexadecimal: BFConfig.shared.styleColor.rawValue), for: .selected)
+        speedStuckBtn.setTitleColor(UIColor.hexColor(hexadecimal: "#959595"), for: .normal)
+        speedStuckBtn.addCorner(corner: 5)
+        speedStuckBtn.imagePosition(at: .top, space: 8)
+        speedStuckBtn.tag = 1
+        speedStuckBtn.adjustsImageWhenHighlighted = false
+        speedStuckBtn.setImage(UIImage.moduleImage(named: BFConfig.shared.speedStuckBtnImage_N, moduleName: "BFFramework", isAssets: false), for: .normal)
+        speedStuckBtn.setImage(UIImage.moduleImage(named: BFConfig.shared.speedStuckBtnImage_H, moduleName: "BFFramework", isAssets: false), for: .selected)
+        return speedStuckBtn
+    }()
+
+//    //
+//    lazy var speedStuckBtnGif: UIImageView = {
+//        let speedStuckBtnGif = UIImageView()
+//        speedStuckBtnGif.kf.setImage(with: URL(fileURLWithPath: currentBundlePath()!.path(forResource: "speedstuck_h", ofType: "gif")!))
+//        speedStuckBtnGif.isHidden = true
+//        return speedStuckBtnGif
+//
+//    }()
+
+    // 跳转卡点模式 btn
+    lazy var jumpPointBtn: UIButton = {
+        let jumpPointBtn = UIButton(type: .custom)
+
+        jumpPointBtn.setTitle("跳跃卡点", for: .normal)
+        jumpPointBtn.titleLabel?.font = UIFont.systemFont(ofSize: 13, weight: .regular)
+        jumpPointBtn.backgroundColor =  BFConfig.shared.pointEditNamalBackgroundColor
+        jumpPointBtn.setTitleColor(UIColor.hexColor(hexadecimal: BFConfig.shared.styleColor.rawValue), for: .selected)
+        jumpPointBtn.setTitleColor(UIColor.hexColor(hexadecimal: "#959595"), for: .normal)
+        jumpPointBtn.imagePosition(at: .top, space: 8)
+        jumpPointBtn.addCorner(corner: 5)
+        jumpPointBtn.tag = 2
+        jumpPointBtn.addTarget(self, action: #selector(editModelClick1(sender:)), for: .touchUpInside)
+        jumpPointBtn.adjustsImageWhenHighlighted = false
+        jumpPointBtn.setImage(UIImage.moduleImage(named: BFConfig.shared.jumpPointBtnImage_N, moduleName: "BFFramework", isAssets: false), for: .normal)
+        jumpPointBtn.setImage(UIImage.moduleImage(named: BFConfig.shared.jumpPointBtnImage_H, moduleName: "BFFramework", isAssets: false), for: .selected)
+        return jumpPointBtn
+    }()
+
+//    lazy var jumpPointBtnGif: UIImageView = {
+//        let jumpPointBtnGif = UIImageView()
+//        jumpPointBtnGif.kf.setImage(with: URL(fileURLWithPath: currentBundlePath()!.path(forResource: "jumpPoint_n", ofType: "gif")!))
+//        jumpPointBtnGif.isHidden = true
+//        return jumpPointBtnGif
+//
+//    }()
+
+    // 仅配乐模式 btn
+    lazy var onlyMusicBtn: UIButton = {
+        let onlyMusicBtn = UIButton(type: .custom)
+
+        onlyMusicBtn.setTitle("仅配乐", for: .normal)
+        onlyMusicBtn.titleLabel?.font = UIFont.systemFont(ofSize: 13, weight: .regular)
+        onlyMusicBtn.backgroundColor =  BFConfig.shared.pointEditNamalBackgroundColor
+        onlyMusicBtn.setTitleColor(UIColor.hexColor(hexadecimal: BFConfig.shared.styleColor.rawValue), for: .selected)
+        onlyMusicBtn.setTitleColor(UIColor.hexColor(hexadecimal: "#959595"), for: .normal)
+        onlyMusicBtn.addCorner(corner: 5)
+        onlyMusicBtn.tag = 3
+        onlyMusicBtn.addTarget(self, action: #selector(editModelClick1(sender:)), for: .touchUpInside)
+        onlyMusicBtn.adjustsImageWhenHighlighted = false
+        onlyMusicBtn.setImage(UIImage.moduleImage(named: BFConfig.shared.onlyMusicBtnImage_N, moduleName: "BFFramework", isAssets: false), for: .normal)
+        onlyMusicBtn.setImage(UIImage.moduleImage(named: BFConfig.shared.onlyMusicBtnImage_H, moduleName: "BFFramework", isAssets: false), for: .selected)
+        return onlyMusicBtn
+    }()
+
+    // 操作面板上的分割线
+    lazy var optionlineView: UIView = {
+        let optionlineView = UIView()
+        optionlineView.backgroundColor = BFConfig.shared.pointEditNamalBackgroundColor
+        return optionlineView
+    }()
+
+    // 固定速度 UI
+    lazy var speedSettingView: PQSpeedSettingView = {
+        let speedSetView = PQSpeedSettingView()
+        speedSetView.backgroundColor = .clear
+        speedSetView.selectSpeedCallBack = { [weak self] maxSpeed, minSpeed, selectIndex, isSettingPlayer in
+            BFLog(message: "固定maxSpeed is\(maxSpeed) minSpeed \(minSpeed)")
+            self?.musicEditBGView.pausePlayer()
+
+            if maxSpeed == -1.0, minSpeed == -1.0 {
+                self?.customSpeedSettingView.isHidden = false
+                self?.customSpeedSettingView.viewType = self?.speedSettingView.viewType ?? 2
+            } else {
+                if maxSpeed != 0.0 {
+                    // 更新最后一次选择的位置恢复时使用
+                    if self?.speedSettingView.viewType == 1 {
+                        self?.lastSpeedSelectIndex = selectIndex
+                        self?.modelSpeed_maxSpeed = maxSpeed
+                        self?.modelSpeed_minSpeed = minSpeed
+                    } else if self?.speedSettingView.viewType == 2 {
+                        self?.lastJumpSpeedSelectIndex = selectIndex
+                        self?.modelPoint_speed = maxSpeed
+                    } else {
+                        self?.lastCyclesSelectIndex = selectIndex
+                    }
+
+                } else {
+                    BFLog(message: "设置速度无效")
+                }
+            }
+            if isSettingPlayer {
+                self?.settingPlayerView()
+            }
+        }
+        return speedSetView
+
+    }()
+
+    // 自定义速度
+    lazy var customSpeedSettingView: PQCustomSpeedSettingView = {
+        let customSpeedSetView = PQCustomSpeedSettingView(frame: CGRect(x: 0, y: cScreenHeigth - 354, width: cScreenWidth, height: 354))
+        customSpeedSetView.isHidden = true
+        customSpeedSetView.selectSpeedCallBack = { [weak self, weak customSpeedSetView] maxSpeed, minSpeed, isJumpSpeedModel, isCancle in
+            if !isCancle {
+                BFLog(message: "自定义速度maxSpeed is\(maxSpeed) minSpeed \(minSpeed) \(isJumpSpeedModel)")
+                self?.musicEditBGView.pausePlayer()
+
+                // 自定定义的更新一下最后的选择位置
+                if self?.speedSettingView.viewType == 1 {
+                    self?.lastSpeedSelectIndex = -1
+                    self?.modelSpeed_maxSpeed = maxSpeed
+                    self?.modelSpeed_minSpeed = minSpeed
+                } else if self?.speedSettingView.viewType == 2 {
+                    self?.lastJumpSpeedSelectIndex = -1
+                    self?.modelPoint_speed = maxSpeed
+                } else {
+                    self?.lastCyclesSelectIndex = Int(maxSpeed - 1)
+                }
+
+                self?.settingPlayerView()
+                // 确认后 选中自定义
+                self?.speedSettingView.selectCustom()
+            } else {
+                // 取消后恢复上一次选择的位置
+                if self?.speedSettingView.viewType == 1 {
+                    self?.speedSettingView.setSelectItem(index: self?.lastSpeedSelectIndex ?? 0, isSettingPlayer: false)
+                } else if self?.speedSettingView.viewType == 2 {
+                    self?.speedSettingView.setSelectItem(index: self?.lastJumpSpeedSelectIndex ?? 0, isSettingPlayer: false)
+                } else {
+                    self?.speedSettingView.setSelectItem(index: self?.lastCyclesSelectIndex ?? 0, isSettingPlayer: false)
+                }
+
+                customSpeedSetView?.isHidden = true
+            }
+        }
+        return customSpeedSetView
+
+    }()
+
+    /// 音乐标题
+    lazy var musicNameView: UIView = {
+        let musicNameView = UIView()
+        musicNameView.addSubview(musicNameLab)
+        let nameWidth: CGFloat = musicNameLab.frame.width + (25 + cDefaultMargin * 3)
+        musicNameView.frame = CGRect(x: (view.frame.width - nameWidth) / 2, y: cDevice_iPhoneStatusBarHei + (cDevice_iPhoneNavBarHei - cDefaultMargin * 3) / 2, width: nameWidth, height: cDefaultMargin * 3)
+//        musicNameView.backgroundColor = UIColor.hexColor(hexadecimal: "#333333")
+        musicNameView.addCorner(corner: musicNameView.frame.height / 2)
+        let musicImageView = UIImageView()
+        musicImageView.tintColor = BFConfig.shared.styleTitleColor
+        musicImageView.image = UIImage.moduleImage(named: "stuckPoint_reCreate_music", moduleName: "BFFramework", isAssets: false)?.withRenderingMode(.alwaysTemplate)
+        musicImageView.frame = CGRect(x: musicNameView.frame.height / 2 - 5, y: (musicNameView.frame.height - 22) / 2, width: 22, height: 22)
+        musicNameView.addSubview(musicImageView)
+        musicNameLab.frame.origin.x = musicImageView.frame.maxX + 5
+        return musicNameView
+    }()
+
+    /// 音乐歌曲名称
+    lazy var musicNameLab: LMJHorizontalScrollText = {
+        let nameWidth: CGFloat = sizeWithText(text: "\(stuckPointMusicData?.musicName ?? "")", font: UIFont.systemFont(ofSize: 13), size: CGSize(width: view.frame.width - ((cDefaultMargin * 6 + 16 * 2) * 2) - (25 + cDefaultMargin * 3), height: cDefaultMargin * 3)).width
+        let musicNameLab = LMJHorizontalScrollText(frame: CGRect(x: 0, y: 0, width: nameWidth < cDefaultMargin * 4 ? cDefaultMargin * 4 : nameWidth, height: cDefaultMargin * 3))
+        musicNameLab.textColor = BFConfig.shared.styleTitleColor
+        musicNameLab.textFont = UIFont.systemFont(ofSize: 13)
+        musicNameLab.speed = 0.03
+        musicNameLab.moveDirection = LMJTextScrollMoveLeft
+        musicNameLab.moveMode = LMJTextScrollContinuous
+        if nameWidth < cDefaultMargin * 4 {
+            musicNameLab.text = " \(stuckPointMusicData?.musicName ?? "") "
+        } else {
+            musicNameLab.text = " \(stuckPointMusicData?.musicName ?? "") "
+        }
+        return musicNameLab
+    }()
+
+    /// 同步进度显示
+    lazy var synchroMarskView: PQStuckPointLoadingView = {
+        var synchroMarskView = PQStuckPointLoadingView(frame: CGRect(x: 0, y: 0, width: cScreenWidth, height: cScreenHeigth))
+        synchroMarskView.cancelHandle = { [weak self] _ in
+            self?.navigationController?.popViewController(animated: true)
+        }
+        return synchroMarskView
+    }()
+
+    @objc func willEnterForeground() {
+        BFLog(message: "进入到前台")
+        if navigationController?.topViewController == self {
+            if projectModel.sData!.sections.count > 0 {
+                settingPlayerView()
+            } else {
+                prepareMeta()
+            }
+        }
+    }
+
+    @objc func enterBackground() {
+        BFLog(message: "进入到后台")
+        // 取消导出
+        if navigationController?.topViewController == self {
+            playerView.pause()
+        }
+    }
+
+    override func backBtnClick() {
+        super.backBtnClick()
+//        playerView.pause()
+        // 点击上报:返回按钮
+        PQEventTrackViewModel.baseReportUpload(businessType: .bt_buttonClick, objectType: .ot_click_back, pageSource: .sp_stuck_previewSyncedUp, extParams: nil, remindmsg: "卡点视频数据上报-(点击上报:返回按钮)")
+    }
+
+    // 使用选择音乐 调用情况:1,操作面板直接选择 ,2 搜索界面点击使用
+    func userstuckPointMusic(musicData: PQVoiceModel?) {
+        // 1,音乐面板点击了使用
+        stuckPointMusicData = musicData
+        // 2,同步最新音乐数据
+        synchroMusicInfoData(resetSelectIndex: false)
+
+        // 3,更新音乐标题UI
+        let nameWidth: CGFloat = sizeWithText(text: "\(stuckPointMusicData?.musicName ?? "")", font: UIFont.systemFont(ofSize: 13), size: CGSize(width: view.frame.width - ((cDefaultMargin * 6 + 16 * 2) * 2) - (25 + cDefaultMargin * 3), height: cDefaultMargin * 3)).width
+
+        if nameWidth < cDefaultMargin * 4 {
+            musicNameLab.text = " \(stuckPointMusicData?.musicName ?? "") "
+        } else {
+            musicNameLab.text = " \(stuckPointMusicData?.musicName ?? "") "
+        }
+
+        // 更新一下节奏的 UI
+        sustomSwitchView.selectOneBtn(Index: stuckPointMusicData?.speed ?? 2)
+    }
+
+    // 点击搜索音乐
+    @objc func musicSearchBtnClick(sender _: UIButton) {
+        let searchVC = PQEditMusicSearchController()
+
+        searchVC.btnClickHandle = { [weak self] _, bgmData in
+            // 使用音乐
+            BFLog(message: "搜索音乐点击了使用")
+            self?.musicEditBGView.insertSearchMusic(model: bgmData as! PQVoiceModel)
+            self?.userstuckPointMusic(musicData: bgmData as? PQVoiceModel)
+        }
+
+        let navigationController: UINavigationController = UINavigationController(rootViewController: searchVC)
+        navigationController.modalPresentationStyle = .fullScreen
+        present(navigationController, animated: true, completion: nil)
+    }
+
+    // 卡点编辑
+    @objc func pointEditerBtnClick(sender: UIButton) {
+        if sender.isSelected { return }
+        sender.isSelected = !sender.isSelected
+        musicEditerBtn.isSelected = false
+        pointEditBGView.isHidden = false
+        musicEditBGView.isHidden = true
+
+        PQEventTrackViewModel.baseReportUpload(businessType: .bt_buttonClick, objectType: .ot_shanyinApp_musicVideoPreview_videoTab, pageSource: .sp_shanyinApp_main, extParams: nil, remindmsg: "")
+    }
+
+    // 音乐编辑
+    @objc func musicEditerBtnClick(sender: UIButton) {
+        if sender.isSelected { return }
+        sender.isSelected = !sender.isSelected
+        pointEditerBtn.isSelected = false
+        pointEditBGView.isHidden = true
+
+        musicEditBGView.showData()
+
+        PQEventTrackViewModel.baseReportUpload(businessType: .bt_buttonClick, objectType: .ot_shanyinApp_musicVideoPreview_musicTab, pageSource: .sp_shanyinApp_main, extParams: nil, remindmsg: "")
+    }
+
+    @objc func editModelClick1(sender: UIButton) {
+        editModelClick(sender: sender)
+    }
+
+    // 三种模式修改
+    @objc func editModelClick(sender: UIButton, reportLog: Bool = true) {
+//        if sender.isSelected {
+//            BFLog(message: "已经是选中状态")
+//            return  “”
+//        }
+
+        musicEditBGView.pausePlayer()
+        sharedImageProcessingContext.framebufferCache.purgeAllUnassignedFramebuffers()
+
+        if sender == jumpPointBtn, selectedTotalDuration < 6, selectedDataCount != selectedImageDataCount, reCreateVideoData == nil {
+            cShowHUB(superView: view, msg: "素材时长需要大于6秒才\n可选择“跳跃卡点”模式")
+            return
+        }
+        lastEditModelBtn?.isSelected = false
+        //设置取消选中的背景色
+        lastEditModelBtn?.backgroundColor = BFConfig.shared.pointEditNamalBackgroundColor
+        sender.isSelected = !sender.isSelected
+        lastEditModelBtn = sender
+        //设置选中的背景色
+        let styleColor = UIColor.hexColor(hexadecimal: BFConfig.shared.styleColor.rawValue)
+        lastEditModelBtn?.backgroundColor = UIColor(red: styleColor.rgbaf[0], green: styleColor.rgbaf[1], blue: styleColor.rgbaf[2], alpha: 0.15)
+
+        BFLog(message: "sender tag is \(sender.tag)")
+        // 2素材全是图片的时候三个模式都显示循环设置 UI
+        if selectedDataCount == selectedImageDataCount {
+            speedSettingView.viewType = 3
+            customSpeedSettingView.viewType = speedSettingView.viewType
+            speedSettingView.snp.remakeConstraints { make in
+                make.left.equalToSuperview().offset(16)
+                make.right.equalToSuperview()
+                make.top.equalTo(onlyMusicBtn.snp.bottom).offset(10)
+                make.height.equalTo(30)
+            }
+
+            speedSettingView.isHidden = false
+            speedTitleLab.isHidden = false
+            sustomSwitchView.isHidden = false
+            if lastCyclesSelectIndex != -1 {
+                speedSettingView.setSelectItem(index: lastCyclesSelectIndex, isSettingPlayer: false)
+            }
+
+        } else {
+            // 1 ui 调整
+            if sender.tag == 1 || sender.tag == 2 {
+                speedSettingView.viewType = sender.tag
+                customSpeedSettingView.viewType = speedSettingView.viewType
+
+                speedSettingView.snp.remakeConstraints { make in
+                    make.left.equalToSuperview().offset(16)
+                    make.right.equalToSuperview()
+                    make.top.equalTo(onlyMusicBtn.snp.bottom).offset(10)
+                    make.height.equalTo(sender.tag == 1 ? 44 : 30)
+                }
+                speedSettingView.isHidden = false
+                speedTitleLab.isHidden = false
+                sustomSwitchView.isHidden = false
+                if sender.tag == 1 { // 快慢速
+                    speedSettingView.setSelectItem(index: lastSpeedSelectIndex, isSettingPlayer: false, setDisable: (selectedTotalDuration < 6 && selectedDataCount != selectedImageDataCount) ? true : false)
+                } else if sender.tag == 2 { // 跳跃卡点
+                    speedSettingView.setSelectItem(index: lastJumpSpeedSelectIndex, isSettingPlayer: false)
+                }
+            } else {
+                speedTitleLab.isHidden = true
+                speedSettingView.isHidden = true
+                sustomSwitchView.isHidden = true
+            }
+        }
+
+        // 3 设置 btn 不同显示状态
+        var speedStuckBtnGifName = ""
+        var jumpPointBtnGifName = ""
+        if sender.tag == 1 { // 快慢速
+            speedStuckBtnGifName = "speedstuck_h_pq"
+            jumpPointBtnGifName = "jumpPoint_n_pq"
+            currentCreateStickersModel = .createStickersModelSpeed
+
+            if reportLog {
+                PQEventTrackViewModel.baseReportUpload(businessType: .bt_buttonClick, objectType: .ot_shanyinApp_musicVideoPreview_selectPatternSpeed, pageSource: .sp_shanyinApp_main, extParams: nil, remindmsg: "")
+            }
+
+        } else if sender.tag == 2 { // 跳跃卡点
+            speedStuckBtnGifName = "speedstuck_n_pq"
+            jumpPointBtnGifName = "jumpPoint_h_pq"
+            currentCreateStickersModel = .createStickersModelPoint
+
+            if reportLog {
+                PQEventTrackViewModel.baseReportUpload(businessType: .bt_buttonClick, objectType: .ot_shanyinApp_musicVideoPreview_selectPatternMusicVideo, pageSource: .sp_shanyinApp_main, extParams: nil, remindmsg: "")
+            }
+
+        } else if sender.tag == 3 { // 仅音乐
+            speedStuckBtnGifName = "speedstuck_n_pq"
+            jumpPointBtnGifName = "jumpPoint_n_pq"
+            currentCreateStickersModel = .createStickersModelOnlyMusic
+            if reportLog {
+                PQEventTrackViewModel.baseReportUpload(businessType: .bt_buttonClick, objectType: .ot_shanyinApp_musicVideoPreview_selectPatternBgm, pageSource: .sp_shanyinApp_main, extParams: nil, remindmsg: "")
+            }
+        }
+        settingPlayerView()
+    }
+
+    override func viewWillAppear(_ animated: Bool) {
+        super.viewWillAppear(animated)
+        lineView?.isHidden = true
+        DispatchQueue.main.async {
+            UIApplication.shared.isIdleTimerDisabled = true
+        }
+        musicNameLab.move()
+
+        PQNotification.addObserver(self, selector: #selector(enterBackground), name: UIApplication.didEnterBackgroundNotification, object: nil)
+        PQNotification.addObserver(self, selector: #selector(willEnterForeground), name: UIApplication.willEnterForegroundNotification, object: nil)
+
+        // 从分享返回后从重初始化播放器
+        if isClickNextBtn {
+            isClickNextBtn = false
+            settingPlayerView()
+        }
+    }
+
+    override func viewWillDisappear(_ animated: Bool) {
+        super.viewWillDisappear(animated)
+        DispatchQueue.main.async {
+            UIApplication.shared.isIdleTimerDisabled = false
+        }
+        musicNameLab.stop()
+        playerView.stop()
+
+        musicEditBGView.pausePlayer()
+        PQNotification.removeObserver(self)
+
+        synchroMarskView.removeMarskView()
+    }
+
+    override func viewDidLoad() {
+        super.viewDidLoad()
+        leftButton(image: nil, tintColor: BFConfig.shared.styleTitleColor)
+        navHeadImageView?.addSubview(nextBtn)
+        navHeadImageView?.addSubview(musicNameView)
+
+        // 添加子视图
+        addSubViews()
+
+        prepareMeta()
+
+        // 曝光上报:预览页面曝光上报
+        PQEventTrackViewModel.baseReportUpload(businessType: .bt_windowView, objectType: .ot_view_previewSyncedUp, pageSource: .sp_stuck_previewSyncedUp, extParams: nil, remindmsg: "卡点视频数据上报-(曝光上报:预览页面曝光上报)")
+
+        // 从选择的素材中 第一个素材设置封面
+        if selectedPhotoData != nil, selectedPhotoData!.count > 0 {
+            let photo = selectedPhotoData!.first!
+            let option = PHImageRequestOptions()
+            option.isNetworkAccessAllowed = true // 允许下载iCloud的图片
+            option.resizeMode = .none
+            option.deliveryMode = .highQualityFormat
+            PHImageManager.default().requestImage(for: photo, targetSize: CGSize(width: 1920, height: 1920), contentMode: .aspectFill, options: option) { [weak self] image, _ in
+                // image就是图片
+                if image != nil {
+                    self?.firstFrameImage = image
+                }
+            }
+        }
+    }
+
+    override func viewDidLayoutSubviews() {
+        super.viewDidLayoutSubviews()
+        playerView.resetCanvasFrame(frame: coculationPlayViewRect())
+    }
+
+    func prepareMeta() {
+        // 导出相册视频
+        exportPhotoData()
+        // 同步音乐数据
+        synchroMusicInfoData()
+
+        // 插入选择的音乐信息
+        musicEditBGView.firstInsertVoiceModel = stuckPointMusicData!
+    }
+
+    /// 添加子视图
+    /// - Returns: <#description#>
+    func addSubViews() {
+        if (stuckPointMusicData?.rhythmSdata.count ?? 0) <= 0 {
+            return
+        }
+        view.addSubview(playerView)
+        view.addSubview(pointEditBGView)
+        view.addSubview(musicEditBGView)
+        view.addSubview(optionlineView)
+
+        view.addSubview(pointEditerBtn)
+        view.addSubview(musicEditerBtn)
+        view.addSubview(customSpeedSettingView)
+
+        // 卡点
+        pointEditBGView.addSubview(pointEditRemindLab)
+        pointEditBGView.addSubview(speedTitleLab)
+        pointEditBGView.addSubview(speedStuckBtn)
+        pointEditBGView.addSubview(jumpPointBtn)
+        pointEditBGView.addSubview(onlyMusicBtn)
+        pointEditBGView.addSubview(speedSettingView)
+        pointEditBGView.addSubview(sustomSwitchView)
+
+        // 音乐
+        musicEditBGView.addSubview(stuckPointCuttingView)
+
+        pointEditerBtn.snp.makeConstraints { make in
+            make.left.equalToSuperview().offset(100)
+            make.bottom.equalToSuperview().offset(-(14 + cAKSafeAreaHeight))
+            make.height.equalTo(24)
+            make.width.equalTo(24)
+        }
+
+        musicEditerBtn.snp.makeConstraints { make in
+            make.right.equalToSuperview().offset(-100)
+            make.top.equalTo(pointEditerBtn.snp.top)
+            make.height.equalTo(24)
+            make.width.equalTo(24)
+        }
+        pointEditBGView.snp.makeConstraints { make in
+            make.left.right.equalTo(view)
+            make.bottom.equalTo(pointEditerBtn.snp.top).offset(-7)
+            make.height.equalTo(290)
+        }
+        musicEditBGView.snp.makeConstraints { make in
+            make.left.right.equalToSuperview()
+            make.bottom.equalTo(pointEditerBtn.snp.top).offset(-7)
+            make.height.equalTo(290)
+        }
+
+        optionlineView.snp.makeConstraints { make in
+            make.left.right.equalToSuperview()
+            make.bottom.equalTo(pointEditBGView.snp.bottom).offset(-1)
+            make.height.equalTo(1)
+        }
+
+        stuckPointCuttingView.snp.makeConstraints { make in
+            make.left.right.equalToSuperview()
+            make.bottom.equalTo(musicEditBGView.snp.bottom).offset(-1)
+            make.height.equalTo(85)
+        }
+        pointEditRemindLab.snp.makeConstraints { make in
+            make.left.equalToSuperview().offset(16)
+            make.top.equalToSuperview().offset(16)
+            make.height.equalTo(20)
+            make.width.equalTo(80)
+        }
+        speedStuckBtn.snp.makeConstraints { make in
+            make.left.equalToSuperview().offset(16)
+            make.top.equalTo(pointEditRemindLab.snp.bottom).offset(8)
+            make.height.equalTo(80)
+            make.width.equalTo(80)
+        }
+
+        jumpPointBtn.snp.makeConstraints { make in
+            make.left.equalTo(speedStuckBtn.snp.right).offset(10)
+            make.top.equalTo(speedStuckBtn.snp.top)
+            make.height.equalTo(80)
+            make.width.equalTo(80)
+        }
+
+        onlyMusicBtn.snp.makeConstraints { make in
+            make.left.equalTo(jumpPointBtn.snp.right).offset(10)
+            make.top.equalTo(speedStuckBtn.snp.top)
+            make.height.equalTo(80)
+            make.width.equalTo(64)
+        }
+
+        // 重新设置三个模式 btn 图片和title的位置
+        speedStuckBtn.titleEdgeInsets = UIEdgeInsets(top: 0, left: -(speedStuckBtn.imageView?.frame.size.width ?? 0), bottom: -(speedStuckBtn.imageView?.frame.size.height ?? 0), right: 0)
+        speedStuckBtn.imageEdgeInsets = UIEdgeInsets(top: -(speedStuckBtn.titleLabel?.intrinsicContentSize.height ?? 0), left: 0, bottom: 0, right: -(speedStuckBtn.titleLabel?.intrinsicContentSize.width ?? 0))
+
+        jumpPointBtn.titleEdgeInsets = UIEdgeInsets(top: 0, left: -(jumpPointBtn.imageView?.frame.size.width ?? 0), bottom: -(jumpPointBtn.imageView?.frame.size.height ?? 0), right: 0)
+        jumpPointBtn.imageEdgeInsets = UIEdgeInsets(top: -(jumpPointBtn.titleLabel?.intrinsicContentSize.height ?? 0), left: 0, bottom: 0, right: -(jumpPointBtn.titleLabel?.intrinsicContentSize.width ?? 0))
+
+        onlyMusicBtn.titleEdgeInsets = UIEdgeInsets(top: 0, left: -(onlyMusicBtn.imageView?.frame.size.width ?? 0), bottom: -(onlyMusicBtn.imageView?.frame.size.height ?? 0), right: 0)
+        onlyMusicBtn.imageEdgeInsets = UIEdgeInsets(top: -(onlyMusicBtn.titleLabel?.intrinsicContentSize.height ?? 0), left: 0, bottom: 0, right: -(onlyMusicBtn.titleLabel?.intrinsicContentSize.width ?? 0))
+
+        speedSettingView.snp.makeConstraints { make in
+            make.left.equalToSuperview().offset(16)
+            make.right.equalToSuperview()
+            make.top.equalTo(onlyMusicBtn.snp.bottom).offset(10)
+            make.height.equalTo(44)
+        }
+        speedTitleLab.snp.makeConstraints { make in
+            make.left.equalToSuperview().offset(16)
+            make.top.equalToSuperview().offset(190)
+            make.height.equalTo(20)
+            make.width.equalTo(80)
+        }
+        sustomSwitchView.snp.makeConstraints { make in
+            make.left.equalToSuperview().offset(16)
+            make.top.equalTo(speedTitleLab.snp.bottom).offset(8)
+            make.height.equalTo(30)
+            make.width.equalTo(180)
+        }
+    }
+
+    @objc func nextBtnClick(sender _: UIButton) {
+        BFLog(message: "去发布")
+
+        isClickNextBtn = true
+        playerView.pause()
+        // 使用深 copy
+        let json = projectModel.toJSONString(prettyPrint: false)
+
+        if json == nil {
+            BFLog(message: "数据转换有问题 跳转")
+            return
+        }
+        let tempModel: PQEditProjectModel? = Mapper<PQEditProjectModel>().map(JSONString: json!)
+        let materialVC: PQStuckPointMaterialController? = navigationController?.viewControllers.first(where: { (vc) -> Bool in
+            vc is PQStuckPointMaterialController
+        }) as? PQStuckPointMaterialController
+
+        if materialVC != nil, materialVC?.isToPublicHandle != nil {
+            materialVC?.isToPublicHandle!(isReCreate, selectedTotalDuration, selectedDataCount, selectedImageDataCount, mStickers, stuckPointMusicData, tempModel, currentCreateStickersModel, modelSpeed_maxSpeed, modelSpeed_minSpeed, Float(finallyStuckPoints.last ?? 0) - Float(finallyStuckPoints.first ?? 0), getClipAudioRange(), playeTimeRange)
+        } else {
+            if finallyStuckPoints.count == 0 {
+                cShowHUB(superView: nil, msg: "无卡点信息,返回重新选择音乐")
+                return
+            }
+            let videoExporter = PQStuckPointPublicController()
+            videoExporter.rhythmMode = currentCreateStickersModel
+            videoExporter.syncedUpVideoSpeedMin = modelSpeed_maxSpeed
+            videoExporter.syncedUpVideoSpeedMax = modelSpeed_minSpeed
+            videoExporter.isReCreate = isReCreate
+            videoExporter.selectedTotalDuration = selectedTotalDuration
+            videoExporter.selectedDataCount = selectedDataCount
+            videoExporter.selectedImageDataCount = selectedImageDataCount
+            videoExporter.finallyUserAudioTime = Float(finallyStuckPoints.last ?? 0) - Float(finallyStuckPoints.first ?? 0)
+            videoExporter.clipAudioRange = getClipAudioRange()
+            videoExporter.playeTimeRange = playeTimeRange
+            videoExporter.mStickers = mStickers
+            videoExporter.audioMixModel = stuckPointMusicData
+            videoExporter.coverImage = selectedMetarialData?.first?.coverImageUI ?? UIImage.init()
+            videoExporter.editProjectModel = tempModel
+            navigationController?.pushViewController(videoExporter, animated: true)
+        }
+        // 点击上报:去合成
+        PQEventTrackViewModel.baseReportUpload(businessType: .bt_buttonClick, objectType: .ot_click_commit, pageSource: .sp_stuck_previewSyncedUp, extParams: ["musicName": stuckPointMusicData?.musicName ?? "", "musicId": stuckPointMusicData?.musicId ?? "", "rhythmNumber": stuckPointMusicData?.speed ?? 2, "duration": ((stuckPointMusicData?.endTime ?? 0) - (stuckPointMusicData?.startTime ?? 0)) * 1000], remindmsg: "点击上报:去合成")
+    }
+
+    override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
+        super.touchesBegan(touches, with: event)
+        if touches.first?.view != customSpeedSettingView {
+            if !customSpeedSettingView.isHidden {
+                customSpeedSettingView.isHidden = true
+            }
+        }
+    }
+
+    // MARK: - 播放器相关操作
+
+    /// seek 播放器
+    /// - Parameter playeTimeRange: 开始和结束时间
+    func seekPlayer(playeTimeRange: CMTimeRange) {
+        playerView.setEnableSeek(isSeek: true)
+        playerView.play(pauseFirstFrame: false, playeTimeRange: playeTimeRange)
+    }
+
+    /// 通过传入的    selectedPhotoData 、 stuckPointMusicData 创建 projectModel 模型 后面都使用 projectModel 参数
+    func createPorjectData() {
+        // 1,添加选择的视觉素材
+        if projectModel.sData?.sections.count == 0 {
+            let section: PQEditSectionModel = PQEditSectionModel()
+            selectedMetarialData?.forEach { model in
+//                let json = model.toJSONString(prettyPrint: false)
+//                if json == nil {
+//                    BFLog(message: "数据转换有问题 跳转")
+//                    return
+//                }
+//                let tempModel: PQEditVisionTrackMaterialsModel = Mapper<PQEditVisionTrackMaterialsModel>().map(JSONString: json!)!
+                section.sectionTimeline?.visionTrack?.visionTrackMaterials.append(model)
+            }
+            projectModel.sData?.sections.append(section)
+        }
+
+        // 2,添加背景音乐
+        projectModel.sData?.addBGM(audioMix: stuckPointMusicData!)
+    }
+
+    // 计算拼接音乐的开始和结束点
+    func getClipAudioRange() -> CMTimeRange {
+        // 设置音乐的拼接范围,开始:推荐的卡点  结束:点到的倒数第二位
+        if stuckPointMusicData!.rhythmSdata.count > 0, stuckPointMusicData?.rhythmSdata[0].pointTimes.count ?? 0 > 2 {
+            let lastSecondPoint = Float(stuckPointMusicData!.rhythmSdata[0].pointTimes[stuckPointMusicData!.rhythmSdata[0].pointTimes.count - 2]) / Float(BASE_FILTER_TIMESCALE)
+            let clipAudioRange =
+                CMTimeRange(start: CMTime(value: CMTimeValue(Float(stuckPointMusicData?.startTime ?? 0) * Float(BASE_FILTER_TIMESCALE)), timescale: BASE_FILTER_TIMESCALE), end: CMTime(value: CMTimeValue(Float(lastSecondPoint) * Float(BASE_FILTER_TIMESCALE)), timescale: BASE_FILTER_TIMESCALE))
+
+            return clipAudioRange
+        }
+        return CMTimeRange(start: CMTime(value: 0, timescale: 1), duration: CMTime(value: 1, timescale: 1))
+    }
+
+    // 设置播放器
+    func coculationPlayViewRect() -> CGRect {
+        let playerShowHeight = pointEditBGView.frame.minY - (navHeadImageView?.frame.maxY ?? 0)
+        var showRect: CGRect = CGRect(x: (cScreenWidth - playerShowHeight) / 2, y: 0, width: playerShowHeight, height: playerShowHeight)
+        if firstFrameImage != nil {
+            let w = firstFrameImage!.size.width
+            let h = firstFrameImage!.size.height
+            let ratioMaterial: Float = Float(w / max(h, 1))
+            if ratioMaterial > 1 {
+                // 横屏
+                let tempPlayerHeight = min(cScreenWidth * CGFloat(h / w), playerShowHeight)
+                showRect = CGRect(x: (cScreenWidth - tempPlayerHeight * CGFloat(ratioMaterial)) / 2, y: (playerShowHeight - tempPlayerHeight) / 2, width: tempPlayerHeight * CGFloat(ratioMaterial), height: tempPlayerHeight)
+            } else {
+                // 竖屏
+                let playerViewWidth = (CGFloat(w) / CGFloat(h)) * playerShowHeight
+                showRect = CGRect(x: (cScreenWidth - playerViewWidth) / 2, y: 0, width: playerViewWidth, height: playerShowHeight)
+            }
+        }
+        if showRect.size.width == showRect.size.height {
+            if cScreenWidth > playerShowHeight {
+                showRect.origin.x = (cScreenWidth - playerShowHeight) / 2
+                showRect.size.width = playerShowHeight
+                showRect.size.height = playerShowHeight
+            } else {
+                showRect.origin.x = 0
+                showRect.size.width = cScreenWidth
+                showRect.size.height = cScreenWidth
+            }
+        }
+
+        if showRect.size.width != 0, showRect.size.height != 0 {
+            if Int(showRect.height) % 2 != 0 {
+                showRect.size.height = showRect.size.height + 1.0
+            }
+            if Int(showRect.width) % 2 != 0 {
+                showRect.size.width = showRect.size.width + 1.0
+            }
+        }
+        showRect.origin.y = (playerShowHeight - showRect.size.height) / 2.0 + (navHeadImageView?.frame.maxY ?? 0)
+
+        return showRect
+    }
+
+    func settingPlayerView() {
+        stuckPointCuttingView.resetDefaultsColor()
+        synchroMarskView.show()
+        // 1,设置播放器的显示区域 和画布大小
+        //  - 按第一个素材尺寸自适应
+        playerView.pause()
+
+        var firstModel: PQEditVisionTrackMaterialsModel?
+        for part in projectModel.sData!.sections {
+            if part.sectionTimeline?.visionTrack?.getEnableVisionTrackMaterials().count ?? 0 > 0 {
+                firstModel = part.sectionTimeline?.visionTrack?.getEnableVisionTrackMaterials().first
+                break
+            }
+        }
+
+        var videoSize: CGSize = CGSize(width: Int(firstModel?.width ?? 0), height: Int(firstModel?.height ?? 0))
+
+        var minSlider = min(videoSize.width, videoSize.height)
+        var maxSlider = max(videoSize.width, videoSize.height)
+        let ration = 1080 / minSlider
+        minSlider = minSlider * ration
+        maxSlider = maxSlider * ration
+        if videoSize.width > videoSize.height { // 宽屏
+            videoSize = CGSize(width: maxSlider, height: minSlider)
+        } else {
+            videoSize = CGSize(width: minSlider, height: maxSlider)
+        }
+        if videoSize.width.isNaN || videoSize.height.isNaN {
+            BFLog(1, message: "宽高无效NaN")
+            return
+        }
+
+        let maxValue = max(videoSize.width, videoSize.height)
+
+        if maxValue > 1920 {
+            let maxRation = 1920 / maxValue
+
+            videoSize = CGSize(width: videoSize.width * CGFloat(maxRation), height: videoSize.height * CGFloat(maxRation))
+
+            BFLog(message: "最长边已经超过 1920 要等比缩小 缩放后\(videoSize)")
+        }
+
+        if (Int(videoSize.width) % 2) != 0 {
+            videoSize.width = videoSize.width - 1
+        }
+        if (Int(videoSize.height) % 2) != 0 {
+            videoSize.height = videoSize.height - 1
+        }
+
+        projectModel.sData?.videoMetaData?.videoWidth = Int(videoSize.width)
+        projectModel.sData?.videoMetaData?.videoHeight = Int(videoSize.height)
+
+        let beginTime = Date()
+        dealParameter(model: currentCreateStickersModel)
+
+        // 更新裁剪时间条的的ui数据
+        stuckPointCuttingView.videoDuration = max(CGFloat(finallyUserAudioTime), CGFloat(finallyStuckPoints.last!))
+        let counn = (stuckPointMusicData?.rhythmSdata[0].pointTimes.count)! - 2
+        let suggestRhythmStartTime = CGFloat(stuckPointMusicData?.suggestRhythmStartTime ?? 0)
+        let suggestRhythmEndTime = max(suggestRhythmStartTime, CGFloat(stuckPointMusicData?.rhythmSdata[0].pointTimes[max(counn, 0)] ?? 0) / CGFloat(BASE_FILTER_TIMESCALE))
+        stuckPointCuttingView.updateEndTime(
+            startTime: CGFloat(CMTimeGetSeconds(playeTimeRange.start)),
+            endTime: CGFloat(CMTimeGetSeconds(playeTimeRange.end)),
+            suggestRhythmStartTime: suggestRhythmStartTime,
+            suggestRhythmEndTime: suggestRhythmEndTime
+        )
+
+        // 2,创建滤镜
+        DispatchQueue.global().async {
+            self.mStickers = self.createStickers(sections: self.projectModel.sData?.sections ?? List(), inputSize: CGSize(width: CGFloat(self.projectModel.sData?.videoMetaData?.videoWidth ?? 0), height: CGFloat(self.projectModel.sData?.videoMetaData?.videoHeight ?? 0)), model: self.currentCreateStickersModel)
+            DispatchQueue.main.async { // 串行、异步
+                self.playerView.mStickers = self.mStickers
+
+                BFLog(message: "createStickers tiskskskskme  \(Date().timeIntervalSince(beginTime))")
+
+                // 3,设置音频
+                let audioPath = self.stuckPointMusicData?.localPath ?? ""
+
+                BFLog(message: "初始化音频播放器的音频地址为:\(audioPath)")
+                self.playerView.stop()
+                // 这里的测试这个音乐播放有问题
+                //        self.playerView.updateAsset(URL(fileURLWithPath: "63930549652d74e477141e3b79c8d29a9ef8af81625053214516.mp3", relativeTo:Bundle.main.resourceURL!), videoComposition: nil, audioMixModel: nil)
+
+                self.playerView.updateAsset(URL(fileURLWithPath: documensDirectory + audioPath), videoComposition: nil, audioMixModel: nil, originMusicDuration: self.finallyUserAudioTime, clipAudioRange: self.getClipAudioRange(), isUsedAVPlayer: true)
+
+                // 4, 设置播放器的输出画布大小
+                self.playerView.movie?.mShowVidoSize = CGSize(width: CGFloat(self.projectModel.sData?.videoMetaData?.videoWidth ?? 0), height: CGFloat(self.projectModel.sData?.videoMetaData?.videoHeight ?? 0))
+
+                // 传给movie 音频的原始卡点
+                let fir = Int64(self.stuckPointsTemp.first ?? 0)
+                let endd = Int64(self.stuckPointsTemp.last ?? 0)
+                self.playerView.movie?.orginStuckRange = CMTimeRange(start: CMTime(value: Int64(fir) * Int64(BASE_FILTER_TIMESCALE), timescale: BASE_FILTER_TIMESCALE), end: CMTime(value: Int64(endd) * Int64(BASE_FILTER_TIMESCALE), timescale: BASE_FILTER_TIMESCALE))
+
+                // 5,开始播放
+                self.playerView.isLoop = false
+                self.playerView.showProgressLab = true
+
+                // 初始化音频的开始和结束时间
+                BFLog(message: "播放的器 开始\(String(describing: CMTimeGetSeconds(self.playeTimeRange.start))) 结束 \(String(describing: CMTimeGetSeconds(self.playeTimeRange.end)))")
+                let end3: TimeInterval = Date().timeIntervalSince1970
+
+                self.playerView.play(pauseFirstFrame: false, playeTimeRange: CMTimeRange(start: self.playeTimeRange.start, end: self.playeTimeRange.end))
+                self.stuckPointCuttingView.updateProgress(progress: 0)
+                self.synchroMarskView.removeMarskView()
+
+                let end4: TimeInterval = Date().timeIntervalSince1970
+                BFLog(message: " playerView.play tiskskskskme  \(end4 - end3)")
+
+                // 6,进度回调
+                self.playerView.progress = { [weak self] currentTime, tatolTime, percent in
+                    if percent == 1 {
+                        self?.stuckPointCuttingView.resetDefaultsColor(clearData: false)
+                        sharedImageProcessingContext.framebufferCache.purgeAllUnassignedFramebuffers()
+                        return
+                    }
+                    if CMTimeGetSeconds(self?.playeTimeRange.duration ?? .zero) <= 0.0 {
+                        BFLog(message: "时长错误!!!!")
+                        return
+                    }
+                    // 更新进度
+                    let progress = (currentTime - CMTimeGetSeconds(self?.playeTimeRange.start ?? .zero)) / CMTimeGetSeconds(self?.playeTimeRange.duration ?? .zero)
+                    BFLog(message: "\(currentTime) \(tatolTime) 显示播放器进度为: \(progress)")
+
+                    self?.stuckPointCuttingView.updateProgress(progress: CGFloat(progress))
+                }
+            }
+        }
+    }
+
+    deinit {
+        musicNameLab.stop()
+//        playerView.pause()
+        // 取消所有的导出
+        PQSingletoMemoryUtil.shared.allExportSession.forEach { _, exportSession in
+            exportSession.cancelExport()
+        }
+        self.synchroMarskView.removeMarskView()
+        sharedImageProcessingContext.framebufferCache.purgeAllUnassignedFramebuffers()
+        BFLog(1, message: "卡点视频预览界面release")
+    }
+}
+
+// MARK: - 视频渲染相关逻辑方法
+
+extension PQStuckPointEditerController {
+    /// 分割视频 这里只设置视频类型的 in 和 out 并不设置显示的开始和结束时间   mp4 ,png ,png ,mp4
+    /// - Parameter section: 当前段
+    /// - Parameter stuckPoints: 用户选择的,或推荐的卡点数
+    /// - Returns: 返回分割后的所有 stickers 和卡点数是一致的
+    func clipVideoMerage(section: PQEditSectionModel, stuckPoints: [Float]) -> [PQEditVisionTrackMaterialsModel] {
+        var stickers: Array = Array<PQEditVisionTrackMaterialsModel>.init()
+        // 第二种情况:有视频要进行分割
+        /*
+         1, 确定每个视频素材需要切的段数p
+         2, 将所有视频时长相加,得到总视频素材时长L = l1 + l2 + ... + ln
+         3, 视频素材a1需要切分的个数clipNum = max (round (kongduan * a1 / L) , 1)
+         */
+        // 要补的空位数
+        let kongduan: Int = Int(stuckPoints.count - 1) - selectedImageDataCount
+
+        // 所有视频总时长
+        var videoTotalDuration: Float64 = 0.0
+        for video in section.sectionTimeline!.visionTrack!.getEnableVisionTrackMaterials(type: "video") {
+            // MARK: SanW-2021.11.15-不再导出到沙盒,直接使用本地相册地址
+
+            let asset: AVURLAsset = AVURLAsset(url: URL(fileURLWithPath: video.locationPath), options: nil)
+//            let asset: AVURLAsset = AVURLAsset(url: URL(fileURLWithPath: documensDirectory + video.locationPath), options: nil)
+            videoTotalDuration = videoTotalDuration + Float64(CMTimeGetSeconds(asset.duration))
+        }
+        if videoTotalDuration == 0 {
+            BFLog(message: "视频总时长出现错误!!!!这里应该有视频素材的")
+            return stickers
+        }
+        for sticker in section.sectionTimeline!.visionTrack!.getEnableVisionTrackMaterials() {
+            if sticker.type == StickerType.VIDEO.rawValue {
+                // MARK: SanW-2021.11.15-不再导出到沙盒,直接使用本地相册地址
+
+                let asset: AVURLAsset = AVURLAsset(url: URL(fileURLWithPath: sticker.locationPath), options: nil)
+//                let asset: AVURLAsset = AVURLAsset(url: URL(fileURLWithPath: documensDirectory + sticker.locationPath), options: nil)
+                // 要分割的段落
+                let clipNum = Int(max(round(Double(kongduan) * CMTimeGetSeconds(asset.duration) / videoTotalDuration), 1))
+                sticker.duration = CMTimeGetSeconds(asset.duration)
+                BFLog(message: "单个视频\(sticker.locationPath)时长::\(CMTimeGetSeconds(asset.duration)) ,clipNum is:\(clipNum)")
+                for clipindex in 0 ..< clipNum {
+                    // deep copy sticker model 防止只有一个对象
+                    let deepCopySticker: PQEditVisionTrackMaterialsModel? = sticker.copy() as? PQEditVisionTrackMaterialsModel
+
+                    // 设置循环模式和适配模式
+                    deepCopySticker?.generateDefaultValues()
+
+                    deepCopySticker?.model_in = clipindex == 0 ? 0 : CMTimeGetSeconds(asset.duration) / Double(clipNum) * Double(clipindex)
+                    deepCopySticker?.out = (deepCopySticker?.model_in ?? 0) + CMTimeGetSeconds(asset.duration) / Double(clipNum)
+
+                    if (deepCopySticker?.model_in ?? 0) >= CMTimeGetSeconds(asset.duration) || (deepCopySticker?.out ?? 0) >= CMTimeGetSeconds(asset.duration) {
+                        deepCopySticker?.model_in = CMTimeGetSeconds(asset.duration) - CMTimeGetSeconds(asset.duration) / Double(clipNum)
+
+                        deepCopySticker?.out = CMTimeGetSeconds(asset.duration)
+                    }
+
+                    BFLog(message: " crilp is in \(deepCopySticker?.model_in ?? 0) out \(deepCopySticker?.out ?? 0) 总时长\(CMTimeGetSeconds(asset.duration))")
+
+                    if deepCopySticker != nil {
+                        stickers.append(deepCopySticker!)
+                    }
+                }
+            } else if sticker.type == StickerType.IMAGE.rawValue {
+                sticker.generateDefaultValues()
+                stickers.append(sticker)
+            }
+        }
+//        kongduan = clipNumTep
+        return stickers
+    }
+
+    // 更新 playeTimeRange & finallyUserAudioTime
+    func updateTimeInfomation() {
+        // 四,背景音乐时长处理)计算最后使用的音频时长, 如果不用拼接音频时长度是卡点的倒数第二位时间
+        let asset = AVURLAsset(url: URL(fileURLWithPath: documensDirectory + (stuckPointMusicData?.localPath ?? "")), options: nil)
+
+        // 原推荐卡点的倒数第二位时间
+        let lastSecondPoint = Float(stuckPointMusicData!.rhythmSdata[0].pointTimes[stuckPointMusicData!.rhythmSdata[0].pointTimes.count - 2]) / Float(BASE_FILTER_TIMESCALE)
+
+        finallyUserAudioTime = Float(lastSecondPoint)
+        if (finallyStuckPoints.last ?? 0) > Float(CMTimeGetSeconds(asset.duration)) {
+            finallyUserAudioTime = Float(finallyStuckPoints.last ?? 0) + (Float(CMTimeGetSeconds(asset.duration)) - Float(lastSecondPoint))
+        }
+
+        playeTimeRange = CMTimeRange(start: CMTime(value: CMTimeValue(Float64(finallyStuckPoints.first ?? 0) * Float64(BASE_FILTER_TIMESCALE)), timescale: BASE_FILTER_TIMESCALE), end: CMTime(value: CMTimeValue(Float64(finallyStuckPoints.last ?? 0) * Float64(BASE_FILTER_TIMESCALE)), timescale: BASE_FILTER_TIMESCALE))
+
+        for (index, usePoint) in finallyStuckPoints.enumerated() {
+            BFLog(message: "测试人员最后使用的卡点信息 \(index) : \(usePoint)")
+        }
+
+        BFLog(message: "计算后给播放器使用的开始:\(CMTimeGetSeconds(playeTimeRange.start)) 结束时间\(CMTimeGetSeconds(playeTimeRange.end)) 播放总时长:\(CMTimeGetSeconds(playeTimeRange.end) - CMTimeGetSeconds(playeTimeRange.start))")
+    }
+
+    /// 创建sticker
+    /// - Parameters:
+    ///   - sections: 项目所有段落数据信息
+    ///   - inputSize: 画布大小
+    /// - Returns: filters 数据 播放器可直接使用
+    func createStickers(sections: List<PQEditSectionModel>, inputSize _: CGSize = .zero, model: createStickersModel = .createStickersModelPoint) -> [PQEditVisionTrackMaterialsModel] {
+        // 推荐卡点数
+        let beginDecoderTime: TimeInterval = Date().timeIntervalSince1970
+
+        // 保存滤镜对象数据
+        var stickers: Array = Array<PQEditVisionTrackMaterialsModel>.init()
+        for section in sections {
+            if section.sectionType == "normal" {
+                // 第一种情况:全是图片,三种模式都进行图片回环播放
+                if section.sectionTimeline!.visionTrack!.getEnableVisionTrackMaterials(type: "video").count == 0, section.sectionTimeline!.visionTrack!.getEnableVisionTrackMaterials(type: "image").count > 0 {
+                    for (index, _) in finallyStuckPoints.enumerated() {
+                        let sticker: PQEditVisionTrackMaterialsModel = section.sectionTimeline!.visionTrack!.getEnableVisionTrackMaterials()[index % section.sectionTimeline!.visionTrack!.getEnableVisionTrackMaterials().count]
+                        BFLog(message: "stickerlocationPath sticker : \(sticker.locationPath)")
+//
+                        let deepCopySticker: PQEditVisionTrackMaterialsModel? = sticker.copy() as? PQEditVisionTrackMaterialsModel
+
+                        if deepCopySticker!.type == StickerType.IMAGE.rawValue {
+                            if index + 1 < finallyStuckPoints.count {
+                                deepCopySticker!.timelineIn = Float64(String(format: "%.6f", finallyStuckPoints[index])) ?? 0.0
+
+                                deepCopySticker!.timelineOut = Float64(String(format: "%.6f", finallyStuckPoints[index + 1])) ?? 0.0
+                                if deepCopySticker != nil {
+                                    deepCopySticker?.generateDefaultValues()
+                                    stickers.append(deepCopySticker!)
+
+                                    BFLog(1, message: "测试人员index is 纯图片 timelineOut:\(deepCopySticker!.timelineIn) timelineOut :\(deepCopySticker!.timelineOut)")
+                                }
+                            }
+                        }
+                    }
+                } else { // 第二种情况:视频 + 图片情况、视频要分段+图片按卡点时长创建
+                    if model == .createStickersModelPoint { // 跳跃卡点
+                        // 第二种情况:有视频要进行分割
+                        let clipFilters = clipVideoMerage(section: section, stuckPoints: finallyStuckPoints)
+                        // 数据不一致时要对数据进行二次处理,不是最好方案
+                        if clipFilters.count > finallyStuckPoints.count - 1 {
+                            clipPoint(clipNum: clipFilters.count - finallyStuckPoints.count, oldPoints: finallyStuckPoints)
+
+                        } else if clipFilters.count < finallyStuckPoints.count - 1 {
+                            while clipFilters.count < finallyStuckPoints.count - 1 {
+                                finallyStuckPoints.removeLast()
+                            }
+                        }
+                        // 更新最终使用值
+                        updateTimeInfomation()
+                        // stikcer段数比clipFilters 数 大于 1才是正确的
+                        BFLog(message: "stikcer count is\(clipFilters.count) finallyStuckPoints count is\(finallyStuckPoints.count)")
+                        for (index, point) in finallyStuckPoints.enumerated() {
+                            BFLog(message: "aaaaaindexindeindexxindexindexindex \(index) \(point)")
+                            if index + 1 < finallyStuckPoints.count, index < clipFilters.count {
+                                BFLog(message: "bbbbbindexindeindexxindexindexindex \(index) \(point)")
+                                let sticker: PQEditVisionTrackMaterialsModel = clipFilters[index]
+
+                                if sticker.type == StickerType.IMAGE.rawValue {
+                                    BFLog(message: "当前是image filter !!!!!")
+                                }
+                                sticker.timelineIn = Float64(String(format: "%.6f", finallyStuckPoints[index])) ?? 0.0
+
+                                sticker.timelineOut = Float64(String(format: "%.6f", finallyStuckPoints[index + 1])) ?? 0.0
+                                // 卡点的时间 >  in out 值 这里就会出现鬼畜效果
+                                let timelineInterval = sticker.timelineOut - sticker.timelineIn
+                                let inOutInterval = sticker.out - sticker.model_in
+                                if timelineInterval > inOutInterval {
+                                    BFLog(message: "实际要显示卡点时长\(timelineInterval) 素材裁剪时长:\(inOutInterval)")
+                                    sticker.out = sticker.model_in + timelineInterval
+
+                                    // 下面只是 LOG 方便查问题
+                                    let stickerInOut = sticker.out - sticker.model_in
+                                    let stickerTimelineInOut = sticker.timelineOut - sticker.timelineIn
+                                    if stickerInOut != stickerTimelineInOut {
+                                        BFLog(message: "sticker.timelineIn \(sticker.timelineIn) stickerTimelineInOut is\(stickerTimelineInOut) stickerInOut is\(stickerInOut) 相差\(stickerTimelineInOut - stickerInOut)")
+                                    }
+                                }
+
+                                // out > 素材的总时长in out 进行前移操作
+                                let offsetAssetDuration = sticker.out - sticker.duration
+                                if offsetAssetDuration > 0 {
+                                    sticker.model_in = sticker.model_in - offsetAssetDuration
+                                    sticker.out = sticker.out - offsetAssetDuration
+                                }
+                                print("跳跃卡点测试人员index is \(index)分割后 创建 filter timelineIn :\(sticker.timelineIn) timelineOut :\(sticker.timelineOut)  in :\(sticker.model_in) out:\(sticker.out) type is \(sticker.type) 显示总时长为:\(sticker.timelineOut - sticker.timelineIn)  裁剪总时长\(sticker.out - sticker.model_in)")
+
+                                stickers.append(sticker)
+                            }
+                        }
+                    } else if model == .createStickersModelOnlyMusic || model == .createStickersModelSpeed { // 仅音乐 和 快慢速卡点
+                        BFLog(message: "stuckPoints count is \(finallyStuckPoints.count)")
+
+                        for sticker in section.sectionTimeline!.visionTrack!.getEnableVisionTrackMaterials() {
+                            if sticker.type == StickerType.VIDEO.rawValue {
+                                // MARK: SanW-2021.11.15-不再导出到沙盒,直接使用本地相册地址
+
+                                let asset: AVURLAsset = AVURLAsset(url: URL(fileURLWithPath: sticker.locationPath), options: nil)
+//                                let asset: AVURLAsset = AVURLAsset(url: URL(fileURLWithPath: documensDirectory + sticker.locationPath), options: nil)
+                                BFLog(message: "单个视频\(sticker.locationPath)时长::\(CMTimeGetSeconds(asset.duration)) ,clipNum is:\(sticker.clipCount)")
+                                var lastOutTime: Float64 = 0.0
+                                for _ in 1 ... sticker.clipCount {
+                                    // deep copy sticker model 防止只有一个对象
+                                    let deepCopyStickerDecoderTime: TimeInterval = Date().timeIntervalSince1970
+
+                                    let deepCopySticker: PQEditVisionTrackMaterialsModel? = sticker.copy() as? PQEditVisionTrackMaterialsModel
+
+                                    BFLog(message: "生成stickers 总时长为 aaa\(Date().timeIntervalSince1970 - deepCopyStickerDecoderTime)")
+                                    // 设置循环模式和适配模式
+                                    deepCopySticker?.generateDefaultValues()
+                                    deepCopySticker?.materialDurationFit?.fitType = adapterMode.staticFrame.rawValue
+                                    // 当前分段的速度
+                                    var tempSpeed: Float = 1.0
+                                    if model == .createStickersModelSpeed {
+                                        tempSpeed = (stickers.count % 2) == 0 ? modelSpeed_maxSpeed : modelSpeed_minSpeed
+                                    }
+
+                                    if stickers.count + 1 < finallyStuckPoints.count {
+                                        deepCopySticker?.speedRate = tempSpeed
+                                        // 定义临时使用的变量
+                                        // 素材显示的开始时间和结束时间
+                                        let tempTimelineIn: Float64 = Float64(String(format: "%.6f", finallyStuckPoints[stickers.count])) ?? 0.0
+
+                                        let timelineOut: Float64 = Float64(String(format: "%.6f", finallyStuckPoints[stickers.count + 1])) ?? 0.0
+
+                                        // 素材分割的开始时间和结束时间
+                                        let tempModel_In = lastOutTime
+                                        var tempOut = lastOutTime + Float64(tempSpeed) * (timelineOut - tempTimelineIn)
+
+                                        // 处理最后一点视频素材不够卡点时长 e.g. 0.3 卡点时长0.5
+                                        if tempOut > CMTimeGetSeconds(asset.duration) {
+                                            // 最后一点素材时长
+                                            let lastAssetDuration = CMTimeGetSeconds(asset.duration) - lastOutTime
+                                            let pointDuration = timelineOut - tempTimelineIn
+                                            // 要适应到卡点内要使用的C速度
+                                            let needSpeed = lastAssetDuration / pointDuration
+                                            // 当前卡点段为快速 快速都用 C 速处理
+                                            BFLog(message: "最后一点视频素材不够卡点时长要做变速C处理 差\(tempOut - CMTimeGetSeconds(asset.duration)) \(needSpeed)")
+                                            deepCopySticker?.speedRate = Float(needSpeed)
+                                            tempOut = CMTimeGetSeconds(asset.duration)
+                                            if needSpeed == 0 {
+                                                BFLog(message: "needSpeed is 0 出现在时长和卡点正好相等")
+                                                continue
+                                            }
+                                        }
+
+                                        deepCopySticker?.model_in = tempModel_In
+                                        deepCopySticker?.out = tempOut
+
+                                        deepCopySticker?.timelineIn = tempTimelineIn
+                                        deepCopySticker?.timelineOut = timelineOut
+
+                                        lastOutTime = deepCopySticker?.out ?? 0
+                                    }
+                                    BFLog(message: "测试人员创建 sticker  crilp is in 视频 \(String(format: "%.6f", deepCopySticker?.model_in ?? 0))  out  \(String(format: "%.6f", deepCopySticker?.out ?? 0)) ,分段素材时长:\(String(format: "%.6f", (deepCopySticker?.out ?? 0) - (deepCopySticker?.model_in ?? 0))) ,分段显示时长:\(String(format: "%.6f", (deepCopySticker?.timelineOut ?? 0) - (deepCopySticker?.timelineIn ?? 0))), 视频素材原时长\(CMTimeGetSeconds(asset.duration)) clipNum \(sticker.clipCount) timelineIN: \(String(format: "%.6f", deepCopySticker?.timelineIn ?? 0)) timelineOUT:\(String(format: "%.6f", deepCopySticker?.timelineOut ?? 0)) speedRate:\(deepCopySticker?.speedRate ?? 0.0)")
+
+                                    if deepCopySticker != nil {
+                                        if deepCopySticker?.timelineIn == 0 {
+                                            BFLog(message: "timelineIn data is error!!!")
+                                        }
+                                        stickers.append(deepCopySticker!)
+                                    }
+                                }
+                            } else if sticker.type == StickerType.IMAGE.rawValue {
+                                if stickers.count + 1 >= finallyStuckPoints.count {
+                                    BFLog(message: "数据出现错误!!!查正") // 155.318253
+                                    break
+                                }
+                                sticker.generateDefaultValues()
+
+                                sticker.timelineIn = Float64(String(format: "%.6f", finallyStuckPoints[stickers.count])) ?? 0.0
+                                sticker.timelineOut = Float64(String(format: "%.6f", finallyStuckPoints[stickers.count + 1])) ?? 0.0
+                                stickers.append(sticker)
+                                BFLog(message: "测试人员创建 sticker  crilp is in 图片 \(String(format: "%.6f", sticker.model_in))  out  \(String(format: "%.6f", sticker.out)) ,分段素材时长:\(String(format: "%.6f", sticker.out - sticker.model_in)) ,分段显示时长:\(String(format: "%.6f", sticker.timelineOut - sticker.timelineIn)),   timelineIN: \(String(format: "%.6f", sticker.timelineIn)) timelineOUT:\(String(format: "%.6f", sticker.timelineOut)) speedRate:\(sticker.speedRate)")
+                            }
+                        }
+                    }
+                }
+            }
+        }
+
+        BFLog(message: "生成stickers 总时长为:\(Date().timeIntervalSince1970 - beginDecoderTime)")
+        return stickers
+    }
+
+    /// 根据档位取最后使用的卡点数据
+    /// - Parameter seed: 档位速度
+    /// - Returns: 最后使用的卡点
+    func getUsedStuckPoint(seed: Int) -> [Float] {
+        if !(stuckPointMusicData!.rhythmSdata.count > 0 && stuckPointMusicData!.rhythmSdata[0].pointTimes.count > 1) {
+            return []
+        }
+
+        // 推荐卡点数
+        var stuckPoints: Array = Array<Float>.init()
+
+        var pointsTemp = Array<Float>.init()
+        //
+        // 最后一个卡点时间(原推荐卡点的倒数第二位时间)
+        let lastPoint = stuckPointMusicData!.rhythmSdata[0].pointTimes[stuckPointMusicData!.rhythmSdata[0].pointTimes.count - 2]
+
+        for (index, dunshu) in stuckPointMusicData!.rhythmSdata[0].pointTimes.enumerated() {
+            if dunshu >= Int64((stuckPointMusicData?.startTime ?? 0) * Float64(BASE_FILTER_TIMESCALE)), dunshu < lastPoint {
+                let savePointStr = String(format: "%.6f", Float(dunshu) / Float(BASE_FILTER_TIMESCALE))
+                BFLog(message: "原所有卡点数:\(index) \(savePointStr)")
+
+                pointsTemp.append(Float(savePointStr) ?? 0.0)
+            }
+        }
+
+        /*
+         一,快慢速模式下取卡点 2 3 4
+         二,跳跃卡点模式下根据不同速度 取卡点 1,2,3
+         快节奏为选中区域的所有点位,即0,1,2,3,4 5 6 7 8 9 10 ……
+         适中为每两个点位取一个,即0,2,4,6……
+         慢节奏为每三个点位取一个,即0,3,6,9……
+         */
+        // 不丢
+        if seed == 1 {
+            stuckPoints = pointsTemp
+        } else {
+            // 根据档位要丢
+            for (index, point) in pointsTemp.enumerated() {
+                if index % seed == 0 {
+                    stuckPoints.append(point)
+                }
+            }
+        }
+
+//        for point in stuckPoints {
+//            BFLog(message: "没有 start end 计算后的卡点数\(point)")
+//        }
+        // 若音乐起点至第一个卡点点位之间时长t0<0.3时,此段时长与下一个点位时长合并,故第一段卡点部分时长为t0+d
+        while (stuckPoints.first ?? 0.0) - Float(stuckPointMusicData?.startTime ?? 0) < 0.3 {
+            if stuckPoints.first != nil {
+                stuckPoints.removeFirst()
+            }
+        }
+        stuckPoints.insert(Float(stuckPointMusicData?.startTime ?? 0), at: 0)
+
+//        for point in stuckPoints {
+//            BFLog(message: "有 start end 计算后的卡点数\(point)")
+//        }
+
+        BFLog(message: "处理节奏后 stuckPoints count is \(stuckPoints.count) seed \(seed), start time:\(stuckPoints.first ?? 0.0),end time:\(stuckPoints.last ?? 0.0) 总时长为:\((stuckPoints.last ?? 0.0) - (stuckPoints.first ?? 0.0))")
+
+        return stuckPoints
+    }
+
+    func clipPoint(clipNum: Int, oldPoints: [Float]) {
+        BFLog(message: "拼接卡点数:\(clipNum)")
+        if clipNum < 0 || oldPoints.count < 2 {
+            BFLog(message: "clipNum is error!!!! \(clipNum)")
+            return
+        }
+        // 如果是第一次拼接先补第0位
+        if finallyStuckPoints.count == 0 {
+            finallyStuckPoints.append(stuckPointsTemp.first ?? 0.0)
+        }
+
+        for i in finallyStuckPoints.count ... clipNum + finallyStuckPoints.count {
+            if (i % (oldPoints.count - 1)) != 1 {
+                let value = String(format: "%.6f", finallyStuckPoints[i - 1] + oldPoints[((i - 1) % (oldPoints.count - 1)) + 1] - oldPoints[((i - 2) % (oldPoints.count - 1)) + 1])
+
+                finallyStuckPoints.append(Float(value) ?? 0.0)
+            } else {
+                let value = String(format: "%.6f", finallyStuckPoints[i - 1] + oldPoints[1] - oldPoints[0])
+                finallyStuckPoints.append(Float(value) ?? 0.0)
+            }
+        }
+    }
+
+    /// 根据不同模式model,    maxSpeed ,minSpeed,      self?.stuckPointMusicData?.speed 档位,生成音乐时长和最终使用的卡点信息
+    func dealParameter(model: createStickersModel) {
+        // 清空上一次使用的卡点数据
+        finallyStuckPoints.removeAll()
+
+        // 已经取到的视频素材总长度,用于和原视频素材时长做对比,不够多加一个点
+        var useAssestDuration: Float = 0.0
+        switch model {
+        case .createStickersModelPoint: // 跳跃卡点
+            stuckPointsTemp = getUsedStuckPoint(seed: stuckPointMusicData?.speed ?? 0)
+
+            // 要拼接的段数
+            var clipNum: Int = 0
+
+            var i: Int = 0
+            // L/(n+1)  L -原视觉素材总时长  n-抛留倍数  lastJumpSpeedSelectIndex 是位置 对应的值要+1
+            // 根据公式计划出的总时长
+            let jumpTime = Float(selectedTotalDuration) / Float(modelPoint_speed + 1)
+            // 只有图片素材时会为0
+            if jumpTime > 0 {
+                while useAssestDuration < Float(jumpTime) {
+                    // 回环从头取\
+                    if i + 1 >= stuckPointsTemp.count {
+                        i = 0
+                    }
+                    // 快速段
+                    let LA = (stuckPointsTemp[i + 1] - stuckPointsTemp[i])
+                    useAssestDuration = useAssestDuration + Float(LA)
+
+                    i = i + 1
+                    clipNum = clipNum + 1
+                }
+                // 拼接要使用的卡点信息
+                clipPoint(clipNum: clipNum, oldPoints: stuckPointsTemp)
+            }
+
+        case .createStickersModelSpeed, .createStickersModelOnlyMusic: // 快慢速
+            // 快慢速  (2:快节奏,3:适中,4:慢节奏)
+            var tempMaxSpeed: Float = 1
+            var tempMinSpeed: Float = 1
+            if model == .createStickersModelSpeed {
+                // 改变速率,.只有快慢速且非只有图片素材时自动+1处理
+                if model == .createStickersModelSpeed, selectedDataCount != selectedImageDataCount {
+                    stuckPointsTemp = getUsedStuckPoint(seed: (stuckPointMusicData?.speed ?? 0) + 1)
+                } else {
+                    stuckPointsTemp = getUsedStuckPoint(seed: stuckPointMusicData?.speed ?? 0)
+                }
+
+                tempMaxSpeed = modelSpeed_maxSpeed
+                tempMinSpeed = modelSpeed_minSpeed
+            } else {
+                stuckPointsTemp = getUsedStuckPoint(seed: stuckPointMusicData?.speed ?? 0)
+            }
+
+            /*
+             - A-视频中的快速片段
+             - B-视频中的慢速片段
+             - d-在一档下音乐每个点位时长
+             - n-不同音乐档位对应的d倍数,快节奏时,n=1;适中时,n=3;慢节奏时,n=5
+             - L-原视觉素材时长
+             - x-视频在A片段的播放倍速
+             - y-视频在B片段的播放倍速
+             */
+            // LA=x*n*d,LB=y*n*d (n=1/3/5) 注:视频经过快慢速处理后的总时长约=L*2/(x+y)
+            BFLog(message: "Ax快速为:\(tempMaxSpeed) By慢速为:\(tempMinSpeed) 档位 N为:\(stuckPointMusicData?.speed ?? 0)  使用的卡点总数:\(stuckPointsTemp.count)")
+            // 使用新方法取使用的卡点数据
+            for section in projectModel.sData?.sections ?? List() {
+                if section.sectionType == "normal" {
+                    for sticker in section.sectionTimeline!.visionTrack!.getEnableVisionTrackMaterials() {
+                        if sticker.type == StickerType.VIDEO.rawValue {
+                            // MARK: SanW-2021.11.15-不再导出到沙盒,直接使用本地相册地址
+
+                            let asset: AVURLAsset = AVURLAsset(url: URL(fileURLWithPath: sticker.locationPath), options: nil)
+//                            let asset: AVURLAsset = AVURLAsset(url: URL(fileURLWithPath: documensDirectory + sticker.locationPath), options: nil)
+                            let assetDuration = Float(CMTimeGetSeconds(asset.duration))
+                            BFLog(message: "输入素材时长 \(assetDuration)")
+
+                            if finallyStuckPoints.count == 0 {
+                                finallyStuckPoints.append(stuckPointsTemp[0])
+                            }
+                            var j = finallyStuckPoints.count
+                            // 添加卡点的段数 用于判断当前卡点是快/慢速
+                            var pointCount: Int = 1
+                            // 已经取到的视频素材总长度,用于和原视频素材时长做对比,不够多加一个卡点
+                            var useAssestDurationTemp: Float = 0.0
+                            while useAssestDurationTemp < assetDuration {
+                                // 当前段的应该使用的速度 A/B
+                                let useSpeed = (pointCount % 2 != 0) ? tempMaxSpeed : tempMinSpeed
+                                // 计算卡点
+                                // 要添加的卡点数值
+                                var sub: Float = 0.0
+                                if stuckPointsTemp.count > 2 {
+                                    if ((j - 1) % (stuckPointsTemp.count - 1)) != 0 {
+                                        sub = stuckPointsTemp[((j - 1) % (stuckPointsTemp.count - 1)) + 1] - stuckPointsTemp[(j - 1) % (stuckPointsTemp.count - 1)]
+                                    } else {
+                                        sub = stuckPointsTemp[1] - stuckPointsTemp[0]
+                                    }
+
+                                    finallyStuckPoints.append(finallyStuckPoints[j - 1] + sub)
+                                    j += 1
+                                }
+                                useAssestDurationTemp += sub * useSpeed
+                                if useAssestDurationTemp > assetDuration {
+                                    useAssestDurationTemp -= sub * useSpeed
+                                    break
+                                }
+                                pointCount += 1
+                                BFLog(2, message: "finallyStuckPoints last ;\((finallyStuckPoints.last ?? 0.0) - (finallyStuckPoints.first ?? 0.0)), tmp:\(useAssestDurationTemp)")
+                            }
+
+                            // 2拼接要使用的卡点信息
+                            sticker.clipCount = pointCount
+                            if stuckPointsTemp.count < 1 {
+                                // todo 和产品沟通提示
+                                BFLog(message: "卡点数据有错误!!!")
+                                return
+                            }
+                            BFLog(message: "finallyStuckPoints\(finallyStuckPoints)")
+
+                            // 3,多补一个卡点 做 C级 速处理,要根据条件不满足 要删除最后一位,
+                            if useAssestDurationTemp < assetDuration {
+                                /*
+                                                                 // 下一个卡的的速度性质快、慢,e.g. sticker.clipCount = 5时 tempSpeed 应该是慢速。下面计算正确。
+                                                                 var tempSpeed: Float = 1.0
+                                                                 if model == .createStickersModelSpeed {
+                                                                     tempSpeed = (sticker.clipCount) % 2 != 0 ? modelSpeed_maxSpeed : modelSpeed_minSpeed
+                                                                 }
+                                                                 // 最后一点素材时长
+                                                                 let lastAssetDuration = Float(CMTimeGetSeconds(asset.duration)) - useAssestDurationTemp
+
+                                                                 let lastPointIndex = (sticker.clipCount % stuckPointsTemp.count)
+                                                                 // 两个卡点
+                                                                 let a: Float = stuckPointsTemp[lastPointIndex]
+                                                                 var b: Float = 0.0
+                                                                 if lastPointIndex + 1 < stuckPointsTemp.count {
+                                                                     b = stuckPointsTemp[lastPointIndex + 1]
+                                                                     let pointDuration = b - a
+                                                                     // 要适应到卡点内要使用的C速度
+                                                                     let needSpeed = lastAssetDuration / pointDuration
+                                                                     // 当前卡点段为快速
+                                                                     if tempSpeed >= 1 {
+                                 //                                        if needSpeed < 0.4 * tempSpeed {
+                                 //                                            BFLog(message: "条件不满足不用补位 删除多加的一位")
+                                 //                                            finallyStuckPoints.removeLast()
+                                 //                                        }
+                                                                     } else { // 当前卡点段为慢速
+                                                                         if needSpeed >= 0.4 * tempSpeed && needSpeed >= 0.2 {
+                                                                             // 查找使用的最后一个卡点在原数组中的位置
+                                                                         } else {
+                                                                             BFLog(message: "条件不满足不用补位 删除多加的一位")
+                                                                             finallyStuckPoints.removeLast()
+                                                                         }
+
+                                                                     }
+                                                                 }
+                                  */
+                            } else {
+                                // 出现在第一个卡点X 倍速 > 原素材
+                                finallyStuckPoints.removeLast()
+                            }
+                        }
+                    }
+                }
+            }
+        }
+
+        // 拼接图片所使用的时长.选择一组图片 按图片数量计算卡点的总时长
+        if selectedImageDataCount > 0 {
+            clipPoint(clipNum: selectedImageDataCount - 1, oldPoints: stuckPointsTemp)
+        }
+
+        // 全是图片时数组里放着的一定都是图片的使用的卡点
+        // 定义一次循环的总时长
+        var oneSelectImageDuration: Float = 0.0
+        if selectedDataCount == selectedImageDataCount {
+            oneSelectImageDuration = (finallyStuckPoints.last ?? 0) - (finallyStuckPoints.first ?? 0)
+        }
+
+        // 3)素材全是图片处理
+        if selectedDataCount == selectedImageDataCount {
+            // lastCyclesSelectIndex != -1 已经设置过循环次数 应该是手动设置的值
+            if lastCyclesSelectIndex != -1 {
+                // 纯图片时 已经默认添加一次循环 所以要用lastCyclesSelectIndex - 1
+                if lastCyclesSelectIndex != 0 {
+                    clipPoint(clipNum: selectedImageDataCount * lastCyclesSelectIndex - 1, oldPoints: stuckPointsTemp)
+                }
+
+            } else {
+                if oneSelectImageDuration < 10 {
+                    lastCyclesSelectIndex = 0
+                    while oneSelectImageDuration < 10 {
+                        // 不够10S 时 一次加图片数量的卡点数
+                        clipPoint(clipNum: selectedImageDataCount - 1, oldPoints: stuckPointsTemp)
+                        oneSelectImageDuration = Float((finallyStuckPoints.last ?? 0) - (finallyStuckPoints.first ?? 0))
+
+                        lastCyclesSelectIndex = lastCyclesSelectIndex + 1
+                    }
+                    speedSettingView.setSelectItem(index: lastCyclesSelectIndex, isSettingPlayer: false, enableInsert: true)
+                } else {
+                    lastCyclesSelectIndex = 0
+                }
+            }
+        }
+
+        if finallyStuckPoints.count < 2 {
+            cShowHUB(superView: nil, msg: "视频资源导入失败,请重新选择!!")
+            exportResourceFailed()
+            return
+        }
+
+        // 设置速度选择的位置
+        if speedSettingView.viewType == 1 {
+            speedSettingView.setSelectItem(index: lastSpeedSelectIndex, isSettingPlayer: false)
+        } else if speedSettingView.viewType == 2 {
+            speedSettingView.setSelectItem(index: lastJumpSpeedSelectIndex, isSettingPlayer: false)
+        } else if speedSettingView.viewType == 3 {
+            if lastCyclesSelectIndex != -1 {
+                speedSettingView.setSelectItem(index: lastCyclesSelectIndex, isSettingPlayer: false)
+            } else {
+                speedSettingView.setSelectItem(index: 0, isSettingPlayer: false)
+            }
+        }
+
+        updateTimeInfomation()
+    }
+}
+
+// MARK: - 同步/下载素材相关
+
+/// 同步/下载素材相关
+extension PQStuckPointEditerController {
+    /// 同步音乐相关数据
+    /// - Returns: <#description#>
+    func synchroMusicInfoData(resetSelectIndex: Bool = true) {
+        if (stuckPointMusicData?.rhythmSdata.count ?? 0) <= 0 {
+            synchroMarskView.show()
+            PQStuckPointViewModel.stuckPointMusicDetailData(musicId: stuckPointMusicData?.musicId ?? "", originType: stuckPointMusicData?.originType ?? 1) { [weak self] newMusicData, _ in
+                if newMusicData != nil, (newMusicData?.rhythmSdata.count ?? 0) > 0 {
+                    self?.isStuckPointDataSuccess = true
+                    self?.stuckPointMusicData?.rhythmSdata = newMusicData?.rhythmSdata ?? []
+                    self?.stuckPointMusicData?.startTime = newMusicData?.startTime ?? 0
+                    self?.stuckPointMusicData?.endTime = newMusicData?.endTime ?? 0
+                    if newMusicData?.speed != nil {
+                        self?.stuckPointMusicData?.speed = newMusicData?.speed ?? 2
+                    }
+
+                    if self?.stuckPointMusicData?.localPath == nil || (self?.stuckPointMusicData?.localPath?.count ?? 0) > 0 {
+                        PQDownloadManager.downLoadFile(url: self?.stuckPointMusicData?.musicPath ?? "") { [weak self] filePath, error in
+                            if error == nil, filePath != nil {
+                                self?.isSynchroMusicInfoSuccess = true
+                                self?.stuckPointMusicData?.localPath = filePath?.replacingOccurrences(of: documensDirectory, with: "")
+                                // 处理所有数据完成
+                                self?.dealWithDataSuccess(resetSelectIndex: resetSelectIndex)
+                            }
+                            self?.synchroMarskView.removeMarskView()
+                        }
+                    } else {
+                        self?.isSynchroMusicInfoSuccess = true
+                        // 处理所有数据完成
+                        self?.dealWithDataSuccess(resetSelectIndex: resetSelectIndex)
+                    }
+                    // 添加子视图
+                    self?.addSubViews()
+                } else {
+                    self?.synchroMarskView.removeMarskView()
+                    cShowHUB(superView: nil, msg: "音乐信息加载失败,请重新选择音乐")
+                    self?.navigationController?.popViewController(animated: true)
+                }
+            }
+        } else if stuckPointMusicData?.localPath == nil || (stuckPointMusicData?.localPath?.count ?? 0) > 0 {
+            synchroMarskView.show()
+            isStuckPointDataSuccess = true
+
+            PQDownloadManager.downLoadFile(url: stuckPointMusicData?.musicPath ?? "") { [weak self] filePath, error in
+                if error == nil, filePath != nil {
+                    self?.isSynchroMusicInfoSuccess = true
+                    self?.stuckPointMusicData?.localPath = filePath?.replacingOccurrences(of: documensDirectory, with: "")
+                    // 处理所有数据完成
+                    self?.dealWithDataSuccess(resetSelectIndex: resetSelectIndex)
+                } else {
+                    self?.synchroMarskView.removeMarskView()
+                    cShowHUB(superView: nil, msg: "音乐信息加载失败,请重新选择音乐")
+//                    BFUploadRemindView.showUploadRemindView(title: nil, attributedTitle: NSAttributedString(string: "加载音乐失败,请重新选择音乐"), summary: "", confirmTitle: nil) { [weak self] _, _ in
+                        self?.navigationController?.popViewController(animated: true)
+                }
+            }
+        } else {
+            isStuckPointDataSuccess = true
+            // 处理所有数据完成
+            dealWithDataSuccess(resetSelectIndex: resetSelectIndex)
+        }
+    }
+
+    /// 导出相册数据
+    /// - Returns: <#description#>
+    func exportPhotoData() {
+        // 取消所有的导出
+        PQSingletoMemoryUtil.shared.allExportSession.forEach { _, exportSession in
+            exportSession.cancelExport()
+        }
+        var isHaveVideo: Bool = false
+        var failedExprot: Bool = false
+        if selectedMetarialData != nil, (selectedMetarialData?.count ?? 0) > 0 {
+            if synchroMarskView.superview == nil {
+                UIApplication.shared.keyWindow?.addSubview(synchroMarskView)
+            }
+            let dispatchGroup = DispatchGroup()
+
+            for photo in selectedMetarialData! {
+                if photo.asset != nil, photo.asset?.mediaType == .video {
+                    if !isHaveVideo {
+                        isHaveVideo = true
+                    }
+                    dispatchGroup.enter()
+
+                    PQPHAssetVideoParaseUtil.parasToAVAsset(phAsset: photo.asset!) { avAsset, _, _, _ in
+                        if avAsset is AVURLAsset {
+                            // 创建目录
+
+                            let fileName = (avAsset as! AVURLAsset).url.absoluteString
+
+                            BFLog(message: "video  fileName is\(fileName)")
+                            let tempPhoto = self.selectedMetarialData?.first(where: { material in
+                                material.asset == photo.asset
+                            })
+// MARK: SanW-2021.11.15-不在导出到沙盒,直接用本地地址
+                            tempPhoto?.locationPath = fileName.replacingOccurrences(of: "file://", with: "")
+                            dispatchGroup.leave()
+//                            if fileName.count > 0 {
+//                                createDirectory(path: photoLibraryDirectory)
+//                                let outFilePath = photoLibraryDirectory + fileName.md5 + ".mp4"
+//                                // 文件存在先删除老文件
+//                                if FileManager.default.fileExists(atPath: outFilePath) {
+//                                    do {
+//                                        try FileManager.default.removeItem(at: NSURL.fileURL(withPath: outFilePath))
+//                                    } catch {
+//                                        BFLog(message: "导出相册视频-error == \(error)")
+//                                    }
+//                                }
+//                                let curr = Date()
+//                                let assetResources = PHAssetResource.assetResources(for: photo.asset!)
+//                                if let rsc = assetResources.first(where: { res in
+//                                    res.type == .video  || res.type == .pairedVideo
+//                                }) {
+//                                    PHAssetResourceManager.default().writeData(for: rsc, toFile: URL(fileURLWithPath: outFilePath), options: nil) { error in
+//                                        if error == nil {
+//                                            BFLog(message: "导出视频相exportAsynchronously \(String(describing: outFilePath)) \(Date().timeIntervalSince(curr))")
+//                                            tempPhoto?.locationPath = outFilePath.replacingOccurrences(of: documensDirectory, with: "")
+//                                        }else{
+//                                            failedExprot = true
+//                                            BFLog(message: "导出视频相exportAsynchro faile")
+//                                        }
+//                                        dispatchGroup.leave()
+//                                    }
+//
+//                                }else {
+//                                    BFLog(message: "导出视频相exportAsynchro faile")
+//                                    dispatchGroup.leave()
+//                                }
+//                            }
+                        }
+                    }
+                }
+            }
+
+            dispatchGroup.notify(queue: DispatchQueue.main) { [weak self] in
+                if failedExprot {
+                    cShowHUB(superView: nil, msg: "视频导入失败,请返回重试")
+                    self?.exportResourceFailed()
+                    return
+                }
+                self?.isExportVideosSuccess = true
+                BFLog(message: "所有相册视频导出成功")
+                // 处理所有数据完成
+                if isHaveVideo {
+                    self?.dealWithDataSuccess()
+                }
+            }
+
+            // 只有图片
+            if !isHaveVideo {
+                isExportVideosSuccess = true
+                BFLog(message: "所有相册视频导出成功")
+
+                dealWithDataSuccess()
+            }
+        }
+    }
+
+    func exportResourceFailed() {
+        DispatchQueue.main.async {
+            self.synchroMarskView.removeMarskView()
+            self.navigationController?.popViewController(animated: true)
+        }
+    }
+
+    /// 处理所有数据完成
+    /// - Returns: <#description#>
+    /// resetSelectIndex : 是否重置用户选择的速度设置,在编辑界面切换音乐时不重置
+    func dealWithDataSuccess(resetSelectIndex: Bool = true) {
+        BFLog(message: "三组参数:\(isSynchroMusicInfoSuccess)  \(isStuckPointDataSuccess) \(isExportVideosSuccess)")
+        if !isSynchroMusicInfoSuccess || !isStuckPointDataSuccess || !isExportVideosSuccess {
+            return
+        }
+
+        createPorjectData()
+        BFLog(1, message: "界面编辑界面时参数 选择素材时长:\(selectedTotalDuration) 选择素材总数:\(selectedDataCount) 选择图片总数\(selectedImageDataCount) 再创建类型:\(String(describing: reCreateVideoData?.rhythmMode))")
+
+        if resetSelectIndex {
+            // 1 生成默认参数值
+            /*
+             - 当素材总时长∈[0-6)s 时,提示推荐仅配乐模式 or 快慢速模式
+             - 当素材总时长∈[6-80)s 时,卡点抛留倍数为1x
+             - 当素材总时长∈[80,120)s 时,卡点抛留倍数为2x
+             - 当素材总时长∈[120,160)s 时,卡点抛留倍数为3x
+             - 当素材总时长∈[160,200)s 时,卡点抛留倍数为4x
+             - 当素材总时长∈[200,240)s 时,卡点抛留倍数为5x
+             - 当素材总时长∈[240,∞)s 时,卡点抛留倍数为5x
+             */
+            // 1.1) 生成跳跃卡点的默认值
+            if selectedTotalDuration >= 6 && selectedTotalDuration < 80 {
+                lastJumpSpeedSelectIndex = 0
+            } else if selectedTotalDuration >= 80 && selectedTotalDuration < 120 {
+                lastJumpSpeedSelectIndex = 1
+            } else if selectedTotalDuration >= 120 && selectedTotalDuration < 160 {
+                lastJumpSpeedSelectIndex = 2
+            } else if selectedTotalDuration >= 160 && selectedTotalDuration < 200 {
+                lastJumpSpeedSelectIndex = 3
+            } else if (selectedTotalDuration >= 200 && selectedTotalDuration < 240) || selectedTotalDuration >= 240 {
+                lastJumpSpeedSelectIndex = 4
+            }
+
+            /* 默认进入快慢速模式
+             - 当素材总时长∈[120,144]s 时,快慢速处理方式:快速为 6x,慢速为 1.2x,效果是快&快
+             - 当素材总时长∈[70,120)s 时,快慢速处理方式:快速为5x,慢速为 1x,效果是快&正常
+             - 当素材总时长∈[56,70)s 时,快慢速处理方式:快速为3x,慢速为 0.5x,效果是快&慢
+             - 当素材总时长∈[17.5,56)s 时,快慢速处理方式:快速为 2.4x,慢速为 0.4x,效果是快&慢
+             - 当素材总时长∈[10.5,17.5)s 时,快慢速处理方式:快速为 1.8x,慢速为 0.3x,效果是快&慢
+             - 当素材总时长∈(0,10.5)s 时,快慢速处理方式:快速为 1x,慢速为 0.2x,效果是正常&慢
+
+             */
+            // 1.2)生成快慢速的默认值
+            if selectedTotalDuration >= 120 && selectedTotalDuration <= 144 || selectedTotalDuration > 144 {
+                lastSpeedSelectIndex = 5
+            } else if selectedTotalDuration >= 70, selectedTotalDuration < 120 {
+                lastSpeedSelectIndex = 4
+            } else if selectedTotalDuration >= 56, selectedTotalDuration < 70 {
+                lastSpeedSelectIndex = 3
+            } else if selectedTotalDuration >= 17.5, selectedTotalDuration < 56 {
+                lastSpeedSelectIndex = 2
+            } else if selectedTotalDuration >= 10.5, selectedTotalDuration < 17.5 {
+                lastSpeedSelectIndex = 1
+            } else if selectedTotalDuration > 0, selectedTotalDuration < 10.5 {
+                lastSpeedSelectIndex = 0
+            }
+
+            // 如果是再创作进来的按原视频的模式
+            if reCreateVideoData != nil {
+                BFLog(message: "是再创作进来的 \(reCreateVideoData!.rhythmMode)")
+                switch reCreateVideoData!.rhythmMode {
+                case 1:
+                    editModelClick(sender: jumpPointBtn, reportLog: false)
+                case 2:
+                    editModelClick(sender: speedStuckBtn, reportLog: false)
+
+                case 3:
+                    editModelClick(sender: onlyMusicBtn, reportLog: false)
+                default: break
+                }
+                return
+            } 
+             // 跳跃卡点不可用
+             if selectedTotalDuration < 6 && selectedDataCount != selectedImageDataCount {
+                
+                let styleColor = UIColor.hexColor(hexadecimal: BFConfig.shared.styleColor.rawValue)
+                jumpPointBtn.setTitleColor(UIColor.init(red: styleColor.rgbaf[0], green: styleColor.rgbaf[1], blue: styleColor.rgbaf[2], alpha: 0.3), for: .selected)
+                
+                let pointEditNamalBackgroundColor = BFConfig.shared.pointEditNamalBackgroundColor
+                jumpPointBtn.backgroundColor = UIColor.init(red: pointEditNamalBackgroundColor.rgbaf[0], green: pointEditNamalBackgroundColor.rgbaf[1], blue: pointEditNamalBackgroundColor.rgbaf[2], alpha: 0.3)
+ 
+             }
+            /*
+             文档规则 https://w42nne6hzg.feishu.cn/docs/doccnQZm1uCfkU4QtJb5fLxYk4d#
+             */
+            // 2,根据所选择所有素材时长进入默认模式
+            // 全是图片
+            if selectedDataCount == selectedImageDataCount {
+                BFLog(message: "全是图片 \(selectedDataCount) \(selectedImageDataCount)")
+
+                // 默认进入跳跃卡点模式
+                editModelClick(sender: jumpPointBtn, reportLog: false)
+
+            } else {
+                // 默认进入快慢速模式
+                if selectedTotalDuration > 0, selectedTotalDuration <= 144 {
+                    editModelClick(sender: speedStuckBtn, reportLog: false)
+
+                } else {
+                    // 默认进入跳跃卡点模式
+                    editModelClick(sender: jumpPointBtn, reportLog: false)
+                }
+            }
+        } else {
+            editModelClick(sender: lastEditModelBtn ?? jumpPointBtn, reportLog: false)
+        }
+    }
+}

+ 384 - 0
BFStuckPointKit/Classes/Controller/PQStuckPointMaterialController.swift

@@ -0,0 +1,384 @@
+//
+//  PQStuckPointMaterialController.swift
+//  PQSpeed
+//
+//  Created by SanW on 2021/4/26.
+//  Copyright © 2021 BytesFlow. All rights reserved.
+//
+
+import UIKit
+import Photos
+import BFUIKit
+import BFMaterialKit
+
+public class PQStuckPointMaterialController: BFBaseViewController {
+    public var isToPublicHandle:((_ isReCreate:Bool,_ selectedTotalDuration: Float64,_ selectedDataCount:Int,_ selectedImageDataCount: Int,_ mStickers: [PQEditVisionTrackMaterialsModel]?,_ stuckPointMusicData: PQVoiceModel?,_ editProjectModel: PQEditProjectModel?,_ rhythmMode:createStickersModel,_ speedMin:Float,_ speedMax:Float,_ audioTime:Float,_ clipAudioRange:CMTimeRange,_ playeTimeRange:CMTimeRange) -> Void)?
+               
+    // 按钮高
+    let choseBtnH: CGFloat = cDefaultMargin * 3
+    // 按钮宽
+    let choseBtnW: CGFloat = cDefaultMargin * 5
+    // 操作视图高度
+    let topImageH: CGFloat = 76
+    // 底部操作视图高度
+    let bottomH: CGFloat = cDefaultMargin * 5
+    // 间隔
+    let margin: CGFloat = 12
+    // 选择的总数
+    var selectedDataCount: Int = 0
+    // 选择的图片总数
+    var selectedImageDataCount: Int = 0
+    // 再创作音乐数据
+    public var reCreateMusicData: PQVoiceModel?
+    public var reCreateVideoData: PQReCreateModel? // 再创作数据
+    lazy var changeCollecBtn: UIButton = {
+        let changeCollecBtn = UIButton(frame: CGRect(x: cDefaultMargin * 5, y: cDevice_iPhoneStatusBarHei, width: cScreenWidth - cDefaultMargin * 10, height: cDefaultMargin * 4))
+        changeCollecBtn.titleLabel?.lineBreakMode = .byTruncatingTail
+        changeCollecBtn.tintColor = BFConfig.shared.styleTitleColor
+        changeCollecBtn.setTitle("我的相册", for: .normal)
+        changeCollecBtn.setImage(UIImage.moduleImage(named: "icon_selected_down", moduleName: "BFFramework",isAssets: false)?.withRenderingMode(.alwaysTemplate), for: .normal)
+        changeCollecBtn.setImage(UIImage.moduleImage(named: "icon_selected_up", moduleName: "BFFramework",isAssets: false)?.withRenderingMode(.alwaysTemplate), for: .selected)
+        changeCollecBtn.setTitleColor(BFConfig.shared.styleTitleColor, for: .normal)
+        changeCollecBtn.titleLabel?.font = UIFont.systemFont(ofSize: 18, weight: .medium)
+        changeCollecBtn.tag = 1
+        changeCollecBtn.imagePosition(at: PQButtonImageEdgeInsetsStyle.right, space: cDefaultMargin / 2)
+        changeCollecBtn.addTarget(self, action: #selector(btnClick(sender:)), for: .touchUpInside)
+        return changeCollecBtn
+    }()
+
+    lazy var albumController: BFPhotoAlbumController = {
+        let albumController = BFPhotoAlbumController()
+        albumController.isTopShow = true
+        albumController.categoryH = cDefaultMargin * 40
+        addChild(albumController)
+        view.insertSubview(albumController.view, belowSubview: navHeadImageView!)
+        albumController.updateViewFrame(frame: CGRect(x: 0, y: navHeadImageView?.frame.maxY ?? cDevice_iPhoneNavBarAndStatusBarHei, width: view.frame.width, height: view.frame.height - (navHeadImageView?.frame.maxY ?? cDevice_iPhoneNavBarAndStatusBarHei)))
+        albumController.selectedHandle = { [weak self] seletedData in
+            if seletedData != nil {
+                self?.albumSelectedHandle(seletedData: seletedData)
+            } else {
+                self?.changeCollecBtn.isSelected = false
+            }
+        }
+        return albumController
+    }()
+
+    lazy var choseLocalAllBtn: UIButton = {
+        let choseLocalAllBtn = UIButton(frame: CGRect(x: (view.frame.width - choseBtnW * 3) / 4, y: cDevice_iPhoneNavBarAndStatusBarHei + margin, width: choseBtnW, height: choseBtnH))
+        choseLocalAllBtn.setTitleColor(UIColor.hexColor(hexadecimal: "#999999"), for: .normal)
+        choseLocalAllBtn.setTitleColor(BFConfig.shared.styleTitleColor, for: .selected)
+        choseLocalAllBtn.setTitle("全部", for: .normal)
+        choseLocalAllBtn.titleLabel?.font = UIFont.systemFont(ofSize: 17, weight: .medium)
+        choseLocalAllBtn.addCorner(corner: 6)
+        choseLocalAllBtn.tag = 10
+        choseLocalAllBtn.addTarget(self, action: #selector(btnClick(sender:)), for: .touchUpInside)
+        return choseLocalAllBtn
+    }()
+
+    lazy var choseLocalVideoBtn: UIButton = {
+        let choseLocalVideoBtn = UIButton(frame: CGRect(x: choseLocalAllBtn.frame.maxX + (view.frame.width - choseBtnW * 3) / 4, y: choseLocalAllBtn.frame.minY, width: choseBtnW, height: choseBtnH))
+        choseLocalVideoBtn.setTitleColor(UIColor.hexColor(hexadecimal: "#999999"), for: .normal)
+        choseLocalVideoBtn.setTitleColor(BFConfig.shared.styleTitleColor, for: .selected)
+        choseLocalVideoBtn.setTitle("视频", for: .normal)
+        choseLocalVideoBtn.titleLabel?.font = UIFont.systemFont(ofSize: 17, weight: .medium)
+        choseLocalVideoBtn.addCorner(corner: 6)
+        choseLocalVideoBtn.tag = 11
+        choseLocalVideoBtn.isSelected = true
+        choseLocalVideoBtn.addTarget(self, action: #selector(btnClick(sender:)), for: .touchUpInside)
+        return choseLocalVideoBtn
+    }()
+
+    lazy var choseLocalImageBtn: UIButton = {
+        let choseLocalImageBtn = UIButton(frame: CGRect(x: choseLocalVideoBtn.frame.maxX + (view.frame.width - choseBtnW * 3) / 4, y: choseLocalAllBtn.frame.minY, width: choseBtnW, height: choseBtnH))
+        choseLocalImageBtn.setTitleColor(UIColor.hexColor(hexadecimal: "#999999"), for: .normal)
+        choseLocalImageBtn.setTitleColor(BFConfig.shared.styleTitleColor, for: .selected)
+        choseLocalImageBtn.setTitle("照片", for: .normal)
+        choseLocalImageBtn.titleLabel?.font = UIFont.systemFont(ofSize: 17, weight: .medium)
+        choseLocalImageBtn.addCorner(corner: 6)
+        choseLocalImageBtn.tag = 12
+        choseLocalImageBtn.addTarget(self, action: #selector(btnClick(sender:)), for: .touchUpInside)
+        return choseLocalImageBtn
+    }()
+
+    lazy var choseLineView: UIView = {
+        let choseLineView = UIView(frame: CGRect(x: 0, y: 0, width: 25, height: 3))
+        choseLineView.frame.origin.y = (navHeadImageView?.frame.maxY ?? 0) - 6
+        choseLineView.center.x = choseLocalVideoBtn.center.x
+        choseLineView.backgroundColor = UIColor.hexColor(hexadecimal: BFConfig.shared.styleColor.rawValue)
+        return choseLineView
+    }()
+
+    // 顶部提示视图
+//    lazy var materialHeadView: PQStuckPointMaterialHeadView = {
+//        let materialHeadView: PQStuckPointMaterialHeadView = PQStuckPointMaterialHeadView(frame: CGRect(x: 0, y: navHeadImageView?.frame.maxY ?? cDevice_iPhoneNavBarAndStatusBarHei, width: view.frame.width, height: topImageH))
+//        return materialHeadView
+//    }()
+
+    // 底部操作视图
+    lazy var bottomRemindView: UIView = {
+        let bottomRemindView = UIView(frame: CGRect(x: 0, y: view.frame.height - (bottomH + cSafeAreaHeight), width: view.frame.width, height: bottomH + cSafeAreaHeight))
+        bottomRemindView.backgroundColor = BFConfig.shared.styleBackGroundColor
+        return bottomRemindView
+    }()
+
+    // 确定按钮
+    lazy var confirmBtn: UIButton = {
+        let confirmBtn = UIButton(frame: CGRect(x: bottomRemindView.frame.width - cDefaultMargin * 9 - margin, y: margin / 2, width: cDefaultMargin * 9, height: bottomH - margin))
+        confirmBtn.backgroundColor = BFConfig.shared.otherTintColor
+//        confirmBtn.backgroundColor = UIColor.hexColor(hexadecimal: "#EE0051")
+        confirmBtn.setTitle("确定", for: .normal)
+        confirmBtn.setTitleColor(UIColor.hexColor(hexadecimal: "#999999"), for: .normal)
+        confirmBtn.setTitleColor(UIColor.white, for: .selected)
+        confirmBtn.titleLabel?.font = UIFont.systemFont(ofSize: 13, weight: .medium)
+        confirmBtn.addCorner(corner: 3)
+        confirmBtn.tag = 13
+        confirmBtn.addTarget(self, action: #selector(btnClick(sender:)), for: .touchUpInside)
+        return confirmBtn
+    }()
+
+    // 底部提示视图
+    lazy var bottomRemindLab: UILabel = {
+        let bottomRemindLab = UILabel(frame: CGRect(x: margin, y: 0, width: bottomRemindView.frame.width - margin * 3 - confirmBtn.frame.width, height: bottomH))
+        bottomRemindLab.attributedText = NSAttributedString(string: "至少选择 1 个视频或 2 张照片")
+        bottomRemindLab.textAlignment = .left
+        bottomRemindLab.textColor = BFConfig.shared.styleTitleColor
+        bottomRemindLab.font = UIFont.systemFont(ofSize: 14, weight: .medium)
+        return bottomRemindLab
+    }()
+
+    /// 已选素材列表
+    lazy var materialListView: BFSelectedMaterialListView = {
+        let materialListView = BFSelectedMaterialListView(frame: CGRect(x: 0, y: cScreenHeigth, width: cScreenWidth, height: 88))
+        materialListView.deletedMaterialHandle = { [weak self] materialData, isDissmiss in
+            /// 处理已经选择的数据
+            self?.dealWithSelectedMaterials(isDissmiss: isDissmiss, isChose: false, materialData: materialData)
+        }
+        materialListView.detailMaterialHandle = { [weak self] _, materialData in
+            let detailVc: BFMaterialDetailController = BFMaterialDetailController()
+            detailVc.materialDetailClickHandle = { [weak self] isMaterialSelected, materialData in
+                if isMaterialSelected != materialData?.isSelected {
+                    self?.photoMaterialVc.updateMaterials(isSelected:!(isMaterialSelected ?? false), materialData: materialData)
+                }
+            }
+            if !(materialData?.isSelected ?? false) {
+                materialData?.selectedIndex = (self?.selectedDataCount ?? 0) + 1
+            }
+            detailVc.materialData = materialData
+            detailVc.isStuckPoint = true
+            self?.navigationController?.pushViewController(detailVc, animated: true)
+        }
+        return materialListView
+    }()
+
+    /// 图片加载视图
+    lazy var photoMaterialVc: BFPhotosMaterialController = {
+        let photoMaterialVc = BFPhotosMaterialController()
+        photoMaterialVc.isShowMediaTag = false
+        photoMaterialVc.imageDuration = 1.5
+        addChild(photoMaterialVc)
+        view.insertSubview(photoMaterialVc.view, belowSubview: bottomRemindView)
+        photoMaterialVc.updateFrame(newFrame: CGRect(x: 0, y: (navHeadImageView?.frame.maxY ?? cDevice_iPhoneNavBarAndStatusBarHei) + margin / 2, width: view.frame.width, height: view.frame.height - ((navHeadImageView?.frame.maxY ?? cDevice_iPhoneNavBarAndStatusBarHei) + margin + bottomRemindView.frame.height)))
+        photoMaterialVc.selectedMaterialHandle = { [weak self] currentMaterialData, selectedPhotoData, selectedTotalDuration, imageCount in
+            
+            self?.selectedDataCount = selectedPhotoData.count
+            self?.selectedImageDataCount = imageCount
+            
+            self?.dealWithSelectedMaterial(materialData: currentMaterialData, totalDuration: selectedTotalDuration)
+            /// 处理已经选择的数据
+            self?.dealWithSelectedMaterials(isDissmiss: selectedPhotoData.count <= 0, isChose: true, materialData: currentMaterialData)
+        }
+        photoMaterialVc.detailMaterialHandle = { [weak self] _, currentMaterialData in
+            let detailVc: BFMaterialDetailController = BFMaterialDetailController()
+            detailVc.isStuckPoint = true
+            detailVc.materialDetailClickHandle = { [weak self] isMaterialSelected, materialData in
+                if isMaterialSelected != materialData?.isSelected {
+                    self?.photoMaterialVc.updateMaterials(isSelected:!(isMaterialSelected ?? false), materialData: materialData)
+                }
+            }
+            if !(currentMaterialData?.isSelected ?? false) {
+                currentMaterialData?.selectedIndex = (self?.selectedDataCount ?? 0) + 1
+            }
+            detailVc.materialData = currentMaterialData
+            self?.navigationController?.pushViewController(detailVc, animated: true)
+        }
+        photoMaterialVc.emptyRefreshHandle = { [weak self] msgType in
+            self?.btnClick(sender: msgType == .video ? self?.choseLocalImageBtn : (msgType == .image ? self?.choseLocalVideoBtn : self?.choseLocalAllBtn))
+        }
+        photoMaterialVc.scrollViewDidScroll = { [weak self] _ in
+            // 更新frame
+//            self?.updateMaterialHeadFrame(contentOffset: contentOffset)
+        }
+        return photoMaterialVc
+    }()
+
+    override public func viewDidLoad() {
+        super.viewDidLoad()
+        leftButton(image: UIImage.init(named: "upload_delete"), tintColor: BFConfig.shared.styleTitleColor)
+        navHeadImageView?.addSubview(changeCollecBtn)
+        navHeadImageView?.frame.size.height = cDevice_iPhoneNavBarAndStatusBarHei + margin * 2 + choseBtnH
+        navHeadImageView?.addSubview(choseLocalAllBtn)
+        navHeadImageView?.addSubview(choseLocalVideoBtn)
+        navHeadImageView?.addSubview(choseLocalImageBtn)
+        navHeadImageView?.addSubview(choseLineView)
+        view.addSubview(bottomRemindView)
+        bottomRemindView.addSubview(confirmBtn)
+        bottomRemindView.addSubview(bottomRemindLab)
+        addChild(photoMaterialVc)
+        view.insertSubview(photoMaterialVc.view, belowSubview: bottomRemindView)
+        view.bringSubviewToFront(navHeadImageView!)
+        view.insertSubview(materialListView, belowSubview: bottomRemindView)
+        // 卡点音乐素材选择曝光上报
+        PQEventTrackViewModel.baseReportUpload(businessType: .bt_windowView, objectType: .ot_view_selectSyncedUpMaterial, pageSource: .sp_stuck_selectMaterial, extParams: nil, remindmsg: "卡点视频数据上报-(曝光上报:卡点视频素材选择页)")
+        // 注册通知
+        addNotification(self, selector: #selector(dismissVc), name: cFinishedPublishedNotiKey, object: nil)
+    }
+
+    /// 按钮点击事件
+    /// - Parameter sender: <#sender description#>
+    /// - Returns: <#description#>
+    @objc func btnClick(sender: UIButton?) {
+        switch sender?.tag {
+        case 1: // 选择图库
+            sender?.isSelected = !(sender?.isSelected ?? false)
+            if sender?.isSelected ?? false {
+                albumController.showCategoryView()
+            } else {
+                albumController.dismissCategoryView()
+            }
+        case 10, 11, 12: // 筛选全部图库
+            choseLocalAllBtn.isSelected = sender?.tag == 10
+            choseLocalVideoBtn.isSelected = sender?.tag == 11
+            choseLocalImageBtn.isSelected = sender?.tag == 12
+            if sender?.tag == 11 {
+                photoMaterialVc.msgType = .video
+            } else if sender?.tag == 12 {
+                photoMaterialVc.msgType = .image
+            } else {
+                photoMaterialVc.msgType = .all
+            }
+            UIView.animate(withDuration: 0.3, delay: 0, options: .allowUserInteraction) { [weak self] in
+                self?.choseLineView.center.x = sender?.center.x ?? 0
+            } completion: { _ in
+            }
+        case 13:
+            if confirmBtn.isSelected {
+//                reCreateMusicData?.endTime = (reCreateMusicData?.startTime ?? 0) + (reCreateMusicData?.stuckPointCuttingTime(videoCount: selectedDataCount - selectedImageDataCount, imageCount: selectedImageDataCount, totalDuration: photoMaterialVc.selectedTotalDuration) ?? 0)
+                
+                if(reCreateMusicData != nil){
+                    let editerVC: PQStuckPointEditerController = PQStuckPointEditerController()
+                    editerVC.stuckPointMusicData = reCreateMusicData
+                    editerVC.selectedDataCount = selectedDataCount
+                    editerVC.selectedImageDataCount = selectedImageDataCount
+                    //mdf by ak 进入编辑界面的时候去掉图片的时长
+                    editerVC.selectedTotalDuration = photoMaterialVc.selectedTotalDuration - (Double(selectedImageDataCount) * 1.5)
+                    editerVC.selectedPhotoData = photoMaterialVc.selectedPhotoData
+                    editerVC.reCreateVideoData = reCreateVideoData
+                    editerVC.isReCreate = true
+                    navigationController?.pushViewController(editerVC, animated: true)
+                }else{
+                    let stuckPointMusicVc = PQStuckPointMusicController()
+                    stuckPointMusicVc.selectedMusicData = reCreateMusicData
+                    stuckPointMusicVc.selectedDataCount = selectedDataCount
+                    stuckPointMusicVc.reCreateVideoData = reCreateVideoData
+                    stuckPointMusicVc.selectedImageDataCount = selectedImageDataCount
+                    stuckPointMusicVc.selectedTotalDuration = photoMaterialVc.selectedTotalDuration
+                    stuckPointMusicVc.selectedPhotoData = photoMaterialVc.selectedPhotoData
+                    navigationController?.pushViewController(stuckPointMusicVc, animated: true)
+                }
+             
+                // 卡点视频素材确认按钮
+                PQEventTrackViewModel.baseReportUpload(businessType: .bt_buttonClick, objectType: .ot_click_confirmMaterial, pageSource: .sp_stuck_selectMaterial, extParams: ["materialNumber": selectedDataCount], remindmsg: "卡点视频数据上报-(点击上报:卡点视频素材确认按钮)")
+            }
+        default:
+            break
+        }
+    }
+
+    public override func backBtnClick() {
+        super.backBtnClick()
+        if isPresent {
+            postNotification(name: cFinishedPublishedNotiKey)
+        }
+        // 卡点视频返回按钮点击上报
+        PQEventTrackViewModel.baseReportUpload(businessType: .bt_buttonClick, objectType: .ot_click_back, pageSource: .sp_stuck_selectMaterial, extParams: nil, remindmsg: "卡点视频数据上报-(点击上报:返回按钮)")
+    }
+
+    // 返回
+    @objc func dismissVc() {
+        DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.2) { [weak self] in
+            if self?.isPresent ?? false {
+                self?.dismiss(animated: true, completion: nil)
+            }
+        }
+    }
+
+    deinit {
+        photoMaterialVc.assetCollection = nil
+        photoMaterialVc.allPhotos = nil
+        PQNotification.removeObserver(self)
+        BFLog(message: "\(self) 已销毁")
+    }
+
+    /// 图库选择的回调
+    /// - Parameter seletedData: <#seletedData description#>
+    /// - Returns: <#description#>
+    func albumSelectedHandle(seletedData: PHAsset?) {
+        changeCollecBtn.isSelected = false
+        if seletedData != nil {
+            changeCollecBtn.setTitle(seletedData?.title ?? "全部", for: .normal)
+            changeCollecBtn.imagePosition(at: PQButtonImageEdgeInsetsStyle.right, space: cDefaultMargin / 2)
+            photoMaterialVc.assetCollection = seletedData?.assetCollection
+        }
+    }
+
+    /// 点击选择的回调
+    /// - Parameter materialData: <#materialData description#>
+    /// - Returns: <#description#>
+    func dealWithSelectedMaterial(materialData _: PHAsset?, totalDuration: Float64) {
+        confirmBtn.isSelected = (selectedDataCount > 0 && (selectedImageDataCount >= 2 || selectedDataCount > selectedImageDataCount))
+        if confirmBtn.isSelected {
+            confirmBtn.backgroundColor = UIColor.hexColor(hexadecimal: BFConfig.shared.styleColor.rawValue)
+        } else {
+            confirmBtn.backgroundColor = BFConfig.shared.otherTintColor
+        }
+        if selectedDataCount <= 0 {
+            bottomRemindLab.attributedText = NSAttributedString(string: "至少选择 1 个视频或 2 张照片")
+        } else if selectedImageDataCount == 1, selectedDataCount == selectedImageDataCount {
+            let att = NSMutableAttributedString(string: "至少选择 1 个视频或 2 张照片")
+            bottomRemindLab.attributedText = att
+        } else {
+            let att = NSMutableAttributedString(string: "素材总时长 \(totalDuration.formatDurationToHMS())")
+            att.setAttributes([.foregroundColor: UIColor.hexColor(hexadecimal: BFConfig.shared.styleColor.rawValue)], range: NSRange(location: 6, length: "\(totalDuration.formatDurationToHMS())".count))
+            bottomRemindLab.attributedText = att
+        }
+        confirmBtn.setTitle(selectedDataCount > 0 ? "确定(\(selectedDataCount))" : "确定", for: .normal)
+
+    }
+
+    /// 处理已经选择的数据
+    /// - Returns: <#description#>
+    func dealWithSelectedMaterials(isDissmiss: Bool, isChose: Bool, materialData: PHAsset?) {
+        // 添加当前素材
+        if materialData != nil {
+            if isChose {
+                materialListView.addMaterialData(materialData: materialData!)
+            } else {
+                photoMaterialVc.deSeletedMaterialData(indexPath: nil, materialData: materialData)
+            }
+        }
+        if isChose && !isDissmiss && materialListView.frame.minY > (bottomRemindView.frame.minY - 88) {
+            UIView.animate(withDuration: 0.5, delay: 0, options: .allowUserInteraction) { [weak self] in
+                self?.materialListView.frame = CGRect(x: 0, y: (self?.bottomRemindView.frame.minY ?? 0) - 88, width: cScreenWidth, height: 88)
+                self?.photoMaterialVc.updateFrame(newFrame: CGRect(x: 0, y: self?.photoMaterialVc.view.frame.minY ?? 0, width: self?.photoMaterialVc.view.frame.width ?? 0, height: (self?.photoMaterialVc.view.frame.height ?? 0) - 88))
+            } completion: { _ in
+            }
+
+        } else if isDissmiss && materialListView.frame.minY != cScreenHeigth {
+            UIView.animate(withDuration: 0.5, delay: 0, options: .allowUserInteraction) { [weak self] in
+                self?.materialListView.frame = CGRect(x: 0, y: cScreenHeigth, width: cScreenWidth, height: 88)
+                self?.photoMaterialVc.updateFrame(newFrame: CGRect(x: 0, y: self?.photoMaterialVc.view.frame.minY ?? 0, width: self?.photoMaterialVc.view.frame.width ?? 0, height: (self?.photoMaterialVc.view.frame.height ?? 0) + 88))
+            } completion: { _ in
+            }
+        }
+    }
+}

+ 321 - 0
BFStuckPointKit/Classes/Controller/PQStuckPointMusicContentController.swift

@@ -0,0 +1,321 @@
+//
+//  PQStuckPointMusicContentController.swift
+//  PQSpeed
+//
+//  Created by SanW on 2021/4/28.
+//  Copyright © 2021 BytesFlow. All rights reserved.
+//
+
+import BFCommonKit
+@_exported import BFUIKit
+import UIKit
+
+class PQStuckPointMusicContentController: BFBaseViewController {
+    var itemList: [Any] = Array<Any>.init() // 所有分类数据
+    // 当前选择的位置
+    var lastIndexPath: IndexPath?
+    let lineSpacing: CGFloat = 12
+    let leftMargin: CGFloat = 16
+    // cell高度
+    var cellHight: CGFloat = cDefaultMargin * 5
+    // 顶部距离
+    var topHight: CGFloat = cDefaultMargin * 6
+    /// 二级标签信息
+    var tagsInfo: ([PQStuckPointMusicTagsModel], ([UICollectionViewLayoutAttributes], CGFloat))?
+    // 点击的回调
+    var didSelectedHandle: ((_ isTagsClick: Bool, _ contentType: stuckPointMusicContentType, _ indexPath: IndexPath, _ itemData: Any) -> Void)?
+    // 按钮点击的回调
+    var btnClickHandle: ((_ sender: UIButton, _ bgmData: Any?) -> Void)?
+    // 滑动的回调
+    var scroDidScroHandle: (() -> Void)?
+    // 刷新的回调
+    var refreshHandle: ((_ isRefresh: Bool, _ contentType: stuckPointMusicContentType) -> Void)?
+    // 卡点音乐页面类型
+    var contentType: stuckPointMusicContentType = .catagery {
+        didSet {
+            if contentType == .catagery {
+                lastIndexPath = IndexPath(item: 0, section: 0)
+            }
+        }
+    }
+
+    lazy var collectionView: UICollectionView = {
+        let flowLayout = UICollectionViewFlowLayout()
+        flowLayout.sectionInset = UIEdgeInsets.zero
+        flowLayout.minimumLineSpacing = 0
+        flowLayout.minimumInteritemSpacing = 0
+        flowLayout.scrollDirection = .vertical
+        let collectionView = UICollectionView(frame: CGRect(x: 0, y: 0, width: view.frame.width, height: view.frame.height), collectionViewLayout: flowLayout)
+        collectionView.showsVerticalScrollIndicator = false
+        collectionView.showsHorizontalScrollIndicator = false
+        collectionView.delegate = self
+        collectionView.dataSource = self
+        collectionView.backgroundColor = UIColor.clear
+        collectionView.register(PQStuckPointMusicContentCell.self, forCellWithReuseIdentifier: String(describing: PQStuckPointMusicContentCell.self))
+        collectionView.register(PQStuckPointMusicTagsCell.self, forCellWithReuseIdentifier: String(describing: PQStuckPointMusicTagsCell.self))
+        collectionView.register(PQStuckPointSearchEmptyCell.self, forCellWithReuseIdentifier: String(describing: PQStuckPointSearchEmptyCell.self))
+        if #available(iOS 11.0, *) {
+            collectionView.contentInsetAdjustmentBehavior = .never
+        } else {
+            automaticallyAdjustsScrollViewInsets = false
+        }
+        // 延迟scrollView上子视图的响应,所以当直接拖动UISlider时,如果此时touch时间在150ms以内,UIScrollView会认为是拖动自己,从而拦截了event,导致UISlider接收不到滑动的event
+        collectionView.delaysContentTouches = false
+        collectionView.addRefreshView(type: .REFRESH_TYPE_FOOTER) { [weak self, weak collectionView] isRefresh in
+            if !isRefresh, self?.contentType != .catagery {
+                // 请求一下加载更多
+                if self?.refreshHandle != nil {
+                    self?.refreshHandle!(isRefresh, self?.contentType ?? .catagery)
+                }
+            } else {
+                collectionView?.mj_footer?.endRefreshing()
+            }
+        }
+        return collectionView
+    }()
+
+    lazy var emptyRemindView: BFEmptyRemindView = {
+        let emptyRemindView = BFEmptyRemindView(frame: collectionView.bounds)
+        emptyRemindView.remindLab.font = UIFont.systemFont(ofSize: 16)
+        emptyRemindView.remindLab.textColor = UIColor.hexColor(hexadecimal: "#575757")
+        emptyRemindView.isHidden = true
+        emptyRemindView.emptyData = emptyData
+        emptyRemindView.fullRefreshBloc = { [weak self] _, _ in
+            if self?.refreshHandle != nil {
+                self?.refreshHandle!(true, self?.contentType ?? .catagery)
+            }
+        }
+        collectionView.addSubview(emptyRemindView)
+        return emptyRemindView
+    }()
+
+    var emptyData: BFEmptyModel? = {
+        let emptyData = BFEmptyModel()
+        emptyData.title = "暂无音乐"
+        emptyData.netDisRefreshBgColor = UIColor.hexColor(hexadecimal: "#FA6400")
+        emptyData.netDisTitle = "内容加载失败"
+        emptyData.netDisTitleColor = UIColor.hexColor(hexadecimal: "#333333")
+        emptyData.netemptyDisImage = UIImage(named: "empty_netDis_icon")
+        emptyData.netDisRefreshTitle = NSMutableAttributedString(string: "重新加载", attributes: [.font: UIFont.systemFont(ofSize: 16, weight: .medium), .foregroundColor: UIColor.white])
+        return emptyData
+    }()
+
+    override func viewDidLoad() {
+        super.viewDidLoad()
+        hiddenNavigation()
+        view.addSubview(collectionView)
+    }
+
+    /// 更新frame
+    /// - Parameter newFrame: <#newFrame description#>
+    /// - Returns: <#description#>
+    func updateViewFrame(newFrame: CGRect) {
+        view.frame = newFrame
+        collectionView.frame = CGRect(origin: CGPoint.zero, size: newFrame.size)
+        emptyRemindView.frame = collectionView.bounds
+        collectionView.reloadData()
+    }
+
+    /// 配置数据
+    /// - Parameter musicListData: <#musicListData description#>
+    /// - Returns: <#description#>
+    func configMusicListData(isResetData: Bool = false, isRefresh: Bool = true, musicListData: [Any]) {
+        if isRefresh {
+            if contentType == .catagery {
+                lastIndexPath = IndexPath(item: 0, section: 0)
+            } else {
+                lastIndexPath = nil
+            }
+            itemList.removeAll()
+            if tagsInfo != nil {
+                itemList = musicListData
+                itemList.insert(tagsInfo!, at: 0)
+            } else {
+                itemList = musicListData
+            }
+        } else {
+            itemList = itemList + musicListData
+        }
+        if (contentType != .serach && musicListData.count < 20) || (contentType == .serach && musicListData.count <= 0) || itemList.first is BFEmptyModel {
+            collectionView.mj_footer?.endRefreshingWithNoMoreData()
+        } else {
+            collectionView.mj_footer?.endRefreshing()
+        }
+        collectionView.reloadData()
+        // 展示空页面
+        if isResetData {
+            emptyRemindView.isHidden = true
+        } else {
+            showEmptyView()
+        }
+    }
+
+    /// 展示空页面
+    func showEmptyView() {
+        if contentType != .catagery {
+            emptyRemindView.isHidden = contentType == .serach ? itemList.count > 0 : itemList.count > 1
+            if !emptyRemindView.isHidden, itemList.count > 0, ((itemList.first as? ([PQStuckPointMusicTagsModel], ([UICollectionViewLayoutAttributes], CGFloat)))?.0.count ?? 0) > 0 {
+                let maxH = (((itemList.first as? ([PQStuckPointMusicTagsModel], ([UICollectionViewLayoutAttributes], CGFloat)))?.1.1 ?? 0) + 35)
+                emptyRemindView.frame = CGRect(x: 0, y: maxH, width: collectionView.frame.width, height: collectionView.frame.height - maxH)
+            } else {
+                emptyRemindView.frame = collectionView.bounds
+            }
+            emptyRemindView.emptyData = emptyData
+        }
+    }
+
+    /// 更新当前播放视频
+    /// - Returns: <#description#>
+    func updateCurrentPlayMusic(isPlaying: Bool, isClearCurrentMusic: Bool) {
+        if lastIndexPath != nil, itemList.count > (lastIndexPath?.item ?? 0) {
+            (itemList[lastIndexPath?.item ?? 0] as? PQVoiceModel)?.isPlaying = isPlaying
+            collectionView.reloadItems(at: [lastIndexPath!])
+        }
+        if isClearCurrentMusic {
+            if lastIndexPath != nil, itemList.count > (lastIndexPath?.item ?? 0) {
+                (itemList[lastIndexPath?.item ?? 0] as? PQVoiceModel)?.isSelected = false
+                (itemList[lastIndexPath?.item ?? 0] as? PQVoiceModel)?.isPlaying = false
+                collectionView.reloadItems(at: [lastIndexPath!])
+            }
+            lastIndexPath = nil
+        }
+    }
+}
+
+// MARK: - 卡点音乐相关代理
+
+/// 卡点音乐相关代理
+extension PQStuckPointMusicContentController: UICollectionViewDelegate, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout, UIScrollViewDelegate {
+    func collectionView(_: UICollectionView, numberOfItemsInSection _: Int) -> Int {
+        return itemList.count
+    }
+
+    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
+        let itemData: Any = itemList[indexPath.item]
+        if itemData is ([PQStuckPointMusicTagsModel], ([UICollectionViewLayoutAttributes], CGFloat)) {
+            let cell = PQStuckPointMusicTagsCell.stuckPointMusicTagsCell(collectionView: collectionView, indexPath: indexPath)
+            cell.itemData = itemData as! ([PQStuckPointMusicTagsModel], ([UICollectionViewLayoutAttributes], CGFloat))
+            cell.tagsDidSelectedHandle = { [weak self] indexPath, tagsData in
+                if self?.didSelectedHandle != nil {
+                    self?.didSelectedHandle!(true, self?.contentType ?? .catagery, indexPath, tagsData ?? PQStuckPointMusicTagsModel())
+                }
+                if tagsData != nil {
+                    // 点击上报:选择音乐分类下的 TAG
+                    PQEventTrackViewModel.baseReportUpload(businessType: .bt_buttonClick, objectType: .ot_click_chooseMusicCategoryTag, pageSource: .sp_stuck_selectSynceedUpMusic, extParams: ["categoryName": tagsData?.tagName ?? "", "categoryId": tagsData?.tagId ?? ""], remindmsg: "卡点视频数据上报-(点击上报:选择音乐分类下的 TAG)")
+                }
+            }
+            return cell
+        } else if itemData is BFEmptyModel {
+            let cell = PQStuckPointSearchEmptyCell.stuckPointSearchEmptyCell(collectionView: collectionView, indexPath: indexPath)
+            cell.bgmData = itemData as? BFEmptyModel
+            return cell
+        } else {
+            let cell = PQStuckPointMusicContentCell.stuckPointMusicContentCell(collectionView: collectionView, indexPath: indexPath)
+            cell.contentType = contentType
+            cell.bgmData = itemList[indexPath.item]
+            if cell.bgmData is PQVoiceModel {
+                let bgmData = cell.bgmData as! PQVoiceModel
+                if bgmData.isSelected {
+                    lastIndexPath = indexPath
+                }
+            }
+            cell.btnClickHandle = { [weak self] sender, bgmData in
+                if self?.btnClickHandle != nil {
+                    self?.btnClickHandle!(sender, bgmData)
+                }
+                if bgmData is PQVoiceModel {
+                    if self?.contentType == .page {
+                        // 卡点视频音乐选择音乐素材
+                        PQEventTrackViewModel.baseReportUpload(businessType: .bt_buttonClick, objectType: .ot_click_chooseMusic, pageSource: .sp_stuck_selectSynceedUpMusic, extParams: ["musicName": (bgmData as? PQVoiceModel)?.musicName ?? "", "musicId": (bgmData as? PQVoiceModel)?.musicId ?? ""], remindmsg: "卡点视频数据上报-(点击上报:选择音乐素材)")
+                    } else if self?.contentType == .serach {
+                        // 点击上报:选择音乐素材
+                        PQEventTrackViewModel.baseReportUpload(businessType: .bt_buttonClick, objectType: .ot_click_chooseSearchMusic, pageSource: .sp_stuck_searchSyncedUpMusic, extParams: ["musicName": (bgmData as? PQVoiceModel)?.musicName ?? "", "musicId": (bgmData as? PQVoiceModel)?.musicId ?? "", "isHotMusic": self?.itemList.first is BFEmptyModel], remindmsg: "卡点视频数据上报-(点击上报:选择音乐素材)")
+                    }
+                }
+            }
+            return cell
+        }
+    }
+
+    func collectionView(_ collectionView: UICollectionView, layout _: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
+        let itemData: Any = itemList[indexPath.item]
+        if let sul = itemData as? ([PQStuckPointMusicTagsModel], ([UICollectionViewLayoutAttributes], CGFloat)) {
+            let height: CGFloat = (sul.0.count > 0) ? (sul.1.1 + 35) : 0
+            return CGSize(width: collectionView.frame.width, height: height)
+        } else if itemData is BFEmptyModel {
+            return CGSize(width: collectionView.frame.width, height: 290)
+        } else {
+            return CGSize(width: collectionView.frame.width, height: cellHight)
+        }
+    }
+
+    func collectionView(_: UICollectionView, didSelectItemAt indexPath: IndexPath) {
+        if !isNetConnected() {
+            cShowHUB(superView: nil, msg: "请有网时再试")
+            return
+        }
+        if !(itemList[indexPath.item] is BFEmptyModel) {
+            if lastIndexPath != indexPath {
+                if contentType == .catagery {
+                    (itemList[lastIndexPath?.item ?? 0] as? PQStuckPointMusicTagsModel)?.isSelected = false
+                    (itemList[indexPath.item] as? PQStuckPointMusicTagsModel)?.isSelected = true
+                } else {
+                    (itemList[lastIndexPath?.item ?? 0] as? PQVoiceModel)?.isSelected = false
+                    (itemList[lastIndexPath?.item ?? 0] as? PQVoiceModel)?.isPlaying = false
+                    (itemList[indexPath.item] as? PQVoiceModel)?.isSelected = true
+                    (itemList[indexPath.item] as? PQVoiceModel)?.isPlaying = true
+                }
+                if lastIndexPath != nil {
+                    collectionView.reloadItems(at: [lastIndexPath!])
+                }
+                let cell = collectionView.cellForItem(at: indexPath) as? PQStuckPointMusicContentCell
+                cell?.bgmData = itemList[indexPath.item]
+                lastIndexPath = indexPath
+//                collectionView.reloadData()
+            } else if contentType != .catagery {
+                (itemList[indexPath.item] as? PQVoiceModel)?.isPlaying = !((itemList[indexPath.item] as? PQVoiceModel)?.isPlaying ?? false)
+                (itemList[indexPath.item] as? PQVoiceModel)?.isSelected = true
+                let cell = collectionView.cellForItem(at: indexPath) as? PQStuckPointMusicContentCell
+                cell?.bgmData = itemList[indexPath.item]
+//                collectionView.reloadItems(at: [indexPath])
+            }
+        }
+        if contentType == .catagery {
+            collectionView.scrollToItem(at: indexPath, at: .centeredVertically, animated: true)
+        }
+        if didSelectedHandle != nil {
+            didSelectedHandle!(false, contentType, indexPath, itemList[indexPath.item])
+        }
+        if itemList[indexPath.item] is PQVoiceModel {
+            if contentType == .page {
+                // 卡点视频音乐素材试听
+                PQEventTrackViewModel.baseReportUpload(businessType: .bt_buttonClick, objectType: .ot_click_auditionMusic, pageSource: .sp_stuck_selectSynceedUpMusic, extParams: ["musicName": (itemList[indexPath.item] as? PQVoiceModel)?.musicName ?? "", "musicId": (itemList[indexPath.item] as? PQVoiceModel)?.musicId ?? ""], remindmsg: "卡点视频数据上报-(点击上报:音乐素材试听)")
+            } else if contentType == .serach {
+                // 点击上报:试听音乐素材
+                PQEventTrackViewModel.baseReportUpload(businessType: .bt_buttonClick, objectType: .ot_click_auditionSearchMusic, pageSource: .sp_stuck_searchSyncedUpMusic, extParams: ["musicName": (itemList[indexPath.item] as? PQVoiceModel)?.musicName ?? "", "musicId": (itemList[indexPath.item] as? PQVoiceModel)?.musicId ?? "", "isHotMusic": itemList.first is BFEmptyModel], remindmsg: "卡点视频数据上报-(点击上报:试听音乐素材)")
+            }
+        } else if contentType == .catagery && (itemList[indexPath.item] is PQStuckPointMusicTagsModel) {
+            // 点击上报:选择音乐分类
+            PQEventTrackViewModel.baseReportUpload(businessType: .bt_buttonClick, objectType: .ot_click_chooseMusicCategory, pageSource: .sp_stuck_selectSynceedUpMusic, extParams: ["categoryName": (itemList[indexPath.item] as? PQStuckPointMusicTagsModel)?.tagName ?? "", "categoryId": (itemList[indexPath.item] as? PQStuckPointMusicTagsModel)?.tagId ?? ""], remindmsg: "卡点视频数据上报-(点击上报:选择音乐分类)")
+        }
+    }
+
+    func scrollViewDidScroll(_: UIScrollView) {
+        if scroDidScroHandle != nil {
+            scroDidScroHandle!()
+        }
+    }
+
+    func collectionView(_: UICollectionView, willDisplay _: UICollectionViewCell, forItemAt indexPath: IndexPath) {
+        let itemData: Any = itemList[indexPath.item]
+        if itemData is PQVoiceModel {
+            if contentType == .page {
+                // 卡点视频音乐音乐素材曝光
+                PQEventTrackViewModel.baseReportUpload(businessType: .bt_buttonView, objectType: .ot_view_syncedUpMusic, pageSource: .sp_stuck_selectSynceedUpMusic, extParams: ["musicName": (itemData as? PQVoiceModel)?.musicName ?? "", "musicId": (itemData as? PQVoiceModel)?.musicId ?? ""], remindmsg: "卡点视频数据上报-(曝光上报:音乐素材曝光)")
+            } else if contentType == .serach {
+                // 曝光上报:搜索结果音乐素材曝光
+                PQEventTrackViewModel.baseReportUpload(businessType: .bt_buttonView, objectType: .ot_view_searchMusic, pageSource: .sp_stuck_searchSyncedUpMusic, extParams: ["musicName": (itemData as? PQVoiceModel)?.musicName ?? "", "musicId": (itemData as? PQVoiceModel)?.musicId ?? "", "isHotMusic": itemList.first is BFEmptyModel], remindmsg: "卡点视频数据上报-(曝光上报:搜索结果音乐素材曝光)")
+            }
+        }
+    }
+}

+ 705 - 0
BFStuckPointKit/Classes/Controller/PQStuckPointMusicController.swift

@@ -0,0 +1,705 @@
+//
+//  PQStuckPointMusicController.swift
+//  PQSpeed
+//
+//  Created by SanW on 2021/4/28.
+//  Copyright © 2021 BytesFlow. All rights reserved.
+//
+
+import AVFoundation
+import UIKit
+import Photos
+import BFUIKit
+
+class PQStuckPointMusicController: BFBaseViewController {
+    // 选中的总时长
+    var selectedTotalDuration: Float64 = 0
+    // 选择的总数
+    var selectedDataCount: Int = 0
+    // 选择的图片总数
+    var selectedImageDataCount: Int = 0
+    weak var stuckPointEditVC : PQStuckPointEditerController?
+    var firstFrameImage:UIImage?
+    // 选中的素材数据
+    var selectedPhotoData: [PHAsset]?{
+        didSet {
+            if selectedPhotoData != nil && selectedPhotoData!.count > 0 {
+                let photo = selectedPhotoData!.first!
+                let option = PHImageRequestOptions()
+                option.isNetworkAccessAllowed = true //允许下载iCloud的图片
+                option.resizeMode = .none
+                option.deliveryMode = .highQualityFormat
+                let startTime = Date()
+                PHImageManager.default().requestImage(for: photo,
+                                                      targetSize: CGSize(width: 1920, height: 1920),
+                                                      contentMode: .aspectFit,
+                                                      options: option)
+                { (image, nil) in
+                     //image就是图片
+                    if image != nil {
+                        self.firstFrameImage = image
+                        if self.stuckPointEditVC != nil {
+                            self.stuckPointEditVC?.firstFrameImage = image
+                        }
+                        
+//                        BFLog(1, message: "aaa: \(Date().timeIntervalSince(startTime)), \(String(describing: image?.size))")
+                    }
+                }
+            }
+        }
+    }
+    // 选中的音乐数据
+    var selectedMusicData: PQVoiceModel?
+    /// 缓存数据
+    /// ["tagId":
+    ///         [
+    ///         "tagInfo":([PQStuckPointMusicTagsModel], ([UICollectionViewLayoutAttributes], CGFloat)),
+    ///         "pageList":[PQVoiceModel],
+    ///         "currentTag": PQStuckPointMusicTagsModel
+    ///         ]
+    /// ]
+    var cacheMusicData: [String: [String: Any]] = Dictionary<String, [String: Any]>.init()
+    // 热搜数据
+    var hotList: [Any] = Array<Any>.init()
+    // 左边距离
+    let leftMargin: CGFloat = 16
+    // 搜索拦高度
+    let searchTFH: CGFloat = 37
+    // 当前页码
+    var pageNum: Int = 0
+    // 当前标签信息-如果是热门则为空
+    var tagData: PQStuckPointMusicTagsModel?
+    // 当前播放的音乐
+    var currentPlayData: PQVoiceModel?
+    // 当前播放的视频
+    var playerItem: AVPlayerItem?
+
+    public var reCreateVideoData: PQReCreateModel? // 再创作数据
+    lazy var avPlayer: AVPlayer = {
+        let avPlayer = AVPlayer()
+        PQNotification.addObserver(forName: .AVPlayerItemDidPlayToEndTime, object: avPlayer.currentItem, queue: .main) { [weak self] notify in
+            BFLog(message: "AVPlayerItemDidPlayToEndTime = \(notify)")
+            avPlayer.seek(to: CMTime(value: CMTimeValue((self?.currentPlayData?.startTime ?? 0) * 1000), timescale: CMTimeScale(playerTimescale)))
+//            avPlayer.play()
+            self?.playStuckPointMusic(itemData: nil)
+        }
+        PQNotification.addObserver(forName: .AVPlayerItemNewErrorLogEntry, object: avPlayer.currentItem, queue: .main) { notify in
+            BFLog(message: "AVPlayerItemNewErrorLogEntry = \(notify)")
+        }
+        PQNotification.addObserver(forName: .AVPlayerItemFailedToPlayToEndTime, object: avPlayer.currentItem, queue: .main) { notify in
+            BFLog(message: "AVPlayerItemFailedToPlayToEndTime = \(notify)")
+        }
+        PQNotification.addObserver(forName: .AVPlayerItemPlaybackStalled, object: avPlayer.currentItem, queue: .main) { notify in
+            BFLog(message: "AVPlayerItemPlaybackStalled = \(notify)")
+        }
+        avPlayer.addPeriodicTimeObserver(forInterval: CMTime(value: 1, timescale: CMTimeScale(playerTimescale)), queue: .main) { [weak self] cmTime in
+            BFLog(message: "addPeriodicTimeObserver = \(cmTime)")
+        }
+        return avPlayer
+    }()
+
+    // 输入框清空按钮
+    lazy var clearBtn: UIButton = {
+        let clearBtn = UIButton(type: .custom)
+        clearBtn.setImage(UIImage.moduleImage(named: "icon_search_delete", moduleName: "BFFramework",isAssets: false), for: .normal)
+        clearBtn.frame = CGRect(x: 0, y: 0, width: 28, height: 32)
+        clearBtn.tag = 1
+        clearBtn.addTarget(self, action: #selector(btnClick(sender:)), for: .touchUpInside)
+        clearBtn.isHidden = true
+        return clearBtn
+    }()
+
+    // 搜索框
+    lazy var searchTF: UITextField = {
+        let searchTF = UITextField(frame: CGRect(x: leftMargin, y: cDevice_iPhoneNavBarAndStatusBarHei, width: cScreenWidth - leftMargin * 2, height: searchTFH))
+        searchTF.font = UIFont.systemFont(ofSize: 17)
+        searchTF.backgroundColor = BFConfig.shared.otherTintColor
+        searchTF.attributedPlaceholder = NSAttributedString(string: "搜索歌曲名称/歌手", attributes: [.foregroundColor: UIColor.hexColor(hexadecimal: "#BDBDBD"), .font: UIFont.systemFont(ofSize: 14)])
+        searchTF.textColor = BFConfig.shared.styleTitleColor
+        searchTF.addCorner(corner: searchTFH / 2)
+
+        searchTF.leftViewMode = .always
+        let leftView = UIView(frame: CGRect(x: 0, y: 0, width: 35, height: 32))
+        let imageView = UIImageView(image:UIImage.moduleImage(named: "icon_search_s", moduleName: "BFFramework",isAssets: false))
+        imageView.frame = CGRect(x: 15, y: 8, width: 16, height: 16)
+        leftView.addSubview(imageView)
+        searchTF.leftView = leftView
+        searchTF.delegate = self
+
+        searchTF.rightViewMode = .always
+        let rightView = UIView(frame: CGRect(x: 0, y: 0, width: 40, height: 32))
+        rightView.addSubview(clearBtn)
+        searchTF.rightView = rightView
+        searchTF.delegate = self
+        searchTF.returnKeyType = .search
+        searchTF.addTarget(self, action: #selector(editingChanged), for: .editingChanged)
+        return searchTF
+    }()
+
+    /// 一级分类视图
+    lazy var topCategoryController: PQStuckPointMusicContentController = {
+        let topCategoryController = PQStuckPointMusicContentController()
+        addChild(topCategoryController)
+        view.insertSubview(topCategoryController.view, belowSubview: searchController.view)
+        topCategoryController.updateViewFrame(newFrame: CGRect(x: 0, y: searchTF.frame.maxY + cDefaultMargin * 2, width: cDefaultMargin * 12, height: view.frame.height - (searchTF.frame.maxY + cDefaultMargin * 2)))
+        // 一级分类点击
+        topCategoryController.didSelectedHandle = { [weak self] _, _, _, itemData in
+            if itemData is PQStuckPointMusicTagsModel {
+                self?.musicPageController.updateCurrentPlayMusic(isPlaying: false, isClearCurrentMusic: true)
+                self?.searchController.updateCurrentPlayMusic(isPlaying: false, isClearCurrentMusic: true)
+                self?.tagData = itemData as? PQStuckPointMusicTagsModel
+                self?.dealWithCategorySelectedData(itemData: itemData as? PQStuckPointMusicTagsModel)
+                // 更新当前标签
+                self?.updateMusicCacheData(tagId: (itemData as? PQStuckPointMusicTagsModel)?.tagId ?? 0, tagInfo: nil, pageList: nil, currentTag: self?.tagData)
+            }
+            self?.playStuckPointMusic(itemData: nil, isClearCurrentMusic: true)
+        }
+        return topCategoryController
+    }()
+
+    /// 音乐列表视图
+    lazy var musicPageController: PQStuckPointMusicContentController = {
+        let musicPageController = PQStuckPointMusicContentController()
+        musicPageController.cellHight = cDefaultMargin * 8
+        addChild(musicPageController)
+        view.insertSubview(musicPageController.view, belowSubview: searchController.view)
+        musicPageController.updateViewFrame(newFrame: CGRect(x: topCategoryController.view.frame.maxX, y: topCategoryController.view.frame.minY, width: view.frame.width - topCategoryController.view.frame.maxX, height: topCategoryController.view.frame.height))
+        musicPageController.contentType = .page
+        musicPageController.didSelectedHandle = { [weak self] isTagsClick, _, _, itemData in
+            if isTagsClick { // 二级分类点击
+                self?.tagData = (itemData as? PQStuckPointMusicTagsModel)
+                let tagId: Int64 = (itemData as? PQStuckPointMusicTagsModel)?.tagId ?? 0
+                let parentTagId: Int64 = (itemData as? PQStuckPointMusicTagsModel)?.parentTagId ?? 0
+                // 请求列表数据
+                self?.requestPageListData(tagId: tagId, parentTagId: parentTagId)
+            } else {
+                self?.playStuckPointMusic(itemData: itemData as? PQVoiceModel)
+            }
+        }
+        musicPageController.refreshHandle = { [weak self] isRefresh, _ in
+            let tagId: Int64 = self?.tagData?.tagId ?? 0
+            let parentTagId: Int64 = self?.tagData?.parentTagId ?? 0
+//            self?.musicPageController.tagsInfo = (tags, tagAttributes) as? ([PQStuckPointMusicTagsModel], ([UICollectionViewLayoutAttributes], CGFloat))
+            // 请求列表数据
+            self?.requestPageListData(isRefresh: isRefresh, tagId: tagId, parentTagId: parentTagId)
+            // 请求二级标签数据
+//            self?.loadRequestSubTagsList()
+            self?.playStuckPointMusic(itemData: nil, isClearCurrentMusic: true)
+        }
+        musicPageController.btnClickHandle = { [weak self] _, bgmData in
+            // 使用音乐
+            self?.userstuckPointMusic(musicData: bgmData as? PQVoiceModel)
+        }
+        return musicPageController
+    }()
+
+    /// 搜索控制器
+    lazy var searchController: PQStuckPointMusicSearchController = {
+        let searchController = PQStuckPointMusicSearchController()
+        searchController.selectedDataCount = selectedDataCount
+        searchController.selectedImageDataCount = selectedImageDataCount
+        searchController.selectedTotalDuration = selectedTotalDuration
+        searchController.cellHight = cDefaultMargin * 8
+        addChild(searchController)
+        view.addSubview(searchController.view)
+        searchController.updateViewFrame(newFrame: CGRect(x: 0, y: cDevice_iPhoneNavBarAndStatusBarHei + cDefaultMargin * 2, width: view.frame.width, height: view.frame.height - (cDevice_iPhoneNavBarAndStatusBarHei + cDefaultMargin * 2)))
+        searchController.didSelectedHandle = { [weak self] isTagsClick, _, _, itemData in
+            if !isTagsClick {
+                self?.view.endEditing(true)
+                if !(itemData is BFEmptyModel) {
+                    self?.playStuckPointMusic(itemData: itemData as? PQVoiceModel)
+                }
+            }
+        }
+        searchController.btnClickHandle = { [weak self] _, bgmData in
+            // 使用音乐
+            self?.userstuckPointMusic(musicData: bgmData as? PQVoiceModel)
+        }
+        searchController.scroDidScroHandle = { [weak self] in
+            self?.view.endEditing(true)
+        }
+        searchController.view.isHidden = true
+        searchController.contentType = .serach
+        return searchController
+    }()
+
+    lazy var emptyRemindView: BFEmptyRemindView = {
+        let emptyRemindView = BFEmptyRemindView(frame: CGRect(x: 0, y: searchTF.frame.maxY + cDefaultMargin * 2, width: view.frame.width, height: view.frame.height - (searchTF.frame.maxY + cDefaultMargin * 2)))
+        emptyRemindView.remindLab.font = UIFont.systemFont(ofSize: 20)
+        emptyRemindView.remindLab.textColor = UIColor.hexColor(hexadecimal: "#575757")
+        emptyRemindView.isHidden = true
+        let emptyData = BFEmptyModel()
+        emptyData.title = "暂无音乐"
+        emptyRemindView.emptyData = emptyData
+        emptyRemindView.fullRefreshBloc = { [weak self] _, _ in
+            /// 请求标签数据
+            self?.loadRequestTagsList()
+        }
+        view.addSubview(emptyRemindView)
+        return emptyRemindView
+    }()
+
+//    /// 已选择音乐view
+//    lazy var selectedMusciView: PQStuckPointMusicContentCell = {
+//        let selectedMusciView = PQStuckPointMusicContentCell(frame: CGRect(x: 0, y: view.frame.height - cDefaultMargin * 6, width: cScreenWidth, height: cDefaultMargin * 6))
+//        selectedMusciView.confirmContentView.backgroundColor = UIColor.hexColor(hexadecimal: "#333333")
+//        selectedMusciView.playImageView.isHidden = true
+//        selectedMusciView.confirmBtn.setTitle("  继续使用  ", for: .normal)
+//        selectedMusciView.backgroundColor = UIColor.hexColor(hexadecimal: "#333333")
+//        view.addSubview(selectedMusciView)
+//        return selectedMusciView
+//    }()
+
+    override func viewDidLoad() {
+        super.viewDidLoad()
+        // 拦截侧滑返回
+        disablePopGesture().popGestureHandle = { [weak self] in
+            self?.backBtnClick()
+        }
+        leftButton(image: nil, tintColor: BFConfig.shared.styleTitleColor)
+        setTitle(title: "选择音乐主题", color: BFConfig.shared.styleTitleColor)
+        view.addSubview(searchTF)
+      
+        /// 请求标签数据
+        loadRequestTagsList()
+        PQNotification.addObserver(self, selector: #selector(enterBackground), name: UIApplication.didEnterBackgroundNotification, object: nil)
+        if selectedMusicData != nil {
+            let editerVC: PQStuckPointEditerController = PQStuckPointEditerController()
+            editerVC.stuckPointMusicData = selectedMusicData
+            editerVC.selectedDataCount = selectedDataCount
+            editerVC.selectedImageDataCount = selectedImageDataCount
+            //mdf by ak 进入编辑界面的时候去掉图片的时长
+            editerVC.selectedTotalDuration = selectedTotalDuration - (Double(selectedImageDataCount) * 1.5)
+            editerVC.selectedPhotoData = selectedPhotoData
+            editerVC.reCreateVideoData = reCreateVideoData
+            editerVC.isReCreate = true
+            navigationController?.pushViewController(editerVC, animated: true)
+            stuckPointEditVC = editerVC
+            if firstFrameImage != nil {
+                editerVC.firstFrameImage = firstFrameImage
+            }
+        }
+        // 卡点视频音乐选择曝光上报
+        PQEventTrackViewModel.baseReportUpload(businessType: .bt_windowView, objectType: .ot_view_selectSyncedUpMusic, pageSource: .sp_stuck_selectSynceedUpMusic, extParams: nil, remindmsg: "卡点视频数据上报-(曝光上报:卡点视频音乐选择页)")
+    }
+
+    override func viewWillAppear(_ animated: Bool) {
+        // mdf by ak 修复其它界面的键盘会影响这个界面问题
+        addKeyboardObserver()
+    }
+    override func viewWillDisappear(_ animated: Bool) {
+        super.viewWillDisappear(animated)
+        playStuckPointMusic(itemData: nil)
+        
+        PQNotification.removeObserver(self, name: UIResponder.keyboardWillShowNotification, object: nil)
+        PQNotification.removeObserver(self, name: UIResponder.keyboardWillHideNotification, object: nil)
+ 
+ 
+    }
+
+    /// 按钮点击事件
+    /// - Parameter sender: <#sender description#>
+    /// - Returns: <#description#>
+    @objc func btnClick(sender: UIButton) {
+        switch sender.tag {
+        case 1: // 清除
+            searchTF.text = nil
+//            emptyRemindView.isHidden = true
+            clearBtn.isHidden = true
+        default:
+            break
+        }
+    }
+
+    /// 请求标签数据
+    /// - Returns: <#description#>
+    func loadRequestTagsList() {
+        BFLoadingHUB.shared.showHUB()
+        PQStuckPointViewModel.stuckPointMusicCategoryList { [weak self] tags, _, _ in
+            BFLoadingHUB.shared.dismissHUB()
+            if tags.count > 0 {
+                self?.tagData = tags.first
+            }
+            self?.emptyRemindView.isHidden = tags.count > 0
+            if tags.count > 0 {
+                self?.topCategoryController.configMusicListData(isRefresh: true, musicListData: tags)
+                // 请求列表数据
+                self?.requestPageListData(isHotPage: true, tagId: tags.first?.tagId ?? 0, parentTagId: 0)
+            }
+        }
+    }
+
+    /// 请求二级标签数据
+    /// - Returns: <#description#>
+    func loadRequestSubTagsList() {
+        BFLoadingHUB.shared.showHUB()
+        PQStuckPointViewModel.stuckPointMusicCategoryList(parentTagId: tagData?.tagId ?? 0) { [weak self] tags, _, tagAttributes in
+            BFLoadingHUB.shared.dismissHUB()
+            if tagAttributes != nil {
+                let tagId: Int64 = tags.count > 0 ? (tags.first?.tagId ?? 0) : (self?.tagData?.tagId ?? 0)
+                let parentTagId: Int64 = tags.count > 0 ? (self?.tagData?.tagId ?? 0) : 0
+                self?.musicPageController.tagsInfo = (tags, tagAttributes) as? ([PQStuckPointMusicTagsModel], ([UICollectionViewLayoutAttributes], CGFloat))
+                if tags.count > 0 {
+                    self?.tagData = tags.first
+                }
+                // 添加缓存
+                self?.updateMusicCacheData(tagId: parentTagId > 0 ? parentTagId : tagId, isClearOld: false, tagInfo: self?.musicPageController.tagsInfo, pageList: nil, currentTag: self?.tagData)
+                // 请求列表数据
+                self?.requestPageListData(tagId: tagId, parentTagId: parentTagId)
+            } else {
+                // 重置数据
+                self?.resetPageData(isResetData: false)
+            }
+        }
+    }
+
+    /// 处理cell点击回调
+    /// - Parameter itemData: <#itemData description#>
+    /// - Returns: <#description#>
+    func dealWithCategorySelectedData(itemData: PQStuckPointMusicTagsModel?) {
+        tagData = itemData
+        // 重置数据
+        resetPageData(isResetData: true)
+        let cacheData = getMusicCacheData(itemData: itemData)
+        if cacheData != nil, (cacheData?.0 != nil && (cacheData?.0?.0.count ?? 0) > 0) || (cacheData?.1 != nil && (cacheData?.1?.count ?? 0) > 0) {
+            musicPageController.tagsInfo = cacheData?.0
+            musicPageController.configMusicListData(isRefresh: true, musicListData: cacheData?.1 ?? [])
+            pageNum = ((cacheData?.1?.count ?? 0) / 20) + 1
+            tagData = cacheData?.2
+        } else {
+            pageNum = 1
+            // 请求二级标签数据
+            loadRequestSubTagsList()
+        }
+    }
+
+    /// 请求列表数据
+    /// - Parameters:
+    ///   - tagId: <#tagId description#>
+    ///   - parentTagId: <#parentTagId description#>
+    ///   - tagsInfo: <#tagsInfo description#>
+    /// - Returns: <#description#>
+    func requestPageListData(isRefresh: Bool = true, isHotPage: Bool = false, tagId: Int64, parentTagId: Int64) {
+        BFLoadingHUB.shared.showHUB()
+        if isRefresh {
+            pageNum = 1
+        }
+        PQStuckPointViewModel.stuckPointMusicPageList(tagId: tagId, parentTagId: parentTagId, pageNum: pageNum, videoCount: selectedDataCount - selectedImageDataCount, imageCount: selectedImageDataCount, totalDuration: selectedTotalDuration) { [weak self] musicInfo, _ in
+            BFLoadingHUB.shared.dismissHUB()
+            if musicInfo.count > 0 {
+                self?.pageNum = (self?.pageNum ?? 0) + 1
+            }
+            self?.musicPageController.configMusicListData(isRefresh: isRefresh, musicListData: musicInfo)
+            // 添加缓存
+            self?.updateMusicCacheData(tagId: parentTagId > 0 ? parentTagId : tagId, isClearOld: isRefresh, tagInfo: nil, pageList: musicInfo, currentTag: nil)
+            if musicInfo.count > 0, isHotPage, isRefresh {
+                self?.hotList = musicInfo
+                self?.searchController.hotList = self?.hotList ?? []
+                self?.searchController.configMusicListData(isResetData: false, isRefresh: true, musicListData: musicInfo)
+            }
+        }
+    }
+
+    /// 重置页面数据
+    /// - Parameter isResetData: <#isResetData description#>
+    /// - Returns: <#description#>
+    func resetPageData(isResetData: Bool) {
+        musicPageController.tagsInfo = nil
+        musicPageController.configMusicListData(isResetData: isResetData, isRefresh: true, musicListData: [])
+        pageNum = 1
+    }
+
+    var avPlayerTimeObserver:Any?
+    /// 播放音乐
+    /// - Parameter itemData: <#itemData description#>
+    func playStuckPointMusic(itemData: PQVoiceModel?, isClearCurrentMusic: Bool = false) {
+        if itemData != nil, currentPlayData != itemData {
+            if !isValidURL(url: itemData?.musicPath ?? "") {
+                cShowHUB(superView: nil, msg: "本歌曲暂无伴奏版本哦~")
+                return
+            }
+            avPlayer.pause()
+            playerItem?.removeObserver(self, forKeyPath: "status")
+            playerItem?.removeObserver(self, forKeyPath: "error")
+            if avPlayerTimeObserver != nil {
+                avPlayer.removeTimeObserver(avPlayerTimeObserver as Any)
+            }
+            
+            playerItem = AVPlayerItem(url: URL(string: itemData?.musicPath ?? "")!)
+            if (itemData?.endTime ?? 0) > 0, (itemData?.endTime ?? 0) > (itemData?.startTime ?? 0) {
+                playerItem?.forwardPlaybackEndTime = CMTime(value: CMTimeValue((itemData?.endTime ?? 0) * playerTimescale), timescale: CMTimeScale(playerTimescale))
+            }
+            avPlayer.replaceCurrentItem(with: playerItem)
+            playerItem?.addObserver(self, forKeyPath: "status", options: .new, context: nil)
+            playerItem?.addObserver(self, forKeyPath: "error", options: .new, context: nil)
+            avPlayer.seek(to: CMTime(value: CMTimeValue((itemData?.startTime ?? 0) * playerTimescale), timescale: CMTimeScale(playerTimescale)))
+            avPlayer.play()
+            
+            avPlayerTimeObserver = avPlayer.addPeriodicTimeObserver(forInterval: CMTime(value: 1, timescale: 10), queue: DispatchQueue.global()) {[weak self] time in
+                if fabs(CMTimeGetSeconds(time) - (itemData?.startTime ?? 0)) > 0.1 {
+                    self?.avPlayer.removeTimeObserver(self?.avPlayerTimeObserver as Any)
+                    self?.avPlayerTimeObserver = nil
+                    // 停止cell loading动画
+                    PQNotification.post(name: NSNotification.Name(rawValue: "MusicContentCellIconLoadingAnimationStop"), object: nil)
+                }
+             //    进度监控
+//                BFLog(1, message: "\(Int(CMTimeGetSeconds((self?.playerItem?.asset.duration)!))),curr:\(CMTimeGetSeconds(time))")
+            }
+//            let player = TXVodPlayer()
+//            let config = TXVodPlayConfig()
+//            config.cacheFolderPath = videoCacheDirectory
+//            config.maxCacheItems = 0
+//            player.config = config
+            ////            player.vodDelegate = self
+//            player.setRenderMode(.RENDER_MODE_FILL_EDGE)
+//            player.startPlay("https://clipres.yishihui.com/longvideo/material/voice/prod/20210512/MUSIC_QQ_000T1Ws32MWrUj")
+            currentPlayData = itemData
+        } else if itemData != nil, avPlayer.rate == 0.0 {
+            avPlayer.play()
+        } else {
+            avPlayer.pause()
+            musicPageController.updateCurrentPlayMusic(isPlaying: false, isClearCurrentMusic: isClearCurrentMusic)
+            searchController.updateCurrentPlayMusic(isPlaying: false, isClearCurrentMusic: isClearCurrentMusic)
+        }
+        if isClearCurrentMusic {
+            avPlayer.pause()
+            currentPlayData = nil
+        }
+    }
+
+    /// 使用卡点音乐
+    /// - Parameter musicData: <#musicData description#>
+    /// - Returns: <#description#>
+    func userstuckPointMusic(musicData: PQVoiceModel?) {
+        if musicData != nil {
+            let editerVC: PQStuckPointEditerController = PQStuckPointEditerController()
+            editerVC.selectedDataCount = selectedDataCount
+            editerVC.selectedImageDataCount = selectedImageDataCount
+            editerVC.selectedTotalDuration = selectedTotalDuration - (Double(selectedImageDataCount) * 1.5)
+            editerVC.stuckPointMusicData = musicData
+            editerVC.selectedPhotoData = selectedPhotoData
+            navigationController?.pushViewController(editerVC, animated: true)
+            stuckPointEditVC = editerVC
+            if firstFrameImage != nil {
+                editerVC.firstFrameImage = firstFrameImage
+            }
+        }
+    }
+
+    /// 返回按钮操作
+    override func backBtnClick() {
+        if searchController.view.isHidden {
+            navigationController?.popViewController(animated: true)
+            // 点击上报:选择音乐分类
+            PQEventTrackViewModel.baseReportUpload(businessType: .bt_buttonClick, objectType: .ot_click_back, pageSource: .sp_stuck_selectSynceedUpMusic, extParams: nil, remindmsg: "卡点视频数据上报-(点击上报:返回按钮)")
+        } else {
+            hiddenSearchController()
+            // 点击上报:返回按钮
+            PQEventTrackViewModel.baseReportUpload(businessType: .bt_buttonClick, objectType: .ot_click_back, pageSource: .sp_stuck_searchSyncedUpMusic, extParams: nil, remindmsg: "卡点视频数据上报-(点击上报:返回按钮)")
+        }
+    }
+
+    /// 进入后台暂停播放
+    @objc func enterBackground() {
+        playStuckPointMusic(itemData: nil)
+    }
+
+    deinit {
+        PQNotification.removeObserver(self)
+        PQNotification.removeObserver(self.avPlayer.currentItem as Any)
+        avPlayer.currentItem?.removeObserver(self, forKeyPath: "status")
+        avPlayer.currentItem?.removeObserver(self, forKeyPath: "error")
+        avPlayer.pause()
+        avPlayer.replaceCurrentItem(with: nil)
+        playerItem = nil
+    }
+}
+
+// MARK: - 音乐搜索相关
+
+/// 音乐搜索相关
+extension PQStuckPointMusicController: UITextFieldDelegate {
+    /// 点击输入框
+    @objc func editingChanged() {
+        if searchTF.text != nil, (searchTF.text?.count ?? 0) > 0, searchTF.markedTextRange == nil {
+            if searchTF.text?.isSpace ?? false {
+                return
+            }
+//            loadSearchData()
+            clearBtn.isHidden = false
+        }
+    }
+
+    /// 搜索
+    @objc func loadSearchData() {
+        if searchTF.text != nil, (searchTF.text?.count ?? 0) > 0, searchTF.text?.isSpace ?? false {
+            cShowHUB(superView: nil, msg: "搜索内容不能为空")
+            return
+        }
+        if searchTF.text != nil, (searchTF.text?.count ?? 0) > 0 {
+            BFLog(message: "背景音乐--开始搜索背景音乐-1")
+            searchController.loadSearchData(keyword: searchTF.text)
+        }
+    }
+
+    /// 添加键盘监听
+    /// - Returns: <#description#>
+    func addKeyboardObserver() {
+        // 监听键盘的显示和隐藏
+        PQNotification.addObserver(self, selector: #selector(keyboardWillShow(notification:)), name: UIResponder.keyboardWillShowNotification, object: nil)
+        PQNotification.addObserver(self, selector: #selector(keyboardWillHidden), name: UIResponder.keyboardWillHideNotification, object: nil)
+    }
+
+    /// 键盘显示
+    /// - Parameter notification: <#notification description#>
+    @objc func keyboardWillShow(notification: Notification) {
+        let duration: TimeInterval = TimeInterval("\(notification.userInfo?["UIKeyboardAnimationDurationUserInfoKey"] ?? "1")") ?? 1
+        UIView.animate(withDuration: duration) { [weak self] in
+            self?.navTitleLabel?.alpha = 0
+            self?.searchTF.frame = CGRect(x: cDefaultMargin * 5, y: (cDevice_iPhoneNavBarHei - (self?.searchTFH ?? 37.0)) / 2 + cDevice_iPhoneStatusBarHei, width: cScreenWidth - cDefaultMargin * 7, height: self?.searchTFH ?? 37.0)
+        } completion: { _ in
+        }
+        searchController.view.isHidden = false
+        hotList.forEach { item in
+            if item is PQVoiceModel {
+                (item as? PQVoiceModel)?.isSelected = false
+                (item as? PQVoiceModel)?.isPlaying = false
+            }
+        }
+        musicPageController.collectionView.reloadData()
+        searchController.hotList = hotList
+        playStuckPointMusic(itemData: nil)
+        // 曝光上报:音乐素材搜索页
+        PQEventTrackViewModel.baseReportUpload(businessType: .bt_windowView, objectType: .ot_view_searchSyncedUpMusic, pageSource: .sp_stuck_searchSyncedUpMusic, extParams: nil, remindmsg: "卡点视频数据上报-(曝光上报:音乐素材搜索页)")
+    }
+
+    /// 键盘将要隐藏
+    @objc func keyboardWillHidden() {}
+
+    /// 隐藏搜索界面
+    /// - Returns: <#description#>
+    func hiddenSearchController() {
+        view.endEditing(true)
+        UIView.animate(withDuration: 0.3) { [weak self] in
+            self?.navTitleLabel?.alpha = 1
+            self?.searchTF.frame = CGRect(x: self?.leftMargin ?? 0, y: cDevice_iPhoneNavBarAndStatusBarHei, width: cScreenWidth - (self?.leftMargin ?? 0) * 2, height: self?.searchTFH ?? 0)
+        } completion: { _ in
+        }
+        clearBtn.isHidden = true
+        searchController.view.isHidden = true
+//        searchTF.text = nil
+//        searchController.clearData()
+//        currentPlayData?.isSelected = false
+//        currentPlayData?.isPlaying = false
+        playStuckPointMusic(itemData: nil)
+    }
+
+    override func touchesBegan(_: Set<UITouch>, with _: UIEvent?) {
+        view.endEditing(true)
+    }
+
+    func textFieldShouldReturn(_ textField: UITextField) -> Bool {
+        loadSearchData()
+        view.endEditing(true)
+        if textField.text == nil || (textField.text?.count ?? 0) <= 0 {
+            cShowHUB(superView: nil, msg: "请先输入搜索内容")
+        }
+        return true
+    }
+}
+
+// MARK: - 缓存相关
+//缓存歌曲列表 KEY
+let kSaveMusicListDatas = "kSaveMusicListDatas"
+/// 缓存相关
+extension PQStuckPointMusicController {
+    /// 获取缓存数据
+    /// - Parameter itemData: <#itemData description#>
+    /// - Returns: <#description#>
+    func getMusicCacheData(itemData: PQStuckPointMusicTagsModel?) -> (([PQStuckPointMusicTagsModel], ([UICollectionViewLayoutAttributes], CGFloat))?, [PQVoiceModel]?, PQStuckPointMusicTagsModel?)? {
+        let saveMusicListStr =  getUserDefaults(key: kSaveMusicListDatas)
+    
+        if(((saveMusicListStr as? String)?.count ?? 0 ) > 0){
+            let cacheMusicDataTemp = jsonStringToDictionary(saveMusicListStr as! String)
+            
+            if itemData != nil, cacheMusicData.keys.contains("\(itemData?.tagId ?? 0)") {
+                let tempData = cacheMusicDataTemp?["\(itemData?.tagId ?? 0)"] as? Dictionary<String, Any>
+                
+                return (tempData?["tagInfo"] as? ([PQStuckPointMusicTagsModel],([UICollectionViewLayoutAttributes], CGFloat)),tempData?["pageList"] as? [PQVoiceModel],tempData?["currentTag"] as? PQStuckPointMusicTagsModel)
+
+            } else {
+                return nil
+            }
+        }else{ return nil}
+     
+    }
+
+    /// 更新缓存
+    /// - Parameters:
+    ///   - tagId: 一级分类id
+    ///   - isClearOld: 是否清空老数据
+    ///   - tagInfo: 二级标签信息
+    ///   - pageList: 音乐数据
+    ///   - currentTag: 当前页当前标签
+    /// - Returns: <#description#>
+    func updateMusicCacheData(tagId: Int64, isClearOld: Bool = false, tagInfo: ([PQStuckPointMusicTagsModel], ([UICollectionViewLayoutAttributes], CGFloat))?, pageList: [PQVoiceModel]?, currentTag: PQStuckPointMusicTagsModel?) {
+        if (tagInfo != nil && (tagInfo?.0.count ?? 0) > 0) || (pageList != nil && (pageList?.count ?? 0) > 0) || currentTag != nil {
+            if cacheMusicData.keys.contains("\(tagId)") {
+                var tempDic = cacheMusicData["\(tagId)"]
+                if tagInfo != nil, (tagInfo?.0.count ?? 0) > 0 {
+                    tempDic?["tagInfo"] = tagInfo!
+                }
+                if pageList != nil, (pageList?.count ?? 0) > 0 {
+                    var oldPageList: [PQVoiceModel] = (tempDic?["pageList"] as? [PQVoiceModel]) ?? Array<PQVoiceModel>.init()
+                    if isClearOld {
+                        oldPageList.removeAll()
+                    }
+                    if oldPageList.count > 0 {
+                        tempDic?["pageList"] = oldPageList + pageList!
+                    } else {
+                        tempDic?["pageList"] = pageList!
+                    }
+                }
+                if currentTag != nil {
+                    tempDic?["currentTag"] = currentTag!
+                }
+                cacheMusicData["\(tagId)"] = tempDic ?? Dictionary<String, Any>.init()
+            } else {
+                var tempDic: [String: Any] = Dictionary<String, Any>.init()
+                if tagInfo != nil, (tagInfo?.0.count ?? 0) > 0 {
+                    tempDic["tagInfo"] = tagInfo!
+                }
+                if pageList != nil, (pageList?.count ?? 0) > 0 {
+                    tempDic["pageList"] = pageList!
+                }
+                if currentTag != nil {
+                    tempDic["currentTag"] = currentTag!
+                }
+                cacheMusicData["\(tagId)"] = tempDic
+            }
+            
+            saveUserDefaults(key: kSaveMusicListDatas, value: dictionaryToJsonString(cacheMusicData) as Any)
+        
+        }
+    }
+}
+
+extension PQStuckPointMusicController {
+    override func observeValue(forKeyPath keyPath: String?, of object: Any?, change _: [NSKeyValueChangeKey: Any]?, context _: UnsafeMutableRawPointer?) {
+        if object is AVPlayerItem, keyPath == "status" {
+            BFLog(message: "AVPlayerItem - status = \((object as! AVPlayerItem).status.rawValue)")
+            switch (object as! AVPlayerItem).status {
+            case .unknown:
+                break
+            case .readyToPlay:
+                break
+            case .failed:
+                break
+            default:
+                break
+            }
+        } else if object is AVPlayerItem, keyPath == "error" {
+            BFLog(message: "AVPlayerItem - error = \(String(describing: (object as! AVPlayerItem).error))")
+        }
+    }
+}

+ 102 - 0
BFStuckPointKit/Classes/Controller/PQStuckPointMusicSearchController.swift

@@ -0,0 +1,102 @@
+//
+//  PQStuckPointMusicSearchController.swift
+//  PQSpeed
+//
+//  Created by SanW on 2021/5/6.
+//  Copyright © 2021 BytesFlow. All rights reserved.
+//
+
+import UIKit
+import BFCommonKit
+import BFUIKit
+
+class PQStuckPointMusicSearchController: PQStuckPointMusicContentController {
+    // 选中的总时长
+    var selectedTotalDuration: Float64 = 0
+    // 选择的总数
+    var selectedDataCount: Int = 0
+    // 选择的图片总数
+    var selectedImageDataCount: Int = 0
+    // 当前搜索页数
+    var pageNum: Int = 1
+    // 当前搜索关键字
+    var searchWord: String?
+    // 当前选中的index
+    var lastSelectedIndex: IndexPath?
+    // 热搜数据
+    var hotList: [Any] = Array<Any>.init()
+
+    override func viewDidLoad() {
+        super.viewDidLoad()
+//        emptyData?.title = "没有搜索到相关音乐"
+//        emptyData?.emptyImageName = "pic_search_empty"
+//        emptyRemindView.remindLab.font = UIFont.systemFont(ofSize: 14)
+//        emptyRemindView.remindLab.textColor = UIColor.hexColor(hexadecimal: "#999999")
+//        emptyRemindView.frame = CGRect(x: 0, y: 0, width: view.frame.width, height: 200)
+//        emptyRemindView.emptyData = emptyData
+//        refreshHandle = { [weak self] _, _ in
+//            self?.loadRequestData(isRefresh: false)
+//        }
+    }
+
+    /// 开始搜索
+    /// - Parameter keyword: 搜索文字
+    /// - Returns: <#description#>
+    func loadSearchData(keyword: String?) {
+        if keyword == nil || (keyword?.count ?? 0) <= 0 {
+            cShowHUB(superView: nil, msg: "请先输入搜索内容")
+            return
+        }
+        BFLog(message: "背景音乐--开始搜索背景音乐")
+        if keyword != searchWord || (keyword == searchWord && itemList.count <= 0) {
+            showEmptyView()
+            itemList.removeAll()
+            collectionView.reloadData()
+            pageNum = 0
+            searchWord = keyword
+//            SWNetRequest.cancelTask(url: PQENVUtil.shared.materialSearchApi + searchBGMMaterialUrl)
+            loadRequestData()
+        }
+    }
+
+    // 发起搜索请求
+    @objc func loadRequestData(isRefresh: Bool = true) {
+        pageNum = pageNum + 1
+        PQBaseViewModel.searchBGMListData(searchWord, pageNum, 20, videoCount: selectedDataCount - selectedImageDataCount, imageCount: selectedImageDataCount, totalDuration: selectedTotalDuration) { [weak self] bgmList, msg in
+            // 处理请求数据
+            BFLog(message: "背景音乐--搜索背景音乐成功")
+            if bgmList.count <= 0 {
+                self?.pageNum = (self?.pageNum ?? 0) - 1
+            }
+ 
+            if bgmList.count <= 0, (self?.itemList.count ?? 0) <= 0, (self?.hotList.count ?? 0) > 0 {
+                if !(self?.hotList.first is BFEmptyModel){
+                    self?.hotList.insert(BFEmptyModel(), at: 0)
+                }
+              
+                self?.configMusicListData(isRefresh: true, musicListData: self?.hotList ?? [])
+            } else {
+                self?.configMusicListData(isRefresh: isRefresh, musicListData: bgmList)
+            }
+            // 点击上报:用户在搜索框输入文字然后按回车-返回结果后上报
+            PQEventTrackViewModel.baseReportUpload(businessType: .bt_buttonClick, objectType: .ot_click_searchSyncedUpMusic, pageSource: .sp_stuck_searchSyncedUpMusic, extParams: ["searchText": self?.searchWord ?? "", "searchResultNumber": bgmList.count, "isSuccess": msg == nil], remindmsg: "卡点视频数据上报-(点击上报:用户在搜索框输入文字然后按回车)")
+        }
+    }
+
+    /// 清空搜索数据
+    /// - Returns: <#description#>
+    func clearData() {
+        BFLog(message: "背景音乐--清空背景音乐搜索数据")
+        hotList.forEach { item in
+            if item is PQVoiceModel {
+                (item as? PQVoiceModel)?.isSelected = false
+                (item as? PQVoiceModel)?.isPlaying = false
+            }
+        }
+        itemList.removeAll()
+        searchWord = nil
+        pageNum = 0
+        lastSelectedIndex = nil
+        collectionView.reloadData()
+    }
+}

+ 1947 - 0
BFStuckPointKit/Classes/Controller/PQStuckPointPublicController.swift

@@ -0,0 +1,1947 @@
+//
+//  PQStuckPointPublicController.swift
+//  PQSpeed
+//
+//  Created by SanW on 2021/5/6.
+//  Copyright © 2021 BytesFlow. All rights reserved.
+//
+
+import Alamofire
+import BFCommonKit
+import BFUIKit
+import Kingfisher
+import ObjectMapper
+import Photos
+import UIKit
+import WechatOpenSDK
+
+// mdf by ak 按 UI图 下方操作区的高度是固定的, 其它区高度和设备自适应
+public let bottomOprationBgViewHeight: CGFloat = 322.0
+class PQStuckPointPublicController: BFBaseViewController {
+    private var isShared: Bool = false // 是否在分享
+    private var isExportSuccess: Bool = false // 是否导出完成
+    private var isSaveDraftSuccess: Bool = false // 是否保存草稿完成
+    private var isSaveProjectSuccess: Bool = false // 是否保存项目完成
+    private var isUploadSuccess: Bool = false // 是否上传完成
+    private var isPublicSuccess: Bool = false // 是否发布完成
+    // 导出正片的地址
+    private var exportLocalURL: URL?
+    // 再创作数据
+    private var reCreateData: PQReCreateModel?
+    // 确定上传的数据
+    private var uploadData: PQUploadModel?
+    // 发布成功的视频数据
+    private var videoData: PQVideoListModel?
+    // 视频创作埋点数据
+    private var eventTrackData: PQVideoMakeEventTrackModel?
+    // 选中的总时长-统计使用
+    var selectedTotalDuration: Float64 = 0
+    // 选择的总数-统计使用
+    var selectedDataCount: Int = 0
+    // 选择的图片总数-统计使用
+    var selectedImageDataCount: Int = 0
+
+    // 最大的宽度
+    private var maxWidth: CGFloat = cScreenWidth
+    // 最大的高度
+    private var maxHeight: CGFloat = cScreenHeigth - bottomOprationBgViewHeight - cDevice_iPhoneNavBarAndStatusBarHei
+    // 开始导出的时间
+    private let startExportDate: Float64 = Date().timeIntervalSince1970
+    // 导出结束的时间
+    private var exportEndDate: Float64 = Date().timeIntervalSince1970
+    // 取到的封面 给发布界面使用
+    public var coverImage: UIImage?
+    // 导出视频工具类
+    private var exporter: PQCompositionExporter!
+    // 导出进度
+    private var exportProgrss = 0
+    var mStickers: [PQEditVisionTrackMaterialsModel]?
+
+    var remindView: BFRemindView?
+    // 已经选择标题内容,加一个属性接收 使用有不在主线不能直接使用 titleLabel text
+    var selectTitle: String = ""
+
+    // add by ak 玩法类型 调用 producevideo/saveProject 时使用
+    var rhythmMode: createStickersModel = .createStickersModelPoint
+
+    // add by ak 设置的速度
+    var syncedUpVideoSpeedMax: Float = 0.0
+    var syncedUpVideoSpeedMin: Float = 0.0
+
+    // add by ak 是否是再创作模式
+    var isReCreate: Bool = false
+
+    // 最终使用的音频时长,用于拼接音乐使用
+    var finallyUserAudioTime: Float = 0.0
+    // 拼接音乐的开始和结束位置
+    var clipAudioRange: CMTimeRange = CMTimeRange.zero
+    // 导出的开始的开始和结束时间
+    var playeTimeRange: CMTimeRange = CMTimeRange()
+
+    // ---------------------------add by ak 保存系统相册使用的变量
+    // 导出有水印的正片
+    private var watermarkMovieExporter: PQCompositionExporter!
+    // 带水印 MP4 导出地址
+    private var watermarkMovieLocalURL: URL?
+    // 导出片尾
+    private var endMovieExporter: PQCompositionExporter!
+    // 导出片尾 MP4 地址
+    private var endMovieLocalURL: URL?
+    // 保存相册的合成视频地址 水印+片尾 MP4 地址
+    private var saveMovieLocalURL: URL?
+
+    private var isSaveingLocalVideo = false
+
+    // ----------------------------
+
+    // 预览大小
+    private var preViewSize: CGSize {
+        switch aspectRatio {
+        case let .origin(width, height):
+            var tempHeight: CGFloat = 0
+            var tempWidth: CGFloat = 0
+            if width > height {
+                tempWidth = maxWidth
+                tempHeight = (maxWidth * height / width)
+                if tempHeight > maxHeight {
+                    tempHeight = maxHeight
+                    tempWidth = (maxHeight * width / height)
+                }
+            } else {
+                tempHeight = maxHeight
+                tempWidth = (maxHeight * width / height)
+                if tempWidth > maxWidth {
+                    tempWidth = maxWidth
+                    tempHeight = (maxWidth * height / width)
+                }
+            }
+            if tempHeight.isNaN || tempWidth.isNaN {
+                return CGSize.zero
+            } else {
+                return CGSize(width: tempWidth, height: tempHeight)
+            }
+        case .oneToOne:
+            if maxWidth > maxHeight {
+                return CGSize(width: maxHeight, height: maxHeight)
+            } else {
+                return CGSize(width: maxWidth, height: maxWidth)
+            }
+        case .sixteenToNine:
+            return CGSize(width: maxWidth, height: maxWidth * 9.0 / 16.0)
+        case .nineToSixteen:
+            return CGSize(width: maxHeight * 9.0 / 16.0, height: maxHeight)
+        default:
+            break
+        }
+        return CGSize(width: maxHeight, height: maxHeight)
+    }
+
+    // 背景音乐
+    var audioMixModel: PQVoiceModel?
+    // 画面比例
+    var aspectRatio: aspectRatio?
+    // 导出的项目数据
+    var editProjectModel: PQEditProjectModel? {
+        didSet {
+            aspectRatio = PQPlayerViewModel.videoCanvasTypeToAspectRatio(projectModel: editProjectModel)
+            var totalDuration: Float64 = 0
+            if editProjectModel?.sData?.sections.count ?? 0 > 0 {
+                for section in (editProjectModel?.sData?.sections)! {
+                    totalDuration = totalDuration + section.sectionDuration
+                }
+            }
+            editProjectModel?.sData?.videoMetaData?.duration = totalDuration
+            if editProjectModel?.sData?.sections != nil, (editProjectModel?.sData?.sections.count ?? 0) > 0 {
+                // 查找出背景图并设置
+                var coverImageMaterialsModel: PQEditVisionTrackMaterialsModel?
+                for section in (editProjectModel?.sData?.sections)! {
+                    if coverImageMaterialsModel != nil {
+                        break
+                    }
+                    coverImageMaterialsModel = section.sectionTimeline?.visionTrack?.getEnableVisionTrackMaterials().first
+                }
+                if coverImageMaterialsModel != nil {
+                    playerHeaderView.image = coverImage
+                    playerHeaderView.contentMode = coverImageMaterialsModel!.canvasFillType == stickerContentMode.aspectFitStr.rawValue ? .scaleAspectFill : .scaleAspectFit
+                }
+            }
+        }
+    }
+
+    /// 所有需要导出的filter
+    var filters: Array = Array<ImageProcessingOperation>.init()
+    /// 预览背景页
+    lazy var bgTopView: UIView = {
+        let bgTopView = UIView(frame: CGRect(x: 0, y: cDevice_iPhoneNavBarAndStatusBarHei, width: cScreenWidth, height: maxHeight))
+        bgTopView.backgroundColor = BFConfig.shared.styleBackGroundColor
+        return bgTopView
+    }()
+
+    // 预览界面
+    var playerHeaderView: UIImageView = {
+        let playerHeaderView = UIImageView(frame: CGRect(x: 0, y: cDevice_iPhoneNavBarAndStatusBarHei, width: cScreenWidth, height: 0))
+        playerHeaderView.isUserInteractionEnabled = true
+        playerHeaderView.contentMode = .scaleAspectFit
+        playerHeaderView.clipsToBounds = true
+        return playerHeaderView
+    }()
+
+    // add by ak 播放器的封面 为了不和原有的播放器层级单独添加一个 view
+    lazy var playerHeaderCoverImageView: UIImageView = {
+        let playerHeaderCoverImageView = UIImageView()
+        playerHeaderCoverImageView.isUserInteractionEnabled = true
+        playerHeaderCoverImageView.contentMode = .scaleAspectFit
+        playerHeaderCoverImageView.clipsToBounds = true
+
+        let playBtn = UIButton(type: .custom)
+        playBtn.setImage(UIImage.moduleImage(named: "icon_video_play", moduleName: "BFFramework", isAssets: false), for: .normal)
+        playBtn.tag = 4
+        playBtn.isUserInteractionEnabled = false
+        playerHeaderCoverImageView.addSubview(playBtn)
+        playerHeaderCoverImageView.isHidden = true
+        return playerHeaderCoverImageView
+    }()
+
+    /// 播放器
+    lazy var avPlayer: AVPlayer = {
+        let avPlayer = AVPlayer()
+        NotificationCenter.default.addObserver(forName: .AVPlayerItemDidPlayToEndTime, object: avPlayer.currentItem, queue: .main) { [weak self] notify in
+            BFLog(message: "AVPlayerItemDidPlayToEndTime = \(notify)")
+            avPlayer.seek(to: CMTime.zero)
+            if self?.playerHeaderCoverImageView.image != nil {
+                self?.playerHeaderCoverImageView.isHidden = false
+            }
+            self?.playBtn.isHidden = false
+        }
+        NotificationCenter.default.addObserver(forName: .AVPlayerItemNewErrorLogEntry, object: avPlayer.currentItem, queue: .main) { notify in
+            BFLog(message: "AVPlayerItemNewErrorLogEntry = \(notify)")
+        }
+        NotificationCenter.default.addObserver(forName: .AVPlayerItemFailedToPlayToEndTime, object: avPlayer.currentItem, queue: .main) { notify in
+            BFLog(message: "AVPlayerItemFailedToPlayToEndTime = \(notify)")
+        }
+        NotificationCenter.default.addObserver(forName: .AVPlayerItemPlaybackStalled, object: avPlayer.currentItem, queue: .main) { notify in
+            BFLog(message: "AVPlayerItemPlaybackStalled = \(notify)")
+        }
+        avPlayer.addPeriodicTimeObserver(forInterval: CMTime(value: 1, timescale: 1000), queue: .main) { [weak self] _ in
+            let progress = CMTimeGetSeconds(avPlayer.currentItem?.currentTime() ?? CMTime.zero) / CMTimeGetSeconds(avPlayer.currentItem?.duration ?? CMTime.zero)
+            if progress >= 1 {
+                self?.playBtn.isHidden = false
+            }
+        }
+        return avPlayer
+    }()
+
+    /// 预览layer
+    lazy var playerLayer: AVPlayerLayer = {
+        let playerLayer = AVPlayerLayer(player: avPlayer)
+        playerLayer.frame = playerHeaderView.bounds
+        return playerLayer
+    }()
+
+    /// 播放按钮
+    lazy var playBtn: UIButton = {
+        let playBtn = UIButton(type: .custom)
+        playBtn.frame = CGRect(x: (preViewSize.width - cDefaultMargin * 5) / 2, y: (preViewSize.height - cDefaultMargin * 5) / 2, width: cDefaultMargin * 5, height: cDefaultMargin * 5)
+        playBtn.setImage(UIImage.moduleImage(named: "icon_video_play", moduleName: "BFFramework", isAssets: false), for: .normal)
+        playBtn.tag = 4
+        playBtn.isHidden = true
+        playBtn.isUserInteractionEnabled = false
+        return playBtn
+    }()
+
+    // progressTipsLab
+    lazy var progressTipsLab: UILabel = {
+        let progressTipsLab = UILabel()
+        progressTipsLab.textAlignment = .center
+        progressTipsLab.font = UIFont.systemFont(ofSize: 14, weight: .medium)
+        progressTipsLab.numberOfLines = 2
+        progressTipsLab.textColor = UIColor.white
+        let attributedText = NSMutableAttributedString(string: "0%\n视频正在处理中,请勿离开")
+        attributedText.addAttributes([.font: UIFont.systemFont(ofSize: 34)], range: NSRange(location: 0, length: 2))
+        progressTipsLab.attributedText = attributedText
+        progressTipsLab.addShadow()
+        return progressTipsLab
+    }()
+
+    // 进度条
+    lazy var progressView: UIProgressView = {
+        let progressView = UIProgressView(progressViewStyle: .default)
+        progressView.trackTintColor = UIColor(white: 0, alpha: 0.5)
+        progressView.progressTintColor = UIColor.hexColor(hexadecimal: BFConfig.shared.styleColor.rawValue)
+        progressView.transform = CGAffineTransform(scaleX: 1.0, y: playerHeaderView.frame.height)
+        return progressView
+    }()
+
+    // 提示
+    lazy var remindLab: UILabel = {
+        let remindLab = UILabel()
+        remindLab.font = UIFont.boldSystemFont(ofSize: 18)
+        remindLab.textColor = BFConfig.shared.styleTitleColor
+        remindLab.textAlignment = .center
+        remindLab.numberOfLines = 2
+        remindLab.backgroundColor = .clear
+        remindLab.text = "为你的大作起个响亮的标题\n分享秀一下🎉"
+        return remindLab
+    }()
+
+    // 输入框背景
+    lazy var inputBackView: UIView = {
+        let inputBackView = UIView()
+        inputBackView.backgroundColor = .clear
+        inputBackView.layer.cornerRadius = 7
+        inputBackView.layer.borderWidth = 2
+        inputBackView.layer.borderColor = UIColor.hexColor(hexadecimal: BFConfig.shared.styleColor.rawValue).cgColor
+        return inputBackView
+    }()
+
+    // 手势提示
+    lazy var pinView: UIImageView = {
+        let pinView = UIImageView()
+        pinView.kf.setImage(with: URL(fileURLWithPath: currentBundlePath()!.path(forResource: "editCoverPin", ofType: ".gif")!))
+        return pinView
+    }()
+
+    // 封面
+    lazy var coverImageView: UIImageView = {
+        let coverImageView = UIImageView()
+        coverImageView.isUserInteractionEnabled = true
+        coverImageView.backgroundColor = .clear
+        coverImageView.contentMode = .scaleToFill
+        return coverImageView
+    }()
+
+    // 封面标题
+    lazy var coverImageTitle: UILabel = {
+        let coverImageTitle = UILabel()
+        coverImageTitle.text = "换封面"
+        coverImageTitle.textAlignment = .center
+        coverImageTitle.backgroundColor = UIColor(red: 0.22, green: 0.26, blue: 0.35, alpha: 0.5)
+        coverImageTitle.isUserInteractionEnabled = true
+        coverImageTitle.textColor = .white
+        coverImageTitle.font = UIFont.boldSystemFont(ofSize: 12)
+        return coverImageTitle
+
+    }()
+
+    // 标题
+    lazy var titleLabel: UILabel = {
+        let titleLabel = UILabel()
+        titleLabel.numberOfLines = 2
+        titleLabel.isUserInteractionEnabled = true
+
+        titleLabel.textColor = BFConfig.shared.styleTitleColor
+        titleLabel.textAlignment = .left
+        titleLabel.font = UIFont.systemFont(ofSize: 17)
+        let ges = UITapGestureRecognizer(target: self, action: #selector(titleLabelClick))
+        titleLabel.addGestureRecognizer(ges)
+        return titleLabel
+    }()
+
+    // 编辑发布标题
+    lazy var publicTitleView: PQEditPublicTitleView = {
+        let publicTitleView = PQEditPublicTitleView()
+        publicTitleView.isHidden = true
+        publicTitleView.confirmBtnClock = { [weak self] title in
+            BFLog(message: "传出的 title  is :\(String(describing: title))")
+            if title?.count != 0, title != self?.titleLabel.text {
+                self?.changPlayerIsPause(isPause: false)
+
+                // 判断文字是否有效
+                var inputText = ""
+                inputText = title?.replacingOccurrences(of: "\n", with: "") ?? ""
+                inputText = inputText.replacingOccurrences(of: " ", with: "")
+
+                if inputText.count > 0 {
+                    self?.setTitleText(text: title ?? "", textColor: BFConfig.shared.styleTitleColor)
+                    // 更新数据
+                    self?.videoData?.title = title
+                    self?.updateCoverImagegOrTitle()
+                }
+            }
+        }
+        publicTitleView.viewIsHiddenCallBack = { [weak self] in
+
+            self?.changPlayerIsPause(isPause: false)
+        }
+
+        return publicTitleView
+    }()
+
+    // 编辑发布封面
+    lazy var publicEditCoverView: PQEditPublicCoverImageView = {
+        let publicEditCoverView = PQEditPublicCoverImageView(frame: CGRect(x: 0, y: 0, width: cScreenWidth, height: cScreenHeigth))
+        publicEditCoverView.isHidden = true
+        return publicEditCoverView
+    }()
+
+    // 分享到朋友圈
+    lazy var shareWechatBtn: UIButton = {
+        let shareWechatBtn = UIButton(type: .custom)
+        shareWechatBtn.frame = CGRect(x: 0, y: 0, width: 70, height: 70)
+        shareWechatBtn.setImage(UIImage.moduleImage(named: "reCreate_opration_wechat", moduleName: "BFFramework", isAssets: false), for: .normal)
+        shareWechatBtn.backgroundColor = BFConfig.shared.styleBackGroundColor
+        shareWechatBtn.addCorner(corner: 6)
+        shareWechatBtn.tag = 2
+        shareWechatBtn.addTarget(self, action: #selector(btnClick(sender:)), for: .touchUpInside)
+        return shareWechatBtn
+    }()
+
+    // 分享到好友
+    lazy var shareFriendBtn: UIButton = {
+        let shareFriendBtn = UIButton(type: .custom)
+        shareFriendBtn.frame = CGRect(x: 0, y: 0, width: 70, height: 70)
+        shareFriendBtn.setImage(UIImage.moduleImage(named: BFConfig.shared.shareFriendBtnImage, moduleName: "BFFramework", isAssets: false), for: .normal)
+        shareFriendBtn.addCorner(corner: 6)
+        shareFriendBtn.tag = 1
+        shareFriendBtn.addTarget(self, action: #selector(btnClick(sender:)), for: .touchUpInside)
+        return shareFriendBtn
+    }()
+
+    // 关闭
+    lazy var finishedBtn: UIButton = {
+        let finishedBtn = UIButton(type: .custom)
+        finishedBtn.setTitle("完成", for: .normal)
+        finishedBtn.setTitleColor(UIColor.hexColor(hexadecimal: BFConfig.shared.styleColor.rawValue), for: .normal)
+        finishedBtn.titleLabel?.font = UIFont.systemFont(ofSize: 16, weight: .medium)
+        finishedBtn.backgroundColor = .clear
+        finishedBtn.tag = 3
+        finishedBtn.addCorner(corner: 3)
+        finishedBtn.addTarget(self, action: #selector(btnClick(sender:)), for: .touchUpInside)
+        return finishedBtn
+
+    }()
+
+    /// 背景View
+    lazy var oprationBgView: UIView = {
+        let oprationBgView = UIView(frame: CGRect(x: 0, y: cDevice_iPhoneNavBarAndStatusBarHei, width: cScreenWidth, height: view.frame.height - cDevice_iPhoneNavBarAndStatusBarHei))
+        oprationBgView.backgroundColor = .clear
+        return oprationBgView
+    }()
+
+    // 除了播放器以外的 下半部分操作区
+    lazy var bottomOprationBgView: UIView = {
+        let bottomOprationBgView = UIView(frame: CGRect(x: 0, y: cDevice_iPhoneNavBarAndStatusBarHei + maxHeight, width: cScreenWidth, height: bottomOprationBgViewHeight))
+        bottomOprationBgView.backgroundColor = .clear
+        bottomOprationBgView.isHidden = true
+        return bottomOprationBgView
+    }()
+
+    /// 保存视频到相册提示
+    lazy var saveVideoTipsBgView: UIView = {
+        let saveVideoTipsBgView = UIView(frame: CGRect(x: 0, y: cDevice_iPhoneNavBarAndStatusBarHei, width: cScreenWidth, height: 40))
+        saveVideoTipsBgView.backgroundColor = UIColor(red: 0, green: 0, blue: 0, alpha: 0.4)
+        saveVideoTipsBgView.isHidden = true
+        saveVideoTipsBgView.alpha = 1
+        return saveVideoTipsBgView
+    }()
+
+    lazy var saveVideoTipsLabel: UILabel = {
+        let saveVideoTipsLabel = UILabel(frame: CGRect(x: 0, y: 0, width: cScreenWidth, height: 40))
+        saveVideoTipsLabel.textColor = .white
+        saveVideoTipsLabel.textAlignment = .center
+        saveVideoTipsLabel.font = UIFont.boldSystemFont(ofSize: 17)
+        saveVideoTipsLabel.text = "视频保存中..."
+        saveVideoTipsLabel.sizeToFit()
+        return saveVideoTipsLabel
+    }()
+
+    // 保存重试
+    lazy var saveRetryBtn: UIButton = {
+        let saveRetryBtn = UIButton(type: .custom)
+        saveRetryBtn.setTitle("重试", for: .normal)
+        saveRetryBtn.setTitleColor(UIColor.white, for: .normal)
+        saveRetryBtn.titleLabel?.font = UIFont.systemFont(ofSize: 15, weight: .medium)
+        saveRetryBtn.backgroundColor = UIColor.hexColor(hexadecimal: BFConfig.shared.styleColor.rawValue)
+        saveRetryBtn.tag = 97
+        saveRetryBtn.isHidden = true
+        saveRetryBtn.addCorner(corner: 5)
+        saveRetryBtn.addTarget(self, action: #selector(btnClick(sender:)), for: .touchUpInside)
+        return saveRetryBtn
+
+    }()
+
+    override func backBtnClick() {
+        if isExportSuccess {
+            navigationController?.popViewController(animated: true)
+        } else {
+            view.endEditing(true)
+            let remindData = BFBaseModel()
+            remindData.title = "编辑的内容,将不会被保存"
+            remindView = BFRemindView(frame: CGRect(x: 0, y: 0, width: cScreenWidth, height: cScreenHeigth))
+            remindView?.isBanned = true
+            remindView?.confirmBtn.setTitle("确认", for: .normal)
+            remindView?.cancelBtn.setTitleColor(UIColor.hexColor(hexadecimal: "#333333"), for: .normal)
+            remindView?.confirmBtn.setTitleColor(UIColor.hexColor(hexadecimal: "#EE0051"), for: .normal)
+            UIApplication.shared.keyWindow?.addSubview(remindView!)
+            remindView?.remindData = remindData
+            remindView?.remindBlock = { [weak self] item, _ in
+                if item.tag == 2 {
+                    // 取消导出
+                    if self?.exporter != nil {
+                        self?.exporter.cancel()
+                    }
+                    self?.navigationController?.popViewController(animated: true)
+                }
+            }
+        }
+    }
+
+    override func viewDidLoad() {
+        super.viewDidLoad()
+        // 注册上传成功的通知
+        addNotification(self, selector: #selector(uploadSuccess(notify:)), name: cUploadSuccessKey, object: nil)
+        PQNotification.addObserver(self, selector: #selector(didBecomeActiveNotification), name: UIApplication.didBecomeActiveNotification, object: nil)
+        leftButton(image: nil, tintColor: BFConfig.shared.styleTitleColor)
+        navHeadImageView?.backgroundColor = UIColor.clear
+        lineView?.removeFromSuperview()
+        view.addSubview(bgTopView)
+        playerHeaderView.frame = CGRect(origin: CGPoint(x: (cScreenWidth - preViewSize.width) / 2, y: (maxHeight - preViewSize.height) / 2), size: preViewSize)
+        let ges = UITapGestureRecognizer(target: self, action: #selector(playVideo))
+        playerHeaderView.addGestureRecognizer(ges)
+
+        if playerLayer.superlayer == nil {
+            playerHeaderView.layer.insertSublayer(playerLayer, at: 0)
+        }
+        playerHeaderView.addSubview(playBtn)
+        playerHeaderView.addSubview(progressView)
+        view.addSubview(oprationBgView)
+        oprationBgView.addSubview(progressTipsLab)
+
+        // 添加导出view
+        bgTopView.addSubview(playerHeaderView)
+
+        playerHeaderCoverImageView.frame = playerHeaderView.frame
+        playerHeaderCoverImageView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(playVideo)))
+
+        (playerHeaderCoverImageView.viewWithTag(4))?.frame =
+            CGRect(x: (preViewSize.width - cDefaultMargin * 5) / 2, y: (preViewSize.height - cDefaultMargin * 5) / 2, width: cDefaultMargin * 5, height: cDefaultMargin * 5)
+        bgTopView.addSubview(playerHeaderCoverImageView)
+
+        view.addSubview(bottomOprationBgView)
+        bottomOprationBgView.addSubview(remindLab)
+        bottomOprationBgView.addSubview(shareWechatBtn)
+        bottomOprationBgView.addSubview(shareFriendBtn)
+        bottomOprationBgView.addSubview(finishedBtn)
+        bottomOprationBgView.addSubview(inputBackView)
+        bottomOprationBgView.addSubview(pinView)
+        inputBackView.addSubview(coverImageView)
+        coverImageView.addSubview(coverImageTitle)
+        inputBackView.addSubview(titleLabel)
+
+        view.addSubview(publicTitleView)
+        view.addSubview(publicEditCoverView)
+
+        view.addSubview(saveVideoTipsBgView)
+        saveVideoTipsBgView.addSubview(saveVideoTipsLabel)
+        saveVideoTipsBgView.addSubview(saveRetryBtn)
+        saveVideoTipsLabel.snp.makeConstraints { make in
+            make.top.height.equalToSuperview()
+            make.centerX.equalToSuperview()
+        }
+        saveRetryBtn.snp.makeConstraints { make in
+            make.left.equalTo(saveVideoTipsLabel.snp.right).offset(10)
+            make.top.equalTo(6)
+            make.bottom.equalTo(-6)
+            make.width.equalTo(50)
+        }
+
+        coverImageTitle.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(settingCoverImage)))
+        coverImageView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(settingCoverImage)))
+
+        progressView.snp.makeConstraints { make in
+            make.left.right.centerY.equalTo(playerHeaderView)
+            make.height.equalTo(3)
+        }
+        progressTipsLab.snp.makeConstraints { make in
+            make.centerX.equalToSuperview()
+            make.top.equalToSuperview().offset(((preViewSize.height - 90) / 2) + ((maxHeight - preViewSize.height) / 2))
+            make.width.equalToSuperview()
+            make.height.equalTo(90)
+        }
+        finishedBtn.snp.makeConstraints { make in
+            make.centerX.equalToSuperview()
+            make.bottom.equalToSuperview().offset(-cSafeAreaHeight)
+            make.width.equalTo(100)
+            make.height.equalTo(22)
+        }
+        shareWechatBtn.snp.makeConstraints { make in
+            make.right.equalTo(view.snp.centerX).offset(-cDefaultMargin)
+            make.width.equalTo(164)
+            make.height.equalTo(52)
+            make.bottom.equalTo(finishedBtn.snp.top).offset(-32)
+        }
+        shareFriendBtn.snp.makeConstraints { make in
+            make.left.equalTo(view.snp.centerX).offset(cDefaultMargin)
+            make.width.bottom.height.equalTo(shareWechatBtn)
+        }
+
+        inputBackView.snp.makeConstraints { make in
+            make.centerX.equalToSuperview()
+            make.bottom.equalTo(shareWechatBtn.snp.top).offset(-16)
+            make.width.equalTo(343)
+            make.height.equalTo(109)
+        }
+
+        // 根据横竖屏设置不同的 UI
+        let isWidth: Bool = (Float(editProjectModel?.sData?.videoMetaData?.videoWidth ?? 0) / Float(editProjectModel?.sData?.videoMetaData?.videoHeight ?? 0)) >= 1
+        var coverImageViewHeight = 50.0 * Float(editProjectModel?.sData?.videoMetaData?.videoHeight ?? 0) / Float(editProjectModel?.sData?.videoMetaData?.videoWidth ?? 0)
+        if coverImageViewHeight > 89 {
+            coverImageViewHeight = 89
+        }
+
+        coverImageView.snp.makeConstraints { make in
+            make.left.equalToSuperview().offset(12)
+            make.width.equalTo(50)
+            make.top.equalToSuperview().offset(10)
+            make.height.equalTo(coverImageViewHeight)
+        }
+
+        coverImageTitle.snp.makeConstraints { make in
+            make.left.equalToSuperview()
+            make.width.equalTo(50)
+            make.top.equalTo(coverImageView.snp.bottom).offset(isWidth ? 0 : -23)
+            make.height.equalTo(23)
+        }
+
+        remindLab.snp.makeConstraints { make in
+            make.centerX.equalToSuperview()
+            make.bottom.equalTo(inputBackView.snp.top).offset(-16).priorityHigh()
+            make.height.equalTo(44)
+            make.top.greaterThanOrEqualTo(5)
+            make.bottom.lessThanOrEqualTo(inputBackView.snp.top).offset(-5)
+        }
+
+        titleLabel.snp.makeConstraints { make in
+            make.height.equalTo(48)
+            make.left.equalTo(coverImageView.snp.right).offset(12)
+            make.right.equalToSuperview().offset(-14)
+            make.top.equalToSuperview().offset(10)
+        }
+
+        pinView.snp.makeConstraints { make in
+            make.height.width.equalTo(72)
+            make.right.equalToSuperview()
+            make.bottom.equalTo(inputBackView.snp.bottom)
+        }
+
+        publicTitleView.snp.makeConstraints { make in
+            make.height.equalTo(cScreenHeigth)
+            make.width.equalTo(cScreenWidth)
+            make.bottom.equalToSuperview()
+        }
+
+        // 取消所有的导出
+        PQSingletoMemoryUtil.shared.allExportSession.forEach { _, exportSession in
+            exportSession.cancelExport()
+        }
+        // 开始导出
+        appendAudio()
+        /// 保存草稿
+        saveDraftbox()
+        // 曝光上报:窗口曝光
+        PQEventTrackViewModel.baseReportUpload(businessType: .bt_windowView, objectType: .ot_view_publishSyncedUp, pageSource: .sp_stuck_publishSyncedUp, extParams: nil, remindmsg: "卡点视频数据上报-(曝光上报:窗口曝光)")
+
+        // 取推荐标题
+        getTitles()
+
+        networkStausListen()
+    }
+
+    override func viewWillAppear(_ animated: Bool) {
+        super.viewWillAppear(animated)
+        PQNotification.addObserver(self, selector: #selector(enterBackground), name: UIApplication.didEnterBackgroundNotification, object: nil)
+        PQNotification.addObserver(self, selector: #selector(willEnterForeground), name: UIApplication.willEnterForegroundNotification, object: nil)
+
+        DispatchQueue.main.async {
+            UIApplication.shared.isIdleTimerDisabled = true
+        }
+        // 从相册选择一个照片后回调
+        addNotification(self, selector: #selector(imageSelectedImage(notify:)), name: cSelectedImageSuccessKey, object: nil)
+
+        #if swift(>=4.2)
+            let memoryNotification = UIApplication.didReceiveMemoryWarningNotification
+            _ = UIApplication.willTerminateNotification
+            _ = UIApplication.didEnterBackgroundNotification
+        #else
+            let memoryNotification = NSNotification.Name.UIApplicationDidReceiveMemoryWarning
+            let terminateNotification = NSNotification.Name.UIApplicationWillTerminate
+            let enterbackgroundNotification = NSNotification.Name.UIApplicationDidEnterBackground
+        #endif
+
+        NotificationCenter.default.addObserver(
+            self, selector: #selector(clearMemoryCache), name: memoryNotification, object: nil
+        )
+    }
+
+    @objc public func clearMemoryCache() {
+        BFLog(message: "收到内存警告")
+    }
+
+    override func viewWillDisappear(_ animated: Bool) {
+        super.viewWillDisappear(animated)
+        DispatchQueue.main.async {
+            UIApplication.shared.isIdleTimerDisabled = false
+        }
+    }
+
+    deinit {
+        BFLog(1, message: "发布界面析构release")
+        view.endEditing(true)
+        PQNotification.removeObserver(self)
+        // 取消导出
+        if exporter != nil {
+            exporter.cancel()
+        }
+        if watermarkMovieExporter != nil {
+            watermarkMovieExporter.cancel()
+        }
+        if endMovieExporter != nil {
+            endMovieExporter.cancel()
+        }
+
+        avPlayer.pause()
+        avPlayer.replaceCurrentItem(with: nil)
+        // 点击上报:返回按钮
+        PQEventTrackViewModel.baseReportUpload(businessType: .bt_buttonClick, objectType: .ot_click_back, pageSource: .sp_stuck_publishSyncedUp, extParams: nil, remindmsg: "卡点视频数据上报-(点击上报:返回按钮)")
+    }
+
+    // MARK: - 网络监控
+
+    func networkStausListen() {
+        manager?.startListening(onUpdatePerforming: { status in
+            if status == .reachable(.cellular) || status == .reachable(.ethernetOrWiFi) {
+                cHiddenHUB(superView: nil)
+            } else {
+                cShowHUB(superView: nil, msg: "当前网络不佳,请尝试重新连接")
+            }
+        })
+    }
+}
+
+// MARK: - 导出/上传/下载及其他方法
+
+/// 导出/上传/下载及其他方法
+extension PQStuckPointPublicController {
+    /// fp1 - 导出视频
+    /// 开始导出视频
+
+    /// 合并声音
+    /// - Parameter urls: 所有音频的URL  是全路径方便复用
+    /// - Parameter completeHander: 返回的 URL 全路径的 URL 如果要保存替换掉前缀
+    func mergeAudios(originAsset: AVURLAsset, mTotalDuration: Float, clipAudioRange: CMTimeRange = CMTimeRange.zero, mStartTime _: CMTime = .zero, completeHander: @escaping (_ fileURL: URL?) -> Void) {
+        let timeInterval: TimeInterval = Date().timeIntervalSince1970
+        let composition = AVMutableComposition()
+
+        let originaDuration = CMTimeGetSeconds(clipAudioRange.duration)
+        BFLog(message: "处理主音频 原始时长startTime = \(originaDuration) 要显示时长totalDuration = \(mTotalDuration)")
+        // originaDuration =  37.616768 mTotalDuration = 37.616776 TODO 都用 INT 微秒级
+        if Float64(String(format: "%.3f", mTotalDuration)) ?? 0.0 <= Float64(String(format: "%.3f", originaDuration)) ?? 0.0 {
+            BFLog(message: "不用拼接音频文件 \(originAsset.url) 时长is \(CMTimeGetSeconds(originAsset.duration))")
+            completeHander(originAsset.url)
+            return
+        }
+
+        // 整倍数
+        let count = Int(mTotalDuration) / Int(originaDuration)
+
+        // 有余数多 clip 一整段
+        let row = mTotalDuration - Float(count) * Float(originaDuration)
+        // 已经拼接的总时长
+        var totalDuration: CMTime = .zero
+        // 第一段的时长
+        var duration: CMTime = .zero
+        // 第一段的区间
+        var timeRange: CMTimeRange = CMTimeRange.zero
+        if count > 0 {
+            for index in 0 ..< count {
+                // 第0段从0开始到推荐的结束,播放器的开始时间不是从0开始的
+                duration = CMTime(value: CMTimeValue(CMTimeGetSeconds(clipAudioRange.end) * Double(playerTimescaleInt)), timescale: playerTimescaleInt)
+                BFLog(message: "每一个文件的 duration \(CMTimeGetSeconds(duration))")
+                var timeRange = CMTimeRangeMake(start: .zero, duration: duration)
+
+                if index != 0 {
+                    // (CMTimeGetSeconds(clipAudioRange.end) - CMTimeGetSeconds(mStartTime))为用户选择的第一段时长
+                    timeRange = clipAudioRange
+                }
+
+                BFLog(message: "合并的文件地址: \(originAsset.url)")
+                let audioAsset = originAsset
+                let tracks = audioAsset.tracks(withMediaType: .audio)
+                if tracks.count == 0 {
+                    BFLog(message: "音频数据无效不进行合并,所有任务结束要确保输入的数据都正常! \(originAsset.url)")
+                    completeHander(URL(string: ""))
+                    return
+                }
+                let assetTrack: AVAssetTrack = tracks[0]
+
+                let compositionAudioTrack: AVMutableCompositionTrack = composition.addMutableTrack(withMediaType: AVMediaType.audio, preferredTrackID: CMPersistentTrackID())!
+
+                do {
+                    //
+                    try compositionAudioTrack.insertTimeRange(timeRange, of: assetTrack, at: totalDuration)
+
+                } catch {
+                    BFLog(message: "error is \(error)")
+                    completeHander(URL(string: ""))
+                    return
+                }
+                totalDuration = CMTimeAdd(totalDuration, timeRange.duration)
+            }
+        }
+
+        if row > 0 {
+            duration = CMTime(value: CMTimeValue(Float(CMTimeGetSeconds(totalDuration)) * Float(playerTimescaleInt)), timescale: playerTimescaleInt)
+            timeRange = CMTimeRange(start: clipAudioRange.start, duration: CMTime(value: Int64(Double(row) * Double(playerTimescaleInt)), timescale: playerTimescaleInt))
+
+            BFLog(message: "合并的文件地址: \(originAsset.url)")
+            let audioAsset = originAsset
+            let tracks = audioAsset.tracks(withMediaType: .audio)
+            if tracks.count == 0 {
+                BFLog(message: "音频数据无效不进行合并,所有任务结束要确保输入的数据都正常! \(originAsset.url)")
+                completeHander(URL(string: ""))
+                return
+            }
+            let assetTrack: AVAssetTrack = tracks[0]
+
+            let compositionAudioTrack: AVMutableCompositionTrack = composition.addMutableTrack(withMediaType: AVMediaType.audio, preferredTrackID: CMPersistentTrackID())!
+
+            do {
+                //
+                try compositionAudioTrack.insertTimeRange(timeRange, of: assetTrack, at: totalDuration)
+
+            } catch {
+                BFLog(message: "合并音频 error is \(error)")
+                completeHander(URL(string: ""))
+                return
+            }
+            totalDuration = CMTimeAdd(totalDuration, timeRange.duration)
+        }
+
+        let assetExport = AVAssetExportSession(asset: composition, presetName: AVAssetExportPresetAppleM4A)
+        BFLog(message: "assetExport.supportedFileTypes is \(String(describing: assetExport?.supportedFileTypes))")
+
+        assetExport?.outputFileType = .m4a
+        // XXXX 注意文件名的后缀要和outputFileType 一致 否则会导出失败
+        var audioFilePath = exportAudiosDirectory
+
+        if !directoryIsExists(dicPath: audioFilePath) {
+            BFLog(message: "文件夹不存在")
+            createDirectory(path: audioFilePath)
+        }
+        audioFilePath.append("merge_\(timeInterval).m4a")
+
+        let fileUrl = URL(fileURLWithPath: audioFilePath)
+
+        assetExport?.outputURL = fileUrl
+        assetExport?.exportAsynchronously {
+            if assetExport!.status == .completed {
+                let audioAsset = AVURLAsset(url: fileUrl, options: avAssertOptions)
+                BFLog(1, message: "拼接声音文件 完成 \(fileUrl) 时长is \(CMTimeGetSeconds(audioAsset.duration))")
+                completeHander(fileUrl)
+
+            } else {
+                print("拼接出错 \(String(describing: assetExport?.error))")
+                completeHander(URL(string: ""))
+            }
+        }
+    }
+
+    func appendAudio() {
+        // 更新一下假进度
+        updatePublicCurrentProgress(useProgress: 0.01)
+        let inputAsset = AVURLAsset(url: URL(fileURLWithPath: documensDirectory + (audioMixModel?.localPath ?? "")), options: nil)
+        let startMergeTime = CFAbsoluteTimeGetCurrent()
+        mergeAudios(originAsset: inputAsset, mTotalDuration: finallyUserAudioTime, clipAudioRange: clipAudioRange, mStartTime: CMTime(value: CMTimeValue((mStickers?.first?.timelineIn ?? 0.0) * Float64(playerTimescaleInt)), timescale: playerTimescaleInt)) { [weak self] completURL in
+
+            if completURL != nil {
+                let asset = AVURLAsset(url: completURL!, options: nil)
+                BFLog(message: "拼接后音频时长\(asset.duration.seconds)  url is \(String(describing: completURL)) 用时\(CFAbsoluteTimeGetCurrent() - startMergeTime)")
+                // 导出不带水印的正片
+                self?.beginExport(inputAsset: asset)
+
+                if BFConfig.shared.enableWatermarkMovie {
+                    // 导出带水印的正片
+                    self?.beginExportWatermarkMovie(inputAsset: asset)
+                }
+            } else {
+                cShowHUB(superView: self?.view, msg: "合成失败请重试。")
+            }
+        }
+    }
+
+    func beginExport(inputAsset: AVURLAsset!) {
+        if !(editProjectModel?.sData?.sections != nil && (editProjectModel?.sData?.sections.count ?? 0) > 0) {
+            BFLog(message: "项目段落错误❌")
+            return
+        }
+        // 输出视频地址
+        var outPutMP4Path = exportVideosDirectory
+        if !directoryIsExists(dicPath: outPutMP4Path) {
+            BFLog(message: "文件夹不存在")
+            createDirectory(path: outPutMP4Path)
+        }
+        outPutMP4Path.append("video_\(String.qe.timestamp()).mp4")
+        let outPutMP4URL = URL(fileURLWithPath: outPutMP4Path)
+        BFLog(message: "导出视频地址 \(outPutMP4URL)")
+
+        exporter = PQCompositionExporter(asset: inputAsset, videoComposition: nil, audioMix: nil, filters: nil, stickers: mStickers, animationTool: nil, exportURL: outPutMP4URL)
+
+        var orgeBitRate = (editProjectModel?.sData?.videoMetaData?.videoWidth ?? 0) * (editProjectModel?.sData?.videoMetaData?.videoHeight ?? 0) * 3
+
+        if mStickers != nil {
+            for stick in mStickers! {
+                if stick.type == StickerType.VIDEO.rawValue {
+                    let asset = AVURLAsset(url: URL(fileURLWithPath: documensDirectory + stick.locationPath), options: avAssertOptions)
+
+                    let cbr = asset.tracks(withMediaType: .video).first?.estimatedDataRate
+                    if Int(cbr ?? 0) > orgeBitRate {
+                        orgeBitRate = Int(cbr ?? 0)
+                    }
+                }
+            }
+        }
+        BFLog(message: "导出设置的码率为:\(orgeBitRate)")
+        let tempBeginExport = Date().timeIntervalSince1970
+        exporter.showGaussianBlur = true
+        if exporter.prepare(videoSize: CGSize(width: editProjectModel?.sData?.videoMetaData?.videoWidth ?? 0, height: editProjectModel?.sData?.videoMetaData?.videoHeight ?? 0), videoAverageBitRate: orgeBitRate) {
+            BFLog(message: "开始导出 \(String(describing: playeTimeRange.start)) 结束 \(String(describing: playeTimeRange.end))")
+            exporter.start(playeTimeRange: playeTimeRange)
+            BFLog(message: "开始导出")
+        }
+        exporter.progressClosure = { [weak self] _, _, progress in
+            BFLog(message: "正片合成进度 \(progress * 100)%")
+            let useProgress = progress > 1 ? 1 : progress
+            if progress > 0, Int(useProgress * 100) > (self?.exportProgrss ?? 0) {
+                // 更新进度
+                self?.updatePublicCurrentProgress(useProgress: useProgress * 0.88)
+            }
+        }
+        exporter.completion = { [weak self] url in
+
+            // 输出视频时长
+            let outSeconds = CMTimeGetSeconds(AVAsset(url: url ?? URL(string: "https://media.w3.org/2010/05/sintel/trailer.mp4")!).duration)
+
+            BFLog(message: "无水印的视频导出完成: \(String(describing: url)) 生成视频时长为:\(outSeconds)")
+            if outSeconds == 0 {
+                cShowHUB(superView: self?.view, msg: "合成失败请重试。")
+                return
+            }
+
+            // 导出完成后取消导出
+            if self?.exporter != nil {
+                self?.exporter.cancel()
+            }
+            self?.remindView?.removeFromSuperview()
+            if !(self?.isExportSuccess ?? false) {
+                self?.isExportSuccess = true
+                self?.exportEndDate = Date().timeIntervalSince1970
+                BFLog(message: "视频导出完成-开始去发布视频 总时长为\((self?.exportEndDate ?? 0) - (self?.startExportDate ?? 0) * 1000) 总用时\((self?.exportEndDate ?? 0) - tempBeginExport)")
+
+                self?.exportLocalURL = url
+
+                // add by ak 不生成水印视频时直接自动保存系统相册,e.g. 乐活圈中会执行
+                if !BFConfig.shared.enableWatermarkMovie {
+                    self?.authorizationStatus()
+                }
+
+                /// fp2-1-1 - 请求权限
+//                self?.authorizationStatus()
+                /// fp2-2 - 保存草稿
+//                self?.saveDraftbox()
+                /// fp2 - 处理视频数据
+                self?.dealWithVideoData()
+            }
+        }
+    }
+
+    /// fp2-1-1 - 请求权限
+    func authorizationStatus() {
+        let authStatus = PHPhotoLibrary.authorizationStatus()
+        if authStatus == .notDetermined {
+            // 第一次触发授权 alert
+            PHPhotoLibrary.requestAuthorization { [weak self] (status: PHAuthorizationStatus) -> Void in
+                if status != .authorized {
+                    cShowHUB(superView: nil, msg: "您尚未打开相册权限,请到设置页打开相册权限")
+                } else {
+                    /// fp2-1-2 - 保存视频到相册
+                    self?.saveStuckPointVideo()
+                }
+            }
+        } else if authStatus == .authorized {
+            /// fp2-1-2 - 保存视频到相册
+            saveStuckPointVideo()
+        } else {
+            cShowHUB(superView: nil, msg: "您尚未打开相册权限,请到设置页打开相册权限")
+        }
+    }
+
+    /// fp2-1-2 - 保存视频到相册
+    /// - Parameter localPath: localPath description
+    /// - Returns: <#description#>
+    func saveStuckPointVideo() {
+        let tempSaveMoveiLocal: URL? = BFConfig.shared.enableWatermarkMovie ? saveMovieLocalURL : exportLocalURL
+
+        if tempSaveMoveiLocal == nil {
+            BFLog(message: "保存相册的视频导出地址无效!!!")
+            cShowHUB(superView: nil, msg: "保存相册的视频导出地址无效")
+            saveVideoTipsLabel.text = "视频保存失败"
+            saveRetryBtn.isHidden = false
+            saveVideoTipsBgView.isHidden = false
+//            DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.8) { [weak self] in
+//                self?.saveVideoTipsBgView.isHidden = true
+//            }
+            return
+        }
+        let authStatus = PHPhotoLibrary.authorizationStatus()
+        if authStatus == .authorized {
+            let photoLibrary = PHPhotoLibrary.shared()
+            photoLibrary.performChanges({ [weak self] in
+                self?.isSaveingLocalVideo = true
+                PHAssetChangeRequest.creationRequestForAssetFromVideo(atFileURL: (tempSaveMoveiLocal)!)
+            }) { [weak self] isFinished, _ in
+                self?.isSaveingLocalVideo = false
+                DispatchQueue.main.async { [weak self] in
+                    if self?.view != nil {
+                        if isFinished {
+                            self?.saveVideoTipsLabel.text = "视频已保存到相册"
+                            self?.saveRetryBtn.isHidden = true
+                            DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.8) { [weak self] in
+                                self?.saveVideoTipsBgView.isHidden = true
+                            }
+
+                        } else {
+                            self?.saveVideoTipsLabel.text = "视频保存失败"
+                            self?.saveRetryBtn.isHidden = false
+//                            DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.8) { [weak self] in
+//                                self?.saveVideoTipsBgView.isHidden = true
+//                            }
+                        }
+                    }
+                }
+            }
+        } else {
+            cShowHUB(superView: nil, msg: "您尚未打开相册权限,请到设置页打开相册权限")
+        }
+    }
+
+    /// fp2-2 - 保存草稿
+    /// - Returns: <#description#>
+    @objc func saveDraftbox() {
+        let sdata = editProjectModel?.sData?.toJSONString(prettyPrint: false)
+        if sdata != nil, (sdata?.count ?? 0) > 0 {
+            DispatchQueue.global().async { [weak self] in
+                PQBaseViewModel.saveDraftbox(draftboxId: self?.editProjectModel?.draftboxId, title: self?.editProjectModel?.sData?.videoMetaData?.title, coverUrl: self?.editProjectModel?.sData?.videoMetaData?.coverUrl, sdata: sdata!, videoFromScene: .stuckPoint, copyType: (self?.audioMixModel != nil && self?.audioMixModel?.originProjectId != nil && (self?.audioMixModel?.originProjectId?.count ?? 0) > 0) ? 3 : nil, originProjectId: self?.audioMixModel?.originProjectId) { [weak self] draftboxInfo, _ in
+                    if draftboxInfo != nil {
+                        self?.editProjectModel?.draftboxId = draftboxInfo?["draftboxId"] as? String ?? ""
+                        self?.editProjectModel?.sData?.videoMetaData?.title = draftboxInfo?["title"] as? String ?? ""
+                        self?.editProjectModel?.sData?.videoMetaData?.coverUrl = draftboxInfo?["coverUrl"] as? String ?? ""
+                        self?.editProjectModel?.dataVersionCode = draftboxInfo?["dataVersionCode"] as? Int ?? 0
+                        BFLog(message: "保存远程的草稿成功")
+                        self?.isSaveDraftSuccess = true
+                        /// fp3 - 保存项目
+                        self?.saveProject()
+                    } else {
+                        // 保存草稿失败-播放视频
+//                        self?.publicEnd(isError: true)
+                    }
+                }
+            }
+        } else {
+            cShowHUB(superView: nil, msg: "您尚未打开相册权限,请到设置页打开相册权限")
+            // 保存草稿失败-播放视频
+            publicEnd(isError: true)
+        }
+    }
+
+    /// fp3 - 保存项目
+    /// - Returns: description
+    func saveProject() {
+        if isSaveDraftSuccess {
+            let sdata = editProjectModel?.sData?.toJSONString(prettyPrint: false) ?? ""
+            let draftboxId: String? = editProjectModel?.draftboxId
+            PQBaseViewModel.saveProject(draftboxId: draftboxId, sdata: sdata, videoFromScene: .stuckPoint, rhythmMode: rhythmMode) { [weak self] projectId, msg in
+                BFLog(message: "生成的项目id1111 :\(projectId ?? ""),msg = \(msg ?? "")")
+                if projectId == nil || (projectId?.count ?? 0) <= 0 {
+                    PQBaseViewModel.saveProject(draftboxId: draftboxId, sdata: sdata, videoFromScene: .stuckPoint, rhythmMode: self?.rhythmMode ?? .createStickersModelPoint) { [weak self] projectId, msg in
+                        BFLog(message: "生成的项目id222 :\(projectId ?? ""),msg = \(msg ?? "")")
+                        if projectId == nil || (projectId?.count ?? 0) <= 0 {
+                            PQBaseViewModel.saveProject(draftboxId: draftboxId, sdata: sdata, videoFromScene: .stuckPoint, rhythmMode: self?.rhythmMode ?? .createStickersModelPoint) { [weak self] projectId, msg in
+                                BFLog(message: "生成的项目id 3333:\(projectId ?? ""),msg = \(msg ?? "")")
+                                if projectId != nil, (projectId?.count ?? 0) > 0 {
+                                    self?.editProjectModel?.projectId = projectId ?? ""
+                                }
+                                /// fp4 - 处理视频数据
+//                                self?.dealWithVideoData()
+                            }
+                        } else {
+                            self?.editProjectModel?.projectId = projectId ?? ""
+                            /// fp4 - 处理视频数据
+//                            self?.dealWithVideoData()
+                        }
+                    }
+                } else {
+                    self?.editProjectModel?.projectId = projectId ?? ""
+                    /// fp4 - 处理视频数据
+//                    self?.dealWithVideoData()
+                }
+            }
+        }
+    }
+
+    /// fp4 - 处理视频数据
+    /// - Returns: description
+    @objc func dealWithVideoData() {
+        BFLog(message: "开始去发布视频12")
+        isSaveProjectSuccess = true
+        if isExportSuccess && exportLocalURL != nil {
+            BFLog(message: "素材上传完成同时视频导出完成开始发布视频")
+            // 更新项目
+            PQBaseViewModel.updateProject(projectId: editProjectModel?.projectId ?? "", produceStatus: "5") { repseon, _ in
+                BFLog(message: "updateProject 结果 is \(String(describing: repseon))")
+            }
+            let asset = AVURLAsset(url: exportLocalURL!, options: nil)
+            let tempUploadData = PQUploadModel()
+            tempUploadData.duration = CMTimeGetSeconds(asset.duration)
+            tempUploadData.localPath = exportLocalURL?.absoluteString
+            tempUploadData.videoWidth = CGFloat(editProjectModel?.sData?.videoMetaData?.videoWidth ?? 0)
+            tempUploadData.videoHeight = CGFloat(editProjectModel?.sData?.videoMetaData?.videoHeight ?? 0)
+            tempUploadData.image = PQVideoSnapshotUtil.videoSnapshot(videoURL: exportLocalURL!, time: 0)
+            if tempUploadData.image == nil {
+                tempUploadData.image = coverImage
+            }
+            tempUploadData.videoFromScene = .stuckPoint
+            eventTrackData = getExportEventTrackData()
+            eventTrackData?.projectId = editProjectModel?.projectId ?? ""
+            uploadData = tempUploadData
+            if uploadData?.image == nil {
+                uploadData?.image = PQVideoSnapshotUtil.videoSnapshot(videoURL: exportLocalURL!, time: 0)
+            }
+            if uploadData?.image != nil {
+                playerHeaderView.image = uploadData?.image
+                coverImageView.image = uploadData?.image
+            }
+            if isExportSuccess, exportLocalURL != nil {
+                let size = try! exportLocalURL?.resourceValues(forKeys: [.fileSizeKey])
+                BFLog(message: "size = \(String(describing: size))")
+                if (size?.fileSize ?? 0) > 0, Float64(size?.fileSize ?? 0) <= maxUploadSize {
+                    /// fp5 - 上传视频
+                    reUploadVideo()
+                }
+            }
+        }
+    }
+
+    /// fp5 - 上传视频
+    /// - Returns: <#description#>
+    @objc func reUploadVideo() {
+        if uploadData?.stsToken != nil {
+            multipartUpload(response: uploadData?.stsToken)
+        } else {
+            uploadVideo()
+        }
+    }
+
+    /// fp5-1 - 开始上传视频
+    /// - Returns: description
+    func uploadVideo() {
+        let uploadRequest: OSSMultipartUploadRequest? = PQAliOssUtil.shared.allTasks[uploadData?.videoBucketKey ?? ""]
+        if uploadRequest != nil, "\(uploadRequest?.callbackParam["code"] ?? "0")" == "1" {
+            return
+        }
+        // 更新进度
+        updatePublicCurrentProgress(useProgress: 0.89)
+        DispatchQueue.global().async {
+            PQBaseViewModel.getStsToken { [weak self] response, _ in
+                if response == nil {
+                    self?.showUploadRemindView(isNetCollected: false, msg: "token获取数据失败了哦~")
+                    return
+                }
+                // 更新进度
+                self?.updatePublicCurrentProgress(useProgress: 0.90)
+                BFLog(message: "取我方服务器STS 返回数据 \(String(describing: response))")
+                self?.multipartUpload(response: response)
+            }
+        }
+    }
+
+    /// fp5-2 - 继续上传视频
+    /// - Parameter response: <#response description#>
+    func multipartUpload(response: [String: Any]?) {
+        let FileName: String = "\(response?["FileName"] ?? "")"
+        let uploadID: String = "\(response?["Upload"] ?? "")"
+        uploadData?.stsToken = response
+        uploadData?.videoBucketKey = FileName
+        uploadData?.uploadID = uploadID
+        if uploadData?.asset != nil && isValidURL(url: uploadData?.localPath) {
+            PQPHAssetVideoParaseUtil.exportPHAssetToMP4(phAsset: (uploadData?.asset)!, isCancelCurrentExport: true) { [weak self] _, _, filePath, _ in
+                if filePath != nil, (filePath?.count ?? 0) > 0 {
+                    self?.uploadData?.localPath = filePath
+                    PQAliOssUtil.multipartUpload(localPath: self?.uploadData?.localPath ?? "", response: response, videoSource: "videoCompose")
+                }
+            }
+        } else {
+            PQAliOssUtil.multipartUpload(localPath: uploadData?.localPath ?? "", response: response, videoSource: "videoCompose")
+        }
+        PQAliOssUtil.shared.aliOssHander = { [weak self] isMatarialUpload, materialType, _, code, objectkey, _, _, _, _, _, _, _, _, _ in
+            if !isMatarialUpload, materialType == .VIDEO, self?.uploadData?.videoBucketKey == objectkey {
+                if code == 6 { // 无网
+                    let uploadRequest: OSSMultipartUploadRequest? = PQAliOssUtil.shared.allTasks[self?.uploadData?.videoBucketKey ?? ""]
+                    if !(uploadRequest != nil && "\(uploadRequest?.callbackParam["code"] ?? "0")" == "1") {
+                        self?.showUploadRemindView(msg: "aliOss")
+                    }
+                } else if code == 260 {
+                    self?.showUploadRemindView(isNetCollected: false, msg: "aliOss")
+                } else if code != 1 {
+                    // 上传失败-播放视频
+                    self?.publicEnd(isError: true)
+                }
+            }
+        }
+
+        PQAliOssUtil.shared.aliOssProgressHander = { [weak self] bytesSent, totalBytesSent, totalBytesExpectedToSend, _, _ in
+            let progress: Float = 0.90 + Float(Float(totalBytesSent) / Float(totalBytesExpectedToSend)) * 0.09
+            BFLog(message: "卡点视频上传:bytesSent = \(bytesSent),totalBytesSent = \(totalBytesSent),totalBytesExpectedToSend = \(totalBytesExpectedToSend),progress = \(progress)")
+            if progress >= 0.90, progress <= 0.99 {
+                // 更新进度
+                self?.updatePublicCurrentProgress(useProgress: progress)
+            }
+        }
+    }
+
+    /// fp6 - 视频上传成功,处理要发布视频数据
+    /// - Parameter notify: <#notify description#>
+    @objc func uploadSuccess(notify: NSNotification) {
+        let objectKey: String = "\(notify.userInfo?["objectKey"] ?? "")"
+        BFLog(message: "收到上传成功请求==\(notify.userInfo ?? [:])")
+        if uploadData?.videoBucketKey == objectKey {
+            // 上传成功
+            isUploadSuccess = true
+            /// fp7 - 处理要发布视频数据
+            dealWithPublicData()
+        }
+    }
+
+    /// fp7 - 处理要发布视频数据
+    /// - Returns: <#description#>
+    func dealWithPublicData() {
+        if uploadData?.localPath != nil {
+            let size = try! URL(string: uploadData?.localPath ?? "")?.resourceValues(forKeys: [.fileSizeKey])
+            BFLog(message: "size = \(String(describing: size))")
+            if Float64(size?.fileSize ?? 0) > maxUploadSize {
+                cShowHUB(superView: nil, msg: "无法发布大于10G的视频,请重新选择/合成发布")
+                // 上传失败-播放视频
+                publicEnd(isError: true)
+                return
+            }
+        }
+        let projectId: String? = editProjectModel?.projectId
+        let uploadRequest: OSSMultipartUploadRequest? = PQAliOssUtil.shared.allTasks[uploadData?.videoBucketKey ?? ""]
+        if uploadRequest == nil {
+            reUploadVideo()
+            return
+        }
+        let tempModel = PQVideoListModel()
+        tempModel.title = selectTitle
+        tempModel.summary = ""
+        tempModel.duration = Float64(uploadData?.duration ?? 0)
+        tempModel.uplpadImage = uploadData?.image
+        tempModel.uplpadBucketKey = uploadRequest?.objectKey
+        tempModel.localPath = uploadData?.localPath
+        tempModel.reCreateVideoData = reCreateData
+        tempModel.eventTrackData = eventTrackData
+        tempModel.uplpadStatus = 1
+        tempModel.videoFromScene = .stuckPoint
+        tempModel.uid = Int(BFLoginUserInfo.shared.uid) ?? 0
+        tempModel.uplpadRequest = PQAliOssUtil.shared.allTasks[uploadData?.videoBucketKey ?? ""]
+        tempModel.stsToken = uploadData?.stsToken
+        tempModel.projectId = projectId
+
+        /// fp8 - 发布视频
+        publicVideo(videoData: tempModel)
+    }
+
+    /// fp8 - 发布视频
+    /// - Parameter videoData: <#videoData description#>
+    func publicVideo(videoData: PQVideoListModel) {
+        if videoData.uplpadBucketKey == nil {
+            BFLog(message: "发布视频:视频uplpadBucketKey为空-\(String(describing: videoData.uplpadBucketKey))")
+            // 上传失败-播放视频
+            publicEnd(isError: true)
+            return
+        }
+        BFLog(message: "开始发布")
+        if (videoData.eventTrackData?.endUploadDate ?? 0) <= 0 {
+            // 结束上传时间
+            videoData.eventTrackData?.endUploadDate = Date().timeIntervalSince1970
+        }
+        DispatchQueue.global().async {
+//            PQBaseViewModel.ossTempToken { [weak self] response, _ in
+//                let image: UIImage = videoData.uplpadImage ?? UIImage()
+//                let data = image.jpegData(compressionQuality: 1)
+//                let accessKeyId: String = "\(response?["accessKeyId"] ?? "")"
+//                let secretKeyId: String = "\(response?["accessKeySecret"] ?? "")"
+//                let securityToken: String = "\(response?["securityToken"] ?? "")"
+//                let endpoint: String = "\(response?["uploadDomain"] ?? "")"
+//                let bucketName: String = "\(response?["bucketName"] ?? "")"
+//                let objectKey: String = "\(response?["objectKey"] ?? "")"
+//                BFLog(message: "开始上传视频图片==\(videoData.title ?? ""),uplpadBucketKey = \(videoData.uplpadBucketKey ?? ""),objectKey =\(objectKey)")
+//                PQAliOssUtil.shared
+//                    .startClient(
+//                        accessKeyId: accessKeyId,
+//                        secretKeyId: secretKeyId,
+//                        securityToken: securityToken,
+//                        endpoint: endpoint
+//                    )
+//                    .uploadObjectAsync(bucketName: bucketName, objectKey: objectKey, data: data!, fileExtensions: "png", imageUploadBlock: { _, code, ossObjectKey, _ in
+//                        BFLog(message: "图片上传完成==\(videoData.title ?? ""),uplpadBucketKey = \(videoData.uplpadBucketKey ?? ""),objectKey =\(objectKey),ossObjectKey = \(ossObjectKey)")
+//                        if code == 1 && ossObjectKey == objectKey && objectKey.count > 0 {
+//                            BFLog(message: "开始发布==\(videoData.title ?? ""),uplpadBucketKey = \(videoData.uplpadBucketKey ?? ""),objectKey =\(objectKey),ossObjectKey = \(ossObjectKey)")
+            PQUploadViewModel.publishVideo(projectId: videoData.projectId, fileExtensions: videoData.localPath?.pathExtension, title: videoData.title ?? "", videoPath: videoData.uplpadBucketKey ?? "", coverImgPath: nil, descr: videoData.summary ?? "", videoFromScene: .stuckPoint, reCreateData: videoData.reCreateVideoData, eventTrackData: videoData.eventTrackData) { [weak self] newVideoData, _, _ in
+                self?.videoData = newVideoData
+                self?.videoData?.title = self?.titleLabel.text
+                if self?.videoData?.reCreateVideoData == nil {
+                    let reCreateVideo = PQReCreateModel()
+                    reCreateVideo.reProduceVideoFlag = 1
+                    self?.videoData?.reCreateVideoData = reCreateVideo
+                }
+                if self?.videoData != nil {
+                    postNotification(name: cUpdateVideoSuccessKey, userInfo: ["videoData": (self?.videoData)!])
+                }
+                postNotification(name: cPublishStuckPointSuccessKey, userInfo: ["newVideoData": self?.videoData ?? PQVideoListModel()])
+                BFLog(message: "发布成功==\(videoData.title ?? ""),uplpadBucketKey = \(videoData.uplpadBucketKey ?? "")")
+//                                cShowHUB(superView: nil, msg: "视频发布成功")
+                // 发布成功后续操作
+                self?.publicEnd()
+                PQEventTrackViewModel.publishReportUpload(projectId: videoData.projectId, businessType: .bt_publish_success, ossInfo: videoData.stsToken ?? [:], params: ["title": videoData.title ?? "", "videoPath": videoData.uplpadBucketKey ?? "", "descr": videoData.summary ?? ""])
+            }
+//                        } else {
+//                            // 图片上传失败
+//                            BFLog(message: "图片上传失败重新发布视频==\(videoData.title ?? ""),\(videoData.uplpadBucketKey ?? "")")
+//                            self?.publicVideo(videoData: videoData)
+//                        }
+//                    })
+//            }
+        }
+    }
+
+    /// 发布结束操作
+    /// - Parameter isError: <#isError description#>
+    /// - Returns: <#description#>
+    func publicEnd(isError: Bool = false) {
+        UIApplication.shared.keyWindow?.viewWithTag(100_100)?.removeFromSuperview()
+        isPublicSuccess = true
+        progressView.removeFromSuperview()
+        progressTipsLab.removeFromSuperview()
+        oprationBgView.removeFromSuperview()
+        playBtn.isHidden = true
+        avPlayer.replaceCurrentItem(with: AVPlayerItem(url: URL(fileURLWithPath: (exportLocalURL?.absoluteString ?? "").replacingOccurrences(of: "file:///", with: ""))))
+        avPlayer.play()
+        if isError {
+            cShowHUB(superView: nil, msg: "视频发布失败,请重新合成")
+        } else {
+            bottomOprationBgView.isHidden = false
+            // add by ak 发布成功后如果带片尾的视频还没有生成成功时,出提示
+            saveRetryBtn.isHidden = true
+            saveVideoTipsBgView.isHidden = false
+            if isSaveingLocalVideo {
+                saveVideoTipsLabel.text = "视频保存中..."
+            } else {
+                saveVideoTipsLabel.text = "视频已保存到相册"
+                DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.8) { [weak self] in
+                    self?.saveVideoTipsBgView.isHidden = true
+                }
+            }
+            if isSaveingLocalVideo {
+                saveVideoTipsBgView.isHidden = false
+            }
+        }
+    }
+
+    /// 生成创作工具埋点数据
+    /// - Returns: <#description#>
+    func getExportEventTrackData() -> PQVideoMakeEventTrackModel? {
+        let eventTrackData = PQVideoMakeEventTrackModel(projectModel: editProjectModel, reCreateData: reCreateData)
+        eventTrackData.entrance = .entranceStuckPointPublic
+        eventTrackData.editTimeCost = 0
+        eventTrackData.composeTimeCost = (exportEndDate - startExportDate) * 1000
+        eventTrackData.musicName = audioMixModel?.musicName ?? ""
+        eventTrackData.syncedUpMusicName = audioMixModel?.musicName ?? ""
+        eventTrackData.musicId = audioMixModel?.musicId ?? ""
+        eventTrackData.syncedUpMusicId = audioMixModel?.musicId ?? ""
+        eventTrackData.musicUrl = audioMixModel?.selectVoiceType == 1 ? (audioMixModel?.musicPath ?? "") : (audioMixModel?.accompanimentPath ?? "")
+        eventTrackData.musicType = audioMixModel != nil ? (audioMixModel?.selectVoiceType == 1 ? "original" : "accompaniment") : ""
+        eventTrackData.isMusicClip = (audioMixModel?.startTime ?? 0) > 0
+        if editProjectModel?.sData?.videoMetaData?.canvasType == videoCanvasType.origin.rawValue {
+            eventTrackData.canvasRatio = "original"
+        } else if editProjectModel?.sData?.videoMetaData?.canvasType == videoCanvasType.nineToSixteen.rawValue {
+            eventTrackData.canvasRatio = "9:16"
+        } else if editProjectModel?.sData?.videoMetaData?.canvasType == videoCanvasType.oneToOne.rawValue {
+            eventTrackData.canvasRatio = "1:1"
+        } else if editProjectModel?.sData?.videoMetaData?.canvasType == videoCanvasType.sixteenToNine.rawValue {
+            eventTrackData.canvasRatio = "16:9"
+        }
+        eventTrackData.syncedUpVideoNumber = selectedDataCount - selectedImageDataCount
+        eventTrackData.syncedUpImageNumber = selectedImageDataCount
+        eventTrackData.syncedUpOriginalMaterialDuration = selectedTotalDuration * 1000
+        eventTrackData.syncedUpRhythmNumber = audioMixModel?.speed ?? 2
+        eventTrackData.syncedUpVideoDuration = ((audioMixModel?.endTime ?? 0) - (audioMixModel?.startTime ?? 0)) * 1000
+        // add by ak
+        eventTrackData.syncedUpVideoType = rhythmMode
+        eventTrackData.syncedUpVideoSpeedMax = syncedUpVideoSpeedMax
+        eventTrackData.syncedUpVideoSpeedMin = syncedUpVideoSpeedMin
+
+        return eventTrackData
+    }
+
+    /// 播放视频
+    /// - Returns: description
+    @objc func playVideo() {
+        playBtn.isHidden = !playBtn.isHidden
+
+        changPlayerIsPause(isPause: !playBtn.isHidden)
+    }
+
+    /// 按钮点击事件
+    /// - Parameter sender: <#sender description#>
+    /// - Returns: <#description#>
+    @objc func btnClick(sender: UIButton) {
+        switch sender.tag {
+        case 1:
+            if !(isExportSuccess && isSaveProjectSuccess && isUploadSuccess && isPublicSuccess) {
+                cShowHUB(superView: nil, msg: "视频发布失败,请重新合成")
+                return
+            }
+            if !PQSingletoWXApiUtil.shared.isInstallWX() {
+                cShowHUB(superView: nil, msg: "您还未安装微信客户端!")
+                return
+            }
+            cShowHUB(superView: nil, msg: nil)
+            let shareId = getUniqueId(desc: "\(videoData?.uniqueId ?? "")shareId")
+            PQBaseViewModel.wxFriendShareInfo(videoId: (videoData?.uniqueId)!) { [weak self] imagePath, title, shareWeappRawId, msg in
+                if msg != nil {
+                    cShowHUB(superView: nil, msg: "网络不佳哦")
+                    return
+                }
+                self?.isShared = true
+                PQSingletoWXApiUtil.shared.share(type: 3, scene: Int32(WXSceneSession.rawValue), shareWeappRawId: shareWeappRawId, title: title, description: title, imageUrl: imagePath, path: self?.videoData?.videoPath, videoId: (self?.videoData?.uniqueId)!, pageSource: self?.videoData?.pageSource ?? .sp_category, shareId: shareId).wxApiUtilHander = { _, _ in
+                }
+                cHiddenHUB(superView: nil)
+            }
+            // 点击上报:分享微信
+            PQEventTrackViewModel.baseReportUpload(businessType: .bt_buttonClick, objectType: .ot_click_shareWechat, pageSource: .sp_stuck_publishSyncedUp, extParams: ["videoId": videoData?.uniqueId ?? ""], remindmsg: "卡点视频数据上报-(点击上报:分享微信)")
+        case 2:
+            if !(isExportSuccess && isSaveProjectSuccess && isUploadSuccess && isPublicSuccess) {
+                cShowHUB(superView: nil, msg: "视频发布失败,请重新合成")
+                return
+            }
+            if !PQSingletoWXApiUtil.shared.isInstallWX() {
+                cShowHUB(superView: nil, msg: "您还未安装微信客户端!")
+                return
+            }
+            let shareId = getUniqueId(desc: "\(videoData?.uniqueId ?? "")shareId")
+            PQBaseViewModel.h5ShareLinkInfo(videoId: videoData?.uniqueId ?? "", pageSource: videoData?.pageSource ?? .sp_category) { [weak self] path, _ in
+                cHiddenHUB(superView: nil)
+                if path != nil {
+                    PQBaseViewModel.wxFriendShareInfo(videoId: (self?.videoData?.uniqueId)!) { [weak self] imagePath, _, _, msg in
+                        if msg != nil {
+                            cShowHUB(superView: nil, msg: "网络不佳哦")
+                            return
+                        }
+                        self?.isShared = true
+
+                        PQSingletoWXApiUtil.shared.share(type: 1, scene: Int32(WXSceneTimeline.rawValue), title: self?.videoData?.title ?? "\(BFLoginUserInfo.shared.nickName)made a music video for you", description: "", imageUrl: imagePath, path: path, videoId: (self?.videoData?.uniqueId)!, pageSource: self?.videoData?.pageSource ?? .sp_category, shareId: shareId).wxApiUtilHander = { _, _ in
+                        }
+                    }
+                } else {
+                    cShowHUB(superView: nil, msg: "网络不佳哦")
+                }
+            }
+            // 点击上报:分享朋友圈
+            PQEventTrackViewModel.baseReportUpload(businessType: .bt_buttonClick, objectType: .ot_click_shareWechatMoment, pageSource: .sp_stuck_publishSyncedUp, extParams: ["videoId": videoData?.uniqueId ?? ""], remindmsg: "卡点视频数据上报-(点击上报:分享朋友圈)")
+        case 3:
+
+            // 点击上报:完成
+            PQEventTrackViewModel.baseReportUpload(businessType: .bt_buttonClick, objectType: .ot_click_finished, pageSource: .sp_stuck_publishSyncedUp, extParams: ["videoId": videoData?.uniqueId ?? ""], remindmsg: "卡点视频数据上报-(点击上报:完成)")
+            bf_getCurrentViewController()?.dismiss(animated: false) {
+                bf_getCurrentViewController()?.navigationController?.viewControllers = [bf_getCurrentViewController()?.navigationController?.viewControllers.first ?? BFBaseViewController()]
+                (bf_getRootViewController() as? UITabBarController)?.selectedIndex = 4
+                DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.5) {
+                    postNotification(name: cPublishSuccessKey)
+                }
+            }
+            // 发送通知
+            postNotification(name: cFinishedPublishedNotiKey)
+        case 97:
+            // 视频保存重试
+            saveRetryBtn.isHidden = true
+            saveVideoTipsLabel.text = "视频保存中..."
+            saveStuckPointVideo()
+        default:
+            break
+        }
+    }
+
+    /// 添加提示视图
+    /// - Parameters:
+    ///   - isNetCollected: <#isNetCollected description#>
+    ///   - msg: <#msg description#>
+    func showUploadRemindView(isNetCollected _: Bool = true, msg: String? = nil) {
+        view.endEditing(true)
+
+        let emptyData = BFEmptyModel()
+        emptyData.isRefreshHidden = false
+        emptyData.title = "上传失败"
+        emptyData.titleColor = UIColor.hexColor(hexadecimal: "#353535")
+        emptyData.summary = "建议切换 WIFI/移动网络后再重试"
+        emptyData.summaryColor = UIColor.hexColor(hexadecimal: "#353535")
+        emptyData.refreshBgColor = UIColor.hexColor(hexadecimal: BFConfig.shared.styleColor.rawValue)
+        emptyData.refreshTitle = NSMutableAttributedString(string: "立即重试", attributes: [.foregroundColor: UIColor.white])
+        emptyData.emptySoureImage = UIImage.moduleImage(named: "stuckPoint_video_empty", moduleName: "BFMaterialKit", isAssets: false)
+        emptyData.netDisRefreshBgColor = UIColor.hexColor(hexadecimal: "#FA6400")
+        emptyData.netDisTitle = "内容加载失败"
+        emptyData.netDisTitleColor = UIColor.hexColor(hexadecimal: "#333333")
+        emptyData.netemptyDisImage = UIImage.moduleImage(named: "empty_netDis_icon", moduleName: "BFMaterialKit", isAssets: false)
+        emptyData.netDisRefreshTitle = NSMutableAttributedString(string: "重新加载", attributes: [.font: UIFont.systemFont(ofSize: 16, weight: .medium), .foregroundColor: UIColor.white])
+
+        let emptyRemindView = BFEmptyRemindView(frame: CGRect(x: 0, y: cDevice_iPhoneNavBarAndStatusBarHei, width: view.frame.width, height: view.frame.height - cDevice_iPhoneNavBarAndStatusBarHei))
+//        emptyRemindView.isHidden = true
+        emptyRemindView.emptyData = emptyData
+        emptyRemindView.backgroundColor = BFConfig.shared.styleBackGroundColor
+        emptyRemindView.fullRefreshBloc = { [weak self, weak emptyRemindView] _, _ in
+            if emptyRemindView?.refreshBtn.currentAttributedTitle?.string == "立即重试" {
+                emptyRemindView?.isHidden = true
+                // 重试逻辑
+                if let message = msg {
+                    if message.contains("token") {
+                        self?.uploadVideo()
+                    } else if message.contains("aliOss") {
+                        self?.uploadVideo()
+                    }
+                }
+            }
+        }
+        emptyRemindView.refreshBtn.addCorner(corner: 4)
+        view.addSubview(emptyRemindView)
+
+//        BFRemindView.showUploadRemindView(title: "上传失败", summary: (msg != nil ? msg! : "视频文件已丢失"), confirmTitle:  "立即重试") { [weak self] _, _ in
+//            self?.navigationController?.popToViewController((self?.navigationController?.viewControllers[1])!, animated: true)
+//        }
+    }
+
+    @objc func enterBackground() {
+        BFLog(message: "进入到后台")
+        // 取消导出
+        if exporter != nil {
+            exporter.cancel()
+        }
+        playBtn.isHidden = false
+        avPlayer.pause()
+    }
+
+    @objc func willEnterForeground() {
+        BFLog(message: "进入到前台")
+        if !isExportSuccess {
+            appendAudio()
+        }
+
+        playBtn.isHidden = true
+        playerHeaderCoverImageView.isHidden = true
+        avPlayer.play()
+    }
+
+    @objc func didBecomeActiveNotification() {
+        if isShared {
+            DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 1) { [weak self] in
+                self?.isShared = false
+                cShowHUB(superView: nil, msg: "分享成功")
+                self?.playBtn.isHidden = true
+                self?.avPlayer.play()
+            }
+        }
+    }
+
+    /// 更新进度
+    /// - Returns: <#description#>
+    func updatePublicCurrentProgress(useProgress: Float) {
+        exportProgrss = Int(useProgress * 100)
+        progressView.setProgress(useProgress, animated: true)
+        let attributedText = NSMutableAttributedString(string: "\(exportProgrss)%\n视频正在处理中,请勿离开")
+        attributedText.addAttributes([.font: UIFont.systemFont(ofSize: 34)], range: NSRange(location: 0, length: "\(exportProgrss)%".count))
+        progressTipsLab.attributedText = attributedText
+    }
+
+    func changPlayerIsPause(isPause: Bool) {
+        if isPause {
+            playBtn.isHidden = false
+            avPlayer.pause()
+            playerHeaderCoverImageView.isHidden = false
+
+        } else {
+            playBtn.isHidden = true
+            avPlayer.play()
+            playerHeaderCoverImageView.isHidden = true
+        }
+    }
+
+    @objc func titleLabelClick() {
+        BFLog(message: "点击输入框")
+        changPlayerIsPause(isPause: true)
+
+        pinView.isHidden = true
+        publicTitleView.show()
+
+        if publicTitleView.inputTV.text.count > 0 {
+            publicTitleView.inputTV.text = titleLabel.text
+        }
+
+        PQEventTrackViewModel.baseReportUpload(businessType: .bt_buttonClick, objectType: .ot_shanyinApp_clickButton_changeTitle, pageSource: .sp_stuck_publishSyncedUp, eventData: ["videoId": videoData?.uniqueId ?? "", "rootPageSource": isReCreate ? "shanyinApp-main-syncedUpMusicRecreate" : "shanyinApp-main-syncedUpMusic"], remindmsg: "")
+    }
+
+    @objc func settingCoverImage() {
+        if exportLocalURL == nil {
+            BFLog(message: "导出的视频地址错误???。。。")
+            return
+        }
+        changPlayerIsPause(isPause: true)
+
+        let asset = AVURLAsset(url: exportLocalURL!, options: nil)
+
+        publicEditCoverView.show(videoURL: exportLocalURL!, duration: CMTimeGetSeconds(asset.duration))
+
+        // 点击了确认 btn
+        publicEditCoverView.selectImageCallBack = { [weak self] imageData in
+
+            self?.changPlayerIsPause(isPause: false)
+            if imageData != nil {
+                self?.coverImageView.image = imageData
+                self?.playerHeaderCoverImageView.image = imageData
+                self?.uploadData?.image = imageData
+                self?.updateCoverImagegOrTitle()
+            }
+        }
+        // 点击了从相册选择
+        publicEditCoverView.selectPhotoBtnCallBack = { [weak self] in
+            let imageSelected = PQImageSelectedController()
+            imageSelected.isAssetImage = true
+
+            imageSelected.videoWidth = CGFloat(self?.editProjectModel?.sData?.videoMetaData?.videoWidth ?? 0)
+            imageSelected.videoHeight = CGFloat(self?.editProjectModel?.sData?.videoMetaData?.videoHeight ?? 0)
+//           imageSelected.uploadData = uploadData
+            //        imageSelected.updataVideoData = updataVideoData
+            self?.navigationController?.pushViewController(imageSelected, animated: true)
+        }
+
+        PQEventTrackViewModel.baseReportUpload(businessType: .bt_buttonClick, objectType: .ot_shanyinApp_clickButton_changeCover, pageSource: .sp_stuck_publishSyncedUp, eventData: ["videoId": videoData?.uniqueId ?? "", "rootPageSource": isReCreate ? "shanyinApp-main-syncedUpMusicRecreate" : "shanyinApp-main-syncedUpMusic"], remindmsg: "")
+    }
+
+    // 更新标题或封面
+    func updateCoverImagegOrTitle() {
+        BFLoadingHUB.shared.showHUB(isMode: true)
+        PQBaseViewModel.ossTempToken { [weak self] response, msg in
+            let image: UIImage = (self?.uploadData?.image)!
+            let data = image.jpegData(compressionQuality: 1)
+            let accessKeyId: String = "\(response?["accessKeyId"] ?? "")"
+            let secretKeyId: String = "\(response?["accessKeySecret"] ?? "")"
+            let securityToken: String = "\(response?["securityToken"] ?? "")"
+            let endpoint: String = "\(response?["endPoint"] ?? "")"
+            let bucketName: String = "\(response?["bucketName"] ?? "")"
+            let objectKey: String = "\(response?["objectKey"] ?? "")"
+            _ = PQAliOssUtil.shared
+                .startClient(
+                    accessKeyId: accessKeyId,
+                    secretKeyId: secretKeyId,
+                    securityToken: securityToken,
+                    endpoint: endpoint
+                )
+                .uploadObjectAsync(bucketName: bucketName, objectKey: objectKey, data: data!, fileExtensions: "png", imageUploadBlock: { _, code, ossObjectKey, _ in
+                    if code == 1 && ossObjectKey == objectKey && ossObjectKey.count > 0 {
+                        // add by ak 这里会在服务器生成分享使用的图片到1-2S 时间
+                        PQUploadViewModel.updateVideo(title: self?.videoData?.title ?? "", videoId: self?.videoData?.uniqueId ?? "", coverImgPath: objectKey, descr: "") { _, newVideoData, msg in
+
+                            if newVideoData == nil {
+                                cShowHUB(superView: self?.view, msg: msg)
+                                // 可能有敏感词 要刷一组新标题并自动更新
+                                let numberRandom: UInt32 = UInt32(arc4random_uniform(UInt32(self?.publicTitleView.titles.count ?? 0)))
+
+                                self?.setTitleText(text: self?.publicTitleView.titles[Int(numberRandom)] ?? "")
+                                self?.updateCoverImagegOrTitle()
+                                sleep(UInt32(1.5))
+                                BFLoadingHUB.shared.dismissHUB()
+
+                                return
+                            } else {
+                                BFLoadingHUB.shared.dismissHUB()
+                            }
+                        }
+                    } else {
+                        BFLoadingHUB.shared.dismissHUB()
+                    }
+                })
+        }
+    }
+
+    func setTitleText(text: String, textColor: UIColor = UIColor.hexColor(hexadecimal: "#ABABAB")) {
+        selectTitle = text
+        // 更新 UI
+        titleLabel.text = text
+        titleLabel.textColor = textColor
+        publicTitleView.inputTV.placeHolder = text
+        // 更新数据
+        videoData?.title = text
+    }
+
+    // 取推荐的10个标题
+    func getTitles() {
+        PQBaseViewModel.getBaseConfig(completeHander: { [weak self] titles in
+
+            if (titles?.count ?? 0) > 0 {
+                var temp: [String] = titles!
+                if (titles?.count ?? 0) <= 13 {
+                    for _ in 0 ... (13 - (titles?.count ?? 0)) {
+                        temp.append("")
+                    }
+                }
+
+                self?.publicTitleView.titles = temp
+
+                let numberRandom: UInt32 = UInt32(arc4random_uniform(UInt32(titles!.count)))
+                BFLog(message: "接收到的 titles\(String(describing: titles))")
+                self?.setTitleText(text: titles?[Int(numberRandom)] ?? "")
+            }
+
+        })
+    }
+
+    @objc func imageSelectedImage(notify: Notification) {
+        let imageData: UIImage? = notify.userInfo?["image"] as? UIImage
+        if imageData != nil {
+            changPlayerIsPause(isPause: false)
+            BFLog(message: "从系统相册选择了一个照片")
+            publicEditCoverView.isHidden = true
+            coverImageView.image = imageData
+            playerHeaderCoverImageView.image = imageData
+            uploadData?.image = imageData
+            updateCoverImagegOrTitle()
+        }
+    }
+}
+
+// MARK: - 导出带水印+片尾的视频相关方法
+
+extension PQStuckPointPublicController {
+    // 导出有水印的正片子
+    func beginExportWatermarkMovie(inputAsset: AVURLAsset!) {
+        if !(editProjectModel?.sData?.sections != nil && (editProjectModel?.sData?.sections.count ?? 0) > 0) {
+            BFLog(message: "项目段落错误❌")
+            return
+        }
+        // 输出视频地址
+        var outPutMP4Path = exportVideosDirectory
+        if !directoryIsExists(dicPath: outPutMP4Path) {
+            BFLog(message: "文件夹不存在")
+            createDirectory(path: outPutMP4Path)
+        }
+        outPutMP4Path.append("saveMovie_\(String.qe.timestamp()).mp4")
+        let outPutMP4URL = URL(fileURLWithPath: outPutMP4Path)
+        BFLog(message: "导出视频地址 \(outPutMP4URL)")
+
+        watermarkMovieExporter = PQCompositionExporter(asset: inputAsset, videoComposition: nil, audioMix: nil, filters: nil, stickers: mStickers, animationTool: nil, exportURL: outPutMP4URL)
+        watermarkMovieExporter.isAddWatermark = true
+        var orgeBitRate = (editProjectModel?.sData?.videoMetaData?.videoWidth ?? 0) * (editProjectModel?.sData?.videoMetaData?.videoHeight ?? 0) * 3
+
+        if mStickers != nil {
+            for stick in mStickers! {
+                if stick.type == StickerType.VIDEO.rawValue {
+                    let asset = AVURLAsset(url: URL(fileURLWithPath: documensDirectory + stick.locationPath), options: avAssertOptions)
+
+                    let cbr = asset.tracks(withMediaType: .video).first?.estimatedDataRate
+                    if Int(cbr ?? 0) > orgeBitRate {
+                        orgeBitRate = Int(cbr ?? 0)
+                    }
+                }
+            }
+        }
+        BFLog(message: "导出设置的码率为:\(orgeBitRate)")
+        watermarkMovieExporter.showGaussianBlur = true
+        if watermarkMovieExporter.prepare(videoSize: CGSize(width: editProjectModel?.sData?.videoMetaData?.videoWidth ?? 0, height: editProjectModel?.sData?.videoMetaData?.videoHeight ?? 0), videoAverageBitRate: orgeBitRate) {
+            BFLog(message: "开始导出 \(String(describing: playeTimeRange.start)) 结束 \(String(describing: playeTimeRange.end))")
+            watermarkMovieExporter.start(playeTimeRange: playeTimeRange)
+            BFLog(message: "开始导出")
+        }
+        watermarkMovieExporter.progressClosure = { _, _, progress in
+            BFLog(message: "带水印的合成进度 \(progress) ")
+        }
+        watermarkMovieExporter.completion = { [weak self] url in
+            BFLog(message: "有水印的视频导出完成: \(String(describing: url)) 生成视频时长为:\(CMTimeGetSeconds(AVAsset(url: url ?? URL(string: "https://media.w3.org/2010/05/sintel/trailer.mp4")!).duration))")
+
+            // 导出完成后取消导出
+            if self?.watermarkMovieExporter != nil {
+                self?.watermarkMovieExporter.cancel()
+            }
+
+            self?.watermarkMovieLocalURL = url
+
+            // 开始导出片尾 成功后自动保存到相册
+            self?.beginExportEndMovie()
+        }
+    }
+
+    // 导出片尾视频
+    func beginExportEndMovie() {
+        if !(editProjectModel?.sData?.sections != nil && (editProjectModel?.sData?.sections.count ?? 0) > 0) {
+            BFLog(message: "项目段落错误❌")
+            return
+        }
+        // 输出视频地址
+        var outPutMP4Path = exportVideosDirectory
+        if !directoryIsExists(dicPath: outPutMP4Path) {
+            BFLog(message: "文件夹不存在")
+            createDirectory(path: outPutMP4Path)
+        }
+        outPutMP4Path.append("endMovie_\(String.qe.timestamp()).mp4")
+        let outPutMP4URL = URL(fileURLWithPath: outPutMP4Path)
+        BFLog(message: "导出视频地址 \(outPutMP4URL)")
+
+        var orgeBitRate = (editProjectModel?.sData?.videoMetaData?.videoWidth ?? 0) * (editProjectModel?.sData?.videoMetaData?.videoHeight ?? 0) * 3
+
+        // 片尾的视频素材地址
+        let moveResPath = currentBundlePath()!.path(forResource: (editProjectModel?.sData?.videoMetaData?.videoWidth ?? 0) < (editProjectModel?.sData?.videoMetaData?.videoHeight ?? 0) ? "endMovieB" : "endMovieA", ofType: "mp4")
+        if moveResPath?.count ?? 0 == 0 {
+            BFLog(message: "片尾的视频素材地址无效!!!")
+            return
+        }
+
+        let movieAsset = AVURLAsset(url: URL(fileURLWithPath: moveResPath!), options: avAssertOptions)
+        let cbr = movieAsset.tracks(withMediaType: .video).first?.estimatedDataRate
+        BFLog(message: "cbr  is\(cbr ?? 0)")
+        if Int(cbr ?? 0) > orgeBitRate {
+            orgeBitRate = Int(cbr ?? 0)
+        }
+        BFLog(message: "导出设置的码率为:\(orgeBitRate)")
+
+        // 头像保存沙盒地址
+        BFLog(message: "头像的网络地址\(BFLoginUserInfo.shared.avatarUrl)")
+        let avatarFilePath = NSHomeDirectory().appending("/Documents/").appending("user_avatar.jpg")
+
+        // warning:给默认头像吧
+        ImageDownloader.default.downloadImage(with: URL(string: BFLoginUserInfo.shared.avatarUrl)!, options: nil) { [weak self] result in
+            var image: UIImage?
+            switch result {
+            case let .success(imageResult):
+                image = UIImage.nx_circleImage(imageResult.image)
+
+            case let .failure(error):
+                image = UIImage.moduleImage(named: "user_avatar_normal", moduleName: "BFFramework", isAssets: false)
+                BFLog(message: "下载头像图片失败:\(error.localizedDescription)")
+            }
+            if image == nil {
+                BFLog(message: "image date is error!!")
+                return
+            }
+            UIImage.saveImage(currentImage: image!, outFilePath: avatarFilePath)
+
+            // 1,背景视频素材
+            let bgMovieInfo: PQEditVisionTrackMaterialsModel = PQEditVisionTrackMaterialsModel()
+            bgMovieInfo.type = StickerType.VIDEO.rawValue
+            bgMovieInfo.locationPath = moveResPath ?? ""
+            bgMovieInfo.timelineIn = 0
+            bgMovieInfo.timelineOut = CMTimeGetSeconds(movieAsset.duration)
+            bgMovieInfo.model_in = bgMovieInfo.timelineIn
+            bgMovieInfo.out = bgMovieInfo.timelineOut
+            bgMovieInfo.canvasFillType = stickerContentMode.aspectFitStr.rawValue
+            // 2,用户头像素材
+            BFLog(message: "头像的沙盒地址:\(avatarFilePath)")
+            let avatarSticker: PQEditVisionTrackMaterialsModel = PQEditVisionTrackMaterialsModel()
+            avatarSticker.locationPath = avatarFilePath.replacingOccurrences(of: documensDirectory, with: "")
+            avatarSticker.timelineIn = bgMovieInfo.timelineIn
+            avatarSticker.timelineOut = bgMovieInfo.timelineOut
+            avatarSticker.canvasFillType = stickerContentMode.aspectFitStr.rawValue
+
+            // 头像绘制大小\位置
+            var avatarSize: Float = 0.0
+            var avatarTop: Float = 0.0
+            if (self?.editProjectModel?.sData?.videoMetaData?.videoHeight ?? 0) > (self?.editProjectModel?.sData?.videoMetaData?.videoWidth ?? 0) {
+                // 竖屏
+                avatarSize = Float(self?.editProjectModel?.sData?.videoMetaData?.videoWidth ?? 0) * 360.0 / 1080.0
+                avatarTop = 430
+            } else {
+                // 横屏屏
+                avatarSize = Float(self?.editProjectModel?.sData?.videoMetaData?.videoHeight ?? 0) * 300.0 / 1080.0
+                avatarTop = Float(self?.editProjectModel?.sData?.videoMetaData?.videoHeight ?? 0) * 130.0 / 1080.0
+            }
+
+            let avatarPostion: PQEditMaterialPositionModel = PQEditMaterialPositionModel()
+            avatarPostion.width = Int(avatarSize)
+            avatarPostion.height = Int(avatarSize)
+            avatarPostion.x = ((self?.editProjectModel?.sData?.videoMetaData?.videoWidth ?? 0) - Int(avatarSize)) / 2
+            avatarPostion.y = Int(avatarTop)
+            avatarSticker.materialPosition = avatarPostion
+
+            // 3,用户名素材
+            let userNameSticker: PQEditVisionTrackMaterialsModel = PQEditVisionTrackMaterialsModel()
+            userNameSticker.timelineIn = bgMovieInfo.timelineIn
+            userNameSticker.timelineOut = bgMovieInfo.timelineOut
+            userNameSticker.type = StickerType.SUBTITLE.rawValue
+
+            // 用户名绘制用到的参数
+            var userNameTop: Float = 0.0
+            var userNameFontSize: Float = 0.0
+            if (self?.editProjectModel?.sData?.videoMetaData?.videoHeight ?? 0) > (self?.editProjectModel?.sData?.videoMetaData?.videoWidth ?? 0) {
+                // 竖屏
+                userNameTop = 870
+                userNameFontSize = Float(self?.editProjectModel?.sData?.videoMetaData?.videoWidth ?? 0) * 100.0 / 1080.0
+            } else {
+                // 横屏
+                userNameTop = Float(self?.editProjectModel?.sData?.videoMetaData?.videoHeight ?? 0) * 480 / 1080.0
+                userNameFontSize = Float(self?.editProjectModel?.sData?.videoMetaData?.videoHeight ?? 0) * 70.0 / 1080.0
+            }
+
+            let subtitleInfo: PQEditSubtitleInfoModel = PQEditSubtitleInfoModel()
+            subtitleInfo.fontSize = Int(userNameFontSize)
+            subtitleInfo.text = BFLoginUserInfo.shared.nickName
+            userNameSticker.subtitleInfo = subtitleInfo
+
+            let userNamePostion: PQEditMaterialPositionModel = PQEditMaterialPositionModel()
+            userNamePostion.width = Int(userNameFontSize) * 10
+            userNamePostion.height = Int(userNameFontSize) * 3
+            userNamePostion.x = ((self?.editProjectModel?.sData?.videoMetaData?.videoWidth ?? 0) - userNamePostion.width) / 2
+            userNamePostion.y = Int(userNameTop)
+            userNameSticker.materialPosition = userNamePostion
+
+            // 4,音频
+            let soundResPath = currentBundlePath()!.path(forResource: "endMovieSound", ofType: "mp3")
+            let soundAsset = AVURLAsset(url: URL(fileURLWithPath: soundResPath ?? ""), options: nil)
+            self?.endMovieExporter = PQCompositionExporter(asset: soundAsset, videoComposition: nil, audioMix: nil, filters: nil, stickers: [bgMovieInfo, avatarSticker, userNameSticker], animationTool: nil, exportURL: outPutMP4URL)
+            self?.endMovieExporter.isEndMovie = true
+            if (self?.endMovieExporter.prepare(videoSize: CGSize(width: self?.editProjectModel?.sData?.videoMetaData?.videoWidth ?? 0, height: self?.editProjectModel?.sData?.videoMetaData?.videoHeight ?? 0), videoAverageBitRate: orgeBitRate)) != nil {
+                self?.endMovieExporter.start(playeTimeRange: CMTimeRange(start: CMTime.zero, duration: CMTimeMakeWithSeconds(Float64(bgMovieInfo.out), preferredTimescale: BASE_FILTER_TIMESCALE)))
+                BFLog(message: "开始导出")
+            }
+            self?.endMovieExporter.progressClosure = { _, _, progress in
+                BFLog(message: "片尾合成进度 \(progress) ")
+            }
+
+            self?.endMovieExporter.completion = { [weak self] url in
+                BFLog(message: "片尾的视频导出完成: \(String(describing: url)) 生成视频时长为:\(CMTimeGetSeconds(AVAsset(url: url ?? URL(string: "https://media.w3.org/2010/05/sintel/trailer.mp4")!).duration))")
+
+                // 导出完成后取消导出
+                if self?.endMovieExporter != nil {
+                    self?.endMovieExporter.cancel()
+                }
+                self?.endMovieLocalURL = url
+                // 拼接水印正片和片尾
+                if self?.watermarkMovieLocalURL != nil, self?.endMovieLocalURL != nil {
+                    let videoMerge: NXVideoMerge = NXVideoMerge()
+                    videoMerge.mergeAndExportVideos(withFileURLs: [self!.watermarkMovieLocalURL!, self!.endMovieLocalURL!], renderSize: CGSize(width: self?.editProjectModel?.sData?.videoMetaData?.videoWidth ?? 0, height: self?.editProjectModel?.sData?.videoMetaData?.videoHeight ?? 0)) { isSuccess, outFileURL in
+                        if isSuccess {
+                            BFLog(message: "合并视频成功 outFilePath is \(outFileURL ?? "")")
+                            self?.saveMovieLocalURL = outFileURL as? URL
+                            // 保存到相册 fp2-1-1 - 请求权限
+                            self?.authorizationStatus()
+                        }
+                    }
+                }
+            }
+        }
+    }
+}

+ 48 - 0
BFStuckPointKit/Classes/Model/PQStuckPointMusicTagsModel.swift

@@ -0,0 +1,48 @@
+//
+//  PQStuckPointMusicTagsModel.swift
+//  PQSpeed
+//
+//  Created by SanW on 2021/4/28.
+//  Copyright © 2021 BytesFlow. All rights reserved.
+//
+
+import UIKit
+import BFCommonKit
+
+public class PQStuckPointMusicTagsModel: NSObject {
+    public var parentTagId: Int64? // 父级 ID(如果是第一层,值为 0) ,
+    public var rankScore: Int64 = 0 // 排序分数 ,
+    public var tagColor: String? // 标签颜色(16进制格式,例如 #FF0000) ,
+    public var tagEmoji: String? // 标签emoji表情 ,
+    public var tagId: Int64? // 标签ID ,
+    public var tagName: String? // 标签名
+    public var isSelected: Bool = false // 是否被选中
+    // 标签大小
+    public var tagSize: CGSize = CGSize(width: cDefaultMargin * 6, height: cDefaultMargin * 3)
+
+    public override init() {
+        super.init()
+    }
+
+    public init(jsonDict: [String: Any]) {
+        super.init()
+        if jsonDict.keys.contains("parentTagId") {
+            parentTagId = Int64("\(jsonDict["parentTagId"] ?? "")") ?? 0
+        }
+        if jsonDict.keys.contains("rankScore") {
+            rankScore = Int64("\(jsonDict["rankScore"] ?? "")") ?? 0
+        }
+        if jsonDict.keys.contains("tagColor"), "\(jsonDict["tagColor"] ?? "")" != "<null>" {
+            tagColor = "\(jsonDict["tagColor"] ?? "")"
+        }
+        if jsonDict.keys.contains("tagEmoji"), "\(jsonDict["tagEmoji"] ?? "")" != "<null>" {
+            tagEmoji = "\(jsonDict["tagEmoji"] ?? "")"
+        }
+        if jsonDict.keys.contains("tagId") {
+            tagId = Int64("\(jsonDict["tagId"] ?? "")") ?? 0
+        }
+        if jsonDict.keys.contains("tagName"), "\(jsonDict["tagName"] ?? "")" != "<null>" {
+            tagName = "\(jsonDict["tagName"] ?? "")"
+        }
+    }
+}

+ 28 - 0
BFStuckPointKit/Classes/Model/PQStuckPointTimesModel.swift

@@ -0,0 +1,28 @@
+//
+//  PQStuckPointTimesModel.swift
+//  PQSpeed
+//
+//  Created by SanW on 2021/5/8.
+//  Copyright © 2021 BytesFlow. All rights reserved.
+//
+
+import UIKit
+
+public class PQStuckPointTimesModel: NSObject {
+    public var rhythmType: Int = 0 // 卡点类型
+    public var pointTimes: [Int64] = Array<Int64>.init() // 卡点时间-单位:微秒
+
+    override init() {
+        super.init()
+    }
+
+    public init(jsonDict: [String: Any]) {
+        super.init()
+        if jsonDict.keys.contains("rhythmType") {
+            rhythmType = Int("\(jsonDict["rhythmType"] ?? "0")") ?? 0
+        }
+        if jsonDict.keys.contains("pointTimes") {
+            pointTimes = (jsonDict["pointTimes"] as? [Int64]) ?? []
+        }
+    }
+}

+ 329 - 0
BFStuckPointKit/Classes/Model/PQVoiceModel.swift

@@ -0,0 +1,329 @@
+//
+//  PQVoiceModel.swift
+//  PQSpeed
+//
+//  Created by ak on 2020/8/18.
+//  Copyright © 2020 BytesFlow. All rights reserved.
+//
+
+import Foundation
+import BFCommonKit
+
+public enum voiceStatue: Int {
+    case isLoading = 0 // 加载中
+    case isPlaying = 1 // 播放中
+    case isNormal = 2 // 正常状态
+    case isSelected = 3 // 选中状态,有红框 字红色 ,别的都没有
+    case isPause = 4 // 播放状态暂停中
+}
+
+open class PQVoiceModel: NSObject, NSCopying {
+    public var name: String = ""
+    // 对应接口的 KEY
+    public var voice: String = ""
+    public var cateId: Int = 0
+    // 性别
+    public var gender: Int = 0
+    public var avatarUrl: String = ""
+    public var channel: String = "aliyun" // aliyun , azure
+    // 是否为精品
+    public var qualityFlag: Int = 0
+    // 微软语音设置
+    public var azureStyleConfig: [PQAzureStyleModel] = Array()
+    // 声音文件沙盒位置 是 URI
+    public var wavFilePath: String!
+
+    // 是否收藏
+    public var isFavorite: Bool = false
+    // 是否在加载状态 是否在播放状态f
+    public var voiceStatue: voiceStatue = .isNormal
+
+    // 收藏 ID
+    public var favoriteId: Int = 0
+
+    public var speedRate: Float = 1.0
+    public var pitchRate: Float = 0
+    public var azureStyle: String = "general"
+
+    public var isSelected: Bool = false // 是否被选中
+    public  var isPlaying: Bool = false // 是否在播放
+    public var itemWidth: CGFloat = 0 // 元素宽度
+    public var materialUrl: String? // 播放地址
+    public var localPath: String? // 本地存储地址
+    public var duration: String? // 时长
+    public var currentTime: Float64? // 当前已播放时长
+    public var uniqueId: Int = 0 // 背景音乐ID
+    public var materialId: String? // 保存发音人后素材IDmaterialUrl
+    public var volume: Int = 0 // 0-100 素材音量
+    public var startTime: Float64 = 0 // 开始时间
+    public var suggestRhythmStartTime: Float64  = 0 //推荐起始点
+    public var endTime: Float64 = 0 // 结束时间
+    public var createTimestamp: Int64 = 0 // 创建时间
+    public var voiceType: String = VOICETYPT.PRODUCE.rawValue // 音乐类型
+    public  var accompanimentPath: String? // 伴奏地址
+    public var musicId: String? // 音乐ID
+    public var musicLabels: String? // 标签
+    public var musicName: String? // 歌名
+    public var musicPath: String? // 音乐地址
+    public var musicSinger: String? // 歌手
+    public var originType: Int = 1 // 音乐来源: 1上传, 2爬取
+    public var sortNum: Int = 0 // 排序值
+    public var vodAccompanimentMediaId: String? // 伴奏vod mediaId
+    public var vodMusicMediaId: String? // 音乐vod mediaId
+    // add by ak json 结构化数据传值使用
+    public var wavfileDuration: Float64 = 0
+    public var selectVoiceType: Int = 1 // 选择的声音类型,1:原声 ,2:背景声
+    // 卡点视频-分类信息
+    public var tagsInfo: PQStuckPointMusicTagsModel?
+    // 卡点视频-卡点时间数据
+    public var rhythmSdata: [PQStuckPointTimesModel] = Array<PQStuckPointTimesModel>.init()
+    // 卡点视频-默认卡点速度(1:快节奏,2:适中,3:慢节奏) ,
+    public var speed: Int = 2
+    // 卡点视频-卡点音乐入点
+    public var rhythmMusicIn: Float64 = 0
+    // 卡点视频-卡点音乐出点
+    public var rhythmMusicOut: Float64 = 0
+    // 卡点视频-源项目ID(从那个项目做同款)
+    public var originProjectId: String?
+    
+    //缓存使用的所在的分类 ID 不能用 tagsInfo 的 一首歌有可能在多个分类当中
+    public var cacheTagID:Int64?
+    public func copy(with _: NSZone? = nil) -> Any {
+        let voice = PQVoiceModel()
+        voice.name = name
+        voice.channel = channel
+        voice.azureStyle = azureStyle
+        voice.voice = self.voice
+        voice.cateId = cateId
+        voice.gender = gender
+        voice.duration = duration
+        voice.avatarUrl = avatarUrl
+        voice.wavFilePath = wavFilePath
+        voice.isFavorite = isFavorite
+        voice.isSelected = isSelected
+        voice.voiceStatue = voiceStatue
+        voice.speedRate = speedRate
+        voice.pitchRate = pitchRate
+        voice.qualityFlag = qualityFlag
+//        voice.azureStyleConfig = azureStyleConfig
+
+        // ui 使用
+        voice.isSelected = isSelected
+        voice.isPlaying = isPlaying
+        voice.itemWidth = itemWidth
+        voice.materialUrl = materialUrl
+        voice.localPath = localPath
+
+        voice.duration = duration
+        voice.currentTime = currentTime
+        voice.uniqueId = uniqueId
+        voice.volume = volume
+        voice.startTime = startTime
+        voice.endTime = endTime
+        voice.createTimestamp = createTimestamp
+        voice.voiceType = voiceType
+        voice.accompanimentPath = accompanimentPath
+        voice.musicId = musicId
+        voice.musicLabels = musicLabels
+        voice.musicName = musicName
+        voice.musicPath = musicPath
+        voice.musicSinger = musicSinger
+        voice.originType = originType
+        voice.sortNum = sortNum
+        voice.vodAccompanimentMediaId = vodAccompanimentMediaId
+        voice.vodMusicMediaId = vodMusicMediaId
+        return voice
+    }
+
+   public override init() {
+        super.init()
+    }
+
+    public init(jsonDict: [String: Any]) {
+        super.init()
+        if jsonDict.keys.contains("name"), "\(jsonDict["name"] ?? "")" != "<null>" {
+            name = "\(jsonDict["name"] ?? "")"
+        }
+        if jsonDict.keys.contains("cateName"), "\(jsonDict["cateName"] ?? "")" != "<null>" {
+            name = "\(jsonDict["cateName"] ?? "")"
+        }
+        if jsonDict.keys.contains("coverUrl") {
+            avatarUrl = "\(jsonDict["coverUrl"] ?? "")"
+        }
+        if jsonDict.keys.contains("avatarUrl") {
+            avatarUrl = "\(jsonDict["avatarUrl"] ?? "")"
+        }
+        if jsonDict.keys.contains("coverImgPath") {
+            avatarUrl = "\(jsonDict["coverImgPath"] ?? "")"
+        }
+        if jsonDict.keys.contains("bgmId") {
+            uniqueId = Int("\(jsonDict["bgmId"] ?? "0")") ?? 0
+        }
+
+        if jsonDict.keys.contains("gender") {
+            gender = Int("\(jsonDict["gender"] ?? "0")") ?? 0
+        }
+        if jsonDict.keys.contains("id") {
+            uniqueId = Int("\(jsonDict["id"] ?? "0")") ?? 0
+        }
+        if jsonDict.keys.contains("cateId") {
+            cateId = Int("\(jsonDict["cateId"] ?? "0")") ?? 0
+        }
+        if jsonDict.keys.contains("cid") {
+            cateId = Int("\(jsonDict["cid"] ?? "0")") ?? 0
+        }
+        if jsonDict.keys.contains("createTimestamp") {
+            createTimestamp = Int64("\(jsonDict["createTimestamp"] ?? "0")") ?? 0
+        }
+        if name.count > 0 {
+            itemWidth = sizeWithText(text: name, font: UIFont.systemFont(ofSize: 14, weight: .semibold), size: CGSize(width: cDefaultMargin * 10, height: cDefaultMargin * 4)).width + cDefaultMargin * 3
+        }
+        if jsonDict.keys.contains("favoriteStatus") {
+            isFavorite = "\(jsonDict["favoriteStatus"] ?? "0")" == "1"
+        }
+        if jsonDict.keys.contains("materialUrl") {
+            materialUrl = "\(jsonDict["materialUrl"] ?? "")"
+        }
+        if jsonDict.keys.contains("duration") {
+            duration = "\(jsonDict["duration"] ?? "")"
+            if (Float64("\(duration ?? "0")") ?? 0) < 1_000_000 {
+                duration = "\(Float64("\(duration ?? "0")") ?? 0)"
+            } else {
+                duration = "\((Float64("\(duration ?? "0")") ?? 0) / 1_000_000)"
+            }
+        }
+        if jsonDict.keys.contains("accompanimentPath") {
+            accompanimentPath = "\(jsonDict["accompanimentPath"] ?? "")"
+        }
+        if jsonDict.keys.contains("musicId") {
+            musicId = "\(jsonDict["musicId"] ?? "")"
+        }
+        if jsonDict.keys.contains("musicLabels") {
+            musicLabels = "\(jsonDict["musicLabels"] ?? "")"
+        }
+        if jsonDict.keys.contains("musicName") {
+            musicName = "\(jsonDict["musicName"] ?? "")"
+        }
+        if jsonDict.keys.contains("musicPath") {
+            musicPath = "\(jsonDict["musicPath"] ?? "")"
+        }
+        if jsonDict.keys.contains("coverMusicPath") {
+            musicPath = "\(jsonDict["coverMusicPath"] ?? "")"
+        }
+        if jsonDict.keys.contains("musicSinger"), "\(jsonDict["musicSinger"] ?? "")" != "<null>" {
+            musicSinger = "\(jsonDict["musicSinger"] ?? "")"
+        }
+        if jsonDict.keys.contains("author"), "\(jsonDict["author"] ?? "")" != "<null>" {
+            musicSinger = "\(jsonDict["author"] ?? "")"
+        }
+        if jsonDict.keys.contains("originType") {
+            originType = Int("\(jsonDict["originType"] ?? "0")") ?? 0
+        }
+        if jsonDict.keys.contains("sortNum") {
+            sortNum = Int("\(jsonDict["sortNum"] ?? "0")") ?? 0
+        }
+        if jsonDict.keys.contains("vodAccompanimentMediaId") {
+            vodAccompanimentMediaId = "\(jsonDict["vodAccompanimentMediaId"] ?? "")"
+        }
+        if jsonDict.keys.contains("vodMusicMediaId") {
+            vodAccompanimentMediaId = "\(jsonDict["vodMusicMediaId"] ?? "")"
+        }
+        if jsonDict.keys.contains("musicTagInfos") {
+            let musicTagInfos = jsonDict["musicTagInfos"] as? [String: Any]
+            if musicTagInfos != nil, (musicTagInfos?.keys.count ?? 0) > 0 {
+                tagsInfo = PQStuckPointMusicTagsModel(jsonDict: musicTagInfos!)
+            }
+        }
+        if jsonDict.keys.contains("rhythmSdata"), "\(jsonDict["rhythmSdata"] ?? "")".count > 0, "\(jsonDict["rhythmSdata"] ?? "")" != "<null>" {
+            let tempArr = jsonStringToArray(jsonString: "\(jsonDict["rhythmSdata"] ?? "")")
+            if tempArr != nil, (tempArr?.count ?? 0) > 0 {
+                tempArr?.forEach { sdata in
+                    let tempTimesModel: PQStuckPointTimesModel = PQStuckPointTimesModel(jsonDict: (sdata as? [String: Any]) ?? [:])
+                    rhythmSdata.append(tempTimesModel)
+                }
+            }
+        }
+        if jsonDict.keys.contains("suggestRhythmStart") {
+            startTime = (Float64("\(jsonDict["suggestRhythmStart"] ?? "0")") ?? 0) / 1_000_000
+        }
+        
+        if jsonDict.keys.contains("suggestRhythmStart") {
+            suggestRhythmStartTime = (Float64("\(jsonDict["suggestRhythmStart"] ?? "0")") ?? 0) / 1_000_000
+        }
+        
+        if jsonDict.keys.contains("suggestRhythmEnd") {
+            endTime = (Float64("\(jsonDict["suggestRhythmEnd"] ?? "0")") ?? 0) / 1_000_000
+        }
+        if jsonDict.keys.contains("speed"), "\(jsonDict["speed"] ?? "")" != "<null>" {
+            speed = Int("\(jsonDict["speed"] ?? "2")") ?? 2
+        }
+        if jsonDict.keys.contains("rhythmMusicIn") {
+            rhythmMusicIn = (Float64("\(jsonDict["rhythmMusicIn"] ?? "0")") ?? 0) / 1_000_000
+        }
+        if jsonDict.keys.contains("rhythmMusicOut") {
+            rhythmMusicOut = (Float64("\(jsonDict["rhythmMusicOut"] ?? "0")") ?? 0) / 1_000_000
+        }
+        if jsonDict.keys.contains("originProjectId"), "\(jsonDict["originProjectId"] ?? "")" != "<null>" {
+            originProjectId = "\(jsonDict["originProjectId"] ?? "")"
+        }
+        if jsonDict.keys.contains("projectId"), "\(jsonDict["projectId"] ?? "")" != "<null>" {
+            originProjectId = "\(jsonDict["projectId"] ?? "")"
+        }
+    }
+
+    /// 计算卡点时长 策略1 :
+    /// - Parameters:
+    ///   - videoCount: 视频个数
+    ///   - imageCount: 图片个数
+    /// - Returns: <#description#>
+    public func stuckPointCuttingTime(videoCount _: Int, imageCount: Int, totalDuration: Float64) -> Float64 {
+        if totalDuration <= 0 {
+            return 0
+        }
+        // 默认比例
+        let rate: Float64 = 1.5
+        // 音乐最大时长
+        let MaxM: Float64 = 40
+        // 音乐最小时长
+        let MinM: Float64 = 10
+        // 视频个数
+//        var V1: Float64 = Float64(videoCount)
+        // 图片个数
+        let V2: Float64 = Float64(imageCount)
+        // 视频总时长
+        let V1T: Float64 = totalDuration - V2
+        if endTime <= startTime {
+            endTime = startTime + MaxM
+        }
+        // 推荐音乐时长
+        var M: Float64 = endTime - startTime
+        // 音频段数
+//        let MC: Float64 = Float64(rhythmSdata.first?.pointTimes.count ?? 1)
+        // 档位时长平均值
+//        let MST: Float64 = (Float64(duration ?? "0") ?? 0) / MC
+        // 从推荐点位开始speed*V2 的点位个数的时长/V2
+        let startS: Float64 = Float64(Double(rhythmSdata.first?.pointTimes.first ?? 0) / 1_000_000.0)
+        var endS: Float64 = Float64(Double(rhythmSdata.first?.pointTimes.last ?? 0) / 1_000_000.0)
+        if V2 > 0, speed > 0, Float64(rhythmSdata.first?.pointTimes.count ?? 0) > (V2 * Float64(speed) + 2) {
+            endS = Float64(Double(rhythmSdata.first?.pointTimes[Int(V2 * Float64(speed) + 2)] ?? 0) / 1_000_000.0)
+        }
+        let MST: Float64 = V2 <= 0 ? 0 : (endS - startS) / V2
+        if (V2 * MST + V1T / 2) >= MaxM {
+            M = MaxM
+        } else if (V2 * MST + V1T / 2) >= MinM && MaxM > (V2 * MST + V1T / 2) {
+            M = V2 * MST + V1T / 2
+        } else if (V2 * MST + V1T / 2) <= MinM && (V2 * MST + V1T) * rate >= MinM {
+            M = (V2 * MST + V1T) * rate
+        } else if (V2 * MST + V1T) * rate < MinM {
+            M = MinM
+        }
+        // 限制卡点时长最大值不能超过duration
+        if (M + startTime) > endTime {
+            M = endTime - startTime
+        } else if (M + startTime) > (Float64(duration ?? "0") ?? 0) {
+            M = (Float64(duration ?? "0") ?? 0) - startTime
+        }
+        BFLog(message: "计算当前裁剪时长:\(M),开始时间:\(startTime),结束时间:\(endTime),总时长:\(Float64(duration ?? "0") ?? 0)")
+        return M
+    }
+}

+ 270 - 0
BFStuckPointKit/Classes/View/PQCustomSpeedSettingView.swift

@@ -0,0 +1,270 @@
+//
+//  PQCustomSpeedSettingView.swift
+//  BFFramework
+//
+//  Created by ak on 2021/8/3.
+//  功能:自定义速度界面
+
+import Foundation
+import BFCommonKit
+
+class PQCustomSpeedSettingView: UIView {
+    // 左上角返回
+    lazy var backBtn: BFUIButton = {
+        let backBtn = BFUIButton(type: .custom)
+        backBtn.addTarget(self, action: #selector(backClick(sender:)), for: .touchUpInside)
+        backBtn.setImage(UIImage().BF_Image(named: "customSpeedClosed"), for: .normal)
+        backBtn.adjustsImageWhenHighlighted = false
+        return backBtn
+    }()
+
+    // 标题
+    public lazy var titleLab: UILabel = {
+        let titleLab = UILabel()
+        titleLab.font = UIFont.systemFont(ofSize: 14, weight: .medium)
+        titleLab.textAlignment = .center
+        titleLab.text = "自定义快慢速"
+        titleLab.textColor = BFConfig.shared.styleTitleColor
+        return titleLab
+    }()
+
+    // 确定
+    public lazy var confirmBtn: UIButton = {
+        let confirmBtn = UIButton(type: .custom)
+        confirmBtn.setTitle("确定", for: .normal)
+        confirmBtn.setTitleColor(.white, for: .normal)
+        confirmBtn.addTarget(self, action: #selector(confirmClick(sender:)), for: .touchUpInside)
+        confirmBtn.backgroundColor = UIColor.hexColor(hexadecimal: BFConfig.shared.styleColor.rawValue)
+        confirmBtn.titleLabel?.font = UIFont.systemFont(ofSize: 17, weight: .medium)
+        confirmBtn.addCorner(corner: 11)
+        return confirmBtn
+    }()
+
+    // 取消
+    public lazy var cancleBtn: UIButton = {
+        let cancleBtn = UIButton(type: .custom)
+        cancleBtn.setTitle("取消", for: .normal)
+        cancleBtn.setTitleColor(UIColor.hexColor(hexadecimal: BFConfig.shared.styleColor.rawValue), for: .normal)
+        cancleBtn.backgroundColor = BFConfig.shared.pointEditNamalBackgroundColor
+        cancleBtn.addTarget(self, action: #selector(backClick(sender:)), for: .touchUpInside)
+        cancleBtn.titleLabel?.font = UIFont.systemFont(ofSize: 17, weight: .medium)
+        cancleBtn.addCorner(corner: 11)
+        return cancleBtn
+    }()
+
+    // 提示1
+    lazy var customSpeedFastView: UIImageView = {
+        let customSpeedFastView = UIImageView(image: UIImage().BF_Image(named: "customSpeedFast"))
+        return customSpeedFastView
+    }()
+
+    // 提示2
+    lazy var customSpeedSlowView: UIImageView = {
+        let customSpeedSlowView = UIImageView(image: UIImage().BF_Image(named: "customSpeedSlow"))
+        return customSpeedSlowView
+    }()
+
+    lazy var fastSlider: BFUISlider = {
+        let fastSlider = BFUISlider()
+        let thbImage = UIImage.moduleImage(named: BFConfig.shared.silderPinUsedImageName, moduleName: "BFFramework", isAssets: false)
+        fastSlider.setMinimumTrackImage(thbImage, for: .normal)
+        fastSlider.setMaximumTrackImage(thbImage, for: .normal)
+        fastSlider.setThumbImage(thbImage, for: .highlighted)
+        fastSlider.setThumbImage(thbImage, for: .normal)
+        fastSlider.maximumTrackTintColor = UIColor.hexColor(hexadecimal: "#E6E8E8")
+        fastSlider.minimumTrackTintColor = UIColor.hexColor(hexadecimal: BFConfig.shared.styleColor.rawValue)
+        fastSlider.tag = 10
+//        speechSlidView.addTarget(self, action: #selector(sliderTouchEnded(sender:)), for: .touchUpInside)
+        fastSlider.maximumValue = 8
+        fastSlider.minimumValue = 0.2
+        fastSlider.valueTextColor = UIColor.hexColor(hexadecimal: BFConfig.shared.styleColor.rawValue)
+        fastSlider.value = 0.2
+
+        return fastSlider
+    }()
+
+    lazy var slowSlider: BFUISlider = {
+        let slowSlider = BFUISlider()
+        let thbImage =  UIImage.moduleImage(named: BFConfig.shared.silderPinUsedImageName, moduleName: "BFFramework", isAssets: false)
+        slowSlider.setMinimumTrackImage(thbImage, for: .normal)
+        slowSlider.setMaximumTrackImage(thbImage, for: .normal)
+        slowSlider.setThumbImage(thbImage, for: .highlighted)
+        slowSlider.setThumbImage(thbImage, for: .normal)
+        slowSlider.maximumTrackTintColor = UIColor.hexColor(hexadecimal: "#E6E8E8")
+        slowSlider.minimumTrackTintColor = UIColor.hexColor(hexadecimal: BFConfig.shared.styleColor.rawValue)
+        slowSlider.tag = 10
+//        speechSlidView.addTarget(self, action: #selector(sliderTouchEnded(sender:)), for: .touchUpInside)
+        slowSlider.maximumValue = 4
+        slowSlider.minimumValue = 0.2
+        slowSlider.valueTextColor = UIColor.hexColor(hexadecimal: BFConfig.shared.styleColor.rawValue)
+        slowSlider.value = 0.2
+
+        return slowSlider
+    }()
+
+    lazy var jumpSpeedSlider: BFUISlider = {
+        let jumpSpeedSlider = BFUISlider()
+        let thbImage =  UIImage.moduleImage(named: BFConfig.shared.silderPinUsedImageName, moduleName: "BFFramework", isAssets: false)
+        jumpSpeedSlider.setMinimumTrackImage(thbImage, for: .normal)
+        jumpSpeedSlider.setMaximumTrackImage(thbImage, for: .normal)
+        jumpSpeedSlider.setThumbImage(thbImage, for: .highlighted)
+        jumpSpeedSlider.setThumbImage(thbImage, for: .normal)
+        jumpSpeedSlider.maximumTrackTintColor = UIColor.hexColor(hexadecimal: "#E6E8E8")
+        jumpSpeedSlider.minimumTrackTintColor = UIColor.hexColor(hexadecimal: BFConfig.shared.styleColor.rawValue)
+        jumpSpeedSlider.tag = 10
+//        speechSlidView.addTarget(self, action: #selector(sliderTouchEnded(sender:)), for: .touchUpInside)
+        jumpSpeedSlider.maximumValue = 10
+        jumpSpeedSlider.minimumValue = 1
+        jumpSpeedSlider.valueTextColor = UIColor.hexColor(hexadecimal: BFConfig.shared.styleColor.rawValue)
+        jumpSpeedSlider.value = 1
+
+        return jumpSpeedSlider
+    }()
+ 
+    // view 初化的类型 1, 快慢速度卡点  2,跳跃卡点 ,3,循环设置
+    var viewType: Int = 0 {
+        didSet{
+            showHiddenView()
+            if(viewType == 1){
+                titleLab.text = "自定义快慢速"
+            }else if(viewType == 2){
+                titleLab.text = "自定义跳跃快慢速"
+            }else if(viewType == 3){
+                titleLab.text = "自定义循环"
+                jumpSpeedSlider.maximumValue = 20
+                jumpSpeedSlider.valueIsInt = true
+            }
+        }
+    }
+
+    // 点击完成回调
+    public var selectSpeedCallBack: ((_ fastSpeed: Float, _ slowSpeed: Float, _ viewType: Int , _ isCancle :Bool) -> Void)?
+
+    override init(frame: CGRect) {
+        super.init(frame: frame)
+        backgroundColor = BFConfig.shared.otherTintColor
+       
+        layer.shadowColor = UIColor(red: 0, green: 0, blue: 0, alpha: 0.15).cgColor
+        layer.shadowOpacity = 1
+        layer.shadowOffset = CGSize(width: 0, height: -6)
+        layer.shadowRadius = 30
+        // 切圆角
+        layer.cornerRadius = 15
+
+        addSubview(backBtn)
+        addSubview(titleLab)
+        addSubview(confirmBtn)
+        addSubview(cancleBtn)
+        addSubview(customSpeedFastView)
+        addSubview(customSpeedSlowView)
+        addSubview(fastSlider)
+        addSubview(slowSlider)
+        addSubview(jumpSpeedSlider)
+
+        autolayout()
+        showHiddenView()
+        self.addGestureRecognizer(UIPanGestureRecognizer(target: self, action: #selector(panGes(_:))))
+    }
+
+    required init?(coder _: NSCoder) {
+        fatalError("init(coder:) has not been implemented")
+    }
+    
+    deinit {
+        BFLog(1, message: "custom speed setting view release")
+    }
+
+    @objc func panGes(_ ges:UIPanGestureRecognizer){
+        
+    }
+    
+    func showHiddenView() {
+        customSpeedFastView.isHidden = !(viewType == 1)
+        customSpeedSlowView.isHidden =  !(viewType == 1)
+        fastSlider.isHidden =  !(viewType == 1)
+        slowSlider.isHidden =  !(viewType == 1)
+        jumpSpeedSlider.isHidden = (viewType == 1)
+    }
+
+    func autolayout() {
+        backBtn.snp.makeConstraints { make in
+            make.height.width.equalTo(24)
+            make.left.equalToSuperview().offset(16)
+            make.top.equalToSuperview().offset(10)
+        }
+
+        titleLab.snp.makeConstraints { make in
+            make.height.equalTo(20)
+            make.width.equalTo(150)
+            make.centerX.equalToSuperview()
+            make.top.equalToSuperview().offset(10)
+        }
+        cancleBtn.snp.makeConstraints { make in
+            make.height.equalTo(50)
+            make.width.equalTo(61)
+            make.left.equalToSuperview().offset(19)
+            make.bottom.equalToSuperview().offset(-60)
+        }
+
+        confirmBtn.snp.makeConstraints { make in
+            make.height.equalTo(50)
+            make.width.equalTo(254)
+            make.right.equalToSuperview().offset(-30)
+            make.top.equalTo(cancleBtn.snp.top)
+        }
+
+        customSpeedFastView.snp.makeConstraints { make in
+            make.height.width.equalTo(30)
+            make.left.equalToSuperview().offset(18)
+            make.top.equalToSuperview().offset(98)
+        }
+
+        customSpeedSlowView.snp.makeConstraints { make in
+            make.height.width.equalTo(30)
+            make.left.equalTo(customSpeedFastView.snp.left)
+            make.top.equalTo(customSpeedFastView.snp.bottom).offset(31)
+        }
+
+        fastSlider.snp.makeConstraints { make in
+            make.left.equalToSuperview().offset(65)
+            make.right.equalToSuperview().offset(-36)
+            make.centerY.equalTo(customSpeedFastView.snp.centerY)
+        }
+        slowSlider.snp.makeConstraints { make in
+            make.left.equalToSuperview().offset(65)
+            make.right.equalToSuperview().offset(-36)
+            make.centerY.equalTo(customSpeedSlowView.snp.centerY)
+        }
+
+        jumpSpeedSlider.snp.makeConstraints { make in
+            make.left.equalToSuperview().offset(30)
+            make.right.equalToSuperview().offset(-36)
+            make.top.equalTo(titleLab.snp.bottom).offset(104)
+        }
+    }
+
+    // 返回
+    @objc func backClick(sender _: UIButton) {
+        isHidden = true
+        selectSpeedCallBack!(jumpSpeedSlider.value, 0, viewType,true)
+
+    }
+
+    // 确认
+    @objc func confirmClick(sender _: UIButton) {
+        isHidden = true
+        BFLog(message: "fastSlider: \(fastSlider.value.decimalNumber()) slowSlider: \(slowSlider.value.decimalNumber()) jumpSpeedSlider:\(jumpSpeedSlider.value.decimalNumber(0))")
+        if selectSpeedCallBack != nil {
+            if(viewType == 1){
+                selectSpeedCallBack!(fastSlider.value.decimalNumber(), slowSlider.value.decimalNumber(), viewType,false)
+            }else if(viewType == 2){
+                //跳跃模式时 支持小数值
+                selectSpeedCallBack!(jumpSpeedSlider.value, 0, viewType,false)
+            }else{
+                //循环比时都是整数
+                selectSpeedCallBack!(jumpSpeedSlider.value.decimalNumber(0), 0, viewType,false)
+            }
+        
+        }
+    }
+}

+ 103 - 0
BFStuckPointKit/Classes/View/PQCustomSwitchView.swift

@@ -0,0 +1,103 @@
+//
+//  PQCustomSwitchView.swift
+//  PQSpeed
+//
+//  Created by SanW on 2021/5/12.
+//  Copyright © 2021 BytesFlow. All rights reserved.
+//
+
+import UIKit
+import BFCommonKit
+
+class PQCustomSwitchView: UIView {
+    /// 当前选中的item
+    var currentItemBtn: UIButton?
+    /// 按钮点击的回调
+    var switchChangeHandle: ((_ sender: UIButton) -> Void)?
+ 
+    var saveBtns:Array<UIButton> = Array.init()
+    
+    override private init(frame: CGRect) {
+        super.init(frame: frame)
+    }
+
+    required init?(coder _: NSCoder) {
+        fatalError("init(coder:) has not been implemented")
+    }
+
+    /*
+     服务器返回的 (1:快节奏,2:适中,3:慢节奏) ,
+     一,快慢速模式下取卡点 2 3 4
+     二,跳跃卡点模式下根据不同速度 取卡点 1,2,3
+     快节奏为选中区域的所有点位,即0,1,2,3,4 5 6 7 8 9 10 ……
+     适中为每两个点位取一个,即0,2,4,6……
+     慢节奏为每三个点位取一个,即0,3,6,9……
+     */
+    init(frame: CGRect, titles: [String], defaultIndex: Int = 1) {
+        super.init(frame: frame)
+        BFLog(message: "初始时选择的位置 is \(defaultIndex)")
+        let itemWidth: CGFloat = frame.width / CGFloat(titles.count)
+        for (index, itemTitle) in titles.enumerated() {
+            let itemBtn = UIButton(type: .custom)
+            //8 是每一个btn 的间隔
+            itemBtn.frame = CGRect(x: CGFloat(index) * itemWidth + CGFloat(index) * 8, y: 0, width: itemWidth, height: frame.height)
+            itemBtn.tag = (3 - index)
+            itemBtn.setTitle(itemTitle, for: .normal)
+            itemBtn.setTitleColor(UIColor.hexColor(hexadecimal: "#959595"), for: .normal)
+            itemBtn.setTitleColor(UIColor.hexColor(hexadecimal: BFConfig.shared.styleColor.rawValue), for: .selected)
+            itemBtn.addTarget(self, action: #selector(btnClick(sender:)), for: .touchUpInside)
+  
+            if itemBtn.tag == defaultIndex  {
+                itemBtn.isSelected = true
+              
+                currentItemBtn = itemBtn
+                let styleColor = UIColor.hexColor(hexadecimal: BFConfig.shared.styleColor.rawValue)
+                itemBtn.backgroundColor = UIColor(red: styleColor.rgbaf[0], green: styleColor.rgbaf[1], blue: styleColor.rgbaf[2], alpha: 0.15)
+                
+                itemBtn.titleLabel?.font = UIFont.boldSystemFont(ofSize: 13)
+            }else{
+                itemBtn.backgroundColor = BFConfig.shared.pointEditNamalBackgroundColor
+                itemBtn.titleLabel?.font = UIFont.systemFont(ofSize: 13)
+            }
+           
+            itemBtn.addCorner(corner:5)
+            addSubview(itemBtn)
+            
+            saveBtns.append(itemBtn)
+        }
+    }
+    
+    func selectOneBtn(Index:Int) {
+         
+        let selectIndex = saveBtns.firstIndex(where: { (svaeBtn) -> Bool in
+
+            (svaeBtn.tag == Index)
+        })
+        BFLog(message: "选择节奏 \(selectIndex ?? -1)")
+        updateSelectBtn(sender: saveBtns[selectIndex ?? 0])
+    }
+    
+    func updateSelectBtn(sender: UIButton) {
+        currentItemBtn?.isSelected = false
+        currentItemBtn?.titleLabel?.font = UIFont.systemFont(ofSize: 13)
+        currentItemBtn?.backgroundColor = BFConfig.shared.pointEditNamalBackgroundColor
+        
+        sender.titleLabel?.font = UIFont.boldSystemFont(ofSize: 13)
+        sender.isSelected = true
+        let styleColor = UIColor.hexColor(hexadecimal: BFConfig.shared.styleColor.rawValue)
+        sender.backgroundColor = UIColor(red: styleColor.rgbaf[0], green: styleColor.rgbaf[1], blue: styleColor.rgbaf[2], alpha: 0.15)
+        currentItemBtn = sender
+    }
+ 
+    /// 按钮点击事件
+    /// - Parameter sender: <#sender description#>
+    /// - Returns: <#description#>
+    @objc func btnClick(sender: UIButton) {
+
+        updateSelectBtn(sender: sender)
+        if switchChangeHandle != nil {
+            switchChangeHandle!(sender)
+        }
+ 
+    }
+}

+ 42 - 0
BFStuckPointKit/Classes/View/PQCuttingPointView.swift

@@ -0,0 +1,42 @@
+//
+//  PQCuttingPointView.swift
+//  PQSpeed
+//
+//  Created by SanW on 2021/5/12.
+//  Copyright © 2021 BytesFlow. All rights reserved.
+//
+
+import UIKit
+import BFCommonKit
+
+class PQCuttingPointView: UIView {
+    lazy var pointView: UIView = {
+        let pointView = UIView()
+        pointView.addCorner(corner: 1.5)
+        pointView.backgroundColor = UIColor.hexColor(hexadecimal: BFConfig.shared.styleColor.rawValue)
+        return pointView
+    }()
+
+    lazy var dragingImageView: UIImageView = {
+        let dragingImageView = UIImageView(image:UIImage.moduleImage(named: "stuckPoint_dragingImage", moduleName: "BFFramework",isAssets: false)?.withRenderingMode(.alwaysTemplate))
+        dragingImageView.tintColor = UIColor.hexColor(hexadecimal: BFConfig.shared.styleColor.rawValue)
+        return dragingImageView
+    }()
+
+    override init(frame: CGRect) {
+        super.init(frame: frame)
+        isUserInteractionEnabled = true
+        addSubview(pointView)
+        addSubview(dragingImageView)
+        dragingImageView.frame = CGRect(x: (frame.width - 12) / 2, y: frame.height - 14, width: 12, height: 14)
+        pointView.frame = CGRect(x: (frame.width - 3) / 2, y: 0, width: 3, height: frame.height - dragingImageView.frame.height + 2)
+    }
+
+    required init?(coder _: NSCoder) {
+        fatalError("init(coder:) has not been implemented")
+    }
+
+    deinit {
+        BFLog(message: "卡点裁剪-指示器销毁")
+    }
+}

+ 203 - 0
BFStuckPointKit/Classes/View/PQEditPublicCoverImageView.swift

@@ -0,0 +1,203 @@
+//
+//  PQEditPublicCoverImageView.swift
+//  BFFramework
+//
+//  Created by ak on 2021/7/22.
+//  功能:选择封面
+
+import Foundation
+import BFCommonKit
+
+class PQEditPublicCoverImageView: UIView {
+    
+    //确认选择回调
+    public var selectImageCallBack: ((_ image:  UIImage?) -> Void)?
+    
+    //从相册选择 BTN 点击回调 用于弹出照片选择界面
+    public var selectPhotoBtnCallBack:(() -> Void)?
+    //选择的封面图片
+    var selectImage:UIImage?
+    //最后选择的封面 btn 用于还原角标
+    var lastSelectcoverImageBtn:UIButton?
+    
+    lazy var backView: UIView = {
+        let backView = UIView()
+        backView.addCorner(corner: 1.5)
+        backView.backgroundColor = BFConfig.shared.otherTintColor
+        return backView
+    }()
+
+    lazy var closeView: UIView = {
+        let closeView = UIView()
+        closeView.backgroundColor = UIColor(red: 0, green: 0, blue: 0, alpha: 0.8)
+        return closeView
+    }()
+
+    // 确定按钮
+    lazy var compliteBtn: UIButton = {
+        let compliteBtn = UIButton(type: .custom)
+        compliteBtn.backgroundColor =  UIColor.hexColor(hexadecimal: BFConfig.shared.styleColor.rawValue)
+        compliteBtn.setTitle("确定", for: .normal)
+        compliteBtn.titleLabel?.font = UIFont.systemFont(ofSize: 18, weight: .medium)
+        compliteBtn.setTitleColor(UIColor.hexColor(hexadecimal: "#FFFFFF"), for: .normal)
+        compliteBtn.adjustsImageWhenHighlighted = false
+        compliteBtn.tag = 2
+        compliteBtn.addCorner(corner: 11)
+        compliteBtn.addTarget(self, action: #selector(btnClick(sender:)), for: .touchUpInside)
+        return compliteBtn
+    }()
+
+    // 从相册选择
+    lazy var selectPhotoBtn: UIButton = {
+        let selectPhotoBtn = UIButton(type: .custom)
+       
+        selectPhotoBtn.setImage(UIImage.moduleImage(named:  BFConfig.shared.editCoverimageSelectImage, moduleName: "BFFramework",isAssets: false), for: .normal)
+        selectPhotoBtn.adjustsImageWhenHighlighted = false
+        selectPhotoBtn.addCorner(corner: 11)
+        selectPhotoBtn.tag = 1
+        selectPhotoBtn.addTarget(self, action: #selector(btnClick(sender:)), for: .touchUpInside)
+        return selectPhotoBtn
+    }()
+
+    override init(frame: CGRect) {
+        super.init(frame: frame)
+ 
+        closeView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(viewHidden)))
+        backgroundColor = .clear
+
+        addSubview(closeView)
+        addSubview(backView)
+        addSubview(compliteBtn)
+        addSubview(selectPhotoBtn)
+ 
+        // 9个封面
+        for i in 0 ... 8 {
+            let coverImageBtn = UIButton(type: .custom)
+            var frame: CGRect = .zero
+            frame.size.width = 108.0 * cAdaptatWidth
+            frame.size.height = 108.0 * cAdaptatWidth
+            
+            // 按钮横向间隔10,左边距16
+            let spedScorp = 10.0 * cAdaptatWidth
+            let spedHeght = 16.0 * cAdaptatWidth
+            frame.origin.x = CGFloat((i % 3) * (Int(frame.size.width) + Int(spedScorp)) + Int(spedHeght))
+            // 按钮竖向间隔10,左边距16
+            frame.origin.y = floor(CGFloat(i / 3)) * (frame.size.height + spedScorp) + spedHeght
+            coverImageBtn.frame = frame
+            coverImageBtn.backgroundColor = UIColor.hexColor(hexadecimal: "#F6F6F6")
+            coverImageBtn.tag = (i + 1) * 100
+            coverImageBtn.adjustsImageWhenHighlighted = false
+            coverImageBtn.imageView?.contentMode = .scaleAspectFit
+            coverImageBtn.addTarget(self, action: #selector(coverImageBtnClick(sender:)), for: .touchUpInside)
+            //选中后的角标
+            let iconView = UIImageView.init(frame: CGRect(x: frame.size.width  - 22.0 - 6, y: 6, width: 22, height: 22))
+            iconView.image = UIImage.moduleImage(named:  BFConfig.shared.editCoverimageSelectedImage, moduleName: "BFFramework",isAssets: false)
+           
+          
+            iconView.tag = 1000
+            if(i == 0){
+                iconView.isHidden = false
+                lastSelectcoverImageBtn = coverImageBtn
+            }else{
+                iconView.isHidden = true
+            }
+    
+            coverImageBtn.addSubview(iconView)
+            backView.addSubview(coverImageBtn)
+
+        }
+
+        backView.snp.makeConstraints { make in
+            make.right.equalToSuperview()
+            make.width.equalTo(cScreenWidth)
+            make.height.equalTo(459 * cAdaptatWidth + cAKSafeAreaHeight)
+            make.bottom.equalToSuperview()
+        }
+
+        closeView.snp.makeConstraints { make in
+            make.right.equalToSuperview()
+            make.width.equalTo(cScreenWidth)
+            make.height.equalTo(cScreenHeigth - 459)
+            make.top.equalToSuperview()
+        }
+
+        selectPhotoBtn.snp.makeConstraints { make in
+            make.left.equalToSuperview().offset(16 * cAdaptatWidth)
+            make.width.equalTo(164 * cAdaptatWidth )
+            make.height.equalTo(54 * cAdaptatWidth)
+            make.bottom.equalToSuperview().offset(-19 - cAKSafeAreaHeight)
+        }
+
+        compliteBtn.snp.makeConstraints { make in
+            make.left.equalTo(selectPhotoBtn.snp.right).offset(14.0 * cAdaptatWidth)
+            make.width.equalTo(164 * cAdaptatWidth)
+            make.height.equalTo(54 * cAdaptatWidth)
+            make.top.equalTo(selectPhotoBtn.snp.top)
+        }
+    }
+
+    required init?(coder _: NSCoder) {
+        fatalError("init(coder:) has not been implemented")
+    }
+
+    @objc func viewHidden() {
+        isHidden = true
+        if(selectImageCallBack != nil){
+            selectImageCallBack!(nil)
+        }
+    }
+    
+    func show(videoURL: URL ,duration:Float64) {
+       isHidden = false
+        
+        PQVideoSnapshotUtil.videoSnapshot(videoURL: videoURL, duration: TimeInterval(duration), count: 9) { [weak self] images in
+            DispatchQueue.main.async {
+                for i in 0...(images?.count ?? 0) - 1{
+                    
+                    let btn = self?.backView.viewWithTag((i + 1) * 100) as? UIButton
+                    btn?.setImage(images?[i], for: .normal)
+               
+                }
+            }
+
+        }
+    }
+    
+    
+
+    /// 按钮点击事件
+    /// - Parameter sender: <#sender description#>
+    /// - Returns: <#description#>
+    @objc func btnClick(sender: UIButton?) {
+        switch sender?.tag {
+        case 1:
+            BFLog(message: "选择图库")
+            if(selectPhotoBtnCallBack != nil){
+                selectPhotoBtnCallBack!()
+            }
+            
+            break
+        case 2:
+            BFLog(message: "确认选择")
+            if(selectImageCallBack != nil){
+                selectImageCallBack!(selectImage)
+            }
+            isHidden = true
+            break
+        default:
+            break
+        }
+    }
+
+    // 封面选择
+    @objc func coverImageBtnClick(sender: UIButton?) {
+        //角标的显示
+        lastSelectcoverImageBtn?.viewWithTag(1000)?.isHidden = true
+        sender?.viewWithTag(1000)?.isHidden = false
+        lastSelectcoverImageBtn = sender
+        
+        BFLog(message: "封面选择了\(String(describing: sender?.tag))")
+        selectImage = sender?.currentImage
+ 
+    }
+}

+ 306 - 0
BFStuckPointKit/Classes/View/PQEditPublicTitleView.swift

@@ -0,0 +1,306 @@
+//
+//  PQEditPublicTitleView.swift
+//  BFFramework
+//
+//  Created by ak on 2021/7/22.
+//  功能:编辑标题
+
+import Foundation
+import BFUIKit
+
+class PQEditPublicTitleView: UIView {
+    
+    //点击确认回调
+    public var confirmBtnClock: ((_ selectTitle: String?) -> Void)?
+    
+    //VIEW 隐藏回调事件
+    public var viewIsHiddenCallBack: (() -> Void)?
+
+    lazy var backView: UIView = {
+        let backView = UIView()
+        backView.addCorner(corner: 1.5)
+        backView.backgroundColor = .white
+        return backView
+    }()
+
+    lazy var closeView: UIView = {
+        let closeView = UIView()
+        closeView.backgroundColor = UIColor(red: 0, green: 0, blue: 0, alpha: 0.8)
+        return closeView
+    }()
+
+    // 输入框
+    lazy var inputTV: BFTextView = {
+        let inputTV = BFTextView()
+        inputTV.font = UIFont.systemFont(ofSize: 17, weight: .regular)
+        inputTV.backgroundColor = .clear
+        inputTV.textColor = .black
+        inputTV.maxTextLength = 30
+        inputTV.placeHolderDefultPoint = CGPoint(x: 5, y: 0)
+        inputTV.tintColor = UIColor.hexColor(hexadecimal: BFConfig.shared.styleColor.rawValue)
+        inputTV.placeHolder = "我见过你眼中的春与秋,胜过我见过的所有山川河流"
+        inputTV.showsVerticalScrollIndicator = false
+        inputTV.textContainerInset = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0)
+        return inputTV
+    }()
+
+    // 输入框背景
+    lazy var inputBgView: UIView = {
+        let inputBgView = UIView()
+        inputBgView.backgroundColor = .clear
+        inputBgView.addCorner(corner: 10)
+        inputBgView.layer.cornerRadius = 7
+        inputBgView.layer.borderWidth = 2
+        inputBgView.layer.borderColor = UIColor.hexColor(hexadecimal: BFConfig.shared.styleColor.rawValue).cgColor
+        return inputBgView
+    }()
+
+    // 确定按钮
+    lazy var confirmBtn: UIButton = {
+        let confirmBtn = UIButton()
+        confirmBtn.backgroundColor = UIColor.hexColor(hexadecimal: BFConfig.shared.styleColor.rawValue)
+        confirmBtn.setTitle("确定", for: .normal)
+        confirmBtn.setTitleColor(.white, for: .normal)
+        confirmBtn.setTitleColor(UIColor.white, for: .selected)
+        confirmBtn.titleLabel?.font = UIFont.systemFont(ofSize: 17, weight: .medium)
+        confirmBtn.addCorner(corner: 3)
+
+        confirmBtn.addTarget(self, action: #selector(btnClick(sender:)), for: .touchUpInside)
+        return confirmBtn
+    }()
+
+    // 标题列表
+    lazy var titleCollectionView: UICollectionView = {
+        let flowLayout = UICollectionViewFlowLayout()
+        flowLayout.sectionInset = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0)
+        flowLayout.minimumLineSpacing = 0
+        flowLayout.minimumInteritemSpacing = 0
+        flowLayout.scrollDirection = .vertical
+ 
+        let collectionView = UICollectionView(frame: .zero, collectionViewLayout: flowLayout)
+
+        collectionView.showsVerticalScrollIndicator = false
+        collectionView.showsHorizontalScrollIndicator = false
+        collectionView.delegate = self
+        collectionView.dataSource = self
+        collectionView.backgroundColor = .clear
+        collectionView.register(PQEditPublicTitleViewContentCell.self, forCellWithReuseIdentifier: String(describing: PQEditPublicTitleViewContentCell.self))
+        if #available(iOS 11.0, *) {
+            collectionView.contentInsetAdjustmentBehavior = .never
+        }
+        // 延迟scrollView上子视图的响应,所以当直接拖动UISlider时,如果此时touch时间在150ms以内,UIScrollView会认为是拖动自己,从而拦截了event,导致UISlider接收不到滑动的event
+        collectionView.delaysContentTouches = false
+        return collectionView
+    }()
+
+    // 标题数据
+    var titles: Array<String> = Array() {
+        didSet {
+            titleCollectionView.reloadData()
+        }
+        
+    }
+
+    override init(frame: CGRect) {
+        super.init(frame: frame)
+
+        let ges = UITapGestureRecognizer(target: self, action: #selector(viewClick))
+        closeView.addGestureRecognizer(ges)
+        backgroundColor = .clear
+
+        addSubview(closeView)
+        addSubview(backView)
+
+        backView.addSubview(inputBgView)
+        inputBgView.addSubview(inputTV)
+
+        inputBgView.addSubview(confirmBtn)
+
+        backView.addSubview(titleCollectionView)
+
+        backView.snp.makeConstraints { make in
+            make.left.width.bottom.equalToSuperview()
+            make.height.equalTo(min(640, cScreenHeigth))
+//            make.width.equalTo(cScreenWidth)
+//            make.height.equalTo(min(640, cScreenHeigth))
+//            make.bottom.equalToSuperview()
+        }
+
+        closeView.snp.makeConstraints { make in
+            make.right.equalToSuperview()
+            make.width.equalTo(cScreenWidth)
+            make.height.equalTo(cScreenHeigth - 640)
+            make.top.equalToSuperview()
+        }
+
+        inputBgView.snp.makeConstraints { make in
+            make.left.equalToSuperview().offset(16)
+            make.right.equalToSuperview().offset(-16)
+            make.height.equalTo(68)
+            make.top.equalToSuperview().offset(20)
+        }
+
+        inputTV.snp.makeConstraints { make in
+            make.left.equalToSuperview().offset(14)
+            make.width.equalTo(259)
+            make.height.equalTo(48)
+            make.top.equalToSuperview().offset(10)
+        }
+
+        confirmBtn.snp.makeConstraints { make in
+            make.right.equalToSuperview()
+            make.width.equalTo(58)
+            make.height.equalTo(68)
+            make.top.equalToSuperview()
+        }
+
+        titleCollectionView.snp.makeConstraints { make in
+            make.top.equalTo(inputBgView.snp.bottom).offset(10)
+            make.width.right.equalToSuperview()
+            make.bottom.equalTo(0 - cAKSafeAreaHeight)
+//            make.width.equalTo(cScreenWidth)
+//            make.height.equalTo(542 - cAKSafeAreaHeight)
+        }
+ 
+//        titleCollectionView.reloadData()
+    }
+
+    required init?(coder _: NSCoder) {
+        fatalError("init(coder:) has not been implemented")
+    }
+
+    /// 按钮点击事件
+    @objc func btnClick(sender _: UIButton?) {
+        BFLog(message: "点击了确定 \(String(describing: inputTV.text))")
+   
+        if confirmBtnClock != nil {
+            confirmBtnClock!(inputTV.text)
+            inputTV.text = ""
+        }
+        viewClick()
+    }
+
+    @objc func viewClick() {
+        self.isHidden = true
+        inputTV.resignFirstResponder()
+        if viewIsHiddenCallBack != nil{
+            viewIsHiddenCallBack!()
+        }
+    }
+    
+    //显示界面
+    func show() {
+        isHidden = false
+        inputTV.becomeFirstResponder()
+    }
+}
+
+extension PQEditPublicTitleView: UICollectionViewDelegate, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout, UIScrollViewDelegate {
+    func collectionView(_: UICollectionView, numberOfItemsInSection _: Int) -> Int {
+        return titles.count
+    }
+
+    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
+        let cell = collectionView.dequeueReusableCell(withReuseIdentifier: String(describing: PQEditPublicTitleViewContentCell.self), for: indexPath) as! PQEditPublicTitleViewContentCell
+        cell.titleStr = titles[indexPath.row]
+        return cell
+    }
+
+    func collectionView(_: UICollectionView, didSelectItemAt indexPath: IndexPath) {
+        BFLog(message: "选择了 \(titles[indexPath.item])")
+        inputTV.text = titles[indexPath.item]
+    }
+
+    func collectionView(_ collectionView: UICollectionView, layout _: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
+        let title = titles[indexPath.row]
+ 
+        let textSize =  sizeWithText(text: title, font: UIFont.systemFont(ofSize: 17, weight: .regular), size: CGSize.init(width: 295, height: CGFloat.greatestFiniteMagnitude))
+
+        //28  是 cell label 上下边距总和
+        return CGSize(width: cScreenWidth, height: textSize.height + 28)
+    }
+
+     func scrollViewWillBeginDecelerating(_: UIScrollView) {
+        inputTV.resignFirstResponder()
+    }
+}
+
+class PQEditPublicTitleViewContentCell: UICollectionViewCell {
+    lazy var titleLab: UILabel = {
+        let titleLab = UILabel()
+        titleLab.font = UIFont.systemFont(ofSize: 17, weight: .regular)
+        titleLab.textColor = .black
+        titleLab.numberOfLines = 0
+        titleLab.lineBreakMode = .byCharWrapping
+        titleLab.isUserInteractionEnabled = true
+        titleLab.textAlignment = .left
+        return titleLab
+    }()
+
+    // 手势提示
+    lazy var iconView: UIImageView = {
+        let iconView = UIImageView()
+        iconView.backgroundColor = .clear
+        iconView.image = UIImage.moduleImage(named: "editTitleTips", moduleName: "BFFramework",isAssets: false)
+        return iconView
+    }()
+
+    lazy var lineView: UIView = {
+        let lineView = UIView()
+        lineView.backgroundColor = UIColor.hexColor(hexadecimal: "#EFEFEF")
+        return lineView
+    }()
+
+    override init(frame: CGRect) {
+        super.init(frame: frame)
+        contentView.addSubview(titleLab)
+        contentView.addSubview(iconView)
+        contentView.addSubview(lineView)
+        addLayout()
+    }
+
+    required init?(coder _: NSCoder) {
+        fatalError("init(coder:) has not been implemented")
+    }
+
+    var titleStr: String? {
+        didSet {
+            titleLab.text = titleStr
+            
+            if(titleLab.text?.count ?? 0 == 0){
+                lineView.backgroundColor = .white
+                iconView.isHidden = true
+            }else{
+                lineView.backgroundColor = UIColor.hexColor(hexadecimal: "#EFEFEF")
+                iconView.isHidden = false
+            }
+            addLayout()
+        }
+    }
+
+    func addLayout() {
+        
+        let textSize =  sizeWithText(text: titleStr ?? "", font: UIFont.systemFont(ofSize: 17, weight: .regular), size: CGSize.init(width: 295, height: CGFloat.greatestFiniteMagnitude))
+  
+        titleLab.snp.remakeConstraints { make in
+            make.height.equalTo(textSize.height * cAdaptatWidth)
+            make.right.equalToSuperview().offset(-64)
+            make.left.equalToSuperview().offset(16)
+            make.top.equalToSuperview().offset(12)
+        }
+  
+        lineView.snp.makeConstraints { make in
+           
+            make.right.equalToSuperview().offset(-16)
+            make.left.equalToSuperview().offset(16)
+            make.bottom.equalToSuperview().offset(-1)
+            make.height.equalTo(1)
+        }
+
+        iconView.snp.remakeConstraints { make in
+            make.width.height.equalTo(24)
+            make.right.equalToSuperview().offset(-16)
+            make.top.equalTo(contentView.snp.top).offset(14)
+        }
+    }
+}

+ 726 - 0
BFStuckPointKit/Classes/View/PQSelecteMusicView.swift

@@ -0,0 +1,726 @@
+//
+//  PQSelecteMusic.swift
+//  BFFramework
+//
+//  Created by ak on 2021/8/4.
+//  功能:显示编辑界面里的音乐选择界面
+
+import Foundation
+import BFUIKit
+import BFCommonKit
+
+class PQSelecteMusicView: UIView {
+    // 所有分类数据
+    var catageryDatas: [PQStuckPointMusicTagsModel] = Array<PQStuckPointMusicTagsModel>.init()
+
+    // 歌单数据
+    var musicDatas: [PQVoiceModel] = Array<PQVoiceModel>.init()
+    // 当前页码
+    var pageNum: Int = 0
+    
+    // 当前视频使用的音乐数据
+    var currentPlayingInVideoData : PQVoiceModel?
+    // 当前试听播放的音乐数据
+    var currentPlayData: PQVoiceModel?
+    // 当前播放的音频
+    var playerItem: AVPlayerItem?
+    var playerItemHavObserver: Bool = false
+    
+    // 按钮点击的回调
+    var btnClickHandle: ((_ sender: UIButton, _ bgmData: Any?) -> Void)?
+    
+    // 点击播放一个歌,回调
+    var didSelectItemHandle:((_ statue:voiceStatue) -> Void)?
+    // 当前选择的分类
+    var currentSelectTag:PQStuckPointMusicTagsModel?
+    
+    //搜索出来的歌曲要插入到热门的前面 有可能是多个
+    var searchMusiceDatas: [PQVoiceModel] = Array<PQVoiceModel>.init()
+    
+    //第一次进入时自动插入的数据
+    var firstInsertVoiceModel:PQVoiceModel?
+ 
+    lazy var avPlayer: AVPlayer = {
+        let avPlayer = AVPlayer()
+        PQNotification.addObserver(forName: .AVPlayerItemDidPlayToEndTime, object: avPlayer.currentItem, queue: .main) { [weak self] notify in
+            BFLog(message: "AVPlayerItemDidPlayToEndTime = \(notify)")
+            avPlayer.seek(to: CMTime(value: CMTimeValue((self?.currentPlayData?.startTime ?? 0) * 1000), timescale: CMTimeScale(playerTimescale)))
+            
+            if(self?.currentPlayData?.voiceStatue == .isPlaying){
+                self?.currentPlayData?.voiceStatue = .isPause
+                self?.selectMusicCollection.reloadData()
+            }
+           
+        }
+//        PQNotification.addObserver(forName: .AVPlayerItemNewErrorLogEntry, object: avPlayer.currentItem, queue: .main) { notify in
+//            BFLog(message: "AVPlayerItemNewErrorLogEntry = \(notify)")
+//        }
+//        PQNotification.addObserver(forName: .AVPlayerItemFailedToPlayToEndTime, object: avPlayer.currentItem, queue: .main) { notify in
+//            BFLog(message: "AVPlayerItemFailedToPlayToEndTime = \(notify)")
+//        }
+//        PQNotification.addObserver(forName: .AVPlayerItemPlaybackStalled, object: avPlayer.currentItem, queue: .main) { notify in
+//            BFLog(message: "AVPlayerItemPlaybackStalled = \(notify)")
+//        }
+//        avPlayer.addPeriodicTimeObserver(forInterval: CMTime(value: 1, timescale: CMTimeScale(playerTimescale)), queue: .main) { [weak self] cmTime in
+//            BFLog(message: "addPeriodicTimeObserver = \(cmTime)")
+//        }
+        return avPlayer
+    }()
+    
+    // 音乐分类的
+    lazy var categoryCollection: UICollectionView = {
+        let flowLayout = UICollectionViewFlowLayout()
+        flowLayout.sectionInset = UIEdgeInsets.zero
+        flowLayout.minimumLineSpacing = 0
+        flowLayout.minimumInteritemSpacing = 0
+        flowLayout.scrollDirection = .horizontal
+
+        let categoryCollection = UICollectionView(frame: .zero, collectionViewLayout: flowLayout)
+        categoryCollection.showsVerticalScrollIndicator = false
+        categoryCollection.showsHorizontalScrollIndicator = false
+        categoryCollection.delegate = self
+        categoryCollection.dataSource = self
+        categoryCollection.backgroundColor = .clear
+        categoryCollection.register(PQSelectMusicTagsCell.self, forCellWithReuseIdentifier: "PQSelectMusicTagsCell")
+        categoryCollection.delaysContentTouches = false
+
+        return categoryCollection
+    }()
+
+    // 每一个分类下所有歌曲
+    lazy var selectMusicCollection: UICollectionView = {
+        let flowLayout = UICollectionViewFlowLayout()
+        flowLayout.sectionInset = UIEdgeInsets.zero
+        flowLayout.minimumLineSpacing = 0
+        flowLayout.minimumInteritemSpacing = 0
+        flowLayout.scrollDirection = .horizontal
+
+        let selectMusicCollection = UICollectionView(frame: .zero, collectionViewLayout: flowLayout)
+        selectMusicCollection.showsVerticalScrollIndicator = false
+        selectMusicCollection.showsHorizontalScrollIndicator = false
+        selectMusicCollection.delegate = self
+        selectMusicCollection.dataSource = self
+        selectMusicCollection.backgroundColor = .clear
+        selectMusicCollection.register(PQSelectMusicCell.self, forCellWithReuseIdentifier: String(describing: PQSelectMusicCell.self))
+        selectMusicCollection.delaysContentTouches = false
+        selectMusicCollection.contentInset = UIEdgeInsets(top: 0, left: 17, bottom: 0, right: 0)
+        return selectMusicCollection
+    }()
+
+    // 搜索音乐btn
+    lazy var musicSearchBtn: UIButton = {
+        let musicSearchBtn = UIButton(type: .custom)
+        musicSearchBtn.setTitle("搜索", for: .normal)
+        musicSearchBtn.setImage(UIImage().BF_Image(named: "musicSearch"), for: .normal)
+        musicSearchBtn.setTitleColor(UIColor.hexColor(hexadecimal: "#959595"), for: .normal)
+        musicSearchBtn.titleLabel?.font = UIFont.systemFont(ofSize: 14)
+
+        return musicSearchBtn
+    }()
+    
+    deinit {
+        PQNotification.removeObserver(self)
+        PQNotification.removeObserver(self.avPlayer.currentItem as Any)
+        if playerItemHavObserver {
+            avPlayer.currentItem?.removeObserver(self, forKeyPath: "status")
+            avPlayer.currentItem?.removeObserver(self, forKeyPath: "error")
+        }
+        avPlayer.pause()
+        avPlayer.replaceCurrentItem(with: nil)
+        playerItem = nil
+    }
+
+    override init(frame: CGRect) {
+        super.init(frame: frame)
+        addSubview(categoryCollection)
+        addSubview(selectMusicCollection)
+        addSubview(musicSearchBtn)
+    }
+    func showData(){
+        
+        autolayout()
+
+        isHidden = false
+        //不是每一次点击都显示数据
+        if(musicDatas.count == 0){
+            loadRequestTagsList()
+        }
+     
+    }
+
+    required init?(coder _: NSCoder) {
+        fatalError("init(coder:) has not been implemented")
+    }
+
+    //插入数据
+    func insertSearchMusic(model:PQVoiceModel) {
+      
+        for oldModel in musicDatas {
+            oldModel.voiceStatue = .isNormal
+        }
+   
+        let musicIndex = musicDatas.firstIndex(where: { (music) -> Bool in
+            (music.musicId == model.musicId)
+        })
+        
+        if(musicIndex == nil){
+            model.voiceStatue = .isSelected
+            searchMusiceDatas.insert(model, at: 0)
+            //有搜索的数据
+            if(currentSelectTag?.tagId == 425){
+                musicDatas.insert(model, at: 0)
+            }
+        }else{
+            
+            let selectMusicData = musicDatas[musicIndex!]
+            selectMusicData.voiceStatue = .isSelected
+            musicDatas.remove(at: musicIndex!)
+            musicDatas.insert(selectMusicData, at: 0)
+        }
+        self.currentPlayingInVideoData = model
+        selectMusicCollection.reloadData()
+       
+        //划动到选择的音乐位置
+        selectMusicCollection.scrollToItem(at: IndexPath(row: 0, section: 0), at: .centeredHorizontally, animated: true)
+ 
+    }
+    func autolayout() {
+        categoryCollection.snp.removeConstraints()
+        selectMusicCollection.snp.removeConstraints()
+        musicSearchBtn.snp.removeConstraints()
+
+        categoryCollection.snp.makeConstraints { make in
+            make.height.equalTo(20)
+            make.left.equalToSuperview().offset(91)
+            make.right.equalToSuperview()
+            make.top.equalToSuperview().offset(14)
+        }
+        selectMusicCollection.snp.makeConstraints { make in
+            make.height.equalTo(131)
+            make.left.equalToSuperview()
+            make.right.equalToSuperview()
+            make.top.equalToSuperview().offset(54)
+        }
+
+        musicSearchBtn.snp.makeConstraints { make in
+            make.left.equalToSuperview().offset(12)
+            make.top.equalToSuperview().offset(14)
+            make.height.equalTo(20)
+            make.width.equalTo(60)
+        }
+        musicSearchBtn.imagePosition(at: PQButtonImageEdgeInsetsStyle.left, space: 8)
+    }
+
+    /// 请求标签数据
+    /// - Returns: <#description#>
+    func loadRequestTagsList() {
+        PQStuckPointViewModel.stuckPointMusicCategoryList { [weak self] tags, _, _ in
+
+            if tags.count > 0 {
+                // 请求列表数据
+                self?.requestPageListData(isRefresh: true, tagId: tags.first?.tagId ?? 0)
+                self?.catageryDatas = tags
+                self?.currentSelectTag = tags[0]
+                self?.categoryCollection.reloadData()
+            }
+        }
+    }
+
+    /// 请求指定分类的歌列表数据
+    /// - Returns:
+    func requestPageListData(isRefresh: Bool = true, isHotPage _: Bool = false, tagId: Int64) {
+        if isRefresh {
+            pageNum = 1
+            musicDatas = []
+        }
+        PQStuckPointViewModel.stuckPointMusicPageList(tagId: tagId, pageNum: pageNum, videoCount: 0, imageCount: 0, totalDuration: 0,oldDataMusic: musicDatas) { [weak self] musicInfo, _ in
+            BFLog(message: "请求音乐列表 pageNum\(String(describing: self?.pageNum)) tagId \(tagId) 返回条数\(musicInfo.count)")
+            if musicInfo.count > 0 {
+                self?.pageNum = (self?.pageNum ?? 0) + 1
+                
+                self?.musicDatas = self!.musicDatas + musicInfo
+                
+                //有搜索的数据
+                if((self?.searchMusiceDatas.count ?? 0) > 0 && self!.musicDatas.first?.cacheTagID == 425 && self?.pageNum == 2){
+                    self?.musicDatas.insert(contentsOf: self?.searchMusiceDatas ?? Array.init(), at: 0)
+                   
+                }
+                
+                if(self?.musicDatas.count ?? 0 > 0){
+                    self?.selectMusicCollection.reloadData()
+                }else{
+                    BFLog(message: "分类歌曲数据为空!!!!")
+                }
+         
+                if( self?.pageNum == 2){
+                    //歌曲列表返回到头部
+                    self?.selectMusicCollection.setContentOffset(.zero, animated: false)
+                    
+                    //第一次进入插入的歌曲,插入后置空 如果 不存在只插入一次 防止重
+                    if(self?.firstInsertVoiceModel != nil){
+                        self?.insertSearchMusic(model: (self?.firstInsertVoiceModel)!)
+                        self?.currentPlayingInVideoData = self?.firstInsertVoiceModel
+                        self?.firstInsertVoiceModel = nil
+                    }
+                    
+                    
+                }
+           
+            }
+
+        }
+    }
+    
+    /// 播放音乐
+    /// - Parameter itemData: <#itemData description#>
+    func playStuckPointMusic(itemData: PQVoiceModel?, isClearCurrentMusic: Bool = false) {
+        if itemData != nil, currentPlayData != itemData {
+            if !isValidURL(url: itemData?.musicPath ?? "") {
+                cShowHUB(superView: nil, msg: "本歌曲暂无伴奏版本哦~")
+                return
+            }
+            avPlayer.pause()
+            if playerItemHavObserver {
+                playerItem?.removeObserver(self, forKeyPath: "status")
+                playerItem?.removeObserver(self, forKeyPath: "error")
+                playerItemHavObserver = false
+            }
+
+            if itemData!.musicId == currentPlayingInVideoData?.musicId {
+                itemData?.voiceStatue = .isSelected
+                currentPlayData = itemData
+                playerItem = nil
+                return
+            }
+            
+            playerItem = AVPlayerItem(url: URL(string: itemData?.musicPath ?? "")!)
+            if (itemData?.endTime ?? 0) > 0, (itemData?.endTime ?? 0) > (itemData?.startTime ?? 0) {
+                playerItem?.forwardPlaybackEndTime = CMTime(value: CMTimeValue((itemData?.endTime ?? 0) * playerTimescale), timescale: CMTimeScale(playerTimescale))
+            }
+            avPlayer.replaceCurrentItem(with: playerItem)
+            playerItem?.addObserver(self, forKeyPath: "status", options: .new, context: nil)
+            playerItem?.addObserver(self, forKeyPath: "error", options: .new, context: nil)
+            playerItemHavObserver = true
+            avPlayer.seek(to: CMTime(value: CMTimeValue((itemData?.startTime ?? 0) * playerTimescale), timescale: CMTimeScale(playerTimescale)))
+            avPlayer.play()
+            itemData?.voiceStatue = .isPlaying
+
+            currentPlayData = itemData
+        } else if itemData != nil, avPlayer.rate == 0.0 {
+            if itemData?.musicId != currentPlayingInVideoData?.musicId {
+                avPlayer.play()
+                itemData?.voiceStatue = .isPlaying
+            }
+        } else {
+            avPlayer.pause()
+            itemData?.voiceStatue = .isPause
+      
+        }
+        if isClearCurrentMusic {
+            avPlayer.pause()
+            currentPlayData = nil
+        }
+    }
+    
+    //暂停播放音乐 并刷新 UI
+    func pausePlayer() {
+        avPlayer.pause()
+        if(currentPlayData?.voiceStatue == .isPlaying){
+            currentPlayData?.voiceStatue = .isPause
+            selectMusicCollection.reloadData()
+        }
+
+    }
+   
+}
+
+extension PQSelecteMusicView {
+    override func observeValue(forKeyPath keyPath: String?, of object: Any?, change _: [NSKeyValueChangeKey: Any]?, context _: UnsafeMutableRawPointer?) {
+        if object is AVPlayerItem, keyPath == "status" {
+            BFLog(message: "AVPlayerItem - status = \((object as! AVPlayerItem).status.rawValue)")
+            switch (object as! AVPlayerItem).status {
+            case .unknown:
+                break
+            case .readyToPlay:
+                break
+            case .failed:
+                break
+            default:
+                break
+            }
+        } else if object is AVPlayerItem, keyPath == "error" {
+            BFLog(message: "AVPlayerItem - error = \(String(describing: (object as! AVPlayerItem).error))")
+        }
+    }
+}
+
+/// 卡点音乐相关代理
+extension PQSelecteMusicView: UICollectionViewDelegate, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout, UIScrollViewDelegate {
+    func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection _: Int) -> Int {
+        if collectionView == selectMusicCollection {
+            return musicDatas.count
+        }
+        return catageryDatas.count
+    }
+
+    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
+        if collectionView == selectMusicCollection {
+
+            let itemData: Any = musicDatas[indexPath.item]
+            let cell = collectionView.dequeueReusableCell(withReuseIdentifier: String(describing: PQSelectMusicCell.self), for: indexPath) as! PQSelectMusicCell
+            if (currentPlayingInVideoData?.musicId != nil && currentPlayingInVideoData?.musicId == (itemData as? PQVoiceModel)?.musicId){
+                (itemData as? PQVoiceModel)?.voiceStatue = .isSelected
+            }
+            cell.bgmData = itemData as? PQVoiceModel
+            cell.btnClickHandle = { [weak self] sender, bgmData in
+                
+                //暂停播放音乐
+                self?.pausePlayer()
+                self?.currentPlayingInVideoData?.voiceStatue = .isNormal
+                self?.currentPlayingInVideoData = bgmData as? PQVoiceModel
+                PQEventTrackViewModel.baseReportUpload(businessType: .bt_buttonClick, objectType: .ot_shanyinApp_musicVideoPreview_musicSelect, pageSource: .sp_shanyinApp_main, extParams: ["musicName":(bgmData as? PQVoiceModel)?.musicName ?? "" ,"musicId":(bgmData as? PQVoiceModel)?.musicId ?? ""], remindmsg: "")
+                
+                if self?.btnClickHandle != nil {
+                    self?.btnClickHandle!(sender, bgmData)
+                    
+                }
+                //恢复原来状态
+                if(self?.musicDatas.count != 0){
+                    for oldModel in self!.musicDatas {
+                        oldModel.voiceStatue = .isNormal
+                    }
+                }
+             
+                let musicIndex = self?.musicDatas.firstIndex(where: { (music) -> Bool in
+                    (music.musicId == (bgmData as? PQVoiceModel)?.musicId)
+                })
+                
+                self?.musicDatas[musicIndex ?? 0].voiceStatue = .isSelected
+
+                self?.selectMusicCollection.reloadData()
+            }
+            
+            //自动请求下一页数据
+            if(indexPath.row == musicDatas.count - 5){
+                // 请求这个分类的歌单
+                requestPageListData(isRefresh: false, tagId:   currentSelectTag?.tagId ?? 0)
+            }
+            return cell
+        }
+        
+        
+        let itemData: Any = catageryDatas[indexPath.item]
+        let cell = collectionView.dequeueReusableCell(withReuseIdentifier: String(describing: PQSelectMusicTagsCell.self), for: indexPath) as! PQSelectMusicTagsCell
+        cell.tagData = itemData as? PQStuckPointMusicTagsModel
+
+        return cell
+    }
+
+    func collectionView(_ collectionView: UICollectionView, layout _: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
+        if collectionView == selectMusicCollection {
+            return CGSize(width: 60 + 20, height: 131)
+        }
+        // 音乐分类要根据文字自适应宽度
+        let textSize = sizeWithText(text: catageryDatas[indexPath.item].tagName ?? "", font: UIFont.systemFont(ofSize: 14, weight: .regular), size: CGSize(width: CGFloat.greatestFiniteMagnitude, height: 20.0))
+
+        return CGSize(width: textSize.width + 26, height: collectionView.frame.height)
+    }
+
+    func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
+        
+        //歌曲列表
+        if (collectionView == selectMusicCollection ){
+            musicDatas.forEach { item in
+                item.voiceStatue = .isNormal
+            }
+            let music = musicDatas[indexPath.item]
+            music.voiceStatue = .isSelected
+//            if music.musicId != currentPlayingInVideoData?.musicId{
+            playStuckPointMusic(itemData:music)
+//            }else{
+//                avPlayer.pause()
+//            }
+            PQEventTrackViewModel.baseReportUpload(businessType: .bt_buttonClick, objectType: .ot_shanyinApp_musicVideoPreview_musicCategorySelect, pageSource: .sp_shanyinApp_main, extParams: ["categoryName":currentSelectTag?.tagName ?? "","categoryId":currentSelectTag?.tagId ?? ""], remindmsg: "")
+
+//            if music.musicId != currentPlayingInVideoData?.musicId{
+//                BFLog(1, message: "预览:\(music.musicName) 当前:\(currentPlayingInVideoData?.musicName)")
+//            }
+            if didSelectItemHandle != nil {
+                didSelectItemHandle!(music.voiceStatue)
+            }
+            selectMusicCollection.reloadData()
+
+         
+        }else{
+            //停止分类列表的滑动。防止切换分类时 crash
+            selectMusicCollection.setContentOffset(selectMusicCollection.contentOffset , animated: false)
+            if !isNetConnected() {
+                cShowHUB(superView: nil, msg: "请有网时再试")
+                return
+            }
+
+            catageryDatas.forEach { item in
+                item.isSelected = false
+            }
+            catageryDatas[indexPath.item].isSelected = true
+            categoryCollection.reloadData()
+            currentSelectTag = catageryDatas[indexPath.item]
+            // 请求这个分类的歌单
+            requestPageListData(isRefresh: true, tagId:   currentSelectTag?.tagId ?? 0)
+        }
+   
+    }
+
+    func collectionView(_ collectionView: UICollectionView, willDisplay _: UICollectionViewCell, forItemAt indexPath: IndexPath) {
+        
+        if (collectionView == selectMusicCollection && musicDatas.count > indexPath.item ){
+            let music = musicDatas[indexPath.item]
+            
+            PQEventTrackViewModel.baseReportUpload(businessType: .bt_buttonView, objectType: .ot_shanyinApp_musicVideoPreview_musicView, pageSource: .sp_shanyinApp_main, extParams: ["musicName":music.musicName ?? "" ,"musicId":music.musicId ?? ""], remindmsg: "")
+        }
+    }
+}
+
+// 分类 cell
+class PQSelectMusicTagsCell: UICollectionViewCell {
+    lazy var titleLab: UILabel = {
+        let titleLab = UILabel()
+        titleLab.font = UIFont.systemFont(ofSize: 14)
+        titleLab.textColor = UIColor.hexColor(hexadecimal: "#959595")
+        titleLab.textAlignment = .center
+        titleLab.backgroundColor = BFConfig.shared.styleBackGroundColor
+        return titleLab
+    }()
+
+    override init(frame: CGRect) {
+        super.init(frame: frame)
+        contentView.addSubview(titleLab)
+    }
+
+    required init?(coder _: NSCoder) {
+        fatalError("init(coder:) has not been implemented")
+    }
+
+    var tagData: PQStuckPointMusicTagsModel? {
+        didSet {
+            addData()
+            addLayout()
+        }
+    }
+
+    func addData() {
+        titleLab.text = "\(tagData?.tagName ?? "")"
+        if tagData?.isSelected ?? false {
+            titleLab.textColor = UIColor.hexColor(hexadecimal: BFConfig.shared.styleColor.rawValue)
+            titleLab.font = UIFont.boldSystemFont(ofSize: 14)
+
+        } else {
+            titleLab.textColor = UIColor.hexColor(hexadecimal: "#959595")
+            titleLab.font = UIFont.systemFont(ofSize: 14)
+        }
+    }
+
+    func addLayout() {
+        titleLab.snp.makeConstraints { make in
+            make.size.equalToSuperview()
+            make.left.top.equalToSuperview()
+        }
+    }
+}
+
+// 歌曲cell PQSelectMusicCell
+class PQSelectMusicCell: UICollectionViewCell {
+    // 按钮点击的回调
+    var btnClickHandle: ((_ sender: UIButton, _ bgmData: Any?) -> Void)?
+    var contentType: stuckPointMusicContentType = .catagery
+
+    lazy var audioImageView: UIImageView = {
+        let audioImageView = UIImageView(image: bfFramworkImage(by: "videomk_music_default"))
+        audioImageView.addCorner(corner: 4)
+        audioImageView.contentMode = .scaleAspectFill
+        return audioImageView
+    }()
+
+    lazy var imageMaskView: UIView = {
+        let imageMaskView = UIView()
+        imageMaskView.backgroundColor = UIColor.hexColor(hexadecimal: BFConfig.shared.styleColor.rawValue,alpha: 0.76)
+        imageMaskView.addCorner(corner: 4)
+        return imageMaskView
+    }()
+
+    lazy var playImageView: UIImageView = {
+        let playImageView = UIImageView()
+        playImageView.image = UIImage.moduleImage(named: "stuckPoint_music_pause", moduleName: "BFFramework",isAssets: false)
+        playImageView.contentMode = .scaleAspectFit
+        return playImageView
+    }()
+
+    lazy var titleLab: UILabel = {
+        let titleLab = UILabel()
+        titleLab.font = UIFont.systemFont(ofSize: 14)
+        titleLab.textColor = BFConfig.shared.styleTitleColor
+        return titleLab
+    }()
+
+    /// 音乐歌曲名称
+    lazy var musicNameLab: UILabel = {
+        let musicNameLab = UILabel()
+        musicNameLab.textColor = UIColor.hexColor(hexadecimal: "#959595")
+        musicNameLab.font = UIFont.systemFont(ofSize: 12)
+        musicNameLab.textAlignment = .center
+        musicNameLab.lineBreakMode = .byTruncatingTail
+        musicNameLab.numberOfLines = 2
+        return musicNameLab
+    }()
+
+    // 使用按钮
+    lazy var confirmBtn: UIButton = {
+        let confirmBtn = UIButton(type: .custom)
+        confirmBtn.setTitle("使用", for: .normal)
+        confirmBtn.setTitleColor(UIColor.white, for: .normal)
+        confirmBtn.titleLabel?.font = UIFont.systemFont(ofSize: 14, weight: .medium)
+        confirmBtn.addTarget(self, action: #selector(confirmClick(sender:)), for: .touchUpInside)
+        confirmBtn.addCorner(corner: 4)
+        confirmBtn.backgroundColor = UIColor.hexColor(hexadecimal: BFConfig.shared.styleColor.rawValue)
+        return confirmBtn
+    }()
+
+    lazy var remindView: UIView = {
+        let remindView = UIView()
+        remindView.backgroundColor = UIColor.hexColor(hexadecimal: BFConfig.shared.styleColor.rawValue)
+        remindView.addCorner(corner: 3)
+        return remindView
+    }()
+
+    @objc class func stuckPointMusicContentCell(collectionView: UICollectionView, indexPath: IndexPath) -> PQStuckPointMusicContentCell {
+        let cell: PQStuckPointMusicContentCell = collectionView.dequeueReusableCell(withReuseIdentifier: String(describing: PQStuckPointMusicContentCell.self), for: indexPath) as! PQStuckPointMusicContentCell
+        return cell
+    }
+
+    override init(frame: CGRect) {
+        super.init(frame: frame)
+        contentView.addSubview(audioImageView)
+        audioImageView.addSubview(imageMaskView)
+        audioImageView.addSubview(playImageView)
+        contentView.addSubview(titleLab)
+        contentView.addSubview(musicNameLab)
+        contentView.addSubview(remindView)
+        contentView.addSubview(confirmBtn)
+        
+        
+    }
+
+    required init?(coder _: NSCoder) {
+        fatalError("init(coder:) has not been implemented")
+    }
+
+    var bgmData: PQVoiceModel? {
+        didSet {
+            addData()
+            addLayout()
+        }
+    }
+
+    func addData() {
+        audioImageView.setNetImage(url: "\(bgmData?.avatarUrl ?? "")", placeholder: bfFramworkImage(by: "videomk_music_default")!)
+        
+        if((bgmData?.musicName ?? "").count <= 4){
+            musicNameLab.text = (bgmData?.musicName ?? "").appending("\n ")
+        }else{
+            musicNameLab.text = (bgmData?.musicName ?? "")
+        }
+     
+        
+        if  bgmData?.voiceStatue == .isSelected{
+            playImageView.isHidden = false
+            imageMaskView.isHidden = false
+            playImageView.image = UIImage().BF_Image(named: "stuckPoint_music_selected")
+    
+        } else if  bgmData?.voiceStatue == .isPlaying{
+            playImageView.isHidden = false
+            imageMaskView.isHidden = false
+            playImageView.image = nil
+            playImageView.kf.setImage(with: URL(fileURLWithPath: currentBundle()!.path(forResource: "stuckPoint_music_playing", ofType: ".gif")!))
+      
+        } else if  bgmData?.voiceStatue == .isPause{
+            playImageView.isHidden = false
+            imageMaskView.isHidden = false
+            playImageView.image = UIImage.moduleImage(named: "stuckPoint_music_pause", moduleName: "BFFramework",isAssets: false)
+      
+        }else {
+            playImageView.isHidden = true
+            playImageView.image = nil
+            
+            imageMaskView.isHidden = true
+        }
+        
+//        if  bgmData?.voiceStatue == .isSelected{
+//            playImageView.isHidden = false
+//            imageMaskView.isHidden = false
+//            if bgmData?.voiceStatue == .isPlaying {
+//                playImageView.image = nil
+//                playImageView.kf.setImage(with: URL(fileURLWithPath: currentBundle()!.path(forResource: "stuckPoint_music_playing", ofType: ".gif")!))
+//
+//            } else {
+//                playImageView.image = UIImage.moduleImage(named: "stuckPoint_music_pause", moduleName: "BFFramework",isAssets: false)
+//            }
+//
+//        } else {
+//            playImageView.isHidden = true
+//            playImageView.image = nil
+//
+//            imageMaskView.isHidden = true
+//        }
+        
+        confirmBtn.isHidden = !(bgmData?.voiceStatue == .isPause || bgmData?.voiceStatue == .isPlaying)
+
+        if(bgmData?.voiceStatue == .isSelected || bgmData?.voiceStatue == .isPause || bgmData?.voiceStatue == .isPlaying){
+            musicNameLab.textColor = UIColor.hexColor(hexadecimal: BFConfig.shared.styleColor.rawValue)
+            musicNameLab.font = UIFont.boldSystemFont(ofSize: 12)
+        }else{
+            musicNameLab.textColor =  UIColor.hexColor(hexadecimal: "#959595")
+            musicNameLab.font = UIFont.systemFont(ofSize: 12)
+        }
+        
+
+    }
+
+    func addLayout() {
+      
+        audioImageView.snp.remakeConstraints { make in
+            make.left.top.equalToSuperview()
+            make.width.height.equalTo(60)
+        }
+        imageMaskView.snp.makeConstraints { make in
+            make.size.equalTo(audioImageView)
+        }
+        playImageView.snp.remakeConstraints { make in
+            make.center.equalToSuperview()
+            make.width.height.equalTo(24)
+        }
+
+        musicNameLab.snp.remakeConstraints { make in
+            make.width.equalTo(60)
+            make.height.equalTo(30)
+            make.top.equalTo(audioImageView.snp.bottom).offset(6)
+        }
+ 
+        confirmBtn.snp.remakeConstraints { make in
+            make.width.equalTo(54)
+            make.height.equalTo(29)
+            make.top.equalTo(musicNameLab.snp.bottom).offset(6)
+            make.centerX.equalTo(audioImageView.snp.centerX)
+          
+        }
+        audioImageView.addCorner(corner: 60 / 2)
+        imageMaskView.addCorner(corner: 60 / 2)
+    }
+
+    @objc func confirmClick(sender: UIButton) {
+        if btnClickHandle != nil {
+            btnClickHandle!(confirmBtn, bgmData)
+        }
+    }
+}
+
+

+ 93 - 0
BFStuckPointKit/Classes/View/PQSelectedMaterialListView.swift

@@ -0,0 +1,93 @@
+//
+//  PQSelectedMaterialListView.swift
+//  PQSpeed
+//
+//  Created by SanW on 2021/5/16.
+//  Copyright © 2021 BytesFlow. All rights reserved.
+//
+
+import UIKit
+import BFMaterialKit
+
+class PQSelectedMaterialListView: UIView {
+    var photoData: [PQEditVisionTrackMaterialsModel] = Array<PQEditVisionTrackMaterialsModel>.init() // 相册数据
+    var deletedMaterialHandle: ((_ materialData: PQEditVisionTrackMaterialsModel?, _ isDissmiss: Bool) -> Void)? // 删除已选素材回调
+    var detailMaterialHandle: ((_ indexPath: IndexPath, _ currentMaterialData: PQEditVisionTrackMaterialsModel?) -> Void)? // 点击详情
+    lazy var photoCollectionView: UICollectionView = {
+        let photoFlowLayout = UICollectionViewFlowLayout()
+        photoFlowLayout.itemSize = CGSize(width: 74, height: 74)
+        photoFlowLayout.minimumLineSpacing = cDefaultMargin
+        photoFlowLayout.minimumInteritemSpacing = 0
+        photoFlowLayout.scrollDirection = .horizontal
+        let photoCollectionView = UICollectionView(frame: bounds, collectionViewLayout: photoFlowLayout)
+        photoCollectionView.register(BFChoseMaterialCell.self, forCellWithReuseIdentifier: String(describing: BFChoseMaterialCell.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 {}
+        return photoCollectionView
+    }()
+
+    override init(frame: CGRect) {
+        super.init(frame: frame)
+        backgroundColor = BFConfig.shared.styleBackGroundColor
+        addSubview(photoCollectionView)
+    }
+
+    required init?(coder _: NSCoder) {
+        fatalError("init(coder:) has not been implemented")
+    }
+    
+    deinit {
+        BFLog(1, message: "meterialview release")
+    }
+
+    /// 添加新素材
+    /// - Parameter itemData: <#itemData description#>
+    /// - Returns: <#description#>
+    func addMaterialData(materialData: PQEditVisionTrackMaterialsModel) {
+        let temp = photoData.firstIndex { item in
+            item.asset == materialData.asset
+        }
+        if temp == nil, materialData.isSelected {
+            photoData.append(materialData)
+            photoCollectionView.reloadData()
+            photoCollectionView.scrollToItem(at: IndexPath(item: photoData.count - 1, section: 0), at: .right, animated: true)
+        } else if temp != nil, !materialData.isSelected {
+            photoData.remove(at: temp ?? 0)
+            photoCollectionView.reloadData()
+        }
+    }
+}
+
+extension PQSelectedMaterialListView: UICollectionViewDelegate, UICollectionViewDataSource, UIScrollViewDelegate {
+    func collectionView(_: UICollectionView, numberOfItemsInSection _: Int) -> Int {
+        return photoData.count
+    }
+
+    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
+        let cell = BFChoseMaterialCell.choseMaterialCell(collectionView: collectionView, indexPath: indexPath)
+        cell.isShowMediaTag = false
+        cell.isAdded = true
+        cell.materialData = photoData[indexPath.item].asset
+        cell.materialClicHandle = { [weak self] _, _ in
+            if self?.deletedMaterialHandle != nil {
+                self?.deletedMaterialHandle!(self?.photoData[indexPath.item], (self?.photoData.count ?? 0) <= 1)
+            }
+            self?.photoData[indexPath.item].isSelected = false
+            self?.photoData.remove(at: indexPath.item)
+            collectionView.reloadData()
+        }
+        return cell
+    }
+
+    func collectionView(_: UICollectionView, didSelectItemAt indexPath: IndexPath) {
+        if detailMaterialHandle != nil {
+            detailMaterialHandle!(indexPath, photoData[indexPath.item])
+        }
+    }
+}

+ 346 - 0
BFStuckPointKit/Classes/View/PQSpeedSettingView.swift

@@ -0,0 +1,346 @@
+//
+//  PQSpeedSettingView.swift
+//  BFFramework
+//
+//  Created by ak on 2021/8/2.
+//  功能:设置快慢速 跳越卡点 的倍速 VIEW
+
+import Foundation
+class PQSpeedSettingView: UIView {
+    // 速度列表
+    lazy var titleCollectionView: UICollectionView = {
+        let flowLayout = UICollectionViewFlowLayout()
+        flowLayout.sectionInset = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0)
+        flowLayout.minimumLineSpacing = 0
+        flowLayout.minimumInteritemSpacing = 0
+        flowLayout.scrollDirection = .horizontal
+
+        let collectionView = UICollectionView(frame: .zero, collectionViewLayout: flowLayout)
+
+        collectionView.showsVerticalScrollIndicator = false
+        collectionView.showsHorizontalScrollIndicator = false
+        collectionView.delegate = self
+        collectionView.dataSource = self
+        collectionView.backgroundColor = .clear
+        collectionView.register(PQSpeedTitleCell.self, forCellWithReuseIdentifier: String(describing: PQSpeedTitleCell.self))
+        if #available(iOS 11.0, *) {
+            collectionView.contentInsetAdjustmentBehavior = .never
+        }
+        // 延迟scrollView上子视图的响应,所以当直接拖动UISlider时,如果此时touch时间在150ms以内,UIScrollView会认为是拖动自己,从而拦截了event,导致UISlider接收不到滑动的event
+        collectionView.delaysContentTouches = false
+        return collectionView
+    }()
+
+    // 保存数据
+    var datas: Array<PQSpeedTitleModel> = Array()
+    var lastSelectModel: PQSpeedTitleModel?
+    // view 初化的类型 1, 快慢速度卡点  2,跳跃卡点 ,3,循环设置
+    var viewType: Int = 0 {
+        didSet {
+            
+            if(viewType == oldValue){
+                return
+            }
+            
+            titleCollectionView.snp.remakeConstraints { make in
+                make.right.equalToSuperview()
+                make.width.equalToSuperview()
+                make.height.equalTo(viewType == 1 ? 44 : 30)
+                make.top.equalToSuperview()
+            }
+            datas.removeAll()
+            if viewType == 1 {
+                let tempTitle =
+                    ["1.0x\n0.2x",
+                     "1.8x\n0.3x",
+                     "2.4x\n0.4x",
+                     "3.0x\n0.5x",
+                     "5.0x\n1.0x",
+                     "6.0x\n1.2x",
+                     "自定义\n快慢速"]
+                
+                let tempMaxSpeed = [1.0,   1.8,   2.4,   3, 5, 6, 0.0]
+                let tempMinSpeed = [0.2,   0.3,  0.4,0.5,1.0,1.2, 0.0]
+                for (index, str) in tempTitle.enumerated() {
+                    let model = PQSpeedTitleModel()
+                    model.title = str
+                    model.maxSpeed = Float(tempMaxSpeed[index])
+                    model.minSpeed = Float(tempMinSpeed[index])
+                    datas.append(model)
+                }
+
+            } else {
+                let str:String = (viewType == 2) ? "跳跃" : "循环"
+                let tempTitle =
+                    ["\(str)1x",
+                     "2x",
+                     "3x",
+                     "4x",
+                     "5x",
+                     "自定义"]
+                let tempMaxSpeed = [1, 2, 3, 4, 5, 0]
+                for (index, str) in tempTitle.enumerated() {
+                    let model = PQSpeedTitleModel()
+                    model.title = str
+                    model.maxSpeed = Float(tempMaxSpeed[index])
+                    datas.append(model)
+                }
+            }
+            //如果有老数据先插入 补位
+            if viewType == 3 && insertModle != nil{
+                datas.insert(insertModle!, at: 5)
+            }
+            if(lastSelectModel != nil){
+                selectCustom()
+            }else{
+                titleCollectionView.reloadData()
+            }
+
+        }
+    }
+    
+    //上一次插入的倍速数据
+    var insertModle:PQSpeedTitleModel?
+
+    // 点击回调 maxSpeed,minSpeed 同时为0 说明点击的是自定义速度
+    public var selectSpeedCallBack: ((_ maxSpeed: Float, _ minSpeed: Float,_ selectIndex:Int,_ isSettingPlayer:Bool) -> Void)?
+
+    override init(frame: CGRect) {
+        super.init(frame: frame)
+        addSubview(titleCollectionView)
+    }
+
+    required init?(coder _: NSCoder) {
+        fatalError("init(coder:) has not been implemented")
+    }
+    
+    
+    deinit {
+        BFLog(1, message: "speed setting view release")
+    }
+    //设置默认选择的
+    /// - Parameters:
+    ///   - index: index: 第几位 从0 开始
+    ///   - isSettingPlayer: 是否重启播放器
+    ///   - setEnable: 设置不可用状态
+    ///   - enableInsert: 是否插入一组新数据,只有第一次 < 10s 设置为 true ,自定义及其它情况都不设置 true
+
+    func setSelectItem(index:Int,isSettingPlayer:Bool = true,setDisable:Bool = false,isCancle:Bool = false,enableInsert:Bool = false) {
+       BFLog(message: "setSelectItem is \(index)")
+        if(index < 0 ){
+            BFLog(message: "选择位置数据出错\(index)")
+            return
+        }
+        //设置不可用状态
+        if(setDisable){
+            for (i,model) in datas.enumerated() {
+                model.isDisable = i == index ? false: true
+            }
+        }
+        lastSelectModel?.isSelected = false
+       
+        if viewType == 3{
+            if(index > 4 && datas.count < 7 && enableInsert){
+                if(index >= datas.count - 1){
+                    let model = PQSpeedTitleModel()
+                    model.title = "\(index + 1)x"
+                    model.maxSpeed = Float(index + 1)
+                    insertModle = model
+                    datas.insert(insertModle!, at: 5)
+                    lastSelectModel = datas[5]
+                }else{
+                    lastSelectModel = datas[index]
+                }
+         
+            }else{
+                if(index > datas.count){
+                    lastSelectModel?.isSelected = true
+                    titleCollectionView.reloadData()
+
+                    return
+                }
+                lastSelectModel = datas[index]
+            }
+        }else{
+            lastSelectModel = datas[index]
+        }
+        lastSelectModel?.isSelected = true
+        titleCollectionView.reloadData()
+ 
+        
+        //发出回调,调用方走统一处理逻辑
+        if selectSpeedCallBack != nil {
+            BFLog(message: "选择的速度为 max: \(lastSelectModel?.maxSpeed ?? 0.0) min: \(lastSelectModel?.minSpeed ?? 0.0) title  \(lastSelectModel?.title ?? "")")
+      
+                
+            let lastSelectIndex = datas.firstIndex(where: { (model) -> Bool in
+                (model.maxSpeed == lastSelectModel?.maxSpeed)
+            }) ?? 0
+
+            selectSpeedCallBack!(lastSelectModel?.maxSpeed ?? 0.0, lastSelectModel?.minSpeed ?? 0.0,lastSelectIndex,isSettingPlayer)
+       
+ 
+        }
+    }
+    
+    //选中自定义
+    func selectCustom() {
+        
+        lastSelectModel?.isSelected = false
+        lastSelectModel = datas.last
+        lastSelectModel?.isSelected = true
+        titleCollectionView.reloadData()
+         
+    }
+}
+
+extension PQSpeedSettingView: UICollectionViewDelegate, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout, UIScrollViewDelegate {
+    func collectionView(_: UICollectionView, numberOfItemsInSection _: Int) -> Int {
+        return datas.count
+    }
+
+    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
+        let cell = collectionView.dequeueReusableCell(withReuseIdentifier: String(describing: PQSpeedTitleCell.self), for: indexPath) as! PQSpeedTitleCell
+
+        cell.titleModel = datas[indexPath.row]
+        return cell
+    }
+
+    func collectionView(_: UICollectionView, didSelectItemAt indexPath: IndexPath) {
+        BFLog(message: "选择了 \(String(describing: datas[indexPath.row]))")
+        if(datas[indexPath.row].isDisable){
+            BFLog(message: "不可用速度")
+            cShowHUB(superView: nil, msg: "素材时长需要大于6秒\n   才可选择其他档位")
+            return
+        }
+        
+        if(datas[indexPath.row].title == "自定义" || datas[indexPath.row].title == "自定义\n快慢速"){
+            selectSpeedCallBack!(-1,-1,indexPath.row, false)
+        }else{
+        
+            setSelectItem(index: indexPath.row)
+        }
+        //下面只是统计  //1, 快慢速度卡点  2,跳跃卡点 ,3,循环设置
+        if(viewType == 1){
+            
+            if(datas[indexPath.row].title == "自定义" || datas[indexPath.row].title == "自定义\n快慢速"){
+                PQEventTrackViewModel.baseReportUpload(businessType: .bt_buttonClick, objectType: .ot_shanyinApp_musicVideoPreview_customizeSpeed, pageSource: .sp_stuck_previewSyncedUp, extParams:nil, remindmsg: "")
+            }else{
+                PQEventTrackViewModel.baseReportUpload(businessType: .bt_buttonClick, objectType: .ot_shanyinApp_musicVideoPreview_selectSpeed, pageSource: .sp_stuck_previewSyncedUp, extParams:nil, remindmsg: "")
+            }
+        }else if(viewType == 2){
+           
+            if(datas[indexPath.row].title == "自定义" || datas[indexPath.row].title == "自定义\n快慢速"){
+                PQEventTrackViewModel.baseReportUpload(businessType: .bt_buttonClick, objectType: .ot_shanyinApp_musicVideoPreview_customizeRatio, pageSource: .sp_stuck_previewSyncedUp, extParams:nil, remindmsg: "")
+            }else{
+                PQEventTrackViewModel.baseReportUpload(businessType: .bt_buttonClick, objectType: .ot_shanyinApp_musicVideoPreview_selectRatio, pageSource: .sp_stuck_previewSyncedUp, extParams:nil, remindmsg: "")
+            }
+     
+        }else if(viewType == 3){
+            if(datas[indexPath.row].title == "自定义" || datas[indexPath.row].title == "自定义\n快慢速"){
+                PQEventTrackViewModel.baseReportUpload(businessType: .bt_buttonClick, objectType: .ot_shanyinApp_musicVideoPreview_customizeRepeatTimes, pageSource: .sp_stuck_previewSyncedUp, extParams:nil, remindmsg: "")
+            }else{
+                PQEventTrackViewModel.baseReportUpload(businessType: .bt_buttonClick, objectType: .ot_shanyinApp_musicVideoPreview_selectRepeatTimes, pageSource: .sp_stuck_previewSyncedUp, extParams:nil, remindmsg: "")
+            }
+        }
+ 
+    }
+
+    func collectionView(_ collectionView: UICollectionView, layout _: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
+        // 20  是 cell label 上下边距总和
+        if viewType == 1 {
+            if indexPath.row == datas.count - 1 {
+                return CGSize(width: 65, height: 44)
+            }
+            return CGSize(width: 44 + 10, height: 24 + 20)
+        } else {
+            if indexPath.row == 0 || indexPath.row == datas.count - 1 {
+                return CGSize(width: 60 + 10, height: 30)
+            }
+            return CGSize(width: 30 + 10, height: 30)
+        }
+    }
+}
+
+class PQSpeedTitleModel: NSObject {
+    // UI 上显示的文字
+    var title: String = ""
+    // 是否已经选择
+    var isSelected: Bool = false
+
+    // 最大、最小速度
+    var maxSpeed: Float = 0.0
+    var minSpeed: Float = 0.0
+    //是否可用
+    var isDisable:Bool = false
+    public override init() {
+        super.init()
+    }
+}
+
+class PQSpeedTitleCell: UICollectionViewCell {
+    lazy var titleLab: UILabel = {
+        let titleLab = UILabel()
+        titleLab.font = UIFont.systemFont(ofSize: 13, weight: .regular)
+        titleLab.textColor = UIColor.hexColor(hexadecimal: "#959595")
+        titleLab.numberOfLines = 0
+        titleLab.lineBreakMode = .byCharWrapping
+        titleLab.isUserInteractionEnabled = true
+        titleLab.textAlignment = .center
+        titleLab.addCorner(corner: 5)
+
+        return titleLab
+    }()
+
+    override init(frame: CGRect) {
+        super.init(frame: frame)
+        contentView.addSubview(titleLab)
+        titleLab.snp.remakeConstraints { make in
+            make.height.equalToSuperview()
+            make.width.equalToSuperview().offset(-10)
+            make.left.equalToSuperview()
+            make.top.equalToSuperview()
+        }
+    }
+
+    required init?(coder _: NSCoder) {
+        fatalError("init(coder:) has not been implemented")
+    }
+
+    var titleModel: PQSpeedTitleModel? {
+        didSet {
+            titleLab.text = titleModel?.title
+
+            titleLab.snp.remakeConstraints { make in
+                make.height.equalToSuperview()
+                make.width.equalToSuperview().offset(-10)
+                make.left.equalToSuperview()
+                make.top.equalToSuperview()
+            }
+
+            if titleModel?.isSelected ?? false {
+            
+                let styleColor = UIColor.hexColor(hexadecimal: BFConfig.shared.styleColor.rawValue)
+                titleLab.backgroundColor = UIColor(red: styleColor.rgbaf[0], green: styleColor.rgbaf[1], blue: styleColor.rgbaf[2], alpha: 0.15)
+                titleLab.textColor = UIColor.hexColor(hexadecimal: BFConfig.shared.styleColor.rawValue)
+                
+                titleLab.font = UIFont.boldSystemFont(ofSize: 13)
+
+            } else {
+                if(titleModel?.isDisable ?? false){
+                    
+                    let backColor = BFConfig.shared.pointEditNamalBackgroundColor
+                    let textColor = UIColor.hexColor(hexadecimal: "#959595")
+                    titleLab.backgroundColor = UIColor.init(red: backColor.rgbaf[0], green: backColor.rgbaf[1], blue: backColor.rgbaf[2], alpha: 0.3)
+                    titleLab.textColor =  UIColor.init(red: textColor.rgbaf[0], green: textColor.rgbaf[1], blue: textColor.rgbaf[2], alpha: 0.3)
+                    
+                }else{
+                    titleLab.backgroundColor = BFConfig.shared.pointEditNamalBackgroundColor
+                    titleLab.textColor = UIColor.hexColor(hexadecimal: "#959595")
+                }
+                titleLab.font = UIFont.systemFont(ofSize: 13, weight: .regular)
+              
+            }
+        }
+    }
+}

+ 458 - 0
BFStuckPointKit/Classes/View/PQStuckPointCuttingView.swift

@@ -0,0 +1,458 @@
+//
+//  PQStuckPointCuttingView.swift
+//  PQSpeed
+//
+//  Created by SanW on 2021/5/8.
+//  Copyright © 2021 BytesFlow. All rights reserved.
+//
+
+import UIKit
+import BFCommonKit
+
+class PQStuckPointCuttingView: UIView {
+    // 视频时长
+    var videoDuration: CGFloat = 0
+    var lastVideoDuration:CGFloat = 0
+    // 卡点开始时间 默认 0
+    var stuckPointStartTime: CGFloat = 0
+    // 卡点结束时间
+    var stuckPointEndTime: CGFloat = 0
+    // 裁剪开始时间 默认 0
+    private var cutStartTime: CGFloat = 0
+
+//    /// 裁剪结束最终的开始时间
+//    private var cutFinishedStartTime: CGFloat {
+//        (scrollView.contentOffset.x / perSecondWidth) + (((scrollView.frame.width - videoCropView.frame.width) / 2 + 15) / perSecondWidth) + cutStartTime
+//    }
+//
+//    /// 裁剪结束最终的结束时间
+//    private var cutFinishedEndTime: CGFloat {
+//        cutFinishedStartTime + (cutEndTime - cutStartTime)
+//    }
+
+    // 播放进度
+    private var videoProgress: CGFloat = 0
+
+    // 最小时长 默认 10s
+    private var minCutTime: CGFloat = 10
+    /// 时间间隔
+    private var timeRange: CGFloat = cDefaultMargin
+    /// 刻度高
+    private var rateHeight: CGFloat = 23
+    /// 时间线宽
+    private var timeLineWidth: CGFloat = 35
+    /// 时间线间隔
+    private var timeLineMargin: CGFloat = 35
+    /// 时间线高
+    private var timeHeight: CGFloat = cDefaultMargin * 4
+    /// 频率线宽
+    private var frequencyWidth: CGFloat =  adapterWidth(width: 1.5)
+    /// 频率间隔
+    private var frequencyMargin: CGFloat =  adapterWidth(width: 3)
+    /// 竖线和contentview 父视图的左右间隔
+    private var margin: CGFloat = (cScreenWidth - adapterWidth(width: 250)) / 2
+
+    /// 滑动区域大小
+    private var contentWidth: CGFloat = 0
+
+    // 竖线一个间隔代表多少 S 是动态的
+    private var oneMarginTime: CGFloat = 0
+
+    private var isDrawLine: Bool = false
+
+    // 保存已经绘制的竖线用于变色使用
+    var lineLayerArray: Array = Array<CAShapeLayer>.init()
+    var lastDrawedLineIndex : Int = 0
+
+    // 裁剪区的相素大小
+    var cropViewWidth: CGFloat = adapterWidth(width: 250)
+    /// 拖拽改变实时的回调
+    var videoRangeDidChanged: ((_ startTime: CGFloat, _ endTime: CGFloat) -> Void)?
+    /// 进度改变实时的回调
+    var videoProgressDidChanged: ((_ progress: CGFloat) -> Void)?
+    /// 拖缀结束的回调 type - 1-拖动左边裁剪结束 2--拖动右边裁剪结束 3-进度条拖动结束 4-滑动结束
+    var videoDidEndDragging: ((_ type: Int, _ startTime: CGFloat, _ endTime: CGFloat, _ progress: CGFloat) -> Void)?
+    // 开始划动
+    var videoDidBeginDrag: (() -> Void)?
+
+    // 选择区内的线个数
+    var wavSelectCount: Int = 0
+    // 整首歌的线的个数
+    var wavTotalCount: Int = 0
+    
+    //推荐虚线的位置
+    var startLineX:CGFloat = 0.0
+    
+    //如果是用户主动划动的 就不自动滚动到推荐位置了
+    var isUserDrag:Bool = false
+    // 推荐卡点起始时间
+    var suggestRhythmStartTime:CGFloat = 0.0
+    var suggestRhythmEndTime:CGFloat = 0.0
+    
+    /// 滚动视图
+    lazy var scrollView: UIScrollView = {
+        let scrollView = UIScrollView(frame: bounds)
+        scrollView.showsVerticalScrollIndicator = false
+        scrollView.showsHorizontalScrollIndicator = false
+        scrollView.bounces = false
+        scrollView.delegate = self
+        if #available(iOS 11.0, *) {
+            scrollView.contentInsetAdjustmentBehavior = .never
+        } else {
+//            automaticallyAdjustsScrollViewInsets = false
+        }
+        scrollView.backgroundColor = .clear
+        return scrollView
+    }()
+
+    //
+    lazy var rateView: UIView = {
+        let rateView = UIView(frame: CGRect(x: 0, y: 22, width: scrollView.contentSize.width, height: rateHeight))
+        rateView.backgroundColor = .clear
+        return rateView
+    }()
+
+    // 总时长
+    lazy var tatalTimeLabel: UILabel = {
+        let tatalTimeLabel = UILabel()
+        tatalTimeLabel.font = UIFont.systemFont(ofSize: 11)
+        tatalTimeLabel.textAlignment = .right
+        tatalTimeLabel.tag = 66
+        tatalTimeLabel.textColor = UIColor.hexColor(hexadecimal: BFConfig.shared.styleColor.rawValue)
+        return tatalTimeLabel
+    }()
+
+    // 显示选择框
+    lazy var videoCropView: UIView = {
+        let videoCropView: UIView = UIView(frame: CGRect(x: (cScreenWidth - cropViewWidth) / 2, y: 0, width: cropViewWidth, height: 80))
+        videoCropView.isUserInteractionEnabled = false
+        videoCropView.layer.borderColor = UIColor.hexColor(hexadecimal: BFConfig.shared.styleColor.rawValue).cgColor
+        videoCropView.layer.borderWidth = 2
+        videoCropView.layer.cornerRadius = 8
+
+        return videoCropView
+    }()
+    //两边的mask 2 是裁剪区的边框
+    lazy var leftMaskView: UIView = {
+        let leftMaskView: UIView = UIView(frame: CGRect(x:0, y: 0, width: (cScreenWidth - cropViewWidth) / 2 - 2, height: 80))
+        leftMaskView.backgroundColor = BFConfig.shared.styleBackGroundColor
+        leftMaskView.alpha = 0.7
+        return leftMaskView
+    }()
+    
+    //右边的mask 2 是裁剪区的边框
+    lazy var rightMaskView: UIView = {
+        let rightMaskView: UIView = UIView(frame: CGRect(x:videoCropView.frame.maxX + 2, y: 0, width: (cScreenWidth - cropViewWidth) / 2, height: 80))
+        rightMaskView.backgroundColor = BFConfig.shared.styleBackGroundColor
+        rightMaskView.alpha = 0.7
+        return rightMaskView
+    }()
+    
+    
+
+    private override init(frame: CGRect) {
+        super.init(frame: frame)
+    }
+
+    required init?(coder _: NSCoder) {
+        fatalError("init(coder:) has not been implemented")
+    }
+
+    init(frame: CGRect, duration: CGFloat, suggestRhythmStartTime: CGFloat) {
+        super.init(frame: frame)
+        videoDuration = duration
+        self.suggestRhythmStartTime = suggestRhythmStartTime
+    
+    }
+
+    /// 更新卡点值
+    /// - Parameter endTime: endTime description
+    /// - Returns: <#description#>
+    func updateEndTime(startTime: CGFloat, endTime: CGFloat,
+                       suggestRhythmStartTime: CGFloat, suggestRhythmEndTime: CGFloat) {
+        
+//        videoDuration = duration
+        self.suggestRhythmStartTime = suggestRhythmStartTime
+        self.suggestRhythmEndTime = suggestRhythmEndTime
+        startLineX = 0
+        
+        stuckPointStartTime = startTime
+        stuckPointEndTime = endTime
+        
+        tatalTimeLabel.text = "\(Float64(stuckPointEndTime - stuckPointStartTime).formatDurationToHMS())"
+  
+        BFLog(1, message: "播放开始:\(stuckPointStartTime) 结束:\(stuckPointEndTime) 时长为:\(stuckPointEndTime - stuckPointStartTime); 音乐总时长为:\(videoDuration);推荐卡点开始:\(suggestRhythmStartTime) 结束:\(suggestRhythmEndTime)")
+        backgroundColor = BFConfig.shared.styleBackGroundColor
+        addSubview(scrollView)
+
+        addSubview(videoCropView)
+        addSubview(leftMaskView)
+        addSubview(rightMaskView)
+        videoCropView.addSubview(tatalTimeLabel)
+        addData()
+        videoCropView.frame = CGRect(x: (cScreenWidth - cropViewWidth) / 2, y: 0, width: cropViewWidth, height: 80)
+        leftMaskView.frame = CGRect(x:0, y: 0, width: (cScreenWidth - cropViewWidth) / 2 - 2, height: 80)
+        rightMaskView.frame = CGRect(x:videoCropView.frame.maxX + 2, y: 0, width: (cScreenWidth - cropViewWidth) / 2 - 2, height: 80)
+
+        tatalTimeLabel.snp.remakeConstraints { make in
+            make.width.equalTo(40)
+            make.height.equalTo(15)
+            make.top.equalTo(videoCropView.snp.top).offset(6)
+            make.right.equalTo(videoCropView.snp.right).offset(-6)
+        }
+        
+      
+    }
+
+    func addData() {
+        // 1,选择区内的线个数 ,划动区域后 个数会变???
+        wavSelectCount = Int(ceil((adapterWidth(width: 250) - frequencyWidth) / (frequencyWidth + frequencyMargin)) + 1)
+        
+        cropViewWidth = CGFloat(wavSelectCount) * (frequencyWidth + frequencyMargin) + frequencyWidth
+        margin = (cScreenWidth - cropViewWidth) / 2.0
+
+     
+        // 2竖线一个间隔代表多少 S 是动态的
+        oneMarginTime = (stuckPointEndTime - stuckPointStartTime) / CGFloat(wavSelectCount)
+        
+        // 如果视频结束时间点大于歌曲有效结束点,则拼接推荐的时间段直到满足视频播放
+        var videoDurationTemp = suggestRhythmEndTime
+        while stuckPointEndTime > videoDurationTemp {
+            videoDurationTemp  += (suggestRhythmEndTime - suggestRhythmStartTime)
+        }
+        videoDuration = videoDurationTemp
+        // 3,一共绘制的竖线个数
+        wavTotalCount = Int(ceil(videoDuration / oneMarginTime) + 1)
+
+        timeRange = oneMarginTime * 10
+        // 显示时间 label 的个数 , -1 不够整倍数就不显示时间了
+        let timeLabelCount = Int(wavTotalCount / 10)
+        
+        contentWidth = CGFloat(wavTotalCount - 1) * (frequencyWidth + frequencyMargin) + frequencyWidth + (cScreenWidth - cropViewWidth)
+        if contentWidth < scrollView.frame.width {
+            contentWidth = scrollView.frame.width
+        }
+        scrollView.contentSize = CGSize(width: contentWidth, height: scrollView.frame.height)
+        
+        BFLog(1, message: "框内个数:\(wavSelectCount), 总线条数:\(wavTotalCount), 框宽:\(cropViewWidth), 最终音乐时长:\(videoDuration)")
+        scrollView.subviews.forEach { lable in
+            if lable is UILabel && lable.tag != 66 {
+                lable.removeFromSuperview()
+            }
+        }
+        for index in 0 ... timeLabelCount {
+//            scrollView.viewWithTag(100 + index)?.removeFromSuperview()
+            let titleLab = UILabel(frame: CGRect(x: CGFloat(index) *  (frequencyWidth + frequencyMargin) * 10 + margin - timeLineWidth / 2, y: rateView.frame.maxY, width: timeLineWidth, height: 30))
+            titleLab.font = UIFont.systemFont(ofSize: 11)
+            titleLab.textAlignment = .center
+            titleLab.numberOfLines = 1
+            titleLab.tag = 100 + index
+            titleLab.backgroundColor = .clear
+            titleLab.textColor = UIColor.hexColor(hexadecimal: "#999999")
+          
+            titleLab.text = "\(Float64(Int(CGFloat(index) * timeRange)).formatDurationToHMS())"
+            scrollView.addSubview(titleLab)
+        }
+        if oneMarginTime > 0 {
+            // 1,处理音频频率
+            configVoiceFrequency()
+            // 2,滚动到推荐位置
+            if(!isUserDrag){
+                scrollView.contentOffset = CGPoint(x: startLineX - margin, y: 0)
+            }
+        
+            scrollView.addSubview(rateView)
+        }
+    }
+
+    /// 处理音频频率
+    /// - Returns: <#description#>
+    func configVoiceFrequency() {
+        // 整倍数
+        let waveTotalCount = Int(wavTotalCount) / cFrequency.count
+        // 余多少个未画的
+        var remainder = Int(wavTotalCount % cFrequency.count)
+        var totalWave: [CGFloat] = Array<CGFloat>.init()
+//         1,先画整倍数个竖线
+        for _ in 0 ..< waveTotalCount {
+            totalWave = totalWave + cFrequency
+        }
+        if remainder > cFrequency.count - 1 {
+            remainder = cFrequency.count - 1
+        }
+        // 1,再画余数个竖线
+        if remainder > 0 {
+            totalWave = totalWave + cFrequency[0 ... (remainder - 1)]
+        }
+
+        createWave(waveArr: totalWave)
+    }
+
+    /// 更新进度绘制不同色值
+    /// progress <#progress description#>
+    func updateProgress(progress: CGFloat) {
+        
+        if(progress <= 0 || lineLayerArray.count == 0 || progress.isNaN){
+            BFLog(message: "progress is error \(progress) lineLayerArray \(lineLayerArray)")
+            return
+        }
+        
+        let startIndex = scrollView.contentOffset.x / (frequencyWidth + frequencyMargin)
+        lastDrawedLineIndex = max(lastDrawedLineIndex, Int(ceil(startIndex)))
+        let selectIndex = Int(ceil(startIndex + progress * CGFloat(wavSelectCount)))
+        while(selectIndex < lineLayerArray.count && selectIndex > lastDrawedLineIndex){
+            let drawLayer:CAShapeLayer = lineLayerArray[lastDrawedLineIndex]
+            if drawLayer.strokeColor != UIColor.hexColor(hexadecimal: BFConfig.shared.styleColor.rawValue).cgColor{
+//                BFLog(1, message: "progress is \(progress) i \(lastDrawedLineIndex) 命中的位置:\(CGFloat(lastDrawedLineIndex) * oneMarginTime)")
+                drawLayer.strokeColor = UIColor.hexColor(hexadecimal: BFConfig.shared.styleColor.rawValue).cgColor
+                drawLayer.setNeedsDisplay()
+                drawLayer.layoutIfNeeded()
+            }
+            lastDrawedLineIndex += 1
+
+        }
+
+        
+        if(progress >= 0.999){
+            BFLog(message: "播放完成 重新更新 UI ")
+            resetDefaultsColor(clearData: false)
+        }
+
+    }
+
+    // 竖线恢复到原有色值
+    func resetDefaultsColor(clearData:Bool = true) {
+        lastDrawedLineIndex = 0
+        for layer in lineLayerArray {
+            layer.strokeColor = UIColor.hexColor(hexadecimal: "#999999").cgColor
+            layer.setNeedsDisplay()
+        }
+        if(clearData == true){
+            lineLayerArray.removeAll()
+            if(rateView.layer.sublayers != nil){
+                for (_,layer) in rateView.layer.sublayers!.enumerated() {
+                    layer.removeFromSuperlayer()
+                }
+            }
+      
+            isUserDrag = false
+            isDrawLine = false
+        }
+   
+    }
+
+    /// 生成波纹
+    /// - Parameter waveArr: <#waveArr description#> // warning 有崩溃 _buffer    _ArrayBuffer<CoreGraphics.CGFloat>    wavearr为CoreGraph.CGFloat数组
+    /// - Returns: <#description#>
+    func createWave(waveArr: [CGFloat]) {
+        for (i, power) in waveArr.enumerated() {
+            // 画布高度
+            let hight: CGFloat = rateView.frame.height
+            // 开始 Y 值
+            var startY: CGFloat = (hight - power) / 2.0
+            if startY < 0 { startY = 0 }
+            // 结束 Y 值
+            var endY: CGFloat = startY + power
+            if endY > CGFloat(hight) { endY = hight }
+            // 线的路径
+            let linePath = UIBezierPath()
+            // 起点 timeLineWidth / 2 处理显示时间的 label中心为时间点
+            let originX: CGFloat = CGFloat(Float(i) * Float(frequencyWidth + frequencyMargin)) + margin
+
+            linePath.move(to: CGPoint(x: originX, y: startY))
+            // 终点
+            linePath.addLine(to: CGPoint(x: originX, y: endY))
+            let lineLayer = CAShapeLayer()
+
+            lineLayer.lineWidth = frequencyWidth
+            lineLayer.strokeColor = UIColor.hexColor(hexadecimal: "#999999").cgColor
+            lineLayer.path = linePath.cgPath
+            lineLayer.fillColor = UIColor.black.cgColor
+
+            // 推荐的开始起点是虚线 减0.0001因为精度问题
+//            BFLog(1, message: "suggestRhythmStartTime is \(suggestRhythmStartTime)")
+            if oneMarginTime * CGFloat(i) >= (suggestRhythmStartTime-0.0001) && !isDrawLine {
+                isDrawLine = true
+                linePath.move(to: CGPoint(x: originX, y: -10))
+                // 终点
+                linePath.addLine(to: CGPoint(x: originX, y: 30))
+                lineLayer.path = linePath.cgPath
+                lineLayer.lineDashPhase = 0
+                lineLayer.lineDashPattern = [3, 3]
+            }
+            if startLineX == 0 && oneMarginTime * CGFloat(i) >= stuckPointStartTime{
+                startLineX = originX
+            }
+
+            lineLayerArray.append(lineLayer)
+            rateView.layer.insertSublayer(lineLayer, at: 0)
+        }
+    }
+
+    deinit {
+        BFLog(message: "卡点裁剪-裁剪视图销毁")
+    }
+    
+    //划动结速后处理
+    func moveEnd() {
+        //最后一个竖线VIEW
+        let lastLine:UIView = scrollView.viewWithTag(100 +  Int(videoDuration / timeRange) - 1) ?? UIView.init()
+        //移动后的开始时间
+        let startTime =  videoDuration / lastLine.frame.maxX * scrollView.contentOffset.x
+//        let startTime =  videoDuration * (margin + scrollView.contentOffset.x) / scrollView.contentSize.width
+        //选中的时长
+        let selectDuration:CGFloat = CGFloat(stuckPointEndTime - stuckPointStartTime)
+        BFLog(message: "拖拽结束 - 回调\(scrollView.contentOffset)  \(scrollView.contentSize) 开始时间为:\(startTime) 结束时间为:\(startTime + selectDuration)")
+        stuckPointStartTime = startTime
+        stuckPointEndTime = stuckPointStartTime + selectDuration
+        if(videoDidEndDragging != nil){
+            videoDidEndDragging!(1,startTime,startTime + CGFloat(stuckPointEndTime - stuckPointStartTime),0)
+        }
+        resetDefaultsColor(clearData: false)
+        PQEventTrackViewModel.baseReportUpload(businessType: .bt_buttonClick, objectType: objectType.ot_shanyinApp_musicVideoPreview_musicPeriodSelect, pageSource: PAGESOURCE.sp_shanyinApp_main, extParams: nil, remindmsg: "")
+
+
+
+    }
+}
+
+// MARK: - scrollView滑动代理
+
+/// scrollView滑动代理
+extension PQStuckPointCuttingView: UIScrollViewDelegate {
+    func scrollViewDidScroll(_: UIScrollView) {}
+ 
+    func scrollViewWillBeginDragging(_ :UIScrollView){
+        isUserDrag = true
+        if(videoDidBeginDrag != nil){
+            videoDidBeginDrag!()
+        }
+        
+    }
+    
+    func scrollViewDidEndDecelerating(_: UIScrollView) {
+        if !scrollView.isDragging, !scrollView.isDecelerating {
+
+            DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.2) { [weak self] in
+                self?.moveEnd()
+            }
+        }
+       
+    }
+    func scrollViewDidEndDragging(_:UIScrollView,willDecelerate decelerate:Bool){
+        if !decelerate, !scrollView.isDragging, !scrollView.isDecelerating {
+            
+            DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.2) { [weak self] in
+                self?.moveEnd()
+            }
+ 
+        }
+       
+    }
+    
+    func scrollViewDidEndScrollingAnimation(_: UIScrollView) {
+        BFLog(message: "scrollViewDidEndScrollingAnimation")
+        
+        
+    }
+}

+ 102 - 0
BFStuckPointKit/Classes/View/PQStuckPointLoadingView.swift

@@ -0,0 +1,102 @@
+//
+//  PQStuckPointLoadingView.swift
+//  PQSpeed
+//
+//  Created by SanW on 2021/5/24.
+//  Copyright © 2021 BytesFlow. All rights reserved.
+//
+
+import Kingfisher
+import UIKit
+import BFCommonKit
+import BFUIKit
+
+class PQStuckPointLoadingView: UIView {
+    var cancelHandle: ((_ sender: UIButton) -> Void)?
+    
+    /// 同步进度显示
+    lazy var loadingView: AnimatedImageView = {
+        let videoLoadingView = AnimatedImageView()
+        videoLoadingView.kf.setImage(with: URL(fileURLWithPath: (Bundle.current(moduleName: "BFFramework", isAssets: false)?.path(forResource: "stuckPoint_edit_loading", ofType: ".gif"))!))
+        videoLoadingView.stopAnimating()
+        return videoLoadingView
+    }()
+ 
+    lazy var navBarLeftBtn: UIButton = {
+        let navBarLeftBtn = UIButton(type: .custom)
+        navBarLeftBtn.frame = CGRect(x: 0, y: cDevice_iPhoneStatusBarHei, width: cDefaultMargin * 4, height: cDefaultMargin * 4)
+        navBarLeftBtn.imageEdgeInsets = UIEdgeInsets(top: 0, left: 0, bottom: -5, right: 0)
+        navBarLeftBtn.imageEdgeInsets = UIEdgeInsets(top: 0, left: 0, bottom: -5, right: 0)
+        navBarLeftBtn.tintColor = BFConfig.shared.styleTitleColor
+        navBarLeftBtn.setImage(imageInUIKit(by: "icon_detail_back")?.withRenderingMode(.alwaysTemplate), for: .normal)
+        navBarLeftBtn.addTarget(self, action: #selector(cancelDownload(sender:)), for: .touchUpInside)
+        return navBarLeftBtn
+    }()
+
+    override init(frame: CGRect) {
+        super.init(frame: frame)
+        backgroundColor = UIColor.init(red: 1, green: 1, blue: 1, alpha: 0.5)
+        addSubViews()
+        addLayout()
+        
+        loadingView.startAnimating()
+    }
+    
+    required init?(coder _: NSCoder) {
+        fatalError("init(coder:) has not been implemented")
+    }
+
+    override func layoutSubviews() {
+        super.layoutSubviews()
+    }
+
+    func addSubViews() {
+    
+        addSubview(loadingView)
+        addSubview(navBarLeftBtn)
+        
+    }
+
+    func addLayout() {
+        loadingView.snp.makeConstraints { make in
+//            make.top.equalToSuperview().offset(cScreenWidth / 2.0 + cDevice_iPhoneStatusBarHei)
+            make.centerX.equalTo(cScreenWidth / 2.0)
+            make.centerY.equalTo(cScreenHeigth / 2.0)
+            make.width.height.equalTo(cDefaultMargin * 10)
+        }
+        
+    }
+    
+    
+    func show() {
+        if self.superview != nil {
+            return
+        }
+        UIApplication.shared.keyWindow?.addSubview(self)
+        loadingView.startAnimating()
+
+    }
+
+    /// 移除视图
+    /// - Returns: <#description#>
+    func removeMarskView() {
+        loadingView.stopAnimating()
+     
+        if self.superview != nil {
+            removeFromSuperview()            
+        }
+      
+        BFLog(message: "removeMarskViewremoveMarskViewremoveMarskViewremoveMarskView")
+    }
+
+    @objc func cancelDownload(sender: UIButton) {
+        if cancelHandle != nil {
+            cancelHandle!(sender)
+            removeMarskView()
+        }
+    }
+
+    deinit {
+        BFLog(message: "销毁加载中视图1111111")
+    }
+}

+ 67 - 0
BFStuckPointKit/Classes/View/PQStuckPointMaterialHeadView.swift

@@ -0,0 +1,67 @@
+//
+//  PQStuckPointMaterialHeadView.swift
+//  PQSpeed
+//
+//  Created by SanW on 2021/5/16.
+//  Copyright © 2021 BytesFlow. All rights reserved.
+//
+
+import SnapKit
+import UIKit
+import BFCommonKit
+
+class PQStuckPointMaterialHeadView: UIView {
+    lazy var iconImageView: UIImageView = {
+        let iconImageView = UIImageView(image:UIImage.moduleImage(named: "videomk_netMaterial_selected", moduleName: "BFFramework",isAssets: false))
+        return iconImageView
+    }()
+
+    lazy var titleLab: UILabel = {
+        let titleLab = UILabel()
+        titleLab.font = UIFont.systemFont(ofSize: 14, weight: .semibold)
+        titleLab.textColor = UIColor.white
+        let attString = NSMutableAttributedString(string: "选择的视频总时长 ≥20 秒,效果会更佳哦~")
+        attString.addAttributes([.foregroundColor: UIColor.hexColor(hexadecimal: "#FBCC37")], range: NSRange(location: 10, length: 3))
+        titleLab.attributedText = attString
+        return titleLab
+    }()
+
+    lazy var desLab: UILabel = {
+        let desLab = UILabel()
+        desLab.font = UIFont.systemFont(ofSize: 14)
+        desLab.textColor = UIColor.white
+        desLab.text = "可同时选视频与图片"
+        return desLab
+    }()
+
+    override init(frame: CGRect) {
+        super.init(frame: frame)
+        backgroundColor = UIColor.hexColor(hexadecimal: "#333333")
+        addSubview(iconImageView)
+        addSubview(titleLab)
+        addSubview(desLab)
+    }
+
+    required init?(coder _: NSCoder) {
+        fatalError("init(coder:) has not been implemented")
+    }
+
+    override func layoutSubviews() {
+        super.layoutSubviews()
+        iconImageView.snp.makeConstraints { make in
+            make.width.height.equalTo(40)
+            make.left.equalToSuperview().offset(cDefaultMargin * 2)
+            make.centerY.equalToSuperview()
+        }
+        titleLab.snp.makeConstraints { make in
+            make.left.equalTo(iconImageView.snp.right).offset(cDefaultMargin)
+            make.right.equalToSuperview().offset(-cDefaultMargin)
+            make.top.equalTo(iconImageView)
+        }
+        desLab.snp.makeConstraints { make in
+            make.left.right.equalTo(titleLab)
+            make.bottom.equalTo(iconImageView)
+        }
+        addCorner(roundingCorners: [.topLeft, .topRight], corner: 6)
+    }
+}

+ 260 - 0
BFStuckPointKit/Classes/View/PQStuckPointMusicContentCell.swift

@@ -0,0 +1,260 @@
+//
+//  PQStuckPointMusicContentCell.swift
+//  PQSpeed
+//
+//  Created by SanW on 2021/4/28.
+//  Copyright © 2021 BytesFlow. All rights reserved.
+//
+
+import UIKit
+import BFCommonKit
+
+class PQStuckPointMusicContentCell: UICollectionViewCell {
+    // 按钮点击的回调
+    var btnClickHandle: ((_ sender: UIButton, _ bgmData: Any?) -> Void)?
+    var contentType: stuckPointMusicContentType = .catagery
+
+    lazy var audioImageView: UIImageView = {
+         let audioImageView = UIImageView(image:bfFramworkImage(by: "videomk_music_default")!)
+        audioImageView.addCorner(corner: 4)
+        audioImageView.contentMode = .scaleAspectFill
+        return audioImageView
+    }()
+
+    lazy var imageMaskView: UIView = {
+        let imageMaskView = UIView()
+        imageMaskView.backgroundColor = UIColor(red: 0, green: 0, blue: 0, alpha: 0.6)
+        imageMaskView.addCorner(corner: 4)
+        return imageMaskView
+    }()
+
+    lazy var playImageView: UIImageView = {
+        let playImageView = UIImageView()
+        playImageView.image = UIImage.moduleImage(named: "stuckPoint_music_pause", moduleName: "BFFramework",isAssets: false)
+        return playImageView
+    }()
+
+    lazy var titleLab: UILabel = {
+        let titleLab = UILabel()
+        titleLab.font = UIFont.systemFont(ofSize: 14)
+        titleLab.textColor = BFConfig.shared.styleTitleColor
+        return titleLab
+    }()
+
+    /// 音乐歌曲名称
+    lazy var musicNameLab: LMJHorizontalScrollText = {
+        let musicNameLab = LMJHorizontalScrollText(frame: CGRect(x: 0, y: 0, width: cDefaultMargin * 6, height: cDefaultMargin * 3))
+        musicNameLab.textColor = BFConfig.shared.styleTitleColor
+        musicNameLab.textFont = UIFont.systemFont(ofSize: 16)
+        musicNameLab.speed = 0.03
+        musicNameLab.moveDirection = LMJTextScrollMoveLeft
+        musicNameLab.moveMode = LMJTextScrollContinuous
+        musicNameLab.stop()
+        return musicNameLab
+    }()
+
+    // 使用按钮
+    lazy var confirmContentView: UIView = {
+        let confirmContentView = UIView()
+        confirmContentView.backgroundColor = BFConfig.shared.styleBackGroundColor
+        let ges = UITapGestureRecognizer(target: self, action: #selector(confirmClick))
+        confirmContentView.addGestureRecognizer(ges)
+        confirmContentView.isHidden = true
+        return confirmContentView
+    }()
+
+    // 使用按钮
+    lazy var confirmBtn: UIButton = {
+        let confirmBtn = UIButton(type: .custom)
+        confirmBtn.setTitle("  使用  ", for: .normal)
+        confirmBtn.setTitleColor(UIColor.white, for: .normal)
+        confirmBtn.titleLabel?.font = UIFont.systemFont(ofSize: 14, weight: .medium)
+        confirmBtn.isUserInteractionEnabled = false
+        confirmBtn.addCorner(corner: 5)
+        confirmBtn.backgroundColor = UIColor.hexColor(hexadecimal: BFConfig.shared.styleColor.rawValue)
+        return confirmBtn
+    }()
+
+    lazy var remindView: UIView = {
+        let remindView = UIView()
+        remindView.backgroundColor = UIColor.hexColor(hexadecimal: BFConfig.shared.styleColor.rawValue)
+        remindView.addCorner(corner: 3)
+        return remindView
+    }()
+
+    @objc class func stuckPointMusicContentCell(collectionView: UICollectionView, indexPath: IndexPath) -> PQStuckPointMusicContentCell {
+        let cell: PQStuckPointMusicContentCell = collectionView.dequeueReusableCell(withReuseIdentifier: String(describing: PQStuckPointMusicContentCell.self), for: indexPath) as! PQStuckPointMusicContentCell
+        return cell
+    }
+
+    override init(frame: CGRect) {
+        super.init(frame: frame)
+        contentView.addSubview(audioImageView)
+        audioImageView.addSubview(imageMaskView)
+        audioImageView.addSubview(playImageView)
+        contentView.addSubview(titleLab)
+        contentView.addSubview(musicNameLab)
+        contentView.addSubview(remindView)
+        contentView.addSubview(confirmContentView)
+        confirmContentView.addSubview(confirmBtn)
+        PQNotification.addObserver(forName: Notification.Name(rawValue: "MusicContentCellIconLoadingAnimationStop"), object: nil, queue: .main) { [weak self] notice in
+            if !(self?.imageMaskView.isHidden ?? true){
+                self?.stopLoadingAnimation()
+            }
+        }
+    }
+
+    required init?(coder _: NSCoder) {
+        fatalError("init(coder:) has not been implemented")
+    }
+    deinit {
+        PQNotification.removeObserver(self)
+    }
+
+    var bgmData: Any? {
+        didSet {
+            addData()
+            addLayout()
+        }
+    }
+
+    func addData() {
+        imageMaskView.isHidden = !(bgmData is PQVoiceModel) || BFConfig.shared.hiddenMusicMask
+        musicNameLab.isHidden = !(bgmData is PQVoiceModel)
+        titleLab.isHidden = (bgmData is PQVoiceModel)
+        if bgmData is PQVoiceModel {
+            audioImageView.setNetImage(url: "\((bgmData as? PQVoiceModel)?.avatarUrl ?? "")", placeholder: bfFramworkImage(by: "videomk_music_default")!)
+            confirmContentView.isHidden = !((bgmData as? PQVoiceModel)?.isSelected ?? false)
+            if (bgmData as? PQVoiceModel)?.isSelected ?? false {
+                imageMaskView.isHidden = false
+                playImageView.isHidden = false
+                if (bgmData as? PQVoiceModel)?.isPlaying ?? false {
+
+                    if playImageView.image == nil {
+                        playImageView.image = UIImage.moduleImage(named: "loading", moduleName: "BFFramework", isAssets: false)
+                        startLoadingAnimation()
+                    }else {
+                        playImageView.kf.setImage(with: URL(fileURLWithPath: (currentBundlePath()!.path(forResource: "stuckPoint_music_playing", ofType: ".gif")!)))
+
+                    }
+                    musicNameLab.move()
+                } else {
+                    playImageView.image = UIImage.moduleImage(named: "stuckPoint_music_pause", moduleName: "BFFramework",isAssets: false)
+                    musicNameLab.stop()
+                }
+            } else {
+                playImageView.isHidden = true
+                playImageView.image = nil
+                musicNameLab.stop()
+            }
+        } else {
+            if (bgmData as? PQStuckPointMusicTagsModel)?.tagEmoji != nil {
+                audioImageView.setNetImage(url: "\((bgmData as? PQStuckPointMusicTagsModel)?.tagEmoji ?? "")", placeholder:bfFramworkImage(by: "videomk_music_default")!)
+            } else {
+                audioImageView.image = bfFramworkImage(by: "videomk_music_default")
+            }
+            titleLab.text = " \((bgmData as? PQStuckPointMusicTagsModel)?.tagName ?? "")"
+            if (titleLab.text?.count ?? 0) > 8 {
+                titleLab.font = UIFont.systemFont(ofSize: 8)
+            } else if (titleLab.text?.count ?? 0) > 7 {
+                titleLab.font = UIFont.systemFont(ofSize: 10)
+            } else if (titleLab.text?.count ?? 0) > 6 {
+                titleLab.font = UIFont.systemFont(ofSize: 11)
+            } else {
+                titleLab.font = UIFont.systemFont(ofSize: 14)
+            }
+            contentView.backgroundColor = ((bgmData as? PQStuckPointMusicTagsModel)?.isSelected ?? false) ? BFConfig.shared.styleBackGroundColor : BFConfig.shared.otherTintColor
+            titleLab.textColor = ((bgmData as? PQStuckPointMusicTagsModel)?.isSelected ?? false) ? UIColor.hexColor(hexadecimal: BFConfig.shared.styleColor.rawValue) : BFConfig.shared.styleTitleColor
+            remindView.isHidden = !((bgmData as? PQStuckPointMusicTagsModel)?.isSelected ?? false)
+        }
+    }
+
+    func addLayout() {
+        let margin: CGFloat = 12
+        let leftmargin: CGFloat = 16
+        let imageW: CGFloat = bgmData is PQVoiceModel ? 55 : 22
+        if bgmData is PQVoiceModel {
+            var nameW: CGFloat = sizeWithText(text: (bgmData as? PQVoiceModel)?.musicName ?? "", font: UIFont.systemFont(ofSize: 16), size: CGSize(width: frame.width - leftmargin - imageW - margin * 2, height: cDefaultMargin * 3)).width
+            if nameW < cDefaultMargin * 3 {
+                musicNameLab.text = "\((bgmData as? PQVoiceModel)?.musicName ?? "")              "
+                nameW = cDefaultMargin * 6
+            } else if nameW < cDefaultMargin * 6 {
+                musicNameLab.text = "\((bgmData as? PQVoiceModel)?.musicName ?? "")        "
+                nameW = cDefaultMargin * 6
+            } else {
+                musicNameLab.text = "\((bgmData as? PQVoiceModel)?.musicName ?? "") "
+            }
+            audioImageView.snp.remakeConstraints { make in
+                make.left.equalToSuperview().offset(leftmargin)
+                make.centerY.equalToSuperview()
+                make.width.height.equalTo(imageW)
+            }
+            imageMaskView.snp.makeConstraints { make in
+                make.size.equalToSuperview()
+            }
+            playImageView.snp.remakeConstraints { make in
+                make.center.equalToSuperview()
+                make.width.height.equalTo(cDefaultMargin * 2)
+            }
+
+            musicNameLab.snp.remakeConstraints { make in
+                make.left.equalTo(audioImageView.snp.right).offset(margin)
+                make.width.equalTo(nameW)
+                make.height.equalTo(cDefaultMargin * 3)
+                make.centerY.equalToSuperview()
+            }
+            confirmContentView.snp.remakeConstraints { make in
+                make.right.equalToSuperview()
+                make.centerY.equalToSuperview()
+                make.width.equalTo(85)
+                make.height.equalTo(55)
+            }
+            confirmBtn.snp.remakeConstraints { make in
+                make.right.equalToSuperview().offset(-leftmargin)
+                make.centerY.equalToSuperview()
+//                make.width.equalTo(confirmBtn)
+                make.height.equalTo(cDefaultMargin * 3)
+            }
+            audioImageView.addCorner(corner: imageW / 2)
+            imageMaskView.addCorner(corner: imageW / 2)
+        } else {
+            audioImageView.snp.remakeConstraints { make in
+                make.left.equalToSuperview().offset(cDefaultMargin)
+                make.centerY.equalToSuperview()
+                make.width.height.equalTo(imageW)
+            }
+            remindView.snp.makeConstraints { make in
+                make.left.equalToSuperview().offset(-4)
+                make.width.equalTo(8)
+                make.height.equalTo(22)
+                make.centerY.equalToSuperview()
+            }
+            titleLab.snp.remakeConstraints { make in
+                make.left.equalTo(audioImageView.snp.right)
+                make.right.equalToSuperview().offset(-margin)
+                make.centerY.equalToSuperview()
+            }
+        }
+    }
+
+    @objc func confirmClick() {
+        if btnClickHandle != nil {
+            btnClickHandle!(confirmBtn, bgmData)
+        }
+    }
+    
+    func startLoadingAnimation(){
+        let rotateAnimation = CABasicAnimation(keyPath: "transform.rotation")
+        rotateAnimation.toValue = Double.pi * 2
+        rotateAnimation.duration = 1
+        rotateAnimation.repeatCount = .infinity
+        playImageView.layer.add(rotateAnimation, forKey: nil)
+    }
+    
+    func stopLoadingAnimation(){
+        playImageView.layer.removeAllAnimations()
+        if (bgmData as? PQVoiceModel)?.isPlaying ?? false {
+            playImageView.kf.setImage(with: URL(fileURLWithPath: currentBundlePath()!.path(forResource: "stuckPoint_music_playing", ofType: ".gif")!))
+        }
+    }
+}

+ 89 - 0
BFStuckPointKit/Classes/View/PQStuckPointMusicTagsCell.swift

@@ -0,0 +1,89 @@
+//
+//  PQStuckPointMusicTagsCell.swift
+//  PQSpeed
+//
+//  Created by SanW on 2021/4/29.
+//  Copyright © 2021 BytesFlow. All rights reserved.
+//
+
+import UIKit
+
+class PQStuckPointMusicTagsCell: UICollectionViewCell {
+    /// 点击标签
+    var tagsDidSelectedHandle: ((_ indexPath: IndexPath, _ tagData: PQStuckPointMusicTagsModel?) -> Void)?
+
+    lazy var tagsFlowLayout: PQStuckPointMusciTagsFlowLayout = {
+        let tagsFlowLayout = PQStuckPointMusciTagsFlowLayout()
+        tagsFlowLayout.sectionInset = UIEdgeInsets.zero
+        tagsFlowLayout.minimumLineSpacing = 8
+        tagsFlowLayout.minimumInteritemSpacing = 8
+        tagsFlowLayout.scrollDirection = .horizontal
+        return tagsFlowLayout
+    }()
+
+    lazy var collectionView: UICollectionView = {
+        let collectionView = UICollectionView(frame: CGRect(x: 0, y: 0, width: frame.width, height: frame.height), collectionViewLayout: tagsFlowLayout)
+        collectionView.showsVerticalScrollIndicator = false
+        collectionView.showsHorizontalScrollIndicator = false
+        collectionView.delegate = self
+        collectionView.dataSource = self
+        collectionView.backgroundColor = UIColor.clear
+        collectionView.register(PQStuckPointMusicTagsContentCell.self, forCellWithReuseIdentifier: String(describing: PQStuckPointMusicTagsContentCell.self))
+        if #available(iOS 11.0, *) {
+            collectionView.contentInsetAdjustmentBehavior = .never
+        }
+        // 延迟scrollView上子视图的响应,所以当直接拖动UISlider时,如果此时touch时间在150ms以内,UIScrollView会认为是拖动自己,从而拦截了event,导致UISlider接收不到滑动的event
+        collectionView.delaysContentTouches = false
+        return collectionView
+    }()
+
+    @objc class func stuckPointMusicTagsCell(collectionView: UICollectionView, indexPath: IndexPath) -> PQStuckPointMusicTagsCell {
+        let cell: PQStuckPointMusicTagsCell = collectionView.dequeueReusableCell(withReuseIdentifier: String(describing: PQStuckPointMusicTagsCell.self), for: indexPath) as! PQStuckPointMusicTagsCell
+        return cell
+    }
+
+    override init(frame: CGRect) {
+        super.init(frame: frame)
+        addSubview(collectionView)
+    }
+
+    required init?(coder _: NSCoder) {
+        fatalError("init(coder:) has not been implemented")
+    }
+
+    // 所有分类数据
+    var itemData: ([PQStuckPointMusicTagsModel], ([UICollectionViewLayoutAttributes], CGFloat)) = ([PQStuckPointMusicTagsModel].init(), ([UICollectionViewLayoutAttributes].init(), CGFloat())) {
+        didSet {
+            collectionView.frame = CGRect(x: 16, y: 25, width: frame.width - 32, height: frame.height - 25)
+            tagsFlowLayout.layoutAttributesArray = itemData.1.0
+            tagsFlowLayout.maxH = itemData.1.1
+            collectionView.reloadData()
+        }
+    }
+}
+
+// MARK: - 标签cell相关代理
+
+/// 标签cell相关代理
+extension PQStuckPointMusicTagsCell: UICollectionViewDelegate, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout {
+    func collectionView(_: UICollectionView, numberOfItemsInSection _: Int) -> Int {
+        return itemData.0.count
+    }
+
+    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
+        let cell = PQStuckPointMusicTagsContentCell.stuckPointMusicTagsContentCell(collectionView: collectionView, indexPath: indexPath)
+        cell.tagData = itemData.0[indexPath.item]
+        return cell
+    }
+
+    func collectionView(_: UICollectionView, didSelectItemAt indexPath: IndexPath) {
+        itemData.0.forEach { item in
+            item.isSelected = false
+        }
+        itemData.0[indexPath.item].isSelected = true
+        collectionView.reloadData()
+        if tagsDidSelectedHandle != nil {
+            tagsDidSelectedHandle!(indexPath, itemData.0[indexPath.item])
+        }
+    }
+}

+ 62 - 0
BFStuckPointKit/Classes/View/PQStuckPointMusicTagsContentCell.swift

@@ -0,0 +1,62 @@
+//
+//  PQStuckPointMusicTagsContentCell.swift
+//  PQSpeed
+//
+//  Created by SanW on 2021/4/29.
+//  Copyright © 2021 BytesFlow. All rights reserved.
+//
+
+import UIKit
+import BFCommonKit
+
+class PQStuckPointMusicTagsContentCell: UICollectionViewCell {
+    lazy var titleLab: UILabel = {
+        let titleLab = UILabel()
+        titleLab.font = UIFont.systemFont(ofSize: 12)
+        titleLab.textColor = UIColor.hexColor(hexadecimal: "#959595")
+        titleLab.textAlignment = .center
+        titleLab.backgroundColor = BFConfig.shared.styleBackGroundColor
+        titleLab.addCorner(corner: 4)
+        return titleLab
+    }()
+
+    @objc class func stuckPointMusicTagsContentCell(collectionView: UICollectionView, indexPath: IndexPath) -> PQStuckPointMusicTagsContentCell {
+        let cell: PQStuckPointMusicTagsContentCell = collectionView.dequeueReusableCell(withReuseIdentifier: String(describing: PQStuckPointMusicTagsContentCell.self), for: indexPath) as! PQStuckPointMusicTagsContentCell
+        return cell
+    }
+
+    override init(frame: CGRect) {
+        super.init(frame: frame)
+        contentView.addSubview(titleLab)
+    }
+
+    required init?(coder _: NSCoder) {
+        fatalError("init(coder:) has not been implemented")
+    }
+
+    var tagData: PQStuckPointMusicTagsModel? {
+        didSet {
+            addData()
+            addLayout()
+        }
+    }
+
+    func addData() {
+        titleLab.text = "\(tagData?.tagName ?? "")"
+        if tagData?.isSelected ?? false {
+            titleLab.textColor = UIColor.hexColor(hexadecimal: BFConfig.shared.styleColor.rawValue)
+            titleLab.layer.borderWidth = 0.5
+            titleLab.layer.borderColor = UIColor.hexColor(hexadecimal: BFConfig.shared.styleColor.rawValue).cgColor
+        } else {
+            titleLab.textColor = UIColor.hexColor(hexadecimal: "#959595")
+            titleLab.layer.borderWidth = 0.5
+            titleLab.layer.borderColor = UIColor.hexColor(hexadecimal: "#E9E9E9").cgColor
+        }
+    }
+
+    func addLayout() {
+        titleLab.snp.makeConstraints { make in
+            make.size.equalToSuperview()
+        }
+    }
+}

+ 115 - 0
BFStuckPointKit/Classes/View/PQStuckPointSearchEmptyCell.swift

@@ -0,0 +1,115 @@
+//
+//  PQStuckPointSearchEmptyCell.swift
+//  PQSpeed
+//
+//  Created by SanW on 2021/5/7.
+//  Copyright © 2021 BytesFlow. All rights reserved.
+//
+
+import UIKit
+import BFCommonKit
+import BFUIKit
+
+class PQStuckPointSearchEmptyCell: UICollectionViewCell {
+    lazy var emptyImageView: UIImageView = {
+        let emptyImageView = UIImageView(image:UIImage.moduleImage(named: "pic_search_empty", moduleName: "BFUIKit",isAssets: false))
+        emptyImageView.backgroundColor = UIColor.clear
+        emptyImageView.contentMode = .scaleAspectFit
+        return emptyImageView
+    }()
+
+    lazy var remindLab: UILabel = {
+        let remindLab = UILabel()
+        remindLab.textColor = UIColor.hexColor(hexadecimal: "#999999")
+        remindLab.text = "没有搜索到相关音乐"
+        remindLab.font = UIFont.systemFont(ofSize: 14)
+        remindLab.numberOfLines = 1
+        remindLab.textAlignment = NSTextAlignment.center
+        return remindLab
+    }()
+
+    lazy var hotRemindLab: UILabel = {
+        let hotRemindLab = UILabel()
+        hotRemindLab.textColor = UIColor.hexColor(hexadecimal: "#999999")
+        hotRemindLab.text = "试试热门音乐"
+        hotRemindLab.font = UIFont.systemFont(ofSize: 15)
+        hotRemindLab.numberOfLines = 1
+        hotRemindLab.textAlignment = NSTextAlignment.center
+        return hotRemindLab
+    }()
+
+    lazy var leftLineView: UIView = {
+        let leftLineView = UIView()
+        leftLineView.backgroundColor = UIColor.hexColor(hexadecimal: "#515151")
+        return leftLineView
+    }()
+
+    lazy var rightLineView: UIView = {
+        let rightLineView = UIView()
+        rightLineView.backgroundColor = UIColor.hexColor(hexadecimal: "#515151")
+        return rightLineView
+    }()
+
+    @objc class func stuckPointSearchEmptyCell(collectionView: UICollectionView, indexPath: IndexPath) -> PQStuckPointSearchEmptyCell {
+        let cell: PQStuckPointSearchEmptyCell = collectionView.dequeueReusableCell(withReuseIdentifier: String(describing: PQStuckPointSearchEmptyCell.self), for: indexPath) as! PQStuckPointSearchEmptyCell
+        return cell
+    }
+
+    override init(frame: CGRect) {
+        super.init(frame: frame)
+        contentView.addSubview(emptyImageView)
+        contentView.addSubview(remindLab)
+        contentView.addSubview(leftLineView)
+        contentView.addSubview(hotRemindLab)
+        contentView.addSubview(rightLineView)
+    }
+
+    required init?(coder _: NSCoder) {
+        fatalError("init(coder:) has not been implemented")
+    }
+
+    var bgmData: BFEmptyModel? {
+        didSet {
+            addData()
+            addLayout()
+        }
+    }
+
+    func addData() {}
+
+    func addLayout() {
+        let margin: CGFloat = 24
+        let leftMargin: CGFloat = 16
+        let imageW: CGFloat = 109
+        let imageH: CGFloat = 69
+        emptyImageView.snp.makeConstraints { make in
+            make.width.equalTo(imageW)
+            make.height.equalTo(imageH)
+            make.centerX.equalToSuperview()
+            make.top.equalToSuperview().offset(imageH)
+        }
+        remindLab.snp.makeConstraints { make in
+            make.top.equalTo(emptyImageView.snp.bottom).offset(margin)
+            make.centerX.equalToSuperview()
+        }
+        hotRemindLab.snp.makeConstraints { make in
+            make.centerX.equalToSuperview()
+            make.width.equalTo(imageW)
+            make.bottom.equalToSuperview().offset(-margin)
+        }
+        leftLineView.snp.makeConstraints { make in
+            make.left.equalToSuperview().offset(leftMargin)
+            make.height.equalTo(1)
+            make.centerY.equalTo(hotRemindLab)
+            make.right.equalTo(hotRemindLab.snp.left).offset(-leftMargin)
+        }
+        rightLineView.snp.makeConstraints { make in
+            make.right.equalToSuperview().offset(-leftMargin)
+            make.height.equalTo(1)
+            make.centerY.equalTo(hotRemindLab)
+            make.left.equalTo(hotRemindLab.snp.right).offset(leftMargin)
+        }
+    }
+
+    @objc func btnClick(sender _: UIButton) {}
+}

+ 300 - 0
BFStuckPointKit/Classes/View/PQVideoCutingOprateView.swift

@@ -0,0 +1,300 @@
+//
+//  PQVideoCutingOprateView.swift
+//  PQSpeed
+//
+//  Created by SanW on 2021/5/9.
+//  Copyright © 2021 BytesFlow. All rights reserved.
+//
+
+import UIKit
+import BFCommonKit
+
+class PQVideoCutingOprateView: UIView {
+    // 距离左边间隔
+    var leftMargin: CGFloat = 0
+    // 距离右边间隔
+    var rightMargin: CGFloat = 0
+    // 距离上边间隔
+    var topMargin: CGFloat = 14
+    // 距离下边间隔
+    var bottomMargin: CGFloat = 20
+    /// 上下线条的高度
+    var lineHeight: CGFloat = 3.0
+    /// 进度宽度
+    var progressWidth: CGFloat = 12.0
+    // 左右裁剪操作宽度
+    var cutingOprateWidth: CGFloat = 15
+    // 开始时间 默认 0
+    private var cutStartTime: CGFloat = 0
+    // 结束时间
+    private var cutEndTime: CGFloat = 0
+    // 视频总时长
+    private var totalDuration: CGFloat = 0
+    // 最小裁剪大小 默认 10s
+    private var cutMinDuration: CGFloat = 10
+    // 最大裁剪大小 默认 40s
+    private var cutMaxDuration: CGFloat = 40
+    /// 每秒宽度
+    private var perSecondWidth: CGFloat = 0
+    /// 裁剪总时长
+    private var cutTotalTime: CGFloat = 0
+    /// 当前滑动的位置
+    private var preOriginX: CGFloat = 0
+    /// 当前滑动的view
+    private var currentPanView: UIView?
+    private var progress: CGFloat {
+        if progressView.frame.minX >= (rightOprateView.frame.minX - (((progressView.frame.width - 3) / 2) + 3)) {
+            return 1
+        } else {
+            return ((progressView.frame.minX - (leftOprateView.frame.maxX - ((progressView.frame.width - 3) / 2))) / perSecondWidth) / (cutEndTime - cutStartTime)
+        }
+    }
+
+    /// 拖缀结束的回调 type - 1-拖动左边裁剪结束 2--拖动右边裁剪结束 3-进度条拖动结束 4-滑动结束
+    var didEndDragging: ((_ type: Int, _ startTime: CGFloat, _ endTime: CGFloat, _ progress: CGFloat) -> Void)?
+    /// 裁剪实时回调
+    var cutRangeDidChanged: ((_ startTime: CGFloat, _ endTime: CGFloat, _ cutTotalTime: CGFloat) -> Void)?
+    /// 进度回调
+    var progressDidChanged: ((_ progress: CGFloat) -> Void)?
+
+    lazy var durationLabel: UILabel = {
+        let durationLabel = UILabel()
+        durationLabel.font = UIFont.systemFont(ofSize: 16, weight: .medium)
+        durationLabel.backgroundColor = BFConfig.shared.cutDurationColor
+        durationLabel.textColor = UIColor.white
+        durationLabel.textAlignment = .center
+        durationLabel.addShadow()
+        return durationLabel
+    }()
+
+    lazy var leftOprateView: UIImageView = {
+        let leftOprateView = UIImageView(image:UIImage.moduleImage(named: "videomk_crop_left", moduleName: "BFFramework",isAssets: false)?.withRenderingMode(.alwaysTemplate))
+        leftOprateView.tintColor = BFConfig.shared.cutViewTintColor
+        leftOprateView.contentMode = .scaleAspectFill
+        leftOprateView.isUserInteractionEnabled = true
+        leftOprateView.backgroundColor = BFConfig.shared.cutViewStyleColor
+        let panGes = UIPanGestureRecognizer(target: self, action: #selector(panGesture(gesture:)))
+        panGes.maximumNumberOfTouches = 1
+        panGes.minimumNumberOfTouches = 1
+        leftOprateView.addGestureRecognizer(panGes)
+        return leftOprateView
+    }()
+
+    lazy var rightOprateView: UIImageView = {
+        let rightOprateView = UIImageView(image:UIImage.moduleImage(named: "videomk_crop_right", moduleName: "BFFramework",isAssets: false)?.withRenderingMode(.alwaysTemplate))
+        rightOprateView.tintColor = BFConfig.shared.cutViewTintColor
+        rightOprateView.contentMode = .scaleAspectFill
+        rightOprateView.isUserInteractionEnabled = true
+        rightOprateView.backgroundColor = BFConfig.shared.cutViewStyleColor
+        let panGes = UIPanGestureRecognizer(target: self, action: #selector(panGesture(gesture:)))
+        panGes.maximumNumberOfTouches = 1
+        panGes.minimumNumberOfTouches = 1
+        rightOprateView.addGestureRecognizer(panGes)
+        return rightOprateView
+    }()
+
+    lazy var topLineView: UIImageView = {
+        let topLineView = UIImageView()
+        topLineView.backgroundColor = BFConfig.shared.cutViewStyleColor
+        return topLineView
+    }()
+
+    lazy var bottomLineView: UIImageView = {
+        let bottomLineView = UIImageView()
+        bottomLineView.backgroundColor = BFConfig.shared.cutViewStyleColor
+        return bottomLineView
+    }()
+
+    lazy var progressView: PQCuttingPointView = {
+        let progressView = PQCuttingPointView(frame: CGRect(x: 0, y: 0, width: progressWidth, height: frame.height))
+        let panGes = UIPanGestureRecognizer(target: self, action: #selector(panGesture(gesture:)))
+        panGes.maximumNumberOfTouches = 1
+        panGes.minimumNumberOfTouches = 1
+        progressView.addGestureRecognizer(panGes)
+        return progressView
+    }()
+
+    override private init(frame: CGRect) {
+        super.init(frame: frame)
+    }
+
+    required init?(coder _: NSCoder) {
+        fatalError("init(coder:) has not been implemented")
+    }
+
+    init(frame: CGRect, duration: CGFloat, startTime: CGFloat, endTime: CGFloat, minDuration: CGFloat, maxDuration: CGFloat) {
+        super.init(frame: frame)
+        isUserInteractionEnabled = true
+        clipsToBounds = true
+        backgroundColor = UIColor.clear
+        totalDuration = duration
+        cutStartTime = startTime
+        cutEndTime = endTime
+        cutMinDuration = minDuration
+        cutMaxDuration = maxDuration
+        if cutMaxDuration <= 0 || cutMaxDuration > totalDuration {
+            cutMaxDuration = totalDuration
+        }
+        if cutMinDuration <= 0 || (cutMinDuration > totalDuration) {
+            cutMinDuration = 7
+        }
+        if cutEndTime <= 0 {
+            cutEndTime = cutStartTime + cutMinDuration
+        }
+        perSecondWidth = 7
+        cutTotalTime = cutEndTime - cutStartTime
+        configSubview()
+        BFLog(message: "======\(frame),perSecondWidth = \(perSecondWidth),cutMaxDuration = \(cutMaxDuration * perSecondWidth)")
+    }
+
+    /// 初始化视图
+    /// - Returns: <#description#>
+    func configSubview() {
+        if leftOprateView.superview == nil {
+            addSubview(leftOprateView)
+        }
+        if rightOprateView.superview == nil {
+            addSubview(rightOprateView)
+        }
+        if topLineView.superview == nil {
+            addSubview(topLineView)
+        }
+        if bottomLineView.superview == nil {
+            addSubview(bottomLineView)
+        }
+        if durationLabel.superview == nil {
+            addSubview(durationLabel)
+        }
+        if progressView.superview == nil {
+            addSubview(progressView)
+        }
+        leftOprateView.frame = CGRect(x: leftMargin + perSecondWidth * cutStartTime, y: topMargin, width: cutingOprateWidth, height: frame.height - topMargin - bottomMargin)
+        rightOprateView.frame = CGRect(x: leftOprateView.frame.maxX + perSecondWidth * (cutEndTime - cutStartTime), y: leftOprateView.frame.minY, width: cutingOprateWidth, height: frame.height - topMargin - bottomMargin)
+        // 更新子视图布局
+        updateSubViewFrame()
+    }
+
+    /// 更新子视图布局
+    /// - Returns: <#description#>
+    func updateSubViewFrame() {
+        topLineView.frame = CGRect(x: leftOprateView.frame.maxX, y: leftOprateView.frame.minY, width: rightOprateView.frame.minX - leftOprateView.frame.maxX, height: lineHeight)
+        bottomLineView.frame = CGRect(x: topLineView.frame.minX, y: leftOprateView.frame.maxY - lineHeight, width: topLineView.frame.width, height: lineHeight)
+        progressView.frame = CGRect(x: leftOprateView.frame.maxX - ((progressView.frame.width - 3) / 2), y: 0, width: progressWidth, height: frame.height)
+        durationLabel.frame = CGRect(x: topLineView.frame.minX, y: topLineView.frame.maxY, width: topLineView.frame.width, height: bottomLineView.frame.minY - topLineView.frame.maxY)
+        durationLabel.text = "\(lround(Double(cutTotalTime)))s"
+    }
+
+    /// 更新进度
+    /// progress <#progress description#>
+    func updateProgress(progress: CGFloat) {
+        if currentPanView != nil {
+            return
+        }
+        let width = rightOprateView.frame.minX - leftOprateView.frame.maxX
+        var newX = leftOprateView.frame.maxX - ((progressView.frame.width - 3) / 2) + progress * width
+        BFLog(message: "progress = \(progress),newX = \(newX)")
+        if newX.isNaN || newX <= (leftOprateView.frame.maxX - ((progressView.frame.width - 3) / 2)) {
+            newX = (leftOprateView.frame.maxX - ((progressView.frame.width - 3) / 2))
+        }
+        if newX >= (rightOprateView.frame.minX - (((progressView.frame.width - 3) / 2) + 3)) {
+            newX = (rightOprateView.frame.minX - (((progressView.frame.width - 3) / 2) + 3))
+        }
+        progressView.frame.origin.x = newX
+    }
+
+    deinit {
+        BFLog(message: "卡点裁剪-裁剪时长视图销毁")
+    }
+}
+
+extension PQVideoCutingOprateView {
+    /// 操作手势
+    /// - Parameter ges: <#ges description#>
+    /// - Returns: <#description#>
+    @objc func panGesture(gesture: UIPanGestureRecognizer) {
+        switch gesture.state {
+        case .began:
+            preOriginX = 0
+            currentPanView = gesture.view
+        case .changed:
+            if currentPanView == leftOprateView || currentPanView == rightOprateView || currentPanView == progressView {
+                let point = gesture.translation(in: superview)
+                var offsetX = point.x - preOriginX
+                preOriginX = point.x
+                if currentPanView == leftOprateView {
+                    var oprateFrame = leftOprateView.frame
+                    oprateFrame.origin.x = oprateFrame.origin.x + offsetX
+                    if oprateFrame.origin.x <= leftMargin {
+                        offsetX += leftMargin - oprateFrame.origin.x
+                        oprateFrame.origin.x = leftMargin
+                    }
+                    let minLength = rightOprateView.frame.minX - cutingOprateWidth - cutMinDuration * perSecondWidth
+                    if oprateFrame.origin.x >= minLength {
+                        offsetX -= oprateFrame.origin.x - minLength
+                        oprateFrame.origin.x = minLength
+                    }
+                    let maxLength = rightOprateView.frame.minX - cutingOprateWidth - cutMaxDuration * perSecondWidth
+                    if oprateFrame.origin.x <= maxLength {
+                        offsetX += maxLength - oprateFrame.origin.x
+                        oprateFrame.origin.x = maxLength
+                    }
+                    let time = offsetX / perSecondWidth
+                    cutStartTime = cutStartTime + time
+                    leftOprateView.frame = oprateFrame
+                } else if currentPanView == rightOprateView {
+                    var oprateFrame = rightOprateView.frame
+                    oprateFrame.origin.x += offsetX
+                    var rightImageMaxX = leftOprateView.frame.maxX + cutMaxDuration * perSecondWidth
+                    if rightImageMaxX > frame.width - rightMargin - cutingOprateWidth {
+                        rightImageMaxX = frame.width - rightMargin - cutingOprateWidth
+                    }
+                    if oprateFrame.origin.x >= rightImageMaxX {
+                        offsetX -= oprateFrame.origin.x - rightImageMaxX
+                        oprateFrame.origin.x = rightImageMaxX
+                    }
+                    let rightImageMinX = leftOprateView.frame.maxX + cutMinDuration * perSecondWidth
+                    if oprateFrame.origin.x <= rightImageMinX {
+                        offsetX += rightImageMinX - oprateFrame.origin.x
+                        oprateFrame.origin.x = rightImageMinX
+                    }
+                    let time = offsetX / perSecondWidth
+                    cutEndTime = cutEndTime + time
+                    rightOprateView.frame = oprateFrame
+                } else if currentPanView == progressView {
+                    var progressFrame = progressView.frame
+                    BFLog(message: "progressFrame = \(progressFrame),offsetX = \(offsetX)")
+                    progressFrame.origin.x += offsetX
+                    if progressFrame.origin.x <= (leftOprateView.frame.maxX - ((progressView.frame.width - 3) / 2)) {
+                        progressFrame.origin.x = (leftOprateView.frame.maxX - ((progressView.frame.width - 3) / 2))
+                    }
+                    if progressFrame.origin.x >= (rightOprateView.frame.minX - (((progressView.frame.width - 3) / 2) + 3)) {
+                        progressFrame.origin.x = (rightOprateView.frame.minX - (((progressView.frame.width - 3) / 2) + 3))
+                    }
+                    progressView.frame = progressFrame
+                    BFLog(message: "======\(progressView.frame.minX - (leftOprateView.frame.maxX - ((progressView.frame.width - 3) / 2)))")
+                    if progressDidChanged != nil {
+                        progressDidChanged!(progress)
+                    }
+                }
+                if currentPanView != progressView {
+                    cutTotalTime = cutEndTime - cutStartTime
+                    // 更新子视图布局
+                    updateSubViewFrame()
+                    if cutRangeDidChanged != nil {
+                        BFLog(message: "cutStartTime = \(cutStartTime),cutEndTime = \(cutEndTime)")
+                        cutRangeDidChanged!(cutStartTime, cutEndTime, cutTotalTime)
+                    }
+                }
+            }
+        case .ended:
+            if currentPanView == leftOprateView || currentPanView == rightOprateView || currentPanView == progressView {
+                if didEndDragging != nil {
+                    didEndDragging!(currentPanView == leftOprateView ? 1 : (currentPanView == rightOprateView ? 2 : 3), cutStartTime, cutEndTime, progress)
+                }
+            }
+            currentPanView = nil
+        default:
+            break
+        }
+    }
+}

+ 802 - 0
BFStuckPointKit/Classes/ViewModel/PQGPUImagePlayerView.swift

@@ -0,0 +1,802 @@
+//
+//  PQGPUImagePlayer.swift
+//  GPUImage_iOS
+//
+//  Created by ak on 2020/8/27.
+//  Copyright © 2020 Sunset Lake Software LLC. All rights reserved.
+//  功能:滤镜播放器 支持音频 https://juejin.im/post/6844904024760664078 这个有用
+
+import AVFoundation
+import AVKit
+import UIKit
+import ObjectMapper
+import BFCommonKit
+
+// import GPUImage
+struct AVAssetKey {
+    static let tracks = "tracks"
+    static let duration = "duration"
+    static let metadata = "commonMetadata"
+}
+
+// 播放器状态
+public enum PQGPUImagePlayerViewStatus: Int {
+    case playing = 10
+    case pause = 20
+    case stop = 30
+    case error = 0
+    case unknow = -1000
+}
+
+public class PQGPUImagePlayerView: UIView {
+     
+    public private(set) var playbackTime: TimeInterval = 0 {
+        willSet {
+            playbackTimeChangeClosure?(newValue)
+        }
+    }
+
+    public var mCanverSize: CGSize = .zero
+
+    // 自动隐藏边框
+    public var isAutoHiden: Bool = false
+
+    // 是否显示边框
+    public  var isShowLine: Bool = true
+
+    // 播放进度
+    public var playbackTimeChangeClosure: ((_ time: TimeInterval) -> Void)?
+    // 参数说明:1,当前时间 2,总时长 3,进度
+    public var progress: ((Double, Double, Double) -> Void)?
+
+    /// 预览区域点击回调
+    public  var renderViewOnClickHandle: (() -> Void)?
+
+    public private(set) var asset: AVAsset?
+
+    public var duration: TimeInterval {
+        return asset?.duration.seconds ?? 0
+    }
+
+    public private(set) var status: PQGPUImagePlayerViewStatus = .unknow {
+        willSet {
+            statusChangeClosure?(newValue)
+        }
+    }
+
+    public var statusChangeClosure: ((_ status: PQGPUImagePlayerViewStatus) -> Void)?
+
+    public private(set) var isReadyToPlay = false {
+        willSet {
+            assetLoadClosure?(newValue)
+        }
+    }
+
+    public var assetLoadClosure: ((_ isReadyToPlay: Bool) -> Void)?
+
+    /// Called when video finished
+    /// This closure will not called if isLoop is true
+    public var finishedClosure: (() -> Void)?
+
+    /// Set this attribute to true will print debug info
+    public var enableDebug = false {
+        willSet {
+            movie?.runBenchmark = newValue
+        }
+    }
+
+    /// Setting this attribute before the end of the video works
+    public var isLoop = false {
+        willSet {
+            movie?.loop = newValue
+        }
+    }
+
+    /// The player will control the animationLayer of animation with the property `timeOffset`
+    /// You can set up some animations in this layer like caption
+    public var animationLayer: CALayer? {
+        willSet {
+            // Set speed to 0, use timeOffset to control the animation
+            newValue?.speed = 0
+
+            newValue?.timeOffset = playbackTime
+        }
+        didSet {
+            oldValue?.removeFromSuperlayer()
+        }
+    }
+
+    /// Add filters to this array and call updateAsset(_:) method
+    public var filters: [ImageProcessingOperation] = []
+
+    public var movie: PQMovieInput?
+
+    public var speaker: SpeakerOutput?
+
+    /// Volumn of original sounds in AVAsset
+    public var originVolumn: Float = 1.0 {
+        didSet {}
+    }
+
+    public var playerLayer: AVPlayerLayer?
+    public var player: AVPlayer?
+
+    public var playerEmptyView: UIImageView!
+
+    public var borderLayer: CAShapeLayer?
+
+    public var mPlayeTimeRange: CMTimeRange?
+
+    var mStickers: [PQEditVisionTrackMaterialsModel]? {
+        didSet {
+            
+            BFLog(2, message: "设置线程为: \(Thread.current) \(OperationQueue.current?.underlyingQueue?.label as Any)")
+        
+            configCache(beginTime: mStickers?.first?.timelineIn ?? 0)
+        }
+    }
+ 
+    // 是否显示时间条
+    var showProgressLab: Bool = true
+
+    // 缓存创建filter 防止 seek 100ms 慢
+    @Atomic var cacheFilters: Array<PQBaseFilter> = Array()
+    // 缓存个数 XXXX 经过测试如果是4K 视频解码器不能创建太多,4是可以工作
+    var cacheFiltersMaxCount: Int = 8
+  
+    /// Use serial queue to ensure that the picture is smooth
+    var createFiltersQueue: DispatchQueue!
+    
+    //是否显示高斯
+    public  var showGaussianBlur:Bool = false
+    
+    //是否使用AVPlayer播放音乐
+    public var isUsedAVPlayer:Bool = false
+
+    // 渲染区view
+    private lazy var renderView: RenderView = {
+        let view = RenderView()
+        view.backgroundColor = BFConfig.shared.styleBackGroundColor
+        view.frame = self.bounds
+        view.delegate = self
+        let tap = UITapGestureRecognizer(target: self, action: #selector(RenderViewOnclick))
+        view.addGestureRecognizer(tap)
+ 
+        view.backgroundRenderColor =  Color.init(red: Float(BFConfig.shared.styleBackGroundColor.rgbaf[0]), green: Float(BFConfig.shared.styleBackGroundColor.rgbaf[1]), blue: Float(BFConfig.shared.styleBackGroundColor.rgbaf[2]))
+
+        return view
+    }()
+
+    // 暂停播放view
+    lazy var playView: UIImageView = {
+        let view = UIImageView(frame: CGRect(x: (self.frame.size.width - self.frame.size.height / 3.6) / 2, y: (self.frame.size.height - self.frame.size.height / 3.6) / 2, width: self.frame.size.height / 3.6, height: self.frame.size.height / 3.6))
+//        view.tintColor = UIColor.white
+        view.image = UIImage.moduleImage(named: "gpuplayBtn", moduleName: "BFFramework",isAssets: false)?.withRenderingMode(.alwaysTemplate)
+        view.tintColor = UIColor.hexColor(hexadecimal: BFConfig.shared.styleColor.rawValue)
+        view.isHidden = true
+        return view
+
+    }()
+    
+    // 暂停播放view
+    lazy var playMaskView: UIView = {
+        let playMaskView = UIView.init()
+        playMaskView.backgroundColor = UIColor.init(red: 0, green: 0, blue: 0, alpha: 0.5)
+        playMaskView.isUserInteractionEnabled = false
+        playMaskView.isHidden = true
+        return playMaskView
+
+    }()
+
+    // 播放进度/总时长
+    lazy var progressLab: UILabel = {
+        let titleLab = UILabel(frame: CGRect(x: (self.frame.size.width - 140) / 2, y: 0, width: 140, height: 12))
+        titleLab.font = UIFont.systemFont(ofSize: 12, weight: .medium)
+        titleLab.textColor = UIColor.white
+        titleLab.textAlignment = .center
+        titleLab.text = ""
+        titleLab.layer.shadowColor = UIColor.black.cgColor
+        titleLab.layer.shadowOpacity = 0.3
+        titleLab.layer.shadowOffset = .zero
+        titleLab.layer.shadowRadius = 1
+//        titleLab.backgroundColor = UIColor.hexColor(hexadecimal: "#FFFFFF",alpha: 0.3)
+//        titleLab.addCorner(corner:7)
+
+        return titleLab
+
+    }()
+
+    lazy var tipLab: UILabel = {
+        let tipLab = UILabel(frame: CGRect(x: (self.frame.size.width - 100) / 2, y: (self.frame.size.height - 14) / 2, width: 100, height: 14))
+        tipLab.font = UIFont.systemFont(ofSize: 14, weight: .medium)
+        tipLab.textColor = UIColor.white
+        tipLab.textAlignment = .center
+        tipLab.text = "资源加载中..."
+        tipLab.layer.shadowColor = UIColor.white.cgColor
+        tipLab.layer.shadowOpacity = 0.5
+        tipLab.layer.shadowOffset = .zero
+        tipLab.layer.shadowRadius = 1
+        tipLab.isHidden = true
+        return tipLab
+
+    }()
+
+    //进度的开始时间
+    var showProgressStartTime:Float = 0.0
+
+    required public init?(coder _: NSCoder) {
+        fatalError("init(coder:) has not been implemented")
+    }
+    
+    override public init(frame: CGRect) {
+        super.init(frame: frame)
+
+        
+        addSubview(renderView)
+        addSubview(progressLab)
+        addSubview(playMaskView)
+        addSubview(playView)
+     
+        backgroundColor = BFConfig.shared.styleBackGroundColor
+        playerEmptyView = UIImageView(frame: bounds)
+        playerEmptyView.backgroundColor = .black
+        playerEmptyView.image = UIImage.moduleImage(named: "playEmpty", moduleName: "BFFramework",isAssets: false)
+        playerEmptyView.contentMode = .center
+        addSubview(playerEmptyView)
+
+        addSubview(tipLab)
+        
+        if #available(iOS 10.0, *) {
+            createFiltersQueue = DispatchQueue(label: "PQ.moveFiler.seeking111", qos: .default, attributes: .initiallyInactive, autoreleaseFrequency: .never, target: nil)
+        } else {
+            createFiltersQueue = DispatchQueue(label: "PQ.moveFiler.seeking111", qos: .userInteractive, attributes: [], autoreleaseFrequency: .inherit, target: nil)
+        }
+        if #available(iOS 10.0, *) {
+            createFiltersQueue.activate()
+        }
+    }
+
+    func showBorderLayer() {
+        if borderLayer != nil {
+            borderLayer?.removeFromSuperlayer()
+        }
+        // 线条颜色
+        borderLayer = CAShapeLayer()
+        borderLayer?.strokeColor = UIColor.hexColor(hexadecimal: "#FFFFFF").cgColor
+        borderLayer?.fillColor = nil
+        borderLayer?.path = UIBezierPath(rect: CGRect(x: 1, y: 1, width: bounds.width - 2, height: bounds.height - 2)).cgPath
+        borderLayer?.frame = bounds
+        borderLayer?.lineWidth = 2.0
+        borderLayer?.lineCap = .round
+        // 第一位是 线条长度   第二位是间距 nil时为实线
+        if borderLayer != nil {
+            renderView.layer.addSublayer(borderLayer!)
+        }
+        
+
+        if isAutoHiden {
+            borderLayer?.opacity = 0
+            let groupAnimation = CAAnimationGroup()
+            groupAnimation.beginTime = CACurrentMediaTime()
+            groupAnimation.duration = 1
+            groupAnimation.fillMode = .forwards
+            groupAnimation.isRemovedOnCompletion = true
+            groupAnimation.repeatCount = 3
+
+            let opacity = CABasicAnimation(keyPath: "opacity")
+            opacity.fromValue = 0
+            opacity.toValue = 1
+            opacity.isRemovedOnCompletion = true
+
+            let opacity2 = CABasicAnimation(keyPath: "opacity")
+            opacity2.fromValue = 1
+            opacity2.toValue = 0
+            opacity2.isRemovedOnCompletion = false
+            groupAnimation.animations = [opacity, opacity2]
+
+            borderLayer?.add(groupAnimation, forKey: nil)
+        }
+    }
+
+    // 设置画布比例
+    public func resetCanvasFrame(frame: CGRect) {
+        if self.frame.equalTo(frame) {
+            BFLog(2, message: "新老值一样,不重置")
+            return
+        }
+
+        self.frame = frame
+        
+        mCanverSize = frame.size
+
+        if isShowLine {
+            showBorderLayer()
+        }
+
+        BFLog(2, message: "new frame is \(frame)")
+        renderView.isHidden = true
+        renderView.frame = CGRect(x: 0, y: 0, width: self.frame.size.width, height: self.frame.size.height)
+        renderView.resatSize()
+
+        playerEmptyView.frame = CGRect(x: 0, y: 0, width: self.frame.size.width, height: self.frame.size.height)
+        playMaskView.frame = CGRect.init(x: 0, y: 0, width: self.frame.width, height: self.frame.height)
+        tipLab.frame = CGRect(x: (self.frame.size.width - 100) / 2, y: (self.frame.size.height - 14) / 2, width: 100, height: 14)
+        progressLab.frame = CGRect(x: (self.frame.size.width - 140) / 2, y: 8, width: 140, height: 14)
+
+        let bord = frame.size.width > frame.size.height ? CGFloat(60) : CGFloat(60)
+        playView.frame = CGRect(x: (CGFloat(frame.size.width) - bord) / 2 , y: (CGFloat(frame.size.height) - bord) / 2, width: bord, height: bord)
+    
+//        playView.frame = CGRect(x: (self.frame.size.width - self.frame.size.height / 3.6) / 2, y: (self.frame.size.height - self.frame.size.height / 3.6) / 2, width: self.frame.size.height / 3.6, height: self.frame.size.height / 3.6)
+    }
+
+    override public func layoutSubviews() {
+        super.layoutSubviews()
+    }
+
+    @objc func RenderViewOnclick() {
+        if status == .playing {
+            playView.isHidden = false
+            playMaskView.isHidden = false
+            pause()
+
+        } else if status == .stop || status == .pause {
+            playView.isHidden = true
+            playMaskView.isHidden = true
+            movie?.resume()
+            speaker?.start()
+            status = .playing
+        }
+        if renderViewOnClickHandle != nil {
+            renderViewOnClickHandle!()
+        }
+    }
+
+    func showPlayBtn(isHidden: Bool) {
+        playView.isHidden = isHidden
+        playMaskView.isHidden = isHidden
+    }
+
+    deinit {
+        stop()
+        movie = nil
+        speaker = nil
+        BFLog(1, message: "play view release")
+    }
+
+    /// XXXX 这里的 URL 使用的是全路径 ,如果不是全的会 crash ,方便复用 (不用处理业务的文件放在哪里)
+    public func updateAsset(_ url: URL, videoComposition: AVVideoComposition? = nil, audioMixModel: PQVoiceModel? = nil, videoStickers: [PQEditVisionTrackMaterialsModel]? = nil,originMusicDuration:Float = 0,lastPoint:Float = 0,clipAudioRange: CMTimeRange = CMTimeRange.zero ,isUsedAVPlayer:Bool = false) {
+        self.isUsedAVPlayer = isUsedAVPlayer
+        // 每次初始化的时候设置初始值 为 nIl
+        var audioMix: AVMutableAudioMix?
+        var composition: AVMutableComposition?
+
+        let asset = AVURLAsset(url: url, options: nil)
+        BFLog(1, message:  "播放器初始化的音频时长\(asset.duration.seconds)  url is \(url),最终使用时长\(originMusicDuration),裁剪范围\(CMTimeGetSeconds(clipAudioRange.start)) 到 \(CMTimeGetSeconds(clipAudioRange.end))")
+
+        self.asset = asset
+        if (audioMixModel != nil && audioMixModel?.localPath != nil) || (videoStickers != nil && (videoStickers?.count ?? 0) > 0 || originMusicDuration != 0) {
+            BFLog(2, message: "有参加混音的数据。")
+            (audioMix, composition) = PQPlayerViewModel.setupAudioMix(originAsset: asset, bgmData: audioMixModel, videoStickers: videoStickers,originMusicDuration:originMusicDuration,clipAudioRange: clipAudioRange)
+        } else {
+            audioMix = nil
+        }
+
+        isReadyToPlay = false
+        asset.loadValuesAsynchronously(forKeys: ["tracks", "duration", "commonMetadata"]) { [weak self] in
+            guard let strongSelf = self else { return }
+            let tracksStatus = strongSelf.asset?.statusOfValue(forKey: AVAssetKey.tracks, error: nil) ?? .unknown
+            let durationStatus = strongSelf.asset?.statusOfValue(forKey: AVAssetKey.duration, error: nil) ?? .unknown
+            strongSelf.isReadyToPlay = tracksStatus == .loaded && durationStatus == .loaded
+        }
+        var audioSettings: [String: Any] = [
+            AVFormatIDKey: kAudioFormatLinearPCM,
+        ]
+//        if #available(iOS 14.0, *) {
+            audioSettings[AVLinearPCMIsFloatKey] = false
+            audioSettings[AVLinearPCMBitDepthKey] = 16
+//        }
+        do {
+            if composition != nil {
+                BFLog(2, message: "composition 方式初始化")
+                movie = try PQMovieInput(asset: composition!, videoComposition: videoComposition, audioMix: audioMix, playAtActualSpeed: true, loop: isLoop, audioSettings: audioSettings)
+//                movie?.exportAudioUrl = url // clipAudioRange
+                var ranges = Array<CMTimeRange>()
+                if CMTimeGetSeconds(clipAudioRange.duration) ==  0 {
+                    let range = CMTimeRange(start: CMTime.zero, duration: asset.duration)
+                    ranges.append(range)
+                }else{
+                    ranges.append(clipAudioRange)
+                }
+                movie?.configAVPlayer(assetUrl: url, ranges: ranges)
+            } else {
+                movie = try PQMovieInput(url: url, playAtActualSpeed: true, loop: isLoop, audioSettings: audioSettings)
+
+                /* 测试代码
+                 let audioDecodeSettings = [AVFormatIDKey:kAudioFormatLinearPCM]
+                 let bundleURL = Bundle.main.resourceURL!
+                 let movieURL = URL(string:"11111.mp4", relativeTo:bundleURL)!
+                 movie = try MovieInput(url:movieURL, playAtActualSpeed:true, loop:true, audioSettings:audioDecodeSettings)
+                 */
+            }
+
+            movie!.runBenchmark = false
+            movie!.synchronizedEncodingDebug = false
+            
+            movie!.isUsedAVPlayer = isUsedAVPlayer
+
+        } catch {
+            status = .error
+            if enableDebug {
+                debugPrint(error)
+            }
+        }
+        guard let movie = movie else { return }
+        movie.progress = { [weak self] currTime, duration, prgressValue in
+            guard let strongSelf = self else { return }
+
+//            BFLog(1, message: " movie 进度\(currTime)")
+            strongSelf.changeFilter(currTime: currTime)
+            strongSelf.progress?(currTime, duration, prgressValue)
+
+            DispatchQueue.main.async {
+                strongSelf.playbackTime = currTime
+
+                // Non-main thread change this property is not valid
+                strongSelf.animationLayer?.timeOffset = strongSelf.playbackTime
+                if strongSelf.showProgressLab {
+
+                    if(strongSelf.showProgressStartTime == 0 ){
+                        strongSelf.showProgressStartTime = Float(CMTimeGetSeconds(strongSelf.movie?.startTime ?? .zero))
+                    }
+                    if duration < 1 {
+
+                        strongSelf.progressLab.text =  "\((currTime - Double(CMTimeGetSeconds(strongSelf.movie?.startTime ?? .zero))).formatDurationToHMS()) / 00:01"
+                    } else {
+
+                        var showTime = currTime -  Double(strongSelf.showProgressStartTime)
+                        if (showTime < 0){
+                            showTime = 0
+                        }
+                        strongSelf.progressLab.text = "\(showTime.formatDurationToHMS()) / \( (duration - Double(strongSelf.showProgressStartTime)).formatDurationToHMS())"
+                    }
+                }
+            }
+        }
+        movie.completion = { [weak self] in
+            guard let strongSelf = self else { return }
+            //缓存已经用完,重新初始化缓存
+            if(strongSelf.filters.count == 0){
+                strongSelf.configCache(beginTime: strongSelf.mStickers?.first?.timelineIn ?? 0)
+            }
+            
+            DispatchQueue.main.async {
+                strongSelf.status = .stop
+                strongSelf.finishedClosure?()
+                strongSelf.showPlayBtn(isHidden: false)
+                if(strongSelf.progress != nil){
+                    strongSelf.progress!(0,0,1)
+                }
+                
+            }
+        }
+        speaker = SpeakerOutput()
+        movie.audioEncodingTarget = speaker
+
+        applyFilters()
+    }
+
+    /// 初始化缓存,默认选创建 cacheFiltersMaxCount 个缓存 filterrs
+    /// - Parameter beginTime: 开始缓存的开始时间,用在 seek操作时 老的缓存已经无效不能在使用了
+    func configCache(beginTime: Float64 ) {
+        cacheFilters.removeAll()
+        BFLog(2, message: "原素材 总数:\(mStickers?.count ?? 0) ")
+       
+        if mStickers?.count ?? 0 > 0 {
+            for (index, currentSticker) in mStickers!.enumerated() {
+                BFLog(message: "mStickers timelinein:\(currentSticker.timelineIn) timelineout: \(currentSticker.timelineOut) index : \(index)")
+               //到达最大缓存数退出
+                if cacheFilters.count == cacheFiltersMaxCount {
+                    break
+                }
+                //小于缓存的开始时间继续查找
+                if(currentSticker.timelineOut < beginTime){
+                    continue
+                }
+                var showFitler: PQBaseFilter?
+                if currentSticker.type == StickerType.VIDEO.rawValue {
+                    showFitler = PQMovieFilter(movieSticker: currentSticker)
+
+                } else if currentSticker.type == StickerType.IMAGE.rawValue {
+                    showFitler = PQImageFilter(sticker: currentSticker, isExport: (movie?.mIsExport) ?? false, showUISize: mCanverSize)
+                    (showFitler as? PQImageFilter)?.isPointModel = ((mStickers?.count ?? 0) > 0)
+                }
+                if showFitler != nil {
+                    BFLog(message: " 加入到缓存 的 filter timelinein:\(currentSticker.timelineIn) timelineout: \(currentSticker.timelineOut) in :\(currentSticker.model_in) out: \(currentSticker.out) index : \(index)")
+                    cacheFilters.append(showFitler!)
+                }
+
+            }
+            
+            DispatchQueue.global().async {[weak self] in
+                if let strongSelf = self {
+                    for (index, filter) in strongSelf.cacheFilters.enumerated() {
+                        BFLog(2, message: " 初始化 config create currentSticker timelinein \(String(describing: filter.stickerInfo?.timelineIn)) timelineout \(String(describing: filter.stickerInfo?.timelineOut))  in :\(String(describing: filter.stickerInfo?.model_in)) out \(String(describing: filter.stickerInfo?.out))  index\(index)")
+                    }
+                }
+            }
+            
+            if(cacheFilters.first != nil){
+                movie?.removeAllTargets()
+                let showFilter: PQBaseFilter = cacheFilters.first!
+                movie?.addTarget(showFilter, atTargetIndex: 0)
+                showFilter.addTarget(renderView, atTargetIndex: 0)
+            }
+      
+        }
+ 
+    }
+
+    //创建下一个filter 数据
+    func createNextFilter() {
+        BFLog(2, message: "加入前 当前的缓存个数为: \(cacheFilters.count)  maxCount \(cacheFiltersMaxCount) 最后一个显示时间 \(String(describing: cacheFilters.last?.stickerInfo?.timelineIn))")
+          if cacheFilters.count <=  cacheFiltersMaxCount {
+              let showIndex = mStickers?.firstIndex(where: { (sticker) -> Bool in
+                (cacheFilters.last?.stickerInfo == sticker)
+              })
+                BFLog(2, message: "当前显示的showIndex: \(String(describing: showIndex))")
+              if ((showIndex ?? 0) + 1) < (mStickers?.count ?? 0) {
+                  let currentSticker = mStickers?[(showIndex ?? 0) + 1]
+                  if currentSticker != nil {
+                      var showFitler: PQBaseFilter?
+                      if currentSticker!.type == StickerType.VIDEO.rawValue {
+                          showFitler = PQMovieFilter(movieSticker: currentSticker!)
+
+                      } else if currentSticker!.type == StickerType.IMAGE.rawValue {
+                        showFitler = PQImageFilter(sticker: currentSticker!, isExport: (movie?.mIsExport) ?? false, showUISize: mCanverSize)
+                        (showFitler as? PQImageFilter)?.isPointModel = ((mStickers?.count ?? 0) > 0)
+                      }
+                      if showFitler != nil {
+
+                          cacheFilters.append(showFitler!)
+                      }
+                  }else{
+                    BFLog(2, message: "缓存数据加入不成功!!!!!")
+                  }
+              }
+            
+            BFLog(2, message: "加入后 当前的缓存个数为: \(cacheFilters.count)  maxCount \(cacheFiltersMaxCount) 最后一个显示时间 \(String(describing: cacheFilters.last?.stickerInfo?.timelineIn))")
+             
+          }
+        
+        
+      }
+ 
+    
+    /// 按时间从缓存中取出要显示的filter
+    /// - Parameter currTime: 当前播放时间
+    func changeFilter(currTime: Float64) {
+//        let  starts:CFTimeInterval = CFAbsoluteTimeGetCurrent()
+        BFLog(message: " 要查找的 currTime is \(currTime)")
+        //1,删除已经显示过的 filter
+        self.cacheFilters.removeAll(where: {(filter) -> Bool in
+
+            (currTime > (filter.stickerInfo?.timelineOut ?? 0.0))
+
+        })
+ 
+        // 2,找出一个要显示的 fitler
+        let showIndex = cacheFilters.firstIndex(where: { (filter) -> Bool in
+            (currTime >= (filter.stickerInfo?.timelineIn ?? 0.0) && currTime <= (filter.stickerInfo?.timelineOut ?? 0.0))
+
+        })
+        if(showIndex == nil){
+            BFLog(2, message: "缓存没有查找到?出现数据错误!!!!")
+            return
+        }
+  
+        let showFilter: PQBaseFilter = cacheFilters[showIndex ?? 0]
+        
+        BFLog(2, message: "缓存操作   查找到命中的显示是为:\(currTime) 缓存数据timeline in :\(showFilter.stickerInfo?.timelineIn ?? 0.0)) timelineOut:\(showFilter.stickerInfo?.timelineOut ?? 0.0) in:\(showFilter.stickerInfo?.model_in ?? 0.0) out:\(showFilter.stickerInfo?.out ?? 0.0) 缓存数 \(cacheFilters.count) index: \(String(describing: showIndex))")
+        
+        if(!(showFilter.isShow)){
+            BFLog(2, message: "showIndex当前时间为  \(currTime) showIndex is \(String(describing: showIndex)) 显示 filter timelineIn is: \(String(describing: showFilter.stickerInfo?.timelineIn)) timelineOut is: \(String(describing: showFilter.stickerInfo?.timelineOut))")
+ 
+            showFilter.isShow = true
+            
+            movie!.removeAllTargets()
+        
+            //为了优化性能只有素材宽高比和画面宽高比不一样时才做高斯
+            //原图的比例
+            let stickerAspectRatio = String(format: "%.6f", (showFilter.stickerInfo?.width ?? 0.0 ) / (showFilter.stickerInfo?.height ?? 0.0))
+            //画面的比例
+            let canverAspectRatio = String(format: "%.6f",(movie?.mShowVidoSize.width ?? 0.0) /  (movie?.mShowVidoSize.height ?? 0.0))
+            if(showFilter.stickerInfo?.type == StickerType.IMAGE.rawValue && showGaussianBlur && Float(stickerAspectRatio) != Float(canverAspectRatio)){
+                      BFLog(2, message: "显示图片filter")
+//                    //高斯层
+                        let  blurStickerModel:PQEditVisionTrackMaterialsModel? = showFilter.stickerInfo?.copy() as? PQEditVisionTrackMaterialsModel
+                        blurStickerModel?.canvasFillType = stickerContentMode.aspectFillStr.rawValue
+
+                        if blurStickerModel == nil {
+                            BFLog(2, message: "显示图片filter blurStickerModel is nil")
+                            return
+                        }
+                        let showGaussianFitler:PQBaseFilter = PQImageFilter(sticker: blurStickerModel!, isExport: (movie?.mIsExport) ?? false, showUISize: mCanverSize)
+                        (showGaussianFitler as? PQImageFilter)?.isPointModel = ((mStickers?.count ?? 0) > 0)
+                        
+                        let iosb:GaussianBlur = GaussianBlur.init()
+                        iosb.blurRadiusInPixels = 20
+                        showGaussianFitler.addTarget(iosb)
+                        
+                        self.movie?.addTarget(showGaussianFitler, atTargetIndex: 0)
+                        iosb.addTarget(showFilter,atTargetIndex: 0)
+                        showFilter.addTarget(self.renderView as ImageConsumer, atTargetIndex: 0)
+                
+                        BFLog(2, message: "filter 添加成功 注意是否添加成功。")
+                        
+//                    }
+ 
+            }else{
+                movie?.addTarget(showFilter, atTargetIndex: 0)
+                showFilter.addTarget(renderView, atTargetIndex: 0)
+
+            }
+            self.createFiltersQueue.async {
+                self.createNextFilter()
+            }
+
+        }else{
+            BFLog(2, message: " 添加过了 currTime is \(currTime) timelineIn:\(showFilter.stickerInfo?.timelineIn ?? 0.0)")
+        }
+    }
+
+    /// 设置 filter 是否为 seek 状态
+    func setEnableSeek(isSeek: Bool) {
+        for filter in filters {
+            (filter as? PQBaseFilter)?.enableSeek = isSeek
+        }
+    }
+
+    private func applyFilters() {
+        guard let movie = movie else { return }
+        movie.removeAllTargets()
+        var currentTarget: ImageSource = movie
+        filters.forEach {
+            let f = $0
+            currentTarget.addTarget(f, atTargetIndex: 0)
+            currentTarget = f
+        }
+        currentTarget.addTarget(renderView, atTargetIndex: 0)
+    }
+}
+
+// MARK: Player control
+
+public extension PQGPUImagePlayerView {
+    ///  开始播放
+    /// - Parameter pauseFirstFrame: 是否暂停到第一帧
+    func play(pauseFirstFrame: Bool = false, playeTimeRange: CMTimeRange = CMTimeRange()) {
+        DispatchQueue.main.async {
+            self.playerEmptyView.isHidden = true
+            self.playView.isHidden = !pauseFirstFrame
+            self.playMaskView.isHidden = !pauseFirstFrame
+            self.renderView.isHidden = false
+            self.progressLab.isHidden = false
+        }
+//        guard status != .playing else {
+//            BFLog(2, message: "已经是播放状态")
+//            return
+//        }
+
+        // 如果没有设置开始结束时长 使用默认音频总时长(创作工具就不会传值)
+        if CMTIMERANGE_IS_INVALID(playeTimeRange) {
+            let endTime = CMTime(value: CMTimeValue(CMTimeGetSeconds(asset?.duration ?? .zero) * 600), timescale: 600)
+            mPlayeTimeRange = CMTimeRange(start: .zero, end: endTime)
+
+        } else {
+            mPlayeTimeRange = playeTimeRange
+        }
+        // 清空音频缓存
+        speaker?.clearBuffer()
+
+        movie?.start(timeRange: mPlayeTimeRange ?? CMTimeRange())
+
+        speaker?.start()
+
+        status = pauseFirstFrame ? .pause : .playing
+        
+        showProgressStartTime = 0
+    }
+
+    // 快进
+    func seek(to time: CMTime) {
+        mPlayeTimeRange?.start = time
+        play(pauseFirstFrame: false, playeTimeRange: mPlayeTimeRange ?? .zero)
+    }
+
+    // 暂停
+    func pause() {
+        guard status != .pause else {
+            return
+        }
+        movie?.pause()  // 可能会引起crash: configureThread()里timebaseInfo为0,除法出错
+        speaker?.pause()
+        status = .pause
+        showPlayBtn(isHidden: false)
+    }
+
+    // 停止f解码状态
+    func stop() {
+        //        guard status != .stop else {
+        //            return
+        //        }
+
+        movie?.removeAllTargets()
+        movie?.cancel()
+        speaker?.cancel()
+        status = .stop
+    }
+
+    // 清空播放器状态,到空状态
+    func clearPlayerView() {
+        playerEmptyView.isHidden = false
+        renderView.isHidden = true
+        progressLab.isHidden = true
+    }
+
+    // 显示提示文字
+    func showTip(show: Bool) {
+        BFLog(2, message: "showTip \(show)")
+        tipLab.isHidden = !show
+        if show {
+            playerEmptyView.isHidden = true
+
+            renderView.isHidden = true
+            progressLab.isHidden = true
+        }
+    }
+}
+
+// MARK: Filter 操作
+
+public extension PQGPUImagePlayerView {
+    // 添加 filter
+    func appendFilter(_ filter: ImageProcessingOperation) {
+        filters.append(filter)
+    }
+
+    // 添加一组filters
+    func appendFilters(_ newFilters: [ImageProcessingOperation]) {
+        filters = filters + newFilters
+    }
+
+    // 移除所有filter
+    func removeAllFilters() {
+        filters.removeAll()
+    }
+
+    // 重置所有 filer
+    func appendFiltersClearOldFilter(_ newFilters: [ImageProcessingOperation]) {
+        filters.removeAll()
+        filters = newFilters
+    }
+
+}
+
+// MARK: - RenderViewDelegate
+extension PQGPUImagePlayerView: RenderViewDelegate{
+    public func willDisplayFramebuffer(renderView _: RenderView, framebuffer _: Framebuffer) {
+        BFLog(2, message: "willDisplayFramebuffer")
+    }
+
+    public func didDisplayFramebuffer(renderView _: RenderView, framebuffer: Framebuffer) {
+        BFLog(2, message: "didDisplayFramebuffer")
+    }
+
+    public func shouldDisplayNextFramebufferAfterMainThreadLoop() -> Bool {
+        BFLog(2, message: "didDisplayFramebuffer")
+        
+        return false
+    }
+}
+
+

+ 854 - 0
BFStuckPointKit/Classes/ViewModel/PQPlayerViewModel.swift

@@ -0,0 +1,854 @@
+//
+//  PQPlayerViewModel.swift
+//  PQSpeed
+//
+//  Created by ak on 2021/1/27.
+//  Copyright © 2021 BytesFlow. All rights reserved.
+//  视频渲染相关逻辑方法
+
+import RealmSwift
+import UIKit
+import BFCommonKit
+import BFUIKit
+
+open class PQPlayerViewModel: NSObject {
+    /// 根据贴纸信息转成种 fitler ,编辑 ,总览,导出共用
+    /// - Parameter parts: filter 组
+    public class func partModelToFilters(sections: [PQEditSectionModel], inputSize: CGSize = .zero) -> ([PQBaseFilter], [URL]) {
+        // 所有段的声音位置
+        var audioFiles: Array = Array<URL>.init()
+        // 所有滤镜数组
+        var filters: Array = Array<PQBaseFilter>.init()
+
+        /*
+         一, 默认素材时长
+         图片:2S
+         视频: X1倍速 播一边
+         GIF: X1倍速 播一边
+
+         二,资源适配规则
+         1,有配音声音 也就是有文字
+         适配系数 = 配音时长/视觉总时长
+         视觉元素最终时长 = 视觉元素原时长 * 适配系数
+         2,无配音无文字
+         使用素材的默认时长
+         3,无配音有文字
+         适配系数 = 视频总时长/文字总时长
+         文字每一句的实际时长 = 文字分段落的原始时长 * 适配系统
+
+         */
+
+        // 返回时自动预览开始播放 添加有贴纸开始自动播放
+
+        var partTotaDuration: Float64 = 0
+        for section in sections {
+            autoreleasepool {
+                // 优先使用 mix audio
+                if section.mixEmptyAuidoFilePath.count > 0 {
+                    audioFiles.append(URL(fileURLWithPath: documensDirectory + section.mixEmptyAuidoFilePath.replacingOccurrences(of: documensDirectory, with: "")))
+                    BFLog(message: "add mixEmptyAuidoFilePath mixEmptyAuidoFilePath")
+                } else {
+                    if section.audioFilePath.count > 0 {
+                        audioFiles.append(URL(fileURLWithPath: documensDirectory + section.audioFilePath.replacingOccurrences(of: documensDirectory, with: "")))
+                        BFLog(message: "add audioFilePath audioFilePath")
+                    }
+                }
+
+                var totalDuration: Float64 = 0
+                // 根据已经选择的贴纸类型创建各自filters
+                for sticker in section.sectionTimeline!.visionTrack!.getEnableVisionTrackMaterials() {
+                    autoreleasepool {
+                        
+                        sticker.timelineIn = totalDuration + partTotaDuration
+                        totalDuration = totalDuration + sticker.aptDuration
+                        sticker.timelineOut = totalDuration + partTotaDuration
+                        BFLog(message: "创建 filter start :\(sticker.timelineIn) end :\(sticker.timelineOut) type is \(sticker.type)")
+                        if(sticker.aptDuration > 0){
+                            if sticker.type == StickerType.IMAGE.rawValue {
+                                let imageFilter = PQImageFilter(sticker: sticker)
+                                filters.append(imageFilter)
+
+                            } else if sticker.type == StickerType.VIDEO.rawValue {
+                                let videoFilter = PQMovieFilter(movieSticker: sticker)
+
+                                filters.append(videoFilter)
+
+                            } else if sticker.type == StickerType.GIF.rawValue {
+                                let gifFilter = PQGifFilter(sticker: sticker)
+                                filters.append(gifFilter)
+                            }
+                        }else{
+                            BFLog(message: "sticker.aptDuration is error create filter error!!! \(sticker.aptDuration )")
+                        }
+                 
+                    }
+                }
+
+                // 字幕如果是多段的 ,字幕的开始时间是 前几段 part duration 总时长 所以要重新计算
+                var newSubtitleData: [PQEditSubTitleModel] = Array()
+
+                // 如果有录制声音转的字幕优先使用,在使用人工输入文字字幕s
+                let recorderSubtitle = List<PQEditSubTitleModel>()
+                if section.sectionTimeline?.visionTrack?.getSubtitleMatraislInfo() != nil {
+                    for subtitleMatraislInfo in section.sectionTimeline!.visionTrack!.getSubtitleMatraislInfo() {
+                        BFLog(message: "有录音字幕")
+                        let editSubTitleModel = PQEditSubTitleModel()
+                        editSubTitleModel.text = subtitleMatraislInfo.subtitleInfo?.text ?? ""
+                        editSubTitleModel.timelineIn = subtitleMatraislInfo.timelineIn
+                        editSubTitleModel.timelineOut = subtitleMatraislInfo.timelineOut
+                        recorderSubtitle.append(editSubTitleModel)
+                    }
+                }
+
+                for (index, subTitle) in recorderSubtitle.count > 0 ? recorderSubtitle.enumerated() : section.subTitles.enumerated() {
+                    BFLog(message: "有配音字幕")
+                    let newSubtitle = PQEditSubTitleModel()
+                    newSubtitle.timelineIn = subTitle.timelineIn
+                    newSubtitle.timelineOut = subTitle.timelineOut
+                    newSubtitle.text = subTitle.text.replacingOccurrences(of: "\n", with: "")
+                    BFLog(message: "第\(index)个字幕 subTitle old start : \(newSubtitle.timelineIn)  end: \(newSubtitle.timelineOut) text: \(newSubtitle.text)")
+
+                    // subtitle duration
+                    let duration: Float64 = (newSubtitle.timelineOut - newSubtitle.timelineIn)
+
+                    newSubtitle.timelineIn = partTotaDuration + newSubtitle.timelineIn
+                    newSubtitle.timelineOut = newSubtitle.timelineIn + duration
+
+                    BFLog(message: "第\(index)个字幕 subTitle new start : \(newSubtitle.timelineIn)  end: \(newSubtitle.timelineOut) text: \(newSubtitle.text)")
+
+                    newSubtitleData.append(newSubtitle)
+
+//                    let subTitle = PQSubTitleFilter(st: [newSubtitle], isBig: section.sectionTimeline?.visionTrack?.getEnableVisionTrackMaterials().count == 0, inputSize: inputSize)
+//                    filters.append(subTitle)
+                }
+                // 无视觉素材是大字幕方式 有数据在初始字幕filter
+
+//                for subtitle in newSubtitleData{
+//                    let subTitleFilter = PQSubTitleFilter(st: [newSubtitleData[0]], isBig: section.sectionTimeline?.visionTrack?.getEnableVisionTrackMaterials().count == 0,inputSize: inputSize)
+//                    filters.append(subTitleFilter)
+//                }
+
+                if newSubtitleData.count > 0 {
+                    let subTitleFilter = PQSubTitleFilter(st: newSubtitleData, isBig: section.sectionTimeline?.visionTrack?.getEnableVisionTrackMaterials().count == 0, inputSize: inputSize)
+                    filters.append(subTitleFilter)
+
+//                    DispatchQueue.main.async {
+
+//                    }
+                }
+
+                var tempDuration = section.allStickerAptDurationNoRound() == 0 ? section.sectionDuration : section.allStickerAptDurationNoRound()
+                BFLog(message: "tempDuration 1 is \(tempDuration)")
+                // 如果音频时长是经过加空音频 加长后的 要使用长音频
+                if section.mixEmptyAuidoFilePath.count > 0 {
+                    BFLog(message: "有拼接的数据")
+                    let audioAsset = AVURLAsset(url: URL(fileURLWithPath: documensDirectory + section.mixEmptyAuidoFilePath), options: avAssertOptions)
+                    if tempDuration <= audioAsset.duration.seconds {
+                        tempDuration = audioAsset.duration.seconds
+                    } else {
+                        BFLog(message: "音频文件时长为0?")
+                    }
+                }
+                BFLog(message: "tempDuration 2 is \(tempDuration)")
+
+                partTotaDuration = partTotaDuration + tempDuration
+            }
+            BFLog(message: "audioFiles 声音文件总数\(audioFiles.count)")
+        }
+        //"/Resource/DownloadImages/images_1631358852.933532"
+        //""/Resource/DownloadImages/images_1631358852.933532""
+        return (filters, audioFiles)
+    }
+
+    public class func calculationStickAptDurationReal(currentPart: PQEditSectionModel, completeHander: @escaping (_ returnPart: PQEditSectionModel?) -> Void) {
+        // XXXXXX如果 没有选择发音人 就算有自动的转的声音文件也不按声音时长计算,都是素材原有时长
+//        let audioTotalDuration: Float64 = Float64(currentPart.sectionDuration)
+        // 1,计算贴纸所有原始时长
+        var stickerTotalDuration: Float64 = 0
+
+        for sticker in currentPart.sectionTimeline!.visionTrack!.getEnableVisionTrackMaterials() {
+            var stikcerDuration: Float64 = sticker.duration
+            if sticker.videoIsCrop() {
+                BFLog(message: "这个视频有裁剪 \(sticker.locationPath)")
+                stikcerDuration = sticker.out - sticker.model_in
+            }
+
+            stickerTotalDuration = stickerTotalDuration + stikcerDuration
+        }
+
+        // 真人声音时长
+        var realAudioDuration = 0.0
+        BFLog(message: "currentPart.audioFilePath is \(currentPart.audioFilePath)")
+        if currentPart.audioFilePath.count > 0 {
+            let audioAsset = AVURLAsset(url: URL(fileURLWithPath: documensDirectory + currentPart.audioFilePath), options: avAssertOptions)
+            realAudioDuration = audioAsset.duration.seconds
+        }
+
+        BFLog(message: "所有素材的总时 \(stickerTotalDuration)  文字转语音的时长:\(realAudioDuration)")
+
+        if stickerTotalDuration == 0 && realAudioDuration == 0 {
+            DispatchQueue.main.async {
+                completeHander(currentPart)
+            }
+            return
+        }
+
+        // 所有视频素材原有时长 > 音频文件(字幕时长 有可能有声音,有可能没有声音自动转的)
+        if stickerTotalDuration - realAudioDuration > 0.01 {
+            // 要创建空文件加长原有声音
+            let tool = PQCreateEmptyWAV(sampleRate: 8000,
+                                        channel: 1,
+                                        duration: stickerTotalDuration - realAudioDuration,
+                                        bit: 16)
+            let timeInterval: TimeInterval = Date().timeIntervalSince1970
+
+            var audioFileTempPath = exportAudiosDirectory
+            if !directoryIsExists(dicPath: audioFileTempPath) {
+                BFLog(message: "文件夹不存在 \(audioFileTempPath)")
+                createDirectory(path: audioFileTempPath)
+            }
+
+            audioFileTempPath.append("empty_\(timeInterval).wav")
+
+            tool.createEmptyWAVFile(url: URL(fileURLWithPath: audioFileTempPath)) { _ in
+
+                var tempUrls: Array = NSArray() as! [URL]
+
+                if currentPart.audioFilePath.count > 0 {
+                    BFLog(message: "currentPart.audioFilePath is \(String(describing: currentPart.audioFilePath))")
+                    tempUrls.append(URL(fileURLWithPath: documensDirectory + currentPart.audioFilePath))
+                }
+                tempUrls.append(URL(fileURLWithPath: audioFileTempPath))
+
+                PQPlayerViewModel.mergeAudios(urls: tempUrls) { completURL in
+
+                    if completURL == nil {
+                        BFLog(message: "合并文件有问题!")
+                        return
+                    }
+                    //                file:///var/mobile/Containers/Data/Application/2A008644-31A6-4D7E-930B-F1099F36D577/Documents/Resource/ExportAudios/merge_1618817019.789495.m4a
+                    let audioAsset = AVURLAsset(url: completURL!, options: avAssertOptions)
+
+                    BFLog(message: "completURL mix : \(String(describing: completURL)) audioFilePath durtion  \(audioAsset.duration.seconds)")
+
+                    currentPart.mixEmptyAuidoFilePath = completURL!.absoluteString.replacingOccurrences(of: documensDirectory, with: "").replacingOccurrences(of: "file://", with: "")
+                    currentPart.sectionDuration = audioAsset.duration.seconds
+
+                    BFLog(message: "stickerTotalDuration is \(stickerTotalDuration)  mixEmptyAuidoFilePath 设置后 是\(currentPart.mixEmptyAuidoFilePath) 时长是:\(currentPart.sectionDuration)")
+
+                    // 1.2)计算贴纸的逻辑显示时长
+                    for sticker in currentPart.sectionTimeline!.visionTrack!.getEnableVisionTrackMaterials() {
+                        var tempDuration = sticker.duration
+                        if sticker.videoIsCrop() {
+                            tempDuration = sticker.out - sticker.model_in
+                            BFLog(message: "这个视频有裁剪后:\(tempDuration) \(String(describing: sticker.locationPath))")
+                        }
+                        sticker.aptDuration = tempDuration
+                    }
+
+                    DispatchQueue.main.async {
+                        completeHander(currentPart)
+                    }
+                }
+            }
+
+        } else {
+            // 这种情况下 mixEmptyAuidoFilePath  应该为空
+            currentPart.mixEmptyAuidoFilePath = ""
+//            currentPart.audioFilePath = ""
+            currentPart.sectionDuration = realAudioDuration
+            // 1.1)计算系数
+            let coefficient: Float64 = realAudioDuration / stickerTotalDuration
+
+            BFLog(message: "系数 is: \(coefficient) stickerTotalDuration is \(stickerTotalDuration) audioTotalDuration is :\(realAudioDuration)")
+
+            // 1.2)计算贴纸的逻辑显示时长
+            for sticker in currentPart.sectionTimeline!.visionTrack!.getEnableVisionTrackMaterials() {
+                // 如果是视频素材有过裁剪 就使用裁剪时长
+                var tempDuration = sticker.duration
+
+                if sticker.videoIsCrop() {
+                    tempDuration = sticker.out - sticker.model_in
+                    BFLog(message: "这个视频有裁剪后:\(tempDuration) \(String(describing: sticker.locationPath))")
+                }
+                // 如果没有音频 系数为0时 使用素材的原始时长
+                sticker.aptDuration = (coefficient == 0) ? tempDuration : tempDuration * coefficient
+            }
+
+            DispatchQueue.main.async {
+                completeHander(currentPart)
+            }
+        }
+    }
+
+    // 计算所有贴纸的逻辑时长
+    public class func calculationStickAptDuration(currentPart: PQEditSectionModel, createFirst: Bool = true, completeHander: @escaping (_ returnPart: PQEditSectionModel?) -> Void) {
+        if currentPart.sectionType == "global" {
+            BFLog(message: "音频段落不处理计算")
+            return
+        }
+        // 从素材详细界面返回 有可能是删除素材操作 这时如果没有选择发音人同时没有录音和导入数据要重新计算空文件时长
+        let speeckAudioTrackModel = currentPart.sectionTimeline?.audioTrack?.getAudioTrackModel(voiceType: VOICETYPT.SPEECH.rawValue)
+
+        let localAudioTrackModel = currentPart.sectionTimeline?.audioTrack?.getAudioTrackModel(voiceType: VOICETYPT.LOCAL.rawValue)
+
+        if !currentPart.haveSelectVoice(), speeckAudioTrackModel == nil, localAudioTrackModel == nil, createFirst {
+            // 只有视觉素材 没有文字
+            if currentPart.sectionText.count == 0 {
+                // 根据视觉的总时长生成空音频数据
+                var timeCount: Double = 0
+
+                for sticker in (currentPart.sectionTimeline!.visionTrack?.getEnableVisionTrackMaterials())! {
+                    if sticker.out != 0 || sticker.model_in == 0 {
+                        timeCount = timeCount + (sticker.out - sticker.model_in)
+
+                    } else {
+                        timeCount = timeCount + sticker.aptDuration
+                    }
+                }
+                BFLog(message: "计算视觉的总时长 \(timeCount)")
+                if timeCount > 0 {
+                    let tool = PQCreateEmptyWAV(sampleRate: 8000,
+                                                channel: 1,
+                                                duration: timeCount,
+                                                bit: 16)
+                    let timeInterval: TimeInterval = Date().timeIntervalSince1970
+
+                    var audioFileTempPath = exportAudiosDirectory
+                    if !directoryIsExists(dicPath: audioFileTempPath) {
+                        BFLog(message: "文件夹不存在 \(audioFileTempPath)")
+                        createDirectory(path: audioFileTempPath)
+                    }
+
+                    audioFileTempPath.append("empty_\(timeInterval).wav")
+
+                    tool.createEmptyWAVFile(url: URL(fileURLWithPath: audioFileTempPath)) { _ in
+                        currentPart.audioFilePath = audioFileTempPath.replacingOccurrences(of: documensDirectory, with: "")
+
+                        calculationStickAptDurationReal(currentPart: currentPart, completeHander: completeHander)
+                    }
+                } else {
+                    calculationStickAptDurationReal(currentPart: currentPart, completeHander: completeHander)
+                }
+            } else {
+                calculationStickAptDurationReal(currentPart: currentPart, completeHander: completeHander)
+            }
+        } else {
+            calculationStickAptDurationReal(currentPart: currentPart, completeHander: completeHander)
+        }
+    }
+
+    // 首尾拼接音频文件
+    /*
+     因为在对音频做合并或者裁切的时候生成的音频格式是m4a的,但是m4a转成mp3会损坏音频格式,所以我当时采用先把m4a转为wav,再用wav转成mp3。
+     */
+
+    /// 合并声音
+    /// - Parameter urls: 所有音频的URL  是全路径方便复用
+    /// - Parameter completeHander: 返回的 URL 全路径的 URL 如果要保存替换掉前缀
+    public class func mergeAudios(urls: [URL], completeHander: @escaping (_ fileURL: URL?) -> Void) {
+        let timeInterval: TimeInterval = Date().timeIntervalSince1970
+        let composition = AVMutableComposition()
+        var totalDuration: CMTime = .zero
+        BFLog(message: "合并文件总数 \(urls.count)")
+        for urlStr in urls {
+            BFLog(message: "合并的文件地址: \(urlStr)")
+            let audioAsset = AVURLAsset(url: urlStr, options: avAssertOptions)
+            let tracks1 = audioAsset.tracks(withMediaType: .audio)
+            if tracks1.count == 0 {
+                BFLog(message: "音频数据无效不进行合并,所有任务结束要确保输入的数据都正常! \(urlStr)")
+                break
+            }
+            let assetTrack1: AVAssetTrack = tracks1[0]
+
+            let duration1: CMTime = assetTrack1.timeRange.duration
+
+            BFLog(message: "每一个文件的 duration \(CMTimeGetSeconds(duration1))")
+
+            let timeRange1 = CMTimeRangeMake(start: .zero, duration: duration1)
+
+            let compositionAudioTrack: AVMutableCompositionTrack = composition.addMutableTrack(withMediaType: AVMediaType.audio, preferredTrackID: CMPersistentTrackID())!
+
+            do {
+                //
+                try compositionAudioTrack.insertTimeRange(timeRange1, of: assetTrack1, at: totalDuration)
+
+            } catch {
+                BFLog(message: "error is \(error)")
+            }
+
+            totalDuration = CMTimeAdd(totalDuration, audioAsset.duration)
+        }
+
+        if CMTimeGetSeconds(totalDuration) == 0 {
+            BFLog(message: "所有数据无效")
+            completeHander(nil)
+            return
+        } else {
+//            拼接声音文件 完成
+            BFLog(message: "totalDuration is \(CMTimeGetSeconds(totalDuration))")
+        }
+
+        let assetExport = AVAssetExportSession(asset: composition, presetName: AVAssetExportPresetAppleM4A)
+        BFLog(message: "assetExport.supportedFileTypes is \(String(describing: assetExport?.supportedFileTypes))")
+
+        assetExport?.outputFileType = .m4a
+        // XXXX 注意文件名的后缀要和outputFileType 一致 否则会导出失败
+        var audioFilePath = exportAudiosDirectory
+
+        if !directoryIsExists(dicPath: audioFilePath) {
+            BFLog(message: "文件夹不存在")
+            createDirectory(path: audioFilePath)
+        }
+        audioFilePath.append("merge_\(timeInterval).m4a")
+
+        let fileUrl = URL(fileURLWithPath: audioFilePath)
+
+        assetExport?.outputURL = fileUrl
+        assetExport?.exportAsynchronously {
+            if assetExport!.status == .completed {
+                // 85.819125
+                let audioAsset = AVURLAsset(url: fileUrl, options: avAssertOptions)
+
+                BFLog(message: "拼接声音文件 完成 \(fileUrl) 时长is \(CMTimeGetSeconds(audioAsset.duration))")
+                completeHander(fileUrl)
+
+            } else {
+                print("拼接出错 \(String(describing: assetExport?.error))")
+                completeHander(URL(string: ""))
+            }
+        }
+    }
+
+    /// 根据选择的画布类型计算播放器显示的位置和大小
+    /// - Parameters:
+    ///   - editProjectModel: 项目数据
+    ///   - showType: 显示类型 1, 编辑界面  2,总览界面
+    /// - Returns: 显示的坐标和位置
+    public class func getShowCanvasRect(editProjectModel: PQEditProjectModel?, showType: Int, playerViewHeight: CGFloat = 216 / 667 * cScreenHeigth) -> CGRect {
+        if editProjectModel == nil {
+            BFLog(message: "editProjectModel is error")
+            return CGRect()
+        }
+        // UI播放器的最大高度,同时最大宽度为设备宽度
+        var showRect: CGRect = CGRect(x: (cScreenWidth - playerViewHeight) / 2, y: 0, width: playerViewHeight, height: playerViewHeight)
+
+        let canvasType: Int = editProjectModel!.sData!.videoMetaData!.canvasType
+
+        if showType == 1 { // 编辑界面
+            switch canvasType {
+            case videoCanvasType.origin.rawValue:
+
+                // 使用有效素材第一位
+                var firstModel: PQEditVisionTrackMaterialsModel?
+                for part in editProjectModel!.sData!.sections {
+                    if part.sectionTimeline?.visionTrack?.getEnableVisionTrackMaterials().count ?? 0 > 0 {
+                        firstModel = part.sectionTimeline?.visionTrack?.getEnableVisionTrackMaterials().first
+                        break
+                    }
+                }
+                if firstModel != nil {
+                    if firstModel?.width == 0 || firstModel?.height == 0 {
+                        BFLog(message: "!!!!!!!!!!!素材宽高有问题!!!!!!!!!!!")
+                    }
+                    BFLog(1, message: "第一个有效素材的大小 \(String(describing: firstModel?.width)) \(String(describing: firstModel?.height))")
+                    let ratioMaterial: Float = (firstModel?.width ?? 0) / (firstModel?.height ?? 0)
+                    if ratioMaterial > 1 {
+                        // 横屏
+                        var tempPlayerHeight = cScreenWidth * CGFloat(firstModel!.height / firstModel!.width)
+                        var scale: CGFloat = 1.0
+                        if tempPlayerHeight > playerViewHeight {
+                            scale = CGFloat(playerViewHeight) / CGFloat(tempPlayerHeight)
+                            tempPlayerHeight = tempPlayerHeight * scale
+                        }
+                        showRect = CGRect(x: (cScreenWidth - cScreenWidth * scale) / 2, y: (playerViewHeight - tempPlayerHeight) / 2, width: cScreenWidth * scale, height: tempPlayerHeight)
+                    } else {
+                        // 竖屏
+                        let playerViewWidth = (CGFloat(firstModel!.width) / CGFloat(firstModel!.height)) * playerViewHeight
+                        showRect = CGRect(x: (cScreenWidth - playerViewWidth) / 2, y: 0, width: playerViewWidth, height: playerViewHeight)
+                    }
+                } else {
+                    // 没有视觉素材时,只有文字,语音时,默认为原始但显示的 VIEW 为 1:1
+                    showRect = CGRect(x: (cScreenWidth - playerViewHeight) / 2, y: 0, width: playerViewHeight, height: playerViewHeight)
+                }
+
+            case videoCanvasType.oneToOne.rawValue:
+                showRect = CGRect(x: (cScreenWidth - playerViewHeight) / 2, y: 0, width: playerViewHeight, height: playerViewHeight)
+            case videoCanvasType.nineToSixteen.rawValue:
+                showRect = CGRect(x: (cScreenWidth - playerViewHeight * (9.0 / 16.0)) / 2, y: 0, width: playerViewHeight * (9.0 / 16.0), height: playerViewHeight)
+            case videoCanvasType.sixteenToNine.rawValue:
+                showRect = CGRect(x: 0, y: 0 + (playerViewHeight - cScreenWidth * (9.0 / 16.0)) / 2, width: cScreenWidth, height: cScreenWidth * (9.0 / 16.0))
+            default:
+                break
+            }
+        } else if showType == 2 { // 总览界面
+            switch canvasType {
+            case videoCanvasType.origin.rawValue:
+
+                BFLog(message: "总览时画布的大小 \(String(describing: editProjectModel!.sData!.videoMetaData?.videoWidth)) \(String(describing: editProjectModel!.sData!.videoMetaData?.videoHeight))")
+                // 画布的宽高 和宽高比值
+                let materialWidth = editProjectModel!.sData!.videoMetaData?.videoWidth ?? 0
+                let materialHeight = editProjectModel!.sData!.videoMetaData?.videoHeight ?? 1
+                let ratioMaterial: Float = Float(materialWidth) / Float(materialHeight)
+
+                if ratioMaterial > 1 {
+                    // 横屏
+                    showRect = CGRect(x: 0, y: 0, width: cScreenWidth, height: cScreenWidth * CGFloat(materialHeight) / CGFloat(materialWidth))
+                } else if ratioMaterial < 1 {
+                    // 竖屏
+                    showRect = CGRect(x: (cScreenWidth - cScreenWidth * CGFloat(materialWidth) / CGFloat(materialHeight)) / 2, y: 0, width: cScreenWidth * (CGFloat(materialWidth) / CGFloat(materialHeight)), height: cScreenWidth)
+                    BFLog(message: "showRect is \(showRect)")
+                } else {
+                    showRect = CGRect(x: 0, y: 0, width: cScreenWidth - 2, height: cScreenWidth - 2)
+                }
+
+            case videoCanvasType.oneToOne.rawValue:
+                showRect = CGRect(x: 0, y: 0, width: cScreenWidth - 2, height: cScreenWidth - 2)
+            case videoCanvasType.nineToSixteen.rawValue:
+                showRect = CGRect(x: (cScreenWidth - cScreenWidth * (9.0 / 16.0)) / 2, y: 0, width: cScreenWidth * (9.0 / 16.0), height: cScreenWidth)
+            case videoCanvasType.sixteenToNine.rawValue:
+                showRect = CGRect(x: 0, y: 0, width: cScreenWidth, height: cScreenWidth * (9.0 / 16.0))
+
+            default:
+                break
+            }
+        }
+
+        return showRect
+    }
+
+    /*
+     1, 加工入口进入编辑界面 默认画布?默认为 原始
+     2,进入编辑界面如果选了一个素材 画布就是实际大小,
+     3,没视觉素材时 点击原始显示1:1
+     4, 上传入口进入编辑界面 默认画布为原始
+     5, 从草稿箱进来时,使用恢复的画布大小
+     6, 如果选择了原始,移动素材后都按最新的第一个素材修改画布
+     */
+
+    /// sdata json canvastype 转到 UI 所使用类型
+    /// - Parameter projectModel: project sdata
+    /// - Returns: UI 使用类型
+    public class func videoCanvasTypeToAspectRatio(projectModel: PQEditProjectModel?) -> aspectRatio? {
+        // add by ak 给素材详情界面传比例参数如果是原始大小的要传 size
+        var aspectRatioTemp: aspectRatio?
+        if projectModel?.sData?.videoMetaData?.canvasType == videoCanvasType.origin.rawValue {
+            var firstModel: PQEditVisionTrackMaterialsModel?
+            for part in projectModel!.sData!.sections {
+                if part.sectionTimeline?.visionTrack?.getEnableVisionTrackMaterials().count ?? 0 > 0 {
+                    firstModel = part.sectionTimeline?.visionTrack?.getEnableVisionTrackMaterials().first
+                    break
+                }
+            }
+
+            if firstModel != nil {
+                aspectRatioTemp = .origin(width: CGFloat(firstModel!.width), height: CGFloat(firstModel!.height))
+            } else {
+                aspectRatioTemp = .origin(width: CGFloat(projectModel?.sData?.videoMetaData?.videoWidth ?? 0), height: CGFloat(projectModel?.sData?.videoMetaData?.videoHeight ?? 0))
+            }
+
+        } else if projectModel?.sData?.videoMetaData?.canvasType == videoCanvasType.oneToOne.rawValue {
+            aspectRatioTemp = .oneToOne
+        } else if projectModel?.sData?.videoMetaData?.canvasType == videoCanvasType.nineToSixteen.rawValue {
+            aspectRatioTemp = .nineToSixteen
+        } else if projectModel?.sData?.videoMetaData?.canvasType == videoCanvasType.sixteenToNine.rawValue {
+            aspectRatioTemp = .sixteenToNine
+        }
+        return aspectRatioTemp
+    }
+
+    public class func getCanvasBtnName(canvasType: videoCanvasType) -> (String, String) {
+        var btnText: String = "自适应"
+        var btnImageName: String = "settingZoom_origin_h"
+
+        if canvasType == .origin {
+            btnText = "自适应"
+            btnImageName = "settingZoom_origin_h"
+
+        } else if canvasType == .oneToOne {
+            btnText = "1:1"
+            btnImageName = "settingZoom_oneToOne_h"
+        } else if canvasType == .sixteenToNine {
+            btnText = "16:9"
+            btnImageName = "settingZoom_sixteenToNine_h"
+        } else if canvasType == .nineToSixteen {
+            btnText = "9:16"
+            btnImageName = "settingZoom_nineToSixteen_h"
+        }
+
+        return (btnText, btnImageName)
+    }
+}
+
+// MARK: - 混音相关
+
+/// 混音相关
+extension PQPlayerViewModel {
+    /// 混音合成
+    /// - Parameters:
+    ///   - originAsset: 空音乐文件素材
+    ///   - bgmData: 背景音乐
+    ///   - videoStickers: 视频素材
+    ///   - originMusicDuration : 要播放的时长
+    ///   - lastSecondPoint : 音频长度不够时,拼接音频文件时的结束时间,推荐卡点的倒数第二位
+    ///   - startTime: 裁剪的开始位置。
+    /// - Returns:
+    public class func setupAudioMix(originAsset: AVURLAsset, bgmData: PQVoiceModel?, videoStickers: [PQEditVisionTrackMaterialsModel]?,originMusicDuration:Float = 0,clipAudioRange: CMTimeRange = CMTimeRange.zero,startTime:CMTime = .zero ) -> (AVMutableAudioMix, AVMutableComposition) {
+        let composition = AVMutableComposition()
+        let audioMix = AVMutableAudioMix()
+        var tempParameters: [AVMutableAudioMixInputParameters] = [AVMutableAudioMixInputParameters].init()
+     
+        // 处理选择的主音乐
+        if(originMusicDuration > Float(CMTimeGetSeconds(clipAudioRange.duration))){
+            BFLog(message: "要播放的时间长,比原音频要长进行拼接originMusicDuration:\(originMusicDuration)   originAsset.duration \(CMTimeGetSeconds(clipAudioRange.duration))")
+            let originaParameters =  dealWithOriginAssetTrack(originAsset: originAsset, totalDuration: Float64(originMusicDuration), composition: composition,clipAudioRange: clipAudioRange,mStartTime: startTime)
+            BFLog(message: "originaParameters count \(originaParameters.count)")
+            if originaParameters.count > 0 {
+                tempParameters = tempParameters + originaParameters
+            }
+            
+        }else{
+            BFLog(message: "音频不用拼接:\(CMTimeGetSeconds(originAsset.duration))")
+            let parameters = mixAudioTrack(audioAsset: originAsset, trackTimeRange: CMTimeRange(start: .zero, end: originAsset.duration), composition: composition)
+            if parameters != nil {
+                tempParameters.append(parameters!)
+            }else{
+                
+                BFLog(message: "parameters is error \(CMTimeGetSeconds(originAsset.duration))")
+            }
+        }
+     
+        // 处理背景音乐
+        if bgmData != nil, bgmData?.localPath != nil {
+            let bgmParameters = dealWithBGMTrack(bgmData: bgmData!, totalDuration: originAsset.duration.seconds, composition: composition)
+            if bgmParameters.count > 0 {
+                tempParameters = tempParameters + bgmParameters
+            }
+        }
+        // 处理素材音乐
+        if videoStickers != nil, (videoStickers?.count ?? 0) > 0 {
+            for sticker in videoStickers! {
+                if sticker.volumeGain == 0 {
+                    // 如果添加了会有刺啦音
+                    BFLog(message: "音频音量 为0 不添加")
+                    continue
+                }
+                let stickerParameters = dealWithMaterialTrack(stickerModel: sticker, composition: composition)
+                if stickerParameters.count > 0 {
+                    tempParameters = tempParameters + stickerParameters
+                }
+            }
+        }
+        audioMix.inputParameters = tempParameters
+        // 导出音乐
+        // exportAudio(comosition: composition)
+        return (audioMix, composition)
+    }
+    
+    /// 处理原主音乐音轨  e.g. 原音频时长只有30s  要播放 250s 的音频 拼接原音频音轨
+    /// - Parameters:
+    ///   - originAsset: 原音频文件地址
+    ///   - composition:
+    /// - Returns:
+    public class func dealWithOriginAssetTrack(originAsset: AVURLAsset, totalDuration: Float64, composition: AVMutableComposition,clipAudioRange: CMTimeRange = CMTimeRange.zero,mStartTime:CMTime = .zero ) -> [AVMutableAudioMixInputParameters] {
+        var tempParameters: [AVMutableAudioMixInputParameters] = [AVMutableAudioMixInputParameters].init()
+        let volume:Float = 1.0
+        let originaDuration = CMTimeGetSeconds(clipAudioRange.duration)
+        BFLog(message: "处理主音频 原始时长startTime = \(originaDuration) 要显示时长totalDuration = \(totalDuration)")
+        //整倍数
+        let  count = Int(totalDuration) / Int(originaDuration)
+//        count = count + 1
+        //有余数多 clip 一整段
+        let row = totalDuration - Double(count) * originaDuration
+        //已经拼接的总时长
+        var clipTotalDuration:Float = 0.0
+        if count > 0 {
+            for index in 0 ..< count {
+                BFLog(message: "this is running running")
+                //第一段是用户选择的开始时间 到倒数第二个卡点, 其它段都是从推荐卡点到倒数第二个卡点
+                var startTime = CMTime.zero
+                var trackTimeRange = clipAudioRange
+       
+                if(index == 0){
+                    startTime = mStartTime
+                    trackTimeRange =  CMTimeRange(start: startTime, end: CMTime(value: CMTimeValue(CMTimeGetSeconds(clipAudioRange.end)), timescale: playerTimescaleInt))
+                    clipTotalDuration = clipTotalDuration + Float(CMTimeGetSeconds(trackTimeRange.duration))
+                }else{
+                    // (CMTimeGetSeconds(clipAudioRange.end) - CMTimeGetSeconds(mStartTime))为用户选择的第一段时长
+                    startTime = CMTime(value: CMTimeValue((CMTimeGetSeconds( clipAudioRange.duration) * Double(index) + (CMTimeGetSeconds(clipAudioRange.end) - CMTimeGetSeconds(mStartTime))) * Float64(playerTimescaleInt)), timescale: playerTimescaleInt)
+                    trackTimeRange = clipAudioRange
+                    
+                    clipTotalDuration = clipTotalDuration + Float(CMTimeGetSeconds(trackTimeRange.duration))
+                }
+//                BFLog(1, message: "原音频时长短:count = \(count),startTime = \(startTime),trackTimeRange = \(trackTimeRange)")
+                let parameters = mixAudioTrack(audioAsset: originAsset, startTime: startTime, trackTimeRange: trackTimeRange, volume: volume, composition: composition)
+                if parameters != nil {
+                    tempParameters.append(parameters!)
+                }else{
+                    BFLog(message: "接拼出现错误!!!!")
+                }
+            }
+        }
+        if(row > 0){
+            
+            let startTime = CMTime(value: CMTimeValue(clipTotalDuration * Float(playerTimescaleInt)), timescale: playerTimescaleInt)
+            
+            let trackTimeRange = CMTimeRange(start: startTime, end: CMTime(value: CMTimeValue((CMTimeGetSeconds(startTime) + row) * Double(playerTimescaleInt)), timescale: playerTimescaleInt))
+            BFLog(1, message: "最后一小段音乐时长短:count = \(count),startTime = \(CMTimeShow(startTime)),trackTimeRange = \(CMTimeRangeShow(trackTimeRange))")
+            let parameters = mixAudioTrack(audioAsset: originAsset, startTime: startTime, trackTimeRange: trackTimeRange, volume: volume, composition: composition)
+            if parameters != nil {
+                tempParameters.append(parameters!)
+            }
+            clipTotalDuration = clipTotalDuration + Float(row)
+            
+        }
+        BFLog(message: "拼接的音频总时长: \(clipTotalDuration)")
+
+        return tempParameters
+    }
+
+    /// 处理背景音乐音轨
+    /// - Parameters:
+    ///   - stickerModel: <#stickerModel description#>
+    ///   - composition: <#composition description#>
+    /// - Returns: <#description#>
+    public class func dealWithBGMTrack(bgmData: PQVoiceModel, totalDuration: Float64, composition: AVMutableComposition) -> [AVMutableAudioMixInputParameters] {
+        var tempParameters: [AVMutableAudioMixInputParameters] = [AVMutableAudioMixInputParameters].init()
+        let bgmAsset = AVURLAsset(url: URL(fileURLWithPath: bgmData.localPath ?? ""), options: avAssertOptions)
+        let volume = Float(bgmData.volume) / 100.0
+        let bgmDuration = (Float64(bgmData.duration ?? "0") ?? 0) - bgmData.startTime
+        BFLog(message: "处理背景音乐:startTime = \(bgmData.startTime),bgmDuration = \(bgmDuration),totalDuration = \(totalDuration)")
+
+        if bgmDuration < totalDuration {
+            let count = Int(totalDuration) / Int(bgmDuration)
+            let row = totalDuration - Double(count) * bgmDuration
+            if count > 0 {
+                for index in 0 ..< count {
+                    let startTime = CMTime(value: CMTimeValue(bgmDuration * Double(index) * Double(playerTimescaleInt)), timescale: playerTimescaleInt)
+                    let trackTimeRange = CMTimeRange(start: CMTime(value: CMTimeValue(bgmData.startTime * Double(playerTimescaleInt)), timescale: playerTimescaleInt), end: CMTime(value: CMTimeValue((bgmData.startTime + bgmDuration) * Double(playerTimescaleInt)), timescale: playerTimescaleInt))
+                    BFLog(message: "背景音乐时长短:count = \(count),startTime = \(startTime),trackTimeRange = \(trackTimeRange)")
+                    let parameters = mixAudioTrack(audioAsset: bgmAsset, startTime: startTime, trackTimeRange: trackTimeRange, volume: volume, composition: composition)
+                    if parameters != nil {
+                        tempParameters.append(parameters!)
+                    }
+                }
+            }
+            if row > 0 {
+                let startTime = CMTime(value: CMTimeValue(bgmDuration * Double(count) * Double(playerTimescaleInt)), timescale: playerTimescaleInt)
+                let trackTimeRange = CMTimeRange(start: CMTime(value: CMTimeValue(bgmData.startTime * Double(playerTimescaleInt)), timescale: playerTimescaleInt), end: CMTime(value: CMTimeValue((bgmData.startTime + row) * Double(playerTimescaleInt)), timescale: playerTimescaleInt))
+                BFLog(message: "背景音乐时长短:count = \(count),startTime = \(startTime),trackTimeRange = \(trackTimeRange)")
+                let parameters = mixAudioTrack(audioAsset: bgmAsset, startTime: startTime, trackTimeRange: trackTimeRange, volume: volume, composition: composition)
+                if parameters != nil {
+                    tempParameters.append(parameters!)
+                }
+            }
+        } else {
+            let trackTimeRange = CMTimeRange(start: CMTime(value: CMTimeValue(bgmData.startTime * Double(playerTimescaleInt)), timescale: playerTimescaleInt), end: CMTime(value: CMTimeValue((bgmData.startTime + totalDuration) * Double(playerTimescaleInt)), timescale: playerTimescaleInt))
+            BFLog(message: "背景音乐时长长:trackTimeRange = \(trackTimeRange)")
+            let bgmParameters = mixAudioTrack(audioAsset: bgmAsset, trackTimeRange: trackTimeRange, volume: volume, composition: composition)
+            if bgmParameters != nil {
+                tempParameters.append(bgmParameters!)
+            }
+        }
+        return tempParameters
+    }
+
+    /// 处理素材音轨
+    /// - Parameters:
+    ///   - stickerModel: <#stickerModel description#>
+    ///   - composition: <#composition description#>
+    /// - Returns: <#description#>
+    public class func dealWithMaterialTrack(stickerModel: PQEditVisionTrackMaterialsModel, composition: AVMutableComposition) -> [AVMutableAudioMixInputParameters] {
+        var tempParameters: [AVMutableAudioMixInputParameters] = [AVMutableAudioMixInputParameters].init()
+        let audioAsset = AVURLAsset(url: URL(fileURLWithPath: documensDirectory + stickerModel.locationPath), options: avAssertOptions)
+        let volume = Float(stickerModel.volumeGain) / 100
+        let rangeStart = stickerModel.model_in
+        var rangeEnd = stickerModel.out
+        if rangeEnd == 0 {
+            rangeEnd = audioAsset.duration.seconds
+        }
+        var originDuration = (rangeEnd - rangeStart)
+        if stickerModel.aptDuration < originDuration {
+            originDuration = stickerModel.aptDuration
+        }
+
+        if stickerModel.aptDuration > originDuration, stickerModel.materialDurationFit?.fitType == adapterMode.loopAuto.rawValue {
+            let count = originDuration == 0 ? 0 : Int(stickerModel.aptDuration) / Int(originDuration)
+            let row = stickerModel.aptDuration - Double(count) * originDuration
+            if count > 0 {
+                for index in 0 ..< count {
+                    let startTime = CMTime(value: CMTimeValue((stickerModel.timelineIn + originDuration * Double(index)) * Double(playerTimescaleInt)), timescale: playerTimescaleInt)
+                    let trackTimeRange = CMTimeRange(start: CMTime(value: CMTimeValue(rangeStart * Double(playerTimescaleInt)), timescale: playerTimescaleInt), end: CMTime(value: CMTimeValue((rangeStart + originDuration) * Double(playerTimescaleInt)), timescale: playerTimescaleInt))
+                    let parameters = mixAudioTrack(audioAsset: audioAsset, startTime: startTime, trackTimeRange: trackTimeRange, volume: volume, composition: composition)
+                    if parameters != nil {
+                        tempParameters.append(parameters!)
+                    }
+                }
+            }
+            if row > 0 {
+                let startTime = CMTime(value: CMTimeValue((stickerModel.timelineIn + originDuration * Double(count)) * Double(playerTimescaleInt)), timescale: playerTimescaleInt)
+                let trackTimeRange = CMTimeRange(start: CMTime(value: CMTimeValue(rangeStart * Double(playerTimescaleInt)), timescale: playerTimescaleInt), end: CMTime(value: CMTimeValue((rangeStart + row) * Double(playerTimescaleInt)), timescale: playerTimescaleInt))
+                let parameters = mixAudioTrack(audioAsset: audioAsset, startTime: startTime, trackTimeRange: trackTimeRange, volume: volume, composition: composition)
+                if parameters != nil {
+                    tempParameters.append(parameters!)
+                }
+            }
+        } else {
+            let startTime = CMTime(value: CMTimeValue(stickerModel.timelineIn * Double(playerTimescaleInt)), timescale: playerTimescaleInt)
+            let trackTimeRange = CMTimeRange(start: CMTime(value: CMTimeValue(rangeStart * Double(playerTimescaleInt)), timescale: playerTimescaleInt), end: CMTime(value: CMTimeValue((rangeStart + originDuration) * Double(playerTimescaleInt)), timescale: playerTimescaleInt))
+            let parameters = mixAudioTrack(audioAsset: audioAsset, startTime: startTime, trackTimeRange: trackTimeRange, volume: volume, composition: composition)
+            if parameters != nil {
+                tempParameters.append(parameters!)
+            }
+        }
+        return tempParameters
+    }
+
+    /// 混音添加音轨
+    /// - Parameters:
+    ///   - audioAsset: 素材资源
+    ///   - startTime: 从什么时间开始播放
+    ///   - trackTimeRange: 播放素材范围
+    ///   - volume:音轨音量
+    ///   - composition: <#composition description#>
+    /// - Returns: <#description#>
+    public class func mixAudioTrack(audioAsset: AVURLAsset, startTime: CMTime = CMTime.zero, trackTimeRange: CMTimeRange, volume: Float = 1, composition: AVMutableComposition) -> AVMutableAudioMixInputParameters? {
+        BFLog(message: "startTime = \(startTime),trackTimeRange = \(trackTimeRange)")
+        // 第一个音轨
+        // let assetTrack : AVAssetTrack? = audioAsset.tracks(withMediaType: .audio).first
+        // 所有音轨
+        let assetTracks: [AVAssetTrack]? = audioAsset.tracks(withMediaType: .audio)
+        if assetTracks != nil, (assetTracks?.count ?? 0) > 0 {
+            let audioTrack: AVMutableCompositionTrack? = composition.addMutableTrack(withMediaType: .audio, preferredTrackID: kCMPersistentTrackID_Invalid)
+            let mixInputParameters = AVMutableAudioMixInputParameters(track: audioTrack)
+            mixInputParameters.setVolume(volume, at: startTime)
+            do {
+                // 第一个音轨插入到原音的开始和结束位置
+                // try audioTrack?.insertTimeRange(trackTimeRange, of: assetTrack!, at: startTime)
+                // 所有音轨插入到原音的开始和结束位置
+                let timeRanges = Array(repeating: NSValue(timeRange: trackTimeRange), count: assetTracks!.count)
+                try audioTrack?.insertTimeRanges(timeRanges, of: assetTracks!, at: startTime)
+            } catch {
+                BFLog(message: "error is \(error)")
+            }
+            return mixInputParameters
+        }
+        return nil
+    }
+
+    // 导出音频
+    /// - Parameter comosition: <#comosition description#>
+    /// - Returns: <#description#>
+    public class func exportAudio(comosition: AVAsset) {
+        let outPutFilePath = URL(fileURLWithPath: tempDirectory + "/temp.mp4")
+        // 删除以创建地址
+        try? FileManager.default.removeItem(at: outPutFilePath)
+        let assetExport = AVAssetExportSession(asset: comosition, presetName: AVAssetExportPresetMediumQuality)
+        assetExport?.outputFileType = .mp4
+        assetExport?.outputURL = outPutFilePath
+        assetExport?.exportAsynchronously(completionHandler: {
+            print("assetExport == \(assetExport?.status.rawValue ?? 0),error = \(String(describing: assetExport?.error))")
+            DispatchQueue.main.async {}
+        })
+    }
+}

+ 30 - 0
BFStuckPointKit/Classes/ViewModel/PQStuckPointMusciTagsFlowLayout.swift

@@ -0,0 +1,30 @@
+//
+//  PQStuckPointMusciTagsFlowLayout.swift
+//  PQSpeed
+//
+//  Created by SanW on 2021/4/30.
+//  Copyright © 2021 BytesFlow. All rights reserved.
+//
+
+import UIKit
+
+class PQStuckPointMusciTagsFlowLayout: UICollectionViewFlowLayout {
+    // 整个高度
+    var maxH: CGFloat = 0
+    // 所有item的属性
+    var layoutAttributesArray = [UICollectionViewLayoutAttributes]()
+
+    override func layoutAttributesForElements(in _: CGRect) -> [UICollectionViewLayoutAttributes]? {
+        return layoutAttributesArray
+    }
+
+    /// 重写设置contentSize
+    override var collectionViewContentSize: CGSize {
+        get {
+            return CGSize(width: (collectionView?.bounds.width)!, height: maxH)
+        }
+        set {
+            self.collectionViewContentSize = newValue
+        }
+    }
+}

+ 163 - 0
BFStuckPointKit/Classes/ViewModel/PQStuckPointViewModel.swift

@@ -0,0 +1,163 @@
+//
+//  PQStuckPointViewModel.swift
+//  PQSpeed
+//
+//  Created by SanW on 2021/4/28.
+//  Copyright © 2021 BytesFlow. All rights reserved.
+//
+
+import UIKit
+import BFCommonKit
+import BFNetRequestKit
+
+public class PQStuckPointViewModel: NSObject {
+    /// 获取卡点音乐分类列表
+    /// - Parameters:
+    /// 通过 parentTagId 来区分层级,第一级会带上 '热门' 标签音乐列表,获取第二级标签列表会带上所选第二级的 '全部' 标签音乐列表
+    ///   - parentTagId: 标签父级 ID(默认 0 第一层)
+    ///   - pageSize: 音乐列表每页个数(默认 20)
+    ///   - complateHandle: <#complateHandle description#>
+    class func stuckPointMusicCategoryList(parentTagId: Int64 = 0, pageSize _: Int = 20, complateHandle: @escaping (_ categoryList: [PQStuckPointMusicTagsModel], _ msg: String?, _ tagAttributes: ([UICollectionViewLayoutAttributes], CGFloat)?) -> Void) {
+        BFNetRequestAdaptor.postRequestData(url: PQENVUtil.shared.longvideoapi + stuckPointMusicCategoryUrl, parames: ["parentTagId": parentTagId], commonParams: commonParams()) { response, _, error, _ in
+            var tagsList = Array<PQStuckPointMusicTagsModel>.init()
+            if response is NSNull || response == nil {
+                complateHandle(tagsList, error?.msg, nil)
+                return
+            }
+            let tagsTempArr = response as? [[String: Any]]
+            if tagsTempArr != nil, (tagsTempArr?.count ?? 0) > 0 {
+                for dict in tagsTempArr! {
+                    let tempMusic = PQStuckPointMusicTagsModel(jsonDict: dict)
+                    tempMusic.parentTagId = parentTagId
+                    tagsList.append(tempMusic)
+                }
+            }
+            tagsList.first?.isSelected = true
+            complateHandle(tagsList, nil, (parentTagId != 0) ? PQStuckPointViewModel.dealWithAttributesWithTags(tagsList: tagsList) : nil)
+        }
+    }
+
+    /// 计算标签frame
+    /// - Parameter tagsList: <#tagsList description#>
+    /// - Returns: <#description#>
+    class func dealWithAttributesWithTags(tagsList: [PQStuckPointMusicTagsModel]) -> ([UICollectionViewLayoutAttributes], CGFloat) {
+        var attributesArray = [UICollectionViewLayoutAttributes]()
+        // 遍历数据计算每个item的属性并布局
+        let lineSpace: CGFloat = 8
+        let itemSpace: CGFloat = 8
+        var currtentX: CGFloat = 0
+        var currtentY: CGFloat = 0
+        let itemHeight: CGFloat = cDefaultMargin * 3
+        let maxWidth: CGFloat = cScreenWidth - cDefaultMargin * 12 - 32
+        for (index, data) in tagsList.enumerated() {
+            let indexPath = IndexPath(item: index, section: 0)
+            let attributes = UICollectionViewLayoutAttributes(forCellWith: indexPath)
+            var width: CGFloat = sizeWithText(text: "  \(data.tagName ?? "")  ", font: UIFont.systemFont(ofSize: 12), size: CGSize(width: maxWidth, height: itemHeight)).width
+            if width > maxWidth {
+                width = maxWidth
+            }
+            if (currtentX + width + itemSpace) > maxWidth {
+                currtentY = currtentY + lineSpace + itemHeight
+                currtentX = 0
+            }
+            attributes.frame = CGRect(x: currtentX, y: currtentY, width: width, height: itemHeight)
+            currtentX = currtentX + itemSpace + width
+            attributesArray.append(attributes)
+        }
+        return (attributesArray, attributesArray.count > 0 ? currtentY + lineSpace + itemHeight : 0)
+    }
+
+    /// 卡点音乐某个分类下列表
+    /// - Parameters:
+    ///   - tagId: 标签 ID
+    ///   - parentTagId: 标签父级 ID
+    ///   - pageNum: 音乐列表页码(默认 1)
+    ///   - pageSize: 音乐列表每页个数(默认 20)
+    ///   - videoCount: 选择视频素材个数
+    ///   - imageCount: 选择图片素材个数
+    ///   - totalDuration: 选择素材总时长
+    ///   - complateHandle: <#complateHandle description#>
+    /// - Returns: <#description#>
+    class func stuckPointMusicPageList(tagId: Int64, parentTagId: Int64 = 0, pageNum: Int = 1, pageSize: Int = 20,videoCount: Int = 0, imageCount: Int = 0, totalDuration: Float64 = 0, oldDataMusic:[PQVoiceModel]? = nil, complateHandle: @escaping (_ musicPageList: [PQVoiceModel], _ msg: String?) -> Void) {
+        BFNetRequestAdaptor.postRequestData(url: PQENVUtil.shared.longvideoapi + stuckPointMusicPageUrl, parames: ["tagId": tagId, "parentTagId": parentTagId, "pageNum": pageNum, "pageSize": pageSize], commonParams: commonParams()) { response, _, error, _ in
+            var musicPageList = Array<PQVoiceModel>.init()
+            if response is NSNull || response == nil {
+                complateHandle(musicPageList, error?.msg)
+                return
+            }
+            let tempArr = response as? [[String: Any]]
+            if tempArr != nil, (tempArr?.count ?? 0) > 0 {
+                for (_,dict) in tempArr!.enumerated() {
+                    let tempMusic = PQVoiceModel(jsonDict: dict)
+                    tempMusic.cacheTagID = tagId
+                    if tempMusic.rhythmSdata.count > 0 && (videoCount > 0 || imageCount > 0 ||  totalDuration > 0)  {
+//                        tempMusic.endTime = tempMusic.startTime + tempMusic.stuckPointCuttingTime(videoCount: videoCount, imageCount: imageCount, totalDuration: totalDuration)
+                        BFLog(message: "music:\(tempMusic.musicName ?? ""),\(tempMusic.startTime),\(tempMusic.endTime)")
+                    }
+                    if tempMusic.endTime <= tempMusic.startTime {
+                        tempMusic.endTime = tempMusic.startTime + 40
+                    }
+                    
+                    let haveIndex = oldDataMusic?.firstIndex(where: { (music) -> Bool in
+                        (music.musicId == tempMusic.musicId)
+                    })
+                    if(haveIndex == nil){
+                        musicPageList.append(tempMusic)
+                    }
+    
+                }
+            }
+            complateHandle(musicPageList, nil)
+        }
+    }
+
+    /// 获取某个音乐的卡点数据
+    /// - Parameters:
+    ///   - musicId: <#musicId description#>
+    ///   - originType:音乐来源:1 上传 2 爬取
+    ///   - complateHandle: <#complateHandle description#>
+    /// - Returns: description
+    class func stuckPointMusicDetailData(musicId: String, originType: Int, complateHandle: @escaping (_ musicDetaiData: PQVoiceModel?, _ msg: String?) -> Void) {
+        BFNetRequestAdaptor.postRequestData(url: PQENVUtil.shared.longvideoapi + stuckPointMusicDetailUrl, parames: ["musicId": musicId, "originType": originType], commonParams: commonParams()) { response, _, error, _ in
+            if response is NSNull || response == nil {
+                complateHandle(nil, error?.msg)
+                return
+            }
+            let tempDict = (response as? [String: Any])
+            if tempDict != nil, (tempDict?.keys.count ?? 0) > 0 {
+                complateHandle(PQVoiceModel(jsonDict: tempDict!), nil)
+            } else {
+                complateHandle(nil, nil)
+            }
+        }
+    }
+
+    /// 请求再创作项目信息
+    /// - Parameter projectId: 项目id
+    /// - Returns: <#description#>
+    class public func stuckPointProjectMusicInfo(projectId: String, complateHandle: @escaping (_ musicDetaiData: PQVoiceModel?, _ msg: String?) -> Void) {
+        BFNetRequestAdaptor.postRequestData(url: PQENVUtil.shared.longvideoapi + stuckPointProjectMusicInfoUrl, parames: ["projectId": projectId], commonParams: commonParams()) { response, _, error, _ in
+            if response is NSNull || response == nil {
+                complateHandle(nil, error?.msg)
+                return
+            }
+            let tempDict = (response as? [String: Any])
+            if tempDict != nil, tempDict?.keys.contains("rhythmMusicData") ?? false {
+                let musicData: PQVoiceModel = PQVoiceModel(jsonDict: tempDict?["rhythmMusicData"] as? [String: Any] ?? [:])
+                if tempDict?.keys.contains("rhythmMusicIn") ?? false {
+                    musicData.rhythmMusicIn = (Float64("\(tempDict?["rhythmMusicIn"] ?? "0")") ?? 0) / 1_000_000
+                }
+                if tempDict?.keys.contains("rhythmMusicOut") ?? false {
+                    musicData.rhythmMusicOut = (Float64("\(tempDict?["rhythmMusicOut"] ?? "0")") ?? 0) / 1_000_000
+                }
+                if (tempDict?.keys.contains("rhythmMusicSpeed") ?? false) && "\(tempDict?["rhythmMusicSpeed"] ?? "")" != "<null>"  {
+                    musicData.speed = Int("\(tempDict?["rhythmMusicSpeed"] ?? "2")") ?? 2
+                }
+                musicData.originProjectId = projectId
+                complateHandle(musicData, nil)
+            } else {
+                complateHandle(nil, nil)
+            }
+        }
+    }
+}

+ 57 - 3
Example/BFStuckPointKit.xcodeproj/project.pbxproj

@@ -171,6 +171,7 @@
 				607FACCD1AFB9204008FA782 /* Frameworks */,
 				607FACCE1AFB9204008FA782 /* Resources */,
 				248D719BCE4360CF0AD34334 /* [CP] Embed Pods Frameworks */,
+				2ADEFE6F3D6034BB2AF009C6 /* [CP] Copy Pods Resources */,
 			);
 			buildRules = (
 			);
@@ -206,9 +207,10 @@
 		607FACC81AFB9204008FA782 /* Project object */ = {
 			isa = PBXProject;
 			attributes = {
+				CLASSPREFIX = BF;
 				LastSwiftUpdateCheck = 0830;
 				LastUpgradeCheck = 0830;
-				ORGANIZATIONNAME = CocoaPods;
+				ORGANIZATIONNAME = BytesFlow;
 				TargetAttributes = {
 					607FACCF1AFB9204008FA782 = {
 						CreatedOnToolsVersion = 6.3.1;
@@ -269,17 +271,69 @@
 			);
 			inputPaths = (
 				"${PODS_ROOT}/Target Support Files/Pods-BFStuckPointKit_Example/Pods-BFStuckPointKit_Example-frameworks.sh",
-				"${BUILT_PRODUCTS_DIR}/BFStuckPointKit/BFStuckPointKit.framework",
+				"${BUILT_PRODUCTS_DIR}/Alamofire/Alamofire.framework",
+				"${BUILT_PRODUCTS_DIR}/BFCommonKit/BFCommonKit.framework",
+				"${BUILT_PRODUCTS_DIR}/BFNetRequestKit/BFNetRequestKit.framework",
+				"${BUILT_PRODUCTS_DIR}/BFUIKit/BFUIKit.framework",
+				"${BUILT_PRODUCTS_DIR}/FDFullscreenPopGesture/FDFullscreenPopGesture.framework",
+				"${BUILT_PRODUCTS_DIR}/KeychainAccess/KeychainAccess.framework",
+				"${BUILT_PRODUCTS_DIR}/Kingfisher/Kingfisher.framework",
+				"${BUILT_PRODUCTS_DIR}/KingfisherWebP/KingfisherWebP.framework",
+				"${BUILT_PRODUCTS_DIR}/MGSwipeTableCell/MGSwipeTableCell.framework",
+				"${BUILT_PRODUCTS_DIR}/MJRefresh/MJRefresh.framework",
+				"${BUILT_PRODUCTS_DIR}/Realm/Realm.framework",
+				"${BUILT_PRODUCTS_DIR}/RealmSwift/RealmSwift.framework",
+				"${BUILT_PRODUCTS_DIR}/SVProgressHUD/SVProgressHUD.framework",
+				"${BUILT_PRODUCTS_DIR}/SnapKit/SnapKit.framework",
+				"${BUILT_PRODUCTS_DIR}/Toast-Swift/Toast_Swift.framework",
+				"${BUILT_PRODUCTS_DIR}/libwebp/libwebp.framework",
 			);
 			name = "[CP] Embed Pods Frameworks";
 			outputPaths = (
-				"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/BFStuckPointKit.framework",
+				"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Alamofire.framework",
+				"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/BFCommonKit.framework",
+				"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/BFNetRequestKit.framework",
+				"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/BFUIKit.framework",
+				"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/FDFullscreenPopGesture.framework",
+				"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/KeychainAccess.framework",
+				"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Kingfisher.framework",
+				"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/KingfisherWebP.framework",
+				"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/MGSwipeTableCell.framework",
+				"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/MJRefresh.framework",
+				"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Realm.framework",
+				"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/RealmSwift.framework",
+				"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/SVProgressHUD.framework",
+				"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/SnapKit.framework",
+				"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Toast_Swift.framework",
+				"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/libwebp.framework",
 			);
 			runOnlyForDeploymentPostprocessing = 0;
 			shellPath = /bin/sh;
 			shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-BFStuckPointKit_Example/Pods-BFStuckPointKit_Example-frameworks.sh\"\n";
 			showEnvVarsInLog = 0;
 		};
+		2ADEFE6F3D6034BB2AF009C6 /* [CP] Copy Pods Resources */ = {
+			isa = PBXShellScriptBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+			);
+			inputPaths = (
+				"${PODS_ROOT}/Target Support Files/Pods-BFStuckPointKit_Example/Pods-BFStuckPointKit_Example-resources.sh",
+				"${PODS_CONFIGURATION_BUILD_DIR}/BFMaterialKit/BFMaterialKit_Resources.bundle",
+				"${PODS_CONFIGURATION_BUILD_DIR}/BFMediaKit/BFMediaKit_Resources.bundle",
+				"${PODS_CONFIGURATION_BUILD_DIR}/BFStuckPointKit/BFStuckPointKit_Resources.bundle",
+			);
+			name = "[CP] Copy Pods Resources";
+			outputPaths = (
+				"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/BFMaterialKit_Resources.bundle",
+				"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/BFMediaKit_Resources.bundle",
+				"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/BFStuckPointKit_Resources.bundle",
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+			shellPath = /bin/sh;
+			shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-BFStuckPointKit_Example/Pods-BFStuckPointKit_Example-resources.sh\"\n";
+			showEnvVarsInLog = 0;
+		};
 		4A5558C015D9E393993AF4DB /* [CP] Check Pods Manifest.lock */ = {
 			isa = PBXShellScriptBuildPhase;
 			buildActionMask = 2147483647;

+ 1 - 1
Example/Podfile

@@ -8,7 +8,7 @@ target 'BFStuckPointKit_Example' do
   pod 'BFNetRequestKit',        :path => '../../../BFNetRequestKit/Trunk'
   pod 'BFUIKit',                :path => '../../../BFUIKit/Trunk'
   pod 'BFMaterialKit',          :path => '../../../BFMaterialKit/Trunk'
-  
+  pod 'BFMediaKit',             :path => '../../../BFMediaKit/Trunk'
   target 'BFStuckPointKit_Tests' do
     inherit! :search_paths
 

+ 146 - 3
Example/Podfile.lock

@@ -1,16 +1,159 @@
 PODS:
-  - BFStuckPointKit (0.1.0)
+  - Alamofire (5.4.4)
+  - BFCommonKit (1.5.2):
+    - BFCommonKit/BFCategorys (= 1.5.2)
+    - BFCommonKit/BFConfig (= 1.5.2)
+    - BFCommonKit/BFEnums (= 1.5.2)
+    - BFCommonKit/BFUtility (= 1.5.2)
+  - BFCommonKit/BFCategorys (1.5.2):
+    - KingfisherWebP (= 1.3.0)
+  - BFCommonKit/BFConfig (1.5.2):
+    - BFCommonKit/BFCategorys
+    - BFCommonKit/BFEnums
+  - BFCommonKit/BFEnums (1.5.2)
+  - BFCommonKit/BFUtility (1.5.2):
+    - Alamofire (= 5.4.4)
+    - BFCommonKit/BFCategorys
+    - BFCommonKit/BFConfig
+    - KeychainAccess (= 4.2.2)
+    - Kingfisher (= 6.3.1)
+    - KingfisherWebP (= 1.3.0)
+    - Toast-Swift (= 5.0.1)
+  - BFMaterialKit (0.2.0):
+    - BFUIKit
+  - BFMediaKit (0.1.0)
+  - BFNetRequestKit (1.0.1):
+    - Alamofire (= 5.4.4)
+  - BFStuckPointKit (0.1.0):
+    - BFCommonKit
+    - BFMaterialKit
+    - BFMediaKit
+    - BFNetRequestKit
+    - BFUIKit
+  - BFUIKit (0.1.2):
+    - BFCommonKit
+    - BFUIKit/Comm (= 0.1.2)
+    - BFUIKit/Controller (= 0.1.2)
+    - BFUIKit/View (= 0.1.2)
+    - FDFullscreenPopGesture (= 1.1)
+    - Kingfisher (~> 6.0)
+    - MGSwipeTableCell (~> 1.0)
+    - MJRefresh (~> 3.0)
+    - RealmSwift (= 10.7.6)
+    - SnapKit (~> 5.0)
+    - SVProgressHUD (~> 2.0)
+  - BFUIKit/Comm (0.1.2):
+    - BFCommonKit
+    - FDFullscreenPopGesture (= 1.1)
+    - Kingfisher (~> 6.0)
+    - MGSwipeTableCell (~> 1.0)
+    - MJRefresh (~> 3.0)
+    - RealmSwift (= 10.7.6)
+    - SnapKit (~> 5.0)
+    - SVProgressHUD (~> 2.0)
+  - BFUIKit/Controller (0.1.2):
+    - BFCommonKit
+    - FDFullscreenPopGesture (= 1.1)
+    - Kingfisher (~> 6.0)
+    - MGSwipeTableCell (~> 1.0)
+    - MJRefresh (~> 3.0)
+    - RealmSwift (= 10.7.6)
+    - SnapKit (~> 5.0)
+    - SVProgressHUD (~> 2.0)
+  - BFUIKit/View (0.1.2):
+    - BFCommonKit
+    - FDFullscreenPopGesture (= 1.1)
+    - Kingfisher (~> 6.0)
+    - MGSwipeTableCell (~> 1.0)
+    - MJRefresh (~> 3.0)
+    - RealmSwift (= 10.7.6)
+    - SnapKit (~> 5.0)
+    - SVProgressHUD (~> 2.0)
+  - FDFullscreenPopGesture (1.1)
+  - KeychainAccess (4.2.2)
+  - Kingfisher (6.3.1)
+  - KingfisherWebP (1.3.0):
+    - Kingfisher (~> 6.2)
+    - libwebp (>= 1.1.0)
+  - libwebp (1.2.1):
+    - libwebp/demux (= 1.2.1)
+    - libwebp/mux (= 1.2.1)
+    - libwebp/webp (= 1.2.1)
+  - libwebp/demux (1.2.1):
+    - libwebp/webp
+  - libwebp/mux (1.2.1):
+    - libwebp/demux
+  - libwebp/webp (1.2.1)
+  - MGSwipeTableCell (1.6.11)
+  - MJRefresh (3.7.2)
+  - Realm (10.7.6):
+    - Realm/Headers (= 10.7.6)
+  - Realm/Headers (10.7.6)
+  - RealmSwift (10.7.6):
+    - Realm (= 10.7.6)
+  - SnapKit (5.0.1)
+  - SVProgressHUD (2.2.5)
+  - Toast-Swift (5.0.1)
 
 DEPENDENCIES:
+  - BFCommonKit (from `../../../BFCommonKit/Trunk`)
+  - BFMaterialKit (from `../../../BFMaterialKit/Trunk`)
+  - BFMediaKit (from `../../../BFMediaKit/Trunk`)
+  - BFNetRequestKit (from `../../../BFNetRequestKit/Trunk`)
   - BFStuckPointKit (from `../`)
+  - BFUIKit (from `../../../BFUIKit/Trunk`)
+
+SPEC REPOS:
+  trunk:
+    - Alamofire
+    - FDFullscreenPopGesture
+    - KeychainAccess
+    - Kingfisher
+    - KingfisherWebP
+    - libwebp
+    - MGSwipeTableCell
+    - MJRefresh
+    - Realm
+    - RealmSwift
+    - SnapKit
+    - SVProgressHUD
+    - Toast-Swift
 
 EXTERNAL SOURCES:
+  BFCommonKit:
+    :path: "../../../BFCommonKit/Trunk"
+  BFMaterialKit:
+    :path: "../../../BFMaterialKit/Trunk"
+  BFMediaKit:
+    :path: "../../../BFMediaKit/Trunk"
+  BFNetRequestKit:
+    :path: "../../../BFNetRequestKit/Trunk"
   BFStuckPointKit:
     :path: "../"
+  BFUIKit:
+    :path: "../../../BFUIKit/Trunk"
 
 SPEC CHECKSUMS:
-  BFStuckPointKit: 88dceb906c9cfbc3a95e6f6e1ee8f13d5e5b1dc1
+  Alamofire: f3b09a368f1582ab751b3fff5460276e0d2cf5c9
+  BFCommonKit: a730c6fb330ac0521a691c7da6d7a4fe764e2767
+  BFMaterialKit: 0a15786e2a55587f1b2b4b74c0bff5321ebf3630
+  BFMediaKit: 43941454b03463ad423f4cb30fc73ae7aed94679
+  BFNetRequestKit: 1d074023eafe7c272fab4ed3a608e685902235d0
+  BFStuckPointKit: bc8a8dba6ddab6aeccd375d0e697aae27d224d20
+  BFUIKit: f209190fb92c8f9050554ac5950a2e4852e8a481
+  FDFullscreenPopGesture: a8a620179e3d9c40e8e00256dcee1c1a27c6d0f0
+  KeychainAccess: c0c4f7f38f6fc7bbe58f5702e25f7bd2f65abf51
+  Kingfisher: 016c8b653a35add51dd34a3aba36b580041acc74
+  KingfisherWebP: dec17a5eb1af2658791bde1f93ae9a853678f826
+  libwebp: 98a37e597e40bfdb4c911fc98f2c53d0b12d05fc
+  MGSwipeTableCell: b804e4e450dee439c42250be90bd50458bf67fce
+  MJRefresh: 30997d30b347c8e9508a4db11e3a690da0c9b85a
+  Realm: ed860452717c8db8f4bf832b6807f7f2ce708839
+  RealmSwift: e31c4ddbcc42ac879313d656b86f9ca539f6f4f4
+  SnapKit: 97b92857e3df3a0c71833cce143274bf6ef8e5eb
+  SVProgressHUD: 1428aafac632c1f86f62aa4243ec12008d7a51d6
+  Toast-Swift: 9b6a70f28b3bf0b96c40d46c0c4b9d6639846711
 
-PODFILE CHECKSUM: 6412502f11480e3867515908964b24f66a8fcacf
+PODFILE CHECKSUM: 59f2edd1cfd880b4f00e01a26d248091a6751849
 
 COCOAPODS: 1.11.2