使用ReplayKit流式传输时从CMSampleBuffer保存视频

Dmi*_*yoN 5 video ios swift avkit replaykit

我正在将应用程序的内容流式传输到RTMP服务器,并使用RPBroadcastSampleHandler。

方法之一是

override func processSampleBuffer(_ sampleBuffer: CMSampleBuffer, with sampleBufferType: RPSampleBufferType) {
    switch sampleBufferType {
    case .video:
        streamer.appendSampleBuffer(sampleBuffer, withType: .video)
        captureOutput(sampleBuffer)
    case .audioApp:
        streamer.appendSampleBuffer(sampleBuffer, withType: .audio)
        captureAudioOutput(sampleBuffer)
    case .audioMic:
        ()
    }
}
Run Code Online (Sandbox Code Playgroud)

而captureOutput方法是

self.lastSampleTime = CMSampleBufferGetPresentationTimeStamp(sampleBuffer);

    // Append the sampleBuffer into videoWriterInput
    if self.isRecordingVideo {
        if self.videoWriterInput!.isReadyForMoreMediaData {
            if self.videoWriter!.status == AVAssetWriterStatus.writing {
                let whetherAppendSampleBuffer = self.videoWriterInput!.append(sampleBuffer)
                print(">>>>>>>>>>>>>The time::: \(self.lastSampleTime.value)/\(self.lastSampleTime.timescale)")
                if whetherAppendSampleBuffer {
                    print("DEBUG::: Append sample buffer successfully")
                } else {
                    print("WARN::: Append sample buffer failed")
                }
            } else {
                print("WARN:::The videoWriter status is not writing")
            }
        } else {
            print("WARN:::Cannot append sample buffer into videoWriterInput")
        }
    }
Run Code Online (Sandbox Code Playgroud)

由于此示例缓冲区包含音频/视频数据,因此我认为可以在流式传输时使用AVKit将其保存在本地。所以我要做的是在流的开头创建一个资产编写器:

    let fileManager = FileManager.default
    let documentsPath = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)[0]
    self.videoOutputFullFileName = documentsPath.stringByAppendingPathComponent(str: "test_capture_video.mp4")

    if self.videoOutputFullFileName == nil {
        print("ERROR:The video output file name is nil")
        return
    }

    self.isRecordingVideo = true
    if fileManager.fileExists(atPath: self.videoOutputFullFileName!) {
        print("WARN:::The file: \(self.videoOutputFullFileName!) exists, will delete the existing file")
        do {
            try fileManager.removeItem(atPath: self.videoOutputFullFileName!)
        } catch let error as NSError {
            print("WARN:::Cannot delete existing file: \(self.videoOutputFullFileName!), error: \(error.debugDescription)")
        }

    } else {
        print("DEBUG:::The file \(self.videoOutputFullFileName!) doesn't exist")
    }

    let screen = UIScreen.main
    let screenBounds = info.size
    let videoCompressionPropertys = [
        AVVideoAverageBitRateKey: screenBounds.width * screenBounds.height * 10.1
    ]

    let videoSettings: [String: Any] = [
        AVVideoCodecKey: AVVideoCodecH264,
        AVVideoWidthKey: screenBounds.width,
        AVVideoHeightKey: screenBounds.height,
        AVVideoCompressionPropertiesKey: videoCompressionPropertys
    ]

    self.videoWriterInput = AVAssetWriterInput(mediaType: AVMediaTypeVideo, outputSettings: videoSettings)

    guard let videoWriterInput = self.videoWriterInput else {
        print("ERROR:::No video writer input")
        return
    }

    videoWriterInput.expectsMediaDataInRealTime = true

    // Add the audio input
    var acl = AudioChannelLayout()
    memset(&acl, 0, MemoryLayout<AudioChannelLayout>.size)
    acl.mChannelLayoutTag = kAudioChannelLayoutTag_Mono;
    let audioOutputSettings: [String: Any] =
        [ AVFormatIDKey: kAudioFormatMPEG4AAC,
          AVSampleRateKey : 44100,
          AVNumberOfChannelsKey : 1,
          AVEncoderBitRateKey : 64000,
          AVChannelLayoutKey : Data(bytes: &acl, count: MemoryLayout<AudioChannelLayout>.size)]

    audioWriterInput = AVAssetWriterInput(mediaType: AVMediaTypeAudio, outputSettings: audioOutputSettings)

    guard let audioWriterInput = self.audioWriterInput else {
        print("ERROR:::No audio writer input")
        return
    }

    audioWriterInput.expectsMediaDataInRealTime = true

    do {
        self.videoWriter = try AVAssetWriter(outputURL: URL(fileURLWithPath: self.videoOutputFullFileName!), fileType: AVFileTypeMPEG4)
    } catch let error as NSError {
        print("ERROR:::::>>>>>>>>>>>>>Cannot init videoWriter, error:\(error.localizedDescription)")
    }

    guard let videoWriter = self.videoWriter else {
        print("ERROR:::No video writer")
        return
    }

    if videoWriter.canAdd(videoWriterInput) {
        videoWriter.add(videoWriterInput)
    } else {
        print("ERROR:::Cannot add videoWriterInput into videoWriter")
    }

    //Add audio input
    if videoWriter.canAdd(audioWriterInput) {
        videoWriter.add(audioWriterInput)
    } else {
        print("ERROR:::Cannot add audioWriterInput into videoWriter")
    }

    if videoWriter.status != AVAssetWriterStatus.writing {
        print("DEBUG::::::::::::::::The videoWriter status is not writing, and will start writing the video.")

        let hasStartedWriting = videoWriter.startWriting()
        if hasStartedWriting {
            videoWriter.startSession(atSourceTime: self.lastSampleTime)
            print("DEBUG:::Have started writting on videoWriter, session at source time: \(self.lastSampleTime)")
            LOG(videoWriter.status.rawValue)
        } else {
            print("WARN:::Fail to start writing on videoWriter")
        }
    } else {
        print("WARN:::The videoWriter.status is writing now, so cannot start writing action on videoWriter")
    }
Run Code Online (Sandbox Code Playgroud)

然后在流的末尾保存并完成编写:

    print("DEBUG::: Starting to process recorder final...")
    print("DEBUG::: videoWriter status: \(self.videoWriter!.status.rawValue)")
    self.isRecordingVideo = false

    guard let videoWriterInput = self.videoWriterInput else {
        print("ERROR:::No video writer input")
        return
    }
    guard let videoWriter = self.videoWriter else {
        print("ERROR:::No video writer")
        return
    }

    guard let audioWriterInput = self.audioWriterInput else {
        print("ERROR:::No audio writer input")
        return
    }

    videoWriterInput.markAsFinished()
    audioWriterInput.markAsFinished()
    videoWriter.finishWriting {
        if videoWriter.status == AVAssetWriterStatus.completed {
            print("DEBUG:::The videoWriter status is completed")

            let fileManager = FileManager.default
            if fileManager.fileExists(atPath: self.videoOutputFullFileName!) {
                print("DEBUG:::The file: \(self.videoOutputFullFileName ?? "") has been saved in documents folder, and is ready to be moved to camera roll")


                let sharedFileURL = fileManager.containerURL(forSecurityApplicationGroupIdentifier: "group.jp.awalker.co.Hotter")
                guard let documentsPath = sharedFileURL?.path else {
                    LOG("ERROR:::No shared file URL path")
                    return
                }
                let finalFilename = documentsPath.stringByAppendingPathComponent(str: "test_capture_video.mp4")

                //Check whether file exists
                if fileManager.fileExists(atPath: finalFilename) {
                    print("WARN:::The file: \(finalFilename) exists, will delete the existing file")
                    do {
                        try fileManager.removeItem(atPath: finalFilename)
                    } catch let error as NSError {
                        print("WARN:::Cannot delete existing file: \(finalFilename), error: \(error.debugDescription)")
                    }
                } else {
                    print("DEBUG:::The file \(self.videoOutputFullFileName!) doesn't exist")
                }

                do {
                    try fileManager.copyItem(at: URL(fileURLWithPath: self.videoOutputFullFileName!), to: URL(fileURLWithPath: finalFilename))
                }
                catch let error as NSError {
                    LOG("ERROR:::\(error.debugDescription)")
                }

                PHPhotoLibrary.shared().performChanges({
                    PHAssetChangeRequest.creationRequestForAssetFromVideo(atFileURL: URL(fileURLWithPath: finalFilename))
                }) { completed, error in
                    if completed {
                        print("Video \(self.videoOutputFullFileName ?? "") has been moved to camera roll")
                    }

                    if error != nil {
                        print ("ERROR:::Cannot move the video \(self.videoOutputFullFileName ?? "") to camera roll, error: \(error!.localizedDescription)")
                    }
                }

            } else {
                print("ERROR:::The file: \(self.videoOutputFullFileName ?? "") doesn't exist, so can't move this file camera roll")
            }
        } else {
            print("WARN:::The videoWriter status is not completed, stauts: \(videoWriter.status)")
        }
    }
Run Code Online (Sandbox Code Playgroud)

我遇到的问题是,从未达到finishWriting完成代码。写入器处于“写入”状态,因此未保存视频文件。

如果删除“ finishWriting”行并仅使完成代码运行,则表示正在保存文件,但未正确完成文件,当我尝试查看该文件时,该文件无法播放,因为它可能缺少元数据。

还有其他方法吗?我不想真正开始使用AVKit进行捕捉以保存记录,因为它占用了过多的CPU,并且RPBroadcastSampleHandler的CMSampleBuffer已经具有视频数据,但是也许在这里完全使用AVKit是错误的举动吗?

我应该改变什么?如何从该CMSampleBuffer保存视频?

Mar*_*rty 4

来自https://developer.apple.com/documentation/avfoundation/avassetwriter/1390432-finishwritingwithcompletionhandl

This method returns immediately and causes its work to be performed asynchronously

返回时broadcastFinished,您的扩展将被终止。我能够使其工作的唯一方法是阻止该方法返回,直到视频处理完成。我不确定这是否是正确的方法(看起来很奇怪),但它有效。像这样的东西:

        var finishedWriting = false
        videoWriter.finishWriting {
            NSLog("DEBUG:::The videoWriter finished writing.")
            if videoWriter.status == .completed {
                NSLog("DEBUG:::The videoWriter status is completed")

                let fileManager = FileManager.default
                if fileManager.fileExists(atPath: self.videoOutputFullFileName!) {
                    NSLog("DEBUG:::The file: \(self.videoOutputFullFileName ?? "") has been saved in documents folder, and is ready to be moved to camera roll")

                    let sharedFileURL = fileManager.containerURL(forSecurityApplicationGroupIdentifier: "group.xxx.com")
                    guard let documentsPath = sharedFileURL?.path else {
                        NSLog("ERROR:::No shared file URL path")
                        finishedWriting = true
                        return
                    }
                    let finalFilename = documentsPath + "/test_capture_video.mp4"

                    //Check whether file exists
                    if fileManager.fileExists(atPath: finalFilename) {
                        NSLog("WARN:::The file: \(finalFilename) exists, will delete the existing file")
                        do {
                            try fileManager.removeItem(atPath: finalFilename)
                        } catch let error as NSError {
                            NSLog("WARN:::Cannot delete existing file: \(finalFilename), error: \(error.debugDescription)")
                        }
                    } else {
                        NSLog("DEBUG:::The file \(self.videoOutputFullFileName!) doesn't exist")
                    }

                    do {
                        try fileManager.copyItem(at: URL(fileURLWithPath: self.videoOutputFullFileName!), to: URL(fileURLWithPath: finalFilename))
                    }
                    catch let error as NSError {
                        NSLog("ERROR:::\(error.debugDescription)")
                    }

                    PHPhotoLibrary.shared().performChanges({
                        PHAssetCollectionChangeRequest.creationRequestForAssetCollection(withTitle: "xxx")
                        PHAssetChangeRequest.creationRequestForAssetFromVideo(atFileURL: URL(fileURLWithPath: finalFilename))
                    }) { completed, error in
                        if completed {
                            NSLog("Video \(self.videoOutputFullFileName ?? "") has been moved to camera roll")
                        }

                        if error != nil {
                            NSLog("ERROR:::Cannot move the video \(self.videoOutputFullFileName ?? "") to camera roll, error: \(error!.localizedDescription)")
                        }

                        finishedWriting = true
                    }

                } else {
                    NSLog("ERROR:::The file: \(self.videoOutputFullFileName ?? "") doesn't exist, so can't move this file camera roll")
                    finishedWriting = true
                }
            } else {
                NSLog("WARN:::The videoWriter status is not completed, status: \(videoWriter.status)")
                finishedWriting = true
            }
        }

        while finishedWriting == false {
    //          NSLog("DEBUG:::Waiting to finish writing...")
        }
Run Code Online (Sandbox Code Playgroud)

我想你也必须extensionContext.completeRequest在某个时候打电话,但我的工作正常,没有耸肩