传统视频上传是将视频完整上传至服务器,读取视频时需要加载整个视频,知道将完整视频读取后才会就行播放。而使用视频切片后会将原视频切片,分割为一个.m3u8与若干.ts文件,当对视频读取时,会按照时间节点通过.m3u8中的文件地址读取相应的.ts文件,大大减少了视频加载时的等待时间。
本功能用了两种方式。第一种是通过本地安装ffmpeg软件进行视频切割,第二种是通过应用ffmpeg依赖进行视频切割。
安装ffmpeg方式
安装ffmpeg
工具类完整代码
import com.liuniu.common.utils.StringUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;
/**
* mp4转换m3u8工具类
**/
@Slf4j
@Component
public class ConvertM3U8Api {
// ffmPeg.exe的目录
private static final String ffmPegPath = "e:\\ffmPeg\\bin\\ffmPeg";
public static boolean convertOss(String folderUrl, String fileName) {
if (!checkFile(folderUrl + fileName)) {
System.out.println("文件不存在!");
return false;
}
//验证文件后缀
String suffix = StringUtils.substringAfter(fileName, ".");
String fileFullName = StringUtils.substringBefore(fileName, ".");
if (!validFileType(suffix)) {
return false;
}
return processM3U8(folderUrl, fileName, fileFullName);
}
/**
* 验证上传文件后缀
*
* @param type 类型
* @return 是否为mp4
*/
private static boolean validFileType(String type) {
return "mp4".equals(type);
}
/**
* 验证是否是文件格式
*
* @param path 路径
* @return 是否存在
*/
private static boolean checkFile(String path) {
File file = new File(path);
return file.isFile();
}
//
/**
* ffmPeg程序转换m3u8
* ffmPeg -i vue.mp4 -c:v libx264 -hls_time 20 -hls_list_size 0 -c:a aac -strict -2 -f hls vue.m3u8
* ffmPeg能解析的格式:(asx,asf,mpg,wmv,3gp,mp4,mov,avi,flv等)
*
* @param folderUrl 文件路径
* @param fileName 名称
* @param fileFullName 全名
* @return 是否成功
*/
private static boolean processM3U8(String folderUrl, String fileName, String fileFullName) {
//这里就写入执行语句就可以了
List<String> commend = new ArrayList<>();
commend.add(ffmPegPath);
commend.add(" -i");
commend.add(folderUrl + fileName);
commend.add("-c:v");
commend.add("libx264");
commend.add("-hls_time");
commend.add("5");
commend.add("-hls_list_size");
commend.add("0");
commend.add("-c:a");
commend.add("aac");
commend.add("-strict");
commend.add("-2");
commend.add("-f");
commend.add("hls");
commend.add(folderUrl + fileFullName + ".m3u8");
try {
ProcessBuilder builder = new ProcessBuilder();//java
builder.command(commend);
Process p = builder.start();
int i = doWaitFor(p);
log.info("***i=【{}】***", i);
p.destroy();
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 监听ffmPeg运行过程
*
* @param p 进程
* @return 直接结果
*/
public static int doWaitFor(Process p) {
InputStream in = null;
InputStream err = null;
int exitValue = -1; // returned to caller when p is finished
try {
log.info("***检测ffmPeg运行***");
in = p.getInputStream();
err = p.getErrorStream();
boolean finished = false; // Set to true when p is finished
while (!finished) {
try {
while (in.available() > 0) {
Character c = (char) in.read();
System.out.print(c);
}
while (err.available() > 0) {
Character c = (char) err.read();
System.out.print(c);
}
exitValue = p.exitValue();
finished = true;
} catch (IllegalThreadStateException e) {
Thread.sleep(500);
}
}
} catch (Exception e) {
log.error("doWaitFor();: unexpected exception - "
+ e.getMessage());
} finally {
try {
if (in != null) {
in.close();
}
} catch (IOException e) {
System.out.println(e.getMessage());
}
if (err != null) {
try {
err.close();
} catch (IOException e) {
log.error(e.getMessage());
}
}
}
return exitValue;
}
}
方法调用完整代码
import com.aliyun.oss.OSS;
import com.aliyun.oss.OSSClientBuilder;
import com.liuniu.common.core.controller.BaseController;
import com.liuniu.common.core.domain.AjaxResult;
import com.liuniu.common.utils.StringUtils;
import com.liuniu.system.service.ISysConfigService;
import org.joda.time.DateTime;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import java.io.File;
import java.io.FileInputStream;
import java.util.UUID;
/**
* ffmPeg
*
* @author
*/
@RestController
@RequestMapping("/api/ffmPeg")
public class FfmPegController extends BaseController {
@Autowired
private ISysConfigService configService;
/**
* 将表单提交的MultipartFile上传到OSS中
* 实现思路:
* 先将文件本地上传
* 重命名,生成对应文件夹
* 调用ffmpeg切片转成m3u8
* 将mp4视频和转换后的m3u8以及ts放在一起,然后遍历文件目录
* 将文件上传后,删除本地文件夹和文件在将转换后文件上传到oss上
*
* @param file 上传的文件
* @return ResponseResult 结果
*/
@PostMapping("/m3U8InOSSUpload")
public AjaxResult testM3U8InOSSUpload(MultipartFile file) {
//oss域名
String endpoint = configService.selectConfigByKey("sys.alioss.endpoint");
//ossId
String accessKeyId = configService.selectConfigByKey("sys.alioss.keyid");
//ossKey
String accessKeySecret = configService.selectConfigByKey("sys.alioss.keysecret");
String bucketName = configService.selectConfigByKey("sys.alioss.bucket");
OSS ossClient = new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret);
String url ="";
try {
String f = file.getOriginalFilename();
//获取文件后缀
String suffix = StringUtils.substringAfter(f, ".");
String uuid = UUID.randomUUID().toString().replaceAll("-", "");
//创建文件存储再oss的路径,按日期存储
String filePath = new DateTime().toString("yyyy/MM/dd");
filePath = filePath+"/"+uuid;
//获取文件名后重命名
String filename =uuid+ StringUtils.substringBefore(f, ".") ;
/*本地上传*/
//转储路径D:\mp4\output
String localPath = "d:\\file\\";
//完整转储路径
String folderUrl = localPath + filename;
//文件名带后缀
String fileName = filename + "." + suffix;
//上传oss后路径
String uploadPath = folderUrl + "/" + fileName;
File fileFolder = new File(folderUrl);
if (!fileFolder.exists()) {
fileFolder.mkdirs();
}
File newFile = new File(uploadPath);
file.transferTo(newFile);
//mp4转m3u8
boolean b = ConvertM3U8Api.convertOss(folderUrl + "/", fileName);
if (!b) {
return AjaxResult.error(300, "上传失败!系统转码异常!");
}
//访问本地上传文件夹所有文件,依次上传至oss服务器
File[] files = fileFolder.listFiles();
if (null == files || files.length == 0) {
return null;
}
boolean flag = true;
for (int i = 0; i < files.length; i++) {
if (!files[i].isDirectory()) {
//上传
String name = files[i].getName();
String suf = StringUtils.substringAfter(name, ".");
String pre = StringUtils.substringBefore(name, ".");
FileInputStream fis = new FileInputStream(files[i]);
//获取文件流的文件名
//拼接路径和文件名
if ("m3u8".equals(suf)) {
if (flag && filename.equals(pre)) {
//这是封装的上传阿里云oss的方法
ossClient.putObject(bucketName,
filePath + "/" + name,
fis);
flag = false;
}
url = "https://" + bucketName + "." + endpoint + "/" + filePath + "/" + name;
} else if ("ts".equals(suf)) {
ossClient.putObject(bucketName,
filePath + "/" + name,
fis);
}
fis.close();
if (files[i].exists()) {
files[i].delete();
}
}
}
//删除文件夹
fileFolder.delete();
/*********本地上传(Tomcat配置映射C:/upload/file)*********/
} catch (Exception e) {
e.printStackTrace();
log.error("上传异常");
}
return AjaxResult.success(url);
}
}
若想保留本地文件,可将已下代码注释
if (files[i].exists()) {
files[i].delete();
}
java引用ffmpeg依赖
pom文件依赖
<dependency>
<groupId>ws.schild</groupId>
<artifactId>jave-all-deps</artifactId>
<version>3.1.1</version>
</dependency>
cmd方式调用ffmpeg
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import ws.schild.jave.process.ProcessKiller;
import ws.schild.jave.process.ProcessWrapper;
import ws.schild.jave.process.ffmpeg.DefaultFFMPEGLocator;
/**
*
* @Description:(cmd方式调用ffmpeg)
*/
public class FfmpegCmd {
private static final Logger LOG = LoggerFactory.getLogger(ProcessWrapper.class);
private Process ffmpeg = null;
private ProcessKiller ffmpegKiller = null;
private InputStream inputStream = null;
private OutputStream outputStream = null;
private InputStream errorStream = null;
public void execute(boolean destroyOnRuntimeShutdown, boolean openIOStreams, String ffmpegCmd) throws IOException {
DefaultFFMPEGLocator defaultFFMPEGLocator = new DefaultFFMPEGLocator();
StringBuffer cmd = new StringBuffer(defaultFFMPEGLocator.getExecutablePath());
cmd.append(" ");
cmd.append(ffmpegCmd);
String cmdStr = String.format("ffmpegCmd final is :%s", cmd.toString());
System.out.println(cmdStr);
LOG.info(cmdStr);
Runtime runtime = Runtime.getRuntime();
try {
ffmpeg = runtime.exec(cmd.toString());
if (destroyOnRuntimeShutdown) {
ffmpegKiller = new ProcessKiller(ffmpeg);
runtime.addShutdownHook(ffmpegKiller);
}
if (openIOStreams) {
inputStream = ffmpeg.getInputStream();
outputStream = ffmpeg.getOutputStream();
errorStream = ffmpeg.getErrorStream();
}
} catch (Exception e) {
e.printStackTrace();
}
}
public InputStream getInputStream() {
return inputStream;
}
public OutputStream getOutputStream() {
return outputStream;
}
public InputStream getErrorStream() {
return errorStream;
}
public void destroy() {
if (inputStream != null) {
try {
inputStream.close();
} catch (Throwable t) {
LOG.warn("Error closing input stream", t);
}
inputStream = null;
}
if (outputStream != null) {
try {
outputStream.close();
} catch (Throwable t) {
LOG.warn("Error closing output stream", t);
}
outputStream = null;
}
if (errorStream != null) {
try {
errorStream.close();
} catch (Throwable t) {
LOG.warn("Error closing error stream", t);
}
errorStream = null;
}
if (ffmpeg != null) {
ffmpeg.destroy();
ffmpeg = null;
}
if (ffmpegKiller != null) {
Runtime runtime = Runtime.getRuntime();
runtime.removeShutdownHook(ffmpegKiller);
ffmpegKiller = null;
}
}
public int getProcessExitCode() {
try {
ffmpeg.waitFor();
} catch (InterruptedException ex) {
LOG.warn("Interrupted during waiting on process, forced shutdown?", ex);
}
return ffmpeg.exitValue();
}
public void close() {
destroy();
}
}
工具类
import com.liuniu.common.utils.StringUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;
/**
* mp4转换m3u8工具类
*
**/
@Slf4j
@Component
public class ConvertM3U8Api {
public static boolean convertOss(String folderUrl, String fileName) {
if (!checkFile(folderUrl + fileName)) {
System.out.println("文件不存在!");
return false;
}
//验证文件后缀
String suffix = StringUtils.substringAfter(fileName, ".");
String fileFullName = StringUtils.substringBefore(fileName, ".");
if (!validFileType(suffix)) {
return false;
}
return processM3U8(folderUrl, fileName, fileFullName);
}
/**
* 验证上传文件后缀
*
* @param type 类型
* @return 是否为mp4
*/
private static boolean validFileType(String type) {
return "mp4".equals(type);
}
/**
* 验证是否是文件格式
*
* @param path 路径
* @return 是否存在
*/
private static boolean checkFile(String path) {
File file = new File(path);
return file.isFile();
}
//
/**
* ffmPeg程序转换m3u8
* ffmPeg -i vue.mp4 -c:v libx264 -hls_time 20 -hls_list_size 0 -c:a aac -strict -2 -f hls vue.m3u8
* ffmPeg能解析的格式:(asx,asf,mpg,wmv,3gp,mp4,mov,avi,flv等)
*
* @param folderUrl 文件路径
* @param fileName 名称
* @param fileFullName 全名
* @return 是否成功
*/
private static boolean processM3U8(String folderUrl, String fileName, String fileFullName) {
//这里就写入执行语句就可以了
List<String> commend = new ArrayList<>();
commend.add(" -i");
commend.add(folderUrl + fileName);
commend.add("-c:v");
commend.add("libx264");
commend.add("-hls_time");
commend.add("5");
commend.add("-hls_list_size");
commend.add("0");
commend.add("-c:a");
commend.add("aac");
commend.add("-strict");
commend.add("-2");
commend.add("-f");
commend.add("hls");
commend.add(folderUrl + fileFullName + ".m3u8");
try {
String commendStr = String.join(" ",commend);
Integer codeTmp = cmdExecut(commendStr);
log.info("***i=【{}】***", codeTmp);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
*
* @Description: (执行ffmpeg自定义命令)
* @param: @param cmdStr
* @param: @return
* @return: Integer
* @throws
*/
public static Integer cmdExecut(String cmdStr) {
//code=0表示正常
Integer code = null;
FfmpegCmd ffmpegCmd = new FfmpegCmd();
/**
* 错误流
*/
InputStream errorStream = null;
try {
//destroyOnRuntimeShutdown表示是否立即关闭Runtime
//如果ffmpeg命令需要长时间执行,destroyOnRuntimeShutdown = false
//openIOStreams表示是不是需要打开输入输出流:
// inputStream = processWrapper.getInputStream();
// outputStream = processWrapper.getOutputStream();
// errorStream = processWrapper.getErrorStream();
ffmpegCmd.execute(false, true, cmdStr);
errorStream = ffmpegCmd.getErrorStream();
//打印过程
int len = 0;
while ((len=errorStream.read())!=-1){
System.out.print((char)len);
}
//code=0表示正常
code = ffmpegCmd.getProcessExitCode();
} catch (IOException e) {
e.printStackTrace();
} finally {
//关闭资源
ffmpegCmd.close();
}
//返回
return code;
}
}
方法调用同上
video视频读取
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link
href="https://unpkg.com/video.js@7.10.2/dist/video-js.min.css"
rel="stylesheet"
/>
<title>视频在线播放</title>
<style>
html,
body {
margin: 0;
padding: 0;
}
.video-container {
display: flex; /* 1.设置为弹性盒子 */
align-items: center; /* 2.让子项盒子纵向 居中排列 */
justify-content: center; /* 3.让子项盒子横向 居中排列 */
height: 100vh;
background: rgb(255, 247, 247);
}
.video-player {
width: 50%;
height: 50%;
}
</style>
</head>
<body>
<div class="video-container">
<!-- 一定要记得 在这里加上类名 video-js -->
<video id="videoPlayer" class="video-js video-player">
<p class="vjs-no-js">
To view this video please enable JavaScript, and consider upgrading to
a web browser that
<a href="https://videojs.com/html5-video-support/" target="_blank">
supports HTML5 video
</a>
</p>
</video>
</div>
<script src="https://unpkg.com/video.js@7.10.2/dist/video.min.js"></script>
<script>
const nowPlayVideoUrl = 'http://1257120875.vod2.myqcloud.com/0ef121cdvodtransgzp1257120875/3055695e5285890780828799271/v.f230.m3u8';
const options = {
autoplay: true, // 设置自动播放
muted: true, // 设置了它为true,才可实现自动播放,同时视频也被静音 (Chrome66及以上版本,禁止音视频的自动播放)
preload: 'auto', // 预加载
controls: true, // 显示播放的控件
playbackRates: [0.5, 1, 1.25, 1.5, 2, 3] // 倍速播放
};
let player = null;
function getVideo(nowPlayVideoUrl) {
player = videojs('#videoPlayer', options, function onPlayerReady() {
// In this context, `this` is the player that was created by Video.js.
videojs.log('Your player is ready!', this);
//关键代码, 动态设置src,才可实现换台操作
this.src([
{
src: nowPlayVideoUrl,
type: 'application/x-mpegURL' // 告诉videojs,这是一个hls流
}
]);
this.play();
});
}
getVideo(nowPlayVideoUrl);
window.addEventListener('unload', function () {
player.dispose(); // Removing Players,该方法会重置videojs的内部状态并移除dom
});
</script>
</body>
</html>