ffmpeg可以做很多事,今天只用它来做切片。切片就是将一个大文件切成多个小文件然后再播放,小文件下载速度快,所以播放的时候就会流畅很多。
将文件变成m3u8格式后,写代码的时候src直接写m3u8的文件名即可<source id="videoUrl" src="test.m3u8" type="" />,播放器会关联切片后的所有文件使之成为一个完整的文件。
1、转换类
import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.List;
/**
* @author Kyle
* @version 1.0 2019年4月3日 NEW
* @function 前提需要本机有ffmpeg,并且配置好环境变量,即把E:\ffmpeg-20180430-0807a77-win64-static\
* bin加入到path中
*/
public class Mp4ConvertM3u8Util {
private static String inputPath = ""; // 完整的路径和文件名
private static String outputPath = ""; // 完整的路径和文件名
public static void main(String args[]) throws IOException {
// 转换过程需要两步
convert("D:\\MyEclipseProject\\Test\\input\\test2.mp4", "D:\\MyEclipseProject\\Test\\output\\test.ts");
}
/**
* 转换
* @param inputPath 原文件存在的完整路径和文件名
* @param outputPath 目标文件存在的完整路径和文件名
* @return
*/
private static boolean convert(String inputPath, String outputPath) {
Mp4ConvertM3u8Util.inputPath = inputPath;
Mp4ConvertM3u8Util.outputPath = outputPath;
boolean status = false;
status = mp4ConvertTs(); // mp4转成ts格式
if (status) {
status = tsConvertM3u8();
if(status){
// 删除.ts文件, 因为我们直接播放m3u8就够了
File file = new File(outputPath);
if(file.exists()){
file.delete();
}
}
}
return status;
}
// 将mp4转成ts格式
private static boolean mp4ConvertTs() {
List<String> command = new ArrayList<String>();
// 先用ffmpeg把file.mp4文件转换为file.ts文件:
// ffmpeg -y -i <in file> -vcodec copy -acodec copy -vbsf
// h264_mp4toannexb <output file>(这个为转换命令)
// 以下命令可以按照自己的需要改动
command.add("ffmpeg");
command.add("-y"); // 不用提示直接覆盖
command.add("-i");
command.add(inputPath);
command.add("-vcodec");
command.add("copy");
command.add("-acodec");
command.add("copy");
command.add("-vbsf");
command.add("h264_mp4toannexb");
command.add(outputPath);
try {
// redirectErrorStream(true)合并错误和标准输出,这样所有的信息通过getInputStream()来获取
Process tsProcess = new ProcessBuilder(command).redirectErrorStream(true).start();
// 目前必须加这句话,如果不加子进程等待主进程读取数据,主进程等待子进程结束,两个进程相互等待,最终导致死锁
new SkipStream(tsProcess.getInputStream()).start();
// 判断子进程是否活着jdk1.8加的此方法,其实不加这个判断也一样可以执行
if (tsProcess.isAlive()) {
tsProcess.waitFor();
}
// 销毁子进程
tsProcess.destroy();
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
// 将ts转成m3u8
private static boolean tsConvertM3u8() {
// ffmpeg -i abc.ts -c copy -map 0 -f segment -segment_list playlist.m3u8 -segment_time 5 abc%03d.ts
// 以下命令可以按照自己的需要改动
List<String> command = new ArrayList<String>();
command.add("ffmpeg");
command.add("-i");
command.add(outputPath);
command.add("-c"); // -c和-codec 一样,-c是简写
command.add("copy");
command.add("-map"); // -map 0 把第一个文件的所有流都复制到输出流中
command.add("0");
command.add("-f"); // -f segment -segment_list 分片列表
command.add("segment");
command.add("-segment_list");
// 去掉.ts扩展名
outputPath = outputPath.replace(".ts", "");
// 现在m3u8和切片后的文件名都与.ts的文件名一致
command.add(outputPath + ".m3u8");
command.add("-segment_time"); // 5秒一个切片
command.add("5");
command.add(outputPath + "%03d.ts"); // %03d代表文件名后的数字test001.ts,改成%01d就是test1.ts,其中test是上面定义的文件名
try {
Process videoProcess2 = new ProcessBuilder(command).redirectErrorStream(true).start();
// 目前必须加这句话,如果不加子进程等待主进程读取数据,主进程等待子进程结束,两个进程相互等待,最终导致死锁。
new SkipStream(videoProcess2.getInputStream()).start();
if (videoProcess2.isAlive()) {
videoProcess2.waitFor();
}
videoProcess2.destroy();
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
}
// 只是为了不死锁,不干其它的事情,只是跳过流
class SkipStream extends Thread {
InputStream input = null;
public SkipStream(InputStream input) {
this.input = input;
}
public void run() {
try {
String line = null;
BufferedReader in = new BufferedReader(new InputStreamReader(input));
while ((line = in.readLine()) != null) {
System.out.println("output: " + line);
}
} catch (Exception e) {
e.printStackTrace();
} finally {
if (input != null) {
try {
input.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
2、播放页代码片段,以下代码既可以播放m3u8也可以播放mp4,代码中做了判断,如果在手机上播放直接用h5的播放标签就可以不需要下面代码,要是在PC端播放需要引入播放m3u8的videojs-contrib-hls.min.js,我还引入了弹出模态框的bootstrap.min.js
<%@ page language="java" pageEncoding="utf-8"%>
<%@ taglib prefix="s" uri="/struts-tags"%>
<link href="/css/video/video-js.min.css" rel="stylesheet">
<script src="/js/video/video.min.js"></script>
<script src="/js/video/videojs-contrib-hls.min.js"></script>
<div id="myModal" class="modal fade" id="myModal" tabindex="-1" role="dialog" aria-labelledby="myModalLabel" aria-hidden="true" data-backdrop="static">
<div class="modal-dialog" style="z-index:2041">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal"
aria-hidden="true">×</button>
<h4 class="modal-title" id="myModalLabel">视频播放</h4>
</div>
<div class="modal-body" id="myvideo">
<!--
<video id="media" class="video-js vjs-default-skin" controls width="570" height="350" >
<source id="videoUrl" src="" type="" />
</video>
-->
</div>
</div>
</div>
</div>
<script language="javascript">
var player;
$(function() {
$('#myModal').on('hide.bs.modal',
function() {
// 关闭的时候销毁video
player.dispose();
})
});
function modal(videoSrc) {
$('#myModal').modal('show');
// 每次都创建一个播放器
var container = jQuery("#myvideo");
var content = '';
// 让播放按钮居中显示,默认在左上角vjs-big-play-centered
content += '<video id="media" class="video-js vjs-default-skin vjs-big-play-centered" controls width="570" height="350" >';
content += ' <source id="videoUrl" src="" type="" />';
content += '</video>';
container.append(content);
var ext = videoSrc.substring(videoSrc.lastIndexOf("."));
if (ext == ".m3u8") {
document.getElementById("videoUrl").type = "application/x-mpegURL";
} else {
document.getElementById("videoUrl").type = "video/mp4";
}
document.getElementById("videoUrl").src = videoSrc;
document.getElementById("media").style.width = "570px";
document.getElementById("media").style.height = "350px";
// 加载视频
player = videojs("media");
player.play();
}
</script>