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

标签 ios swift video 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:
        ()
    }
}

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")
        }
    }

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

    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")
    }

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

    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)")
        }
    }

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

如果我删除“finishWriting”行并让完成代码继续运行,则会保存一个文件,但没有正确完成,当我试图查看它时它无法播放,因为它可能缺少元数据。

还有其他方法吗?我不想真正开始使用 AVKit 进行捕获以保存录制内容,因为它占用了过多的 CPU,并且 RPBroadcastSampleHandler 的 CMSampleBuffer 已经有了视频数据,但在这里使用 AVKit 可能是一个错误的举动?

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

最佳答案

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

此方法立即返回并使其工作异步执行

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...")
        }

我认为您还必须在某些时候调用 extensionContext.completeRequest,但没有它我的工作正常耸耸肩

关于ios - 使用 ReplayKit 流式传输时从 CMSampleBuffer 保存视频,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/46903118/

相关文章:

ios - 无法在物理设备上构建 ios 应用程序

ios - 应用程序未上传到 iOS 应用程序连接商店,原因是非公共(public) API

ios - 如何将 AFIncrementalStore 映射到 Twitter API v1.1?

ios - 绘制完美的圆角线,Swift

ios - 通用类型

swift - 如果让 url = URL(string :"") 对于 .JPEG 图像总是失败

php - 如何在 laravel 代码中嵌入视频

ios - UISearchController 隐藏搜索栏

ios - 视频无法在 iphone 上播放

.net - 加入和切割 mpeg 2 视频文件