若该文为原创文章,转载请注明原文出处。
一、背景与目标
最近在基于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配置脚本,方便我们修改。
四、编写配置脚本
我们需要启用必要的组件: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 \
--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会关掉所有编码器。 -
禁用了
openssl和zlib,避免因交叉编译环境中缺少这些库而导致配置失败。 -
保留了硬解码相关路径(
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. 链接时找不到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中硬件解码库可用。 -
可增加参数控制输出帧间隔或分辨率。
如有侵权,或需要完整代码,请及时联系博主。