开源项目点播方案-上海怡健医学

HLS的工作方式是:将视频分割成多个ts格式的小文件,通过m3u8格式的索引文件对这些m3u8格式的小文件进行索引。一般每10秒一个ts文件html 多视频播放器代码,播放器连接m3u8文件播放。快进时,可以通过m3u8找到对应的index文件,下载对应的ts文件,实现快进快退,近乎实时地播放视频。

IOS、Android设备和主流浏览器都支持HLS协议。

详细参考:

使用HLS方案,可以同时下载和播放,不能使用rtmp等流媒体协议,也无需搭建专用媒体服务器,节省成本。

本项目的按需方案确定为方案3。

2 FFmpeg的基本使用

录制完视频后,我们使用视频编码软件对视频进行编码。本项目使用 FFmpeg 对视频进行编码。

下载:ffmpeg-20180227-fa0c9d6-win64-static.zip,解压。本教程将ffmpeg解压到C:Java_Softxczxffmpeg-20180227-fa0c9d6-win64-static。

FFmpeg被很多开源项目采用,如QQ视频、暴风视频、VLC等。

下载:FFmpeg #build-windows

在path环境变量中配置C:Java_Softxczxffmpeg-20180227-fa0c9d6-win64-staticbin目录。

简单测试:

将 .avi 文件转换为 mp4、mp3、gif 等

比如我们把lucene.avi文件转成mp4,运行如下命令:

ffmpeg -i lucene.avi lucene.mp4

转为mp3:ffmpeg -i lucene.avi lucene.mp3

转成gif:ffmpeg -i lucene.avi lucene.gif

官方文档(英文):

2.1 生成m3u8/ts文件

使用ffmpeg生成m3u8的步骤如下:

第一步:先将avi视频转为mp4

ffmpeg.exe -i  lucene.avi -c:v libx264 -s 1280x720 -pix_fmt yuv420p -b:a 63k -b:v 753k -r 18 .lucene.mp4

简单说一下各个参数的含义,理解含义就够了。我不会在这里解释流媒体专业知识。

-c:v 视频编码为x264,是H264的开源编码格式。

-s 设置分辨率

-pix_fmt yuv420p:设置像素采样方式,主流的采样方式有YUV4:4:4、YUV4:2:2、YUV4:2:0三种,其作用是从码流中恢复采样方式 每个像素的 YUV(亮度和颜色信息)值。

-b 设置码率,-b:a 和-b:v 分别代表音频和视频的码率,-b 代表音频加视频的总码率。比特率对视频质量有很大影响。

-r:帧率,表示图像每秒更新的次数,一般大于24,肉眼没有连贯感和停顿感。

第二步:从mp4生成m3u8

 ffmpeg -i  lucene.mp4   -hls_time 10 -hls_list_size 0  -hls_segment_filename ./hls/lucene_d.ts ./hls/lucene.m3u8

-hls_time 以秒为单位设置每个切片的长度

-hls_list_size n:要保存的分片数,设置为0保存所有分片

-hls_segment_filename : 段文件名,d代表5位数字

生成的效果是:lucene.mp4视频文件每隔10秒生成一个ts文件,最后生成一个m3u8文件,就是ts的索引文件。

使用VLC打开m3u8文件,测试播放效果。 VLC是一个免费开源的跨平台多媒体播放器和框架,可以播放大部分多媒体文件,以及DVD、音频CD、VCD和各种流媒体协议。 ()

3.视频(媒体资产)处理开发环境1.创建媒体资产数据库

导入shcool.sql

2. 创建媒体资产服务项目

基于springboot创建项目

3.上传文件3.简历上传的1个解决方案

通常视频文件比较大,所以从媒体资产系统上传文件的要求必须满足大文件的上传要求。 http协议本身对上传文件的大小没有限制,但是客户的网络环境质量和电脑硬件环境参差不齐。是致命的,所以上传大文件最基本的要求就是从断点处恢复上传。

什么是恢复:

引用百度百科:断点续传是指在下载或上传时手动将下载或上传任务(文件或压缩包)分成若干部分,每部分使用一个线程进行上传或上传。下载时,如果遇到网络故障,可以从已经上传或下载的部分继续上传和下载未完成的部分,无需从头开始上传和下载。断点续传可以节省操作时间,提升用户体验。

上传过程如下:

1、在上传之前将文件分成块

2、一块一块上传,上传中断后重新上传,已经上传的块不重新上传就上传

3、上传每个chunk后的最终合并文件

文件下载也是如此。

3.2 文件分块和合并

为了更好的理解分块上传文件的原理,下面使用java代码来测试文件的分块和合并。

3.3 个文件分区

文件分区的过程如下:

1、获取源文件长度

2、根据设置的块文件大小计算块数

3、从源文件中读取数据,依次向每个块文件中写入数据

//测试文件分块方法
    @Test
    public void testChunk() throws IOException {
        File sourceFile = new File("F:/develop/ffmpeg/lucene.mp4");
//        File sourceFile = new File("d:/logo.png");
        String chunkPath = "F:/develop/ffmpeg/chunk/";
        File chunkFolder = new File(chunkPath);
        if(!chunkFolder.exists()){
            chunkFolder.mkdirs();
        }
        //分块大小
        long chunkSize = 1024*1024*1;
        //分块数量
        long chunkNum = (long) Math.ceil(sourceFile.length() * 1.0 / chunkSize );
        if(chunkNum<=0){
            chunkNum = 1;
        }
        //缓冲区大小
        byte[] b = new byte[1024];
        //使用RandomAccessFile访问文件
        RandomAccessFile raf_read = new RandomAccessFile(sourceFile, "r");
        //分块
        for(int i=0;ichunkSize){
                        break;
                    }
                }
                raf_write.close();
            }
        }
        raf_read.close();
​
    }

3.4个文件合并

文件合并过程:

1、找到要合并的文件,按照合并的顺序排序

2、创建合并文件

3、从合并文件中依次读取数据到合并文件中

//测试文件合并方法
@Test
public void testMerge() throws IOException {
    //块文件目录
    File chunkFolder = new File("F:/develop/ffmpeg/chunk/");
    //合并文件
    File mergeFile = new File("F:/develop/ffmpeg/lucene1.mp4");
    if(mergeFile.exists()){
        mergeFile.delete();
    }
    //创建新的合并文件
    mergeFile.createNewFile();
    //用于写文件
    RandomAccessFile raf_write = new RandomAccessFile(mergeFile, "rw");
    //指针指向文件顶端
    raf_write.seek(0);
    //缓冲区
    byte[] b = new byte[1024];
    //分块列表
    File[] fileArray = chunkFolder.listFiles();
    // 转成集合,便于排序
    List fileList = new ArrayList(Arrays.asList(fileArray));
    // 从小到大排序
    Collections.sort(fileList, new Comparator() {
        @Override
        public int compare(File o1, File o2) {
            if (Integer.parseInt(o1.getName()) < Integer.parseInt(o2.getName())) {
                return -1;
            }
            return 1;
        }
    });
    //合并文件
    for(File chunkFile:fileList){
        RandomAccessFile raf_read = new RandomAccessFile(chunkFile,"rw");
        int len = -1;
        while((len=raf_read.read(b))!=-1){
            raf_write.write(b,0,len);
​
        }
        raf_read.close();
    }
    raf_write.close();
​
}

4.前端WebUploader介绍

如何在网页上实现可续传?

常见的场景包括:

1、通过 Flash 上传,如 SWFupload、Uploadify。

2、安装浏览器插件,伪装成PC客户端,很少使用。

3、HTML5

随着html5的普及,本项目使用Html5完成分块上传文件。

本项目使用WebUploader完成大文件上传功能的开发。 WebUploader官网地址:

使用WebUploader上传过程如下:

挂钩方法

webuploader 中有很多可用的钩子方法,下面列出了一些重要的方法:

本项目使用以下钩子方法:

1)发送前文件

在开始分割文件之前调用,可以在上传文件之前做一些准备工作,比如检查文件目录是否创建等

2)发送前

在上传文件段之前调用此方法。您可以请求服务器检查该段是否存在。如果已存在,则不会上传该片段。

3)发送后文件

在所有分块上传完成后触发,可以请求服务器合并分块文件。

注册钩子方法源码:

 
WebUploader.Uploader.register({
    "before-send-file":"beforeSendFile",
    "before-send":"beforeSend",
    "after-send-file":"afterSendFile"
  }

构建网络上传器

在使用 webUploader 之前,您需要创建一个 webUploader 对象。

指定上传chunk的地址:/api/media/upload/uploadchunk

 
// 创建uploader对象,配置参数
this.uploader = WebUploader.create(
  {
    swf:"/static/plugins/webuploader/dist/Uploader.swf",//上传文件的flash文件,浏览器不支持h5时启动flash
    server:"/api/media/upload/uploadchunk",//上传分块的服务端地址,注意跨域问题
    fileVal:"file",//文件上传域的name
    pick:"#picker",//指定选择文件的按钮容器
    auto:false,//手动触发上传
    disableGlobalDnd:true,//禁掉整个页面的拖拽功能
    chunked:true,// 是否分块上传
    chunkSize:1*1024*1024, // 分块大小(默认5M)
    threads:3, // 开启多个线程(默认3个)
    prepareNextFile:true// 允许在文件传输时提前把下一个文件准备好
  }
)

发送前文件

文件开始上传前,前端请求服务器准备上传。

type:"POST",
url:"/api/media/upload/register",
data:{
  // 文件唯一表示
  fileMd5:this.fileMd5,
  fileName: file.name,
  fileSize:file.size,
  mimetype:file.type,
  fileExt:file.ext
}

发送前

在上传区块之前,前端会请求服务器验证区块是否存在。

 
type:"POST",
url:"/api/media/upload/checkchunk",
data:{
  // 文件唯一表示
  fileMd5:this.fileMd5,
  // 当前分块下标
  chunk:block.chunk,
  // 当前分块大小
  chunkSize:block.end-block.start
}

文件发送后

在所有分块上传完成后触发,可以请求服务器合并分块文件。

 
type:"POST",
url:"/api/media/upload/mergechunks",
data:{
  fileMd5:this.fileMd5,
  fileName: file.name,
  fileSize:file.size,
  mimetype:file.type,
  fileExt:file.ext
}

页面效果

学生使用:

直接解压数据xc-ui-pc-teach到webstorm工作目录通过nginx访问,在nginx中配置(解决跨域)

server {
        listen       82;     
        server_name localhost;     
        
        #视频中心     
        location / {        
            proxy_pass http://127.0.0.1:12000; 
            proxy_set_header Host $http_host;     
            add_header Access-Control-Allow-Origin *;         
            add_header Access-Control-Allow-Credentials true;           
            add_header Access-Control-Allow-Methods GET;            
        } 
​
        #媒资管理后台跨域     
        location ^~ /api/media/ {       
            proxy_pass http://127.0.0.1:9000/media/; 
​
            proxy_set_header Host $http_host;  
            add_header Access-Control-Allow-Origin *;         
            add_header Access-Control-Allow-Credentials true;           
            add_header Access-Control-Allow-Methods "GET,POST,OPTIONS";         
        }                
    }

您可以访问:12000/#/media/upload或:82/#/media/upload/查看页面效果。

5.媒体资产服务器编写

服务器需要实现以下功能:

1、上传前检查上传环境

检查文件是否上传,如果已经上传则直接返回。

检查文件上传路径是否存在,不存在则创建。

2、块检查

检查分块文件是否上传,如果已经上传则返回true。

如果没有上传,检查上传路径是否存在。如果不存在,则创建它。

3、分块上传

将分块文件上传到指定路径。

4、合并块

将所有分块文件合并为一个文件。

在数据库中记录文件信息。

配置

application.yml 配置上传文件的路径:

xc-service-manage-media:
  upload-location: C:/school/video/  # 媒资保存路径
  ffmpeg-path: C:/Java_Soft/xczx/ffmpeg-20180227-fa0c9d6-win64-static/bin/ffmpeg.exe # ffmpeg路径

定义道

使用mybatis-plus

@Mapper
@Component
public interface FileMsgMapper extends BaseMapper {
}

定义控制器

@RestController
@RequestMapping("/media/upload")
public class MediaUploadController {
​
​
    @Autowired
    MediaUploadService mediaUploadService;
​
    /**
     * 文件上传前的注册
     */
​
    @PostMapping("/register")
    public ResponseResult register(String fileMd5, String fileName, Long fileSize, String mimetype, String fileExt) {
​
        return mediaUploadService.register(fileMd5, fileName, fileSize, mimetype, fileExt);
​
    }
​
    /**
     * 检测分块
     *
     * @param fileMd5
     * @param chunk
     * @param chunkSize
     * @return
     */
​
    @PostMapping("/checkchunk")
    public ResponseResult checkchunk(String fileMd5, Integer chunk, Integer chunkSize) {
        return mediaUploadService.checkchunk(fileMd5, chunk, chunkSize);
    }
​
    /**
     * 上传分块
     *
     * @param file
     * @param fileMd5
     * @param chunk
     * @return
     */
    @PostMapping("/uploadchunk")
    public ResponseResult uploadchunk(MultipartFile file, String fileMd5, Integer chunk) {
        return mediaUploadService.uploadchunk(file, fileMd5, chunk);
    }
​
    /**
     * 合并分块
     *
     * @param fileMd5
     * @param fileName
     * @param fileSize
     * @param mimetype
     * @param fileExt
     * @return
     */
    @PostMapping("/mergechunks")
    public ResponseResult mergechunks(String fileMd5, String fileName, Long fileSize, String mimetype, String fileExt) {
        return mediaUploadService.mergechunks(fileMd5, fileName, fileSize, mimetype, fileExt);
    }
}
​

定义服务(略)由于代码过多,请参考源码。

现在视频已经通过恢复上传的方式上传到了我们的媒体资产服务器,接下来就是使用FFmpeg将视频转换成流媒体了。

6.视频处理技术

如何以编程方式进行视频处理?

ffmpeg是一个可行的视频处理程序,可以通过Java调用ffmpeg.exe完成视频处理。

在java中,可以使用Runtime类和Process Builder类来执行外部程序,工作中至少要掌握一个。

本项目使用Process Builder调用ffmpeg完成视频处理。

Process Builder 的测试如下:

 @Test
    public void testProcessBuilder(){
        ProcessBuilder processBuilder = new ProcessBuilder();
//       processBuilder.command("ping","127.0.0.1");
       processBuilder.command("ipconfig");
        //将标准输入流和错误输入流合并,通过标准输入流读取信息
        processBuilder.redirectErrorStream(true);
        try {
            //启动进程
            Process start = processBuilder.start();
            //获取输入流
            InputStream inputStream = start.getInputStream();
            //转成字符输入流
            InputStreamReader inputStreamReader = new InputStreamReader(inputStream,"gbk");
            int len = -1;
            char[] c = new char[1024];
            StringBuffer outputString = new StringBuffer();
            //读取进程输入流中的内容
            while ((len= inputStreamReader.read(c))!=-1) {
                String s = new String(c,0,len);
                outputString.append(s);
                System.out.print(s);
            }
            inputStream.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    @Test
    public void testFFmpeg(){
        ProcessBuilder processBuilder = new ProcessBuilder();
        //定义命令内容
        List command = new ArrayList();
        command.add("D:\Program Files\ffmpeg-20180227-fa0c9d6-win64-static\bin\ffmpeg.exe");
        command.add("-i");
        command.add("E:\ffmpeg_test\1.avi");
        command.add("-y");//覆盖输出文件
        command.add("-c:v");
        command.add("libx264");
        command.add("-s");
        command.add("1280x720");
        command.add("-pix_fmt");
        command.add("yuv420p");
        command.add("-b:a");
        command.add("63k");
        command.add("-b:v");
        command.add("753k");
        command.add("-r");
        command.add("18");
        command.add("E:\ffmpeg_test\1.mp4");
        processBuilder.command(command);
        //将标准输入流和错误输入流合并,通过标准输入流读取信息
        processBuilder.redirectErrorStream(true);
        try {
            //启动进程
            Process start = processBuilder.start();
            //获取输入流
            InputStream inputStream = start.getInputStream();
            //转成字符输入流
            InputStreamReader inputStreamReader = new InputStreamReader(inputStream,"gbk");
            int len = -1;
            char[] c = new char[1024];
            StringBuffer outputString = new StringBuffer();
            //读取进程输入流中的内容
            while ((len= inputStreamReader.read(c))!=-1) {
                String s = new String(c,0,len);
                outputString.append(s);
                System.out.print(s);
            }
            inputStream.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

我们已经准备好了工具类:

Mp4VideoUtil.java完成avi到mp4的转换

HlsVideoUtil.java完成mp4到hls的转换

可以直接使用。

6.1 处理流程

视频合并成功时

将视频处理成m3u8流媒体并将视频信息写入数据库

具体代码:在合并完成方法中调用

    @Value("${xc-service-manage-media.ffmpeg-path}")
    String ffmpeg_path;
​
​
    private void ChangeHLS(File mergeFile, String fileMd5) {
​
        // mp4 文件保存目录
        String fileFolderPath = getFileFolderPath(fileMd5);
        //生成的mp4的文件名称
        String mp4_name = fileMd5 + ".mp4";
        //生成的mp4所在的路径
        String mp4folder_path = fileFolderPath;
​
​
        //创建工具类对象
        Mp4VideoUtil mp4VideoUtil = new Mp4VideoUtil(ffmpeg_path, mergeFile.getAbsolutePath(), mp4_name, mp4folder_path);
​
        //进行处理
        String result = mp4VideoUtil.generateMp4();
​
​
        //4、将mp4生成m3u8和ts文件
        //String ffmpeg_path, String video_path, String m3u8_name,String m3u8folder_path
        //mp4视频文件路径
        String mp4_video_path = mp4folder_path + mp4_name;
​
        //m3u8_name文件名称
        String m3u8_name = fileMd5 + ".m3u8";
        //m3u8文件所在目录
        String m3u8folder_path = fileFolderPath + "hls/";
​
        HlsVideoUtil hlsVideoUtil = new HlsVideoUtil(ffmpeg_path, mp4_video_path, m3u8_name, m3u8folder_path);
​
        //生成m3u8和ts文件
        String tsResult = hlsVideoUtil.generateM3u8();
​
        //保存fileUrl(此url就是视频播放的相对路径)
        String filePath = fileFolderPath + "hls/" + m3u8_name;
​
​
        // 将fileUrl 保存到数据库
        FileMsg fileMsg = new FileMsg();
        fileMsg.setFileId(fileMd5);
        fileMsg.setFileName(mergeFile.getName());
        fileMsg.setFilePath(filePath);
        fileMsg.setFileUrl(filePath.split("C:/school/video")[1]);
​
        fileMsgMapper.insert(fileMsg);
​
​
        //优化:  文件处理成功后, 可以删除mp4文件
​
​
    }

说明:

如何判断mp4转m3u8是否成功?

第一个一、是根据视频的长度来判断,和判断mp4转换成功的方式是一样的。

最后一个二、是判断m3u8文件内容是否完整。

4 玩家4.1 技术选择

视频编码后,使用播放器解码,播放视频内容。 Web应用中常用的播放器有flash播放器、H5播放器或浏览器插件播放器,其中flash和H5播放器最为常见。

flash player:缺点是需要在客户端电脑上安装 Adob​​e Flash Player。优点是flash播放器非常成熟,浏览器对flash的支持非常好。

H5播放器:基于h5自带的video标签,优点是大部分浏览器都支持H5,无需安装第三方flash播放器,而且随着前端技术的发展,h5技术会越来越成熟.

本项目采用H5播放器和Video.js开源播放器。

Video.js 是一个基于 HTML5 世界的网络视频播放器。它支持 HTML5 和 Flash 视频html 多视频播放器代码,并且支持在桌面和移动设备上播放视频。该项目于 2010 年年中启动,目前有 400,000 个网站使用。

官方地址:

4.2 下载video.js

视频.js:

videojs-contrib-hls: #安装

(videojs-contrib-hls是一个播放hls的插件)

使用文档:

本教程使用video.js 6.7.version 3,videojs-contrib-hls 5.14.version 1

将数据中提供的插件分配到xc-ui-pc-video项目目录

4.3 搭建媒体服务器

正常使用 video.js 播放视频是通过网页。用户通过浏览器打开网页来播放视频。网页和视频都是从网络服务器请求的。

4.3.1 Nginx 媒体服务器

按照上面的流程,我们在媒体服务器上安装Nginx,配置如下:

server {    
        listen       81;
        server_name  localhost;
        
        #  流媒体 静态资源
        location ^~ /school/video/ {       
            alias   C:/school/video/;
        }       
        # 视频静态资源
        location / {        
            alias   C:/CODE/JAVA/school/xc-ui-pc-video/;
            index  index.html index.htm;
        }       
    }

4.4 测试video.js并将数据中的video.html复制到C:/CODE/JAVA/school/xc-ui-pc-video/如图:

以上已经使用nginx代理了C:/CODE/JAVA/school/xc-ui-pc-video/目录。所以可以直接通过http请求访问video.html页面

修改src路径为自己的媒体访问路径

用户打开浏览器并输入 :81/video.html 。您可以打开视频播放页面进行播放。

© 版权声明
THE END
喜欢就支持一下吧
点赞246 分享
评论 抢沙发
头像
欢迎您留下宝贵的见解!
提交
头像

昵称

取消
昵称表情代码图片