)
若该文为原创文章转载请注明原文出处。一、背景与目标最近在基于SigmaStar SSD202DARM Cortex-A7的嵌入式开发板上做一个视频抓图功能需要将MP4视频文件解码成单帧图片JPG格式。板子跑的是Linux资源有限所以打算使用FFmpeg库来实现。然而直接在板子上编译FFmpeg不现实必须在Ubuntu主机上交叉编译生成ARM架构的动态库和可执行程序。本文记录了从下载FFmpeg源码、配置交叉编译、解决各种依赖问题到编写一个简单的C程序完成MP4解码并输出JPG的全过程希望对类似项目的朋友有所帮助。二、环境准备主机Ubuntu 20.04 x86_64目标芯片SigmaStar SSD202DARMv7-A双核交叉编译工具链gcc-arm-8.2-2018.08-x86_64-arm-linux-gnueabihfGCC 8.2.1FFmpeg源码经适配SSD202硬件解码的版本来自DongshanPI仓库SDKSigmaStar 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配置脚本方便我们修改。四、编写配置脚本我们需要启用必要的组件avcodec、avformat、swscale、swresample并且要启用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 \ --prefixhost \ --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 \ --archarm \ --target-oslinux \ --cross-prefixarm-linux-gnueabihf- \ --enable-shared \ --disable-gpl \ --enable-nonfree \ --disable-openssl \ # 禁用OpenSSL不需要HTTPS --disable-zlib \ # 禁用zlib不需要PNG输出 --enable-pthreads \ --enable-error-resilience \ --enable-debug \ --enable-encodermjpeg \ # 启用MJPEG编码器 --disable-hwaccels \ --disable-muxers \ --disable-indevs \ --disable-outdevs \ --disable-devices \ --disable-filters \ --disable-decoderssh264_ss268 \ --disable-decodersshevc_ss268 \ --shlibdirhost/dynamic \ --libdirhost/static \ --pkg-configpkg-config \ --extra-cflags${cflags} -g -rdynamic -funwind-tables -ffunction-sections \ --extra-ldflags${ldflags}关键点指定了交叉编译工具链前缀arm-linux-gnueabihf-。显式启用--enable-encodermjpeg因为默认--disable-encoders会关掉所有编码器。禁用了openssl和zlib避免因交叉编译环境中缺少这些库而导致配置失败。保留了硬解码相关路径mi_vdec等但此处我们只用软解所以实际上硬解解码器被禁用了--disable-decoderssh264_ss268。五、编译FFmpeg执行配置假定SDK根目录为/home/yifeng/dirik_ssd20x_sdkcd 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-encodermjpeg同时去掉--disable-encoders。4. 链接时找不到libswresample和MI_*符号现象编译测试程序时提示undefined reference to swr_alloc和MI_VDEC_*。原因缺少-lswresample链接。由于FFmpeg编译时开启了硬解支持libavcodec依赖SigmaStar的MI库libmi_vdec、libmi_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.jpg、frame_0002.jpg…… 直到视频结束。十、总结与反思成功要点正确配置FFmpeg交叉编译的关键在于选项的裁剪既要满足功能解码编码又要避免不必要的依赖OpenSSL、zlib。链接顺序与依赖嵌入式环境中共享库的间接依赖需要显式指定-rpath-link和所需的附加库如-ldl。编码器启用如果想输出JPG必须启用MJPEG编码器而非一揽子禁用编码器。走过的弯路最初想生成ffmpeg命令行工具来截图但因依赖问题多次失败最终决定自己写程序调用库更灵活可控。忽略了MI库的依赖导致链接错误不断直到把SigmaStar SDK中的MI库路径加入才解决。后续可优化点若需高性能可启用硬件解码-DSUPPORT_4K并启用对应解码器但需确保SDK中硬件解码库可用。可增加参数控制输出帧间隔或分辨率。如有侵权或需要完整代码请及时联系博主。