Во время работы над приложением для редактирования видео я (и несколько пользователей на разных машинах) заметил нечто необычное в файлах QuickTime нечетной ширины, закодированных с помощью кода ProRes422 c. При открытии в стандартном AVPlayer
они содержат артефакт (маленькая розовая полоска) на правом краю. Такой же артефакт присутствует в CMSampleBuffer
при чтении файла с использованием стандартного AVAssetReader
. Этого не происходит с видео равной ширины или при использовании кодировщика ProRes4444. Вот как это выглядит.
Я пробовал открывать оба видео в других приложениях (Elmedia Player, Camera Bag Pro, VL C) и во всех приложениях сторонних производителей, которые я test отображает видео нечетной ширины с артефактом. Стандартный проигрыватель QuickTime Player не показывает артефакт, как и Safari при перетаскивании файла туда. Другие приложения Apple, которые я пробовал (Фотографии, Предварительный просмотр, Apple TV), также отображают его без розовой линии.
Я действительно хотел бы знать, почему это происходит, и если ProRes422 требует особой обработки, как может артефакт? следует избегать:
- в
CMSampleBuffer
при чтении видео с помощью AVAssetReader
; - и при воспроизведении видео в стандартном управлении
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!
}
}