音视频一些笔记
- 序列参数集SPS:作用于一系列连续的编码图像;
- 图像参数集PPS:作用于编码视频序列中一个或多个独立的图像;
1.调用VTCompressionSessionCreate创建编码session,然后调用VTSessionSetProperty设置参数,最后调用VTCompressionSessionPrepareToEncodeFrames开始编码;
int width = 480, height = 640;
OSStatus status = VTCompressionSessionCreate(NULL, width, height, kCMVideoCodecType_H264, NULL, NULL, NULL, didCompressH264, (__bridge void *)(self), &EncodingSession);
2.调用VTCompressionSessionEncodeFrame传入需要编码的视频帧,如果返回失败,调用VTCompressionSessionInvalidate销毁session,然后释放session;
CVImageBufferRef imageBuffer = (CVImageBufferRef)CMSampleBufferGetImageBuffer(sampleBuffer);
// 帧时间,如果不设置会导致时间轴过长。
CMTime presentationTimeStamp = CMTimeMake(frameID++, 1000);
VTEncodeInfoFlags flags;
OSStatus statusCode = VTCompressionSessionEncodeFrame(EncodingSession,
imageBuffer,
presentationTimeStamp,
kCMTimeInvalid,
NULL, NULL, &flags);
3、每一帧视频编码完成后会调用预先设置的编码函数didCompressH264,如果是关键帧需要用CMSampleBufferGetFormatDescription获取CMFormatDescriptionRef,然后用CMVideoFormatDescriptionGetH264ParameterSetAtIndex取得PPS和SPS;最后把每一帧的所有NALU数据前四个字节变成0x00 00 00 01之后再写入文件。
OSStatus statusCode = CMVideoFormatDescriptionGetH264ParameterSetAtIndex(format, 1, &pparameterSet, &pparameterSetSize, &pparameterSetCount, 0 );
if (statusCode == noErr)
{
// Found pps
NSData *sps = [NSData dataWithBytes:sparameterSet length:sparameterSetSize];
NSData *pps = [NSData dataWithBytes:pparameterSet length:pparameterSetSize];
if (encoder)
{
[encoder gotSpsPps:sps pps:pps];
}
}
4、调用VTCompressionSessionCompleteFrames完成编码,然后销毁session:VTCompressionSessionInvalidate,释放session。
把原始码流包装成CMSampleBuffer
1.用CMBlockBuffer把NALUnit包装起来
CMBlockBufferRef blockBuffer = NULL;
OSStatus status = CMBlockBufferCreateWithMemoryBlock(kCFAllocatorDefault,
(void*)packetBuffer, packetSize,
kCFAllocatorNull,
NULL, 0, packetSize,
0, &blockBuffer);
2、把SPS和PPS包装成CMVideoFormatDescription;
const uint8_t* parameterSetPointers[2] = {mSPS, mPPS};
const size_t parameterSetSizes[2] = {mSPSSize, mPPSSize};
OSStatus status = CMVideoFormatDescriptionCreateFromH264ParameterSets(kCFAllocatorDefault 2, //param count parameterSetPointers, parameterSetSizes, 4, //nal start code size &mFormatDescription);
3.创建CMSampleBuffer;
CMSampleBufferRef sampleBuffer = NULL;
const size_t sampleSizeArray[] = {packetSize};
status = CMSampleBufferCreateReady(kCFAllocatorDefault,blockBuffer,
mFormatDescription,
1, 0, NULL, 1, sampleSizeArray,
&sampleBuffer);
解码并显示
1、传入CMSampleBuffer
VTDecodeFrameFlags flags = 0;
VTDecodeInfoFlags flagOut = 0;
// 默认是同步操作。
// 调用didDecompress,返回后再回调
OSStatus decodeStatus = VTDecompressionSessionDecodeFrame(mDecodeSession,
sampleBuffer,
flags,
&outputPixelBuffer,
&flagOut);
2、回调didDecompress
void didDecompress(void *decompressionOutputRefCon, void *sourceFrameRefCon, OSStatus status, VTDecodeInfoFlags infoFlags, CVImageBufferRef pixelBuffer, CMTime presentationTimeStamp, CMTime presentationDuration ){
CVPixelBufferRef *outputPixelBuffer = (CVPixelBufferRef *)sourceFrameRefCon;
*outputPixelBuffer = CVPixelBufferRetain(pixelBuffer);
}
3、显示解码的结果
[self.mOpenGLView displayPixelBuffer:pixelBuffer];
当遇到IDR帧时,更合适的做法是通过
VTDecompressionSessionCanAcceptFormatDescription判断原来的session是否能接受新的SPS和PPS,如果不能再新建session。
PCM通过抽样、量化、编码三个步骤将连续变化的模拟信号转换为数字编码。
- 抽样:对模拟信号进行周期性扫描,把时间上连续的信号变成时间上离散的信号;
- 量化:用一组规定的电平,把瞬时抽样值用最接近的电平值来表示,通常是用二进制表示;
- 编码:用一组二进制码组来表示每一个有固定电平的量化值;
iOS上把PCM音频编码成AAC音频流
- 1、设置编码器(codec),并开始录制;
- 2、收集到PCM数据,传给编码器;
- 3、编码完成回调callback,写入文件。
1、创建并配置AVCaptureSession
-
(void)startCapture { self.mCaptureSession = [[AVCaptureSession alloc] init]; mCaptureQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); mEncodeQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); AVCaptureDevice *audioDevice = [[AVCaptureDevice devicesWithMediaType:AVMediaTypeAudio] lastObject]; self.mCaptureAudioDeviceInput = [[AVCaptureDeviceInput alloc]initWithDevice:audioDevice error:nil]; if ([self.mCaptureSession canAddInput:self.mCaptureAudioDeviceInput]) { [self.mCaptureSession addInput:self.mCaptureAudioDeviceInput]; } self.mCaptureAudioOutput = [[AVCaptureAudioDataOutput alloc] init]; if ([self.mCaptureSession canAddOutput:self.mCaptureAudioOutput]) { [self.mCaptureSession addOutput:self.mCaptureAudioOutput]; } [self.mCaptureAudioOutput setSampleBufferDelegate:self queue:mCaptureQueue]; NSString *audioFile = [[NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) lastObject] stringByAppendingPathComponent:@"abc.aac"]; [[NSFileManager defaultManager] removeItemAtPath:audioFile error:nil]; [[NSFileManager defaultManager] createFileAtPath:audioFile contents:nil attributes:nil]; audioFileHandle = [NSFileHandle fileHandleForWritingAtPath:audioFile]; [self.mCaptureSession startRunning]; }
2、创建转换器
AudioStreamBasicDescription是输出流的结构体描述,
配置好outAudioStreamBasicDescription后,
根据AudioClassDescription(编码器),
调用AudioConverterNewSpecific创建转换器。
(void) setupEncoderFromSampleBuffer:(CMSampleBufferRef)sampleBuffer {
AudioStreamBasicDescription inAudioStreamBasicDescription = *CMAudioFormatDescriptionGetStreamBasicDescription((CMAudioFormatDescriptionRef)CMSampleBufferGetFormatDescription(sampleBuffer));AudioClassDescription *description = [self getAudioClassDescriptionWithType:kAudioFormatMPEG4AAC fromManufacturer:kAppleSoftwareAudioCodecManufacturer]; //软编 OSStatus status = AudioConverterNewSpecific(&inAudioStreamBasicDescription, &outAudioStreamBasicDescription, 1, description, &_audioConverter); // 创建转换器 }
编解码器(codec)指的是一个能够对一个信号或者一个数据流进行变换的设备或者程序。这里指的变换既包括将 信号或者数据流进行编码(通常是为了传输、存储或者加密)或者提取得到一个编码流的操作,也包括为了观察或者处理从这个编码流中恢复适合观察或操作的形式的操作。编解码器经常用在视频会议和流媒体等应用中。
3、获取到PCM数据并传入编码器
用CMSampleBufferGetDataBuffer获取到CMSampleBufferRef里面的CMBlockBufferRef,
再通过CMBlockBufferGetDataPointer获取到pcmBufferSize和pcmBuffer;
调用AudioConverterFillComplexBuffer传入数据,并在callBack函数调用填充buffer的方法。
CMBlockBufferRef blockBuffer = CMSampleBufferGetDataBuffer(sampleBuffer);
CFRetain(blockBuffer);
OSStatus status = CMBlockBufferGetDataPointer(blockBuffer, 0, NULL, &_pcmBufferSize, &_pcmBuffer);
status = AudioConverterFillComplexBuffer(_audioConverter, inInputDataProc, (__bridge void *)(self), &ioOutputDataPacketSize, &outAudioBufferList, outPacketDescription);
-
*/
OSStatus inInputDataProc(AudioConverterRef inAudioConverter, UInt32 *ioNumberDataPackets, AudioBufferList *ioData, AudioStreamPacketDescription **outDataPacketDescription, void *inUserData)
{
AACEncoder *encoder = (__bridge AACEncoder *)(inUserData);
UInt32 requestedPackets = *ioNumberDataPackets;size_t copiedSamples = [encoder copyPCMSamplesIntoBuffer:ioData]; if (copiedSamples < requestedPackets) { //PCM 缓冲区还没满 *ioNumberDataPackets = 0; return -1; } *ioNumberDataPackets = 1; return noErr;
}
/**
填充PCM到缓冲区
*/(size_t) copyPCMSamplesIntoBuffer:(AudioBufferList*)ioData {
size_t originalBufferSize = _pcmBufferSize;
if (!originalBufferSize) {
return 0;
}
ioData->mBuffers[0].mData = _pcmBuffer;
ioData->mBuffers[0].mDataByteSize = (int)_pcmBufferSize;
_pcmBuffer = NULL;
_pcmBufferSize = 0;
return originalBufferSize;
}
Audio Queue Services的播放步骤如下:
- 1,给buffer填充数据,并把buffer放入就绪的buffer queue;
- 2,应用通知队列开始播放;
- 3、队列播放第一个填充的buffer;
- 4、队列返回已经播放完毕的buffer,并开始播放下面一个填充好的buffer;
- 5、队列调用之前设置的回调函数,填充播放完毕的buffer;
- 6、回调函数中把buffer填充完毕,并放入buffer queue中。
/********FFMpeg从入门到精通********/
花上几天学一个可以用几十年的技术是何等高的学习“性价比”。
//生成的视频结果是保留视频的上半部分,同时上半部分会镜像到视频的下半部分,二者合成之后作为输出视频
•相同的Filter线性链之间用逗号分隔
•不同的Filter线性链之间用分号分隔
./ffmpeg -i INPUT -vf "split [main][tmp]; [tmp] crop=iw:ih/2:0:0, vflip [flip]; [main][flip] overlay=0:H/2" OUTPUT
ffmpeg -i /Users/zly/Desktop/login_video.mp4 -vf "split [main][tmp]; [tmp] crop=iw:ih/2:0:0, vflip [flip]; [main][flip] overlay=0:H/2" /Users/zly/Desktop/login.mp4
ffmpeg--help查看到的help信息是ffmpeg命令的基础信息
想获得高级参数部分,那么可以通过使用ffmpeg--help long参数来查看
希望获得全部的帮助信息,那么可以通过使用ffmpeg--help full参数来获得。
如果要进行格式化的显示,这样就需要用到ffprobe-print_format或者ffprobe-of参数来进行相应的格式输出,而-print_format支持多种格式输出,包括XML、INI、JSON、CSV、FLAT等。
ffprobe -of xml -show_streams input.flv
ffprobe-of ini-show_streams input.flv
ffprobe-of flat-show_streams input.flv
ffprobe-of json-show_packets input.flv
ffprobe-of csv-show_packets input.flv
ffplay常用的命令:
如果希望从视频的第30秒开始播放,播放10秒钟的文件,则可以使用如下命令:
ffplay -ss 30 -t 10 input.mp4
如果希望视频播放时播放器的窗口显示标题为自定义标题,则可以使用如下命令:
ffplay -window_title "Hello World, This is a sample" output.mp4
如果希望使用ffplay打开网络直播流,则可以使用如下命令:
ffplay -window_title "播放测试" rtmp://up.v.test.com/live/stream
ffplay -window_title "播放测试" http://hdl.9158.com/live/b791155d46ff5e8940c795a41e43f67b.flv
vismv参数则是用来显示图像解码时的运动向量信息的
ffplay -vismv pf output.mp4
ffplay -debug vis_mb_type -window_title "show vis_mb_type" -ss 20 -t 10 -autoexit output.mp4
FFmpeg转封装
moov音视频数据的metadata信息
mdat media数据容器
因为MP4的标准中描述的moov与mdat的存放位置前后并没有进行强制要求,所以有些时候moov这个Box在mdat的后面,有些时候moov被存放在mdat的前面。在互联网的视频点播中,如果希望MP4文件被快速打开,则需要将moov存放在mdat的前面;如果放在后面,则需要将MP4文件下载完成后才可以进行播放。
常规的从文件转换HLS直播时,使用的参数如下:
./ffmpeg -re -i input.mp4 -c copy -f hls -bsf:v h264_mp4toannexb output.m3u8
hls_time参数用于设置M3U8列表中切片的duration;例如使用如下命令行控制转码切片长度为10秒钟左右一片,该切片规则采用的方式是从关键帧处开始切片,所以时间并不是很均匀,如果先转码再进行切片,则会比较规律:
./ffmpeg -re -i input.mp4 -c copy -f hls -bsf:v h264_mp4toannexb -hls_time 10 output.m3u8
hls_list_size参数用于设置M3U8列表中TS切片的个数,通过hls_list_size可以控制M3U8列表中TS分片的个数,命令行如下:
./ffmpeg -re -i input.mp4 -c copy -f hls -bsf:v h264_mp4toannexb -hls_list_size 3 output.m3u8
hls_base_url参数用于为M3U8列表中的文件路径设置前置基本路径参数,因为在FFmpeg中生成M3U8时写入的TS切片路径默认为与M3U8生成的路径相同,但是实际上TS所存储的路径既可以为本地绝对路径,也可以为当前相对路径,还可以为网络路径
./ffmpeg -re -i input.mp4 -c copy -f hls -hls_base_url http://192.168.0.1/live/ -bsf:v h264_mp4toannexb output.m3u8
method参数用于设置HLS将M3U8及TS文件上传至HTTP服务器,使用该功能的前提是需要有一台HTTP服务器,支持上传相关的方法,例如PUT、POST等,method方法的PUT方法可用于实现通过HTTP推流HLS的功能,首先需要配置一个支持上传文件的HTTP服务器,本例使用Nginx来作为HLS直播的推流服务器,并且需要支持WebDAV功能,Nginx配置如下:
location / {
client_max_body_size 10M;
dav_access
group:rw all:rw;
dav_methods PUT DELETE MKCOL COPY MOVE;
root html/;
}
配置完成后启动Nginx即可。通过ffmpeg执行HLS推流命令行如下:
./ffmpeg -i input.mp4 -c copy -f hls -hls_time 3 -hls_list_size 0 -method PUT -t 30 http://127.0.0.1/test/output_test.m3u8
FFmpeg抽取音视频文件中的AAC音频流:
FFmpeg提取MP4文件中的AAC音频流的方法:
./ffmpeg -i input.mp4 -vn -acodec copy output.aac
FFmpeg抽取音视频文件中的H.264视频流
./ffmpeg -i input.mp4 -vcodec copy -an output.h264
FFmpeg抽取音视频文件中的H.265数据
./ffmpeg -i input.mp4 -vcodec copy -an -bsf hevc_mp4toannexb -f hevc output.hevc
使用FFmpeg进行封装转换时并不会占用大量的CPU资源,因为使用FFmpeg进行封装转换时主要是以读取音视频数据、写入音视频数据为主,并不会涉及复杂的计算。如果使用FFmpeg进行编码转换,则需要进行大量的计算,从而将会占用大量的CPU资源。
/*************************iOS音视频开发进阶*************************/
所谓采样就是在时间轴上对信号进行数字化。按比声音最高频率高2倍以上的频率对声音进行采样,(人耳能够听到的频率范围)是20Hz~20kHz,所以采样频率一般为44.1kHz,而所谓的44.1kHz就是代表1秒会采样44100次.
量化是指在幅度轴上对信号进行数字化,比如用16比特的二进制信号来表示声音的一个采样,而16比特(一个short)所表示的范围是[-32768,32767],共有65536个可能取值,因此最终模拟的音频信号在幅度上也分为了65536层
既然每一个量化都是一个采样,那么这么多的采样该如何进行存储呢?这就涉及将要讲解的第三个概念:编码。所谓编码,就是按照一定的格式记录采样和量化后的数字数据.
通常所说的音频的裸数据格式就是脉冲编码调制(Pulse Code Modulation,PCM)数据。描述一段PCM数据一般需要以下几个概念:量化格式(sampleFor-mat)、采样率(sampleRate)、声道数(channel)。以CD的音质为例:量化格式(有的地方描述为位深度)为16比特(2字节),采样率为44100,声道数为2,这些信息就描述了CD的音质。
比特率,即1秒时间内的比特数目,它用于衡量音频数据单位时间内的容量大小。44100 * 16 * 2 = 1378.125kbps
那么在1分钟里,这类CD音质的数据需要占据多大的存储空间呢?计算如下:1378.125 * 60 / 8 / 1024 = 10.09MB
当然,如果sampleFormat更加精确(比如用4字节来描述一个采样),或者sampleRate更加密集(比如48kHz的采样率),那么所占的存储空间就会更大,同时能够描述的声音细节就会越精确。存储的这段二进制数据即表示将模拟信号转换为数字信号了,以后就可以对这段二进制数据进行存储、播放、复制,或者进行其他任何操作。
麦克风是如何采集声音的:麦克风里面有一层碳膜,非常薄而且十分敏感。声音其实是一种纵波,会压缩空气也会压缩这层碳膜,碳膜在受到挤压时也会发出振动,在碳膜的下方就是一个电极,碳膜在振动的时候会接触电极,接触时间的长短和频率与声波的振动幅度和频率有关,这样就完成了声音信号到电信号的转换。之后再经过放大电路处理,就可以实施后面的采样量化处理了。
压缩编码的原理实际上是压缩掉冗余信号,冗余信号是指不能被人耳感知到的信号,包含人耳听觉范围之外的音频信号以及被掩蔽掉的音频信号等。
各色光因其所形成的折射角不同而彼此分离,就像彩虹一样,所以白光能够分解成多种色彩的光。后来人们通过实验证明,红绿蓝三种色光无法被分解,故称为三原色光。
假设一部手机屏幕的分辨率是1280×720,说明水平方向有720个像素点,垂直方向有1280个像素点,所以整个手机屏幕就有1280×720个像素点(这也是分辨率的含义)。每个像素点都由三个子像素点组成(如图1-7所示),这些密密麻麻的子像素点在显微镜下可以看得一清二楚。
RGB表示方式:取值范围为0~255或者00~FF,8个比特表示一个子像素,32个比特表示一个像素,An-droid平台上RGB_565的表示方法为16比特模式表示一个像素,R用5个比特来表示,G用6个比特来表示,B用5个比特来表示。对于一幅图像,一般使用整数表示方法来进行描述,比如计算一张1280×720的RGBA_8888图像的大小,可采用如下方式:1280 * 720 * 4 = 3.516MB
YUV表示方式:YUV最常用的采样格式是4:2:0,4:2:0并不意味着只有Y、Cb而没有Cr分量。它指的是对每行扫描线来说,只有一种色度分量是以2:1的抽样率来存储的。相邻的扫描行存储着不同的色度分量,也就是说,如果某一行是4:2:0,那么其下一行就是4:0:2,再下一行是4:2:0,以此类推。
I帧:帧内编码帧(intra picture),I帧通常是每个GOP(MPEG所使用的一种视频压缩技术)的第一个帧,经过适度地压缩,作为随机访问的参考点,可以当成静态图像。
I帧自身可以通过视频解压算法解压成一张单独的完整视频画面,所以I帧去掉的是视频帧在空间维度上的冗余信息。
P帧:前向预测编码帧(predictive-frame),通过将图像序列中前面已编码帧的时间冗余信息充分去除来压缩传输数据量的编码图像,也称为预测帧。
P帧需要参考其前面的一个I帧或者P帧来解码成一张完整的视频画面。
B帧:双向预测内插编码帧(bi-directional interpolatedprediction frame),既考虑源图像序列前面的已编码帧,又顾及源图像序列后面的已编码帧之间的时间冗余信息,来压缩传输数据量的编码图像,也称为双向预测帧。
B帧则需要参考其前一个I帧或者P帧及其后面的一个P帧来生成一张完整的视频画面,所以P帧与B帧去掉的是视频帧在时间维度上的冗余信息。
IDR帧与I帧的理解:IDR的英文全称instantaneous decoding refresh picture,因为H264采用了多帧预测,所以I帧之后的P帧有可能会参考I帧之前的帧,这就使得在随机访问的时候不能以找到I帧作为参考条件,因为即使找到I帧,I帧之后的帧还是有可能解析不出来,而IDR帧就是一种特殊的I帧,即这一帧之后的所有参考帧只会参考到这个IDR帧,而不会再参考前面的帧。
对于视频来说,AVFrame就是视频的一帧图像,这帧图像什么时候显示给用户,取决于它的PTS(Presentation Time Stamp)。DTS(Decoding Time Stamp)是AVPacket里的一个成员,表示该压缩包应该在什么时候被解码,如果视频里各帧的编码是按输入顺序(显示顺序)依次进行的,那么解码和显示时间应该是一致的,但是事实上,在大多数编解码标准(如H.264或HEVC)中,编码顺序和输入顺序并不一致,于是才会需要PTS和DTS这两种不同的时间戳。
GOP的概念:两个I帧之间形成的一组图片,就是GOP(Group Of Pic-ture)的概念。通常在为编码器设置参数的时候,必须要设置gop_size的值,其代表的是两个I帧之间的帧数目。解码端必须从接收到的第一个I帧开始才可以正确解码出原始图像,否则会无法正确解码。
增加C++支持
在单独编写一个C或C++的项目时,如果该项目需要引用到第三方库,那么编译阶段需要配置参数“extra-cflags,-I”来指定引用头文件的位置,链接阶段需要配置参数“ld-flags,-L”来指定静态库的位置,并且使用-l来指定引用的是哪一个库。
首先对应于-I来指定头文件的目录,Xcode使用Search Paths来设置头文件的搜索路径,通常会用到Header Search Paths选项来指定头文件的搜索路径。其中预定义变量$(SRCROOT)和$(PROJECT_DIR)都是项目的根目录,可以基于这两个预定义变量再加上相对路径来指定头文件所在的具体位置;
在指定第三方库时,-L对应到Xcode中就是other Link flags选项,其中可以写入需要链接的库文件;在Xcode项目中直接添加一个静态库文件,Xcode会默认在Build phases选项的Link Binary withLibrary里加入该静态库,但是如果Xcode没有自动加入该静态库的话,就需要开发者手动加一下,这里其实也是-L的一种表示方式。
使用本机器的编译器,将源代码编译链接成为一个可以在本机器上运行的程序。这就是正常的编译过程,也称为Native Compilation,中文译作本机编译。