// WA_LiveVideoVC.swift // wallpaper_project // Created by 忆海16 on 2024/3/12. import UIKit import AVFoundation import MobileCoreServices import FLAnimatedImage import Photos import PhotosUI import SVProgressHUD import Lottie struct FilePaths { // 创建目录搜索路径的列表。 static let documentsPath : AnyObject = NSSearchPathForDirectoriesInDomains(.cachesDirectory,.userDomainMask,true)[0] as AnyObject struct VidToLive { // 返回通过将给定字符串附加到接收器而生成的新字符串。 static var livePath = FilePaths.documentsPath.appending("/") } } class WA_LiveVideoVC: WA_RootVC { @IBOutlet weak var bgView: UIView! @IBOutlet weak var tipLabel: UILabel! var model = WA_3DModel() var livePhotoView = PHLivePhotoView() let activityView = UIActivityIndicatorView() let btn = UIButton() // let bgView = UIView() var sizi:CGSize? let Retrybtn = UIButton() private var animationView: LottieAnimationView? var animationDuration: TimeInterval = 1.0 var workProgress: Float = 0.0 override func viewDidLoad() { super.viewDidLoad() livePhotoView.frame = CGRect(x: 0, y: 0, width: Screen_Width, height: screenHeight); btn.setImage(UIImage(named: "back_black"), for: .normal) btn.backgroundColor = .white btn.addTarget(self, action: #selector(buttonTapped), for: .touchUpInside) // 添加按钮 btn.layer.cornerRadius = 20 self.view.addSubview(self.livePhotoView) self.livePhotoView.addSubview(btn) btn.snp.makeConstraints { make in make.left.equalTo(livePhotoView).offset(20) make.top.equalTo(livePhotoView).offset(50) make.height.equalTo(40) make.width.equalTo(40) } Retrybtn.setTitle("Retry", for: .normal) Retrybtn.setTitleColor(.white, for: .normal) Retrybtn.backgroundColor = .black Retrybtn.addTarget(self, action: #selector(Retrybtntapped), for: .touchUpInside) Retrybtn.layer.cornerRadius = 10 Retrybtn.isHidden = true self.livePhotoView.addSubview(Retrybtn) Retrybtn.snp.makeConstraints { make in make.centerX.equalTo(livePhotoView) make.centerY.equalTo(livePhotoView) make.height.equalTo(40) make.width.equalTo(80) } animationView = .init(name: "loadProgress") animationView!.frame = CGRect(x: 0, y: 100, width: Int(Screen_Width), height: Int(Screen_height) - 100) animationView!.contentMode = .scaleAspectFit // animationView!.loopMode = .loop animationView!.loopMode = .autoReverse self.livePhotoView.addSubview(animationView!) // animationView!.play() self.animationView!.currentProgress = 0.1 animationView!.sendSubviewToBack(btn) self.steMoV() activityView.center = CGPoint(x: self.view.center.x, y: self.view.center.y - kSafeArea_Top - 44) // 停止后,隐藏菊花 activityView.hidesWhenStopped = true activityView.color = .black; activityView.style = UIActivityIndicatorView.Style.whiteLarge self.view.addSubview(activityView) self.view.bringSubviewToFront(tipLabel) self.view.bringSubviewToFront(bgView) self.bgView.isHidden = true self.tipLabel.isHidden = true // self.btn.isHidden = true // self.activityView.startAnimating() self.view.makeToast("Picture loading Please wait", duration: 1.5, position: .center) // 添加手势识别器到 UIView 上 let tapGesture = UITapGestureRecognizer(target: self, action: #selector(viewTapped)) livePhotoView.isUserInteractionEnabled = true livePhotoView.addGestureRecognizer(tapGesture) let model1 = WA_3DModel() model1.preview = self.model.preview model1.title = self.model.title model1.thumbnail = self.model.thumbnail model1.is_free = self.model.is_free model1.category = self.model.category model1.id = self.model.id model1.cnt_like = self.model.cnt_like liveManager.shared.addHistoryItem(item: model1) } @objc func buttonTapped(){ navigationController?.popViewController(animated: true) } @objc func Retrybtntapped(){ // self.activityView.startAnimating() self.animationView?.isHidden = false self.Retrybtn.isHidden = true steMoV() } // 手势识别器的处理方法 @objc func viewTapped() { // 根据当前 UIView 的位置和透明度进行不同的动画 if bgView.frame.origin.y == UIScreen.main.bounds.height - bgView.frame.height { // 当 UIView 在底部时,执行向下移动并渐变消失的动画 UIView.animate(withDuration: 0.5, animations: { self.bgView.frame.origin.y += self.bgView.frame.height self.tipLabel.frame.origin.y += self.bgView.frame.height - 20 self.bgView.alpha = 0 self.btn.isHidden = true }) } else { self.btn.isHidden = false // 当 UIView不在底部时,执行向上移动并渐变显示的动画 UIView.animate(withDuration: 0.5, animations: { self.bgView.frame.origin.y -= self.bgView.frame.height self.tipLabel.frame.origin.y -= self.bgView.frame.height - 20 self.bgView.alpha = 1 }) } } override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) self.navigationController?.navigationBar.isHidden = true let size = self.view.bounds.size self.sizi = size } @IBAction func saveBtn(_ sender: UIButton) { self.exportLivePhoto() self.activityView.startAnimating() DispatchQueue.global().asyncAfter(deadline: .now() + 1.5) { DispatchQueue.main.asyncAfter(deadline: .now() + 0){ self.activityView.stopAnimating() // self.view.makeToast("Save Success", duration: 1.5, position: .center) // 创建一个 UIAlertController let alertController = UIAlertController(title: "Successfully saved", message: "Do you need to set up a tutorial", preferredStyle: .alert) // 添加一个取消按钮 alertController.addAction(UIAlertAction(title: "cancel", style: .cancel, handler: nil)) // 添加一个确定按钮 alertController.addAction(UIAlertAction(title: "Need", style: .default, handler: { action in // 点击确定按钮后执行的操作 // print("点击了确定按钮") let vc = WA_TipVC() self.present(vc, animated: true) })) // 在当前视图控制器中 present 显示提示框 self.present(alertController, animated: true, completion: nil) } } } @IBAction func shareBtn(_ sender: UIButton) { self.activityView.startAnimating() // 创建要分享的图片 guard let image = self.livePhotoView.livePhoto else { SVProgressHUD.showInfo(withStatus: "Can't find the image to share") DispatchQueue.global().asyncAfter(deadline: .now() + 2.0) { SVProgressHUD.dismiss() } print("找不到要分享的图片") return } // 创建要分享的对象数组 let items: [Any] = [image] // 创建 UIActivityViewController 实例 let activityViewController = UIActivityViewController(activityItems: items, applicationActivities: nil) // 设置 UIActivityViewController 的主题 activityViewController.modalPresentationStyle = .popover // 在 iPad 上设置 UIActivityViewController 的位置 if let popoverController = activityViewController.popoverPresentationController { popoverController.sourceView = self.view popoverController.sourceRect = CGRect(x: self.view.bounds.midX, y: self.view.bounds.midY, width: 0, height: 0) popoverController.permittedArrowDirections = [] } // 弹出分享视图 self.present(activityViewController, animated: true, completion: nil) DispatchQueue.main.asyncAfter(deadline: .now() + 0){ self.activityView.stopAnimating() } } //初始化下载mp4到沙盒路径 func steMoV(){ // 原始的 mp4 文件 URL let mp4URL = URL(string: "\(model.preview ?? "")")! // 本地存储路径 let documentsDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first! let str = generateRandomString(length: 10) let destinationURL = documentsDirectory.appendingPathComponent("\(str).mov") // 创建下载任务 let task = URLSession.shared.downloadTask(with: mp4URL) { (tempURL, response, error) in guard let tempURL = tempURL else { print("下载失败:\(error?.localizedDescription ?? "未知错误")") DispatchQueue.main.asyncAfter(deadline: .now() + 0){ self.animationView?.isHidden = true self.Retrybtn.isHidden = false SVProgressHUD.showInfo(withStatus: "request timeout") DispatchQueue.global().asyncAfter(deadline: .now() + 2.0) { SVProgressHUD.dismiss() } } return } do { // 将临时文件移动到目标位置 try FileManager.default.moveItem(at: tempURL, to: destinationURL) // 输出下载完成的信息 print("下载完成:\(destinationURL)") print("文档目录路径:\(documentsDirectory.path)") self.animationView!.currentProgress = 0.5 // 将 mp4 转换为 mov 格式 self.convertMP4ToMOV(mp4URL: destinationURL) } catch { print("文件移动失败:\(error.localizedDescription)") DispatchQueue.main.asyncAfter(deadline: .now() + 0){ self.animationView?.isHidden = true self.Retrybtn.isHidden = false SVProgressHUD.showInfo(withStatus: "request timeout") DispatchQueue.global().asyncAfter(deadline: .now() + 2.0) { SVProgressHUD.dismiss() } } } } // 开始下载任务 task.resume() } //转化mp4为mov,并截取视频第一帧为jpg func convertMP4ToMOV(mp4URL: URL) { let asset = AVAsset(url: mp4URL) let exportSession = AVAssetExportSession(asset: asset, presetName: AVAssetExportPresetHighestQuality) // 生成一个随机的字符串作为文件名 let randomString = generateRandomString(length: 10) // 创建输出文件路径 guard let outputURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first?.appendingPathComponent("\(randomString).mov") else { print("无法创建输出文件路径") DispatchQueue.main.asyncAfter(deadline: .now() + 0){ self.animationView?.isHidden = true self.Retrybtn.isHidden = false SVProgressHUD.showInfo(withStatus: "request timeout") DispatchQueue.global().asyncAfter(deadline: .now() + 2.0) { SVProgressHUD.dismiss() } } return } exportSession?.outputURL = outputURL exportSession?.outputFileType = .mov exportSession?.exportAsynchronously(completionHandler: { DispatchQueue.main.async { if exportSession?.status == .completed { // 转换完成,打印输出URL print("转换完成,保存位置:\(outputURL)") self.animationView!.currentProgress = 0.8 self.loadVideoWithVideoURL(outputURL) } else if exportSession?.status == .failed { // 转换失败,打印错误信息 print("转换失败:\(exportSession?.error?.localizedDescription ?? "未知错误")") DispatchQueue.main.asyncAfter(deadline: .now() + 0){ self.animationView?.isHidden = true self.Retrybtn.isHidden = false SVProgressHUD.showInfo(withStatus: "request timeout") DispatchQueue.global().asyncAfter(deadline: .now() + 2.0) { SVProgressHUD.dismiss() } } } } }) } func loadVideoWithVideoURL(_ videoURL: URL) { // self.view.makeToast("Start", duration: 0.5, position: .center) livePhotoView.livePhoto = nil //下载到本地的URL let asset = AVURLAsset(url: videoURL) //从视频资源生成图像对像 let generator = AVAssetImageGenerator(asset: asset) //制定资源是否对应多个跟踪矩阵 generator.appliesPreferredTrackTransform = true // 创建一个时间,表示首选时间刻度中的秒数。 let time = NSValue(time: CMTimeMakeWithSeconds(CMTimeGetSeconds(asset.duration)/2, preferredTimescale: asset.duration.timescale)) // 为请求的时间数组异步生成图像,并在回调中返回结果。 generator.generateCGImagesAsynchronously(forTimes: [time]) { [weak self] _, image, _, _, _ in if let image = image, let data = UIImage(cgImage: image).pngData() { let urls = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask) // 通过将指定的路径组件附加到self,返回URL。 let imageURL = urls[0].appendingPathComponent("image.jpg") // 将数据缓冲区的内容写入某个位置。 try? data.write(to: imageURL, options: [.atomic]) let image = imageURL.path let mov = videoURL.path let output = FilePaths.VidToLive.livePath // 返回从UUID创建的字符串 let assetIdentifier = UUID().uuidString // 在指定的路径上创建具有给定属性的目录。 let _ = try? FileManager.default.createDirectory(atPath: output, withIntermediateDirectories: true, attributes: nil) do { // 删除指定路径中的文件或目录。 try FileManager.default.removeItem(atPath: output + "/IMG.JPG") try FileManager.default.removeItem(atPath: output + "/IMG.MOV") } catch { } JPEG(path: image).write(output + "/IMG.JPG", assetIdentifier: assetIdentifier) QuickTimeMov(path: mov).write(output + "/IMG.MOV", assetIdentifier: assetIdentifier) if let sizi = self?.sizi { _ = DispatchQueue.main.sync { PHLivePhoto.request(withResourceFileURLs: [ URL(fileURLWithPath: FilePaths.VidToLive.livePath + "/IMG.MOV"), URL(fileURLWithPath: FilePaths.VidToLive.livePath + "/IMG.JPG")], placeholderImage: nil, targetSize: (self?.sizi)!, contentMode: PHImageContentMode.aspectFit, resultHandler:{ (livePhoto, info) -> Void in self?.livePhotoView.livePhoto = livePhoto if (info.first?.key.description == "PHLivePhotoInfoIsDegradedKey") && (info.first?.value as? NSInteger == 1) { self?.animationView!.currentProgress = 1.0 self?.animationView?.isHidden = true self?.bgView.isHidden = false self?.tipLabel.isHidden = false self?.btn.isHidden = false } }) } } else { // 处理 sizi 为 nil 的情况 } } } } func exportLivePhoto () { PHPhotoLibrary.shared().performChanges({ () -> Void in // 创建使用资产资源向照片库添加新资产的请求 let creationRequest = PHAssetCreationRequest.forAsset() let options = PHAssetResourceCreationOptions() // 使用指定URL处的文件将数据资源添加到正在创建的资产中。 creationRequest.addResource(with: PHAssetResourceType.pairedVideo, fileURL: URL(fileURLWithPath: FilePaths.VidToLive.livePath + "/IMG.MOV"), options: options) creationRequest.addResource(with: PHAssetResourceType.photo, fileURL: URL(fileURLWithPath: FilePaths.VidToLive.livePath + "/IMG.JPG"), options: options) }, completionHandler: { (success, error) -> Void in if !success { // DTLog((error?.localizedDescription)!) print("转化错误: \(error?.localizedDescription ?? "")") DispatchQueue.main.asyncAfter(deadline: .now() + 0){ self.animationView?.isHidden = true self.Retrybtn.isHidden = false SVProgressHUD.showInfo(withStatus: "request timeout") DispatchQueue.global().asyncAfter(deadline: .now() + 2.0) { SVProgressHUD.dismiss() } } } }) } }