微服务[学成在线] day13:使用FFmpeg进行格式转换以及m3u8文件生成、文件分块上传接口实现

关于-RTSP推流方案, ffmpeg 视频转 m3u8 视频推流。 阅读详情

😎 知识点概览

为了方便后续回顾该项目时能够清晰的知道本章节讲了哪些内容,并且能够从该章节的笔记中得到一些帮助,所以在完成本章节的学习后在此对本章节所涉及到的知识点进行总结概述。

本章节为【学成在线】项目的 day13 的内容

  • FFmpeg 的基本使用
  • 使用 m3u8 和 video.js技术实现视频的在线播放
  • 搭建媒资服务工程实现文件的分块储存

目录

内容会比较多,小伙伴们可以根据目录进行按需查阅。

一、在线学习需求分析

0x01 需求描述

学成在线作为在线教育网站,提供多种学习形式,包括:录播、直播、图文、社群等,学生登录进入学习中心即可

在线学习,本章节将开发录播课程的在线学习功能,需求如下:

1、学生可以在 windows 浏览器上在线观看视频。

2、播放器具有快进、快退、暂停等基本功能。

3、学生可以方便切换章节进行学习。

什么是录播课程?

录播课程就是提供录制好课程视频,供用户在线点播,反复学习。

课程视频如何管理?

媒资管理系统专门来管理课程视频,用户视频文件上传到媒资系统,并对视频进行编码处理。

0x02 视频点播解决方案

流媒体方案

详细参考:https://baike.baidu.com/item/%E6%B5%81%E5%AA%92%E4%BD%93/98740?fr=aladdin

概括理解:流媒体就是将视频文件分成许多小块儿,将这些小块儿作为数据包通过网络发送出去,实现一边传输视
频 数据 包一边观看视频。

  • 流式传输

    在网络上传输音、视频信息有两个方式:下载 和 流式传输。

    下载:就是把音、视频文件完全下载到本机后开始播放,它的特点是必须等到视频文件下载完成方可播放,播放等待时间较长,无法去播放还未下载的部分视频。

    流式传输:就是客户端通过链接视频服务器实时传输音、视频信息,实现 “边下载边播放”。

    流式传输包括如下两种方式:

    • 1) 顺序流式传输

      即顺序下载音、视频文件,可以实现边下载边播放,不过,用户只能观看已下载的视频内容,无法快进到未下载的视频部分,顺序流式传输可以使用 Http 服务器来实现,比如 Nginx、Apache 等。

    • 2)实时流式传输

      实时流式传输可以解决顺序流式传输无法快进的问题,它与Http流式传输不同,它必须使用流媒体服务器并且使用流媒体协议来传输视频,它比 Http 流式传输复杂。常见的实时流式传输协议有RTSP、RTMP、RSVP 等。

  • 流媒体系统的概要结构

    通过流媒体系统的概要结构,学习流媒体系统的基本业务流程。

1、将原始的视频文件通过编码器转换为适合网络传输的流格式,编码后的视频直接输送给媒体服务器。
原始的视频文件通常是事先录制好的视频,比如通过摄像机、摄像头等录像、录音设备采集到的音视频文
件,体积较大,要想在网络上传输需要经过压缩处理,即通过编码器进行编码 。

2、媒体服务获取到编码好的视频文件,对外提供流媒体数据传输接口,接口协议包括 :HTTP、RTSP、RTMP 等 。

3、播放器通过流媒体协议与媒体服务器通信,获取视频数据,播放视频。

点播方案

本项目包括点播和直播两种方式,我们先调研一下几个点播的方案,如下:

  1. 播放器通过 http 协议从 http 服务器上下载视频文件进行播放

    问题:必须等到视频下载完才可以播放,不支持快进到某个时间点进行播放

  2. 播放器通过 rtmp 协议连接媒体服务器以实时流方式播放视频

    使用rtmp协议需要架设媒体服务器,造价高,对于直播多采用此方案。

  3. 播放器使用 HLS 协议连接 http 服务器(Nginx、Apache等)实现近实时流方式播放视频

    HLS协议规定:基于 Http 协议,视频封装格式为 ts,视频的编码格式为 H264,音频编码格式为MP3、AAC或者 AC-3。

那么 HLS 是什么?

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

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

详细参考:https://baike.baidu.com/item/HLS/8328931?fr=aladdin

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

二、视频编码

0x01 视频编码格式

先来看一下百度百科中的简介

详情参考 :https://baike.baidu.com/item/%E8%A7%86%E9%A2%91%E7%BC%96%E7%A0%81/839038

首先我们要分清文件格式和编码格式:

文件格式:是指 .mp4、.avi、.rmvb 等 这些不同扩展名的视频文件的文件格式 ,视频文件的内容主要包括视频和音频,其文件格式是按照一 定的编码格式去编码,并且按照该文件所规定的封装格式将视频、音频、字幕等信息封装在一起,播放器会根据它们的封装格式去提取出编码,然后由播放器解码,最终播放音视频。

音视频编码格式:通过音视频的压缩技术,将视频格式转换成另一种视频格式,通过视频编码实现流媒体的传输。

比如:一个 .avi 的视频文件原来的编码是 a,通过编码后编码格式变为 b,音频原来为 c,通过编码后变为d。

0x02 FFmpeg 的基本使用

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

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

下载:FFmpeg https://www.ffmpeg.org/download.html#build-windows

下载完成后,将 ffmpeg 解压到磁盘下,设置环境变量 FFMPEG_HOME 的值为 ffmpeg 的安装目录

将 %FFMPEG_HOME%/bin 添加到环境变量Path 中

0x03 生成 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

mkdir hls
ffmpeg -i lucene.mp4 -hls_time 10 -hls_list_size 0 -hls_segment_filename ./hls/lucene_%05d.ts ./hls/lucene.m3u8
  • -hls_time 设置每片的长度,单位为秒

  • -hls_list_size n: 保存的分片的数量,设置为 0 表示保存所有分片

  • -hls_segment_filename :段文件的名称,%05d 表示 5 位数字

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

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

如果这里出现无法播放的情况,请将 FFmpeg 和 VLC 播放器更新到最新版本。

码率的设置

码率又叫比特率即每秒传输的 bit 数,单位为 bps(Bit Per Second),码率越大传送数据的速度越快。

码率的计算公式是:文件大小(转成 bit)/ 时长(秒)/1024 = kbps 即每秒传输千位数

例如一个 1M 的视频,它的时长是 10s,它的码率等于 1*1024*1024*8/10/1024 = 819Kbps

码率设置到多少才能达到最好,通过根据个人的经验或参考一些视频网台给出的参考,下图是优酷对码率的要求:

如果要将视频上传到优酷则必须按照上面的要求,如果是自己搭建视频服务器,码率设置不易过大,最终达到的视频清晰度满足业务需求即可。

三、播放器

0x01 技术选型

视频编码后要使用播放器对其进行解码、播放视频内容。在 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 万网站使用。

官方地址:http://videojs.com/

0x02 下载 video.js

Video.js: https://github.com/videojs/video.js

videojs-contrib-hls: https://github.com/videojs/videojs-contrib-hls#installation(videojs-contrib-hls是播放 hls 的一个插件)

使用文档:http://docs.videojs.com/tutorial-videojs_.html

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

下载上边两个文件,为了测试需求将其放在门户工程的 plugins 目录中。

0x03 搭建媒体播放器

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

配置 Nginx媒体服务器

HLS 协议基于 Http 协议,本项目使用 Nginx 作为视频服务器。下图是 Nginx 媒体服务器的配置流程图:

1、用户打开www.xuecheng.com上边的 video.html 网页 ,在此网页中引入视频链接,视频地址指向video.xuecheng.com

2、video.xuecheng.com 进行负载均衡处理,将视频请求转发到媒体服务器

根据上边的流程,我们在媒体服务器上安装 Nginx,并配置如下:

#学成网媒体服务
server {
    listen 90;
    server_name localhost;
    #视频目录
    location /video/ {
        alias F:/develop/video/;
    }
}

配置媒体服务器代理

媒体服务器不止一台,通过代理实现负载均衡功能,使用 Nginx 作为媒体服务器的代理,此代理服务器作为 video.xuecheng.com 域名服务器。

配置 video.xuecheng.com 虚拟主机:

注意:这里我们开发环境中代理服务器和媒体服务器在同一台服务器,使用同一个Nginx。

#学成网媒体服务代理
map $http_origin $origin_list{
    default http://www.xuecheng.com;
    "~http://www.xuecheng.com" http://www.xuecheng.com;
    "~http://ucenter.xuecheng.com" http://ucenter.xuecheng.com;
} 
#学成网媒体服务代理
server {
    listen 80;
    server_name video.xuecheng.com;
    location /video {
        proxy_pass http://video_server_pool;
        add_header Access-Control-Allow-Origin $origin_list;
        #add_header Access-Control-Allow-Origin *;
        add_header Access-Control-Allow-Credentials true;
        add_header Access-Control-Allow-Methods GET;
    }
}

cors跨域参数:

  • Access-Control-Allow-Origin:允许跨域访问的外域地址

  • Access-Control-Allow-Credentials: 允许客户端携带证书访问

  • Access-Control-Allow-Methods:允许客户端跨域访问的方法

通常允许跨域访问的站点不是一个,所以这里用 map 定义了多个站点。

如果允许任何站点跨域访问则设置为 *,通常这是不建议的。

video_server_pool 的配置如下:

#媒体服务
upstream video_server_pool{
    server 127.0.0.1:90 weight=10;
}

配置 hosts 文件,本教程开发环境使用 Windows10,修改 hosts 文件,路径为 C:\Windows\System32\drivers\etc\hosts

127.0.0.1 video.xuecheng.com

0x04 测试 video.js

参考文档如下:

https://github.com/videojs/videojs-contrib-hls#installation

http://jsbin.com/vokipos/8/edit?html,output

1、编写测试页面 video.html。

<!DOCTYPE html>
<html lang="en">
    <head>
        <meta http-equiv="content-type" content="text/html; charset=utf-8" />
        <title>视频播放</title>
        <link href="/plugins/videojs/video-js.css" rel="stylesheet">
    </head>
    <body>
        <video id=example-video width=800 height=600 class="video-js vjs-default-skin vjs-big-play-
                                                            centered" controls poster="http://127.0.0.1:90/video/add.jpg">
            <source
                    src="http://video.xuecheng.com/video/hls/lucene.m3u8"
                    type="application/x-mpegURL">
        </video>
        <input type="button" onClick="switchvideo()" value="switch"/>
        <script src="/plugins/videojs/video.js"></script>
        <script src="/plugins/videojs/videojs-contrib-hls.js"></script>
        <script>
            var player = videojs('example-video');
            //player.play();
            //切换视频
            function switchvideo(){
                player.src({
                    src: 'http://video.xuecheng.com/video/hls/lucene.m3u8',
                    type: 'application/x-mpegURL',
                    withCredentials: true
                });
                player.play();
            }
        </script>
    </body>
</html>

2、测试

点击 switch 测试切换视频功能。

四、搭建学习中心前端

0x01 界面原型

先看一下界面原型,如下图,最终的目标是在此页面使用 video.js 播放视频。

0x02 创建学习中心工程

学习中心的用户是学生,为了便于系统维护和扩展,单独创建学习中心工程:

1、从资料目录拷贝 xc-ui-pc-leanring.zip 并解压到 xc-ui-pc-leanring 目录。

2、使用 webstorm 创建打开 xc-ui-pc-leanring 目录

3、进入 xc-ui-pc-leanring 目录,执行 cnpm install,将根据 package.json 的依赖配置远程下载依赖的 js 包。

创建完成,xc-ui-pc-leanring工程如下:

配置域名

学习中心的二级域名为 ucenter.xuecheng.com,我们在 nginx 中配置 ucenter 虚拟主机。

#学成网用户中心
server {
    listen 80;
    server_name ucenter.xuecheng.com;
    #个人中心
    location / {
        proxy_pass http://ucenter_server_pool;
    }
} 
#前端ucenter
upstream ucenter_server_pool{
    #server 127.0.0.1:7081 weight=10;
    server 127.0.0.1:13000 weight=10;
}

在根服务下添加一个 /plugins/ 站点

# 根服务
server{
    listen       80;
    server_name  www.xuecheng.com;
    ssi on;
    ssi_silent_errors on;
    location /plugins/ {      
        # 跨域参数
        alias  E:/Project/XueChengOnline/xcEduUI01/xc-ui-pc-static-portal/plugins/;   
        add_header Access-Control-Allow-Origin http://ucenter.xuecheng.com; 
        add_header Access-Control-Allow-Credentials true;          
        add_header Access-Control-Allow-Methods GET;        
    }
    .....
}

访问测试

启动工程,看到下边的界面说明本工程创建完成

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-AtcW0UdA-1595567116697)(https://qnoss.codeyee.com/20200704_13/image16)]

0x03 调试视频播放页面

使用 vue-video-player 组件将 video.js 集成到 vue.js 中,本项目使用 vue-video-player实现video.js 播放。

组件地址:https://github.com/surmon-china/vue-video-player

上面的 xc-ui-pc-learning 工程已经添加 vue-video-player 组件,我们在 vue 页面直接使用即可。

前边我们已经测试通过 video.js,下面我们直接在 vue 页面中使用 vue-video-player 完成视频播放。

导入 learning_video.vue 页面到 course 模块下。

配置路由

import learning_video from '@/module/course/page/learning_video.vue';
{
    path: '/learning/:courseId/:chapter',
    component: learning_video,
    name: '录播视频学习',
    hidden: false,
    iconCls: 'el‐icon‐document'
}

预览效果

请求:http://ucenter.xuecheng.com/#/learning/1/2

第一个参数:courseId,课程id,这里是测试页面效果随便输入一个 ID 即可,这里输入1

第二个参数:chapter,课程计划id,这里是测试页面效果随便输入一个 ID 即可,这里输入2

五、媒资管理

前边章节完成在线视频播放,如何实现点击课程计划播放视频呢,课程视频如何管理呢?

本节开始将对课程视频进行管理。

0x01 需求分析

媒资管理系统是每个在线教育平台所必须具备的,百度百科对它的定义如下:

每个教学机构都可以在媒资系统管理自己的教学资源,包括:视频、教案等文件。

目前媒资管理的主要管理对象是课程录播视频,包括:媒资文件的查询、视频上传、视频删除、视频处理等。

  • 媒资查询:教学机构查询自己所拥有的媒体文件。

  • 视频上传:将用户线下录制的教学视频上传到媒资系统。

  • 视频处理:视频上传成功,系统自动对视频进行编码处理。

  • 视频删除 :如果该视频已不再使用,可以从媒资系统删除。

下边是媒资系统与其它系统的交互情况:

1、上传媒资文件

前端/客户端请求媒资系统上传文件。

文件上传成功将文件存储到媒资服务器,将文件信息存储到数据库。

2、使用媒资

课程管理请求媒资系统查询媒资信息,将课程计划与媒资信息对应、存储。

3、视频播放

用户进入学习中心请求学习服务学习在线播放视频。

学习服务校验用户资格通过后请求媒资系统获取视频地址。

0x02 开发环境搭建

创建媒资数据库

1、媒资文件信息

package com.xuecheng.framework.domain.media;

import lombok.Data;
import lombok.ToString;
import org.springframework.data.annotation.Id;
import org.springframework.data.mongodb.core.mapping.Document;

import java.util.Date;

@Data
@ToString
@Document(collection = "media_file")
public class MediaFile {
    /*
    文件id、名称、大小、文件类型、文件状态(未上传、上传完成、上传失败)、上传时间、视频处理方式、视频处理状态、hls_m3u8,hls_ts_list、课程视频信息(课程id、章节id)
     */
    @Id
    //文件id
    private String fileId;
    //文件名称
    private String fileName;
    //文件原始名称
    private String fileOriginalName;
    //文件路径
    private String filePath;
    //文件url
    private String fileUrl;
    //文件类型
    private String fileType;
    //mimetype
    private String mimeType;
    //文件大小
    private Long fileSize;
    //文件状态
    private String fileStatus;
    //上传时间
    private Date uploadTime;
    //处理状态
    private String processStatus;
    //hls处理
    private MediaFileProcess_m3u8 mediaFileProcess_m3u8;

    //tag标签用于查询
    private String tag;

}

2、创建 xc_media 数据库

媒资系统使用 mongodb 数据库存储媒资信息,再创建集合 media_file

创建媒资服务工程

媒资管理的相关功能单独在媒资服务中开发,下边创建媒资服务工程(xc-service-manage-media)。
媒资服务的配置与 cms 类似,导入 资料/xc-service-manage-media 工程,工程结构如下:

0x03 上传文件

断点续传解决方案

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

什么是断点续传?

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

如下图:

上传流程如下:

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

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

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

文件下载则同理。

文件分块与合并

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

1、分块

文件分块的流程如下:

1、获取源文件长度

2、根据设定的分块文件的大小计算出块数

3、从源文件读数据,再依次向每一个块文件写入数据。

单元测试代码如下

/**
     * 测试文件分块
     */
@Test
public void testChunk() throws IOException {
    String chunkPath = "E:/Project/XueChengOnline/xcEduUI01/xuecheng/video/";
    String fileName = "lucene.mp4";
    File sourceFile = new File(chunkPath + fileName);
    File chunkFolder = new File(chunkPath + fileName + ".chunk");
    if(!chunkFolder.exists()){
        chunkFolder.mkdir();
    }

    //分块大小
    long chunkSize = 1024*1024*1;
    //分块数量
    //Math.ceil向上取整,例如 12.1=13,12.8=13
    long chunkNum = (long) Math.ceil((sourceFile.length() * 1.0) / chunkSize);
    chunkNum = chunkNum <= 0 ? 1 : chunkNum;
    //缓冲区大小
    byte[] byte_cache = new byte[1024];
    //使用RandomAccessFile访问文件
    RandomAccessFile rafRead = new RandomAccessFile(sourceFile, "r");
    //分块
    for (int i = 0; i < chunkNum; i++) {
        //创建分块文件
        File chunkFile = new File(chunkFolder.getPath() + "/" + i);
        boolean newFile = chunkFile.createNewFile();
        if (newFile){
            //向分块文件中写入数据
            RandomAccessFile raf_write = new RandomAccessFile(chunkFile, "rw");
            int len = -1;
            //读取到-1则表示读取完成
            len = rafRead.read(byte_cache);
            while (len != -1){
                raf_write.write(byte_cache,0, len);
                //读取到预期块大小时结束
                if (chunkFile.length() >= chunkSize){
                    break;
                }
            }
            raf_write.close();
        }
    }
    rafRead.close();
}
2、合并

文件合并流程:

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<File> fileList = new ArrayList<File>(Arrays.asList(fileArray));
    // 从小到大排序
    Collections.sort(fileList, new Comparator<File>() {
        @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();
}

前端页面

上传文件的页面内容参考:资料/upload.vue 文件

1、WebUploader 介绍

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

常见的方案有:

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

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

3、HTML5

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

本项目使用 WebUploader 完成大文件上传功能的开发,WebUploader 官网地址:
http://fexteam.gz01.bdysite.com/webuploader/

WebUploader 简介

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-wXWLwmLa-1595567116713)(https://qnoss.codeyee.com/20200704_13/image23)]

WebUploader 的特性

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3pG0sVSF-1595567116716)(https://qnoss.codeyee.com/20200704_13/image24)]

使用 WebUploader 的上传流程如下:

2、钩子方法

在 webuploader 中提供很多钩子方法,下边列出一些重要的:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-YFlMfWdv-1595567116719)(https://qnoss.codeyee.com/20200704_13/image26)]

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

  • before-send-file

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

  • before-send

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

  • after-send-file

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

注册钩子方法源代码:

WebUploader.Uploader.register({
    "before‐send‐file":"beforeSendFile",
    "before‐send":"beforeSend",
    "after‐send‐file":"afterSendFile"
}
3、构建 WebUploader

使用 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// 允许在文件传输时提前把下一个文件准备好
    }
)
3、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
}
4、before-send

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

参考源代码如下:

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

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

参考代码如下:

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

定义文件上传的Api接口,此接收是前端 WebUploader 调用服务端的接口。

编写此接口需要参数前端 WebUploader 应用代码。

Api接口

定义文件上传的 Api 接口,此接收是前端 WebUploader 调用服务端的接口。

编写此接口需要参数前端 WebUploader 应用代码。

package com.xuecheng.api.media;

import com.xuecheng.framework.domain.media.response.CheckChunkResult;
import com.xuecheng.framework.model.response.ResponseResult;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.web.multipart.MultipartFile;

@Api(value = "媒资管理接口", description = "媒资管理接口,提供文件上传,文件处理等接口")
public interface MediaUploadControllerApi {
    @ApiOperation("文件上传注册")
    public ResponseResult register(String fileMd5,
                                   String fileName,
                                   Long fileSize,
                                   String mimetype,
                                   String fileExt);
    @ApiOperation("分块检查")
    public CheckChunkResult checkchunk(String fileMd5,
                                       Integer chunk,
                                       Integer chunkSize);
    @ApiOperation("上传分块")
    public ResponseResult uploadchunk(MultipartFile file,
                                      Integer chunk,
                                      String fileMd5);
    @ApiOperation("合并文件")
    public ResponseResult mergechunks(String fileMd5,
                                      String fileName,
                                      Long fileSize,
                                      String mimetype,
                                      String fileExt);
}

媒资服务端编写

1、业务流程

服务端需要实现如下功能:

1)上传前检查上传环境

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

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

2)分块检查

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

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

3)分块上传

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

4)合并分块

将所有分块文件合并为一个文件,在数据库记录文件信息。

2、上传注册

由于上传过程复杂,开发时按业务流程分别实现。

1、配置

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

xc‐service‐manage‐media:
  # 媒体文件存放路径
  upload‐location: F:/develop/video/

2、定义DAO

public interface MediaFileRepository extends MongoRepository<MediaFile,String> {
}

3、编写Service

功能:

1)检查上传文件是否存在

2)创建文件目录

package com.xuecheng.manage_media.service.impl;

import com.xuecheng.framework.domain.media.MediaFile;
import com.xuecheng.framework.domain.media.response.MediaCode;
import com.xuecheng.framework.exception.ExceptionCast;
import com.xuecheng.framework.model.response.CommonCode;
import com.xuecheng.framework.model.response.ResponseResult;
import com.xuecheng.manage_media.controller.MediaUploadController;
import com.xuecheng.manage_media.dao.MediaFileRepository;
import com.xuecheng.manage_media.service.MediaUploadService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;

import java.io.File;
import java.util.Optional;

@Service
public class MediaUploadServiceImpl implements MediaUploadService {
    private final static Logger LOGGER = LoggerFactory.getLogger(MediaUploadController.class);

    @Autowired
    MediaFileRepository mediaFileRepository;

    //上传文件根目录
    @Value("${xc‐service‐manage‐media.upload‐location}")
    String uploadPath;

   /**
     * 检查文件信息是否已经存在本地以及mongodb内,其中一者不存在则重新注册
     * @param fileMd5 文件md5值
     * @param fileExt 文件扩展名
     * @return 文件路径
     */
    @Override
    public ResponseResult register(String fileMd5, String fileName, Long fileSize, String mimetype, String fileExt) {
        //1.检查文件在磁盘上是否存在
        //2.检查文件信息在mongodb上是否存在

        //获取文件所属目录以及文件路径
        String fileFloderPath = this.getFileFloderPath(fileMd5);
        String filePath = this.getFileFullPath(fileMd5, fileExt);
        File file = new File(filePath);
        boolean exists = file.exists();

        //查询mongodb上的文件信息
        Optional<MediaFile> optional = mediaFileRepository.findById(fileMd5);
        if(exists && optional.isPresent()){
            ExceptionCast.cast(MediaCode.UPLOAD_FILE_REGISTER_EXIST);
        }
        //其中一者不存在则重新注册文件信息
        File fileFloder = new File(fileFloderPath);
        if(!fileFloder.exists()){
            //创建文件目录
            fileFloder.mkdirs();
        }
        return new ResponseResult(CommonCode.SUCCESS);
    }

    
    /**
     * 根据文件md5得到文件的所属目录
     * 规则:
     * 一级目录:md5的第一个字符
     * 二级目录:md5的第二个字符
     * 三级目录:md5
     */
    private String getFileFloderPath(String fileMd5){
        String floderPath = uploadPath + "/" + fileMd5.substring(0,1) + "/" + fileMd5.substring(1,2) + "/" + fileMd5  + "/";
        return floderPath;
    }

    /**
     * 获取全文件路径
     * 文件名:md5+文件扩展名
     */
    private String getFileFullPath(String fileMd5, String fileExt){
        String floderPath = this.getFileFloderPath(fileMd5);
        String filePath = floderPath + fileMd5 + "." + fileExt;
        return filePath;
    }
    
    /**
     * 获取文件路径
     * 文件名:md5+文件扩展名
     */
    private String getFilePath(String fileMd5, String fileExt){
        String filePath = "/" + fileMd5.substring(0,1) + "/" + fileMd5.substring(1,2) + "/" + fileMd5 + "/";
        return filePath;
    }
}

3、分块检查

在 Service 中定义分块检查方法

/**
 * 检查文件块是否存在
 * @param fileMd5 文件md5
 * @param chunk 块编号
 * @param chunkSize 块大小
 * @return CheckChunkResult
 */
@Override
public CheckChunkResult checkChunk(String fileMd5, Integer chunk, Integer chunkSize) {
    //获取文件块路径
    String chunkFloder = this.getChunkFloderPath(fileMd5);
    File chunkFile = new File(chunkFloder + chunk);
    if(chunkFile.exists()){
        return new CheckChunkResult(CommonCode.SUCCESS, true);
    }

    return new CheckChunkResult(CommonCode.SUCCESS, false);
}

//获取文件块路径
private String getChunkFloderPath(String fileMd5) {
    //获取分块文件所属目录
    String fileFloderPath = this.getFileFloderPath(fileMd5);
    String chunkFloder = fileFloderPath + "chunk/";
    File fileChunkFloder = new File(chunkFloder);
    //如果分块所属目录不存在则创建
    if(!fileChunkFloder.exists()){
        fileChunkFloder.mkdirs();
    }
    return chunkFloder;
}
4、上传分块
/**
     * 上传分块文件
     * @param file 上传的文件
     * @param chunk 分块号
     * @param fileMd5 文件MD5
     * @return
     */
@Override
public ResponseResult uploadChunk(MultipartFile file, Integer chunk, String fileMd5) {
    //获取分块文件所属目录
    String chunkFloder = this.getChunkFloderPath(fileMd5);
    InputStream inputStream = null;
    FileOutputStream fileOutputStream = null;
    try {
        inputStream = file.getInputStream();
        fileOutputStream = new FileOutputStream(chunkFloder + chunk);
        IOUtils.copy(inputStream,fileOutputStream);
    } catch (IOException e) {
        //文件保存失败
        e.printStackTrace();
        LOGGER.error("upload chunk file fail:{}",e.getMessage());
        ExceptionCast.cast(MediaCode.CHUNK_FILE_UPLOAD_FAIL);
    }
    return new ResponseResult(CommonCode.SUCCESS);
}

5、合并分块

在 Service 中定义分块合并分块方法,功能如下:

1)将块文件合并

2)校验文件 md5 是否正确

3)向 Mongodb 写入文件信息

/**
     * 合并文件块信息
     * @param fileMd5 文件MD5
     * @param fileName 文件名称
     * @param fileSize 文件大小
     * @param mimetype 文件类型
     * @param fileExt 文件拓展名
     * @return ResponseResult
     */
@Override
public ResponseResult mergeChunks(String fileMd5, String fileName, Long fileSize, String mimetype, String fileExt) {
    //获取文件块路径
    String chunkFloderPath = getChunkFloderPath(fileMd5);
    //合并文件路径
    String fileFullPath = this.getFileFullPath(fileMd5, fileExt);
    File mergeFile = new File(fileFullPath);
    //创建合并文件,如果存在则先删除再创建
    if(mergeFile.exists()){
        mergeFile.delete();
    }
    boolean newFile = false;
    try {
        newFile = mergeFile.createNewFile();
    } catch (IOException e) {
        e.printStackTrace();
        LOGGER.error("mergechunks..create mergeFile fail:{}",e.getMessage());
    }
    if(!newFile){
        //文件创建失败
        ExceptionCast.cast(MediaCode.MERGE_FILE_CREATEFAIL);
    }

    //获取块文件列表,此列表是已经排序好的
    List<File> chunkFiles = this.getChunkFiles(chunkFloderPath);

    //合并文件
    mergeFile = this.mergeFile(mergeFile, chunkFiles);
    if(mergeFile == null){
        ExceptionCast.cast(MediaCode.MERGE_FILE_FAIL);
    }

    //校验文件
    boolean checkResult = this.checkFileMd5(mergeFile, fileMd5);
    if(!checkResult){
        ExceptionCast.cast(MediaCode.MERGE_FILE_CHECKFAIL);
    }

    //将文件信息保存到数据库
    MediaFile mediaFile = new MediaFile();
    mediaFile.setFileId(fileMd5);
    mediaFile.setFileName(fileMd5+"."+fileExt);
    mediaFile.setFileOriginalName(fileName);

    //文件路径保存相对路径
    String filePath = this.getFilePath(fileMd5,fileExt);
    mediaFile.setFilePath(this.getFilePath(fileMd5,fileExt));
    mediaFile.setFileUrl(filePath + fileName + "." + fileExt);
    mediaFile.setFileSize(fileSize);
    mediaFile.setUploadTime(new Date());
    mediaFile.setMimeType(mimetype);
    mediaFile.setFileType(fileExt);

    //状态为上传成功
    mediaFile.setFileStatus("301002");
    MediaFile save = mediaFileRepository.save(mediaFile);

    //向MQ发送视频处理消息
    this.sendProcessVideoMsg(fileMd5);

    return new ResponseResult(CommonCode.SUCCESS);

}

//校验文件MD5
private boolean checkFileMd5(File mergeFile, String fileMd5) {
    if(mergeFile == null || StringUtils.isEmpty(fileMd5)){
        return false;
    }
    //进行md5校验
    try {
        FileInputStream fileInputStream = new FileInputStream(mergeFile);
        //得到文件的MD5
        String md5Hex = DigestUtils.md5Hex(fileInputStream);
        //比较两个MD5值
        if(md5Hex.equalsIgnoreCase(fileMd5)){
            return true;
        }
    } catch (FileNotFoundException e) {
        e.printStackTrace();
        LOGGER.error("未找到该文件 {}",e.getMessage());
    } catch (IOException e) {
        e.printStackTrace();
    }

    return false;
}

//合并文件
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();
        LOGGER.error("merge file error:{}",e.getMessage());
        return null;
    } 
    return mergeFile;
}

//获取块文件列表
private List<File> getChunkFiles(String chunkFloderPath) {
    //块文件目录
    File chunkFolder = new File(chunkFloderPath);
    //分块文件列表
    File[] fileArray = chunkFolder.listFiles();
    //将分块列表转为集合,便于排序
    ArrayList<File> fileList = new ArrayList<>(Arrays.asList(fileArray));
    //从小到大排序,按名称升序
    Collections.sort(fileList, new Comparator<File>() {
        @Override
        public int compare(File o1, File o2) {
            //比较两个文件的名称
            if (Integer.parseInt(o1.getName()) < Integer.parseInt(o2.getName())) {
                return -1;
            }
            return 1;
        }
    });
    return fileList;
}
6、Controller
package com.xuecheng.manage_media.controller;

import com.xuecheng.api.media.MediaUploadControllerApi;
import com.xuecheng.framework.domain.media.response.CheckChunkResult;
import com.xuecheng.framework.model.response.ResponseResult;
import com.xuecheng.manage_media.service.MediaUploadService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;

@RestController
@RequestMapping("/media/upload")
public class MediaUploadController implements MediaUploadControllerApi {

    @Autowired
    MediaUploadService mediaUploadService;

    @PostMapping("/register")
    @Override
    public ResponseResult register(String fileMd5, String fileName, Long fileSize, String mimetype, String fileExt) {
        return mediaUploadService.register(fileMd5,fileName,fileSize,mimetype,fileExt);
    }

    @PostMapping("/checkchunk")
    @Override
    public CheckChunkResult checkChunk(String fileMd5, Integer chunk, Integer chunkSize) {
        return mediaUploadService.checkChunk(fileMd5,chunk,chunkSize);
    }

    @PostMapping("/uploadchunk")
    @Override
    public ResponseResult uploadChunk(MultipartFile file, Integer chunk, String fileMd5) {
            return mediaUploadService.uploadChunk(file, chunk, fileMd5);
    }

    @PostMapping("/mergechunks")
    @Override
    public ResponseResult mergeChunks(String fileMd5, String fileName, Long fileSize, String mimetype, String fileExt) {
        return mediaUploadService.mergeChunks(fileMd5,fileName,fileSize,mimetype,fileExt);
    }
}
7、测试

建议第一次测试时使用调试模式,在每个模块下都打一个断点进行单步调试,逐步的执行代码,以便检查代码的逻辑。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-htVHhNFC-1595567116728)(https://qnoss.codeyee.com/20200704_13/image28)]

我们在前端工程选择一个文件进行上传,点击开始上传

开始上传后,我们可以看到文件所属的 chunk 目录下在不断的生成块文件

块文件全部生成后,会自动调用合并的接口,将所有块文件合并成单个文件

上传成功页面提示

0x04 完整的代码

MediaUploadService

package com.xuecheng.manage_media.service;

import com.xuecheng.framework.domain.media.response.CheckChunkResult;
import com.xuecheng.framework.model.response.ResponseResult;
import org.springframework.web.multipart.MultipartFile;

/**
 * 媒体文件上传服务
 */
public interface MediaUploadService {
    /**
     * 文件信息校验并注册
     * @param fileMd5 文件md5值
     * @param fileName 文件名称
     * @param fileSize 文件大小
     * @param mimetype 文件类型
     * @param fileExt 文件扩展名
     * @return 通用的响应信息
     */
    ResponseResult register(String fileMd5, String fileName, Long fileSize, String mimetype, String fileExt);

    /**
     * 检查文件块是否存在
     * @param fileMd5 文件md5
     * @param chunk 块编号
     * @param chunkSize 块大小
     * @return 检查文件块信息的响应
     */
    CheckChunkResult checkChunk(String fileMd5,Integer chunk,Integer chunkSize);

    /**
     * 上传分块文件
     * @param file 上传的文件
     * @param chunk 分块号
     * @param fileMd5 文件MD5
     * @return ResponseResult
     */
    ResponseResult uploadChunk(MultipartFile file,Integer chunk,String fileMd5);

    /**
     * 合并文件块
     * @param fileMd5 文件MD5
     * @param fileName 文件名称
     * @param fileSize 文件大小
     * @param mimetype 文件类型
     * @param fileExt 文件拓展名
     * @return ResponseResult
     */
    ResponseResult mergeChunks(String fileMd5,String fileName,Long fileSize,String mimetype,String fileExt);

}

MediaUploadServiceImpl

package com.xuecheng.manage_media.service.impl;

import com.alibaba.fastjson.JSON;
import com.netflix.discovery.converters.Auto;
import com.xuecheng.framework.domain.media.MediaFile;
import com.xuecheng.framework.domain.media.response.CheckChunkResult;
import com.xuecheng.framework.domain.media.response.MediaCode;
import com.xuecheng.framework.exception.ExceptionCast;
import com.xuecheng.framework.model.response.CommonCode;
import com.xuecheng.framework.model.response.ResponseResult;
import com.xuecheng.manage_media.config.RabbitMQConfig;
import com.xuecheng.manage_media.controller.MediaUploadController;
import com.xuecheng.manage_media.dao.MediaFileRepository;
import com.xuecheng.manage_media.service.MediaUploadService;
import org.apache.commons.codec.digest.DigestUtils;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.multipart.MultipartFile;

import javax.jws.Oneway;
import java.io.*;
import java.util.*;

@Service
class MediaUploadServiceImpl implements MediaUploadService {
    private final static Logger LOGGER = LoggerFactory.getLogger(MediaUploadController.class);

    @Autowired
    MediaFileRepository mediaFileRepository;

    //上传文件根目录
    @Value("${xc-service-manage-media.upload-location}")
    String uploadPath;

   /**
     * 检查文件信息是否已经存在本地以及mongodb内,其中一者不存在则重新注册
     * @param fileMd5 文件md5值
     * @param fileExt 文件扩展名
     * @return 文件路径
     */
    @Override
    public ResponseResult register(String fileMd5, String fileName, Long fileSize, String mimetype, String fileExt) {
        //1.检查文件在磁盘上是否存在
        //2.检查文件信息在mongodb上是否存在

        //获取文件所属目录以及文件路径
        String fileFloderPath = this.getFileFloderPath(fileMd5);
        String filePath = this.getFileFullPath(fileMd5, fileExt);
        File file = new File(filePath);
        boolean exists = file.exists();

        //查询mongodb上的文件信息
        Optional<MediaFile> optional = mediaFileRepository.findById(fileMd5);
        if(exists && optional.isPresent()){
            ExceptionCast.cast(MediaCode.UPLOAD_FILE_REGISTER_EXIST);
        }
        //其中一者不存在则重新注册文件信息
        File fileFloder = new File(fileFloderPath);
        if(!fileFloder.exists()){
            //创建文件目录
            fileFloder.mkdirs();
        }
        return new ResponseResult(CommonCode.SUCCESS);
    }

    /**
     * 检查文件块是否存在
     * @param fileMd5 文件md5
     * @param chunk 块编号
     * @param chunkSize 块大小
     * @return CheckChunkResult
     */
    @Override
    public CheckChunkResult checkChunk(String fileMd5, Integer chunk, Integer chunkSize) {
        //获取文件块路径
        String chunkFloder = this.getChunkFloderPath(fileMd5);
        File chunkFile = new File(chunkFloder + chunk);
        if(chunkFile.exists()){
            return new CheckChunkResult(CommonCode.SUCCESS, true);
        }

        return new CheckChunkResult(CommonCode.SUCCESS, false);
    }

    /**
     * 上传分块文件
     * @param file 上传的文件
     * @param chunk 分块号
     * @param fileMd5 文件MD5
     * @return
     */
    @Override
    public ResponseResult uploadChunk(MultipartFile file, Integer chunk, String fileMd5) {
        //获取分块文件所属目录
        String chunkFloder = this.getChunkFloderPath(fileMd5);
        InputStream inputStream = null;
        FileOutputStream fileOutputStream = null;
        try {
            inputStream = file.getInputStream();
            fileOutputStream = new FileOutputStream(chunkFloder + chunk);
            IOUtils.copy(inputStream,fileOutputStream);
        } catch (IOException e) {
            //文件保存失败
            e.printStackTrace();
            LOGGER.error("upload chunk file fail:{}",e.getMessage());
            ExceptionCast.cast(MediaCode.CHUNK_FILE_UPLOAD_FAIL);
        }
        return new ResponseResult(CommonCode.SUCCESS);
    }

    /**
     * 合并文件块信息
     * @param fileMd5 文件MD5
     * @param fileName 文件名称
     * @param fileSize 文件大小
     * @param mimetype 文件类型
     * @param fileExt 文件拓展名
     * @return ResponseResult
     */
    @Override
    public ResponseResult mergeChunks(String fileMd5, String fileName, Long fileSize, String mimetype, String fileExt) {
        //获取文件块路径
        String chunkFloderPath = getChunkFloderPath(fileMd5);
        //合并文件路径
        String fileFullPath = this.getFileFullPath(fileMd5, fileExt);
        File mergeFile = new File(fileFullPath);
        //创建合并文件,如果存在则先删除再创建
        if(mergeFile.exists()){
            mergeFile.delete();
        }
        boolean newFile = false;
        try {
            newFile = mergeFile.createNewFile();
        } catch (IOException e) {
            e.printStackTrace();
            LOGGER.error("mergechunks..create mergeFile fail:{}",e.getMessage());
        }
        if(!newFile){
            //文件创建失败
            ExceptionCast.cast(MediaCode.MERGE_FILE_CREATEFAIL);
        }

        //获取块文件列表,此列表是已经排序好的
        List<File> chunkFiles = this.getChunkFiles(chunkFloderPath);

        //合并文件
        mergeFile = this.mergeFile(mergeFile, chunkFiles);
        if(mergeFile == null){
            ExceptionCast.cast(MediaCode.MERGE_FILE_FAIL);
        }

        //校验文件
        boolean checkResult = this.checkFileMd5(mergeFile, fileMd5);
        if(!checkResult){
            ExceptionCast.cast(MediaCode.MERGE_FILE_CHECKFAIL);
        }

        //将文件信息保存到数据库
        MediaFile mediaFile = new MediaFile();
        mediaFile.setFileId(fileMd5);
        mediaFile.setFileName(fileMd5+"."+fileExt);
        mediaFile.setFileOriginalName(fileName);

        //文件路径保存相对路径
        String filePath = this.getFilePath(fileMd5,fileExt);
        mediaFile.setFilePath(this.getFilePath(fileMd5,fileExt));
        mediaFile.setFileUrl(filePath + fileName + "." + fileExt);
        mediaFile.setFileSize(fileSize);
        mediaFile.setUploadTime(new Date());
        mediaFile.setMimeType(mimetype);
        mediaFile.setFileType(fileExt);

        //状态为上传成功
        mediaFile.setFileStatus("301002");
        MediaFile save = mediaFileRepository.save(mediaFile);

        //向MQ发送视频处理消息
        this.sendProcessVideoMsg(fileMd5);

        return new ResponseResult(CommonCode.SUCCESS);

    }

    //视频处理路由
    @Value("${xc-service-manage-media.mq.routingkey-media-video}")
    public String routingkey_media_video;

    @Autowired
    RabbitTemplate rabbitTemplate;

    //向MQ发送视频处理消息
    private ResponseResult sendProcessVideoMsg(String mediaId){
        Optional<MediaFile> optional = mediaFileRepository.findById(mediaId);
        if(!optional.isPresent()){
            return new ResponseResult(CommonCode.FAIL);
        }
        MediaFile mediaFile = optional.get();
        //发送视频处理消息
        Map<String,String> msgMap = new HashMap<>();
        msgMap.put("mediaId",mediaId);
        //发送的消息
        String msg = JSON.toJSONString(msgMap);
        try {
            this.rabbitTemplate.convertAndSend(RabbitMQConfig.EX_MEDIA_PROCESSTASK,routingkey_media_video,msg);
            LOGGER.info("send media process task msg:{}",msg);
        }catch (Exception e){
            e.printStackTrace();
            LOGGER.info("send media process task error,msg is:{},error:{}",msg,e.getMessage());
            return new ResponseResult(CommonCode.FAIL);
        }
        return new ResponseResult(CommonCode.SUCCESS);
    }

    //校验文件MD5
    private boolean checkFileMd5(File mergeFile, String fileMd5) {
        if(mergeFile == null || StringUtils.isEmpty(fileMd5)){
            return false;
        }
        //进行md5校验
        try {
            FileInputStream fileInputStream = new FileInputStream(mergeFile);
            //得到文件的MD5
            String md5Hex = DigestUtils.md5Hex(fileInputStream);
            //比较两个MD5值
            if(md5Hex.equalsIgnoreCase(fileMd5)){
                return true;
            }
        } catch (FileNotFoundException e) {
            e.printStackTrace();
            LOGGER.error("未找到该文件 {}",e.getMessage());
        } catch (IOException e) {
            e.printStackTrace();
        }

        return false;
    }
    //合并文件
    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();
            LOGGER.error("merge file error:{}",e.getMessage());
            return null;
        } 
        return mergeFile;
    }

    //获取块文件列表
    private List<File> getChunkFiles(String chunkFloderPath) {
        //块文件目录
        File chunkFolder = new File(chunkFloderPath);
        //分块文件列表
        File[] fileArray = chunkFolder.listFiles();
        //将分块列表转为集合,便于排序
        ArrayList<File> fileList = new ArrayList<>(Arrays.asList(fileArray));
        //从小到大排序,按名称升序
        Collections.sort(fileList, new Comparator<File>() {
            @Override
            public int compare(File o1, File o2) {
                //比较两个文件的名称
                if (Integer.parseInt(o1.getName()) < Integer.parseInt(o2.getName())) {
                    return -1;
                }
                return 1;
            }
        });
        return fileList;
    }

    //获取文件块路径
    private String getChunkFloderPath(String fileMd5) {
        //获取分块文件所属目录
        String fileFloderPath = this.getFileFloderPath(fileMd5);
        String chunkFloder = fileFloderPath + "chunk/";
        File fileChunkFloder = new File(chunkFloder);
        //如果分块所属目录不存在则创建
        if(!fileChunkFloder.exists()){
            fileChunkFloder.mkdirs();
        }
        return chunkFloder;
    }



    /**
     * 根据文件md5得到文件的所属目录
     * 规则:
     * 一级目录:md5的第一个字符
     * 二级目录:md5的第二个字符
     * 三级目录:md5
     */
    private String getFileFloderPath(String fileMd5){
            String floderPath = uploadPath + "/" + fileMd5.substring(0,1) + "/" + fileMd5.substring(1,2) + "/" +fileMd5  + "/";
            return floderPath;
    }

    /**
     * 获取全文件路径
     * 文件名:md5+文件扩展名
     */
    private String getFileFullPath(String fileMd5, String fileExt){
        String floderPath = this.getFileFloderPath(fileMd5);
        String filePath = floderPath + fileMd5 + "." + fileExt;
        return filePath;
    }
    /**
     * 获取文件路径
     * 文件名:md5+文件扩展名
     */
    private String getFilePath(String fileMd5, String fileExt){
        String filePath = "/" + fileMd5.substring(0,1) + "/" + fileMd5.substring(1,2) + "/" + fileMd5 + "/";
        return filePath;
    }
}

😁 认识作者

作者:👦 LCyee ,全干型代码🐕

自建博客:https://www.codeyee.com

记录学习以及项目开发过程中的笔记与心得,记录认知迭代的过程,分享想法与观点。

优快云 博客:https://blog.youkuaiyun.com/codeyee

记录和分享一些开发过程中遇到的问题以及解决的思路。

欢迎加入微服务练习生的队伍,一起交流项目学习过程中的一些问题、分享学习心得等,不定期组织一起刷题、刷项目,共同见证成长。

SpringBoot整合FFmpeg进行视频分片上传 SpringBoot整合FFmpeg进行视频分片上传------>Windows 分片上传的核心思路: 将文件按一定的分割规则(静态或动态设定,如手动设置20M为一个分片),用slice分割成多个数据块。 为每个文件生成一个唯一标识Key,用于多数据块上传时区分所属文件。 所有分片上传完成,服务端校验合并标识为Key的所有分片为一个最终文件。 分片上传的意义: 将文件分片上传,在网络环境不佳时,可以对文件上传失败的部分重新上传,避免了每次上传都需要从文件起始位置上传的问题。分片的附带好处还能很方便 阅读详情

相关推荐

学成在线-第13天-讲义-在线学习 HLS 二

2视频编码2.1视频编码格式首先我们要分清文件格式和编码格式:文件格式:是指.mp4、.avi、.rmvb等 这些不同扩展名的视频文件的文件格式 ,视频文件的内容主要包括视频和音频,其文件格式是按照一 定的编码格式去编码,并且按照该文件所规定的封装格式将视频、音频、字幕等信息封装在一起,播放器会根据它们的封装格式去提取出编码,然后由播放器解码,最终播放音视频。音视频编码格式:通过音视频的...

qq_40208605的博客 386

学成在线笔记+踩坑(6)——【媒资模块】视频处理。FFmpeg+XXL-JOB

FFmpeg处理视频、XXL-JOB分布式任务调度平台

种一棵树最好的时间是十年前,其次是现在 6024

ffmpeg文件生成m3u8文件及ts切片程序_1

ffmpeg文件生成m3u8文件及ts切片程序_1,实现输入本地文件切片成ts文件,m3u8,demo级别

【NodeJs】使用ffmpeg把任意视频文件格式转为加密后的m3u8文件

1. 第一行: – 含义:这是客户端请求密钥的 URL。播放器在播放时会访问这个 URL 来获取密钥。 – 用途:用于告诉播放器在哪里可以找到加密所需的密钥。 – 示例:http://example.com/keys/key.key 2. 第二行: – 含义:这是密钥文件的路径,通常是服务器上存储密钥的本地路径。 – 用途:FFmpeg 在生成加密的 TS 文件时会使用这个密钥文件。 – 示例: enc.key 3. 第三行(

m0_62442882的博客 1376

星链V3的2048条波束与1Tbps:多波束相控阵的工程代价

低轨卫星通信的容量提升,核心在于多波束相控阵天线的空间复用能力。相控阵通过控制大量天线单元的相位,可在同一阵面同时形成多条独立指向的波束,从而在有限频谱内成倍提升系统容量。星链V3宣称的2048条波束与1 Tbps汇聚吞吐量,正是基于这一原理:更多波束意味着更高的频率复用系数和更灵活的资源调度。但工程实现并非免费午餐,数字背后是供电、散热、重量、发射成本及运维复杂度的急剧攀升。从用户侧看,更精细的波束也带来切换频繁、低仰角衰减等新挑战。本文从多波束相控阵的基础原理出发,拆解波束形成、频谱复用和动态调度的工程

weixin_33462167的博客 347

如何使用windows搭建一个公网ftp/sftp服务器

在有公网ip的情况下,使用windows系统搭建一个ftp/sftp服务器会带来很多的方便,可以实现远程移动端的一键式访问,对于文件的备份、多设备的共享都十分的方便。然而网上许多的教程都是基于windows自带的iis搭建一个局域网ftp服务器,然而在搭建好后,我想要通过端口映射的方式实现在外网访问时,却发现连接不上的问题,感觉可能是因为windows家庭版的原因导致的。因此在查阅很多资料之后终于找到了一个公网ftp/sftp服务器的搭建方式,实测可用。这里把流程记录一下,做一个备忘录,也方便大家搭建。

weixin_43354138的博客 1万+

ffmpeg 安装,转视频格式为m3u8,压缩视频

本文介绍 ffmpeg 的安装,转视频格式为m3u8,压缩视频 ## ffmpeg 安装 直接安装: apt-get install ffmpeg 运行 ffmpeg看是否出现版本号以判断是否安装成功 如果不成功运行full-ffmpeg.sh ./full-ffmpeg.sh ## ffmpeg转视频格式为m3u8 ffmpeg -i test.m...

dingbenji5337的博客 646

SFF-8654接口引脚定义全解析:从SAS到U.2的高速硬件连接核心

在高速数据存储与传输领域,接口标准是连接主机与存储设备、确保信号完整性与协议互通性的物理基础。其核心原理在于通过精确定义的物理引脚和电气特性,承载不同的高速串行通信协议,如SAS和PCIe。这类高密度连接器的技术价值在于以紧凑的形态支持多协议兼容与高速率传输,满足了企业级存储、高性能计算对带宽和可靠性的苛刻要求。典型的应用场景包括服务器硬盘背板连接、NVMe U.2固态硬盘直连以及外部存储扩展。本文聚焦于SFF-8654(亦称Mini SAS HD)这一关键接口,深入剖析其引脚定义、信号分区(如高速数据通道

weixin_29188043的博客 711

使用ffmpeg将视频转成HLS(m3u8)格式

HLS (HTTP Live Streaming)是苹果推出的视频流协议,HLS格式的视频包含一个m3u8文本文件,以及众多的.ts的视频片段,而m3u8文本文件的作用就是将这些ts片段索引起来。这里有个坑,当我用上面命令行生成hls视频后,我发现每个ts的时长不是我指定的2s,而是10s。查阅资料后发现,ts切片的大小严格依赖于原始视频的GOP大小,因为必选保证一个ts内至少包含一个GOP,否则这个ts分片就无法使用。这个参数的作用就是让视频GOP大小为2s,这样就能保证ts分片大小是我们想要的2s了。

xindoo 7838

电磁炉线圈加热仿真全流程:用Ansys Maxwell分析涡流效应与热损耗

本文详细介绍了使用Ansys Maxwell进行电磁炉线圈加热仿真的全流程。通过构建2D轴对称涡流场模型,深入分析了高频下的涡流效应、趋肤效应与热损耗分布,为优化线圈设计、提升加热效率与均匀性提供了量化依据和实用指南。

sss66的博客 965

第二十七届华东杯数学建模A 题 跳台滑雪问题 完整思路模型及代码

考虑到滑行过程中的空气阻力和摩擦力对速度的影响,我们引入了不同姿态下运动员迎风面积和空气阻力系数的变化,利用牛顿第二定律建立加速度表达式,并通过数值模拟比较常见姿态下的终端速度。针对问题二,我们构建了二维空中飞行动力学模型,结合起跳速度、身体迎角、滑雪板倾角等因素,模拟运动员在空中飞行的轨迹与姿态变化,采用欧拉法对运动过程进行仿真,优化飞行路径和稳定性。仿真结果表明,运动员应在起跳后保持滑雪板与身体成“V”字形打开、重心略后倾的姿态,以增强升力、延长飞行距离并提高姿态得分。,并模拟出跳跃的轨迹和最终得分。

lichensun的博客 3209

学成在线--day13 在线学习 HLS(视频编码)

学成在线-第13天-讲义-在线学习 HLS 1 在线学习需求分析 配置ffmpeg 将视频进行转码 下面开始转成m3u8进行MP4的格式转换 生成么m3u8切割成功 这块地址再次埋下坑,没有hls做为子路径,视频会找不到,并且这里面还有个坑,就是host要配置video.xuecheng,com这点视频里面没有说,请结合文档...

zgz102928的博客 788

3步!Spring Boot+OCR引擎秒提身份证/营业执照?手把手教你从零实现!

OCR技术革新文档识别效率 本文演示了如何利用Spring Boot+Tesseract OCR+OpenCV构建自动化文档识别系统。相比传统人工录入,OCR技术可实现: 5秒极速提取身份证/营业执照关键信息 精准识别中文文本(准确率90%+) 正则过滤自动匹配结构化数据 技术要点: OpenCV图像预处理(灰度化+二值化) Tesseract多语言OCR识别 正则表达式精准提取证件号码 效率提升达100倍,错误率降低90%,适用于金融、政务等需要批量处理证件的场景。

java专栏 905

使用ffmpeg转码m3u8并播放

简介 随着客户的增加,mp4文件播放的弊端也日益凸显,主要集中在两个方面 当视频时长比较长的时候,mp4的关键帧元素往往很大,需要加载很长时间才能开始播放,网速不好的情况缓冲加载就要20多秒的时间,客户早已急不可耐。 当用户打开一个视频播放的时候,浏览器会持续请求下载mp4文件直到下载完成,就算是用户暂停视频播放浏览器也会持续这种下载状态,如果这个视频文件是500M则会请求服务器下载500M...

悠闲咖啡007的博客 4万+

uniapp 如何封装实现任意页面都能使用的全局弹窗

项目内容场景全局监听消息提示难点uniapp 无法全局挂载组件解决方案动态创建 DOM + 动态 Vue 实例优势跨页面 / 不依赖页面组件树 / 灵活调用如果你使用的是小程序平台,DOM 操作可能受限,需要用或方式兼容处理。想支持多个弹窗堆叠?你可以扩展,加个弹窗队列管理器。

代码简单说 Vue、JAVA、PHP、Node.js 熟练运用,接口、架构、性能全搞定。 944

反卷积通俗详细解析与nn.ConvTranspose2d重要参数解释

反卷积的作用 卷积中padding的几个概念 No Padding Half(Same) Padding Full Padding 反卷积 反卷积中的Padding参数 反卷积的stride参数 反卷积总结 参考资料

iioSnail的博客 2万+

帝国cms后台上传大视频上传m3u8批量上传一次多个的解决思路和方法

今天我们要解决的问题是,大视频文件上传以及切片m3u8文件的上传。上面一节内容,我们已经整体描述了视频类网站的解决思路和方法 。这节我们将分块来讲。假设的前提条件是,我们本地准备了一些视频原始素材,比如高清avi的视频,供用户下载。转码后的mp4文件,可供电脑,手机端播放,m3u8切片文件,也可供电脑和手机端进行播放,这处相对mp4格式速度上会好一些,至于其它的优点,在另外一篇文章会讲。 第一、...

php全栈程序员。国学api开发。 2160

精雕软件(JDpaint) 5.5 绿色版

精雕软件(JDpaint) 5.5 绿色版精雕软件(JDpaint) 5.5 绿色版精雕软件(JDpaint) 5.5 绿色版

大文件切片上传、视频切片上传转m3u8播放

大文件切片上传、视频文件上传转码、视频文件切片上传转码 调用ffmpeg视频转码不需配置环境变量、mp4转m3u8文件优化

sirwsl 1万+

廖雪峰Python教程[完整版]

廖雪峰的python教程整理的,已经排好版了,而且还是完整的,适合小白

Data-Structures-and-Algorithm-Analysis-in-Java.pdf

作者: Weiss, Mark A. 出版社: Addison Wesley Data Structures and Algorithm Analysis in Java is an “advanced algorithms” book that fits between traditional CS2 and Algorithms Analysis courses. In the old ACM Curriculum Guidelines, this course was known as CS7. This text is for readers who want to learn good programming and algorithm analysis skills simultaneously so that they can develop such programs with the maximum amo...

vue分片上传视频并转换为m3u8文件并播放

基于若依开源框架的前后端分离版本的实践,后端java的springboot,前端若依的vue2,做一个分片上传视频并分段播放的功能,因为是小项目,并没有专门准备文件服务器和CDN服务,后端也是套用的若依的上传功能。

占星安啦的博客 4811

大中型超市收银系统(可直接运行)

光步前台收银系统系统功能:商品销售、商品退货、挂单、收银员权限、会员销售、单品取消、整单取消、商品查询、脱机销售、脱机数据上传,基本资料下载,开钱箱、小票打印、收款结算,支持多币种付款方式(现金、储值卡、礼券、微信等),支持多种促销。运行环境:.net 4.0登陆信息用户名:1001 密码:123456内置商品信息,商品编码可输入00001000 - 00010000,测试效果相关功能键F5 结算 F9挂单 F11商品查询 F8退货 Insert单品取消 Del整单取消 F12开钱箱F2 会员 Ctrl+Z退出系统 Home 锁屏 相关源码待公开

m3u8视频文件夹COS上传及播放

m3u8视频文件夹COS上传及播放。

Guojun19900715的博客 4111

php上传视频到七牛云并切片转m3u8,如何利用七牛云存储实现 HLS 的多码率播放

当前的音视频应用必须面对各种不同的播放客户端,从 PC 端(Web)开始,到智能手机。这些播放客户端有不同的尺寸,因而需要不同的分辨率。视频不同的客户端也会有不同的网络环境,比如 PC 上通常都有 WiFi 或有线网络,有较高的带宽保证,而手机、平板电脑在室外时,使用 2G/3G 网络,往往带宽不足,或者不稳定。因而需要针对不同的网络环境采用不同的码率。HLS 本身所具备的的多码率播放能力对于音视...

weixin_39729840的博客 620

Linux视频切片m3u8,Rtmp转m3u8视频切片系统及方法

Rtmp转m3u8视频切片系统及方法【技术领域】[0001]本发明涉及视频数据传输及播放技术领域,尤其涉及一种RTMP转M3U8视频切片系统及方法。【背景技术】[0002]传统的视频监控系统是一种模拟视频监控系统,模拟视频监控系统是由监控终端和电视墙组成,在模拟视频监控系统中,前端的摄像头通过视频电缆将模拟视频信号发送给矩阵监控主机,矩阵监控主机再将收到的模拟视频信号输送到主控室的电视墙上,供用户...

weixin_31520593的博客 1499

05-视频编码-FFmpeg生成m3u8文件

2.2 生成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_fm

minihuabei的博客 5975

m3u8在线播放接口附成品

简介: m3u8在线播放接口及成品(不仅限于m3u8格式) 使用方法: 源码上传好输入 m3u8地址回车即可 接口测试:视频格式基本全部支持在线播放,不仅限于m3u8 网盘下载地址: http://kekewangLuo.cc/ksvgnAJ5TG70 图片: ...

www_5438xiazai的博客 3万+

ffmpeg转码生成的m3u8格式详解

ffmpeg转码生成的m3u8格式详解

m0_60259116的博客 4594

FFMPEG实现MP4和m3u8视频文件的转换

一、什么是ffmpeg? FFmpeg是一套可以用来记录、转换数字音频、视频,并能将其转化为流的开源计算机程序。采用LGPL或GPL许可证。它提供了录制、转换以及流化音视频的完整解决方案。它包含了非常先进的音频/视频编解码库libavcodec,为了保证高可移植性和编解码质量,libavcodec里很多code都是从头开发的。 FFmpeg在Linux平台下开发,但它同样也可以在其它操作系统环境中编译运行,包括Windows、Mac OS X等。这个项目最早由Fabrice Bellard发起,2004

JYliangliang的博客 2812
上一篇: 微服务[学成在线] day12:基于 Nuxt.js 构建搜索前端工程
下一篇: 微服务[学成在线] day14:媒资管理
cory_lin
博客等级 码龄7年 22粉丝 24原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值