如果說,視頻的解碼是最核心的一步,那么視頻的顯示播放,就是最復雜的一步,也是最難的一步。
接著上一篇文章的激情,這一篇文章主要是講述解碼后的數據是怎么有順序,有規律地顯示到我們的手機屏幕上的。
基于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
有兩個最重要的屬性data
和linesize
。
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會很大,下載的時候比較費時。
- 謝謝閱讀