Swift/AVReaderWriter/CyanifyOperation.swift
/* |
Copyright (C) 2016 Apple Inc. All Rights Reserved. |
See LICENSE.txt for this sample’s licensing information |
Abstract: |
Defines a subclass of NSOperation that adjusts the color of a video file. |
*/ |
import AVFoundation |
import Dispatch |
enum CyanifyError: ErrorType { |
case NoMediaData |
} |
class CyanifyOperation: NSOperation { |
// MARK: Types |
enum Result { |
case Success |
case Cancellation |
case Failure(ErrorType) |
} |
// MARK: Properties |
override var executing: Bool { |
return result == nil |
} |
override var finished: Bool { |
return result != nil |
} |
private let asset: AVAsset |
private let outputURL: NSURL |
private var sampleTransferError: ErrorType? |
var result: Result? { |
willSet { |
willChangeValueForKey("isExecuting") |
willChangeValueForKey("isFinished") |
} |
didSet { |
didChangeValueForKey("isExecuting") |
didChangeValueForKey("isFinished") |
} |
} |
// MARK: Initialization |
init(sourceURL: NSURL, outputURL: NSURL) { |
asset = AVAsset(URL: sourceURL) |
self.outputURL = outputURL |
} |
override var asynchronous: Bool { |
return true |
} |
// Every path through `start()` must call `finish()` exactly once. |
override func start() { |
guard !cancelled else { |
finish(.Cancellation) |
return |
} |
// Load asset properties in the background, to avoid blocking the caller with synchronous I/O. |
asset.loadValuesAsynchronouslyForKeys(["tracks"]) { |
guard !self.cancelled else { |
self.finish(.Cancellation) |
return |
} |
// These are all initialized in the below 'do' block, assuming no errors are thrown. |
let assetReader: AVAssetReader |
let assetWriter: AVAssetWriter |
let videoReaderOutputsAndWriterInputs: [ReaderOutputAndWriterInput] |
let passthroughReaderOutputsAndWriterInputs: [ReaderOutputAndWriterInput] |
do { |
// Make sure that the asset tracks loaded successfully. |
var trackLoadingError: NSError? |
guard self.asset.statusOfValueForKey("tracks", error: &trackLoadingError) == .Loaded else { |
throw trackLoadingError! |
} |
let tracks = self.asset.tracks |
// Create reader/writer objects. |
assetReader = try AVAssetReader(asset: self.asset) |
assetWriter = try AVAssetWriter(URL: self.outputURL, fileType: AVFileTypeQuickTimeMovie) |
let (videoReaderOutputs, passthroughReaderOutputs) = try self.makeReaderOutputsForTracks(tracks, availableMediaTypes: assetWriter.availableMediaTypes) |
videoReaderOutputsAndWriterInputs = try self.makeVideoWriterInputsForVideoReaderOutputs(videoReaderOutputs) |
passthroughReaderOutputsAndWriterInputs = try self.makePassthroughWriterInputsForPassthroughReaderOutputs(passthroughReaderOutputs) |
// Hook everything up. |
for (readerOutput, writerInput) in videoReaderOutputsAndWriterInputs { |
assetReader.addOutput(readerOutput) |
assetWriter.addInput(writerInput) |
} |
for (readerOutput, writerInput) in passthroughReaderOutputsAndWriterInputs { |
assetReader.addOutput(readerOutput) |
assetWriter.addInput(writerInput) |
} |
/* |
Remove file if necessary. AVAssetWriter will not overwrite |
an existing file. |
*/ |
let fileManager = NSFileManager() |
if let outputPath = self.outputURL.path where fileManager.fileExistsAtPath(outputPath) { |
try fileManager.removeItemAtURL(self.outputURL) |
} |
// Start reading/writing. |
guard assetReader.startReading() else { |
// `error` is non-nil when startReading returns false. |
throw assetReader.error! |
} |
guard assetWriter.startWriting() else { |
// `error` is non-nil when startWriting returns false. |
throw assetWriter.error! |
} |
assetWriter.startSessionAtSourceTime(kCMTimeZero) |
} |
catch { |
self.finish(.Failure(error)) |
return |
} |
let writingGroup = dispatch_group_create() |
// Transfer data from input file to output file. |
self.transferVideoTracks(videoReaderOutputsAndWriterInputs, group: writingGroup) |
self.transferPassthroughTracks(passthroughReaderOutputsAndWriterInputs, group: writingGroup) |
// Handle completion. |
let queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0) |
dispatch_group_notify(writingGroup, queue) { |
// `readingAndWritingDidFinish()` is guaranteed to call `finish()` exactly once. |
self.readingAndWritingDidFinish(assetReader, assetWriter: assetWriter) |
} |
} |
} |
/** |
A type used for correlating an `AVAssetWriterInput` with the `AVAssetReaderOutput` |
that is the source of appended samples. |
*/ |
private typealias ReaderOutputAndWriterInput = (readerOutput: AVAssetReaderOutput, writerInput: AVAssetWriterInput) |
private func makeReaderOutputsForTracks(tracks: [AVAssetTrack], availableMediaTypes: [String]) throws -> (videoReaderOutputs: [AVAssetReaderTrackOutput], passthroughReaderOutputs: [AVAssetReaderTrackOutput]) { |
// Decompress source video to 32ARGB. |
let videoDecompressionSettings: [String: AnyObject] = [ |
String(kCVPixelBufferPixelFormatTypeKey): NSNumber(unsignedInt: kCVPixelFormatType_32ARGB), |
String(kCVPixelBufferIOSurfacePropertiesKey): [:] |
] |
// Partition tracks into "video" and "passthrough" buckets, create reader outputs. |
var videoReaderOutputs = [AVAssetReaderTrackOutput]() |
var passthroughReaderOutputs = [AVAssetReaderTrackOutput]() |
for track in tracks { |
guard availableMediaTypes.contains(track.mediaType) else { continue } |
switch track.mediaType { |
case AVMediaTypeVideo: |
let videoReaderOutput = AVAssetReaderTrackOutput(track: track, outputSettings: videoDecompressionSettings) |
videoReaderOutputs += [videoReaderOutput] |
default: |
// `nil` output settings means "passthrough." |
let passthroughReaderOutput = AVAssetReaderTrackOutput(track: track, outputSettings: nil) |
passthroughReaderOutputs += [passthroughReaderOutput] |
} |
} |
return (videoReaderOutputs, passthroughReaderOutputs) |
} |
private func makeVideoWriterInputsForVideoReaderOutputs(videoReaderOutputs: [AVAssetReaderTrackOutput]) throws -> [ReaderOutputAndWriterInput] { |
// Compress modified source frames to H.264. |
let videoCompressionSettings: [String: AnyObject] = [ |
AVVideoCodecKey: AVVideoCodecH264 |
] |
/* |
In order to find the source format we need to create a temporary asset |
reader, plus a temporary track output for each "real" track output. |
We will only read as many samples (typically just one) as necessary |
to discover the format of the buffers that will be read from each "real" |
track output. |
*/ |
let tempAssetReader = try AVAssetReader(asset: asset) |
let videoReaderOutputsAndTempVideoReaderOutputs: [(videoReaderOutput: AVAssetReaderTrackOutput, tempVideoReaderOutput: AVAssetReaderTrackOutput)] = videoReaderOutputs.map { videoReaderOutput in |
let tempVideoReaderOutput = AVAssetReaderTrackOutput(track: videoReaderOutput.track, outputSettings: videoReaderOutput.outputSettings) |
tempAssetReader.addOutput(tempVideoReaderOutput) |
return (videoReaderOutput, tempVideoReaderOutput) |
} |
// Start reading. |
guard tempAssetReader.startReading() else { |
// 'error' will be non-nil if startReading fails. |
throw tempAssetReader.error! |
} |
/* |
Create video asset writer inputs, using the source format hints read |
from the "temporary" reader outputs. |
*/ |
var videoReaderOutputsAndWriterInputs = [ReaderOutputAndWriterInput]() |
for (videoReaderOutput, tempVideoReaderOutput) in videoReaderOutputsAndTempVideoReaderOutputs { |
// Fetch format of source sample buffers. |
var videoFormatHint: CMFormatDescriptionRef? |
while videoFormatHint == nil { |
guard let sampleBuffer = tempVideoReaderOutput.copyNextSampleBuffer() else { |
// We ran out of sample buffers before we found one with a format description |
throw CyanifyError.NoMediaData |
} |
videoFormatHint = CMSampleBufferGetFormatDescription(sampleBuffer) |
} |
// Create asset writer input. |
let videoWriterInput = AVAssetWriterInput(mediaType: AVMediaTypeVideo, outputSettings: videoCompressionSettings, sourceFormatHint: videoFormatHint) |
videoReaderOutputsAndWriterInputs.append((readerOutput: videoReaderOutput, writerInput: videoWriterInput)) |
} |
// Shut down processing pipelines, since only a subset of the samples were read. |
tempAssetReader.cancelReading() |
return videoReaderOutputsAndWriterInputs |
} |
private func makePassthroughWriterInputsForPassthroughReaderOutputs(passthroughReaderOutputs: [AVAssetReaderTrackOutput]) throws -> [ReaderOutputAndWriterInput] { |
/* |
Create passthrough writer inputs, using the source track's format |
descriptions as the format hint for each writer input. |
*/ |
var passthroughReaderOutputsAndWriterInputs = [ReaderOutputAndWriterInput]() |
for passthroughReaderOutput in passthroughReaderOutputs { |
/* |
For passthrough, we can simply ask the track for its format |
description and use that as the writer input's format hint. |
*/ |
let trackFormatDescriptions = passthroughReaderOutput.track.formatDescriptions as! [CMFormatDescriptionRef] |
guard let passthroughFormatHint = trackFormatDescriptions.first else { |
throw CyanifyError.NoMediaData |
} |
// Create asset writer input with nil (passthrough) output settings |
let passthroughWriterInput = AVAssetWriterInput(mediaType: passthroughReaderOutput.mediaType, outputSettings: nil, sourceFormatHint: passthroughFormatHint) |
passthroughReaderOutputsAndWriterInputs.append((readerOutput: passthroughReaderOutput, writerInput: passthroughWriterInput)) |
} |
return passthroughReaderOutputsAndWriterInputs |
} |
private func transferVideoTracks(videoReaderOutputsAndWriterInputs: [ReaderOutputAndWriterInput], group: dispatch_group_t) { |
for (videoReaderOutput, videoWriterInput) in videoReaderOutputsAndWriterInputs { |
let perTrackDispatchQueue = dispatch_queue_create("Track data transfer queue: \(videoReaderOutput) -> \(videoWriterInput).", nil) |
// A block for changing color values of each video frame. |
let videoProcessor: CMSampleBufferRef throws -> Void = { sampleBuffer in |
if let imageBuffer = CMSampleBufferGetImageBuffer(sampleBuffer), |
pixelBuffer: CVPixelBufferRef = imageBuffer |
where CFGetTypeID(imageBuffer) == CVPixelBufferGetTypeID() { |
let redComponentIndex = 1 |
try pixelBuffer.removeARGBColorComponentAtIndex(redComponentIndex) |
} |
} |
dispatch_group_enter(group) |
transferSamplesAsynchronouslyFromReaderOutput(videoReaderOutput, toWriterInput: videoWriterInput, onQueue: perTrackDispatchQueue, sampleBufferProcessor: videoProcessor) { |
dispatch_group_leave(group) |
} |
} |
} |
private func transferPassthroughTracks(passthroughReaderOutputsAndWriterInputs: [ReaderOutputAndWriterInput], group: dispatch_group_t) { |
for (passthroughReaderOutput, passthroughWriterInput) in passthroughReaderOutputsAndWriterInputs { |
let perTrackDispatchQueue = dispatch_queue_create("Track data transfer queue: \(passthroughReaderOutput) -> \(passthroughWriterInput).", nil) |
dispatch_group_enter(group) |
transferSamplesAsynchronouslyFromReaderOutput(passthroughReaderOutput, toWriterInput: passthroughWriterInput, onQueue: perTrackDispatchQueue) { |
dispatch_group_leave(group) |
} |
} |
} |
private func transferSamplesAsynchronouslyFromReaderOutput(readerOutput: AVAssetReaderOutput, toWriterInput writerInput: AVAssetWriterInput, onQueue queue: dispatch_queue_t, sampleBufferProcessor: ((sampleBuffer: CMSampleBufferRef) throws -> Void)? = nil, completionHandler: Void -> Void) { |
// Provide the asset writer input with a block to invoke whenever it wants to request more samples |
writerInput.requestMediaDataWhenReadyOnQueue(queue) { |
var isDone = false |
/* |
Loop, transferring one sample per iteration, until the asset writer |
input has enough samples. At that point, exit the callback block |
and the asset writer input will invoke the block again when it |
needs more samples. |
*/ |
while writerInput.readyForMoreMediaData { |
guard !self.cancelled else { |
isDone = true |
break |
} |
// Grab next sample from the asset reader output. |
guard let sampleBuffer = readerOutput.copyNextSampleBuffer() else { |
/* |
At this point, the asset reader output has no more samples |
to vend. |
*/ |
isDone = true |
break |
} |
// Process the sample, if requested. |
do { |
try sampleBufferProcessor?(sampleBuffer: sampleBuffer) |
} |
catch { |
// This error will be picked back up in `readingAndWritingDidFinish()`. |
self.sampleTransferError = error |
isDone = true |
} |
// Append the sample to the asset writer input. |
guard writerInput.appendSampleBuffer(sampleBuffer) else { |
/* |
The sample buffer could not be appended. Error information |
will be fetched from the asset writer in |
`readingAndWritingDidFinish()`. |
*/ |
isDone = true |
break |
} |
} |
if isDone { |
/* |
Calling `markAsFinished()` on the asset writer input will both: |
1. Unblock any other inputs that need more samples. |
2. Cancel further invocations of this "request media data" |
callback block. |
*/ |
writerInput.markAsFinished() |
// Tell the caller that we are done transferring samples. |
completionHandler() |
} |
} |
} |
private func readingAndWritingDidFinish(assetReader: AVAssetReader, assetWriter: AVAssetWriter) { |
if cancelled { |
assetReader.cancelReading() |
assetWriter.cancelWriting() |
} |
// Deal with any error that occurred during processing of the video. |
guard sampleTransferError == nil else { |
assetReader.cancelReading() |
assetWriter.cancelWriting() |
finish(.Failure(sampleTransferError!)) |
return |
} |
// Evaluate result of reading samples. |
guard assetReader.status == .Completed else { |
let result: Result |
switch assetReader.status { |
case .Cancelled: |
assetWriter.cancelWriting() |
result = .Cancellation |
case .Failed: |
// `error` property is non-nil in the `.Failed` status. |
result = .Failure(assetReader.error!) |
default: |
fatalError("Unexpected terminal asset reader status: \(assetReader.status).") |
} |
finish(result) |
return |
} |
// Finish writing, (asynchronously) evaluate result of writing samples. |
assetWriter.finishWritingWithCompletionHandler { |
let result: Result |
switch assetWriter.status { |
case .Completed: |
result = .Success |
case .Cancelled: |
result = .Cancellation |
case .Failed: |
// `error` property is non-nil in the `.Failed` status. |
result = .Failure(assetWriter.error!) |
default: |
fatalError("Unexpected terminal asset writer status: \(assetWriter.status).") |
} |
self.finish(result) |
} |
} |
func finish(result: Result) { |
self.result = result |
} |
} |
extension CVPixelBufferRef { |
/** |
Iterates through each pixel in the receiver (assumed to be in ARGB format) |
and overwrites the color component at the given index with a zero. This |
has the effect of "cyanifying," "rosifying," etc (depending on the chosen |
color component) the overall image represented by the pixel buffer. |
*/ |
func removeARGBColorComponentAtIndex(componentIndex: size_t) throws { |
let lockBaseAddressResult = CVPixelBufferLockBaseAddress(self, CVPixelBufferLockFlags(rawValue: CVOptionFlags(0))) |
guard lockBaseAddressResult == kCVReturnSuccess else { |
throw NSError(domain: NSOSStatusErrorDomain, code: Int(lockBaseAddressResult), userInfo: nil) |
} |
let bufferHeight = CVPixelBufferGetHeight(self) |
let bufferWidth = CVPixelBufferGetWidth(self) |
let bytesPerRow = CVPixelBufferGetBytesPerRow(self) |
let bytesPerPixel = bytesPerRow / bufferWidth |
let base = UnsafeMutablePointer<Int8>(CVPixelBufferGetBaseAddress(self)) |
// For each pixel, zero out selected color component. |
for row in 0..<bufferHeight { |
for column in 0..<bufferWidth { |
let pixel: UnsafeMutablePointer<Int8> = base + (row * bytesPerRow) + (column * bytesPerPixel) |
pixel[componentIndex] = 0 |
} |
} |
let unlockBaseAddressResult = CVPixelBufferUnlockBaseAddress(self, CVPixelBufferLockFlags(rawValue: CVOptionFlags(0))) |
guard unlockBaseAddressResult == kCVReturnSuccess else { |
throw NSError(domain: NSOSStatusErrorDomain, code: Int(unlockBaseAddressResult), userInfo: nil) |
} |
} |
} |
Copyright © 2016 Apple Inc. All Rights Reserved. Terms of Use | Privacy Policy | Updated: 2016-09-13