PQStuckPointEditerController.swift 41 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764
  1. //
  2. // PQStuckPointEditerController.swift
  3. // PQSpeed
  4. //
  5. // Created by ak on 2021/4/26.
  6. // Copyright © 2021 BytesFlow. All rights reserved.
  7. // 功能:卡点音乐编辑界面
  8. import Foundation
  9. import ObjectMapper
  10. import RealmSwift
  11. import UIKit
  12. class PQStuckPointEditerController: PQBaseViewController {
  13. // 是否导出视频成功
  14. var isExportVideosSuccess: Bool = false
  15. // 是否请求卡点数据成功
  16. var isStuckPointDataSuccess: Bool = false
  17. // 是否同步音乐成功
  18. var isSynchroMusicInfoSuccess: Bool = false
  19. /// 当前所有的filter
  20. var filters: Array = Array<ImageProcessingOperation>.init()
  21. // 选中的总时长
  22. var selectedTotalDuration: Float64 = 0
  23. // 选择的总数
  24. var selectedDataCount: Int = 0
  25. // 选择的图片总数
  26. var selectedImageDataCount: Int = 0
  27. // 选中的素材数据
  28. var selectedPhotoData: [PQEditVisionTrackMaterialsModel]?
  29. // 选中的音乐数据
  30. var stuckPointMusicData: PQVoiceModel?
  31. // 保存所有段的所有贴纸,音频信息,用于播放器的渲染使用
  32. var projectModel: PQEditProjectModel = PQEditProjectModel()
  33. // 从草稿箱进入的项目数据
  34. var draftProjectModel: PQEditProjectModel?
  35. var mStickers: [PQEditVisionTrackMaterialsModel]?
  36. // 播放器的开始和结束时间,1,刚进界面使用推荐的开始结束时间,2,用户修改起结点时修改
  37. var playeTimeRange: CMTimeRange = CMTimeRange()
  38. // 下一步
  39. lazy var nextBtn: UIButton = {
  40. let nextBtn = UIButton(type: .custom)
  41. nextBtn.frame = CGRect(x: cScreenWidth - 16 - cDefaultMargin * 6, y: cDevice_iPhoneStatusBarHei + (cDevice_iPhoneNavBarHei - cDefaultMargin * 3) / 2, width: cDefaultMargin * 6, height: cDefaultMargin * 3)
  42. nextBtn.setTitle("去合成", for: .normal)
  43. nextBtn.titleLabel?.font = UIFont.systemFont(ofSize: 13)
  44. nextBtn.addTarget(self, action: #selector(nextBtnClick(sender:)), for: .touchUpInside)
  45. nextBtn.backgroundColor = UIColor.hexColor(hexadecimal: PQBFConfig.shared.styleColor.rawValue)
  46. nextBtn.setTitleColor(UIColor.hexColor(hexadecimal: "#FFFFFF"), for: .normal)
  47. nextBtn.uxy_acceptEventInterval = 0.5
  48. nextBtn.addCorner(corner: 3)
  49. return nextBtn
  50. }()
  51. // 播放器显示 view
  52. lazy var playerView: PQGPUImagePlayerView = {
  53. let playerHeight = cScreenWidth
  54. let playerView = PQGPUImagePlayerView(frame: CGRect(x: 0, y: navHeadImageView?.frame.maxY ?? 0, width: playerHeight, height: playerHeight))
  55. playerView.backgroundColor = PQBFConfig.shared.styleBackGroundColor
  56. playerView.isShowLine = false
  57. return playerView
  58. }()
  59. /// 节奏选择视图
  60. lazy var sustomSwitchView: PQCustomSwitchView = {
  61. let sustomSwitchView = PQCustomSwitchView(frame: CGRect(x: (view.frame.width - cDefaultMargin * 28) / 2, y: view.frame.height - cSafeAreaHeight - cDefaultMargin * 3 - cDefaultMargin * 3, width: cDefaultMargin * 28, height: 35), titles: ["快节奏", "适中", "慢节奏"], defaultIndex: stuckPointMusicData?.speed ?? 2)
  62. sustomSwitchView.switchChangeHandle = { [weak self] sender in
  63. // 改变速率
  64. self?.stuckPointMusicData?.speed = sender.tag
  65. self?.projectModel.sData?.getBGMSession()?.sectionTimeline?.audioTrack?.audioTrackMaterials.first?.bgmInfo?.rhythmMusicSpeed = sender.tag
  66. // 播放前先暂停
  67. self?.playerView.stop()
  68. // 开始播放
  69. self?.settingPlayerView()
  70. // 点击上报:选择节奏
  71. PQEventTrackViewModel.baseReportUpload(businessType: .bt_buttonClick, objectType: .ot_click_selectRhythm, pageSource: .sp_stuck_previewSyncedUp, extParams: ["rhythmNumber": sender.tag], remindmsg: "点击上报:选择节奏")
  72. }
  73. return sustomSwitchView
  74. }()
  75. /// 裁剪视图
  76. lazy var stuckPointCuttingView: PQStuckPointCuttingView = {
  77. let stuckPointCuttingView = PQStuckPointCuttingView(frame: CGRect(x: 0, y: sustomSwitchView.frame.minY - cDefaultMargin * 14 - cDefaultMargin * 2, width: view.frame.width, height: cDefaultMargin * 14), duration: CGFloat(Float(stuckPointMusicData?.duration ?? "0") ?? 0), startTime: CGFloat(stuckPointMusicData?.startTime ?? 0), endTime: CGFloat(stuckPointMusicData?.endTime ?? 0))
  78. /// 裁剪进度回调
  79. stuckPointCuttingView.videoRangeDidChanged = { [weak self] startTime, endTime in
  80. BFLog(message: "裁剪返回--startTime = \(startTime),endTime = \(endTime)")
  81. }
  82. /// 播放进度回调
  83. stuckPointCuttingView.videoProgressDidChanged = { [weak self] progress in
  84. BFLog(message: "进度更新返回--progress = \(progress) \(String(describing: self?.playerView.mPlayeTimeRange))")
  85. }
  86. /// 拖缀结束的回调 type - 1-拖动左边裁剪结束 2--拖动右边裁剪结束 3-进度条拖动结束 4-滑动结束
  87. stuckPointCuttingView.videoDidEndDragging = { [weak self] type, startTime, endTime, progress in
  88. BFLog(message: "拖拽结束返回--type = \(type),startTime = \(startTime),endTime = \(endTime),progress = \(progress)")
  89. self?.playerView.pause()
  90. // 修改最新值
  91. self?.stuckPointMusicData?.startTime = Float64(startTime)
  92. self?.stuckPointMusicData?.endTime = Float64(endTime)
  93. // 红的指针完成
  94. if type == 3 {
  95. if CMTimeGetSeconds(self?.playerView.mPlayeTimeRange?.end ?? .zero) == 0 {
  96. BFLog(message: "mPlayeTimeRange is error")
  97. return
  98. }
  99. let newBeginSconds = (Double(startTime) + (Double(endTime) - Double(startTime)) * Double(progress)) * 600
  100. BFLog(message: " newBeginSconds is \(newBeginSconds)")
  101. let seekTimeRange: CMTimeRange = CMTimeRange(start: CMTime(value: CMTimeValue(Int64(newBeginSconds)), timescale: 600), end:
  102. CMTime(value: CMTimeValue(Int64(endTime * 600)), timescale: 600))
  103. BFLog(message: "修改的开始 \(CMTimeGetSeconds(seekTimeRange.start)) 结束 \(CMTimeGetSeconds(seekTimeRange.end))")
  104. self?.playerView.play(pauseFirstFrame: false, playeTimeRange: seekTimeRange)
  105. } else {
  106. // 更改素材开始时间及结束时间
  107. self?.projectModel.sData?.getBGMSession()?.sectionTimeline?.audioTrack?.audioTrackMaterials.first?.out = Float64(endTime)
  108. self?.projectModel.sData?.getBGMSession()?.sectionTimeline?.audioTrack?.audioTrackMaterials.first?.model_in = Float64(startTime)
  109. self?.projectModel.sData?.getBGMSession()?.sectionTimeline?.audioTrack?.audioTrackMaterials.first?.timelineIn = Float64(startTime)
  110. self?.projectModel.sData?.getBGMSession()?.sectionTimeline?.audioTrack?.audioTrackMaterials.first?.timelineOut = Float64(endTime)
  111. // 初始化音频的开始和结束时间
  112. self?.playeTimeRange = CMTimeRange(start: CMTime(value: CMTimeValue(Int64(startTime * 600)), timescale: 600), end: CMTime(value: CMTimeValue(Int64(endTime * 600)), timescale: 600))
  113. DispatchQueue.global().async { // 并行、异步
  114. self?.mStickers = self?.createStickers(sections: self?.projectModel.sData?.sections ?? List(), inputSize: CGSize(width: CGFloat(self?.projectModel.sData?.videoMetaData?.videoWidth ?? 0), height: CGFloat(self?.projectModel.sData?.videoMetaData?.videoHeight ?? 0)))
  115. self?.playerView.mStickers = self?.mStickers
  116. DispatchQueue.main.async { // 串行、异步
  117. self?.playerView.play(pauseFirstFrame: false, playeTimeRange: self!.playeTimeRange)
  118. }
  119. }
  120. }
  121. // 埋点上报
  122. if type == 1 || type == 2 {
  123. // 点击上报:拖动拖拽条(左/右部分)
  124. PQEventTrackViewModel.baseReportUpload(businessType: .bt_buttonClick, objectType: type == 1 ? .ot_click_dragFront : .ot_click_dragBehind, pageSource: .sp_stuck_previewSyncedUp, extParams: ["targetTime": type == 1 ? startTime * 1000 : endTime * 1000], remindmsg: "点击上报:拖动拖拽条(左/右部分)")
  125. }
  126. }
  127. return stuckPointCuttingView
  128. }()
  129. /// 卡点时长显示视图
  130. lazy var timeRemindLab: UILabel = {
  131. let timeRemindLab = UILabel(frame: CGRect(x: 0, y: stuckPointCuttingView.frame.minY - cDefaultMargin * 4 - cDefaultMargin * 3, width: view.frame.width, height: cDefaultMargin * 4))
  132. timeRemindLab.backgroundColor = UIColor.hexColor(hexadecimal: "#262626")
  133. timeRemindLab.textAlignment = .center
  134. timeRemindLab.font = UIFont.systemFont(ofSize: 12)
  135. timeRemindLab.textColor = UIColor.hexColor(hexadecimal: "#999999")
  136. let total: Float64 = ((stuckPointMusicData?.endTime ?? Float64(stuckPointMusicData?.duration ?? "0") ?? 0) - (stuckPointMusicData?.startTime ?? 0))
  137. timeRemindLab.text = "现卡点时长\(total.formatDurationToHMS()) / 原视频总时长\(selectedTotalDuration.formatDurationToHMS())"
  138. return timeRemindLab
  139. }()
  140. /// 音乐标题
  141. lazy var musicNameView: UIView = {
  142. let musicNameView = UIView()
  143. musicNameView.addSubview(musicNameLab)
  144. let nameWidth: CGFloat = musicNameLab.frame.width + (25 + cDefaultMargin * 3)
  145. musicNameView.frame = CGRect(x: (view.frame.width - nameWidth) / 2, y: cDevice_iPhoneStatusBarHei + (cDevice_iPhoneNavBarHei - cDefaultMargin * 3) / 2, width: nameWidth, height: cDefaultMargin * 3)
  146. // musicNameView.backgroundColor = UIColor.hexColor(hexadecimal: "#333333")
  147. musicNameView.addCorner(corner: musicNameView.frame.height / 2)
  148. let musicImageView = UIImageView()
  149. musicImageView.tintColor = PQBFConfig.shared.styleTitleColor
  150. musicImageView.image = UIImage().BF_Image(named: "stuckPoint_reCreate_music").withRenderingMode(.alwaysTemplate)
  151. musicImageView.frame = CGRect(x: musicNameView.frame.height / 2 - 5, y: (musicNameView.frame.height - 22) / 2, width: 22, height: 22)
  152. musicNameView.addSubview(musicImageView)
  153. musicNameLab.frame.origin.x = musicImageView.frame.maxX + 5
  154. let ges = UITapGestureRecognizer(target: self, action: #selector(musicNameClick))
  155. musicNameView.addGestureRecognizer(ges)
  156. return musicNameView
  157. }()
  158. /// 音乐歌曲名称
  159. lazy var musicNameLab: LMJHorizontalScrollText = {
  160. let nameWidth: CGFloat = sizeWithText(text: "\(stuckPointMusicData?.musicName ?? "")", font: UIFont.systemFont(ofSize: 13), size: CGSize(width: view.frame.width - ((cDefaultMargin * 6 + 16 * 2) * 2) - (25 + cDefaultMargin * 3), height: cDefaultMargin * 3)).width
  161. let musicNameLab = LMJHorizontalScrollText(frame: CGRect(x: 0, y: 0, width: nameWidth < cDefaultMargin * 4 ? cDefaultMargin * 4 : nameWidth, height: cDefaultMargin * 3))
  162. musicNameLab.textColor = PQBFConfig.shared.styleTitleColor
  163. musicNameLab.textFont = UIFont.systemFont(ofSize: 13)
  164. musicNameLab.speed = 0.03
  165. musicNameLab.moveDirection = LMJTextScrollMoveLeft
  166. musicNameLab.moveMode = LMJTextScrollContinuous
  167. if nameWidth < cDefaultMargin * 4 {
  168. musicNameLab.text = " \(stuckPointMusicData?.musicName ?? "") "
  169. } else {
  170. musicNameLab.text = "\(stuckPointMusicData?.musicName ?? "")"
  171. }
  172. return musicNameLab
  173. }()
  174. /// 同步进度显示
  175. lazy var synchroMarskView: PQStuckPointLoadingView = {
  176. var synchroMarskView = PQStuckPointLoadingView(frame: CGRect(x: 0, y: 0, width: cScreenWidth, height: cScreenHeigth))
  177. synchroMarskView.cancelHandle = { [weak self] _ in
  178. self?.navigationController?.popViewController(animated: true)
  179. }
  180. return synchroMarskView
  181. }()
  182. override func viewWillAppear(_ animated: Bool) {
  183. super.viewDidAppear(animated)
  184. lineView?.isHidden = true
  185. UIApplication.shared.isIdleTimerDisabled = true
  186. musicNameLab.move()
  187. PQNotification.addObserver(self, selector: #selector(enterBackground), name: UIApplication.didEnterBackgroundNotification, object: nil)
  188. }
  189. @objc func enterBackground() {
  190. BFLog(message: "进入到后台")
  191. // 取消导出
  192. playerView.pause()
  193. }
  194. override func backBtnClick() {
  195. super.backBtnClick()
  196. playerView.pause()
  197. // 点击上报:返回按钮
  198. PQEventTrackViewModel.baseReportUpload(businessType: .bt_buttonClick, objectType: .ot_click_back, pageSource: .sp_stuck_previewSyncedUp, extParams: nil, remindmsg: "卡点视频数据上报-(点击上报:返回按钮)")
  199. }
  200. @objc func musicNameClick() {
  201. // let musicVc = navigationController?.viewControllers.first(where: { vc in
  202. // vc is PQStuckPointMusicController
  203. // })
  204. // if musicVc != nil {
  205. // navigationController?.popToViewController(musicVc!, animated: true)
  206. // }
  207. }
  208. override func viewWillDisappear(_ animated: Bool) {
  209. super.viewWillDisappear(animated)
  210. UIApplication.shared.isIdleTimerDisabled = false
  211. musicNameLab.stop()
  212. playerView.pause()
  213. }
  214. override func viewDidLoad() {
  215. super.viewDidLoad()
  216. leftButton(image: "icon_detail_back", tintColor: PQBFConfig.shared.styleTitleColor)
  217. navHeadImageView?.addSubview(nextBtn)
  218. navHeadImageView?.addSubview(musicNameView)
  219. // 添加子视图
  220. addSubViews()
  221. // 导出相册视频
  222. exportPhotoData()
  223. // 同步音乐数据
  224. synchroMusicInfoData()
  225. // 曝光上报:预览页面曝光上报
  226. PQEventTrackViewModel.baseReportUpload(businessType: .bt_windowView, objectType: .ot_view_previewSyncedUp, pageSource: .sp_stuck_previewSyncedUp, extParams: nil, remindmsg: "卡点视频数据上报-(曝光上报:预览页面曝光上报)")
  227. }
  228. /// 添加子视图
  229. /// - Returns: <#description#>
  230. func addSubViews() {
  231. if (stuckPointMusicData?.rhythmSdata.count ?? 0) <= 0 {
  232. return
  233. }
  234. view.addSubview(playerView)
  235. view.addSubview(sustomSwitchView)
  236. view.addSubview(stuckPointCuttingView)
  237. // view.addSubview(timeRemindLab)
  238. // 添加一个背景区分不同色值
  239. let backView: UIView = UIView()
  240. backView.backgroundColor = UIColor.hexColor(hexadecimal: "#262626")
  241. view.insertSubview(backView, aboveSubview: navHeadImageView!)
  242. backView.frame = CGRect(x: 0, y: navHeadImageView?.frame.height ?? 0, width: cScreenWidth, height: (stuckPointCuttingView.frame.minY - cDefaultMargin * 3) - (navHeadImageView?.frame.height ?? 0))
  243. }
  244. @objc func nextBtnClick(sender _: UIButton) {
  245. BFLog(message: "去发布")
  246. playerView.pause()
  247. let videoExporter = PQStuckPointPublicController()
  248. videoExporter.selectedTotalDuration = selectedTotalDuration
  249. videoExporter.selectedDataCount = selectedDataCount
  250. videoExporter.selectedImageDataCount = selectedImageDataCount
  251. // 使用深 copy
  252. let json = projectModel.toJSONString(prettyPrint: false)
  253. if json == nil {
  254. BFLog(message: "数据转换有问题 跳转")
  255. return
  256. }
  257. let tempModel: PQEditProjectModel? = Mapper<PQEditProjectModel>().map(JSONString: json!)
  258. videoExporter.mStickers = mStickers
  259. videoExporter.audioMixModel = stuckPointMusicData
  260. videoExporter.editProjectModel = tempModel
  261. navigationController?.pushViewController(videoExporter, animated: true)
  262. // 点击上报:去合成
  263. PQEventTrackViewModel.baseReportUpload(businessType: .bt_buttonClick, objectType: .ot_click_commit, pageSource: .sp_stuck_previewSyncedUp, extParams: ["musicName": stuckPointMusicData?.musicName ?? "", "musicId": stuckPointMusicData?.musicId ?? "", "rhythmNumber": stuckPointMusicData?.speed ?? 2, "duration": ((stuckPointMusicData?.endTime ?? 0) - (stuckPointMusicData?.startTime ?? 0)) * 1000], remindmsg: "点击上报:去合成")
  264. }
  265. // MARK: - 播放器相关操作
  266. /// seek 播放器
  267. /// - Parameter playeTimeRange: 开始和结束时间
  268. func seekPlayer(playeTimeRange: CMTimeRange) {
  269. playerView.setEnableSeek(isSeek: true)
  270. playerView.play(pauseFirstFrame: false, playeTimeRange: playeTimeRange)
  271. }
  272. /// 通过传入的 selectedPhotoData 、 stuckPointMusicData 创建 projectModel 模型 后面都使用 projectModel 参数
  273. func createPorjectData() {
  274. // 1,添加选择的视觉素材
  275. let section: PQEditSectionModel = PQEditSectionModel()
  276. selectedPhotoData?.forEach { model in
  277. let json = model.toJSONString(prettyPrint: false)
  278. if json == nil {
  279. BFLog(message: "数据转换有问题 跳转")
  280. return
  281. }
  282. let tempModel: PQEditVisionTrackMaterialsModel = Mapper<PQEditVisionTrackMaterialsModel>().map(JSONString: json!)!
  283. section.sectionTimeline?.visionTrack?.visionTrackMaterials.append(tempModel)
  284. }
  285. projectModel.sData?.sections.append(section)
  286. // 2,添加背景音乐
  287. projectModel.sData?.addBGM(audioMix: stuckPointMusicData!)
  288. }
  289. // 设置播放器
  290. func settingPlayerView() {
  291. // 1,设置播放器的显示区域 和画布大小
  292. // - 按第一个素材尺寸自适应
  293. let playerShowHeight = (stuckPointCuttingView.frame.minY - cDefaultMargin * 3) - (navHeadImageView?.frame.maxY ?? 0)
  294. var showRect: CGRect = PQPlayerViewModel.getShowCanvasRect(editProjectModel: projectModel, showType: 1, playerViewHeight: playerShowHeight)
  295. if showRect.size.width == showRect.size.height {
  296. if cScreenWidth > playerShowHeight {
  297. showRect.origin.x = (cScreenWidth - playerShowHeight) / 2
  298. showRect.size.width = playerShowHeight
  299. showRect.size.height = playerShowHeight
  300. } else {
  301. showRect.origin.x = 0
  302. showRect.size.width = cScreenWidth
  303. showRect.size.height = cScreenWidth
  304. }
  305. }
  306. showRect.origin.y = (playerShowHeight - showRect.size.height) / 2.0 + (navHeadImageView?.frame.maxY ?? 0)
  307. if showRect.size.width != 0, showRect.size.height != 0 {
  308. playerView.resetCanvasFrame(frame: showRect)
  309. }
  310. var firstModel: PQEditVisionTrackMaterialsModel?
  311. for part in projectModel.sData!.sections {
  312. if part.sectionTimeline?.visionTrack?.getEnableVisionTrackMaterials().count ?? 0 > 0 {
  313. firstModel = part.sectionTimeline?.visionTrack?.getEnableVisionTrackMaterials().first
  314. break
  315. }
  316. }
  317. var videoSize: CGSize = CGSize(width: Int(firstModel?.width ?? 0), height: Int(firstModel?.height ?? 0))
  318. var minSlider = min(videoSize.width, videoSize.height)
  319. var maxSlider = max(videoSize.width, videoSize.height)
  320. let ration = 1080 / minSlider
  321. minSlider = minSlider * ration
  322. maxSlider = maxSlider * ration
  323. if videoSize.width > videoSize.height { // 宽屏
  324. videoSize = CGSize(width: maxSlider, height: minSlider)
  325. } else {
  326. videoSize = CGSize(width: minSlider, height: maxSlider)
  327. }
  328. let maxValue = max(firstModel?.width ?? 0, firstModel?.height ?? 0)
  329. if maxValue > 1920 {
  330. let maxRation = 1920 / maxValue
  331. videoSize = CGSize(width: videoSize.width * CGFloat(maxRation), height: videoSize.height * CGFloat(maxRation))
  332. BFLog(message: "最长边已经超过 1920 要等比缩小 缩放后\(videoSize)")
  333. }
  334. if (Int(videoSize.width) % 2) != 0 {
  335. videoSize.width = videoSize.width - 1
  336. }
  337. if (Int(videoSize.height) % 2) != 0 {
  338. videoSize.height = videoSize.height - 1
  339. }
  340. projectModel.sData?.videoMetaData?.videoWidth = Int(videoSize.width)
  341. projectModel.sData?.videoMetaData?.videoHeight = Int(videoSize.height)
  342. // 2,创建滤镜
  343. mStickers = createStickers(sections: projectModel.sData?.sections ?? List(), inputSize: CGSize(width: CGFloat(projectModel.sData?.videoMetaData?.videoWidth ?? 0), height: CGFloat(projectModel.sData?.videoMetaData?.videoHeight ?? 0)))
  344. playerView.mStickers = mStickers
  345. // 3,设置音频
  346. let audioPath = stuckPointMusicData?.localPath ?? ""
  347. BFLog(message: "初始化音频播放器的音频地址为:\(audioPath)")
  348. playerView.updateAsset(URL(fileURLWithPath: documensDirectory + audioPath), videoComposition: nil, audioMixModel: nil)
  349. // 4, 设置播放器的输出画布大小
  350. playerView.movie?.mShowVidoSize = CGSize(width: CGFloat(projectModel.sData?.videoMetaData?.videoWidth ?? 0), height: CGFloat(projectModel.sData?.videoMetaData?.videoHeight ?? 0))
  351. // 5,开始播放
  352. playerView.isLoop = false
  353. playerView.showProgressLab = false
  354. // 初始化音频的开始和结束时间
  355. BFLog(message: "播放的器 开始\(String(describing: CMTimeGetSeconds(playeTimeRange.start))) 结束 \(String(describing: CMTimeGetSeconds(playeTimeRange.end)))")
  356. playerView.play(pauseFirstFrame: false, playeTimeRange: CMTimeRange(start: playeTimeRange.start, end: playeTimeRange.end))
  357. // 6,进度回调
  358. playerView.progress = { [weak self] currentTime, tatolTime, _ in
  359. // 更新进度
  360. let progress = (currentTime - CMTimeGetSeconds(self?.playeTimeRange.start ?? .zero
  361. )) / CMTimeGetSeconds(self?.playeTimeRange.duration ?? .zero
  362. )
  363. BFLog(message: "\(currentTime) \(tatolTime) 显示播放器进度为: \(progress)")
  364. self?.stuckPointCuttingView.videoCropView.updateProgress(progress: CGFloat(progress))
  365. }
  366. }
  367. deinit {
  368. BFLog(message: "卡点视频预览界面销毁")
  369. musicNameLab.stop()
  370. playerView.pause()
  371. // 取消所有的导出
  372. PQSingletoMemoryUtil.shared.allExportSession.forEach { _, exportSession in
  373. exportSession.cancelExport()
  374. }
  375. }
  376. }
  377. // MARK: - 视频渲染相关逻辑方法
  378. extension PQStuckPointEditerController {
  379. /// 分割视频 这里只设置视频类型的 in 和 out 并不设置显示的开始和结束时间 mp4 ,png ,png ,mp4
  380. /// - Parameter section: 当前段
  381. /// - Parameter stuckPoints: 用户选择的,或推荐的卡点数
  382. /// - Returns: 返回分割后的所有 stickers 和卡点数是一致的
  383. func clipVideoMerage(section: PQEditSectionModel, stuckPoints: [Float]) -> [PQEditVisionTrackMaterialsModel] {
  384. var stickers: Array = Array<PQEditVisionTrackMaterialsModel>.init()
  385. // 第二种情况:有视频要进行分割
  386. /*
  387. 1, 确定每个视频素材需要切的段数p
  388. 2, 将所有视频时长相加,得到总视频素材时长L = l1 + l2 + ... + ln
  389. 3, 视频素材a1需要切分的个数clipNum = max (round (kongduan * a1 / L) , 1)
  390. */
  391. // 要补的空位数
  392. let kongduan: Int = Int(stuckPoints.count) - 1 - Int(section.sectionTimeline!.visionTrack?.getEnableVisionTrackMaterials().count ?? 0)
  393. // 所有视频总时长
  394. var videoTotalDuration: Float64 = 0.0
  395. for video in section.sectionTimeline!.visionTrack!.getEnableVisionTrackMaterials(type: "video") {
  396. let asset: AVURLAsset = AVURLAsset(url: URL(fileURLWithPath: documensDirectory + video.locationPath), options: avAssertOptions)
  397. videoTotalDuration = videoTotalDuration + Float64(CMTimeGetSeconds(asset.duration))
  398. }
  399. if videoTotalDuration == 0 {
  400. BFLog(message: "视频总时长出现错误!!!!这里应该有视频素材的")
  401. return stickers
  402. }
  403. for sticker in section.sectionTimeline!.visionTrack!.getEnableVisionTrackMaterials() {
  404. if sticker.type == StickerType.VIDEO.rawValue {
  405. let asset: AVURLAsset = AVURLAsset(url: URL(fileURLWithPath: documensDirectory + sticker.locationPath), options: avAssertOptions)
  406. // 要分割的段落
  407. let clipNum = Int(max(round(Double(kongduan) * CMTimeGetSeconds(asset.duration) / videoTotalDuration), 1))
  408. sticker.duration = CMTimeGetSeconds(asset.duration)
  409. BFLog(message: "单个视频\(sticker.locationPath)时长::\(CMTimeGetSeconds(asset.duration)) ,clipNum is:\(clipNum)")
  410. for clipindex in 0 ... clipNum {
  411. // deep copy sticker model 防止只有一个对象
  412. let stickerjson = sticker.toJSONString(prettyPrint: false)
  413. let deepCopySticker = Mapper<PQEditVisionTrackMaterialsModel>().map(JSONString: stickerjson!)
  414. // 设置循环模式和适配模式
  415. deepCopySticker?.generateDefaultValues()
  416. deepCopySticker?.model_in = clipindex == 0 ? 0 : CMTimeGetSeconds(asset.duration) / Double(clipNum) * Double(clipindex)
  417. deepCopySticker?.out = (deepCopySticker?.model_in ?? 0) + CMTimeGetSeconds(asset.duration) / Double(clipNum)
  418. if (deepCopySticker?.model_in ?? 0) >= CMTimeGetSeconds(asset.duration) || (deepCopySticker?.out ?? 0) >= CMTimeGetSeconds(asset.duration) {
  419. deepCopySticker?.model_in = CMTimeGetSeconds(asset.duration) - CMTimeGetSeconds(asset.duration) / Double(clipNum)
  420. deepCopySticker?.out = CMTimeGetSeconds(asset.duration)
  421. }
  422. BFLog(message: " crilp is in \(deepCopySticker?.model_in ?? 0) out \(deepCopySticker?.out ?? 0) 总时长\(CMTimeGetSeconds(asset.duration))")
  423. if deepCopySticker != nil {
  424. stickers.append(deepCopySticker!)
  425. }
  426. }
  427. } else if sticker.type == StickerType.IMAGE.rawValue {
  428. sticker.generateDefaultValues()
  429. stickers.append(sticker)
  430. }
  431. }
  432. return stickers
  433. }
  434. /// 创建sticker
  435. /// - Parameters:
  436. /// - sections: 项目所有段落数据信息
  437. /// - inputSize: 画布大小
  438. /// - Returns: filters 数据 播放器可直接使用
  439. func createStickers(sections: List<PQEditSectionModel>, inputSize _: CGSize = .zero) -> [PQEditVisionTrackMaterialsModel] {
  440. // 保存滤镜对象数据
  441. var stickers: Array = Array<PQEditVisionTrackMaterialsModel>.init()
  442. for section in sections {
  443. if section.sectionType == "normal" {
  444. // 推荐卡点数
  445. var stuckPoints: Array = Array<Float>.init()
  446. var stuckPointsTemp = Array<Float>.init()
  447. for (index, dunshu) in stuckPointMusicData!.rhythmSdata[0].pointTimes.enumerated() {
  448. BFLog(message: "所有卡点数:\(Float64(dunshu) / 1_000_000.0)")
  449. if Float64(dunshu) / 1_000_000.0 > CMTimeGetSeconds(playeTimeRange.start), Float64(dunshu) / 1_000_000.0 < CMTimeGetSeconds(playeTimeRange.end) {
  450. stuckPointsTemp.append(Float(dunshu) / 1_000_000.0)
  451. }
  452. }
  453. // 根据不同速度 取卡点 1,2,3
  454. /*
  455. - 快节奏为选中区域的所有点位,即0,1,2,3,4……
  456. - 适中为每两个点位取一个,即0,2,4,6……
  457. - 慢节奏为每三个点位取一个,即0,3,6,9……
  458. */
  459. BFLog(message: "stuckPointMusicData?.speed is \(String(describing: stuckPointMusicData?.speed))")
  460. for (index, point) in stuckPointsTemp.enumerated() {
  461. if stuckPointMusicData?.speed == 1 {
  462. stuckPoints.append(point)
  463. } else if stuckPointMusicData?.speed == 2 {
  464. if index % 2 == 0 {
  465. stuckPoints.append(point)
  466. }
  467. } else if stuckPointMusicData?.speed == 3 {
  468. if index % 3 == 0 {
  469. stuckPoints.append(point)
  470. }
  471. }
  472. }
  473. for point in stuckPoints {
  474. BFLog(message: "有 start end 计算后的卡点数\(point)")
  475. }
  476. if stuckPoints.first != nil {
  477. stuckPoints.removeFirst()
  478. }
  479. if stuckPoints.last != nil {
  480. stuckPoints.removeLast()
  481. }
  482. stuckPoints.insert(Float(CMTimeGetSeconds(playeTimeRange.start)), at: 0)
  483. stuckPoints.insert(Float(CMTimeGetSeconds(playeTimeRange.end)), at: stuckPoints.count)
  484. BFLog(message: "stuckPoints count is \(stuckPoints.count)")
  485. // 当用户上传视觉素材个数大于等于音乐选择区域节拍分割个数时,无需进行视频分割,只显示卡点数-1 个素材
  486. if section.sectionTimeline!.visionTrack!.getEnableVisionTrackMaterials().count >= stuckPointMusicData!.rhythmSdata[0].pointTimes.count {
  487. for (index, sticker) in section.sectionTimeline!.visionTrack!.getEnableVisionTrackMaterials().enumerated() {
  488. if index == stuckPointMusicData!.rhythmSdata[0].pointTimes.count {
  489. BFLog(message: "到达卡点数量")
  490. break
  491. }
  492. BFLog(message: "创建 filter start :\(sticker.timelineIn) end :\(sticker.timelineOut) type is \(sticker.type) \(sticker.locationPath)")
  493. sticker.timelineIn = Float64(stuckPoints[index])
  494. sticker.timelineOut = Float64(stuckPoints[index + 1])
  495. BFLog(message: "卡点 间隔 \(sticker.timelineIn - sticker.timelineOut)")
  496. sticker.generateDefaultValues()
  497. stickers.append(sticker)
  498. }
  499. } else {
  500. // 卡点数 > 选择素材数
  501. // 第一种情况:全是图片,图片回环播放
  502. if section.sectionTimeline!.visionTrack!.getEnableVisionTrackMaterials(type: "video").count == 0, section.sectionTimeline!.visionTrack!.getEnableVisionTrackMaterials(type: "image").count > 0 {
  503. for (index, point) in stuckPoints.enumerated() {
  504. let sticker: PQEditVisionTrackMaterialsModel = section.sectionTimeline!.visionTrack!.getEnableVisionTrackMaterials()[index % section.sectionTimeline!.visionTrack!.getEnableVisionTrackMaterials().count]
  505. BFLog(message: "stickerlocationPath sticker : \(sticker.locationPath)")
  506. let stickerjson = sticker.toJSONString(prettyPrint: false)
  507. let deepCopySticker = Mapper<PQEditVisionTrackMaterialsModel>().map(JSONString: stickerjson!)
  508. if deepCopySticker!.type == StickerType.IMAGE.rawValue {
  509. if index + 1 < stuckPoints.count {
  510. deepCopySticker!.timelineIn = Float64(stuckPoints[index])
  511. deepCopySticker!.timelineOut = Float64(stuckPoints[index + 1])
  512. if deepCopySticker != nil {
  513. deepCopySticker?.generateDefaultValues()
  514. stickers.append(deepCopySticker!)
  515. }
  516. }
  517. }
  518. }
  519. } else {
  520. // 第二种情况:有视频要进行分割
  521. let clipFilters = clipVideoMerage(section: section, stuckPoints: stuckPoints)
  522. for (index, point) in stuckPoints.enumerated() {
  523. if index + 1 < stuckPoints.count, index < clipFilters.count {
  524. let sticker: PQEditVisionTrackMaterialsModel = clipFilters[index]
  525. sticker.timelineIn = Float64(stuckPoints[index])
  526. sticker.timelineOut = Float64(stuckPoints[index + 1])
  527. // 卡点的时间 > in out 值
  528. let timelineInterval = sticker.timelineOut - sticker.timelineIn
  529. let inOutInterval = sticker.out - sticker.model_in
  530. if timelineInterval > inOutInterval {
  531. sticker.out = sticker.model_in + timelineInterval
  532. }
  533. // out > 素材的总时长in out 进行前移操作
  534. let offsetAssetDuration = sticker.out - sticker.duration
  535. if offsetAssetDuration > 0 {
  536. sticker.model_in = sticker.model_in - offsetAssetDuration
  537. sticker.out = sticker.out - offsetAssetDuration
  538. }
  539. BFLog(message: "分割后 创建 filter start :\(sticker.timelineIn) end :\(sticker.timelineOut) type is \(sticker.type)")
  540. stickers.append(sticker)
  541. }
  542. }
  543. }
  544. }
  545. }
  546. }
  547. return stickers
  548. }
  549. }
  550. // MARK: - 同步/下载素材相关
  551. /// 同步/下载素材相关
  552. extension PQStuckPointEditerController {
  553. /// 同步音乐相关数据
  554. /// - Returns: <#description#>
  555. func synchroMusicInfoData() {
  556. if (stuckPointMusicData?.rhythmSdata.count ?? 0) <= 0 {
  557. if synchroMarskView.superview == nil {
  558. UIApplication.shared.keyWindow?.addSubview(synchroMarskView)
  559. }
  560. PQStuckPointViewModel.stuckPointMusicDetailData(musicId: stuckPointMusicData?.musicId ?? "", originType: stuckPointMusicData?.originType ?? 1) { [weak self] newMusicData, _ in
  561. if newMusicData != nil, (newMusicData?.rhythmSdata.count ?? 0) > 0 {
  562. self?.isStuckPointDataSuccess = true
  563. self?.stuckPointMusicData?.rhythmSdata = newMusicData?.rhythmSdata ?? []
  564. self?.stuckPointMusicData?.startTime = newMusicData?.startTime ?? 0
  565. self?.stuckPointMusicData?.endTime = newMusicData?.endTime ?? 0
  566. if newMusicData?.speed != nil {
  567. self?.stuckPointMusicData?.speed = newMusicData?.speed ?? 2
  568. }
  569. if (self?.stuckPointMusicData?.rhythmSdata.count ?? 0) > 0 && (((self?.selectedDataCount ?? 0) - (self?.selectedImageDataCount ?? 0)) > 0 || (self?.selectedImageDataCount ?? 0) > 0 || (self?.selectedTotalDuration ?? 0) > 0) {
  570. self?.stuckPointMusicData?.endTime = (self?.stuckPointMusicData?.startTime ?? 0) + (self?.stuckPointMusicData?.stuckPointCuttingTime(videoCount: (self?.selectedDataCount ?? 0) - (self?.selectedImageDataCount ?? 0), imageCount: self?.selectedImageDataCount ?? 0, totalDuration: self?.selectedTotalDuration ?? 0) ?? 0)
  571. }
  572. self?.stuckPointCuttingView.updateEndTime(startTime: CGFloat(self?.stuckPointMusicData?.startTime ?? 0), endTime: CGFloat(self?.stuckPointMusicData?.endTime ?? 0))
  573. if self?.stuckPointMusicData?.localPath == nil || (self?.stuckPointMusicData?.localPath?.count ?? 0) > 0 {
  574. PQDownloadManager.downLoadFile(url: self?.stuckPointMusicData?.musicPath ?? "") { [weak self] filePath, error in
  575. if error == nil, filePath != nil {
  576. self?.isSynchroMusicInfoSuccess = true
  577. self?.stuckPointMusicData?.localPath = filePath?.replacingOccurrences(of: documensDirectory, with: "")
  578. // 处理所有数据完成
  579. self?.dealWithDataSuccess()
  580. } else {
  581. if self?.synchroMarskView.superview != nil {
  582. self?.synchroMarskView.removeMarskView()
  583. }
  584. // PQUploadRemindView.showUploadRemindView(title: nil, attributedTitle: NSAttributedString(string: "加载音乐失败,请重新选择音乐"), summary: "", confirmTitle: nil) { [weak self] _, _ in
  585. // self?.navigationController?.popViewController(animated: true)
  586. // }
  587. }
  588. }
  589. } else {
  590. self?.isSynchroMusicInfoSuccess = true
  591. // 处理所有数据完成
  592. self?.dealWithDataSuccess()
  593. }
  594. // 添加子视图
  595. self?.addSubViews()
  596. } else {
  597. if self?.synchroMarskView.superview != nil {
  598. self?.synchroMarskView.removeMarskView()
  599. }
  600. // PQUploadRemindView.showUploadRemindView(title: nil, attributedTitle: NSAttributedString(string: "加载音乐失败,请重新选择音乐"), summary: "", confirmTitle: nil) { [weak self] _, _ in
  601. // self?.navigationController?.popViewController(animated: true)
  602. // }
  603. }
  604. }
  605. } else if stuckPointMusicData?.localPath == nil || (stuckPointMusicData?.localPath?.count ?? 0) > 0 {
  606. if synchroMarskView.superview == nil {
  607. UIApplication.shared.keyWindow?.addSubview(synchroMarskView)
  608. }
  609. isStuckPointDataSuccess = true
  610. PQDownloadManager.downLoadFile(url: stuckPointMusicData?.musicPath ?? "") { [weak self] filePath, error in
  611. if error == nil, filePath != nil {
  612. self?.isSynchroMusicInfoSuccess = true
  613. self?.stuckPointMusicData?.localPath = filePath?.replacingOccurrences(of: documensDirectory, with: "")
  614. // 处理所有数据完成
  615. self?.dealWithDataSuccess()
  616. } else {
  617. if self?.synchroMarskView.superview != nil {
  618. self?.synchroMarskView.removeMarskView()
  619. }
  620. // PQUploadRemindView.showUploadRemindView(title: nil, attributedTitle: NSAttributedString(string: "加载音乐失败,请重新选择音乐"), summary: "", confirmTitle: nil) { [weak self] _, _ in
  621. // self?.navigationController?.popViewController(animated: true)
  622. // }
  623. }
  624. }
  625. } else {
  626. isStuckPointDataSuccess = true
  627. // 处理所有数据完成
  628. dealWithDataSuccess()
  629. }
  630. }
  631. /// 导出相册数据
  632. /// - Returns: <#description#>
  633. func exportPhotoData() {
  634. // 取消所有的导出
  635. PQSingletoMemoryUtil.shared.allExportSession.forEach { _, exportSession in
  636. exportSession.cancelExport()
  637. }
  638. var isHaveVideo: Bool = false
  639. if selectedPhotoData != nil, (selectedPhotoData?.count ?? 0) > 0 {
  640. if synchroMarskView.superview == nil {
  641. UIApplication.shared.keyWindow?.addSubview(synchroMarskView)
  642. }
  643. let dispatchGroup = DispatchGroup()
  644. for photo in selectedPhotoData! {
  645. if photo.asset != nil, photo.asset?.mediaType == .video, photo.locationPath.count <= 0 {
  646. if !isHaveVideo {
  647. isHaveVideo = true
  648. }
  649. dispatchGroup.enter()
  650. PQPHAssetVideoParaseUtil.exportPHAssetToMP4(phAsset: photo.asset!, deliveryMode: .highQualityFormat) { [weak self] phAsset, _, filePath, _ in
  651. let tempPhoto = self?.selectedPhotoData?.first(where: { material in
  652. material.asset == phAsset
  653. })
  654. if tempPhoto != nil {
  655. if filePath != nil, (filePath?.count ?? 0) > 0 {
  656. tempPhoto?.locationPath = filePath?.replacingOccurrences(of: documensDirectory, with: "") ?? ""
  657. BFLog(message: "导出视频相册地址为")
  658. }
  659. dispatchGroup.leave()
  660. }
  661. }
  662. }
  663. }
  664. dispatchGroup.notify(queue: DispatchQueue.main) { [weak self] in
  665. self?.isExportVideosSuccess = true
  666. BFLog(message: "所有相册视频导出成功")
  667. // 处理所有数据完成
  668. if isHaveVideo {
  669. self?.dealWithDataSuccess()
  670. }
  671. }
  672. }
  673. if !isHaveVideo {
  674. isExportVideosSuccess = true
  675. // 处理所有数据完成
  676. dealWithDataSuccess()
  677. }
  678. }
  679. /// 处理所有数据完成
  680. /// - Returns: <#description#>
  681. func dealWithDataSuccess() {
  682. if !isSynchroMusicInfoSuccess || !isExportVideosSuccess || !isStuckPointDataSuccess {
  683. return
  684. }
  685. playeTimeRange = CMTimeRange(start: CMTime(value: CMTimeValue(Int64((stuckPointMusicData?.startTime ?? 0) * 600)), timescale: 600), end: CMTime(value: CMTimeValue(Int64((stuckPointMusicData?.endTime ?? 0) * 600)), timescale: 600))
  686. createPorjectData()
  687. settingPlayerView()
  688. if synchroMarskView.superview != nil {
  689. synchroMarskView.removeMarskView()
  690. }
  691. }
  692. }