如果說低斋,視頻的解碼是最核心的一步,那么視頻的顯示播放匪凡,就是最復雜的一步膊畴,也是最難的一步。
接著上一篇文章的激情病游,這一篇文章主要是講述解碼后的數(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
有兩個最重要的屬性data
和linesize
。
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會很大嗜愈,下載的時候比較費時旧蛾。
- 謝謝閱讀