上一节说到了播放本地的yuv文件,这节我们省去解码保存yuv文件,直接边解码边播放。流程还是同直接解码流程一样。
SDL2.0显示YUV的流程图:
源码
#include <jni.h>
#include <android/log.h>
#define LOG_I(...) __android_log_print(ANDROID_LOG_ERROR , "main", __VA_ARGS__)
#include "SDL.h"
#include "SDL_log.h"
#include "SDL_main.h"
////avcodec:编解码(最重要的库)
//#include "libavcodec/avcodec.h"
////avformat:封装格式处理
//#include "libavformat/avformat.h"
////avutil:工具库(大部分库都需要这个库的支持)
//#include "libavutil/imgutils.h"
////swscale:视频像素数据格式转换
//#include "libswscale/swscale.h"
////导入音频采样数据格式转换库
//#include "libswresample/swresample.h"
extern "C" {
#include "libavcodec/avcodec.h"
#include "libavformat/avformat.h"
#include "libavutil/imgutils.h"
#include "libswscale/swscale.h"
}
int main(int argc, char *argv[]) {
const char *cinputFilePath = "/storage/emulated/0/Test.mov";
//第一步:注册所有组件
av_register_all();
//支持网络流输入
avformat_network_init();
//第二步:打开视频输入文件
//参数一:封装格式上下文->AVFormatContext->包含了视频信息(视频格式、大小等等...)
AVFormatContext *pFormatCtx = avformat_alloc_context();
//参数二:打开文件(入口文件)->url
int avformat_open_result = avformat_open_input(&pFormatCtx, cinputFilePath, NULL, NULL);
if (avformat_open_result != 0) {
//获取异常信息
char *error_info;
av_strerror(avformat_open_result, error_info, 1024);
__android_log_print(ANDROID_LOG_INFO, "main", "异常信息:%s", error_info);
return 0;
}
//第三步:查找视频文件信息
//参数一:封装格式上下文->AVFormatContext
//参数二:配置
//返回值:0>=返回OK,否则失败
int avformat_find_stream_info_result = avformat_find_stream_info(pFormatCtx, NULL);
if (avformat_find_stream_info_result < 0) {
//获取失败
char *error_info;
av_strerror(avformat_find_stream_info_result, error_info, 1024);
__android_log_print(ANDROID_LOG_INFO, "main", "异常信息:%s", error_info);
return 0;
}
// Dump valid information onto standard error可忽略
av_dump_format(pFormatCtx, 0, cinputFilePath, false);
//第四步:查找解码器
//第一点:获取当前解码器是属于什么类型解码器->找到了视频流
//音频解码器、视频解码器、字幕解码器等等...
//获取视频解码器流引用->指针
int av_stream_index = -1;
for (int i = 0; i < pFormatCtx->nb_streams; ++i) {
//循环遍历每一流
//视频流、音频流、字幕流等等...
if (pFormatCtx->streams[i]->codec->codec_type == AVMEDIA_TYPE_VIDEO) {
//找到了
av_stream_index = i;
break;
}
}
if (av_stream_index == -1) {
__android_log_print(ANDROID_LOG_INFO, "main", "%s", "没有找到视频流");
return 0;
}
//第二点:根据视频流->查找到视频解码器上下文->视频压缩数据
AVCodecContext *avcodec_context = pFormatCtx->streams[av_stream_index]->codec;
//第三点:根据解码器上下文->获取解码器ID
AVCodec *avcodec = avcodec_find_decoder(avcodec_context->codec_id);
if (avcodec == NULL) {
__android_log_print(ANDROID_LOG_INFO, "main", "%s", "没有找到视频解码器");
return 0;
}
//第五步:打开解码器
int avcodec_open2_result = avcodec_open2(avcodec_context, avcodec, NULL);
if (avcodec_open2_result != 0) {
char *error_info;
av_strerror(avcodec_open2_result, error_info, 1024);
__android_log_print(ANDROID_LOG_INFO, "main", "异常信息:%s", error_info);
return 0;
}
//输出视频信息
//输出:文件格式
__android_log_print(ANDROID_LOG_INFO, "main", "文件格式:%s", pFormatCtx->iformat->name);
//输出:解码器名称
__android_log_print(ANDROID_LOG_INFO, "main", "解码器名称:%s", avcodec->name);
//第六步:循环读取视频帧,进行循环解码->输出YUV420P视频->格式:yuv格式
//读取帧数据换成到哪里->缓存到packet里面
AVPacket *av_packet = (AVPacket *) av_malloc(sizeof(AVPacket));
//输入->环境一帧数据->缓冲区->类似于一张图
AVFrame *av_frame_in = av_frame_alloc();
//输出->帧数据->视频像素数据格式->yuv420p
AVFrame *av_frame_out_yuv420p = av_frame_alloc();
//解码的状态类型(0:表示解码完毕,非0:表示正在解码)
int av_decode_result, current_frame_index = 0;
//只有指定了AVFrame的像素格式、画面大小才能真正分配内存
//缓冲区
//作用:计算音频/视频占用的字节数,开辟对应的内存空间
//参数一:缓冲区格式
//参数二:缓冲区宽度
//参数三:缓冲区高度
//参数四:字节对齐(设置通用1)
int image_size = av_image_get_buffer_size(AV_PIX_FMT_YUV420P, avcodec_context->width, avcodec_context->height,1);
//开辟缓存空间
uint8_t *frame_buffer_out = (uint8_t *)av_malloc(image_size);
//对开辟的缓存空间指定填充数据格式
//参数一:数据
//参数二:行数
//参数三:缓存区
//参数四:格式
//参数五:宽度
//参数六:高度
//参数七:字节对齐(设置通用1)
av_image_fill_arrays(av_frame_out_yuv420p->data, av_frame_out_yuv420p->linesize,frame_buffer_out,
AV_PIX_FMT_YUV420P,avcodec_context->width, avcodec_context->height,1);
//准备一个视频像素数据格式上下文
//参数一:输入帧数据宽
//参数二:输入帧数据高
//参数三:输入帧数据格式
//参数四:输出帧数据宽
//参数五:输出帧数据高
//参数六:输出帧数据格式->AV_PIX_FMT_YUV420P
//参数七:视频像素数据格式转换算法类型
//参数八:字节对齐类型(C/C++里面)->提高读取效率
SwsContext *sws_context = sws_getContext(avcodec_context->width,
avcodec_context->height,
avcodec_context->pix_fmt,
avcodec_context->width,
avcodec_context->height,
AV_PIX_FMT_YUV420P,
SWS_BICUBIC, NULL, NULL, NULL);
// 加载SDL
//第一步:初始化SDL多媒体框架->SDL_Init
if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO | SDL_INIT_TIMER) == -1) {
LOG_I("SDL_Init failed %s", SDL_GetError());
return 0;
}
LOG_I("SDL_Init Success!");
//第二步:初始化SDL窗口
//参数一:窗口名称->要求必需是UTF-8编码
//参数二:窗口在屏幕上面X坐标
//参数三:窗口在屏幕上面Y坐标
//参数四:窗口在屏幕上面宽
int width = 640;
//参数五:窗口在屏幕上面高
int height = 352;
//参数六:窗口状态(打开的状态:SDL_WINDOW_OPENGL)
SDL_Window *sdl_window = SDL_CreateWindow("SDL播放器",
SDL_WINDOWPOS_CENTERED,
SDL_WINDOWPOS_CENTERED,
width,
height,
SDL_WINDOW_OPENGL);
if (sdl_window == NULL) {
LOG_I("窗口创建失败");
return 0;
}
//第三步:创建渲染器->渲染窗口(OpenGL ES)
//最新一期VIP课程
//参数一:渲染目标窗口
//参数二:从哪里开始渲染(-1:默认从第一个为止开始)
//参数三:渲染类型
//SDL_RENDERER_SOFTWARE:软件渲染
//...
SDL_Renderer *sdl_renderer = SDL_CreateRenderer(sdl_window, -1, 0);
//第四步:创建纹理
//参数一:纹理目标渲染器
//参数二:渲染格式
//参数三:绘制方式(SDL_TEXTUREACCESS_STREAMING:频繁绘制)
//参数四:纹理宽
//参数五:纹理高
SDL_Texture *sdl_texture = SDL_CreateTexture(sdl_renderer,
SDL_PIXELFORMAT_IYUV,
SDL_TEXTUREACCESS_STREAMING,
width,
height);
SDL_Rect sdl_rect;
sdl_rect.x = 0;
sdl_rect.y = 0;
sdl_rect.w = width;
sdl_rect.h = height;
//>=0:说明有数据,继续读取
//<0:说明读取完毕,结束
while (av_read_frame(pFormatCtx, av_packet) >= 0) {
//解码什么类型流(视频流、音频流、字幕流等等...)
if (av_packet->stream_index == av_stream_index) {
//扩展知识面(有更新)
//解码一帧视频流数据
//分析:avcodec_decode_video2函数
//参数一:解码器上下文
//参数二:一帧数据
//参数三:got_picture_ptr->是否正在解码(0:表示解码完毕,非0:表示正在解码)
//参数四:一帧压缩数据(对压缩数据进行解码操作)
//返回值:av_decode_result == 0表示解码一帧数据成功,否则失败
//av_decode_result = avcodec_decode_video2(avcodec_context,av_frame_in,&got_picture_ptr,av_packet);
//新的API操作
//发送一帧数据->接收一帧数据
//发送一帧数据
avcodec_send_packet(avcodec_context, av_packet);
//接收一帧数据->解码一帧
av_decode_result = avcodec_receive_frame(avcodec_context, av_frame_in);
//解码出来的每一帧数据成功之后,将每一帧数据保存为YUV420格式文件类型(.yuv文件格式)
if (av_decode_result == 0) {
//sws_scale:作用将视频像素数据格式->yuv420p格式
//输出.yuv文件->视频像素数据格式文件->输出到文件API
//参数一:视频像素数据格式->上下文
//参数二:输入数据
//参数三:输入画面每一行的大小
//参数四:输入画面每一行的要转码的开始位置
//参数五:每一帧数据高
//参数六:输出画面数据
//参数七:输出画面每一行的大小
sws_scale(sws_context,
(const uint8_t *const *) av_frame_in->data,
av_frame_in->linesize,
0,
avcodec_context->height,
av_frame_out_yuv420p->data,
av_frame_out_yuv420p->linesize);
// sart SDL //
//SDL渲染实现
//设置纹理数据
//参数一:目标纹理对象
//参数二:渲染区域(NULL:表示默认屏幕窗口宽高)
//参数三:视频像素数据
//参数四:帧画面宽
SDL_UpdateTexture(sdl_texture, NULL, av_frame_out_yuv420p->data[0],
av_frame_out_yuv420p->linesize[0]);
//先清空
SDL_RenderClear(sdl_renderer);
//再渲染
SDL_RenderCopy(sdl_renderer, sdl_texture, NULL, &sdl_rect);
//第七步:显示帧画面
SDL_RenderPresent(sdl_renderer);
//第八步:延时渲染(没渲染一帧间隔时间)
SDL_Delay(20);
// end SDL //
current_frame_index++;
__android_log_print(ANDROID_LOG_INFO, "main", "当前遍历第%d帧", current_frame_index);
}
}
}
//第七步:关闭解码组件->释放内存
SDL_DestroyTexture(sdl_texture);
SDL_DestroyRenderer(sdl_renderer);
//第十步:推出SDL程序
SDL_Quit();
av_packet_free(&av_packet);
av_frame_free(&av_frame_in);
av_frame_free(&av_frame_out_yuv420p);
avcodec_close(avcodec_context);
avformat_free_context(pFormatCtx);
return 0;
}
FFmpeg在解码一帧之后转换像素数据格式,并没有立马进行渲染,这里延时了20毫秒,如果没有延时这20毫秒,视频一下子就可以播放完毕了。实际在播放视频时,SDL延时不能使用固定值,需要根据视频的pts来计算,同时要考虑视频和音频直接的同步,这里先不做进一步研究了,之后的博客会给出延时时间的计算。