You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Java基于Jaffree转RTSP流推JSMpeg的内存优化问题

问题背景

  • 使用Jaffree库调用FFmpeg转换并展示RTSP流,功能与node-rtsp-stream库完全一致,仅将实现语言从Node.js更换为Java
  • 核心处理流程:
    1. 携带正确参数启动ffmpeg进程,输出内容到标准输出
    2. 捕获标准输出产生的数据包/字节流
    3. 通过WebSocket将数据发送到客户端
  • 客户端使用JSMpeg播放流,逻辑和node-rtsp-stream方案完全一致,目前功能可正常运行,但存在内存占用过高的问题

现有代码片段

Jaffree库进程处理代码

protected Executor startExecution(final Process process,
                                  final AtomicReference<T> resultReference) {
    Executor executor = new Executor(contextName);

    if (stdOutReader != null) {
        executor.execute("StdOut", new Runnable() {
            @Override
            public void run() {
                T result = stdOutReader.read(process.getInputStream());
                if (result != null) {
                    boolean set = resultReference.compareAndSet(null, result);
                    if (!set) {
                        LOGGER.warn("Ignored result of reading STD OUT: {}", result);
                    }
                }
            }
        });
    }
}

StdOutReader实现代码

private static final int BUFFER_SIZE = 2048000;

@Override
public T read(final InputStream stream) {
  Try.run(() -> {
    final byte[] buffer = new byte[BUFFER_SIZE];
    int length;

    while ((length = stream.read(buffer)) >= 0) {
      final byte[] finalBuffer = new byte[length];
      System.arraycopy(buffer, 0, finalBuffer, 0, length);
      socket.sendMessage(finalBuffer);
    }
  }).onFailure(throwable -> {
    throw new RuntimeException("FFmpeg error", throwable);
  });

  return null;
}

问题说明

  • 底层流是Process返回的InputStream,实际为BufferedInputStream
  • 无法提前获知流中可用字节的长度,而JSMpeg需要接收准确长度的帧/数据包才能无异常播放,因此预先声明了超大字节数组来适配最大的数据包,导致内存占用过高
  • 预期流程:FFmpeg标准输出每输出一个数据包就直接通过WebSocket转发,当前实际运行流程确实符合预期:
    StdOut sends packet -> passing packet to WebSocket -> StdOut sends packet -> passing packet to WebSocket -> ...
  • 尝试过获取当前InputStream中待读取的字节数,但是BufferedInputStream无法实现该需求

已尝试无效方案

  • 用Reader按行读取再转字节,返回的长度不符合要求,无法正常播放
  • 在ffmpeg参数中指定数据包的缓冲区/长度,要么不生效要么流损坏
  • 观察到数据包长度都是188字节的倍数,但按该规则对齐后流仍损坏,推测数据包长度必须和标准输出返回的实际长度完全一致
  • 调用readAllBytes()读取会导致循环直接终止
  • 使用transferTo和ByteArrayOutputStream相关方法也没有效果
  • 调用InputStream的available()方法获取长度也不符合要求
  • 尝试用Spring 5 WebFlux的DataBuffer将Stream转成Flux订阅,但只能读取单个字节,不知道怎么读取当前完整缓冲区内容

待解决问题

有没有办法修改JSMpeg让它支持接收固定长度的数据包(比如固定2048字节)?或者能不能通过标准输出的特定字节预测数据包长度?请问该需求是否可实现,要怎么做?


补充说明

  • 按照Jaffree作者Denis Kokorin的回答用PipeOutput实现了功能,但存在完全一样的限制,PipeOutput底层也是将流内容复制到预先指定大小的byte数组中,输出结果如下:
Array length: 262144, actual length: 9588
Array length: 262144, actual length: 4700
Array length: 262144, actual length: 9024
Array length: 262144, actual length: 10152
Array length: 262144, actual length: 18048
  • 需要通过WebSocket发送的是实际长度的数组,而非整个预定义数组的长度,原因是JSMpeg的运行机制:

音视频内部缓冲区很小(分别为512kb和128kb),JSMpeg 会丢弃旧的甚至未播放的数据来腾出空间接收新数据,以此保证最低延迟,网络拥堵时可能会出现解码 artifacts,但延迟控制效果很好。

  • JSMpeg默认缓冲区大小是524288,如果发送预定义大小的数组会导致没有内容能正常展示
  • 猜测用ChannelOutput也会有类似问题,如果没有的话麻烦告知怎么把SeekableByteChannel的数据通过WebSocket发送

你提到了Spring,大概率也用了Spring的WebSocket实现,Spring默认在WebSocket上用STOMP协议,WebSocket本身和TCP Socket类似可以发送任意字节,STOMP和HTTP类似,所以不用STOMP可以直接流式发送字节。

  • 确认使用Spring 5的spring-boot-starter-websocket依赖,场景必须用STOMP,是通过SimpMessagingTemplate.convertAndSend方法传入byte[]参数来发送数据的

我还想建议一个替代方案:如果内容存不下就写入磁盘。

  • 该方案不适用,因为处理的是直播流,要求延迟尽可能低

文件上传后直接编码为MJPEG。

  • 流格式不是MJPEG是mpegts,JSMpeg只是库名而已

所用FFmpeg命令

ffmpeg -loglevel level+info -i rtsp://test:test@10.10.10.10:554/stream -n -rtsp_transport tcp -f mpegts -codec:v mpeg1video -r 30 -s 480x320 -b:v 1500k -bf 0 -an -vf scale=480:320 -

解决方案

方案1:直接调整缓冲区大小(成本最低,立即可用)

你当前设置的2MB缓冲区完全是冗余的,从你给出的日志可以看到实际单包最大长度只有18KB左右,直接把BUFFER_SIZE调整为64KB就可以覆盖所有场景,内存占用直接降到原来的1/32,完全满足需求。
InputStream.read(byte[])本身就会返回实际读取到的字节数,你后续只复制前length个字节发送,只要缓冲区大小大于最大可能的单包长度就不会出现截断问题,64KB的缓冲区完全不会有内存压力。

方案2:基于MPEG-TS格式拆分数据包(更规范)

你输出的是标准mpegts格式,每个ts包固定为188字节,且包头有固定同步字节0x47。你可以把缓冲区设为188字节的整数倍(比如188*20=3760字节),读取到数据后按同步字节拆分出完整的188字节ts包再发送,完全不需要大缓冲区,也能保证JSMpeg正常解码。

方案3:修改JSMpeg适配固定长度包

如果需要Java侧发送固定长度的包,只需要修改JSMpeg的输入逻辑:在JSMpeg的接收层增加缓冲区拼接逻辑,收到固定长度的包后先写入内部缓冲区,累积到足够的完整ts包长度再送入解码器即可,不影响原有低延迟特性。

内容的提问来源于stack exchange,提问作者MattIT

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.09.24 09:09:13