Видео, созданное из массива UIImage, повреждено при определенных размерах - PullRequest
0 голосов
/ 13 июля 2020

Я следил за парой руководств по общему процессу создания видео в 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.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...