最近在学习音视频的相关知识,在接触到ffmpeg库后尝试着使用其编写了一个视频播放器
音视频解码
视频播放器播放一个互联网上的视频文件,需要经过以下几个步骤:解协议,解封装,解码视音频,视音频同步。如果播放本地文件则不需要解协议,为以下几个步骤:解封装,解码视音频,视音频同步。他们的过程如图所示。
本文示例使用的是本地视频文件,对解码过程中使用到的api不做过多讲解,具体的api介绍可以参考雷神的博客,或者阅读demo中“FFMpeg解码中”解码音频、解码视频的文件。解码的步骤如下图所示,新版的ffmpeg已经不需要使用av_register_all(),图片来源于网络
解码后得到的音频数据采用AudioQueue进行播放,视频数据使用OpenGL ES来进行展示,具体可以参照文章末尾处的demo
关于音视频的同步,有三种方式:
- 参考一个外部时钟,将音频与视频同步至此时间
- 以视频为基准,音频去同步视频的时间
- 以音频为基准,视频去同步音频的时间
由于某些生物学的原理,人对声音的变化比较敏感,但是对视觉变化不太敏感。所以频繁的去调整声音的播放会有些刺耳或者杂音吧影响用户体验,所以普遍使用第三种方式来做音视频同步
音视频编码
音频的录制采用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(@"转换出错");
}
// TMSCheckStatusUnReturn(status, @"转换出错");
if (current == leftBuf) {
current = inBuffer->mData + bufferLengthPerConvert - lastLeftLength;
}else{
current += bufferLengthPerConvert;
}
leftLength -= bufferLengthPerConvert;
//输出数据到下一个环节
// NSLog(@"output buffer size:%d",outputBuffers.mBuffers[0].mDataByteSize);
self.bufferData->bufferList = &outputBuffers;
self.bufferData->inNumberFrames = packetPerConvert*_outputDesc.mFramesPerPacket; //包数 * 每个包的帧数(帧数+采样率计算时长)
[self transportAudioBuffersToNext];
视频的录制采用AVCaptureSession,视频的编码使用ffmpeg
- (void)encoderToH264:(CMSampleBufferRef)sampleBuffer
{
// 1.通过CMSampleBufferRef对象获取CVPixelBufferRef对象
CVPixelBufferRef imageBuffer = CMSampleBufferGetImageBuffer(sampleBuffer);
// 2.锁定imageBuffer内存地址开始进行编码
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.根据像素获取图片的真实宽度&高度
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数据转成YUV420P(I420)数据
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的数据
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.对编码前的原始数据(AVFormat)利用编码器进行编码,将 pFrame 编码后的数据传入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语言函数:fwrite()、fflush()写文件和清空文件写入缓冲区。
// 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数据
free(yuv420_data);
}
CVPixelBufferUnlockBaseAddress(imageBuffer, 0);
}
编码后得到的h264文件通过H264BSAnalyzer解析发现,每个IDR帧之前都含有SPS和PPS,说明此种方式进行的编码可用于网络流的传输视频封装
本文示例将H264和AAC封装成FLV,封装流程示意图如下,具体代码实现请参照文章末尾处demo
直播推流
推流:使用的是LFLiveKit三方库
拉流:可以使用ijkplayer,也可以使用mac端的VLC播放器
服务器:nginx
具体的配置及使用可以参考这里
由于ffmpeg库占用空间过大,需自行引入方可运行
demo下载
参考文章:
雷神博客
https://github.com/czqasngit/ffmpeg-player
https://www.jianshu.com/p/ba5045da282c