首先准备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<存储类型名称>(行,列)[通道]
H265编码器仍旧采用变换和预测的混合编码方法。输入帧以宏块为单位被编码器处理,首先按照帧内或帧间预测编码的方法进行处理;接着,预测值与当前块相减,相减后得到的残差块经变换、量化后产生一组量化后的变换系数;最后,这组量化后的变换系数经过熵编码,与解码所需的一些头信息(如预测模式量化参数、运动矢量等)一起组成一个压缩后的码流,经NAL(网络自适应层)供传输和存储用。为了提供进一步预测用的参考图像,编码器必须有重建的功能。为了去除编解码环路中产生的噪声,提高参考帧的图像质量,从而提高图像压缩性能,设置了一个环路滤波器,滤波后的输出即是重建图像,可用作参考图像。
帧间和帧内预测(Estimation):图像经过帧内预测和帧间预测后,与原始视频帧进行相减形成预测残差。
变换(Transform)和反变换:将图像的时域信号变换为频域的信号,在频域中信号的能量集中在低频区域,并使其码率相对于空间信号有大幅下降。
量化(Quantization)和反量化:不降低视觉效果的前提下,保留图像的细节,确定量化参数(QP),减少图像的编码长度。
环路滤波(Loop Filter):对块边界处的像素进行滤波以平滑像素值的突变,消除视频图像中的块效应,同时可以达到降低噪音的效果。
熵编码(Entropy Coding):利用信息的统计冗余进行数据压缩的无损编码方法.
构成FFmpeg主要有三个部分,第一部分是四个作用不同的工具软件,分别是:ffmpeg.exe,ffplay.exe,ffserver.exe和ffprobe.exe。
ffmpeg.exe:音视频转码、转换器
ffplay.exe:简单的音视频播放器
ffserver.exe:流媒体服务器
ffprobe.exe:简单的多媒体码流分析器
第二部分是可以供开发者使用的SDK,为各个不同平台编译完成的库。如果说上面的四个工具软件都是完整成品形式的玩具,那么这些库就相当于乐高积木一样,我们可以根据自己的需求使用这些库开发自己的应用程序。这些库有:
为了实现调用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;
}
data[]
对于packed格式的数据(例如RGB24),会存到data[0]里面。
对于planar格式的数据(例如YUV420P),则会分开成data[0],data[1],data[2]…(YUV420P中data[0]存Y,data[1]存U,data[2]存V)
uint8_t *data:压缩编码的数据。
int size:data的大小
int64_t pts:显示时间戳
int64_t dts:解码时间戳
int stream_index:标识该AVPacket所属的视频/音频流。
1.注册所有编解码器:av_register_all();
2.声明一个AVCodec类型的指针,比如说AVCodec* first_c;
3.调用av_codec_next()函数,即可获得指向链表下一个解码器的指针,循环往复可以获得所有解码器的信息。注意,如果想要获得指向第一个解码器的指针,则需要将该函数的参数设置为NULL。
如下图是基于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()等。
(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()
/**
* 基于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(Simple DirectMedia Layer)库的作用说白了就是封装了复杂的视音频底层交互工作,简化了视音频处理的难度。
主要用来做游戏,现在只用到其视频显示部分。特点:跨平台,开源
3.SDL视频显示的流程图见图
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
V:RTP协议的版本号,占2位,当前协议版本号为2
P:填充标志,占1位,如果P=1,则在该报文的尾部填充一个或多个额外的八位组,它们不是有效载荷的一部分。
X:扩展标志,占1位,如果X=1,则在RTP报头后跟有一个扩展报头
CC:CSRC计数器,占4位,指示CSRC 标识符的个数
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荷载的第一个字节后5位(如图2)识别荷载结构。
单个NAL单元包:荷载中只包含一个NAL单元。NAL头类型域等于原始 NAL单元类型,即在范围1到23之间
聚合包:本类型用于聚合多个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
分片单元:用于分片单个NAL单元到多个RTP包。现存两个版本FU-A,FU-B,用NAL单元类型 28,29标识
常用的打包时的分包规则是:如果小于MTU采用单个NAL单元包,如果大于MTU就采用FUs分片方式。
因为常用的打包方式就是单个NAL包和FU-A方式,所以我们只解析这两种。
S: 1 bit 当设置成1,开始位指示分片NAL单元的开始。当跟随的FU荷载不是分片NAL单元荷载的开始,开始位设为0。
E: 1 bit 当设置成1, 结束位指示分片NAL单元的结束,即, 荷载的最后字节也是分片NAL单元的最后一个字节。当跟随的 FU荷载不是分片NAL单元的最后分片,结束位设置为0。
R: 1 bit 保留位必须设置为0,接收者必须忽略该位
实时传输协议(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被划分在传输层
,它建立在UDP(一般实际情况是基于UDP,基于TCP效率太低)上。同UDP协议一样,为了实现其实时传输功能,RTP也有固定的封装形式。RTP用来为端到端的实时传输提供时间信息和流同步,但并不保证服务质量。服务质量由RTCP来提供。这些特点,在第4章可以看到。不少人也把RTP归为应用层
的一部分,这是从应用开发者的角度来说的。操作系统中的TCP/IP等协议栈所提供的是我们最常用的服务,而RTP的实现还是要靠开发者自己