在线学习需求分析
需求描述
学成在线作为在线教育网站,提供多种学习形式,包括:录播,直播,图文,社群等,学生登录进入学习中心即可在线学习,本章节将开发录播课程的在线学习功能,需求如下:
1.学生可以在windows浏览器上观看视频
2.播放器具有快进,快退,暂停等功能.
3.学生可以方便进行切换章节学习.
什么是录播课程?
即使提前录制好视频,供用户在线点播,反复学习.
课程视频如何管理?
媒资管理系统专门来管理视频,用户视频上传到媒体资源,并对视频进行编码处理.
视频点播解决方案
流媒体
所谓流媒体,就是采用流式传输的方式在Internet播放的媒体格式.流媒体又叫流式媒体 ,它是指商家用一个视频传送服务器把节目当成数据包发送出区,传送到网络上,用黑奴通过结扎设备对这些数据进行解压后,节目回想发送之前那样显示出来.
流媒体(StreamingMedia)的出现,极大的方便了人们的工作和生活,在地球的另一端,某大学的课堂上,某个教授正在兴致盎然的传授一门你喜欢的课程,想听?太远!放弃?可惜?没关系,网络时代能满足的你的愿望.在网络上装到该在在线课程,课程很长,的那没有关系,只管点击播放,教授的身影很快出现在屏幕上,课程一边播放一遍下载,虽然远在天涯,确如亲临现场!除了远程教育,流媒体在视频点播,玩过电台,网络视频等方面也有着广泛的应用.
概括理解:流媒体就是将视频文件分成许多的小块儿,将这些小块儿作为数据包通过网络发送出去,实现一边传输数据包,一边观看视频.
流式传输
在网络上传输音,视频信息有两个方式:下载和流式传输.
下载:就是把音,视频文件王权下载到本机后开始播放,他的特点是必须等要视频文件下载完成方可播放,播放时等待时间较长,无法去播放还未下载的部分视频.
流式传输:就是客户端通过连接视频服务器试试传输音,视频信息,实现"便下载边播放".
流式传输包括如下两种方式:
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(Http Live Streaming)是Apple的动态码率自适应技术.主要用于PC和Apple终端的音视频服务.包括一个m3u(8)的索引文件,TS媒体分片文件和key加密串文件.
HLS的工作方式是:将视频拆分成若干ts格式的小文件,通过m3u8格式的索引文件对这些ts小文件建立索引.一般是10s一个ts文件,播放器连接m3u8文件播放,当快进是,通过m3u8即可找到对应的索引文件,并去下载对应的ts文件,从而实现快进,快退以近实时的方式播放 视频.
IOS,Android设备,及各大浏览器都支持HLS协议.
详细参考:https://baike.baidu.com/item/HLS/8328931?fr=aladdin
采用HLS方案即可实现边下载边播放,并可不用使用rtmp等流媒体协议,不用构建专用的媒体服务器,节省成本。本项目点播方案确定为方案3。
视屏编码
视频编码格式
所谓视频编码方式就是指通过特定的压缩技术,将某个视频格式的文件转换成另一中视频格式文件的方式.视频流传输中最为中的编解码标准由国际电联的H.261,H.263,H.264,运动精致图像专家组的M-JPEG和国际标准化组织运动图像专家组的MPEG系列标准,此外,在互联网上被广泛应用的还有Real-Networks的ResalVideo,微软公司的WMV以及Apple公司的QuickTIme等.
详情参考: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.
音视频编码格式种类反对,主要由以下几类:
MPEG系列,(由ISO[国际标准直至机构]下属的MPEG[运动图像专家组]开发)视频编码方面主要是Mpeg1(vcd用的就是它),Mpeg2(DVD使用),Mpeg4(DVDRIP使用的都是他的变种,如:divx,xvid等),Mpeg4 AVC(正热门);音频编码方面主要是MPEGAudioLayer1/2,MPEGAudio Layer 3(大名鼎鼎的mp3),MPEG-2AAC,MPEG-4AAC等等,注意:DVD音频没有采用Mpeg的.
H.26X系列(由ITU[国际电传视讯联盟]主导,侧重网络传输,注意:只是视频编码)包括H.261,H.262,H.263,H.263+,H.263++,H.264(就是MpEG4AVC-合作结晶)
目前最常用的编码标准是H.264,音频AAC.
提问:
H.264是编码格式还是文件格式?
编码格式
mp4是编码格式还是文件格式?
文件格式,是一个拓展名
FFmpeg的基本使用
FFmpeg
是一套可以用来记录转换数字音频,视频,并能将其转化为流的开源计算机程序,采用LGPL或者GPL许可证,他提供了录制,转换以及流化音视频的玩财政解决方案,它包括了非常陷阱的音频/视频编码库libavcodec,为了保证高可以执行和编码质量,libavcodec里很多code都是从头开发的.
FFmpeg
在linux频台下开发,但它同样也是可以在其他操作系统环境中编译运行,包括Windows,MacOsX等.这个项目最早是由Fabrice Bellard发起,2004年至2015年由Michael Niedermayer主要负责维护,许多FFmpeg的开人员都来自Mplayer项目,而且当前FFmpeg也是放在MPlayere项目组的服务器上,项目的名称来自MPEG视频编码标准,前面的"FF"代表"Fast Forward"/
FFmpeg被许多开源项目采用,QQ影音,暴风影音,Vlc等.
下载:FFmpeghttps://www.ffmpeg.org/download.html#build-windows
下载:ffmpeg-20180227-fa0c9d6-win64-static.zip,并解压,本教程将ffmpeg解压到了F:\devenv\edusoft\ffmpeg-20180227-fa0c9d6-win64-static\ffmpeg-20180227-fa0c9d6-win64-static下
将F:\devenv\edusoft\ffmpeg-20180227-fa0c9d6-win64-static\ffmpeg-20180227-fa0c9d6-win64-static\bin目录配置在path环境变量中。
检测是否安装成功:
简单的测试:
将一个.avi文件转成mp4、mp3、gif等。
比如我们将lucene.avi文件转成mp4,运行如下命令:ffmpeg-ilucene.avilucene.mp4
转成mp3:ffmpeg-ilucene.avilucene.mp3
转成gif:ffmpeg-ilucene.avilucene.gif
官方文档(英文):http://ffmpeg.org/ffmpeg.html
生成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,YUV:4: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_%05d.ts ./hls/lucene.m3u8
-hls_time 设置每篇的长度,单位为秒
-hls_list_size n: 保存的分片的数量,设置为0表示保所有的分片
-hls_segment_filename: 段文件的名称,%05d表示5位数字
生成的效果是:将lucene.mp4视频文件将每10秒生成一个ts文件,最后生成一个m3u8文件时ts的索引文件 .
使用VLC打开m3u8文件,测试播放效果,VLC是一款自由,开源的跨平台多媒体播放器及框架,可播放大多数多媒体文件,以及DVD,音频CD,VCD及各类流媒体协议.(http://www.video.org/)
码率的设置
码率又叫比特率,即每秒传输的bit数,单位为bps(Bit Per Second),码率越大,传送数据的速度越快.码率的计算公式时:文件大小(转成bit)/市场(秒)/1024 = kbps 即每秒传输千位数
例如一个1M的视频,他的市场时10s,他的码率等于
1*1024*1024*8/10/1024 = 819Kbps
码率设置到多少才能达到最好?通过根据个人的经验或者参考一些网络平台给出的参考,下图时优酷对码率的要求:
如果要将视频上传到优酷则必须按照上面的要求,如果是自己搭建视频服务器,码率设置不易过大,最终达到的视频清晰度满足业务需求即可。
播放器
技术选型
视频编码后要使用播放器对其进行解码,播放视频内容.咋iweb应用中常用的播放器由flash播放器,h5播放器或则和浏览器插件播放器,其中以flash和H5播放器最常见.
flash播放器:缺点是需要客户机安装Adobe FlashPlayer 播放器,优点是flash播放器已经很成熟了,并且浏览器对flash支持也很好.
H5播放器: 基于h5自带video标签进行构建,优点是大部分浏览器支持h5,不用再安装第三方的flash播放器,并且随着前端技术的发展,h5技术会越来越成熟.
本项目采用
H5播放器,使用Video.js开源播放器.
Video.js是一款基于Html5世界网络视频播放器,它支持Html5和Flash视频,它支持在台式机和一地哦那个设备上播放视频,这个项目于2010年终开始,目前已经在40万网站使用.
官方地址:http://videojs.com/
下载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.js6.7.3版本,videojs-contrib-hls5.14.1版本。
下载上边两个文件,为了测试需求将其放在门户的plugins目录中。
搭建媒体服务器
正常使用video.js播放视频是通过一个网页,用户通过使用浏览器打开网页,去播放视频,网页和视频都从web服务器请求,通常视频的url地址使用单独的域名.
Nginx媒体服务器
HLS协议基于Http协议,本项目使用Nginx作为视频服务器.下图是Nginx媒体服务器的配置流程图:
媒体服务器代理
媒体服务器不止一台,通过代理实现负载均衡功能,使用Nginx作为媒体服务器的代理,此代理服务器作为video.xuecheng.com域名服务器.
配置video.xuecheng.com虚拟主机:
注意:开发中代理服务器和媒体服务器在同一台服务器中,使用同一个nginx
#学成网媒体服务代理map$http_origin$origin_list{
defaulthttp://www.xuecheng.com;"~http://www.xuecheng.com"http://www.xuecheng.com;"~http://ucenter.xuecheng.com"http://ucenter.xuecheng.com;
}
#学成网媒体服务代理server{
listen80;
server_namevideo.xuecheng.com;
location/video{
proxy_passhttp://video_server_pool;
add_headerAccess‐Control‐Allow‐Origin$origin_list;#add_headerAccess‐Control‐Allow‐Origin*;add_headerAccess‐Control‐Allow‐Credentialstrue;add_headerAccess‐Control‐Allow‐MethodsGET;
}
}
cors跨域参数:
Access-Control-Allow-Origin:允许跨域访问的外域地址
通常允许跨域访问的站点不是一个,所以这里用map定义了多个站点。如果允许任何站点跨域访问则设置为*,通常这是不建议的。
Access-Control-Allow-Credentials:允许客户端携带证书访问
Access-Control-Allow-Methods:允许客户端跨域访问的方法
video_server_pool的配置如下:
#媒体服务
upstreamvideo_server_pool{server127.0.0.1:90weight=10;
}
测试video.js
2、测试
配置hosts文件,本教程开发环境使用Window10,修改C:\Windows\System32\drivers\etc\hosts文件
127.0.0.1 video.xuecheng.com
搭建学习中心前端
学成网学习中西提供学生在线学习的各各模块,上一章节测试的点播学习哦功能也属于学习中心的一部分,本章节将实现喜喜中心点播学习的前端部分,之所以实现前端部分,主要是 因为要将video.js+vue.js集成,一部分激励还是要防砸i技术研究.
界面原型
先看一看界面原型,如下图,最终的目标时再次页面使用video.js播放视频
创建学习中工程
学习中心的用户时学生,为了便于维护和拓展,单独创建学习中心工程:
1、从资料目录拷贝xc-ui-pc-leanring.zip并解压到xc-ui-pc-leanring目录。
2、使用webstorm创建打开xc-ui-pc-leanring目录
3、进入xc-ui-pc-leanring目录,执行cnpminstall,将根据package.json的依赖配置远程下载依赖的js包。创建完成,xc-ui-pc-leanring工程如下:
配置域名
#gzip on;
#CMS页面预览
upstream cms_server_pool{
server 127.0.0.1:31001 weight=10;
}
#图片服务配置了group1下的storage服务器地址
upstream img_server_pool{
#server 192.168.234.128:80 weight=10;
server 192.168.234.131:80 weight=10;
}
#静态资源服务
upstream static_server_pool{
server 127.0.0.1:91 weight=10;
}
#前端动态门户nuxt.js
upstream dynamic_portal_server_pool{
server 127.0.0.1:10001 weight=10;
}
#后台搜索(公开api)
upstream search_server_pool{
server 127.0.0.1:40100 weight=10;
}
#媒体服务
upstream video_server_pool{
server 127.0.0.1:90 weight=10;
}
#前端ucenter
upstream ucenter_server_pool{
#server 127.0.0.1:7081 weight=10;
server 127.0.0.1:13000 weight=10;
}
server {
listen 80;
server_name www.xuecheng.com;
location / {
ssi on;
ssi_silent_errors on;
alias E:/jetbrain/ws/xc-ui-pc-static-portal/;
index index.html;
}
#静态资源,包括系统所需的图片,js,css等静态资源
location /static/img/ {
alias E:/jetbrain/ws/xc-ui-pc-static-portal/img/;
}
location /static/css/ {
alias E:/jetbrain/ws/xc-ui-pc-static-portal/css/;
}
location /static/js/ {
alias E:/jetbrain/ws/xc-ui-pc-static-portal/js/;
}
location /static/plugins/ {
alias E:/jetbrain/ws/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;
}
location /plugins/ {
alias E:/jetbrain/ws/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;
}
#页面预览
location /cms/preview/{
proxy_pass http://cms_server_pool/cms/preview/;
}
location /static/company/ {
proxy_pass http://static_server_pool;
}
location /tatic/teacher/ {
proxy_pass http://static_server_pool;
}
location /static/stat/ {
proxy_pass http://static_server_pool;
}
location /course/detail/ {
proxy_pass http://static_server_pool;
}
#前端门户课程搜索
location ^~ /course/search{
proxy_pass http://dynamic_portal_server_pool;
}
#后端搜索服务
location /openapi/search/ {
proxy_pass http://search_server_pool/search/;
}
#分类信息
location /static/category/ {
proxy_pass http://static_server_pool;
}
#开发环境webpack定时加载此文件
location ^~ /__webpack_hmr{
proxy_pass http://dynamic_portal_server_pool/__webpack_hmr;
}
#开发环境nuxt访问_nuxt
location ^~ /_nuxt/{
proxy_pass http://dynamic_portal_server_pool/_nuxt/;
}
}
#学成网图片服务
server{
listen 80;
server_name img.xuecheng.com;
#个人中心
location /group1{
proxy_pass http://img_server_pool;
}
}
#学成网用户中心
server{
listen 80;
server_name ucenter.xuecheng.com;
#个人中心
location / {
proxy_pass http://ucenter_server_pool;
}
}
#学成网媒体服务代理
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;
}
}
#学成网媒体服务
server{
listen 90;
server_name localhost;
#视频目录
location /video/ {
#当外界有/video请求时,就去下面的目录寻找
alias E:/jetbrain/video/;
}
}
#学成网静态资源
server{
listen 91;
server_name localhost;
#公司信息
location /static/company/ {
ssi on;
ssi_silent_errors on;
alias E:/jetbrain/ws/xc-ui-pc-static-portal/static/company/;
}
#老师信息
location /static/teacher/ {
ssi on;
ssi_silent_errors on;
alias E:/jetbrain/ws/xc-ui-pc-static-portal/static/teacher/;
}
#统计信息
location /static/stat/ {
ssi on;
ssi_silent_errors on;
alias E:/jetbrain/ws/xc-ui-pc-static-portal/static/stat/;
}
location /course/detail/ {
ssi on;
ssi_silent_errors on;
alias E:/jetbrain/ws/xc-ui-pc-static-portal/static/course/detail/;
}
#分类信息
location /static/category/{
ssi on;
ssi_silent_errors on;
alias E:/jetbrain/ws/xc-ui-pc-static-portal/static/category/;
}
}
访问
访问的时候会有一个跨域问题,在nginx中配置相应路由解决
调试视频播放页面
媒资管理
媒体资源管理(Media Asset Management ,简称MAM)是对各种类型的媒体资料数据,如视音频资料,文本文件,图标进行全面管理的完整解决方案,其目的时将现有的影视节目进行数字化或者数据化,并采用适当的方式编码,在记录到成熟稳定的媒体上,达到影视节目长期保存和重复利用的目的,以满足影视节目的制作,播出和交换的需求.
每个教学机构都可以在媒资系统管理自己的教学资源,包括视频,教案等文件
目前媒资管理主要管理对象时课程录播视频,包括妹子文件的查询 ,视频上床,视频删除,视频处理等.
媒资查询:教学机构查询自己所拥有的媒体文件.
视频上传:将用户线下录制的教学视频上传到媒资系统.
视频处理:视频上传成功,系统自动对食品进行编码处理.
视频删除:如果该视频已不再使用,可以从媒资系统删除.
下边是媒资系统与其他系统的交互情况:
需求分析
1.上传媒资文件
文件成功上传,将文件存储到媒资服务器中,将文件信息存储到数据库中.
2.使用媒资
课程管理请求媒资系统查询媒资信息,将课程计划于媒资信息对应,存储
3.用户进入学习中心,请求学习服务学习在线播放视频.
学习服务校验用户资格通过后请求媒资烯体哦那个获取视频地址.
开发环境
创建媒资数据
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;
/**
* @Author: mrt.
* @Description:
* @Date:Created in 2018/1/24 10:04.
* @Modified By:
*/
@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;
}
在mongoDB中建立相应文件
创建媒资服务工程
导入文件,之后导入工程
上传文件
断点续传解决方案
通常视频文件都比较大,所以对于媒资系统上传文件的需求要满足大文件上传要求.http协议本身对上传文件大小没有限制,但是客户的网络环境质量,电脑硬件环境等参差不齐,如果一个大文件快上船完了网断了,电断了没有完成上传,需要客户重新上传,这是致命的,所以,对于大文件的上传要求时最基本的是断电续传:
应用百度百科:断点续传值得是在下载或上传的时候,将下载或者上传任务(一个文件或者一个压缩包)认为划分为几个部分,每一个部分采用一个线程进行上传或者下载,如果碰到网络故障,可以从已经上传或者下载的部分开始继续上传或者下载未完成的部分,而且没有必要从头开始下载,断电续传可以提高节省操作的时间,提高用户的体验性.
如下图:
文件分块与合并
为了更好的理解文件上传的原理,下边用java代码测试文件的分块与合并
文件分块
package com.xuecheng.manage_media;
import org.junit.Test;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.RandomAccessFile;
/**
* @author Andrewer
* @version 1.0
* @project xcEduService01
* @description
* @date 2023/1/4 21:18:58
*/
public class TestFile {
// 测试文件的分块
@Test
public void testChunk() throws IOException {
// 源文件
File sourceFile = new File("E:\\jetbrain\\video\\lucene.avi");
// 快文件目录
String chunkFileFolder = "E:\\jetbrain\\video\\chunks\\";
// 定义快文件的大小1M
long chunkFileSize = 1 * 1024 * 1024;
// 定义块数
long chunkFileNum = (long)Math.ceil(sourceFile.length()*1.0/chunkFileSize);
// 创建一个读文件的对象
RandomAccessFile raf_read = new RandomAccessFile(sourceFile,"r");
// 缓冲区
byte[] bytes = new byte[1024];
for (int i = 0; i < chunkFileNum; i++) {
// 块文件
File chunkFile = new File(chunkFileFolder + i);
RandomAccessFile raf_write = new RandomAccessFile(chunkFile, "rw");
int len = -1;
while ((len = raf_read.read(bytes))!=-1){
// 创建向块文件的写对象
raf_write.write(bytes,0,len);
// 如果块文件的大小达到了1M,那么就可以读下一块
if (chunkFile.length()>=chunkFileSize){
break;
}
}
raf_write.close();
}
raf_read.close();
}
// 测试文件的合并
}
实现分块
文件合并
文件合并流程:
1.找到要合并的二我呢见,并按文件合并的向后进行排序.
2.创建合并文件
3.依次从合并的文件中读取数据乡合并文件写入数据
// 测试文件的合并
@Test
public void testMergeFile() throws IOException {
// 得到块文件的目录
String chunkFielFolderPath = "E:\\jetbrain\\video\\chunks\\";
// 块文件目录对象
File chunkFileFolder = new File(chunkFielFolderPath);
File[] files = chunkFileFolder.listFiles();
List<File> fileList = Arrays.asList(files);
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;
}
});
// 创建一个合并对象
File mergeFile = new File("E:\\jetbrain\\video\\lucene_merge.avi");
// 创建一个新文件
boolean newFile = mergeFile.createNewFile();
// 创建一个写对象
RandomAccessFile raf_write = new RandomAccessFile(mergeFile,"rw");
byte[] bytes = new byte[1024];
for (File file : fileList) {
// 创建一个读文件对象
RandomAccessFile raf_read = new RandomAccessFile(file,"r");
int len = -1;
while ((len=raf_read.read(bytes))!=-1){
raf_write.write(bytes,0,len);
}
raf_read.close();
}
raf_write.close();
}
}
前端页面
上传文件的页面内容参考: “资料”—>upload.vue文件
WebUploader介绍
如何在web页面实现断点续传?
创建方案有:
1.通过Flash上传,比如SWFupload,Uploadify.
2.安装浏览器插件,变相的pc客户端,用的比较少
3.Html5
跟着html5的流行,本项目采用Html5完成分块上传
本项目使用WebUploader完成大文件上传功能的开发,WebUploader官网地址,:http://fexteam.gz01.bdysite.com/webuploader/
钩子方法
构建WebUPloader
before-send-file
before-send
after-send-file
页面效果
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 jdk.management.resource.ResourceRequest;
import org.springframework.web.multipart.MultipartFile;
/**
* @author Andrewer
* @version 1.0
* @project xcEduService01
* @description
* @date 2023/1/6 00:46:50
*/
@Api(value = "媒资管理接口",description = "媒资管理,提供文件上传,处理等接口")
public interface MediaUploadControllerApi {
// 文件上传前的准备工作,以及校验工作(文件是否存在)
@ApiOperation("文件上传注册")
public ResourceRequest register(String fileMD5,
String fileName,
Long fileSize,
String mimetype,
String fileExt);
@ApiOperation("校验分块是否存在")
public CheckChunkResult checkchunk(String fileMd5,
Integer chunck,
Integer chunckSize);
@ApiOperation("上传分块")
public ResponseResult uploadchunk(MultipartFile file,
String fileMd5,
Integer chunck);
@ApiOperation("合并分块")
public ResponseResult mergechunks(String fileMd5,
String fileName,
Long fileSize,
String mimetype,
String fileExt);
}
媒资服务端编写
业务流程
服务端需要实现如下功能:
1.上传前检查上传环境
减产文件是否上传,已上传则直接返回.
检查文件上传路径是否存在,不存在则创建.
2.分块检查
检查分块文件是否上传,已经上传返回true.
未上传检查上传路径是否存在,不存在则创建.
3.分块上船
将分快文件上传到指定的路径.
4.合并分块
将所有分块文件合并为一个文件.
上传注册
由于上传过程复杂,开发时按业务流程分别实现.
1.配置(检查上传文件是否存在)
application.yml配置上传文件的路径:
2.定义Dao
媒资管理Dao
最总上传的文件需要在媒资管理Dao中查看
package com.xuecheng.manage_media.dao;
import com.xuecheng.framework.domain.media.MediaFile;
import org.springframework.data.mongodb.repository.MongoRepository;
public interface MediaFileRepository extends MongoRepository<MediaFile,String> {
}
分块注册
Controller:
@Override
@PostMapping("/register")
public ResponseResult register(String fileMd5, String fileName, Long fileSize, String mimetype, String fileExt) {
return mediaUploadService.register(fileMd5,fileName,fileSize,mimetype,fileExt);
}
Service
package com.xuecheng.manage_media.service;
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.dao.MediaFileRepository;
import jdk.management.resource.ResourceRequest;
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.DigestUtils;
import org.springframework.web.multipart.MultipartFile;
import java.io.*;
import java.util.*;
/**
* @author Andrewer
* @version 1.0
* @project xcEduService01
* @description
* @date 2023/1/6 02:45:43
*/
@Service
public class MediaUploadService {
@Autowired
MediaFileRepository mediaFileRepository;
@Value("${xc-service-manage-media.upload-location}")
String uploadlocation;
//得到文件所属目录的路径
private String getFileFolderPath(String fileMd5) {
return uploadlocation + fileMd5.substring(0, 1) + "/" + fileMd5.substring(1, 2) + "/" + fileMd5;
}
//得到文件的路径
private String getFilePath(String fileMd5, String fileExt) {
return uploadlocation + fileMd5.substring(0, 1) + "/" + fileMd5.substring(1, 2) + "/" + fileMd5 + "/" + fileMd5 + "." + fileExt;
}
//得到块文件的路径
private String getChunkFileFolderPath(String fileMd5) {
return uploadlocation + fileMd5.substring(0, 1) + "/" + fileMd5.substring(1, 2) + "/" + fileMd5 + "/chunk/";
}
/*
* 根据文件md5得到文件的路径
* 规则:
* 一级目录:md5的第一字符
* 二级目录:md5的第二个字符
* 三级目录:md5
* 文件名:md5+文件拓展名
* @param fileMd5文件md5值
* @Param fileExt 文件拓展名
* @return 文件路径
*
* */
//文件上传前的注册,检查文件是否存在
public ResponseResult register(String fileMd5, String fileName, Long fileSize, String mimetype, String fileExt) {
// 1.检查文件在磁盘上是否存在
// 先拿到文件所属目录的路径
String fileFolderPath = this.getFileFolderPath(fileMd5);
// 再拿到文件的路径
String filePath = this.getFilePath(fileMd5, fileExt);
// 检查文件是否存在
File file = new File(filePath);
boolean exists = file.exists();
// 这个如果时true表示存在
// 2.检查文件的信息在mongdb中是否存在
Optional<MediaFile> optional = mediaFileRepository.findById(fileMd5);
if (exists && optional.isPresent()) {
// 文件存在
ExceptionCast.cast(MediaCode.UPLOAD_FILE_REGISTER_EXIST);
}
// 文件不存在时,做一些准备工作,检查文件所在目录是否存在,不存在则创建
File fileFolder = new File(fileFolderPath);
if (!fileFolder.exists()) {
fileFolder.mkdirs();
}
return new ResponseResult(CommonCode.SUCCESS);
}
分块检查
Controller
@Override
@PostMapping("/checkchunk")
public CheckChunkResult checkchunk(String fileMd5, Integer chunk, Integer chunkSize) {
return mediaUploadService.checkchunk(fileMd5,chunk,chunkSize);
}
Service
// 分块检查,检查分块是否存在
/**
* @param fileMd5 文件md5
* @param chunk 块的下标
* @param chunckSize 块的大小
* @return
*/
public CheckChunkResult checkchunk(String fileMd5, Integer chunk, Integer chunckSize) {
//检查分块文件是否存在在
// 得到分块文件所在的目录
String chunkFileFolderPath = this.getChunkFileFolderPath(fileMd5);
File file = new File(chunkFileFolderPath + chunk);
if (file.exists()) {
//文件存在
return new CheckChunkResult(CommonCode.SUCCESS, true);
} else {
//文件不存在
return new CheckChunkResult(CommonCode.SUCCESS, false);
}
}
//上传分块
public ResponseResult uploadchunk(MultipartFile file, String fileMd5, Integer chunk) {
// 检查分块目录,如果不存在,则要自动创建
// 得到分块的目录
String chunkFileFolderPath = this.getChunkFileFolderPath(fileMd5);
// 得到分块文件的路径
String chunkFilePath = chunkFileFolderPath + chunk;
File chunkFileFolder = new File(chunkFileFolderPath);
// 如果不存在,则需要自动创建
if (!chunkFileFolder.exists()) {
chunkFileFolder.mkdirs();
}
// 得到上传文件的输入流
InputStream inputStream = null;
FileOutputStream outputStream = null;
try {
inputStream = file.getInputStream();
outputStream = new FileOutputStream(new File(chunkFilePath));
IOUtils.copy(inputStream, outputStream);
} catch (IOException e) {
} finally {
try {
inputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
try {
outputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
return new ResponseResult(CommonCode.SUCCESS);
}
上传分块
Controller
@Override
@PostMapping("/uploadchunk")
public ResponseResult uploadchunk(MultipartFile file, String fileMd5, Integer chunk) {
return mediaUploadService.uploadchunk(file,fileMd5,chunk);
}
Service:
//上传分块
public ResponseResult uploadchunk(MultipartFile file, String fileMd5, Integer chunk) {
// 检查分块目录,如果不存在,则要自动创建
// 得到分块的目录
String chunkFileFolderPath = this.getChunkFileFolderPath(fileMd5);
// 得到分块文件的路径
String chunkFilePath = chunkFileFolderPath + chunk;
File chunkFileFolder = new File(chunkFileFolderPath);
// 如果不存在,则需要自动创建
if (!chunkFileFolder.exists()) {
chunkFileFolder.mkdirs();
}
// 得到上传文件的输入流
InputStream inputStream = null;
FileOutputStream outputStream = null;
try {
inputStream = file.getInputStream();
outputStream = new FileOutputStream(new File(chunkFilePath));
IOUtils.copy(inputStream, outputStream);
} catch (IOException e) {
} finally {
try {
inputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
try {
outputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
return new ResponseResult(CommonCode.SUCCESS);
}
合并分块
Controller:
@Override
@PostMapping("/mergechunks")
public ResponseResult mergechunks(String fileMd5, String fileName, Long fileSize, String mimetype, String fileExt) {
return mediaUploadService.mergechunks(fileMd5,fileName,fileSize,mimetype,fileExt);
}
Service
public ResponseResult mergechunks(String fileMd5, String fileName, Long fileSize, String mimetype, String fileExt) {
//合并所有的分块
String chunkFileFolderPath = this.getChunkFileFolderPath(fileMd5);
File chunkFileFolder = new File(chunkFileFolderPath);
// 创建分块文件列表
File[] files = chunkFileFolder.listFiles();
List<File> files1 = Arrays.asList(files);
//1创建一个合并文件
String filePath = this.getFilePath(fileMd5, fileExt);
File mergeFile = new File(filePath);
// 执行合并
// 方法复杂一些,就创建私有方法,执行私有方法
mergeFile = this.mergeFile(files1, mergeFile);
//
if (mergeFile == null) {
// 合并文件失败
ExceptionCast.cast(MediaCode.MERGE_FILE_FAIL);
}
// 2 校验文件的md5值是否和前端的md5值一致
boolean checkFileMd5 = this.checkFileMd5(mergeFile, fileMd5);
if (!checkFileMd5) {
//校验文件失败
ExceptionCast.cast(MediaCode.MERGE_FILE_CHECKFAIL);
}
// 如果一致的化,说明前端上传的文件和合并后的文件一至
// 3 将文件的信息写入mongodb中
MediaFile mediaFile = new MediaFile();
mediaFile.setFileId(fileMd5);
mediaFile.setFileOriginalName(fileName);
mediaFile.setFileName(fileMd5 + "." + fileExt);
// 文件路径保存相对路径
String filePath1 =fileMd5.substring(0, 1) + "/" + fileMd5.substring(1, 2) + "/" + fileMd5 + "/" + fileMd5 + "." + fileExt;
mediaFile.setFilePath(filePath1);
mediaFile.setFileSize(fileSize);
mediaFile.setUploadTime(new Date());
mediaFile.setMimeType(mimetype);
mediaFile.setFileType(fileExt);
// 状态码设置上传成功
mediaFile.setFileStatus("301002");
MediaFile save = mediaFileRepository.save(mediaFile);
return new ResponseResult(CommonCode.SUCCESS);
}
// 校验文件
private boolean checkFileMd5(File mergeFile, String md5) {
// 创建一个文件的输入流
try {
FileInputStream fileInputStream = new FileInputStream(mergeFile);
// 得到文件的md5,
String md5Hex = DigestUtils.md5DigestAsHex(fileInputStream);
// 和传入文件的md5进行比较
if (md5Hex.equalsIgnoreCase(md5)) {
return true;
}
} catch (Exception e) {
e.printStackTrace();
return false;
}
return false;
}
// 合并文件
private File mergeFile(List<File> chunkFilelist, File mergeFile) {
try {
// 执行前先判断合并的文件是否存在
if (mergeFile.exists()) {
// 如果存在的化,就删掉
mergeFile.delete();
} else {
// 没有文件,那么,就创建一个新的文件
mergeFile.createNewFile();
}
// 对文件进行排序
Collections.sort(chunkFilelist, new Comparator<File>() {
@Override
public int compare(File o1, File o2) {
if ((Integer.parseInt(o1.getName()) > Integer.parseInt(o2.getName()))){
// 升序排序
return 1;
}
return -1;
}
});
//创建一个写对象
RandomAccessFile raf_write = new RandomAccessFile(mergeFile, "rw");
//读文件,有一个缓冲去
byte[] bytes = new byte[1024];
for (File file : chunkFilelist) {
//创建一个读对象
RandomAccessFile raf_read = new RandomAccessFile(file, "r");
int len = -1;
while ((len = raf_read.read(bytes)) != -1) {
raf_write.write(bytes, 0, len);
}
raf_read.close();
}
raf_write.close();
return mergeFile;
} catch (IOException e) {
e.printStackTrace();
return null;
}
}
文件上传成功
前端upload页面
<template>
<div><br/>
操作步骤:<br/>
1、点击“选择文件”,选择要上传的文件<br/>
2、点击“开始上传”,开始上传文件<br/>
3、如需重新上传请重复上边的步骤。<br/><br/>
<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>
<!--用来存放文件信息-->
<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 '../../../../static/plugins/jquery/dist/jquery.js'
import webuploader from '../../../../static/plugins/webuploader/dist/webuploader.js'
import '../../../../static/css/webuploader/webuploader.css'
export default{
data(){
return{
uploader:{},
uploadFile:{},
percentage:0,
fileMd5:''
}
},
methods:{
//开始上传
upload(){
if(this.uploadFile && this.uploadFile.id){
this.uploader.upload(this.uploadFile.id);
}else{
alert("请选择文件");
}
}
},
mounted(){
// var fileMd5;
// var uploadFile;
WebUploader.Uploader.register({
"before-send-file":"beforeSendFile",
"before-send":"beforeSend",
"after-send-file":"afterSendFile"
},{
beforeSendFile:function(file) {
// 创建一个deffered,用于通知是否完成操作
var deferred = WebUploader.Deferred();
// 计算文件的唯一标识,用于断点续传
(new WebUploader.Uploader()).md5File(file, 0, 100*1024*1024)
.then(function(val) {
this.fileMd5 = val;
this.uploadFile = file;
// alert(this.fileMd5 )
//向服务端请求注册上传文件
$.ajax(
{
type:"POST",
url:"/api/media/upload/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) {
//alert('上传文件注册成功开始上传');
deferred.resolve();
} else {
alert(response.message);
deferred.reject();
} ` `
}
}
);
}.bind(this));
return deferred.promise();
}.bind(this),
beforeSend:function(block) {
var deferred = WebUploader.Deferred();
// 每次上传分块前校验分块,如果已存在分块则不再上传,达到断点续传的目的
$.ajax(
{
type:"POST",
url:"/api/media/upload/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) {
// 合并分块
$.ajax(
{
type:"POST",
url:"/api/media/upload/mergechunks",
data:{
fileMd5:this.fileMd5,
fileName: file.name,
fileSize:file.size,
mimetype:file.type,
fileExt:file.ext
},
success:function(response){
//在这里解析合并成功结果
if(response && response.success){
alert("上传成功")
}else{
alert("上传失败")
}
}
}
);
}.bind(this)
}
);
// 创建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// 允许在文件传输时提前把下一个文件准备好
}
);
// 将文件添加到队列
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) {
console.log(reason)
alert("上传文件失败");
});
//上传成功触发
this.uploader.on("uploadSuccess", function(file,response ) {
console.log(response)
// alert("上传文件成功!");
});
//每个分块上传请求后触发
this.uploader.on( 'uploadAccept', function( file, response ) {
if(!(response && response.success)){//分块上传失败,返回false
return false;
}
});
}
}
</script>
<style scoped>
</style>