最近在學習音視頻的相關(guān)知識葛碧,在接觸到ffmpeg庫后嘗試著使用其編寫了一個視頻播放器
音視頻解碼
視頻播放器播放一個互聯(lián)網(wǎng)上的視頻文件借杰,需要經(jīng)過以下幾個步驟:解協(xié)議,解封裝进泼,解碼視音頻蔗衡,視音頻同步。如果播放本地文件則不需要解協(xié)議乳绕,為以下幾個步驟:解封裝绞惦,解碼視音頻,視音頻同步洋措。他們的過程如圖所示济蝉。
本文示例使用的是本地視頻文件,對解碼過程中使用到的api不做過多講解菠发,具體的api介紹可以參考雷神的博客王滤,或者閱讀demo中“FFMpeg解碼中”解碼音頻、解碼視頻的文件滓鸠。解碼的步驟如下圖所示雁乡,新版的ffmpeg已經(jīng)不需要使用av_register_all(),圖片來源于網(wǎng)絡
解碼后得到的音頻數(shù)據(jù)采用AudioQueue進行播放糜俗,視頻數(shù)據(jù)使用OpenGL ES來進行展示蔗怠,具體可以參照文章末尾處的demo
關(guān)于音視頻的同步墩弯,有三種方式:
- 參考一個外部時鐘,將音頻與視頻同步至此時間
- 以視頻為基準寞射,音頻去同步視頻的時間
- 以音頻為基準渔工,視頻去同步音頻的時間
由于某些生物學的原理,人對聲音的變化比較敏感桥温,但是對視覺變化不太敏感引矩。所以頻繁的去調(diào)整聲音的播放會有些刺耳或者雜音吧影響用戶體驗,所以普遍使用第三種方式來做音視頻同步
音視頻編碼
音頻的錄制采用AudioUnit侵浸,音頻的編碼使用AudioConverterRef
//輸入
AudioBuffer encodeBuffer;
encodeBuffer.mNumberChannels = inBuffer->mNumberChannels;
encodeBuffer.mDataByteSize = (UInt32)bufferLengthPerConvert;
encodeBuffer.mData = current;
UInt32 packetPerConvert = PACKET_PER_CONVERT;
//輸出
AudioBufferList outputBuffers;
outputBuffers.mNumberBuffers = 1;
outputBuffers.mBuffers[0].mNumberChannels =inBuffer->mNumberChannels;
outputBuffers.mBuffers[0].mDataByteSize = outPacketLength*packetPerConvert;
outputBuffers.mBuffers[0].mData = _convertedDataBuf;
memset(_convertedDataBuf, 0, bufferLengthPerConvert);
OSStatus status = AudioConverterFillComplexBuffer(_audioConverter, convertDataProc, &encodeBuffer, &packetPerConvert, &outputBuffers, NULL);
if (status != noErr) {
NSLog(@"轉(zhuǎn)換出錯");
}
// TMSCheckStatusUnReturn(status, @"轉(zhuǎn)換出錯");
if (current == leftBuf) {
current = inBuffer->mData + bufferLengthPerConvert - lastLeftLength;
}else{
current += bufferLengthPerConvert;
}
leftLength -= bufferLengthPerConvert;
//輸出數(shù)據(jù)到下一個環(huán)節(jié)
// NSLog(@"output buffer size:%d",outputBuffers.mBuffers[0].mDataByteSize);
self.bufferData->bufferList = &outputBuffers;
self.bufferData->inNumberFrames = packetPerConvert*_outputDesc.mFramesPerPacket; //包數(shù) * 每個包的幀數(shù)(幀數(shù)+采樣率計算時長)
[self transportAudioBuffersToNext];
視頻的錄制采用AVCaptureSession旺韭,視頻的編碼使用ffmpeg
- (void)encoderToH264:(CMSampleBufferRef)sampleBuffer
{
// 1.通過CMSampleBufferRef對象獲取CVPixelBufferRef對象
CVPixelBufferRef imageBuffer = CMSampleBufferGetImageBuffer(sampleBuffer);
// 2.鎖定imageBuffer內(nèi)存地址開始進行編碼
if (CVPixelBufferLockBaseAddress(imageBuffer, 0) == kCVReturnSuccess) {
// 3.從CVPixelBufferRef讀取YUV的值
// NV12和NV21屬于YUV格式,是一種two-plane模式掏觉,即Y和UV分為兩個Plane区端,但是UV(CbCr)為交錯存儲,而不是分為三個plane
// 3.1.獲取Y分量的地址
UInt8 *bufferPtr = (UInt8 *)CVPixelBufferGetBaseAddressOfPlane(imageBuffer,0);
// 3.2.獲取UV分量的地址
UInt8 *bufferPtr1 = (UInt8 *)CVPixelBufferGetBaseAddressOfPlane(imageBuffer,1);
// 3.3.根據(jù)像素獲取圖片的真實寬度&高度
size_t width = CVPixelBufferGetWidth(imageBuffer);
size_t height = CVPixelBufferGetHeight(imageBuffer);
// 獲取Y分量長度
size_t bytesrow0 = CVPixelBufferGetBytesPerRowOfPlane(imageBuffer,0);
size_t bytesrow1 = CVPixelBufferGetBytesPerRowOfPlane(imageBuffer,1);
UInt8 *yuv420_data = (UInt8 *)malloc(width * height * 3 / 2);
// 3.4.將NV12數(shù)據(jù)轉(zhuǎn)成YUV420P(I420)數(shù)據(jù)
UInt8 *pY = bufferPtr;
UInt8 *pUV = bufferPtr1;
UInt8 *pU = yuv420_data + width * height;
UInt8 *pV = pU + width * height / 4;
for(int i =0;i<height;i++)
{
memcpy(yuv420_data+i*width,pY+i*bytesrow0,width);
}
for(int j = 0;j<height/2;j++)
{
for(int i =0;i<width/2;i++)
{
*(pU++) = pUV[i<<1];
*(pV++) = pUV[(i<<1) + 1];
}
pUV += bytesrow1;
}
// 3.5.分別讀取YUV的數(shù)據(jù)
picture_buf = yuv420_data;
pFrame->data[0] = picture_buf; // Y
pFrame->data[1] = picture_buf + y_size; // U
pFrame->data[2] = picture_buf + y_size * 5 / 4; // V
// 4.設置當前幀
pFrame->pts = framecnt;
// 4.設置寬度高度以及YUV格式
pFrame->width = encoder_h264_frame_width;
pFrame->height = encoder_h264_frame_height;
pFrame->format = AV_PIX_FMT_YUV420P;
// 5.對編碼前的原始數(shù)據(jù)(AVFormat)利用編碼器進行編碼澳腹,將 pFrame 編碼后的數(shù)據(jù)傳入pkt 中
int ret = avcodec_send_frame(pCodecCtx, pFrame);
if (ret != 0) {
printf("Failed to encode! \n");
return;
}
while (avcodec_receive_packet(pCodecCtx, &pkt) == 0) {
framecnt++;
pkt.stream_index = video_st->index;
//也可以使用C語言函數(shù):fwrite()织盼、fflush()寫文件和清空文件寫入緩沖區(qū)。
// ret = av_write_frame(pFormatCtx, &pkt);
fwrite(pkt.data, 1, pkt.size, file);
if (ret < 0) {
printf("Failed write to file酱塔!\n");
}
//釋放packet
av_packet_unref(&pkt);
}
// 7.釋放yuv數(shù)據(jù)
free(yuv420_data);
}
CVPixelBufferUnlockBaseAddress(imageBuffer, 0);
}
編碼后得到的h264文件通過H264BSAnalyzer解析發(fā)現(xiàn)沥邻,每個IDR幀之前都含有SPS和PPS,說明此種方式進行的編碼可用于網(wǎng)絡流的傳輸視頻封裝
本文示例將H264和AAC封裝成FLV羊娃,封裝流程示意圖如下唐全,具體代碼實現(xiàn)請參照文章末尾處demo
直播推流
推流:使用的是LFLiveKit三方庫
拉流:可以使用ijkplayer,也可以使用mac端的VLC播放器
服務器:nginx
具體的配置及使用可以參考這里
由于ffmpeg庫占用空間過大蕊玷,需自行引入方可運行
demo下載
參考文章:
雷神博客
https://github.com/czqasngit/ffmpeg-player
http://www.reibang.com/p/ba5045da282c