Сохранение видео из CMSampleBuffer при потоковой передаче с помощью 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, чтобы сохранить его локально во время потоковой передачи. Итак, что я делаю, так это создаю средство записи активов в начале потока:

    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 для сохранения записи, потому что это требует слишком много ресурсов ЦП, а CMSampleBuffer RPBroadcastSampleHandler уже имеет видеоданные, но, может быть, использование AVKit вообще неправильный ход?

Что мне изменить? Как мне сохранить видео из этого CMSampleBuffer?


person DmitryoN    schedule 24.10.2017    source источник
comment
Вы также захотите умножить размер экрана на масштаб экрана, чтобы получить полное качество.   -  person Marty    schedule 09.10.2018
comment
Как ты это решил? Я также застрял в той же строке Если я удалю строку finishWriting. Я могу сохранить файл в doc.dir, но я не могу воспроизвести этот файл, а также не могу сохранить этот видеофайл в фотопленке моего телефона. Вы можете мне помочь ??   -  person McDonal_11    schedule 06.02.2019
comment
@DmitryoN почему 10.1 в [AVVideoAverageBitRateKey: screenBounds.width * screenBounds.height * 10.1] ??   -  person Martin    schedule 13.06.2019
comment
Благодаря вашему коду я понял, что не могу напрямую писать в группе containerURL. Сначала выведите videoWriter в каталог документов, а затем, когда закончите, скопируйте (или переместите) видеофайл в общий containerURL. Я потратил часы на то, чтобы разобраться в ошибке, вы сделали мой день DmitryoN   -  person Martin    schedule 13.06.2019
comment
Что касается 10.1, я не помню этот номер. Я бы сегодня отрубил себе руки за то, что написал магическое число в коде, но, думаю, тогда это было нормально. Кроме того, это были горячие исследования и разработки, поэтому мне было наплевать, чтобы код был красивым, но это не извиняет меня за то, что я опубликовал магическое число.   -  person DmitryoN    schedule 14.06.2019
comment
@DmitryoN: Где вы реализовали метод captureOutput?   -  person Daxesh Nagar    schedule 01.07.2020


Ответы (3)


Из 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, но мой отлично работает без этого пожимать плечами.

person Marty    schedule 08.10.2018
comment
Я пытаюсь сохранить записанное видео в Camera Roll. Но это не работает. У меня есть контент в этом записанном URL. Я проверил это, преобразовав в Data. Теперь я вызываю PHPhotoLibrary.shared (). PerformChanges ({, затем обработчик завершения, ни успех, ни сбой не запускается. Вы можете мне помочь?) - person McDonal_11; 06.02.2019
comment
На самом деле здесь должен использоваться DispatchGroup вместо цикла while. Вы блокируете возврат метода до тех пор, пока не будет достигнут этот обработчик завершения? - person Marty; 07.02.2019
comment
Я попробовал ваш метод с DispatchGroups, и он отлично работает! Напишем еще один ответ с реализацией DispatchGroup. - person Martin; 13.06.2019
comment
@ Марти, не могли бы вы мне помочь? stackoverflow.com/questions/59681554/ - person Govind Rakholiya; 10.01.2020

Ответ @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
}
person Martin    schedule 13.06.2019

Вы можете попробовать это:

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.
}
person zuizhe    schedule 26.04.2019