参考链接:www.seafog.cn/archives/19…
问题
在docker容器内部署了图像识别应用,容器启动时执行程序中的拉流步骤发现时好时坏,而且拉流失败次数大于成功次数,对此很迷惑。
分析
怀疑是opencv依赖库没有缓存上,进入容器中执行echo $LD_LIBRARY_PATH 发现没问题
怀疑是容器内其他依赖或者网络的问题,在容器内安装了ffmpeg并试了一下拉流发现还是没问题
最后在网上又搜了下资料,发现容器启动时也需要先执行一些初始化操作,缓存包括host、dns等配置。因此怀疑可能是拉流的时候网络不通导致的。
解决
修改docker的配置文件,添加dns配置,把该加的都加上
sudo vim /etc/docker/daemon.json
"dns": ["8.8.8.8"]
接着在代码中对拉流操作添加重试,失败后release,最后发现确实稳定了好多,目前还没发现什么大问题
如果不行就在docker run的时候加上--network host,使用宿主机的网络配置。
重开
过了几天发现又变回了原样,这次不管怎么去尝试,总是时好时坏的,代码中打开异常模式查看一下报错信息。
查看opencv470对应位置的源码,猜测可能是backends没有获取到导致的
接下来尝试指定捕获api
发现还是拉流失败,怀疑可能是编译opencv的时候,依赖库不全导致的。
因为项目使用的jdk17,接下来尝试使用jdk17重新编译opencv470。
先下载opencv470然后解压,进入目录,创建build文件夹,进入build文件夹。
使用cmake构建opencv,CMAKE_INSTALL_PREFIX是自定义的构建后保存路径
cmake -D CMAKE_BUILD_TYPE=RELEASE \
-D CMAKE_INSTALL_PREFIX=/userdata/opencv470 \
-D BUILD_opencv_java=ON \
-D JAVA_HOME=/usr/lib/jvm/java-17-openjdk-arm64 \
..
然后发现缺少ffmpeg的库,相关打印日志如下
于是开始安装一些需要的基础依赖
apt update
apt install build-essential cmake net-tools git vim unzip iputils-ping -y
apt install -y libgtk2.0-dev pkg-config libavcodec-dev libavformat-dev libswscale-dev libswresample-dev libavresample-dev libgphoto2-dev
apt install python3-dev python3-numpy libtbb2 libtbb-dev libjpeg-dev libpng-dev libtiff-dev libdc1394-22-dev
apt-get install -y libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev libgstreamer-plugins-bad1.0-dev gstreamer1.0-plugins-base gstreamer1.0-plugins-good gstreamer1.0-plugins-bad gstreamer1.0-libav ffmpeg
接下来删除build目录并重建,然后重新cmake,发现相关库都有了
接下来想继续编译时发现ant存在问题,这样编译不了Java的依赖库了,安装ant
apt install -y ant
但是发现还是不行,搜了一下相关帖子,发现了如下解决方法
重新链接ant
ln -snf /usr/share/ant/bin/ant /bin/ant
然后继续cmake
构建成功,接下来继续按照常规步骤进行编译和安装
make -j$(nproc)
sudo make install
重新制作镜像然后启动,发现还是不行 !!!
接下来得改变思路了,不一定非得Java去拉流,可以在容器内起一个ffmpeg去拉流,然后持续保存在一个固定路径下面,接下来Java应用只需要去监听该路径下的文件变化就ok了。
Java可以使用文件监听器WatchService来实现对文件变化的感知,下图中右终端路径下1-13图片未被删除,是因为脚本先后启动顺序,漏掉了,这个部分无伤大雅。或者ffmpeg反复覆盖同一个文件。
考虑到开发板闪存反复读写可能会导致损坏,最好不要直接存硬盘,参考了网上的帖子,可以考虑额外挂载一个内存分区。
接下来就是重新编写dockerfile和启动脚本
Dockerfile文件
# 使用官方的 Ubuntu 20.04 作为基础镜像
FROM ubuntu:20.04
# 设置工作目录
WORKDIR /usr/app
# 安装 ca-certificates(确保证书可用)
RUN apt-get update && \
apt-get install -y ca-certificates && \
rm -rf /var/lib/apt/lists/*
# 复制宿主机的 APT 源列表和配置文件到容器内
COPY ./sources.list /etc/apt/sources.list
RUN apt-get update
# 安装基础工具
RUN DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
build-essential \
cmake \
net-tools \
git \
vim \
unzip \
iputils-ping
# 安装多媒体库
RUN DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
libavcodec-dev \
libavformat-dev \
libswscale-dev \
libswresample-dev \
libavresample-dev \
libgphoto2-dev \
python3-dev \
python3-numpy \
libtbb2 \
libtbb-dev \
libjpeg-dev \
libpng-dev \
libtiff-dev \
libdc1394-22-dev
# 安装 GStreamer 相关包
RUN DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
libgstreamer1.0-dev \
libgstreamer-plugins-base1.0-dev \
libgstreamer-plugins-bad1.0-dev \
gstreamer1.0-plugins-base \
gstreamer1.0-plugins-good \
gstreamer1.0-plugins-bad \
gstreamer1.0-libav \
ffmpeg
# 安装 Java 开发环境
RUN DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
openjdk-17-jdk
# 清理 APT 缓存
RUN rm -rf /var/lib/apt/lists/*
# 将外部的 JAR 包复制到容器内
COPY start-1.0.0-SNAPSHOT.jar .
# 将配置文件夹复制到容器内
COPY ./config ./config
COPY ./opencv-build ./opencv-build
# 更新环境变量 LD_LIBRARY_PATH
ENV PATH=/usr/app/opencv-build/bin:$PATH
ENV LD_LIBRARY_PATH=/usr/app/opencv-build/lib:$LD_LIBRARY_PATH
# 运行 ldconfig 来更新动态库缓存
RUN ldconfig
# 将脚本复制到容器中
COPY entrypoint.sh .
# 确保脚本可执行
RUN chmod +x /usr/app/entrypoint.sh
# 设置容器启动时运行的命令
ENTRYPOINT ["/usr/app/entrypoint.sh"]
entrypoint.sh脚本
#!/bin/bash
# 设置一些默认值
FFMPEG_INPUT="rtsp://username:password@ip:554/cam/realmonitor?channel=1&subtype=0"
TEMP_DIR="/mnt/memdrive"
OUTPUT_DIR="$TEMP_DIR/output" # 使用 tmpfs 中的目录作为输出路径
INTERVAL=100 # 每n帧保存一次
# 创建输出目录(如果不存在)
mkdir -p $TEMP_DIR
mkdir -p $OUTPUT_DIR
# 使用 ffmpeg 拉流,并每隔 n 帧保存一帧为图片到 tmpfs,放到后台执行
ffmpeg -i $FFMPEG_INPUT -vf "select='not(mod(n,$INTERVAL))',setpts=N/FRAME_RATE/TB" -q:v 2 "$OUTPUT_DIR/image-%04d.jpg" &
# 获取 ffmpeg 的进程 ID 并保存到变量
FFMPEG_PID=$!
# 启动 Java 应用程序
echo "Starting Java application..."
exec java -jar -Djava.library.path=/usr/app/opencv-build/lib /usr/app/start-1.0.0-SNAPSHOT.jar --spring.config.location=file:///usr/app/config/application.yaml
# 让脚本挂起,防止容器退出
tail -f /dev/null
Java代码也得做修改
public void processImagesFromDirectory(String imagePath) {
Path dir = Paths.get(imagePath);
if (!Files.isDirectory(dir)) {
log.error("Provided path is not a directory: " + imagePath);
return;
}
log.info("Starting to watch and process images from directory: " + imagePath);
Thread thread = new Thread(() -> {
try (WatchService watchService = FileSystems.getDefault().newWatchService()) {
dir.register(watchService, StandardWatchEventKinds.ENTRY_CREATE);
while (true) {
WatchKey key;
try {
key = watchService.take(); // 阻塞等待事件
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
log.info("Image processing thread interrupted.");
return;
}
for (WatchEvent<?> event : key.pollEvents()) {
WatchEvent.Kind<?> kind = event.kind();
if (kind == StandardWatchEventKinds.OVERFLOW) {
continue;
}
@SuppressWarnings("unchecked")
WatchEvent<Path> ev = (WatchEvent<Path>) event;
Path filename = ev.context();
if (filename.toString().toLowerCase().endsWith(".jpg") || filename.toString().toLowerCase().endsWith(".png")) {
File imgFile = dir.resolve(filename).toFile();
try {
// 读取图片
Mat img = Imgcodecs.imread(imgFile.getAbsolutePath());
if (img.empty()) {
log.error("Failed to read image file: " + imgFile.getAbsolutePath());
continue;
}
// 将图像的副本放入队列
if (!waitForDetectImgQueue.offer(img.clone())) {
log.warn("Image queue is full, dropping image.");
}
// 释放原始图像
img.release();
// 处理完成后删除图片文件
if (!imgFile.delete()) {
log.warn("Failed to delete image file: " + imgFile.getAbsolutePath());
} else {
log.info("Deleted image file: " + imgFile.getAbsolutePath());
}
} catch (Exception e) {
log.error("Error processing image file: " + imgFile.getAbsolutePath(), e);
}
}
}
boolean valid = key.reset();
if (!valid) {
break;
}
}
} catch (Exception e) {
log.error("Error setting up watch service.", e);
}
});
thread.start();
}
容器启动指令
docker run -d --name uj2 --mount type=tmpfs,destination=/mnt/memdrive,tmpfs-size=100m ubuntu:v2
好!重新制作和启动后发现问题完美解决!!!
思路有了,大部分脚本和代码都是gpt帮忙写的,哈哈!