【iOS】视频录像相关功能调研(二)

@[toc]

1、获取视频时长(秒数)

    //MARK: 获取视频时长(秒数)
    @objc func getVideoLength() {
        let paths = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true )
        let documentsDirectory = paths[0]  as  String
        let videoPath = documentsDirectory + "/" + "1619172935.mp4"
        
        if !FileManager.default.fileExists(atPath: videoPath) {
            print("文件不存在,请先拍照,再修改视频地址")
            return
        }
        
        let avUrlAsset = AVURLAsset.init(url: URL(fileURLWithPath: videoPath))
        let cmtime = avUrlAsset.duration
        let second = Int(cmtime.seconds)
        
        print("视频秒数 == \(second)")
    }

2、获取视频文件大小

    //MARK: 获取视频文件大小//文件属性
    @objc func getVideoSize() {
        
        let paths = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true )
        let documentsDirectory = paths[0]  as  String
        let videoPath = documentsDirectory + "/" + "1619172935.mp4"

        if !FileManager.default.fileExists(atPath: videoPath) {
            print("文件不存在,请先拍照,再修改视频地址")
            return
        }
        
        let fileManager = FileManager.default
        if fileManager.fileExists(atPath: videoPath) {
            let fileDic = try! fileManager.attributesOfItem(atPath: videoPath)
            let size = fileDic[FileAttributeKey(rawValue: "NSFileSize")] as? Int ?? 0
            print("\(size)B")
            print("\(size/1024)KB")
            let sizeM = String(format: "%.2f", Float(size)/1024/1024)
            print(sizeM + "M")
            
        }else{
            print("文件不存在")
        }
    }

3、获取指定时间帧图片

    //MARK: 获取指定时间帧图片
    
    /// 获取指定时间帧图片
    /// - Parameters:
    ///   - videoUrl: 视频地址
    ///   - cmtime: 指定的时间
    ///   - width: 宽度 根据视频的宽高比来计算图片的高度
    @objc func getImage() {
        let paths = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true )
        let documentsDirectory = paths[0]  as  String
        let videoPath = documentsDirectory + "/" + "1619172935.mp4"
        
        if !FileManager.default.fileExists(atPath: videoPath) {
            print("文件不存在,请先拍照,再修改视频地址")
            return
        }
        
        let cmtime = CMTimeMake(value: 1, timescale: 10)
        let width = 300
        
        // 获取指定时间的帧图片
        DispatchQueue.global().async {
            //建立新的AVAsset & AVAssetImageGenerator
            let asset = AVAsset.init(url: URL(fileURLWithPath: videoPath))
            let imageGenerator = AVAssetImageGenerator.init(asset: asset)
            //设置maximumSize 宽为100,高为0 根据视频的宽高比来计算图片的高度 控制图片清晰度
            imageGenerator.maximumSize = CGSize(width: width, height: 0)
            //捕捉视频缩略图会考虑视频的变化(如视频的方向变化),如果不设置,缩略图的方向可能出错
            imageGenerator.appliesPreferredTrackTransform = true
            // CMTimeMake第一个参数是时间,第二个参数是 每秒的分数 第一个/第二个 才是秒
            let imageRef = try! imageGenerator.copyCGImage(at:cmtime, actualTime: nil)
            //将图片转化为UIImage
            let image = UIImage.init(cgImage: imageRef)
            DispatchQueue.main.async {
                //保存到相册
                self.saveImage(image: image)
            }
        }
    }

4、多视频合成


    @objc func starMerge() {
        let videoPaths = [
            "1619163716.mp4",
            "1619163734.mp4",
            "1619163741.mp4"
        ]
        
        for i in 0..<videoPaths.count {
            if !FileManager.default.fileExists(atPath: videoPaths[i]) {
                print("文件不存在,请先拍照,再修改视频地址")
                return
            }
        }
   
        let outputPath = self.getNewPath(videoTyle: AVFileType.mp4)
        self.mergeVideo(videoPaths: videoPaths, outputPath: outputPath) { (success) in
            if success {
                print("多视频合成 成功")
            }else{
                print("多视频合成 失败")
            }
        }
    }
    
    //MARK: 多视频合成
    func mergeVideo(videoPaths:[String], outputPath:String, completeHandler:@escaping (Bool)->()) {
        if videoPaths.count < 2 {
            return
        }
        
        let mixComposition = AVMutableComposition()
        //音频轨道
        let audioTrack = mixComposition.addMutableTrack(withMediaType: AVMediaType.audio, preferredTrackID: kCMPersistentTrackID_Invalid)
        
        //视频轨道
        let videoTrack = mixComposition.addMutableTrack(withMediaType: AVMediaType.video, preferredTrackID: kCMPersistentTrackID_Invalid)
        
        var totalDuration = CMTime.zero
        let paths = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true )
        let documentsDirectory = paths[0]  as  String
        
        for i in 0 ..< videoPaths.count {
            let pathUrl = documentsDirectory + "/\(videoPaths[i])"
            if !FileManager.default.fileExists(atPath: pathUrl) {
                print("文件不存在")
                break
            }
            
            let asset = AVURLAsset.init(url: URL(fileURLWithPath: pathUrl))
            // 获取AVAsset中的音频
            let assetAudioTracks = asset.tracks(withMediaType: AVMediaType.audio)
            if assetAudioTracks.count == 0 {
                print("未获取到音频")
                break
            }
            // 向通道内加入音频
            try! audioTrack?.insertTimeRange(CMTimeRangeMake(start: CMTime.zero, duration: asset.duration), of: assetAudioTracks.first!, at: totalDuration)
            
            // 获取AVAsset中的视频
            let assetVideoTracks = asset.tracks(withMediaType: AVMediaType.video)
            if assetVideoTracks.count == 0 {
                print("未获取到视频")
                break
            }
            // 向通道内加入视频
            try! videoTrack?.insertTimeRange(CMTimeRangeMake(start: CMTime.zero, duration: asset.duration), of: assetVideoTracks.first!, at: totalDuration)
            totalDuration = CMTimeAdd(totalDuration, asset.duration)
        }
        
        // 导出合成后的视频
        let outputURL = URL(fileURLWithPath: outputPath)
        let avAssetExportSession = AVAssetExportSession.init(asset: mixComposition, presetName: AVAssetExportPresetMediumQuality)
        avAssetExportSession?.outputURL = outputURL
        avAssetExportSession?.outputFileType = .mp4
        avAssetExportSession?.shouldOptimizeForNetworkUse = true
        avAssetExportSession?.exportAsynchronously {
            switch avAssetExportSession?.status {
            
            case .unknown:
                print("AVAssetExportSessionStatusUnknown")
                break
            case .waiting:
                print("AVAssetExportSessionStatusWaiting")
                break
            case .exporting:
                print("AVAssetExportSessionStatusExporting")
                break
            case .completed:
                print("AVAssetExportSessionStatusCompleted")
                self.getVideoSize(videoUrl: outputURL)
                self.getVideoLength(videoUrl: outputURL)
                completeHandler(true)
                break
            case .failed:
                print("AVAssetExportSessionStatusFailed")
                completeHandler(false)
                break
            case .cancelled:
                print("AVAssetExportSessionStatusCancelled")
                completeHandler(false)
                break

            default:
                break
            }
        }
    }

5、视频压缩/转码

    
    @objc func starConvert() {
        let paths = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true )
        let documentsDirectory = paths[0]  as  String
        let videoPath = documentsDirectory + "/" + "1619331826.mp4"
        
        
        if !FileManager.default.fileExists(atPath: videoPath) {
            print("文件不存在,请先拍照,再修改视频地址")
            return
        }

        let outputPath = self.getNewPath(videoTyle: AVFileType.mov)
        
        self.convertVideo(inputURL: URL(fileURLWithPath: videoPath),
                          outputURL: URL(fileURLWithPath: outputPath),
                          presetName: AVAssetExportPresetMediumQuality) { (success) in
            if success {
                print("压缩/转码 成功")
            }else{
                print("压缩/转码 失败")
            }
        }
    }

    //MARK: 视频压缩//转换格式
    
    /// 视频压缩//转换格式
    /// - Parameters:
    ///   - inputURL: 视频地址
    ///   - outputURL: 视频压缩后的地址
    ///   - presetName: 视频预设
    ///   - completeHandler: <#completeHandler description#>
    /// - Returns: <#description#>
    func convertVideo(inputURL:URL, outputURL:URL, presetName:String = AVAssetExportPresetMediumQuality, completeHandler:@escaping (Bool)->()) {
        let avAsset = AVURLAsset.init(url: inputURL)
        let avAssetExportSession = AVAssetExportSession.init(asset: avAsset, presetName: presetName)
        avAssetExportSession?.outputURL = outputURL
        avAssetExportSession?.outputFileType = .mp4
        avAssetExportSession?.shouldOptimizeForNetworkUse = true
        avAssetExportSession?.exportAsynchronously {
            switch avAssetExportSession?.status {
            
            case .unknown:
                print("AVAssetExportSessionStatusUnknown")
                break
            case .waiting:
                print("AVAssetExportSessionStatusWaiting")
                break
            case .exporting:
                print("AVAssetExportSessionStatusExporting")
                break
            case .completed:
                print("AVAssetExportSessionStatusCompleted")
                self.getVideoSize(videoUrl: outputURL)
                self.getVideoLength(videoUrl: outputURL)
                completeHandler(true)
                break
            case .failed:
                print("AVAssetExportSessionStatusFailed")
                completeHandler(false)
                break
            case .cancelled:
                print("AVAssetExportSessionStatusCancelled")
                completeHandler(false)
                break
            default:
                break
            }
        }
    }

6、添加水印(图片、文字)

    @objc func starAddImage() {
        let paths = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true )
        let documentsDirectory = paths[0]  as  String
        let videoPath = documentsDirectory + "/" + "1619343879.mp4"
        
        
        if !FileManager.default.fileExists(atPath: videoPath) {
            print("文件不存在,请先拍照,再修改视频地址")
            return
        }
        
        
        let outputPath = self.getNewPath(videoTyle: AVFileType.mp4)
        print(outputPath)

        self.videoAddMark(imageName: "good", title: nil, inputPath: videoPath, outputPath: outputPath) { (success) in
            if success {
                print("添加水印 成功")
            }else{
                print("添加水印 失败")
            }
        }
    }
    
    @objc func starAddTitle() {
        let paths = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true )
        let documentsDirectory = paths[0]  as  String
        let videoPath = documentsDirectory + "/" + "1619343879.mp4"
        
        
        if !FileManager.default.fileExists(atPath: videoPath) {
            print("文件不存在,请先拍照,再修改视频地址")
            return
        }
        
        
        let outputPath = self.getNewPath(videoTyle: AVFileType.mp4)
        print(outputPath)
        self.videoAddMark(imageName: nil, title: "啦啦啦", inputPath: videoPath, outputPath: outputPath) { (success) in
            if success {
                print("添加水印 成功")
            }else{
                print("添加水印 失败")
            }
        }
    }
    
    //添加水印
    func videoAddMark(imageName:String?, title:String?, inputPath:String, outputPath:String, completeHandler:@escaping (Bool)->()) {
        
        //创建AVAsset实例
        let videoAsset = AVURLAsset.init(url: URL(fileURLWithPath: inputPath))
        
        let mixComposition = AVMutableComposition()
        //音频轨道
        let audioTrack = mixComposition.addMutableTrack(withMediaType: AVMediaType.audio, preferredTrackID: kCMPersistentTrackID_Invalid)
        // 获取AVAsset中的音频
        let assetAudioTracks = videoAsset.tracks(withMediaType: AVMediaType.audio)
        if assetAudioTracks.count == 0 {
            print("未获取到音频")
            return
        }
        // 向通道内加入音频
        try! audioTrack?.insertTimeRange(CMTimeRangeMake(start: CMTime.zero, duration: videoAsset.duration), of: assetAudioTracks.first!, at: CMTime.zero)
        
        //视频轨道
        let videoTrack = mixComposition.addMutableTrack(withMediaType: AVMediaType.video, preferredTrackID: kCMPersistentTrackID_Invalid)
        // 获取AVAsset中的视频
        let assetVideoTracks = videoAsset.tracks(withMediaType: AVMediaType.video)
        if assetVideoTracks.count == 0 {
            print("未获取到视频")
            return
        }
        // 向通道内加入视频
        try! videoTrack?.insertTimeRange(CMTimeRangeMake(start: CMTime.zero, duration: videoAsset.duration), of: assetVideoTracks.first!, at: CMTime.zero)
        
        //1 AVMutableVideoCompositionInstruction 视频轨道中的一个视频,可以缩放、旋转等
        let mainInstruction = AVMutableVideoCompositionInstruction()
        mainInstruction.timeRange = CMTimeRangeMake(start: CMTime.zero, duration: videoAsset.duration)
        
        // 2 AVMutableVideoCompositionLayerInstruction 一个视频轨道,包含了这个轨道上的所有视频素材
        let videolayerInstruction = AVMutableVideoCompositionLayerInstruction.init(assetTrack: videoTrack!)

        let videoTransform = (assetVideoTracks.first?.preferredTransform)!
        var isVideoAssetPortrait = true
        var naturalSize = (assetVideoTracks.first?.naturalSize)!

        if videoTransform.a == 0,
           videoTransform.b == 1,
           videoTransform.c == -1,
           videoTransform.d == 0
           {
            isVideoAssetPortrait = true
            
        } else if videoTransform.a == 0,
                 videoTransform.b == -1,
                 videoTransform.c == 1,
                 videoTransform.d == 0 {
            isVideoAssetPortrait = true
        } else if videoTransform.a == 1,
                  videoTransform.b == 0,
                  videoTransform.c == 0,
                  videoTransform.d == 1 {
             isVideoAssetPortrait = false
         } else if videoTransform.a == -1,
                   videoTransform.b == 0,
                   videoTransform.c == 0,
                   videoTransform.d == -1 {
              isVideoAssetPortrait = false
          }
        videolayerInstruction.setTransform(videoTransform, at: CMTime.zero)
        
        // 3 - Add instructions
        mainInstruction.layerInstructions = [videolayerInstruction]

        //AVMutableVideoComposition:管理所有视频轨道,水印添加就在这上面
        let mainCompositionInst = AVMutableVideoComposition()
        
        if isVideoAssetPortrait {
            naturalSize = CGSize(width: naturalSize.height, height: naturalSize.width)
        }
        let width = naturalSize.width
        let height = naturalSize.height
        mainCompositionInst.renderSize = CGSize.init(width: width, height: height)
        mainCompositionInst.instructions = [mainInstruction]
        mainCompositionInst.frameDuration = CMTimeMake(value: 1, timescale: 30)
                
        if title == nil && (imageName == nil || UIImage(named: imageName!) == nil) {
            completeHandler(false)
            return
        }
        
        let overlayLayer = CALayer()

        if title != nil {
            // 文字
            let subtitle1Text = CATextLayer()
            subtitle1Text.font = "Helvetica-Bold" as CFTypeRef
            subtitle1Text.fontSize = 40
            subtitle1Text.frame = CGRect(x: width/2-100, y: height/2-60, width: 200, height: 120)
            subtitle1Text.string = title
            subtitle1Text.alignmentMode = .center
            subtitle1Text.foregroundColor = UIColor.white.cgColor
            overlayLayer.addSublayer(subtitle1Text)
        }
        if imageName != nil, let image = UIImage(named: imageName!) {
            //图片
            let picLayer = CALayer()
            picLayer.contents = image.cgImage
            picLayer.frame = CGRect(x: width/2-80, y: height/2-80, width: 160, height: 160)
            overlayLayer.addSublayer(picLayer)
        }
        
        overlayLayer.frame = CGRect(x: 0, y: 0, width: width, height: height)
        overlayLayer.masksToBounds = true
        
        let parentLayer = CALayer()
        let videoLayer = CALayer()
        parentLayer.frame = CGRect(x: 0, y: 0, width: width, height: height)
        videoLayer.frame = CGRect(x: 0, y: 0, width: width, height: height)
        parentLayer.addSublayer(videoLayer)
        parentLayer.addSublayer(overlayLayer)
        mainCompositionInst.animationTool = AVVideoCompositionCoreAnimationTool.init(postProcessingAsVideoLayer: videoLayer, in: parentLayer)
        
        // 导出合成后的视频
        let outputURL = URL(fileURLWithPath: outputPath)
        let avAssetExportSession = AVAssetExportSession.init(asset: mixComposition, presetName: AVAssetExportPresetMediumQuality)
        avAssetExportSession?.outputURL = outputURL
        avAssetExportSession?.outputFileType = .mp4
        avAssetExportSession?.shouldOptimizeForNetworkUse = true
        avAssetExportSession?.videoComposition = mainCompositionInst;

        avAssetExportSession?.exportAsynchronously {
            switch avAssetExportSession?.status {
            
            case .unknown:
                print("AVAssetExportSessionStatusUnknown")
                break
            case .waiting:
                print("AVAssetExportSessionStatusWaiting")
                break
            case .exporting:
                print("AVAssetExportSessionStatusExporting")
                break
            case .completed:
                print("AVAssetExportSessionStatusCompleted")
                self.getVideoSize(videoUrl: outputURL)
                self.getVideoLength(videoUrl: outputURL)
                completeHandler(true)
                break
            case .failed:
                print("AVAssetExportSessionStatusFailed")
                completeHandler(false)
                break
            case .cancelled:
                print("AVAssetExportSessionStatusCancelled")
                completeHandler(false)
                break
            default:
                break
            }
        }
    }

7、获取一个新的沙盒存储地址

    //MARK: 获取一个新的沙盒存储地址
    /// 获取一个新的沙盒存储地址
    /// - Returns: <#description#>
    func getNewPath(videoTyle: AVFileType) -> String {
        let paths = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true )
        let documentsDirectory = paths[0]  as  String
        let timeInterval = Int(Date().timeIntervalSince1970)
        var filePath = "\(documentsDirectory)/\(timeInterval)"

        switch videoTyle {
        case .mp4:
            filePath = filePath + ".mp4"
            break
        case .mov:
            filePath = filePath + ".mov"
            break
        default:
            filePath = filePath + ".mp4"
            break
        }
        
        return filePath
    }

8、根据路径删除沙盒中某个文件

    //MARK: 根据路径删除沙盒中某个文件
    func deleteFile(path:String) -> Bool {
        if FileManager.default.fileExists(atPath: path) {
            do {
                try FileManager.default.removeItem(atPath: path)
                return true
            } catch  {
                return false
            }
        }
        return false
    }

9、保存图片到相册

    //MARK: 保存图片到相册
    func saveImage(image: UIImage) {
        UIImageWriteToSavedPhotosAlbum(image, self, #selector(self.saveImage(image:didFinishSavingWithError:contextInfo:)), nil)
    }
    
    @objc private func saveImage(image: UIImage, didFinishSavingWithError error: NSError?, contextInfo: AnyObject) {
        var info = ""
        if error != nil{
            info = "保存图片失败"
        }else{
            info = "保存图片成功"
        }
        print(info)
    }

10、保存视频到相册

    //MARK:保存视频到相册
    func saveVideoToAlbum(videoUrl: URL) {
        var info = ""
        PHPhotoLibrary.shared().performChanges({
            PHAssetChangeRequest.creationRequestForAssetFromVideo(atFileURL: videoUrl)
        }) { (success, error) in
            if success {
                info = "保存成功"
            } else {
                info = "保存失败,err = \(error.debugDescription)"
            }
            
            DispatchQueue.main.async {
                let alertVC = UIAlertController(title: info, message: nil, preferredStyle: .alert)
                alertVC.addAction(UIAlertAction(title: "OK", style: .default, handler: nil))
                self.present(alertVC, animated: true, completion: nil)
            }
        }
    }
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 157,198评论 4 359
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,663评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 106,985评论 0 237
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,673评论 0 202
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 51,994评论 3 285
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,399评论 1 211
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,717评论 2 310
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,407评论 0 194
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,112评论 1 239
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,371评论 2 241
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,891评论 1 256
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,255评论 2 250
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,881评论 3 233
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,010评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,764评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,412评论 2 269
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,299评论 2 260

推荐阅读更多精彩内容