Skip to content

java-native-media ​

音视频处理优化库


项目背景 ​

在传统的 Java 音视频处理方案中,开发者通常依赖调用 FFmpeg 命令行工具来实现音视频处理需求。但这种方式存在两个主要痛点:

  1. 性能损耗 调用外部进程会带来额外的进程间通信开销,从而影响整体处理效率。

  2. 包体积膨胀 为了功能完整性,往往需要捆绑 FFmpeg 二进制文件,导致最终应用包体积显著增加。

为了解决以上问题,本项目使用 JNI 技术,使 Java 能直接调用经过深度优化的 C 语言音视频处理库,在特定场景下实现高性能、低体积的音视频处理方案。


核心功能(已实现) ​

下面列出的功能都属于“已有功能”,包括最初实现的功能和后续新增/扩展的方法,它们已被整合为统一的能力集合,便于开发者快速查阅与使用:

  • MP3 智能分片 按指定字节数分割 MP3,保证帧边界完整、播放正确,内存高效分流(适合大语言模型输入限制等场景)。

  • MP4 → MP3 提取 从 MP4 视频中高效提取音频并转为 MP3,尽量保留音质,适用于语音识别、音频分析。

  • 通用格式转换(容器/编码器映射) 支持传入容器格式(如 mp3、wav)或编码器名称(如 libmp3lame、aac),内部自动映射并高效转换。

  • 通用格式拆分(流拷贝、实时监控大小、无损拆分) 对任意 FFmpeg 支持的格式进行按大小拆分,采用流拷贝,避免重编码,动态监控输出文件大小,保证数据无损。

  • 支持格式查询 返回当前 JNI 库支持的所有格式与编码器名称,方便参数选择。

  • HLS 相关功能(持久会话与即时追加)

    • 将 MP4 拆为 HLS 段并追加到指定 playlist(支持单次追加)。
    • 持久 HLS 会话:初始化会话、追加 MP4 段、插入静音段、结束会话(写入 #EXT-X-ENDLIST)或直接释放会话资源。
    • 列出当前活跃 HLS 会话(返回 JSON 信息)。
  • 静音处理与拼接支持 在转码或拼接流程中可插入指定时长的静音段,支持 toMp3ForSilence 等场景。

  • 合并(stream copy) 使用流拷贝方式合并多个媒体文件(前提:输入流参数兼容),高效且避免重新编码。

  • 信息获取 获取视频时长等元信息方法(getVideoLength)。

  • 视频处理工具

    • 给视频添加右下角水印(支持中文,需 UTF-8 文本和字体文件)。
    • 保存视频的最后一帧为图片(返回 0 表示成功,其他为错误码或 FFmpeg 错误码)。
  • 初始化与库加载 提供 init() 方法用于初始化和加载 native 库(建议应用启动时调用)。


Maven 坐标 ​

xml
<dependency>
  <groupId>nexus.io</groupId>
  <artifactId>java-native-media</artifactId>
  <version>1.0.0</version>
</dependency>

Java API(关键方法说明与示例) ​

初始化 ​

首次使用NativeMedia类时会自动进行初始化,加载本地库与必要资源,无需显示调用

MP3 智能分片 ​

java
String[] segments = NativeMedia.splitMp3("/data/input/long_audio.mp3", 25L * 1024 * 1024);
for (String s : segments) {
  System.out.println("segment: " + s);
}

MP4 转 MP3 ​

java
String out = NativeMedia.mp4ToMp3("/data/video/input.mp4");
if (out.startsWith("Error:")) {
  System.err.println("转换失败:" + out);
} else {
  System.out.println("输出:" + out);
}

通用转 MP3(含插入静音) ​

java
String out1 = NativeMedia.toMp3("/data/audio/input.wav");
String out2 = NativeMedia.toMp3ForSilence("/data/audio/part.wav", 0.8 /* seconds */);

通用格式转换 ​

java
String out = NativeMedia.convertTo("/data/video/input.flv", "libmp3lame");
// 或者传入 "mp3" 作为容器名

通用拆分(任意格式) ​

java
String[] parts = NativeMedia.split("/data/video/input.mp4", 10 * 1024 * 1024);

HLS:一次性分片并追加到 playlist ​

java
String result = NativeMedia.splitVideoToHLS("./data/hls/playlist.m3u8", "/data/video/scene1.mp4", "./data/hls/segment_%03d.ts", 6);
System.out.println(result); // 成功或错误信息

HLS:持久会话(推荐用于长流程拼接) ​

java
long session = NativeMedia.initPersistentHls("./data/hls/playlist.m3u8", "./data/hls/segment_%03d.ts", 1, 6);

// 追加段
NativeMedia.appendVideoSegmentToHls(session, "/data/video/part1.mp4");
NativeMedia.appendVideoSegmentToHls(session, "/data/video/part2.mp4");

// 插入静音段
NativeMedia.insertSilentSegment(session, 1.5);

// 查询会话列表(JSON)
String sessionsJson = NativeMedia.listHlsSession();

// 结束并写入 ENDLIST
NativeMedia.finishPersistentHls(session, "./data/hls/playlist.m3u8");

// 或者直接释放会话(不写 ENDLIST)
NativeMedia.freeHlsSession(session);

合并(stream copy) ​

java
String[] inputs = { "/data/part1.ts", "/data/part2.ts" };
boolean ok = NativeMedia.merge(inputs, "/data/output/merged.mp4");
System.out.println("merge ok = " + ok);

其它实用方法 ​

java
double seconds = NativeMedia.getVideoLength("/data/video/input.mp4");
int ret = NativeMedia.saveLastFrame("/data/video/input.mp4", "/data/output/last_frame.jpg");
String res = NativeMedia.addWatermarkToVideo("/data/video/in.mp4", "/data/video/out_watermark.mp4", "版权信息", "/usr/share/fonts/truetype/arial.ttf");

返回值与错误处理 ​

  • 多数方法以字符串返回:成功返回输出路径或成功信息;失败通常以 Error: 前缀或非零状态码表示,请在业务代码中进行判定。
  • merge 返回 boolean,true 表示合并成功(FFmpeg 退出码 0)。
  • saveLastFrame 返回 0 表示成功;其它为 FFmpeg 错误码或自定义错误码。
  • HLS 会话相关方法返回状态字符串或 session 指针(long)。会话结束后请务必调用 finishPersistentHls 或 freeHlsSession 做清理,避免资源泄露。

部署与依赖安装 ​

构建/编译 JNI C 代码时需要开发包(headers 与 .so/.a 链接),运行时若使用预编译 native 库通常只需运行时库(非 -dev 包)。下面按平台列出常见安装方法与建议。

Debian / Ubuntu(构建与运行) ​

开发环境(包含头文件):

bash
sudo apt-get update
sudo apt-get install -y build-essential cmake pkg-config openjdk-8-jdk \
  libavcodec-dev libavformat-dev libavutil-dev libavfilter-dev libswresample-dev \
  libmp3lame-dev

仅运行时(使用已编译 native 二进制):

bash
sudo apt-get install -y ffmpeg libavcodec57 libavformat57 libavutil55 libswresample2 libmp3lame0

示例快捷命令(题中提及):

bash
sudo apt-get install libavcodec-dev libavformat-dev libavutil-dev libavfilter-dev libswresample-dev libmp3lame-dev -y

Fedora / CentOS / RHEL(通过 RPM Fusion / EPEL 提供 FFmpeg) ​

  • 启用 EPEL 与 RPM Fusion,然后安装 ffmpeg 与开发包(ffmpeg-devel、lame-devel 等)。
  • 示例(请根据发行版版本调整):
bash
sudo yum install -y epel-release
# 添加 RPM Fusion 并安装 ffmpeg/开发包,具体命令参考 rpmfusion.org
sudo yum install -y ffmpeg ffmpeg-devel lame-devel

Alpine Linux ​

bash
# 运行时
sudo apk add ffmpeg

# 构建/开发
sudo apk add build-base cmake pkgconfig openjdk11-jdk ffmpeg-dev libmp3lame-dev

macOS(Homebrew) ​

bash
xcode-select --install
brew install pkg-config cmake openjdk ffmpeg

Homebrew 的 ffmpeg 通常已包含主流编码器(如 libmp3lame);如需自定义编译请参阅 Homebrew 公式。

Windows ​

  • 运行与测试(推荐使用 Chocolatey 或 Scoop):
powershell
choco install ffmpeg
# 或
scoop install ffmpeg
  • 构建:推荐使用 MSYS2(mingw-w64)或 Visual Studio + vcpkg,根据选择安装相应的 ffmpeg/lame 开发包。

Docker(统一环境示例) ​

以下 Dockerfile 基于 JDK 8,并安装必要依赖(FFmpeg、libmp3lame0):

dockerfile
FROM litongjava/jdk:8u411-stable-slim

RUN apt-get update && apt-get install -y ffmpeg libmp3lame0

WORKDIR /app

COPY target/java-native-media-test-1.0.0.jar /app/

CMD ["java", "-jar", "java-native-media-test-1.0.0.jar"]

构建镜像命令:

bash
docker build -t litongjava/java-native-media-test:1.0.0 .

运行镜像示例:

bash
docker run --rm --name java-native-media-test -p 80:80 litongjava/java-native-media-test:1.0.0

测试 Controller(基于 tio-boot) ​

为了便于集成到 HTTP 服务中,本项目在 java-native-media-test 开源项目中提供了一系列基于 tio-boot 的 HTTP 请求处理器。下面是各功能的 Controller 示例,开发者可直接参考并集成到自己的项目中。

1. MP3 分片 Controller ​

java
package nexus.io.test.controller;

import nexus.io.tio.boot.http.TioRequestContext;
import nexus.io.tio.http.common.HttpRequest;
import nexus.io.tio.http.common.HttpResponse;
import nexus.io.tio.http.server.util.Resps;
import nexus.io.tio.utils.hutool.FileUtil;
import nexus.io.tio.utils.hutool.FilenameUtils;
import nexus.io.tio.utils.http.ContentTypeUtils;
import nexus.io.model.upload.UploadFile;
import nexus.io.media.NativeMedia;
import nexus.io.annotation.RequestPath;

import java.io.File;

@RequestPath("/media/splitMp3")
public class SplitMp3Controller {

  public HttpResponse splitMp3(HttpRequest request) {
    UploadFile uploadFile = request.getUploadFile("file");
    if (uploadFile == null) {
      return Resps.error("文件上传失败!");
    }

    byte[] data = uploadFile.getData();
    new File("upload").mkdirs();
    File file = new File("upload", uploadFile.getName());
    FileUtil.writeBytes(data, file);

    // 默认分片大小 25MB
    long splitSize = 25 * 1024 * 1024;
    String[] segments = NativeMedia.splitMp3(file.getAbsolutePath(), splitSize);

    StringBuilder sb = new StringBuilder("生成的分片文件:<br/>");
    for (String seg : segments) {
      sb.append(seg).append("<br/>");
    }
    return Resps.html(sb.toString());
  }
}

2. MP4 转 MP3 Controller ​

java
package nexus.io.test.controller;

import nexus.io.tio.boot.http.TioRequestContext;
import nexus.io.tio.http.common.HttpRequest;
import nexus.io.tio.http.common.HttpResponse;
import nexus.io.tio.http.server.util.Resps;
import nexus.io.media.NativeMedia;
import nexus.io.annotation.RequestPath;
import nexus.io.model.upload.UploadFile;
import nexus.io.tio.utils.hutool.FileUtil;

import java.io.File;

@RequestPath("/media/mp4ToMp3")
public class Mp4ToMp3Controller {

  public HttpResponse mp4ToMp3(HttpRequest request) {
    UploadFile uploadFile = request.getUploadFile("file");
    if (uploadFile == null) {
      return Resps.error("文件上传失败!");
    }

    byte[] data = uploadFile.getData();
    new File("upload").mkdirs();
    File file = new File("upload", uploadFile.getName());
    FileUtil.writeBytes(data, file);

    String result = NativeMedia.mp4ToMp3(file.getAbsolutePath());
    if (result.startsWith("Error:")) {
      return Resps.error("转换失败:" + result);
    }
    return Resps.text("转换成功!输出文件:" + result);
  }
}

3. 通用格式转换 Controller ​

java
package nexus.io.test.controller;

import nexus.io.tio.boot.http.TioRequestContext;
import nexus.io.tio.http.common.HttpRequest;
import nexus.io.tio.http.common.HttpResponse;
import nexus.io.tio.http.server.util.Resps;
import nexus.io.media.NativeMedia;
import nexus.io.annotation.RequestPath;
import nexus.io.model.upload.UploadFile;
import nexus.io.tio.utils.hutool.FileUtil;

import java.io.File;

@RequestPath("/media/convertTo")
public class ConvertToController {

  public HttpResponse convertTo(HttpRequest request) {
    UploadFile uploadFile = request.getUploadFile("file");
    if (uploadFile == null) {
      return Resps.error("文件上传失败!");
    }

    byte[] data = uploadFile.getData();
    new File("upload").mkdirs();
    File file = new File("upload", uploadFile.getName());
    FileUtil.writeBytes(data, file);

    // 例如转换为 MP3(可以传入 "libmp3lame" 或 "mp3")
    String outputPath = NativeMedia.convertTo(file.getAbsolutePath(), "libmp3lame");
    if (outputPath.startsWith("Error:")) {
      return Resps.error("转换失败:" + outputPath);
    }
    return Resps.text("转换成功!输出文件:" + outputPath);
  }
}

4. 通用格式拆分 Controller ​

java
package nexus.io.test.controller;

import nexus.io.tio.boot.http.TioRequestContext;
import nexus.io.tio.http.common.HttpRequest;
import nexus.io.tio.http.common.HttpResponse;
import nexus.io.tio.http.server.util.Resps;
import nexus.io.media.NativeMedia;
import nexus.io.annotation.RequestPath;
import nexus.io.model.upload.UploadFile;
import nexus.io.tio.utils.hutool.FileUtil;

import java.io.File;

@RequestPath("/media/split")
public class SplitController {

  public HttpResponse split(HttpRequest request) {
    UploadFile uploadFile = request.getUploadFile("file");
    if (uploadFile == null) {
      return Resps.error("文件上传失败!");
    }

    byte[] data = uploadFile.getData();
    new File("upload").mkdirs();
    File file = new File("upload", uploadFile.getName());
    FileUtil.writeBytes(data, file);

    // 按 10MB 拆分
    String[] segments = NativeMedia.split(file.getAbsolutePath(), 10 * 1024 * 1024);

    StringBuilder sb = new StringBuilder("生成的拆分文件:<br/>");
    for (String seg : segments) {
      sb.append(seg).append("<br/>");
    }
    return Resps.html(sb.toString());
  }
}

5. 支持格式查询 Controller ​

java
package nexus.io.test.controller;

import nexus.io.tio.boot.http.TioRequestContext;
import nexus.io.tio.http.common.HttpRequest;
import nexus.io.tio.http.common.HttpResponse;
import nexus.io.tio.http.server.util.Resps;
import nexus.io.media.NativeMedia;
import nexus.io.annotation.RequestPath;

@RequestPath("/media/supportFormats")
public class SupportFormatsController {

  public HttpResponse supportFormats(HttpRequest request) {
    String[] formats = NativeMedia.supportFormats();
    StringBuilder sb = new StringBuilder("支持的格式和编码器列表:<br/>");
    for (String format : formats) {
      sb.append(format).append("<br/>");
    }
    return Resps.html(sb.toString());
  }
}

测试 ​

  • /media/supportFormats
  • /media/splitMp3
  • /media/split
  • /media/convertTo
  • /media/mp4ToMp3
  • /media/toMp3

构建 & 本地调试提示 ​

  1. JNI 编译时需要 JDK 的头文件(例如 openjdk-devel / default-jdk)。
  2. 确保 FFmpeg 的开发包(-dev / -devel / MSYS2/vcpkg 对应包)可用,或在 CMake 中提供正确的 pkg-config 路径。
  3. 运行时需让系统能找到 native 库(LD_LIBRARY_PATH / DYLD_LIBRARY_PATH / PATH)。
  4. addWatermarkToVideo 使用中文水印时需传入支持中文的字体文件路径(UTF-8),否则可能乱码或无法渲染。
  5. 若为多平台发布,建议为每个平台预构建 native 二进制并在 NativeMedia.init() 根据 os.name/os.arch 动态加载。

常见问题(FAQ) ​

  • 只运行不编译是否需要 -dev 包? 运行时通常只需运行时库(非 -dev)。若 native 库在运行时需要某些开发库的动态链接,请安装对应运行时包。编译 JNI 代码必须安装 -dev 包(头文件与静态链接符号)。

  • Windows 下如何获得 libmp3lame 的头文件? 可通过 MSYS2 的 mingw 包或 vcpkg 获取,或下载 FFmpeg 源码并自行编译。

  • HLS 会话指针(sessionPtr)是否线程安全? sessionPtr 由 native 层维护,为安全起见应在 Java 层做并发保护或保证对同一会话的调用序列化;若需跨线程并发操作,建议在 native 层实现线程安全保护。

  • 如何诊断转换失败?

    • 检查方法返回的错误字符串(通常包含 FFmpeg 的输出)。
    • 确认系统 FFmpeg 库版本与 native 库编译时版本兼容。
    • 检查文件读写权限与字体路径(中文水印)。

许可证与项目定位 ​

本项目聚焦特定场景的垂直优化,不是通用多媒体处理框架。如需更多高级功能(GPU 加速、复杂滤镜、跨平台 GUI 等),建议参考 FFmpeg 原生工具链、gstreamer 或商业 SDK。

MIT Licensed