【ffmpeg】ffmpeg+SDL实现播放器

ffmpegAPI+SDL实现一个播放器

  • 准备
  • Demon演示
  • 流程图
    • 播放器解码的流程图
    • SDL显示YUV图像的流程图
  • 播放器实现
    • FFmpeg组件初始化
      • 核心代码:
      • code解析
    • SDL组件初始化
      • 核心代码:
      • code解析
    • 播放器解码+SDL渲染YUV
      • 核心代码
      • code解析
  • 项目工程下载
  • 参考资料

准备

  1. MinGW 64-bit
  2. ffmpeg version4.2.2(这个版本和雷神的代码会有很多不同)
  3. SD2

Demon演示

流程图

播放器解码的流程图

【ffmpeg】ffmpeg+SDL实现播放器_第1张图片

SDL显示YUV图像的流程图

【ffmpeg】ffmpeg+SDL实现播放器_第2张图片

播放器实现

FFmpeg组件初始化

核心代码:

AVFormatContext *pFormatCtx{};
AVCodecParameters *pCodeParameters{};
const AVCodec *pCodec {};
AVCodecContext *pCodecCtx {};
AVFrame *pFrame {};
AVPacket packet {};

code解析

  1. AVFormatContext:声明格式化I/O的上下文
  2. AVCodecParameters:这个结构体用来描述编码流的属性
  3. AVCodec:解码器,这个是ffmpeg非常核心的模块,后续会做源码解读
  4. AVCodecContext:解码器上下文
  5. AVFrame:该结构描述解码(原始)音频或视频数据,AVFrame通常分配一次,然后多次重复使用以保存不同的数据(例如,在本次demon单个AVFrame保存从解码器接受的帧
  6. AVPacket:该结构存储压缩数据。它通常由解复用器导出,然后作为输入传递到解码器,或者作为输出从编码器接受然后传递到多路复用器;即存储解码前的帧

SDL组件初始化

核心代码:

SDL_Rect rect;
SDL_Window *window{};
SDL_Renderer *renderer{};
SDL_Texture *texture{};
Uint32 pixformat;
SDL_Thread *timer_thread{};
SDL_Event event;

code解析

  1. SDL_Rect:矩形,原点位于左上角(整数),后续要渲染的显示面积
  2. SDL_Window:用于标识窗口的类型 SDL窗口
  3. SDL_Renderer:渲染:表示渲染状态的结构体
  4. SDL_Texture:纹理:像素数据的高效驱动程序特定表示
  5. Uint32 pixformat:视频格式:这里设置为平面模式:Y+U+V(3个平面)
  6. SDL_Thread: SDL线程
  7. SDL_Event :SDL一般事件结构

播放器解码+SDL渲染YUV

核心代码

// 视频分辨率
int vWidth = s_vWidth;
int vHeight = s_vHeight;

// 窗口的分辨率
int sdlWidth = s_vWidth;
int sdlHeight = s_vHeight;
if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO | SDL_INIT_TIMER)) {
    cout << "initialize SDL failed" << endl;
    return ret;
}
if (avformat_open_input(&pFormatCtx, file, 0, nullptr) != 0) {
    cout << "open the video file failed!" << endl;
    return 0;
}
videostream = av_find_best_stream(pFormatCtx, AVMEDIA_TYPE_VIDEO, -1, -1,
                                  nullptr, 0);
if (videostream == -1) {
    cout << "open a video stream failed!" << endl;
    return 0;
}
pCodec = avcodec_find_decoder(
             pFormatCtx->streams[videostream]->codecpar->codec_id);
if (pCodec == nullptr) {
    cout << "find a codec failed!" << endl;
    return 0;
}
pCodecCtx = avcodec_alloc_context3(pCodec);
if (avcodec_parameters_to_context(pCodecCtx,
                                  pFormatCtx->streams[videostream]->codecpar) != 0) {
    cout << "copy codec context failed!" << endl;
    return 0;
}
if (avcodec_open2(pCodecCtx, pCodec, nullptr) < 0) {
    cout << "open the decoder failed!" << endl;
    return 0;
}
pFrame = av_frame_alloc();

// 获得显示的视频画面的长度与宽度
vWidth = pCodecCtx->width;
vHeight = pCodecCtx->height;

// 创建窗口
window = SDL_CreateWindow("SDL PLAYER",
                          SDL_WINDOWPOS_UNDEFINED,
                          SDL_WINDOWPOS_UNDEFINED,
                          vWidth, vHeight,
                          SDL_WINDOW_OPENGL | SDL_WINDOW_RESIZABLE);

if (!window) {
    cout << "create window failed!" << endl;
    return 0;
}

// 创建render
renderer = SDL_CreateRenderer(window, -1, 0);

if (!renderer) {
    cout << "create renderer failed!" << endl;
    return 0;
}

// 视频格式设置为SDL_PIXELFORMAT_IYUV
pixformat = SDL_PIXELFORMAT_IYUV;
// 创建纹理
texture = SDL_CreateTexture(renderer,
                            pixformat,
                            SDL_TEXTUREACCESS_STREAMING,
                            vWidth,
                            vHeight);

// 创建SDL线程
timer_thread = SDL_CreateThread(refresh_video_timer, nullptr, nullptr);

// 主循环
while (1) {
    // 无限期等待下一个可用事件
    SDL_WaitEvent(&event);

    if (event.type == REFRESH_EVENT) {// 画面刷新事件
        // 每次只需要读取一个packet
        while (1) {
            // 如果已经读取完所有的帧,则退出
            if (av_read_frame(pFormatCtx, &packet) < 0) {
                s_thread_exit = 1;
            }

            // 读取的是视频帧就退出
            if (packet.stream_index == videostream) {
                break;
            }
        }

        // 将一个packet进行解码操作
        avcodec_send_packet(pCodecCtx, &packet);

        // 将frame进行读取
        while (avcodec_receive_frame(pCodecCtx, pFrame) == 0) {
            // 刷新纹理
            SDL_UpdateYUVTexture(texture, NULL,
                                 pFrame->data[0], pFrame->linesize[0],
                                 pFrame->data[1], pFrame->linesize[1],
                                 pFrame->data[2], pFrame->linesize[2]);
            // 设置渲染目标的显示区域
            rect.x = 0;
            rect.y = 0;
            rect.w = pCodecCtx->width;
            rect.h = pCodecCtx->height;
            // 清空渲染器内容
            SDL_RenderClear(renderer);
            // 将纹理复制到渲染器
            SDL_RenderCopy(renderer, texture, NULL, &rect);
            // 渲染
            SDL_RenderPresent(renderer);
        }

        // 将packet内容清空
        av_packet_unref(&packet);
    } else if (event.type == SDL_WINDOWEVENT) { // 如果窗口出现了调整
        SDL_GetWindowSize(window, &sdlWidth, &sdlHeight);
    } else if (event.type == SDL_QUIT) { // 如果窗口被关闭
        s_thread_exit = 1;// 退出画面刷新线程
    } else if (event.type == QUIT_EVENT) { // 退出主线程循环
        break;
    }
}

// 释放所有内存
if (pFrame) {
    av_frame_free(&pFrame);
    pFrame = nullptr;
}

if (pCodecCtx) {
    avcodec_close(pCodecCtx);
    pCodecCtx = nullptr;
    pCodec = nullptr;
}

if (pFormatCtx) {
    avformat_close_input(&pFormatCtx);
    pFormatCtx = nullptr;
}

if (pCodeParameters) {
    avcodec_parameters_free(&pCodeParameters);
    pCodeParameters = nullptr;
}

if (texture) {
    SDL_DestroyTexture(texture);
    texture = nullptr;
}

if (renderer) {
    SDL_DestroyRenderer(renderer);
    renderer = nullptr;
}

if (window) {
    SDL_DestroyWindow(window);
    window = nullptr;
}

SDL_Quit();
return ret;

code解析

  1. SDL_Init:SDL初始化,这个功能自SDL 2.0.0起可用
  2. avformat_open_input:打开输入流并读取标题。编解码器未打开。必须使用avformat_close_input()关闭流;本Demon时读取视频文件
// 参数解析:
/**
 * @param ps   向用户提供的AVFormatContext的指针(avformat_alloc_context分配)。可能是
 *             NULL指针,这种情况下,此函数分配AVFormatContext并将其写入ps。
 *
 *             请注意,用户提供的AVFormatContext将在出现故障时释放。
 * @param url  要打开的流的URL
 * @param fmt  如果非空,则此参数强制使用特定的输入格式。否则,将自动检测格式。
 *
 * @param options 充满AVFormatContext和解复用器专用选项的字典。返回时,此参数将被销毁并替换未包含
 *                未找到选项的dict。可以为空。
 *
 * @return 成功时为0,失败时为负平均值。
 *
 * @note   如果要使用自定义IO,请预先分配格式上下文并设置其pb字段。
 */
  1. av_find_best_stream:在文件中查找“最佳”流。根据各种启发式方法确定最佳流,因为它最有可能是用户期望的。如果解码器参数为非空,av_find_best_stream将找到流的编解码器的默认解码器;找不到解码器的流将会被忽略
// 参数解析
/**
 * @param ic                媒体文件句柄
 * @param type              流类型:视频、音频、字幕等。
 * @param wanted_stream_nb  用户请求的流号,或-1用于自动选择
 * @param related_stream    尝试查找与此流相关的流(例如,在同一程序中),如果没有,则为-1
 * @param decoder_ret       如果非空,则返回所选流的解码器
 * @param flags             标识;当前未定义任何
 * @return  成功情况下的非负流号,AVERROR_STREAM_NOT_FOUND如果找不到具有请求类型的流AVERROR_DECODER_NOT_FOUND如果找到流但没有解码器
 * @note    如果av_find_best_stream成功返回并且decoder_ret不是NULL,则*decoder_ret保证设置为有效的AVCodec
 */
  1. avcodec_find_decoder: 查找具有匹配编解码器ID的注册解码器
// 参数解析
/**
 * @param id 所请求解码器的AVCodecID
 * @return   找到返回一个解码器,否则返回NULL
 */
  1. avcodec_alloc_context3:分配AVCodecContext并将其字段设置为默认值。应使用avcodec_free_context()释放生成的结构。本Demon返回编解码器信息配置
// 参数解析

/**
 * @param codec 如果非空,则为给定编解码器分配私有数据并初始化默认值。然后使用不同的编解码器调用avcodec_open2()是非法的。
 *              如果为空,则特定于编解码器的默认值将不会初始化,这可能会导致次优的默认设置(这主要对编码器很重要,例如libx264)。
 *
 * @return 一个填充了默认值的AVCodecContext或者失败时为空
 */
  1. avcodec_parameters_to_context:**根据提供的编解码器参数中的值填充编解码器上下文。**将释放编解码器中具有par中相应字段的任何分配 字段,并将其替换为par中对应字段的副本。不会触摸编解码器中没有par中的对应字段。
// 参数解析
/**
 * @return 成功时返回>=0,失败时为负平均错误代码
 */
  1. avcodec_open2:初始化AVCodecContext以使用给定的AVCodec。在使用此函数之前,必须使用avcodec_alloc_context3()分配上下文。本Demon即启动解码器
// 参数解析
/**
 * 函数avcodec_find_decoder_by_name(), avcodec_find_encoder_by_name(), 函数
 * avcodec_find_decoder() and avcodec_find_encoder()提供了检索编解码器的简单方法
 *
 * @warning 此函数不是线程安全的
 *
 * @note 在使用解码例程(如avcodec_receive_frame())之前,始终调用此函数
 *
 * @code
 * av_dict_set(&opts, "b", "2.5M", 0);
 * codec = avcodec_find_decoder(AV_CODEC_ID_H264);
 * if (!codec)
 *     exit(1);
 *
 * context = avcodec_alloc_context3(codec);
 *
 * if (avcodec_open2(context, codec, opts) < 0)
 *     exit(1);
 * @endcode
 *
 * @param avctx 要初始化的上下文
 * @param codec 打开此上下文的编解码器。如果先前已将非空编解码器传递给avcodec_alloc_context3(),
 *              或对于此上下文,则此参数必须为空或等于先前传递的编解码器。
 * @param options 一个包含AVCodecContext和编解码器专用选项的字典。返回时,此对象将填充未找到的选项。
 *
 * @return 成功时为0,错误时为负值。
 * @see avcodec_alloc_context3(), avcodec_find_decoder(), avcodec_find_encoder(),
 *      av_dict_set(), av_opt_find().
 */
  1. av_frame_alloc: 分配AVFrame并将其字段设置为默认值。 必须使用av_frame_free()释放生成的结构。本Demon初始化解码的帧
// 参数解析
/**
 * @return 一个填充了默认数据的AVFrame或失败了返回NULL
 *
 * @note 这仅分配AVFrame本身,而不是数据缓冲区。必须通过其他方式进行分配,例如使用av_frame_get_buffer()或手动分配。
 */

项目工程下载

项目下载链接

参考资料

【1】100行代码实现最简单的基于FFMPEG+SDL的视频播放器(SDL1.x)
【2】ffmpeg官方文档

你可能感兴趣的:(#,FFmpeg基础,ffmpeg,sdl,播放器,sdl2)