基于iOS平臺的最簡單的FFmpeg視頻播放器(三)

如果說,視頻的解碼是最核心的一步,那么視頻的顯示播放,就是最復(fù)雜的一步,也是最難的一步。
接著上一篇文章的激情,這一篇文章主要是講述解碼后的數(shù)據(jù)是怎么有順序,有規(guī)律地顯示到我們的手機(jī)屏幕上的。

基于iOS平臺的最簡單的FFmpeg視頻播放器(一)
基于iOS平臺的最簡單的FFmpeg視頻播放器(二)
基于iOS平臺的最簡單的FFmpeg視頻播放器(三)

正式開始

  • 視頻數(shù)據(jù)顯示的步驟和原理,這里我們需要好好地理一理思路。
    1.先初始化一個基于OpenGL的顯示的范圍。
    2.把準(zhǔn)備顯示的數(shù)據(jù)處理好(就是上一篇文章沒有講完的那個部分)。
    3.在 OpenGL上繪制一幀圖片,然后刪除數(shù)組中已經(jīng)顯示過的幀。
    4.計算數(shù)組中剩余的還沒有解碼的幀,如果不夠了那就繼續(xù)開始解碼。
    5.通過一開始處理過的數(shù)據(jù)中獲取時間戳,通過定時器控制顯示幀率,然后回到步驟3。
    6.所有的視頻都解碼顯示完了,播放結(jié)束。

1.準(zhǔn)備活動

1.1 初始化OpenGL的類

  • 接下來我們使用AieGLView類,都是仿照自Kxmovie中的 KxMovieGLView類。里面的具體實現(xiàn)內(nèi)容比較多,以后我們單獨分出一個模塊來講。
- (void)setupPresentView
{
    _glView = [[AieGLView alloc] initWithFrame:CGRectMake(0, self.view.frame.size.height - 200, 300, 200) decoder:_decoder];
    [self.view addSubview:_glView];
    
    self.view.backgroundColor = [UIColor clearColor];
}

1.2 處理解碼后的數(shù)據(jù)

  • 這里就是上一篇文章中,解碼結(jié)束之后,應(yīng)該對數(shù)據(jù)做的處理。
- (AieVideoFrame *)handleVideoFrame
{
    if (!_videoFrame->data[0]) {
        return nil;
    }
    
    AieVideoFrame * frame;
    if (_videoFrameFormat == AieVideoFrameFormatYUV) {
        AieVideoFrameYUV * yuvFrame = [[AieVideoFrameYUV alloc] init];
        
        yuvFrame.luma = copyFrameData(_videoFrame->data[0],
                                      _videoFrame->linesize[0],
                                      _videoCodecCtx->width,
                                      _videoCodecCtx->height);
        
        yuvFrame.chromaB = copyFrameData(_videoFrame->data[1],
                                      _videoFrame->linesize[1],
                                      _videoCodecCtx->width / 2,
                                      _videoCodecCtx->height / 2);
        
        yuvFrame.chromaR = copyFrameData(_videoFrame->data[2],
                                      _videoFrame->linesize[2],
                                      _videoCodecCtx->width / 2,
                                      _videoCodecCtx->height / 2);
        
        frame = yuvFrame;
    }
    
    frame.width = _videoCodecCtx->width;
    frame.height = _videoCodecCtx->height;
    // 以流中的時間為基礎(chǔ) 預(yù)估的時間戳
    frame.position = av_frame_get_best_effort_timestamp(_videoFrame) * _videoTimeBase;
    
    // 獲取當(dāng)前幀的持續(xù)時間
    const int64_t frameDuration = av_frame_get_pkt_duration(_videoFrame);
    
    if (frameDuration) {
        frame.duration = frameDuration * _videoTimeBase;
        frame.duration += _videoFrame->repeat_pict * _videoTimeBase * 0.5;
    }
    else {
        frame.duration = 1.0 / _fps;
    }
    return frame;
}
  • 以上的代碼比較多,涉及的只是也比較廣,所以我們還是一段一段的來分析。

1.2.1 AVFrame數(shù)據(jù)分析

if (!_videoFrame->data[0]) {
        return nil;
    }
  • _videoFrame就是之前存儲解碼后數(shù)據(jù)的AVFrame,之前我們只說到AVFrame的定義,現(xiàn)在來說說它的結(jié)構(gòu)。
  • AVFrame有兩個最重要的屬性datalinesize。
    1.data是用來存儲解碼后的原始數(shù)據(jù),對于視頻來說就是YUV、RGB,對于音頻來說就是PCM,順便說一下,蘋果手機(jī)錄音出來的原始數(shù)據(jù)就是PCM。
    2.linesize是data數(shù)據(jù)中‘一行’數(shù)據(jù)的大小,一般大于圖像的寬度。
  • data其實是個指針數(shù)組,所以它存儲的方式是隨著數(shù)據(jù)格式的變化而變化的。
    1.對于packed格式的數(shù)據(jù)(比如RGB24),會存到data[0]中。
    2.對于planar格式的數(shù)據(jù)(比如YUV420P),則會data[0]存Y,data[1]存U,data[2]存V,數(shù)據(jù)的大小的比例也是不同的,朋友們可以了解下。

1.2.2 把數(shù)據(jù)封裝成自己的格式

AieVideoFrame * frame;
    if (_videoFrameFormat == AieVideoFrameFormatYUV) {
        AieVideoFrameYUV * yuvFrame = [[AieVideoFrameYUV alloc] init];
        
        yuvFrame.luma = copyFrameData(_videoFrame->data[0],
                                      _videoFrame->linesize[0],
                                      _videoCodecCtx->width,
                                      _videoCodecCtx->height);
        
        yuvFrame.chromaB = copyFrameData(_videoFrame->data[1],
                                      _videoFrame->linesize[1],
                                      _videoCodecCtx->width / 2,
                                      _videoCodecCtx->height / 2);
        
        yuvFrame.chromaR = copyFrameData(_videoFrame->data[2],
                                      _videoFrame->linesize[2],
                                      _videoCodecCtx->width / 2,
                                      _videoCodecCtx->height / 2);
        
        frame = yuvFrame;
    }
  • AieVideoFrame ,AieVideoFrameYUV是我們自己定義的簡單的類,不懂的可以去看代碼,結(jié)構(gòu)很簡單?,F(xiàn)在我們只考慮YUV的存儲,暫時不考慮RGB。
  • 上面的luma, chromaB,chromaR正好對應(yīng)的YUV,從傳進(jìn)去的參數(shù)就可以發(fā)現(xiàn)。
static NSData * copyFrameData(UInt8 *src, int linesize, int width, int height)
{
    width = MIN(linesize, width);
    NSMutableData *md = [NSMutableData dataWithLength: width * height];
    Byte *dst = md.mutableBytes;
    for (NSUInteger i = 0; i < height; ++i)
    {
        memcpy(dst, src, width);
        dst += width;
        src += linesize;
    }
    return md;
}
  • 說好的一行行的看代碼就得一行行看,之前我們說過linesize中的一行的數(shù)據(jù)大小,一般情況下比實際寬度大一點,但是為了避免特殊情況,這里還是需要判斷一下,取最小的那個。
  • 下面就是把數(shù)據(jù)裝到NSMutableData這個容器中,顯而易見,數(shù)據(jù)的總大小就是width * height。所以遍歷的時候就遍歷它的height,然后把整個寬度的數(shù)據(jù)全部拷貝到目標(biāo)容器中,由于這里是指針操作,所以我們需要把指針往后便宜到末尾,下一次拷貝的時候才可以繼續(xù)從末尾添加數(shù)據(jù)。
  • 有的朋友可能會問,為什么dst偏移的是width, 但是src偏移的是linesize?理由還是之前的那一個linesize可能會比width大一點,我們的最終數(shù)據(jù)dst應(yīng)該根據(jù)width來計算,但是src(就是之前的data)他的每一行實際大小是linesize,所以才需要分開來偏移。

1.2.3 解碼后數(shù)據(jù)的信息

    frame.width = _videoCodecCtx->width;
    frame.height = _videoCodecCtx->height;
    // 以流中的時間為基礎(chǔ) 預(yù)估的時間戳
    frame.position = av_frame_get_best_effort_timestamp(_videoFrame) * _videoTimeBase;
    
    // 獲取當(dāng)前幀的持續(xù)時間
    const int64_t frameDuration = av_frame_get_pkt_duration(_videoFrame);
    
    if (frameDuration) {
        frame.duration = frameDuration * _videoTimeBase;
        frame.duration += _videoFrame->repeat_pict * _videoTimeBase * 0.5;
    }
    else {
        frame.duration = 1.0 / _fps;
    }
  • AVCodecContext中的長寬,才是視頻的實際的長寬。
  • av_frame_get_best_effort_timestamp ()是以AVFrame中的時間為基礎(chǔ),預(yù)估的時間戳,然后乘以_videoTimeBase(之前默認(rèn)是0.25的那個),就是這個視頻幀當(dāng)先的時間位置。
  • av_frame_get_pkt_duration ()是獲取當(dāng)前幀的持續(xù)時間。
  • 接下來的一個if語句很刁鉆,我也不是很理解,但是查了資料,大致是這樣的。如何獲取當(dāng)前的播放時間:當(dāng)前幀的顯示時間戳 * 時基 + 額外的延遲時間,額外的延遲時間進(jìn)入repeat_pict就會發(fā)現(xiàn)官方已經(jīng)給我們了extra_delay = repeat_pict / (2*fps),轉(zhuǎn)化一下其實也就是我們代碼中的格式(因為fps = 1.0 / timeBase)。

2. 開始播放視頻

2.1 播放邏輯處理

dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, 0.1 * NSEC_PER_SEC);
    dispatch_after(popTime, dispatch_get_main_queue(), ^(void){
        [self tick];
    });
  • 上面代碼的意思就是通過GCD的方式延遲0.1秒之后再開始顯示,為什么要延遲0.1秒呢?因為這個一段代碼是跟在解碼視頻的后面的,解碼一幀視頻也是需要時間的,所以需要延遲0.1秒。那么為什么是0.1秒呢?朋友們是否還記得上一篇文章中NSArray * frames = [strongDecoder decodeFrames:0.1];,這里設(shè)置的最小的時間也是0.1秒,所以現(xiàn)在就可以共通了。

2.2 播放視頻

  • 做了這么多的鋪墊,終于輪到我們的主角出場了,當(dāng)當(dāng)當(dāng)。。。
- (void)tick
{
    // 返回當(dāng)前播放幀的播放時間
    CGFloat interval = [self presentFrame];
    const NSUInteger leftFrames =_videoFrames.count;
    
    // 當(dāng)_videoFrames中已經(jīng)沒有解碼過后的數(shù)據(jù) 或者剩余的時間小于_minBufferedDuration最小 就繼續(xù)解碼
    if (!leftFrames ||
        !(_bufferedDuration > _minBufferedDuration))  {
        [self asyncDecodeFrames];
    }
    
    // 播放完一幀之后 繼續(xù)播放下一幀 兩幀之間的播放間隔不能小于0.01秒
    const NSTimeInterval time = MAX(interval, 0.01);
    dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, time * NSEC_PER_SEC);
    dispatch_after(popTime, dispatch_get_main_queue(), ^{
        [self tick];
    });
 
}

2.2.1 繪制圖像

- (CGFloat)presentFrame
{
    CGFloat interval = 0;
    AieVideoFrame * frame;
    
    @synchronized (_videoFrames) {
        if (_videoFrames.count > 0) {
            frame = _videoFrames[0];
            [_videoFrames removeObjectAtIndex:0];
            _bufferedDuration -= frame.duration;
        }
    }
    
    if (frame) {
        if (_glView) {
            [_glView render:frame];
        }
        interval = frame.duration;
    }
    return interval;
}
  • @synchronized是一個互斥鎖,為了不讓其他的線程同時訪問鎖中的資源。
  • 線程里面的內(nèi)容就很簡單了,就是取出解碼后的數(shù)組的第一幀,然后從數(shù)組中刪除。
  • _bufferedDuration就是數(shù)組中的數(shù)據(jù)剩余的時間的總和,所以取出數(shù)據(jù)之后,需要把這一幀的時間減掉。
  • 如果第一幀存在,那就[_glView render:frame],把視頻幀繪制到屏幕上,這個函數(shù)涉及到OpenGL的很多知識,比較復(fù)雜,如果有朋友感興趣的話,以后可以單獨設(shè)一個模塊仔細(xì)的講一講。

2.2.2 再次開始解碼

   const NSUInteger leftFrames =_videoFrames.count;
    if (0 == leftFrames) {
        return;
    }
    if (!leftFrames ||
        !(_bufferedDuration > _minBufferedDuration))
    {
        [self asyncDecodeFrames];
    }
  • 當(dāng)_videoFrames中已經(jīng)沒有可以播放的數(shù)據(jù),說明視頻已經(jīng)播放完了,所以可以退出了,停止播放也可遵循一樣的原理。
  • 當(dāng)剩余的時間_bufferedDuration小于_minBufferedDuration時,那就繼續(xù)開始解碼。

2.2.3 播放下一幀

const NSTimeInterval time = MAX(interval, 0.01);
    dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, time * NSEC_PER_SEC);
    dispatch_after(popTime, dispatch_get_main_queue(), ^{
        [self tick];
    });
  • 這個其實是一個遞歸函數(shù),只是在中間加了一個正常的延時,兩幀之間的播放間隔不能小于0.01秒,這樣就可以達(dá)到我們看見的播放視頻的效果了。

結(jié)尾

  • 到這里我們關(guān)于最簡單的視頻播放器的內(nèi)容就全部結(jié)束了,其實,我只是在Kxmovie的基礎(chǔ)上,抽離出其中的核心代碼,然后組成這樣一系列的代碼。如果反應(yīng)好的話,我會繼續(xù)把剩下完整的部分也陸續(xù)給大家分享出來的,謝謝大家的支持。
  • 有興趣的朋友也可以仔細(xì)的去解讀Kxmovie的源碼,如果文章中有錯誤的地方還希望大佬們可以指出。
  • 由于放了FFmpeg庫,所以Demo會很大,下載的時候比較費時。
  • 謝謝閱讀
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容