简历当中的知识点

文章目录

    • 1、opencv移植ARM的过程
    • 2、H265
      • 2.1 H265的编解码流程主要功能:
      • 2.2 FFMPEG组成
        • 2.2.1 FFMpeg进行视频编码所需要的结构:
      • FFMPEG结构体
      • 2.3 FFmpeg编码的流程图
      • sdl
    • RTP原理
      • RTP Header解析
      • RTP荷载H264码流
      • RTP定义
      • RTP是传输层的子层

1、opencv移植ARM的过程

首先准备cmake,libv4l-0.6.3.tar.gz

sudo vi CMakeCache.txt
找到:CMAKE_EXE_LINKER_FLAGS:STRING=
修改为:CMAKE_EXE_LINKER_FLAGS:STRING=-lpthread -ldl -lrt
意思是:-lpthread支持线程,-ldl避免未定义dlopen,-lrt避免未定义

这是因为在新的版本里,已经不用videodev.h这个文件了
sudo ln -s /usr/include/libv4l1-videodev.h /usr/include/linux/videodev.h 

CMakeCache.txt,该文件是上次cmake时候留下的缓存文件,如果在编译过程中报错,可以将该文件删除,然后再执行cmake

摄像头调用的两种方法总结:
  (1) 先实例化再初始化
     VideoCapture capture;
      capture.open(0);
  (2)在实例化的同时初始化:
     VideoCapture capture(0);
 //=========================================
#include 
using namespace cv;

int main()
{
	//从摄像头读取视频
	VideoCapture capture(0);
	//循环显示每一帧
	while (1) 
	{
		Mat  frame;//定义一个Mat变量,用于存储每一帧的图像
		capture >> frame;//读取当前帧
		imshow("读取视频帧",frame);//显示当前帧
		waitKey(30);//延时30ms
	}

	system("pause");
	return 0;
}

Mat类,是一个类,有很多种构造函数。
at函数具体用法
Mat.at<存储类型名称>(行,列)[通道]

2、H265

H265编码器仍旧采用变换和预测的混合编码方法。输入帧以宏块为单位被编码器处理,首先按照帧内或帧间预测编码的方法进行处理;接着,预测值与当前块相减,相减后得到的残差块经变换、量化后产生一组量化后的变换系数;最后,这组量化后的变换系数经过熵编码,与解码所需的一些头信息(如预测模式量化参数、运动矢量等)一起组成一个压缩后的码流,经NAL(网络自适应层)供传输和存储用。为了提供进一步预测用的参考图像,编码器必须有重建的功能。为了去除编解码环路中产生的噪声,提高参考帧的图像质量,从而提高图像压缩性能,设置了一个环路滤波器,滤波后的输出即是重建图像,可用作参考图像。

简历当中的知识点_第1张图片

2.1 H265的编解码流程主要功能:

帧间和帧内预测(Estimation):图像经过帧内预测和帧间预测后,与原始视频帧进行相减形成预测残差。
变换(Transform)和反变换:将图像的时域信号变换为频域的信号,在频域中信号的能量集中在低频区域,并使其码率相对于空间信号有大幅下降。
量化(Quantization)和反量化:不降低视觉效果的前提下,保留图像的细节,确定量化参数(QP),减少图像的编码长度。
环路滤波(Loop Filter):对块边界处的像素进行滤波以平滑像素值的突变,消除视频图像中的块效应,同时可以达到降低噪音的效果。
熵编码(Entropy Coding):利用信息的统计冗余进行数据压缩的无损编码方法.

2.2 FFMPEG组成

构成FFmpeg主要有三个部分,第一部分是四个作用不同的工具软件,分别是:ffmpeg.exe,ffplay.exe,ffserver.exe和ffprobe.exe。

ffmpeg.exe:音视频转码、转换器
ffplay.exe:简单的音视频播放器
ffserver.exe:流媒体服务器
ffprobe.exe:简单的多媒体码流分析器
第二部分是可以供开发者使用的SDK,为各个不同平台编译完成的库。如果说上面的四个工具软件都是完整成品形式的玩具,那么这些库就相当于乐高积木一样,我们可以根据自己的需求使用这些库开发自己的应用程序。这些库有:

  • libavcodec:包含音视频编码器和解码器
  • libavutil:包含多媒体应用常用的简化编程的工具,如随机数生成器、数据结构、数学函数等功能
  • libavformat:包含多种多媒体容器格式的封装、解封装工具
  • libavfilter:包含多媒体处理常用的滤镜功能
  • libavdevice:用于音视频数据采集和渲染等功能的设备相关
  • libswscale:用于图像缩放和色彩空间和像素格式转换功能
  • libswresample:用于音频重采样和格式转换等功能
    第三部分是整个工程的源代码,无论是编译出来的可执行程序还是SDK,都是由这些源代码编译出来的。FFmpeg的源代码由C语言实现,主要在Linux平台上进行开发。FFmpeg不是一个孤立的工程,它还存在多个依赖的第三方工程来增强它自身的功能。在当前这一系列的博文/视频中,我们暂时不会涉及太多源代码相关的内容,主要以FFmpeg的工具和SDK的调用为主。到下一系列我们将专门研究如何编译源代码并根据源代码来进行二次开发

2.2.1 FFMpeg进行视频编码所需要的结构:

为了实现调用FFMpeg的API实现视频的编码,以下结构是必不可少的:

  • AVCodec:AVCodec结构保存了一个编解码器的实例,实现实际的编码功能。通常我们在程序中定义一个指向AVCodec结构的指针指向该实例。

  • AVCodecContext:AVCodecContext表示AVCodec所代表的上下文信息,保存了AVCodec所需要的一些参数。对于实现编码功能,我们可以在这个结构中设置我们指定的编码参数。通常也是定义一个指针指向AVCodecContext。

  • AVFrame:AVFrame结构保存编码之前的像素数据,并作为编码器的输入数据。其在程序中也是一个指针的形式。

  • AVPacket:AVPacket表示码流包结构,包含编码之后的码流数据。该结构可以不定义指针,以一个对象的形式定义。

  • FFMpeg编码的主要步骤:

(1)、输入编码参数
这一步我们可以设置一个专门的配置文件,并将参数按照某个事写入这个配置文件中,再在程序中解析这个配置文件获得编码的参数。
如果参数不多的话,我们可以直接使用命令行将编码参数传入即可。
(2)、按照要求初始化需要的FFMpeg结构
首先,所有涉及到编解码的的功能,都必须要注册音视频编解码器之后才能使用。注册编解码调用下面的函数
avcodec_register_all();
编解码器注册完成之后,根据指定的CODEC_ID查找指定的codec实例。CODEC_ID通常指定了编解码器的格式,
在这里我们使用当前应用最为广泛的H.264格式为例。查找codec调用的函数为avcodec_find_encoder,其声明格式为:
AVCodec *avcodec_find_encoder(enum AVCodecID id);
该函数的输入参数为一个AVCodecID的枚举类型,返回值为一个指向AVCodec结构的指针,
用于接收找到的编解码器实例。如果没有找到,那么该函数会返回一个空指针。调用方法如下:
/* find the mpeg1 video encoder */
ctx.codec = avcodec_find_encoder(AV_CODEC_ID_H264); //根据CODEC_ID查找编解码器对象实例的指针
if (!ctx.codec) 
{
    fprintf(stderr, "Codec not found\n");
    return false;
}
AVCodec查找成功后,下一步是分配AVCodecContext实例。分配AVCodecContext实例需要我们前面查找到的AVCodec作为参数,
调用的是avcodec_alloc_context3函数。其声明方式为:
AVCodecContext *avcodec_alloc_context3(const AVCodec *codec);
其特点同avcodec_find_encoder类似,返回一个指向AVCodecContext实例的指针。
如果分配失败,会返回一个空指针。调用方式为
ctx.c = avcodec_alloc_context3(ctx.codec);          //分配AVCodecContext实例
if (!ctx.c)
{
    fprintf(stderr, "Could not allocate video codec context\n");
    return false;
}
需注意,在分配成功之后,应将编码的参数设置赋值给AVCodecContext的成员。

现在,AVCodec、AVCodecContext的指针都已经分配好,然后以这两个对象的指针作为参数打开编码器对象。
调用的函数为avcodec_open2,声明方式为:
int avcodec_open2(AVCodecContext *avctx, const AVCodec *codec, AVDictionary **options);
该函数的前两个参数是我们刚刚建立的两个对象,第三个参数为一个字典类型对象,
用于保存函数执行过程总未能识别的AVCodecContext和另外一些私有设置选项。
函数的返回值表示编码器是否打开成功,若成功返回0,失败返回一个负数。调用方式为
if (avcodec_open2(ctx.c, ctx.codec, NULL) < 0)      //根据编码器上下文打开编码器
{
    fprintf(stderr, "Could not open codec\n");
    exit(1);
}

然后,我们需要处理AVFrame对象。AVFrame表示视频原始像素数据的一个容器,处理该类型数据需要两个步骤,
其一是分配AVFrame对象,其二是分配实际的像素数据的存储空间。分配对象空间类似于new操作符一样,
只是需要调用函数av_frame_alloc。如果失败,那么函数返回一个空指针。AVFrame对象分配成功后,
需要设置图像的分辨率和像素格式等。实际调用过程如下:

ctx.frame = av_frame_alloc();                       //分配AVFrame对象
if (!ctx.frame) 
{
    fprintf(stderr, "Could not allocate video frame\n");
    return false;
}
ctx.frame->format = ctx.c->pix_fmt;
ctx.frame->width = ctx.c->width;
ctx.frame->height = ctx.c->height;

分配像素的存储空间需要调用av_image_alloc函数,其声明方式为:
int av_image_alloc(uint8_t *pointers[4], int linesizes[4], int w, int h, enum AVPixelFormat pix_fmt, int align);
该函数的四个参数分别表示AVFrame结构中的缓存指针、各个颜色分量的宽度、图像分辨率(宽、高)、像素格式和内存对其的大小。
该函数会返回分配的内存的大小,如果失败则返回一个负值。具体调用方式如:
ret = av_image_alloc(ctx.frame->data, ctx.frame->linesize, ctx.c->width, ctx.c->height, ctx.c->pix_fmt, 32);
if (ret < 0) 
{
    fprintf(stderr, "Could not allocate raw picture buffer\n");
    return false;
}


FFMPEG结构体

  • AVFrame结构体
    AVFrame结构体一般用于存储原始数据(即非压缩数据,例如对视频来说是YUV,RGB,对音频来说是PCM),此外还包含了一些相关的信息。比如说,解码的时候存储了宏块类型表,QP表,运动矢量表等数据。编码的时候也存储了相关的数据。因此在使用FFMPEG进行码流分析的时候,AVFrame是一个很重要的结构体。

data[]
对于packed格式的数据(例如RGB24),会存到data[0]里面。
对于planar格式的数据(例如YUV420P),则会分开成data[0],data[1],data[2]…(YUV420P中data[0]存Y,data[1]存U,data[2]存V)

  • AVPacket
    AVPacket是存储压缩编码数据相关信息的结构体
    针对data做一下说明:对于H.264格式来说,在使用FFMPEG进行视音频处理的时候,我们常常可以将得到的AVPacket的data数据直接写成文件,从而得到视音频的码流文件。
uint8_t *data:压缩编码的数据。

int   size:data的大小

int64_t pts:显示时间戳

int64_t dts:解码时间戳

int   stream_index:标识该AVPacket所属的视频/音频流。
  • AVCodec是存储编解码器信息的结构体
    AVCodec是存储编解码器信息的结构体
    下面简单介绍一下遍历ffmpeg中的解码器信息的方法(这些解码器以一个链表的形式存储):

1.注册所有编解码器:av_register_all();

2.声明一个AVCodec类型的指针,比如说AVCodec* first_c;

3.调用av_codec_next()函数,即可获得指向链表下一个解码器的指针,循环往复可以获得所有解码器的信息。注意,如果想要获得指向第一个解码器的指针,则需要将该函数的参数设置为NULL。

2.3 FFmpeg编码的流程图

如下图是基于FFMPEG的H265视频编码器流程图,该编码器实现了YUV420P的像素数据编码为H265(H264,MPEG2,VP8)的压缩编码数据。
首先用函数avcodec_find_encoder()查找编码器;然后用函数avcodec_alloc_context()申请CODEC,函数avcodec_alloc_frame()申请编码器中的图像帧空间;设置编码器参数,包括宽度、高度等;avcodec_open()打开编码器CODEC;获取图像数据;编码当前图像avcodec_encode_video();写入码流文件;编码完毕后,销毁各种资源,关闭编码器avcodec_close()等。

简历当中的知识点_第2张图片
(1)av_register_all():注册FFmpeg 的H265编码器。调用了avcodec_register_all(),avcodec_register_all()注册了H265编码器有关的组件:硬件加速器,编码器,Parser,Bitstream Filter等;
(2)avformat_alloc_output_context2():初始化输出码流的AVFormatContext,获取输出文件的编码格式;
(3)avio_open():打开输出文件,调用了2个函数:ffurl_open()和ffio_fdopen()。其中ffurl_open()用于初始化URLContext,ffio_fdopen()用于根据URLContext初始化AVIOContext。URLContext中包含的URLProtocol完成了具体的协议读写等工作。AVIOContext则是在URLContext的读写函数外面加上了一层“包装”;
(4)av_new_stream():创建输出码流的AVStream结构体,为输出文件设置编码所需要的参数和格式;
(5)avcodec_find_encoder():通过 codec_id查找H265编码器

HEVC解码器对应的AVCodec结构体ff_hevc_decoder:
AVCodec ff_hevc_decoder = {
    .name                  = "hevc",
    .long_name             = NULL_IF_CONFIG_SMALL("HEVC (High Efficiency Video Coding)"),
    .type                  = AVMEDIA_TYPE_VIDEO,
    .id                    = AV_CODEC_ID_HEVC,
    .priv_data_size        = sizeof(HEVCContext),
    .priv_class            = &hevc_decoder_class,
    .init                  = hevc_decode_init,
    .close                 = hevc_decode_free,
    .decode                = hevc_decode_frame,
    .flush                 = hevc_decode_flush,
    .update_thread_context = hevc_update_thread_context,
    .init_thread_copy      = hevc_init_thread_copy,
    .capabilities          = AV_CODEC_CAP_DR1 | AV_CODEC_CAP_DELAY |
                             AV_CODEC_CAP_SLICE_THREADS | AV_CODEC_CAP_FRAME_THREADS,
    .caps_internal         = FF_CODEC_CAP_INIT_THREADSAFE | FF_CODEC_CAP_EXPORTS_CROPPING,
    .profiles              = NULL_IF_CONFIG_SMALL(ff_hevc_profiles),
    .hw_configs            = (const AVCodecHWConfigInternal*[]) {
#if CONFIG_HEVC_DXVA2_HWACCEL
                               HWACCEL_DXVA2(hevc),
#endif
#if CONFIG_HEVC_D3D11VA_HWACCEL
                               HWACCEL_D3D11VA(hevc),
#endif
#if CONFIG_HEVC_D3D11VA2_HWACCEL
                               HWACCEL_D3D11VA2(hevc),
#endif
#if CONFIG_HEVC_NVDEC_HWACCEL
                               HWACCEL_NVDEC(hevc),
#endif
#if CONFIG_HEVC_VAAPI_HWACCEL
                               HWACCEL_VAAPI(hevc),
#endif
#if CONFIG_HEVC_VDPAU_HWACCEL
                               HWACCEL_VDPAU(hevc),
#endif
#if CONFIG_HEVC_VIDEOTOOLBOX_HWACCEL
                               HWACCEL_VIDEOTOOLBOX(hevc),
#endif
                               NULL

(6)avcodec_open2():打开编码器。调用AVCodec的libx265_encode_init()初始化H265解码器 avcodec_open2()函数;
avcodec_open2() -> libx265_encode_init() -> x265_param_alloc(), x265_param_default_preset(), x265_encoder_open()
(7)avformat_write_header():写入编码的H265码流的文件头;
(8)avcodec_encode_video2():编码一帧视频。将AVFrame(存储YUV像素数据)编码为AVPacket(存储H265格式的码流数据)。调用H265编码器的libx265_encode_frame()函数;
avcodec_encode_video2() -> libx265_encode_frame() -> x265_encoder_encode()
(9)av_write_frame():将编码后的视频码流写入文件中;
(10)flush_encoder():输入的像素数据读取完成后调用此函数,用于输出编码器中剩余的AVPacket;
(11)av_write_trailer():写入编码的H265码流的文件尾;
(12)close():释放 AVFrame和图片buf,关闭H265编码器,调用AVCodec的libx265_encode_close()函数
avcodec_close() -> libx265_encode_close() -> x265_param_free(), x265_encoder_close()

  • 通过elecard hevc可以查看帧内预测和运动补偿相关信息。
/**
 * 基于FFmpeg的视频编码器
 * 功能:实现了YUV420像素数据编码为视频码流(H264,H265,MPEG2,VP8)。
 * ffmpeg编码yuv文件的命令:
 * H264:ffmpeg -s cif -i foreman_cif.yuv -vcodec libx264 -level 40 -profile baseline -me_method epzs -qp 23 -i_qfactor 1.0  -g 12 -refs 1 -frames 50 -r 25 output.264 
 * H265:ffmpeg -s cif -foreman_cif.yuv -vcodec libx265  -frames 100  output.265
 */
 
#include 
 
#define __STDC_CONSTANT_MACROS
 
#ifdef _WIN32
//Windows
extern "C"
{
#include "libavutil/opt.h"
#include "libavcodec/avcodec.h"
#include "libavformat/avformat.h"
};
#else
//Linux...
#ifdef __cplusplus
extern "C"
{
#endif
#include 
#include 
#include 
#ifdef __cplusplus
};
#endif
#endif
 
//H.265码流与YUV输入的帧数不同。经过观察对比其他程序后发现需要调用flush_encoder()将编码器中剩余的视频帧输出。当av_read_frame()循环退出的时候,实际上解码器中可能还包含剩余的几帧数据。
//因此需要通过“flush_decoder”将这几帧数据输出。“flush_decoder”功能简而言之即直接调用avcodec_decode_video2()获得AVFrame,而不再向解码器传递AVPacket
int flush_encoder(AVFormatContext *fmt_ctx, unsigned int stream_index){
	int ret;
	int got_frame;
	AVPacket enc_pkt;
 
	if (!(fmt_ctx->streams[stream_index]->codec->codec->capabilities &
		CODEC_CAP_DELAY))
		return 0;
	while (1) {
		enc_pkt.data = NULL;
		enc_pkt.size = 0;
		av_init_packet(&enc_pkt);
		ret = avcodec_encode_video2(fmt_ctx->streams[stream_index]->codec, &enc_pkt,
			NULL, &got_frame);
		av_frame_free(NULL);
		if (ret < 0)
			break;
		if (!got_frame){
			ret = 0;
			break;
		}
		printf("Flush Encoder: Succeed to encode 1 frame!\tsize:%5d\n", enc_pkt.size);
		/* mux encoded frame */
		ret = av_write_frame(fmt_ctx, &enc_pkt);
		if (ret < 0)
			break;
	}
	return ret;
}
 
int main(int argc, char* argv[])
{
	AVFormatContext* pFormatCtx = NULL;
	AVOutputFormat* fmt;
	AVStream* video_st;
	AVCodecContext* pCodecCtx;
	AVCodec* pCodec;
	AVPacket pkt;
	uint8_t* picture_buf;
	AVFrame* pFrame;
	int picture_size;
	int y_size;
	int framecnt = 0;
	FILE *in_file = fopen("chezaiyundong_1280x720_30_300.yuv", "rb");
	int in_w = 1280, in_h = 720;
	int framenum = 10;
	const char* out_file = "chezaiyundong_1280x720_30_300.hevc";
 
	av_register_all();//注册FFmpeg所有编解码器
	avformat_alloc_output_context2(&pFormatCtx, NULL, NULL, out_file);//初始化输出码流的AVFormatContext(获取输出文件的编码格式)
	fmt = pFormatCtx->oformat;
 
	// 打开文件的缓冲区输入输出,flags 标识为  AVIO_FLAG_READ_WRITE ,可读写;将输出文件中的数据读入到程序的 buffer 当中,方便之后的数据写入fwrite
	if (avio_open(&pFormatCtx->pb, out_file, AVIO_FLAG_READ_WRITE) < 0){
		printf("Failed to open output file! \n");
		return -1;
	}
	video_st = avformat_new_stream(pFormatCtx, 0);//创建输出码流的AVStream。
	// 设置 码率25 帧每秒(fps=25)
	video_st->time_base.num = 1;
	video_st->time_base.den = 25;
	if (video_st == NULL){
		return -1;
	}
 
	//为输出文件设置编码的参数和格式
	pCodecCtx = video_st->codec;// 从媒体流中获取到编码结构体,一个 AVStream 对应一个  AVCodecContext
	pCodecCtx->codec_id = fmt->video_codec;// 设置编码器的 id,例如 h265 的编码 id 就是 AV_CODEC_ID_H265
	pCodecCtx->codec_type = AVMEDIA_TYPE_VIDEO;//编码器视频编码的类型
	pCodecCtx->pix_fmt = AV_PIX_FMT_YUV420P;//设置像素格式为 yuv 格式
	pCodecCtx->width = in_w; //设置视频的宽高
	pCodecCtx->height = in_h;
	pCodecCtx->time_base.num = 1;
	pCodecCtx->time_base.den = 25;
	pCodecCtx->bit_rate = 400000;  //采样的码率;采样码率越大,视频大小越大
	pCodecCtx->gop_size = 250;//每250帧插入1个I帧,I帧越少,视频越小
	pCodecCtx->qmin = 10;最大和最小量化系数 
	//(函数输出的延时仅仅跟max_b_frames的设置有关,想进行实时编码,将max_b_frames设置为0便没有编码延时了)
	pCodecCtx->max_b_frames = 3;// 设置 B 帧最大的数量,B帧为视频图片空间的前后预测帧, B 帧相对于 I、P 帧来说,压缩率比较大,采用多编码 B 帧提高清晰度
 
	//设置编码速度
	AVDictionary *param = 0;
	//preset的参数调节编码速度和质量的平衡。
	//tune的参数值指定片子的类型,是和视觉优化的参数,
	//zerolatency: 零延迟,用在需要非常低的延迟的情况下,比如电视电话会议的编码
	if (pCodecCtx->codec_id == AV_CODEC_ID_H264) {
		av_dict_set(¶m, "preset", "slow", 0);
		av_dict_set(¶m, "tune", "zerolatency", 0);
		//av_dict_set(¶m, "profile", "main", 0);
	}
	//H.265
	if (pCodecCtx->codec_id == AV_CODEC_ID_H265){
		av_dict_set(¶m, "preset", "ultrafast", 0);
		av_dict_set(¶m, "tune", "zero-latency", 0);
	}
 
	//输出格式的信息,例如时间,比特率,数据流,容器,元数据,辅助数据,编码,时间戳
	av_dump_format(pFormatCtx, 0, out_file, 1);
 
	pCodec = avcodec_find_encoder(pCodecCtx->codec_id);//查找编码器
	if (!pCodec){
		printf("Can not find encoder! \n");
		return -1;
	}
	// 打开编码器,并设置参数 param
	if (avcodec_open2(pCodecCtx, pCodec, ¶m) < 0){
		printf("Failed to open encoder! \n");
		return -1;
	}
 
	//设置原始数据 AVFrame
	pFrame = av_frame_alloc();
	if (!pFrame) {
		printf("Could not allocate video frame\n");
		return -1;
	}
	pFrame->format = pCodecCtx->pix_fmt;
	pFrame->width = pCodecCtx->width;
	pFrame->height = pCodecCtx->height;
 
	// 获取YUV像素格式图片的大小
	picture_size = avpicture_get_size(pCodecCtx->pix_fmt, pCodecCtx->width, pCodecCtx->height);
	// 将 picture_size 转换成字节数据
	picture_buf = (uint8_t *)av_malloc(picture_size);
	// 设置原始数据 AVFrame 的每一个frame 的图片大小,AVFrame 这里存储着 YUV 非压缩数据
	avpicture_fill((AVPicture *)pFrame, picture_buf, pCodecCtx->pix_fmt, pCodecCtx->width, pCodecCtx->height);
	//写封装格式文件头
	avformat_write_header(pFormatCtx, NULL);
	//创建编码后的数据 AVPacket 结构体来存储 AVFrame 编码后生成的数据  //编码前:AVFrame  //编码后:AVPacket
	av_new_packet(&pkt, picture_size);
	// 设置 yuv 数据中Y亮度图片的宽高,写入数据到 AVFrame 结构体中
	y_size = pCodecCtx->width * pCodecCtx->height;
 
	for (int i = 0; i<framenum; i++){
		//Read raw YUV data
		if (fread(picture_buf, 1, y_size * 3 / 2, in_file) <= 0){
			printf("Failed to read raw data! \n");
			return -1;
		}
		else if (feof(in_file)){
			break;
		}
		pFrame->data[0] = picture_buf;              // 亮度Y
		pFrame->data[1] = picture_buf + y_size;      // U 
		pFrame->data[2] = picture_buf + y_size * 5 / 4;  // V
		//顺序显示解码后的视频帧
		pFrame->pts = i;
		// 设置这一帧的显示时间
		//pFrame->pts=i*(video_st->time_base.den)/((video_st->time_base.num)*25);
		int got_picture = 0;
		int ret = avcodec_encode_video2(pCodecCtx, &pkt, pFrame, &got_picture);//编码一帧视频。即将AVFrame(存储YUV像素数据)编码为AVPacket(存储H.264等格式的码流数据)
		if (ret < 0){
			printf("Failed to encode! \n");
			return -1;
		}
 
		if (got_picture == 1){
			printf("Succeed to encode frame: %5d\tsize:%5d\n", framecnt, pkt.size);
			framecnt++;
			pkt.stream_index = video_st->index;
			printf("video_st->index = %d\n", video_st->index);
			av_write_frame(pFormatCtx, &pkt);//将编码后的视频码流写入文件(fwrite)
			av_free_packet(&pkt);//释放内存
		}
	}
 
	//输出编码器中剩余的AVPacket
	int ret = flush_encoder(pFormatCtx, 0);
	if (ret < 0) {
		printf("Flushing encoder failed\n");
		return -1;
	}
 
	// 写入数据流尾部到输出文件当中,表示结束并释放文件的私有数据
	av_write_trailer(pFormatCtx);
 
	if (video_st){
		// 关闭编码器
		avcodec_close(video_st->codec);
		// 释放 AVFrame
		av_free(pFrame);
		// 释放图片 buf
		av_free(picture_buf);
	}
	// 关闭输入数据的缓存
	avio_close(pFormatCtx->pb);
	// 释放 AVFromatContext 结构体
	avformat_free_context(pFormatCtx);
	// 关闭输入文件
	fclose(in_file);
 
	return 0;
}

sdl

SDL(Simple DirectMedia Layer)库的作用说白了就是封装了复杂的视音频底层交互工作,简化了视音频处理的难度。

主要用来做游戏,现在只用到其视频显示部分。特点:跨平台,开源

1.库的结构图见图:
简历当中的知识点_第3张图片

3.SDL视频显示的流程图见图
简历当中的知识点_第4张图片
1).SDL视频显示函数简介

SDL_Init():初始化SDL系统

SDL_CreateWindow():创建窗口SDL_Window

SDL_CreateRenderer():创建渲染器SDL_Renderer

SDL_CreateTexture():创建纹理SDL_Texture

SDL_UpdateTexture():设置纹理的数据

SDL_RenderCopy():将纹理的数据拷贝给渲染器

SDL_RenderPresent():显示

SDL_Delay():工具函数,用于延时。

SDL_Quit():退出SDL系统

其中SDL_Delay 延时函数,控制显示的速度,即控制帧率。通常每秒25帧,所以通常延时也就是40ms

/*****************************************************************************
* Copyright (C) 2017-2020 Hanson Yu  All rights reserved.
------------------------------------------------------------------------------
* File Module       :     FFmpegAndSDL.cpp
* Description       :     FFmpegAndSDL Demo


* Created           :     2017.09.21.
* Author            :     Yu Weifeng
* Function List     :     
* Last Modified     :     
* History           :     
* Modify Date      Version         Author           Modification
* -----------------------------------------------
* 2017/09/21      V1.0.0         Yu Weifeng       Created
******************************************************************************/

#include "stdafx.h"
#include 


/*解决错误:
LNK2019    无法解析的外部符号 __imp__fprintf,该符号在函数 _ShowError 中被引用

原因:
……这是链接库问题
就是工程里面没有添加那两个函数需要的库,#progma这个是代码链接库
第二句是vs2015兼容的问题。
lib库的vs编译版本 和 工程的vs开发版本 不一致。
导出函数定义变了。所以要人为加一个函数导出。
*/
#pragma comment(lib, "legacy_stdio_definitions.lib")
extern "C" { FILE __iob_func[3] = { *stdin,*stdout,*stderr }; }

/*
__STDC_LIMIT_MACROS and __STDC_CONSTANT_MACROS are a workaround to allow C++ programs to use stdint.h
macros specified in the C99 standard that aren't in the C++ standard. The macros, such as UINT8_MAX, INT64_MIN,
and INT32_C() may be defined already in C++ applications in other ways. To allow the user to decide
if they want the macros defined as C99 does, many implementations require that __STDC_LIMIT_MACROS
and __STDC_CONSTANT_MACROS be defined before stdint.h is included.

This isn't part of the C++ standard, but it has been adopted by more than one implementation.
*/
#define __STDC_CONSTANT_MACROS

extern "C"
{
#include "libavcodec/avcodec.h"
#include "libavformat/avformat.h"
#include "libswscale/swscale.h"
#include "SDL2/SDL.h"
};


//Refresh Event 自定义事件
#define PLAY_REFRESH_EVENT       (SDL_USEREVENT + 1)//自定义刷新图像(播放)事件
#define PLAY_BREAK_EVENT         (SDL_USEREVENT + 2) //自定义退出播放事件


static int g_iThreadExitFlag = 0;
/*****************************************************************************
-Fuction        : RefreshPlayThread
-Description    : RefreshPlayThread
-Input          : 
-Output         : 
-Return         : 
* Modify Date      Version         Author           Modification
* -----------------------------------------------
* 2017/09/21      V1.0.0         Yu Weifeng       Created
******************************************************************************/
int RefreshPlayThread(void *opaque) 
{
    g_iThreadExitFlag = 0;
    SDL_Event tEvent={0};
    
    while (!g_iThreadExitFlag) 
    {
        tEvent.type = PLAY_REFRESH_EVENT;
        SDL_PushEvent(&tEvent);//发送事件给其他线程
        SDL_Delay(20);//延时函数 填40的时候,视频会有种卡的感觉
    }
    //Break
    g_iThreadExitFlag = 0;
    tEvent.type = PLAY_BREAK_EVENT;
    SDL_PushEvent(&tEvent);//发送事件给其他线程 发送一个事件

    return 0;
}

/*****************************************************************************
-Fuction        : main
-Description    : main
-Input          : 
-Output         : 
-Return         : 
* Modify Date      Version         Author           Modification
* -----------------------------------------------
* 2017/09/21      V1.0.0         Yu Weifeng       Created
******************************************************************************/
int main(int argc, char* argv[])
{
    /*------------FFmpeg----------------*/
    const char *strFilePath = "屌丝男士.mov";
    AVFormatContext    *ptFormatContext = NULL;//封装格式上下文,内部包含所有的视频信息
    int                i = 0; 
    int             iVideoindex=0;//纯视频信息在音视频流中的位置,也就是指向音视频流数组中的视频元素
    AVCodecContext    *ptCodecContext;//编码器相关信息上下文,内部包含编码器相关的信息,指向AVFormatContext中的streams成员中的codec成员
    AVCodec            *ptCodec;//编码器,使用函数avcodec_find_decoder或者,该函数需要的id参数,来自于ptCodecContext中的codec_id成员
    AVFrame            *ptFrame=NULL;//存储一帧解码后像素(采样)数据
    AVFrame            *ptFrameAfterScale=NULL;//存储(解码数据)转换后的像素(采样)数据
    unsigned char   *pucFrameAfterScaleBuf=NULL;//用于存储ptFrameAfterScale中的像素(采样)缓冲数据
    AVPacket        *ptPacket=NULL;//存储一帧压缩编码数据
    int             iRet =0;
    int             iGotPicture=0;//解码函数的返回参数,got_picture_ptr Zero if no frame could be decompressed, otherwise, it is nonzero

    /*------------SDL----------------*/
    int iScreenWidth=0, iScreenHeight=0;//视频的宽和高,指向ptCodecContext中的宽和高
    SDL_Window *ptSdlWindow=NULL;//用于sdl显示视频的窗口(用于显示的屏幕)
    SDL_Renderer* ptSdlRenderer=NULL;//sdl渲染器,把纹理数据画(渲染)到window上
    SDL_Texture* ptSdlTexture=NULL;//sdl纹理数据,用于存放像素(采样)数据,然后给渲染器
    SDL_Rect tSdlRect ={0};//正方形矩形结构,存了矩形的坐标,长宽,以便确定纹理数据画在哪个位置,确定位置用,比如画在左上角就用这个来确定。被渲染器调用
    SDL_Thread *ptVideoControlTID=NULL;//sdl线程id,线程的句柄
    SDL_Event tSdlEvent = {0};//sdl事件,代表一个事件

    /*------------像素数据处理----------------*/
    struct SwsContext *ptImgConvertInfo;//图像转换(上下文)信息,图像转换函数sws_scale需要的参数,由sws_getContext函数赋值



    /*------------FFmpeg----------------*/
    av_register_all();//注册FFmpeg所有组件
    avformat_network_init();//初始化网络组件
    
    ptFormatContext = avformat_alloc_context();//分配空间给ptFormatContext
    if (avformat_open_input(&ptFormatContext, strFilePath, NULL, NULL) != 0) 
    {//打开输入视频文件
        printf("Couldn't open input stream.\n");
        return -1;
    }
    if (avformat_find_stream_info(ptFormatContext, NULL)<0) 
    {//获取视频文件信息
        printf("Couldn't find stream information.\n");
        return -1;
    }
    //获取编码器相关信息上下文,并赋值给ptCodecContext
    iVideoindex = -1;
    for (i = 0; i<ptFormatContext->nb_streams; i++)
    {
        if (ptFormatContext->streams[i]->codec->codec_type == AVMEDIA_TYPE_VIDEO) 
        {
            iVideoindex = i;
            break;
        }
    }
    if (iVideoindex == -1) 
    {
        printf("Didn't find a video stream.\n");
        return -1;
    }
    ptCodecContext = ptFormatContext->streams[iVideoindex]->codec;
    
    ptCodec = avcodec_find_decoder(ptCodecContext->codec_id);//查找解码器
    if (ptCodec == NULL) 
    {
        printf("Codec not found.\n");
        return -1;
    }
    if (avcodec_open2(ptCodecContext, ptCodec, NULL)<0) 
    {//打开解码器
        printf("Could not open codec.\n");
        return -1;
    }
    
    ptPacket = (AVPacket *)av_malloc(sizeof(AVPacket));//分配保存解码前数据的空间
    ptFrame = av_frame_alloc();//分配结构体空间,结构体内部的指针指向的数据暂未分配,用于保存图像转换前的像素数据
    
    /*------------像素数据处理----------------*/
    ptFrameAfterScale = av_frame_alloc();//分配结构体空间,结构体内部的指针指向的数据暂未分配,用于保存图像转换后的像素数据
    pucFrameAfterScaleBuf = (uint8_t *)av_malloc(avpicture_get_size(PIX_FMT_YUV420P, ptCodecContext->width, ptCodecContext->height));//分配保存数据的空间
     /*int avpicture_fill(AVPicture *picture, uint8_t *ptr,int pix_fmt, int width, int height);
    这个函数的使用本质上是为已经分配的空间的结构体(AVPicture *)ptFrame挂上一段用于保存数据的空间,
    这个结构体中有一个指针数组data[AV_NUM_DATA_POINTERS],挂在这个数组里。一般我们这么使用:
    1) pFrameRGB=avcodec_alloc_frame();
    2) numBytes=avpicture_get_size(PIX_FMT_RGB24, pCodecCtx->width,pCodecCtx->height);
        buffer=(uint8_t *)av_malloc(numBytes*sizeof(uint8_t));
    3) avpicture_fill((AVPicture *)pFrameRGB, buffer, PIX_FMT_RGB24,pCodecCtx->width, pCodecCtx->height);
    以上就是为pFrameRGB挂上buffer。这个buffer是用于存缓冲数据的。
    ptFrame为什么不用fill空间。主要是下面这句:
    avcodec_decode_video(pCodecCtx, pFrame, &frameFinished,packet.data, packet.size);
    很可能是ptFrame已经挂上了packet.data,所以就不用fill了。*/
    avpicture_fill((AVPicture *)ptFrameAfterScale, pucFrameAfterScaleBuf, PIX_FMT_YUV420P, ptCodecContext->width, ptCodecContext->height);    
    //sws开头的函数用于处理像素(采样)数据
    ptImgConvertInfo = sws_getContext(ptCodecContext->width, ptCodecContext->height, ptCodecContext->pix_fmt,
        ptCodecContext->width, ptCodecContext->height, PIX_FMT_YUV420P, SWS_BICUBIC, NULL, NULL, NULL);//获取图像转换(上下文)信息

    /*------------SDL----------------*/
    if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_TIMER)) 
    {//初始化SDL系统
        printf("Could not initialize SDL - %s\n", SDL_GetError());
        return -1;
    }
    //SDL 2.0 Support for multiple windows
    iScreenWidth = ptCodecContext->width;
    iScreenHeight = ptCodecContext->height;
    ptSdlWindow = SDL_CreateWindow("Simplest ffmpeg player's Window", SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED,
        iScreenWidth, iScreenHeight, SDL_WINDOW_OPENGL);//创建窗口SDL_Window

    if (!ptSdlWindow) 
    {
        printf("SDL: could not create window - exiting:%s\n", SDL_GetError());
        return -1;
    }
    ptSdlRenderer = SDL_CreateRenderer(ptSdlWindow, -1, 0);//创建渲染器SDL_Renderer
    //IYUV: Y + U + V  (3 planes)
    //YV12: Y + V + U  (3 planes)
    //创建纹理SDL_Texture
    ptSdlTexture = SDL_CreateTexture(ptSdlRenderer, SDL_PIXELFORMAT_IYUV, SDL_TEXTUREACCESS_STREAMING, ptCodecContext->width, ptCodecContext->height);

    tSdlRect.x = 0;//x y值是左上角为圆点开始的坐标值,调整x y值以及w h值,就可以实现在窗口的指定位置显示,没有画面的地方为黑框
    tSdlRect.y = 0;//当x y等于0,w h等于窗口的宽高时即为全屏显示,此时调整宽高大小,只需调整窗口大小即可
    tSdlRect.w = iScreenWidth;
    tSdlRect.h = iScreenHeight;

    ptVideoControlTID = SDL_CreateThread(RefreshPlayThread, NULL, NULL);//创建一个线程
    
    while (1) 
    {//Event Loop        
        SDL_WaitEvent(&tSdlEvent);//Wait,等待其他线程过来的事件
        if (tSdlEvent.type == PLAY_REFRESH_EVENT) //自定义刷新图像(播放)事件
        {
            /*------------FFmpeg----------------*/
            if (av_read_frame(ptFormatContext, ptPacket) >= 0) //从输入文件读取一帧压缩数据
            {
                if (ptPacket->stream_index == iVideoindex) 
                {
                    iRet = avcodec_decode_video2(ptCodecContext, ptFrame, &iGotPicture, ptPacket);//解码一帧压缩数据
                    if (iRet < 0) 
                    {
                        printf("Decode Error.\n");
                        return -1;
                    }
                    if (iGotPicture) 
                    {
                        //图像转换,sws_scale()函数需要用到的转换信息,即第一个参数,是由sws_getContext函数获得的
                        sws_scale(ptImgConvertInfo, (const uint8_t* const*)ptFrame->data, ptFrame->linesize, 0, ptCodecContext->height, ptFrameAfterScale->data, ptFrameAfterScale->linesize);

                        /*------------SDL----------------*/
                        SDL_UpdateTexture(ptSdlTexture, NULL, ptFrameAfterScale->data[0], ptFrameAfterScale->linesize[0]);//设置(更新)纹理的数据
                        SDL_RenderClear(ptSdlRenderer);//先清除渲染器里的数据
                        //SDL_RenderCopy( ptSdlRenderer, ptSdlTexture, &tSdlRect, &tSdlRect );  //将纹理的数据拷贝给渲染器
                        SDL_RenderCopy(ptSdlRenderer, ptSdlTexture, NULL, NULL);//将纹理的数据拷贝给渲染器
                        SDL_RenderPresent(ptSdlRenderer);//显示
                    }
                }
                av_free_packet(ptPacket);//释放空间
            }
            else 
            {                
                g_iThreadExitFlag = 1;//Exit Thread
            }
        }
        else if (tSdlEvent.type == SDL_QUIT) //也是SDL自带的事件,当点击窗口的×时触发//SDL_WINDOWENVENT sdl系统自带的事件,当拉伸窗口的时候会触发
        {
            g_iThreadExitFlag = 1;
        }
        else if (tSdlEvent.type == PLAY_BREAK_EVENT) //自定义退出播放事件
        {
            break;
        }

    }
    
    /*------------像素数据处理----------------*/
    sws_freeContext(ptImgConvertInfo);//释放空间
    
    /*------------SDL----------------*/
    SDL_Quit();//退出SDL系统

    /*------------FFmpeg----------------*/
    av_frame_free(&ptFrameAfterScale);//释放空间
    av_frame_free(&ptFrame);//释放空间
    avcodec_close(ptCodecContext);//关闭解码器
    avformat_close_input(&ptFormatContext);//关闭输入视频文件

    return 0;
}

FFmpegAndSDL.cpp

RTP原理

RTP Header解析

简历当中的知识点_第5张图片

  1.    V:RTP协议的版本号,占2位,当前协议版本号为2
    
  2.    P:填充标志,占1位,如果P=1,则在该报文的尾部填充一个或多个额外的八位组,它们不是有效载荷的一部分。
    
  3.    X:扩展标志,占1位,如果X=1,则在RTP报头后跟有一个扩展报头
    
  4.    CC:CSRC计数器,占4位,指示CSRC 标识符的个数
    
  5.    M: 标记,占1位,不同的有效载荷有不同的含义,对于视频,标记一帧的结束;对于音频,标记会话的开始。
    

6、 PT: 有效荷载类型,占7位,用于说明RTP报文中有效载荷的类型,
如GSM音频、JPEM图像等,在流媒体中大部分是用来区分音频流和视频流的,这样便于客户端进行解析。

7、 序列号:占16位,用于标识发送者所发送的RTP报文的序列号,
每发送一个报文,序列号增1。这个字段当下层的承载协议用UDP的时候,
网络状况不好的时候可以用来检查丢包。同时出现网络抖动的情况可以用来对数据进行重新排序,
序列号的初始值是随机的,同时音频包和视频包的sequence是分别记数的。

8、 时戳(Timestamp):占32位,必须使用90 kHz 时钟频率。
时戳反映了该RTP报文的第一个八位组的采样时刻。接收者使用时戳来计算延迟和延迟抖动,
并进行同步控制。

9、 同步信源(SSRC)标识符:占32位,
用于标识同步信源。该标识符是随机选择的,参加同一视频会议的两个同步信源不能有相同的SSRC。

10、 特约信源(CSRC)标识符:每个CSRC标识符占32位,可以有0~15个。
每个CSRC标识了包含在该RTP报文有效载荷中的所有特约信源。

注:基本的RTP说明并不定义任何头扩展本身,如果遇到X=1,需要特殊处理

RTP荷载H264码流

简历当中的知识点_第6张图片
荷载格式定义三个不同的基本荷载结构,接收者可以通过RTP荷载的第一个字节后5位(如图2)识别荷载结构。

  1. 单个NAL单元包:荷载中只包含一个NAL单元。NAL头类型域等于原始 NAL单元类型,即在范围1到23之间

  2. 聚合包:本类型用于聚合多个NAL单元到单个RTP荷载中。本包有四种版本,单时间聚合包类型A (STAP-A),单时间聚合包类型B (STAP-B),多时间聚合包类型(MTAP)16位位移(MTAP16), 多时间聚合包类型(MTAP)24位位移(MTAP24)。赋予STAP-A, STAP-B, MTAP16, MTAP24的NAL单元类型号分别是 24,25, 26, 27

  3. 分片单元:用于分片单个NAL单元到多个RTP包。现存两个版本FU-A,FU-B,用NAL单元类型 28,29标识

常用的打包时的分包规则是:如果小于MTU采用单个NAL单元包,如果大于MTU就采用FUs分片方式。
因为常用的打包方式就是单个NAL包和FU-A方式,所以我们只解析这两种。
简历当中的知识点_第7张图片
S: 1 bit 当设置成1,开始位指示分片NAL单元的开始。当跟随的FU荷载不是分片NAL单元荷载的开始,开始位设为0。

E: 1 bit 当设置成1, 结束位指示分片NAL单元的结束,即, 荷载的最后字节也是分片NAL单元的最后一个字节。当跟随的 FU荷载不是分片NAL单元的最后分片,结束位设置为0。

R: 1 bit 保留位必须设置为0,接收者必须忽略该位

RTP定义

实时传输协议(Real-time Transport Protocol,RTP)是在Internet上处理多媒体数据流的一种网络协议,利用它能够在一对一(unicast,单播)或者一对多(multicast,多播)的网络环境中实现传流媒体数据的实时传输(不需要下载完毕后才能看视频)。RTP通常使用UDP来进行多媒体数据的传输,但如果需要的话可以使用TCP等其它协议,整个RTP协议由两个密切相关的部分组成:RTP数据协议和RTCP控制协议。
RTP数据协议负责对流媒体数据进行封包并实现媒体流的实时传输,每一个RTP数据报都由头部(Header)和负载(Payload)两个部分组成,其中头部前12个字节的含义是固定的,而负载则可以是音频或者视频数据。

RTCP 控制协议需要与RTP数据协议一起配合使用,当应用程序启动一个RTP会话时将同时占用两个端口,分别供RTP和RTCP使用。RTP本身并不能为按序传输数据包提供可靠的保证,也不提供流量控制和拥塞控制,这些都由RTCP来负责完成。通常RTCP会采用与RTP相同的分发机制,向会话中的所有成员周期性地发送控制信息,应用程序通过接收这些数据,从中获取会话参与者的相关资料,以及网络状况、分组丢失概率等反馈信息,从而能 够对服务质量进行控制或者对网络状况进行诊断。

实时流协议(RealTime Streaming Protocol,RTSP),它的意义在于使得实时流媒体数据的受控和点播变得可能。总的说来,RTSP是一个流媒体表示协议, 主要用来控制具有实时特性的数据发送,但它本身并不传输数据,而是必须依赖于下层传输协议所提供的某些服务。RTSP 可以对流媒体提供诸如播放、暂停、快进等操作,它负责定义具体的控制消息、操作方法、状态码等,此外还描述了与RTP间的交互操作。

  • 如何使用RTP
    参考连接参考连接
    rtp的运行当然少不了JRTPLIB库的支持,JRTPLIB是一个面向对象的RTP封装库,安装过程如下:
    1、这里用的是jrtplib-3.7.1,下载地址:

RTP是传输层的子层

RTP(实时传输协议),顾名思义它是用来提供实时传输的,因而可以看成是传输层的一个子层。下图给出了流媒体应用中的一个典型的协议体系结构。
简历当中的知识点_第8张图片

从图中可以看出,RTP被划分在传输层,它建立在UDP(一般实际情况是基于UDP,基于TCP效率太低)上。同UDP协议一样,为了实现其实时传输功能,RTP也有固定的封装形式。RTP用来为端到端的实时传输提供时间信息和流同步,但并不保证服务质量。服务质量由RTCP来提供。这些特点,在第4章可以看到。不少人也把RTP归为应用层的一部分,这是从应用开发者的角度来说的。操作系统中的TCP/IP等协议栈所提供的是我们最常用的服务,而RTP的实现还是要靠开发者自己

你可能感兴趣的:(C/C++)