美文网首页
Swift 录制视频实例

Swift 录制视频实例

作者: 大成小栈 | 来源:发表于2024-04-05 15:32 被阅读0次

    Swift 中,使用 AVCaptureSession + AVCaptureMovieFileOutput 录制视频实例:

    import Photos
    import PhotosUI
    import AVFoundation
    
    class VideoRecorder: NSObject {
        
        // 结束回调
        typealias VideoRecorderCallback = (_ didFinish: Bool, _ videoUrl: URL, _ progress: CGFloat) -> Void
        // 翻转回调
        typealias SwapCallback = ((_ position: AVCaptureDevice.Position) -> Void)
        // 会话错误回调
        typealias ErrorCallback = () -> Void
        
        // 录制过程回调
        var recordAction: VideoRecorderCallback?
        // 录制过程出错
        var errorAction: ErrorCallback?
        // 翻转回调
        var swapAction: SwapCallback?
        
        // 视频捕获会话,协调input和output间的数据传输
        let captureSession = AVCaptureSession()
        // 将捕获到的视频输出到文件
        let fileOut = AVCaptureMovieFileOutput()
        // 视频输入
        private var videoInput: AVCaptureDeviceInput?
        // 预览视图
        lazy var previewLayer: AVCaptureVideoPreviewLayer = .init(session: captureSession)
        // 串行队列
        private let serialQueue = DispatchQueue(label: "VideoRecorderQueue")
        // 记录当前设备是横是竖
        var orientation: OrientationDetector.ScreenOrientation = .portrait
        // 记录当前摄像头是前是后
        private var position: AVCaptureDevice.Position = .front
        
        // 视频输入设备,前后摄像头
        var camera: AVCaptureDevice?
        // 录制计时
        private var timer: Timer?
        // 文件存储位置url
        private var fileUrl: URL = VideoRecorder.newVideoUrl()
        // 超时后自动停止
        let limitDuration: CGFloat = 59.9
        // 设置帧率
        var frameDuration = CMTime(value: 1, timescale: 25)
        // 设置码率
        var bitRate: Int = 2000 * 1024
        // 摄像头正在翻转
        var isSwapping: Bool = false
        
        // 是否正在录制
        var isRecording: Bool = false
        
        // MARK: - Life Cycle
        
        deinit {
            sessionStopRunning()
            NotificationCenter.default.removeObserver(self)
            print("Running ☠️ \(Self.self) 💀 deinit")
        }
        
        override init() {
            super.init()
            clearOldFile()
            initCaptureSession()
            sessionStartRunning()
        }
        
        private func clearOldFile() {
            if FileManager.default.fileExists(atPath: fileUrl.path) {
                try? FileManager.default.removeItem(at: fileUrl)
            }
        }
        
        private func initCaptureSession() {
            guard let newCamera = getCamera(with: position) else {
                errorAction?()
                return
            }
            camera = newCamera
            serialQueue.async { [weak self] in
                guard let self else { return }
                captureSession.beginConfiguration()
                configureSessionPreset(for: newCamera)
                sessionAddOutput()
                sessionAddInput(for: newCamera)
                captureSession.commitConfiguration()
            }
        }
        
        private func configureSessionPreset(for device: AVCaptureDevice) {
            if device.supportsSessionPreset(.hd1280x720) {
                captureSession.sessionPreset = .hd1280x720
            } else {
                captureSession.sessionPreset = .high
            }
        }
        
        private func sessionRemoveInputs() {
            if let allInputs = captureSession.inputs as? [AVCaptureDeviceInput] {
                for input in allInputs {
                    captureSession.removeInput(input)
                }
            }
        }
        
        private func sessionAddOutput() {
            // 视频输出文件
            addOutput(fileOut)
            // 输出视频的码率
            setBitRate(fileOut: fileOut)
        }
        
        private func sessionAddInput(for camera: AVCaptureDevice) {
            // 加音频设备
            if let audioDevice = AVCaptureDevice.default(for: .audio) {
                addInput(for: audioDevice)
            }
            // 加摄像头
            configCamera(camera)
            addInput(for: camera)
            setupFileOutConnection()
        }
        
        private func setupFileOutConnection() {
            if let connection = fileOut.connection(with: .video) {
                switch orientation {
                case .landscapeLeft:
                    connection.videoOrientation = .landscapeRight
                case .landscapeRight:
                    connection.videoOrientation = .landscapeLeft
                case .portraitUpsideDown:
                    connection.videoOrientation = .portraitUpsideDown
                default:
                    connection.videoOrientation = .portrait
                }
            }
        }
        
        private func addInput(for device: AVCaptureDevice) {
            do {
                let input = try AVCaptureDeviceInput(device: device)
                if captureSession.canAddInput(input) {
                    captureSession.addInput(input)
                    // 更新全局变量
                    videoInput = input
                } else {
                    errorAction?()
                }
            } catch {
                errorAction?()
            }
        }
        
        private func addOutput(_ output: AVCaptureOutput) {
            if captureSession.canAddOutput(output) {
                captureSession.addOutput(output)
            } else {
                errorAction?()
            }
        }
        
        private func configCamera(_ camera: AVCaptureDevice) {
            do {
                try camera.lockForConfiguration()
                camera.activeVideoMinFrameDuration = frameDuration
                camera.activeVideoMaxFrameDuration = frameDuration
                if camera.isSmoothAutoFocusSupported {
                    camera.isSmoothAutoFocusEnabled = true
                }
                if camera.isFocusPointOfInterestSupported && camera.isFocusModeSupported(.continuousAutoFocus) {
                    camera.focusMode = .continuousAutoFocus
                }
                camera.unlockForConfiguration()
            } catch {
                errorAction?()
            }
        }
        
        private func setBitRate(fileOut: AVCaptureMovieFileOutput) {
            if let connection = fileOut.connection(with: .video) {
                let compressionSettings: [String: Any] = [AVVideoAverageBitRateKey: bitRate]
                let codecSettings: [String: Any] = [AVVideoCodecKey: AVVideoCodecType.h264,
                                                    AVVideoCompressionPropertiesKey: compressionSettings]
                fileOut.setOutputSettings(codecSettings, for: connection)
            }
        }
        
        // MARK: - swap Camera
        
        private func getCamera(with position: AVCaptureDevice.Position) -> AVCaptureDevice? {
            let discoverySession = AVCaptureDevice.DiscoverySession(deviceTypes: [.builtInWideAngleCamera],
                                                                      mediaType: .video,
                                                                       position: .unspecified)
            for item in discoverySession.devices where item.position == position {
                return item
            }
            return nil
        }
        
        private func transAnimate() {
            let transition = CATransition()
            transition.duration = 0.4
            transition.delegate = self
            transition.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeInEaseOut)
            transition.type = CATransitionType(rawValue: "flip")
            if camera?.position == .front {
                transition.subtype = .fromLeft
            } else {
                transition.subtype = .fromRight
            }
            previewLayer.add(transition, forKey: "changeCamera")
        }
        
        func swapCamera(callback: ((_ position: AVCaptureDevice.Position) -> Void)?) {
            guard !isSwapping else { return }
            
            isSwapping = true
            captureSession.stopRunning()
            swapAction = callback
            
            serialQueue.sync { [weak self] in
                guard let self else { return }
                captureSession.beginConfiguration()
                sessionRemoveInputs()
                let toPosition: AVCaptureDevice.Position = position == .back ? .front : .back
                if let newCamera = getCamera(with: toPosition) {
                    camera = newCamera
                    sessionRemoveInputs()
                    sessionAddInput(for: newCamera)
                    position = toPosition
                }
                captureSession.commitConfiguration()
            }
            
            transAnimate()
        }
        
        // MARK: - flash Light
        
        func setFlash(callback: ((_ torchMode: AVCaptureDevice.TorchMode) -> Void)?) {
            guard let camera = getCamera(with: .back) else { return }
            do {
                try camera.lockForConfiguration()
                if camera.torchMode == AVCaptureDevice.TorchMode.off {
                    camera.torchMode = AVCaptureDevice.TorchMode.on
                    callback?(.on)
                } else {
                    camera.torchMode = AVCaptureDevice.TorchMode.off
                    callback?(.off)
                }
                camera.unlockForConfiguration()
            } catch let error as NSError {
                print("setFlash Error: \(error)")
            }
        }
        
        // MARK: - timer
        
        func resumeTimer() {
            cancelTimer()
            timer = Timer.scheduledTimer(withTimeInterval: 0.1, repeats: true) { [weak self] _ in
                guard let self = self else { return }
                let duration: CGFloat = fileOut.recordedDuration.seconds
                if duration >= limitDuration {
                    stopRecording()
                } else {
                    recordAction?(false, fileUrl, duration / limitDuration)
                }
            }
        }
    
        func cancelTimer() {
            timer?.invalidate()
            timer = nil
        }
        
        // MARK: - Actions
        
        func startRecording() {
            if !isRecording, !isSwapping {
                isRecording = true
                setupFileOutConnection()
                if !captureSession.isRunning {
                    sessionStartRunning()
                }
                serialQueue.async { [weak self] in
                    guard let self = self else { return }
                    fileOut.startRecording(to: fileUrl, recordingDelegate: self)
                }
                resumeTimer()
            }
        }
    
        func stopRecording() {
            if isRecording, !isSwapping {
                isRecording = false
                cancelTimer()
                serialQueue.async { [weak self] in
                    guard let self = self else { return }
                    fileOut.stopRecording()
                }
            }
        }
    
        func sessionStartRunning() {
            serialQueue.async { [weak self] in
                guard let self else { return }
                if !captureSession.isRunning {
                    captureSession.startRunning()
                }
            }
        }
    
        func sessionStopRunning() {
            stopRecording()
            captureSession.stopRunning()
        }
        
    }
    
    // MARK: - CAAnimationDelegate
    extension VideoRecorder: CAAnimationDelegate {
        
        func animationDidStart(_ anim: CAAnimation) {
            sessionStartRunning()
        }
        
        func animationDidStop(_ anim: CAAnimation, finished flag: Bool) {
            isSwapping = false
            if let position = videoInput?.device.position {
                swapAction?(position)
            }
        }
    }
    
    // MARK: - AVCaptureFileOutputRecordingDelegate
    
    extension VideoRecorder: AVCaptureFileOutputRecordingDelegate {
        
        func fileOutput(_ output: AVCaptureFileOutput, didStartRecordingTo fileURL: URL, from connections: [AVCaptureConnection]) {
            recordAction?(false, fileURL, 0.0)
        }
        
        func fileOutput(_ output: AVCaptureFileOutput, didFinishRecordingTo outputFileURL: URL, from connections: [AVCaptureConnection], error: Error?) {
            let duration: CGFloat = fileOut.recordedDuration.seconds
            if position == .front {
                HXFFmpegUtil.flipVideo(withInputUrl: outputFileURL, outputUrl: VideoRecorder.newVideoUrl()) { [weak self] success, url in
                    guard let self else { return }
                    if success, let url {
                        recordAction?(true, url, duration / limitDuration)
                        clearOldFile()
                    } else {
                        recordAction?(true, outputFileURL, duration / limitDuration)
                    }
                }
            } else {
                recordAction?(true, outputFileURL, duration / limitDuration)
            }
        }
        
        static func newVideoUrl() -> URL {
            do {
                return try AppDatabase.shared.folder(for: .newDidVideo).appendingPathComponent("video_\(UUID().uuidString).mp4")
            } catch {
                HXLogger.info("createDirectory error")
            }
            return URL(fileURLWithPath: "\(NSHomeDirectory())/video_\(UUID().uuidString).mp4")
        }
    }
    
    ///// test
    //extension VideoRecorder {
    //    
    //    func saveVideoToAlbum() {
    //        PHPhotoLibrary.shared().performChanges({ [weak self] in
    //            guard let self else { return }
    //            PHAssetChangeRequest.creationRequestForAssetFromVideo(atFileURL: url)
    //        }) { (success, err) in
    //            if success {
    //                HXLogger.info(">>>>> save video success")
    //            }
    //        }
    //        
    //    }
    //    
    //}
    

    其中,前置摄像头时可通过fileOut.connection来设置输出视频的镜像形式,但是所得结果总是跟我的期望不一致。索性就不主动设置了,直接使用HXFFmpegUtil.flipVideo翻转,完美解决。

    相关文章

      网友评论

          本文标题:Swift 录制视频实例

          本文链接:https://www.haomeiwen.com/subject/spvutjtx.html