Преобразование CMSampleBuffer в .mov во время трансляции с ReplayKit
Aug 19 2020
Подскажите пожалуйста, что может быть не так с моим кодом. Я пытаюсь транслировать свой экран в файл (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 AntonDanilov Aug 27 2020 at 07:00
Этот код у меня работает, но помимо моего каталога в группах приложений мне пришлось использовать 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
}
}