Пользовательский класс AVVideoCompositing не работает должным образом
Я пытаюсь применить CIFilter к AVAsset, а затем сохраните его с применением фильтра. Способ, которым я это делаю, заключается в использовании AVAssetExportSession
с videoComposition
для объекта AVMutableVideoComposition
с пользовательским классом AVVideoCompositing
.
Я также устанавливаю instructions
моего объекта AVMutableVideoComposition
в пользовательскую композиционную инструкцию класс (соответствует AVMutableVideoCompositionInstruction
). Этот класс передается идентификатором дорожки, а также несколькими другими несущественными переменными.
К сожалению, у меня возникла проблема - startVideoCompositionRequest:
в моем пользовательском классе композитора видео (в соответствии с AVVideoCompositing
) не вызывается правильно.
Когда я устанавливаю переменную passthroughTrackID
моего настраиваемого класса команды в идентификатор дорожки, функция startVideoCompositionRequest(request)
в моей AVVideoCompositing
не вызывается.
Тем не менее, когда я не устанавливаю переменную passthroughTrackID
в свой класс пользовательских команд, startVideoCompositionRequest(request)
вызывается, но не правильно - печатает request.sourceTrackIDs
приводит к пустому массиву, а request.sourceFrameByTrackID(trackID)
приводит к значению nil.
Что-то интересное, что я обнаружил, заключается в том, что функция cancelAllPendingVideoCompositionRequests:
всегда вызывается дважды при попытке экспортировать видео с фильтрами. Он либо вызывается один раз перед startVideoCompositionRequest:
, либо один раз или два раза подряд в случае, когда startVideoCompositionRequest:
не вызывается.
Я создал три класса для экспорта видео с фильтрами. Здесь класс утилиты, который в основном включает в себя функцию export
и вызывает весь требуемый код
class VideoFilterExport{
let asset: AVAsset
init(asset: AVAsset){
self.asset = asset
}
func export(toURL url: NSURL, callback: (url: NSURL?) -> Void){
guard let track: AVAssetTrack = self.asset.tracksWithMediaType(AVMediaTypeVideo).first else{callback(url: nil); return}
let composition = AVMutableComposition()
let compositionTrack = composition.addMutableTrackWithMediaType(AVMediaTypeVideo, preferredTrackID: kCMPersistentTrackID_Invalid)
do{
try compositionTrack.insertTimeRange(track.timeRange, ofTrack: track, atTime: kCMTimeZero)
}
catch _{callback(url: nil); return}
let videoComposition = AVMutableVideoComposition(propertiesOfAsset: composition)
videoComposition.customVideoCompositorClass = VideoFilterCompositor.self
videoComposition.frameDuration = CMTimeMake(1, 30)
videoComposition.renderSize = compositionTrack.naturalSize
let instruction = VideoFilterCompositionInstruction(trackID: compositionTrack.trackID)
instruction.timeRange = CMTimeRangeMake(kCMTimeZero, self.asset.duration)
videoComposition.instructions = [instruction]
let session: AVAssetExportSession = AVAssetExportSession(asset: composition, presetName: AVAssetExportPresetMediumQuality)!
session.videoComposition = videoComposition
session.outputURL = url
session.outputFileType = AVFileTypeMPEG4
session.exportAsynchronouslyWithCompletionHandler(){
callback(url: url)
}
}
}
Здесь два других класса - я помещу их в один блок кода, чтобы сделать этот пост короче
// Video Filter Composition Instruction Class - from what I gather,
// AVVideoCompositionInstruction is used only to pass values to
// the AVVideoCompositing class
class VideoFilterCompositionInstruction : AVMutableVideoCompositionInstruction{
let trackID: CMPersistentTrackID
let filters: ImageFilterGroup
let context: CIContext
// When I leave this line as-is, startVideoCompositionRequest: isn't called.
// When commented out, startVideoCompositionRequest(request) is called, but there
// are no valid CVPixelBuffers provided by request.sourceFrameByTrackID(below value)
override var passthroughTrackID: CMPersistentTrackID{get{return self.trackID}}
override var requiredSourceTrackIDs: [NSValue]{get{return []}}
override var containsTweening: Bool{get{return false}}
init(trackID: CMPersistentTrackID, filters: ImageFilterGroup, context: CIContext){
self.trackID = trackID
self.filters = filters
self.context = context
super.init()
//self.timeRange = timeRange
self.enablePostProcessing = true
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
// My custom AVVideoCompositing class. This is where the problem lies -
// although I don't know if this is the root of the problem
class VideoFilterCompositor : NSObject, AVVideoCompositing{
var requiredPixelBufferAttributesForRenderContext: [String : AnyObject] = [
kCVPixelBufferPixelFormatTypeKey as String : NSNumber(unsignedInt: kCVPixelFormatType_32BGRA), // The video is in 32 BGRA
kCVPixelBufferOpenGLESCompatibilityKey as String : NSNumber(bool: true),
kCVPixelBufferOpenGLCompatibilityKey as String : NSNumber(bool: true)
]
var sourcePixelBufferAttributes: [String : AnyObject]? = [
kCVPixelBufferPixelFormatTypeKey as String : NSNumber(unsignedInt: kCVPixelFormatType_32BGRA),
kCVPixelBufferOpenGLESCompatibilityKey as String : NSNumber(bool: true),
kCVPixelBufferOpenGLCompatibilityKey as String : NSNumber(bool: true)
]
let renderQueue = dispatch_queue_create("co.getblix.videofiltercompositor.renderingqueue", DISPATCH_QUEUE_SERIAL)
override init(){
super.init()
}
func startVideoCompositionRequest(request: AVAsynchronousVideoCompositionRequest){
// This code block is never executed when the
// passthroughTrackID variable is in the above class
autoreleasepool(){
dispatch_async(self.renderQueue){
guard let instruction = request.videoCompositionInstruction as? VideoFilterCompositionInstruction else{
request.finishWithError(NSError(domain: "getblix.co", code: 760, userInfo: nil))
return
}
guard let pixels = request.sourceFrameByTrackID(instruction.passthroughTrackID) else{
// This code block is executed when I comment out the
// passthroughTrackID variable in the above class
request.finishWithError(NSError(domain: "getblix.co", code: 761, userInfo: nil))
return
}
// I have not been able to get the code to reach this point
// This function is either not called, or the guard
// statement above executes
let image = CIImage(CVPixelBuffer: pixels)
let filtered: CIImage = //apply the filter here
let width = CVPixelBufferGetWidth(pixels)
let height = CVPixelBufferGetHeight(pixels)
let format = CVPixelBufferGetPixelFormatType(pixels)
var newBuffer: CVPixelBuffer?
CVPixelBufferCreate(kCFAllocatorDefault, width, height, format, nil, &newBuffer)
if let buffer = newBuffer{
instruction.context.render(filtered, toCVPixelBuffer: buffer)
request.finishWithComposedVideoFrame(buffer)
}
else{
request.finishWithComposedVideoFrame(pixels)
}
}
}
}
func renderContextChanged(newRenderContext: AVVideoCompositionRenderContext){
// I don't have any code in this block
}
// This is interesting - this is called twice,
// Once before startVideoCompositionRequest is called,
// And once after. In the case when startVideoCompositionRequest
// Is not called, this is simply called twice in a row
func cancelAllPendingVideoCompositionRequests(){
dispatch_barrier_async(self.renderQueue){
print("Cancelled")
}
}
}
Я смотрел образец проекта Apple AVCustomEdit для руководства с этим, но я не могу найти в нем никаких причин, почему это происходит.
Как я могу получить функцию request.sourceFrameByTrackID:
для правильного вызова и предоставить допустимый CVPixelBuffer
для каждого кадра?
Ответы
Ответ 1
Оказывается, переменная requiredSourceTrackIDs
в пользовательском AVVideoCompositionInstruction
class (VideoFilterCompositionInstruction
в вопросе) должен быть установлен в массив, содержащий идентификаторы дорожки
override var requiredSourceTrackIDs: [NSValue]{
get{
return [
NSNumber(value: Int(self.trackID))
]
}
}
Таким образом, конечный класс команд пользовательской композиции
class VideoFilterCompositionInstruction : AVMutableVideoCompositionInstruction{
let trackID: CMPersistentTrackID
let filters: [CIFilter]
let context: CIContext
override var passthroughTrackID: CMPersistentTrackID{get{return self.trackID}}
override var requiredSourceTrackIDs: [NSValue]{get{return [NSNumber(value: Int(self.trackID))]}}
override var containsTweening: Bool{get{return false}}
init(trackID: CMPersistentTrackID, filters: [CIFilter], context: CIContext){
self.trackID = trackID
self.filters = filters
self.context = context
super.init()
self.enablePostProcessing = true
}
required init?(coder aDecoder: NSCoder){
fatalError("init(coder:) has not been implemented")
}
}
Весь код этой утилиты также находится в GitHub
Ответ 2
Как вы уже отметили, если passthroughTrackID
вернуть дорожку, которую вы хотите фильтровать, это не правильный подход - вам нужно вернуть трек, который будет отфильтрован от requiredSourceTrackIDs
. (И похоже, как только вы это сделаете, неважно, верните ли вы его из passthroughTrackID
.) Чтобы ответить на оставшийся вопрос о том, почему он работает таким образом...
Документы для passthroughTrackID
и requiredSourceTrackIDs
, безусловно, не являются явным написанием Apple. (Изложите ошибку об этом, и они могут улучшиться.) Но если вы внимательно присмотритесь к описанию первого, появится подсказка (выделено мной)...
Если на время выполнения команды результат композиции видео является одним из исходных кадров, это свойство возвращает соответствующий идентификатор дорожки. Композитор не будет запускаться в течение продолжительности команды, и вместо этого используется правильный исходный кадр.
Таким образом, вы используете passthroughTrackID
только тогда, когда вы создаете класс команд, который пропускает один трек без обработки.
Если вы планируете выполнять любую обработку изображений, даже если это только один трек без композиции, укажите этот трек в requiredSourceTrackIDs
.