Bläddra i källkod

Merge branch 'mv-dev-130'
合并片尾版本代码

jsonwang 3 år sedan
förälder
incheckning
a685c17e70

+ 1 - 1
BFFramework.podspec

@@ -33,7 +33,7 @@ TODO: Add long description of the pod here.
   s.source_files = 'BFFramework/Classes/**/*'
 
    s.resource_bundles = {
-     'BFFramework' => ['BFFramework/Assets/**/*.png','BFFramework/Assets/**/*.gif']
+     'BFFramework' => ['BFFramework/Assets/**/*.png','BFFramework/Assets/**/*.gif','BFFramework/Assets/**/*.mp3','BFFramework/Assets/**/*.mp4']
    }
    s.static_framework = true
 

BIN
BFFramework/Assets/Stuckpoint/endMovieA.mp4


BIN
BFFramework/Assets/Stuckpoint/endMovieB.mp4


BIN
BFFramework/Assets/Stuckpoint/endMovieSound.mp3


BIN
BFFramework/Assets/base/user_avatar_normal.png


+ 21 - 0
BFFramework/Classes/BFModules/BFCategorys/BFUIImage+Ext.swift

@@ -230,5 +230,26 @@ public extension UIImage {
         UIGraphicsEndImageContext()
         return tintedImage
     }
+    
+    /// 保存图片文件到指定目录, 如果目录已经存在会先删除老文件
+    /// - Parameters:
+    ///   - currentImage: 图片数据
+    ///   - persent: 质量
+    ///   - outFilePath: 输出目录
+    class func saveImage(currentImage: UIImage,outFilePath: String) {
+        // 文件存在先删除老文件
+        if FileManager.default.fileExists(atPath: outFilePath) {
+            do {
+                try FileManager.default.removeItem(at: NSURL.fileURL(withPath: outFilePath))
+            } catch {
+                BFLog(message: "删除文件出错 == \(error) \(outFilePath)")
+            }
+        }
+        
+        if let imageData = currentImage.pngData() {
+            try? imageData.write(to: URL(fileURLWithPath: outFilePath))
+            print("保存图片成功到:filePath=\(outFilePath)")
+        }
+    }
 }
 

+ 13 - 3
BFFramework/Classes/PQGPUImage/akfilters/PQImageFilter.swift

@@ -40,7 +40,7 @@ open class PQImageFilter: PQBaseFilter {
 //        print("mSticker path : \(String(describing: mSticker!.locationPath))")
 
         newImage = UIImage(contentsOfFile: documensDirectory + sticker.locationPath)
-        //支持 bundle 加载图片
+        //try find image file frome in BFFramework bundle
         if(newImage == nil){
             newImage = UIImage().BF_Image(named: sticker.locationPath)
         }
@@ -130,7 +130,7 @@ open class PQImageFilter: PQBaseFilter {
 //        if currTime >= mSticker!.timelineIn && currTime <= mSticker!.timelineOut {
         BFLog(2, message: " 显示图片当前时间: \(currTime) 开始时间:\(mSticker!.timelineIn) 结束时间:\(mSticker!.timelineOut)  \(String(describing: newImage?.size))")
         // 取纹理坐标
-        let textureCoordinates = PQGPUImageTools.getTextureCoordinates(sticker: mSticker!, textureSize: newImage!.size, cannvasSize: inputSize)
+        var textureCoordinates = PQGPUImageTools.getTextureCoordinates(sticker: mSticker!, textureSize: newImage!.size, cannvasSize: inputSize)
 
         BFLog(2, message: "textureCoordinates is \(textureCoordinates)")
 
@@ -141,13 +141,23 @@ open class PQImageFilter: PQBaseFilter {
         }
 
         
+        //如果设置过大小位置,使用设置值,比如水印
+        if(stickerInfo?.materialPosition?.width != 0){
+           textureCoordinates = [
+                0.0, 0.0, // 1 bottom left
+                1.0, 0.0, // 2 bottom right
+                0.0, 1.0, // 3 top left
+                1.0, 1.0, // 4 top right
+            ]
+        }
+        
         let texturePropertiesimagetwo = InputTextureProperties(textureCoordinates: textureCoordinates, texture: imageTexture)
 
         var verticesPoint: [GLfloat] = PQGPUImageTools.getVerticesPoint(sticker: mSticker!, textureSize: newImage!.size, cannvasSize: inputSize)
         
         //如果设置过大小位置,使用设置值,比如水印
         if(stickerInfo?.materialPosition?.width != 0){
-            verticesPoint = PQGPUImageTools.computeVertices(viewSize: CGSize.init(width: CGFloat(inputSize.width), height: CGFloat(inputSize.height)), _bounds: CGRect.init(x: 0, y: 0, width: stickerInfo?.materialPosition?.width ?? 0, height: stickerInfo?.materialPosition?.height ?? 0))
+            verticesPoint = PQGPUImageTools.computeVertices(viewSize: CGSize.init(width: CGFloat(inputSize.width), height: CGFloat(inputSize.height)), _bounds: CGRect.init(x: stickerInfo?.materialPosition?.x ?? 0, y:  stickerInfo?.materialPosition?.y ?? 0, width: stickerInfo?.materialPosition?.width ?? 0, height: stickerInfo?.materialPosition?.height ?? 0))
         }
         
         // 设置融合模式支持 alpha

+ 4 - 2
BFFramework/Classes/PQGPUImage/akfilters/PQMovieFilter.swift

@@ -147,9 +147,11 @@ class PQMovieFilter: PQBaseFilter {
         do {
             // 测试代码
             //            try  loadAsset(url:URL(fileURLWithPath:"22222.MP4", relativeTo:Bundle.main.resourceURL!), videoComposition: nil)
-            // locationPath 有可能直接使用系统相册地址  处理不同 IOS 版本 路径有所区别 e.g.视频地址 var/mobile/Media/DCIM/125APPLE/IMG_5189.MOV 就不用拼接沙盒地址了
+            /* locationPath 有可能直接使用系统相册地址   处理不同 IOS 版本 路径有所区别 1,e.g.视频地址 var/mobile/Media/DCIM/125APPLE/IMG_5189.MOV 就不用拼接沙盒地址了
+            2,try find move file from bfframework bundle e.g. 库 bundle 的地址 "/var/containers/Bundle/Application/AD663220-6AF2-4841-AF82-071C10D78959/MusicVideoPlus.app/BFFramework.bundle/endMovieA.mp4"
+            */
             var videoFilePath = movieSticker.locationPath
-            if !videoFilePath.contains("var/mobile/Media") {
+            if (!videoFilePath.contains("var/mobile/Media")) && (!videoFilePath.contains("BFFramework.bundle")) {
                 videoFilePath = documensDirectory + videoFilePath
             }
             BFLog(2, message: "视频地址 \(String(describing: videoFilePath))")

+ 128 - 0
BFFramework/Classes/PQGPUImage/akfilters/PQTextFilter.swift

@@ -0,0 +1,128 @@
+//
+//  PQTextFilter.swift
+//  BFFramework
+//
+//  Created by ak on 2021/10/9.
+//  功能:创建文本fitler
+
+import Foundation
+import UIKit
+
+import OpenGLES
+
+import AVFoundation
+ 
+open class PQTextFilter: PQBaseFilter {
+ 
+ 
+    // 字幕gpu texture
+    var subTitleTexture: GLuint = 0
+
+    var subtitleImage: UIImage?
+    deinit {
+        FilterLog(message: "字幕析构 ")
+        clearData()
+    }
+
+    init(sticker: PQEditVisionTrackMaterialsModel) {
+        super.init(fragmentShader: AlphaPassthroughFragmentShader, numberOfInputs: 1)
+        stickerInfo = sticker
+        subTitleTexture = 0
+ 
+        createTexture()
+    }
+ 
+    // 清空数据
+    public override func clearData() {
+        super.clearData()
+
+        subtitleImage = nil
+        glDeleteTextures(1, &subTitleTexture)
+        subTitleTexture = 0
+
+    }
+  
+    func createTexture() {
+        DispatchQueue.main.async {[weak self] in
+ 
+            autoreleasepool {
+                let subtitleLab =
+                UILabel.init(frame: CGRect(x: 0, y: 0, width: self?.stickerInfo?.materialPosition?.width ?? 0, height: self?.stickerInfo?.materialPosition?.height ?? 0))
+             
+                subtitleLab.numberOfLines = 2
+                subtitleLab.lineBreakMode = .byWordWrapping
+
+                FilterLog(message: "字幕初始化时大小 \(subtitleLab.frame)")
+                subtitleLab.backgroundColor = UIColor(red: 1, green: 1, blue: 1, alpha: 0.5)
+                subtitleLab.alpha = 1
+          
+                let paraph = NSMutableParagraphStyle()
+                paraph.lineSpacing = 0
+                paraph.alignment = .center
+ 
+                
+                let attributedText: NSMutableAttributedString = NSMutableAttributedString(string: (self?.stickerInfo?.subtitleInfo?.text ?? ""), attributes: [NSAttributedString.Key.font: UIFont.boldSystemFont(ofSize: CGFloat(self?.stickerInfo?.subtitleInfo?.fontSize ?? 0)), NSAttributedString.Key.foregroundColor:UIColor.white,NSAttributedString.Key.kern: 0,NSAttributedString.Key.paragraphStyle:paraph])
+                subtitleLab.attributedText = attributedText
+ 
+                let size: CGSize = subtitleLab.bounds.size
+                //如果传0,则这个参数会用设备的scale,
+                UIGraphicsBeginImageContextWithOptions(size, false, 2)
+                subtitleLab.layer.render(in: UIGraphicsGetCurrentContext()!)
+
+                self?.subtitleImage = UIGraphicsGetImageFromCurrentImageContext() ?? UIImage()
+                UIGraphicsEndImageContext()
+                
+                FilterLog(message: "合成图片大小: \(String(describing: self?.subtitleImage?.size))")
+
+            }
+ 
+            if self?.subtitleImage?.cgImage != nil, self?.subTitleTexture == 0 {
+                sharedImageProcessingContext.runOperationSynchronously {
+                    autoreleasepool {
+                        self?.subTitleTexture = PQGPUImageTools.setupTexture(image: (self?.subtitleImage!.cgImage!)!)
+                    }
+                }
+            }
+        }
+    }
+ 
+    override open func renderFrame() {
+        let inputFramebuffer: Framebuffer = inputFramebuffers[0]!
+        let inputSize = inputFramebuffer.sizeForTargetOrientation(.portrait)
+
+        let currTime = CMTimeGetSeconds(CMTime(value: inputFramebuffer.timingStyle.timestamp!.value, timescale: inputFramebuffer.timingStyle.timestamp!.timescale))
+        FilterLog(message: "subtitle 当前时间: \(currTime)")
+
+        // 原有画布
+        renderFramebuffer = sharedImageProcessingContext.framebufferCache.requestFramebufferWithProperties(orientation: .portrait, size: inputSize, stencil: false)
+
+        let textureProperties = InputTextureProperties(textureCoordinates: inputFramebuffer.orientation.rotationNeededForOrientation(.portrait).textureCoordinates(), texture: inputFramebuffer.texture)
+
+        renderFramebuffer.activateFramebufferForRendering()
+        clearFramebufferWithColor(backgroundColor)
+        renderQuadWithShader(shader, uniformSettings: uniformSettings,
+                             vertexBufferObject: sharedImageProcessingContext.standardImageVBO, inputTextures: [textureProperties])
+        releaseIncomingFramebuffers()
+
+        if subTitleTexture != 0 {
+            FilterLog(message: "subTitleTexture 有值可以正常显示")
+            let texturePropertiesimagetwo = InputTextureProperties(textureCoordinates: inputFramebuffer.orientation.rotationNeededForOrientation(.portrait).textureCoordinates(), texture: subTitleTexture)
+ 
+            let verticesPoint = PQGPUImageTools.computeVertices(viewSize: CGSize.init(width: CGFloat(inputSize.width), height: CGFloat(inputSize.height)), _bounds: CGRect.init(x: stickerInfo?.materialPosition?.x ?? 0, y:  stickerInfo?.materialPosition?.y ?? 0, width: stickerInfo?.materialPosition?.width ?? 0, height: stickerInfo?.materialPosition?.height ?? 0))
+            
+            renderQuadWithShader(shader,
+                                 uniformSettings: uniformSettings,
+                                 vertexBufferObject: PQGPUImageTools.NXGenerateVBO(for: verticesPoint),
+                                 inputTextures: [texturePropertiesimagetwo])
+
+            releaseIncomingFramebuffers()
+        }else{
+            FilterLog(message: "subTitleTexture is nil!!!!!")
+        }
+    }
+
+    override public func transmitPreviousImage(to _: ImageConsumer, atIndex _: UInt) {
+        print("this is running")
+    }
+}
+ 

+ 457 - 0
BFFramework/Classes/PQGPUImage/akfilters/Tools/NXFileManager.h

@@ -0,0 +1,457 @@
+//
+//  NXFFileManager.h
+//  NXlib
+//
+//  Created by AK on 15/8/30.
+//  Copyright (c) 2015年 AK. All rights reserved.
+//
+
+/*
+
+ 本类功能, 文件操作.(ios, mac)
+
+ 一,iOS目录结构说明
+ 1,沙盒目录结构
+ ├── Documents - 存储用户数据或其它应该定期备份的
+ ├── Library
+ │   ├── Caches -
+ 用于存放应用程序专用的支持文件,保存应用程序再次启动过程中需要的信息
+ │   │   └── Snapshots
+ │   │       └── com.youyouxingyuan.re
+ │   │           ├── A85B73F0-26A8-44E4-A761-446CAB8DAB38@2x.png
+ │   │           └── BFAD5885-B767-4320-9A4B-555EC881C50D@2x.png
+ │   └── Preferences - 偏好设置文件 NSUserDefaults 保存的数据
+ └── tmp - 这个目录用于存放临时文件,保存应用程序再次启动过程中不需要的信息
+
+ 2,在iOS8之后,应用每一次重启,沙盒路径都动态的发生了变化但不用担心数据问题,苹果会把你上一个路径中的数据转移到你新的路径中。你上一个路径也会被苹果毫无保留的删除,只保留最新的路径。
+
+
+ */
+//文件操作
+#import <Foundation/Foundation.h>
+#import <UIKit/UIKit.h>
+
+@interface NXFileManager : NSObject<NSFileManagerDelegate>
+
+#pragma mark - 常用路径
+
+/**
+ *  app bundle path 每次运行会改变
+ *  //mobile/Containers/Bundle/Application/84C95A20-21A7-4E39-AFFC-E63FE5586BDA/YOSticker.app>
+ *  //mobile/Containers/Bundle/Application/3BC8A0E0-0177-400A-AECE-6B940F8F2DF6/YOSticker.app>
+ *
+ *  @return mainBundle
+ */
++ (NSBundle *)getMainBundle;
+
++ (NSString *)getMainBundleRes;
+
+/**
+ *  获取Document path
+ *
+ *  @return Document path
+ */
++ (NSString *)getDocumentDir;
+
+/**
+ *  获取Cache path
+ *
+ *  @return Cache path
+ */
++ (NSString *)getCacheDir;
+
+/**
+ *  获取temp path
+ *
+ *  @return temp 路径
+ */
++ (NSString *)getTmpDir;
+
+/**
+ *  得到文件所在目录
+ *
+ *  @param filePath 文件路径
+ *
+ *  @return 文件所在目录
+ */
++ (NSString *)deletingLastPathComponent:(NSString *)filePath;
+
+/**
+ *  校验目录路径是否有效  如果目录不存在 会自动创建
+ *
+ *  @param dir 目录路径
+ *
+ *  @return 目录路径
+ */
++ (NSString *)validateDir:(NSString *)dir;
+
+/**
+ *  文件路径校验是否存在
+ *
+ *  @param filePath 文件路径
+ *
+ *  @return YES 存在
+ */
++ (BOOL)validateFile:(NSString *)filePath;
+
+/**
+ *  通过文件名返回Documents 目录下的全路径
+ *
+ *  @param filename 文件名
+ *
+ *  @return 返回Doc下的全路径
+ */
++ (NSString *)getPathForDocuments:(NSString *)filename;
+
+/**
+ *  通过文件名返回Documents 目录下的全路径
+ *
+ *  @param filename 文件名
+ *  @param dir      目录名可以是多层目录如"aaa/bbb"
+ *
+ *  @return 全路径
+ */
++ (NSString *)getPathForDocuments:(NSString *)filename inDir:(NSString *)dir;
+
+#pragma mark -  get文件的属性
+/*
+     NSFileAppendOnly: 文件是否只读
+     NSFileBusy: 文件是否繁忙
+     NSFileCreationDate: 文件创建日期
+     NSFileOwnerAccountName: 文件所有者的名字
+     NSFileGroupOwnerAccountName: 文件所有组的名字
+     NSFileDeviceIdentifier: 文件所在驱动器的标示符
+     NSFileExtensionHidden: 文件后缀是否隐藏
+     NSFileGroupOwnerAccountID: 文件所有组的group ID
+     NSFileHFSCreatorCode: 文件的HFS创建者的代码
+     NSFileHFSTypeCode: 文件的HFS类型代码
+     NSFileImmutable: 文件是否可以改变
+     NSFileModificationDate: 文件修改日期
+     NSFileOwnerAccountID: 文件所有者的ID
+     NSFilePosixPermissions: 文件的Posix权限
+     NSFileReferenceCount: 文件的链接数量
+     NSFileSize: 文件的字节
+     NSFileSystemFileNumber: 文件在文件系统的文件数量
+     NSFileType: 文件类型
+     NSDirectoryEnumerationSkipsSubdirectoryDescendants:
+   浅层的枚举,不会枚举子目录
+     NSDirectoryEnumerationSkipsPackageDescendants: 不会扫描pakages的内容
+     NSDirectoryEnumerationSkipsHiddenFile: 不会扫描隐藏文件
+ */
+
+/**
+ *   取文件的所有属性
+ *
+ *  @param path 文件path
+ *
+ *  @return 文件的所有属性(MAC)
+ */
++ (NSDictionary *)attributesOfItemAtPath:(NSString *)path;
+
+/**
+ *  取文件的所有属性 &error
+ *
+ *  @param path  文件path
+ *  @param error &error
+ *
+ *  @return 文件的所有属性
+ */
++ (NSDictionary *)attributesOfItemAtPath:(NSString *)path error:(NSError **)error;
+/**
+ *  取文件属性
+ *
+ *  @param path 文件目录
+ *  @param key  属性KEY
+ *
+ *  @return 属性 value
+ */
++ (id)attributeOfItemAtPath:(NSString *)path forKey:(NSString *)key;
+
+/**
+ *  取文件属性
+ *
+ *  @param path  文件目录
+ *  @param key   属性KEY
+ *  @param error &error
+ *
+ *  @return 属性 value
+ */
++ (id)attributeOfItemAtPath:(NSString *)path forKey:(NSString *)key error:(NSError **)error;
+
+#pragma mark - 文件C-M-D-L操作
+/**
+ *  复制一个文件到指定目录
+ *
+ *  @param path 原目录
+ *  @param toPath 目标目录
+ *
+ *  @return 操作结果
+ */
++ (BOOL)copyItemAtPath:(NSString *)path toPath:(NSString *)toPath;
+
++ (BOOL)copyItemAtPath:(NSString *)path toPath:(NSString *)path error:(NSError **)error;
+
+/**
+ *  创建文件夹
+ *
+ *  @param path
+ *
+ *  @return 创建文件夹结果
+ */
+
+/**
+ 创建文件所在目录 如~/aaaa/xxxx.txt 生成的为 文件夹 ~/aaaa/
+
+ @param path 文件所在目录
+ @return 是否创建成功
+ */
++ (BOOL)createDirectoriesForFileAtPath:(NSString *)path;
++ (BOOL)createDirectoriesForFileAtPath:(NSString *)path error:(NSError **)error;
+
+
+/**
+ 创建目录 如~/aaaa/xxxx 生成的为 文件夹 ~/aaaa/xxxx/   (注意 即使指定xxxx的类型 生成的也是文件夹)
+
+ @param path 目录
+ @return 是否创建成功
+ */
++ (BOOL)createDirectoriesForPath:(NSString *)path;
++ (BOOL)createDirectoriesForPath:(NSString *)path error:(NSError **)error;
+
+
+/**
+ 
+ 创建指定文件 如~/aaaa/xxxx.txt 生成的为 文件 ~/aaaa/xxxx.txt   (注意 即使不指定xxxx的类型 生成的也是文件)
+ @param path 文件路径
+ @return 是否创建成功
+ */
++ (BOOL)createFileAtPath:(NSString *)path;
++ (BOOL)createFileAtPath:(NSString *)path error:(NSError **)error;
+
++ (BOOL)createFileAtPath:(NSString *)path withContent:(NSObject *)content;
++ (BOOL)createFileAtPath:(NSString *)path withContent:(NSObject *)content error:(NSError **)error;
+
+#pragma mark -
+/**
+ *  取文件的创建时间
+ *
+ *  @param path 文件路径
+ *
+ *  @return 时间
+ */
++ (NSDate *)creationDateOfItemAtPath:(NSString *)path;
++ (NSDate *)creationDateOfItemAtPath:(NSString *)path error:(NSError **)error;
+
+/**
+ *  清空Caches目录
+ *
+ *  @return 清空结果
+ */
++ (BOOL)emptyCachesDirectory;
+/**
+ *  清空Temporary目录
+ *
+ *  @return 清空结果
+ */
++ (BOOL)emptyTemporaryDirectory;
+
+/**
+ *  判断文件是否存在
+ *
+ *  @param path 文件路径
+ *
+ *  @return 存在 返回YES
+ */
++ (BOOL)existsItemAtPath:(NSString *)path;
+
+/**
+ *  判断路径是不是目录 类型
+ *
+ *  @param path 路径
+ *
+ *  @return YES 是目录
+ */
++ (BOOL)isDirectoryItemAtPath:(NSString *)path;
++ (BOOL)isDirectoryItemAtPath:(NSString *)path error:(NSError **)error;
+
++ (BOOL)isEmptyItemAtPath:(NSString *)path;
++ (BOOL)isEmptyItemAtPath:(NSString *)path error:(NSError **)error;
+
+/**
+ *  判断路径是不是一个文件类型
+ *
+ *  @param path 路径
+ *
+ *  @return YES 是文件
+ */
++ (BOOL)isFileItemAtPath:(NSString *)path;
++ (BOOL)isFileItemAtPath:(NSString *)path error:(NSError **)error;
+
+// 判断某个路径下文件是否可执行
++ (BOOL)isExecutableItemAtPath:(NSString *)path;
+// 判断某个路径下文件是否可读
++ (BOOL)isReadableItemAtPath:(NSString *)path;
+// 判断某个路径下文件是否可写
++ (BOOL)isWritableItemAtPath:(NSString *)path;
+
+/**
+ *  获取某个路径下的所有子路径
+ *
+ *  @param path 路径
+ *
+ *  @return 子路径数组
+ */
++ (NSArray *)listDirectoriesInDirectoryAtPath:(NSString *)path;
++ (NSArray *)listDirectoriesInDirectoryAtPath:(NSString *)path deep:(BOOL)deep;
+
++ (NSArray *)listFilesInDirectoryAtPath:(NSString *)path;
++ (NSArray *)listFilesInDirectoryAtPath:(NSString *)path deep:(BOOL)deep;
+
++ (NSArray *)listFilesInDirectoryAtPath:(NSString *)path withExtension:(NSString *)extension;
++ (NSArray *)listFilesInDirectoryAtPath:(NSString *)path withExtension:(NSString *)extension deep:(BOOL)deep;
+
++ (NSArray *)listFilesInDirectoryAtPath:(NSString *)path withPrefix:(NSString *)prefix;
++ (NSArray *)listFilesInDirectoryAtPath:(NSString *)path withPrefix:(NSString *)prefix deep:(BOOL)deep;
+
++ (NSArray *)listFilesInDirectoryAtPath:(NSString *)path withSuffix:(NSString *)suffix;
++ (NSArray *)listFilesInDirectoryAtPath:(NSString *)path withSuffix:(NSString *)suffix deep:(BOOL)deep;
+
++ (NSArray *)listItemsInDirectoryAtPath:(NSString *)path deep:(BOOL)deep;
+
+/**
+ *  移动某个路径下文件到另一个路径
+ *
+ *  @param path 路径
+ *  @param toPath 目标路径
+ *  @return 是否移动成功
+ */
++ (BOOL)moveItemAtPath:(NSString *)path toPath:(NSString *)toPath;
++ (BOOL)moveItemAtPath:(NSString *)path toPath:(NSString *)toPath error:(NSError **)error;
+
+// location of application support files
++ (NSString *)pathForApplicationSupportDirectory;
++ (NSString *)pathForApplicationSupportDirectoryWithPath:(NSString *)path;
+
+// location of discardable cache files (Library/Caches)
++ (NSString *)pathForCachesDirectory;
++ (NSString *)pathForCachesDirectoryWithPath:(NSString *)path;
+
+// documents (Documents)
++ (NSString *)pathForDocumentsDirectory;
++ (NSString *)pathForDocumentsDirectoryWithPath:(NSString *)path;
+
+// various documentation, support, and configuration files, resources (Library)
++ (NSString *)pathForLibraryDirectory;
++ (NSString *)pathForLibraryDirectoryWithPath:(NSString *)path;
+
+// 沙盒路径
++ (NSString *)pathForMainBundleDirectory;
++ (NSString *)pathForMainBundleDirectoryWithPath:(NSString *)path;
+
+// plist 文件路径
++ (NSString *)pathForPlistNamed:(NSString *)name;
+
+// 临时文件路径
++ (NSString *)pathForTemporaryDirectory;
++ (NSString *)pathForTemporaryDirectoryWithPath:(NSString *)path;
+
+#pragma mark - 以不同的形式读写文件的内容
+// 读取指定路径中的文件内容
++ (NSString *)readFileAtPath:(NSString *)path;
++ (NSString *)readFileAtPath:(NSString *)path error:(NSError **)error;
+
++ (NSArray *)readFileAtPathAsArray:(NSString *)path;
+
++ (NSObject *)readFileAtPathAsCustomModel:(NSString *)path;
+
++ (NSData *)readFileAtPathAsData:(NSString *)path;
++ (NSData *)readFileAtPathAsData:(NSString *)path error:(NSError **)error;
+
++ (NSDictionary *)readFileAtPathAsDictionary:(NSString *)path;
+
++ (UIImage *)readFileAtPathAsImage:(NSString *)path;
++ (UIImage *)readFileAtPathAsImage:(NSString *)path error:(NSError **)error;
+
++ (UIImageView *)readFileAtPathAsImageView:(NSString *)path;
++ (UIImageView *)readFileAtPathAsImageView:(NSString *)path error:(NSError **)error;
+
++ (NSJSONSerialization *)readFileAtPathAsJSON:(NSString *)path;
++ (NSJSONSerialization *)readFileAtPathAsJSON:(NSString *)path error:(NSError **)error;
+
++ (NSMutableArray *)readFileAtPathAsMutableArray:(NSString *)path;
+
++ (NSMutableData *)readFileAtPathAsMutableData:(NSString *)path;
++ (NSMutableData *)readFileAtPathAsMutableData:(NSString *)path error:(NSError **)error;
+
++ (NSMutableDictionary *)readFileAtPathAsMutableDictionary:(NSString *)path;
+
++ (NSString *)readFileAtPathAsString:(NSString *)path;
++ (NSString *)readFileAtPathAsString:(NSString *)path error:(NSError **)error;
+
+// 删除指定路径的文件/目录
++ (BOOL)removeFilesInDirectoryAtPath:(NSString *)path;
++ (BOOL)removeFilesInDirectoryAtPath:(NSString *)path error:(NSError **)error;
+
++ (BOOL)removeFilesInDirectoryAtPath:(NSString *)path withExtension:(NSString *)extension;
++ (BOOL)removeFilesInDirectoryAtPath:(NSString *)path withExtension:(NSString *)extension error:(NSError **)error;
+
++ (BOOL)removeFilesInDirectoryAtPath:(NSString *)path withPrefix:(NSString *)prefix;
++ (BOOL)removeFilesInDirectoryAtPath:(NSString *)path withPrefix:(NSString *)prefix error:(NSError **)error;
+
++ (BOOL)removeFilesInDirectoryAtPath:(NSString *)path withSuffix:(NSString *)suffix;
++ (BOOL)removeFilesInDirectoryAtPath:(NSString *)path withSuffix:(NSString *)suffix error:(NSError **)error;
+
++ (BOOL)removeItemsInDirectoryAtPath:(NSString *)path;
++ (BOOL)removeItemsInDirectoryAtPath:(NSString *)path error:(NSError **)error;
+
++ (BOOL)removeItemAtPath:(NSString *)path;
++ (BOOL)removeItemAtPath:(NSString *)path error:(NSError **)error;
+
+// 对指定路径的文件/目录重新命名
++ (BOOL)renameItemAtPath:(NSString *)path withName:(NSString *)name;
++ (BOOL)renameItemAtPath:(NSString *)path withName:(NSString *)name error:(NSError **)error;
+
+#pragma mark - 文件 size
++ (NSString *)sizeFormatted:(NSNumber *)size;
+
++ (NSString *)sizeFormattedOfDirectoryAtPath:(NSString *)path;
++ (NSString *)sizeFormattedOfDirectoryAtPath:(NSString *)path error:(NSError **)error;
+
++ (NSString *)sizeFormattedOfFileAtPath:(NSString *)path;
++ (NSString *)sizeFormattedOfFileAtPath:(NSString *)path error:(NSError **)error;
+
++ (NSString *)sizeFormattedOfItemAtPath:(NSString *)path;
++ (NSString *)sizeFormattedOfItemAtPath:(NSString *)path error:(NSError **)error;
+
++ (NSNumber *)sizeOfDirectoryAtPath:(NSString *)path;
++ (NSNumber *)sizeOfDirectoryAtPath:(NSString *)path error:(NSError **)error;
+
++ (NSNumber *)sizeOfFileAtPath:(NSString *)path;
++ (NSNumber *)sizeOfFileAtPath:(NSString *)path error:(NSError **)error;
+
++ (NSNumber *)sizeOfItemAtPath:(NSString *)path;
++ (NSNumber *)sizeOfItemAtPath:(NSString *)path error:(NSError **)error;
+
+#pragma mark -
+//将文件路径转化为url
++ (NSURL *)urlForItemAtPath:(NSString *)path;
+
+#pragma mark -
+//将文件写入指定路径
++ (BOOL)writeFileAtPath:(NSString *)path content:(NSObject *)content;
++ (BOOL)writeFileAtPath:(NSString *)path content:(NSObject *)content error:(NSError **)error;
+#pragma mark -
+// 获取指定路径的 图像 元数据,EXIF数据,TIFF数据
++ (NSDictionary *)metadataOfImageAtPath:(NSString *)path;
++ (NSDictionary *)exifDataOfImageAtPath:(NSString *)path;
++ (NSDictionary *)tiffDataOfImageAtPath:(NSString *)path;
+
+#pragma mark -
+// 读写xattr
++ (NSDictionary *)xattrOfItemAtPath:(NSString *)path;
++ (NSString *)xattrOfItemAtPath:(NSString *)path getValueForKey:(NSString *)key;
++ (BOOL)xattrOfItemAtPath:(NSString *)path hasValueForKey:(NSString *)key;
++ (BOOL)xattrOfItemAtPath:(NSString *)path removeValueForKey:(NSString *)key;
++ (BOOL)xattrOfItemAtPath:(NSString *)path setValue:(NSString *)value forKey:(NSString *)key;
+
+@end

+ 1041 - 0
BFFramework/Classes/PQGPUImage/akfilters/Tools/NXFileManager.m

@@ -0,0 +1,1041 @@
+//
+//  NXFFileManager.m
+//  NXlib
+//
+//  Created by AK on 15/8/30.
+//  Copyright (c) 2015年 AK. All rights reserved.
+//
+
+#import "NXFileManager.h"
+
+#import <ImageIO/ImageIO.h>
+#import <sys/xattr.h>
+
+@implementation NXFileManager
+
++ (NSBundle *)getMainBundle;
+{
+    return [NSBundle mainBundle];
+}
+
++ (NSString *)getMainBundleRes { return [[NSBundle mainBundle] resourcePath]; }
++ (NSString *)getDocumentDir
+{
+    NSString *documentDirectory =
+        [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) objectAtIndex:0];
+    return documentDirectory;
+}
+
++ (NSString *)getCacheDir
+{
+    NSString *cacheDirectory =
+        [NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) objectAtIndex:0];
+    return cacheDirectory;
+}
+
++ (NSString *)getTmpDir { return NSTemporaryDirectory(); }
+
++ (NSString *)getPathForDocuments:(NSString *)filename
+{
+    return [[self getDocumentDir] stringByAppendingPathComponent:filename];
+}
++ (NSString *)getPathForDocuments:(NSString *)filename inDir:(NSString *)dir
+{
+    return [[self getDirectoryForDocuments:dir] stringByAppendingPathComponent:filename];
+}
+
++ (NSString *)getDirectoryForDocuments:(NSString *)dir
+{
+    NSString *dirPath = [[self getDocumentDir] stringByAppendingPathComponent:dir];
+    BOOL isDir = NO;
+    BOOL isCreated = [[NSFileManager defaultManager] fileExistsAtPath:dirPath isDirectory:&isDir];
+    if (isCreated == NO || isDir == NO)
+    {
+        NSError *error = nil;
+        BOOL success = [[NSFileManager defaultManager] createDirectoryAtPath:dirPath
+                                                 withIntermediateDirectories:YES
+                                                                  attributes:nil
+                                                                       error:&error];
+        if (success == NO) NSLog(@"create dir error: %@", error.debugDescription);
+    }
+    return dirPath;
+}
+
++ (NSString *)deletingLastPathComponent:(NSString *)filePath { return [filePath stringByDeletingLastPathComponent]; }
++ (NSArray *)scanFilesInDirectory:(NSString *)directoryPath
+{
+    NSFileManager *fileManager = [NSFileManager defaultManager];
+    NSError *error = nil;
+    // fileList便是包含有该文件夹下所有文件的文件名及文件夹名的数组
+    NSArray *fileList = [fileManager contentsOfDirectoryAtPath:directoryPath error:&error];
+    NSLog(@"路径==%@,fileList%@", directoryPath, fileList);
+    return fileList;
+}
+
++ (NSString *)validateDir:(NSString *)dir
+{
+    BOOL isDir = NO;
+    NSFileManager *fileManager = [NSFileManager defaultManager];
+    BOOL existed = [fileManager fileExistsAtPath:dir isDirectory:&isDir];
+    if (!(isDir == YES && existed == YES))
+    {
+        [self createDirectoriesForPath:dir];
+    }
+    return dir;
+}
+
++ (BOOL)validateFile:(NSString *)filePath { return [[NSFileManager defaultManager] fileExistsAtPath:filePath]; }
++ (NSArray *)pathComponent:(NSString *)url { return [url componentsSeparatedByString:@"/"]; }
++ (NSMutableArray *)absoluteDirectories
+{
+    static NSMutableArray *directories = nil;
+    static dispatch_once_t token;
+
+    dispatch_once(&token, ^{
+
+        directories =
+            [NSMutableArray arrayWithObjects:[self pathForApplicationSupportDirectory], [self pathForCachesDirectory],
+                                             [self pathForDocumentsDirectory], [self pathForLibraryDirectory],
+                                             [self pathForMainBundleDirectory], [self pathForTemporaryDirectory], nil];
+
+        [directories sortUsingComparator:^NSComparisonResult(id obj1, id obj2) {
+
+            return (((NSString *)obj1).length > ((NSString *)obj2).length) ? 0 : 1;
+
+        }];
+    });
+
+    return directories;
+}
+
++ (NSString *)absoluteDirectoryForPath:(NSString *)path
+{
+    [self assertPath:path];
+
+    if ([path isEqualToString:@"/"])
+    {
+        return nil;
+    }
+
+    NSMutableArray *directories = [self absoluteDirectories];
+
+    for (NSString *directory in directories)
+    {
+        NSRange indexOfDirectoryInPath = [path rangeOfString:directory];
+
+        if (indexOfDirectoryInPath.location == 0)
+        {
+            return directory;
+        }
+    }
+
+    return nil;
+}
+
++ (NSString *)absolutePath:(NSString *)path
+{
+    [self assertPath:path];
+
+    NSString *defaultDirectory = [self absoluteDirectoryForPath:path];
+
+    if (defaultDirectory != nil)
+    {
+        return path;
+    }
+    else
+    {
+        return [self pathForDocumentsDirectoryWithPath:path];
+    }
+}
+
++ (void)assertPath:(NSString *)path
+{
+    NSAssert(path != nil, @"Invalid path. Path cannot be nil.");
+    NSAssert(![path isEqualToString:@""], @"Invalid path. Path cannot be empty string.");
+}
+
++ (id)attributeOfItemAtPath:(NSString *)path forKey:(NSString *)key
+{
+    return [[self attributesOfItemAtPath:path] objectForKey:key];
+}
+
++ (id)attributeOfItemAtPath:(NSString *)path forKey:(NSString *)key error:(NSError **)error
+{
+    return [[self attributesOfItemAtPath:path error:error] objectForKey:key];
+}
+
++ (NSDictionary *)attributesOfItemAtPath:(NSString *)path { return [self attributesOfItemAtPath:path error:nil]; }
++ (NSDictionary *)attributesOfItemAtPath:(NSString *)path error:(NSError **)error
+{
+    return [[NSFileManager defaultManager] attributesOfItemAtPath:[self absolutePath:path] error:error];
+}
+
++ (BOOL)copyItemAtPath:(NSString *)path toPath:(NSString *)toPath
+{
+    return [self copyItemAtPath:path toPath:toPath error:nil];
+}
+
++ (BOOL)copyItemAtPath:(NSString *)path toPath:(NSString *)toPath error:(NSError **)error
+{
+    return ([self createDirectoriesForFileAtPath:toPath error:error] &&
+            [[NSFileManager defaultManager] copyItemAtPath:[self absolutePath:path]
+                                                    toPath:[self absolutePath:toPath]
+                                                     error:error]);
+}
+
++ (BOOL)createDirectoriesForFileAtPath:(NSString *)path { return [self createDirectoriesForFileAtPath:path error:nil]; }
++ (BOOL)createDirectoriesForFileAtPath:(NSString *)path error:(NSError **)error
+{
+    NSString *pathLastChar = [path substringFromIndex:(path.length - 1)];
+
+    if ([pathLastChar isEqualToString:@"/"])
+    {
+        [NSException raise:@"Invalid path" format:@"file path can't have a trailing '/'."];
+
+        return NO;
+    }
+
+    return [self createDirectoriesForPath:[[self absolutePath:path] stringByDeletingLastPathComponent] error:error];
+}
+
++ (BOOL)createDirectoriesForPath:(NSString *)path { return [self createDirectoriesForPath:path error:nil]; }
++ (BOOL)createDirectoriesForPath:(NSString *)path error:(NSError **)error
+{
+    return [[NSFileManager defaultManager] createDirectoryAtPath:[self absolutePath:path]
+                                     withIntermediateDirectories:YES
+                                                      attributes:nil
+                                                           error:error];
+}
+
++ (BOOL)createFileAtPath:(NSString *)path { return [self createFileAtPath:path withContent:nil error:nil]; }
++ (BOOL)createFileAtPath:(NSString *)path error:(NSError **)error
+{
+    return [self createFileAtPath:path withContent:nil error:error];
+}
+
++ (BOOL)createFileAtPath:(NSString *)path withContent:(NSObject *)content
+{
+    return [self createFileAtPath:path withContent:content error:nil];
+}
+
++ (BOOL)createFileAtPath:(NSString *)path withContent:(NSObject *)content error:(NSError **)error
+{
+    if (![self existsItemAtPath:path] && [self createDirectoriesForFileAtPath:path error:error])
+    {
+        [[NSFileManager defaultManager] createFileAtPath:[self absolutePath:path] contents:nil attributes:nil];
+
+        if (content != nil)
+        {
+            [self writeFileAtPath:path content:content error:error];
+        }
+
+        return (error == nil);
+    }
+
+    return NO;
+}
+
++ (NSDate *)creationDateOfItemAtPath:(NSString *)path { return [self creationDateOfItemAtPath:path error:nil]; }
++ (NSDate *)creationDateOfItemAtPath:(NSString *)path error:(NSError **)error
+{
+    return (NSDate *)[self attributeOfItemAtPath:path forKey:NSFileCreationDate error:error];
+}
+
++ (BOOL)emptyCachesDirectory { return [self removeFilesInDirectoryAtPath:[self pathForCachesDirectory]]; }
++ (BOOL)emptyTemporaryDirectory { return [self removeFilesInDirectoryAtPath:[self pathForTemporaryDirectory]]; }
++ (BOOL)existsItemAtPath:(NSString *)path
+{
+    return [[NSFileManager defaultManager] fileExistsAtPath:[self absolutePath:path]];
+}
+
++ (BOOL)isDirectoryItemAtPath:(NSString *)path { return [self isDirectoryItemAtPath:path error:nil]; }
++ (BOOL)isDirectoryItemAtPath:(NSString *)path error:(NSError **)error
+{
+    return ([self attributeOfItemAtPath:path forKey:NSFileType error:error] == NSFileTypeDirectory);
+}
+
++ (BOOL)isEmptyItemAtPath:(NSString *)path { return [self isEmptyItemAtPath:path error:nil]; }
++ (BOOL)isEmptyItemAtPath:(NSString *)path error:(NSError **)error
+{
+    return ([self isFileItemAtPath:path error:error] && ([[self sizeOfItemAtPath:path error:error] intValue] == 0)) ||
+           ([self isDirectoryItemAtPath:path error:error] &&
+            ([[self listItemsInDirectoryAtPath:path deep:NO] count] == 0));
+}
+
++ (BOOL)isFileItemAtPath:(NSString *)path { return [self isFileItemAtPath:path error:nil]; }
++ (BOOL)isFileItemAtPath:(NSString *)path error:(NSError **)error
+{
+    return ([self attributeOfItemAtPath:path forKey:NSFileType error:error] == NSFileTypeRegular);
+}
+
++ (BOOL)isExecutableItemAtPath:(NSString *)path
+{
+    return [[NSFileManager defaultManager] isExecutableFileAtPath:[self absolutePath:path]];
+}
+
++ (BOOL)isReadableItemAtPath:(NSString *)path
+{
+    return [[NSFileManager defaultManager] isReadableFileAtPath:[self absolutePath:path]];
+}
+
++ (BOOL)isWritableItemAtPath:(NSString *)path
+{
+    return [[NSFileManager defaultManager] isWritableFileAtPath:[self absolutePath:path]];
+}
+
++ (NSArray *)listDirectoriesInDirectoryAtPath:(NSString *)path
+{
+    return [self listDirectoriesInDirectoryAtPath:path deep:NO];
+}
+
++ (NSArray *)listDirectoriesInDirectoryAtPath:(NSString *)path deep:(BOOL)deep
+{
+    NSArray *subpaths = [self listItemsInDirectoryAtPath:path deep:deep];
+
+    return [subpaths
+        filteredArrayUsingPredicate:[NSPredicate predicateWithBlock:^BOOL(id evaluatedObject, NSDictionary *bindings) {
+
+            NSString *subpath = (NSString *)evaluatedObject;
+
+            return [self isDirectoryItemAtPath:subpath];
+        }]];
+}
+
++ (NSArray *)listFilesInDirectoryAtPath:(NSString *)path { return [self listFilesInDirectoryAtPath:path deep:NO]; }
++ (NSArray *)listFilesInDirectoryAtPath:(NSString *)path deep:(BOOL)deep
+{
+    NSArray *subpaths = [self listItemsInDirectoryAtPath:path deep:deep];
+
+    return [subpaths
+        filteredArrayUsingPredicate:[NSPredicate predicateWithBlock:^BOOL(id evaluatedObject, NSDictionary *bindings) {
+
+            NSString *subpath = (NSString *)evaluatedObject;
+
+            return [self isFileItemAtPath:subpath];
+        }]];
+}
+
++ (NSArray *)listFilesInDirectoryAtPath:(NSString *)path withExtension:(NSString *)extension
+{
+    return [self listFilesInDirectoryAtPath:path withExtension:extension deep:NO];
+}
+
++ (NSArray *)listFilesInDirectoryAtPath:(NSString *)path withExtension:(NSString *)extension deep:(BOOL)deep
+{
+    NSArray *subpaths = [self listFilesInDirectoryAtPath:path deep:deep];
+
+    return [subpaths
+        filteredArrayUsingPredicate:[NSPredicate predicateWithBlock:^BOOL(id evaluatedObject, NSDictionary *bindings) {
+
+            NSString *subpath = (NSString *)evaluatedObject;
+            NSString *subpathExtension = [[subpath pathExtension] lowercaseString];
+            NSString *filterExtension =
+                [[extension lowercaseString] stringByReplacingOccurrencesOfString:@"." withString:@""];
+
+            return [subpathExtension isEqualToString:filterExtension];
+        }]];
+}
+
++ (NSArray *)listFilesInDirectoryAtPath:(NSString *)path withPrefix:(NSString *)prefix
+{
+    return [self listFilesInDirectoryAtPath:path withPrefix:prefix deep:NO];
+}
+
++ (NSArray *)listFilesInDirectoryAtPath:(NSString *)path withPrefix:(NSString *)prefix deep:(BOOL)deep
+{
+    NSArray *subpaths = [self listFilesInDirectoryAtPath:path deep:deep];
+
+    return [subpaths
+        filteredArrayUsingPredicate:[NSPredicate predicateWithBlock:^BOOL(id evaluatedObject, NSDictionary *bindings) {
+
+            NSString *subpath = (NSString *)evaluatedObject;
+
+            return ([subpath hasPrefix:prefix] || [subpath isEqualToString:prefix]);
+        }]];
+}
+
++ (NSArray *)listFilesInDirectoryAtPath:(NSString *)path withSuffix:(NSString *)suffix
+{
+    return [self listFilesInDirectoryAtPath:path withSuffix:suffix deep:NO];
+}
+
++ (NSArray *)listFilesInDirectoryAtPath:(NSString *)path withSuffix:(NSString *)suffix deep:(BOOL)deep
+{
+    NSArray *subpaths = [self listFilesInDirectoryAtPath:path deep:deep];
+
+    return [subpaths
+        filteredArrayUsingPredicate:[NSPredicate predicateWithBlock:^BOOL(id evaluatedObject, NSDictionary *bindings) {
+
+            NSString *subpath = (NSString *)evaluatedObject;
+            NSString *subpathName = [subpath stringByDeletingPathExtension];
+
+            return ([subpath hasSuffix:suffix] || [subpath isEqualToString:suffix] || [subpathName hasSuffix:suffix] ||
+                    [subpathName isEqualToString:suffix]);
+        }]];
+}
+
++ (NSArray *)listItemsInDirectoryAtPath:(NSString *)path deep:(BOOL)deep
+{
+    NSString *absolutePath = [self absolutePath:path];
+    NSArray *relativeSubpaths =
+        (deep ? [[NSFileManager defaultManager] subpathsOfDirectoryAtPath:absolutePath error:nil]
+              : [[NSFileManager defaultManager] contentsOfDirectoryAtPath:absolutePath error:nil]);
+
+    NSMutableArray *absoluteSubpaths = [[NSMutableArray alloc] init];
+
+    for (NSString *relativeSubpath in relativeSubpaths)
+    {
+        NSString *absoluteSubpath = [absolutePath stringByAppendingPathComponent:relativeSubpath];
+        [absoluteSubpaths addObject:absoluteSubpath];
+    }
+
+    return [NSArray arrayWithArray:absoluteSubpaths];
+}
+
++ (BOOL)moveItemAtPath:(NSString *)path toPath:(NSString *)toPath
+{
+    return [self moveItemAtPath:path toPath:toPath error:nil];
+}
+
++ (BOOL)moveItemAtPath:(NSString *)path toPath:(NSString *)toPath error:(NSError **)error
+{
+    return ([self createDirectoriesForFileAtPath:toPath error:error] &&
+            [[NSFileManager defaultManager] moveItemAtPath:[self absolutePath:path]
+                                                    toPath:[self absolutePath:toPath]
+                                                     error:error]);
+}
+
++ (NSString *)pathForApplicationSupportDirectory
+{
+    static NSString *path = nil;
+    static dispatch_once_t token;
+
+    dispatch_once(&token, ^{
+
+        NSArray *paths = NSSearchPathForDirectoriesInDomains(NSApplicationSupportDirectory, NSUserDomainMask, YES);
+
+        path = [paths lastObject];
+    });
+
+    return path;
+}
+
++ (NSString *)pathForApplicationSupportDirectoryWithPath:(NSString *)path
+{
+    return [[NXFileManager pathForApplicationSupportDirectory] stringByAppendingPathComponent:path];
+}
+
++ (NSString *)pathForCachesDirectory
+{
+    static NSString *path = nil;
+    static dispatch_once_t token;
+
+    dispatch_once(&token, ^{
+
+        NSArray *paths = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES);
+
+        path = [paths lastObject];
+    });
+
+    return path;
+}
+
++ (NSString *)pathForCachesDirectoryWithPath:(NSString *)path
+{
+    return [[NXFileManager pathForCachesDirectory] stringByAppendingPathComponent:path];
+}
+
++ (NSString *)pathForDocumentsDirectory
+{
+    static NSString *path = nil;
+    static dispatch_once_t token;
+
+    dispatch_once(&token, ^{
+
+        NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
+
+        path = [paths lastObject];
+    });
+
+    return path;
+}
+
++ (NSString *)pathForDocumentsDirectoryWithPath:(NSString *)path
+{
+    return [[NXFileManager pathForDocumentsDirectory] stringByAppendingPathComponent:path];
+}
+
++ (NSString *)pathForLibraryDirectory
+{
+    static NSString *path = nil;
+    static dispatch_once_t token;
+
+    dispatch_once(&token, ^{
+
+        NSArray *paths = NSSearchPathForDirectoriesInDomains(NSLibraryDirectory, NSUserDomainMask, YES);
+
+        path = [paths lastObject];
+    });
+
+    return path;
+}
+
++ (NSString *)pathForLibraryDirectoryWithPath:(NSString *)path
+{
+    return [[NXFileManager pathForLibraryDirectory] stringByAppendingPathComponent:path];
+}
+
++ (NSString *)pathForMainBundleDirectory { return [NSBundle mainBundle].resourcePath; }
++ (NSString *)pathForMainBundleDirectoryWithPath:(NSString *)path
+{
+    return [[NXFileManager pathForMainBundleDirectory] stringByAppendingPathComponent:path];
+}
+
++ (NSString *)pathForPlistNamed:(NSString *)name
+{
+    NSString *nameExtension = [name pathExtension];
+    NSString *plistExtension = @"plist";
+
+    if ([nameExtension isEqualToString:@""])
+    {
+        name = [name stringByAppendingPathExtension:plistExtension];
+    }
+
+    return [self pathForMainBundleDirectoryWithPath:name];
+}
+
++ (NSString *)pathForTemporaryDirectory
+{
+    static NSString *path = nil;
+    static dispatch_once_t token;
+
+    dispatch_once(&token, ^{
+
+        path = NSTemporaryDirectory();
+    });
+
+    return path;
+}
+
++ (NSString *)pathForTemporaryDirectoryWithPath:(NSString *)path
+{
+    return [[NXFileManager pathForTemporaryDirectory] stringByAppendingPathComponent:path];
+}
+
++ (NSString *)readFileAtPath:(NSString *)path { return [self readFileAtPathAsString:path error:nil]; }
++ (NSString *)readFileAtPath:(NSString *)path error:(NSError **)error
+{
+    return [self readFileAtPathAsString:path error:error];
+}
+
++ (NSArray *)readFileAtPathAsArray:(NSString *)path
+{
+    return [NSArray arrayWithContentsOfFile:[self absolutePath:path]];
+}
+
++ (NSObject *)readFileAtPathAsCustomModel:(NSString *)path
+{
+    return [NSKeyedUnarchiver unarchiveObjectWithFile:[self absolutePath:path]];
+}
+
++ (NSData *)readFileAtPathAsData:(NSString *)path { return [self readFileAtPathAsData:path error:nil]; }
++ (NSData *)readFileAtPathAsData:(NSString *)path error:(NSError **)error
+{
+    return [NSData dataWithContentsOfFile:[self absolutePath:path] options:NSDataReadingMapped error:error];
+}
+
++ (NSDictionary *)readFileAtPathAsDictionary:(NSString *)path
+{
+    return [NSDictionary dictionaryWithContentsOfFile:[self absolutePath:path]];
+}
+
++ (UIImage *)readFileAtPathAsImage:(NSString *)path { return [self readFileAtPathAsImage:path error:nil]; }
++ (UIImage *)readFileAtPathAsImage:(NSString *)path error:(NSError **)error
+{
+    NSData *data = [self readFileAtPathAsData:path error:error];
+
+    if (error == nil)
+    {
+        return [UIImage imageWithData:data];
+    }
+
+    return nil;
+}
+
++ (UIImageView *)readFileAtPathAsImageView:(NSString *)path { return [self readFileAtPathAsImageView:path error:nil]; }
++ (UIImageView *)readFileAtPathAsImageView:(NSString *)path error:(NSError **)error
+{
+    UIImage *image = [self readFileAtPathAsImage:path error:error];
+
+    if (error == nil)
+    {
+        UIImageView *imageView = [[UIImageView alloc] initWithImage:image];
+        [imageView sizeToFit];
+        return imageView;
+    }
+
+    return nil;
+}
+
++ (NSJSONSerialization *)readFileAtPathAsJSON:(NSString *)path { return [self readFileAtPathAsJSON:path error:nil]; }
++ (NSJSONSerialization *)readFileAtPathAsJSON:(NSString *)path error:(NSError **)error
+{
+    NSData *data = [self readFileAtPathAsData:path error:error];
+
+    if (error == nil)
+    {
+        NSJSONSerialization *json = [NSJSONSerialization JSONObjectWithData:data options:0 error:error];
+
+        if ([NSJSONSerialization isValidJSONObject:json])
+        {
+            return json;
+        }
+    }
+
+    return nil;
+}
+
++ (NSMutableArray *)readFileAtPathAsMutableArray:(NSString *)path
+{
+    return [NSMutableArray arrayWithContentsOfFile:[self absolutePath:path]];
+}
+
++ (NSMutableData *)readFileAtPathAsMutableData:(NSString *)path
+{
+    return [self readFileAtPathAsMutableData:path error:nil];
+}
+
++ (NSMutableData *)readFileAtPathAsMutableData:(NSString *)path error:(NSError **)error
+{
+    return [NSMutableData dataWithContentsOfFile:[self absolutePath:path] options:NSDataReadingMapped error:error];
+}
+
++ (NSMutableDictionary *)readFileAtPathAsMutableDictionary:(NSString *)path
+{
+    return [NSMutableDictionary dictionaryWithContentsOfFile:[self absolutePath:path]];
+}
+
++ (NSString *)readFileAtPathAsString:(NSString *)path { return [self readFileAtPath:path error:nil]; }
++ (NSString *)readFileAtPathAsString:(NSString *)path error:(NSError **)error
+{
+    return [NSString stringWithContentsOfFile:[self absolutePath:path] encoding:NSUTF8StringEncoding error:error];
+}
+
++ (BOOL)removeFilesInDirectoryAtPath:(NSString *)path
+{
+    return [self removeItemsAtPaths:[self listFilesInDirectoryAtPath:path] error:nil];
+}
+
++ (BOOL)removeFilesInDirectoryAtPath:(NSString *)path error:(NSError **)error
+{
+    return [self removeItemsAtPaths:[self listFilesInDirectoryAtPath:path] error:error];
+}
+
++ (BOOL)removeFilesInDirectoryAtPath:(NSString *)path withExtension:(NSString *)extension
+{
+    return [self removeItemsAtPaths:[self listFilesInDirectoryAtPath:path withExtension:extension] error:nil];
+}
+
++ (BOOL)removeFilesInDirectoryAtPath:(NSString *)path withExtension:(NSString *)extension error:(NSError **)error
+{
+    return [self removeItemsAtPaths:[self listFilesInDirectoryAtPath:path withExtension:extension] error:error];
+}
+
++ (BOOL)removeFilesInDirectoryAtPath:(NSString *)path withPrefix:(NSString *)prefix
+{
+    return [self removeItemsAtPaths:[self listFilesInDirectoryAtPath:path withPrefix:prefix] error:nil];
+}
+
++ (BOOL)removeFilesInDirectoryAtPath:(NSString *)path withPrefix:(NSString *)prefix error:(NSError **)error
+{
+    return [self removeItemsAtPaths:[self listFilesInDirectoryAtPath:path withPrefix:prefix] error:error];
+}
+
++ (BOOL)removeFilesInDirectoryAtPath:(NSString *)path withSuffix:(NSString *)suffix
+{
+    return [self removeItemsAtPaths:[self listFilesInDirectoryAtPath:path withSuffix:suffix] error:nil];
+}
+
++ (BOOL)removeFilesInDirectoryAtPath:(NSString *)path withSuffix:(NSString *)suffix error:(NSError **)error
+{
+    return [self removeItemsAtPaths:[self listFilesInDirectoryAtPath:path withSuffix:suffix] error:error];
+}
+
++ (BOOL)removeItemsInDirectoryAtPath:(NSString *)path { return [self removeItemsInDirectoryAtPath:path error:nil]; }
++ (BOOL)removeItemsInDirectoryAtPath:(NSString *)path error:(NSError **)error
+{
+    return [self removeItemsAtPaths:[self listItemsInDirectoryAtPath:path deep:NO] error:error];
+}
+
++ (BOOL)removeItemAtPath:(NSString *)path { return [self removeItemAtPath:path error:nil]; }
++ (BOOL)removeItemAtPath:(NSString *)path error:(NSError **)error
+{
+    return [[NSFileManager defaultManager] removeItemAtPath:[self absolutePath:path] error:error];
+}
+
++ (BOOL)removeItemsAtPaths:(NSArray *)paths { return [self removeItemsAtPaths:paths error:nil]; }
++ (BOOL)removeItemsAtPaths:(NSArray *)paths error:(NSError **)error
+{
+    BOOL success = YES;
+
+    for (NSString *path in paths)
+    {
+        success &= [self removeItemAtPath:[self absolutePath:path] error:error];
+    }
+
+    return success;
+}
+
++ (BOOL)renameItemAtPath:(NSString *)path withName:(NSString *)name
+{
+    return [self renameItemAtPath:path withName:name error:nil];
+}
+
++ (BOOL)renameItemAtPath:(NSString *)path withName:(NSString *)name error:(NSError **)error
+{
+    NSRange indexOfSlash = [name rangeOfString:@"/"];
+
+    if (indexOfSlash.location < name.length)
+    {
+        [NSException raise:@"Invalid name" format:@"file name can't contain a '/'."];
+
+        return NO;
+    }
+
+    return [self moveItemAtPath:path
+                         toPath:[[[self absolutePath:path] stringByDeletingLastPathComponent]
+                                    stringByAppendingPathComponent:name]
+                          error:error];
+}
+
++ (NSString *)sizeFormatted:(NSNumber *)size
+{
+    // TODO if OS X 10.8 or iOS 6
+    // return [NSByteCountFormatter stringFromByteCount:[size intValue]
+    // countStyle:NSByteCountFormatterCountStyleFile];
+
+    double convertedValue = [size doubleValue];
+    int multiplyFactor = 0;
+
+    NSArray *tokens = @[ @"bytes", @"KB", @"MB", @"GB", @"TB" ];
+
+    while (convertedValue > 1024)
+    {
+        convertedValue /= 1024;
+
+        multiplyFactor++;
+    }
+
+    NSString *sizeFormat = ((multiplyFactor > 1) ? @"%4.2f %@" : @"%4.0f %@");
+
+    return [NSString stringWithFormat:sizeFormat, convertedValue, tokens[multiplyFactor]];
+}
+
++ (NSString *)sizeFormattedOfDirectoryAtPath:(NSString *)path
+{
+    return [self sizeFormattedOfDirectoryAtPath:path error:nil];
+}
+
++ (NSString *)sizeFormattedOfDirectoryAtPath:(NSString *)path error:(NSError **)error
+{
+    NSNumber *size = [self sizeOfDirectoryAtPath:path error:error];
+
+    if (size != nil && error == nil)
+    {
+        return [self sizeFormatted:size];
+    }
+
+    return nil;
+}
+
++ (NSString *)sizeFormattedOfFileAtPath:(NSString *)path { return [self sizeFormattedOfFileAtPath:path error:nil]; }
++ (NSString *)sizeFormattedOfFileAtPath:(NSString *)path error:(NSError **)error
+{
+    NSNumber *size = [self sizeOfFileAtPath:path error:error];
+
+    if (size != nil && error == nil)
+    {
+        return [self sizeFormatted:size];
+    }
+
+    return nil;
+}
+
++ (NSString *)sizeFormattedOfItemAtPath:(NSString *)path { return [self sizeFormattedOfItemAtPath:path error:nil]; }
++ (NSString *)sizeFormattedOfItemAtPath:(NSString *)path error:(NSError **)error
+{
+    NSNumber *size = [self sizeOfItemAtPath:path error:error];
+
+    if (size != nil && error == nil)
+    {
+        return [self sizeFormatted:size];
+    }
+
+    return nil;
+}
+
++ (NSNumber *)sizeOfDirectoryAtPath:(NSString *)path { return [self sizeOfDirectoryAtPath:path error:nil]; }
++ (NSNumber *)sizeOfDirectoryAtPath:(NSString *)path error:(NSError **)error
+{
+    if ([self isDirectoryItemAtPath:path error:error])
+    {
+        if (error == nil)
+        {
+            NSNumber *size = [self sizeOfItemAtPath:path error:error];
+            double sizeValue = [size doubleValue];
+
+            if (error == nil)
+            {
+                NSArray *subpaths = [self listItemsInDirectoryAtPath:path deep:YES];
+                NSUInteger subpathsCount = [subpaths count];
+
+                for (NSUInteger i = 0; i < subpathsCount; i++)
+                {
+                    NSString *subpath = [subpaths objectAtIndex:i];
+                    NSNumber *subpathSize = [self sizeOfItemAtPath:subpath error:error];
+
+                    if (error == nil)
+                    {
+                        sizeValue += [subpathSize doubleValue];
+                    }
+                    else
+                    {
+                        return nil;
+                    }
+                }
+
+                return [NSNumber numberWithDouble:sizeValue];
+            }
+        }
+    }
+
+    return nil;
+}
+
++ (NSNumber *)sizeOfFileAtPath:(NSString *)path { return [self sizeOfFileAtPath:path error:nil]; }
++ (NSNumber *)sizeOfFileAtPath:(NSString *)path error:(NSError **)error
+{
+    if ([self isFileItemAtPath:path error:error])
+    {
+        if (error == nil)
+        {
+            return [self sizeOfItemAtPath:path error:error];
+        }
+    }
+
+    return nil;
+}
+
++ (NSNumber *)sizeOfItemAtPath:(NSString *)path { return [self sizeOfItemAtPath:path error:nil]; }
++ (NSNumber *)sizeOfItemAtPath:(NSString *)path error:(NSError **)error
+{
+    return (NSNumber *)[self attributeOfItemAtPath:path forKey:NSFileSize error:error];
+}
+
++ (NSURL *)urlForItemAtPath:(NSString *)path { return [NSURL fileURLWithPath:[self absolutePath:path]]; }
++ (BOOL)writeFileAtPath:(NSString *)path content:(NSObject *)content
+{
+    return [self writeFileAtPath:path content:content error:nil];
+}
+
++ (BOOL)writeFileAtPath:(NSString *)path content:(NSObject *)content error:(NSError **)error
+{
+    if (content == nil)
+    {
+        [NSException raise:@"Invalid content" format:@"content can't be nil."];
+    }
+
+    [self createFileAtPath:path withContent:nil error:error];
+
+    NSString *absolutePath = [self absolutePath:path];
+
+    if ([content isKindOfClass:[NSMutableArray class]])
+    {
+        [((NSMutableArray *)content) writeToFile:absolutePath atomically:YES];
+    }
+    else if ([content isKindOfClass:[NSArray class]])
+    {
+        [((NSArray *)content) writeToFile:absolutePath atomically:YES];
+    }
+    else if ([content isKindOfClass:[NSMutableData class]])
+    {
+        [((NSMutableData *)content) writeToFile:absolutePath atomically:YES];
+    }
+    else if ([content isKindOfClass:[NSData class]])
+    {
+        [((NSData *)content) writeToFile:absolutePath atomically:YES];
+    }
+    else if ([content isKindOfClass:[NSMutableDictionary class]])
+    {
+        [((NSMutableDictionary *)content) writeToFile:absolutePath atomically:YES];
+    }
+    else if ([content isKindOfClass:[NSDictionary class]])
+    {
+        [((NSDictionary *)content) writeToFile:absolutePath atomically:YES];
+    }
+    else if ([content isKindOfClass:[NSJSONSerialization class]])
+    {
+        [((NSDictionary *)content) writeToFile:absolutePath atomically:YES];
+    }
+    else if ([content isKindOfClass:[NSMutableString class]])
+    {
+        [[((NSString *)content) dataUsingEncoding:NSUTF8StringEncoding] writeToFile:absolutePath atomically:YES];
+    }
+    else if ([content isKindOfClass:[NSString class]])
+    {
+        [[((NSString *)content) dataUsingEncoding:NSUTF8StringEncoding] writeToFile:absolutePath atomically:YES];
+    }
+    else if ([content isKindOfClass:[UIImage class]])
+    {
+        [UIImagePNGRepresentation((UIImage *)content) writeToFile:absolutePath atomically:YES];
+    }
+    else if ([content isKindOfClass:[UIImageView class]])
+    {
+        return [self writeFileAtPath:absolutePath content:((UIImageView *)content).image error:error];
+    }
+    else if ([content conformsToProtocol:@protocol(NSCoding)])
+    {
+        [NSKeyedArchiver archiveRootObject:content toFile:absolutePath];
+    }
+    else
+    {
+        [NSException raise:@"Invalid content type"
+                    format:@"content of type %@ is not handled.", NSStringFromClass([content class])];
+
+        return NO;
+    }
+
+    return YES;
+}
+
++ (NSDictionary *)metadataOfImageAtPath:(NSString *)path
+{
+    if ([self isFileItemAtPath:path])
+    {
+        // http://blog.depicus.com/getting-exif-data-from-images-on-ios/
+
+        NSURL *url = [self urlForItemAtPath:path];
+        CGImageSourceRef sourceRef = CGImageSourceCreateWithURL((CFURLRef)url, NULL);
+        NSDictionary *metadata =
+            (NSDictionary *)CFBridgingRelease(CGImageSourceCopyPropertiesAtIndex(sourceRef, 0, NULL));
+
+        return metadata;
+    }
+
+    return nil;
+}
+
++ (NSDictionary *)exifDataOfImageAtPath:(NSString *)path
+{
+    NSDictionary *metadata = [self metadataOfImageAtPath:path];
+
+    if (metadata)
+    {
+        return [metadata objectForKey:(NSString *)kCGImagePropertyExifDictionary];
+    }
+
+    return nil;
+}
+
++ (NSDictionary *)tiffDataOfImageAtPath:(NSString *)path
+{
+    NSDictionary *metadata = [self metadataOfImageAtPath:path];
+
+    if (metadata)
+    {
+        return [metadata objectForKey:(NSString *)kCGImagePropertyTIFFDictionary];
+    }
+
+    return nil;
+}
+
++ (NSDictionary *)xattrOfItemAtPath:(NSString *)path
+{
+    NSMutableDictionary *values = [[NSMutableDictionary alloc] init];
+
+    const char *upath = [path UTF8String];
+
+    ssize_t ukeysSize = listxattr(upath, NULL, 0, 0);
+
+    if (ukeysSize > 0)
+    {
+        char *ukeys = malloc(ukeysSize);
+
+        ukeysSize = listxattr(upath, ukeys, ukeysSize, 0);
+
+        NSUInteger keyOffset = 0;
+        NSString *key;
+        NSString *value;
+
+        while (keyOffset < ukeysSize)
+        {
+            key = [NSString stringWithUTF8String:(keyOffset + ukeys)];
+            keyOffset += ([key length] + 1);
+
+            value = [self xattrOfItemAtPath:path getValueForKey:key];
+            [values setObject:value forKey:key];
+        }
+
+        free(ukeys);
+    }
+
+    return [NSDictionary dictionaryWithObjects:[values allKeys] forKeys:[values allValues]];
+}
+
++ (NSString *)xattrOfItemAtPath:(NSString *)path getValueForKey:(NSString *)key
+{
+    NSString *value = nil;
+
+    const char *ukey = [key UTF8String];
+    const char *upath = [path UTF8String];
+
+    ssize_t uvalueSize = getxattr(upath, ukey, NULL, 0, 0, 0);
+
+    if (uvalueSize > -1)
+    {
+        if (uvalueSize == 0)
+        {
+            value = @"";
+        }
+        else
+        {
+            char *uvalue = malloc(uvalueSize);
+
+            if (uvalue)
+            {
+                getxattr(upath, ukey, uvalue, uvalueSize, 0, 0);
+                uvalue[uvalueSize] = '\0';
+                value = [NSString stringWithUTF8String:uvalue];
+                free(uvalue);
+            }
+        }
+    }
+
+    return value;
+}
+
++ (BOOL)xattrOfItemAtPath:(NSString *)path hasValueForKey:(NSString *)key
+{
+    return ([self xattrOfItemAtPath:path getValueForKey:key] != nil);
+}
+
++ (BOOL)xattrOfItemAtPath:(NSString *)path removeValueForKey:(NSString *)key
+{
+    int result = removexattr([path UTF8String], [key UTF8String], 0);
+
+    return (result == 0);
+}
+
++ (BOOL)xattrOfItemAtPath:(NSString *)path setValue:(NSString *)value forKey:(NSString *)key
+{
+    if (value == nil)
+    {
+        return NO;
+    }
+
+    int result = setxattr([path UTF8String], [key UTF8String], [value UTF8String], [value length], 0, 0);
+
+    return (result == 0);
+}
+
+@end

+ 53 - 0
BFFramework/Classes/PQGPUImage/akfilters/Tools/NXVideoMerge.h

@@ -0,0 +1,53 @@
+//
+//  NXVideoRecorder.h
+//  Philm
+//
+//  Created by AK on 2017/2/16.
+//  Copyright © 2017年 yoyo. All rights reserved.
+//
+/**
+ *  功能 :将多段视频合并成一个MP4视频
+ *
+ */
+#import <Foundation/Foundation.h>
+
+//通用 callback
+typedef void(^NXGenericCallback)(BOOL success, id result);
+
+
+//合成进度
+typedef void (^NXProgressHandler)(double progress);
+@interface NXVideoMerge : NSObject
+{
+ 
+}
+
+//合成进度 ,回调会回到主线
+@property (nonatomic, copy) NXProgressHandler progressHandler;
+
+
+/**
+ *  合成多段视频
+ *
+ *  @param fileURLArray 包含所有视频分段的文件URL数组,必须是[NSURL fileURLWithString:...]得到的
+ *  @param renderSize   输出视频的宽高
+ *  @param finishBlock  生成视频结果回调 返回的是NSURL类型 并回到主线
+ */
+- (void)mergeAndExportVideosWithFileURLs:(NSArray<NSURL *> *)fileURLArray
+                              renderSize:(CGSize)renderSize
+                             finishBlock:(NXGenericCallback)finishBlock;
+
+/**
+ *  合成多段视频
+ *
+ *  @param fileURLArray 包含所有视频分段的文件URL数组,必须是[NSURL fileURLWithString:...]得到的
+ *  @param renderSize   输出视频的宽高
+ *  @param savePath     输出视频MP4的文件路径 delault is ~/Library/Caches/NXExportVideo.mp4
+ *  @param finishBlock  生成视频结果回调 返回的是NSURL类型 并回到主线
+ */
+- (void)mergeAndExportVideosWithFileURLs:(NSArray<NSURL *> *)fileURLArray
+                              renderSize:(CGSize)renderSize
+                                savePath:(NSString *)savePath
+                             finishBlock:(NXGenericCallback)finishBlock;
+@end
+

+ 343 - 0
BFFramework/Classes/PQGPUImage/akfilters/Tools/NXVideoMerge.m

@@ -0,0 +1,343 @@
+//
+//  NXVideoRecorder.m
+//  Philm
+//
+//  Created by AK on 2017/2/16.
+//  Copyright © 2017年 yoyo. All rights reserved.
+//
+
+//合并输出视频的文件名
+#define NXExportVideoName @"NXExportVideo.mp4"
+
+//视频叠加 http://www.theappguruz.com/blog/ios-overlap-multiple-videos
+
+#import "NXVideoMerge.h"
+#import "NXFileManager.h"
+#import <AVFoundation/AVFoundation.h>
+
+static void *ExportProcess = &ExportProcess;
+
+@interface NXVideoMerge ()
+{
+    AVAssetExportSession *exporterSession;
+    NSTimer *exportProgressTimer;  //监听导出的进度
+}
+
+@end
+
+@implementation NXVideoMerge
+
+- (void)applyVideoEffectsToComposition:(AVMutableVideoComposition *)composition size:(CGSize)size
+{
+    // 在子类中实现本方法可以自定义显示layer
+}
+
+- (void)mergeAndExportVideosWithFileURLs:(NSArray<NSURL *> *)fileURLArray
+                              renderSize:(CGSize)renderSize
+                             finishBlock:(NXGenericCallback)finishBlock;
+{
+    [self mergeAndExportVideosWithFileURLs:fileURLArray renderSize:renderSize savePath:@"" finishBlock:finishBlock];
+}
+
+- (void)mergeAndExportVideosWithFileURLs:(NSArray<NSURL *> *)fileURLArray
+                              renderSize:(CGSize)renderSize
+                                savePath:(NSString *)savePath
+                             finishBlock:(NXGenericCallback)finishBlock
+{
+    NSLog(@"合成视频个数:  %zd", fileURLArray.count);
+
+    NSMutableArray *layerInstructionArray = [[NSMutableArray alloc] init];
+    AVMutableComposition *mixComposition = [[AVMutableComposition alloc] init];
+
+    CMTime totalDuration = kCMTimeZero;
+
+    for (int i = 0; i < [fileURLArray count]; i++)
+    {
+        // 1 准备 asset & AssetTrack
+        NSURL *videoURL = [fileURLArray objectAtIndex:i];
+        AVAsset *asset = [AVAsset assetWithURL:videoURL];
+
+        if (!asset)
+        {
+            NSLog(@"视频文件 %@ 读取失败", [videoURL absoluteString]);
+            continue;
+        }
+
+        NSLog(@"视频idx %d path:%@ duration: %f timescale%d", i, videoURL.absoluteString,
+              CMTimeGetSeconds(asset.duration), asset.duration.timescale);
+
+        //如果视频没有视频数据不处理本视频,视频轨道总数
+        NSArray *assetVideoTracks = [asset tracksWithMediaType:AVMediaTypeVideo];
+        //音频轨道总数
+        NSArray *assetAudioTracks = [asset tracksWithMediaType:AVMediaTypeAudio];
+
+        if (assetVideoTracks.count == 0)
+        {
+            NSLog(@"此视频没有视频轨信息!!!!!");
+            continue;
+        }
+
+        NSLog(@"声音轨道数: %lu", (unsigned long)assetAudioTracks.count);
+
+        AVAssetTrack *videoAssetTrack = [assetVideoTracks objectAtIndex:0];
+
+        //加载指定声音文件 test
+        /*
+          NSString *auidoPath1 = [[NSBundle mainBundle] pathForResource:@"music" ofType:@"mp3"];
+          AVURLAsset *audioAsset = [AVURLAsset assetWithURL:[NSURL fileURLWithPath:auidoPath1]];
+         AVAssetTrack *audioAssetTrack = [[audioAsset1 tracksWithMediaType:AVMediaTypeAudio] firstObject];
+
+        */
+
+        // 2,合并音频数据 ,如果有声音数据才进行声音的合并,否则会CRASH
+        if (assetAudioTracks.count > 0)
+        {
+            // audio track
+            NSError *error = nil;
+
+            AVMutableCompositionTrack *audioTrack =
+                [mixComposition addMutableTrackWithMediaType:AVMediaTypeAudio
+                                            preferredTrackID:kCMPersistentTrackID_Invalid];
+
+            [audioTrack insertTimeRange:CMTimeRangeMake(kCMTimeZero, asset.duration)
+                                ofTrack:[assetAudioTracks objectAtIndex:0]
+                                 atTime:totalDuration
+                                  error:&error];
+            if (error)
+            {
+                NSLog(@"插入声音数据失败! %@", error);
+            }
+        }
+        else
+        {
+            NSLog(@"没有声音数据 %@", asset);
+        }
+
+        AVMutableCompositionTrack *videoTrack =
+            [mixComposition addMutableTrackWithMediaType:AVMediaTypeVideo
+                                        preferredTrackID:kCMPersistentTrackID_Invalid];
+
+        // 3,合并视频数据 video track
+        if (assetVideoTracks.count > 0)
+        {
+            NSError *error = nil;
+
+            [videoTrack insertTimeRange:CMTimeRangeMake(kCMTimeZero, asset.duration)
+                                ofTrack:videoAssetTrack
+                                 atTime:totalDuration
+                                  error:&error];
+
+            if (error)
+            {
+                NSLog(@"videoTrack insertTime error: %@", error);
+            }
+        }
+        else
+        {
+            NSLog(@"没有视频数据 %@", asset);
+        }
+
+        // fix orientationissue
+        AVMutableVideoCompositionLayerInstruction *layerInstruciton =
+            [AVMutableVideoCompositionLayerInstruction videoCompositionLayerInstructionWithAssetTrack:videoTrack];
+
+        totalDuration = CMTimeAdd(totalDuration, asset.duration);
+        // set other layerTransform? 如多屏指定大小显示
+        /*
+        CGAffineTransform SecondScale = CGAffineTransformMakeScale(0.5f,0.5f);
+        CGAffineTransform SecondMove = CGAffineTransformMakeTranslation(0,0);
+        [layerInstruciton setTransform:CGAffineTransformConcat(SecondScale,SecondMove) atTime:kCMTimeZero];
+        */
+        
+        //XXXX 全屏合成有问题先不进行Transform
+        CGAffineTransform layerTransform = [self applayAfftransform:videoAssetTrack renderSize:renderSize isCut:YES];
+        
+        [layerInstruciton setTransform:layerTransform atTime:kCMTimeZero];
+
+        [layerInstruciton setOpacity:0.0 atTime:totalDuration];
+
+        // data
+        [layerInstructionArray addObject:layerInstruciton];
+    }
+
+    // LayerInstruction's count is 0
+    if (layerInstructionArray.count == 0)
+    {
+        if (finishBlock)
+        {
+            finishBlock(NO, @"视频数据都为空!!!");
+        }
+        return;
+    }
+
+    //导出视频
+    [self exportVideoWithsavePath:savePath
+                        timeRange:CMTimeRangeMake(kCMTimeZero, totalDuration)
+                layerInstructions:layerInstructionArray
+                       renderSize:renderSize
+                   mixComposition:mixComposition
+                      finishBlock:finishBlock];
+}
+
+- (void)exportVideoWithsavePath:(NSString *)savePath
+                      timeRange:(CMTimeRange)timeRange
+              layerInstructions:(NSArray<AVVideoCompositionLayerInstruction *> *)layerInstructions
+                     renderSize:(CGSize)renderSize
+                 mixComposition:(AVMutableComposition *)mixComposition
+                    finishBlock:(NXGenericCallback)finishBlock
+
+{
+    //设置输出文件路径
+    savePath =
+        savePath.length > 0 ? savePath : [[NXFileManager getCacheDir] stringByAppendingPathComponent:NXExportVideoName];
+    //如果文件存在 删除老数据
+    unlink([savePath UTF8String]);
+
+    AVMutableVideoCompositionInstruction *mainInstruciton =
+        [AVMutableVideoCompositionInstruction videoCompositionInstruction];
+
+    mainInstruciton.timeRange = timeRange;
+    mainInstruciton.layerInstructions = layerInstructions;
+
+    AVMutableVideoComposition *mainCompositionInst = [AVMutableVideoComposition videoComposition];
+    mainCompositionInst.instructions = @[ mainInstruciton ];
+    mainCompositionInst.frameDuration = CMTimeMake(1, 30);
+    mainCompositionInst.renderSize = renderSize;
+
+    [self applyVideoEffectsToComposition:mainCompositionInst size:renderSize];
+
+    exporterSession =
+        [[AVAssetExportSession alloc] initWithAsset:mixComposition presetName:AVAssetExportPresetHighestQuality];
+    exporterSession.videoComposition = mainCompositionInst;
+    exporterSession.outputURL = [NSURL fileURLWithPath:savePath];
+    exporterSession.outputFileType = AVFileTypeMPEG4;
+
+    exporterSession.shouldOptimizeForNetworkUse = YES;
+    NSLog(@"支持的文件格式 %@", [exporterSession supportedFileTypes]);
+
+    exportProgressTimer = [NSTimer scheduledTimerWithTimeInterval:1 / 60.
+                                                           target:self
+                                                         selector:@selector(updateExportDisplay)
+                                                         userInfo:nil
+                                                          repeats:YES];
+
+    [exporterSession exportAsynchronouslyWithCompletionHandler:^{
+
+        dispatch_async(dispatch_get_main_queue(), ^{
+
+            AVAsset *asset = [AVAsset assetWithURL:[NSURL fileURLWithPath:savePath]];
+            NSLog(@"导出视频结果 %ld 路径: %@ 时长 %f", (long)exporterSession.status, savePath,
+                  CMTimeGetSeconds(asset.duration));
+            //导出完成且时长不为0
+            if (exporterSession.status == AVAssetExportSessionStatusCompleted && CMTimeGetSeconds(asset.duration) != 0)
+            {
+
+                if (finishBlock)
+                {
+                    finishBlock(YES, [NSURL fileURLWithPath:savePath]);
+                }
+            }
+            else
+            {
+                NSLog(@"处理视频失败 %ld", (long)exporterSession.status);
+                //合成视频失败时不在发进度block
+                [exportProgressTimer invalidate];
+
+                if (finishBlock)
+                {
+                    finishBlock(NO, @"");
+                }
+            }
+        });
+
+    }];
+}
+
+//处理进度
+- (void)updateExportDisplay
+{
+    NSLog(@"导出进度 progress %f", exporterSession.progress);
+    if (exporterSession.progress < 1)
+    {
+        //回到主线程
+        dispatch_async(dispatch_get_main_queue(), ^{
+
+            if (_progressHandler)
+            {
+                _progressHandler(exporterSession.progress);
+                _progressHandler = nil;
+            }
+        });
+    }
+    else
+    {
+        [exportProgressTimer invalidate];
+    }
+}
+
+
+- (CGAffineTransform )applayAfftransform:(AVAssetTrack *)videoAssetTrack renderSize:(CGSize)renderSize isCut:(BOOL)cut{
+
+    CGAffineTransform layerTransform = CGAffineTransformIdentity;
+    double rate = 1.0f;
+    CGPoint point = CGPointZero;
+    [self computeRate: &rate targetPoint:&point renderSize:renderSize vidoSize:videoAssetTrack.naturalSize cut:cut];
+    layerTransform = CGAffineTransformMake(videoAssetTrack.preferredTransform.a,
+                                           videoAssetTrack.preferredTransform.b,
+                                           videoAssetTrack.preferredTransform.c,
+                                           videoAssetTrack.preferredTransform.d,
+                                           videoAssetTrack.preferredTransform.tx * rate,
+                                           videoAssetTrack.preferredTransform.ty * rate);
+    layerTransform = CGAffineTransformScale(layerTransform, rate, rate);  //放缩,解决前后摄像结果大小不对称
+    layerTransform = CGAffineTransformConcat(
+                                             layerTransform,
+                                             CGAffineTransformMake(1, 0, 0, 1, -point.x * rate , -point.y * rate));
+    return layerTransform;
+}
+- (void)computeRate:(double *)rate targetPoint:(CGPoint *) point renderSize:(CGSize)renderSize vidoSize:(CGSize)videoSize cut:(BOOL) cut{
+
+    if (cut) {
+        //按短边充满画布
+        if (videoSize.width >= videoSize.height)
+        {
+            *rate = renderSize.height / videoSize.height;
+            //视频实际宽度比画布要宽
+            if(renderSize.width < *rate *videoSize.width )
+            {
+                (*point).x = (videoSize.width - videoSize.height)/2.0f;
+            }
+            
+        } else {
+            
+            *rate = renderSize.width / videoSize.width;
+            //视频实际高度比画布要高
+            if(renderSize.height < *rate *videoSize.height){
+                
+                (*point).y = (videoSize.height - videoSize.width)/2.0f;
+            }
+        }
+        
+    } else {
+        
+        //不足部分留黑边
+        if(videoSize.width >= videoSize.height){
+            
+            *rate = renderSize.width / videoSize.width;
+            
+            if(renderSize.height > *rate * videoSize.height){
+                
+                (*point).y = (videoSize.height - videoSize.width)/2.0f;
+            }
+        } else {
+            
+            *rate = renderSize.height / videoSize.height;
+            
+            if (renderSize.width > *rate * videoSize.width) {
+                
+                (*point).x = (videoSize.width - videoSize.height) /2.0f;
+            }
+        }
+    }
+}
+@end
+

+ 94 - 21
BFFramework/Classes/PQGPUImage/akfilters/Tools/PQCompositionExporter.swift

@@ -44,6 +44,12 @@ public class PQCompositionExporter {
   
     /// Use serial queue to ensure that the picture is smooth
     var createFiltersQueue: DispatchQueue!
+    
+    //导出时是否添加水印
+    var isAddWatermark:Bool = false
+    
+    //是否合成片尾模式
+    var isEndMovie:Bool = false
 
     public init(asset: AVAsset, videoComposition: AVVideoComposition? = nil, audioMix: AVAudioMix? = nil, filters: [ImageProcessingOperation]? = nil,stickers:[PQEditVisionTrackMaterialsModel]? = nil, animationTool: AVVideoCompositionCoreAnimationTool? = nil, exportURL: URL) {
         self.asset = asset
@@ -191,31 +197,98 @@ public class PQCompositionExporter {
             }
             input!.removeAllTargets()
             let currentTarget: ImageSource = input!
-            if(currentSticker?.type == StickerType.IMAGE.rawValue && showGaussianBlur){
-                //高斯层
-                let json = currentSticker?.toJSONString(prettyPrint: false)
-                if json == nil {
-                    BFLog(message: "数据转换有问题 跳转")
-                    return
-                }
-                let blurStickerModel: PQEditVisionTrackMaterialsModel? = Mapper<PQEditVisionTrackMaterialsModel>().map(JSONString: json!)
-                blurStickerModel?.canvasFillType = stickerContentMode.aspectFillStr.rawValue
-                let showGaussianFitler:PQBaseFilter = PQImageFilter(sticker: blurStickerModel!)
-                
-                let iosb:GaussianBlur = GaussianBlur.init()
-                iosb.blurRadiusInPixels = 20
-                showGaussianFitler.addTarget(iosb)
+            
+            //XXXXX TODO 这里不是最好的方案,应该使用group filter 目前能确保mStickers每一位: 0是背景视频,1,用户头像,2,用户名
+            if isEndMovie {
+                //头像
+                let avatarFilter = PQImageFilter(sticker: mStickers![1], isExport: true)
+                //用户名
+                let userNameFitler = PQTextFilter(sticker: mStickers![2])
+                currentTarget.addTarget(showFitler!, atTargetIndex: 0)
+                showFitler?.addTarget(avatarFilter,atTargetIndex: 0)
+                avatarFilter.addTarget(userNameFitler,atTargetIndex: 0)
+                userNameFitler.addTarget(output!, atTargetIndex: 0)
+            }else{
                 
-                currentTarget.addTarget(showGaussianFitler, atTargetIndex: 0)
+                //是否添加水印
+                var weatMaskFilter:PQBaseFilter?
+                if(isAddWatermark){
+                    //创建水印filter
+                    let  weatMaskSticker:PQEditVisionTrackMaterialsModel = PQEditVisionTrackMaterialsModel.init()
+                    weatMaskSticker.locationPath = "watermark"
+                    weatMaskSticker.timelineIn = mStickers?.first?.timelineIn ?? 0.0
+                    weatMaskSticker.timelineOut = mStickers?.last?.timelineOut ?? 0.0
+                    /*
+                    1080P        最短的一边 ≥1080    100%     距右40,距下30
+                    720P         最短的一边 ≥720,<1080    66.60%    距右27,距下20
+                    480P及以下    最短的一边<720    44.40%    距右18,距下14
+                    */
+                    let postion:PQEditMaterialPositionModel = PQEditMaterialPositionModel.init()
+                    let minSlider = min(input?.mShowVidoSize.width ?? 0, input?.mShowVidoSize.height ?? 0)
+                    postion.width = 350
+                    postion.height = 100
+                    if(minSlider >= 1080){
+                        postion.x = Int(input?.mShowVidoSize.width ?? 0) - 40 - postion.width
+                        postion.y = Int(input?.mShowVidoSize.height ?? 0) - 30 - postion.height
+                    }else if(minSlider >= 720 && minSlider < 1080){
+                        postion.width = Int(Float(postion.width) * 0.666)
+                        postion.height = Int(Float(postion.height) * 0.666)
+                        postion.x = Int(input?.mShowVidoSize.width ?? 0) - 27 - postion.width
+                        postion.y = Int(input?.mShowVidoSize.height ?? 0) - 20 - postion.height
+                    
+                    }else if(minSlider < 720){
+                        postion.width = Int(Float(postion.width) * 0.444)
+                        postion.height = Int(Float(postion.height) * 0.444)
+                        postion.x = Int(input?.mShowVidoSize.width ?? 0) - 18 - postion.width
+                        postion.y = Int(input?.mShowVidoSize.height ?? 0) - 14 - postion.height
+                        
+                    }
+                    weatMaskSticker.materialPosition = postion
+                   
+                    weatMaskFilter = PQImageFilter(sticker: weatMaskSticker, isExport: true)
+                    
+                }
                 
-                iosb.addTarget(showFitler!)
+                if(currentSticker?.type == StickerType.IMAGE.rawValue && showGaussianBlur){
+                    //高斯层
+                    let json = currentSticker?.toJSONString(prettyPrint: false)
+                    if json == nil {
+                        BFLog(message: "数据转换有问题 跳转")
+                        return
+                    }
+                    let blurStickerModel: PQEditVisionTrackMaterialsModel? = Mapper<PQEditVisionTrackMaterialsModel>().map(JSONString: json!)
+                    blurStickerModel?.canvasFillType = stickerContentMode.aspectFillStr.rawValue
+                    let showGaussianFitler:PQBaseFilter = PQImageFilter(sticker: blurStickerModel!)
+                    
+                    let iosb:GaussianBlur = GaussianBlur.init()
+                    iosb.blurRadiusInPixels = 20
+                    showGaussianFitler.addTarget(iosb)
+                    
+                    currentTarget.addTarget(showGaussianFitler, atTargetIndex: 0)
+                    
+                    iosb.addTarget(showFitler!)
+                    
+                    if(weatMaskFilter != nil){
+                        showFitler?.addTarget(weatMaskFilter!, atTargetIndex: 0)
+                        weatMaskFilter?.addTarget(output!,atTargetIndex: 0)
+                    }else{
+                        showFitler?.addTarget(output!, atTargetIndex: 0)
+                    }
+                  
+                }else{
+                    if(weatMaskFilter != nil){
+                        
+                        currentTarget.addTarget(showFitler!, atTargetIndex: 0)
+                        showFitler?.addTarget(weatMaskFilter!, atTargetIndex: 0)
+                        weatMaskFilter?.addTarget(output!, atTargetIndex: 0)
+                    }else{
+                        currentTarget.addTarget(showFitler!, atTargetIndex: 0)
+                        showFitler?.addTarget(output!, atTargetIndex: 0)
+                    }
      
-                showFitler?.addTarget(output!, atTargetIndex: 0)
-            }else{
-                currentTarget.addTarget(showFitler!, atTargetIndex: 0)
-                showFitler?.addTarget(output!, atTargetIndex: 0)
-
+                }
             }
+         
 
             lastshowSticker = currentSticker
         }

+ 251 - 5
BFFramework/Classes/Stuckpoint/Controller/PQStuckPointPublicController.swift

@@ -68,6 +68,20 @@ class PQStuckPointPublicController: PQBaseViewController {
     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 {
@@ -571,6 +585,7 @@ class PQStuckPointPublicController: PQBaseViewController {
 
         // 取推荐标题
         getTitles()
+      
     }
 
     override func viewWillAppear(_ animated: Bool) {
@@ -607,12 +622,20 @@ class PQStuckPointPublicController: PQBaseViewController {
     }
 
     deinit {
+        BFLog(message: "发布界面析构 1")
         view.endEditing(true)
         PQNotification.removeObserver(self)
         // 取消导出
         if exporter != nil {
             exporter.cancel()
         }
+        if watermarkMovieExporter != nil{
+            watermarkMovieExporter.cancel()
+        }
+        if endMovieExporter != nil{
+            endMovieExporter.cancel()
+        }
+ 
         avPlayer.pause()
         avPlayer.replaceCurrentItem(with: nil)
         // 点击上报:返回按钮
@@ -758,7 +781,10 @@ extension PQStuckPointPublicController {
             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: "合成失败请重试。")
             }
@@ -804,7 +830,7 @@ extension PQStuckPointPublicController {
             BFLog(message: "开始导出")
         }
         exporter.progressClosure = { [weak self] _, _, progress in
-            BFLog(message: "合成进度 \(progress)")
+            BFLog(message: "正片合成进度 \(progress)")
             let useProgress = progress > 1 ? 1 : progress
             if progress > 0, Int(useProgress * 100) > (self?.exportProgrss ?? 0) {
                 // 更新进度
@@ -812,7 +838,7 @@ extension PQStuckPointPublicController {
             }
         }
         exporter.completion = { [weak self] url in
-            BFLog(message: "MovieOutput total frames appended:导了完成: \(url) 生成视频时长为:\(CMTimeGetSeconds(AVAsset(url: url).duration))")
+            BFLog(message: "无水印的视频导出完成: \(url) 生成视频时长为:\(CMTimeGetSeconds(AVAsset(url: url).duration))")
 
             // 导出完成后取消导出
             if self?.exporter != nil {
@@ -860,11 +886,16 @@ extension PQStuckPointPublicController {
     /// - Parameter localPath: localPath description
     /// - Returns: <#description#>
     func saveStuckPointVideo() {
+        
+        if(saveMovieLocalURL == nil){
+            BFLog(message: "保存相册的视频导出地址无效!!!")
+            return
+        }
         let authStatus = PHPhotoLibrary.authorizationStatus()
         if authStatus == .authorized {
             let photoLibrary = PHPhotoLibrary.shared()
             photoLibrary.performChanges({ [weak self] in
-                PHAssetChangeRequest.creationRequestForAssetFromVideo(atFileURL: (self?.exportLocalURL)!)
+                PHAssetChangeRequest.creationRequestForAssetFromVideo(atFileURL: (self?.saveMovieLocalURL)!)
             }) { [weak self] isFinished, _ in
                 DispatchQueue.main.async { [weak self] in
                     if self?.view != nil {
@@ -1194,8 +1225,7 @@ extension PQStuckPointPublicController {
             cShowHUB(superView: nil, msg: "视频发布失败,请重新合成")
         } else {
             bottomOprationBgView.isHidden = false
-            /// fp2-1-1 - 请求权限
-            authorizationStatus()
+         
         }
     }
 
@@ -1526,3 +1556,219 @@ extension PQStuckPointPublicController {
         }
     }
 }
+
+// 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 = { [weak self] _, _, progress in
+            BFLog(message: "带水印的合成进度 \(progress) ")
+          
+        }
+        watermarkMovieExporter.completion = { [weak self] url in
+            BFLog(message: "有水印的视频导出完成: \(url) 生成视频时长为:\(CMTimeGetSeconds(AVAsset(url: url).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 = Bundle().BF_mainbundle().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")
+        coverImageView.kf.setImage(with: URL(string: BFLoginUserInfo.shared.avatarUrl), placeholder: UIImage().BF_Image(named: "user_avatar_normal"), progressBlock: { _, _ in
+
+        }) { [weak self] result in
+            switch result {
+            case let .failure(failure):
+                BFLog(message: "图片请求失败:\(failure.localizedDescription)")
+            case let .success(imageResult):
+             
+                
+                let image = UIImage.nx_circleImage(imageResult.image)
+                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
+            
+                //头像绘制大小
+                var avatarSize = Int(360 * (self?.editProjectModel?.sData?.videoMetaData?.videoWidth ?? 0) / 1080)
+                //头像到顶部的高度
+                var avatarTop =   Int(430 * (self?.editProjectModel?.sData?.videoMetaData?.videoHeight ?? 0) / 1920)
+                if((self?.editProjectModel?.sData?.videoMetaData?.videoHeight ?? 0) <= (self?.editProjectModel?.sData?.videoMetaData?.videoWidth ?? 0)){//横屏
+                    avatarSize = 300
+                    avatarTop =  130
+                }
+                let avatarPostion:PQEditMaterialPositionModel = PQEditMaterialPositionModel.init()
+                avatarPostion.width = avatarSize
+                avatarPostion.height = avatarSize
+                avatarPostion.x = ((self?.editProjectModel?.sData?.videoMetaData?.videoWidth ?? 0) - avatarSize) / 2
+                avatarPostion.y = 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 =  Int(870 * (self?.editProjectModel?.sData?.videoMetaData?.videoHeight ?? 0) / 1920)
+                var userNameFontSize = Int(100 * (self?.editProjectModel?.sData?.videoMetaData?.videoWidth ?? 0) / 1080)
+                if((self?.editProjectModel?.sData?.videoMetaData?.videoHeight ?? 0) <= (self?.editProjectModel?.sData?.videoMetaData?.videoWidth ?? 0)){
+                    userNameTop = 480
+                    userNameFontSize = 70
+                }
+                let subtitleInfo:PQEditSubtitleInfoModel = PQEditSubtitleInfoModel.init()
+                subtitleInfo.fontSize = 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 = userNameTop
+                userNameSticker.materialPosition = userNamePostion
+
+                //4,音频
+                let soundResPath = Bundle().BF_mainbundle().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) ?? false {
+           
+                    self?.endMovieExporter.start(playeTimeRange: CMTimeRange.init(start: CMTime.zero, duration: CMTimeMakeWithSeconds(Float64(bgMovieInfo.out), preferredTimescale: BASE_FILTER_TIMESCALE)))
+                    BFLog(message: "开始导出")
+                }
+                self?.endMovieExporter.progressClosure = { [weak self] _, _, progress in
+                    BFLog(message: "片尾合成进度 \(progress) ")
+                  
+                }
+                self?.endMovieExporter.completion = { [weak self] url in
+                    BFLog(message: "片尾的视频导出完成: \(url) 生成视频时长为:\(CMTimeGetSeconds(AVAsset(url: url).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()
+                            }
+                        }
+                    }
+              
+ 
+                }
+
+            }
+        }
+       
+    }
+    
+    
+
+}