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

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

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

正式開始

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

1.準備活動

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;
    
    // 獲取當前幀的持續(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樟遣,順便說一下而叼,蘋果手機錄音出來的原始數(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)很簡單∪欢簦現(xiàn)在我們只考慮YUV的存儲贫途,暫時不考慮RGB。
  • 上面的luma待侵, chromaB丢早,chromaR正好對應(yīng)的YUV,從傳進去的參數(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ù)全部拷貝到目標容器中料身,由于這里是指針操作,所以我們需要把指針往后便宜到末尾鹤啡,下一次拷貝的時候才可以繼續(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;
    
    // 獲取當前幀的持續(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(之前默認是0.25的那個)傲宜,就是這個視頻幀當先的時間位置。
  • av_frame_get_pkt_duration ()是獲取當前幀的持續(xù)時間夫啊。
  • 接下來的一個if語句很刁鉆函卒,我也不是很理解,但是查了資料撇眯,大致是這樣的报嵌。如何獲取當前的播放時間:當前幀的顯示時間戳 * 時基 + 額外的延遲時間,額外的延遲時間進入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 播放視頻

  • 做了這么多的鋪墊,終于輪到我們的主角出場了休蟹,當當當沸枯。。赂弓。
- (void)tick
{
    // 返回當前播放幀的播放時間
    CGFloat interval = [self presentFrame];
    const NSUInteger leftFrames =_videoFrames.count;
    
    // 當_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的很多知識车柠,比較復雜剔氏,如果有朋友感興趣的話,以后可以單獨設(shè)一個模塊仔細的講一講竹祷。

2.2.2 再次開始解碼

   const NSUInteger leftFrames =_videoFrames.count;
    if (0 == leftFrames) {
        return;
    }
    if (!leftFrames ||
        !(_bufferedDuration > _minBufferedDuration))
    {
        [self asyncDecodeFrames];
    }
  • _videoFrames中已經(jīng)沒有可以播放的數(shù)據(jù)谈跛,說明視頻已經(jī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秒彭则,這樣就可以達到我們看見的播放視頻的效果了鳍刷。

結(jié)尾

  • 到這里我們關(guān)于最簡單的視頻播放器的內(nèi)容就全部結(jié)束了,其實俯抖,我只是在Kxmovie的基礎(chǔ)上输瓜,抽離出其中的核心代碼,然后組成這樣一系列的代碼。如果反應(yīng)好的話尤揣,我會繼續(xù)把剩下完整的部分也陸續(xù)給大家分享出來的搔啊,謝謝大家的支持。
  • 有興趣的朋友也可以仔細的去解讀Kxmovie的源碼北戏,如果文章中有錯誤的地方還希望大佬們可以指出负芋。
  • 由于放了FFmpeg庫,所以Demo會很大嗜愈,下載的時候比較費時旧蛾。
  • 謝謝閱讀
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市蠕嫁,隨后出現(xiàn)的幾起案子锨天,更是在濱河造成了極大的恐慌,老刑警劉巖剃毒,帶你破解...
    沈念sama閱讀 216,372評論 6 498
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件病袄,死亡現(xiàn)場離奇詭異,居然都是意外死亡赘阀,警方通過查閱死者的電腦和手機益缠,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,368評論 3 392
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來纤壁,“玉大人左刽,你說我怎么就攤上這事∽妹剑” “怎么了欠痴?”我有些...
    開封第一講書人閱讀 162,415評論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長秒咨。 經(jīng)常有香客問我喇辽,道長,這世上最難降的妖魔是什么雨席? 我笑而不...
    開封第一講書人閱讀 58,157評論 1 292
  • 正文 為了忘掉前任菩咨,我火速辦了婚禮,結(jié)果婚禮上陡厘,老公的妹妹穿的比我還像新娘抽米。我一直安慰自己,他們只是感情好糙置,可當我...
    茶點故事閱讀 67,171評論 6 388
  • 文/花漫 我一把揭開白布云茸。 她就那樣靜靜地躺著,像睡著了一般谤饭。 火紅的嫁衣襯著肌膚如雪标捺。 梳的紋絲不亂的頭發(fā)上懊纳,一...
    開封第一講書人閱讀 51,125評論 1 297
  • 那天,我揣著相機與錄音亡容,去河邊找鬼嗤疯。 笑死,一個胖子當著我的面吹牛闺兢,可吹牛的內(nèi)容都是我干的茂缚。 我是一名探鬼主播,決...
    沈念sama閱讀 40,028評論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼屋谭,長吁一口氣:“原來是場噩夢啊……” “哼阱佛!你這毒婦竟也來了硕淑?” 一聲冷哼從身側(cè)響起咕别,我...
    開封第一講書人閱讀 38,887評論 0 274
  • 序言:老撾萬榮一對情侶失蹤呻畸,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后所意,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,310評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡催首,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,533評論 2 332
  • 正文 我和宋清朗相戀三年扶踊,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片郎任。...
    茶點故事閱讀 39,690評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡秧耗,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出舶治,到底是詐尸還是另有隱情分井,我是刑警寧澤,帶...
    沈念sama閱讀 35,411評論 5 343
  • 正文 年R本政府宣布霉猛,位于F島的核電站尺锚,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏惜浅。R本人自食惡果不足惜瘫辩,卻給世界環(huán)境...
    茶點故事閱讀 41,004評論 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望坛悉。 院中可真熱鬧伐厌,春花似錦、人聲如沸裸影。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,659評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽空民。三九已至刃唐,卻和暖如春羞迷,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背画饥。 一陣腳步聲響...
    開封第一講書人閱讀 32,812評論 1 268
  • 我被黑心中介騙來泰國打工衔瓮, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人抖甘。 一個月前我還...
    沈念sama閱讀 47,693評論 2 368
  • 正文 我出身青樓热鞍,卻偏偏與公主長得像,于是被迫代替她去往敵國和親衔彻。 傳聞我的和親對象是個殘疾皇子薇宠,可洞房花燭夜當晚...
    茶點故事閱讀 44,577評論 2 353

推薦閱讀更多精彩內(nèi)容