0

    如何实现在线视频播放?

    2023.07.27 | admin | 121次围观

    HLS的工作方式是:将视频拆分成若干ts格式的小文件,通过m3u8格式的索引文件对这些ts小文件建立索引。一般10秒一个ts文件,播放器连接m3u8文件播放,当快进时通过m3u8即可找到对应的索引文件,并去下载对应的ts文件,从而实现快进、快退以近实时 的方式播放视频。

    IOS、Android设备、及各大浏览器都支持HLS协议。

    详细参考:

    采用HLS方案即可实现边下载边播放,可以不用使用rtmp等流媒体协议,不用构建专用的媒体服务器,节省成本。

    本项目点播方案确定为方案3。

    2 FFmpeg 的基本使用

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

    下载 :ffmpeg-20180227-fa0c9d6-win64-static.zip,并解压,本教程将ffmpeg解压到了C:\Java_Soft\xczx\ffmpeg-20180227-fa0c9d6-win64-static下。

    FFmpeg被许多开源项目采用,QQ影音、暴风影音、VLC等。

    下载:FFmpeg #build-windows

    将C:\Java_Soft\xczx\ffmpeg-20180227-fa0c9d6-win64-static\bin目录配置在path环境变量中。

    简单的测试:

    将一个.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 ,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文件,m3u8文件是ts的索引文件。

    使用VLC打开m3u8文件,测试播放效果,VLC 是一款自由、开源的跨平台多媒体播放器及框架,可播放大多数多媒体文件,以及 DVD、音频 CD、VCD 及各类流媒体协议。()

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

    导入shcool.sql

    2. 创建媒资服务工程

    基于springboot创建工程

    3. 上传文件3.1 断点续传解决方案

    通常视频文件都比较大,所以对于媒资系统上传文件的需求要满足大文件的上传要求。http协议本身对上传文件大小没有限制js上传图片到项目服务器上,但是客户的网络环境质量、电脑硬件环境等参差不齐,如果一个大文件快上传完了网断了,电断了没有上传完成,需要客户重新上传,这是致命的,所以对于大文件上传的要求最基本的是断点续传。

    什么是断点续传:

    引用百度百科:断点续传指的是在下载或上传时,将下载或上传任务(一个文件或一个压缩包)人为的划分为几个部分,每一个部分采用一个线程进行上传或下载,如果碰到网络故障,可以从已经上传或下载的部分开始继续上传下载未完成的部分,而没有必要从头开始上传下载,断点续传可以提高节省操作时间,提高用户体验性。

    上传流程如下:

    1、上传前先把文件分成块

    2、一块一块的上传,上传中断后重新上传,已上传的分块则不用再上传

    3、各分块上传完成最后合并文件

    文件下载则同理。

    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介绍

    如何在web页面实现断点续传?

    常见的方案有:

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

    2、安装浏览器插件,变相的pc客户端,用的比较少。

    3、Html5

    随着html5的流行,本项目采用Html5完成文件分块上传。

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

    使用WebUploader上传流程如下:

    钩子方法

    在webuploader中提供很多钩子方法js上传图片到项目服务器上,下边列出一些重要的:

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

    1)before-send-file

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

    2)before-send

    在上传文件分块之前调用此方法,可以请求服务端检查分块是否存在,如果已存在则此分块儿不再上传。

    3)after-send-file

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

    注册钩子方法源代码:

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

    构建WebUploader

    上传照片到服务器失败_js上传图片到项目服务器上_上传至服务器是什么意思

    使用webUploader前需要创建webUploader对象。

    指定上传分块的地址:/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// 允许在文件传输时提前把下一个文件准备好
      }
    )

    before-send-file

    文件开始上传前前端请求服务端准备上传工作。

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

    before-send

    上传分块前前端请求服务端校验分块是否存在。

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

    after-send-file

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

     
    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路径

    定义Dao

    使用mybatis-plus

    @Mapper
    @Component
    public interface FileMsgMapper extends BaseMapper {
    }

    定义controller

    @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);
        }
    }
    ​

    定义service (略)由于代码过多,请参见源码。

    现在视频已经通过断点续传的方式,上传到了我们的媒资服务器, 接下来就要使用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播放器:缺点是需要在客户机安装Adobe Flash Player播放器,优点是flash播放器已经很成熟了,并且浏览器对flash支持也很好。

    H5播放器:基于h5自带video标签进行构建,优点是大部分浏览器支持H5,不用再安装第三方的flash播放器,并且随着前端技术的发展,h5技术会越来越成熟。

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

    Video.js是一款基于HTML5世界的网络视频播放器。它支持HTML5和Flash视频,它支持在台式机和移动设备上播放视频。这个项目于2010年中开始,目前已在40万网站使用。

    官方地址:

    4.2 下载video.js

    Video.js:

    videojs-contrib-hls: #installation

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

    使用文档:

    本教程使用 video.js 6.7.3 版本,videojs-contrib-hls 5.14.1版本

    将资料中提供的plugins赋值到xc-ui-pc-video项目目录下

    4.3 搭建媒体服务器

    正常使用video.js播放视频是通过一个网页,用户通过浏览器打开网页去播放视频,网页和视频都从web服务器请求。

    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 。即可打开视频播放页面,并播放。

    版权声明

    本文仅代表作者观点。
    本文系作者授权发表,未经许可,不得转载。

    发表评论