【问题标题】:Saving video from CMSampleBuffer while streaming using ReplayKit使用 ReplayKit 流式传输时从 CMSampleBuffer 保存视频
【发布时间】:2018-04-04 19:31:50
【问题描述】:

我正在将我的应用内容流式传输到我的 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 将其保存在本地。所以我正在做的是在流的开头创建一个资产编写器:

    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”行并让完成代码继续运行,则会保存一个文件,但未正确完成,当我尝试查看它时它无法播放,因为它可能缺少元数据。

还有其他方法可以做到这一点吗?我不想真正开始使用 AVKit 捕获来保存记录,因为它占用了太多的 CPU 并且 RPBroadcastSampleHandler 的 CMSampleBuffer 已经有视频数据,但也许在这里使用 AVKit 是一个错误的举动?

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

【问题讨论】:

  • 您还需要将屏幕尺寸乘以屏幕比例,以获得完整的质量。
  • 你是怎么解决这个问题的?我也被困在同一行 如果我删除 "finishWriting" 行 。我可以将文件保存在 doc.dir 中,但我无法播放该文件,也无法将该视频文件保存在手机的相机胶卷中。你能帮我吗??
  • @DmitryoN 为什么10.1[AVVideoAverageBitRateKey: screenBounds.width * screenBounds.height * 10.1]??
  • 感谢您的代码,我发现我无法直接在群组containerURL 上写信。首先将 videoWriter 输出到 Document 目录,然后,完成后,将视频文件复制(或移动)到共享 containerURL。我花了几个小时来理解错误,你让我很开心 DmitryoN
  • 至于10.1,我不记得那个数字了。今天我会因为在代码中写一个幻数而砍掉我的手,但我想当时还不错。此外,这是一个热门的研发,所以我并不关心代码是否漂亮,但这并不能成为我公开一个神奇数字的借口。

标签: ios swift video avkit replaykit


【解决方案1】:

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

我认为您有时还必须致电 extensionContext.completeRequest,但没有它我的工作正常耸耸肩

【讨论】:

  • 我正在尝试将录制的视频保存在相机胶卷中。但是,这是行不通的。我在该记录的 URL 中有内容。我已经通过转换为 Data 进行了检查。现在,我正在调用 PHPhotoLibrary.shared().performChanges({ ,然后是 Completion 处理程序,既不会触发成功也不会触发失败。你能帮我解决这个问题吗??
  • 这实际上应该使用 DispatchGroup 而不是 while 循环。您是否阻止该方法返回,直到完成处理程序到达?
  • 我用DispatchGroups 试过你的方法,效果很好!让我们用DispatchGroup 实现写另一个答案。
  • @Marty 你能帮帮我吗? stackoverflow.com/questions/59681554/…
【解决方案2】:

你可以试试这个:

override func broadcastFinished() {
    Log(#function)
    ...
    // Need to give the end CMTime, if not set, the video cannot be used
    videoWriter.endSession(atSourceTime: ...)
    videoWriter.finishWriting {
        // Callback cannot be executed here
    }
    ...
    // The program has been executed.
}

【讨论】:

    【解决方案3】:

    @Marty 的回答应该被接受,因为他指出了问题所在,并且它的DispatchGroup 解决方案完美运行。
    由于他使用了while循环并且没有描述如何使用DispatchGroups,所以这是我实现它的方式。

    override func broadcastFinished() {
        let dispatchGroup = DispatchGroup()
        dispatchGroup.enter()
        self.writerInput.markAsFinished()
        self.writer.finishWriting {
            // Do your work to here to make video available
            dispatchGroup.leave()
        }
        dispatchGroup.wait() // <= blocks the thread here
    }
    

    【讨论】:

      猜你喜欢
      • 2010-10-28
      • 2011-05-23
      • 2017-06-07
      • 1970-01-01
      • 2015-10-08
      • 1970-01-01
      • 2013-11-08
      • 2012-01-09
      相关资源
      最近更新 更多