本文是講解特效相機中的視頻播放器的實現,完整源碼可查看AwemeLike。node
首先咱們先來看一下播放器的結構git
--> 音頻幀隊列 --> --> 音頻處理 --> 音頻渲染
/ \ /
視頻文件 --> 解碼器 --> --> 音視頻同步 -->
\ / \
--> 視頻幀隊列 --> --> 視頻處理 --> 視頻渲染
複製代碼
能夠看到,播放一個視頻文件須要通過解碼、音視頻同步、音視頻處理等步驟,而後才能渲染出來。 相對於通常的播放器,視頻編輯器的播放器須要修改它的音視頻數據,也就是多了音視頻處理這個步驟。因此咱們的播放器不只須要有play、pause和seekTime等功能,還須要提供一些接口來讓外部修改音視頻數據。 (視頻編輯器的工做主要是集中音視頻處理這個步驟,但這個不是這篇文章的重點)github
對音頻的處理,目前只支持添加配樂和修改音量,其內部是使用AudioUnit實現的數組
- (void)play;
- (void)playWithMusic:(NSString *)musicFilePath;
- (void)changeVolume:(CGFloat)volume isMusic:(CGFloat)isMusic;
複製代碼
對視頻的處理,使用OpenGLES來實現的,GPUImageOutput<GPUImageInput> *
是GPUImage這個庫中的類緩存
NSArray<GPUImageOutput<GPUImageInput> *> *filters;
複製代碼
下面再來看看播放器HPPlayer的頭文件bash
@interface HPPlayer : NSObject
- (instancetype)initWithFilePath:(NSString *)path preview:(UIView *)preview playerStateDelegate:(id<PlayerStateDelegate>)delegate;
- (instancetype)initWithFilePath:(NSString *)path playerStateDelegate:(id<PlayerStateDelegate>)delegate;
@property (nonatomic, strong) UIView *preview;
@property(nonatomic, copy) NSArray<GPUImageOutput<GPUImageInput> *> *filters;
@property (nonatomic, copy) NSString *musicFilePath;
@property(nonatomic, assign) BOOL shouldRepeat;
@property(nonatomic, assign) BOOL enableFaceDetector;
- (CMTime)currentTime;
- (CMTime)duration;
- (NSInteger)sampleRate;
- (NSUInteger)channels;
- (void)play;
- (void)pause;
- (BOOL)isPlaying;
- (void)playWithMusic:(NSString *)musicFilePath;
- (CGFloat)musicVolume;
- (CGFloat)originVolume;
- (void)changeVolume:(CGFloat)volume isMusic:(CGFloat)isMusic;
- (void)seekToTime:(CMTime)time;
- (void)seekToTime:(CMTime)time status:(HPPlayerSeekTimeStatus)status;
@end
複製代碼
解碼工做是由項目中的HPVideoDecoder
類完成的,其內部是使用系統自帶的AVAssetReader
來解碼的,AVAssetReader
是一個高層的API,使用起來很是方便,只須要一個本地視頻文件和一些簡單的參數就能夠直接獲得解碼後音視頻幀--系統會幫咱們作解封裝和音視頻幀解碼的工做。app
初始化時,咱們須要設置一些解碼參數,用來指定解碼後音視頻的格式dom
- (AVAssetReader*)createAssetReader {
NSError *error = nil;
AVAssetReader *assetReader = [AVAssetReader assetReaderWithAsset:self->asset error:&error];
NSMutableDictionary *outputSettings = [NSMutableDictionary dictionary];
[outputSettings setObject:@(kCVPixelFormatType_32BGRA) forKey:(id)kCVPixelBufferPixelFormatTypeKey];
readerVideoTrackOutput = [AVAssetReaderTrackOutput assetReaderTrackOutputWithTrack:[[self->asset tracksWithMediaType:AVMediaTypeVideo] objectAtIndex:0] outputSettings:outputSettings];
readerVideoTrackOutput.alwaysCopiesSampleData = false;
readerVideoTrackOutput.supportsRandomAccess = true;
[assetReader addOutput:readerVideoTrackOutput];
NSArray *audioTracks = [self->asset tracksWithMediaType:AVMediaTypeAudio];
BOOL shouldRecordAudioTrack = [audioTracks count] > 0;
audioEncodingIsFinished = true;
if (shouldRecordAudioTrack)
{
audioEncodingIsFinished = false;
AVAssetTrack* audioTrack = [audioTracks objectAtIndex:0];
readerAudioTrackOutput = [AVAssetReaderTrackOutput assetReaderTrackOutputWithTrack:audioTrack outputSettings:@{AVFormatIDKey: @(kAudioFormatLinearPCM), AVLinearPCMIsFloatKey: @(false), AVLinearPCMBitDepthKey: @(16), AVLinearPCMIsNonInterleaved: @(false), AVLinearPCMIsBigEndianKey: @(false)}];
readerAudioTrackOutput.alwaysCopiesSampleData = false;
readerAudioTrackOutput.supportsRandomAccess = true;
[assetReader addOutput:readerAudioTrackOutput];
}
return assetReader;
}
複製代碼
能夠看到,視頻幀會給解碼成BGRA
的格式,這是爲了方便以後人臉識別的使用,而音頻幀解碼後的格式是LPCM 16int 交叉存儲
,這是爲了匹配HPAudioOutput
中的設置。編輯器
初始化以後,調用AVAssetReader
的startReading
,接着咱們就可使用AVAssetReaderOutput
的copyNextSampleBuffer
方法來獲取音視頻幀了。ide
在播放視頻的過程當中是能夠改變播放進度的,在AVAssetReader
中有兩種方式來重置進度 一種是使用AVAssetReader
的timeRange
屬性,使用這種方式時,必須從新建立一個新的AVAssetReader
對象,由於timeRange
屬性只能在startReading
調用以前修改。 另外一種是使用AVAssetReaderOutput
的- (void)resetForReadingTimeRanges:(NSArray<NSValue *> *)timeRanges
方法,使用這個方法不須要從新建立一個新的AVAssetReader
對象,可是必須是在copyNextSampleBuffer
方法返回NULL
以後才能調用。
項目中使用的第二種方法,如下是seekTime的具體實現
- (CMTime)seekToTime:(CMTime)time {
[lock lock];
CMTime maxTime = CMTimeSubtract(asset.duration, CMTimeMake(5, 100));
if (CMTIME_COMPARE_INLINE(time, >=, maxTime)) {
time = maxTime;
}
CMSampleBufferRef buffer;
while ((buffer = [readerVideoTrackOutput copyNextSampleBuffer])) {
CFRelease(buffer);
};
while ((buffer = [readerAudioTrackOutput copyNextSampleBuffer])) {
CFRelease(buffer);
};
audioEncodingIsFinished = false;
videoEncodingIsFinished = false;
NSValue *videoTimeValue = [NSValue valueWithCMTimeRange:CMTimeRangeMake(time, videoBufferDuration)];
NSValue *audioTimeValue = [NSValue valueWithCMTimeRange:CMTimeRangeMake(time, audioBufferDuration)];
[readerVideoTrackOutput resetForReadingTimeRanges:@[videoTimeValue]];
[readerAudioTrackOutput resetForReadingTimeRanges:@[audioTimeValue]];
[lock unlock];
return time;
}
複製代碼
其實,AVAssetReader
並不適合用來作這種能夠重置播放進度的實時視頻播放,這是由於上述兩種重置播放進度的方法都是一個很是耗時的操做,並且視頻文件越大耗時越多,耗時多了就會致使聲音出現噪音。 在本項目中,這種方式勉強可以工做,由於咱們編輯的視頻通常在1分鐘之內,可能播放一遍會有一兩次噪音出現,這是一個很大的缺點,並且很難避免。 一種更好的方式是,使用FFmpeg解封裝,而後使用VideoToolBox解碼視頻幀。
resetForReadingTimeRanges
崩潰問題app從後臺到前臺時,會在執行resetForReadingTimeRanges
時崩潰。
這是由於app從後臺到前臺以後,經過copyNextSampleBuffer
返回的值很大多是NULL,但實際上當前AVAssetReader
緩存的音視頻幀尚未讀完,這時執行resetForReadingTimeRanges
就會崩潰,因此在進入前臺以後,最好是從新在建立一個新的AVAssetReader
對象(也就是執行openFile
方法)
- (void)addNotification {
__weak typeof(self) wself = self;
observer1 = [[NSNotificationCenter defaultCenter] addObserverForName:UIApplicationWillResignActiveNotification object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification * _Nonnull note) {
__strong typeof(wself) self = wself;
[self->lock lock];
self->isActive = false;
[self->lock unlock];
}];
observer2 = [[NSNotificationCenter defaultCenter] addObserverForName:UIApplicationDidBecomeActiveNotification object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification * _Nonnull note) {
__strong typeof(wself) self = wself;
[self->lock lock];
self->isActive = true;
BOOL opened = [self openFile];
[self->lock unlock];
if (opened) {
CMTime nextTime = CMTimeAdd(self->audioLastTime, self->audioLastDuration);
[self seekToTime:nextTime];
}
}];
}
複製代碼
音頻渲染是由HPAudioOutput
類來完成的,它的功能是將輸入的原始音頻和配樂音頻這兩路音頻合併成一路,而後經過揚聲器或耳機播放出來。其具體實現是使用AudioUnit將多個音頻單元組合起來構成一個圖狀結構來處理音頻,圖狀結構以下
convertNode -->
\
--> mixerNode --> ioNode
/
musicFileNode -->
複製代碼
上圖中這四個Node均可以看作是音頻單元
convertNode
是帶有音頻格式轉換功能的AUNode,其對應的AudioUnit是convertUnit
,經過設置它的回調函數InputRenderCallback
來獲取輸入的原始音頻
AURenderCallbackStruct callbackStruct;
callbackStruct.inputProc = &InputRenderCallback;
callbackStruct.inputProcRefCon = (__bridge void *)self;
AudioUnitSetProperty(_convertUnit, kAudioUnitProperty_SetRenderCallback, kAudioUnitScope_Input, 0, &callbackStruct, sizeof(callbackStruct));
複製代碼
musicFileNode
是一個能夠關聯媒體文件的AUNode,其對應的AudioUnit是musicFileUnit
,它將關聯的媒體文件解碼以後做爲輸入數據源,在項目中咱們會關聯一個配樂文件來做爲輸入
AudioFileID musicFile;
CFURLRef songURL = (__bridge CFURLRef)[NSURL URLWithString:filePath];
AudioFileOpenURL(songURL, kAudioFileReadPermission, 0, &musicFile);
AudioUnitSetProperty(_musicFileUnit, kAudioUnitProperty_ScheduledFileIDs,
kAudioUnitScope_Global, 0, &musicFile, sizeof(musicFile));
複製代碼
設置從哪一個地方開始讀取媒體文件
ScheduledAudioFileRegion rgn;
memset (&rgn.mTimeStamp, 0, sizeof(rgn.mTimeStamp));
rgn.mTimeStamp.mFlags = kAudioTimeStampSampleTimeValid;
rgn.mTimeStamp.mSampleTime = 0;
rgn.mCompletionProc = NULL;
rgn.mCompletionProcUserData = NULL;
rgn.mAudioFile = musicFile;
rgn.mLoopCount = 0;
rgn.mStartFrame = (SInt64)(startOffset * fileASBD.mSampleRate);;
rgn.mFramesToPlay = MAX(1, (UInt32)nPackets * fileASBD.mFramesPerPacket - (UInt32)rgn.mStartFrame);
AudioUnitSetProperty(_musicFileUnit, kAudioUnitProperty_ScheduledFileRegion,
kAudioUnitScope_Global, 0,&rgn, sizeof(rgn));
複製代碼
mixerNode
是一個具備多路混音效果的AUNode,其對應的AudioUnit是 mixerUnit
,它的做用就是講上述兩路輸入音頻合併成一路,而後輸出到ioNode
。
同時,它還能夠修改輸入的每一路音頻的音量大小,如下代碼表示將element爲0(也就是原始音頻)的那一路音頻的音量設置爲0.5
AudioUnitSetParameter(_mixerUnit, kMultiChannelMixerParam_Volume, kAudioUnitScope_Input, inputNum, 0.5, 0);
複製代碼
ioNode
是一個用來採集和播放音頻的AUNode,對應的AudioUnit是ioUnit
。經過麥克風採集音頻時會使用element爲1的那一路通道,播放音頻時使用element爲0的那一路。當使用它來播放音頻時,它就會成爲整個數據流的驅動方。
在項目中咱們只是使用ioNode
來播放音頻,因此在鏈接ioNode
時,注意必須指定它的element爲0
- (void)makeNodeConnections {
OSStatus status = noErr;
status = AUGraphConnectNodeInput(_auGraph, _convertNode, 0, _mixerNode, 0);
CheckStatus(status, @"Could not connect I/O node input to mixer node input", YES);
//music file input
status = AUGraphConnectNodeInput(_auGraph, _musicFileNode, 0, _mixerNode, 1);
CheckStatus(status, @"Could not connect I/O node input to mixer node input", YES);
//output
status = AUGraphConnectNodeInput(_auGraph, _mixerNode, 0, _ioNode, 0);
CheckStatus(status, @"Could not connect I/O node input to mixer node input", YES);
}
複製代碼
在HPAudioOutput
中使用了兩種音頻格式clientFormat16int
和clientFormat32float
。 因爲咱們將convertUnit
的輸入端的格式設置爲了clientFormat16int
,因此輸入的原始音頻數據必需要符合這種格式。 因爲使用ioUnit
播放音頻時,它只支持clientFormat32float
格式的輸入音頻數據,因此convertUnit
的輸出端格式也必須是clientFormat32float
。
- (void)configPCMDataFromat {
Float64 sampleRate = _sampleRate;
UInt32 channels = _channels;
UInt32 bytesPerSample = sizeof(Float32);
AudioStreamBasicDescription clientFormat32float;
bzero(&clientFormat32float, sizeof(clientFormat32float));
clientFormat32float.mSampleRate = sampleRate;
clientFormat32float.mFormatID = kAudioFormatLinearPCM;
clientFormat32float.mFormatFlags = kAudioFormatFlagsNativeFloatPacked | kAudioFormatFlagIsNonInterleaved;
clientFormat32float.mBitsPerChannel = 8 * bytesPerSample;
clientFormat32float.mBytesPerFrame = bytesPerSample;
clientFormat32float.mBytesPerPacket = bytesPerSample;
clientFormat32float.mFramesPerPacket = 1;
clientFormat32float.mChannelsPerFrame = channels;
self.clientFormat32float = clientFormat32float;
bytesPerSample = sizeof(SInt16);
AudioStreamBasicDescription clientFormat16int;
bzero(&clientFormat16int, sizeof(clientFormat16int));
clientFormat16int.mFormatID = kAudioFormatLinearPCM;
clientFormat16int.mFormatFlags = kLinearPCMFormatFlagIsSignedInteger | kLinearPCMFormatFlagIsPacked;
clientFormat16int.mBytesPerPacket = bytesPerSample * channels;
clientFormat16int.mFramesPerPacket = 1;
clientFormat16int.mBytesPerFrame= bytesPerSample * channels;
clientFormat16int.mChannelsPerFrame = channels;
clientFormat16int.mBitsPerChannel = 8 * bytesPerSample;
clientFormat16int.mSampleRate = sampleRate;
self.clientFormat16int = clientFormat16int;
}
複製代碼
HPVideoOutput
視頻渲染是由HPVideoOutput
類來完成的,它的做用是將傳入的視頻幀上傳到紋理,而後應用filters
屬性所包含的濾鏡,最後經過GPUImageView
顯示到屏幕上。
@interface HPVideoOutput : NSObject
@property(nonatomic, readonly) UIView *preview;
@property(nonatomic, copy) NSArray<GPUImageOutput<GPUImageInput> *> *filters;
@property(nonatomic, assign) BOOL enableFaceDetector;
- (instancetype)initWithFrame:(CGRect)frame orientation:(CGFloat)orientation;
- (void)presentVideoSampleBuffer:(CMSampleBufferRef)sampleBuffer;
@end
複製代碼
GPUImageRawDataInput
上傳到紋理若是須要人臉信息,就須要將視頻幀傳給Face++作人臉檢測。須要注意的是,人臉檢測和上傳視頻幀到紋理的操做都須要放在OpenGL的特定子線程中執行,防止阻塞其餘線程,且能保證在以後執行其餘濾鏡時拿到的人臉信息的正確性。
- (void)presentVideoSampleBuffer:(CMSampleBufferRef)sampleBuffer {
if (!sampleBuffer) {
return;
}
runAsynchronouslyOnVideoProcessingQueue(^{
if (self.enableFaceDetector) {
[self faceDetect:sampleBuffer];
}
CVImageBufferRef cameraFrame = CMSampleBufferGetImageBuffer(sampleBuffer);
int bufferWidth = (int) CVPixelBufferGetBytesPerRow(cameraFrame) / 4;
int bufferHeight = (int) CVPixelBufferGetHeight(cameraFrame);
CMTime currentTime = CMSampleBufferGetPresentationTimeStamp(sampleBuffer);
CVPixelBufferLockBaseAddress(cameraFrame, 0);
void *bytes = CVPixelBufferGetBaseAddress(cameraFrame);
[self.input updateDataFromBytes:bytes size:CGSizeMake(bufferWidth, bufferHeight)];
CVPixelBufferUnlockBaseAddress(cameraFrame, 0);
CFRelease(sampleBuffer);
[self.input processDataForTimestamp:currentTime];
});
}
複製代碼
filters
中包含的濾鏡應用到紋理上- (void)_refreshFilters {
runAsynchronouslyOnVideoProcessingQueue(^{
[self.input removeAllTargets];
[self.input addTarget:self.rotateFilter];
GPUImageOutput *prevFilter = self.rotateFilter;
GPUImageOutput<GPUImageInput> *theFilter = nil;
for (int i = 0; i < [self.filters count]; i++) {
theFilter = [self.filters objectAtIndex:i];
[prevFilter removeAllTargets];
[prevFilter addTarget:theFilter];
prevFilter = theFilter;
}
[prevFilter removeAllTargets];
if (self.output != nil) {
[prevFilter addTarget:self.output];
}
});
}
複製代碼
GPUImageView
顯示到屏幕上上述代碼中的self.output
就是一個GPUImageView
的對象
因爲OpenGL並不負責窗口管理和上下文管理,因此想要將紋理顯示到屏幕上,須要使用CAEAGLLayer
和EAGLContext
這兩個類。
下面以GPUImage
庫的GPUImageView
爲例,講解如何進行上下文環境的搭建
layerClass
+ (Class)layerClass
{
return [CAEAGLLayer class];
}
複製代碼
CAEAGLLayer
設置參數CAEAGLLayer *eaglLayer = (CAEAGLLayer *)self.layer;
eaglLayer.opaque = YES;
eaglLayer.drawableProperties = [NSDictionary dictionaryWithObjectsAndKeys:[NSNumber numberWithBool:NO], kEAGLDrawablePropertyRetainedBacking, kEAGLColorFormatRGBA8, kEAGLDrawablePropertyColorFormat, nil];
複製代碼
EAGLContext
,必須爲每個線程綁定一個EAGLContext
[GPUImageContext useImageProcessingContext];
複製代碼
+ (void)useImageProcessingContext;
{
[[GPUImageContext sharedImageProcessingContext] useAsCurrentContext];
}
- (void)useAsCurrentContext;
{
EAGLContext *imageProcessingContext = [self context];
if ([EAGLContext currentContext] != imageProcessingContext)
{
[EAGLContext setCurrentContext:imageProcessingContext];
}
}
複製代碼
CAEAGLLayer
綁定幀緩存displayFramebuffer
(因爲iOS不容許使用OpenGLES直接渲染屏幕,因此須要先將幀緩存渲染到displayRenderbuffer
上)glGenFramebuffers(1, &displayFramebuffer);
glBindFramebuffer(GL_FRAMEBUFFER, displayFramebuffer);
glGenRenderbuffers(1, &displayRenderbuffer);
glBindRenderbuffer(GL_RENDERBUFFER, displayRenderbuffer);
[[[GPUImageContext sharedImageProcessingContext] context] renderbufferStorage:GL_RENDERBUFFER fromDrawable:(CAEAGLLayer*)self.layer];
GLint backingWidth, backingHeight;
glGetRenderbufferParameteriv(GL_RENDERBUFFER, GL_RENDERBUFFER_WIDTH, &backingWidth);
glGetRenderbufferParameteriv(GL_RENDERBUFFER, GL_RENDERBUFFER_HEIGHT, &backingHeight);
if ( (backingWidth == 0) || (backingHeight == 0) )
{
[self destroyDisplayFramebuffer];
return;
}
_sizeInPixels.width = (CGFloat)backingWidth;
_sizeInPixels.height = (CGFloat)backingHeight;
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, displayRenderbuffer);
複製代碼
另外,因爲OpenGLES的原點在左下角,而CAEAGLLayer
的原點在左上角,若是不作改動,最終顯示的圖像是上下顛倒的。 爲了修正這個問題,在GPUImageView
中,默認的紋理座標是上下顛倒的
+ (const GLfloat *)textureCoordinatesForRotation:(GPUImageRotationMode)rotationMode;
{
static const GLfloat noRotationTextureCoordinates[] = {
0.0f, 1.0f,
1.0f, 1.0f,
0.0f, 0.0f,
1.0f, 0.0f,
};
........
}
複製代碼
爲何須要音視頻同步模塊,由於音頻和視頻都是在各自的線程中播放的,致使它們的播放速率可能不一致,爲了統一音視頻的時間,因此須要一個模塊來作同步工做。
這個模塊是由HPPlayerSynchronizer
這個類完成的,它的主要工做是維護一個內部的音視頻幀的緩存隊列,而後外界經過下列兩個方法來從緩存隊列中獲取同步好的音視頻幀,若是沒有,則會運行內部的解碼線程來填充緩存隊列。
- (void)audioCallbackFillData:(SInt16 *)outData
numFrames:(UInt32)numFrames
numChannels:(UInt32)numChannels;
- (CMSampleBufferRef)getCorrectVideoSampleBuffer;
複製代碼
這兩個獲取音視頻幀的方法、音視頻幀緩存隊列和解碼線程三者共同構成了一個生產者-消費者模型。
HPPlayerSynchronizer
是如何保證音視頻幀是同步的呢
通常來講有三種方式,音頻向視頻同步、視頻向音頻同步和音頻視頻都向外部時鐘同步。 本項目中使用的是視頻向音頻同步,由於相對於畫面,咱們對聲音更加敏感,當發生丟幀或插入空數據時,咱們的耳朵是能清晰的感受到的。使用視頻向音頻同步能夠保證音頻的每一幀都會播放出來,相應的,視頻幀可能會發生丟幀或跳幀,不過,咱們的眼睛通常發現不了。
因爲是視頻向音頻同步,因此音頻幀只須要按照順序從緩存隊列中取出就能夠。 在獲取視頻幀時,則須要對比當前的音頻時間audioPosition
,若是差值沒有超過閾值,則返回此視頻幀。 若是超過了閾值,則須要分爲兩種狀況,一種是視頻幀比較大,則說明視頻太快了,直接返回空,表示繼續渲染上一幀;另外一種是視頻幀比較小,則說明視頻太慢了,須要將當前視頻幀丟棄,而後從視頻緩存隊列中獲取下一幀繼續對比,直到差值在閾值以內爲止。
static float lastPosition = -1.0;
- (CMSampleBufferRef)getCorrectVideoSampleBuffer {
CMSampleBufferRef sample = NULL;
CMTime position;
[bufferlock lock];
while (videoBuffers.count > 0) {
sample = (__bridge CMSampleBufferRef)videoBuffers[0];
position = CMSampleBufferGetPresentationTimeStamp(sample);
CGFloat delta = CMTimeGetSeconds(CMTimeSubtract(audioPosition, position));
if (delta < (0 - syncMaxTimeDiff)) {//視頻太快了
sample = NULL;
break;
}
CFRetain(sample);
[videoBuffers removeObjectAtIndex:0];
if (delta > syncMaxTimeDiff) {//視頻太慢了
CFRelease(sample);
sample = NULL;
continue;
}
break;
}
[bufferlock unlock];
if(sample && fabs(CMTimeGetSeconds(audioPosition) - lastPosition) > 0.01f){
lastPosition = CMTimeGetSeconds(audioPosition);
return sample;
} else {
return nil;
}
}
複製代碼
解碼器HPVideoDecoder
返回的音視頻幀和HPPlayerSynchronizer
中的音視頻緩存隊列存儲的都是CMSampleBufferRef
類型的對象,若是沒有維護好CMSampleBufferRef
的引用計數,會致使大量的內存泄漏。
咱們只要記住一點就能夠維護好引用計數,即保持CMSampleBufferRef
對象的引用計數爲1。
下面以一個CMSampleBufferRef
的生命週期爲例
CMSampleBufferRef
,通常是由解碼器HPVideoDecoder
完成的,這時的引用計數爲1,CMSampleBufferRef sampleBufferRef = [readerVideoTrackOutput copyNextSampleBuffer]
複製代碼
CMSampleBufferRef
添加到數組videos
,而後將videos
返回給音視頻同步類HPPlayerSynchronizer
。這時,被添加到數組會致使其引用計數加1,因此使用CFBridgingRelease
減1,最終引用計數是1[videos addObject:CFBridgingRelease(sampleBufferRef)];
複製代碼
videos
添加到音視頻隊列數組videoBuffers
,引用計數加1,臨時數組videos
銷燬時減1,最終引用計數是1CMSampleBufferRef sample = (__bridge CMSampleBufferRef)videos[i];
[videoBuffers addObject:(__bridge id)(sample)];
複製代碼
sample
的引用計數仍然是1CMSampleBufferRef sample = (__bridge CMSampleBufferRef)videoBuffers[0];
CFRetain(sample);
[videoBuffers removeObjectAtIndex:0];
複製代碼
HPVideoOutput
中將視頻幀上傳到紋理後,釋放sample
CFRelease(sample);
複製代碼