Repository: 0voice/ffmpeg_develop_doc Branch: main Commit: cf5d48aea3b6 Files: 147 Total size: 1.0 MB Directory structure: gitextract_9rzpuyiq/ ├── 3个重点,20个函数分析,浅析FFmpeg转码过程.md ├── FFMpeg写MP4文件例子分析.c ├── FFmpeg source code structure AVPacket, AVPacketSideData, AVBufferRef and AVBuffer.md ├── FFmpeg 学习(一):FFmpeg 简介 .md ├── FFmpeg 学习(七):FFmpeg 学习整理总结.md ├── FFmpeg 学习(三):将 FFmpeg 移植到 Android平台.md ├── FFmpeg 学习(二):Mac下安装FFmpeg.md ├── FFmpeg 学习(五):FFmpeg 编解码 API 分析.md ├── FFmpeg 学习(六):FFmpeg 核心模块 libavformat 与 libavcodec 分析.md ├── FFmpeg 学习(四):FFmpeg API 介绍与通用 API 分析.md ├── FFmpeg 开发之 AVFilter 使用流程总结.md ├── FFmpeg 结构体学习(一): AVFormatContext 分析.md ├── FFmpeg 结构体学习(七): AVIOContext 分析.md ├── FFmpeg 结构体学习(三): AVPacket 分析.md ├── FFmpeg 结构体学习(二): AVStream 分析.md ├── FFmpeg 结构体学习(五): AVCodec 分析.md ├── FFmpeg 结构体学习(八):FFMPEG中重要结构体之间的关系.md ├── FFmpeg 结构体学习(六): AVCodecContext 分析.md ├── FFmpeg 结构体学习(四): AVFrame 分析.md ├── FFmpeg源码分析:内存管理系统.md ├── H.264视频解码优化及DSP实现.caj ├── Linux上的ffmpeg完全使用指南.md ├── README.md ├── case_interview/ │ ├── 001-README.md │ └── 002-README.md ├── ffmpeg常用命令.md ├── ffmpeg源码example解析之decode_audio.md ├── ffmpeg源码example解析之decode_video.md ├── ffplay源码和书籍/ │ └── ffplay/ │ ├── Debug/ │ │ ├── allcodecs.obj │ │ ├── allformats.obj │ │ ├── avidec.obj │ │ ├── avio.obj │ │ ├── aviobuf.obj │ │ ├── cutils.obj │ │ ├── dsputil.obj │ │ ├── ffplay.Build.CppClean.log │ │ ├── ffplay.ilk │ │ ├── ffplay.log │ │ ├── ffplay.obj │ │ ├── ffplay.pdb │ │ ├── ffplay.tlog/ │ │ │ ├── CL.read.1.tlog │ │ │ ├── CL.write.1.tlog │ │ │ ├── cl.command.1.tlog │ │ │ ├── ffplay.lastbuildstate │ │ │ ├── link.command.1.tlog │ │ │ ├── link.read.1.tlog │ │ │ └── link.write.1.tlog │ │ ├── file.obj │ │ ├── imgconvert.obj │ │ ├── msrle.obj │ │ ├── truespeech.obj │ │ ├── utils_codec.obj │ │ ├── utils_format.obj │ │ ├── vc120.idb │ │ └── vc120.pdb │ ├── berrno.h │ ├── ffplay.c │ ├── ffplay.dsp │ ├── ffplay.vcxproj │ ├── ffplay.vcxproj.filters │ ├── libavcodec/ │ │ ├── allcodecs.c │ │ ├── avcodec.h │ │ ├── dsputil.c │ │ ├── dsputil.h │ │ ├── imgconvert.c │ │ ├── imgconvert_template.h │ │ ├── msrle.c │ │ ├── truespeech.c │ │ ├── truespeech_data.h │ │ └── utils_codec.c │ ├── libavformat/ │ │ ├── allformats.c │ │ ├── avformat.h │ │ ├── avidec.c │ │ ├── avio.c │ │ ├── avio.h │ │ ├── aviobuf.c │ │ ├── cutils.c │ │ ├── file.c │ │ └── utils_format.c │ ├── libavutil/ │ │ ├── avutil.h │ │ ├── bswap.h │ │ ├── common.h │ │ ├── mathematics.h │ │ └── rational.h │ └── update.txt ├── iOS资料/ │ ├── AVFoundation之视频捕捉.md │ ├── IOS 剪辑编辑器.md │ ├── IOS之多媒体API.md │ ├── MACiOS利用FFmpeg解析音视频数据流.md │ ├── iOS - 图形高级处理 (一、图片显示相关理论).md │ ├── iOS AVDemo(5):音频解码.md │ ├── iOS AVDemo(6):音频渲染.md │ ├── iOS AVDemo(7):视频采集.md │ ├── iOS AVDemo(8):视频编码,H.264 和 H.265 都支持.md │ ├── iOS AVDemo(9):视频封装,采集编码 H.264H.265 并封装 MP4.md │ ├── iOS AVDemo:音频封装,采集编码并封装为 M4A.md │ ├── iOS AVDemo:音频编码,采集 PCM 数据编码为 AAC.md │ ├── iOS AVDemo:音频解封装,从 MP4 中解封装出 AAC.md │ ├── iOS AVDemo:音频采集.md │ ├── iOS Runtime详解.md │ ├── iOS 入门(2):管理第三方库.md │ ├── iOS 离屏渲染探究.md │ ├── iOS 系统架构及常用框架.md │ ├── iOS-WebRTC静态库,framework下载、编译,使用.md │ ├── iOSAVDemo(10):视频解封装,从 MP4 解出 H.264H.265.md │ ├── iOS下 WebRTC 视频渲染.md │ ├── iOS下的渲染框架.md │ ├── iOS使用AVPlayer,播放本地,在线音频 │ ├── iOS动画系列之三:Core Animation.md │ ├── iOS图像渲染及卡顿问题优化.md │ ├── iOS学习音视频的过程.md │ ├── iOS拉取SRS流媒体服务器的Rtc流.md │ ├── iOS短视频篇:音视频编辑之音视频合成,添加水印及音视频导出.md │ ├── iOS硬编解码相关知识.md │ ├── iOS视图渲染与性能优化.md │ ├── iOS视频开发:视频H264硬编码.md │ ├── iOS视频推流、拉流原理.md │ ├── iOS逆向 MachO文件.md │ ├── iOS配置FFmpeg框架.md │ ├── iOS音视频 -- AVFoundation捕捉.md │ ├── iOS音视频同步探讨.md │ ├── iOS音视频开发-了解编码及视频.md │ ├── iOS音视频开发-代码实现视频编码.md │ ├── iOS音视频开发-采集、编码、滤镜.md │ ├── iOS音视频开发——FFmpeg库编译.md │ ├── iOS音视频开发——视频采集.md │ ├── iOS音视频开源框架WebRTC入门-AppRTCMobile.md │ ├── iOS音视频的那些事儿:数据的采集和编码.md │ ├── iOS音视频:OpenGL常用术语介绍.md │ ├── iOS音频录制及合成,以及优化处理 │ ├── iOS音频视频开发.md │ ├── iOS音频采集过程中的音效实现.md │ ├── iOS项目集成OpenCV及踩过的坑.md │ ├── iOS高级视频渲染.md │ ├── macOS 下单步调试 WebRTC Android & iOS.md │ ├── 【OpenGL入门】iOS 图像渲染原理.md │ ├── 【如何快速的开发一个完整的iOS直播app】(美颜篇).md │ ├── 关于iOS离屏渲染的深入研究.md │ ├── 基于 AVFoundation 框架开发小视频功能的方案解析.md │ ├── 最简单的基于FFmpeg的移动端例子:IOS HelloWorld.md │ ├── 视频直播iOS端技术.md │ ├── 资深程序员的Metal入门教程总结.md │ └── 音视频学习--iOS适配H265实战踩坑记.md ├── paper/ │ └── README.md ├── teaching video/ │ └── video.md ├── 使用FFMpeg进行H264编码.c └── 基于RTMP的高清流媒体直播点播封装技术的研究与实现.caj ================================================ FILE CONTENTS ================================================ ================================================ FILE: 3个重点,20个函数分析,浅析FFmpeg转码过程.md ================================================ ## **写在前面** 最近在做和转码有关的项目,接触到ffmpeg这个神器。从一开始简单的写脚本直接调用ffmpeg的可执行文件做些转码的工作,到后来需要写程序调用ffmpeg的API。虽然上网搜了别人的demo稍微改改顺利完成了工作,但是对于ffmpeg这个黑盒子,还是有些好奇心和担心(项目中使用不了解的代码总是不那么放心),于是抽空翻了翻ffmpeg的源码,整理成文章给大家分享分享。 由于我并非做音频出身,对于音频一窍不通。ffmpeg整个也非常庞大,所以这篇文章从ffmpeg提供的转码的demo开始,侧重于讲清楚整个输入->转码->输出的流程,并学习ffmpeg如何做到通用和可扩展性。 注:本文基于ffmpeg提供的transcode_aac.c样例。 ## **三个重点** 转码的过程是怎么样的?简单来说就是从输入读取数据,解析原来的数据格式,转成目标数据格式,再将最终数据输出。这里就涉及到三个**点**:**数据输入和输出方式**,**数据的编码方式**及**数据的容器格式**(容器是用来区分不同文件的数据类型的,而编码格式则由音视频的压缩算法决定,一般所说的文件格式或者后缀名指的就是文件的容器。对于一种容器,可以包含不同编码格式的一种视频和音频)。 ffmpeg是一个非常非常通用的工具,支持非常广的数据输入和输出,包括:hls流,文件,内存等,支持各类数据编码格式,包括:aac,mp3等等,同时支持多种容器格式,包括ts,aac等。另外ffmpeg是通过C语言实现的,如果是C++,我们可以通过继承和多态来实现。定义一个IO的基类,一个Format的基类和一个Codec的基类,具体的输入输出协议继承IO基类实现各自的输入输出方法,具体的容器格式继承Format基类,具体的编码格式继承Codec基类。这篇文章也会简单讲解ffmpeg如何用C语言实现类似C++的继承和多态。 ## **基本数据结构** ffmpeg转码中最基本的结构为AVFormatContext和AVCodecContext。AVCodecContext负责编码,AVFormatContext负责IO和容器格式。 我从AVFormatContext类抽离出三个基本的成员iformat,oformat,pb。分别属于AVInputFormat,AVOutputFormat,AVIOContext类。iformat为输入的数据格式,oformat为输出的数据格式,pb则负责输入输出。 ![img](https://pic4.zhimg.com/v2-2632063d41df7a34fb60304434a75f1b_b.png) 我把这三个类的定义抽离了出来简化了下,可以看出AVInputFormat声明了read_packet方法,AVOutputFormat声明了write_packet方法,AVIOContext声明了read_packet, write_packet方法。同时AVInputFormat和AVOutputFormat还有一个成员变量name用以标识该格式的后缀名。 ![img](https://pic3.zhimg.com/v2-58239fb65a540dc8b19241d70f7c9e7a_b.png) 下一节我们会看到Input/OutputForm的read/write packet方法和IOContext的关系。 ## **输入函数调用图** 下面是初始化输入的整个过程的函数调用图。 ![img](https://pic3.zhimg.com/v2-4b021abefdc7ab53c82833f722bf6e7a_b.png) 首先从调用open_input_file开始,首先**解析输入的protocol**。avio_open2函数会调用一系列helper函数(ffurl_open,ffio_fdopen)分析输入的协议,设置AVFormatContext的pb变量的read_packet方法。而av_probe_input_buffer2函数则会**分析输入文件的格式**(从文件名解析或输入数据做判断),设置AVFormatContext的iformat的read_packet方法。 ![img](https://pic2.zhimg.com/v2-e9a529f86175c4cb2f21aa7f190e1769_b.png) 两个read_packet有什么关系呢?第二个函数调用图可以看出,iformat的read_packet最终会调用pb的read_packet方法。意思就是**数据本身由pb的read_packet方法来读取,而iformat则会在输入的数据上做些格式相关的解析操作**(比如解析输入数据的头部,提取出输入数据中真正的音频/视频数据,再加以转码)。 ## **IO相关代码** 直接看上面的图不太直观,这一节我把源码中各个步骤截图下来进行分析。 转码开始步骤,调用open_input_file函数,传入文件名。 ![img](https://pic3.zhimg.com/v2-31edf2063a8c04656557b946311f7492_b.png) avformat_open_input函数会调用init_input()来处理输入文件。 ![img](https://pic2.zhimg.com/v2-71305dac46d81e0e44fff17439bbabe9_b.png) init_input函数主要做两个事情,一是解析输入协议(如何读取数据?hls流?文件?内存?),二是解析输入数据的格式(输入数据为aac?ts?m4a?) ![img](https://pic4.zhimg.com/v2-d37bf73363d41ea844ac698758a0b353_b.png) avio_open2函数首先调用ffurl_open函数,根据文件名来推断所属的输入协议(URLProtocol)。之后再调用ffio_fdopen设置pb的read_packet方法。 ![img](https://pic3.zhimg.com/v2-89e21351616f4b8c65dd386b8a41dc9e_b.png) ![img](https://pic1.zhimg.com/v2-1e33906ff3c6f718970753d4eda2aaf0_b.png) ![img](https://pic4.zhimg.com/v2-aeb0dbfcf245564406274ff644d63123_b.png) 上面几段代码的逻辑为:根据文件名查找对应的URLProtocol->把该URLProtocol赋值给URLContext的prot成员变量->创建AVIOContext实例,赋值给AVFormatContext的pb成员变量。 ![img](https://pic3.zhimg.com/v2-2620110211da572988203229d29d0416_b.png) ![img](https://pic4.zhimg.com/v2-fa4a5834ba7b3709ca9c5af2e1349b37_b.png) 这里设置了AVIOContext实例的read_packet为ffurl_read方法。 ![img](https://pic4.zhimg.com/v2-520147188f5f044fed71dd64776a75ff_b.png) ffurl_read方法其实就是调用URLContext的prot(上面赋值的)的url_read方法。通过函数指针去调用具体的URLContext对象的prot成员变量的url_read方法。 ![img](https://pic2.zhimg.com/v2-c250ed674ddac54fd9d3ef80b070a729_b.png) 接下来看看解析输入数据格式的代码。av_probe_input_buffer2函数调用av_probe_input_format2函数来推断数据数据的格式。从之前的图我们知道*fmt其实就是&s->iformat。因此这里设置了AVFormatContext的iformat成员变量。 ![img](https://pic1.zhimg.com/v2-bfcaa2cb999687df6ddb09c1165f5310_b.png) 至此AVFormatContext对象的iformat和pb成员变量就设置好了。接下来看看如何读取输入开始转码。 av_read_frame函数调用read_frame_internal函数开始读取数据。 ![img](https://pic2.zhimg.com/v2-8dfe519e59eab253c3360e4eed36eef5_b.png) read_frame_internal会调用ff_read_packet,后者最终调用的是iformat成员变量的read_packet方法。 ![img](https://pic2.zhimg.com/v2-98b6b38ae8793c193d97ea70c1e78e39_b.png) ![img](https://pic2.zhimg.com/v2-b47fb24bdbf9657b3ac1921dab6d4749_b.png) 拿aac举例,aac的read_packet方法实际上是ff_raw_read_partial_packet函数。 ![img](https://pic3.zhimg.com/v2-bfd1eeaa1f4cd1c5733dc1392782f5d2_b.png) ff_raw_read_partial_packet会调用ffio_read_partial,后者最终调用的是AVFormatContext的pb成员变量的read_packet方法。而我们知道pb成员的read_packet其实就是ffurl_read,也就是具体输入URLProtocl的read_packet方法。 ![img](https://pic2.zhimg.com/v2-e529b8f31ed3f9c136c38197a6f896e5_b.png) ![img](https://pic2.zhimg.com/v2-c6f53da467f8278936ed779d2182e3dd_b.png) 至此已经走完了整个输入的流程,输出也是类似的代码,这里就不再赘述。 ## **转码函数调用图** 上面关于IO的介绍我从输入的角度进行分析。接下来的转码过程我则从输出的角度进行分析。下图是转码过程的函数调用图(做了简化)。load_encode_and_write调用encode_audio_frame, encode_audio_frame调用avcodec_encode_audio2来做实际的编码工作,最后调用av_write_frame将编码完的数据写入输出。 ![img](https://pic4.zhimg.com/v2-673c2b1a1d1db45ad83e93e4149f487f_b.png) ## **转码相关代码** 首先需要设置输出目标编码格式,下面的代码为设置编码格式(aac)的片段: ![img](https://pic1.zhimg.com/v2-b7db4b56403fca052660be8b437e1df4_b.png) 在这里设置了output_codec_context(AVCodecContext类对象)之后,从前面的函数调用图,我们知道是avcodec_encode_audio2函数执行的转码过程: ![img](https://pic3.zhimg.com/v2-c24e2a95d172b8b5a6006097b565d48e_b.png) 这里看到调用了avctx(AVCodecContext类对象)的codec(AVCodec类对象)成员变量的encode2方法去做编码操作。 转码这里专业性比较强,我并没有细读,因此这里简单带过。 ## **总结** 可以看出ffmpeg大量使用函数指针来实现类似C++的继承/多态的效果。并且ffmpeg具有非常好的扩展性。如果我需要自定义一个新的输入协议,只需要自己定义一个新的URLProtocol对象,实现read_packet方法即可。如果需要自定义一个新的容器格式,只需要定义一个新的AVInputFormat对象,实现read_packet方法即可。如果需要自定义一个新的编码格式,只需要定义一个新的AVCodec对象,实现encode2方法即可。真是非常赞的代码架构设计! ================================================ FILE: FFMpeg写MP4文件例子分析.c ================================================ /** 这段时间看了FFMpeg提供的例子muxing.c,我略微修改了下源代码,使其生成一个MP4文件,音频使用AAC编码,视频使用H.264编码。代码很简单,我就不做说明了,代码如下。 以后我们继续写如何将DirectShow中采集的音视频数据编码并生成MP4文件。 */ /* 5 seconds stream duration */ #define STREAM_DURATION 5.0 #define STREAM_FRAME_RATE 25 /* 25 images/s */ #define STREAM_NB_FRAMES ((int)(STREAM_DURATION * STREAM_FRAME_RATE)) #define STREAM_PIX_FMT PIX_FMT_YUV420P /* default pix_fmt */ static int sws_flags = SWS_BICUBIC; /**************************************************************/ /* audio output */ static float t, tincr, tincr2; static int16_t *samples; static uint8_t *audio_outbuf; static int audio_outbuf_size; static int audio_input_frame_size; /* * add an audio output stream */ static AVStream *add_audio_stream(AVFormatContext *oc, enum CodecID codec_id) { AVCodecContext *c; AVStream *st; st = avformat_new_stream(oc, NULL); if (!st) { fprintf(stderr, "Could not alloc stream\n"); exit(1); } st->id = 1; c = st->codec; c->codec_id = codec_id; c->codec_type = AVMEDIA_TYPE_AUDIO; /* put sample parameters */ c->sample_fmt = AV_SAMPLE_FMT_S16; c->bit_rate = 64000; c->sample_rate = 44100; c->channels = 2; // some formats want stream headers to be separate if (oc->oformat->flags & AVFMT_GLOBALHEADER) c->flags |= CODEC_FLAG_GLOBAL_HEADER; return st; } static void open_audio(AVFormatContext *oc, AVStream *st) { AVCodecContext *c; AVCodec *codec; c = st->codec; /* find the audio encoder */ codec = avcodec_find_encoder(c->codec_id); if (!codec) { fprintf(stderr, "codec not found\n"); exit(1); } /* open it */ if (avcodec_open(c, codec) < 0) { fprintf(stderr, "could not open codec\n"); exit(1); } /* init signal generator */ t = 0; tincr = 2 * M_PI * 110.0 / c->sample_rate; /* increment frequency by 110 Hz per second */ tincr2 = 2 * M_PI * 110.0 / c->sample_rate / c->sample_rate; audio_outbuf_size = 10000; audio_outbuf = (uint8_t *)av_malloc(audio_outbuf_size); /* ugly hack for PCM codecs (will be removed ASAP with new PCM support to compute the input frame size in samples */ if (c->frame_size <= 1) { audio_input_frame_size = audio_outbuf_size / c->channels; switch(st->codec->codec_id) { case CODEC_ID_PCM_S16LE: case CODEC_ID_PCM_S16BE: case CODEC_ID_PCM_U16LE: case CODEC_ID_PCM_U16BE: audio_input_frame_size >>= 1; break; default: break; } } else { audio_input_frame_size = c->frame_size; } samples = (int16_t *)av_malloc(audio_input_frame_size * 2 * c->channels); } /* prepare a 16 bit dummy audio frame of 'frame_size' samples and 'nb_channels' channels */ static void get_audio_frame(int16_t *samples, int frame_size, int nb_channels) { int j, i, v; int16_t *q; q = samples; for (j = 0; j < frame_size; j++) { v = (int)(sin(t) * 10000); for(i = 0; i < nb_channels; i++) *q++ = v; t += tincr; tincr += tincr2; } } static void write_audio_frame(AVFormatContext *oc, AVStream *st) { AVCodecContext *c; AVPacket pkt; av_init_packet(&pkt); c = st->codec; get_audio_frame(samples, audio_input_frame_size, c->channels); pkt.size = avcodec_encode_audio(c, audio_outbuf, audio_outbuf_size, samples); if (c->coded_frame && c->coded_frame->pts != AV_NOPTS_VALUE) pkt.pts= av_rescale_q(c->coded_frame->pts, c->time_base, st->time_base); pkt.flags |= AV_PKT_FLAG_KEY; pkt.stream_index = st->index; pkt.data = audio_outbuf; /* write the compressed frame in the media file */ if (av_interleaved_write_frame(oc, &pkt) != 0) { fprintf(stderr, "Error while writing audio frame\n"); exit(1); } } static void close_audio(AVFormatContext *oc, AVStream *st) { avcodec_close(st->codec); av_free(samples); av_free(audio_outbuf); } /**************************************************************/ /* video output */ static AVFrame *picture, *tmp_picture; static uint8_t *video_outbuf; static int frame_count, video_outbuf_size; /* add a video output stream */ static AVStream *add_video_stream(AVFormatContext *oc, enum CodecID codec_id) { AVCodecContext *c; AVStream *st; AVCodec *codec; st = avformat_new_stream(oc, NULL); if (!st) { fprintf(stderr, "Could not alloc stream\n"); exit(1); } c = st->codec; /* find the video encoder */ codec = avcodec_find_encoder(codec_id); if (!codec) { fprintf(stderr, "codec not found\n"); exit(1); } avcodec_get_context_defaults3(c, codec); c->codec_id = codec_id; /* put sample parameters */ c->bit_rate = /*400000*/3000000; /* resolution must be a multiple of two */ c->width = /*352*/640; c->height = /*288*/480; /* time base: this is the fundamental unit of time (in seconds) in terms of which frame timestamps are represented. for fixed-fps content, timebase should be 1/framerate and timestamp increments should be identically 1. */ c->time_base.den = STREAM_FRAME_RATE; c->time_base.num = 1; c->gop_size = 12; /* emit one intra frame every twelve frames at most */ c->pix_fmt = STREAM_PIX_FMT; if (c->codec_id == CODEC_ID_MPEG2VIDEO) { /* just for testing, we also add B frames */ c->max_b_frames = 2; } if (c->codec_id == CODEC_ID_MPEG1VIDEO){ /* Needed to avoid using macroblocks in which some coeffs overflow. This does not happen with normal video, it just happens here as the motion of the chroma plane does not match the luma plane. */ c->mb_decision=2; } // some formats want stream headers to be separate if (oc->oformat->flags & AVFMT_GLOBALHEADER) c->flags |= CODEC_FLAG_GLOBAL_HEADER; return st; } static AVFrame *alloc_picture(enum PixelFormat pix_fmt, int width, int height) { AVFrame *picture; uint8_t *picture_buf; int size; picture = avcodec_alloc_frame(); if (!picture) return NULL; size = avpicture_get_size(pix_fmt, width, height); picture_buf = (uint8_t *)av_malloc(size); if (!picture_buf) { av_free(picture); return NULL; } avpicture_fill((AVPicture *)picture, picture_buf, pix_fmt, width, height); return picture; } static void open_video(AVFormatContext *oc, AVStream *st) { AVCodec *codec; AVCodecContext *c; c = st->codec; /* find the video encoder */ codec = avcodec_find_encoder(c->codec_id); if (!codec) { fprintf(stderr, "codec not found\n"); exit(1); } /* open the codec */ if (avcodec_open(c, codec) < 0) { fprintf(stderr, "could not open codec\n"); exit(1); } video_outbuf = NULL; if (!(oc->oformat->flags & AVFMT_RAWPICTURE)) { /* allocate output buffer */ /* XXX: API change will be done */ /* buffers passed into lav* can be allocated any way you prefer, as long as they're aligned enough for the architecture, and they're freed appropriately (such as using av_free for buffers allocated with av_malloc) */ video_outbuf_size = 200000; video_outbuf = (uint8_t *)av_malloc(video_outbuf_size); } /* allocate the encoded raw picture */ picture = alloc_picture(c->pix_fmt, c->width, c->height); if (!picture) { fprintf(stderr, "Could not allocate picture\n"); exit(1); } /* if the output format is not YUV420P, then a temporary YUV420P picture is needed too. It is then converted to the required output format */ tmp_picture = NULL; if (c->pix_fmt != PIX_FMT_YUV420P) { tmp_picture = alloc_picture(PIX_FMT_YUV420P, c->width, c->height); if (!tmp_picture) { fprintf(stderr, "Could not allocate temporary picture\n"); exit(1); } } } /* prepare a dummy image */ static void fill_yuv_image(AVFrame *pict, int frame_index, int width, int height) { int x, y, i; i = frame_index; /* Y */ for (y = 0; y < height; y++) { for (x = 0; x < width; x++) { pict->data[0][y * pict->linesize[0] + x] = x + y + i * 3; } } /* Cb and Cr */ for (y = 0; y < height/2; y++) { for (x = 0; x < width/2; x++) { pict->data[1][y * pict->linesize[1] + x] = 128 + y + i * 2; pict->data[2][y * pict->linesize[2] + x] = 64 + x + i * 5; } } } static void write_video_frame(AVFormatContext *oc, AVStream *st) { int out_size, ret; AVCodecContext *c; static struct SwsContext *img_convert_ctx; c = st->codec; if (frame_count >= STREAM_NB_FRAMES) { /* no more frame to compress. The codec has a latency of a few frames if using B frames, so we get the last frames by passing the same picture again */ } else { if (c->pix_fmt != PIX_FMT_YUV420P) { /* as we only generate a YUV420P picture, we must convert it to the codec pixel format if needed */ if (img_convert_ctx == NULL) { img_convert_ctx = sws_getContext(c->width, c->height, PIX_FMT_YUV420P, c->width, c->height, c->pix_fmt, sws_flags, NULL, NULL, NULL); if (img_convert_ctx == NULL) { fprintf(stderr, "Cannot initialize the conversion context\n"); exit(1); } } fill_yuv_image(tmp_picture, frame_count, c->width, c->height); sws_scale(img_convert_ctx, tmp_picture->data, tmp_picture->linesize, 0, c->height, picture->data, picture->linesize); } else { fill_yuv_image(picture, frame_count, c->width, c->height); } } if (oc->oformat->flags & AVFMT_RAWPICTURE) { /* raw video case. The API will change slightly in the near future for that. */ AVPacket pkt; av_init_packet(&pkt); pkt.flags |= AV_PKT_FLAG_KEY; pkt.stream_index = st->index; pkt.data = (uint8_t *)picture; pkt.size = sizeof(AVPicture); ret = av_interleaved_write_frame(oc, &pkt); } else { /* encode the image */ out_size = avcodec_encode_video(c, video_outbuf, video_outbuf_size, picture); /* if zero size, it means the image was buffered */ if (out_size > 0) { AVPacket pkt; av_init_packet(&pkt); if (c->coded_frame->pts != AV_NOPTS_VALUE) pkt.pts= av_rescale_q(c->coded_frame->pts, c->time_base, st->time_base); if(c->coded_frame->key_frame) pkt.flags |= AV_PKT_FLAG_KEY; pkt.stream_index = st->index; pkt.data = video_outbuf; pkt.size = out_size; // printf("pts %d \n", c->coded_frame->pts); /* write the compressed frame in the media file */ ret = av_interleaved_write_frame(oc, &pkt); } else { ret = 0; } } if (ret != 0) { fprintf(stderr, "Error while writing video frame\n"); exit(1); } frame_count++; } static void close_video(AVFormatContext *oc, AVStream *st) { avcodec_close(st->codec); av_free(picture->data[0]); av_free(picture); if (tmp_picture) { av_free(tmp_picture->data[0]); av_free(tmp_picture); } av_free(video_outbuf); } /**************************************************************/ /* media file output */ int main(int argc, char **argv) { const char *filename; AVOutputFormat *fmt; AVFormatContext *oc; AVStream *audio_st, *video_st; double audio_pts, video_pts; int i; /* initialize libavcodec, and register all codecs and formats */ av_register_all(); #if 0 if (argc != 2) { printf("usage: %s output_file\n" "API example program to output a media file with libavformat.\n" "The output format is automatically guessed according to the file extension.\n" "Raw images can also be output by using '%%d' in the filename\n" "\n", argv[0]); return 1; } filename = argv[1]; #endif //#define RTMP_STREAM #ifdef RTMP_STREAM filename = "rtmp://192.168.0.239/live/livestream"; #else filename = "1.mp4"; #endif /* allocate the output media context */ avformat_alloc_output_context2(&oc, NULL, NULL, filename); if (!oc) { printf("Could not deduce output format from file extension: using MPEG.\n"); avformat_alloc_output_context2(&oc, NULL, /*"mpeg"*/"flv", filename); } if (!oc) { return 1; } // 强制指定 264 编码 oc->oformat->video_codec = CODEC_ID_H264; oc->oformat->audio_codec = CODEC_ID_AAC; fmt = oc->oformat; /* add the audio and video streams using the default format codecs and initialize the codecs */ video_st = NULL; audio_st = NULL; if (fmt->video_codec != CODEC_ID_NONE) { video_st = add_video_stream(oc, fmt->video_codec); } if (fmt->audio_codec != CODEC_ID_NONE) { audio_st = add_audio_stream(oc, fmt->audio_codec); } av_dump_format(oc, 0, filename, 1); /* now that all the parameters are set, we can open the audio and video codecs and allocate the necessary encode buffers */ if (video_st) open_video(oc, video_st); if (audio_st) open_audio(oc, audio_st); /* open the output file, if needed */ if (!(fmt->flags & AVFMT_NOFILE)) { if (avio_open(&oc->pb, filename, AVIO_FLAG_WRITE) < 0) { fprintf(stderr, "Could not open '%s'\n", filename); return 1; } } /* write the stream header, if any */ avformat_write_header(oc, NULL); picture->pts = 0; for(;;) { /* compute current audio and video time */ if (audio_st) audio_pts = (double)audio_st->pts.val * audio_st->time_base.num / audio_st->time_base.den; else audio_pts = 0.0; if (video_st) video_pts = (double)video_st->pts.val * video_st->time_base.num / video_st->time_base.den; else video_pts = 0.0; if ((!audio_st || audio_pts >= STREAM_DURATION) && (!video_st || video_pts >= STREAM_DURATION)) break; /* write interleaved audio and video frames */ if (!video_st || (video_st && audio_st && audio_pts < video_pts)) { write_audio_frame(oc, audio_st); } else { write_video_frame(oc, video_st); picture->pts++; } } /* write the trailer, if any. the trailer must be written * before you close the CodecContexts open when you wrote the * header; otherwise write_trailer may try to use memory that * was freed on av_codec_close() */ av_write_trailer(oc); /* close each codec */ if (video_st) close_video(oc, video_st); if (audio_st) close_audio(oc, audio_st); /* free the streams */ for(i = 0; i < oc->nb_streams; i++) { av_freep(&oc->streams[i]->codec); av_freep(&oc->streams[i]); } if (!(fmt->flags & AVFMT_NOFILE)) { /* close the output file */ avio_close(oc->pb); } /* free the stream */ av_free(oc); return 0; } ================================================ FILE: FFmpeg source code structure AVPacket, AVPacketSideData, AVBufferRef and AVBuffer.md ================================================ AVPacket stores the encoded frame data, which is usually output by demuxer and then transferred to decoder as input, or received from encoder as output and then transferred to muxer. ![img](https://www.fatalerrors.org/images/blog/e39fa076ec6156ba98db984ecab4c8ad.jpg) For video, it should usually contain a compressed frame. For audio, it may contain several compressed frames. Encoders allow output of empty packets, no compressed data, and only side data (for example, updating some stream parameters at the end of encoding). The semantics of data ownership depends on the buf field. If this value is set, the Packet data is dynamically allocated and valid indefinitely until it is used for AV_ Packet_ The call to unref() reduces the reference count to 0. If the buf field is not set, av_packet_ref() will make a copy instead of increasing the reference count. side data is always generated by av_malloc(), assigned by av_packet_ref() is copied by av_packet_unref() is released. sizeof(AVPacket) has been abandoned as a part of public ABI. Once AV_ init_ The Packet() function is removed, and the new Packet can only be created by av_packet_alloc(), new fields may be added to the end of the structure. Next, we will learn about AVPacket, and then we will lead out AVBufferRef and AVPacketSideData from AVPacket structure. Finally, we will lead out AVBuffer from AVBufferRef and AVPacketSideData from AVPacketSideData enumeration. ## 1, AVPacket libavcodec/packet.h ``` typedef struct AVPacket { /** * A reference to the reference-counted buffer where the packet data is * stored. * May be NULL, then the packet data is not reference-counted. */ AVBufferRef *buf; /** * Presentation timestamp in AVStream->time_base units; the time at which * the decompressed packet will be presented to the user. * Can be AV_NOPTS_VALUE if it is not stored in the file. * pts MUST be larger or equal to dts as presentation cannot happen before * decompression, unless one wants to view hex dumps. Some formats misuse * the terms dts and pts/cts to mean something different. Such timestamps * must be converted to true pts/dts before they are stored in AVPacket. */ int64_t pts; /** * Decompression timestamp in AVStream->time_base units; the time at which * the packet is decompressed. * Can be AV_NOPTS_VALUE if it is not stored in the file. */ int64_t dts; uint8_t *data; int size; int stream_index; /** * A combination of AV_PKT_FLAG values */ int flags; /** * Additional packet data that can be provided by the container. * Packet can contain several types of side information. */ AVPacketSideData *side_data; int side_data_elems; /** * Duration of this packet in AVStream->time_base units, 0 if unknown. * Equals next_pts - this_pts in presentation order. */ int64_t duration; int64_t pos; ///< byte position in stream, -1 if unknown #if FF_API_CONVERGENCE_DURATION /** * @deprecated Same as the duration field, but as int64_t. This was required * for Matroska subtitles, whose duration values could overflow when the * duration field was still an int. */ attribute_deprecated int64_t convergence_duration; #endif } AVPacket; ``` Here's what each field means. | field | meaning | | ---------------------------- | ------------------------------------------------------------ | | AVBufferRef * buf | The reference to the reference count buffer that stores the packet data. | | int64_t pts | Using avstream > time_ The time stamp displayed in the base time base is the time when the unpacked packet is presented to the user. | | int64_t dts | Using avstream > time_ Base is the time stamp of unpacking, and the time when the packet is unpacked. | | uint8_t * data | The actual data buffer of the packet. | | int size | The actual data size of the packet. | | int stream_index | The index of the stream. | | int flags | AV_ PKT_ A combination of flag values. | | AVPacketSideData * side_data | Additional data that the container can provide. | | int side_data_elems | side_ The number of data elements. | | int64_t duration | The duration of this packet is avstream > time_ Base, or 0 if unknown. | | int64_t pos | The byte position in the stream, or - 1 if unknown. | Here's AV_ PKT_ The combined values that flag can use. libavcodec/packet.h ``` #define AV_PKT_FLAG_KEY 0x0001 / / key frame #define AV_PKT_FLAG_CORRUPT 0x0002 / / corrupt data #define AV_PKT_FLAG_DISCARD 0x0004 / / is used to discard packet s that need to remain in a valid decoder state but do not need to be output, and should be discarded after decoding. #define AV_ PKT_ FLAG_ Trusted 0x0008 / / packet comes from a trusted source. #define AV_PKT_FLAG_DISPOSABLE 0x0010 / / used to indicate a packet containing a frame that can be discarded by the decoder, that is, a non referenced frame. ``` ## 2, AVBufferRef A reference to a data buffer. The size of this structure is not part of the public ABI and is not intended to be allocated directly. libavutil/buffer.h ``` typedef struct AVBufferRef { AVBuffer *buffer; /** * The data buffer. It is considered writable if and only if * this is the only reference to the buffer, in which case * av_buffer_is_writable() returns 1. */ uint8_t *data; /** * Size of data in bytes. */ #if FF_API_BUFFER_SIZE_T int size; #else size_t size; #endif } AVBufferRef; ``` | field | meaning | | ----------------- | ------------------------------------------------------------ | | AVBuffer *buffer | A reference count buffer type. It is opaque, which means to use it by reference (AVBufferRef). | | uint8_t *data | Data buffer. If and only if this is the only reference to the buffer, it is considered writable, in which case av_buffer_is_writable() returns 1. | | size_t / int size | The size of data in bytes. | ## 3, AVBuffer A reference count buffer type. Defined in libavutil / buffer_ In internal. H. It is opaque, which means to use it by reference (AVBufferRef). libavutil/buffer_internal.h ``` struct AVBuffer { uint8_t *data; /**< data described by this buffer */ buffer_size_t size; /**< size of data in bytes */ /** * number of existing AVBufferRef instances referring to this buffer */ atomic_uint refcount; /** * a callback for freeing the data */ void (*free)(void *opaque, uint8_t *data); /** * an opaque pointer, to be used by the freeing callback */ void *opaque; /** * A combination of AV_BUFFER_FLAG_* */ int flags; /** * A combination of BUFFER_FLAG_* */ int flags_internal; }; ``` | field | meaning | | ----------------------------------------- | ------------------------------------------------------------ | | uint8_t *data | The data described by the buffer. | | buffer_size_t size | The size of data in bytes. | | atomic_uint refcount | The number of existing AVBufferRef instances that reference this buffer. | | void (*free)(void *opaque, uint8_t *data) | Callback used to release data. | | void *opaque | An opaque pointer used by the release callback function. | | int flags | AV_BUFFER_FLAG_ *The combination of the two. | | int flags_internal | BUFFER_FLAG_ *The combination of the two. | AVBuffer is an API for referencing count data buffers. There are two core objects AVBuffer and AVBufferRef in this API. AVBuffer represents the data buffer itself; it is opaque and cannot be accessed directly by the caller, but only through AVBufferRef. However, the caller may compare two AVBuffer pointers to check whether two different references describe the same data buffer. AVBufferRef represents a single reference to AVBuffer, which can be operated directly by the caller. There are two functions that can create a new AVBuffer with one reference -- av_buffer_alloc() is used to allocate a new buffer, av_buffer_create() is used to wrap an existing array in AVBuffer. From existing references, you can use av_buffer_ref() creates another reference. Using av_buffer_unref() releases a reference (once all references are released, the data is automatically released). The Convention between this API and the rest of FFmpeg is that a buffer is considered writable if there is only one reference to it (and it is not marked read-only). AV is provided_ buffer_ is_ Write() function to check if this is true, and av_buffer_make_writable() will automatically create a new writable buffer if necessary. Of course, nothing prevents the calling code from violating this Convention, but it is only safe if all existing references are under its control. Reference and dereference buffers are thread safe, so they can be used by multiple threads at the same time without any additional locks. Two different references to the same buffer can point to different parts of the buffer (for example, their AVBufferRef.data The data will not be equal). ## 4, AVPacketSideData Additional Packet data that the container can provide. A Packet can contain several types of side information. libavcodec/packet.h ``` typedef struct AVPacketSideData { uint8_t *data; #if FF_API_BUFFER_SIZE_T int size; #else size_t size; #endif enum AVPacketSideDataType type; } AVPacketSideData; ``` | field | meaning | | ------------------------------ | ------------------------------------ | | uint8_t *data | Data cache. | | int / size_t size | The size of the data cache in bytes. | | enum AVPacketSideDataType type | Packet side data type. | The AVPacketSideDataType enumeration defines various side data types. libavcodec/packet.h ``` /** * @defgroup lavc_packet AVPacket * * Types and functions for working with AVPacket. * @{ */ enum AVPacketSideDataType { /** * An AV_PKT_DATA_PALETTE side data packet contains exactly AVPALETTE_SIZE * bytes worth of palette. This side data signals that a new palette is * present. */ AV_PKT_DATA_PALETTE, /** * The AV_PKT_DATA_NEW_EXTRADATA is used to notify the codec or the format * that the extradata buffer was changed and the receiving side should * act upon it appropriately. The new extradata is embedded in the side * data buffer and should be immediately used for processing the current * frame or packet. */ AV_PKT_DATA_NEW_EXTRADATA, /** * An AV_PKT_DATA_PARAM_CHANGE side data packet is laid out as follows: * @code * u32le param_flags * if (param_flags & AV_SIDE_DATA_PARAM_CHANGE_CHANNEL_COUNT) * s32le channel_count * if (param_flags & AV_SIDE_DATA_PARAM_CHANGE_CHANNEL_LAYOUT) * u64le channel_layout * if (param_flags & AV_SIDE_DATA_PARAM_CHANGE_SAMPLE_RATE) * s32le sample_rate * if (param_flags & AV_SIDE_DATA_PARAM_CHANGE_DIMENSIONS) * s32le width * s32le height * @endcode */ AV_PKT_DATA_PARAM_CHANGE, /** * An AV_PKT_DATA_H263_MB_INFO side data packet contains a number of * structures with info about macroblocks relevant to splitting the * packet into smaller packets on macroblock edges (e.g. as for RFC 2190). * That is, it does not necessarily contain info about all macroblocks, * as long as the distance between macroblocks in the info is smaller * than the target payload size. * Each MB info structure is 12 bytes, and is laid out as follows: * @code * u32le bit offset from the start of the packet * u8 current quantizer at the start of the macroblock * u8 GOB number * u16le macroblock address within the GOB * u8 horizontal MV predictor * u8 vertical MV predictor * u8 horizontal MV predictor for block number 3 * u8 vertical MV predictor for block number 3 * @endcode */ AV_PKT_DATA_H263_MB_INFO, /** * This side data should be associated with an audio stream and contains * ReplayGain information in form of the AVReplayGain struct. */ AV_PKT_DATA_REPLAYGAIN, /** * This side data contains a 3x3 transformation matrix describing an affine * transformation that needs to be applied to the decoded video frames for * correct presentation. * * See libavutil/display.h for a detailed description of the data. */ AV_PKT_DATA_DISPLAYMATRIX, /** * This side data should be associated with a video stream and contains * Stereoscopic 3D information in form of the AVStereo3D struct. */ AV_PKT_DATA_STEREO3D, /** * This side data should be associated with an audio stream and corresponds * to enum AVAudioServiceType. */ AV_PKT_DATA_AUDIO_SERVICE_TYPE, /** * This side data contains quality related information from the encoder. * @code * u32le quality factor of the compressed frame. Allowed range is between 1 (good) and FF_LAMBDA_MAX (bad). * u8 picture type * u8 error count * u16 reserved * u64le[error count] sum of squared differences between encoder in and output * @endcode */ AV_PKT_DATA_QUALITY_STATS, /** * This side data contains an integer value representing the stream index * of a "fallback" track. A fallback track indicates an alternate * track to use when the current track can not be decoded for some reason. * e.g. no decoder available for codec. */ AV_PKT_DATA_FALLBACK_TRACK, /** * This side data corresponds to the AVCPBProperties struct. */ AV_PKT_DATA_CPB_PROPERTIES, /** * Recommmends skipping the specified number of samples * @code * u32le number of samples to skip from start of this packet * u32le number of samples to skip from end of this packet * u8 reason for start skip * u8 reason for end skip (0=padding silence, 1=convergence) * @endcode */ AV_PKT_DATA_SKIP_SAMPLES, /** * An AV_PKT_DATA_JP_DUALMONO side data packet indicates that * the packet may contain "dual mono" audio specific to Japanese DTV * and if it is true, recommends only the selected channel to be used. * @code * u8 selected channels (0=mail/left, 1=sub/right, 2=both) * @endcode */ AV_PKT_DATA_JP_DUALMONO, /** * A list of zero terminated key/value strings. There is no end marker for * the list, so it is required to rely on the side data size to stop. */ AV_PKT_DATA_STRINGS_METADATA, /** * Subtitle event position * @code * u32le x1 * u32le y1 * u32le x2 * u32le y2 * @endcode */ AV_PKT_DATA_SUBTITLE_POSITION, /** * Data found in BlockAdditional element of matroska container. There is * no end marker for the data, so it is required to rely on the side data * size to recognize the end. 8 byte id (as found in BlockAddId) followed * by data. */ AV_PKT_DATA_MATROSKA_BLOCKADDITIONAL, /** * The optional first identifier line of a WebVTT cue. */ AV_PKT_DATA_WEBVTT_IDENTIFIER, /** * The optional settings (rendering instructions) that immediately * follow the timestamp specifier of a WebVTT cue. */ AV_PKT_DATA_WEBVTT_SETTINGS, /** * A list of zero terminated key/value strings. There is no end marker for * the list, so it is required to rely on the side data size to stop. This * side data includes updated metadata which appeared in the stream. */ AV_PKT_DATA_METADATA_UPDATE, /** * MPEGTS stream ID as uint8_t, this is required to pass the stream ID * information from the demuxer to the corresponding muxer. */ AV_PKT_DATA_MPEGTS_STREAM_ID, /** * Mastering display metadata (based on SMPTE-2086:2014). This metadata * should be associated with a video stream and contains data in the form * of the AVMasteringDisplayMetadata struct. */ AV_PKT_DATA_MASTERING_DISPLAY_METADATA, /** * This side data should be associated with a video stream and corresponds * to the AVSphericalMapping structure. */ AV_PKT_DATA_SPHERICAL, /** * Content light level (based on CTA-861.3). This metadata should be * associated with a video stream and contains data in the form of the * AVContentLightMetadata struct. */ AV_PKT_DATA_CONTENT_LIGHT_LEVEL, /** * ATSC A53 Part 4 Closed Captions. This metadata should be associated with * a video stream. A53 CC bitstream is stored as uint8_t in AVPacketSideData.data. * The number of bytes of CC data is AVPacketSideData.size. */ AV_PKT_DATA_A53_CC, /** * This side data is encryption initialization data. * The format is not part of ABI, use av_encryption_init_info_* methods to * access. */ AV_PKT_DATA_ENCRYPTION_INIT_INFO, /** * This side data contains encryption info for how to decrypt the packet. * The format is not part of ABI, use av_encryption_info_* methods to access. */ AV_PKT_DATA_ENCRYPTION_INFO, /** * Active Format Description data consisting of a single byte as specified * in ETSI TS 101 154 using AVActiveFormatDescription enum. */ AV_PKT_DATA_AFD, /** * Producer Reference Time data corresponding to the AVProducerReferenceTime struct, * usually exported by some encoders (on demand through the prft flag set in the * AVCodecContext export_side_data field). */ AV_PKT_DATA_PRFT, /** * ICC profile data consisting of an opaque octet buffer following the * format described by ISO 15076-1. */ AV_PKT_DATA_ICC_PROFILE, /** * DOVI configuration * ref: * dolby-vision-bitstreams-within-the-iso-base-media-file-format-v2.1.2, section 2.2 * dolby-vision-bitstreams-in-mpeg-2-transport-stream-multiplex-v1.2, section 3.3 * Tags are stored in struct AVDOVIDecoderConfigurationRecord. */ AV_PKT_DATA_DOVI_CONF, /** * Timecode which conforms to SMPTE ST 12-1:2014. The data is an array of 4 uint32_t * where the first uint32_t describes how many (1-3) of the other timecodes are used. * The timecode format is described in the documentation of av_timecode_get_smpte_from_framenum() * function in libavutil/timecode.h. */ AV_PKT_DATA_S12M_TIMECODE, /** * The number of side data types. * This is not part of the public API/ABI in the sense that it may * change when new side data types are added. * This must stay the last enum value. * If its value becomes huge, some code using it * needs to be updated as it assumes it to be smaller than other limits. */ AV_PKT_DATA_NB }; ``` | type | meaning | | -------------------------------------- | ------------------------------------------------------------ | | AV_PKT_DATA_PALETTE | Palette, data size by AVPALETTE_SIZE decision. | | AV_PKT_DATA_NEW_EXTRADATA | Used to inform the codec or format that the extradata buffer has changed, and the receiver should take appropriate measures to do so. The new extradata is embedded in the side data buffer and should be used immediately to process the current frame or packet. | | AV_PKT_DATA_PARAM_CHANGE | The layout is affected by the AVSideDataParamChangeFlags type. | | AV_PKT_DATA_H263_MB_INFO | It contains a lot of structure about macroblock information, which is related to dividing the packet into smaller packets at the edge of macroblock. | | AV_PKT_DATA_REPLAYGAIN | It is associated with audio stream and contains replay gain information in the form of AVReplayGain structure. | | AV_PKT_DATA_DISPLAYMATRIX | It contains a 3x3 transformation matrix, which describes an affine transformation, which needs to be applied to the decoded video frame to display correctly. | | AV_PKT_DATA_STEREO3D | It is associated with video stream and contains stereo 3D information in the form of avstereo 3D structure. | | AV_PKT_DATA_AUDIO_SERVICE_TYPE | Associated with an audio stream and corresponding to enum type enum AVAudioServiceType. | | AV_PKT_DATA_QUALITY_STATS | Contains quality related information from the encoder. | | AV_PKT_DATA_FALLBACK_TRACK | Contains an integer value that represents the stream index of the fallback track. | | AV_PKT_DATA_CPB_PROPERTIES | It corresponds to AVCPBProperties structure. | | AV_PKT_DATA_SKIP_SAMPLES | It is recommended to skip the specified number of samples. | | AV_PKT_DATA_JP_DUALMONO | Indicates that the packet may contain "dual mono" audio specific to Japanese DTV. If it is true, it is recommended to use only the selected channel. | | AV_PKT_DATA_STRINGS_METADATA | List of string key value pairs. | | AV_PKT_DATA_SUBTITLE_POSITION | The location of the subtitle event. | | AV_PKT_DATA_MATROSKA_BLOCKADDITIONAL | The data found in the BlockAdditional element of the matroska container. | | AV_PKT_DATA_WEBVTT_IDENTIFIER | The optional first identifier line of the WebVTT cue. | | AV_PKT_DATA_WEBVTT_SETTINGS | Optional setting (rendering description) after the timestamp specifier of WebVTT cue. | | AV_PKT_DATA_METADATA_UPDATE | List of string key value pairs. Include update metadata that appears in the stream. | | AV_PKT_DATA_MPEGTS_STREAM_ID | uint8_t type MPEGTS stream ID, which needs to transfer stream ID information from demuxer to corresponding muxer. | | AV_PKT_DATA_MASTERING_DISPLAY_METADATA | Mastering display metadata (based on SMPTE-2086:2014), which should be associated with video stream and stored in the form of avmasteringdisplay metadata structure. | | AV_PKT_DATA_SPHERICAL | It is associated with video stream and corresponds to avspherical mapping structure. | | AV_PKT_DATA_CONTENT_LIGHT_LEVEL | Content light level (based on CTA-861.3). The metadata should be associated with the video stream and stored in the form of AVContentLightMetadata structure. | | AV_PKT_DATA_A53_CC | ATSC A53 Part 4 Closed Captions. | | AV_PKT_DATA_ENCRYPTION_INIT_INFO | Encrypt initialization data. | | AV_PKT_DATA_ENCRYPTION_INFO | Contains encrypted information about how to decrypt a packet. | | AV_PKT_DATA_AFD | Active Format Description data. Describes the use of AVActiveFormatDescription in ETSI TS 101 154 to enumerate specified data consisting of a single byte. | | AV_PKT_DATA_PRFT | Producer reference time data corresponds to avproducer reference time structure, which is usually exported by some encoders (by exporting in AVCodecContext)_ side_ The prft tag is set in the data field. | | AV_PKT_DATA_ICC_PROFILE | ICC profile data consisting of opaque eight byte buffers in the format described in ISO 15076-1. | | AV_PKT_DATA_DOVI_CONF | DOVI configuration. | | AV_PKT_DATA_S12M_TIMECODE | Timecode in accordance with SMPTE ST 12-1:2014. | | AV_PKT_DATA_NB | Number of side data types. | reference material: 1. https://ffmpeg.org/doxygen/trunk/structAVPacket.html 2. https://ffmpeg.org/doxygen/trunk/structAVBufferRef.html 3. https://ffmpeg.org/doxygen/trunk/structAVBuffer.html 4. https://ffmpeg.org/doxygen/trunk/structAVPacketSideData.html 5. https://ffmpeg.org/doxygen/trunk/group__lavc__packet.html#ga9a80bfcacc586b483a973272800edb97 ================================================ FILE: FFmpeg 学习(一):FFmpeg 简介 .md ================================================ > 本文转载自博客园:https://www.cnblogs.com/renhui/p/6922971.html # 一、FFmpeg 介绍 FFmpeg是一套可以用来记录、转换数字音频、视频,并能将其转化为流的开源计算机程序。采用LGPL或GPL许可证。它提供了录制、转换以及流化音视频的完整解决方案。它包含了非常先进的音频/视频编解码库。 # 二、FFmpeg 组成 - libavformat:用于各种音视频[封装格式](https://baike.baidu.com/item/封装格式)的生成和解析,包括获取解码所需信息以生成解码上下文结构和读取音视频帧等功能; - libavcodec:用于各种类型声音/图像编解码; - libavutil:包含一些公共的工具函数; - libswscale:用于视频场景比例缩放、色彩映射转换; - libpostproc:用于后期效果处理; - ffmpeg:该项目提供的一个工具,可用于格式转换、解码或[电视卡](https://baike.baidu.com/item/电视卡)即时编码等; - ffsever:一个 HTTP 多媒体即时广播串流服务器; - ffplay:是一个简单的播放器,使用ffmpeg 库解析和解码,通过SDL显示; # 三、FFmpeg包含类库说明 ### 2.1 类库说明 - libavformat - 用于各种音视频封装格式的生成和解析,包括获取解码所需信息、读取音视频数据等功能。各种流媒体协议代码(如rtmpproto.c等)以及音视频格式的(解)复用代码(如flvdec.c、flvenc.c等)都位于该目录下。 - libavcodec - 音视频各种格式的编解码。各种格式的编解码代码(如aacenc.c、aacdec.c等)都位于该目录下。 - libavutil - 包含一些公共的工具函数的使用库,包括算数运算,字符操作等。 - libswscale - 提供原始视频的比例缩放、色彩映射转换、图像颜色空间或格式转换的功能。 - libswresample - 提供音频重采样,采样格式转换和混合等功能。 - libavfilter - 各种音视频滤波器。 - libpostproc - 用于后期效果处理,如图像的去块效应等。 - libavdevice - 用于硬件的音视频采集、加速和显示。 如果您之前没有阅读FFmpeg代码的经验,建议优先阅读libavformat、libavcodec以及libavutil下面的代码,它们提供了音视频开发的最基本功能,应用范围也是最广的。 ### 2.2 常用结构 FFmpeg里面最常用的数据结构,按功能可大致分为以下几类(以下代码行数,以branch: origin/release/3.4为准): #### 1. 封装格式 - AVFormatContext - 描述了媒体文件的构成及基本信息,是统领全局的基本结构体,贯穿程序始终,很多函数都要用它作为参数; - AVInputFormat - 解复用器对象,每种作为输入的封装格式(例如FLV、MP4、TS等)对应一个该结构体,如libavformat/flvdec.c的ff_flv_demuxer; - AVOutputFormat - 复用器对象,每种作为输出的封装格式(例如FLV, MP4、TS等)对应一个该结构体,如libavformat/flvenc.c的ff_flv_muxer; - AVStream - 用于描述一个视频/音频流的相关数据信息。 #### 2.编解码 - AVCodecContext - 描述编解码器上下文的数据结构,包含了众多编解码器需要的参数信息; - AVCodec - 编解码器对象,每种编解码格式(例如H.264、AAC等)对应一个该结构体,如libavcodec/aacdec.c的ff_aac_decoder。每个AVCodecContext中含有一个AVCodec; - AVCodecParameters - 编解码参数,每个AVStream中都含有一个AVCodecParameters,用来存放当前流的编解码参数。 #### 3. 网络协议 - AVIOContext - 管理输入输出数据的结构体; - URLProtocol - 描述了音视频数据传输所使用的协议,每种传输协议(例如HTTP、RTMP)等,都会对应一个URLProtocol结构,如libavformat/http.c中的ff_http_protocol; - URLContext - 封装了协议对象及协议操作对象。 #### 4. 数据存放 - AVPacket - 存放编码后、解码前的压缩数据,即ES数据; - AVFrame - 存放编码前、解码后的原始数据,如YUV格式的视频数据或PCM格式的音频数据等; ================================================ FILE: FFmpeg 学习(七):FFmpeg 学习整理总结.md ================================================ # 一、FFmpeg 播放视频的基本流程整理 播放流程: video.avi(Container) -> 打开得到 Video_Stream -> 读取Packet -> 解析到 Frame -> 显示Frame。 - Container:在音视频中的容器,一般指的是一种特定的文件格式(如 AVI/QT ),里面指明了所包含的音视频,字幕等相关信息。 - Stream:媒体流,指时间轴上的一段连续数据,如一段声音、视频或字幕数据。 - Packet:Stream中的Raw数据,包含了可以被解码成方便我们最后在应用程序中操作的帧的原始数据。 - Frame:Stream中的一个数据单元。 - Codec:编解码器(Code 和 Decode),如 Divx和 MP3,以帧为单位实现压缩数据和原始数据之间的相互转换。 # 二、FFmpeg 各个结构体及相关方法流程整理 ### 1. AVCodec AVCodec -- 编解码器,采用链表维护,每一个都有其对应的名字、类型、CodecID和对数据进行处理的编解码函数指针。 - avcodec_find_decoder/avcodec_find_encoder :根据给定的codec id或解码器名称从系统中搜寻并返回一个AVCodec结构的指针 - avcodec_alloc_context3:根据 AVCodec 分配合适的 AVCodecContext - avcodec_open/avcodec_open2/avcodec_close :根据给定的 AVCodec 打开对应的Codec,并初始化 AVCodecContext/ 关闭Codec - avcodec_alloc_frame:分配编解码需要的 AVFrame 结构 - avcodec_decode_video/avcodec_decode_video2 :解码一个视频帧,输入数据在AVPacket结构中,输出数据在AVFrame结构中 - avcodec_decode_audio4:解码一个音频帧。输入数据在AVPacket结构中,输出数据在AVFrame结构中 - avcodec_encode_video/avcodec_encode_video2 :编码一个视频帧,输入数据在AVFrame结构中,输出数据在AVPacket结构中 ### 2. AVCodecContext AVCodecContext -- 和具体媒体数据相关的编解码器上下文,保存AVCodec指针和与codec相关的数据,包含了流中所使用的关于编解码器的所有信息 - codec_name[32]、codec_type(AVMediaType)、codec_id(CodecID)、codec_tag:编解码器的名字、类型(音频/视频/字幕等)、ID(H264/MPEG4等)、FOURC等信息 - hight/width,coded_width/coded_height: Video的高宽 - sample_fmt:音频的原始采样格式, 是 SampleFormat 枚举 - time_base:采用分数(den/num)保存了帧率的信息 ### 3. AVFrame - data/linesize:FFMpeg内部以平面的方式存储原始图像数据,即将图像像素分为多个平面(R/G/B或Y/U/V)数组 - data数组:其中的指针指向各个像素平面的起始位置,编码时需要用户设置数据 - linesize数组 :存放各个存贮各个平面的缓冲区的行宽,编码时需要用户设置数据 - key_frame:该图像是否是关键帧,由 libavcodec 设置 - pict_type:该图像的编码类型:Intra(1)/Predicted(2)/Bi-dir(3) 等,默认值是 NONE(0),其值由libavcodec设置 - pts:呈现时间,编码时由用户设置 - quality:从1(最好)到FF_LAMBDA_MAX(256*128-1,最差),编码时用户设置,默认值是0 - nterlaced_frame:表明是否是隔行扫描的,编码时用户指定,默认0 ### 4. AVFormatContext AVFormatContext -- 格式转换过程中实现输入和输出功能、保存相关数据的主要结构,描述了一个媒体文件或媒体流的构成和基本信息 - nb_streams/streams :AVStream结构指针数组, 包含了所有内嵌媒体流的描述,其内部有 AVInputFormat + AVOutputFormat 结构体,来表示输入输出的文件格式 - avformat_open_input:创建并初始化部分值,但其他一些值(如 mux_rate、key 等)需要手工设置初始值,否则可能出现异常 - avformat_alloc_output_context2:根据文件的输出格式、扩展名或文件名等分配合适的 AVFormatContext 结构 ### 5. AVPacket AVPacket -- 暂存解码之前的媒体数据(一个音/视频帧、一个字幕包等)及附加信息(解码时间戳、显示时间戳、时长等),主要用于建立缓冲区并装载数据。 - data/size/pos: 数据缓冲区指针、长度和媒体流中的字节偏移量 - flags:标志域的组合,1(AV_PKT_FLAG_KEY)表示该数据是一个关键帧, 2(AV_PKT_FLAG_CORRUPT)表示该数据已经损坏 - destruct:释放数据缓冲区的函数指针,其值可为 [av_destruct_packet]/av_destruct_packet_nofree, 会被 av_free_packet 调用 ### 6. AVStream AVStream -- 描述一个媒体流,其大部分信息可通过 avformat_open_input 根据文件头信息确定,其他信息可通过 avformat_find_stream_info 获取,典型的有 视频流、中英文音频流、中英文字幕流(Subtitle),可通过 av_new_stream、avformat_new_stream 等创建。 - index:在AVFormatContext中流的索引,其值自动生成(AVFormatContext::streams[index]) - nb_frames:流内的帧数目 - time_base:流的时间基准,是一个实数,该流中媒体数据的pts和dts都将以这个时间基准为粒度。通常,使用av_rescale/av_rescale_q可以实现不同时间基准的转换 - avformat_find_stream_info:获取必要的编解码器参数(如 AVMediaType、CodecID ),设置到 AVFormatContext::streams[i]::codec 中 - av_read_frame:从多媒体文件或多媒体流中读取媒体数据,获取的数据由 AVPacket 来存放 - av_seek_frame:改变媒体文件的读写指针来实现对媒体文件的随机访问,通常支持基于时间、文件偏移、帧号(AVSEEK_FLAG_FRAME)的随机访问方式 ================================================ FILE: FFmpeg 学习(三):将 FFmpeg 移植到 Android平台.md ================================================ 首先需要去FFmpeg的官网http://www.ffmpeg.org/去下载FFmpeg的源码,目前的版本号为FFmpeg3.3(Hilbert)。 下载的文件为压缩包,解压后得到ffmpeg-3.3目录。 **修改ffmpeg-3.3的configure文件:** ``` # 原来的配置内容: SLIBNAME_WITH_MAJOR='$(SLIBNAME).$(LIBMAJOR)' LIB_INSTALL_EXTRA_CMD='$$(RANLIB)"$(LIBDIR)/$(LIBNAME)"' SLIB_INSTALL_NAME='$(SLIBNAME_WITH_VERSION)' SLIB_INSTALL_LINKS='$(SLIBNAME_WITH_MAJOR)$(SLIBNAME)' #替换后的内容: SLIBNAME_WITH_MAJOR='$(SLIBPREF)$(FULLNAME)-$(LIBMAJOR)$(SLIBSUF)' LIB_INSTALL_EXTRA_CMD='$$(RANLIB)"$(LIBDIR)/$(LIBNAME)"' SLIB_INSTALL_NAME='$(SLIBNAME_WITH_MAJOR)' SLIB_INSTALL_LINKS='$(SLIBNAME)' ``` 原因:如果不修改配置,直接进行编译出来的so文件类似libavcodec.so.55.39.101,文件的版本号位于so之后,这样在Android上无法加载,所以需要修改! **编写build_android.sh脚本文件:** 在编译FFmpeg之前需要进行配置,设置相应的环境变量等。所有的配置选项都在ffmpeg-3.3/configure这个脚本文件中,执行如下命令可查看所有的配置选项: $ ./configure –help 下面将配置项和环境变量设置写成一个sh脚本文件来运行以便编译出Android平台需要的so文件出来。 build_android.sh的内容如下: ``` #!/bin/bash NDK=/Users/renhui/framework/android-ndk-r14b SYSROOT=$NDK/platforms/android-9/arch-arm/ TOOLCHAIN=$NDK/toolchains/arm-linux-androideabi-4.9/prebuilt/darwin-x86_64 function build_one { ./configure \ --prefix=$PREFIX \ --enable-shared \ --disable-static \ --disable-doc \--enable-cross-compile \ --cross-prefix=$TOOLCHAIN/bin/arm-linux-androideabi- \ --target-os=linux \ --arch=arm \ --sysroot=$SYSROOT \ --extra-cflags="-Os -fpic $ADDI_CFLAGS" \ --extra-ldflags="$ADDI_LDFLAGS" \ $ADDITIONAL_CONFIGURE_FLAG } CPU=arm PREFIX=$(pwd)/android/$CPU ADDI_CFLAGS="-marm" build_one ``` 需要确定的是NDK,SYSROOT和TOOLCHAIN是否是本地的环境,并确定cross-prefix指向的路径存在。 保存脚本文件后,将脚本的权限提升: ``` chmod 777 build_android.sh ``` 然后执行脚本,该脚本会完成对ffmpeg的配置,并生成config.h等配置文件,后面的编译会用到。如果未经过配置直接进行编译会提示无法找到config.h文件等错误。 然后执行下面两个命令: ``` $make $make install ``` 至此,会在ffmpeg-3.3目录下生成一个android目录,其/android/arm/lib目录下的so库文件就是能够在Android上运行的so库。 创建Demo工程,测试上面生成的so文件能否正常使用: 1. 创建一个新的Android工程 2. 在工程根目录下创建jni文件夹 3. 在jni下创建prebuilt目录,然后:将上面编译成功的so文件放入到该目录下 4. 创建包含native方法的类,先在src下创建cn.renhui包,然后创建FFmpegNative.java类文件。主要包括加载so库文件和一个native测试方法两部分,其内容如下: ``` package cn.renhui; public class FFmpegNative { static { System.loadLibrary("avutil-55"); System.loadLibrary("avcodec-57"); System.loadLibrary("swresample-2"); System.loadLibrary("avformat-57"); System.loadLibrary("swscale-4"); System.loadLibrary("avfilter-6"); System.loadLibrary("avdevice-57"); System.loadLibrary("ffmpeg_codec"); } public native int avcodec_find_decoder(int codecID); } ``` 1. 用javah创建.头文件: classes目录,执行:javah-jni cn.renhui.FFmpegNative,会在当前目录产生cn_renhui_FFmpegNative.h的C头文件; 1. 根据头文件名,建立相同名字c文件cn_renhui_FFmpegNative.c,在这个源文件中实现头文件中定义的方法,代码如下: ``` #include "cn_renhui_FFmpegNative.h" #ifdef __cplusplus extern "C" { #endif JNIEXPORT jint JNICALL Java_cn_renhui_FFmpegNative_avcodec_1find_1decoder (JNIEnv *env, jobject obj, jint codecID) { AVCodec *codec = NULL; /* register all formats and codecs */ av_register_all(); codec = avcodec_find_decoder(codecID); if (codec != NULL) { return 0; } else { return -1; } } #ifdef __cplusplus } #endif ``` 1. 编写Android.mk,内容如下: ``` LOCAL_PATH := $(call my-dir) include $(CLEAR_VARS) LOCAL_MODULE := avcodec-57-prebuilt LOCAL_SRC_FILES := prebuilt/libavcodec-57.so include $(PREBUILT_SHARED_LIBRARY) include $(CLEAR_VARS) LOCAL_MODULE := avdevice-57-prebuilt LOCAL_SRC_FILES := prebuilt/libavdevice-57.so include $(PREBUILT_SHARED_LIBRARY) include $(CLEAR_VARS) LOCAL_MODULE := avfilter-6-prebuilt LOCAL_SRC_FILES := prebuilt/libavfilter-6.so include $(PREBUILT_SHARED_LIBRARY) include $(CLEAR_VARS) LOCAL_MODULE := avformat-57-prebuilt LOCAL_SRC_FILES := prebuilt/libavformat-57.so include $(PREBUILT_SHARED_LIBRARY) include $(CLEAR_VARS) LOCAL_MODULE := avutil-55-prebuilt LOCAL_SRC_FILES := prebuilt/libavutil-55.so include $(PREBUILT_SHARED_LIBRARY) include $(CLEAR_VARS) LOCAL_MODULE := avswresample-2-prebuilt LOCAL_SRC_FILES := prebuilt/libswresample-2.so include $(PREBUILT_SHARED_LIBRARY) include $(CLEAR_VARS) LOCAL_MODULE := swscale-4-prebuilt LOCAL_SRC_FILES := prebuilt/libswscale-4.so include $(PREBUILT_SHARED_LIBRARY) include $(CLEAR_VARS) LOCAL_MODULE := ffmpeg_codec LOCAL_SRC_FILES := cn_dennishucd_FFmpegNative.c LOCAL_LDLIBS := -llog -ljnigraphics -lz -landroid LOCAL_SHARED_LIBRARIES := avcodec-57-prebuilt avdevice-57-prebuilt avfilter-6-prebuilt avformat-57-prebuilt avutil-55-prebuilt include $(BUILD_SHARED_LIBRARY) ``` 1. 编译so文件,执行ndk-build 2. 新建一个Activity,进行测试,测试核心代码: ``` FFmpegNative ffmpeg = new FFmpegNative(); int codecID = 28; int res = ffmpeg.avcodec_find_decoder(codecID); if (res == 0) { tv.setText("Success!"); } else { tv.setText("Failed!"); } ``` ​ 28是H264的编解码ID,可以在ffmpeg的源代码中找到,它是枚举类型定义的。在C语言中,可以换算为整型值。这里测试能否找到H264编解码,如果能找到,说明调用ffmpeg的库函数是成功的,这也表明我们编译的so文件是基本可用。 ================================================ FILE: FFmpeg 学习(二):Mac下安装FFmpeg.md ================================================ > 本文转载自博客园:https://www.cnblogs.com/renhui/p/8458150.html # 一、安装ffmpeg 分为两种安装方式: ### 1. 命令行安装 ``` brew install ffmpeg ``` ### 2. 下载压缩包安装 去 http://evermeet.cx/ffmpeg/ 下载7z压缩包,解压缩后,将ffmpeg文件拷贝到一个地方,然后在bash_profile里面配置好环境变量 # 二、安装ffplay 分为两种安装方式: ### 1. 命令行安装 执行下面的命令就可以进行安装操作 ``` brew install ffmpeg --with-ffplay ``` > - 注:目前使用此安装方式安装后,执行ffplay会出现command not found的问题,可能是因为SDL的配置问题导致的。 ### 2. 下载压缩包安装 去 http://evermeet.cx/ffmpeg/ 下载7z压缩包,解压缩后,将ffplay文件拷贝到一个地方,然后在bash_profile里面配置好环境变量 # 三、附言 在上面我们接触到了命令行安装ffmpeg的方法,除了安装选项 --with-ffplay外还有更多的选项如下: ```xml –with-fdk-aac (Enable the Fraunhofer FDK AAC library) –with-ffplay (Enable FFplay media player) –with-freetype (Build with freetype support) –with-frei0r (Build with frei0r support) –with-libass (Enable ASS/SSA subtitle format) –with-libcaca (Build with libcaca support) –with-libvo-aacenc (Enable VisualOn AAC encoder) –with-libvorbis (Build with libvorbis support) –with-libvpx (Build with libvpx support) –with-opencore-amr (Build with opencore-amr support) –with-openjpeg (Enable JPEG 2000 image format) –with-openssl (Enable SSL support) –with-opus (Build with opus support) –with-rtmpdump (Enable RTMP protocol) –with-schroedinger (Enable Dirac video format) –with-speex (Build with speex support) –with-theora (Build with theora support) –with-tools (Enable additional FFmpeg tools) –without-faac (Build without faac support) –without-lame (Disable MP3 encoder) –without-x264 (Disable H.264 encoder) –without-xvid (Disable Xvid MPEG-4 video encoder) –devel (install development version 2.1.1) –HEAD (install HEAD version) ``` ================================================ FILE: FFmpeg 学习(五):FFmpeg 编解码 API 分析.md ================================================ 在上一篇文章 FFmpeg学习(四):FFmpeg API 介绍与通用 API 分析 中,我们简单的讲解了一下FFmpeg 的API基本概念,并分析了一下通用API,本文我们将分析 FFmpeg 在编解码时使用的API。 ## 一、FFmpeg 解码 API 分析 ### 1. avformat_open_input 分析 函数 avformat_open_input 会根据所提供的文件路径判断文件的格式,其实就是通过这一步来决定到底是使用哪个Demuxer。 举个例子:如果是flv,那么Demuxer就会使用对应的ff_flv_demuxer,所以对应的关键生命周期的方法read_header、read_packet、read_seek、read_close都会使用该flv的Demuxer中函数指针指定的函数。read_header会将AVStream结构体构造好,以方便后续的步骤继续使用AVStream作为输入参数。 ### 2. avformat_find_stream_info 分析 该方法的作用就是把所有的Stream的MetaData信息填充好。方法内部会先查找对于的解码器,然后打开对应的解码器,紧接着会利用Demuxer中的read_packet函数读取一段数据进行解码,当然,解码的数据越多,分析出来的流信息就越准确,如果是本地资源,那么很快就可以得到准确的信息了。但是对于网络资源来说,则会比较慢,因此该函数有几个参数可以控制读取数据的长度,一个是probe size,一个是max_analyze_duration, 还有一个就是fps_probe_size,这三个参数共同控制解码数据的长度,如果配置的这几个参数的数值越小,那么这个函数执行的时间就会越快,但会导致AVStream结构体里面的信息(视频的宽、高、fps、编码类型)不准确。 ### 3. av_read_frame 分析 该方法读取出来的数据是AVPacket,在FFmpeg的早期版本中开发给开发者的函数其实就是av_read_packet,但是需要开发者自己来处理AVPacket中的数据不能被解码器处理完的情况,即需要把未处理完的压缩数据缓存起来的问题。所以在新版本的FFmpeg中,提供了该函数,用于处理此状况。 该函数的实现首先会委托到Demuxer的read_packet方法中,当然read_packet通过解服用层和协议层的处理后,会将数据返回到这里,在该函数中进行数据缓冲处理。 对于音频流,一个AVPacket可能会包含多个AVFrame,但是对于一个视频流,一个AVPacket只包含一个AVFrame,该函数最终只会返回一个AVPacket结构体。 ### 4. avcodec_decode分析 该方法包含了两部分内容:一部分是解码视频,一部分是解码音频。在上面的函数分析中,我们知道,解码是会委托给对应的解码器来实施的,在打开解码器的时候就找到了对应的解码器的实现,比如对于解码H264来讲,会找到ff_h264_decoder,其中会有对应的生命周期函数的实现,最重要的就是init,decode,close三个方法,分别对应于打开解码器、解码及关闭解码器的操作,而解码过程就是调用decode方法。 ### 5. avformat_close_input 分析 该函数负责释放对应的资源,首先会调用对应的Demuxer中的生命周期read_close方法,然后释放掉,AVFormatContext,最后关闭文件或者远程网络链接。 ## 二、FFmpeg 编码 API 分析 ### 1. avformat_alloc_output_context2 分析 该函数内部需要调用方法avformat_alloc_context来分配一个AVFormatContext结构体,当然最关键的还是根据上一步注册的Muxer和Demuxer部分(也就是封装格式部分)去找对应的格式。有可能是flv格式、MP4格式、mov格式,甚至是MP3格式等,如果找不到对应的格式(应该是因为在configure选项中没有打开这个格式的开关),那么这里会返回找不到对于的格式的错误提示。在调用API的时候,可以使用av_err2str把返回的整数类型的错误代码转换为肉眼可读的字符串,这是个在调试中非常有用的工具函数。该函数最终会将找出来的格式赋值给AVFormatContext类型的oformat。 ### 2. avio_open2 分析 首先会调用函数ffurl_open,构造出URLContext结构体,这个结构体中包含了URLProtocol(需要去第一步register_protocol中已经注册的协议链表)中去寻找;接着会调用avio_alloc_contex方法,分配出AVIOContext结构体,并将上一步构造出来的URLProtocol传递进来;然后把上一步分配出来的AVIOContext结构体赋值给AVFormatContext属性。 下面就是针对上面的描述总结的结构之间的构架图,各位可以参考此图进行进一步的理解: ![img](https://images2018.cnblogs.com/blog/682616/201807/682616-20180720100812049-1285283157.png) avio_open2的过程也恰好是在上面我们分析avformat_open_input过程的一个逆过程。编码过程和解码过程从逻辑上来讲,也是一个逆过程,所以在FFmpeg实现的过程中,他们也互为逆过程。 ### 3. 编码其他API(步骤)分析 编码的其他步骤也是解码的一个逆过程,解码过程中的avformat_find_stream_info对应到编码就是avformat_new_stream和avformat_write_header。 - avformat_new_stream函数会将音频流或者视频流的信息填充好,分配出AVStream结构体,在音频流中分配声道、采样率、表示格式、编码器等信息,在视频中分配宽、高、帧率、表示格式、编码器等信息。 - avformat_write_header函数与解码过程中的read_header恰好是一个逆过程,这里就不多赘述了。 接下来就是编码阶段了: 1. 将手动封装好的AVFrame结构体,作为avcodec_encodec_video方法的输入,然后将其编码成为AVPacket,然后调用av_write_frame方法输出到媒体文件中。 2. av_write_frame 方法会将编码后的AVPacket结构体作为Muxer中的write_packet生命周期方法的输入,write_packet会加上自己封装格式的头信息,然后调用协议层,写到本地文件或者网络服务器上。 3. 最后一步就是av_write_trailer(该函数有一个非常大的坑,如果没执行write_header操作,就直接执行write_trailer操作,程序会直接Carsh掉,所以这两个函数必须成对出现),av_write_trailer会把没有输出的AVPacket全部丢给协议层去做输出,然后会调用Muxer的write_trailer生命周期方法(不同的格式,写出的尾部也不一样)。 ## 三、FFmpeg 解码 API 超时设置 当视频流地址能打开,但是视频流中并没有流内容的时候,可能会导致整体执行流程阻塞在 avformat_open_input 或者 av_read_frame 方法上。 主要原因就是avformat_open_input 和av_read_frame 这两个方法是阻塞的。 av_read_frame() -> read_frame_internal() -> ff_read_packet() -> s->iformat->read_packet() -> read_from_url() -> ffurl_read() -> retry_transfer_wrapper() (此方法会堵塞) 虽然我们可以通过设置 ic->flags |= AVFMT_FLAG_NONBLOCK; 将操作设置为非阻塞,但这样设置是不推荐的,会导致后续的其他操作出现问题。 一般情况下,我们推荐另外两种机制进行设置: ### 1. 设置开流的超时时间 在设置开流超时时间的时候,需要注意 不同的协议设置的方式是不一样的。 ``` 方法:timeout --> 单位:(http:ms udp:s)``方法:stimeout --> 单位:(rtsp us)  ``` 设置udp、http 超时的示例代码如下: ``` AVDictionary* opts = NULL; av_dict_set(&opts, "timeout", "3000000", 0);//单位 如果是http:ms 如果是udp:s int ret = avformat_open_input(&ctx, url, NULL, &opts); ``` 设置rtsp超时的示例代码如下: ``` AVDictionary* opts = NULL; av_dict_set(&opts, "rtsp_transport", m_bTcp ? "tcp" : "udp", 0); //设置tcp or udp,默认一般优先tcp再尝试udp av_dict_set(&opts, "stimeout", "3000000", 0);//单位us 也就是这里设置的是3s ret = avformat_open_input(&ctx, url, NULL, &opts); ``` ### 2. 设置interrupt_callback定义返回机制 设置回调,监控read超时情况,回调方法为: ``` int64_t lastReadPacktTime; static int interrupt_cb(void *ctx) { int timeout = 3; if (av_gettime() - lastReadPacktTime > timeout * 1000 * 1000) { return -1; } return 0; } ``` 回调函数中返回0则代表ffmpeg继续阻塞直到ffmpeg正常工作为止,否则就代表ffmpeg结束阻塞可以将操纵权交给用户线程并返回错误码。 对指定的 AVFormatContext 进行设置,并在需要调用的设置的时间之前,记录当前的时间,这样在回调的时候就能根据时间差,判断执行相应的逻辑: avformat_open_input 设置方式: ``` inputContext = avformat_alloc_context(); lastReadPacktTime = av_gettime(); inputContext->interrupt_callback.callback = interrupt_cb; int ret = avformat_open_input(&inputContext, inputUrl.c_str(), nullptr, nullptr); ``` av_read_frame 设置方式: ``` lastReadPacktTime = av_gettime(); ret = av_read_frame(inputContext, packet); ``` 在实际开发中,只是设计这个机制,很容易出现超时,但如果超时时间设置过程,又容易阻塞线程。一般推荐的方案为:在超时的机制上增加连续读流的时长统计,当连续读流超时超过一定时间时就通知当前读流操作已失败。 ================================================ FILE: FFmpeg 学习(六):FFmpeg 核心模块 libavformat 与 libavcodec 分析.md ================================================ # 一、libavformat介绍 libavformat的主要组成与层次调用关系如下图: ![image](https://user-images.githubusercontent.com/87457873/148345134-91ce7724-18ec-4b1b-823b-12238e9c7a31.png) AVFromatContext是API层直接接触到的结构体,它会进行格式的封装和解封装,它的数据部分由底层提供,底层使用了AVIOContext,这个AVIOContext实际上就是为普通的I/O增加了一层Buffer缓冲区,再往底层就是URLContext,也就是达到了协议层,协议层的实现由很多,如rtmp、http、hls、file等,这个就是libavformat的内部封装结构了。 # 二、libavcodec介绍 libavcodec模块的主要组成和数据结构图如下: ![img](https://images2018.cnblogs.com/blog/682616/201807/682616-20180720180926182-1853199081.png) 对于开发者来说,这个模块我们能接触到的最顶层的数据结构就是AVCodecContext,该结构体包含的就是与实际的编解码有关的部分。 首先AVCodecContext是包含在一个AVStream里面的,即描述了这路流的编码格式是什么,然后利用该编码器或者解码器进行AVPacket与AVFrame之间的转换(实际上就是编码或者解码的过程),这是FFmpeg中最重要的一部分。 ================================================ FILE: FFmpeg 学习(四):FFmpeg API 介绍与通用 API 分析.md ================================================ ## 一、FFmpeg 编解码流程 FFmpeg编解码流程图如下,此图包含了整体的解封装、编解码的基本流程。 ![img](https://img2020.cnblogs.com/blog/682616/202104/682616-20210402124122366-1492811886.png) 下面我们要介绍的术语及相关API都是围绕这个流程图展开的。 ## 二、FFmpeg 相关术语 **1. 容器/文件(Container/File)**:即特定格式的多媒体文件,比如MP4,flv,mov等。 **2. 媒体流(Stream)**:表示在时间轴上的一段连续的数据,比如一段声音数据、一段视频数据或者一段字母数据,可以是压缩的,也可以是非压缩的,压缩的数据需要关联特定的编解码器。 **3. 数据帧/数据包(Frame/Packet)**:通常一个媒体流是由大量的数据帧组成的,对于压缩数据,帧对应着编解码器的最小处理单元,分属于不同媒体流的数据帧交错存储与容器之中。 **4. 编解码器**:编解码器是以帧为单位实现压缩数据和原始数据之间的相互转换的。 前面介绍的术语,就是FFmpeg中抽象出来的概念。其中: **1. AVFormatContext**:就是对容器或者媒体文件层次的抽象。 **2. AVStream**:在文件中(容器里面)包含了多路流(音频流、视频流、字幕流),AVStream 就是对流的抽象。 **3. AVCodecContext 与 AVCodec**:在每一路流中都会描述这路流的编码格式,对编解码器格式以及编解码器的抽象就是AVCodecContext 与 AVCodec。 **4. AVPacket 与 AVFrame**:对于编码器或者解码器的输入输出部分,也就是压缩数据以及原始数据的抽象就是AVPacket与AVFrame。 **5. AVFilte**r:除了编解码之外,对音视频的处理肯定是针对于原始数据的处理,也就是针对AVFrame的处理,使用的就是AVFilter。 ## 三、FFmpeg 通用 API 分析 ### 1. av_register_all 分析 在最开始编译FFmpeg的时候,我们做了一个configure的配置,其中开启或者关闭了很多选项。configure的配置会生成两个文件:config.mk和config.h。 > config.mk:就是makefile文件需要包含进去的子模块,会作用在编译阶段,帮助开发者编译出正确的库。 > > config.h:作用在运行阶段,主要是确定需要注册那些容器及编解码格式到FFmpeg框架中。 调用 av_register_all 就可以注册config.h里面开发的编解码器,然后会注册所有的Muxer和Demuxer(封装格式),最后注册所有的Protocol(协议)。 这样在configure时开启或者关闭的选项就作用到了运行时,该函数的源码分析设计的源码文件包括:url.c、allformats.c、mux.c、format.c 等文件。已经将这几个源码文件单独提出来了,并放在百度网盘上了,地址:https://pan.baidu.com/s/1p8-ish6oeRTaUs84juQtHg。 ### 2. av_find_codec 分析 这个方法包含了两部分的内容:一部分是寻找解码器,一部分是寻找编码器。其实在av_register_all的函数执行时,就已经把编码器和解码器都存放到一个链表中了。这里寻找编解码器就是从上一步构造的链表中遍历,通过Codec的ID或者name进行条件匹配,最终返回对于的Codec。 ### 3. avcodec_open2 分析 该函数是打开编解码器(Codec)的函数,无论是编码过程还是解码过程,都会用到这个函数。该函数的输入参数有三个:第一个是AVCodecContext,解码过程由FFmpeg引擎填充,编码过程由开发者自己构造,如果想传入私有参数,则为它的priv_data设置参数;第二个参数是上一步通过av_find_codec寻找出来的编解码器(Codec);第三个参数一般传NULL。 ### 4. avcodec_close 分析 如果理解了avcodec_open,那么对应的close就是一个逆过程,找到对应的实现文件中的close函数指针所只指向的函数,然后该函数会调用对应第三方库的API来关闭掉对应的编码库。 ## 四、总结 本文主要是讲述了FFmpeg的相关术语,并讲解了一下通用的API的分析,不难看出其实FFmpeg所做的事情就是透明化所有的编解码库,用自己的封装来为开发者提供统一的接口。开发者使用不同的编码库时,只需要指明要用哪一个即可,这也充分体现了面向对象编程中的封装特性。 ================================================ FILE: FFmpeg 开发之 AVFilter 使用流程总结.md ================================================ 在使用FFmpeg开发时,使用AVFilter的流程较为复杂,涉及到的数据结构和函数也比较多,那么使用FFmpeg AVFilter的整体流程是什么样,在其执行过程中都有哪些步骤,需要注意哪些细节?这些都是需要我们整理和总结的。 首先,我们需要引入三个概念结构体:AVFilterGraph 、AVFilterContext、AVFilter。 ## 一、AVFilterGraph 、AVFilterContext、AVFilter 在 FFmpeg 中有多种多样的滤镜,你可以把他们当成一个个小工具,专门用于处理视频和音频数据,以便实现一定的目的。如 overlay 这个滤镜,可以将一个图画覆盖到另一个图画上;transport 这个滤镜可以将图画做旋转等等。 一个 filter 的输出可以作为另一个 filter 的输入,因此多个 filter 可以组织成为一个网状的 filter graph,从而实现更加复杂或者综合的任务。 在 libavfilter 中,我们用类型 AVFilter 来表示一个 filter,每一个 filter 都是经过注册的,其特性是相对固定的。而 AVFilterContext 则表示一个真正的 filter 实例,这和 AVCodec 以及 AVCodecContext 的关系是类似的。 AVFilter 中最重要的特征就是其所需的输入和输出。 AVFilterContext 表示一个 AVFilter 的实例,我们在实际使用 filter 时,就是使用这个结构体。AVFilterContext 在被使用前,它必须是 被初始化的,就是需要对 filter 进行一些选项上的设置,通过初始化告诉 FFmpeg 我们已经做了相关的配置。 AVFilterGraph 表示一个 filter graph,当然它也包含了 filter chain的概念。graph 包含了诸多 filter context 实例,并负责它们之间的 link,graph 会负责创建,保存,释放 这些相关的 filter context 和 link,一般不需要用户进行管理。除此之外,它还有线程特性和最大线程数量的字段,和filter context类似。graph 的操作有:分配一个graph,往graph中添加一个filter context,添加一个 filter graph,对 filter 进行 link 操作,检查内部的link和format是否有效,释放graph等。 ## 二、AVFilter 相关Api使用方法整理 ### 1. AVFilterContext 初始化方法 AVFilterContext 的初始化方式有三种,avfilter_init_str() 和 avfilter_init_dict()、avfilter_graph_create_filter(). ``` /* 使用提供的参数初始化 filter。 参数args:表示用于初始化 filter 的 options。该字符串必须使用 ":" 来分割各个键值对, 而键值对的形式为 'key=value'。如果不需要设置选项,args为空。 除了这种方式设置选项之外,还可以利用 AVOptions API 直接对 filter 设置选项。 返回值:成功返回0,失败返回一个负的错误值 */ int avfilter_init_str(AVFilterContext *ctx, const char *args); ``` ``` /* 使用提供的参数初始化filter。 参数 options:以 dict 形式提供的 options。 返回值:成功返回0,失败返回一个负的错误值 注意:这个函数和 avfilter_init_str 函数的功能是一样的,只不过传递的参数形式不同。 但是当传入的 options 中有不被 filter 所支持的参数时,这两个函数的行为是不同: avfilter_init_str 调用会失败,而这个函数则不会失败,它会将不能应用于指定 filter 的 option 通过参数 options 返回,然后继续执行任务。 */ int avfilter_init_dict(AVFilterContext *ctx, AVDictionary **options); ``` ``` /** * 创建一个Filter实例(根据args和opaque的参数),并添加到已存在的AVFilterGraph. * 如果创建成功*filt_ctx会指向一个创建好的Filter实例,否则会指向NULL. * @return 失败返回负数,否则返回大于等于0的数 */ int avfilter_graph_create_filter(AVFilterContext **filt_ctx, const AVFilter *filt, const char *name,                   const char *args, void *opaque, AVFilterGraph *graph_ctx); ``` ### 2. AVFilterGraph 相关的Api AVFilterGraph 表示一个 filter graph,当然它也包含了 filter chain的概念。graph 包含了诸多 filter context 实例,并负责它们之间的 link,graph 会负责创建,保存,释放 这些相关的 filter context 和 link,一般不需要用户进行管理。 graph 的操作有:分配一个graph,往graph中添加一个filter context,添加一个 filter graph,对 filter 进行 link 操作,检查内部的link和format是否有效,释放graph等。 根据上述操作,可以列举的方法分别为: **分配空的filter graph:** ``` /* 分配一个空的 filter graph. 成功返回一个 filter graph,失败返回 NULL */ AVFilterGraph *avfilter_graph_alloc(void); ``` **创建一个新的filter实例:** ``` /* 在 filter graph 中创建一个新的 filter 实例。这个创建的实例尚未初始化。 详细描述:在 graph 中创建一个名称为 name 的 filter类型的实例。 创建失败,返回NULL。创建成功,返回 filter context实例。创建成功后的实例会加入到graph中, 可以通过 AVFilterGraph.filters 或者 avfilter_graph_get_filter() 获取。 */ AVFilterContext *avfilter_graph_alloc_filter(AVFilterGraph *graph, const AVFilter *filter, const char *name); ``` **返回名字为name的filter context:** ``` /* 返回 graph 中的名为 name 的 filter context。 */ AVFilterContext *avfilter_graph_get_filter(AVFilterGraph *graph, const char *name); ``` **在 filter graph 中创建一个新的 filter context 实例,并使用args和opaque初始化这个filter context:** ``` /* 在 filter graph 中创建一个新的 filter context 实例,并使用 args 和 opaque 初始化这个实例。 参数 filt_ctx:返回成功创建的 filter context 返回值:成功返回正数,失败返回负的错误值。 */ int avfilter_graph_create_filter(AVFilterContext **filt_ctx, const AVFilter *filt, const char *name,                        const char *args, void *opaque, AVFilterGraph *graph_ctx); ``` **配置 AVFilterGraph 的链接和格式:** ``` /* 检查 graph 的有效性,并配置其中所有的连接和格式。 有效则返回 >= 0 的数,否则返回一个负值的 AVERROR. */ int avfilter_graph_config(AVFilterGraph *graphctx, void *log_ctx); ``` **释放AVFilterGraph:** ``` /* 释放graph,摧毁内部的连接,并将其置为NULL。 */ void avfilter_graph_free(AVFilterGraph **graph); ``` **在一个已经存在的link中插入一个FilterContext:** ``` /* 在一个已经存在的 link 中间插入一个 filter context。 参数filt_srcpad_idx和filt_dstpad_idx:指定filt要连接的输入和输出pad的index。 成功返回0. */ int avfilter_insert_filter(AVFilterLink *link, AVFilterContext *filt,                   unsigned filt_srcpad_idx, unsigned filt_dstpad_idx); ``` 将字符串描述的filter graph 加入到一个已存在的graph中: ``` /* 将一个字符串描述的 filter graph 加入到一个已经存在的 graph 中。 注意:调用者必须提供 inputs 列表和 outputs 列表。它们在调用这个函数之前必须是已知的。 注意:inputs 参数用于描述已经存在的 graph 的输入 pad 列表,也就是说,从新的被创建的 graph 来讲,它们是 output。 outputs 参数用于已经存在的 graph 的输出 pad 列表,从新的被创建的 graph 来说,它们是 input。 成功返回 >= 0,失败返回负的错误值。 */ int avfilter_graph_parse(AVFilterGraph *graph, const char *filters, AVFilterInOut *inputs, AVFilterInOut *outputs, void *log_ctx); ``` ``` /* 和 avfilter_graph_parse 类似。不同的是 inputs 和 outputs 参数,即做输入参数,也做输出参数。 在函数返回时,它们将会保存 graph 中所有的处于 open 状态的 pad。返回的 inout 应该使用 avfilter_inout_free() 释放掉。 注意:在字符串描述的 graph 中,第一个 filter 的输入如果没有被一个字符串标识,默认其标识为"in",最后一个 filter 的输出如果没有被标识,默认为"output"。 intpus:作为输入参数是,用于保存已经存在的graph的open inputs,可以为NULL。 作为输出参数,用于保存这个parse函数之后,仍然处于open的inputs,当然如果传入为NULL,则并不输出。 outputs:同上。 */ int avfilter_graph_parse_ptr(AVFilterGraph *graph, const char *filters, AVFilterInOut **inputs, AVFilterInOut **outputs, void *log_ctx); ``` ``` /* 和 avfilter_graph_parse_ptr 函数类似,不同的是,inputs 和 outputs 函数不作为输入参数, 仅作为输出参数,返回字符串描述的新的被解析的graph在这个parse函数后,仍然处于open状态的inputs和outputs。 返回的 inout 应该使用 avfilter_inout_free() 释放掉。 成功返回0,失败返回负的错误值。 */ int avfilter_graph_parse2(AVFilterGraph *graph, const char *filters, AVFilterInOut **inputs, AVFilterInOut **outputs); ``` **将graph转换为可读取的字符串描述:** ``` /* 将 graph 转化为可读的字符串描述。 参数options:未使用,忽略它。 */ char *avfilter_graph_dump(AVFilterGraph *graph, const char *options); ``` ## 三、FFmpeg Filter Buffer 和 BufferSink 相关APi的使用方法整理 Buffer 和 BufferSink 作为 graph 的输入点和输出点来和我们交互,我们仅需要和其进行数据交互即可。其API如下: ``` //buffersrc flag enum { //不去检测 format 的变化 AV_BUFFERSRC_FLAG_NO_CHECK_FORMAT = 1, //立刻将 frame 推送到 output AV_BUFFERSRC_FLAG_PUSH = 4, //对输入的frame新建一个引用,而非接管引用 //如果 frame 是引用计数的,那么对它创建一个新的引用;否则拷贝frame中的数据 AV_BUFFERSRC_FLAG_KEEP_REF = 8, }; ``` **向 buffer_src 添加一个Frame:** ``` /* 向 buffer_src 添加一个 frame。 默认情况下,如果 frame 是引用计数的,那么这个函数将会接管其引用并重新设置 frame。 但这个行为可以由 flags 来控制。如果 frame 不是引用计数的,那么拷贝该 frame。 如果函数返回一个 error,那么 frame 并未被使用。frame为NULL时,表示 EOF。 成功返回 >= 0,失败返回负的AVERROR。 */ int av_buffersrc_add_frame_flags(AVFilterContext *buffer_src, AVFrame *frame, int flags); ``` **添加一个frame到 src filter:** ``` /* 添加一个 frame 到 src filter。 这个函数等同于没有 AV_BUFFERSRC_FLAG_KEEP_REF 的 av_buffersrc_add_frame_flags() 函数。 */ int av_buffersrc_add_frame(AVFilterContext *ctx, AVFrame *frame); /* 添加一个 frame 到 src filter。 这个函数等同于设置了 AV_BUFFERSRC_FLAG_KEEP_REF 的av_buffersrc_add_frame_flags() 函数。 */ int av_buffersrc_write_frame(AVFilterContext *ctx, const AVFrame *frame); ``` **从sink获取已filtered处理的帧,并放到参数frame中:** ``` /* 从 sink 中获取已进行 filtered 处理的帧,并将其放到参数 frame 中。 参数ctx:指向 buffersink 或 abuffersink 类型的 filter context 参数frame:获取到的被处理后的frame,使用后必须使用av_frame_unref() / av_frame_free()释放掉它 成功返回非负数,失败返回负的错误值,如 EAGAIN(表示需要新的输入数据来产生filter后的数据), AVERROR_EOF(表示不会再有新的输入数据) */ int av_buffersink_get_frame_flags(AVFilterContext *ctx, AVFrame *frame, int flags); ``` ``` /* 同 av_buffersink_get_frame_flags ,不过不能指定 flag。 */ int av_buffersink_get_frame(AVFilterContext *ctx, AVFrame *frame) /* 和 av_buffersink_get_frame 相同,不过这个函数是针对音频的,而且可以指定读取的取样数。此时 ctx 只能指向 abuffersink 类型的 filter context。 */ int av_buffersink_get_samples(AVFilterContext *ctx, AVFrame *frame, int nb_samples); ``` ## 四、FFmpeg AVFilter 使用整体流程 下图就是FFmpeg AVFilter在使用过程中的流程图: ![img](https://img2020.cnblogs.com/blog/682616/202104/682616-20210415141146493-1433896880.jpg) 我们对上图先做下说明,理解下图中每个步骤的关系,然后,才从代码的角度来给出其使用的步骤。 1. 最顶端的AVFilterGraph,这个结构前面介绍过,主要管理加入的过滤器,其中加入的过滤器就是通过函数avfilter_graph_create_filter来创建并加入,这个函数返回是AVFilterContext(其封装了AVFilter的详细参数信息)。 2. buffer和buffersink这两个过滤器是FFMpeg为我们实现好的,buffer表示源,用来向后面的过滤器提供数据输入(其实就是原始的AVFrame);buffersink过滤器是最终输出的(经过过滤器链处理后的数据AVFrame),其它的诸如filter 1 等过滤器是由avfilter_graph_parse_ptr函数解析外部传入的过滤器描述字符串自动生成的,内部也是通过avfilter_graph_create_filter来创建过滤器的。 3. 上面的buffer、filter 1、filter 2、filter n、buffersink之间是通过avfilter_link函数来进行关联的(通过AVFilterLink结构),这样子过滤器和过滤器之间就通过AVFilterLink进行关联上了,前一个过滤器的输出就是下一个过滤器的输入,注意,除了源和接收过滤器之外,其它的过滤器至少有一个输入和输出,这很好理解,中间的过滤器处理完AVFrame后,得到新的处理后的AVFrame数据,然后把新的AVFrame数据作为下一个过滤器的输入。 4. 过滤器建立完成后,首先我们通过av_buffersrc_add_frame把最原始的AVFrame(没有经过任何过滤器处理的)加入到buffer过滤器的fifo队列。 5. 然后调用buffersink过滤器的av_buffersink_get_frame_flags来获取处理完后的数据帧(这个最终放入buffersink过滤器的AVFrame是通过之前创建的一系列过滤器处理后的数据)。 使用流程图就介绍到这里,下面结合上面的使用流程图详细说下FFMpeg中使用过滤器的步骤,这个过程我们分为三个部分:过滤器构建、数据加工、资源释放。 ### 1. 过滤器构建: 1)分配AVFilterGraph ``` AVFilterGraph* graph = avfilter_graph_alloc(); ``` 2)创建过滤器源 ``` char srcArgs[256] = {0}; AVFilterContext *srcFilterCtx; AVFilter* srcFilter = avfilter_get_by_name("buffer"); avfilter_graph_create_filter(&srcFilterCtx, srcFilter ,"out_buffer", srcArgs, NULL, graph); ``` 3)创建接收过滤器 ``` AVFilterContext *sinkFilterCtx; AVFilter* sinkFilter = avfilter_get_by_name("buffersink"); avfilter_graph_create_filter(&sinkFilterCtx, sinkFilter,"in_buffersink", NULL, NULL, graph); ``` 4)生成源和接收过滤器的输入输出 这里主要是把源和接收过滤器封装给AVFilterInOut结构,使用这个中间结构来把过滤器字符串解析并链接进graph,主要代码如下: ``` AVFilterInOut *inputs = avfilter_inout_alloc(); AVFilterInOut *outputs = avfilter_inout_alloc(); outputs->name = av_strdup("in"); outputs->filter_ctx = srcFilterCtx; outputs->pad_idx = 0; outputs->next = NULL; inputs->name = av_strdup("out"); inputs->filter_ctx = sinkFilterCtx; inputs->pad_idx = 0; inputs->next = NULL; ``` 这里源对应的AVFilterInOut的name最好定义为in,接收对应的name为out,因为FFMpeg源码里默认会通过这样个name来对默认的输出和输入进行查找。 5)通过解析过滤器字符串添加过滤器 ``` const *char filtergraph = "[in1]过滤器名称=参数1:参数2[out1]"; int ret = avfilter_graph_parse_ptr(graph, filtergraph, &inputs, &outputs, NULL); ``` 这里过滤器是以字符串形式描述的,其格式为:[in]过滤器名称=参数[out],过滤器之间用,或;分割,如果过滤器有多个参数,则参数之间用:分割,其中[in]和[out]分别为过滤器的输入和输出,可以有多个。 6)检查过滤器的完整性 ``` avfilter_graph_config(graph, NULL); ``` ### 2. 数据加工 1)向源过滤器加入AVFrame ``` AVFrame* frame; // 这是解码后获取的数据帧 int ret = av_buffersrc_add_frame(srcFilterCtx, frame); ``` 2)从buffersink接收处理后的AVFrame ``` int ret = av_buffersink_get_frame_flags(sinkFilterCtx, frame, 0); ``` 现在我们就可以使用处理后的AVFrame,比如显示或播放出来。 ### 3.资源释放 使用结束后,调用avfilter_graph_free(&graph);释放掉AVFilterGraph类型的graph。 ================================================ FILE: FFmpeg 结构体学习(一): AVFormatContext 分析.md ================================================ 在 FFmpeg 学习(六):FFmpeg 核心模块 libavformat 与 libavcodec 分析 中,我们分析了FFmpeg中最重要的两个模块以及重要的结构体之间的关系。 后面的文章,我们先不去继续了解其他模块,先针对在之前的学习中接触到的结构体进行分析,然后在根据功能源码,继续了解FFmpeg。 **AVFormatContext是包含码流参数较多的结构体。本文将会详细分析一下该结构体里每个变量的含义和作用。** # 一、源码整理 首先我们先看一下结构体AVFormatContext的定义的结构体源码(位于libavformat/avformat.h,本人已经将相关注释翻译成中文,方便大家理解): View Code # 二、AVForamtContext 重点字段 在使用FFMPEG进行开发的时候,AVFormatContext是一个贯穿始终的数据结构,很多函数都要用到它作为参数。它是FFMPEG解封装(flv,mp4,rmvb,avi)功能的结构体。下面看几个主要变量的作用(在这里考虑解码的情况): ``` struct AVInputFormat *iformat:输入数据的封装格式 AVIOContext *pb:输入数据的缓存 unsigned int nb_streams:视音频流的个数 AVStream **streams:视音频流 char filename[1024]:文件名 int64_t duration:时长(单位:微秒us,转换为秒需要除以1000000) int bit_rate:比特率(单位bps,转换为kbps需要除以1000) AVDictionary *metadata:元数据 ``` 视频的时长可以转换成HH:MM:SS的形式,示例代码如下: ``` AVFormatContext *pFormatCtx; CString timelong; ... //duration是以微秒为单位 //转换成hh:mm:ss形式 int tns, thh, tmm, tss; tns = (pFormatCtx->duration)/1000000; thh = tns / 3600; tmm = (tns % 3600) / 60; tss = (tns % 60); timelong.Format("%02d:%02d:%02d",thh,tmm,tss); ``` 视频的原数据(metadata)信息可以通过AVDictionary获取。元数据存储在AVDictionaryEntry结构体中,如下所示: ``` typedef struct AVDictionaryEntry { char *key; char *value; } AVDictionaryEntry; ``` 每一条元数据分为key和value两个属性。 在ffmpeg中通过av_dict_get()函数获得视频的原数据。 下列代码显示了获取元数据并存入meta字符串变量的过程,注意每一条key和value之间有一个"\t:",value之后有一个"\r\n" ``` //MetaData------------------------------------------------------------ //从AVDictionary获得 //需要用到AVDictionaryEntry对象 //CString author,copyright,description; CString meta=NULL,key,value; AVDictionaryEntry *m = NULL; //不用一个一个找出来 /* m=av_dict_get(pFormatCtx->metadata,"author",m,0); author.Format("作者:%s",m->value); m=av_dict_get(pFormatCtx->metadata,"copyright",m,0); copyright.Format("版权:%s",m->value); m=av_dict_get(pFormatCtx->metadata,"description",m,0); description.Format("描述:%s",m->value); */ //使用循环读出 //(需要读取的数据,字段名称,前一条字段(循环时使用),参数) while(m=av_dict_get(pFormatCtx->metadata,"",m,AV_DICT_IGNORE_SUFFIX)){ key.Format(m->key); value.Format(m->value); meta+=key+"\t:"+value+"\r\n" ; } ``` ================================================ FILE: FFmpeg 结构体学习(七): AVIOContext 分析.md ================================================ AVIOContext是FFMPEG管理输入输出数据的结构体。下面我们来分析一下该结构体里重要变量的含义和作用。 # 一、源码整理 首先我们先看一下结构体AVIOContext的定义的结构体源码(位于libavformat/avio.h): ``` /** * Bytestream IO Context. * New fields can be added to the end with minor version bumps. * Removal, reordering and changes to existing fields require a major * version bump. * sizeof(AVIOContext) must not be used outside libav*. * * @note None of the function pointers in AVIOContext should be called * directly, they should only be set by the client application * when implementing custom I/O. Normally these are set to the * function pointers specified in avio_alloc_context() */ typedef struct { /** * A class for private options. * * If this AVIOContext is created by avio_open2(), av_class is set and * passes the options down to protocols. * * If this AVIOContext is manually allocated, then av_class may be set by * the caller. * * warning -- this field can be NULL, be sure to not pass this AVIOContext * to any av_opt_* functions in that case. */ AVClass *av_class; unsigned char *buffer; /**< Start of the buffer. */ int buffer_size; /**< Maximum buffer size */ unsigned char *buf_ptr; /**< Current position in the buffer */ unsigned char *buf_end; /**< End of the data, may be less than buffer+buffer_size if the read function returned less data than requested, e.g. for streams where no more data has been received yet. */ void *opaque; /**< A private pointer, passed to the read/write/seek/... functions. */ int (*read_packet)(void *opaque, uint8_t *buf, int buf_size); int (*write_packet)(void *opaque, uint8_t *buf, int buf_size); int64_t (*seek)(void *opaque, int64_t offset, int whence); int64_t pos; /**< position in the file of the current buffer */ int must_flush; /**< true if the next seek should flush */ int eof_reached; /**< true if eof reached */ int write_flag; /**< true if open for writing */ int max_packet_size; unsigned long checksum; unsigned char *checksum_ptr; unsigned long (*update_checksum)(unsigned long checksum, const uint8_t *buf, unsigned int size); int error; /**< contains the error code or 0 if no error happened */ /** * Pause or resume playback for network streaming protocols - e.g. MMS. */ int (*read_pause)(void *opaque, int pause); /** * Seek to a given timestamp in stream with the specified stream_index. * Needed for some network streaming protocols which don't support seeking * to byte position. */ int64_t (*read_seek)(void *opaque, int stream_index, int64_t timestamp, int flags); /** * A combination of AVIO_SEEKABLE_ flags or 0 when the stream is not seekable. */ int seekable; /** * max filesize, used to limit allocations * This field is internal to libavformat and access from outside is not allowed. */ int64_t maxsize; } AVIOContext; ``` # 二、AVIOContext 重点字段 AVIOContext中有以下几个变量比较重要: ``` unsigned char *buffer:缓存开始位置 int buffer_size:缓存大小(默认32768) unsigned char *buf_ptr:当前指针读取到的位置 unsigned char *buf_end:缓存结束的位置 void *opaque:URLContext结构体 ``` 在解码的情况下,buffer用于存储ffmpeg读入的数据。例如打开一个视频文件的时候,先把数据从硬盘读入buffer,然后在送给解码器用于解码。 其中opaque指向了URLContext。注意,这个结构体并不在FFMPEG提供的头文件中,而是在FFMPEG的源代码中。从FFMPEG源代码中翻出的定义如下所示: ``` typedef struct URLContext { const AVClass *av_class; ///< information for av_log(). Set by url_open(). struct URLProtocol *prot; int flags; int is_streamed; /**< true if streamed (no seek possible), default = false */ int max_packet_size; /**< if non zero, the stream is packetized with this max packet size */ void *priv_data; char *filename; /**< specified URL */ int is_connected; AVIOInterruptCB interrupt_callback; } URLContext; ``` URLContext结构体中还有一个结构体URLProtocol。注:每种协议(rtp,rtmp,file等)对应一个URLProtocol。这个结构体也不在FFMPEG提供的头文件中。从FFMPEG源代码中翻出其的定义: ``` typedef struct URLProtocol { const char *name; int (*url_open)(URLContext *h, const char *url, int flags); int (*url_read)(URLContext *h, unsigned char *buf, int size); int (*url_write)(URLContext *h, const unsigned char *buf, int size); int64_t (*url_seek)(URLContext *h, int64_t pos, int whence); int (*url_close)(URLContext *h); struct URLProtocol *next; int (*url_read_pause)(URLContext *h, int pause); int64_t (*url_read_seek)(URLContext *h, int stream_index, int64_t timestamp, int flags); int (*url_get_file_handle)(URLContext *h); int priv_data_size; const AVClass *priv_data_class; int flags; int (*url_check)(URLContext *h, int mask); } URLProtocol; ``` 在这个结构体中,除了一些回调函数接口之外,有一个变量const char *name,该变量存储了协议的名称。每一种输入协议都对应这样一个结构体。 比如说,文件协议中代码如下(file.c): ``` URLProtocol ff_file_protocol = { .name = "file", .url_open = file_open, .url_read = file_read, .url_write = file_write, .url_seek = file_seek, .url_close = file_close, .url_get_file_handle = file_get_handle, .url_check = file_check, }; ``` libRTMP中代码如下(libRTMP.c): ``` URLProtocol ff_rtmp_protocol = { .name = "rtmp", .url_open = rtmp_open, .url_read = rtmp_read, .url_write = rtmp_write, .url_close = rtmp_close, .url_read_pause = rtmp_read_pause, .url_read_seek = rtmp_read_seek, .url_get_file_handle = rtmp_get_file_handle, .priv_data_size = sizeof(RTMP), .flags = URL_PROTOCOL_FLAG_NETWORK, }; ``` udp协议代码如下(udp.c): ``` URLProtocol ff_udp_protocol = { .name = "udp", .url_open = udp_open, .url_read = udp_read, .url_write = udp_write, .url_close = udp_close, .url_get_file_handle = udp_get_file_handle, .priv_data_size = sizeof(UDPContext), .flags = URL_PROTOCOL_FLAG_NETWORK, }; ``` 等号右边的函数是完成具体读写功能的函数。可以看一下file协议的几个函数(其实就是读文件,写文件这样的操作)(file.c): ``` /* standard file protocol */ static int file_read(URLContext *h, unsigned char *buf, int size) { int fd = (intptr_t) h->priv_data; int r = read(fd, buf, size); return (-1 == r)?AVERROR(errno):r; } static int file_write(URLContext *h, const unsigned char *buf, int size) { int fd = (intptr_t) h->priv_data; int r = write(fd, buf, size); return (-1 == r)?AVERROR(errno):r; } static int file_get_handle(URLContext *h) { return (intptr_t) h->priv_data; } static int file_check(URLContext *h, int mask) { struct stat st; int ret = stat(h->filename, &st); if (ret < 0) return AVERROR(errno); ret |= st.st_mode&S_IRUSR ? mask&AVIO_FLAG_READ : 0; ret |= st.st_mode&S_IWUSR ? mask&AVIO_FLAG_WRITE : 0; return ret; } #if CONFIG_FILE_PROTOCOL static int file_open(URLContext *h, const char *filename, int flags) { int access; int fd; av_strstart(filename, "file:", &filename); if (flags & AVIO_FLAG_WRITE && flags & AVIO_FLAG_READ) { access = O_CREAT | O_TRUNC | O_RDWR; } else if (flags & AVIO_FLAG_WRITE) { access = O_CREAT | O_TRUNC | O_WRONLY; } else { access = O_RDONLY; } #ifdef O_BINARY access |= O_BINARY; #endif fd = open(filename, access, 0666); if (fd == -1) return AVERROR(errno); h->priv_data = (void *) (intptr_t) fd; return 0; } /* XXX: use llseek */ static int64_t file_seek(URLContext *h, int64_t pos, int whence) { int fd = (intptr_t) h->priv_data; if (whence == AVSEEK_SIZE) { struct stat st; int ret = fstat(fd, &st); return ret < 0 ? AVERROR(errno) : st.st_size; } return lseek(fd, pos, whence); } static int file_close(URLContext *h) { int fd = (intptr_t) h->priv_data; return close(fd); } ``` ================================================ FILE: FFmpeg 结构体学习(三): AVPacket 分析.md ================================================ AVPacket是存储压缩编码数据相关信息的结构体。下面我们来分析一下该结构体里重要变量的含义和作用。 # 一、源码整理 首先我们先看一下结构体AVPacket的定义的结构体源码(位于libavcodec/avcodec.h): ``` typedef struct AVPacket { /** * Presentation timestamp in AVStream->time_base units; the time at which * the decompressed packet will be presented to the user. * Can be AV_NOPTS_VALUE if it is not stored in the file. * pts MUST be larger or equal to dts as presentation cannot happen before * decompression, unless one wants to view hex dumps. Some formats misuse * the terms dts and pts/cts to mean something different. Such timestamps * must be converted to true pts/dts before they are stored in AVPacket. */ int64_t pts; /** * Decompression timestamp in AVStream->time_base units; the time at which * the packet is decompressed. * Can be AV_NOPTS_VALUE if it is not stored in the file. */ int64_t dts; uint8_t *data; int size; int stream_index; /** * A combination of AV_PKT_FLAG values */ int flags; /** * Additional packet data that can be provided by the container. * Packet can contain several types of side information. */ struct { uint8_t *data; int size; enum AVPacketSideDataType type; } *side_data; int side_data_elems; /** * Duration of this packet in AVStream->time_base units, 0 if unknown. * Equals next_pts - this_pts in presentation order. */ int duration; void (*destruct)(struct AVPacket *); void *priv; int64_t pos; ///< byte position in stream, -1 if unknown /** * Time difference in AVStream->time_base units from the pts of this * packet to the point at which the output from the decoder has converged * independent from the availability of previous frames. That is, the * frames are virtually identical no matter if decoding started from * the very first frame or from this keyframe. * Is AV_NOPTS_VALUE if unknown. * This field is not the display duration of the current packet. * This field has no meaning if the packet does not have AV_PKT_FLAG_KEY * set. * * The purpose of this field is to allow seeking in streams that have no * keyframes in the conventional sense. It corresponds to the * recovery point SEI in H.264 and match_time_delta in NUT. It is also * essential for some types of subtitle streams to ensure that all * subtitles are correctly displayed after seeking. */ int64_t convergence_duration; } AVPacket; ``` # 二、AVPacket 重点字段 ``` uint8_t *data:压缩编码的数据。 int size:data的大小 int64_t pts:显示时间戳 int64_t dts:解码时间戳 int stream_index:标识该AVPacket所属的视频/音频流。 ``` 针对data做一下说明:对于H.264格式来说,在使用FFMPEG进行视音频处理的时候,我们常常可以将得到的AVPacket的data数据直接写成文件,从而得到视音频的码流文件。 ================================================ FILE: FFmpeg 结构体学习(二): AVStream 分析.md ================================================ AVStream是存储每一个视频/音频流信息的结构体。下面我们来分析一下该结构体里重要变量的含义和作用。 # 一、源码整理 首先我们先看一下结构体AVStream的定义的结构体源码(位于libavformat/avformat.h): ``` /** * Stream structure. * New fields can be added to the end with minor version bumps. * Removal, reordering and changes to existing fields require a major * version bump. * sizeof(AVStream) must not be used outside libav*. */ typedef struct AVStream { int index; /**< stream index in AVFormatContext */ /** * Format-specific stream ID. * decoding: set by libavformat * encoding: set by the user */ int id; AVCodecContext *codec; /**< codec context */ /** * Real base framerate of the stream. * This is the lowest framerate with which all timestamps can be * represented accurately (it is the least common multiple of all * framerates in the stream). Note, this value is just a guess! * For example, if the time base is 1/90000 and all frames have either * approximately 3600 or 1800 timer ticks, then r_frame_rate will be 50/1. */ AVRational r_frame_rate; void *priv_data; /** * encoding: pts generation when outputting stream */ struct AVFrac pts; /** * This is the fundamental unit of time (in seconds) in terms * of which frame timestamps are represented. For fixed-fps content, * time base should be 1/framerate and timestamp increments should be 1. * decoding: set by libavformat * encoding: set by libavformat in av_write_header */ AVRational time_base; /** * Decoding: pts of the first frame of the stream in presentation order, in stream time base. * Only set this if you are absolutely 100% sure that the value you set * it to really is the pts of the first frame. * This may be undefined (AV_NOPTS_VALUE). * @note The ASF header does NOT contain a correct start_time the ASF * demuxer must NOT set this. */ int64_t start_time; /** * Decoding: duration of the stream, in stream time base. * If a source file does not specify a duration, but does specify * a bitrate, this value will be estimated from bitrate and file size. */ int64_t duration; int64_t nb_frames; ///< number of frames in this stream if known or 0 int disposition; /**< AV_DISPOSITION_* bit field */ enum AVDiscard discard; ///< Selects which packets can be discarded at will and do not need to be demuxed. /** * sample aspect ratio (0 if unknown) * - encoding: Set by user. * - decoding: Set by libavformat. */ AVRational sample_aspect_ratio; AVDictionary *metadata; /** * Average framerate */ AVRational avg_frame_rate; /** * For streams with AV_DISPOSITION_ATTACHED_PIC disposition, this packet * will contain the attached picture. * * decoding: set by libavformat, must not be modified by the caller. * encoding: unused */ AVPacket attached_pic; /***************************************************************** * All fields below this line are not part of the public API. They * may not be used outside of libavformat and can be changed and * removed at will. * New public fields should be added right above. ***************************************************************** */ /** * Stream information used internally by av_find_stream_info() */ #define MAX_STD_TIMEBASES (60*12+5) struct { int64_t last_dts; int64_t duration_gcd; int duration_count; double duration_error[2][2][MAX_STD_TIMEBASES]; int64_t codec_info_duration; int nb_decoded_frames; int found_decoder; } *info; int pts_wrap_bits; /**< number of bits in pts (used for wrapping control) */ // Timestamp generation support: /** * Timestamp corresponding to the last dts sync point. * * Initialized when AVCodecParserContext.dts_sync_point >= 0 and * a DTS is received from the underlying container. Otherwise set to * AV_NOPTS_VALUE by default. */ int64_t reference_dts; int64_t first_dts; int64_t cur_dts; int64_t last_IP_pts; int last_IP_duration; /** * Number of packets to buffer for codec probing */ #define MAX_PROBE_PACKETS 2500 int probe_packets; /** * Number of frames that have been demuxed during av_find_stream_info() */ int codec_info_nb_frames; /** * Stream Identifier * This is the MPEG-TS stream identifier +1 * 0 means unknown */ int stream_identifier; int64_t interleaver_chunk_size; int64_t interleaver_chunk_duration; /* av_read_frame() support */ enum AVStreamParseType need_parsing; struct AVCodecParserContext *parser; /** * last packet in packet_buffer for this stream when muxing. */ struct AVPacketList *last_in_packet_buffer; AVProbeData probe_data; #define MAX_REORDER_DELAY 16 int64_t pts_buffer[MAX_REORDER_DELAY+1]; AVIndexEntry *index_entries; /**< Only used if the format does not support seeking natively. */ int nb_index_entries; unsigned int index_entries_allocated_size; /** * flag to indicate that probing is requested * NOT PART OF PUBLIC API */ int request_probe; } AVStream; ``` # 二、AVStream 重点字段 ``` int index:标识该视频/音频流 AVCodecContext *codec:指向该视频/音频流的AVCodecContext(它们是一一对应的关系) AVRational time_base:时基。通过该值可以把PTS,DTS转化为真正的时间。FFMPEG其他结构体中也有这个字段,但是根据我的经验,只有AVStream中的time_base是可用的。PTS*time_base=真正的时间 int64_t duration:该视频/音频流长度 AVDictionary *metadata:元数据信息 AVRational avg_frame_rate:帧率(注:对视频来说,这个挺重要的) AVPacket attached_pic:附带的图片。比如说一些MP3,AAC音频文件附带的专辑封面。 ``` ================================================ FILE: FFmpeg 结构体学习(五): AVCodec 分析.md ================================================ AVCodec是存储编解码器信息的结构体。下面我们来分析一下该结构体里重要变量的含义和作用。 # 一、源码整理 首先我们先看一下结构体AVCodec的定义的结构体源码(位于libavcodec/avcodec.h): ``` /* 雷霄骅 * 中国传媒大学/数字电视技术 * leixiaohua1020@126.com * */ /** * AVCodec. */ typedef struct AVCodec { /** * Name of the codec implementation. * The name is globally unique among encoders and among decoders (but an * encoder and a decoder can share the same name). * This is the primary way to find a codec from the user perspective. */ const char *name; /** * Descriptive name for the codec, meant to be more human readable than name. * You should use the NULL_IF_CONFIG_SMALL() macro to define it. */ const char *long_name; enum AVMediaType type; enum CodecID id; /** * Codec capabilities. * see CODEC_CAP_* */ int capabilities; const AVRational *supported_framerates; ///< array of supported framerates, or NULL if any, array is terminated by {0,0} const enum PixelFormat *pix_fmts; ///< array of supported pixel formats, or NULL if unknown, array is terminated by -1 const int *supported_samplerates; ///< array of supported audio samplerates, or NULL if unknown, array is terminated by 0 const enum AVSampleFormat *sample_fmts; ///< array of supported sample formats, or NULL if unknown, array is terminated by -1 const uint64_t *channel_layouts; ///< array of support channel layouts, or NULL if unknown. array is terminated by 0 uint8_t max_lowres; ///< maximum value for lowres supported by the decoder const AVClass *priv_class; ///< AVClass for the private context const AVProfile *profiles; ///< array of recognized profiles, or NULL if unknown, array is terminated by {FF_PROFILE_UNKNOWN} /***************************************************************** * No fields below this line are part of the public API. They * may not be used outside of libavcodec and can be changed and * removed at will. * New public fields should be added right above. ***************************************************************** */ int priv_data_size; struct AVCodec *next; /** * @name Frame-level threading support functions * @{ */ /** * If defined, called on thread contexts when they are created. * If the codec allocates writable tables in init(), re-allocate them here. * priv_data will be set to a copy of the original. */ int (*init_thread_copy)(AVCodecContext *); /** * Copy necessary context variables from a previous thread context to the current one. * If not defined, the next thread will start automatically; otherwise, the codec * must call ff_thread_finish_setup(). * * dst and src will (rarely) point to the same context, in which case memcpy should be skipped. */ int (*update_thread_context)(AVCodecContext *dst, const AVCodecContext *src); /** @} */ /** * Private codec-specific defaults. */ const AVCodecDefault *defaults; /** * Initialize codec static data, called from avcodec_register(). */ void (*init_static_data)(struct AVCodec *codec); int (*init)(AVCodecContext *); int (*encode)(AVCodecContext *, uint8_t *buf, int buf_size, void *data); /** * Encode data to an AVPacket. * * @param avctx codec context * @param avpkt output AVPacket (may contain a user-provided buffer) * @param[in] frame AVFrame containing the raw data to be encoded * @param[out] got_packet_ptr encoder sets to 0 or 1 to indicate that a * non-empty packet was returned in avpkt. * @return 0 on success, negative error code on failure */ int (*encode2)(AVCodecContext *avctx, AVPacket *avpkt, const AVFrame *frame, int *got_packet_ptr); int (*decode)(AVCodecContext *, void *outdata, int *outdata_size, AVPacket *avpkt); int (*close)(AVCodecContext *); /** * Flush buffers. * Will be called when seeking */ void (*flush)(AVCodecContext *); } AVCodec; ``` # 二、AVCodec 重点字段 下面说一下最主要的几个变量: ``` const char *name:编解码器的名字,比较短 const char *long_name:编解码器的名字,全称,比较长 enum AVMediaType type:指明了类型,是视频,音频,还是字幕 enum AVCodecID id:ID,不重复 const AVRational *supported_framerates:支持的帧率(仅视频) const enum AVPixelFormat *pix_fmts:支持的像素格式(仅视频) const int *supported_samplerates:支持的采样率(仅音频) const enum AVSampleFormat *sample_fmts:支持的采样格式(仅音频) const uint64_t *channel_layouts:支持的声道数(仅音频) int priv_data_size:私有数据的大小 ``` 详细介绍几个变量: ### 1.enum AVMediaType type AVMediaType定义如下: ``` enum AVMediaType { AVMEDIA_TYPE_UNKNOWN = -1, ///< Usually treated as AVMEDIA_TYPE_DATA AVMEDIA_TYPE_VIDEO, AVMEDIA_TYPE_AUDIO, AVMEDIA_TYPE_DATA, ///< Opaque data information usually continuous AVMEDIA_TYPE_SUBTITLE, AVMEDIA_TYPE_ATTACHMENT, ///< Opaque data information usually sparse AVMEDIA_TYPE_NB }; ``` ### 2.enum AVCodecID id AVCodecID定义如下: ``` enum AVCodecID { AV_CODEC_ID_NONE, /* video codecs */ AV_CODEC_ID_MPEG1VIDEO, AV_CODEC_ID_MPEG2VIDEO, ///< preferred ID for MPEG-1/2 video decoding AV_CODEC_ID_MPEG2VIDEO_XVMC, AV_CODEC_ID_H261, AV_CODEC_ID_H263, AV_CODEC_ID_RV10, AV_CODEC_ID_RV20, AV_CODEC_ID_MJPEG, AV_CODEC_ID_MJPEGB, AV_CODEC_ID_LJPEG, AV_CODEC_ID_SP5X, AV_CODEC_ID_JPEGLS, AV_CODEC_ID_MPEG4, AV_CODEC_ID_RAWVIDEO, AV_CODEC_ID_MSMPEG4V1, AV_CODEC_ID_MSMPEG4V2, AV_CODEC_ID_MSMPEG4V3, AV_CODEC_ID_WMV1, AV_CODEC_ID_WMV2, AV_CODEC_ID_H263P, AV_CODEC_ID_H263I, AV_CODEC_ID_FLV1, AV_CODEC_ID_SVQ1, AV_CODEC_ID_SVQ3, AV_CODEC_ID_DVVIDEO, AV_CODEC_ID_HUFFYUV, AV_CODEC_ID_CYUV, AV_CODEC_ID_H264, ... } ``` ### 3.const enum AVPixelFormat *pix_fmts AVPixelFormat定义如下: ``` enum AVPixelFormat { AV_PIX_FMT_NONE = -1, AV_PIX_FMT_YUV420P, ///< planar YUV 4:2:0, 12bpp, (1 Cr & Cb sample per 2x2 Y samples) AV_PIX_FMT_YUYV422, ///< packed YUV 4:2:2, 16bpp, Y0 Cb Y1 Cr AV_PIX_FMT_RGB24, ///< packed RGB 8:8:8, 24bpp, RGBRGB... AV_PIX_FMT_BGR24, ///< packed RGB 8:8:8, 24bpp, BGRBGR... AV_PIX_FMT_YUV422P, ///< planar YUV 4:2:2, 16bpp, (1 Cr & Cb sample per 2x1 Y samples) AV_PIX_FMT_YUV444P, ///< planar YUV 4:4:4, 24bpp, (1 Cr & Cb sample per 1x1 Y samples) AV_PIX_FMT_YUV410P, ///< planar YUV 4:1:0, 9bpp, (1 Cr & Cb sample per 4x4 Y samples) AV_PIX_FMT_YUV411P, ///< planar YUV 4:1:1, 12bpp, (1 Cr & Cb sample per 4x1 Y samples) AV_PIX_FMT_GRAY8, ///< Y , 8bpp AV_PIX_FMT_MONOWHITE, ///< Y , 1bpp, 0 is white, 1 is black, in each byte pixels are ordered from the msb to the lsb AV_PIX_FMT_MONOBLACK, ///< Y , 1bpp, 0 is black, 1 is white, in each byte pixels are ordered from the msb to the lsb AV_PIX_FMT_PAL8, ///< 8 bit with PIX_FMT_RGB32 palette AV_PIX_FMT_YUVJ420P, ///< planar YUV 4:2:0, 12bpp, full scale (JPEG), deprecated in favor of PIX_FMT_YUV420P and setting color_range AV_PIX_FMT_YUVJ422P, ///< planar YUV 4:2:2, 16bpp, full scale (JPEG), deprecated in favor of PIX_FMT_YUV422P and setting color_range AV_PIX_FMT_YUVJ444P, ///< planar YUV 4:4:4, 24bpp, full scale (JPEG), deprecated in favor of PIX_FMT_YUV444P and setting color_range AV_PIX_FMT_XVMC_MPEG2_MC,///< XVideo Motion Acceleration via common packet passing AV_PIX_FMT_XVMC_MPEG2_IDCT, ...(代码太长,略) } ``` ### 4.const enum AVSampleFormat *sample_fmts ``` enum AVSampleFormat { AV_SAMPLE_FMT_NONE = -1, AV_SAMPLE_FMT_U8, ///< unsigned 8 bits AV_SAMPLE_FMT_S16, ///< signed 16 bits AV_SAMPLE_FMT_S32, ///< signed 32 bits AV_SAMPLE_FMT_FLT, ///< float AV_SAMPLE_FMT_DBL, ///< double AV_SAMPLE_FMT_U8P, ///< unsigned 8 bits, planar AV_SAMPLE_FMT_S16P, ///< signed 16 bits, planar AV_SAMPLE_FMT_S32P, ///< signed 32 bits, planar AV_SAMPLE_FMT_FLTP, ///< float, planar AV_SAMPLE_FMT_DBLP, ///< double, planar AV_SAMPLE_FMT_NB ///< Number of sample formats. DO NOT USE if linking dynamically }; ``` 每一个编解码器对应一个该结构体,查看一下ffmpeg的源代码,我们可以看一下H.264解码器的结构体如下所示(h264.c): ``` AVCodec ff_h264_decoder = { .name = "h264", .type = AVMEDIA_TYPE_VIDEO, .id = CODEC_ID_H264, .priv_data_size = sizeof(H264Context), .init = ff_h264_decode_init, .close = ff_h264_decode_end, .decode = decode_frame, .capabilities = /*CODEC_CAP_DRAW_HORIZ_BAND |*/ CODEC_CAP_DR1 | CODEC_CAP_DELAY | CODEC_CAP_SLICE_THREADS | CODEC_CAP_FRAME_THREADS, .flush= flush_dpb, .long_name = NULL_IF_CONFIG_SMALL("H.264 / AVC / MPEG-4 AVC / MPEG-4 part 10"), .init_thread_copy = ONLY_IF_THREADS_ENABLED(decode_init_thread_copy), .update_thread_context = ONLY_IF_THREADS_ENABLED(decode_update_thread_context), .profiles = NULL_IF_CONFIG_SMALL(profiles), .priv_class = &h264_class, }; ``` JPEG2000解码器结构体(j2kdec.c): ``` AVCodec ff_jpeg2000_decoder = { .name = "j2k", .type = AVMEDIA_TYPE_VIDEO, .id = CODEC_ID_JPEG2000, .priv_data_size = sizeof(J2kDecoderContext), .init = j2kdec_init, .close = decode_end, .decode = decode_frame, .capabilities = CODEC_CAP_EXPERIMENTAL, .long_name = NULL_IF_CONFIG_SMALL("JPEG 2000"), .pix_fmts = (const enum PixelFormat[]) {PIX_FMT_GRAY8, PIX_FMT_RGB24, PIX_FMT_NONE} }; ``` 下面简单介绍一下遍历ffmpeg中的解码器信息的方法(这些解码器以一个链表的形式存储): 1.注册所有编解码器:av_register_all(); 2.声明一个AVCodec类型的指针,比如说AVCodec* first_c; 3.调用av_codec_next()函数,即可获得指向链表下一个解码器的指针,循环往复可以获得所有解码器的信息。注意,如果想要获得指向第一个解码器的指针,则需要将该函数的参数设置为NULL。 ================================================ FILE: FFmpeg 结构体学习(八):FFMPEG中重要结构体之间的关系.md ================================================ FFMPEG中结构体很多。最关键的结构体可以分成以下几类: ### 解协议(http,rtsp,rtmp,mms) AVIOContext,URLProtocol,URLContext主要存储视音频使用的协议的类型以及状态。URLProtocol存储输入视音频使用的封装格式。每种协议都对应一个URLProtocol结构。 ### 解封装(flv,avi,rmvb,mp4) AVFormatContext主要存储视音频封装格式中包含的信息;AVInputFormat存储输入视音频使用的封装格式。每种视音频封装格式都对应一个AVInputFormat 结构。 ### 解码(h264,mpeg2,aac,mp3) 每个AVStream存储一个视频/音频流的相关数据;每个AVStream对应一个AVCodecContext,存储该视频/音频流使用解码方式的相关数据;每个AVCodecContext中对应一个AVCodec,包含该视频/音频对应的解码器。每种解码器都对应一个AVCodec结构。 ### 存数据 视频的话,每个结构一般是存一帧;音频可能有好几帧 解码前数据:AVPacket 解码后数据:AVFrame 他们之间的对应关系如下所示: ![img](https://images2018.cnblogs.com/blog/682616/201808/682616-20180818164850060-1975380874.png) ================================================ FILE: FFmpeg 结构体学习(六): AVCodecContext 分析.md ================================================ AVCodecContext是包含变量较多的结构体(感觉差不多是变量最多的结构体)。下面我们来分析一下该结构体里重要变量的含义和作用。 # 一、源码整理 首先我们先看一下结构体AVCodecContext的定义的结构体源码(位于libavcodec/avcodec.h): ``` /** * main external API structure. * New fields can be added to the end with minor version bumps. * Removal, reordering and changes to existing fields require a major * version bump. * Please use AVOptions (av_opt* / av_set/get*()) to access these fields from user * applications. * sizeof(AVCodecContext) must not be used outside libav*. */ typedef struct AVCodecContext { /** * information on struct for av_log * - set by avcodec_alloc_context3 */ const AVClass *av_class; int log_level_offset; enum AVMediaType codec_type; /* see AVMEDIA_TYPE_xxx */ const struct AVCodec *codec; char codec_name[32]; enum AVCodecID codec_id; /* see AV_CODEC_ID_xxx */ /** * fourcc (LSB first, so "ABCD" -> ('D'<<24) + ('C'<<16) + ('B'<<8) + 'A'). * This is used to work around some encoder bugs. * A demuxer should set this to what is stored in the field used to identify the codec. * If there are multiple such fields in a container then the demuxer should choose the one * which maximizes the information about the used codec. * If the codec tag field in a container is larger than 32 bits then the demuxer should * remap the longer ID to 32 bits with a table or other structure. Alternatively a new * extra_codec_tag + size could be added but for this a clear advantage must be demonstrated * first. * - encoding: Set by user, if not then the default based on codec_id will be used. * - decoding: Set by user, will be converted to uppercase by libavcodec during init. */ unsigned int codec_tag; /** * fourcc from the AVI stream header (LSB first, so "ABCD" -> ('D'<<24) + ('C'<<16) + ('B'<<8) + 'A'). * This is used to work around some encoder bugs. * - encoding: unused * - decoding: Set by user, will be converted to uppercase by libavcodec during init. */ unsigned int stream_codec_tag; #if FF_API_SUB_ID /** * @deprecated this field is unused */ attribute_deprecated int sub_id; #endif void *priv_data; /** * Private context used for internal data. * * Unlike priv_data, this is not codec-specific. It is used in general * libavcodec functions. */ struct AVCodecInternal *internal; /** * Private data of the user, can be used to carry app specific stuff. * - encoding: Set by user. * - decoding: Set by user. */ void *opaque; /** * the average bitrate * - encoding: Set by user; unused for constant quantizer encoding. * - decoding: Set by libavcodec. 0 or some bitrate if this info is available in the stream. */ int bit_rate; /** * number of bits the bitstream is allowed to diverge from the reference. * the reference can be CBR (for CBR pass1) or VBR (for pass2) * - encoding: Set by user; unused for constant quantizer encoding. * - decoding: unused */ int bit_rate_tolerance; /** * Global quality for codecs which cannot change it per frame. * This should be proportional to MPEG-1/2/4 qscale. * - encoding: Set by user. * - decoding: unused */ int global_quality; /** * - encoding: Set by user. * - decoding: unused */ int compression_level; #define FF_COMPRESSION_DEFAULT -1 /** * CODEC_FLAG_*. * - encoding: Set by user. * - decoding: Set by user. */ int flags; /** * CODEC_FLAG2_* * - encoding: Set by user. * - decoding: Set by user. */ int flags2; /** * some codecs need / can use extradata like Huffman tables. * mjpeg: Huffman tables * rv10: additional flags * mpeg4: global headers (they can be in the bitstream or here) * The allocated memory should be FF_INPUT_BUFFER_PADDING_SIZE bytes larger * than extradata_size to avoid prolems if it is read with the bitstream reader. * The bytewise contents of extradata must not depend on the architecture or CPU endianness. * - encoding: Set/allocated/freed by libavcodec. * - decoding: Set/allocated/freed by user. */ uint8_t *extradata; int extradata_size; /** * This is the fundamental unit of time (in seconds) in terms * of which frame timestamps are represented. For fixed-fps content, * timebase should be 1/framerate and timestamp increments should be * identically 1. * - encoding: MUST be set by user. * - decoding: Set by libavcodec. */ AVRational time_base; /** * For some codecs, the time base is closer to the field rate than the frame rate. * Most notably, H.264 and MPEG-2 specify time_base as half of frame duration * if no telecine is used ... * * Set to time_base ticks per frame. Default 1, e.g., H.264/MPEG-2 set it to 2. */ int ticks_per_frame; /** * Encoding: Number of frames delay there will be from the encoder input to * the decoder output. (we assume the decoder matches the spec) * Decoding: Number of frames delay in addition to what a standard decoder * as specified in the spec would produce. * * Video: * Number of frames the decoded output will be delayed relative to the * encoded input. * * Audio: * For encoding, this is the number of "priming" samples added to the * beginning of the stream. The decoded output will be delayed by this * many samples relative to the input to the encoder. Note that this * field is purely informational and does not directly affect the pts * output by the encoder, which should always be based on the actual * presentation time, including any delay. * For decoding, this is the number of samples the decoder needs to * output before the decoder's output is valid. When seeking, you should * start decoding this many samples prior to your desired seek point. * * - encoding: Set by libavcodec. * - decoding: Set by libavcodec. */ int delay; /* video only */ /** * picture width / height. * - encoding: MUST be set by user. * - decoding: Set by libavcodec. * Note: For compatibility it is possible to set this instead of * coded_width/height before decoding. */ int width, height; /** * Bitstream width / height, may be different from width/height if lowres enabled. * - encoding: unused * - decoding: Set by user before init if known. Codec should override / dynamically change if needed. */ int coded_width, coded_height; #define FF_ASPECT_EXTENDED 15 /** * the number of pictures in a group of pictures, or 0 for intra_only * - encoding: Set by user. * - decoding: unused */ int gop_size; /** * Pixel format, see AV_PIX_FMT_xxx. * May be set by the demuxer if known from headers. * May be overridden by the decoder if it knows better. * - encoding: Set by user. * - decoding: Set by user if known, overridden by libavcodec if known */ enum AVPixelFormat pix_fmt; /** * Motion estimation algorithm used for video coding. * 1 (zero), 2 (full), 3 (log), 4 (phods), 5 (epzs), 6 (x1), 7 (hex), * 8 (umh), 9 (iter), 10 (tesa) [7, 8, 10 are x264 specific, 9 is snow specific] * - encoding: MUST be set by user. * - decoding: unused */ int me_method; /** * If non NULL, 'draw_horiz_band' is called by the libavcodec * decoder to draw a horizontal band. It improves cache usage. Not * all codecs can do that. You must check the codec capabilities * beforehand. * When multithreading is used, it may be called from multiple threads * at the same time; threads might draw different parts of the same AVFrame, * or multiple AVFrames, and there is no guarantee that slices will be drawn * in order. * The function is also used by hardware acceleration APIs. * It is called at least once during frame decoding to pass * the data needed for hardware render. * In that mode instead of pixel data, AVFrame points to * a structure specific to the acceleration API. The application * reads the structure and can change some fields to indicate progress * or mark state. * - encoding: unused * - decoding: Set by user. * @param height the height of the slice * @param y the y position of the slice * @param type 1->top field, 2->bottom field, 3->frame * @param offset offset into the AVFrame.data from which the slice should be read */ void (*draw_horiz_band)(struct AVCodecContext *s, const AVFrame *src, int offset[AV_NUM_DATA_POINTERS], int y, int type, int height); /** * callback to negotiate the pixelFormat * @param fmt is the list of formats which are supported by the codec, * it is terminated by -1 as 0 is a valid format, the formats are ordered by quality. * The first is always the native one. * @return the chosen format * - encoding: unused * - decoding: Set by user, if not set the native format will be chosen. */ enum AVPixelFormat (*get_format)(struct AVCodecContext *s, const enum AVPixelFormat * fmt); /** * maximum number of B-frames between non-B-frames * Note: The output will be delayed by max_b_frames+1 relative to the input. * - encoding: Set by user. * - decoding: unused */ int max_b_frames; /** * qscale factor between IP and B-frames * If > 0 then the last P-frame quantizer will be used (q= lastp_q*factor+offset). * If < 0 then normal ratecontrol will be done (q= -normal_q*factor+offset). * - encoding: Set by user. * - decoding: unused */ float b_quant_factor; /** obsolete FIXME remove */ int rc_strategy; #define FF_RC_STRATEGY_XVID 1 int b_frame_strategy; #if FF_API_MPV_GLOBAL_OPTS /** * luma single coefficient elimination threshold * - encoding: Set by user. * - decoding: unused */ attribute_deprecated int luma_elim_threshold; /** * chroma single coeff elimination threshold * - encoding: Set by user. * - decoding: unused */ attribute_deprecated int chroma_elim_threshold; #endif /** * qscale offset between IP and B-frames * - encoding: Set by user. * - decoding: unused */ float b_quant_offset; /** * Size of the frame reordering buffer in the decoder. * For MPEG-2 it is 1 IPB or 0 low delay IP. * - encoding: Set by libavcodec. * - decoding: Set by libavcodec. */ int has_b_frames; /** * 0-> h263 quant 1-> mpeg quant * - encoding: Set by user. * - decoding: unused */ int mpeg_quant; /** * qscale factor between P and I-frames * If > 0 then the last p frame quantizer will be used (q= lastp_q*factor+offset). * If < 0 then normal ratecontrol will be done (q= -normal_q*factor+offset). * - encoding: Set by user. * - decoding: unused */ float i_quant_factor; /** * qscale offset between P and I-frames * - encoding: Set by user. * - decoding: unused */ float i_quant_offset; /** * luminance masking (0-> disabled) * - encoding: Set by user. * - decoding: unused */ float lumi_masking; /** * temporary complexity masking (0-> disabled) * - encoding: Set by user. * - decoding: unused */ float temporal_cplx_masking; /** * spatial complexity masking (0-> disabled) * - encoding: Set by user. * - decoding: unused */ float spatial_cplx_masking; /** * p block masking (0-> disabled) * - encoding: Set by user. * - decoding: unused */ float p_masking; /** * darkness masking (0-> disabled) * - encoding: Set by user. * - decoding: unused */ float dark_masking; /** * slice count * - encoding: Set by libavcodec. * - decoding: Set by user (or 0). */ int slice_count; /** * prediction method (needed for huffyuv) * - encoding: Set by user. * - decoding: unused */ int prediction_method; #define FF_PRED_LEFT 0 #define FF_PRED_PLANE 1 #define FF_PRED_MEDIAN 2 /** * slice offsets in the frame in bytes * - encoding: Set/allocated by libavcodec. * - decoding: Set/allocated by user (or NULL). */ int *slice_offset; /** * sample aspect ratio (0 if unknown) * That is the width of a pixel divided by the height of the pixel. * Numerator and denominator must be relatively prime and smaller than 256 for some video standards. * - encoding: Set by user. * - decoding: Set by libavcodec. */ AVRational sample_aspect_ratio; /** * motion estimation comparison function * - encoding: Set by user. * - decoding: unused */ int me_cmp; /** * subpixel motion estimation comparison function * - encoding: Set by user. * - decoding: unused */ int me_sub_cmp; /** * macroblock comparison function (not supported yet) * - encoding: Set by user. * - decoding: unused */ int mb_cmp; /** * interlaced DCT comparison function * - encoding: Set by user. * - decoding: unused */ int ildct_cmp; #define FF_CMP_SAD 0 #define FF_CMP_SSE 1 #define FF_CMP_SATD 2 #define FF_CMP_DCT 3 #define FF_CMP_PSNR 4 #define FF_CMP_BIT 5 #define FF_CMP_RD 6 #define FF_CMP_ZERO 7 #define FF_CMP_VSAD 8 #define FF_CMP_VSSE 9 #define FF_CMP_NSSE 10 #define FF_CMP_W53 11 #define FF_CMP_W97 12 #define FF_CMP_DCTMAX 13 #define FF_CMP_DCT264 14 #define FF_CMP_CHROMA 256 /** * ME diamond size & shape * - encoding: Set by user. * - decoding: unused */ int dia_size; /** * amount of previous MV predictors (2a+1 x 2a+1 square) * - encoding: Set by user. * - decoding: unused */ int last_predictor_count; /** * prepass for motion estimation * - encoding: Set by user. * - decoding: unused */ int pre_me; /** * motion estimation prepass comparison function * - encoding: Set by user. * - decoding: unused */ int me_pre_cmp; /** * ME prepass diamond size & shape * - encoding: Set by user. * - decoding: unused */ int pre_dia_size; /** * subpel ME quality * - encoding: Set by user. * - decoding: unused */ int me_subpel_quality; /** * DTG active format information (additional aspect ratio * information only used in DVB MPEG-2 transport streams) * 0 if not set. * * - encoding: unused * - decoding: Set by decoder. */ int dtg_active_format; #define FF_DTG_AFD_SAME 8 #define FF_DTG_AFD_4_3 9 #define FF_DTG_AFD_16_9 10 #define FF_DTG_AFD_14_9 11 #define FF_DTG_AFD_4_3_SP_14_9 13 #define FF_DTG_AFD_16_9_SP_14_9 14 #define FF_DTG_AFD_SP_4_3 15 /** * maximum motion estimation search range in subpel units * If 0 then no limit. * * - encoding: Set by user. * - decoding: unused */ int me_range; /** * intra quantizer bias * - encoding: Set by user. * - decoding: unused */ int intra_quant_bias; #define FF_DEFAULT_QUANT_BIAS 999999 /** * inter quantizer bias * - encoding: Set by user. * - decoding: unused */ int inter_quant_bias; #if FF_API_COLOR_TABLE_ID /** * color table ID * - encoding: unused * - decoding: Which clrtable should be used for 8bit RGB images. * Tables have to be stored somewhere. FIXME */ attribute_deprecated int color_table_id; #endif /** * slice flags * - encoding: unused * - decoding: Set by user. */ int slice_flags; #define SLICE_FLAG_CODED_ORDER 0x0001 ///< draw_horiz_band() is called in coded order instead of display #define SLICE_FLAG_ALLOW_FIELD 0x0002 ///< allow draw_horiz_band() with field slices (MPEG2 field pics) #define SLICE_FLAG_ALLOW_PLANE 0x0004 ///< allow draw_horiz_band() with 1 component at a time (SVQ1) /** * XVideo Motion Acceleration * - encoding: forbidden * - decoding: set by decoder */ int xvmc_acceleration; /** * macroblock decision mode * - encoding: Set by user. * - decoding: unused */ int mb_decision; #define FF_MB_DECISION_SIMPLE 0 ///< uses mb_cmp #define FF_MB_DECISION_BITS 1 ///< chooses the one which needs the fewest bits #define FF_MB_DECISION_RD 2 ///< rate distortion /** * custom intra quantization matrix * - encoding: Set by user, can be NULL. * - decoding: Set by libavcodec. */ uint16_t *intra_matrix; /** * custom inter quantization matrix * - encoding: Set by user, can be NULL. * - decoding: Set by libavcodec. */ uint16_t *inter_matrix; /** * scene change detection threshold * 0 is default, larger means fewer detected scene changes. * - encoding: Set by user. * - decoding: unused */ int scenechange_threshold; /** * noise reduction strength * - encoding: Set by user. * - decoding: unused */ int noise_reduction; #if FF_API_INTER_THRESHOLD /** * @deprecated this field is unused */ attribute_deprecated int inter_threshold; #endif #if FF_API_MPV_GLOBAL_OPTS /** * @deprecated use mpegvideo private options instead */ attribute_deprecated int quantizer_noise_shaping; #endif /** * Motion estimation threshold below which no motion estimation is * performed, but instead the user specified motion vectors are used. * * - encoding: Set by user. * - decoding: unused */ int me_threshold; /** * Macroblock threshold below which the user specified macroblock types will be used. * - encoding: Set by user. * - decoding: unused */ int mb_threshold; /** * precision of the intra DC coefficient - 8 * - encoding: Set by user. * - decoding: unused */ int intra_dc_precision; /** * Number of macroblock rows at the top which are skipped. * - encoding: unused * - decoding: Set by user. */ int skip_top; /** * Number of macroblock rows at the bottom which are skipped. * - encoding: unused * - decoding: Set by user. */ int skip_bottom; /** * Border processing masking, raises the quantizer for mbs on the borders * of the picture. * - encoding: Set by user. * - decoding: unused */ float border_masking; /** * minimum MB lagrange multipler * - encoding: Set by user. * - decoding: unused */ int mb_lmin; /** * maximum MB lagrange multipler * - encoding: Set by user. * - decoding: unused */ int mb_lmax; /** * * - encoding: Set by user. * - decoding: unused */ int me_penalty_compensation; /** * * - encoding: Set by user. * - decoding: unused */ int bidir_refine; /** * * - encoding: Set by user. * - decoding: unused */ int brd_scale; /** * minimum GOP size * - encoding: Set by user. * - decoding: unused */ int keyint_min; /** * number of reference frames * - encoding: Set by user. * - decoding: Set by lavc. */ int refs; /** * chroma qp offset from luma * - encoding: Set by user. * - decoding: unused */ int chromaoffset; /** * Multiplied by qscale for each frame and added to scene_change_score. * - encoding: Set by user. * - decoding: unused */ int scenechange_factor; /** * * Note: Value depends upon the compare function used for fullpel ME. * - encoding: Set by user. * - decoding: unused */ int mv0_threshold; /** * Adjust sensitivity of b_frame_strategy 1. * - encoding: Set by user. * - decoding: unused */ int b_sensitivity; /** * Chromaticity coordinates of the source primaries. * - encoding: Set by user * - decoding: Set by libavcodec */ enum AVColorPrimaries color_primaries; /** * Color Transfer Characteristic. * - encoding: Set by user * - decoding: Set by libavcodec */ enum AVColorTransferCharacteristic color_trc; /** * YUV colorspace type. * - encoding: Set by user * - decoding: Set by libavcodec */ enum AVColorSpace colorspace; /** * MPEG vs JPEG YUV range. * - encoding: Set by user * - decoding: Set by libavcodec */ enum AVColorRange color_range; /** * This defines the location of chroma samples. * - encoding: Set by user * - decoding: Set by libavcodec */ enum AVChromaLocation chroma_sample_location; /** * Number of slices. * Indicates number of picture subdivisions. Used for parallelized * decoding. * - encoding: Set by user * - decoding: unused */ int slices; /** Field order * - encoding: set by libavcodec * - decoding: Set by user. */ enum AVFieldOrder field_order; /* audio only */ int sample_rate; ///< samples per second int channels; ///< number of audio channels /** * audio sample format * - encoding: Set by user. * - decoding: Set by libavcodec. */ enum AVSampleFormat sample_fmt; ///< sample format /* The following data should not be initialized. */ /** * Samples per packet, initialized when calling 'init'. */ int frame_size; /** * Frame counter, set by libavcodec. * * - decoding: total number of frames returned from the decoder so far. * - encoding: total number of frames passed to the encoder so far. * * @note the counter is not incremented if encoding/decoding resulted in * an error. */ int frame_number; /** * number of bytes per packet if constant and known or 0 * Used by some WAV based audio codecs. */ int block_align; /** * Audio cutoff bandwidth (0 means "automatic") * - encoding: Set by user. * - decoding: unused */ int cutoff; #if FF_API_REQUEST_CHANNELS /** * Decoder should decode to this many channels if it can (0 for default) * - encoding: unused * - decoding: Set by user. * @deprecated Deprecated in favor of request_channel_layout. */ int request_channels; #endif /** * Audio channel layout. * - encoding: set by user. * - decoding: set by user, may be overwritten by libavcodec. */ uint64_t channel_layout; /** * Request decoder to use this channel layout if it can (0 for default) * - encoding: unused * - decoding: Set by user. */ uint64_t request_channel_layout; /** * Type of service that the audio stream conveys. * - encoding: Set by user. * - decoding: Set by libavcodec. */ enum AVAudioServiceType audio_service_type; /** * desired sample format * - encoding: Not used. * - decoding: Set by user. * Decoder will decode to this format if it can. */ enum AVSampleFormat request_sample_fmt; /** * Called at the beginning of each frame to get a buffer for it. * * The function will set AVFrame.data[], AVFrame.linesize[]. * AVFrame.extended_data[] must also be set, but it should be the same as * AVFrame.data[] except for planar audio with more channels than can fit * in AVFrame.data[]. In that case, AVFrame.data[] shall still contain as * many data pointers as it can hold. * * if CODEC_CAP_DR1 is not set then get_buffer() must call * avcodec_default_get_buffer() instead of providing buffers allocated by * some other means. * * AVFrame.data[] should be 32- or 16-byte-aligned unless the CPU doesn't * need it. avcodec_default_get_buffer() aligns the output buffer properly, * but if get_buffer() is overridden then alignment considerations should * be taken into account. * * @see avcodec_default_get_buffer() * * Video: * * If pic.reference is set then the frame will be read later by libavcodec. * avcodec_align_dimensions2() should be used to find the required width and * height, as they normally need to be rounded up to the next multiple of 16. * * If frame multithreading is used and thread_safe_callbacks is set, * it may be called from a different thread, but not from more than one at * once. Does not need to be reentrant. * * @see release_buffer(), reget_buffer() * @see avcodec_align_dimensions2() * * Audio: * * Decoders request a buffer of a particular size by setting * AVFrame.nb_samples prior to calling get_buffer(). The decoder may, * however, utilize only part of the buffer by setting AVFrame.nb_samples * to a smaller value in the output frame. * * Decoders cannot use the buffer after returning from * avcodec_decode_audio4(), so they will not call release_buffer(), as it * is assumed to be released immediately upon return. * * As a convenience, av_samples_get_buffer_size() and * av_samples_fill_arrays() in libavutil may be used by custom get_buffer() * functions to find the required data size and to fill data pointers and * linesize. In AVFrame.linesize, only linesize[0] may be set for audio * since all planes must be the same size. * * @see av_samples_get_buffer_size(), av_samples_fill_arrays() * * - encoding: unused * - decoding: Set by libavcodec, user can override. */ int (*get_buffer)(struct AVCodecContext *c, AVFrame *pic); /** * Called to release buffers which were allocated with get_buffer. * A released buffer can be reused in get_buffer(). * pic.data[*] must be set to NULL. * May be called from a different thread if frame multithreading is used, * but not by more than one thread at once, so does not need to be reentrant. * - encoding: unused * - decoding: Set by libavcodec, user can override. */ void (*release_buffer)(struct AVCodecContext *c, AVFrame *pic); /** * Called at the beginning of a frame to get cr buffer for it. * Buffer type (size, hints) must be the same. libavcodec won't check it. * libavcodec will pass previous buffer in pic, function should return * same buffer or new buffer with old frame "painted" into it. * If pic.data[0] == NULL must behave like get_buffer(). * if CODEC_CAP_DR1 is not set then reget_buffer() must call * avcodec_default_reget_buffer() instead of providing buffers allocated by * some other means. * - encoding: unused * - decoding: Set by libavcodec, user can override. */ int (*reget_buffer)(struct AVCodecContext *c, AVFrame *pic); /* - encoding parameters */ float qcompress; ///< amount of qscale change between easy & hard scenes (0.0-1.0) float qblur; ///< amount of qscale smoothing over time (0.0-1.0) /** * minimum quantizer * - encoding: Set by user. * - decoding: unused */ int qmin; /** * maximum quantizer * - encoding: Set by user. * - decoding: unused */ int qmax; /** * maximum quantizer difference between frames * - encoding: Set by user. * - decoding: unused */ int max_qdiff; /** * ratecontrol qmin qmax limiting method * 0-> clipping, 1-> use a nice continuous function to limit qscale wthin qmin/qmax. * - encoding: Set by user. * - decoding: unused */ float rc_qsquish; float rc_qmod_amp; int rc_qmod_freq; /** * decoder bitstream buffer size * - encoding: Set by user. * - decoding: unused */ int rc_buffer_size; /** * ratecontrol override, see RcOverride * - encoding: Allocated/set/freed by user. * - decoding: unused */ int rc_override_count; RcOverride *rc_override; /** * rate control equation * - encoding: Set by user * - decoding: unused */ const char *rc_eq; /** * maximum bitrate * - encoding: Set by user. * - decoding: unused */ int rc_max_rate; /** * minimum bitrate * - encoding: Set by user. * - decoding: unused */ int rc_min_rate; float rc_buffer_aggressivity; /** * initial complexity for pass1 ratecontrol * - encoding: Set by user. * - decoding: unused */ float rc_initial_cplx; /** * Ratecontrol attempt to use, at maximum, of what can be used without an underflow. * - encoding: Set by user. * - decoding: unused. */ float rc_max_available_vbv_use; /** * Ratecontrol attempt to use, at least, times the amount needed to prevent a vbv overflow. * - encoding: Set by user. * - decoding: unused. */ float rc_min_vbv_overflow_use; /** * Number of bits which should be loaded into the rc buffer before decoding starts. * - encoding: Set by user. * - decoding: unused */ int rc_initial_buffer_occupancy; #define FF_CODER_TYPE_VLC 0 #define FF_CODER_TYPE_AC 1 #define FF_CODER_TYPE_RAW 2 #define FF_CODER_TYPE_RLE 3 #define FF_CODER_TYPE_DEFLATE 4 /** * coder type * - encoding: Set by user. * - decoding: unused */ int coder_type; /** * context model * - encoding: Set by user. * - decoding: unused */ int context_model; /** * minimum Lagrange multipler * - encoding: Set by user. * - decoding: unused */ int lmin; /** * maximum Lagrange multipler * - encoding: Set by user. * - decoding: unused */ int lmax; /** * frame skip threshold * - encoding: Set by user. * - decoding: unused */ int frame_skip_threshold; /** * frame skip factor * - encoding: Set by user. * - decoding: unused */ int frame_skip_factor; /** * frame skip exponent * - encoding: Set by user. * - decoding: unused */ int frame_skip_exp; /** * frame skip comparison function * - encoding: Set by user. * - decoding: unused */ int frame_skip_cmp; /** * trellis RD quantization * - encoding: Set by user. * - decoding: unused */ int trellis; /** * - encoding: Set by user. * - decoding: unused */ int min_prediction_order; /** * - encoding: Set by user. * - decoding: unused */ int max_prediction_order; /** * GOP timecode frame start number * - encoding: Set by user, in non drop frame format * - decoding: Set by libavcodec (timecode in the 25 bits format, -1 if unset) */ int64_t timecode_frame_start; /* The RTP callback: This function is called */ /* every time the encoder has a packet to send. */ /* It depends on the encoder if the data starts */ /* with a Start Code (it should). H.263 does. */ /* mb_nb contains the number of macroblocks */ /* encoded in the RTP payload. */ void (*rtp_callback)(struct AVCodecContext *avctx, void *data, int size, int mb_nb); int rtp_payload_size; /* The size of the RTP payload: the coder will */ /* do its best to deliver a chunk with size */ /* below rtp_payload_size, the chunk will start */ /* with a start code on some codecs like H.263. */ /* This doesn't take account of any particular */ /* headers inside the transmitted RTP payload. */ /* statistics, used for 2-pass encoding */ int mv_bits; int header_bits; int i_tex_bits; int p_tex_bits; int i_count; int p_count; int skip_count; int misc_bits; /** * number of bits used for the previously encoded frame * - encoding: Set by libavcodec. * - decoding: unused */ int frame_bits; /** * pass1 encoding statistics output buffer * - encoding: Set by libavcodec. * - decoding: unused */ char *stats_out; /** * pass2 encoding statistics input buffer * Concatenated stuff from stats_out of pass1 should be placed here. * - encoding: Allocated/set/freed by user. * - decoding: unused */ char *stats_in; /** * Work around bugs in encoders which sometimes cannot be detected automatically. * - encoding: Set by user * - decoding: Set by user */ int workaround_bugs; #define FF_BUG_AUTODETECT 1 ///< autodetection #define FF_BUG_OLD_MSMPEG4 2 #define FF_BUG_XVID_ILACE 4 #define FF_BUG_UMP4 8 #define FF_BUG_NO_PADDING 16 #define FF_BUG_AMV 32 #define FF_BUG_AC_VLC 0 ///< Will be removed, libavcodec can now handle these non-compliant files by default. #define FF_BUG_QPEL_CHROMA 64 #define FF_BUG_STD_QPEL 128 #define FF_BUG_QPEL_CHROMA2 256 #define FF_BUG_DIRECT_BLOCKSIZE 512 #define FF_BUG_EDGE 1024 #define FF_BUG_HPEL_CHROMA 2048 #define FF_BUG_DC_CLIP 4096 #define FF_BUG_MS 8192 ///< Work around various bugs in Microsoft's broken decoders. #define FF_BUG_TRUNCATED 16384 /** * strictly follow the standard (MPEG4, ...). * - encoding: Set by user. * - decoding: Set by user. * Setting this to STRICT or higher means the encoder and decoder will * generally do stupid things, whereas setting it to unofficial or lower * will mean the encoder might produce output that is not supported by all * spec-compliant decoders. Decoders don't differentiate between normal, * unofficial and experimental (that is, they always try to decode things * when they can) unless they are explicitly asked to behave stupidly * (=strictly conform to the specs) */ int strict_std_compliance; #define FF_COMPLIANCE_VERY_STRICT 2 ///< Strictly conform to an older more strict version of the spec or reference software. #define FF_COMPLIANCE_STRICT 1 ///< Strictly conform to all the things in the spec no matter what consequences. #define FF_COMPLIANCE_NORMAL 0 #define FF_COMPLIANCE_UNOFFICIAL -1 ///< Allow unofficial extensions #define FF_COMPLIANCE_EXPERIMENTAL -2 ///< Allow nonstandardized experimental things. /** * error concealment flags * - encoding: unused * - decoding: Set by user. */ int error_concealment; #define FF_EC_GUESS_MVS 1 #define FF_EC_DEBLOCK 2 /** * debug * - encoding: Set by user. * - decoding: Set by user. */ int debug; #define FF_DEBUG_PICT_INFO 1 #define FF_DEBUG_RC 2 #define FF_DEBUG_BITSTREAM 4 #define FF_DEBUG_MB_TYPE 8 #define FF_DEBUG_QP 16 #define FF_DEBUG_MV 32 #define FF_DEBUG_DCT_COEFF 0x00000040 #define FF_DEBUG_SKIP 0x00000080 #define FF_DEBUG_STARTCODE 0x00000100 #define FF_DEBUG_PTS 0x00000200 #define FF_DEBUG_ER 0x00000400 #define FF_DEBUG_MMCO 0x00000800 #define FF_DEBUG_BUGS 0x00001000 #define FF_DEBUG_VIS_QP 0x00002000 #define FF_DEBUG_VIS_MB_TYPE 0x00004000 #define FF_DEBUG_BUFFERS 0x00008000 #define FF_DEBUG_THREADS 0x00010000 /** * debug * - encoding: Set by user. * - decoding: Set by user. */ int debug_mv; #define FF_DEBUG_VIS_MV_P_FOR 0x00000001 //visualize forward predicted MVs of P frames #define FF_DEBUG_VIS_MV_B_FOR 0x00000002 //visualize forward predicted MVs of B frames #define FF_DEBUG_VIS_MV_B_BACK 0x00000004 //visualize backward predicted MVs of B frames /** * Error recognition; may misdetect some more or less valid parts as errors. * - encoding: unused * - decoding: Set by user. */ int err_recognition; #define AV_EF_CRCCHECK (1<<0) #define AV_EF_BITSTREAM (1<<1) #define AV_EF_BUFFER (1<<2) #define AV_EF_EXPLODE (1<<3) #define AV_EF_CAREFUL (1<<16) #define AV_EF_COMPLIANT (1<<17) #define AV_EF_AGGRESSIVE (1<<18) /** * opaque 64bit number (generally a PTS) that will be reordered and * output in AVFrame.reordered_opaque * @deprecated in favor of pkt_pts * - encoding: unused * - decoding: Set by user. */ int64_t reordered_opaque; /** * Hardware accelerator in use * - encoding: unused. * - decoding: Set by libavcodec */ struct AVHWAccel *hwaccel; /** * Hardware accelerator context. * For some hardware accelerators, a global context needs to be * provided by the user. In that case, this holds display-dependent * data FFmpeg cannot instantiate itself. Please refer to the * FFmpeg HW accelerator documentation to know how to fill this * is. e.g. for VA API, this is a struct vaapi_context. * - encoding: unused * - decoding: Set by user */ void *hwaccel_context; /** * error * - encoding: Set by libavcodec if flags&CODEC_FLAG_PSNR. * - decoding: unused */ uint64_t error[AV_NUM_DATA_POINTERS]; /** * DCT algorithm, see FF_DCT_* below * - encoding: Set by user. * - decoding: unused */ int dct_algo; #define FF_DCT_AUTO 0 #define FF_DCT_FASTINT 1 #define FF_DCT_INT 2 #define FF_DCT_MMX 3 #define FF_DCT_ALTIVEC 5 #define FF_DCT_FAAN 6 /** * IDCT algorithm, see FF_IDCT_* below. * - encoding: Set by user. * - decoding: Set by user. */ int idct_algo; #define FF_IDCT_AUTO 0 #define FF_IDCT_INT 1 #define FF_IDCT_SIMPLE 2 #define FF_IDCT_SIMPLEMMX 3 #define FF_IDCT_LIBMPEG2MMX 4 #define FF_IDCT_MMI 5 #define FF_IDCT_ARM 7 #define FF_IDCT_ALTIVEC 8 #define FF_IDCT_SH4 9 #define FF_IDCT_SIMPLEARM 10 #define FF_IDCT_H264 11 #define FF_IDCT_VP3 12 #define FF_IDCT_IPP 13 #define FF_IDCT_XVIDMMX 14 #define FF_IDCT_CAVS 15 #define FF_IDCT_SIMPLEARMV5TE 16 #define FF_IDCT_SIMPLEARMV6 17 #define FF_IDCT_SIMPLEVIS 18 #define FF_IDCT_WMV2 19 #define FF_IDCT_FAAN 20 #define FF_IDCT_EA 21 #define FF_IDCT_SIMPLENEON 22 #define FF_IDCT_SIMPLEALPHA 23 #define FF_IDCT_BINK 24 #if FF_API_DSP_MASK /** * Unused. * @deprecated use av_set_cpu_flags_mask() instead. */ attribute_deprecated unsigned dsp_mask; #endif /** * bits per sample/pixel from the demuxer (needed for huffyuv). * - encoding: Set by libavcodec. * - decoding: Set by user. */ int bits_per_coded_sample; /** * Bits per sample/pixel of internal libavcodec pixel/sample format. * - encoding: set by user. * - decoding: set by libavcodec. */ int bits_per_raw_sample; /** * low resolution decoding, 1-> 1/2 size, 2->1/4 size * - encoding: unused * - decoding: Set by user. */ int lowres; /** * the picture in the bitstream * - encoding: Set by libavcodec. * - decoding: Set by libavcodec. */ AVFrame *coded_frame; /** * thread count * is used to decide how many independent tasks should be passed to execute() * - encoding: Set by user. * - decoding: Set by user. */ int thread_count; /** * Which multithreading methods to use. * Use of FF_THREAD_FRAME will increase decoding delay by one frame per thread, * so clients which cannot provide future frames should not use it. * * - encoding: Set by user, otherwise the default is used. * - decoding: Set by user, otherwise the default is used. */ int thread_type; #define FF_THREAD_FRAME 1 ///< Decode more than one frame at once #define FF_THREAD_SLICE 2 ///< Decode more than one part of a single frame at once /** * Which multithreading methods are in use by the codec. * - encoding: Set by libavcodec. * - decoding: Set by libavcodec. */ int active_thread_type; /** * Set by the client if its custom get_buffer() callback can be called * synchronously from another thread, which allows faster multithreaded decoding. * draw_horiz_band() will be called from other threads regardless of this setting. * Ignored if the default get_buffer() is used. * - encoding: Set by user. * - decoding: Set by user. */ int thread_safe_callbacks; /** * The codec may call this to execute several independent things. * It will return only after finishing all tasks. * The user may replace this with some multithreaded implementation, * the default implementation will execute the parts serially. * @param count the number of things to execute * - encoding: Set by libavcodec, user can override. * - decoding: Set by libavcodec, user can override. */ int (*execute)(struct AVCodecContext *c, int (*func)(struct AVCodecContext *c2, void *arg), void *arg2, int *ret, int count, int size); /** * The codec may call this to execute several independent things. * It will return only after finishing all tasks. * The user may replace this with some multithreaded implementation, * the default implementation will execute the parts serially. * Also see avcodec_thread_init and e.g. the --enable-pthread configure option. * @param c context passed also to func * @param count the number of things to execute * @param arg2 argument passed unchanged to func * @param ret return values of executed functions, must have space for "count" values. May be NULL. * @param func function that will be called count times, with jobnr from 0 to count-1. * threadnr will be in the range 0 to c->thread_count-1 < MAX_THREADS and so that no * two instances of func executing at the same time will have the same threadnr. * @return always 0 currently, but code should handle a future improvement where when any call to func * returns < 0 no further calls to func may be done and < 0 is returned. * - encoding: Set by libavcodec, user can override. * - decoding: Set by libavcodec, user can override. */ int (*execute2)(struct AVCodecContext *c, int (*func)(struct AVCodecContext *c2, void *arg, int jobnr, int threadnr), void *arg2, int *ret, int count); /** * thread opaque * Can be used by execute() to store some per AVCodecContext stuff. * - encoding: set by execute() * - decoding: set by execute() */ void *thread_opaque; /** * noise vs. sse weight for the nsse comparsion function * - encoding: Set by user. * - decoding: unused */ int nsse_weight; /** * profile * - encoding: Set by user. * - decoding: Set by libavcodec. */ int profile; #define FF_PROFILE_UNKNOWN -99 #define FF_PROFILE_RESERVED -100 #define FF_PROFILE_AAC_MAIN 0 #define FF_PROFILE_AAC_LOW 1 #define FF_PROFILE_AAC_SSR 2 #define FF_PROFILE_AAC_LTP 3 #define FF_PROFILE_AAC_HE 4 #define FF_PROFILE_AAC_HE_V2 28 #define FF_PROFILE_AAC_LD 22 #define FF_PROFILE_AAC_ELD 38 #define FF_PROFILE_DTS 20 #define FF_PROFILE_DTS_ES 30 #define FF_PROFILE_DTS_96_24 40 #define FF_PROFILE_DTS_HD_HRA 50 #define FF_PROFILE_DTS_HD_MA 60 #define FF_PROFILE_MPEG2_422 0 #define FF_PROFILE_MPEG2_HIGH 1 #define FF_PROFILE_MPEG2_SS 2 #define FF_PROFILE_MPEG2_SNR_SCALABLE 3 #define FF_PROFILE_MPEG2_MAIN 4 #define FF_PROFILE_MPEG2_SIMPLE 5 #define FF_PROFILE_H264_CONSTRAINED (1<<9) // 8+1; constraint_set1_flag #define FF_PROFILE_H264_INTRA (1<<11) // 8+3; constraint_set3_flag #define FF_PROFILE_H264_BASELINE 66 #define FF_PROFILE_H264_CONSTRAINED_BASELINE (66|FF_PROFILE_H264_CONSTRAINED) #define FF_PROFILE_H264_MAIN 77 #define FF_PROFILE_H264_EXTENDED 88 #define FF_PROFILE_H264_HIGH 100 #define FF_PROFILE_H264_HIGH_10 110 #define FF_PROFILE_H264_HIGH_10_INTRA (110|FF_PROFILE_H264_INTRA) #define FF_PROFILE_H264_HIGH_422 122 #define FF_PROFILE_H264_HIGH_422_INTRA (122|FF_PROFILE_H264_INTRA) #define FF_PROFILE_H264_HIGH_444 144 #define FF_PROFILE_H264_HIGH_444_PREDICTIVE 244 #define FF_PROFILE_H264_HIGH_444_INTRA (244|FF_PROFILE_H264_INTRA) #define FF_PROFILE_H264_CAVLC_444 44 #define FF_PROFILE_VC1_SIMPLE 0 #define FF_PROFILE_VC1_MAIN 1 #define FF_PROFILE_VC1_COMPLEX 2 #define FF_PROFILE_VC1_ADVANCED 3 #define FF_PROFILE_MPEG4_SIMPLE 0 #define FF_PROFILE_MPEG4_SIMPLE_SCALABLE 1 #define FF_PROFILE_MPEG4_CORE 2 #define FF_PROFILE_MPEG4_MAIN 3 #define FF_PROFILE_MPEG4_N_BIT 4 #define FF_PROFILE_MPEG4_SCALABLE_TEXTURE 5 #define FF_PROFILE_MPEG4_SIMPLE_FACE_ANIMATION 6 #define FF_PROFILE_MPEG4_BASIC_ANIMATED_TEXTURE 7 #define FF_PROFILE_MPEG4_HYBRID 8 #define FF_PROFILE_MPEG4_ADVANCED_REAL_TIME 9 #define FF_PROFILE_MPEG4_CORE_SCALABLE 10 #define FF_PROFILE_MPEG4_ADVANCED_CODING 11 #define FF_PROFILE_MPEG4_ADVANCED_CORE 12 #define FF_PROFILE_MPEG4_ADVANCED_SCALABLE_TEXTURE 13 #define FF_PROFILE_MPEG4_SIMPLE_STUDIO 14 #define FF_PROFILE_MPEG4_ADVANCED_SIMPLE 15 /** * level * - encoding: Set by user. * - decoding: Set by libavcodec. */ int level; #define FF_LEVEL_UNKNOWN -99 /** * * - encoding: unused * - decoding: Set by user. */ enum AVDiscard skip_loop_filter; /** * * - encoding: unused * - decoding: Set by user. */ enum AVDiscard skip_idct; /** * * - encoding: unused * - decoding: Set by user. */ enum AVDiscard skip_frame; /** * Header containing style information for text subtitles. * For SUBTITLE_ASS subtitle type, it should contain the whole ASS * [Script Info] and [V4+ Styles] section, plus the [Events] line and * the Format line following. It shouldn't include any Dialogue line. * - encoding: Set/allocated/freed by user (before avcodec_open2()) * - decoding: Set/allocated/freed by libavcodec (by avcodec_open2()) */ uint8_t *subtitle_header; int subtitle_header_size; /** * Simulates errors in the bitstream to test error concealment. * - encoding: Set by user. * - decoding: unused */ int error_rate; /** * Current packet as passed into the decoder, to avoid having * to pass the packet into every function. Currently only valid * inside lavc and get/release_buffer callbacks. * - decoding: set by avcodec_decode_*, read by get_buffer() for setting pkt_pts * - encoding: unused */ AVPacket *pkt; /** * VBV delay coded in the last frame (in periods of a 27 MHz clock). * Used for compliant TS muxing. * - encoding: Set by libavcodec. * - decoding: unused. */ uint64_t vbv_delay; /** * Timebase in which pkt_dts/pts and AVPacket.dts/pts are. * Code outside libavcodec should access this field using: * avcodec_set_pkt_timebase(avctx) * - encoding unused. * - decodimg set by user */ AVRational pkt_timebase; /** * AVCodecDescriptor * Code outside libavcodec should access this field using: * avcodec_get_codec_descriptior(avctx) * - encoding: unused. * - decoding: set by libavcodec. */ const AVCodecDescriptor *codec_descriptor; /** * Current statistics for PTS correction. * - decoding: maintained and used by libavcodec, not intended to be used by user apps * - encoding: unused */ int64_t pts_correction_num_faulty_pts; /// Number of incorrect PTS values so far int64_t pts_correction_num_faulty_dts; /// Number of incorrect DTS values so far int64_t pts_correction_last_pts; /// PTS of the last frame int64_t pts_correction_last_dts; /// DTS of the last frame } AVCodecContext; ``` # 二、AVCodecContext 重点字段 下面挑一些关键的变量来看看(这里只考虑解码): ``` enum AVMediaType codec_type:编解码器的类型(视频,音频...) struct AVCodec *codec:采用的解码器AVCodec(H.264,MPEG2...) int bit_rate:平均比特率 uint8_t *extradata; int extradata_size:针对特定编码器包含的附加信息(例如对于H.264解码器来说,存储SPS,PPS等) AVRational time_base:根据该参数,可以把PTS转化为实际的时间(单位为秒s) int width, height:如果是视频的话,代表宽和高 int refs:运动估计参考帧的个数(H.264的话会有多帧,MPEG2这类的一般就没有了) int sample_rate:采样率(音频) int channels:声道数(音频) enum AVSampleFormat sample_fmt:采样格式 int profile:型(H.264里面就有,其他编码标准应该也有) int level:级(和profile差不太多) ``` 在这里需要注意:AVCodecContext中很多的参数是编码的时候使用的,而不是解码的时候使用的。 其实这些参数都比较容易理解。就不多费篇幅了。在这里看一下以下几个参数: ### 1.codec_type 编解码器类型有以下几种: ``` enum AVMediaType { AVMEDIA_TYPE_UNKNOWN = -1, ///< Usually treated as AVMEDIA_TYPE_DATA AVMEDIA_TYPE_VIDEO, AVMEDIA_TYPE_AUDIO, AVMEDIA_TYPE_DATA, ///< Opaque data information usually continuous AVMEDIA_TYPE_SUBTITLE, AVMEDIA_TYPE_ATTACHMENT, ///< Opaque data information usually sparse AVMEDIA_TYPE_NB }; ``` ### 2.sample_fmt 在FFMPEG中音频采样格式有以下几种: ``` enum AVSampleFormat { AV_SAMPLE_FMT_NONE = -1, AV_SAMPLE_FMT_U8, ///< unsigned 8 bits AV_SAMPLE_FMT_S16, ///< signed 16 bits AV_SAMPLE_FMT_S32, ///< signed 32 bits AV_SAMPLE_FMT_FLT, ///< float AV_SAMPLE_FMT_DBL, ///< double AV_SAMPLE_FMT_U8P, ///< unsigned 8 bits, planar AV_SAMPLE_FMT_S16P, ///< signed 16 bits, planar AV_SAMPLE_FMT_S32P, ///< signed 32 bits, planar AV_SAMPLE_FMT_FLTP, ///< float, planar AV_SAMPLE_FMT_DBLP, ///< double, planar AV_SAMPLE_FMT_NB ///< Number of sample formats. DO NOT USE if linking dynamically }; ``` ### 3.profile 在FFMPEG中型有以下几种,可以看出AAC,MPEG2,H.264,VC-1,MPEG4都有型的概念。 ``` #define FF_PROFILE_UNKNOWN -99 #define FF_PROFILE_RESERVED -100 #define FF_PROFILE_AAC_MAIN 0 #define FF_PROFILE_AAC_LOW 1 #define FF_PROFILE_AAC_SSR 2 #define FF_PROFILE_AAC_LTP 3 #define FF_PROFILE_AAC_HE 4 #define FF_PROFILE_AAC_HE_V2 28 #define FF_PROFILE_AAC_LD 22 #define FF_PROFILE_AAC_ELD 38 #define FF_PROFILE_DTS 20 #define FF_PROFILE_DTS_ES 30 #define FF_PROFILE_DTS_96_24 40 #define FF_PROFILE_DTS_HD_HRA 50 #define FF_PROFILE_DTS_HD_MA 60 #define FF_PROFILE_MPEG2_422 0 #define FF_PROFILE_MPEG2_HIGH 1 #define FF_PROFILE_MPEG2_SS 2 #define FF_PROFILE_MPEG2_SNR_SCALABLE 3 #define FF_PROFILE_MPEG2_MAIN 4 #define FF_PROFILE_MPEG2_SIMPLE 5 #define FF_PROFILE_H264_CONSTRAINED (1<<9) // 8+1; constraint_set1_flag #define FF_PROFILE_H264_INTRA (1<<11) // 8+3; constraint_set3_flag #define FF_PROFILE_H264_BASELINE 66 #define FF_PROFILE_H264_CONSTRAINED_BASELINE (66|FF_PROFILE_H264_CONSTRAINED) #define FF_PROFILE_H264_MAIN 77 #define FF_PROFILE_H264_EXTENDED 88 #define FF_PROFILE_H264_HIGH 100 #define FF_PROFILE_H264_HIGH_10 110 #define FF_PROFILE_H264_HIGH_10_INTRA (110|FF_PROFILE_H264_INTRA) #define FF_PROFILE_H264_HIGH_422 122 #define FF_PROFILE_H264_HIGH_422_INTRA (122|FF_PROFILE_H264_INTRA) #define FF_PROFILE_H264_HIGH_444 144 #define FF_PROFILE_H264_HIGH_444_PREDICTIVE 244 #define FF_PROFILE_H264_HIGH_444_INTRA (244|FF_PROFILE_H264_INTRA) #define FF_PROFILE_H264_CAVLC_444 44 #define FF_PROFILE_VC1_SIMPLE 0 #define FF_PROFILE_VC1_MAIN 1 #define FF_PROFILE_VC1_COMPLEX 2 #define FF_PROFILE_VC1_ADVANCED 3 #define FF_PROFILE_MPEG4_SIMPLE 0 #define FF_PROFILE_MPEG4_SIMPLE_SCALABLE 1 #define FF_PROFILE_MPEG4_CORE 2 #define FF_PROFILE_MPEG4_MAIN 3 #define FF_PROFILE_MPEG4_N_BIT 4 #define FF_PROFILE_MPEG4_SCALABLE_TEXTURE 5 #define FF_PROFILE_MPEG4_SIMPLE_FACE_ANIMATION 6 #define FF_PROFILE_MPEG4_BASIC_ANIMATED_TEXTURE 7 #define FF_PROFILE_MPEG4_HYBRID 8 #define FF_PROFILE_MPEG4_ADVANCED_REAL_TIME 9 #define FF_PROFILE_MPEG4_CORE_SCALABLE 10 #define FF_PROFILE_MPEG4_ADVANCED_CODING 11 #define FF_PROFILE_MPEG4_ADVANCED_CORE 12 #define FF_PROFILE_MPEG4_ADVANCED_SCALABLE_TEXTURE 13 #define FF_PROFILE_MPEG4_SIMPLE_STUDIO 14 #define FF_PROFILE_MPEG4_ADVANCED_SIMPLE 15 ``` ================================================ FILE: FFmpeg 结构体学习(四): AVFrame 分析.md ================================================ AVFrame是包含码流参数较多的结构体。下面我们来分析一下该结构体里重要变量的含义和作用。 # 一、源码整理 首先我们先看一下结构体AVFrame的定义的结构体源码(位于libavcodec/avcodec.h): ``` /* *雷霄骅 *leixiaohua1020@126.com *中国传媒大学/数字电视技术 */ /** * Audio Video Frame. * New fields can be added to the end of AVFRAME with minor version * bumps. Similarly fields that are marked as to be only accessed by * av_opt_ptr() can be reordered. This allows 2 forks to add fields * without breaking compatibility with each other. * Removal, reordering and changes in the remaining cases require * a major version bump. * sizeof(AVFrame) must not be used outside libavcodec. */ typedef struct AVFrame { #define AV_NUM_DATA_POINTERS 8 /**图像数据 * pointer to the picture/channel planes. * This might be different from the first allocated byte * - encoding: Set by user * - decoding: set by AVCodecContext.get_buffer() */ uint8_t *data[AV_NUM_DATA_POINTERS]; /** * Size, in bytes, of the data for each picture/channel plane. * * For audio, only linesize[0] may be set. For planar audio, each channel * plane must be the same size. * * - encoding: Set by user * - decoding: set by AVCodecContext.get_buffer() */ int linesize[AV_NUM_DATA_POINTERS]; /** * pointers to the data planes/channels. * * For video, this should simply point to data[]. * * For planar audio, each channel has a separate data pointer, and * linesize[0] contains the size of each channel buffer. * For packed audio, there is just one data pointer, and linesize[0] * contains the total size of the buffer for all channels. * * Note: Both data and extended_data will always be set by get_buffer(), * but for planar audio with more channels that can fit in data, * extended_data must be used by the decoder in order to access all * channels. * * encoding: unused * decoding: set by AVCodecContext.get_buffer() */ uint8_t **extended_data; /**宽高 * width and height of the video frame * - encoding: unused * - decoding: Read by user. */ int width, height; /** * number of audio samples (per channel) described by this frame * - encoding: Set by user * - decoding: Set by libavcodec */ int nb_samples; /** * format of the frame, -1 if unknown or unset * Values correspond to enum AVPixelFormat for video frames, * enum AVSampleFormat for audio) * - encoding: unused * - decoding: Read by user. */ int format; /**是否是关键帧 * 1 -> keyframe, 0-> not * - encoding: Set by libavcodec. * - decoding: Set by libavcodec. */ int key_frame; /**帧类型(I,B,P) * Picture type of the frame, see ?_TYPE below. * - encoding: Set by libavcodec. for coded_picture (and set by user for input). * - decoding: Set by libavcodec. */ enum AVPictureType pict_type; /** * pointer to the first allocated byte of the picture. Can be used in get_buffer/release_buffer. * This isn't used by libavcodec unless the default get/release_buffer() is used. * - encoding: * - decoding: */ uint8_t *base[AV_NUM_DATA_POINTERS]; /** * sample aspect ratio for the video frame, 0/1 if unknown/unspecified * - encoding: unused * - decoding: Read by user. */ AVRational sample_aspect_ratio; /** * presentation timestamp in time_base units (time when frame should be shown to user) * If AV_NOPTS_VALUE then frame_rate = 1/time_base will be assumed. * - encoding: MUST be set by user. * - decoding: Set by libavcodec. */ int64_t pts; /** * reordered pts from the last AVPacket that has been input into the decoder * - encoding: unused * - decoding: Read by user. */ int64_t pkt_pts; /** * dts from the last AVPacket that has been input into the decoder * - encoding: unused * - decoding: Read by user. */ int64_t pkt_dts; /** * picture number in bitstream order * - encoding: set by * - decoding: Set by libavcodec. */ int coded_picture_number; /** * picture number in display order * - encoding: set by * - decoding: Set by libavcodec. */ int display_picture_number; /** * quality (between 1 (good) and FF_LAMBDA_MAX (bad)) * - encoding: Set by libavcodec. for coded_picture (and set by user for input). * - decoding: Set by libavcodec. */ int quality; /** * is this picture used as reference * The values for this are the same as the MpegEncContext.picture_structure * variable, that is 1->top field, 2->bottom field, 3->frame/both fields. * Set to 4 for delayed, non-reference frames. * - encoding: unused * - decoding: Set by libavcodec. (before get_buffer() call)). */ int reference; /**QP表 * QP table * - encoding: unused * - decoding: Set by libavcodec. */ int8_t *qscale_table; /** * QP store stride * - encoding: unused * - decoding: Set by libavcodec. */ int qstride; /** * */ int qscale_type; /**跳过宏块表 * mbskip_table[mb]>=1 if MB didn't change * stride= mb_width = (width+15)>>4 * - encoding: unused * - decoding: Set by libavcodec. */ uint8_t *mbskip_table; /**运动矢量表 * motion vector table * @code * example: * int mv_sample_log2= 4 - motion_subsample_log2; * int mb_width= (width+15)>>4; * int mv_stride= (mb_width << mv_sample_log2) + 1; * motion_val[direction][x + y*mv_stride][0->mv_x, 1->mv_y]; * @endcode * - encoding: Set by user. * - decoding: Set by libavcodec. */ int16_t (*motion_val[2])[2]; /**宏块类型表 * macroblock type table * mb_type_base + mb_width + 2 * - encoding: Set by user. * - decoding: Set by libavcodec. */ uint32_t *mb_type; /**DCT系数 * DCT coefficients * - encoding: unused * - decoding: Set by libavcodec. */ short *dct_coeff; /**参考帧列表 * motion reference frame index * the order in which these are stored can depend on the codec. * - encoding: Set by user. * - decoding: Set by libavcodec. */ int8_t *ref_index[2]; /** * for some private data of the user * - encoding: unused * - decoding: Set by user. */ void *opaque; /** * error * - encoding: Set by libavcodec. if flags&CODEC_FLAG_PSNR. * - decoding: unused */ uint64_t error[AV_NUM_DATA_POINTERS]; /** * type of the buffer (to keep track of who has to deallocate data[*]) * - encoding: Set by the one who allocates it. * - decoding: Set by the one who allocates it. * Note: User allocated (direct rendering) & internal buffers cannot coexist currently. */ int type; /** * When decoding, this signals how much the picture must be delayed. * extra_delay = repeat_pict / (2*fps) * - encoding: unused * - decoding: Set by libavcodec. */ int repeat_pict; /** * The content of the picture is interlaced. * - encoding: Set by user. * - decoding: Set by libavcodec. (default 0) */ int interlaced_frame; /** * If the content is interlaced, is top field displayed first. * - encoding: Set by user. * - decoding: Set by libavcodec. */ int top_field_first; /** * Tell user application that palette has changed from previous frame. * - encoding: ??? (no palette-enabled encoder yet) * - decoding: Set by libavcodec. (default 0). */ int palette_has_changed; /** * codec suggestion on buffer type if != 0 * - encoding: unused * - decoding: Set by libavcodec. (before get_buffer() call)). */ int buffer_hints; /** * Pan scan. * - encoding: Set by user. * - decoding: Set by libavcodec. */ AVPanScan *pan_scan; /** * reordered opaque 64bit (generally an integer or a double precision float * PTS but can be anything). * The user sets AVCodecContext.reordered_opaque to represent the input at * that time, * the decoder reorders values as needed and sets AVFrame.reordered_opaque * to exactly one of the values provided by the user through AVCodecContext.reordered_opaque * @deprecated in favor of pkt_pts * - encoding: unused * - decoding: Read by user. */ int64_t reordered_opaque; /** * hardware accelerator private data (FFmpeg-allocated) * - encoding: unused * - decoding: Set by libavcodec */ void *hwaccel_picture_private; /** * the AVCodecContext which ff_thread_get_buffer() was last called on * - encoding: Set by libavcodec. * - decoding: Set by libavcodec. */ struct AVCodecContext *owner; /** * used by multithreading to store frame-specific info * - encoding: Set by libavcodec. * - decoding: Set by libavcodec. */ void *thread_opaque; /** * log2 of the size of the block which a single vector in motion_val represents: * (4->16x16, 3->8x8, 2-> 4x4, 1-> 2x2) * - encoding: unused * - decoding: Set by libavcodec. */ uint8_t motion_subsample_log2; /**(音频)采样率 * Sample rate of the audio data. * * - encoding: unused * - decoding: read by user */ int sample_rate; /** * Channel layout of the audio data. * * - encoding: unused * - decoding: read by user. */ uint64_t channel_layout; /** * frame timestamp estimated using various heuristics, in stream time base * Code outside libavcodec should access this field using: * av_frame_get_best_effort_timestamp(frame) * - encoding: unused * - decoding: set by libavcodec, read by user. */ int64_t best_effort_timestamp; /** * reordered pos from the last AVPacket that has been input into the decoder * Code outside libavcodec should access this field using: * av_frame_get_pkt_pos(frame) * - encoding: unused * - decoding: Read by user. */ int64_t pkt_pos; /** * duration of the corresponding packet, expressed in * AVStream->time_base units, 0 if unknown. * Code outside libavcodec should access this field using: * av_frame_get_pkt_duration(frame) * - encoding: unused * - decoding: Read by user. */ int64_t pkt_duration; /** * metadata. * Code outside libavcodec should access this field using: * av_frame_get_metadata(frame) * - encoding: Set by user. * - decoding: Set by libavcodec. */ AVDictionary *metadata; /** * decode error flags of the frame, set to a combination of * FF_DECODE_ERROR_xxx flags if the decoder produced a frame, but there * were errors during the decoding. * Code outside libavcodec should access this field using: * av_frame_get_decode_error_flags(frame) * - encoding: unused * - decoding: set by libavcodec, read by user. */ int decode_error_flags; #define FF_DECODE_ERROR_INVALID_BITSTREAM 1 #define FF_DECODE_ERROR_MISSING_REFERENCE 2 /** * number of audio channels, only used for audio. * Code outside libavcodec should access this field using: * av_frame_get_channels(frame) * - encoding: unused * - decoding: Read by user. */ int64_t channels; } AVFrame; ``` # 二、AVFrame 重点字段 AVFrame结构体一般用于存储原始数据(即非压缩数据,例如对视频来说是YUV,RGB,对音频来说是PCM),此外还包含了一些相关的信息。比如说,解码的时候存储了宏块类型表,QP表,运动矢量表等数据。编码的时候也存储了相关的数据。因此在使用FFMPEG进行码流分析的时候,AVFrame是一个很重要的结构体。 下面看几个主要变量的作用(在这里考虑解码的情况): ``` uint8_t *data[AV_NUM_DATA_POINTERS]:解码后原始数据(对视频来说是YUV,RGB,对音频来说是PCM) int linesize[AV_NUM_DATA_POINTERS]:data中“一行”数据的大小。注意:未必等于图像的宽,一般大于图像的宽。 int width, height:视频帧宽和高(1920x1080,1280x720...) int nb_samples:音频的一个AVFrame中可能包含多个音频帧,在此标记包含了几个 int format:解码后原始数据类型(YUV420,YUV422,RGB24...) int key_frame:是否是关键帧 enum AVPictureType pict_type:帧类型(I,B,P...) AVRational sample_aspect_ratio:宽高比(16:9,4:3...) int64_t pts:显示时间戳 int coded_picture_number:编码帧序号 int display_picture_number:显示帧序号 int8_t *qscale_table:QP表 uint8_t *mbskip_table:跳过宏块表 int16_t (*motion_val[2])[2]:运动矢量表 uint32_t *mb_type:宏块类型表 short *dct_coeff:DCT系数,这个没有提取过 int8_t *ref_index[2]:运动估计参考帧列表(貌似H.264这种比较新的标准才会涉及到多参考帧) int interlaced_frame:是否是隔行扫描 uint8_t motion_subsample_log2:一个宏块中的运动矢量采样个数,取log的 ``` 其他的变量不再一一列举,源代码中都有详细的说明。在这里重点分析一下几个需要一定的理解的变量: ### 1.data[] 对于packed格式的数据(例如RGB24),会存到data[0]里面。 对于planar格式的数据(例如YUV420P),则会分开成data[0],data[1],data[2]...(YUV420P中data[0]存Y,data[1]存U,data[2]存V) ### 2.pict_type 包含以下类型: ``` enum AVPictureType { AV_PICTURE_TYPE_NONE = 0, ///< Undefined AV_PICTURE_TYPE_I, ///< Intra AV_PICTURE_TYPE_P, ///< Predicted AV_PICTURE_TYPE_B, ///< Bi-dir predicted AV_PICTURE_TYPE_S, ///< S(GMC)-VOP MPEG4 AV_PICTURE_TYPE_SI, ///< Switching Intra AV_PICTURE_TYPE_SP, ///< Switching Predicted AV_PICTURE_TYPE_BI, ///< BI type }; ``` ### 3.sample_aspect_ratio 宽高比是一个分数,FFMPEG中用AVRational表达分数: ``` /** * rational number numerator/denominator */ typedef struct AVRational{ int num; ///< numerator int den; ///< denominator } AVRational; ``` ### 4.qscale_table QP表指向一块内存,里面存储的是每个宏块的QP值。宏块的标号是从左往右,一行一行的来的。每个宏块对应1个QP。 qscale_table[0]就是第1行第1列宏块的QP值;qscale_table[1]就是第1行第2列宏块的QP值;qscale_table[2]就是第1行第3列宏块的QP值。以此类推... 宏块的个数用下式计算: 注:宏块大小是16x16的。 每行宏块数: ``` int mb_stride = pCodecCtx->width/16+1 ``` 宏块的总数: ``` int mb_sum = ((pCodecCtx->height+15)>>4)*(pCodecCtx->width/16+1) ``` ================================================ FILE: FFmpeg源码分析:内存管理系统.md ================================================ FFmpeg有专门的内存管理系统,包括:内存分配、内存拷贝、内存释放。其中内存分配包含分配内存与对齐、内存分配与清零、分配指定大小的内存块、重新分配内存块、快速分配内存、分配指定最大值的内存、分配数组内存、快速分配数组内存、重新分配数组内存。 FFmpeg的内存管理位于libavutil/mem.c,相关函数如下图所示: ![img](https://img-blog.csdnimg.cn/ec570c9e0f8f49fdb21ec2819acee3d9.png?x-oss-process=image/watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBA5b6Q56aP6K6wNDU2,size_20,color_FFFFFF,t_70,g_se,x_16) 一、内存分配 1、av_malloc av_malloc()内存分配,并且内存对齐,方便系统快速访问内存。代码如下: ```c void *av_malloc(size_t size) { void *ptr = NULL; if (size > max_alloc_size) return NULL; #if HAVE_POSIX_MEMALIGN if (size) if (posix_memalign(&ptr, ALIGN, size)) ptr = NULL; #elif HAVE_ALIGNED_MALLOC ptr = _aligned_malloc(size, ALIGN); #elif HAVE_MEMALIGN #ifndef __DJGPP__ ptr = memalign(ALIGN, size); #else ptr = memalign(size, ALIGN); #endif /* Why 64? * Indeed, we should align it: * on 4 for 386 * on 16 for 486 * on 32 for 586, PPro - K6-III * on 64 for K7 (maybe for P3 too). * Because L1 and L2 caches are aligned on those values. * But I don't want to code such logic here! */ /* Why 32? * For AVX ASM. SSE / NEON needs only 16. * Why not larger? Because I did not see a difference in benchmarks ... */ /* benchmarks with P3 * memalign(64) + 1 3071, 3051, 3032 * memalign(64) + 2 3051, 3032, 3041 * memalign(64) + 4 2911, 2896, 2915 * memalign(64) + 8 2545, 2554, 2550 * memalign(64) + 16 2543, 2572, 2563 * memalign(64) + 32 2546, 2545, 2571 * memalign(64) + 64 2570, 2533, 2558 * * BTW, malloc seems to do 8-byte alignment by default here. */ #else ptr = malloc(size); #endif if(!ptr && !size) { size = 1; ptr= av_malloc(1); } #if CONFIG_MEMORY_POISONING if (ptr) memset(ptr, FF_MEMORY_POISON, size); #endif return ptr; }2、av_mallocz av_mallocz()是在av_malloc()基础上,调用memset()进行内存清零: ``` ```c void *av_mallocz(size_t size) { void *ptr = av_malloc(size); if (ptr) memset(ptr, 0, size); return ptr; } ``` 3、av_malloc_array av_malloc_array()先计算数组所需要内存块大小,然后用av_malloc()分配数组内存: ```c void *av_malloc_array(size_t nmemb, size_t size) { size_t result; if (av_size_mult(nmemb, size, &result) < 0) return NULL; return av_malloc(result); } ``` 4、av_mallocz_array av_mallocz_array()先计算数组所需要内存块大小,然后用av_mallocz()分配数组内存: ```c void *av_mallocz_array(size_t nmemb, size_t size) { size_t result; if (av_size_mult(nmemb, size, &result) < 0) return NULL; return av_mallocz(result); } ``` 5、av_calloc av_calloc()操作与av_mallocz_array(),先计算内存大小再用av_mallocz()分配内存: ```c void *av_calloc(size_t nmemb, size_t size) { size_t result; if (av_size_mult(nmemb, size, &result) < 0) return NULL; return av_mallocz(result); } ``` 6、av_max_alloc av_max_alloc()主要是指定分配内存的最大值: ```c static size_t max_alloc_size= INT_MAX; void av_max_alloc(size_t max) { max_alloc_size = max; } ``` 在av_malloc()用于判断size是否超出最大值: ```c void *av_malloc(size_t size) { void *ptr = NULL; if (size > max_alloc_size) return NULL; ...... }7、av_realloc av_realloc()是对系统的realloc函数进行封装,重新分配内存块: ``` ```c void *av_realloc(void *ptr, size_t size) { if (size > max_alloc_size) return NULL; #if HAVE_ALIGNED_MALLOC return _aligned_realloc(ptr, size + !size, ALIGN); #else return realloc(ptr, size + !size); #endif } ``` 8、av_realloc_array av_realloc_array()先计算内存块大小,然后用av_realloc()重新分配数组内存: ```c void *av_realloc_array(void *ptr, size_t nmemb, size_t size) { size_t result; if (av_size_mult(nmemb, size, &result) < 0) return NULL; return av_realloc(ptr, result); } ``` 9、av_fast_realloc av_fast_realloc()快速重新分配内存,如果原始内存块足够大直接复用: ```c void *av_fast_realloc(void *ptr, unsigned int *size, size_t min_size) { if (min_size <= *size) return ptr; if (min_size > max_alloc_size) { *size = 0; return NULL; } min_size = FFMIN(max_alloc_size, FFMAX(min_size + min_size / 16 + 32, min_size)); ptr = av_realloc(ptr, min_size); /* we could set this to the unmodified min_size but this is safer * if the user lost the ptr and uses NULL now */ if (!ptr) min_size = 0; *size = min_size; return ptr; } ``` 10、av_fast_malloc av_fast_malloc()快速分配内存: ```c void av_fast_malloc(void *ptr, unsigned int *size, size_t min_size) { ff_fast_malloc(ptr, size, min_size, 0); } ``` 其中ff_fast_malloc()代码位于libavutil/mem_internal.h: ```c static inline int ff_fast_malloc(void *ptr, unsigned int *size, size_t min_size, int zero_realloc) { void *val; memcpy(&val, ptr, sizeof(val)); if (min_size <= *size) { av_assert0(val || !min_size); return 0; } min_size = FFMAX(min_size + min_size / 16 + 32, min_size); av_freep(ptr); val = zero_realloc ? av_mallocz(min_size) : av_malloc(min_size); memcpy(ptr, &val, sizeof(val)); if (!val) min_size = 0; *size = min_size; return 1; } ``` 11、av_fast_mallocz av_fast_mallocz()快速分配内存,并且内存清零: ```c void av_fast_mallocz(void *ptr, unsigned int *size, size_t min_size) { ff_fast_malloc(ptr, size, min_size, 1); } ``` 二、内存拷贝 1、av_strdup av_strdup()用于重新分配内存与拷贝字符串: ```c char *av_strdup(const char *s) { char *ptr = NULL; if (s) { size_t len = strlen(s) + 1; ptr = av_realloc(NULL, len); if (ptr) memcpy(ptr, s, len); } return ptr; } ``` 2、av_strndup av_strndup()用于分配指定大小内存与拷贝字符串,先用memchr()获取有效字符串长度,然后使用av_realloc()重新分配内存,再用memcpy()拷贝字符串: ```c char *av_strndup(const char *s, size_t len) { char *ret = NULL, *end; if (!s) return NULL; end = memchr(s, 0, len); if (end) len = end - s; ret = av_realloc(NULL, len + 1); if (!ret) return NULL; memcpy(ret, s, len); ret[len] = 0; return ret; } ``` 3、av_memdup av_memdup()用于内存分配与内存拷贝,先用av_malloc()分配内存,再用memcpy()拷贝内存: ```c void *av_memdup(const void *p, size_t size) { void *ptr = NULL; if (p) { ptr = av_malloc(size); if (ptr) memcpy(ptr, p, size); } return ptr; } ``` 4、av_memcpy_backptr av_memcpy_backptr()用于内存拷贝,与系统提供的memcpy()类似,并且考虑16位、24位、32位内存对齐: ```c void av_memcpy_backptr(uint8_t *dst, int back, int cnt) { const uint8_t *src = &dst[-back]; if (!back) return; if (back == 1) { memset(dst, *src, cnt); } else if (back == 2) { fill16(dst, cnt); } else if (back == 3) { fill24(dst, cnt); } else if (back == 4) { fill32(dst, cnt); } else { if (cnt >= 16) { int blocklen = back; while (cnt > blocklen) { memcpy(dst, src, blocklen); dst += blocklen; cnt -= blocklen; blocklen <<= 1; } memcpy(dst, src, cnt); return; } if (cnt >= 8) { AV_COPY32U(dst, src); AV_COPY32U(dst + 4, src + 4); src += 8; dst += 8; cnt -= 8; } if (cnt >= 4) { AV_COPY32U(dst, src); src += 4; dst += 4; cnt -= 4; } if (cnt >= 2) { AV_COPY16U(dst, src); src += 2; dst += 2; cnt -= 2; } if (cnt) *dst = *src; } ``` } 三、内存释放 1、av_free av_free()用于释放内存块,主要是调用系统free()进行释放。如果宏定义了对齐分配,那么要对齐释放: ```c void av_free(void *ptr) { #if HAVE_ALIGNED_MALLOC _aligned_free(ptr); #else free(ptr); #endif } ``` 2、av_freep av_freep()用于释放内存指针,先备份内存指针,然后把指针地址清空,再释放内存: ```c void av_freep(void *arg) { void *val; memcpy(&val, arg, sizeof(val)); memcpy(arg, &(void *){ NULL }, sizeof(val)); av_free(val); } ``` ================================================ FILE: Linux上的ffmpeg完全使用指南.md ================================================ **[ffmpeg](https://ffmpeg.org/)** 是一个处理媒体文件的命令行工具 (command line based) 。它是一个拥有非常多功能的框架,并且因为他是开源的,很多知名的工具如 VLC,YouTube, iTunes 等等,都是再其之上开发出来的。 ffmpeg最吸引我的地方就是它可以用非常简练的方式(通过一两个命令)完成许多的处理任务,当然,作为一个强大的工具,他也有很多较为复杂的使用方式,有些时候甚至可以代替一个完整的视频处理流程。 在这个ffmpeg教程中,我会告诉你如何安装ffmpeg,以及各种使用方法,也会讲解一些复杂的功能。 我会详细的说明各个方面,这样即便你是linux新手也能明白。 我使用的linux是 **Ubuntu 18.04**, 不过下面的命令应该可以在其他的linux发行版中同样适用。 ![img](https://eyehere.net/wp-content/uploads/2019/04/image.png) ## 在 Ubuntu 和其他 Linux 系统上安装 ffmpeg 安装 **ffmpeg** 是非常容易的,它是个很流行的程序,所以大多数的linux发行版中您都可以通过包管理器直接安装。 ### 在 Ubuntu 上安装 ffmpeg 在 Ubuntu 上,ffmpeg 存在于 “Universe repository”, 所以确保您开启了enable universe repository,然后更新并安装ffmpeg。下面就是您可能需要的命令。 ```c sudo add-apt-repository universe sudo apt update sudo apt install ffmpeg ``` 这就OK了,您可以通过下面的命令尝试一下有没有正确安装: ```c ffmpeg ``` 他会打印出一些ffmpeg的配置和版本信息。 ![img](https://eyehere.net/wp-content/uploads/2019/04/image-1.png) 正如上图所示,安装的版本是 3.4.4。不过ffmpeg的最新版本应该是4.1。为了安装4.x的版本,您需要使用ffmpeg ppa, 您可以自己研究一下…… ### 在 Arch 系的Linux上安装 ffmpeg 这个也非常简单,用下面的命令就行: ```c sudo pacman -S ffmpeg ``` ### 在 Fedora 系的Linux上安装 ffmpeg 使用下面的命令就好了: ```c sudo dnf install ffmpeg ``` ## 如何使用 ffmpeg: 基础 **ffmpeg** 安装就绪了,我来讲述一些使用这个强力工具的基本概念。 ### 0. ffmpeg 命令 使用 **ffmpeg 命令** 的**基本形式**是: ```c ffmpeg [全局参数] {[输入文件参数] -i 输入文件地址} ... {[输出文件参数] 输出文件地址} ... ``` 要注意的是,所有的参数仅仅对仅接下来的文件有效(下一个文件得把参数再写一遍)。 所有没有使用 **-i** 指定的文件都被认为是输出文件。 **Ffmpeg** 可以接受多个输入文件并输出到您指定的位置。你也可以将输入输出都指定为同一个文件名,不过这个时候要在输出文件前使用用 **-y** 标记。 Note *你不应该将输入和输出混淆,先指定输入,再指定输出文件* ### 1. 获得媒体文件的信息 **ffmpeg** 最简单的使用就是用来 **显示文件信息** 。不用给输出,只是简单的写: ```c ffmpeg -i file_name ``` 视频和音频文件都可以使用: ```c ffmpeg -i video_file.mp4 ffmpeg -i audio_file.mp3 ``` ![Display information about a media file with ffmpeg](https://eyehere.net/wp-content/uploads/2019/04/image-2.png) 通过ffmpeg查看文件属性 命令会输出很多与您文件无关的信息(ffmpeg本身的信息),虽说这个蛮有用的,你可以使用 **-hide_banner** 来隐藏掉它们: ```c ffmpeg -i video_file.mp4 -hide_banner ffmpeg -i audio_file.mp3 -hide_banner ``` ![img](https://eyehere.net/wp-content/uploads/2019/04/image-3.png) 如图所示,现在命令只显示你文件相关的信息了(编码器,数据流等)。 ### 2. 转换媒体文件 **ffmpeg** 最让人称道常用的恐怕就是你轻而易举的在不同媒体格式之间进行自由转换了。你是要指明输入和输出文件名就行了, **ffmpeg** 会从后缀名猜测格式,这个方法同时适用于视频和音频文件 下面是一些例子: ```c ffmpeg -i video_input.mp4 video_output.avi ffmpeg -i video_input.webm video_output.flv ffmpeg -i audio_input.mp3 audio_output.ogg ffmpeg -i audio_input.wav audio_output.flac ``` 你也可以同时指定多个输出后缀: ```c ffmpeg -i audio_input.wav audio_output_1.mp3 audio_output_2.ogg ``` 这样会同时输出多个文件. 想看支持的格式,可以用: ```c ffmpeg -formats ``` 同样的,你可以使用 **-hide_banner** 来省略一些程序信息。 你可以在输出文件前使用 **-qscale 0** 来保留原始的视频质量: ```c ffmpeg -i video_input.wav -qscale 0 video_output.mp4 ``` 进一步,你可以指定编码器,使用 **-c:a** (音频) 和 **g-c:v** (视频) 来指定编码器名称,或者写 **copy** 来使用与源文件相同的编码器: ```c ffmpeg -i video_input.mp4 -c:v copy -c:a libvorbis video_output.avi ``` **Note:** *这样做会让文件后缀使人困惑,所以请避免这么做。* ### 3. 从视频中抽取音频 为了从视频文件中抽取音频,直接加一个 **-vn** 参数就可以了: ```c ffmpeg -i video.mp4 -vn audio.mp3 ``` 这会让命令复用原有文件的比特率,一般来说,使用 **-ab** (音频比特率)来指定编码比特率是比较好的: ```c ffmpeg -i video.mp4 -vn -ab 128k audio.mp3 ``` 一些常见的比特率有 96k, 128k, 192k, 256k, 320k (mp3也可以使用最高的比特率)。 其他的一些常用的参数比如 **-ar** **(采样率**: 22050, 441000, 48000), **-ac** (**声道数**), **-f** (**音频格式**, 通常会自动识别的). **-ab** 也可以使用 **-b:a** 来替代. 比如: ```c ffmpeg -i video.mov -vn -ar 44100 -ac 2 -b:a 128k -f mp3 audio.mp3 ``` ### 4. 让视频静音 和之前的要求类似,我们可以使用 **-an** 来获得纯视频(之前是 **-vn**). ```c ffmpeg -i video_input.mp4 -an -video_output.mp4 ``` **Note:** *这个 **-an** 标记会让所有的音频参数无效,因为最后没有音频会产生。* ### 5. 从视频中提取图片 这个功能可能对很多人都挺有用,比如你可能有一些幻灯片,你想从里面提取所有的图片,那么下面这个命令就能帮你: ```c ffmpeg -i video.mp4 -r 1 -f image2 image-%3d.png ``` 我们来解释一下这个命令: **-r** 代表了帧率(一秒内导出多少张图像,默认25), **-f** 代表了输出格式(**image2** 实际上上 image2 序列的意思)。 最后一个参数 (输出文件) 有一个有趣的命名:它使用 **%3d** 来指示输出的图片有三位数字 (000, 001, 等等.)。你也可以用 **%2d** (两位数字) 或者 **%4d** (4位数字) ,只要你愿意,你可以随便实验 一下可以怎么写! **Note:** *同样也有将图片转变为视频/幻灯片的方式,下面的**高级应用**中会讲到。* ### 6. 更改视频分辨率或长宽比 对 **ffmpeg** 来说又是个简单的任务,你只需要使用 **-s** 参数来缩放视频就行了: ```c ffmpeg -i video_input.mov -s 1024x576 video_output.mp4 ``` 同时,你可能需要使用 **-c:a** 来保证音频编码是正确的: ```c ffmpeg -i video_input.h264 -s 640x480 -c:a video_output.mov ``` 你也可是使用**-aspect** 来更改长宽比: ```c ffmpeg -i video_input.mp4 -aspect 4:3 video_output.mp4 ``` **Note:** 在高级应用中还会提到更强大的方法 ### 7. 为音频增加封面图片 有个很棒的方法把音频变成视频,全程使用一张图片(比如专辑封面)。当你想往某个网站上传音频,但那个网站又仅接受视频(比如YouTube, Facebook等)的情况下会非常有用。 下面是例子: ```c ffmpeg -loop 1 -i image.jpg -i audio.wav -c:v libx264 -c:a aac -strict experimental -b:a 192k -shortest output.mp4 ``` 只要改一下编码设置 (**-c:v** 是 视频编码, **-c:a** 是音频编码) 和文件的名称就能用了。 **Note:** *如果你使用一个较新的ffmpeg版本(4.x),你就可以不指定 **-strict experimental*** ### 8. 为视频增加字幕 另一个常见又很容易实现的要求是给视频增加字母,比如一部外文电源,使用下面的命令: ```c ffmpeg -i video.mp4 -i subtitles.srt -c:v copy -c:a copy -preset veryfast -c:s mov_text -map 0 -map 1 output.mp4 ``` 当然,你可以指定自己的编码器和任何其他的音频视频参数。 ### 9. 压缩媒体文件 压缩文件可以极大减少文件的体积,节约存储空间,这对于文件传输尤为重要。通过ffmepg,有好几个方法来压缩文件体积。 **Note:** 文件压缩的太厉害会让文件质量显著降低。 首先,对于音频文件,可以通过降低比特率(使用 **-b:a** 或 **-ab**): ```c ffmpeg -i audio_input.mp3 -ab 128k audio_output.mp3 ffmpeg -i audio_input.mp3 -b:a 192k audio_output.mp3 ``` 再次重申,一些常用的比特率有: 96k, 112k, 128k, 160k, 192k, 256k, 320k.值越大,文件所需要的体积就越大。 对于视频文件,选项就多了,一个简单的方法是通过降低视频比特率 (通过 **-b:v**): ```c ffmpeg -i video_input.mp4 -b:v 1000k -bufsize 1000k video_output.mp4 ``` **Note:** 视频的比特率和音频是不同的(一般要大得多)。 你也可以使用 **-crf** 参数 (恒定质量因子). 较小的**crf** 意味着较大的码率。同时使用 **libx264** 编码器也有助于减小文件体积。这里有个例子,压缩的不错,质量也不会显著变化: ```c ffmpeg -i video_input.mp4 -c:v libx264 -crf 28 video_output.mp4 ``` **crf** 设置为20 到 30 是最常见的,不过您也可以尝试一些其他的值。 降低帧率在有些情况下也能有效(不过这往往让视频看起来很卡): ```c ffmpeg -i video_input.mp4 -r 24 video_output.mp4 ``` **-r** 指示了帧率 (这里是 **24**)。 你还可以通过压缩音频来降低视频文件的体积,比如设置为立体声或者降低比特率: ```c ffmpeg -i video_input.mp4 -c:v libx264 -ac 2 -c:a aac -strict -2 -b:a 128k -crf 28 video_output.mp4 ``` **Note:** ***-strict -2** 和 **-ac 2** 是来处理立体声部分的。* ### 10. 裁剪媒体文件(基础) 想要从开头开始剪辑一部分,使用T **-t** 参数来指定一个时间: ```c ffmpeg -i input_video.mp4 -t 5 output_video.mp4 ffmpeg -i input_audio.wav -t 00:00:05 output_audio.wav ``` 这个参数对音频和视频都适用,上面两个命令做了类似的事情:保存一段5s的输出文件(文件开头开始算)。上面使用了两种不同的表示时间的方式,一个单纯的数字(描述)或者 **HH:MM:SS** (小时, 分钟, 秒). 第二种方式实际上指示了结束时间。 也可以通过 **-ss** 给出一个开始时间,**-to** 给出结束时间: ```c ffmpeg -i input_audio.mp3 -ss 00:01:14 output_audio.mp3 ffmpeg -i input_audio.wav -ss 00:00:30 -t 10 output_audio.wav ffmpeg -i input_video.h264 -ss 00:01:30 -to 00:01:40 output_video.h264 ffmpeg -i input_audio.ogg -ss 5 output_audio.ogg ``` 可以看到 **开始时间** (**-ss HH:MM:SS**), **持续秒数** (**-t duration**), **结束时间** (**-to HH:MM:SS**), 和**开始秒数** (**-s duration**)的用法. 你可以在媒体文件的任何部分使用这些命令。 ## ffmpeg: 高级使用 现在该开始讲述一些高级的特性了(比如截屏等),让我们开始吧。 ### 1. 分割媒体文件 前面已经讲述了如何裁剪文件,那么如何分割媒体文件呢?只需要为每个输出文件分别指定开始时间、结束或者持续时间就可以了。 看下面这个例子: ```c ffmpeg -i video.mp4 -t 00:00:30 video_1.mp4 -ss 00:00:30 video_2.mp4 ``` 语法很简单,为第一个文件指定了 **-t 00:00:30** 作为持续时间(第一个部分是原始文件的前30秒内容),然后指定接下来的所有内容作为第二个文件(从第一部分的结束时间开始,也就是 **00:00:30**)。 你可以任意指定多少个部分,尝试一下吧,这个功能真的很厉害,同时它也适用用音频文件。 ### 2. 拼接媒体文件 **ffmpeg** 也可以进行相反的动作:把多个文件合在一起。 为了实现这一点,你得用自己顺手的编辑器来创建一个文本文件。 因为我喜欢使用终端,所以这里我用了 **touch** 和 **vim**. 文件名无关紧要,这里我用 **touch** 命令创建 **video_to_join.txt** 文件: ```c touch videos_to_join.txt ``` 现在,使用 **vim** 编辑它: ```c vim videos_to_join.txt ``` 你可以使用任何你喜欢的工具,比如nano,gedit等等。 在文件内容中, 输入您想拼接的文件的完整路径(文件会按照顺序拼合在一起),一行一个文件。确保他们拥有相同的后缀名。下面是我的例子: ```c /home/ubuntu/Desktop/video_1.mp4 /home/ubuntu/Desktop/video_2.mp4 /home/ubuntu/Desktop/video_3.mp4 ``` 保存这个文件,同样这个方法适用与任何音频或者视频文件。 然后使用下面的命令: ```c ffmpeg -f concat -i join.txt output.mp4 ``` **Note:** *使用的输出文件的名称是 **output.mp4**, 因为我的输入文件都是mp4的 。* 这样,你 **videos_to_join.txt** 里的所有文件都会被拼接成一个独立的文件了。 ### 3. 将图片转变为视频 这会告诉你如何将图片变成幻灯片秀,同时也会告诉你如何加上音频。 首先我建议您将所有的图片放到一个文件夹下面,我把它们放到了 **my_photos** 里,同时图片的后缀名最好是 **.png** 或者 **.jpg**, 不管选那个,他们应该是同一个后缀名,否则ffmpeg可能会工作的不正常,您可以很方便的把 .png 转变为 .jpg (或者倒过来也行)。 我们这次转换的格式 (**-f**) 应该被设置为 **image2pipe**. 你必须使用使用连词符(**–**)来指明输入。 **image2pipe** 允许你使用管道 (在命令间使用 **|**)的结果而不是文件作为ffmpeg的输入。命令结果便是将所有图片的内容逐个输出,还要注意指明视频编码器是 copy (**-c:v copy**) 以正确使用图片输入: ```c cat my_photos/* | ffmpeg -f image2pipe -i - -c:v copy video.mkv ``` 如果你播放这个文件,你可能会觉得只有一部分图片被加入了,事实上所有的图片都在,但是**ffmpeg** 播放它们的时候太快了,默认是23fps,一秒播放了23张图片。 你应该指定帧率 (**-framerate**) : ```c cat my_photos/* | ffmpeg -framerate 1 -f image2pipe -i - -c:v copy video.mkv ``` 在这个例子里,把帧率设置为1,也就是每帧(每张图)会显示1秒。 为了加一些声音,可以使用音频文件作为输入 (**-i audo_file**) 并且设定copy音频编码 (**-c:a copy**). 你可以同时为音频和视频设定编码器,在输出文件前设置就可以了。你要计算一下音频文件的长度和图片张数,已确定合适的帧率。比如我的音频文件是22秒,图片有9张,那么帧率应该是 9 / 22 大约0.4,所以我这么输入命令: ```c cat my_photos/* | ffmpeg -framerate 0.40 -f image2pipe -i - -i audio.wav -c copy video.mkv ``` ### 4. 录制屏幕 通过 **ffmpeg** 录制屏幕同样没有困难的,将格式(**-f**) 设定为**x11grab**. 他就会抓取你的**XSERVER**. 输入的话可以这是屏幕编号(一般都是**0:0**). 抓取是从左上角开始计算的,可以指定屏幕分辨率 (**-s**). 我的屏幕是 **1920×1080**. 注意屏幕分辨率硬在输入之前指定**t**: ```c ffmpeg -f x11grab -s 1920x1080 -i :0.0 output.mp4 ``` 按 **q** 或者 **CTRL+C** 以结束录制屏幕。 **小技巧:**你可以通过命令获得真实的分辨率而不是写死一个固定的大小**:** ```c -s $(xdpyinfo | grep dimensions | awk '{print $2;}') ``` 完整的命令这么写: ```c ffmpeg -f x11grab -s $(xdpyinfo | grep dimensions | awk '{print $2;}') -i :0.0 output.mp4 ``` ### 5. 录制摄像头 从摄像头录制就更简单了,linux上设备都是在/dev中的,比如 **/dev/video0, /dev/video1, etc.**: ```c ffmpeg -i /dev/video0 output.mkv ``` 同样, **q** 或者 **CTRL+C** 来结束录制。 ### 6. 录制声音 Linux上同时是使用 **ALSA** 和 **pulseaudio** 来处理声音的。 **ffmpeg** 可以录制两者,不过我要特别说明 **pulseaudio**, 因为 Debian 系列的发行版默认用了它。命令如下: 在 **pulseaudio**, 你必须强制指定(**-f**) **alsa** 然后指定 **default** 作为输入**t** (**-i default**): ```c ffmpeg -f alsa -i default output.mp3 ``` **Note:** *在你系统音频设置里,应该能看到默认的录音设备。* 我经常玩吉他,我平时使用一个专业音频设备才能录制声音,当我发现ffmpeg也可以很轻松的录制的时候颇为惊讶。 ### 录制小贴士 对于录制任务来说,通常都需要指定编码器以及帧率,之前讲过的参数当然也可以用到这里来! ```c ffmpeg -i /dev/video0 -f alsa -i default -c:v libx264 -c:a flac -r 30 output.mkv ``` 有时候不直接录音,而是在录屏/录像的时候给一个音频文件,那么可以这么做: ```c ffmpeg -f x11grab -s $(xdpyinfo | grep dimensions | awk '{print $2;}') -i :0.0 -i audio.wav -c:a copy output.mp4 ``` **Note:** ***ffmpeg** 使用片段录取,所有有时候非常短的录制可能不会保存文件。我建议录地可以稍微长一些(然后后期裁剪),已保证录制的文件成功写到磁盘上。* ## ffmpeg中的过滤器的基本使用 **过滤器** 是 **ffmpeg** 中最为强大的功能。在ffmepg中有数不甚数的过滤器存在,可以满足各种编辑需要。因为过滤器实在太多了,这里只会简单讲述几个常用的。 使用 过滤的基本结构是: ```c ffmpeg -i input.mp4 -vf "filter=setting_1=value_1:setting_2=value_2,etc" output.mp4 ffmpeg -i input.wav -af "filter=setting_1=value_1:setting_2=value_2,etc" output.wav ``` 可以指定视频过滤器 (**-vf**, **-filter:v**的简写) 和 音频过滤器 (**-af**, **-filter:a**的简写). 过滤器的内容写到双引号里面 (**“**) 并且可以使用逗号(**,**)连接。你可以使用任意数量的过滤器(我写了个etc代表更多的,这不是做一个真实的过滤器)。 过滤器设定的通常格式是: ```c filter=setting_2=value_2:setting_2=value_2 ``` 过滤器不同的值使用冒号分割。 你甚至可以在值里面使用进行数学符号计算。 **Note:** 参考 **[ffmpeg 过滤器手册](https://ffmpeg.org/ffmpeg-filters.html)**查看更多高级用法 这里举几个例子来说明视频和音频的过滤器。 ### 1. 视频缩放 这是个简单过滤器,设定里只有 **width** 和 **height**: ```c ffmpeg -i input.mp4 -vf "scale=w=800:h=600" output.mp4 ``` 我说过你可以使用数学运算来给值: ```c ffmpeg -i input.mkv -vf "scale=w=1/2*in_w:h=1/2*in_h" output.mkv ``` 很明显,这个命令让输入的尺寸变成了输入尺寸(in_w, in_h)的1/2. ### 2. 视频裁剪 类似缩放,这个设定也有 **width** 和 **height** ,另外可以指定裁剪的原点(默认是视频的中心) ```c ffmpeg -i input.mp4 -vf "crop=w=1280:h=720:x=0:y=0" output.mp4 ffmpeg -i input.mkv -vf "crop=w=400:h=400" output.mkv ``` 第二个命令裁剪原点是视频的中心点(因为我没有给x和y坐标),第一个命令会从左上角开始裁剪 (**x=0:y=0**). 这里也有一个使用数学计算的例子: ```c ffmpeg -i input.mkv -vf "crop=w=3/4*in_w:h=3/4*in_h" output.mkv ``` 这会把视频裁剪剩下原大小的3/4/。 ### 3. 视频旋转 你可以指定一个弧度,顺时针旋转视频。为了让计算简单一些,你可以给角度然后乘以 **PI/180**: ```c ffmpeg -i input.avi -vf "rotate=90*PI/180" ffmpeg -i input.mp4 -vf "rotate=PI" ``` 第一个命令将视频顺时针旋转90°,第二个则是上下颠倒了视频(翻转了180°)。 ### 4. 音频声道重映射 有的时候,你的音频只有右耳可以听到声音,那么这个功能就很有用了。你可以让声音同时在左右声道出现: ```c ffmpeg -i input.mp3 -af "channelmap=1-0|1-1" output.mp3 ``` 这将右声道(1)同时映射到左(0)右(1)两个声道(左边的数字是输入,右边的数字是输出)。 ### 5. 更改音量 你可以将音量大小乘以一个实数(可以是整数也可以不是),你只需要给出那个数大小就行了。 ```c ffmpeg -i input.wav -af "volume=1.5" output.wav ffmpeg -i input.ogg -af "volume=0.75" output.ogg ``` 第一个将音量变为1.5倍,第二个则让音量变成了原来的1/4那么安静。 ### 技巧:更改播放速度 这里会介绍视频(不影响音频)和音频的过滤器。 1. **视频** 视频过滤器是 **setpts** (PTS = presentation time stamp). 这个参数以一种有趣的方式工作,因为我们修改的是PTS,所以较大的数值意味着较慢的播放速度,反之亦然: ```c ffmpeg -i input.mkv -vf "setpts=0.5*PTS" output.mkv ffmpeg -i input.mp4 -vf "setpts=2*PTS" output,mp4 ``` 第一个命令让播放速度加倍了,第二个则是让播放速度降低了一半。 **2. 音频** 这里的过滤器是 **atempo**. 这里有个限制,它只接受 **0.5**(半速) 到 **2** (倍速)之间的值。为了越过这个限制,你可以链式使用这个过滤器: ```c ffmpeg -i input.wav -af "atempo=0.75" output.wav ffmpeg -i input.mp3 -af "atempo=2.0,atempo=2.0" ouutput.mp3 ``` 第一个命令让音频速度慢了1/4,第二个则是加速到原来的4(2*2)倍。 **Note:** *如果想在同一个命令中同时修改视频和音频的速度,你得查看一下 **[filtergraphs](https://ffmpeg.org/ffmpeg-filters.html#Filtergraph-description)**.* **小结** 在这个手册中,我讲述了安装、基本的使用、高级的使用和一些过滤器的基础。 我希望这对于一些尝试使用ffmpeg的人,或者希望使用ffmpeg做很多工作的人来说是个有用的资源,ffmepg真的是个多功能又极其好用的工具。 ================================================ FILE: README.md ================================================ # 💯 2024年,最新 ffmpeg 资料整理,项目(调试可用),命令手册,文章,编解码论文,视频讲解,面试题全套资料


本repo搜集整理全网ffmpeg学习资料。 所有数据来源于互联网。所谓取之于互联网,用之于互联网。 如果涉及版权侵犯,请邮件至 wchao_isvip@163.com ,我们将第一时间处理。 如果您对我们的项目表示赞同与支持,欢迎您 [lssues](https://github.com/0voice/ffmpeg_develop_doc/issues) 我们,或者邮件 wchao_isvip@163.com 我们,更加欢迎您 pull requests 加入我们。 感谢您的支持!

- 目录 - [@ 开源项目](https://github.com/0voice/ffmpeg_develop_doc#-%E5%BC%80%E6%BA%90%E9%A1%B9%E7%9B%AE) - [@ 典藏文档](https://github.com/0voice/ffmpeg_develop_doc#-%E5%85%B8%E8%97%8F%E6%96%87%E6%A1%A3) - [@ 系列文章](https://github.com/0voice/ffmpeg_develop_doc#-%E6%96%87%E7%AB%A0) - [@ 面试题](https://github.com/0voice/ffmpeg_develop_doc#-%E9%9D%A2%E8%AF%95%E9%A2%98) - [@ 教学视频](https://github.com/0voice/ffmpeg_develop_doc#-%E8%A7%86%E9%A2%91) - [@ 学术论文](https://github.com/0voice/ffmpeg_develop_doc#-%E8%AE%BA%E6%96%87) - [@ 资料下载](https://github.com/0voice/ffmpeg_develop_doc#%E8%81%94%E7%B3%BB%E4%B8%93%E6%A0%8F) ## 🏗 开源项目 - [bilibili/ijkplayer](https://github.com/bilibili/ijkplayer): 基于FFmpeg n3.4的Android/iOS视频播放器,支持MediaCodec, VideoToolbox。 - [befovy/fijkplayer](https://github.com/befovy/fijkplayer): ijkplayer for flutter. ijkplayer 的 flutter 封装。 Flutter video/audio player. Flutter media player plugin for android/iOS based on ijkplayer. fijkplayer 是基于 ijkplayer 封装的 flutter 媒体播放器,开箱即用,无需编译 ijkplayer - [mpv-player/mpv](https://github.com/mpv-player/mpv): 命令行视频播放器 - [CarGuo/GSYVideoPlayer](https://github.com/CarGuo/GSYVideoPlayer): 视频播放器(IJKplayer、ExoPlayer、MediaPlayer),HTTPS,支持弹幕,外挂字幕,支持滤镜、水印、gif截图,片头广告、中间广告,多个同时播放,支持基本的拖动,声音、亮度调节,支持边播边缓存,支持视频自带rotation的旋转(90,270之类),重力旋转与手动旋转的同步支持,支持列表播放 ,列表全屏动画,视频加载速度,列表小窗口支持拖动,动画效果,调整比例,多分辨率切换,支持切换播放器,进度条小窗口预览,列表切换详情页面无缝播放,rtsp、concat、mpeg。 - [mpenkov/ffmpeg-tutorial](https://github.com/mpenkov/ffmpeg-tutorial): 教程,演示如何编写一个基于FFmpeg的视频播放器 - [imoreapps/ffmpeg-avplayer-for-ios-tvos](https://github.com/imoreapps/ffmpeg-avplayer-for-ios-tvos): 一个微小但强大的iOS和Apple TV OS的av播放器框架,是基于FFmpeg库。 - [unosquare/ffmediaelement](https://github.com/unosquare/ffmediaelement): FFME:高级WPF MediaElement(基于FFmpeg) - [microshow/RxFFmpeg](https://github.com/microshow/RxFFmpeg):RxFFmpeg 是基于 ( FFmpeg 4.0 + X264 + mp3lame + fdk-aac + opencore-amr + openssl ) 编译的适用于 Android 平台的音视频编辑、视频剪辑的快速处理框架,包含以下功能:视频拼接,转码,压缩,裁剪,片头片尾,分离音视频,变速,添加静态贴纸和gif动态贴纸,添加字幕,添加滤镜,添加背景音乐,加速减速视频,倒放音视频,音频裁剪,变声,混音,图片合成视频,视频解码图片,抖音首页,视频播放器及支持 OpenSSL https 等主流特色功能 - [wang-bin/QtAV](https://github.com/wang-bin/QtAV): 基于Qt和FFmpeg的跨平台多媒体框架,高性能。用户和开发人员友好。支持Android, iOS, Windows商店和桌面。基于Qt和FFmpeg的跨平台高性能音视频播放框架 - [xufuji456/FFmpegAndroid](https://github.com/xufuji456/FFmpegAndroid): android端基于FFmpeg实现音频剪切、拼接、转码、编解码;视频剪切、水印、截图、转码、编解码、转Gif动图;音视频合成与分离,配音;音视频解码、同步与播放;FFmpeg本地推流、H264与RTMP实时推流直播;FFmpeg滤镜:素描、色彩平衡、hue、lut、模糊、九宫格等;歌词解析与显示 - [Zhaoss/WeiXinRecordedDemo](https://github.com/Zhaoss/WeiXinRecordedDemo): 仿微信视频拍摄UI, 基于ffmpeg的视频录制编辑 - [yangjie10930/EpMedia](https://github.com/yangjie10930/EpMedia): Android上基于FFmpeg开发的视频处理框架,简单易用,体积小,帮助使用者快速实现视频处理功能。包含以下功能:剪辑,裁剪,旋转,镜像,合并,分离,变速,添加LOGO,添加滤镜,添加背景音乐,加速减速视频,倒放音视频 - [goldvideo/h265player](https://github.com/goldvideo/h265player): 一套完整的Web版H.265播放器解决方案,非常适合学习交流和实际应用。基于JS码流解封装、WebAssembly(FFmpeg)视频解码,利用Canvas画布投影、AudioContext播放音频。 - [wanliyang1990/wlmusic](https://github.com/wanliyang1990/wlmusic): 基于FFmpeg + OpenSL ES的音频播放SDK。可循环不间断播放短音频;播放raw和assets音频文件;可独立设置音量大小;可实时现在音量分贝大小(用于绘制波形图);可改变音频播放速度和音调(变速不变调、变调不变速、变速又变调);可设置播放声道(左声道、右声道和立体声);可边播边录留住美好音乐;可裁剪指定时间段的音频,制作自己的彩铃;还可以从中获取音频原始PCM数据(可指定采样率),方便二次开发等。 - [Jackarain/avplayer](https://github.com/Jackarain/avplayer): 一个基于FFmpeg、libtorrent的P2P播放器实现 - [tsingsee/EasyPlayerPro-Win](https://github.com/tsingsee/EasyPlayerPro-Win): EasyPlayerPro是一款免费的全功能流媒体播放器,支持RTSP、RTMP、HTTP、HLS、UDP、RTP、File等多种流媒体协议播放、支持本地文件播放,支持本地抓拍、本地录像、播放旋转、多屏播放、倍数播放等多种功能特性,核心基于ffmpeg,稳定、高效、可靠、可控,支持Windows、Android、iOS三个平台,目前在多家教育、安防、行业型公司,都得到的应用,广受好评! - [yangfeng1994/FFmpeg-Android](https://github.com/yangfeng1994/FFmpeg-Android): FFmpeg-Android 是基于ffmpeg n4.0-39-gda39990编译运行在android平台的音视频的处理框架, 使用的是ProcessBuilder执行命令行操作, 可实现视频字幕添加、尺寸剪切、添加或去除水印、时长截取、转GIF动图、涂鸦、音频提取、拼接、质量压缩、加减速、涂鸦、 倒放、素描、色彩平衡、模糊、九宫格、添加贴纸、滤镜、分屏、图片合成视频等,音视频合成、截取、拼接,混音、音视频解码,视频特效等等音视频处理... - [yangjie10930/EpMediaDemo](https://github.com/yangjie10930/EpMediaDemo): 基于FFmpeg开发的视频处理框架,简单易用,体积小,帮助使用者快速实现视频处理功能。包含以下功能:剪辑,裁剪,旋转,镜像,合并,分离,添加LOGO,添加滤镜,添加背景音乐,加速减速视频,倒放音视频。简单的Demo,后面逐渐完善各类功能的使用。 - [qingkouwei/oarplayer](https://github.com/qingkouwei/oarplayer): Android Rtmp播放器,基于MediaCodec与srs-librtmp,不依赖ffmpeg - [goldvideo/decoder_wasm](https://github.com/goldvideo/decoder_wasm): 借助于WebAssembly技术,基于ffmpeg的H.265解码器。 - [HeZhang1994/video-audio-tools](https://github.com/HeZhang1994/video-audio-tools): To process/edit video and audio with Python+FFmpeg. [简单实用] 基于Python+FFmpeg的视频和音频的处理/剪辑。 - [jordiwang/web-capture](https://github.com/jordiwang/web-capture): 基于 ffmpeg + Webassembly 实现前端视频帧提取 - [ccj659/NDK-FFmpeg-master](https://github.com/ccj659/NDK-FFmpeg-master): Video and audio decoding based with FFmpeg 基于ffmpeg的 视频解码 音频解码.播放等 - [kolyvan/kxmovie](https://github.com/kolyvan/kxmovie):iOS电影播放器使用ffmpeg - [CainKernel/CainCamera](https://github.com/CainKernel/CainCamera):一个关于美容相机、图像和短视频开发的Android项目 - [mifi/lossless-cut](https://github.com/mifi/lossless-cut): 一个基于FFmpeg的无损剪辑软件 ## 📂 典藏文档 - [AAC解码算法原理详解](https://github.com/0voice/ffmpeg_develop_doc/blob/main/%E6%96%87%E6%A1%A3%E5%BA%93/AAC%E8%A7%A3%E7%A0%81%E7%AE%97%E6%B3%95%E5%8E%9F%E7%90%86%E8%AF%A6%E8%A7%A3.pdf) - [FFMPEG教程完美排版](https://github.com/0voice/ffmpeg_develop_doc/blob/main/%E6%96%87%E6%A1%A3%E5%BA%93/FFMPEG%E6%95%99%E7%A8%8B%E5%AE%8C%E7%BE%8E%E6%8E%92%E7%89%88.pdf) - [FFMpeg-SDK-开发手册](https://github.com/0voice/ffmpeg_develop_doc/blob/main/%E6%96%87%E6%A1%A3%E5%BA%93/FFMpeg-SDK-%E5%BC%80%E5%8F%91%E6%89%8B%E5%86%8C.pdf) - [FFmpeg Basics](https://github.com/0voice/ffmpeg_develop_doc/blob/main/%E6%96%87%E6%A1%A3%E5%BA%93/FFmpeg%20Basics.pdf) - [ffmpeg(libav)解码全解析(带书签)](https://github.com/0voice/ffmpeg_develop_doc/blob/main/%E6%96%87%E6%A1%A3%E5%BA%93/ffmpeg(libav)%E8%A7%A3%E7%A0%81%E5%85%A8%E8%A7%A3%E6%9E%90(%E5%B8%A6%E4%B9%A6%E7%AD%BE).pdf) - [ffmpeg的tutorial中文版](https://github.com/0voice/ffmpeg_develop_doc/blob/main/%E6%96%87%E6%A1%A3%E5%BA%93/ffmpeg%E7%9A%84tutorial%E4%B8%AD%E6%96%87%E7%89%88.pdf) - [ffmpeg中文文档](https://github.com/0voice/ffmpeg_develop_doc/blob/main/%E6%96%87%E6%A1%A3%E5%BA%93/ffmpeg%E7%9A%84%E4%B8%AD%E6%96%87%E6%96%87%E6%A1%A3.pdf) - [详解FFMPEG API](https://github.com/0voice/ffmpeg_develop_doc/blob/main/%E6%96%87%E6%A1%A3%E5%BA%93/%E8%AF%A6%E8%A7%A3FFMPEG%20API.pdf) - [ffmpeg常用命令参数详解](https://github.com/0voice/ffmpeg_develop_doc/blob/main/ffmpeg%E5%B8%B8%E7%94%A8%E5%91%BD%E4%BB%A4.md) - [ffmepg整体分析](https://github.com/0voice/ffmpeg_develop_doc/blob/main/ffmepg%E6%95%B4%E4%BD%93%E5%88%86%E6%9E%90.pdf) ## 📃 文章 - [FFmpeg 学习(一):FFmpeg 简介](https://github.com/0voice/ffmpeg_develop_doc/blob/main/FFmpeg%20%E5%AD%A6%E4%B9%A0(%E4%B8%80)%EF%BC%9AFFmpeg%20%E7%AE%80%E4%BB%8B%20.md) - [FFmpeg 学习(二):Mac下安装FFmpepg](https://github.com/0voice/ffmpeg_develop_doc/blob/main/FFmpeg%20%E5%AD%A6%E4%B9%A0(%E4%BA%8C)%EF%BC%9AMac%E4%B8%8B%E5%AE%89%E8%A3%85FFmpeg.md) - [FFmpeg 学习(三):将 FFmpeg 移植到 Android平台](https://github.com/0voice/ffmpeg_develop_doc/blob/main/FFmpeg%20%E5%AD%A6%E4%B9%A0(%E4%B8%89)%EF%BC%9A%E5%B0%86%20FFmpeg%20%E7%A7%BB%E6%A4%8D%E5%88%B0%20Android%E5%B9%B3%E5%8F%B0.md) - [FFmpeg 学习(四):FFmpeg API 介绍与通用 API 分析](https://github.com/0voice/ffmpeg_develop_doc/blob/main/FFmpeg%20%E5%AD%A6%E4%B9%A0(%E5%9B%9B)%EF%BC%9AFFmpeg%20API%20%E4%BB%8B%E7%BB%8D%E4%B8%8E%E9%80%9A%E7%94%A8%20API%20%E5%88%86%E6%9E%90.md) - [FFmpeg 学习(五):FFmpeg 编解码 API 分析](https://github.com/0voice/ffmpeg_develop_doc/blob/main/FFmpeg%20%E5%AD%A6%E4%B9%A0(%E4%BA%94)%EF%BC%9AFFmpeg%20%E7%BC%96%E8%A7%A3%E7%A0%81%20API%20%E5%88%86%E6%9E%90.md) - [FFmpeg 学习(六):FFmpeg 核心模块 libavformat 与 libavcodec 分析](https://github.com/0voice/ffmpeg_develop_doc/blob/main/FFmpeg%20%E5%AD%A6%E4%B9%A0(%E5%85%AD)%EF%BC%9AFFmpeg%20%E6%A0%B8%E5%BF%83%E6%A8%A1%E5%9D%97%20libavformat%20%E4%B8%8E%20libavcodec%20%E5%88%86%E6%9E%90.md) - [FFmpeg 学习(七):FFmpeg 学习整理总结](https://github.com/0voice/ffmpeg_develop_doc/blob/main/FFmpeg%20%E5%AD%A6%E4%B9%A0(%E4%B8%83)%EF%BC%9AFFmpeg%20%E5%AD%A6%E4%B9%A0%E6%95%B4%E7%90%86%E6%80%BB%E7%BB%93.md)
- [FFmpeg 结构体学习(一): AVFormatContext 分析](https://github.com/0voice/ffmpeg_develop_doc/blob/main/FFmpeg%20%E7%BB%93%E6%9E%84%E4%BD%93%E5%AD%A6%E4%B9%A0(%E4%B8%80)%EF%BC%9A%20AVFormatContext%20%E5%88%86%E6%9E%90.md) - [FFmpeg 结构体学习(二): AVStream 分析](https://github.com/0voice/ffmpeg_develop_doc/blob/main/FFmpeg%20%E7%BB%93%E6%9E%84%E4%BD%93%E5%AD%A6%E4%B9%A0(%E4%BA%8C)%EF%BC%9A%20AVStream%20%E5%88%86%E6%9E%90.md) - [FFmpeg 结构体学习(三): AVPacket 分析](https://github.com/0voice/ffmpeg_develop_doc/blob/main/FFmpeg%20%E7%BB%93%E6%9E%84%E4%BD%93%E5%AD%A6%E4%B9%A0(%E4%B8%89)%EF%BC%9A%20AVPacket%20%E5%88%86%E6%9E%90.md) - [FFmpeg 结构体学习(四): AVFrame 分析](https://github.com/0voice/ffmpeg_develop_doc/blob/main/FFmpeg%20%E7%BB%93%E6%9E%84%E4%BD%93%E5%AD%A6%E4%B9%A0(%E5%9B%9B)%EF%BC%9A%20AVFrame%20%E5%88%86%E6%9E%90.md) - [FFmpeg 结构体学习(五): AVCodec 分析](https://github.com/0voice/ffmpeg_develop_doc/blob/main/FFmpeg%20%E7%BB%93%E6%9E%84%E4%BD%93%E5%AD%A6%E4%B9%A0(%E4%BA%94)%EF%BC%9A%20AVCodec%20%E5%88%86%E6%9E%90.md) - [FFmpeg 结构体学习(六): AVCodecContext 分析](https://github.com/0voice/ffmpeg_develop_doc/blob/main/FFmpeg%20%E7%BB%93%E6%9E%84%E4%BD%93%E5%AD%A6%E4%B9%A0(%E5%85%AD)%EF%BC%9A%20AVCodecContext%20%E5%88%86%E6%9E%90.md) - [FFmpeg 结构体学习(七): AVIOContext 分析](https://github.com/0voice/ffmpeg_develop_doc/blob/main/FFmpeg%20%E7%BB%93%E6%9E%84%E4%BD%93%E5%AD%A6%E4%B9%A0(%E4%B8%83)%EF%BC%9A%20AVIOContext%20%E5%88%86%E6%9E%90.md) - [FFmpeg 结构体学习(八):FFMPEG中重要结构体之间的关系](https://github.com/0voice/ffmpeg_develop_doc/blob/main/FFmpeg%20%E7%BB%93%E6%9E%84%E4%BD%93%E5%AD%A6%E4%B9%A0(%E5%85%AB)%EF%BC%9AFFMPEG%E4%B8%AD%E9%87%8D%E8%A6%81%E7%BB%93%E6%9E%84%E4%BD%93%E4%B9%8B%E9%97%B4%E7%9A%84%E5%85%B3%E7%B3%BB.md)
- [Linux上的ffmpeg完全使用指南](https://github.com/0voice/ffmpeg_develop_doc/blob/main/Linux%E4%B8%8A%E7%9A%84ffmpeg%E5%AE%8C%E5%85%A8%E4%BD%BF%E7%94%A8%E6%8C%87%E5%8D%97.md) - [3个重点,20个函数分析,浅析FFmpeg转码过程](https://github.com/0voice/ffmpeg_develop_doc/blob/main/3%E4%B8%AA%E9%87%8D%E7%82%B9%EF%BC%8C20%E4%B8%AA%E5%87%BD%E6%95%B0%E5%88%86%E6%9E%90%EF%BC%8C%E6%B5%85%E6%9E%90FFmpeg%E8%BD%AC%E7%A0%81%E8%BF%87%E7%A8%8B.md) ## 🌅 面试题 ##### [1. 为什么巨大的原始视频可以编码成很小的视频呢?这其中的技术是什么呢?](https://github.com/0voice/ffmpeg_develop_doc/blob/main/case_interview/001-README.md#subject_001) ##### [2. 怎么做到直播秒开优化?](https://github.com/0voice/ffmpeg_develop_doc/blob/main/case_interview/001-README.md#subject_002) ##### [3. 直方图在图像处理里面最重要的作用是什么?](https://github.com/0voice/ffmpeg_develop_doc/blob/main/case_interview/001-README.md#subject_003) ##### [4. 数字图像滤波有哪些方法?](https://github.com/0voice/ffmpeg_develop_doc/blob/main/case_interview/001-README.md#subject_004) ##### [5. 图像可以提取的特征有哪些?](https://github.com/0voice/ffmpeg_develop_doc/blob/main/case_interview/001-README.md#subject_005) ##### [6. 衡量图像重建好坏的标准有哪些?怎样计算?](https://github.com/0voice/ffmpeg_develop_doc/blob/main/case_interview/001-README.md#subject_006) ##### [7. AAC和PCM的区别?](https://github.com/0voice/ffmpeg_develop_doc/blob/main/case_interview/001-README.md#subject_007) ##### [8. H264存储的两个形态?](https://github.com/0voice/ffmpeg_develop_doc/blob/main/case_interview/001-README.md#subject_008) ##### [9. FFMPEG:图片如何合成视频?](https://github.com/0voice/ffmpeg_develop_doc/blob/main/case_interview/001-README.md#subject_009) ##### [10. 常见的音视频格式有哪些?](https://github.com/0voice/ffmpeg_develop_doc/blob/main/case_interview/001-README.md#subject_010) ##### [11. 请指出“1080p”的意义?](https://github.com/0voice/ffmpeg_develop_doc/blob/main/case_interview/001-README.md#subject_011) ##### [12. 请解释颜色的本质及其数字记录原理,并说出几个你所知道的色域。](https://github.com/0voice/ffmpeg_develop_doc/blob/main/case_interview/001-README.md#subject_012) ##### [13. 请解释“矢量图”和“位图”的区别?](https://github.com/0voice/ffmpeg_develop_doc/blob/main/case_interview/001-README.md#subject_013) ##### [14. 请从“光圈”“快门速度”“感光度”“白平衡”“景深”中任选2个进行叙述?](https://github.com/0voice/ffmpeg_develop_doc/blob/main/case_interview/001-README.md#subject_014) ##### [15. 视频分量YUV的意义及数字化格式?](https://github.com/0voice/ffmpeg_develop_doc/blob/main/case_interview/001-README.md#subject_015) ##### [16. 在MPEG标准中图像类型有哪些?](https://github.com/0voice/ffmpeg_develop_doc/blob/main/case_interview/001-README.md#subject_016) ##### [17. 列举一些音频编解码常用的实现方案?](https://github.com/0voice/ffmpeg_develop_doc/blob/main/case_interview/001-README.md#subject_017) ##### [18. 请叙述MPEG视频基本码流结构?](https://github.com/0voice/ffmpeg_develop_doc/blob/main/case_interview/001-README.md#subject_018) ##### [19. sps和pps的区别?](https://github.com/0voice/ffmpeg_develop_doc/blob/main/case_interview/001-README.md#subject_019) ##### [20. 请叙述AMR基本码流结构?](https://github.com/0voice/ffmpeg_develop_doc/blob/main/case_interview/001-README.md#subject_020) ##### [21. 预测编码的基本原理是什么?](https://github.com/0voice/ffmpeg_develop_doc/blob/main/case_interview/001-README.md#subject_021) ##### [22. 说一说ffmpeg的数据结构?](https://github.com/0voice/ffmpeg_develop_doc/blob/main/case_interview/001-README.md#subject_022) ##### [23. 说一说AVFormatContext 和 AVInputFormat之间的关系?](https://github.com/0voice/ffmpeg_develop_doc/blob/main/case_interview/001-README.md#subject_023) ##### [24. 说一说AVFormatContext, AVStream和AVCodecContext之间的关系?](https://github.com/0voice/ffmpeg_develop_doc/blob/main/case_interview/001-README.md#subject_024) ##### [25. 说一说视频拼接处理步骤?(细节处理,比如分辨率大小不一,时间处理等等)](https://github.com/0voice/ffmpeg_develop_doc/blob/main/case_interview/001-README.md#subject_025) ##### [26. NV21如何转换成I420?](https://github.com/0voice/ffmpeg_develop_doc/blob/main/case_interview/001-README.md#subject_026) ##### [27. DTS与PTS共同点?](https://github.com/0voice/ffmpeg_develop_doc/blob/main/case_interview/001-README.md#subject_027) ##### [28. 影响视频清晰度的指标有哪些?](https://github.com/0voice/ffmpeg_develop_doc/blob/main/case_interview/001-README.md#subject_028) ##### [29. 编解码处理时遇到什么困难?](https://github.com/0voice/ffmpeg_develop_doc/blob/main/case_interview/001-README.md#subject_029) ##### [30. 如何秒开视频?什么是秒开视频?](https://github.com/0voice/ffmpeg_develop_doc/blob/main/case_interview/001-README.md#subject_030) ##### [31. 如何降低延迟?如何保证流畅性?如何解决卡顿?解决网络抖动?](https://github.com/0voice/ffmpeg_develop_doc/blob/main/case_interview/001-README.md#subject_031) ##### [32. 需要把网络上一段视频存储下来(比如作为mp4 ), 请实现并说出方法(第一个视频需要翻墙才能进)?](https://github.com/0voice/ffmpeg_develop_doc/blob/main/case_interview/001-README.md#subject_032) ##### [33. 需要把网络上一段语音存储下来(比如作为mp3 ), 请实现并说出方法?](https://github.com/0voice/ffmpeg_develop_doc/blob/main/case_interview/001-README.md#subject_033) ##### [34. 为什么要有YUV这种数据出来?(YUV相比RGB来说的优点)](https://github.com/0voice/ffmpeg_develop_doc/blob/main/case_interview/001-README.md#subject_034) ##### [35. H264/H265有什么区别?](https://github.com/0voice/ffmpeg_develop_doc/blob/main/case_interview/001-README.md#subject_035) ##### [36. 视频或者音频传输,你会选择TCP协议还是UDP协议?为什么?](https://github.com/0voice/ffmpeg_develop_doc/blob/main/case_interview/001-README.md#subject_036) ##### [37. 平时说的软解和硬解,具体是什么?](https://github.com/0voice/ffmpeg_develop_doc/blob/main/case_interview/001-README.md#subject_037) ##### [38. 何为直播?何为点播?](https://github.com/0voice/ffmpeg_develop_doc/blob/main/case_interview/001-README.md#subject_038) ##### [39. 简述推流、拉流的工作流程?](https://github.com/0voice/ffmpeg_develop_doc/blob/main/case_interview/001-README.md#subject_039) ##### [40. 如何在直播中I帧间隔设置、与帧率分辨率选定?](https://github.com/0voice/ffmpeg_develop_doc/blob/main/case_interview/001-README.md#subject_040) ##### [41. 直播推流中推I帧与推非I帧区别是什么?](https://github.com/0voice/ffmpeg_develop_doc/blob/main/case_interview/001-README.md#subject_041) ##### [42. 常见的直播协议有哪些?之间有什么区别?](https://github.com/0voice/ffmpeg_develop_doc/blob/main/case_interview/001-README.md#subject_042) ##### [43. 点播中常见的数据传输协议主要有哪些?](https://github.com/0voice/ffmpeg_develop_doc/blob/main/case_interview/001-README.md#subject_043) ##### [44. RTMP、HLS协议各自的默认端口号是?](https://github.com/0voice/ffmpeg_develop_doc/blob/main/case_interview/001-README.md#subject_044) ##### [45. 简述RTMP协议,如何封装RTMP包?](https://github.com/0voice/ffmpeg_develop_doc/blob/main/case_interview/001-README.md#subject_045) ##### [46. m3u8构成是?直播中m3u8、ts如何实时更新?](https://github.com/0voice/ffmpeg_develop_doc/blob/main/case_interview/001-README.md#subject_046) ##### [47. 何为音视频同步,音视频同步是什么标准?](https://github.com/0voice/ffmpeg_develop_doc/blob/main/case_interview/001-README.md#subject_047) ##### [48. 播放器暂停、快进快退、seek、逐帧、变速怎么实现?](https://github.com/0voice/ffmpeg_develop_doc/blob/main/case_interview/001-README.md#subject_048) ##### [49. 说说你平时在播放过程中做的优化工作?](https://github.com/0voice/ffmpeg_develop_doc/blob/main/case_interview/001-README.md#subject_049) ##### [50. 你研究过哪些具体的流媒体服务器,是否做过二次开发?](https://github.com/0voice/ffmpeg_develop_doc/blob/main/case_interview/001-README.md#subject_050) ##### [51. 什么是GOP?](https://github.com/0voice/ffmpeg_develop_doc/blob/main/case_interview/002-README.md#subject_051) ##### [52. 音频测试的测试点,音频时延如何测试?](https://github.com/0voice/ffmpeg_develop_doc/blob/main/case_interview/002-README.md#subject_052) ##### [53. 美颜的实现原理,具体实现步骤?](https://github.com/0voice/ffmpeg_develop_doc/blob/main/case_interview/002-README.md#subject_053) ##### [54. 如何直播APP抓包过来的文件,如何过滤上行,下行,总码率?](https://github.com/0voice/ffmpeg_develop_doc/blob/main/case_interview/002-README.md#subject_054) ##### [55. 如何测试一个美颜挂件?](https://github.com/0voice/ffmpeg_develop_doc/blob/main/case_interview/002-README.md#subject_055) ##### [56. 为什么要用FLV?](https://github.com/0voice/ffmpeg_develop_doc/blob/main/case_interview/002-README.md#subject_056) ##### [57. 如何测试一个美颜挂件?](https://github.com/0voice/ffmpeg_develop_doc/blob/main/case_interview/002-README.md#subject_057) ##### [58. 平常的视频格式?](https://github.com/0voice/ffmpeg_develop_doc/blob/main/case_interview/002-README.md#subject_058) ##### [59. 何为homebrew?你用它安装过什么?常用命令有哪些?](https://github.com/0voice/ffmpeg_develop_doc/blob/main/case_interview/002-README.md#subject_059) ##### [60. RTMP、HLS协议各自的默认端口号是?](https://github.com/0voice/ffmpeg_develop_doc/blob/main/case_interview/002-README.md#subject_060) ## 🧿 视频 ### 国外大神 No.|title :------- | :--------------- 1|[如何使用FFMPEG将MP4视频文件转换为GIF](https://www.0voice.com/uiwebsite/audio_video_streaming/video/001-如何使用FFMPEG将MP4视频文件转换为GIF.mp4) 2|[FFMPEG Introduction & Examples](https://www.0voice.com/uiwebsite/audio_video_streaming/video/002-FFMPEG%20Introduction%20%26%20Examples.mp4) 3|[Live Streaming with Nginx and FFmpeg](https://www.0voice.com/uiwebsite/audio_video_streaming/video/003-Live%20Streaming%20with%20Nginx%20and%20FFmpeg.mp4) 4|[Ep2 Ffmpeg Nginx & Nginx-Rtmp-Module Streaming to Server](https://www.0voice.com/uiwebsite/audio_video_streaming/video/004-Ep2%20Ffmpeg%20Nginx%20%26%20Nginx-Rtmp-Module%20Streaming%20to%20Server.mp4) 5|[Streaming an IP Camera to a Web Browser using FFmpeg](https://www.0voice.com/uiwebsite/audio_video_streaming/video/005-Streaming%20an%20IP%20Camera%20to%20a%20Web%20Browser%20using%20FFmpeg.mp4) 6|[Easy Screencasting and Webcamming with ffmpeg in Linux](https://www.0voice.com/uiwebsite/audio_video_streaming/video/006-Easy%20Screencasting%20and%20Webcamming%20with%20ffmpeg%20in%20Linux.mp4) 7|[Streaming an IP Camera to a Web Browser using FFmpeg](https://www.0voice.com/uiwebsite/audio_video_streaming/video/007-Streaming%20an%20IP%20Camera%20to%20a%20Web%20Browser%20using%20FFmpeg.mp4) 8|[FFMPEG Advanced Techniques Pt2 - Filtergraphs & Timeline](https://www.0voice.com/uiwebsite/audio_video_streaming/video/008-FFMPEG%20Advanced%20Techniques%20Pt2%20-%20Filtergraphs%20%26%20Timeline.mp4) 9|[Convert HEVCh265 mkv video to AVCh264 mp4 with ffmpeg](https://www.0voice.com/uiwebsite/audio_video_streaming/video/009-Convert%20HEVCh265%20mkv%20video%20to%20AVCh264%20mp4%20with%20ffmpeg.mp4) 10|[How to add soft subtitles( srt subrip) to mp4 video using ffmpeg](https://www.0voice.com/uiwebsite/audio_video_streaming/video/010-How%20to%20add%20soft%20subtitles(%20srt%20subrip)%20to%20mp4%20video%20using%20ffmpeg.mp4) 11|[FFmpeg Processing multiple video files by using.bat file](https://www.0voice.com/uiwebsite/audio_video_streaming/video/011-FFmpeg%20Processing%20multiple%20video%20files%20by%20using.bat%20file.mp4) 12|[Opensource Multimedia Framework -- FFmpeg](https://www.0voice.com/uiwebsite/audio_video_streaming/video/012-Opensource%20Multimedia%20Framework%20--%20FFmpeg.mp4) 13|[rtsp streaming node js ip camera jsmpeg](https://www.0voice.com/uiwebsite/audio_video_streaming/video/013-rtsp%20streaming%20node%20js%20ip%20camera%20jsmpeg.mp4) 14|[H.265 RTSP Streaming to VLC + NewTek NDI Integration](https://www.0voice.com/uiwebsite/audio_video_streaming/video/014-H.265%20RTSP%20Streaming%20to%20VLC%20+%20NewTek%20NDI%20Integration.mp4) 15|[IP camera stream using RTSP and openCV python](https://www.0voice.com/uiwebsite/audio_video_streaming/video/015-IP%20camera%20stream%20using%20RTSP%20and%20openCV%20python.mp4) 16|[NAT Traversal & RTSP](https://www.0voice.com/uiwebsite/audio_video_streaming/video/016-NAT%20Traversal%20%26%20RTSP.mp4) 17|[Simple client et serveur de Streaming RTSP MJPEG(JAVA SE)](https://www.0voice.com/uiwebsite/audio_video_streaming/video/017-Simple%20client%20et%20serveur%20de%20Streaming%20RTSP%20MJPEG(JAVA%20SE).mp4) 18|[Build Your First WebRTC Video Chat App](https://www.0voice.com/uiwebsite/audio_video_streaming/video/018-Build%20Your%20First%20WebRTC%20Video%20Chat%20App.mp4) 19|[P2P Video Chat with JavaScript/WebRTC](https://www.0voice.com/uiwebsite/audio_video_streaming/video/019-P2P%20Video%20Chat%20with%20JavaScript%20WebRTC.mp4) 20|[Building a WebRTC app - LIVE](https://www.0voice.com/uiwebsite/audio_video_streaming/video/020-Building%20a%20WebRTC%20app%20-%20LIVE.mp4) 21|[Zoom vs WebRTC](https://www.0voice.com/uiwebsite/audio_video_streaming/video/021-Zoom%20vs%20WebRTC.mp4) 22|[Architectures for a kickass WebRTC application](https://www.0voice.com/uiwebsite/audio_video_streaming/video/022-Architectures%20for%20a%20kickass%20WebRTC%20application.mp4) 23|[(REACT NATIVE) - integrate webRTC](https://www.0voice.com/uiwebsite/audio_video_streaming/video/023-(REACT%20NATIVE)%20-%20integrate%20webRTC.mp4) 24|[How to build Serverless Video Chat App using Firebase and WebRTC in React](https://www.0voice.com/uiwebsite/audio_video_streaming/video/024-How%20to%20build%20Serverless%20Video%20Chat%20App%20using%20Firebase%20and%20WebRTC%20in%20React.mp4) 25|[Implementation Lessons using WebRTC in Asterisk](https://www.0voice.com/uiwebsite/audio_video_streaming/video/025-Implementation%20Lessons%20using%20WebRTC%20in%20Asterisk.mp4) ### 国内大佬 No.|title | 地址 :------- | :---------------| :--------------- 26|windows ffmpeg命令行环境搭建|[百度网盘](https://pan.baidu.com/s/1eCQ7o3gcuU06k6-ZcXUASQ) 提取码:i3f2 27|FFMPEG如何查询命令帮助文档|[百度网盘](https://pan.baidu.com/s/1oA2OErmfZZpEEY_wRQrl_A) 提取码:9mqk 28|ffmpeg音视频处理流程|[百度网盘](https://pan.baidu.com/s/1jSIop6IUtxOwkse7xnCI7Q) 提取码:azx3 29|ffmpeg命令分类查询|[百度网盘](https://pan.baidu.com/s/1VGwop_lOJozEh_gYpKYkrw) 提取码:odhc 30|ffplay播放控制|[百度网盘](https://pan.baidu.com/s/1BbKQvJdokQrazoNtYjhA2Q) 提取码:e51s 31|ffplay命令选项(上)|[百度网盘](https://pan.baidu.com/s/1upOGZQdmXyiZbWO1LBcTCQ) 提取码:n1zx 32|ffplay命令选项(下)|[百度网盘](https://pan.baidu.com/s/1d55H9PyK1CU9Nfu37NIBhw) 提取码:rtn0 33|ffplay命令播放媒体|[百度网盘](https://pan.baidu.com/s/1FjJnW8eBZxsKIIdvbh0f-A) 提取码:bs9s 34|ffplay简单过滤器|[百度网盘](https://pan.baidu.com/s/1YlkCGIMH62Wj0-OTRLxDkA) 提取码:r4rk 35|ffmpeg命令参数说明|[百度网盘](https://pan.baidu.com/s/1aOL7vXnspVAh-iNYsz_5xA) 提取码:5q18 36|ffmpeg命令提取音视频数据|[百度网盘](https://pan.baidu.com/s/1Zlv_6a-O9Fj9HFpt9S6Z5g) 提取码:v807 37|ffmpeg命令提取像素格式和PCM数据|[百度网盘](https://pan.baidu.com/s/1Z1cdwVexIvAiyCQNPA0k3A) 提取码:az9x 38|ffmpeg命令转封装|[百度网盘](https://pan.baidu.com/s/1TxZpe2RicrGWgZPhi81E2g) 提取码:s7ez 39|fmpeg命令裁剪和合并视频|[百度网盘](https://pan.baidu.com/s/1W8b_krHc3PzAfoRXneS2Wg) 提取码:6g0g 40|fmpeg命令图片与视频互转|[百度网盘](https://pan.baidu.com/s/1nHhhA3y8dHneFVfNoY_fHg) 提取码:a3p5 41|ffmpeg命令视频录制|[百度网盘](https://pan.baidu.com/s/1zGz_P34GHKE5KVt_b8bT3w) 提取码:em7b 42|ffmpeg命令直播(上)|[百度网盘](https://pan.baidu.com/s/1rtCfJWWaanK6Syk2254h2g) 提取码:ilxz 43|ffmpeg命令直播(下)|[百度网盘](https://pan.baidu.com/s/1mo7vo4d_ghqrue7gzE0M1g) 提取码:akyr 44|ffmpeg过滤器-裁剪|[百度网盘](https://pan.baidu.com/s/1vuQLx_ff8ZnlStxX2aOeXA) 提取码:toii 45|ffmpeg过滤器-文字水印|[百度网盘](https://pan.baidu.com/s/1YilCkZg99xhwEQBwjenWKQ) 提取码:unuu 46|ffmpeg过滤器-图片水印|[百度网盘](https://pan.baidu.com/s/11VFsXn-c8e9GZ3Wy4M8hAA) 提取码:mw4v 47|ffmpeg过滤器-画中画|[百度网盘](https://pan.baidu.com/s/1TFiR47qhPTHAzbSQhatEBA) 提取码:c6fc 48|ffmpeg过滤器-多宫格|[百度网盘](https://pan.baidu.com/s/1Ib73MtuqgaFoECuSrzOApQ) 提取码:aioi 49|SRS流媒体服务器实战(上)|[百度网盘](https://pan.baidu.com/s/1kZTa5-0kfCcdMiObpJdOfQ) 提取码:4134 50|SRS流媒体服务器实战(下)|[百度网盘](https://pan.baidu.com/s/1goy3g9rmHc-JmO9VpsCKvg) 提取码:g4be 51|音视频开发-ffplay.iikplayer、vlc的播放器设计实现|[百度网盘](https://pan.baidu.com/s/1NTT_fzfkWIYy2DX90joAoA) 提取码:1img 52|音视频成长之路-进阶三部曲|[百度网盘](https://pan.baidu.com/s/1XUTn60ZHTBt63CmQe2vObw) 提取码:4nw3 53|为什么直播领域也要搞WebRTC-srs4.0|[百度网盘](https://pan.baidu.com/s/1c9dexc7-QglR-0hkvqnUEQ) 提取码:m47a 54|腾讯课堂直播如何做到低延迟|[百度网盘](https://pan.baidu.com/s/1oRuwvWRyw7YjDAqzMPnZyQ) 提取码:jruh 55|rtmp2webrtc提出问题-灵魂拷问|[百度网盘](https://pan.baidu.com/s/1cyf0qCYUYKNyfSchyY6aWQ) 提取码:pupp ## 📰 论文 [分布式视频处理系统设计与实现](https://github.com/0voice/ffmpeg_develop_doc/blob/main/%E5%88%86%E5%B8%83%E5%BC%8F%E8%A7%86%E9%A2%91%E5%A4%84%E7%90%86%E7%B3%BB%E7%BB%9F%E8%AE%BE%E8%AE%A1%E4%B8%8E%E5%AE%9E%E7%8E%B0.pdf) [基于Android的H.264_AVC解码器的设计与实现](https://github.com/0voice/ffmpeg_develop_doc/blob/main/%E5%9F%BA%E4%BA%8EAndroid%E7%9A%84H.264_AVC%E8%A7%A3%E7%A0%81%E5%99%A8%E7%9A%84%E8%AE%BE%E8%AE%A1%E4%B8%8E%E5%AE%9E%E7%8E%B0.pdf) [基于FFMPEG的视频转换系统](https://github.com/0voice/ffmpeg_develop_doc/blob/main/%E5%9F%BA%E4%BA%8EFFMPEG%E7%9A%84%E8%A7%86%E9%A2%91%E8%BD%AC%E6%8D%A2%E7%B3%BB%E7%BB%9F.pdf) [基于FFMPEG的跨平台视频编解码研究](https://github.com/0voice/ffmpeg_develop_doc/blob/main/%E5%9F%BA%E4%BA%8EFFMPEG%E7%9A%84%E8%B7%A8%E5%B9%B3%E5%8F%B0%E8%A7%86%E9%A2%91%E7%BC%96%E8%A7%A3%E7%A0%81%E7%A0%94%E7%A9%B6.pdf) [基于FFMPEG解码的音视频同步实现](https://github.com/0voice/ffmpeg_develop_doc/blob/main/%E5%9F%BA%E4%BA%8EFFMPEG%E8%A7%A3%E7%A0%81%E7%9A%84%E9%9F%B3%E8%A7%86%E9%A2%91%E5%90%8C%E6%AD%A5%E5%AE%9E%E7%8E%B0.pdf) [基于FFMpeg的稳定应用层组播流媒体直播系统研究](https://github.com/0voice/ffmpeg_develop_doc/blob/main/%E5%9F%BA%E4%BA%8EFFMpeg%E7%9A%84%E7%A8%B3%E5%AE%9A%E5%BA%94%E7%94%A8%E5%B1%82%E7%BB%84%E6%92%AD%E6%B5%81%E5%AA%92%E4%BD%93%E7%9B%B4%E6%92%AD%E7%B3%BB%E7%BB%9F%E7%A0%94%E7%A9%B6.pdf) [基于FFmpeg和SDL的智能录屏及播放系统](https://github.com/0voice/ffmpeg_develop_doc/blob/main/%E5%9F%BA%E4%BA%8EFFmpeg%E5%92%8CSDL%E7%9A%84%E6%99%BA%E8%83%BD%E5%BD%95%E5%B1%8F%E5%8F%8A%E6%92%AD%E6%94%BE%E7%B3%BB%E7%BB%9F.pdf) [基于FFmpeg和SDL的视频流播放存储研究综述](https://github.com/0voice/ffmpeg_develop_doc/blob/main/%E5%9F%BA%E4%BA%8EFFmpeg%E5%92%8CSDL%E7%9A%84%E8%A7%86%E9%A2%91%E6%B5%81%E6%92%AD%E6%94%BE%E5%AD%98%E5%82%A8%E7%A0%94%E7%A9%B6%E7%BB%BC%E8%BF%B0.pdf) [基于FFmpeg的H.264解码器实现](https://github.com/0voice/ffmpeg_develop_doc/blob/main/%E5%9F%BA%E4%BA%8EFFmpeg%E7%9A%84H.264%E8%A7%A3%E7%A0%81%E5%99%A8%E5%AE%9E%E7%8E%B0.pdf) [基于FFmpeg的网络视频监控系统的设计与实现](https://github.com/0voice/ffmpeg_develop_doc/blob/main/%E5%9F%BA%E4%BA%8EFFmpeg%E7%9A%84%E7%BD%91%E7%BB%9C%E8%A7%86%E9%A2%91%E7%9B%91%E6%8E%A7%E7%B3%BB%E7%BB%9F%E7%9A%84%E8%AE%BE%E8%AE%A1%E4%B8%8E%E5%AE%9E%E7%8E%B0.pdf) [基于FFmpeg的视频转码与保护系统的设计与实现](https://github.com/0voice/ffmpeg_develop_doc/blob/main/%E5%9F%BA%E4%BA%8EFFmpeg%E7%9A%84%E8%A7%86%E9%A2%91%E8%BD%AC%E7%A0%81%E4%B8%8E%E4%BF%9D%E6%8A%A4%E7%B3%BB%E7%BB%9F%E7%9A%84%E8%AE%BE%E8%AE%A1%E4%B8%8E%E5%AE%9E%E7%8E%B0.pdf) [基于FFmpeg的高清实时直播系统设计与实现](https://github.com/0voice/ffmpeg_develop_doc/blob/main/%E5%9F%BA%E4%BA%8EFFmpeg%E7%9A%84%E9%AB%98%E6%B8%85%E5%AE%9E%E6%97%B6%E7%9B%B4%E6%92%AD%E7%B3%BB%E7%BB%9F%E8%AE%BE%E8%AE%A1%E4%B8%8E%E5%AE%9E%E7%8E%B0.pdf) [基于H.264与H.265的低延时视频监控系统的设计与实现](https://github.com/0voice/ffmpeg_develop_doc/blob/main/%E5%9F%BA%E4%BA%8EH.264%E4%B8%8EH.265%E7%9A%84%E4%BD%8E%E5%BB%B6%E6%97%B6%E8%A7%86%E9%A2%91%E7%9B%91%E6%8E%A7%E7%B3%BB%E7%BB%9F%E7%9A%84%E8%AE%BE%E8%AE%A1%E4%B8%8E%E5%AE%9E%E7%8E%B0.pdf) [基于H.265的无线视频监控系统设计与实现](https://github.com/0voice/ffmpeg_develop_doc/blob/main/%E5%9F%BA%E4%BA%8EH.265%E7%9A%84%E6%97%A0%E7%BA%BF%E8%A7%86%E9%A2%91%E7%9B%91%E6%8E%A7%E7%B3%BB%E7%BB%9F%E8%AE%BE%E8%AE%A1%E4%B8%8E%E5%AE%9E%E7%8E%B0.pdf) [基于H.265的视频教育系统的设计与实现](https://github.com/0voice/ffmpeg_develop_doc/blob/main/%E5%9F%BA%E4%BA%8EH.265%E7%9A%84%E8%A7%86%E9%A2%91%E6%95%99%E8%82%B2%E7%B3%BB%E7%BB%9F%E7%9A%84%E8%AE%BE%E8%AE%A1%E4%B8%8E%E5%AE%9E%E7%8E%B0.pdf) [基于Hadoop的视频转码优化的研究](https://github.com/0voice/ffmpeg_develop_doc/blob/main/%E5%9F%BA%E4%BA%8EHadoop%E7%9A%84%E8%A7%86%E9%A2%91%E8%BD%AC%E7%A0%81%E4%BC%98%E5%8C%96%E7%9A%84%E7%A0%94%E7%A9%B6.pdf) [基于RTMP协议的流媒体系统的设计实现](https://github.com/0voice/ffmpeg_develop_doc/blob/main/%E5%9F%BA%E4%BA%8ERTMP%E5%8D%8F%E8%AE%AE%E7%9A%84%E6%B5%81%E5%AA%92%E4%BD%93%E7%B3%BB%E7%BB%9F%E7%9A%84%E8%AE%BE%E8%AE%A1%E5%AE%9E%E7%8E%B0.pdf) [基于RTMP的高清流媒体直播点播封装技术的研究与实现](https://github.com/0voice/ffmpeg_develop_doc/blob/main/%E5%9F%BA%E4%BA%8ERTMP%E7%9A%84%E9%AB%98%E6%B8%85%E6%B5%81%E5%AA%92%E4%BD%93%E7%9B%B4%E6%92%AD%E7%82%B9%E6%92%AD%E5%B0%81%E8%A3%85%E6%8A%80%E6%9C%AF%E7%9A%84%E7%A0%94%E7%A9%B6%E4%B8%8E%E5%AE%9E%E7%8E%B0.caj) [基于RTSP协议的iOS视频播放器的设计与实现](https://github.com/0voice/ffmpeg_develop_doc/blob/main/%E5%9F%BA%E4%BA%8ERTSP%E5%8D%8F%E8%AE%AE%E7%9A%84iOS%E8%A7%86%E9%A2%91%E6%92%AD%E6%94%BE%E5%99%A8%E7%9A%84%E8%AE%BE%E8%AE%A1%E4%B8%8E%E5%AE%9E%E7%8E%B0.pdf) [基于RTSP协议的多源视音频实时直播系统的设计与实现](https://github.com/0voice/ffmpeg_develop_doc/blob/main/%E5%9F%BA%E4%BA%8ERTSP%E5%8D%8F%E8%AE%AE%E7%9A%84%E5%A4%9A%E6%BA%90%E8%A7%86%E9%9F%B3%E9%A2%91%E5%AE%9E%E6%97%B6%E7%9B%B4%E6%92%AD%E7%B3%BB%E7%BB%9F%E7%9A%84%E8%AE%BE%E8%AE%A1%E4%B8%8E%E5%AE%9E%E7%8E%B0.pdf) [基于RTSP的H.264实时流媒体传输方案的研究与实现](https://github.com/0voice/ffmpeg_develop_doc/blob/main/%E5%9F%BA%E4%BA%8ERTSP%E7%9A%84H.264%E5%AE%9E%E6%97%B6%E6%B5%81%E5%AA%92%E4%BD%93%E4%BC%A0%E8%BE%93%E6%96%B9%E6%A1%88%E7%9A%84%E7%A0%94%E7%A9%B6%E4%B8%8E%E5%AE%9E%E7%8E%B0.pdf) [基于RTSP的音视频传输系统研究与实现](https://github.com/0voice/ffmpeg_develop_doc/blob/main/%E5%9F%BA%E4%BA%8ERTSP%E7%9A%84%E9%9F%B3%E8%A7%86%E9%A2%91%E4%BC%A0%E8%BE%93%E7%B3%BB%E7%BB%9F%E7%A0%94%E7%A9%B6%E4%B8%8E%E5%AE%9E%E7%8E%B0.pdf) [基于TCP传输的嵌入式流媒体播放系统](https://github.com/0voice/ffmpeg_develop_doc/blob/main/%E5%9F%BA%E4%BA%8ETCP%E4%BC%A0%E8%BE%93%E7%9A%84%E5%B5%8C%E5%85%A5%E5%BC%8F%E6%B5%81%E5%AA%92%E4%BD%93%E6%92%AD%E6%94%BE%E7%B3%BB%E7%BB%9F.pdf) [基于ffmpeg的高性能高清流媒体播放器软件设计](https://github.com/0voice/ffmpeg_develop_doc/blob/main/%E5%9F%BA%E4%BA%8Effmpeg%E7%9A%84%E9%AB%98%E6%80%A7%E8%83%BD%E9%AB%98%E6%B8%85%E6%B5%81%E5%AA%92%E4%BD%93%E6%92%AD%E6%94%BE%E5%99%A8%E8%BD%AF%E4%BB%B6%E8%AE%BE%E8%AE%A1.pdf) [基于流媒体技术的移动视频直播系统的设计与实现](https://github.com/0voice/ffmpeg_develop_doc/blob/main/%E5%9F%BA%E4%BA%8E%E6%B5%81%E5%AA%92%E4%BD%93%E6%8A%80%E6%9C%AF%E7%9A%84%E7%A7%BB%E5%8A%A8%E8%A7%86%E9%A2%91%E7%9B%B4%E6%92%AD%E7%B3%BB%E7%BB%9F%E7%9A%84%E8%AE%BE%E8%AE%A1%E4%B8%8E%E5%AE%9E%E7%8E%B0.pdf) [直播聚合平台的设计与实现](https://github.com/0voice/ffmpeg_develop_doc/blob/main/%E7%9B%B4%E6%92%AD%E8%81%9A%E5%90%88%E5%B9%B3%E5%8F%B0%E7%9A%84%E8%AE%BE%E8%AE%A1%E4%B8%8E%E5%AE%9E%E7%8E%B0.pdf) [音视频信号采集压缩及传输系统的设计与实现](https://github.com/0voice/ffmpeg_develop_doc/blob/main/%E9%9F%B3%E8%A7%86%E9%A2%91%E4%BF%A1%E5%8F%B7%E9%87%87%E9%9B%86%E5%8E%8B%E7%BC%A9%E5%8F%8A%E4%BC%A0%E8%BE%93%E7%B3%BB%E7%BB%9F%E7%9A%84%E8%AE%BE%E8%AE%A1%E4%B8%8E%E5%AE%9E%E7%8E%B0.pdf)

零领工作

--- ##### 实时提供,每周发布北京,上海,广州,深圳,杭州,南京,合肥,武汉,长沙,重庆,成都,西安,厦门的c/c++,golang方向的招聘岗位信息。 包含校招,社招,实习岗位, 面经,八股,简历 零领工作

================================================ FILE: case_interview/001-README.md ================================================ #

面试题1

为什么巨大的原始视频可以编码成很小的视频呢?这其中的技术是什么呢? ##### 参考答案 * 1)空间冗余:图像相邻像素之间有较强的相关性 * 2)时间冗余:视频序列的相邻图像之间内容相似 * 3)编码冗余:不同像素值出现的概率不同 * 4)视觉冗余:人的视觉系统对某些细节不敏感 * 5)知识冗余:规律性的结构可由先验知识和背景知识得到 #

面试题2

怎么做到直播秒开优化? ##### 参考答案 * DNS 解析慢 为了有效降低 DNS 解析对首开的影响,我们可以提前完成播放域名->IP 地址的解析, 并缓存起来,播放的时候,直接传入带 IP 地址的播放地址,从而省去了 DNS 解析的耗时。 如果要支持用 IP 地址播放,是需要修改底层 ffmpeg 源码的。 * 播放策略 很多侧重点播的播放器,为了减少卡顿,会有一些缓冲策略,当缓冲足够多的数据之后 ,再送入解码播放。 而为了加快首开效果,需要对播放的缓冲策略做一些调整,如果第一帧还没有渲染出来的情况下, 不要做任何缓冲,直接送入解码器解码播放,这样就可以保证没有任何因为「主动」缓冲带来的首开延时。 * 播放参数设置 所有基于 ffmpeg 的播放器,都会遇到avformat_find_stream_info这个函数耗时比较久, 从而增大了首开时间,该函数主要作用是通过读取一定字节的码流数据, 来分析码流的基本信息,如编码信息、时长、码率、帧率等等,它由两个参数来控制其读取的数据量大小和时长, 一个是 probesize,一个是 analyzeduration。 减少 probesize 和 analyzeduration 可以有效地减少avformat_find_stream_info的函数耗时, 从而加快首开,但是需要注意的是,设置地太小可能会导致读取的数据量不足,从而无法解析出码流信息,导致播放失败, 或者出现只有音频没有视频,只有视频没有音频的问题。 * 服务端优化 * 服务器关键帧缓冲 * CDN最近策略 #

面试题3

直方图在图像处理里面最重要的作用是什么? ##### 参考答案 1. 灰度直方图的定义:灰度级的函数,描述图像中该灰度级的像素个数或该灰度级像素出现的频率。反映了图像灰度分布的情况。 2. 灰度直方图只能反映图像的灰度分布情况,不能反映图像像素的位置,即所有的空间信息全部丢失。 * 直方图的应用: * a.数字化参数:判断一幅图像是否合理的利用了全部被允许的灰度级范围。一般一幅图应该利用全部或几乎全部可能的灰度级,否则等于增加了量化间隔,丢失的信息将不能恢复。 * b.边界阈值选取(确定图像二值化的阈值):假定某图像的灰度直方图具有二峰性,则表明这个图像的较亮区域和较暗区域可以很好地分离,以这一点为阈值点,可以得到很好地2值处理效果(区分物体与背景)。 * c.当物体部分的灰度值比其他部分的灰度值大时,可利用直方图统计图像中物体的面积。 * d.计算图像的信息量H。 #

面试题4

数字图像滤波有哪些方法? ##### 参考答案 均值滤波(邻域平均法)、中值滤波(消除独立的噪声点)、高斯滤波(线性平滑滤波,消除高斯噪声,对整幅图像进行加权平均,每一个像素点的值都由其本身和邻域内的其他像素值经过加权平均后得到)、KNN滤波、高通滤波、低通滤波等。 #

面试题5

图像可以提取的特征有哪些? ##### 参考答案 颜色、纹理(粗糙度、方向度、对比度)、形状(曲率、离心率、主轴方向)、色彩等。 #

面试题6

衡量图像重建好坏的标准有哪些?怎样计算? ##### 参考答案 * SNR(信噪比) * PSNR=10*log10((2^n-1)^2/MSE) (MSE是原图像与处理图像之间均方误差,所以计算PSNR需要2幅图像的数据!) * SSIM (结构相似性分别从亮度对比度、对比度、结构3方面度量图像的相似性) #

面试题7

AAC和PCM的区别? ##### 参考答案 AAC在数据开始时候加了一些参数:采样率、声道、采样大小 #

面试题8

H264存储的两个形态? ##### 参考答案 * a. Annex B : >StartCode :NALU单元,开头一般是0001或者001
>防竞争字节:为了区分 0 0 0 1,它采用0 0 0 0x3 1作为区分
>多用于网络流媒体中:rtmp、rtp格式 * b. AVCC : >表示NALU长度的前缀,不定长用4、2、1来存储这个NALU的长度
>防竞争字节
>多用于文件存储中mp4的格式 #

面试题9

FFMPEG:图片如何合成视频 ##### 参考答案 编码流程: 1. av_register_all 2. 为AVFormatContext 分配内存 3. 打开文件 4. 创建输出码流AVSream 5. 找到编码器 6. 打开编码器 7. 写文件头,没有的就不写入 8. 循环编码视频像素数据->视频压缩数据 >* 循环编码音频采样数据->音频压缩数据 ———>AVFrame转化为AVPacket > >9. 将编码后的视频码流写入文件 ——>AVPacket转化为AVFormat函数 10. 关闭编码器 11. 写文件尾 12. 关闭资源文件 解码流程: 1. av_register_all 2. 创建AVFormatContext的对象上下文 3. 打开文件 4. avformat_find_stream_info 5. 找到解码器 6. 打开解码器 7. 创建AVCodecContext上下文 8. av_read_frame :将avPacket数据转换为avFrame数据 glUniform1i() ——>这个可以设置对应纹理的第几层 glTexSubImage2D() 和glTexImage2D区别————>替换纹理的内容 #

面试题10

常见的音视频格式有哪些? ##### 参考答案 1. MPEG(运动图像专家组)是Motion Picture Experts Group 的缩写。这类格式包括了MPEG-1,MPEG-2和MPEG-4在内的多种视频格式。 2. AVI,音频视频交错(Audio Video Interleaved)的英文缩写。AVI这个由微软公司发布的视频格式,在视频领域可以说是最悠久的格式之一。 3. MOV,使用过Mac机的朋友应该多少接触过QuickTime。QuickTime原本是Apple公司用于Mac计算机上的一种图像视频处理软件。 4. ASF(Advanced Streaming format高级流格式)。ASF 是MICROSOFT 为了和的Real player 竞争而发展出来的一种可以直接在网上观看视频节目的文件压缩格式。 5. WMV,一种独立于编码方式的在Internet上实时传播多媒体的技术标准,Microsoft公司希望用其取代QuickTime之类的技术标准以及WAV、AVI之类的文件扩展名。 6. NAVI,如果发现原来的播放软件突然打不开此类格式的AVI文件,那你就要考虑是不是碰到了n AVI。n AVI是New AVI 的缩写,是一个名为Shadow Realm 的地下组织发展起来的一种新视频格式。 7. 3GP是一种3G流媒体的视频编码格式,主要是为了配合3G网络的高传输速度而开发的,也是目前手机中最为常见的一种视频格式。 8. REAL VIDEO(RA、RAM)格式由一开始就是定位在视频流应用方面的,也可以说是视频流技术的始创者。 9. MKV,一种后缀为MKV的视频文件频频出现在网络上,它可在一个文件中集成多条不同类型的音轨和字幕轨,而且其视频编码的自由度也非常大,可以是常见的DivX、XviD、3IVX,甚至可以是RealVideo、QuickTime、WMV 这类流式视频。 10. FLV是FLASH VIDEO的简称,FLV流媒体格式是一种新的视频格式。由于它形成的文件极小、加载速度极快,使得网络观看视频文件成为可能,它的出现有效地解决了视频文件导入Flash后,使导出的SWF文件体积庞大,不能在网络上很好的使用等缺点。 11. F4V,作为一种更小更清晰,更利于在网络传播的格式,F4V已经逐渐取代了传统FLV,也已经被大多数主流播放器兼容播放,而不需要通过转换等复杂的方式。 #

面试题11

请指出“1080p”的意义? #

面试题12

请解释颜色的本质及其数字记录原理,并说出几个你所知道的色域。 #

面试题13

请解释“矢量图”和“位图”的区别? #

面试题14

请从“光圈”“快门速度”“感光度”“白平衡”“景深”中任选2个进行叙述? #

面试题15

视频分量YUV的意义及数字化格式? ##### 参考答案 4:2:0;4:1:1;4:2:2;4:4:4;多种 #

面试题16

在MPEG标准中图像类型有哪些? ##### 参考答案 I帧图像, P帧图像, B帧图像 #

面试题17

列举一些音频编解码常用的实现方案? ##### 参考答案 * 第一种就是采用专用的音频芯片对 语音信号进行采集和处理,音频编解码算法集成在硬件内部,如 MP3 编解码芯片、语音合成 分析芯片等。使用这种方案的优点就是处理速度块,设计周期短;缺点是局限性比较大,不灵活,难以进行系统升级。 * 第二种方案就是利用 A/D 采集卡加上计算机组成硬件平台,音频编解码算法由计算机上的软件来实现。使用这种方案的优点是价格便 宜,开发灵活并且利于系统的升级;缺点是处理速度较慢,开发难度较大。 * 第三种方案是使用高精度、高速度 的 A/D 采集芯片来完成语音信号的采集,使用可编程的数据处理能力强的芯片来实现语音信号处理的算法,然后 用 ARM 进行控制。采用这种方案的优点是系统升级能力强,可以兼容多种音频压缩格式甚至未来的音频压缩格 式,系统成本较低;缺点是开发难度较大,设计者需要移植音频的解码算法到相应的 ARM 芯 片中去。 #

面试题18

请叙述MPEG视频基本码流结构? ##### 参考答案 1. Sequence Header 2. Sequence Extention 3. Group of picture Header 4. Picture Header 5. Picture coding extension #

面试题19

sps和pps的区别? ##### 参考答案 SPS是序列参数集 0x67 PPS是图像参数集 0x68 在SPS序列参数集中可以解析出图像的宽,高和帧率等信息。而在h264文件中,最开始的两帧数据就是SPS和PPS,这个h264文件只存在一个SPS帧和一个PPS帧。 #

面试题20

请叙述AMR基本码流结构? ##### 参考答案 AMR文件由文件头和数据帧组成,文件头标识占6个字节,后面紧跟着就是音频帧; 格式如下所示: 文件头(占 6 字节)| :--- | 语音帧1 | 语音帧2 | … | * 文件头: 单声道和多声道情况下文件的头部是不一致的,单声道情况下的文件头只包括一个Magic number,而多声道情况下文件头既包含Magic number,在其之后还包含一个32位的Chanel description field。多声道情况下的32位通道描述字符,前28位都是保留字符,必须设置成0,最后4位说明使用的声道个数。 * 语音数据: 文件头之后就是时间上连续的语音帧块了,每个帧块包含若干个8位组对齐的语音帧,相对于若干个声道,从第一个声道开始依次排列。每一个语音帧都是从一个8位的帧头开始:其中P为填充位必须设为0,每个帧都是8位组对齐的。 #

面试题21

说一说ffmpeg的数据结构? ##### 参考答案 ffmpeg的数据结构可以分为以下几类: ![image](https://user-images.githubusercontent.com/87458342/127322083-11b41004-9943-45e5-879c-978e8cfd089f.png) * (1)解协议(http,rtsp,rtmp,mms) AVIOContext,URLProtocol,URLContext主要存储视音频使用的协议的类型以及状态。 URLProtocol存储输入音视频使用的封装格式。每种协议都对应一个URLProtocol结构。(注意:FFMPEG中文件也被当做一种协议“file”) * (2)解封装(flv,avi,rmvb,mp4) AVFormatContext主要存储视音频封装格式中包含的信息 ffmpeg支持各种各样的音视频输入和输出文件格式(例如FLV, MKV, MP4, AVI),而 AVInputFormat和AVOutputFormat 结构体则保存了这些格式的信息和一些常规设置。 * (3)解码(h264,mpeg2,aac,mp3) AVStream是存储每一个视频/音频流信息的结构体。 AVCodecContext: 编解码器上下文结构体,存储该视频/音频流使用解码方式的相关数据。 AVCodec: 每种视频(音频)编解码器(例如H.264解码器)对应一 个该结构体。 三者的关系如下图: ![image](https://user-images.githubusercontent.com/87458342/127322117-99c30273-f25f-42a6-b9cc-16f5358ecf50.png) * (4)存数据 对于视频,每个结构一般是存一帧;音频可能有好几帧 * 解码前数据:AVPacket * 解码后数据:AVFrame #

面试题22

说一说AVFormatContext 和 AVInputFormat之间的关系? ##### 参考答案 ![image](https://user-images.githubusercontent.com/87458342/127322362-0ff60540-c900-45b9-aa77-77835d7f7386.png) * AVInputFormat被封装在AVFormatContext里 * AVFormatContext 作为API被外界调用 * AVInputFormat 主要是FFmpeg内部调用 * AVFormatContext里保存了视频文件封装格式相关信息,它是负责储存数据的结构体。而AVInputFormat代表了各个封装格式,属于方法,这是一种面向对象的封装。 通过 int avformat_open_input(AVFormatContext **ps, const char *filename,AVInputFormat *fmt, AVDictionary **options)函数装载解封装器. AVFormatContext 和 AVInputFormat之间的关系 #

面试题23

说一说AVFormatContext, AVStream和AVCodecContext之间的关系? ##### 参考答案 ![image](https://user-images.githubusercontent.com/87458342/127322480-74df8276-4929-44f5-a350-feebcfcaa0c4.png) AVStream和AVpacket中都有index字段用于区分不同的码流(视频、音频、字幕等),AVFormatContext中包含输入的AVStream数组用于记录各个码流,nb_streams记录输入码流的数量。AVCodecContext记录着AVStream需要用那种解码器来进行解码。 #

面试题24

说一说视频拼接处理步骤?(细节处理,比如分辨率大小不一,时间处理等等) ##### 参考答案 解封装、解码、决定分辨率大小、编码、时间处理、封装。 #

面试题25

NV21如何转换成I420? ##### 参考答案 >首先需要明白为什么需要将NV21转换成I420,这是因为x264只支持编码I420的数据。 >实际上就是YUV420p与YUV420sp之间的转换。 >YUV420p与YUV420sp的相关知识请参考:《音视频基础知识-YUV图像》 #

面试题26

DTS与PTS共同点? ##### 参考答案 * PTS就是Presentation Time Stamp也就说这个帧什么时候会放在显示器上; * DTS就是Decode Time Stamp,就是说这个帧什么时候被放在编码器去解。 在没有B帧的情况下,DTS和PTS的输出顺序是一样的。 #

面试题27

影响视频清晰度的指标有哪些? ##### 参考答案 帧率 码率 分辨率 量化参数(压缩比) #

面试题28

编解码处理时遇到什么困难? ##### 参考答案 ffmpeg 在编解码的过程: * 使用 ffmpeg 的 libavcoder,libavformat 的库及可能性编解码 * 编码过程: * 采集出来的视频、音频数据使用 ffmpeg 进行压缩 * 进行按连续的视频,音频进行分组打包,为了区分各种包,也要包加上包头,包头写上显示时间戳 PTS,解码时间戳 DTS,通过网络传输到播放端 * 解码过程: * 通过 TCP 协议接收到媒体流,FFmpeg 解封装,解码 * 最终获取到原始视频 YUV,音频 PCM 格式,利用播放器进行播放 #

面试题29

如何秒开视频?什么是秒开视频? ##### 参考答案 * 1. 什么是秒开视频? 秒开是指用户点击播放到看到画面的时间非常短,在 1 秒之内。 * 2. 为什么需要秒开? 目前主流的直播协议是 RTMP,HTTP-FLV 和 HLS,都是基于 TCP 的长连接。在播放的过程中,若播放端所处的网络环境在一个较佳的状态,此时播放会很流畅。若网络环境不是很稳定,经常会发生抖动,如果播放端没有特殊处理,可能会经常发生卡顿,严重的甚至会出现黑屏。而移动直播由于其便捷性,用户可以随时随地发起和观看直播,我们无法保证用户的网络一直处于非常好的状态,所以,在网络不稳定的情况下保证播放的流畅度是非常重要的。 * 3. 解决思路 * 3.1 获取关键帧后显示 改写播放器逻辑让播放器拿到第一个关键帧后就给予显示。 GOP 的第一个帧通常都是关键帧,由于加载的数据较少,可以达到 "首帧秒开"。如果直播服务器支持 GOP 缓存,意味着播放器在和服务器建立连接后可立即拿到数据,从而省却跨地域和跨运营商的回源传输时间。 GOP 体现了关键帧的周期,也就是两个关键帧之间的距离,即一个帧组的最大帧数。假设一个视频的恒定帧率是 24fps(即 1 秒 24 帧图像),关键帧周期为 2s,那么一个 GOP 就是 48 张图像。一般而言,每一秒视频至少需要使用一个关键帧。 增加关键帧个数可改善画质(GOP通常为 FPS 的倍数),但是同时增加了带宽和网络负载。这意味着,客户端播放器下载一个 GOP,毕竟该 GOP 存在一定的数据体积,如果播放端网络不佳,有可能不是能够快速在秒级以内下载完该 GOP,进而影响观感体验。 如果不能更改播放器行为逻辑为首帧秒开,直播服务器也可以做一些取巧处理,比如从缓存 GOP 改成缓存双关键帧(减少图像数量),这样可以极大程度地减少播放器加载 GOP 要传输的内容体积。 * 3.2 app 业务逻辑层面优化 比如提前做好 DNS 解析(省却几十毫秒),和提前做好测速选线(择取最优线路)。经过这样的预处理之后,在点击播放按钮时,将极大提高下载性能。 一方面,可以围绕传输层面做性能优化;另一方面,可以围绕客户播放行为做业务逻辑优化。两者可以有效的互为补充,作为秒开的优化空间。 * 4. 秒开视频方案 * 4.1 优化服务器策略 播放器接入服务器请求数据的时间点的视频不一定是关键帧,那么需要等到下一个关键帧的到来,如果关键帧的周期是 2s 的话,那么等待的时间可能会在 0~2s 的范围内,这段等待的时间会影响首屏的加载时间。如果服务器有缓存,则播放端在接入的时候,服务器可以向前找最近的关键帧发给播放端,这样就可以省去等待的时间,可以大大的减少首屏的加载时间。 * 4.2 优化播放端策略 播放端请求到的第一帧数据肯定是关键帧,关键帧能够通过帧内参考进行解码。这样播放端就可以在接收到第一个关键帧的时候就立即开始解码显示,而不需要等到缓存一定数量的视频帧才开始解码,这样也能减少首屏画面显示的时间。 * 5 播放端首屏时长的优化 播放器的首屏过程中的几个步骤: >* 首屏时间,指的是从进入直播间开始到第一次看到直播画面的时间。首屏时间过长极易导致用户失去对直播的耐心,降低用户的留存。但游戏直播对画面质量和连贯性的要求高,对应推流端编码后的数据量和其他类型直播相比大的多,如何降低首屏时间是一个不小的难题。 >* 在播放端的首屏过程中,主要有以下三个操作需要进行:加载直播间 UI(包括播放器本身)、下载直播数据流(未解码)和解码数据播放。其中数据解码播放又细分为以下几个步骤: >* 检测传输协议类型(RTMP、RTSP、HTTP 等)并与服务器建立连接接收数据; >* 视频流解复用得到音视频编码数据(H.264/H.265、AAC 等); >* 音视频数据解码,音频数据同步至外设,视频数据渲染都屏幕,至此,视频开始播放,首屏时间结束。 * 总结: 首先,加载 UI 可以以单例的方式进行,能够一定程度地提升首屏展示速度;其次,可以预设解码类型,减少数据类型检测时间;再次,设定合理的下载缓冲区大小,尽可能减少下载的数据量,当检测到 I 帧数据,立即开始解码单帧画面进行播放,提高首屏展示时间。 #

面试题30

如何降低延迟?如何保证流畅性?如何解决卡顿?解决网络抖动? ##### 参考答案 * 1. 产生原因 保证直播的流畅性是指在直播过程中保证播放不发生卡顿,卡顿是指在播放过程中声音和画面出现停滞,非常影响用户体验。造成卡顿的原因有几种情况: 推流端网络抖动导致数据无法发送到服务器,造成播放端卡顿; 播放端网络抖动导致数据累计在服务器上拉不下来,造成博凡卡顿。 由于从服务器到播放器的网络情况复杂,尤其是在 3G 和带宽较差的 WIFI 环境下,抖动和延迟经常发生,导致播放不流畅,播放不流畅带来的负面影响就是延时增大。如何在网络抖动的情况下保证播放的流畅性和实时性是保障直播性能的难点。 * 2. 流畅度优化 目前主流的直播协议是 RTMP、HTTP-FLV 和 HLS,都是基于 TCP 的长连接。在播放的过程中,若播放端所处的网络环境在一个较佳的状态,此时播放会很流畅。若网络环境不是很稳定,经常会发生抖动,如果播放端没有做特殊处理,可能会经常发生卡顿,严重的甚至会出现黑屏。而移动直播由于其便捷性,用户可以随时随地发起和观看直播,我们无法保证用户的网络一直处于一个非常好的状态,所以,在网络不稳定的情况下保证播放的流畅度是非常重要的。 为了解决这个问题,首先播放器需要将拉流线程和解码线程分开,并建立一个缓冲队列用于缓冲音视频数据。拉流线程将从服务器上获取到的音视频流放入队列,解码线程从队列中获取音视频数据进行解码播放,队列的长度可以调整。当网络发生抖动时,播放器无法从服务器上获取到数据或获取数据的速度较慢,此时队列中缓存的数据可以起到一个过渡的作用,让用户感觉不到网络发生了抖动。 当然这是对于网络发生抖动的情况所采取的策略,如果播放端的网络迟迟不能恢复或服务器的边缘结点出现宕机,则需要应用层进行重连或调度。 #

面试题31

预测编码的基本原理是什么? ##### 参考答案 预测编码是数据压缩理论的一个重要分支。根据离散信号之间存在一定相关性特点,利用前面的一个或多个信号对下一个信号进行预测,然后对实际值和预值的差(预测误差)进行编码。如果预测比较准确,那么误差信号就会很小,就可以用较少的码位进行编码,以达到数据压缩的目的。 >* 原理:利用以往的样本值对新样本值进行预测,将新样本值的实际值与其预测值相减,得到误差值,对该误差值进行编码,传送此编码即可。理论上数据源可以准确地用一个数学模型表示,使其输出数据总是与模型的输出一致,因此可以准确地预测数据,但是实际上预测器不可能找到如此完美的数学模型;预测本身不会造成失真。误差值的編码可以采用无失真压縮法或失真压縮法。 #

面试题32

需要把网络上一段视频存储下来(比如作为mp4 ), 请实现并说出方法(第一个视频需要翻墙才能进)? #

面试题33

需要把网络上一段语音存储下来(比如作为mp3 ), 请实现并说出方法? #

面试题34

为什么要有YUV这种数据出来?(YUV相比RGB来说的优点) ##### 参考答案 RGB是指光学三原色红、绿和蓝,通过这3种的数值(0-255)改变可以组成其他颜色,全0时为黑色,全255时为白色。RGB是一种依赖于设备的颜色空间:不同设备对特定RGB值的检测和重现都不一样,因为颜色物质(荧光剂或者染料)和它们对红、绿和蓝的单独响应水平随着制造商的不同而不同,甚至是同样的设备不同的时间也不同。 YUV,是一种颜色编码方法。常使用在各个视频处理组件中。三个字母分别表示亮度信号Y和两个色差信号R-Y(即U)、B-Y(即V),作用是描述影像色彩及饱和度,用于指定像素的颜色。Y'UV的发明是由于彩色电视与黑白电视的过渡时期。黑白视频只有Y视频,也就是灰阶值。与我们熟知的RGB类似,YUV也是一种颜色编码方法,主要用于电视系统以及模拟视频领域,它将亮度信息(Y)与色彩信息(UV)分离,没有UV信息一样可以显示完整的图像,只不过是黑白的,这样的设计很好地解决了彩色电视机与黑白电视的兼容问题。并且,YUV不像RGB那样要求三个独立的视频信号同时传输,所以用YUV方式传送占用极少的频宽。 YUV和RGB是可以相互转换的,基本上所有图像算法都是基于YUV的,所有显示面板都是接收RGB数据。 #

面试题35

H264/H265有什么区别? ##### 参考答案 同样的画质和同样的码率,H.265比H2.64 占用的存储空间要少理论50%。如果存储空间一样大,那么意味着,在一样的码率下H.265会比H.264 画质要高一些理论值是30%~40%。 比起H.264,H.265提供了更多不同的工具来降低码率,以编码单位来说,最小的8x8到最大的64x64。信息量不多的区域(颜色变化不明显)划分的宏块较大,编码后的码字较少,而细节多的地方划分的宏块就相应的小和多一些,编码后的码字较多,这样就相当于对图像进行了有重点的编码,从而降低了整体的码率,编码效率就相应提高了。 H.265标准主要是围绕着现有的视频编码标准H.264,在保留了原有的某些技术外,增加了能够改善码流、编码质量、延时及算法复杂度之间的关系等相关的技术。H.265研究的主要内容包括,提高压缩效率、提高鲁棒性和错误恢复能力、减少实时的时延、减少信道获取时间和随机接入时延、降低复杂度。 #

面试题36

视频或者音频传输,你会选择TCP协议还是UDP协议?为什么? ##### 参考答案 选择UDP协议,UDP实时性好。TCP要保证丢失的package会被再次重发,确保对方能够收到。 而在视频播放中,如果有一秒钟的信号确实,导致画面出现了一点瑕疵,那么最合适的办法是把这点瑕疵用随便哪些信号补充上,这样虽然画面有一点点瑕疵但是不影响观看。如果用的TCP的话,这点缺失的信号会被一遍又一遍的发送过来直到接收端确认收到。这不是音视频播放所期待的。而UDP就很适合这种情况。UDP不会一遍遍发送丢失的package。 #

面试题37

平时说的软解和硬解,具体是什么? ##### 参考答案 硬解就是硬件解码,指利用GPU来部分代替CPU进行解码,软解就是软件解码,指利用软件让CPU来进行解码。两者的具体区别如下所示: 硬解码:是将原来全部交由CPU来处理的视频数据的一部分交由GPU来做,而GPU的并行运算能力要远远高于CPU,这样可以大大的降低对CPU的负载,CPU的占用率较低了之后就可以同时运行一些其他的程序了,当然,对于较好的处理器来说,比如i5 2320,或者AMD 任何一款四核心处理器来说,硬解和软件的区别只是个人偏好问题了吧。   软解码:即通过软件让CPU来对视频进行解码处理;而硬解码:指不借助于CPU,而通过专用的子卡设备来独立完成视频解码任务。曾经的VCD/DVD解压卡、视频压缩卡等都隶属于硬解码这个范畴。而现如今,要完成高清解码已经不再需要额外的子卡,因为硬解码的模块已经被整合到显卡GPU的内部,所以目前的主流显卡(集显)都能够支持硬解码技术。 #

面试题38

何为直播?何为点播? ##### 参考答案 直播:是一个三方交互(主播、服务器、观众),这个交互式实时的!尽管会根据选择的协议不同而有一些延迟,但我们仍认为它直播是实时的!--->主播在本地发送音视频给服务器(推流),观众从服务器实时解码(拉流)收看收听主播发送给服务器的音视频(直播内容)。直播是不能快进的点播:首先一定要明确的一点,点播不存在推流这一过程,你本身你的流已经早就推给服务器了,或者这么说也不对,应该是你的音视频早就上传到了服务器,观众只需要在线收看即可,由于你的音视频上传到了服务器,观众则可以通过快进,快退,调整进度条等方式进行收看!  #

面试题39

简述推流、拉流的工作流程? ##### 参考答案 推流:在直播中,一方向服务器发送请求,向服务器推送自己正在实时直播的数据,而这些内容在推送到服务器的这一过程中是以 “流” 的形式传递的,这就是“推流”,把音视频数据以流的方式推送(或上传)到服务器的过程就是“推流”! 推流方的音视频往往会很大,在推流的过程中首先按照 aac音频-编码 和 h264视【公众平台不能出现视频这两个字,真是坑】频-编码的标准把推过来的音视频压缩 ,然后合并成 MP4或者 FLV格式,然后根据直播的封装协议,最后传给服务器完成推流过程。  拉流:与推流正好相反,拉流是用户从服务器获取推流方给服务器的音视频的过程,这就是“拉流”!拉流首先aac音频-解码 和 h.264视【公众平台不能出现视频这两个字,真是坑】 频-解码的内部把推过来的音视频解压缩,然后合成 MP4或者 FLV 格式,再解封装,最后到我们的客户端与观众进行交互。 #

面试题40

如何在直播中I帧间隔设置、与帧率分辨率选定? #

面试题41

直播推流中推I帧与推非I帧区别是什么? #

面试题42

常见的直播协议有哪些?之间有什么区别? #

面试题43

点播中常见的数据传输协议主要有哪些? #

面试题44

RTMP、HLS协议各自的默认端口号是? #

面试题45

简述RTMP协议,如何封装RTMP包? #

面试题46

m3u8构成是?直播中m3u8、ts如何实时更新? #

面试题47

何为音视频同步,音视频同步是什么标准? #

面试题48

播放器暂停、快进快退、seek、逐帧、变速怎么实现? #

面试题49

说说你平时在播放过程中做的优化工作? #

面试题50

你研究过哪些具体的流媒体服务器,是否做过二次开发? ================================================ FILE: case_interview/002-README.md ================================================ #

面试题51

什么是GOP? ##### 参考答案 GOP ( Group of Pictures ) 是一组连续的画面,由一张 I 帧和数张 B / P 帧组成,是视频图像编码器和解码器存取的基本单位。 也就是说GOP组是指一个关键帧I帧所在的组的长度,每个 GOP 组只有 1 个 I 帧。 GOP 组的长度格式也决定了码流的大小。 GOP越大,中间的P帧和B帧的数量就越多,所以解码出来的视频质量就越高,但是会影响编码效率。 #

面试题52

音频测试的测试点,音频时延如何测试? ##### 参考答案 测试点:功能,性能,兼容性,耗电量,安全性,压力测试,客观音质POLQA分,音质主观体验,主播到观众时延,观众到观众时延,3A效果 音频时延:通过音频线将2个被测对象连接在电脑,用PESQ脚本算出音频时延 #

面试题53

美颜的实现原理,具体实现步骤? #

面试题54

如何直播APP抓包过来的文件,如何过滤上行,下行,总码率?
上行:ip.src192.168.x.x
下行:ip.dst192.168.x.x
总码率:ip.src192.168.x.x and ip.dst192.168.x.x #

面试题55

如何测试一个美颜挂件? #

面试题56

为什么要用FLV? ##### 参考答案 是因为传输的协议要求,RTMP协议只支持FLV格式流 #

面试题57

如何测试一个美颜挂件? #

面试题58

平常的视频格式? ##### 参考答案 MP4/RMVB/FLY/AVI/MOV/MKV等 #

面试题59

何为homebrew?你用它安装过什么?常用命令有哪些? ##### 参考答案 homebrew是一个 Mac系统下所独有的套件管理器,我要做直播,需要 rtmp 和 nginx ,单独安装很复杂,只要在终端里输入简单的安装相应的套件命令即可完成安装,复杂的过程都靠 homebrew 规避掉了!我用它安装过很多东西,比如nginx 搭建流媒体服务器等。常用命令:brew install 、brew uninstall、brew search、brew list、brew update、brew help 等~ #

面试题60

RTMP、HLS协议各自的默认端口号是? ##### 参考答案 RTMP端口号:1935 HLS端口号 :8080 ================================================ FILE: ffmpeg常用命令.md ================================================ # ffmpeg常用命令 ## `ffmpeg --help`大概分为6个部分,具体如下: - ffmpeg信息查询部分 - 公共操作参数部分 - 文件主要操作参数部分 - 视频操作参数部分 - 音频操作参数部分 - 字母操作参数部分 ## 查看支持的容器格式 ``` # 封装和解封装 ffmpeg -formats # 解封装 ffmpeg -demuxers # 封装 ffmpeg -muxers # 查看FLV封装器的参数支持 ffmpeg -h muxer=flv # 查看FLV解封装器的参数支持 ffmpeg -h demuxer=flv ``` ## 查看支持的编解码格式 ``` # 编解码 ffmpeg -codecs # 解码 ffmpeg -decoders # 编码 ffmpeg -encoders # 查看H.264(AVC)的编码参数支持 ffmpeg -h encoder=h264 # 查看H.264(AVC)的解码参数支持 ffmpeg -h decoder=h264 ``` ## 查看支持的滤镜 ``` # 滤镜 ffmpeg -filters # 查看colorkey滤镜的参数支持 ffmpeg -h filter=colorkey ``` ## 转码 ``` ffmpeg -i WMV9_1280x720.wmv -vcodec mpeg4 -b:v 200 -r 15 -an output.mp4 # -i 文件 (后缀名)封装格式 # -vcodec 视频编码格式 # -b:v 视频码率 # -r 视频帧率 # -an 不包括音频 ``` **a) 通用选项** - -L license - -h 帮助 - -fromats 显示可用的格式,编解码的,协议的... - -f fmt 强迫采用格式fmt - -I filename 输入文件 - -y 覆盖输出文件 - -t duration 设置纪录时间 hh:mm:ss[.xxx]格式的记录时间也支持 - -ss position 搜索到指定的时间 [-]hh:mm:ss[.xxx]的格式也支持 - -title string 设置标题 - -author string 设置作者 - -copyright string 设置版权 - -comment string 设置评论 - -target type 设置目标文件类型(vcd,svcd,dvd) 所有的格式选项(比特率,编解码以及缓冲区大小)自动设置,只需要输入如下的就可以了:ffmpeg -i myfile.avi -target vcd /tmp/vcd.mpg - -hq 激活高质量设置 - -itsoffset offset 设置以秒为基准的时间偏移,该选项影响所有后面的输入文件。该偏移被加到输入文件的时戳,定义一个正偏移意味着相应的流被延迟了 offset秒。 [-]hh:mm:ss[.xxx]的格式也支持 **b) 视频选项** - -b bitrate 设置比特率,缺省200kb/s - -r fps 设置帧频 缺省25 - -s size 设置帧大小 格式为WXH 缺省160X128.下面的简写也可以直接使用: - Sqcif 128X96 qcif 176X144 cif 252X288 4cif 704X576 - -aspect aspect 设置横纵比 4:3 16:9 或 1.3333 1.7777 - -croptop size 设置顶部切除带大小 像素单位 - -cropbottom size –cropleft size –cropright size - -padtop size 设置顶部补齐的大小 像素单位 - -padbottom size –padleft size –padright size –padcolor color 设置补齐条颜色(hex,6个16进制的数,红:绿:兰排列,比如 000000代表黑色) - -vn 不做视频记录 - -bt tolerance 设置视频码率容忍度kbit/s - -maxrate bitrate设置最大视频码率容忍度 - -minrate bitreate 设置最小视频码率容忍度 - -bufsize size 设置码率控制缓冲区大小 - -vcodec codec 强制使用codec编解码方式。如果用copy表示原始编解码数据必须被拷贝。 - -sameq 使用同样视频质量作为源(VBR) - -pass n 选择处理遍数(1或者2)。两遍编码非常有用。第一遍生成统计信息,第二遍生成精确的请求的码率 - -passlogfile file 选择两遍的纪录文件名为file **c)高级视频选项** - -g gop_size 设置图像组大小 - -intra 仅适用帧内编码 - -qscale q 使用固定的视频量化标度(VBR) - -qmin q 最小视频量化标度(VBR) - -qmax q 最大视频量化标度(VBR) - -qdiff q 量化标度间最大偏差 (VBR) - -qblur blur 视频量化标度柔化(VBR) - -qcomp compression 视频量化标度压缩(VBR) - -rc_init_cplx complexity 一遍编码的初始复杂度 - -b_qfactor factor 在p和b帧间的qp因子 - -i_qfactor factor 在p和i帧间的qp因子 - -b_qoffset offset 在p和b帧间的qp偏差 - -i_qoffset offset 在p和i帧间的qp偏差 - -rc_eq equation 设置码率控制方程 默认tex^qComp - -rc_override override 特定间隔下的速率控制重载 - -me method 设置运动估计的方法 可用方法有 zero phods log x1 epzs(缺省) full - -dct_algo algo 设置dct的算法 可用的有 0 FF_DCT_AUTO 缺省的DCT 1 FF_DCT_FASTINT 2 FF_DCT_INT 3 FF_DCT_MMX 4 FF_DCT_MLIB 5 FF_DCT_ALTIVEC - -idct_algo algo 设置idct算法。可用的有 0 FF_IDCT_AUTO 缺省的IDCT 1 FF_IDCT_INT 2 FF_IDCT_SIMPLE 3 FF_IDCT_SIMPLEMMX 4 FF_IDCT_LIBMPEG2MMX 5 FF_IDCT_PS2 6 FF_IDCT_MLIB 7 FF_IDCT_ARM 8 FF_IDCT_ALTIVEC 9 FF_IDCT_SH4 10 FF_IDCT_SIMPLEARM - -er n 设置错误残留为n 1 FF_ER_CAREFULL 缺省 2 FF_ER_COMPLIANT 3 FF_ER_AGGRESSIVE 4 FF_ER_VERY_AGGRESSIVE - -ec bit_mask 设置错误掩蔽为bit_mask,该值为如下值的位掩码 1 FF_EC_GUESS_MVS (default=enabled) 2 FF_EC_DEBLOCK (default=enabled) - -bf frames 使用frames B 帧,支持mpeg1,mpeg2,mpeg4 - -mbd mode 宏块决策 0 FF_MB_DECISION_SIMPLE 使用mb_cmp 1 FF_MB_DECISION_BITS 2 FF_MB_DECISION_RD - -4mv 使用4个运动矢量 仅用于mpeg4 - -part 使用数据划分 仅用于mpeg4 - -bug param 绕过没有被自动监测到编码器的问题 - -strict strictness 跟标准的严格性 - -aic 使能高级帧内编码 h263+ - -umv 使能无限运动矢量 h263+ - -deinterlace 不采用交织方法 - -interlace 强迫交织法编码仅对mpeg2和mpeg4有效。当你的输入是交织的并且你想要保持交织以最小图像损失的时候采用该选项。可选的方法是不交织,但是损失更大 - -psnr 计算压缩帧的psnr - -vstats 输出视频编码统计到vstats_hhmmss.log - -vhook module 插入视频处理模块 module 包括了模块名和参数,用空格分开 **D)音频选项** - -ab bitrate 设置音频码率 - -ar freq 设置音频采样率 - -ac channels 设置通道 缺省为1 - -an 不使能音频纪录 - -acodec codec 使用codec编解码 **E)音频/视频捕获选项** - -vd device 设置视频捕获设备。比如/dev/video0 - -vc channel 设置视频捕获通道 DV1394专用 - -tvstd standard 设置电视标准 NTSC PAL(SECAM) - -dv1394 设置DV1394捕获 - -av device 设置音频设备 比如/dev/dsp **F)高级选项** - -map file:stream 设置输入流映射 - -debug 打印特定调试信息 - -benchmark 为基准测试加入时间 - -hex 倾倒每一个输入包 - -bitexact 仅使用位精确算法 用于编解码测试 - -ps size 设置包大小,以bits为单位 - -re 以本地帧频读数据,主要用于模拟捕获设备 - -loop 循环输入流(只工作于图像流,用于ffserver测试) # ffprobe常用命令 ## `-show_packets` 查看多媒体数据包信息 | 字段 | 说明 | | :------------ | :------------------------------------------- | | codec_type | 多媒体类型,如视频包、音频包等 | | stream_index | 多媒体的stream索引 | | pts | 多媒体的显示时间值 | | pts_time | 根据不同格式计算过后的多媒体的显示时间 | | dts | 多媒体解码时间值 | | dts_time | 根据不同格式计算过后的多媒体的解码时间 | | duration | 多媒体包占用的时间值 | | duration_time | 根据不同格式计算过后的多媒体包所占用的时间值 | | size | 多媒体包的大小 | | pos | 多媒体包所在的文件偏移位置 | | flags | 多媒体包标记,如关键包与非关键包的标记 | ## `-show_format` 查看多媒体的封装格式 | 字段 | 说明 | | :--------------- | :------------------- | | filename | 文件名 | | nb_streams | 媒体中包含的流的个数 | | nb_programs | 节目数 | | format_name | 使用的封装模块的名称 | | format_long_name | 封装的完整名称 | | start_time | 媒体文件的起始时间 | | duration | 媒体文件的总时间长度 | | size | 媒体文件的大小 | | bit_rate | 媒体文件的码率 | ## `-show_frames` 查看视频文件中的帧信息 | 属性 | 说明 | 值 | | :---------------- | :----------------------------- | :------- | | media_type | 帧的类型(视频、音频、字幕等) | video | | stream_index | 帧所在的索引区域 | 0 | | key_frame | 是否为关键帧 | 1 | | pkt_pts | Frame包的pts | 0 | | pkt_pts_time | Frame包的pts的时间显示 | 0.080000 | | pkt_dts | Frame包的dts | 80 | | pkt_dts_time | Frame包的dts的时间显示 | 0.080000 | | pkt_duration | Frame包的时长 | N/A | | pkt_duration_time | Frame包的时长时间显示 | N/A | | pkt_pos | Frame包所在文件的偏移位置 | 344 | | width | 帧显示的宽度 | 1280 | | height | 帧显示的高度 | 714 | | pix_fmt | 帧的图像色彩格式 | yuv420p | | pict_type | 帧类型 | I | ## `-show_streams` 查看多媒体文件中的流信息 | 属性 | 说明 | 值 | | :--------------- | :-------------------------------- | :------------- | | index | 流所在的索引区域 | 0 | | codec_name | 编码名 | h264 | | codec_long_name | 编码全名 | MPEG-4 part 10 | | profile | 编码的profile | High | | level | 编码的level | 31 | | has_b_frames | 包含B帧信息 | 2 | | codec_type | 编码类型 | video | | codec_time_base | 编码的时间戳计算基础单位 | 1/50 | | pix_fmt | 图像显示的色彩格式 | yuv420p | | coded_width | 图像的宽度 | 1280 | | coded_height | 图像的高度 | 714 | | codec_tag_string | 编码的标签数据 | [0][0][0][0] | | r_frame_rate | 实际帧率 | 25/1 | | avg_frame_rate | 平均帧率 | 25/1 | | time_base | 时间基数(用来进行timestamp计算) | 1/1000 | | bit_rate | 码率 | 200000 | | max_bit_rate | 最大码率 | N/A | | nb_frames | 帧数 | N/A | ## `-printf_format`或`-of` 格式化输出支持XML、INI、JSON、CSV、FLAT等 # ffplay ## 可视化 Visualize information exported by some codecs. http://ffmpeg.org/ffmpeg-all.html#codecview https://trac.ffmpeg.org/wiki/Debug/MacroblocksAndMotionVectors ``` ffmpeg -h filter=codecview ``` - Visualize forward predicted MVs of all frames using ``` ffplay -flags2 +export_mvs input.mp4 -vf codecview=mv_type=fp ``` - Visualize multi-directionals MVs of P and B-Frames using ``` ffplay -flags2 +export_mvs input.mp4 -vf codecview=mv=pf+bf+bb ``` # ffmpeg转封装格式 - 需要知道 **源容器** 和 **目标容器** 的可容纳的编码格式 - 编码格式如果相互兼容,可以用`-c copy`拷贝原有的stream `ffmpeg -i input.mp4 -c copy -f flv output.flv` - 编码格式如果不兼容,需要转化成目标文件支持的编码 `ffmpeg -i input_ac3.mp4 -vcodec copy -acodec aac -f flv output.flv` ## HLS 1. FFmpeg转HLS举例 常规的从文件转换HLS直播时: ``` ffmpeg -re -i input.mp4 -c copy -f hls -bsf:v h264_mp4toannexb output.m3u8 # -bsf:v h264_mp4toannexb 作用是将MP4中的H.264数据转换成H.264 AnnexB标准编码,AnnexB标准的编码常见于实时传输流中 ``` 如果源文件为FLV、TS等可以作为直播传输流的视频,则不需要这个参数。 1. ffmpeg推流上传HLS相关的M3U8以及TS文件 Nginx配置webdav模块 ``` ffmpeg -re -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.m3u8 ``` ## 音视频文件音视频流抽取 1. FFmpeg抽取音视频文件中的AAC音频流 `ffmpeg -i input.mp4 -vn -acodec copy output.aac` 2. FFmpeg抽取音视频文件中的H.264视频流 `ffmpeg -i input.mp4 -vcodec copy -an output.h264` 3. FFmpeg抽取音视频文件中的H.265视频流(前提文件视频编码格式为hevc) ``` ffmpeg -i input.mp4 -vcodec copy -an -bsf hevc_mp4toannexb -f hevc output.hevc ``` # ffmpeg转码 ## h264转h265(HEVC) ``` ffmpeg -i input.mp4 -c:v libx265 -vtag hvc1 h265_output.mp4 ``` ## aac转MP3(需要安装libmp3lame) ``` ffmpeg -i AVC_high_1280x720_2013.mp4 -vn -acodec libmp3lame -f mp3 out.mp3 ``` ## x264 ### 安装 ``` $git clone git://git.videolan.org/x264.git $cd x264 $./configure –enable-shared $make $sudo make install ``` ### 查看 ``` x264 --full help ``` ### 设置编码参数 1. 编码器预设参数设置preset 通常通过preset来设置编码的速度,影响清晰度 `ffmpeg -i input.mp4 -vcodec libx264 -preset ultrafast -b:v 2000k output.mp4` 2. H.264编码优化参数tune 在使用ffmpeg与x264进行H.264直播编码并进行推流时,只用tune参数的zerolatency将会提升效率,因为其降低了因编码导致的延迟。 3. H.264的profile与level设置 baseline profile编码的H.264视频不会包含B Slice,而使用main profile、high profile编码出来的视频,均可以包含B Slice `ffmpeg -i input.mp4 -vcodec libx264 -profile:v baseline -level 3.1 -s 352x288 -an -y -t 10 output_baseline.ts` `ffmpeg -i input.mp4 -vcodec libx264 -profile:v high -level 3.1 -s 352x288 -an -y -t 10 output_high.ts` 查看包含B帧的情况: `ffprobe -v quiet -show_frames -select_streams v output_baseline.ts | grep "pict_type=B" | wc -l` 当进行实时流媒体直播时,采用baseline编码相对main或high的profile会更可靠些。 4. 控制场景切花关键帧插入参数 sc_threshold ffmpeg通过-g参数设置以帧数间隔为GOP的长度,但是当遇到场景切换时,例如从一个画面突然变成另一个画面时,会强行插入一个关键帧,这是GOP的间隔将会重新开始,可以通过使用sc_threshold参数进行设定以决定是否在场景切换时插入关键帧。 ffmpeg命令控制编码时的GOP大小 `ffmpeg -i AVC_high_1280x720_2013.mp4 -c:v libx264 -g 50 -t 60 output.mp4` 为了使得GOP的插入更加均匀,使用参数 sc_threshold `ffmpeg -i AVC_high_1280x720_2013.mp4 -c:v libx264 -g 50 -sc_threshold 0 -t 60 -y output.mp4` 5. 设置x264内部参数x264opts 去掉B帧 `ffmpeg -i input.mp4 -c:v libx264 -x264opts "bframes=0" -g 50 -sc_threshold 0 output.mp4` 控制I帧、P帧、B帧的频率与规律 例如设置GOP中,每2个P帧之间存放3个B帧: `ffmpeg -i input.mp4 -c:v libx264 -x264opts "bframes=3:b-adapt=0" -g 50 -sc_threshold 0 output.mp4` 6. CBR 恒定码率设置参数 nal-hrd (固定码率好处,可能是网络传输) VBR:可变码率 CBR:恒定码率 ABR:平均码率。VBR和CBR混合产物。 ``` ffmpeg -i input.mp4 -c:v libx264 -x264opts "bframes=10:b-adapt=0" -b:v 1000k -maxrate 1000k -minrate 1000k -bufsize 50k -nal-hrd cbr -g 50 -sc_threshold 0 output.ts # 设置B帧的个数,并且是每2个P帧之间包含10个B帧 # 设置视频码率为 1000 kbit/s # 设置最大码率为 1000 kbit/s # 设置最小码率为 1000 kbit/s # 设置编码的buffer大小为 50KB # 设置 H.264 的编码HRD信号形式为 CBR # 设置每50帧一个GOP # 设置场景切换不强行插入关键帧 ``` ## MP3/AAC 1. MP3转码 `ffmpeg -i INPUT -acodec libmp3lame output.mp3` 2. 参数控制 ``` # -q 控制码率(0~9) 高->低 ffmpeg -i input.mp3 -acodec libmp3lame -q:a 8 output.mp3 # -b 设置为CBR ffmpeg -i input.mp3 -acodec libmp3lame -b:a 64k output.mp3 # -abr 设置为abr编码 ffmpeg -i input.mp3 -acodec libmp3lame -b:a 64k -abr 1 output.mp3 ``` # ffmpeg流媒体 ## ffmpeg发布与录制RTMP流 FFmpeg操作RTMP的参数 | 参数 | 类型 | 说明 | | :------------- | :----- | :----------------------------------------------------------- | | rtmp_app | 字符串 | RTMP流发布点,又称为APP | | rtmp_buffer | 整数 | 客户端buffer大小(单位:毫秒),默认为3秒 | | rtmp_conn | 字符串 | 在RTMP的Connect命令中增加自定义AMF数据 | | rtmp_flashver | 字符串 | 设置模拟的flashplugin的版本号 | | rtmp_live | 整数 | 指定RTMP流媒体播放类型,具体如下: - any:直播或点播 - live:直播 - recorded:点播 | | rtmp_pageurl | 字符串 | RTMP在Connect命令中设置的PageURL字段,其为播放时所在的Web页面URL | | rtmp_playpath | 字符串 | RTMP流播放的Stream地址,或者成为**秘钥**,或者成为发布流 | | rtmp_subscribe | 字符串 | 直播名称,默认设置为rtmp_playpath的值 | | rtmp_swfhash | 二进制 | 解压swf文件后的SHA256的hash值 | | rtmp_swfsize | 整数 | swf文件解压后的大小,用于swf认证 | | rtmp_swfurl | 字符串 | RTMP的Connect命令中设置的swfURL播放器的URL | | rtmp_swfverify | 字符串 | 设置swf认证时swf文件的URL地址 | | rtmp_tcurl | 字符串 | RTMP的Connect命令中设置的tcURL目标发布点地址,一般形如 rtmp://xxx.xxx.xxx/app | | rtmp_listen | 整数 | 开启RTMP服务时所监听的端口 | | listen | 整数 | 与rtmp_listen相同 | | timeout | 整数 | 监听rtmp端口时设置的超时时间,以秒为单位 | 1. rtmp_app、rtmp_playpath 参数 通过rtmp_app、rtmp_playpath参数设置rtmp的推流发布点 `ffmpeg -re -i AVC_high_1280x720_2013.mp4 -c copy -f flv -rtmp_app live -rtmp_playpath play rtmp://127.0.0.1` 等价于 `ffmpeg -re -i AVC_high_1280x720_2013.mp4 -c copy -f flv rtmp://127.0.0.1/live/play` ## ffmpeg录制RTSP流 FFmpeg操作RTSP的参数 | 参数 | 类型 | 说明 | | :------------------ | :----- | :----------------------------------------------------------- | | initial_pause | 布尔 | 建立连接后暂停播放 | | rtsp_transport | 标记 | 设置RTSP传输协议,具体如下: - udp:UDP - tcp:TCP -udp_multicast:UDP多播协议 - http:HTTP隧道 | | rtsp_flags | 标记 | RTSP使用标记,具体如下: - filter_src:只接收指定IP的流 - listen:设置为被动接收模式 - prefer_tcp:TCP亲和模式,如果TCP可用则首选TCP传输 | | allowed_media_types | 标记 | 设置允许接收的数据模式(默认全部开启),具体如下: - video:只接收视频 - audio:只接收音频 - data:只接收数据 - subtitle:只接收字幕 | | min_port | 整数 | 设置最小本地UDP端口,默认为5000 | | max_port | 整数 | 设置最大本地UDP端口,默认为65000 | | timeout | 整数 | 设置监听端口超时时间 | | reorder_queue_size | 整数 | 设置录制数据Buffer的大小 | | buffer_size | 整数 | 设置底层传输包Buffer的大小 | | user-agent | 字符串 | 用户客户端标识 | 1. TCP方式录制RTSP直播流 ffmpeg默认使用的rtsp拉流方式为UDP,为了避免丢包导致的花屏、绿屏、灰屏、马赛克等问题,将UDP改为TCP传输: ``` ffmpeg -rtsp_transport tcp -i rtsp://127.0.0.1/test.mkv -c copy -f mp4 output.mp4 ``` 1. User-Agent设置参数 ``` ffmpeg -user-agent "Alex-Player" -i rtsp://input:554/live/1/stream.sdp -c copy -f mp4 -u output.mp4 ``` ## FFmpeg录制HTTP流 FFmpeg操作HTTP的参数 | 参数 | 类型 | 说明 | | :---------------- | :----- | :------------------------------- | | seekable | 布尔 | 设置HTTP连接为可seek操作 | | chunked_post | 布尔 | 使用Chunked模式post数据 | | http_proxy | 字符串 | 设置HTTP代理传输数据 | | headers | 字符串 | 自定义HTTP Header数据 | | content_type | 字符串 | 设置POST的内容类型 | | user_agent | 字符串 | 设置HTTP请求客户端信息 | | multiple_requests | 布尔 | HTTP长连接开启 | | post_data | 二进制 | 设置将要POST的数据 | | cookies | 字符串 | 设置HTTP请求时写代码的Cookies | | icy | 布尔 | 请求ICY源数据:默认开关 | | auth_type | 整数 | HTTP验证类型设置 | | offset | 整数 | 初始化HTTP请求时的偏移位置 | | method | 字符串 | 发起HTTP请求时使用的HTTP的方法 | | reconnect | 布尔 | 在EOF之前断开发起重连 | | reconnect_at_eof | 布尔 | 在得到EOF时发起重连 | | reply_code | 整数 | 作为HTTP服务时向客户端反馈状态码 | ## FFmpeg录制和发布TCP与UDP流 略 ## FFmpeg推多路流 1. 推流(tee协议输出多路流) `ffmpeg -re -i AVC_high_1280x720_2013.mp4 -vcodec libx264 -acodec aac -map 0 -f flv "tee:rtmp://127.0.0.1/live/p1|rtmp://127.0.0.1/live/p2"` 2. 验证 `ffmpeg -i rtmp://127.0.0.1/live/p1 -i rtmp://127.0.0.1/live/p2` # ffmpeg滤镜使用 http://ffmpeg.org/ffmpeg-filters.html ## FFmpeg滤镜Filter描述格式 1. FFmpeg滤镜Filter的参数排列方式 [输入流或标记]滤镜参数[临时标记名];[输入流或标记]滤镜参数[临时标记名]… 输入两个文件,一个视频,一个图片,将logo进行缩放,然后放在视频的左上角: ``` ffmpeg -i input.mp4 -i input.jpg -filter_complex " [1:v] scale=176:144[logo];[0:v][logo]overlay=x=0:y=0" output.mp4 # [0:v]/[1:v]代表第几个输入的视频 ``` 2. FFmpeg为视频加水印 - `drawtext`滤镜 ``` ffmpeg -h filter=drawtext # 文字水印 ffmpeg -i input.mp4 -ss 50 -vf "drawtext=fontsize=100:fontfile=/usr/share/fonts/truetype/freefont/FreeSerif.ttf :text='Hello World':fontcolor='yellow':x=20:y=20" output.mp4 # 动态日期 ffmpeg -i input.mp4 -ss 50 -vf "drawtext=fontsize=100:fontfile=/usr/share/fonts/truetype/freefont/FreeSerif.ttf :text='%{localtime\:%Y\-%m\-%d %H-%M-%S}':fontcolor='yellow':x=20:y=20" output.mp4 # 闪烁 ffmpeg -i input.mp4 -ss 50 -vf "drawtext=fontsize=100:fontfile=/usr/share/fonts/truetype/freefont/FreeSerif.ttf :text='%{localtime\:%Y\-%m\-%d %H-%M-%S}':fontcolor='yellow':x=20:y=20:enable=lt(mod(t\,3)\,1)" output.mp4 ``` - `movie`滤镜 ``` # 图片水印 ffmpeg -i input.mp4 -vf "movie=logo.png[wm];[in][wm]overlay=30:10[out]" output.mp4 # colorkey 半透明 ffmpeg -i input.mp4 -ss 55 -vf "movie=../picture/3d_data.png,colorkey=black:1.0:0.1[wm];[in][wm]overlay=30:10[out]" output.mp4 ``` - `overlay`滤镜 ``` # 画中画 ffmpeg -re -i input.mp4 -vf "movie=sub.mp4,scale=480x320[test];[in][test]overlay[out]" -vcodec libx264 output.flv # 跑马灯 ffmpeg -re -i input.mp4 -vf "movie=sub.wmv,scale=480x320[test];[in][test]overlay=x='if(gte(t,2), -w+(t-2)*50, NAN)':y=0[out]" -vcodec libx264 output.flv # 视频多宫格处理 ffmpeg -i input1.mp4 -i input2.mp4 -i input3.mp4 -i input4.mp4 -filter_complex " nullsrc=size=1280x720 [background]; [0:v] setpts=PTS-STARTPTS, scale=640x360 [upleft]; [1:v] setpts=PTS-STARTPTS, scale=640x360 [upright]; [2:v] setpts=PTS-STARTPTS, scale=640x360 [downleft]; [3:v] setpts=PTS-STARTPTS, scale=640x360 [downright]; [background][upleft] overlay=shortest=1 [background+upleft]; [background+upleft][upright] overlay=shortest=1:x=640 [background+up]; [background+up][downleft] overlay=shortest=1:y=360 [background+up+downleft]; [background+up+downleft][downright] overlay=shortest=1:x=640:y=360 " output.mp4 ``` ## FFmpeg音频流滤镜操作 1. 双声道合并单声道 `fmpeg -i input.mp3 -ac 1 output.mp3` 2. 双声道提取 - map_channel `ffmpeg -i input.mp3 -map_channel 0.0.0 left.mp3 -map_channel 0.0.1 right.mp3` - pan `ffmpeg -i input.mp3 -filter_complex "[0:0]pan=1c|c0=c0[left];[0:0]pan=1c|c0=c1[right]" -map "[left]" left.mp3 -map "[right]" right.mp3` 1. 双声道转双音频流 ``` ffmpeg -i input.mp4 -filter_complex channelsplit=channel_layout=stereo output.mka ffprobe output.mka # 可以看到有两个stream ``` 不常用,大多数播放器也只会播放第一个流 1. 单声道转双声道 `ffmpeg -i left.aac -ac 2 output.m4a` 这样的双声道并不是真正的双声道,而是单声道处理成的多声道,效果不会比原来多声道效果好 2. 两个音频源合并双声道 `ffmpeg -i left.mp3 -i right.mp3 -filter_complex "[0:a][1:a]amerge=inputs=2[aout]" -map "[aout]" output.mka` 3. 多个音频合并为多声道 ``` ffmpeg -i front_left.wav -i front_right.wav -i front_center.wav -i lfe.wav -i back_left.wav -i back_right.wav -filter_complex "[0:a][1:a][2:a][3:a][4:a][5:a]amerge=inputs=6[aout]" -map "[aout]" output.wav ``` ## FFmpeg音频音量探测 4. 音频音量获得 `ffmpeg -i input.wav -filter_complex volumedetect -f null -` 5. 绘制音频波形 ``` ffmpeg -i input.wav -filter_complex "showwavespic=s=640x120" -frames:v 1 output.png # 不通声道的波形图 ffmpeg -i input.wav -filter_complex "showwavespic=s=640x120:split_channels=1" -frames:v 1 output.png ``` ## FFmpeg为视频加字母 1. ASS字母流写入视频流 `ffmpeg -i input.mp4 -vf ass=t1.ass -f mp4 output.mp4` 2. ASS字母流写入封装容器 ``` ffmpeg -i input.mp4 -vf ass=t1.ass -acodec copy -vcodec copy -scodec copy output.mp4 # 输入的视频文件汇总原本同样带有字幕流,希望使用t1.ass字幕流,通过map写入 # 下面命令会分别将第一个输入文件的第一个流和第二个流与第二个输入文件的第一个流写入output.mkv ffmpeg -i input.mp4 -i t1.ass -map 0:0 -map 0:1 -map 1:0 -acodec copy -vcodec copy -scodec copy output.mkv ``` ## FFmpeg视频抠图合并 1. chromakey 抠图和背景视频合并的操作 ``` # 查询颜色支持 ffmpeg -colors # chromakey滤镜将绿色背景中的人物抠出来,贴到input.mp4为背景的视频中 ffmpeg -i input.mp4 -i input_green.mp4 -filter_complex "[1:v]chromakey=Green:0.1:0.2[ckout];[0:v][ckout]overlay[out]" -map "[out]" output.mp4 # FFmpeg中除了有chromakey滤镜外,还有colorkey参数,chromakey滤镜主要用于YUV数据,所以一般来说做绿幕处理更有优势;而colorkey处理纯色均可以,因为colorkey主要用于RGB数据。 ``` ## FFmpeg 3D视频处理 - ``` stereo3d ``` 滤镜 ``` # 黄蓝 ffplay -vf "stereo3d=sbsl:aybd" AVC_high_1280x720_2013.mp4 # 红蓝 ffplay -vf "stereo3d=sbsl:aybg" AVC_high_1280x720_2013.mp4 ``` ## FFmpeg定时视频截图 - `vframe`参数截取一张图片 `ffmpeg -i input.flv -ss 00:00:7.435 -vframes 1 output.png` - ``` fps ``` 滤镜定时获得图片 ``` # 每隔1秒钟生成一张PNG图片 ffmpeg -i input.flv -vf fps=1 out%d.png # 每隔一封中生成一张jpg图片 ffmpeg -i input.flv -vf fps=1/60 out%d.jpg # select 按照关键帧截取图片 ffmpeg -i input.flv -vf "select='eq(pict_type,PICT_TYPE_I)'" -vsync vfr thumb%04d.png ``` ## FFmpeg 生成测试源数据 1. 音频测试流 lavfi 模拟音频源的abuffer、aevalsrc、anullsrc、flite、anoisesrc、sine滤镜生成音频流 ``` # 白噪声 ffmpeg -re -f lavfi -i aevalsrc="-2+random(0)" -t 5 output.mp3 # 正弦波 ffmpeg -re -f lavfi -i "sine" -t 5 output.mp3 ``` 2. 视频测试流 通过FFmpeg模拟多种视频源:allrgb、allyuv、color、haldclutsrc、nullsrc、rgbtestsrc、smptebars、smptehdbars、testsrc、testsrc2、yuvtestsrc ``` # 生成长度为5.3秒、图像大小为QCIF分辨率、帧率为25fps的视频图像数据,并编码成H.264 ffmpeg -re -f lavfi -i testsrc=duration=5.3:size=qcif:rate=25 -vcodec libx264 -r:v 25 output.mp4 # 纯红 ffmpeg -re -f lavfi -i color=c=red@0.2:s=qcif:r=25 -vcodec libx264 -r:v 25 output.mp4 # 随机雪花 ffmpeg -re -f lavfi -i "nullsrc=s=256x256,geq=random(1)*255:128:128" -vcodec libx264 -r:v 25 output.mp4 ``` ## FFmpeg对音视频倍速处理 1. `atempo`音频倍速处理 取值范围:0.5 ~ 2.0 ``` # 半速处理 ffmpeg -i input.wav -filter_complex "atempo=tempo=0.5" -acodec aac output.aac ``` 2. `setpts`视频倍速处理 使用PTS控制播放速度的 ``` # 半速处理 ffmpeg -re -i input.mp4 -filter_complex "setpts=PTS*2" output.mp4 ``` # ffmpeg采集设备 1. Linux下查看设备列表 `ffmpeg -h demuxer=fbdev` 2. Linux采集设备fbdev FrameBuffer是一个比较有年份的设备,专门用于图像展示操作,早期的图形界面也是基于FrameBuffer进行绘制的,有时在向外界展示Linux的命令行操作又不希望别人看到你的桌面时,可以通过获取FrameBuffer设备图像数据进行编码后推流或录制: ``` ffmpeg -framerate 30 -f fbdev -i /dev/fb0 output.mp4 # ctrl+alt+F1 进入命令行界面 # ctrl+alt+F7 进入图形界面 ``` 3. Linux采集设备v4l2 v4l2主要用来采集摄像头,而摄像头通常支持多种像素格式,有些摄像头还支持直接输出已经编码好的H.264数据 - 查看参数 `ffmpeg -h demuxer=v4l2` - 查看v4l2摄像头锁支持的色彩格式及分辨率 `ffmpeg -hide_banner -f v4l2 -list_formats all -i /dev/vide0` - 采集摄像头 `ffmpeg -hide_banner -s 1920x1080 -i /dev/vide0 output.avi` 1. Linux采集设备x11grab Linux下面采集桌面图像时,通常采用x11grab设备采集图像,输入设备的设备名规则: [主机名]: 显示编号id.屏幕编号id+起始x轴,起始y轴 ``` # 桌面录制(帧率:25,图像分辨率:1366x768,采集的设备:0.0) ffmpeg -f x11grab -framerate 25 -video_size 1366x768 -i :0.0 out.mp4 # 桌面录制指定起始位置(:0.0+300,200 指定了x坐标300,y坐标200) # 注意:video_size不要超过实际采集区域的大小 ffmpeg -f x11grab -framerate 25 -video_size 352x288 -i :0.0+300,200 out.mp4 # 桌面录制带鼠标记录的视频 ffmpeg -f x11grab -video_size 1366x768 -follow_mouse 1 -i :0.0 out.mp4 ``` # 其他 ## x265安装 1. 下载 网站1:http://www.videolan.org/developers/x265.html `hg clone http://hg.videolan.org/x265` 网站2:https://bitbucket.org/multicoreware/x265 `hg clone https://bitbucket.org/multicoreware/x265` 2. 编译 ``` sudo apt-get install mercurial cmake cmake-curses-gui build-essential yasm cd x265/build/linux ./make-Makefiles.bash make sudo make install ``` ## DTS、PTS 的概念 DTS、PTS 的概念如下所述: - DTS(Decoding Time Stamp):即解码时间戳,这个时间戳的意义在于告诉播放器该在什么时候解码这一帧的数据。 - PTS(Presentation Time Stamp):即显示时间戳,这个时间戳用来告诉播放器该在什么时候显示这一帧的数据。 需要注意的是:虽然 DTS、PTS 是用于指导播放端的行为,但它们是在编码的时候由编码器生成的。 当视频流中没有 B 帧时,通常 DTS 和 PTS 的顺序是一致的。但如果有 B 帧时,就回到了我们前面说的问题:解码顺序和播放顺序不一致了。 比如一个视频中,帧的显示顺序是:I B B P,现在我们需要在解码 B 帧时知道 P 帧中信息,因此这几帧在视频流中的顺序可能是:I P B B,这时候就体现出每帧都有 DTS 和 PTS 的作用了。DTS 告诉我们该按什么顺序解码这几帧图像,PTS 告诉我们该按什么顺序显示这几帧图像。顺序大概如下: ``` PTS: 1 4 2 3 DTS: 1 2 3 4 Stream: I P B B ``` ## 其他常用命令 1、将文件当作源推送到RTMP服务器 ``` ffmpeg -re -i localFile.mp4 -c copy -f flv rtmp://server/live/streamName ``` 参数解释 -r 以本地帧频读数据,主要用于模拟捕获设备。表示ffmpeg将按照帧率发送数据,不会按照最高的效率发送 2、将直播文件保存至本地 ``` ffmpeg -i rtmp://server/live/streamName -c copy dump.flv ``` 3、将其中一个直播流中的视频改用H.264压缩,音频不变,推送到另外一个直播服务器 ``` ffmpeg -i rtmp://server/live/originalStream -c:a copy -c:v libx264 -vpre slow -f flv rtmp://server/live/h264Stream ``` 4、将其中一个直播流中的视频改用H.264压缩,音频改用aac压缩,推送到另外一个直播服务器 ``` ffmpeg -i rtmp://server/live/originalStream -c:a libfaac -ar 44100 -ab 48k -c:v libx264 -vpre slow -vpre baseline -f flv rtmp://server/live/h264Stream ``` 5、将其中一个直播流中的视频不变,音频改用aac压缩,推送到另外一个直播服务器 ``` ffmpeg -i rtmp://server/live/originalStream -acodec libfaac -ar 44100 -ab 48k -vcodec copy -f flv rtmp://server/live/h264_AAC_Stream ``` 6、将一个高清流复制为几个不同清晰度的流重新发布,其中音频不变 ``` ffmpeg -re -i rtmp://server/live/high_FMLE_stream -acodec copy -vcodec x264lib -s 640×360 -b 500k -vpre medium -vpre baseline rtmp://server/live/baseline_500k -acodec copy -vcodec x264lib -s 480×272 -b 300k -vpre medium -vpre baseline rtmp://server/live/baseline_300k -acodec copy -vcodec x264lib -s 320×200 -b 150k -vpre medium -vpre baseline rtmp://server/live/baseline_150k -acodec libfaac -vn -ab 48k rtmp://server/live/audio_only_AAC_48k ``` 7、将当前摄像头以及扬声器通过DSHOW采集,使用H.264/AAC压缩后推送到RTMP服务器 ``` ffmpeg -r 25 -f dshow -s 640×480 -i video=”video source name”:audio=”audio source name” -vcodec libx264 -b 600k -vpre slow -acodec libfaac -ab 128k -f flv rtmp://server/application/stream_name ``` 8、将一个JPG图片经过H.264压缩后输出为MP4文件 ``` ffmpeg -i INPUT.jpg -an -vcodec libx264 -coder 1 -flags +loop -cmp +chroma -subq 10 -qcomp 0.6 -qmin 10 -qmax 51 -qdiff 4 -flags2 +dct8x8 -trellis 2 -partitions +parti8x8+parti4x4 -crf 24 -threads 0 -r 25 -g 25 -y OUTPUT.mp4 ``` 9、将MP3转化为AAC ``` ffmpeg -i 20120814164324_205.wav -acodec libfaac -ab 64k -ar 44100 output.aac ``` 10、将AAC文件转化为flv文件,编码格式采用AAC ``` ffmpeg -i output.aac -acodec libfaac -y -ab 32 -ar 44100 -qscale 10 -s 640*480 -r 15 outp ``` ================================================ FILE: ffmpeg源码example解析之decode_audio.md ================================================ # 音频播放 ## 源码文件 ``` /doc/examples/decode_audio.c /doc/examples/muxing.c /doc/examples/resampling_audio.c /doc/examples/transcode_aac.c ``` ## 代码调用流程 代码流程跟《ffmpeg源码example解析之decode-video》基本类似,主要的区别是在播放上。对于播放器来说需要设置一套播放参数,比如:采样率,通道数,大小端,采样大小以及数据类型。理论上可以通过ffmpeg解析出来的的这些参数设置给播放器,但是ffmpeg的format的跟播放器的format不是同一个枚举,需要建立一个映射关系,所以觉得一般的播放器会统一设置成固定值,比如: 采样率:44100,通道数:2,大小端:LittleEndian,采样大小:16bit,数据类型:有符号 也就是说无论输入是什么配置参数,统一重采样成这套参数跟播放匹配。 # 项目 该项目用于学习ffmpeg编解码 使用qt主要是比较方便,后续采用glfw和sdl显示 ## 功能描述 ### 播放视频 1. 使用FFMpeg解码 2. 使用sws_scale将FFMpeg解码后的yuv数据转换成rgb 3. 使用QT的QLabel组件,通过QImage显示rgb数据 ### 播放音频 1. 使用FFMpeg解码 2. 使用swr_convert将FFMpeg解码后的一帧数据转换成播放器指定的播放参数 3. 使用QT的QAudioOutput播放pcm数据 ================================================ FILE: ffmpeg源码example解析之decode_video.md ================================================ # 视频解码流程 ## 源码文件 ``` /doc/examples/decode_video.c ``` ## 代码调用流程 [![image](https://xuleilx.github.io/images/decode_video.png)](https://xuleilx.github.io/images/decode_video.png) 该流程并不是一个正常的流程,它假设了该文件是mpeg1video的编码格式,并且没有封装容器。 通常情况下是需要解封装的,比如说拿到一个视频文件,并不知道是什么编码,这时候就需要解封装来了解容器里面数据流了。 首先我们先要了解ffmpeg的几个大类: - AVFormat:封装、解封装、包含协议封装 - AVCodec:编解码 - AVFilter:音视频滤镜 - swscale:视频图像转换 - swresample:音频转换计算 - AVUtil :工具类 # 视频解封装,解码,图像转换流程 根据ffmpeg的几个大类,介绍解码视频并显示的一般流程和操作,序号为程序调用顺序 ## AVFormat:封装、解封装、包含协议封装 ### 解封装 ```c avformat_alloc_context #封装结构体分配内存 // 可以不调用,avformat_open_input会判断入参是否为NULL,自行分配 avformat_open_input #打开输入文件用于读取数据 avformat_find_stream_info#获取流信息 针对每个stream处理 - pFormatContext->nb_streams - avcodec_find_decoder #根据流中的编码参数AVCodecParameters,查找是否支持该编码 - 判断流的类型 pLocalCodecParameters->codec_type - 保存AVCodecParameters和AVCodec,用于后续处理 av_read_frame #读取一包AVPacket数据包 ``` ## AVCodec:编解码 ### 解码 ```c avcodec_alloc_context3 #编解码结构体分配内存 avcodec_parameters_to_context#将解封装得到的编码参数AVCodecParameters赋值给编解码结构体 avcodec_open2 #打开编码器 avcodec_send_packet #将解封装中得到的AVPacket数据包送给解码器 avcodec_receive_frame #读回一帧解码后的数据AVFrame ``` ### AVPacket:压缩的数据包 ``` av_packet_alloc #压缩的数据包分配内存 ``` ## swscale:视频图像转换 ``` sws_getContext #给SwsContext结构体分配内存 sws_scale #视频图像转换 ``` ## AVUtil :工具类 ### AVFrame:解码后的数据帧 ``` av_frame_alloc #解码后的数据帧分配内存 ``` ### image ``` av_image_alloc #分配内存用于存放一张图片 ``` # 项目 该项目用于学习ffmpeg编解码 使用qt主要是比较方便,后续采用glfw和sdl显示 ## 功能描述 1. 使用FFMpeg解码 2. 将FFMpeg解码后的yuv数据转换成rgb 3. 使用QT的QLabel组件,通过QImage显示rgb数据 ================================================ FILE: ffplay源码和书籍/ffplay/Debug/ffplay.Build.CppClean.log ================================================ e:\work\研究生工作\costream以及视频编码工程\音视频编解码的书籍\ffplay源码和书籍\ffplay\debug\vc120.pdb e:\work\研究生工作\costream以及视频编码工程\音视频编解码的书籍\ffplay源码和书籍\ffplay\debug\vc120.idb e:\work\研究生工作\costream以及视频编码工程\音视频编解码的书籍\ffplay源码和书籍\ffplay\debug\allcodecs.obj e:\work\研究生工作\costream以及视频编码工程\音视频编解码的书籍\ffplay源码和书籍\ffplay\debug\dsputil.obj e:\work\研究生工作\costream以及视频编码工程\音视频编解码的书籍\ffplay源码和书籍\ffplay\debug\imgconvert.obj e:\work\研究生工作\costream以及视频编码工程\音视频编解码的书籍\ffplay源码和书籍\ffplay\debug\msrle.obj e:\work\研究生工作\costream以及视频编码工程\音视频编解码的书籍\ffplay源码和书籍\ffplay\debug\truespeech.obj e:\work\研究生工作\costream以及视频编码工程\音视频编解码的书籍\ffplay源码和书籍\ffplay\debug\utils_codec.obj e:\work\研究生工作\costream以及视频编码工程\音视频编解码的书籍\ffplay源码和书籍\ffplay\debug\allformats.obj e:\work\研究生工作\costream以及视频编码工程\音视频编解码的书籍\ffplay源码和书籍\ffplay\debug\avidec.obj e:\work\研究生工作\costream以及视频编码工程\音视频编解码的书籍\ffplay源码和书籍\ffplay\debug\avio.obj e:\work\研究生工作\costream以及视频编码工程\音视频编解码的书籍\ffplay源码和书籍\ffplay\debug\aviobuf.obj e:\work\研究生工作\costream以及视频编码工程\音视频编解码的书籍\ffplay源码和书籍\ffplay\debug\cutils.obj e:\work\研究生工作\costream以及视频编码工程\音视频编解码的书籍\ffplay源码和书籍\ffplay\debug\file.obj e:\work\研究生工作\costream以及视频编码工程\音视频编解码的书籍\ffplay源码和书籍\ffplay\debug\utils_format.obj e:\work\研究生工作\costream以及视频编码工程\音视频编解码的书籍\ffplay源码和书籍\ffplay\debug\ffplay.obj e:\work\研究生工作\costream以及视频编码工程\音视频编解码的书籍\ffplay源码和书籍\ffplay\debug\ffplay.ilk e:\work\研究生工作\costream以及视频编码工程\音视频编解码的书籍\ffplay源码和书籍\ffplay\debug\ffplay.exe e:\work\研究生工作\costream以及视频编码工程\音视频编解码的书籍\ffplay源码和书籍\ffplay\debug\ffplay.pdb e:\work\研究生工作\costream以及视频编码工程\音视频编解码的书籍\ffplay源码和书籍\ffplay\.\debug\ffplay.exe e:\work\研究生工作\costream以及视频编码工程\音视频编解码的书籍\ffplay源码和书籍\ffplay\debug\ffplay.tlog\cl.command.1.tlog e:\work\研究生工作\costream以及视频编码工程\音视频编解码的书籍\ffplay源码和书籍\ffplay\debug\ffplay.tlog\cl.read.1.tlog e:\work\研究生工作\costream以及视频编码工程\音视频编解码的书籍\ffplay源码和书籍\ffplay\debug\ffplay.tlog\cl.write.1.tlog e:\work\研究生工作\costream以及视频编码工程\音视频编解码的书籍\ffplay源码和书籍\ffplay\debug\ffplay.tlog\link.command.1.tlog e:\work\研究生工作\costream以及视频编码工程\音视频编解码的书籍\ffplay源码和书籍\ffplay\debug\ffplay.tlog\link.read.1.tlog e:\work\研究生工作\costream以及视频编码工程\音视频编解码的书籍\ffplay源码和书籍\ffplay\debug\ffplay.tlog\link.write.1.tlog ================================================ FILE: ffplay源码和书籍/ffplay/Debug/ffplay.log ================================================ 生成启动时间为 2016/11/18 15:28:25。 1>项目“E:\Work\研究生工作\COStream以及视频编码工程\音视频编解码的书籍\ffplay源码和书籍\ffplay\ffplay.vcxproj”在节点 2 上(Rebuild 个目标)。 1>ClCompile: D:\Program Files\Microsoft Visual Studio 12.0\VC\bin\CL.exe /c /ZI /nologo /W3 /WX- /Od /Oy- /D WIN32 /D _DEBUG /D _CONSOLE /D _VC80_UPGRADE=0x0600 /D _MBCS /Gm /EHsc /RTC1 /MTd /GS /Gy- /fp:precise /Zc:wchar_t /Zc:forScope /Fo".\Debug\\" /Fd".\Debug\vc120.pdb" /Gd /TC /analyze- /errorReport:prompt ffplay.c libavcodec\allcodecs.c libavcodec\dsputil.c libavcodec\imgconvert.c libavcodec\msrle.c libavcodec\truespeech.c libavcodec\utils_codec.c libavformat\allformats.c libavformat\avidec.c libavformat\avio.c libavformat\aviobuf.c libavformat\cutils.c libavformat\file.c libavformat\utils_format.c utils_format.c 1>e:\work\研究生工作\costream以及视频编码工程\音视频编解码的书籍\ffplay源码和书籍\ffplay\libavformat\utils_format.c(5): warning C4005: “UINT_MAX”: 宏重定义 d:\program files\microsoft visual studio 12.0\vc\include\limits.h(41) : 参见“UINT_MAX”的前一个定义 file.c 1>e:\work\研究生工作\costream以及视频编码工程\音视频编解码的书籍\ffplay源码和书籍\ffplay\libavformat\file.c(31): warning C4996: '_open': This function or variable may be unsafe. Consider using _sopen_s instead. To disable deprecation, use _CRT_SECURE_NO_WARNINGS. See online help for details. d:\program files\microsoft visual studio 12.0\vc\include\io.h(237) : 参见“_open”的声明 1>e:\work\研究生工作\costream以及视频编码工程\音视频编解码的书籍\ffplay源码和书籍\ffplay\libavformat\file.c(41): warning C4996: 'read': The POSIX name for this item is deprecated. Instead, use the ISO C++ conformant name: _read. See online help for details. d:\program files\microsoft visual studio 12.0\vc\include\io.h(337) : 参见“read”的声明 1>e:\work\研究生工作\costream以及视频编码工程\音视频编解码的书籍\ffplay源码和书籍\ffplay\libavformat\file.c(47): warning C4996: 'write': The POSIX name for this item is deprecated. Instead, use the ISO C++ conformant name: _write. See online help for details. d:\program files\microsoft visual studio 12.0\vc\include\io.h(342) : 参见“write”的声明 1>e:\work\研究生工作\costream以及视频编码工程\音视频编解码的书籍\ffplay源码和书籍\ffplay\libavformat\file.c(53): warning C4244: “函数”: 从“offset_t”转换到“long”,可能丢失数据 1>e:\work\研究生工作\costream以及视频编码工程\音视频编解码的书籍\ffplay源码和书籍\ffplay\libavformat\file.c(53): warning C4996: 'lseek': The POSIX name for this item is deprecated. Instead, use the ISO C++ conformant name: _lseek. See online help for details. d:\program files\microsoft visual studio 12.0\vc\include\io.h(334) : 参见“lseek”的声明 1>e:\work\研究生工作\costream以及视频编码工程\音视频编解码的书籍\ffplay源码和书籍\ffplay\libavformat\file.c(59): warning C4996: 'close': The POSIX name for this item is deprecated. Instead, use the ISO C++ conformant name: _close. See online help for details. d:\program files\microsoft visual studio 12.0\vc\include\io.h(326) : 参见“close”的声明 cutils.c aviobuf.c avio.c 1>e:\work\研究生工作\costream以及视频编码工程\音视频编解码的书籍\ffplay源码和书籍\ffplay\libavformat\avio.c(41): warning C4996: 'strcpy': This function or variable may be unsafe. Consider using strcpy_s instead. To disable deprecation, use _CRT_SECURE_NO_WARNINGS. See online help for details. d:\program files\microsoft visual studio 12.0\vc\include\string.h(112) : 参见“strcpy”的声明 1>e:\work\研究生工作\costream以及视频编码工程\音视频编解码的书籍\ffplay源码和书籍\ffplay\libavformat\avio.c(64): warning C4996: 'strcpy': This function or variable may be unsafe. Consider using strcpy_s instead. To disable deprecation, use _CRT_SECURE_NO_WARNINGS. See online help for details. d:\program files\microsoft visual studio 12.0\vc\include\string.h(112) : 参见“strcpy”的声明 avidec.c 1>e:\work\研究生工作\costream以及视频编码工程\音视频编解码的书籍\ffplay源码和书籍\ffplay\libavformat\avidec.c(121): warning C4244: “函数”: 从“int64_t”转换到“int”,可能丢失数据 1>e:\work\研究生工作\costream以及视频编码工程\音视频编解码的书籍\ffplay源码和书籍\ffplay\libavformat\avidec.c(325): warning C4018: “>”: 有符号/无符号不匹配 1>e:\work\研究生工作\costream以及视频编码工程\音视频编解码的书籍\ffplay源码和书籍\ffplay\libavformat\avidec.c(642): warning C4244: “+=”: 从“int64_t”转换到“unsigned int”,可能丢失数据 1>e:\work\研究生工作\costream以及视频编码工程\音视频编解码的书籍\ffplay源码和书籍\ffplay\libavformat\avidec.c(646): warning C4018: “>=”: 有符号/无符号不匹配 allformats.c utils_codec.c truespeech.c msrle.c imgconvert.c 1>e:\work\研究生工作\costream以及视频编码工程\音视频编解码的书籍\ffplay源码和书籍\ffplay\libavcodec\imgconvert.c(1091): warning C4146: 一元负运算符应用于无符号类型,结果仍为无符号类型 1>e:\work\研究生工作\costream以及视频编码工程\音视频编解码的书籍\ffplay源码和书籍\ffplay\libavcodec\imgconvert_template.h(465): warning C4146: 一元负运算符应用于无符号类型,结果仍为无符号类型 1>e:\work\研究生工作\costream以及视频编码工程\音视频编解码的书籍\ffplay源码和书籍\ffplay\libavcodec\imgconvert_template.h(859): warning C4146: 一元负运算符应用于无符号类型,结果仍为无符号类型 dsputil.c allcodecs.c ffplay.c 1>e:\work\研究生工作\costream以及视频编码工程\音视频编解码的书籍\ffplay源码和书籍\ffplay\ffplay.c(93): warning C4996: '_ftime64': This function or variable may be unsafe. Consider using _ftime64_s instead. To disable deprecation, use _CRT_SECURE_NO_WARNINGS. See online help for details. d:\program files\microsoft visual studio 12.0\vc\include\sys\timeb.h(131) : 参见“_ftime64”的声明 1>e:\work\研究生工作\costream以及视频编码工程\音视频编解码的书籍\ffplay源码和书籍\ffplay\ffplay.c(376): warning C4018: “>=”: 有符号/无符号不匹配 正在生成代码... Link: D:\Program Files\Microsoft Visual Studio 12.0\VC\bin\link.exe /ERRORREPORT:PROMPT /OUT:".\Debug\ffplay.exe" /INCREMENTAL /NOLOGO /LIBPATH:"E:\Work\SDL-1.2.15\lib\x86" odbc32.lib odbccp32.lib kernel32.lib user32.lib gdi32.lib winspool.lib comdlg32.lib advapi32.lib shell32.lib ole32.lib oleaut32.lib uuid.lib odbc32.lib odbccp32.lib /MANIFEST /MANIFESTUAC:"level='asInvoker' uiAccess='false'" /manifest:embed /DEBUG /PDB:".\Debug\ffplay.pdb" /SUBSYSTEM:CONSOLE /TLBID:1 /DYNAMICBASE /NXCOMPAT /IMPLIB:".\Debug\ffplay.lib" /MACHINE:X86 /SAFESEH .\Debug\ffplay.obj .\Debug\allcodecs.obj .\Debug\dsputil.obj .\Debug\imgconvert.obj .\Debug\msrle.obj .\Debug\truespeech.obj .\Debug\utils_codec.obj .\Debug\allformats.obj .\Debug\avidec.obj .\Debug\avio.obj .\Debug\aviobuf.obj .\Debug\cutils.obj .\Debug\file.obj .\Debug\utils_format.obj 1>ffplay.obj : warning LNK4075: 忽略“/EDITANDCONTINUE”(由于“/SAFESEH”规范) ffplay.vcxproj -> E:\Work\研究生工作\COStream以及视频编码工程\音视频编解码的书籍\ffplay源码和书籍\ffplay\.\Debug\ffplay.exe 1>已完成生成项目“E:\Work\研究生工作\COStream以及视频编码工程\音视频编解码的书籍\ffplay源码和书籍\ffplay\ffplay.vcxproj”(Rebuild 个目标)的操作。 生成成功。 已用时间 00:00:01.76 ================================================ FILE: ffplay源码和书籍/ffplay/Debug/ffplay.tlog/ffplay.lastbuildstate ================================================ #TargetFrameworkVersion=v4.0:PlatformToolSet=v120:EnableManagedIncrementalBuild=false:VCToolArchitecture=Native32Bit Debug|Win32|E:\Work\研究生工作\COStream以及视频编码工程\HomerHEVC\HomerHEVC\xHEVC\| ================================================ FILE: ffplay源码和书籍/ffplay/berrno.h ================================================ #ifndef BERRNO_H #define BERRNO_H /* õĴ Ҳʾжϴźŵȣ*/ #ifdef ENOENT #undef ENOENT #endif #define ENOENT 2 #ifdef EINTR #undef EINTR #endif #define EINTR 4 #ifdef EIO #undef EIO #endif #define EIO 5 #ifdef EAGAIN #undef EAGAIN #endif #define EAGAIN 11 #ifdef ENOMEM #undef ENOMEM #endif #define ENOMEM 12 #ifdef EINVAL #undef EINVAL #endif #define EINVAL 22 #ifdef EPIPE #undef EPIPE #endif #define EPIPE 32 #endif ================================================ FILE: ffplay源码和书籍/ffplay/ffplay.c ================================================ #include "./libavformat/avformat.h" #if defined(CONFIG_WIN32) #include #include #include #else #include #include #endif #include #include #include #include #ifdef CONFIG_WIN32 #undef main // We don't want SDL to override our main() #endif #pragma comment(lib, "SDL.lib") /* ˳¼ */ #define FF_QUIT_EVENT (SDL_USEREVENT + 2) /* Ƶе */ #define MAX_VIDEOQ_SIZE (5 * 256 * 1024) /* Ƶе󳤶 */ #define MAX_AUDIOQ_SIZE (5 * 16 * 1024) /* ͼеij */ #define VIDEO_PICTURE_QUEUE_SIZE 1 /* ݰ */ typedef struct PacketQueue { // ͷָβָ AVPacketList *first_pkt, *last_pkt; // г int size; // ж int abort_request; // SDL_mutex *mutex; // SDL_cond *cond; } PacketQueue; /* Ƶͼ */ typedef struct VideoPicture { // ʾ SDL_Overlay *bmp; // int width, height; // source height & width } VideoPicture; /* Ƶ״̬ */ typedef struct VideoState { // ߳ SDL_Thread *parse_tid; // Ƶ߳ SDL_Thread *video_tid; // ж int abort_request; // ʾһģ߱ʾһļģ AVFormatContext *ic; int audio_stream; int video_stream; // Ƶ AVStream *audio_st; // Ƶ AVStream *video_st; // Ƶ PacketQueue audioq; // Ƶ PacketQueue videoq; // Ŷ VideoPicture pictq[VIDEO_PICTURE_QUEUE_SIZE]; double frame_last_delay; uint8_t audio_buf[(AVCODEC_MAX_AUDIO_FRAME_SIZE *3) / 2]; unsigned int audio_buf_size; int audio_buf_index; // Ƶݰ AVPacket audio_pkt; // Ƶݺͳ uint8_t *audio_pkt_data; int audio_pkt_size; // Ƶ SDL_mutex *video_decoder_mutex; // Ƶ SDL_mutex *audio_decoder_mutex; // Ƶļ char filename[240]; } VideoState; static AVInputFormat *file_iformat; static const char *input_filename; static VideoState *cur_stream; static SDL_Surface *screen; /* ȡǰʱ */ int64_t av_gettime(void) { #if defined(CONFIG_WINCE) return timeGetTime() *int64_t_C(1000); #elif defined(CONFIG_WIN32) struct _timeb tb; _ftime(&tb); return ((int64_t)tb.time *int64_t_C(1000) + (int64_t)tb.millitm) *int64_t_C(1000); #else struct timeval tv; gettimeofday(&tv, NULL); return (int64_t)tv.tv_sec *1000000+tv.tv_usec; #endif } /* ݰгʼ */ static void packet_queue_init(PacketQueue *q) // packet queue handling { memset(q, 0, sizeof(PacketQueue)); // q->mutex = SDL_CreateMutex(); // q->cond = SDL_CreateCond(); } /* ˢݰУͷݰ */ static void packet_queue_flush(PacketQueue *q) { AVPacketList *pkt, *pkt1; SDL_LockMutex(q->mutex); for (pkt = q->first_pkt; pkt != NULL; pkt = pkt1) { pkt1 = pkt->next; av_free_packet(&pkt->pkt); av_freep(&pkt); } q->last_pkt = NULL; q->first_pkt = NULL; q->size = 0; SDL_UnlockMutex(q->mutex); } /* ݰ */ static void packet_queue_end(PacketQueue *q) { packet_queue_flush(q); SDL_DestroyMutex(q->mutex); SDL_DestroyCond(q->cond); } /* ݰѹһݰ */ static int packet_queue_put(PacketQueue *q, AVPacket *pkt) { AVPacketList *pkt1; pkt1 = av_malloc(sizeof(AVPacketList)); if (!pkt1) return - 1; pkt1->pkt = *pkt; pkt1->next = NULL; SDL_LockMutex(q->mutex); if (!q->last_pkt) q->first_pkt = pkt1; else q->last_pkt->next = pkt1; q->last_pkt = pkt1; q->size += pkt1->pkt.size; SDL_CondSignal(q->cond); SDL_UnlockMutex(q->mutex); return 0; } /* ˳ */ static void packet_queue_abort(PacketQueue *q) { SDL_LockMutex(q->mutex); q->abort_request = 1; SDL_CondSignal(q->cond); SDL_UnlockMutex(q->mutex); } /* return < 0 if aborted, 0 if no packet and > 0 if packet. */ /* Ӷȡһݰ */ static int packet_queue_get(PacketQueue *q, AVPacket *pkt, int block) { AVPacketList *pkt1; int ret; SDL_LockMutex(q->mutex); for (;;) { if (q->abort_request) { ret = - 1; // 쳣 break; } pkt1 = q->first_pkt; if (pkt1) { q->first_pkt = pkt1->next; if (!q->first_pkt) q->last_pkt = NULL; q->size -= pkt1->pkt.size; *pkt = pkt1->pkt; av_free(pkt1); ret = 1; break; } else if (!block)// ǣ1(ģʽ)0(ģʽ) { ret = 0; // ģʽûֱӷ0 break; } else { SDL_CondWait(q->cond, q->mutex); } } SDL_UnlockMutex(q->mutex); return ret; } /* һVideoPicture */ static void alloc_picture(void *opaque) { VideoState *is = opaque; VideoPicture *vp; vp = &is->pictq[0]; if (vp->bmp) SDL_FreeYUVOverlay(vp->bmp); vp->bmp = SDL_CreateYUVOverlay(is->video_st->actx->width, is->video_st->actx->height, SDL_YV12_OVERLAY, screen); vp->width = is->video_st->actx->width; vp->height = is->video_st->actx->height; } /* ʾͼ */ static int video_display(VideoState *is, AVFrame *src_frame, double pts) { VideoPicture *vp; int dst_pix_fmt; AVPicture pict; if (is->videoq.abort_request) return - 1; vp = &is->pictq[0]; /* if the frame is not skipped, then display it */ if (vp->bmp) { SDL_Rect rect; if (pts) Sleep((int)(is->frame_last_delay *1000)); #if 1 /* get a pointer on the bitmap */ SDL_LockYUVOverlay(vp->bmp); dst_pix_fmt = PIX_FMT_YUV420P; pict.data[0] = vp->bmp->pixels[0]; pict.data[1] = vp->bmp->pixels[2]; pict.data[2] = vp->bmp->pixels[1]; pict.linesize[0] = vp->bmp->pitches[0]; pict.linesize[1] = vp->bmp->pitches[2]; pict.linesize[2] = vp->bmp->pitches[1]; img_convert(&pict, dst_pix_fmt, (AVPicture*)src_frame, is->video_st->actx->pix_fmt, is->video_st->actx->width, is->video_st->actx->height); SDL_UnlockYUVOverlay(vp->bmp); /* update the bitmap content */ rect.x = 0; rect.y = 0; rect.w = is->video_st->actx->width; rect.h = is->video_st->actx->height; SDL_DisplayYUVOverlay(vp->bmp, &rect); #endif } return 0; } /* Ƶ߳ ߽*/ static int video_thread(void *arg) { VideoState *is = arg; AVPacket pkt1, *pkt = &pkt1; int len1, got_picture; double pts = 0; // һ֡ AVFrame *frame = av_malloc(sizeof(AVFrame)); memset(frame, 0, sizeof(AVFrame)); alloc_picture(is); // ѭ for (;;) { // ȡһݰ if (packet_queue_get(&is->videoq, pkt, 1) < 0) break; // Ȼ룬Ȼ SDL_LockMutex(is->video_decoder_mutex); len1 = avcodec_decode_video(is->video_st->actx, frame, &got_picture, pkt->data, pkt->size); SDL_UnlockMutex(is->video_decoder_mutex); // if (pkt->dts != AV_NOPTS_VALUE) pts = av_q2d(is->video_st->time_base) *pkt->dts; // ʾ֡ if (got_picture) { if (video_display(is, frame, pts) < 0) goto the_end; } av_free_packet(pkt); } the_end: av_free(frame); return 0; } /* decode one audio frame and returns its uncompressed size */ static int audio_decode_frame(VideoState *is, uint8_t *audio_buf, double *pts_ptr) { AVPacket *pkt = &is->audio_pkt; int len1, data_size; for (;;) { /* NOTE: the audio packet can contain several frames */ while (is->audio_pkt_size > 0) { SDL_LockMutex(is->audio_decoder_mutex); len1 = avcodec_decode_audio(is->audio_st->actx, (int16_t*)audio_buf, &data_size, is->audio_pkt_data, is->audio_pkt_size); SDL_UnlockMutex(is->audio_decoder_mutex); if (len1 < 0) { /* if error, we skip the frame */ is->audio_pkt_size = 0; break; } is->audio_pkt_data += len1; is->audio_pkt_size -= len1; if (data_size <= 0) continue; return data_size; } /* free the current packet */ if (pkt->data) av_free_packet(pkt); /* read next packet */ if (packet_queue_get(&is->audioq, pkt, 1) < 0) return - 1; is->audio_pkt_data = pkt->data; is->audio_pkt_size = pkt->size; } } /* prepare a new audio buffer */ void sdl_audio_callback(void *opaque, Uint8 *stream, int len) { VideoState *is = opaque; int audio_size, len1; double pts = 0; while (len > 0) { if (is->audio_buf_index >= is->audio_buf_size) { audio_size = audio_decode_frame(is, is->audio_buf, &pts); if (audio_size < 0) { /* if error, just output silence */ is->audio_buf_size = 1024; memset(is->audio_buf, 0, is->audio_buf_size); } else { // audio_size = synchronize_audio(is, (int16_t*)is->audio_buf, audio_size, pts); is->audio_buf_size = audio_size; } is->audio_buf_index = 0; } len1 = is->audio_buf_size - is->audio_buf_index; if (len1 > len) len1 = len; memcpy(stream, (uint8_t*)is->audio_buf + is->audio_buf_index, len1); len -= len1; stream += len1; is->audio_buf_index += len1; } } /* open a given stream. Return 0 if OK */ /* һƵƵ ߳ȥ */ static int stream_component_open(VideoState *is, int stream_index) { AVFormatContext *ic = is->ic; AVCodecContext *enc; AVCodec *codec; SDL_AudioSpec wanted_spec, spec; if (stream_index < 0 || stream_index >= ic->nb_streams) return - 1; enc = ic->streams[stream_index]->actx; /* prepare audio output */ /* Ƶ */ if (enc->codec_type == CODEC_TYPE_AUDIO) { wanted_spec.freq = enc->sample_rate; wanted_spec.format = AUDIO_S16SYS; /* hack for AC3. XXX: suppress that */ if (enc->channels > 2) enc->channels = 2; wanted_spec.channels = enc->channels; wanted_spec.silence = 0; wanted_spec.samples = 1024; //SDL_AUDIO_BUFFER_SIZE; wanted_spec.callback = sdl_audio_callback; wanted_spec.userdata = is; if (SDL_OpenAudio(&wanted_spec, &spec) < 0) { fprintf(stderr, "SDL_OpenAudio: %s\n", SDL_GetError()); return - 1; } } // һ codec = avcodec_find_decoder(enc->codec_id); // 򿪱 if (!codec || avcodec_open(enc, codec) < 0) return - 1; switch (enc->codec_type) { case CODEC_TYPE_AUDIO: // Ƶ is->audio_stream = stream_index; is->audio_st = ic->streams[stream_index]; is->audio_buf_size = 0; is->audio_buf_index = 0; memset(&is->audio_pkt, 0, sizeof(is->audio_pkt)); packet_queue_init(&is->audioq); SDL_PauseAudio(0); break; case CODEC_TYPE_VIDEO: is->video_stream = stream_index; is->video_st = ic->streams[stream_index]; is->frame_last_delay = is->video_st->frame_last_delay; packet_queue_init(&is->videoq); // гʼ is->video_tid = SDL_CreateThread(video_thread, is); // Ƶ߳ȥ break; default: break; } return 0; } static void stream_component_close(VideoState *is, int stream_index) { AVFormatContext *ic = is->ic; AVCodecContext *enc; if (stream_index < 0 || stream_index >= ic->nb_streams) return ; enc = ic->streams[stream_index]->actx; switch (enc->codec_type) { case CODEC_TYPE_AUDIO: packet_queue_abort(&is->audioq); SDL_CloseAudio(); packet_queue_end(&is->audioq); break; case CODEC_TYPE_VIDEO: packet_queue_abort(&is->videoq); SDL_WaitThread(is->video_tid, NULL); packet_queue_end(&is->videoq); break; default: break; } avcodec_close(enc); } /* ߳ */ static int decode_thread(void *arg) { VideoState *is = arg; AVFormatContext *ic; int err, i, ret, video_index, audio_index; AVPacket pkt1, *pkt = &pkt1; AVFormatParameters params, *ap = ¶ms; int flags = SDL_HWSURFACE | SDL_ASYNCBLIT | SDL_HWACCEL | SDL_RESIZABLE; video_index = - 1; audio_index = - 1; is->video_stream = - 1; is->audio_stream = - 1; memset(ap, 0, sizeof(*ap)); // ļϢ䵽IOĶ err = av_open_input_file(&ic, is->filename, NULL, 0, ap); if (err < 0) { ret = - 1; goto fail; } is->ic = ic; // for (i = 0; i < ic->nb_streams; i++) { // AVCodecContext *enc = ic->streams[i]->actx; // switch (enc->codec_type) { case CODEC_TYPE_AUDIO: // Ƶ if (audio_index < 0) audio_index = i; break; case CODEC_TYPE_VIDEO: // Ƶ if (video_index < 0) video_index = i; // ƵģʽõĻ screen = SDL_SetVideoMode(enc->width, enc->height, 0, flags); SDL_WM_SetCaption("FFplay", "FFplay"); // ޸ΪƵС // schedule_refresh(is, 40); break; default: break; } } // 򿪸ijɷ֣÷߳ if (audio_index >= 0) stream_component_open(is, audio_index); // ĸɷ֣÷߳ if (video_index >= 0) stream_component_open(is, video_index); if (is->video_stream < 0 && is->audio_stream < 0) { fprintf(stderr, "%s: could not open codecs\n", is->filename); ret = - 1; goto fail; } // ѭ for (;;) { if (is->abort_request) break; // Ѿˣôӳȥ if (is->audioq.size > MAX_AUDIOQ_SIZE || is->videoq.size > MAX_VIDEOQ_SIZE || url_feof(&ic->pb)) { SDL_Delay(10); // if the queue are full, no need to read more,wait 10 ms continue; } // ȡһݰʾһ֡ ret = av_read_packet(ic, pkt); //av_read_frame(ic, pkt); if (ret < 0) { if (url_ferror(&ic->pb) == 0) { SDL_Delay(100); // wait for user event continue; } else break; } // ݰӵ if (pkt->stream_index == is->audio_stream) { packet_queue_put(&is->audioq, pkt); } else if (pkt->stream_index == is->video_stream) { packet_queue_put(&is->videoq, pkt); } else { av_free_packet(pkt); } } while (!is->abort_request) // wait until the end { SDL_Delay(100); } ret = 0; fail: if (is->audio_stream >= 0) stream_component_close(is, is->audio_stream); if (is->video_stream >= 0) stream_component_close(is, is->video_stream); if (is->ic) { av_close_input_file(is->ic); is->ic = NULL; } if (ret != 0) { SDL_Event event; event.type = FF_QUIT_EVENT; event.user.data1 = is; SDL_PushEvent(&event); } return 0; } /* ļ */ static VideoState *stream_open(const char *filename, AVInputFormat *iformat) { // Ƶ״̬ VideoState *is; is = av_mallocz(sizeof(VideoState)); if (!is) return NULL; pstrcpy(is->filename, sizeof(is->filename), filename); // Ƶ is->audio_decoder_mutex = SDL_CreateMutex(); // Ƶ is->video_decoder_mutex = SDL_CreateMutex(); // ߳ is->parse_tid = SDL_CreateThread(decode_thread, is); if (!is->parse_tid) { av_free(is); return NULL; } return is; } static void stream_close(VideoState *is) { VideoPicture *vp; int i; is->abort_request = 1; SDL_WaitThread(is->parse_tid, NULL); for (i = 0; i < VIDEO_PICTURE_QUEUE_SIZE; i++) { vp = &is->pictq[i]; if (vp->bmp) { SDL_FreeYUVOverlay(vp->bmp); vp->bmp = NULL; } } SDL_DestroyMutex(is->audio_decoder_mutex); SDL_DestroyMutex(is->video_decoder_mutex); } void do_exit(void) { if (cur_stream) { stream_close(cur_stream); cur_stream = NULL; } SDL_Quit(); exit(0); } void event_loop(void) // handle an event sent by the GUI { SDL_Event event; for (;;) { SDL_WaitEvent(&event); switch (event.type) { case SDL_KEYDOWN: switch (event.key.keysym.sym) { case SDLK_ESCAPE: case SDLK_q: do_exit(); break; default: break; } break; case SDL_QUIT: case FF_QUIT_EVENT: do_exit(); break; default: break; } } } int main(int argc, char **argv) { int flags = SDL_INIT_VIDEO | SDL_INIT_AUDIO | SDL_INIT_TIMER; // עֵ֧ĸʽ av_register_all(); // ļ input_filename = "clocktxt_320.avi"; // input_filename = "d:/yuv/clocktxt.avi"; // sdlʼsdlһƽ̨ý忪⣩ if (SDL_Init(flags)) exit(1); // ע߼¼¼¼windowsϵͳϢû¼ SDL_EventState(SDL_ACTIVEEVENT, SDL_IGNORE); SDL_EventState(SDL_MOUSEMOTION, SDL_IGNORE); SDL_EventState(SDL_SYSWMEVENT, SDL_IGNORE); SDL_EventState(SDL_USEREVENT, SDL_IGNORE); // cur_stream = stream_open(input_filename, file_iformat); event_loop(); return 0; } ================================================ FILE: ffplay源码和书籍/ffplay/ffplay.dsp ================================================ # Microsoft Developer Studio Project File - Name="ffplay" - Package Owner=<4> # Microsoft Developer Studio Generated Build File, Format Version 6.00 # ** DO NOT EDIT ** # TARGTYPE "Win32 (x86) Console Application" 0x0103 CFG=ffplay - Win32 Debug !MESSAGE This is not a valid makefile. To build this project using NMAKE, !MESSAGE use the Export Makefile command and run !MESSAGE !MESSAGE NMAKE /f "ffplay.mak". !MESSAGE !MESSAGE You can specify a configuration when running NMAKE !MESSAGE by defining the macro CFG on the command line. For example: !MESSAGE !MESSAGE NMAKE /f "ffplay.mak" CFG="ffplay - Win32 Debug" !MESSAGE !MESSAGE Possible choices for configuration are: !MESSAGE !MESSAGE "ffplay - Win32 Release" (based on "Win32 (x86) Console Application") !MESSAGE "ffplay - Win32 Debug" (based on "Win32 (x86) Console Application") !MESSAGE # Begin Project # PROP AllowPerConfigDependencies 0 # PROP Scc_ProjName "" # PROP Scc_LocalPath "" CPP=cl.exe RSC=rc.exe !IF "$(CFG)" == "ffplay - Win32 Release" # PROP BASE Use_MFC 0 # PROP BASE Use_Debug_Libraries 0 # PROP BASE Output_Dir "Release" # PROP BASE Intermediate_Dir "Release" # PROP BASE Target_Dir "" # PROP Use_MFC 0 # PROP Use_Debug_Libraries 0 # PROP Output_Dir "Release" # PROP Intermediate_Dir "Release" # PROP Target_Dir "" # ADD BASE CPP /nologo /W3 /GX /O2 /D "WIN32" /D "NDEBUG" /D "_CONSOLE" /D "_MBCS" /YX /FD /c # ADD CPP /nologo /W3 /GX /D "WIN32" /D "NDEBUG" /D "_CONSOLE" /D "_MBCS" /YX /FD /c # ADD BASE RSC /l 0x804 /d "NDEBUG" # ADD RSC /l 0x804 /d "NDEBUG" BSC32=bscmake.exe # ADD BASE BSC32 /nologo # ADD BSC32 /nologo LINK32=link.exe # ADD BASE LINK32 kernel32.lib user32.lib gdi32.lib winspool.lib comdlg32.lib advapi32.lib shell32.lib ole32.lib oleaut32.lib uuid.lib odbc32.lib odbccp32.lib /nologo /subsystem:console /machine:I386 # ADD LINK32 kernel32.lib user32.lib gdi32.lib winspool.lib comdlg32.lib advapi32.lib shell32.lib ole32.lib oleaut32.lib uuid.lib odbc32.lib odbccp32.lib /nologo /subsystem:console /machine:I386 !ELSEIF "$(CFG)" == "ffplay - Win32 Debug" # PROP BASE Use_MFC 0 # PROP BASE Use_Debug_Libraries 1 # PROP BASE Output_Dir "Debug" # PROP BASE Intermediate_Dir "Debug" # PROP BASE Target_Dir "" # PROP Use_MFC 0 # PROP Use_Debug_Libraries 1 # PROP Output_Dir "Debug" # PROP Intermediate_Dir "Debug" # PROP Ignore_Export_Lib 0 # PROP Target_Dir "" # ADD BASE CPP /nologo /W3 /Gm /GX /ZI /Od /D "WIN32" /D "_DEBUG" /D "_CONSOLE" /D "_MBCS" /YX /FD /GZ /c # ADD CPP /nologo /W3 /Gm /GX /ZI /Od /D "WIN32" /D "_DEBUG" /D "_CONSOLE" /D "_MBCS" /YX /FD /GZ /c # ADD BASE RSC /l 0x804 /d "_DEBUG" # ADD RSC /l 0x804 /d "_DEBUG" BSC32=bscmake.exe # ADD BASE BSC32 /nologo # ADD BSC32 /nologo LINK32=link.exe # ADD BASE LINK32 kernel32.lib user32.lib gdi32.lib winspool.lib comdlg32.lib advapi32.lib shell32.lib ole32.lib oleaut32.lib uuid.lib odbc32.lib odbccp32.lib /nologo /subsystem:console /debug /machine:I386 /pdbtype:sept # ADD LINK32 kernel32.lib user32.lib gdi32.lib winspool.lib comdlg32.lib advapi32.lib shell32.lib ole32.lib oleaut32.lib uuid.lib odbc32.lib odbccp32.lib /nologo /subsystem:console /debug /machine:I386 /pdbtype:sept !ENDIF # Begin Target # Name "ffplay - Win32 Release" # Name "ffplay - Win32 Debug" # Begin Group "libavcodec" # PROP Default_Filter "" # Begin Source File SOURCE=.\libavcodec\allcodecs.c # End Source File # Begin Source File SOURCE=.\libavcodec\avcodec.h # End Source File # Begin Source File SOURCE=.\libavcodec\dsputil.c # End Source File # Begin Source File SOURCE=.\libavcodec\dsputil.h # End Source File # Begin Source File SOURCE=.\libavcodec\imgconvert.c # End Source File # Begin Source File SOURCE=.\libavcodec\imgconvert_template.h # End Source File # Begin Source File SOURCE=.\libavcodec\msrle.c # End Source File # Begin Source File SOURCE=.\libavcodec\truespeech.c # End Source File # Begin Source File SOURCE=.\libavcodec\truespeech_data.h # End Source File # Begin Source File SOURCE=.\libavcodec\utils_codec.c # End Source File # End Group # Begin Group "libavformat" # PROP Default_Filter "" # Begin Source File SOURCE=.\libavformat\allformats.c # End Source File # Begin Source File SOURCE=.\libavformat\avformat.h # End Source File # Begin Source File SOURCE=.\libavformat\avidec.c # End Source File # Begin Source File SOURCE=.\libavformat\avio.c # End Source File # Begin Source File SOURCE=.\libavformat\avio.h # End Source File # Begin Source File SOURCE=.\libavformat\aviobuf.c # End Source File # Begin Source File SOURCE=.\libavformat\cutils.c # End Source File # Begin Source File SOURCE=.\libavformat\file.c # End Source File # Begin Source File SOURCE=.\libavformat\utils_format.c # End Source File # End Group # Begin Group "libavutil" # PROP Default_Filter "" # Begin Source File SOURCE=.\libavutil\avutil.h # End Source File # Begin Source File SOURCE=.\libavutil\bswap.h # End Source File # Begin Source File SOURCE=.\libavutil\common.h # End Source File # Begin Source File SOURCE=.\libavutil\mathematics.h # End Source File # Begin Source File SOURCE=.\libavutil\rational.h # End Source File # End Group # Begin Source File SOURCE=.\berrno.h # End Source File # Begin Source File SOURCE=.\ffplay.c # End Source File # End Target # End Project ================================================ FILE: ffplay源码和书籍/ffplay/ffplay.vcxproj ================================================  Debug Win32 Release Win32 {F914CB69-0432-4937-87FD-D02757DD4B64} Application v120 false MultiByte Application v120 false MultiByte .\Release\ .\Release\ false .\Debug\ .\Debug\ true E:\Work\SDL-1.2.15\include;$(IncludePath) E:\Work\SDL-1.2.15\lib\x86;$(LibraryPath) MultiThreaded Default false Disabled true Level3 WIN32;NDEBUG;_CONSOLE;%(PreprocessorDefinitions) .\Release\ .\Release\ffplay.pch .\Release\ .\Release\ .\Release\ffplay.tlb 0x0804 NDEBUG;%(PreprocessorDefinitions) true .\Release\ffplay.bsc true Console .\Release\ffplay.exe odbc32.lib;odbccp32.lib;%(AdditionalDependencies) MultiThreadedDebug Default false Disabled true Level3 true EditAndContinue WIN32;_DEBUG;_CONSOLE;%(PreprocessorDefinitions) .\Debug\ .\Debug\ffplay.pch .\Debug\ .\Debug\ EnableFastChecks .\Debug\ffplay.tlb 0x0804 _DEBUG;%(PreprocessorDefinitions) true .\Debug\ffplay.bsc true true Console .\Debug\ffplay.exe odbc32.lib;odbccp32.lib;%(AdditionalDependencies) E:\Work\SDL-1.2.15\lib\x86;%(AdditionalLibraryDirectories) ================================================ FILE: ffplay源码和书籍/ffplay/ffplay.vcxproj.filters ================================================  {94bac1c7-3a5d-4c5c-9ada-d7f42de7259f} {d17f4d25-f28a-4175-92be-4834f6481e36} {e95c5c62-22fc-4e57-8867-b02483bd60f4} libavcodec libavcodec libavcodec libavcodec libavcodec libavcodec libavformat libavformat libavformat libavformat libavformat libavformat libavformat libavcodec libavcodec libavcodec libavcodec libavformat libavformat libavutil libavutil libavutil libavutil libavutil ================================================ FILE: ffplay源码和书籍/ffplay/libavcodec/allcodecs.c ================================================ #include "avcodec.h" /************************************************************************/ /* 򵥵ע/ʼѱӦڲʶ */ /************************************************************************/ /* ȫֵı */ extern AVCodec truespeech_decoder; extern AVCodec msrle_decoder; /* עеı */ void avcodec_register_all(void) { static int inited = 0; if (inited != 0) return ; inited = 1; // 򻯰ffmpegֱֻ֧뷽ʽMSRLEtruespeech register_avcodec(&msrle_decoder); register_avcodec(&truespeech_decoder); } ================================================ FILE: ffplay源码和书籍/ffplay/libavcodec/avcodec.h ================================================ #ifndef AVCODEC_H #define AVCODEC_H /* ** ʹõĺꡢݽṹͺͨЩꡢݽṹͺڴģȫЧ */ #ifdef __cplusplus extern "C" { #endif #include "../libavutil/avutil.h" #define FFMPEG_VERSION_INT 0x000409 #define FFMPEG_VERSION "CVS" #define AV_STRINGIFY(s) AV_TOSTRING(s) #define AV_TOSTRING(s) #s #define LIBAVCODEC_VERSION_INT ((51<<16)+(8<<8)+0) #define LIBAVCODEC_VERSION 51.8.0 #define LIBAVCODEC_BUILD LIBAVCODEC_VERSION_INT #define LIBAVCODEC_IDENT "Lavc" AV_STRINGIFY(LIBAVCODEC_VERSION) #define AV_NOPTS_VALUE int64_t_C(0x8000000000000000) #define AV_TIME_BASE 1000000 /* ID 򻯰Ľֻֽ֧뷽ʽtruespeechmsrle*/ enum CodecID { CODEC_ID_TRUESPEECH, CODEC_ID_MSRLE, CODEC_ID_NONE }; /* */ enum CodecType { CODEC_TYPE_UNKNOWN = - 1, // Ƶ CODEC_TYPE_VIDEO, // Ƶ CODEC_TYPE_AUDIO, CODEC_TYPE_DATA }; #define AVCODEC_MAX_AUDIO_FRAME_SIZE 192000 // 1 second of 48khz 32bit audio #define FF_INPUT_BUFFER_PADDING_SIZE 8 /* AVPicture AVFrame Ҫʾеʹû棬ͨ֡ YUV ʽʽ YUV Ҳ RGB ʽԶ 4 data ָʾ */ /* ͼ */ typedef struct AVPicture { uint8_t *data[4]; int linesize[4]; } AVPicture; /* ֡ */ typedef struct AVFrame { uint8_t *data[4]; int linesize[4]; uint8_t *base[4]; } AVFrame; /* ** ʱǰ Codec ʹõģ Codec е(ڳʱȷ ֵ)codec priv_data ṹֶΣݽṹת */ /* еĵǰ Codec ʹõ */ typedef struct AVCodecContext { int bit_rate; int frame_number; // audio or video frame number unsigned char *extradata; // Codec˽ݣAudioWAVEFORMATEXṹչֽڡ int extradata_size; // VideoBITMAPINFOHEADERչֽ int width, height; enum PixelFormat pix_fmt; int sample_rate; // samples per sec // audio only int channels; int bits_per_sample; int block_align; // struct AVCodec *codec; void *priv_data; enum CodecType codec_type; // see CODEC_TYPE_xxx enum CodecID codec_id; // see CODEC_ID_xxx int(*get_buffer)(struct AVCodecContext *c, AVFrame *pic); void(*release_buffer)(struct AVCodecContext *c, AVFrame *pic); int(*reget_buffer)(struct AVCodecContext *c, AVFrame *pic); int internal_buffer_count; void *internal_buffer; // ɫ struct AVPaletteControl *palctrl; }AVCodecContext; /* ͿԱʾ*/ /* ʾƵ ڹܺ һýͶӦһ AVCodec ṹڳʱжʵڲ*/ typedef struct AVCodec { const char *name; // enum CodecType type; // ͣ VideoAudioData enum CodecID id; // id int priv_data_size; // ӦĵĴС int(*init)(AVCodecContext*); // ʼ int(*encode)(AVCodecContext *, uint8_t *buf, int buf_size, void *data); // 뺯 int(*close)(AVCodecContext*); // ر int(*decode)(AVCodecContext *, void *outdata, int *outdata_size, uint8_t *buf, int buf_size); // 뺯 int capabilities; // ʾCodec struct AVCodec *next; // ڰ Codec һڱ }AVCodec; #define AVPALETTE_SIZE 1024 #define AVPALETTE_COUNT 256 /* ɫСʹС궨壬ÿɫֽ(R,G,B,)кܶƵͼɫȽ٣ ӱʾÿصɫֵͿõɫֵʵּ򵥵ĴԼ 4:1 ѹȡ */ /* ɫ,ɫݽṹ壬ɫݡ */ typedef struct AVPaletteControl { // demuxer sets this to 1 to indicate the palette has changed; decoder resets to 0 int palette_changed; /* 4-byte ARGB palette entries, stored in native byte order; note that * the individual palette components should be on a 8-bit scale; if * the palette data comes from a IBM VGA native format, the component * data is probably 6 bits in size and needs to be scaled */ unsigned int palette[AVPALETTE_COUNT]; } AVPaletteControl; int avpicture_alloc(AVPicture *picture, int pix_fmt, int width, int height); void avpicture_free(AVPicture *picture); int avpicture_fill(AVPicture *picture, uint8_t *ptr, int pix_fmt, int width, int height); int avpicture_get_size(int pix_fmt, int width, int height); void avcodec_get_chroma_sub_sample(int pix_fmt, int *h_shift, int *v_shift); int img_convert(AVPicture *dst, int dst_pix_fmt, const AVPicture *src, int pix_fmt, int width, int height); void avcodec_init(void); void register_avcodec(AVCodec *format); AVCodec *avcodec_find_decoder(enum CodecID id); AVCodecContext *avcodec_alloc_context(void); int avcodec_default_get_buffer(AVCodecContext *s, AVFrame *pic); void avcodec_default_release_buffer(AVCodecContext *s, AVFrame *pic); int avcodec_default_reget_buffer(AVCodecContext *s, AVFrame *pic); void avcodec_align_dimensions(AVCodecContext *s, int *width, int *height); int avcodec_check_dimensions(void *av_log_ctx, unsigned int w, unsigned int h); int avcodec_open(AVCodecContext *avctx, AVCodec *codec); int avcodec_decode_audio(AVCodecContext *avctx, int16_t *samples, int *frame_size_ptr, uint8_t *buf, int buf_size); int avcodec_decode_video(AVCodecContext *avctx, AVFrame *picture, int *got_picture_ptr, uint8_t *buf, int buf_size); int avcodec_close(AVCodecContext *avctx); void avcodec_register_all(void); void avcodec_default_free_buffers(AVCodecContext *s); void *av_malloc(unsigned int size); void *av_mallocz(unsigned int size); void *av_realloc(void *ptr, unsigned int size); void av_free(void *ptr); void av_freep(void *ptr); void *av_fast_realloc(void *ptr, unsigned int *size, unsigned int min_size); void img_copy(AVPicture *dst, const AVPicture *src, int pix_fmt, int width, int height); #ifdef __cplusplus } #endif #endif ================================================ FILE: ffplay源码和书籍/ffplay/libavcodec/dsputil.c ================================================ /************************************************************************/ /* dsp Ż޷ʹõIJұʵʼ */ /************************************************************************/ #include "avcodec.h" #include "dsputil.h" uint8_t cropTbl[256+2 * MAX_NEG_CROP] = {0, }; /* dspľ̬ʼ ʵcropTblֵ */ void dsputil_static_init(void) { int i; for (i = 0; i < 256; i++) cropTbl[i + MAX_NEG_CROP] = i; for (i = 0; i < MAX_NEG_CROP; i++) { cropTbl[i] = 0; cropTbl[i + MAX_NEG_CROP + 256] = 255; } } ================================================ FILE: ffplay源码和书籍/ffplay/libavcodec/dsputil.h ================================================ #ifndef DSPUTIL_H #define DSPUTIL_H /************************************************************************/ /* dsp Ż޷ʹõIJұʼ */ /************************************************************************/ #define MAX_NEG_CROP 1024 extern uint8_t cropTbl[256+2 * MAX_NEG_CROP]; void dsputil_static_init(void); #endif ================================================ FILE: ffplay源码和书籍/ffplay/libavcodec/imgconvert.c ================================================ /************************************************************************/ /* 岢ʵͼɫռתʹõĺͺ */ /************************************************************************/ #include "avcodec.h" #include "dsputil.h" #define xglue(x, y) x ## y #define glue(x, y) xglue(x, y) #define FF_COLOR_RGB 0 // RGB color space #define FF_COLOR_GRAY 1 // gray color space #define FF_COLOR_YUV 2 // YUV color space. 16 <= Y <= 235, 16 <= U, V <= 240 #define FF_COLOR_YUV_JPEG 3 // YUV color space. 0 <= Y <= 255, 0 <= U, V <= 255 #define FF_PIXEL_PLANAR 0 // each channel has one component in AVPicture #define FF_PIXEL_PACKED 1 // only one components containing all the channels #define FF_PIXEL_PALETTE 2 // one components containing indexes for a palette typedef struct PixFmtInfo { const char *name; uint8_t nb_channels; // number of channels (including alpha) uint8_t color_type; // color type (see FF_COLOR_xxx constants) uint8_t pixel_type; // pixel storage type (see FF_PIXEL_xxx constants) uint8_t is_alpha; // true if alpha can be specified uint8_t x_chroma_shift; // X chroma subsampling factor is 2 ^ shift uint8_t y_chroma_shift; // Y chroma subsampling factor is 2 ^ shift uint8_t depth; // bit depth of the color components } PixFmtInfo; // this table gives more information about formats static PixFmtInfo pix_fmt_info[PIX_FMT_NB] = { { "yuv420p", 3, FF_COLOR_YUV, FF_PIXEL_PLANAR, 0, 1, 1, 8}, { "yuv422", 1, FF_COLOR_YUV, FF_PIXEL_PACKED, 0, 1, 0, 8}, { "rgb24", 3, FF_COLOR_RGB, FF_PIXEL_PACKED, 0, 0, 0, 8}, { "bgr24", 3, FF_COLOR_RGB, FF_PIXEL_PACKED, 0, 0, 0, 8}, { "yuv422p", 3, FF_COLOR_YUV, FF_PIXEL_PLANAR, 0, 1, 0, 8}, { "yuv444p", 3, FF_COLOR_YUV, FF_PIXEL_PLANAR, 0, 0, 0, 8}, { "rgba32", 4, FF_COLOR_RGB, FF_PIXEL_PACKED, 1, 0, 0, 8}, { "yuv410p", 3, FF_COLOR_YUV, FF_PIXEL_PLANAR, 0, 2, 2, 8}, { "yuv411p", 3, FF_COLOR_YUV, FF_PIXEL_PLANAR, 0, 2, 0, 8}, { "rgb565", 3, FF_COLOR_RGB, FF_PIXEL_PACKED, 0, 0, 0, 5}, { "rgb555", 4, FF_COLOR_RGB, FF_PIXEL_PACKED, 1, 0, 0, 5}, { "gray", 1, FF_COLOR_GRAY, FF_PIXEL_PLANAR, 0, 0, 0, 8}, { "monow", 1, FF_COLOR_GRAY, FF_PIXEL_PLANAR, 0, 0, 0, 1}, { "monob", 1, FF_COLOR_GRAY, FF_PIXEL_PLANAR, 0, 0, 0, 1}, { "pal8", 4, FF_COLOR_RGB, FF_PIXEL_PALETTE, 1, 0, 0, 8}, { "yuvj420p", 3, FF_COLOR_YUV_JPEG, FF_PIXEL_PLANAR, 0, 1, 1, 8}, { "yuvj422p", 3, FF_COLOR_YUV_JPEG, FF_PIXEL_PLANAR, 0, 1, 0, 8}, { "yuvj444p", 3, FF_COLOR_YUV_JPEG, FF_PIXEL_PLANAR, 0, 0, 0, 8}, { "xvmcmc", }, { "xvmcidct",}, { "uyvy422", 1, FF_COLOR_YUV, FF_PIXEL_PACKED, 0, 1, 0, 8}, { "uyvy411", 1, FF_COLOR_YUV, FF_PIXEL_PACKED, 0, 2, 0, 8}, }; void avcodec_get_chroma_sub_sample(int pix_fmt, int *h_shift, int *v_shift) { *h_shift = pix_fmt_info[pix_fmt].x_chroma_shift; *v_shift = pix_fmt_info[pix_fmt].y_chroma_shift; } // Picture field are filled with 'ptr' addresses. Also return size int avpicture_fill(AVPicture *picture, uint8_t *ptr, int pix_fmt, int width, int height) { int size, w2, h2, size2; PixFmtInfo *pinfo; if (avcodec_check_dimensions(NULL, width, height)) goto fail; pinfo = &pix_fmt_info[pix_fmt]; size = width * height; switch (pix_fmt) { case PIX_FMT_YUV420P: case PIX_FMT_YUV422P: case PIX_FMT_YUV444P: case PIX_FMT_YUV410P: case PIX_FMT_YUV411P: case PIX_FMT_YUVJ420P: case PIX_FMT_YUVJ422P: case PIX_FMT_YUVJ444P: w2 = (width + (1 << pinfo->x_chroma_shift) - 1) >> pinfo->x_chroma_shift; h2 = (height + (1 << pinfo->y_chroma_shift) - 1) >> pinfo->y_chroma_shift; size2 = w2 * h2; picture->data[0] = ptr; picture->data[1] = picture->data[0] + size; picture->data[2] = picture->data[1] + size2; picture->linesize[0] = width; picture->linesize[1] = w2; picture->linesize[2] = w2; return size + 2 * size2; case PIX_FMT_RGB24: case PIX_FMT_BGR24: picture->data[0] = ptr; picture->data[1] = NULL; picture->data[2] = NULL; picture->linesize[0] = width * 3; return size *3; case PIX_FMT_RGBA32: picture->data[0] = ptr; picture->data[1] = NULL; picture->data[2] = NULL; picture->linesize[0] = width * 4; return size *4; case PIX_FMT_RGB555: case PIX_FMT_RGB565: case PIX_FMT_YUV422: picture->data[0] = ptr; picture->data[1] = NULL; picture->data[2] = NULL; picture->linesize[0] = width * 2; return size *2; case PIX_FMT_UYVY422: picture->data[0] = ptr; picture->data[1] = NULL; picture->data[2] = NULL; picture->linesize[0] = width * 2; return size *2; case PIX_FMT_UYVY411: picture->data[0] = ptr; picture->data[1] = NULL; picture->data[2] = NULL; picture->linesize[0] = width + width / 2; return size + size / 2; case PIX_FMT_GRAY8: picture->data[0] = ptr; picture->data[1] = NULL; picture->data[2] = NULL; picture->linesize[0] = width; return size; case PIX_FMT_MONOWHITE: case PIX_FMT_MONOBLACK: picture->data[0] = ptr; picture->data[1] = NULL; picture->data[2] = NULL; picture->linesize[0] = (width + 7) >> 3; return picture->linesize[0] *height; case PIX_FMT_PAL8: size2 = (size + 3) &~3; picture->data[0] = ptr; picture->data[1] = ptr + size2; // palette is stored here as 256 32 bit words picture->data[2] = NULL; picture->linesize[0] = width; picture->linesize[1] = 4; return size2 + 256 * 4; default: fail: picture->data[0] = NULL; picture->data[1] = NULL; picture->data[2] = NULL; picture->data[3] = NULL; return - 1; } } int avpicture_get_size(int pix_fmt, int width, int height) { AVPicture dummy_pict; return avpicture_fill(&dummy_pict, NULL, pix_fmt, width, height); } int avpicture_alloc(AVPicture *picture, int pix_fmt, int width, int height) { unsigned int size; void *ptr; size = avpicture_get_size(pix_fmt, width, height); if (size < 0) goto fail; ptr = av_malloc(size); if (!ptr) goto fail; avpicture_fill(picture, ptr, pix_fmt, width, height); return 0; fail: memset(picture, 0, sizeof(AVPicture)); return - 1; } void avpicture_free(AVPicture *picture) { av_free(picture->data[0]); } static int avg_bits_per_pixel(int pix_fmt) { int bits; const PixFmtInfo *pf; pf = &pix_fmt_info[pix_fmt]; switch (pf->pixel_type) { case FF_PIXEL_PACKED: switch (pix_fmt) { case PIX_FMT_YUV422: case PIX_FMT_UYVY422: case PIX_FMT_RGB565: case PIX_FMT_RGB555: bits = 16; break; case PIX_FMT_UYVY411: bits = 12; break; default: bits = pf->depth *pf->nb_channels; break; } break; case FF_PIXEL_PLANAR: if (pf->x_chroma_shift == 0 && pf->y_chroma_shift == 0) { bits = pf->depth *pf->nb_channels; } else { bits = pf->depth + ((2 *pf->depth) >> (pf->x_chroma_shift + pf->y_chroma_shift)); } break; case FF_PIXEL_PALETTE: bits = 8; break; default: bits = - 1; break; } return bits; } //////////////////////////// void ff_img_copy_plane(uint8_t *dst, int dst_wrap, const uint8_t *src, int src_wrap, int width, int height) { if ((!dst) || (!src)) return ; for (; height > 0; height--) { memcpy(dst, src, width); dst += dst_wrap; src += src_wrap; } } void img_copy(AVPicture *dst, const AVPicture *src, int pix_fmt, int width, int height) { int bwidth, bits, i; PixFmtInfo *pf = &pix_fmt_info[pix_fmt]; pf = &pix_fmt_info[pix_fmt]; switch (pf->pixel_type) { case FF_PIXEL_PACKED: switch (pix_fmt) { case PIX_FMT_YUV422: case PIX_FMT_UYVY422: case PIX_FMT_RGB565: case PIX_FMT_RGB555: bits = 16; break; case PIX_FMT_UYVY411: bits = 12; break; default: bits = pf->depth *pf->nb_channels; break; } bwidth = (width *bits + 7) >> 3; ff_img_copy_plane(dst->data[0], dst->linesize[0], src->data[0], src->linesize[0], bwidth, height); break; case FF_PIXEL_PLANAR: for (i = 0; i < pf->nb_channels; i++) { int w, h; w = width; h = height; if (i == 1 || i == 2) { w >>= pf->x_chroma_shift; h >>= pf->y_chroma_shift; } bwidth = (w *pf->depth + 7) >> 3; ff_img_copy_plane(dst->data[i], dst->linesize[i], src->data[i], src->linesize[i], bwidth, h); } break; case FF_PIXEL_PALETTE: ff_img_copy_plane(dst->data[0], dst->linesize[0], src->data[0], src->linesize[0], width, height); // copy the palette ff_img_copy_plane(dst->data[1], dst->linesize[1], src->data[1], src->linesize[1], 4, 256); break; } } static void yuv422_to_yuv420p(AVPicture *dst, const AVPicture *src, int width, int height) { const uint8_t *p, *p1; uint8_t *lum, *cr, *cb, *lum1, *cr1, *cb1; int w; p1 = src->data[0]; lum1 = dst->data[0]; cb1 = dst->data[1]; cr1 = dst->data[2]; for (; height >= 1; height -= 2) { p = p1; lum = lum1; cb = cb1; cr = cr1; for (w = width; w >= 2; w -= 2) { lum[0] = p[0]; cb[0] = p[1]; lum[1] = p[2]; cr[0] = p[3]; p += 4; lum += 2; cb++; cr++; } if (w) { lum[0] = p[0]; cb[0] = p[1]; cr[0] = p[3]; cb++; cr++; } p1 += src->linesize[0]; lum1 += dst->linesize[0]; if (height > 1) { p = p1; lum = lum1; for (w = width; w >= 2; w -= 2) { lum[0] = p[0]; lum[1] = p[2]; p += 4; lum += 2; } if (w) { lum[0] = p[0]; } p1 += src->linesize[0]; lum1 += dst->linesize[0]; } cb1 += dst->linesize[1]; cr1 += dst->linesize[2]; } } static void uyvy422_to_yuv420p(AVPicture *dst, const AVPicture *src, int width, int height) { const uint8_t *p, *p1; uint8_t *lum, *cr, *cb, *lum1, *cr1, *cb1; int w; p1 = src->data[0]; lum1 = dst->data[0]; cb1 = dst->data[1]; cr1 = dst->data[2]; for (; height >= 1; height -= 2) { p = p1; lum = lum1; cb = cb1; cr = cr1; for (w = width; w >= 2; w -= 2) { lum[0] = p[1]; cb[0] = p[0]; lum[1] = p[3]; cr[0] = p[2]; p += 4; lum += 2; cb++; cr++; } if (w) { lum[0] = p[1]; cb[0] = p[0]; cr[0] = p[2]; cb++; cr++; } p1 += src->linesize[0]; lum1 += dst->linesize[0]; if (height > 1) { p = p1; lum = lum1; for (w = width; w >= 2; w -= 2) { lum[0] = p[1]; lum[1] = p[3]; p += 4; lum += 2; } if (w) { lum[0] = p[1]; } p1 += src->linesize[0]; lum1 += dst->linesize[0]; } cb1 += dst->linesize[1]; cr1 += dst->linesize[2]; } } static void uyvy422_to_yuv422p(AVPicture *dst, const AVPicture *src, int width, int height) { const uint8_t *p, *p1; uint8_t *lum, *cr, *cb, *lum1, *cr1, *cb1; int w; p1 = src->data[0]; lum1 = dst->data[0]; cb1 = dst->data[1]; cr1 = dst->data[2]; for (; height > 0; height--) { p = p1; lum = lum1; cb = cb1; cr = cr1; for (w = width; w >= 2; w -= 2) { lum[0] = p[1]; cb[0] = p[0]; lum[1] = p[3]; cr[0] = p[2]; p += 4; lum += 2; cb++; cr++; } p1 += src->linesize[0]; lum1 += dst->linesize[0]; cb1 += dst->linesize[1]; cr1 += dst->linesize[2]; } } static void yuv422_to_yuv422p(AVPicture *dst, const AVPicture *src, int width, int height) { const uint8_t *p, *p1; uint8_t *lum, *cr, *cb, *lum1, *cr1, *cb1; int w; p1 = src->data[0]; lum1 = dst->data[0]; cb1 = dst->data[1]; cr1 = dst->data[2]; for (; height > 0; height--) { p = p1; lum = lum1; cb = cb1; cr = cr1; for (w = width; w >= 2; w -= 2) { lum[0] = p[0]; cb[0] = p[1]; lum[1] = p[2]; cr[0] = p[3]; p += 4; lum += 2; cb++; cr++; } p1 += src->linesize[0]; lum1 += dst->linesize[0]; cb1 += dst->linesize[1]; cr1 += dst->linesize[2]; } } static void yuv422p_to_yuv422(AVPicture *dst, const AVPicture *src, int width, int height) { uint8_t *p, *p1; const uint8_t *lum, *cr, *cb, *lum1, *cr1, *cb1; int w; p1 = dst->data[0]; lum1 = src->data[0]; cb1 = src->data[1]; cr1 = src->data[2]; for (; height > 0; height--) { p = p1; lum = lum1; cb = cb1; cr = cr1; for (w = width; w >= 2; w -= 2) { p[0] = lum[0]; p[1] = cb[0]; p[2] = lum[1]; p[3] = cr[0]; p += 4; lum += 2; cb++; cr++; } p1 += dst->linesize[0]; lum1 += src->linesize[0]; cb1 += src->linesize[1]; cr1 += src->linesize[2]; } } static void yuv422p_to_uyvy422(AVPicture *dst, const AVPicture *src, int width, int height) { uint8_t *p, *p1; const uint8_t *lum, *cr, *cb, *lum1, *cr1, *cb1; int w; p1 = dst->data[0]; lum1 = src->data[0]; cb1 = src->data[1]; cr1 = src->data[2]; for (; height > 0; height--) { p = p1; lum = lum1; cb = cb1; cr = cr1; for (w = width; w >= 2; w -= 2) { p[1] = lum[0]; p[0] = cb[0]; p[3] = lum[1]; p[2] = cr[0]; p += 4; lum += 2; cb++; cr++; } p1 += dst->linesize[0]; lum1 += src->linesize[0]; cb1 += src->linesize[1]; cr1 += src->linesize[2]; } } static void uyvy411_to_yuv411p(AVPicture *dst, const AVPicture *src, int width, int height) { const uint8_t *p, *p1; uint8_t *lum, *cr, *cb, *lum1, *cr1, *cb1; int w; p1 = src->data[0]; lum1 = dst->data[0]; cb1 = dst->data[1]; cr1 = dst->data[2]; for (; height > 0; height--) { p = p1; lum = lum1; cb = cb1; cr = cr1; for (w = width; w >= 4; w -= 4) { cb[0] = p[0]; lum[0] = p[1]; lum[1] = p[2]; cr[0] = p[3]; lum[2] = p[4]; lum[3] = p[5]; p += 6; lum += 4; cb++; cr++; } p1 += src->linesize[0]; lum1 += dst->linesize[0]; cb1 += dst->linesize[1]; cr1 += dst->linesize[2]; } } static void yuv420p_to_yuv422(AVPicture *dst, const AVPicture *src, int width, int height) { int w, h; uint8_t *line1, *line2, *linesrc = dst->data[0]; uint8_t *lum1, *lum2, *lumsrc = src->data[0]; uint8_t *cb1, *cb2 = src->data[1]; uint8_t *cr1, *cr2 = src->data[2]; for (h = height / 2; h--;) { line1 = linesrc; line2 = linesrc + dst->linesize[0]; lum1 = lumsrc; lum2 = lumsrc + src->linesize[0]; cb1 = cb2; cr1 = cr2; for (w = width / 2; w--;) { *line1++ = *lum1++; *line2++ = *lum2++; *line1++ = *line2++ = *cb1++; *line1++ = *lum1++; *line2++ = *lum2++; *line1++ = *line2++ = *cr1++; } linesrc += dst->linesize[0] *2; lumsrc += src->linesize[0] *2; cb2 += src->linesize[1]; cr2 += src->linesize[2]; } } static void yuv420p_to_uyvy422(AVPicture *dst, const AVPicture *src, int width, int height) { int w, h; uint8_t *line1, *line2, *linesrc = dst->data[0]; uint8_t *lum1, *lum2, *lumsrc = src->data[0]; uint8_t *cb1, *cb2 = src->data[1]; uint8_t *cr1, *cr2 = src->data[2]; for (h = height / 2; h--;) { line1 = linesrc; line2 = linesrc + dst->linesize[0]; lum1 = lumsrc; lum2 = lumsrc + src->linesize[0]; cb1 = cb2; cr1 = cr2; for (w = width / 2; w--;) { *line1++ = *line2++ = *cb1++; *line1++ = *lum1++; *line2++ = *lum2++; *line1++ = *line2++ = *cr1++; *line1++ = *lum1++; *line2++ = *lum2++; } linesrc += dst->linesize[0] *2; lumsrc += src->linesize[0] *2; cb2 += src->linesize[1]; cr2 += src->linesize[2]; } } #define SCALEBITS 10 #define ONE_HALF (1 << (SCALEBITS - 1)) #define FIX(x) ((int) ((x) * (1<> SCALEBITS];\ g = cm[(y + g_add) >> SCALEBITS];\ b = cm[(y + b_add) >> SCALEBITS];\ } #define YUV_TO_RGB1(cb1, cr1)\ {\ cb = (cb1) - 128;\ cr = (cr1) - 128;\ r_add = FIX(1.40200) * cr + ONE_HALF;\ g_add = - FIX(0.34414) * cb - FIX(0.71414) * cr + ONE_HALF;\ b_add = FIX(1.77200) * cb + ONE_HALF;\ } #define YUV_TO_RGB2(r, g, b, y1)\ {\ y = (y1) << SCALEBITS;\ r = cm[(y + r_add) >> SCALEBITS];\ g = cm[(y + g_add) >> SCALEBITS];\ b = cm[(y + b_add) >> SCALEBITS];\ } #define Y_CCIR_TO_JPEG(y)\ cm[((y) * FIX(255.0/219.0) + (ONE_HALF - 16 * FIX(255.0/219.0))) >> SCALEBITS] #define Y_JPEG_TO_CCIR(y)\ (((y) * FIX(219.0/255.0) + (ONE_HALF + (16 << SCALEBITS))) >> SCALEBITS) #define C_CCIR_TO_JPEG(y)\ cm[(((y) - 128) * FIX(127.0/112.0) + (ONE_HALF + (128 << SCALEBITS))) >> SCALEBITS] /* NOTE: the clamp is really necessary! */ static inline int C_JPEG_TO_CCIR(int y) { y = (((y - 128) *FIX(112.0 / 127.0) + (ONE_HALF + (128 << SCALEBITS))) >> SCALEBITS); if (y < 16) y = 16; return y; } #define RGB_TO_Y(r, g, b) \ ((FIX(0.29900) * (r) + FIX(0.58700) * (g) + \ FIX(0.11400) * (b) + ONE_HALF) >> SCALEBITS) #define RGB_TO_U(r1, g1, b1, shift)\ (((- FIX(0.16874) * r1 - FIX(0.33126) * g1 + \ FIX(0.50000) * b1 + (ONE_HALF << shift) - 1) >> (SCALEBITS + shift)) + 128) #define RGB_TO_V(r1, g1, b1, shift)\ (((FIX(0.50000) * r1 - FIX(0.41869) * g1 - \ FIX(0.08131) * b1 + (ONE_HALF << shift) - 1) >> (SCALEBITS + shift)) + 128) #define RGB_TO_Y_CCIR(r, g, b) \ ((FIX(0.29900*219.0/255.0) * (r) + FIX(0.58700*219.0/255.0) * (g) + \ FIX(0.11400*219.0/255.0) * (b) + (ONE_HALF + (16 << SCALEBITS))) >> SCALEBITS) #define RGB_TO_U_CCIR(r1, g1, b1, shift)\ (((- FIX(0.16874*224.0/255.0) * r1 - FIX(0.33126*224.0/255.0) * g1 + \ FIX(0.50000*224.0/255.0) * b1 + (ONE_HALF << shift) - 1) >> (SCALEBITS + shift)) + 128) #define RGB_TO_V_CCIR(r1, g1, b1, shift)\ (((FIX(0.50000*224.0/255.0) * r1 - FIX(0.41869*224.0/255.0) * g1 - \ FIX(0.08131*224.0/255.0) * b1 + (ONE_HALF << shift) - 1) >> (SCALEBITS + shift)) + 128) static uint8_t y_ccir_to_jpeg[256]; static uint8_t y_jpeg_to_ccir[256]; static uint8_t c_ccir_to_jpeg[256]; static uint8_t c_jpeg_to_ccir[256]; /* apply to each pixel the given table */ static void img_apply_table(uint8_t *dst, int dst_wrap, const uint8_t *src, int src_wrap, int width, int height, const uint8_t *table1) { int n; const uint8_t *s; uint8_t *d; const uint8_t *table; table = table1; for (; height > 0; height--) { s = src; d = dst; n = width; while (n >= 4) { d[0] = table[s[0]]; d[1] = table[s[1]]; d[2] = table[s[2]]; d[3] = table[s[3]]; d += 4; s += 4; n -= 4; } while (n > 0) { d[0] = table[s[0]]; d++; s++; n--; } dst += dst_wrap; src += src_wrap; } } /* XXX: use generic filter ? */ /* XXX: in most cases, the sampling position is incorrect */ /* 4x1 -> 1x1 */ static void shrink41(uint8_t *dst, int dst_wrap, const uint8_t *src, int src_wrap, int width, int height) { int w; const uint8_t *s; uint8_t *d; for (; height > 0; height--) { s = src; d = dst; for (w = width; w > 0; w--) { d[0] = (s[0] + s[1] + s[2] + s[3] + 2) >> 2; s += 4; d++; } src += src_wrap; dst += dst_wrap; } } /* 2x1 -> 1x1 */ static void shrink21(uint8_t *dst, int dst_wrap, const uint8_t *src, int src_wrap, int width, int height) { int w; const uint8_t *s; uint8_t *d; for (; height > 0; height--) { s = src; d = dst; for (w = width; w > 0; w--) { d[0] = (s[0] + s[1]) >> 1; s += 2; d++; } src += src_wrap; dst += dst_wrap; } } /* 1x2 -> 1x1 */ static void shrink12(uint8_t *dst, int dst_wrap, const uint8_t *src, int src_wrap, int width, int height) { int w; uint8_t *d; const uint8_t *s1, *s2; for (; height > 0; height--) { s1 = src; s2 = s1 + src_wrap; d = dst; for (w = width; w >= 4; w -= 4) { d[0] = (s1[0] + s2[0]) >> 1; d[1] = (s1[1] + s2[1]) >> 1; d[2] = (s1[2] + s2[2]) >> 1; d[3] = (s1[3] + s2[3]) >> 1; s1 += 4; s2 += 4; d += 4; } for (; w > 0; w--) { d[0] = (s1[0] + s2[0]) >> 1; s1++; s2++; d++; } src += 2 * src_wrap; dst += dst_wrap; } } /* 2x2 -> 1x1 */ void ff_shrink22(uint8_t *dst, int dst_wrap, const uint8_t *src, int src_wrap, int width, int height) { int w; const uint8_t *s1, *s2; uint8_t *d; for (; height > 0; height--) { s1 = src; s2 = s1 + src_wrap; d = dst; for (w = width; w >= 4; w -= 4) { d[0] = (s1[0] + s1[1] + s2[0] + s2[1] + 2) >> 2; d[1] = (s1[2] + s1[3] + s2[2] + s2[3] + 2) >> 2; d[2] = (s1[4] + s1[5] + s2[4] + s2[5] + 2) >> 2; d[3] = (s1[6] + s1[7] + s2[6] + s2[7] + 2) >> 2; s1 += 8; s2 += 8; d += 4; } for (; w > 0; w--) { d[0] = (s1[0] + s1[1] + s2[0] + s2[1] + 2) >> 2; s1 += 2; s2 += 2; d++; } src += 2 * src_wrap; dst += dst_wrap; } } /* 4x4 -> 1x1 */ void ff_shrink44(uint8_t *dst, int dst_wrap, const uint8_t *src, int src_wrap, int width, int height) { int w; const uint8_t *s1, *s2, *s3, *s4; uint8_t *d; for (; height > 0; height--) { s1 = src; s2 = s1 + src_wrap; s3 = s2 + src_wrap; s4 = s3 + src_wrap; d = dst; for (w = width; w > 0; w--) { d[0] = (s1[0] + s1[1] + s1[2] + s1[3] + s2[0] + s2[1] + s2[2] + s2[3] + s3[0] + s3[1] + s3[2] + s3[3] + s4[0] + s4[1] + s4[2] + s4[3] + 8) >> 4; s1 += 4; s2 += 4; s3 += 4; s4 += 4; d++; } src += 4 * src_wrap; dst += dst_wrap; } } static void grow21_line(uint8_t *dst, const uint8_t *src, int width) { int w; const uint8_t *s1; uint8_t *d; s1 = src; d = dst; for (w = width; w >= 4; w -= 4) { d[1] = d[0] = s1[0]; d[3] = d[2] = s1[1]; s1 += 2; d += 4; } for (; w >= 2; w -= 2) { d[1] = d[0] = s1[0]; s1++; d += 2; } /* only needed if width is not a multiple of two */ /* XXX: veryfy that */ if (w) { d[0] = s1[0]; } } static void grow41_line(uint8_t *dst, const uint8_t *src, int width) { int w, v; const uint8_t *s1; uint8_t *d; s1 = src; d = dst; for (w = width; w >= 4; w -= 4) { v = s1[0]; d[0] = v; d[1] = v; d[2] = v; d[3] = v; s1++; d += 4; } } /* 1x1 -> 2x1 */ static void grow21(uint8_t *dst, int dst_wrap, const uint8_t *src, int src_wrap, int width, int height) { for (; height > 0; height--) { grow21_line(dst, src, width); src += src_wrap; dst += dst_wrap; } } /* 1x1 -> 2x2 */ static void grow22(uint8_t *dst, int dst_wrap, const uint8_t *src, int src_wrap, int width, int height) { for (; height > 0; height--) { grow21_line(dst, src, width); if (height % 2) src += src_wrap; dst += dst_wrap; } } /* 1x1 -> 4x1 */ static void grow41(uint8_t *dst, int dst_wrap, const uint8_t *src, int src_wrap, int width, int height) { for (; height > 0; height--) { grow41_line(dst, src, width); src += src_wrap; dst += dst_wrap; } } /* 1x1 -> 4x4 */ static void grow44(uint8_t *dst, int dst_wrap, const uint8_t *src, int src_wrap, int width, int height) { for (; height > 0; height--) { grow41_line(dst, src, width); if ((height &3) == 1) src += src_wrap; dst += dst_wrap; } } /* 1x2 -> 2x1 */ static void conv411(uint8_t *dst, int dst_wrap, const uint8_t *src, int src_wrap, int width, int height) { int w, c; const uint8_t *s1, *s2; uint8_t *d; width >>= 1; for (; height > 0; height--) { s1 = src; s2 = src + src_wrap; d = dst; for (w = width; w > 0; w--) { c = (s1[0] + s2[0]) >> 1; d[0] = c; d[1] = c; s1++; s2++; d += 2; } src += src_wrap * 2; dst += dst_wrap; } } /* XXX: add jpeg quantize code */ #define TRANSP_INDEX (6*6*6) /* this is maybe slow, but allows for extensions */ static inline unsigned char gif_clut_index(uint8_t r, uint8_t g, uint8_t b) { return ((((r) / 47) % 6) *6 * 6+(((g) / 47) % 6) *6+(((b) / 47) % 6)); } static void build_rgb_palette(uint8_t *palette, int has_alpha) { uint32_t *pal; static const uint8_t pal_value[6] = {0x00, 0x33, 0x66, 0x99, 0xcc, 0xff }; int i, r, g, b; pal = (uint32_t*)palette; i = 0; for (r = 0; r < 6; r++) { for (g = 0; g < 6; g++) { for (b = 0; b < 6; b++) { pal[i++] = (0xff << 24) | (pal_value[r] << 16) | (pal_value[g] << 8) | pal_value[b]; } } } if (has_alpha) pal[i++] = 0; while (i < 256) pal[i++] = 0xff000000; } /* copy bit n to bits 0 ... n - 1 */ static inline unsigned int bitcopy_n(unsigned int a, int n) { int mask; mask = (1 << n) - 1; return (a &(0xff &~mask)) | (( - ((a >> n) &1)) &mask); } /* rgb555 handling */ #define RGB_NAME rgb555 #define RGB_IN(r, g, b, s)\ {\ unsigned int v = ((const uint16_t *)(s))[0];\ r = bitcopy_n(v >> (10 - 3), 3);\ g = bitcopy_n(v >> (5 - 3), 3);\ b = bitcopy_n(v << 3, 3);\ } #define RGBA_IN(r, g, b, a, s)\ {\ unsigned int v = ((const uint16_t *)(s))[0];\ r = bitcopy_n(v >> (10 - 3), 3);\ g = bitcopy_n(v >> (5 - 3), 3);\ b = bitcopy_n(v << 3, 3);\ a = (-(v >> 15)) & 0xff;\ } #define RGBA_OUT(d, r, g, b, a)\ {\ ((uint16_t *)(d))[0] = ((r >> 3) << 10) | ((g >> 3) << 5) | (b >> 3) | \ ((a << 8) & 0x8000);\ } #define BPP 2 #include "imgconvert_template.h" /* rgb565 handling */ #define RGB_NAME rgb565 #define RGB_IN(r, g, b, s)\ {\ unsigned int v = ((const uint16_t *)(s))[0];\ r = bitcopy_n(v >> (11 - 3), 3);\ g = bitcopy_n(v >> (5 - 2), 2);\ b = bitcopy_n(v << 3, 3);\ } #define RGB_OUT(d, r, g, b)\ {\ ((uint16_t *)(d))[0] = ((r >> 3) << 11) | ((g >> 2) << 5) | (b >> 3);\ } #define BPP 2 #include "imgconvert_template.h" /* bgr24 handling */ #define RGB_NAME bgr24 #define RGB_IN(r, g, b, s)\ {\ b = (s)[0];\ g = (s)[1];\ r = (s)[2];\ } #define RGB_OUT(d, r, g, b)\ {\ (d)[0] = b;\ (d)[1] = g;\ (d)[2] = r;\ } #define BPP 3 #include "imgconvert_template.h" #undef RGB_IN #undef RGB_OUT #undef BPP /* rgb24 handling */ #define RGB_NAME rgb24 #define FMT_RGB24 #define RGB_IN(r, g, b, s)\ {\ r = (s)[0];\ g = (s)[1];\ b = (s)[2];\ } #define RGB_OUT(d, r, g, b)\ {\ (d)[0] = r;\ (d)[1] = g;\ (d)[2] = b;\ } #define BPP 3 #include "imgconvert_template.h" /* rgba32 handling */ #define RGB_NAME rgba32 #define FMT_RGBA32 #define RGB_IN(r, g, b, s)\ {\ unsigned int v = ((const uint32_t *)(s))[0];\ r = (v >> 16) & 0xff;\ g = (v >> 8) & 0xff;\ b = v & 0xff;\ } #define RGBA_IN(r, g, b, a, s)\ {\ unsigned int v = ((const uint32_t *)(s))[0];\ a = (v >> 24) & 0xff;\ r = (v >> 16) & 0xff;\ g = (v >> 8) & 0xff;\ b = v & 0xff;\ } #define RGBA_OUT(d, r, g, b, a)\ {\ ((uint32_t *)(d))[0] = (a << 24) | (r << 16) | (g << 8) | b;\ } #define BPP 4 #include "imgconvert_template.h" static void mono_to_gray(AVPicture *dst, const AVPicture *src, int width, int height, int xor_mask) { const unsigned char *p; unsigned char *q; int v, dst_wrap, src_wrap; int y, w; p = src->data[0]; src_wrap = src->linesize[0] - ((width + 7) >> 3); q = dst->data[0]; dst_wrap = dst->linesize[0] - width; for (y = 0; y < height; y++) { w = width; while (w >= 8) { v = *p++ ^ xor_mask; q[0] = - (v >> 7); q[1] = - ((v >> 6) &1); q[2] = - ((v >> 5) &1); q[3] = - ((v >> 4) &1); q[4] = - ((v >> 3) &1); q[5] = - ((v >> 2) &1); q[6] = - ((v >> 1) &1); q[7] = - ((v >> 0) &1); w -= 8; q += 8; } if (w > 0) { v = *p++ ^ xor_mask; do { q[0] = - ((v >> 7) &1); q++; v <<= 1; } while (--w); } p += src_wrap; q += dst_wrap; } } static void monowhite_to_gray(AVPicture *dst, const AVPicture *src, int width, int height) { mono_to_gray(dst, src, width, height, 0xff); } static void monoblack_to_gray(AVPicture *dst, const AVPicture *src, int width, int height) { mono_to_gray(dst, src, width, height, 0x00); } static void gray_to_mono(AVPicture *dst, const AVPicture *src, int width, int height, int xor_mask) { int n; const uint8_t *s; uint8_t *d; int j, b, v, n1, src_wrap, dst_wrap, y; s = src->data[0]; src_wrap = src->linesize[0] - width; d = dst->data[0]; dst_wrap = dst->linesize[0] - ((width + 7) >> 3); for (y = 0; y < height; y++) { n = width; while (n >= 8) { v = 0; for (j = 0; j < 8; j++) { b = s[0]; s++; v = (v << 1) | (b >> 7); } d[0] = v ^ xor_mask; d++; n -= 8; } if (n > 0) { n1 = n; v = 0; while (n > 0) { b = s[0]; s++; v = (v << 1) | (b >> 7); n--; } d[0] = (v << (8-(n1 &7))) ^ xor_mask; d++; } s += src_wrap; d += dst_wrap; } } static void gray_to_monowhite(AVPicture *dst, const AVPicture *src, int width, int height) { gray_to_mono(dst, src, width, height, 0xff); } static void gray_to_monoblack(AVPicture *dst, const AVPicture *src, int width, int height) { gray_to_mono(dst, src, width, height, 0x00); } typedef struct ConvertEntry { void(*convert)(AVPicture *dst, const AVPicture *src, int width, int height); } ConvertEntry; /* Add each new convertion function in this table. In order to be able to convert from any format to any format, the following constraints must be satisfied: - all FF_COLOR_RGB formats must convert to and from PIX_FMT_RGB24 - all FF_COLOR_GRAY formats must convert to and from PIX_FMT_GRAY8 - all FF_COLOR_RGB formats with alpha must convert to and from PIX_FMT_RGBA32 - PIX_FMT_YUV444P and PIX_FMT_YUVJ444P must convert to and from PIX_FMT_RGB24. - PIX_FMT_422 must convert to and from PIX_FMT_422P. The other conversion functions are just optimisations for common cases. */ static ConvertEntry convert_table[PIX_FMT_NB][PIX_FMT_NB]; static void img_convert_init(void) { int i; uint8_t *cm = cropTbl + MAX_NEG_CROP; for (i = 0; i < 256; i++) { y_ccir_to_jpeg[i] = Y_CCIR_TO_JPEG(i); y_jpeg_to_ccir[i] = Y_JPEG_TO_CCIR(i); c_ccir_to_jpeg[i] = C_CCIR_TO_JPEG(i); c_jpeg_to_ccir[i] = C_JPEG_TO_CCIR(i); } convert_table[PIX_FMT_YUV420P][PIX_FMT_YUV422].convert = yuv420p_to_yuv422; convert_table[PIX_FMT_YUV420P][PIX_FMT_YUV422].convert = yuv420p_to_yuv422; convert_table[PIX_FMT_YUV420P][PIX_FMT_RGB555].convert = yuv420p_to_rgb555; convert_table[PIX_FMT_YUV420P][PIX_FMT_RGB565].convert = yuv420p_to_rgb565; convert_table[PIX_FMT_YUV420P][PIX_FMT_BGR24].convert = yuv420p_to_bgr24; convert_table[PIX_FMT_YUV420P][PIX_FMT_RGB24].convert = yuv420p_to_rgb24; convert_table[PIX_FMT_YUV420P][PIX_FMT_RGBA32].convert = yuv420p_to_rgba32; convert_table[PIX_FMT_YUV420P][PIX_FMT_UYVY422].convert = yuv420p_to_uyvy422; convert_table[PIX_FMT_YUV422P][PIX_FMT_YUV422].convert = yuv422p_to_yuv422; convert_table[PIX_FMT_YUV422P][PIX_FMT_UYVY422].convert = yuv422p_to_uyvy422; convert_table[PIX_FMT_YUV444P][PIX_FMT_RGB24].convert = yuv444p_to_rgb24; convert_table[PIX_FMT_YUVJ420P][PIX_FMT_RGB555].convert = yuvj420p_to_rgb555; convert_table[PIX_FMT_YUVJ420P][PIX_FMT_RGB565].convert = yuvj420p_to_rgb565; convert_table[PIX_FMT_YUVJ420P][PIX_FMT_BGR24].convert = yuvj420p_to_bgr24; convert_table[PIX_FMT_YUVJ420P][PIX_FMT_RGB24].convert = yuvj420p_to_rgb24; convert_table[PIX_FMT_YUVJ420P][PIX_FMT_RGBA32].convert = yuvj420p_to_rgba32; convert_table[PIX_FMT_YUVJ444P][PIX_FMT_RGB24].convert = yuvj444p_to_rgb24; convert_table[PIX_FMT_YUV422][PIX_FMT_YUV420P].convert = yuv422_to_yuv420p; convert_table[PIX_FMT_YUV422][PIX_FMT_YUV422P].convert = yuv422_to_yuv422p; convert_table[PIX_FMT_UYVY422][PIX_FMT_YUV420P].convert = uyvy422_to_yuv420p; convert_table[PIX_FMT_UYVY422][PIX_FMT_YUV422P].convert = uyvy422_to_yuv422p; convert_table[PIX_FMT_RGB24][PIX_FMT_YUV420P].convert = rgb24_to_yuv420p; convert_table[PIX_FMT_RGB24][PIX_FMT_RGB565].convert = rgb24_to_rgb565; convert_table[PIX_FMT_RGB24][PIX_FMT_RGB555].convert = rgb24_to_rgb555; convert_table[PIX_FMT_RGB24][PIX_FMT_RGBA32].convert = rgb24_to_rgba32; convert_table[PIX_FMT_RGB24][PIX_FMT_BGR24].convert = rgb24_to_bgr24; convert_table[PIX_FMT_RGB24][PIX_FMT_GRAY8].convert = rgb24_to_gray; convert_table[PIX_FMT_RGB24][PIX_FMT_PAL8].convert = rgb24_to_pal8; convert_table[PIX_FMT_RGB24][PIX_FMT_YUV444P].convert = rgb24_to_yuv444p; convert_table[PIX_FMT_RGB24][PIX_FMT_YUVJ420P].convert = rgb24_to_yuvj420p; convert_table[PIX_FMT_RGB24][PIX_FMT_YUVJ444P].convert = rgb24_to_yuvj444p; convert_table[PIX_FMT_RGBA32][PIX_FMT_RGB24].convert = rgba32_to_rgb24; convert_table[PIX_FMT_RGBA32][PIX_FMT_RGB555].convert = rgba32_to_rgb555; convert_table[PIX_FMT_RGBA32][PIX_FMT_PAL8].convert = rgba32_to_pal8; convert_table[PIX_FMT_RGBA32][PIX_FMT_YUV420P].convert = rgba32_to_yuv420p; convert_table[PIX_FMT_RGBA32][PIX_FMT_GRAY8].convert = rgba32_to_gray; convert_table[PIX_FMT_BGR24][PIX_FMT_RGB24].convert = bgr24_to_rgb24; convert_table[PIX_FMT_BGR24][PIX_FMT_YUV420P].convert = bgr24_to_yuv420p; convert_table[PIX_FMT_BGR24][PIX_FMT_GRAY8].convert = bgr24_to_gray; convert_table[PIX_FMT_RGB555][PIX_FMT_RGB24].convert = rgb555_to_rgb24; convert_table[PIX_FMT_RGB555][PIX_FMT_RGBA32].convert = rgb555_to_rgba32; convert_table[PIX_FMT_RGB555][PIX_FMT_YUV420P].convert = rgb555_to_yuv420p; convert_table[PIX_FMT_RGB555][PIX_FMT_GRAY8].convert = rgb555_to_gray; convert_table[PIX_FMT_RGB565][PIX_FMT_RGB24].convert = rgb565_to_rgb24; convert_table[PIX_FMT_RGB565][PIX_FMT_YUV420P].convert = rgb565_to_yuv420p; convert_table[PIX_FMT_RGB565][PIX_FMT_GRAY8].convert = rgb565_to_gray; convert_table[PIX_FMT_GRAY8][PIX_FMT_RGB555].convert = gray_to_rgb555; convert_table[PIX_FMT_GRAY8][PIX_FMT_RGB565].convert = gray_to_rgb565; convert_table[PIX_FMT_GRAY8][PIX_FMT_RGB24].convert = gray_to_rgb24; convert_table[PIX_FMT_GRAY8][PIX_FMT_BGR24].convert = gray_to_bgr24; convert_table[PIX_FMT_GRAY8][PIX_FMT_RGBA32].convert = gray_to_rgba32; convert_table[PIX_FMT_GRAY8][PIX_FMT_MONOWHITE].convert = gray_to_monowhite; convert_table[PIX_FMT_GRAY8][PIX_FMT_MONOBLACK].convert = gray_to_monoblack; convert_table[PIX_FMT_MONOWHITE][PIX_FMT_GRAY8].convert = monowhite_to_gray; convert_table[PIX_FMT_MONOBLACK][PIX_FMT_GRAY8].convert = monoblack_to_gray; convert_table[PIX_FMT_PAL8][PIX_FMT_RGB555].convert = pal8_to_rgb555; convert_table[PIX_FMT_PAL8][PIX_FMT_RGB565].convert = pal8_to_rgb565; convert_table[PIX_FMT_PAL8][PIX_FMT_BGR24].convert = pal8_to_bgr24; convert_table[PIX_FMT_PAL8][PIX_FMT_RGB24].convert = pal8_to_rgb24; convert_table[PIX_FMT_PAL8][PIX_FMT_RGBA32].convert = pal8_to_rgba32; convert_table[PIX_FMT_UYVY411][PIX_FMT_YUV411P].convert = uyvy411_to_yuv411p; } static inline int is_yuv_planar(PixFmtInfo *ps) { return (ps->color_type == FF_COLOR_YUV || ps->color_type == FF_COLOR_YUV_JPEG) && ps->pixel_type == FF_PIXEL_PLANAR; } int img_convert(AVPicture *dst, int dst_pix_fmt, const AVPicture *src, int src_pix_fmt, int src_width, int src_height) { static int inited; int i, ret, dst_width, dst_height, int_pix_fmt; PixFmtInfo *src_pix, *dst_pix; ConvertEntry *ce; AVPicture tmp1, *tmp = &tmp1; if (src_pix_fmt < 0 || src_pix_fmt >= PIX_FMT_NB || dst_pix_fmt < 0 || dst_pix_fmt >= PIX_FMT_NB) return - 1; if (src_width <= 0 || src_height <= 0) return 0; if (!inited) { inited = 1; img_convert_init(); } dst_width = src_width; dst_height = src_height; dst_pix = &pix_fmt_info[dst_pix_fmt]; src_pix = &pix_fmt_info[src_pix_fmt]; if (src_pix_fmt == dst_pix_fmt) // no conversion needed: just copy { img_copy(dst, src, dst_pix_fmt, dst_width, dst_height); return 0; } ce = &convert_table[src_pix_fmt][dst_pix_fmt]; if (ce->convert) { ce->convert(dst, src, dst_width, dst_height); // specific conversion routine return 0; } if (is_yuv_planar(dst_pix) && src_pix_fmt == PIX_FMT_GRAY8) // gray to YUV { int w, h, y; uint8_t *d; if (dst_pix->color_type == FF_COLOR_YUV_JPEG) { ff_img_copy_plane(dst->data[0], dst->linesize[0], src->data[0], src->linesize[0], dst_width, dst_height); } else { img_apply_table(dst->data[0], dst->linesize[0], src->data[0], src->linesize[0], dst_width, dst_height, y_jpeg_to_ccir); } w = dst_width; // fill U and V with 128 h = dst_height; w >>= dst_pix->x_chroma_shift; h >>= dst_pix->y_chroma_shift; for (i = 1; i <= 2; i++) { d = dst->data[i]; for (y = 0; y < h; y++) { memset(d, 128, w); d += dst->linesize[i]; } } return 0; } if (is_yuv_planar(src_pix) && dst_pix_fmt == PIX_FMT_GRAY8) // YUV to gray { if (src_pix->color_type == FF_COLOR_YUV_JPEG) { ff_img_copy_plane(dst->data[0], dst->linesize[0], src->data[0], src->linesize[0], dst_width, dst_height); } else { img_apply_table(dst->data[0], dst->linesize[0], src->data[0], src->linesize[0], dst_width, dst_height, y_ccir_to_jpeg); } return 0; } if (is_yuv_planar(dst_pix) && is_yuv_planar(src_pix)) // YUV to YUV planar { int x_shift, y_shift, w, h, xy_shift; void(*resize_func)(uint8_t *dst, int dst_wrap, const uint8_t *src, int src_wrap, int width, int height); // compute chroma size of the smallest dimensions w = dst_width; h = dst_height; if (dst_pix->x_chroma_shift >= src_pix->x_chroma_shift) w >>= dst_pix->x_chroma_shift; else w >>= src_pix->x_chroma_shift; if (dst_pix->y_chroma_shift >= src_pix->y_chroma_shift) h >>= dst_pix->y_chroma_shift; else h >>= src_pix->y_chroma_shift; x_shift = (dst_pix->x_chroma_shift - src_pix->x_chroma_shift); y_shift = (dst_pix->y_chroma_shift - src_pix->y_chroma_shift); xy_shift = ((x_shift &0xf) << 4) | (y_shift &0xf); // there must be filters for conversion at least from and to YUV444 format switch (xy_shift) { case 0x00: resize_func = ff_img_copy_plane; break; case 0x10: resize_func = shrink21; break; case 0x20: resize_func = shrink41; break; case 0x01: resize_func = shrink12; break; case 0x11: resize_func = ff_shrink22; break; case 0x22: resize_func = ff_shrink44; break; case 0xf0: resize_func = grow21; break; case 0xe0: resize_func = grow41; break; case 0xff: resize_func = grow22; break; case 0xee: resize_func = grow44; break; case 0xf1: resize_func = conv411; break; default: goto no_chroma_filter; // currently not handled } ff_img_copy_plane(dst->data[0], dst->linesize[0], src->data[0], src->linesize[0], dst_width, dst_height); for (i = 1; i <= 2; i++) resize_func(dst->data[i], dst->linesize[i], src->data[i], src->linesize[i], dst_width >> dst_pix->x_chroma_shift, dst_height >> dst_pix->y_chroma_shift); // if yuv color space conversion is needed, we do it here on the destination image if (dst_pix->color_type != src_pix->color_type) { const uint8_t *y_table, *c_table; if (dst_pix->color_type == FF_COLOR_YUV) { y_table = y_jpeg_to_ccir; c_table = c_jpeg_to_ccir; } else { y_table = y_ccir_to_jpeg; c_table = c_ccir_to_jpeg; } img_apply_table(dst->data[0], dst->linesize[0], dst->data[0], dst->linesize[0], dst_width, dst_height, y_table); for (i = 1; i <= 2; i++) img_apply_table(dst->data[i], dst->linesize[i], dst->data[i], dst->linesize[i], dst_width >> dst_pix->x_chroma_shift, dst_height >> dst_pix->y_chroma_shift, c_table); } return 0; } no_chroma_filter: // try to use an intermediate format if (src_pix_fmt == PIX_FMT_YUV422 || dst_pix_fmt == PIX_FMT_YUV422) { int_pix_fmt = PIX_FMT_YUV422P; // specific case: convert to YUV422P first } else if (src_pix_fmt == PIX_FMT_UYVY422 || dst_pix_fmt == PIX_FMT_UYVY422) { int_pix_fmt = PIX_FMT_YUV422P; // specific case: convert to YUV422P first } else if (src_pix_fmt == PIX_FMT_UYVY411 || dst_pix_fmt == PIX_FMT_UYVY411) { int_pix_fmt = PIX_FMT_YUV411P; // specific case: convert to YUV411P first } else if ((src_pix->color_type == FF_COLOR_GRAY && src_pix_fmt != PIX_FMT_GRAY8) || (dst_pix->color_type == FF_COLOR_GRAY && dst_pix_fmt != PIX_FMT_GRAY8)) { int_pix_fmt = PIX_FMT_GRAY8; // gray8 is the normalized format } else if ((is_yuv_planar(src_pix) && src_pix_fmt != PIX_FMT_YUV444P && src_pix_fmt != PIX_FMT_YUVJ444P)) { if (src_pix->color_type == FF_COLOR_YUV_JPEG) // yuv444 is the normalized format int_pix_fmt = PIX_FMT_YUVJ444P; else int_pix_fmt = PIX_FMT_YUV444P; } else if ((is_yuv_planar(dst_pix) && dst_pix_fmt != PIX_FMT_YUV444P && dst_pix_fmt != PIX_FMT_YUVJ444P)) { if (dst_pix->color_type == FF_COLOR_YUV_JPEG) // yuv444 is the normalized format int_pix_fmt = PIX_FMT_YUVJ444P; else int_pix_fmt = PIX_FMT_YUV444P; } else // the two formats are rgb or gray8 or yuv[j]444p { if (src_pix->is_alpha && dst_pix->is_alpha) int_pix_fmt = PIX_FMT_RGBA32; else int_pix_fmt = PIX_FMT_RGB24; } if (avpicture_alloc(tmp, int_pix_fmt, dst_width, dst_height) < 0) return - 1; ret = - 1; if (img_convert(tmp, int_pix_fmt, src, src_pix_fmt, src_width, src_height) < 0) goto fail1; if (img_convert(dst, dst_pix_fmt, tmp, int_pix_fmt, dst_width, dst_height) < 0) goto fail1; ret = 0; fail1: avpicture_free(tmp); return ret; } #undef FIX ================================================ FILE: ffplay源码和书籍/ffplay/libavcodec/imgconvert_template.h ================================================ #ifndef RGB_OUT #define RGB_OUT(d, r, g, b) RGBA_OUT(d, r, g, b, 0xff) #endif /************************************************************************/ /* 岢ʵͼɫռתʹõĺͺ */ /************************************************************************/ #pragma warning (disable:4305 4244) static void glue(yuv420p_to_, RGB_NAME)(AVPicture *dst, const AVPicture *src, int width, int height) { const uint8_t *y1_ptr, *y2_ptr, *cb_ptr, *cr_ptr; uint8_t *d, *d1, *d2; int w, y, cb, cr, r_add, g_add, b_add, width2; uint8_t *cm = cropTbl + MAX_NEG_CROP; unsigned int r, g, b; d = dst->data[0]; y1_ptr = src->data[0]; cb_ptr = src->data[1]; cr_ptr = src->data[2]; width2 = (width + 1) >> 1; for (; height >= 2; height -= 2) { d1 = d; d2 = d + dst->linesize[0]; y2_ptr = y1_ptr + src->linesize[0]; for (w = width; w >= 2; w -= 2) { YUV_TO_RGB1_CCIR(cb_ptr[0], cr_ptr[0]); YUV_TO_RGB2_CCIR(r, g, b, y1_ptr[0]); /* output 4 pixels */ RGB_OUT(d1, r, g, b); YUV_TO_RGB2_CCIR(r, g, b, y1_ptr[1]); RGB_OUT(d1 + BPP, r, g, b); YUV_TO_RGB2_CCIR(r, g, b, y2_ptr[0]); RGB_OUT(d2, r, g, b); YUV_TO_RGB2_CCIR(r, g, b, y2_ptr[1]); RGB_OUT(d2 + BPP, r, g, b); d1 += 2 * BPP; d2 += 2 * BPP; y1_ptr += 2; y2_ptr += 2; cb_ptr++; cr_ptr++; } if (w) /* handle odd width */ { YUV_TO_RGB1_CCIR(cb_ptr[0], cr_ptr[0]); YUV_TO_RGB2_CCIR(r, g, b, y1_ptr[0]); RGB_OUT(d1, r, g, b); YUV_TO_RGB2_CCIR(r, g, b, y2_ptr[0]); RGB_OUT(d2, r, g, b); d1 += BPP; d2 += BPP; y1_ptr++; y2_ptr++; cb_ptr++; cr_ptr++; } d += 2 * dst->linesize[0]; y1_ptr += 2 * src->linesize[0] - width; cb_ptr += src->linesize[1] - width2; cr_ptr += src->linesize[2] - width2; } if (height) /* handle odd height */ { d1 = d; for (w = width; w >= 2; w -= 2) { YUV_TO_RGB1_CCIR(cb_ptr[0], cr_ptr[0]); YUV_TO_RGB2_CCIR(r, g, b, y1_ptr[0]); /* output 2 pixels */ RGB_OUT(d1, r, g, b); YUV_TO_RGB2_CCIR(r, g, b, y1_ptr[1]); RGB_OUT(d1 + BPP, r, g, b); d1 += 2 * BPP; y1_ptr += 2; cb_ptr++; cr_ptr++; } if (w) /* handle width */ { YUV_TO_RGB1_CCIR(cb_ptr[0], cr_ptr[0]); YUV_TO_RGB2_CCIR(r, g, b, y1_ptr[0]); /* output 2 pixels */ RGB_OUT(d1, r, g, b); d1 += BPP; y1_ptr++; cb_ptr++; cr_ptr++; } } } static void glue(yuvj420p_to_, RGB_NAME)(AVPicture *dst, const AVPicture *src, int width, int height) { const uint8_t *y1_ptr, *y2_ptr, *cb_ptr, *cr_ptr; uint8_t *d, *d1, *d2; int w, y, cb, cr, r_add, g_add, b_add, width2; uint8_t *cm = cropTbl + MAX_NEG_CROP; unsigned int r, g, b; d = dst->data[0]; y1_ptr = src->data[0]; cb_ptr = src->data[1]; cr_ptr = src->data[2]; width2 = (width + 1) >> 1; for (; height >= 2; height -= 2) { d1 = d; d2 = d + dst->linesize[0]; y2_ptr = y1_ptr + src->linesize[0]; for (w = width; w >= 2; w -= 2) { YUV_TO_RGB1(cb_ptr[0], cr_ptr[0]); YUV_TO_RGB2(r, g, b, y1_ptr[0]); /* output 4 pixels */ RGB_OUT(d1, r, g, b); YUV_TO_RGB2(r, g, b, y1_ptr[1]); RGB_OUT(d1 + BPP, r, g, b); YUV_TO_RGB2(r, g, b, y2_ptr[0]); RGB_OUT(d2, r, g, b); YUV_TO_RGB2(r, g, b, y2_ptr[1]); RGB_OUT(d2 + BPP, r, g, b); d1 += 2 * BPP; d2 += 2 * BPP; y1_ptr += 2; y2_ptr += 2; cb_ptr++; cr_ptr++; } if (w) /* handle odd width */ { YUV_TO_RGB1(cb_ptr[0], cr_ptr[0]); YUV_TO_RGB2(r, g, b, y1_ptr[0]); RGB_OUT(d1, r, g, b); YUV_TO_RGB2(r, g, b, y2_ptr[0]); RGB_OUT(d2, r, g, b); d1 += BPP; d2 += BPP; y1_ptr++; y2_ptr++; cb_ptr++; cr_ptr++; } d += 2 * dst->linesize[0]; y1_ptr += 2 * src->linesize[0] - width; cb_ptr += src->linesize[1] - width2; cr_ptr += src->linesize[2] - width2; } if (height) /* handle odd height */ { d1 = d; for (w = width; w >= 2; w -= 2) { YUV_TO_RGB1(cb_ptr[0], cr_ptr[0]); YUV_TO_RGB2(r, g, b, y1_ptr[0]); /* output 2 pixels */ RGB_OUT(d1, r, g, b); YUV_TO_RGB2(r, g, b, y1_ptr[1]); RGB_OUT(d1 + BPP, r, g, b); d1 += 2 * BPP; y1_ptr += 2; cb_ptr++; cr_ptr++; } if (w) /* handle width */ { YUV_TO_RGB1(cb_ptr[0], cr_ptr[0]); YUV_TO_RGB2(r, g, b, y1_ptr[0]); /* output 2 pixels */ RGB_OUT(d1, r, g, b); d1 += BPP; y1_ptr++; cb_ptr++; cr_ptr++; } } } static void glue(RGB_NAME, _to_yuv420p)(AVPicture *dst, const AVPicture *src, int width, int height) { int wrap, wrap3, width2; int r, g, b, r1, g1, b1, w; uint8_t *lum, *cb, *cr; const uint8_t *p; lum = dst->data[0]; cb = dst->data[1]; cr = dst->data[2]; width2 = (width + 1) >> 1; wrap = dst->linesize[0]; wrap3 = src->linesize[0]; p = src->data[0]; for (; height >= 2; height -= 2) { for (w = width; w >= 2; w -= 2) { RGB_IN(r, g, b, p); r1 = r; g1 = g; b1 = b; lum[0] = RGB_TO_Y_CCIR(r, g, b); RGB_IN(r, g, b, p + BPP); r1 += r; g1 += g; b1 += b; lum[1] = RGB_TO_Y_CCIR(r, g, b); p += wrap3; lum += wrap; RGB_IN(r, g, b, p); r1 += r; g1 += g; b1 += b; lum[0] = RGB_TO_Y_CCIR(r, g, b); RGB_IN(r, g, b, p + BPP); r1 += r; g1 += g; b1 += b; lum[1] = RGB_TO_Y_CCIR(r, g, b); cb[0] = RGB_TO_U_CCIR(r1, g1, b1, 2); cr[0] = RGB_TO_V_CCIR(r1, g1, b1, 2); cb++; cr++; p += - wrap3 + 2 * BPP; lum += - wrap + 2; } if (w) { RGB_IN(r, g, b, p); r1 = r; g1 = g; b1 = b; lum[0] = RGB_TO_Y_CCIR(r, g, b); p += wrap3; lum += wrap; RGB_IN(r, g, b, p); r1 += r; g1 += g; b1 += b; lum[0] = RGB_TO_Y_CCIR(r, g, b); cb[0] = RGB_TO_U_CCIR(r1, g1, b1, 1); cr[0] = RGB_TO_V_CCIR(r1, g1, b1, 1); cb++; cr++; p += - wrap3 + BPP; lum += - wrap + 1; } p += wrap3 + (wrap3 - width * BPP); lum += wrap + (wrap - width); cb += dst->linesize[1] - width2; cr += dst->linesize[2] - width2; } if (height) /* handle odd height */ { for (w = width; w >= 2; w -= 2) { RGB_IN(r, g, b, p); r1 = r; g1 = g; b1 = b; lum[0] = RGB_TO_Y_CCIR(r, g, b); RGB_IN(r, g, b, p + BPP); r1 += r; g1 += g; b1 += b; lum[1] = RGB_TO_Y_CCIR(r, g, b); cb[0] = RGB_TO_U_CCIR(r1, g1, b1, 1); cr[0] = RGB_TO_V_CCIR(r1, g1, b1, 1); cb++; cr++; p += 2 * BPP; lum += 2; } if (w) { RGB_IN(r, g, b, p); lum[0] = RGB_TO_Y_CCIR(r, g, b); cb[0] = RGB_TO_U_CCIR(r, g, b, 0); cr[0] = RGB_TO_V_CCIR(r, g, b, 0); } } } static void glue(RGB_NAME, _to_gray)(AVPicture *dst, const AVPicture *src, int width, int height) { const unsigned char *p; unsigned char *q; int r, g, b, dst_wrap, src_wrap; int x, y; p = src->data[0]; src_wrap = src->linesize[0] - BPP * width; q = dst->data[0]; dst_wrap = dst->linesize[0] - width; for (y = 0; y < height; y++) { for (x = 0; x < width; x++) { RGB_IN(r, g, b, p); q[0] = RGB_TO_Y(r, g, b); q++; p += BPP; } p += src_wrap; q += dst_wrap; } } static void glue(gray_to_, RGB_NAME)(AVPicture *dst, const AVPicture *src, int width, int height) { const unsigned char *p; unsigned char *q; int r, dst_wrap, src_wrap; int x, y; p = src->data[0]; src_wrap = src->linesize[0] - width; q = dst->data[0]; dst_wrap = dst->linesize[0] - BPP * width; for (y = 0; y < height; y++) { for (x = 0; x < width; x++) { r = p[0]; RGB_OUT(q, r, r, r); q += BPP; p++; } p += src_wrap; q += dst_wrap; } } static void glue(pal8_to_, RGB_NAME)(AVPicture *dst, const AVPicture *src, int width, int height) { const unsigned char *p; unsigned char *q; int r, g, b, dst_wrap, src_wrap; int x, y; uint32_t v; const uint32_t *palette; p = src->data[0]; src_wrap = src->linesize[0] - width; palette = (uint32_t*)src->data[1]; q = dst->data[0]; dst_wrap = dst->linesize[0] - BPP * width; for (y = 0; y < height; y++) { for (x = 0; x < width; x++) { v = palette[p[0]]; r = (v >> 16) &0xff; g = (v >> 8) &0xff; b = (v) &0xff; #ifdef RGBA_OUT { int a; a = (v >> 24) &0xff; RGBA_OUT(q, r, g, b, a); } #else RGB_OUT(q, r, g, b); #endif q += BPP; p++; } p += src_wrap; q += dst_wrap; } } #if !defined(FMT_RGBA32) && defined(RGBA_OUT) /* alpha support */ static void glue(rgba32_to_, RGB_NAME)(AVPicture *dst, const AVPicture *src, int width, int height) { const uint8_t *s; uint8_t *d; int src_wrap, dst_wrap, j, y; unsigned int v, r, g, b, a; s = src->data[0]; src_wrap = src->linesize[0] - width * 4; d = dst->data[0]; dst_wrap = dst->linesize[0] - width * BPP; for (y = 0; y < height; y++) { for (j = 0; j < width; j++) { v = ((const uint32_t*)(s))[0]; a = (v >> 24) &0xff; r = (v >> 16) &0xff; g = (v >> 8) &0xff; b = v &0xff; RGBA_OUT(d, r, g, b, a); s += 4; d += BPP; } s += src_wrap; d += dst_wrap; } } static void glue(RGB_NAME, _to_rgba32)(AVPicture *dst, const AVPicture *src, int width, int height) { const uint8_t *s; uint8_t *d; int src_wrap, dst_wrap, j, y; unsigned int r, g, b, a; s = src->data[0]; src_wrap = src->linesize[0] - width * BPP; d = dst->data[0]; dst_wrap = dst->linesize[0] - width * 4; for (y = 0; y < height; y++) { for (j = 0; j < width; j++) { RGBA_IN(r, g, b, a, s); ((uint32_t*)(d))[0] = (a << 24) | (r << 16) | (g << 8) | b; d += 4; s += BPP; } s += src_wrap; d += dst_wrap; } } #endif /* !defined(FMT_RGBA32) && defined(RGBA_IN) */ #ifndef FMT_RGB24 static void glue(rgb24_to_, RGB_NAME)(AVPicture *dst, const AVPicture *src, int width, int height) { const uint8_t *s; uint8_t *d; int src_wrap, dst_wrap, j, y; unsigned int r, g, b; s = src->data[0]; src_wrap = src->linesize[0] - width * 3; d = dst->data[0]; dst_wrap = dst->linesize[0] - width * BPP; for (y = 0; y < height; y++) { for (j = 0; j < width; j++) { r = s[0]; g = s[1]; b = s[2]; RGB_OUT(d, r, g, b); s += 3; d += BPP; } s += src_wrap; d += dst_wrap; } } static void glue(RGB_NAME, _to_rgb24)(AVPicture *dst, const AVPicture *src, int width, int height) { const uint8_t *s; uint8_t *d; int src_wrap, dst_wrap, j, y; unsigned int r, g, b; s = src->data[0]; src_wrap = src->linesize[0] - width * BPP; d = dst->data[0]; dst_wrap = dst->linesize[0] - width * 3; for (y = 0; y < height; y++) { for (j = 0; j < width; j++) { RGB_IN(r, g, b, s)d[0] = r; d[1] = g; d[2] = b; d += 3; s += BPP; } s += src_wrap; d += dst_wrap; } } #endif /* !FMT_RGB24 */ #ifdef FMT_RGB24 static void yuv444p_to_rgb24(AVPicture *dst, const AVPicture *src, int width, int height) { const uint8_t *y1_ptr, *cb_ptr, *cr_ptr; uint8_t *d, *d1; int w, y, cb, cr, r_add, g_add, b_add; uint8_t *cm = cropTbl + MAX_NEG_CROP; unsigned int r, g, b; d = dst->data[0]; y1_ptr = src->data[0]; cb_ptr = src->data[1]; cr_ptr = src->data[2]; for (; height > 0; height--) { d1 = d; for (w = width; w > 0; w--) { YUV_TO_RGB1_CCIR(cb_ptr[0], cr_ptr[0]); YUV_TO_RGB2_CCIR(r, g, b, y1_ptr[0]); RGB_OUT(d1, r, g, b); d1 += BPP; y1_ptr++; cb_ptr++; cr_ptr++; } d += dst->linesize[0]; y1_ptr += src->linesize[0] - width; cb_ptr += src->linesize[1] - width; cr_ptr += src->linesize[2] - width; } } static void yuvj444p_to_rgb24(AVPicture *dst, const AVPicture *src, int width, int height) { const uint8_t *y1_ptr, *cb_ptr, *cr_ptr; uint8_t *d, *d1; int w, y, cb, cr, r_add, g_add, b_add; uint8_t *cm = cropTbl + MAX_NEG_CROP; unsigned int r, g, b; d = dst->data[0]; y1_ptr = src->data[0]; cb_ptr = src->data[1]; cr_ptr = src->data[2]; for (; height > 0; height--) { d1 = d; for (w = width; w > 0; w--) { YUV_TO_RGB1(cb_ptr[0], cr_ptr[0]); YUV_TO_RGB2(r, g, b, y1_ptr[0]); RGB_OUT(d1, r, g, b); d1 += BPP; y1_ptr++; cb_ptr++; cr_ptr++; } d += dst->linesize[0]; y1_ptr += src->linesize[0] - width; cb_ptr += src->linesize[1] - width; cr_ptr += src->linesize[2] - width; } } static void rgb24_to_yuv444p(AVPicture *dst, const AVPicture *src, int width, int height) { int src_wrap, x, y; int r, g, b; uint8_t *lum, *cb, *cr; const uint8_t *p; lum = dst->data[0]; cb = dst->data[1]; cr = dst->data[2]; src_wrap = src->linesize[0] - width * BPP; p = src->data[0]; for (y = 0; y < height; y++) { for (x = 0; x < width; x++) { RGB_IN(r, g, b, p); lum[0] = RGB_TO_Y_CCIR(r, g, b); cb[0] = RGB_TO_U_CCIR(r, g, b, 0); cr[0] = RGB_TO_V_CCIR(r, g, b, 0); p += BPP; cb++; cr++; lum++; } p += src_wrap; lum += dst->linesize[0] - width; cb += dst->linesize[1] - width; cr += dst->linesize[2] - width; } } static void rgb24_to_yuvj420p(AVPicture *dst, const AVPicture *src, int width, int height) { int wrap, wrap3, width2; int r, g, b, r1, g1, b1, w; uint8_t *lum, *cb, *cr; const uint8_t *p; lum = dst->data[0]; cb = dst->data[1]; cr = dst->data[2]; width2 = (width + 1) >> 1; wrap = dst->linesize[0]; wrap3 = src->linesize[0]; p = src->data[0]; for (; height >= 2; height -= 2) { for (w = width; w >= 2; w -= 2) { RGB_IN(r, g, b, p); r1 = r; g1 = g; b1 = b; lum[0] = RGB_TO_Y(r, g, b); RGB_IN(r, g, b, p + BPP); r1 += r; g1 += g; b1 += b; lum[1] = RGB_TO_Y(r, g, b); p += wrap3; lum += wrap; RGB_IN(r, g, b, p); r1 += r; g1 += g; b1 += b; lum[0] = RGB_TO_Y(r, g, b); RGB_IN(r, g, b, p + BPP); r1 += r; g1 += g; b1 += b; lum[1] = RGB_TO_Y(r, g, b); cb[0] = RGB_TO_U(r1, g1, b1, 2); cr[0] = RGB_TO_V(r1, g1, b1, 2); cb++; cr++; p += - wrap3 + 2 * BPP; lum += - wrap + 2; } if (w) { RGB_IN(r, g, b, p); r1 = r; g1 = g; b1 = b; lum[0] = RGB_TO_Y(r, g, b); p += wrap3; lum += wrap; RGB_IN(r, g, b, p); r1 += r; g1 += g; b1 += b; lum[0] = RGB_TO_Y(r, g, b); cb[0] = RGB_TO_U(r1, g1, b1, 1); cr[0] = RGB_TO_V(r1, g1, b1, 1); cb++; cr++; p += - wrap3 + BPP; lum += - wrap + 1; } p += wrap3 + (wrap3 - width * BPP); lum += wrap + (wrap - width); cb += dst->linesize[1] - width2; cr += dst->linesize[2] - width2; } if (height) /* handle odd height */ { for (w = width; w >= 2; w -= 2) { RGB_IN(r, g, b, p); r1 = r; g1 = g; b1 = b; lum[0] = RGB_TO_Y(r, g, b); RGB_IN(r, g, b, p + BPP); r1 += r; g1 += g; b1 += b; lum[1] = RGB_TO_Y(r, g, b); cb[0] = RGB_TO_U(r1, g1, b1, 1); cr[0] = RGB_TO_V(r1, g1, b1, 1); cb++; cr++; p += 2 * BPP; lum += 2; } if (w) { RGB_IN(r, g, b, p); lum[0] = RGB_TO_Y(r, g, b); cb[0] = RGB_TO_U(r, g, b, 0); cr[0] = RGB_TO_V(r, g, b, 0); } } } static void rgb24_to_yuvj444p(AVPicture *dst, const AVPicture *src, int width, int height) { int src_wrap, x, y; int r, g, b; uint8_t *lum, *cb, *cr; const uint8_t *p; lum = dst->data[0]; cb = dst->data[1]; cr = dst->data[2]; src_wrap = src->linesize[0] - width * BPP; p = src->data[0]; for (y = 0; y < height; y++) { for (x = 0; x < width; x++) { RGB_IN(r, g, b, p); lum[0] = RGB_TO_Y(r, g, b); cb[0] = RGB_TO_U(r, g, b, 0); cr[0] = RGB_TO_V(r, g, b, 0); p += BPP; cb++; cr++; lum++; } p += src_wrap; lum += dst->linesize[0] - width; cb += dst->linesize[1] - width; cr += dst->linesize[2] - width; } } #endif /* FMT_RGB24 */ #if defined(FMT_RGB24) || defined(FMT_RGBA32) static void glue(RGB_NAME, _to_pal8)(AVPicture *dst, const AVPicture *src, int width, int height) { const unsigned char *p; unsigned char *q; int dst_wrap, src_wrap; int x, y, has_alpha; unsigned int r, g, b; p = src->data[0]; src_wrap = src->linesize[0] - BPP * width; q = dst->data[0]; dst_wrap = dst->linesize[0] - width; has_alpha = 0; for (y = 0; y < height; y++) { for (x = 0; x < width; x++) { #ifdef RGBA_IN { unsigned int a; RGBA_IN(r, g, b, a, p); if (a < 0x80) /* crude approximation for alpha ! */ { has_alpha = 1; q[0] = TRANSP_INDEX; } else { q[0] = gif_clut_index(r, g, b); } } #else RGB_IN(r, g, b, p); q[0] = gif_clut_index(r, g, b); #endif q++; p += BPP; } p += src_wrap; q += dst_wrap; } build_rgb_palette(dst->data[1], has_alpha); } #endif /* defined(FMT_RGB24) || defined(FMT_RGBA32) */ #ifdef RGBA_IN #define FF_ALPHA_TRANSP 0x0001 /* image has some totally transparent pixels */ #define FF_ALPHA_SEMI_TRANSP 0x0002 /* image has some transparent pixels */ static int glue(get_alpha_info_, RGB_NAME)(const AVPicture *src, int width, int height) { const unsigned char *p; int src_wrap, ret, x, y; unsigned int r, g, b, a; p = src->data[0]; src_wrap = src->linesize[0] - BPP * width; ret = 0; for (y = 0; y < height; y++) { for (x = 0; x < width; x++) { RGBA_IN(r, g, b, a, p); if (a == 0x00) { ret |= FF_ALPHA_TRANSP; } else if (a != 0xff) { ret |= FF_ALPHA_SEMI_TRANSP; } p += BPP; } p += src_wrap; } return ret; } #endif /* RGBA_IN */ #undef RGB_IN #undef RGBA_IN #undef RGB_OUT #undef RGBA_OUT #undef BPP #undef RGB_NAME #undef FMT_RGB24 #undef FMT_RGBA32 ================================================ FILE: ffplay源码和书籍/ffplay/libavcodec/msrle.c ================================================ /* ** ļʵ΢г̳ѹ㷨 */ #include #include #include #include "../libavutil/common.h" #include "avcodec.h" #include "dsputil.h" #define FF_BUFFER_HINTS_VALID 0x01 // Buffer hints value is meaningful (if 0 ignore) #define FF_BUFFER_HINTS_READABLE 0x02 // Codec will read from buffer #define FF_BUFFER_HINTS_PRESERVE 0x04 // User must not alter buffer content #define FF_BUFFER_HINTS_REUSABLE 0x08 // Codec will reuse the buffer (update) /* Msrle */ typedef struct MsrleContext { // ĽģAVCodecContextһȽϳĸMsrleContextһȽϾĸ AVCodecContext *avctx; // ֡ AVFrame frame; unsigned char *buf; int size; } MsrleContext; #define FETCH_NEXT_STREAM_BYTE() \ if (stream_ptr >= s->size) \ { \ return; \ } \ stream_byte = s->buf[stream_ptr++]; static void msrle_decode_pal4(MsrleContext *s) { int stream_ptr = 0; unsigned char rle_code; unsigned char extra_byte, odd_pixel; unsigned char stream_byte; int pixel_ptr = 0; int row_dec = s->frame.linesize[0]; int row_ptr = (s->avctx->height - 1) *row_dec; int frame_size = row_dec * s->avctx->height; int i; // make the palette available memcpy(s->frame.data[1], s->avctx->palctrl->palette, AVPALETTE_SIZE); if (s->avctx->palctrl->palette_changed) { // s->frame.palette_has_changed = 1; s->avctx->palctrl->palette_changed = 0; } while (row_ptr >= 0) { FETCH_NEXT_STREAM_BYTE(); rle_code = stream_byte; if (rle_code == 0) { // fetch the next byte to see how to handle escape code FETCH_NEXT_STREAM_BYTE(); if (stream_byte == 0) { // line is done, goto the next one row_ptr -= row_dec; pixel_ptr = 0; } else if (stream_byte == 1) { // decode is done return ; } else if (stream_byte == 2) { // reposition frame decode coordinates FETCH_NEXT_STREAM_BYTE(); pixel_ptr += stream_byte; FETCH_NEXT_STREAM_BYTE(); row_ptr -= stream_byte * row_dec; } else { // copy pixels from encoded stream odd_pixel = stream_byte &1; rle_code = (stream_byte + 1) / 2; extra_byte = rle_code &0x01; if ((row_ptr + pixel_ptr + stream_byte > frame_size) || (row_ptr < 0)) { return ; } for (i = 0; i < rle_code; i++) { if (pixel_ptr >= s->avctx->width) break; FETCH_NEXT_STREAM_BYTE(); s->frame.data[0][row_ptr + pixel_ptr] = stream_byte >> 4; pixel_ptr++; if (i + 1 == rle_code && odd_pixel) break; if (pixel_ptr >= s->avctx->width) break; s->frame.data[0][row_ptr + pixel_ptr] = stream_byte &0x0F; pixel_ptr++; } // if the RLE code is odd, skip a byte in the stream if (extra_byte) stream_ptr++; } } else { // decode a run of data if ((row_ptr + pixel_ptr + stream_byte > frame_size) || (row_ptr < 0)) { return ; } FETCH_NEXT_STREAM_BYTE(); for (i = 0; i < rle_code; i++) { if (pixel_ptr >= s->avctx->width) break; if ((i &1) == 0) s->frame.data[0][row_ptr + pixel_ptr] = stream_byte >> 4; else s->frame.data[0][row_ptr + pixel_ptr] = stream_byte &0x0F; pixel_ptr++; } } } // one last sanity check on the way out if (stream_ptr < s->size) { // error } } static void msrle_decode_pal8(MsrleContext *s) { int stream_ptr = 0; unsigned char rle_code; unsigned char extra_byte; unsigned char stream_byte; int pixel_ptr = 0; int row_dec = s->frame.linesize[0]; int row_ptr = (s->avctx->height - 1) *row_dec; int frame_size = row_dec * s->avctx->height; // make the palette available memcpy(s->frame.data[1], s->avctx->palctrl->palette, AVPALETTE_SIZE); if (s->avctx->palctrl->palette_changed) { // s->frame.palette_has_changed = 1; s->avctx->palctrl->palette_changed = 0; } while (row_ptr >= 0) { FETCH_NEXT_STREAM_BYTE(); rle_code = stream_byte; if (rle_code == 0) { // fetch the next byte to see how to handle escape code FETCH_NEXT_STREAM_BYTE(); if (stream_byte == 0) { // line is done, goto the next one row_ptr -= row_dec; pixel_ptr = 0; } else if (stream_byte == 1) { // decode is done return ; } else if (stream_byte == 2) { // reposition frame decode coordinates FETCH_NEXT_STREAM_BYTE(); pixel_ptr += stream_byte; FETCH_NEXT_STREAM_BYTE(); row_ptr -= stream_byte * row_dec; } else { // copy pixels from encoded stream if ((row_ptr + pixel_ptr + stream_byte > frame_size) || (row_ptr < 0)) { return ; } rle_code = stream_byte; extra_byte = stream_byte &0x01; if (stream_ptr + rle_code + extra_byte > s->size) { return ; } while (rle_code--) { FETCH_NEXT_STREAM_BYTE(); s->frame.data[0][row_ptr + pixel_ptr] = stream_byte; pixel_ptr++; } // if the RLE code is odd, skip a byte in the stream if (extra_byte) stream_ptr++; } } else { // decode a run of data if ((row_ptr + pixel_ptr + stream_byte > frame_size) || (row_ptr < 0)) { return ; } FETCH_NEXT_STREAM_BYTE(); while (rle_code--) { s->frame.data[0][row_ptr + pixel_ptr] = stream_byte; pixel_ptr++; } } } // one last sanity check on the way out if (stream_ptr < s->size) { // error } } static int msrle_decode_init(AVCodecContext *avctx) { MsrleContext *s = (MsrleContext*)avctx->priv_data; s->avctx = avctx; avctx->pix_fmt = PIX_FMT_PAL8; s->frame.data[0] = NULL; return 0; } static int msrle_decode_frame(AVCodecContext *avctx, void *data, int *data_size, uint8_t *buf, int buf_size) { MsrleContext *s = (MsrleContext*)avctx->priv_data; s->buf = buf; s->size = buf_size; if (avctx->reget_buffer(avctx, &s->frame)) return - 1; switch (avctx->bits_per_sample) { case 8: msrle_decode_pal8(s); break; case 4: msrle_decode_pal4(s); break; default: break; } *data_size = sizeof(AVFrame); *(AVFrame*)data = s->frame; // report that the buffer was completely consumed return buf_size; } static int msrle_decode_end(AVCodecContext *avctx) { MsrleContext *s = (MsrleContext*)avctx->priv_data; // release the last frame if (s->frame.data[0]) avctx->release_buffer(avctx, &s->frame); return 0; } AVCodec msrle_decoder = { "msrle", CODEC_TYPE_VIDEO, CODEC_ID_MSRLE, sizeof(MsrleContext), msrle_decode_init, NULL, msrle_decode_end, msrle_decode_frame }; ================================================ FILE: ffplay源码和书籍/ffplay/libavcodec/truespeech.c ================================================ /************************************************************************/ /* ļʵ truespeed Ƶ */ /************************************************************************/ #include "avcodec.h" #include "truespeech_data.h" // TrueSpeech decoder context typedef struct TSContext { // input data int16_t vector[8]; // input vector: 5/5/4/4/4/3/3/3 int offset1[2]; // 8-bit value, used in one copying offset int offset2[4]; // 7-bit value, encodes offsets for copying and for two-point filter int pulseoff[4]; // 4-bit offset of pulse values block int pulsepos[4]; // 27-bit variable, encodes 7 pulse positions int pulseval[4]; // 7x2-bit pulse values int flag; // 1-bit flag, shows how to choose filters // temporary data int filtbuf[146]; // some big vector used for storing filters int prevfilt[8]; // filter from previous frame int16_t tmp1[8]; // coefficients for adding to out int16_t tmp2[8]; // coefficients for adding to out int16_t tmp3[8]; // coefficients for adding to out int16_t cvector[8]; // correlated input vector int filtval; // gain value for one function int16_t newvec[60]; // tmp vector int16_t filters[32]; // filters for every subframe } TSContext; #if !defined(LE_32) #define LE_32(x) ((((uint8_t*)(x))[3] << 24)| (((uint8_t*)(x))[2] << 16) | \ (((uint8_t*)(x))[1] << 8) | ((uint8_t*)(x))[0]) #endif static int truespeech_decode_init(AVCodecContext *avctx) { return 0; // TSContext *c = avctx->priv_data; } static void truespeech_read_frame(TSContext *dec, uint8_t *input) { uint32_t t; t = LE_32(input); // first dword input += 4; dec->flag = t &1; dec->vector[0] = ts_codebook[0][(t >> 1) &0x1F]; dec->vector[1] = ts_codebook[1][(t >> 6) &0x1F]; dec->vector[2] = ts_codebook[2][(t >> 11) &0xF]; dec->vector[3] = ts_codebook[3][(t >> 15) &0xF]; dec->vector[4] = ts_codebook[4][(t >> 19) &0xF]; dec->vector[5] = ts_codebook[5][(t >> 23) &0x7]; dec->vector[6] = ts_codebook[6][(t >> 26) &0x7]; dec->vector[7] = ts_codebook[7][(t >> 29) &0x7]; t = LE_32(input); // second dword input += 4; dec->offset2[0] = (t >> 0) &0x7F; dec->offset2[1] = (t >> 7) &0x7F; dec->offset2[2] = (t >> 14) &0x7F; dec->offset2[3] = (t >> 21) &0x7F; dec->offset1[0] = ((t >> 28) &0xF) << 4; t = LE_32(input); // third dword input += 4; dec->pulseval[0] = (t >> 0) &0x3FFF; dec->pulseval[1] = (t >> 14) &0x3FFF; dec->offset1[1] = (t >> 28) &0x0F; t = LE_32(input); // fourth dword input += 4; dec->pulseval[2] = (t >> 0) &0x3FFF; dec->pulseval[3] = (t >> 14) &0x3FFF; dec->offset1[1] |= ((t >> 28) &0x0F) << 4; t = LE_32(input); // fifth dword input += 4; dec->pulsepos[0] = (t >> 4) &0x7FFFFFF; dec->pulseoff[0] = (t >> 0) &0xF; dec->offset1[0] |= (t >> 31) &1; t = LE_32(input); // sixth dword input += 4; dec->pulsepos[1] = (t >> 4) &0x7FFFFFF; dec->pulseoff[1] = (t >> 0) &0xF; dec->offset1[0] |= ((t >> 31) &1) << 1; t = LE_32(input); // seventh dword input += 4; dec->pulsepos[2] = (t >> 4) &0x7FFFFFF; dec->pulseoff[2] = (t >> 0) &0xF; dec->offset1[0] |= ((t >> 31) &1) << 2; t = LE_32(input); // eighth dword input += 4; dec->pulsepos[3] = (t >> 4) &0x7FFFFFF; dec->pulseoff[3] = (t >> 0) &0xF; dec->offset1[0] |= ((t >> 31) &1) << 3; } static void truespeech_correlate_filter(TSContext *dec) { int16_t tmp[8]; int i, j; for (i = 0; i < 8; i++) { if (i > 0) { memcpy(tmp, dec->cvector, i *2); for (j = 0; j < i; j++) dec->cvector[j] = ((tmp[i - j - 1] *dec->vector[i]) + (dec->cvector[j] << 15) + 0x4000) >> 15; } dec->cvector[i] = (8-dec->vector[i]) >> 3; } for (i = 0; i < 8; i++) dec->cvector[i] = (dec->cvector[i] *ts_230[i]) >> 15; dec->filtval = dec->vector[0]; } static void truespeech_filters_merge(TSContext *dec) { int i; if (!dec->flag) { for (i = 0; i < 8; i++) { dec->filters[i + 0] = dec->prevfilt[i]; dec->filters[i + 8] = dec->prevfilt[i]; } } else { for (i = 0; i < 8; i++) { dec->filters[i + 0] = (dec->cvector[i] *21846+dec->prevfilt[i] *10923+16384) >> 15; dec->filters[i + 8] = (dec->cvector[i] *10923+dec->prevfilt[i] *21846+16384) >> 15; } } for (i = 0; i < 8; i++) { dec->filters[i + 16] = dec->cvector[i]; dec->filters[i + 24] = dec->cvector[i]; } } static void truespeech_apply_twopoint_filter(TSContext *dec, int quart) { int16_t tmp[146+60], *ptr0, *ptr1, *filter; int i, t, off; t = dec->offset2[quart]; if (t == 127) { memset(dec->newvec, 0, 60 *2); return ; } for (i = 0; i < 146; i++) tmp[i] = dec->filtbuf[i]; off = (t / 25) + dec->offset1[quart >> 1] + 18; ptr0 = tmp + 145-off; ptr1 = tmp + 146; filter = (int16_t*)ts_240 + (t % 25) *2; for (i = 0; i < 60; i++) { t = (ptr0[0] *filter[0] + ptr0[1] *filter[1] + 0x2000) >> 14; ptr0++; dec->newvec[i] = t; ptr1[i] = t; } } static void truespeech_place_pulses(TSContext *dec, int16_t *out, int quart) { int16_t tmp[7]; int i, j, t; int16_t *ptr1, *ptr2; int coef; memset(out, 0, 60 *2); for (i = 0; i < 7; i++) { t = dec->pulseval[quart] &3; dec->pulseval[quart] >>= 2; tmp[6-i] = ts_562[dec->pulseoff[quart] *4+t]; } coef = dec->pulsepos[quart] >> 15; ptr1 = (int16_t*)ts_140 + 30; ptr2 = tmp; for (i = 0, j = 3; (i < 30) && (j > 0); i++) { t = *ptr1++; if (coef >= t) coef -= t; else { out[i] = *ptr2++; ptr1 += 30; j--; } } coef = dec->pulsepos[quart] &0x7FFF; ptr1 = (int16_t*)ts_140; for (i = 30, j = 4; (i < 60) && (j > 0); i++) { t = *ptr1++; if (coef >= t) coef -= t; else { out[i] = *ptr2++; ptr1 += 30; j--; } } } static void truespeech_update_filters(TSContext *dec, int16_t *out, int quart) { int i; for (i = 0; i < 86; i++) dec->filtbuf[i] = dec->filtbuf[i + 60]; for (i = 0; i < 60; i++) { dec->filtbuf[i + 86] = out[i] + dec->newvec[i] - (dec->newvec[i] >> 3); out[i] += dec->newvec[i]; } } static void truespeech_synth(TSContext *dec, int16_t *out, int quart) { int i, k; int t[8]; int16_t *ptr0, *ptr1; ptr0 = dec->tmp1; ptr1 = dec->filters + quart * 8; for (i = 0; i < 60; i++) { int sum = 0; for (k = 0; k < 8; k++) sum += ptr0[k] *ptr1[k]; sum = (sum + (out[i] << 12) + 0x800) >> 12; out[i] = clip(sum, - 0x7FFE, 0x7FFE); for (k = 7; k > 0; k--) ptr0[k] = ptr0[k - 1]; ptr0[0] = out[i]; } for (i = 0; i < 8; i++) t[i] = (ts_5E2[i] *ptr1[i]) >> 15; ptr0 = dec->tmp2; for (i = 0; i < 60; i++) { int sum = 0; for (k = 0; k < 8; k++) sum += ptr0[k] *t[k]; for (k = 7; k > 0; k--) ptr0[k] = ptr0[k - 1]; ptr0[0] = out[i]; out[i] = ((out[i] << 12) - sum) >> 12; } for (i = 0; i < 8; i++) t[i] = (ts_5F2[i] *ptr1[i]) >> 15; ptr0 = dec->tmp3; for (i = 0; i < 60; i++) { int sum = out[i] << 12; for (k = 0; k < 8; k++) sum += ptr0[k] *t[k]; for (k = 7; k > 0; k--) ptr0[k] = ptr0[k - 1]; ptr0[0] = clip((sum + 0x800) >> 12, - 0x7FFE, 0x7FFE); sum = ((ptr0[1]*(dec->filtval - (dec->filtval >> 2))) >> 4) + sum; sum = sum - (sum >> 3); out[i] = clip((sum + 0x800) >> 12, - 0x7FFE, 0x7FFE); } } static void truespeech_save_prevvec(TSContext *c) { int i; for (i = 0; i < 8; i++) c->prevfilt[i] = c->cvector[i]; } static int truespeech_decode_frame(AVCodecContext *avctx, void *data, int *data_size, uint8_t *buf, int buf_size) { TSContext *c = avctx->priv_data; int i; short *samples = data; int consumed = 0; int16_t out_buf[240]; if (!buf_size) return 0; while (consumed < buf_size) { truespeech_read_frame(c, buf + consumed); consumed += 32; truespeech_correlate_filter(c); truespeech_filters_merge(c); memset(out_buf, 0, 240 *2); for (i = 0; i < 4; i++) { truespeech_apply_twopoint_filter(c, i); truespeech_place_pulses(c, out_buf + i * 60, i); truespeech_update_filters(c, out_buf + i * 60, i); truespeech_synth(c, out_buf + i * 60, i); } truespeech_save_prevvec(c); for (i = 0; i < 240; i++) // finally output decoded frame *samples++ = out_buf[i]; } *data_size = consumed * 15; return buf_size; } AVCodec truespeech_decoder = { "truespeech", CODEC_TYPE_AUDIO, CODEC_ID_TRUESPEECH, sizeof(TSContext), truespeech_decode_init, NULL, NULL, truespeech_decode_frame, }; ================================================ FILE: ffplay源码和书籍/ffplay/libavcodec/truespeech_data.h ================================================ #ifndef __TRUESPEECH_DATA__ #define __TRUESPEECH_DATA__ /************************************************************************/ /* ļ truespeed Ƶʹõij */ /************************************************************************/ #pragma warning(disable:4305 ) /* codebooks fo expanding input filter */ static const int16_t ts_cb_0[32] = { 0x8240, 0x8364, 0x84CE, 0x865D, 0x8805, 0x89DE, 0x8BD7, 0x8DF4, 0x9051, 0x92E2, 0x95DE, 0x990F, 0x9C81, 0xA079, 0xA54C, 0xAAD2, 0xB18A, 0xB90A, 0xC124, 0xC9CC, 0xD339, 0xDDD3, 0xE9D6, 0xF893, 0x096F, 0x1ACA, 0x29EC, 0x381F, 0x45F9, 0x546A, 0x63C3, 0x73B5, }; static const int16_t ts_cb_1[32] = { 0x9F65, 0xB56B, 0xC583, 0xD371, 0xE018, 0xEBB4, 0xF61C, 0xFF59, 0x085B, 0x1106, 0x1952, 0x214A, 0x28C9, 0x2FF8, 0x36E6, 0x3D92, 0x43DF, 0x49BB, 0x4F46, 0x5467, 0x5930, 0x5DA3, 0x61EC, 0x65F9, 0x69D4, 0x6D5A, 0x709E, 0x73AD, 0x766B, 0x78F0, 0x7B5A, 0x7DA5, }; static const int16_t ts_cb_2[16] = { 0x96F8, 0xA3B4, 0xAF45, 0xBA53, 0xC4B1, 0xCECC, 0xD86F, 0xE21E, 0xEBF3, 0xF640, 0x00F7, 0x0C20, 0x1881, 0x269A, 0x376B, 0x4D60, }; static const int16_t ts_cb_3[16] = { 0xC654, 0xDEF2, 0xEFAA, 0xFD94, 0x096A, 0x143F, 0x1E7B, 0x282C, 0x3176, 0x3A89, 0x439F, 0x4CA2, 0x557F, 0x5E50, 0x6718, 0x6F8D, }; static const int16_t ts_cb_4[16] = { 0xABE7, 0xBBA8, 0xC81C, 0xD326, 0xDD0E, 0xE5D4, 0xEE22, 0xF618, 0xFE28, 0x064F, 0x0EB7, 0x17B8, 0x21AA, 0x2D8B, 0x3BA2, 0x4DF9, }; static const int16_t ts_cb_5[8] = { 0xD51B, 0xF12E, 0x042E, 0x13C7, 0x2260, 0x311B, 0x40DE, 0x5385,}; static const int16_t ts_cb_6[8] = { 0xB550, 0xC825, 0xD980, 0xE997, 0xF883, 0x0752, 0x1811, 0x2E18,}; static const int16_t ts_cb_7[8] = { 0xCEF0, 0xE4F9, 0xF6BB, 0x0646, 0x14F5, 0x23FF, 0x356F, 0x4A8D,}; static const int16_t *ts_codebook[8] = {ts_cb_0, ts_cb_1, ts_cb_2, ts_cb_3, ts_cb_4, ts_cb_5, ts_cb_6, ts_cb_7}; /* table used for decoding pulse positions */ static const int16_t ts_140[120] = { 0x0E46, 0x0CCC, 0x0B6D, 0x0A28, 0x08FC, 0x07E8, 0x06EB, 0x0604, 0x0532, 0x0474, 0x03C9, 0x0330, 0x02A8, 0x0230, 0x01C7, 0x016C, 0x011E, 0x00DC, 0x00A5, 0x0078, 0x0054, 0x0038, 0x0023, 0x0014, 0x000A, 0x0004, 0x0001, 0x0000, 0x0000, 0x0000, 0x0196, 0x017A, 0x015F, 0x0145, 0x012C, 0x0114, 0x00FD, 0x00E7, 0x00D2, 0x00BE, 0x00AB, 0x0099, 0x0088, 0x0078, 0x0069, 0x005B, 0x004E, 0x0042, 0x0037, 0x002D, 0x0024, 0x001C, 0x0015, 0x000F, 0x000A, 0x0006, 0x0003, 0x0001, 0x0000, 0x0000, 0x001D, 0x001C, 0x001B, 0x001A, 0x0019, 0x0018, 0x0017, 0x0016, 0x0015, 0x0014, 0x0013, 0x0012, 0x0011, 0x0010, 0x000F, 0x000E, 0x000D, 0x000C, 0x000B, 0x000A, 0x0009, 0x0008, 0x0007, 0x0006, 0x0005, 0x0004, 0x0003, 0x0002, 0x0001, 0x0000, 0x0001, 0x0001, 0x0001, 0x0001, 0x0001, 0x0001, 0x0001, 0x0001, 0x0001, 0x0001, 0x0001, 0x0001, 0x0001, 0x0001, 0x0001, 0x0001, 0x0001, 0x0001, 0x0001, 0x0001, 0x0001, 0x0001, 0x0001, 0x0001, 0x0001, 0x0001, 0x0001, 0x0001, 0x0001, 0x0001 }; /* filter for correlated input filter */ static const int16_t ts_230[8] = { 0x7F3B, 0x7E78, 0x7DB6, 0x7CF5, 0x7C35, 0x7B76, 0x7AB8, 0x79FC }; /* two-point filters table */ static const int16_t ts_240[25 * 2] = { 0xED2F, 0x5239, 0x54F1, 0xE4A9, 0x2620, 0xEE3E, 0x09D6, 0x2C40, 0xEFB5, 0x2BE0, 0x3FE1, 0x3339, 0x442F, 0xE6FE, 0x4458, 0xF9DF, 0xF231, 0x43DB, 0x3DB0, 0xF705, 0x4F7B, 0xFEFB, 0x26AD, 0x0CDC, 0x33C2, 0x0739, 0x12BE, 0x43A2, 0x1BDF, 0x1F3E, 0x0211, 0x0796, 0x2AEB, 0x163F, 0x050D, 0x3A38, 0x0D1E, 0x0D78, 0x150F, 0x3346, 0x38A4, 0x0B7D, 0x2D5D, 0x1FDF, 0x19B7, 0x2822, 0x0D99, 0x1F12, 0x194C, 0x0CE6 }; /* possible pulse values */ static const int16_t ts_562[64] = { 0x0002, 0x0006, 0xFFFE, 0xFFFA, 0x0004, 0x000C, 0xFFFC, 0xFFF4, 0x0006, 0x0012, 0xFFFA, 0xFFEE, 0x000A, 0x001E, 0xFFF6, 0xFFE2, 0x0010, 0x0030, 0xFFF0, 0xFFD0, 0x0019, 0x004B, 0xFFE7, 0xFFB5, 0x0028, 0x0078, 0xFFD8, 0xFF88, 0x0040, 0x00C0, 0xFFC0, 0xFF40, 0x0065, 0x012F, 0xFF9B, 0xFED1, 0x00A1, 0x01E3, 0xFF5F, 0xFE1D, 0x0100, 0x0300, 0xFF00, 0xFD00, 0x0196, 0x04C2, 0xFE6A, 0xFB3E, 0x0285, 0x078F, 0xFD7B, 0xF871, 0x0400, 0x0C00, 0xFC00, 0xF400, 0x0659, 0x130B, 0xF9A7, 0xECF5, 0x0A14, 0x1E3C, 0xF5EC, 0xE1C4 }; /* filters used in final output calculations */ static const int16_t ts_5E2[8] = { 0x4666, 0x26B8, 0x154C, 0x0BB6, 0x0671, 0x038B, 0x01F3, 0x0112 }; static const int16_t ts_5F2[8] = { 0x6000, 0x4800, 0x3600, 0x2880, 0x1E60, 0x16C8, 0x1116, 0x0CD1 }; #endif ================================================ FILE: ffplay源码和书籍/ffplay/libavcodec/utils_codec.c ================================================ /************************************************************************/ /* ʹõİ͹ߺ */ /************************************************************************/ #include #include "avcodec.h" #include "dsputil.h" #define EDGE_WIDTH 16 #define STRIDE_ALIGN 16 #define INT_MAX 2147483647 #define FFMAX(a,b) ((a) > (b) ? (a) : (b)) void *av_malloc(unsigned int size) { void *ptr; if (size > INT_MAX) return NULL; ptr = malloc(size); return ptr; } void *av_realloc(void *ptr, unsigned int size) { if (size > INT_MAX) return NULL; return realloc(ptr, size); } void av_free(void *ptr) { if (ptr) free(ptr); } void *av_mallocz(unsigned int size) { void *ptr; ptr = av_malloc(size); if (!ptr) return NULL; memset(ptr, 0, size); return ptr; } void *av_fast_realloc(void *ptr, unsigned int *size, unsigned int min_size) { if (min_size < *size) return ptr; *size = FFMAX(17 *min_size / 16+32, min_size); return av_realloc(ptr, *size); } void av_freep(void *arg) { void **ptr = (void **)arg; av_free(*ptr); *ptr = NULL; } /* Ϊֶ֧Ҫеı */ AVCodec *first_avcodec = NULL; /* ע */ void register_avcodec(AVCodec *format) { /* ѱӵ */ AVCodec **p; p = &first_avcodec; while (*p != NULL) p = &(*p)->next; *p = format; format->next = NULL; } typedef struct InternalBuffer { uint8_t *base[4]; uint8_t *data[4]; int linesize[4]; } InternalBuffer; #define INTERNAL_BUFFER_SIZE 32 #define ALIGN(x, a) (((x)+(a)-1)&~((a)-1)) void avcodec_align_dimensions(AVCodecContext *s, int *width, int *height) { int w_align = 1; int h_align = 1; switch (s->pix_fmt) { case PIX_FMT_YUV420P: case PIX_FMT_YUV422: case PIX_FMT_UYVY422: case PIX_FMT_YUV422P: case PIX_FMT_YUV444P: case PIX_FMT_GRAY8: case PIX_FMT_YUVJ420P: case PIX_FMT_YUVJ422P: case PIX_FMT_YUVJ444P: //FIXME check for non mpeg style codecs and use less alignment w_align = 16; h_align = 16; break; case PIX_FMT_YUV411P: case PIX_FMT_UYVY411: w_align = 32; h_align = 8; break; case PIX_FMT_YUV410P: case PIX_FMT_RGB555: case PIX_FMT_PAL8: break; case PIX_FMT_BGR24: break; default: w_align = 1; h_align = 1; break; } *width = ALIGN(*width, w_align); *height = ALIGN(*height, h_align); } int avcodec_check_dimensions(void *av_log_ctx, unsigned int w, unsigned int h) { if ((int)w > 0 && (int)h > 0 && (w + 128)*(uint64_t)(h + 128) < INT_MAX / 4) return 0; return - 1; } int avcodec_default_get_buffer(AVCodecContext *s, AVFrame *pic) { int i; int w = s->width; int h = s->height; int align_off; InternalBuffer *buf; assert(pic->data[0] == NULL); assert(INTERNAL_BUFFER_SIZE > s->internal_buffer_count); if (avcodec_check_dimensions(s, w, h)) return - 1; if (s->internal_buffer == NULL) s->internal_buffer = av_mallocz(INTERNAL_BUFFER_SIZE *sizeof(InternalBuffer)); buf = &((InternalBuffer*)s->internal_buffer)[s->internal_buffer_count]; if (buf->base[0]) {} else { int h_chroma_shift, v_chroma_shift; int pixel_size, size[3]; AVPicture picture; avcodec_get_chroma_sub_sample(s->pix_fmt, &h_chroma_shift, &v_chroma_shift); avcodec_align_dimensions(s, &w, &h); w+= EDGE_WIDTH*2; h+= EDGE_WIDTH*2; avpicture_fill(&picture, NULL, s->pix_fmt, w, h); pixel_size = picture.linesize[0] * 8 / w; assert(pixel_size >= 1); if (pixel_size == 3 *8) w = ALIGN(w, STRIDE_ALIGN << h_chroma_shift); else w = ALIGN(pixel_size *w, STRIDE_ALIGN << (h_chroma_shift + 3)) / pixel_size; size[1] = avpicture_fill(&picture, NULL, s->pix_fmt, w, h); size[0] = picture.linesize[0] *h; size[1] -= size[0]; if (picture.data[2]) size[1] = size[2] = size[1] / 2; else size[2] = 0; memset(buf->base, 0, sizeof(buf->base)); memset(buf->data, 0, sizeof(buf->data)); for (i = 0; i < 3 && size[i]; i++) { const int h_shift = i == 0 ? 0 : h_chroma_shift; const int v_shift = i == 0 ? 0 : v_chroma_shift; buf->linesize[i] = picture.linesize[i]; buf->base[i] = av_malloc(size[i] + 16); //FIXME 16 if (buf->base[i] == NULL) return - 1; memset(buf->base[i], 128, size[i]); align_off = ALIGN((buf->linesize[i] * EDGE_WIDTH >> v_shift) + ( EDGE_WIDTH >> h_shift), STRIDE_ALIGN); if ((s->pix_fmt == PIX_FMT_PAL8) || !size[2]) buf->data[i] = buf->base[i]; else buf->data[i] = buf->base[i] + align_off; } } for (i = 0; i < 4; i++) { pic->base[i] = buf->base[i]; pic->data[i] = buf->data[i]; pic->linesize[i] = buf->linesize[i]; } s->internal_buffer_count++; return 0; } void avcodec_default_release_buffer(AVCodecContext *s, AVFrame *pic) { int i; InternalBuffer *buf, *last, temp; assert(s->internal_buffer_count); buf = NULL; for (i = 0; i < s->internal_buffer_count; i++) { buf = &((InternalBuffer*)s->internal_buffer)[i]; //just 3-5 checks so is not worth to optimize if (buf->data[0] == pic->data[0]) break; } assert(i < s->internal_buffer_count); s->internal_buffer_count--; last = &((InternalBuffer*)s->internal_buffer)[s->internal_buffer_count]; temp = *buf; *buf = *last; *last = temp; for (i = 0; i < 3; i++) { pic->data[i] = NULL; } } int avcodec_default_reget_buffer(AVCodecContext *s, AVFrame *pic) { if (pic->data[0] == NULL) // If no picture return a new buffer { return s->get_buffer(s, pic); } return 0; } void avcodec_default_free_buffers(AVCodecContext *s) { int i, j; if (s->internal_buffer == NULL) return ; for (i = 0; i < INTERNAL_BUFFER_SIZE; i++) { InternalBuffer *buf = &((InternalBuffer*)s->internal_buffer)[i]; for (j = 0; j < 4; j++) { av_freep(&buf->base[j]); buf->data[j] = NULL; } } av_freep(&s->internal_buffer); s->internal_buffer_count = 0; } AVCodecContext *avcodec_alloc_context(void) { AVCodecContext *s = av_malloc(sizeof(AVCodecContext)); if (s == NULL) return NULL; memset(s, 0, sizeof(AVCodecContext)); s->get_buffer = avcodec_default_get_buffer; s->release_buffer = avcodec_default_release_buffer; s->pix_fmt = PIX_FMT_NONE; s->palctrl = NULL; s->reget_buffer = avcodec_default_reget_buffer; return s; } int avcodec_open(AVCodecContext *avctx, AVCodec *codec) { int ret = - 1; if (avctx->codec) goto end; if (codec->priv_data_size > 0) { avctx->priv_data = av_mallocz(codec->priv_data_size); if (!avctx->priv_data) goto end; } else { avctx->priv_data = NULL; } avctx->codec = codec; avctx->codec_id = codec->id; avctx->frame_number = 0; ret = avctx->codec->init(avctx); if (ret < 0) { av_freep(&avctx->priv_data); avctx->codec = NULL; goto end; } ret = 0; end: return ret; } int avcodec_decode_video(AVCodecContext *avctx, AVFrame *picture, int *got_picture_ptr, uint8_t *buf, int buf_size) { int ret; *got_picture_ptr = 0; if (buf_size) { ret = avctx->codec->decode(avctx, picture, got_picture_ptr, buf, buf_size); // ʵʵıı뺯msrleȱ if (*got_picture_ptr) avctx->frame_number++; } else ret = 0; return ret; } int avcodec_decode_audio(AVCodecContext *avctx, int16_t *samples, int *frame_size_ptr, uint8_t *buf, int buf_size) { int ret; *frame_size_ptr = 0; if (buf_size) { ret = avctx->codec->decode(avctx, samples, frame_size_ptr, buf, buf_size); avctx->frame_number++; } else ret = 0; return ret; } int avcodec_close(AVCodecContext *avctx) { if (avctx->codec->close) avctx->codec->close(avctx); avcodec_default_free_buffers(avctx); av_freep(&avctx->priv_data); avctx->codec = NULL; return 0; } AVCodec *avcodec_find_decoder(enum CodecID id) { AVCodec *p; p = first_avcodec; while (p) { if (p->decode != NULL && p->id == id) return p; p = p->next; } return NULL; } /* ʼ */ void avcodec_init(void) { static int inited = 0; if (inited != 0) return ; inited = 1; dsputil_static_init(); } ================================================ FILE: ffplay源码和书籍/ffplay/libavformat/allformats.c ================================================ /* ** 򵥵ע/ʼӦЭ飬ļʽӦڲ */ #include "avformat.h" extern URLProtocol file_protocol; /* עֵ֧ĸʽ */ void av_register_all(void) { static int inited = 0; // ǷѾʼ if (inited != 0) return ; inited = 1; // ffplay CPU һ DSPЩ CPU ԴļָŻffplay ຯ // ŵ dsputil.h dsputil.c ļУ úָķӳ䵽 CPU ļŻʵֺ // ˴ʼЩָ avcodec_init(); // עеı avcodec_register_all(); // עֵ֧ĸʽ avidec_init(); // עЭ飨ļЭ顢Эȣ register_protocol(&file_protocol); } ================================================ FILE: ffplay源码和书籍/ffplay/libavformat/avformat.h ================================================ #ifndef AVFORMAT_H #define AVFORMAT_H /************************************************* ** ʶļʽýͿʹõĺꡢݽṹͺ **ͨЩꡢݽṹͺڴģȫЧ *************************************************/ #ifdef __cplusplus extern "C" { #endif /* 汾 */ #define LIBAVFORMAT_VERSION_INT ((50<<16)+(4<<8)+0) #define LIBAVFORMAT_VERSION 50.4.0 #define LIBAVFORMAT_BUILD LIBAVFORMAT_VERSION_INT #define LIBAVFORMAT_IDENT "Lavf" AV_STRINGIFY(LIBAVFORMAT_VERSION) /* ͷļ */ #include "../libavcodec/avcodec.h" /* ioͷļ */ #include "avio.h" /* 붨 */ #define AVERROR_UNKNOWN (-1) // unknown error #define AVERROR_IO (-2) // i/o error #define AVERROR_NUMEXPECTED (-3) // number syntax expected in filename #define AVERROR_INVALIDDATA (-4) // invalid data found #define AVERROR_NOMEM (-5) // not enough memory #define AVERROR_NOFMT (-6) // unknown format #define AVERROR_NOTSUPP (-7) // operation not supported /* ļת */ #define AVSEEK_FLAG_BACKWARD 1 // seek backward #define AVSEEK_FLAG_BYTE 2 // seeking based on position in bytes #define AVSEEK_FLAG_ANY 4 // seek to any frame, even non keyframes #define AVFMT_NOFILE 0x0001 // no file should be opened #define PKT_FLAG_KEY 0x0001 #define AVINDEX_KEYFRAME 0x0001 #define AVPROBE_SCORE_MAX 100 #define MAX_STREAMS 20 /* ** ݰƵƵһһݰһ֡ݣ ** δ֡ */ typedef struct AVPacket { int64_t pts; // presentation time stamp in time_base units ʾʱ䣬Ƶʾʱ int64_t dts; // decompression time stamp in time_base units ʱ䣬ǺҪ int64_t pos; // byte position in stream, -1 if unknown uint8_t *data; // ʵʱƵݻ׵ַ int size; // ʵʱƵݻĴС int stream_index; // ǰƵݰӦڱƵƵ int flags; //ݰһЩǣǷǹؼ֡ȡ void(*destruct)(struct AVPacket*); // ٺ } AVPacket; /* Ƶб */ typedef struct AVPacketList { AVPacket pkt; struct AVPacketList *next; } AVPacketList; /* ͷݰڵ */ static inline void av_destruct_packet(AVPacket *pkt) { av_free(pkt->data); pkt->data = NULL; pkt->size = 0; } /* ͷһƵ */ static inline void av_free_packet(AVPacket *pkt) { if (pkt && pkt->destruct) pkt->destruct(pkt); } /* ** ȡһƵ ** ļݰݣ ** עܵʱļƫȷҪݵĴСҲȷ ** ݰĻûз䡣ڴҪʼһЩ */ static inline int av_get_packet(ByteIOContext *s, AVPacket *pkt, int size) { int ret; unsigned char *data; if ((unsigned)size > (unsigned)size + FF_INPUT_BUFFER_PADDING_SIZE) return AVERROR_NOMEM; // ڴ data = av_malloc(size + FF_INPUT_BUFFER_PADDING_SIZE); if (!data) return AVERROR_NOMEM; memset(data + size, 0, FF_INPUT_BUFFER_PADDING_SIZE); pkt->pts = AV_NOPTS_VALUE; pkt->dts = AV_NOPTS_VALUE; pkt->pos = - 1; pkt->flags = 0; pkt->stream_index = 0; pkt->data = data; pkt->size = size; pkt->destruct = av_destruct_packet; pkt->pos = url_ftell(s); // ȡʵʵ ret = url_fread(s, pkt->data, size); if (ret <= 0) av_free_packet(pkt); else pkt->size = ret; return ret; } /* ** ̽ ** ΪʶļʽҪһļͷƥ ffplay ֵ֧ļʽļ ** ýṹļ׵ַʹС˴Ķļ */ typedef struct AVProbeData { // ļ const char *filename; // ̽⵽ unsigned char *buf; // ݳ int buf_size; } AVProbeData; /* ** ļṹflags size λΪ˽ʡڴ */ typedef struct AVIndexEntry { int64_t pos; int64_t timestamp; int flags: 2; int size: 30; //yeah trying to keep the size of this small to reduce memory requirements (its 24 vs 32 byte due to possible 8byte align) } AVIndexEntry; /* ** ƵƵ ** AVStream ıʾһýýһЩͨõ ** ϸΪƵƵƵֿԷֳAVIͣmp4 */ typedef struct AVStream { // AVCodecContext *actx; // codec context, change from AVCodecContext *codec; // void *priv_data; // AVIStream ڱУ AVIStream // ʱ׼ AVRational time_base; // av_set_pts_info()ʼ // 󣬼粻֧IJҵʱʹ AVIndexEntry *index_entries; // only used if the format does not support seeking natively int nb_index_entries; int index_entries_allocated_size; double frame_last_delay; // ֡ӳ } AVStream; /* ʽ */ typedef struct AVFormatParameters { int dbg; //only for debug ֻһԱ־ } AVFormatParameters; /* ** ʽļĽṹָ룩 ** AVInputFormat ļʽעⲻAVIAV Inputڹܺ ** ڳʱжʵ */ typedef struct AVInputFormat { // ļ const char *name; int priv_data_size; // ̽⺯ int(*read_probe)(AVProbeData*); // ȡͷ int(*read_header)(struct AVFormatContext *, AVFormatParameters *ap); // ȡһ֡ݣһݰ int(*read_packet)(struct AVFormatContext *, AVPacket *pkt); // رļ int(*read_close)(struct AVFormatContext*); const char *extensions; // ļչ // һļĸʽ struct AVInputFormat *next; } AVInputFormat; /* ** AVFormatContext ṹʾеĵǰļʽʹõģ ** ļеԣкһʵ */ typedef struct AVFormatContext // format I/O context { // ļĸʽAVFormatContextһĸAVInputFormatһĸ struct AVInputFormat *iformat; void *priv_data; // ļ ByteIOContext pb; // int nb_streams; // ƵƵ AVStream *streams[MAX_STREAMS]; } AVFormatContext; int avidec_init(void); void av_register_input_format(AVInputFormat *format); void av_register_all(void); AVInputFormat *av_probe_input_format(AVProbeData *pd, int is_opened); int match_ext(const char *filename, const char *extensions); int av_open_input_stream(AVFormatContext **ic_ptr, ByteIOContext *pb, const char *filename, AVInputFormat *fmt, AVFormatParameters *ap); int av_open_input_file(AVFormatContext **ic_ptr, const char *filename, AVInputFormat *fmt, int buf_size, AVFormatParameters *ap); int av_read_frame(AVFormatContext *s, AVPacket *pkt); int av_read_packet(AVFormatContext *s, AVPacket *pkt); void av_close_input_file(AVFormatContext *s); AVStream *av_new_stream(AVFormatContext *s, int id); void av_set_pts_info(AVStream *s, int pts_wrap_bits, int pts_num, int pts_den); int av_index_search_timestamp(AVStream *st, int64_t timestamp, int flags); int av_add_index_entry(AVStream *st, int64_t pos, int64_t timestamp, int size, int distance, int flags); int strstart(const char *str, const char *val, const char **ptr); void pstrcpy(char *buf, int buf_size, const char *str); #ifdef __cplusplus } #endif #endif ================================================ FILE: ffplay源码和书籍/ffplay/libavformat/avidec.c ================================================ /* ** AVI ļغעЩطЩԴ롣 ע 1AVI ļýִŷʽǽ֯źͽ֯š֯žƵ֡Ϊ Сλ໥ţƵཻ֡֯һ𣬲Ҵŵļûر涨ǽ֯žǰ һý֡һ𣬷ǽ֯ŵ avi ļ١ ע 2AVI ļṹ AVIINDEXENTRY е dwChunkOffset ֶָʾƫеļʼֽڵƫ ƣеļݿ chunk ƫơ ע 3 avi ļǽ֯ŵġ */ #include "avformat.h" #include #define AVIIF_INDEX 0x10 #define AVIF_HASINDEX 0x00000010 // Index at end of file? #define AVIF_MUSTUSEINDEX 0x00000020 #define INT_MAX 2147483647 #define MKTAG(a,b,c,d) (a | (b << 8) | (c << 16) | (d << 24)) #define FFMIN(a,b) ((a) > (b) ? (b) : (a)) #define FFMAX(a,b) ((a) > (b) ? (a) : (b)) static int avi_load_index(AVFormatContext *s); static int guess_ni_flag(AVFormatContext *s); /* AVIʽƵ */ typedef struct { int64_t frame_offset; // current frame(video) or byte(audio) counter(used to compute the pts) int remaining; int packet_size; int scale; int rate; int sample_size; // size of one sample (or packet) (in the rate/scale sense) in bytes int64_t cum_len; // temporary storage (used during seek) int prefix; // normally 'd'<<8 + 'c' or 'w'<<8 + 'b' int prefix_count; } AVIStream; /* AVIļ*/ typedef struct { int64_t riff_end; // RIFFС int64_t movi_list; int64_t movi_end; int non_interleaved; int stream_index_2; // Ϊ˺AVPacketеstream_index } AVIContext; typedef struct { int id; unsigned int tag; } CodecTag; const CodecTag codec_bmp_tags[] = { {CODEC_ID_MSRLE, MKTAG('m', 'r', 'l', 'e')}, {CODEC_ID_MSRLE, MKTAG(0x1, 0x0, 0x0, 0x0)}, {CODEC_ID_NONE, 0}, }; const CodecTag codec_wav_tags[] = { {CODEC_ID_TRUESPEECH, 0x22}, {0, 0}, }; enum CodecID codec_get_id(const CodecTag *tags, unsigned int tag) { while (tags->id != CODEC_ID_NONE) { if (toupper((tag >> 0) &0xFF) == toupper((tags->tag >> 0) &0xFF) && toupper((tag >> 8) &0xFF) == toupper((tags->tag >> 8) &0xFF) && toupper((tag >> 16)&0xFF) == toupper((tags->tag >> 16)&0xFF) && toupper((tag >> 24)&0xFF) == toupper((tags->tag >> 24)&0xFF)) return tags->id; tags++; } return CODEC_ID_NONE; } static int get_riff(AVIContext *avi, ByteIOContext *pb) { uint32_t tag; tag = get_le32(pb); if (tag != MKTAG('R', 'I', 'F', 'F')) return - 1; avi->riff_end = get_le32(pb); // RIFF chunk size avi->riff_end += url_ftell(pb); // RIFF chunk end tag = get_le32(pb); if (tag != MKTAG('A', 'V', 'I', ' ') && tag != MKTAG('A', 'V', 'I', 'X')) return - 1; return 0; } static void clean_index(AVFormatContext *s) { int i, j; for (i = 0; i < s->nb_streams; i++) { AVStream *st = s->streams[i]; AVIStream *ast = st->priv_data; int n = st->nb_index_entries; int max = ast->sample_size; int64_t pos, size, ts; if (n != 1 || ast->sample_size == 0) continue; while (max < 1024) max += max; pos = st->index_entries[0].pos; size = st->index_entries[0].size; ts = st->index_entries[0].timestamp; for (j = 0; j < size; j += max) { av_add_index_entry(st, pos + j, ts + j / ast->sample_size, FFMIN(max, size - j), 0, AVINDEX_KEYFRAME); } } } static int avi_read_header(AVFormatContext *s, AVFormatParameters *ap) { AVIContext *avi = s->priv_data; ByteIOContext *pb = &s->pb; uint32_t tag, tag1, handler; int codec_type, stream_index, frame_period, bit_rate; unsigned int size, nb_frames; int i, n; AVStream *st; AVIStream *ast; avi->stream_index_2 = - 1; if (get_riff(avi, pb) < 0) return - 1; stream_index = - 1; // first list tag codec_type = - 1; frame_period = 0; for (;;) { if (url_feof(pb)) goto fail; tag = get_le32(pb); size = get_le32(pb); switch (tag) { case MKTAG('L', 'I', 'S', 'T'): // ignored, except when start of video packets tag1 = get_le32(pb); if (tag1 == MKTAG('m', 'o', 'v', 'i')) { avi->movi_list = url_ftell(pb) - 4; if (size) avi->movi_end = avi->movi_list + size; else avi->movi_end = url_fsize(pb); goto end_of_header; // ݶξΪļͷˣgoto } break; case MKTAG('a', 'v', 'i', 'h'): // avi header, using frame_period is bad idea frame_period = get_le32(pb); bit_rate = get_le32(pb) *8; get_le32(pb); avi->non_interleaved |= get_le32(pb) &AVIF_MUSTUSEINDEX; url_fskip(pb, 2 *4); n = get_le32(pb); for (i = 0; i < n; i++) { AVIStream *ast; st = av_new_stream(s, i); if (!st) goto fail; ast = av_mallocz(sizeof(AVIStream)); if (!ast) goto fail; st->priv_data = ast; st->actx->bit_rate = bit_rate; } url_fskip(pb, size - 7 * 4); break; case MKTAG('s', 't', 'r', 'h'): // stream header stream_index++; tag1 = get_le32(pb); handler = get_le32(pb); if (stream_index >= s->nb_streams) { url_fskip(pb, size - 8); break; } st = s->streams[stream_index]; ast = st->priv_data; get_le32(pb); // flags get_le16(pb); // priority get_le16(pb); // language get_le32(pb); // initial frame ast->scale = get_le32(pb); ast->rate = get_le32(pb); if (ast->scale && ast->rate) {} else if (frame_period) { ast->rate = 1000000; ast->scale = frame_period; } else { ast->rate = 25; ast->scale = 1; } av_set_pts_info(st, 64, ast->scale, ast->rate); ast->cum_len = get_le32(pb); // start nb_frames = get_le32(pb); get_le32(pb); // buffer size get_le32(pb); // quality ast->sample_size = get_le32(pb); // sample ssize switch (tag1) { case MKTAG('v', 'i', 'd', 's'): codec_type = CODEC_TYPE_VIDEO; ast->sample_size = 0; break; case MKTAG('a', 'u', 'd', 's'): codec_type = CODEC_TYPE_AUDIO; break; case MKTAG('t', 'x', 't', 's'): //FIXME codec_type = CODEC_TYPE_DATA; //CODEC_TYPE_SUB ? FIXME break; case MKTAG('p', 'a', 'd', 's'): codec_type = CODEC_TYPE_UNKNOWN; stream_index--; break; default: goto fail; } ast->frame_offset = ast->cum_len *FFMAX(ast->sample_size, 1); url_fskip(pb, size - 12 * 4); break; case MKTAG('s', 't', 'r', 'f'): // stream header if (stream_index >= s->nb_streams) { url_fskip(pb, size); } else { st = s->streams[stream_index]; switch (codec_type) { case CODEC_TYPE_VIDEO: // BITMAPINFOHEADER get_le32(pb); // size st->actx->width = get_le32(pb); st->actx->height = get_le32(pb); get_le16(pb); // panes st->actx->bits_per_sample = get_le16(pb); // depth tag1 = get_le32(pb); get_le32(pb); // ImageSize get_le32(pb); // XPelsPerMeter get_le32(pb); // YPelsPerMeter get_le32(pb); // ClrUsed get_le32(pb); // ClrImportant if (size > 10 *4 && size < (1 << 30)) { st->actx->extradata_size = size - 10 * 4; st->actx->extradata = av_malloc(st->actx->extradata_size + FF_INPUT_BUFFER_PADDING_SIZE); url_fread(pb, st->actx->extradata, st->actx->extradata_size); } if (st->actx->extradata_size &1) get_byte(pb); /* Extract palette from extradata if bpp <= 8 */ /* This code assumes that extradata contains only palette */ /* This is true for all paletted codecs implemented in ffmpeg */ if (st->actx->extradata_size && (st->actx->bits_per_sample <= 8)) { int min = FFMIN(st->actx->extradata_size, AVPALETTE_SIZE); st->actx->palctrl = av_mallocz(sizeof(AVPaletteControl)); memcpy(st->actx->palctrl->palette, st->actx->extradata, min); st->actx->palctrl->palette_changed = 1; } st->actx->codec_type = CODEC_TYPE_VIDEO; st->actx->codec_id = codec_get_id(codec_bmp_tags, tag1); st->frame_last_delay = 1.0 * ast->scale / ast->rate; break; case CODEC_TYPE_AUDIO: { AVCodecContext *actx = st->actx; int id = get_le16(pb); actx->codec_type = CODEC_TYPE_AUDIO; actx->channels = get_le16(pb); actx->sample_rate = get_le32(pb); actx->bit_rate = get_le32(pb) *8; actx->block_align = get_le16(pb); if (size == 14) // We're dealing with plain vanilla WAVEFORMAT actx->bits_per_sample = 8; else actx->bits_per_sample = get_le16(pb); actx->codec_id = codec_get_id(codec_wav_tags, id); // wav_codec_get_id(id, codec->bits_per_sample); if (size > 16) { actx->extradata_size = get_le16(pb); // We're obviously dealing with WAVEFORMATEX if (actx->extradata_size > 0) { if (actx->extradata_size > size - 18) actx->extradata_size = size - 18; actx->extradata = av_mallocz(actx->extradata_size + FF_INPUT_BUFFER_PADDING_SIZE); url_fread(pb, actx->extradata, actx->extradata_size); } else { actx->extradata_size = 0; } // It is possible for the chunk to contain garbage at the end if (size - actx->extradata_size - 18 > 0) url_fskip(pb, size - actx->extradata_size - 18); } } if (size % 2) // 2-aligned (fix for Stargate SG-1 - 3x18 - Shades of Grey.avi) url_fskip(pb, 1); break; default: st->actx->codec_type = CODEC_TYPE_DATA; st->actx->codec_id = CODEC_ID_NONE; url_fskip(pb, size); break; } } break; default: // skip tag size += (size &1); url_fskip(pb, size); break; } } end_of_header: if (stream_index != s->nb_streams - 1) // check stream number { fail: for (i = 0; i < s->nb_streams; i++) { av_freep(&s->streams[i]->actx->extradata); av_freep(&s->streams[i]); } return - 1; } avi_load_index(s); avi->non_interleaved |= guess_ni_flag(s); if (avi->non_interleaved) clean_index(s); return 0; } int avi_read_packet(AVFormatContext *s, AVPacket *pkt) { AVIContext *avi = s->priv_data; ByteIOContext *pb = &s->pb; int n, d[8], size; offset_t i, sync; if (avi->non_interleaved) { int best_stream_index = 0; AVStream *best_st = NULL; AVIStream *best_ast; int64_t best_ts = INT64_MAX; int i; for (i = 0; i < s->nb_streams; i++) { AVStream *st = s->streams[i]; AVIStream *ast = st->priv_data; int64_t ts = ast->frame_offset; if (ast->sample_size) ts /= ast->sample_size; ts = av_rescale(ts, AV_TIME_BASE *(int64_t)st->time_base.num, st->time_base.den); if (ts < best_ts) { best_ts = ts; best_st = st; best_stream_index = i; } } best_ast = best_st->priv_data; best_ts = av_rescale(best_ts, best_st->time_base.den, AV_TIME_BASE *(int64_t)best_st->time_base.num); if (best_ast->remaining) i = av_index_search_timestamp(best_st, best_ts, AVSEEK_FLAG_ANY | AVSEEK_FLAG_BACKWARD); else i = av_index_search_timestamp(best_st, best_ts, AVSEEK_FLAG_ANY); if (i >= 0) { int64_t pos = best_st->index_entries[i].pos; pos += best_ast->packet_size - best_ast->remaining; url_fseek(&s->pb, pos + 8, SEEK_SET); assert(best_ast->remaining <= best_ast->packet_size); avi->stream_index_2 = best_stream_index; if (!best_ast->remaining) best_ast->packet_size = best_ast->remaining = best_st->index_entries[i].size; } } resync: if (avi->stream_index_2 >= 0) { AVStream *st = s->streams[avi->stream_index_2]; AVIStream *ast = st->priv_data; int size; if (ast->sample_size <= 1) // minorityreport.AVI block_align=1024 sample_size=1 IMA-ADPCM size = INT_MAX; else if (ast->sample_size < 32) size = 64 * ast->sample_size; else size = ast->sample_size; if (size > ast->remaining) size = ast->remaining; av_get_packet(pb, pkt, size); pkt->dts = ast->frame_offset; if (ast->sample_size) pkt->dts /= ast->sample_size; pkt->stream_index = avi->stream_index_2; if (st->actx->codec_type == CODEC_TYPE_VIDEO) { if (st->index_entries) { AVIndexEntry *e; int index; index = av_index_search_timestamp(st, pkt->dts, 0); e = &st->index_entries[index]; if (index >= 0 && e->timestamp == ast->frame_offset) { if (e->flags &AVINDEX_KEYFRAME) pkt->flags |= PKT_FLAG_KEY; } } else { pkt->flags |= PKT_FLAG_KEY; // if no index, better to say that all frames are key frames } } else { pkt->flags |= PKT_FLAG_KEY; } if (ast->sample_size) ast->frame_offset += pkt->size; else ast->frame_offset++; ast->remaining -= size; if (!ast->remaining) { avi->stream_index_2 = - 1; ast->packet_size = 0; if (size &1) { get_byte(pb); size++; } } return size; } memset(d, - 1, sizeof(int) *8); for (i = sync = url_ftell(pb); !url_feof(pb); i++) { int j; if (i >= avi->movi_end) break; for (j = 0; j < 7; j++) d[j] = d[j + 1]; d[7] = get_byte(pb); size = d[4] + (d[5] << 8) + (d[6] << 16) + (d[7] << 24); if (d[2] >= '0' && d[2] <= '9' && d[3] >= '0' && d[3] <= '9') { n = (d[2] - '0') *10+(d[3] - '0'); } else { n = 100; //invalid stream id } if (i + size > avi->movi_end || d[0] < 0) continue; if ((d[0] == 'i' && d[1] == 'x' && n < s->nb_streams) || (d[0] == 'J' && d[1] == 'U' && d[2] == 'N' && d[3] == 'K')) { url_fskip(pb, size); goto resync; } if (d[0] >= '0' && d[0] <= '9' && d[1] >= '0' && d[1] <= '9') { n = (d[0] - '0') *10+(d[1] - '0'); } else { n = 100; //invalid stream id } //parse ##dc/##wb if (n < s->nb_streams) { AVStream *st; AVIStream *ast; st = s->streams[n]; ast = st->priv_data; if(sync + 9 <= i) { int dbg=0; } else { int dbg1=0; } if (((ast->prefix_count < 5 || sync + 9 > i) && d[2] < 128 && d[3] < 128) || d[2] * 256 + d[3] == ast->prefix) { if (d[2] * 256 + d[3] == ast->prefix) ast->prefix_count++; else { ast->prefix = d[2] *256+d[3]; ast->prefix_count = 0; } avi->stream_index_2 = n; ast->packet_size = size + 8; ast->remaining = size; goto resync; } } // palette changed chunk if (d[0] >= '0' && d[0] <= '9' && d[1] >= '0' && d[1] <= '9' && (d[2] == 'p' && d[3] == 'c') && n < s->nb_streams && i + size <= avi->movi_end) { AVStream *st; int first, clr, flags, k, p; st = s->streams[n]; first = get_byte(pb); clr = get_byte(pb); if (!clr) // all 256 colors used clr = 256; flags = get_le16(pb); p = 4; for (k = first; k < clr + first; k++) { int r, g, b; r = get_byte(pb); g = get_byte(pb); b = get_byte(pb); get_byte(pb); st->actx->palctrl->palette[k] = b + (g << 8) + (r << 16); } st->actx->palctrl->palette_changed = 1; goto resync; } } return - 1; } static int avi_read_idx1(AVFormatContext *s, int size) { AVIContext *avi = s->priv_data; ByteIOContext *pb = &s->pb; int nb_index_entries, i; AVStream *st; AVIStream *ast; unsigned int index, tag, flags, pos, len; unsigned last_pos = - 1; nb_index_entries = size / 16; if (nb_index_entries <= 0) return - 1; for (i = 0; i < nb_index_entries; i++)// read the entries and sort them in each stream component { tag = get_le32(pb); flags = get_le32(pb); pos = get_le32(pb); len = get_le32(pb); if (i == 0 && pos > avi->movi_list) avi->movi_list = 0; pos += avi->movi_list; index = ((tag &0xff) - '0') *10; index += ((tag >> 8) &0xff) - '0'; if (index >= s->nb_streams) continue; st = s->streams[index]; ast = st->priv_data; if (last_pos == pos) avi->non_interleaved = 1; else av_add_index_entry(st, pos, ast->cum_len, len, 0, (flags &AVIIF_INDEX) ? AVINDEX_KEYFRAME : 0); if (ast->sample_size) ast->cum_len += len / ast->sample_size; else ast->cum_len++; last_pos = pos; } return 0; } static int guess_ni_flag(AVFormatContext *s) { int i; int64_t last_start = 0; int64_t first_end = INT64_MAX; for (i = 0; i < s->nb_streams; i++) { AVStream *st = s->streams[i]; int n = st->nb_index_entries; if (n <= 0) continue; if (st->index_entries[0].pos > last_start) last_start = st->index_entries[0].pos; if (st->index_entries[n - 1].pos < first_end) first_end = st->index_entries[n - 1].pos; } return last_start > first_end; } static int avi_load_index(AVFormatContext *s) { AVIContext *avi = s->priv_data; ByteIOContext *pb = &s->pb; uint32_t tag, size; offset_t pos = url_ftell(pb); url_fseek(pb, avi->movi_end, SEEK_SET); for (;;) { if (url_feof(pb)) break; tag = get_le32(pb); size = get_le32(pb); switch (tag) { case MKTAG('i', 'd', 'x', '1'): if (avi_read_idx1(s, size) < 0) goto skip; else goto the_end; break; default: skip: size += (size &1); url_fskip(pb, size); break; } } the_end: url_fseek(pb, pos, SEEK_SET); return 0; } static int avi_read_close(AVFormatContext *s) { int i; AVIContext *avi = s->priv_data; for (i = 0; i < s->nb_streams; i++) { AVStream *st = s->streams[i]; AVIStream *ast = st->priv_data; av_free(ast); av_free(st->actx->extradata); av_free(st->actx->palctrl); } return 0; } static int avi_probe(AVProbeData *p) { if (p->buf_size <= 32) // check file header return 0; if (p->buf[0] == 'R' && p->buf[1] == 'I' && p->buf[2] == 'F' && p->buf[3] == 'F' && p->buf[8] == 'A' && p->buf[9] == 'V' && p->buf[10] == 'I'&& p->buf[11] == ' ') return AVPROBE_SCORE_MAX; else return 0; } AVInputFormat avi_iformat = { "avi", sizeof(AVIContext), avi_probe, avi_read_header, avi_read_packet, avi_read_close, }; /* ֵ֧ĸʽijʼ */ int avidec_init(void) { // עʽ av_register_input_format(&avi_iformat); return 0; } /* AVIF_HASINDEXAVIļ"idx1" AVIF_MUSTUSEINDEXָ˳ AVIF_ISINTERLEAVEDAVIļinterleavedʽ AVIF_WASCAPTUREFILEAVIļò׽ʵʱƵרŷļ AVIF_COPYRIGHTEDAVIļаȨϢ AVIF_MUSTUSEINDEX : ӦóҪʹindexϵ˳ݵչ˳ 磬ñ־ڴһ༭õ֡б // */ ================================================ FILE: ffplay源码和书籍/ffplay/libavformat/avio.c ================================================ /************************************************************************ ** ļʵ URLProtocol ļ ** URLProtocol ǵײļ(file,pipe )ļ򵥷װ ** һֻһתվ󲿷ֺǼתײľʵֺ /************************************************************************/ #include "../berrno.h" #include "avformat.h" /* ffmpegֶ֧Э飬ֵ֧Э鴮 */ URLProtocol *first_protocol = NULL; /* עЭ飨νЭݵ壺ļ׽ֵʽ */ int register_protocol(URLProtocol *protocol) { URLProtocol **p; p = &first_protocol; while (*p != NULL) p = &(*p)->next; *p = protocol; protocol->next = NULL; return 0; } /* URL */ int url_open(URLContext **puc, const char *filename, int flags) { // URLContext *uc; // Э鼰 URLProtocol *up; const char *p; // Эַ char proto_str[128], *q; int err; p = filename; q = proto_str; // ȷЭ while (*p != '\0' && *p != ':') { if (!isalpha(*p)) // protocols can only contain alphabetic chars goto file_proto; if ((q - proto_str) < sizeof(proto_str) - 1) *q++ = *p; p++; } // if the protocol has length 1, we consider it is a dos drive if (*p == '\0' || (q - proto_str) <= 1) { file_proto: strcpy(proto_str, "file"); } else { *q = '\0'; } up = first_protocol; // ѰҺЭ while (up != NULL) { if (!strcmp(proto_str, up->name)) goto found; up = up->next; } err = - ENOENT; goto fail; found: // URL uc = av_malloc(sizeof(URLContext) + strlen(filename)); if (!uc) { err = - ENOMEM; goto fail; } strcpy(uc->filename, filename); uc->prot = up; uc->flags = flags; uc->max_packet_size = 0; // default: stream file // URL err = up->url_open(uc, filename, flags); if (err < 0) { av_free(uc); *puc = NULL; return err; } *puc = uc; return 0; fail: *puc = NULL; return err; } /* ȡ */ int url_read(URLContext *h, unsigned char *buf, int size) { int ret; if (h->flags &URL_WRONLY) return AVERROR_IO; ret = h->prot->url_read(h, buf, size); return ret; } /* ڹļת */ offset_t url_seek(URLContext *h, offset_t pos, int whence) { offset_t ret; if (!h->prot->url_seek) return - EPIPE; ret = h->prot->url_seek(h, pos, whence); return ret; } /* رURL */ int url_close(URLContext *h) { int ret; ret = h->prot->url_close(h); av_free(h); return ret; } int url_get_max_packet_size(URLContext *h) { return h->max_packet_size; } ================================================ FILE: ffplay源码和书籍/ffplay/libavformat/avio.h ================================================ #ifndef AVIO_H #define AVIO_H /* ** ļдģ鶨ݽṹͺ */ #define URL_EOF (-1) typedef int64_t offset_t; /* ļʵȨ޵Ķ */ #define URL_RDONLY 0 #define URL_WRONLY 1 #define URL_RDWR 2 /* ** URLģʾһ루ļ׽ ** URLContext ṹʾеĵǰļЭʹõģйļЭ鹲е ** (ڳʱȷֵ)͹ṹֶ */ typedef struct URLContext { // Э struct URLProtocol *prot; int flags; int max_packet_size; // if non zero, the stream is packetized with this max packet size void *priv_data; // fileһļЭ飬Ϳ char filename[1]; // specified filename } URLContext; /* ** URLЭ飨ļ׽IJ ** URLProtocol ļЭ飬ڹܺ ** һֹļЭӦһ URLProtocol ṹ ** ɾ pipeudptcpЭ飬һ file Э */ typedef struct URLProtocol { const char *name; int(*url_open)(URLContext *h, const char *filename, int flags); int(*url_read)(URLContext *h, unsigned char *buf, int size); int(*url_write)(URLContext *h, unsigned char *buf, int size); offset_t(*url_seek)(URLContext *h, offset_t pos, int whence); int(*url_close)(URLContext *h); struct URLProtocol *next; } URLProtocol; /* ** ϵģļ */ typedef struct ByteIOContext { // ݻ unsigned char *buffer; // С int buffer_size; // ЧֽڷΧ unsigned char *buf_ptr, *buf_end; // void *opaque; // ȡ int (*read_buf)(void *opaque, uint8_t *buf, int buf_size); // д int (*write_buf)(void *opaque, uint8_t *buf, int buf_size); // bufferָ offset_t(*seek)(void *opaque, offset_t offset, int whence); // λ offset_t pos; // position in the file of the current buffer // DzDZҪˢ int must_flush; // true if the next seek should flush // Ƿ񵽴ļβ int eof_reached; // true if eof reached // Ƿд int write_flag; // true if open for writing // ݰĴС int max_packet_size; // int error; // contains the error code or 0 if no error happened } ByteIOContext; int url_open(URLContext **h, const char *filename, int flags); int url_read(URLContext *h, unsigned char *buf, int size); int url_write(URLContext *h, unsigned char *buf, int size); offset_t url_seek(URLContext *h, offset_t pos, int whence); int url_close(URLContext *h); int url_get_max_packet_size(URLContext *h); int register_protocol(URLProtocol *protocol); int init_put_byte(ByteIOContext *s, unsigned char *buffer, int buffer_size, int write_flag, void *opaque, int(*read_buf)(void *opaque, uint8_t *buf, int buf_size), int(*write_buf)(void *opaque, uint8_t *buf, int buf_size), offset_t(*seek)(void *opaque, offset_t offset, int whence)); offset_t url_fseek(ByteIOContext *s, offset_t offset, int whence); void url_fskip(ByteIOContext *s, offset_t offset); offset_t url_ftell(ByteIOContext *s); offset_t url_fsize(ByteIOContext *s); int url_feof(ByteIOContext *s); int url_ferror(ByteIOContext *s); int url_fread(ByteIOContext *s, unsigned char *buf, int size); // get_buffer int get_byte(ByteIOContext *s); unsigned int get_le32(ByteIOContext *s); unsigned int get_le16(ByteIOContext *s); int url_setbufsize(ByteIOContext *s, int buf_size); int url_fopen(ByteIOContext *s, const char *filename, int flags); int url_fclose(ByteIOContext *s); int url_open_buf(ByteIOContext *s, uint8_t *buf, int buf_size, int flags); int url_close_buf(ByteIOContext *s); #endif ================================================ FILE: ffplay源码和书籍/ffplay/libavformat/aviobuf.c ================================================ /* ** лĹļ ByteIOContext صļ ** ByteIOContextURLContextIJͬڣByteIOContextǴģByteIOContextIJǻڻĶǻļ ** URLContextȡݽByteIOContextURLContextByteIOContext棬ByteIOContextĵײ */ #include "../berrno.h" #include "avformat.h" #include "avio.h" #include /* Ļ泤 */ #define IO_BUFFER_SIZE 32768 /* ** ʼ ByteIOContext ṹ */ int init_put_byte(ByteIOContext *s, unsigned char *buffer, int buffer_size, int write_flag, void *opaque, int(*read_buf)(void *opaque, uint8_t *buf, int buf_size), int(*write_buf)(void *opaque, uint8_t *buf, int buf_size), offset_t(*seek)(void *opaque, offset_t offset, int whence)) { s->buffer = buffer; s->buffer_size = buffer_size; s->buf_ptr = buffer; s->write_flag = write_flag; if (!s->write_flag) s->buf_end = buffer; else s->buf_end = buffer + buffer_size; s->opaque = opaque; s->write_buf = write_buf; s->read_buf = read_buf; s->seek = seek; s->pos = 0; s->must_flush = 0; s->eof_reached = 0; s->error = 0; s->max_packet_size = 0; return 0; } /* ** ļ ByteIOContext seek */ offset_t url_fseek(ByteIOContext *s, offset_t offset, int whence) { offset_t offset1; if (whence != SEEK_CUR && whence != SEEK_SET) return - EINVAL; if (whence == SEEK_CUR) { offset1 = s->pos - (s->buf_end - s->buffer) + (s->buf_ptr - s->buffer); if (offset == 0) return offset1; offset += offset1; } offset1 = offset - (s->pos - (s->buf_end - s->buffer)); if (offset1 >= 0 && offset1 <= (s->buf_end - s->buffer)) { s->buf_ptr = s->buffer + offset1; // can do the seek inside the buffer } else { if (!s->seek) return - EPIPE; s->buf_ptr = s->buffer; s->buf_end = s->buffer; if (s->seek(s->opaque, offset, SEEK_SET) == (offset_t) - EPIPE) return - EPIPE; s->pos = offset; } s->eof_reached = 0; return offset; } /* ת */ void url_fskip(ByteIOContext *s, offset_t offset) { url_fseek(s, offset, SEEK_CUR); } /* ȡ */ offset_t url_ftell(ByteIOContext *s) { return url_fseek(s, 0, SEEK_CUR); } /* ļС */ offset_t url_fsize(ByteIOContext *s) { offset_t size; if (!s->seek) return - EPIPE; size = s->seek(s->opaque, - 1, SEEK_END) + 1; s->seek(s->opaque, s->pos, SEEK_SET); return size; } /* Ƿ񵽴ļβ */ int url_feof(ByteIOContext *s) { return s->eof_reached; } /* */ int url_ferror(ByteIOContext *s) { return s->error; } // Input stream /* bufferҲһȡļIJ */ static void fill_buffer(ByteIOContext *s) { int len; if (s->eof_reached) return ; len = s->read_buf(s->opaque, s->buffer, s->buffer_size); if (len <= 0) { // do not modify buffer if EOF reached so that a seek back can be done without rereading data s->eof_reached = 1; if (len < 0) s->error = len; } else { s->pos += len; s->buf_ptr = s->buffer; s->buf_end = s->buffer + len; } } /* ȡһֽ */ int get_byte(ByteIOContext *s) // NOTE: return 0 if EOF, so you cannot use it if EOF handling is necessary { if (s->buf_ptr < s->buf_end) { return *s->buf_ptr++; } else { // ȡ fill_buffer(s); if (s->buf_ptr < s->buf_end) return *s->buf_ptr++; else return 0; } } /* ӹļ ByteIOContext С˷ʽȡֽ,ʵִָ get_byte() */ unsigned int get_le16(ByteIOContext *s) { unsigned int val; val = get_byte(s); val |= get_byte(s) << 8; return val; } /* ӹļ ByteIOContext С˷ʽȡĸֽ,ʵִָ get_le16() */ unsigned int get_le32(ByteIOContext *s) { unsigned int val; val = get_le16(s); val |= get_le16(s) << 16; return val; } #define url_write_buf NULL /* ת */ static int url_read_buf(void *opaque, uint8_t *buf, int buf_size) { URLContext *h = opaque; return url_read(h, buf, buf_size); } /* ת seek */ static offset_t url_seek_buf(void *opaque, offset_t offset, int whence) { URLContext *h = opaque; return url_seek(h, offset, whence); } /* òļ ByteIOContext ڲĴСӦ޸ڲС */ int url_setbufsize(ByteIOContext *s, int buf_size) // must be called before any I/O { uint8_t *buffer; buffer = av_malloc(buf_size); if (!buffer) return - ENOMEM; av_free(s->buffer); s->buffer = buffer; s->buffer_size = buf_size; s->buf_ptr = buffer; if (!s->write_flag) s->buf_end = buffer; else s->buf_end = buffer + buf_size; return 0; } /* 򿪹ļ ByteIOContext */ int url_fopen(ByteIOContext *s, const char *filename, int flags) { URLContext *h; uint8_t *buffer; int buffer_size, max_packet_size; int err; err = url_open(&h, filename, flags); if (err < 0) return err; max_packet_size = url_get_max_packet_size(h); if (max_packet_size) { buffer_size = max_packet_size; // no need to bufferize more than one packet } else { buffer_size = IO_BUFFER_SIZE; } buffer = av_malloc(buffer_size); if (!buffer) { url_close(h); return - ENOMEM; } if (init_put_byte(s, buffer, buffer_size, (h->flags & URL_WRONLY || h->flags & URL_RDWR), h, url_read_buf, url_write_buf, url_seek_buf) < 0) { url_close(h); av_free(buffer); return AVERROR_IO; } s->max_packet_size = max_packet_size; return 0; } /* رչļ ByteIOContext */ int url_fclose(ByteIOContext *s) { URLContext *h = s->opaque; av_free(s->buffer); memset(s, 0, sizeof(ByteIOContext)); return url_close(h); } /* ļ ByteIOContext */ int url_fread(ByteIOContext *s, unsigned char *buf, int size) // get_buffer { int len, size1; size1 = size; while (size > 0) { len = s->buf_end - s->buf_ptr; if (len > size) len = size; if (len == 0) { if (size > s->buffer_size) { len = s->read_buf(s->opaque, buf, size); if (len <= 0) { s->eof_reached = 1; if (len < 0) s->error = len; break; } else { s->pos += len; size -= len; buf += len; s->buf_ptr = s->buffer; s->buf_end = s->buffer /* + len*/; } } else { fill_buffer(s); len = s->buf_end - s->buf_ptr; if (len == 0) break; } } else { memcpy(buf, s->buf_ptr, len); buf += len; s->buf_ptr += len; size -= len; } } return size1 - size; } ================================================ FILE: ffplay源码和书籍/ffplay/libavformat/cutils.c ================================================ /* ** ַIJ */ #include "avformat.h" /* ** str ַ val ַָʾͷȥͷ*ptr */ int strstart(const char *str, const char *val, const char **ptr) { const char *p, *q; p = str; q = val; while (*q != '\0') { if (*p != *q) return 0; p++; q++; } if (ptr) *ptr = p; return 1; } /* ** ַ */ void pstrcpy(char *buf, int buf_size, const char *str) { int c; char *q = buf; if (buf_size <= 0) return ; for (;;) { c = *str++; if (c == 0 || q >= buf + buf_size - 1) break; *q++ = c; } *q = '\0'; } ================================================ FILE: ffplay源码和书籍/ffplay/libavformat/file.c ================================================ /* ** ffplay file rtsprtptcp ЭһЭ飬 file:ǰ׺ʾ file Э顣 ** URLContext ṹͳһʾЩϵЭ飬ṩͳһijӿڡ ** ĹЭʵļʵ URLContext ӿڡļʵ file Э URLContext ӿ */ #include "../berrno.h" #include "avformat.h" #include #ifndef CONFIG_WIN32 #include #include #include #else #include #define open(fname,oflag,pmode) _open(fname,oflag,pmode) #endif /* ** ļ */ static int file_open(URLContext *h, const char *filename, int flags) { int access; int fd; // fileЭ strstart(filename, "file:", &filename); // ʱ־ if (flags &URL_RDWR) access = O_CREAT | O_TRUNC | O_RDWR; else if (flags &URL_WRONLY) access = O_CREAT | O_TRUNC | O_WRONLY; else access = O_RDONLY; #if defined(CONFIG_WIN32) || defined(CONFIG_OS2) || defined(__CYGWIN__) access |= O_BINARY; #endif // ļ fd = open(filename, access, 0666); if (fd < 0) return - ENOENT; // ļURLContextpriv_data h->priv_data = (void*)(size_t)fd; return 0; } /* ȡ */ static int file_read(URLContext *h, unsigned char *buf, int size) { int fd = (size_t)h->priv_data; return read(fd, buf, size); } /* д */ static int file_write(URLContext *h, unsigned char *buf, int size) { int fd = (size_t)h->priv_data; return write(fd, buf, size); } /* ת */ static offset_t file_seek(URLContext *h, offset_t pos, int whence) { int fd = (size_t)h->priv_data; return lseek(fd, pos, whence); } /* رļ */ static int file_close(URLContext *h) { int fd = (size_t)h->priv_data; return close(fd); } /* FILE͵protocol */ URLProtocol file_protocol = { "file", file_open, file_read, file_write, file_seek, file_close, }; ================================================ FILE: ffplay源码和书籍/ffplay/libavformat/utils_format.c ================================================ /* ** ʶļʽýʽʹõһЩຯ */ #include "../berrno.h" #include "avformat.h" #include #define UINT_MAX (0xffffffff) #define PROBE_BUF_MIN 2048 #define PROBE_BUF_MAX 131072 /* Ϊֵ֧ĸʽкܶ࣬Ҫֵ֧ĸʽ */ AVInputFormat *first_iformat = NULL; /* עĸʽ*/ void av_register_input_format(AVInputFormat *format) { AVInputFormat **p; p = &first_iformat; while (*p != NULL) p = &(*p)->next; *p = format; format->next = NULL; } /* Ƚļչʶļ */ int match_ext(const char *filename, const char *extensions) { const char *ext, *p; char ext1[32], *q; if (!filename) return 0; ext = strrchr(filename, '.'); if (ext) { ext++; p = extensions; for (;;) { q = ext1; while (*p != '\0' && *p != ',' && q - ext1 < sizeof(ext1) - 1) *q++ = *p++; *q = '\0'; if (!strcasecmp(ext1, ext)) return 1; if (*p == '\0') break; p++; } } return 0; } /* ̽ļʽʶļʽ */ AVInputFormat *av_probe_input_format(AVProbeData *pd, int is_opened) { AVInputFormat *fmt1, *fmt; int score, score_max; fmt = NULL; score_max = 0; for (fmt1 = first_iformat; fmt1 != NULL; fmt1 = fmt1->next) { if (!is_opened) continue; score = 0; if (fmt1->read_probe) { score = fmt1->read_probe(pd); } else if (fmt1->extensions) { if (match_ext(pd->filename, fmt1->extensions)) score = 50; } if (score > score_max) { score_max = score; fmt = fmt1; } } return fmt; } /* */ int av_open_input_stream(AVFormatContext **ic_ptr, ByteIOContext *pb, const char *filename, AVInputFormat *fmt, AVFormatParameters *ap) { int err; AVFormatContext *ic; AVFormatParameters default_ap; if (!ap) { ap = &default_ap; memset(ap, 0, sizeof(default_ap)); } ic = av_mallocz(sizeof(AVFormatContext)); if (!ic) { err = AVERROR_NOMEM; goto fail; } ic->iformat = fmt; if (pb) ic->pb = *pb; if (fmt->priv_data_size > 0) { ic->priv_data = av_mallocz(fmt->priv_data_size); if (!ic->priv_data) { err = AVERROR_NOMEM; goto fail; } } else { ic->priv_data = NULL; } err = ic->iformat->read_header(ic, ap); if (err < 0) goto fail; *ic_ptr = ic; return 0; fail: if (ic) av_freep(&ic->priv_data); av_free(ic); *ic_ptr = NULL; return err; } /* ļʶļʽȻúʶýʽ */ int av_open_input_file(AVFormatContext **ic_ptr, const char *filename, AVInputFormat *fmt, int buf_size, AVFormatParameters *ap) { int err, must_open_file, file_opened, probe_size; AVProbeData probe_data, *pd = &probe_data; ByteIOContext pb1, *pb = &pb1; file_opened = 0; pd->filename = ""; if (filename) pd->filename = filename; pd->buf = NULL; pd->buf_size = 0; must_open_file = 1; if (!fmt || must_open_file) { if (url_fopen(pb, filename, URL_RDONLY) < 0) { err = AVERROR_IO; goto fail; } file_opened = 1; if (buf_size > 0) url_setbufsize(pb, buf_size); for (probe_size = PROBE_BUF_MIN; probe_size <= PROBE_BUF_MAX && !fmt; probe_size <<= 1) { pd->buf = av_realloc(pd->buf, probe_size); pd->buf_size = url_fread(pb, pd->buf, probe_size); if (url_fseek(pb, 0, SEEK_SET) == (offset_t) - EPIPE) { url_fclose(pb); if (url_fopen(pb, filename, URL_RDONLY) < 0) { file_opened = 0; err = AVERROR_IO; goto fail; } } fmt = av_probe_input_format(pd, 1); } av_freep(&pd->buf); } if (!fmt) { err = AVERROR_NOFMT; goto fail; } err = av_open_input_stream(ic_ptr, pb, filename, fmt, ap); if (err) goto fail; return 0; fail: av_freep(&pd->buf); if (file_opened) url_fclose(pb); *ic_ptr = NULL; return err; } /* һζȡһݰ */ int av_read_packet(AVFormatContext *s, AVPacket *pkt) { return s->iformat->read_packet(s, pkt); } /* ** ЩýļΪ seekƵ֡ffplay Щʱ ** ŵһСֵ */ int av_add_index_entry(AVStream *st, int64_t pos, int64_t timestamp, int size, int distance, int flags) { AVIndexEntry *entries, *ie; int index; if ((unsigned)st->nb_index_entries + 1 >= UINT_MAX / sizeof(AVIndexEntry)) // Խж return - 1; entries = av_fast_realloc(st->index_entries, &st->index_entries_allocated_size, (st->nb_index_entries + 1) * sizeof(AVIndexEntry)); if (!entries) return - 1; st->index_entries = entries; index = av_index_search_timestamp(st, timestamp, AVSEEK_FLAG_ANY); if (index < 0) // { index = st->nb_index_entries++; ie = &entries[index]; assert(index == 0 || ie[ - 1].timestamp < timestamp); } else // в { ie = &entries[index]; if (ie->timestamp != timestamp) { if (ie->timestamp <= timestamp) return - 1; memmove(entries + index + 1, entries + index, sizeof(AVIndexEntry)*(st->nb_index_entries - index)); st->nb_index_entries++; } } ie->pos = pos; ie->timestamp = timestamp; ie->size = size; ie->flags = flags; return index; } int av_index_search_timestamp(AVStream *st, int64_t wanted_timestamp, int flags) { AVIndexEntry *entries = st->index_entries; int nb_entries = st->nb_index_entries; int a, b, m; int64_t timestamp; a = - 1; b = nb_entries; while (b - a > 1) //ûм¼idxֵõ۰ { m = (a + b) >> 1; timestamp = entries[m].timestamp; if (timestamp >= wanted_timestamp) b = m; if (timestamp <= wanted_timestamp) a = m; } m = (flags &AVSEEK_FLAG_BACKWARD) ? a : b; if (!(flags &AVSEEK_FLAG_ANY)) { while (m >= 0 && m < nb_entries && !(entries[m].flags &AVINDEX_KEYFRAME)) { m += (flags &AVSEEK_FLAG_BACKWARD) ? - 1: 1; } } if (m == nb_entries) return - 1; return m; } void av_close_input_file(AVFormatContext *s) { int i; AVStream *st; if (s->iformat->read_close) s->iformat->read_close(s); for (i = 0; i < s->nb_streams; i++) { st = s->streams[i]; av_free(st->index_entries); av_free(st->actx); av_free(st); } url_fclose(&s->pb); av_freep(&s->priv_data); av_free(s); } AVStream *av_new_stream(AVFormatContext *s, int id) { AVStream *st; if (s->nb_streams >= MAX_STREAMS) return NULL; st = av_mallocz(sizeof(AVStream)); if (!st) return NULL; st->actx = avcodec_alloc_context(); s->streams[s->nb_streams++] = st; return st; } void av_set_pts_info(AVStream *s, int pts_wrap_bits, int pts_num, int pts_den) { s->time_base.num = pts_num; s->time_base.den = pts_den; } ================================================ FILE: ffplay源码和书籍/ffplay/libavutil/avutil.h ================================================ #ifndef AVUTIL_H #define AVUTIL_H #ifdef __cplusplus extern "C" { #endif #include "common.h" #include "bswap.h" #include "mathematics.h" #include "rational.h" #define AV_STRINGIFY(s) AV_TOSTRING(s) #define AV_TOSTRING(s) #s #define LIBAVUTIL_VERSION_INT ((49<<16)+(0<<8)+0) #define LIBAVUTIL_VERSION 49.0.0 #define LIBAVUTIL_BUILD LIBAVUTIL_VERSION_INT #define LIBAVUTIL_IDENT "Lavu" AV_STRINGIFY(LIBAVUTIL_VERSION) /* ظʽ */ enum PixelFormat { PIX_FMT_NONE = - 1, PIX_FMT_YUV420P, // Planar YUV 4:2:0 (1 Cr & Cb sample per 2x2 Y samples) PIX_FMT_YUV422, // Packed pixel, Y0 Cb Y1 Cr PIX_FMT_RGB24, // Packed pixel, 3 bytes per pixel, RGBRGB... PIX_FMT_BGR24, // Packed pixel, 3 bytes per pixel, BGRBGR... PIX_FMT_YUV422P, // Planar YUV 4:2:2 (1 Cr & Cb sample per 2x1 Y samples) PIX_FMT_YUV444P, // Planar YUV 4:4:4 (1 Cr & Cb sample per 1x1 Y samples) PIX_FMT_RGBA32, // Packed pixel, 4 bytes per pixel, BGRABGRA..., stored in cpu endianness PIX_FMT_YUV410P, // Planar YUV 4:1:0 (1 Cr & Cb sample per 4x4 Y samples) PIX_FMT_YUV411P, // Planar YUV 4:1:1 (1 Cr & Cb sample per 4x1 Y samples) PIX_FMT_RGB565, // always stored in cpu endianness PIX_FMT_RGB555, // always stored in cpu endianness, most significant bit to 1 PIX_FMT_GRAY8, PIX_FMT_MONOWHITE, // 0 is white PIX_FMT_MONOBLACK, // 0 is black PIX_FMT_PAL8, // 8 bit with RGBA palette PIX_FMT_YUVJ420P, // Planar YUV 4:2:0 full scale (jpeg) PIX_FMT_YUVJ422P, // Planar YUV 4:2:2 full scale (jpeg) PIX_FMT_YUVJ444P, // Planar YUV 4:4:4 full scale (jpeg) PIX_FMT_XVMC_MPEG2_MC, // XVideo Motion Acceleration via common packet passing(xvmc_render.h) PIX_FMT_XVMC_MPEG2_IDCT, PIX_FMT_UYVY422, // Packed pixel, Cb Y0 Cr Y1 PIX_FMT_UYVY411, // Packed pixel, Cb Y0 Y1 Cr Y2 Y3 PIX_FMT_NB, }; #ifdef __cplusplus } #endif #endif ================================================ FILE: ffplay源码和书籍/ffplay/libavutil/bswap.h ================================================ /************************************************************************/ /* ֽ˳򽻻 */ /************************************************************************/ #ifndef __BSWAP_H__ #define __BSWAP_H__ /* 16bitֽ˳򽻻 */ static inline uint16_t bswap_16(uint16_t x) { return (x >> 8) | (x << 8); } /* 32bitֽ˳򽻻 */ static inline uint32_t bswap_32(uint32_t x) { x = ((x << 8) &0xFF00FF00) | ((x >> 8) &0x00FF00FF); return (x >> 16) | (x << 16); } // be2me ... BigEndian to MachineEndian // le2me ... LittleEndian to MachineEndian #define be2me_16(x) bswap_16(x) #define be2me_32(x) bswap_32(x) #define le2me_16(x) (x) #define le2me_32(x) (x) #endif ================================================ FILE: ffplay源码和书籍/ffplay/libavutil/common.h ================================================ /************************************************************************/ /* õͺͺ */ /************************************************************************/ #ifndef COMMON_H #define COMMON_H #include #include #include #include #if defined(WIN32) && !defined(__MINGW32__) && !defined(__CYGWIN__) #define CONFIG_WIN32 #endif #ifdef CONFIG_WIN32 #define inline __inline #endif typedef signed char int8_t; typedef signed short int16_t; typedef signed int int32_t; typedef unsigned char uint8_t; typedef unsigned short uint16_t; typedef unsigned int uint32_t; #ifdef CONFIG_WIN32 typedef signed __int64 int64_t; typedef unsigned __int64 uint64_t; #else typedef signed long long int64_t; typedef unsigned long long uint64_t; #endif #ifdef CONFIG_WIN32 #define int64_t_C(c) (c ## i64) #define uint64_t_C(c) (c ## i64) #else #define int64_t_C(c) (c ## LL) #define uint64_t_C(c) (c ## ULL) #endif #ifndef INT64_MAX #define INT64_MAX int64_t_C(9223372036854775807) #endif /* Сд޹ַıȽ */ static int strcasecmp(char *s1, const char *s2) { while (toupper((unsigned char) *s1) == toupper((unsigned char) *s2++)) if (*s1++ == '\0') return 0; return (toupper((unsigned char) *s1) - toupper((unsigned char) *--s2)); } /* ޷ */ static inline int clip(int a, int amin, int amax) { if (a < amin) return amin; else if (a > amax) return amax; else return a; } #endif ================================================ FILE: ffplay源码和书籍/ffplay/libavutil/mathematics.h ================================================ #ifndef MATHEMATICS_H #define MATHEMATICS_H /* */ static inline int64_t av_rescale(int64_t a, int64_t b, int64_t c) { return a *b / c; } #endif ================================================ FILE: ffplay源码和书籍/ffplay/libavutil/rational.h ================================================ /************************************************************************/ /* */ /************************************************************************/ #ifndef RATIONAL_H #define RATIONAL_H /* ṹ */ typedef struct AVRational { int num; // numerator // int den; // denominator // ĸ } AVRational; /* */ static inline double av_q2d(AVRational a) { return a.num / (double)a.den; } #endif ================================================ FILE: ffplay源码和书籍/ffplay/update.txt ================================================ ӭ mcodec.cnblogs.com ϵ tslking@tom.com ================================================ FILE: iOS资料/AVFoundation之视频捕捉.md ================================================ # AVFoundation之视频捕捉 ## 1.概念 ### 1.1 捕捉会话 AV Foundation 捕捉栈核心类是AVCaptureSession。一个捕捉会话相当于一个虚拟的“插线板”。用于连接输入和输出的资源。 ### 1.2 捕捉设备 AVCaptureDevice为摄像头、麦克风等物理设备提供接口。大部分我们使用的设备都是内置于MAC或者iPhone、iPad上的。当然也可能出现外部设备。但是AVCaptureDevice 针对物理设备提供了大量的控制方法。比如控制摄像头聚焦、曝光、白平衡、闪光灯等。 ### 1.3 捕捉设备的输入 注意:为捕捉设备添加输入,不能添加到AVCaptureSession 中,必须通过将它封装到一个AVCaptureDeviceInputs实例中。这个对象在设备输出数据和捕捉会话间扮演接线板的作用。 ### 1.4 捕捉的输出 AVCaptureOutput 是一个抽象类。用于为捕捉会话得到的数据寻找输出的目的地。框架定义了一些抽象类的高级扩展类。例如 AVCaptureStillImageOutput 和 AVCaptureMovieFileOutput类。使用它们来捕捉静态照片、视频。例如 AVCaptureAudioDataOutput 和 AVCaptureVideoDataOutput ,使用它们来直接访问硬件捕捉到的数字样本。 ### 1.5 捕捉连接 AVCaptureConnection类.捕捉会话先确定由给定捕捉设备输入渲染的媒体类型,并自动建立其到能够接收该媒体类型的捕捉输出端的连接。 ### 1.6 捕捉预览 如果不能在影像捕捉中看到正在捕捉的场景,那么应用程序用户体验就会很差。幸运的是框架定义了 AVCaptureVideoPreviewLayer 类来满足该需求。这样就可以对捕捉的数据进行实时预览。 ## 2. 创建预览视图 ### 2.1 创建预览视图 图2-1是项目的用户界面的组成图示。我们把重点放在中间层THPreview View的实现上。因为它直接包含了AV Foundation 的用户界面。 ![img](https://p3-sign.toutiaoimg.com/tos-cn-i-qvj2lq49k0/97b8ce0f077a4437b2db557425802903~noop.image?_iz=58558&from=article.pc_detail&x-expires=1679402552&x-signature=tzSzjuY1UOAY1nwbQMhLCDMSSes%3D) 图2-1所示的THPreview View 类提供给用户用i 个摄像头当前拍摄内容的实时预览图。我们将使用 AVCaptureVideoPreviewLayer方法实现这个行为。首先我们通过认识THPreview View 的接口开始了解如何实现。 #### 2.1.1THPreviewView 接口 ![img](https://p3-sign.toutiaoimg.com/tos-cn-i-qvj2lq49k0/8ffe110e42414d15b877bb2f4ae00e57~noop.image?_iz=58558&from=article.pc_detail&x-expires=1679402552&x-signature=hoaTJPClxcEAZVMyi9ezCKNE7ns%3D) #### 2.1.2THPreviewView 实现 ![img](https://p3-sign.toutiaoimg.com/tos-cn-i-qvj2lq49k0/abd585067b624409bcea8ea059be0db3~noop.image?_iz=58558&from=article.pc_detail&x-expires=1679402552&x-signature=PHbdx0dkHlwYZ2dmrsRd5ICWPHU%3D) ### 2.2 坐标空间转换 当使用AV Foundation 的捕捉API时,一定要理解屏幕坐标系和捕捉设备坐标系不同。iOS6.0之前的版本,要在这2个坐标空间进行转换非常困难。要精确的将屏幕坐标点转换为摄像头坐标点。开发者必须考虑诸如视频重力、镜像、图层变换和方向等因素进行综合计算。幸运的是, AVCaptureVideoPreviewLayer现在定义了一个转换方法让这一过程变得简单多了。 AVCaptureVideoPreviewLayer定义了2个方法用于坐标系间进行转换: - captureDevicePointOfInterestForPoint:获取屏幕坐标系的CGPoint 数据,返回转换得到的设备坐标系CGPoint数据。 - pointForCaptureDevicePointOfInterest:获取摄像头坐标系的CGPoint数据,返回转换得到的屏幕坐标系CGPoint 数据。 THPreview View 使用 captureDevicePointOfInterestForPoint:方法将用户触点信息转换为摄像头设备坐标系的中的点。在项目中点击对焦和点击曝光功能实现会用到这个转换坐标点。 ## 3.创建捕捉控制器 ### 3.1 在THCameraController 类中实现 用于配置不同的捕捉设备,同时对捕捉的输出进行控制和交互。 ![img](https://p3-sign.toutiaoimg.com/tos-cn-i-qvj2lq49k0/5c5dfd39e9154a068507a2f776d443c1~noop.image?_iz=58558&from=article.pc_detail&x-expires=1679402552&x-signature=ixPXlqM5TvyQfLExtH9BZJXCBVk%3D) ![img](https://p3-sign.toutiaoimg.com/tos-cn-i-qvj2lq49k0/16d1828e1ace4de889235d53b9d77676~noop.image?_iz=58558&from=article.pc_detail&x-expires=1679402552&x-signature=MNP4kjNXVmwVjNKj5O%2B%2BEPXaR%2Fs%3D) ![img](https://p3-sign.toutiaoimg.com/tos-cn-i-qvj2lq49k0/220a430de18440ac9c7f3a425bfb853e~noop.image?_iz=58558&from=article.pc_detail&x-expires=1679402552&x-signature=y3EcxH%2BTrCU%2Ff3t4vy3Upc8Laig%3D) ### 3.2 在设置捕捉会话 在 THCameraController.m 需要导入系统框架< AVFoundation/AVFoundation.h> ![img](https://p3-sign.toutiaoimg.com/tos-cn-i-qvj2lq49k0/c6e13258eddd452287a98dfc14f30522~noop.image?_iz=58558&from=article.pc_detail&x-expires=1679402552&x-signature=v01VMyWDlInvJuGVyVZWfZi%2FYe8%3D) ### 3.3 设置捕捉会话 setupSession:方法实现 ![img](https://p3-sign.toutiaoimg.com/tos-cn-i-qvj2lq49k0/e40438ea757548499723d948784ff164~noop.image?_iz=58558&from=article.pc_detail&x-expires=1679402552&x-signature=SDOIHWQbjSAtHev0lbttFghgBSk%3D) ![img](https://p3-sign.toutiaoimg.com/tos-cn-i-qvj2lq49k0/c8d98d66fb434f89a72c39f87e98496d~noop.image?_iz=58558&from=article.pc_detail&x-expires=1679402552&x-signature=UcFU3oqhz4HjhIllroWdDdPh87E%3D) ![img](https://p3-sign.toutiaoimg.com/tos-cn-i-qvj2lq49k0/8d949f76a00f4be4ab3dd9b83de17e6d~noop.image?_iz=58558&from=article.pc_detail&x-expires=1679402552&x-signature=%2BxBhWlJLIrasEw%2BAtWSBxJnAvfk%3D) ![img](https://p3-sign.toutiaoimg.com/tos-cn-i-qvj2lq49k0/f94e86a092f5407f9efdf75934bfe606~noop.image?_iz=58558&from=article.pc_detail&x-expires=1679402552&x-signature=9ygkpa1RVPiNOsoL%2F3xmUXyWbHw%3D) ### 3.4 启动和停止捕捉会话 ![img](https://p3-sign.toutiaoimg.com/tos-cn-i-qvj2lq49k0/661936fd08874be1a03c3d6fd2ffffc7~noop.image?_iz=58558&from=article.pc_detail&x-expires=1679402552&x-signature=TfJNFmT5QXrHOaPBU2vznJk47Ss%3D) ### 3.5 处理隐私需求 在这个项目会涉及到摄像头、相册、麦克风。需要给出用户提醒,处理隐私需求 注意:iOS7版本只有特定地区有法律规定才会询问用户是否可以访问设备的相机。而从iOS8.0之后,所有的地区和用户都要在应用程序中取得授权才可以访问相机。 常用的隐私设置 plist 修改 ![img](https://p3-sign.toutiaoimg.com/tos-cn-i-qvj2lq49k0/5600be7b6e6e4d0c89170f69de3f3de4~noop.image?_iz=58558&from=article.pc_detail&x-expires=1679402552&x-signature=hUuY66COJM2WmI6Du1ViwMK7aJU%3D) ## 4.切换摄像头 ### 4.1 摄像头的支撑方法 ![img](https://p3-sign.toutiaoimg.com/tos-cn-i-qvj2lq49k0/20b77c0aa78745ab9edc67cf06e7f36f~noop.image?_iz=58558&from=article.pc_detail&x-expires=1679402552&x-signature=1S1gNxTAmLU9Ov5r366YPtR%2BWSM%3D) ![img](https://p3-sign.toutiaoimg.com/tos-cn-i-qvj2lq49k0/5529768d56704407bee3a7b55e534952~noop.image?_iz=58558&from=article.pc_detail&x-expires=1679402552&x-signature=6%2F%2FwlQ1uy8%2F65fL3YdhSddvHY7s%3D) ![img](https://p3-sign.toutiaoimg.com/tos-cn-i-qvj2lq49k0/c47178a6641e47558f4df7b3ddb6f8f2~noop.image?_iz=58558&from=article.pc_detail&x-expires=1679402552&x-signature=7mlepY%2Fu0yyHOSt16Xz7dynX9Tk%3D) ### 4.2 切换摄像头 ![img](https://p3-sign.toutiaoimg.com/tos-cn-i-qvj2lq49k0/731d6bde611a48409d5b918a471205f4~noop.image?_iz=58558&from=article.pc_detail&x-expires=1679402552&x-signature=guZRPhpTFBSEi3zIa7Pw8TMMO64%3D) ![img](https://p3-sign.toutiaoimg.com/tos-cn-i-qvj2lq49k0/ff086142e3394cc6aea384a4aa572108~noop.image?_iz=58558&from=article.pc_detail&x-expires=1679402552&x-signature=I2QrflvR1TfTqVaN0lXqU5bhi9s%3D) ![img](https://p3-sign.toutiaoimg.com/tos-cn-i-qvj2lq49k0/bcce814c0b7e40ff9089e1449f97e439~noop.image?_iz=58558&from=article.pc_detail&x-expires=1679402552&x-signature=gZDdPyUYgSf35JhZyaf2DCWbTiE%3D) 原文https://www.toutiao.com/article/7130582816781369894/?channel=&source=search_tab ================================================ FILE: iOS资料/IOS 剪辑编辑器.md ================================================ # IOS 剪辑编辑器 最近这两年视频剪辑非常火,很多APP都内置了视频剪辑功能。 IOS视频剪辑主要依赖AVFoundation实现。 ## 1、AVMutableComposition 视频剪辑需要创建一个工程,这个工程只负责把来自不同素材的视频和音频重新组合到自己的轨道track上,等于在内存里把不同素材重新构建成一个新的视频素材。因此你可以直接在AVPlayer上播放这个工程,就像播放一个正常的视频,AVMutableComposition可以等同于AVAsset被使用。 如果你只想把不同的素材拼接起来,仅使用AVMutableComposition就足够了。 ![img](https:////upload-images.jianshu.io/upload_images/9906913-2f1246cfa0635caf.png?imageMogr2/auto-orient/strip|imageView2/2/w/444/format/webp) composition结构 ### 1.1 插入视频轨道 ``` let composition = AVMutableComposition() let track = composition.addMutableTrack(withMediaType: .video, preferredTrackID: kCMPersistentTrackID_Invalid) ``` ### 1.2 插入素材 ``` var path = Bundle.main.path(forResource:"clip", ofType:"mp4") let asset =AVURLAsset(url:URL(fileURLWithPath: path!)) let assetTrack = asset.tracks(withMediaType: .video).first var assetTimeRange = CMTimeRangeMake(start:CMTime.zero, duration: assetA.duration) var startTime = CMTime.zero trystack.insertTimeRange(assetTimeRange, of: assetTrackA, at: startTime) ``` 插入音频的方法大同小异,如果只是为了拼接视频,到这里就足够了。 ## 2、AVMutableVideoComposition 如果你想对合并后的视频素材进行处理,例如转场、特效、滤镜、文本、贴图这样的操作,你需要在AVMutableVideoComposition里完成。 ![img](https:////upload-images.jianshu.io/upload_images/9906913-394a710334638c6b.png?imageMogr2/auto-orient/strip|imageView2/2/w/544/format/webp) video composition结构 ### 2.1 直接创建 ``` let videoComposition = AVMutableVideoComposition(propertiesOf:composition) ``` 当在composition上创建videoComposition之后,会自动创建AVMutableVideoCompositionInstruction和AVMutableVideoCompositionLayerInstruction。 #### 2.1.1 AVMutableVideoCompositionInstruction 一个等待处理的视频剪辑composition会根据插入素材的情况被自动划分为多个可剪辑的区域,例如0-1秒或者2-5秒,你不需要去思考,当你直接创建videoComposition的时候instruction会被自动的创建出来,每一个instruction代表一个可编辑的时间范围。 #### 2.1.2 AVMutableVideoCompositionLayerInstruction 如果一个视频存在2个视频轨道和2个音频轨道,instruction代表1-3秒的时间范围,那么layerInstruction会自动绑定当前这个时间范围里的所有轨道,有几个轨道就有几个layerInstruction。 #### 2.1.3 利用instruction和layerInstruction转场 假设你有两个视频,被插入到两个轨道里,第一个视频插入到track 1的0-3秒,第二个视频插入到track 2的2-5秒,那么合并后的视频长度就是5秒。中间叠加的1秒就可以进行转场的操作。 ![img](https:////upload-images.jianshu.io/upload_images/9906913-826e379e064b5082.png?imageMogr2/auto-orient/strip|imageView2/2/w/636/format/webp) Instruction结构 ``` let instructions = videoComposition.instructions as! [AVMutableVideoCompositionInstruction] for instruction in instructions { if instruction.layerInstructions.count < 2 { continue } var layerA = instruction.layerInstructions.first as! AVMutableVideoCompositionLayerInstruction let layerB = instruction.layerInstructions.last as! AVMutableVideoCompositionLayerInstruction let fromEndTranform = CGAffineTransform(translationX: composition.naturalSize.width, y: 0) let toStartTranform = CGAffineTransform(translationX: -composition.naturalSize.width, y: 0) let range = CMTimeRangeMake(start: instruction.timeRange.start, duration: CMTimeMake(value: 60, timescale: 30)) let identityTransform = CGAffineTransform.identity layerB.setTransformRamp(fromStart: fromEndTranform, toEnd: identityTransform, timeRange: range) layerA.setTransformRamp(fromStart: identityTransform, toEnd: toStartTranform, timeRange: range) layerA.setOpacityRamp(fromStartOpacity: 1.0, toEndOpacity: 0.0, timeRange: range) } ``` 利用CGAffineTransform进行转场,两个轨道的视频都进行位移进行转场。 ## 3、AVAsynchronousCIImageFilteringRequest 这种方式不需要instruction和layer instruction,也是简单的一种方式,只能做滤镜做不了转场,它是把整个composition看成一个。所以你能从里面截取当前的一帧视频,并对这一帧视频做处理。 ``` letvideoComposition = AVMutableVideoComposition(asset:composition) { (request)in let sourceImage = request.sourceImage.clampedToExtent() let outputImage = sourceImage.applyingFilter("CIPhotoEffectProcess") request.finish(with: outputImage, context:nil) } ``` 如果你只想给视频套个滤镜,这就足够了。 ## 4、customVideoCompositorClass 如果你想给视频添加滤镜又想添加转场,你需要使用第三种,这种模式下可以让你针对每一帧视频做特效,并且可以针对不同的轨道做转场。 ``` let videoComposition = AVMutableVideoComposition(propertiesOf: composition) videoComposition.customVideoCompositorClass = CustomVideoCompositor.self _ = CMTimeRangeMake(start:CMTimeMake(value:30, timescale:30), duration: CMTimeMake(value:30, timescale:30)) letinstruction =VideoCompositionInstruction(timeRange:assetTimeRange) // instruction.timeRange = assetTimeRange videoComposition.instructions= [instruction] ``` 和前面的方法不同我们需要自定义两个类CustomVideoCompositor和VideoCompositionInstruction,前者用来处理每一帧的视频。如果你想传递滤镜的名称、参数,转场的参数、类别你还是需要定义后面的这个类,通过它来传递信息。 ### 4.1 VideoCompositionInstruction 作为例子,只传递了一个时间范围 ``` import Foundation import AVFoundation final class VideoCompositionInstruction: NSObject, AVVideoCompositionInstructionProtocol { var timeRange: CMTimeRange let enablePostProcessing: Bool = true let containsTweening: Bool = false var requiredSourceTrackIDs: [NSValue]? var passthroughTrackID: CMPersistentTrackID = kCMPersistentTrackID_Invalid lazy var transform:CGAffineTransform! = { let obj = CGAffineTransform() return obj }() init(timeRange: CMTimeRange) { self.timeRange = timeRange super.init() } } ``` ### 4.2 CustomVideoCompositor 作为例子,这个视频只有一个轨道,设定了一个转场的状态,对视频增加了一个滤镜,实际情况你还需要开发更多,但方法就是这样的,在这个类里你可以做任何想做的修改,只是比前两种方法要复杂一些。例如转场,你需要自己计算每一帧的状态,而前面你只需要指定几个参数。 ``` import AVFoundation import CoreImage class CustomVideoCompositor : NSObject, AVVideoCompositing { private let queue = DispatchQueue(label: "com.langwan.videoclipeditor.Langwan-VideoClipEditor.render", qos: .default) private var renderContext: AVVideoCompositionRenderContext = AVVideoCompositionRenderContext() private let colorSpace = CGColorSpaceCreateDeviceRGB() private let ciContext: CIContext = { if let eaglContext = EAGLContext(api: .openGLES3) ?? EAGLContext(api: .openGLES2) { return CIContext(eaglContext: eaglContext) } return CIContext() }() private static let pixelFormat = kCVPixelFormatType_32BGRA let sourcePixelBufferAttributes: [String : Any]? = [ kCVPixelBufferPixelFormatTypeKey as String : NSNumber(value: CustomVideoCompositor.pixelFormat), kCVPixelBufferOpenGLESCompatibilityKey as String : NSNumber(value: true), ] let requiredPixelBufferAttributesForRenderContext: [String : Any] = [ kCVPixelBufferPixelFormatTypeKey as String : NSNumber(value: CustomVideoCompositor.pixelFormat), kCVPixelBufferOpenGLESCompatibilityKey as String : NSNumber(value: true), ] func renderContextChanged(_ newRenderContext: AVVideoCompositionRenderContext) { renderContext = newRenderContext } func startRequest(_ request: AVAsynchronousVideoCompositionRequest) { // print("startRequest") autoreleasepool { queue.async { guard let instruction = request.videoCompositionInstruction as? VideoCompositionInstruction else { print("instruction is not VideoCompositionInstruction") return } let frameBuffer = self.renderFrame(forRequest: request) request.finish(withComposedVideoFrame: frameBuffer) } } } private func renderFrame(forRequest request: AVAsynchronousVideoCompositionRequest) -> CVPixelBuffer { let stackId = request.sourceTrackIDs[0] guard let frameBuffer = request.sourceFrame(byTrackID: CMPersistentTrackID(stackId)) else { let blankBuffer = self.renderContext.newPixelBuffer() return blankBuffer! } let sourceImage = CIImage(cvPixelBuffer: frameBuffer) //let filter = CIFilter(name: "CIPhotoEffectProcess")! let w = CVPixelBufferGetWidth(frameBuffer) let transform = CGAffineTransform(translationX: CGFloat(w / 2), y: 0) let outputImage = sourceImage.applyingFilter("CIPhotoEffectProcess").transformed(by: transform) let renderedBuffer = renderContext.newPixelBuffer() ciContext.render(outputImage, to: renderedBuffer!, bounds: outputImage.extent, colorSpace: self.colorSpace) return renderedBuffer! } } ``` 具体的例子可以参考 [swift_video_clip_editor_exmaple](https://links.jianshu.com/go?to=https%3A%2F%2Fgithub.com%2Flangwan%2Fswift_video_clip_editor_exmaple) 并下载源代码。 原文链接:https://www.jianshu.com/p/24c918d128a4 ================================================ FILE: iOS资料/IOS之多媒体API.md ================================================ # IOS之多媒体API ## 1、播放视频 **视频文件介绍** 视频格式可以分为适合本地播放的本地影像视频和适合在网络中播放的网络流媒体影像视频两大类。尽管后者在播放的稳定性和播放画面质量上可能没有前者优秀,但网络流媒体影像视频的广泛传播性使之正被广泛应用于视频点播、网络演示、远程教育、网络视频广告等等互联网信息服务领域。 **适合移动设备的视频文件** 3GP,3GP是一种3G流媒体的视频编码格式,主要是为了配合3G网络的高传输速度而开发的,也是目前手机中最为常见的一种视频格式。 视频MP4格式,除了支持MP3所具有的音乐播放功能外,还具备强大的MPEG-4视频播放能力。 iPhone中还支持mov格式文件。 **iOS播放视频** iOS sdk为播放视频提供了非常简便方法,提供的MPMoviePlayerViewController类作为开发使用,在iOS4以前的版本是MPMoviePlayerController。 在iPhone开发规范中禁止使用私有API播放视频,因此播放画面的控制的控件都是有iPhone提供好的,我们没有别的选择。我们能做的: 加载URL中视频 播放、暂停视频 用户控制行为和缩放模式 产生通知 **视频播放案例** [![wps_clip_image-11562](https://images.cnblogs.com/cnblogs_com/syxchina/201210/201210142308039718.png)](http://images.cnblogs.com/cnblogs_com/syxchina/201210/201210142308033929.png) 添加 MediaPlayer.framework MoviePlayerViewController.h ``` #import @interface MoviePlayerViewController : UIViewController { MPMoviePlayerViewController * moviePlayerView; } @property (nonatomic, retain) MPMoviePlayerViewController * moviePlayerView; -(IBAction) playMovie: (id) sender; - (void) playingDone; @end ``` m文件的加载和卸载方法 ``` - (void) viewDidLoad { [[NSNotificationCenter defaultCenter] addObserver:self selector: @selector(playingDone) name:MPMoviePlayerPlaybackDidFinishNotification object:nil]; } - (void)dealloc { [[NSNotificationCenter defaultCenter] removeObserver:self]; [moviePlayerView release]; [super dealloc]; } ``` MPMoviePlayerViewController提供了在播放过程中的状态改变和其它事件的通知。在viewDidLoad注册了一个播放完成的通知,常用的通知有: MPMoviePlayerPlaybackDidFinishNotification通知接收者播放结束。 MPMoviePlayerScalingModeDidChangeNotification改变影片的尺寸。 MPMoviePlayerContentPreloadDidFinishNotification表示预处理以及完成,准备开始播放影片。 dealloc方法中的[[NSNotificationCenter defaultCenter] removeObserver:self];影片播放完成要注销通知。 **播放事件** ``` - (IBAction) playMovie: (id) sender { moviePlayerView = [[MPMoviePlayerViewController alloc] initWithContentURL:[NSURL fileURLWithPath:[[NSBundle mainBundle] pathForResource:@"short" ofType:@"3gp"]]]; moviePlayerView.moviePlayer.controlStyle = MPMovieControlStyleFullscreen; moviePlayerView.moviePlayer.scalingMode = MPMovieScalingModeAspectFit; // MPMovieControlStyleNone //MPMovieControlStyleEmbedded //MPMovieControlStyleDefault //[movieplayer play]; //在当前view上添加视频的视图 [[[UIApplication sharedApplication] keyWindow] addSubview:moviePlayerView.view]; } ``` 视频文件可以播放资源目录、沙箱目录和网络播放。本例中我们采用资源目录。 moviePlayerView.moviePlayer属性是MPMoviePlayerController类型,它有的controlStyle属性 可以控制播放行为,它的取值有: > MPMovieControlStyleFullscreen > > MPMovieControlStyleNone没有播放控件 > > MPMovieControlStyleEmbedded > > MPMovieControlStyleDefault > > MPMoviePlayerController类还有scalingMode属性用于控制影片的尺寸,它的取值有: > > MPMovieScalingModeNone原始尺寸 > > MPMovieScalingModeAspectFit缩放到一个填充方向 > > MPMovieScalingModeAspectFill填充两边可能会切除一部分 > > MPMovieScalingModeFill填充两边可能会改变比例 **播放完成** ``` - (void) playingDone { NSLog(@"播放完成"); [moviePlayerView.view removeFromSuperview]; [moviePlayerView release]; moviePlayerView = nil; } ``` playingDone 方法是在影片播放完成时候调用,这是因为我们在通知中心注册的方法。 播放完成需要把播放视图remove这样才可以获得上一个屏幕。 ## 2、播放音频 ### 2.1 音频文件介绍 有两类主要的音频文件格式: 无损格式,例如WAV,PCM,TTA,FLAC,AU,APE,TAK,WavPack(WV) ,CAF 有损格式,例如MP3,Windows Media Audio(WMA),Ogg Vorbis(OGG),AAC **移动音频文件** 作为移动设备音频文件应该原则上比较小,一般的格式: WAV、由于无损压缩效果最好。 MP3、有损压缩,文件比较小,由于去除的是人类无法感应到的声音,效果也很好。这是目前常用格式。 AAC、压缩比例更大,比MP3文件还要小。 CAF(Core Audio Format)是Apple专用的无损压缩格式。 ### 2.2 Core Audio [![wps_clip_image-7562](https://images.cnblogs.com/cnblogs_com/syxchina/201210/201210142308209920.png)](http://images.cnblogs.com/cnblogs_com/syxchina/201210/201210142308171297.png) **高级API,易用** System Sound API –播放短声音、警告音等。 AVFoundation 可以播放长时间声音,简单易用。 低级API,能够对音频有更多的控制 Audio Toolbox – 录制、播放、音频流有全面的控制。 OpenAL – 播放立体声,常用于游戏。 ### 2.3 System Sound API System Sound 可以播放“短的”声音,所谓短声音就是5秒以内。 不循环、没有声音控制、立即播放。 播放格式限制: 线性PCM 和 IMA4 .caf .aif 或 .wav **播放“短声音”** 播放“短声音”主要就是两个步骤: 注册声音 AudioServicesCreateSystemSoundID ((CFURLRef)fileURL, &myID); 播放声音 AudioServicesPlaySystemSound (myID); 监听完成事件方法 AudioServicesAddSystemSoundCompletion 清除播放sound ID SystemSoundID myID; AudioServicesDisposeSystemSoundID (myID); 震动 也可以通过System Sound API让iPhone震动,但是iPod touch不能震动。 震动可以通过指定一个特殊的system sound ID—— kSystemSoundID_Vibrate实现。 AudioServicesPlaySystemSound (kSystemSoundID_Vibrate); **实例** [![wps_clip_image-14723](https://images.cnblogs.com/cnblogs_com/syxchina/201210/201210142308255488.png)](http://images.cnblogs.com/cnblogs_com/syxchina/201210/201210142308258029.png) SystemSoundServices 添加AudioToolbox.framework框架 SystemSoundServicesViewController.h文件 ``` #import #include @interface SystemSoundServicesViewController : UIViewController; - (IBAction) playSystemSound; - (IBAction) vibrate; @end ``` **播放事件** ``` - (IBAction) playSystemSound{ NSURL* system_sound_url = [NSURL fileURLWithPath:[[NSBundle mainBundle] pathForResource:@"BeepGMC500" ofType:@"wav"]]; SystemSoundID system_sound_id; AudioServicesCreateSystemSoundID( (CFURLRef)system_sound_url, &system_sound_id ); // Register the sound completion callback. AudioServicesAddSystemSoundCompletion( system_sound_id, NULL, // uses the main run loop NULL, // uses kCFRunLoopDefaultMode MySoundFinishedPlayingCallback, // the name of our custom callback function NULL // for user data, but we don't need to do that in this case, so we just pass NULL ); // Play the System Sound AudioServicesPlaySystemSound(system_sound_id); } ``` AudioServicesAddSystemSoundCompletion方法5个参数,第一参数SystemSoundID,第二参数是是否使用循环,第三个参数是循环模式,第四个参数是回调函数,就是当播放完成时候回调的方法,第五个参数是为回调函数提供参数。 这里回调的方法是C语言风格的函数:MySoundFinishedPlayingCallback。 **回调函数** ``` void MySoundFinishedPlayingCallback(SystemSoundID sound_id, void* user_data){ AudioServicesDisposeSystemSoundID(sound_id); } ``` **震动方法调用** ``` // Vibrate on action - (IBAction) vibrate{ AudioServicesPlaySystemSound(kSystemSoundID_Vibrate); } ``` ## 3、播放和录制音频 AVFoundation控件可以实现一般音频播放和录制。 AVAudioPlayer音频播放类,用于播放大于5秒钟声音,可以播放本地声音,但是不能播放网络媒体文件。能够播放、 暂停、循环和跳过等操作。 AVAudioRecorder音频录制类。 **实例AVAudioPlayer** [![wps_clip_image-30534](https://images.cnblogs.com/cnblogs_com/syxchina/201210/201210142308488094.png)](http://images.cnblogs.com/cnblogs_com/syxchina/201210/201210142308476698.png) 添加AVFoundation.framework框架 AvplayerViewController.h文件 ``` #import #import @interface AvplayerViewController : UIViewController { AVAudioPlayer * player; } - (IBAction) stopSong: (id) sender; - (IBAction) playSong: (id) sender; @end ``` AvplayerViewController.m ``` #import "AvplayerViewController.h" @implementation AvplayerViewController - (IBAction) playSong: (id) sender { NSError *error = nil; player = [[AVAudioPlayer alloc] initWithContentsOfURL: [NSURL fileURLWithPath:[[NSBundle mainBundle] pathForResource:@"charleston1925_64kb" ofType:@"mp3"]] error:&error]; player.delegate = self; if(error) { NSLog(@"%@",[error description]); [error release]; } [player play]; } - (IBAction) stopSong: (id) sender { [player stop]; } - (void)audioPlayerDidFinishPlaying:(AVAudioPlayer *)player successfully:(BOOL)flag { NSLog(@"播放完成。"); } - (void)audioPlayerDecodeErrorDidOccur:(AVAudioPlayer *)player error:(NSError *)error { NSLog(@"播放错误发生: %@", [error localizedDescription]); } - (void)dealloc { [player release]; [super dealloc]; } @end ``` **AVAudioPlayer委托** AVAudioPlayerDelegate委托对象提供了两个主要方法: audioPlayerDidFinishPlaying:successfully: audioPlayerDecodeErrorDidOccur:error: AVAudioRecorder **新建实例:Recorder** [![wps_clip_image-7998](https://images.cnblogs.com/cnblogs_com/syxchina/201210/201210142308583657.png)](http://images.cnblogs.com/cnblogs_com/syxchina/201210/201210142308574562.png) RecorderViewController.h文件 ``` #import #import @interface RecorderViewController : UIViewController { AVAudioRecorder *recorder; AVAudioPlayer *player; UILabel *label; } @property (retain, nonatomic) AVAudioRecorder * recorder; @property (retain, nonatomic) AVAudioPlayer * player; @property (retain, nonatomic) IBOutlet UILabel *label; -(IBAction)recordPushed:(id)sender; -(IBAction)playPushed:(id)sender; -(IBAction)stopPushed:(id)sender; @end ``` 音频录制方法 ``` -(IBAction)recordPushed:(id)sender { label.text = @"recode..."; if([recorder isRecording]) return; if([player isPlaying]) [player stop]; NSError *error = nil; [[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryRecord error:&error]; [[AVAudioSession sharedInstance] setActive:YES error:&error]; ``` AVAudioSession 是iOS提供音频会话类,音频会话是指定应用程序与音频系统如何交互。AVAudioSession 通过指定一个音频类别(Category)实现的,音频类别(Category)描述了应用程序使用音频的方式。下面是语句是设定音频会话类别: [[AVAudioSession sharedInstance]setCategory:AVAudioSessionCategoryRecord error:&error]; AVAudioSessionCategoryRecord代表只能输入音频,即录制音频了。其效果是停止其它音频播放。 使用类别后,音频会话要设置为“活跃的”Active,这会把后台的任何系统声音关闭。 [[AVAudioSession sharedInstance] setActive:YES error:&error]; 音频录制方法 ``` NSMutableDictionary *settings = [NSMutableDictionary dictionary]; [settings setValue:[NSNumber numberWithInt:kAudioFormatLinearPCM] forKey:AVFormatIDKey]; [settings setValue:[NSNumber numberWithFloat:44100.0] forKey:AVSampleRateKey]; //采样率 [settings setValue:[NSNumber numberWithInt:1] forKey:AVNumberOfChannelsKey];//通道的数目 [settings setValue:[NSNumber numberWithInt:16] forKey:AVLinearPCMBitDepthKey];//采样位数 默认 16 [settings setValue:[NSNumber numberWithBool:NO] forKey:AVLinearPCMIsBigEndianKey];//大端还是小端 是内存的组织方式 [settings setValue:[NSNumber numberWithBool:NO] forKey:AVLinearPCMIsFloatKey];//采样信号是整数还是浮点数 NSString *filePath = [NSString stringWithFormat:@"%@/rec_audio.caf", [self documentsDirectory]]; NSURL *fileUrl = [NSURL fileURLWithPath:filePath]; //[self setRecorder:nil]; recorder = [[AVAudioRecorder alloc] initWithURL:fileUrl settings:settings error:&error]; // [recorder setMeteringEnabled:YES]; [recorder record]; } ``` ``` -(NSString *)documentsDirectory{ NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES); return [paths objectAtIndex:0]; } ``` 音频播放方法 ``` -(IBAction)playPushed:(id)sender{ label.text = @"play..."; if([recorder isRecording]) [recorder stop]; if([player isPlaying]) [player stop]; NSString *filePath = [NSString stringWithFormat:@"%@/rec_audio.caf", [self documentsDirectory]]; NSURL *fileUrl = [NSURL fileURLWithPath:filePath]; NSError *error = nil; // [self setPlayer:nil]; [[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryPlayback error:&error]; [[AVAudioSession sharedInstance] setActive:YES error:&error]; player = [[AVAudioPlayer alloc] initWithContentsOfURL:fileUrl error:&error]; // [player setMeteringEnabled:YES]; [player play]; } ``` 音频停止方法 ``` -(IBAction)stopPushed:(id)sender{ label.text = @"stop..."; if([recorder isRecording]) [recorder stop]; if([player isPlaying]) [player stop]; } ``` > 注: > 1 本教程是基于关东升老师的教程 > 2 基于黑苹果10.6.8和xcode4.2 > 3 本人初学,有什么不对的望指教 > 4 教程会随着本人学习,持续更新 > 5 教程是本人从word笔记中拷贝出来了,所以格式请见谅 原文链接:https://www.cnblogs.com/syxchina/archive/2012/10/14/2723531.html ================================================ FILE: iOS资料/MACiOS利用FFmpeg解析音视频数据流.md ================================================ # MAC/iOS利用FFmpeg解析音视频数据流 ## 1.简易流程 **使用流程** - 初始化解析类: `- (instancetype)initWithPath:(NSString *)path;` - 开始解析: `startParseWithCompletionHandler` - 获取解析后的数据: 从上一步中`startParseWithCompletionHandler`方法中的Block获取解析后的音视频数据. **FFmpeg parse流程** - 创建format context: `avformat_alloc_context` - 打开文件流: `avformat_open_input` - 寻找流信息: `avformat_find_stream_info` - 获取音视频流的索引值: `formatContext->streams[i]->codecpar->codec_type == (isVideoStream ? AVMEDIA_TYPE_VIDEO : AVMEDIA_TYPE_AUDIO)` - 获取音视频流: `m_formatContext->streams[m_audioStreamIndex]` - 解析音视频数据帧: `av_read_frame` - 获取extra data: `av_bitstream_filter_filter` 具体步骤 \1. 将FFmpeg框架导入项目中 下面的链接中包含搭建iOS需要的FFmpeg环境的详细步骤,需要的可以提前阅读. [iOS编译FFmpeg](https://zhuanlan.zhihu.com/p/533700525) 导入FFmpeg框架后,首先需要将用到FFmpeg的文件改名为`.mm`, 因为涉及C,C++混编,所以需要更改文件名 然后在头文件中导入FFmpeg头文件. ```text // FFmpeg Header File #ifdef __cplusplus extern "C" { #endif #include "libavformat/avformat.h" #include "libavcodec/avcodec.h" #include "libavutil/avutil.h" #include "libswscale/swscale.h" #include "libswresample/swresample.h" #include "libavutil/opt.h" #ifdef __cplusplus }; #endif ``` 注意: FFmpeg是一个广为流传的框架,其结构复杂,一般导入都按照如上格式,以文件夹名为根目录进行导入,具体设置,请参考上文链接. ## 2. 初始化 ### **2.1. 注册FFmpeg** - `void av_register_all(void);` 初始化libavformat并注册所有muxers,demuxers与协议。如果不调用此功能,则可以选择一个特定想要支持的格式。 一般在程序中的main函数或是主程序启动的代理方法`- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions`中初始化FFmpeg,执行一次即可. ```text av_register_all(); ``` ### **2.2. 利用视频文件生成格式上下文对象** - `avformat_alloc_context()`: 初始化avformat上下文对象. - `int avformat_open_input(AVFormatContext **ps, const char *url, AVInputFormat *fmt, AVDictionary **options)`函数 - - `fmt`: 如果非空表示强制指定一个输入流的格式, 设置为空会自动选择. - `int avformat_find_stream_info(AVFormatContext *ic, AVDictionary **options);` :读取媒体文件的数据包以获取流信息 ```text - (AVFormatContext *)createFormatContextbyFilePath:(NSString *)filePath { if (filePath == nil) { log4cplus_error(kModuleName, "%s: file path is NULL",__func__); return NULL; } AVFormatContext *formatContext = NULL; AVDictionary *opts = NULL; av_dict_set(&opts, "timeout", "1000000", 0);//设置超时1秒 formatContext = avformat_alloc_context(); BOOL isSuccess = avformat_open_input(&formatContext, [filePath cStringUsingEncoding:NSUTF8StringEncoding], NULL, &opts) < 0 ? NO : YES; av_dict_free(&opts); if (!isSuccess) { if (formatContext) { avformat_free_context(formatContext); } return NULL; } if (avformat_find_stream_info(formatContext, NULL) < 0) { avformat_close_input(&formatContext); return NULL; } return formatContext; } ``` ### **2.3. 获取Audio / Video流的索引值.** 通过遍历format context对象可以从`nb_streams`数组中找到音频或视频流索引,以便后续使用 注意: 后面代码中仅需要知道音频,视频的索引就可以快速读取到format context对象中对应流的信息. ```text - (int)getAVStreamIndexWithFormatContext:(AVFormatContext *)formatContext isVideoStream:(BOOL)isVideoStream { int avStreamIndex = -1; for (int i = 0; i < formatContext->nb_streams; i++) { if ((isVideoStream ? AVMEDIA_TYPE_VIDEO : AVMEDIA_TYPE_AUDIO) == formatContext->streams[i]->codecpar->codec_type) { avStreamIndex = i; } } if (avStreamIndex == -1) { log4cplus_error(kModuleName, "%s: Not find video stream",__func__); return NULL; }else { return avStreamIndex; } } ``` ### **2.4. 是否支持音视频流** 目前视频仅支持H264, H265编码的格式.实际过程中,解码得到视频的旋转角度可能是不同的,以及不同机型可以支持的解码文件格式也是不同的,所以可以用这个方法手动过滤一些不支持的情况.具体请下载代码观看,这里仅列出实战中测试出支持的列表. ```text /* 各机型支持的最高分辨率和FPS组合: iPhone 6S: 60fps -> 720P 30fps -> 4K iPhone 7P: 60fps -> 1080p 30fps -> 4K iPhone 8: 60fps -> 1080p 30fps -> 4K iPhone 8P: 60fps -> 1080p 30fps -> 4K iPhone X: 60fps -> 1080p 30fps -> 4K iPhone XS: 60fps -> 1080p 30fps -> 4K */ ``` 音频本例中仅支持AAC格式.其他格式可根据需求自行更改. ## 3. 开始解析 - 初始化AVPacket以存放解析后的数据 使用AVPacket这个结构体来存储压缩数据.对于视频而言, 它通常包含一个压缩帧,对音频而言,可能包含多个压缩帧,该结构体类型通过`av_malloc()`函数分配内存,通过`av_packet_ref()`函数拷贝,通过`av_packet_unref().`函数释放内存. ```text AVPacket packet; av_init_packet(&packet); ``` 解析数据 `int av_read_frame(AVFormatContext *s, AVPacket *pkt);` : 此函数返回存储在文件中的内容,并且不验证解码器的有效帧是什么。它会将存储在文件中的内容分成帧,并为每次调用返回一个。它不会在有效帧之间省略无效数据,以便为解码器提供解码时可能的最大信息。 ```text int size = av_read_frame(formatContext, &packet); if (size < 0 || packet.size < 0) { handler(YES, YES, NULL, NULL); log4cplus_error(kModuleName, "%s: Parse finish",__func__); break; } ``` 获取sps, pps等NALU Header信息 通过调用`av_bitstream_filter_filter`可以从码流中过滤得到sps, pps等NALU Header信息. av_bitstream_filter_init: 通过给定的比特流过滤器名词创建并初始化一个比特流过滤器上下文. av_bitstream_filter_filter: 此函数通过过滤`buf`参数中的数据,将过滤后的数据放在`poutbuf`参数中.输出的buffer必须被调用者释放. 此函数使用buf_size大小过滤缓冲区buf,并将过滤后的缓冲区放在poutbuf指向的缓冲区中。 ```text attribute_deprecated int av_bitstream_filter_filter ( AVBitStreamFilterContext * bsfc, AVCodecContext * avctx, const char * args, // filter 配置参数 uint8_t ** poutbuf, // 过滤后的数据 int * poutbuf_size, // 过滤后的数据大小 const uint8_t * buf,// 提供给过滤器的原始数据 int buf_size, // 提供给过滤器的原始数据大小 int keyframe // 如果要过滤的buffer对应于关键帧分组数据,则设置为非零 ) ``` 注意: 下面使用`new_packet`是为了解决`av_bitstream_filter_filter`会产生内存泄漏的问题.每次使用完后将用`new_packet`释放即可. ```text if (packet.stream_index == videoStreamIndex) { static char filter_name[32]; if (formatContext->streams[videoStreamIndex]->codecpar->codec_id == AV_CODEC_ID_H264) { strncpy(filter_name, "h264_mp4toannexb", 32); videoInfo.videoFormat = XDXH264EncodeFormat; } else if (formatContext->streams[videoStreamIndex]->codecpar->codec_id == AV_CODEC_ID_HEVC) { strncpy(filter_name, "hevc_mp4toannexb", 32); videoInfo.videoFormat = XDXH265EncodeFormat; } else { break; } AVPacket new_packet = packet; if (self->m_bitFilterContext == NULL) { self->m_bitFilterContext = av_bitstream_filter_init(filter_name); } av_bitstream_filter_filter(self->m_bitFilterContext, formatContext->streams[videoStreamIndex]->codec, NULL, &new_packet.data, &new_packet.size, packet.data, packet.size, 0); } ``` - 根据特定规则生成时间戳 可以根据自己的需求自定义时间戳生成规则.这里使用当前系统时间戳加上数据包中的自带的pts/dts生成了时间戳. ```text CMSampleTimingInfo timingInfo; CMTime presentationTimeStamp = kCMTimeInvalid; presentationTimeStamp = CMTimeMakeWithSeconds(current_timestamp + packet.pts * av_q2d(formatContext->streams[videoStreamIndex]->time_base), fps); timingInfo.presentationTimeStamp = presentationTimeStamp; timingInfo.decodeTimeStamp = CMTimeMakeWithSeconds(current_timestamp + av_rescale_q(packet.dts, formatContext->streams[videoStreamIndex]->time_base, input_base), fps); ``` - 获取parse到的数据 本例将获取到的数据放在自定义的结构体中,然后通过block回调传给方法的调用者,调用者可以在回调函数中处理parse到的视频数据. ```text struct XDXParseVideoDataInfo { uint8_t *data; int dataSize; uint8_t *extraData; int extraDataSize; Float64 pts; Float64 time_base; int videoRotate; int fps; CMSampleTimingInfo timingInfo; XDXVideoEncodeFormat videoFormat; }; ... videoInfo.data = video_data; videoInfo.dataSize = video_size; videoInfo.extraDataSize = formatContext->streams[videoStreamIndex]->codec->extradata_size; videoInfo.extraData = (uint8_t *)malloc(videoInfo.extraDataSize); videoInfo.timingInfo = timingInfo; videoInfo.pts = packet.pts * av_q2d(formatContext->streams[videoStreamIndex]->time_base); videoInfo.fps = fps; memcpy(videoInfo.extraData, formatContext->streams[videoStreamIndex]->codec->extradata, videoInfo.extraDataSize); av_free(new_packet.data); // send videoInfo if (handler) { handler(YES, NO, &videoInfo, NULL); } free(videoInfo.extraData); free(videoInfo.data); ``` 获取parse到的音频数据 ```text struct XDXParseAudioDataInfo { uint8_t *data; int dataSize; int channel; int sampleRate; Float64 pts; }; ... if (packet.stream_index == audioStreamIndex) { XDXParseAudioDataInfo audioInfo = {0}; audioInfo.data = (uint8_t *)malloc(packet.size); memcpy(audioInfo.data, packet.data, packet.size); audioInfo.dataSize = packet.size; audioInfo.channel = formatContext->streams[audioStreamIndex]->codecpar->channels; audioInfo.sampleRate = formatContext->streams[audioStreamIndex]->codecpar->sample_rate; audioInfo.pts = packet.pts * av_q2d(formatContext->streams[audioStreamIndex]->time_base); // send audio info if (handler) { handler(NO, NO, NULL, &audioInfo); } free(audioInfo.data); } ``` - 释放packet 因为我们已经将packet中的关键数据拷贝到自定义的结构体中,所以使用完后需要释放packet. ```text av_packet_unref(&packet); ``` parse完成后释放相关资源 ```text - (void)freeAllResources { if (m_formatContext) { avformat_close_input(&m_formatContext); m_formatContext = NULL; } if (m_bitFilterContext) { av_bitstream_filter_close(m_bitFilterContext); m_bitFilterContext = NULL; } } ``` 注意: 如果使用FFmpeg硬解,则仅仅需要获取到AVPacket数据结构即可.不需要再将数据封装到自定义的结构体中 ## 4. 外部调用 上面操作执行完后,即可通过如下block获取解析后的数据,一般需要继续对音视频进行解码操作.后面文章会讲到,请持续关注. ```text XDXAVParseHandler *parseHandler = [[XDXAVParseHandler alloc] initWithPath:path]; [parseHandler startParseGetAVPackeWithCompletionHandler:^(BOOL isVideoFrame, BOOL isFinish, AVPacket packet) { if (isFinish) { // parse finish ... return; } if (isVideoFrame) { // decode video ... }else { // decode audio ... } }]; ``` 原文https://zhuanlan.zhihu.com/p/533710513 ================================================ FILE: iOS资料/iOS - 图形高级处理 (一、图片显示相关理论).md ================================================ # iOS - 图形高级处理 (一、图片显示相关理论) ## 1、图片从磁盘中读入到显示到屏幕全过程 ### 1.1图片的加载过程: - 使用 +imageWithContentsOfFile: 方法从磁盘中加载一张图片或 -[UIImage imageNamed:@"xx.JPG"]此时图片并没有解码;([两种方式的区别](https://link.juejin.cn?target=http%3A%2F%2Fwww.cocoachina.com%2Farticles%2F26556)) - 初始化完成的UITmage 赋值给 UIImageView; - 接着一个隐式的 CATransaction 捕获到了 UIImageView 图层树的变化; - 在主线程的下一个 runloop 到来时,Core Animation 提交了这个隐式的 transaction ,这个过程可能会对图片进行 copy 操作,而受图片是否字节对齐等因素的影响,这个 copy 操作可能会涉及以下部分或全部步骤: - 分配内存缓冲区用于管理文件 IO 和解压缩操作; - 将文件数据从磁盘读到内存中; - 将压缩的图片数据解码成未压缩的位图形式,这是一个非常耗时的 CPU 操作; - 最后 Core Animation 中CALayer使用未压缩的位图数据渲染 UIImageView 的图层。 - CPU计算好图片的Frame,对图片解压之后.就会交给GPU来做图片渲染。 由上面的步骤可知,图片的解压缩是一个非常耗时的 CPU 操作,并且它默认是在主线程中执行的。那么当需要加载的图片比较多时,就会对我们应用的响应性造成严重的影响,尤其是在快速滑动的列表上,这个问题会表现得更加突出。 ### 1.2渲染图片到屏幕上 - iOS设备给用户视觉反馈其实都是通过QuartzCore框架来进行的,说白了,所有用户最终看到的显示界面都是图层合成的结果,而图层即是QuartzCore中的CALayer。 - 通常我们开发中使用的视图即UIView,他并不是直接显示在屏幕上的,你可以把他想象成一个装有显示层CALayer的容器。我们在在创建视图对象的时候,系统会自动为该视图创建一个CALayer;当然我们也可以自己再往该视图中加入新的CALayer层。等到需要显示的时候,系统硬件将把所有层进行拷贝,然后按Z轴的高低合成最终的合成效果。 ![img](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/5ec12ca4a8084c9785c190fa1f30bcde~tplv-k3u1fbpfcp-zoom-in-crop-mark:4536:0:0:0.awebp) ### 1.3图片渲染大致流程 - 在 VSync 信号到来后,主线程开始在cpu上做计算。 - CPU计算显示内容:视图创建、布局计算、图片解码、文本绘制等。 - GPU进行渲染:CPU将计算好的内容提交给GPU,GPU 进行变换、合成、渲染。 - GPU 会把渲染结果提交到帧缓冲区去,等待下一次 VSync 信号到来时显示到屏幕上。 关于渲染更多知识点,例如离屏渲染等因为篇幅太长不利于学习,这部分放在后面app性能篇继续学习。 ## 2、图形处理相关框架 通过以上图片加载显示的理论学习,我们就需要来继续学习一下图形处理的相关理论,毕竟在开发过程中我们无法,性能上也不允许,所有图片的显示都用UIimage从磁盘或内存中读入。同时一些界面显示也或多或少要使用到图形处理框架。 ![img](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/6be2bf274d9e473cbb1364518d20f6d7~tplv-k3u1fbpfcp-zoom-in-crop-mark:4536:0:0:0.awebp) ### 2.1iOS与图形图像处理相关的框架汇总: - 界面图形框架 -- UIKit - 核心动画框架 -- Core Animation - 苹果封装的图形框架 -- Core Graphics & Quartz 2D - 传统跨平台图形框架 -- OpenGL ES - 苹果最新力推的图形框架 -- Metal - 适合图片的苹果滤镜框架 -- Core Image - `适合视频的第三方滤镜方案 -- GPUImage (第三方不属于系统,这里列出来学习)` - 游戏引擎 -- Scene Kit (3D) 和 Sprite Kit (2D) - 计算机视觉在iOS的应用 -- OpenCV for iOS 毫无疑问,开发者们接触得最多的框架是以下几个,UIKit、Core Animation,Core Graphic, Core Image。下面简要介绍这几个框架,顺便介绍下`GPUImage`: ### 2.2界面图形框架 -- UIKit(穿插使用其他图形处理框架) - UIKit是一组Objective-C API,为线条图形、Quartz图像和颜色操作提供Objective-C 封装,并提供2D绘制、图像处理及用户接口级别的动画。 - UIKit包括UIBezierPath(绘制线、角度、椭圆及其它图形)、UIImage(显示图像)、UIColor(颜色操作)、UIFont和UIScreen(提供字体和屏幕信息)等类以及在位图图形环境、PDF图形环境上进行绘制和 操作的功能等, 也提供对标准视图的支持,也提供对打印功能的支持。 - UIKit与Core Graphics的关系: > 在UIKit中,UIView类本身在绘制时自动创建一个图形环境,即Core Graphics层的CGContext类型,作为当前的图形绘制环境。在绘制时可以调用 UIGraphicsGetCurrentContext 函数获得当前的图形环境; 例如: > ```objectivec > //这段代码就是在UIView的子类中调用 UIGraphicsGetCurrentContext 函数获得当前的图形环境,然后向该图形环境添加路径,最后绘制。 > - (void)drawRect:(CGRect)rect { > //1.获取上下文 > CGContextRef contextRef = UIGraphicsGetCurrentContext(); > //2.描述路径 > UIBezierPath * path = [UIBezierPath bezierPath]; > //起点 > [path moveToPoint:CGPointMake(10, 10)]; > //终点 > [path addLineToPoint:CGPointMake(100, 100)]; > //设置颜色 > [[UIColor whiteColor]setStroke]; > //3.添加路径 > CGContextAddPath(contextRef, path.CGPath); > //显示路径 > CGContextStrokePath(contextRef); > } > 复制代码 > ``` ### 2.3核心动画框架 -- Core Animation - Core Animation 是常用的框架之一。它比 UIKit 和 AppKit 更底层。正如我们所知,UIView底下封装了一层CALayer树,Core Animation 层是真正的渲染层,我们之所以能在屏幕上看到内容,真正的渲染工作是在 Core Animation 层进行的。 - Core Animation 是一套Objective-C API,实现了一个高性能的复合引擎,并提供一个简单易用的编程接口,给用户UI添加平滑运动和动态反馈能力。 - Core Animation 是 UIKit 实现动画和变换的基础,也负责视图的复合功能。使用Core Animation可以实现定制动画和细粒度的动画控制,创建复杂的、支持动画和变换的layered 2D视图 - OpenGL ES的内容也可以与Core Animation内容进行集成。 - 为了使用Core Animation实现动画,可以修改 层的属性值 来触发一个action对象的执行,不同的action对象实现不同的动画。Core Animation 提供了一组基类及子类,提供对不同动画类型的支持: - CAAnimation 是一个抽象公共基类,CAAnimation采用CAMediaTiming 和CAAction协议为动画提供时间(如周期、速度、重复次数等)和action行为(启动、停止等)。 - CAPropertyAnimation 是 CAAnimation的抽象子类,为动画提供一个由一个key路径规定的层属性的支持; - CABasicAnimation 是CAPropertyAnimation的具体子类,为一个层属性提供简单插入能力。 - CAKeyframeAnimation 也是CAPropertyAnimation的具体子类,提供key帧动画支持。 ### 2.4苹果封装的图形框架 -- Core Graphics & Quartz 2D - Core Graphics(使用Quartz 2D引擎) - Core Graphics是一套C-based API, 支持向量图形,线、形状、图案、路径、剃度、位图图像和pdf 内容的绘制 - Core Graphics 也是常用的框架之一。它用于运行时绘制图像。开发者们可以通过 Core Graphics 绘制路径、颜色。当开发者需要在运行时创建图像时,可以使用 Core Graphics 去绘制,运行时实时计算、绘制一系列图像帧来实现动画。与之相对的是运行前创建图像(例如从磁盘中或内存中已经创建好的UIImage图像)。 - Quartz 2D - Quartz 2D是Core Graphics中的2D 绘制呈现引擎。Quartz是资源和设备无关的,提供路径绘制,anti-aliased呈现,剃度填充图案,图像,透明绘制和透明层、遮蔽和阴影、颜色管理,坐标转换,字体、offscreen呈现、pdf文档创建、显示和分析等功能。 - Quartz 2D能够与所有的图形和动画技术(如Core Animation, OpenGL ES, 和 UIKit 等)一起使用。Quartz 2D采用paint模式进行绘制。 - Quartz 2D提供的主要类包括: - CGContext:表示一个图形环境; - CGPath:使用向量图形来创建路径,并能够填充和stroke; - CGImage:用来表示位图; - CGLayer:用来表示一个能够用于重复绘制和offscreen绘制的绘制层; - CGPattern:用来表示Pattern,用于重复绘制; - CGShading和 CGGradient:用于绘制剃度; - CGColor 和 CGColorSpace;用来进行颜色和颜色空间管理; - CGFont, 用于绘制文本; - CGPDFContentStream、CGPDFScanner、CGPDFPage、CGPDFObject,CGPDFStream, CGPDFString等用来进行pdf文件的创建、解析和显示。 ### 2.5适合图片的苹果滤镜框架 -- Core Image - Core Image 与 Core Graphics 恰恰相反,Core Graphics 用于在运行时创建图像,而 Core Image 是用来处理已经创建的图像的。Core Image 框架拥有一系列现成的图像过滤器,能对已存在的图像进行高效的处理。 - Core Image 是 iOS5 新加入到 iOS 平台的一个图像处理框架,提供了强大高效的图像处理功能, 用来对基于像素的图像进行操作与分析, 内置了很多强大的滤镜(Filter) (目前数量超过了180种), 这些Filter 提供了各种各样的效果, 并且还可以通过 滤镜链 将各种效果的 Filter叠加 起来形成强大的自定义效果。 - 一个 滤镜 是一个对象,有很多输入和输出,并执行一些变换。例如,模糊滤镜可能需要输入图像和一个模糊半径来产生适当的模糊后的输出图像。 - 一个 滤镜链 是一个链接在一起的滤镜网络,使得一个滤镜的输出可以是另一个滤镜的输入。以这种方式,可以实现精心制作的效果。 - iOS8 之后更是支持自定义 CIFilter,可以定制满足业务需求的复杂效果。 - Core Image 的优点在于十分高效。大部分情况下,它会在 GPU 中完成工作,但如果 GPU 忙,会使用 CPU 进行处理。如果设备支持 Metal,那么会使用 Metal 处理。这些操作会在底层完成,Apple 的工程师们已经帮助开发者们完成这些操作了。 - 例如他可以根据需求选择 CPU 或者 GPU 来处理。 > ```ini > // 创建基于 CPU 的 CIContext 对象 (默认是基于 GPU,CPU 需要额外设置参数) > context = [CIContext contextWithOptions: [NSDictionary dictionaryWithObject:[NSNumber numberWithBool:YES] forKey:kCIContextUseSoftwareRenderer]]; > // 创建基于 GPU 的 CIContext 对象 > context = [CIContext contextWithOptions: nil]; > // 创建基于 GPU 的 CIContext 对象 > EAGLContext *eaglctx = [[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES2]; > context = [CIContext contextWithEAGLContext:eaglctx]; > 复制代码 > ``` - Core Image 的 API 主要就是三类: - CIImage 保存图像数据的类,可以通过UIImage,图像文件或者像素数据来创建,包括未处理的像素数据。 - CIFilter 表示应用的滤镜,这个框架中对图片属性进行细节处理的类。它对所有的像素进行操作,用一些键-值设置来决定具体操作的程度。 - CIContext 表示上下文,如 Core Graphics 以及 Core Data 中的上下文用于处理绘制渲染以及处理托管对象一样,Core Image 的上下文也是实现对图像处理的具体对象。可以从其中取得图片的信息。 ### 2.5适合视频的第三方滤镜方案 -- GPUImage - GPUImage 是一个基于OpenGL ES 2.0的开源的图像处理库,优势: - 最低支持 iOS 4.0,iOS 5.0 之后就支持自定义滤镜。在低端机型上,GPUImage 有更好的表现。 - GPUImage 在视频处理上有更好的表现。 - GPUImage 的代码已经开源。可以根据自己的业务需求,定制更加复杂的管线操作。可定制程度高。 原文链接:https://juejin.cn/post/6847902216238399496 ================================================ FILE: iOS资料/iOS AVDemo(5):音频解码.md ================================================ # iOS AVDemo(5):音频解码 iOS/Android 客户端开发同学如果想要开始学习音视频开发,最丝滑的方式是对[音视频基础概念知识](https://mp.weixin.qq.com/mp/appmsgalbum?__biz=MjM5MTkxOTQyMQ==&action=getalbum&album_id=2140155659944787969#wechat_redirect)有一定了解后,再借助本地平台的音视频能力上手去实践音视频的`采集 → 编码 → 封装 → 解封装 → 解码 → 渲染`过程,并借助[音视频工具](https://mp.weixin.qq.com/mp/appmsgalbum?__biz=MjM5MTkxOTQyMQ==&action=getalbum&album_id=2216997905264082945#wechat_redirect)来分析和理解对应的音视频数据。 在[音视频工程示例](https://mp.weixin.qq.com/mp/appmsgalbum?__biz=MjM5MTkxOTQyMQ==&action=getalbum&album_id=2273301900659851268#wechat_redirect)这个栏目,我们将通过拆解`采集 → 编码 → 封装 → 解封装 → 解码 → 渲染`流程并实现 Demo 来向大家介绍如何在 iOS/Android 平台上手音视频开发。 这里是第五篇:**iOS 音频解码 Demo**。这个 Demo 里包含以下内容: - 1)实现一个音频解封装模块; - 2)实现一个音频解码模块; - 3)实现对 MP4 文件中音频部分的解封装和解码逻辑,并将解封装、解码后的数据存储为 PCM 文件; - 4)详尽的代码注释,帮你理解代码逻辑和原理。 ## 1、音频解封装模块 在这个 Demo 中,解封装模块 `KFMP4Demuxer` 的实现与 [《iOS 音频解封装 Demo》](https://mp.weixin.qq.com/s?__biz=MjM5MTkxOTQyMQ==&mid=2257484932&idx=1&sn=04fa6fb220574c0a5d417f4b527c0142&scene=21#wechat_redirect) 中一样,这里就不再重复介绍了,其接口如下: ``` KFMP4Demuxer.h #import #import #import "KFDemuxerConfig.h" NS_ASSUME_NONNULL_BEGIN typedef NS_ENUM(NSInteger, KFMP4DemuxerStatus) { KFMP4DemuxerStatusUnknown = 0, KFMP4DemuxerStatusRunning = 1, KFMP4DemuxerStatusFailed = 2, KFMP4DemuxerStatusCompleted = 3, KFMP4DemuxerStatusCancelled = 4, }; @interface KFMP4Demuxer : NSObject + (instancetype)new NS_UNAVAILABLE; - (instancetype)init NS_UNAVAILABLE; - (instancetype)initWithConfig:(KFDemuxerConfig *)config; @property (nonatomic, strong, readonly) KFDemuxerConfig *config; @property (nonatomic, copy) void (^errorCallBack)(NSError *error); @property (nonatomic, assign, readonly) BOOL hasAudioTrack; // 是否包含音频数据。 @property (nonatomic, assign, readonly) BOOL hasVideoTrack; // 是否包含视频数据。 @property (nonatomic, assign, readonly) CGSize videoSize; // 视频大小。 @property (nonatomic, assign, readonly) CMTime duration; // 媒体时长。 @property (nonatomic, assign, readonly) CMVideoCodecType codecType; // 编码类型。 @property (nonatomic, assign, readonly) KFMP4DemuxerStatus demuxerStatus; // 解封装器状态。 @property (nonatomic, assign, readonly) BOOL audioEOF; // 是否音频结束。 @property (nonatomic, assign, readonly) BOOL videoEOF; // 是否视频结束。 @property (nonatomic, assign, readonly) CGAffineTransform preferredTransform; // 图像的变换信息。比如:视频图像旋转。 - (void)startReading:(void (^)(BOOL success, NSError *error))completeHandler; // 开始读取数据解封装。 - (void)cancelReading; // 取消读取。 - (BOOL)hasAudioSampleBuffer; // 是否还有音频数据。 - (CMSampleBufferRef)copyNextAudioSampleBuffer CF_RETURNS_RETAINED; // 拷贝下一份音频采样。 - (BOOL)hasVideoSampleBuffer; // 是否还有视频数据。 - (CMSampleBufferRef)copyNextVideoSampleBuffer CF_RETURNS_RETAINED; // 拷贝下一份视频采样。 @end NS_ASSUME_NONNULL_END ``` ## 2、音频解码模块 接下来,我们来实现一个音频解码模块 `KFAudioDecoder`,在这里输入解封装后的编码数据,输出解码后的数据。 ``` KFAudioDecoder.h #import #import NS_ASSUME_NONNULL_BEGIN @interface KFAudioDecoder : NSObject @property (nonatomic, copy) void (^sampleBufferOutputCallBack)(CMSampleBufferRef sample); // 解码器数据回调。 @property (nonatomic, copy) void (^errorCallBack)(NSError *error); // 解码器错误回调。 - (void)decodeSampleBuffer:(CMSampleBufferRef)sampleBuffer; // 解码。 @end NS_ASSUME_NONNULL_END ``` 上面是 `KFAudioDecoder` 接口的设计,主要是有音频解码`数据回调`和`错误回调`的接口,另外就是`解码`的接口。 在上面的`解码`接口和`解码器数据回调`接口中,我们使用的是依然 **CMSampleBufferRef**[1] 作为参数或返回值类型。 在`解码`接口中,我们通过 `CMSampleBufferRef` 打包的是解封装后得到的 AAC 编码数据。 在`解码器数据回调`接口中,我们通过 `CMSampleBufferRef` 打包的是对 AAC 解码后得到的音频 PCM 数据。 ``` KFAudioDecoder.m #import "KFAudioDecoder.h" #import // 自定义数据,用于封装音频解码回调中用到的数据。 typedef struct KFAudioUserData { UInt32 mChannels; UInt32 mDataSize; void *mData; AudioStreamPacketDescription mPacketDesc; } KFAudioUserData; @interface KFAudioDecoder () { UInt8 *_pcmBuffer; // 解码缓冲区。 } @property (nonatomic, assign) AudioConverterRef audioDecoderInstance; // 音频解码器实例。 @property (nonatomic, assign) CMFormatDescriptionRef pcmFormat; // 音频解码参数。 @property (nonatomic, strong) dispatch_queue_t decoderQueue; @property (nonatomic, assign) BOOL isError; @end @implementation KFAudioDecoder #pragma mark - Lifecycle - (instancetype)init { self = [super init]; if (self) { _decoderQueue = dispatch_queue_create("com.KeyFrameKit.audioDecoder", DISPATCH_QUEUE_SERIAL); } return self; } - (void)dealloc { // 清理解码器。 if (_audioDecoderInstance) { AudioConverterDispose(_audioDecoderInstance); _audioDecoderInstance = nil; } if (_pcmFormat) { CFRelease(_pcmFormat); _pcmFormat = NULL; } // 清理缓冲区。 if (_pcmBuffer) { free(_pcmBuffer); _pcmBuffer = NULL; } } #pragma mark - Public Method - (void)decodeSampleBuffer:(CMSampleBufferRef)sampleBuffer { if (!sampleBuffer || !CMSampleBufferGetDataBuffer(sampleBuffer) || self.isError) { return; } // 异步处理,防止主线程卡顿。 __weak typeof(self) weakSelf = self; CFRetain(sampleBuffer); dispatch_async(_decoderQueue, ^{ [weakSelf _decodeSampleBuffer:sampleBuffer]; CFRelease(sampleBuffer); }); } #pragma mark - Private Method - (void)_setupAudioDecoderInstanceWithInputAudioFormat:(AudioStreamBasicDescription)inputFormat error:(NSError **)error{ if (_audioDecoderInstance != nil) { return; } // 1、设置音频解码器输出参数。其中一些参数与输入的音频数据参数一致。 AudioStreamBasicDescription outputFormat = {0}; outputFormat.mSampleRate = inputFormat.mSampleRate; // 输出采样率与输入一致。 outputFormat.mFormatID = kAudioFormatLinearPCM; // 输出的 PCM 格式。 outputFormat.mFormatFlags = kAudioFormatFlagIsSignedInteger | kAudioFormatFlagsNativeEndian | kAudioFormatFlagIsPacked; outputFormat.mChannelsPerFrame = (UInt32) inputFormat.mChannelsPerFrame; // 输出声道数与输入一致。 outputFormat.mFramesPerPacket = 1; // 每个包的帧数。对于 PCM 这样的非压缩音频数据,设置为 1。 outputFormat.mBitsPerChannel = 16; // 对于 PCM,表示采样位深。 outputFormat.mBytesPerFrame = outputFormat.mChannelsPerFrame * outputFormat.mBitsPerChannel / 8; // 每帧字节数 (byte = bit / 8)。 outputFormat.mBytesPerPacket = outputFormat.mFramesPerPacket * outputFormat.mBytesPerFrame; // 每个包的字节数。 outputFormat.mReserved = 0; // 对齐方式,0 表示 8 字节对齐。 // 2、基于音频输入和输出参数创建音频解码器。 OSStatus status = AudioConverterNew(&inputFormat, &outputFormat, &_audioDecoderInstance); if (status != 0) { *error = [NSError errorWithDomain:NSStringFromClass(self.class) code:status userInfo:nil]; } // 3、创建编码格式信息 _pcmFormat。 OSStatus result = CMAudioFormatDescriptionCreate(kCFAllocatorDefault, &outputFormat, 0, NULL, 0, NULL, NULL, &_pcmFormat); if (result != noErr) { *error = [NSError errorWithDomain:NSStringFromClass(self.class) code:result userInfo:nil]; return; } } - (void)_decodeSampleBuffer:(CMSampleBufferRef)sampleBuffer { // 1、从输入数据中获取音频格式信息。 CMAudioFormatDescriptionRef audioFormatRef = CMSampleBufferGetFormatDescription(sampleBuffer); if (!audioFormatRef) { return; } // 获取音频参数信息,AudioStreamBasicDescription 包含了音频的数据格式、声道数、采样位深、采样率等参数。 AudioStreamBasicDescription audioFormat = *CMAudioFormatDescriptionGetStreamBasicDescription(audioFormatRef); // 2、根据音频参数创建解码器实例。 NSError *error = nil; // 第一次解码时创建解码器。 if (!_audioDecoderInstance) { [self _setupAudioDecoderInstanceWithInputAudioFormat:audioFormat error:&error]; if (error) { [self _callBackError:error]; return; } if (!_audioDecoderInstance) { return; } } // 3、获取输入数据中的 AAC 编码数据。 CMBlockBufferRef blockBuffer = CMSampleBufferGetDataBuffer(sampleBuffer); size_t audioLength; char *dataPointer = NULL; CMBlockBufferGetDataPointer(blockBuffer, 0, NULL, &audioLength, &dataPointer); if (audioLength == 0 || !dataPointer) { return; } // 4、创建解码回调中要用到的自定义数据。 KFAudioUserData userData = {0}; userData.mChannels = (UInt32) audioFormat.mChannelsPerFrame; userData.mDataSize = (UInt32) audioLength; userData.mData = (void *) dataPointer; // 绑定 AAC 编码数据。 userData.mPacketDesc.mDataByteSize = (UInt32) audioLength; userData.mPacketDesc.mStartOffset = 0; userData.mPacketDesc.mVariableFramesInPacket = 0; // 5、创建解码输出数据缓冲区内存空间。 // AAC 编码的每个包有 1024 帧。 UInt32 pcmDataPacketSize = 1024; // 缓冲区长度:pcmDataPacketSize * 2(16 bit 采样深度) * 声道数量。 UInt32 pcmBufferSize = (UInt32) (pcmDataPacketSize * 2 * audioFormat.mChannelsPerFrame); if (!_pcmBuffer) { _pcmBuffer = malloc(pcmBufferSize); } memset(_pcmBuffer, 0, pcmBufferSize); // 6、创建解码器接口对应的解码缓冲区 AudioBufferList,绑定缓冲区的内存空间。 AudioBufferList outAudioBufferList = {0}; outAudioBufferList.mNumberBuffers = 1; outAudioBufferList.mBuffers[0].mNumberChannels = (UInt32) audioFormat.mChannelsPerFrame; outAudioBufferList.mBuffers[0].mDataByteSize = (UInt32) pcmBufferSize; // 设置解码缓冲区大小。 outAudioBufferList.mBuffers[0].mData = _pcmBuffer; // 绑定缓冲区空间。 // 7、输出数据描述。 AudioStreamPacketDescription outputPacketDesc = {0}; // 9、解码。 OSStatus status = AudioConverterFillComplexBuffer(self.audioDecoderInstance, inputDataProcess, &userData, &pcmDataPacketSize, &outAudioBufferList, &outputPacketDesc); if (status != noErr) { [self _callBackError:[NSError errorWithDomain:NSStringFromClass(self.class) code:status userInfo:nil]]; return; } if (outAudioBufferList.mBuffers[0].mDataByteSize > 0) { // 10、获取解码后的 PCM 数据并进行封装。 // 把解码后的 PCM 数据先封装到 CMBlockBuffer 中。 CMBlockBufferRef pcmBlockBuffer; size_t pcmBlockBufferSize = outAudioBufferList.mBuffers[0].mDataByteSize; char *pcmBlockBufferDataPointer = malloc(pcmBlockBufferSize); memcpy(pcmBlockBufferDataPointer, outAudioBufferList.mBuffers[0].mData, pcmBlockBufferSize); OSStatus status = CMBlockBufferCreateWithMemoryBlock(kCFAllocatorDefault, pcmBlockBufferDataPointer, pcmBlockBufferSize, NULL, NULL, 0, pcmBlockBufferSize, 0, &pcmBlockBuffer); if (status != noErr) { return; } // 把 PCM 数据所在的 CMBlockBuffer 封装到 CMSampleBuffer 中。 CMSampleBufferRef pcmSampleBuffer = NULL; CMSampleTimingInfo timingInfo = {CMTimeMake(1, audioFormat.mSampleRate), CMSampleBufferGetPresentationTimeStamp(sampleBuffer), kCMTimeInvalid }; status = CMSampleBufferCreateReady(kCFAllocatorDefault, pcmBlockBuffer, _pcmFormat, pcmDataPacketSize, 1, &timingInfo, 0, NULL, &pcmSampleBuffer); CFRelease(pcmBlockBuffer); // 11、回调解码数据。 if (pcmSampleBuffer) { if (self.sampleBufferOutputCallBack) { self.sampleBufferOutputCallBack(pcmSampleBuffer); } CFRelease(pcmSampleBuffer); } } } - (void)_callBackError:(NSError*)error { self.isError = YES; if (error && self.errorCallBack) { dispatch_async(dispatch_get_main_queue(), ^{ self.errorCallBack(error); }); } } #pragma mark - Decoder CallBack static OSStatus inputDataProcess(AudioConverterRef inConverter, UInt32 *ioNumberDataPackets, AudioBufferList *ioData, AudioStreamPacketDescription **outDataPacketDescription, void *inUserData) { KFAudioUserData *userData = (KFAudioUserData *) inUserData; if (userData->mDataSize <= 0) { ioNumberDataPackets = 0; return -1; } // 设置解码输出数据格式信息。 *outDataPacketDescription = &userData->mPacketDesc; (*outDataPacketDescription)[0].mStartOffset = 0; (*outDataPacketDescription)[0].mDataByteSize = userData->mDataSize; (*outDataPacketDescription)[0].mVariableFramesInPacket = 0; // 将待解码的数据拷贝到解码器的缓冲区的对应位置进行解码。 ioData->mBuffers[0].mData = userData->mData; ioData->mBuffers[0].mDataByteSize = userData->mDataSize; ioData->mBuffers[0].mNumberChannels = userData->mChannels; return noErr; } @end ``` 上面是 `KFAudioDecoder` 的实现,从代码上可以看到主要有这几个部分: - 1)创建音频解码实例。第一次调用 `-decodeSampleBuffer:` → `-_decodeSampleBuffer:` 才会创建音频解码实例。 - - 在 `-_setupAudioDecoderInstanceWithInputAudioFormat:error:` 方法中实现。 - 2)实现音频解码逻辑,并在将数据封装到 `CMSampleBufferRef` 结构中,抛给 KFAudioDecoder 的对外数据回调接口。 - - 在 `-decodeSampleBuffer:` → `-_decodeSampleBuffer:` 中实现解码流程,其中涉及到待解码缓冲区、解码缓冲区的管理,并最终在 `inputDataProcess(...)` 回调中将待解码的数据拷贝到解码器的缓冲区进行解码,并设置对应的解码数据格式。 - 3)捕捉音频解码过程中的错误,抛给 KFAudioDecoder 的对外错误回调接口。 - - 在 `-_decodeSampleBuffer:` 方法中捕捉错误,在 `-_callBackError:` 方法向外回调。 - 4)清理音频解码器实例、解码缓冲区。 - - 在 `-dealloc` 方法中实现。 更具体细节见上述代码及其注释。 ## 3、解封装和解码 MP4 文件中的音频部分存储为 PCM 文件 我们在一个 ViewController 中来实现音频解封装及解码逻辑,并将解码后的数据存储为 PCM 文件。 ``` #import "KFAudioDecoderViewController.h" #import "KFMP4Demuxer.h" #import "KFAudioDecoder.h" @interface KFAudioDecoderViewController () @property (nonatomic, strong) KFDemuxerConfig *demuxerConfig; @property (nonatomic, strong) KFMP4Demuxer *demuxer; @property (nonatomic, strong) KFAudioDecoder *decoder; @property (nonatomic, strong) NSFileHandle *fileHandle; @end @implementation KFAudioDecoderViewController #pragma mark - Property - (KFDemuxerConfig *)demuxerConfig { if (!_demuxerConfig) { _demuxerConfig = [[KFDemuxerConfig alloc] init]; _demuxerConfig.demuxerType = KFMediaAudio; NSString *videoPath = [[NSBundle mainBundle] pathForResource:@"input" ofType:@"mp4"]; _demuxerConfig.asset = [AVAsset assetWithURL:[NSURL fileURLWithPath:videoPath]]; } return _demuxerConfig; } - (KFMP4Demuxer *)demuxer { if (!_demuxer) { _demuxer = [[KFMP4Demuxer alloc] initWithConfig:self.demuxerConfig]; _demuxer.errorCallBack = ^(NSError *error) { NSLog(@"KFMP4Demuxer error:%zi %@", error.code, error.localizedDescription); }; } return _demuxer; } - (KFAudioDecoder *)decoder { if (!_decoder) { __weak typeof(self) weakSelf = self; _decoder = [[KFAudioDecoder alloc] init]; _decoder.errorCallBack = ^(NSError *error) { NSLog(@"KFAudioDecoder error:%zi %@", error.code, error.localizedDescription); }; // 解码数据回调。在这里把解码后的音频 PCM 数据存储为文件。 _decoder.sampleBufferOutputCallBack = ^(CMSampleBufferRef sampleBuffer) { if (sampleBuffer) { CMBlockBufferRef blockBuffer = CMSampleBufferGetDataBuffer(sampleBuffer); size_t totolLength; char *dataPointer = NULL; CMBlockBufferGetDataPointer(blockBuffer, 0, NULL, &totolLength, &dataPointer); if (totolLength == 0 || !dataPointer) { return; } [weakSelf.fileHandle writeData:[NSData dataWithBytes:dataPointer length:totolLength]]; } }; } return _decoder; } - (NSFileHandle *)fileHandle { if (!_fileHandle) { NSString *videoPath = [[NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) lastObject] stringByAppendingPathComponent:@"output.pcm"]; [[NSFileManager defaultManager] removeItemAtPath:videoPath error:nil]; [[NSFileManager defaultManager] createFileAtPath:videoPath contents:nil attributes:nil]; _fileHandle = [NSFileHandle fileHandleForWritingAtPath:videoPath]; } return _fileHandle; } #pragma mark - Lifecycle - (void)viewDidLoad { [super viewDidLoad]; [self setupUI]; // 完成音频解码后,可以将 App Document 文件夹下面的 output.pcm 文件拷贝到电脑上,使用 ffplay 播放: // ffplay -ar 44100 -channels 1 -f s16le -i output.pcm } - (void)dealloc { if (_fileHandle) { [_fileHandle closeFile]; _fileHandle = nil; } } #pragma mark - Setup - (void)setupUI { self.edgesForExtendedLayout = UIRectEdgeAll; self.extendedLayoutIncludesOpaqueBars = YES; self.title = @"Audio Decoder"; self.view.backgroundColor = [UIColor whiteColor]; // Navigation item. UIBarButtonItem *startBarButton = [[UIBarButtonItem alloc] initWithTitle:@"Start" style:UIBarButtonItemStylePlain target:self action:@selector(start)]; self.navigationItem.rightBarButtonItems = @[startBarButton]; } #pragma mark - Action - (void)start { __weak typeof(self) weakSelf = self; NSLog(@"KFMP4Demuxer start"); [self.demuxer startReading:^(BOOL success, NSError * _Nonnull error) { if (success) { // Demuxer 启动成功后,就可以从它里面获取解封装后的数据了。 [weakSelf fetchAndDecodeDemuxedData]; } else { NSLog(@"KFMP4Demuxer error:%zi %@", error.code, error.localizedDescription); } }]; } #pragma mark - Utility - (void)fetchAndDecodeDemuxedData { // 异步地从 Demuxer 获取解封装后的 AAC 编码数据,送给解码器进行解码。 __weak typeof(self) weakSelf = self; dispatch_async(dispatch_get_global_queue(0, 0), ^{ while (weakSelf.demuxer.hasAudioSampleBuffer) { CMSampleBufferRef audioBuffer = [weakSelf.demuxer copyNextAudioSampleBuffer]; if (audioBuffer) { [weakSelf decodeSampleBuffer:audioBuffer]; CFRelease(audioBuffer); } } if (weakSelf.demuxer.demuxerStatus == KFMP4DemuxerStatusCompleted) { NSLog(@"KFMP4Demuxer complete"); } }); } - (void)decodeSampleBuffer:(CMSampleBufferRef)sampleBuffer { // 获取解封装后的 AAC 编码裸数据。 CMBlockBufferRef blockBuffer = CMSampleBufferGetDataBuffer(sampleBuffer); size_t totolLength; char *dataPointer = NULL; CMBlockBufferGetDataPointer(blockBuffer, 0, NULL, &totolLength, &dataPointer); if (totolLength == 0 || !dataPointer) { return; } // 目前 AudioDecoder 的解码接口实现的是单包(packet,1 packet 有 1024 帧)解码。而从 Demuxer 获取的一个 CMSampleBuffer 可能包含多个包,所以这里要拆一下包,再送给解码器。 NSLog(@"SampleNum: %ld", CMSampleBufferGetNumSamples(sampleBuffer)); for (NSInteger index = 0; index < CMSampleBufferGetNumSamples(sampleBuffer); index++) { // 1、获取一个包的数据。 size_t sampleSize = CMSampleBufferGetSampleSize(sampleBuffer, index); CMSampleTimingInfo timingInfo; CMSampleBufferGetSampleTimingInfo(sampleBuffer, index, &timingInfo); char *sampleDataPointer = malloc(sampleSize); memcpy(sampleDataPointer, dataPointer, sampleSize); // 2、将数据封装到 CMBlockBuffer 中。 CMBlockBufferRef packetBlockBuffer; OSStatus status = CMBlockBufferCreateWithMemoryBlock(kCFAllocatorDefault, sampleDataPointer, sampleSize, NULL, NULL, 0, sampleSize, 0, &packetBlockBuffer); if (status == noErr) { // 3、将 CMBlockBuffer 封装到 CMSampleBuffer 中。 CMSampleBufferRef frameSampleBuffer = NULL; const size_t sampleSizeArray[] = {sampleSize}; status = CMSampleBufferCreateReady(kCFAllocatorDefault, packetBlockBuffer, CMSampleBufferGetFormatDescription(sampleBuffer), 1, 1, &timingInfo, 1, sampleSizeArray, &frameSampleBuffer); CFRelease(packetBlockBuffer); // 4、解码这个包的数据。 if (frameSampleBuffer) { [self.decoder decodeSampleBuffer:frameSampleBuffer]; CFRelease(frameSampleBuffer); } } dataPointer += sampleSize; } } @end ``` 上面是 `KFAudioDecoderViewController` 的实现,其中主要包含这几个部分: - 1)通过启动音频解封装来驱动整个解封装和解码流程。 - - 在 `-start` 中实现开始动作。 - 2)在解封装模块 `KFMP4Demuxer` 启动成功后,开始读取解封装数据并启动解码。 - - 在 `-fetchAndDecodeDemuxedData` 方法中实现。 - 3)将解封装后的数据拆包,以包为单位封装为 `CMSampleBuffer` 送给解码器解码。 - - 在 `-decodeSampleBuffer:` 方法中实现。 - 4)在解码模块 `KFAudioDecoder` 的数据回调中获取解码后的 PCM 数据存储为文件。 - - 在 `KFAudioDecoder` 的 `sampleBufferOutputCallBack` 回调中实现。 ## 4、用工具播放 PCM 文件 完成音频解码后,可以将 App Document 文件夹下面的 `output.pcm` 文件拷贝到电脑上,使用 `ffplay` 播放来验证一下音频采集是效果是否符合预期: ``` $ ffplay -ar 44100 -channels 1 -f s16le -i output.pcm ``` 注意这里的参数要对齐在工程中输入视频源的`采样率`、`声道数`、`采样位深`。比如我们的 Demo 中输入视频源的声道数是 1,所以上面的声道数需要设置为 1 才能播放正常的声音。 关于播放 PCM 文件的工具,可以参考[《FFmpeg 工具》第 2 节 ffplay 命令行工具](https://mp.weixin.qq.com/s?__biz=MjM5MTkxOTQyMQ==&mid=2257484831&idx=1&sn=6bab905a5040c46b971bab05f787788b&scene=21#wechat_redirect)和[《可视化音视频分析工具》第 1.1 节 Adobe Audition](https://mp.weixin.qq.com/s?__biz=MjM5MTkxOTQyMQ==&mid=2257484834&idx=1&sn=5dd9768bfc0d01ca1b036be8dd2f5fa1&scene=21#wechat_redirect)。 ## 5、参考资料 [1]CMSampleBufferRef: *https://developer.apple.com/documentation/coremedia/cmsamplebufferref/* 原文链接:https://mp.weixin.qq.com/s/7Db81B9i16cLuq0jS42bmg ================================================ FILE: iOS资料/iOS AVDemo(6):音频渲染.md ================================================ # iOS AVDemo(6):音频渲染 iOS/Android 客户端开发同学如果想要开始学习音视频开发,最丝滑的方式是对[音视频基础概念知识](https://mp.weixin.qq.com/mp/appmsgalbum?__biz=MjM5MTkxOTQyMQ==&action=getalbum&album_id=2140155659944787969#wechat_redirect)有一定了解后,再借助 iOS/Android 平台的音视频能力上手去实践音视频的`采集 → 编码 → 封装 → 解封装 → 解码 → 渲染`过程,并借助[音视频工具](https://mp.weixin.qq.com/mp/appmsgalbum?__biz=MjM5MTkxOTQyMQ==&action=getalbum&album_id=2216997905264082945#wechat_redirect)来分析和理解对应的音视频数据。 在[音视频工程示例](https://mp.weixin.qq.com/mp/appmsgalbum?__biz=MjM5MTkxOTQyMQ==&action=getalbum&album_id=2273301900659851268#wechat_redirect)这个栏目,我们将通过拆解`采集 → 编码 → 封装 → 解封装 → 解码 → 渲染`流程并实现 Demo 来向大家介绍如何在 iOS/Android 平台上手音视频开发。 这里是第六篇:**iOS 音频渲染 Demo**。这个 Demo 里包含以下内容: - 1)实现一个音频解封装模块; - 2)实现一个音频解码模块; - 3)实现一个音频渲染模块; - 4)实现对 MP4 文件中音频部分的解封装和解码逻辑,并将解封装、解码后的数据送给渲染模块播放; - 5)详尽的代码注释,帮你理解代码逻辑和原理。 ## 1、音频解封装模块 在这个 Demo 中,解封装模块 `KFMP4Demuxer` 的实现与 [《iOS 音频解封装 Demo》](https://mp.weixin.qq.com/s?__biz=MjM5MTkxOTQyMQ==&mid=2257484932&idx=1&sn=04fa6fb220574c0a5d417f4b527c0142&scene=21#wechat_redirect) 中一样,这里就不再重复介绍了,其接口如下: ``` KFMP4Demuxer.h #import #import #import "KFDemuxerConfig.h" NS_ASSUME_NONNULL_BEGIN typedef NS_ENUM(NSInteger, KFMP4DemuxerStatus) { KFMP4DemuxerStatusUnknown = 0, KFMP4DemuxerStatusRunning = 1, KFMP4DemuxerStatusFailed = 2, KFMP4DemuxerStatusCompleted = 3, KFMP4DemuxerStatusCancelled = 4, }; @interface KFMP4Demuxer : NSObject + (instancetype)new NS_UNAVAILABLE; - (instancetype)init NS_UNAVAILABLE; - (instancetype)initWithConfig:(KFDemuxerConfig *)config; @property (nonatomic, strong, readonly) KFDemuxerConfig *config; @property (nonatomic, copy) void (^errorCallBack)(NSError *error); @property (nonatomic, assign, readonly) BOOL hasAudioTrack; // 是否包含音频数据。 @property (nonatomic, assign, readonly) BOOL hasVideoTrack; // 是否包含视频数据。 @property (nonatomic, assign, readonly) CGSize videoSize; // 视频大小。 @property (nonatomic, assign, readonly) CMTime duration; // 媒体时长。 @property (nonatomic, assign, readonly) CMVideoCodecType codecType; // 编码类型。 @property (nonatomic, assign, readonly) KFMP4DemuxerStatus demuxerStatus; // 解封装器状态。 @property (nonatomic, assign, readonly) BOOL audioEOF; // 是否音频结束。 @property (nonatomic, assign, readonly) BOOL videoEOF; // 是否视频结束。 @property (nonatomic, assign, readonly) CGAffineTransform preferredTransform; // 图像的变换信息。比如:视频图像旋转。 - (void)startReading:(void (^)(BOOL success, NSError *error))completeHandler; // 开始读取数据解封装。 - (void)cancelReading; // 取消读取。 - (BOOL)hasAudioSampleBuffer; // 是否还有音频数据。 - (CMSampleBufferRef)copyNextAudioSampleBuffer CF_RETURNS_RETAINED; // 拷贝下一份音频采样。 - (BOOL)hasVideoSampleBuffer; // 是否还有视频数据。 - (CMSampleBufferRef)copyNextVideoSampleBuffer CF_RETURNS_RETAINED; // 拷贝下一份视频采样。 @end NS_ASSUME_NONNULL_END ``` ## 2、音频解码模块 同样的,解封装模块 `KFAudioDecoder` 的实现与 [《iOS 音频解码 Demo》](https://mp.weixin.qq.com/s?__biz=MjM5MTkxOTQyMQ==&mid=2257484944&idx=1&sn=63616655888d93557f935bc12088873e&scene=21#wechat_redirect) 中一样,这里就不再重复介绍了,其接口如下: ``` KFAudioDecoder.h #import #import NS_ASSUME_NONNULL_BEGIN @interface KFAudioDecoder : NSObject @property (nonatomic, copy) void (^sampleBufferOutputCallBack)(CMSampleBufferRef sample); // 解码器数据回调。 @property (nonatomic, copy) void (^errorCallBack)(NSError *error); // 解码器错误回调。 - (void)decodeSampleBuffer:(CMSampleBufferRef)sampleBuffer; // 解码。 @end NS_ASSUME_NONNULL_END ``` ## 3、音频渲染模块 接下来,我们来实现一个音频渲染模块 `KFAudioRender`,在这里输入解码后的数据进行渲染播放。 ``` KFAudioRender.h #import #import @class KFAudioRender; NS_ASSUME_NONNULL_BEGIN @interface KFAudioRender : NSObject + (instancetype)new NS_UNAVAILABLE; - (instancetype)init NS_UNAVAILABLE; - (instancetype)initWithChannels:(NSInteger)channels bitDepth:(NSInteger)bitDepth sampleRate:(NSInteger)sampleRate; @property (nonatomic, copy) void (^audioBufferInputCallBack)(AudioBufferList *audioBufferList); // 音频渲染数据输入回调。 @property (nonatomic, copy) void (^errorCallBack)(NSError *error); // 音频渲染错误回调。 @property (nonatomic, assign, readonly) NSInteger audioChannels; // 声道数。 @property (nonatomic, assign, readonly) NSInteger bitDepth; // 采样位深。 @property (nonatomic, assign, readonly) NSInteger audioSampleRate; // 采样率。 - (void)startPlaying; // 开始渲染。 - (void)stopPlaying; // 结束渲染。 @end NS_ASSUME_NONNULL_END ``` 上面是 `KFAudioRender` 接口的设计,除了`初始化`接口,主要是有音频渲染`数据输入回调`和`错误回调`的接口,另外就是`获取声道数`和`获取采样率`的接口,以及`开始渲染`和`结束渲染`的接口。 这里重点需要看一下音频渲染`数据输入回调`接口,系统的音频渲染单元每次会主动通过回调的方式要数据,我们这里封装的 `KFAudioRender` 则是用`数据输入回调`接口来从外部获取一组待渲染的音频数据送给系统的音频渲染单元。 ``` KFAudioRender.m #import "KFAudioRender.h" #define OutputBus 0 @interface KFAudioRender () @property (nonatomic, assign) AudioComponentInstance audioRenderInstance; // 音频渲染实例。 @property (nonatomic, assign, readwrite) NSInteger audioChannels; // 声道数。 @property (nonatomic, assign, readwrite) NSInteger bitDepth; // 采样位深。 @property (nonatomic, assign, readwrite) NSInteger audioSampleRate; // 采样率。 @property (nonatomic, strong) dispatch_queue_t renderQueue; @property (nonatomic, assign) BOOL isError; @end @implementation KFAudioRender #pragma mark - Lifecycle - (instancetype)initWithChannels:(NSInteger)channels bitDepth:(NSInteger)bitDepth sampleRate:(NSInteger)sampleRate { self = [super init]; if (self) { _audioChannels = channels; _bitDepth = bitDepth; _audioSampleRate = sampleRate; _renderQueue = dispatch_queue_create("com.KeyFrameKit.audioRender", DISPATCH_QUEUE_SERIAL); } return self; } - (void)dealloc { // 清理音频渲染实例。 if (_audioRenderInstance) { AudioOutputUnitStop(_audioRenderInstance); AudioUnitUninitialize(_audioRenderInstance); AudioComponentInstanceDispose(_audioRenderInstance); _audioRenderInstance = nil; } } #pragma mark - Action - (void)startPlaying { __weak typeof(self) weakSelf = self; dispatch_async(_renderQueue, ^{ if (!weakSelf.audioRenderInstance) { NSError *error = nil; // 第一次 startPlaying 时创建音频渲染实例。 [weakSelf _setupAudioRenderInstance:&error]; if (error) { // 捕捉并回调创建音频渲染实例时的错误。 [weakSelf _callBackError:error]; return; } } // 开始渲染。 OSStatus status = AudioOutputUnitStart(weakSelf.audioRenderInstance); if (status != noErr) { // 捕捉并回调开始渲染时的错误。 [weakSelf _callBackError:[NSError errorWithDomain:NSStringFromClass([KFAudioRender class]) code:status userInfo:nil]]; } }); } - (void)stopPlaying { __weak typeof(self) weakSelf = self; dispatch_async(_renderQueue, ^{ if (weakSelf.audioRenderInstance && !self.isError) { // 停止渲染。 OSStatus status = AudioOutputUnitStop(weakSelf.audioRenderInstance); // 捕捉并回调停止渲染时的错误。 if (status != noErr) { [weakSelf _callBackError:[NSError errorWithDomain:NSStringFromClass([KFAudioRender class]) code:status userInfo:nil]]; } } }); } #pragma mark - Private Method - (void)_setupAudioRenderInstance:(NSError**)error { // 1、设置音频组件描述。 AudioComponentDescription audioComponentDescription = { .componentType = kAudioUnitType_Output, //.componentSubType = kAudioUnitSubType_VoiceProcessingIO, // 回声消除模式 .componentSubType = kAudioUnitSubType_RemoteIO, .componentManufacturer = kAudioUnitManufacturer_Apple, .componentFlags = 0, .componentFlagsMask = 0 }; // 2、查找符合指定描述的音频组件。 AudioComponent inputComponent = AudioComponentFindNext(NULL, &audioComponentDescription); // 3、创建音频组件实例。 OSStatus status = AudioComponentInstanceNew(inputComponent, &_audioRenderInstance); if (status != noErr) { *error = [NSError errorWithDomain:NSStringFromClass(self.class) code:status userInfo:nil]; return; } // 4、设置实例的属性:可读写。0 不可读写,1 可读写。 UInt32 flag = 1; status = AudioUnitSetProperty(_audioRenderInstance, kAudioOutputUnitProperty_EnableIO, kAudioUnitScope_Output, OutputBus, &flag, sizeof(flag)); if (status != noErr) { *error = [NSError errorWithDomain:NSStringFromClass(self.class) code:status userInfo:nil]; return; } // 5、设置实例的属性:音频参数,如:数据格式、声道数、采样位深、采样率等。 AudioStreamBasicDescription inputFormat = {0}; inputFormat.mFormatID = kAudioFormatLinearPCM; // 原始数据为 PCM,采用声道交错格式。 inputFormat.mFormatFlags = kAudioFormatFlagIsSignedInteger | kAudioFormatFlagsNativeEndian | kAudioFormatFlagIsPacked; inputFormat.mChannelsPerFrame = (UInt32) self.audioChannels; // 每帧的声道数。 inputFormat.mFramesPerPacket = 1; // 每个数据包帧数。 inputFormat.mBitsPerChannel = (UInt32) self.bitDepth; // 采样位深。 inputFormat.mBytesPerFrame = inputFormat.mChannelsPerFrame * inputFormat.mBitsPerChannel / 8; // 每帧字节数 (byte = bit / 8)。 inputFormat.mBytesPerPacket = inputFormat.mFramesPerPacket * inputFormat.mBytesPerFrame; // 每个包字节数。 inputFormat.mSampleRate = self.audioSampleRate; // 采样率 status = AudioUnitSetProperty(_audioRenderInstance, kAudioUnitProperty_StreamFormat, kAudioUnitScope_Input, OutputBus, &inputFormat, sizeof(inputFormat)); if (status != noErr) { *error = [NSError errorWithDomain:NSStringFromClass(self.class) code:status userInfo:nil]; return; } // 6、设置实例的属性:数据回调函数。 AURenderCallbackStruct renderCallbackRef = { .inputProc = audioRenderCallback, .inputProcRefCon = (__bridge void *) (self) // 对应回调函数中的 *inRefCon。 }; status = AudioUnitSetProperty(_audioRenderInstance, kAudioUnitProperty_SetRenderCallback, kAudioUnitScope_Global, OutputBus, &renderCallbackRef, sizeof(renderCallbackRef)); if (status != noErr) { *error = [NSError errorWithDomain:NSStringFromClass(self.class) code:status userInfo:nil]; return; } // 7、初始化实例。 status = AudioUnitInitialize(_audioRenderInstance); if (status != noErr) { *error = [NSError errorWithDomain:NSStringFromClass(self.class) code:status userInfo:nil]; return; } } - (void)_callBackError:(NSError*)error { self.isError = YES; if (self.errorCallBack) { dispatch_async(dispatch_get_main_queue(), ^{ self.errorCallBack(error); }); } } #pragma mark - Render Callback static OSStatus audioRenderCallback(void *inRefCon, AudioUnitRenderActionFlags *ioActionFlags, const AudioTimeStamp *inTimeStamp, UInt32 inOutputBusNumber, UInt32 inNumberFrames, AudioBufferList *ioData) { // 通过音频渲染数据输入回调从外部获取待渲染的数据。 KFAudioRender *audioRender = (__bridge KFAudioRender *) inRefCon; if (audioRender.audioBufferInputCallBack) { audioRender.audioBufferInputCallBack(ioData); } return noErr; } @end ``` 上面是 `KFAudioRender` 的实现,从代码上可以看到主要有这几个部分: - 1)创建音频渲染实例。第一次调用 `-startPlaying` 才会创建音频渲染实例。 - - 在 `-_setupAudioRenderInstance:` 方法中实现。 - 2)处理音频渲染实例的数据回调,并在回调中通过 KFAudioRender 的对外数据输入回调接口向更外层要待渲染的数据。 - - 在 `audioRenderCallback(...)` 方法中实现回调处理逻辑。通过 `audioBufferInputCallBack` 回调接口向更外层要数据。 - 3)实现开始渲染和停止渲染逻辑。 - - 分别在 `-startPlaying` 和 `-stopPlaying` 方法中实现。注意,这里是开始和停止操作都是放在串行队列中通过 `dispatch_async` 异步处理的,这里主要是为了防止主线程卡顿。 - 4)捕捉音频渲染开始和停止操作中的错误,抛给 KFAudioRender 的对外错误回调接口。 - - 在 `-startPlaying` 和 `-stopPlaying` 方法中捕捉错误,在 `-_callBackError:` 方法向外回调。 - 5)清理音频渲染实例。 - - 在 `-dealloc` 方法中实现。 更具体细节见上述代码及其注释。 ## 4、解封装和解码 MP4 文件中的音频部分并渲染播放 我们在一个 ViewController 中来实现从 MP4 文件中解封装和解码音频数据进行渲染播放。 ``` KFAudioRenderViewController.m #import "KFAudioRenderViewController.h" #import #import "KFAudioRender.h" #import "KFMP4Demuxer.h" #import "KFAudioDecoder.h" #import "KFWeakProxy.h" #define KFDecoderMaxCache 4096 * 5 // 解码数据缓冲区最大长度。 @interface KFAudioRenderViewController () @property (nonatomic, strong) KFDemuxerConfig *demuxerConfig; @property (nonatomic, strong) KFMP4Demuxer *demuxer; @property (nonatomic, strong) KFAudioDecoder *decoder; @property (nonatomic, strong) KFAudioRender *audioRender; @property (nonatomic, strong) dispatch_semaphore_t semaphore; @property (nonatomic, strong) NSMutableData *pcmDataCache; // 解码数据缓冲区。 @property (nonatomic, assign) NSInteger pcmDataCacheLength; @property (nonatomic, strong) CADisplayLink *timer; @end @implementation KFAudioRenderViewController #pragma mark - Property - (KFDemuxerConfig *)demuxerConfig { if (!_demuxerConfig) { _demuxerConfig = [[KFDemuxerConfig alloc] init]; _demuxerConfig.demuxerType = KFMediaAudio; NSString *videoPath = [[NSBundle mainBundle] pathForResource:@"input" ofType:@"mp4"]; _demuxerConfig.asset = [AVAsset assetWithURL:[NSURL fileURLWithPath:videoPath]]; } return _demuxerConfig; } - (KFMP4Demuxer *)demuxer { if (!_demuxer) { _demuxer = [[KFMP4Demuxer alloc] initWithConfig:self.demuxerConfig]; _demuxer.errorCallBack = ^(NSError *error) { NSLog(@"KFMP4Demuxer error:%zi %@", error.code, error.localizedDescription); }; } return _demuxer; } - (KFAudioDecoder *)decoder { if (!_decoder) { __weak typeof(self) weakSelf = self; _decoder = [[KFAudioDecoder alloc] init]; _decoder.errorCallBack = ^(NSError *error) { NSLog(@"KFAudioDecoder error:%zi %@", error.code, error.localizedDescription); }; // 解码数据回调。在这里把解码后的音频 PCM 数据缓冲起来等待渲染。 _decoder.sampleBufferOutputCallBack = ^(CMSampleBufferRef sampleBuffer) { if (sampleBuffer) { CMBlockBufferRef blockBuffer = CMSampleBufferGetDataBuffer(sampleBuffer); size_t totolLength; char *dataPointer = NULL; CMBlockBufferGetDataPointer(blockBuffer, 0, NULL, &totolLength, &dataPointer); if (totolLength == 0 || !dataPointer) { return; } dispatch_semaphore_wait(weakSelf.semaphore, DISPATCH_TIME_FOREVER); [weakSelf.pcmDataCache appendData:[NSData dataWithBytes:dataPointer length:totolLength]]; weakSelf.pcmDataCacheLength += totolLength; dispatch_semaphore_signal(weakSelf.semaphore); } }; } return _decoder; } - (KFAudioRender *)audioRender { if (!_audioRender) { __weak typeof(self) weakSelf = self; // 这里设置的音频声道数、采样位深、采样率需要跟输入源的音频参数一致。 _audioRender = [[KFAudioRender alloc] initWithChannels:1 bitDepth:16 sampleRate:44100]; _audioRender.errorCallBack = ^(NSError* error) { NSLog(@"KFAudioRender error:%zi %@", error.code, error.localizedDescription); }; // 渲染输入数据回调。在这里把缓冲区的数据交给系统音频渲染单元渲染。 _audioRender.audioBufferInputCallBack = ^(AudioBufferList * _Nonnull audioBufferList) { if (weakSelf.pcmDataCacheLength < audioBufferList->mBuffers[0].mDataByteSize) { memset(audioBufferList->mBuffers[0].mData, 0, audioBufferList->mBuffers[0].mDataByteSize); } else { dispatch_semaphore_wait(weakSelf.semaphore, DISPATCH_TIME_FOREVER); memcpy(audioBufferList->mBuffers[0].mData, weakSelf.pcmDataCache.bytes, audioBufferList->mBuffers[0].mDataByteSize); [weakSelf.pcmDataCache replaceBytesInRange:NSMakeRange(0, audioBufferList->mBuffers[0].mDataByteSize) withBytes:NULL length:0]; weakSelf.pcmDataCacheLength -= audioBufferList->mBuffers[0].mDataByteSize; dispatch_semaphore_signal(weakSelf.semaphore); } }; } return _audioRender; } #pragma mark - Lifecycle - (void)viewDidLoad { [super viewDidLoad]; _semaphore = dispatch_semaphore_create(1); _pcmDataCache = [[NSMutableData alloc] init]; [self setupAudioSession]; [self setupUI]; // 通过一个 timer 来保证持续从文件中解封装和解码一定量的数据。 _timer = [CADisplayLink displayLinkWithTarget:[KFWeakProxy proxyWithTarget:self] selector:@selector(timerCallBack:)]; [_timer addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes]; [_timer setPaused:NO]; [self.demuxer startReading:^(BOOL success, NSError * _Nonnull error) { NSLog(@"KFMP4Demuxer start:%d", success); }]; } - (void)dealloc { } #pragma mark - Setup - (void)setupUI { self.edgesForExtendedLayout = UIRectEdgeAll; self.extendedLayoutIncludesOpaqueBars = YES; self.title = @"Audio Render"; self.view.backgroundColor = [UIColor whiteColor]; // Navigation item. UIBarButtonItem *startRenderBarButton = [[UIBarButtonItem alloc] initWithTitle:@"Start" style:UIBarButtonItemStylePlain target:self action:@selector(startRender)]; UIBarButtonItem *stopRenderBarButton = [[UIBarButtonItem alloc] initWithTitle:@"Stop" style:UIBarButtonItemStylePlain target:self action:@selector(stopRender)]; self.navigationItem.rightBarButtonItems = @[startRenderBarButton, stopRenderBarButton]; } #pragma mark - Action - (void)startRender { [self.audioRender startPlaying]; } - (void)stopRender { [self.audioRender stopPlaying]; } #pragma mark - Utility - (void)setupAudioSession { // 1、获取音频会话实例。 AVAudioSession *session = [AVAudioSession sharedInstance]; // 2、设置分类。 NSError *error = nil; [[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryPlayback error:&error]; if (error) { NSLog(@"AVAudioSession setCategory error"); } // 3、激活会话。 [session setActive:YES error:&error]; if (error) { NSLog(@"AVAudioSession setActive error"); } } - (void)timerCallBack:(CADisplayLink *)link { // 定时从文件中解封装和解码一定量(不超过 KFDecoderMaxCache)的数据。 if (self.pcmDataCacheLength < KFDecoderMaxCache && self.demuxer.demuxerStatus == KFMP4DemuxerStatusRunning && self.demuxer.hasAudioSampleBuffer) { CMSampleBufferRef audioBuffer = [self.demuxer copyNextAudioSampleBuffer]; if (audioBuffer) { [self decodeSampleBuffer:audioBuffer]; CFRelease(audioBuffer); } } } - (void)decodeSampleBuffer:(CMSampleBufferRef)sampleBuffer { // 获取解封装后的 AAC 编码裸数据。 CMBlockBufferRef blockBuffer = CMSampleBufferGetDataBuffer(sampleBuffer); size_t totolLength; char *dataPointer = NULL; CMBlockBufferGetDataPointer(blockBuffer, 0, NULL, &totolLength, &dataPointer); if (totolLength == 0 || !dataPointer) { return; } // 目前 AudioDecoder 的解码接口实现的是单包(packet,1 packet 有 1024 帧)解码。而从 Demuxer 获取的一个 CMSampleBuffer 可能包含多个包,所以这里要拆一下包,再送给解码器。 NSLog(@"SampleNum: %ld", CMSampleBufferGetNumSamples(sampleBuffer)); for (NSInteger index = 0; index < CMSampleBufferGetNumSamples(sampleBuffer); index++) { // 1、获取一个包的数据。 size_t sampleSize = CMSampleBufferGetSampleSize(sampleBuffer, index); CMSampleTimingInfo timingInfo; CMSampleBufferGetSampleTimingInfo(sampleBuffer, index, &timingInfo); char *sampleDataPointer = malloc(sampleSize); memcpy(sampleDataPointer, dataPointer, sampleSize); // 2、将数据封装到 CMBlockBuffer 中。 CMBlockBufferRef packetBlockBuffer; OSStatus status = CMBlockBufferCreateWithMemoryBlock(kCFAllocatorDefault, sampleDataPointer, sampleSize, NULL, NULL, 0, sampleSize, 0, &packetBlockBuffer); if (status == noErr) { // 3、将 CMBlockBuffer 封装到 CMSampleBuffer 中。 CMSampleBufferRef packetSampleBuffer = NULL; const size_t sampleSizeArray[] = {sampleSize}; status = CMSampleBufferCreateReady(kCFAllocatorDefault, packetBlockBuffer, CMSampleBufferGetFormatDescription(sampleBuffer), 1, 1, &timingInfo, 1, sampleSizeArray, &packetSampleBuffer); CFRelease(packetBlockBuffer); // 4、解码这个包的数据。 if (packetSampleBuffer) { [self.decoder decodeSampleBuffer:packetSampleBuffer]; CFRelease(packetSampleBuffer); } } dataPointer += sampleSize; } } @end ``` 上面是 `KFAudioRenderViewController` 的实现,其中主要包含这几个部分: - 1)在页面加载完成后就启动解封装和解码模块,并通过一个 timer 来驱动解封装器和解码器。 - - 在 `-viewDidLoad` 中实现。 - 2)定时从文件中解封装一定量(不超过 KFDecoderMaxCache)的数据送给解码器。 - - 在 `-timerCallBack:` 方法中实现。 - 3)解封装后,需要将数据拆包,以包为单位封装为 `CMSampleBuffer` 送给解码器解码。 - - 在 `-decodeSampleBuffer:` 方法中实现。 - 4)在解码模块 `KFAudioDecoder` 的数据回调中获取解码后的 PCM 数据缓冲起来等待渲染。 - - 在 `KFAudioDecoder` 的 `sampleBufferOutputCallBack` 回调中实现。 - 5)在渲染模块 `KFAudioRender` 的输入数据回调中把缓冲区的数据交给系统音频渲染单元渲染。 - - 在 `KFAudioRender` 的 `audioBufferInputCallBack` 回调中实现。 更具体细节见上述代码及其注释。 原文链接:https://mp.weixin.qq.com/s/xrt277Ia1OFP_XtwK1qlQg ================================================ FILE: iOS资料/iOS AVDemo(7):视频采集.md ================================================ # iOS AVDemo(7):视频采集 iOS/Android 客户端开发同学如果想要开始学习音视频开发,最丝滑的方式是对[音视频基础概念知识](https://mp.weixin.qq.com/mp/appmsgalbum?__biz=MjM5MTkxOTQyMQ==&action=getalbum&album_id=2140155659944787969#wechat_redirect)有一定了解后,再借助 iOS/Android 平台的音视频能力上手去实践音视频的`采集 → 编码 → 封装 → 解封装 → 解码 → 渲染`过程,并借助[音视频工具](https://mp.weixin.qq.com/mp/appmsgalbum?__biz=MjM5MTkxOTQyMQ==&action=getalbum&album_id=2216997905264082945#wechat_redirect)来分析和理解对应的音视频数据。 在[音视频工程示例](https://mp.weixin.qq.com/mp/appmsgalbum?__biz=MjM5MTkxOTQyMQ==&action=getalbum&album_id=2273301900659851268#wechat_redirect)这个栏目,我们将通过拆解`采集 → 编码 → 封装 → 解封装 → 解码 → 渲染`流程并实现 Demo 来向大家介绍如何在 iOS/Android 平台上手音视频开发。 这里是第七篇:**iOS 视频采集 Demo**。这个 Demo 里包含以下内容: - 1)实现一个视频采集模块; - 2)实现视频采集逻辑并将采集的视频图像渲染进行预览,同时支持将数据转换为图片存储到相册; - 3)详尽的代码注释,帮你理解代码逻辑和原理。 ## 1、视频采集模块 首先,实现一个 `KFVideoCaptureConfig` 类用于定义视频采集参数的配置。 ``` KFVideoCaptureConfig.h #import #import NS_ASSUME_NONNULL_BEGIN typedef NS_ENUM(NSInteger, KFVideoCaptureMirrorType) { KFVideoCaptureMirrorNone = 0, KFVideoCaptureMirrorFront = 1 << 0, KFVideoCaptureMirrorBack = 1 << 1, KFVideoCaptureMirrorAll = (KFVideoCaptureMirrorFront | KFVideoCaptureMirrorBack), }; @interface KFVideoCaptureConfig : NSObject @property (nonatomic, copy) AVCaptureSessionPreset preset; // 视频采集参数,比如分辨率等,与画质相关。 @property (nonatomic, assign) AVCaptureDevicePosition position; // 摄像头位置,前置/后置摄像头。 @property (nonatomic, assign) AVCaptureVideoOrientation orientation; // 视频画面方向。 @property (nonatomic, assign) NSInteger fps; // 视频帧率。 @property (nonatomic, assign) OSType pixelFormatType; // 颜色空间格式。 @property (nonatomic, assign) KFVideoCaptureMirrorType mirrorType; // 镜像类型。 @end NS_ASSUME_NONNULL_END ``` 这里的参数包括了:分辨率、摄像头位置、画面方向、帧率、颜色空间格式、镜像类型这几个参数。 其中`画面方向`是指采集的视频画面是可以带方向的,包括:`Portrait`、`PortraitUpsideDown`、`LandscapeRight`、`LandscapeLeft` 这几种。 `颜色空间格式`对应 RGB、YCbCr 这些概念,具体来讲,一般我们采集图像用于后续的编码时,这里设置 `kCVPixelFormatType_420YpCbCr8BiPlanarFullRange` 即可;如果想支持 HDR 时(iPhone12 及之后设备才支持),这里设置 `kCVPixelFormatType_420YpCbCr10BiPlanarVideoRange`。在我们这个 Demo 中,我们想要将采集的图像数据直接转换并存储为图片,所以我们会设置采集的颜色空间格式为 `kCVPixelFormatType_32BGRA`,这样将更方便将 CMSampleBuffer 转换为 UIImage。后面你会看到这个逻辑。 `镜像类型`表示采集的画面是否左右镜像,这个在直播时,主播经常需要考虑是否对自己的画面进行镜像,从而决定主播和观众的所见画面是否在『左右』概念的理解上保持一致。 其他的几个参数大家应该从字面上就能理解,就不做过多解释了。 ``` KFVideoCaptureConfig.m #import "KFVideoCaptureConfig.h" @implementation KFVideoCaptureConfig - (instancetype)init { self = [super init]; if (self) { _preset = AVCaptureSessionPreset1920x1080; _position = AVCaptureDevicePositionFront; _orientation = AVCaptureVideoOrientationPortrait; _fps = 30; _mirrorType = KFVideoCaptureMirrorFront; // 设置颜色空间格式,这里要注意了: // 1、一般我们采集图像用于后续的编码时,这里设置 kCVPixelFormatType_420YpCbCr8BiPlanarFullRange 即可。 // 2、如果想支持 HDR 时(iPhone12 及之后设备才支持),这里设置为:kCVPixelFormatType_420YpCbCr10BiPlanarVideoRange。 _pixelFormatType = kCVPixelFormatType_420YpCbCr8BiPlanarFullRange; } return self; } @end ``` 上面我们在 `KFVideoCaptureConfig` 的初始化方法里提供了一些默认值。 接下来,我们实现一个 `KFVideoCapture` 类来实现视频采集。 ``` KFVideoCapture.h #import #import "KFVideoCaptureConfig.h" NS_ASSUME_NONNULL_BEGIN @interface KFVideoCapture : NSObject + (instancetype)new NS_UNAVAILABLE; - (instancetype)init NS_UNAVAILABLE; - (instancetype)initWithConfig:(KFVideoCaptureConfig *)config; @property (nonatomic, strong, readonly) KFVideoCaptureConfig *config; @property (nonatomic, strong, readonly) AVCaptureVideoPreviewLayer *previewLayer; // 视频预览渲染 layer。 @property (nonatomic, copy) void (^sampleBufferOutputCallBack)(CMSampleBufferRef sample); // 视频采集数据回调。 @property (nonatomic, copy) void (^sessionErrorCallBack)(NSError *error); // 视频采集会话错误回调。 @property (nonatomic, copy) void (^sessionInitSuccessCallBack)(void); // 视频采集会话初始化成功回调。 - (void)startRunning; // 开始采集。 - (void)stopRunning; // 停止采集。 - (void)changeDevicePosition:(AVCaptureDevicePosition)position; // 切换摄像头。 @end NS_ASSUME_NONNULL_END ``` 上面是 `KFVideoCapture` 的接口设计,可以看到这些接口类似音频采集器的接口设计,除了`初始化方法`,主要是有`获取视频配置`以及视频采集`数据回调`和`错误回调`的接口,另外就是`开始采集`和`停止采集`的接口。 有一些不同的是,这里还提供了`初始化成功回调`、`视频预览渲染 Layer`、以及`切换摄像头`的接口,这个主要是因为视频采集一般会实现所见即所得,能让用户看到实时采集的画面,这样就需要在初始化成功后让业务层感知到来做一些 UI 布局,并通过预览渲染的 Layer 来展示采集的画面。`切换摄像头`的接口则主要是对应了手机设备常见的前置、后置等多摄像头的能力。 在上面的音频采集`数据回调`接口中,我们依然使用了 **CMSampleBufferRef**[1],可见这个数据结构的通用性和重要性。 ``` KFVideoCapture.m #import "KFVideoCapture.h" #import @interface KFVideoCapture () @property (nonatomic, strong, readwrite) KFVideoCaptureConfig *config; @property (nonatomic, strong, readonly) AVCaptureDevice *captureDevice; // 视频采集设备。 @property (nonatomic, strong) AVCaptureDeviceInput *backDeviceInput; // 后置摄像头采集输入。 @property (nonatomic, strong) AVCaptureDeviceInput *frontDeviceInput; // 前置摄像头采集输入。 @property (nonatomic, strong) AVCaptureVideoDataOutput *videoOutput; // 视频采集输出。 @property (nonatomic, strong) AVCaptureSession *captureSession; // 视频采集会话。 @property (nonatomic, strong, readwrite) AVCaptureVideoPreviewLayer *previewLayer; // 视频预览渲染 layer。 @property (nonatomic, assign, readonly) CMVideoDimensions sessionPresetSize; // 视频采集分辨率。 @property (nonatomic, strong) dispatch_queue_t captureQueue; @end @implementation KFVideoCapture #pragma mark - Property - (AVCaptureDevice *)backCamera { return [self cameraWithPosition:AVCaptureDevicePositionBack]; } - (AVCaptureDeviceInput *)backDeviceInput { if (!_backDeviceInput) { _backDeviceInput = [[AVCaptureDeviceInput alloc] initWithDevice:[self backCamera] error:nil]; } return _backDeviceInput; } - (AVCaptureDevice *)frontCamera { return [self cameraWithPosition:AVCaptureDevicePositionFront]; } - (AVCaptureDeviceInput *)frontDeviceInput { if (!_frontDeviceInput) { _frontDeviceInput = [[AVCaptureDeviceInput alloc] initWithDevice:[self frontCamera] error:nil]; } return _frontDeviceInput; } - (AVCaptureVideoDataOutput *)videoOutput { if (!_videoOutput) { _videoOutput = [[AVCaptureVideoDataOutput alloc] init]; [_videoOutput setSampleBufferDelegate:self queue:self.captureQueue]; // 设置返回采集数据的代理和回调。 _videoOutput.videoSettings = @{(id)kCVPixelBufferPixelFormatTypeKey: @(_config.pixelFormatType)}; _videoOutput.alwaysDiscardsLateVideoFrames = YES; // YES 表示:采集的下一帧到来前,如果有还未处理完的帧,丢掉。 } return _videoOutput; } - (AVCaptureSession *)captureSession { if (!_captureSession) { AVCaptureDeviceInput *deviceInput = self.config.position == AVCaptureDevicePositionBack ? self.backDeviceInput : self.frontDeviceInput; if (!deviceInput) { return nil; } // 1、初始化采集会话。 _captureSession = [[AVCaptureSession alloc] init]; // 2、添加采集输入。 for (AVCaptureSessionPreset selectPreset in [self sessionPresetList]) { if ([_captureSession canSetSessionPreset:selectPreset]) { [_captureSession setSessionPreset:selectPreset]; if ([_captureSession canAddInput:deviceInput]) { [_captureSession addInput:deviceInput]; break; } } } // 3、添加采集输出。 if ([_captureSession canAddOutput:self.videoOutput]) { [_captureSession addOutput:self.videoOutput]; } // 4、更新画面方向。 [self _updateOrientation]; // 5、更新画面镜像。 [self _updateMirror]; // 6、更新采集实时帧率。 [self.captureDevice lockForConfiguration:nil]; [self _updateActiveFrameDuration]; [self.captureDevice unlockForConfiguration]; // 7、回报成功。 if (self.sessionInitSuccessCallBack) { self.sessionInitSuccessCallBack(); } } return _captureSession; } - (AVCaptureVideoPreviewLayer *)previewLayer { if (!_captureSession) { return nil; } if (!_previewLayer) { // 初始化预览渲染 layer。这里就直接用系统提供的 API 来渲染。 _previewLayer = [[AVCaptureVideoPreviewLayer alloc] initWithSession:_captureSession]; [_previewLayer setVideoGravity:AVLayerVideoGravityResizeAspectFill]; } return _previewLayer; } - (AVCaptureDevice *)captureDevice { // 视频采集设备。 return (self.config.position == AVCaptureDevicePositionBack) ? [self backCamera] : [self frontCamera]; } - (CMVideoDimensions)sessionPresetSize { // 视频采集分辨率。 return CMVideoFormatDescriptionGetDimensions([self captureDevice].activeFormat.formatDescription); } #pragma mark - LifeCycle - (instancetype)initWithConfig:(KFVideoCaptureConfig *)config { self = [super init]; if (self) { _config = config; _captureQueue = dispatch_queue_create("com.KeyFrameKit.videoCapture", DISPATCH_QUEUE_SERIAL); [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(sessionRuntimeError:) name:AVCaptureSessionRuntimeErrorNotification object:nil]; } return self; } - (void)dealloc { [[NSNotificationCenter defaultCenter] removeObserver:self]; } #pragma mark - Public Method - (void)startRunning { typeof(self) __weak weakSelf = self; dispatch_async(_captureQueue, ^{ [weakSelf _startRunning]; }); } - (void)stopRunning { typeof(self) __weak weakSelf = self; dispatch_async(_captureQueue, ^{ [weakSelf _stopRunning]; }); } - (void)changeDevicePosition:(AVCaptureDevicePosition)position { typeof(self) __weak weakSelf = self; dispatch_async(_captureQueue, ^{ [weakSelf _updateDeveicePosition:position]; }); } #pragma mark - Private Method - (void)_startRunning { AVAuthorizationStatus status = [AVCaptureDevice authorizationStatusForMediaType:AVMediaTypeVideo]; if (status == AVAuthorizationStatusAuthorized) { if (!self.captureSession.isRunning) { [self.captureSession startRunning]; } } else { NSLog(@"没有相机使用权限"); } } - (void)_stopRunning { if (_captureSession && _captureSession.isRunning) { [_captureSession stopRunning]; } } - (void)_updateDeveicePosition:(AVCaptureDevicePosition)position { // 切换采集的摄像头。 if (position == self.config.position || !_captureSession.isRunning) { return; } // 1、切换采集输入。 AVCaptureDeviceInput *curInput = self.config.position == AVCaptureDevicePositionBack ? self.backDeviceInput : self.frontDeviceInput; AVCaptureDeviceInput *addInput = self.config.position == AVCaptureDevicePositionBack ? self.frontDeviceInput : self.backDeviceInput; if (!curInput || !addInput) { return; } [self.captureSession removeInput:curInput]; for (AVCaptureSessionPreset selectPreset in [self sessionPresetList]) { if ([_captureSession canSetSessionPreset:selectPreset]) { [_captureSession setSessionPreset:selectPreset]; if ([_captureSession canAddInput:addInput]) { [_captureSession addInput:addInput]; self.config.position = position; break; } } } // 2、更新画面方向。 [self _updateOrientation]; // 3、更新画面镜像。 [self _updateMirror]; // 4、更新采集实时帧率。 [self.captureDevice lockForConfiguration:nil]; [self _updateActiveFrameDuration]; [self.captureDevice unlockForConfiguration]; } - (void)_updateOrientation { // 更新画面方向。 AVCaptureConnection *connection = [self.videoOutput connectionWithMediaType:AVMediaTypeVideo]; // AVCaptureConnection 用于把输入和输出连接起来。 if ([connection isVideoOrientationSupported] && connection.videoOrientation != self.config.orientation) { connection.videoOrientation = self.config.orientation; } } - (void)_updateMirror { // 更新画面镜像。 AVCaptureConnection *connection = [self.videoOutput connectionWithMediaType:AVMediaTypeVideo]; if ([connection isVideoMirroringSupported]) { if ((self.config.mirrorType & KFVideoCaptureMirrorFront) && self.config.position == AVCaptureDevicePositionFront) { connection.videoMirrored = YES; } else if ((self.config.mirrorType & KFVideoCaptureMirrorBack) && self.config.position == AVCaptureDevicePositionBack) { connection.videoMirrored = YES; } else { connection.videoMirrored = NO; } } } - (BOOL)_updateActiveFrameDuration { // 更新采集实时帧率。 // 1、帧率换算成帧间隔时长。 CMTime frameDuration = CMTimeMake(1, (int32_t) self.config.fps); // 2、设置帧率大于 30 时,找到满足该帧率及其他参数,并且当前设备支持的 AVCaptureDeviceFormat。 if (self.config.fps > 30) { for (AVCaptureDeviceFormat *vFormat in [self.captureDevice formats]) { CMFormatDescriptionRef description = vFormat.formatDescription; CMVideoDimensions dims = CMVideoFormatDescriptionGetDimensions(description); float maxRate = ((AVFrameRateRange *) [vFormat.videoSupportedFrameRateRanges objectAtIndex:0]).maxFrameRate; if (maxRate >= self.config.fps && CMFormatDescriptionGetMediaSubType(description) == self.config.pixelFormatType && self.sessionPresetSize.width * self.sessionPresetSize.height == dims.width * dims.height) { self.captureDevice.activeFormat = vFormat; break; } } } // 3、检查设置的帧率是否在当前设备的 activeFormat 支持的最低和最高帧率之间。如果是,就设置帧率。 __block BOOL support = NO; [self.captureDevice.activeFormat.videoSupportedFrameRateRanges enumerateObjectsUsingBlock:^(AVFrameRateRange * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) { if (CMTimeCompare(frameDuration, obj.minFrameDuration) >= 0 && CMTimeCompare(frameDuration, obj.maxFrameDuration) <= 0) { support = YES; *stop = YES; } }]; if (support) { [self.captureDevice setActiveVideoMinFrameDuration:frameDuration]; [self.captureDevice setActiveVideoMaxFrameDuration:frameDuration]; return YES; } return NO; } #pragma mark - NSNotification - (void)sessionRuntimeError:(NSNotification *)notification { if (self.sessionErrorCallBack) { self.sessionErrorCallBack(notification.userInfo[AVCaptureSessionErrorKey]); } } #pragma mark - Utility - (AVCaptureDevice *)cameraWithPosition:(AVCaptureDevicePosition)position { // 从当前手机寻找符合需要的采集设备。 NSArray *devices = nil; NSString *version = [UIDevice currentDevice].systemVersion; if (version.doubleValue >= 10.0) { AVCaptureDeviceDiscoverySession *deviceDiscoverySession = [AVCaptureDeviceDiscoverySession discoverySessionWithDeviceTypes:@[AVCaptureDeviceTypeBuiltInWideAngleCamera] mediaType:AVMediaTypeVideo position:position]; devices = deviceDiscoverySession.devices; } else { #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wdeprecated-declarations" devices = [AVCaptureDevice devicesWithMediaType:AVMediaTypeVideo]; #pragma GCC diagnostic pop } for (AVCaptureDevice *device in devices) { if ([device position] == position) { return device; } } return nil; } - (NSArray *)sessionPresetList { return @[self.config.preset, AVCaptureSessionPreset3840x2160, AVCaptureSessionPreset1920x1080, AVCaptureSessionPreset1280x720, AVCaptureSessionPresetLow]; } #pragma mark - AVCaptureVideoDataOutputSampleBufferDelegate - (void)captureOutput:(AVCaptureOutput *)output didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer fromConnection:(AVCaptureConnection *)connection { // 向外回调数据。 if (output == self.videoOutput) { if (self.sampleBufferOutputCallBack) { self.sampleBufferOutputCallBack(sampleBuffer); } } } @end ``` 上面是 `KFVideoCapture` 的实现,结合下面这两张图可以让我们更好地理解这些代码: ![图片](https://mmbiz.qpic.cn/mmbiz_png/gUnqKPeSueia18FN4ruiaBg7SCSGYib5COvp4hpQicLHFyc6g5skO9SqLuSsf9PB5hWpgzWQzna9adUm4ibUv7DcKnA/640?wx_fmt=png&wxfrom=5&wx_lazy=1&wx_co=1) ![图片](data:image/svg+xml,<%3Fxml version='1.0' encoding='UTF-8'%3F>)AVCaptureSession 配置多组输入输出 ![图片](https://mmbiz.qpic.cn/mmbiz_png/gUnqKPeSueia18FN4ruiaBg7SCSGYib5COv5wqXuYibj9TdBibmW6JQgczYsAR4KzNaiaGa2fOv1sGFyq4jt98na8F7g/640?wx_fmt=png&wxfrom=5&wx_lazy=1&wx_co=1)AVCaptureConnection 连接单或多输入到单输出 可以看到在实现采集时,我们是用 `AVCaptureSession` 来串联采集设备作为输入,其他输出对象作为输出。我们这个 Demo 里的一个输出对象就是 `AVCaptureVideoPreviewLayer`,用它来接收输出的数据并渲染。此外,还可以使用 `AVCaptureConnection` 来连接一个或多个输入到一个输出。 从代码上可以看到主要有这几个部分: - 1)创建采集设备 `AVCaptureDevice`。 - - 在 `-captureDevice` 中实现。 - 由于我们这里的采集模块支持前置和后置摄像头,所以这里的采集设备是根据当前选择的摄像头位置动态指定的。分别对应 `-backCamera` 和 `-frontCamera`。 - 2)基于采集设备,创建对应的采集输入 `AVCaptureDeviceInput`。 - - 由于支持前置和后置摄像头切换,所以这里我们有两个采集输入对象,分别绑定前置和后置摄像头。对应实现在 `-backDeviceInput` 和 `-frontDeviceInput`。 - 3)创建采集视频数据输出 `AVCaptureVideoDataOutput`。 - - 在 `-videoOutput` 中实现。 - 4)创建采集会话 `AVCaptureSession`,绑定上面创建的采集输入和视频数据输出。 - - 在 `-captureSession` 中实现。 - 5)创建采集画面预览渲染层 `AVCaptureVideoPreviewLayer`,将它绑定到上面创建的采集会话上。 - - 在 `-previewLayer` 中实现。 - 该 layer 可以被外层获取用于 UI 布局和展示。 - 6)基于采集会话的能力封装开始采集和停止采集的对外接口。 - - 分别在 `-startRunning` 和 `-stopRunning` 方法中实现。注意,这里是开始和停止操作都是放在串行队列中通过 `dispatch_async` 异步处理的,这里主要是为了防止主线程卡顿。 - 7)实现切换摄像头的功能。 - - 在 `-changeDevicePosition:` → `-_updateDeveicePosition:` 方法中实现。注意,这里同样是异步处理。 - 8)实现采集初始化成功回调、数据回调、采集会话错误回调等对外接口。 - - 采集初始化成功回调:在 `-captureSession` 中初始化采集会话成功后,向外层回调。 - 数据回调:在 `AVCaptureVideoDataOutputSampleBufferDelegate` 的回调接口 `-captureOutput:didOutputSampleBuffer:fromConnection:` 中接收采集数据并回调给外层。 - 采集会话错误回调:在 `-sessionRuntimeError:` 中监听 `AVCaptureSessionRuntimeErrorNotification` 通知并向外层回调错误。 更具体细节见上述代码及其注释。 ## 2、采集视频并实时展示或截图 我们在一个 ViewController 中来实现视频采集并实时预览的逻辑,也提供了对采集的视频数据截图保存到相册的功能。 ``` KFVideoCaptureViewController.m #import "KFVideoCaptureViewController.h" #import "KFVideoCapture.h" #import @interface KFVideoCaptureViewController () @property (nonatomic, strong) KFVideoCaptureConfig *videoCaptureConfig; @property (nonatomic, strong) KFVideoCapture *videoCapture; @property (nonatomic, assign) int shotCount; @end @implementation KFVideoCaptureViewController #pragma mark - Property - (KFVideoCaptureConfig *)videoCaptureConfig { if (!_videoCaptureConfig) { _videoCaptureConfig = [[KFVideoCaptureConfig alloc] init]; // 由于我们的想要从采集的图像数据里直接转换并存储图片,所以我们这里设置采集处理的颜色空间格式为 32bit BGRA,这样方便将 CMSampleBuffer 转换为 UIImage。 _videoCaptureConfig.pixelFormatType = kCVPixelFormatType_32BGRA; } return _videoCaptureConfig; } - (KFVideoCapture *)videoCapture { if (!_videoCapture) { _videoCapture = [[KFVideoCapture alloc] initWithConfig:self.videoCaptureConfig]; __weak typeof(self) weakSelf = self; _videoCapture.sessionInitSuccessCallBack = ^() { dispatch_async(dispatch_get_main_queue(), ^{ [weakSelf.view.layer addSublayer:weakSelf.videoCapture.previewLayer]; weakSelf.videoCapture.previewLayer.frame = weakSelf.view.bounds; }); }; _videoCapture.sampleBufferOutputCallBack = ^(CMSampleBufferRef sample) { if (weakSelf.shotCount > 0) { weakSelf.shotCount--; [weakSelf saveSampleBuffer:sample]; } }; _videoCapture.sessionErrorCallBack = ^(NSError* error) { NSLog(@"KFVideoCapture Error:%zi %@", error.code, error.localizedDescription); }; } return _videoCapture; } #pragma mark - Lifecycle - (void)viewDidLoad { [super viewDidLoad]; self.edgesForExtendedLayout = UIRectEdgeAll; self.extendedLayoutIncludesOpaqueBars = YES; self.title = @"Video Capture"; self.view.backgroundColor = [UIColor whiteColor]; self.shotCount = 0; [self requestAccessForVideo]; // Navigation item. UIBarButtonItem *cameraBarButton = [[UIBarButtonItem alloc] initWithTitle:@"切换" style:UIBarButtonItemStylePlain target:self action:@selector(changeCamera)]; UIBarButtonItem *shotBarButton = [[UIBarButtonItem alloc] initWithTitle:@"截图" style:UIBarButtonItemStylePlain target:self action:@selector(shot)]; self.navigationItem.rightBarButtonItems = @[cameraBarButton, shotBarButton]; } - (void)viewWillLayoutSubviews { [super viewWillLayoutSubviews]; self.videoCapture.previewLayer.frame = self.view.bounds; } - (void)dealloc { } #pragma mark - Action - (void)changeCamera { [self.videoCapture changeDevicePosition:self.videoCapture.config.position == AVCaptureDevicePositionBack ? AVCaptureDevicePositionFront : AVCaptureDevicePositionBack]; } - (void)shot { self.shotCount = 1; } #pragma mark - Utility - (void)requestAccessForVideo { __weak typeof(self) weakSelf = self; AVAuthorizationStatus status = [AVCaptureDevice authorizationStatusForMediaType:AVMediaTypeVideo]; switch (status) { case AVAuthorizationStatusNotDetermined: { // 许可对话没有出现,发起授权许可。 [AVCaptureDevice requestAccessForMediaType:AVMediaTypeVideo completionHandler:^(BOOL granted) { if (granted) { [weakSelf.videoCapture startRunning]; } else { // 用户拒绝。 } }]; break; } case AVAuthorizationStatusAuthorized: { // 已经开启授权,可继续。 [weakSelf.videoCapture startRunning]; break; } default: break; } } - (void)saveSampleBuffer:(CMSampleBufferRef)sampleBuffer { __block UIImage *image = [self imageFromSampleBuffer:sampleBuffer]; PHAuthorizationStatus authorizationStatus = [PHPhotoLibrary authorizationStatus]; if (authorizationStatus == PHAuthorizationStatusAuthorized) { PHPhotoLibrary *library = [PHPhotoLibrary sharedPhotoLibrary]; [library performChanges:^{ [PHAssetChangeRequest creationRequestForAssetFromImage:image]; } completionHandler:^(BOOL success, NSError * _Nullable error) { }]; } else if (authorizationStatus == PHAuthorizationStatusNotDetermined) { // 如果没请求过相册权限,弹出指示框,让用户选择。 [PHPhotoLibrary requestAuthorization:^(PHAuthorizationStatus status) { // 如果用户选择授权,则保存图片。 if (status == PHAuthorizationStatusAuthorized) { [PHAssetChangeRequest creationRequestForAssetFromImage:image]; } }]; } else { NSLog(@"无相册权限。"); } } - (UIImage *)imageFromSampleBuffer:(CMSampleBufferRef)sampleBuffer { // 从 CMSampleBuffer 中创建 UIImage。 // 从 CMSampleBuffer 获取 CVImageBuffer(也是 CVPixelBuffer)。 CVImageBufferRef imageBuffer = CMSampleBufferGetImageBuffer(sampleBuffer); // 锁定 CVPixelBuffer 的基地址。 CVPixelBufferLockBaseAddress(imageBuffer, 0); void *baseAddress = CVPixelBufferGetBaseAddress(imageBuffer); // 获取 CVPixelBuffer 每行的字节数。 size_t bytesPerRow = CVPixelBufferGetBytesPerRow(imageBuffer); // 获取 CVPixelBuffer 的宽高。 size_t width = CVPixelBufferGetWidth(imageBuffer); size_t height = CVPixelBufferGetHeight(imageBuffer); // 创建设备相关的 RGB 颜色空间。这里的颜色空间要与 CMSampleBuffer 图像数据的颜色空间一致。 CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB(); // 基于 CVPixelBuffer 的数据创建绘制 bitmap 的上下文。 CGContextRef context = CGBitmapContextCreate(baseAddress, width, height, 8, bytesPerRow, colorSpace, kCGBitmapByteOrder32Little | kCGImageAlphaPremultipliedFirst); // 从 bitmap 绘制的上下文中获取 CGImage 图像。 CGImageRef quartzImage = CGBitmapContextCreateImage(context); // 解锁 CVPixelBuffer。 CVPixelBufferUnlockBaseAddress(imageBuffer, 0); // 是否上下文和颜色空间。 CGContextRelease(context); CGColorSpaceRelease(colorSpace); // 从 CGImage 转换到 UIImage。 UIImage *image = [UIImage imageWithCGImage:quartzImage]; // 释放 CGImage。 CGImageRelease(quartzImage); return image; } @end ``` 上面是 `KFVideoCaptureViewController` 的实现,主要分为以下几个部分: - 1)在 `-videoCaptureConfig` 中初始化采集配置参数。 - - 这里需要注意的是,我们设置了采集的颜色空间格式为 `kCVPixelFormatType_32BGRA`。这主要是为了方便后面截图时转换数据。 - 2)在 `-videoCapture` 中初始化采集器,并实现了采集会话初始化成功的回调、采集数据回调、采集错误回调。 - 3)在采集会话初始化成功的回调 `sessionInitSuccessCallBack` 中,对采集预览渲染视图层进行布局。 - 4)在采集数据回调 `sampleBufferOutputCallBack` 中,实现了截图逻辑。 - - 通过 `-saveSampleBuffer:` → `-imageFromSampleBuffer:` 方法中实现截图。 - `-saveSampleBuffer:` 方法主要实现请求相册权限,以及获取图像存储到相册的逻辑。 - `-imageFromSampleBuffer:` 方法实现了将 `CMSampleBuffer` 转换为 `UIImage` 的逻辑。这里需要注意的是,我们在绘制 bitmap 时使用的是 RGB 颜色空间,与前面设置的采集的颜色空间一致。如果这里前后设置不一致,转换图像会出问题。 - 5)在 `-requestAccessForVideo` 方法中请求相机权限并启动采集。 - 6)在 `-changeCamera` 方法中实现切换摄像头。 更具体细节见上述代码及其注释。 ## 3、参考资料 [1]CMSampleBufferRef: *https://developer.apple.com/documentation/coremedia/cmsamplebufferref/* 原文链接:https://mp.weixin.qq.com/s/CJAhkk9BmhMOXgD2pl_rjg ================================================ FILE: iOS资料/iOS AVDemo(8):视频编码,H.264 和 H.265 都支持.md ================================================ # iOS AVDemo(8):视频编码,H.264 和 H.265 都支持 iOS/Android 客户端开发同学如果想要开始学习音视频开发,最丝滑的方式是对[音视频基础概念知识](https://mp.weixin.qq.com/mp/appmsgalbum?__biz=MjM5MTkxOTQyMQ==&action=getalbum&album_id=2140155659944787969#wechat_redirect)有一定了解后,再借助 iOS/Android 平台的音视频能力上手去实践音视频的`采集 → 编码 → 封装 → 解封装 → 解码 → 渲染`过程,并借助[音视频工具](https://mp.weixin.qq.com/mp/appmsgalbum?__biz=MjM5MTkxOTQyMQ==&action=getalbum&album_id=2216997905264082945#wechat_redirect)来分析和理解对应的音视频数据。 在[音视频工程示例](https://mp.weixin.qq.com/mp/appmsgalbum?__biz=MjM5MTkxOTQyMQ==&action=getalbum&album_id=2273301900659851268#wechat_redirect)这个栏目,我们将通过拆解`采集 → 编码 → 封装 → 解封装 → 解码 → 渲染`流程并实现 Demo 来向大家介绍如何在 iOS/Android 平台上手音视频开发。 这里是第八篇:**iOS 视频编码 Demo**。这个 Demo 里包含以下内容: - 1)实现一个视频采集模块; - 2)实现一个视频编码模块,支持 H.264/H.265; - 3)串联视频采集和编码模块,将采集到的视频数据输入给编码模块进行编码,并存储为文件; - 4)详尽的代码注释,帮你理解代码逻辑和原理。 想要了解视频编码,可以看看这几篇: - [《视频编码(1):H.264(AVC)》](https://mp.weixin.qq.com/s?__biz=MjM5MTkxOTQyMQ==&mid=2257484471&idx=1&sn=421be18e5b591043f13996734c60780b&scene=21#wechat_redirect) - [《视频编码(2):H.265(HEVC)》](https://mp.weixin.qq.com/s?__biz=MjM5MTkxOTQyMQ==&mid=2257484563&idx=1&sn=f08f9994ef7d8a6ee09491e870c6e843&scene=21#wechat_redirect) - [《视频编码(3):H.266(VVC)》](https://mp.weixin.qq.com/s?__biz=MjM5MTkxOTQyMQ==&mid=2257484673&idx=1&sn=9a6f5e69d9af825b85210c023b85b81a&scene=21#wechat_redirect) ## 1、视频采集模块 在这个 Demo 中,视频采集模块 `KFVideoCapture` 的实现与 [《iOS 视频采集 Demo》](https://mp.weixin.qq.com/s?__biz=MjM5MTkxOTQyMQ==&mid=2257485011&idx=1&sn=8bb9cfa01deba9670e9999bd20892440&scene=21#wechat_redirect) 中一样,这里就不再重复介绍了,其接口如下: ``` KFVideoCapture.h #import #import "KFVideoCaptureConfig.h" NS_ASSUME_NONNULL_BEGIN @interface KFVideoCapture : NSObject + (instancetype)new NS_UNAVAILABLE; - (instancetype)init NS_UNAVAILABLE; - (instancetype)initWithConfig:(KFVideoCaptureConfig *)config; @property (nonatomic, strong, readonly) KFVideoCaptureConfig *config; @property (nonatomic, strong, readonly) AVCaptureVideoPreviewLayer *previewLayer; // 视频预览渲染 layer。 @property (nonatomic, copy) void (^sampleBufferOutputCallBack)(CMSampleBufferRef sample); // 视频采集数据回调。 @property (nonatomic, copy) void (^sessionErrorCallBack)(NSError *error); // 视频采集会话错误回调。 @property (nonatomic, copy) void (^sessionInitSuccessCallBack)(void); // 视频采集会话初始化成功回调。 - (void)startRunning; // 开始采集。 - (void)stopRunning; // 停止采集。 - (void)changeDevicePosition:(AVCaptureDevicePosition)position; // 切换摄像头。 @end NS_ASSUME_NONNULL_END ``` ## 2、视频编码模块 在实现视频编码模块之前,我们先实现一个视频编码配置类 `KFVideoEncoderConfig`: ``` KFVideoEncoderConfig.h #import #import NS_ASSUME_NONNULL_BEGIN @interface KFVideoEncoderConfig : NSObject @property (nonatomic, assign) CGSize size; // 分辨率。 @property (nonatomic, assign) NSInteger bitrate; // 码率。 @property (nonatomic, assign) NSInteger fps; // 帧率。 @property (nonatomic, assign) NSInteger gopSize; // GOP 帧数。 @property (nonatomic, assign) BOOL openBFrame; // 编码是否使用 B 帧。 @property (nonatomic, assign) CMVideoCodecType codecType; // 编码器类型。 @property (nonatomic, assign) NSString *profile; // 编码 profile。 @end NS_ASSUME_NONNULL_END KFVideoEncoderConfig.m #import "KFVideoEncoderConfig.h" #import @implementation KFVideoEncoderConfig - (instancetype)init { self = [super init]; if (self) { _size = CGSizeMake(1080, 1920); _bitrate = 5000 * 1024; _fps = 30; _gopSize = _fps * 5; _openBFrame = YES; BOOL supportHEVC = NO; if (@available(iOS 11.0, *)) { if (&VTIsHardwareDecodeSupported) { supportHEVC = VTIsHardwareDecodeSupported(kCMVideoCodecType_HEVC); } } _codecType = supportHEVC ? kCMVideoCodecType_HEVC : kCMVideoCodecType_H264; _profile = supportHEVC ? (__bridge NSString *) kVTProfileLevel_HEVC_Main_AutoLevel : AVVideoProfileLevelH264HighAutoLevel; } return self; } @end ``` 这里实现了在设备支持 H.265 时,默认选择 H.265 编码。 接下来,我们来实现一个视频编码模块 `KFVideoEncoder`,在这里输入采集后的数据,输出编码后的数据。 ``` KFVideoEncoder.h #import #import "KFVideoEncoderConfig.h" NS_ASSUME_NONNULL_BEGIN @interface KFVideoEncoder : NSObject + (instancetype)new NS_UNAVAILABLE; - (instancetype)init NS_UNAVAILABLE; - (instancetype)initWithConfig:(KFVideoEncoderConfig*)config; @property (nonatomic, strong, readonly) KFVideoEncoderConfig *config; // 视频编码配置参数。 @property (nonatomic, copy) void (^sampleBufferOutputCallBack)(CMSampleBufferRef sampleBuffer); // 视频编码数据回调。 @property (nonatomic, copy) void (^errorCallBack)(NSError *error); // 视频编码错误回调。 - (void)encodePixelBuffer:(CVPixelBufferRef)pixelBuffer ptsTime:(CMTime)timeStamp; // 编码。 - (void)refresh; // 刷新重建编码器。 - (void)flush; // 清空编码缓冲区。 - (void)flushWithCompleteHandler:(void (^)(void))completeHandler; // 清空编码缓冲区并回调完成。 @end NS_ASSUME_NONNULL_END ``` 上面是 `KFVideoEncoder` 接口的设计,除了`初始化方法`,主要是有`获取视频编码配置`以及视频编码`数据回调`和`错误回调`的接口,另外就是`编码`、`刷新重建编码器`、`清空编码缓冲区`的接口。 其中`编码`接口对应着视频编码模块输入,`数据回调`接口则对应着输出。可以看到这里输出参数我们依然用的是 **CMSampleBufferRef**[1] 这个数据结构。不过输入的参数换成了 **CVPixelBufferRef**[2] 这个数据结构。它是对 `CVPixelBuffer` 的一个引用。 之前我们介绍过,`CMSampleBuffer` 中包含着零个或多个某一类型(audio、video、muxed 等)的采样数据。比如: - 要么是一个或多个媒体采样的 **CMBlockBuffer**[3]。其中可以封装:音频采集后、编码后、解码后的数据(如:PCM 数据、AAC 数据);视频编码后的数据(如:H.264/H.265 数据)。 - 要么是一个 **CVImageBuffer**[4](也作 **CVPixelBuffer**[5])。其中包含媒体流中 CMSampleBuffers 的格式描述、每个采样的宽高和时序信息、缓冲级别和采样级别的附属信息。缓冲级别的附属信息是指缓冲区整体的信息,比如播放速度、对后续缓冲数据的操作等。采样级别的附属信息是指单个采样的信息,比如视频帧的时间戳、是否关键帧等。其中可以封装:视频采集后、解码后等未经编码的数据(如:YCbCr 数据、RGBA 数据)。 所以,因为是视频编码的接口,这里用 `CVPixelBufferRef` 也就是图一个方便,其实也可以用 `CMSampleBufferRef`,只要编码用 `CMSampleBufferGetImageBuffer(...)` 取出对应的 `CVPixelBufferRef` 即可。 ``` KFVideoEncoder.m #import "KFVideoEncoder.h" #import #import #define KFEncoderRetrySessionMaxCount 5 #define KFEncoderEncodeFrameFailedMaxCount 20 @interface KFVideoEncoder () @property (nonatomic, assign) VTCompressionSessionRef compressionSession; @property (nonatomic, strong, readwrite) KFVideoEncoderConfig *config; // 视频编码配置参数。 @property (nonatomic, strong) dispatch_queue_t encoderQueue; @property (nonatomic, strong) dispatch_semaphore_t semaphore; @property (nonatomic, assign) BOOL needRefreshSession; // 是否需要刷新重建编码器。 @property (nonatomic, assign) NSInteger retrySessionCount; // 刷新重建编码器的次数。 @property (nonatomic, assign) NSInteger encodeFrameFailedCount; // 编码失败次数。 @end @implementation KFVideoEncoder #pragma mark - LifeCycle - (instancetype)initWithConfig:(KFVideoEncoderConfig *)config { self = [super init]; if (self) { _config = config; _encoderQueue = dispatch_queue_create("com.KeyFrameKit.videoEncoder", DISPATCH_QUEUE_SERIAL); _semaphore = dispatch_semaphore_create(1); [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(didEnterBackground:) name:UIApplicationDidEnterBackgroundNotification object:nil]; } return self; } - (void)dealloc { [[NSNotificationCenter defaultCenter] removeObserver:self]; dispatch_semaphore_wait(_semaphore, DISPATCH_TIME_FOREVER); [self _releaseCompressionSession]; dispatch_semaphore_signal(_semaphore); } #pragma mark - Public Method - (void)refresh { self.needRefreshSession = YES; // 标记位待刷新重建编码器。 } - (void)flush { // 清空编码缓冲区。 __weak typeof(self) weakSelf = self; dispatch_async(self.encoderQueue, ^{ dispatch_semaphore_wait(weakSelf.semaphore, DISPATCH_TIME_FOREVER); [weakSelf _flush]; dispatch_semaphore_signal(weakSelf.semaphore); }); } - (void)flushWithCompleteHandler:(void (^)(void))completeHandler { // 清空编码缓冲区并回调完成。 __weak typeof(self) weakSelf = self; dispatch_async(self.encoderQueue, ^{ dispatch_semaphore_wait(weakSelf.semaphore, DISPATCH_TIME_FOREVER); [weakSelf _flush]; dispatch_semaphore_signal(weakSelf.semaphore); if (completeHandler) { completeHandler(); } }); } - (void)encodePixelBuffer:(CVPixelBufferRef)pixelBuffer ptsTime:(CMTime)timeStamp { // 编码。 if (!pixelBuffer || self.retrySessionCount >= KFEncoderRetrySessionMaxCount || self.encodeFrameFailedCount >= KFEncoderEncodeFrameFailedMaxCount) { return; } CFRetain(pixelBuffer); __weak typeof(self) weakSelf = self; dispatch_async(self.encoderQueue, ^{ dispatch_semaphore_wait(weakSelf.semaphore, DISPATCH_TIME_FOREVER); OSStatus setupStatus = noErr; // 1、如果还没创建过编码器或者需要刷新重建编码器,就创建编码器。 if (!weakSelf.compressionSession || weakSelf.needRefreshSession) { [weakSelf _releaseCompressionSession]; setupStatus = [weakSelf _setupCompressionSession]; // 支持重试,记录重试次数。 weakSelf.retrySessionCount = setupStatus == noErr ? 0 : (weakSelf.retrySessionCount + 1); if (setupStatus != noErr) { [weakSelf _releaseCompressionSession]; NSLog(@"KFVideoEncoder setupCompressionSession error:%d", setupStatus); } else { weakSelf.needRefreshSession = NO; } } // 重试超过 KFEncoderRetrySessionMaxCount 次仍然失败则认为创建失败,报错。 if (!weakSelf.compressionSession) { CFRelease(pixelBuffer); dispatch_semaphore_signal(weakSelf.semaphore); if (weakSelf.retrySessionCount >= KFEncoderRetrySessionMaxCount && weakSelf.errorCallBack) { dispatch_async(dispatch_get_main_queue(), ^{ weakSelf.errorCallBack([NSError errorWithDomain:NSStringFromClass([KFVideoEncoder class]) code:setupStatus userInfo:nil]); }); } return; } // 2、对 pixelBuffer 进行编码。 VTEncodeInfoFlags flags; OSStatus encodeStatus = VTCompressionSessionEncodeFrame(weakSelf.compressionSession, pixelBuffer, timeStamp, CMTimeMake(1, (int32_t) weakSelf.config.fps), NULL, NULL, &flags); if (encodeStatus == kVTInvalidSessionErr) { // 编码失败进行重建编码器重试。 [weakSelf _releaseCompressionSession]; setupStatus = [weakSelf _setupCompressionSession]; weakSelf.retrySessionCount = setupStatus == noErr ? 0 : (weakSelf.retrySessionCount + 1); if (setupStatus == noErr) { encodeStatus = VTCompressionSessionEncodeFrame(weakSelf.compressionSession, pixelBuffer, timeStamp, CMTimeMake(1, (int32_t) weakSelf.config.fps), NULL, NULL, &flags); } else { [weakSelf _releaseCompressionSession]; } NSLog(@"KFVideoEncoder kVTInvalidSessionErr"); } // 记录编码失败次数。 if (encodeStatus != noErr) { NSLog(@"KFVideoEncoder VTCompressionSessionEncodeFrame error:%d", encodeStatus); } weakSelf.encodeFrameFailedCount = encodeStatus == noErr ? 0 : (weakSelf.encodeFrameFailedCount + 1); CFRelease(pixelBuffer); dispatch_semaphore_signal(weakSelf.semaphore); // 编码失败次数超过 KFEncoderEncodeFrameFailedMaxCount 次,报错。 if (weakSelf.encodeFrameFailedCount >= KFEncoderEncodeFrameFailedMaxCount && weakSelf.errorCallBack) { dispatch_async(dispatch_get_main_queue(), ^{ weakSelf.errorCallBack([NSError errorWithDomain:NSStringFromClass([KFVideoEncoder class]) code:encodeStatus userInfo:nil]); }); } }); } #pragma mark - Privte Method - (OSStatus)_setupCompressionSession { if (_compressionSession) { return noErr; } // 1、创建视频编码器实例。 // 这里要设置画面尺寸、编码器类型、编码数据回调。 OSStatus status = VTCompressionSessionCreate(NULL, _config.size.width, _config.size.height, _config.codecType, NULL, NULL, NULL, encoderOutputCallback, (__bridge void *) self, &_compressionSession); if (status != noErr) { return status; } // 2、设置编码器属性:实时编码。 VTSessionSetProperty(_compressionSession, kVTCompressionPropertyKey_RealTime, (__bridge CFTypeRef) @(YES)); // 3、设置编码器属性:编码 profile。 status = VTSessionSetProperty(_compressionSession, kVTCompressionPropertyKey_ProfileLevel, (__bridge CFStringRef) self.config.profile); if (status != noErr) { return status; } // 4、设置编码器属性:是否支持 B 帧。 status = VTSessionSetProperty(_compressionSession, kVTCompressionPropertyKey_AllowFrameReordering, (__bridge CFTypeRef) @(self.config.openBFrame)); if (status != noErr) { return status; } if (self.config.codecType == kCMVideoCodecType_H264) { // 5、如果是 H.264 编码,设置编码器属性:熵编码类型为 CABAC,上下文自适应的二进制算术编码。 status = VTSessionSetProperty(_compressionSession, kVTCompressionPropertyKey_H264EntropyMode, kVTH264EntropyMode_CABAC); if (status != noErr) { return status; } } // 6、设置编码器属性:画面填充模式。 NSDictionary *transferDic= @{ (__bridge NSString *) kVTPixelTransferPropertyKey_ScalingMode: (__bridge NSString *) kVTScalingMode_Letterbox, }; status = VTSessionSetProperty(_compressionSession, kVTCompressionPropertyKey_PixelTransferProperties, (__bridge CFTypeRef) (transferDic)); if (status != noErr) { return status; } // 7、设置编码器属性:平均码率。 status = VTSessionSetProperty(_compressionSession, kVTCompressionPropertyKey_AverageBitRate, (__bridge CFTypeRef) @(self.config.bitrate)); if (status != noErr) { return status; } // 8、设置编码器属性:码率上限。 if (!self.config.openBFrame && self.config.codecType == kCMVideoCodecType_H264) { NSArray *limit = @[@(self.config.bitrate * 1.5 / 8), @(1)]; status = VTSessionSetProperty(_compressionSession, kVTCompressionPropertyKey_DataRateLimits, (__bridge CFArrayRef) limit); if (status != noErr) { return status; } } // 9、设置编码器属性:期望帧率。 status = VTSessionSetProperty(_compressionSession, kVTCompressionPropertyKey_ExpectedFrameRate, (__bridge CFTypeRef) @(self.config.fps)); if (status != noErr) { return status; } // 10、设置编码器属性:最大关键帧间隔帧数,也就是 GOP 帧数。 status = VTSessionSetProperty(_compressionSession, kVTCompressionPropertyKey_MaxKeyFrameInterval, (__bridge CFTypeRef) @(self.config.gopSize)); if (status != noErr) { return status; } // 11、设置编码器属性:最大关键帧间隔时长。 status = VTSessionSetProperty(_compressionSession, kVTCompressionPropertyKey_MaxKeyFrameIntervalDuration, (__bridge CFTypeRef)@(_config.gopSize / _config.fps)); if (status != noErr) { return status; } // 12、预备编码。 status = VTCompressionSessionPrepareToEncodeFrames(_compressionSession); return status; } - (void)_releaseCompressionSession { if (_compressionSession) { // 强制处理完所有待编码的帧。 VTCompressionSessionCompleteFrames(_compressionSession, kCMTimeInvalid); // 销毁编码器。 VTCompressionSessionInvalidate(_compressionSession); CFRelease(_compressionSession); _compressionSession = NULL; } } - (void)_flush { // 清空编码缓冲区。 if (_compressionSession) { // 传入 kCMTimeInvalid 时,强制处理完所有待编码的帧,清空缓冲区。 VTCompressionSessionCompleteFrames(_compressionSession, kCMTimeInvalid); } } #pragma mark - NSNotification - (void)didEnterBackground:(NSNotification *)notification { self.needRefreshSession = YES; // 退后台回来后需要刷新重建编码器。 } #pragma mark - EncoderOutputCallback static void encoderOutputCallback(void * CM_NULLABLE outputCallbackRefCon, void * CM_NULLABLE sourceFrameRefCon, OSStatus status, VTEncodeInfoFlags infoFlags, CMSampleBufferRef sampleBuffer) { if (!sampleBuffer) { if (infoFlags & kVTEncodeInfo_FrameDropped) { NSLog(@"VideoToolboxEncoder kVTEncodeInfo_FrameDropped"); } return; } // 向外层回调编码数据。 KFVideoEncoder *videoEncoder = (__bridge KFVideoEncoder *) outputCallbackRefCon; if (videoEncoder && videoEncoder.sampleBufferOutputCallBack) { videoEncoder.sampleBufferOutputCallBack(sampleBuffer); } } @end ``` 上面是 `KFVideoEncoder` 的实现,从代码上可以看到主要有这几个部分: - 1)创建视频编码实例。 - - 在 `-_setupCompressionSession` 方法中实现。 - 2)实现视频编码逻辑,并在编码实例的数据回调中接收编码后的数据,抛给对外数据回调接口。 - - 在 `-encodePixelBuffer:ptsTime:` 方法中实现。 - 回调在 `encoderOutputCallback` 中实现。 - 3)实现清空编码缓冲区功能。 - - 在 `-_flush` 方法中实现。 - 4)刷新重建编码器功能。 - - 在 `-refresh` 方法中标记需要刷新重建,在 `-encodePixelBuffer:ptsTime:` 方法检查标记并重建编码器实例。 - 5)捕捉视频编码过程中的错误,抛给对外错误回调接口。 - - 主要在 `-encodePixelBuffer:ptsTime:` 方法捕捉错误。 - 6)清理视频编码器实例。 - - 在 `-_releaseCompressionSession` 方法中实现。 更具体细节见上述代码及其注释。 ## 3、采集视频数据进行 H.264/H.265 编码和存储 我们在一个 ViewController 中来实现视频采集及编码逻辑,并且示范了将 iOS 编码的 AVCC/HVCC 码流格式转换为 AnnexB 码流格式后再存储。 我们先来简单介绍一下这两种格式的区别: AVCC/HVCC 码流格式类似: ``` [extradata]|[length][NALU]|[length][NALU]|... ``` - VPS、SPS、PPS 不用 NALU 来存储,而是存储在 `extradata` 中; - 每个 NALU 前有个 `length` 字段表示这个 NALU 的长度(不包含 `length` 字段),`length` 字段通常是 4 字节。 AnnexB 码流格式: ``` [startcode][NALU]|[startcode][NALU]|... ``` 需要注意的是: - 每个 NALU 前要添加起始码:`0x00000001`; - VPS、SPS、PPS 也都用这样的 NALU 来存储,一般在码流最前面。 iOS 的 VideoToolbox 编码和解码只支持 AVCC/HVCC 的码流格式。但是 Android 的 MediaCodec 只支持 AnnexB 的码流格式。 ``` KFVideoEncoderViewController.m #import "KFVideoEncoderViewController.h" #import "KFVideoCapture.h" #import "KFVideoEncoder.h" @interface KFVideoPacketExtraData : NSObject @property (nonatomic, strong) NSData *sps; @property (nonatomic, strong) NSData *pps; @property (nonatomic, strong) NSData *vps; @end @implementation KFVideoPacketExtraData @end @interface KFVideoEncoderViewController () @property (nonatomic, strong) KFVideoCaptureConfig *videoCaptureConfig; @property (nonatomic, strong) KFVideoCapture *videoCapture; @property (nonatomic, strong) KFVideoEncoderConfig *videoEncoderConfig; @property (nonatomic, strong) KFVideoEncoder *videoEncoder; @property (nonatomic, assign) BOOL isEncoding; @property (nonatomic, strong) NSFileHandle *fileHandle; @end @implementation KFVideoEncoderViewController #pragma mark - Property - (KFVideoCaptureConfig *)videoCaptureConfig { if (!_videoCaptureConfig) { _videoCaptureConfig = [[KFVideoCaptureConfig alloc] init]; // 这里我们采集数据用于编码,颜色格式用了默认的:kCVPixelFormatType_420YpCbCr8BiPlanarFullRange。 } return _videoCaptureConfig; } - (KFVideoCapture *)videoCapture { if (!_videoCapture) { _videoCapture = [[KFVideoCapture alloc] initWithConfig:self.videoCaptureConfig]; __weak typeof(self) weakSelf = self; _videoCapture.sessionInitSuccessCallBack = ^() { dispatch_async(dispatch_get_main_queue(), ^{ // 预览渲染。 [weakSelf.view.layer insertSublayer:weakSelf.videoCapture.previewLayer atIndex:0]; weakSelf.videoCapture.previewLayer.backgroundColor = [UIColor blackColor].CGColor; weakSelf.videoCapture.previewLayer.frame = weakSelf.view.bounds; }); }; _videoCapture.sampleBufferOutputCallBack = ^(CMSampleBufferRef sampleBuffer) { if (weakSelf.isEncoding && sampleBuffer) { // 编码。 [weakSelf.videoEncoder encodePixelBuffer:CMSampleBufferGetImageBuffer(sampleBuffer) ptsTime:CMSampleBufferGetPresentationTimeStamp(sampleBuffer)]; } }; _videoCapture.sessionErrorCallBack = ^(NSError* error) { NSLog(@"KFVideoCapture Error:%zi %@", error.code, error.localizedDescription); }; } return _videoCapture; } - (KFVideoEncoderConfig *)videoEncoderConfig { if (!_videoEncoderConfig) { _videoEncoderConfig = [[KFVideoEncoderConfig alloc] init]; } return _videoEncoderConfig; } - (KFVideoEncoder *)videoEncoder { if (!_videoEncoder) { _videoEncoder = [[KFVideoEncoder alloc] initWithConfig:self.videoEncoderConfig]; __weak typeof(self) weakSelf = self; _videoEncoder.sampleBufferOutputCallBack = ^(CMSampleBufferRef sampleBuffer) { // 保存编码后的数据。 [weakSelf saveSampleBuffer:sampleBuffer]; }; } return _videoEncoder; } - (NSFileHandle *)fileHandle { if (!_fileHandle) { NSString *fileName = @"test.h264"; if (self.videoEncoderConfig.codecType == kCMVideoCodecType_HEVC) { fileName = @"test.h265"; } NSString *videoPath = [[NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) lastObject] stringByAppendingPathComponent:fileName]; [[NSFileManager defaultManager] removeItemAtPath:videoPath error:nil]; [[NSFileManager defaultManager] createFileAtPath:videoPath contents:nil attributes:nil]; _fileHandle = [NSFileHandle fileHandleForWritingAtPath:videoPath]; } return _fileHandle; } #pragma mark - Lifecycle - (void)viewDidLoad { [super viewDidLoad]; // Navigation item. UIBarButtonItem *startBarButton = [[UIBarButtonItem alloc] initWithTitle:@"Start" style:UIBarButtonItemStylePlain target:self action:@selector(start)]; UIBarButtonItem *stopBarButton = [[UIBarButtonItem alloc] initWithTitle:@"Stop" style:UIBarButtonItemStylePlain target:self action:@selector(stop)]; UIBarButtonItem *cameraBarButton = [[UIBarButtonItem alloc] initWithTitle:@"Camera" style:UIBarButtonItemStylePlain target:self action:@selector(changeCamera)]; self.navigationItem.rightBarButtonItems = @[stopBarButton,startBarButton,cameraBarButton]; [self requestAccessForVideo]; UITapGestureRecognizer *doubleTapGesture = [[UITapGestureRecognizer alloc]initWithTarget:self action:@selector(handleDoubleTap:)]; doubleTapGesture.numberOfTapsRequired = 2; doubleTapGesture.numberOfTouchesRequired = 1; [self.view addGestureRecognizer:doubleTapGesture]; } - (void)viewWillLayoutSubviews { [super viewWillLayoutSubviews]; self.videoCapture.previewLayer.frame = self.view.bounds; } - (void)dealloc { } #pragma mark - Action - (void)start { if (!self.isEncoding) { self.isEncoding = YES; [self.videoEncoder refresh]; } } - (void)stop { if (self.isEncoding) { self.isEncoding = NO; [self.videoEncoder flush]; } } - (void)onCameraSwitchButtonClicked:(UIButton *)button { [self.videoCapture changeDevicePosition:self.videoCapture.config.position == AVCaptureDevicePositionBack ? AVCaptureDevicePositionFront : AVCaptureDevicePositionBack]; } - (void)changeCamera { [self.videoCapture changeDevicePosition:self.videoCapture.config.position == AVCaptureDevicePositionBack ? AVCaptureDevicePositionFront : AVCaptureDevicePositionBack]; } -(void)handleDoubleTap:(UIGestureRecognizer *)sender { [self.videoCapture changeDevicePosition:self.videoCapture.config.position == AVCaptureDevicePositionBack ? AVCaptureDevicePositionFront : AVCaptureDevicePositionBack]; } #pragma mark - Private Method - (void)requestAccessForVideo{ __weak typeof(self) weakSelf = self; AVAuthorizationStatus status = [AVCaptureDevice authorizationStatusForMediaType:AVMediaTypeVideo]; switch (status) { case AVAuthorizationStatusNotDetermined: { // 许可对话没有出现,发起授权许可。 [AVCaptureDevice requestAccessForMediaType:AVMediaTypeVideo completionHandler:^(BOOL granted) { if (granted) { [weakSelf.videoCapture startRunning]; } else { // 用户拒绝。 } }]; break; } case AVAuthorizationStatusAuthorized: { // 已经开启授权,可继续。 [weakSelf.videoCapture startRunning]; break; } default: break; } } - (KFVideoPacketExtraData *)getPacketExtraData:(CMSampleBufferRef)sampleBuffer { // 从 CMSampleBuffer 中获取 extra data。 if (!sampleBuffer) { return nil; } // 获取编码类型。 CMVideoCodecType codecType = CMVideoFormatDescriptionGetCodecType(CMSampleBufferGetFormatDescription(sampleBuffer)); KFVideoPacketExtraData *extraData = nil; if (codecType == kCMVideoCodecType_H264) { // 获取 H.264 的 extra data:sps、pps。 CMFormatDescriptionRef format = CMSampleBufferGetFormatDescription(sampleBuffer); size_t sparameterSetSize, sparameterSetCount; const uint8_t *sparameterSet; OSStatus statusCode = CMVideoFormatDescriptionGetH264ParameterSetAtIndex(format, 0, &sparameterSet, &sparameterSetSize, &sparameterSetCount, 0); if (statusCode == noErr) { size_t pparameterSetSize, pparameterSetCount; const uint8_t *pparameterSet; OSStatus statusCode = CMVideoFormatDescriptionGetH264ParameterSetAtIndex(format, 1, &pparameterSet, &pparameterSetSize, &pparameterSetCount, 0); if (statusCode == noErr) { extraData = [[KFVideoPacketExtraData alloc] init]; extraData.sps = [NSData dataWithBytes:sparameterSet length:sparameterSetSize]; extraData.pps = [NSData dataWithBytes:pparameterSet length:pparameterSetSize]; } } } else if (codecType == kCMVideoCodecType_HEVC) { // 获取 H.265 的 extra data:vps、sps、pps。 CMFormatDescriptionRef format = CMSampleBufferGetFormatDescription(sampleBuffer); size_t vparameterSetSize, vparameterSetCount; const uint8_t *vparameterSet; if (@available(iOS 11.0, *)) { OSStatus statusCode = CMVideoFormatDescriptionGetHEVCParameterSetAtIndex(format, 0, &vparameterSet, &vparameterSetSize, &vparameterSetCount, 0); if (statusCode == noErr) { size_t sparameterSetSize, sparameterSetCount; const uint8_t *sparameterSet; OSStatus statusCode = CMVideoFormatDescriptionGetHEVCParameterSetAtIndex(format, 1, &sparameterSet, &sparameterSetSize, &sparameterSetCount, 0); if (statusCode == noErr) { size_t pparameterSetSize, pparameterSetCount; const uint8_t *pparameterSet; OSStatus statusCode = CMVideoFormatDescriptionGetHEVCParameterSetAtIndex(format, 2, &pparameterSet, &pparameterSetSize, &pparameterSetCount, 0); if (statusCode == noErr) { extraData = [[KFVideoPacketExtraData alloc] init]; extraData.vps = [NSData dataWithBytes:vparameterSet length:vparameterSetSize]; extraData.sps = [NSData dataWithBytes:sparameterSet length:sparameterSetSize]; extraData.pps = [NSData dataWithBytes:pparameterSet length:pparameterSetSize]; } } } } else { // 其他编码格式。 } } return extraData; } - (BOOL)isKeyFrame:(CMSampleBufferRef)sampleBuffer { CFArrayRef array = CMSampleBufferGetSampleAttachmentsArray(sampleBuffer, true); if (!array) { return NO; } CFDictionaryRef dic = (CFDictionaryRef)CFArrayGetValueAtIndex(array, 0); if (!dic) { return NO; } // 检测 sampleBuffer 是否是关键帧。 BOOL keyframe = !CFDictionaryContainsKey(dic, kCMSampleAttachmentKey_NotSync); return keyframe; } - (void)saveSampleBuffer:(CMSampleBufferRef)sampleBuffer { // 将编码数据存储为文件。 // iOS 的 VideoToolbox 编码和解码只支持 AVCC/HVCC 的码流格式。但是 Android 的 MediaCodec 只支持 AnnexB 的码流格式。这里我们做一下两种格式的转换示范,将 AVCC/HVCC 格式的码流转换为 AnnexB 再存储。 // 1、AVCC/HVCC 码流格式:[extradata]|[length][NALU]|[length][NALU]|... // VPS、SPS、PPS 不用 NALU 来存储,而是存储在 extradata 中;每个 NALU 前有个 length 字段表示这个 NALU 的长度(不包含 length 字段),length 字段通常是 4 字节。 // 2、AnnexB 码流格式:[startcode][NALU]|[startcode][NALU]|... // 每个 NAL 前要添加起始码:0x00000001;VPS、SPS、PPS 也都用这样的 NALU 来存储,一般在码流最前面。 if (sampleBuffer) { NSMutableData *resultData = [NSMutableData new]; uint8_t nalPartition[] = {0x00, 0x00, 0x00, 0x01}; // 关键帧前添加 vps(H.265)、sps、pps。这里要注意顺序别乱了。 if ([self isKeyFrame:sampleBuffer]) { KFVideoPacketExtraData *extraData = [self getPacketExtraData:sampleBuffer]; if (extraData.vps) { [resultData appendBytes:nalPartition length:4]; [resultData appendData:extraData.vps]; } [resultData appendBytes:nalPartition length:4]; [resultData appendData:extraData.sps]; [resultData appendBytes:nalPartition length:4]; [resultData appendData:extraData.pps]; } // 获取编码数据。这里的数据是 AVCC/HVCC 格式的。 CMBlockBufferRef dataBuffer = CMSampleBufferGetDataBuffer(sampleBuffer); size_t length, totalLength; char *dataPointer; OSStatus statusCodeRet = CMBlockBufferGetDataPointer(dataBuffer, 0, &length, &totalLength, &dataPointer); if (statusCodeRet == noErr) { size_t bufferOffset = 0; static const int NALULengthHeaderLength = 4; // 拷贝编码数据。 while (bufferOffset < totalLength - NALULengthHeaderLength) { // 通过 length 字段获取当前这个 NALU 的长度。 uint32_t NALUnitLength = 0; memcpy(&NALUnitLength, dataPointer + bufferOffset, NALULengthHeaderLength); NALUnitLength = CFSwapInt32BigToHost(NALUnitLength); // 拷贝 AnnexB 起始码字节。 [resultData appendData:[NSData dataWithBytes:nalPartition length:4]]; // 拷贝这个 NALU 的字节。 [resultData appendData:[NSData dataWithBytes:(dataPointer + bufferOffset + NALULengthHeaderLength) length:NALUnitLength]]; // 步进。 bufferOffset += NALULengthHeaderLength + NALUnitLength; } } [self.fileHandle writeData:resultData]; } } @end ``` 上面是 `KFVideoEncoderViewController` 的实现,主要分为以下几个部分: - 1)在 `-videoCaptureConfig` 中初始化采集配置参数,在 `-videoEncoderConfig` 中初始化编码配置参数。 - - 这里需要注意的是,由于采集的数据后续用于编码,我们设置了采集的颜色空间格式为默认的 `kCVPixelFormatType_420YpCbCr8BiPlanarFullRange`。 - 编码参数配置这里,默认是在设备支持 H.265 时,选择 H.265 编码。 - 2)在 `-videoCapture` 中初始化采集器,并实现了采集会话初始化成功的回调、采集数据回调、采集错误回调。 - 3)在采集会话初始化成功的回调 `sessionInitSuccessCallBack` 中,对采集预览渲染视图层进行布局。 - 4)在采集数据回调 `sampleBufferOutputCallBack` 中,从 CMSampleBufferRef 中取出 CVPixelBufferRef 送给编码器编码。 - 5)在编码数据回调 `sampleBufferOutputCallBack` 中,调用 `-saveSampleBuffer:` 将编码数据存储为 H.264/H.265 文件。 - - 这里示范了将 AVCC/HVCC 格式的码流转换为 AnnexB 再存储的过程。 ## 4、用工具播放 H.264/H.265 文件 完成视频采集和编码后,可以将 App Document 文件夹下面的 `test.h264` 或 `test.h265` 文件拷贝到电脑上,使用 `ffplay` 播放来验证一下视频采集是效果是否符合预期: ``` $ ffplay -i test.h264 $ ffplay -i test.h265 ``` 关于播放 H.264/H.265 文件的工具,可以参考[《FFmpeg 工具》第 2 节 ffplay 命令行工具](https://mp.weixin.qq.com/s?__biz=MjM5MTkxOTQyMQ==&mid=2257484831&idx=1&sn=6bab905a5040c46b971bab05f787788b&scene=21#wechat_redirect)和[《可视化音视频分析工具》第 2.1 节 StreamEye](https://mp.weixin.qq.com/s?__biz=MjM5MTkxOTQyMQ==&mid=2257484834&idx=1&sn=5dd9768bfc0d01ca1b036be8dd2f5fa1&scene=21#wechat_redirect)。 ## 5、参考资料 [1]CMSampleBufferRef: *https://developer.apple.com/documentation/coremedia/cmsamplebufferref/* [2]CVPixelBufferRef: *https://developer.apple.com/documentation/corevideo/cvpixelbufferref/* [3]CMBlockBuffer: *https://developer.apple.com/documentation/coremedia/cmblockbuffer-u9i* [4]CVImageBuffer: *https://developer.apple.com/documentation/corevideo/cvimagebuffer-q40* [5]CVPixelBuffer: *https://developer.apple.com/documentation/corevideo/cvpixelbuffer-q2e* 原文链接:https://mp.weixin.qq.com/s/M2l-9_W8heu_NjSYKQLCRA ================================================ FILE: iOS资料/iOS AVDemo(9):视频封装,采集编码 H.264H.265 并封装 MP4.md ================================================ # iOS AVDemo(9):视频封装,采集编码 H.264/H.265 并封装 MP4 iOS/Android 客户端开发同学如果想要开始学习音视频开发,最丝滑的方式是对[音视频基础概念知识](https://mp.weixin.qq.com/mp/appmsgalbum?__biz=MjM5MTkxOTQyMQ==&action=getalbum&album_id=2140155659944787969#wechat_redirect)有一定了解后,再借助 iOS/Android 平台的音视频能力上手去实践音视频的`采集 → 编码 → 封装 → 解封装 → 解码 → 渲染`过程,并借助[音视频工具](https://mp.weixin.qq.com/mp/appmsgalbum?__biz=MjM5MTkxOTQyMQ==&action=getalbum&album_id=2216997905264082945#wechat_redirect)来分析和理解对应的音视频数据。 在[音视频工程示例](https://mp.weixin.qq.com/mp/appmsgalbum?__biz=MjM5MTkxOTQyMQ==&action=getalbum&album_id=2273301900659851268#wechat_redirect)这个栏目,我们将通过拆解`采集 → 编码 → 封装 → 解封装 → 解码 → 渲染`流程并实现 Demo 来向大家介绍如何在 iOS/Android 平台上手音视频开发。 这里是第九篇:**iOS 视频封装 Demo**。这个 Demo 里包含以下内容: - 1)实现一个视频采集模块; - 2)实现一个视频编码模块,支持 H.264/H.265; - 3)实现一个视频封装模块; - 4)串联视频采集、编码、封装模块,将采集到的视频数据输入给编码模块进行编码,再将编码后的数据输入给 MP4 封装模块封装和存储; - 5)详尽的代码注释,帮你理解代码逻辑和原理。 在本文中,我们将详解一下 Demo 的具体实现和源码。读完本文内容相信就能帮你掌握相关知识。 ## 1、视频采集模块 在这个 Demo 中,视频采集模块 `KFVideoCapture` 的实现与 [《iOS 视频采集 Demo》](https://mp.weixin.qq.com/s?__biz=MjM5MTkxOTQyMQ==&mid=2257485011&idx=1&sn=8bb9cfa01deba9670e9999bd20892440&scene=21#wechat_redirect) 中一样,这里就不再重复介绍了,其接口如下: ``` KFVideoCapture.h #import #import "KFVideoCaptureConfig.h" NS_ASSUME_NONNULL_BEGIN @interface KFVideoCapture : NSObject + (instancetype)new NS_UNAVAILABLE; - (instancetype)init NS_UNAVAILABLE; - (instancetype)initWithConfig:(KFVideoCaptureConfig *)config; @property (nonatomic, strong, readonly) KFVideoCaptureConfig *config; @property (nonatomic, strong, readonly) AVCaptureVideoPreviewLayer *previewLayer; // 视频预览渲染 layer。 @property (nonatomic, copy) void (^sampleBufferOutputCallBack)(CMSampleBufferRef sample); // 视频采集数据回调。 @property (nonatomic, copy) void (^sessionErrorCallBack)(NSError *error); // 视频采集会话错误回调。 @property (nonatomic, copy) void (^sessionInitSuccessCallBack)(void); // 视频采集会话初始化成功回调。 - (void)startRunning; // 开始采集。 - (void)stopRunning; // 停止采集。 - (void)changeDevicePosition:(AVCaptureDevicePosition)position; // 切换摄像头。 @end NS_ASSUME_NONNULL_END ``` ## 2、视频编码模块 同样的,视频编码模块 `KFVideoEncoder` 的实现与[《iOS 视频编码 Demo》](https://mp.weixin.qq.com/s?__biz=MjM5MTkxOTQyMQ==&mid=2257485273&idx=1&sn=0d876a49c4e46f369f6a578856221f5d&scene=21#wechat_redirect)中一样,这里就不再重复介绍了,其接口如下: ``` KFVideoEncoder.h #import #import "KFVideoEncoderConfig.h" NS_ASSUME_NONNULL_BEGIN @interface KFVideoEncoder : NSObject + (instancetype)new NS_UNAVAILABLE; - (instancetype)init NS_UNAVAILABLE; - (instancetype)initWithConfig:(KFVideoEncoderConfig*)config; @property (nonatomic, strong, readonly) KFVideoEncoderConfig *config; // 视频编码配置参数。 @property (nonatomic, copy) void (^sampleBufferOutputCallBack)(CMSampleBufferRef sampleBuffer); // 视频编码数据回调。 @property (nonatomic, copy) void (^errorCallBack)(NSError *error); // 视频编码错误回调。 - (void)encodePixelBuffer:(CVPixelBufferRef)pixelBuffer ptsTime:(CMTime)timeStamp; // 编码。 - (void)refresh; // 刷新重建编码器。 - (void)flush; // 清空编码缓冲区。 - (void)flushWithCompleteHandler:(void (^)(void))completeHandler; // 清空编码缓冲区并回调完成。 @end NS_ASSUME_NONNULL_END ``` ## 3、视频封装模块 视频编码模块即 `KFMP4Muxer`,复用了[《iOS 音频封装 Demo》](https://mp.weixin.qq.com/s?__biz=MjM5MTkxOTQyMQ==&mid=2257484907&idx=1&sn=e2418939db2e199c42130e20cb9e1a34&scene=21#wechat_redirect)中介绍的 muxer,这里就不再重复介绍了,其接口如下: ``` KFMP4Muxer.h #import #import #import "KFMuxerConfig.h" NS_ASSUME_NONNULL_BEGIN @interface KFMP4Muxer : NSObject + (instancetype)new NS_UNAVAILABLE; - (instancetype)init NS_UNAVAILABLE; - (instancetype)initWithConfig:(KFMuxerConfig *)config; @property (nonatomic, strong, readonly) KFMuxerConfig *config; @property (nonatomic, copy) void (^errorCallBack)(NSError *error); // 封装错误回调。 - (void)startWriting; // 开始封装写入数据。 - (void)cancelWriting; // 取消封装写入数据。 - (void)appendSampleBuffer:(CMSampleBufferRef)sampleBuffer; // 添加封装数据。 - (void)stopWriting:(void (^)(BOOL success, NSError *error))completeHandler; // 停止封装写入数据。 @end NS_ASSUME_NONNULL_END ``` ## 4、采集视频数据进行 H.264/H.265 编码以及 MP4 封装和存储 我们还是在一个 ViewController 中来实现采集视频数据进行 H.264/H.265 编码以及 MP4 封装和存储的逻辑。 ``` KFVideoMuxerViewController.m #import "KFVideoMuxerViewController.h" #import "KFVideoCapture.h" #import "KFVideoEncoder.h" #import "KFMP4Muxer.h" @interface KFVideoMuxerViewController () @property (nonatomic, strong) KFVideoCaptureConfig *videoCaptureConfig; @property (nonatomic, strong) KFVideoCapture *videoCapture; @property (nonatomic, strong) KFVideoEncoderConfig *videoEncoderConfig; @property (nonatomic, strong) KFVideoEncoder *videoEncoder; @property (nonatomic, strong) KFMuxerConfig *muxerConfig; @property (nonatomic, strong) KFMP4Muxer *muxer; @property (nonatomic, assign) BOOL isWriting; @end @implementation KFVideoMuxerViewController #pragma mark - Property - (KFVideoCaptureConfig *)videoCaptureConfig { if (!_videoCaptureConfig) { _videoCaptureConfig = [[KFVideoCaptureConfig alloc] init]; } return _videoCaptureConfig; } - (KFVideoCapture *)videoCapture { if (!_videoCapture) { _videoCapture = [[KFVideoCapture alloc] initWithConfig:self.videoCaptureConfig]; __weak typeof(self) weakSelf = self; _videoCapture.sessionInitSuccessCallBack = ^() { dispatch_async(dispatch_get_main_queue(), ^{ // 预览渲染。 [weakSelf.view.layer insertSublayer:weakSelf.videoCapture.previewLayer atIndex:0]; weakSelf.videoCapture.previewLayer.backgroundColor = [UIColor blackColor].CGColor; weakSelf.videoCapture.previewLayer.frame = weakSelf.view.bounds; }); }; _videoCapture.sampleBufferOutputCallBack = ^(CMSampleBufferRef sampleBuffer) { if (sampleBuffer && weakSelf.isWriting) { // 编码。 [weakSelf.videoEncoder encodePixelBuffer:CMSampleBufferGetImageBuffer(sampleBuffer) ptsTime:CMSampleBufferGetPresentationTimeStamp(sampleBuffer)]; } }; _videoCapture.sessionErrorCallBack = ^(NSError *error) { NSLog(@"KFVideoCapture Error:%zi %@", error.code, error.localizedDescription); }; } return _videoCapture; } - (KFVideoEncoderConfig *)videoEncoderConfig { if (!_videoEncoderConfig) { _videoEncoderConfig = [[KFVideoEncoderConfig alloc] init]; } return _videoEncoderConfig; } - (KFVideoEncoder *)videoEncoder { if (!_videoEncoder) { _videoEncoder = [[KFVideoEncoder alloc] initWithConfig:self.videoEncoderConfig]; __weak typeof(self) weakSelf = self; _videoEncoder.sampleBufferOutputCallBack = ^(CMSampleBufferRef sampleBuffer) { // 视频编码数据回调。 if (weakSelf.isWriting) { // 当标记封装写入中时,将编码的 H.264/H.265 数据送给封装器。 [weakSelf.muxer appendSampleBuffer:sampleBuffer]; } }; } return _videoEncoder; } - (KFMuxerConfig *)muxerConfig { if (!_muxerConfig) { _muxerConfig = [[KFMuxerConfig alloc] init]; NSString *videoPath = [[NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) lastObject] stringByAppendingPathComponent:@"test.mp4"]; NSLog(@"MP4 file path: %@", videoPath); [[NSFileManager defaultManager] removeItemAtPath:videoPath error:nil]; _muxerConfig.outputURL = [NSURL fileURLWithPath:videoPath]; _muxerConfig.muxerType = KFMediaVideo; } return _muxerConfig; } - (KFMP4Muxer *)muxer { if (!_muxer) { _muxer = [[KFMP4Muxer alloc] initWithConfig:self.muxerConfig]; } return _muxer; } #pragma mark - Lifecycle - (void)viewDidLoad { [super viewDidLoad]; // 启动后即开始请求视频采集权限并开始采集。 [self requestAccessForVideo]; [self setupUI]; } - (void)viewWillLayoutSubviews { [super viewWillLayoutSubviews]; self.videoCapture.previewLayer.frame = self.view.bounds; } - (void)dealloc { } #pragma mark - Action - (void)start { if (!self.isWriting) { // 启动封装, [self.muxer startWriting]; // 标记开始封装写入。 self.isWriting = YES; } } - (void)stop { if (self.isWriting) { __weak typeof(self) weakSelf = self; [self.videoEncoder flushWithCompleteHandler:^{ weakSelf.isWriting = NO; [weakSelf.muxer stopWriting:^(BOOL success, NSError * _Nonnull error) { NSLog(@"muxer stop %@", success ? @"success" : @"failed"); }]; }]; } } - (void)changeCamera { [self.videoCapture changeDevicePosition:self.videoCapture.config.position == AVCaptureDevicePositionBack ? AVCaptureDevicePositionFront : AVCaptureDevicePositionBack]; } - (void)singleTap:(UIGestureRecognizer *)sender { } -(void)handleDoubleTap:(UIGestureRecognizer *)sender { [self.videoCapture changeDevicePosition:self.videoCapture.config.position == AVCaptureDevicePositionBack ? AVCaptureDevicePositionFront : AVCaptureDevicePositionBack]; } #pragma mark - Private Method - (void)requestAccessForVideo { __weak typeof(self) weakSelf = self; AVAuthorizationStatus status = [AVCaptureDevice authorizationStatusForMediaType:AVMediaTypeVideo]; switch (status) { case AVAuthorizationStatusNotDetermined:{ // 许可对话没有出现,发起授权许可。 [AVCaptureDevice requestAccessForMediaType:AVMediaTypeVideo completionHandler:^(BOOL granted) { if (granted) { [weakSelf.videoCapture startRunning]; } else { // 用户拒绝。 } }]; break; } case AVAuthorizationStatusAuthorized:{ // 已经开启授权,可继续。 [weakSelf.videoCapture startRunning]; break; } default: break; } } - (void)setupUI { self.edgesForExtendedLayout = UIRectEdgeAll; self.extendedLayoutIncludesOpaqueBars = YES; self.title = @"Video Muxer"; self.view.backgroundColor = [UIColor whiteColor]; // 添加手势。 UITapGestureRecognizer *singleTapGesture = [[UITapGestureRecognizer alloc]initWithTarget:self action:@selector(singleTap:)]; singleTapGesture.numberOfTapsRequired = 1; singleTapGesture.numberOfTouchesRequired = 1; [self.view addGestureRecognizer:singleTapGesture]; UITapGestureRecognizer *doubleTapGesture = [[UITapGestureRecognizer alloc]initWithTarget:self action:@selector(handleDoubleTap:)]; doubleTapGesture.numberOfTapsRequired = 2; doubleTapGesture.numberOfTouchesRequired = 1; [self.view addGestureRecognizer:doubleTapGesture]; [singleTapGesture requireGestureRecognizerToFail:doubleTapGesture]; // Navigation item. UIBarButtonItem *startBarButton = [[UIBarButtonItem alloc] initWithTitle:@"Start" style:UIBarButtonItemStylePlain target:self action:@selector(start)]; UIBarButtonItem *stopBarButton = [[UIBarButtonItem alloc] initWithTitle:@"Stop" style:UIBarButtonItemStylePlain target:self action:@selector(stop)]; UIBarButtonItem *cameraBarButton = [[UIBarButtonItem alloc] initWithTitle:@"Camera" style:UIBarButtonItemStylePlain target:self action:@selector(changeCamera)]; self.navigationItem.rightBarButtonItems = @[stopBarButton, startBarButton, cameraBarButton]; } @end ``` 上面是 `KFVideoMuxerViewController` 的实现,其中主要包含这几个部分: - 1)启动后即开始请求视频采集权限并开始采集。 - - 在 `-requestAccessForVideo` 方法中实现。 - 2)在采集会话初始化成功的回调中,对采集预览渲染视图层进行布局。 - - 在 `KFVideoCapture` 的 `sessionInitSuccessCallBack` 回调中实现。 - 2)在采集模块的数据回调中将数据交给编码模块进行编码。 - - 在 `KFVideoCapture` 的 `sampleBufferOutputCallBack` 回调中实现。 - 3)在编码模块的数据回调中获取编码后的 H.264/H.265 数据,并将数据交给封装器 `KFMP4Muxer` 进行封装。 - - 在 `KFVideoEncoder` 的 `sampleBufferOutputCallBack` 回调中实现。 - 4)在调用 `-stop` 停止整个流程后,如果没有出现错误,封装的 MP4 文件会被存储到 `muxerConfig` 设置的路径。 ## 5、用工具播放 MP4 文件 完成 Demo 后,可以将 App Document 文件夹下面的 `test.mp4` 文件拷贝到电脑上,使用 `ffplay` 播放来验证一下效果是否符合预期: ``` $ ffplay -i test.mp4 ``` 关于播放 MP4 文件的工具,可以参考[《FFmpeg 工具》第 2 节 ffplay 命令行工具](https://mp.weixin.qq.com/s?__biz=MjM5MTkxOTQyMQ==&mid=2257484831&idx=1&sn=6bab905a5040c46b971bab05f787788b&scene=21#wechat_redirect)和[《可视化音视频分析工具》第 3.5 节 VLC 播放器](https://mp.weixin.qq.com/s?__biz=MjM5MTkxOTQyMQ==&mid=2257484834&idx=1&sn=5dd9768bfc0d01ca1b036be8dd2f5fa1&scene=21#wechat_redirect)。 我们还可以用[《可视化音视频分析工具》第 3.1 节 MP4Box.js](https://mp.weixin.qq.com/s?__biz=MjM5MTkxOTQyMQ==&mid=2257484834&idx=1&sn=5dd9768bfc0d01ca1b036be8dd2f5fa1&scene=21#wechat_redirect) 等工具来查看它的格式: ![图片](https://mmbiz.qpic.cn/mmbiz_png/gUnqKPeSueiavb3sxPjmibY0C1fFYXUXcI1CP7x0G6f5eNZ4G8pcEojOya39XgK1icVz7MCTQrlG0micRUkF84wfpA/640?wx_fmt=png&wxfrom=5&wx_lazy=1&wx_co=1)Demo 生成的 MP4 文件结构 原文链接:https://mp.weixin.qq.com/s/W17eLiUeCszNM8Kg-rlmBg ================================================ FILE: iOS资料/iOS AVDemo:音频封装,采集编码并封装为 M4A.md ================================================ # iOS AVDemo:音频封装,采集编码并封装为 M4A iOS/Android 客户端开发同学如果想要开始学习音视频开发,最丝滑的方式是对[音视频基础概念知识](https://mp.weixin.qq.com/mp/appmsgalbum?__biz=MjM5MTkxOTQyMQ==&action=getalbum&album_id=2140155659944787969#wechat_redirect)有一定了解后,再借助本地平台的音视频能力上手去实践音视频的`采集 → 编码 → 封装 → 解封装 → 解码 → 渲染`过程,并借助[音视频工具](https://mp.weixin.qq.com/mp/appmsgalbum?__biz=MjM5MTkxOTQyMQ==&action=getalbum&album_id=2216997905264082945#wechat_redirect)来分析和理解对应的音视频数据。 在[音视频工程示例](https://mp.weixin.qq.com/mp/appmsgalbum?__biz=MjM5MTkxOTQyMQ==&action=getalbum&album_id=2273301900659851268#wechat_redirect)这个栏目,我们将通过拆解`采集 → 编码 → 封装 → 解封装 → 解码 → 渲染`流程并实现 Demo 来向大家介绍如何在 iOS/Android 平台上手音视频开发。 这里是第三篇:**iOS 音频封装 Demo**。这个 Demo 里包含以下内容: - 1)实现一个音频采集模块; - 2)实现一个音频编码模块; - 3)实现一个音频封装模块; - 4)串联音频采集、编码、封装模块,将采集到的音频数据输入给 AAC 编码模块进行编码,再将编码后的数据输入给 M4A 封装模块封装和存储; - 5)详尽的代码注释,帮你理解代码逻辑和原理。 ## 1、音频采集模块 在这个 Demo 中,音频采集模块 `KFAudioCapture` 的实现与 [《iOS 音频采集 Demo》](https://mp.weixin.qq.com/s?__biz=MjM5MTkxOTQyMQ==&mid=2257484867&idx=1&sn=d857104930a86de8ab0bdf2358ca6283&scene=21#wechat_redirect) 中一样,这里就不再重复介绍了,其接口如下: ``` KFAudioCapture.h #import #import #import "KFAudioConfig.h" NS_ASSUME_NONNULL_BEGIN @interface KFAudioCapture : NSObject + (instancetype)new NS_UNAVAILABLE; - (instancetype)init NS_UNAVAILABLE; - (instancetype)initWithConfig:(KFAudioConfig *)config; @property (nonatomic, strong, readonly) KFAudioConfig *config; @property (nonatomic, copy) void (^sampleBufferOutputCallBack)(CMSampleBufferRef sample); // 音频采集数据回调。 @property (nonatomic, copy) void (^errorCallBack)(NSError *error); // 音频采集错误回调。 - (void)startRunning; // 开始采集音频数据。 - (void)stopRunning; // 停止采集音频数据。 @end NS_ASSUME_NONNULL_END ``` ## 2、音频编码模块 同样的,音频编码模块 `KFAudioEncoder` 的实现与[《iOS 音频编码 Demo》](https://mp.weixin.qq.com/s?__biz=MjM5MTkxOTQyMQ==&mid=2257484887&idx=1&sn=ac142cbeafddc27f3a8c2902524831c8&scene=21#wechat_redirect)中一样,这里就不再重复介绍了,其接口如下: ``` #import #import NS_ASSUME_NONNULL_BEGIN @interface KFAudioEncoder : NSObject + (instancetype)new NS_UNAVAILABLE; - (instancetype)init NS_UNAVAILABLE; - (instancetype)initWithAudioBitrate:(NSInteger)audioBitrate; @property (nonatomic, assign, readonly) NSInteger audioBitrate; // 音频编码码率。 @property (nonatomic, copy) void (^sampleBufferOutputCallBack)(CMSampleBufferRef sample); // 音频编码数据回调。 @property (nonatomic, copy) void (^errorCallBack)(NSError *error); // 音频编码错误回调。 - (void)encodeSampleBuffer:(CMSampleBufferRef)buffer; // 编码。 @end NS_ASSUME_NONNULL_END ``` ## 3、音频封装模块 接下来,我们来实现一个音频封装模块,在这里输入编码后的数据,输出封装后的文件。 这次我们要封装的格式是 M4A,属于 MPEG-4 标准,通常普通的 MPEG-4 文件扩展名是 `.mp4`,只包含音频的 MPEG-4 文件扩展名用 `.m4a`。所以,其实我们这里实现的是一个 MP4 封装模块,支持将音频编码数据封装成 M4A,也支持将音视频数据封装成 MP4。关于 MP4 格式,可以看一看[《MP4 格式》](https://mp.weixin.qq.com/s?__biz=MjM5MTkxOTQyMQ==&mid=2257484677&idx=1&sn=a868911489519592930e18a60966d6a1&scene=21#wechat_redirect)这篇文章了解一下。 由于 MP4 封装涉及到一些参数设置,所以我们先实现一个 `KFMuxerConfig` 类用于定义 MP4 封装的参数的配置。这里包括了:封装文件输出地址、封装文件类型、图像变换信息这几个参数。 ``` KFMuxerConfig.h #import #import #import "KFMediaBase.h" NS_ASSUME_NONNULL_BEGIN @interface KFMuxerConfig : NSObject @property (nonatomic, strong) NSURL *outputURL; // 封装文件输出地址。 @property (nonatomic, assign) KFMediaType muxerType; // 封装文件类型。 @property (nonatomic, assign) CGAffineTransform preferredTransform; // 图像的变换信息。比如:视频图像旋转。 @end NS_ASSUME_NONNULL_END KFMuxerConfig.m #import "KFMuxerConfig.h" @implementation KFMuxerConfig - (instancetype)init { self = [super init]; if (self) { _muxerType = KFMediaAV; _preferredTransform = CGAffineTransformIdentity; } return self; } @end ``` 其中用到的 `KFMediaType` 是定义在 `KFMediaBase.h` 中的一个枚举: ``` KFMediaBase.h #ifndef KFMediaBase_h #define KFMediaBase_h #import typedef NS_ENUM(NSInteger, KFMediaType) { KFMediaNone = 0, KFMediaAudio = 1 << 0, // 仅音频。 KFMediaVideo = 1 << 1, // 仅视频。 KFMediaAV = KFMediaAudio | KFMediaVideo, // 音视频都有。 }; #endif /* KFMediaBase_h */ ``` 接下来,我们来实现 `KFMP4Muxer` 模块。 ``` KFMP4Muxer.h #import #import #import "KFMuxerConfig.h" NS_ASSUME_NONNULL_BEGIN @interface KFMP4Muxer : NSObject + (instancetype)new NS_UNAVAILABLE; - (instancetype)init NS_UNAVAILABLE; - (instancetype)initWithConfig:(KFMuxerConfig *)config; @property (nonatomic, strong, readonly) KFMuxerConfig *config; @property (nonatomic, copy) void (^errorCallBack)(NSError *error); // 封装错误回调。 - (void)startWriting; // 开始封装写入数据。 - (void)cancelWriting; // 取消封装写入数据。 - (void)appendSampleBuffer:(CMSampleBufferRef)sampleBuffer; // 添加封装数据。 - (void)stopWriting:(void (^)(BOOL success, NSError *error))completeHandler; // 停止封装写入数据。 @end NS_ASSUME_NONNULL_END ``` 上面是 `KFMP4Muxer` 的接口设计,除了`初始化方法`,主要是有`获取封装配置`以及`封装错误回调`的接口,另外就是`开始写入封装数据`、`取消写入封装数据`、`添加封装数据`、`停止写入封装数据`的接口。 在上面的`添加封装数据`接口中,我们使用的是依然 **CMSampleBufferRef**[1] 作为参数类型,再次体现了它作为 `iOS 音视频处理 pipeline 中的流通货币`的通用性。关于这点,我们在[《iOS 音频采集 Demo》](https://mp.weixin.qq.com/s?__biz=MjM5MTkxOTQyMQ==&mid=2257484867&idx=1&sn=d857104930a86de8ab0bdf2358ca6283&scene=21#wechat_redirect)和[《iOS 音频编码 Demo》](https://mp.weixin.qq.com/s?__biz=MjM5MTkxOTQyMQ==&mid=2257484887&idx=1&sn=ac142cbeafddc27f3a8c2902524831c8&scene=21#wechat_redirect)两篇文章中都提到过。 在这个 Demo 里我们通过 `CMSampleBufferRef` 打包的是编码后的 AAC 数据,将其作为输入送给封装模块。 ``` KFMP4Muxer.m #import "KFMP4Muxer.h" #import #define KFMP4MuxerAddOutputError 1000 #define KFMP4MuxerMaxQueueCount 10000 // 封装器的状态机。 typedef NS_ENUM(NSInteger, KFMP4MuxerStatus) { KFMP4MuxerStatusUnknown = 0, KFMP4MuxerStatusRunning = 1, KFMP4MuxerStatusFailed = 2, KFMP4MuxerStatusCompleted = 3, KFMP4MuxerStatusCancelled = 4, }; @interface KFMP4Muxer () { CMSimpleQueueRef _audioQueue; // 音频数据队列。 CMSimpleQueueRef _videoQueue; // 视频数据队列。 } @property (nonatomic, strong, readwrite) KFMuxerConfig *config; @property (nonatomic, strong) AVAssetWriter *muxWriter; // 封装器实例。 @property (nonatomic, strong) AVAssetWriterInput *writerVideoInput; // Muxer 的视频输入。 @property (nonatomic, strong) AVAssetWriterInput *writerAudioInput; // Muxer 的音频输入。 @property (nonatomic, strong) dispatch_queue_t muxerQueue; @property (nonatomic, strong) dispatch_semaphore_t semaphore; @property (nonatomic, assign) KFMP4MuxerStatus muxerStatus; @end @implementation KFMP4Muxer #pragma mark - LifeCycle - (instancetype)initWithConfig:(KFMuxerConfig *)config { self = [super init]; if (self) { _config = config; _muxerQueue = dispatch_queue_create("com.KeyFrameKit.muxerQueue", DISPATCH_QUEUE_SERIAL); // 封装任务队列。 _semaphore = dispatch_semaphore_create(1); CMSimpleQueueCreate(kCFAllocatorDefault, KFMP4MuxerMaxQueueCount, &_audioQueue); CMSimpleQueueCreate(kCFAllocatorDefault, KFMP4MuxerMaxQueueCount, &_videoQueue); } return self; } - (void)dealloc { dispatch_semaphore_wait(_semaphore, DISPATCH_TIME_FOREVER); [self _reset]; // 清理。 dispatch_semaphore_signal(_semaphore); } #pragma mark - Public Method - (void)startWriting { // 开始写入。 __weak typeof(self) weakSelf = self; dispatch_async(self.muxerQueue, ^{ dispatch_semaphore_wait(weakSelf.semaphore, DISPATCH_TIME_FOREVER); [weakSelf _reset]; // 清理。 weakSelf.muxerStatus = KFMP4MuxerStatusRunning; // 标记状态。 dispatch_semaphore_signal(weakSelf.semaphore); }); } - (void)cancelWriting { // 取消写入。 __weak typeof(self) weakSelf = self; dispatch_async(self.muxerQueue, ^{ dispatch_semaphore_wait(weakSelf.semaphore, DISPATCH_TIME_FOREVER); if (weakSelf.muxWriter && weakSelf.muxWriter.status == AVAssetWriterStatusWriting) { [weakSelf.muxWriter cancelWriting]; } weakSelf.muxerStatus = KFMP4MuxerStatusCancelled; // 标记状态。 dispatch_semaphore_signal(weakSelf.semaphore); }); } - (void)appendSampleBuffer:(CMSampleBufferRef)sampleBuffer { if (!sampleBuffer || !CMSampleBufferGetDataBuffer(sampleBuffer) || self.muxerStatus != KFMP4MuxerStatusRunning) { return; } // 异步添加数据。 __weak typeof(self) weakSelf = self; CFRetain(sampleBuffer); dispatch_async(self.muxerQueue, ^{ dispatch_semaphore_wait(weakSelf.semaphore, DISPATCH_TIME_FOREVER); // 1、添加数据到队列。 [weakSelf _enqueueSampleBuffer:sampleBuffer]; // 2、第一次添加数据时,创建 Muxer 实例并触发写数据操作。 if (!weakSelf.muxWriter) { // 检查数据是否正常。队列里面有数据则表示对应的数据携带的音视频格式信息是正常的,这个在初始化 Muxer 的输入源时需要用到。 if (![weakSelf _checkFormatDescriptionLoadSuccess]) { CFRelease(sampleBuffer); dispatch_semaphore_signal(weakSelf.semaphore); return; } // 创建 Muxer 实例。 NSError *error = nil; BOOL success = [weakSelf _setupMuxWriter:&error]; if (!success) { weakSelf.muxerStatus = KFMP4MuxerStatusFailed; CFRelease(sampleBuffer); dispatch_semaphore_signal(weakSelf.semaphore); [weakSelf _callBackError:error]; return; } // 开始封装写入。 success = [weakSelf.muxWriter startWriting]; if (success) { // 启动封装会话,传入数据起始时间。这个起始时间是音视频 pts 的最小值。 [weakSelf.muxWriter startSessionAtSourceTime:[weakSelf _sessionSourceTime]]; } } // 3、检查 Muxer 状态。 if (!weakSelf.muxWriter || weakSelf.muxWriter.status != AVAssetWriterStatusWriting) { weakSelf.muxerStatus = KFMP4MuxerStatusFailed; CFRelease(sampleBuffer); dispatch_semaphore_signal(weakSelf.semaphore); [weakSelf _callBackError:weakSelf.muxWriter.error]; return; } // 4、做音视频数据交织。 [weakSelf _avInterLeavedSample]; CFRelease(sampleBuffer); dispatch_semaphore_signal(weakSelf.semaphore); }); } - (void)stopWriting:(void (^)(BOOL success, NSError *error))completeHandler { // 停止写入。 __weak typeof(self) weakSelf = self; dispatch_async(self.muxerQueue, ^{ dispatch_semaphore_wait(weakSelf.semaphore, DISPATCH_TIME_FOREVER); [weakSelf _stopWriting:^(BOOL success, NSError *error) { weakSelf.muxerStatus = success ? KFMP4MuxerStatusCompleted : KFMP4MuxerStatusFailed; dispatch_semaphore_signal(weakSelf.semaphore); if (completeHandler) { completeHandler(success, error); } }]; }); } #pragma mark - Private Method - (BOOL)_setupMuxWriter:(NSError **)error { if (!self.config.outputURL) { *error = [NSError errorWithDomain:NSStringFromClass([KFMP4Muxer class]) code:40003 userInfo:nil]; return NO; } // 1、清理写入路径的文件。 if ([[NSFileManager defaultManager] fileExistsAtPath:self.config.outputURL.path]) { [[NSFileManager defaultManager] removeItemAtPath:self.config.outputURL.path error:nil]; } // 2、创建封装器实例。 if (_muxWriter) { return YES; } // 使用 AVAssetWriter 作为封装器,类型使用 AVFileTypeMPEG4。M4A 格式是遵循 MPEG4 规范的一种音频格式。 _muxWriter = [[AVAssetWriter alloc] initWithURL:self.config.outputURL fileType:AVFileTypeMPEG4 error:error]; if (*error) { return NO; } _muxWriter.movieTimeScale = 1000000000; _muxWriter.shouldOptimizeForNetworkUse = YES; // 这个选项会将 MP4 的 moov box 前置。 // 3、当封装内容包含视频时,创建 Muxer 的视频输入。 if ((self.config.muxerType & KFMediaVideo) && !_writerVideoInput) { // 从队列中的视频数据里获取视频格式信息,用于初始化视频输入源。 CMVideoFormatDescriptionRef videoDecscription = CMSampleBufferGetFormatDescription((CMSampleBufferRef)CMSimpleQueueGetHead(_videoQueue)); _writerVideoInput = [AVAssetWriterInput assetWriterInputWithMediaType:AVMediaTypeVideo outputSettings:nil sourceFormatHint:videoDecscription]; _writerVideoInput.expectsMediaDataInRealTime = YES; // 输入是否为实时数据源,比如相机采集。 _writerVideoInput.transform = self.config.preferredTransform; // 画面是否做变换。 if ([self.muxWriter canAddInput:self.writerVideoInput]) { [self.muxWriter addInput:self.writerVideoInput]; } else { *error = self.muxWriter.error ? self.muxWriter.error : [NSError errorWithDomain:NSStringFromClass([KFMP4Muxer class]) code:KFMP4MuxerAddOutputError userInfo:nil]; return NO; } } // 4、当封装内容包含音频时,创建 Muxer 的音频输入。 if ((self.config.muxerType & KFMediaAudio) && !_writerAudioInput) { // 从队列中的音频数据里获取音频格式信息,用于初始化音频输入源。 CMAudioFormatDescriptionRef audioDecscription = CMSampleBufferGetFormatDescription((CMSampleBufferRef)CMSimpleQueueGetHead(_audioQueue)); _writerAudioInput = [AVAssetWriterInput assetWriterInputWithMediaType:AVMediaTypeAudio outputSettings:nil sourceFormatHint:audioDecscription]; _writerAudioInput.expectsMediaDataInRealTime = YES; // 输入是否为实时数据源,比如麦克风采集。 if ([self.muxWriter canAddInput:self.writerAudioInput]) { [self.muxWriter addInput:self.writerAudioInput]; } else { *error = self.muxWriter.error ? self.muxWriter.error : [NSError errorWithDomain:NSStringFromClass([KFMP4Muxer class]) code:KFMP4MuxerAddOutputError userInfo:nil]; return NO; } } return YES; } - (void)_enqueueSampleBuffer:(CMSampleBufferRef)sampleBuffer { CFRetain(sampleBuffer); // 音频、视频的格式信息正确才能入队。因为后面创建 Muxer 实例的输入源时也需要从队列中的音视频数据中获取相关格式信息。 if (CMFormatDescriptionGetMediaType(CMSampleBufferGetFormatDescription(sampleBuffer)) == kCMMediaType_Audio) { CMSimpleQueueEnqueue(_audioQueue, sampleBuffer); // 音频数据入队列。 } else if (CMFormatDescriptionGetMediaType(CMSampleBufferGetFormatDescription(sampleBuffer)) == kCMMediaType_Video) { CMSimpleQueueEnqueue(_videoQueue, sampleBuffer); // 视频数据入队列。 } } - (void)_flushMuxer { // 将队列数据消费掉。 [self _appendAudioSample]; [self _appendVideoSample]; } - (void)_appendAudioSample { // 音频写入封装。 while (self.writerAudioInput && self.writerAudioInput.readyForMoreMediaData && CMSimpleQueueGetCount(_audioQueue) > 0) { CMSampleBufferRef audioSample = (CMSampleBufferRef)CMSimpleQueueDequeue(_audioQueue); [self.writerAudioInput appendSampleBuffer:audioSample]; CFRelease(audioSample); } } - (void)_appendVideoSample { // 视频写入封装。 while (self.writerVideoInput && self.writerVideoInput.readyForMoreMediaData && CMSimpleQueueGetCount(_videoQueue) > 0) { CMSampleBufferRef videoSample = (CMSampleBufferRef)CMSimpleQueueDequeue(_videoQueue); [self.writerVideoInput appendSampleBuffer:videoSample]; CFRelease(videoSample); } } - (void)_avInterLeavedSample { // 当同时封装音频和视频时,需要做好交织,这样可以提升音视频播放时的体验。 if ((self.config.muxerType & KFMediaAudio) && (self.config.muxerType & KFMediaVideo)) { // 同时封装音频和视频。 while (CMSimpleQueueGetCount(_audioQueue) > 0 && CMSimpleQueueGetCount(_videoQueue) > 0) { if (self.writerAudioInput.readyForMoreMediaData && self.writerVideoInput.readyForMoreMediaData) { // 音频、视频队列数据各出队 1 个。 CMSampleBufferRef audioHeader = (CMSampleBufferRef)CMSimpleQueueGetHead(_audioQueue); CMTime audioDtsTime = CMSampleBufferGetPresentationTimeStamp(audioHeader); CMSampleBufferRef videoHeader = (CMSampleBufferRef)CMSimpleQueueGetHead(_videoQueue); CMTime videoDtsTime = CMSampleBufferGetDecodeTimeStamp(videoHeader).value > 0 ? CMSampleBufferGetDecodeTimeStamp(videoHeader) : CMSampleBufferGetPresentationTimeStamp(videoHeader); // 比较 dts 较小者写入封装。 if (CMTimeGetSeconds(audioDtsTime) >= CMTimeGetSeconds(videoDtsTime)) { CMSampleBufferRef videoSample = (CMSampleBufferRef)CMSimpleQueueDequeue(_videoQueue); [self.writerVideoInput appendSampleBuffer:videoSample]; CFRelease(videoSample); } else { CMSampleBufferRef audioSample = (CMSampleBufferRef)CMSimpleQueueDequeue(_audioQueue); [self.writerAudioInput appendSampleBuffer:audioSample]; CFRelease(audioSample); } } else { break; } } } else if (self.config.muxerType & KFMediaAudio) { // 只封装音频。 [self _appendAudioSample]; } else if (self.config.muxerType & KFMediaVideo) { // 只封装视频。 [self _appendVideoSample]; } } - (BOOL)_checkFormatDescriptionLoadSuccess { // 检查数据是否正常。 if (!_muxWriter) { if ((self.config.muxerType & KFMediaAudio) && (self.config.muxerType & KFMediaVideo)) { return CMSimpleQueueGetCount(_videoQueue) > 0 && CMSimpleQueueGetCount(_audioQueue) > 0; } else if (self.config.muxerType & KFMediaAudio) { return CMSimpleQueueGetCount(_audioQueue) > 0; } else if (self.config.muxerType & KFMediaVideo) { return CMSimpleQueueGetCount(_videoQueue) > 0; } } return NO; } - (CMTime)_sessionSourceTime { // 数据起始时间:音视频 pts 的最小值。 CMSampleBufferRef audioFirstBuffer = (CMSampleBufferRef)CMSimpleQueueGetHead(_audioQueue); CMSampleBufferRef videoFirstBuffer = (CMSampleBufferRef)CMSimpleQueueGetHead(_videoQueue); if (audioFirstBuffer && videoFirstBuffer) { Float64 audioPtsTime = CMTimeGetSeconds(CMSampleBufferGetPresentationTimeStamp(audioFirstBuffer)); Float64 videoPtsTime = CMTimeGetSeconds(CMSampleBufferGetPresentationTimeStamp(videoFirstBuffer)); return audioPtsTime >= videoPtsTime ? CMSampleBufferGetPresentationTimeStamp(videoFirstBuffer) : CMSampleBufferGetPresentationTimeStamp(audioFirstBuffer); } else if (audioFirstBuffer) { return CMSampleBufferGetPresentationTimeStamp(audioFirstBuffer); } else if (videoFirstBuffer) { return CMSampleBufferGetPresentationTimeStamp(videoFirstBuffer); } return kCMTimeInvalid; } - (void)_stopWriting:(void (^)(BOOL success, NSError *error))completeHandler { // 1、状态不对,回调错误。 if (!self.muxWriter || self.muxWriter.status == AVAssetWriterStatusCompleted || self.muxWriter.status == AVAssetWriterStatusCancelled || self.muxWriter.status == AVAssetWriterStatusUnknown) { if (completeHandler) { completeHandler(NO, self.muxWriter.error ? self.muxWriter.error : [NSError errorWithDomain:NSStringFromClass(self.class) code:self.muxWriter.status userInfo:nil]); } return; } // 2、消费掉队列中剩余的数据。 // 先做剩余数据的音视频交织。 [self _avInterLeavedSample]; // 消费剩余数据。 [self _flushMuxer]; // 3、标记视频输入和音频输入为结束状态。 [self _markVideoAsFinished]; [self _markAudioAsFinished]; // 4、结束写入。 __weak typeof(self) weakSelf = self; [self.muxWriter finishWritingWithCompletionHandler:^{ BOOL complete = weakSelf.muxWriter.status == AVAssetWriterStatusCompleted; if (completeHandler) { completeHandler(complete, complete ? nil : weakSelf.muxWriter.error); } }]; } - (void)_markVideoAsFinished { // 标记视频输入源为结束状态。 if (self.muxWriter.status == AVAssetWriterStatusWriting && self.writerVideoInput) { [self.writerVideoInput markAsFinished]; } } - (void)_markAudioAsFinished { // 标记音频输入源为结束状态。 if (self.muxWriter.status == AVAssetWriterStatusWriting && self.writerAudioInput) { [self.writerAudioInput markAsFinished]; } } - (void)_reset { // 取消写入操作。 if (_muxWriter && _muxWriter.status == AVAssetWriterStatusWriting) { [_muxWriter cancelWriting]; } // 清理实例。 _muxWriter = nil; _writerVideoInput = nil; _writerVideoInput = nil; // 清理音频和视频数据队列。 while (CMSimpleQueueGetCount(_audioQueue) > 0) { CMSampleBufferRef sampleBuffer = (CMSampleBufferRef) CMSimpleQueueDequeue(_audioQueue); CFRelease(sampleBuffer); } while (CMSimpleQueueGetCount(_videoQueue) > 0) { CMSampleBufferRef sampleBuffer = (CMSampleBufferRef) CMSimpleQueueDequeue(_videoQueue); CFRelease(sampleBuffer); } } - (void)_callBackError:(NSError *)error { if (error && self.errorCallBack) { dispatch_async(dispatch_get_main_queue(), ^{ self.errorCallBack(error); }); } } @end ``` 上面是 `KFMP4Muxer` 的实现,从代码上可以看到主要有这几个部分: - 1)创建封装器实例及对应的音频和视频数据输入源。第一次调用 `-appendSampleBuffer:` 添加待封装数据时才会创建封装器实例。 - - 在 `-_setupMuxWriter:` 方法中实现。音频和视频的输入源分别是 `writerAudioInput` 和 `writerVideoInput`。 - 2)用两个队列作为缓冲区,分别管理音频和视频待封装数据。 - - 这两个队列分别是 `_audioQueue` 和 `_videoQueue`。 - 每次当外部调用 `-appendSampleBuffer:` 方法送入待封装数据时,其实都是先调用 `-_enqueueSampleBuffer:` 把数据放入两个队列中的一个,以便根据情况进行后续的音视频数据交织。 - 3)同时封装音频和视频数据时,进行音视频数据交织。 - - 在 `-_avInterLeavedSample` 方法中实现音视频数据交织。当带封装的数据既有音频又有视频,就需要根据他们的时间戳信息进行交织,这样便于在播放该音视频时提升体验。 - 4)音视频数据写入封装。 - - 同时封装音频和视频数据时,在做完音视频交织后,即分别将交织后的音视频数据写入对应的 `writerAudioInput` 和 `writerVideoInput`。在 `-_avInterLeavedSample` 中实现。 - 单独封装音频或视频数据时,则直接将数据写入对应的 `writerAudioInput` 和 `writerVideoInput`。分别在 `-_appendAudioSample` 和 `-_appendVideoSample` 方法中实现。 - 5)停止写入。 - - 在 `-stopWriting:` → `-_stopWriting:` 方法中实现。 - 在停止前,还需要消费掉 `_audioQueue` 和 `_videoQueue` 的剩余数据,要调用 `-_avInterLeavedSample` → `-_flushMuxer`。 - 并将视频输入源和音频输入源标记位结束,分别在 `-_markVideoAsFinished` 和 `-_markAudioAsFinished` 方法中实现。 - 6)贯穿整个封装过程的状态机管理。 - - 在枚举 `KFMP4MuxerStatus` 中定义了封装器的各种状态,对于封装器的状态机管理贯穿在封装的整个过程中。 - 7)错误回调。 - - 在 `-callBackError:` 方法向外回调错误。 - 8)清理封装器实例及数据缓冲区。 - - 在 `-dealloc` 方法中实现。需要调用 `-_reset` 方法清理封装器实例、音频和视频输入源、音频和视频缓冲区。 更具体细节见上述代码及其注释。 ## 4、采集音频数据进行 AAC 编码以及 M4A 封装和存储 我们还是在一个 ViewController 中来实现采集音频数据进行 AAC 编码、M4A 封装和存储的逻辑。 ``` KFAudioCaptureViewController.m #import "KFAudioMuxerViewController.h" #import #import "KFAudioCapture.h" #import "KFAudioEncoder.h" #import "KFMP4Muxer.h" @interface KFAudioMuxerViewController () @property (nonatomic, strong) KFAudioConfig *audioConfig; @property (nonatomic, strong) KFAudioCapture *audioCapture; @property (nonatomic, strong) KFAudioEncoder *audioEncoder; @property (nonatomic, strong) KFMuxerConfig *muxerConfig; @property (nonatomic, strong) KFMP4Muxer *muxer; @end @implementation KFAudioMuxerViewController #pragma mark - Property - (KFAudioConfig *)audioConfig { if (!_audioConfig) { _audioConfig = [KFAudioConfig defaultConfig]; } return _audioConfig; } - (KFAudioCapture *)audioCapture { if (!_audioCapture) { __weak typeof(self) weakSelf = self; _audioCapture = [[KFAudioCapture alloc] initWithConfig:self.audioConfig]; _audioCapture.errorCallBack = ^(NSError* error) { NSLog(@"KFAudioCapture error:%zi %@", error.code, error.localizedDescription); }; // 音频采集数据回调。在这里采集的 PCM 数据送给编码器。 _audioCapture.sampleBufferOutputCallBack = ^(CMSampleBufferRef sampleBuffer) { [weakSelf.audioEncoder encodeSampleBuffer:sampleBuffer]; }; } return _audioCapture; } - (KFAudioEncoder *)audioEncoder { if (!_audioEncoder) { __weak typeof(self) weakSelf = self; _audioEncoder = [[KFAudioEncoder alloc] initWithAudioBitrate:96000]; _audioEncoder.errorCallBack = ^(NSError* error) { NSLog(@"KFAudioEncoder error:%zi %@", error.code, error.localizedDescription); }; // 音频编码数据回调。这里编码的 AAC 数据送给封装器。 // 与之前将编码后的 AAC 数据存储为 AAC 文件不同的是,这里编码后送给封装器的 AAC 数据是没有添加 ADTS 头的,因为我们这里封装的是 M4A 格式,不需要 ADTS 头。 _audioEncoder.sampleBufferOutputCallBack = ^(CMSampleBufferRef sampleBuffer) { [weakSelf.muxer appendSampleBuffer:sampleBuffer]; }; } return _audioEncoder; } - (KFMuxerConfig *)muxerConfig { if (!_muxerConfig) { _muxerConfig = [[KFMuxerConfig alloc] init]; NSString *audioPath = [[NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) lastObject] stringByAppendingPathComponent:@"test.m4a"]; NSLog(@"M4A file path: %@", audioPath); [[NSFileManager defaultManager] removeItemAtPath:audioPath error:nil]; _muxerConfig.outputURL = [NSURL fileURLWithPath:audioPath]; _muxerConfig.muxerType = KFMediaAudio; } return _muxerConfig; } - (KFMP4Muxer *)muxer { if (!_muxer) { _muxer = [[KFMP4Muxer alloc] initWithConfig:self.muxerConfig]; _muxer.errorCallBack = ^(NSError* error) { NSLog(@"KFMP4Muxer error:%zi %@", error.code, error.localizedDescription); }; } return _muxer; } #pragma mark - Lifecycle - (void)viewDidLoad { [super viewDidLoad]; [self setupAudioSession]; [self setupUI]; // 完成音频编码后,可以将 App Document 文件夹下面的 test.m4a 文件拷贝到电脑上,使用 ffplay 播放: // ffplay -i test.m4a } #pragma mark - Setup - (void)setupUI { self.edgesForExtendedLayout = UIRectEdgeAll; self.extendedLayoutIncludesOpaqueBars = YES; self.title = @"Audio Muxer"; self.view.backgroundColor = [UIColor whiteColor]; // Navigation item. UIBarButtonItem *startBarButton = [[UIBarButtonItem alloc] initWithTitle:@"Start" style:UIBarButtonItemStylePlain target:self action:@selector(start)]; UIBarButtonItem *stopBarButton = [[UIBarButtonItem alloc] initWithTitle:@"Stop" style:UIBarButtonItemStylePlain target:self action:@selector(stop)]; self.navigationItem.rightBarButtonItems = @[startBarButton, stopBarButton]; } - (void)setupAudioSession { NSError *error = nil; // 1、获取音频会话实例。 AVAudioSession *session = [AVAudioSession sharedInstance]; // 2、设置分类和选项。 [session setCategory:AVAudioSessionCategoryPlayAndRecord withOptions:AVAudioSessionCategoryOptionMixWithOthers | AVAudioSessionCategoryOptionDefaultToSpeaker error:&error]; if (error) { NSLog(@"AVAudioSession setCategory error."); error = nil; return; } // 3、设置模式。 [session setMode:AVAudioSessionModeVideoRecording error:&error]; if (error) { NSLog(@"AVAudioSession setMode error."); error = nil; return; } // 4、激活会话。 [session setActive:YES error:&error]; if (error) { NSLog(@"AVAudioSession setActive error."); error = nil; return; } } #pragma mark - Action - (void)start { // 启动采集器。 [self.audioCapture startRunning]; // 启动封装器。 [self.muxer startWriting]; } - (void)stop { // 停止采集器。 [self.audioCapture stopRunning]; // 停止封装器。 [self.muxer stopWriting:^(BOOL success, NSError * _Nonnull error) { NSLog(@"KFMP4Muxer %@", success ? @"success" : [NSString stringWithFormat:@"error %zi %@", error.code, error.localizedDescription]); }]; } @end ``` 上面是 `KFAudioMuxerViewController` 的实现,其中主要包含这几个部分: - 1)在采集音频前需要设置 **AVAudioSession**[2] 为正确的采集模式。 - - 在 `-setupAudioSession` 中实现。 - 2)通过启动和关闭音频采集和封装来驱动整个采集、编码、封装流程。 - - 分别在 `-start` 和 `-stop` 中实现开始和停止动作。 - 3)在采集模块 `KFAudioCapture` 的数据回调中将数据交给编码模块 `KFAudioEncoder` 进行编码。 - - 在 `KFAudioCapture` 的 `sampleBufferOutputCallBack` 回调中实现。 - 4)在编码模块 `KFAudioEncoder` 的数据回调中获取编码后的 AAC 裸流数据,并将数据交给封装器 `KFMP4Muxer` 进行封装。 - - 在 `KFAudioEncoder` 的 `sampleBufferOutputCallBack` 回调中实现。 - 5)在调用 `-stop` 停止整个流程后,如果没有出现错误,封装的 M4A 文件会被存储到 `muxerConfig` 设置的路径。 ## 5、用工具播放 M4A 文件 完成音频采集和编码后,可以将 App Document 文件夹下面的 `test.m4a` 文件拷贝到电脑上,使用 `ffplay` 播放来验证一下音频采集是效果是否符合预期: ``` $ ffplay -i test.m4a ``` 关于播放 M4A 文件的工具,可以参考[《FFmpeg 工具》第 2 节 ffplay 命令行工具](https://mp.weixin.qq.com/s?__biz=MjM5MTkxOTQyMQ==&mid=2257484831&idx=1&sn=6bab905a5040c46b971bab05f787788b&scene=21#wechat_redirect)和[《可视化音视频分析工具》第 1.1 节 Adobe Audition](https://mp.weixin.qq.com/s?__biz=MjM5MTkxOTQyMQ==&mid=2257484834&idx=1&sn=5dd9768bfc0d01ca1b036be8dd2f5fa1&scene=21#wechat_redirect)。 上面我们讲过 M4A 格式是属于 MPEG-4 标准,所以我们这里还可以用[《可视化音视频分析工具》第 3.1 节 MP4Box.js](https://mp.weixin.qq.com/s?__biz=MjM5MTkxOTQyMQ==&mid=2257484834&idx=1&sn=5dd9768bfc0d01ca1b036be8dd2f5fa1&scene=21#wechat_redirect) 等工具来查看它的格式: ![图片](https://mmbiz.qpic.cn/mmbiz_png/gUnqKPeSuejDJHRicNdoGX06V5TeO2y8kKRhgQmZzal2dlyNdiaVRalLv4KHU1BlpTFPX4aS7oKqCM0jG7hVjW1w/640?wx_fmt=png&wxfrom=5&wx_lazy=1&wx_co=1)Demo 生成的 M4A 文件结构 ## 6、参考资料 [1]CMSampleBufferRef: *https://developer.apple.com/documentation/coremedia/cmsamplebufferref/* [2]AVAudioSession: *https://developer.apple.com/documentation/avfaudio/avaudiosession/* 原文链接:https://mp.weixin.qq.com/s/R86qnQAi2njr6k7tFvTF-w ================================================ FILE: iOS资料/iOS AVDemo:音频编码,采集 PCM 数据编码为 AAC.md ================================================ # iOS AVDemo:音频编码,采集 PCM 数据编码为 AAC iOS/Android 客户端开发同学如果想要开始学习音视频开发,最丝滑的方式是对[音视频基础概念知识](https://mp.weixin.qq.com/mp/appmsgalbum?__biz=MjM5MTkxOTQyMQ==&action=getalbum&album_id=2140155659944787969#wechat_redirect)有一定了解后,再借助 本地平台的音视频能力上手去实践音视频的`采集 → 编码 → 封装 → 解封装 → 解码 → 渲染`过程,并借助[音视频工具](https://mp.weixin.qq.com/mp/appmsgalbum?__biz=MjM5MTkxOTQyMQ==&action=getalbum&album_id=2216997905264082945#wechat_redirect)来分析和理解对应的音视频数据。 在[音视频工程示例](https://mp.weixin.qq.com/mp/appmsgalbum?__biz=MjM5MTkxOTQyMQ==&action=getalbum&album_id=2273301900659851268#wechat_redirect)这个栏目,我们将通过拆解`采集 → 编码 → 封装 → 解封装 → 解码 → 渲染`流程并实现 Demo 来向大家介绍如何在 iOS/Android 平台上手音视频开发。 这里是第二篇:**iOS 音频编码 Demo**。这个 Demo 里包含以下内容: - 1)实现一个音频采集模块; - 2)实现一个音频编码模块; - 3)串联音频采集和编码模块,将采集到的音频数据输入给 AAC 编码模块进行编码和存储; - 4)详尽的代码注释,帮你理解代码逻辑和原理。 想要了解 AAC 编码,可以看看[《音频编码:PCM 和 AAC 编码》](https://mp.weixin.qq.com/s?__biz=MjM5MTkxOTQyMQ==&mid=2257484452&idx=1&sn=3b5fdd120be300b62a5334c073a9fcbf&scene=21#wechat_redirect)。 ## 1、音频采集模块 在这个 Demo 中,音频采集模块 `KFAudioCapture` 的实现与 [《iOS 音频采集 Demo》](https://mp.weixin.qq.com/s?__biz=MjM5MTkxOTQyMQ==&mid=2257484867&idx=1&sn=d857104930a86de8ab0bdf2358ca6283&scene=21#wechat_redirect) 中一样,这里就不再重复介绍了,其接口如下: ``` KFAudioCapture.h #import #import #import "KFAudioConfig.h" NS_ASSUME_NONNULL_BEGIN @interface KFAudioCapture : NSObject + (instancetype)new NS_UNAVAILABLE; - (instancetype)init NS_UNAVAILABLE; - (instancetype)initWithConfig:(KFAudioConfig *)config; @property (nonatomic, strong, readonly) KFAudioConfig *config; @property (nonatomic, copy) void (^sampleBufferOutputCallBack)(CMSampleBufferRef sample); // 音频采集数据回调。 @property (nonatomic, copy) void (^errorCallBack)(NSError *error); // 音频采集错误回调。 - (void)startRunning; // 开始采集音频数据。 - (void)stopRunning; // 停止采集音频数据。 @end NS_ASSUME_NONNULL_END ``` ## 2、音频编码模块 接下来,我们来实现一个音频编码模块 `KFAudioEncoder`,在这里输入采集后的数据,输出编码后的数据。 ``` KFAudioEncoder.h #import #import NS_ASSUME_NONNULL_BEGIN @interface KFAudioEncoder : NSObject + (instancetype)new NS_UNAVAILABLE; - (instancetype)init NS_UNAVAILABLE; - (instancetype)initWithAudioBitrate:(NSInteger)audioBitrate; @property (nonatomic, assign, readonly) NSInteger audioBitrate; // 音频编码码率。 @property (nonatomic, copy) void (^sampleBufferOutputCallBack)(CMSampleBufferRef sample); // 音频编码数据回调。 @property (nonatomic, copy) void (^errorCallBack)(NSError *error); // 音频编码错误回调。 - (void)encodeSampleBuffer:(CMSampleBufferRef)buffer; // 编码。 @end NS_ASSUME_NONNULL_END ``` 上面是 `KFAudioEncoder` 接口的设计,除了`初始化方法`,主要是有`获取音频编码码率`以及音频编码`数据回调`和`错误回调`的接口,另外就是`编码`的接口。 其中`编码`接口对应着音频编码模块输入,`数据回调`接口则对应着输出。可以看到这里输入输出的参数都是 **CMSampleBufferRef**[1] 这个数据结构。它是对 `CMSampleBuffer` 的一个引用。 `CMSampleBuffer` 是 iOS 系统用来在音视频处理的 pipeline 中使用和传递媒体采样数据的核心数据结构。你可以认为它是 iOS 音视频处理 pipeline 中的流通货币,摄像头采集的视频数据接口、麦克风采集的音频数据接口、编码和解码数据接口、读取和存储视频接口、视频渲染接口等等,都以它作为参数。我们在 [《iOS 音频采集 Demo》](https://mp.weixin.qq.com/s?__biz=MjM5MTkxOTQyMQ==&mid=2257484867&idx=1&sn=d857104930a86de8ab0bdf2358ca6283&scene=21#wechat_redirect) 一文中介绍音频采集接口的时候详细介绍过 `CMSampleBuffer`,可以去看看回顾一下。 所以,在这里我们也以 `CMSampleBufferRef` 作为编码模块输入和输出的接口参数。 ``` KFAudioEncoder.m #import "KFAudioEncoder.h" #import @interface KFAudioEncoder () { char *_leftBuffer; // 待编码缓冲区。 NSInteger _leftLength; // 待编码缓冲区的长度,动态。 char *_aacBuffer; // 编码缓冲区。 NSInteger _bufferLength; // 每次送给编码器的数据长度。 } @property (nonatomic, assign) AudioConverterRef audioEncoderInstance; // 音频编码器实例。 @property (nonatomic, assign) CMFormatDescriptionRef aacFormat; // 音频编码参数。 @property (nonatomic, assign, readwrite) NSInteger audioBitrate; // 音频编码码率。 @property (nonatomic, assign) BOOL isError; @property (nonatomic, strong) dispatch_queue_t encoderQueue; @end @implementation KFAudioEncoder #pragma mark - Lifecycle - (instancetype)initWithAudioBitrate:(NSInteger)audioBitrate { self = [super init]; if (self) { _audioBitrate = audioBitrate; _encoderQueue = dispatch_queue_create("com.KeyFrameKit.audioEncoder", DISPATCH_QUEUE_SERIAL); } return self; } - (void)dealloc { // 清理编码器。 if (_audioEncoderInstance) { AudioConverterDispose(_audioEncoderInstance); _audioEncoderInstance = nil; } if (_aacFormat) { CFRelease(_aacFormat); _aacFormat = NULL; } // 清理缓冲区。 if (_aacBuffer) { free(_aacBuffer); _aacBuffer = NULL; } if (_leftBuffer) { free(_leftBuffer); _leftBuffer = NULL; } } #pragma mark - Utility - (void)setupAudioEncoderInstanceWithInputAudioFormat:(AudioStreamBasicDescription)inputFormat error:(NSError **)error { // 1、设置音频编码器输出参数。其中一些参数与输入的音频数据参数一致。 AudioStreamBasicDescription outputFormat = {0}; outputFormat.mSampleRate = inputFormat.mSampleRate; // 输出采样率与输入一致。 outputFormat.mFormatID = kAudioFormatMPEG4AAC; // AAC 编码格式。常用的 AAC 编码格式:kAudioFormatMPEG4AAC、kAudioFormatMPEG4AAC_HE_V2。 outputFormat.mChannelsPerFrame = (UInt32) inputFormat.mChannelsPerFrame; // 输出声道数与输入一致。 outputFormat.mFramesPerPacket = 1024; // 每个包的帧数。AAC 固定是 1024,这个是由 AAC 编码规范规定的。对于未压缩数据设置为 1。 outputFormat.mBytesPerPacket = 0; // 每个包的大小。动态大小设置为 0。 outputFormat.mBytesPerFrame = 0; // 每帧的大小。压缩格式设置为 0。 outputFormat.mBitsPerChannel = 0; // 压缩格式设置为 0。 // 2、基于音频输入和输出参数创建音频编码器。 OSStatus result = AudioConverterNew(&inputFormat, &outputFormat, &_audioEncoderInstance); if (result != noErr) { *error = [NSError errorWithDomain:NSStringFromClass(self.class) code:result userInfo:nil]; return; } // 3、设置编码器参数:音频编码码率。 UInt32 outputBitrate = (UInt32) self.audioBitrate; result = AudioConverterSetProperty(_audioEncoderInstance, kAudioConverterEncodeBitRate, sizeof(outputBitrate), &outputBitrate); if (result != noErr) { *error = [NSError errorWithDomain:NSStringFromClass(self.class) code:result userInfo:nil]; return; } // 4、创建编码格式信息。 result = CMAudioFormatDescriptionCreate(kCFAllocatorDefault, &outputFormat, 0, NULL, 0, NULL, nil, &_aacFormat); if (result != noErr) { *error = [NSError errorWithDomain:NSStringFromClass(self.class) code:result userInfo:nil]; return; } // 5、设置每次送给编码器的数据长度。 // 这里设置每次送给编码器的数据长度为:1024 * 2(16 bit 采样深度) * 声道数量,这个长度为什么要这么计算呢? // 因为我们每次调用 AudioConverterFillComplexBuffer 编码时,是送进去一个包(packet),而对于 AAC 来讲,mFramesPerPacket 需要是 1024,即 1 个 packet 有 1024 帧,而每个音频帧的大小是:2(16 bit 采样深度) * 声道数量。 _bufferLength = 1024 * 2 * inputFormat.mChannelsPerFrame; // 6、初始化待编码缓冲区和编码缓冲区。 if (!_leftBuffer) { // 待编码缓冲区长度达到 _bufferLength,就会送一波给编码器,所以大小 _bufferLength 够用了。 _leftBuffer = malloc(_bufferLength); } if (!_aacBuffer) { // AAC 编码缓冲区只要装得下 _bufferLength 长度的 PCM 数据编码后的数据就好了,编码是压缩,所以大小 _bufferLength 也够用了。 _aacBuffer = malloc(_bufferLength); } } - (void)encodeSampleBuffer:(CMSampleBufferRef)buffer { if (!buffer || !CMSampleBufferGetDataBuffer(buffer) || self.isError) { return; } // 异步处理,防止主线程卡顿。 __weak typeof(self) weakSelf = self; CFRetain(buffer); dispatch_async(_encoderQueue, ^{ [weakSelf encodeSampleBufferInternal:buffer]; CFRelease(buffer); }); } - (void)encodeSampleBufferInternal:(CMSampleBufferRef)buffer { // 1、从输入数据中获取音频格式信息。 CMAudioFormatDescriptionRef audioFormatRef = CMSampleBufferGetFormatDescription(buffer); if (!audioFormatRef) { return; } // 获取音频参数信息,AudioStreamBasicDescription 包含了音频的数据格式、声道数、采样位深、采样率等参数。 AudioStreamBasicDescription audioFormat = *CMAudioFormatDescriptionGetStreamBasicDescription(audioFormatRef); // 2、根据音频参数创建编码器实例。 NSError *error = nil; // 第一次编码时创建编码器。 if (!_audioEncoderInstance) { [self setupAudioEncoderInstanceWithInputAudioFormat:audioFormat error:&error]; if (error) { [self callBackError:error]; return; } if (!_audioEncoderInstance) { return; } } // 3、获取输入数据中的 PCM 数据。 CMBlockBufferRef blockBuffer = CMSampleBufferGetDataBuffer(buffer); size_t audioLength; char *dataPointer = NULL; CMBlockBufferGetDataPointer(blockBuffer, 0, NULL, &audioLength, &dataPointer); if (audioLength == 0 || !dataPointer) { return; } // 4、处理音频时间戳信息。 CMSampleTimingInfo timingInfo = {CMTimeMake(CMSampleBufferGetNumSamples(buffer), audioFormat.mSampleRate), CMSampleBufferGetPresentationTimeStamp(buffer), kCMTimeInvalid}; // 5、基于编码缓冲区对 PCM 数据进行编码。 if (_leftLength + audioLength >= _bufferLength) { // 当待编码缓冲区遗留数据加上新来的数据长度(_leftLength + audioLength)大于每次给编码器的数据长度(_bufferLength)时,则进行循环编码,每次送给编码器长度为 _bufferLength 的数据量。 // 拷贝待编码的数据到缓冲区 totalBuffer。 NSInteger totalSize = _leftLength + audioLength; // 当前总数据长度。 NSInteger encodeCount = totalSize / _bufferLength; // 计算给编码器送数据的次数。 char *totalBuffer = malloc(totalSize); char *p = totalBuffer; memset(totalBuffer, 0, (int) totalSize); memcpy(totalBuffer, _leftBuffer, _leftLength); // 拷贝上次遗留的数据。 memcpy(totalBuffer + _leftLength, dataPointer, audioLength); // 拷贝这次新来的数据。 // 分 encodeCount 次给编码器送数据。 for (NSInteger index = 0; index < encodeCount; index++) { [self encodeBuffer:p timing:timingInfo]; // 调用编码方法。 p += _bufferLength; } // 处理不够 _bufferLength 长度的剩余数据,先存在 _leftBuffer 中,等下次凑足一次编码需要的数据再编码。 _leftLength = totalSize % _bufferLength; memset(_leftBuffer, 0, _bufferLength); memcpy(_leftBuffer, totalBuffer + (totalSize - _leftLength), _leftLength); // 清理。 free(totalBuffer); } else { // 否则,就先存到待编码缓冲区,等下一次数据够了再送给编码器。 memcpy(_leftBuffer + _leftLength, dataPointer, audioLength); _leftLength = _leftLength + audioLength; } } - (void)encodeBuffer:(char *)buffer timing:(CMSampleTimingInfo)timing { // 1、创建编码器接口对应的待编码缓冲区 AudioBufferList,填充待编码的数据。 AudioBuffer inBuffer; AudioStreamBasicDescription audioFormat = *CMAudioFormatDescriptionGetStreamBasicDescription(_aacFormat); inBuffer.mNumberChannels = (UInt32) audioFormat.mChannelsPerFrame; inBuffer.mData = buffer; // 填充待编码数据。 inBuffer.mDataByteSize = (UInt32) _bufferLength; // 设置待编码数据长度。 AudioBufferList inBufferList; inBufferList.mNumberBuffers = 1; inBufferList.mBuffers[0] = inBuffer; // 2、创建编码输出缓冲区 AudioBufferList 接收编码后的数据。 AudioBufferList outBufferList; outBufferList.mNumberBuffers = 1; outBufferList.mBuffers[0].mNumberChannels = inBuffer.mNumberChannels; outBufferList.mBuffers[0].mDataByteSize = inBuffer.mDataByteSize; // 设置编码缓冲区大小。 outBufferList.mBuffers[0].mData = _aacBuffer; // 绑定缓冲区空间。 // 3、编码。 UInt32 outputDataPacketSize = 1; // 每次编码 1 个包。1 个包有 1024 个帧,这个对应创建编码器实例时设置的 mFramesPerPacket。 // 需要在回调方法 inputDataProcess 中将待编码的数据拷贝到编码器的缓冲区的对应位置。这里把我们自己创建的待编码缓冲区 AudioBufferList 作为 inInputDataProcUserData 传入,在回调方法中直接拷贝它。 OSStatus status = AudioConverterFillComplexBuffer(_audioEncoderInstance, inputDataProcess, &inBufferList, &outputDataPacketSize, &outBufferList, NULL); if (status != noErr) { [self callBackError:[NSError errorWithDomain:NSStringFromClass(self.class) code:status userInfo:nil]]; return; } // 4、获取编码后的 AAC 数据并进行封装。 size_t aacEncoderSize = outBufferList.mBuffers[0].mDataByteSize; char *blockBufferDataPoter = malloc(aacEncoderSize); memcpy(blockBufferDataPoter, _aacBuffer, aacEncoderSize); // 编码数据封装到 CMBlockBuffer 中。 CMBlockBufferRef blockBuffer = NULL; status = CMBlockBufferCreateWithMemoryBlock(kCFAllocatorDefault, blockBufferDataPoter, aacEncoderSize, NULL, NULL, 0, aacEncoderSize, 0, &blockBuffer); if (status != kCMBlockBufferNoErr) { return; } // 编码数据 CMBlockBuffer 再封装到 CMSampleBuffer 中。 CMSampleBufferRef sampleBuffer = NULL; const size_t sampleSizeArray[] = {aacEncoderSize}; status = CMSampleBufferCreateReady(kCFAllocatorDefault, blockBuffer, _aacFormat, 1, 1, &timing, 1, sampleSizeArray, &sampleBuffer); CFRelease(blockBuffer); // 5、回调编码数据。 if (self.sampleBufferOutputCallBack) { self.sampleBufferOutputCallBack(sampleBuffer); } if (sampleBuffer) { CFRelease(sampleBuffer); } } - (void)callBackError:(NSError*)error { self.isError = YES; if(error && self.errorCallBack){ dispatch_async(dispatch_get_main_queue(), ^{ self.errorCallBack(error); }); } } #pragma mark - Encoder CallBack static OSStatus inputDataProcess(AudioConverterRef inConverter, UInt32 *ioNumberDataPackets, AudioBufferList *ioData, AudioStreamPacketDescription **outDataPacketDescription, void *inUserData) { // 将待编码的数据拷贝到编码器的缓冲区的对应位置进行编码。 AudioBufferList bufferList = *(AudioBufferList *) inUserData; ioData->mBuffers[0].mNumberChannels = 1; ioData->mBuffers[0].mData = bufferList.mBuffers[0].mData; ioData->mBuffers[0].mDataByteSize = bufferList.mBuffers[0].mDataByteSize; return noErr; } @end ``` 上面是 `KFAudioEncoder` 的实现,从代码上可以看到主要有这几个部分: - 1)创建音频编码实例。第一次调用 `-encodeSampleBuffer:` → `-encodeSampleBufferInternal:` 才会创建音频编码实例。 - - 在 `-setupAudioEncoderInstanceWithInputAudioFormat:` 方法中实现。 - 2)实现音频编码逻辑,并在将数据封装到 `CMSampleBufferRef` 结构中,抛给 KFAudioEncoder 的对外数据回调接口。 - - 在 `-encodeSampleBuffer:` → `-encodeSampleBufferInternal:` → `-encodeBuffer:timing:` 中实现编码流程,其中涉及到待编码缓冲区、编码缓冲区的管理,并最终在 `inputDataProcess(...)` 回调中将待编码的数据拷贝到编码器的缓冲区进行编码。 - 3)捕捉音频编码过程中的错误,抛给 KFAudioEncoder 的对外错误回调接口。 - - 在 `-encodeSampleBufferInternal:`、`-encodeBuffer:timing:` 等方法中捕捉错误,在 `-callBackError:` 方法向外回调。 - 4)清理音频编码器实例、待编码缓冲区、编码缓冲区。 - - 在 `-dealloc` 方法中实现。 更具体细节见上述代码及其注释。 ## 3、采集音频数据进行 AAC 编码和存储 我们在一个 ViewController 中来实现音频采集及编码逻辑,并将编码后的数据加上 **ADTS**[2] 头信息存储为 AAC 数据。 关于 ADTS,在[《音频编码:PCM 和 AAC 编码》](https://mp.weixin.qq.com/s?__biz=MjM5MTkxOTQyMQ==&mid=2257484452&idx=1&sn=3b5fdd120be300b62a5334c073a9fcbf&scene=21#wechat_redirect)中也有介绍,可以去看看了解一下。 ``` #import "KFAudioEncoderViewController.h" #import #import "KFAudioCapture.h" #import "KFAudioEncoder.h" #import "KFAudioTools.h" @interface KFAudioEncoderViewController () @property (nonatomic, strong) KFAudioConfig *audioConfig; @property (nonatomic, strong) KFAudioCapture *audioCapture; @property (nonatomic, strong) KFAudioEncoder *audioEncoder; @property (nonatomic, strong) NSFileHandle *fileHandle; @end @implementation KFAudioEncoderViewController #pragma mark - Property - (KFAudioConfig *)audioConfig { if (!_audioConfig) { _audioConfig = [KFAudioConfig defaultConfig]; } return _audioConfig; } - (KFAudioCapture *)audioCapture { if (!_audioCapture) { __weak typeof(self) weakSelf = self; _audioCapture = [[KFAudioCapture alloc] initWithConfig:self.audioConfig]; _audioCapture.errorCallBack = ^(NSError* error) { NSLog(@"KFAudioCapture error:%zi %@", error.code, error.localizedDescription); }; // 音频采集数据回调。在这里采集的 PCM 数据送给编码器。 _audioCapture.sampleBufferOutputCallBack = ^(CMSampleBufferRef sampleBuffer) { [weakSelf.audioEncoder encodeSampleBuffer:sampleBuffer]; }; } return _audioCapture; } - (KFAudioEncoder *)audioEncoder { if (!_audioEncoder) { __weak typeof(self) weakSelf = self; _audioEncoder = [[KFAudioEncoder alloc] initWithAudioBitrate:96000]; _audioEncoder.errorCallBack = ^(NSError* error) { NSLog(@"KFAudioEncoder error:%zi %@", error.code, error.localizedDescription); }; // 音频编码数据回调。在这里将 AAC 数据写入文件。 _audioEncoder.sampleBufferOutputCallBack = ^(CMSampleBufferRef sampleBuffer) { if (sampleBuffer) { // 1、获取音频编码参数信息。 AudioStreamBasicDescription audioFormat = *CMAudioFormatDescriptionGetStreamBasicDescription(CMSampleBufferGetFormatDescription(sampleBuffer)); // 2、获取音频编码数据。AAC 裸数据。 CMBlockBufferRef blockBuffer = CMSampleBufferGetDataBuffer(sampleBuffer); size_t totolLength; char *dataPointer = NULL; CMBlockBufferGetDataPointer(blockBuffer, 0, NULL, &totolLength, &dataPointer); if (totolLength == 0 || !dataPointer) { return; } // 3、在每个 AAC packet 前先写入 ADTS 头数据。 // 由于 AAC 数据存储文件时需要在每个包(packet)前添加 ADTS 头来用于解码器解码音频流,所以这里添加一下 ADTS 头。 [weakSelf.fileHandle writeData:[KFAudioTools adtsDataWithChannels:audioFormat.mChannelsPerFrame sampleRate:audioFormat.mSampleRate rawDataLength:totolLength]]; // 4、写入 AAC packet 数据。 [weakSelf.fileHandle writeData:[NSData dataWithBytes:dataPointer length:totolLength]]; } }; } return _audioEncoder; } - (NSFileHandle *)fileHandle { if (!_fileHandle) { NSString *audioPath = [[NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) lastObject] stringByAppendingPathComponent:@"test.aac"]; NSLog(@"AAC file path: %@", audioPath); [[NSFileManager defaultManager] removeItemAtPath:audioPath error:nil]; [[NSFileManager defaultManager] createFileAtPath:audioPath contents:nil attributes:nil]; _fileHandle = [NSFileHandle fileHandleForWritingAtPath:audioPath]; } return _fileHandle; } #pragma mark - Lifecycle - (void)viewDidLoad { [super viewDidLoad]; [self setupAudioSession]; [self setupUI]; // 完成音频编码后,可以将 App Document 文件夹下面的 test.aac 文件拷贝到电脑上,使用 ffplay 播放: // ffplay -i test.aac } - (void)dealloc { if (_fileHandle) { [_fileHandle closeFile]; } } #pragma mark - Setup - (void)setupUI { self.edgesForExtendedLayout = UIRectEdgeAll; self.extendedLayoutIncludesOpaqueBars = YES; self.title = @"Audio Encoder"; self.view.backgroundColor = [UIColor whiteColor]; // Navigation item. UIBarButtonItem *startBarButton = [[UIBarButtonItem alloc] initWithTitle:@"Start" style:UIBarButtonItemStylePlain target:self action:@selector(start)]; UIBarButtonItem *stopBarButton = [[UIBarButtonItem alloc] initWithTitle:@"Stop" style:UIBarButtonItemStylePlain target:self action:@selector(stop)]; self.navigationItem.rightBarButtonItems = @[startBarButton, stopBarButton]; } - (void)setupAudioSession { NSError *error = nil; // 1、获取音频会话实例。 AVAudioSession *session = [AVAudioSession sharedInstance]; // 2、设置分类和选项。 [session setCategory:AVAudioSessionCategoryPlayAndRecord withOptions:AVAudioSessionCategoryOptionMixWithOthers | AVAudioSessionCategoryOptionDefaultToSpeaker error:&error]; if (error) { NSLog(@"AVAudioSession setCategory error."); error = nil; return; } // 3、设置模式。 [session setMode:AVAudioSessionModeVideoRecording error:&error]; if (error) { NSLog(@"AVAudioSession setMode error."); error = nil; return; } // 4、激活会话。 [session setActive:YES error:&error]; if (error) { NSLog(@"AVAudioSession setActive error."); error = nil; return; } } #pragma mark - Action - (void)start { [self.audioCapture startRunning]; } - (void)stop { [self.audioCapture stopRunning]; } @end ``` 上面是 `KFAudioEncoderViewController` 的实现,其中主要包含这几个部分: - 1)在采集音频前需要设置 **AVAudioSession**[3] 为正确的采集模式。 - - 在 `-setupAudioSession` 中实现。 - 2)通过启动和停止音频采集来驱动整个采集和编码流程。 - - 分别在 `-start` 和 `-stop` 中实现开始和停止动作。 - 3)在采集模块 `KFAudioCapture` 的数据回调中将数据交给编码模块 `KFAudioEncoder` 进行编码。 - - 在 `KFAudioCapture` 的 `sampleBufferOutputCallBack` 回调中实现。 - 4)在编码模块 `KFAudioEncoder` 的数据回调中获取编码后的 AAC 裸流数据,并在每个 AAC packet 前写入 ADTS 头数据,存储到文件中。 - - 在 `KFAudioEncoder` 的 `sampleBufferOutputCallBack` 回调中实现。 - 其中生成一个 AAC packet 对应的 ADTS 头数据在 `KFAudioTools` 类的工具方法 `+adtsDataWithChannels:sampleRate:rawDataLength:` 中实现。 ``` KFAudioTools.m #import "KFAudioTools.h" @implementation KFAudioTools // 按音频参数生产 AAC packet 对应的 ADTS 头数据。 // 当编码器编码的是 AAC 裸流数据时,需要在每个 AAC packet 前添加一个 ADTS 头用于解码器解码音频流。 // 参考文档: // ADTS 格式参考:http://wiki.multimedia.cx/index.php?title=ADTS // MPEG-4 Audio 格式参考:http://wiki.multimedia.cx/index.php?title=MPEG-4_Audio#Channel_Configurations + (NSData *)adtsDataWithChannels:(NSInteger)channels sampleRate:(NSInteger)sampleRate rawDataLength:(NSInteger)rawDataLength { // 1、创建数据缓冲区。 int adtsLength = 7; // ADTS 头固定 7 字节。 char *packet = malloc(sizeof(char) * adtsLength); // 2、设置各数据字段。 int profile = 2; // 2 表示 AAC LC。 NSInteger sampleRateIndex = [self.class sampleRateIndex:sampleRate]; // 取得采样率对应的 index。 int channelCfg = (int) channels; // MPEG-4 Audio Channel Configuration。 NSUInteger fullLength = adtsLength + rawDataLength; // 这里的长度字段是:ADTS 头数据和 AAC packet 数据的总长度。 // 3、填充 ADTS 数据。 packet[0] = (char) 0xFF; // 11111111 = syncword packet[1] = (char) 0xF9; // 1111 1 00 1 = syncword MPEG-2 Layer CRC packet[2] = (char) (((profile - 1) << 6) + (sampleRateIndex << 2) + (channelCfg >> 2)); packet[3] = (char) (((channelCfg & 3) << 6) + (fullLength >> 11)); packet[4] = (char) ((fullLength & 0x7FF) >> 3); packet[5] = (char) (((fullLength & 7) << 5) + 0x1F); packet[6] = (char) 0xFC; NSData *data = [NSData dataWithBytesNoCopy:packet length:adtsLength freeWhenDone:YES]; return data; } // 音频采样率对应的 index。 + (NSInteger)sampleRateIndex:(NSInteger)frequencyInHz { NSInteger sampleRateIndex = 0; switch (frequencyInHz) { case 96000: sampleRateIndex = 0; break; case 88200: sampleRateIndex = 1; break; case 64000: sampleRateIndex = 2; break; case 48000: sampleRateIndex = 3; break; case 44100: sampleRateIndex = 4; break; case 32000: sampleRateIndex = 5; break; case 24000: sampleRateIndex = 6; break; case 22050: sampleRateIndex = 7; break; case 16000: sampleRateIndex = 8; break; case 12000: sampleRateIndex = 9; break; case 11025: sampleRateIndex = 10; break; case 8000: sampleRateIndex = 11; break; case 7350: sampleRateIndex = 12; break; default: sampleRateIndex = 15; } return sampleRateIndex; } @end ``` ## 4、用工具播放 AAC 文件 完成音频采集和编码后,可以将 App Document 文件夹下面的 `test.aac` 文件拷贝到电脑上,使用 `ffplay` 播放来验证一下音频采集是效果是否符合预期: ``` $ ffplay -i test.aac ``` 这里在播放 AAC 文件时不必像播放 PCM 文件那样设置音频参数,这正是因为我们已经将对应的参数信息编码到 ADTS 头部数据中去了,播放解码时可以从中解析出这些信息从而正确的解码 AAC。 关于播放 AAC 文件的工具,可以参考[《FFmpeg 工具》第 2 节 ffplay 命令行工具](https://mp.weixin.qq.com/s?__biz=MjM5MTkxOTQyMQ==&mid=2257484831&idx=1&sn=6bab905a5040c46b971bab05f787788b&scene=21#wechat_redirect)和[《可视化音视频分析工具》第 1.1 节 Adobe Audition](https://mp.weixin.qq.com/s?__biz=MjM5MTkxOTQyMQ==&mid=2257484834&idx=1&sn=5dd9768bfc0d01ca1b036be8dd2f5fa1&scene=21#wechat_redirect)。 ## 5、参考资料 [1]CMSampleBufferRef: *https://developer.apple.com/documentation/coremedia/cmsamplebufferref/* [2]ADTS 格式: *http://wiki.multimedia.cx/index.php?title=ADTS* [3]AVAudioSession: *https://developer.apple.com/documentation/avfaudio/avaudiosession/* 原文链接:https://mp.weixin.qq.com/s/q4n1dYTjcJVJolX-Wrdr9Q ================================================ FILE: iOS资料/iOS AVDemo:音频解封装,从 MP4 中解封装出 AAC.md ================================================ # iOS AVDemo:音频解封装,从 MP4 中解封装出 AAC iOS/Android 客户端开发同学如果想要开始学习音视频开发,最丝滑的方式是对[音视频基础概念知识](https://mp.weixin.qq.com/mp/appmsgalbum?__biz=MjM5MTkxOTQyMQ==&action=getalbum&album_id=2140155659944787969#wechat_redirect)有一定了解后,再借助本地平台的音视频能力上手去实践音视频的`采集 → 编码 → 封装 → 解封装 → 解码 → 渲染`过程,并借助[音视频工具](https://mp.weixin.qq.com/mp/appmsgalbum?__biz=MjM5MTkxOTQyMQ==&action=getalbum&album_id=2216997905264082945#wechat_redirect)来分析和理解对应的音视频数据。 在[音视频工程示例](https://mp.weixin.qq.com/mp/appmsgalbum?__biz=MjM5MTkxOTQyMQ==&action=getalbum&album_id=2273301900659851268#wechat_redirect)这个栏目,我们将通过拆解`采集 → 编码 → 封装 → 解封装 → 解码 → 渲染`流程并实现 Demo 来向大家介绍如何在 iOS/Android 平台上手音视频开发。 这里是第四篇:**iOS 音频解封装 Demo**。这个 Demo 里包含以下内容: - 1)实现一个音频解封装模块; - 2)实现对 MP4 文件中音频部分的解封装逻辑并将解封装后的编码数据存储为 AAC 文件; - 3)详尽的代码注释,帮你理解代码逻辑和原理。 ## 1、音频解封装模块 首先,实现一个 `KFDemuxerConfig` 类用于定义音频解封装参数的配置。这里包括了:待解封装的资源、解封装类型这几个参数。这样设计是因为这个配置类不仅会用于音频解封装,后续的视频解封装也会使用。 ``` KFDemuxerConfig.h #import #import #import #import "KFMediaBase.h" NS_ASSUME_NONNULL_BEGIN @interface KFDemuxerConfig : NSObject @property (nonatomic, strong) AVAsset *asset; // 待解封装的资源。 @property (nonatomic, assign) KFMediaType demuxerType; // 解封装类型。 @end NS_ASSUME_NONNULL_END KFDemuxerConfig.m #import "KFDemuxerConfig.h" @implementation KFDemuxerConfig - (instancetype)init { self = [super init]; if (self) { _demuxerType = KFMediaAV; } return self; } @end ``` 其中用到的 `KFMediaType` 是定义在 `KFMediaBase.h` 中的一个枚举: ``` #ifndef KFMediaBase_h #define KFMediaBase_h #import typedef NS_ENUM(NSInteger, KFMediaType) { KFMediaNone = 0, KFMediaAudio = 1 << 0, // 仅音频。 KFMediaVideo = 1 << 1, // 仅视频。 KFMediaAV = KFMediaAudio | KFMediaVideo, // 音视频都有。 }; #endif /* KFMediaBase_h */ ``` 接下来,我们实现一个 `KFMP4Demuxer` 类来实现 MP4 的解封装。它能从符合 MP4 标准的文件中解封装出音频编码数据。 ``` KFMP4Demuxer.h #import #import #import "KFDemuxerConfig.h" NS_ASSUME_NONNULL_BEGIN typedef NS_ENUM(NSInteger, KFMP4DemuxerStatus) { KFMP4DemuxerStatusUnknown = 0, KFMP4DemuxerStatusRunning = 1, KFMP4DemuxerStatusFailed = 2, KFMP4DemuxerStatusCompleted = 3, KFMP4DemuxerStatusCancelled = 4, }; @interface KFMP4Demuxer : NSObject + (instancetype)new NS_UNAVAILABLE; - (instancetype)init NS_UNAVAILABLE; - (instancetype)initWithConfig:(KFDemuxerConfig *)config; @property (nonatomic, strong, readonly) KFDemuxerConfig *config; @property (nonatomic, copy) void (^errorCallBack)(NSError *error); @property (nonatomic, assign, readonly) BOOL hasAudioTrack; // 是否包含音频数据。 @property (nonatomic, assign, readonly) BOOL hasVideoTrack; // 是否包含视频数据。 @property (nonatomic, assign, readonly) CGSize videoSize; // 视频大小。 @property (nonatomic, assign, readonly) CMTime duration; // 媒体时长。 @property (nonatomic, assign, readonly) CMVideoCodecType codecType; // 编码类型。 @property (nonatomic, assign, readonly) KFMP4DemuxerStatus demuxerStatus; // 解封装器状态。 @property (nonatomic, assign, readonly) BOOL audioEOF; // 是否音频结束。 @property (nonatomic, assign, readonly) BOOL videoEOF; // 是否视频结束。 @property (nonatomic, assign, readonly) CGAffineTransform preferredTransform; // 图像的变换信息。比如:视频图像旋转。 - (void)startReading:(void (^)(BOOL success, NSError *error))completeHandler; // 开始读取数据解封装。 - (void)cancelReading; // 取消读取。 - (BOOL)hasAudioSampleBuffer; // 是否还有音频数据。 - (CMSampleBufferRef)copyNextAudioSampleBuffer CF_RETURNS_RETAINED; // 拷贝下一份音频采样。 - (BOOL)hasVideoSampleBuffer; // 是否还有视频数据。 - (CMSampleBufferRef)copyNextVideoSampleBuffer CF_RETURNS_RETAINED; // 拷贝下一份视频采样。 @end NS_ASSUME_NONNULL_END ``` 上面是 `KFMP4Demuxer` 的接口设计,除了初始化方法,主要还有一些获取解封装器信息或者状态的属性接口,此外就是执行开始/取消读取数据、拷贝音频/视频采样数据的操作接口。 这里大家可能会疑惑,为什么 `KFMP4Demuxer` 不像前面的 Demo 中设计的 `KFAudioCapture`、`KFAudioEncoder` 的接口那样,有一个解封装后的数据回调接口。主要是因为解封装的速度是非常快的,不会成为一个音视频 pipeline 的瓶颈,而且考虑到解封装的资源可能会很大,所以一般不会一直不停地解出数据往外抛,这样下一个处理节点可能处理不过来这些数据。基于这些原因,解封装器的接口设计是让外部调用方主动找解封装器要数据来触发解封装操作,并且还要控制一定的缓存量防止内存占用过大。 在上面的`拷贝下一份音频/视频采样数据`接口中,我们使用的是依然 **CMSampleBufferRef**[1] 作为返回值类型。在这个接口中我们通过 `CMSampleBufferRef` 打包的是从 MP4/M4A 文件解封装后得到的 AAC 编码数据。 ``` KFMP4Demuxer.m #import "KFMP4Demuxer.h" #define KFMP4DemuxerBadFileError 2000 #define KFMP4DemuxerAddVideoOutputError 2001 #define KFMP4DemuxerAddAudioOutputError 2002 #define KFMP4DemuxerQueueMaxCount 3 @interface KFMP4Demuxer () { CMSimpleQueueRef _audioQueue; CMSimpleQueueRef _videoQueue; } @property (nonatomic, strong, readwrite) KFDemuxerConfig* config; @property (nonatomic, strong) AVAssetReader *demuxReader; // 解封装器实例。 @property (nonatomic, strong) AVAssetReaderTrackOutput *readerAudioOutput; // Demuxer 的音频输出。 @property (nonatomic, strong) AVAssetReaderTrackOutput *readerVideoOutput; // Demuxer 的视频输出。 @property (nonatomic, strong) dispatch_queue_t demuxerQueue; @property (nonatomic, strong) dispatch_semaphore_t demuxerSemaphore; @property (nonatomic, strong) dispatch_semaphore_t audioQueueSemaphore; @property (nonatomic, strong) dispatch_semaphore_t videoQueueSemaphore; @property (nonatomic, assign) CMTime lastAudioCopyNextTime; // 上一次拷贝的音频采样的时间戳。 @property (nonatomic, assign) CMTime lastVideoCopyNextTime; // 上一次拷贝的视频采样的时间戳。 @property (nonatomic, assign, readwrite) BOOL hasAudioTrack; // 是否包含音频数据。 @property (nonatomic, assign, readwrite) BOOL hasVideoTrack; // 是否包含视频数据。 @property (nonatomic, assign, readwrite) CGSize videoSize; // 视频大小。 @property (nonatomic, assign, readwrite) CMTime duration; // 媒体时长。 @property (nonatomic, assign, readwrite) CMVideoCodecType codecType; // 编码类型。 @property (nonatomic, assign, readwrite) KFMP4DemuxerStatus demuxerStatus; // 解封装器状态。 @property (nonatomic, assign, readwrite) BOOL audioEOF; // 是否音频结束。 @property (nonatomic, assign, readwrite) BOOL videoEOF; // 是否视频结束。 @property (nonatomic, assign, readwrite) CGAffineTransform preferredTransform; // 图像的变换信息。比如:视频图像旋转。 @end @implementation KFMP4Demuxer #pragma mark - LifeCycle - (instancetype)initWithConfig:(KFDemuxerConfig *)config { self = [super init]; if (self) { _config = config; _demuxerSemaphore = dispatch_semaphore_create(1); _audioQueueSemaphore = dispatch_semaphore_create(1); _videoQueueSemaphore = dispatch_semaphore_create(1); _demuxerStatus = KFMP4DemuxerStatusUnknown; _demuxerQueue = dispatch_queue_create("com.KeyFrameKit.demuxerQueue", DISPATCH_QUEUE_SERIAL); CMSimpleQueueCreate(kCFAllocatorDefault, KFMP4DemuxerQueueMaxCount, &_audioQueue); CMSimpleQueueCreate(kCFAllocatorDefault, KFMP4DemuxerQueueMaxCount, &_videoQueue); } return self; } - (void)dealloc { // 清理状态机。 if (self.demuxerStatus == KFMP4DemuxerStatusRunning) { self.demuxerStatus = KFMP4DemuxerStatusCancelled; } // 清理解封装器实例。 dispatch_semaphore_wait(_demuxerSemaphore, DISPATCH_TIME_FOREVER); if (self.demuxReader && self.demuxReader.status == AVAssetReaderStatusReading) { [self.demuxReader cancelReading]; } dispatch_semaphore_signal(_demuxerSemaphore); // 清理音频数据队列。 dispatch_semaphore_wait(_audioQueueSemaphore, DISPATCH_TIME_FOREVER); while (CMSimpleQueueGetCount(_audioQueue) > 0) { CMSampleBufferRef sampleBuffer = (CMSampleBufferRef)CMSimpleQueueDequeue(_audioQueue); CFRelease(sampleBuffer); } dispatch_semaphore_signal(_audioQueueSemaphore); // 清理视频数据队列。 dispatch_semaphore_wait(_videoQueueSemaphore, DISPATCH_TIME_FOREVER); while (CMSimpleQueueGetCount(_videoQueue) > 0) { CMSampleBufferRef sampleBuffer = (CMSampleBufferRef)CMSimpleQueueDequeue(_videoQueue); CFRelease(sampleBuffer); } dispatch_semaphore_signal(_videoQueueSemaphore); } #pragma mark - Public Method - (void)startReading:(void (^)(BOOL success, NSError *error))completeHandler { __weak typeof(self) weakSelf = self; dispatch_async(_demuxerQueue, ^{ dispatch_semaphore_wait(weakSelf.demuxerSemaphore, DISPATCH_TIME_FOREVER); // 在第一次开始读数据时,创建解封装器实例。 if (!weakSelf.demuxReader) { NSError *error; [weakSelf _setupDemuxReader:&error]; weakSelf.audioEOF = !weakSelf.hasAudioTrack; weakSelf.videoEOF = !weakSelf.hasVideoTrack; weakSelf.demuxerStatus = error ? KFMP4DemuxerStatusFailed : KFMP4DemuxerStatusRunning; dispatch_semaphore_signal(weakSelf.demuxerSemaphore); if (completeHandler) { dispatch_async(dispatch_get_main_queue(), ^{ completeHandler(error ? NO : YES, error); }); } return; } dispatch_semaphore_signal(weakSelf.demuxerSemaphore); }); } - (void)cancelReading { __weak typeof(self) weakSelf = self; dispatch_async(_demuxerQueue, ^{ dispatch_semaphore_wait(weakSelf.demuxerSemaphore, DISPATCH_TIME_FOREVER); // 取消读数据。 if (weakSelf.demuxReader && weakSelf.demuxReader.status == AVAssetReaderStatusReading) { [weakSelf.demuxReader cancelReading]; } weakSelf.demuxerStatus = KFMP4DemuxerStatusCancelled; dispatch_semaphore_signal(weakSelf.demuxerSemaphore); }); } - (BOOL)hasAudioSampleBuffer { // 是否还有音频数据。 if (self.hasAudioTrack && self.demuxerStatus == KFMP4DemuxerStatusRunning && !self.audioEOF) { int32_t audioCount = 0; dispatch_semaphore_wait(_audioQueueSemaphore, DISPATCH_TIME_FOREVER); if (CMSimpleQueueGetCount(_audioQueue) > 0) { audioCount = CMSimpleQueueGetCount(_audioQueue); } dispatch_semaphore_signal(_audioQueueSemaphore); return (audioCount == 0 && self.audioEOF) ? NO : YES; } return NO; } - (CMSampleBufferRef)copyNextAudioSampleBuffer CF_RETURNS_RETAINED { // 拷贝下一份音频采样。 CMSampleBufferRef sampleBuffer = NULL; while (!sampleBuffer && self.demuxerStatus == KFMP4DemuxerStatusRunning && !self.audioEOF) { // 先从缓冲队列取数据。 dispatch_semaphore_wait(_audioQueueSemaphore, DISPATCH_TIME_FOREVER); if (CMSimpleQueueGetCount(_audioQueue) > 0) { sampleBuffer = (CMSampleBufferRef) CMSimpleQueueDequeue(_audioQueue); } dispatch_semaphore_signal(_audioQueueSemaphore); // 缓冲队列没有数据,就同步加载一下试试。 if (!sampleBuffer && self.demuxerStatus == KFMP4DemuxerStatusRunning) { [self _syncLoadNextSampleBuffer]; } } // 异步加载一下,先缓冲到数据队列中,等下次取。 [self _asyncLoadNextSampleBuffer]; return sampleBuffer; } - (BOOL)hasVideoSampleBuffer { // 是否还有视频数据。 if (self.hasVideoTrack && self.demuxerStatus == KFMP4DemuxerStatusRunning && !self.videoEOF) { int32_t videoCount = 0; dispatch_semaphore_wait(_videoQueueSemaphore, DISPATCH_TIME_FOREVER); if (CMSimpleQueueGetCount(_videoQueue) > 0) { videoCount = CMSimpleQueueGetCount(_videoQueue); } dispatch_semaphore_signal(_videoQueueSemaphore); return (videoCount == 0 && self.videoEOF) ? NO : YES; } return NO; } - (CMSampleBufferRef)copyNextVideoSampleBuffer CF_RETURNS_RETAINED { // 拷贝下一份视频采样。 CMSampleBufferRef sampleBuffer = NULL; while (!sampleBuffer && self.demuxerStatus == KFMP4DemuxerStatusRunning && !self.videoEOF) { // 先从缓冲队列取数据。 dispatch_semaphore_wait(_videoQueueSemaphore, DISPATCH_TIME_FOREVER); if (CMSimpleQueueGetCount(_videoQueue) > 0) { sampleBuffer = (CMSampleBufferRef) CMSimpleQueueDequeue(_videoQueue); } dispatch_semaphore_signal(_videoQueueSemaphore); // 缓冲队列没有数据,就同步加载一下试试。 if (!sampleBuffer && self.demuxerStatus == KFMP4DemuxerStatusRunning) { [self _syncLoadNextSampleBuffer]; } } // 异步加载一下,先缓冲到数据队列中,等下次取。 [self _asyncLoadNextSampleBuffer]; return sampleBuffer; } #pragma mark - Private Method - (void)_setupDemuxReader:(NSError**)error { if (!self.config.asset) { *error = [NSError errorWithDomain:NSStringFromClass([self class]) code:40003 userInfo:nil]; return; } // 1、创建解封装器实例。 // 使用 AVAssetReader 作为解封装器。解封装的目标是 config 中的 AVAsset 资源。 _demuxReader = [[AVAssetReader alloc] initWithAsset:self.config.asset error:error]; if (!_demuxReader) { return; } // 2、获取时间信息。 _duration = [self.config.asset duration]; // 3、处理待解封装的资源中的视频。 if (self.config.demuxerType & KFMediaVideo) { // 取出视频轨道。 AVAssetTrack *videoTrack = [[self.config.asset tracksWithMediaType:AVMediaTypeVideo] firstObject]; _hasVideoTrack = videoTrack ? YES : NO; if (_hasVideoTrack) { // 获取图像变换信息。 _preferredTransform = videoTrack.preferredTransform; // 获取图像大小。要应用上图像变换信息。 _videoSize = CGSizeApplyAffineTransform(videoTrack.naturalSize, videoTrack.preferredTransform); _videoSize = CGSizeMake(fabs(_videoSize.width), fabs(_videoSize.height)); // 获取编码格式。 CMVideoFormatDescriptionRef formatDescription = (__bridge CMVideoFormatDescriptionRef)[[videoTrack formatDescriptions] firstObject]; if (formatDescription) { _codecType = CMVideoFormatDescriptionGetCodecType(formatDescription); } // 基于轨道创建视频输出。 _readerVideoOutput = [[AVAssetReaderTrackOutput alloc] initWithTrack:videoTrack outputSettings:nil]; _readerVideoOutput.alwaysCopiesSampleData = NO; // 避免总是做数据拷贝,影响性能。 // 给解封装器绑定视频输出。 if ([_demuxReader canAddOutput:_readerVideoOutput]) { [_demuxReader addOutput:_readerVideoOutput]; } else { *error = _demuxReader.error ? _demuxReader.error : [NSError errorWithDomain:NSStringFromClass([self class]) code:KFMP4DemuxerAddVideoOutputError userInfo:nil]; return; } } } // 4、处理待解封装的资源中的音频。 if (self.config.demuxerType & KFMediaAudio) { // 取出音频轨道。 AVAssetTrack *audioTrack = [[self.config.asset tracksWithMediaType:AVMediaTypeAudio] firstObject]; _hasAudioTrack = audioTrack ? YES : NO; if (_hasAudioTrack) { // 基于轨道创建音频输出。 _readerAudioOutput = [[AVAssetReaderTrackOutput alloc] initWithTrack:audioTrack outputSettings:nil]; _readerAudioOutput.alwaysCopiesSampleData = NO; // 避免总是做数据拷贝,影响性能。 // 给解封装器绑定音频输出。 if ([_demuxReader canAddOutput:_readerAudioOutput]) { [_demuxReader addOutput:_readerAudioOutput]; } else { *error = _demuxReader.error ? _demuxReader.error : [NSError errorWithDomain:NSStringFromClass([self class]) code:KFMP4DemuxerAddAudioOutputError userInfo:nil]; return; } } } // 5、音频和视频数据都没有,就报错。 if (!_hasVideoTrack && !_hasAudioTrack) { *error = [NSError errorWithDomain:NSStringFromClass([self class]) code:KFMP4DemuxerBadFileError userInfo:nil]; return; } // 6、启动解封装。 BOOL startSuccess = [self.demuxReader startReading]; if (!startSuccess) { *error = self.demuxReader.error; } } - (void)_asyncLoadNextSampleBuffer { // 异步加载下一份采样数据。 __weak typeof(self) weakSelf = self; dispatch_async(_demuxerQueue, ^{ dispatch_semaphore_wait(weakSelf.demuxerSemaphore, DISPATCH_TIME_FOREVER); [weakSelf _loadNextSampleBuffer]; dispatch_semaphore_signal(weakSelf.demuxerSemaphore); }); } - (void)_syncLoadNextSampleBuffer { // 同步加载下一份采样数据。 dispatch_semaphore_wait(self.demuxerSemaphore, DISPATCH_TIME_FOREVER); [self _loadNextSampleBuffer]; dispatch_semaphore_signal(self.demuxerSemaphore); } - (void)_loadNextSampleBuffer { if (self.demuxerStatus != KFMP4DemuxerStatusRunning) { return; } // 1、根据解封装器的状态,处理异常情况。 if (self.demuxReader.status == AVAssetWriterStatusCompleted) { self.demuxerStatus = KFMP4DemuxerStatusCompleted; return; } else if (self.demuxReader.status == AVAssetWriterStatusFailed) { if (self.demuxReader.error.code == AVErrorOperationInterrupted) { // 如果当前解封装器的状态是被打断而失败,就尝试重新创建一下。 NSError *error; [self _setupDemuxReader:&error]; if (!error) { // 同时做一下恢复处理。 [self _resumeLastTime]; } } if (self.demuxReader.status == AVAssetWriterStatusFailed) { // 如果状态依然是失败,就上报错误。 self.demuxerStatus = KFMP4DemuxerStatusFailed; if (self.errorCallBack) { NSError *error = self.demuxReader.error; dispatch_async(dispatch_get_main_queue(), ^{ self.errorCallBack(error); }); } return; } } else if (self.demuxReader.status == AVAssetWriterStatusCancelled) { // 如果状态是取消,就直接 return。 self.demuxerStatus = KFMP4DemuxerStatusCancelled; return; } // 2、解封装器状态正常,加载下一份采样数据。 BOOL audioNeedLoad = (self.config.demuxerType & KFMediaAudio) && !self.audioEOF; BOOL videoNeedLoad = (self.config.demuxerType & KFMediaVideo) && !self.videoEOF; while (self.demuxReader && self.demuxReader.status == AVAssetReaderStatusReading && (audioNeedLoad || videoNeedLoad)) { // 加载音频数据。 if (audioNeedLoad) { dispatch_semaphore_wait(_audioQueueSemaphore, DISPATCH_TIME_FOREVER); int32_t audioCount = CMSimpleQueueGetCount(_audioQueue); dispatch_semaphore_signal(_audioQueueSemaphore); if (audioCount < KFMP4DemuxerQueueMaxCount) { // 从音频输出源读取音频数据。 CMSampleBufferRef next = [self.readerAudioOutput copyNextSampleBuffer]; if (next) { if (!CMSampleBufferGetDataBuffer(next)) { CFRelease(next); } else { // 将数据从音频输出源 readerAudioOutput 拷贝到缓冲队列 _audioQueue 中。 self.lastAudioCopyNextTime = CMSampleBufferGetPresentationTimeStamp(next); dispatch_semaphore_wait(_audioQueueSemaphore, DISPATCH_TIME_FOREVER); CMSimpleQueueEnqueue(_audioQueue, next); audioCount = CMSimpleQueueGetCount(_audioQueue); dispatch_semaphore_signal(_audioQueueSemaphore); } } else { self.audioEOF = self.demuxReader.status == AVAssetReaderStatusReading || self.demuxReader.status == AVAssetWriterStatusCompleted; audioNeedLoad = NO; } } else { audioNeedLoad = NO; } } // 加载视频数据。 if (videoNeedLoad) { dispatch_semaphore_wait(_videoQueueSemaphore, DISPATCH_TIME_FOREVER); int32_t videoCount = CMSimpleQueueGetCount(_videoQueue); dispatch_semaphore_signal(_videoQueueSemaphore); if (videoCount < KFMP4DemuxerQueueMaxCount) { // 从视频输出源读取视频数据。 CMSampleBufferRef next = [self.readerVideoOutput copyNextSampleBuffer]; if (next) { if (!CMSampleBufferGetDataBuffer(next)) { CFRelease(next); } else { // 将数据从视频输出源 readerVideoOutput 拷贝到缓冲队列 _videoQueue 中。 self.lastVideoCopyNextTime = CMSampleBufferGetDecodeTimeStamp(next); dispatch_semaphore_wait(_videoQueueSemaphore, DISPATCH_TIME_FOREVER); CMSimpleQueueEnqueue(_videoQueue, next); videoCount = CMSimpleQueueGetCount(_videoQueue); dispatch_semaphore_signal(_videoQueueSemaphore); } } else { self.videoEOF = self.demuxReader.status == AVAssetReaderStatusReading || self.demuxReader.status == AVAssetWriterStatusCompleted; videoNeedLoad = NO; } } else { videoNeedLoad = NO; } } } } - (void)_resumeLastTime { // 对于异常中断后的处理,需要根据记录的时间戳 _lastAudioCopyNextTime/_lastVideoCopyNextTime 做恢复操作。 BOOL audioNeedLoad = (_lastAudioCopyNextTime.value > 0) && !self.audioEOF; BOOL videoNeedLoad = (_lastVideoCopyNextTime.value > 0) && !self.videoEOF; while (self.demuxReader && self.demuxReader.status == AVAssetReaderStatusReading && (audioNeedLoad || videoNeedLoad)) { if (audioNeedLoad) { // 从音频输出源读取音频数据。 CMSampleBufferRef next = [self.readerAudioOutput copyNextSampleBuffer]; if (next) { if (CMTimeGetSeconds(CMSampleBufferGetPresentationTimeStamp(next)) <= CMTimeGetSeconds(_lastAudioCopyNextTime) || !CMSampleBufferGetDataBuffer(next)) { // 从输出源取出的数据时间戳小于上次标记的时间,则表示这份采样数据已经处理过了。 CFRelease(next); } else { dispatch_semaphore_wait(_audioQueueSemaphore, DISPATCH_TIME_FOREVER); CMSimpleQueueEnqueue(_audioQueue, next); dispatch_semaphore_signal(_audioQueueSemaphore); audioNeedLoad = NO; } } else { self.audioEOF = self.demuxReader.status == AVAssetReaderStatusReading || self.demuxReader.status == AVAssetWriterStatusCompleted; audioNeedLoad = NO; } } if (videoNeedLoad) { // 从视频输出源读取视频数据。 CMSampleBufferRef next = [self.readerVideoOutput copyNextSampleBuffer]; if (next) { if (CMTimeGetSeconds(CMSampleBufferGetDecodeTimeStamp(next)) <= CMTimeGetSeconds(_lastVideoCopyNextTime) || !CMSampleBufferGetDataBuffer(next)) { // 从输出源取出的数据时间戳小于上次标记的时间,则表示这份采样数据已经处理过了。 CFRelease(next); } else { dispatch_semaphore_wait(_videoQueueSemaphore, DISPATCH_TIME_FOREVER); CMSimpleQueueEnqueue(_videoQueue, next); dispatch_semaphore_signal(_videoQueueSemaphore); videoNeedLoad = NO; } } else { self.videoEOF = self.demuxReader.status == AVAssetReaderStatusReading || self.demuxReader.status == AVAssetWriterStatusCompleted; videoNeedLoad = NO; } } } } @end ``` 上面是 `KFMP4Demuxer` 的实现,从代码上可以看到主要有这几个部分: - 1)创建解封装器实例及对应的音频和视频数据输出源。第一次调用 `-startReading:` 时会创建解封装器实例,另外在 `-_loadNextSampleBuffer` 时如果发现当前解封装器的状态是被打断而失败时,会尝试重新创建解封装器实例。 - - 在 `-_setupDemuxReader:` 方法中实现。音频和视频的输出源分别是 `readerAudioOutput` 和 `readerVideoOutput`。 - 2)用两个队列作为缓冲区,分别管理音频和视频解封装后的数据。 - - 这两个队列分别是 `_audioQueue` 和 `_videoQueue`。 - 当外部向解封装器要数据而触发数据加载时,会把解封装后的数据先缓存到这两个队列中,缓冲的采样数不超过 `KFMP4DemuxerQueueMaxCount`,以减少内存占用。 - 3)从音视频输出源读取数据。 - - 核心逻辑在 `-_loadNextSampleBuffer` 方法中实现:从输出源 `readerAudioOutput` 和 `readerVideoOutput` 读取数据放入缓冲区队列 `_audioQueue` 和 `_videoQueue`。 - 在外部调用 `-copyNextAudioSampleBuffer`、`-copyNextVideoSampleBuffer` 时,触发读取数据。 - 4)从中断中恢复解封装。 - - 在 `-_resumeLastTime` 方法中实现。 - 5)停止解封装。 - - 在 `-cancelReading` 方法中实现。 - 6)解封装状态机管理。 - - 在枚举 `KFMP4DemuxerStatus` 中定义了解封装器的各种状态,对于解封装器的状态机管理贯穿在解封装的整个过程中。 - 7)错误回调。 - - 在 `-callBackError:` 方法向外回调错误。 - 8)清理封装器实例及数据缓冲区。 - - 在 `-dealloc` 方法中实现。 更具体细节见上述代码及其注释。 ## 2、解封装 MP4 文件中的音频部分存储为 AAC 文件 我们还是在一个 ViewController 中来实现对一个 MP4 文件解封装、获取其中的音频编码数据并存储为 AAC 文件。 ``` KFAudioDemuxerViewController.m #import "KFAudioDemuxerViewController.h" #import "KFMP4Demuxer.h" #import "KFAudioTools.h" @interface KFAudioDemuxerViewController () @property (nonatomic, strong) KFDemuxerConfig *demuxerConfig; @property (nonatomic, strong) KFMP4Demuxer *demuxer; @property (nonatomic, strong) NSFileHandle *fileHandle; @end @implementation KFAudioDemuxerViewController #pragma mark - Property - (KFDemuxerConfig *)demuxerConfig { if (!_demuxerConfig) { _demuxerConfig = [[KFDemuxerConfig alloc] init]; // 只解封装音频。 _demuxerConfig.demuxerType = KFMediaAudio; // 待解封装的资源。 NSString *assetPath = [[NSBundle mainBundle] pathForResource:@"input" ofType:@"mp4"]; _demuxerConfig.asset = [AVAsset assetWithURL:[NSURL fileURLWithPath:assetPath]]; } return _demuxerConfig; } - (KFMP4Demuxer *)demuxer { if (!_demuxer) { _demuxer = [[KFMP4Demuxer alloc] initWithConfig:self.demuxerConfig]; _demuxer.errorCallBack = ^(NSError *error) { NSLog(@"KFMP4Demuxer error:%zi %@", error.code, error.localizedDescription); }; } return _demuxer; } - (NSFileHandle *)fileHandle { if (!_fileHandle) { NSString *audioPath = [[NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) lastObject] stringByAppendingPathComponent:@"output.aac"]; [[NSFileManager defaultManager] removeItemAtPath:audioPath error:nil]; [[NSFileManager defaultManager] createFileAtPath:audioPath contents:nil attributes:nil]; _fileHandle = [NSFileHandle fileHandleForWritingAtPath:audioPath]; } return _fileHandle; } #pragma mark - Lifecycle - (void)viewDidLoad { [super viewDidLoad]; [self setupUI]; // 完成音频解封装后,可以将 App Document 文件夹下面的 output.aac 文件拷贝到电脑上,使用 ffplay 播放: // ffplay -i output.aac } - (void)dealloc { if (_fileHandle) { [_fileHandle closeFile]; _fileHandle = nil; } } #pragma mark - Setup - (void)setupUI { self.edgesForExtendedLayout = UIRectEdgeAll; self.extendedLayoutIncludesOpaqueBars = YES; self.title = @"Audio Demuxer"; self.view.backgroundColor = [UIColor whiteColor]; // Navigation item. UIBarButtonItem *startBarButton = [[UIBarButtonItem alloc] initWithTitle:@"Start" style:UIBarButtonItemStylePlain target:self action:@selector(start)]; self.navigationItem.rightBarButtonItems = @[startBarButton]; } #pragma mark - Action - (void)start { NSLog(@"KFMP4Demuxer start"); __weak typeof(self) weakSelf = self; [self.demuxer startReading:^(BOOL success, NSError * _Nonnull error) { if (success) { // Demuxer 启动成功后,就可以从它里面获取解封装后的数据了。 [weakSelf fetchAndSaveDemuxedData]; } else { NSLog(@"KFMP4Demuxer error: %zi %@", error.code, error.localizedDescription); } }]; } #pragma mark - Utility - (void)fetchAndSaveDemuxedData { // 异步地从 Demuxer 获取解封装后的 AAC 编码数据。 dispatch_async(dispatch_get_global_queue(0, 0), ^{ while (self.demuxer.hasAudioSampleBuffer) { CMSampleBufferRef audioBuffer = [self.demuxer copyNextAudioSampleBuffer]; if (audioBuffer) { [self saveSampleBuffer:audioBuffer]; CFRelease(audioBuffer); } } if (self.demuxer.demuxerStatus == KFMP4DemuxerStatusCompleted) { NSLog(@"KFMP4Demuxer complete"); } }); } - (void)saveSampleBuffer:(CMSampleBufferRef)sampleBuffer { // 将解封装后的数据存储为 AAC 文件。 if (sampleBuffer) { // 获取解封装后的 AAC 编码裸数据。 AudioStreamBasicDescription streamBasicDescription = *CMAudioFormatDescriptionGetStreamBasicDescription(CMSampleBufferGetFormatDescription(sampleBuffer)); CMBlockBufferRef blockBuffer = CMSampleBufferGetDataBuffer(sampleBuffer); size_t totolLength; char *dataPointer = NULL; CMBlockBufferGetDataPointer(blockBuffer, 0, NULL, &totolLength, &dataPointer); if (totolLength == 0 || !dataPointer) { return; } // 将 AAC 编码裸数据存储为 AAC 文件,这时候需要在每个包前增加 ADTS 头信息。 for (NSInteger index = 0; index < CMSampleBufferGetNumSamples(sampleBuffer); index++) { size_t sampleSize = CMSampleBufferGetSampleSize(sampleBuffer, index); [self.fileHandle writeData:[KFAudioTools adtsDataWithChannels:streamBasicDescription.mChannelsPerFrame sampleRate:streamBasicDescription.mSampleRate rawDataLength:sampleSize]]; [self.fileHandle writeData:[NSData dataWithBytes:dataPointer length:sampleSize]]; dataPointer += sampleSize; } } } @end ``` 上面是 `KFAudioDemuxerViewController` 的实现,其中主要包含这几个部分: - 1)设置好待解封装的资源。 - - 在 `-demuxerConfig` 中实现,我们这里是一个 MP4 文件。 - 2)启动解封装器。 - - 在 `-start` 中实现。 - 3)读取解封装后的音频编码数据并存储为 AAC 文件。 - - 在 `-fetchAndSaveDemuxedData` → `-saveSampleBuffer` 中实现。 - 需要注意的是,我们从解封装器读取的音频 AAC 编码数据在存储为 AAC 文件时需要条件 ADTS 头。生成一个 AAC packet 对应的 ADTS 头数据在 `KFAudioTools` 类的工具方法 `+adtsDataWithChannels:sampleRate:rawDataLength:` 中实现。这个在前面的音频编码的 Demo 中已经介绍过了。 ## 3、用工具播放 AAC 文件 完成音频采集和编码后,可以将 App Document 文件夹下面的 `output.aac` 文件拷贝到电脑上,使用 `ffplay` 播放来验证一下音频采集是效果是否符合预期: ``` $ ffplay -i output.aac ``` 关于播放 AAC 文件的工具,可以参考[《FFmpeg 工具》第 2 节 ffplay 命令行工具](https://mp.weixin.qq.com/s?__biz=MjM5MTkxOTQyMQ==&mid=2257484831&idx=1&sn=6bab905a5040c46b971bab05f787788b&scene=21#wechat_redirect)和[《可视化音视频分析工具》第 1.1 节 Adobe Audition](https://mp.weixin.qq.com/s?__biz=MjM5MTkxOTQyMQ==&mid=2257484834&idx=1&sn=5dd9768bfc0d01ca1b036be8dd2f5fa1&scene=21#wechat_redirect)。 ## 4、参考资料 [1]CMSampleBufferRef: *https://developer.apple.com/documentation/coremedia/cmsamplebufferref/* 原文链接:https://mp.weixin.qq.com/s/fCZfIXriTXUPcI4d4te_ew ================================================ FILE: iOS资料/iOS AVDemo:音频采集.md ================================================ # iOS AVDemo:音频采集 iOS/Android 客户端开发同学如果想要开始学习音视频开发,最丝滑的方式是对[音视频基础概念知识](https://mp.weixin.qq.com/mp/appmsgalbum?__biz=MjM5MTkxOTQyMQ==&action=getalbum&album_id=2140155659944787969#wechat_redirect)有一定了解后,再借助本地平台的音视频能力上手去实践音视频的`采集 → 编码 → 封装 → 解封装 → 解码 → 渲染`过程,并借助[音视频工具](https://mp.weixin.qq.com/mp/appmsgalbum?__biz=MjM5MTkxOTQyMQ==&action=getalbum&album_id=2216997905264082945#wechat_redirect)来分析和理解对应的音视频数据。 在[音视频工程示例](https://mp.weixin.qq.com/mp/appmsgalbum?__biz=MjM5MTkxOTQyMQ==&action=getalbum&album_id=2273301900659851268#wechat_redirect)这个栏目,我们将通过拆解`采集 → 编码 → 封装 → 解封装 → 解码 → 渲染`流程并实现 Demo 来向大家介绍如何在 iOS/Android 平台上手音视频开发。 这里是第一篇:**iOS 音频采集 Demo**。这个 Demo 里包含以下内容: - 1)实现一个音频采集模块; - 2)实现音频采集逻辑并将采集的音频存储为 PCM 数据; - 3)详尽的代码注释,帮你理解代码逻辑和原理。 ## 1、音频采集模块 首先,实现一个 `KFAudioConfig` 类用于定义音频采集参数的配置。这里包括了:采样率、量化位深、声道数这几个参数。这几个参数的含义在前面介绍声音基础的文章[《声音的表示(3):声音的数字化》](https://mp.weixin.qq.com/s?__biz=MjM5MTkxOTQyMQ==&mid=2257484445&idx=1&sn=a1b2f1c71e54ca5c311fedd561fdee4d&scene=21#wechat_redirect)中有过介绍。 ``` KFAudioConfig.h #import NS_ASSUME_NONNULL_BEGIN @interface KFAudioConfig : NSObject + (instancetype)defaultConfig; @property (nonatomic, assign) NSUInteger channels; // 声道数,default: 2。 @property (nonatomic, assign) NSUInteger sampleRate; // 采样率,default: 44100。 @property (nonatomic, assign) NSUInteger bitDepth; // 量化位深,default: 16。 @end NS_ASSUME_NONNULL_END KFAudioConfig.m #import "KFAudioConfig.h" @implementation KFAudioConfig + (instancetype)defaultConfig { KFAudioConfig *config = [[self alloc] init]; config.channels = 2; config.sampleRate = 44100; config.bitDepth = 16; return config; } @end ``` 接下来,我们实现一个 `KFAudioCapture` 类来实现音频采集。 ``` KFAudioCapture.h #import #import #import "KFAudioConfig.h" NS_ASSUME_NONNULL_BEGIN @interface KFAudioCapture : NSObject + (instancetype)new NS_UNAVAILABLE; - (instancetype)init NS_UNAVAILABLE; - (instancetype)initWithConfig:(KFAudioConfig *)config; @property (nonatomic, strong, readonly) KFAudioConfig *config; @property (nonatomic, copy) void (^sampleBufferOutputCallBack)(CMSampleBufferRef sample); // 音频采集数据回调。 @property (nonatomic, copy) void (^errorCallBack)(NSError *error); // 音频采集错误回调。 - (void)startRunning; // 开始采集音频数据。 - (void)stopRunning; // 停止采集音频数据。 @end NS_ASSUME_NONNULL_END ``` 上面是 `KFAudioCapture` 的接口设计,可以看到这里除了`初始化方法`,主要是有`获取音频配置`以及音频采集`数据回调`和`错误回调`的接口,另外就是`开始采集`和`停止采集`的接口。 在上面的音频采集`数据回调`接口中,我们返回的是 **CMSampleBufferRef**[1] 这个数据结构,这里我们重点介绍一下。官方文档对 `CMSampleBufferRef` 描述如下: > A reference to a CMSampleBuffer. A CMSampleBuffer is a Core Foundation object containing zero or more compressed (or uncompressed) samples of a particular media type (audio, video, muxed, and so on). 即 `CMSampleBufferRef` 是对 **CMSampleBuffer**[2] 的一个引用。所里这里核心的数据结构是 `CMSampleBuffer`,关于它有如下几点需要注意: - `CMSampleBuffer` 则是一个 Core Foundation 的对象,这意味着它的接口是 C 语言实现,它的内存管理是非 ARC 的,需要手动管理,它与 Foundation 对象之间需要进行桥接转换。 - `CMSampleBuffer` 是系统用来在音视频处理的 pipeline 中使用和传递媒体采样数据的核心数据结构。你可以认为它是 iOS 音视频处理 pipeline 中的流通货币,摄像头采集的视频数据接口、麦克风采集的音频数据接口、编码和解码数据接口、读取和存储视频接口、视频渲染接口等等,都以它作为参数。 - `CMSampleBuffer` 中包含着零个或多个某一类型(audio、video、muxed 等)的采样数据。比如: - - 要么是一个或多个媒体采样的 **CMBlockBuffer**[3]。其中可以封装:音频采集后、编码后、解码后的数据(如:PCM 数据、AAC 数据);视频编码后的数据(如:H.264 数据)。 - 要么是一个 **CVImageBuffer**[4](也作 **CVPixelBuffer**[5])。其中包含媒体流中 CMSampleBuffers 的格式描述、每个采样的宽高和时序信息、缓冲级别和采样级别的附属信息。缓冲级别的附属信息是指缓冲区整体的信息,比如播放速度、对后续缓冲数据的操作等。采样级别的附属信息是指单个采样的信息,比如视频帧的时间戳、是否关键帧等。其中可以封装:视频采集后、解码后等未经编码的数据(如:YCbCr 数据、RGBA 数据)。 所以,了解完这些,就知道上面的音频采集`数据回调`接口为什么会返回 `CMSampleBufferRef` 这个数据结构了。因为它通用,同时我们也可以从里面获取到我们想要的 PCM 数据。 ``` KFAudioCapture.m #import "KFAudioCapture.h" #import #import @interface KFAudioCapture () @property (nonatomic, assign) AudioComponentInstance audioCaptureInstance; // 音频采集实例。 @property (nonatomic, assign) AudioStreamBasicDescription audioFormat; // 音频采集参数。 @property (nonatomic, strong, readwrite) KFAudioConfig *config; @property (nonatomic, strong) dispatch_queue_t captureQueue; @property (nonatomic, assign) BOOL isError; @end @implementation KFAudioCapture #pragma mark - Lifecycle - (instancetype)initWithConfig:(KFAudioConfig *)config { self = [super init]; if (self) { _config = config; _captureQueue = dispatch_queue_create("com.KeyFrameKit.audioCapture", DISPATCH_QUEUE_SERIAL); } return self; } - (void)dealloc { // 清理音频采集实例。 if (_audioCaptureInstance) { AudioOutputUnitStop(_audioCaptureInstance); AudioComponentInstanceDispose(_audioCaptureInstance); _audioCaptureInstance = nil; } } #pragma mark - Action - (void)startRunning { if (self.isError) { return; } __weak typeof(self) weakSelf = self; dispatch_async(_captureQueue, ^{ if (!weakSelf.audioCaptureInstance) { NSError *error = nil; // 第一次 startRunning 时创建音频采集实例。 [weakSelf setupAudioCaptureInstance:&error]; if (error) { // 捕捉并回调创建音频实例时的错误。 [weakSelf callBackError:error]; return; } } // 开始采集。 OSStatus startStatus = AudioOutputUnitStart(weakSelf.audioCaptureInstance); if (startStatus != noErr) { // 捕捉并回调开始采集时的错误。 [weakSelf callBackError:[NSError errorWithDomain:NSStringFromClass([KFAudioCapture class]) code:startStatus userInfo:nil]]; } }); } - (void)stopRunning { if (self.isError) { return; } __weak typeof(self) weakSelf = self; dispatch_async(_captureQueue, ^{ if (weakSelf.audioCaptureInstance) { // 停止采集。 OSStatus stopStatus = AudioOutputUnitStop(weakSelf.audioCaptureInstance); if (stopStatus != noErr) { // 捕捉并回调停止采集时的错误。 [weakSelf callBackError:[NSError errorWithDomain:NSStringFromClass([KFAudioCapture class]) code:stopStatus userInfo:nil]]; } } }); } #pragma mark - Utility - (void)setupAudioCaptureInstance:(NSError **)error { // 1、设置音频组件描述。 AudioComponentDescription acd = { .componentType = kAudioUnitType_Output, //.componentSubType = kAudioUnitSubType_VoiceProcessingIO, // 回声消除模式 .componentSubType = kAudioUnitSubType_RemoteIO, .componentManufacturer = kAudioUnitManufacturer_Apple, .componentFlags = 0, .componentFlagsMask = 0, }; // 2、查找符合指定描述的音频组件。 AudioComponent component = AudioComponentFindNext(NULL, &acd); // 3、创建音频组件实例。 OSStatus status = AudioComponentInstanceNew(component, &_audioCaptureInstance); if (status != noErr) { *error = [NSError errorWithDomain:NSStringFromClass(self.class) code:status userInfo:nil]; return; } // 4、设置实例的属性:可读写。0 不可读写,1 可读写。 UInt32 flagOne = 1; AudioUnitSetProperty(_audioCaptureInstance, kAudioOutputUnitProperty_EnableIO, kAudioUnitScope_Input, 1, &flagOne, sizeof(flagOne)); // 5、设置实例的属性:音频参数,如:数据格式、声道数、采样位深、采样率等。 AudioStreamBasicDescription asbd = {0}; asbd.mFormatID = kAudioFormatLinearPCM; // 原始数据为 PCM,采用声道交错格式。 asbd.mFormatFlags = kAudioFormatFlagIsSignedInteger | kAudioFormatFlagsNativeEndian | kAudioFormatFlagIsPacked; asbd.mChannelsPerFrame = (UInt32) self.config.channels; // 每帧的声道数 asbd.mFramesPerPacket = 1; // 每个数据包帧数 asbd.mBitsPerChannel = (UInt32) self.config.bitDepth; // 采样位深 asbd.mBytesPerFrame = asbd.mChannelsPerFrame * asbd.mBitsPerChannel / 8; // 每帧字节数 (byte = bit / 8) asbd.mBytesPerPacket = asbd.mFramesPerPacket * asbd.mBytesPerFrame; // 每个包的字节数 asbd.mSampleRate = self.config.sampleRate; // 采样率 self.audioFormat = asbd; status = AudioUnitSetProperty(_audioCaptureInstance, kAudioUnitProperty_StreamFormat, kAudioUnitScope_Output, 1, &asbd, sizeof(asbd)); if (status != noErr) { *error = [NSError errorWithDomain:NSStringFromClass(self.class) code:status userInfo:nil]; return; } // 6、设置实例的属性:数据回调函数。 AURenderCallbackStruct cb; cb.inputProcRefCon = (__bridge void *) self; cb.inputProc = audioBufferCallBack; status = AudioUnitSetProperty(_audioCaptureInstance, kAudioOutputUnitProperty_SetInputCallback, kAudioUnitScope_Global, 1, &cb, sizeof(cb)); if (status != noErr) { *error = [NSError errorWithDomain:NSStringFromClass(self.class) code:status userInfo:nil]; return; } // 7、初始化实例。 status = AudioUnitInitialize(_audioCaptureInstance); if (status != noErr) { *error = [NSError errorWithDomain:NSStringFromClass(self.class) code:status userInfo:nil]; return; } } - (void)callBackError:(NSError *)error { self.isError = YES; if (error && self.errorCallBack) { dispatch_async(dispatch_get_main_queue(), ^{ self.errorCallBack(error); }); } } + (CMSampleBufferRef)sampleBufferFromAudioBufferList:(AudioBufferList)buffers inTimeStamp:(const AudioTimeStamp *)inTimeStamp inNumberFrames:(UInt32)inNumberFrames description:(AudioStreamBasicDescription)description { CMSampleBufferRef sampleBuffer = NULL; // 待生成的 CMSampleBuffer 实例的引用。 // 1、创建音频流的格式描述信息。 CMFormatDescriptionRef format = NULL; OSStatus status = CMAudioFormatDescriptionCreate(kCFAllocatorDefault, &description, 0, NULL, 0, NULL, NULL, &format); if (status != noErr) { CFRelease(format); return nil; } // 2、处理音频帧的时间戳信息。 mach_timebase_info_data_t info = {0, 0}; mach_timebase_info(&info); uint64_t time = inTimeStamp->mHostTime; // 转换为纳秒。 time *= info.numer; time /= info.denom; // PTS。 CMTime presentationTime = CMTimeMake(time, 1000000000.0f); // 对于音频,PTS 和 DTS 是一样的。 CMSampleTimingInfo timing = {CMTimeMake(1, description.mSampleRate), presentationTime, presentationTime}; // 3、创建 CMSampleBuffer 实例。 status = CMSampleBufferCreate(kCFAllocatorDefault, NULL, false, NULL, NULL, format, (CMItemCount) inNumberFrames, 1, &timing, 0, NULL, &sampleBuffer); if (status != noErr) { CFRelease(format); return nil; } // 4、创建 CMBlockBuffer 实例。其中数据拷贝自 AudioBufferList,并将 CMBlockBuffer 实例关联到 CMSampleBuffer 实例。 status = CMSampleBufferSetDataBufferFromAudioBufferList(sampleBuffer, kCFAllocatorDefault, kCFAllocatorDefault, 0, &buffers); if (status != noErr) { CFRelease(format); return nil; } CFRelease(format); return sampleBuffer; } #pragma mark - Capture CallBack static OSStatus audioBufferCallBack(void *inRefCon, AudioUnitRenderActionFlags *ioActionFlags, const AudioTimeStamp *inTimeStamp, UInt32 inBusNumber, UInt32 inNumberFrames, AudioBufferList *ioData) { @autoreleasepool { KFAudioCapture *capture = (__bridge KFAudioCapture *) inRefCon; if (!capture) { return -1; } // 1、创建 AudioBufferList 空间,用来接收采集回来的数据。 AudioBuffer buffer; buffer.mData = NULL; buffer.mDataByteSize = 0; // 采集的时候设置了数据格式是 kAudioFormatLinearPCM,即声道交错格式,所以即使是双声道这里也设置 mNumberChannels 为 1。 // 对于双声道的数据,会按照采样位深 16 bit 每组,一组接一组地进行两个声道数据的交错拼装。 buffer.mNumberChannels = 1; AudioBufferList buffers; buffers.mNumberBuffers = 1; buffers.mBuffers[0] = buffer; // 2、获取音频 PCM 数据,存储到 AudioBufferList 中。 // 这里有几个问题要说明清楚: // 1)每次回调会过来多少数据? // 按照上面采集音频参数的设置:PCM 为声道交错格式、每帧的声道数为 2、采样位深为 16 bit。这样每帧的字节数是 4 字节(左右声道各 2 字节)。 // 返回数据的帧数是 inNumberFrames。这样一次回调回来的数据字节数是多少就是:mBytesPerFrame(4) * inNumberFrames。 // 2)这个数据回调的频率跟音频采样率有关系吗? // 这个数据回调的频率与音频采样率(上面设置的 mSampleRate 44100)是没关系的。声道数、采样位深、采样率共同决定了设备单位时间里采样数据的大小,这些数据是会缓冲起来,然后一块一块的通过这个数据回调给我们,这个回调的频率是底层一块一块给我们数据的速度,跟采样率无关。 // 3)这个数据回调的频率是多少? // 这个数据回调的间隔是 [AVAudioSession sharedInstance].preferredIOBufferDuration,频率即该值的倒数。我们可以通过 [[AVAudioSession sharedInstance] setPreferredIOBufferDuration:1 error:nil] 设置这个值来控制回调频率。 OSStatus status = AudioUnitRender(capture.audioCaptureInstance, ioActionFlags, inTimeStamp, inBusNumber, inNumberFrames, &buffers); // 3、数据封装及回调。 if (status == noErr) { // 使用工具方法将数据封装为 CMSampleBuffer。 CMSampleBufferRef sampleBuffer = [KFAudioCapture sampleBufferFromAudioBufferList:buffers inTimeStamp:inTimeStamp inNumberFrames:inNumberFrames description:capture.audioFormat]; // 回调数据。 if (capture.sampleBufferOutputCallBack) { capture.sampleBufferOutputCallBack(sampleBuffer); } if (sampleBuffer) { CFRelease(sampleBuffer); } } return status; } } @end ``` 上面是 `KFAudioCapture` 的实现,从代码上可以看到主要有这几个部分: - 1)创建音频采集实例。第一次调用 `-startRunning` 才会创建音频采集实例。 - - 在 `-setupAudioCaptureInstance:` 方法中实现。 - 2)处理音频采集实例的数据回调,并在回调中将数据封装到 `CMSampleBufferRef` 结构中,抛给 KFAudioCapture 的对外数据回调接口。 - - 在 `audioBufferCallBack(...)` 方法中实现回调处理逻辑。 - 其中封装 `CMSampleBufferRef` 用到了 `+sampleBufferFromAudioBufferList:inTimeStamp:inNumberFrames:description:` 方法。 - 3)实现开始采集和停止采集逻辑。 - - 分别在 `-startRunning` 和 `-stopRunning` 方法中实现。注意,这里是开始和停止操作都是放在串行队列中通过 `dispatch_async` 异步处理的,这里主要是为了防止主线程卡顿。 - 4)捕捉音频采集开始和停止操作中的错误,抛给 KFAudioCapture 的对外错误回调接口。 - - 在 `-startRunning` 和 `-stopRunning` 方法中捕捉错误,在 `-callBackError:` 方法向外回调。 - 5)清理音频采集实例。 - - 在 `-dealloc` 方法中实现。 更具体细节见上述代码及其注释。 ## 2、采集音频存储为 PCM 文件 我们在一个 ViewController 中来实现音频采集逻辑并将采集的音频存储为 PCM 数据。 ``` KFAudioCaptureViewController.m #import "KFAudioCaptureViewController.h" #import #import "KFAudioCapture.h" @interface KFAudioCaptureViewController () @property (nonatomic, strong) KFAudioConfig *audioConfig; @property (nonatomic, strong) KFAudioCapture *audioCapture; @property (nonatomic, strong) NSFileHandle *fileHandle; @end @implementation KFAudioCaptureViewController #pragma mark - Property - (KFAudioConfig *)audioConfig { if (!_audioConfig) { _audioConfig = [KFAudioConfig defaultConfig]; } return _audioConfig; } - (KFAudioCapture *)audioCapture { if (!_audioCapture) { __weak typeof(self) weakSelf = self; _audioCapture = [[KFAudioCapture alloc] initWithConfig:self.audioConfig]; _audioCapture.errorCallBack = ^(NSError* error) { NSLog(@"KFAudioCapture error: %zi %@", error.code, error.localizedDescription); }; // 音频采集数据回调。在这里将 PCM 数据写入文件。 _audioCapture.sampleBufferOutputCallBack = ^(CMSampleBufferRef sampleBuffer) { if (sampleBuffer) { // 1、获取 CMBlockBuffer,这里面封装着 PCM 数据。 CMBlockBufferRef blockBuffer = CMSampleBufferGetDataBuffer(sampleBuffer); size_t lengthAtOffsetOutput, totalLengthOutput; char *dataPointer; // 2、从 CMBlockBuffer 中获取 PCM 数据存储到文件中。 CMBlockBufferGetDataPointer(blockBuffer, 0, &lengthAtOffsetOutput, &totalLengthOutput, &dataPointer); [weakSelf.fileHandle writeData:[NSData dataWithBytes:dataPointer length:totalLengthOutput]]; } }; } return _audioCapture; } - (NSFileHandle *)fileHandle { if (!_fileHandle) { NSString *audioPath = [[NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) lastObject] stringByAppendingPathComponent:@"test.pcm"]; NSLog(@"PCM file path: %@", audioPath); [[NSFileManager defaultManager] removeItemAtPath:audioPath error:nil]; [[NSFileManager defaultManager] createFileAtPath:audioPath contents:nil attributes:nil]; _fileHandle = [NSFileHandle fileHandleForWritingAtPath:audioPath]; } return _fileHandle; } #pragma mark - Lifecycle - (void)viewDidLoad { [super viewDidLoad]; [self setupAudioSession]; [self setupUI]; // 完成音频采集后,可以将 App Document 文件夹下面的 test.pcm 文件拷贝到电脑上,使用 ffplay 播放: // ffplay -ar 44100 -channels 2 -f s16le -i test.pcm } - (void)dealloc { if (_fileHandle) { [_fileHandle closeFile]; } } #pragma mark - Setup - (void)setupUI { self.edgesForExtendedLayout = UIRectEdgeAll; self.extendedLayoutIncludesOpaqueBars = YES; self.title = @"Audio Capture"; self.view.backgroundColor = [UIColor whiteColor]; // Navigation item. UIBarButtonItem *startBarButton = [[UIBarButtonItem alloc] initWithTitle:@"Start" style:UIBarButtonItemStylePlain target:self action:@selector(start)]; UIBarButtonItem *stopBarButton = [[UIBarButtonItem alloc] initWithTitle:@"Stop" style:UIBarButtonItemStylePlain target:self action:@selector(stop)]; self.navigationItem.rightBarButtonItems = @[startBarButton, stopBarButton]; } - (void)setupAudioSession { NSError *error = nil; // 1、获取音频会话实例。 AVAudioSession *session = [AVAudioSession sharedInstance]; // 2、设置分类和选项。 [session setCategory:AVAudioSessionCategoryPlayAndRecord withOptions:AVAudioSessionCategoryOptionMixWithOthers | AVAudioSessionCategoryOptionDefaultToSpeaker error:&error]; if (error) { NSLog(@"AVAudioSession setCategory error."); error = nil; return; } // 3、设置模式。 [session setMode:AVAudioSessionModeVideoRecording error:&error]; if (error) { NSLog(@"AVAudioSession setMode error."); error = nil; return; } // 4、激活会话。 [session setActive:YES error:&error]; if (error) { NSLog(@"AVAudioSession setActive error."); error = nil; return; } } #pragma mark - Action - (void)start { [self.audioCapture startRunning]; } - (void)stop { [self.audioCapture stopRunning]; } @end ``` 上面是 `KFAudioCaptureViewController` 的实现,这里需要注意的是在采集音频前需要设置 **AVAudioSession**[6] 为正确的采集模式。 ## 3、用工具播放 PCM 文件 完成音频采集后,可以将 App Document 文件夹下面的 `test.pcm` 文件拷贝到电脑上,使用 `ffplay` 播放来验证一下音频采集是效果是否符合预期: ``` $ ffplay -ar 44100 -channels 2 -f s16le -i test.pcm ``` 注意这里的参数要对齐在工程代码中设置的`采样率`、`声道数`、`采样位深`。 关于播放 PCM 文件的工具,可以参考[《FFmpeg 工具》第 2 节 ffplay 命令行工具](https://mp.weixin.qq.com/s?__biz=MjM5MTkxOTQyMQ==&mid=2257484831&idx=1&sn=6bab905a5040c46b971bab05f787788b&scene=21#wechat_redirect)和[《可视化音视频分析工具》第 1.1 节 Adobe Audition](https://mp.weixin.qq.com/s?__biz=MjM5MTkxOTQyMQ==&mid=2257484834&idx=1&sn=5dd9768bfc0d01ca1b036be8dd2f5fa1&scene=21#wechat_redirect)。 ## 4、参考资料 [1]CMSampleBufferRef: *https://developer.apple.com/documentation/coremedia/cmsamplebufferref/* [2]CMSampleBuffer: *https://developer.apple.com/documentation/coremedia/cmsamplebuffer-u71* [3]CMBlockBuffer: *https://developer.apple.com/documentation/coremedia/cmblockbuffer-u9i* [4]CVImageBuffer: *https://developer.apple.com/documentation/corevideo/cvimagebuffer-q40* [5]CVPixelBuffer: *https://developer.apple.com/documentation/corevideo/cvpixelbuffer-q2e* [6]AVAudioSession: *https://developer.apple.com/documentation/avfaudio/avaudiosession/* 原文链接:https://mp.weixin.qq.com/s/FDR_5cMfAJQgZhSvjgeWYA ================================================ FILE: iOS资料/iOS Runtime详解.md ================================================ # iOS Runtime详解 Runtime的特性主要是消息(`方法`)传递,如果消息(`方法`)在对象中找不到,就进行转发,具体怎么实现的呢。我们从下面几个方面探寻Runtime的实现机制。 - Runtime介绍 - Runtime消息传递 - Runtime消息转发 - Runtime应用 ## 1、Runtime介绍 > Objective-C 扩展了 C 语言,并加入了面向对象特性和 Smalltalk 式的消息传递机制。而这个扩展的核心是一个用 C 和 编译语言 写的 Runtime 库。它是 Objective-C 面向对象和动态机制的基石。 > Objective-C 是一个动态语言,这意味着它不仅需要一个编译器,也需要一个运行时系统来动态得创建类和对象、进行消息传递和转发。理解 Objective-C 的 Runtime 机制可以帮我们更好的了解这个语言,适当的时候还能对语言进行扩展,从系统层面解决项目中的一些设计或技术问题。了解 Runtime ,要先了解它的核心 - 消息传递 (Messaging)。 `Runtime`其实有两个版本: “`modern`” 和 “`legacy`”。我们现在用的 `Objective-C 2.0` 采用的是现行 (`Modern`) 版的 `Runtime` 系统,只能运行在 `iOS` 和 `macOS 10.5` 之后的 `64` 位程序中。而 `macOS` 较老的`32`位程序仍采用 `Objective-C 1` 中的(早期)`Legacy` 版本的 `Runtime` 系统。这两个版本最大的区别在于当你更改一个类的实例变量的布局时,在早期版本中你需要重新编译它的子类,而现行版就不需要。 `Runtime` 基本是用 `C` 和`汇编`写的,可见苹果为了动态系统的高效而作出的努力。你可以在[这里](https://links.jianshu.com/go?to=http%3A%2F%2Fwww.opensource.apple.com%2Fsource%2Fobjc4%2F)下到苹果维护的开源代码。苹果和GNU各自维护一个开源的 [runtime](https://links.jianshu.com/go?to=https%3A%2F%2Fgithub.com%2FRetVal%2Fobjc-runtime) 版本,这两个版本之间都在努力的保持一致。 平时的业务中主要是使用[官方Api](https://links.jianshu.com/go?to=https%3A%2F%2Fdeveloper.apple.com%2Freference%2Fobjectivec%2Fobjective_c_runtime%23%2F%2Fapple_ref%2Fdoc%2Fuid%2FTP40001418-CH1g-126286),解决我们框架性的需求。 高级编程语言想要成为可执行文件需要先编译为汇编语言再汇编为机器语言,机器语言也是计算机能够识别的唯一语言,但是`OC`并不能直接编译为汇编语言,而是要先转写为纯`C`语言再进行编译和汇编的操作,从`OC`到`C`语言的过渡就是由runtime来实现的。然而我们使用`OC`进行面向对象开发,而`C`语言更多的是面向过程开发,这就需要将面向对象的类转变为面向过程的结构体。 ## 2、Runtime消息传递 一个对象的方法像这样`[obj foo]`,编译器转成消息发送`objc_msgSend(obj, foo)`,`Runtime`时执行的流程是这样的: - 首先,通过`obj`的`isa`指针找到它的 `class` ; - 在 `class` 的 `method list` 找 `foo` ; - 如果 `class` 中没到 `foo`,继续往它的 `superclass` 中找 ; - 一旦找到 `foo` 这个函数,就去执行它的实现`IMP` 。 但这种实现有个问题,效率低。但一个`class` 往往只有 `20%` 的函数会被经常调用,可能占总调用次数的 `80%` 。每个消息都需要遍历一次`objc_method_list` 并不合理。如果把经常被调用的函数缓存下来,那可以大大提高函数查询的效率。这也就是`objc_class` 中另一个重要成员`objc_cache` 做的事情 - 再找到`foo` 之后,把`foo` 的`method_name` 作为`key` ,`method_imp`作为`value` 给存起来。当再次收到`foo` 消息的时候,可以直接在`cache` 里找到,避免去遍历`objc_method_list`。从前面的源代码可以看到`objc_cache`是存在`objc_class` 结构体中的。 objec_msgSend的方法定义如下: ```objectivec OBJC_EXPORT id objc_msgSend(id self, SEL op, ...) ``` 那消息传递是怎么实现的呢?我们看看对象(object),类(class),方法(method)这几个的结构体: ```cpp //对象 struct objc_object { Class isa OBJC_ISA_AVAILABILITY; }; //类 struct objc_class { Class isa OBJC_ISA_AVAILABILITY; #if !__OBJC2__ Class super_class OBJC2_UNAVAILABLE; const char *name OBJC2_UNAVAILABLE; long version OBJC2_UNAVAILABLE; long info OBJC2_UNAVAILABLE; long instance_size OBJC2_UNAVAILABLE; struct objc_ivar_list *ivars OBJC2_UNAVAILABLE; struct objc_method_list **methodLists OBJC2_UNAVAILABLE; struct objc_cache *cache OBJC2_UNAVAILABLE; struct objc_protocol_list *protocols OBJC2_UNAVAILABLE; #endif } OBJC2_UNAVAILABLE; //方法列表 struct objc_method_list { struct objc_method_list *obsolete OBJC2_UNAVAILABLE; int method_count OBJC2_UNAVAILABLE; #ifdef __LP64__ int space OBJC2_UNAVAILABLE; #endif /* variable length structure */ struct objc_method method_list[1] OBJC2_UNAVAILABLE; } OBJC2_UNAVAILABLE; //方法 struct objc_method { SEL method_name OBJC2_UNAVAILABLE; char *method_types OBJC2_UNAVAILABLE; IMP method_imp OBJC2_UNAVAILABLE; } ``` 1. 系统首先找到消息的接收对象,然后通过对象的`isa`找到它的类。 2. 在它的类中查找`method_list`,是否有`selector`方法。 3. 没有则查找父类的`method_list`。 4. 找到对应的`method`,执行它的`IMP`。 5. 转发`IMP`的`return`值。 下面讲讲消息传递用到的一些概念: - 类对象(objc_class) - 实例(objc_object) - 元类(Meta Class) - Method(objc_method) - SEL(objc_selector) - IMP - 类缓存(objc_cache) - Category(objc_category) ### 2.1 类对象(objc_class) `Objective-C`类是由`Class`类型来表示的,它实际上是一个指向`objc_class`结构体的指针。 ```cpp typedef struct objc_class *Class; ``` 查看`objc/runtime.h`中`objc_class`结构体的定义如下: ```cpp struct objc_class { Class _Nonnull isa OBJC_ISA_AVAILABILITY; #if !__OBJC2__ Class _Nullable super_class OBJC2_UNAVAILABLE; const char * _Nonnull name OBJC2_UNAVAILABLE; long version OBJC2_UNAVAILABLE; long info OBJC2_UNAVAILABLE; long instance_size OBJC2_UNAVAILABLE; struct objc_ivar_list * _Nullable ivars OBJC2_UNAVAILABLE; struct objc_method_list * _Nullable * _Nullable methodLists OBJC2_UNAVAILABLE; struct objc_cache * _Nonnull cache OBJC2_UNAVAILABLE; struct objc_protocol_list * _Nullable protocols OBJC2_UNAVAILABLE; #endif } OBJC2_UNAVAILABLE; ``` `struct objc_class`结构体定义了很多变量,通过命名不难发现, 结构体里保存了指向父类的指针、类的名字、版本、实例大小、实例变量列表、方法列表、缓存、遵守的协议列表等, 一个类包含的信息也不就正是这些吗?没错,类对象就是一个结构体`struct objc_class`,这个结构体存放的数据称为元数据(`metadata`), 该结构体的第一个成员变量也是`isa`指针,这就说明了`Class`本身其实也是一个对象,因此我们称之为类对象,类对象在编译期产生用于创建实例对象,是单例。 ### 2.2 实例(objc_object) ```cpp /// Represents an instance of a class. struct objc_object { Class isa OBJC_ISA_AVAILABILITY; }; /// A pointer to an instance of a class. typedef struct objc_object *id; ``` 类对象中的元数据存储的都是如何创建一个实例的相关信息,那么类对象和类方法应该从哪里创建呢? 就是从`isa`指针指向的结构体创建,类对象的`isa`指针指向的我们称之为元类(`metaclass`), 元类中保存了创建类对象以及类方法所需的所有信息,因此整个结构应该如下图所示: ![img](https:////upload-images.jianshu.io/upload_images/301129-cc9c0a7ffb147fed.png?imageMogr2/auto-orient/strip|imageView2/2/w/847/format/webp) ### 2.3 元类(Meta Class) 通过上图我们可以看出整个体系构成了一个自闭环,`struct objc_object`结构体`实例`它的`isa`指针指向类对象, 类对象的`isa`指针指向了元类,`super_class`指针指向了父类的类对象, 而元类的`super_class`指针指向了父类的元类,那元类的`isa`指针又指向了自己。 元类(Meta Class)是一个类对象的类。 在上面我们提到,所有的类自身也是一个对象,我们可以向这个对象发送消息(即调用类方法)。 为了调用类方法,这个类的`isa`指针必须指向一个包含这些类方法的一个`objc_class`结构体。这就引出了`meta-class`的概念,元类中保存了创建类对象以及类方法所需的所有信息。 任何`NSObject`继承体系下的`meta-class`都使用`NSObject`的`meta-class`作为自己的所属类,而基类的`meta-class`的`isa`指针是指向它自己。 ### 2.4 Method(objc_method) 先看下定义 ```cpp runtime.h /// An opaque type that represents a method in a class definition.代表类定义中一个方法的不透明类型 typedef struct objc_method *Method; struct objc_method { SEL method_name OBJC2_UNAVAILABLE; char *method_types OBJC2_UNAVAILABLE; IMP method_imp OBJC2_UNAVAILABLE; ``` `Method`和我们平时理解的函数是一致的,就是表示能够独立完成一个功能的一段代码,比如: ```objectivec - (void)logName { NSLog(@"name"); } ``` 这段代码,就是一个函数。 我们来看下`objc_method`这个结构体的内容: - SEL method_name 方法名 - char *method_types 方法类型 - IMP method_imp 方法实现 在这个结构体重,我们已经看到了`SEL`和`IMP`,说明`SEL`和`IMP`其实都是`Method`的属性。 我们接着来看`SEL`。 ### 2.5 SEL(objc_selector) 先看下定义 ```cpp Objc.h /// An opaque type that represents a method selector.代表一个方法的不透明类型 typedef struct objc_selector *SEL; ``` `objc_msgSend`函数第二个参数类型为`SEL`,它是`selector`在`Objective-C`中的表示类型(`Swift`中是`Selector`类)。`selector`是方法选择器,可以理解为区分方法的 `ID`,而这个 `ID` 的数据结构是`SEL`: ```css @property SEL selector; ``` 可以看到`selector`是`SEL`的一个实例。 ```csharp A method selector is a C string that has been registered (or “mapped“) with the Objective-C runtime. Selectors generated by the compiler are automatically mapped by the runtime when the class is loaded. ``` 其实`selector`就是个映射到方法的`C`字符串,你可以用 `Objective-C` 编译器命令`@selector()`或者 `Runtime` 系统的`sel_registerName`函数来获得一个 `SEL` 类型的方法选择器。 `selector`既然是一个`string`,我觉得应该是类似`className+method`的组合,命名规则有两条: - 同一个类,selector不能重复 - 不同的类,selector可以重复 这也带来了一个弊端,我们在写`C`代码的时候,经常会用到函数重载,就是函数名相同,参数不同,但是这在`Objective-C`中是行不通的,因为`selector`只记了`method`的`name`,没有参数,所以没法区分不同的`method`。 比如: ```dart - (void)caculate(NSInteger)num; - (void)caculate(CGFloat)num; ``` 是会报错的。 我们只能通过命名来区别: ```dart - (void)caculateWithInt(NSInteger)num; - (void)caculateWithFloat(CGFloat)num; ``` 在不同类中相同名字的方法所对应的方法选择器是相同的,即使方法名字相同而变量类型不同也会导致它们具有相同的方法选择器。 ### 2.6 IMP 看下`IMP`的定义 ```cpp /// A pointer to the function of a method implementation. 指向一个方法实现的指针 typedef id (*IMP)(id, SEL, ...); #endif ``` 就是指向最终实现程序的内存地址的指针。 在`iOS`的`Runtime`中,`Method`通过`selector`和`IMP`两个属性,实现了快速查询方法及实现,相对提高了性能,又保持了灵活性。 ### 2.7 类缓存(objc_cache) 当`Objective-C`运行时通过跟踪它的`isa`指针检查对象时,它可以找到一个实现许多方法的对象。然而,你可能只调用它们的一小部分,并且每次查找时,搜索所有选择器的类分派表没有意义。所以类实现一个缓存,每当你搜索一个类分派表,并找到相应的选择器,它把它放入它的缓存。所以当`objc_msgSend`查找一个类的选择器,它首先搜索类缓存。这是基于这样的理论:如果你在类上调用一个消息,你可能以后再次调用该消息。 为了加速消息分发, 系统会对方法和对应的地址进行缓存,就放在上述的`objc_cache`,所以在实际运行中,大部分常用的方法都是会被缓存起来的,`Runtime`系统实际上非常快,接近直接执行内存地址的程序速度。 ### 2.8 Category(objc_category) `Category`是表示一个指向分类的结构体的指针,其定义如下: ```cpp struct category_t { const char *name; classref_t cls; struct method_list_t *instanceMethods; struct method_list_t *classMethods; struct protocol_list_t *protocols; struct property_list_t *instanceProperties; }; ``` ```undefined name:是指 class_name 而不是 category_name。 cls:要扩展的类对象,编译期间是不会定义的,而是在Runtime阶段通过name对 应到对应的类对象。 instanceMethods:category中所有给类添加的实例方法的列表。 classMethods:category中所有添加的类方法的列表。 protocols:category实现的所有协议的列表。 instanceProperties:表示Category里所有的properties,这就是我们可以通过objc_setAssociatedObject和objc_getAssociatedObject增加实例变量的原因,不过这个和一般的实例变量是不一样的。 ``` **从上面的`category_t`的结构体中可以看出,分类中可以添加实例方法,类方法,甚至可以实现协议,添加属性,不可以添加成员变量。** ### 2.9 Runtime消息转发 前文介绍了进行一次发送消息会在相关的类对象中搜索方法列表,如果找不到则会沿着继承树向上一直搜索知道继承树根部(通常为`NSObject`),如果还是找不到并且消息转发都失败了就回执行`doesNotRecognizeSelector:`方法报`unrecognized selector`错。那么消息转发到底是什么呢?接下来将会逐一介绍最后的三次机会。 - 动态方法解析 - 备用接收者 - 完整消息转发 ![img](https:////upload-images.jianshu.io/upload_images/301129-a1159ef51f453da8.png?imageMogr2/auto-orient/strip|imageView2/2/w/1039/format/webp) ### 2.10 动态方法解析 首先,`Objective-C`运行时会调用 `+resolveInstanceMethod:`或者 `+resolveClassMethod:`,让你有机会提供一个函数实现。如果你添加了函数并返回`YES`, 那运行时系统就会重新启动一次消息发送的过程。 实现一个动态方法解析的例子如下: ```objectivec - (void)viewDidLoad { [super viewDidLoad]; // Do any additional setup after loading the view, typically from a nib. //执行foo函数 [self performSelector:@selector(foo:)]; } + (BOOL)resolveInstanceMethod:(SEL)sel { if (sel == @selector(foo:)) {//如果是执行foo函数,就动态解析,指定新的IMP class_addMethod([self class], sel, (IMP)fooMethod, "v@:"); return YES; } return [super resolveInstanceMethod:sel]; } void fooMethod(id obj, SEL _cmd) { NSLog(@"Doing foo");//新的foo函数 } ``` > 打印结果: > 2018-04-01 12:23:35.952670+0800 ocram[87546:23235469] Doing foo 可以看到虽然没有实现`foo:`这个函数,但是我们通过`class_addMethod`动态添加`fooMethod`函数,并执行`fooMethod`这个函数的`IMP`。从打印结果看,成功实现了。 如果`resolve`方法返回 `NO` ,运行时就会移到下一步:`forwardingTargetForSelector`。 ### 2.11 备用接收者 如果目标对象实现了`-forwardingTargetForSelector:`,`Runtime` 这时就会调用这个方法,给你把这个消息转发给其他对象的机会。 实现一个备用接收者的例子如下: ```objectivec #import "ViewController.h" #import "objc/runtime.h" @interface Person: NSObject @end @implementation Person - (void)foo { NSLog(@"Doing foo");//Person的foo函数 } @end @interface ViewController () @end @implementation ViewController - (void)viewDidLoad { [super viewDidLoad]; // Do any additional setup after loading the view, typically from a nib. //执行foo函数 [self performSelector:@selector(foo)]; } + (BOOL)resolveInstanceMethod:(SEL)sel { return YES;//返回YES,进入下一步转发 } - (id)forwardingTargetForSelector:(SEL)aSelector { if (aSelector == @selector(foo)) { return [Person new];//返回Person对象,让Person对象接收这个消息 } return [super forwardingTargetForSelector:aSelector]; } @end ``` > 打印结果: > 2018-04-01 12:45:04.757929+0800 ocram[88023:23260346] Doing foo 可以看到我们通过`forwardingTargetForSelector`把当前`ViewController`的方法转发给了`Person`去执行了。打印结果也证明我们成功实现了转发。 ### 2.12 完整消息转发 如果在上一步还不能处理未知消息,则唯一能做的就是启用完整的消息转发机制了。 首先它会发送`-methodSignatureForSelector:`消息获得函数的参数和返回值类型。如果`-methodSignatureForSelector:`返回`nil` ,`Runtime`则会发出 `-doesNotRecognizeSelector:` 消息,程序这时也就挂掉了。如果返回了一个函数签名,`Runtime`就会创建一个`NSInvocation` 对象并发送 `-forwardInvocation:`消息给目标对象。 实现一个完整转发的例子如下: ```objectivec #import "ViewController.h" #import "objc/runtime.h" @interface Person: NSObject @end @implementation Person - (void)foo { NSLog(@"Doing foo");//Person的foo函数 } @end @interface ViewController () @end @implementation ViewController - (void)viewDidLoad { [super viewDidLoad]; // Do any additional setup after loading the view, typically from a nib. //执行foo函数 [self performSelector:@selector(foo)]; } + (BOOL)resolveInstanceMethod:(SEL)sel { return YES;//返回YES,进入下一步转发 } - (id)forwardingTargetForSelector:(SEL)aSelector { return nil;//返回nil,进入下一步转发 } - (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector { if ([NSStringFromSelector(aSelector) isEqualToString:@"foo"]) { return [NSMethodSignature signatureWithObjCTypes:"v@:"];//签名,进入forwardInvocation } return [super methodSignatureForSelector:aSelector]; } - (void)forwardInvocation:(NSInvocation *)anInvocation { SEL sel = anInvocation.selector; Person *p = [Person new]; if([p respondsToSelector:sel]) { [anInvocation invokeWithTarget:p]; } else { [self doesNotRecognizeSelector:sel]; } } @end ``` > 打印结果: > 2018-04-01 13:00:45.423385+0800 ocram[88353:23279961] Doing foo 从打印结果来看,我们实现了完整的转发。通过签名,`Runtime`生成了一个对象`anInvocation`,发送给了`forwardInvocation`,我们在`forwardInvocation`方法里面让`Person`对象去执行了`foo`函数。签名参数`v@:`怎么解释呢,这里苹果文档[Type Encodings](https://links.jianshu.com/go?to=https%3A%2F%2Fdeveloper.apple.com%2Flibrary%2Fcontent%2Fdocumentation%2FCocoa%2FConceptual%2FObjCRuntimeGuide%2FArticles%2FocrtTypeEncodings.html%23%2F%2Fapple_ref%2Fdoc%2Fuid%2FTP40008048-CH100-SW1)有详细的解释。 以上就是`Runtime`的三次转发流程。下面我们讲讲`Runtime`的实际应用。 ## 3、Runtime应用 `Runtime`简直就是做大型框架的利器。它的应用场景非常多,下面就介绍一些常见的应用场景。 - 关联对象(Objective-C Associated Objects)给分类增加属性 - 方法魔法(Method Swizzling)方法添加和替换和KVO实现 - 消息转发(热更新)解决Bug(JSPatch) - 实现NSCoding的自动归档和自动解档 - 实现字典和模型的自动转换(MJExtension) ### 3.1 关联对象(Objective-C Associated Objects)给分类增加属性 我们都是知道分类是不能自定义属性和变量的。下面通过关联对象实现给分类添加属性。 关联对象Runtime提供了下面几个接口: ```objectivec //关联对象 void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy) //获取关联的对象 id objc_getAssociatedObject(id object, const void *key) //移除关联的对象 void objc_removeAssociatedObjects(id object) ``` 参数解释 ```csharp id object:被关联的对象 const void *key:关联的key,要求唯一 id value:关联的对象 objc_AssociationPolicy policy:内存管理的策略 ``` 内存管理的策略 ```php typedef OBJC_ENUM(uintptr_t, objc_AssociationPolicy) { OBJC_ASSOCIATION_ASSIGN = 0, /**< Specifies a weak reference to the associated object. */ OBJC_ASSOCIATION_RETAIN_NONATOMIC = 1, /**< Specifies a strong reference to the associated object. * The association is not made atomically. */ OBJC_ASSOCIATION_COPY_NONATOMIC = 3, /**< Specifies that the associated object is copied. * The association is not made atomically. */ OBJC_ASSOCIATION_RETAIN = 01401, /**< Specifies a strong reference to the associated object. * The association is made atomically. */ OBJC_ASSOCIATION_COPY = 01403 /**< Specifies that the associated object is copied. * The association is made atomically. */ }; ``` 下面实现一个`UIView`的`Category`添加自定义属性`defaultColor`。 ```objectivec #import "ViewController.h" #import "objc/runtime.h" @interface UIView (DefaultColor) @property (nonatomic, strong) UIColor *defaultColor; @end @implementation UIView (DefaultColor) @dynamic defaultColor; static char kDefaultColorKey; - (void)setDefaultColor:(UIColor *)defaultColor { objc_setAssociatedObject(self, &kDefaultColorKey, defaultColor, OBJC_ASSOCIATION_RETAIN_NONATOMIC); } - (id)defaultColor { return objc_getAssociatedObject(self, &kDefaultColorKey); } @end @interface ViewController () @end @implementation ViewController - (void)viewDidLoad { [super viewDidLoad]; // Do any additional setup after loading the view, typically from a nib. UIView *test = [UIView new]; test.defaultColor = [UIColor blackColor]; NSLog(@"%@", test.defaultColor); } @end ``` > 打印结果: > 2018-04-01 15:41:44.977732+0800 ocram[2053:63739] UIExtendedGrayColorSpace 0 1 打印结果来看,我们成功在分类上添加了一个属性,实现了它的`setter`和`getter`方法。 通过关联对象实现的属性的内存管理也是有`ARC`管理的,所以我们只需要给定适当的内存策略就行了,不需要操心对象的释放。 我们看看内存测量对于的属性修饰。 | 内存策略 | 属性修饰 | 描述 | | --------------------------------- | --------------------------------------------------- | ------------------------------------------------------------ | | OBJC_ASSOCIATION_ASSIGN | @property (assign) 或 @property (unsafe_unretained) | 指定一个关联对象的弱引用。 | | OBJC_ASSOCIATION_RETAIN_NONATOMIC | @property (nonatomic, strong) | @property (nonatomic, strong) 指定一个关联对象的强引用,不能被原子化使用。 | | OBJC_ASSOCIATION_COPY_NONATOMIC | @property (nonatomic, copy) | 指定一个关联对象的copy引用,不能被原子化使用。 | | OBJC_ASSOCIATION_RETAIN | @property (atomic, strong) | 指定一个关联对象的强引用,能被原子化使用。 | | OBJC_ASSOCIATION_COPY | @property (atomic, copy) | 指定一个关联对象的copy引用,能被原子化使用。 | ### 3.2 方法魔法(Method Swizzling)方法添加和替换和KVO实现 #### 3.2.1 方法添加 实际上添加方法刚才在讲消息转发的时候,动态方法解析的时候就提到了。 ```kotlin //class_addMethod(Class _Nullable __unsafe_unretained cls, SEL _Nonnull name, IMP _Nonnull imp, const char * _Nullable types) class_addMethod([self class], sel, (IMP)fooMethod, "v@:"); ``` - cls 被添加方法的类 - name 添加的方法的名称的SEL - imp 方法的实现。该函数必须至少要有两个参数,self,_cmd - 类型编码 #### 3.2.2 方法替换 下面实现一个替换`ViewController`的`viewDidLoad`方法的例子。 ```kotlin @implementation ViewController + (void)load { static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ Class class = [self class]; SEL originalSelector = @selector(viewDidLoad); SEL swizzledSelector = @selector(jkviewDidLoad); Method originalMethod = class_getInstanceMethod(class,originalSelector); Method swizzledMethod = class_getInstanceMethod(class,swizzledSelector); //judge the method named swizzledMethod is already existed. BOOL didAddMethod = class_addMethod(class, originalSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod)); // if swizzledMethod is already existed. if (didAddMethod) { class_replaceMethod(class, swizzledSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod)); } else { method_exchangeImplementations(originalMethod, swizzledMethod); } }); } - (void)jkviewDidLoad { NSLog(@"替换的方法"); [self jkviewDidLoad]; } - (void)viewDidLoad { NSLog(@"自带的方法"); [super viewDidLoad]; } @end ``` `swizzling`应该只在`+load`中完成。 在 `Objective-C` 的运行时中,每个类有两个方法都会自动调用。`+load` 是在一个类被初始装载时调用,`+initialize` 是在应用第一次调用该类的类方法或实例方法前调用的。两个方法都是可选的,并且只有在方法被实现的情况下才会被调用。 `swizzling`应该只在`dispatch_once` 中完成,由于`swizzling` 改变了全局的状态,所以我们需要确保每个预防措施在运行时都是可用的。原子操作就是这样一个用于确保代码只会被执行一次的预防措施,就算是在不同的线程中也能确保代码只执行一次。`Grand Central Dispatch 的 dispatch_once`满足了所需要的需求,并且应该被当做使用`swizzling` 的初始化单例方法的标准。 实现图解如下图。 ![img](https:////upload-images.jianshu.io/upload_images/301129-dbc739cb752e60c3.png?imageMogr2/auto-orient/strip|imageView2/2/w/581/format/webp) > 从图中可以看出,我们通过swizzling特性,将selectorC的方法实现IMPc与selectorN的方法实现IMPn交换了,当我们调用selectorC,也就是给对象发送selectorC消息时,所查找到的对应的方法实现就是IMPn而不是IMPc了。 #### 3.2.3 KVO实现 > 全称是Key-value observing,翻译成键值观察。提供了一种当其它对象属性被修改的时候能通知当前对象的机制。再MVC大行其道的Cocoa中,KVO机制很适合实现model和controller类之间的通讯。 `KVO`的实现依赖于 `Objective-C` 强大的 `Runtime`,当观察某对象 `A` 时,`KVO` 机制动态创建一个对象`A`当前类的子类,并为这个新的子类重写了被观察属性 `keyPath` 的 `setter` 方法。`setter` 方法随后负责通知观察对象属性的改变状况。 `Apple` 使用了 `isa-swizzling` 来实现 `KVO` 。当观察对象`A`时,`KVO`机制动态创建一个新的名为:`NSKVONotifying_A`的新类,该类继承自对象A的本类,且 `KVO` 为 `NSKVONotifying_A` 重写观察属性的 `setter` 方法,`setter` 方法会负责在调用原 `setter` 方法之前和之后,通知所有观察对象属性值的更改情况。 - NSKVONotifying_A 类剖析 ```objectivec NSLog(@"self->isa:%@",self->isa); NSLog(@"self class:%@",[self class]); ``` 在建立KVO监听前,打印结果为: ```ruby self->isa:A self class:A ``` 在建立KVO监听之后,打印结果为: ```objectivec self->isa:NSKVONotifying_A self class:A ``` 在这个过程,被观察对象的 `isa` 指针从指向原来的 `A` 类,被`KVO` 机制修改为指向系统新创建的子类`NSKVONotifying_A` 类,来实现当前类属性值改变的监听; 所以当我们从应用层面上看来,完全没有意识到有新的类出现,这是系统“隐瞒”了对 `KVO` 的底层实现过程,让我们误以为还是原来的类。但是此时如果我们创建一个新的名为“`NSKVONotifying_A`”的类,就会发现系统运行到注册 `KVO` 的那段代码时程序就崩溃,因为系统在注册监听的时候动态创建了名为 `NSKVONotifying_A` 的中间类,并指向这个中间类了。 - 子类setter方法剖析 `KVO` 的键值观察通知依赖于 NSObject 的两个方法:`willChangeValueForKey:`和 `didChangeValueForKey:` ,在存取数值的前后分别调用 2 个方法: 被观察属性发生改变之前,`willChangeValueForKey:`被调用,通知系统该 `keyPath` 的属性值即将变更; 当改变发生后, `didChangeValueForKey:` 被调用,通知系统该`keyPath` 的属性值已经变更;之后, `observeValueForKey:ofObject:change:context:`也会被调用。且重写观察属性的`setter` 方法这种继承方式的注入是在运行时而不是编译时实现的。 `KVO` 为子类的观察者属性重写调用存取方法的工作原理在代码中相当于: ```objectivec - (void)setName:(NSString *)newName { [self willChangeValueForKey:@"name"]; //KVO 在调用存取方法之前总调用 [super setValue:newName forKey:@"name"]; //调用父类的存取方法 [self didChangeValueForKey:@"name"]; //KVO 在调用存取方法之后总调用 } ``` ### 3.3 消息转发(热更新)解决Bug(JSPatch) > [JSPatch](https://links.jianshu.com/go?to=https%3A%2F%2Fgithub.com%2Fbang590%2FJSPatch%2Fwiki%2FJSPatch-%E5%AE%9E%E7%8E%B0%E5%8E%9F%E7%90%86%E8%AF%A6%E8%A7%A3) 是一个 iOS 动态更新框架,只需在项目中引入极小的引擎,就可以使用 JavaScript 调用任何 Objective-C 原生接口,获得脚本语言的优势:为项目动态添加模块,或替换项目原生代码动态修复 bug。 关于消息转发,前面已经讲到过了,消息转发分为三级,我们可以在每级实现替换功能,实现消息转发,从而不会造成崩溃。[JSPatch](https://links.jianshu.com/go?to=https%3A%2F%2Fgithub.com%2Fbang590%2FJSPatch%2Fwiki%2FJSPatch-%E5%AE%9E%E7%8E%B0%E5%8E%9F%E7%90%86%E8%AF%A6%E8%A7%A3)不仅能够实现消息转发,还可以实现方法添加、替换能一系列功能。 ### 3.4 实现NSCoding的自动归档和自动解档 原理描述:用`runtime`提供的函数遍历`Model`自身所有属性,并对属性进行`encode`和`decode`操作。 核心方法:在`Model`的基类中重写方法: ```objectivec - (id)initWithCoder:(NSCoder *)aDecoder { if (self = [super init]) { unsigned int outCount; Ivar * ivars = class_copyIvarList([self class], &outCount); for (int i = 0; i < outCount; i ++) { Ivar ivar = ivars[i]; NSString * key = [NSString stringWithUTF8String:ivar_getName(ivar)]; [self setValue:[aDecoder decodeObjectForKey:key] forKey:key]; } } return self; } - (void)encodeWithCoder:(NSCoder *)aCoder { unsigned int outCount; Ivar * ivars = class_copyIvarList([self class], &outCount); for (int i = 0; i < outCount; i ++) { Ivar ivar = ivars[i]; NSString * key = [NSString stringWithUTF8String:ivar_getName(ivar)]; [aCoder encodeObject:[self valueForKey:key] forKey:key]; } } ``` ### 3.5 实现字典和模型的自动转换(MJExtension) 原理描述:用`runtime`提供的函数遍历`Model`自身所有属性,如果属性在`json`中有对应的值,则将其赋值。 核心方法:在`NSObject`的分类中添加方法 ```objectivec - (instancetype)initWithDict:(NSDictionary *)dict { if (self = [self init]) { //(1)获取类的属性及属性对应的类型 NSMutableArray * keys = [NSMutableArray array]; NSMutableArray * attributes = [NSMutableArray array]; /* * 例子 * name = value3 attribute = T@"NSString",C,N,V_value3 * name = value4 attribute = T^i,N,V_value4 */ unsigned int outCount; objc_property_t * properties = class_copyPropertyList([self class], &outCount); for (int i = 0; i < outCount; i ++) { objc_property_t property = properties[i]; //通过property_getName函数获得属性的名字 NSString * propertyName = [NSString stringWithCString:property_getName(property) encoding:NSUTF8StringEncoding]; [keys addObject:propertyName]; //通过property_getAttributes函数可以获得属性的名字和@encode编码 NSString * propertyAttribute = [NSString stringWithCString:property_getAttributes(property) encoding:NSUTF8StringEncoding]; [attributes addObject:propertyAttribute]; } //立即释放properties指向的内存 free(properties); //(2)根据类型给属性赋值 for (NSString * key in keys) { if ([dict valueForKey:key] == nil) continue; [self setValue:[dict valueForKey:key] forKey:key]; } } return self; } ``` 以上就是`Runtime`应用的一些场景,本文到此结束了。 原文链接:https://www.jianshu.com/p/6ebda3cd8052 ================================================ FILE: iOS资料/iOS 入门(2):管理第三方库.md ================================================ # iOS 入门(2):管理第三方库 ## **1、安装 CocoaPods** 代码复用是提高工程开发效率的重要方法,使用第三方库就是一种普遍的方式。在 iOS 开发中使用最广泛的管理第三方库的方案就是使用 CocoaPods。 1)安装 Ruby 环境。CocoaPods 是使用 Ruby 实现的,可以通过 gem 命令来安装,Mac OS X 中一般自带 Ruby 环境。接下来将默认的 RubyGems 替换为淘宝的 RubyGems 镜像,速度要快很多。 ```text $ sudo gem sources -a https://ruby.taobao.org/$ sudo gem sources -r https://rubygems.org/$ sudo gem sources -l ``` 2)安装 CocoaPods。 ```text $ sudo gem update$ sudo gem install -n /usr/local/bin cocoapods -v 0.39$ pod setup$ pod --version ``` ## **2、在当前项目中引入 CocoaPods 和第三方库** 1)安装好 CocoaPods 后,接着我们前面讲的项目,在项目的根目录下创建一个名为 `Podfile` 的文件。 ![img](https://pic2.zhimg.com/80/v2-280f081b8ed28045bf7f4b77c2c7b7d1_720w.webp) 在文件中添加如下内容: ```text source 'https://github.com/CocoaPods/Specs.git'platform :ios, "8.0"target "iOSStartDemo" do pod 'SVProgressHUD', '1.1.3' pod 'Masonry', '0.6.3'end ``` 代码解释:我们通过 CocoaPods 引用了两个第三方库:`SVProgressHUD` 一个展示各种类型提示信息的库;`Masonry` 是一个封装了 Autolayout API 使得它们更易使用的库。 2)在 Terminal 命令行中进入项目的根目录(即上面创建的 Podfile 所在的目录)。执行下列命令来安装第三方库: ```text $ pod install ``` 如果成功执行,将会为你生成一个 `iOSStartDemo.xcworkspace` 文件。如果你在 Xcode 中已经打开了 iOSStartDemo 项目,那么先关闭它,然后双击 iOSStartDemo.xcworkspace 文件或者在命令行下执行: ```text $ open iOSStartDemo.xcworkspace ``` 即可用 Xcode 打开新的项目。 ![img](https://pic2.zhimg.com/80/v2-0404563fd9389c57d0d96f3d7bb19b75_720w.webp) ## **3、在代码中使用三方库。** 修改 `STMainViewController.m` 代码如下: ```text #import "STMainViewController.h"#import #import @interface STMainViewController ()@end@implementation STMainViewController#pragma mark - Lifecycle- (void)viewDidLoad { [super viewDidLoad]; // Do any additional setup after loading the view, typically from a nib. // Setup. [self setupUI];}#pragma mark - Setup- (void)setupUI { // Hello button. UIButton *helloButton = [UIButton buttonWithType:UIButtonTypeSystem]; [helloButton setTitle:@"Hello" forState:UIControlStateNormal]; [helloButton addTarget:self action:@selector(onHelloButtonClicked:) forControlEvents:UIControlEventTouchUpInside]; [self.view addSubview:helloButton]; [helloButton mas_makeConstraints:^(MASConstraintMaker *make) { make.width.equalTo(@60.0); make.height.equalTo(@40.0); make.center.equalTo(self.view); }];}#pragma mark - Action- (void)onHelloButtonClicked:(id)sender { NSLog(@"Hello, world!"); [SVProgressHUD showSuccessWithStatus:@"Hello, world!" maskType:SVProgressHUDMaskTypeBlack];}@end ``` 代码解释:通过 `#import ` 和 `#import ` 引用第三方库。将 `helloButton` 的布局代码用 `Masonry` 重写;用 `SVProgressHUD` 替代 `UIAlertController` 展示信息。 修改后你看到的界面如下: ![img](https://pic3.zhimg.com/80/v2-18bbd0de0ef8d0d403b1223efbc614a6_720w.webp) 最后,我一直认为对于一门语言的初学者来说,了解该语言的标准编码风格是十分紧要的事情之一,这样可以使得你的代码与周围的环境和谐一致,也能便于你去了解这门语言的一些设计思想。如果你想要了解 Objective-C 的编码风格,你可以看看:[Objective-C 编码风格指南](https://link.zhihu.com/?target=http%3A//www.samirchen.com/objective-c-style-guide)。 ## **4、Demo** 如果你还没有下载 iOSStartDemo,请先执行下列命令下载: ```text $ git clone https://github.com/samirchen/iOSStartDemo.git$ cd iOSStartDemo/iOSStartDemo ``` 如果已经下载过了,则接着进入正确的目录并执行下列命令: ```text $ git fetch origin s2$ git checkout s2$ pod install$ open iOSStartDemo.xcworkspace ``` 原文 http://www.samirchen.com/ios-start-2/ ================================================ FILE: iOS资料/iOS 离屏渲染探究.md ================================================ # iOS 离屏渲染探究 ## 1.为什么要理解离屏渲染 离屏渲染(Offscreen rendering)对iOS开发者来说不是一个陌生的东西,项目中或多或少都会存在离屏渲染,也是面试中经常考察的知识点。一般来说,大多数人都能知道设置圆角、mask、阴影等会触发离屏渲染,但我们深入的探究一下,大家能够很清楚的知道下面几个问题吗? - 离屏渲染是在哪一步发生的吗? - 离屏渲染产生的原因是什么呢? - 设置圆角一定会触发离屏渲染吗? - 离屏渲染既然会影响性能我们为什么还要使用呢?优化方案又有那些? 今天我就带着这几个问题探究一下离屏渲染。 ## 2.ios平台的渲染框架 ![img](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/7/3/17310948698d1b33~tplv-t2oaga2asx-zoom-in-crop-mark:3024:0:0:0.awebp) ## 3.Core Animation 流水线: ![img](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/7/8/1732f1743c0078c7~tplv-t2oaga2asx-zoom-in-crop-mark:3024:0:0:0.awebp) 这是在WWDC的Advanced Graphics and Animations for iOS Apps(WWDC14 419)中有这样一张图,我们可以看到,在Application这一层中主要是CPU在操作,而到了Render Server这一层,CoreAnimation会将具体操作转换成发送给GPU的draw calls(以前是call OpenGL ES,现在慢慢转到了Metal),显然CPU和GPU双方同处于一个流水线中,协作完成整个渲染工作。我们也可以把iOS下的Core Animation可以理解为一个复合引擎,主要职责包含:渲染、构建和实现动画。 ## 4.离屏渲染的定义 1. OpenGL中,GPU屏幕渲染有以下两种方式当前屏幕渲染(On-Screen Rendering):正常情况下,我们在屏幕上显示都是GPU读取帧缓冲区(Frame Buffer)渲染好的的数据,然后显示在屏幕上。流程如图:![img](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/7/9/1732f3543028519f~tplv-t2oaga2asx-zoom-in-crop-mark:3024:0:0:0.awebp) 2. (Off-Screen Rendering ):如果有时因为一些限制,无法把渲染结果直接写入frame buffer,而是先暂存在另外的内存区域,之后再写入frame buffer,那么这个过程被称之为离屏渲染。也就是GPU需要在当前屏幕缓冲区以外新开辟一个缓冲区进行渲染操作。流程如图:![img](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/7/9/173313b3aebc4252~tplv-t2oaga2asx-zoom-in-crop-mark:3024:0:0:0.awebp) 在上面的CoreAnimation流水线示意图中,我们可以得知主要的渲染操作是由CoreAnimation的Render Server模块,通过调用显卡驱动提供的OpenGL或Metal接口执行,对于每一层layer,Render Server会遵循“[画家算法](https://link.juejin.cn?target=https%3A%2F%2Flink.zhihu.com%2F%3Ftarget%3Dhttps%3A%2F%2Fen.wikipedia.org%2Fwiki%2FPainter%2527s_algorithm)”(由远及近),按次序输出到frame buffer,然后按照次序绘制到屏幕,当绘制完一层,就会将该层从帧缓存区中移除(以节省空间)如下图,从左至右依次输出,得到最后的显示结果。 ![img](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/7/9/1732f47f04a367c7~tplv-t2oaga2asx-zoom-in-crop-mark:3024:0:0:0.awebp) 但在某些场景下“画家算法”虽然可以逐层输出,但是无法在某一层渲染完成后,在回过头来擦除/修改某一部分,因为这一层之前的layer像素数据已经被永久覆盖了。这就意味着对于每一层的layer要么能够通过单次遍历就能完成渲染,要么就只能令开辟一块内存作为临时中转区来完成复杂的修改/裁剪等操作。 > 举例说明:对图3进行圆角和裁剪:imageView.clipsToBounds = YES,imageView.layer.cornerRadius=10时,这就不是简单的图层叠加了,图1,图2,图3渲染完成后,还要进行裁减,而且子视图layer因为父视图有圆角,也需要被裁剪,无法在某一层渲染完成之后,再回过头来擦除/改变其中的某个部分。所以不能按照正常的流程,因此苹果会先渲染好每一层,存入一个缓冲区中,即**离屏缓冲区**,然后经过层叠加和处理后,再存储到帧缓存去中,然后绘制到屏幕上,这种处理方式叫做**离屏渲染** ## 5.常见离屏渲染场景分析 使用Simulator检测项目中触发离屏渲染的图层,如下图: ![img](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/7/9/1732f593d2a2f32d~tplv-t2oaga2asx-zoom-in-crop-mark:3024:0:0:0.awebp) 打开 Color Off-screen Rendered,同时我们可以借助Xcode或 [Reveal](https://link.juejin.cn?target=https%3A%2F%2Frevealapp.com%2F) 清楚的看到那些图层触发了离屏渲染。 关于常见的设置圆角触发离屏渲染示例说明: ![img](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/7/9/1732f615301291d8~tplv-t2oaga2asx-zoom-in-crop-mark:3024:0:0:0.awebp) 如上图示例代码中(btn.png是一个200x300的本地图片), - btn1设置了图片,设置了圆角,打开了clipsToBounds = YES,触发了离屏渲染, - btn2设置了背景颜色,设置了圆角,打开了clipsToBounds = YES,没有触发离屏渲染, - img1设置了图片,设置了圆角,打开了masksToBounds = YES,触发了离屏渲染, - img2设置了背景颜色,设置了圆角,打开了masksToBounds = YES,没有触发离屏渲染 > 解释:btn1和img1触发了离屏渲染,原因是btn1是由它的layer和UIImageView的layer混合起来的效果(UIButton有imageView),所以设置圆角的时候会触发离屏渲染。img1设置cornerRadius和masksToBounds是不会触发离屏渲染的,如果再对img1设置背景色,则会触发离屏渲染。 根据示例可以得出只是控件设置了圆角或(圆角+裁剪)并不会触发离屏渲染,同时需要满足父layer需要裁剪时,子layer也因为父layer设置了圆角也需要被裁剪(即视图contents有内容并发生了多图层被裁剪)时才会触发离屏渲染。 苹果官方文档对于`cornerRadius`的描述: > Setting the radius to a value greater than `0.0` causes the layer to begin drawing rounded corners on its background. By default, the corner radius does not apply to the image in the layer’s `contents` property; it applies only to the background color and border of the layer. However, setting the `masksToBounds` property to `true` causes the content to be clipped to the rounded corners. 设置`cornerRadius`大于0时,只为layer的`backgroundColor`和`border`设置圆角;而不会对layer的`contents`设置圆角,除非同时设置了`layer.masksToBounds`为`true`(对应UIView的`clipsToBounds`属性)。 ## 6.圆角触发离屏渲染示意图 ![img](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/7/9/1732f78b680ed8b0~tplv-t2oaga2asx-zoom-in-crop-mark:3024:0:0:0.awebp) > 一旦我们 **为contents设置了内容** ,无论是图片、绘制内容、有图像信息的子视图等,再加上圆角+裁剪,就会触发离屏渲染。 ## 7.其他触发离屏渲染的场景: > - 采用了光栅化的 layer (layer.shouldRasterize) > - 使用了 mask 的 layer (layer.mask) > - 需要进行裁剪的 layer (layer.masksToBounds /view.clipsToBounds) > - 设置了组透明度为 YES,并且透明度不为 1 的layer (layer.allowsGroupOpacity/ layer.opacity) > - 使用了高斯模糊 > - 添加了投影的 layer (layer.shadow*) > - 绘制了文字的 layer (UILabel, CATextLayer, Core Text 等) shouldRasterize 光栅化 shouldRasterize开启后,会将layer作为位图保存下来,下次直接与其他内容进行混合。这个保存的位置就是OffscreenBuffer中。这样下次需要再次渲染的时候,就可以直接拿来使用了。 shouldRasterize使用建议: - layer不复用,没必要打开shouldRasterize - layer不是静态的,也就是说要频繁的进行修改,没必要使用shouldRasterize - 离屏渲染缓存内容有100ms时间限制,超过该时间的内容会被丢弃,进而无法复用 - 离屏渲染空间是屏幕像素的2.5倍,如果超过也无法复用 ## 8.离屏渲染的优劣 ### 8.1劣势 离屏渲染增大了系统的负担,会形象App性能。主要表现在以下几个方面: - 离屏渲染需要额外的存储空间,渲染空间大小的上限是2.5倍的屏幕像素大小,超过无法使用离屏渲染 - 容易掉帧:一旦因为离屏渲染导致最终存入帧缓存区的时候,已经超过了16.67ms,则会出现掉帧的情况,造成卡顿 ### 8.2优势 虽然离屏渲染会需要多开辟出新的临时缓存区来存储中间状态,但是对于多次出现在屏幕上的数据,可以提前渲染好,从而进行复用,这样CPU/GPU就不用做一些重复的计算。 特殊产品需求,为实现一些特殊动效果,需要多图层以及离屏缓存区保存中间状态,这种情况下就不得不使用离屏渲染。比如产品需要实现高斯模糊,无论自定义高斯模糊还是调用系统API都会触发离屏渲染。 ## 9.离屏渲染优化方案(关于实现圆角造成的离屏渲染优化) 方案一 ```gml self.view.layer.clipsToBounds = YES;self.view.layer.cornerRadius = 4.f;复制代码 ``` > - clipsToBounds:UIView中的属性,其值主要决定了在视图上的子视图,超出父视图的部分是否截取,默认为NO,即不裁剪子视图超出部分。 > - masksToBounds:CALayer中的属性,其值主要决定了视图的图层上的子图层,超出父图层的部分是否需要裁减掉。默认NO。 方案二 > 如果产品设计圆角+阴影的卡片,可以使用切图实现圆角+阴影,避免触发离屏渲染 方案三 > 贝塞尔曲线绘制圆角 ```objectivec - (UIImage *)imageWithCornerRadius:(CGFloat)radius ofSize:(CGSize)size{ /* 当前UIImage的可见绘制区域 */ CGRect rect = (CGRect){0.f,0.f,size}; /* 创建基于位图的上下文 */ UIGraphicsBeginImageContextWithOptions(size, NO, UIScreen.mainScreen.scale); /* 在当前位图上下文添加圆角绘制路径 */ CGContextAddPath(UIGraphicsGetCurrentContext(), [UIBezierPath bezierPathWithRoundedRect:rect cornerRadius:radius].CGPath); /* 当前绘制路径和原绘制路径相交得到最终裁剪绘制路径 */ CGContextClip(UIGraphicsGetCurrentContext()); /* 绘制 */ [self drawInRect:rect]; /* 取得裁剪后的image */ UIImage *image = UIGraphicsGetImageFromCurrentImageContext(); /* 关闭当前位图上下文 */ UIGraphicsEndImageContext(); return image; }复制代码 ``` 方案四 > CAShapeLayer + UIBezierPath 绘制圆角来实现UITableViewCell圆角并绘制边框颜色(这种方式比直接设置圆角方式好,但也会触发离屏渲染),代码如下: ```swift - (void)tableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath{ CAShapeLayer *maskLayer = [CAShapeLayer layer]; maskLayer.frame = CGRectMake(0, 0, cell.width, cell.height); CAShapeLayer *borderLayer = [CAShapeLayer layer]; borderLayer.frame = CGRectMake(0, 0, cell.width, cell.height); borderLayer.lineWidth = 1.f; borderLayer.strokeColor = COLOR_LINE.CGColor; borderLayer.fillColor = [UIColor clearColor].CGColor; UIBezierPath *bezierPath = [UIBezierPath bezierPathWithRoundedRect:CGRectMake(0, 0, cell.width, cell.height) cornerRadius:kRadiusCard]; maskLayer.path = bezierPath.CGPath; borderLayer.path = bezierPath.CGPath; [cell.contentView.layer insertSublayer:borderLayer atIndex:0]; [cell.layer setMask:maskLayer]; } ``` > 关于方案四的解释: > > - CAShapeLayer继承于CALayer,因而可以使用CALayer的所有属性值; > - CAShapeLayer需要和贝塞尔曲线配合使用才能够实现效果; > - CAShapeLayer(属于CoreAnimation)与贝塞尔曲线配合使用可以实现不在view的drawRect(继承于CoreGraphics走的是CPU,消耗的性能较大)方法中画出想要的图形; > - CAShapeLayer动画渲染是驱动GPU,而view的drawRect方法使用CPU渲染,相比其效率更高,消耗内存更少。 > > 总的来说使用CAShapeLayer的内存消耗少,渲染速度快。 YYKit是开发中经常用的三方库,YYImage对图片圆角的处理方法是值得推荐的,附上实现源码: ```swift - (UIImage *)imageByRoundCornerRadius:(CGFloat)radius corners:(UIRectCorner)corners borderWidth:(CGFloat)borderWidth borderColor:(UIColor *)borderColor borderLineJoin:(CGLineJoin)borderLineJoin { if (corners != UIRectCornerAllCorners) { UIRectCorner tmp = 0; if (corners & UIRectCornerTopLeft) tmp |= UIRectCornerBottomLeft; if (corners & UIRectCornerTopRight) tmp |= UIRectCornerBottomRight; if (corners & UIRectCornerBottomLeft) tmp |= UIRectCornerTopLeft; if (corners & UIRectCornerBottomRight) tmp |= UIRectCornerTopRight; corners = tmp; } UIGraphicsBeginImageContextWithOptions(self.size, NO, self.scale); CGContextRef context = UIGraphicsGetCurrentContext(); CGRect rect = CGRectMake(0, 0, self.size.width, self.size.height); CGContextScaleCTM(context, 1, -1); CGContextTranslateCTM(context, 0, -rect.size.height); CGFloat minSize = MIN(self.size.width, self.size.height); if (borderWidth < minSize / 2) { UIBezierPath *path = [UIBezierPath bezierPathWithRoundedRect:CGRectInset(rect, borderWidth, borderWidth) byRoundingCorners:corners cornerRadii:CGSizeMake(radius, borderWidth)]; [path closePath]; CGContextSaveGState(context); [path addClip]; CGContextDrawImage(context, rect, self.CGImage); CGContextRestoreGState(context); } if (borderColor && borderWidth < minSize / 2 && borderWidth > 0) { CGFloat strokeInset = (floor(borderWidth * self.scale) + 0.5) / self.scale; CGRect strokeRect = CGRectInset(rect, strokeInset, strokeInset); CGFloat strokeRadius = radius > self.scale / 2 ? radius - self.scale / 2 : 0; UIBezierPath *path = [UIBezierPath bezierPathWithRoundedRect:strokeRect byRoundingCorners:corners cornerRadii:CGSizeMake(strokeRadius, borderWidth)]; [path closePath]; path.lineWidth = borderWidth; path.lineJoinStyle = borderLineJoin; [borderColor setStroke]; [path stroke]; } UIImage *image = UIGraphicsGetImageFromCurrentImageContext(); UIGraphicsEndImageContext(); return image; } ``` 原文链接:https://juejin.cn/post/6847902222567604231 ================================================ FILE: iOS资料/iOS 系统架构及常用框架.md ================================================ # iOS 系统架构及常用框架 ## 1、ios稳定性 iOS基于UNIX系统,因此从系统的稳定性上来说它要比其他操作系统的产品好很多 ## 2、ios系统架构 iOS的系统架构分为四层,由上到下一次为:可触摸层(Cocoa Touch layer)、媒体层(Media layer)、核心服务层(Core Services layer)、核心操作系统层(Core OS layer)如图: ![图片](http://mmbiz.qpic.cn/mmbiz/8RTSPr4mlykpQPkcN0u41BL4DosIMoD3Jq6WmHlicQEAPE0nibB3Oj04iccY7KbM7pE7j70YUNlgWD7Js0zAN0Y6g/640?wx_fmt=jpeg&wxfrom=5&wx_lazy=1&wx_co=1) (1)触摸层:为应用程序开发提供了各种常用的框架并且大部分框架与界面有关,本质上来说它负责用户在iOS设备上的触摸交互操作。它包括以下这些组件: ``` Multi-Touch Events Core Motion Camera View Hierarchy Localization Alerts Web Views Image Picker Multi-Touch Controls ``` (2)媒体层:通过它我们可以在应用程序中使用各种媒体文件,进行音频与视频的录制,图形的绘制,以及制作基础的动画效果。它包括以下这些组件: ``` Core Audio OpenGL Audio Mixing Audio Recording Video Playback JPG,PNG,TIFF PDF Quartz Core Animation OpenGL ES ``` (3)核心服务层:我们可以通过它来访问iOS的一些服务。它包括以下这些组件: ``` Collections Address Book Networking File Access SQLite Core Location Net Services Threading Preferences URL Utilities ``` (4)核心操作系统层包括:内存管理、文件系统、电源管理以及一些其他的操作系统任务。它可以直接和硬件设备进行交互。核心操作系统层包括以下这些组件: ``` OS X Kernel Mach 3.0 BSD Sockets Power Mgmt File System Keychain Certificates Security Bonjour ``` ## 3、Cocoa Touch简介 (1)在最上层Cocoa Touch层中的很多技术都是基于Objective-C语言的。Objective-C语言为iOS提供了集合、文件管理、网络操作等支持。比如UIKit框架,它为应用程序提供了各种可视化组件,比如像窗口(Window)、视图(View)和按钮组件(UIButton)。Cocoa Touch层中的其他框架,对我们在应用程序中的开发来说也是非常有用的,如访问用户通信录功能框架、获取照片信息功能的框架、负责加速感应器和三维陀螺仪等硬件支持的框架。 ## 4、应用程序的框架 应用程序项目都是从Cocoa Touch层开始的,具体来说就是从UIKit Framework开始的。当在编写程序的过程中需要用到一些特殊功能的时候,我们应该从框架的最顶端技术开始寻找相应的框架,只有在上层结构无法解决时,才能使用其下层的技术。其实,顶层的框架已经涵盖了我们绝大多数需要的功能。 常用的iOS SDK框架: ![图片](http://mmbiz.qpic.cn/mmbiz/8RTSPr4mlykpQPkcN0u41BL4DosIMoD3jPzbXfQZXA3qXtTTyxh7JeNw9wANLWTlGUPv4BkjJ7gIWjkb5yFytA/640?wx_fmt=jpeg&wxfrom=5&wx_lazy=1&wx_co=1) 续表:                             ![图片](http://mmbiz.qpic.cn/mmbiz/8RTSPr4mlykpQPkcN0u41BL4DosIMoD3iaf3b40WOqSZ9UdmwwP9UyqtCiapcPQuuQgLkp8ONHx9V5IpG8Gouysg/640?wx_fmt=jpeg&wxfrom=5&wx_lazy=1&wx_co=1) 原文链接:http://www.cnblogs.com/leo_wl/p/3629606.html ================================================ FILE: iOS资料/iOS-WebRTC静态库,framework下载、编译,使用.md ================================================ # iOS-WebRTC静态库,framework下载、编译,使用 网上的对于WebRTC的下载和编译的文章其实已经很多,但是有些比较久远,里面很多方法都无法再使用,所以写一篇文章来简单的说下当前的一些使用方法和注意事项。 ## 1、编译环境: 硬件:MacBook Pro(Mid 2015) 系统:macOS High Sierra 10.13.3 网络环境:普通的稳定网络就可以 ## 2、安装git 这里不做详细介绍,相信做iOS开发的都有,没有的话,百度和Google都是很快可以安装成功。 ## 3、安装depot_tools 在你的硬盘上找一个空间专门来做我们的编译工作,保证空间在8G以上 打开终端新建一个文件夹用来存放我们之后所有的相关文件: > mkdir webrtc_build 文件夹建好后,我们就要确保自己的网络环境是可以用的,当然因为WebRTC的资源是在国外,所以这里要FQ一下,关于FQ真的是因人而异,如果你的工具足够强大下载源码毫无障碍最好了。确定已经FQ,就继续下一步。 确定网络OK后继续: depot_tools,这是一套Google用来编译Chromium或者WebRTC的构建工具,在我们后续的编译过程中也将使用它。 > a.cd webrtc_build > > b. git clone https://chromium.googlesource.com/chromium/tools/depot_tools.git > > c. 把depot_tools 设置到PATH中: > > echo "export PATH=$PWD/depot_tools:$PATH" > $HOME/.bash_profile > > d. 使PATH设置生效:source $HOME/.bash_profile > > e. echo $PATH查看设置是否生效。 ### 3.1 安装ninja **ninja** 是 **WebRTC** 的编译工具,我们需要对其进行编译,步骤如下: > 1 git clone git://github.com/martine/ninja.git > > 2 cd ninja/ > > 3 ./bootstrap.py 复制到系统目录(也可配置坏境变量) > 1 sudo cp ninja /usr/local/bin/ > > 2 sudo chmod a+rx /usr/local/bin/ninja ps:这个ninja在我编译的时候我并没有手动去调用它,但是应该再后面的编译脚本里会用到它,所以我建议大家还是按照步骤安装下。 ## 4、下载WebRTC源码 在我们的编译工作目录webrtc_build下创建一个webtrtc子目录来存放代码: > mkdir webrtc > > cd webrtc 接下来开始下载源码,这个过程会因为不同的网络环境而异,资源一共大约6G多,我的网络FQ环境下,下载速度大概是1M/S,下载部分一共将近2个小时,这一步....下载过程中去做些别的事情吧,时间会非常长: > a. 设置要编译的平台到环境变量中:export GYP_DEFINES="OS=ios" > > b. fetch --nohooks webrtc_ios > > c. gclient sync 这里要提示下,第一步设置好编译目标的平台以后,b步骤输入执行后,就进入下载步骤了,之前我以为c步骤才是下载的大头,所以被坑了一下(本来要去办事的,结果被b步骤耽误了),b步骤会下载大概6G多的文件,这个是时间最长的,当漫长的全部下载完毕以后,成功的话,大概是这个样子: ![img](https:////upload-images.jianshu.io/upload_images/2149132-2b828c266a74a9d7.png?imageMogr2/auto-orient/strip|imageView2/2/w/1200/format/webp) 下载过程大部分时间你看到的都是这个样子 ![img](https:////upload-images.jianshu.io/upload_images/2149132-3c4b2f97cefaf974.png?imageMogr2/auto-orient/strip|imageView2/2/w/1200/format/webp) 下载成功后是这样的 接下来执行c步骤,我使用的时候直接输入了我需要的版本号去同步,并没有直接执行上面写的c步骤的终端代码,可以使用 gclient sync -r 9f7e2a90da243288657e1802af85168e87daab01 来下载指定版本,9f7e2a90da243288657e1802af85168e87daab01 这个东西是commit id 我下的是18年3月1日的版本貌似,是我写文章的时候最新的,如果你有项目需求要别的版本,你可以自己改一下,具体方式为: 1、进入[WebRTC](https://webrtc.org/native-code/ios/)官网 2、点击上方About->Release Notes ![img](https:////upload-images.jianshu.io/upload_images/2149132-ceb6531c76909461.png?imageMogr2/auto-orient/strip|imageView2/2/w/1200/format/webp) 选择你要的版本,比如M65 ![img](https:////upload-images.jianshu.io/upload_images/2149132-091cb1a6f69bb5cf.png?imageMogr2/auto-orient/strip|imageView2/2/w/1018/format/webp) 进入后,再点击WebRTC M65 branch ![img](https:////upload-images.jianshu.io/upload_images/2149132-8fa6c0c12534677c.png?imageMogr2/auto-orient/strip|imageView2/2/w/1200/format/webp) 进入后选择第一条 ![img](https:////upload-images.jianshu.io/upload_images/2149132-1de4bdfd32ed9797.png?imageMogr2/auto-orient/strip|imageView2/2/w/1200/format/webp) 点击进入后,你就可以看到你需要的commit id了 ![img](https:////upload-images.jianshu.io/upload_images/2149132-567b25df3b4ec544.png?imageMogr2/auto-orient/strip|imageView2/2/w/1200/format/webp) 在gclient sync -r 9f7e2a90da243288657e1802af85168e87daab01这一步之后,终端大概是这样子的 ![img](https:////upload-images.jianshu.io/upload_images/2149132-9e0bf6d91239866b.png?imageMogr2/auto-orient/strip|imageView2/2/w/1200/format/webp) 项目的目录大概是这样样子 ![img](https:////upload-images.jianshu.io/upload_images/2149132-f63bb99f781dd00a.png?imageMogr2/auto-orient/strip|imageView2/2/w/1200/format/webp) 到这一步,你已经成功下载到了源码。 ## 5、编译WebRTC 编译的方式,我看了几个帖子,什么方法都有,这里我根据我的需求,说说我的做法。我的主要目的是因为网上找不到.a模式的webrtc的静态库,都是framework,所以我才自己编译的。我的需求很简单,就是需要.a出来。ios版本的webrtc是有自己的编译脚本文件的,我们在这里面做些修改就可以,脚本文件的路径是:webrtc_build/webrtc/src/tools_webrtc/ios/build_ios_libs.sh 正常情况,如果你不做任何更改,直接执行这个脚本,会编译出WebRTC.framework,默认路径会在:webrtc_build/webrtc/src/out_ios_libs 由于我没有编译framework,所以没有图,但是我猜测的话,应该会有真机的32位和64位的还有模拟器的,因为我编译的.a就是这几个。 所以如果你需要去掉不要的版本,还有你需要编译的.a,在这几个地方更改下就可以,记得更改是在webrtc_build/webrtc/src/tools_webrtc/ios/build_ios_libs.py 看清楚后缀 看清楚后缀 看清楚后缀是py ![img](https:////upload-images.jianshu.io/upload_images/2149132-3f31cdb92cbb6b2c.png?imageMogr2/auto-orient/strip|imageView2/2/w/1200/format/webp) arm64真机64位,个人觉得编这个就够了其实.....别的都可以删掉,剩下的是arm是真机32位,x64模拟器64位,x86模拟器32位,根据个人需要删减。建议大家先备份啊!! 之后的default位置后面改成'static_only'这个大家看到help也知道意思了,我这个图是改了后的,之前是default='framework' 脚本对于我来说更改这些就行,如果有其他的需要的话大家可以多研究研究其他的选项。 这里更改完后保存退出。 然后执行 ./build_ios_libs.sh 就开始一顿编辑....这个时间不算短,我的机器应该有个20分钟左右。 编译成功以后,你可以在这个地方找到你的.a静态库 webrtc_build/webrtc/src/out_ios_libs ![img](https:////upload-images.jianshu.io/upload_images/2149132-6b252b6ce042ac44.png?imageMogr2/auto-orient/strip|imageView2/2/w/880/format/webp) 因为我当时忘记删除不要的架构,所以把4个都编出来了,如果你只编一个,时间会节省不少,这里根据你自己的需要,拿去用就好了。还有目前我找到的头文件的位置貌似这个 webrtc_build/webrtc/src/sdk/objc/Framework/Headers/WebRTC ![img](https:////upload-images.jianshu.io/upload_images/2149132-5a0c4b93f452a4a9.png?imageMogr2/auto-orient/strip|imageView2/2/w/1200/format/webp) 之后,把.a静态库和头文件拖进你的项目里去使用就好了,当然是建议建个专门的文件夹来存放.a和头文件,记得在xcode中设置build setting里的Header search path 还有设置bitcode为no,静态库使用的时候,需要引入其他一些依赖: ![img](https:////upload-images.jianshu.io/upload_images/2149132-99d2376add136570.png?imageMogr2/auto-orient/strip|imageView2/2/w/1140/format/webp) 不引用的话,项目编译期就会报错。github上有很多开源的Demo,本篇主要是为了解决需要.a静态库的朋友,以及一些当前下载编译可行简便的方法。后续如果有新的体验会更新,有问题大家可以留言。 写这篇文章的时候,借鉴了如下的文章,有兴趣的可以也去看看: [iOS下载、编译WebRTC及demo](https://www.jianshu.com/p/64bd7f5b18b1) //这篇文章最后说了framework的集成,需要freamwork怎么集成到项目里的可以看下 [WebRTC iOS&OSX 库的编译](https://links.jianshu.com/go?to=http%3A%2F%2Fwww.enkichen.com%2F2017%2F05%2F12%2Fwebrtc-ios-build%2F) //我是从这篇文章确定了新的源码库也是可以编译出.a的,才下了决心去下6个G 原文链接:https://www.jianshu.com/p/2ecb9d846b35 ================================================ FILE: iOS资料/iOSAVDemo(10):视频解封装,从 MP4 解出 H.264H.265.md ================================================ # iOSAVDemo(10):视频解封装,从 MP4 解出 H.264/H.265 iOS/Android 客户端开发同学如果想要开始学习音视频开发,最丝滑的方式是对[音视频基础概念知识](https://mp.weixin.qq.com/mp/appmsgalbum?__biz=MjM5MTkxOTQyMQ==&action=getalbum&album_id=2140155659944787969#wechat_redirect)有一定了解后,再借助 iOS/Android 平台的音视频能力上手去实践音视频的`采集 → 编码 → 封装 → 解封装 → 解码 → 渲染`过程,并借助[音视频工具](https://mp.weixin.qq.com/mp/appmsgalbum?__biz=MjM5MTkxOTQyMQ==&action=getalbum&album_id=2216997905264082945#wechat_redirect)来分析和理解对应的音视频数据。 在[音视频工程示例](https://mp.weixin.qq.com/mp/appmsgalbum?__biz=MjM5MTkxOTQyMQ==&action=getalbum&album_id=2273301900659851268#wechat_redirect)这个栏目,我们将通过拆解`采集 → 编码 → 封装 → 解封装 → 解码 → 渲染`流程并实现 Demo 来向大家介绍如何在 iOS/Android 平台上手音视频开发。 这里是第十篇:**iOS 视频解封装 Demo**。这个 Demo 里包含以下内容: - 1)实现一个视频解封装模块; - 2)实现对 MP4 文件中视频部分的解封装逻辑并将解封装后的编码数据存储为 H.264/H.265 文件; - 3)详尽的代码注释,帮你理解代码逻辑和原理。 在本文中,我们将详解一下 Demo 的具体实现和源码。读完本文内容相信就能帮你掌握相关知识。 ## 1、视频解封装模块 视频解封装模块即 `KFMP4Demuxer`,复用了[《iOS 音频解封装 Demo》](https://mp.weixin.qq.com/s?__biz=MjM5MTkxOTQyMQ==&mid=2257484932&idx=1&sn=04fa6fb220574c0a5d417f4b527c0142&scene=21#wechat_redirect)中介绍的 demuxer,这里就不再重复介绍了,其接口如下: ``` KFMP4Demuxer.h #import #import #import "KFDemuxerConfig.h" NS_ASSUME_NONNULL_BEGIN typedef NS_ENUM(NSInteger, KFMP4DemuxerStatus) { KFMP4DemuxerStatusUnknown = 0, KFMP4DemuxerStatusRunning = 1, KFMP4DemuxerStatusFailed = 2, KFMP4DemuxerStatusCompleted = 3, KFMP4DemuxerStatusCancelled = 4, }; @interface KFMP4Demuxer : NSObject + (instancetype)new NS_UNAVAILABLE; - (instancetype)init NS_UNAVAILABLE; - (instancetype)initWithConfig:(KFDemuxerConfig *)config; @property (nonatomic, strong, readonly) KFDemuxerConfig *config; @property (nonatomic, copy) void (^errorCallBack)(NSError *error); @property (nonatomic, assign, readonly) BOOL hasAudioTrack; // 是否包含音频数据。 @property (nonatomic, assign, readonly) BOOL hasVideoTrack; // 是否包含视频数据。 @property (nonatomic, assign, readonly) CGSize videoSize; // 视频大小。 @property (nonatomic, assign, readonly) CMTime duration; // 媒体时长。 @property (nonatomic, assign, readonly) CMVideoCodecType codecType; // 编码类型。 @property (nonatomic, assign, readonly) KFMP4DemuxerStatus demuxerStatus; // 解封装器状态。 @property (nonatomic, assign, readonly) BOOL audioEOF; // 是否音频结束。 @property (nonatomic, assign, readonly) BOOL videoEOF; // 是否视频结束。 @property (nonatomic, assign, readonly) CGAffineTransform preferredTransform; // 图像的变换信息。比如:视频图像旋转。 - (void)startReading:(void (^)(BOOL success, NSError *error))completeHandler; // 开始读取数据解封装。 - (void)cancelReading; // 取消读取。 - (BOOL)hasAudioSampleBuffer; // 是否还有音频数据。 - (CMSampleBufferRef)copyNextAudioSampleBuffer CF_RETURNS_RETAINED; // 拷贝下一份音频采样。 - (BOOL)hasVideoSampleBuffer; // 是否还有视频数据。 - (CMSampleBufferRef)copyNextVideoSampleBuffer CF_RETURNS_RETAINED; // 拷贝下一份视频采样。 @end NS_ASSUME_NONNULL_END ``` ## 2、解封装 MP4 文件中的视频部分存储为 H.264/H.265 文件 我们还是在一个 ViewController 中来实现对一个 MP4 文件解封装、获取其中的视频编码数据并存储为 H.264/H.265 文件。 ``` KFVideoDemuxerViewController.m #import "KFVideoDemuxerViewController.h" #import "KFMP4Demuxer.h" @interface KFVideoPacketExtraData : NSObject @property (nonatomic, strong) NSData *sps; @property (nonatomic, strong) NSData *pps; @property (nonatomic, strong) NSData *vps; @end @implementation KFVideoPacketExtraData @end @interface KFVideoDemuxerViewController () @property (nonatomic, strong) KFDemuxerConfig *demuxerConfig; @property (nonatomic, strong) KFMP4Demuxer *demuxer; @property (nonatomic, strong) NSFileHandle *fileHandle; @end @implementation KFVideoDemuxerViewController #pragma mark - Property - (KFDemuxerConfig *)demuxerConfig { if (!_demuxerConfig) { _demuxerConfig = [[KFDemuxerConfig alloc] init]; // 只解封装视频。 _demuxerConfig.demuxerType = KFMediaVideo; // 待解封装的资源。 NSString *videoPath = [[NSBundle mainBundle] pathForResource:@"input" ofType:@"mp4"]; _demuxerConfig.asset = [AVAsset assetWithURL:[NSURL fileURLWithPath:videoPath]]; } return _demuxerConfig; } - (KFMP4Demuxer*)demuxer { if (!_demuxer) { _demuxer = [[KFMP4Demuxer alloc] initWithConfig:self.demuxerConfig]; _demuxer.errorCallBack = ^(NSError* error) { NSLog(@"KFMP4Demuxer error:%zi %@", error.code, error.localizedDescription); }; } return _demuxer; } - (NSFileHandle *)fileHandle { if (!_fileHandle) { NSString *videoPath = [[NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) lastObject] stringByAppendingPathComponent:@"output.h264"]; [[NSFileManager defaultManager] removeItemAtPath:videoPath error:nil]; [[NSFileManager defaultManager] createFileAtPath:videoPath contents:nil attributes:nil]; _fileHandle = [NSFileHandle fileHandleForWritingAtPath:videoPath]; } return _fileHandle; } #pragma mark - Lifecycle - (void)viewDidLoad { [super viewDidLoad]; self.view.backgroundColor = [UIColor whiteColor]; self.title = @"Video Demuxer"; UIBarButtonItem *startBarButton = [[UIBarButtonItem alloc] initWithTitle:@"Start" style:UIBarButtonItemStylePlain target:self action:@selector(start)]; self.navigationItem.rightBarButtonItems = @[startBarButton]; } #pragma mark - Action - (void)start { __weak typeof(self) weakSelf = self; NSLog(@"KFMP4Demuxer start"); [self.demuxer startReading:^(BOOL success, NSError * _Nonnull error) { if (success) { // Demuxer 启动成功后,就可以从它里面获取解封装后的数据了。 [weakSelf fetchAndSaveDemuxedData]; } else { NSLog(@"KFMP4Demuxer error: %zi %@", error.code, error.localizedDescription); } }]; } #pragma mark - Utility - (void)fetchAndSaveDemuxedData { // 异步地从 Demuxer 获取解封装后的 H.264/H.265 编码数据。 dispatch_async(dispatch_get_global_queue(0, 0), ^{ while (self.demuxer.hasVideoSampleBuffer) { CMSampleBufferRef videoBuffer = [self.demuxer copyNextVideoSampleBuffer]; if (videoBuffer) { [self saveSampleBuffer:videoBuffer]; CFRelease(videoBuffer); } } if (self.demuxer.demuxerStatus == KFMP4DemuxerStatusCompleted) { NSLog(@"KFMP4Demuxer complete"); } }); } - (KFVideoPacketExtraData *)getPacketExtraData:(CMSampleBufferRef)sampleBuffer { // 从 CMSampleBuffer 中获取 extra data。 if (!sampleBuffer) { return nil; } // 获取编码类型。 CMVideoCodecType codecType = CMVideoFormatDescriptionGetCodecType(CMSampleBufferGetFormatDescription(sampleBuffer)); KFVideoPacketExtraData *extraData = nil; if (codecType == kCMVideoCodecType_H264) { // 获取 H.264 的 extra data:sps、pps。 CMFormatDescriptionRef format = CMSampleBufferGetFormatDescription(sampleBuffer); size_t sparameterSetSize, sparameterSetCount; const uint8_t *sparameterSet; OSStatus statusCode = CMVideoFormatDescriptionGetH264ParameterSetAtIndex(format, 0, &sparameterSet, &sparameterSetSize, &sparameterSetCount, 0); if (statusCode == noErr) { size_t pparameterSetSize, pparameterSetCount; const uint8_t *pparameterSet; OSStatus statusCode = CMVideoFormatDescriptionGetH264ParameterSetAtIndex(format, 1, &pparameterSet, &pparameterSetSize, &pparameterSetCount, 0); if (statusCode == noErr) { extraData = [[KFVideoPacketExtraData alloc] init]; extraData.sps = [NSData dataWithBytes:sparameterSet length:sparameterSetSize]; extraData.pps = [NSData dataWithBytes:pparameterSet length:pparameterSetSize]; } } } else if (codecType == kCMVideoCodecType_HEVC) { // 获取 H.265 的 extra data:vps、sps、pps。 CMFormatDescriptionRef format = CMSampleBufferGetFormatDescription(sampleBuffer); size_t vparameterSetSize, vparameterSetCount; const uint8_t *vparameterSet; if (@available(iOS 11.0, *)) { OSStatus statusCode = CMVideoFormatDescriptionGetHEVCParameterSetAtIndex(format, 0, &vparameterSet, &vparameterSetSize, &vparameterSetCount, 0); if (statusCode == noErr) { size_t sparameterSetSize, sparameterSetCount; const uint8_t *sparameterSet; OSStatus statusCode = CMVideoFormatDescriptionGetHEVCParameterSetAtIndex(format, 1, &sparameterSet, &sparameterSetSize, &sparameterSetCount, 0); if (statusCode == noErr) { size_t pparameterSetSize, pparameterSetCount; const uint8_t *pparameterSet; OSStatus statusCode = CMVideoFormatDescriptionGetHEVCParameterSetAtIndex(format, 2, &pparameterSet, &pparameterSetSize, &pparameterSetCount, 0); if (statusCode == noErr) { extraData = [[KFVideoPacketExtraData alloc] init]; extraData.vps = [NSData dataWithBytes:vparameterSet length:vparameterSetSize]; extraData.sps = [NSData dataWithBytes:sparameterSet length:sparameterSetSize]; extraData.pps = [NSData dataWithBytes:pparameterSet length:pparameterSetSize]; } } } } else { // 其他编码格式。 } } return extraData; } - (BOOL)isKeyFrame:(CMSampleBufferRef)sampleBuffer { CFArrayRef array = CMSampleBufferGetSampleAttachmentsArray(sampleBuffer, true); if (!array) { return NO; } CFDictionaryRef dic = (CFDictionaryRef)CFArrayGetValueAtIndex(array, 0); if (!dic) { return NO; } // 检测 sampleBuffer 是否是关键帧。 BOOL keyframe = !CFDictionaryContainsKey(dic, kCMSampleAttachmentKey_NotSync); return keyframe; } - (void)saveSampleBuffer:(CMSampleBufferRef)sampleBuffer { // 将编码数据存储为文件。 // iOS 的 VideoToolbox 编码和解码只支持 AVCC/HVCC 的码流格式。但是 Android 的 MediaCodec 只支持 AnnexB 的码流格式。这里我们做一下两种格式的转换示范,将 AVCC/HVCC 格式的码流转换为 AnnexB 再存储。 // 1、AVCC/HVCC 码流格式:[extradata]|[length][NALU]|[length][NALU]|... // VPS、SPS、PPS 不用 NALU 来存储,而是存储在 extradata 中;每个 NALU 前有个 length 字段表示这个 NALU 的长度(不包含 length 字段),length 字段通常是 4 字节。 // 2、AnnexB 码流格式:[startcode][NALU]|[startcode][NALU]|... // 每个 NAL 前要添加起始码:0x00000001;VPS、SPS、PPS 也都用这样的 NALU 来存储,一般在码流最前面。 if (sampleBuffer) { NSMutableData *resultData = [NSMutableData new]; uint8_t nalPartition[] = {0x00, 0x00, 0x00, 0x01}; // 关键帧前添加 vps(H.265)、sps、pps。这里要注意顺序别乱了。 if ([self isKeyFrame:sampleBuffer]) { KFVideoPacketExtraData *extraData = [self getPacketExtraData:sampleBuffer]; if (extraData.vps) { [resultData appendBytes:nalPartition length:4]; [resultData appendData:extraData.vps]; } [resultData appendBytes:nalPartition length:4]; [resultData appendData:extraData.sps]; [resultData appendBytes:nalPartition length:4]; [resultData appendData:extraData.pps]; } // 获取编码数据。这里的数据是 AVCC/HVCC 格式的。 CMBlockBufferRef dataBuffer = CMSampleBufferGetDataBuffer(sampleBuffer); size_t length, totalLength; char *dataPointer; OSStatus statusCodeRet = CMBlockBufferGetDataPointer(dataBuffer, 0, &length, &totalLength, &dataPointer); if (statusCodeRet == noErr) { size_t bufferOffset = 0; static const int NALULengthHeaderLength = 4; // 拷贝编码数据。 while (bufferOffset < totalLength - NALULengthHeaderLength) { // 通过 length 字段获取当前这个 NALU 的长度。 uint32_t NALUnitLength = 0; memcpy(&NALUnitLength, dataPointer + bufferOffset, NALULengthHeaderLength); NALUnitLength = CFSwapInt32BigToHost(NALUnitLength); // 拷贝 AnnexB 起始码字节。 [resultData appendData:[NSData dataWithBytes:nalPartition length:4]]; // 拷贝这个 NALU 的字节。 [resultData appendData:[NSData dataWithBytes:(dataPointer + bufferOffset + NALULengthHeaderLength) length:NALUnitLength]]; // 步进。 bufferOffset += NALULengthHeaderLength + NALUnitLength; } } [self.fileHandle writeData:resultData]; } } @end ``` 上面是 `KFVideoDemuxerViewController` 的实现,其中主要包含这几个部分: - 1)设置好待解封装的资源。 - - 在 `-demuxerConfig` 中实现,我们这里是一个 MP4 文件。 - 2)启动解封装器。 - - 在 `-start` 中实现。 - 3)读取解封装后的音频编码数据并存储为 H.264/H.265 文件。 - - 在 `-fetchAndSaveDemuxedData` → `-saveSampleBuffer:` 中实现。 - 需要注意的是,我们从解封装器读取的视频 H.264/H.265 编码数据是 AVCC/HVCC 码流格式,我们在这里示范了将 AVCC/HVCC 格式的码流转换为 AnnexB 再存储的过程。这个在前面的[《iOS 视频编码 Demo》](https://mp.weixin.qq.com/s?__biz=MjM5MTkxOTQyMQ==&mid=2257485273&idx=1&sn=0d876a49c4e46f369f6a578856221f5d&scene=21#wechat_redirect)中已经介绍过了。 ## 3、用工具播放 H.264/H.265 文件 完成视频解封装后,可以将 App Document 文件夹下面的 `output.h264` 或 `output.h265` 文件拷贝到电脑上,使用 `ffplay` 播放来验证一下视频解封装的效果是否符合预期: ``` $ ffplay -i output.h264 $ ffplay -i output.h265 ``` 关于播放 H.264/H.265 文件的工具,可以参考[《FFmpeg 工具》第 2 节 ffplay 命令行工具](https://mp.weixin.qq.com/s?__biz=MjM5MTkxOTQyMQ==&mid=2257484831&idx=1&sn=6bab905a5040c46b971bab05f787788b&scene=21#wechat_redirect)和[《可视化音视频分析工具》第 2.1 节 StreamEye](https://mp.weixin.qq.com/s?__biz=MjM5MTkxOTQyMQ==&mid=2257484834&idx=1&sn=5dd9768bfc0d01ca1b036be8dd2f5fa1&scene=21#wechat_redirect)。 原文链接:https://mp.weixin.qq.com/s/4Ua9PZllWRLYF79hwsH0DQ ================================================ FILE: iOS资料/iOS下 WebRTC 视频渲染.md ================================================ # iOS下 WebRTC 视频渲染 ## 1、前言 今天为大家介绍一下 iOS 下 WebRTC是如何渲染视频的。在iOS中有两种加速渲染视频的方法。一种是使用OpenGL;另一种是使用 Metal。 OpenGL的好处是跨平台,推出时间比较长,因此比较稳定。兼容性也比较好。而Metal是iOS最近才推出的技术,理论上来说比OpenGL ES效率更高。 WebRTC中这两种渲染方式都支持。它首先会判断当前iOS系统是否支持Metal,如果支持的话,优先使用Metal。如果不支持的话,就使用 OpenGL ES。 我们今天介绍的是 OpenGL ES的方案。 ![图片](https://mmbiz.qpic.cn/mmbiz/lf9n56ou2IDAKqhqw3uKVicy6tEBjv5vdIiahpIDjEg3Lb1Ric4AKOPPT2TaLZF74n61UG1ooTg9KXlqeTw2XQpQA/640?wx_fmt=other&wxfrom=5&wx_lazy=1&wx_co=1) ## 2、创建 OpenGL 上下文 在iOS中使用OpenGL ES做视频渲染时,首先要创建EAGLContext对象。这是因为,EAGLContext管理着 OpengGL ES 渲染上下文。该上下文中,包括了状态信息,渲染命令以及OpenGL ES绘制资源(如纹理和renderbuffers)。为了执行OpenGL ES命令,你需要将创建的EAGLContext设置为当前渲染上下文。 EAGLContext并不直接管理绘制资源,它通过与上下文相关的EAGLSharegroup对象来管理。当创建EAGLContext时,你可以选择创建一个新的sharegroup或与之前创建的EAGLContext共享EAGLSharegroup。 EAGLContext与EAGLSharegroup的关系如下图所示: ![图片](https://mmbiz.qpic.cn/mmbiz/lf9n56ou2IDAKqhqw3uKVicy6tEBjv5vdZsH3otqEQQ24Be4APupysvf6h55RUdJSNFk5bZyH5HhibeBVrLDm2ng/640?wx_fmt=other&wxfrom=5&wx_lazy=1&wx_co=1) WebRTC中并没有使用共享EAGLSharegroup的情况,所以对于这种情况我们这里就不做特别讲解了。有兴趣的同学可以在网上查找相关资料。 目前,OpenGL ES有3个版本,主要使用版本2和版本3 。所以我们在创建时要对其作判断。首先看是否支持版本3,如果不支持我们就使用版本2。 **代码如下:** ![图片](https://mmbiz.qpic.cn/mmbiz_png/lf9n56ou2IDAKqhqw3uKVicy6tEBjv5vdXRLhL2v4rOsCKTbrV4bdw8pkWld3vLTiaOnrzU8fXslnHd7TXcSzalw/640?wx_fmt=png&tp=wxpic&wxfrom=5&wx_lazy=1&wx_co=1) 创建完上下文后,我们还要将它设置为当前上下文,这样它才能真正起作用。 **代码如下**: ![图片](https://mmbiz.qpic.cn/mmbiz_png/lf9n56ou2IDAKqhqw3uKVicy6tEBjv5vdsWgeCOSwuYY2sPWicb3dGcBRu8jZzLulh5ZBHKerib4MCQUVDXR8rY9Q/640?wx_fmt=png&tp=wxpic&wxfrom=5&wx_lazy=1&wx_co=1) 需要注意的是,由于应用切换到后台后,上下文就发生了切换。所以当它切换到前台时,也要做上面那个判断。 OpenGL ES上下文创建好后,下面我们看一下如何创建View。 ## 3、创建 OpenGL View 在iOS中,有两种展示层,一种是 GLKView,另一种是 CAEAGLLayer。WebRTC中使用GLKView进行展示。CAEAGLLayer暂不做介绍。 GLKit框架提供了View和View Controller类以减少建立和维护绘制 OpenGL ES 内容的代码。GLKView类用于管理展示部分;GLKViewController类用于管理绘制的内容。它们都是继承自UIKit。GLKView的好处是,开发人员可以将自己的精力聚焦在OpenGL ES渲染的工作上。 GLKView展示的基本流程如下: ![图片](https://mmbiz.qpic.cn/mmbiz/lf9n56ou2IDAKqhqw3uKVicy6tEBjv5vdmY0kLVMhXUGmOT3LMiakUQ159fufrfu6eKHDWy0JkVtdEibrgpm1SUCQ/640?wx_fmt=other&wxfrom=5&wx_lazy=1&wx_co=1) 如上图所示,绘制 OpenGL ES 内容有三步: - 准备 OpenGL ES 环境; - 发送绘制命令; - 展示渲染内容。 GLKView类自己实现了第一步和第三步。第二步由开发人员来完成,也就是要实现drawRect函数。GLKView之所以能为OpenGL ES提供简单的绘制接口,是因为它管理了OpenGL ES渲染过程的标准部分: - 在调用绘制方法之前: - - 使用 EAGLContext 作为当前上下文。 - 根据size, 缩放因子和绘制属性,创建 FBO 和 renderbuffer。 - 绑定 FBO,作为绘制命令的当前目的地。 - 匹配 OpenGL ES viewport与 framebuffer size 。 - 在绘制方法返回之后: - - 解决多采样 buffers(如果开启了多采样)。 - 当内容不在需要时,丢掉 renderbuffers。 - 展示renderbuffer内容。 使用GLKView有两种方法,一种是实现一个类,直接继承自GLKView,并实现drawRect方法。另一种是实现GLKView的代理,也就是GLKViewDelegate,并实现drawInRect方法。 在WebRTC中,使用的是第二种方法。RTCEAGLVideoView 是GLKView的包裹类,并且继承自GLKViewDelegate。 首先,创建GLKView. ![图片](https://mmbiz.qpic.cn/mmbiz_png/lf9n56ou2IDAKqhqw3uKVicy6tEBjv5vd88ym9FFJP53IzINibzxicYLdIUvGmeaswLuRGSW5xe6JTgibarl7ChGsA/640?wx_fmt=png&tp=wxpic&wxfrom=5&wx_lazy=1&wx_co=1) 创建好GLKView后,需要将glkView.delegate设置为RTCEAGLVideoView,这样就可以将绘制工作交由RTCEAGLVideoView来完成了。另外,glkView.enableSetNeedsDisplay 设置为 NO,由我们自己来控制何时进行绘制。 然后,实现drawInRect方法。 ![图片](https://mmbiz.qpic.cn/mmbiz_png/lf9n56ou2IDAKqhqw3uKVicy6tEBjv5vdw42PymUibWQxGj9GdNDOibtSv9uowaR4qSFtQtwEuibF2ecKaYRkEyfVQ/640?wx_fmt=png&tp=wxpic&wxfrom=5&wx_lazy=1&wx_co=1) 上面的代码就是通过Shader来绘制NV12的YUV数据到View中。这段代码的基本意思是将一个解码后的视频帧分解成Y数据纹理,UV数据纹理。然后调用Shader程序将纹理转成rgb数据,最终渲染到View中。 ## 4、Shader程序 OpenGL ES 有两种 Shader。一种是顶点(Vetex)Shader; 另一种是片元(fragment )Shader。 - Vetex Shader: 用于绘制顶点。 - Fragment Shader:用于绘制像素点。 ### 4.1 Vetex Shader Vetex Shader用于绘制图形的顶点。我们都知道,无论是2D还是3D图形,它们都是由顶点构成的。 在OpenGL ES中,有三种基本图元,分别是点,线,三角形。由它们再构成更复杂的图形。而点、线、三角形又都是由点组成的。 视频是在一个矩形里显示,所以我们要通过基本图元构建一个矩形。理论上,矩形可以通过点、线绘制出来,但这样做的话,OpenGL ES就要绘制四次。而通过三角形绘制只需要两次,所以使用三角形执行速度更快。 下面的代码就是 WebRTC 中的Vetex Shader程序。该程序的作用是每个顶点执行一次,将用户输入的顶点输出到 gl_Position中,并将顶点的纹理作标点转作为 Fragment Shader 的输入。 1. OpenGL坐标原点是屏幕的中心。纹理坐标的原点是左下角。 2. gl_Position是Shader的内部变量,存放一个项点的坐标。 ![图片](https://mmbiz.qpic.cn/mmbiz_png/lf9n56ou2IDAKqhqw3uKVicy6tEBjv5vdRERDUX1HiaSxuicic3PqD9iaEF8OjUBZS3pGD39UPagsGhyE6YqRkmmw0A/640?wx_fmt=png&tp=wxpic&wxfrom=5&wx_lazy=1&wx_co=1) OpenGL ES Shader语法请见我的另一篇文章着色器 ### 4.2 fragment Shader fragment Shader程序是对片元着色,每个片元执行一次。片元与像素差不多。可以简单的把片元理解为像素。 下面的代码是WebRTC中的 fragment Shader程序。WebRTC收到远端传来的H264视频帧后,解码成YUV数据。之后,对YUV数据进行分解,如移动端使用的YUV数据格式为NV12, 所以就被分成了两部分,一部分是Y数据纹理,另一部分是UV数据纹理。 YUV有多种格式,可以参见我的另一篇文章YUV。 在代码中,使用FRAGMENT_SHADER_TEXTURE命令,也就是OpenGL ES中的 texture2D 函数,分别从 Y 数据纹理中取出 y值,从 UV 数据纹理中取出 uv值,然后通过公式计算出每个像素(实际是片元)的 rgb值。 ![图片](https://mmbiz.qpic.cn/mmbiz_png/lf9n56ou2IDAKqhqw3uKVicy6tEBjv5vdsjYWbrNc4Jf0cxOLbJjB2ic0SIWG1DsU5dMuKXAiarvO32ZDmpVHbL1A/640?wx_fmt=png&tp=wxpic&wxfrom=5&wx_lazy=1&wx_co=1) 有了顶点数据和片元的RGB值后,就可以调用OpenGL ES的 draw 方法进行视频的绘制了。 ## 5、Shader的编译、链接与使用 上面介绍了 WebRTC下 Vetex Shader 和 Fragment Shader程序。要想让程序运行起来,还要额外做一些工作。 OpenGL ES的 shader程序与C程序差不多。想像一下C程序,要想让一个C程序运行起来,要有以下几个步骤: - 1.写好程序代码 - 2.编译 - 3.链接 - 4.执行 Shader程序的运行也是如此。我们看看 WebRTC是如何做的。 ![图片](https://mmbiz.qpic.cn/mmbiz_png/lf9n56ou2IDAKqhqw3uKVicy6tEBjv5vdHHzaHWLTfGn1dBa89CtvRviamLiaIPfM1ssX7x1oqjtk1M2YBPicP7Xog/640?wx_fmt=png&tp=wxpic&wxfrom=5&wx_lazy=1&wx_co=1) 它首先创建一个 Shader, 然后将上面的 Shader 程序与 Shader 绑定。之后编译 Shader。 ![图片](https://mmbiz.qpic.cn/mmbiz_png/lf9n56ou2IDAKqhqw3uKVicy6tEBjv5vdJib52B34h6AMNVsHvExCPJXDYpIzTKm7lu8JEHTVb2jeKce3VC7GDBA/640?wx_fmt=png&tp=wxpic&wxfrom=5&wx_lazy=1&wx_co=1) 编译成功后,创建 program 对象。将之前创建的 Shader 与program绑定到一起。之后做链接工作。一切准备就绪后,就可以使用Shader程序绘制视频了。 ![图片](https://mmbiz.qpic.cn/mmbiz_png/lf9n56ou2IDAKqhqw3uKVicy6tEBjv5vdyC0pUxEHjqReQDtYE33pCIxIH4Sx7KGhTib2jbzDjnBic92AvSrkhTgg/640?wx_fmt=png&tp=wxpic&wxfrom=5&wx_lazy=1&wx_co=1) ## 6、WebRTC中视频渲染相关文件 - RTCEAGLVideoView.m/h:创建 EAGLContext及OpenGL ES View,并将视频数据显示出来。 - RTCShader.mm/h:OpenGL ES Shader 程序的创建,编译与链接相关的代码。 - RTCDefaultShader.mm/h: Shader 程序,绘制相关的代码。 - RTCNV12TextureCache.mm/h: 用于生成 YUV NV12 相关纹理的代码。 - RTCI420TexutreCache.mm/h: 用于生成 I420 相关纹理的代码。 ## 7、小结 本文对 WebRTC 中 OpenGL ES 渲染做了介绍。通过本篇文章大家可以了解到WebRTC是如何将视频渲染出来的。包括: - 上下文的创建与初始化。 - GLKView的创建。 - 绘制方法的实现。 - Shader代码的分析。 - Shader的编译与执行。 对于 OpenGL ES 是一个相当大的主题,如果没有相应的基础,看本篇文章还是比较困难的。大家可以参考我前面写的几篇关于 OpenGL 的文章。 原文链接:https://mp.weixin.qq.com/s/IYJSYw4-o5IYyoKdgYs4wg ================================================ FILE: iOS资料/iOS下的渲染框架.md ================================================ # iOS下的渲染框架 ## 1.图形渲染框架 iOS APP图形渲染框架,APP在显示可视化的图形时,使用到了Core Animation、Core Graphics、Core Image等框架,这些框架在渲染图形时,都需要通过OpenGL ES / Metal来驱动GPU进行渲染与绘制。 ![img](https://pic2.zhimg.com/80/v2-b3b623f9e42eec9c52331e524f6b320d_720w.webp) - **UIKit** UIKit是iOS开发最常用的框架,可以通过设置UIKit组件的布局以及相关属性来绘制界面。 事实上,UIKit自身并不具备在屏幕成像的能力,其主要负责对用户操作事件的响应(UIView继承自UIResponder),事件响应的传递大体是经过逐层的**视图树**遍历实现的。 - **Core Animation** Core Animation源自于Layer Kit,动画只是Core Animation的冰山一角。 Core Animation是一个复合引擎,其职责是**尽可能快地组合屏幕上不同的可视内容,这些可视内容可被分解成独立的图层(即CALayer),这些图层会被存储在一个叫做图层树的体系之中**。从本质上而言,CALayer是用户所能在屏幕上看见的一切的基础。 - **Core Graphics** Core Graphics是基于Quartz 的高级绘图引擎,主要用于运行时绘制图像。开发者可以使用此框架来处理基于路径的绘图,转换,颜色管理,离屏渲染,图案,渐变和阴影,图像数据管理,图像创建和图像遮罩以及PDF文档创建,显示和分析。 - **Core Image** Core Image与Core Graphics恰恰相反,Core Graphics用于在运行时创建图像,而Core Image用于处理运行前创建的图像。Core Image框架拥有一系列现成的图像过滤器,能对一寸照的图像进行高效的处理。大部分情况下,Core Image会在GPU中完成工作,如果GPU忙,会使用CPU进行处理。 ## 2.UIView与CALayer的关系 CALayer事实上是用户所能在屏幕上看见的一切的基础。为什么UIKit中的视图能够呈现可视化内容,就是因为UIKit中的每一个UI视图控件其实内部都有一个关联的CALayer,即backing layer。 由于这种一一对应的关系,视图层级有用**视图树**的树形结构,对应CALayer层级也拥有**图层树**的树形结构。 其中,视图的职责是创建并管理图层,以确保当子视图在层级关系中添加或被移除时,其关联的图层在图层树中也有相同的操作,即保证视图树和图层树在结构上的一致性。 为什么iOS要基于UIView和CALayer提供两个平行的层级关系呢? 其原因在于要做**职责分离**,这样也能避免很多重复代码。在iOS和Mac OSX两个平台上,事件和用户交互有很多地方的不同,基于多点触控的用户界面和基于鼠标键盘的交互有着本质的区别,这就是为什么iOS有UIKit和UIView,对应Mac OSX有AppKit和NSView的原因。它们在功能上很相似,但是在实现上有着显著的区别。实际上,这里并不是两个层级关系,而是四个。每一个都扮演着不同的角色。除了**视图树**和**图层树**,还有**呈现树**和**渲染树**。 那么为什么CALayer可以呈现可视化内容呢?因为CALayer基本等同于一个**纹理**。纹理是GPU进行图像渲染的重要依据。 在[图形渲染原理](https://link.zhihu.com/?target=https%3A//links.jianshu.com/go%3Fto%3Dhttp%3A%2F%2Fchuquan.me%2F2018%2F08%2F26%2Fgraphics-rending-principle-gpu%2F)中提到纹理本质上就是一张图片,因此CALayer也包含一个contents属性指向一块缓存区,称为backing store,可以存放位图(Bitmap)。iOS中将该缓存区保存的图片称为**寄宿图**。 ![img](https://pic1.zhimg.com/80/v2-ed1a8236002c33eeace98438d7bec358_720w.webp) 图形渲染流水线支持从顶点开始进行绘制(在流水线中,顶点会被处理生成纹理),也支持直接使用纹理(图片)进行渲染。相应地,在实际开发中,绘制界面也有两种方式: 一种是**手动绘制**;另一种是**使用图片**。 对此,iOS中也有两种相应的实现方式: - 使用图片:contents image - 手动绘制:custom drawing **Contents Image** Contents Image是指通过CALayer的contents属性来配置图片。然而,contents属性的类型为id,在这种情况下,可以给contents属性赋予任何值,app仍可以编译通过。但是在实践中,如果contents的值不是CGImage,得到的图层将是空白的。 既然如此,为什么要将contents的属性类型定义为id而非CGImage。因为在Mac OS系统中,该属性对CGImage和NSImage类型的值都起作用,而在iOS系统中,该属性只对CGImage起作用。 本质上,contents属性指向的一块缓存区域,称为backing store,可以存放bitmap数据。 **Custom Drawing** Custom Drawing是指使用Core Graphics直接绘制寄宿图。实际开发中,一般通过继承UIView并实现-drawRect:方法来自定义绘制。 虽然-drawRect:是一个UIView方法,但事实上都是底层的CALayer完成了重绘工作并保存了产生的图片。 下图所示为drawRect:绘制定义寄宿图的基本原理 ![img](https://pic2.zhimg.com/80/v2-59ba203ff08d01499f194f0c258da479_720w.webp) - UIView有一个关联图层,即CALayer。 - CALayer有一个可选的delegate属性,实现了CALayerDelegate协议。UIView作为CALayer的代理实现了CALayerDelegate协议。 - 当需要重绘时,即调用-drawRect:,CALayer请求其代理给予一个寄宿图来显示。 - CALayer首先会尝试调用-displayLayer:方法,此时代理可以直接设置contents属性。 ```objective-c - (void)displayLayer:(CALayer *)layer; ``` - 如果代理没有实现-displayLayer:方法,CALayer则会尝试调用`-`drawLayer:inContext:方法。在调用该方法前,CALayer会创建一个空的寄宿图(尺寸由bounds和contentScale决定)和一个Core Graphics的绘制上下文,为绘制寄宿图做准备,作为ctx参数传入。 ```objective-c - (void)drawLayer:(CALayer *)layer inContext:(CGContextRef)ctx; ``` - 最后,有Core Graphics绘制生成的寄宿图会存入backing store。 **三个框架间的依赖关系** Core Animation、Core Graphics、Core Image这个三个框架间也存在着依赖关系。 上面提到CALayer是用户所能在屏幕上看到一切的基础。所以Core Graphics、Core Image是需要依赖于CALayer来显示界面的。由于CALayer又是Core Animation框架提供的,所以说Core Graphics、Core Image是依赖于``Core Animation ```的。 上文还提到每一个 UIView 内部都关联一个CALayer图层,即backing layer,每一个CALayer都包含一个content属性指向一块缓存区,即backing store, 里面存放位图(Bitmap)。iOS中将该缓存区保存的图片称为寄宿图。 这个寄宿图有两个设置方式: 直接向content设置CGImage图片,这需要依赖Core Image来提供图片。 通过实现UIView的drawRect方法自定义绘图,这需要借助Core Graphics来绘制图形,再由CALayer生成图片。 ![img](https://pic2.zhimg.com/80/v2-765f27162bb7790d79d84a8a8894917d_720w.webp) ## 3.Core Animation 流水线 CALayer是如何调用 GPU 并显示可视化内容的呢?下面我们就需要介绍一下 Core Animation 流水线的工作原理。 ![img](https://pic4.zhimg.com/80/v2-90eb26de8df27fc3b3ff74f918af5293_720w.webp) 事实上,app 本身并不负责渲染,渲染则是由一个独立的进程负责,即Render Server进程。 App 通过 IPC 将渲染任务及相关数据提交给Render Server。Render Server处理完数据后,再传递至 GPU。最后由 GPU 调用 iOS 的图像设备进行显示。 Core Animation 流水线的详细过程如下: 首先,由 app 处理事件(Handle Events),如:用户的点击操作,在此过程中 app 可能需要更新**视图树**,相应地,**图层树**也会被更新。 其次,app 通过 CPU 完成对显示内容的计算,如:视图的创建、布局计算、图片解码、文本绘制等。在完成对显示内容的计算之后,app 对图层进行打包,并在下一次 RunLoop 时将其发送至Render Server,即完成了一次Commit Transaction操作。 Render Server主要执行 Open GL、Core Graphics 相关程序,并调用 GPU GPU 则在物理层上完成了对图像的渲染。 最终,GPU 通过 Frame Buffer、视频控制器等相关部件,将图像显示在屏幕上。 对上述步骤进行串联,它们执行所消耗的时间远远超过 16.67 ms,因此为了满足对屏幕的 60 FPS 刷新率的支持,需要将这些步骤进行分解,通过流水线的方式进行并行执行,如下图所示。 ![img](https://pic1.zhimg.com/80/v2-3a9da6baa3cfb091ab559abad14f6da4_720w.webp) **Commit Transaction** 在 Core Animation 流水线中,app 调用Render Server前的最后一步 Commit Transaction 其实可以细分为 4 个步骤: Layout:主要进行视图构建,包括:LayoutSubviews方法的重载,addSubview:方法填充子视图等。 Display:视图绘制,这里仅仅是绘制寄宿图,该过程使用CPU和内存 Prepare:阶段属于附加步骤,一般处理图像的解码和转换等操作 Commit:主要将图层进行打包,并将它们发送至 Render Server。该过程会递归执行,因为图层和视图都是以树形结构存在。 原文https://zhuanlan.zhihu.com/p/157556221 ================================================ FILE: iOS资料/iOS使用AVPlayer,播放本地,在线音频 ================================================ **AVPlayer**属于AVFoundation框架,不仅能够播放音频,还可以播放视频,支持本地和网链,更加接近底层,定制也更加灵活。 为什么要写这篇文章呢?其因有二: - 1、github上有很多播放音频的优秀三方的框架,很方便,也很容易集成,但问题也比较多,底层都是C或者C++,要修改一个小BUG,难度系数比较高,例如: [StreamingKit](https://link.jianshu.com/?t=https://github.com/tumtumtum/StreamingKit) [FreeStreamer](https://link.jianshu.com/?t=https://github.com/muhku/FreeStreamer) [AudioStreamer](https://link.jianshu.com/?t=https://github.com/mattgallagher/AudioStreamer) [AFSoundManager](https://link.jianshu.com/?t=https://github.com/AlvaroFranco/AFSoundManager) [DOUAudioStreamer](https://link.jianshu.com/?t=https://github.com/douban/DOUAudioStreamer) 以上,除了FreeStreamer,其他的几个框架都使用过,其中StreamingKit和DOUAudioStreamer在线上版本使用过,性能都比较不错。 **DOUAudioStreamer** 唯一的问题就是在使用缓存继续播放,有问题,并且不支持seekToTime,这句话的意思呢就是,整个音频缓冲完毕才能继续播放,如果快进的时候整个音频没有缓冲完成(网络较差的时候,音频较大),这个就比较坑了。 **StreamingKit** 这个有个问题就是缓冲的进度无法回调,无法获取,源码都是互斥锁,自旋锁,看着各种晕菜,有个问题就是,可能是我使用的姿势不对,缓存文件比较大,但是在沙盒又找不见这个文件的存在,无法删除,API也没有提供对应的接口,在手机的空间存储中查看,占用空间很大。 2、这些框架很久都没人维护,面对现在的产品需求,不能满足。 *基于以上原因,结合自己项目中出现的问题,决定用强悍的AV框架中AVPlayer,自己写一个, 功能如下:* 不要求实现流播 能支持缓存播放 有缓存进度回调 可以清除缓存文件即可 ## 1.AVPlayer基础用法介绍 以前做视频开发,在播放视频时,只是简单的播放一个视频,而不需要考虑播放器的界面。 1、iOS9.0 之前使用 MPMoviePlayerController, 或者自带一个 view 的 MPMoviePlayerViewController。 2、iOS9.0 之后,可以使用新的API AVPictureInPictureController, AVPlayerViewController。 3、甚至使用WKWebView。 以上播放器都是系统提供,优点:封装性很强,使用简单方便。缺点:自由定制度太低。 所以在我们需要自己定制播放器的时候,就需要时用**AVPlayer**。 AVPlayer继承NSObject,所以单独使用AVPlayer时无法显示视频的,必须将视频图层添加到AVPlayerLayer中方能显示视频。使用AVPlayer首先了解一下几个常用的类: 1、**AVAsset**:AVAsset类专门用于获取多媒体的相关信息,包括获取多媒体的画面、声音等信息,属于一个抽象类,不能直接使用。 2、**AVURLAsset**:AVAsset的子类,可以根据一个URL路径 一个包含媒体信息的AVURLAsset对象。 3、**AVPlayerItem**:一个媒体资源管理对象,管理者视频的一些基本信息和状态,一个AVPlayerItem对应着一个视频资源。 4、**AVPlayer**:播放器。 5、**CMTime**:是一个结构体,里面存储着当前的播放进度,总的播放时长。 一般项目中会初始化一个播放管理的工具类(单例): ## 2.实例化一个AVPlayer: ``` - (AVPlayer *)player { if (_player == nil) { _player = [[AVPlayer alloc] init]; _player.volume = 1.0; // 默认最大音量 } return _player; } ``` 2、播放一个音频(本地和网络都可以) ``` //播放音频的方法 - (void)p_musicPlayerWithURL:(NSURL *)playerItemURL{ // 移除监听 [self p_currentItemRemoveObserver]; // 创建要播放的资源 AVPlayerItem *playerItem = [[AVPlayerItem alloc]initWithURL:playerItemURL]; // 播放当前资源 [self.player replaceCurrentItemWithPlayerItem:playerItem]; // 添加观察者 [self p_currentItemAddObserver]; } ``` ## 3.注册,使用KVO监听**self.player.currentItem** > 1、监听status,AVPlayerItemStatus有三种状态: ``` typedef NS_ENUM(NSInteger, AVPlayerItemStatus) { AVPlayerItemStatusUnknown, AVPlayerItemStatusReadyToPlay, AVPlayerItemStatusFailed }; ``` 2、监听loadedTimeRanges,这个就是缓冲进度,可以进行缓冲进度条的设置 3、AVPlayerItemDidPlayToEndTimeNotification,注册这个通知,当播放器播放完成的时候进行回调。 4、addPeriodicTimeObserverForInterval,监听当前播放进度。 监听和移除代码如下: ``` - (void)p_currentItemRemoveObserver { [self.player.currentItem removeObserver:self forKeyPath:@"status"]; [self.player.currentItem removeObserver:self forKeyPath:@"loadedTimeRanges"]; [[NSNotificationCenter defaultCenter] removeObserver:self name:AVPlayerItemDidPlayToEndTimeNotification object:nil]; [self.player removeTimeObserver:self.timeObserver]; } - (void)p_currentItemAddObserver { //监控状态属性,注意AVPlayer也有一个status属性,通过监控它的status也可以获得播放状态 [self.player.currentItem addObserver:self forKeyPath:@"status" options:(NSKeyValueObservingOptionOld|NSKeyValueObservingOptionNew) context:nil]; //监控缓冲加载情况属性 [self.player.currentItem addObserver:self forKeyPath:@"loadedTimeRanges" options:NSKeyValueObservingOptionOld|NSKeyValueObservingOptionNew context:nil]; //监控播放完成通知 [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(playbackFinished:) name:AVPlayerItemDidPlayToEndTimeNotification object:self.player.currentItem]; //监控时间进度 @weakify(self); self.timeObserver = [self.player addPeriodicTimeObserverForInterval:CMTimeMake(1, 1) queue:dispatch_get_main_queue() usingBlock:^(CMTime time) { @strongify(self); // 在这里将监听到的播放进度代理出去,对进度条进行设置 if (self.delegate && [self.delegate respondsToSelector:@selector(updateProgressWithPlayer:)]) { [self.delegate updateProgressWithPlayer:self.player]; } }]; } ``` 4、KVO处理 ``` #pragma mark - KVO - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context { AVPlayerItem *playerItem = object; if ([keyPath isEqualToString:@"status"]) { AVPlayerItemStatus status = [change[@"new"] integerValue]; switch (status) { case AVPlayerItemStatusReadyToPlay: { // 开始播放 [self play]; // 代理回调,开始初始化状态 if (self.delegate && [self.delegate respondsToSelector:@selector(startPlayWithplayer:)]) { [self.delegate startPlayWithplayer:self.player]; } } break; case AVPlayerItemStatusFailed: { NSLog(@"加载失败"); TOAST_MSG(@"播放错误"); } break; case AVPlayerItemStatusUnknown: { NSLog(@"未知资源"); TOAST_MSG(@"播放错误"); } break; default: break; } } else if([keyPath isEqualToString:@"loadedTimeRanges"]){ NSArray *array=playerItem.loadedTimeRanges; //本次缓冲时间范围 CMTimeRange timeRange = [array.firstObject CMTimeRangeValue]; float startSeconds = CMTimeGetSeconds(timeRange.start); float durationSeconds = CMTimeGetSeconds(timeRange.duration); //缓冲总长度 NSTimeInterval totalBuffer = startSeconds + durationSeconds; NSLog(@"共缓冲:%.2f",totalBuffer); if (self.delegate && [self.delegate respondsToSelector:@selector(updateBufferProgress:)]) { [self.delegate updateBufferProgress:totalBuffer]; } } else if ([keyPath isEqualToString:@"rate"]) { // rate=1:播放,rate!=1:非播放 float rate = self.player.rate; if (self.delegate && [self.delegate respondsToSelector:@selector(player:changeRate:)]) { [self.delegate player:self.player changeRate:rate]; } } else if ([keyPath isEqualToString:@"currentItem"]) { NSLog(@"新的currentItem"); if (self.delegate && [self.delegate respondsToSelector:@selector(changeNewPlayItem:)]) { [self.delegate changeNewPlayItem:self.player]; } } } - (void)playbackFinished:(NSNotification *)notifi { NSLog(@"播放完成"); } ``` 以上只是demo片段,[核心代码](https://link.jianshu.com/?t=https://git.oschina.net/jueying/AVPlayerDemo.git)。 后面会介绍如何处理缓冲进度条、如何使用缓存进行播放。 未完待续... 作者:jueyingxx 链接:https://www.jianshu.com/p/bb2060fe6d5e 来源:简书 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 ================================================ FILE: iOS资料/iOS动画系列之三:Core Animation.md ================================================ # iOS动画系列之三:Core Animation ## 1. 介绍 - Core Animation是一个非常强大的动画处理 API,使用它能做出非常绚丽的动画效果,而且往往是事半功倍,也就是说,使用少量的代码就可以实现非常强大的功能。 - 苹果封装的 UIView 的 block 动画就是对核心动画的封装,使用起来更加简单。 - 绝大多数情况下,使用 UIView 的 block 动画能够满足开发中的日常需求。 - 一些很酷炫的动画,还是需要通过核心动画来完成的。 ## 2. 支持的平台 - Core Animation 同时支持 MAC OS 和 iOS 平台 - Core Animation 是直接作用在 CALayer 的,并非 UIView。所以这个系列,咱们是从CALayer开始的。 - Core Animation 的动画执行过程都是在后台操作的,不会阻塞主线程。 ## 3. `Core Animation` 的继承结构图 - 是所有动画对象的父类,负责控制动画的持续时间和速度、是个抽象类,不能直接使用,应该使用具体子类。需要注意的是`CAAnimation` 和 `CAPropertyAnimation` 都是抽象类。 - view是负责响应事件的,layer是负责显示的。 下面盗用了一张网络上的图片用来解释继承结构。 ![继承结构图](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2017/8/13/700e6bd0a70d6bba9d2233ad1e424930~tplv-t2oaga2asx-zoom-in-crop-mark:4536:0:0:0.image)继承结构图 黄色的区块是常用的属性、方法或者需要遵守的协议,灰色的是名称。 其中CAAnimationGroup、CABasicAnimation、CAKeyFramkeAnimation咱们会在下次更新中写一些小例子。 ## 4. 常见属性和使用步骤 ### 4.1 使用步骤 通常分成三部完成: 1,创建核心动画对象; 2,设置动画属性; 3,添加到要作用的layer上。 就想把大象放进冰箱需要三步一样。哈哈~ ### 4.2 常用属性 就是咱们上面图片中的小黄图显示的。 - `duration`:持续时间,默认值是0.25秒 - `repeatCount`:重复次数,无线循环可以设置HUGE_VALF或者CGFLOAT_MAX - `repeatDuration`:重复时间 - `removeOnCompletion`: 默认为YES,代表动画执行完毕后就从图层上移除,图形会恢复到执行动画之前的状态。*如果想要图层保持显示动画执行后的状态,那就设置为NO,同时设置fillMode为kCAFillModeForwards* - `fillMode`:决定当前对象在非active时间段的行为 - `beginTime`:可以用来设置动画延时执行,若想延迟2s,就设置为`CACurrentMediaTIme() + 2` - `CACurrentMediaTIme()`:图层的当前时间 - `timingFunction`:速度控制函数,控制动画运行节奏 - `delegate`:动画代理 ### 4.3 animationWithKeyPath中,常用的keyPath | 属性名称 | 类型 | 作用 | | ----------------------- | ---------------- | -------------------------------- | | transform.rotation.x | CGFloat或float | 绕X轴坐标旋转 角度 | | transform.rotation.y | CGFloat或float | 绕Y轴坐标旋转 角度 | | transform.rotation.z | CGFloat或float | 绕Z轴坐标旋转 角度 | | transform.rotation | CGFloat或float | 作用与transform.tation.z一样 | | ---- | ---- | ---- | | transform.scale | CGFloat | 整个layer的比例 | | transform.scale.x | CGFloat | x轴坐标比例变化 | | transform.scale.y | CGFloat | y轴坐标比例变化 | | transform.scale.z | CGFloat | z轴坐标比例变化 | | ---- | ---- | ---- | | transform.translation | CGMutablePathRef | 整个layer的xyz轴都进行移动 | | transform.translation.x | CGMutablePathRef | 横向移动 | | transform.translation.y | CGMutablePathRef | 纵向移动 | | transform.translation.z | CGMutablePathRef | 纵深移动 | | ---- | ---- | ---- | | opacity | CGFloat | 透明度,闪烁等动画用 。范围是0~1 | | backgroundColor | CGColor | 背景颜色 | | cornerRadius | CGFloat | 圆角 | ### 4.4 动画填充模式 - kCAFillModeForwards:当动画结束后,layer会一直保持着动画最后的状态 - kCAFillModeBackwards:在动画开始前,只需要将动画加入了一个layer,layer便立即进入动画的初始状态并等待动画开始 - kCAFillModeBoth:这个其实就是上面两个合成,动画加入后,开始之前,layer便处于动画初始状态,动画结束后layer保持动画最后的状态 - kCAFillModeRemoved:这个是默认值,也就是说当动画开始前和动画结束后,动画对layer都没有影响,动画结束后,layer会恢复到之前的状态 ```ini keyArc.calculationMode = kCAAnimationPaced ``` ### 4.5 速度控制函数 - kCAMediaTimingFunctionLinear(线性):匀速,给你一个相对静态的感觉 - kCAMediaTimingFunctionEaseIn(渐进):动画缓慢进入,然后加速离开 - kCAMediaTimingFunctionEaseOut(渐出):动画全速进入,然后减速的到达目的地 - kCAMediaTimingFunctionEaseInEaseOut(渐进渐出):动画缓慢的进入,中间加速,然后减速的到达目的地。这个是默认的动画行为。 ```ini keyArc.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut]; ``` 原文链接:https://juejin.cn/post/6844903490980954119 ================================================ FILE: iOS资料/iOS图像渲染及卡顿问题优化.md ================================================ # iOS图像渲染及卡顿问题优化 ## 1.基本知识 下面来看下GPU和CPU的基本概念: - **CPU(Central Processing Unit)**:系统的运算和控制单元,是信息处理、程序执行的最终执行单元。CPU内部结构是具有一定程度的并行计算能力。CPU的主要功效是:处理指令、执行操作、控制时间、处理数据。 - **GPU(Graphics Processing Unit)**:进行绘图工作的处理器,GPU可以生成2D/3D图形图像和视频,同时GPU具有超强的并行计算能力。GPU使显卡减少了对CPU的依赖,并进行部分原本CPU的工作,尤其是在3D图形处理时GPU所采用的核心技术有硬件T&L(几何转换和光照处理)、立方环境材质贴图和顶点混合、纹理压缩和凹凸映射贴图、双重纹理四像素256位渲染引擎等,其中GPU的生产商主要有NVIDIA和ATI。 ## 2.CPU-GPU工作流 ### 2.1工作流 当CPU遇到图像处理时,会调用GPU进行处理,主要流程可以分为如下四步: 1. 将主存的处理数据复制到显存中 2. CPU指令驱动GPU 3. GPU中每个运算单元并行处理 4. GPU将显存结果传回主存 ![img](https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/5057f71d1c1d4ce68cc8fd799da1730c~tplv-k3u1fbpfcp-zoom-in-crop-mark:4536:0:0:0.awebp) ### 2.2屏幕成像显示原理 如果要研究图片显示原理,需要先从 CRT 显示器原理说起,如下经典图所示。CRT 的电子枪从上到下逐行扫描,扫描完成后显示器就呈现一帧画面。然后电子枪回到初始位置进行下一次扫描。为了同步显示器的显示过程和系统的视频控制器,显示器会用硬件时钟产生一系列的定时信号。当电子枪换行进行扫描时,显示器会发出一个水平同步信号(horizonal synchronization),简称 HSync;而当一帧画面绘制完成后,电子枪回复到原位,准备画下一帧前,显示器会发出一个垂直同步信号(vertical synchronization),简称 VSync。显示器通常以固定频率进行刷新,这个刷新率就是 VSync 信号产生的频率。虽然现在的显示器基本都是液晶显示屏了,但其原理基本一致。 ``` 拓展:CRT显示器学名为“阴极射线显像管”,是一种使用阴极射线管(Cathode Ray Tube)的显示器。 复制代码 ``` ![img](https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/1db4a9f9d1ee45bdbde9a3a1bba6f9fc~tplv-k3u1fbpfcp-zoom-in-crop-mark:4536:0:0:0.awebp) 下图CPU、GPU、显示器工作方式。CPU计算好显示内容提交到GPU,GPU渲染完成后将渲染结果存入到帧缓冲区,视频控制器会按照VSync信号逐帧读取帧缓冲区的数据,经过数据转换后最终由显示器进行显示。 ![img](https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/16e9f44698a948ddb40c47f884a0009e~tplv-k3u1fbpfcp-zoom-in-crop-mark:4536:0:0:0.awebp) 最简单的时候,帧缓冲区只有一个。此时,帧缓冲区和读取和刷新都会有比较大的效率问题。为了解决效率问题,GPU通常会引入两个缓冲区,即**双缓冲机制**,即这种情况下,GPU会预先渲染一帧放入缓冲区中,用于视频控制器的读取,当下一帧渲染完毕后,GPU会直接把视频控制器的指针指向第二个缓冲区。 双缓冲虽然能解决效率问题,但会引入一个新的问题。当视频控制器还未读取完成时,即屏幕内容刚显示一半时,GPU 将新的一帧内容提交到帧缓冲区并把两个缓冲区进行交换后,视频控制器就会把新的一帧数据的下半段显示到屏幕上,造成画面撕裂现象,如下图 ![img](https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/9bd1da7ed73242baadf375a126154331~tplv-k3u1fbpfcp-zoom-in-crop-mark:4536:0:0:0.awebp) 为了解决这个问题,GPU 通常有一个机制叫做垂直同步(简写也是 V-Sync),当开启垂直同步后,GPU 会等待显示器的 VSync 信号发出后,才进行新的一帧渲染和缓冲区更新。这样能解决画面撕裂现象,也增加了画面流畅度,但需要消费更多的计算资源,也会带来部分延迟。 ## 3.iOS 渲染框架 iOS为开发者提供了丰富的Framework(UIKit,Core Animation,Core Graphic,OpenGL 等等)来满足开发从上到底层各种需求,下面是iOS渲染视图框架图: ![img](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/5aade2b4426e42b1b868b28baa847748~tplv-k3u1fbpfcp-zoom-in-crop-mark:4536:0:0:0.awebp) 可以看出iOS渲染视图的核心是 Core Animation。从底层到上层依此是 GPU->(OpenGL、Core Graphic) -> Core Animation -> UIKit。 UIKit UIKit是iOS开发者最常用的框架,通过设置UIKit的控件来实现绘制界面,其实UIKit自身不具备屏幕成像的能力,它的主要职责是对用户操作事件的响应【继承自UIResponder】。 Core Animation Core Animation源自Layer kit,是一个复合引擎,职责是绘制不同的可视化内容,这些图层都是在图层树的体系之中,从本质上看:CALayer是用户所能在屏幕看见的一切的基础。 Core Graphics Core Graphics是基于Quartz绘图引擎,主要用于运行时绘制图像。可以使用此框架来处理绘图,转换,离屛渲染,图像创建,和PDF文档创建以及显示和分析。 Core Image Core Image与Core Graphics恰恰相反,Core Graphics用于运行时创建图像,而Core Image用于处理运行前创建的图像。 大部分情况下,Core Image会在GPU中完成工作,如果GPU忙,会使用CPU进行处理。 OpenGL ES `OpenGL ES`是`OpenGL`的子集,函数的内部实现是由厂家GPU开发实现。 Metal 苹果自己推出的图形图像处理框架。Metal类似于OpenGL ES ,也是一套第三方标准,具体实现是由苹果实现.相信大多数开发者没有直接使用过Metal, 但其实所有开发者在间接地使用Metal, Core Animation, Core Image, SceneKit, SpriteKit等等渲染框架都是在构建在Metal之上. ## 4.Core Animation流水线 介绍一下Core Animation工作原理如下: ![img](https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/c4737154047f48ba8abee53c54fa9cd3~tplv-k3u1fbpfcp-zoom-in-crop-mark:4536:0:0:0.awebp) 事实上, APP本身并不负责渲染, 渲染会交给一个独立的进程负责, 即Render Sever进程. APP通过IPC将渲染任务及相关数据提交给Render Server. Render Server处理完数据之后,再传至GPU,最后由GPU调用iOS的图像设备进行显示. Core Animation 流水线的详细过程: 1. 首先,由App处理事件, 如点击操作, 在此过程中app可能需要更新视图树, 相应地,图层也会发生被更新 2. 其次, App通过CPU完成对显示内容的更新, 如: 视图的创建、布局计算, 图片解码,文本绘制等, 在完成对显示内容的计算之后, app会对图层打包, 并在下一次Runloop时将其发送至Render Server, 即完成了一次Commit Transaction操作 3. Render Server主要执行OpenGL, Core Graphics相关程序, 并调用GPU 4. GPU在物理层上完成对图像的渲染 5. GPU通过Frame Buffer,视频控制器等相关部件, 将图像显示在屏幕上. 它们执行所消耗的时间远远超过 16.67 ms,因此为了满足对屏幕的 60 FPS 刷新率的支持,需要将这些步骤进行分解,通过流水线的方式进行并行执行,如下图所示。 ![img](https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/16cff7985e9a4a679cecacc094d86659~tplv-k3u1fbpfcp-zoom-in-crop-mark:4536:0:0:0.awebp) ### 4.1Commit Transaction Commit Transaction ,App调用Render Server 前最后一步Commit Transaction其实可以细分4个步骤: 1. Layout 2. Display 3. Prepare 4. Commit **Layout** Layout 阶段主要进行视图构建, 包括LayoutSubviews方法的重载, addSubview添加子视图 **Display** Display主要进行视图绘制, drawRect方法可以自定义UIView的现实,其原理是drawRect方法内部绘制寄宿图,过程使用到了CPU和内存 **Prepare** Prepare阶段属于附加步骤,一般处理图像的解码和转换操作 **Commit** Commit 用于对图层进行打包, 将它们发送至Render Server,会递归进行,因为图层和视图都是以树形结构存在 ### 4.2动画渲染原理 iOS 动画的渲染也是基于上述 Core Animation 流水线完成的。这里我们重点关注 app 与 `Render Server` 的执行流程。 如果不是特别复杂的动画,一般使用 `UIView` Animation 实现,iOS 将其处理过程分为如下三部阶段: - Step 1:调用 `animationWithDuration:animations:` 方法 - Step 2:在 Animation Block 中进行 `Layout`,`Display`,`Prepare`,`Commit` 等步骤。 - Step 3:`Render Server` 根据 Animation 逐帧进行渲染。 ![img](https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/d50822d474084adda2152c67078f33cf~tplv-k3u1fbpfcp-zoom-in-crop-mark:4536:0:0:0.awebp) ## 5.卡顿原因和解决方案 ### 5.1卡顿原理 FPS (Frames Per Second) 表示每秒渲染帧数,通常用于衡量画面的流畅度,每秒帧数越多,则表示画面越流畅,60fps 最佳,一般我们的APP的FPS 只要保持在 50-60之间,用户体验都是比较流畅的。 ![img](https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/c5223e72e1494a6db6864802460590e7~tplv-k3u1fbpfcp-zoom-in-crop-mark:4536:0:0:0.awebp) 在 VSync 信号到来后,系统图形服务会通过 CADisplayLink 等机制通知 App,App 主线程开始在 CPU 中计算显示内容,比如视图的创建、布局计算、图片解码、文本绘制等。随后 CPU 会将计算好的内容提交到 GPU 去,由 GPU 进行变换、合成、渲染。随后 GPU 会把渲染结果提交到帧缓冲区去,等待下一次 VSync 信号到来时显示到屏幕上。由于垂直同步的机制,如果在一个 VSync 时间内,CPU 或者 GPU 没有完成内容提交,则那一帧就会被丢弃,等待下一次机会再显示,而这时显示屏会保留之前的内容不变。这就是界面卡顿的原因。 从上面的图中可以看到,CPU 和 GPU 不论哪个阻碍了显示流程,都会造成掉帧现象。所以开发时,也需要分别对 CPU 和 GPU 压力进行评估和优化。 ### 5.2CPU优化 **1. 布局计算** 视图布局的计算是APP最消耗CPU资源的地方,如果在后台线程提前计算好视图布局,并且对视图布局进行缓存,这样就可以解决性能问题啦!一次性调整好对应属性,而不要多次、频繁的计算和调整控件的frame/bounds/center属性。 **2. 文本计算** 如果一个界面中包含大量文本(比如微博微信朋友圈等),文本的宽高计算会占用很大一部分资源,并且不可避免。如果你对文本显示没有特殊要求,可以参考下 UILabel 内部的实现方式:用 [NSAttributedString boundingRectWithSize:options:context:] 来计算文本宽高,用 -[NSAttributedString drawWithRect:options:context:] 来绘制文本。尽管这两个方法性能不错,但仍旧需要放到后台线程进行以避免阻塞主线程。 **3. 图片的绘制** 图像的绘制通常是指用那些以 CG 开头的方法把图像绘制到画布中,然后从画布创建图片并显示这样一个过程。这个最常见的地方就是 [UIView drawRect:] 里面了。由于 CoreGraphic 方法通常都是线程安全的,所以图像的绘制可以很容易的放到后台线程进行。一个简单异步绘制的过程大致如下(实际情况会比这个复杂得多,但原理基本一致): ```ini - (void)display { dispatch_async(backgroundQueue, ^{ CGContextRef ctx = CGBitmapContextCreate(...); // draw in context... CGImageRef img = CGBitmapContextCreateImage(ctx); CFRelease(ctx); dispatch_async(mainQueue, ^{ layer.contents = img; }); }); } 复制代码 ``` **4. 对象创建** 对象的创建会分配内存、调整属性、甚至还有读取文件等操作,比较消耗 CPU 资源。尽量用轻量的对象代替重量的对象,可以对性能有所优化。比如 CALayer 比 UIView 要轻量许多,那么不需要响应触摸事件的控件,用 CALayer 显示会更加合适。如果对象不涉及 UI 操作,则尽量放到后台线程去创建,但可惜的是包含有 CALayer 的控件,都只能在主线程创建和操作。通过 Storyboard 创建视图对象时,其资源消耗会比直接通过代码创建对象要大非常多,在性能敏感的界面里,Storyboard 并不是一个好的技术选择。 尽量推迟对象创建的时间,并把对象的创建分散到多个任务中去。尽管这实现起来比较麻烦,并且带来的优势并不多,但如果有能力做,还是要尽量尝试一下。如果对象可以复用,并且复用的代价比释放、创建新对象要小,那么这类对象应当尽量放到一个缓存池里复用。 ### 5.3GPU优化 相对于 CPU 来说,GPU 能干的事情比较单一:接收提交的纹理(Texture)和顶点描述(三角形),应用变换(transform)、混合并渲染,然后输出到屏幕上。通常你所能看到的内容,主要也就是纹理(图片)和形状(三角模拟的矢量图形)两类。 **1. 纹理的渲染** 所有的Bitmap,包括图片,栅格化等的内容,最终要由内存提交到显存里面,不论是提交到显存的过程,还是渲染Texture过程都是消耗了不少的GPU。当在较短时间显示大量图片时(比如 TableView 存在非常多的图片并且快速滑动时),CPU 占用率很低,GPU 占用非常高,界面仍然会掉帧。避免这种情况的方法只能是尽量减少在短时间内大量图片的显示,尽可能将多张图片合成为一张进行显示。 **2. 视图混合** 当多个视图(或者说 CALayer)重叠在一起显示时,GPU 会首先把他们混合到一起。如果视图结构过于复杂,混合的过程也会消耗很多 GPU 资源。为了减轻这种情况的 GPU 消耗,应用应当尽量减少视图数量和层次,并在不透明的视图里标明 opaque 属性以避免无用的 Alpha 通道合成。当然,这也可以用上面的方法,把多个视图预先渲染为一张图片来显示。 **3. 图形生成** CALayer的border、圆角、阴影以及遮罩,CASharpLayer的矢量图形显示,这样通常会造成离屏渲染,而离屏渲染通常会发生在GPU中,当一个列表有大量的圆角时候,并且快速欢动,GPU资源已经占满,而CPU资源消耗较少。 最彻底的解法是:把需要显示的图形在后台线程绘制为图片,避免使用圆角、阴影、遮罩属性等。 对于如何去监控卡顿,通过Runloop机制,可以参考掘金里面有很多文章,都是大同小异,在这就不做叙述啦!!! 原文链接:https://juejin.cn/post/6874046143160909838 ================================================ FILE: iOS资料/iOS学习音视频的过程.md ================================================ # iOS:学习音视频的过程 ## 1.音视频学习中涉及到的概念 1.我们常见的音视频格式有.mp4,mkv.avi,正如我们常见的.word 需要word 工具打开,不同格式的音视频也需要不同格式的播放器打开,这种视频格式相当于存储视频信息的容器,里面包含了音频信息,视频信息和相关的配置信息(比如.mp4格式 音视频是如何关联的信息,如何解码等) 2.封装格式:简单的来说就是一个容器,里面存储已经编码压缩好的视频数据 和 音频数据,按照一定的格式存放到一个文件中,这个文件可以称之为容器;通常除了存储音频/视频数据,还有存放音视频同步的元数据比如说字幕。 常见的视频容器格式有:mp4 mov AVI mkv 等等 通俗的来说: 容器指的是一种音视频文件格式比如.avi,协议指的是存放在音视频文件中的数据的编解码方式,一个容器可以装有各种不同的编解码方式的数据,每种编解码方式都需要不同的编解码器。MPEG、H.26X等等编码方式比较常见。 AVI、MPG、MP4等等容器比较常见。 ## 2.视频编解码方式 视频解编码的过程:对数字视频进行压缩或者解压缩 的一个过程,这期间需要考虑:视频的质量,视频的码率(用来表示视频所需要的数据量),编码算法/解码算法的复杂度,针对数据存储的错误与延迟等等因素 ### 2.1 常见的编码方式有 H.26X系列,其中H.265被称为高效视频编码,常用的是H.264,优势在于低码率,高质量的图像,容错能力强,网络适用性强 MPEG系列 视频编解码方式与视频封装格式之间有啥关系呐? 【视频封装格式】可以理解为 装着视频/音频/【视频编解码方式】等信息的容器 一种视频封装格式可以支持多种视频编码方式,但是无法确切的知道视频编解码方式 ### 2.2音频编码方式 视频中除了画面通常还有声音这就涉及到了音频编解码。视频中经常使用的音频编码方式有AAC,MP3等等,中AAC是目前比较热门的有损压缩编码技术。 为什么要做音频编码:从存储的角度或者实时传播的角度来说,数据量较大,所以需要通过压缩编码 压缩编码的基本指标是压缩比,压缩比通常小于1,如果大于1就没有意义了,压缩算法分为2种有损压缩+无损压缩 无损压缩:解压后的数据可以完全复原,有所压缩用的较多 有损压缩:解压后的数据不能完全复原,会丢失一部分信息,压缩比越小,丢失的信息就会越多,信号还有的失真就会越大 压缩编码的原理实际上就是压缩冗余的信号.冗余信号就是指不能被人耳感知的信号.包括人耳听觉范围之外的音频信号以及被掩盖掉的音频信号. 何为编码?编码就是按照一杜杰不到个格式记录采样和量化后的数据 硬编码: 使用非CPU进行编码,例如使用GPU芯片处理,性能高,低码率下通常质量低于硬编码器,但部分产品在GPU硬件平台移植了优秀的软编码算法(如X264)的,质量基本等同于软编码。 软编码: 使用CPU来进行编码计算.实现直接、简单,参数调整方便,升级易,但CPU负载重,性能较硬编码低,低码率下质量通常比硬编码要好一点。 ## 3.直播项目的流程 音视频采集 主要使用原生框架AVFoundation.framework 视频滤镜 开发中比较倾向于第三方框架GPUImage,框架是基于OpenGL ES 音视频编码压缩 硬编码: 视频: VideoToolBox框架 音频: AudioToolBox 框架 软编码: 视频: 使用FFmpeg,X264算法把视频原数据YUV/RGB编码成H264 音频: 使用fdk_aac 将音频数据PCM转换成AAC 推流 什么是推流?将采集的音频.视频数据通过流媒体协议发送到流媒体服务器 推流的技术如下: .1流媒体协议: RTMP\RTSP\HLS\FLV RTMP:长连接(TCP)每个时刻的数据收到立刻转发,低延时,跨平台差 HLS:基于http协议,集合一段时间数据,生成 ts 切片文件。 .2视频封装格式: TS\FLV .3音频封装格式: Mp3\AAC 流媒体服务器处理 .1数据分发 .2 截屏 .3实时转码 .4内容检测 拉流 什么是拉流?从流媒体服务器中 获取音频\视频数据; 拉流框架有:LFLiveKit,七牛等。 音视频解码 硬解码: 视频: VideoToolBox框架 音频: AudioToolBox 框架 软解码: 视频: 使用FFmpeg,X264算法解码 音频: 使用fdk_aac 解码 音视频播放 ijkplayer /kxmovie 播放框架;全部都是基于FFmpeg框架封装的 从网上盗了张图,方便了解直播的整个流程。 原文链接:https://blog.csdn.net/qq_33726122/article/details/89182802 ================================================ FILE: iOS资料/iOS拉取SRS流媒体服务器的Rtc流.md ================================================ # iOS拉取SRS流媒体服务器的Rtc流 ## 1、搭建flutter环境 已经有的可以跳过 安装教程:https://flutter.cn/docs/get-started/install/macos 1.下载以下安装包 stable Flutter SDK: 下载地址:https://flutter.dev/docs/development/tools/sdk/releases 2.将文件解压到目标路径 ```text cd ~/Desktop mkdir development cd ~/development unzip ~/Downloads/flutter_macos_3.0.3-stable.zip // 这里要替换成你下载的文件名 ``` 3.永久配置 flutter 的 PATH 环境变量: macOS Catalina 操作系统默认使用 Z Shell,所以需要修改 $HOME/.zshrc 文件。 ```text vim $HOME/.zshrc ``` 文件中增加下列这行命令 ```text export PATH="$PATH:/Users/zyb/Desktop/development/flutter/bin" ``` 关闭终端然后重新打开 4.验证 flutter 命令是否可用,可以执行下面的命令检测: ```text which flutter ``` 5.运行 flutter doctor 命令 ```text flutter doctor ``` ## 2、设置 iOS 开发环境 1. 通过 直接下载 或者通过 Mac App Store 来安装最新稳定版 Xcode; 2. 配置 Xcode 命令行工具以使用新安装的 Xcode 版本。从命令行中运行以下命令: ```text $ sudo xcode-select --switch /Applications/Xcode.app/Contents/Developer $ sudo xcodebuild -runFirstLaunch ``` ## 3、运行flutter_live项目 1.下载flutter_live和配置项目 ```text git clone https://github.com/ossrs/flutter_live.git cd flutter_live cd example git checkout -b ios origin/ios flutter run ``` 2.安装iOS依赖项目 ```text flutter packages get flutter pub get pod update ``` 这里会报错,把flutter_live/example/android/app/src/main/AndroidManifest.xml改成 ```text android:name="${applicationName}" ``` ![img](https://pic4.zhimg.com/80/v2-363e1378353c0dbfee645a405c3e8f2b_720w.webp) 3.配置编译证书(个人就行) ![img](https://pic3.zhimg.com/80/v2-d7d0d238efa7d1651cdc7b349d5f5a36_720w.webp) ## 4、配置环境进行拉流 ![img](https://pic4.zhimg.com/80/v2-9dcb02c7bab127433ca1e5460f68a593_720w.webp) 配置环境界面 ![img](https://pic1.zhimg.com/80/v2-7ab56ba26204d5a48e95cdcd63b8b514_720w.webp) 拉流成功 原文链接:[iOS拉取SRS流媒体服务器的Rtc流 - 资料 - 我爱音视频网 - 构建全国最权威的音视频技术交流分享论坛]( ================================================ FILE: iOS资料/iOS短视频篇:音视频编辑之音视频合成,添加水印及音视频导出.md ================================================ # iOS短视频篇:音视频编辑之音视频合成,添加水印及音视频导出 ## 1.基本介绍 音视频编辑主要依靠AVFoundation框架,首先要有一个AVMutableComposition对象composition,一个composition可以简单的认为是一组轨道(tracks)的集合,这些轨道可以是来自不同媒体资源AVAsset,AVMutableComposition提供了接口来插入或者删除轨道,也可以调整这些轨道的顺序。 下面这张图反映了一个新的 mixComposition 是怎么从已有的 AVAsset 中获取对应的 track 并进行拼接形成新的 AVAsset ![img](https://img-blog.csdnimg.cn/20190511151431700.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MjQzMzQ4MA==,size_16,color_FFFFFF,t_70) 如下图所示,我们还可以使用 AVMutableVideoComposition 来直接处理 composition 中的视频轨道。处理一个单独的 video composition 时,你可以指定它的渲染尺寸、缩放比例、帧率等参数并输出最终的视频文件。通过一些针对 video composition 的指令(AVMutableVideoCompositionInstruction 等),我们可以修改视频的背景颜色、应用 layer instructions。这些 layer instructions(AVMutableVideoCompositionLayerInstruction 等)可以用来对 composition 中的视频轨道实施图形变换、添加图形渐变、透明度变换、增加透明度渐变。此外,你还能通过设置 video composition 的 animationTool 属性来应用 Core Animation Framework 框架中的动画效果。 ![img](https://img-blog.csdnimg.cn/20190511152526982.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MjQzMzQ4MA==,size_16,color_FFFFFF,t_70) 如下图所示,你可以使用 AVAssetExportSession 相关的接口来合并你的 composition 中的 audio mix 和 video composition。你只需要初始化一个 AVAssetExportSession 对象,然后将其 audioMix 和 videoComposition 属性分别设置为你的 audio mix 和 video composition 即可。 ![img](https://img-blog.csdnimg.cn/20190511152942596.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MjQzMzQ4MA==,size_16,color_FFFFFF,t_70) ## 2.创建Composition对象 当使用 AVMutableComposition 创建自己的 composition 时,最典型的,我们可以使用 AVMutableCompositionTrack 来向 composition 中添加一个或多个 composition tracks,比如下面这个简单的例子便是向一个 composition 中添加一个音频轨道和一个视频轨道: //音频视频合成体 AVMutableComposition *mixComposition = [[AVMutableComposition alloc] init]; //创建音频通道容器 AVMutableCompositionTrack *audioTrack = [mixComposition addMutableTrackWithMediaType:AVMediaTypeAudio preferredTrackID:kCMPersistentTrackID_Invalid]; //创建视频通道容器 AVMutableCompositionTrack *videoTrack = [mixComposition addMutableTrackWithMediaType:AVMediaTypeVideo preferredTrackID:kCMPersistentTrackID_Invalid]; 当为 composition 添加一个新的 track 的时候,需要设置其媒体类型(media type)和 track ID,主要的媒体类型包括:音频、视频、字幕、文本等等。 这里需要注意的是,每个 track 都需要一个唯一的 track ID,比较方便的做法是:设置 track ID 为 kCMPersistentTrackID_Invalid 来为对应的 track 获得一个自动生成的唯一 ID。 ## 3.向 Composition 添加音视频数据 要将音视频数据添加到一个 composition track 中需要访问媒体数据所在的 AVAsset,可以使用 AVMutableCompositionTrack 的接口将具有相同媒体类型的多个 track 添加到同一个 composition track 中。下面的例子便是从多个 AVAsset 中各取出一份 video asset track,再添加到一个新的 composition track 中去 CMTime totalDuration = kCMTimeZero; for (int i = 0; i < videosPathArray.count; i++) { // AVURLAsset *asset = [AVURLAsset assetWithURL:[NSURL URLWithString:videosPathArray[i]]]; //如果创建AVURLAsset时传入的AVURLAssetPreferPreciseDurationAndTimingKey值为NO(不传默认为NO),duration会取一个估计值,计算量比较小。反之如果为YES,duration需要返回一个精确值,计算量会比较大,耗时比较长 NSDictionary* options = @{AVURLAssetPreferPreciseDurationAndTimingKey:@YES}; AVAsset* asset = [AVURLAsset URLAssetWithURL:videosPathArray[i] options:options]; NSError *erroraudio = nil; //获取AVAsset中的音频 或者视频 AVAssetTrack *assetAudioTrack = [[asset tracksWithMediaType:AVMediaTypeAudio] firstObject]; //向通道内加入音频或者视频 BOOL ba = [audioTrack insertTimeRange:CMTimeRangeMake(kCMTimeZero, asset.duration) ofTrack:assetAudioTrack atTime:totalDuration error:&erroraudio]; NSLog(@"erroraudio:%@%d",erroraudio,ba); NSError *errorVideo = nil; AVAssetTrack *assetVideoTrack = [[asset tracksWithMediaType:AVMediaTypeVideo]firstObject]; BOOL bl = [videoTrack insertTimeRange:CMTimeRangeMake(kCMTimeZero, asset.duration) ofTrack:assetVideoTrack atTime:totalDuration error:&errorVideo]; NSLog(@"errorVideo:%@%d",errorVideo,bl); totalDuration = CMTimeAdd(totalDuration, asset.duration); } 这里需要注意:如果创建AVURLAsset时传入的AVURLAssetPreferPreciseDurationAndTimingKey值为NO(不传默认为NO),duration会取一个估计值,计算量比较小。反之如果为YES,duration需要返回一个精确值,计算量会比较大,耗时比较长,一般我们对短视频的音视频编辑时都是将这个属性设置为YES,来获取准确的duration。 ## 4.视频水印处理 处理音频是我们使用 AVMutableAudioMix,那么处理视频时,我们就使用 AVMutableVideoComposition,只需要一个 AVMutableVideoComposition 实例就可以为 composition 中所有的 video track 做处理,比如设置渲染尺寸、缩放、播放帧率等等。所有的 video composition 也必然对应一组 AVVideoCompositionInstruction 实例,每个 AVVideoCompositionInstruction 中至少包含一条 video composition instruction。我们可以使用 AVMutableVideoCompositionInstruction 来创建我们自己的 video composition instruction,通过这些指令,我们可以修改 composition 的背景颜色、后处理、layer instruction 等等。 我们也可以用 video composition instructions 来应用 video composition layer instructions。AVMutableVideoCompositionLayerInstruction 可以用来设置 video track 的图形变换、图形渐变、透明度、透明度渐变等等。一个 video composition instruction 的 layerInstructions 属性中所存储的 layer instructions 的顺序决定了 tracks 中的视频帧是如何被放置和组合的。 我们还能通过设置 video composition 的 animationTool 属性来使用 Core Animation Framework 框架的强大能力。比如:设置视频水印、视频标题、动画浮层等。 下面的代码展示了直接使用 Core Animation Layer 在视频帧中渲染动画效果的一个例子,在视频右上角添加水印: CGSize videoSize = [videoTrack naturalSize]; CALayer* aLayer = [CALayer layer]; aLayer.contents = (id)waterImg.CGImage; aLayer.frame = CGRectMake(videoSize.width - waterImg.size.width - 30, videoSize.height - waterImg.size.height*3, waterImg.size.width, waterImg.size.height); aLayer.opacity = 0.9; CALayer *parentLayer = [CALayer layer]; CALayer *videoLayer = [CALayer layer]; parentLayer.frame = CGRectMake(0, 0, videoSize.width, videoSize.height); videoLayer.frame = CGRectMake(0, 0, videoSize.width, videoSize.height); [parentLayer addSublayer:videoLayer]; [parentLayer addSublayer:aLayer]; AVMutableVideoComposition* videoComp = [AVMutableVideoComposition videoComposition]; videoComp.renderSize = videoSize; //表示 30 帧每秒 videoComp.frameDuration = CMTimeMake(1, 30); //应用 Core Animation Framework 框架中的动画效果。 videoComp.animationTool = [AVVideoCompositionCoreAnimationTool videoCompositionCoreAnimationToolWithPostProcessingAsVideoLayer:videoLayer inLayer:parentLayer]; AVMutableVideoCompositionInstruction* instruction = [AVMutableVideoCompositionInstruction videoCompositionInstruction]; instruction.timeRange = CMTimeRangeMake(kCMTimeZero, [mixComposition duration]); AVAssetTrack* mixVideoTrack = [[mixComposition tracksWithMediaType:AVMediaTypeVideo] objectAtIndex:0]; AVMutableVideoCompositionLayerInstruction* layerInstruction = [AVMutableVideoCompositionLayerInstruction videoCompositionLayerInstructionWithAssetTrack:mixVideoTrack]; //layerInstructions 属性中所存储的 layer instructions 的顺序决定了 tracks 中的视频帧是如何被放置和组合的。 instruction.layerInstructions = [NSArray arrayWithObject:layerInstruction]; videoComp.instructions = [NSArray arrayWithObject: instruction]; ## 5.音视频导出 主要是通过AVAssetExportSession这个类来导出音视频,它可以用来合并你添加了水印等的videoComposition和audioMix。 下面是使用AVAssetExportSession导出音视频的例子: NSURL *mergeFileURL = [NSURL fileURLWithPath:outpath]; //视频导出工具 AVAssetExportSession *exporter = [[AVAssetExportSession alloc] initWithAsset:mixComposition presetName:AVAssetExportPreset1280x720]; exporter.videoComposition = videoComp; /* exporter.progress 导出进度 This property is not key-value observable. 不支持kvo 监听 只能用定时器监听了 NStimer */ exporter.outputURL = mergeFileURL; exporter.outputFileType = AVFileTypeQuickTimeMovie; exporter.shouldOptimizeForNetworkUse = YES; [exporter exportAsynchronouslyWithCompletionHandler:^{ dispatch_async(dispatch_get_main_queue(), ^{ [videoCamera stopCameraCapture]; EditVideoViewController* view = [[EditVideoViewController alloc]init]; view.width = _width; view.hight = _hight; view.bit = _bit; view.frameRate = _frameRate; view.videoURL = [NSURL fileURLWithPath:outpath];; [[NSNotificationCenter defaultCenter] removeObserver:self]; // [[AppDelegate sharedAppDelegate] pushViewController:view animated:YES]; if (self.delegate&&[self.delegate respondsToSelector:@selector(pushCor:)]) { [self.delegate pushCor:view]; } [self removeFromSuperview]; }); 原文链接:https://blog.csdn.net/weixin_42433480/article/details/90109873 ================================================ FILE: iOS资料/iOS硬编解码相关知识.md ================================================ # iOS硬编解码相关知识 ## 1、软编与硬编概念 **1、软编码:使用CPU进行编码。** 实现直接、简单,参数调整方便,升级易,但CPU负载重,性能较硬编码低,低码率下质量通常比硬编码要好一点。 **2、硬编码:不使用CPU进行编码,使用显卡GPU,专用的DSP、FPGA、ASIC芯片等硬件进行编码。** 性能高,低码率下通常质量低于软编码器,但部分产品在GPU硬件平台移植了优秀的软编码算法(如X264)的,质量基本等同于软编码。 苹果在iOS 8.0系统之前,没有开放系统的硬件编码解码功能,不过Mac OS系统一直有,被称为Video ToolBox的框架来处理硬件的编码和解码,终于在iOS 8.0(即[WWDC 2014 513](https://developer.apple.com/videos/play/wwdc2014/513/))后,苹果将该框架引入iOS系统。 ## 2、H.264编码原理 H.264是新一代的编码标准,以高压缩高质量和支持多种网络的流媒体传输著称,在编码方面,我理解的理论依据是:参照一段时间内图像的统计结果表明,在相邻几幅图像画面中,一般有差别的像素只有10%以内的点,亮度差值变化不超过2%,而色度差值的变化只有1%以内。所以对于一段变化不大图像画面,我们可以先编码出一个完整的图像帧A,随后的B帧就不编码全部图像,只写入与A帧的差别,这样B帧的大小就只有完整帧的1/10或更小!B帧之后的C帧如果变化不大,我们可以继续以参考B的方式编码C帧,这样循环下去。这段图像我们称为一个序列(序列就是有相同特点的一段数据),当某个图像与之前的图像变化很大,无法参考前面的帧来生成,那我们就结束上一个序列,开始下一段序列,也就是对这个图像生成一个完整帧A1,随后的图像就参考A1生成,只写入与A1的差别内容。 需要注意的是: > 在H264协议里定义了三种帧,完整编码的帧叫I帧,参考之前的I帧生成的只包含差异部分编码的帧叫P帧,还有一种参考前后的帧编码的帧叫B帧。 > H264采用的核心算法是帧内压缩和帧间压缩,帧内压缩是生成I帧的算法,帧间压缩是生成B帧和P帧的算法。 ## 3、序列的说明 > 在H264中图像以序列为单位进行组织,一个序列是一段图像编码后的数据流,以I帧开始,到下一个I帧结束。 一个序列的第一个图像叫做 IDR 图像(立即刷新图像),IDR 图像都是 I 帧图像。H.264 引入 IDR 图像是为了解码的重同步,当解码器解码到 IDR 图像时,立即将参考帧队列清空,将已解码的数据全部输出或抛弃,重新查找参数集,开始一个新的序列。这样,如果前一个序列出现重大错误,在这里可以获得重新同步的机会。IDR图像之后的图像永远不会使用IDR之前的图像的数据来解码。 一个序列就是一段内容差异不太大的图像编码后生成的一串数据流。当运动变化比较少时,一个序列可以很长,因为运动变化少就代表图像画面的内容变动很小,所以就可以编一个I帧,然后一直P帧、B帧了。当运动变化多时,可能一个序列就比较短了,比如就包含一个I帧和3、4个P帧。 ## 4、对三种帧的简单介绍 I、B、P各帧是根据压缩算法的需要,是人为定义的,它们都是实实在在的物理帧。一般来说,I帧的压缩率是7(跟JPG差不多),P帧是20,B帧可以达到50。可见使用B帧能节省大量空间,节省出来的空间可以用来保存多一些I帧,这样在相同码率下,可以提供更好的画质。 ![img](https:////upload-images.jianshu.io/upload_images/808077-86b328a3dc0aaa1e.png?imageMogr2/auto-orient/strip|imageView2/2/w/643/format/webp) B-012.png 说明: I帧:红色;P帧:蓝色;B帧:绿色。 ## 5、H264压缩算法的说明 1、分组:把几帧图像分为一组(GOP,也就是一个序列),为防止运动变化,帧数不宜取多。 2、定义帧:将每组内各帧图像定义为三种类型,即I帧、B帧和P帧; 3、预测帧:以I帧做为基础帧,以I帧预测P帧,再由I帧和P帧预测B帧; 4、数据传输:最后将I帧数据与预测的差值信息进行存储和传输。 5、帧内(Intraframe)压缩也称为空间压缩(Spatial compression)。 当压缩一帧图像时,仅考虑本帧的数据而不考虑相邻帧之间的冗余信息,这实际上与静态图像压缩类似。帧内一般采用有损压缩算法,由于帧内压缩是编码一个完整的图像,所以可以独立的解码、显示。帧内压缩一般达不到很高的压缩,跟编码jpeg差不多。 6、帧间(Interframe)压缩。 相邻几帧的数据有很大的相关性,或者说前后两帧信息变化很小的特点。也即连续的视频其相邻帧之间具有冗余信息,根据这一特性,压缩相邻帧之间的冗余量就可以进一步提高压缩量,减小压缩比。帧间压缩也称为时间压缩(Temporal compression),它通过比较时间轴上不同帧之间的数据进行压缩。帧间压缩一般是无损的。帧差值(Frame differencing)算法是一种典型的时间压缩法,它通过比较本帧与相邻帧之间的差异,仅记录本帧与其相邻帧的差值,这样可以大大减少数据量。 7、有损(Lossy)压缩和无损(Lossy less)压缩。 无损压缩也即压缩前和解压缩后的数据完全一致。多数的无损压缩都采用RLE行程编码算法。 有损压缩意味着解压缩后的数据与压缩前的数据不一致。在压缩的过程中要丢失一些人眼和人耳所不敏感的图像或音频信息,而且丢失的信息不可恢复。几乎所有高压缩的算法都采用有损压缩,这样才能达到低数据率的目标。丢失的数据率与压缩比有关,压缩比越小,丢失的数据越多,解压缩后的效果一般越差。此外,某些有损压缩算法采用多次重复压缩的方式,这样还会引起额外的数据丢失。 ## 6、DTS与PTS的区别 DTS主要用于视频的解码,在解码阶段使用. PTS主要用于视频的同步和输出. 在display的时候使用.在没有B frame的情况下.DTS和PTS的输出顺序是一样的。 下面给出一个GOP为15的例子,其解码的参照frame及其解码的顺序都在里面: ![img](https:////upload-images.jianshu.io/upload_images/808077-4a1af513608a4e73.png?imageMogr2/auto-orient/strip|imageView2/2/w/610/format/webp) B-001.png 如上图: I frame 的解码不依赖于任何的其它的帧.而p frame的解码则依赖于其前面的I frame或者P frame.B frame的解码则依赖于其前的最近的一个I frame或者P frame 及其后的最近的一个P frame. ## 7、iOS系统 H.264视频硬件编解码说明 ### 7.1 VideoToolbox的介绍 在iOS中,与视频相关的Framework库有5个,从顶层开始分别是 `AVKit` -> `AVFoundation` -> `VideoToolbox` -> `Core Media` -> `Core Video` 其中VideoToolbox可以将视频解压到`CVPixelBuffer`,也可以压缩到`CMSampleBuffer`。 但是我们常用的是`CMSampleBuffer`. ### 7.2 VideoToolbox中的对象 #### 7.2.1 CVPixelBuffer 编码前和解码后的图像数据结构(未压缩光栅图像缓存区-Uncompressed Raster Image Buffer) ![img](https:////upload-images.jianshu.io/upload_images/808077-0e993eef7d26cea9.png?imageMogr2/auto-orient/strip|imageView2/2/w/359/format/webp) B-002.png #### 7.2.2 CVPixelBufferPool 存放CVPixelBuffer ![img](https:////upload-images.jianshu.io/upload_images/808077-603c2783fbb1bbdc.png?imageMogr2/auto-orient/strip|imageView2/2/w/448/format/webp) B-003.png #### 7.2.3 pixelBufferAttributes CFDictionary对象,可能包含了视频的宽高,像素格式类型(32RGBA, YCbCr420),是否可以用于OpenGL ES等相关信息 #### 7.2.4 CMTime 时间戳相关。时间以 64-big/32-bit形式出现。 分子是64-bit的时间值,分母是32-bit的时标(time scale) #### 7.2.5 CMClock 时间戳相关。时间以 64-big/32-bit形式出现。 分子是64-bit的时间值,分母是32-bit的时标(time scale)。它封装了时间源,其中CMClockGetHostTimeClock()封装了mach_absolute_time() #### 7.2.6 CMTimebase 时间戳相关。时间以 64-big/32-bit形式出现。CMClock上的控制视图。提供了时间的映射:CMTimebaseSetTime(timebase, kCMTimeZero); 速率控制: CMTimebaseSetRate(timebase, 1.0); #### 7.2.7 CMBlockBuffer 编码后,结果图像的数据结构 #### 7.2.8 CMVideoFormatDescription 编解码前后的视频图像均封装在CMSampleBuffer中,如果是编码后的图像,以CMBlockBuffe方式存储;解码后的图像,以CVPixelBuffer存储。 #### 7.2.9 CMSampleBuffer 存放编解码前后的视频图像的容器数据结构。如图所示,编解码前后的视频图像均封装在CMSampleBuffer中,如果是编码后的图像,以CMBlockBuffer方式存储;解码后的图像,以CVPixelBuffer存储。CMSampleBuffer里面还有另外的时间信息CMTime和视频描述信息CMVideoFormatDesc。 ![img](https:////upload-images.jianshu.io/upload_images/808077-83cf3921e0ba9840.png?imageMogr2/auto-orient/strip|imageView2/2/w/506/format/webp) B-004.png ## 8、硬解码 > 目标:如何将从网络处传来H.264编码后的视频码流显示在手机屏幕上? 实现步骤如下: ### 8.1 将 H.264码流转换为 CMSampleBuffer > CMSampleBuffer = CMTime + FormatDesc + CMBlockBuffer 需要从H.264的码流里面提取出以上的三个信息。最后组合成CMSampleBuffer,提供给硬解码接口来进行解码工作。 在H.264的语法中,有一个最基础的层,叫做Network Abstraction Layer, 简称为NAL。H.264流数据正是由一系列的NAL单元(NAL Unit, 简称NAUL)组成的。 ![img](https:////upload-images.jianshu.io/upload_images/808077-46e13cc6c5037eff.png?imageMogr2/auto-orient/strip|imageView2/2/w/667/format/webp) B-005.png H264的码流由NALU单元组成,一个NALU可能包含有: 1. 视频帧,视频帧也就是视频片段,具体有 P帧, I帧,B帧 ![img](https:////upload-images.jianshu.io/upload_images/808077-1d1ca83dc6ea0098.png?imageMogr2/auto-orient/strip|imageView2/2/w/666/format/webp) B-006.png 2)H.264属性合集-FormatDesc(包含 SPS和PPS),即流数据中,属性集合可能是这样的: ![img](https:////upload-images.jianshu.io/upload_images/808077-8702dddef541bcf9.png?imageMogr2/auto-orient/strip|imageView2/2/w/317/format/webp) B-007.png 经过处理之后,在Format Description中则是: ![img](https:////upload-images.jianshu.io/upload_images/808077-abc92ebd63fe9aa1.png?imageMogr2/auto-orient/strip|imageView2/2/w/461/format/webp) B-008.png 需要注意的是: 要从基础的流数据将SPS和PPS转化为Format Desc中的话,需要调用`CMVideoFormatDescriptionCreateFromH264ParameterSets()`方法。 3)NALU header 对于流数据来说,一个NAUL的Header中,可能是0x00 00 01或者是0x00 00 00 01作为开头(两者都有可能,下面以0x00 00 01作为例子)。0x00 00 01因此被称为开始码(Start code). ![img](https:////upload-images.jianshu.io/upload_images/808077-27500d650ed825d5.png?imageMogr2/auto-orient/strip|imageView2/2/w/252/format/webp) B-009.png 总结以上知识,我们知道H264的码流由NALU单元组成,NALU单元包含视频图像数据和H264的参数信息。其中视频图像数据就是CMBlockBuffer,而H264的参数信息则可以组合成FormatDesc。具体来说参数信息包含SPS(Sequence Parameter Set)和PPS(Picture Parameter Set).如下图显示了一个H.264码流结构: ![img](https:////upload-images.jianshu.io/upload_images/808077-48d2b9429814ce3d.png?imageMogr2/auto-orient/strip|imageView2/2/w/668/format/webp) B-010.png **(1)提取sps和pps生成FormatDesc** - 每个NALU的开始码是0x00 00 01,按照开始码定位NALU - 通过类型信息找到sps和pps并提取,开始码后第一个byte的后5位,7代表sps,8代表pps - 使用CMVideoFormatDescriptionCreateFromH264ParameterSets函数来构建CMVideoFormatDescriptionRef **(2)提取视频图像数据生成CMBlockBuffer** - 通过开始码,定位到NALU - 确定类型为数据后,将开始码替换成NALU的长度信息(4 Bytes) - 使用CMBlockBufferCreateWithMemoryBlock接口构造CMBlockBufferRef **(3)根据需要,生成CMTime信息。** (实际测试时,加入time信息后,有不稳定的图像,不加入time信息反而没有,需要进一步研究,这里建议不加入time信息) 根据上述得到CMVideoFormatDescriptionRef、CMBlockBufferRef和可选的时间信息,使用CMSampleBufferCreate接口得到CMSampleBuffer数据这个待解码的原始的数据。如下图所示的H264数据转换示意图。 ![img](https:////upload-images.jianshu.io/upload_images/808077-e2452efdcc319ff6.png?imageMogr2/auto-orient/strip|imageView2/2/w/669/format/webp) B-011.png ### 8.2 将 CMSampleBuffer显示出来 显示的方式有两种: 1)、将CMSampleBuffers提供给系统的AVSampleBufferDisplayLayer 直接显示 使用方式和其它CALayer类似。该层内置了硬件解码功能,将原始的CMSampleBuffer解码后的图像直接显示在屏幕上面,非常的简单方便。 2)、利用OPenGL渲染 通过VTDecompression接口来,将CMSampleBuffer解码成图像,将图像通过UIImageView或者OpenGL上显示。 初始化VTDecompressionSession,设置解码器的相关信息。初始化信息需要CMSampleBuffer里面的FormatDescription,以及设置解码后图像的存储方式。demo里面设置的CGBitmap模式,使用RGB方式存放。编码后的图像经过解码后,会调用一个回调函数,将解码后的图像交个这个回调函数来进一步处理。我们就在这个回调里面,将解码后的图像发给control来显示,初始化的时候要将回调指针作为参数传给create接口函数。最后使用create接口对session来进行初始化。 上所述的回调函数可以完成CGBitmap图像转换成UIImage图像的处理,将图像通过队列发送到Control来进行显示处理。 调用VTDecompresSessionDecodeFrame接口进行解码操作。解码后的图像会交由以上两步骤设置的回调函数,来进一步的处理。 ## 9、硬解码 > 硬编码的使用也通过一个典型的应用场景来描述。首先,通过摄像头来采集图像,然后将采集到的图像,通过硬编码的方式进行编码,最后编码后的数据将其组合成H264的码流通过网络传播。 ### 9.1 摄像头采集数据 摄像头采集,iOS系统提供了AVCaptureSession来采集摄像头的图像数据。设定好session的采集解析度。再设定好input和output即可。output设定的时候,需要设置delegate和输出队列。在delegate方法,处理采集好的图像。 图像输出的格式,是未编码的CMSampleBuffer形式。 ### 9.2 使用VTCompressionSession进行硬编码 1)初始化VTCompressionSession VTCompressionSession初始化的时候,一般需要给出width宽,height长,编码器类型kCMVideoCodecType_H264等。然后通过调用VTSessionSetProperty接口设置帧率等属性,demo里面提供了一些设置参考,测试的时候发现几乎没有什么影响,可能需要进一步调试。最后需要设定一个回调函数,这个回调是视频图像编码成功后调用。全部准备好后,使用VTCompressionSessionCreate创建session 2)提取摄像头采集的原始图像数据给VTCompressionSession来硬编码 摄像头采集后的图像是未编码的CMSampleBuffer形式,利用给定的接口函数CMSampleBufferGetImageBuffer从中提取出CVPixelBufferRef,使用硬编码接口VTCompressionSessionEncodeFrame来对该帧进行硬编码,编码成功后,会自动调用session初始化时设置的回调函数。 3)利用回调函数,将因编码成功的CMSampleBuffer转换成H264码流,通过网络传播。 > 基本上是硬解码的一个逆过程。解析出参数集SPS和PPS,加上开始码后组装成NALU。提取出视频数据,将长度码转换成开始码,组长成NALU。将NALU发送出去。 ------ 本文主要为[转载学习](https://www.jianshu.com/p/668e6abbed8c),部分细节有删改。 ------ 相关资料传送: [iOS8系统H264视频硬件编解码说明](https://www.jianshu.com/p/a6530fa46a88) [简单谈谈硬编码和软编码](https://blog.csdn.net/charleslei/article/details/44599041) [I,P,B帧和PTS,DTS的关系](http://www.cnblogs.com/qingquan/archive/2011/07/27/2118967.html) 原文链接:https://www.jianshu.com/p/5d555aa55ea1 ================================================ FILE: iOS资料/iOS视图渲染与性能优化.md ================================================ # iOS视图渲染与性能优化 ## 1、视图渲染 视图渲染的处理层级图如下: ![img](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/34ae35cf609343fc96cc102b64c579de~tplv-k3u1fbpfcp-zoom-in-crop-mark:4536:0:0:0.awebp) UIKit是常用的框架,显示、动画都通过CoreAnimation; CoreAnimation是核心动画,依赖于OpenGL ES做GPU渲染(目前最新的iPhone已经都使用Metal,**为了和图文一致,本文后面继续使用OpenGL ES来描述**),CoreGraphics做CPU渲染; 最底层的GraphicsHardWare是图形硬件。 视图渲染的整体流程如下: ![img](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/3f7686424cb748159594d73a3c967e45~tplv-k3u1fbpfcp-zoom-in-crop-mark:4536:0:0:0.awebp) 视图渲染到屏幕上需要CPU和GPU一起协作。App将一部分数据通过CoreGraphics、CoreImage调用CPU进行预处理,最终通过OpenGL ES将数据传送到 GPU,最终显示到屏幕。 ## 2、渲染过程 ![img](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/90c29bf8d22e466087f14d04d484b05f~tplv-k3u1fbpfcp-zoom-in-crop-mark:4536:0:0:0.awebp) 渲染的具体过程可以用上图来描述: - 1、CoreAnimation提交会话,包括自己和子树(view hierarchy)的layout状态等; - 2、RenderServer解析提交的子树状态,生成绘制指令; - 3、GPU执行绘制指令; - 4、显示渲染后的数据; 其中App的Commit流程又可以分为Layout、Display、Prepare、Commit四个步骤。 ![img](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/c53940fec0624136a53d36d4e2688eee~tplv-k3u1fbpfcp-zoom-in-crop-mark:4536:0:0:0.awebp) ### 2.1布局(Layout) 调用layoutSubviews方法; 调用addSubview:方法; > 会造成CPU和I/O瓶颈; ### 2.2显示(Display) 通过drawRect绘制视图; 绘制string(字符串); > 会造成CPU和内存瓶颈; 每个UIView都有CALayer,同时图层有一个像素存储空间,存放视图;调用`-setNeedsDisplay`的时候,仅会设置图层为dirty。 当渲染系统准备就绪时会调用视图的`-display`方法,同时装配像素存储空间,建立一个CoreGraphics上下文(CGContextRef),将上下文push进上下文堆栈,绘图程序进入对应的内存存储空间。 ```ini UIBezierPath *path = [UIBezierPath bezierPath]; [path moveToPoint:CGPointMake(10, 10)]; [path addLineToPoint:CGPointMake(20, 20)]; [path closePath]; path.lineWidth = 1; [[UIColor redColor] setStroke]; [path stroke]; 复制代码 ``` 在-drawRect方法中实现如上代码,UIKit会将自动生成的CGContextRef 放入上下文堆栈。 当绘制完成后,视图的像素会被渲染到屏幕上;当下次再次调用视图的-setNeedsDisplay,将会再次调用-drawRect方法。 ### 2.3准备提交(Prepare) 解码图片; 图片格式转换; > 当我们使用UIImage、CGImage时,图片并没有真正解码。iOS会先用一些基础的图像信息创建对象,等到真正使用时再创建bitmap并进行解码。尽量避免使用不支持硬解的图片格式,比如说webp; ### 2.4提交(Commit) 打包layers并发送到渲染server; 递归提交子树的layers; 如果子树太复杂,会消耗很大,对性能造成影响; > 尽可能简化viewTree; 当显示一个UIImageView时,Core Animation会创建一个OpenGL ES纹理,并确保在这个图层中的位图被上传到对应的纹理中。当你重写` -drawInContext`方法时,Core Animation会请求分配一个纹理,同时确保Core Graphics会将你在`-drawInContext`中绘制的东西放入到纹理的位图数据中。 ## 3、Tile-Based 渲染 Tiled-Based 渲染是移动设备的主流。整个屏幕会分解成N*Npixels组成的瓦片(Tiles),tiles存储于SoC 缓存(SoC=system on chip,片上系统,是在整块芯片上实现一个复杂系统功能,如intel cpu,整合了集显,内存控制器,cpu运核心,缓存,队列、非核心和I/O控制器)。 几何形状会分解成若干个tiles,对于每一块tile,把必须的几何体提交到OpenGL ES,然后进行渲染(光栅化)。完毕后,将tile的数据发送回cpu。 ![img](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/b4c94a4466f346d6a134588b514a0017~tplv-k3u1fbpfcp-zoom-in-crop-mark:4536:0:0:0.awebp) > 传送数据是非常消耗性能的。相对来说,多次计算比多次发送数据更加经济高效,但是额外的计算也会产生一些性能损耗。 > PS:在移动平台控制帧率在一个合适的水平可以节省电能,会有效的延长电池寿命,同时会相对的提高用户体验。 ### 3.1渲染流程 普通的Tile-Based渲染流程如下: ![img](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/eaf4ae319bcc45bdb15e6f7baea7e479~tplv-k3u1fbpfcp-zoom-in-crop-mark:4536:0:0:0.awebp) 1、CommandBuffer,接受OpenGL ES处理完毕的渲染指令; 2、Tiler,调用顶点着色器,把顶点数据进行分块(Tiling); 3、ParameterBuffer,接受分块完毕的tile和对应的渲染参数; 4、Renderer,调用片元着色器,进行像素渲染; 5、RenderBuffer,存储渲染完毕的像素; ### 3.2离屏渲染 —— 遮罩(Mask) ![img](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/a7a2f02c88674a9197ec4be63a6efe6d~tplv-k3u1fbpfcp-zoom-in-crop-mark:4536:0:0:0.awebp) 1、渲染layer的mask纹理,同Tile-Based的基本渲染逻辑; 2、渲染layer的content纹理,同Tile-Based的基本渲染逻辑; 3、Compositing操作,合并1、2的纹理; ### 3.3离屏渲染 ——UIVisiualEffectView ![img](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/0accc7bfd39143618c3461bb0b6eeccd~tplv-k3u1fbpfcp-zoom-in-crop-mark:4536:0:0:0.awebp) 使用UIBlurEffect,应该是尽可能小的view,因为性能消耗巨大。 60FPS的设备,每帧只有16.67ms的时间进行处理。 ![img](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/1102fb148b3b4e96af06491d65fa069c~tplv-k3u1fbpfcp-zoom-in-crop-mark:4536:0:0:0.awebp) ### 3.4渲染等待 由于每一帧的顶点和像素处理相对独立,iOS会将CPU处理,顶点处理,像素处理安排在相邻的三帧中。如图,当一个渲染命令提交后,要在当帧之后的第三帧,渲染结果才会显示出来。 ![img](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/558ee835ce2e4b7fbe5656e097a3f0c8~tplv-k3u1fbpfcp-zoom-in-crop-mark:4536:0:0:0.awebp) ### 3.5光栅化 把视图的内容渲染成纹理并缓存,可以通过CALayer的shouldRasterize属性开启光栅化。 注意,光栅化的元素,总大小限制为2.5倍的屏幕。 更新内容时,会启用离屏渲染,所以更新代价较大,只能用于静态内容;而且如果光栅化的元素100ms没有被使用将被移除,故而不常用元素的光栅化并不会优化显示。 ### 3.6组透明度 CALayer的allowsGroupOpacity属性,UIView 的alpha属性等同于 CALayer opacity属性。 当`GroupOpacity=YES`时,会先不考虑透明度,等绘制完成所有layer(自身+子layers),再统一计算透明。 假设某个视图A有一个字视图B,他们的alpha都是0.5(根视图是黑色,A和B都是白色),当我们绘制视图的时候: 如果未开启组透明,首先是绘制视图A(0.5白色),然后再绘制视图B,绘制视图B的时候是在父视图0.5白色和根视图0.5黑色的基础上叠加视图B的0.5白色,最终就是0.75白色。 ![img](https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/29ef7e8ee11c4bc9a516f29c0a50e5f8~tplv-k3u1fbpfcp-zoom-in-crop-mark:4536:0:0:0.awebp) 如果开启了组透明,首先是绘制视图A(白色),然后在A的基础上直接绘制视图B(白色),最终再统一计算透明0.5,所以A和B的颜色保持一致。(边界是特意加的,为了区分视图B) ![img](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/e45e3486092240b09aa71e0a88ee9a48~tplv-k3u1fbpfcp-zoom-in-crop-mark:4536:0:0:0.awebp) > The default value is read from the boolean UIViewGroupOpacity property in the main bundle’s Info.plist file. If no value is found, the default value is YES for apps linked against the iOS 7 SDK or later and NO for apps linked against an earlier SDK. > 为了让子视图与父视图保持同样的透明度和优化性能,从 iOS 7 以后默认全局开启了这个功能。对现在的开发者来说,几乎可以不用关注。 ## 4、性能优化 这个是WWDC推荐的检查项目: ![img](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/0f13020c1dce4d98978be2f50e99ee1b~tplv-k3u1fbpfcp-zoom-in-crop-mark:4536:0:0:0.awebp) 1、帧率一般在多少? > 60帧每秒;(TimeProfiler工具可以查看耗时) 2、是否存在CPU和GPU瓶颈? (查看占有率) > 更少的使用CPU和GPU可以有效的保存电量; 3、是否额外使用CPU来进行渲染? > 重写了drawRect会导致CPU渲染;在CPU进行渲染时,GPU大多数情况是处于等待状态; 4、是否存在过多离屏渲染? > 越少越好;离屏渲染会导致上下文切换,GPU产生idle; 5、是否渲染过多视图? > 视图越少越好;透明度为1的视图更受欢迎; 6、使用奇怪的图片格式和大小? > 避免格式转换和调整图片大小;一个图片如果不被GPU支持,那么需要CPU来转换。(Xcode有对PNG图片进行特殊的算法优化) 7、是否使用昂贵的特效? > 视图特效存在消耗,调整合适的大小;例如前面提到的UIBlurEffect; 8、是否视图树上存在不必要的元素? > 理解视图树上所有点的必要性,去掉不必要的元素;忘记remove视图是很常见的事情,特别是当View的类比较大的时候。 ![img](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/de7725099d01477c91df8a402c07d328~tplv-k3u1fbpfcp-zoom-in-crop-mark:4536:0:0:0.awebp) 以上,是8个问题对应的工具。遇到性能问题,先**分析、定位问题所在**,而不是埋头钻进代码的海洋。 ![img](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/ea5287bed6b0482d9f98d62021d51cf8~tplv-k3u1fbpfcp-zoom-in-crop-mark:4536:0:0:0.awebp) ## 5、性能优化实例 ### 5.1阴影 ![img](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/8202dc6d12334d6292c2d4fc5f2924a6~tplv-k3u1fbpfcp-zoom-in-crop-mark:4536:0:0:0.awebp) 上面的做法,会导致离屏渲染;下面的做法是正确的做法。 ### 5.2圆角 ![img](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/f77b427dbea342a5bdd8e6824ff51c4f~tplv-k3u1fbpfcp-zoom-in-crop-mark:4536:0:0:0.awebp) 不要使用不必要的mask,可以预处理图片为圆形;或者添加中间为圆形透明的白色背景视图。即使添加额外的视图,会导致额外的计算;但仍然会快一点,因为相对于切换上下文,GPU更擅长渲染。 离屏渲染会导致GPU利用率不到100%,帧率却很低。(切换上下文会产生idle time) ### 5.3工具 使用instruments的CoreAnimation工具来检查离屏渲染,黄色是我们不希望看到的颜色。 ![img](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/4deff2d45f6347a0bcc525e1a6f2d434~tplv-k3u1fbpfcp-zoom-in-crop-mark:4536:0:0:0.awebp) > 使用真机来调试,因为模拟器使用的CALayer是OSX的CALayer,不是iOS的CALayer。如果用模拟器调试,会发现所有的视图都是黄色。 原文链接:https://juejin.cn/post/6960516630975774734 ================================================ FILE: iOS资料/iOS视频开发:视频H264硬编码.md ================================================ # iOS视频开发:视频H264硬编码 ## 1、前言 前面我们已经介绍了在iOS开发中如果调用摄像头进行视频数据的采集和编解码。但折腾了这么多,对于YUV这玩意儿还是不是特别理解。其实在我的个人实践过程中我也一直搞不懂这个YUV,一顿恶补之后,我们来通俗一点地讲YUV这个数据格式。 1、YUV & RGB概述 2、YUV的采样方式 3、YUV的储存方式及常见格式 4、YUV数据量计算 5、YUV裁剪 ## 2、YUV & RGB概述 RGB色彩模式是工业界的一种颜色标准,我们知道三原色(RGB)通过互相叠加可以得到各式各样的颜色,是目前运用最广的颜色系统之一。 与我们熟知的RGB一样,YUV也是一种颜色编码方法,主要运用在电视系统及模拟视频领域。YUV的原理是把亮度和色度分离,利用人眼对亮度的敏感度超过色度这个特性,我们偷摸摸把色度信息减少一些,人眼也很难察觉到。甚至没有色度信息(UV分量)依旧可以显示完整的图像,只不过是黑白的。这样的设计就很好地解决了彩色电视机跟黑白电视机的兼容问题。并且由于可以减少一些色度信息,YUV数据的总尺寸相对RGB数据要小一些。YUV这三个字母中,Y表示亮度(灰度值),U和V表示色度(色彩及饱和度)。YUV是编译true-color颜色空间(colorspace)的种类,Y'UV,YUV,[YCbCr](https://baike.baidu.com/item/YCbCr),[YPbPr](https://baike.baidu.com/item/YPbPr)等专有名词都可以称为YUV。 ## 3、YUV的采样方式 YUV的主流采样方式有:`YUV4:4:4`、`YUV4:2:2`、`YUV4:2:0`,这是个什么意思呢?我们看下面这张图,黑色实心的点表示Y分量,黑色空心的点表示UV分量,那么下图的意思就是: `YUV4:4:4` 每一个Y分量都有对应一组UV分量 `YUV4:2:2` 每两个Y分量共用一组UV分量 `YUV4:2:0` 每四个Y分量共用一组UV分量 ![img](https:////upload-images.jianshu.io/upload_images/7682685-0a3c6e7b455ae850.jpg?imageMogr2/auto-orient/strip|imageView2/2/w/930/format/webp) YUV采样示意图.jpg 上图我们可以看到: `YUV4:4:4`:也就是说每4个Y采样,就有相对应的4个U和4个V采样,即画面中每个像素都有Y分量和UV分量,这种格式储存了图像所有的亮度和色度信息。 `YUV4:2:2`:也就是说每4个Y采样,就有相对应的2个U和2个V采样。它的色度信号的扫描线(也就是上面图里的横线)和亮度信号一样多,但每条扫描线上的色度采样点只有亮度信号的一半(看4:2:2图一根横线上,空心圆只有实心圆的一半)。当4:2:2信号被解码的时候,“缺失”的色度采样通常由一定的内插补点算法通过它两侧的色度信息运算补充。 `YUV4:2:0`按字面意思应该4个Y采样,就有相对应的2个U和0个V采样,**事实上并不是这样的**,事实上,4:2:0的意思是,色度采样在每条横向扫描线上只有亮度采样的一半,扫描线的条数上,也只有亮度的一半!(看上面4:2:0这个图,空心是上下共用的,如果放到第一根线上,那就是空心只有实心的一半。再看前4根线,4根亮度线对应两行色度,也就是说空心的扫描线也只有实心的一半。)换句话说,无论是横向还是纵向, 色度信号的分辨率都只有亮度信号的一半。说得再通俗一些就是,如果第一行是4:2:0,那么第二行就是4:0:2,第三行就是4:2:0这样交替。举个例子,如果整张画面的尺寸是720x480,那么亮度信号是720x480,色度信号只有360x240。 ## 4、YUV的储存方式 YUV的储存格式有两大类: `planar`:先连续存储所有像素点的Y,紧接着存储所有像素点的U,随后是所有像素点的V。 `packed`:每个像素点的Y、U、V是连续交替存储的 ### 4.1 YUYV(YUY2)格式(属于YUV422) ![img](https:////upload-images.jianshu.io/upload_images/7682685-4e02972d7d289899.png?imageMogr2/auto-orient/strip|imageView2/2/w/623/format/webp) `YUYV`格式为像素保留Y,而UV在水平空间上相隔二个像素采样一次(Y0U0Y1V0),(Y2U2Y3V2)…其中,(Y0U0Y1V0)就是一个macro-pixel(宏像素),它表示了2个像素,(Y2U2Y3V2)是另外的2个像素。以此类推。 ### 4.2 YVYU、UYVY格式(属于YUV422) 跟YUY2类似,只不过是排列顺序不一样而已。 例如UYVY格式,那么排列顺序就是(U0Y0V0Y1)、(U2Y2V2Y3)... ### 4.3 YUV422P格式(属于YUV422) `YUV422P`也属于YUV422的一种,但它并不是将YUV数据交错存储,而是先存放所有的Y分量,然后存储所有的U(Cb)分量,最后存储所有的V(Cr)分量,其每一个像素点的YUV值提取方法也是遵循YUV422格式的最基本提取方法,即两个Y共用一个UV。 ### 4.4 YV12格式(属于YUV420) ![img](https:////upload-images.jianshu.io/upload_images/7682685-5642eaa12e43b6e3.png?imageMogr2/auto-orient/strip|imageView2/2/w/626/format/webp) `YV12`是Plane模式的,也就是Y、U、V三个分量分别打包,依次存储。例如2x2图像:YYYYVU,4*4图像:YYYYYYYYYYYYYYYYVVVVUUUU ### 4.5 NV12、NV21格式(属于YUV420) ![img](https:////upload-images.jianshu.io/upload_images/7682685-88887da62b28a2e8.png?imageMogr2/auto-orient/strip|imageView2/2/w/605/format/webp) `NV12`和`NV21`是一种two-plane模式,即Y和UV分为两个Plane,其中U和V是交错储存的。`NV12`是U在前,V在后,而`NV21`是V在前U在后。例如4x4图像: `NV12`:YYYYYYYYYYYYYYYYUVUVUVUV `NV21`:YYYYYYYYYYYYYYYYVUVUVUVU ### 4.6 I420格式(属于YUV420) I420是 planar 存储方式,分量存储顺序依次是 Y, Cb(U), Cr(V),例如4x4图像:YYYYYYYYYYYYYYYYUUUUVVVV 更多格式可参考:[YUV pixel formats](https://www.fourcc.org/yuv.php) ------ ## 5、YUV数据量计算 我们以`YUV444`、`YUV422`、`YUV420`这三种采样格式举例。我们知道,RGB图像三个分量都必须全部存储,例如一张4x4像素的RGB图像大小为4x4x3=48字节。 - `YUV444`为一个像素点有一个Y、一个U、一个V,所以一张4x4像素的`YUV444`图像大小为4x4x3=48字节,跟RGB一样大。Y、U、V三个分量的大小都是4x4=16字节 - `YUV422`为两个Y共用一个U和V,Y分量为全采样,即4x4=16字节,U分量和V分量只有Y分量的一半,即U分量为4x4/2=8字节,V分量也是4x4/2=8字节,也就是说一张4x4像素的`YUV422`图像大小为4x4x2=32字节。 - `YUV420`为4个Y共用一个U和V,Y分量为全采样,及4x4=16字节,U分量和V分量只有Y分量的四分之一,即U分量和V分量的大小均为4x4/4=4字节,也就是说一张4x4像素的`YUV420`图像大小为:4x4x(3/2)=24字节。 ------ ## 6、YUV裁剪 我们来运用一下上面所讲的关于YUV的知识,下面我们拿一张I420的YUV图像来对其进行裁剪。 上面说到,I420的排列方式就是先全部放Y,然后放U,然后放V,由于I420是4:2:0采样的,那么一幅YUV图像的总大小为4x4x(3/2)=24字节,前4x4字节存放全部的Y,从4x4到4x4x(5/4)存放U,最后从4x4x(5/4)到4x4x(3/2)存放V。 ### 6.1先从简单的入手 假如把一幅4x4的图像,将其左上角2x2的画面裁剪出来,应该怎么做呢?我们把YUV三个分量画成下面的图来分析一下: ![img](https:////upload-images.jianshu.io/upload_images/7682685-b4ae3aa84c35519c.png?imageMogr2/auto-orient/strip|imageView2/2/w/1200/format/webp) 如上图所示,首先Y分量每个像素点都有一个Y,那么总用就有16个Y,U和V水平和垂直都是Y的一半,也就是只有4个U和4个V,其中,Y1、Y2、Y5、Y6共用U1和V1,以此类推,那么我们要裁剪左上角2x2像素,不就是把Y1,Y2,Y5,Y6,U1,V1取出来,按I420的方式排列,就这么简单! 也就是说,原来4x4的I420图像数据为Y1Y2Y3Y4Y5Y6Y7Y8Y9Y10Y11Y12Y13Y14Y15Y16U1U2U3U4V1V2V3V4,我们裁出的左上角2x2图像数据为Y1Y2Y5Y6U1V1。就这么简单! ### 6.2 加强一下 我们用一张256x256大小的图片,来做裁剪,这次我们要裁中间的那部分:将256x256的图片裁出(x, y, w, h) = (64, 64, 128, 128)的那部分。大致就是下面这张图上的红框区域 ![img](https:////upload-images.jianshu.io/upload_images/7682685-a4bb15ca01b9eff0.jpg?imageMogr2/auto-orient/strip|imageView2/2/w/256/format/webp) 我们分几步走: - 读取源图像数据,记为sourceData - 开辟一块内存来放裁剪后的数据,记为destData - 读取sourceData的Y分量并提取要裁剪的Y分量数据写入destData - 读取sourceData的U分量并提取要裁剪的U分量数据写入destData - 读取sourceData的V分量并提取要裁剪的V分量数据写入destData - destData写入到文件 我们直接把I420裁剪过程封装成一个方法,代码加注释看看如何一步一步实现: ```c /** I420裁剪 @param sourceData 源图像数据 @param sourceW 源图像宽度 @param sourceH 源图像高度 @param destData 裁剪后图像数据 @param x 从源图像的x坐标开始裁剪 @param y 从源图像的y坐标开始裁剪 @param w 裁剪宽度 @param h 裁剪高度 */ void clipI420(const unsigned char *sourceData, const int sourceW, const int sourceH, unsigned char *destData, int x, int y, const int w, const int h) { // 拷贝一只指针出来使唤 const unsigned char *source = sourceData; // 把要裁剪的x和y都换成偶数,因为我们2行Y用一行U和一行V,奇数就难搞了 x = (int)(x + 1) / 2 * 2; y = (int)(y + 1) / 2 * 2; // 指针移动到要裁剪的Y数据的开头 source += y * sourceW + x; //总共要扫描要裁剪的高的行数,将里面在区域内的Y值提取出来 for (int i = 0; i < h; i++) { // 裁剪这一行的目标Y分量 memcpy(destData, source, w); // 移动指针到下一行 source += sourceW; destData += w; } //把源数据指针移到U分量的开头 source = sourceData + sourceW * sourceH; // 把源数据的指针移到要裁剪的U分量的地方,UV分量都是行列只有Y分量的一半,所以U分量的要裁剪的y坐标应该是在(y/2)*(sourceW/2)的地方,再加上UV分量的x坐标的偏移量 source += (y * sourceW / 4 + x / 2); // UV分量都是行列都只有Y分量的一半,所以指针的移动也是一半的步长 for (int i = 0; i < h / 2; i++) { memcpy(destData, source, w / 2); source += sourceW / 2; destData += w / 2; } //把源数据指针移到V分量的开头 source = sourceData + sourceW * sourceH * 5 / 4; // 以下跟U分量一个道理 source += (y * sourceW / 4 + x / 2); for (int i = 0; i < h / 2; i++) { memcpy(destData, source, w / 2); source += sourceW / 2; destData += w / 2; } } void clipLena() { int sourceW = 256; int sourceH = 256; int souceDataLength = sourceW * sourceH * 3 / 2; // 读取源图像 FILE *fp = fopen("lena_256x256_yuv420p.yuv", "rb+"); unsigned char *sourceData = (unsigned char *)malloc(souceDataLength); fread(sourceData, 1, souceDataLength, fp); fclose(fp); // 开辟储存裁剪后的图像的内存 int clipX = 64; int clipY = 64; int clipW = 128; int clipH = 128; int destDataLength = clipW * clipH * 3 / 2; unsigned char *destData = (unsigned char *)malloc(souceDataLength); // 进行裁剪 clipI420(sourceData, sourceW, sourceH, destData, clipX, clipY, clipW, clipH); // 裁剪后数据写入文件 FILE *fp1 = fopen("lena_64x64_yuv420p","wb+"); fwrite(destData, 1, destDataLength, fp1); fclose(fp1); free(sourceData); free(destData); } ``` 我自己在做这个的时候的一些疑问: **1、裁剪的x和y为什么得偶数?** 4:2:0这种格式不就是两行用的是同一个UV,两列用的也是同一个UV,例如第一行和第二行Y共用了第一行UV,针对这个256x256的图像来说,我们把数据分为三部分:|----Y----|-U-|-V-|,前两行像素(0, 0, 256, 2),取的是数据是Y部分的前256x2字节,U部分的前128字节,V部分的前128字节,也就是两行Y,一行U和一行V。如果我们只取一行像素(0, 0, 256, 1),那就没法玩了,因为这第一行的U和V跟第二行Y共享的。 **2、怎么计算UV分量数据从哪里开始取?** 还是拿4x4图像举例,4行4列有16个像素,也就是有16个Y,4个U和4个V。也就是数据最前面4x4=16字节是Y分量,U分量是接着2x2=4字节,V分量是最后2x2=4字节。UV的行列数都是Y分量的一半。回到256x256的图像,我们裁(64, 64, 128, 128)这一块,第64行Y的地方是第32行U,每行U是128字节,那就是U分量128x32,这也就是要裁剪的y坐标。一行U是128字节,那么在原图64列的位置U应该是在这一行的64/2=32的位置。这也就是source += (y * sourceW / 4 + x / 2);这一行的由来。 **3、换成其他格式怎么裁剪呢?** 例如NV21怎么裁呢?其实也就是根据数据储存格式和采样方式从源数据中取出我们想要的数据并按规则排列就可以实现了。 **4、还有哪些知识点没碰到?** 给YUV图像加水印、YUV旋转、翻转、YUV与RGB互转等等。留坑后面再补。。。 ## 7、总结 YUV其实没那么神秘,主要是理解采样比例和储存方式也就没什么太大的难度了。本篇没单独整理Demo,上面I420裁剪的代码是在Mac上写的,理论上Windows也可以跑。YUV文件大家可以在下面雷神的文章《[视音频数据处理入门:RGB、YUV像素数据处理》里头找到。 最近在学习研究OpenGL和Metal渲染相关的知识,下一篇来讲一下关于视频渲染相关的知识吧! ## 8、参考文献 [YUV pixel formats](https://www.fourcc.org/yuv.php) [视音频数据处理入门:RGB、YUV像素数据处理](https://blog.csdn.net/leixiaohua1020/article/details/50534150) [YUV格式详解](https://www.cnblogs.com/ALittleDust/p/5935983.html) [YUV和RGB格式分析](https://www.cnblogs.com/silence-hust/p/4465354.html) [YUV 数据格式完全解析](https://blog.piasy.com/2018/04/27/YUV/index.html) 原文链接:https://www.jianshu.com/p/d9631596e9c7 ================================================ FILE: iOS资料/iOS视频推流、拉流原理.md ================================================ # iOS视频推流、拉流原理 ## 1、HLS协议: 简单讲就是把整个流分成一个个小的,基于 HTTP 的文件来下载,每次只下载一些,前面提到了用于 H5 播放直播视频时引入的一个 .m3u8 的文件,这个文件就是基于 HLS 协议,存放视频流元数据的文件。 每一个 .m3u8 文件,分别对应若干个 ts 文件,这些 ts 文件才是真正存放视频的数据,m3u8 文件只是存放了一些 ts 文件的配置信息和相关路径,当视频播放时,.m3u8 是动态改变的,video 标签会解析这个文件,并找到对应的 ts 文件来播放,所以一般为了加快速度,.m3u8 放在 web 服务器上,ts 文件放在 cdn 上。 .m3u8 文件,其实就是以 UTF-8 编码的 m3u 文件,这个文件本身不能播放,只是存放了播放信息的文本文件: ``` #EXTM3U m3u文件头#EXT-X-MEDIA-SEQUENCE 第一个TS分片的序列号#EXT-X-TARGETDURATION 每个分片TS的最大的时长#EXT-X-ALLOW-CACHE 是否允许cache#EXT-X-ENDLIST m3u8文件结束符#EXTINF 指定每个媒体段(ts)的持续时间(秒),仅对其后面的URI有效mystream-12.ts ``` ## 2、HLS 的请求流程是: 1.http 请求 m3u8 的 url。 2.服务端返回一个 m3u8 的播放列表,这个播放列表是实时更新的,一般一次给出5段数据的 url。 3.客户端解析 m3u8 的播放列表,再按序请求每一段的 url,获取 ts 数据流。 ### 2.1 关于HLS延迟原因: hls 协议是将直播流分成一段一段的小段视频去下载播放的,所以假设列表里面的包含5个 ts 文件,每个 TS 文件包含5秒的视频内容,那么整体的延迟就是25秒。因为当你看到这些视频时,主播已经将视频录制好上传上去了,所以时这样产生的延迟。当然可以缩短列表的长度和单个 ts 文件的大小来降低延迟,极致来说可以缩减列表长度为1,并且 ts 的时长为1s,但是这样会造成请求次数增加,增大服务器压力,当网速慢时回造成更多的缓冲,所以苹果官方推荐的ts时长时10s,所以这样就会大改有30s的延迟; ### 2.2 数据采集原理: 下面将利用 ios 上的摄像头,进行音视频的数据采集,主要分为以下几个步骤: - 音视频的采集,ios 中,利用 AVCaptureSession和AVCaptureDevice 可以采集到原始的音视频数据流。 - 对视频进行 H264 编码,对音频进行 AAC 编码,在 ios 中分别有已经封装好的编码库来实现对音视频的编码。 - 对编码后的音、视频数据进行组装封包; - 建立 RTMP 连接并上推到服务端。 ps:由于编码库大多使用 c 语言编写,需要自己使用时编译,对于 ios,可以使用已经编译好的编码库。 ## 3、RTMP介绍: Real Time Messaging Protocol(简称 RTMP)是 Macromedia 开发的一套视频直播协议。和 HLS 一样都可以应用于视频直播,区别是 RTMP 基于 flash 无法在 ios 的浏览器里播放,但是实时性比 HLS 要好。所以一般使用这种协议来上传视频流,也就是视频流推送到服务器。 对比: - RTMP 首先就是延迟低,基于TCP的长链接,对于数据处理及时,收到即刻发送,推荐使用场景:即时互动。 - HLS 延迟高,短链接,原理是集合了一段时间的视频数据,切割ts片,逐个下载播放。优点是跨平台。 ### 3.1 推流: 推流,就是将我们已经编码好的音视频数据发往视频流服务器中,一般常用的是使用 rtmp 推流,可以使用第三方库 librtmp-iOS 进行推流,librtmp 封装了一些核心的 api 供使用者调用,如果觉得麻烦,可以使用现成的 ios 视频推流sdk,也是基于 rtmp 的。具体说下: 也就是对编码好的音视频数据推到服务器上,这里我们又分为两类推流模式:手机端推流,服务器本地推流。就拿我上一家公司的电视直播来说,视频源是来自电视台的,需要通过ffmpeg命令来进行个推流,那么推流协议的话这里又分为了:HLS推流和rtmp推流,这里的取舍主要涉及到了是否需要及其实时的直播问题,也就是延迟20 30s是否接受,当然电视直播并不是主播实时互动,所以不需要使用实时流媒体协议的rtmp,所以通过ffmpeg -loglevel 这么一个命令将电视台给的视频进行各像nginx服务器的一个推流,那么我们就可以通过nginx服务器给的链接,配合我的第三方的直播框架,就可以实现个直播,这个是服务器本地的HLS协议的一个推流。当然如果我们要做一个没有延迟的比如实现各主播互动的一个直播,那么就是iOS客户端用rtmp协议的一个往nginx服务器的一个推流了。在iOS设备上进行各推流的话,是通过AVCaptureSession这么一个捕捉会话,指定两个AVCaptureDevice 也就是iOS的摄像头和麦克风,获取个原始视频和音频,然后需要进行个H.264的视频编码和AAC的音频编码,再将编码后的数据整合成一个音视频包,通过rmtp推送到nginx服务器。这里这些步骤,我们可以通过各第三方集成好的推流工具进行推流,这个工具有librtmp,和腾讯的GDLiveStreaming进行个推流。 原文链接:https://mp.weixin.qq.com/s?src=11×tamp=1678859405&ver=4407&signature=kQhf-SSTbagt8wSRLirZVEAn4Z7le*p*PSZzTVEyG5DIHR0aY2ntdmVcLf9R*b2hcV04rJL07lwRtcW6hl9pNNHgl59TGPjSUu4My9Yo-gFiWJJUIoJlhv4i8Mll6W7e&new=1 ================================================ FILE: iOS资料/iOS逆向 MachO文件.md ================================================ # iOS逆向 MachO文件 ## 1、MachO初探 #### 1.1定义 `MachO`其实是`Mach Object`文件格式的缩写,是mac以及iOS上可执行文件的格式,类似于Windows上的PE格式(Portable Executable)、Linux上的elf格式(Executable and Linking Format) 它是一种用于可执行文件、目标代码、动态库的文件格式,作为.out格式的替代,MachO提供了更强的扩展性 #### 1.2常见的MachO文件 - 目标文件.o - 库文件 - .a - .dylib - .Framework - 可执行文件 - dyld(动态链接器) - .dsym(符号表:Relese环境运行生成) #### 1.3查看文件类型 ``` $ file xxx.xx ``` ![img](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2019/12/14/16f03b938a746084~tplv-t2oaga2asx-zoom-in-crop-mark:4536:0:0:0.image) ## 2、关于架构 #### 2.1架构表 其实iPhone不同的型号对应的架构是不一样的 | 架构 | 手机型号 | | ------ | ----------------------------------------------- | | i386 | 32位模拟器 | | x86_64 | 64位模拟器 | | armv7 | iPhone4、iPhone4S | | armv7s | iPhone5、iPhone5C | | arm64 | iPhone5s——iPhoneX | | arm64e | iPhone XS、iPhone XS Max、iPhoneXR、iPhone11... | #### 2.2生成多种架构 新建一个工程,真机运行,查看可执行文件仅仅是一个arm64架构的 ![img](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2019/12/15/16f07f9f9126dda9~tplv-t2oaga2asx-zoom-in-crop-mark:4536:0:0:0.image) 将项目最低适配系统调为iOS9.0,真机运行`Relese环境` ![img](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2019/12/15/16f08008f3d55aab~tplv-t2oaga2asx-zoom-in-crop-mark:4536:0:0:0.image) **为什么要改为iOS9.0呢**?是因为iPhone5c等armv7、armv7s架构不支持iOS11.0 **为什么要Relese环境运行呢**?因为Xcode默认Debug只生成单一架构 ![img](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2019/12/15/16f080382e437831~tplv-t2oaga2asx-zoom-in-crop-mark:4536:0:0:0.image) **怎么生成所有架构**?Xcode10中只包含了v7和64,需要在`Architectures`中添加 ![img](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2019/12/15/16f080ececc0f693~tplv-t2oaga2asx-zoom-in-crop-mark:4536:0:0:0.image) ![img](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2019/12/15/16f080d661eb4112~tplv-t2oaga2asx-zoom-in-crop-mark:4536:0:0:0.image) ## 3、通用二进制文件 #### 3.1定义 通用二进制文件(Universal binary)也被叫做`胖二进制(Fat binary)` - 苹果公司提出的一种程序代码,能同时适用多种架构的二进制文件 - 同一个程序包中同时为多种架构提供最理想的性能 - 因为需要储存多种代码,通用二进制应用程序通常比单一平台二进制的程序要大 - 但是由于两种架构有共通的非执行资源,所以并不会达到单一版本的两倍之多 - 而且由于执行中只调用一部分代码,运行起来也不需要额外的内存 #### 3.2拆分/合并架构 架构拆分 ![img](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2019/12/15/16f08240102dc4d4~tplv-t2oaga2asx-zoom-in-crop-mark:4536:0:0:0.image) 合并架构 ![img](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2019/12/15/16f082abf93c1aab~tplv-t2oaga2asx-zoom-in-crop-mark:4536:0:0:0.image) `通用二进制`大小为342kb,四个架构大小为80+80+80+81=321kb What!为什么不是单纯的1+1=2? 因为不同架构之间代码部分是不共用的 (因为代码的二进制文件不同的组合在不同的 cpu 上可能会是不同的意义),而公共资源文件是公用的 > 利用上述方法可以给我们的app瘦身 **结论:** ①`胖二进制`拆分后再重组会得到原始`胖二进制` ②`通用二进制`的大小可能大于子架构大小之和,也可能小于,也可能等于,取决于`公共资源文件`的多少 #### 3.3终端命令行 ```scss // 查看二进制文件 $ lipo -info xx // 通用二进制文件 // 拆分二进制文件 lipo xxx -thin armv7 -output xxx // 组合二进制文件 lipo -create x1 x2 x3 x4 -output xxx 复制代码 ``` ## 4、MachO文件 #### 4.1整体结构 用`MachOView`打开会看到`通用二进制文件`由`Fat Header`和`四个可执行文件`组成 ![img](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2019/12/15/16f084564833fe48~tplv-t2oaga2asx-zoom-in-crop-mark:4536:0:0:0.image) `可执行文件`是由`Header`、`Load commands`和`Data`组成 ![img](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2019/12/15/16f0845ec5968b12~tplv-t2oaga2asx-zoom-in-crop-mark:4536:0:0:0.image) 我们可以这么理解,把`通用二进制文件`看作四本翻译语言不同的书,每本书有`标题(header)`、`目录(load commands)`、`内容(data)` - header: - load commands: - data: 另外我们也可以通过`otool`命令行查看MachO文件结构 ```ruby $ otool -f universe ``` ![img](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2019/12/15/16f08502c785de7b~tplv-t2oaga2asx-zoom-in-crop-mark:4536:0:0:0.image) #### 4.2header `header`包含了该二进制文件的字节顺序、架构类型、加载指令的数量等,使得可以快速确认一些信息,比如当前文件用于`32 位`还是`64 位`,对应的处理器是什么、文件类型是什么 Xcode中 `shift+command+O`->`load.h`->如下信息 ```arduino struct mach_header_64 { uint32_t magic; /* 魔数,快速定位64位/32位 */ cpu_type_t cputype; /* cpu 类型 比如 ARM */ cpu_subtype_t cpusubtype; /* cpu 具体类型 比如arm64 , armv7 */ uint32_t filetype; /* 文件类型 例如可执行文件 .. */ uint32_t ncmds; /* load commands 加载命令条数 */ uint32_t sizeofcmds; /* load commands 加载命令大小*/ uint32_t flags; /* 标志位标识二进制文件支持的功能 , 主要是和系统加载、链接有关*/ uint32_t reserved; /* reserved , 保留字段 */ }; ``` > mach_header_64(64位)对比mach_header(32位)只多了一个保留字段 #### 4.3load commands `load commands`是一张包括区域的位置、符号表、动态符号表等内容的表。 它详细保存着加载指令的内容,告诉链接器如何去加载这个 Mach-O 文件。 通过查看内存地址我们发现,在内存中`load commands`是紧跟在`header`之后的 | 名称 | 内容 | | --------------------- | ---------------------------------------------- | | LC_SEGMENT_64 | 将文件中(32位或64位)的段映射到进程地址空间中 | | LC_DYLD_INFO_ONLY | 动态链接相关信息 | | LC_SYMTAB | 符号地址 | | LC_DYSYMTAB | 动态链接相关信息 | | LC_LOAD_DYLINKER | 动态链接相关信息 | | LC_UUID | 动态链接相关信息 | | LC_VERSION_MIN_MACOSX | 支持最低的操作系统版本 | | LC_SOURCE_VERSION | 源代码版本 | | LC_MAIN | 设置程序主线程的入口地址和栈大小 | | LC_LOAD_DYLIB | 依赖库的路径,包含三方库 | | LC_FUNCTION_STARTS | 函数起始地址表 | | LC_CODE_SIGNATURE | 代码签名 | #### 4.4data `data`是MachO文件中最大的部分,其中`_TEXT段`、`_DATA段`能给到很多信息 `load commands`和`data`之间还留有不少空间,给我们留下了注入代码的冲破口 ![img](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2019/12/15/16f0949ed460a46a~tplv-t2oaga2asx-zoom-in-crop-mark:4536:0:0:0.image) **_TEXT段** | 名称 | 作用 | | -------------------- | -------------- | | _text | 主程序代码 | | _stubs、_stub_helper | 动态链接 | | _objc_methodname | 方法名称 | | _objc_classname | 类名称 | | _objc_methtype | 方法类型(v@:) | | _cstring | 静态字符串常量 | **_DATA段** | 名称 | 作用 | | ------------------------------------ | -------------- | | _got=>Non-Lazy Symbol Pointers | 非懒加载符号表 | | _la_symbol_ptr=>Lazy Symbol Pointers | 懒加载符号表 | | _objc_classlist | 方法名称 | | ... | ... | ## 5、dyld dyld(the dynamic link editor)是苹果的动态链接器,是苹果操作系统的一个重要组成部分,在系统内容做好程序准备工作之后,交由dyld负责余下的工作 系统库的方法由于是公用的,存放在共享缓存中,那么我们的MachO在调用系统方法时,dyld会将MachO里调用存放在共享缓存中的方法进行符号绑定。这个符号在`release环境` 是会被自动去掉的,这也是我们经常使用收集 bug 工具时需要恢复符号表的原因 原文链接:https://juejin.cn/post/6844904021010939918 ================================================ FILE: iOS资料/iOS配置FFmpeg框架.md ================================================ # iOS配置FFmpeg框架 一、下载一个Perl写的脚本文件,该脚本在执行编译脚本的时候需要依赖。https://github.com/libav/gas-preprocessor 解压下载好的文件,里面有一个 gas-preprocessor.pl 脚本文件,需要将该文件copy到 /usr/bin 目录下。终端执行: sudo cp -f /Users/lotheve/Desktop/gas-preprocessor-master/gas-preprocessor.pl /usr/bin 第一个路径为本机中gas-preprocessor.pl所在路径,请自行修改。 注意:Capitan系统开始,苹果在EI Capitan系统中加入了Rootless机制,即使root权限下也无法对 /usr/bin 目录进行读写。解决办法见 http://www.jianshu.com/p/22b89f19afd6 确认文件已经copy到 /usr/bin 之后,修改该文件的权限为可读可写可执行: chmod a+rwx gas-preprocessor.pl 二、下载脚本文件 https://github.com/kewlbear/FFmpeg-iOS-build-script 解压下载好的文件,里面有一个 build-ffmpeg.sh 文件,这个就是我们要用的编译脚本文件,打开可以查看编译配置信息。实际使用FFmpeg时我们往往只需要库中的部分功能,例如实现播放器,仅仅需要FFmpeg的解码功能,如果将整个库都编译,编译得到的静态库体积非常大,造成不必要的浪费。因此往往只对相关的模块进行编译,而就需要在配置文件里做一些设置,具体将在后面跟进。 执行编译。进入 build-ffmpeg.sh 文件所在的目录,执行脚本文件。在编译的时候会用到yasm汇编器,如果你的环境没有安装yasm,终端会给出提示。如果你安装了Homebrew包管理工具,那么在执行编译的时候会自动安装yasm(当然也可以事先用Homebrew安装: brew install ffmpeg)。 等必要环境准备完毕之后,终端开始下载FFmpeg库,之后开始编译,编译结束之前全程无需干预,可以来杯咖啡压压惊。要是不出意外,一杯咖啡的工夫差不多就编译完毕了。然而我一开始在编译的时候采到一个坑,每次编译到armv7的时候,总是提示 ffmpeg GNU assembler not found, install/update gas-preprocessor报错。检查该支持库的位置,已经在 /usr/bin 中乖乖呆着了,权限也已经设置好,这弄得我一头雾水。折腾了好久最后在网上翻别人博客,才知道自己采了个坑。原因在于之前下载的 gas-preprocessor.sh 版本太老了!我一看last commit 4 years ago!关键是该项目有近200的星,也一直没更新,太坑了。最后换了一个较新的版本,顺利编译。 看一下编译完毕后文件夹的内容: ffmpeg-3.0:FFmpeg库的源码,当前版本为3.0 FFmpeg-iOS:编译好的iOS下使用的FFmpeg Library库,费尽周折全为它!里面的include文件夹里是相关接口,lib是编译出来的几个静态库。可以用如下命令查看指定静态库的支持架构,发现均支持armv7 i386 x86_64 arm64。 lipo -info libavcodec.a 三、将FFmpeg-iOS库导入工程 直接将FFmpeg-iOS拖到工程里,在一个m文件中 #include "avformat.h" 引入该头文件测试,添加一条执行语句 av_register_all();。 设置头文件搜索路径 在 Header Search Paths 中添加路径 $(PROJECT_DIR)/FFmpeg-iOS/include: 引入依赖库 libiconv.tbd libbz2.tbd libz.tbd 本以为完事具备,运行一看,报错如下: 网上一查,还需要添加几个系统库: CoreMedia.framework VideoToolbox.framework 再次运行,Build Succeeded! 原文链接:https://blog.csdn.net/lotheve/article/details/51517875 ================================================ FILE: iOS资料/iOS音视频 -- AVFoundation捕捉.md ================================================ # iOS音视频 -- AVFoundation捕捉 ## 1、视频捕捉 ### **1.1、实现QuickTime视频的录制** 在上文中,简述了通过AVCapturePhotoOutput、AVCapturePhotoSettings来实现代理,获取当前摄像头所捕捉到的photo数据,生成一张图片。 视频录制过程大致也是如此,通过AVCaptureMovieFileOutput来获取视频数据,大致流程如下: \1. 开启录制之前需要判断当前是否处于录制状态,只有在非录制状态才能进入录制状态 ```text /// 是否在录制状态 - (BOOL)isRecording { return self.movieOutput.isRecording; } ``` \2. 通过AVCaptureConnection来获取当前视频捕捉的连接信息 \1. 调整视频方向 \2. 判断是否支持视频稳定功能(非必要) \3. 判读是否支持平滑对焦(非必要) \4. 为视频输出配置输出路径 \5. 开始视频recording ```text /// 开始录制 - (void)startRecording { if (![self isRecording]) { //获取当前视频捕捉连接信息 AVCaptureConnection *videoConnection = [self.movieOutput connectionWithMediaType:AVMediaTypeVideo]; //调整方向 if ([videoConnection isVideoOrientationSupported]) { videoConnection.videoOrientation = [self currentVideoOrientation]; } //判断是否支持视频稳定功能(保证视频质量) if ([videoConnection isVideoStabilizationSupported]) { videoConnection.preferredVideoStabilizationMode = YES; } //拿到活跃的摄像头 AVCaptureDevice *device = [self activeCamera]; //判断是否支持平滑对焦(当用户移动设备时, 能自动且快速的对焦) if (device.isSmoothAutoFocusEnabled) { NSError *error; if ([device lockForConfiguration:&error]) { device.smoothAutoFocusEnabled = YES; [device unlockForConfiguration]; } else { //失败回调 } } //获取路径 self.outputURL = [self uniqueURL]; //摄像头的相关配置完成, 也获取到路径, 开始录制(这里录制QuckTime视频文件, 保存到相册) [self.movieOutput startRecordingToOutputFileURL:self.outputURL recordingDelegate:self]; } } ``` \3. 停止视频recording ```text /// 停止录制 - (void)stopRecording { if ([self isRecording]) { [self.movieOutput stopRecording]; } } ///路径转换 - (NSURL *)uniqueURL { NSURL *url = [NSURL fileURLWithPath:[NSString stringWithFormat:@"%@%@", NSTemporaryDirectory(), @"output.mov"]]; return url; } ///获取方向值 - (AVCaptureVideoOrientation)currentVideoOrientation { AVCaptureVideoOrientation result; UIDeviceOrientation deviceOrientation = [UIDevice currentDevice].orientation; switch (deviceOrientation) { case UIDeviceOrientationPortrait: case UIDeviceOrientationFaceUp: case UIDeviceOrientationFaceDown: result = AVCaptureVideoOrientationPortrait; break; case UIDeviceOrientationPortraitUpsideDown: //如果这里设置成AVCaptureVideoOrientationPortraitUpsideDown,则视频方向和拍摄时的方向是相反的。 result = AVCaptureVideoOrientationPortrait; break; case UIDeviceOrientationLandscapeLeft: result = AVCaptureVideoOrientationLandscapeRight; break; case UIDeviceOrientationLandscapeRight: result = AVCaptureVideoOrientationLandscapeLeft; break; default: result = AVCaptureVideoOrientationPortrait; break; } return result; } ``` \4. 保存影片至相册 ```text ///通过代理来获取视频数据#pragma mark - AVCaptureFileOutputRecordingDelegate - (void)captureOutput:(AVCaptureFileOutput *)captureOutput didFinishRecordingToOutputFileAtURL:(NSURL *)outputFileURL fromConnections:(NSArray *)connections error:(NSError *)error { if (error) { //错误回调 } else { //视频写入到相册 [self writeVideoToAssetsLibrary:[self.outputURL copy]]; } self.outputURL = nil; } //写入捕捉到的视频 - (void)writeVideoToAssetsLibrary:(NSURL *)videoURL { __block PHObjectPlaceholder *assetPlaceholder = nil; [[PHPhotoLibrary sharedPhotoLibrary] performChanges:^{ //保存进相册 PHAssetChangeRequest *changeRequest = [PHAssetChangeRequest creationRequestForAssetFromVideoAtFileURL:videoURL]; assetPlaceholder = changeRequest.placeholderForCreatedAsset; } completionHandler:^(BOOL success, NSError * _Nullable error) { NSLog(@"OK"); //保存成功 dispatch_async(dispatch_get_main_queue(), ^{ //通知外部一个略缩图 [self generateThumbnailForVideoAtURL:videoURL]; }); }]; } ``` \5. 生成一个略缩图通知外部 ```text ///通过视频获取视频的第一帧图片当做略缩图 - (void)generateThumbnailForVideoAtURL:(NSURL *)videoURL { dispatch_async(self.videoQueue, ^{ //拿到视频信息 AVAsset *asset = [AVAsset assetWithURL:videoURL]; AVAssetImageGenerator *imageGenerator = [AVAssetImageGenerator assetImageGeneratorWithAsset:asset]; imageGenerator.maximumSize = CGSizeMake(100, 0); imageGenerator.appliesPreferredTrackTransform = YES; //通过视频将第一帧图片数据转化为CGImage CGImageRef imageRef = [imageGenerator copyCGImageAtTime:kCMTimeZero actualTime:NULL error:nil]; UIImage *image = [UIImage imageWithCGImage:imageRef]; //通知外部 NSNotificationCenter *nc = [NSNotificationCenter defaultCenter]; [nc postNotificationName:ThumbnailCreatedNotification object:image]; }); } ``` ### **1.2、关于QuickTime** 上文主要是通过AVCaptureMovieFileOutput将QuickTime影片捕捉到磁盘,这个类大多数核心功能继承与超类AVCaptureFileOutput。它有很多实用的功能,例如:录制到最长时限或录制到特定文件大小为止。 通常当QuickTime影片准备发布时,影片头的元数据处于文件的开始位置。这样可以让视频播放器快速读取头包含信息,来确定文件的内容、结构和其包含的多个样本的位置。当录制一个QuickTime影片时,直到所有的样片都完成捕捉后才能创建信息头。当录制结束时,创建头数据并将它附在文件结尾。 ![img](https://pic2.zhimg.com/80/v2-02766b0c53845574a1dbe90928e475e1_720w.webp) 将创建头的过程放在所有影片样本完成捕捉之后存在一个问题。 在移动设备中,比如录制的时候接到电话或者程序崩溃等问题,影片头就不能被正确写入。会在磁盘生成一个不可读的影片文件。AVCaptureMovieFileOutput提供一个核心功能就是分段捕捉QuickTime影片。 ![img](https://pic2.zhimg.com/80/v2-6c28625c289244f30bd03bf76b185be5_720w.webp) ## 2、AVFoundation的人脸识别 人脸识别实际上是非常复杂的一个功能,要想自己完全实现人脸识别是非常困难的。苹果为我们做了很多人脸识别的功能,例如CoreImage、AVFoundation,都是有人脸识别的功能的。还有Vision face++ 等。这里就简单介绍一下AVFoundation中的人脸识别。 在拍摄视频中,我们通过AVFoundation的人脸识别,在屏幕界面上用一个红色矩形来标识识别到的人脸。 ### **2.1、人脸识别流程** \1. 使用AVCaptureMetadataOutput来建立输出 \1. 添加进session \2. 设置获取数据类型 \3. 在主线程中执行任务 ```text - (BOOL)setupSessionOutputs:(NSError **)error { //配置输入信息 self.metadataOutput = [[AVCaptureMetadataOutput alloc] init]; //对session添加输出 if ([self.captureSession canAddOutput:self.metadataOutput]) { [self.captureSession addOutput:self.metadataOutput]; //从输出数据中设置只获取人脸数据(可以是人脸、二维码、一维码....) NSArray *metadataObjectType = @[AVMetadataObjectTypeFace]; self.metadataOutput.metadataObjectTypes = metadataObjectType; //因为人脸检测使用了硬件加速器GPU, 所以它的任务需要在主线程中执行 dispatch_queue_t mainQueue = dispatch_get_main_queue(); //设置metadataOutput代理方法, 检测视频中一帧一帧数据里是否包含人脸数据. 如果包含则调用回调方法 [self.metadataOutput setMetadataObjectsDelegate:self queue:mainQueue]; return YES; } else { //错误回调 } return NO; } ``` \2. 实现相关代理方法,将捕捉到的人脸数据传递给layer层 ```text - (void)captureOutput:(AVCaptureOutput *)output didOutputMetadataObjects:(NSArray<__kindof AVMetadataObject *> *)metadataObjects fromConnection:(AVCaptureConnection *)connection { //metadataObjects包含了捕获到的人脸数据(人脸数据会重复, 会一直捕获人脸数据) for (AVMetadataFaceObject *face in metadataObjects) { NSLog(@"Face ID:%li",(long)face.faceID); } //将人脸数据通过代理发送给外部的layer层 [self.faceDetectionDelegate didDetectFaces:metadataObjects]; } ``` \3. 配置相关显示的图层。在layer层的视图中配置图层,我们在人脸四周添加一个矩形是在这个AVCaptureVideoPreviewLayer上进行一个个添加矩形。(因为人脸在识别过程中会出现旋转抖动等,需要进行一些3D转换等操作,后续也会出现此类操作,不在此篇作过多讲解) ```text - (void)setupView { //用来记录人脸图层 self.faceLayers = [NSMutableDictionary dictionary]; //图层的填充方式: 设置videoGravity 使用AVLayerVideoGravityResizeAspectFill 铺满整个预览层的边界范围 self.previewLayer.videoGravity = AVLayerVideoGravityResizeAspectFill; //在previewLayer上添加一个透明的图层 self.overlayLayer = [CALayer layer]; self.overlayLayer.frame = self.bounds; //假设你的图层上的图形会发生3D变换, 设置投影方式 self.overlayLayer.sublayerTransform = CATransform3DMakePerspective(1000); [self.previewLayer addSublayer:self.overlayLayer]; } static CATransform3D CATransform3DMakePerspective(CGFloat eyePosition) { //CATransform3D 图层的旋转,缩放,偏移,歪斜和应用的透 //CATransform3DIdentity是单位矩阵,该矩阵没有缩放,旋转,歪斜,透视。该矩阵应用到图层上,就是设置默认值。 CATransform3D transform = CATransform3DIdentity; //透视效果(就是近大远小),是通过设置m34 m34 = -1.0/D 默认是0.D越小透视效果越明显 //D:eyePosition 观察者到投射面的距离 transform.m34 = -1.0/eyePosition; return transform; } ``` \4. 处理通过代理传递过来的人脸数据 \1. 将人脸在摄像头中的坐标转化为屏幕坐标 \2. 定义一个数组,保存所有的人脸数据,用于存放待从屏幕上删除的人脸数据 \3. 遍历人脸数据 \1. 通过对比屏幕上的layer(框框)数量来与传递过来的人脸进行对比,判断是否需要移除layer(框框) \2. 根据人脸数据的ID来从屏幕上的layer(框框)中查找是否已经存在,不存在则需要生成一个layer(框框),并更新屏幕的layer(框框)数组。 \3. 根据传递过来的人脸数据来设置layer(框框)的位置,注意:当最后一个人脸离开屏幕,此时代理方法不会调用,会导致最后一个layer(框框)仍停留在屏幕上,所以需要处理一下人脸将要离开屏幕就对其进行移除处理。 \4. 在捕捉过程中,人脸会左右前后摆动(即z、y轴变化),来做不同的处理 \4. 遍历一下待删除数组,将之与传递过来的人脸数据进行对比,删除多余的人脸数据 注意:此处省略了一些3D转换的方法 ```text - (void)didDetectFaces:(NSArray *)faces { //人脸数据位置信息(摄像头坐标系)转换为屏幕坐标系 NSArray *transfromedFaces = [self transformedFacesFromFaces:faces]; //人脸消失, 删除图层 //需要删除的人脸数据列表 NSMutableArray *lostFaces = [self.faceLayers.allValues mutableCopy]; //遍历每个人脸数据 for (AVMetadataFaceObject *face in transfromedFaces) { //face ID NSNumber *faceID = @(face.faceID); //face ID存在即不需要删除(从删除列表中移除) [lostFaces removeObject:faceID]; //假如有新的人脸加入 CALayer *layer = self.faceLayers[faceID]; if (!layer) { NSLog(@"新增人脸"); layer = [self makeFaceLayer]; [self.overlayLayer addSublayer:layer]; //更新字典 self.faceLayers[faceID] = layer; } //根据人脸的bounds设置layer的frame layer.frame = face.bounds; CGSize size = self.bounds.size; //当人脸特别靠近屏幕边缘, 直接当作无法识别此人脸(因为人脸离开屏幕不会走此代理方法, 需要提前做移除) if (face.bounds.origin.x < 3 || face.bounds.origin.x > size.width - layer.frame.size.width - 3 || face.bounds.origin.y < 3 || face.bounds.origin.y > size.height - layer.frame.size.height - 3 ) { [layer removeFromSuperlayer]; [self.faceLayers removeObjectForKey:faceID]; } //设置3D属性(人脸是3D的, 需要根据人脸的3D变化做不同的变化处理) layer.transform = CATransform3DIdentity; //人脸z轴变化 if (face.hasRollAngle) { CATransform3D t = [self transformForRollAngle:face.rollAngle]; //矩阵相乘 layer.transform = CATransform3DConcat(layer.transform, t); } //人脸y轴变化 if (face.hasYawAngle) { CATransform3D t = [self transformForYawAngle:face.hasYawAngle]; //矩阵相乘 layer.transform = CATransform3DConcat(layer.transform, t); } } //处理已经从镜头消失的人脸(人脸消失,图层并没有消失) for (NSNumber *faceID in lostFaces) { CALayer *layer = self.faceLayers[faceID]; [self.faceLayers removeObjectForKey:faceID]; [layer removeFromSuperlayer]; } } ``` ### **2.1、其他类型数据的识别** 有的同学在设置AVMetadataObjectTypeFace的可能会发现,还有会有一些其他的类型,例如AVMetadataObjectTypeQRCode等,就是从摄像头中捕获二维码数据,它的流程与人脸识别极度相似,甚至要更为简单一些,因为二维码并不像人脸一样需要做一些3D的转换等操作,所以此处不再示例捕捉二维码。 原文https://zhuanlan.zhihu.com/p/222418988 ================================================ FILE: iOS资料/iOS音视频同步探讨.md ================================================ # iOS音视频同步探讨 ## 1.音视频同步的原理 音视频采集的数据分别来自于麦克风与摄像头,而摄像头与麦克风其实是两个独立的硬件,而音视频同步的原理是相信摄像头与麦克风采集数据是实时的,并在采集到数据时给他们一个时间戳来标明数据所属的时间,而编码封装模块只要不改动音视频时间的相对关系就能保证音频与视频在时间上的对应。如此封装好数据之后,播放端就能够根据音视频的时间戳来播放对应的音视频,从实现音视频同步的效果。 ## 2.时间戳参考标准 - 取格林威治时间做为对比标准,即音视频时间戳都为采集时间点相对于格林威治标准时间的时间差 - 取系统开机时间做为对比标准,即音视频时间戳都是采集时间点相对于手机开机时间的时间差。目前iOS上AVCaptureSession这套API就是参考这个时间标准给的时间戳 - 其它时间戳标准 ## 3.基于“开源项目1”的音视频同步探讨 - 原生某开源框架 - - 如图: ![img](https://pic2.zhimg.com/80/v2-739435aa0160ba1a3cfa530b6d9f1e55_720w.webp) - - 简介 - - 音/视频被采集到之后会先经过音/视频处理模块,音/视频在被处理之后才进入计算时间戳的模块 - 在第一帧到达时记一个计时起点,然后根据采集的帧间隔对接下来每一帧的时间戳进行计算:frameTimeStamp = lastFrameTimeStamp + frameDuration - 优点 - - 能输出**frame duration**稳定的音视频时间戳 - 风险 - - 无论是音频还是视频,在手机过热、性能不足等极端情况下有可能出现采集不稳定的情况,比如说预计1s采集30帧,实际只采集到28帧,而音视频的时间戳是通过累加来计算的,这样就有会出现音视频不同步的情况 - **Video Process**(人脸检测、滤镜、3D贴纸)有可能无法在一帧时间内处理完当前帧,这样就会出现帧数比预期低的情况,从而出现音视频不同步 - 帧间隔涉及到无限小数时,因为计算机的精度有限会引发的时间戳偏移,此偏移会随着帧数的增加而逐渐被放大 - 基于**开源项目1**的改进方案1 - - 如图: ![img](https://pic3.zhimg.com/80/v2-2e35a9575eb5093a27c8e6ec77975072_720w.webp) - 简介 - - - 音/视频被采集到之后会先经过音/视频处理模块,音/视频在被处理之后才进入计算时间戳的模块 - 时间戳的获取方法非常直接——每一帧都在改帧进入时间戳计算模块时获取当前系统时间作为时间戳 - 优点: - - APP性能正常的情况下肯定不会出现音视频不同步 - 能够实时纠正时间戳,只要APP正常运转,就能立即恢复正确的时间戳 - 风险: - - 依赖**Video Process**与**Audio Process**模块处理时长相近,而实际工程中因为人脸检测、贴纸等原因,**Video Process**可能会出现阻塞的情况,从而导致临时性的音视频不同步 - 在**Audio Process**与**Video Process**模块处理帧耗时不均匀的情况下会出现音视频时间戳不均匀的问题,能否正常播放依赖于终端 - 基于**开源项目1**的一个改进方案2 - - 如图: ![img](https://pic1.zhimg.com/80/v2-7c63822aec44096e639ccf6666b180bc_720w.webp) - 简介 - - - 音/视频被采集到之后,先获取采集模块提供的音视频时间戳,然后在音/视频处理模块透传采集模块获取到的音/视频时间戳,在时间戳计算模块继续透传采集模块给的时间戳 - 优点: - - 除非采集模块给出错误数据,否则音视频都一定是同步的 - 风险: - - 可能会出现音视频时间戳不均匀的情况,尤其是在手机过热、性能不足等极端情况下 - 直播方向更进一步的优化探讨 - - 大致流程如图: ![img](https://pic1.zhimg.com/80/v2-ab7607b8c5cc174d48efca60fed39f30_720w.webp) - 简介 - - - - 音/视频被采集到之后,先获取采集模块提供的音视频时间戳,然后在音/视频处理模块透传采集模块获取到的音/视频时间戳 - 在时间戳计算模块透传视频时间戳,并根据下文中提到的方法计算音频时间戳 - 音频时间戳计算方法 - - 实时计算时间戳:当前时间戳=起始时间戳+帧数*帧采样数/采样率 - 如果时间戳偏移量超出阈值,纠正时间戳 - 纠正频率达到超出阈值,直接透传采集时间戳 - 优点: - - 能够提供一个稳定的音频时间戳,可以兼容帧间隔小幅抖动造成的音频时间戳不均匀 - 兼容性能不足时导致实际采集帧数低于帧率 - 风险 - - 纠正时间戳时可能会造成声音卡顿的感觉 - 总结 - - 具体方案最好是针对实际应用场景有选择性的做优化,比如说,在可以控制播放器策略的情况,可以考虑根据自研播放器特性做时间戳处理。而如果播放器不可控,则尽量通过策略保障帧间隔稳定。 原文https://zhuanlan.zhihu.com/p/28557179 ================================================ FILE: iOS资料/iOS音视频开发-了解编码及视频.md ================================================ # iOS音视频开发-了解编码及视频 ## 1. 视频H264编码 ### 1.1 为何编码? 从存储角度和网络传输以及通用性 3个角度,压缩已经成了不可或缺的动作.压缩编码最基本的指标,就是压缩比. 压缩比通常都是小于1(如果等于或者大于1,是不是就失去了编码的意义了.编码的目的就是为了压缩数据体量). ### 1.2 何为编码? 编码就是按照一定的格式记录采样和量化后的数据. #### 1.2.1编码中软编码和硬编码的区别? - 硬编码: 使用非CPU进行编码,例如使用GPU芯片处理 - 软编码: 使用CPU来进行编码计算. #### 1.2.2 软编码与硬编码的区分? - 软编码: 实现直接、简单,参数调整方便,升级易,但CPU负载重,性能较硬编码低,低码率下质量通常比硬编码要好一点。 - 硬编码:性能高,低码率下通常质量低于硬编码器,但部分产品在GPU硬件平台移植了优秀的软编码算法(如X264)的,质量基本等同于软编码。 - 硬编码,就是使用GPU计算,获取数据结果,优点速度快,效率高.\ - 软编码,就是通过CPU来计算,获取数据结果. #### 1.2.3 压缩算法 **压缩算法分为2种,有损压缩与无损压缩.** - 无损压缩:解压后的数据可以完全复原,在常用的压缩格式中,无损压缩使用频次较低 - 有损压缩:解压后数据不能完全复原,会丢失一部分信息.压缩比越小,丢失的信息就会越多.信号还原的失真就会越大. **需要根据不同的场景(考虑因素包括存储设备,传输网络环境,播放设备等)选用不同的压缩编码算法.** ### 1.3 VideoToolBox 硬编码 VideoToolbox 是一套纯C语言API。其中包含了很多C语言函数 VideoToolBox实际上属于低级框架,它是可以直接访问硬件编码器和解码器.它存在于视频压缩和解压缩以及存储在像素缓存区中的数据转换提供服务. **硬编码的优点:提高性能、增加效率、延长电量的使用** ## 2.了解视频 ![img](https://pic1.zhimg.com/80/v2-fd236dc3d800828e9d6fd5c48dba472c_720w.webp) ### 2.1视频的构成: - 图像 - 音频 - 元信息 **图像:** 视频内容本身就是一帧一帧的图片构成.人眼只要1秒钟连续播放16张以上的图片,就会认为这是一段连贯的视频.这种物理现象叫视觉暂留. **音频:** 视频一定是由音频+图像内容构成的.所以音频在视频中是单独的一个部分.针对这一块我们需要单独编码的. **元信息:** 元信息其实就是描述信息的信息.用于描述信息的结构\语义\用途\用法等.比如视频元信息就包含了视频的具体信息,比如编码格式,分辨率等等. ### 2.2视频中的编码格式 - **视频编码格式** - - H264编码的优势: - - 低码率 - 高质量的图像 - 容错能力强 - 网络适应性强 - **总结:** H264最大的优势,具有很高的数据压缩比率,在同等图像质量下,H264的压缩比是MPEG-2的2倍以上,MPEG-4的1.5~2倍. - **举例:** 原始文件的大小如果为88GB,采用MPEG-2压缩标准压缩后变成3.5GB,压缩比为25∶1,而采用H.264压缩标准压缩后变为879MB,从88GB到879MB,H.264的压缩比达到惊人的102∶1 - **音频编码格式:** - - AAC是目前比较热门的有损压缩编码技术,并且衍生了LC-AAC,HE-AAC,HE-AAC v2 三种主要编码格式. - - LC-AAC 是比较传统的AAC,主要应用于中高码率的场景编码(>= 80Kbit/s) - HE-AAC 主要应用于低码率场景的编码(<= 48Kbit/s) - **优势**:在小于128Kbit/s的码率下表现优异,并且多用于视频中的音频编码 - **适合场景**:于128Kbit/s以下的音频编码,多用于视频中的音频轨的编码 ### 2.3 容器(视频封装格式) **封装格式:** 就是将已经编码压缩好的视频数据 和音频数据按照一定的格式放到一个文件中.这个文件可以称为容器. 当然可以理解为这只是一个外壳. 通常我们不仅仅只存放音频数据和视频数据,还会存放 一下视频同步的元数据.例如字幕.这多种数据会不同的程序来处理,但是它们在传输和存储的时候,这多种数据都是被绑定在一起的. - **常见的视频容器格式**: - - AVI: 是当时为对抗quicktime格式(mov)而推出的,只能支持固定CBR恒定定比特率编码的声音文件 - MOV:是Quicktime封装 - WMV:微软推出的,作为市场竞争 - mkv:万能封装器,有良好的兼容和跨平台性、纠错性,可带外挂字幕 - flv: 这种封装方式可以很好的保护原始地址,不容易被下载到,目前一些视频分享网站都采用这种封装方式 - MP4:主要应用于mpeg4的封装,主要在手机上使用。 ## 3.视频压缩的可能性 视频压缩,该从那几个方向去进行数据的压缩了? 实际上压缩的本质都是从冗余信息开始出发压缩的. 而视频数据之间是有极强的相关性.也就是这样会产生大量的冗余信息.这样的冗余包括空间上的冗余信息和时间上的冗余信息. - **使用帧间编码技术可以去除时间上的冗余信息,具体包括如下** - - **运动补偿**: 运动补偿是通过先前的局部图形来预测,补偿当前的局部图像.它是减少帧序列冗余信息很有效的方法. - **运动表示**: 不同区域的图像需要使用不同的运动矢量来描述运动信息 - **运动估计**: 运动估计就是从视频序列中抽取运动信息的一整套技术. ### 3.1 编码概念 **IPB帧** 视频压缩中,每帧代表着一副静止的图像.而进行实际压缩时,会采用各种算法以减少数据的容量.其实IPB帧是最常用的一种方式: - **I帧**:**关键帧,采用帧内压缩技术**.帧内编码帧(intra picture),I帧通常是每个GOP(MPEG所使用的一种视频压缩技术)的第一帧.经过适度的压缩.作为随机访问的参考点,可以当做静态图像.I帧可以看做一个图像经过压缩后的产物.I帧压缩可以得到6:1的压缩比而不会产生任何可察觉的模糊现象.I帧压缩去除了视频空间的冗余信息. - **P帧**:**向前参考帧.压缩时只参考前一个帧.属于帧间压缩技术**. 前后预测编码帧(predictive-frame),通过将图像序列中前面已编码帧的时间冗余信息充分去除来压缩传输数据量的编码图像. - **B帧**:**双向参考帧,压缩时即参考前一帧也参考后一帧.帧间压缩技术.** 双向预测编码帧(bi-directional interpolated prediction frame),既要考虑源图像序列前面已编码帧,又要顾及源图像序列后面的已编码帧之间的时间冗余信息,来压缩传输数据量的编码图像. **读者角度解读** - **I帧**,自身可以通过视频解码算法解压成一张单独的完整的视频画面.所以I帧去掉的是视频帧在空间维度上的冗余信息. - **P帧**,需要参考前面的一个I帧或P帧解码成一个完整的视频画面 - **B帧**,需要参考前面的一个I帧或者P帧以及后面的一个P帧来生成一个完整的视频画面. - **所以,P和B帧去掉的视频帧在时间上维度上的冗余信息**. ### 3.2 解码中PTS 与 DTS **DTS(Decoding Time Stamp)** ,主要用于视频的解码; **PTS(Presentation Time Stamp)** ,主要用于解码节点进行视频的同步和输出. 在没有B帧的情况下,DTS和PST的输出顺序是一样的.因为B帧会打乱了解码和显示顺序.所以一旦存在B帧,PTS和DTS势必会不同.实际上在大多数编解码标准中,编码顺序和输入顺序并不一致.于是需要PTS和DST这2种不同的时间戳. ### 3.3 GOP概念 **两个I帧之间形成的一组图片,就是GOP(Group of Picture).** 通常在编码器设置参数时,必须会设置gop_size的值.其实就是代表2个I帧之间的帧数目. 在一个GOP组中容量最大的就是I帧.所以相对而言,gop_size设置的越大,整个视频画面质量就会越好.但是解码端必须从接收的第一个I帧开始才可以正确解码出原始图像.否则无法正确解码. 如果在一秒钟内,有30帧.这30帧可以画成一组.如果摄像机或者镜头它一分钟之内它都没有发生大的变化.那也可以把这一分钟内所有的帧画做一组. ![img](https://pic3.zhimg.com/80/v2-4afb22faa0edb9a88c373ba5bc064ac2_720w.webp) **一组帧**就是一个I帧到下一个I帧.这一组的数据.包括B帧/P帧. ### 3.3 SPS/PPS SPS/PPS实际上就是存储GOP的参数. **SPS: (Sequence Parameter Set,序列参数集)存放帧数,参考帧数目,解码图像尺寸,帧场编码模式选择标识等**. - 一组帧的参数集. **PPS:(Picture Parameter Set,图像参数集).存放熵编码模式选择标识,片组数目,初始量化参数和去方块滤波系数调整标识等.(与图像相关的信息)** 在一组帧之前我们首先收到的是SPS/PPS数据.如果没有这组参数的话,我们是无法解码. 如果我们在解码时发生错误,首先要检查是否有SPS/PPS.如果没有,是因为对端没有发送过来还是因为对端在发送过程中丢失了. SPS/PPS数据,我们也把其归类到I帧.这2组数据是绝对不能丢的. ## 4. 视频花屏/卡顿原因 我们在观看视频时,会遇到花屏或者卡顿现象.那这个与我们刚刚所讲的GOF就息息相关了. - 如果GOP分组中的P帧丢失就会造成解码端的图像发生错误. - 为了避免花屏问题的发生,一般如果发现P帧或者I帧丢失.就不显示本GOP内的所有帧.只到下一个I帧来后重新刷新图像. - 当这时因为没有刷新屏幕.丢包的这一组帧全部扔掉了.图像就会卡在哪里不动.这就是卡顿的原因. **所以总结起来,花屏是因为你丢了P帧或者I帧.导致解码错误. 而卡顿是因为为了怕花屏,将整组错误的GOP数据扔掉了.直达下一组正确的GOP再重新刷屏.而这中间的时间差,就是我们所感受的卡顿.** ## 5.颜色模型 我们开发场景中使用最多的应该是 RGB 模型 ![img](https://pic3.zhimg.com/80/v2-892a6dad41d32ac11611566523626bce_720w.webp) 在 RGB 模型中每种颜色需要 3 个数字,分别表示 R、G、B,比如 (255, 0, 0) 表示红色,通常一个数字占用 1 字节,那么表示一种颜色需要 24 bits。那么有没有更高效的颜色模型能够用更少的 bit 来表示颜色呢? 现在我们假设我们定义一个**「亮度(Luminance)」**的概念来表示颜色的亮度,那它就可以用含 R、G、B 的表达式表示为: ```text Y = kr*R + kg*G + kb*B ``` Y 即「亮度」,kr、kg、kb 即 R、G、B 的权重值。 这时,我们可以定义一个**「色度(Chrominance)」**的概念来表示颜色的差异: ```text Cr = R – Y Cg = G – Y Cb = B – Y ``` Cr、Cg、Cb 分别表示在 R、G、B 上的色度分量。上述模型就是 **YCbCr** 颜色模型基本原理。 **YCbCr** 是属于 **YUV** 家族的一员,是在计算机系统中应用最为广泛的颜色模型,就比如在本文所讲的视频领域。***在 YUV 中 Y 表示的是「亮度」,也就是灰阶值,U 和 V 则是表示「色度」。\*** **YUV 的关键是在于它的亮度信号 Y 和色度信号 U、V 是分离的。那就是说即使只有 Y 信号分量而没有 U、V 分量,我们仍然可以表示出图像,只不过图像是黑白灰度图像**。在YCbCr 中 Y 是指亮度分量,Cb 指蓝色色度分量,而 Cr 指红色色度分量。 现在我们从 ITU-R BT.601-7 标准中拿到推荐的相关系数,就可以得到 YCbCr 与 RGB 相互转换的公式 ```text Y = 0.299R + 0.587G + 0.114B Cb = 0.564(B - Y) Cr = 0.713(R - Y) R = Y + 1.402Cr G = Y - 0.344Cb - 0.714Cr B = Y + 1.772Cb ``` 这样对于 **YCbCr** 这个颜色模型我们就有个初步认识了,但是我们会发现,这里 **YCbCr** 也仍然用了 3 个数字来表示颜色啊,有节省 **bit** 吗?为了回答这个问题,我们来结合视频中的图像和图像中的像素表示来说明 - 假设图片有如下像素组成 ![img](https://pic2.zhimg.com/80/v2-e834a437ca3971e80af48ec35d303bb9_720w.webp) 一副图片就是一个像素阵列.每个像素的 3 个分量的信息是完整的,**YCbCr 4:4:4**。 ![img](https://pic3.zhimg.com/80/v2-bc186e8c85439db556230dd77df7947a_720w.webp) 下图中,对于每个像素点都保留「亮度」值,但是省略每行中偶素位像素点的「色度」值,从而节省了 bit。**YCbCr4:2:2** ![img](https://pic1.zhimg.com/80/v2-0f6c0c6f7fe0cdc3bf918c8b6bc9be4c_720w.webp) 上图,做了更多的省略,但是对图片质量的影响却不会太大.**YCbCr4:2:0** ![img](https://pic1.zhimg.com/80/v2-c956932981497aa3bc3e67ef56159ab0_720w.webp) ## 6. 音频编码 常用压缩编码格式 WAV编码 WAV编码的一种实现方式(其实它有非常多实现方式,但都是不会进行压缩操作).就是在源PCM数据格式的前面加上44个字节.分别用来描述PCM的采样率,声道数,数据格式等信息. - 特点:音质非常好,大量软件都支持其播放 - 适合场合:多媒体开发的中间文件,保存音乐和音效素材 MP3编码 MP3编码具有不错的压缩比,而且听感也接近于WAV文件,当然在不同的环境下,应该调整合适的参数来达到更好的效果. - 特点:音质在128Kbit/s以上表现不错,压缩比比较高.大量软件和硬件都支持.兼容性高. - 适合场合:高比特率下对兼容性有要求的音乐欣赏. AAC编码 AAC是目前比较热门的有损压缩编码技术,并且衍生了LC-AAC,HE-AAC,HE-AAC v2 三种主要编码格式. - LC-AAC 是比较传统的AAC,主要应用于中高码率的场景编码(>= 80Kbit/s) - HE-AAC 主要应用于低码率场景的编码(<= 48Kbit/s) - 特点:在小于128Kbit/s的码率下表现优异,并且多用于视频中的音频编码 - 适合场景:于128Kbit/s以下的音频编码,多用于视频中的音频轨的编码 Ogg编码 Ogg编码是一种非常有潜力的编码,在各种码率下都有比较优秀的表现.尤其在低码率场景下.Ogg除了音质好之外,Ogg的编码算法也是非常出色.可以用更小的码率达到更好的音质.128Kbit/s的Ogg比192Kbit/s甚至更高码率的MP3更优质.但目前由软件还是硬件支持问题,都没法达到与MP3的使用广度. - 特点:可以用比MP3更小的码率实现比MP3更好的音质,高中低码率下均有良好的表现,兼容不够好,流媒体特性不支持. - 适合场景:语言聊天的音频消息场景 原文地址:[iOS音视频开发-了解编码及视频 - 资料 - 我爱音视频网 - 构建全国最权威的音视频技术交流分享论坛]( ================================================ FILE: iOS资料/iOS音视频开发-代码实现视频编码.md ================================================ # iOS音视频开发-代码实现视频编码 **硬编码的优点** - 提高编码性能(使用CPU的使用率大大降低,倾向使用GPU) - 增加编码效率(将编码一帧的时间缩短) - 延长电量使用(耗电量大大降低) **VideoToolBox框架的流程** - 创建session - 设置编码相关参数 - 开始编码 - 循环获取采集数据 - 获取编码后数据 - 将数据写入H264文件 编码的输入和输出 ![img](https://pic3.zhimg.com/80/v2-a234942364dbe630f66a1f095f5d57e6_720w.webp) 如图所示,左边的三帧视频帧是发送給编码器之前的数据,开发者必须将原始图像数据封装为CVPixelBuufer的数据结构.该数据结构是使用VideoToolBox的核心. CVPixelBuffer 解析 在这个官方文档的介绍中,CVPixelBuffer 给的官方解释,是其主内存存储所有像素点数据的一个对象.那么什么是主内存了? 其实它并不是我们平常所操作的内存,它指的是存储区域存在于缓存之中. 我们在访问这个块内存区域,需要先锁定这块内存区域 ```text //1.锁定内存区域: CVPixelBufferLockBaseAddress(pixel_buffer,0); //2.读取该内存区域数据到NSData对象中 Void *data = CVPixelBufferGetBaseAddress(pixel_buffer); //3.数据读取完毕后,需要释放锁定区域 CVPixelBufferRelease(pixel_buffer); ``` 单纯从它的使用方式,我们就可以知道这一块内存区域不是普通内存区域.它需要加锁,解锁等一系列操作. **作为视频开发,尽量减少进行显存和内存的交换.所以在iOS开发过程中也要尽量减少对它的内存区域访问**.建议使用iOS平台提供的对应的API来完成相应的一系列操作. 在AVFoundation 回调方法中,它有提供我们的数据其实就是CVPixelBuffer.只不过当时使用的是引用类型CVImageBufferRef,其实就是CVPixelBuffer的另外一个定义. Camera 返回的CVImageBuffer 中存储的数据是一个CVPixelBuffer,而经过VideoToolBox编码输出的CMSampleBuffer中存储的数据是一个CMBlockBuffer的引用. ![img](https://pic2.zhimg.com/80/v2-ebaf9479545cbe34e9033a966a9019a1_720w.webp) 在**iOS**中,会经常使用到session的方式.比如我们使用任何硬件设备都要使用对应的session,麦克风就要使用AudioSession,使用Camera就要使用AVCaptureSession,使用编码则需要使用VTCompressionSession.解码时,要使用VTDecompressionSessionRef. 视频编码步骤分解 第一步: 使用VTCompressionSessionCreate方法,创建编码会话; ```text //1.调用VTCompressionSessionCreate创建编码session //参数1:NULL 分配器,设置NULL为默认分配 //参数2:width //参数3:height //参数4:编码类型,如kCMVideoCodecType_H264 //参数5:NULL encoderSpecification: 编码规范。设置NULL由videoToolbox自己选择 //参数6:NULL sourceImageBufferAttributes: 源像素缓冲区属性.设置NULL不让videToolbox创建,而自己创建 //参数7:NULL compressedDataAllocator: 压缩数据分配器.设置NULL,默认的分配 //参数8:回调 当VTCompressionSessionEncodeFrame被调用压缩一次后会被异步调用.注:当你设置NULL的时候,你需要调用VTCompressionSessionEncodeFrameWithOutputHandler方法进行压缩帧处理,支持iOS9.0以上 //参数9:outputCallbackRefCon: 回调客户定义的参考值 //参数10:compressionSessionOut: 编码会话变量 OSStatus status = VTCompressionSessionCreate(NULL, width, height, kCMVideoCodecType_H264, NULL, NULL, NULL, didCompressH264, (__bridge void *)(self), &cEncodeingSession); ``` 第二步:设置相关的参数 ```text /* session: 会话 propertyKey: 属性名称 propertyValue: 属性值 */ VT_EXPORT OSStatus VTSessionSetProperty( CM_NONNULL VTSessionRef session, CM_NONNULL CFStringRef propertyKey, CM_NULLABLE CFTypeRef propertyValue ) API_AVAILABLE(macosx(10.8), ios(8.0), tvos(10.2)); ``` **kVTCompressionPropertyKey_RealTime**:设置是否实时编码 **kVTProfileLevel_H264_Baseline_AutoLevel**:表示使用H264的Profile规格,可以设置Hight的AutoLevel规格. **kVTCompressionPropertyKey_AllowFrameReordering**:表示是否使用产生B帧数据(因为B帧在解码是非必要数据,所以开发者可以抛弃B帧数据) **kVTCompressionPropertyKey_MaxKeyFrameInterval** : 表示关键帧的间隔,也就是我们常说的gop size. **kVTCompressionPropertyKey_ExpectedFrameRate** : 表示设置帧率 **kVTCompressionPropertyKey_AverageBitRate**/**kVTCompressionPropertyKey_DataRateLimits** 设置编码输出的码率. 第三步: 准备编码 ```text //开始编码 VTCompressionSessionPrepareToEncodeFrames(cEncodeingSession); ``` 第四步: 捕获编码数据 - 通过AVFoundation 捕获的视频,这个时候我们会走到AVFoundation捕获结果代理方法: ```text #pragma mark - AVCaptureVideoDataOutputSampleBufferDelegate //AV Foundation 获取到视频流 -(void)captureOutput:(AVCaptureOutput *)captureOutput didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer fromConnection:(AVCaptureConnection *)connection { //开始视频录制,获取到摄像头的视频帧,传入encode 方法中 dispatch_sync(cEncodeQueue, ^{ [self encode:sampleBuffer]; }); ``` 第五步:数据编码 - 将获取的视频数据编码 ```text - (void) encode:(CMSampleBufferRef )sampleBuffer { //拿到每一帧未编码数据 CVImageBufferRef imageBuffer = (CVImageBufferRef)CMSampleBufferGetImageBuffer(sampleBuffer); //设置帧时间,如果不设置会导致时间轴过长。时间戳以ms为单位 CMTime presentationTimeStamp = CMTimeMake(frameID++, 1000); VTEncodeInfoFlags flags; //参数1:编码会话变量 //参数2:未编码数据 //参数3:获取到的这个sample buffer数据的展示时间戳。每一个传给这个session的时间戳都要大于前一个展示时间戳. //参数4:对于获取到sample buffer数据,这个帧的展示时间.如果没有时间信息,可设置kCMTimeInvalid. //参数5:frameProperties: 包含这个帧的属性.帧的改变会影响后边的编码帧. //参数6:ourceFrameRefCon: 回调函数会引用你设置的这个帧的参考值. //参数7:infoFlagsOut: 指向一个VTEncodeInfoFlags来接受一个编码操作.如果使用异步运行,kVTEncodeInfo_Asynchronous被设置;同步运行,kVTEncodeInfo_FrameDropped被设置;设置NULL为不想接受这个信息. OSStatus statusCode = VTCompressionSessionEncodeFrame(cEncodeingSession, imageBuffer, presentationTimeStamp, kCMTimeInvalid, NULL, NULL, &flags); if (statusCode != noErr) { NSLog(@"H.264:VTCompressionSessionEncodeFrame faild with %d",(int)statusCode); VTCompressionSessionInvalidate(cEncodeingSession); CFRelease(cEncodeingSession); cEncodeingSession = NULL; return NSLog(@"H264:VTCompressionSessionEncodeFrame Success"); ``` 第六步: 编码数据处理-获取SPS/PPS 当编码成功后,就会回调到最开始初始化编码器会话时传入的回调函数,回调函数的原型如下: **void didCompressH264(void \*outputCallbackRefCon, void \*sourceFrameRefCon, OSStatus status, VTEncodeInfoFlags infoFlags, CMSampleBufferRef sampleBuffer)** - **判断status,如果成功则返回**0(noErr) **;成功则继续处理,不成功则不处理.** - **判断是否关键帧** - - **为什么要判断关键帧呢?** - 因为VideoToolBox编码器在每一个关键帧前面都会输出SPS/PPS信息.所以如果本帧未关键帧,则可以取出对应的SPS/PPS信息. ```text //判断当前帧是否为关键帧 //获取sps & pps 数据 只获取1次,保存在h264文件开头的第一帧中 //sps(sample per second 采样次数/s),是衡量模数转换(ADC)时采样速率的单位 //pps() if (keyFrame) { //图像存储方式,编码器等格式描述 CMFormatDescriptionRef format = CMSampleBufferGetFormatDescription(sampleBuffer); //sps size_t sparameterSetSize,sparameterSetCount; const uint8_t *sparameterSet; OSStatus statusCode = CMVideoFormatDescriptionGetH264ParameterSetAtIndex(format, 0, &sparameterSet, &sparameterSetSize, &sparameterSetCount, 0); if (statusCode == noErr) { //获取pps size_t pparameterSetSize,pparameterSetCount; const uint8_t *pparameterSet; //从第一个关键帧获取sps & pps OSStatus statusCode = CMVideoFormatDescriptionGetH264ParameterSetAtIndex(format, 1, &pparameterSet, &pparameterSetSize, &pparameterSetCount, 0); //获取H264参数集合中的SPS和PPS if (statusCode == noErr) { //Found pps & sps NSData *sps = [NSData dataWithBytes:sparameterSet length:sparameterSetSize]; NSData *pps = [NSData dataWithBytes:pparameterSet length:pparameterSetSize]; if(encoder) { [encoder gotSpsPps:sps pps:pps]; ``` 第七步 编码压缩数据并写入H264文件 当我们获取了SPS/PPS信息之后,我们就获取实际的内容来进行处理了 ```text CMBlockBufferRef dataBuffer = CMSampleBufferGetDataBuffer(sampleBuffer); size_t length,totalLength; char *dataPointer; OSStatus statusCodeRet = CMBlockBufferGetDataPointer(dataBuffer, 0, &length, &totalLength, &dataPointer); if (statusCodeRet == noErr) { size_t bufferOffset = 0; static const int AVCCHeaderLength = 4;//返回的nalu数据前4个字节不是001的startcode,而是大端模式的帧长度length //循环获取nalu数据 while (bufferOffset < totalLength - AVCCHeaderLength) { uint32_t NALUnitLength = 0; //读取 一单元长度的 nalu memcpy(&NALUnitLength, dataPointer + bufferOffset, AVCCHeaderLength); //从大端模式转换为系统端模式 NALUnitLength = CFSwapInt32BigToHost(NALUnitLength); //获取nalu数据 NSData *data = [[NSData alloc]initWithBytes:(dataPointer + bufferOffset + AVCCHeaderLength) length:NALUnitLength]; //将nalu数据写入到文件 [encoder gotEncodedData:data isKeyFrame:keyFrame]; //move to the next NAL unit in the block buffer //读取下一个nalu 一次回调可能包含多个nalu数据 bufferOffset += AVCCHeaderLength + NALUnitLength; //第一帧写入 sps & pps - (void)gotSpsPps:(NSData*)sps pps:(NSData*)pps { NSLog(@"gotSpsPp %d %d",(int)[sps length],(int)[pps length]); const char bytes[] = "\x00\x00\x00\x01"; size_t length = (sizeof bytes) - 1; NSData *ByteHeader = [NSData dataWithBytes:bytes length:length]; [fileHandele writeData:ByteHeader]; [fileHandele writeData:sps]; [fileHandele writeData:ByteHeader]; [fileHandele writeData:pps]; - (void)gotEncodedData:(NSData*)data isKeyFrame:(BOOL)isKeyFrame { NSLog(@"gotEncodeData %d",(int)[data length]); if (fileHandele != NULL) { //添加4个字节的H264 协议 start code 分割符 //一般来说编码器编出的首帧数据为PPS & SPS //H264编码时,在每个NAL前添加起始码 0x000001,解码器在码流中检测起始码,当前NAL结束。 /* 为了防止NAL内部出现0x000001的数据,h.264又提出'防止竞争 emulation prevention"机制,在编码完一个NAL时,如果检测出有连续两个0x00字节,就在后面插入一个0x03。当解码器在NAL内部检测到0x000003的数据,就把0x03抛弃,恢复原始数据。 总的来说H264的码流的打包方式有两种,一种为annex-b byte stream format 的格式,这个是绝大部分编码器的默认输出格式,就是每个帧的开头的3~4个字节是H264的start_code,0x00000001或者0x000001。 另一种是原始的NAL打包格式,就是开始的若干字节(1,2,4字节)是NAL的长度,而不是start_code,此时必须借助某个全局的数据来获得编 码器的profile,level,PPS,SPS等信息才可以解码。 */ const char bytes[] ="\x00\x00\x00\x01"; //长度 size_t length = (sizeof bytes) - 1; //头字节 NSData *ByteHeader = [NSData dataWithBytes:bytes length:length]; //写入头字节 [fileHandele writeData:ByteHeader]; //写入H264数据 [fileHandele writeData:data]; ``` 原文https://zhuanlan.zhihu.com/p/552724772 ================================================ FILE: iOS资料/iOS音视频开发-采集、编码、滤镜.md ================================================ # iOS音视频开发-采集、编码、滤镜 ## 1.OpenGL(绘制点 线 三角形) **OpenGL 是⼀种图形编程接口(Application Programming Interface, API).简单理解就是开发的图形库,可以进行一些视频,图形,图⽚的渲染(顶点着色 光栅化(连线 完成定点之间的像素点) 片元着色)处理、2D/3D 游戏引擎开发等。** ### **1.1 管线(渲染流程)** **管线分为2个部分,上半部分是客户端(C/C++,以及OpenGL API), 下半部分为服务器端(接受Attributies属性、Uniforms、TextureData纹理数据,传递到顶点着色器(处理坐标),顶点着色器的输出最终会传递到片元着色器(注意片元着色器不接受Attributies,但可以接受Uniforms和TextureData)).** ![img](https://pic2.zhimg.com/80/v2-ce98a91ee2c7d68fc8c829a30cec0429_720w.webp) - **固定管线:**提供一个渲染流程的管线; - **可编程管线(**GLSL(着色语言): 专⻔为图形开发设计的编程语⾔**):只有定点着色器(旋转平移缩放投影)和片元着色器部分来进行编程;** **其中:** - **Attributies:**就是对⼀个顶点都要作出改变的数据元素.实际上,顶点位置本身就是⼀个属性。 - **Uniform:**通过设置Uniform 变量就紧接着发送⼀个图元批次处理命令. Uniform 变量实际上可以⽆限次的使⽤. 设置⼀个应⽤于整个表⾯的单个颜⾊值,还可也是⼀个时间值. - **Texture Data**: 一般在片元着色器中处理,给图形填充/添加一些颜色/质感; ### 1.2 iOS实现OpenGL ES(Embedded Systems)的两种方式 - **GLSL** - **GLKit** ## 2. GLSL语法 ### 2.1 基本数据类型 - 整型(有符号/无符号) uint a= 32u; - 浮点数(单精度) float fValue = 3.1f; - 布尔值 bool isDisplay= false; - 向量、分量类型/矩阵类型 ![img](https://pic3.zhimg.com/80/v2-f12e44feca3486c61ae16be91e5dc64a_720w.webp) ![img](https://pic1.zhimg.com/80/v2-d47398af2078833c3a0d9199a1ad66d8_720w.webp) ```text v1 = vec4(10,10,10,10); //឴ 通过x y z w来获取向量中的元素 v1.x = 3.0f; v1.y = 4.0f; v1.z = 5.0f; v1.w = 1.0f; v1.xyz = vec3(1,2,3); //឴ 也可通过r g b a来获取向量中的元素 v1.r = 1.0f; //឴ 也可通过s t p q来获取向量中的元素 v1.st = vec2(1.0,2.0); m1 = mat4( 1.0,1.0,1.0,1.0, 1.0,1.0,1.0,1.0, 1.0,1.0,1.0,1.0, 1.0,1.0,1.0,1.0, ) m1 = mat4(1.0f); ``` ### 2.2 存储限定符(着色器变量(输入输出变量)声明过程中的修饰符) - 输入变量: 从外部(客户端/上一个阶段着色器传递的属性/Uniform等). - 输出变量: 从任何着色器阶段进行写入的变量。 ![img](https://pic1.zhimg.com/80/v2-bf31dee882fe2b08c8700450fd81e4e0_720w.webp) 常用const varying attribute uniform - **Const**:⽤来修饰任何基本数据类型,不能⽤来修饰包含数组的数组、结构体;声明的变量在其所属的着⾊器器中均是只读的。 - **其中varying**: 传递变量的作用,表示从顶点着色器传递到片元着色器.一般会传递顶点坐标/顶点颜色/纹理值(在.vsh中定义varying修饰的变量,在.fsh中定义varying修饰的同名变量,实现将一个变量从定点着色器传递到片元着色器)。 - 被**uniform**(glUniform**())修饰的变量,只能被shader使用,不能被修改;可理解问vertext和fragment的全局变量。比如变换矩阵/材质/光照/颜色。 - 被**attribute**修饰的变量只能永在vertex shader中。使用场景:顶点坐标/法线/纹理坐标/顶点颜色。 **GLSL渲染思路**: - -> 创建图层(CAEAGLayer 针对OpenGL ES的渲染的图层) - -> 创建图形上下文(EAGLContext) - -> 清空缓冲区 - -> 设置RenderBuffer (glGenRenderbuffers) - -> 设置FrameBuffer (glGenFrameBuffers) - -> 开始绘制 loadShaders ## 3. GLKit(无法处理3个以上的光源 和2个以上的纹理) - -> 新建上下文 并配置GLKView ```text self.context = [[EAFGContext all] init... APIOpenGLES2]; GLKView *view = self.view; view.context = self,context; 然后配置颜色深度... // 设置上下文 [EAGLContext setCurrentContext: self。context]; // 开启深度测试 glEnable(GL_DEPTH_TEST) ``` - -> 实现glkView:drawInRect代理方法 ```text //在这个代理方法中准备绘制 [baseEffect prepareToDraw..]; //索引绘图 glDrawElements(...) ``` - -> 渲染图形 ```text //确定顶点数据GLFloat 绘制索引GLuint 索引数组个数 sizeOf(indices)/sizeOf(GLunit) //绑定缓冲区:将顶点数组数据载入数组缓冲区 将索引数据存储到索引数组缓冲区 //给着色器中传入顶点数据 颜色数据 纹理数据(用GLKBaseEffect(着色器对象)加载纹理数据) ``` - -> 实现update代理方法 ## 4. 滤镜(GPUImage(底层是OpenGL ES、AV Foundation采集)、CoreImage) ### 4.1 用OpenGL ES的片元着色器来实现滤镜 **原理**:利用片元着色器读取每一个像素,对其灰度进行处理(饱和度、曝光),计算出一个新的颜色。 ### 4.2 GPUImage实现滤镜(100多种滤镜,也可以自定义滤镜) **原理**:把图片或者视频的每一帧图片进行图形变化(饱和度/色温)处理之后,再显示到屏幕上。本质就是像素点颜色的变化。 **流程**:数据源Source -> 滤镜Filter -> Final **Source(数据源环节)**: - GPUImageVideoCamera :摄像头(用于拍摄视频); - GPUImageStillCamera:摄像头(用于拍摄照片); - GPUImagePicture : 用于处理拍摄完成的图片; - GPUImageMovie : 用于处理已拍摄好的视频; **Filter(滤镜环节)**: GPUImageFilter:用来接受图形源,通过(自定义)顶点/片源着色器来渲染新的图像。 **Final(输出Outputs):** - GPUImageView - GPUImageMovieWrite ## 5. Matal MTKView:在MetalKit中提供了一个视图类`MTKView`,类似于GLKit中`GLKView` ,用于处理metal绘制并显示到屏幕过程中的细节。即首先需要先创建`MTKView`对象。 MTLDevice:由于metal是操作GPU的,所以Metal中提供了`MTLDevice`协议表示GPU接口,通过式`MTLCreateSystemDefaultDevice()`获取GPU。 MTLCommandQueue:在获取了GPU后,创建渲染队列,队列中存储的是将要渲染的命令。`MTLCommandBuffer`。 渲染流程: - 先用MTLCommandBuffer创建渲染缓存区。 - 其次通过MTLRenderPassDescriptor创建渲染描述符。 - 然后再通过**渲染缓存区**和**渲染描述符**创建命令编辑器MTLRenderCommandEncoder进行编码。 - 最后是结束编码 -> 提交渲染命令 -> 在完成渲染后,将命令缓存区提交至GPU。 ## 6. 音视频开发 **音视频采集 -> 视频滤镜(**GPUImage**) -> 音视频编码 -> 推流 -> 流媒体服务器处理 -> 拉流 -> 音视频解码 -> 音视频播放** ### 6.1 AVFoundation(主要是音视频的采集) - 捕捉会话: AVCaptureSession. - 捕捉设备: AVCaptureDevice(获取摄像头设备,以及调节摄像头一些属性 聚焦、曝光、闪光灯). - 捕捉设备输入: AVCaptureDeviceInput (音频输入、视频输入) - 捕捉设备输出: AVCaptureOutput抽象类. - ->AVCaptureStillImageOutput - ->AVCaputureMovieFileOutput - ->AVCaputureAudioDataOutput - ->AVCaputureVideoDataOutput - 捕捉链接 : AVCaptureConnection - 捕捉预览: AVCaptureVideoPreviewLayer (显示摄像头实时捕捉的内容) ### 6.2 音视频编码 **硬编码(GPU编码**):VideoToolBox和AudioToolBox **软编码(CPU编码)**: - 视频:用FFmpeg,x264算法把视频原数据YUV/RGB编码成H264编码格式,码率比较低,压缩比高 - 音频:使用fdk_aac 将音频数据PCM转成AAC ### 6.3 推流(将音视频数据通过流媒体协议发送到流媒体服务器) 流媒体协议:RTMP\RTSP\HLS\FLV - 视频流式封装格式:TS\FLV - 音频封装格式:Mp3\AAC ### 6.3 流媒体服务器 - 数据分发 - 实时转码 - 内容检测 ### 6.4 播放 ijkplayer 播放框架 原文https://zhuanlan.zhihu.com/p/584447691 ================================================ FILE: iOS资料/iOS音视频开发——FFmpeg库编译.md ================================================ # iOS音视频开发——FFmpeg库编译 ## 1.安装 Homebrew **Homebrew:**是 Mac 平台上的一个包管理工具,提供了许多 Mac 下没有的 Linux工具等, 而且安装工具极其简单,一句命令行的事。 1. 检查是否已安装 `Homebrew`,只需在终端输入: ```text $ brew ``` 2.得到如图结果,说明已经安装,否则需要安装。 ![img](https://pic2.zhimg.com/80/v2-84818233bf0e17574fa9adbb86033c89_720w.webp) 3.安装 `Homebrew` ,在终端输入: ```text $ ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)" ``` ## 2.编译 iOS 版可用的 FFmpeg库 > 需要用到的工具: 1. gas-preprocessor 2. yasm 3. FFmpeg-iOS-build-script ## 3.下载 gas-preprocessor > gas-preprocessor 就是我们要编译 FFmpeg 的所需脚本文件。 1. 将其解压后,其内部只有简单的 4 个文件,如下图: ![img](https://pic1.zhimg.com/80/v2-4d1c8fac8a887c38be36172ff4df5f18_720w.webp) 2.将 **gas-preprocessor.pl** 文件复制到 `/usr/local/bin/` 目录下,然后为文件开启可执行权限: ```text $ chmod 777 /usr/local/bin/gas-preprocessor.pl ``` ## 4.安装 yasm > Yasm是一个完全重写的 NASM 汇编。目前,它支持x86和AMD64指令集,接受NASM和气体汇编语法,产出二进制,ELF32 , ELF64 , COFF , Mach - O的( 32和64 ),RDOFF2 ,的Win32和Win64对象的格式,并生成STABS 调试信息的来源,DWARF 2 ,CodeView 8格式。 1. 下载 yasm: ```text $ brew install yasm ``` ![img](https://pic3.zhimg.com/80/v2-4ae32b038ed108a2a11dcf9ba1c43b9a_720w.webp) 2.检测是否已安装 yasm ```text $ brew install yasm ``` ![img](https://pic3.zhimg.com/80/v2-98c23d9d3374d2c77a38f23f6fbee082_720w.webp) 3.编译 `FFmpeg` 脚本如图: ![img](https://pic1.zhimg.com/80/v2-68c7746aa926d9391b92d18714bf0d24_720w.webp) ## 5.编译 FFmpeg-iOS-build-script,得到我们需要的 iOS 能用的 ffmpeg 库 > 这个脚本是转为 `iOS` 编译出可用的 `ffmpeg` 的库,有了这个脚本,就不用下载 ffmpeg 了,脚本会自动下载好最新版本的 ffmpeg,并打包成一个 iOS 可用的 ffmpeg 库提供给我们了 1. 下载FFmpeg-iOS-build-script压缩包。 2. 解压 FFmpeg-iOS-build-script 得到的文件如下: ![img](https://pic4.zhimg.com/80/v2-cefee949add18eb65069f937d4cc32c7_720w.webp) 3.终端 cd 到文件的目录,然后执行以下命令,编译脚本,打包出我们需要的 iOS 的 ffmpeg 库: ```text $ ./build-ffmpeg.sh ``` 4.编译完成后,就可以得到 `FFmpeg` 源码以及我们需要的 `lib` : ![img](https://pic3.zhimg.com/80/v2-f6706caed048cd5f89acb77ed4b6dfba_720w.webp) ## 6.集成FFmpeg 库开发工程当中 1. 把 FFmpeg-iOS 直接复制到你的工程目录下,如图: ![img](https://pic1.zhimg.com/80/v2-e7682effea8758e33155c42cd81692a4_720w.webp) 2.设置环境:进入 `Build Setting` ,修改 `header search Path` 链接到工程的 include 文件当中 操作如下: ![img](https://pic2.zhimg.com/80/v2-ec1b7ad9a5600ec52af045c2dc30257d_720w.webp) 3.至此,已经成功编译并集成了 ffmpeg 了。 原文链接:[iOS音视频开发--FFmpeg库编译 - 资料 - 我爱音视频网 - 构建全国最权威的音视频技术交流分享论坛]( ================================================ FILE: iOS资料/iOS音视频开发——视频采集.md ================================================ # iOS音视频开发——视频采集 ## 1.认识 AVCapture 系列 AVCapture 系列是 AVFoundation 框架为我们提供的用于管理输入设备、采集、输出、预览等一系列接口,其工作原理如下: ![img](https://pic1.zhimg.com/80/v2-5e1574831251fe8f220730e11c69a6ec_720w.webp) \1. AVCaptureDevice: 信号采集硬件设备(摄像头、麦克风、屏幕等) `AVCaptureDevice` 代表硬件设备,并且为 `AVCaptureSession` 提供 input,要想使用 `AVCaptureDevice`,应该先将设备支持的 `device` 枚举出来, 根据摄像头的位置( 前置或者后置摄像头 )获取需要用的那个摄像头, 再使用; 如果想要对 `AVCaptureDevice` 对象的一些属性进行设置,应该先调用 `lockForConfiguration:` 方法, 设置结束后,调用 `unlockForConfiguration` 方法; ```text [self.device lockForConfiguration:&error]; // 设置 *** [self.device unlockForConfiguration]; ``` ## 2. AVCaptureInput: 输入数据管理 AVCaptureInput 继承自 `NSObject`,是向 `AVCaptureSession` 提供输入数据的对象的抽象超类; 要将 `AVCaptureInput` 对象与会话 `AVCaptureSession` 关联,需要 `AVCaptureSession`实例调用 `-addInput:` 方法。 由于 `AVCaptureInput` 是个抽象类,无法直接使用,所以我们一般使用它的子类类管理输入数据。我们常用的 `AVCaptureInput` 的子类有三个: ![img](https://pic1.zhimg.com/80/v2-66c51c4c4ea37d25769283b95b808c78_720w.webp) `AVCaptureDeviceInput`:用于从 `AVCaptureDevice` 对象捕获数据; `AVCaptureScreenInput`:从 macOS 屏幕上录制的一种捕获输入; `AVCaptureMetadataInput`:它为 `AVCaptureSession` 提供 `AVMetadataItems`。 ## 3. AVCaptureOutput:输出数据管理 AVCaptureOutput 继承自 `NSObject`,是输出数据管理,该对象将会被添加到会话`AVCaptureSession`中,用于接收会话`AVCaptureSession`各类输出数据; `AVCaptureOutput`提供了一个抽象接口,用于将捕获输出数据(如文件和视频预览)连接到捕获会话`AVCaptureSession`的实例,捕获输出可以有多个由`AVCaptureConnection`对象表示的连接,一个连接对应于它从捕获输入(`AVCaptureInput`的实例)接收的每个媒体流,捕获输出在首次创建时没有任何连接,当向捕获会话添加输出时,将创建连接,将该会话的输入的媒体数据映射到其输出,调用`AVCaptureSession`的`-addOutput:`方法将`AVCaptureOutput`与`AVCaptureSession`关联。 `AVCaptureOutput` 是个抽象类,我们必须使用它的子类,常用的 `AVCaptureOutput`的子类如下所示: ![img](https://pic4.zhimg.com/80/v2-0e629068674d0290807a5d48343edf9f_720w.webp) `AVCaptureAudioDataOutput`:一种捕获输出,用于记录音频,并在录制音频时提供对音频样本缓冲区的访问; `AVCaptureAudioPreviewOutput` :一种捕获输出,与一个核心音频输出设备相关联、可用于播放由捕获会话捕获的音频; `AVCaptureDepthDataOutput` :在兼容的摄像机设备上记录场景深度信息的捕获输出; `AVCaptureMetadataOutput` :用于处理捕获会话 `AVCaptureSession` 产生的定时元数据的捕获输出; `AVCaptureStillImageOutput`:在macOS中捕捉静止照片的捕获输出。该类在 iOS 10.0 中被弃用,并且不支持新的相机捕获功能,例如原始图像输出和实时照片,在 iOS 10.0 或更高版本中,使用 `AVCapturePhotoOutput` 类代替; `AVCapturePhotoOutput` :静态照片、动态照片和其他摄影工作流的捕获输出; `AVCaptureVideoDataOutput` :记录视频并提供对视频帧进行处理的捕获输出; `AVCaptureFileOutput`:用于捕获输出的抽象超类,可将捕获数据记录到文件中; `AVCaptureMovieFileOutput` :继承自 `AVCaptureFileOutput`,将视频和音频记录到 QuickTime 电影文件的捕获输出; `AVCaptureAudioFileOutput` :继承自 `AVCaptureFileOutput`,记录音频并将录制的音频保存到文件的捕获输出。 ## 4. AVCaptureSession: 用来管理采集数据和输出数据,它负责协调从哪里采集数据,输出到哪里,它是整个Capture的核心,类似于RunLoop,它不断的从输入源获取数据,然后分发给各个输出源 AVCaptureSession 继承自`NSObject`,是`AVFoundation`的核心类,用于管理捕获对象`AVCaptureInput`的视频和音频的输入,协调捕获的输出`AVCaptureOutput` ![img](https://pic3.zhimg.com/80/v2-bcd3fa28a8d2d755fac1fd228e77fe82_720w.webp) ## 5. AVCaptureConnection: 用于 `AVCaptureSession` 来建立和维护 `AVCaptureInput` 和 `AVCaptureOutput` 之间的连接 AVCaptureConnection 是 `Session` 和 `Output` 中间的控制节点,每个 `Output` 与 `Session` 建立连接后,都会分配一个默认的 `AVCpatureConnection`。 ## 6. AVCapturePreviewLayer: 预览层,`AVCaptureSession` 的一个属性,继承自 `CALayer`,提供摄像头的预览功能,照片以及视频就是通过把 `AVCapturePreviewLayer` 添加到 `UIView` 的 `layer` 上来显示 开始视频采集 1、创建并初始化输入`AVCaptureInput`: `AVCaptureDeviceInput` 和输出`AVCaptureOutput`: `AVCaptureVideoDataOutput`; 2、创建并初始化 `AVCaptureSession`,把 `AVCaptureInput` 和 `AVCaptureOutput` 添加到 `AVCaptureSession` 中; 3、调用 `AVCaptureSession` 的 `startRunning` 开启采集 初始化输入 通过 `AVCaptureDevice` 的 `devicesWithMediaType:` 方法获取摄像头,iPhone 都是有前后摄像头的,这里获取到的是一个设备的数组,要从数组里面拿到我们想要的前摄像头或后摄像头,然后将 `AVCaptureDevice` 转化为 `AVCaptureDeviceInput`,添加到 `AVCaptureSession`中 ```text /************************** 设置输入设备 *************************/ // --- 获取所有摄像头 --- NSArray *cameras = [AVCaptureDevice devicesWithMediaType:AVMediaTypeVideo]; // --- 获取当前方向摄像头 --- NSArray *captureDeviceArray = [cameras filteredArrayUsingPredicate:[NSPredicate predicateWithFormat:@"position == %d", _capturerParam.devicePosition]]; if (captureDeviceArray.count == 0) { return nil; } // --- 转化为输入设备 --- AVCaptureDevice *camera = captureDeviceArray.firstObject; self.captureDeviceInput = [AVCaptureDeviceInput deviceInputWithDevice:camera error:&error]; ``` 设置视频采集参数 ```text @implementation VideoCapturerParam - (instancetype)init { self = [super init]; if (self) { _devicePosition = AVCaptureDevicePositionFront; // 摄像头位置,默认为前置摄像头 _sessionPreset = AVCaptureSessionPreset1280x720; // 视频分辨率 默认 AVCaptureSessionPreset1280x720 _frameRate = 15; // 帧 单位为 帧/秒,默认为15帧/秒 _videoOrientation = AVCaptureVideoOrientationPortrait; // 摄像头方向 默认为当前手机屏幕方向 switch ([UIDevice currentDevice].orientation) { case UIDeviceOrientationPortrait: case UIDeviceOrientationPortraitUpsideDown: _videoOrientation = AVCaptureVideoOrientationPortrait; break; case UIDeviceOrientationLandscapeRight: _videoOrientation = AVCaptureVideoOrientationLandscapeRight; break; case UIDeviceOrientationLandscapeLeft: _videoOrientation = AVCaptureVideoOrientationLandscapeLeft; break; default: break; } } return self; } ``` 初始化输出 初始化视频输出 `AVCaptureVideoDataOutput`,并设置视频数据格式,设置采集数据回调线程,这里视频输出格式选的是 kCVPixelFormatType_420YpCbCr8BiPlanarFullRange,YUV 数据格式 ```text /************************** 设置输出设备 *************************/ // --- 设置视频输出 --- self.captureVideoDataOutput = [[AVCaptureVideoDataOutput alloc] init]; NSDictionary *videoSetting = [NSDictionary dictionaryWithObjectsAndKeys:[NSNumber numberWithInt:kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange], kCVPixelBufferPixelFormatTypeKey, nil]; // kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange 表示输出的视频格式为NV12 [self.captureVideoDataOutput setVideoSettings:videoSetting]; // --- 设置输出串行队列和数据回调 --- dispatch_queue_t outputQueue = dispatch_queue_create("VideoCaptureOutputQueue", DISPATCH_QUEUE_SERIAL); // --- 设置代理 --- [self.captureVideoDataOutput setSampleBufferDelegate:self queue:outputQueue]; // --- 丢弃延迟的帧 --- self.captureVideoDataOutput.alwaysDiscardsLateVideoFrames = YES; ``` **初始化 AVCaptureSession 并设置输入输出** 1、初始化 `AVCaptureSession`,把上面的输入和输出加进来,在添加输入和输出到 `AVCaptureSession` 先查询一下 `AVCaptureSession` 是否支持添加该输入或输出端口; 2、设置视频分辨率及图像质量(AVCaptureSessionPreset),设置之前同样需要先查询一下 `AVCaptureSession` 是否支持这个分辨率; 3、如果在已经开启采集的情况下需要修改分辨率或输入输出,需要用 `beginConfiguration` 和`commitConfiguration` 把修改的代码包围起来。在调用 `beginConfiguration` 后,可以配置分辨率、输入输出等,直到调用 `commitConfiguration` 了才会被应用; 4、`AVCaptureSession` 管理了采集过程中的状态,当开始采集、停止采集、出现错误等都会发起通知,我们可以监听通知来获取 `AVCaptureSession` 的状态,也可以调用其属性来获取当前 `AVCaptureSession` 的状态, `AVCaptureSession` 相关的通知都是在主线程的。 前置摄像头采集到的画面是翻转的,若要解决画面翻转问题,需要设置 `AVCaptureConnection` 的 `videoMirrored` 为 YES。 ```text /************************** 初始化会话 *************************/ self.captureSession = [[AVCaptureSession alloc] init]; self.captureSession.usesApplicationAudioSession = NO; // --- 添加输入设备到会话 --- if ([self.captureSession canAddInput:self.captureDeviceInput]) { [self.captureSession addInput:self.captureDeviceInput]; } else { NSLog(@"VideoCapture:: Add captureVideoDataInput Faild!"); return nil; } // --- 添加输出设备到会话 --- if ([self.captureSession canAddOutput:self.captureVideoDataOutput]) { [self.captureSession addOutput:self.captureVideoDataOutput]; } else { NSLog(@"VideoCapture:: Add captureVideoDataOutput Faild!"); return nil; } // --- 设置分辨率 --- if ([self.captureSession canSetSessionPreset:self.capturerParam.sessionPreset]) { self.captureSession.sessionPreset = self.capturerParam.sessionPreset; } /************************** 初始化连接 *************************/ self.captureConnection = [self.captureVideoDataOutput connectionWithMediaType:AVMediaTypeVideo]; // --- 设置摄像头镜像,不设置的话前置摄像头采集出来的图像是反转的 --- if (self.capturerParam.devicePosition == AVCaptureDevicePositionFront && self.captureConnection.supportsVideoMirroring) { // supportsVideoMirroring 视频是否支持镜像 self.captureConnection.videoMirrored = YES; } self.captureConnection.videoOrientation = self.capturerParam.videoOrientation; self.videoPreviewLayer = [AVCaptureVideoPreviewLayer layerWithSession:self.captureSession]; self.videoPreviewLayer.connection.videoOrientation = self.capturerParam.videoOrientation; self.videoPreviewLayer.videoGravity = AVLayerVideoGravityResizeAspectFill; ``` 采集视频 / 回调 ```text /** * 开始采集 */ - (NSError *)startCpture { if (self.isCapturing) { return [NSError errorWithDomain:@"VideoCapture:: startCapture faild: is capturing" code:1 userInfo:nil]; } // --- 摄像头权限判断 --- AVAuthorizationStatus videoAuthStatus = [AVCaptureDevice authorizationStatusForMediaType:AVMediaTypeVideo]; if (videoAuthStatus != AVAuthorizationStatusAuthorized) { return [NSError errorWithDomain:@"VideoCapture:: Camera Authorizate faild!" code:1 userInfo:nil]; } [self.captureSession startRunning]; self.isCapturing = YES; kLOGt(@"开始采集视频"); return nil; } /** * 停止采集 */ - (NSError *)stopCapture { if (!self.isCapturing) { return [NSError errorWithDomain:@"VideoCapture:: stop capture faild! is not capturing!" code:1 userInfo:nil]; } [self.captureSession stopRunning]; self.isCapturing = NO; kLOGt(@"停止采集视频"); return nil; } #pragma mark ————— AVCaptureVideoDataOutputSampleBufferDelegate ————— /** * 摄像头采集数据回调 @prama output 输出设备 @prama sampleBuffer 帧缓存数据,描述当前帧信息 @prama connection 连接 */ - (void)captureOutput:(AVCaptureOutput *)output didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer fromConnection:(AVCaptureConnection *)connection { if ([self.delagate respondsToSelector:@selector(videoCaptureOutputDataCallback:)]) { [self.delagate videoCaptureOutputDataCallback:sampleBuffer]; } } ``` ## 7.调用 / 获取数据 调用很简单,初始化视频采集参数 `VideoCapturerParam` 和 视频采集器 `VideoVapturer` , 设置预览图层 `videoPreviewLayer` , 调用 `startCpture` 就可以开始采集了,然后实现数据采集回调的代理方法 `videoCaptureOutputDataCallback` 获取数据 ```text // --- 初始化视频采集参数 --- VideoCapturerParam *param = [[VideoCapturerParam alloc] init]; // --- 初始化视频采集器 --- self.videoCapture = [[VideoVapturer alloc] initWithCaptureParam:param error:nil]; self.videoCapture.delagate = self; // --- 开始采集 --- [self.videoCapture startCpture]; // --- 初始化预览View --- self.recordLayer = self.videoCapture.videoPreviewLayer; self.recordLayer.frame = CGRectMake(0, 0, CGRectGetWidth(self.view.bounds), CGRectGetHeight(self.view.bounds)); [self.view.layer addSublayer:self.recordLayer]; ``` ```text #pragma mark ————— VideoCapturerDelegate ————— 视频采集回调 - (void)videoCaptureOutputDataCallback:(CMSampleBufferRef)sampleBuffer { NSLog(@"%@ sampleBuffer : %@ ", kLOGt(@"视频采集回调"), sampleBuffer); } ``` 至此,我们就完成了视频的采集,在采集前和过程中,我们可能会对采集参数、摄像头方向、帧率等进行修改。 原文https://zhuanlan.zhihu.com/p/485646912 ================================================ FILE: iOS资料/iOS音视频开源框架WebRTC入门-AppRTCMobile.md ================================================ # iOS音视频开源框架WebRTC入门-AppRTCMobile ## 1、系列 [iOS音视频开源框架WebRTC入门-编译(前序-授人鱼不如授人以渔)](https://www.jianshu.com/p/435753014d47) [iOS音视频开源框架WebRTC入门-编译(后序-带WebRTC源码)](https://www.jianshu.com/p/2473b3ab34ba) [iOS音视频开源框架WebRTC入门-AppRTCMobile(WebRTC官网demo)](https://www.jianshu.com/p/f3e89919bd55) [iOS音视频开源框架WebRTC入门-简单应用](https://www.jianshu.com/p/67743301d14d) [iOS音视频开源框架WebRTC入门-本地/远端图像等比缩放](https://www.jianshu.com/p/f59b4bfd1e10) [iOS音视频开源框架WebRTC入门-外网通信](https://www.jianshu.com/p/ddbe5d90332f) ## 2、简介 `AppRTCMobile是WebRTC的一个官方 iOS demo` 来说说在[获取源码](https://www.jianshu.com/p/435753014d47)的基础上,如何去生成项目并运行项目👇 ## 3、生成项目 获取源码以后,根目录是 src, 切换至该目录 ```bash cd /Users/tianjinfeng/Desktop/iOS/WebRTC/webrtcbuilds-master/out/src ``` 编译生成 arm64架构的项目: ```rust gn gen out/iOS_64 --args='target_os="ios" target_cpu="arm64"' --ide=xcode ``` ![img](https:////upload-images.jianshu.io/upload_images/732408-0d8fbbda74979784.png?imageMogr2/auto-orient/strip|imageView2/2/w/1200/format/webp) image.png 执行完成以后在指定目录(out/iOS_64)中就有一个:`all.xcworkspace`: ![img](https:////upload-images.jianshu.io/upload_images/732408-8ee0d4ceb57ceb14.png?imageMogr2/auto-orient/strip|imageView2/2/w/1200/format/webp) image.png ## 4、运行项目 ![img](https:////upload-images.jianshu.io/upload_images/732408-819ccbf762f370e4.png?imageMogr2/auto-orient/strip|imageView2/2/w/1200/format/webp) image.png 直接运行即可: ![img](https:////upload-images.jianshu.io/upload_images/732408-735229a782ac95ba.png?imageMogr2/auto-orient/strip|imageView2/2/w/1200/format/webp) image.png 真机界面: ![img](https:////upload-images.jianshu.io/upload_images/732408-1a66abe88873959a.png?imageMogr2/auto-orient/strip|imageView2/2/w/665/format/webp) image.png 在两台真机(64位)上安装 AppRTCMobile,输入同一个 Room name 之后 Start call 就能互通了(`需要翻墙`) 在这过程中,我并没有遇到签名问题!!! 参考链接: [Webrtc笔记-运行demo到ios真机](https://link.jianshu.com?t=http://www.re2x.com/WebRTC-wiki/zh-CN/#!pages/iOS/Webrtc笔记-运行demo到ios真机.md) [Webrtc笔记-运行demo 遇到的签名问题]([http://www.re2x.com/WebRTC-wiki/zh-CN/#!pages/iOS/./Webrtc](https://link.jianshu.com?t=http://www.re2x.com/WebRTC-wiki/zh-CN/#!pages/iOS/./Webrtc)笔记-运行demo 遇到的签名问题.md) 原文链接:https://www.jianshu.com/p/f3e89919bd55 ================================================ FILE: iOS资料/iOS音视频的那些事儿:数据的采集和编码.md ================================================ # iOS音视频的那些事儿:数据的采集和编码 ## 1、AVFoundation简介 AVFoundation是苹果在iOS和OS X系统中用于处理基于时间的媒体数据的Objective-C框架. 供使用者来开发媒体类型的应用程序。 ![img](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2018/6/1/163b9ca795fb6eb9~tplv-t2oaga2asx-zoom-in-crop-mark:4536:0:0:0.image) 如果只是进行简单的视频录制,使用UIKit中的`UIImagePickerController`就可以了。如果需要播放视频,使用AVKit框架也足够了。但是如果需要进行视频文件的处理等更灵活等操作,就需要使用到AVFoundation等底层的框架了。 - CoreAudio : 处理所有音频事件.是由多个框架整合在一起的总称,为音频和MIDI内容的录制,播放和处理提供相应接口.甚至可以针对音频信号进行完全控制,并通过Audio Units来构建一些复杂的音频处理.有兴趣的可以单独了解一下这个框架. - Core Video : 是Mac OS和ios系统上针对数字视频所提供的普通模式。Core Video为其相对的Core Media提供图片缓存和缓存池支持,提供了一个能够对数字视频逐帧访问的接口。 - CoreMedia: 是AVFoundation所用到低层级媒体管道的一部分.提供音频样本和视频帧处理所需的低层级数据类型和接口. - CoreAnimation: 动画相关框架, 封装了支持OpenGL和OpenGL ES功能的ObjC各种类. AVFoundation可以利用CoreAnimation让开发者能够在视频的编辑和播放过程中添加动画和图片效果. ## 2、数据采集 ### 3.1 采集过程 为了管理从相机或者麦克风等这样的设备捕获到的信息,我们需要输入对象(input)和输出对象(output),并且使用一个会话(AVCaptureSession)来管理 input 和 output 之前的数据流: ![img](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2018/6/1/163b9ca8047af3ed~tplv-t2oaga2asx-zoom-in-crop-mark:4536:0:0:0.image) 通过单个 session,也可以管理多个 input 和 output 对象之间的数据流,从而得到视频、静态图像和预览视图。input 可以有一个或多个输入端口,output 也可以有一个或多个数据来源。 当添加 input 和 output 到 session 中时,session 会自动建立起一个连接(AVCaptureConnection)。我们可以使用这个 connection 来设置从 input 或者 从 output 得到的数据的有效性,也可以用来监控在音频信道中功率的平均值和峰值。 ![img](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2018/6/1/163b9ca7959426cb~tplv-t2oaga2asx-zoom-in-crop-mark:4536:0:0:0.image) #### 3.1.1 示例代码 仅录制音频 ``` let audioSettings = [AVFormatIDKey: kAudioFormatLinearPCM, // 格式 AVSampleRateKey: 44100, // 采样率 AVNumberOfChannelsKey: 1, // 声道数 AVLinearPCMBitDepthKey: 16] // 位深度 let url = URL(fileURLWithPath: "\(NSHomeDirectory())/tmp/audio.pcm") do { let recorder = try AVAudioRecorder(url: url, settings: audioSettings) recorder.record() } catch let error { print(error) } ``` 录制视频 ``` let session = AVCaptureSession() if session.canSetSessionPreset(.hd1280x720) { session.sessionPreset = .hd1280x720 } self.session = session // 添加视频源 let videoDevice = AVCaptureDevice.default(for: .video) if let device = videoDevice { do { let input = try AVCaptureDeviceInput(device: device) if session.canAddInput(input) { session.addInput(input) } } catch let error { print(error) } } // 添加音频源 let audioDevice = AVCaptureDevice.default(for: .audio) if let device = audioDevice { do { let input = try AVCaptureDeviceInput(device: device) if session.canAddInput(input) { session.addInput(input) } } catch let error { print(error) } } // 输出每一帧 let dataOutput = AVCaptureVideoDataOutput() dataOutput.videoSettings = [kCVPixelBufferPixelFormatTypeKey: kCVPixelFormatType_420YpCbCr8BiPlanarFullRange] as [String : Any] dataOutput.setSampleBufferDelegate(self, queue: DispatchQueue.main) if session.canAddOutput(dataOutput) { session.addOutput(dataOutput) } // 输出为文件 let fileOutput = AVCaptureMovieFileOutput() if session.canAddOutput(fileOutput) { session.addOutput(fileOutput) } let connection = fileOutput.connection(with: .video) if let conn = connection, conn.isVideoStabilizationSupported { conn.preferredVideoStabilizationMode = .auto } let url = URL(fileURLWithPath: "\(NSHomeDirectory())/tmp/movie.mov") // 开始录制 fileOutput.startRecording(to: url, recordingDelegate: self) let previewLayer = AVCaptureVideoPreviewLayer(session: session) previewLayer.frame = view.bounds view.layer.addSublayer(previewLayer) session.startRunning() ``` `AVCaptureMovieFileOutput`和`AVCaptureVideoDataOutput`的区别在于,前者是把采集到到数据直接写入到文件,而后者通过下面到代理方法将每一帧到图像数据发送过来,以便于进行一些处理。 ``` func captureOutput(_ output: AVCaptureOutput, didOutput sampleBuffer: CMSampleBuffer, from connection: AVCaptureConnection) { print(sampleBuffer) } ``` ### 3.2音频采集相关概念 音频的采集过程主要是通过设备将环境中的模拟信号转换成`PCM`编码的原始数据,然后编码压缩成`MP3`、`AAC`、`WMA`、`m4a`、`APE`、`FLAC`等格式。 #### 3.2.1PCM > PCM全称Pulse Code Modulation,中文名为脉冲编码调制。脉冲编码调制就是对模拟信号先抽样,再对样值幅度量化,编码的过程。 衡量一个音频文件的质量的一个重要指标是比特率(码率),单位为bps(bit per second),也就是单位时间内传输的比特数。 而影响比特率的因素有: 1. 采样率(Sample Rate):采样的频率,采样的频率越高,数据量就越大,音质就越高。 ![img](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2018/6/1/163b9ca7957dc61a~tplv-t2oaga2asx-zoom-in-crop-mark:4536:0:0:0.image) 1. 位深度(Bit Depth):表示每一个采样点所需要的数值的大小。可以是4bit、8bit、16bit等,位数越多,采样点表示的精度就越高,音质就越高,数据量也会成倍的增加。 2. 声道数(Number of Channels):由于音频的采集和播放是可以叠加的,因此,可以同时从多个音频源采集声音,并分别输出到不同的扬声器,故声道数一般表示声音录制时的音源数量或回放时相应的扬声器数量。声道数为 1 和 2 分别称为单声道和双声道。 根据上面的这些信息我们就可以计算出一个采样率为44100Hz,位深度为16bit,时长为3分钟的单声道CD歌曲的数据量为: `44100Hz x 16bit x 5 x 60s x 2 = 423360000bit = 52.9MB` 比特率为:`423360000bit / (5 x 60s) = 1441kbps` 这样的数据大小显然是不能接受的,所以才有了上面提到的`MP3`、`AAC`、`WMA`、`m4a`、`APE`、`FLAC`这些压缩格式的出现。 音频的压缩分为无损压缩和有损压缩,简单的来说,有损压缩就是通过删除一些已有数据中不太重要的数据来达到压缩目的;无损压缩就是通过优化排列方式来达到压缩目的。 `MP3`、`AAC`、`WMA`、`m4a`都属于有损压缩,假设把上面的歌曲压缩为码率128kpbs的普通音质的MP3,压缩比为`1441kpbs / 128kbps = 11`,因此压缩后的文件大小为`52.9MB / 11 = 5MB`。 `APE`、`FLAC`属于无损压缩,文件相对较大。 ### 3.3 图像采集相关概念 图像采集的图片结果组合成一组连续播放的动画,即构成视频中可肉眼观看的内容。图像的采集过程主要由摄像头等设备拍摄成 YUV 编码的原始数据,然后经过编码压缩成 H.264 等格式的数据分发出去。常见的视频封装格式有:MP4、3GP、AVI、MKV、WMV、MPG、VOB、FLV、SWF、MOV、RMVB 和 WebM 等。 图像数据质量的同样可以通过码率来进行衡量,而影响码率的因素主要有: 1. 图像的分辨率(Resolution):视频采集过程中的原始分辨率决定了视频整体的清晰度,分辨率越高,视频越清晰。常见的分辨率有`480p`、`720p`、`1080p`、`4k`。 2. 图像的格式:视频图像一般采用YUV格式存储原始的数据信息,而不是采用常见的RGB格式。 3. 帧率(Frame Rate):单位时间内捕获到到图像到数量,帧率越高,画面的流畅度就越高。一般的视频的帧率为30fps。延时摄影就是以远低于普通帧率的频率进行图像的采集然后以正常的帧率进行播放,就会有一种加速的效果。而高速摄影(慢动作)就是以远高于普通帧率的频率进行图像的采集,然后以正常的帧率进行播放,就会有一种变慢的效果。 4. 传输通道数:一般情况下都只有一个摄像头进行数据的采样,而随着VR和AR技术的发展,要拍摄一个360度的视频,就需要多个摄像头来同时进行采集了。 #### 3.3.1 图像的位深度 我们知道对于一个普通的不包含Alpha通道的位图图像而言,每一个像素点的数据都可以通过R、G、B三个分量数据来进行表示。每个数据用用多大的数值来表示就决定了这个图像的位深度,也即图像的色彩范围。假设我们用1个字节来表示一个分量的数值,即24位真彩色,那么总共就有`2^8 x 2^8 x 2^8 = 1600万`种颜色,而如果对RGB分别用3bit、3bit、2bit也即8位色来表示的话只有`2^3 x 2^3 x 2^2 = 256`种颜色。 ![img](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2018/6/1/163b9ca80e8204a3~tplv-t2oaga2asx-zoom-in-crop-mark:4536:0:0:0.image) 24位色的图片 ![img](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2018/6/1/163b9ca821d330d7~tplv-t2oaga2asx-zoom-in-crop-mark:4536:0:0:0.image) 8位色的图片 #### 3.3.2 YUV格式 与我们所熟知的RGB类似,YUV也是一种颜色的编码方法,主要用于视频领域。它将亮度信息(Y)与色彩信息(UV也称CbCr)分离,没有UV信息一样可以显示完整的图像,只不过是黑白的。这样的设计很好地解决了彩色电视机与黑白电视的兼容问题。并且,YUV不像RGB那样要求三个独立的视频信号同时传输,所以用YUV方式传送占用极少的频宽。我们人眼对于亮度的感知比对色彩的感知更为强烈,所以可以通过色彩信息(UV)的采样来减少数据量。 ![img](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2018/6/1/163b9ca84a4c3779~tplv-t2oaga2asx-zoom-in-crop-mark:4536:0:0:0.image) 色彩信息(UV) ![img](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2018/6/1/163b9ca85482d7da~tplv-t2oaga2asx-zoom-in-crop-mark:4536:0:0:0.image) 亮度和色度的对比 YUV码流的存储格式其实与其采样的方式密切相关,主流的采样方式有三种,YUV4:4:4、YUV4:2:2、YUV4:2:0,如下图所示: ![img](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2018/6/1/163b9ca86742912c~tplv-t2oaga2asx-zoom-in-crop-mark:4536:0:0:0.image) 对于YUV 4:4:4采样,每一个Y分量对应一组UV分量 对于YUV 4:2:2采样,每两个Y分量公用一组UV分量 对于YUV 4:2:0采样,每四个Y分量公用一组UV分量 YUV格式有两大类:planar和packed。 对于planar的YUV格式,先连续存储所有像素点的Y,紧接着存储所有像素点的U,随后是所有像素点的V。 对于packed的YUV格式,每个像素点的Y,U,V是连续交叉存储的。 一般移动平台视频录制使用的是YUV 4:2:0,而根据各个分量的排列顺序不同又分为`YV12`、`I420(YU12)`、`NV12`、`NV21`。 `YV12`和`I420`也称为YUV420P,即planar平面格式。YV12和I420的区别仅在与UV顺序的不同。顾名思义,YV12中Y平面后面紧跟的是V平面,然后是U平面,12表示它的位深度为12,也就是一个像素占12bit。而I420(YU12)刚好相反。 `NV12`和`NV21`都属于YUV420SP,即Y分量平面格式,UV分量打包格式。也即先存储Y平面,后面是UV分量交错存储。iOS平台使用的是`NV12`格式。 ​ `I420: YYYYYYYY UU VV => YUV420P YV12: YYYYYYYY VV UU => YUV420P NV12: YYYYYYYY UVUV => YUV420SP NV21: YYYYYYYY VUVU => YUV420SP 复制代码` I420(Planar)的单帧结构示意图如下: ![img](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2018/6/1/163b9ca89c1aeeb8~tplv-t2oaga2asx-zoom-in-crop-mark:4536:0:0:0.image) NV12(Planar)的单帧结构示意图如下: ![img](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2018/6/1/163b9ca8b133d70e~tplv-t2oaga2asx-zoom-in-crop-mark:4536:0:0:0.image) 根据上图所示: 一张分辨率为6 x 4的图片采用YUV420的方式存储所占的空间为`6 x 4 + 6 + 6 = 36`个字节,而如果采用RGB的方式存储就需要占用`6 x 4 x 3 = 72`个字节,数据量减少了一半。 如果以YUV420的方式编码一部720p、帧率为30fps、时长两小时的电影所需要的空间为 ``` 12bit x 1280 x 720 x 30 x 120 x 60 = 2.38 x 10^12bit = 298.6GB ``` 这个大小显然是不能接受的,经过编码我们可以把它压缩到2G左右,而画面到清晰度不会有多少损失。 ### 3.4 视频编码 #### 3.4.1 基本原理 为什么视频文件的压缩比可以达到100:1甚至更高?核心的思想就是去除冗余信息。 视频文件中的冗余信息分为以下几类: 1. 空间冗余:每一帧图像相邻像素之间有较强的关联性 2. 时间冗余:视频相邻帧之间的内容相似 3. 编码冗余:不同像素值出现的概率不同 4. 视觉冗余:人眼对于某些细节不敏感 5. 知识冗余:规规律性的结构可由先验知识和背景知识得到 对于视频文件最简单的压缩方式就是对每一帧图像进行压缩,叫做帧内压缩。有一种比较古老的 MJPEG 编码就是这种编码方式。可以理解为把视频的每一帧当作一张图片,然后按照JPEG的方式来进行压缩。这样的压缩方式只处理了空间冗余信息,离理想的压缩比还差得远。 视频的图像的相邻帧之间具有很大的相似性,因此去除时间冗余信息对于提高压缩比有很大的提升空间。比如说一些高级的编码器可以采用帧间编码,简单点说就是通过搜索算法选定了帧上的某些区域,然后通过计算当前帧和前后参考帧的向量差进行编码的一种形式,通过下面两个图 2 连续帧我们可以看到,滑雪的同学是向前位移的,但实际上是雪景在向后位移。后面的帧就可以参考前面的帧进行编码,这样后面的帧编码的结果就非常小,压缩比很高。 ![img](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2018/6/1/163b9ca8df2f0176~tplv-t2oaga2asx-zoom-in-crop-mark:4536:0:0:0.image) 去除其他的冗余信息这里就不多说了。 #### 3.4.2 常用编码器 **H.264** > H.264/AVC 是现在使用最广泛的编码标准。与旧标准相比,它能够在更低带宽下提供优质视频(换言之,只有 MPEG-2,H.263 或 MPEG-4 第 2 部分的一半带宽或更少),也不增加太多设计复杂度使得无法实现或实现成本过高。另一目的是提供足够的灵活性以在各种应用、网络及系统中使用,包括高、低带宽,高、低视频分辨率,广播,DVD 存储,RTP/IP 网络,以及 ITU-T 多媒体电话系统。 **H.265** 2017年6月6日凌晨召开WWDC 2017大会上苹果在iOS11上推出了HEVC,用来取代H.264 > 高效率视频编码(High Efficiency Video Coding,简称HEVC)是一种视频压缩标准,被视为是 ITU-T H.264/MPEG-4 AVC 标准的继任者。2004 年开始由 ISO/IEC Moving Picture Experts Group(MPEG)和 ITU-T Video Coding Experts Group(VCEG)作为 ISO/IEC 23008-2 MPEG-H Part 2 或称作 ITU-T H.265 开始制定。第一版的 HEVC/H.265 视频压缩标准在 2013 年 4 月 13 日被接受为国际电信联盟(ITU-T)的正式标准。HEVC 被认为不仅提升视频质量,同时也能达到 H.264/MPEG-4 AVC 两倍之压缩率(等同于同样画面质量下比特率减少了 50%),可支持 4K 分辨率甚至到超高清电视(UHDTV),最高分辨率可达到 8192×4320(8K分辨率)。 #### 3.4.3 硬件编解码和软件编解码 iOS8开始,苹果通过`Video ToolBox`开放了系统的硬件编解码能力。在这之前,开发者基本上都是使用的是一个叫做[FFmpeg](https://link.juejin.cn?target=https%3A%2F%2Fwww.ffmpeg.org%2F)的多媒体库,利用CPU做视频的编解码,俗称软解码。 > FFmpeg 是一个自由软件,可以运行音频和视频多种格式的录影、转换、流功能,包含了 libavcodec ——这是一个用于多个项目中音频和视频的解码器库,以及 libavformat —— 一个音频与视频格式转换库。 #### 3.4.4 视频容器(格式) 所谓容器,就是把编码器生成的多媒体内容(视频,音频,字幕,章节信息等)混合封装在一起的标准。容器使得不同多媒体内容同步播放变得很简单,而容器的另一个作用就是为多媒体内容提供索引,也就是说如果没有容器存在的话一部影片你只能从一开始看到最后,不能拖动进度条(当然这种情况下有的播放器会花比较长的时间临时创建索引),而且如果你不自己去手动另外载入音频就没有声音。 ![img](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2018/6/1/163b9ca8ef9f8a95~tplv-t2oaga2asx-zoom-in-crop-mark:4536:0:0:0.image) MOV文件的结构 常见的视频容器格式有: - AVI 格式(后缀为 .AVI): 它的英文全称为 Audio Video Interleaved ,即音频视频交错格式。它于 1992 年被 Microsoft 公司推出。 这种视频格式的优点是图像质量好。由于无损AVI可以保存 alpha 通道,经常被我们使用。缺点太多,体积过于庞大,而且更加糟糕的是压缩标准不统一,最普遍的现象就是高版本 Windows 媒体播放器播放不了采用早期编码编辑的AVI格式视频,而低版本 Windows 媒体播放器又播放不了采用最新编码编辑的AVI格式视频,所以我们在进行一些AVI格式的视频播放时常会出现由于视频编码问题而造成的视频不能播放或即使能够播放,但存在不能调节播放进度和播放时只有声音没有图像等一些莫名其妙的问题。 - QuickTime File Format 格式(后缀为 .MOV): 美国Apple公司开发的一种视频格式,默认的播放器是苹果的QuickTime。 具有较高的压缩比率和较完美的视频清晰度等特点,并可以保存alpha通道。 - MPEG 格式(文件后缀可以是 .MPG .MPEG .MPE .DAT .VOB .ASF .3GP .MP4等) : 它的英文全称为 Moving Picture Experts Group,即运动图像专家组格式,该专家组建于1988年,专门负责为 CD 建立视频和音频标准,而成员都是为视频、音频及系统领域的技术专家。 MPEG 文件格式是运动图像压缩算法的国际标准。MPEG 格式目前有三个压缩标准,分别是 MPEG-1、MPEG-2、和MPEG-4 。MPEG-1、MPEG-2 目前已经使用较少,着重介绍 MPEG-4,其制定于1998年,MPEG-4 是为了播放流式媒体的高质量视频而专门设计的,以求使用最少的数据获得最佳的图像质量。目前 MPEG-4 最有吸引力的地方在于它能够保存接近于DVD画质的小体积视频文件。 - Real Video 格式(后缀为 .RM .RMVB): Real Networks 公司所制定的音频视频压缩规范称为Real Media。 用户可以使用 RealPlayer 根据不同的网络传输速率制定出不同的压缩比率,从而实现在低速率的网络上进行影像数据实时传送和播放。RMVB 格式:这是一种由RM视频格式升级延伸出的新视频格式,当然性能上有很大的提升。RMVB 视频也是有着较明显的优势,一部大小为700MB左右的 DVD 影片,如果将其转录成同样品质的 RMVB 格式,其个头最多也就 400MB 左右。大家可能注意到了,以前在网络上下载电影和视频的时候,经常接触到 RMVB 格式,但是随着时代的发展这种格式被越来越多的更优秀的格式替代,著名的人人影视字幕组在2013年已经宣布不再压制 RMVB 格式视频。 - Matroska 格式(后缀位 .MKV):是一种新的多媒体封装格式,这个封装格式可把多种不同编码的视频及16条或以上不同格式的音频和语言不同的字幕封装到一个Matroska Media档内。它也是其中一种开放源代码的多媒体封装格式。Matroska同时还可以提供非常好的交互功能,而且比MPEG更方便、强大。 原文链接:https://juejin.cn/post/6844903615342051336 ================================================ FILE: iOS资料/iOS音视频:OpenGL常用术语介绍.md ================================================ # iOS音视频:OpenGL常用术语介绍 ## 1、前言 【**iOS音视频**】是个系列,里面会记录一些博主在`iOS音视频`方面的学习笔记、踩到的坑,以便温故而知新。 > 此系列文章包括但不限于: > > 1. [iOS音视频:OpenGL常用术语介绍](https://juejin.cn/post/6940900616881307679) > 2. ... 本文是这个系列的第1篇文章,主要目的是帮助大家快速了解`OpenGL`,下面进入正文。 ## 2、OpenGL简介 ### 2.1 OpenGL是什么 `OpenGL`(Open Graphics Library,译为 `开放图形库` 或 `开放式图形库`):是用于 **渲染** 2D、3D矢量图形的跨语言、跨平台的应用程序编程接口库。 它是一种`图形API库`,它把计算机的资源抽象成一个个`OpenGL对象`,对这些资源的操作抽象成一个个`OpenGL指令`。由于它只提供渲染功能(操作的是`GPU芯片`),与窗口系统、音频、打印、键盘/鼠标或其他输入设备无关,所以具备跨平台性(主要运行在PC端,如Mac OS、Linux、Windows等)。 与OpenGL类似的图形API库还有`OpenGL ES`、`Metal`、`DirectX`等,它们之间的主要区别是: - `OpenGL ES`(OpenGL for Embedded Systems):是`OpenGL`的子集,针对手机、PDA和游戏主机等嵌入式设备而设计,去除了许多不必要的、性能较低的API。 - `Metal`:是苹果公司推出的平台技术,主要运行于苹果各大平台上(macOS、iOS、tvOS)。该技术专为多线程而设计,并提供各种出色工具将所有素材整合在XCode中。经过优化,`Metal`使`CPU`和`GPU`能够协同工作来实现最优性能(它能够为3D图像提高10倍的渲染性能)。 - `DirectX`:是微软公司创建的多媒体编程接口,由很多API组成(不仅仅是图形API),仅限于Windows平台上使用(目前不支持Windows以外的平台)。按照性质可分为四大部分,分别是显示部分、声音部分、输⼊部分和网络部分。 > 由于博主主要从事iOS开发,所以`DirectX`在此系列文章中将不做赘述。 ### 2.2 OpenGL解决什么问题 作为图形API库,`OpenGL`、`OpenGL ES`、`Metal`在任何项目中解决问题的本质就是利用`GPU芯片`来高效渲染图形图像。使用这些图形API库也是iOS开发者唯一接近GPU的方式。 因此,图形API库常常被用在下述场景中: - 游戏开发中,对游戏场景的渲染 - 音视频开发中,对视频解码后的数据渲染,给视频加滤镜处理等 - 地图开发中,对地图数据的渲染 - 动画中,实现动画的绘制 - 航空航天、医疗行业等等 ### 2.3 关于选择的问题 苹果于`WWDC 2014`上提出`Metal`,但直到`WWDC 2018`年,苹果才完成系统内部从`OpenGL ES`到`Metal`的过渡,同时宣布在苹果设备上(`macOS Mojave`、`iOS 12`、`tvOS 12`)弃用`OpenGL/OpenGL ES/OpenCL`。从事图形API工作的开发者需要从自身角度考虑由哪个入门,可以从下面几方面综合考虑: - `OpenGL/OpenGL ES`具备跨平台性,而`Metal`仅限于苹果平台。 - 苹果自己的系统从`OpenGL/OpenGL ES`迁移到`Metal`花费了大量时间(4年左右),针对的是苹果内部系统底层API依赖,`OpenGL/OpenGL ES`由此变成了第三方图形API库。 - 目前大多数`OpenGL/OpenGL ES`项目组很庞大(如百度地图、高德地图、大部分音视频项目组),未完成往`Metal`的迁移工作。此时仅仅会`Metal`是不够的。 **所谓艺多不压身,沿着 `OpenGL` -> `OpenGL ES` -> `Metal` 的路线全部掌握也不失为一种选择。** ## 3、 OpenGL常用术语介绍 ### 3.1 OpenGL状态机 状态机是理论上的一种机器,它描述了一个对象在其生命周期内所经历的各种状态,状态间的转变,发生转变的原因、条件及转变中所执行的活动。或者说,状态机是一种行为,说明对象在其生命周期中响应事件所经历的状态序列以及对那些状态事件的响应。因此具有以下特点: - 有记忆功能,能记住其当前的状态; - 可以接收输⼊,根据输⼊的内容和⾃己的原先状态,修改⾃己当前状态,并且可以有对应输出; - 当进⼊某个特殊状态如停机状态的时候,将不再接收输⼊,停⽌⼯作。 `OpenGL`本身就是一个庞大的状态机,它同样: - 可以记录自己的状态(如当前使用的颜色,是否开启了混合功能等); - `OpenGL`可以接收输入(当调用`OpenGL`函数的时候,实际上可以看成`OpenGL`在接收我们的输入),根据输入的内容和自己的状态,修改自己的状态,并且可以得到输出(比如我们调用`glColor3f`,则`OpenGL`接收到这个输入后会修改自己的当前颜色这个状态;我们调用`glRectf`,则`OpenGL`会输出一个矩形); - `OpenGL`可以进入停止状态,不再接收输入。在程序退出前,`OpenGL`总会先停止工作的。 > 需要注意的是,它每一次状态改变都是全局的,因此在完成某状态下的功能后,需要把状态关闭/切换回去。 如可以使用`glColor函数`来选择一种颜色,以后绘制的所有物体都是这种颜色,除非再次使用`glColor函数`重新设定;同理,可以使用`glTexCoord函数`来设置一个纹理坐标,以后绘制的所有物体都是采用这种纹理坐标,除非再次使用`glTexCoord函数`重新设置。 总的来说,**`OpenGL`是一个状态机,它保持自身的状态,除非用户输入一条指令让它改变状态。** 例如: ```c // 获取是否深度测试/混合 glIsEnabled(GL_DEPTH_TEST); glIsEnabled(GL_BLEND); // 开启/关闭深度测试 glEnable(GL_DEPTH_TEST); glDisable(GL_DEPTH_TEST); // 开启/关闭混合 glEnable(GL_BLEND); glDisable(GL_BLEND); 复制代码 ``` ### 3.2 OpenGL上下文 `OpenGL`是面向过程的,它在渲染的时候需要一个`Context`来记录渲染需要的所有信息和状态,也就是`OpenGL上下文`。应用程序在调用任何OpenGL的指令之前,都需要首先创建一个`OpenGL上下文`。 - `OpenGL上下文`和`OpenGL状态机`的联系是紧密的,可以认为`OpenGL上下文`就是一组`OpenGL状态机`。 - `OpenGL`采用了`Client-Server`模式,`GPU`相当于一台服务器,可对应多个客户端即上下文,而一个客户端维护着一组状态机。大部分`OpenGL指令`都是异步的,不是立即执行,只是上下文向服务器发送了一些命令(当然也有一些API可实现同步功能)。 - `OpenGL上下文`是一个线程私有(thread-local)的变量,也就是说如果我们在线程中绘制,那么需要分别为每个线程指定一个上下文的,而且多个线程不能同时指定同一个上下文。 - 由于`OpenGL上下文`是一组庞大的`OpenGL状态机`,切换上下文往往会产生较大的开销,但是不同的绘制模块,可能需要使用完全独立的状态管理。因此,可以在应用程序中分别创建多个不同的上下文,在不同线程中使用不同的上下文,上下文之间共享纹理、缓冲区等资源。这样的方案,会比反复切换上下文,或者大量修改渲染状态更加合理高效。 ### 3.3 图元 `图元`(Primitive),是基本图形元素的简称,在`OpenGL/OpenGL ES`中,任何图像都是由图元组成。 - `OpenGL`的图元:点、线段、三角形、四边形、多边形 - `OpenGL ES`的图元:点、线段、三角形 ### 3.4 顶点数组和顶点缓冲区 在绘制图像时,图像的顶点位置数据就是`顶点数据`。 在调⽤`OpenGL`绘制方法时, - 如果顶点数据是由内存传⼊的,即通常是以数组的形式把顶点数据存储在一块内存中,这个数组被称为`顶点数组`(Vertex Array); - 性能更高的做法是,提前分配⼀块显存,将顶点数据预先存入到显存当中,这部分的显存,就被称为`顶点缓冲区`(Vertex Buffer)。 ### 3.5 渲染(Rendering) 在`OpenGL`中,任何事物都是处于3D空间的,而屏幕/窗口显示的是2D。将原始图形/图像数据转换成3D空间图像,并最终显示在2D屏幕/窗口,这个操作就是`渲染`(Rendering)。 渲染主要有两大流程,分别是: - `顶点渲染`:把顶点数据通过变换、过滤、插值等系列操作形成最终形状的过程。 - `像素渲染`:在形状中填充色彩。在这个过程中,被填充的色彩可以来自于顶点颜色、纹理甚至是通过某些数值计算出来的色彩(如光照)。 ### 3.6 管线 `图形渲染管线`(Graphics Pipeline),简称`管线`,描述的是渲染图形的过程。渲染图形并非是一蹴而就的,它的整个过程又会经历一个个阶段,类似于工厂的流水线作业。 管线是个抽象的概念,之所以称之为管线是因为显卡在处理数据的时候是按照一个固定的顺序来的,⽽且严格按照这个顺序(就像⽔从一根管⼦的⼀端流到另⼀端,这个顺序是不能打破的)。 管线可以分为几个阶段,每个阶段将会把前一个阶段的输出作为输入。所有这些阶段都是高度专门化的(它们都有一个特定的函数),并且很容易并行执行。 管线可分为 `固定管线` 和 `可编程管线`: - `固定管线`是固化的一个渲染流程,只需要开发者在`CPU`端输入渲染所需要的参数/数据,并指定特定的开关,调用函数就能完成渲染操作。它不需要也不允许开发者去自定义渲染的具体逻辑。 - `可编程管线`是必须由开发者实现渲染逻辑,否则无法渲染出最终的图像。开发者可以根据自己的具体需要来编写顶点渲染和像素渲染的具体逻辑,可最大程度的简化渲染管线的逻辑以提高渲染效率,也可自己实现特定的算法和逻辑来渲染出固定管线无法渲染的效果。具有很高的可定制性,但同时也对开发者提出了更高的要求。 ### 3.7 着色器 `着色器`(Shader)是运行在`GPU`上的程序,用于实现实现渲染的,这些小程序为管线的某个特定部分而运行(把输入转化为输出)。`OpenGL`在实际调⽤绘制函数之前,还需要指定⼀个着⾊器程序。 - 着色器只是一种把输入转化为输出的程序,且是一种非常独立的程序,因为它们之间不能相互通信,它们之间唯一的沟通只有通过输入和输出。 - 常见的着色器主要有`顶点着色器`、`片元着色器`这两种,当然也有一些其他着色器(如`几何着色器`、`曲面细分着色器`等),只是没前两种常用(直至`OpenGL ES 3.0`,可编程的着色器也只有`顶点着色器`和`片元着色器`这两种)。 #### 3.7.1 顶点着色器 `顶点着色器`(Vertex Shader)是用来操作顶点数据的(旋转、平移、投影等)。顶点着色器是逐顶点运算的程序,也就是说**每个顶点数据都会执行⼀次顶点着⾊器,当然这是并行的,并且顶点着⾊器运算过程中⽆法访问其他顶点的数据**。 #### 3.7.2 片元着色器 `片元着色器`(Fragment Shader)是用于计算每个像素填充颜色的程序。它是逐像素运算的程序,即**每个像素都会执行一次片元着色器,当然这也是并行的、独立的。** 思考:为什么`OpenGL`使用`GPU`而不是`CPU`? > 有的书籍把`片元着色器`叫做`像素着色器`(Pixel Shader),或者`片段着色器`,开发者只需要知道这3者是同一个东西即可(只是叫法不同,另外,`片元着色器`在`Metal`里叫做`片元函数`)。 #### 3.7.3 GLSL `GLSL(OpenGL Shading Language)`是编写着色器的语言,这是一种类C的语言。`GLSL`是为图形计算量身定制的,它包含一些针对向量和矩阵操作的有用特性。 #### 3.7.4 着色器的渲染流程 着色器也是会经过编译、链接等步骤,并最终生成`着色器程序`(glProgram)的,它必定同时包含`顶点着色器`和`片元着色器`的运算逻辑,其他着色器则是可选的(如细分着色器)。 简单介绍一下着色器的渲染流程,大致如下图所示: ![img](https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/d9dcf688ed984fc7bc242361ed263329~tplv-k3u1fbpfcp-zoom-in-crop-mark:4536:0:0:0.awebp) 说明: 1. 在`OpenGL`进行渲染的时候,首先由顶点着色器对顶点数据进行运算,再经过图元装配,将顶点转化为图元 2. 接着就是光栅化处理,图元数据由此转换为栅格化数据 3. 最后,栅格化数据经由片元着色器运算(逐像素,并决定像素的填充色),渲染成型。 > 注意:这里只是个大致的流程(以着色器的视角)。 ### 3.8 光栅化 `光栅化`(Rasterization)是把顶点数据转换为片元的过程,具有将图转化为一个个栅格组成的图象的作用,特点是每个元素对应帧缓冲区中的一像素。 - 光栅化其实是一种将 ``` 几何图元 ``` 变为 ``` 二维图像 ``` 的过程。该过程包含了两部分的工作,光栅化过程产生的是 ``` 片元 ``` - 第一部分工作:决定窗口坐标中的哪些整型栅格区域被基本图元占用; - 第二部分工作:分配一个颜色值和一个深度值到各个区域。 - 光栅化接收的输入是`几何图元`,其输出的是`像素`(参考着色器渲染流程),所以也可以通俗地理解成`像素化` ### 3.9 纹理 `纹理`可以理解为图片(实质上是`位图`),图像渲染时经常需要填充图片。这里的图片其实就是纹理,在`OpenGL`中,我们更喜欢称之为`纹理`。 - 常见图像文件格式(BMP,TGA,JPG,GIF,PNG) - 常见纹理格式(R5G6B5,A4R4G4B4,A1R5G5B5,R8G8B8, A8R8G8B8等) ### 3.10 混合 `混合`(Blending)是把某一像素位置原来的颜色和将要画上去的颜色,通过某种方式(混合算法)混在一起,从而实现特殊的效果。简单理解就是把两种/多种颜色混合在一起。 - 混合的算法可以通过`OpenGL`的函数进⾏指定。但是`OpenGL`提供的混合算法是有限的,如果需要更加复杂的混合算法,⼀般可以通过`片元着⾊器`实现,当然性能会⽐原⽣的混合算法差一些。 ### 3.11 矩阵 在`OpenGL`中,矩阵常常被用来进行辅助运算,如: - `变换矩阵`(Transformation)用于图形的平移、缩放、旋转变换; - `投影矩阵`(Projection)用于将3D坐标转换为二维屏幕坐标,实际线条也将在二维坐标下进行绘制。 等。 ### 3.12 帧缓存 `帧缓冲存储器`(Frame Buffer),简称`帧缓存`或`显存`,它是接收渲染结果的缓冲区,为`GPU`指定存储渲染结果的区域。 关于`帧缓存`,说明如下: - 全部的图形图像都共享内存中同一个`帧缓存`。 - `帧缓存`是实时的:`帧缓存`中存储的是一帧一帧的、渲染完成的图像,显卡会不停的刷新`帧缓存`, 这每一帧如果不捕获的话,则会被丢弃。 - `帧缓存`的每一帧都是一个显性的信息:假设分辨率是`750 x 1334`,则每一帧保存的是`750 x 1334`个像素点(每一个像素点都有颜色值)。 > `缓冲区`(Buffer)这个中文译意源自当计算机的高速部件与低速部件通讯时,必须将高速部件的输出暂存到某处,以保证高速部件与低速部件相吻合。后来这个意思被扩展了,成为“临时存贮区”的意思。 ## 4、 思考: ### 4.1 Why GPU? 思考:为什么`OpenGL`使用`GPU`而不是`CPU`? 解析:解答这个问题要理解`GPU`(中央处理器)和`CPU`(图形处理器)的区别。首先看一下这两者的设计,大致如下: ![img](https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/3c364d4fc677441caee50e83d057016b~tplv-k3u1fbpfcp-zoom-in-crop-mark:4536:0:0:0.awebp) ![img](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/90066de21339491a88a6cb04924d188e~tplv-k3u1fbpfcp-zoom-in-crop-mark:4536:0:0:0.awebp) > 图片来自 **Nvidia CUDA** 文档,其中绿色的是计算单元,橙红色的是存储单元,黄色的是控制单元。 从图中不难看出, - ``` CPU ``` 具有 - 强大的算术运算单元(ALU),它可以在很少的时钟周期内完成算术运算; - 大的缓存,这可以降低延时; - 复杂的逻辑控制单元,当程序含有多个分支的时候,它通过提供分支预测的能力来降低延时。 - ``` GPU ``` 具有 - 很多的算术运算单元,计算量大; - 很小的缓存,缓存的目的不是保存后面需要访问的数据,而是为`Thread`服务,这点和`CPU`不同。如果有很多线程需要访问同一个数据,缓存会合并这些访问,然后再去访问`DRAM`(因为需要访问的数据保存在`DRAM`中而不是`cache`里面),获取数据后`cache`会转发这个数据给对应的线程,也就是说,`GPU`的小缓存充当的是数据转发的角色。 - 简单的控制单元,主要是把多个访问合并成少的访问。 此外,`CPU`虽然号称多核,但总数没有超过两位数;而`GPU`的核数远超`GPU`,被称为众核。 总的来说,**`CPU`擅长逻辑控制以及串行的运算,而`GPU`则擅长大规模的并发计算**。在`OpenGL`渲染图形图像的时候,往往伴随着海量的计算(如对每一个顶点进行同样的坐标变换,对每一个片元计算颜色值等等),因此,`OpenGL`使用`GPU`而不是`CPU`。 ## 5、友情链接 - [OpenGL专有名词解析(夹杂通俗举例和个人理解)](https://link.juejin.cn?target=https%3A%2F%2Fwww.jianshu.com%2Fp%2Fa7096a6c16a7) - [帧缓存](https://link.juejin.cn?target=https%3A%2F%2Fbaike.baidu.com%2Fitem%2F%E5%B8%A7%E7%BC%93%E5%AD%98%2F5725254) - [CPU和GPU的区别](https://link.juejin.cn?target=https%3A%2F%2Fwww.zhihu.com%2Fquestion%2F19903344%2Fanswer%2F96081382) 原文链接:https://juejin.cn/post/6940900616881307679 ================================================ FILE: iOS资料/iOS音频录制及合成,以及优化处理 ================================================ 这里主要介绍**AVFundation**框架中的组件,讲解音频的录制以及合成,还有一些注意事项。 ## 1、音频的采集 音频既能与图像结合组合成视频数据,也能以纯音频的方式采集播放,后者应用场景多为在线电台和语音电台。音频的采集过程主要是通过设备将环境中的模拟信号采集成[PCM编码](https://link.jianshu.com?t=http://baike.baidu.com/link?url=kHPxW_fEZPhm6aor2AmRQZ06SYvceODR0O6N-3weiuJKT-JrxWXzhdJVJXlELa8Jg29l9fBPJWMcbJaqkymtFBEI93VgjGlg-Fs9kUe1KrCfkr5NoYWTw3qe6npYOIte)的原始数据,然后压缩成mp3等格式分发出去。 音频采集的主要难点:延迟敏感、卡顿敏感、回声消除、噪声消除、静音监测及各种混音算法等。 音频采集的主要技术参数: **采样率** — 采样就是把模拟信号数字化的过程。[采样频率](https://link.jianshu.com/?t=http://baike.baidu.com/link?url=RLXNY4GGdEtR3KNbz6F84Kw-f8aPW97dV-YcWRvLdA9sRkYChIzvxXP9LYLAzLmjWpPpiAxavyqmzvDFU1D_aECudvi6ZhbT5Z-V_C7uiUfulcUhDoWlHE2WJ7zyKgIPgcSZxYScpcGe1t--dpV3k_cyBf6G0dVvlVAUvKt7xcINsWTO2UOx0ry120OOhHnQEn08Cuh6N1hwhKiAvvrEy_)越高,记录这个音频信号所用的数据量也就越大,对应的得到音频的质量也就越高。 **位宽** — 每个采样点的大小需要用一个数值来表示,这个大小为4bit、8bit、16bit、32bit等,这个数值越大,表示的音频的质量越好,对应的数据量也就越大。 **声道数** — 音频录制时音源的数量或者回放时扬声器的数量。比如,单身道,双声道。 **音频帧** — 音频是流式的,没有明确的一帧一帧的概念,它是根据编解码器和具体的引用需求来定的。 如何计算一帧音频帧的大小。假设某音频信号是采样率为 8kHz、双通道、位宽为 16bit,20ms 一帧,则一帧音频数据的大小为: 8000 x 2 x 16bit x 0.02s = 5120 bit = 640 byte ## 2.参考资料 [SCRecorder](https://link.jianshu.com/?t=https://github.com/rFlex/SCRecorder),主要用于录制视频,音频也可以录制。短视频录制,将录制好的视频保存为多个片段,可以选择需要的视频片段合成。 [EZAudio](https://link.jianshu.com/?t=https://github.com/syedhali/EZAudio) 主要用于根据音量显示波形图。 ## 3.在mac os下使用FFmpeg 1、在mac os下使用ffmpeg比较简单,可以直接使用命令行来操作。首先安装ffmpeg,这里默认系统已经安装好brew,只需要在终端上输入: > brew install ffmpeg 2、安装结束后,尝试以下命令: > ffmpeg -i input.mp4 output.avi 如果能顺利转换,表明安装成功 3、万能的编译脚本,歪果仁的脚本编译[传送门](https://link.jianshu.com/?t=https://github.com/kewlbear/FFmpeg-iOS-build-script])。 4、第三步的脚本则会自动从github中把ffmpeg源码下到本地并开始编译。 > ./build-ffmpeg.sh 【iOS开发】iOS下使用FFMPEG的一些总结 其中,ffmpeg-2.5.3是源码,FFmpeg-iOS是编译出来的库,里面有我们需要的.a静态库,一共有7个。 > lipo -info libavcodec.a 查看.a包支持的架构,这几个包都支持了armv7 armv7s i386 x86_64 arm64这几个架构 5、在Xcode中引入FFmpeg library库 #### > \#include "avformat.h" 添加一个空的类,把执行文件.m后缀改为.mm,开启混编模式。 添加相应的framework,包括avfoundation和coremedia。 运行工程,如果没有报错,则表明编译成功。 有可能出现的错误: 关于ffmpeg的libavcodec/avcodec.h' file not found 解决办法: > 1. 因为添加ffmpeg后,会导致三方的Library Search Paths路径不对,需要自己添加路径 > 2. 如果自己添加的路径是找不到。 检查include文件路径,并重新修改$(PROJECT_DIR)/....../FFmpeg-iOS/include 原文:https://www.jianshu.com/p/0b1c98a28fd4 ================================================ FILE: iOS资料/iOS音频视频开发.md ================================================ # iOS音频视频开发 ## 1.直播的基本种类和架构 泛娱乐化直播 主播向信令服务器发起请求,创建一个房间 将视频信息上传到流媒体云 观众向信令服务器发起请求,进入一个房间 观众从流媒体云获取视频数据 实时互动直播架构 tcp:发送 确认 超时 重发 udp:只管发,不管收不收到 但是由于实时互动直播中,强调实时性,不一定所有的包都要受到,所以会使用udp传输协议。 ## 1.CDN网络 解决用户访问网络资源慢而出现的 边缘节点:用户从边缘节点上获取数据 二级节点:主干网节点,主要用于缓存,减轻源站压力 源站:CP(内容提供方)将内容放到源站 ![在这里插入图片描述](https://img-blog.csdnimg.cn/20190426172340128.png?x-oss-process=image/watermark, ,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L2dvbGRmaXNoMw==,size_16,color_FFFFFF,t_70) ## 3.音视频基本概念介绍 数字音频概念介绍 将模拟信号数字化,需要三个步骤: 采样:在时间轴上对信号进行数字化。 量化:在幅度上对每个采样进行数字化,如使用16bit的二进制信号来表示声音的一个采样,16bit所表示的范围是 [-32768,32767],共有65536个可能取值。 编码:对采样结果进行储存。 音频裸数据格式——脉冲编码调制数据PCM,描述一段PCM数据一般需要以下几个概念: 量化格式:如在CD中,为16bit 采样率:在CD中,为44100 声道数:在CD中为2 还有一个概念用于描述它的大小 —— 数据比特率,即1秒时间内的比特数目,按之前的数字,数据比特率计算为: 4410 * 16 * 2 = 1378.125 kbps 这样算下来,一分钟里,这类CD音质的数据需要占 10.09MB,太大了。 如果在网络上,传输这段音频,就需要进行压缩 压缩算法分为有损压缩和无损压缩 有损压缩:解压缩后数据可以完全复原 无损压缩:解压缩后数据不能完全复原,压缩比越小,丢失的越多。 常见的:PCM、WAV、AAC、MP3、Ogg都是压缩算法。 压缩主要去除人耳听觉范围之外的信号和因为掩蔽效应被掩蔽掉的信号。 下面介绍几种编码格式: WAV: 一种无损格式,在PCM数据格式前面加上44字节,分别用来描述采样率、声道数、数据格式等信息。 MP3: 使用LAME编码的中高码率的MAP3文件,听感上非常接近WAV文件,一般来说在128kbit/s以上表现都还不错。 AAC: 小于128kbit/s的码率下表现优异,多用于视频中的音频编码。 Ogg: 用更小的码率达到更好的音质,但缺乏媒体服务软件的支持,适合语言聊天的音频消息场景。 视频和图像介绍: 图像的表示方法: 1:RGB表示 对于每个像素,一般使用8比特表示一个子像素,32比特表示一个像素。一副 1280*720的图像的大小为 1280 * 720 * 4=3.516MB 这也是位图在内存中所占的大小,如此大的图片,在网络上直接传输也是不大可能的,于是便有了图像压缩格式,如 JPEG,一种有损压缩,在提供良好的压缩性能的同时,具有较好的重建质量。但是这种压缩不能直接用于视频压缩,视频压缩不仅仅要考虑帧内编码,还要考虑帧间编码。 2:YUV表示方式 和RGB相比,优点在于只需要占用极少的频宽 Y:亮度,也叫灰阶值 U、V:色度信号 YUV,如YUV420P数据量大小为 1280 * 720 * 1 + 1280 * 720 * 0.5 = 1.318MB 比RGB小一些,但是以fbps 24来计算视频大小,仍然需要 1.318MB * 24fps * 90min * 60s = 166.8GB 还是太大了,所以对电影储存和流媒体播放,需要进行视频编码。 YUV和RGB的转化: 凡是渲染在屏幕上的东西,都要转化为RGB,YUV也不例外,转化公式如图 如在iOS平台中,使用摄像头采集出YUV数据之后(CMSampleBufferRef),上传显卡成为一个纹理ID(使用CVBufferGetAttachment获取YCbCrMatrix),这个时候就需要做YUV到RGB的转换。 视频编码 视频编码和音频编码一样,也是通过去除冗余信息来进行压缩的。冗余信息主要包括空间上的冗余信息和时间上的冗余信息。 使用帧间编码技术,去除时间上的冗余信息: 运动补偿:使用之前的局部图像来预测、补偿当前的局部图像。 运动表示:不同区域的图像需要使用不同的运动矢量来描述运动信息。 运动估计:从视频序列中抽取运动信息。 使用帧间编码技术,去除空间上的冗余信息: ISO标准 Motion JPEG即MPEG,用于动态视频压缩算法,利用图像序列中的相关原则去除冗余,MP4指的就是Mpeg4AVC,流媒体用的最多的就是它了。 ITU-T制定的H.261…H.264是一套单独的体系,目前使用最多的就是H.264,压缩性能大大提高。 编码概念: 如前面所说,编码就是为了减少数据的容量 1:IPB帧 I帧:帧内编码帧,通常是每个GOP(MPEG所使用的视频压缩技术)的第一个帧,经过压缩,成为随机访问的第一个参考点,可以当成静态图像,I帧主要去除空间上的冗余信息。 P帧:考虑前面的I帧和P帧生成完整画面 B帧:考虑前面的I帧、p帧、后面的p帧来生成一张完整的视频画面。 IDR帧:一种特殊的I帧,这一帧之后所有参考帧只会参考到这个IDR帧,保证找到IDR帧之后就能解析出来。 2:PTS和DTS DTS 主要用于视频的解码 PTS 主要用于在解码阶段进行视频的同步和输出 在没有B帧的情况下,DTS和PTS的输出顺序是完全相同的。 3:GOP的概念 两个 I 帧之间形成的一组图片,就是GOP。 gop_size:表示两个I帧之间帧的数目,通常来说,gop_size越大,这个画面质量就越好,但是解码端必须从接收到的第一个I帧开始才可以正确解码。提高视频质量的技巧中,有个技巧就是多使用B帧,一般来说I帧的压缩比为7(和JPG差不多),P是20,B可以达到50,可见B能节省大量空间。 ffmpeg FFmpeg API的介绍与使用 一些基本术语: 容器/文件:特定格式的多媒体文件,如MP4、flv、mov等 媒体流:时间轴上的一段连续数据,如一段声音数据、一段视频数据 数据帧/数据包:通常一个媒体流是由大量的数据帧组成的,对于压缩数据,帧对应着编解码器的最小处理单元,分属于不同媒体流的数据帧交错储存于容器中。 编解码器: 以帧为单位实现压缩数据和原始数据之间的相互转换的 原文链接:https://blog.csdn.net/goldfish3/article/details/89482458 ================================================ FILE: iOS资料/iOS音频采集过程中的音效实现.md ================================================ # iOS音频采集过程中的音效实现 AudioUnit特点解析 从实际效果上说,苹果AudioUnit系列API方案,要比针对谷歌Android系统的跨平台声音处理库libsox等方案要好。一个原因在于,对于音效处理,苹果提供了非常丰富的音频API,涵盖采集、处理、播放各个环节,并按照需求的层次进行了分组。 ![说明: 图1. Core Audio Overview](https://a5img.pncdn.cn/2018/0129/1517189970189.png) 苹果iOS系统音频框架概览 从上图中可以看到,离底层驱动和硬件最近的就是AudioUnit系列API。与其它声音处理方案相比,AudioUnit包含以下这些优缺点: 优点: –低延时,从采集到播放回环可到10ms这一级别 –动态变更配置组合 –直接获得后台执行权限 –CPU资源消耗较少 缺点: –专有概念比较多,接口复杂 –提供C风格API 由于AudioUnit并不完美,特别是专有概念比较多,接口也比较复杂,因此如果技术薄弱,在开发时难度会很大,金山云直播SDK解决了开发难度大等问题,这也是金山云SDK倍受欢迎的原因之一。 AudioUnit这个名字比较形象,它的主体是一系列单元节点(unit),不同的单元节点可实现不同的功能,将一个或多个单元节点添加到AUGraph(全称是Audio Processing Graph,把各个unit组合在一起,起到管理作用)中,并建立单元节点之间的连接,音频数据顺次通过各个节点,即可完成对声音的采集、处理和播放。如下图所示,AUGraph显示出了这个音频处理系统的构成,提供了启动和停止处理系统的接口。 ![说明: 图2. AudioUnit in iOS](https://a5img.pncdn.cn/2018/0129/1517189970629.png) AudioUnit示意图 如下方表格所示,苹果iOS系统提供了四类单元节点: purposeAudio units Effecteg. Reverb mixingeg. Multichannel Mixer I/Oeg. Remote I/O Format conversioneg. Format Converter 其中,I/O主要负责设备,比如采集和播放;Mixing负责将不同来源的音频数据进行混合;Effect负责对音频数据进行音效处理,Format Conversion负责格式转换,比如重采样等。这里有个优化的点,由于Multichannel Mixer本身就有格式转换的功能,输入和输出的音频数据格式可以不同,因此利用这一点,可以节省一个格式转换单元。 AudioUnit中的音频采集 在直播应用中,我们主要使用Remote I/O unit进行采集。由于一个AUGraph中只允许有一个I/O unit,因此Remote I/O需要同时负责采集和播放。当用户开启耳返功能时,需要将采集到的声音,经过处理后再送回当前节点直接播放,这样可将采集和播放的延时控制在50ms以内,主播和观众才不会察觉到声音的延时。基本步骤如下(以下五个过程,均可在苹果官方文档中找到具体说明和代码示例): 1.实例化AUGraph,添加units; 2.配置每个AudioUnit属性; 3.设置渲染回调函数(Render Callback Function); 4.建立units连接; 5.启动AUGraph。 其中第4步较为关键,也就是设置渲染回掉函数,以下是该回掉函数的函数声明: ![说明: 1](https://a5img.pncdn.cn/2018/0129/1517189970106.jpg) 如上图所示,AudioUnit每次都会处理一段音频数据,每次处理完成一段数据的时候,此前设置的回调函数就会被调用一次。在这个回调函数中,通过AudioUnit的AudioUnitRender,可从AUGraph中的某个节点中,获取一段处理后的音频PCM数据。同时,如果需要进行耳返播放,在这个回调中,也需要将取得的音频数据送入到回调函数的最后一个参数ioData对应的buffer中。 在设置单元节点属性时,需要注意里面包含的一些公共属性。例如音频格式属性和MaximumFramesPerSlice。如果音频格式设置错误,容易出现AUGraph启动失败、声音异常等问题。在使用iOS内置的麦克风或有线耳机时,设备支持的采样率较高,44.1KHz可正常工作,整条音频通路基本都采用44.1KHz。当使用蓝牙设备时,一般蓝牙设备无法支持44.1KHz进行采集和播放,通常是16KHz甚至更低,这会导致I/O Unit无法继续使用之前的配置,需要按照实际支持的采样率进行配置。 AudioUnit还要求两个单元衔接处的音频数据格式必须保持一致,当AUGraph中不同unit支持的格式不同时(比如在支持蓝牙设备或者使用回声消除模块时,I/O unit要求的格式和其它单元可能会不同),此时就需要分别设置格式,并通过unit或mixer unit对格式进行转换。 如果MaximumFramesPerSlice设置错误,可能会出现声音异常。MaximumFramesPerSlice表示的是每次回调送入或取出的音频数据的长度,在AUGraph所有节点的这个属性,也需要保持一致,否则会导致其中一些unit丢弃数据而出现声音异常。 AudioUnit中的音效处理 这里所谓的音效处理,主要是指对原本的声音进行改变,比如混响效果,变声效果等。需要用到和数字信号处理有关的一系列时间和频域工具,将PCM数据输入,经过运算后得到变化后的声音。 混响效果 我们在音乐厅、剧院、礼堂等比较空旷的室内说话或唱歌时,能听到和平时不同的声音,原因是声音在墙壁上多次反射后叠加在一起,就有了混响的效果。在声音处理的过程中,可以人为将声音缓存起来,延时一定时间后,和原声音叠加,这样就能够模拟出混响效果。 AudioUnit提供了kAudioUnitSubType_Reverb2负责实现混响效果的生成,将该单元节点接入到AUGraph中之后,配置参数即可实现混响效果。虽然混响原理比较简单,但为了模拟自然界中的实际音效,这个计算过程还是相当复杂的,因为需要模拟大小不一的空间,不同材质的墙壁,包括障碍物的多少,都需要输入很多参数参与运算。对此,iOS的reverb unit提供了七种参数。金山云SDK在直播应用中,可提供四种不同场景的模拟(录音棚、演唱会、KTV、小舞台),主要是通过调整如下参数实现的: kReverb2Param_DryWetMix混响效果声音的大小,与空间大小无关,只与空间内杂物多少以及墙壁和物体的材质有关; kReverb2Param_DecayTimeAt0Hz/kReverb2Param_DecayTimeAtNyquist整个混响的总长度,与空间大小有关,越空旷,时间越长。 变声效果 变声效果是在频域上对人的声音进行处理,例如男声一般比较低沉,女声比较尖锐,这个主要说的是音调,而通过对声音音调的调整,可以让低沉的男声听上去像尖锐的女声。iOS提供的kAudioUnitSubType_NewTimePitch这一单元节点,即负责音调的调整。值得注意的是,kAudioUnitSubType_NewTimePitch不是输入Effect类,而是属于FormatConverter类,通过设置TimePitch unit的kNewTimePitchParam_Pitch属性即可。 ![说明: 2](https://a5img.pncdn.cn/2018/0129/1517189970676.jpg) 如上图所示,变男声,需要强化突出低沉的特点,将音调调低,设置负数参数即可; 变女声,需要强化突出尖锐的特点,将音调调高,设置正数即可; 机器人的音效是一个组合效果,在很多科幻电影中,机器人音调比较高,而且有重音,因此我们采用了TimePitch unit + Delay unit的方式。Delay unit也是iOS提供的一个将声音延时叠加的单元节点,比混音要简单很多,只有单次叠加; 庄严宏大音效一般自带回声,而且较男性化,因此金山云选择了TimePitch unit + Reverb unit的方式实现。 这里推荐一个调节音效的参考软件voxal voice changer。大家可以在这个软件上将不同的工具组件进行组合,通过调试参数,即可实时听到参数对应的结果,效果满意后再移植到AudioUnit中。 ![说明: 图3 voixal voice changer](https://a5img.pncdn.cn/2018/0129/1517189970799.jpg) voixal voice changer软件截图 以上就是在iOS移动设备直播时的音频采集过程中,采用AudioUnit中的音效组件实现混响和变声效果的大致过程。金山云的直播SDK目前已集成此功能,深受直播行业客户的欢迎。实际上,AudioUnit还有很多功能没有被挖掘出来,比如可将背景音乐、混音、回声消除等其他移动多媒体音频相关的功能纳入到这个框架中,对此,金山云正在深入探索,希望带给直播用户更多更好的体验。 原文https://www.admin5.com/article/20180129/821101.shtml ================================================ FILE: iOS资料/iOS项目集成OpenCV及踩过的坑.md ================================================ # iOS项目集成OpenCV及踩过的坑 ### 1、直接下载Framework集成 #### 1.1、下载OpenCV的Framework 从[OpenCV官网](https://link.juejin.cn?target=http%3A%2F%2Fopencv.org)下载框架,拖入Xcode项目。 #### 1.2、导入OpenCV依赖的库 导入路径:选择项目—>Targets—>General—>Linked Frameworks and Libraies,点击”+”添加下方依赖库。 > - libc++.tbd > - AVFoundation.framework > - CoreImage.framework > - CoreGraphics.framework > - QuartzCore.framework > - Accelerate.framework > - CoreVideo.framework > - CoreMedia.framework > - AssetsLibrary.framework #### 1.3、改为Objective-C与C++混编 凡是导入OpenCV头文件的类,[都需要把相应类后缀名.m改为.mm](https://link.juejin.cn?target=http%3A%2F%2Fxn--eqrb591f45df85a6khbreqq0axmpzfo.xn--m-cr6au94e.mm)。 ```arduino #import #import #import 复制代码 ``` ### 2、CocoaPods方式集成(不推荐) #### 2.1 CocoaPods文件配置 在项目Pod文件中配置**pod ‘OpenCV’**,然后**pod update**;同理,使用时导入OpenCV相应的头文件,[并把类后缀名.m改为.mm](https://link.juejin.cn?target=http%3A%2F%2Fxn--eqrb501fl9dz24bvfd.xn--m-cr6au94e.mm)。 #### 2.2 使用CocoaPods集成OpenCV说明 使用CocoaPods虽然配置简单,但自动配置的不正确,存在名称重复等大量的问题。例如: > Warning: Multiple build commands for output file /Users/P85755/Library/Developer/Xcode/DerivedData/PracticeProject-bgmxispyljyrbfdimchwaxacraaa/Build/Products/Debug-iphoneos/OpenCV/calib3d.hpp Warning: Multiple build commands for output file /Users/P85755/Library/Developer/Xcode/DerivedData/PracticeProject-bgmxispyljyrbfdimchwaxacraaa/Build/Products/Debug-iphoneos/OpenCV/core.hpp 。。。。。。。。。等等 是由于CocoaPods自动配置时,生成了相同名称的.h配置文件,虽然在不同路径,Xcode仍旧认为是同一个文件。 ### 3、已经踩过的`深坑` #### 3.1、导入头文件的深坑 导入**#import **报Expected identitier的错误。这是由于opencv 的 import 要写在**#import 、#import **这些系统自带的 framework `前面`,否则会出现重命名的冲突。 ![导入头文件错误](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2018/11/19/1672be3f069ae308~tplv-t2oaga2asx-zoom-in-crop-mark:4536:0:0:0.image) #### 3.2、Objective-C和C++的混编的深坑 OpenCV框架提供是C++的API接口,凡是使用OpenCV的地方,类的文件类型必须由.m类型改为.mm类型,这时候编译器按照OC与C++混编进行编译。 [假设你使用OpenCV的类为A.mm](https://link.juejin.cn?target=http%3A%2F%2Fxn--OpenCVA-k73k98f4ix9e8x3qpyd29zek6c.mm),那如果你在Objective-C的类B.m中导入使用,此时编译器会认为此时A.mm也按照Objective-C类型编译,你必须把B.m类型更改为B.mm类型才不会报错,以此类推,[你在C.m中使用B.mm](https://link.juejin.cn?target=http%3A%2F%2Fxn--C-376ay2w.xn--mB-qy2c05ckz8h.mm),那C也必须更改为C.mm类型。。。有人比喻这样蔓延的有点像森林大火,一个接一个,很形象。 `解决办法:`在导入OpenCV头文件的时候,**#import \**前面加上\**#ifdef __cplusplus**,指明编译器只有使用了OpenCV的.mm类型文件,才按照C++类型编译。如下即可解决: ```arduino #ifdef __cplusplus #import #import #import #endif 复制代码 ``` #### 3.3、编译警告 导入OpenCV使用时,Xcode8会有一堆类似`warning: empty paragraph passed to '@param' command [-Wdocumentation]`的文档警告。 ![导入头文件错误](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2018/11/19/1672be3f06afb20c~tplv-t2oaga2asx-zoom-in-crop-mark:4536:0:0:0.image) 虽然项目目前不报错了,但对于有强迫症的小伙伴来说,还是不能忍。解决办法:导入头文件的时候,忽略文档警告即可;同时只在需要的地方导入C++类,则加上编译器忽略文档警告即可,解决办法如下: ```arduino #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wdocumentation" #ifdef __cplusplus #import #import #import #endif #pragma clang pop 复制代码 ``` #### 3.4、UIImage与cv::Mat转换报错。 读取视频帧,转换为UIImage时报**_CMSampleBufferGetImageBuffer", referenced from:**的错误,是由于缺少**CoreMedia.framework**框架,在**Targets—>General—>Linked Frameworks and Libraies**导入**CoreMedia.framework**框架即可。 原文链接:https://juejin.cn/post/6844903716869373959 ================================================ FILE: iOS资料/iOS高级视频渲染.md ================================================ # iOS高级视频渲染 ## 1.前言 今天为大家介绍一下 iOS 下 WebRTC是如何渲染视频的。在iOS中有两种加速渲染视频的方法。一种是使用OpenGL;另一种是使用 Metal。 OpenGL的好处是跨平台,推出时间比较长,因此比较稳定。兼容性也比较好。而Metal是iOS最近才推出的技术,理论上来说比OpenGL ES效率更高。 WebRTC中这两种渲染方式都支持。它首先会判断当前iOS系统是否支持Metal,如果支持的话,优先使用Metal。如果不支持的话,就使用 OpenGL ES。 我们今天介绍的是 OpenGL ES的方案。 ![img](https://pic3.zhimg.com/80/v2-18de40a7d9e9ab08efebad6f71c11392_720w.webp) ### 1.1创建 OpenGL 上下文 在iOS中使用OpenGL ES做视频渲染时,首先要创建EAGLContext对象。这是因为,EAGLContext管理着 OpengGL ES 渲染上下文。该上下文中,包括了状态信息,渲染命令以及OpenGL ES绘制资源(如纹理和renderbuffers)。为了执行OpenGL ES命令,你需要将创建的EAGLContext设置为当前渲染上下文。 EAGLContext并不直接管理绘制资源,它通过与上下文相关的EAGLSharegroup对象来管理。当创建EAGLContext时,你可以选择创建一个新的sharegroup或与之前创建的EAGLContext共享EAGLSharegroup。 EAGLContext与EAGLSharegroup的关系如下图所示: ![img](https://pic3.zhimg.com/80/v2-d3eb8cc7aea6aa9a60ec977dca31ef66_720w.webp) WebRTC中并没有使用共享EAGLSharegroup的情况,所以对于这种情况我们这里就不做特别讲解了。有兴趣的同学可以在网上查找相关资料。 目前,OpenGL ES有3个版本,主要使用版本2和版本3 。所以我们在创建时要对其作判断。首先看是否支持版本3,如果不支持我们就使用版本2。 代码如下: ![img](https://pic1.zhimg.com/80/v2-ca70e1d8b8dbedb6539a2d652863887c_720w.webp) 创建完上下文后,我们还要将它设置为当前上下文,这样它才能真正起作用。 代码如下: ![img](https://pic2.zhimg.com/80/v2-96930ef218cbfacfece1abc11c39d031_720w.webp) > 需要注意的是,由于应用切换到后台后,上下文就发生了切换。所以当它切换到前台时,也要做上面那个判断。 OpenGL ES上下文创建好后,下面我们看一下如何创建View。 ### 1.2创建 OpenGL View 在iOS中,有两种展示层,一种是 GLKView,另一种是 CAEAGLLayer。WebRTC中使用GLKView进行展示。CAEAGLLayer暂不做介绍。 GLKit框架提供了View和View Controller类以减少建立和维护绘制 OpenGL ES 内容的代码。GLKView类用于管理展示部分;GLKViewController类用于管理绘制的内容。它们都是继承自UIKit。GLKView的好处是,开发人员可以将自己的精力聚焦在OpenGL ES渲染的工作上。 GLKView展示的基本流程如下: ![img](https://pic2.zhimg.com/80/v2-9efd0ffe89f464ff238d6f8c87a41e9d_720w.webp) 如上图所示,绘制 OpenGL ES 内容有三步: - 准备 OpenGL ES环境; - 发送绘制命令; - 展示渲染内容。 GLKView类自己实现了第一步和第三步。第二步由开发人员来完成,也就是要实现drawRect函数。GLKView之所以能为OpenGL ES 提供简单的绘制接口,是因为它管理了OpenGL ES 渲染过程的标准部分: 在调用绘制方法之前: 使用 EAGLContext 作为当前上下文。 根据size, 缩放因子和绘制属性,创建 FBO 和 renderbuffer。 绑定 FBO,作为绘制命令的当前目的地。 匹配 OpenGL ES viewport与 framebuffer size 。 在绘制方法返回之后: 解决多采样 buffers(如果开启了多采样)。 当内容不在需要时,丢掉 renderbuffers。 展示renderbuffer内容。 使用GLKView有两种方法,一种是实现一个类,直接继承自GLKView,并实现drawRect方法。另一种是实现GLKView的代理,也就是GLKViewDelegate,并实现drawInRect方法。 在WebRTC中,使用的是第二种方法。RTCEAGLVideoView 是GLKView的包裹类,并且继承自GLKViewDelegate。 首先,创建GLKView. ![img](https://pic1.zhimg.com/80/v2-df1043d54930c7fec2e5e5133b63c16c_720w.webp) 创建好GLKView后,需要将glkView.delegate设置为RTCEAGLVideoView,这样就可以将绘制工作交由RTCEAGLVideoView来完成了。另外, glkView.enableSetNeedsDisplay 设置为 NO,由我们自己来控制何时进行绘制。 然后,实现drawInRect方法。 ![img](https://pic3.zhimg.com/80/v2-1fe3885ad2803e10b7acfc0fd275244a_720w.webp) 上面的代码就是通过Shader来绘制NV12的YUV数据到View中。这段代码的基本意思是将一个解码后的视频帧分解成Y数据纹理及UV数据纹理。然后调用Shader程序将纹理转成rgb数据,最终渲染到View中。 ## 2.Shader程序 OpenGL ES 有两种 Shader。一种是顶点(Vetex)Shader; 另一种是片元(fragment )Shader。 Vetex Shader: 用于绘制顶点。 Fragment Shader:用于绘制像素点。 Vetex Shader Vetex Shader用于绘制图形的顶点。我们都知道,无论是2D还是3D图形,它们都是由顶点构成的。 在OpenGL ES中,有三种基本图元,分别是点,线,三角形。由它们再构成更复杂的图形。而点、线、三角形又都是由点组成的。 视频是在一个矩形里显示,所以我们要通过基本图元构建一个矩形。理论上,距形可以通过点、线绘制出来,但这样做的话,OpenGL ES就要绘制四次。而通过三角形绘制只需要两次,所以使用三角形执行速度更快。 下面的代码就是 WebRTC 中的Vetex Shader程序。该程序的作用是每个顶点执行一次,将用户输入的顶点输出到 gl_Position中,并将顶点的纹理作标点转作为 Fragment Shader 的输入。 1.OpenGL坐标原点是屏幕的中心。纹理坐标的原点是左下角。 2.gl_Position是Shader的内部变量,存放一个项点的坐标。 ![img](https://pic4.zhimg.com/80/v2-894f2d7ca1405d39b05770fe231a983b_720w.webp) OpenGL ES Shader语法请见我的另一篇文章着色器 ### 2.1fragment Shader fragment Shader程序是对片元着色,每个片元执行一次。片元与像素差不多。可以简单的把片元理解为像素。 下面的代码是WebRTC中的 fragment > YUV有多种格式,可以参见我的另一篇文章YUV。 在代码中,使用FRAGMENT_SHADER_TEXTURE命令,也就是OpenGL ES中的 texture2D 函数,分别从 Y 数据纹理中取出 y值,从 UV 数据纹理中取出 uv值,然后通过公式计算出每个像素(实际是片元)的 rgb值。 ![img](https://pic3.zhimg.com/80/v2-7d0696691c673586cf13ef9f105d7706_720w.webp) 有了顶点数据和片元的RGB值后,就可以调用OpenGL ES的 draw 方法进行视频的绘制了。 Shader的编译、链接与使用 上面介绍了 WebRTC下 Vetex Shader 和 Fragment Shader程序。要想让程序运行起来,还要额外做一些工作。 OpenGL ES的 shader程序与C程序差不多。想像一下C程序,要想让一个C程序运行起来,要有以下几个步骤: - 写好程序代码 - 编译 - 链接 - 执行 Shader程序的运行也是如此。我们看看 WebRTC是如何做的。 ![img](https://pic2.zhimg.com/80/v2-97d828c6e7ca624ed7ef0586c31ff185_720w.webp) 它首先创建一个 Shader, 然后将上面的 Shader 程序与 Shader 绑定。之后编译 Shader。 ![img](https://pic3.zhimg.com/80/v2-0644f5cb89a9cb368aee0bc4e902f0ae_720w.webp) 编译成功后,创建 program 对象。 将之前创建的 Shader 与program绑定到一起。之后做链接工作。一切准备就绪后,就可以使用Shader程序绘制视频了。 ![img](https://pic3.zhimg.com/80/v2-abc7658fdd6ee29ba90b38135928e986_720w.webp) ### 2.2WebRTC中视频渲染相关文件 RTCEAGLVideoView.m/h:创建 EAGLContext及OpenGL ES View,并将视频数据显示出来。 RTCShader.mm/h:OpenGL ES Shader 程序的创建,编译与链接相关的代码。 RTCDefaultShader.mm/h: Shader 程序,绘制相关的代码。 RTCNV12TextureCache.mm/h: 用于生成 YUV NV12 相关纹理的代码。 RTCI420TexutreCache.mm/h: 用于生成 I420 相关纹理的代码。 ## 3.小结 本文对 WebRTC 中 OpenGL ES 渲染做了介绍。通过本篇文章大家可以了解到WebRTC是如何将视频渲染出来的。包括: 上下文的创建与初始化。 GLKView的创建。 绘制方法的实现。 Shader代码的分析。 Shader的编译与执行。 原文https://zhuanlan.zhihu.com/p/146950864 ================================================ FILE: iOS资料/macOS 下单步调试 WebRTC Android & iOS.md ================================================ # macOS 下单步调试 WebRTC Android & iOS 上一篇文章里有位读者朋友咨询我,如何调试 WebRTC iOS demo。显然这个小问题不值一篇文章,所以这周我就花了大量的精力,解决了长久以来困扰广大 WebRTC 安卓开发者的难题:如何在 Android Studio 里单步调试 WebRTC Android 的 native 代码。 今天我就在这里给大家带来一场盛宴 :) ## 1、WebRTC 代码下载 首先给各位上点冰镇白开,解解暑: - depot tools 是 chromium 代码库管理工具,包括代码管理、依赖管理、工作流程管理等; - Android/Linux、Windows、iOS/macOS WebRTC 本身的代码是同一个仓库,但依赖工具不同,所以不可能放到一起,我的移动硬盘里就有 `webrtc_android` 和 `webrtc_ios` 两个目录,一共 46 GB; - depot tools 的运行基于 python 2.x 环境,且需要是官方 build(`--version` 选项不能输出额外信息); - 各个系统的 buildtools 是需要运行 `gclient runhooks` 进行下载的,而且是自动检测运行时的系统,只下载当前系统的; - gn/clang format 下载地址:https://storage.googleapis.com/chromium-clang-format/0679b295e2ce2fce7919d1e8d003e497475f24a3 , https://storage.googleapis.com/chromium-gn/9be792dd9010ce303a9c3a497a67bcc5ac8c7666 , 替换 hash 值即可,其他 `download_from_google_storage` 的步骤都可以这样解决(替换 bucket 和 hash); 顺便说说我下载代码的方式: - 首先准备一台可上网的路由器,我的是华硕 RT-AC68U + koolshare `380.63_0-X7.2`(升级后 format jffs at next boot 并重启) + 离线安装科学上网插件; - 然后在阿里云香港开一个按量付费的最低配云主机,0.07 元/小时,流量 1 元/GB,速度很快,能到 MB/s 级别,完整下载花费也不会超过 50 元,下完之后释放; - 把配好 ss 及 ss 自启动的镜像做成自定义镜像,并创建创建实例模板,再需要更新代码时一键启动无需配置,只需到路由器后台改下 IP 即可; - 有了这个环境之后,也就可以轻松给 WebRTC 提交 patch 了,我都提交了好几个 patch 了; ## 2、Xcode 调试 WebRTC iOS demo 在迎来 Android Studio 这道大菜之前,我们先用 Xcode 开个胃: - `src/examples/BUILD.gn` 中,搜索 `ios_app_bundle("AppRTCMobile")`,为其中增加以下内容(bundle id 设置为实际使用的独特 id): ``` extra_substitutions = [ "PRODUCT_BUNDLE_IDENTIFIER=com.github.piasy.AppRTCMobile", ] ``` - `src` 目录下执行 `gn gen out/xcode_ios_arm64 --args='target_os="ios" target_cpu="arm64"' --ide=xcode`; - 用 Xcode 打开 `src/out/ios/all.xcworkspace`,run target 选择 `AppRTCMobile`,工程文件的设置 target 也选择 `AppRTCMobile`; ![图片](https://mmbiz.qpic.cn/mmbiz_png/gGwQfiaEiaqjib7Xl7bjbIso3kfKHiaicczvSmpm8dzGyTgWMaS1Rno704w8uyJsq2cZZUcsISxaRkeW5CTc7emWd2g/640?wx_fmt=png&wxfrom=5&wx_lazy=1&wx_co=1) - 修改 `src/examples/objc/AppRTCMobile/ios/Info.plist`,设置同样的 bundle id; - 在工程文件的 general tab 中,手动选择 `Info.plist` 为 `src/examples/objc/AppRTCMobile/ios/Info.plist`;选择了 plist 之后,可能 Xcode 不会显示 bundle id 等信息,这是 Xcode 的一个小 bug,先查看一个其他文件,再查看工程文 件,就能看到了; - 勾选「Automatically manage signing」,选择合适的 team; - 点击 run 即可,和调试普通 iOS app 一样; - clean 工程并不会清除 ninja 脚本编译的结果,所以不必担心耗时; - 更新代码后,可能需要删掉老的 `src/third_party/llvm-build/` 目录,然后执行 `gclient run_hooks` 下载新的 llvm; - 在 `examples/objc/AppRTCMobile/ARDAppEngineClient.m` 里,修改 `kARDRoomServerHostUrl`, `kARDRoomServerJoinFormat`, `kARDRoomServerJoinFormatLoopback`, `kARDRoomServerMessageFormat`, `kARDRoomServerLeaveFormat` 这四个变量的域名为实际部署的 AppRTC server 域名/地址,即可连接自己的 server; ## 3、Android Studio 调试 WebRTC Android demo 好了,现在开始正餐部分 :) 首先,官方有个 issue,讲的就是 Android Studio 的支持,遗憾的是现在还没有解决方案。 但是,不就是编译么,手写一个 CMakeLists.txt 就好了嘛(在此省略描述编写 CMakeLists.txt 过程中解决各种问题的五千字)。 写好了的 CMakeLists.txt 和 build.gradle 就在这里!就在这里!!就在这里!!! *超链接文字这么长,应该能点到吧 :)* 注:这个 CMakeLists.txt 基于 `#24277` 提交,且只适配了 arm64-v8a 架构,其他代码版本、架构可能存在问题,不保证可以使用。此外,一定要搭配工程里的 build.gradle 使用,因为还要靠它生成一些代码。 不过 clone 下来之后,别急着用 Android Studio 打开,首先要修改一下 `libjingle_peerconnection/build.gradle`,设置以下变量: - `webrtc_repo`: WebRTC Android 代码仓库路径; - `android_jar`: Android SDK 的 android.jar 路径; - `py2`: Python 2.x 可执行文件的路径; - `protoc`: protobuf 编译程序的路径,注意它的版本需要和 WebRTC 代码库匹配,我没找到这个程序在代码库里的位置,但我发现用 gn + ninja 编译一次后,会在 `out/dir/clang_x64` 目录下生成这个程序; 修改完了 build.gradle 之后也不要着急,还要修改 Android Studio 启动配置,编辑 `~/Library/Preferences/AndroidStudio3.1/studio.vmoptions`(替换为正确版本),修改 `-Xms`, `-Xmx`, `-XX:MaxPermSize`, `-XX:ReservedCodeCacheSize` 这四个参数: ``` -Xms4096m -Xmx14336m -XX:+UseG1GC -XX:-UseParNewGC -XX:-UseConcMarkSweepGC -XX:MaxPermSize=4096m -XX:ReservedCodeCacheSize=2048m -XX:+UseCompressedOops -XX:-OmitStackTraceInFastThrow -Dsun.io.useCanonCaches=false ``` 最后,还要把 CMakeLists.txt 的内容注释了一部分后再打开,sync 成功后解除一部分注释,再 sync,再解除注释,否则 Android Studio build symbols 一整天也不见完。*当然,土豪顶配 MBP 也许可以直接打开。* 对了,我使用的 Android Studio 版本是 3.1.4。 好了,为了表达我激动的心情,放上一个录屏视频: **视频链接:**[https://blog.piasy.com/2018/08/14/build-webrtc/](https://blog.piasy.com/2018/08/14/build-webrtc/index.html) ## 4、macOS 下 用 gn + ninja 编译 WebRTC Android Android Studio 的调试毕竟只是为了加断点做一些流程分析,而且写出来的 CMakeLists.txt 和 build.gradle 也只能算是一个临时解决方案,靠谱的编译方式,还得是 gn + ninja,那么接下来我就补上这道餐后甜点。 - 先在 docker 镜像里 sync 好 linux 的版本(直接在 macOS 里下载我猜应该也可以,但我没试过,欢迎大家尝试后分享结果); - 创建 `src/third_party/android_tools_mac` 目录,并把 macOS 的 ndk 和 sdk 放入其中; - 修改 `src/build/config/android/config.gni`: ``` declare_args() { android_ndk_root = "//third_party/android_tools_mac/ndk" android_ndk_version = "r16" android_ndk_major_version = 16 android_sdk_root = "//third_party/android_tools_mac/sdk" android_sdk_version = 28 android_sdk_build_tools_version = "27.0.3" android_sdk_tools_version_suffix = "-26.0.0-dev" lint_android_sdk_root = "//third_party/android_tools_mac/sdk" lint_android_sdk_version = 26 ``` - 修改 `src/build/toolchain/gcc_solink_wrapper.py` 末尾部分: ``` # Finally, strip the linked shared object file (if desired).if args.strip: result = subprocess.call(wrapper_utils.CommandToRun( [args.readelf[:-7] + "strip", '-o', args.output, args.sofile])) ``` - 下载 `src/buildtools/mac` 下定义版本的 macOS 版的 `clang-format` 和 `gn`,下载方法见上文; - 本地 build llvm,因为下载的 macOS 版本都没有 llvm-ar 这个程序,build 命令:`env LLVM_FORCE_HEAD_REVISION=1 ./src/tools/clang/scripts/update.py`; - 把 `$JAVA_HOME/bin` 加到 PATH `/usr/bin` 的前面,这样找到的就会是正确的 jdk 路径,就能找到 rt.jar 了,否则会报错 `No such file or directory: '/System/Library/Frameworks/JavaVM.framework/Versions/A/jre/lib/rt.jar'`; - 确保在 Python 2.x 的 shell 里执行 gn 和 ninja 即可编译; ## 5、Windows 编译和使用静态库 嗯……最后还有这么个鸡肋,弃之可惜,还是放在最后好了,且将其定义为残羹冷炙 :) - 首先可能会提示 `winsock.h` 和 `winsock2.h` 里的符号重定义,这是因为 `windows.h` 在 `winsock2.h` 之前被 include 导致的,我们在包含 WebRTC 头文件之前,先 `#include winsock2.h` 即可; - 对 std min max 的使用导致 `error C2589: '(' : illegal token on right side of '::'` 错误,在项目中增加 `NOMINMAX` 宏定义即可解决; - 对 `av_err2str()` 的使用会导致 `error C4576: a parenthesized type followed by an initializer list is a non-standard explicit type conversion syntax` 错误,暂时无解,只能去掉; - ninja 编出来的 `webrtc.lib` 太小(7MB 左右),且 VS 提示文件已损坏……我发誓之前编译成功过一个版本,但后来突然就不行了,已弃疗,不过论坛里的这个帖子也许有用; ## 6、总结 好了,WebRTC 编译和调试的这场盛宴就到此为止了,希望大家喜欢。热心的朋友,可以给这个 GitHub 仓库贡献一个 star,或者 follow 我,再会 :) 点击查看原文链接,获取所有链接。 原文链接:[https://blog.piasy.com/2018/08/14/build-webrtc/]( ================================================ FILE: iOS资料/【OpenGL入门】iOS 图像渲染原理.md ================================================ # 【OpenGL入门】iOS 图像渲染原理 - CPU(Central Processing Unit):现代计算机的三大核心部分之一,作为整个系统的运算和控制单元。CPU 内部的流水线结构使其拥有一定程度的并行计算能力。 - GPU(Graphics Processing Unit):一种可进行绘图运算工作的专用微处理器。GPU 能够生成 2D/3D 的图形图像和视频,从而能够支持基于窗口的操作系统、图形用户界面、视频游戏、可视化图像应用和视频播放。GPU 具有非常强的并行计算能力。 使用 GPU 渲染图形的根本原因就是速度问题。GPU 优秀的并行计算能力使其能够快速将图形结果计算出来并在屏幕的所有像素中进行显示。 **屏幕图像的显示原理** 介绍屏幕图像显示的原理,需要先从 CRT 显示器原理说起,如下图所示。CRT 的电子枪从上到下逐行扫描,扫描完成后显示器就呈现一帧画面。然后电子枪回到初始位置进行下一次扫描。为了同步显示器的显示过程和系统的显示控制器,显示器会用硬件时钟产生一系列的定时信号。当电子枪换行进行扫描时,显示器会发出一个水平同步信号(horizonal synchronization),简称 HSync;而当一帧画面绘制完成后,电子枪回复到原位,准备画下一帧前,显示器会发出一个垂直同步信号(vertical synchronization),简称 VSync。显示器通常以固定频率进行刷新,这个刷新率就是 VSync 信号产生的频率。虽然现在的显示器基本都是液晶显示屏了,但其原理基本一致。 ![img](https://pic1.zhimg.com/80/v2-c567191f1ddac6c7906a4ec6fe886730_720w.webp) 下图所示为常见的 CPU、GPU、显示器工作方式。CPU 计算好显示内容提交至 GPU,GPU 渲染完成后将渲染结果存入帧缓冲区,显示控制器会按照 `VSync` 信号逐帧读取帧缓冲区的数据,经过数据转换后最终由显示器进行显示。 ![img](https://pic3.zhimg.com/80/v2-94d08b851b852471ea44957f77e2d27a_720w.webp) **双缓冲机制** 所以,显示一个画面需要两步完成: - CPU把需要显示的画面数据计算出来 - 显示器把这些数据显示出来 这两步工作都需要时间,并且可以并行执行,因为具体执行这两个过程的硬件是相互独立的( `CPU/显卡` 和 `显示控制器` )。但是这两个工作的耗时是不同的。 CPU 以及显卡每秒能计算出的画面数量是根据硬件性能决定的。 但是显示器每秒刷新频率是固定的(一般是 `60hz` ,所以每隔16.667ms就会刷新一次)。 由于存在两边速率不统一的问题,所以引入了 `帧缓冲区(FrameBuffer)` 的概念。 最简单的情况下,帧缓冲区只有一个。此时,帧缓冲区的读取和刷新都都会有比较大的效率问题。为了解决效率问题,GPU 通常会引入两个缓冲区,即 `双缓冲机制` 。在这种情况下,GPU 会预先渲染一帧放入一个缓冲区中,用于显示控制器的读取。当下一帧渲染完毕后,GPU 会直接把显示控制器的指针指向第二个缓冲器。 根据苹果的官方文档描述,iOS 设备会始终使用 `Vsync + Double Buffering` (垂直同步+双缓冲) 的策略。 ![img](https://pic1.zhimg.com/80/v2-0a195e17869e5af5cfa61115d3df726c_720w.webp) **屏幕撕裂** 双缓冲虽然能解决效率问题,但会引入一个新的问题。当显示控制器还未读取完成时,即屏幕内容刚显示一半时,GPU 将新的一帧内容提交到帧缓冲区并把两个缓冲区进行交换后,显示控制器就会把新的一帧数据的下半段显示到屏幕上,造成画面撕裂现象,如下图: ![img](https://pic3.zhimg.com/80/v2-40de91254067227770d4db83a84fc26e_720w.webp) 为了解决这个问题,GPU 通常有一个机制叫做 `垂直同步` (简写也是 `V-Sync` ),当开启 `垂直同步` 后,GPU 会等待显示器的 `VSync` 信号发出后,才进行新的一帧渲染和缓冲区更新。这样能解决画面撕裂现象,也增加了画面流畅度,但需要消费更多的计算资源,也会带来部分延迟。 ![img](https://pic4.zhimg.com/80/v2-c6a164f9341ed757595d4b5cc941119f_720w.webp) ![img](https://pic3.zhimg.com/80/v2-06ea73b0aa1a4cdc964d1ef93138fa36_720w.webp) **掉帧** 开启了 `垂直同步` 后,理想状况下 CPU 和 GPU 可以在16ms内处理完每一帧的渲染。但是如果显卡的帧率小于屏幕的刷新率,CPU 和 GPU 处理完一帧的渲染的时间超过了16ms,就会发生掉帧的情况。那一帧会被丢弃,等待下一次机会再显示,而这时显示屏会保留之前的内容不变。这就是界面卡顿的原因。 此时显示控制器占用一个 Buffer ,GPU 占用一个 Buffer 。两个Buffer都被占用,导致 CPU 空闲下来浪费了资源,因为垂直同步的原因,只有到了 `VSync` 信号的时间点, CPU 才能触发绘制工作。 ![img](https://pic4.zhimg.com/80/v2-2a46ad2c01866efe376dfcce09f753e7_720w.webp) **三缓冲机制** 在Android4.1系统开始,引入了 `三缓冲+垂直同步` 的机制。由于多加了一个 Buffer,实现了 CPU 跟 GPU 并行,便可以做到了只在开始掉一帧,后续却不掉帧, `双缓冲` 充分利用16ms做到低延时, `三缓冲` 保障了其稳定性。 ![img](https://pic4.zhimg.com/80/v2-e0f42cfb324b793488341d9913406897_720w.webp) **iOS的渲染框架** iOS App 的图形渲染使用了 `Core Graphics` 、 `Core Animation` 、 `Core Image` 等框架来绘制可视化内容,这些软件框架相互之间也有着依赖关系。这些框架都需要通过 `OpenGL` 来调用 GPU 进行绘制,最终将内容显示到屏幕之上。 ![img](https://pic2.zhimg.com/80/v2-6e6254a99fca6d11733da229c87f35c5_720w.webp) **UIKit** `UIKit` 是 iOS 开发者最常用的框架,可以通过设置 `UIKit` 组件的布局以及相关属性来绘制界面。 事实上, `UIKit` 自身并不具备在屏幕成像的能力,其主要负责对用户操作事件的响应( `UIView` 继承自 `UIResponder` ),事件响应的传递大体是经过逐层的 `视图树` 遍历实现的。 **Core Animation** `Core Animation` 源自于 `Layer Kit` ,动画只是 `Core Animation` 特性的冰山一角。 `Core Animation` 是一个复合引擎,其职责是 尽可能快地组合屏幕上不同的可视内容,这些可视内容可被分解成独立的图层(即 `CALayer` ),这些图层会被存储在一个叫做 `图层树` 的体系之中。从本质上而言, `CALayer` 是用户所能在屏幕上看见的一切的基础。 **Core Graphics** `Core Graphics` 基于 `Quartz` 高级绘图引擎,主要用于运行时绘制图像。开发者可以使用此框架来处理基于路径的绘图,转换,颜色管理,离屏渲染,图案,渐变和阴影,图像数据管理,图像创建和图像遮罩以及 PDF 文档创建,显示和分析。 当开发者需要在 `运行时创建图像` 时,可以使用 `Core Graphics` 去绘制。与之相对的是 `运行前创建图像` ,例如用 Photoshop 提前做好图片素材直接导入应用。相比之下,我们更需要 `Core Graphics` 去在运行时实时计算、绘制一系列图像帧来实现动画。 **Core Image** `Core Image` 与 `Core Graphics` 恰恰相反, `Core Graphics` 用于在 `运行时创建图像` ,而 `Core Image` 是用来处理 `运行前创建的图像` 的。 `Core Image` 框架拥有一系列现成的图像过滤器,能对已存在的图像进行高效的处理。 大部分情况下, `Core Image` 会在 GPU 中完成工作,但如果 GPU 忙,会使用 CPU 进行处理。 **OpenGL ES** `OpenGL ES(OpenGL for Embedded Systems,简称 GLES)` ,是 `OpenGL` 的子集。 `OpenGL` 是一套第三方标准,函数的内部实现由对应的 GPU 厂商开发实现。 **Metal** `Metal` 类似于 `OpenGL ES` ,也是一套第三方标准,具体实现由苹果实现。大多数开发者都没有直接使用过 `Metal` ,但其实所有开发者都在间接地使用 `Metal` 。 `Core Animation` 、 `Core Image` 、 `SceneKit` 、 `SpriteKit` 等等渲染框架都是构建于 `Metal` 之上的。 当在真机上调试 `OpenGL` 程序时,控制台会打印出启用 `Metal` 的日志。根据这一点可以猜测, `Apple` 已经实现了一套机制将 `OpenGL` 命令无缝桥接到 `Metal` 上,由 `Metal` 担任真正于硬件交互的工作。 **UIView 与 CALayer** `CALayer` 是用户所能在屏幕上看见的一切的基础,用来存放 `位图(Bitmap)` 。 `UIKit` 中的每一个 UI 视图控件( `UIView` )其实内部都有一个关联的 `CALayer` ,即 `backing layer` 。 由于这种一一对应的关系,视图( `UIView` )层级拥有 `视图树` 的树形结构,对应 `CALayer` 层级也拥有 `图层树` 的树形结构。 视图( `UIView` )的职责是 `创建并管理` 图层,以确保当子视图在层级关系中 `添加或被移除` 时,其关联的图层在图层树中也有相同的操作,即保证视图树和图层树在结构上的一致性。 那么为什么 iOS 要基于 UIView 和 CALayer 提供两个平行的层级关系呢? 其原因在于要做 职责分离,这样也能避免很多重复代码。在 `iOS` 和 `Mac OS X` 两个平台上,事件和用户交互有很多地方的不同,基于多点触控的用户界面和基于鼠标键盘的交互有着本质的区别,这就是为什么 `iOS` 有 `UIKit` 和 `UIView` ,对应 `Mac OS X` 有 `AppKit` 和 `NSView` 的原因。它们在功能上很相似,但是在实现上有着显著的区别。 **CALayer** 在 `CALayer.h` 中, `CALayer` 有这样一个属性 `contents` ```text /** Layer content properties and methods. **/ /* An object providing the contents of the layer, typically a CGImageRef, * but may be something else. (For example, NSImage objects are * supported on Mac OS X 10.6 and later.) Default value is nil. * Animatable. */ @property(nullable, strong) id contents; 复制代码 ``` `contents` 提供了 layer 的内容,是一个指针类型,在 `iOS` 中的类型就是 `CGImageRef` (在 `OS X` 中还可以是 `NSImage` )。 `CALayer` 中的 `contents` 属性保存了由设备渲染流水线渲染好的位图 `bitmap` (通常也被称为 `backing store` ),而当设备屏幕进行刷新时,会从 `CALayer` 中读取生成好的 `bitmap` ,进而呈现到屏幕上。 图形渲染流水线支持从顶点开始进行绘制(在流水线中,顶点会被处理生成 `纹理` ),也支持直接使用 `纹理(图片)` 进行渲染。相应地,在实际开发中,绘制界面也有两种方式:一种是 `手动绘制` ;另一种是 `使用图片` 。 - 使用图片: **contents image** - 手动绘制: **custom drawing** Contents Image `Contents Image` 是指通过 `CALayer` 的 `contents` 属性来配置图片。然而, `contents` 属性的类型为 `id` 。在这种情况下,可以给 `contents` 属性赋予任何值,app 仍可以编译通过。但是在实践中,如果 `content` 的值不是 `CGImage` ,得到的图层将是空白的。 本质上, `contents` 属性指向的一块缓存区域,称为 `backing store` ,可以存放 `bitmap` 数据。 Custom Drawing `Custom Drawing` 是指使用 `Core Graphics` 直接绘制 `寄宿图` 。实际开发中,一般通过继承 `UIView` 并实现 `-drawRect:` 方法来自定义绘制。 虽然 `-drawRect:` 是一个 `UIView` 方法,但事实上都是底层的 `CALayer` 完成了重绘工作并保存了产生的图片。下图所示为 `-drawRect:` 绘制定义 `寄宿图` 的基本原理。 ![img](https://pic2.zhimg.com/80/v2-d4d6661fa6f681255a6640870249bfb9_720w.webp) - `UIView` 有一个关联图层,即 `CALayer` 。 - `CALayer` 有一个可选的 `delegate` 属性,实现了 `CALayerDelegate` 协议。 `UIView` 作为 `CALayer` 的代理实现了 `CALayerDelegae` 协议。 - 当需要重绘时,即调用 `-drawRect:` , `CALayer` 请求其代理给予一个寄宿图来显示。 - `CALayer` 首先会尝试调用 `-displayLayer:` 方法,此时代理可以直接设置 `contents` 属性。 ```text - (void)displayLayer:(CALayer *)layer; 复制代码 ``` - 如果代理没有实现 `-displayLayer:` 方法, `CALayer` 则会尝试调用 `-drawLayer:inContext:` 方法。在调用该方法前, `CALayer` 会创建一个空的寄宿图(尺寸由 `bounds` 和 `contentScale` 决定)和一个 `Core Graphics` 的绘制上下文,为绘制寄宿图做准备,作为 `context` 参数传入。 ```text - (void)drawLayer:(CALayer *)layer inContext:(CGContextRef)ctx; 复制代码 ``` - 最后,由 `Core Graphics` 绘制生成的寄宿图会存入 `backing store` 。 **UIView** `UIView` 是 app 中的基本组成结构,定义了一些统一的规范。它会负责内容的渲染以及,处理交互事件。 - Drawing and animation:绘制与动画 - Layout and subview management:布局与子 view 的管理 - Event handling:点击事件处理 `CALayer` 是 `UIView` 的属性之一,负责渲染和动画,提供可视内容的呈现。 `UIView` 提供了对 `CALayer` 部分功能的封装,同时也另外负责了交互事件的处理。 - 相同的层级结构:我们对 `UIView` 的层级结构非常熟悉,由于每个 `UIView` 都对应 `CALayer` 负责页面的绘制,所以 `CALayer` 也具有相应的层级结构。 - 部分效果的设置:因为 `UIView` 只对 `CALayer` 的部分功能进行了封装,而另一部分如圆角、阴影、边框等特效都需要通过调用 layer 属性来设置。 - 是否响应点击事件: `CALayer` 不负责点击事件,所以不响应点击事件,而 `UIView` 会响应。 - 不同继承关系: `CALayer` 继承自 `NSObject` , `UIView` 由于要负责交互事件,所以继承自 `UIResponder` 。 **Core Animation** **Core Animation 流水线** 事实上,app 本身并不负责渲染,渲染则是由一个独立的进程负责,即 Render Server 进程。 ![img](https://pic3.zhimg.com/80/v2-b856501368b5232d47c4280947db8b7a_720w.webp) App 通过 IPC 将渲染任务及相关数据提交给 `Render Server` 。 `Render Server` 处理完数据后,再传递至 GPU。最后由 GPU 调用 iOS 的图像设备进行显示。 - 首先,由 app 处理事件(Handle Events),如:用户的点击操作,在此过程中 app 可能需要更新 视图树,相应地,图层树 也会被更新。 - 其次,app 通过 CPU 完成对显示内容的计算,如:视图的创建、布局计算、图片解码、文本绘制等。在完成对显示内容的计算之后,app 对图层进行打包,并在下一次 `RunLoop` 时将其发送至 `Render Server` ,即完成了一次 `Commit Transaction` 操作。 - `Render Server` 主要执行 `Open GL` 、 `Core Graphics` 相关程序,并调用 GPU。 - GPU 则在物理层上完成了对图像的渲染。 - 最终,GPU 通过 `Frame Buffer` 、 `视频控制器` 等相关部件,将图像显示在屏幕上。 对上述步骤进行串联,它们执行所消耗的时间远远超过 16.67 ms,因此为了满足对屏幕的 60 FPS 刷新率的支持,需要将这些步骤进行分解,通过流水线的方式进行并行执行,如下图所示。 ![img](https://pic1.zhimg.com/80/v2-522c0975e22523eb16b024a4204053a0_720w.webp) **图层树** `CoreAnimation` 作为一个复合引擎,将不同的视图层组合在屏幕中,并且存储在 `图层树` 中,向我们展示了所有屏幕上的一切。 整个过程其实经历了三个树状结构,才显示到了屏幕上: `模型树-->呈现树-->渲染树` 层级关系树中除了 `视图树` 和 `图层树` ,还有 `呈现树` 和 `渲染树` 。他们各自都有各自的职责。 - **呈现树** :我们可以通过 `CALayer` 的 `-presentationLayer` 方法来访问对应的呈现树图层。注意呈现图层仅仅当图层首次被提交(就是首次第一次在屏幕上显示)的时候创建,所以在那之前调用 `-presentationLayer` 将会返回nil。 ```text - (nullable instancetype)presentationLayer; 复制代码 ``` - **模型树** :在呈现图层上调用 `–modelLayer` 将会返回它正在呈现所依赖的 `CALayer` 。通常在一个图层上调用 `-modelLayer` 会返回 `self` (实际上我们已经创建的原始图层就是一种数据模型)。 ```text - (instancetype)modelLayer; 复制代码 ``` 通常,我们操作的是模型树 `modelLayer` ,在重绘周期最后,我们会将模型树相关内容(层次结构、图层属性和动画)序列化,通过IPC传递给专门负责屏幕渲染的渲染进程。渲染进程拿到数据并反序列化出树状结构--呈现树。这个呈现图层实际上是模型图层的复制,但是它的属性值代表了在任何指定时刻当前外观效果。换句话说,可以通过呈现图层的值来获取当前屏幕上真正显示出来的值。 当模型树 `modelLayer` 上带有动画特征时,提交到渲染进程后,渲染进程会根据动画特征,不断修改呈现树 `presentationLayer` 上的图层属性,并同时不断的在屏幕上渲染出来,这样我们就看到了动画。 如果想让动画的图层响应用户输入,可以使用 `-hitTest:` 方法来判断指定图层是否被触摸,这时候对呈现图层而不是模型图层调用 `-hitTest:` 会显得更有意义,因为呈现图层代表了用户当前看到的图层位置,而不是当前动画结束之后的位置。 可以理解为 `modelLayer` 负责数据的存储和获取, `presentationLayer` 负责显示。每次屏幕刷新的时候, `presentationLayer` 会与 `modelLayer` 状态同步。 当 `CAAnimation` 加到layer上之后, `presentationLayer` 每次刷新的时候会去 `CAAnimation` 询问并同步状态, `CAAnimation` 控制 `presentationLayer` 从 `fromValue` 到 `toValue` 来改变值,而动画结束之后, `CAAnimation` 会从layer上被移除,此时屏幕刷新的时候 `presentationLayer` 又会同步 `modelLayer` 的状态, `modelLayer` 没有改变,所以又回到了起点。当然我们可以通过设置,继续影响 `presentationLayer` 的状态。 **Core Animation 动画** `Core Animation` 动画,即基于事务的动画,是最常见的动画实现方式。动画执行者是专门负责渲染的渲染进程,操作的是呈现树。我们应该尽量使用 `Core Animation` 来控制动画,因为 `Core Animation` 是充分优化过的: 基于 `Layer` 的绘图过程中, `Core Animation` 通过硬件操作位图(变换、组合等),产生动画的速度比软件操作的方式快很多。 基于 `View` 的绘图过程中, `view` 被改动时会触发的 `drawRect:` 方法来重新绘制位图,但是这种方式需要CPU在主线程执行,比较耗时。而 `Core Animation` 则尽可能的操作硬件中已缓存的位图,来实现相同的效果,从而减少了资源损耗。 **非 Core Animation 动画** 非 `CoreA nimation` 动画执行者是当前进程,操作的是模型树。常见的有定时器动画和手势动画。定时器动画是在定时周期触发时修改模型树的图层属性;手势动画是手势事件触发时修改模型树的图层属性。两者都能达到视图随着时间不断变化的效果,即实现了动画。 非 `Core Animation` 动画动画过程中实际上不断改动的是模型树,而呈现树仅仅成了模型树的复制品,状态与模型树保持一致。整个过程中,主要是CPU在主线程不断调整图层属性、布局计算、提交数据,没有充分利用到 `Core Animation` 强大的动画控制功能。 原文https://zhuanlan.zhihu.com/p/307909741 ================================================ FILE: iOS资料/【如何快速的开发一个完整的iOS直播app】(美颜篇).md ================================================ # 【如何快速的开发一个完整的iOS直播app】(美颜篇) ## 1、前言 在看这篇之前,如果您还不了解直播原理,请查看这篇文章[如何快速的开发一个完整的iOS直播app(原理篇)](https://www.jianshu.com/p/bd42bacbe4cc) 开发一款直播app,美颜功能是很重要的,如果没有美颜功能,可能分分钟钟掉粉千万,本篇主要讲解直播中`美颜功能的实现原理`,并且实现美颜功能。 如果喜欢我的文章,可以关注我微博:[袁峥Seemygo](https://link.jianshu.com?t=http://weibo.com/2034818060/profile?rightmod=1&wvr=6&mod=personinfo) ### 1.1 利用GPUImage处理直播过程中美颜的流程 - 采集视频 => 获取每一帧图片 => 滤镜处理 => GPUImageView展示 ![img](https:////upload-images.jianshu.io/upload_images/304825-5faefe55f9296071.png?imageMogr2/auto-orient/strip|imageView2/2/w/1200/format/webp) 美颜原理.png ### 1.2 美颜基本概念 `GPU`:(Graphic Processor Unit图形处理单元)手机或者电脑用于图像处理和渲染的硬件 `GPU工作原理`:采集数据-> 存入主内存(RAM) -> CPU(计算处理) -> 存入显存(VRAM) -> GPU(完成图像渲染) -> 帧缓冲区 -> 显示器 ![img](https:////upload-images.jianshu.io/upload_images/304825-67fd2250df30924d.jpg?imageMogr2/auto-orient/strip|imageView2/2/w/1200/format/webp) GPU工作原理.jpg `OpenGL ES`:(Open Graphics Library For Embedded(嵌入的) Systems `开源嵌入式系统图形处理框架`),一套图形与硬件接口,用于把处理好的图片显示到屏幕上。 `GPUImage`:是一个基于OpenGL ES 2.0图像和视频处理的开源iOS框架,提供各种各样的图像处理滤镜,并且支持照相机和摄像机的实时滤镜,内置120多种滤镜效果,并且能够自定义图像滤镜。 `滤镜处理的原理`:就是把静态图片或者视频的每一帧进行图形变换再显示出来。它的本质就是像素点的坐标和颜色变化 ### 1.3 GPUImage处理画面原理 - GPUImage采用链式方式来处理画面,通过addTarget:方法为链条添加每个环节的对象,处理完一个target,就会把上一个环节处理好的图像数据传递下一个target去处理,称为GPUImage处理链。 - 比如:墨镜原理,`从外界传来光线,会经过墨镜过滤,在传给我们的眼睛,就能感受到大白天也是乌黑一片,哈哈`。 - 一般的target可分为两类 - `中间环节`的target, 一般是各种filter, 是GPUImageFilter或者是子类. - `最终环节`的target, GPUImageView:用于显示到屏幕上, 或者GPUImageMovieWriter:写成视频文件。 - GPUImage处理主要分为3个环节 - `source(视频、图片源) -> filter(滤镜) -> final target (处理后视频、图片)` - ``` GPUImaged的Source ``` :都继承GPUImageOutput的子类,作为GPUImage的数据源,就好比外界的光线,作为眼睛的输出源 - GPUImageVideoCamera:用于实时拍摄视频 - GPUImageStillCamera:用于实时拍摄照片 - GPUImagePicture:用于处理已经拍摄好的图片,比如png,jpg图片 - GPUImageMovie:用于处理已经拍摄好的视频,比如mp4文件 - `GPUImage的filter`:GPUimageFilter类或者子类,这个类继承自GPUImageOutput,并且遵守GPUImageInput协议,这样既能流进,又能流出,就好比我们的墨镜,光线通过墨镜的处理,最终进入我们眼睛 - `GPUImage的final target`:GPUImageView,GPUImageMovieWriter就好比我们眼睛,最终输入目标。 ![img](https:////upload-images.jianshu.io/upload_images/304825-84ff54fb516a70ce.png?imageMogr2/auto-orient/strip|imageView2/2/w/1200/format/webp) GPUImage处理原理.png ### 1.4 美颜原理 - `磨皮(GPUImageBilateralFilter)`:本质就是让像素点模糊,可以使用高斯模糊,但是可能导致边缘会不清晰,用双边滤波(Bilateral Filter) ,有针对性的模糊像素点,能保证边缘不被模糊。 - `美白(GPUImageBrightnessFilter)`:本质就是提高亮度。 ### 1.5 美颜效果 - `关注效果,忽悠本人` **GPUImage原生美颜效果** ![img](https:////upload-images.jianshu.io/upload_images/304825-9e89812af6ad2de9.gif?imageMogr2/auto-orient/strip|imageView2/2/w/333/format/webp) GPUImage原生.gif **利用美颜滤镜实现效果** ![img](https:////upload-images.jianshu.io/upload_images/304825-9bbdeb87569ebab3.gif?imageMogr2/auto-orient/strip|imageView2/2/w/333/format/webp) 美颜滤镜处理.gif ### 1.6 GPUImage实战 #### 1.6.1 GPUImage原生美颜 - 步骤一:使用Cocoapods导入GPUImage - 步骤二:创建视频源GPUImageVideoCamera - 步骤三:创建最终目的源:GPUImageView - 步骤四:创建滤镜组(GPUImageFilterGroup),需要组合`亮度(GPUImageBrightnessFilter)`和`双边滤波(GPUImageBilateralFilter)`这两个滤镜达到美颜效果. - 步骤五:设置滤镜组链 - 步骤六:设置GPUImage处理链,从数据源 => 滤镜 => 最终界面效果 - 步骤七:开始采集视频 注意点: - `SessionPreset`最好使用AVCaptureSessionPresetHigh,会自动识别,如果用太高分辨率,当前设备不支持会直接报错 - `GPUImageVideoCamera必须要强引用`,否则会被销毁,不能持续采集视频. - `必须调用startCameraCapture`,底层才会把采集到的视频源,渲染到GPUImageView中,就能显示了。 - GPUImageBilateralFilter的`distanceNormalizationFactor`值越小,磨皮效果越好,`distanceNormalizationFactor`取值范围: `大于1`。 ```csharp - (void)viewDidLoad { [super viewDidLoad]; // 创建视频源 // SessionPreset:屏幕分辨率,AVCaptureSessionPresetHigh会自适应高分辨率 // cameraPosition:摄像头方向 GPUImageVideoCamera *videoCamera = [[GPUImageVideoCamera alloc] initWithSessionPreset:AVCaptureSessionPresetHigh cameraPosition:AVCaptureDevicePositionFront]; videoCamera.outputImageOrientation = UIInterfaceOrientationPortrait; _videoCamera = videoCamera; // 创建最终预览View GPUImageView *captureVideoPreview = [[GPUImageView alloc] initWithFrame:self.view.bounds]; [self.view insertSubview:captureVideoPreview atIndex:0]; // 创建滤镜:磨皮,美白,组合滤镜 GPUImageFilterGroup *groupFilter = [[GPUImageFilterGroup alloc] init]; // 磨皮滤镜 GPUImageBilateralFilter *bilateralFilter = [[GPUImageBilateralFilter alloc] init]; [groupFilter addTarget:bilateralFilter]; _bilateralFilter = bilateralFilter; // 美白滤镜 GPUImageBrightnessFilter *brightnessFilter = [[GPUImageBrightnessFilter alloc] init]; [groupFilter addTarget:brightnessFilter]; _brightnessFilter = brightnessFilter; // 设置滤镜组链 [bilateralFilter addTarget:brightnessFilter]; [groupFilter setInitialFilters:@[bilateralFilter]]; groupFilter.terminalFilter = brightnessFilter; // 设置GPUImage响应链,从数据源 => 滤镜 => 最终界面效果 [videoCamera addTarget:groupFilter]; [groupFilter addTarget:captureVideoPreview]; // 必须调用startCameraCapture,底层才会把采集到的视频源,渲染到GPUImageView中,就能显示了。 // 开始采集视频 [videoCamera startCameraCapture]; } - (IBAction)brightnessFilter:(UISlider *)sender { _brightnessFilter.brightness = sender.value; } - (IBAction)bilateralFilter:(UISlider *)sender { // 值越小,磨皮效果越好 CGFloat maxValue = 10; [_bilateralFilter setDistanceNormalizationFactor:(maxValue - sender.value)]; } ``` #### 1.6.2 利用美颜滤镜实现 - 步骤一:使用Cocoapods导入GPUImage - 步骤二:导入GPUImageBeautifyFilter文件夹 - 步骤三:创建视频源GPUImageVideoCamera - 步骤四:创建最终目的源:GPUImageView - 步骤五:创建最终美颜滤镜:GPUImageBeautifyFilter - 步骤六:设置GPUImage处理链,从数据源 => 滤镜 => 最终界面效果 注意: - `切换美颜效果原理`:移除之前所有处理链,重新设置处理链 ```csharp - (void)viewDidLoad { [super viewDidLoad]; // Do any additional setup after loading the view. // 创建视频源 // SessionPreset:屏幕分辨率,AVCaptureSessionPresetHigh会自适应高分辨率 // cameraPosition:摄像头方向 GPUImageVideoCamera *videoCamera = [[GPUImageVideoCamera alloc] initWithSessionPreset:AVCaptureSessionPresetHigh cameraPosition:AVCaptureDevicePositionFront]; videoCamera.outputImageOrientation = UIInterfaceOrientationPortrait; _videoCamera = videoCamera; // 创建最终预览View GPUImageView *captureVideoPreview = [[GPUImageView alloc] initWithFrame:self.view.bounds]; [self.view insertSubview:captureVideoPreview atIndex:0]; _captureVideoPreview = captureVideoPreview; // 设置处理链 [_videoCamera addTarget:_captureVideoPreview]; // 必须调用startCameraCapture,底层才会把采集到的视频源,渲染到GPUImageView中,就能显示了。 // 开始采集视频 [videoCamera startCameraCapture]; } - (IBAction)openBeautifyFilter:(UISwitch *)sender { // 切换美颜效果原理:移除之前所有处理链,重新设置处理链 if (sender.on) { // 移除之前所有处理链 [_videoCamera removeAllTargets]; // 创建美颜滤镜 GPUImageBeautifyFilter *beautifyFilter = [[GPUImageBeautifyFilter alloc] init]; // 设置GPUImage处理链,从数据源 => 滤镜 => 最终界面效果 [_videoCamera addTarget:beautifyFilter]; [beautifyFilter addTarget:_captureVideoPreview]; } else { // 移除之前所有处理链 [_videoCamera removeAllTargets]; [_videoCamera addTarget:_captureVideoPreview]; } } ``` ### 1.7 GPUImage扩展 - [GPUImage所有滤镜介绍](https://link.jianshu.com?t=http://www.360doc.com/content/15/0907/10/19175681_497418716.shtml) - [美颜滤镜](https://www.jianshu.com/p/945fc806a9b4) - [美图秀秀滤镜大汇总](https://link.jianshu.com?t=http://www.tuicool.com/articles/6bIbQbQ) ## 2、源码下载 [源码](https://link.jianshu.com?t=https://github.com/iThinkerYZ/GPUImgeDemo) 注意:第一次打开需要 `pod install` ## 3、结束语 后续还会讲解GPUImage原理openGL ES,视频编码,推流,聊天室,礼物系统等更多功能,敬请关注!!! 原文链接:https://www.jianshu.com/p/4646894245ba ================================================ FILE: iOS资料/关于iOS离屏渲染的深入研究.md ================================================ # 关于iOS离屏渲染的深入研究 在平时的iOS面试中,我们经常会考察有关离屏渲染(Offscreen rendering)的知识点。一般来说,绝大多数人都能答出“圆角、mask、阴影会触发离屏渲染”,但是也仅止于此。如果再问得深入哪怕一点点,比如: - 离屏渲染是在哪一步进行的?为什么? - 设置cornerRadius一定会触发离屏渲染吗? 90%的候选人都没法非常确定地说出答案。作为一个客户端工程师,把控渲染性能是最关键、最独到的技术要点之一,如果仅仅了解表面知识,到了实际应用时往往会失之毫厘谬以千里,无法得到预期的效果。 iOS渲染架构 在WWDC的Advanced Graphics and Animations for iOS Apps(WWDC14 419,关于UIKit和Core Animation基础的session在早年的WWDC中比较多)中有这样一张图: ![img](https://pic3.zhimg.com/80/v2-98077db5cb31318ec437f00762870142_720w.webp) 我们可以看到,在Application这一层中主要是CPU在操作,而到了Render Server这一层,CoreAnimation会将具体操作转换成发送给GPU的draw calls(以前是call OpenGL ES,现在慢慢转到了Metal),显然CPU和GPU双方同处于一个流水线中,协作完成整个渲染工作。 离屏渲染的定义 如果要在显示屏上显示内容,我们至少需要一块与屏幕像素数据量一样大的frame buffer,作为像素数据存储区域,而这也是GPU存储渲染结果的地方。如果有时因为面临一些限制,无法把渲染结果直接写入frame buffer,而是先暂存在另外的内存区域,之后再写入frame buffer,那么这个过程被称之为离屏渲染。 ![img](https://pic3.zhimg.com/80/v2-c448aaebe3cf19e37101ce16a799cdd2_720w.webp) 渲染结果先经过了离屏buffer,再到frame buffer CPU”离屏渲染“ 大家知道,如果我们在UIView中实现了drawRect方法,就算它的函数体内部实际没有代码,系统也会为这个view申请一块内存区域,等待CoreGraphics可能的绘画操作。 对于类似这种“新开一块CGContext来画图“的操作,有很多文章和视频也称之为“离屏渲染”(因为像素数据是暂时存入了CGContext,而不是直接到了frame buffer)。进一步来说,其实所有CPU进行的光栅化操作(如文字渲染、图片解码),都无法直接绘制到由GPU掌管的frame buffer,只能暂时先放在另一块内存之中,说起来都属于“离屏渲染”。 自然我们会认为,因为CPU不擅长做这件事,所以我们需要尽量避免它,就误以为这就是需要避免离屏渲染的原因。但是[根据苹果工程师的说法](https://link.zhihu.com/?target=https%3A//lobste.rs/s/ckm4uw/performance_minded_take_on_ios_design%23c_itdkfh),CPU渲染并非真正意义上的离屏渲染。另一个证据是,如果你的view实现了drawRect,此时打开Xcode调试的“Color offscreen rendered yellow”开关,你会发现这片区域不会被标记为黄色,说明Xcode并不认为这属于离屏渲染。 其实通过CPU渲染就是俗称的“软件渲染”,而**真正的离屏渲染发生在GPU**。 GPU离屏渲染 在上面的渲染流水线示意图中我们可以看到,主要的渲染操作都是由CoreAnimation的Render Server模块,通过调用显卡驱动所提供的OpenGL/Metal接口来执行的。通常对于每一层layer,Render Server会遵循“[画家算法](https://link.zhihu.com/?target=https%3A//en.wikipedia.org/wiki/Painter%27s_algorithm)”,按次序输出到frame buffer,后一层覆盖前一层,就能得到最终的显示结果(值得一提的是,与一般桌面架构不同,在iOS中,设备主存和GPU的显存[共享物理内存](https://link.zhihu.com/?target=https%3A//apple.stackexchange.com/questions/54977/how-much-gpu-memory-do-iphones-and-ipads-have),这样可以省去一些数据传输开销)。 ![img](https://pic2.zhimg.com/80/v2-24394bcd0b84005553320df018e06999_720w.webp) ”画家算法“,把每一层依次输出到画布 然而有些场景并没有那么简单。作为“画家”的GPU虽然可以一层一层往画布上进行输出,但是无法在某一层渲染完成之后,再回过头来擦除/改变其中的某个部分——因为在这一层之前的若干层layer像素数据,已经在渲染中被永久覆盖了。这就意味着,**对于每一层layer,要么能找到一种通过单次遍历就能完成渲染的算法,要么就不得不另开一块内存,借助这个临时中转区域来完成一些更复杂的、多次的修改/剪裁操作**。 如果要绘制一个带有圆角并剪切圆角以外内容的容器,就会触发离屏渲染。我的猜想是(如果读者中有图形学专家希望能指正): - 将一个layer的内容裁剪成圆角,可能不存在一次遍历就能完成的方法 - 容器的子layer因为父容器有圆角,那么也会需要被裁剪,而这时它们还在渲染队列中排队,尚未被组合到一块画布上,自然也无法统一裁剪 此时我们就不得不开辟一块独立于frame buffer的空白内存,先把容器以及其所有子layer依次画好,然后把四个角“剪”成圆形,再把结果画到frame buffer中。这就是GPU的离屏渲染。 常见离屏渲染场景分析 - cornerRadius+clipsToBounds,原因就如同上面提到的,不得已只能另开一块内存来操作。而如果只是设置cornerRadius(如不需要剪切内容,只需要一个带圆角的边框),或者只是需要裁掉矩形区域以外的内容(虽然也是剪切,但是稍微想一下就可以发现,对于纯矩形而言,实现这个算法似乎并不需要另开内存),并不会触发离屏渲染。关于剪切圆角的性能优化,根据场景不同有几个方案可供选择,非常推荐阅读[AsyncDisplayKit中的一篇文档](https://link.zhihu.com/?target=https%3A//texturegroup.org/docs/corner-rounding.html)。 ![img](https://pic3.zhimg.com/80/v2-dbefda856f2136cfb40ebf6eadbe5ed2_720w.webp) ASDK中对于如何选择圆角渲染策略的流程图,非常实用 - shadow,其原因在于,虽然layer本身是一块矩形区域,但是阴影默认是作用在其中”非透明区域“的,而且需要显示在所有layer内容的下方,因此根据画家算法必须被渲染在先。但矛盾在于**此时阴影的本体(layer和其子layer)都还没有被组合到一起,怎么可能在第一步就画出只有完成最后一步之后才能知道的形状呢**?这样一来又只能另外申请一块内存,把本体内容都先画好,再根据渲染结果的形状,添加阴影到frame buffer,最后把内容画上去(这只是我的猜测,实际情况可能更复杂)。不过如果我们能够预先告诉CoreAnimation(通过shadowPath属性)阴影的几何形状,那么阴影当然可以先被独立渲染出来,不需要依赖layer本体,也就不再需要离屏渲染了。 ![img](https://pic1.zhimg.com/80/v2-fd10caee5c4dbec1f110dc8dd814b4b8_720w.webp) 阴影会作用在所有子layer所组成的形状上,那就只能等全部子layer画完才能得到 - group opacity,其实从名字就可以猜到,alpha并不是分别应用在每一层之上,而是只有到整个layer树画完之后,再统一加上alpha,最后和底下其他layer的像素进行组合。显然也无法通过一次遍历就得到最终结果。将一对蓝色和红色layer叠在一起,然后在父layer上设置opacity=0.5,并复制一份在旁边作对比。左边关闭group opacity,右边保持默认(从iOS7开始,如果没有显式指定,group opacity会默认打开),然后打开offscreen rendering的调试,我们会发现右边的那一组确实是离屏渲染了。 ![img](https://pic3.zhimg.com/80/v2-1308e519fee965306f8ab0d84f4d6f2a_720w.webp) 同样的两个view,右边打开group opacity(默认行为)的被标记为Offscreen rendering - mask,我们知道mask是应用在layer和其所有子layer的组合之上的,而且可能带有透明度,那么其实和group opacity的原理类似,不得不在离屏渲染中完成。 ![img](https://pic2.zhimg.com/80/v2-487022d244a9bdefbf03636f5c15ee89_720w.webp) WWDC中苹果的解释,mask需要遍历至少三次 - UIBlurEffect,同样无法通过一次遍历完成,其原理在WWDC中提到: ![img](https://pic4.zhimg.com/80/v2-ae6ae8a2421a0a1fe3d6d5fdfbfe751b_720w.webp) - 其他还有一些,类似allowsEdgeAntialiasing等等也可能会触发离屏渲染,原理也都是类似:如果你无法仅仅使用frame buffer来画出最终结果,那就只能另开一块内存空间来储存中间结果。这些原理并不神秘。 GPU离屏渲染的性能影响 GPU的操作是高度流水线化的。本来所有计算工作都在有条不紊地正在向frame buffer输出,此时突然收到指令,需要输出到另一块内存,那么流水线中正在进行的一切都不得不被丢弃,切换到只能服务于我们当前的“切圆角”操作。等到完成以后再次清空,再回到向frame buffer输出的正常流程。 在tableView或者collectionView中,滚动的每一帧变化都会触发每个cell的重新绘制,因此一旦存在离屏渲染,上面提到的上下文切换就会每秒发生60次,并且很可能每一帧有几十张的图片要求这么做,对于GPU的性能冲击可想而知(GPU非常擅长大规模并行计算,但是我想频繁的上下文切换显然不在其设计考量之中) ![img](https://pic1.zhimg.com/80/v2-8c01866c33ee33389f8c692835f2c73c_720w.webp) 每16ms就需要根据当前滚动位置渲染整个tableView,是个不小的性能挑战 善用离屏渲染 尽管离屏渲染开销很大,但是当我们无法避免它的时候,可以想办法把性能影响降到最低。优化思路也很简单:既然已经花了不少精力把图片裁出了圆角,如果我能把结果缓存下来,那么下一帧渲染就可以复用这个成果,不需要再重新画一遍了。 CALayer为这个方案提供了对应的解法:shouldRasterize。一旦被设置为true,Render Server就会强制把layer的渲染结果(包括其子layer,以及圆角、阴影、group opacity等等)保存在一块内存中,这样一来在下一帧仍然可以被复用,而不会再次触发离屏渲染。有几个需要注意的点: - shouldRasterize的主旨在于**降低性能损失,但总是至少会触发一次离屏渲染**。如果你的layer本来并不复杂,也没有圆角阴影等等,打开这个开关反而会增加一次不必要的离屏渲染 - 离屏渲染缓存有空间上限,最多不超过屏幕总像素的2.5倍大小 - 一旦缓存超过100ms没有被使用,会自动被丢弃 - layer的内容(包括子layer)必须是静态的,因为一旦发生变化(如resize,动画),之前辛苦处理得到的缓存就失效了。如果这件事频繁发生,我们就又回到了“每一帧都需要离屏渲染”的情景,而这正是开发者需要极力避免的。针对这种情况,Xcode提供了“Color Hits Green and Misses Red”的选项,帮助我们查看缓存的使用是否符合预期 - 其实除了解决多次离屏渲染的开销,shouldRasterize在另一个场景中也可以使用:如果layer的子结构非常复杂,渲染一次所需时间较长,同样可以打开这个开关,把layer绘制到一块缓存,然后在接下来复用这个结果,这样就不需要每次都重新绘制整个layer树了 什么时候需要CPU渲染 渲染性能的调优,其实始终是在做一件事:**平衡CPU和GPU的负载,让他们尽量做各自最擅长的工作**。 ![img](https://pic3.zhimg.com/80/v2-40fa13fb4a98be4ae88c353850cab27e_720w.webp) 平衡CPU和GPU的负载 绝大多数情况下,得益于GPU针对图形处理的优化,我们都会倾向于让GPU来完成渲染任务,而给CPU留出足够时间处理各种各样复杂的App逻辑。为此Core Animation做了大量的工作,尽量把渲染工作转换成适合GPU处理的形式(也就是所谓的硬件加速,如layer composition,设置backgroundColor等等)。 但是对于一些情况,如文字(CoreText使用CoreGraphics渲染)和图片(ImageIO)渲染,由于GPU并不擅长做这些工作,不得不先由CPU来处理好以后,再把结果作为texture传给GPU。除此以外,有时候也会遇到GPU实在忙不过来的情况,而CPU相对空闲(GPU瓶颈),这时可以让CPU分担一部分工作,提高整体效率。 ![img](https://pic2.zhimg.com/80/v2-2f45a2d49c0a4bb97ed975054f0534b5_720w.webp) 来自WWDC18 session 221,可以看到Core Text基于Core Graphics 一个典型的例子是,我们经常会使用CoreGraphics给图片加上圆角(将图片中圆角以外的部分渲染成透明)。整个过程全部是由CPU完成的。这样一来既然我们已经得到了想要的效果,就不需要再另外给图片容器设置cornerRadius。另一个好处是,我们可以灵活地控制裁剪和缓存的时机,巧妙避开CPU和GPU最繁忙的时段,达到平滑性能波动的目的。 这里有几个需要注意的点: - 渲染不是CPU的强项,调用CoreGraphics会消耗其相当一部分计算时间,并且我们也不愿意因此阻塞用户操作,因此一般来说CPU渲染都在后台线程完成(这也是AsyncDisplayKit的主要思想),然后再回到主线程上,把渲染结果传回CoreAnimation。这样一来,多线程间数据同步会增加一定的复杂度 - 同样因为CPU渲染速度不够快,因此只适合渲染静态的元素,如文字、图片(想象一下没有硬件加速的视频解码,性能惨不忍睹) - 作为渲染结果的bitmap数据量较大(形式上一般为解码后的UIImage),消耗内存较多,所以应该在使用完及时释放,并在需要的时候重新生成,否则很容易导致OOM - 如果你选择使用CPU来做渲染,那么就没有理由再触发GPU的离屏渲染了,否则会同时存在两块内容相同的内存,而且CPU和GPU都会比较辛苦 - 一定要使用Instruments的不同工具来测试性能,而不是仅凭猜测来做决定 即刻的优化 由于在iOS10之后,系统的设计风格慢慢从扁平化转变成圆角卡片,即刻的设计风格也随之发生变化,加入了大量圆角与阴影效果,如果在处理上稍有不慎,就很容易触发离屏渲染。为此我们采取了以下一些措施: - 即刻大量应用AsyncDisplayKit(Texture)作为主要渲染框架,对于文字和图片的异步渲染操作交由框架来处理。关于这方面可以看我[之前的一些介绍](https://link.zhihu.com/?target=https%3A//medium.com/jike-engineering/asyncdisplaykit%E4%BB%8B%E7%BB%8D-%E4%B8%80-6b871d29e005) - 对于图片的圆角,统一采用“precomposite”的策略,也就是不经由容器来做剪切,而是预先使用CoreGraphics为图片裁剪圆角 - 对于视频的圆角,由于实时剪切非常消耗性能,我们会创建四个白色弧形的layer盖住四个角,从视觉上制造圆角的效果 - 对于view的圆形边框,如果没有backgroundColor,可以放心使用cornerRadius来做 - 对于所有的阴影,使用shadowPath来规避离屏渲染 - 对于特殊形状的view,使用layer mask并打开shouldRasterize来对渲染结果进行缓存 - 对于模糊效果,不采用系统提供的UIVisualEffect,而是另外实现模糊效果(CIGaussianBlur),并手动管理渲染结果 原文https://zhuanlan.zhihu.com/p/72653360 ================================================ FILE: iOS资料/基于 AVFoundation 框架开发小视频功能的方案解析.md ================================================ # 基于 AVFoundation 框架开发小视频功能的方案解析 开发视频录制功能最简单的就是使用系统封装的 UIImagePickerController,但是这种方式比较封闭,可自定义东西比较少,所以就需要基于 AVFoundation 框架来开发视频录制功能。基于 AVFoundation 框架来开发,则需要自己手动设置设备音频,视频输入、输出。 AVCaptureSession 是 AVFoundation 的核心类,用于管理捕获对象 AVCaptureInput 的视频和音频的输入,协调捕获的输出 AVCaptureOutput。AVCaptureOutput 的输出有两种方法:一种是直接以 movieFileUrl 方式输出;一种是以原始数据流 data 的方式输出,流程对比图如下: ![img](https://p3-sign.toutiaoimg.com/pgc-image/RxRUAMm47IR6ec~noop.image?_iz=58558&from=article.pc_detail&x-expires=1679402928&x-signature=RgONu1b%2F4Ei41%2FEPFHeQK%2FgHFUA%3D) 下面详细讲解两种录制视频的方案: (1)AVCaptureSession + AVCaptureMovieFileOutput \1. 创建 AVCaptureSession ![img](https://p3-sign.toutiaoimg.com/pgc-image/RxRUANXIJMbVmK~noop.image?_iz=58558&from=article.pc_detail&x-expires=1679402928&x-signature=yBCiU2LM%2BWIG4omLjFh8QKPuQ38%3D) 注意:AVCaptureSession 的调用是会阻塞线程的,建议单独开辟子线程处理。 \2. 设置音频、视频输入 ![img](https://p3-sign.toutiaoimg.com/pgc-image/RxRUAOC9Pdz2dg~noop.image?_iz=58558&from=article.pc_detail&x-expires=1679402928&x-signature=DSEI%2B5GyPm2WXBogJyX4BtFAMhU%3D) \3. 设置文件输出源 ![img](https://p3-sign.toutiaoimg.com/pgc-image/RxRUAOoFvkrwiK~noop.image?_iz=58558&from=article.pc_detail&x-expires=1679402928&x-signature=BajEnK1168jjekZluniylzEMz3c%3D) 4.添加视频预览层 ![img](https://p3-sign.toutiaoimg.com/pgc-image/RxRUAPKEmV4zSM~noop.image?_iz=58558&from=article.pc_detail&x-expires=1679402928&x-signature=Eapgzv5PEm6de%2Bru%2BDj6rHbJ9yc%3D) 5.开始采集 ![img](https://p3-sign.toutiaoimg.com/pgc-image/RxRUAjU7v6S49S~noop.image?_iz=58558&from=article.pc_detail&x-expires=1679402928&x-signature=KTF8NOj%2Bqffd3eICGtd9lMVlBME%3D) 6.开始录制 当实际的录制开始或停止时,系统会有代理回调。当开始录制之后,这时可能还没有真正写入,真正开始写入会回调下面代理,停止录制也是如此,所以如果你需要对录制视频起始点操作,建议通过系统的回调代理: ![img](https://p3-sign.toutiaoimg.com/pgc-image/RxRUAlE83mp2bX~noop.image?_iz=58558&from=article.pc_detail&x-expires=1679402928&x-signature=cuTFuZzwvdn0VmF2fjgEekLq45o%3D) 7.停止录制 8.停止采集 ![img](https://p3-sign.toutiaoimg.com/pgc-image/RxRUAmQ670uSRF~noop.image?_iz=58558&from=article.pc_detail&x-expires=1679402928&x-signature=llMYqbnPk90D%2FHFyI5obaswDGHA%3D) (2)AVCaptureSession + AVAssetWriter 1.创建 AVCaptureSession ![img](https://p3-sign.toutiaoimg.com/pgc-image/RxRUB6H9WmgThE~noop.image?_iz=58558&from=article.pc_detail&x-expires=1679402928&x-signature=HZgyugmMYLUyJk%2BEX4OngjfFv54%3D) 2.设置音频、视频输入 ![img](https://p3-sign.toutiaoimg.com/pgc-image/RxRUB6u8SUIOh7~noop.image?_iz=58558&from=article.pc_detail&x-expires=1679402928&x-signature=9767NwZVrpAtJZpORThLBn%2FWbDo%3D) 3.设置音频 Data、视频 Data 输出 ![img](https://p3-sign.toutiaoimg.com/pgc-image/RxRUB7XEzmSLNE~noop.image?_iz=58558&from=article.pc_detail&x-expires=1679402928&x-signature=9iog%2FMynhccQza8NL%2B7hoOjBfwY%3D) 4.添加视频预览层 ![img](https://p3-sign.toutiaoimg.com/pgc-image/RxRUB8HHB4PvBN~noop.image?_iz=58558&from=article.pc_detail&x-expires=1679402928&x-signature=XOn1vzezzebsu4Lzd81AUCQawJU%3D) 5.开始采集 ![img](https://p3-sign.toutiaoimg.com/pgc-image/RxRUB8rDzx8b3n~noop.image?_iz=58558&from=article.pc_detail&x-expires=1679402928&x-signature=%2F2OsCj%2FOt6LRBMQWyXzGnZ4GVuM%3D) 和第一种方式不同,第一种方式是开始录制之后,movieFileOutput 的回调才会触发,停止录制回调触发之后也就完成了。AVCaptureSession + AVAssetWriter 方式因为在设置输出源的时候,把输出代理 (setSampleBufferDelegate)已经设置好了,所以一旦开始采集(startRunning),数据流回调也就触发了。 ![img](https://p3-sign.toutiaoimg.com/pgc-image/RxRUBTO4HgCQ8k~noop.image?_iz=58558&from=article.pc_detail&x-expires=1679402928&x-signature=AopDKBs4SWkxzvWfzIHZFoGDnfA%3D) 6.开始录制 这里需要创建AVAssetWriter,配置音频、视频录制参数,录制写入过程要单独开辟线程处理,避免阻塞线程,可以和 AVCaptureSession 放在同一线程处理。 ![img](https://p3-sign.toutiaoimg.com/pgc-image/RxRUBU84gXFUUO~noop.image?_iz=58558&from=article.pc_detail&x-expires=1679402928&x-signature=1nKhggm5%2BQ7hmvhh0QVFJUF%2FklM%3D) 7.处理数据流 开始采集,数据流就会回调,所以这里用了变量 isRecording 来界定当前是否需要对数据处理,一般是在 startRecord 和 stopRecord 才会去处理数据流。 ![img](https://p3-sign.toutiaoimg.com/pgc-image/RxRUBUj3y7d29w~noop.image?_iz=58558&from=article.pc_detail&x-expires=1679402928&x-signature=YTzwSf5vmzx%2FpXzyKOg6iKVtp70%3D) 8.停止录制 ![img](https://p3-sign.toutiaoimg.com/pgc-image/RxRUBVCJ9rl2pA~noop.image?_iz=58558&from=article.pc_detail&x-expires=1679402928&x-signature=lIUj3lW%2BQZssYs3Su989eY%2FOmAM%3D) 9.停止采集 ![img](https://p3-sign.toutiaoimg.com/pgc-image/RxRUBW8IKWCVyt~noop.image?_iz=58558&from=article.pc_detail&x-expires=1679402928&x-signature=haMWtqJeBEalGVIzYk%2FLwY7SZGg%3D) 两种方案对比: 相同点:他们的数据采集都是通过 AVCaptureSession 处理,音频视频的输入源也是一致的,画面预览一致。 不同点:输出源不一样,前者输出是 fileUrl,也就是说在视频写入完成之前开发者无法操作处理;后者输出是 data,AVAssetWriter 需要拿到 AVCaptureVideoDataOutput 和 AVCaptureAudioDataOutput 两个单独的输出,然后分别处理再写入指定路径。输出方式不同,决定了开发者对视频处理剪裁压缩的方式也就不同,前者如果需要对视频剪裁压缩,就需要从本地取出完整的视频文件,再做处理;而 AVAssetWriter 拿到的是数据流 data,如果需要剪裁压缩,可以直接配置相关参数后处理数据流,这样写入本地的就是已经处理过的视频文件。 其他功能点扩充 1.聚焦处理 ![img](https://p3-sign.toutiaoimg.com/pgc-image/RxRUBx2FBnvf3y~noop.image?_iz=58558&from=article.pc_detail&x-expires=1679402928&x-signature=MGRO3ihlKgJH6tE27gl%2F%2FkhdNgE%3D) 2.摄像头切换 ![img](https://p3-sign.toutiaoimg.com/pgc-image/RxRUBxYBIBi76o~noop.image?_iz=58558&from=article.pc_detail&x-expires=1679402928&x-signature=LG8CnneIYluAk6VesBSP%2FgKgAUo%3D) 原文https://www.toutiao.com/article/6820684475089814030/?channel=&source=search_tab ================================================ FILE: iOS资料/最简单的基于FFmpeg的移动端例子:IOS HelloWorld.md ================================================ # 最简单的基于FFmpeg的移动端例子:IOS HelloWorld 本文记录IOS平台下基于FFmpeg的HelloWorld程序。该示例C语言的源代码来自于《最简单的基于FFMPEG的Helloworld程序》。相关的概念就不再重复记录了。 ![img](https://img-blog.csdn.net/20150801182605734) **IOS程序使用FFmpeg类库的说明** IOS应用程序使用FFmpeg类库的流程如下所示。 # 1、编译FFmpeg类库 编译IOS的FFmpeg类库需要支持5种架构:armv7、armv7s、arm64、i386、x86_64。其中前面3个是给真机使用的,后面2个是给模拟器使用的。本文记录的FFmpeg类库还支持第三方类库libx264和libfaac,所以在编译之前还要先编译libx264和libfaac的源代码。总体说来,IOS下的类库需要编译成两个版本:thin和fat。每种架构对应一个thin版本的类库,将这些不同架构thin版本的类库合成起来之后,就形成了fat版本的类库。下面简单记录一下编译步骤。编译过程中IOS SDK版本为8.3,FFmpeg版本为2.7.1,faac和x264分别使用了最新版本的源代码。 ### 1.1 第三方库libx264的编译 这一步用于生成支持armv7、armv7s、arm64、i386、x86_64几种架构的fat版本的libx264.a。下面这个脚本可以首先编译生成上面5种架构的thin版本的libx264.a,分成5个文件夹存储于thin-x264文件夹中;然后将这些类库合并成为1个fat版本的libx264.a,存储于fat-x264文件夹中。 #### 1.1.1 build_x264.sh ``` #!/bin/sh # LXH,MXY # # directories SOURCE="x264" FAT="fat-x264" SCRATCH="scratch-x264" # must be an absolute path THIN=`pwd`/"thin-x264" #This is decided by your SDK version. SDK_VERSION="8.3" cd ./x264 #============== simulator =============== PLATFORM="iPhoneSimulator" #i386 ARCHS="i386" export DEVROOT=/Applications/Xcode.app/Contents/Developer/Platforms/${PLATFORM}.platform/Developer export SDKROOT=$DEVROOT/SDKs/${PLATFORM}${SDK_VERSION}.sdk export CC=$DEVROOT/usr/bin/gcc export LD=$DEVROOT/usr/bin/ld export CXX=$DEVROOT/usr/bin/g++ export LIBTOOL=$DEVROOT/usr/bin/libtool export HOST=i386-apple-darwin COMMONFLAGS="-pipe -gdwarf-2 -no-cpp-precomp -isysroot ${SDKROOT} -fPIC" export LDFLAGS="${COMMONFLAGS} -fPIC" export CFLAGS="${COMMONFLAGS} -fvisibility=hidden" for ARCH in $ARCHS; do echo "Building $ARCH ......" make clean ./configure \ --host=i386-apple-darwin \ --sysroot=$SDKROOT \ --prefix="$THIN/$ARCH" \ --extra-cflags="-arch $ARCH -miphoneos-version-min=6.0" \ --extra-ldflags="-L$SDKROOT/usr/lib/system -arch $ARCH -miphoneos-version-min=6.0" \ --enable-pic \ --enable-static \ --disable-asm \ make && make install && make clean echo "Installed: $DEST/$ARCH" done #x86_64 ARCHS="x86_64" unset DEVROOT unset SDKROOT unset CC unset LD unset CXX unset LIBTOOL unset HOST unset LDFLAGS unset CFLAGS make clean for ARCH in $ARCHS; do echo "Building $ARCH ......" ./configure \ --prefix="$THIN/$ARCH" \ --enable-pic \ --enable-static \ --disable-asm \ make && make install && make clean echo "Installed: $DEST/$ARCH" done #================ iphone ================== export PLATFORM="iPhoneOS" ARCHS="arm64 armv7 armv7s " export DEVROOT=/Applications/Xcode.app/Contents/Developer export SDKROOT=$DEVROOT/Platforms/${PLATFORM}.platform/Developer/SDKs/${PLATFORM}${SDK_VERSION}.sdk #DEVPATH=/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS${SDK_VERSION}.sdk export CC=$DEVROOT/Toolchains/XcodeDefault.xctoolchain/usr/bin/clang export AS=$DEVROOT/Toolchains/XcodeDefault.xctoolchain/usr/bin/as COMMONFLAGS="-pipe -gdwarf-2 -no-cpp-precomp -isysroot ${SDKROOT} -marm -fPIC" export LDFLAGS="${COMMONFLAGS} -fPIC" export CFLAGS="${COMMONFLAGS} -fvisibility=hidden" export CXXFLAGS="${COMMONFLAGS} -fvisibility=hidden -fvisibility-inlines-hidden" for ARCH in $ARCHS; do echo "Building $ARCH ......" ./configure \ --host=arm-apple-darwin \ --sysroot=$DEVPATH \ --prefix="$THIN/$ARCH" \ --extra-cflags="-arch $ARCH" \ --extra-ldflags="-L$DEVPATH/usr/lib/system -arch $ARCH" \ --enable-pic \ --enable-static \ --disable-asm make && make install && make clean echo "Installed: $DEST/$ARCH" done cd .. #================ fat lib =================== ARCHS="armv7 armv7s i386 x86_64 arm64" echo "building fat binaries..." mkdir -p $FAT/lib set - $ARCHS CWD=`pwd` cd $THIN/$1/lib for LIB in *.a do cd $CWD lipo -create `find $THIN -name $LIB` -output $FAT/lib/$LIB done cd $CWD cp -rf $THIN/$1/include $FAT ``` ### 1.2 第三方库libfaac的编译 这一步用于生成支持armv7、armv7s、arm64、i386、x86_64几种架构的fat版本的libfaac.a。下面这个脚本可以首先编译生成上面5种架构的thin版本的libfaac.a,分成5个文件夹存储于fat-faac中;然后将这些类库合并成为1个fat版本的libfaac.a,存储于fat-faac中。 #### 1.2.1build_faac.sh ``` #!/bin/sh cd ./faac make distclean cd .. CONFIGURE_FLAGS="--enable-static --with-pic" ARCHS="arm64 armv7s x86_64 i386 armv7" # directories SOURCE="faac" FAT="fat-faac" SCRATCH="scratch-faac" # must be an absolute path THIN=`pwd`/"thin-faac" COMPILE="y" LIPO="y" if [ "$*" ] then if [ "$*" = "lipo" ] then # skip compile COMPILE= else ARCHS="$*" if [ $# -eq 1 ] then # skip lipo LIPO= fi fi fi if [ "$COMPILE" ] then CWD=`pwd` for ARCH in $ARCHS do echo "building $ARCH..." mkdir -p "$SCRATCH/$ARCH" cd "$SCRATCH/$ARCH" if [ "$ARCH" = "i386" -o "$ARCH" = "x86_64" ] then PLATFORM="iPhoneSimulator" CPU= if [ "$ARCH" = "x86_64" ] then SIMULATOR="-mios-simulator-version-min=7.0" HOST= else SIMULATOR="-mios-simulator-version-min=5.0" HOST="--host=i386-apple-darwin" fi else PLATFORM="iPhoneOS" if [ $ARCH = "armv7s" ] then CPU="--cpu=swift" else CPU= fi SIMULATOR= HOST="--host=arm-apple-darwin" fi XCRUN_SDK=`echo $PLATFORM | tr '[:upper:]' '[:lower:]'` CC="xcrun -sdk $XCRUN_SDK clang -Wno-error=unused-command-line-argument-hard-error-in-future" AS="/usr/local/bin/gas-preprocessor.pl $CC" CFLAGS="-arch $ARCH $SIMULATOR" CXXFLAGS="$CFLAGS" LDFLAGS="$CFLAGS" CC=$CC CFLAGS=$CXXFLAGS LDFLAGS=$LDFLAGS CPPFLAGS=$CXXFLAGS CXX=$CC CXXFLAGS=$CXXFLAGS $CWD/$SOURCE/configure \ $CONFIGURE_FLAGS \ $HOST \ --prefix="$THIN/$ARCH" \ --disable-shared \ --without-mp4v2 make clean && make && make install-strip cd $CWD done fi #================ fat lib =================== echo "building fat binaries..." mkdir -p $FAT/lib set - $ARCHS CWD=`pwd` cd $THIN/$1/lib for LIB in *.a do cd $CWD lipo -create `find $THIN -name $LIB` -output $FAT/lib/$LIB done cd $CWD cp -rf $THIN/$1/include $FAT ``` ### 1.3 编译armv7版本FFmpeg类库 这一步用于生成支持armv7架构的thin版本的FFmpeg类库,存储于thin-ffmpeg/armv7文件夹中。脚本如下所示。 #### 1.3.1 build_ffmpeg_demo_armv7.sh ``` #!/bin/sh # LXH,MXY modified cd ffmpeg PLATFORM="iPhoneOS" INSTALL="thin-ffmpeg" SDK_VERSION="8.3" # libx264 export X264ROOT=../thin-x264/armv7 export X264LIB=$X264ROOT/lib export X264INCLUDE=$X264ROOT/include # libfaac export FAACROOT=../thin-faac/armv7 export FAACLIB=$FAACROOT/lib export FAACINCLUDE=$FAACROOT/include export DEVROOT=/Applications/Xcode.app/Contents/Developer export SDKROOT=$DEVROOT/Platforms/${PLATFORM}.platform/Developer/SDKs/${PLATFORM}${SDK_VERSION}.sdk export CC=$DEVROOT/Toolchains/XcodeDefault.xctoolchain/usr/bin/clang export AS=$DEVROOT/Toolchains/XcodeDefault.xctoolchain/usr/bin/as COMMONFLAGS="-pipe -gdwarf-2 -no-cpp-precomp -isysroot ${SDKROOT} -marm -fPIC" export LDFLAGS="${COMMONFLAGS} -fPIC" export CFLAGS="${COMMONFLAGS} -fvisibility=hidden" export CXXFLAGS="${COMMONFLAGS} -fvisibility=hidden -fvisibility-inlines-hidden" echo "Building armv7..." make clean ./configure \ --cpu=cortex-a9 \ --extra-cflags='-I$X264INCLUDE -I$FAACINCLUDE -arch armv7 -miphoneos-version-min=5.0 -mthumb' \ --extra-ldflags='-L$X264LIB -L$FAACLIB -arch armv7 -miphoneos-version-min=5.0' \ --enable-cross-compile \ --arch=arm --disable-iconv\ --target-os=darwin \ --cc=${CC} --disable-asm\ --sysroot=${SDKROOT} \ --prefix=../${INSTALL}/armv7 \ --enable-gpl --enable-nonfree --enable-version3 --disable-bzlib --enable-small --disable-vda \ --disable-encoders --enable-libx264 --enable-libfaac --enable-encoder=libx264 --enable-encoder=libfaac \ --disable-muxers --enable-muxer=flv --enable-muxer=mov --enable-muxer=ipod --enable-muxer=mpegts --enable-muxer=psp --enable-muxer=mp4 --enable-muxer=avi \ --disable-decoders --enable-decoder=aac --enable-decoder=aac_latm --enable-decoder=h264 --enable-decoder=mpeg4 \ --disable-demuxers --enable-demuxer=flv --enable-demuxer=h264 --enable-demuxer=mpegts --enable-demuxer=avi --enable-demuxer=mpc --enable-demuxer=mov \ --disable-parsers --enable-parser=aac --enable-parser=ac3 --enable-parser=h264 \ --disable-protocols --enable-protocol=file --enable-protocol=rtmp --enable-protocol=rtp --enable-protocol=udp \ --disable-bsfs --enable-bsf=aac_adtstoasc --enable-bsf=h264_mp4toannexb \ --disable-devices --disable-debug --disable-ffmpeg --disable-ffprobe --disable-ffplay --disable-ffserver --disable-debug make make install cd .. ``` ### 1.4 编译armv7s版本FFmpeg类库 这一步用于生成支持armv7s架构的thin版本的FFmpeg类库,存储于thin-ffmpeg/armv7s文件夹中。脚本如下所示。 #### 1.4.1 build_ffmpeg_demo_armv7s.sh ``` #!/bin/sh # LXH,MXY modified cd ffmpeg PLATFORM="iPhoneOS" INSTALL="thin-ffmpeg" SDK_VERSION="8.3" # libx264 export X264ROOT=../thin-x264/armv7s export X264LIB=$X264ROOT/lib export X264INCLUDE=$X264ROOT/include # libfaac export FAACROOT=../thin-faac/armv7s export FAACLIB=$FAACROOT/lib export FAACINCLUDE=$FAACROOT/include export DEVROOT=/Applications/Xcode.app/Contents/Developer export SDKROOT=$DEVROOT/Platforms/${PLATFORM}.platform/Developer/SDKs/${PLATFORM}${SDK_VERSION}.sdk export CC=$DEVROOT/Toolchains/XcodeDefault.xctoolchain/usr/bin/clang export AS=$DEVROOT/Toolchains/XcodeDefault.xctoolchain/usr/bin/as COMMONFLAGS="-pipe -gdwarf-2 -no-cpp-precomp -isysroot ${SDKROOT} -marm -fPIC" export LDFLAGS="${COMMONFLAGS} -fPIC" export CFLAGS="${COMMONFLAGS} -fvisibility=hidden" export CXXFLAGS="${COMMONFLAGS} -fvisibility=hidden -fvisibility-inlines-hidden" echo "Building armv7s..." make clean ./configure \ --cpu=cortex-a9 \ --extra-cflags='-I$X264INCLUDE -I$FAACINCLUDE -arch armv7s -miphoneos-version-min=5.0 -mthumb' \ --extra-ldflags='-L$X264LIB -L$FAACLIB -arch armv7s -miphoneos-version-min=5.0' \ --enable-cross-compile \ --arch=arm --disable-iconv\ --target-os=darwin \ --cc=${CC} --disable-asm \ --sysroot=${SDKROOT} \ --prefix=../${INSTALL}/armv7s \ --enable-gpl --enable-nonfree --enable-version3 --disable-bzlib --enable-small --disable-vda \ --disable-encoders --enable-libx264 --enable-libfaac --enable-encoder=libx264 --enable-encoder=libfaac \ --disable-muxers --enable-muxer=flv --enable-muxer=mov --enable-muxer=ipod --enable-muxer=mpegts --enable-muxer=psp --enable-muxer=mp4 --enable-muxer=avi \ --disable-decoders --enable-decoder=aac --enable-decoder=aac_latm --enable-decoder=h264 --enable-decoder=mpeg4 \ --disable-demuxers --enable-demuxer=flv --enable-demuxer=h264 --enable-demuxer=avi --enable-demuxer=mpegts --enable-demuxer=mpc --enable-demuxer=mov \ --disable-parsers --enable-parser=aac --enable-parser=ac3 --enable-parser=h264 \ --disable-protocols --enable-protocol=file --enable-protocol=rtmp --enable-protocol=rtp --enable-protocol=udp \ --disable-bsfs --enable-bsf=aac_adtstoasc --enable-bsf=h264_mp4toannexb \ --disable-devices --disable-debug --disable-ffmpeg --disable-ffprobe --disable-ffplay --disable-ffserver --disable-debug make make install cd .. ``` ### 1.5 编译arm64版本FFmpeg类库 这一步用于生成支持arm64架构的thin版本的FFmpeg类库,存储于thin-ffmpeg/arm64文件夹中。脚本如下所示。 #### 1.5.1 build_ffmpeg_demo_arm64.sh ``` #!/bin/sh # LXH,MXY modified cd ffmpeg PLATFORM="iPhoneOS" INSTALL="thin-ffmpeg" SDK_VERSION="8.3" # libx264 export X264ROOT=../thin-x264/arm64 export X264LIB=$X264ROOT/lib export X264INCLUDE=$X264ROOT/include # libfaac export FAACROOT=../thin-faac/arm64 export FAACLIB=$FAACROOT/lib export FAACINCLUDE=$FAACROOT/include export DEVROOT=/Applications/Xcode.app/Contents/Developer export SDKROOT=$DEVROOT/Platforms/${PLATFORM}.platform/Developer/SDKs/${PLATFORM}${SDK_VERSION}.sdk export CC=$DEVROOT/Toolchains/XcodeDefault.xctoolchain/usr/bin/clang export AS=$DEVROOT/Toolchains/XcodeDefault.xctoolchain/usr/bin/as COMMONFLAGS="-pipe -gdwarf-2 -no-cpp-precomp -isysroot ${SDKROOT} -marm -fPIC" export LDFLAGS="${COMMONFLAGS} -fPIC" export CFLAGS="${COMMONFLAGS} -fvisibility=hidden" export CXXFLAGS="${COMMONFLAGS} -fvisibility=hidden -fvisibility-inlines-hidden" echo "Building arm64..." make clean ./configure \ --extra-cflags='-I$X264INCLUDE -I$FAACINCLUDE -arch arm64 -miphoneos-version-min=5.0 -mthumb' \ --extra-ldflags='-L$X264LIB -L$FAACLIB -arch arm64 -miphoneos-version-min=5.0' \ --enable-cross-compile \ --arch=arm --disable-iconv \ --target-os=darwin \ --cc=${CC} --disable-asm \ --sysroot=${SDKROOT} \ --prefix=../${INSTALL}/arm64 \ --enable-gpl --enable-nonfree --enable-version3 --disable-bzlib --enable-small --disable-vda \ --disable-encoders --enable-libx264 --enable-libfaac --enable-encoder=libx264 --enable-encoder=libfaac \ --disable-muxers --enable-muxer=flv --enable-muxer=mov --enable-muxer=ipod --enable-muxer=mpegts --enable-muxer=psp --enable-muxer=mp4 --enable-muxer=avi \ --disable-decoders --enable-decoder=aac --enable-decoder=aac_latm --enable-decoder=h264 --enable-decoder=mpeg4 \ --disable-demuxers --enable-demuxer=flv --enable-demuxer=h264 --enable-demuxer=avi --enable-demuxer=mpegts --enable-demuxer=mpc --enable-demuxer=mov \ --disable-parsers --enable-parser=aac --enable-parser=ac3 --enable-parser=h264 \ --disable-protocols --enable-protocol=file --enable-protocol=rtmp --enable-protocol=rtp --enable-protocol=udp \ --disable-bsfs --enable-bsf=aac_adtstoasc --enable-bsf=h264_mp4toannexb \ --disable-devices --disable-debug --disable-ffmpeg --disable-ffprobe --disable-ffplay --disable-ffserver --disable-debug make make install cd .. ``` ### 1.6 编译i386版本FFmpeg类库 这一步用于生成支持i386架构的thin版本的FFmpeg类库,存储于thin-ffmpeg/i386文件夹中。脚本如下所示。 #### 1.6.1 build_ffmpeg_demo_i386.sh ``` build_ffmpeg_demo_i386.sh #!/bin/sh # LXH,MXY modified cd ffmpeg PLATFORM="iPhoneSimulator" INSTALL="thin-ffmpeg" SDK_VERSION="8.3" # libx264 export X264ROOT=../thin-x264/i386 export X264LIB=$X264ROOT/lib export X264INCLUDE=$X264ROOT/include # libfaac export FAACROOT=../thin-faac/i386 export FAACLIB=$FAACROOT/lib export FAACINCLUDE=$FAACROOT/include export DEVROOT=/Applications/Xcode.app/Contents/Developer/Platforms/${PLATFORM}.platform/Developer export SDKROOT=$DEVROOT/SDKs/${PLATFORM}${SDK_VERSION}.sdk export CC=$DEVROOT/usr/bin/gcc export LD=$DEVROOT/usr/bin/ld export CXX=$DEVROOT/usr/bin/g++ export LIBTOOL=$DEVROOT/usr/bin/libtool COMMONFLAGS="-pipe -gdwarf-2 -no-cpp-precomp -isysroot ${SDKROOT} -fPIC" export LDFLAGS="${COMMONFLAGS} -fPIC" export CFLAGS="${COMMONFLAGS} -fvisibility=hidden" echo "Building i386..." make clean ./configure \ --cpu=i386 \ --extra-cflags='-I$X264INCLUDE -I$FAACINCLUDE -arch i386 -miphoneos-version-min=5.0' \ --extra-ldflags='-L$X264LIB -L$FAACLIB -arch i386 -miphoneos-version-min=5.0' \ --enable-cross-compile \ --arch=i386 --disable-iconv \ --target-os=darwin \ --cc=${CC} \ --sysroot=${SDKROOT} \ --prefix=../${INSTALL}/i386 \ --enable-gpl --enable-nonfree --enable-version3 --disable-bzlib --enable-small --disable-vda \ --disable-encoders --enable-libx264 --enable-libfaac --enable-encoder=libx264 --enable-encoder=libfaac \ --disable-muxers --enable-muxer=flv --enable-muxer=mov --enable-muxer=mpegts --enable-muxer=ipod --enable-muxer=psp --enable-muxer=mp4 --enable-muxer=avi \ --disable-decoders --enable-decoder=aac --enable-decoder=aac_latm --enable-decoder=h264 --enable-decoder=mpeg4 \ --disable-demuxers --enable-demuxer=flv --enable-demuxer=h264 --enable-demuxer=mpegts --enable-demuxer=avi --enable-demuxer=mpc --enable-demuxer=mov \ --disable-parsers --enable-parser=aac --enable-parser=ac3 --enable-parser=h264 \ --disable-protocols --enable-protocol=file --enable-protocol=rtmp --enable-protocol=rtp --enable-protocol=udp \ --disable-bsfs --enable-bsf=aac_adtstoasc --enable-bsf=h264_mp4toannexb \ --disable-devices --disable-debug --disable-ffmpeg --disable-ffprobe --disable-ffplay --disable-ffserver --disable-debug make make install cd .. ``` ### 1.7 编译x86_64版本FFmpeg类库 这一步用于生成支持x86_64架构的thin版本的FFmpeg类库,存储于thin-ffmpeg/x86_64文件夹中。脚本如下所示。 #### 1.7.1 build_ffmpeg_demo_x86_64.sh ``` #!/bin/sh # LXH,MXY modified cd ./ffmpeg INSTALL="thin-ffmpeg" # libx264 export X264ROOT=../thin-x264/x86_64 export X264LIB=$X264ROOT/lib export X264INCLUDE=$X264ROOT/include # libfaac export FAACROOT=../thin-faac/x86_64 export FAACLIB=$FAACROOT/lib export FAACINCLUDE=$FAACROOT/include unset DEVROOT unset SDKROOT unset CC unset LD unset CXX unset LIBTOOL unset HOST unset LDFLAGS unset CFLAGS echo "Building x86_64..." make clean ./configure \ --extra-cflags='-I$X264INCLUDE -I$FAACINCLUDE' \ --extra-ldflags='-L$X264LIB -L$FAACLIB' \ --disable-iconv \ --disable-asm \ --prefix=../${INSTALL}/x86_64 \ --enable-gpl --enable-nonfree --enable-version3 --disable-bzlib --enable-small --disable-vda \ --disable-encoders --enable-libx264 --enable-libfaac --enable-encoder=libx264 --enable-encoder=mpeg2video --enable-encoder=libfaac \ --disable-muxers --enable-muxer=flv --enable-muxer=mov --enable-muxer=ipod --enable-muxer=mpegts --enable-muxer=psp --enable-muxer=mp4 --enable-muxer=avi \ --disable-decoders --enable-decoder=aac --enable-decoder=mpeg2video --enable-decoder=aac_latm --enable-decoder=h264 --enable-decoder=mpeg4 \ --disable-demuxers --enable-demuxer=flv --enable-demuxer=h264 --enable-demuxer=avi --enable-demuxer=mpegts --enable-demuxer=mpc --enable-demuxer=mov \ --disable-parsers --enable-parser=aac --enable-parser=ac3 --enable-parser=h264 \ --disable-protocols --enable-protocol=file --enable-protocol=rtmp --enable-protocol=rtp --enable-protocol=udp \ --enable-bsf=aac_adtstoasc --enable-bsf=h264_mp4toannexb \ --disable-devices --disable-debug --disable-ffmpeg --disable-ffprobe --disable-ffplay --disable-ffserver --disable-debug make make install cd .. ``` (8) 打包类库 这一步用于将上述步骤中生成的5个版本的FFmpeg打包生成fat版本的FFmpeg类库。这一步骤执行完毕后,将thin-ffmpeg中几个thin版本的类库合并为一个fat版本的类库,并存储于fat-ffmpeg文件夹中。脚本如下所示。 build_ffmpeg_fat.sh ``` #!/bin/sh # directories THIN=`pwd`/"thin-ffmpeg" FAT=`pwd`/"fat-ffmpeg" CWD=`pwd` # must be an absolute path ARCHS="armv7s i386 armv7 arm64 x86_64" echo "building fat binaries..." mkdir -p $FAT/lib set - $ARCHS cd thin-ffmpeg/$1/lib for LIB in *.a do cd $CWD lipo -create `find $THIN -name $LIB` -output $FAT/lib/$LIB done cd $CWD cp -rf $THIN/$1/include $FAT ``` 生成完fat版本的类库后,可以在命令行使用lipo命令查看类库的架构,如下所示。 ``` lipo -info libavcodec.a ``` 2. 编写IOS程序 编写包含FFmpeg类库支持的IOS程序分成两步:配置Xcode环境,编写C语言代码。 (1) 配置Xcode环境 下面以Xcode的IOS中的Single View Application为例,记录一下配置步骤: (a) 拷贝头文件所在的include文件夹和fat版本的FFmpeg类库(包括libavformat.a, libavcodec.a, libavutil.a, libavdevice.a, libavfilter.a, libpostproc.a, libswresample.a, libswscale.a;以及第三方fat版本类库libx264.a, libfaac.a)至项目文件夹。并将它们添加至项目中。 (b) 项目属性->Build Settings中配置以下3个选项。 Linking->Other Linker Flags中添加下面内容: ``` -lavformat -lavcodec -lavutil -lavdevice -lavfilter -lpostproc -lswresample -lswscale -lx264 -lfaac ``` Search Paths->Header Search Paths添加下面内容 ``` $(PROJECT_DIR)/include ``` Search Paths->Library Search Paths添加下面内容 ``` $(PROJECT_DIR) ``` 其它的一些配置。这些配置随着FFmpeg版本的不同而有略微的不同(在某些情况下也可能不需要配置)。我目前使用的2.7.1版本的FFmpeg需要配置下面的选项。 项目属性->General->Linked Frameworks and Libraries中添加两个类库:AVFoundation.framework和libz.dylib。 (2) 编写C语言代码 做好上面配置后,就可以在项目中编写代码测试一下FFmpeg是否正确配置了。由于IOS使用的Objective-C是兼容C语言的,所以可以直接写C语言代码调用FFmpeg。可以在ViewController.m中的viewDidLoad()函数中添加一行printf()代码打印FFmpeg类库的版本信息,如下所示。 ``` #import "ViewController.h" #include @interface ViewController () @end @implementation ViewController - (void)viewDidLoad { [super viewDidLoad]; // Do any additional setup after loading the view, typically from a nib. printf("%s",avcodec_configuration()); } - (void)didReceiveMemoryWarning { [super didReceiveMemoryWarning]; // Dispose of any resources that can be recreated. } @end ``` 如果类库编译无误,启动IOS程序的时候会在控制台打印版本信息。 ## 2、源代码 项目的目录结构如图所示。 ![img](https://img-blog.csdn.net/20150726212805864) C代码位于ViewController.m文件中,内容如下所示。 ``` /** * 最简单的基于FFmpeg的HelloWorld程序 - IOS * Simplest FFmpeg Helloworld - IOS * * 雷霄骅 Lei Xiaohua * leixiaohua1020@126.com * 中国传媒大学/数字电视技术 * Communication University of China / Digital TV Technology * http://blog.csdn.net/leixiaohua1020 * * 本程序可以获得FFmpeg类库相关的信息。 * This software can get information about FFmpeg libraries. * */ #import "ViewController.h" #include #include #include @interface ViewController () @end @implementation ViewController - (void)viewDidLoad { [super viewDidLoad]; // Do any additional setup after loading the view, typically from a nib. av_register_all(); char info[10000] = { 0 }; printf("%s\n", avcodec_configuration()); sprintf(info, "%s\n", avcodec_configuration()); NSString * info_ns = [NSString stringWithFormat:@"%s", info]; self.content.text=info_ns; } - (void)didReceiveMemoryWarning { [super didReceiveMemoryWarning]; // Dispose of any resources that can be recreated. } - (IBAction)clickProtocolButton:(id)sender { //Alert /* UIAlertView *alter = [[UIAlertView alloc] initWithTitle:@"Title" message:@"This is content" delegate:nil cancelButtonTitle:@"Close" otherButtonTitles:nil]; [alter show]; */ char info[40000]={0}; av_register_all(); struct URLProtocol *pup = NULL; //Input struct URLProtocol **p_temp = &pup; avio_enum_protocols((void **)p_temp, 0); while ((*p_temp) != NULL){ sprintf(info, "%s[In ][%10s]\n", info, avio_enum_protocols((void **)p_temp, 0)); } pup = NULL; //Output avio_enum_protocols((void **)p_temp, 1); while ((*p_temp) != NULL){ sprintf(info, "%s[Out][%10s]\n", info, avio_enum_protocols((void **)p_temp, 1)); } //printf("%s", info); NSString * info_ns = [NSString stringWithFormat:@"%s", info]; self.content.text=info_ns; } - (IBAction)clickAVFormatButton:(id)sender { char info[40000] = { 0 }; av_register_all(); AVInputFormat *if_temp = av_iformat_next(NULL); AVOutputFormat *of_temp = av_oformat_next(NULL); //Input while(if_temp!=NULL){ sprintf(info, "%s[In ]%10s\n", info, if_temp->name); if_temp=if_temp->next; } //Output while (of_temp != NULL){ sprintf(info, "%s[Out]%10s\n", info, of_temp->name); of_temp = of_temp->next; } //printf("%s", info); NSString * info_ns = [NSString stringWithFormat:@"%s", info]; self.content.text=info_ns; } - (IBAction)clickAVCodecButton:(id)sender { char info[40000] = { 0 }; av_register_all(); AVCodec *c_temp = av_codec_next(NULL); while(c_temp!=NULL){ if (c_temp->decode!=NULL){ sprintf(info, "%s[Dec]", info); } else{ sprintf(info, "%s[Enc]", info); } switch (c_temp->type){ case AVMEDIA_TYPE_VIDEO: sprintf(info, "%s[Video]", info); break; case AVMEDIA_TYPE_AUDIO: sprintf(info, "%s[Audio]", info); break; default: sprintf(info, "%s[Other]", info); break; } sprintf(info, "%s%10s\n", info, c_temp->name); c_temp=c_temp->next; } //printf("%s", info); NSString * info_ns = [NSString stringWithFormat:@"%s", info]; self.content.text=info_ns; } - (IBAction)clickAVFilterButton:(id)sender { char info[40000] = { 0 }; avfilter_register_all(); AVFilter *f_temp = (AVFilter *)avfilter_next(NULL); while (f_temp != NULL){ sprintf(info, "%s[%10s]\n", info, f_temp->name); } //printf("%s", info); NSString * info_ns = [NSString stringWithFormat:@"%s", info]; self.content.text=info_ns; } - (IBAction)clickConfigurationButton:(id)sender { char info[10000] = { 0 }; av_register_all(); sprintf(info, "%s\n", avcodec_configuration()); //printf("%s", info); //self.content.text=@"Lei Xiaohua"; NSString * info_ns = [NSString stringWithFormat:@"%s", info]; self.content.text=info_ns; } @end ``` ## 3、运行结果 App在手机上运行后的结果如下图所示。 ![img](https://img-blog.csdn.net/20150726212715269) 单击不同的按钮,可以得到类库不同方面的信息。单击“Protocol”按钮内容如下所示。 ![img](https://img-blog.csdn.net/20150726212728045) 单击“AVFormat”按钮内容如下所示。 ![img](https://img-blog.csdn.net/20150726212855691) 单击“AVCodec”按钮内容如下所示。 ![img](https://img-blog.csdn.net/20150726212930214) 单击“Configure”按钮即为程序开始运行时候的内容。 ## 4、下载 **simplest ffmpeg mobile** **项目主页** Github:https://github.com/leixiaohua1020/simplest_ffmpeg_mobile 开源中国:https://git.oschina.net/leixiaohua1020/simplest_ffmpeg_mobile SourceForge:https://sourceforge.net/projects/simplestffmpegmobile/ CSDN工程下载地址: http://download.csdn.net/detail/leixiaohua1020/8924391 本解决方案包含了使用FFmpeg在移动端处理多媒体的各种例子: > [Android] > simplest_android_player: 基于安卓接口的视频播放器 > simplest_ffmpeg_android_helloworld: 安卓平台下基于FFmpeg的HelloWorld程序 > simplest_ffmpeg_android_decoder: 安卓平台下最简单的基于FFmpeg的视频解码器 > simplest_ffmpeg_android_decoder_onelib: 安卓平台下最简单的基于FFmpeg的视频解码器-单库版 > simplest_ffmpeg_android_streamer: 安卓平台下最简单的基于FFmpeg的推流器 > simplest_ffmpeg_android_transcoder: 安卓平台下移植的FFmpeg命令行工具 > simplest_sdl_android_helloworld: 移植SDL到安卓平台的最简单程序 > [IOS] > simplest_ios_player: 基于IOS接口的视频播放器 > simplest_ffmpeg_ios_helloworld: IOS平台下基于FFmpeg的HelloWorld程序 > simplest_ffmpeg_ios_decoder: IOS平台下最简单的基于FFmpeg的视频解码器 > simplest_ffmpeg_ios_streamer: IOS平台下最简单的基于FFmpeg的推流器 > simplest_ffmpeg_ios_transcoder: IOS平台下移植的ffmpeg.c命令行工具 > simplest_sdl_ios_helloworld: 移植SDL到IOS平台的最简单程序 原文链接:https://blog.csdn.net/leixiaohua1020/article/details/47071547 ================================================ FILE: iOS资料/视频直播iOS端技术.md ================================================ # 视频直播iOS端技术 直播架构   想必了解过直播的人都清楚直播主要分为3部分:推流->流媒体服务器->拉流。 ![img](https://pic3.zhimg.com/80/v2-7fba8f181122e09818e7c7592caf88fe_720w.webp)   而我们今天需要讲的就是推流这部分,它主要包括音视频采集,音视频前处理,音视频编码,推流和传输4个方面。但是由于网络的复杂性和大数据的统计,推流还需要有全局负载均衡调度GSLB(Global Server Load Balance),以及实时的统计数据上报服务器,包括提供频道管理给用户运营,因此推流SDK需要接入GSLB中心调度,统计服务器,心跳服务器,用于推流分配到网络最好的节点,有大数据的统计和分析。 ![img](https://pic4.zhimg.com/80/v2-4a9b30dd3de6f53a4bc46e23b25722af_720w.webp)   下图涵盖了直播相关的所有服务,红色小标的线条代表指令流向,绿色小标的线条代表数据流向。 ![img](https://pic3.zhimg.com/80/v2-d924c2a29a9e365c811b0feb89bfd96e_720w.webp)   ●●● 直播技术点 ![img](https://pic4.zhimg.com/80/v2-99dacb8cd68341a92d0344bdd76d9477_720w.webp)   音视频采集   采集是所有环节中的第一环,网易云通信与视频使用的系统原生框架AVFoundation采集数据。通过iPhone摄像头(AVCaptureSession)采集视频数据,通过麦克风(AudioUnit)采集音频数据。目前视频的采集源主要来自摄像头采集、屏幕录制(ReplayKit)、从视频文件读取推流。   音视频都支持参数配置。音频可以设置采样率、声道数、帧大小、音频码率、是否使用外部采集、是否使用外部音频前处理;视频可以设置帧率、码率、分辨率、前后摄像头、摄像头采集方向、视频端显示比例、是否开启摄像头闪光灯、是否打开摄像头响应变焦、是否镜像前置摄像头预览、是否镜像前置摄像头编码、是否打开滤镜功能、滤镜类型、是否打开水印支持、是否打开QoS功能、是否输出RGB数据、是否使用外部视频采集。   音视频处理   前处理模块也是主观影响主播观看效果最主要的环节。目前iOS端比较知名的是GPUImage,提供了丰富的预处理效果,我们也在此基础上进行了封装开发。视频前处理包含滤镜、美颜、水印、涂鸦等功能,同时在人脸识别和特效方面接入了第三方厂商FaceU。SDK内置4款滤镜黑白、自然、粉嫩、怀旧;支持16:9裁剪;支持磨皮和美白(高斯模糊加边缘检测);支持静态水印,动态水印,涂鸦等功能。音频前处理则包括回声抑制、啸叫、增益控制等。音视频都支持外部前处理。 ![img](https://pic1.zhimg.com/80/v2-ce8c118370a7571e1a9c34576ef3a4dc_720w.webp)   音视频编码   编码最主要的两个难点是:   处理硬件兼容性问题   在高FPS、低bitrate和音质画质之间找个一个平衡点   由于iOS端硬件兼容性比较好,因此可以采用硬编。SDK目前支持软件编码openH264,硬件编码VideoToolbox。而音频支持软件编码FDK-AAC和硬件编码AudioToolbox。   视频编码的核心思想就是去除冗余信息:   空间冗余:图像相邻像素之间有较强的相关性。   时间冗余:视频序列的相邻图像之间内容相似。   编码冗余:不同像素值出现的概率不同。   视觉冗余:人的视觉系统对某些细节不敏感。   音视频发送   推流SDK使用的流媒体协议是RTMP(RealTime Messaging Protocol)。而音视频发送最困难的就是针对网络的带宽评估。由于从直播端到RTMP服务器的网络情况复杂,尤其是在3G和带宽较差的Wifi环境下,网络丢包、抖动和延迟经常发生,导致直播推流不畅。RTMP基于TCP进行传输,TCP自身实现了网络拥塞下的处理,内部的机制较为复杂,而且对开发者不可见,开发者无法根据TCP协议的信息判断当时的网络情况,导致发送码率大于实际网络带宽,造成比较严重的网络拥塞。因此我们自研开发了一款实时根据网络变化的QoS算法,用于实时调节码率、帧率、分辨率,同时将数据实时上报统计平台。   ●●●   模块设计&线程模型   模块设计   鉴于推流的主流程分为上述描述的4个部分:音视频采集、音视频前处理、音视频编码、音视频发送。因此将推流SDK进行模块划分为LSMediacapture层(对外API+服务器交互)、视频融合模块(视频采集+视频前处理)、音频融合模块(音频采集+音频前处理)、基础服务模块、音视频编码模块、网络发送模块。 ![img](https://pic3.zhimg.com/80/v2-b5df0ed24f36202083b6395ae42e133e_720w.webp)   线程模型   推流SDK总共含有10个线程。视频包含AVCaptureSession的原始采集线程、前处理线程、硬件编码线程、数据流向定义的采集线程、编码线程、发送线程。音频包含AudioUnit包含的原始采集线程、数据流向定义的采集线程、编码线程、发送线程。在数据流向定义的采集线程、编码线程、发送线程之间会创建2个bufferQueue,用于缓存音视频数据。采集编码队列可以有效的控制编码码率,编码发送队列可以有效自适应网络推流。 ![img](https://pic2.zhimg.com/80/v2-90d9e8bf70bac625ac813e450091b19d_720w.webp)   QoS&跳帧   下图是直播的主要流程,用户初始化SDK,创建线程,开始直播,音视频数据采集,编码,发送。在发送线程下,音视频数据发送,QoS开启,根据网络实时评估带宽,调整帧率,码率控制编码器参数,同时触发跳帧,调整分辨率控制采集分辨率参数。用户停止直播,反初始化SDK,销毁线程。QoS&跳帧可以有效的解决用户在网络不好的情况下,直播卡顿的问题。在不同的码率和分辨率情况下,都能够做到让用户流畅地观看视频直播。 ![img](https://pic4.zhimg.com/80/v2-2b8310a58acd9abddf1e9069744920e3_720w.webp) 原文https://zhuanlan.zhihu.com/p/31178008 ================================================ FILE: iOS资料/资深程序员的Metal入门教程总结.md ================================================ # 资深程序员的Metal入门教程总结 ## 1、Metal Metal 是一个和 OpenGL ES 类似的面向底层的图形编程接口,可以直接操作GPU;支持iOS和OS X,提供图形渲染和通用计算能力。(不支持模拟器) ![img](https://pic4.zhimg.com/80/v2-9a014b76a433aa8927e19f80b43c04d7_720w.webp) 图片来源 [https://www.invasivecode.com/weblog/metal-image-processing](https://link.zhihu.com/?target=https%3A//www.invasivecode.com/weblog/metal-image-processing) MTLDevice 对象代表GPU,通常使用MTLCreateSystemDefaultDevice获取默认的GPU; MTLCommandQueue由device创建,用于创建和组织MTLCommandBuffer,保证指令(MTLCommandBuffer)有序地发送到GPU;MTLCommandBuffer会提供一些encoder,包括编码绘制指令的MTLRenderCommandEncoder、编码计算指令的MTLComputeCommandEncoder、编码缓存纹理拷贝指令的MTLBlitCommandEncoder。对于一个commandBuffer,只有调用encoder的结束操作,才能进行下一个encoder的创建,同时可以设置执行完指令的回调。 每一帧都会产生一个MTLCommandBuffer对象,用于填放指令; GPUs的类型很多,每一种都有各自的接收和执行指令方式,在MTLCommandEncoder把指令进行封装后,MTLCommandBuffer再做聚合到一次提交里。 MTLRenderPassDescriptor 是一个轻量级的临时对象,里面存放较多属性配置,供MTLCommandBuffer创建MTLRenderCommandEncoder对象用。 ![img](https://pic2.zhimg.com/80/v2-ce5fa3183999f1d6a3e38d8d16e3bda5_720w.webp) MTLRenderPassDescriptor 用来更方便创建MTLRenderCommandEncoder,由MetalKit的view设置属性,并且在每帧刷新时都会提供新的MTLRenderPassDescriptor;MTLRenderCommandEncoder在创建的时候,会隐式的调用一次clear的命令。 最后再调用present和commit接口。 Metal的viewport是3D的区域,包括宽高和近/远平面。 深度缓冲最大值为1,最小值为0,如下面这两个都不会显示。 ```js // clipSpacePosition为深度缓冲 out.clipSpacePosition = vector_float4(0.0, 0.0, -0.1, 1.0); out.clipSpacePosition = vector_float4(0.0, 0.0, 1.1, 1.0); ``` ### 1.1渲染管道 Metal把输入、处理、输出的管道看成是对指定数据的渲染指令,比如输入顶点数据,输出渲染后纹理。 MTLRenderPipelineState 表示渲染管道,最主要的三个过程:顶点处理、光栅化、片元处理: ![img](https://pic2.zhimg.com/80/v2-98add3a1bd865a34c9ed742a3b79d03d_720w.webp) 转换几何形状数据为帧缓存中的颜色像素,叫做点阵化(rasterizing),也叫光栅化。其实就是根据顶点的数据,检测像素中心是否在三角形内,确定具体哪些像素需要渲染。 对开发者而言,顶点处理和片元处理是可编程的,光栅化是固定的(不可见)。 顶点函数在每个顶点被绘制时都会调用,比如说绘制一个三角形,会调用三次顶点函数。顶点处理函数返回的对象里,必须有带[[position]]描述符的属性,表面这个属性是用来计算下一步的光栅化;返回值没有描述符的部分,则会进行插值处理。 ![img](https://pic4.zhimg.com/80/v2-2e61593a88e21dddd28a7d31ed84bd57_720w.webp) 插值处理 像素处理是针对每一个要渲染的像素进行处理,返回值通常是4个浮点数,表示RGBA的颜色。 在编译的时候,Xcode会单独编译.metal的文件,但不会进行链接;需要在app运行时,手动进行链接。 在包里,可以看到default.metallib,这是对metal shader的编译结果。 ![img](https://pic1.zhimg.com/80/v2-4d0da4be6e468262f4371f4cdb10013c_720w.webp) MTLFunction可以用来创建MTLRenderPipelineState对象,MTLRenderPipelineState代表的是图形渲染的管道; 在调用device的newRenderPipelineStateWithDescriptor:error接口时,会进行顶点、像素函数的链接,形成一个图像处理管道; MTLRenderPipelineDescriptor包括名称、顶点处理函数、片元处理函数、输出颜色格式。 `setVertexBytes:length:atIndex:`这接口的长度限制是4k(4096bytes),对于超过的场景应该使用MTLBuffer。MTLBuffer是GPU能够直接读取的内存,用来存储大量的数据;(常用于顶点数据) `newBufferWithLength:options:`方法用来创建MTLBuffer,参数是大小和访问方式;MTLResourceStorageModeShared是默认的访问方式。 ### 1.2纹理 Metal要求所有的纹理都要符合MTLPixelFormat上面的某一种格式,每个格式都代表对图像数据的不同描述方式。 例如MTLPixelFormatBGRA8Unorm格式,内存布局如下: ![img](https://pic1.zhimg.com/80/v2-461ce8f529fb90b06f4c054e12026c84_720w.webp) 每个像素有32位,分别代表BRGA。 MTLTextureDescriptor 用来设置纹理属性,例如纹理大小和像素格式。 MTLBuffer用于存储顶点数据,MTLTexture则用于存储纹理数据;MTLTexture在创建之后,需要调用`replaceRegion:mipmapLevel:withBytes:bytesPerRow:`填充纹理数据;因为图像数据一般按行进行存储,所以需要每行的像素大小。 [[texture(index)]] 用来描述纹理参数,比如说 `samplingShader(RasterizerData in [[stage_in]], texture2d colorTexture [[ texture(AAPLTextureIndexBaseColor) ]])` 在读取纹理的时候,需要两个参数,一个是sampler和texture coordinate,前者是采样器,后者是纹理坐标。 读取纹理其实就把对应纹理坐标的像素颜色读取出来。 纹理坐标默认是(0,0)到(1,1),如下: ![img](https://pic4.zhimg.com/80/v2-9e77388f8055ee7fd7f6df5b0f49b847_720w.webp) 有时候,纹理的坐标会超过1,采样器会根据事前设置的mag_filter::参数进行计算。 ### 1.3通用计算 通用图形计算是general-purpose GPU,简称GPGPU。 GPU可以用于加密、机器学习、金融等,图形绘制和图形计算并不是互斥的,Metal可以同时使用计算管道进行图形计算,并且用渲染管道进行渲染。 计算管道只有一个步骤,就是kernel function(内核函数),内核函数直接读取并写入资源,不像渲染管道需要经过多个步骤; MTLComputePipelineState 代表一个计算处理管道,只需要一个内核函数就可以创建,相比之下,渲染管道需要顶点和片元两个处理函数; 每次内核函数执行,都会有一个唯一的gid值; 内核函数的执行次数需要事先指定,这个次数由格子大小决定。 threadgroup 指的是设定的处理单元,这个值要根据具体的设备进行区别,但必须是足够小的,能让GPU执行; threadgroupCount 是需要处理的次数,一般来说threadgroupCount*threadgroup=需要处理的大小。 ### 1.4性能相关 临时对象(创建和销毁是廉价的,它们的创建方法都返回 autoreleased对象) 1.Command Buffers 2.Command Encoders 代码中不需要持有。 高消耗对象(在性能相关的代码里应该尽量重用它,避免反复创建) 1.Command Queues 2.Buffers 3.Textures 5.Compute States 6.Render Pipeline States 代码中需长期持有。 Metal常用的四种数据类型:half、float、short(ushort)、int(uint)。 GPU的寄存器是16位,half是性能消耗最低的数据类型;float需要两次读取、消耗两倍的寄存器空间、两倍的带宽、两倍的电量。 为了提升性能,half和float之间的转换由硬件来完成,不占用任何开销。 同时,Metal自带的函数都是经过优化的。 在float和half数据类型混合的计算中,为了保持精度会自动将half转成float来处理,所以如果想用half节省开销的话,要避免和float混用。 Metal同样不擅长处理control flow,应该尽可能使用使用三元表达式,取代简单的if判断。 > 此部分参考自[WWDC](https://link.zhihu.com/?target=https%3A//developer.apple.com/videos/play/wwdc2016/606) ![img](https://pic4.zhimg.com/80/v2-4509c8066ecd8afe3bb432fc50dd1d33_720w.webp) 常见的图形渲染管道 ## 2、Metal Shader Language** Metal Shader Language的使用场景有两个,分别是图形渲染和通用计算;基于C++ 14,运行在GPU上,GPU的特点:带宽大,并行处理,内存小,对条件语句处理较慢(等待时间长)。 Metal着色语言使用clang和 LLVM,支持重载函数,但不支持图形渲染和通用计算入口函数的重载、递归函数调用、new和delete操作符、虚函数、异常处理、函数指针等,也不能用C++ 11的标准库。 ### 2.1基本函数 shader有三个基本函数: - 顶点函数(vertex),对每个顶点进行处理,生成数据并输出到绘制管线; - 像素函数(fragment),对光栅化后的每个像素点进行处理,生成数据并输出到绘制管线; - 通用计算函数(kernel),是并行计算的函数,其返回值类型必须为void; 顶点函数相关的修饰符: - [[vertex_id]] vertex_id是顶点shader每次处理的index,用于定位当前的顶点 - [[instance_id]] instance_id是单个实例多次渲染时,用于表明当前索引; - [[clip_distance]],float 或者 float[n], n必须是编译时常量; - [[point_size]],float; - [[position]],float4; 如果一个顶点函数的返回值不是void,那么返回值必须包含顶点位置; 如果返回值是float4,默认表示位置,可以不带[[ position ]]修饰符; 如果一个顶点函数的返回值是结构体,那么结构体必须包含“[[ position ]]”修饰的变量。 像素函数相关的修饰符: - [[color(m)]] float或half等,m必须是编译时常量,表示输入值从一个颜色attachment中读取,m用于指定从哪个颜色attachment中读取; - [[front_facing]] bool,如果像素所属片元是正面则为true; - [[point_coord]] float2,表示点图元的位置,取值范围是0.0到1.0; - [[position]] float4,表示像素对应的窗口相对坐标(x, y, z, 1/w); - [[sample_id]] uint,The sample number of the sample currently being processed. - [[sample_mask]] uint,The set of samples covered by the primitive generating the fragmentduring multisample rasterization. 以上都是输入相关的描述符。**像素函数的返回值是单个像素的输出,包括一个或是多个渲染结果颜色值,一个深度值,还有一个sample遮罩**,对应的输出描述符是[[color(m)]] floatn、[[depth(depth_qualifier)]] float、[[sample_mask]] uint。 ```js struct LYFragmentOutput { // color attachment 0 float4 color_float [[color(0)]];// color attachment 1 int4 color_int4 [[color(1)]];// color attachment 2 uint4 color_uint4 [[color(2)]];}; fragment LYFragmentOutput fragment_shader( ... ) { ... }; ``` 需要注意,颜色attachment的参数设置要和像素函数的输入和输出的数据类型匹配。 > Metal支持一个功能,叫做前置深度测试(early depth testing),允许在像素着色器运行之前运行深度测试。如果一个像素被覆盖,则会放弃渲染。使用方式是在fragment关键字前面加上[[early_fragment_tests]]: `[[early_fragment_tests]] fragment float4 samplingShader(..)` 使用前置深度测试的要求是不能在fragment shader对深度进行写操作。 深度测试还不熟悉的,可以看[LearnOpenGL关于深度测试的介绍](https://link.zhihu.com/?target=https%3A//learnopengl-cn.readthedocs.io/zh/latest/04%20Advanced%20OpenGL/01%20Depth%20testing/)。 ### 2.2参数的地址空间选择 Metal种的内存访问主要有两种方式:Device模式和Constant模式,由代码中显式指定。 Device模式是比较通用的访问模式,使用限制比较少,而Constant模式是为了多次读取而设计的快速访问只读模式,通过Constant内存模式访问的参数的数据的字节数量是固定的,特点总结为: - Device支持读写,并且没有size的限制; - Constant是只读,并且限定大小; 如何选择Device和Constant模式? 先看数据size是否会变化,再看访问的频率高低,只有那些固定size且经常访问的部分适合使用constant模式,其他的均用Device。 ```js // Metal关键函数用到的指针参数要用地址空间修饰符(device, threadgroup, or constant) 如下 vertex RasterizerData // 返回给片元着色器的结构体 vertexShader(uint vertexID [[ vertex_id ]], // vertex_id是顶点shader每次处理的index,用于定位当前的顶点 constant LYVertex *vertexArray [[ buffer(0) ]]); // buffer表明是缓存数据,0是索引 ``` ![img](https://pic3.zhimg.com/80/v2-33557bec4f4f7c92126eefe93bb2edea_720w.webp) 地址空间的修饰符共有四个,device、threadgroup、constant、thread。 顶点函数(vertex)、像素函数(fragment)、通用计算函数(kernel)的指针或引用参数,都必须带有地址空间修饰符号。 对于顶点函数(vertex)和像素函数(fragment),其指针或引用参数必须定义在device或是constant地址空间; 对于通用计算函数(kernel),其指针或引用参数必须定义在device或是threadgroup或是constant地址空间; `void tranforms(device int *source_data, threadgroup int *dest_data, constant float *param_data) {/*...*/};` 如上使用了三种地址空间修饰符,因为有threadgroup修饰符,tranforms函数只能被通用计算函数调用。 **constant地址空间**用于从设备内存池分配存储的缓存对象,是只读的。constant地址空间的指针或引用可以做函数的参数,向声明为常量的变量赋值会产生编译错误,声明常量但是没有赋予初始值也会产生编译错误。 **在shader中,函数之外的变量(相当于全局变量),其地址空间必须是constant。** **device地址空间**用于从设备内存池分配出来的缓存对象,可读也可写。一个缓存对象可以被声明成一个标量、向量或是用户自定义结构体的指针或是引用。缓存对象使用的内存实际大小,应该在CPU侧调用时就确定。 **纹理对象总是在device地址空间分配内存**,所以纹理类型可以省略修饰符。 **threadgroup地址空间**用于通用计算函数变量的内存分配,变量被一个线程组的所有的线程共享,threadgroup地址空间分配的变量不能用于图形绘制函数。 **thread地址空间**用于每个线程内部的内存分配,被thread修饰的变量在其他线程无法访问,在图形绘制或是通用计算函数内声明的变量是thread地址空间分配。 如下一段代码,包括device、threadgroup、thread的使用: ```js typedef struct { half3 kRec709Luma; // position的修饰符表示这个是顶点 } TransParam; kernel void sobelKernel(texture2d sourceTexture [[texture(LYFragmentTextureIndexTextureSource)]], texture2d destTexture [[texture(LYFragmentTextureIndexTextureDest)]], uint2 grid [[thread_position_in_grid]], device TransParam *param [[buffer(0)]], // param.kRec709Luma = half3(0.2126, 0.7152, 0.0722); // 把rgba转成亮度值 threadgroup float3 *localBuffer [[threadgroup(0)]]) // threadgroup地址空间,这里并没有使用到; { // 边界保护 if(grid.x <= destTexture.get_width() && grid.y <= destTexture.get_height()) { thread half4 color = sourceTexture.read(grid); // 初始颜色 thread half gray = dot(color.rgb, half3(param->kRec709Luma)); // 转换成亮度 destTexture.write(half4(gray, gray, gray, 1.0), grid); // 写回对应纹理 } } ``` ### 2.3数据结构 Metal中常用的数据结构有向量、矩阵、原子数据类型、缓存、纹理、采样器、数组、用户自定义结构体。 half 是16bit是浮点数 0.5h float 是32bit的浮点数 0.5f size_t 是64bit的无符号整数 通常用于sizeof的返回值 ptrdiff_t 是64bit的有符号整数 通常用于指针的差值 half2、half3、half4、float2、float3、float4等,是向量类型,表达方式为基础类型+向量维数。矩阵类似half4x4、half3x3、float4x4、float3x3。 double、long、long long不支持。 对于向量的访问,比如说`vec=float4(1.0f, 1.0f, 1.0f, 1.0f)`,其访问方式可以是vec[0]、vec[1],也可以是vec.x、vec.y,也可以是vec.r、vec.g。(.xyzw和.rgba,前者对应三维坐标,后者对应RGB颜色空间) 只取部分、乱序取均可,比如说我们常用到的`color=texture.bgra`。 > **数据对齐** char3、uchar3的size是4Bytes,而不是3Bytes; 类似的,int是4Bytes,但int3是16而不是12Bytes; 矩阵是由一组向量构成,按照向量的维度对齐;float3x3由3个float3向量构成,那么每个float3的size是16Bytes; **隐式类型转换**(Implicit Type Conversions) 向量到向量或是标量的隐式转换会导致编译错误,比如`int4 i; float4 f = i; // compile error`,无法将一个4维的整形向量转换为4维的浮点向量。 标量到向量的隐式转换,是标量被赋值给向量的每一个分量。 `float4 f = 2.0f; // f = (2.0f, 2.0f, 2.0f, 2.0f)` 标量到矩阵、向量到矩阵的隐式转换,矩阵到矩阵和向量及标量的隐式转换会导致编译错误。 纹理数据结构不支持指针和引用,纹理数据结构包括精度和access描述符,access修饰符描述纹理如何被访问,有三种描述符:sample、read、write,如下: ```js kernel void sobelKernel(texture2d sourceTexture [[texture(LYFragmentTextureIndexTextureSource)]], texture2d destTexture [[texture(LYFragmentTextureIndexTextureDest)]], uint2 grid [[thread_position_in_grid]]) ``` Sampler是采样器,决定如何对一个纹理进行采样操作。寻址模式,过滤模式,归一化坐标,比较函数。 在Metal程序里初始化的采样器必须使用constexpr修饰符声明。 采样器指针和引用是不支持的,将会导致编译错误。 ```js constexpr sampler textureSampler (mag_filter::linear, min_filter::linear); // sampler是采样器 ``` ### 2.4运算符 - 矩阵相乘有一个操作数是标量,那么这个标量和矩阵中的每一个元素相乘,得到一个和矩阵有相同行列的新矩阵。 - 右操作数是一个向量,那么它被看做一个列向量,如果左操作数是一个向量,那么他被看做一个行向量。这个也说明,为什么我们要固定用mvp乘以position(左乘矩阵),而不能position乘以mvp!因为两者的处理结果不一致。 ## 3、Metal和OpenGL ES的差异 OpenGL的历史已经超过25年。基于当时设计原则,OpenGL不支持多线程,异步操作,还有着臃肿的特性。为了更好利用GPU,苹果设计了Metal。 Metal的目标包括更高效的CPU&GPU交互,减少CPU负载,支持多线程执行,可预测的操作,资源控制和同异步控制;接口与OpenGL类似,但更加切合苹果设计的GPUs。 ![img](https://pic4.zhimg.com/80/v2-933eed5b13eb0cd06aca5e8b2ee70ef3_720w.webp) Metal的关系图 Metal的关系图如上,其中的Device是GPU设备的抽象,负责管道相关对象的创建: ![img](https://pic3.zhimg.com/80/v2-617e0a7b430e223ec0652dbdb15163c2_720w.webp) Device ### 3.1Metal和OpenGL ES的代码对比 我们先看一段OpenGL ES的渲染代码,我们可以抽象为Render Targets的设定,Shaders绑定,设置Vertex Buffers、Uniforms和Textures,最后调用Draws指令。 ```js glBindFramebuffer(GL_FRAMEBUFFER, myFramebuffer); glUseProgram(myProgram); glBindBuffer(GL_ARRAY_BUFFER, myVertexBuffer); glBindBuffer(GL_UNIFORM_BUFFER, myUniforms); glBindTexture(GL_TEXTURE_2D, myColorTexture); glDrawArrays(GL_TRIANGLES, 0, numVertices); ``` ![img](https://pic2.zhimg.com/80/v2-30ec2e2dfed0650ba83944aa92690c55_720w.webp) 再看Metal的渲染代码: Render Targets设定 是创建encoder; Shaders绑定 是设置pipelineState; 设置Vertex Buffers、Uniforms和Textures 是setVertexBuffer和setFragmentBuffer; 调用Draws指令 是drawPrimitives; 最后需要再调用一次endEncoding。 ```js encoder = [commandBuffer renderCommandEncoderWithDescriptor:descriptor]; [encoder setPipelineState:myPipeline]; [encoder setVertexBuffer:myVertexData offset:0 atIndex:0]; [encoder setVertexBuffer:myUniforms offset:0 atIndex:1]; [encoder setFragmentBuffer:myUniforms offset:0 atIndex:1]; [encoder setFragmentTexture:myColorTexture atIndex:0]; [encoder drawPrimitives:MTLPrimitiveTypeTriangle vertexStart:0 vertexCount:numVertices]; [encoder endEncoding]; ``` ![img](https://pic1.zhimg.com/80/v2-ea290cebb959100c05f5fd9f4e838868_720w.webp) ### 3.2Metal和OpenGL ES的同异步处理 如下图,是用OpenGL ES实现一段渲染的代码。CPU在Frame1的回调中写入数据到buffer,之后GPU会从buffer中读取Frame1写入的数据。 ![img](https://pic2.zhimg.com/80/v2-223b6c01c703b3a1f6064e4a85a139b1_720w.webp) 但在Frame2 CPU在往Buffer写入数据时,Buffer仍存储着Frame1的数据,且GPU还在使用该buffer,于是Frame2必须等待Frame1渲染完毕,造成阻塞。如下,会产生CPU的wait和GPU的idle。 ![img](https://pic3.zhimg.com/80/v2-6128ce57139275a2a707c8642b9cff52_720w.webp) Metal的处理方案会更加高效。如下图,Metal会申请三个buffer对应三个Frame,然后根据GPU的渲染回调,实时更新buffer的缓存。 在Frame2的时候,CPU会操作Buffer2,而GPU会读取Buffer1,并行操作以提高效率。 ![img](https://pic4.zhimg.com/80/v2-11fb731eb746694395297134ab66d4ab_720w.webp) ## 4.总结 [Metal系列入门教程](https://link.zhihu.com/?target=https%3A//github.com/loyinglin/LearnMetal)介绍了Metal的图片绘制、三维变换、视频渲染、天空盒、计算管道、Metal与OpenGL ES交互。结合本文的总结,能对Metal产生基本的认知,看懂大部分Metal渲染的代码。 接下来的学习方向是Metal进阶,包括Metal滤镜链的设计与实现、多重colorAttachments渲染、绿幕功能实现、更复杂的通用计算比如MPSImageHistogram,Shader的性能优化等。 原文https://zhuanlan.zhihu.com/p/48245068 ================================================ FILE: iOS资料/音视频学习--iOS适配H265实战踩坑记.md ================================================ # 音视频学习--iOS适配H265实战踩坑记 ## 1.背景介绍 熟悉webrtc都知道:谷歌的webrtc,默认不支持h265,毕竟涉及到很多专利的事宜,这中间的八卦就暂时不做探究。但是今天拿到一个IPC,该设备会发送H265数据,如下图所示,要做到兼容相关IPC,只能适配H265编解码了。所以最近熬秃了好几把头发就做了一下相关知识的学习,以下是自己的学习笔记,提供大家一个解决问题思路,由于iOS刚接触,同时H265调试经验也欠缺,很多都是一边查找资料,一边学习的,难免有处理不妥当之处,欢迎一起讨论学习,开发大神请绕行。 ![img](https://pic4.zhimg.com/80/v2-6746a34cecfce9a3d96574f10be3d8bf_720w.webp) ## 2.补充1H265编码特性 学习之前先了解一下H265的编码特性,有的放矢才能遇到问题及时排查。H265的经典编码框架如下图: ![img](https://pic1.zhimg.com/80/v2-b7a8b1fd214402208b2450bef92a596c_720w.webp) HEVC 的编码框架是在H26X的基础上逐步发展起来的,主要包括变换、量化、熵编码、帧内预测、帧间预测以及环路滤波等模块。相关内容推荐阅读万帅老师的书《新一代高效视频编码H.265/HEVC:原理、标准与实现》。 一般提到H265,都难免要和H264对比一番,以下是这次需求端要求适配H265的基本理论依据,如下表格: ![img](https://pic2.zhimg.com/80/v2-8b02b03fdfeb82c405611a201920006d_720w.webp) 理论上要直接适配一种编解码格式,肯定要学习相关理论,然后再做对应适配。然而时间紧张,任务繁杂,根本没有足够时间积累。想要有一个全面直观的认识,同时为了验证IPC是否正常,所以首先用VLC进行播放尝试:PC端确认该IPC可以通过 VLC进行监控,这时我们可以通过wireshark抓包,拿到第一手数据进行分析。通过抓包可以看出:1920*1080的视频流除了IDR数据帧大小略大,其他数据都是一个NALU单元就完成封装了,文件大小确实小很多。 接下来我们依次认识H265的封包和关键信息。 ## 3.H265关键信息 通过相关抓包,可以看到整理结构如下图,包含了VPS,SPS,PPS, FU分片包,Trail_R的包等。 ![img](https://pic4.zhimg.com/80/v2-819861cd09b0d2724727ae8e6d639183_720w.webp) ![img](https://pic4.zhimg.com/80/v2-9e41ceeb0fb2935279250b2ecce4164b_720w.webp) ### 3.1VPS结构 VPS(Video Parameter Set, 视频参数集)依据ITUHEVC的标准文档,VPS的参数结构以及每一个条款的解释,很多大佬已经有写明了,HEVC来说自己还是小萌新在此不再累述,可以参考自己关注的一个大佬的博文: ![img](https://pic1.zhimg.com/v2-95203dc981a68b102381f5bf23acf8b0_120x160.jpg)A//blog.csdn.net/Dillon2015/article/details/104142144) 其抓包中VPS数据参数如下: ![img](https://pic3.zhimg.com/80/v2-2579ff7e1c9b9a27b47f1a2f434c311a_720w.webp) ### 3.2SPS结构 SPS的内容大致包括解码相关信息,如档次级别、分辨率、某档次中编码工具开关标识和涉及的参数、时域可分级信息等。 其抓包中SPS数据参数如下: ![img](https://pic4.zhimg.com/80/v2-3ea2e44759f1d48dc2819df3c3664a23_720w.webp) ### 3.3PPS结构 HEVC的图像参数集PPS包含每一帧可能不同的设置信息;其内容大致包括初始图像控制信息,如量化参数(QP,Quantization Parament)、分块信息等。即PPS包含了每一帧图像所用的公共参数,即一帧图像中的所有SS会引用同一个PPS;每张图像的PPS可能不同。详细介绍说明可以参考: 其抓包中PPS数据参数如下: ![img](https://pic3.zhimg.com/80/v2-dbff33f0871d7ae00e68bfb38f06a866_720w.webp) 有了这些数据,至少可以解析出来该IPC支持的分辨率,profile,level等基本数据信息,为初始化做准备。 ## 4.H265解封包 由上面信息可以知道H265的封包格式和H264基本上保持一致,也是通过NALU单元进行分装,不过不太一样的地方是 H265的NALU Header长度是2个字节,而 H264的NALU Header的长度是1个字节,所以解析时候需要进行移位操作,否则读取数据异常,导致包类型无法辨别(在这里踩了一个小坑,印象深刻)。 H26封包中NALU type主要类型如下图所示。由于不同厂家支持程度不一,本次适配过程中主要关注的几个类型包括:kTrailR(1),kIdrwRadl(1),kVps(1),kSps(1),kPps(1),kFU(1)等。 ![img](https://pic2.zhimg.com/80/v2-d0c4f9c852cffdf051eb7b3b6763d7a5_720w.webp) ### 4.1FUNALU 当 NALU 的长度超过 MTU 时, 用于把当前NALU单元封装成多个 RTP 包,HEVC的FU单元的type值为49,具体组织结构如下: ![img](https://pic2.zhimg.com/80/v2-10c6bb4748daf92590d10d63bf6d36f9_720w.webp) FU Nalu相关解析代码如下: ![img](https://pic4.zhimg.com/80/v2-9105f7823e83bae92b455a1165fa1137_720w.webp) ### 4.2SingleNALU 单个 NAL 单元数据包只包含一个 NAL 单元,由一个有效载荷头(表示为 PayloadHdr)、一个有条件的16位DONL字段和NAL 单元有效载荷数据。 ![img](https://pic3.zhimg.com/80/v2-ddb09b350ee2182e7ca61897a272fc9e_720w.webp) ### 4.3AP NALU HEVC封包另外一个种封包格式:聚合模式(Aggregation Packets,APs),主要为了减少小型 NAL 单元的打包开销,例如大多数非 VCL NAL 单元,它们的大小通常只有几个字节。AP 将 NAL 单元聚合在一个访问单元内。AP 中要携带的每个 NAL 单元都封装在聚合单元中。聚合在一个 AP 中的 NAL 单元按 NAL 单元解码顺序排列。 ![img](https://pic1.zhimg.com/80/v2-874b4764c6017d093175d6bbf0e90930_720w.webp) AP Nalu和Single Nalu相关代码解析如下: ![img](https://pic3.zhimg.com/80/v2-742eb210ca022ef787efb1792e33080e_720w.webp) 当接收到每一个HEVC包之后,一次送到Jitterbuffer中,完成数据帧的重新组装和排序,必要时候进行数据矫正和重传操作,这是另外的技术,此处暂不做讨论。 ## 5.H265 VideoToolBox解码 收到完整数据帧时候,VCM会依据时间戳一次获取相关数据,送解码器,iOS平台就是VideoToolBox解码了。这部分自己是小白,简单说明关键函数,大佬勿喷。 ### 5.1ResetDecompressionSession ResetDecompressionSession完成解码参数的构建,以及Session的创建。主要注意iOS支持的色彩是有差别的,设置时候需要明确是否支持,比如这次就设置了kCVPixelFormatType_420YpCbCr8BiPlanarFullRange类型。 同时在reset函数中注册callback函数,用于接收解码完成后的视频帧。 ![img](https://pic2.zhimg.com/80/v2-dad83cacfc891c741e4427ee9163aed5_720w.webp) ### 5.2decode iOS VideoToolBox相关开发说明,可以参考 ![img](https://pic3.zhimg.com/80/v2-45f14ce7aa9e11dbae029395116afc0e_720w.webp) ![img](https://pic1.zhimg.com/80/v2-323670c3f1501798744085bebe4bb3bc_720w.webp) 获取VPS,SPS,PPS相关数据,构建CMVideoFormatDescription相关数据; ![img](https://pic2.zhimg.com/80/v2-b33f0650bca613e3740704a8c5d8c709_720w.webp) ### 5.3callback callback函数用于接收H265解码数据,并用于送到显示端进行渲染的。 ![img](https://pic3.zhimg.com/80/v2-76d4b4d9b7a3b08978a818fb64ce5de6_720w.webp) ## 6.补充2 Annexb和AVCC格式转换 Android的硬解码接口MediaCodec只能接收Annex-B格式的H264数据,而iOS平台的VideoToolBox则相反,只支持AVCC格式。所以要进行一次转换,相关转换规则有大佬做了说明,可以参考 ![img](https://pic1.zhimg.com/80/v2-26c14b9967d5fc492aa6584a9101fc7c_720w.webp) ## 7.补充3iOS编解码错误说明 在调试iOS编解码过程碰到几个CallBack的错误,于是找了一下相关错误代码,常见的错误如下所示: ![img](https://pic3.zhimg.com/80/v2-dcb0bbeba51baac22487d5e1257e8dfe_720w.webp) 其中一个错误kVTVideoDecoderBadDataErr = -12909,该错误找了很多久,也查了很多资料,一直卡住2天,每天早出晚归,熬最深的夜,加最晚的班,我可怜的头发又少了好几根。RIP. 期间尝试各种办法验证确认: (1)将所有数据包和数据类型打印; (2)送解码前数据打印和保存; (3)反复确认VPS,SPS,PPS数据内容; (4)确认调用流程; (5)查找githubdemo。 最后确认经过多次反复确认代码,验证裸流,比较大小之后,最后发现计算长度时候H265 NAL头计算错误,导致IDR帧无法正确解码,最后找到问题,一行代码解决问题。其实该问题在RFC7798中有说明,只是自己还是按照H264惯性思维处理,这也是基础知识不扎实的根本原因(不过回想一下好像本来也没有这块知识,惭愧,惭愧)。 ![img](https://pic2.zhimg.com/80/v2-dfa623fced640a237a9e8869a64517a5_720w.webp) 题外话:排查问题过程很艰辛,最后一行代码处理完成,这是目前工作中很常见的,所以也是自己给新人,或者要入门音视频强调的一点:保持足够投入度,最好是兴趣驱动。《格局》一书中说:主动做事的收益,或许不会在一两天内显现出来但是长期坚持下来,主动做事的人,就能和其他人拉开距离。 扯远了,最后分享一下排查问题期间也查阅其他人调试过程发现的问题点,在此一起收集一下,方便后来者遇到问题可以快速查阅: (1)比如,省流模式下解码失败 该文章提到一种思路:可以借鉴比较成熟的ijkplayer,对比流程和处理细节,查找得到解决办法。 (2)比如,解码器Session失效问题 该文章中提到,如果VideoToolBox返回码是 kVTInvalidSessionErr =-12903,也就是说解码器Session异常或者失效了。可以在收到该返回码时调用ResetDecompressionSession操作完成重置,再进行切换时就会正常了。该部分优化已经同步了,防止切换异常;手动狗头,感谢大佬。 (3)比如,annexB与hvcc转换异常问题 该Issue中提到,ios硬解h265 NALU失败,看了代码发现在vtbformat_init中对265从annexB=>mp4转换时需要使用ff_isom_write_hvcc,而不能重用264的ff_isom_write_avcc。两者区别还是比较多的,有兴趣的翻阅一下FFMPEG代码自行补充。 原文https://zhuanlan.zhihu.com/p/589832516 ================================================ FILE: paper/README.md ================================================ ================================================ FILE: teaching video/video.md ================================================ ================================================ FILE: 使用FFMpeg进行H264编码.c ================================================ /** 使用FFMpeg可以很方便的对音视频进行编码,并且写文件。 下面的代码是将5幅1280*720大小的图片进行编码,并且写到文件中。 代码有些乱,但希望能抛砖引玉,对学习这方面的朋友有帮助。 */ CFile file[5]; BYTE *szTxt[5]; int nWidth = 0; int nHeight= 0; int nDataLen=0; int nLen; CString csFileName; for (int fileI = 1; fileI <= 5; fileI ++) { csFileName.Format("e:\\pics\\%d.bmp", fileI); file[fileI - 1].Open(csFileName,CFile::modeRead | CFile::typeBinary); nLen = file[fileI - 1].GetLength(); szTxt[fileI -1] = new BYTE[nLen]; file[fileI - 1].Read(szTxt[fileI - 1], nLen); file[fileI - 1].Close(); //BMP bmi;//BITMAPINFO bmi; //int nHeadLen = sizeof(BMP); BITMAPFILEHEADER bmpFHeader; BITMAPINFOHEADER bmpIHeader; memcpy(&bmpFHeader,szTxt[fileI -1],sizeof(BITMAPFILEHEADER)); int nHeadLen = bmpFHeader.bfOffBits - sizeof(BITMAPFILEHEADER); memcpy(&bmpIHeader,szTxt[fileI - 1]+sizeof(BITMAPFILEHEADER),nHeadLen); nWidth = bmpIHeader.biWidth;// 464;// bmi.bmpInfo.bmiHeader.biWidth;// ; nHeight = bmpIHeader.biHeight;//362;// bmi.bmpInfo.bmiHeader.biHeight;// ; szTxt[fileI - 1] += bmpFHeader.bfOffBits; nDataLen = nLen-bmpFHeader.bfOffBits; } av_register_all(); avcodec_register_all(); AVFrame *m_pRGBFrame = new AVFrame[1]; //RGB帧数据 AVFrame *m_pYUVFrame = new AVFrame[1];; //YUV帧数据 AVCodecContext *c= NULL; AVCodecContext *in_c= NULL; AVCodec *pCodecH264; //编码器 uint8_t * yuv_buff;// //查找h264编码器 pCodecH264 = avcodec_find_encoder(CODEC_ID_H264); if(!pCodecH264) { fprintf(stderr, "h264 codec not found\n"); exit(1); } c= avcodec_alloc_context3(pCodecH264); c->bit_rate = 3000000;// put sample parameters c->width =nWidth;// c->height = nHeight;// // frames per second AVRational rate; rate.num = 1; rate.den = 25; c->time_base= rate;//(AVRational){1,25}; c->gop_size = 10; // emit one intra frame every ten frames c->max_b_frames=1; c->thread_count = 1; c->pix_fmt = PIX_FMT_YUV420P;//PIX_FMT_RGB24; //av_opt_set(c->priv_data, /*"preset"*/"libvpx-1080p.ffpreset", /*"slow"*/NULL, 0); //打开编码器 if(avcodec_open2(c,pCodecH264,NULL)<0) TRACE("不能打开编码库"); int size = c->width * c->height; yuv_buff = (uint8_t *) malloc((size * 3) / 2); // size for YUV 420 //将rgb图像数据填充rgb帧 uint8_t * rgb_buff = new uint8_t[nDataLen]; //图象编码 int outbuf_size=100000; uint8_t * outbuf= (uint8_t*)malloc(outbuf_size); int u_size = 0; FILE *f=NULL; char * filename = "e:\\pics\\myData.h264"; f = fopen(filename, "wb"); if (!f) { TRACE( "could not open %s\n", filename); exit(1); } //初始化SwsContext SwsContext * scxt = sws_getContext(c->width,c->height,PIX_FMT_BGR24,c->width,c->height,PIX_FMT_YUV420P,SWS_POINT,NULL,NULL,NULL); AVPacket avpkt; //AVFrame *pTFrame=new AVFrame for (int i=0;i<250;++i) { //AVFrame *m_pYUVFrame = new AVFrame[1]; int index = (i / 25) % 5; memcpy(rgb_buff,szTxt[index],nDataLen); avpicture_fill((AVPicture*)m_pRGBFrame, (uint8_t*)rgb_buff, PIX_FMT_RGB24, nWidth, nHeight); //将YUV buffer 填充YUV Frame avpicture_fill((AVPicture*)m_pYUVFrame, (uint8_t*)yuv_buff, PIX_FMT_YUV420P, nWidth, nHeight); // 翻转RGB图像 m_pRGBFrame->data[0] += m_pRGBFrame->linesize[0] * (nHeight - 1); m_pRGBFrame->linesize[0] *= -1; m_pRGBFrame->data[1] += m_pRGBFrame->linesize[1] * (nHeight / 2 - 1); m_pRGBFrame->linesize[1] *= -1; m_pRGBFrame->data[2] += m_pRGBFrame->linesize[2] * (nHeight / 2 - 1); m_pRGBFrame->linesize[2] *= -1; //将RGB转化为YUV sws_scale(scxt,m_pRGBFrame->data,m_pRGBFrame->linesize,0,c->height,m_pYUVFrame->data,m_pYUVFrame->linesize); int got_packet_ptr = 0; av_init_packet(&avpkt); avpkt.data = outbuf; avpkt.size = outbuf_size; u_size = avcodec_encode_video2(c, &avpkt, m_pYUVFrame, &got_packet_ptr); if (u_size == 0) { fwrite(avpkt.data, 1, avpkt.size, f); } } fclose(f); delete []m_pRGBFrame; delete []m_pYUVFrame; delete []rgb_buff; free(outbuf); avcodec_close(c); av_free(c);