概述
视频上传到本地之后(此处可分片上传到本地,然后合并),使用ffmpeg对视频处理成m3u8文件,暂时只测试了avi和mp4格式的文件。
代码
pom.xml
<project xmlns="http://maven.apache.org/pom/4.0.0"
xmlns:xsi="http://www.w3.org/2001/xmlschema-instance"
xsi:schemalocation="http://maven.apache.org/pom/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelversion>4.0.0</modelversion>
<groupid>io.springboot</groupid>
<artifactid>springboot-ffmpeg-demo</artifactid>
<version>0.0.1-snapshot</version>
<parent>
<groupid>org.springframework.boot</groupid>
<artifactid>spring-boot-starter-parent</artifactid>
<version>2.5.4</version>
<relativepath /> <!-- lookup parent from repository -->
</parent>
<dependencies>
<dependency>
<groupid>org.springframework.boot</groupid>
<artifactid>spring-boot-starter-test</artifactid>
<scope>test</scope>
</dependency>
<dependency>
<groupid>org.junit.vintage</groupid>
<artifactid>junit-vintage-engine</artifactid>
<scope>test</scope>
</dependency>
<dependency>
<groupid>org.springframework.boot</groupid>
<artifactid>spring-boot-starter-web</artifactid>
<exclusions>
<exclusion>
<groupid>org.springframework.boot</groupid>
<artifactid>spring-boot-starter-tomcat</artifactid>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupid>org.springframework.boot</groupid>
<artifactid>spring-boot-starter-undertow</artifactid>
</dependency>
<dependency>
<groupid>commons-codec</groupid>
<artifactid>commons-codec</artifactid>
</dependency>
<dependency>
<groupid>com.google.code.gson</groupid>
<artifactid>gson</artifactid>
</dependency>
</dependencies>
<build>
<finalname>${project.artifactid}</finalname>
<plugins>
<plugin>
<groupid>org.springframework.boot</groupid>
<artifactid>spring-boot-maven-plugin</artifactid>
<configuration>
<executable>true</executable>
</configuration>
</plugin>
</plugins>
</build>
</project>
ffmpeg
ffmpegutils
package com.demo.ffmpeg;
import java.io.bufferedreader;
import java.io.file;
import java.io.ioexception;
import java.io.inputstreamreader;
import java.nio.charset.standardcharsets;
import java.nio.file.files;
import java.nio.file.path;
import java.nio.file.paths;
import java.nio.file.standardopenoption;
import java.security.nosuchalgorithmexception;
import java.util.arraylist;
import java.util.list;
import javax.crypto.keygenerator;
import org.apache.commons.codec.binary.hex;
import org.slf4j.logger;
import org.slf4j.loggerfactory;
import org.springframework.util.stringutils;
import com.google.gson.gson;
public class ffmpegutils {
private static final logger logger = loggerfactory.getlogger(ffmpegutils.class);
// 跨平台换行符
private static final string line_separator = system.getproperty("line.separator");
/**
* 生成随机16个字节的aeskey
* @return
*/
private static byte[] genaeskey () {
try {
keygenerator keygenerator = keygenerator.getinstance("aes");
keygenerator.init(128);
return keygenerator.generatekey().getencoded();
} catch (nosuchalgorithmexception e) {
return null;
}
}
/**
* 在指定的目录下生成key_info, key文件,返回key_info文件
* @param folder
* @throws ioexception
*/
private static path genkeyinfo(string folder) throws ioexception {
// aes 密钥
byte[] aeskey = genaeskey();
// aes 向量
string iv = hex.encodehexstring(genaeskey());
// key 文件写入
path keyfile = paths.get(folder, "key");
files.write(keyfile, aeskey, standardopenoption.create, standardopenoption.truncate_existing);
// key_info 文件写入
stringbuilder stringbuilder = new stringbuilder();
stringbuilder.append("key").append(line_separator); // m3u8加载key文件网络路径
stringbuilder.append(keyfile.tostring()).append(line_separator); // ffmeg加载key_info文件路径
stringbuilder.append(iv); // ase 向量
path keyinfo = paths.get(folder, "key_info");
files.write(keyinfo, stringbuilder.tostring().getbytes(), standardopenoption.create, standardopenoption.truncate_existing);
return keyinfo;
}
/**
* 指定的目录下生成 master index.m3u8 文件
* @param filename master m3u8文件地址
* @param indexpath 访问子index.m3u8的路径
* @param bandwidth 流码率
* @throws ioexception
*/
private static void genindex(string file, string indexpath, string bandwidth) throws ioexception {
stringbuilder stringbuilder = new stringbuilder();
stringbuilder.append("#extm3u").append(line_separator);
stringbuilder.append("#ext-x-stream-inf:bandwidth=" + bandwidth).append(line_separator); // 码率
stringbuilder.append(indexpath);
files.write(paths.get(file), stringbuilder.tostring().getbytes(standardcharsets.utf_8), standardopenoption.create, standardopenoption.truncate_existing);
}
/**
* 转码视频为m3u8
* @param source 源视频
* @param destfolder 目标文件夹
* @param config 配置信息
* @throws ioexception
* @throws interruptedexception
*/
public static void transcodetom3u8(string source, string destfolder, transcodeconfig config) throws ioexception, interruptedexception {
// 判断源视频是否存在
if (!files.exists(paths.get(source))) {
throw new illegalargumentexception("文件不存在:" + source);
}
// 创建工作目录
path workdir = paths.get(destfolder, "ts");
files.createdirectories(workdir);
// 在工作目录生成keyinfo文件
path keyinfo = genkeyinfo(workdir.tostring());
// 构建命令
list<string> commands = new arraylist<>();
commands.add("ffmpeg");
commands.add("-i") ;commands.add(source); // 源文件
commands.add("-c:v") ;commands.add("libx264"); // 视频编码为h264
commands.add("-c:a") ;commands.add("copy"); // 音频直接copy
commands.add("-hls_key_info_file") ;commands.add(keyinfo.tostring()); // 指定密钥文件路径
commands.add("-hls_time") ;commands.add(config.gettsseconds()); // ts切片大小
commands.add("-hls_playlist_type") ;commands.add("vod"); // 点播模式
commands.add("-hls_segment_filename") ;commands.add("%06d.ts"); // ts切片文件名称
if (stringutils.hastext(config.getcutstart())) {
commands.add("-ss") ;commands.add(config.getcutstart()); // 开始时间
}
if (stringutils.hastext(config.getcutend())) {
commands.add("-to") ;commands.add(config.getcutend()); // 结束时间
}
commands.add("index.m3u8"); // 生成m3u8文件
// 构建进程
process process = new processbuilder()
.command(commands)
.directory(workdir.tofile())
.start()
;
// 读取进程标准输出
new thread(() -> {
try (bufferedreader bufferedreader = new bufferedreader(new inputstreamreader(process.getinputstream()))) {
string line = null;
while ((line = bufferedreader.readline()) != null) {
logger.info(line);
}
} catch (ioexception e) {
}
}).start();
// 读取进程异常输出
new thread(() -> {
try (bufferedreader bufferedreader = new bufferedreader(new inputstreamreader(process.geterrorstream()))) {
string line = null;
while ((line = bufferedreader.readline()) != null) {
logger.info(line);
}
} catch (ioexception e) {
}
}).start();
// 阻塞直到任务结束
if (process.waitfor() != 0) {
throw new runtimeexception("视频切片异常");
}
// 切出封面
if (!screenshots(source, string.join(file.separator, destfolder, "poster.jpg"), config.getposter())) {
throw new runtimeexception("封面截取异常");
}
// 获取视频信息
mediainfo mediainfo = getmediainfo(source);
if (mediainfo == null) {
throw new runtimeexception("获取媒体信息异常");
}
// 生成index.m3u8文件
genindex(string.join(file.separator, destfolder, "index.m3u8"), "ts/index.m3u8", mediainfo.getformat().getbitrate());
// 删除keyinfo文件
files.delete(keyinfo);
}
/**
* 获取视频文件的媒体信息
* @param source
* @return
* @throws ioexception
* @throws interruptedexception
*/
public static mediainfo getmediainfo(string source) throws ioexception, interruptedexception {
list<string> commands = new arraylist<>();
commands.add("ffprobe");
commands.add("-i") ;commands.add(source);
commands.add("-show_format");
commands.add("-show_streams");
commands.add("-print_format") ;commands.add("json");
process process = new processbuilder(commands)
.start();
mediainfo mediainfo = null;
try (bufferedreader bufferedreader = new bufferedreader(new inputstreamreader(process.getinputstream()))) {
mediainfo = new gson().fromjson(bufferedreader, mediainfo.class);
} catch (ioexception e) {
e.printstacktrace();
}
if (process.waitfor() != 0) {
return null;
}
return mediainfo;
}
/**
* 截取视频的指定时间帧,生成图片文件
* @param source 源文件
* @param file 图片文件
* @param time 截图时间 hh:mm:ss.[sss]
* @throws ioexception
* @throws interruptedexception
*/
public static boolean screenshots(string source, string file, string time) throws ioexception, interruptedexception {
list<string> commands = new arraylist<>();
commands.add("ffmpeg");
commands.add("-i") ;commands.add(source);
commands.add("-ss") ;commands.add(time);
commands.add("-y");
commands.add("-q:v") ;commands.add("1");
commands.add("-frames:v") ;commands.add("1");
commands.add("-f"); ;commands.add("image2");
commands.add(file);
process process = new processbuilder(commands)
.start();
// 读取进程标准输出
new thread(() -> {
try (bufferedreader bufferedreader = new bufferedreader(new inputstreamreader(process.getinputstream()))) {
string line = null;
while ((line = bufferedreader.readline()) != null) {
logger.info(line);
}
} catch (ioexception e) {
}
}).start();
// 读取进程异常输出
new thread(() -> {
try (bufferedreader bufferedreader = new bufferedreader(new inputstreamreader(process.geterrorstream()))) {
string line = null;
while ((line = bufferedreader.readline()) != null) {
logger.error(line);
}
} catch (ioexception e) {
}
}).start();
return process.waitfor() == 0;
}
}
mediainfo
package com.demo.ffmpeg;
import java.util.list;
import com.google.gson.annotations.serializedname;
public class mediainfo {
public static class format {
@serializedname("bit_rate")
private string bitrate;
public string getbitrate() {
return bitrate;
}
public void setbitrate(string bitrate) {
this.bitrate = bitrate;
}
}
public static class stream {
@serializedname("index")
private int index;
@serializedname("codec_name")
private string codecname;
@serializedname("codec_long_name")
private string codeclongame;
@serializedname("profile")
private string profile;
}
// ----------------------------------
@serializedname("streams")
private list<stream> streams;
@serializedname("format")
private format format;
public list<stream> getstreams() {
return streams;
}
public void setstreams(list<stream> streams) {
this.streams = streams;
}
public format getformat() {
return format;
}
public void setformat(format format) {
this.format = format;
}
}
transcodeconfig
package com.demo.ffmpeg;
public class transcodeconfig {
private string poster; // 截取封面的时间 hh:mm:ss.[sss]
private string tsseconds; // ts分片大小,单位是秒
private string cutstart; // 视频裁剪,开始时间 hh:mm:ss.[sss]
private string cutend; // 视频裁剪,结束时间 hh:mm:ss.[sss]
public string getposter() {
return poster;
}
public void setposter(string poster) {
this.poster = poster;
}
public string gettsseconds() {
return tsseconds;
}
public void settsseconds(string tsseconds) {
this.tsseconds = tsseconds;
}
public string getcutstart() {
return cutstart;
}
public void setcutstart(string cutstart) {
this.cutstart = cutstart;
}
public string getcutend() {
return cutend;
}
public void setcutend(string cutend) {
this.cutend = cutend;
}
@override
public string tostring() {
return "transcodeconfig [poster=" + poster + ", tsseconds=" + tsseconds + ", cutstart=" + cutstart + ", cutend="
+ cutend + "]";
}
}
application.yml
server:
port: 80
#logging:
# level:
# "root": debug
app:
# 存储转码视频的文件夹
video-folder: d:\video
spring:
servlet:
multipart:
enabled: true
# 不限制文件大小
max-file-size: -1
# 不限制请求体大小
max-request-size: -1
# 临时io目录
location: "${java.io.tmpdir}"
# 不延迟解析
resolve-lazily: false
# 超过1mb,就io到临时目录
file-size-threshold: 1mb
web:
resources:
static-locations:
- "classpath:/static/"
- "file:${app.video-folder}" # 把视频文件夹目录,添加到静态资源目录列表
application
@springbootapplication
public class application {
public static void main(string[] args) {
springapplication.run(application.class, args);
}
}
uploadcontroller
package com.demo.web.controller;
import java.io.ioexception;
import java.nio.file.files;
import java.nio.file.path;
import java.nio.file.paths;
import java.time.localdate;
import java.time.format.datetimeformatter;
import java.util.hashmap;
import java.util.map;
import org.slf4j.logger;
import org.slf4j.loggerfactory;
import org.springframework.beans.factory.annotation.value;
import org.springframework.http.httpstatus;
import org.springframework.http.responseentity;
import org.springframework.web.bind.annotation.postmapping;
import org.springframework.web.bind.annotation.requestmapping;
import org.springframework.web.bind.annotation.requestpart;
import org.springframework.web.bind.annotation.restcontroller;
import org.springframework.web.multipart.multipartfile;
import com.demo.ffmpeg.ffmpegutils;
import com.demo.ffmpeg.transcodeconfig;
@restcontroller
@requestmapping("/upload")
public class uploadcontroller {
private static final logger logger = loggerfactory.getlogger(uploadcontroller.class);
@value("${app.video-folder}")
private string videofolder;
private path tempdir = paths.get(system.getproperty("java.io.tmpdir"));
/**
* 上传视频进行切片处理,返回访问路径
* @param video
* @param transcodeconfig
* @return
* @throws ioexception
*/
@postmapping
public object upload (@requestpart(name = "file", required = true) multipartfile video,
@requestpart(name = "config", required = true) transcodeconfig transcodeconfig) throws ioexception {
logger.info("文件信息:title={}, size={}", video.getoriginalfilename(), video.getsize());
logger.info("转码配置:{}", transcodeconfig);
// 原始文件名称,也就是视频的标题
string title = video.getoriginalfilename();
// io到临时文件
path tempfile = tempdir.resolve(title);
logger.info("io到临时文件:{}", tempfile.tostring());
try {
video.transferto(tempfile);
// 删除后缀
title = title.substring(0, title.lastindexof("."));
// 按照日期生成子目录
string today = datetimeformatter.ofpattern("yyyymmdd").format(localdate.now());
// 尝试创建视频目录
path targetfolder = files.createdirectories(paths.get(videofolder, today, title));
logger.info("创建文件夹目录:{}", targetfolder);
files.createdirectories(targetfolder);
// 执行转码操作
logger.info("开始转码");
try {
ffmpegutils.transcodetom3u8(tempfile.tostring(), targetfolder.tostring(), transcodeconfig);
} catch (exception e) {
logger.error("转码异常:{}", e.getmessage());
map<string, object> result = new hashmap<>();
result.put("success", false);
result.put("message", e.getmessage());
return responseentity.status(httpstatus.internal_server_error).body(result);
}
// 封装结果
map<string, object> videoinfo = new hashmap<>();
videoinfo.put("title", title);
videoinfo.put("m3u8", string.join("/", "", today, title, "index.m3u8"));
videoinfo.put("poster", string.join("/", "", today, title, "poster.jpg"));
map<string, object> result = new hashmap<>();
result.put("success", true);
result.put("data", videoinfo);
return result;
} finally {
// 始终删除临时文件
files.delete(tempfile);
}
}
}
index.html
在resources/static/index.html
<html lang="en">
<head>
<meta charset="utf-8">
<title>title</title>
<script src="https://cdn.jsdelivr.net/hls.js/latest/hls.min.js"></script>
</head>
<body>
选择转码文件: <input name="file" type="file" accept="video/*" onchange="upload(event)">
<hr/>
<video id="video" width="500" height="400" controls="controls"></video>
</body>
<script>
const video = document.getelementbyid('video');
function upload (e){
let files = e.target.files
if (!files) {
return
}
// todo 转码配置这里固定死了
var transcodeconfig = {
poster: "00:00:00.001", // 截取第1毫秒作为封面
tsseconds: 15,
cutstart: "",
cutend: ""
}
// 执行上传
let formdata = new formdata();
formdata.append("file", files[0])
formdata.append("config", new blob([json.stringify(transcodeconfig)], {type: "application/json; charset=utf-8"}))
fetch('/upload', {
method: 'post',
body: formdata
})
.then(resp => resp.json())
.then(message => {
if (message.success){
// 设置封面
video.poster = message.data.poster;
// 渲染到播放器
var hls = new hls();
hls.loadsource(message.data.m3u8);
hls.attachmedia(video);
} else {
alert("转码异常,详情查看控制台");
console.log(message.message);
}
})
.catch(err => {
alert("转码异常,详情查看控制台");
throw err
})
}
</script>
</html>
测试

avi格式视频转码m3u8
01-jvm内存与垃圾回收篇概述 // 文件夹名称就是视频标题
|-index.m3u8 // 主m3u8文件,里面可以配置多个码率的播放地址
|-poster.jpg // 截取的封面图片
|-ts // 切片目录
|-index.m3u8 // 切片播放索引
|-key // 播放需要解密的aes key

mp4格式视频转码m3u8

以上就是springboot使用ffmpeg实现m3u8切片转码播放的详细内容,更多关于springboot m3u8转码播放的资料请关注代码网其它相关文章!
发表评论