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

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

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

正式開始

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

1.準備活動

1.1 初始化OpenGL的類

  • 接下來我們使用AieGLView類,都是仿照自Kxmovie中的 KxMovieGLView類。里面的具體實現內容比較多,以后我們單獨分出一個模塊來講。
- (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 處理解碼后的數據

  • 這里就是上一篇文章中,解碼結束之后,應該對數據做的處理。
- (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;
    // 以流中的時間為基礎 預估的時間戳
    frame.position = av_frame_get_best_effort_timestamp(_videoFrame) * _videoTimeBase;
    
    // 獲取當前幀的持續時間
    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數據分析

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

1.2.2 把數據封裝成自己的格式

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是我們自己定義的簡單的類,不懂的可以去看代碼,結構很簡單。現在我們只考慮YUV的存儲,暫時不考慮RGB。
  • 上面的luma, chromaB,chromaR正好對應的YUV,從傳進去的參數就可以發現。
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中的一行的數據大小,一般情況下比實際寬度大一點,但是為了避免特殊情況,這里還是需要判斷一下,取最小的那個。
  • 下面就是把數據裝到NSMutableData這個容器中,顯而易見,數據的總大小就是width * height。所以遍歷的時候就遍歷它的height,然后把整個寬度的數據全部拷貝到目標容器中,由于這里是指針操作,所以我們需要把指針往后便宜到末尾,下一次拷貝的時候才可以繼續從末尾添加數據。
  • 有的朋友可能會問,為什么dst偏移的是width, 但是src偏移的是linesize?理由還是之前的那一個linesize可能會比width大一點,我們的最終數據dst應該根據width來計算,但是src(就是之前的data)他的每一行實際大小是linesize,所以才需要分開來偏移。

1.2.3 解碼后數據的信息

    frame.width = _videoCodecCtx->width;
    frame.height = _videoCodecCtx->height;
    // 以流中的時間為基礎 預估的時間戳
    frame.position = av_frame_get_best_effort_timestamp(_videoFrame) * _videoTimeBase;
    
    // 獲取當前幀的持續時間
    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中的時間為基礎,預估的時間戳,然后乘以_videoTimeBase(之前默認是0.25的那個),就是這個視頻幀當先的時間位置。
  • av_frame_get_pkt_duration ()是獲取當前幀的持續時間。
  • 接下來的一個if語句很刁鉆,我也不是很理解,但是查了資料,大致是這樣的。如何獲取當前的播放時間:當前幀的顯示時間戳 * 時基 + 額外的延遲時間,額外的延遲時間進入repeat_pict就會發現官方已經給我們了extra_delay = repeat_pict / (2*fps),轉化一下其實也就是我們代碼中的格式(因為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];,這里設置的最小的時間也是0.1秒,所以現在就可以共通了。

2.2 播放視頻

  • 做了這么多的鋪墊,終于輪到我們的主角出場了,當當當。。。
- (void)tick
{
    // 返回當前播放幀的播放時間
    CGFloat interval = [self presentFrame];
    const NSUInteger leftFrames =_videoFrames.count;
    
    // 當_videoFrames中已經沒有解碼過后的數據 或者剩余的時間小于_minBufferedDuration最小 就繼續解碼
    if (!leftFrames ||
        !(_bufferedDuration > _minBufferedDuration))  {
        [self asyncDecodeFrames];
    }
    
    // 播放完一幀之后 繼續播放下一幀 兩幀之間的播放間隔不能小于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是一個互斥鎖,為了不讓其他的線程同時訪問鎖中的資源。
  • 線程里面的內容就很簡單了,就是取出解碼后的數組的第一幀,然后從數組中刪除。
  • _bufferedDuration就是數組中的數據剩余的時間的總和,所以取出數據之后,需要把這一幀的時間減掉。
  • 如果第一幀存在,那就[_glView render:frame],把視頻幀繪制到屏幕上,這個函數涉及到OpenGL的很多知識,比較復雜,如果有朋友感興趣的話,以后可以單獨設一個模塊仔細的講一講。

2.2.2 再次開始解碼

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

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];
    });
  • 這個其實是一個遞歸函數,只是在中間加了一個正常的延時,兩幀之間的播放間隔不能小于0.01秒,這樣就可以達到我們看見的播放視頻的效果了。

結尾

  • 到這里我們關于最簡單的視頻播放器的內容就全部結束了,其實,我只是在Kxmovie的基礎上,抽離出其中的核心代碼,然后組成這樣一系列的代碼。如果反應好的話,我會繼續把剩下完整的部分也陸續給大家分享出來的,謝謝大家的支持。
  • 有興趣的朋友也可以仔細的去解讀Kxmovie的源碼,如果文章中有錯誤的地方還希望大佬們可以指出。
  • 由于放了FFmpeg庫,所以Demo會很大,下載的時候比較費時。
  • 謝謝閱讀
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容