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