如果說,視頻的解碼是最核心的一步,那么視頻的顯示播放,就是最復(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有兩個最重要的屬性data和linesize。
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會很大,下載的時候比較費時。
- 謝謝閱讀