视频编解码学习四 边解码边播放视频

  上一节说到了播放本地的yuv文件,这节我们省去解码保存yuv文件,直接边解码边播放。流程还是同直接解码流程一样。

流程图

SDL2.0显示YUV的流程图:


image

源码

#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来计算,同时要考虑视频和音频直接的同步,这里先不做进一步研究了,之后的博客会给出延时时间的计算。

下载

https://github.com/samychen/SDL_FFmpeg_Tutorial

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,386评论 6 479
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,939评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,851评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,953评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,971评论 5 369
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,784评论 1 283
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,126评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,765评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,148评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,744评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,858评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,479评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,080评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,053评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,278评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,245评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,590评论 2 343

推荐阅读更多精彩内容