2. 硬解码器选择与初始化

任务目标

编写一个FFmpeg命令行工具,自动探测当前系统可用的硬件解码器(如h264_cuvid、h264_qsv、h264_videotoolbox等),并优先选择第一个可用的硬解码器完成视频解码初始化。若所有硬解码器均不可用,则回退到软件解码器,并输出明确的初始化状态信息。

环境依赖

  • FFmpeg 4.x 或 5.x(需启用对应硬件解码模块,如 --enable-cuvid / --enable-libmfx / --enable-videotoolbox)
  • 支持 C99 的编译器(gcc/clang)
  • Linux/macOS/Windows(需对应平台硬件驱动)

核心实现

#include <libavcodec/avcodec.h>
#include <libavformat/avformat.h>
#include <stdio.h>
#include <string.h>

static const char *hw_decoders[] = {
    "h264_cuvid", "h264_qsv", "h264_videotoolbox",
    "hevc_cuvid", "hevc_qsv", "hevc_videotoolbox",
    NULL
};

static int try_open_decoder(AVCodecContext *ctx, const AVCodec *decoder,
                            AVCodecParameters *params, char *errbuf, int errbuf_size) {
    ctx->codec_id = decoder->id;
    ctx->codec_type = AVMEDIA_TYPE_VIDEO;
    if (avcodec_parameters_to_context(ctx, params) < 0) {
        av_strerror(AVERROR(EINVAL), errbuf, errbuf_size);
        return -1;
    }
    ctx->flags |= AV_CODEC_FLAG_LOW_DELAY;
    if (avcodec_open2(ctx, decoder, NULL) < 0) {
        av_strerror(AVERROR(EINVAL), errbuf, errbuf_size);
        return -1;
    }
    return 0;
}

int main(int argc, char **argv) {
    if (argc < 2) {
        fprintf(stderr, "Usage: %s <input_file>\n", argv[0]);
        return 1;
    }

    avformat_network_init();
    AVFormatContext *fmt_ctx = NULL;
    if (avformat_open_input(&fmt_ctx, argv[1], NULL, NULL) < 0) {
        fprintf(stderr, "Cannot open input file\n");
        return 1;
    }
    if (avformat_find_stream_info(fmt_ctx, NULL) < 0) {
        fprintf(stderr, "Cannot find stream info\n");
        avformat_close_input(&fmt_ctx);
        return 1;
    }

    int video_stream = av_find_best_stream(fmt_ctx, AVMEDIA_TYPE_VIDEO, -1, -1, NULL, 0);
    if (video_stream < 0) {
        fprintf(stderr, "No video stream found\n");
        avformat_close_input(&fmt_ctx);
        return 1;
    }

    AVCodecParameters *params = fmt_ctx->streams[video_stream]->codecpar;
    const AVCodec *decoder = NULL;
    AVCodecContext *dec_ctx = avcodec_alloc_context3(NULL);
    char errbuf[AV_ERROR_MAX_STRING_SIZE] = {0};
    const char *selected_name = NULL;

    // 1. 尝试硬件解码器
    for (int i = 0; hw_decoders[i] != NULL; i++) {
        decoder = avcodec_find_decoder_by_name(hw_decoders[i]);
        if (!decoder) continue;
        if (decoder->id != params->codec_id) continue;  // 仅匹配当前编码格式

        if (try_open_decoder(dec_ctx, decoder, params, errbuf, sizeof(errbuf)) == 0) {
            selected_name = hw_decoders[i];
            break;
        }
        avcodec_free_context(&dec_ctx);
        dec_ctx = avcodec_alloc_context3(NULL);
    }

    // 2. 回退到软件解码
    if (!selected_name) {
        decoder = avcodec_find_decoder(params->codec_id);
        if (!decoder) {
            fprintf(stderr, "No decoder found for codec_id %d\n", params->codec_id);
            avcodec_free_context(&dec_ctx);
            avformat_close_input(&fmt_ctx);
            return 1;
        }
        if (try_open_decoder(dec_ctx, decoder, params, errbuf, sizeof(errbuf)) < 0) {
            fprintf(stderr, "Software decoder init failed: %s\n", errbuf);
            avcodec_free_context(&dec_ctx);
            avformat_close_input(&fmt_ctx);
            return 1;
        }
        selected_name = decoder->name;
    }

    printf("Selected decoder: %s\n", selected_name);
    printf("Codec: %s, Width: %d, Height: %d, PixFmt: %d\n",
           avcodec_get_name(dec_ctx->codec_id),
           dec_ctx->width, dec_ctx->height, dec_ctx->pix_fmt);

    // 3. 验证:解码第一帧
    AVPacket *pkt = av_packet_alloc();
    AVFrame *frame = av_frame_alloc();
    int got_frame = 0;
    while (av_read_frame(fmt_ctx, pkt) >= 0) {
        if (pkt->stream_index == video_stream) {
            if (avcodec_send_packet(dec_ctx, pkt) == 0) {
                if (avcodec_receive_frame(dec_ctx, frame) == 0) {
                    got_frame = 1;
                    printf("First frame decoded: %dx%d, format=%d\n",
                           frame->width, frame->height, frame->format);
                    break;
                }
            }
        }
        av_packet_unref(pkt);
    }

    if (!got_frame) {
        fprintf(stderr, "Failed to decode any frame\n");
    }

    av_frame_free(&frame);
    av_packet_free(&pkt);
    avcodec_free_context(&dec_ctx);
    avformat_close_input(&fmt_ctx);
    avformat_network_deinit();
    return got_frame ? 0 : 1;
}

编译与运行

# 编译(需pkg-config配置FFmpeg路径)
gcc -o hw_select hw_select.c $(pkg-config --cflags --libs libavformat libavcodec libavutil)

# 运行(以H.264视频为例)
./hw_select input.mp4

验证方法

  • 成功标准:程序输出 Selected decoder: h264_cuvid(或对应平台硬解码器名称),并成功打印第一帧的宽高与像素格式,退出码为0。
  • 回退验证:在无硬件加速环境(如纯软件虚拟机)运行,应输出 Selected decoder: h264,表明自动回退到软件解码。
  • 错误路径:输入不存在的文件或非视频文件时,程序应打印明确错误信息并返回非0退出码。

关键说明

  • 硬解码器列表按优先级排列,当前仅匹配H.264/HEVC编码;如需支持其他编码格式,在 hw_decoders 数组中追加对应名称即可。
  • 初始化失败时通过 avcodec_open2 返回错误码,但FFmpeg对硬件解码器不可用通常返回 AVERROR(EINVAL) 或 AVERROR(ENOTSUP),此处统一按失败处理并尝试下一个候选。
  • 实际工程中应根据 dec_ctx->hw_device_ctx 配置硬件设备上下文(如CUDA/QSV设备),本例聚焦解码器选择与初始化流程,故省略设备创建环节。