嵌入式Linux下基于FFmpeg实现MP4解码为JPG图片(SSD202D交叉编译实战)

若该文为原创文章,转载请注明原文出处。

一、背景与目标

最近在基于SigmaStar SSD202D(ARM Cortex-A7)的嵌入式开发板上做一个视频抓图功能,需要将MP4视频文件解码成单帧图片(JPG格式)。板子跑的是Linux,资源有限,所以打算使用FFmpeg库来实现。

然而,直接在板子上编译FFmpeg不现实,必须在Ubuntu主机上交叉编译,生成ARM架构的动态库和可执行程序。

本文记录了从下载FFmpeg源码、配置交叉编译、解决各种依赖问题,到编写一个简单的C程序完成MP4解码并输出JPG的全过程,希望对类似项目的朋友有所帮助。

二、环境准备

  • 主机:Ubuntu 20.04 x86_64

  • 目标芯片:SigmaStar SSD202D(ARMv7-A,双核)

  • 交叉编译工具链gcc-arm-8.2-2018.08-x86_64-arm-linux-gnueabihf(GCC 8.2.1)

  • FFmpeg源码:经适配SSD202硬件解码的版本(来自DongshanPI仓库)

  • SDK :SigmaStar SSD202D完整SDK(包含mi_vdec.h等头文件和libmi_*.so硬件解码库)

三、下载FFmpeg源码

选用已针对SSD20x系列芯片做过适配的版本,仓库地址:

复制代码
https://github.com/aaron201912/ffmpeg.git

克隆到本地:

复制代码
git clone https://github.com/aaron201912/ffmpeg.git
cd ffmpeg/ffmpeg-4.1.3

该版本包含config_for_ssd20x.sh配置脚本,方便我们修改。

四、编写配置脚本

我们需要启用必要的组件:avcodecavformatswscaleswresample,并且要启用MJPEG编码器(为了输出JPG),同时禁用不必要的模块以减小体积。

最终脚本(config_for_ssd20x.sh)内容如下(注意:根据实际情况调整SDK路径参数):

复制代码
#!/bin/bash
if [ -e "$1" ]; then
    alkaid_path=$1
else
    alkaid_path=./../..
fi
echo "alkaid_path = ${alkaid_path}"

cflags+=" -I${alkaid_path}/project/release/include"
cflags+=" -I./../3rdparty/include"
cflags+=" -I./../3rdparty/include/sstar"
cflags+=" -DFFMPEG_LIBRARY_VERSION=... "
echo "cflags = ${cflags}"

ldflags+=" -L${alkaid_path}/project/release/nvr/i2m/common/glibc/8.2.1/mi_libs/dynamic"
ldflags+=" -L./../3rdparty/lib/8.2.1"
echo "ldflags = ${ldflags}"

./configure \
--prefix=host \
--enable-ffmpeg \
--disable-ffprobe \
--disable-ffplay \
--disable-symver \
--disable-doc \
--disable-htmlpages \
--disable-manpages \
--disable-podpages \
--disable-txtpages \
--disable-avresample \
--disable-avfilter \
--disable-avdevice \
--disable-postproc \
--enable-swscale \
--enable-swresample \
--enable-avcodec \
--enable-avformat \
--enable-small \
--enable-cross-compile \
--arch=arm \
--target-os=linux \
--cross-prefix=arm-linux-gnueabihf- \
--enable-shared \
--disable-gpl \
--enable-nonfree \
--disable-openssl \        # 禁用OpenSSL(不需要HTTPS)
--disable-zlib \           # 禁用zlib(不需要PNG输出)
--enable-pthreads \
--enable-error-resilience \
--enable-debug \
--enable-encoder=mjpeg \   # 启用MJPEG编码器
--disable-hwaccels \
--disable-muxers \
--disable-indevs \
--disable-outdevs \
--disable-devices \
--disable-filters \
--disable-decoder=ssh264_ss268 \
--disable-decoder=sshevc_ss268 \
--shlibdir=host/dynamic \
--libdir=host/static \
--pkg-config=pkg-config \
--extra-cflags="${cflags} -g -rdynamic -funwind-tables -ffunction-sections" \
--extra-ldflags="${ldflags}"

关键点

  • 指定了交叉编译工具链前缀 arm-linux-gnueabihf-

  • 显式启用 --enable-encoder=mjpeg,因为默认 --disable-encoders 会关掉所有编码器。

  • 禁用了 opensslzlib,避免因交叉编译环境中缺少这些库而导致配置失败。

  • 保留了硬解码相关路径(mi_vdec等),但此处我们只用软解,所以实际上硬解解码器被禁用了(--disable-decoder=ssh264_ss268)。


五、编译FFmpeg

执行配置(假定SDK根目录为/home/yifeng/dirik_ssd20x_sdk):

复制代码
cd ffmpeg-4.1.3
make distclean          # 清理之前残留
sh config_for_ssd20x.sh /home/yifeng/ssd20x_sdk

检查配置摘要中的Programs:行,确保包含ffmpeg(可选):

复制代码
grep "Programs:" config.log

然后编译安装:

复制代码
make -j4
make install

编译完成后,库文件位于host/dynamic/下,头文件在host/include/

六、遇到的典型问题及解决

1. OpenSSL依赖导致ffmpeg程序未被构建

  • 现象 :配置后Programs:为空,且没有生成ffmpeg可执行文件。

  • 原因 :脚本中--enable-openssl但交叉编译环境中缺少OpenSSL库,configure自动跳过程序构建。

  • 解决 :删除--enable-openssl,改用--disable-openssl

2. zlib缺失同样导致程序跳过

  • 解决 :添加--disable-zlib

3. MJPEG编码器找不到

  • 现象 :测试程序报错MJPEG encoder not found,然后段错误。

  • 原因 :配置中使用了--disable-encoders,导致所有编码器被禁用。

  • 解决 :改为--enable-encoder=mjpeg(同时去掉--disable-encoders)。

4. 链接时找不到libswresampleMI_*符号

  • 现象 :编译测试程序时,提示undefined reference to swr_allocMI_VDEC_*

  • 原因

    • 缺少-lswresample链接。

    • 由于FFmpeg编译时开启了硬解支持,libavcodec依赖SigmaStar的MI库(libmi_vdeclibmi_sys等)。

  • 解决 :链接时加上-lswresample和MI库路径及库名,同时添加-ldl(因为MI库依赖dlopen等)。

最终编译命令见下文。

七、编写解码程序:MP4转JPG

程序功能

使用FFmpeg API打开MP4文件,读取视频流,解码每一帧,然后通过MJPEG编码器将帧编码为JPG并保存为文件。

完整代码(mp4_to_jpg.c

复制代码
#include <stdio.h>
#include <libavcodec/avcodec.h>
#include <libavformat/avformat.h>
#include <libswscale/swscale.h>
#include <libavutil/imgutils.h>

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

    const char *input_file = argv[1];
    AVFormatContext *fmt_ctx = NULL;
    AVCodecContext *dec_ctx = NULL, *enc_ctx = NULL;
    AVCodec *dec_codec = NULL, *enc_codec = NULL;
    AVStream *video_stream = NULL;
    int video_stream_index = -1;
    AVPacket pkt, enc_pkt;
    AVFrame *frame = NULL, *yuv_frame = NULL;
    struct SwsContext *sws_ctx = NULL;
    int ret;
    int frame_count = 0;
    char out_filename[64];

    // 1. 打开输入文件
    if ((ret = avformat_open_input(&fmt_ctx, input_file, NULL, NULL)) < 0) {
        fprintf(stderr, "Cannot open input file\n");
        return -1;
    }

    // 2. 查找视频流信息
    if ((ret = avformat_find_stream_info(fmt_ctx, NULL)) < 0) {
        fprintf(stderr, "Cannot find stream info\n");
        goto end;
    }

    for (int i = 0; i < fmt_ctx->nb_streams; i++) {
        if (fmt_ctx->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) {
            video_stream_index = i;
            video_stream = fmt_ctx->streams[i];
            break;
        }
    }
    if (video_stream_index == -1) {
        fprintf(stderr, "No video stream found\n");
        goto end;
    }

    AVCodecParameters *codecpar = video_stream->codecpar;
    dec_codec = avcodec_find_decoder(codecpar->codec_id);
    if (!dec_codec) {
        fprintf(stderr, "Decoder not found\n");
        goto end;
    }

    // 3. 解码器上下文
    dec_ctx = avcodec_alloc_context3(dec_codec);
    avcodec_parameters_to_context(dec_ctx, codecpar);
    if ((ret = avcodec_open2(dec_ctx, dec_codec, NULL)) < 0) {
        fprintf(stderr, "Cannot open decoder\n");
        goto end;
    }

    int width = dec_ctx->width;
    int height = dec_ctx->height;
    AVRational time_base = video_stream->time_base;

    // 4. 分配帧
    frame = av_frame_alloc();
    yuv_frame = av_frame_alloc();
    if (!frame || !yuv_frame) {
        fprintf(stderr, "Frame allocation failed\n");
        goto end;
    }

    // 5. 准备 JPEG 编码器
    enc_codec = avcodec_find_encoder(AV_CODEC_ID_MJPEG);
    if (!enc_codec) {
        fprintf(stderr, "MJPEG encoder not found\n");
        goto end;
    }

    enc_ctx = avcodec_alloc_context3(enc_codec);
    if (!enc_ctx) {
        fprintf(stderr, "Cannot allocate encoder context\n");
        goto end;
    }

    // 设置编码参数
    enc_ctx->width = width;
    enc_ctx->height = height;
    enc_ctx->time_base = time_base;
    enc_ctx->pix_fmt = AV_PIX_FMT_YUVJ420P;  // MJPEG常用格式
    enc_ctx->flags |= AV_CODEC_FLAG_QSCALE;
    enc_ctx->global_quality = 90;            // 质量1~100
    enc_ctx->codec_type = AVMEDIA_TYPE_VIDEO;

    if ((ret = avcodec_open2(enc_ctx, enc_codec, NULL)) < 0) {
        fprintf(stderr, "Cannot open encoder\n");
        goto end;
    }

    // 6. 分配 YUV 帧缓冲区
    int num_bytes = av_image_get_buffer_size(enc_ctx->pix_fmt, width, height, 1);
    uint8_t *buffer = (uint8_t *)av_malloc(num_bytes);
    av_image_fill_arrays(yuv_frame->data, yuv_frame->linesize, buffer,
                         enc_ctx->pix_fmt, width, height, 1);
    yuv_frame->width = width;
    yuv_frame->height = height;
    yuv_frame->format = enc_ctx->pix_fmt;

    // 7. 创建色彩空间转换(从解码器格式 → YUVJ420P)
    sws_ctx = sws_getContext(width, height, dec_ctx->pix_fmt,
                             width, height, enc_ctx->pix_fmt,
                             SWS_BILINEAR, NULL, NULL, NULL);
    if (!sws_ctx) {
        fprintf(stderr, "Cannot create sws context\n");
        goto end;
    }

    av_init_packet(&pkt);
    pkt.data = NULL;
    pkt.size = 0;
    av_init_packet(&enc_pkt);
    enc_pkt.data = NULL;
    enc_pkt.size = 0;

    // 8. 读包,解码,编码,保存
    while (av_read_frame(fmt_ctx, &pkt) >= 0) {
        if (pkt.stream_index == video_stream_index) {
            ret = avcodec_send_packet(dec_ctx, &pkt);
            if (ret < 0) {
                fprintf(stderr, "Error sending packet\n");
                av_packet_unref(&pkt);
                continue;
            }

            while (ret >= 0) {
                ret = avcodec_receive_frame(dec_ctx, frame);
                if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF)
                    break;
                if (ret < 0) {
                    fprintf(stderr, "Error receiving frame\n");
                    goto end;
                }

                // 转换色彩空间
                sws_scale(sws_ctx, (const uint8_t * const *)frame->data, frame->linesize,
                          0, height, yuv_frame->data, yuv_frame->linesize);

                // 编码为 JPEG
                yuv_frame->pts = frame->pts;
                ret = avcodec_send_frame(enc_ctx, yuv_frame);
                if (ret < 0) {
                    fprintf(stderr, "Error sending frame to encoder\n");
                    continue;
                }

                while (ret >= 0) {
                    ret = avcodec_receive_packet(enc_ctx, &enc_pkt);
                    if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF)
                        break;
                    if (ret < 0) {
                        fprintf(stderr, "Error receiving packet from encoder\n");
                        goto end;
                    }

                    // 保存为 .jpg 文件
                    snprintf(out_filename, sizeof(out_filename), "frame_%04d.jpg", ++frame_count);
                    FILE *fp = fopen(out_filename, "wb");
                    if (!fp) {
                        fprintf(stderr, "Cannot open output file\n");
                    } else {
                        fwrite(enc_pkt.data, 1, enc_pkt.size, fp);
                        fclose(fp);
                        printf("Saved %s\n", out_filename);
                    }
                    av_packet_unref(&enc_pkt);
                }
            }
        }
        av_packet_unref(&pkt);
    }

    // 9. 冲刷解码器(可能还有缓存的帧)
    avcodec_send_packet(dec_ctx, NULL);
    while (avcodec_receive_frame(dec_ctx, frame) == 0) {
        sws_scale(sws_ctx, (const uint8_t * const *)frame->data, frame->linesize,
                  0, height, yuv_frame->data, yuv_frame->linesize);
        yuv_frame->pts = frame->pts;
        avcodec_send_frame(enc_ctx, yuv_frame);
        while (avcodec_receive_packet(enc_ctx, &enc_pkt) == 0) {
            snprintf(out_filename, sizeof(out_filename), "frame_%04d.jpg", ++frame_count);
            FILE *fp = fopen(out_filename, "wb");
            if (fp) {
                fwrite(enc_pkt.data, 1, enc_pkt.size, fp);
                fclose(fp);
                printf("Saved %s (flush)\n", out_filename);
            }
            av_packet_unref(&enc_pkt);
        }
    }

    // 冲刷编码器(最后一帧)
    avcodec_send_frame(enc_ctx, NULL);
    while (avcodec_receive_packet(enc_ctx, &enc_pkt) == 0) {
        snprintf(out_filename, sizeof(out_filename), "frame_%04d.jpg", ++frame_count);
        FILE *fp = fopen(out_filename, "wb");
        if (fp) {
            fwrite(enc_pkt.data, 1, enc_pkt.size, fp);
            fclose(fp);
            printf("Saved %s (encoder flush)\n", out_filename);
        }
        av_packet_unref(&enc_pkt);
    }

    printf("Decoded and encoded %d frames\n", frame_count);

end:
    // 释放资源
    av_packet_unref(&pkt);
    av_packet_unref(&enc_pkt);
    av_frame_free(&frame);
    av_frame_free(&yuv_frame);
    if (buffer) av_free(buffer);
    if (sws_ctx) sws_freeContext(sws_ctx);
    if (dec_ctx) avcodec_free_context(&dec_ctx);
    if (enc_ctx) avcodec_free_context(&enc_ctx);
    if (fmt_ctx) avformat_close_input(&fmt_ctx);
    return 0;
}

八、交叉编译测试程序

在FFmpeg源码根目录下执行(确保已安装交叉编译工具链):

复制代码
arm-linux-gnueabihf-gcc -o mp4_to_jpg mp4_to_jpg.c \
    -I host/include \
    -L host/dynamic \
    -L /home/yifeng/dirik_ssd20x_sdk/project/release/nvr/i2m/common/glibc/8.2.1/mi_libs/dynamic \
    -Wl,-rpath-link,host/dynamic \
    -Wl,-rpath-link,/home/yifeng/ssd20x_sdk/project/release/nvr/i2m/common/glibc/8.2.1/mi_libs/dynamic \
    -lavformat -lavcodec -lavutil -lswscale -lswresample \
    -lmi_vdec -lmi_sys -lmi_common -ldl -lpthread -lm

链接选项解释

  • -I:指定FFmpeg头文件路径。

  • -L:指定库文件搜索路径。

  • -Wl,-rpath-link:链接时指定共享库的依赖库搜索路径(解决间接依赖)。

  • -l...:链接必要的FFmpeg库和MI硬件解码库(即使不用硬解,libavcodec也可能依赖)。

  • -ldl:因为MI库用到了dlopen等动态加载函数。

若编译通过,会生成ARM可执行文件mp4_to_jpg

九、部署与运行

mp4_to_jpg以及host/dynamic/下的所有.so文件拷贝到开发板的某个目录(例如/usr/lib或程序同目录)。

拷贝一个测试MP4文件到板子,运行:

复制代码
./mp4_to_jpg test.mp4

程序会逐帧解码并输出frame_0001.jpgframe_0002.jpg...... 直到视频结束。

十、总结与反思

成功要点

  1. 正确配置FFmpeg:交叉编译的关键在于选项的裁剪,既要满足功能(解码+编码),又要避免不必要的依赖(OpenSSL、zlib)。

  2. 链接顺序与依赖 :嵌入式环境中,共享库的间接依赖需要显式指定-rpath-link和所需的附加库(如-ldl)。

  3. 编码器启用:如果想输出JPG,必须启用MJPEG编码器,而非一揽子禁用编码器。

走过的弯路

  • 最初想生成ffmpeg命令行工具来截图,但因依赖问题多次失败,最终决定自己写程序调用库,更灵活可控。

  • 忽略了MI库的依赖,导致链接错误不断,直到把SigmaStar SDK中的MI库路径加入才解决。

后续可优化点

  • 若需高性能,可启用硬件解码(-DSUPPORT_4K并启用对应解码器),但需确保SDK中硬件解码库可用。

  • 可增加参数控制输出帧间隔或分辨率。

如有侵权,或需要完整代码,请及时联系博主。

相关推荐
牢姐与蒯1 小时前
Linux进程(二)之进程概念
linux·运维·服务器·ubuntu
HiDev_1 小时前
【非标自动化】2、认识元器件(模拟量模块)
运维·自动化
终末圆1 小时前
GitHub Actions 实现 CI/CD
运维·ci/cd·github·运维开发·devops
草莓熊Lotso2 小时前
【Redis 初阶】Hash 类型深度解析:结构化数据存储的最优解
linux·网络·数据库·redis·tcp/ip·缓存·哈希算法
jieshenai2 小时前
Shell 脚本如何定位项目根目录——从一个 8 行的小脚本讲起
linux·运维
迷途之人不知返2 小时前
自制第一个Linux小程序——进度条
linux
zbyyd2 小时前
深入理解 Linux 文件 IO 与目录 IO:系统调用与库函数的本质区别
linux·c语言
Linux运维技术栈2 小时前
云平台服务器遭遇黑客攻击?用 Nginx 批量封锁敏感接口,自动返回 444 关闭连接
运维·服务器·nginx·安全·防御
qetfw2 小时前
Debian OpenLDAP 目录服务配置:DN、LDIF、ldapsearch 与认证验证
linux·运维·debian