仿抖音特效相機之視頻播放器實現

前言

本文是講解特效相機中的視頻播放器的實現,完整源碼可查看AwemeLikenode

首先咱們先來看一下播放器的結構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
複製代碼

1. 解碼器

解碼工做是由項目中的HPVideoDecoder類完成的,其內部是使用系統自帶的AVAssetReader來解碼的,AVAssetReader是一個高層的API,使用起來很是方便,只須要一個本地視頻文件和一些簡單的參數就能夠直接獲得解碼後音視頻幀--系統會幫咱們作解封裝和音視頻幀解碼的工做。app

1.1. 基本使用

初始化時,咱們須要設置一些解碼參數,用來指定解碼後音視頻的格式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中的設置。編輯器

初始化以後,調用AVAssetReaderstartReading,接着咱們就可使用AVAssetReaderOutputcopyNextSampleBuffer方法來獲取音視頻幀了。ide

1.2. 如何實現seekTime

在播放視頻的過程當中是能夠改變播放進度的,在AVAssetReader中有兩種方式來重置進度 一種是使用AVAssetReadertimeRange屬性,使用這種方式時,必須從新建立一個新的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;
}

複製代碼

1.3. 使用限制

其實,AVAssetReader並不適合用來作這種能夠重置播放進度的實時視頻播放,這是由於上述兩種重置播放進度的方法都是一個很是耗時的操做,並且視頻文件越大耗時越多,耗時多了就會致使聲音出現噪音。 在本項目中,這種方式勉強可以工做,由於咱們編輯的視頻通常在1分鐘之內,可能播放一遍會有一兩次噪音出現,這是一個很大的缺點,並且很難避免。 一種更好的方式是,使用FFmpeg解封裝,而後使用VideoToolBox解碼視頻幀。

1.4. 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];
        }
        
    }];
}
複製代碼

2. 音頻渲染

音頻渲染是由HPAudioOutput類來完成的,它的功能是將輸入的原始音頻和配樂音頻這兩路音頻合併成一路,而後經過揚聲器或耳機播放出來。其具體實現是使用AudioUnit將多個音頻單元組合起來構成一個圖狀結構來處理音頻,圖狀結構以下

convertNode -->
                  \
                     --> mixerNode --> ioNode
                  /
musicFileNode -->
複製代碼

2.1. 音頻單元

上圖中這四個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);
}
複製代碼

2.2. 關於音頻格式

HPAudioOutput中使用了兩種音頻格式clientFormat16intclientFormat32float。 因爲咱們將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;
}

複製代碼

3. 視頻渲染

3.1. 構建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
複製代碼
  1. 將傳入的視頻幀經過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];

    });

}
複製代碼
  1. 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];
        }
    });
}
複製代碼
  1. 最終將紋理經過GPUImageView顯示到屏幕上

上述代碼中的self.output就是一個GPUImageView的對象

3.2. 將紋理顯示到UIView上

因爲OpenGL並不負責窗口管理和上下文管理,因此想要將紋理顯示到屏幕上,須要使用CAEAGLLayerEAGLContext這兩個類。

下面以GPUImage庫的GPUImageView爲例,講解如何進行上下文環境的搭建

  1. 重寫視圖的layerClass
+ (Class)layerClass 
{
	return [CAEAGLLayer class];
}
複製代碼
  1. CAEAGLLayer設置參數
CAEAGLLayer *eaglLayer = (CAEAGLLayer *)self.layer;
eaglLayer.opaque = YES;
eaglLayer.drawableProperties = [NSDictionary dictionaryWithObjectsAndKeys:[NSNumber numberWithBool:NO], kEAGLDrawablePropertyRetainedBacking, kEAGLColorFormatRGBA8, kEAGLDrawablePropertyColorFormat, nil];
複製代碼
  1. 給線程綁定EAGLContext,必須爲每個線程綁定一個EAGLContext
[GPUImageContext useImageProcessingContext];
複製代碼
+ (void)useImageProcessingContext;
{
    [[GPUImageContext sharedImageProcessingContext] useAsCurrentContext];
}

- (void)useAsCurrentContext;
{
    EAGLContext *imageProcessingContext = [self context];
    if ([EAGLContext currentContext] != imageProcessingContext)
    {
        [EAGLContext setCurrentContext:imageProcessingContext];
    }
}
複製代碼
  1. 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,
    };
........
}
複製代碼

4. 音視頻同步模塊

爲何須要音視頻同步模塊,由於音頻和視頻都是在各自的線程中播放的,致使它們的播放速率可能不一致,爲了統一音視頻的時間,因此須要一個模塊來作同步工做。

這個模塊是由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;
    }
}
複製代碼

5. CMSampleBufferRef的引用計數問題

解碼器HPVideoDecoder返回的音視頻幀和HPPlayerSynchronizer中的音視頻緩存隊列存儲的都是CMSampleBufferRef類型的對象,若是沒有維護好CMSampleBufferRef的引用計數,會致使大量的內存泄漏。

咱們只要記住一點就能夠維護好引用計數,即保持CMSampleBufferRef對象的引用計數爲1。

下面以一個CMSampleBufferRef的生命週期爲例

  1. 建立一個CMSampleBufferRef,通常是由解碼器HPVideoDecoder完成的,這時的引用計數爲1,
CMSampleBufferRef sampleBufferRef = [readerVideoTrackOutput copyNextSampleBuffer]
複製代碼
  1. CMSampleBufferRef添加到數組videos,而後將videos返回給音視頻同步類HPPlayerSynchronizer。這時,被添加到數組會致使其引用計數加1,因此使用CFBridgingRelease減1,最終引用計數是1
[videos addObject:CFBridgingRelease(sampleBufferRef)];
複製代碼
  1. 將返回的videos添加到音視頻隊列數組videoBuffers,引用計數加1,臨時數組videos銷燬時減1,最終引用計數是1
CMSampleBufferRef sample = (__bridge CMSampleBufferRef)videos[i];
[videoBuffers addObject:(__bridge id)(sample)];
複製代碼
  1. 這一步是從音視頻隊裏中返回一個視頻幀給外部,很明顯sample的引用計數仍然是1
CMSampleBufferRef sample = (__bridge CMSampleBufferRef)videoBuffers[0];
CFRetain(sample);
[videoBuffers removeObjectAtIndex:0];
複製代碼
  1. 最終在HPVideoOutput中將視頻幀上傳到紋理後,釋放sample
CFRelease(sample);
複製代碼
相關文章
相關標籤/搜索