摘要:

视频上传采用断点续传的模式,在前端上传视频到后端时使用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);
    }
}