AVPlayer и AVAssetReader создают артефакты с видео ProRes422 нечетной ширины - PullRequest
0 голосов
/ 05 мая 2020

Во время работы над приложением для редактирования видео я (и несколько пользователей на разных машинах) заметил нечто необычное в файлах QuickTime нечетной ширины, закодированных с помощью кода ProRes422 c. При открытии в стандартном AVPlayer они содержат артефакт (маленькая розовая полоска) на правом краю. Такой же артефакт присутствует в CMSampleBuffer при чтении файла с использованием стандартного AVAssetReader. Этого не происходит с видео равной ширины или при использовании кодировщика ProRes4444. Вот как это выглядит.

Я пробовал открывать оба видео в других приложениях (Elmedia Player, Camera Bag Pro, VL C) и во всех приложениях сторонних производителей, которые я test отображает видео нечетной ширины с артефактом. Стандартный проигрыватель QuickTime Player не показывает артефакт, как и Safari при перетаскивании файла туда. Другие приложения Apple, которые я пробовал (Фотографии, Предварительный просмотр, Apple TV), также отображают его без розовой линии.

Я действительно хотел бы знать, почему это происходит, и если ProRes422 требует особой обработки, как может артефакт? следует избегать:

  1. в CMSampleBuffer при чтении видео с помощью AVAssetReader;
  2. и при воспроизведении видео в стандартном управлении AVPlayer.

Видео с четной и нечетной шириной из приведенного выше снимка экрана можно создать с помощью следующего кода, который работает в MacOS Xcode Playground:

// Produce two QuickTime MOV (ProRes422) files of odd and even width. Odd one will contain
// a pink line artifact on the right edge in some players. Even one won't. Why?

import AVFoundation
import CoreImage
import CoreMedia

let sizeEven = CGSize(width: 500, height: 250)
let sizeOdd = CGSize(width: 501, height: 250)
let urlEven = URL(fileURLWithPath: "/Users/ianbytchek/Downloads/blue-even.mov")
let urlOdd = URL(fileURLWithPath: "/Users/ianbytchek/Downloads/blue-odd.mov")

func createQuickTime(url: URL, size: CGSize) {
    try? FileManager.default.removeItem(at: url)
    let outputSettings = [AVVideoCodecKey: AVVideoCodecType.proRes422, AVVideoWidthKey: size.width, AVVideoHeightKey: size.height] as [String: Any]
    let input = AVAssetWriterInput(mediaType: .video, outputSettings: outputSettings)
    let writer = AVAssetWriter(url: url, fileType: .mov, inputs: [input])

    precondition(writer.startWriting())
    writer.startSession(atSourceTime: .zero)

    for i in 0 ..< 10 {
        while !input.isReadyForMoreMediaData { Thread.sleep(forTimeInterval: 1 / 100) }
        let image = CIImage.blue.cropped(to: CGRect(origin: .zero, size: size))
        let buffer = CMSampleBuffer.create(size: size, image: image, timestamp: Double(i) / 10)
        precondition(input.append(buffer))
    }

    let semaphore = DispatchSemaphore(value: 0)
    input.markAsFinished()
    writer.finishWriting(completionHandler: { semaphore.signal() })
    semaphore.wait()
}

createQuickTime(url: urlOdd, size: sizeOdd)
createQuickTime(url: urlEven, size: sizeEven)

extension AVAssetWriter {
    convenience init(url: URL, fileType: AVFileType, inputs: [AVAssetWriterInput]) {
        try! self.init(url: url, fileType: fileType)
        inputs.forEach({ precondition(self.canAdd($0)); self.add($0) })
    }
}
extension CMSampleBuffer {
    static func create(imageBuffer: CVImageBuffer, sampleTiming: CMSampleTimingInfo) -> CMSampleBuffer {
        let formatDescription = CMVideoFormatDescription.create(imageBuffer: imageBuffer)
        var sampleTiming = sampleTiming
        var sampleBuffer: CMSampleBuffer?
        CMSampleBufferCreateReadyWithImageBuffer(allocator: nil, imageBuffer: imageBuffer, formatDescription: formatDescription, sampleTiming: &sampleTiming, sampleBufferOut: &sampleBuffer)
        return sampleBuffer!
    }
    static func create(imageBuffer: CVImageBuffer, timestamp: Double) -> CMSampleBuffer {
        let scale = CMTimeScale(NSEC_PER_SEC)
        let presentationTimestamp = CMTime(value: CMTimeValue(timestamp * Double(scale)), timescale: scale)
        let sampleTiming = CMSampleTimingInfo(duration: CMTime.invalid, presentationTimeStamp: presentationTimestamp, decodeTimeStamp: CMTime.invalid)
        return self.create(imageBuffer: imageBuffer, sampleTiming: sampleTiming)
    }
    static func create(size: CGSize, image: CIImage, timestamp: Double) -> CMSampleBuffer {
        let colorSpace = CGColorSpace(name: CGColorSpace.sRGB)!
        let pixelBuffer = CVPixelBuffer.create(size: size, pixelFormat: kCMPixelFormat_32ARGB, attributes: [kCVImageBufferCGColorSpaceKey: colorSpace])
        CIContext().render(image, to: pixelBuffer, bounds: image.extent, colorSpace: colorSpace)
        return self.create(imageBuffer: pixelBuffer, timestamp: timestamp)
    }
}
extension CMVideoFormatDescription {
    static func create(imageBuffer: CVImageBuffer) -> CMVideoFormatDescription {
        var formatDescription: CMVideoFormatDescription?
        CMVideoFormatDescriptionCreateForImageBuffer(allocator: nil, imageBuffer: imageBuffer, formatDescriptionOut: &formatDescription)
        return formatDescription!
    }
}
extension CVPixelBuffer {
    static func create(size: CGSize, pixelFormat: OSType? = nil, attributes: [CFString: Any]? = nil) -> CVPixelBuffer {
        var pixelBuffer: CVPixelBuffer?
        CVPixelBufferCreate(nil, Int(size.width), Int(size.height), pixelFormat ?? kCVPixelFormatType_32ARGB, attributes as CFDictionary?, &pixelBuffer)
        return pixelBuffer!
    }
}
...