雷神simplest_ffmpeg_player解析(三)

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接: https://blog.csdn.net/asd501823206/article/details/96909633

写在前面

学习雷神的博客,向雷神致敬~

看了雷神的小学期视频课,在Github上下载了simplest_ffmpeg_player的代码,为代码加上了注释,作为留存。

2019.07.22

前置知识点
simplest_ffmpeg_player.cpp注释


链接及参考资料

《基于 FFmpeg + SDL 的视频播放器的制作》课程的视频
SDL视频显示
WSAStartup函数


知识点

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 <windows.h>
#include <winsock2.h>
#include <ws2tcpip.h>
#include <stdio.h>

// 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图像输出到屏幕上之后,它才能被我们的看见。这个输出的过程就是渲染。我们也可以调整这个“木箱模型”的参数,达到不同的渲染结果。比如说改变观察角度等等。


simple_ffmpeg_player.cpp
/**
 * 最简单的基于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 <stdio.h>

#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 <libavcodec/avcodec.h>
#include <libavformat/avformat.h>
#include <libswscale/swscale.h>
#include <SDL2/SDL.h>
#include <libavutil/imgutils.h>
#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; i<pFormatCtx->nb_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;
}


猜你喜欢

转载自blog.csdn.net/asd501823206/article/details/96909633