摘要:
视频上传采用断点续传的模式,在前端上传视频到后端时使用webuploader技术将视频分片,同一个视频的分片有相同的md5加密字符串,并且每个分片有自己唯一的id,将这些分片并行上传到后端,后端提供4个接口完成断点续传,第一个接口通过传过来的md5加密串在本地创建一个存放该视频的唯一的文件夹,第二个接口根据当前分片的唯一标识判断是否上传过且是否完整,完整则跳过该分片继续检测下一个分片,不完整或者没有上传则上传该分片,第三个接口将分片上传到创建的文件夹中,第四个接口进行切片的组装,最终完成视频从前端上传到后端,并推流到服务器【使用ffmpeg技术将视频转换为mp4格式,再将mp4分片成多个ts文件和一个m3u8文件记录每个ts的信息,再推流到服务器】
一:安装webuploader【前端通过它实现视频分片上传至后端,提高上传速度】
npm install webuploader --save
npm install jquery@1.12.4
//vue中引入webuploder和jquery
import $ from 'jquery'
import WebUploader from 'webuploader'
二:封装上传组件upload.vue
<template name="">
<div>
<div id="uploader" class="wu-example">
<div class="btns" style="float:left;padding-right: 20px">
<div id="picker">选择文件</div>
</div>
<div id="ctlBtn" class="webuploader-pick" @click="upload()">开始上传</div>
</div>
<br/>
<!--用来存放文件信息-->
<div id="thelist" class="uploader-list" >
<div v-if="uploadFile.id" :id='uploadFile.id'><span>{{uploadFile.name}}</span> <span class='percentage'>{{percentage}}%</span></div>
</div>
</div>
</template>
<script>
import $ from 'jquery'
import WebUploader from 'webuploader'
import "webuploader/css/webuploader.css"
export default{
name:"upload",
props:{ //父组件传入的参数
chapterId:"", //章节ID
courseId:"", //课程ID
number:"", //课程序列号
name:"", //当前课程视频名称,
chapterName:"",
courseName:""
},
data(){
return{
uploader:{},
uploadFile:{},
percentage:0,
fileMd5:''
}
},
methods:{
//开始上传
upload(){
if(!this.number || !this.name){
this.$message({ message: '课程名或序列号', type: 'error' });
return ;
}
if(this.uploadFile && this.uploadFile.id){
this.$message({ message: '正在注册文件,请稍后,如果文件未上传成功,请刷新重试', type: 'success' ,duration:5000});
this.uploader.upload(this.uploadFile.id);
}else{
this.$message({ message: '请选择文件', type: 'error' });
}
}
},
mounted(){
WebUploader.Uploader.register({
"before-send-file":"beforeSendFile",
"before-send":"beforeSend",
"after-send-file":"afterSendFile"
},{
beforeSendFile:function(file) {
// 创建一个deffered,用于通知是否完成操作
var _this = this;
var deferred = WebUploader.Deferred();
// 计算文件的唯一标识,用于断点续传
(new WebUploader.Uploader()).md5File(file, 0, 100*1024*1024).then(function(val) {
this.fileMd5 = val;
this.uploadFile = file;
//向服务端请求注册上传文件
$.ajax(
{
type:"POST",
url:"http://localhost:1021/ymcc/media/mediaFile/register",
data:{
// 文件唯一表示
fileMd5:this.fileMd5,
fileName: file.name,
fileSize:file.size,
mimetype:file.type,
fileExt:file.ext
},
dataType:"json",
success:function(response) {
if(response.success) {
_this.$message({ message: '上传文件注册成功,正在分片上传', type: 'success' ,duration:4000});
deferred.resolve();
} else {
_this.$message({ message: '上传文件注册失败',type: 'error' });
deferred.reject();
}
}
}
);
}.bind(this));
return deferred.promise();
}.bind(this),
beforeSend:function(block) {
var deferred = WebUploader.Deferred();
// 每次上传分块前校验分块,如果已存在分块则不再上传,达到断点续传的目的
$.ajax(
{
type:"POST",
url:"http://localhost:1021/ymcc/media/mediaFile/checkchunk",
data:{
// 文件唯一表示
fileMd5:this.fileMd5,
// 当前分块下标
chunk:block.chunk,
// 当前分块大小
chunkSize:block.end-block.start
},
dataType:"json",
success:function(response) {
if(response.fileExist) {
// 分块存在,跳过该分块
deferred.reject();
} else {
// 分块不存在或不完整,重新发送
deferred.resolve();
}
}
}
);
//构建fileMd5参数,上传分块时带上fileMd5
this.uploader.options.formData.fileMd5 = this.fileMd5;
this.uploader.options.formData.chunk = block.chunk;
return deferred.promise();
}.bind(this),
//分块都上传成功,合并分块
afterSendFile:function(file) {
var _this = this;
// 合并分块
$.ajax(
{
type:"POST",
url:"http://localhost:1021/ymcc/media/mediaFile/mergechunks",
data:{
fileMd5:this.fileMd5,
fileName: file.name,
fileSize:file.size,
mimetype:file.type,
fileExt:file.ext,
chapterId:this.chapterId,
courseId:this.courseId,
videoNumber:this.number,
name:this.name,
courseName:this.courseName,
chapterName:this.chapterName
},
success:function(response){
//在这里解析合并成功结果
if(response && response.success){
//关闭弹出窗口
_this.$emit('addVideoFormVisibleClose' ,false);
_this.$message({ message: '上传成功,文件正同步到云服务器,请在视频管理中查看上传状态',type: 'success' });
}else{
_this.$message({ message: '上传失败'+response.messsage,type: 'error' });
}
}
}
);
}.bind(this)
}
);
// 创建uploader对象,配置参数
this.uploader = WebUploader.create(
{
swf:"/static/plugins/webuploaderUploader.swf",//上传文件的flash文件,浏览器不支持h5时启动flash
server:"http://localhost:1021/ymcc/media/mediaFile/uploadchunk",//上传分块的服务端地址,注意跨域问题
fileVal:"file",//文件上传域的name
pick:"#picker",//指定选择文件的按钮容器
auto:false,//手动触发上传
disableGlobalDnd:true,//禁掉整个页面的拖拽功能
chunked:true,// 是否分块上传
chunkSize:1*1024*1024, // 分块大小(默认5M)
threads:3, // 开启多个线程(默认3个)
prepareNextFile:true// 允许在文件传输时提前把下一个文件准备好
}
);
// 将文件添加到队列
this.uploader.on("fileQueued", function(file) {
this.uploadFile = file;
this.percentage = 0;
}.bind(this)
);
//选择文件后触发
this.uploader.on("beforeFileQueued", function(file) {
// this.uploader.removeFile(file)
//重置uploader
this.uploader.reset()
this.percentage = 0;
}.bind(this));
// 监控上传进度
// percentage:代表上传文件的百分比
this.uploader.on("uploadProgress", function(file, percentage) {
this.percentage = Math.ceil(percentage * 100);
}.bind(this));
//上传失败触发
this.uploader.on("uploadError", function(file,reason) {
this.$message({ message: '上传文件失败',type: 'success' });
});
//上传成功触发
this.uploader.on("uploadSuccess", function(file,response ) {
});
//每个分块上传请求后触发
this.uploader.on( 'uploadAccept', function( file, response ) {
if(!(response && response.success)){//分块上传失败,返回false
return false;
}
});
}
}
</script>
<style>
.webuploader-container,.webuploader-pick{
line-height: 15px !important;
}
</style>
三:main.js引入上传组件
import PartUpload from 'upload.vue'
Vue.component("PartUpload", PartUpload);
四:页面使用组件上传视频【<PartUpload></PartUpload>】
<!--新增视频-->
<el-dialog title="新增视频" :visible.sync="addVideoFormVisible" :close-on-click-modal="false" width="800px">
<el-form :model="addVideoForm" label-width="80px" ref="addForm">
<el-form-item label="章节名称" prop="name">
<el-input v-model="addVideoForm.chapterName" disabled auto-complete="off"></el-input>
</el-form-item>
<el-form-item label="视频名称" prop="name">
<el-input v-model="addVideoForm.name" auto-complete="off"></el-input>
</el-form-item>
<el-form-item label="视频序号" prop="number">
<el-input v-model="addVideoForm.number" type="number" min="1" auto-complete="off"></el-input>
</el-form-item>
<el-form-item label="视频上传" prop="name">
<PartUpload v-bind="addVideoForm" @addVideoFormVisibleClose="addVideoFormVisibleClose"></PartUpload>
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button @click.native="addVideoFormVisible = false" icon="el-icon-remove">取消</el-button>
<el-button type="primary" @click.native="addVideo" icon="el-icon-check">提交</el-button>
</div>
</el-dialog>
五:后端接口
package cn.ybl.web.controller;
import cn.ybl.domain.MediaFile;
import cn.ybl.query.MediaFileQuery;
import cn.ybl.result.JSONResult;
import cn.ybl.result.PageList;
import cn.ybl.service.IMediaFileService;
import com.baomidou.mybatisplus.plugins.Page;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
@RestController
@RequestMapping("/mediaFile")
@Slf4j
public class MediaFileController {
@Autowired
public IMediaFileService mediaFileService;
//===============================================================
//文件注册,检查文件是否已经上传
@PostMapping("/register")
public JSONResult register(@RequestParam("fileMd5") String fileMd5, // 文件唯一标识
@RequestParam("fileName") String fileName, // 文件名
@RequestParam("fileSize") Long fileSize, // 文件大小
@RequestParam("mimetype") String mimetype, // mime类型
@RequestParam("fileExt") String fileExt) { //文件扩展名
log.info("文件上传-文件注册,fileName={},fileMd5={}",fileName,fileMd5);
return mediaFileService.register(fileMd5,fileName,fileSize,mimetype,fileExt);
}
//校验文件块是否已经存在了
@PostMapping("/checkchunk")
public JSONResult checkchunk(
// 文件唯一标识
@RequestParam("fileMd5") String fileMd5,
// 当前分块下标
@RequestParam("chunk") Integer chunk,
// 当前分块大小
@RequestParam("chunkSize") Integer chunkSize){
log.info("文件上传-检查文件块是否存在;{}",fileMd5);
return mediaFileService.checkchunk(fileMd5,chunk,chunkSize);
}
//上传分块后的文件
@PostMapping("/uploadchunk")
public JSONResult uploadchunk(
//分块后的文件
@RequestParam("file") MultipartFile file,
// 文件唯一标识
@RequestParam("fileMd5") String fileMd5,
// 第几块,分块的索引
@RequestParam("chunk") Integer chunk){
log.info("文件上传 fileName={},fileMd5={}",file.getOriginalFilename(),fileMd5);
return mediaFileService.uploadchunk(file,fileMd5,chunk);
}
//分块都上传成功之后,合并分块
@PostMapping("/mergechunks")
public JSONResult mergechunks(
// 课程章节ID
@RequestParam("chapterId") Long chapterId,
// 课程ID
@RequestParam("courseId") Long courseId,
// 课程序列号
@RequestParam("videoNumber") Integer videoNumber,
// 课程章节ID
@RequestParam("name") String name,
//章节名
@RequestParam("chapterName") String chapterName,
//课程名
@RequestParam("courseName") String courseName,
// 文件唯一标识
@RequestParam("fileMd5") String fileMd5,
// 源文件名
@RequestParam("fileName") String fileName,
// 文件总大小
@RequestParam("fileSize") Long fileSize,
// 文件的mimi类型
@RequestParam("mimetype") String mimetype,
// 文件扩展名
@RequestParam("fileExt") String fileExt){
log.info("合并文件 fileName={} ,fileMd5={} ",fileName,fileMd5);
return mediaFileService.mergechunks(fileMd5,fileName,fileSize,mimetype,fileExt,chapterId,courseId,videoNumber,name,courseName,chapterName);
}
//===============================================================
/**
* 保存和修改公用的
*/
@RequestMapping(value="/save",method= RequestMethod.POST)
public JSONResult saveOrUpdate(@RequestBody MediaFile mediaFile){
if(mediaFile.getId()!=null){
mediaFileService.updateById(mediaFile);
}else{
mediaFileService.insert(mediaFile);
}
return JSONResult.success();
}
/**
* 删除对象
*/
@RequestMapping(value="/{id}",method=RequestMethod.DELETE)
public JSONResult delete(@PathVariable("id") Long id){
mediaFileService.deleteById(id);
return JSONResult.success();
}
/**
* 获取对象
*/
@RequestMapping(value = "/{id}",method = RequestMethod.GET)
public JSONResult get(@PathVariable("id")Long id){
return JSONResult.success(mediaFileService.selectById(id));
}
/**
* 查询所有对象
*/
@RequestMapping(value = "/list",method = RequestMethod.GET)
public JSONResult list(){
return JSONResult.success(mediaFileService.selectList(null));
}
/**
* 带条件分页查询数据
*/
@RequestMapping(value = "/pagelist",method = RequestMethod.POST)
public JSONResult page(@RequestBody MediaFileQuery query){
Page<MediaFile> page = new Page<MediaFile>(query.getPage(),query.getRows());
page = mediaFileService.selectPage(page);
return JSONResult.success(new PageList<MediaFile>(page.getTotal(),page.getRecords()));
}
}
六:后端业务层
package cn.ybl.service;
import cn.ybl.domain.MediaFile;
import cn.ybl.result.JSONResult;
import com.baomidou.mybatisplus.service.IService;
import org.springframework.web.multipart.MultipartFile;
/**
* <p>
* 服务类
* </p>
*
* @author whale.chen
* @since 2022-03-25
*/
public interface IMediaFileService extends IService<MediaFile> {
/**
* 1.文件上传之前的注册功能【创建本地唯一文件夹】
*/
JSONResult register(String fileMd5, String fileName, Long fileSize, String mimetype, String fileExt);
/**
* 2.校验文件块是否已经存在了
*/
JSONResult checkchunk(String fileMd5, Integer chunk, Integer chunkSize);
/**
* 3.上传文件块
*/
JSONResult uploadchunk(MultipartFile file, String fileMd5, Integer chunk);
/**
* 4.合并文件快
*/
JSONResult mergechunks(String fileMd5, String fileName, Long fileSize, String mimetype, String fileExt,
Long courseChapterId, Long courseId, Integer number, String name, String courseName, String chapterName);
/**
* 文件推流
*/
JSONResult handleFile2m3u8(MediaFile mediaFile);
}
七:业务实现
package cn.ybl.service.impl;
import cn.ybl.domain.MediaFile;
import cn.ybl.mapper.MediaFileMapper;
import cn.ybl.result.JSONResult;
import cn.ybl.service.IMediaFileService;
import cn.ybl.util.HlsVideoUtil;
import cn.ybl.util.Mp4VideoUtil;
import com.baomidou.mybatisplus.mapper.EntityWrapper;
import com.baomidou.mybatisplus.mapper.Wrapper;
import com.baomidou.mybatisplus.service.impl.ServiceImpl;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.codec.digest.DigestUtils;
import org.apache.commons.io.IOUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import org.springframework.web.multipart.MultipartFile;
import java.io.*;
import java.util.*;
/**
* <p>
* 服务实现类
* </p>
*
* @author whale.chen
* @since 2022-03-25
*/
@Service
@Slf4j
public class MediaFileServiceImpl extends ServiceImpl<MediaFileMapper, MediaFile> implements IMediaFileService {
@Autowired
private MediaFileMapper mediaFileMapper;
//@Autowired
//private MediaProducer mediaProducer;
/**
* 配置
* ===============================================================================================================
*/
//上传文件根目录
@Value("${media.upload-base-dir}")
private String uploadPath;
//推流服务器地址
@Value("${media.rtmp}")
private String srsRtmpPath;
//推流服务器播放
@Value("${media.play}")
private String srsPalyPath;
//ffmpeg绝对路径
@Value("${media.ffmpeg‐path}")
String ffmpeg_path;
/**
* 合并分片
*/
@Override
public JSONResult mergechunks(String fileMd5, String fileName, Long fileSize, String mimetype, String fileExt,
Long courseChapterId, Long courseId, Integer number, String name, String courseName, String chapterName) {
Long startTime = System.currentTimeMillis();
//获取块文件的路径
String chunkfileFolderPath = getChunkFileFolderPath(fileMd5);
//创建文件目录
File chunkfileFolder = new File(chunkfileFolderPath);
//目录是否存在, 不存在就创建目录
if(!chunkfileFolder.exists()){
chunkfileFolder.mkdirs();
}
//合并文件,创建新的文件对象
File mergeFile = new File(getFilePath(fileMd5,fileExt));
// 合并文件存在先删除再创建
if(mergeFile.exists()){
mergeFile.delete();
}
boolean newFile = false;
try {
//创建文件
newFile = mergeFile.createNewFile();
} catch (IOException e){
e.printStackTrace();
}
if(!newFile){
//创建失败
return JSONResult.error("创建文件失败!");
}
//获取块文件,此列表是已经排好序的列表
List<File> chunkFiles = getChunkFiles(chunkfileFolder);
//合并文件
mergeFile = mergeFile(mergeFile, chunkFiles);
if(mergeFile == null){
return JSONResult.error("合并文件失败!");
}
//校验文件
boolean checkResult = this.checkFileMd5(mergeFile, fileMd5);
if(!checkResult){
return JSONResult.error("文件校验失败!");
}
//将文件信息保存到数据库
MediaFile mediaFile = new MediaFile();
//MD5作为文件唯一ID
mediaFile.setFileId(fileMd5);
//文件名
mediaFile.setFileName(fileMd5+"."+fileExt);
//源文件名
mediaFile.setFileOriginalName(fileName);
//文件路径保存相对路径
mediaFile.setFilePath(getFileFolderRelativePath(fileMd5,fileExt));
mediaFile.setFileSize(fileSize);
mediaFile.setUploadTime(new Date());
mediaFile.setMimeType(mimetype);
mediaFile.setFileType(fileExt);
mediaFile.setChapterId(courseChapterId);
mediaFile.setCourseId(courseId);
mediaFile.setName(name);
mediaFile.setCourseName(courseName);
mediaFile.setChapterName(chapterName);
mediaFile.setFileUrl(srsPalyPath+mediaFile.getFileId()+".m3u8");
//状态为上传成功
mediaFile.setFileStatus(1);
//根据最新的number生成number
Wrapper<MediaFile> wrapper = new EntityWrapper<>();
wrapper.eq("course_id", courseId);
wrapper.eq("chapter_id", courseChapterId);
//某个课程下的某个章节下的视频数量
Integer videoCount = mediaFileMapper.selectCount(wrapper);
mediaFile.setNumber(videoCount+1);
mediaFileMapper.insert(mediaFile);
// 文件上传到视频服务器做 断点续播
boolean success = true ; //mediaProducer.synSend(mediaFile);
log.info("合并文件耗时 {}" ,System.currentTimeMillis() - startTime);
//分片上传
handleFile2m3u8(mediaFile);
return success?JSONResult.success():JSONResult.error();
}
/** 文件推流 **/
public JSONResult handleFile2m3u8(MediaFile mediaFile) {
String fileType = mediaFile.getFileType();
if(fileType == null ){
return JSONResult.error("无效的扩展名");
}
//组装MP4文件名
String mp4_name = mediaFile.getFileId()+".mp4";
//如果视频不是MP4需要进行格式转换
if(!fileType.equals("mp4")){
//生成mp4的文件路径
String video_path = uploadPath + mediaFile.getFilePath()+mediaFile.getFileName();
//文件目录
String mp4folder_path = uploadPath + mediaFile.getFilePath();
//视频编码,生成mp4文件
Mp4VideoUtil videoUtil = new Mp4VideoUtil(ffmpeg_path,video_path,mp4_name,mp4folder_path);
//生成MP4文件
String result = videoUtil.generateMp4();
if(result == null || !result.equals("success")){
//操作失败写入处理日志
mediaFile.setFileStatus(3);
mediaFileMapper.updateById(mediaFile);
return JSONResult.error("视频转换mp4失败");
}
}
//此地址为mp4的本地地址
String video_path = uploadPath + mediaFile.getFilePath()+mp4_name;
//初始化推流工具
HlsVideoUtil hlsVideoUtil = new HlsVideoUtil(ffmpeg_path);
hlsVideoUtil.init(srsRtmpPath,video_path,mediaFile.getFileId());
//推流到云端
String result = hlsVideoUtil.generateM3u8ToSrs();
if(result == null || !result.equals("success")){
//操作失败写入处理日志
mediaFile.setFileStatus(3);
//处理状态为处理失败
//MediaFileProcess_m3u8 mediaFileProcess_m3u8 = new MediaFileProcess_m3u8();
//mediaFileProcess_m3u8.setErrorMsg(result);
//mediaFile.setMediaFileProcess_m3u8(mediaFileProcess_m3u8);
mediaFileMapper.updateById(mediaFile);
return JSONResult.error("推流失败");
}
//获取m3u8列表
//更新处理状态为成功
mediaFile.setFileStatus(2);
//m3u8文件url,播放使用
//mediaFile.setFileUrl(srsPalyPath+mediaFile.getFileId()+".m3u8");
mediaFileMapper.updateById(mediaFile);
log.info("视频推流完成...");
return JSONResult.success();
}
/*
*根据文件md5得到文件路径
*/
private String getFilePath(String fileMd5 ,String fileExt) {
String filePath = uploadPath + fileMd5.substring(0, 1) + "/" + fileMd5.substring(1, 2) + "/" + fileMd5 + "/" + fileMd5 + "." + fileExt;
return filePath;
}
//得到文件目录相对路径,路径中去掉根目录
private String getFileFolderRelativePath(String fileMd5 ,String fileExt) {
String filePath=fileMd5 .substring(0, 1) + "/" + fileMd5. substring(1, 2) + "/"+ fileMd5 + "/";
return filePath;
}
//得到文件所在目录
private String getFileFolderPath(String fileMd5) {
String fileFolderPath = uploadPath + fileMd5.substring(0, 1) + "/" + fileMd5.substring(1, 2) + "/" + fileMd5 + "/";
return fileFolderPath;
}
//创建文件目录
private boolean createFileFold(String fileMd5){
//创建上传文件目录
String fileFolderPath = getFileFolderPath(fileMd5);
File fileFolder = new File(fileFolderPath);
if (!fileFolder.exists()) {
//创建文件夹
boolean mkdirs = fileFolder.mkdirs();
log.info("创建文件目录 {} ,结果 {}",fileFolder.getPath(),mkdirs);
return mkdirs;
}
return true;
}
/**
* 上传文件注册
*/
@Override
public JSONResult register(String fileMd5, String fileName, Long fileSize, String mimetype, String fileExt) {
//检查文件是否上传
// 1、得到文件的路径
String filePath = getFilePath(fileMd5, fileExt);
File file = new File(filePath);
//2、查询数据库文件是否存在
MediaFile media = mediaFileMapper.selectById(fileMd5);
//文件存在直接返回
if(file.exists() && media!=null){
log.info("文件注册 {} ,文件已经存在",fileName);
return JSONResult.error("上传文件已存在");
}
boolean fileFold = createFileFold(fileMd5);
if(!fileFold){
//上传文件目录创建失败
log.info("上传文件目录创建失败 {} ,文件已经存在",fileName);
return JSONResult.error("上传文件目录失败");
}
return JSONResult.success();
}
//得到块文件所在目录
private String getChunkFileFolderPath(String fileMd5) {
String fileChunkFolderPath = getFileFolderPath(fileMd5) +"/" + "chunks" + "/";
return fileChunkFolderPath;
}
/**
* 检查分片
*/
@Override
public JSONResult checkchunk(String fileMd5, Integer chunk, Integer chunkSize) {
//获取块文件文件夹路径
String chunkfileFolderPath = getChunkFileFolderPath(fileMd5);
//块文件的文件名称以1,2,3..序号命名,没有扩展名
File chunkFile = new File(chunkfileFolderPath+chunk);
if(!chunkFile.exists()){
return JSONResult.error();
}
return JSONResult.success();
}
/**
* 创建块文件目录
*/
private boolean createChunkFileFolder(String fileMd5){ //创建上传文件目录
String chunkFileFolderPath = getChunkFileFolderPath(fileMd5);
File chunkFileFolder = new File(chunkFileFolderPath);
if (!chunkFileFolder.exists()) {
//创建文件夹
boolean mkdirs = chunkFileFolder.mkdirs();
return mkdirs;
}
return true;
}
/**
* 上传分片
*/
@Override
public JSONResult uploadchunk(MultipartFile file, String fileMd5, Integer chunk) {
if(file == null){
return JSONResult.error("上传文件不能为null");
}
//创建块文件目录
boolean fileFold = createChunkFileFolder(fileMd5);
//块文件存放完整路径
File chunkfile = new File(getChunkFileFolderPath(fileMd5) + chunk);
//上传的块文件
InputStream inputStream= null;
FileOutputStream outputStream = null;
try {
inputStream = file.getInputStream();
outputStream = new FileOutputStream(chunkfile);
IOUtils.copy(inputStream,outputStream); }
catch (Exception e){
e.printStackTrace();
return JSONResult.error("文件上传失败!");
}finally {
try {
inputStream.close();
}
catch (IOException e)
{
e.printStackTrace();
}
try {
outputStream.close();
} catch (IOException e)
{
e.printStackTrace();
}
}
return JSONResult.success();
}
//校验文件的md5值
private boolean checkFileMd5(File mergeFile,String md5) {
if(mergeFile == null || StringUtils.isEmpty(md5))
{
return false;
}
//进行md5校验
FileInputStream mergeFileInputstream = null;
try {
mergeFileInputstream = new FileInputStream(mergeFile);
//得到文件的md5
String mergeFileMd5 = DigestUtils.md5Hex(mergeFileInputstream);
//比较md5
if(md5.equalsIgnoreCase(mergeFileMd5))
{
return true;
}
} catch (Exception e)
{
e.printStackTrace();
}
finally
{
try {
mergeFileInputstream.close();
} catch (IOException e)
{
e.printStackTrace();
}
}
return false;
}
//获取所有块文件
private List<File> getChunkFiles(File chunkfileFolder) {
//获取路径下的所有块文件
File[] chunkFiles = chunkfileFolder.listFiles();
//将文件数组转成list,并排序
List<File> chunkFileList = new ArrayList<File>();
chunkFileList.addAll(Arrays.asList(chunkFiles));
//排序
Collections.sort(chunkFileList, (o1, o2) -> {
if(Integer.parseInt(o1.getName())>Integer.parseInt(o2.getName()))
{
return 1;
}
return -1;
});
return chunkFileList;
}
//合并文件
private File mergeFile(File mergeFile, List<File> chunkFiles) {
try {
//创建写文件对象
RandomAccessFile raf_write = new RandomAccessFile(mergeFile, "rw");
//遍历分块文件开始合并
// 读取文件缓冲区
byte[] b = new byte[1024];
for (File chunkFile : chunkFiles) {
RandomAccessFile raf_read = new RandomAccessFile(chunkFile, "r");
int len = -1;
//读取分块文件
while ((len = raf_read.read(b)) != -1) {
//向合并文件中写数据
raf_write.write(b, 0, len);
}
raf_read.close();
}
raf_write.close();
} catch (Exception e) {
e.printStackTrace();
return null;
}
return mergeFile;
}
}
八:涉及到的四个工具类
package cn.ybl.util;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
public class ConvertVideo {
private static String inputPath = "";
private static String outputPath = "";
private static String ffmpegPath = "";
public static void main(String args[]) throws IOException {
getPath();
if (!checkfile(inputPath)) {
System.out.println(inputPath + " is not file");
return;
}
if (processFlv("d:\\work\\hls\\habor.avi")) {
System.out.println("ok");
}
}
public static void getPath() {
// 先获取当前项目路径,在获得源文件、目标文件、转换器的路径
File diretory = new File("d:\\work\\hls\\");
try {
String currPath = diretory.getAbsolutePath();
inputPath = "d:\\work\\hls\\habor.avi";
outputPath = "d:\\work\\hls\\";
ffmpegPath = "D:\\opensource\\ffmpeg-20180227-fa0c9d6-win64-static\\bin\\";
System.out.println(currPath);
}
catch (Exception e) {
System.out.println("getPath出错");
}
}
public static boolean process() {
int type = checkContentType();
boolean status = false;
System.out.println("直接转成mp4格式");
status = processMp4(inputPath);// 直接转成mp4格式
return status;
}
private static int checkContentType() {
String type = inputPath.substring(inputPath.lastIndexOf(".") + 1, inputPath.length())
.toLowerCase();
// ffmpeg能解析的格式:(asx,asf,mpg,wmv,3gp,mp4,mov,avi,flv等)
if (type.equals("avi")) {
return 0;
} else if (type.equals("mpg")) {
return 0;
} else if (type.equals("wmv")) {
return 0;
} else if (type.equals("3gp")) {
return 0;
} else if (type.equals("mov")) {
return 0;
} else if (type.equals("mp4")) {
return 0;
} else if (type.equals("asf")) {
return 0;
} else if (type.equals("asx")) {
return 0;
} else if (type.equals("flv")) {
return 0;
}
// 对ffmpeg无法解析的文件格式(wmv9,rm,rmvb等),
// 可以先用别的工具(mencoder)转换为avi(ffmpeg能解析的)格式.
else if (type.equals("wmv9")) {
return 1;
} else if (type.equals("rm")) {
return 1;
} else if (type.equals("rmvb")) {
return 1;
}
return 9;
}
private static boolean checkfile(String path) {
File file = new File(path);
if (!file.isFile()) {
return false;
}
return true;
}
// 对ffmpeg无法解析的文件格式(wmv9,rm,rmvb等), 可以先用别的工具(mencoder)转换为avi(ffmpeg能解析的)格式.
private static String processAVI(int type) {
List<String> commend = new ArrayList<String>();
commend.add(ffmpegPath + "mencoder");
commend.add(inputPath);
commend.add("-oac");
commend.add("lavc");
commend.add("-lavcopts");
commend.add("acodec=mp3:abitrate=64");
commend.add("-ovc");
commend.add("xvid");
commend.add("-xvidencopts");
commend.add("bitrate=600");
commend.add("-of");
commend.add("mp4");
commend.add("-o");
commend.add(outputPath + "a.AVI");
try {
ProcessBuilder builder = new ProcessBuilder();
Process process = builder.command(commend).redirectErrorStream(true).start();
new PrintStream(process.getInputStream());
new PrintStream(process.getErrorStream());
process.waitFor();
return outputPath + "a.AVI";
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
// ffmpeg能解析的格式:(asx,asf,mpg,wmv,3gp,mp4,mov,avi,flv等)
private static boolean processFlv(String oldfilepath) {
if (!checkfile(inputPath)) {
System.out.println(oldfilepath + " is not file");
return false;
}
List<String> command = new ArrayList<String>();
command.add(ffmpegPath + "ffmpeg");
command.add("-i");
command.add(oldfilepath);
command.add("-ab");
command.add("56");
command.add("-ar");
command.add("22050");
command.add("-qscale");
command.add("8");
command.add("-r");
command.add("15");
command.add("-s");
command.add("1920x1080");
command.add(outputPath + "a.flv");
try {
// 方案1
// Process videoProcess = Runtime.getRuntime().exec(ffmpegPath + "ffmpeg -i " + oldfilepath
// + " -ab 56 -ar 22050 -qscale 8 -r 15 -s 600x500 "
// + outputPath + "a.flv");
// 方案2
Process videoProcess = new ProcessBuilder(command).redirectErrorStream(true).start();
new PrintStream(videoProcess.getErrorStream()).start();
new PrintStream(videoProcess.getInputStream()).start();
videoProcess.waitFor();
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
private static boolean processMp4(String oldfilepath) {
if (!checkfile(inputPath)) {
System.out.println(oldfilepath + " is not file");
return false;
}
List<String> command = new ArrayList<String>();
command.add(ffmpegPath + "ffmpeg");
command.add("-i");
command.add(oldfilepath);
command.add("-c:v");
command.add("libx264");
command.add("-mbd");
command.add("0");
command.add("-c:a");
command.add("aac");
command.add("-strict");
command.add("-2");
command.add("-pix_fmt");
command.add("yuv420p");
command.add("-movflags");
command.add("faststart");
command.add(outputPath + "a.mp4");
try {
// 方案1
// Process videoProcess = Runtime.getRuntime().exec(ffmpegPath + "ffmpeg -i " + oldfilepath
// + " -ab 56 -ar 22050 -qscale 8 -r 15 -s 600x500 "
// + outputPath + "a.flv");
// 方案2
Process videoProcess = new ProcessBuilder(command).redirectErrorStream(true).start();
new PrintStream(videoProcess.getErrorStream()).start();
new PrintStream(videoProcess.getInputStream()).start();
videoProcess.waitFor();
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
}
class PrintStream extends Thread
{
java.io.InputStream __is = null;
public PrintStream(java.io.InputStream is)
{
__is = is;
}
public void run()
{
try
{
while(this != null)
{
int _ch = __is.read();
if(_ch != -1)
System.out.print((char)_ch);
else break;
}
}
catch (Exception e)
{
e.printStackTrace();
}
}
}
package cn.ybl.util;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
/**
* 此文件用于视频文件处理,步骤如下:
* 1、生成mp4
* 2、生成m3u8
*
*/
public class HlsVideoUtil extends VideoUtil {
//ffmpeg的安装位置
String ffmpeg_path = null;
//要推流的本地视频
String video_path = null;
//本地推流后的媒体文件名
String m3u8_name = null;
//本地推流后的媒体文件路径
String m3u8folder_path = null;
//云端推流,srs服务器路径
String srsPath = null;
//云端推流,srs服务器路径
String fileName = null;
//生成云端推流使用
public HlsVideoUtil(String ffmpeg_path){
super(ffmpeg_path);
this.ffmpeg_path = ffmpeg_path;
}
/**
* @param srsPath :srs服务器地址
* @param video_path :本地视频
* @param fileName :推流后的文件名
*/
public void init(String srsPath, String video_path, String fileName){
this.srsPath = srsPath;
this.video_path = video_path;
this.fileName = fileName;
}
//生成本地推流使用
public HlsVideoUtil(String ffmpeg_path, String video_path, String m3u8_name,String m3u8folder_path){
super(ffmpeg_path);
this.ffmpeg_path = ffmpeg_path;
this.video_path = video_path;
this.m3u8_name = m3u8_name;
this.m3u8folder_path = m3u8folder_path;
}
private void clear_m3u8(String m3u8_path){
//删除原来已经生成的m3u8及ts文件
File m3u8dir = new File(m3u8_path);
if(!m3u8dir.exists()){
m3u8dir.mkdirs();
}
/* if(m3u8dir.exists()&&m3u8_path.indexOf("/hls/")>=0){//在hls目录方可删除,以免错误删除
String[] children = m3u8dir.list();
//删除目录中的文件
for (int i = 0; i < children.length; i++) {
File file = new File(m3u8_path, children[i]);
file.delete();
}
}else{
m3u8dir.mkdirs();
}*/
}
/**
* 生成m3u8文件 , 推流到云端
* @return 成功则返回success,失败返回控制台日志
*/
public String generateM3u8ToSrs(){
//ffmpeg -re -i C:\Users\whale\Desktop\1dd013223fc66fe3166c800a6909099f.mp4 -vcodec libx264 -acodec aac -strict -2 -f flv rtmp://115.159.88.63/live/1dd013223fc66fe3166c800a6909099f
List<String> commend = new ArrayList<String>();
commend.add(ffmpeg_path);
commend.add("-re");
commend.add("-i");
commend.add(video_path);
commend.add("-vcodec");
commend.add("libx264");
commend.add("-acodec");
commend.add("aac");
commend.add("-strict");
commend.add("-2");
commend.add("-f");
commend.add("flv");
commend.add(srsPath+fileName);
System.out.println("===================================================================================================");
commend.forEach(s -> {
System.out.print(s+" ");
});
System.out.println();
System.out.println("===================================================================================================");
String outstring = null;
try {
ProcessBuilder builder = new ProcessBuilder();
builder.command(commend);
//将标准输入流和错误输入流合并,通过标准输入流程读取信息
builder.redirectErrorStream(true);
Process p = builder.start();
outstring = waitFor(p);
System.out.println(outstring);
} catch (Exception ex) {
ex.printStackTrace();
}
return "success";
}
/**
* 生成m3u8文件
* @return 成功则返回success,失败返回控制台日志
*/
public String generateM3u8(){
//清理m3u8文件目录
clear_m3u8(m3u8folder_path);
/*
ffmpeg -i lucene.mp4 -hls_time 10 -hls_list_size 0 -hls_segment_filename ./hls/lucene_%05d.ts ./hls/lucene.m3u8
*/
// String m3u8_name = video_name.substring(0, video_name.lastIndexOf("."))+".m3u8";
List<String> commend = new ArrayList<String>();
commend.add(ffmpeg_path);
commend.add("-i");
commend.add(video_path);
commend.add("-hls_time");
commend.add("10");
commend.add("-hls_list_size");
commend.add("0");
commend.add("-hls_segment_filename");
// commend.add("D:/BaiduNetdiskDownload/Movies/test1/test1_%05d.ts");
commend.add(m3u8folder_path + m3u8_name.substring(0,m3u8_name.lastIndexOf(".")) + "_%05d.ts");
// commend.add("D:/BaiduNetdiskDownload/Movies/test1/test1.m3u8");
commend.add(m3u8folder_path + m3u8_name );
System.out.println(commend);
String outstring = null;
try {
ProcessBuilder builder = new ProcessBuilder();
builder.command(commend);
//将标准输入流和错误输入流合并,通过标准输入流程读取信息
builder.redirectErrorStream(true);
Process p = builder.start();
outstring = waitFor(p);
} catch (Exception ex) {
ex.printStackTrace();
}
//通过查看视频时长判断是否成功
Boolean check_video_time = check_video_time(video_path, m3u8folder_path + m3u8_name);
if(!check_video_time){
return outstring;
}
//通过查看m3u8列表判断是否成功
List<String> ts_list = get_ts_list();
if(ts_list == null){
return outstring;
}
return "success";
}
/**
* 检查视频处理是否完成
* @return ts列表
*/
public List<String> get_ts_list() {
// String m3u8_name = video_name.substring(0, video_name.lastIndexOf("."))+".m3u8";
List<String> fileList = new ArrayList<String>();
List<String> tsList = new ArrayList<String>();
String m3u8file_path =m3u8folder_path + m3u8_name;
BufferedReader br = null;
String str = null;
String bottomline = "";
try {
br = new BufferedReader(new FileReader(m3u8file_path));
while ((str = br.readLine()) != null) {
bottomline = str;
if(bottomline.endsWith(".ts")){
tsList.add(bottomline);
}
//System.out.println(str);
}
} catch (IOException e) {
e.printStackTrace();
}finally {
if(br!=null){
try {
br.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
if (bottomline.contains("#EXT-X-ENDLIST")) {
// fileList.add(hls_relativepath+m3u8_name);
fileList.addAll(tsList);
return fileList;
}
return null;
}
public static void main(String[] args) throws IOException {
//generateM3u8ToSrs();
// String ffmpeg_path = "D:\\opensource\\ffmpeg-20180227-fa0c9d6-win64-static\\bin\\ffmpeg";//ffmpeg的安装位置
// String video_path = "D:\\work\\hls\\lucene.mp4";
// String m3u8_name = "lucene.m3u8";
// String m3u8_path = "D:\\work\\hls\\hls\\";
// HlsVideoUtil videoUtil = new HlsVideoUtil(ffmpeg_path,video_path,m3u8_name,m3u8_path);
// String s = videoUtil.generateM3u8();
// System.out.println(s);
// System.out.println(videoUtil.get_ts_list());
}
}
package cn.ybl.util;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
/**
* Created by admin on 2018/3/6.
*/
public class Mp4VideoUtil extends VideoUtil {
String ffmpeg_path ;//ffmpeg的安装位置
String video_path ;//视频文件保存地址
String mp4_name ; //转换后的mp4地址
String mp4folder_path ;
public Mp4VideoUtil(String ffmpeg_path, String video_path, String mp4_name, String mp4folder_path){
super(ffmpeg_path);
this.ffmpeg_path = ffmpeg_path;
this.video_path = video_path;
this.mp4_name = mp4_name;
this.mp4folder_path = mp4folder_path;
}
//清除已生成的mp4
private void clear_mp4(String mp4_path){
//删除原来已经生成的m3u8及ts文件
File mp4File = new File(mp4_path);
if(mp4File.exists() && mp4File.isFile()){
mp4File.delete();
}
}
/**
* 视频编码,生成mp4文件
* @return 成功返回success,失败返回控制台日志
*/
public String generateMp4(){
//清除已生成的mp4
clear_mp4(mp4folder_path+mp4_name);
/*
ffmpeg.exe -i lucene.avi -c:v libx264 -s 1280x720 -pix_fmt yuv420p -b:a 63k -b:v 753k -r 18 .\lucene.mp4
*/
List<String> commend = new ArrayList<String>();
//commend.add("D:\\Program Files\\ffmpeg-20180227-fa0c9d6-win64-static\\bin\\ffmpeg.exe");
commend.add(ffmpeg_path);
commend.add("-i");
// commend.add("D:\\BaiduNetdiskDownload\\test1.avi");
commend.add(video_path);
commend.add("-c:v");
commend.add("libx264");
commend.add("-y");//覆盖输出文件
commend.add("-s");
commend.add("1280x720");
commend.add("-pix_fmt");
commend.add("yuv420p");
commend.add("-b:a");
commend.add("63k");
commend.add("-b:v");
commend.add("753k");
commend.add("-r");
commend.add("18");
commend.add(mp4folder_path + mp4_name );
String outstring = null;
try {
ProcessBuilder builder = new ProcessBuilder();
builder.command(commend);
//将标准输入流和错误输入流合并,通过标准输入流程读取信息
builder.redirectErrorStream(true);
Process p = builder.start();
outstring = waitFor(p);
} catch (Exception ex) {
ex.printStackTrace();
}
Boolean check_video_time = this.check_video_time(video_path, mp4folder_path + mp4_name);
if(!check_video_time){
return outstring;
}else{
return "success";
}
}
public static void main(String[] args) throws IOException {
String ffmpeg_path = "D:\\opensource\\ffmpeg-20180227-fa0c9d6-win64-static\\bin\\ffmpeg";//ffmpeg的安装位置
String video_path = "D:\\work\\hls\\habor.avi";
String mp4_name = "habor.mp4";
String mp4folder_path = "D:\\work\\hls\\";
Mp4VideoUtil videoUtil = new Mp4VideoUtil(ffmpeg_path,video_path,mp4_name,mp4folder_path);
String s = videoUtil.generateMp4();
System.out.println(s);
}
}
package cn.ybl.util;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;
/**
* 此文件作为视频文件处理父类,提供:
* 1、查看视频时长
* 2、校验两个视频的时长是否相等
*
*/
public class VideoUtil {
String ffmpeg_path = "D:\\opensource\\ffmpeg\\bin";//ffmpeg的安装位置
public VideoUtil(String ffmpeg_path){
this.ffmpeg_path = ffmpeg_path;
}
//检查视频时间是否一致
public Boolean check_video_time(String source,String target) {
String source_time = get_video_time(source);
//取出时分秒
source_time = source_time.substring(0,source_time.lastIndexOf("."));
String target_time = get_video_time(target);
System.out.println(target_time);
//取出时分秒
target_time = target_time.substring(0,target_time.lastIndexOf("."));
System.out.println(target_time);
if(source_time == null || target_time == null){
return false;
}
if(source_time.equals(target_time)){
return true;
}
return false;
}
//获取视频时间(时:分:秒:毫秒)
public String get_video_time(String video_path) {
/*
ffmpeg -i lucene.mp4
*/
List<String> commend = new ArrayList<String>();
commend.add(ffmpeg_path);
commend.add("-i");
commend.add(video_path);
try {
ProcessBuilder builder = new ProcessBuilder();
builder.command(commend);
//将标准输入流和错误输入流合并,通过标准输入流程读取信息
builder.redirectErrorStream(true);
Process p = builder.start();
String outstring = waitFor(p);
System.out.println(outstring);
int start = outstring.trim().indexOf("Duration: ");
if(start>=0){
int end = outstring.trim().indexOf(", start:");
if(end>=0){
String time = outstring.substring(start+10,end);
if(time!=null && !time.equals("")){
return time.trim();
}
}
}
} catch (Exception ex) {
ex.printStackTrace();
}
return null;
}
public String waitFor(Process p) {
InputStream in = null;
InputStream error = null;
String result = "error";
int exitValue = -1;
StringBuffer outputString = new StringBuffer();
try {
in = p.getInputStream();
error = p.getErrorStream();
boolean finished = false;
int maxRetry = 600;//每次休眠1秒,最长执行时间10分种
int retry = 0;
while (!finished) {
if (retry > maxRetry) {
return "error";
}
try {
while (in.available() > 0) {
Character c = new Character((char) in.read());
outputString.append(c);
System.out.print(c);
}
while (error.available() > 0) {
Character c = new Character((char) in.read());
outputString.append(c);
System.out.print(c);
}
//进程未结束时调用exitValue将抛出异常
exitValue = p.exitValue();
finished = true;
} catch (IllegalThreadStateException e) {
Thread.currentThread().sleep(1000);//休眠1秒
retry++;
}
}
} catch (Exception e) {
e.printStackTrace();
} finally {
if (in != null) {
try {
in.close();
} catch (IOException e) {
System.out.println(e.getMessage());
}
}
}
return outputString.toString();
}
public static void main(String[] args) throws IOException {
String ffmpeg_path = "D:\\opensource\\ffmpeg-20180227-fa0c9d6-win64-static\\bin\\ffmpeg";//ffmpeg的安装位置
VideoUtil videoUtil = new VideoUtil(ffmpeg_path);
String video_time = videoUtil.get_video_time("D:\\work\\hls\\hls\\hello.m3u8");
System.out.println(video_time);
}
}