美文网首页
SDL2库(2)-Android 端集成FFmpeg及简单的播放

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

作者: deep_sadness | 来源:发表于2018-11-14 14:15 被阅读74次
    image.png

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

    系列内容导读

    1. SDL2-移植Android Studio+CMakeList集成
    2. Android端FFmpeg +SDL2的简单播放器
    3. SDL2 Android端的简要分析(VideoSubSystem)
    4. SDL2 Android端的简要分析(AudioSubSystem)

    将编译好的FFmpeg集成进来。

    1. 编译FFmpeg
      FFMpeg编译部分的内容可以看之前的文章

    偶遇FFmpeg(番外)——FFmpeg花样编译入魔1之裁剪大小
    偶遇FFmpeg(番外)——FFmpeg花样编译入魔2之单个SO库和ndk15之后
    偶遇FFmpeg(三)——Android集成

    1. CMakeList 编写
    # 添加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当中还有其他的子系统。比如音频。

    SDL_Init(SDL_INIT_VIDEO) 
    

    2. SDL_CreateWindow()

    • 通过SDL_CreateWindow来创建一个SDL_window对象。
     //创建窗口  位置是中间。大小是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渲染的相关方法。

    //-1  表示使用默认的窗口id 0是这是flag
    renderer = SDL_CreateRenderer(window, -1, 0);
    

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

    4.SDL_CreateTexture

    • 创建一个SDL_Texture
        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方法获取
        //创建avformat
        AVFormatContext *pFormatCtx = avformat_alloc_context();
        ret = avformat_open_input(&pFormatCtx, video_path, NULL, NULL);
    
    • 获取AVCodecContext,这个变量内包含了编码器或者解码器的相关数据。
      它的获取需要,先从AVFormatContext中取到对应的流,根据对应的流的信息得到对应的编码器或者解码器AVCodec。然后再来创建。
        // 先去找到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数据(编码前,解码后)
    代码
    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简易播放器实现

    相关文章

      网友评论

          本文标题:SDL2库(2)-Android 端集成FFmpeg及简单的播放

          本文链接:https://www.haomeiwen.com/subject/afzrfqtx.html