Java集成Ffmpeg实现视频转M3U8

Ffmpeg是什么

FFmpeg 强大专用于处理音视频的开源库,包含了先进的音视频编解码库,提供了录制、转换以及流传输音视频的完整跨平台解决方案。
既可以使用它的API对音视频进行处理,也可以使用它提供的工具,如 ffmpeg, ffplay, ffprobe,来编辑音视频文件。

——————引用知乎:1. FFmpeg基本常识及编码流程 - 知乎 (zhihu.com)

FFmpeg是一套可以用来记录、转换数字音频、视频,并能将其转化为流的开源计算机程序。采用LGPL或GPL许可证。它提供了录制、转换以及流化音视频的完整解决方案。它包含了非常先进的音频/视频编解码库libavcodec,为了保证高可移植性和编解码质量,libavcodec里很多code都是从头开发的。

——————引用知乎:30分带你从认识FFmpeg到玩转FFmpeg - 知乎 (zhihu.com)

为什么要使用Ffmpeg

Ffmpeg是一个功能十分强大的视频编解码库(音视频编码解读:引用知乎:[音视频编解码基础 - 知乎 (zhihu.com)](https://zhuanlan.zhihu.com/p/28058109#:~:text=音视频编解码基础 1 流媒体技术的基本过程 总体上说,视频从产生到传递到观看者之间的过程主要分为这么几个阶段:录制—编码—传输—解码—播放 录制:即视频的制作者利用各种摄像设备,将现实中的一些连续的场景片段记录下来。 编码:对录制好的视频进行格式化处理,以方便在网络上传输。 主要有对视频和音频分别进行压缩编码、将音视频进行打包封装两个步骤。 …,和视频一样,音频编码也是为了将音频原始数据转换为音频码流,以便在网络传输。 常见的有: … 6 流媒体协议 RTP RTCP )),我们以下只使用它的其中一个功能,就是将音视频以某种编码格式编码后再切割成M3U8格式。

因为传统视频都是Mp4、MKV、Avi等格式,视频不长时还好说;视频很长时,浏览器或App从网络上加载视频时,需要将一部完整的视频加载下来后,视频才能播放,这无疑是十分耗费网络资源,又浪费时间;为什么不能边看边加载呢,或者我需要看哪一段就加载哪一段,于是M3U8流媒体格式便出来了。

M3U8:其实是一系列视频文件的索引,通过该索引文件,查找需要播放的某一段视频。

HLS:是具体的视频格式,比如将一个十分钟的视频按每10秒分割一段,那么将产生60段长度很短的视频,这种视频的格式就是HLS格式,为什么不用Mp4呢,因为Mp4在两段视频加载间隔会发生画面撕裂,影响观感。而储存这些HLS视频的索引文件就叫M3U8文件。

Java使用Ffmpeg

因为Ffmpeg是用C++编写的一个命令行软件,在没有官方提供的JNI程序的情况下,Java是无法直接调用Ffmpeg的;需要使用Java操作命令行的类以操作命令行的方式操作Ffmpeg;又由于没有对应的jar包支持,需要将网络上下载对应操作系统的Ffmpeg实现,将其封装成对应的jar包。

调用方式

导入依赖

 <!-- ffmpeg视频解析包 -->
<dependency>
<groupId>ws.schild</groupId>
<artifactId>jave-core</artifactId>
<exclusions>
<exclusion>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- ffmpeg win64版本 -->
<dependency>
<groupId>ws.schild</groupId>
<artifactId>jave-nativebin-win64</artifactId>
</dependency>
<!-- ffmpeg linux64版本 -->
<dependency>
<groupId>ws.schild</groupId>
<artifactId>jave-nativebin-linux64</artifactId>
</dependency>

执行相关代码

// 获取ffmpeg的绝对位置
DefaultFFMPEGLocator defaultFFMPEGLocator = new DefaultFFMPEGLocator();
String ffmpegPath = defaultFFMPEGLocator.getExecutablePath();


/**
* 命令详解:
* ffmpeg -i xxx/xxx.mp4 -c:v libx264 -c:a aac -hls_time 10 -hls_list_size 0 -strict -2 -s 1920x1080 -f hls -threads 10 -preset ultrafast xxx/xxx.m3u8
* 1. ffmpeg 指ffmpeg应用的路径
* 2. -i 接需要编码的视频地址
* 3. -c:v libx264 -c:a aac 指视频按h.254编码,音频按aac编码
* 4. -hls_time 10 表示分片大小,既视频按10秒段进行切分
* 5. -hls_list_size 0 -strict -2 表示视频按10秒每段切分,为了控制大小,会发生2秒左右的偏移
* 6. -s 1920x1080 输出视频的分辨率
* 7. -f hls 视频输出格式是hls
* 8. -threads 10 -preset ultrafast 开启多线程编码,10个线程,并非越多越好,10个左右刚好合适
* 9. xxx/xxx.m3u8 输出视频的位置,格式m3u8
*/
/**
* 执行命令
*
* @param inVideoPath 输入视频的地址
* @param outVideoPath 输出视频的地址
*/
private int execute(String inVideoPath, String outVideoPath) {
log.info("ffmepg位置:{}", ffmpegPath);
// ffmpeg程序位置
String cmd = new StringBuilder(ffmpegPath)
.append(" -i ")
// 输入视频位置
.append(inVideoPath)
.append(" -c:v libx264 -c:a aac -hls_time ")
// 分片大小,每个分片大小20秒
.append(20)
.append(" -hls_list_size 0 -strict -2 -s 1920x1080 -f hls -threads ")
// 线程数,10个线程,10个左右最优
.append(10)
.append(" -preset ultrafast ")
// 输出位置
.append(outVideoPath)
.toString();

Runtime runtime = Runtime.getRuntime();
Process ffmpeg = null;
InputStream errorIs = null;
try {
ffmpeg = runtime.exec(cmd);
// 错误日志
errorIs = ffmpeg.getErrorStream();
// info日志
OutputStream os = ffmpeg.getOutputStream();
// 在执行过程中执行y,代表统一执行
os.write("y".getBytes("UTF-8"));
os.close();
} catch (IOException e) {
e.printStackTrace();
}
// false,关闭流信息,确保ffmpeg执行完毕后关闭
int res = close(ffmpeg, errorIs, false);

return res;
}


/**
* 命令详解:
* ffmpeg -i xxx/xxx.mp4 -ss 00:00:01 -frames:v 1 xxx/xxx.png
* 1. ffmpeg 指ffmpeg应用的路径
* 2. -i 接需要编码的视频地址
* 3. -ss 00:00:01 -frames:v 1 获取视频第一秒的第一帧为视频封面
* 4. xxx/xxx.png 封面图片输出地址
*/
/**
* 获取视频封面与时长
*
* @param inVideoPath 输入视频地址
* @param outCoverPath 输出封面地址
* @return duration 0:执行失败
*/
private int getVideoInfo(String inVideoPath, String outCoverPath) {
String cmd = new StringBuilder(ffmpegPath)
.append(" -i ")
// 输入视频位置
.append(inVideoPath)
// 获取视频第一帧为封面
.append(" -ss 00:00:01 -frames:v 1 ")
.append(outCoverPath)
.toString();

Runtime runtime = Runtime.getRuntime();
Process ffmpeg = null;
InputStream errorIs = null;
try {
ffmpeg = runtime.exec(cmd);
// 开启日志
errorIs = ffmpeg.getErrorStream();
// 输入指令y,表示同意跳过执行
OutputStream os = ffmpeg.getOutputStream();
os.write("y".getBytes("UTF-8"));
os.close();
} catch (IOException e) {
e.printStackTrace();
}
int duration = close(ffmpeg, errorIs, true);

return duration;
}


/**
* 打印过程并关闭
*
* @param ffmpeg
* @param errorIs
* @return duration
*/
private int close(Process ffmpeg, InputStream errorIs, boolean printLog) {
//打印过程
StringBuilder info = new StringBuilder();
try {
int len = 0;
while (true) {
if (!((len = errorIs.read()) != -1)) break;
if (printLog) {
info.append((char) len);
}
}
} catch (IOException e) {
e.printStackTrace();
}
// 正则匹配时长信息
int duration = 0;
if (printLog) {
Matcher matcher = Pattern.compile("(?i)duration:\\s*([0-9\\:\\.]+)")
.matcher(info.toString());
while (matcher.find()) {
String group = matcher.group(1);
duration = getSeconds4Str(group);
break;
}
log.info("时长(s):{}", duration);
}

info = null;
if (errorIs != null) {
try {
errorIs.close();
} catch (Throwable t) {
log.warn("关闭输入流失败", t);
}
}
// 确保命令执行完毕
try {
ffmpeg.waitFor();
} catch (InterruptedException ex) {
log.error("在等待过程中强制关闭:{}", ex);
}
int res = ffmpeg.exitValue();
if (res != 0) {
duration = 0;
}

if (ffmpeg != null) {
ffmpeg.destroy();
ffmpeg = null;
}
return duration;
}


/**
* 字符时间格式化为秒
*
* @param durationStr
* @return
*/
private int getSeconds4Str(String durationStr) {
int duration = 0;
if (null != durationStr && durationStr.length() > 0) {
String[] durationStrArr = durationStr.split("\\:");
String hour = durationStrArr[0];
String minute = durationStrArr[1];
//特殊
String second = "";
String secondTmp = durationStrArr[2];
if (secondTmp.contains(".")) {
String[] seconedTmpArr = secondTmp.split("\\.");
second = seconedTmpArr[0];
} else {
second = secondTmp;
}
try {
duration = Integer.parseInt(hour) * 3600 + Integer.parseInt(minute) * 60 + Integer.parseInt(second);
} catch (Exception e) {
return 0;
}
}
return duration;
}

写在最后

  • 以上只是表示Java集成Ffmpeg将视频转成m3u8
  • 其实SpringBoot集成Ffmpeg也是跟以上差不多,只是将代码执行部分放入Spring容器,将部分配置写入配置文件
  • 像视频转码切分这种十分消耗系统资源与时间的操作,将操作放入线程池中
  • 以下简略说明下Web应用从上传视频到将剪切好的视频上传到OSS的具体过程
    • 前端上传的视频放入后端的某个临时目录
    • 上传完毕后调用ffmpeg视频切割方法将视频转成m3u8,转好后放入后端的另一个临时目录(该过程丢到线程池中执行)
    • 删除存储原版视频的目录,将存储m3u8视频目录中的所有.m3u8和.hls文件上传到OSS(该过程也可以丢到线程池中执行)
    • 在上传过程中提前构造好视频目录地址,存入数据库
    • 上传完毕,提示视频转码完毕