Сбой AVFoundation при экспорте видео с текстовым слоем
В свободное время я разрабатываю приложение для редактирования видео для iOS.
Я только что возобновил работу над ним после нескольких недель посещения других проектов, и - хотя я не внес никаких существенных изменений в код - теперь он вылетает каждый раз, когда я пытаюсь экспортировать свою композицию видео.
Я проверил и создал ту же самую фиксацию, которую я успешно загрузил в TestFlight тогда (и она работала на нескольких устройствах без сбоев), так что, возможно, это проблема с последним Xcode / iOS SDK, который я обновил с тех пор?
Код аварийно завершает работу в _xpc_api_misuse в потоке:
com.apple.coremedia.basicvideocompositor.output
Навигатор отладки:
Во время сбоя в навигаторе отладки имеется более 70 потоков, поэтому, возможно, что-то не так, и приложение использует слишком много потоков (никогда не видел такого количества).
Мое приложение накладывает "водяной знак" на экспортируемое видео с помощью текстового слоя. Поигравшись, я обнаружил, что сбой можно предотвратить, закомментировав код водяного знака:
guard let exporter = AVAssetExportSession(asset: composition, presetName: AVAssetExportPresetHighestQuality) else {
return failure(ProjectError.failedToCreateExportSession)
}
guard let documents = try? FileManager.default.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: true) else {
return failure(ProjectError.temporaryOutputDirectoryNotFound)
}
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "yyyy-MM-dd_HHmmss"
let fileName = dateFormatter.string(from: Date())
let fileExtension = "mov"
let fileURL = documents.appendingPathComponent(fileName).appendingPathExtension(fileExtension)
exporter.outputURL = fileURL
exporter.outputFileType = AVFileType.mov
exporter.shouldOptimizeForNetworkUse = true // check if needed
// OFFENDING BLOCK (commenting out averts crash)
if addWaterMark {
let frame = CGRect(origin: .zero, size: videoComposition.renderSize)
let watermark = WatermarkLayer(frame: frame)
let parentLayer = CALayer()
let videoLayer = CALayer()
parentLayer.frame = frame
videoLayer.frame = frame
parentLayer.addSublayer(videoLayer)
parentLayer.addSublayer(watermark)
videoComposition.animationTool = AVVideoCompositionCoreAnimationTool(postProcessingAsVideoLayer: videoLayer, in: parentLayer)
}
// END OF OFFENDING BLOCK
exporter.videoComposition = videoComposition
exporter.exportAsynchronously {
// etc.
Код для слоя водяного знака:
class WatermarkLayer: CATextLayer {
private let defaultFontSize: CGFloat = 48
private let rightMargin: CGFloat = 10
private let bottomMargin: CGFloat = 10
init(frame: CGRect) {
super.init()
guard let appName = Bundle.main.infoDictionary?["CFBundleName"] as? String else {
fatalError("!!!")
}
self.foregroundColor = CGColor.srgb(r: 255, g: 255, b: 255, a: 0.5)
self.backgroundColor = CGColor.clear
self.string = String(format: String.watermarkFormat, appName)
self.font = CTFontCreateWithName(String.watermarkFontName as CFString, defaultFontSize, nil)
self.fontSize = defaultFontSize
self.shadowOpacity = 0.75
self.alignmentMode = .right
self.frame = frame
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented. Use init(frame:) instead.")
}
override func draw(in ctx: CGContext) {
let height = self.bounds.size.height
let fontSize = self.fontSize
let yDiff = (height-fontSize) - fontSize/10 - bottomMargin // Bottom (minus margin)
ctx.saveGState()
ctx.translateBy(x: -rightMargin, y: yDiff)
super.draw(in: ctx)
ctx.restoreGState()
}
}
Есть идеи, что могло случиться?
Возможно, мой код делает что-то не так, что каким-то образом "прошло" в предыдущем SDK из-за какой-то ошибки Apple, которая была исправлена, или из-за "дыры" реализации, которая была заткнута?
ОБНОВЛЕНИЕ: я загрузил образец проекта Рэя Вендерлиха для редактирования видео и попытался добавить "субтитры" к видео (мне пришлось настроить слишком старый проект, чтобы он компилировался под Xcode 11).
О чудо, он точно так же вылетает.
ОБНОВЛЕНИЕ 2: теперь я попробовал устройство (iPhone 8 с последней версией iOS 13.5), и оно работает, без сбоев. Однако Симуляторы для iOS 13.5 дают сбой. Когда я изначально разместил вопрос (iOS 13.4?), Я уверен, что это было как сбоем на устройстве, так и на симуляторе.
Я загружаю симуляторы iOS 12.0, чтобы проверить, но до них еще несколько гигабайт...
4 ответа
У меня такая же проблема. Запускается после iOS 13.4 и отображается только в симуляторе (устройство работает нормально). Если я закомментируюparentLayer.addSublayer(videoLayer)
тогда приложение не падает, но экспортированное видео не является желаемым.
Это исправило это для меня в iOS 14.5:
public static var isSimulator: Bool {
#if targetEnvironment(simulator)
true
#else
false
#endif
}
// ...
let export = AVAssetExportSession(
asset: composition,
presetName: isSimulator ? AVAssetExportPresetPassthrough : AVAssetExportPresetHighestQuality
)
Встречаются те же проблемы, но только на симуляторе (Xcode 12.4 (12D4e)).
После некоторого исследования я обнаружил, что причиной сбоя является
AVVideoCompositionCoreAnimationTool
с
+videoCompositionCoreAnimationToolWithPostProcessingAsVideoLayer:inLayer:
И я исправил это, заменив его на один ниже (но нам нужно обработать
instruction.layerInstructions
таким образом ):
+videoCompositionCoreAnimationToolWithAdditionalLayer:asTrackID:
Ниже приведен пример кода, работающего как на реальном устройстве, так и на симуляторе (поскольку OP явно не помечал Swift, я просто скопирую здесь свой образец Objective-C):
...
// Prepare watermark layer
CALayer *watermarkLayer = ...;
CMPersistentTrackID watermarkLayerTrackID = [asset unusedTrackID];
// !!! NOTE#01: Use as additional layer here instead of animation layer.
videoComposition.animationTool = [AVVideoCompositionCoreAnimationTool videoCompositionCoreAnimationToolWithAdditionalLayer:watermarkLayer asTrackID:watermarkLayerTrackID];
// Create video composition instruction
AVMutableVideoCompositionInstruction *instruction = [AVMutableVideoCompositionInstruction videoCompositionInstruction];
instruction.timeRange = CMTimeRangeMake(kCMTimeZero, asset.duration);
// - Watermark layer instruction
// !!! NOTE#02: Make this instruction track watermark layer by the `trackID`.
AVMutableVideoCompositionLayerInstruction *watermarkLayerInstruction = [AVMutableVideoCompositionLayerInstruction videoCompositionLayerInstruction];
watermarkLayerInstruction.trackID = watermarkLayerTrackID;
// - Video track layer instruction
AVAssetTrack *videoTrack = [asset tracksWithMediaType:AVMediaTypeVideo].firstObject;
AVMutableVideoCompositionLayerInstruction *videoLayerInstruction = [AVMutableVideoCompositionLayerInstruction videoCompositionLayerInstructionWithAssetTrack:videoTrack];
// Watermark layer above video layer here.
instruction.layerInstructions = @[
watermarkLayerInstruction,
videoLayerInstruction,
];
videoComposition.instructions = @[instruction];
// Export the video w/ watermark.
AVAssetExportSession *exportSession = ...;
...
exportSession.videoComposition = videoComposition;
...
И, кстати, если вам просто нужно добавить изображение в качестве водяного знака, другое решение, используя
AVVideoComposition
с
-videoCompositionWithAsset:applyingCIFiltersWithHandler:
также хорошо работает как на реальном устройстве, так и на симуляторе, но я протестировал его и обнаружил, что он медленнее. Кажется, этот способ больше подходит для видео-блендера / фильтра.
#if targetEnvironment(simulator)
// Adding layers while export crashes on simulator as it expects opaque background.
#else
if let animationTool = getAnimationTool() {
videoComposition.animationTool = animationTool
}
#endif
ЗдесьgetAnimationTool()
вернется . Это может быть слой изображения или текстовый слой. Но должен вернутьсяAVVideoCompositionCoreAnimationTool
.