学习雷神的博客,向雷神致敬~
看了雷神的小学期视频课,在Github上下载了simplest_ffmpeg_player的代码,为代码加上了注释,作为留存。
2019.07.22
前置知识点
simplest_ffmpeg_player.cpp注释
《基于 FFmpeg + SDL 的视频播放器的制作》课程的视频
SDL视频显示
WSAStartup函数
// C++
int WSAStartup(
WORD wVersionRequired,
LPWSADATA lpWSAData
);
wVersionRequired
TBD
lpWSAData
指向WSADATA数据结构的指针 ,用于接收Windows套接字实现的详细信息
如果成功, WSAStartup函数返回0。否则,它将返回下面列出的错误代码之一。
错误代码 | 含义 |
---|---|
WSASYSNOTREADY | 底层网络子系统尚未准备好进行网络通信 |
WSAVERNOTSUPPORTED | 此特定Windows套接字实现不提供所请求的Windows套接字支持的版本 |
WSAEINPROGRESS | 正在阻止Windows Sockets 1.1操作 |
WSAEPROCLIM | 已达到对Windows套接字实现支持的任务数量的限制 |
WSAEFAULT | 该lpWSAData参数不是一个有效的指针 |
WSAStartup函数必须是应用程序或DLL调用的第一个Windows socket功能函数。它允许应用程序或DLL指定所需的Windows套接字版本,并检索特定Windows套接字实现的详细信息。应用程序或DLL只能在成功调用WSAStartup后发出更多Windows套接字函数 。
为了支持与最新版本的Windows套接字规范存在功能差异的各种Windows套接字实现和应用程序,在WSAStartup中进行协商 。WSAStartup的调用者 在wVersionRequested参数中传递应用程序支持的最高版本的Windows套接字规范。
Winsock DLL指示它在响应中可以支持的最高版本的Windows套接字规范。Winsock DLL还回复了它期望调用者使用的Windows套接字规范的版本。
当应用程序或DLL调用 WSAStartup函数时,Winsock DLL会检查wVersionRequested参数中传递的应用程序所请求的Windows套接字规范的版本 。如果应用程序请求的版本等于或高于Winsock DLL支持的最低版本,则调用成功,Winsock DLL返回lpWSAData参数指向的WSADATA结构中的详细信息。WSADATA结构的wHighVersion成员表示Winsock DLL支持的最高版本的Windows套接字规范。WSADATA的wVersion成员 structure表示Winsock DLL期望调用者使用的Windows套接字规范的版本。
如果WSADATA结构的wVersion成员对 调用者不可接受,则应用程序或DLL应调用 WSACleanup以释放Winsock DLL资源,并且无法初始化Winsock应用程序。为了支持此应用程序或DLL,有必要搜索要在平台上安装的Winsock DLL的更新版本。
以下代码片段演示了仅支持2.2版Windows套接字的应用程序如何进行 WSAStartup调用:
#define WIN32_LEAN_AND_MEAN
#include
#include
#include
#include
// Need to link with Ws2_32.lib
#pragma comment(lib, "ws2_32.lib")
int __cdecl main()
{
WORD wVersionRequested;
WSADATA wsaData;
int err;
/* Use the MAKEWORD(lowbyte, highbyte) macro declared in Windef.h */
wVersionRequested = MAKEWORD(2, 2);
err = WSAStartup(wVersionRequested, &wsaData);
if (err != 0) {
/* Tell the user that we could not find a usable */
/* Winsock DLL. */
printf("WSAStartup failed with error: %d\n", err);
return 1;
}
/* Confirm that the WinSock DLL supports 2.2.*/
/* Note that if the DLL supports versions greater */
/* than 2.2 in addition to 2.2, it will still return */
/* 2.2 in wVersion since that is the version we */
/* requested. */
if (LOBYTE(wsaData.wVersion) != 2 || HIBYTE(wsaData.wVersion) != 2) {
/* Tell the user that we could not find a usable */
/* WinSock DLL. */
printf("Could not find a usable version of Winsock.dll\n");
WSACleanup();
return 1;
}
else
printf("The Winsock 2.2 dll was found okay\n");
/* The Winsock DLL is acceptable. Proceed to use it. */
/* Add network programming using Winsock here */
/* then call WSACleanup when done using the Winsock dll */
WSACleanup();
}
1.什么是纹理
这里简单介绍一下,先看看现实生活中的例子吧,其实纹理的例子比比皆是,比如地板,墙面都是纹理。在图形学中,纹理主要是为了增强场景的真实感,如果你想绘制一个地面,简单一点可以直接使用一个矩形,稍微复杂一点可以用三角形网格,再复杂一点可以使用地面纹理,有了纹理以后真实感明显增强了。
2.渲染到纹理
常规的渲染操作都是直接将场景呈现到backbuffer中的,backbuffer说白了其实就是一个表面,再说白了就是一块内存,场景通过绘制函数载入显存后,再通过Present函数送至显示器。那么为什么还要渲染到纹理呢?这是为了实现一些特殊的效果,比如常见的环境映射,简单的说,想象你有一个光滑的球体,它应该是可以反射周围环境的,这就是环境映射。
3.视频OpenGL渲染与SDL渲染
不同视频的编码与解码。OpenGL更好的显示视频的方式也是通过纹理(Texture)。
纹理(Texture)和表面(Surface)的区别与联系:
1.Surfaces是一个存储2D图像的内存。
2.Textures是一张贴图。Texture的图像数据存储于它的Surface中。一个Texture可以包含多个Surface。
纹理,渲染
1.纹理,如果可以把一张2D图片“贴”到3D模型的表面上,则不但节约了计算机绘图的计算量,而且也能达到更真实的效果。纹理就是这张“贴图”。
2.渲染就是从模型生成图像的过程,通常是3D制作的最后一步。如在经过纹理贴图之后,形成了一个“木箱”模型。但是只有把它作为2D图像输出到屏幕上之后,它才能被我们的看见。这个输出的过程就是渲染。我们也可以调整这个“木箱模型”的参数,达到不同的渲染结果。比如说改变观察角度等等。
/**
* 最简单的基于FFmpeg的视频播放器 2
* Simplest FFmpeg Player 2
*
* 雷霄骅 Lei Xiaohua
* [email protected]
* 中国传媒大学/数字电视技术
* Communication University of China / Digital TV Technology
* http://blog.csdn.net/leixiaohua1020
*
* 第2版使用SDL2.0取代了第一版中的SDL1.2
* Version 2 use SDL 2.0 instead of SDL 1.2 in version 1.
*
* 本程序实现了视频文件的解码和显示(支持HEVC,H.264,MPEG2等)。
* 是最简单的FFmpeg视频解码方面的教程。
* 通过学习本例子可以了解FFmpeg的解码流程。
* This software is a simplest video player based on FFmpeg.
* Suitable for beginner of FFmpeg.
*
*/
#include
#define __STDC_CONSTANT_MACROS
#ifdef _WIN32
//Windows
extern "C"
{
#include "libavcodec/avcodec.h"
#include "libavformat/avformat.h"
#include "libswscale/swscale.h"
#include "libavutil/imgutils.h"
#include "SDL2/SDL.h"
};
#else
//Linux...
#ifdef __cplusplus
extern "C"
{
#endif
#include
#include
#include
#include
#include
#ifdef __cplusplus
};
#endif
#endif
//Output YUV420P data as a file
#define OUTPUT_YUV420P 0
int main(int argc, char* argv[])
{
// 封装格式上下文的结构体,也是统领全局的结构体,保存了视频文件封装格式的相关信息
AVFormatContext *pFormatCtx;
// 视频流在文件中的位置
int i, videoindex;
// 编码器上下文结构体,保存了视频(音频)编解码相关信息
AVCodecContext *pCodecCtx;
// 每种视频(音频)编解码器(例如H.264解码器)对应一个该结构体
AVCodec *pCodec;
// 存储一帧解码后像素(采样)数据
AVFrame *pFrame,*pFrameYUV;
// 存储图像数据
unsigned char *out_buffer;
// 存储一帧压缩编码数据
AVPacket *packet;
int y_size;
int ret, got_picture;
struct SwsContext *img_convert_ctx;
char filepath[]="bigbuckbunny_480x272.h265";
//SDL---------------------------
int screen_w=0,screen_h=0;
SDL_Window *screen;
SDL_Renderer* sdlRenderer;
SDL_Texture* sdlTexture;
SDL_Rect sdlRect;
FILE *fp_yuv;
// 注册复用器,编码器等(参考FFmpeg解码流程图)
av_register_all();
/**
* libavformat->utils.c
* 进行网络组件的全局初始化。 这是可选的,但建议使用,因为它避免了为每个会话隐式执行设置的开销。
* 如果在某些主要版本的版本中使用网络协议,则调用此函数将成为必需的。
*
* int avformat_network_init(void)
* {
* #if CONFIG_NETWORK
* int ret;
* ff_network_inited_globally = 1;
* if ((ret = ff_network_init()) < 0)
* return ret;
* if ((ret = ff_tls_init()) < 0)
* return ret;
* #endif
* return 0;
* }
*
* ----------------------------------
*
* libavformat->network.c
*
* int ff_network_init(void)
* {
* #if HAVE_WINSOCK2_H
* WSADATA wsaData;
* #endif
*
* if (!ff_network_inited_globally)
* av_log(NULL, AV_LOG_WARNING, "Using network protocols without global "
* "network initialization. Please use "
* "avformat_network_init(), this will "
* "become mandatory later.\n");
* #if HAVE_WINSOCK2_H
* if (WSAStartup(MAKEWORD(1,1), &wsaData))
* return 0;
* #endif
* return 1;
* }
*
* int ff_tls_init(void)
* {
* #if CONFIG_TLS_OPENSSL_PROTOCOL
* int ret;
* if ((ret = ff_openssl_init()) < 0)
* return ret;
* #endif
* #if CONFIG_TLS_GNUTLS_PROTOCOL
* ff_gnutls_init();
* #endif
* return 0;
* }
*/
avformat_network_init();
pFormatCtx = avformat_alloc_context();
// 打开多媒体数据并且获得一些相关的信息(参考FFmpeg解码流程图)
if(avformat_open_input(&pFormatCtx,filepath,NULL,NULL)!=0){
printf("Couldn't open input stream.\n");
return -1;
}
// 读取一部分视音频数据并且获得一些相关的信息(参考FFmpeg解码流程图)
if(avformat_find_stream_info(pFormatCtx,NULL)<0){
printf("Couldn't find stream information.\n");
return -1;
}
// 每个视频文件中有多个流(视频流、音频流、字幕流等,而且可有多个),循环遍历找到视频流
// 判断方式:AVFormatContext->AVStream->AVCodecContext->codec_type是否为AVMEDIA_TYPE_VIDEO
videoindex=-1;
for(i=0; inb_streams; i++)
if(pFormatCtx->streams[i]->codec->codec_type==AVMEDIA_TYPE_VIDEO){
videoindex=i;
break;
}
// 如果没有视频流,返回
if(videoindex==-1){
printf("Didn't find a video stream.\n");
return -1;
}
// 保存视频流中的AVCodecContext
pCodecCtx=pFormatCtx->streams[videoindex]->codec;
// 用于查找FFmpeg的解码器(参考FFmpeg解码流程图)
pCodec=avcodec_find_decoder(pCodecCtx->codec_id);
if(pCodec==NULL){
printf("Codec not found.\n");
return -1;
}
// 初始化一个视音频编解码器的AVCodecContext(参考FFmpeg解码流程图)
if(avcodec_open2(pCodecCtx, pCodec,NULL)<0){
printf("Could not open codec.\n");
return -1;
}
// 创建一个AVFrame,用来存放解码后的一帧的数据
pFrame=av_frame_alloc();
pFrameYUV=av_frame_alloc();
// av_image_get_buffer_size:返回使用给定参数存储图像所需的数据量的字节大小
out_buffer=(unsigned char *)av_malloc(av_image_get_buffer_size(AV_PIX_FMT_YUV420P, pCodecCtx->width, pCodecCtx->height,1));
// 根据指定的图像参数和提供的数组设置数据指针和线条(data pointers and linesizes)
av_image_fill_arrays(pFrameYUV->data, pFrameYUV->linesize,out_buffer,
AV_PIX_FMT_YUV420P,pCodecCtx->width, pCodecCtx->height,1);
// 创建一个AVPacket,用来存放下面循环获取到的未解码帧
packet=(AVPacket *)av_malloc(sizeof(AVPacket));
//Output Info-----------------------------
printf("--------------- File Information ----------------\n");
av_dump_format(pFormatCtx,0,filepath,0);
printf("-------------------------------------------------\n");
// sws_getContext():初始化一个SwsContext
img_convert_ctx = sws_getContext(pCodecCtx->width, pCodecCtx->height, pCodecCtx->pix_fmt,
pCodecCtx->width, pCodecCtx->height, AV_PIX_FMT_YUV420P, SWS_BICUBIC, NULL, NULL, NULL);
#if OUTPUT_YUV420P
fp_yuv=fopen("output.yuv","wb+");
#endif
if(SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO | SDL_INIT_TIMER)) {
printf( "Could not initialize SDL - %s\n", SDL_GetError());
return -1;
}
screen_w = pCodecCtx->width;
screen_h = pCodecCtx->height;
//SDL 2.0 Support for multiple windows
screen = SDL_CreateWindow("Simplest ffmpeg player's Window", SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED,
screen_w, screen_h,
SDL_WINDOW_OPENGL);
if(!screen) {
printf("SDL: could not create window - exiting:%s\n",SDL_GetError());
return -1;
}
sdlRenderer = SDL_CreateRenderer(screen, -1, 0);
//IYUV: Y + U + V (3 planes)
//YV12: Y + V + U (3 planes)
sdlTexture = SDL_CreateTexture(sdlRenderer, SDL_PIXELFORMAT_IYUV, SDL_TEXTUREACCESS_STREAMING,pCodecCtx->width,pCodecCtx->height);
sdlRect.x=0;
sdlRect.y=0;
sdlRect.w=screen_w;
sdlRect.h=screen_h;
//SDL End----------------------
// 循环读取帧数据
while(av_read_frame(pFormatCtx, packet)>=0){
// 取出视频流
if(packet->stream_index==videoindex){
// 解码一帧视频数据:输入一个压缩编码的结构体AVPacket,输出一个解码后的结构体AVFrame
// 详细解析可以参考我的源码解析系列:https://blog.csdn.net/asd501823206/article/details/97013677
ret = avcodec_decode_video2(pCodecCtx, pFrame, &got_picture, packet);
if(ret < 0){
printf("Decode Error.\n");
return -1;
}
if(got_picture){
// sws_scale():处理图像数据,用于转换像素
sws_scale(img_convert_ctx, (const unsigned char* const*)pFrame->data, pFrame->linesize, 0, pCodecCtx->height,
pFrameYUV->data, pFrameYUV->linesize);
#if OUTPUT_YUV420P
// 根据YUV数据格式,分离Y、U、V数据
// 如果视频帧的宽和高分别为w和h,那么一帧YUV420P像素数据一共占用w*h*3/2 Byte的数据
// 其中前w*h Byte存储Y,接着的w*h*1/4 Byte存储U,最后w*h*1/4 Byte存储V
y_size=pCodecCtx->width*pCodecCtx->height;
fwrite(pFrameYUV->data[0],1,y_size,fp_yuv); //Y
fwrite(pFrameYUV->data[1],1,y_size/4,fp_yuv); //U
fwrite(pFrameYUV->data[2],1,y_size/4,fp_yuv); //V
#endif
//SDL---------------------------
#if 0
SDL_UpdateTexture( sdlTexture, NULL, pFrameYUV->data[0], pFrameYUV->linesize[0] );
#else
SDL_UpdateYUVTexture(sdlTexture, &sdlRect,
pFrameYUV->data[0], pFrameYUV->linesize[0],
pFrameYUV->data[1], pFrameYUV->linesize[1],
pFrameYUV->data[2], pFrameYUV->linesize[2]);
#endif
SDL_RenderClear( sdlRenderer );
SDL_RenderCopy( sdlRenderer, sdlTexture, NULL, &sdlRect);
SDL_RenderPresent( sdlRenderer );
//SDL End-----------------------
//Delay 40ms
SDL_Delay(40);
}
}
av_free_packet(packet);
}
//flush decoder
//FIX: Flush Frames remained in Codec
while (1) {
ret = avcodec_decode_video2(pCodecCtx, pFrame, &got_picture, packet);
if (ret < 0)
break;
if (!got_picture)
break;
sws_scale(img_convert_ctx, (const unsigned char* const*)pFrame->data, pFrame->linesize, 0, pCodecCtx->height,
pFrameYUV->data, pFrameYUV->linesize);
#if OUTPUT_YUV420P
int y_size=pCodecCtx->width*pCodecCtx->height;
fwrite(pFrameYUV->data[0],1,y_size,fp_yuv); //Y
fwrite(pFrameYUV->data[1],1,y_size/4,fp_yuv); //U
fwrite(pFrameYUV->data[2],1,y_size/4,fp_yuv); //V
#endif
//SDL---------------------------
// 设置纹理数据
SDL_UpdateTexture( sdlTexture, &sdlRect, pFrameYUV->data[0], pFrameYUV->linesize[0] );
// 清理渲染器
SDL_RenderClear( sdlRenderer );
// 将纹理数据copy到渲染器
SDL_RenderCopy( sdlRenderer, sdlTexture, NULL, &sdlRect);
// 显示
SDL_RenderPresent( sdlRenderer );
//SDL End-----------------------
//Delay 40ms
// 延时:一般为每秒25帧,即40ms一帧;不设置延时的话,循环会快速的将视频加载、解析、展示
SDL_Delay(40);
}
sws_freeContext(img_convert_ctx);
#if OUTPUT_YUV420P
fclose(fp_yuv);
#endif
SDL_Quit();
av_frame_free(&pFrameYUV);
av_frame_free(&pFrame);
avcodec_close(pCodecCtx);
avformat_close_input(&pFormatCtx);
return 0;
}