Я следил за парой руководств по общему процессу создания видео в Swift из массива UIImages. Эти руководства работают, и я могу взять массив UIImages и вывести видео в Camera Roll. Отлично!
Однако я сталкиваюсь с ошибкой с этими методами как в существующем проекте React Native , так и в изобретенном ванильном приложении Swift, которое я создал специально для воссоздания ошибки .
let images: [UIImage] = [
UIImage(data:UIImage.init(named: "1")!.jpegData(compressionQuality: 1.0)!, scale:1.0)!,
UIImage(data:UIImage.init(named: "2")!.jpegData(compressionQuality: 1.0)!, scale:1.0)!,
UIImage(data:UIImage.init(named: "3")!.jpegData(compressionQuality: 1.0)!, scale:1.0)!,
UIImage(data:UIImage.init(named: "4")!.jpegData(compressionQuality: 1.0)!, scale:1.0)!,
UIImage(data:UIImage.init(named: "5")!.jpegData(compressionQuality: 1.0)!, scale:1.0)!,
UIImage(data:UIImage.init(named: "6")!.jpegData(compressionQuality: 1.0)!, scale:1.0)!,
UIImage(data:UIImage.init(named: "7")!.jpegData(compressionQuality: 1.0)!, scale:1.0)!,
]
// The image sizes are consistent. Use the first one to determine the video size. 3024 × 4032
let originalSize = images[0].size
// This fails. ❌
// let outputSize = CGSize(width: originalSize.width, height: originalSize.height)
// This fails. ❌
// let outputSize = CGSize(width: originalSize.width - 512, height: originalSize.height - 509)
// This fails. ❌
// let outputSize = CGSize(width: originalSize.width - 511, height: originalSize.height - 510)
// This works. ✅ We could go smaller and keep the aspect ratio, but the specific numbers
// for the cutoff seem worth noting here.
let outputSize = CGSize(width: originalSize.width - 512, height: originalSize.height - 510)
print("outputSize \(outputSize)")
self.videoWriter!.buildVideoFromImageArray(videoFilename: "output", images: images, size: outputSize) { (url: URL) in
self.saveVideoToAlbum(url) { (error: Error?) in
print("error saving to album \(String(describing: error))")
}
}
Если я использую исходное разрешение изображений в качестве выходного размера видео, видео будет повреждено. Видео не воспроизводится на устройствах Apple, и я получаю сообщение об ошибке при попытке сохранить видео в Фотопленку. Если я использую общий лист, у него нет элемента контекста Save Video
. Что-то в видео заставляет Apple сказать: «Выглядит неправильно».
Есть нет ошибок, возникающих во время генерации видео. У него (на вид) разумный размер файла и он "выглядит" нормально, но он почему-то сломан, и Apple это не нравится. Единственный значимый сигнал, который у меня есть, что что-то не удалось, приходит, когда я пытаюсь сохранить это видео в Camera Roll.
The operation couldn’t be completed. (PHPhotosErrorDomain error -1.)
error saving to album Optional(Error Domain=PHPhotosErrorDomain Code=-1 "(null)")
Эти смещения в приведенном выше коде 512
и 510
позволяют видео генерировать правильно. Если я использую их для обрезки / кадрирования выходного видео, то видео генерируется нормально (хотя и со странным разрешением) и без проблем сохраняется в Camera Roll. Если я уменьшу любое смещение на один пиксель, видео будет повреждено.
I подозреваю Я где-то достиг предельного размера буфера, но я не знаю, как это исправить.
Вот код VideoWriter
и пример приложения , демонстрирующий ошибку .
class VideoWriter {
let imagesPerSecond: TimeInterval = 2
func buildVideoFromImageArray(videoFilename: String, images: [UIImage], size: CGSize, completion: @escaping (URL) -> Void) {
var outputURL: URL {
let fileManager = FileManager.default
if let tmpDirURL = try? fileManager.url(for: .cachesDirectory, in: .userDomainMask, appropriateFor: nil, create: true) {
return tmpDirURL.appendingPathComponent(videoFilename).appendingPathExtension("mp4")
}
fatalError("URLForDirectory() failed")
}
do {
try FileManager.default.removeItem(atPath: outputURL.path)
} catch _ as NSError {
// Assume file didn't already exist.
}
self.animateImages(outputSize: size, outputURL: outputURL, images: images, completion: completion)
}
func animateImages(outputSize: CGSize, outputURL: URL, images: [UIImage], completion: @escaping (URL) -> Void) {
guard let videoWriter = try? AVAssetWriter(outputURL: outputURL, fileType: AVFileType.mp4) else {
fatalError("AVAssetWriter error")
}
let outputSettings = [
AVVideoCodecKey : AVVideoCodecType.h264,
AVVideoWidthKey : NSNumber(value: Float(outputSize.width)),
AVVideoHeightKey : NSNumber(value: Float(outputSize.height))
] as [String : Any]
guard videoWriter.canApply(outputSettings: outputSettings, forMediaType: AVMediaType.video) else {
fatalError("Negative : Can't apply the Output settings...")
}
let videoWriterInput = AVAssetWriterInput(mediaType: AVMediaType.video, outputSettings: outputSettings)
let pixelBufferAdaptor = AVAssetWriterInputPixelBufferAdaptor(
assetWriterInput: videoWriterInput,
sourcePixelBufferAttributes: [
kCVPixelBufferPixelFormatTypeKey as String : NSNumber(value: kCVPixelFormatType_32ARGB),
kCVPixelBufferWidthKey as String: NSNumber(value: Float(outputSize.width)),
kCVPixelBufferHeightKey as String: NSNumber(value: Float(outputSize.height))
]
)
if videoWriter.canAdd(videoWriterInput) {
videoWriter.add(videoWriterInput)
}
if videoWriter.startWriting() {
let zeroTime = CMTimeMake(value: Int64(imagesPerSecond),timescale: Int32(1))
videoWriter.startSession(atSourceTime: zeroTime)
assert(pixelBufferAdaptor.pixelBufferPool != nil)
let media_queue = DispatchQueue(label: "mediaInputQueue")
videoWriterInput.requestMediaDataWhenReady(on: media_queue, using: { () -> Void in
let fps: Int32 = 1
let framePerSecond: Int64 = Int64(self.imagesPerSecond)
let frameDuration = CMTimeMake(value: Int64(self.imagesPerSecond), timescale: fps)
var frameCount: Int64 = 0
var appendSucceeded = true
for image in images {
if (videoWriterInput.isReadyForMoreMediaData) {
let lastFrameTime = CMTimeMake(value: frameCount * framePerSecond, timescale: fps)
let presentationTime = frameCount == 0 ? lastFrameTime : CMTimeAdd(lastFrameTime, frameDuration)
// Ownership of this follows the "Create Rule" but that is auto-managed in Swift so we do not need to release.
var pixelBuffer: CVPixelBuffer? = nil
let status: CVReturn = CVPixelBufferPoolCreatePixelBuffer(kCFAllocatorDefault, pixelBufferAdaptor.pixelBufferPool!, &pixelBuffer)
// Validate that the pixelBuffer is not nil and the status is 0
if let pixelBuffer = pixelBuffer, status == 0 {
self.drawImage(pixelBuffer: pixelBuffer, outputSize: outputSize, image: image)
appendSucceeded = pixelBufferAdaptor.append(pixelBuffer, withPresentationTime: presentationTime)
} else {
print("Failed to allocate pixel buffer")
appendSucceeded = false
}
}
if !appendSucceeded {
print("Failed to append to pixel buffer!")
break
}
frameCount += 1
}
videoWriterInput.markAsFinished()
videoWriter.finishWriting { () -> Void in
completion(outputURL)
}
})
}
}
func drawImage(pixelBuffer: CVPixelBuffer, outputSize: CGSize, image: UIImage) {
CVPixelBufferLockBaseAddress(pixelBuffer, CVPixelBufferLockFlags(rawValue: CVOptionFlags(0)))
let rgbColorSpace = CGColorSpaceCreateDeviceRGB()
let pxdata = CVPixelBufferGetBaseAddress(pixelBuffer)
let context = CGContext(
data: pxdata,
width: Int(outputSize.width),
height: Int(outputSize.height),
bitsPerComponent: 8,
bytesPerRow: CVPixelBufferGetBytesPerRow(pixelBuffer),
space: rgbColorSpace,
bitmapInfo: CGImageAlphaInfo.premultipliedFirst.rawValue
)
let rect = CGRect(x: 0, y: 0, width: CGFloat(outputSize.width), height: CGFloat(outputSize.height))
context!.clear(rect)
context!.translateBy(x: 0, y: outputSize.height)
context!.scaleBy(x: 1, y: -1)
UIGraphicsPushContext(context!)
image.draw(in: CGRect(x: 0, y: 0, width: outputSize.width, height: outputSize.height))
UIGraphicsPopContext()
CVPixelBufferUnlockBaseAddress(pixelBuffer, CVPixelBufferLockFlags(rawValue: CVOptionFlags(0)))
}
}
Я испытываю это неожиданное поведение с Xcode 11.5 (11E608c)
на физическом iPhone 8 с iOS 13.5.1
, а также на моделируемом устройстве iPhone SE (второе поколение) и iPhone 11 Pro Max.