需求
解析文件中的音視頻流以解碼同步并將視頻渲染到屏幕上,音頻通過(guò)揚(yáng)聲器輸出.對(duì)于僅僅需要單純播放一個(gè)視頻文件可直接使用AVFoundation
中上層播放器,這里是用最底層的方式實(shí)現(xiàn),可獲取原始音視頻幀數(shù)據(jù).
實(shí)現(xiàn)原理
本文主要分為三大塊,解析模塊使用FFmpeg parse文件中的音視頻流,解碼模塊使用FFmpeg或蘋(píng)果原生解碼器解碼音視頻,渲染模塊使用OpenGL將視頻流渲染到屏幕,使用Audio Queue Player將音頻以揚(yáng)聲器形式輸出.
閱讀前提
注意: 本文涉及到的所有模塊具體實(shí)現(xiàn)均在如下鏈接中,可根據(jù)需求自行查看講解部分.
- 音視頻基礎(chǔ)
- iOS FFmpeg環(huán)境搭建
- FFmpeg解析視頻數(shù)據(jù)
- VideoToolbox實(shí)現(xiàn)視頻硬解碼
- Audio Converter音頻解碼
- FFmpeg音頻解碼
- FFmpeg視頻解碼
- OpenGL渲染視頻數(shù)據(jù)
- H.264,H.265碼流結(jié)構(gòu)
- 傳輸音頻數(shù)據(jù)隊(duì)列實(shí)現(xiàn)
- Audio Queue 播放器
代碼地址 : iOS File Player
掘金地址 : iOS File Player
簡(jiǎn)書(shū)地址 : iOS File Player
博客地址 : iOS File Player
總體架構(gòu)
本文以解碼一個(gè).MOV媒體文件為例, 該文件中包含H.264編碼的視頻數(shù)據(jù), AAC編碼的音頻數(shù)據(jù),首先要通過(guò)FFmpeg去parse文件中的音視頻流信息,parse出來(lái)的結(jié)果保存在AVPacket
結(jié)構(gòu)體中,然后分別提取音視頻幀數(shù)據(jù),音頻幀通過(guò)FFmpeg解碼器或蘋(píng)果原生框架中的Audio Converter進(jìn)行解碼,視頻通過(guò)FFmpeg或蘋(píng)果原生框架VideoToolbox中的解碼器可將數(shù)據(jù)解碼,解碼后的音頻數(shù)據(jù)格式為PCM,解碼后的視頻數(shù)據(jù)格式為YUV原始數(shù)據(jù),根據(jù)時(shí)間戳對(duì)音視頻數(shù)據(jù)進(jìn)行同步,最后將PCM數(shù)據(jù)音頻傳給Audio Queue以實(shí)現(xiàn)音頻的播放,將YUV視頻原始數(shù)據(jù)封裝為CMSampleBufferRef
數(shù)據(jù)結(jié)構(gòu)并傳給OpenGL以將視頻渲染到屏幕上,至此一個(gè)完整拉取文件視頻流的操作完成.
注意: 通過(guò)網(wǎng)址拉取一個(gè)RTMP流進(jìn)行解碼播放的流程與拉取文件流基本相同, 只是需要通過(guò)socket接收音視頻數(shù)據(jù)后再完成解碼及后續(xù)流程.
簡(jiǎn)易流程
Parse
- 創(chuàng)建
AVFormatContext
上下文對(duì)象:AVFormatContext *avformat_alloc_context(void);
- 從文件中獲取上下文對(duì)象并賦值給指定對(duì)象:
int avformat_open_input(AVFormatContext **ps, const char *url, AVInputFormat *fmt, AVDictionary **options)
- 讀取文件中的流信息:
int avformat_find_stream_info(AVFormatContext *ic, AVDictionary **options);
- 獲取文件中音視頻流:
m_formatContext->streams[audio/video index]e
- 開(kāi)始parse以獲取文件中視頻幀幀:
int av_read_frame(AVFormatContext *s, AVPacket *pkt);
- 如果是視頻幀通過(guò)
av_bitstream_filter_filter
生成sps,pps等關(guān)鍵信息. - 讀取到的
AVPacket
即包含文件中所有的音視頻壓縮數(shù)據(jù).
解碼
通過(guò)FFmpeg解碼
- 獲取文件流的解碼器上下文:
formatContext->streams[a/v index]->codec;
- 通過(guò)解碼器上下文找到解碼器:
AVCodec *avcodec_find_decoder(enum AVCodecID id);
- 打開(kāi)解碼器:
int avcodec_open2(AVCodecContext *avctx, const AVCodec *codec, AVDictionary **options);
- 將文件中音視頻數(shù)據(jù)發(fā)送給解碼器:
int avcodec_send_packet(AVCodecContext *avctx, const AVPacket *avpkt);
- 循環(huán)接收解碼后的音視頻數(shù)據(jù):
int avcodec_receive_frame(AVCodecContext *avctx, AVFrame *frame);
- 如果是音頻數(shù)據(jù)可能需要重新采樣以便轉(zhuǎn)成設(shè)備支持的格式播放.(借助
SwrContext
)
通過(guò)VideoToolbox解碼視頻
- 將從FFmpeg中parse到的extra data中分離提取中NALU頭關(guān)鍵信息sps,pps等
- 通過(guò)上面提取的關(guān)鍵信息創(chuàng)建視頻描述信息:
CMVideoFormatDescriptionRef
,CMVideoFormatDescriptionCreateFromH264ParameterSets / CMVideoFormatDescriptionCreateFromHEVCParameterSets
- 創(chuàng)建解碼器:
VTDecompressionSessionCreate
,并指定一系列相關(guān)參數(shù). - 將壓縮數(shù)據(jù)放入CMBlockBufferRef中:
CMBlockBufferCreateWithMemoryBlock
- 開(kāi)始解碼:
VTDecompressionSessionDecodeFrame
- 在回調(diào)中接收解碼后的視頻數(shù)據(jù)
通過(guò)AudioConvert解碼音頻
- 通過(guò)原始數(shù)據(jù)與解碼后數(shù)據(jù)格式的ASBD結(jié)構(gòu)體創(chuàng)建解碼器:
AudioConverterNewSpecific
- 指定解碼器類(lèi)型
AudioClassDescription
- 開(kāi)始解碼:
AudioConverterFillComplexBuffer
- 注意: 解碼的前提是每次需要有1024個(gè)采樣點(diǎn)才能完成一次解碼操作.
同步
因?yàn)檫@里解碼的是本地文件中的音視頻, 也就是說(shuō)只要本地文件中音視頻的時(shí)間戳打的完全正確,我們解碼出來(lái)的數(shù)據(jù)是可以直接播放以實(shí)現(xiàn)同步的效果.而我們要做的僅僅是保證音視頻解碼后同時(shí)渲染.
注意: 比如通過(guò)一個(gè)RTMP地址拉取的流因?yàn)榇嬖诰W(wǎng)絡(luò)原因可能造成某個(gè)時(shí)間段數(shù)據(jù)丟失,造成音視頻不同步,所以需要有一套機(jī)制來(lái)糾正時(shí)間戳.大體機(jī)制即為視頻追趕音頻,后面會(huì)有文件專門(mén)介紹,這里不作過(guò)多說(shuō)明.
渲染
通過(guò)上面的步驟獲取到的視頻原始數(shù)據(jù)即可通過(guò)封裝好的OpenGL ES直接渲染到屏幕上,蘋(píng)果原生框架中也有GLKViewController
可以完成屏幕渲染.音頻這里通過(guò)Audio Queue接收音頻幀數(shù)據(jù)以完成播放.
文件結(jié)構(gòu)
快速使用
使用FFmpeg解碼
首先根據(jù)文件地址初始化FFmpeg以實(shí)現(xiàn)parse音視頻流.然后利用FFmpeg中的解碼器解碼音視頻數(shù)據(jù),這里需要注意的是,我們將從讀取到的第一個(gè)I幀開(kāi)始作為起點(diǎn),以實(shí)現(xiàn)音視頻同步.解碼后的音頻要先裝入傳輸隊(duì)列中,因?yàn)閍udio queue player設(shè)計(jì)模式是不斷從傳輸隊(duì)列中取數(shù)據(jù)以實(shí)現(xiàn)播放.視頻數(shù)據(jù)即可直接進(jìn)行渲染.
- (void)startRenderAVByFFmpegWithFileName:(NSString *)fileName {
NSString *path = [[NSBundle mainBundle] pathForResource:fileName ofType:@"MOV"];
XDXAVParseHandler *parseHandler = [[XDXAVParseHandler alloc] initWithPath:path];
XDXFFmpegVideoDecoder *videoDecoder = [[XDXFFmpegVideoDecoder alloc] initWithFormatContext:[parseHandler getFormatContext] videoStreamIndex:[parseHandler getVideoStreamIndex]];
videoDecoder.delegate = self;
XDXFFmpegAudioDecoder *audioDecoder = [[XDXFFmpegAudioDecoder alloc] initWithFormatContext:[parseHandler getFormatContext] audioStreamIndex:[parseHandler getAudioStreamIndex]];
audioDecoder.delegate = self;
static BOOL isFindIDR = NO;
[parseHandler startParseGetAVPackeWithCompletionHandler:^(BOOL isVideoFrame, BOOL isFinish, AVPacket packet) {
if (isFinish) {
isFindIDR = NO;
[videoDecoder stopDecoder];
[audioDecoder stopDecoder];
dispatch_async(dispatch_get_main_queue(), ^{
self.startWorkBtn.hidden = NO;
});
return;
}
if (isVideoFrame) { // Video
if (packet.flags == 1 && isFindIDR == NO) {
isFindIDR = YES;
}
if (!isFindIDR) {
return;
}
[videoDecoder startDecodeVideoDataWithAVPacket:packet];
}else { // Audio
[audioDecoder startDecodeAudioDataWithAVPacket:packet];
}
}];
}
-(void)getDecodeVideoDataByFFmpeg:(CMSampleBufferRef)sampleBuffer {
CVPixelBufferRef pix = CMSampleBufferGetImageBuffer(sampleBuffer);
[self.previewView displayPixelBuffer:pix];
}
- (void)getDecodeAudioDataByFFmpeg:(void *)data size:(int)size pts:(int64_t)pts isFirstFrame:(BOOL)isFirstFrame {
// NSLog(@"demon test - %d",size);
// Put audio data from audio file into audio data queue
[self addBufferToWorkQueueWithAudioData:data size:size pts:pts];
// control rate
usleep(14.5*1000);
}
使用原生框架解碼
首先根據(jù)文件地址初始化FFmpeg以實(shí)現(xiàn)parse音視頻流.這里首先根據(jù)文件中實(shí)際的音頻流數(shù)據(jù)構(gòu)造ASBD結(jié)構(gòu)體以初始化音頻解碼器,然后將解碼后的音視頻數(shù)據(jù)分別渲染即可.這里需要注意的是,如果要拉取的文件視頻是H.265編碼格式的,解碼出來(lái)的數(shù)據(jù)的因?yàn)楹蠦幀所以時(shí)間戳是亂序的,我們需要借助一個(gè)鏈表對(duì)其排序,然后再將排序后的數(shù)據(jù)渲染到屏幕上.
- (void)startRenderAVByOriginWithFileName:(NSString *)fileName {
NSString *path = [[NSBundle mainBundle] pathForResource:fileName ofType:@"MOV"];
XDXAVParseHandler *parseHandler = [[XDXAVParseHandler alloc] initWithPath:path];
XDXVideoDecoder *videoDecoder = [[XDXVideoDecoder alloc] init];
videoDecoder.delegate = self;
// Origin file aac format
AudioStreamBasicDescription audioFormat = {
.mSampleRate = 48000,
.mFormatID = kAudioFormatMPEG4AAC,
.mChannelsPerFrame = 2,
.mFramesPerPacket = 1024,
};
XDXAduioDecoder *audioDecoder = [[XDXAduioDecoder alloc] initWithSourceFormat:audioFormat
destFormatID:kAudioFormatLinearPCM
sampleRate:48000
isUseHardwareDecode:YES];
[parseHandler startParseWithCompletionHandler:^(BOOL isVideoFrame, BOOL isFinish, struct XDXParseVideoDataInfo *videoInfo, struct XDXParseAudioDataInfo *audioInfo) {
if (isFinish) {
[videoDecoder stopDecoder];
[audioDecoder freeDecoder];
dispatch_async(dispatch_get_main_queue(), ^{
self.startWorkBtn.hidden = NO;
});
return;
}
if (isVideoFrame) {
[videoDecoder startDecodeVideoData:videoInfo];
}else {
[audioDecoder decodeAudioWithSourceBuffer:audioInfo->data
sourceBufferSize:audioInfo->dataSize
completeHandler:^(AudioBufferList * _Nonnull destBufferList, UInt32 outputPackets, AudioStreamPacketDescription * _Nonnull outputPacketDescriptions) {
// Put audio data from audio file into audio data queue
[self addBufferToWorkQueueWithAudioData:destBufferList->mBuffers->mData size:destBufferList->mBuffers->mDataByteSize pts:audioInfo->pts];
// control rate
usleep(16.8*1000);
}];
}
}];
}
- (void)getVideoDecodeDataCallback:(CMSampleBufferRef)sampleBuffer isFirstFrame:(BOOL)isFirstFrame {
if (self.hasBFrame) {
// Note : the first frame not need to sort.
if (isFirstFrame) {
CVPixelBufferRef pix = CMSampleBufferGetImageBuffer(sampleBuffer);
[self.previewView displayPixelBuffer:pix];
return;
}
[self.sortHandler addDataToLinkList:sampleBuffer];
}else {
CVPixelBufferRef pix = CMSampleBufferGetImageBuffer(sampleBuffer);
[self.previewView displayPixelBuffer:pix];
}
}
#pragma mark - Sort Callback
- (void)getSortedVideoNode:(CMSampleBufferRef)sampleBuffer {
int64_t pts = (int64_t)(CMTimeGetSeconds(CMSampleBufferGetPresentationTimeStamp(sampleBuffer)) * 1000);
static int64_t lastpts = 0;
// NSLog(@"Test marigin - %lld",pts - lastpts);
lastpts = pts;
[self.previewView displayPixelBuffer:CMSampleBufferGetImageBuffer(sampleBuffer)];
}
具體實(shí)現(xiàn)
本文中每一部分的具體實(shí)現(xiàn)均有詳細(xì)介紹, 如需幫助請(qǐng)參考閱讀前提中附帶的鏈接地址.
注意
因?yàn)椴煌募袎嚎s的音視頻數(shù)據(jù)格式不同,這里僅僅兼容部分格式,可自定義進(jìn)行擴(kuò)展.