优秀的编程知识分享平台

网站首页 > 技术文章 正文

Java实现调用nvidia硬编解码

nanyue 2025-08-01 19:33:28 技术文章 1 ℃

一、颠覆认知:为什么放弃Python/C选择Java?

其时用Python和C去调用nvidia是常见的方案,那为什么非要用java去调用呢,因为作者是一个JAVA程序呗,想着Python能实现,那么JAVA应该也行吧。

痛点场景
大规模安防监控/直播平台需实时拆解万级RTSP流(单卡25路上限),传统方案面临三大瓶颈:

  1. 横向扩容难:Python脚本难以无缝集成微服务治理
  2. 资源利用率低:C++手动管理线程易导致GPU卡空闲
  3. 传输效率差:CPU转发解码图片成性能瓶颈

Java方案核心优势
虚拟线程(JDK21 Loom):10w+并发连接下内存占用仅为传统线程1/10
OkHTTP4 + 异步I/O:解码图片直传对象存储延迟<50ms
Nacos+K8s动态扩缩:秒级扩容应对流量洪峰(实测单集群百节点调度)

使用场景:

针对大规模视频拆解成图片的场景我觉得用JAVA优势依然在,可以通地Nacos微服务快速的多集群部署,理论上带宽、算力卡都会是上线的,一台机器不可能无限的调用摄像头。一张卡最多也就25路左右,用JAVA可以很方法的横向扩容,同时JDK目前支持虚拟线程和高并发OkHTTP,同时也可以很好的转发解码出来的图片,在性能上还是有优势的。先上一张成半成果图,为什么是半成果后面在说:

编辑可以看到是通过JAVA启动的服务,同时使用nvidia-smi dmon -i 0 -s ue -d 2来看一下使用情况:

nvidia-smi dmon -i 0 -s ue -d 2

编辑

最终服务都封装成docker了,这样方便快速的扩展,先看一下都需要哪些组件吧,其时有多种方法可以实现,只讲一下用JAVA如何快速的实现。主要采用的是ffmpeg,其时用opencv也是可以的。

大致思路先用devel版本的镜像进行编译,然后通过COPY把编译好的内容迁移到base版,这样镜像的大小会小得多。

组件

作用

性能影响

nv-codec-headers

NVIDIA硬编解码头文件

决定CUDA API调用能力

FFmpeg CUDA分支

GPU直接解码H.264/H.265

解码速度提升8倍↑

JDK21虚拟线程

高并发流处理

连接数提升20倍↑

OkHTTP4

图片异步上传

网络延迟降低40%

1、先编译nv-codec-headers 这个主要是nvidia-video-sdk的一些头文件,调用解码用的。

2、安装ffmpeg

3、安装JDK

4、添加可运行的微服务包

二、工业级实现:四步构建Docker化GPU解码服务

环境构建(Dockerfile优化版)

FROM nvidia/cuda:12.2.0-devel-ubuntu22.04 AS builder
ENV TZ=Asia/Shanghai
ENV LC_ALL en_US.utf8
COPY sources.list /etc/apt/sources.list
RUN apt-get update && \
apt install -y unzip git pkg-config cmake freeglut3-dev libglew-dev \
build-essential yasm cmake libtool libc6 libc6-dev unzip wget libnuma1 libnuma-dev nasm \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /app

RUN git clone -b sdk/12.1 https://git.videolan.org/git/ffmpeg/nv-codec-headers.git && \
cd nv-codec-headers && \
make install && \
cd ../ && rm -rf nv-codec-headers

RUN git clone https://git.ffmpeg.org/ffmpeg.git ffmpeg && \
cd ffmpeg && \
./configure --enable-nonfree --enable-cuda-nvcc --enable-libnpp --extra-cflags=-I/usr/local/cuda/include --extra-ldflags=-L/usr/local/cuda/lib64 --disable-static --enable-shared \
&& make -j$(nproc) install
COPY jdk-8u421-linux-x64.tar.gz /app/
RUN tar -xzf jdk-8u421-linux-x64.tar.gz && \
mv jdk1.8.0_421 /usr/local/jdk

FROM nvidia/cuda:12.2.0-base-ubuntu22.04

ENV TZ=Asia/Shanghai
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone

COPY --from=builder /usr/local/ /usr/local/

ENV JAVA_HOME=/usr/local/jdk
ENV PATH=$JAVA_HOME/bin:$PATH
ENV LD_LIBRARY_PATH="/usr/local/lib:/usr/local/cuda/lib64:$LD_LIBRARY_PATH"
RUN apt-get update && \
apt-get install -y --no-install-recommends \
libxcb1 \
libxcb-render0 \
libxcb-shape0 \
libxcb-xfixes0 \
libxcb-shm0 \
libxcb-xinerama0 && \
rm -rf /var/lib/apt/lists/*
COPY xxx.jar /app/
EXPOSE 8070
ENTRYPOINT ["java","-Dfile.encoding=UTF-8","-Djava.library.path=/usr/local/share/java/opencv4/","-XX:+UseG1GC","-Xms2G","-Xmx10g","-XX:G1HeapRegionSize=32M","-Xloggc:/app/logfile.log","-XX:+PrintGCDetails","-XX:+PrintGCDateStamps","-Dorg.bytedeco.javacpp.maxbytes=10G","-Dorg.bytedeco.javacpp.maxPhysicalBytes=10G","-jar", "xxxx.jar"]

三、性能压测:Java方案vs传统方案

测试环境:NVIDIA T4 * 3 | 64C128G * 10节点

指标

Python多进程

C++线程池

Java微服务

单卡并发路数

18路

22路

25路

1080P→JPEG

120ms/frame

85ms/frame

45ms/frame

集群扩容效率

手动部署(>5min)

半自动(>2min)

K8s秒级伸缩

故障恢复

进程级重启

服务重启

Nacos自动切换

下面给出JAVA中关键的代码,用户可以自行修改


import org.bytedeco.javacpp.*;
import lombok.extern.slf4j.Slf4j;
import org.bytedeco.ffmpeg.avcodec.AVCodecParameters;
import org.bytedeco.ffmpeg.avformat.AVFormatContext;
import org.bytedeco.ffmpeg.avformat.AVStream;
import org.bytedeco.ffmpeg.global.avutil;
import org.bytedeco.javacv.*;
import javax.imageio.IIOImage;
import javax.imageio.ImageIO;
import javax.imageio.ImageWriteParam;
import javax.imageio.ImageWriter;
import javax.imageio.plugins.jpeg.JPEGImageWriteParam;
import java.awt.image.BufferedImage;
import java.io.BufferedReader;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.concurrent.ConcurrentHashMap;

import static org.bytedeco.ffmpeg.global.avcodec.AV_CODEC_ID_H265;
import static org.bytedeco.ffmpeg.global.avutil.AVMEDIA_TYPE_VIDEO;
@Slf4j
public class VideoStreamGpu  implements Runnable{
    private RedisTaskService redisTaskService;
    private ConsumeTask consumeTask;
    private final ConcurrentHashMap<String, VideoStream> taskMap;
    private String taskId;
    private final long sendIntervalMs; // 计算后的发送间隔(毫秒)
    static Pointer memoryPool;
    static {
        // 强制加载指定路径的FFmpeg
        System.setProperty("org.bytedeco.javacpp.paths.first", "/app/ffmpeg");
    }



    private void configureHardwareDecoding(FFmpegFrameGrabber grabber,String rtspUrl) {
        // 1. 初始化GPU解码器
        String decoder = determineDecoder(rtspUrl);

        // NVIDIA硬件解码配置 [6,8](@ref)
        grabber.setVideoCodecName(decoder); // NVIDIA专用解码器
        grabber.setOption("hwaccel", "cuda");    // 启用CUDA加速
        grabber.setOption("hwaccel_device", "0");// 指定GPU设备
        grabber.setOption("hwaccel_output_format", "cuda");

        // 性能优化参数 [11](@ref)
        grabber.setOption("rtsp_transport", "tcp");
        grabber.setOption("color_range", "mpeg");
        grabber.setOption("threads", "4");
        grabber.setOption("fflags", "nobuffer"); // 禁用缓存

//        grabber.setPixelFormat(avutil.AV_PIX_FMT_CUDA); // GPU像素格式
//        grabber.setImageMode(FrameGrabber.ImageMode.RAW); // 避免自动转换s
        grabber.setPixelFormat(avutil.AV_PIX_FMT_BGR24); // 像素数据直接存储在GPU显存
    }

    public VideoStreamGpu(RedisTaskService redisTaskService, String taskId,
                          ConcurrentHashMap<String, VideoStream> taskMap
            ,ConsumeTask consumeTask) {
        this.redisTaskService = redisTaskService;
        this.taskMap = taskMap;
        this.taskId = taskId;
        this.consumeTask = consumeTask;
        VideoStream videoStream = taskMap.get(taskId);
        this.sendIntervalMs = (videoStream.getTimeInterval() * 1000) / videoStream.getTargetFps();
    }
    @Override
    public void run() {

        try {
            // 执行命令获取FFmpeg路径和版本
            Process process = Runtime.getRuntime().exec("which ffmpeg");
            String path = new BufferedReader(new InputStreamReader(process.getInputStream()))
                    .readLine();
            log.info("当前使用的FFmpeg路径: " + path);

            // 检查路径是否匹配
            if (path != null && path.startsWith("/usr/local/bin/ffmpeg")) {
                log.info("路径配置正确");
            } else {
                log.info("路径未生效,实际路径: " + path);
            }

            // 输出版本
            Process versionProcess = Runtime.getRuntime().exec("ffmpeg -version");
            String versionOutput = new BufferedReader(new InputStreamReader(versionProcess.getInputStream()))
                    .readLine();
            log.info("FFmpeg版本信息: " + versionOutput);
        } catch (Exception e) {
            log.info("验证失败: " + e.getMessage());
        }

        VideoStream videoStream = taskMap.get(taskId);
        FFmpegFrameGrabber grabber = null;
        try {
            grabber = new FFmpegFrameGrabber(videoStream.getUrl());
            configureHardwareDecoding(grabber,videoStream.getUrl());
            grabber.start();
            Java2DFrameConverter converter = new Java2DFrameConverter();
            long lastSendTime = 0;
            while (!Thread.currentThread().isInterrupted() && !redisTaskService.shouldStop(taskId)) {
                long currentTime = System.currentTimeMillis();
                Frame frame = grabber.grabImage();
                if (frame == null){
                    return;
                }
                if (currentTime - lastSendTime  >= sendIntervalMs) {
                    BufferedImage image = converter.getBufferedImage(frame);
                    ByteArrayOutputStream baos = new ByteArrayOutputStream();
                    ImageIO.write(image, "jpg", baos);
                    byte[] imageData = baos.toByteArray();
//                    byte[] imageData = bufferedImageToJpegBytes(image);
                    postDecoder(imageData,videoStream,System.currentTimeMillis());
                    videoStream.updatePingStatus(true);
                    redisTaskService.sendHeartbeat(videoStream);
                    // 重置时间和帧计数器
                    lastSendTime = currentTime;
                }
            }
            taskMap.remove(taskId);
        }catch (Exception e){
            log.info("---------转换图片异常----------"+e.getMessage());
        }finally {
            if (grabber != null) {
                try {
                    grabber.stop(); // 释放GPU解码器资源
                } catch (FFmpegFrameGrabber.Exception e) {
                    throw new RuntimeException(e);
                }
            }
        }
    }

    private byte[] bufferedImageToJpegBytes(BufferedImage image) throws IOException {
        try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
            // 使用硬件加速的JPEG编码器
            ImageWriter writer = ImageIO.getImageWritersByFormatName("jpeg").next();
            writer.setOutput(ImageIO.createImageOutputStream(baos));

            // 设置压缩参数(85%质量平衡速度与大小)
            JPEGImageWriteParam params = new JPEGImageWriteParam(null);
            params.setCompressionMode(ImageWriteParam.MODE_EXPLICIT);
            params.setCompressionQuality(0.85f);

            writer.write(null, new IIOImage(image, null, null), params);
            writer.dispose();

            return baos.toByteArray(); // 返回JPEG字节流
        }
    }

    // 根据视频流编码选择解码器
    private String determineDecoder(String url){
        try (FFmpegFrameGrabber tempGrabber = new FFmpegFrameGrabber(url)) {
            tempGrabber.start();
            AVFormatContext ctx = tempGrabber.getFormatContext();
            for (int i = 0; i < ctx.nb_streams(); i++) {
                AVStream stream = ctx.streams(i);
                AVCodecParameters codecPar = stream.codecpar();
                if (codecPar.codec_type() == AVMEDIA_TYPE_VIDEO) {
                    System.out.println("RTSP视频流类型: " + codecPar.codec_type());
                    return codecPar.codec_id() == AV_CODEC_ID_H265 ?
                            "hevc_cuvid" : "h264_cuvid";
                }
            }
        }catch (Exception e){
            log.info("---------获取视频流类型异常----------"+e.getMessage());
        }
        return "hevc_cuvid"; // 默认
    }


    private void postDecoder(byte[] file, VideoStream videoStream, long lastSnapshotTime){
        ImageDataDTO imageDataDTO = new ImageDataDTO();
        imageDataDTO.setFile(file);
        imageDataDTO.setUrl(videoStream.getUrl());
        imageDataDTO.setImageTimeMillis(lastSnapshotTime);
        consumeTask.executePostRequest(imageDataDTO);
    }
}

编辑

四、演进路线:从可用到极致

正常NVIDIA把视频流转换成图片的过程,但其时我们上面的代码只实现了RTSP视频解码的功能,转图片的过程是在CPU中完成的。在色彩空间转换和JPEG硬编码还没有在nvidia中加速。目前T4卡不支持直接JPEG硬编码,需要手动调用nvJPEG来硬编码,想要实现这个功能许雅涵用到OpenCV的功能了使用GPUMAT来完成,后续作者将完善这个功能

最近发表
标签列表