前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >SDL2库(2)-Android 端集成FFmpeg及简单的播放器

SDL2库(2)-Android 端集成FFmpeg及简单的播放器

作者头像
deep_sadness
发布2018-12-14 14:43:04
1.5K0
发布2018-12-14 14:43:04
举报
文章被收录于专栏:Flutter入门Flutter入门

image.png

项目位置 https://github.com/deepsadness/SDLCmakeDemo

将编译好的FFmpeg集成进来。

  1. 编译FFmpeg FFMpeg编译部分的内容可以看之前的文章
  2. CMakeList 编写
代码语言:javascript
复制
# 添加FFMpeg
set(FFMPEG_INCLUDE ${CMAKE_SOURCE_DIR}/libs/ffmpeg/include)
set(LINK_DIR ${CMAKE_SOURCE_DIR}/libs/${ANDROID_ABI})
include_directories(${FFMPEG_INCLUDE})

add_library(ffmpeg SHARED IMPORTED)
set_target_properties(ffmpeg PROPERTIES IMPORTED_LOCATION ${LINK_DIR}/libffmpeg.so)

#并把FFmpeg放到链接库内
target_link_libraries( # Specifies the target library.
        main
        SDL2
        GLESv1_CM
        GLESv2
        ffmpeg
        # Links the target library to the log library
        # included in the NDK.
        ${log-lib})
  1. 检查是否集成成功 修改native-lib.cpp 导入头文件

image.png

修改main方法,打印FFMpeg的编译信息

打印FFMpeg的编译信息.png

运行后,查看编译信息

屏幕快照 2018-11-13 上午11.59.17.png

说明我们集成成功了~~

FFmpeg+SDL2简单的播放器。

视频路径参数传递

简单的通过main方法来传递参数。 0. 修改java方法,给main函数传递参数

SDLActivity中getArguments.png

在SDLMain的Run方法中,会去将参数传递过去

image.png

1. 确定main方法传递过来的参数

SDL_android.c中对应的nativeRunMain方法.png

SDL_android.c中可以看到,我们传递的main方法中得到的第一个参数,都是app_process, 第二个开始才是我们的参数。 因为我们只传递一个参数,所以可以直接取到。

取到我们传递的video_path.png

FFmpeg+SDL2播放流程

FFmpeg+SDL2播放流程.png

SDL的运行流程

1. SDL_Init() 通过SDL_Init 我们传入的flag来初始化SDL的各个子系统。我们这里只是简单的视频播放,所以只初始化了video的部分。SDL当中还有其他的子系统。比如音频。

代码语言:javascript
复制
SDL_Init(SDL_INIT_VIDEO) 

2. SDL_CreateWindow()

  • 通过SDL_CreateWindow来创建一个SDL_window对象。
代码语言:javascript
复制
 //创建窗口  位置是中间。大小是0 ,SDL创建窗口的时候,大小都是0
    window = SDL_CreateWindow("SDL_Window", SDL_WINDOWPOS_UNDEFINED,
                              SDL_WINDOWPOS_UNDEFINED, pCodecCtx->width, pCodecCtx->height,
                              SDL_WINDOW_RESIZABLE|SDL_WINDOW_FULLSCREEN | SDL_WINDOW_OPENGL);

最后一个参数是flag.这样代表的意思是,可以重新获取尺寸的,全屏幕的,使用OPENGL的。

  • SDL_Window表示SDL显示的窗口。 这里其实在Android中,如Flag所示,是通过创建一个NativeWindow,创建了一个OpenGL Surface进行绘制。

3. SDL_CreateRenderer SDL_Renderer负责SDL渲染的相关方法。

代码语言:javascript
复制
//-1  表示使用默认的窗口id 0是这是flag
renderer = SDL_CreateRenderer(window, -1, 0);

后续的渲染循环,都需要用它来完成。

4.SDL_CreateTexture

  • 创建一个SDL_Texture
代码语言:javascript
复制
    SDL_Texture *texture = SDL_CreateTexture(renderer, SDL_PIXELFORMAT_YV12,
                                             SDL_TEXTUREACCESS_STREAMING, pCodecCtx->width,
                                             pCodecCtx->height);

需要制定像素的格式SDL_PIXELFORMAT_YV12,对应的就是YUV420P; 接收的频率,SDL_TEXTUREACCESS_STREAMING这个表示会被频繁刷新。

  • SDL_Texture,用来接收传入的数据。
FFmpeg的运行流程

FFmpeg运行的流程。 简单来说就是

  • 获取AVFormatContext,这个变量内包含了IO的相关数据 通过avformat_open_input方法获取
代码语言:javascript
复制
    //创建avformat
    AVFormatContext *pFormatCtx = avformat_alloc_context();
    ret = avformat_open_input(&pFormatCtx, video_path, NULL, NULL);
  • 获取AVCodecContext,这个变量内包含了编码器或者解码器的相关数据。 它的获取需要,先从AVFormatContext中取到对应的流,根据对应的流的信息得到对应的编码器或者解码器AVCodec。然后再来创建。
代码语言:javascript
复制
    // 先去找到video_stream,然后在找AVCodec
    //先检查一边
    ret = avformat_find_stream_info(pFormatCtx, NULL);
    if (ret < 0) {
        ALOGE("Can not find Stream info!!!");
        return avError(ret);
    }

    int video_stream = -1;
    //这里就是简单的直接去找视频流
    for (int i = 0; i < pFormatCtx->nb_streams; ++i) {
        if (pFormatCtx->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) {
            video_stream = i;
            break;
        }
    }

    if (video_stream == -1) {
        ALOGE("Can not find video stream!!!");
        return -1;
    }

    ALOGI("find video stream ,index = %d", video_stream);
    //创建AVCodecCtx
    //需要先去获得AVCodec
    AVCodec *pCodec = avcodec_find_decoder(pFormatCtx->streams[video_stream]->codecpar->codec_id);
    if (pCodec == NULL) {
        ALOGE("Can not find video decoder!!!");
        return -1;
    }
    //成功获取上下文。获取之后,需要对上下文的部分内容进行初始化
    AVCodecContext *pCodecCtx = avcodec_alloc_context3(pCodec);

    //将解码器的参数复制过去
    AVCodecParameters *codecParameters = pFormatCtx->streams[video_stream]->codecpar;
    ret = avcodec_parameters_to_context(pCodecCtx, pFormatCtx->streams[video_stream]->codecpar);
    if (ret < 0) {
        ALOGE("avcodec_parameters_from_context error!!");
        return avError(ret);
    }

    ret = avcodec_open2(pCodecCtx, pCodec, NULL);
  • 解码的时候,通过av_read_frame进行读取。 通过avcodec_send_packetavcodec_receive_frame不断进行编码和解码。 用AVPacket接收压缩的数据(编码后,解码前)。用AVFrame接收原始的YUV数据(编码前,解码后)
代码
代码语言:javascript
复制
extern "C"
//这里是直接定义了SDL的main方法吗
int main(int argc, char *argv[]) {

    // 打印ffmpeg信息
    const char *str = avcodec_configuration();
    ALOGI("avcodec_configuration: %s", str);

    char *video_path = argv[1];
    ALOGI("video_path  : %s", video_path);

    //开始ffmpeg注册的流程
    int ret = 0;

    //重定向log
    av_log_set_callback(syslog_print);

    //注册
    av_register_all();
    avcodec_register_all();

    //创建avformat
    AVFormatContext *pFormatCtx = avformat_alloc_context();
    ret = avformat_open_input(&pFormatCtx, video_path, NULL, NULL);

    if (ret < 0) {
        ALOGE("avformat open input failed!");
        return avError(ret);
    }

    //输出avformat
    av_dump_format(pFormatCtx, -1, video_path, 0);

    // 先去找到video_stream,然后在找AVCodec
    //先检查一边
    ret = avformat_find_stream_info(pFormatCtx, NULL);
    if (ret < 0) {
        ALOGE("Can not find Stream info!!!");
        return avError(ret);
    }

    int video_stream = -1;
    //这里就是简单的直接去找视频流
    for (int i = 0; i < pFormatCtx->nb_streams; ++i) {
        if (pFormatCtx->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) {
            video_stream = i;
            break;
        }
    }

    if (video_stream == -1) {
        ALOGE("Can not find video stream!!!");
        return -1;
    }

    ALOGI("find video stream ,index = %d", video_stream);
    //创建AVCodecCtx
    //需要先去获得AVCodec
    AVCodec *pCodec = avcodec_find_decoder(pFormatCtx->streams[video_stream]->codecpar->codec_id);
    if (pCodec == NULL) {
        ALOGE("Can not find video decoder!!!");
        return -1;
    }
    //成功获取上下文。获取之后,需要对上下文的部分内容进行初始化
    AVCodecContext *pCodecCtx = avcodec_alloc_context3(pCodec);

    //将解码器的参数复制过去
    AVCodecParameters *codecParameters = pFormatCtx->streams[video_stream]->codecpar;
    ret = avcodec_parameters_to_context(pCodecCtx, pFormatCtx->streams[video_stream]->codecpar);
    if (ret < 0) {
        ALOGE("avcodec_parameters_from_context error!!");
        return avError(ret);
    }

    AVDictionaryEntry *t = NULL;
    while ((t = av_dict_get(pFormatCtx->metadata, "", t, AV_DICT_IGNORE_SUFFIX))) {
        char *key = t->key;
        char *value = t->value;
        ALOGI("key = %s,value = %s", key, value);
    }

    int height = codecParameters->height;
    int width = codecParameters->width;
    ALOGI("width = %d,height = %d", width, height);

    //完成初始化的参数之后,就要打开解码器,准备解码啦!!
    ret = avcodec_open2(pCodecCtx, pCodec, NULL);
    if (ret < 0) {
        ALOGE("avcodec_open2 error!!");
        return avError(ret);
    }

    ALOGI("w = %d,h = %d", pCodecCtx->width, pCodecCtx->height);
    //解码,就对应了 解码器前的数据,压缩数据 AVPacket 解码后的数据 AVFrame 就是我们需要的YUV数据
    //先给AVFrame分配内存空间
    AVFrame *pFrameYUV = av_frame_alloc();
    //pCodecCtx->pix_fmt == AV_PIX_FMT_YUV420P??
    int buffer_size = av_image_get_buffer_size(AV_PIX_FMT_YUV420P, pCodecCtx->width,
                                               pCodecCtx->height, 1);
    uint8_t *buffers = (uint8_t *) av_malloc(buffer_size);
    //将buffers 的地址赋给 AVFrame
    av_image_fill_arrays(pFrameYUV->data, pFrameYUV->linesize, buffers, AV_PIX_FMT_YUV420P,
                         pCodecCtx->width, pCodecCtx->height, 1);


    //开始准备sdl的部分
    //SDL 四大要  window render texture  surface
    SDL_Window *window;
    SDL_Renderer *renderer;
    SDL_Event event;
    SDL_Rect sdlRect;
    SDL_Thread *video_tid;

    //初始化SDL
    if (SDL_Init(SDL_INIT_VIDEO) < 0) {
        ALOGE("Could not initialize SDL - %s", SDL_GetError());
        return 1;
    }

    //创建窗口  位置是中间。大小是0 ,SDL创建窗口的时候,大小都是0
    window = SDL_CreateWindow("SDL_Window", SDL_WINDOWPOS_UNDEFINED,
                              SDL_WINDOWPOS_UNDEFINED, pCodecCtx->width, pCodecCtx->height,
                              SDL_WINDOW_RESIZABLE | SDL_WINDOW_FULLSCREEN | SDL_WINDOW_OPENGL);
    if (!window) {
        ALOGE("SDL:could not set video mode -exiting!!!\n");
        return -1;
    }
    //创建Renderer -1 表示使用默认的窗口 后面一个是Renderer的方式,0的话,应该就是未指定把???
    renderer = SDL_CreateRenderer(window, -1, 0);

    //这里的YU12 对应YUV420P ,SDL_TEXTUREACCESS_STREAMING 是表示texture 是不断被刷新的。
    SDL_Texture *texture = SDL_CreateTexture(renderer, SDL_PIXELFORMAT_YV12,
                                             SDL_TEXTUREACCESS_STREAMING, pCodecCtx->width,
                                             pCodecCtx->height);


    // 设置显示的大小
    sdlRect.x = 0;
    sdlRect.y = 0;
    sdlRect.w = pCodecCtx->width;
    sdlRect.h = pCodecCtx->height;

    //准备好了Window 开始准备解码的数据
    AVPacket *packet = (AVPacket *) av_malloc(sizeof(AVPacket));

//    video_tid
    int yuv_width = pCodecCtx->width * pCodecCtx->height;
    av_new_packet(packet, yuv_width);

    //当你需要对齐进行缩放和转化的时候,需要先申请一个SwsContext
    SwsContext *img_convert = sws_getContext(pCodecCtx->width, pCodecCtx->height,
                                             pCodecCtx->pix_fmt,
                                             pCodecCtx->width,
                                             pCodecCtx->height, AV_PIX_FMT_YUV420P, SWS_BICUBIC,
                                             NULL, NULL, NULL);

    while (av_read_frame(pFormatCtx, packet) >= 0) {
        if (packet->stream_index == video_stream) {
            //送入解码器
            int gop = avcodec_send_packet(pCodecCtx, packet);
            //如果成功获取一帧的数据
            if (gop == 0) {
                //使用pFrame接受数据
                ret = avcodec_receive_frame(pCodecCtx, pFrameYUV);
                if (ret == 0) {
                    //进行缩放。这里可以用libyuv进行转换
                    sws_scale(img_convert, reinterpret_cast<const uint8_t *const *>(pFrameYUV
                                      ->data), pFrameYUV->linesize, 0,
                              pCodecCtx->height,
                              pFrameYUV->data, pFrameYUV->linesize);
                    //应为是YUV,所以调用UpdateYUV方法,分别将YUV填充进去
                    SDL_UpdateYUVTexture(texture, &sdlRect,
                                         pFrameYUV
                                                 ->data[0], pFrameYUV->linesize[0],
                                         pFrameYUV->data[1], pFrameYUV->linesize[1],
                                         pFrameYUV->data[2], pFrameYUV->linesize[2]);

                    //清空数据
                    SDL_RenderClear(renderer);
                    //复制数据
                    SDL_RenderCopy(renderer, texture, &sdlRect, &sdlRect
                    );
                    //渲染到屏幕
                    SDL_RenderPresent(renderer);
                    //延迟40 25 fps??? Android端使用的话,就会卡顿
//                    SDL_Delay(40);
                } else if (ret == AVERROR(EAGAIN)) {
                    ALOGE("%s", "Frame is not available right, please try another input");
                } else if (ret == AVERROR_EOF) {
                    ALOGE("%s", "the decoder has been fully flushed");
                } else if (ret == AVERROR(EINVAL)) {
                    ALOGE("%s", "codec not opened, or it is an encoder");
                } else {
                    ALOGI("%s", "legitimate decoding errors");
                }
            }
        }

        //读完,再次释放这个pack,重新去读
        av_packet_unref(packet);
        
        //每一帧,去相应一次对应的SDL事件
        if (SDL_PollEvent(&event)) {
            SDL_bool needToQuit = SDL_FALSE;
            switch (event.type) {
                case SDL_QUIT:
                case SDL_KEYDOWN:
                    needToQuit = SDL_TRUE;
                    break;
                default:
                    break;
            }

            if (needToQuit) {
                break;
            }
        }
    }

    //SDL资源释放
    SDL_DestroyTexture(texture);
    SDL_DestroyRenderer(renderer);
    SDL_DestroyWindow(window);
    SDL_Quit();

    //FFmpeg资源释放
    sws_freeContext(img_convert);
    av_free(buffers);
    av_free(pFrameYUV);
    avcodec_parameters_free(&pFormatCtx
            ->streams[video_stream]->codecpar);
    avcodec_close(pCodecCtx);
    avformat_close_input(&pFormatCtx);

    return 0;
}
疑问

需要注意的是:在Android上不能使用SDL_Delay(); 在其他平台上视乎是要使用SDL_Delay(40);才能保持帧率,但是Android上,好像不能使用?这个是为什么?

参考

最简单的基于FFMPEG+SDL的视频播放器 ver2 (采用SDL2.0) FFmpeg编程开发笔记 —— Android FFmpeg + SDL2.0简易播放器实现

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2018.11.14 ,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 将编译好的FFmpeg集成进来。
  • FFmpeg+SDL2简单的播放器。
    • 视频路径参数传递
      • FFmpeg+SDL2播放流程
        • 参考
        领券
        问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档