使用GPUImage录制视频时第一帧会出现黑屏或者白屏,并且调用addAudioInputsAndOutputs也不好使
此时需要修改GPUImageMovieWriter.m的源码,在其中添加以下代码
static BOOL allowWriteAudio = NO;
- (void)startRecording;
{
...
allowWriteAudio = NO;
}
- (void)processAudioBuffer:(CMSampleBufferRef)audioBuffer;
{
if (!allowWriteAudio) {
return;
}
...
}
- (void)newFrameReadyAtTime:(CMTime)frameTime atIndex:(NSInteger)textureIndex;
{
...
if (![assetWriterPixelBufferInput appendPixelBuffer:pixel_buffer withPresentationTime:frameTime])
NSLog(@"Problem appending pixel buffer at time: %@", CFBridgingRelease(CMTimeCopyDescription(kCFAllocatorDefault, frameTime)));
allowWriteAudio = YES; //< add this
...
}
上述方法存在拍摄是录音问题
话不多说,把GPUImageMovieWriter.m全部换成下面的代码,亲测有效
#import "GPUImageMovieWriter.h"
#import "GPUImageContext.h"
#import "GLProgram.h"
#import "GPUImageFilter.h"
NSString *const kGPUImageColorSwizzlingFragmentShaderString = SHADER_STRING
(
varying highp vec2 textureCoordinate;
uniform sampler2D inputImageTexture;
void main()
{
gl_FragColor = texture2D(inputImageTexture, textureCoordinate).bgra;
}
);
@interface GPUImageMovieWriter ()
{
GLuint movieFramebuffer, movieRenderbuffer;
GLProgram *colorSwizzlingProgram;
GLint colorSwizzlingPositionAttribute, colorSwizzlingTextureCoordinateAttribute;
GLint colorSwizzlingInputTextureUniform;
GPUImageFramebuffer *firstInputFramebuffer;
BOOL discont;
BOOL allowWriteAudio;
BOOL willFinish;
void (^completeHandler)(void);
CMTime startTime, previousFrameTime, previousAudioTime;
CMTime offsetTime;
dispatch_queue_t audioQueue, videoQueue;
BOOL audioEncodingIsFinished, videoEncodingIsFinished;
BOOL isRecording;
}
// Movie recording
- (void)initializeMovieWithOutputSettings:(NSMutableDictionary *)outputSettings;
// Frame rendering
- (void)createDataFBO;
- (void)destroyDataFBO;
- (void)setFilterFBO;
- (void)renderAtInternalSizeUsingFramebuffer:(GPUImageFramebuffer *)inputFramebufferToUse;
@end
@implementation GPUImageMovieWriter
@synthesize hasAudioTrack = _hasAudioTrack;
@synthesize encodingLiveVideo = _encodingLiveVideo;
@synthesize shouldPassthroughAudio = _shouldPassthroughAudio;
@synthesize completionBlock;
@synthesize failureBlock;
@synthesize videoInputReadyCallback;
@synthesize audioInputReadyCallback;
@synthesize enabled;
@synthesize shouldInvalidateAudioSampleWhenDone = _shouldInvalidateAudioSampleWhenDone;
@synthesize paused = _paused;
@synthesize movieWriterContext = _movieWriterContext;
@synthesize delegate = _delegate;
#pragma mark -
#pragma mark Initialization and teardown
- (id)initWithMovieURL:(NSURL *)newMovieURL size:(CGSize)newSize;
{
return [self initWithMovieURL:newMovieURL size:newSize fileType:AVFileTypeQuickTimeMovie outputSettings:nil];
}
- (id)initWithMovieURL:(NSURL *)newMovieURL size:(CGSize)newSize fileType:(NSString *)newFileType outputSettings:(NSMutableDictionary *)outputSettings;
{
if (!(self = [super init]))
{
return nil;
}
_shouldInvalidateAudioSampleWhenDone = NO;
self.enabled = YES;
alreadyFinishedRecording = NO;
videoEncodingIsFinished = NO;
audioEncodingIsFinished = NO;
discont = NO;
videoSize = newSize;
movieURL = newMovieURL;
fileType = newFileType;
startTime = kCMTimeInvalid;
_encodingLiveVideo = [[outputSettings objectForKey:@"EncodingLiveVideo"] isKindOfClass:[NSNumber class]] ? [[outputSettings objectForKey:@"EncodingLiveVideo"] boolValue] : YES;
previousFrameTime = kCMTimeNegativeInfinity;
previousAudioTime = kCMTimeNegativeInfinity;
inputRotation = kGPUImageNoRotation;
_movieWriterContext = [[GPUImageContext alloc] init];
[_movieWriterContext useSharegroup:[[[GPUImageContext sharedImageProcessingContext] context] sharegroup]];
runSynchronouslyOnContextQueue(_movieWriterContext, ^{
[_movieWriterContext useAsCurrentContext];
if ([GPUImageContext supportsFastTextureUpload])
{
colorSwizzlingProgram = [_movieWriterContext programForVertexShaderString:kGPUImageVertexShaderString fragmentShaderString:kGPUImagePassthroughFragmentShaderString];
}
else
{
colorSwizzlingProgram = [_movieWriterContext programForVertexShaderString:kGPUImageVertexShaderString fragmentShaderString:kGPUImageColorSwizzlingFragmentShaderString];
}
if (!colorSwizzlingProgram.initialized)
{
[colorSwizzlingProgram addAttribute:@"position"];
[colorSwizzlingProgram addAttribute:@"inputTextureCoordinate"];
if (![colorSwizzlingProgram link])
{
NSString *progLog = [colorSwizzlingProgram programLog];
NSLog(@"Program link log: %@", progLog);
NSString *fragLog = [colorSwizzlingProgram fragmentShaderLog];
NSLog(@"Fragment shader compile log: %@", fragLog);
NSString *vertLog = [colorSwizzlingProgram vertexShaderLog];
NSLog(@"Vertex shader compile log: %@", vertLog);
colorSwizzlingProgram = nil;
NSAssert(NO, @"Filter shader link failed");
}
}
colorSwizzlingPositionAttribute = [colorSwizzlingProgram attributeIndex:@"position"];
colorSwizzlingTextureCoordinateAttribute = [colorSwizzlingProgram attributeIndex:@"inputTextureCoordinate"];
colorSwizzlingInputTextureUniform = [colorSwizzlingProgram uniformIndex:@"inputImageTexture"];
[_movieWriterContext setContextShaderProgram:colorSwizzlingProgram];
glEnableVertexAttribArray(colorSwizzlingPositionAttribute);
glEnableVertexAttribArray(colorSwizzlingTextureCoordinateAttribute);
});
[self initializeMovieWithOutputSettings:outputSettings];
return self;
}
- (void)dealloc;
{
[self destroyDataFBO];
#if !OS_OBJECT_USE_OBJC
if( audioQueue != NULL )
{
dispatch_release(audioQueue);
}
if( videoQueue != NULL )
{
dispatch_release(videoQueue);
}
#endif
}
#pragma mark -
#pragma mark Movie recording
- (void)initializeMovieWithOutputSettings:(NSDictionary *)outputSettings;
{
isRecording = NO;
self.enabled = YES;
NSError *error = nil;
assetWriter = [[AVAssetWriter alloc] initWithURL:movieURL fileType:fileType error:&error];
if (error != nil)
{
NSLog(@"Error: %@", error);
if (failureBlock)
{
failureBlock(error);
}
else
{
if(self.delegate && [self.delegate respondsToSelector:@selector(movieRecordingFailedWithError:)])
{
[self.delegate movieRecordingFailedWithError:error];
}
}
}
// Set this to make sure that a functional movie is produced, even if the recording is cut off mid-stream. Only the last second should be lost in that case.
assetWriter.movieFragmentInterval = CMTimeMakeWithSeconds(1.0, 1000);
// use default output settings if none specified
if (outputSettings == nil)
{
NSMutableDictionary *settings = [[NSMutableDictionary alloc] init];
[settings setObject:AVVideoCodecH264 forKey:AVVideoCodecKey];
[settings setObject:[NSNumber numberWithInt:videoSize.width] forKey:AVVideoWidthKey];
[settings setObject:[NSNumber numberWithInt:videoSize.height] forKey:AVVideoHeightKey];
outputSettings = settings;
}
// custom output settings specified
else
{
__unused NSString *videoCodec = [outputSettings objectForKey:AVVideoCodecKey];
__unused NSNumber *width = [outputSettings objectForKey:AVVideoWidthKey];
__unused NSNumber *height = [outputSettings objectForKey:AVVideoHeightKey];
NSAssert(videoCodec && width && height, @"OutputSettings is missing required parameters.");
if( [outputSettings objectForKey:@"EncodingLiveVideo"] ) {
NSMutableDictionary *tmp = [outputSettings mutableCopy];
[tmp removeObjectForKey:@"EncodingLiveVideo"];
outputSettings = tmp;
}
}
/*
NSDictionary *videoCleanApertureSettings = [NSDictionary dictionaryWithObjectsAndKeys:
[NSNumber numberWithInt:videoSize.width], AVVideoCleanApertureWidthKey,
[NSNumber numberWithInt:videoSize.height], AVVideoCleanApertureHeightKey,
[NSNumber numberWithInt:0], AVVideoCleanApertureHorizontalOffsetKey,
[NSNumber numberWithInt:0], AVVideoCleanApertureVerticalOffsetKey,
nil];
NSDictionary *videoAspectRatioSettings = [NSDictionary dictionaryWithObjectsAndKeys:
[NSNumber numberWithInt:3], AVVideoPixelAspectRatioHorizontalSpacingKey,
[NSNumber numberWithInt:3], AVVideoPixelAspectRatioVerticalSpacingKey,
nil];
NSMutableDictionary * compressionProperties = [[NSMutableDictionary alloc] init];
[compressionProperties setObject:videoCleanApertureSettings forKey:AVVideoCleanApertureKey];
[compressionProperties setObject:videoAspectRatioSettings forKey:AVVideoPixelAspectRatioKey];
[compressionProperties setObject:[NSNumber numberWithInt: 2000000] forKey:AVVideoAverageBitRateKey];
[compressionProperties setObject:[NSNumber numberWithInt: 16] forKey:AVVideoMaxKeyFrameIntervalKey];
[compressionProperties setObject:AVVideoProfileLevelH264Main31 forKey:AVVideoProfileLevelKey];
[outputSettings setObject:compressionProperties forKey:AVVideoCompressionPropertiesKey];
*/
assetWriterVideoInput = [AVAssetWriterInput assetWriterInputWithMediaType:AVMediaTypeVideo outputSettings:outputSettings];
assetWriterVideoInput.expectsMediaDataInRealTime = _encodingLiveVideo;
// You need to use BGRA for the video in order to get realtime encoding. I use a color-swizzling shader to line up glReadPixels' normal RGBA output with the movie input's BGRA.
NSDictionary *sourcePixelBufferAttributesDictionary = [NSDictionary dictionaryWithObjectsAndKeys: [NSNumber numberWithInt:kCVPixelFormatType_32BGRA], kCVPixelBufferPixelFormatTypeKey,
[NSNumber numberWithInt:videoSize.width], kCVPixelBufferWidthKey,
[NSNumber numberWithInt:videoSize.height], kCVPixelBufferHeightKey,
nil];
// NSDictionary *sourcePixelBufferAttributesDictionary = [NSDictionary dictionaryWithObjectsAndKeys: [NSNumber numberWithInt:kCVPixelFormatType_32ARGB], kCVPixelBufferPixelFormatTypeKey,
// nil];
assetWriterPixelBufferInput = [AVAssetWriterInputPixelBufferAdaptor assetWriterInputPixelBufferAdaptorWithAssetWriterInput:assetWriterVideoInput sourcePixelBufferAttributes:sourcePixelBufferAttributesDictionary];
[assetWriter addInput:assetWriterVideoInput];
}
- (void)setEncodingLiveVideo:(BOOL) value
{
_encodingLiveVideo = value;
if (isRecording) {
NSAssert(NO, @"Can not change Encoding Live Video while recording");
}
else
{
assetWriterVideoInput.expectsMediaDataInRealTime = _encodingLiveVideo;
assetWriterAudioInput.expectsMediaDataInRealTime = _encodingLiveVideo;
}
}
- (void)startRecording;
{
alreadyFinishedRecording = NO;
startTime = kCMTimeInvalid;
runSynchronouslyOnContextQueue(_movieWriterContext, ^{
if (audioInputReadyCallback == NULL)
{
[assetWriter startWriting];
}
});
isRecording = YES;
allowWriteAudio = NO;
// [assetWriter startSessionAtSourceTime:kCMTimeZero];
}
- (void)startRecordingInOrientation:(CGAffineTransform)orientationTransform;
{
assetWriterVideoInput.transform = orientationTransform;
[self startRecording];
}
- (void)cancelRecording;
{
if (assetWriter.status == AVAssetWriterStatusCompleted)
{
return;
}
isRecording = NO;
runSynchronouslyOnContextQueue(_movieWriterContext, ^{
alreadyFinishedRecording = YES;
if( assetWriter.status == AVAssetWriterStatusWriting && ! videoEncodingIsFinished )
{
videoEncodingIsFinished = YES;
[assetWriterVideoInput markAsFinished];
}
if( assetWriter.status == AVAssetWriterStatusWriting && ! audioEncodingIsFinished )
{
audioEncodingIsFinished = YES;
[assetWriterAudioInput markAsFinished];
}
[assetWriter cancelWriting];
});
}
- (void)finishRecording;
{
[self finishRecordingWithCompletionHandler:NULL];
}
- (void)finishRecordingWithCompletionHandler:(void (^)(void))handler;
{
runSynchronouslyOnContextQueue(_movieWriterContext, ^{
willFinish = NO;
if (assetWriter.status == AVAssetWriterStatusCompleted || assetWriter.status == AVAssetWriterStatusCancelled || assetWriter.status == AVAssetWriterStatusUnknown)
{
isRecording = NO;
if (handler)
runAsynchronouslyOnContextQueue(_movieWriterContext, handler);
return;
}
allowWriteAudio = NO;
if (CMTimeCompare(previousFrameTime, previousAudioTime) == -1) {
// recoreded audio frame is longer than video frame
willFinish = YES;
completeHandler = handler;
return;
}
completeHandler = NULL;
isRecording = NO;
if( assetWriter.status == AVAssetWriterStatusWriting && ! videoEncodingIsFinished )
{
videoEncodingIsFinished = YES;
[assetWriterVideoInput markAsFinished];
}
if( assetWriter.status == AVAssetWriterStatusWriting && ! audioEncodingIsFinished )
{
audioEncodingIsFinished = YES;
[assetWriterAudioInput markAsFinished];
}
#if (!defined(__IPHONE_6_0) || (__IPHONE_OS_VERSION_MAX_ALLOWED < __IPHONE_6_0))
// Not iOS 6 SDK
[assetWriter finishWriting];
if (handler)
runAsynchronouslyOnContextQueue(_movieWriterContext,handler);
#else
// iOS 6 SDK
if ([assetWriter respondsToSelector:@selector(finishWritingWithCompletionHandler:)]) {
// Running iOS 6
[assetWriter finishWritingWithCompletionHandler:(handler ?: ^{ })];
}
else {
// Not running iOS 6
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
[assetWriter finishWriting];
#pragma clang diagnostic pop
if (handler)
runAsynchronouslyOnContextQueue(_movieWriterContext, handler);
}
#endif
});
}
- (void)processAudioBuffer:(CMSampleBufferRef)audioBuffer;
{
if (!isRecording || _paused || !allowWriteAudio)
{
return;
}
// if (_hasAudioTrack && CMTIME_IS_VALID(startTime))
if (_hasAudioTrack)
{
CFRetain(audioBuffer);
CMTime currentSampleTime = CMSampleBufferGetOutputPresentationTimeStamp(audioBuffer);
if (CMTIME_IS_INVALID(startTime))
{
runSynchronouslyOnContextQueue(_movieWriterContext, ^{
if ((audioInputReadyCallback == NULL) && (assetWriter.status != AVAssetWriterStatusWriting))
{
[assetWriter startWriting];
}
[assetWriter startSessionAtSourceTime:currentSampleTime];
startTime = currentSampleTime;
});
}
if (!assetWriterAudioInput.readyForMoreMediaData && _encodingLiveVideo)
{
NSLog(@"1: Had to drop an audio frame: %@", CFBridgingRelease(CMTimeCopyDescription(kCFAllocatorDefault, currentSampleTime)));
if (_shouldInvalidateAudioSampleWhenDone)
{
CMSampleBufferInvalidate(audioBuffer);
}
CFRelease(audioBuffer);
return;
}
if (discont) {
discont = NO;
CMTime current;
if (offsetTime.value > 0) {
current = CMTimeSubtract(currentSampleTime, offsetTime);
} else {
current = currentSampleTime;
}
CMTime offset = CMTimeSubtract(current, previousAudioTime);
if (offsetTime.value == 0) {
offsetTime = offset;
} else {
offsetTime = CMTimeAdd(offsetTime, offset);
}
}
if (offsetTime.value > 0) {
CFRelease(audioBuffer);
audioBuffer = [self adjustTime:audioBuffer by:offsetTime];
CFRetain(audioBuffer);
}
// record most recent time so we know the length of the pause
currentSampleTime = CMSampleBufferGetPresentationTimeStamp(audioBuffer);
previousAudioTime = currentSampleTime;
//if the consumer wants to do something with the audio samples before writing, let him.
if (self.audioProcessingCallback) {
//need to introspect into the opaque CMBlockBuffer structure to find its raw sample buffers.
CMBlockBufferRef buffer = CMSampleBufferGetDataBuffer(audioBuffer);
CMItemCount numSamplesInBuffer = CMSampleBufferGetNumSamples(audioBuffer);
AudioBufferList audioBufferList;
CMSampleBufferGetAudioBufferListWithRetainedBlockBuffer(audioBuffer,
NULL,
&audioBufferList,
sizeof(audioBufferList),
NULL,
NULL,
kCMSampleBufferFlag_AudioBufferList_Assure16ByteAlignment,
&buffer
);
//passing a live pointer to the audio buffers, try to process them in-place or we might have syncing issues.
for (int bufferCount=0; bufferCount < audioBufferList.mNumberBuffers; bufferCount++) {
SInt16 *samples = (SInt16 *)audioBufferList.mBuffers[bufferCount].mData;
self.audioProcessingCallback(&samples, numSamplesInBuffer);
}
}
// NSLog(@"Recorded audio sample time: %lld, %d, %lld", currentSampleTime.value, currentSampleTime.timescale, currentSampleTime.epoch);
void(^write)() = ^() {
while( ! assetWriterAudioInput.readyForMoreMediaData && ! _encodingLiveVideo && ! audioEncodingIsFinished ) {
NSDate *maxDate = [NSDate dateWithTimeIntervalSinceNow:0.5];
//NSLog(@"audio waiting...");
[[NSRunLoop currentRunLoop] runUntilDate:maxDate];
}
if (!assetWriterAudioInput.readyForMoreMediaData)
{
NSLog(@"2: Had to drop an audio frame %@", CFBridgingRelease(CMTimeCopyDescription(kCFAllocatorDefault, currentSampleTime)));
}
else if(assetWriter.status == AVAssetWriterStatusWriting)
{
if (![assetWriterAudioInput appendSampleBuffer:audioBuffer])
NSLog(@"Problem appending audio buffer at time: %@", CFBridgingRelease(CMTimeCopyDescription(kCFAllocatorDefault, currentSampleTime)));
}
else
{
//NSLog(@"Wrote an audio frame %@", CFBridgingRelease(CMTimeCopyDescription(kCFAllocatorDefault, currentSampleTime)));
}
if (_shouldInvalidateAudioSampleWhenDone)
{
CMSampleBufferInvalidate(audioBuffer);
}
CFRelease(audioBuffer);
};
// runAsynchronouslyOnContextQueue(_movieWriterContext, write);
if( _encodingLiveVideo )
{
runAsynchronouslyOnContextQueue(_movieWriterContext, write);
}
else
{
write();
}
}
}
- (void)enableSynchronizationCallbacks;
{
if (videoInputReadyCallback != NULL)
{
if( assetWriter.status != AVAssetWriterStatusWriting )
{
[assetWriter startWriting];
}
videoQueue = dispatch_queue_create("com.sunsetlakesoftware.GPUImage.videoReadingQueue", NULL);
[assetWriterVideoInput requestMediaDataWhenReadyOnQueue:videoQueue usingBlock:^{
if( _paused )
{
//NSLog(@"video requestMediaDataWhenReadyOnQueue paused");
// if we don't sleep, we'll get called back almost immediately, chewing up CPU
usleep(10000);
return;
}
//NSLog(@"video requestMediaDataWhenReadyOnQueue begin");
while( assetWriterVideoInput.readyForMoreMediaData && ! _paused )
{
if( videoInputReadyCallback && ! videoInputReadyCallback() && ! videoEncodingIsFinished )
{
runAsynchronouslyOnContextQueue(_movieWriterContext, ^{
if( assetWriter.status == AVAssetWriterStatusWriting && ! videoEncodingIsFinished )
{
videoEncodingIsFinished = YES;
[assetWriterVideoInput markAsFinished];
}
});
}
}
//NSLog(@"video requestMediaDataWhenReadyOnQueue end");
}];
}
if (audioInputReadyCallback != NULL)
{
audioQueue = dispatch_queue_create("com.sunsetlakesoftware.GPUImage.audioReadingQueue", NULL);
[assetWriterAudioInput requestMediaDataWhenReadyOnQueue:audioQueue usingBlock:^{
if( _paused )
{
//NSLog(@"audio requestMediaDataWhenReadyOnQueue paused");
// if we don't sleep, we'll get called back almost immediately, chewing up CPU
usleep(10000);
return;
}
//NSLog(@"audio requestMediaDataWhenReadyOnQueue begin");
while( assetWriterAudioInput.readyForMoreMediaData && ! _paused )
{
if( audioInputReadyCallback && ! audioInputReadyCallback() && ! audioEncodingIsFinished )
{
runAsynchronouslyOnContextQueue(_movieWriterContext, ^{
if( assetWriter.status == AVAssetWriterStatusWriting && ! audioEncodingIsFinished )
{
audioEncodingIsFinished = YES;
[assetWriterAudioInput markAsFinished];
}
});
}
}
//NSLog(@"audio requestMediaDataWhenReadyOnQueue end");
}];
}
}
#pragma mark -
#pragma mark Frame rendering
- (void)createDataFBO;
{
glActiveTexture(GL_TEXTURE1);
glGenFramebuffers(1, &movieFramebuffer);
glBindFramebuffer(GL_FRAMEBUFFER, movieFramebuffer);
if ([GPUImageContext supportsFastTextureUpload])
{
// Code originally sourced from http://allmybrain.com/2011/12/08/rendering-to-a-texture-with-ios-5-texture-cache-api/
CVPixelBufferPoolCreatePixelBuffer (NULL, [assetWriterPixelBufferInput pixelBufferPool], &renderTarget);
/* AVAssetWriter will use BT.601 conversion matrix for RGB to YCbCr conversion
* regardless of the kCVImageBufferYCbCrMatrixKey value.
* Tagging the resulting video file as BT.601, is the best option right now.
* Creating a proper BT.709 video is not possible at the moment.
*/
CVBufferSetAttachment(renderTarget, kCVImageBufferColorPrimariesKey, kCVImageBufferColorPrimaries_ITU_R_709_2, kCVAttachmentMode_ShouldPropagate);
CVBufferSetAttachment(renderTarget, kCVImageBufferYCbCrMatrixKey, kCVImageBufferYCbCrMatrix_ITU_R_601_4, kCVAttachmentMode_ShouldPropagate);
CVBufferSetAttachment(renderTarget, kCVImageBufferTransferFunctionKey, kCVImageBufferTransferFunction_ITU_R_709_2, kCVAttachmentMode_ShouldPropagate);
CVOpenGLESTextureCacheCreateTextureFromImage (kCFAllocatorDefault, [_movieWriterContext coreVideoTextureCache], renderTarget,
NULL, // texture attributes
GL_TEXTURE_2D,
GL_RGBA, // opengl format
(int)videoSize.width,
(int)videoSize.height,
GL_BGRA, // native iOS format
GL_UNSIGNED_BYTE,
0,
&renderTexture);
glBindTexture(CVOpenGLESTextureGetTarget(renderTexture), CVOpenGLESTextureGetName(renderTexture));
glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, CVOpenGLESTextureGetName(renderTexture), 0);
}
else
{
glGenRenderbuffers(1, &movieRenderbuffer);
glBindRenderbuffer(GL_RENDERBUFFER, movieRenderbuffer);
glRenderbufferStorage(GL_RENDERBUFFER, GL_RGBA8_OES, (int)videoSize.width, (int)videoSize.height);
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, movieRenderbuffer);
}
__unused GLenum status = glCheckFramebufferStatus(GL_FRAMEBUFFER);
NSAssert(status == GL_FRAMEBUFFER_COMPLETE, @"Incomplete filter FBO: %d", status);
}
- (void)destroyDataFBO;
{
runSynchronouslyOnContextQueue(_movieWriterContext, ^{
[_movieWriterContext useAsCurrentContext];
if (movieFramebuffer)
{
glDeleteFramebuffers(1, &movieFramebuffer);
movieFramebuffer = 0;
}
if (movieRenderbuffer)
{
glDeleteRenderbuffers(1, &movieRenderbuffer);
movieRenderbuffer = 0;
}
if ([GPUImageContext supportsFastTextureUpload])
{
if (renderTexture)
{
CFRelease(renderTexture);
}
if (renderTarget)
{
CVPixelBufferRelease(renderTarget);
}
}
});
}
- (void)setFilterFBO;
{
if (!movieFramebuffer)
{
[self createDataFBO];
}
glBindFramebuffer(GL_FRAMEBUFFER, movieFramebuffer);
glViewport(0, 0, (int)videoSize.width, (int)videoSize.height);
}
- (void)renderAtInternalSizeUsingFramebuffer:(GPUImageFramebuffer *)inputFramebufferToUse;
{
[_movieWriterContext useAsCurrentContext];
[self setFilterFBO];
[_movieWriterContext setContextShaderProgram:colorSwizzlingProgram];
glClearColor(1.0f, 0.0f, 0.0f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
// This needs to be flipped to write out to video correctly
static const GLfloat squareVertices[] = {
-1.0f, -1.0f,
1.0f, -1.0f,
-1.0f, 1.0f,
1.0f, 1.0f,
};
const GLfloat *textureCoordinates = [GPUImageFilter textureCoordinatesForRotation:inputRotation];
glActiveTexture(GL_TEXTURE4);
glBindTexture(GL_TEXTURE_2D, [inputFramebufferToUse texture]);
glUniform1i(colorSwizzlingInputTextureUniform, 4);
// NSLog(@"Movie writer framebuffer: %@", inputFramebufferToUse);
glVertexAttribPointer(colorSwizzlingPositionAttribute, 2, GL_FLOAT, 0, 0, squareVertices);
glVertexAttribPointer(colorSwizzlingTextureCoordinateAttribute, 2, GL_FLOAT, 0, 0, textureCoordinates);
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
glFinish();
}
#pragma mark -
#pragma mark GPUImageInput protocol
- (void)newFrameReadyAtTime:(CMTime)frameTime atIndex:(NSInteger)textureIndex;
{
if (!isRecording || _paused)
{
[firstInputFramebuffer unlock];
return;
}
if (discont) {
discont = NO;
CMTime current;
if (offsetTime.value > 0) {
current = CMTimeSubtract(frameTime, offsetTime);
} else {
current = frameTime;
}
CMTime offset = CMTimeSubtract(current, previousFrameTime);
if (offsetTime.value == 0) {
offsetTime = offset;
} else {
offsetTime = CMTimeAdd(offsetTime, offset);
}
}
if (offsetTime.value > 0) {
frameTime = CMTimeSubtract(frameTime, offsetTime);
}
// Drop frames forced by images and other things with no time constants
// Also, if two consecutive times with the same value are added to the movie, it aborts recording, so I bail on that case
if ( (CMTIME_IS_INVALID(frameTime)) || (CMTIME_COMPARE_INLINE(frameTime, ==, previousFrameTime)) || (CMTIME_IS_INDEFINITE(frameTime)) )
{
[firstInputFramebuffer unlock];
return;
}
if (CMTIME_IS_INVALID(startTime))
{
runSynchronouslyOnContextQueue(_movieWriterContext, ^{
if ((videoInputReadyCallback == NULL) && (assetWriter.status != AVAssetWriterStatusWriting))
{
[assetWriter startWriting];
}
[assetWriter startSessionAtSourceTime:frameTime];
startTime = frameTime;
allowWriteAudio = YES;
});
}
GPUImageFramebuffer *inputFramebufferForBlock = firstInputFramebuffer;
glFinish();
runAsynchronouslyOnContextQueue(_movieWriterContext, ^{
if (!assetWriterVideoInput.readyForMoreMediaData && _encodingLiveVideo)
{
[inputFramebufferForBlock unlock];
NSLog(@"1: Had to drop a video frame: %@", CFBridgingRelease(CMTimeCopyDescription(kCFAllocatorDefault, frameTime)));
return;
}
// Render the frame with swizzled colors, so that they can be uploaded quickly as BGRA frames
[_movieWriterContext useAsCurrentContext];
[self renderAtInternalSizeUsingFramebuffer:inputFramebufferForBlock];
CVPixelBufferRef pixel_buffer = NULL;
if ([GPUImageContext supportsFastTextureUpload])
{
pixel_buffer = renderTarget;
CVPixelBufferLockBaseAddress(pixel_buffer, 0);
}
else
{
CVReturn status = CVPixelBufferPoolCreatePixelBuffer (NULL, [assetWriterPixelBufferInput pixelBufferPool], &pixel_buffer);
if ((pixel_buffer == NULL) || (status != kCVReturnSuccess))
{
CVPixelBufferRelease(pixel_buffer);
return;
}
else
{
CVPixelBufferLockBaseAddress(pixel_buffer, 0);
GLubyte *pixelBufferData = (GLubyte *)CVPixelBufferGetBaseAddress(pixel_buffer);
glReadPixels(0, 0, videoSize.width, videoSize.height, GL_RGBA, GL_UNSIGNED_BYTE, pixelBufferData);
}
}
void(^write)() = ^() {
while( ! assetWriterVideoInput.readyForMoreMediaData && ! _encodingLiveVideo && ! videoEncodingIsFinished ) {
NSDate *maxDate = [NSDate dateWithTimeIntervalSinceNow:0.1];
// NSLog(@"video waiting...");
[[NSRunLoop currentRunLoop] runUntilDate:maxDate];
}
if (!assetWriterVideoInput.readyForMoreMediaData)
{
NSLog(@"2: Had to drop a video frame: %@", CFBridgingRelease(CMTimeCopyDescription(kCFAllocatorDefault, frameTime)));
}
else if(self.assetWriter.status == AVAssetWriterStatusWriting)
{
if (![assetWriterPixelBufferInput appendPixelBuffer:pixel_buffer withPresentationTime:frameTime])
NSLog(@"Problem appending pixel buffer at time: %@", CFBridgingRelease(CMTimeCopyDescription(kCFAllocatorDefault, frameTime)));
}
else
{
NSLog(@"Couldn't write a frame");
//NSLog(@"Wrote a video frame: %@", CFBridgingRelease(CMTimeCopyDescription(kCFAllocatorDefault, frameTime)));
}
CVPixelBufferUnlockBaseAddress(pixel_buffer, 0);
previousFrameTime = frameTime;
if (![GPUImageContext supportsFastTextureUpload])
{
CVPixelBufferRelease(pixel_buffer);
}
};
write();
[inputFramebufferForBlock unlock];
if (willFinish && CMTimeCompare(previousFrameTime, previousAudioTime) != -1) {
[self finishRecordingWithCompletionHandler:completeHandler];
}
});
}
- (NSInteger)nextAvailableTextureIndex;
{
return 0;
}
- (void)setInputFramebuffer:(GPUImageFramebuffer *)newInputFramebuffer atIndex:(NSInteger)textureIndex;
{
[newInputFramebuffer lock];
// runSynchronouslyOnContextQueue(_movieWriterContext, ^{
firstInputFramebuffer = newInputFramebuffer;
// });
}
- (void)setInputRotation:(GPUImageRotationMode)newInputRotation atIndex:(NSInteger)textureIndex;
{
inputRotation = newInputRotation;
}
- (void)setInputSize:(CGSize)newSize atIndex:(NSInteger)textureIndex;
{
}
- (CGSize)maximumOutputSize;
{
return videoSize;
}
- (void)endProcessing
{
if (completionBlock)
{
if (!alreadyFinishedRecording)
{
alreadyFinishedRecording = YES;
completionBlock();
}
}
else
{
if (_delegate && [_delegate respondsToSelector:@selector(movieRecordingCompleted)])
{
[_delegate movieRecordingCompleted];
}
}
}
- (BOOL)shouldIgnoreUpdatesToThisTarget;
{
return NO;
}
- (BOOL)wantsMonochromeInput;
{
return NO;
}
- (void)setCurrentlyReceivingMonochromeInput:(BOOL)newValue;
{
}
#pragma mark -
#pragma mark Accessors
- (void)setHasAudioTrack:(BOOL)newValue
{
[self setHasAudioTrack:newValue audioSettings:nil];
}
- (void)setHasAudioTrack:(BOOL)newValue audioSettings:(NSDictionary *)audioOutputSettings;
{
_hasAudioTrack = newValue;
if (_hasAudioTrack)
{
if (_shouldPassthroughAudio)
{
// Do not set any settings so audio will be the same as passthrough
audioOutputSettings = nil;
}
else if (audioOutputSettings == nil)
{
AVAudioSession *sharedAudioSession = [AVAudioSession sharedInstance];
double preferredHardwareSampleRate;
if ([sharedAudioSession respondsToSelector:@selector(sampleRate)])
{
preferredHardwareSampleRate = [sharedAudioSession sampleRate];
}
else
{
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
preferredHardwareSampleRate = [[AVAudioSession sharedInstance] currentHardwareSampleRate];
#pragma clang diagnostic pop
}
AudioChannelLayout acl;
bzero( &acl, sizeof(acl));
acl.mChannelLayoutTag = kAudioChannelLayoutTag_Mono;
audioOutputSettings = [NSDictionary dictionaryWithObjectsAndKeys:
[ NSNumber numberWithInt: kAudioFormatMPEG4AAC], AVFormatIDKey,
[ NSNumber numberWithInt: 1 ], AVNumberOfChannelsKey,
[ NSNumber numberWithFloat: preferredHardwareSampleRate ], AVSampleRateKey,
[ NSData dataWithBytes: &acl length: sizeof( acl ) ], AVChannelLayoutKey,
//[ NSNumber numberWithInt:AVAudioQualityLow], AVEncoderAudioQualityKey,
[ NSNumber numberWithInt: 64000 ], AVEncoderBitRateKey,
nil];
/*
AudioChannelLayout acl;
bzero( &acl, sizeof(acl));
acl.mChannelLayoutTag = kAudioChannelLayoutTag_Mono;
audioOutputSettings = [NSDictionary dictionaryWithObjectsAndKeys:
[ NSNumber numberWithInt: kAudioFormatMPEG4AAC ], AVFormatIDKey,
[ NSNumber numberWithInt: 1 ], AVNumberOfChannelsKey,
[ NSNumber numberWithFloat: 44100.0 ], AVSampleRateKey,
[ NSNumber numberWithInt: 64000 ], AVEncoderBitRateKey,
[ NSData dataWithBytes: &acl length: sizeof( acl ) ], AVChannelLayoutKey,
nil];*/
}
assetWriterAudioInput = [AVAssetWriterInput assetWriterInputWithMediaType:AVMediaTypeAudio outputSettings:audioOutputSettings];
[assetWriter addInput:assetWriterAudioInput];
assetWriterAudioInput.expectsMediaDataInRealTime = _encodingLiveVideo;
}
else
{
// Remove audio track if it exists
}
}
- (NSArray*)metaData {
return assetWriter.metadata;
}
- (void)setMetaData:(NSArray*)metaData {
assetWriter.metadata = metaData;
}
- (CMTime)duration {
if( ! CMTIME_IS_VALID(startTime) )
return kCMTimeZero;
if( ! CMTIME_IS_NEGATIVE_INFINITY(previousFrameTime) )
return CMTimeSubtract(previousFrameTime, startTime);
if( ! CMTIME_IS_NEGATIVE_INFINITY(previousAudioTime) )
return CMTimeSubtract(previousAudioTime, startTime);
return kCMTimeZero;
}
- (CGAffineTransform)transform {
return assetWriterVideoInput.transform;
}
- (void)setTransform:(CGAffineTransform)transform {
assetWriterVideoInput.transform = transform;
}
- (AVAssetWriter*)assetWriter {
return assetWriter;
}
- (void)setPaused:(BOOL)newValue {
if (_paused != newValue) {
_paused = newValue;
if (_paused) {
discont = YES;
}
}
}
- (CMSampleBufferRef)adjustTime:(CMSampleBufferRef) sample by:(CMTime) offset {
CMItemCount count;
CMSampleBufferGetSampleTimingInfoArray(sample, 0, nil, &count);
CMSampleTimingInfo* pInfo = malloc(sizeof(CMSampleTimingInfo) * count);
CMSampleBufferGetSampleTimingInfoArray(sample, count, pInfo, &count);
for (CMItemCount i = 0; i < count; i++) {
pInfo[i].decodeTimeStamp = CMTimeSubtract(pInfo[i].decodeTimeStamp, offset);
pInfo[i].presentationTimeStamp = CMTimeSubtract(pInfo[i].presentationTimeStamp, offset);
}
CMSampleBufferRef sout;
CMSampleBufferCreateCopyWithNewTiming(nil, sample, count, pInfo, &sout);
free(pInfo);
return sout;
}
@end