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