Skip to content

Instantly share code, notes, and snippets.

@johnnyclem
Created September 23, 2015 22:04
Show Gist options
  • Save johnnyclem/4850b03555e57f413b23 to your computer and use it in GitHub Desktop.
Save johnnyclem/4850b03555e57f413b23 to your computer and use it in GitHub Desktop.
Defines a subclass of NSOperation that adjusts the color of a video file.
/*
Copyright (C) 2015 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, 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, 0)
guard unlockBaseAddressResult == kCVReturnSuccess else {
throw NSError(domain: NSOSStatusErrorDomain, code: Int(unlockBaseAddressResult), userInfo: nil)
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment