使用 ReplayKit 广播时将 CMSampleBuffer 转换为 .mov

Posted

技术标签:

【中文标题】使用 ReplayKit 广播时将 CMSampleBuffer 转换为 .mov【英文标题】:Convert CMSampleBuffer to .mov while broadcasting with ReplayKit 【发布时间】:2020-08-19 20:04:50 【问题描述】:

请告诉我我的代码有什么问题。我尝试将我的屏幕广播到文件(newFile.mov),然后将它与一些数据一起发送到服务器,但它没有保存,停止捕获后,文件钢为空。

这是我来自 BExtensionUpload 的代码,它捕获广播并将其保存到我获取它并发送到服务器的组中的文件中

import ReplayKit
class SampleHandler: RPBroadcastSampleHandler 
    let appIdentifier =         "com.group.CY"
    var videoWriterInput:       AVAssetWriterInput!
    var audioWriterInput:       AVAssetWriterInput!
    var videoWriter:            AVAssetWriter!
    var sessionAtSourceTime:    CMTime!
    var outputFileLocation:     URL?
    
    override func broadcastStarted(withSetupInfo setupInfo: [String : NSObject]?) 
        do 
            outputFileLocation = videoFileLocation()
            videoWriter = try AVAssetWriter(outputURL: outputFileLocation!, fileType: AVFileType.mov)
            
            videoWriterInput = AVAssetWriterInput(mediaType: AVMediaType.video, outputSettings: [
                AVVideoCodecKey : AVVideoCodecType.h264,
                AVVideoWidthKey : 720,
                AVVideoHeightKey : 1280,
                AVVideoCompressionPropertiesKey : [
                    AVVideoAverageBitRateKey : 2300000,
                ],
            ])
            
            videoWriterInput.expectsMediaDataInRealTime = true
            
            if videoWriter.canAdd(videoWriterInput) 
                videoWriter.add(videoWriterInput)
             else 
                print("no input added")
            
            
            audioWriterInput = AVAssetWriterInput(mediaType: AVMediaType.audio, outputSettings: nil)
            audioWriterInput.expectsMediaDataInRealTime = true
            
            if videoWriter.canAdd(audioWriterInput!) 
                videoWriter.add(audioWriterInput!)
            
            
            videoWriter.startWriting()
         catch let error 
            debugPrint(error.localizedDescription)
        
    
    
    override func broadcastFinished() 
        super.broadcastFinished()
        
        videoWriterInput.markAsFinished()
        videoWriter.finishWriting 
            ()
        
    
    
    override func processSampleBuffer(_ sampleBuffer: CMSampleBuffer, with sampleBufferType: RPSampleBufferType) 
        super.processSampleBuffer(sampleBuffer, with: sampleBufferType)            
        if writable,
            sessionAtSourceTime == nil 
            sessionAtSourceTime = CMSampleBufferGetPresentationTimeStamp(sampleBuffer)
            videoWriter.startSession(atSourceTime: sessionAtSourceTime!)
        
        
        switch sampleBufferType 
        case .video:
            if videoWriterInput.isReadyForMoreMediaData 
                videoWriterInput.append(sampleBuffer)
            
        case .audioApp:
            if audioWriterInput.isReadyForMoreMediaData 
                audioWriterInput?.append(sampleBuffer)
            
        case .audioMic:
            print("mic")
        @unknown default:
            print("unknown")
        
    
    
    func videoFileLocation() -> URL? 
        let fileManager = FileManager.default
        
        do 
            if let container = fileManager.containerURL(forSecurityApplicationGroupIdentifier: appIdentifier) 
                let videoContainer = container.appendingPathComponent("Video")
                try? fileManager.createDirectory(at: videoContainer, withIntermediateDirectories: false, attributes: nil)
                
                let videoOutputUrl = videoContainer.appendingPathComponent("newFile").appendingPathExtension("mov")
                
                if fileManager.fileExists(atPath: videoOutputUrl.path) 
                    try fileManager.removeItem(at: videoOutputUrl)
                
                
                fileManager.createFile(atPath: videoOutputUrl.path, contents: nil, attributes: nil)
                
                return videoOutputUrl
            
            
         catch 
            print(error)
        
        
        return nil
    

【问题讨论】:

【参考方案1】:

此代码适用于我,但除了我在应用程序组中的目录之外,我还必须使用 phphotoLibrary。应用程序组目录不是创建文件的唯一方法,您可以使用任何您想要的目录。我希望它可以帮助某人。

import ReplayKit
import Photos

class SampleHandler: RPBroadcastSampleHandler 
    
    let appIdentifier = "group.CY"
    let fileManager = FileManager.default
    
    var videoWriterInput: AVAssetWriterInput!
    var microphoneWriterInput: AVAssetWriterInput!
    
    var videoWriter: AVAssetWriter!
    
    var sessionBeginAtSourceTime: CMTime!
    
    var isRecording = false
    
    var outputFileLocation: URL!
    
    override func broadcastStarted(withSetupInfo setupInfo: [String : NSObject]?) 
        guard !isRecording else  return 
        isRecording = true
        sessionBeginAtSourceTime = nil
        setUpWriter()
    
    
    func setUpWriter() 
        let width = UIScreen.main.bounds.width * 2
        let height = UIScreen.main.bounds.height * 2
        
        self.outputFileLocation = videoFileLocation()
        
        // Add the video input
        videoWriter = try? AVAssetWriter.init(outputURL: self.outputFileLocation, fileType: AVFileType.mp4)
        let videoCompressionPropertys = [
            AVVideoAverageBitRateKey: width * height * 10.1
        ]
        
        let videoSettings: [String: Any] = [
            AVVideoCodecKey: AVVideoCodecType.h264,
            AVVideoWidthKey: width,
            AVVideoHeightKey: height,
            AVVideoCompressionPropertiesKey: videoCompressionPropertys
        ]
        
        videoWriterInput = AVAssetWriterInput(mediaType: AVMediaType.video, outputSettings: videoSettings)
        videoWriterInput.expectsMediaDataInRealTime = true
        
        // Add the microphone 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)]
        
        microphoneWriterInput = AVAssetWriterInput(mediaType: AVMediaType.audio, outputSettings: audioOutputSettings)
        microphoneWriterInput.expectsMediaDataInRealTime = true
        
        if videoWriter.canAdd(videoWriterInput) 
            videoWriter.add(videoWriterInput)
        
        
        if videoWriter.canAdd(microphoneWriterInput) 
            videoWriter.add(microphoneWriterInput)
        
        
        videoWriter.startWriting()
    
    
    override func broadcastFinished() 
        guard isRecording else  return 
        isRecording = false
        sessionBeginAtSourceTime = nil
        
        let dispatchGroup = DispatchGroup()
        dispatchGroup.enter()
        
        videoWriterInput.markAsFinished()
        microphoneWriterInput.markAsFinished()
        
        var finishedWriting = false
        videoWriter.finishWriting 
            PHPhotoLibrary.shared().performChanges(
                PHAssetCollectionChangeRequest.creationRequestForAssetCollection(withTitle: "xxx")
                
                PHAssetChangeRequest.creationRequestForAssetFromVideo(atFileURL: self.outputFileLocation)
            )  completed, error in
                if completed 
                    NSLog("Video \(self.outputFileLocation.path ?? "") has been moved to camera roll")
                
                
                if error != nil 
                    NSLog("ERROR:::Cannot move the video \(self.outputFileLocation.path ?? "") to camera roll, error: \(error!.localizedDescription)")
                
                
                finishedWriting = true
            
            
            while finishedWriting == false 
                //          NSLog("DEBUG:::Waiting to finish writing...")
            
            
            dispatchGroup.leave()
        
        
        dispatchGroup.wait() // <= blocks the thread here
    
    
    override func finishBroadcastWithError(_ error: Error) 
        let e = error
        print(e)
    
    
    override func processSampleBuffer(_ sampleBuffer: CMSampleBuffer, with sampleBufferType: RPSampleBufferType) 
        let writable = canWrite()
        
        if writable, sessionBeginAtSourceTime == nil 
            sessionBeginAtSourceTime = CMSampleBufferGetPresentationTimeStamp(sampleBuffer)
            videoWriter.startSession(atSourceTime: sessionBeginAtSourceTime!)
        
        
        if writable 
            switch sampleBufferType 
            case .video:
                if videoWriterInput.isReadyForMoreMediaData 
                    videoWriterInput.append(sampleBuffer)
                
            case .audioApp:
                print("audio")
            case .audioMic:
                if microphoneWriterInput.isReadyForMoreMediaData 
                    microphoneWriterInput.append(sampleBuffer)
                
            @unknown default:
                print("unknown")
            
        
    
    
    func videoFileLocation() -> URL 
        let documentsPath = fileManager.containerURL(forSecurityApplicationGroupIdentifier: appIdentifier)!
        let videoOutputUrl = documentsPath
            .appendingPathComponent("Library")
            .appendingPathComponent("Caches")
            .appendingPathComponent("mobile")
            .appendingPathExtension("mp4")
        do 
            if fileManager.fileExists(atPath: videoOutputUrl.path) 
                try fileManager.removeItem(at: videoOutputUrl)
            
         catch  print(error) 
        
        return videoOutputUrl
    
    
    func canWrite() -> Bool 
        return videoWriter != nil && isRecording && videoWriter?.status == .writing
    

【讨论】:

以上是关于使用 ReplayKit 广播时将 CMSampleBuffer 转换为 .mov的主要内容,如果未能解决你的问题,请参考以下文章

使用 webrtc 时 ReplayKit 不起作用

如何使用应用程序按钮将ReplayKit视频保存到相机胶卷

ReplayKit库,iOS原生直播神器

iOS ReplayKit 屏幕共享,屏幕直播实现

iOS ReplayKit 屏幕共享,屏幕直播实现

iOS ReplayKit 屏幕共享,屏幕直播实现