本文为做者原创:http://www.javashuo.com/article/p-cgzrwfnq-gy.html,转载请注明出处html
基于FFmpeg和SDL实现的简易视频播放器,主要分为读取视频文件解码和调用SDL播放两大部分。
本实验仅实现最简单的视频播放流程,不考虑细节,不考虑音频。本实验主要参考以下两篇文章:
[1]. 最简单的基于FFMPEG+SDL的视频播放器ver2(采用SDL2.0)
[2]. An ffmpeg and SDL Tutorialgit
FFmpeg简易播放器系列文章以下:
[1]. FFmpeg简易播放器的实现-最简版
[2]. FFmpeg简易播放器的实现-视频播放
[3]. FFmpeg简易播放器的实现-音频播放
[4]. FFmpeg简易播放器的实现-音视频播放
[5]. FFmpeg简易播放器的实现-音视频同步github
下图引用自“雷霄骅,视音频编解码技术零基础学习方法”,因原图过小,看不太清楚,故从新制做了一张图片。
以下内容引用自“雷霄骅,视音频编解码技术零基础学习方法”:shell
解协议
将流媒体协议的数据,解析为标准的相应的封装格式数据。视音频在网络上传播的时候,经常采用各类流媒体协议,例如HTTP,RTMP,或是MMS等等。这些协议在传输视音频数据的同时,也会传输一些信令数据。这些信令数据包括对播放的控制(播放,暂停,中止),或者对网络状态的描述等。解协议的过程当中会去除掉信令数据而只保留视音频数据。例如,采用RTMP协议传输的数据,通过解协议操做后,输出FLV格式的数据。数组解封装
将输入的封装格式的数据,分离成为音频流压缩编码数据和视频流压缩编码数据。封装格式种类不少,例如MP4,MKV,RMVB,TS,FLV,AVI等等,它的做用就是将已经压缩编码的视频数据和音频数据按照必定的格式放到一块儿。例如,FLV格式的数据,通过解封装操做后,输出H.264编码的视频码流和AAC编码的音频码流。网络解码
将视频/音频压缩编码数据,解码成为非压缩的视频/音频原始数据。音频的压缩编码标准包含AAC,MP3,AC-3等等,视频的压缩编码标准则包含H.264,MPEG2,VC-1等等。解码是整个系统中最重要也是最复杂的一个环节。经过解码,压缩编码的视频数据输出成为非压缩的颜色数据,例如YUV420P,RGB等等;压缩编码的音频数据输出成为非压缩的音频抽样数据,例如PCM数据。数据结构音视频同步
根据解封装模块处理过程当中获取到的参数信息,同步解码出来的视频和音频数据,并将视频音频数据送至系统的显卡和声卡播放出来。ide
实验平台:openSUSE Leap 42.3
FFmpeg版本:4.1
SDL版本:2.0.9
FFmpeg开发环境搭建可参考“FFmpeg开发环境构建”函数
/***************************************************************** * ffplayer.c * * history: * 2018-11-27 - [lei] created file * * details: * A simple ffmpeg player. * * refrence: * 1. https://blog.csdn.net/leixiaohua1020/article/details/38868499 * 2. http://dranger.com/ffmpeg/ffmpegtutorial_all.html#tutorial01.html * 3. http://dranger.com/ffmpeg/ffmpegtutorial_all.html#tutorial02.html ******************************************************************/ #include <stdio.h> #include <libavcodec/avcodec.h> #include <libavformat/avformat.h> #include <libswscale/swscale.h> #include <SDL2/SDL.h> #include <SDL2/SDL_video.h> #include <SDL2/SDL_render.h> #include <SDL2/SDL_rect.h> int main(int argc, char *argv[]) { // Initalizing these to NULL prevents segfaults! AVFormatContext* p_fmt_ctx = NULL; AVCodecContext* p_codec_ctx = NULL; AVCodecParameters* p_codec_par = NULL; AVCodec* p_codec = NULL; AVFrame* p_frm_raw = NULL; // 帧,由包解码获得原始帧 AVFrame* p_frm_yuv = NULL; // 帧,由原始帧色彩转换获得 AVPacket* p_packet = NULL; // 包,从流中读出的一段数据 struct SwsContext* sws_ctx = NULL; int buf_size; uint8_t* buffer = NULL; int i; int v_idx; int ret; SDL_Window* screen; SDL_Renderer* sdl_renderer; SDL_Texture* sdl_texture; SDL_Rect sdl_rect; if (argc < 2) { printf("Please provide a movie file\n"); return -1; } // 初始化libavformat(全部格式),注册全部复用器/解复用器 // av_register_all(); // 已被申明为过期的,直接再也不使用便可 // A1. 打开视频文件:读取文件头,将文件格式信息存储在"fmt context"中 ret = avformat_open_input(&p_fmt_ctx, argv[1], NULL, NULL); if (ret != 0) { printf("avformat_open_input() failed\n"); return -1; } // A2. 搜索流信息:读取一段视频文件数据,尝试解码,将取到的流信息填入pFormatCtx->streams // p_fmt_ctx->streams是一个指针数组,数组大小是pFormatCtx->nb_streams ret = avformat_find_stream_info(p_fmt_ctx, NULL); if (ret < 0) { printf("avformat_find_stream_info() failed\n"); return -1; } // 将文件相关信息打印在标准错误设备上 av_dump_format(p_fmt_ctx, 0, argv[1], 0); // A3. 查找第一个视频流 v_idx = -1; for (i=0; i<p_fmt_ctx->nb_streams; i++) { if (p_fmt_ctx->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) { v_idx = i; printf("Find a video stream, index %d\n", v_idx); break; } } if (v_idx == -1) { printf("Cann't find a video stream\n"); return -1; } // A5. 为视频流构建解码器AVCodecContext // A5.1 获取解码器参数AVCodecParameters p_codec_par = p_fmt_ctx->streams[v_idx]->codecpar; // A5.2 获取解码器 p_codec = avcodec_find_decoder(p_codec_par->codec_id); if (p_codec == NULL) { printf("Cann't find codec!\n"); return -1; } // A5.3 构建解码器AVCodecContext // A5.3.1 p_codec_ctx初始化:分配结构体,使用p_codec初始化相应成员为默认值 p_codec_ctx = avcodec_alloc_context3(p_codec); // A5.3.2 p_codec_ctx初始化:p_codec_par ==> p_codec_ctx,初始化相应成员 ret = avcodec_parameters_to_context(p_codec_ctx, p_codec_par); if (ret < 0) { printf("avcodec_parameters_to_context() failed %d\n", ret); return -1; } // A5.3.3 p_codec_ctx初始化:使用p_codec初始化p_codec_ctx,初始化完成 ret = avcodec_open2(p_codec_ctx, p_codec, NULL); if (ret < 0) { printf("avcodec_open2() failed %d\n", ret); return -1; } // A6. 分配AVFrame // A6.1 分配AVFrame结构,注意并不分配data buffer(即AVFrame.*data[]) p_frm_raw = av_frame_alloc(); p_frm_yuv = av_frame_alloc(); // A6.2 为AVFrame.*data[]手工分配缓冲区,用于存储sws_scale()中目的帧视频数据 // p_frm_raw的data_buffer由av_read_frame()分配,所以不需手工分配 // p_frm_yuv的data_buffer无处分配,所以在此处手工分配 buf_size = av_image_get_buffer_size(AV_PIX_FMT_YUV420P, p_codec_ctx->width, p_codec_ctx->height, 1 ); // buffer将做为p_frm_yuv的视频数据缓冲区 buffer = (uint8_t *)av_malloc(buf_size); // 使用给定参数设定p_frm_yuv->data和p_frm_yuv->linesize av_image_fill_arrays(p_frm_yuv->data, // dst data[] p_frm_yuv->linesize, // dst linesize[] buffer, // src buffer AV_PIX_FMT_YUV420P, // pixel format p_codec_ctx->width, // width p_codec_ctx->height, // height 1 // align ); // A7. 初始化SWS context,用于后续图像转换 // 此处第6个参数使用的是FFmpeg中的像素格式,对比参考注释B4 // FFmpeg中的像素格式AV_PIX_FMT_YUV420P对应SDL中的像素格式SDL_PIXELFORMAT_IYUV // 若是解码后获得图像的不被SDL支持,不进行图像转换的话,SDL是没法正常显示图像的 // 若是解码后获得图像的能被SDL支持,则没必要进行图像转换 // 这里为了编码简便,统一转换为SDL支持的格式AV_PIX_FMT_YUV420P==>SDL_PIXELFORMAT_IYUV sws_ctx = sws_getContext(p_codec_ctx->width, // src width p_codec_ctx->height, // src height p_codec_ctx->pix_fmt, // src format p_codec_ctx->width, // dst width p_codec_ctx->height, // dst height AV_PIX_FMT_YUV420P, // dst format SWS_BICUBIC, // flags NULL, // src filter NULL, // dst filter NULL // param ); // B1. 初始化SDL子系统:缺省(事件处理、文件IO、线程)、视频、音频、定时器 if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO | SDL_INIT_TIMER)) { printf("SDL_Init() failed: %s\n", SDL_GetError()); return -1; } // B2. 建立SDL窗口,SDL 2.0支持多窗口 // SDL_Window即运行程序后弹出的视频窗口,同SDL 1.x中的SDL_Surface screen = SDL_CreateWindow("Simplest ffmpeg player's Window", SDL_WINDOWPOS_UNDEFINED,// 不关心窗口X坐标 SDL_WINDOWPOS_UNDEFINED,// 不关心窗口Y坐标 p_codec_ctx->width, p_codec_ctx->height, SDL_WINDOW_OPENGL ); if (screen == NULL) { printf("SDL_CreateWindow() failed: %s\n", SDL_GetError()); return -1; } // B3. 建立SDL_Renderer // SDL_Renderer:渲染器 sdl_renderer = SDL_CreateRenderer(screen, -1, 0); // B4. 建立SDL_Texture // 一个SDL_Texture对应一帧YUV数据,同SDL 1.x中的SDL_Overlay // 此处第2个参数使用的是SDL中的像素格式,对比参考注释A7 // FFmpeg中的像素格式AV_PIX_FMT_YUV420P对应SDL中的像素格式SDL_PIXELFORMAT_IYUV sdl_texture = SDL_CreateTexture(sdl_renderer, SDL_PIXELFORMAT_IYUV, SDL_TEXTUREACCESS_STREAMING, p_codec_ctx->width, p_codec_ctx->height); sdl_rect.x = 0; sdl_rect.y = 0; sdl_rect.w = p_codec_ctx->width; sdl_rect.h = p_codec_ctx->height; p_packet = (AVPacket *)av_malloc(sizeof(AVPacket)); // A8. 从视频文件中读取一个packet // packet多是视频帧、音频帧或其余数据,解码器只会解码视频帧或音频帧,非音视频数据并不会被 // 扔掉、从而能向解码器提供尽量多的信息 // 对于视频来讲,一个packet只包含一个frame // 对于音频来讲,如果帧长固定的格式则一个packet可包含整数个frame, // 如果帧长可变的格式则一个packet只包含一个frame while (av_read_frame(p_fmt_ctx, p_packet) == 0) { if (p_packet->stream_index == v_idx) // 仅处理视频帧 { // A9. 视频解码:packet ==> frame // A9.1 向解码器喂数据,一个packet多是一个视频帧或多个音频帧,此处音频帧已被上一句滤掉 ret = avcodec_send_packet(p_codec_ctx, p_packet); if (ret != 0) { printf("avcodec_send_packet() failed %d\n", ret); return -1; } // A9.2 接收解码器输出的数据,此处只处理视频帧,每次接收一个packet,将之解码获得一个frame ret = avcodec_receive_frame(p_codec_ctx, p_frm_raw); if (ret != 0) { printf("avcodec_receive_frame() failed %d\n", ret); return -1; } // A10. 图像转换:p_frm_raw->data ==> p_frm_yuv->data // 将源图像中一片连续的区域通过处理后更新到目标图像对应区域,处理的图像区域必须逐行连续 // plane: 如YUV有Y、U、V三个plane,RGB有R、G、B三个plane // slice: 图像中一片连续的行,必须是连续的,顺序由顶部到底部或由底部到顶部 // stride/pitch: 一行图像所占的字节数,Stride=BytesPerPixel*Width+Padding,注意对齐 // AVFrame.*data[]: 每一个数组元素指向对应plane // AVFrame.linesize[]: 每一个数组元素表示对应plane中一行图像所占的字节数 sws_scale(sws_ctx, // sws context (const uint8_t *const *)p_frm_raw->data, // src slice p_frm_raw->linesize, // src stride 0, // src slice y p_codec_ctx->height, // src slice height p_frm_yuv->data, // dst planes p_frm_yuv->linesize // dst strides ); // B5. 使用新的YUV像素数据更新SDL_Rect SDL_UpdateYUVTexture(sdl_texture, // sdl texture &sdl_rect, // sdl rect p_frm_yuv->data[0], // y plane p_frm_yuv->linesize[0], // y pitch p_frm_yuv->data[1], // u plane p_frm_yuv->linesize[1], // u pitch p_frm_yuv->data[2], // v plane p_frm_yuv->linesize[2] // v pitch ); // B6. 使用特定颜色清空当前渲染目标 SDL_RenderClear(sdl_renderer); // B7. 使用部分图像数据(texture)更新当前渲染目标 SDL_RenderCopy(sdl_renderer, // sdl renderer sdl_texture, // sdl texture NULL, // src rect, if NULL copy texture &sdl_rect // dst rect ); // B8. 执行渲染,更新屏幕显示 SDL_RenderPresent(sdl_renderer); // B9. 控制帧率为25FPS,此处不够准确,未考虑解码消耗的时间 SDL_Delay(40); } av_packet_unref(p_packet); } SDL_Quit(); sws_freeContext(sws_ctx); av_free(buffer); av_frame_free(&p_frm_yuv); av_frame_free(&p_frm_raw); avcodec_close(p_codec_ctx); avformat_close_input(&p_fmt_ctx); return 0; }
源码清单中涉及的一些概念简述以下:
container:
对应数据结构AVFormatContext
封装器,将流数据封装为指定格式的文件,文件格式如AVI、MP4等。
FFmpeg可识别五种流类型:视频video(v)、音频audio(a)、attachment(t)、数据data(d)、字幕subtitle。学习
codec:
对应数据结构AVCodec
编解码器。编码器将未压缩的原始图像或音频数据编码为压缩数据。解码器与之相反。
codec context:
对应数据结构AVCodecContext
编解码器上下文。此为很是重要的一个数据结构,后文分析。各API大量使用AVCodecContext来引用编解码器。
codec par:
对应数据结构AVCodecParameters
编解码器参数。新版本增长的字段。新版本建议使用AVStream->codepar替代AVStream->codec。
packet:
对应数据结构AVPacket
通过编码的数据。经过av_read_frame()从媒体文件中获取获得的一个packet可能包含多个(整数个)音频帧或单个
视频帧,或者其余类型的流数据。
frame:
对应数据结构AVFrame
解码后的原始数据。解码器将packet解码后生成frame。
plane:
如YUV有Y、U、V三个plane,RGB有R、G、B三个plane
slice:
图像中一片连续的行,必须是连续的,顺序由顶部到底部或由底部到顶部
stride/pitch:
一行图像所占的字节数,Stride = BytesPerPixel × Width,x字节对齐[待确认]
sdl window:
对应数据结构SDL_Window
播放视频时弹出的窗口。在SDL1.x版本中,只能够建立一个窗口。在SDL2.0版本中,能够建立多个窗口。
sdl texture:
对应数据结构SDL_Texture
一个SDL_Texture对应一帧解码后的图像数据。
sdl renderer:
对应数据结构SDL_Renderer
渲染器。将SDL_Texture渲染至SDL_Window。
sdl rect:
对应数据结构SDL_Rect
SDL_Rect用于肯定SDL_Texture显示的位置。一个SDL_Window上能够显示多个SDL_Rect。这样能够实现同一窗口的分屏显示。
流程比较简单,不画流程图了,简述以下:
media file --[decode]--> raw frame --[scale]--> yuv frame --[SDL]--> display media file ------------> p_frm_raw -----------> p_frm_yuv ---------> sdl_renderer
加上相关关键函数后,流程以下:
media_file ---[av_read_frame()]-----------> p_packet ---[avcodec_send_packet()]-----> decoder ---[avcodec_receive_frame()]---> p_frm_raw ---[sws_scale()]---------------> p_frm_yuv ---[SDL_UpdateYUVTexture()]----> display
初始化解码及显示环境。
调用av_read_frame()从输入文件中读取视频数据包。
调用avcodec_send_packet()和avcodec_receive_frame()对视频数据解码。
图像格式转换的目的,是为了解码后的视频帧能被SDL正常显示。由于FFmpeg解码后获得的图像格式不必定就能被SDL支持,这种状况下不做图像转换是没法正常显示的。
调用SDL相关函数将图像在屏幕上显示。
gcc -o ffplayer ffplayer.c -lavutil -lavformat -lavcodec -lavutil -lswscale -lSDL2
选用bigbuckbunny_480x272.h265测试文件,测试文件下载:bigbuckbunny_480x272.h265
运行测试命令:
./ffplayer bigbuckbunny_480x272.h265
[1] 雷霄骅,视音频编解码技术零基础学习方法
[2] 雷霄骅,FFmpeg源代码简单分析:常见结构体的初始化和销毁(AVFormatContext,AVFrame等)
[3] 雷霄骅,最简单的基于FFMPEG+SDL的视频播放器ver2(采用SDL2.0)
[4] Martin Bohme, An ffmpeg and SDL Tutorial, Tutorial 01: Making Screencaps
[5] Martin Bohme, An ffmpeg and SDL Tutorial, Tutorial 02: Outputting to the Screen
[6] YUV图像里的stride和plane的解释
[7] 图文详解YUV420数据格式
[8] YUV,https://zh.wikipedia.org/wiki/YUV
2018-11-23 V1.0 初稿 2018-11-29 V1.1 增长定时刷新线程,使解码帧率更加准确