wenweiwei há 3 anos atrás
pai
commit
b54effb736
31 ficheiros alterados com 4011 adições e 405 exclusões
  1. BIN
      BFStuckPointKit/Assets/Resources/editCoverPin.gif
  2. BIN
      BFStuckPointKit/Assets/Resources/endMovieA.mp4
  3. BIN
      BFStuckPointKit/Assets/Resources/endMovieB.mp4
  4. BIN
      BFStuckPointKit/Assets/Resources/endMovieSound.mp3
  5. BIN
      BFStuckPointKit/Assets/Resources/msg_video_tag.png
  6. BIN
      BFStuckPointKit/Assets/Resources/speedstuck_h.gif
  7. BIN
      BFStuckPointKit/Assets/Resources/speedstuck_n.gif
  8. BIN
      BFStuckPointKit/Assets/Resources/stuckPoint_edit_loading.gif
  9. BIN
      BFStuckPointKit/Assets/Resources/stuckPoint_music_playing.gif
  10. BIN
      BFStuckPointKit/Assets/Resources/user_avatar_normal.png
  11. BIN
      BFStuckPointKit/Assets/Resources/watermark.png
  12. 340 0
      BFStuckPointKit/Classes/EventTrack/Model/PQVideoMakeEventTrackModel.swift
  13. 550 0
      BFStuckPointKit/Classes/EventTrack/ViewModel/PQEventTrackViewModel.swift
  14. 26 0
      BFStuckPointKit/Classes/Extensions/OperationQueue+Ext.swift
  15. 39 0
      BFStuckPointKit/Classes/Extensions/Task+Ext.swift
  16. 82 0
      BFStuckPointKit/Classes/Model/PQReCreateModel.swift
  17. 0 48
      BFStuckPointKit/Classes/Model/PQStuckPointMusicTagsModel.swift
  18. 0 28
      BFStuckPointKit/Classes/Model/PQStuckPointTimesModel.swift
  19. 33 0
      BFStuckPointKit/Classes/Model/PQUploadModel.swift
  20. 0 329
      BFStuckPointKit/Classes/Model/PQVoiceModel.swift
  21. 258 0
      BFStuckPointKit/Classes/SelectImage/PQImageCropVC.swift
  22. 52 0
      BFStuckPointKit/Classes/SelectImage/PQImageSelectedController.swift
  23. 130 0
      BFStuckPointKit/Classes/SelectImage/PQSelecteVideoItemCell.swift
  24. 992 0
      BFStuckPointKit/Classes/SelectImage/PQUploadController.swift
  25. 106 0
      BFStuckPointKit/Classes/View/PQAssetCategoryCell.swift
  26. 82 0
      BFStuckPointKit/Classes/View/PQBaseVideoInfoView.swift
  27. 669 0
      BFStuckPointKit/Classes/ViewModel/PQBaseViewModel.swift
  28. 76 0
      BFStuckPointKit/Classes/ViewModel/PQDownloadFileManager.swift
  29. 227 0
      BFStuckPointKit/Classes/ViewModel/PQDownloadManager.swift
  30. 190 0
      BFStuckPointKit/Classes/ViewModel/PQSessionManager.swift
  31. 159 0
      BFStuckPointKit/Classes/ViewModel/PQUploadViewModel.swift

BIN
BFStuckPointKit/Assets/Resources/editCoverPin.gif


BIN
BFStuckPointKit/Assets/Resources/endMovieA.mp4


BIN
BFStuckPointKit/Assets/Resources/endMovieB.mp4


BIN
BFStuckPointKit/Assets/Resources/endMovieSound.mp3


BIN
BFStuckPointKit/Assets/Resources/msg_video_tag.png


BIN
BFStuckPointKit/Assets/Resources/speedstuck_h.gif


BIN
BFStuckPointKit/Assets/Resources/speedstuck_n.gif


BIN
BFStuckPointKit/Assets/Resources/stuckPoint_edit_loading.gif


BIN
BFStuckPointKit/Assets/Resources/stuckPoint_music_playing.gif


BIN
BFStuckPointKit/Assets/Resources/user_avatar_normal.png


BIN
BFStuckPointKit/Assets/Resources/watermark.png


+ 340 - 0
BFStuckPointKit/Classes/EventTrack/Model/PQVideoMakeEventTrackModel.swift

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

+ 550 - 0
BFStuckPointKit/Classes/EventTrack/ViewModel/PQEventTrackViewModel.swift

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

+ 26 - 0
BFStuckPointKit/Classes/Extensions/OperationQueue+Ext.swift

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

+ 39 - 0
BFStuckPointKit/Classes/Extensions/Task+Ext.swift

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

+ 82 - 0
BFStuckPointKit/Classes/Model/PQReCreateModel.swift

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

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

@@ -1,48 +0,0 @@
-//
-//  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"] ?? "")"
-        }
-    }
-}

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

@@ -1,28 +0,0 @@
-//
-//  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]) ?? []
-        }
-    }
-}

+ 33 - 0
BFStuckPointKit/Classes/Model/PQUploadModel.swift

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

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

@@ -1,329 +0,0 @@
-//
-//  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
-    }
-}

+ 258 - 0
BFStuckPointKit/Classes/SelectImage/PQImageCropVC.swift

@@ -0,0 +1,258 @@
+//
+//  PQImageCropVC.swift
+//  PQSpeed
+//
+//  Created by ak on 2020/8/3.
+//  Copyright © 2020 BytesFlow. All rights reserved.
+//
+// add by ak 主要功能 图片裁剪界面
+// e.g.
+// let image = UIImage(named:"img.jpg")!
+// let vc = PQImageCropVC(frame: (self.navigationController?.view.frame)!, image: image, aspectWidth:9, aspectHeight: 16)
+
+import BFUIKit
+import UIKit
+
+class PQImageCropVC: BFBaseViewController, UIScrollViewDelegate {
+    var uploadData: PQUploadModel?
+    var updataVideoData: PQVideoListModel? // 如果updataVideoData不为空则为修改视频
+    var aspectW: CGFloat!
+    var aspectH: CGFloat!
+    var img: UIImage!
+
+    var imageView: UIImageView!
+    var scrollView: UIScrollView!
+
+    var holeRect: CGRect!
+    var gap: CGFloat = 100 // 距屏幕边缘距离
+
+    required init?(coder _: NSCoder) { fatalError("init(coder:) has not been implemented") }
+
+    init(image: UIImage, aspectWidth: CGFloat, aspectHeight: CGFloat) {
+        super.init(nibName: nil, bundle: nil)
+        aspectW = aspectWidth
+        aspectH = aspectHeight
+        img = image
+        BFLog(message: "原图大小 \(img.size) aspectW:\(aspectW!) aspectH:\(aspectH!)")
+    }
+
+    override func viewDidLoad() {
+        super.viewDidLoad()
+        setupView()
+    }
+
+    func setupView() {
+        let holeWidth = view.frame.width - gap
+        BFLog(message: "aspectH :\(String(describing: aspectH))")
+        var holeHeight = holeWidth * aspectH / aspectW
+        if holeHeight.isNaN {
+            holeHeight = 0
+        }
+        if img.imageOrientation != .up {
+            UIGraphicsBeginImageContextWithOptions(img.size, false, img.scale)
+            var rect = CGRect.zero
+            rect.size = img.size
+            img.draw(in: rect)
+            img = UIGraphicsGetImageFromCurrentImageContext()
+            UIGraphicsEndImageContext()
+        }
+
+        holeRect = CGRect(x: 0, y: view.frame.height / 2 - holeHeight / 2, width: holeWidth, height: holeHeight)
+
+        imageView = UIImageView(image: img)
+        scrollView = UIScrollView(frame: CGRect(x: gap / 2.0, y: 0, width: holeWidth, height: holeHeight))
+        scrollView.center = view.center
+        scrollView.addSubview(imageView)
+        scrollView.showsVerticalScrollIndicator = false
+        scrollView.showsHorizontalScrollIndicator = false
+        scrollView.alwaysBounceHorizontal = true
+        scrollView.alwaysBounceVertical = true
+        scrollView.delegate = self
+        view.addSubview(scrollView)
+
+        // 设置最小 zoom 是防止在裁剪区出现黑边导致裁剪后的图不对
+        let minZoom = max(holeWidth / img.size.width, holeHeight / img.size.height)
+        BFLog(message: "minZoom is: \(minZoom)")
+
+        scrollView.minimumZoomScale = minZoom
+        scrollView.maximumZoomScale = minZoom * 4
+        // 后设置自动缩放
+        scrollView.setZoomScale(minZoom, animated: false)
+        scrollView.clipsToBounds = false
+        scrollView.contentOffset = CGPoint(x: (img.size.width * minZoom - holeWidth) / 2, y: (img.size.height * minZoom - holeHeight) / 2)
+
+        // 添加 mark layer
+        let cropView = UIView(frame: view.frame)
+        cropView.isUserInteractionEnabled = false
+        view.addSubview(cropView)
+        let layer = CAShapeLayer()
+        let clipWidth = holeWidth
+        let clipHeight = holeHeight
+
+        let cropAreaX = (cScreenWidth - clipWidth) / 2
+        let cropAreaY = (cScreenHeigth - clipHeight) / 2
+        let cropAreaWidth = clipWidth
+        let cropAreaHeight = clipHeight
+        let cropframe = CGRect(x: cropAreaX, y: cropAreaY, width: cropAreaWidth, height: cropAreaHeight)
+
+        let path = UIBezierPath(roundedRect: cropView.frame, cornerRadius: 0)
+        let cropPath = UIBezierPath(rect: cropframe)
+
+        path.append(cropPath)
+        layer.path = path.cgPath
+        layer.fillRule = CAShapeLayerFillRule.evenOdd
+        layer.fillColor = UIColor.black.cgColor
+        layer.opacity = 0.5
+        layer.frame = cropView.bounds
+        cropView.layer.addSublayer(layer)
+        view.bringSubviewToFront(cropView)
+
+        // 单独画线
+        let drawLine = DrawLine()
+        drawLine.setCropAreaLeft(cropAreaLeft: cropAreaX, cropAreaTop: cropAreaY, cropAreaRight: cropAreaX + cropAreaWidth, cropAreaBottom: cropAreaY + cropAreaHeight)
+        drawLine.frame = cropView.bounds
+        cropView.layer.addSublayer(drawLine)
+        view.bringSubviewToFront(cropView)
+
+        // 添加下边工具条
+        view.addSubview(bottomView)
+
+        // 显示引导
+        let haveShowGuid = getUserDefaults(key: cIsShowImageCropGuid)
+        if haveShowGuid == nil {
+//            let guid = PQGuidClipView(frame: view.frame)
+//            view.addSubview(guid)
+//            saveUserDefaults(key: cIsShowImageCropGuid, value: cIsShowImageCropGuid)
+        }
+    }
+
+    lazy var bottomView: UIView = {
+        let bottomView = UIView(frame: CGRect(x: 0, y: cScreenHeigth - cDevice_iPhoneTabBarHei, width: cScreenWidth, height: cDevice_iPhoneTabBarHei))
+        let deleteBtn = UIButton(frame: CGRect(x: cDefaultMargin, y: 0, width: cDevice_iPhoneTabBarHei, height: cDevice_iPhoneTabBarHei))
+        bottomView.backgroundColor = BFConfig.shared.styleBackGroundColor
+        deleteBtn.setImage(imageInUIKit(by: "icon_detail_back"), for: .normal)
+        deleteBtn.addTarget(self, action: #selector(tappedClose), for: .touchUpInside)
+
+        bottomView.addSubview(deleteBtn)
+
+        let selecteBtn = UIButton(frame: CGRect(x: 0, y: 0, width: 100, height: cDevice_iPhoneTabBarHei))
+        selecteBtn.setTitle("裁剪封面", for: .normal)
+        selecteBtn.setTitleColor(.black, for: .normal)
+        selecteBtn.titleLabel?.font = UIFont.systemFont(ofSize: 16, weight: .medium)
+
+        bottomView.addSubview(selecteBtn)
+        selecteBtn.center.x = bottomView.center.x
+
+        let nextBtn = UIButton(frame: CGRect(x: bottomView.frame.width - 100, y: (cDevice_iPhoneTabBarHei - 40) / 2, width: 80, height: 40))
+        nextBtn.addCorner(corner: 20)
+        nextBtn.setTitle("确定", for: .normal)
+        nextBtn.addTarget(self, action: #selector(tappedCrop), for: .touchUpInside)
+        nextBtn.setTitleColor(UIColor.white, for: .normal)
+        nextBtn.titleLabel?.font = UIFont.systemFont(ofSize: 16, weight: .medium)
+        nextBtn.backgroundColor = UIColor.hexColor(hexadecimal: BFConfig.shared.styleColor.rawValue)
+        bottomView.addSubview(nextBtn)
+
+        return bottomView
+    }()
+
+    // MARK: scrollView delegate
+
+    func viewForZooming(in _: UIScrollView) -> UIView? {
+        BFLog(message: "viewForZooming")
+        // 是imageView的容器,实现这个代理保证图片的放大拖动交互
+        return imageView
+    }
+
+    func scrollViewDidZoom(_ scrollView: UIScrollView) {
+        BFLog(message: "scrollViewDidZoom")
+        let gapToTheHole = scrollView.frame.height / 2 - holeRect.height / 2
+        scrollView.contentInset = UIEdgeInsets(top: gapToTheHole, left: 0, bottom: gapToTheHole, right: 0)
+    }
+
+    // 返回
+    @objc func tappedClose() {
+        BFLog(message: "返回")
+        // add by ak bugfix 图片超出 返回异常
+        navigationController?.popViewController(animated: true)
+    }
+
+    override func viewWillAppear(_ animated: Bool) {
+        super.viewWillAppear(animated)
+        scrollView.isHidden = false
+    }
+
+    override func viewWillDisappear(_ animated: Bool) {
+        super.viewWillDisappear(animated)
+        scrollView.isHidden = true
+    }
+
+    // 裁剪
+    @objc func tappedCrop() {
+        BFLog(message: "开始裁剪图片")
+
+        // 根据scrollView缩放系数和偏移量 对原图进行裁剪
+        var imgX: CGFloat = 0
+        if scrollView.contentOffset.x > 0 {
+            imgX = scrollView.contentOffset.x / scrollView.zoomScale
+        }
+
+        let gapToTheHole: CGFloat = 0
+        var imgY: CGFloat = 0
+        if scrollView.contentOffset.y + 0 > 0 {
+            imgY = (scrollView.contentOffset.y + gapToTheHole) / scrollView.zoomScale
+        }
+
+        let imgW = holeRect.width / scrollView.zoomScale
+        let imgH = holeRect.height / scrollView.zoomScale
+
+        print("IMG x: \(imgX) y: \(imgY) w: \(imgW) h: \(imgH)")
+
+        let cropRect = CGRect(x: imgX, y: imgY, width: imgW, height: imgH)
+        let imageRef = img.cgImage!.cropping(to: cropRect)
+        let croppedImage = UIImage(cgImage: imageRef!)
+        //  这个方法是使用view 截屏
+        //  let croppedImage : UIImage = scrollView.graphicsGetImage() ?? img
+        BFLog(message: "croppedImage\(croppedImage.size)")
+
+        postNotification(name: cSelectedImageSuccessKey, userInfo: ["image": croppedImage])
+
+        let publicVideoVC = navigationController?.viewControllers.first(where: { (vc) -> Bool in
+            vc is PQStuckPointPublicController
+        })
+        if publicVideoVC != nil {
+            navigationController?.popToViewController(publicVideoVC!, animated: true)
+        } else {
+            navigationController?.popViewController(animated: true)
+        }
+    }
+}
+
+class DrawLine: CAShapeLayer {
+    var mCropAreaLeft: CGFloat = 0
+    var mCropAreaTop: CGFloat = 0
+    var mCropAreaRight: CGFloat = cScreenWidth
+    var mCropAreaBottom: CGFloat = 0
+
+    let mLineWidth: CGFloat = 7
+
+    func setCropAreaLeft(cropAreaLeft: CGFloat, cropAreaTop: CGFloat, cropAreaRight: CGFloat, cropAreaBottom: CGFloat) {
+        mCropAreaLeft = cropAreaLeft
+        mCropAreaTop = cropAreaTop
+        mCropAreaRight = cropAreaRight
+        mCropAreaBottom = cropAreaBottom
+
+        setNeedsDisplay()
+    }
+
+    override func draw(in ctx: CGContext) {
+        UIGraphicsPushContext(ctx)
+
+        ctx.setStrokeColor(UIColor.white.cgColor)
+        ctx.setLineWidth(mLineWidth)
+        ctx.addRect(CGRect(x: mCropAreaLeft, y: mCropAreaTop, width: mCropAreaRight - mCropAreaLeft, height: mCropAreaBottom - mCropAreaTop))
+        // 必和路径
+        ctx.strokePath()
+
+        UIGraphicsPopContext()
+    }
+}

+ 52 - 0
BFStuckPointKit/Classes/SelectImage/PQImageSelectedController.swift

@@ -0,0 +1,52 @@
+//
+//  PQImageSelectedController.swift
+//  PQSpeed
+//
+//  Created by SanW on 2020/8/14.
+//  Copyright © 2020 BytesFlow. All rights reserved.
+//
+
+import UIKit
+import BFCommonKit
+import BFUIKit
+
+open class PQImageSelectedController: PQUploadController {
+    public var updataVideoData: PQVideoListModel? // 如果updataVideoData不为空则为修改视频
+    open override func viewDidLoad() {
+        super.viewDidLoad()
+        emptyData?.title = "哦呜~ 你没有可上传的图片~"
+        emptyData?.emptyImageName = "icon_authorError"
+        // Do any additional setup after loading the view.
+    }
+
+    @objc open override func btnClick(sender: UIButton) {
+        switch sender.tag {
+        case 1: // 返回
+            navigationController?.popViewController(animated: true)
+        case 2: // 筛选
+            showCollects()
+        case 3: // 下一步
+            if selectedData == nil {
+                cShowHUB(superView: nil, msg: "请选择图片")
+                return
+            }
+            PQPHAssetVideoParaseUtil.requestAssetOringinImage(asset: (selectedData?.asset)!) { [weak self] _, _, image, _ in
+                if image != nil {
+                    self?.selectedData?.image = image
+                }
+                self?.selectedData?.videoBucketKey = self?.uploadData?.videoBucketKey
+                let vc = PQImageCropVC(image: (self?.selectedData?.image)!, aspectWidth: self?.videoWidth ?? 0.0, aspectHeight: self?.videoHeight ?? 0.0)
+                self?.selectedData?.duration = self?.uploadData?.duration ?? 0.0
+
+                self?.selectedData?.localPath = self?.uploadData?.localPath
+                vc.uploadData = self?.selectedData
+                vc.updataVideoData = self?.updataVideoData
+                self?.navigationController?.pushViewController(vc, animated: true)
+            }
+        case 4: // 播放
+            break
+        default:
+            break
+        }
+    }
+}

+ 130 - 0
BFStuckPointKit/Classes/SelectImage/PQSelecteVideoItemCell.swift

@@ -0,0 +1,130 @@
+//
+//  PQSelecteVideoItemCell.swift
+//  PQSpeed
+//
+//  Created by SanW on 2020/8/1.
+//  Copyright © 2020 BytesFlow. All rights reserved.
+//
+
+import Photos
+import UIKit
+import BFCommonKit
+import BFUIKit
+
+class PQSelecteVideoItemCell: UICollectionViewCell {
+    var representedAssetIdentifier: String!
+    lazy var videoImageView: UIImageView = {
+        let videoImageView = UIImageView()
+        videoImageView.contentMode = .scaleAspectFill
+        videoImageView.clipsToBounds = true
+        return videoImageView
+    }()
+
+    lazy var placeHoldeImageView: UIImageView = {
+        let placeHoldeImageView = UIImageView()
+        placeHoldeImageView.image = UIImage(named: "cut_place")
+        placeHoldeImageView.clipsToBounds = true
+        return placeHoldeImageView
+    }()
+
+    lazy var timeLab: UILabel = {
+        let timeLab = UILabel()
+        timeLab.font = UIFont.systemFont(ofSize: 11)
+        timeLab.textColor = UIColor.white
+        timeLab.text = "00:00"
+        return timeLab
+    }()
+ 
+    lazy var seleImage: UIImageView = {
+        let seleImage = UIImageView(image:UIImage.moduleImage(named: "icon_upload_do", moduleName: "BFFramework",isAssets: false))
+        return seleImage
+    }()
+
+    lazy var bgView: UIView = {
+        let bgView = UIView()
+        bgView.backgroundColor = UIColor.init(red: 61.0 / 255.0 , green: 193.0 / 255.0 , blue: 193.0 / 255.0, alpha: 0.6)
+        return bgView
+    }()
+
+    @objc class func selecteVideoItemCell(collectionView: UICollectionView, indexPath: IndexPath) -> PQSelecteVideoItemCell {
+        let cell: PQSelecteVideoItemCell = collectionView.dequeueReusableCell(withReuseIdentifier: "PQSelecteVideoItemCell", for: indexPath) as! PQSelecteVideoItemCell
+        return cell
+    }
+
+    override init(frame: CGRect) {
+        super.init(frame: frame)
+        contentView.addSubview(videoImageView)
+        videoImageView.addSubview(timeLab)
+        videoImageView.addSubview(placeHoldeImageView)
+        contentView.addSubview(bgView)
+        bgView.addSubview(seleImage)
+        bgView.isHidden = !isSelected
+    }
+
+    required init?(coder _: NSCoder) {
+        fatalError("init(coder:) has not been implemented")
+    }
+
+    var uploadData: PQUploadModel? {
+        didSet {
+            addData()
+            addLayout()
+        }
+    }
+
+    var thubImage: UIImage? {
+        didSet {
+            addData()
+            videoImageView.image = thubImage
+            addLayout()
+        }
+    }
+
+    func addData() {
+        videoImageView.contentMode = uploadData?.contentMode ?? .scaleAspectFill
+        if uploadData?.contentMode == .scaleAspectFit {
+            addCorner(corner: 6)
+        } else {
+            addCorner(corner: 0)
+        }
+        if uploadData?.image != nil {
+            videoImageView.image = uploadData?.image
+        } else if uploadData?.imageUrl != nil, (uploadData?.imageUrl.count ?? 0) > 0 {
+            netImage(url: uploadData?.imageUrl ?? "", mainView: videoImageView)
+//        }else if(uploadData?.asset != nil){
+//            PQPHAssetVideoParaseUtil.requestAssetImage(asset: (uploadData?.asset)!, itemSize: CGSize.init(width: 0, height: 0)) { [weak self](image, info) in
+//                self?.videoImageView.image = image
+//            }
+        } else {
+            videoImageView.image = nil
+        }
+        timeLab.text = (uploadData?.asset?.duration ?? 0).formatDurationToHMS()
+        timeLab.isHidden = (uploadData?.asset?.duration ?? 0) <= 0.0
+        bgView.isHidden = !(uploadData?.isSelected ?? false)
+        videoImageView.backgroundColor = (((uploadData?.videoWidth ?? 0) < (uploadData?.videoHeight ?? 0)) || (uploadData?.image == nil && (uploadData?.imageUrl == nil || (uploadData?.imageUrl.count ?? 0) <= 0))) ? UIColor.hexColor(hexadecimal: "#212223") : UIColor.black
+        placeHoldeImageView.isHidden = !(uploadData?.image == nil && (uploadData?.imageUrl == nil || (uploadData?.imageUrl.count ?? 0) <= 0))
+    }
+
+    func addLayout() {
+        placeHoldeImageView.snp.remakeConstraints { make in
+            make.width.equalTo(28)
+            make.height.equalTo(24)
+            make.center.equalToSuperview()
+        }
+        videoImageView.snp.remakeConstraints { make in
+            make.top.bottom.left.right.equalToSuperview()
+        }
+        timeLab.snp.makeConstraints { make in
+            make.bottom.equalTo(videoImageView).offset(-cDefaultMargin / 2)
+            make.right.equalTo(videoImageView).offset(-cDefaultMargin / 2)
+        }
+        bgView.snp.makeConstraints { make in
+            make.size.equalToSuperview()
+        }
+        seleImage.snp.makeConstraints { make in
+            make.width.height.equalTo(23)
+            make.right.equalToSuperview().offset(-cDefaultMargin)
+            make.top.equalToSuperview().offset(cDefaultMargin)
+        }
+    }
+}

+ 992 - 0
BFStuckPointKit/Classes/SelectImage/PQUploadController.swift

@@ -0,0 +1,992 @@
+//
+//  PQUploadViewController.swift
+//  PQSpeed
+//
+//  Created by SanW on 2020/8/1.
+//  Copyright © 2020 BytesFlow. All rights reserved.
+//
+
+import MobileCoreServices
+import Photos
+import UIKit
+import BFCommonKit
+
+let playerHeaderH: CGFloat = cScreenWidth * (250 / 375)
+
+open class PQUploadController: BFBaseViewController {
+    // 最大的宽度
+    public var maxWidth: CGFloat = cScreenWidth
+    // 最大的高度
+    public var maxHeight: CGFloat = adapterWidth(width: 300)
+    public var jumptoPublicHandle:((_ selectData:PQUploadModel?) -> Void)?
+    // 画面比例
+    public var aspectRatio: aspectRatio?
+    public var preViewSize: CGSize {
+        if aspectRatio == nil, (isMember(of: PQUploadController.self) && selectedData == nil) || (!isMember(of: PQUploadController.self) && uploadData == nil) {
+            return CGSize(width: maxHeight, height: maxHeight)
+        }
+        if selectedData != nil {
+            aspectRatio = .origin(width: CGFloat(videoData[lastSeletedIndex?.item ?? 0].videoWidth), height: CGFloat(videoData[lastSeletedIndex?.item ?? 0].videoHeight))
+        } else if aspectRatio == nil {
+            aspectRatio = .origin(width: CGFloat(uploadData?.videoWidth ?? maxHeight), height: CGFloat(uploadData?.videoHeight ?? maxHeight))
+        }
+        switch aspectRatio {
+        case let .origin(width, height):
+            var tempHeight: CGFloat = 0
+            var tempWidth: CGFloat = 0
+            if width > height {
+                tempHeight = (maxWidth * height / width)
+                tempWidth = maxWidth
+            } else {
+                tempHeight = maxHeight
+                tempWidth = (maxHeight * width / height)
+            }
+            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)
+    }
+
+    // 上传入口类型
+    public var sourceType: videoUploadSourceType = .videoUpload
+    // 制作视频项目Id
+    public var makeVideoProjectId: String?
+    // 再创作数据
+    public var reCreateData: PQReCreateModel?
+    // 视频创作埋点数据
+    public var eventTrackData: PQVideoMakeEventTrackModel?
+    // 制作视频草稿Id
+    public var makeVideoDraftboxId: String?
+    // 制作视频草稿结构化数据
+    public var makeVideoSdata: String?
+    public var isAssetImage: Bool = false // 是否请求的是图片
+    public var videoWidth: CGFloat = 0 // 视频宽
+    public var videoHeight: CGFloat = 0 // 视频高
+    public var isLoop: Bool = true // 是否循环播放
+    public var playerItem: AVPlayerItem? // 当前播放item
+    public var videoOutput: AVPlayerItemVideoOutput?
+    let itemSize = CGSize(width: ((cScreenWidth - cDefaultMargin) / 3) * UIScreen.main.scale, height: ((cScreenWidth - cDefaultMargin) / 3) * UIScreen.main.scale)
+    public var categoryH: CGFloat = cDefaultMargin * 40 // 相簿高度
+
+    public var videoData: [PQUploadModel] = Array<PQUploadModel>.init()
+
+    public var categoryData: [PQUploadModel] = Array<PQUploadModel>.init()
+
+    // 当前选中的数据
+    public var selectedData: PQUploadModel?
+    // 确定上传的数据
+    public var uploadData: PQUploadModel?
+    public var catagerySelectedIndex: IndexPath = IndexPath(item: 0, section: 0) // 更多图库选择
+
+    public var isJumpToAuthorization: Bool = false // 是否跳到授权
+    public var lastSeletedIndex: IndexPath? // 上次选中的index
+    public lazy var imageManager: PHCachingImageManager = {
+        (PHCachingImageManager.default() as? PHCachingImageManager) ?? PHCachingImageManager()
+    }()
+
+    public var allPhotos: PHFetchResult<PHAsset>!
+    public var previousPreheatRect = CGRect.zero
+
+    public var smartAlbums: PHFetchResult<PHAssetCollection>!
+    public var userCollections: PHFetchResult<PHCollection>!
+    public let sectionLocalizedTitles = ["", NSLocalizedString("Smart Albums", comment: ""), NSLocalizedString("Albums", comment: "")]
+    public lazy var fetchOptions: PHFetchOptions = {
+        let fetchOptions = PHFetchOptions()
+        fetchOptions.sortDescriptors = [NSSortDescriptor(key: "creationDate", ascending: false)]
+        fetchOptions.predicate = NSPredicate(format: "mediaType = %d", !isAssetImage ? PHAssetMediaType.video.rawValue : PHAssetMediaType.image.rawValue)
+        return fetchOptions
+    }()
+
+    public lazy var imagesOptions: PHImageRequestOptions = {
+        let imagesOptions = PHImageRequestOptions()
+        imagesOptions.isSynchronous = true
+        imagesOptions.isNetworkAccessAllowed = false
+        imagesOptions.deliveryMode = .opportunistic
+        imagesOptions.resizeMode = .fast
+        imagesOptions.progressHandler = { _, _, _, info in
+            BFLog(message: "progressHandler = \(info)")
+        }
+        return imagesOptions
+    }()
+
+    public lazy var singleImageOptions: PHImageRequestOptions = {
+        let singleImageOptions = PHImageRequestOptions()
+        singleImageOptions.isSynchronous = true
+        singleImageOptions.isNetworkAccessAllowed = false
+        singleImageOptions.deliveryMode = .highQualityFormat
+        return singleImageOptions
+    }()
+
+    public lazy var backBtn: UIButton = {
+        let backBtn = UIButton(type: .custom)
+        backBtn.frame = CGRect(x: 0, y: cDevice_iPhoneStatusBarHei, width: cDefaultMargin * 4, height: cDefaultMargin * 4)
+        backBtn.imageEdgeInsets = UIEdgeInsets(top: 0, left: 0, bottom: -5, right: 0)
+        backBtn.setImage(UIImage(named: "ic_close_black"), for: .normal)
+        backBtn.addTarget(self, action: #selector(backBtnClick), for: .touchUpInside)
+        return backBtn
+    }()
+
+    public var anthorEmptyData: BFEmptyModel? = {
+        let anthorEmptyData = BFEmptyModel()
+        anthorEmptyData.title = "开始上传"
+        anthorEmptyData.summary = "要开始上传视频,请先授予相册使用权限"
+        anthorEmptyData.emptyImageName = "icon_authorError"
+        return anthorEmptyData
+    }()
+
+    public var emptyData: BFEmptyModel? = {
+        let emptyData = BFEmptyModel()
+        emptyData.title = "哦呜~ 你没有可上传的视频~"
+        emptyData.emptyImageName = "video_empty"
+        return emptyData
+    }()
+
+    public lazy var emptyRemindView: BFEmptyRemindView = {
+        let emptyRemindView = BFEmptyRemindView(frame: CGRect(x: 0, y: cDevice_iPhoneNavBarAndStatusBarHei, width: cScreenWidth, height: cScreenHeigth - cDevice_iPhoneNavBarAndStatusBarHei))
+        emptyRemindView.isHidden = true
+        emptyRemindView.emptyData = anthorEmptyData
+        view.addSubview(emptyRemindView)
+        emptyRemindView.fullRefreshBloc = { [weak self, weak emptyRemindView] _, _ in
+            self?.isJumpToAuthorization = true
+            if emptyRemindView?.refreshBtn.currentTitle == "授予权限" {
+                openAppSetting()
+            }
+        }
+        return emptyRemindView
+    }()
+
+    public lazy var collectionView: UICollectionView = {
+        let layout = UICollectionViewFlowLayout()
+        layout.sectionInset = UIEdgeInsets.zero
+        let collectionView = UICollectionView(frame: CGRect(x: 0, y: cDevice_iPhoneNavBarAndStatusBarHei, width: cScreenWidth, height: cScreenHeigth - cDevice_iPhoneNavBarAndStatusBarHei), collectionViewLayout: layout)
+        collectionView.showsVerticalScrollIndicator = false
+        collectionView.register(PQSelecteVideoItemCell.self, forCellWithReuseIdentifier: "PQSelecteVideoItemCell")
+        collectionView.delegate = self
+        collectionView.dataSource = self
+        if #available(iOS 11.0, *) {
+            collectionView.contentInsetAdjustmentBehavior = .never
+        } else {
+            automaticallyAdjustsScrollViewInsets = false
+        }
+        collectionView.backgroundColor = UIColor.hexColor(hexadecimal: "#191919")
+        return collectionView
+    }()
+
+    public lazy var categoryCollectionView: UICollectionView = {
+        let layout = UICollectionViewFlowLayout()
+        layout.sectionInset = UIEdgeInsets.zero
+        let categoryCollectionView = UICollectionView(frame: CGRect(x: 0, y: -categoryH, width: cScreenWidth, height: categoryH), collectionViewLayout: layout)
+        categoryCollectionView.showsVerticalScrollIndicator = false
+        categoryCollectionView.register(PQAssetCategoryCell.self, forCellWithReuseIdentifier: "PQAssetCategoryCell")
+        categoryCollectionView.delegate = self
+        categoryCollectionView.dataSource = self
+        if #available(iOS 11.0, *) {
+            categoryCollectionView.contentInsetAdjustmentBehavior = .never
+        } else {
+            automaticallyAdjustsScrollViewInsets = false
+        }
+        categoryCollectionView.backgroundColor = BFConfig.shared.editCoverimageSelectedbackgroundColor
+        return categoryCollectionView
+    }()
+
+    public lazy var categoryView: UIView = {
+        let categoryView = UIView(frame: CGRect(x: 0, y: cDevice_iPhoneNavBarAndStatusBarHei, width: cScreenWidth, height: cScreenHeigth - cDevice_iPhoneNavBarAndStatusBarHei))
+        categoryView.backgroundColor = UIColor(red: 0, green: 0, blue: 0, alpha: 0.6)
+        let bgView = UIView(frame: CGRect(x: 0, y: categoryH, width: categoryView.frame.width, height: categoryView.frame.height - categoryH))
+        let ges = UITapGestureRecognizer(target: self, action: #selector(cancelClick))
+        bgView.addGestureRecognizer(ges)
+        categoryView.addSubview(bgView)
+        categoryView.isHidden = true
+        return categoryView
+    }()
+
+    public 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?.isLoop ?? false, bf_getCurrentViewController()?.isMember(of: PQUploadController.self) ?? false {
+                if self?.playerLayer.superlayer == nil {
+                    self?.playerHeaderView.layer.insertSublayer(self!.playerLayer, at: 0)
+                }
+                avPlayer.play()
+            } else {
+                self?.sliderView.setValue(1.0, animated: 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)
+            self?.sliderView.setValue(Float(progress), animated: false)
+            if progress >= 1, !(self?.isLoop ?? false) {
+                self?.sliderView.setValue(Float(progress), animated: false)
+                self?.playBtn.isHidden = false
+            }
+        }
+        return avPlayer
+    }()
+
+    public lazy var playerLayer: AVPlayerLayer = {
+        let playerLayer = AVPlayerLayer(player: avPlayer)
+        playerLayer.frame = CGRect(origin: CGPoint.zero, size: preViewSize)
+        return playerLayer
+    }()
+
+    public lazy var sliderView: BFPlayerSlider = {
+        let sliderView = BFPlayerSlider(frame: CGRect(x: 0, y: cDevice_iPhoneNavBarAndStatusBarHei + maxHeight, width: view.frame.width, height: cDefaultMargin))
+        let thbImage = UIImage(named: "icon_point")
+        sliderView.setMinimumTrackImage(thbImage, for: .normal)
+        sliderView.setMaximumTrackImage(thbImage, for: .normal)
+        sliderView.setThumbImage(thbImage, for: .highlighted)
+        sliderView.setThumbImage(thbImage, for: .normal)
+        sliderView.maximumTrackTintColor = UIColor.clear
+        sliderView.addTarget(self, action: #selector(sliderValueDidChanged(sender:)), for: .valueChanged)
+        sliderView.minimumTrackTintColor = UIColor.white
+        sliderView.isHidden = true
+        sliderView.backgroundColor = UIColor.black
+        return sliderView
+    }()
+
+    public 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(named: "icon_video_play_big"), for: .normal)
+        playBtn.tag = 4
+        playBtn.isHidden = true
+        playBtn.isUserInteractionEnabled = false
+        // playBtn.addTarget(self, action: #selector(btnClick(sender:)), for: .touchUpInside)
+        return playBtn
+    }()
+
+    public 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
+        playerHeaderView.backgroundColor = UIColor.black
+        return playerHeaderView
+    }()
+
+    public lazy var selecteBtn: UIButton = {
+        let selecteBtn = UIButton(frame: CGRect(x: deleteBtn.frame.maxX + cDefaultMargin, y: 0, width: cScreenWidth - nextBtn.frame.width - deleteBtn.frame.maxX - cDefaultMargin * 5, height: cDevice_iPhoneTabBarHei))
+        selecteBtn.titleLabel?.lineBreakMode = .byTruncatingTail
+        selecteBtn.setTitle("全部", for: .normal)
+        selecteBtn.setImage(UIImage(named: "icon_uploadVideo_more"), for: .normal)
+        selecteBtn.setTitleColor(UIColor.white, for: .normal)
+        selecteBtn.titleLabel?.font = UIFont.systemFont(ofSize: 16, weight: .medium)
+        selecteBtn.tag = 2
+        selecteBtn.imagePosition(at: .right, space: cDefaultMargin / 2)
+        selecteBtn.addTarget(self, action: #selector(btnClick(sender:)), for: .touchUpInside)
+        return selecteBtn
+    }()
+
+    public lazy var deleteBtn: UIButton = {
+        let deleteBtn = UIButton(frame: CGRect(x: cDefaultMargin, y: 0, width: cDefaultMargin * 4, height: cDevice_iPhoneTabBarHei))
+        deleteBtn.setImage(UIImage(named: "upload_delete"), for: .normal)
+        deleteBtn.tag = 1
+        deleteBtn.addTarget(self, action: #selector(btnClick(sender:)), for: .touchUpInside)
+        return deleteBtn
+    }()
+
+    public lazy var nextBtn: UIButton = {
+        let nextBtn = UIButton(frame: CGRect(x: 0, y: (cDevice_iPhoneTabBarHei - cDefaultMargin * 3) / 2, width: cDefaultMargin * 6, height: cDefaultMargin * 3))
+        nextBtn.addCorner(corner: 3)
+        nextBtn.setTitle("下一步", for: .normal)
+        nextBtn.titleLabel?.font = UIFont.systemFont(ofSize: 13, weight: .medium)
+        nextBtn.tag = 3
+        nextBtn.addTarget(self, action: #selector(btnClick(sender:)), for: .touchUpInside)
+        nextBtn.backgroundColor = UIColor.hexColor(hexadecimal: "#333333")
+        nextBtn.setTitleColor(UIColor.hexColor(hexadecimal: "#999999"), for: .normal)
+        return nextBtn
+    }()
+
+    public lazy var bottomView: UIView = {
+        let bottomView = UIView(frame: CGRect(x: 0, y: cDefaultMargin * 2, width: cScreenWidth, height: cDevice_iPhoneNavBarHei))
+        bottomView.addSubview(selecteBtn)
+        bottomView.backgroundColor = UIColor.hexColor(hexadecimal: "#191919")
+        selecteBtn.center.y = nextBtn.center.y
+        bottomView.addSubview(deleteBtn)
+        bottomView.addSubview(nextBtn)
+        nextBtn.frame.origin.x = bottomView.frame.width - nextBtn.frame.width - cDefaultMargin * 2
+        return bottomView
+    }()
+
+    open override func viewDidLoad() {
+        super.viewDidLoad()
+
+        view.backgroundColor = BFConfig.shared.editCoverimageSelectedbackgroundColor
+        navHeadImageView?.backgroundColor = BFConfig.shared.editCoverimageSelectedbackgroundColor
+        lineView?.backgroundColor = BFConfig.shared.editCoverimageSelectedbackgroundColor
+        addSubViews()
+        bottomView.backgroundColor = BFConfig.shared.editCoverimageSelectedbackgroundColor
+        loadLocalData()
+    }
+
+    deinit {
+        //        PHPhotoLibrary.shared().unregisterChangeObserver(self)
+    }
+
+    open override func viewDidDisappear(_ animated: Bool) {
+        super.viewDidDisappear(animated)
+        if !isAssetImage {
+            avPlayer.pause()
+            playBtn.isHidden = false
+        }
+    }
+
+    open override func viewWillAppear(_ animated: Bool) {
+        super.viewDidAppear(animated)
+        if !isAssetImage {
+            if selectedData != nil {
+                playBtn.isHidden = true
+                if playerLayer.superlayer == nil {
+                    playerHeaderView.layer.insertSublayer(playerLayer, at: 0)
+                }
+                avPlayer.play()
+            }
+        }
+        addPlayerItemObserver()
+    }
+
+    open override func viewWillDisappear(_ animated: Bool) {
+        super.viewWillDisappear(animated)
+        removePlayerItemObserver()
+    }
+
+    open func addSubViews() {
+        view.addSubview(collectionView)
+        navHeadImageView?.addSubview(bottomView)
+        if !isAssetImage {
+            view.addSubview(playerHeaderView)
+            view.addSubview(sliderView)
+            let ges = UITapGestureRecognizer(target: self, action: #selector(playPreVideo))
+            playerHeaderView.addGestureRecognizer(ges)
+            playerHeaderView.addSubview(playBtn)
+            playerHeaderView.layer.insertSublayer(playerLayer, at: 0)
+        }
+        PQNotification.addObserver(self, selector: #selector(didBecomeActiveNotification), name: UIApplication.didBecomeActiveNotification, object: nil)
+    }
+
+    open func loadLocalData() {
+        let authStatus = PHPhotoLibrary.authorizationStatus()
+        if authStatus == .notDetermined {
+            // 第一次触发授权 alert
+            PHPhotoLibrary.requestAuthorization { [weak self] (status: PHAuthorizationStatus) -> Void in
+                if status == .authorized {
+                    if (self?.allPhotos == nil) || (self?.allPhotos.count ?? 0) <= 0 {
+                        self?.loadPhotoData()
+                    }
+                    if self?.backBtn != nil {
+                        self?.backBtn.removeFromSuperview()
+                    }
+                } else {
+                    DispatchQueue.main.async { [weak self] in
+                        self?.emptyRemindView.isHidden = false
+                        self?.emptyRemindView.refreshBtn.isHidden = false
+                        self?.emptyRemindView.refreshBtn.setTitle("授予权限", for: .normal)
+                        if self?.backBtn.superview == nil {
+                            self?.view.addSubview((self?.backBtn)!)
+                        } else {
+                            self?.backBtn.isHidden = false
+                        }
+                    }
+                }
+            }
+        } else if authStatus == .authorized {
+            if allPhotos == nil || allPhotos.count <= 0 {
+                loadPhotoData()
+            }
+            if backBtn.superview != nil {
+                backBtn.removeFromSuperview()
+            }
+
+        } else {
+            emptyRemindView.isHidden = false
+            emptyRemindView.refreshBtn.isHidden = false
+            emptyRemindView.refreshBtn.setTitle("授予权限", for: .normal)
+            if backBtn.superview == nil {
+                view.addSubview(backBtn)
+            } else {
+                backBtn.isHidden = false
+            }
+        }
+    }
+
+    open func loadPhotoData() {
+        DispatchQueue.main.async { [weak self] in
+            BFLoadingHUB.shared.showHUB(superView: self!.view, isVerticality: false)
+        }
+        DispatchQueue.global().async { [weak self] in
+            self?.allPhotos = PHAsset.fetchAssets(with: self?.fetchOptions)
+            DispatchQueue.main.async { [weak self] in
+                if self?.view != nil {
+                    BFLoadingHUB.shared.dismissHUB(superView: self!.view)
+                }
+                self?.collectionView.reloadData()
+                if (self?.allPhotos.count ?? 0) <= 0 {
+                    self?.emptyRemindView.frame = CGRect(x: 0, y: cDevice_iPhoneNavBarAndStatusBarHei, width: cScreenWidth, height: cScreenHeigth - cDevice_iPhoneNavBarAndStatusBarHei - cDevice_iPhoneTabBarHei)
+                    self?.emptyRemindView.emptyData = self?.emptyData
+                    self?.emptyRemindView.isHidden = false
+                } else {
+                    self?.emptyRemindView.isHidden = true
+                }
+                if (self?.allPhotos.count ?? 0) > 0 {
+                    let tempData = PQUploadModel()
+                    tempData.title = "全部"
+                    tempData.categoryList = self!.allPhotos
+                    self?.categoryData.insert(tempData, at: 0)
+                }
+                self?.updateCachedAssets()
+            }
+        }
+        PHPhotoLibrary.shared().register(self)
+
+        DispatchQueue.global().async { [weak self] in
+            self?.smartAlbums = PHAssetCollection.fetchAssetCollections(with: .smartAlbum, subtype: .albumRegular, options: nil)
+            self?.smartAlbums?.enumerateObjects { [weak self] assCollection, _, _ in
+                if assCollection.localizedTitle != "最近删除" {
+                    self?.convertCollection(collection: assCollection)
+                }
+            }
+            self?.userCollections = PHCollectionList.fetchTopLevelUserCollections(with: nil)
+            self?.userCollections.enumerateObjects { assCollection, index, point in
+                BFLog(message: "userCollections == \(assCollection),index = \(index),point = \(point)")
+                if assCollection is PHAssetCollection {
+                    if assCollection.localizedTitle != "最近删除" {
+                        self?.convertCollection(collection: assCollection as? PHAssetCollection)
+                    }
+                }
+            }
+        }
+        if !isAssetImage {
+            // 视频上传相关上报
+            PQEventTrackViewModel.baseReportUpload(businessType: .bt_pageView, objectType: .ot_pageView, pageSource: .sp_upload_videoSelect, extParams: ["source": videoUploadSourceType.videoUpload.rawValue, "projectId": "", "draftboxId": ""], remindmsg: "上传相关")
+        }
+    }
+
+    // 转化处理获取到的相簿
+    open func convertCollection(collection: PHAssetCollection?) {
+        if collection == nil {
+            return
+        }
+        DispatchQueue.global().async { [weak self] in
+            let assetsFetchResult = PHAsset.fetchAssets(in: collection!, options: self?.fetchOptions)
+            if assetsFetchResult.count > 0 {
+                let tempData = PQUploadModel()
+                tempData.title = collection?.localizedTitle
+                tempData.categoryList = assetsFetchResult
+                if tempData.categoryList.count > 0 {
+                    self?.categoryData.append(tempData)
+                }
+                BFLog(message: "assetsFetchResult = \(assetsFetchResult)")
+            }
+        }
+    }
+
+    @objc open func btnClick(sender: UIButton) {
+        switch sender.tag {
+        case 1: // 返回
+            navigationController?.popViewController(animated: true)
+            if !isAssetImage {
+                // 视频上传相关上报
+                // MARK: SanW--待修改-2021.11.08
+                PQEventTrackViewModel.baseReportUpload(businessType: .bt_buttonClick, objectType: .ot_up_backBtn, pageSource: .sp_upload_videoSelect, extParams: ["source": videoUploadSourceType.videoUpload.rawValue, "projectId": "", "draftboxId":""], remindmsg: "上传相关")
+            }
+        case 2: // 筛选
+            showCollects()
+        case 3: // 下一步
+            if selectedData == nil {
+                cShowHUB(superView: nil, msg: isAssetImage ? "请选择图片" : "请选择视频")
+                return
+            }
+            if !isAssetImage {
+                avPlayer.pause()
+                playBtn.isHidden = false
+                if jumptoPublicHandle != nil {
+                    uploadData?.videoFromScene = .UploadNormal
+                    jumptoPublicHandle!(selectedData)
+                }
+                // 视频上传相关上报
+                PQEventTrackViewModel.baseReportUpload(businessType: .bt_buttonClick, objectType: .ot_up_nextBtn, pageSource: .sp_upload_videoSelect, extParams: ["source": videoUploadSourceType.videoUpload.rawValue, "projectId": "", "draftboxId": ""], remindmsg: "上传相关")
+                return
+            }
+            imageManager.requestImage(for: (selectedData?.asset)!, targetSize: itemSize, contentMode: .aspectFill, options: nil) { [weak self] image, _ in
+                self?.selectedData?.image = image
+                let vc = PQImageCropVC(image: (self?.selectedData?.image)!, aspectWidth: self?.videoWidth ?? 0.0, aspectHeight: self?.videoHeight ?? 0.0)
+                vc.uploadData = self?.selectedData
+                self?.navigationController?.pushViewController(vc, animated: true)
+            }
+        case 4: // 播放
+            playBtn.isHidden = true
+            if playerLayer.superlayer == nil {
+                playerHeaderView.layer.insertSublayer(playerLayer, at: 0)
+            }
+            avPlayer.play()
+        default:
+            break
+        }
+    }
+
+    @objc public func showCollects() {
+        if categoryData.count <= 0 {
+            return
+        }
+        categoryCollectionView.reloadData()
+        if categoryView.superview == nil {
+            view.insertSubview(categoryView, belowSubview: navHeadImageView!)
+            categoryView.addSubview(categoryCollectionView)
+            showCategoryView()
+            return
+        }
+        if categoryView.isHidden {
+            showCategoryView()
+        } else {
+            cancelClick()
+        }
+    }
+
+    @objc public func showCategoryView() {
+        categoryView.isHidden = false
+        categoryView.alpha = 0
+        view.bringSubviewToFront(categoryView)
+        view.bringSubviewToFront(bottomView)
+        UIView.animate(withDuration: 0.3, animations: {
+            self.categoryCollectionView.frame = CGRect(x: 0, y: 0, width: cScreenWidth, height: self.categoryH)
+            self.categoryView.alpha = 1
+        }) { _ in
+        }
+    }
+
+    @objc func cancelClick() {
+        UIView.animate(withDuration: 0.3, animations: {
+            self.categoryCollectionView.frame = CGRect(x: 0, y: -self.categoryH, width: cScreenWidth, height: self.categoryH)
+            self.categoryView.alpha = 0
+        }) { _ in
+            self.categoryView.isHidden = true
+        }
+    }
+
+    @objc open func playPreVideo() {
+        playBtn.isHidden = !playBtn.isHidden
+        if playBtn.isHidden {
+            if playerLayer.superlayer == nil {
+                playerHeaderView.layer.insertSublayer(playerLayer, at: 0)
+            }
+            avPlayer.play()
+        } else {
+            avPlayer.pause()
+        }
+    }
+
+    @objc func sliderValueDidChanged(sender: UISlider) {
+        avPlayer.seek(to: CMTime(value: CMTimeValue(sender.value * Float(CMTimeGetSeconds(avPlayer.currentItem?.duration ?? CMTime.zero))), timescale: 1))
+    }
+
+    @objc open func didBecomeActiveNotification() {
+        if isJumpToAuthorization {
+            loadLocalData()
+            isJumpToAuthorization = false
+        }
+    }
+
+    open func isPublishEnabled() {
+        if selectedData != nil {
+            nextBtn.backgroundColor = UIColor.hexColor(hexadecimal: "#EE0051")
+            nextBtn.setTitleColor(UIColor.white, for: .normal)
+        } else {
+            nextBtn.backgroundColor = UIColor.hexColor(hexadecimal: "#333333")
+            nextBtn.setTitleColor(UIColor.hexColor(hexadecimal: "#999999"), for: .normal)
+        }
+    }
+}
+
+extension PQUploadController: UICollectionViewDelegate, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout, UIScrollViewDelegate {
+    open func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection _: Int) -> Int {
+        if collectionView == self.collectionView {
+            return allPhotos == nil ? videoData.count : allPhotos.count
+        }
+        return categoryData.count
+    }
+
+    open func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
+        if collectionView == self.collectionView {
+            let cell = PQSelecteVideoItemCell.selecteVideoItemCell(collectionView: collectionView, indexPath: indexPath)
+            if videoData.count <= indexPath.item, allPhotos != nil {
+                let itemData = PQUploadModel()
+                // itemData.image = UIImage.init(named: "cut_place")
+                let asset = allPhotos.object(at: indexPath.item)
+                itemData.asset = asset
+                itemData.duration = asset.duration
+                videoData.append(itemData)
+            }
+            if videoData.count > indexPath.item {
+                let itemData = videoData[indexPath.item]
+                cell.uploadData = itemData
+                if itemData.image == nil, itemData.asset != nil {
+                    cell.representedAssetIdentifier = itemData.asset?.localIdentifier
+                    imageManager.requestImage(for: itemData.asset!, targetSize: itemSize, contentMode: .aspectFill, options: nil) {[weak self, weak cell] image, info in
+                        if info?.keys.contains("PHImageResultIsDegradedKey") ?? false, "\(info?["PHImageResultIsDegradedKey"] ?? "0")" == "0", cell?.representedAssetIdentifier == itemData.asset?.localIdentifier {
+                            if image != nil {
+                                itemData.image = image
+                                cell?.videoImageView.image = image
+                            } else if image == nil, info?.keys.contains("PHImageResultIsInCloudKey") ?? false {
+                                let option = PHImageRequestOptions()
+                                option.isNetworkAccessAllowed = true
+                                option.resizeMode = .fast
+                                self?.imageManager.requestImageData(for: itemData.asset!, options: option) {[weak cell] data, _, _, _ in
+                                    if data != nil {
+                                        let image = UIImage(data: data!)
+                                        itemData.image = image
+                                        cell?.videoImageView.image = image
+                                    }
+                                }
+                            }
+                        }
+                    }
+                }
+            } else {
+                cell.uploadData = PQUploadModel()
+            }
+            return cell
+        } else {
+            let cell = PQAssetCategoryCell.assetCategoryCell(collectionView: collectionView, indexPath: indexPath)
+            let itemData = categoryData[indexPath.item]
+            let asset = itemData.categoryList.object(at: 0)
+            if itemData.image == nil {
+                cell.representedAssetIdentifier = asset.localIdentifier
+                imageManager.requestImage(for: asset, targetSize: itemSize, contentMode: .aspectFill, options: nil) {[weak cell] image, info in
+                    if info?.keys.contains("PHImageResultIsDegradedKey") ?? false, "\(info?["PHImageResultIsDegradedKey"] ?? "0")" == "0", cell?.representedAssetIdentifier == asset.localIdentifier {
+                        itemData.image = image
+                        cell?.uploadData = itemData
+                    }
+                }
+            } else {
+                cell.uploadData = itemData
+            }
+            cell.isSelected = indexPath == catagerySelectedIndex
+            return cell
+        }
+    }
+
+    open func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
+        if collectionView == self.collectionView {
+            if videoData.count <= indexPath.item {
+                return
+            }
+            let itemData = videoData[indexPath.item]
+            if !isAssetImage, (itemData.asset?.duration ?? 0) < 5.0 {
+                cShowHUB(superView: nil, msg: "请选择大于5s的视频")
+                return
+            }
+            let ratio = Float(itemData.asset?.pixelWidth ?? 0) / Float(itemData.asset?.pixelHeight ?? 1)
+            if ratio < 0.4 || ratio > 4.2 {
+                cShowHUB(superView: nil, msg: "暂不支持该比例的素材")
+                return
+            }
+            if lastSeletedIndex != nil, lastSeletedIndex != indexPath {
+                let lastData = videoData[lastSeletedIndex!.item]
+                lastData.isSelected = false
+                collectionView.reloadItems(at: [lastSeletedIndex!])
+            }
+            lastSeletedIndex = indexPath
+            if itemData.isSelected {
+                itemData.isSelected = false
+                selectedData = nil
+                if !isAssetImage {
+                    avPlayer.pause()
+                    playBtn.isHidden = false
+                    sliderView.isHidden = true
+                    UIView.animate(withDuration: 0.5, animations: { [weak self] in
+                        self?.playerHeaderView.frame = CGRect(x: 0, y: -(self?.preViewSize.height ?? 0) - cDevice_iPhoneNavBarAndStatusBarHei, width: self?.preViewSize.width ?? 0, height: self?.preViewSize.height ?? 0)
+                        self?.sliderView.frame.origin.y = self?.playerHeaderView.frame.maxY ?? 0
+                        self?.playerHeaderView.center.x = self?.view.center.x ?? 0
+                        self?.collectionView.frame = CGRect(x: 0, y: cDevice_iPhoneNavBarAndStatusBarHei, width: cScreenWidth, height: cScreenHeigth - cDevice_iPhoneNavBarAndStatusBarHei)
+                    }) { _ in
+                    }
+                }
+            } else {
+                itemData.isSelected = true
+                let itemData = videoData[indexPath.item]
+                if !isAssetImage {
+                    selectedData = nil
+                    sliderView.setValue(0, animated: false)
+                    // 加载中...
+                    BFLoadingHUB.shared.showHUB(superView: playerHeaderView)
+                    avPlayer.pause()
+                    avPlayer.replaceCurrentItem(with: nil)
+                    playerLayer.removeFromSuperlayer()
+                    PQPHAssetVideoParaseUtil.parasToAVPlayerItem(phAsset: itemData.asset!, isHighQuality: true) { [weak self] playerItem, fileSize, info in
+                        if playerItem == nil || fileSize > maxUploadSize {
+                            if fileSize > maxUploadSize {
+                                cShowHUB(superView: nil, msg: "请选择小于10G的视频")
+                            } else {
+                                cShowHUB(superView: nil, msg: (info != nil && (info?.keys.contains("PHImageResultIsInCloudKey") ?? false) && "\(info?["PHImageResultIsInCloudKey"] ?? "1")" == "1") ? "暂不支持iCloud中的视频" : "此视频已损坏或已删除无法播放")
+                            }
+                            self?.videoData[(self?.lastSeletedIndex?.item)!].isSelected = false
+                            self?.collectionView.reloadItems(at: [(self?.lastSeletedIndex)!])
+                            self?.lastSeletedIndex = nil
+                            self?.selectedData = nil
+                            DispatchQueue.main.async { [weak self] in
+                                self?.sliderView.isHidden = true
+                                if (self?.playerHeaderView.frame.minY ?? 0.0) >= 0.0 {
+                                    UIView.animate(withDuration: 0.5, animations: { [weak self] in
+                                        self?.playerHeaderView.frame = CGRect(x: 0, y: -(self?.preViewSize.height ?? 0) - cDevice_iPhoneNavBarAndStatusBarHei, width: self?.preViewSize.width ?? 0, height: self?.preViewSize.height ?? 0)
+                                        self?.sliderView.frame.origin.y = self?.playerHeaderView.frame.maxY ?? 0
+                                        self?.playerHeaderView.center.x = self?.view.center.x ?? 0
+                                        self?.collectionView.frame = CGRect(x: 0, y: cDevice_iPhoneNavBarAndStatusBarHei, width: cScreenWidth, height: cScreenHeigth - cDevice_iPhoneNavBarAndStatusBarHei)
+                                    }) { _ in
+                                    }
+                                }
+                            }
+                            // 加载中...
+                            if self?.playerHeaderView != nil {
+                                BFLoadingHUB.shared.dismissHUB(superView: (self?.playerHeaderView)!)
+                            }
+                            return
+                        }
+                        DispatchQueue.main.async { [weak self] in
+                            self?.selectedData = itemData
+                            self?.selectedData?.localPath = (playerItem?.asset as! AVURLAsset).url.absoluteString
+                            self?.selectedData?.duration = (playerItem?.asset as! AVURLAsset).duration.seconds
+                            // 移除监听
+                            self?.removePlayerItemObserver()
+                            self?.playerItem = playerItem
+                            // 添加监听
+                            self?.addPlayerItemObserver()
+                            self?.playBtn.isHidden = true
+                            self?.avPlayer.replaceCurrentItem(with: playerItem)
+                            if self?.playerLayer.superlayer == nil {
+                                self?.playerHeaderView.layer.insertSublayer(self!.playerLayer, at: 0)
+                            }
+                            self?.avPlayer.play()
+//                            self?.playerHeaderView.image = itemData.image
+
+                            let tracks = (playerItem?.asset as? AVURLAsset)?.tracks(withMediaType: .video)
+                            if tracks != nil, (tracks?.count ?? 0) > 0 {
+                                let videoTrack = tracks?.first
+                                let transform = videoTrack?.preferredTransform
+                                let width: CGFloat = CGFloat(transform?.a ?? 0.0) * CGFloat(videoTrack?.naturalSize.width ?? 0.0) + CGFloat(transform?.c ?? 0.0) * CGFloat(videoTrack?.naturalSize.height ?? 0.0)
+                                let height: CGFloat = CGFloat(transform?.b ?? 0.0) * CGFloat(videoTrack?.naturalSize.width ?? 0.0) + CGFloat(transform?.d ?? 0.0) * CGFloat(videoTrack?.naturalSize.height ?? 0.0)
+                                self?.selectedData?.videoWidth = abs(width)
+                                self?.selectedData?.videoHeight = abs(height)
+                            } else {
+                                self?.selectedData?.videoWidth = CGFloat(self?.selectedData?.asset?.pixelWidth ?? 0)
+                                self?.selectedData?.videoHeight = CGFloat(self?.selectedData?.asset?.pixelHeight ?? 0)
+                            }
+                            self?.isPublishEnabled()
+                            self?.playerHeaderView.frame = CGRect(x: 0, y: cDevice_iPhoneNavBarAndStatusBarHei, width: self?.preViewSize.width ?? 0, height: self?.preViewSize.height ?? 0)
+                            if self?.playerHeaderView != nil {
+                                BFLoadingHUB.shared.showHUB(superView: (self?.playerHeaderView)!)
+                            }
+                            self?.playerHeaderView.center.x = self?.view.center.x ?? 0
+                            self?.sliderView.frame.origin.y = self?.playerHeaderView.frame.maxY ?? 0
+                            self?.collectionView.frame = CGRect(x: 0, y: cDevice_iPhoneNavBarAndStatusBarHei + CGFloat(self?.preViewSize.height ?? 0) + cDefaultMargin * 1.5, width: cScreenWidth, height: cScreenHeigth - cDevice_iPhoneNavBarAndStatusBarHei - CGFloat(self?.preViewSize.height ?? 0) - cDefaultMargin * 1.5)
+                            self?.collectionView.scrollToItem(at: indexPath, at: .top, animated: true)
+                            self?.sliderView.isHidden = false
+                            self?.playBtn.frame.origin = CGPoint(x: (CGFloat(self?.preViewSize.width ?? 0) - CGFloat(cDefaultMargin * 5)) / 2, y: (CGFloat(self?.preViewSize.height ?? 0) - CGFloat(cDefaultMargin * 5)) / 2)
+                            self?.playerLayer.frame = self?.playerHeaderView.bounds ?? CGRect.zero
+                        }
+                    }
+                } else {
+                    selectedData = itemData
+                }
+            }
+            isPublishEnabled()
+            collectionView.reloadItems(at: [indexPath])
+        } else {
+            videoData.removeAll()
+            self.collectionView.setContentOffset(CGPoint.zero, animated: true)
+            self.collectionView.reloadData()
+            allPhotos = categoryData[indexPath.item].categoryList
+            catagerySelectedIndex = indexPath
+            selecteBtn.setTitle(categoryData[indexPath.item].title, for: .normal)
+            selecteBtn.imagePosition(at: .right, space: cDefaultMargin / 2)
+            if !isAssetImage {
+                playBtn.isHidden = false
+                sliderView.isHidden = true
+                avPlayer.pause()
+                if selectedData != nil {
+                    selectedData = nil
+                    UIView.animate(withDuration: 0.5, animations: { [weak self] in
+                        self?.playerHeaderView.frame = CGRect(x: 0, y: -(self?.preViewSize.height ?? 0) - cDevice_iPhoneNavBarAndStatusBarHei, width: self?.preViewSize.width ?? 0, height: self?.preViewSize.height ?? 0)
+                        self?.playerHeaderView.center.x = self?.view.center.x ?? 0
+                        self?.sliderView.frame.origin.y = self?.playerHeaderView.frame.maxY ?? 0
+                        self?.collectionView.frame = CGRect(x: 0, y: cDevice_iPhoneNavBarAndStatusBarHei, width: cScreenWidth, height: cScreenHeigth - cDevice_iPhoneNavBarAndStatusBarHei)
+                    }) { _ in
+                    }
+                }
+            }
+            lastSeletedIndex = nil
+            isPublishEnabled()
+            if allPhotos.count <= 0 {
+                emptyRemindView.frame = CGRect(x: 0, y: cDevice_iPhoneNavBarAndStatusBarHei, width: cScreenWidth, height: cScreenHeigth - cDevice_iPhoneNavBarAndStatusBarHei - cDevice_iPhoneTabBarHei)
+                emptyRemindView.emptyData = emptyData
+                emptyRemindView.isHidden = false
+            } else {
+                emptyRemindView.isHidden = true
+            }
+            self.collectionView.reloadData()
+            cancelClick()
+        }
+    }
+
+    open func collectionView(_ collectionView: UICollectionView, layout _: UICollectionViewLayout, sizeForItemAt _: IndexPath) -> CGSize {
+        if collectionView == self.collectionView {
+            return CGSize(width: (cScreenWidth - cDefaultMargin) / 3, height: (cScreenWidth - cDefaultMargin) / 3)
+        }
+        return CGSize(width: collectionView.frame.width, height: cDefaultMargin * 8)
+    }
+
+    open func collectionView(_ collectionView: UICollectionView, layout _: UICollectionViewLayout, minimumLineSpacingForSectionAt _: Int) -> CGFloat {
+        if collectionView == self.collectionView {
+            return cDefaultMargin / 2
+        }
+        return 0
+    }
+
+    open func collectionView(_ collectionView: UICollectionView, layout _: UICollectionViewLayout, minimumInteritemSpacingForSectionAt _: Int) -> CGFloat {
+        if collectionView == self.collectionView {
+            return cDefaultMargin / 2
+        }
+        return 0
+    }
+
+    open func scrollViewDidScroll(_: UIScrollView) {
+        // 这里是不是有用的?
+//        if bf_getCurrentViewController() is PQUploadController || bf_getCurrentViewController() is PQImageSelectedController {
+//            if scrollView == collectionView {
+//                updateCachedAssets()
+//            } else {
+//                BFLog(message: "contentOffset = \(scrollView.contentOffset),contentSize = \(scrollView.contentSize)")
+//                if scrollView.contentOffset.y > ((scrollView.contentSize.height - scrollView.frame.height) + cDefaultMargin * 10) {
+//                    cancelClick()
+//                }
+//            }
+//        }
+    }
+}
+
+extension PQUploadController {
+    /// 添加监听
+    /// - Parameter playerItem: <#playerItem description#>
+    /// - Returns: <#description#>
+    func addPlayerItemObserver() {
+        playerItem?.addObserver(self, forKeyPath: "status", options: .new, context: nil)
+    }
+
+    /// 移除监听
+    /// - Returns: <#description#>
+    func removePlayerItemObserver() {
+        playerItem?.removeObserver(self, forKeyPath: "status")
+    }
+
+    open override func observeValue(forKeyPath keyPath: String?, of object: Any?, change _: [NSKeyValueChangeKey: Any]?, context _: UnsafeMutableRawPointer?) {
+        if object is AVPlayerItem, keyPath == "status" {
+            BFLog(message: "(object as! AVPlayerItem).status = \((object as! AVPlayerItem).status.rawValue)")
+            BFLoadingHUB.shared.dismissHUB(superView: playerHeaderView)
+            switch (object as! AVPlayerItem).status {
+            case .unknown:
+                break
+            case .readyToPlay:
+                break
+            case .failed:
+                break
+            default:
+                break
+            }
+        }
+    }
+
+    private func resetCachedAssets() {
+        imageManager.stopCachingImagesForAllAssets()
+        previousPreheatRect = .zero
+    }
+
+    private func updateCachedAssets() {
+        if allPhotos != nil, allPhotos.count <= 0 {
+            return
+        }
+        guard isViewLoaded, view.window != nil else { return }
+        let visibleRect = CGRect(origin: collectionView.contentOffset, size: collectionView.bounds.size)
+        let preheatRect = visibleRect.insetBy(dx: 0, dy: -0.5 * visibleRect.height)
+        let delta = abs(preheatRect.midY - previousPreheatRect.midY)
+        guard delta > view.bounds.height / 3 else { return }
+        let (addedRects, removedRects) = differencesBetweenRects(previousPreheatRect, preheatRect)
+        let addedAssets = addedRects
+            .flatMap { rect in collectionView.indexPathsForElements(in: rect) }
+            .map { indexPath in allPhotos.object(at: indexPath.item) }
+        let removedAssets = removedRects
+            .flatMap { rect in collectionView.indexPathsForElements(in: rect) }
+            .map { indexPath in allPhotos.object(at: indexPath.item) }
+        imageManager.startCachingImages(for: addedAssets,
+                                        targetSize: itemSize, contentMode: .aspectFill, options: nil)
+        imageManager.stopCachingImages(for: removedAssets,
+                                       targetSize: itemSize, contentMode: .aspectFill, options: nil)
+
+        previousPreheatRect = preheatRect
+    }
+
+    private func differencesBetweenRects(_ old: CGRect, _ new: CGRect) -> (added: [CGRect], removed: [CGRect]) {
+        if old.intersects(new) {
+            var added = [CGRect]()
+            if new.maxY > old.maxY {
+                added += [CGRect(x: new.origin.x, y: old.maxY,
+                                 width: new.width, height: new.maxY - old.maxY)]
+            }
+            if old.minY > new.minY {
+                added += [CGRect(x: new.origin.x, y: new.minY,
+                                 width: new.width, height: old.minY - new.minY)]
+            }
+            var removed = [CGRect]()
+            if new.maxY < old.maxY {
+                removed += [CGRect(x: new.origin.x, y: new.maxY,
+                                   width: new.width, height: old.maxY - new.maxY)]
+            }
+            if old.minY < new.minY {
+                removed += [CGRect(x: new.origin.x, y: old.minY,
+                                   width: new.width, height: new.minY - old.minY)]
+            }
+            return (added, removed)
+        } else {
+            return ([new], [old])
+        }
+    }
+}
+
+extension PQUploadController: PHPhotoLibraryChangeObserver {
+    public func photoLibraryDidChange(_ changeInstance: PHChange) {
+        // Change notifications may be made on a background queue. Re-dispatch to the
+        // main queue before acting on the change as we'll be updating the UI.
+        DispatchQueue.main.sync { [weak self] in
+            // Check each of the three top-level fetches for changes.
+            if allPhotos != nil, changeInstance.changeDetails(for: allPhotos) != nil {
+                self?.categoryData.removeAll()
+                self?.loadPhotoData()
+            }
+        }
+    }
+}

+ 106 - 0
BFStuckPointKit/Classes/View/PQAssetCategoryCell.swift

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

+ 82 - 0
BFStuckPointKit/Classes/View/PQBaseVideoInfoView.swift

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

+ 669 - 0
BFStuckPointKit/Classes/ViewModel/PQBaseViewModel.swift

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

+ 76 - 0
BFStuckPointKit/Classes/ViewModel/PQDownloadFileManager.swift

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

+ 227 - 0
BFStuckPointKit/Classes/ViewModel/PQDownloadManager.swift

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

+ 190 - 0
BFStuckPointKit/Classes/ViewModel/PQSessionManager.swift

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

+ 159 - 0
BFStuckPointKit/Classes/ViewModel/PQUploadViewModel.swift

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