FFMPEG+SDL2.0流媒体开发3---简易MP4视频播放器,提取MP4的H264视频序列解码并且显示

简介: 简介 之前写了一遍提取MP4中的音视频并且解码,这一篇引入SDL2.0来显示解码后的视频序列 实现一个简易的 视频播放器。 我这里用的FFMPEG和SDL2.0都是最新版的 可能网上的资料不是很多,API接口也变了很多,不过大体的思路还是一样的。

简介

之前写了一遍提取MP4中的音视频并且解码,这一篇引入SDL2.0来显示解码后的视频序列 实现一个简易的 视频播放器。

我这里用的FFMPEG和SDL2.0都是最新版的 可能网上的资料不是很多,API接口也变了很多,不过大体的思路还是一样的。

分析几个FFMPEG函数

在这之前我们分析几个代码中可能引起疑问的FFMPEG几个函数的源代码,我已经尽我的能力添加了注释,因为实在没有文档可能有的地方也不是很详尽  不过大体还是能看懂的

av_image_alloc (分配图片缓冲区)

我们在FFMPEG中引用了此函数,下面列举的函数都是这个函数里所引用到的 我都 添加了注释  这里注意下面的

pointers 参数是一个指针数组  实际上他在初始化完毕之后会被赋值成连续的内存序列 具体看源代码

int av_image_alloc(uint8_t *pointers[4], int linesizes[4],
                   int w, int h, enum AVPixelFormat pix_fmt, int align)
{   
     //获取描述符
    const AVPixFmtDescriptor *desc = av_pix_fmt_desc_get(pix_fmt);  
	//
    int i, ret;
    uint8_t *buf;
    //如果不存在描述符那么返回错误
    if (!desc)
        return AVERROR(EINVAL);
     //检测图像宽度 高度
    if ((ret = av_image_check_size(w, h, 0, NULL)) < 0)
        return ret;
    //填充line sizes
    if ((ret = av_image_fill_linesizes(linesizes, pix_fmt, align>7 ? FFALIGN(w, 8) : w)) < 0)
        return ret;
     
	 //初始化0
    for (i = 0; i < 4; i++)
        linesizes[i] = FFALIGN(linesizes[i], align);
    //如果计算的缓冲区尺寸<0
    if ((ret = av_image_fill_pointers(pointers, pix_fmt, h, NULL, linesizes)) < 0)
        return ret;  
    //如果失败 重新分配buf
    buf = av_malloc(ret + align);
    if (!buf)
        return AVERROR(ENOMEM);   
		//再次调用 分配连续缓冲区  赋值给 pointers
    if ((ret = av_image_fill_pointers(pointers, pix_fmt, h, buf, linesizes)) < 0) { 
	    //如果分配失败那么释放 缓冲区
        av_free(buf);
        return ret;
    }  
	//检测像素描述符 AV_PIX_FMT_FLAG_PAL 或AV_PIX_FMT_FLAG_PSEUDOPAL 
	//Pixel format has a palette in data[1], values are indexes in this palette.
	/**
		The pixel format is "pseudo-paletted". This means that FFmpeg treats it as
		* paletted internally, but the palette is generated by the decoder and is not
		* stored in the file. *
	*/
    if (desc->flags & AV_PIX_FMT_FLAG_PAL || desc->flags & AV_PIX_FMT_FLAG_PSEUDOPAL) 
	     //设置系统调色板
        avpriv_set_systematic_pal2((uint32_t*)pointers[1], pix_fmt);

    return ret;
}

avpriv_set_systematic_pal2(设置系统调色板)

//设置系统化调色板根据不同像素格式

int avpriv_set_systematic_pal2(uint32_t pal[256], enum AVPixelFormat pix_fmt)
{
    int i;

    for (i = 0; i < 256; i++) {
        int r, g, b;

        switch (pix_fmt) {
        case AV_PIX_FMT_RGB8:
            r = (i>>5    )*36;
            g = ((i>>2)&7)*36;
            b = (i&3     )*85;
            break;
        case AV_PIX_FMT_BGR8:
            b = (i>>6    )*85;
            g = ((i>>3)&7)*36;
            r = (i&7     )*36;
            break;
        case AV_PIX_FMT_RGB4_BYTE:
            r = (i>>3    )*255;
            g = ((i>>1)&3)*85;
            b = (i&1     )*255;
            break;
        case AV_PIX_FMT_BGR4_BYTE:
            b = (i>>3    )*255;
            g = ((i>>1)&3)*85;
            r = (i&1     )*255;
            break;
        case AV_PIX_FMT_GRAY8:
            r = b = g = i;
            break;
        default:
            return AVERROR(EINVAL);
        }
        pal[i] = b + (g << 8) + (r << 16) + (0xFFU << 24);
    }

    return 0;
}

av_image_fill_pointers(填充av_image_alloc传递的unsigned char** data和linesize)

//返回图像所需的大小 
//并且分配了连续缓冲区  将 data 拼接成一个内存连续的 序列
int av_image_fill_pointers(uint8_t *data[4], enum AVPixelFormat pix_fmt, int height,
                           uint8_t *ptr, const int linesizes[4])
{   
    int i, total_size, size[4] = { 0 }, has_plane[4] = { 0 };
    //获取描述符
    const AVPixFmtDescriptor *desc = av_pix_fmt_desc_get(pix_fmt); 
	//清空指针数组
    memset(data  , 0, sizeof(data[0])*4);
   //如果不存在描述符 返回错误
    if (!desc || desc->flags & AV_PIX_FMT_FLAG_HWACCEL)
        return AVERROR(EINVAL);
    //data[0]初始化为ptr
    data[0] = ptr; 
	//如果每行的像素 大于INT类型最大值 -1024/高度 返回
    if (linesizes[0] > (INT_MAX - 1024) / height)
        return AVERROR(EINVAL); 
    //初始化size[0]
    size[0] = linesizes[0] * height;
    //如果 描述符的标志是AV_PIX_FMT_FLAG_PAL或者AV_PIX_FMT_FLAG_PSEUDOPAL 那么表明调色板放在data[1]并且是 256 32位置 
    if (desc->flags & AV_PIX_FMT_FLAG_PAL ||
        desc->flags & AV_PIX_FMT_FLAG_PSEUDOPAL) 
	{
        size[0] = (size[0] + 3) & ~3;
        data[1] = ptr + size[0]; 
        return size[0] + 256 * 4;
    }
     /**
     * Parameters that describe how pixels are packed.
     * If the format has 2 or 4 components, then alpha is last.
     * If the format has 1 or 2 components, then luma is 0.
     * If the format has 3 or 4 components,
     * if the RGB flag is set then 0 is red, 1 is green and 2 is blue;
     * otherwise 0 is luma, 1 is chroma-U and 2 is chroma-V. 

     */
    for (i = 0; i < 4; i++)
        has_plane[desc->comp[i].plane] = 1;
	//下面是计算总的需要的缓冲区大小
    total_size = size[0];
    for (i = 1; i < 4 && has_plane[i]; i++) {
        int h, s = (i == 1 || i == 2) ? desc->log2_chroma_h : 0;
        data[i] = data[i-1] + size[i-1];
        h = (height + (1 << s) - 1) >> s;
        if (linesizes[i] > INT_MAX / h)
            return AVERROR(EINVAL);
        size[i] = h * linesizes[i];
        if (total_size > INT_MAX - size[i])  
            return AVERROR(EINVAL);
        total_size += size[i];
    }
   //返回总的缓冲区 大小
    return total_size;
}

av_image_fill_linesizes(填充行线宽)

//填充LineSize数组 ,linesize代表每一刚的线宽 像素为单位
int av_image_fill_linesizes(int linesizes[4], enum AVPixelFormat pix_fmt, int width)
{
    int i, ret;
	//获取格式描述符
    const AVPixFmtDescriptor *desc = av_pix_fmt_desc_get(pix_fmt);
    int max_step     [4];       /* max pixel step for each plane */
    int max_step_comp[4];       /* the component for each plane which has the max pixel step */
    //初始化指针数组 0
    memset(linesizes, 0, 4*sizeof(linesizes[0]));
    //如果不存在那么返回错误
    if (!desc || desc->flags & AV_PIX_FMT_FLAG_HWACCEL)
        return AVERROR(EINVAL);
    //下面的代码都是填充线宽的代码 
    av_image_fill_max_pixsteps(max_step, max_step_comp, desc);
    for (i = 0; i < 4; i++) { 
        if ((ret = image_get_linesize(width, i, max_step[i], max_step_comp[i], desc)) < 0)
            return ret;
        linesizes[i] = ret;
    }

    return 0;
}

例子 提取MP4文件的视频,并播放实现简易视频播放器

#include "stdafx.h"
/************************************************************************/
/* 利用分流器分流MP4文件音视频并进行解码输出  
Programmer小卫-USher 2014/12/17
/************************************************************************/
//打开
#define __STDC_FORMAT_MACROS
#ifdef _CPPRTTI 
extern "C"
{
#endif
#include "libavutil/imgutils.h"    //图像工具 
#include "libavutil/samplefmt.h"  // 音频样本格式
#include "libavutil/timestamp.h"  //时间戳工具可以 被用于调试和日志目的 
#include "libavformat/avformat.h" //Main libavformat public API header  包含了libavf I/O和   Demuxing  和Muxing 库   
#include "SDL.h"
#ifdef _CPPRTTI 
};
#endif

//音视频编码器上下文
static AVCodecContext *pVideoContext,*pAudioContext;
static FILE *fVideoFile,*fAudioFile;  //输出文件句柄
static AVStream *pStreamVideo,*pStreamAudio; //媒体流  
static unsigned char * videoDstData[4];  //视频数据 
static int videoLineSize[4]; // 
static int videoBufferSize; //视频缓冲区大小 
static AVFormatContext *pFormatCtx=NULL; //格式上下文
static AVFrame*pFrame=NULL ; //
static AVPacket pkt;  //解码媒体包
static int ret=0; //状态
static int gotFrame; //获取到的视频流
//音视频流的索引
static int videoStreamIndex,audioStreamIndex;
//解码媒体包
//SDL定义  
SDL_Window * pWindow = NULL;
SDL_Renderer *pRender = NULL;
SDL_Texture *pTexture = NULL;
SDL_Rect dstrect = {0,0,800,600};
int frame = 0;
int indexFrameVideo=0;
static int decode_packet(int* gotFrame, int param2)
{
	int ret  = 0 ;
	//解码数据大小
	int decodedSize=pkt.size ; 
	//初始化获取的数据帧为0
	*gotFrame=0;
	//如果是视频流那么 解包视频流  
	if(pkt.stream_index==videoStreamIndex)
	{  

		//解码数据到视频帧
		if((ret=avcodec_decode_video2(pVideoContext,pFrame,gotFrame,&pkt))<0)
		{  
			//解码视频帧失败
			return ret ;
		}
		indexFrameVideo++;		
		//copy 解压后的数据到我们分配的空间中
		if(*gotFrame)
		{
			//拷贝数据
			av_image_copy(videoDstData,videoLineSize, (const uint8_t **)(pFrame->data), pFrame->linesize,pVideoContext->pix_fmt, pVideoContext->width, pVideoContext->height);
			//写入数据到缓冲区
			//fwrite(videoDstData[0], 1, videoBufferSize, fVideoFile);
		    printf("输出当前第%d帧,大小:%d\n",indexFrameVideo,videoBufferSize); 
    	int n = SDL_BYTESPERPIXEL(pStreamVideo->codec->pix_fmt);
		
		//更新纹理

		SDL_UpdateTexture(pTexture, &dstrect, (const void*)videoDstData[0], videoLineSize[0]);

		//拷贝纹理到2D模块
		SDL_RenderCopy(pRender, pTexture,NULL, &dstrect);
		//延时 1000ms*1/25
		SDL_Delay(1000 * 1 / frame);
		//显示Render渲染曾
		SDL_RenderPresent(pRender);
		}else
		{
			printf("第%d帧,丢失\n",indexFrameVideo);
		}
	}
	//音频不管
	else if(pkt.stream_index==audioStreamIndex)
	{  
		///解码音频信息
// 		if ((ret = avcodec_decode_audio4(pAudioContext, pFrame, gotFrame, &pkt)) < 0)
// 			return ret;
// 		decodedSize = FFMIN(ret, pkt.size);
// 		//算出当前帧的大小
// 		size_t unpadded_linesize = pFrame->nb_samples * av_get_bytes_per_sample((AVSampleFormat)pFrame->format); 
// 		///写入数据到音频文件
// 		fwrite(pFrame->extended_data[0], 1, unpadded_linesize, fAudioFile);   
	} 
	//取消所有引用  并且重置frame字段
	av_frame_unref(pFrame);
	return decodedSize ;
}

	int Demuxing(int argc, char** argv)
	{
		if (argc < 4)
		{
			printf("Parameter Error!\n");
			return 0;
		}

		//注册所有混流器 过滤器
		av_register_all();
		//注册所有编码器
		avcodec_register_all();
		//媒体输入源头
		char*pInputFile = argv[1];
		//视频输出文件
		char*pOutputVideoFile = argv[3];
		//音频输出文件
		char*pOutputAudioFile = argv[2];
		//分配环境上下文
		pFormatCtx = avformat_alloc_context();
		//打开输入源  并且读取输入源的头部
		if (avformat_open_input(&pFormatCtx, pInputFile, NULL, NULL) < 0)
		{
			printf("Open Input Error!\n");
			return 0;
		}
		//获取流媒体信息
		if (avformat_find_stream_info(pFormatCtx, NULL) < 0)
		{
			printf("获取流媒体信息失败!\n");
			return 0;
		}
		//打印媒体信息
		av_dump_format(pFormatCtx, 0, pInputFile, 0);
		for (unsigned i = 0; i < pFormatCtx->nb_streams; i++)
		{
			AVStream *pStream = pFormatCtx->streams[i];
			AVMediaType mediaType = pStream->codec->codec_type;
			//提取不同的编解码器
			if (mediaType == AVMEDIA_TYPE_VIDEO)
			{
				videoStreamIndex = i;
				pVideoContext = pStream->codec;
				pStreamVideo = pStream;
				fVideoFile = fopen(pOutputVideoFile, "wb"); 
				frame = pVideoContext->framerate.num;
				if (!fVideoFile)
				{
					printf("con't open file!\n");
					goto end;
				}
				//计算解码后一帧图像的大小
				//int nsize = avpicture_get_size(PIX_FMT_YUV420P, 1280, 720);
				//分配计算初始化 图像缓冲区 调色板数据
				int ret = av_image_alloc(videoDstData, videoLineSize, pVideoContext->width, pVideoContext->height, pVideoContext->pix_fmt, 1);
				if (ret < 0)
				{
					printf("Alloc video buffer error!\n");
					goto end;
				}
				//avpicture_fill((AVPicture *)pFrame, videoDstData[0], PIX_FMT_YUV420P, 1280, 720);
				videoBufferSize = ret;
			}
			else if (mediaType == AVMEDIA_TYPE_AUDIO)
			{
				audioStreamIndex = i;
				pAudioContext = pStream->codec;
				pStreamAudio = pStream;
				fAudioFile = fopen(pOutputAudioFile, "wb");
				if (!fAudioFile)
				{
					printf("con't open file!\n");
					goto end;
				}
				//分配视频帧
				pFrame = av_frame_alloc();
				if (pFrame == NULL)
				{
					av_freep(&videoDstData[0]);
					printf("alloc audio frame error\n");
					goto end;
				}
			}
			AVCodec *dec;
			//根据编码器id查找编码器
			dec = avcodec_find_decoder(pStream->codec->codec_id);
			if (dec == NULL)
			{
				printf("查找编码器失败!\n");
				goto end;
			}
			if (avcodec_open2(pStream->codec, dec, nullptr) != 0)
			{
				printf("打开编码器失败!\n");
				goto end;
			}

		}
		av_init_packet(&pkt);
		pkt.data = NULL;
		pkt.size = 0;

		//读取媒体数据包  数据要大于等于0
		while (av_read_frame(pFormatCtx, &pkt) >= 0)
		{
			AVPacket oriPkt = pkt;
			do
			{
				//返回每个包解码的数据
				ret = decode_packet(&gotFrame, 0);
				if (ret < 0)
					break;
				//指针后移  空闲内存减少
				pkt.data += ret;
				pkt.size -= ret;
				//
			} while (pkt.size > 0);
			//释放之前分配的空间  读取完毕必须释放包
			av_free_packet(&oriPkt);
		}

	end:
		//关闭视频编码器
		avcodec_close(pVideoContext);
		//关闭音频编码器
		avcodec_close(pAudioContext);
		avformat_close_input(&pFormatCtx);
		fclose(fVideoFile);
		fclose(fAudioFile);
		//释放编码帧
		avcodec_free_frame(&pFrame);
		//释放视频数据区
		av_free(videoDstData[0]);  
		return 0;
	}

	int _tmain(int argc, char*argv[])
	{  
		SDL_Init(SDL_INIT_VIDEO);  
		//创建窗口
		pWindow = SDL_CreateWindow("YUV420P", 200, 100, 800, 600, 0);
		//启用硬件加速 
		pRender=SDL_CreateRenderer(pWindow, -1, 0);  
		dstrect.x = 0;
		dstrect.y = 0;
		dstrect.w = 1280;
		dstrect.h = 720;
		//创建一个纹理  设置可以Lock  YUV420P 格式 1280*720 
		pTexture = SDL_CreateTexture(pRender, SDL_PIXELFORMAT_IYUV, SDL_TEXTUREACCESS_STREAMING, 1280, 720);
		Demuxing(argc, argv);	
		//释放
		SDL_RenderClear(pRender);
		SDL_DestroyTexture(pTexture);
		SDL_DestroyRenderer(pRender);
		SDL_DestroyWindow(pWindow);
		SDL_Quit();
		return  0;
	}

代码运行界面





目录
相关文章
|
2天前
|
编解码 Windows
FFmpeg开发笔记(二十九)Windows环境给FFmpeg集成libxvid
XviD是开源MPEG-4视频编码器,与DivX相似但后者非开源。早期MP4常使用XviD或DivX编码,现已被H.264取代。在Windows上集成FFmpeg的XviD编解码库libxvid,需访问<https://labs.xvid.com/source/>下载源码,解压后在MSYS环境中配置、编译和安装。之后重新配置FFmpeg,启用libxvid并编译安装。详细步骤包括configure命令、make和make install。成功后,通过`ffmpeg -version`检查是否启用libxvid。更多音视频开发技术可参考《FFmpeg开发实战:从零基础到短视频上线》。
23 0
FFmpeg开发笔记(二十九)Windows环境给FFmpeg集成libxvid
|
3天前
|
编解码 Linux
FFmpeg开发笔记(二十八)Linux环境给FFmpeg集成libxvid
XviD是开源的MPEG-4视频编解码器,曾与DivX一起用于早期MP4视频编码,但现在已被H.264取代。要集成XviD到Linux上的FFmpeg,首先下载源码,解压后配置并编译安装libxvid。接着,在FFmpeg源码目录中,重新配置FFmpeg以启用libxvid,然后编译并安装。成功后,通过`ffmpeg -version`检查是否启用libxvid。详细步骤包括下载、解压libxvid,使用`configure`和`make`命令安装,以及更新FFmpeg配置并安装。
13 2
FFmpeg开发笔记(二十八)Linux环境给FFmpeg集成libxvid
|
5天前
|
Linux 开发工具
Linux下视频截取命令 使用【ffmpeg】使用
Linux下视频截取命令 使用【ffmpeg】使用
9 1
|
5天前
|
编解码 Linux 计算机视觉
python 调用ffmpeg使用usb摄像头录制视频,输出h264格式,自动获取摄像头的最佳帧率和最大画面尺寸
使用 Python 调用 FFmpeg 进行 USB 摄像头视频录制,需先确保安装 FFmpeg 和 Python 的 `subprocess` 模块。代码示例展示了如何自动获取摄像头的最佳帧率和最大分辨率,然后录制视频。首先通过 FFmpeg 列出摄像头格式获取信息,解析出帧率和分辨率,选择最优值。之后调用 FFmpeg 命令录制视频,设置帧率、分辨率等参数。注意 `/dev/video0` 是 Linux 的摄像头设备路径,Windows 系统需相应调整。代码中未直接实现自动获取最佳参数,通常需要借助其他库如 OpenCV。
|
8天前
|
移动开发 小程序 视频直播
FFmpeg开发笔记(二十七)解决APP无法访问ZLMediaKit的直播链接问题
本文讲述了在使用ZLMediaKit进行视频直播时,遇到移动端通过ExoPlayer和微信小程序播放HLS直播地址失败的问题。错误源于ZLMediaKit对HTTP地址的Cookie校验导致401无权限响应。通过修改ZLMediaKit源码,注释掉相关鉴权代码并重新编译安装,解决了此问题,使得ExoPlayer和小程序能成功播放HLS视频。详细解决方案及FFmpeg集成可参考《FFmpeg开发实战:从零基础到短视频上线》一书。
17 3
FFmpeg开发笔记(二十七)解决APP无法访问ZLMediaKit的直播链接问题
|
9天前
|
Web App开发 安全 Linux
FFmpeg开发笔记(二十六)Linux环境安装ZLMediaKit实现视频推流
《FFmpeg开发实战》书中介绍轻量级流媒体服务器MediaMTX,但其功能有限,不适合生产环境。推荐使用国产开源的ZLMediaKit,它支持多种流媒体协议和音视频编码标准。以下是华为欧拉系统下编译安装ZLMediaKit和FFmpeg的步骤,包括更新依赖、下载源码、配置、编译、安装以及启动MediaServer服务。此外,还提供了通过FFmpeg进行RTSP和RTMP推流,并使用VLC播放器拉流的示例。
22 3
FFmpeg开发笔记(二十六)Linux环境安装ZLMediaKit实现视频推流
|
10天前
|
编解码 Linux
FFmpeg开发笔记(二十五)Linux环境给FFmpeg集成libwebp
《FFmpeg开发实战》书中指导如何在Linux环境下为FFmpeg集成libwebp以支持WebP图片编解码。首先,从GitHub下载libwebp源码,解压后通过`libtoolize`,`autogen.sh`,`configure`,`make -j4`和`make install`步骤安装。接着,在FFmpeg源码目录中重新配置并添加`--enable-libwebp`选项,然后进行`make clean`,`make -j4`和`make install`以编译安装FFmpeg。最后,验证FFmpeg版本信息确认libwebp已启用。
17 1
FFmpeg开发笔记(二十五)Linux环境给FFmpeg集成libwebp
|
16天前
|
Linux 编解码 Python
FFmpeg开发笔记(二十四)Linux环境给FFmpeg集成AV1的编解码器
AV1是一种高效免费的视频编码标准,由AOM联盟制定,相比H.265压缩率提升约27%。各大流媒体平台倾向使用AV1。本文介绍了如何在Linux环境下为FFmpeg集成AV1编解码库libaom、libdav1d和libsvtav1。涉及下载源码、配置、编译和安装步骤,包括设置环境变量以启用这三个库。
37 3
FFmpeg开发笔记(二十四)Linux环境给FFmpeg集成AV1的编解码器
|
17天前
|
编解码 Linux iOS开发
FFmpeg开发笔记(二十三)使用OBS Studio开启RTMP直播推流
OBS(Open Broadcaster Software)是一款开源、跨平台的直播和和Linux。官网为<https://obsproject.com/>。要使用OBS进行直播,需执行四步:1) 下载并安装OBS Studio(<https://obsproject.com/download>),2) 启动流媒体服务器如MediaMTX,生成RTMP推流地址,3) 打开OBS Studio,设置直播服务为自定义RTMP服务器(127.0.0.1:1935/stream),调整视频分辨率,4) 添加视频来源并开始直播。同时,通过FFmpeg的拉流程序验证直播功能正常。
30 4
FFmpeg开发笔记(二十三)使用OBS Studio开启RTMP直播推流
|
22天前
FFmpeg开发笔记(二十二)FFmpeg中SAR与DAR的显示宽高比
《FFmpeg开发实战》书中指出,视频宽高处理需考虑采样宽高比(SAR),像素宽高比(PAR)和显示宽高比(DAR)。SAR对应AVCodecParameters的sample_aspect_ratio,PAR为width/height。当SAR的num与den不为1时,需计算DAR以正确显示视频。书中提供了转换公式和代码示例,通过SAR或DAR调整视频尺寸。在修正后的playsync2.c程序中,成功调整了meg.vob视频的比例,实现了正确的画面显示。
41 0
FFmpeg开发笔记(二十二)FFmpeg中SAR与DAR的显示宽高比