前段时间做视频上传业务,通过网页上传视频到服务器。
视频大小 小则几十M,大则 1G+,以一般的HTTP请求发送数据的方式的话,会遇到的问题:1,文件过大,超出服务端的请求大小限制;2,请求时间过长,请求超时;3,传输中断,必须重新上传导致前功尽弃;
解决方案:
1,修改服务端上传的限制配置;Nginx 以及 PHP 的上传文件限制 不宜过大,一般5M 左右为好;
2,大文件分片,一片一片的传到服务端,再由服务端合并。这么做的好处在于一旦上传失败只是损失一个分片而已,不用整个文件重传,而且每个分片的大小可以控制在4MB以内,服务端限制在4M即可。
前端
Web前端可使用HttpUploader6的大文件上传控件6;官网地址:/EyI6vHh
<divclass="section section6 section5">
<divclass="part1"><ahref="javascript:;"target="_blank"class="part1__btn">批量删除</a><spanclass="part1__txt"><emclass="part1__num"id="upload_num">0</em>个视频,共<emclass="part1__num"id="upload_size">0M</em></span></div>
<tableclass="section5__table">
<tbodyid="thelist">
<trclass="thead">
<thclass="col1 allCkeck"><inputtype="checkbox"name=""class="col1__checkBox"/>视频名称</th><thclass="col2">视频大小</th><thclass="col3">视频分类</th><thclass="col4">状态</th><thclass="col5">进度</th><th>操作</th>
</tr>
</tbody>
</table>
<divclass="selFile"id="selFile">
<divid="drag_tips">
<divid="btns__add2"></div>
<h2class="txt1">选择视频文件</h2>
<spanclass="txt2">或直接将文件拖拽至此窗口</span>
</div>
</div>
<divclass="btns"><spanclass="btns__add"id="btns__add">+添加视频文件</span><spanclass="btns__upload btns__upload-start"id="uploadBtn"><iclass="btns__upload_icon"></i>开始上传视频</span></div>
</div>
//引入插件
<scripttype="text/javascript"src="media/js/lib/webuploader/js/webuploader.min.js"></script>
upload.js
1//文件上传
2jQuery(function() {
3var$ = jQuery,
4$list = $('#thelist'),
5$btn = $('#upload-start'),
6$thead = $('.thead'),
7$part_btn = $('.part1__btn'),//批量上传按钮
8state = 'pending',
9fileCount = 0,//上传文件总数
10fileSize = 0,//上传文件的总大小
11//上传按钮
12$upload = $('#uploadBtn'),
13//所有文件的进度信息,key为file id
14percentages = {},
15//所有文件的md5,key为file id
16md5Obj = {},
17//可能有pedding, ready, uploading, confirm, done.
18state = 'pedding',
19uploader;
20
21//浏览器关闭提醒
22window.is_confirm =false;
23$(window).bind('beforeunload',function(){
24//只有在标识变量is_confirm不为false时,才弹出确认提示
25if(window.is_confirm !==false)
26return'正在上传视频,该操作将丢失视频,是否继续?';
27})
28
29if( !WebUploader.Uploader.support() ) {
30alert( 'Web Uploader不支持您的浏览器!如果你使用的是IE浏览器,请尝试升级浏览器');
31thrownewError( 'WebUploader does not support the browser you are using.' );
32}
33
34$(".pop2 .btns__sure").click(function(){
35$('.popup,.pop').hide();
36});
37
38uploader = WebUploader.create({
39//拖拽容器
40dnd:'#selFile',
41
42//不压缩image
43resize:false,
44
45// swf文件路径
46swf: '/media/js/lib/webuploader/js/Uploader.swf',
47
48//文件接收服务端。
49server: '/service/upload/upload_file',
50//server:'http://vod./service/upload/ssl_upload_file',
51formData: {
52file_id: 'file',
53guid:newDate().getTime() + Math.ceil(Math.random()*100),
54file_name:''
55},
56
57//选择文件的按钮。可选。
58//内部根据当前运行是创建,可能是input元素,也可能是flash.
59pick: {
60id:'#btns__add',
61innerHTML:"+添加视频文件"
62},
63
64//开起分片上传。
65chunked:true,
66
67//如果要分片,分多大一片2M
68chunkSize:2*1024*1024,
69
70//上传文件的类型
71accept:{
72title: 'Videos',
73extensions: 'mp4,avi,flv',
74mimeTypes: 'video/*'
75},
76//验证文件总数量, 超出则不允许加入队列。
77fileNumLimit: 10,
78//单个文件上传的大小限制 2G
79fileSingleSizeLimit:2*1024*1024*1024,
80
81});
82
83//添加文件具体函数
84functionaddFile( file ){
85vardata =newDate(),
86month = (data.getMonth()+1)<10 ? '0'+(data.getMonth()+1) : (data.getMonth()+1),
87day = data.getDate()<10 ? '0'+ data.getDate(): data.getDate(),
88time = data.getFullYear() + "-" + month + "-" + day,
89$tr = $('<tr class="toBeUploaded" id="'+file.id+'"></tr>'),
90$td = $('<td class="col1"><input type="checkbox" name="" class="col1__checkBox"/><input type="text" value="'+ file.name +'" name="" class="name"/></td><td class="col2">'+convert_size(file.size)+'</td><td class="col3"><select class="class_id">'+ class_options +'</select></td><td class="col4">读取视频中</td><td class="col5">0%</td><td class="col6"><ul><li class="view"><a target="_blank" href="javascript:;">查看</a></li><li class="delete">删除</li></ul></td>').appendTo($tr),
91$state = $tr.find('td.col4'),
92$prgress = $tr.find('td.col5'),
93$delbtn = $tr.find('li.delete');
94
95$("#selFile").hide();
96
97if( file.getStatus() === 'invalid' ) {
98switch( file.statusText ) {
99case'exceed_size':
100text = '文件大小超出';
101break;
102
103case'interrupt':
104text = '上传暂停';
105break;
106
107default:
108text = '上传失败,请重试';
109break;
110}
111showError(text);
112}else{
113// @todo lazyload
114percentages[ file.id ] = [ file.size, 0 ];
115file.rotation = 0;
116}
117
118file.on('statuschange',function( cur, prev ) {
119if( prev === 'progress' ) {
120//上传成功
121}elseif( prev === 'queued' ) {
122//开始上传
123}
124
125//成功
126if( cur === 'error' || cur === 'invalid' ) {
127console.log( file.statusText );
128showError( file.statusText );
129percentages[ file.id ][ 1 ] = 1;
130}elseif( cur === 'interrupt' ) {
131showError( 'interrupt' );
132}elseif( cur === 'queued' ) {
133percentages[ file.id ][ 1 ] = 0;
134}elseif( cur === 'progress' ) {
135//正在上传
136
137}elseif( cur === 'complete' ) {
138//上传完成
139
140}
141
142$tr.removeClass( 'state-' + prev ).addClass( 'state-' + cur );
143});
144$delbtn.on('click',function(){
145uploader.removeFile( file );
146});
147$tr.appendTo($list);
148//$tr.insertAfter($thead);
149}
150
151//负责view的销毁
152functionremoveFile( file ) {
153var$tr = $('#'+file.id);
154
155deletepercentages[ file.id ];
156$tr.off().find('.col6').off().end().remove();
157}
158
159functionsetState( val ) {
160varfile, stats;
161
162if( val === state ) {
163return;
164}
165
166$upload.removeClass( 'state-' + state );
167$upload.addClass( 'state-' + val );
168state = val;
169
170switch( state ) {
171case'pedding':
172uploader.refresh();
173break;
174
175case'ready':
176uploader.refresh();
177break;
178
179case'uploading':
180$upload.text( '暂停上传' );
181break;
182case'paused':
183$upload.text( '继续上传' );
184break;
185
186case'confirm':
187//$progress.hide();
188$upload.text( '开始上传' ).addClass( 'disabled' );
189
190stats = uploader.getStats();
191if( stats.successNum && !stats.uploadFailNum ) {
192setState( 'finish' );
193return;
194}
195break;
196case'finish':
197stats = uploader.getStats();
198if( stats.successNum ) {
199alert( '上传成功' );
200}else{
201//没有成功的图片,重设
202state = 'done';
203location.reload();
204}
205break;
206}
207}
208
209
210//当有文件添加进来的时候
211uploader.on( 'fileQueued',function( file ) {
212fileCount++;
213fileSize += file.size;
214$("#upload_num").text(fileCount);
215$("#upload_size").text(convert_size(fileSize));
216md5Obj[ file.id ] = '';
217//获取文件MD5 值
218uploader.md5File( file )
219//及时显示进度
220.progress(function(percentage) {
221$( '#'+file.id ).find('.col4').text('读取文件'+parseInt(percentage*100)+"%");
222})
223//完成
224.then(function(val) {
225console.log('md5 result:', val);
226md5Obj[ file.id ] = val;
227$( '#'+file.id ).find('.col4').text('待上传');
228setState( 'ready' );
229});
230addFile( file );
231});
232
233//删除文件
234uploader.onFileDequeued =function( file ) {
235fileCount--;
236fileSize -= file.size;
237$("#upload_num").text(fileCount);
238$("#upload_size").text(convert_size(fileSize));
239if( !fileCount ) {
240setState( 'pedding' );
241}
242removeFile( file );
243
244};
245
246//添加“添加文件”的按钮,
247uploader.addButton({
248id: '#btns__add2',
249label: ''
250});
251
252//文件上传过程中创建进度实时显示。
253uploader.onUploadProgress =function( file, percentage ) {
254var$tr = $('#'+file.id),
255$percent = $tr.find('td.col5'),
256$state = $tr.find('td.col4');
257percentage = parseInt(percentage*100);
258if(! (percentage == 0 && percentage == 100)){
259$state.text("正在上传");
260}
261$percent.text( percentage + "%")
262percentages[ file.id ][ 1 ] = percentage;
263};
264
265//上传前,请求服务端 判断文件是否已经上传过
266uploader.on( 'uploadStart',function( file ) {
267vartype = 'POST';
268varurl = '/service/upload/determine_video_exist';
269varrequest_data = {
270'md5': md5Obj[ file.id ],
271'type':1
272};
273varsuccess =function(r) {
274uploader.upload( file );
275console.log(r);
276if(r.code == 1) {
277uploader.skipFile( file );
278$( '#'+file.id ).find('.col4').text('视频已存在');
279$( '#'+file.id ).find('.col5').text('100%');
280$('#'+file.id).find('.view').find('a').attr('href',playmain +'/?video_id='+ r.data.video_id);
281$('.pop2 .video_game').text("所在游戏:"+r.data.game_name);
282$('.pop2 .create_time').text("上传时间:"+r.data.create_time);
283$('.pop').hide();
284$('.pop2').show();
285$('.popup').show();
286}elseif(r.code <= 0) {
287showError(r.msg);
288}else{
289
290}
291};
292request(type, url, request_data, success);
293});
294
295//当某个文件的分块在发送前触发,主要用来询问是否要添加附带参数,大文件在开起分片上传的前提下此事件可能会触发多次。
296uploader.on('uploadBeforeSend',function(obj, data, headers) {
297$tr = $("#"+data.id);
298varfile_name = $tr.find(".name").val();
299varclass_id = $tr.find("select.class_id").val();
300varreg = /[1-9][0-9]*/g;
301data.md5 = md5Obj[ obj.file.id ];
302data.file_name = file_name;
303data.class_id = class_id;
304data.guid = data.guid + data.id.replace(/[^0-9]+/g, '');
305});
306
307uploader.on( 'uploadSuccess',function( file ,res) {
308if(res.code == 1){
309$( '#'+file.id ).find('.col4').text('成功上传');
310console.log(res);
311$('#'+file.id).find('.view').find('a').attr('href',playmain +'/?video_id='+ res.data.video_id);
312}elseif(res.code == 2) {
313$( '#'+file.id ).find('.col4').text('视频已存在');
314console.log(res);
315$('#'+file.id).find('.view').find('a').attr('href',playmain +'/?video_id='+ res.data.video_id);
316}else{
317showError(res.msg);
318}
319});
320
321uploader.on( 'uploadError',function( file,reason ) {
322$( '#'+file.id ).find('.col4').text('上传失败');
323console.log(reason);
324});
325
326uploader.on( 'uploadComplete',function( file ) {
327$( '#'+file.id ).find('.progress').fadeOut();
328});
329
330uploader.on( 'all',function( type ) {
331if( type === 'startUpload' ) {
332state = 'uploading';
333}elseif( type === 'stopUpload' ) {
334state = 'paused';
335}elseif( type === 'uploadFinished' ) {
336state = 'done';
337}
338
339if( state === 'uploading' ) {
340window.is_confirm =true;
341$('.toBeUploaded').addClass("uploaded").removeClass("toBeUploaded");
342$('input.name').attr("disabled","disabled");
343$('input.col1__checkBox').hide();
344$('input').attr("disabled","disabled");
345$('select.class_id').attr("disabled","disabled");
346$('.btns__add').remove();
347$upload.addClass("btns__upload-ing").removeClass("btns__upload-start").html('<i class="btns__upload_icon"></i>正在上传视频');
348
349}elseif(state === 'done') {
350window.is_confirm =false;
351console.log("上传完成");
352$upload.addClass("btns__upload-start btns__upload-refresh").removeClass("btns__upload-ing").html('<i class="btns__upload_icon"></i>开始上传视频');
353}
354});
355
356/**
357*验证文件格式以及文件大小
358*/
359uploader.on("error",function(type){
360varmsg = ''
361switch(type){
362case"Q_TYPE_DENIED": msg = "请上传mp4格式文件";break;
363case"F_EXCEED_SIZE": msg = "文件大小不能超过1G";break;
364case"Q_EXCEED_NUM_LIMIT" : msg = "一次最多能上传10个文件";break;
365default: msg='';
366}
367if(msg != ''){
368showError(msg);
369}
370});
371
372$part_btn.on('click',function(){
373$('td .col1__checkBox').each(function(){
374if($(this).is(':checked')){
375var$tr = $(this).parents('tr');
376varid = $tr.attr('id');
377uploader.removeFile( id );
378}
379});
380});
381$upload.on('click',function() {
382varisbreak =false;
383$(".name").each(function(){
384if(!$(this).val()|| $(this).val() == ''){
385isbreak =true;
386}
387})
388if(isbreak){
389showError("文件名不能存在为空");
390return;
391}
392$("select.class_id").each(function(){
393if(!$(this).val()|| $(this).val() == ''){
394isbreak =true;
395}
396})
397if(isbreak){
398showError("分类不能为空,请先添加分类");
399return;
400}
401if( $(this).hasClass( 'btns__upload-refresh' ) ) {
402location.reload();
403}
404if( $(this).hasClass( 'btns__upload-ing' ) ) {
405returnfalse;
406}
407varmd5Ready =true;
408$.each(md5Obj,function(index,item){
409if(!item || item==''){
410md5Ready =false;
411}
412});
413if(!md5Ready){
414showError('文件尚未读取完成,请耐心等待');
415returnfalse;
416}
417if( state === 'ready' && md5Ready ) {
418uploader.upload();
419}elseif( state === 'paused' ) {
420uploader.upload();
421}elseif( state === 'uploading' ) {
422uploader.stop();
423}
424});
425$upload.addClass( 'state-' + state );
426
427});
后台(PHP)【仅分片上传相关代码】
1publicfunctionaction_upload_file(){
2$file_id= R::string('file_id', 'file');
3$keepFileName= R::string('keepFileName', 0);
4$unsize_change= R::numeric('unsize_change',0);
5$id= R::string('id');//插件每上传一个视频自带id
6$guid= R::string('guid');//标识视频
7$chunks= R::numeric('chunks');//分片数
8$chunk= R::numeric('chunk');//分片号
9$file_name= R::string('file_name');
10$file=isset($_FILES[$file_id])?$_FILES[$file_id]:'';
11$md5= R::string('md5');
12$this->upload =newCommon_Upload();
13
14if(empty($guid) ||empty($file_name) ||empty($md5)){
15$this->response_msg(-1, 'guid或 file_name 或 md5 不能为空');
16return;
17}
18
19if(empty($file['name'])){
20$this->response_msg(-1, '请上传一个文件');
21return;
22}else{
23if($chunks){
24$res=$this->upload->saveFile_chunks($file,$chunks,$chunk,$guid);
25if(empty($res)){
26$this->response_msg(-2, '分片上传失败');
27return;
28}
29
30}elseif($keepFileName){
31$res=$this->upload->saveFile_nochunks($file, '', '',$keepFileName);
32}else{
33$res=$this->upload->saveFile_nochunks($file);
34}
35if(empty($res)){
36$err=$this->upload->getError();
37$this->response_msg(-3, '上传文件出错!msg:'.print_r($err,true));
38return;
39}
40if($unsize_change){
41$size=$res['size'];
42}else{
43$size=$this->convert_size($res['size']);
44}
45
46//视频上传完成
47if($chunks&&$res['last_chunk']){
48$domain= Kohana::$config->load('domain');
49$video_domain=$domain[RUN_MOD]['VIDEO'];
50
51if(!empty($file_name)){
52$res['name'] =$file_name;
53}
54$video_data=array(
55'video_name'=>$file_name,
56'video_url'=>$video_domain."/".$res['path'],
57'size'=>$res['size'],
58'create_time'=>date('y-m-d H:i:s',time()),
59'update_time'=>date('y-m-d H:i:s',time()),
60'duration'=>$res['time'],
61'md5'=>$md5
62);
63$video_mod=newModel_Videoinfo();
64$video=$video_mod->save_video($video_data,$guid);
65$res=array(
66'path'=>$res['path'],
67'chunks'=>$chunks,
68'chunk'=>$chunk,
69'size'=>$size,
70'guid'=>$guid,
71'video_id'=>$video[0],
72'file'=>$file,
73'id'=>$id
74);
75$this->response_msg(1,'视频上传成功',$res);
76return;
77}
78//非分片上传
79if(!$chunks){
80$domain= Kohana::$config->load('domain');
81$video_domain=$domain[RUN_MOD]['VIDEO'];
82if(!empty($file_name)){
83$res['name'] =$file_name;
84}
85$video_data=array(
86'video_name'=>$file_name,
87'video_url'=>$video_domain."/".$res['path'],
88'size'=>$res['size'],
89'create_time'=>date('y-m-d H:i:s',time()),
90'update_time'=>date('y-m-d H:i:s',time()),
91'duration'=>$res['time'],
92'md5'=>$md5
93);
94$video_mod=newModel_Videoinfo();
95$video=$video_mod->save_video($video_data,$guid);
96if(empty($video)){
97$this->response_msg(-6, '视频信息保存失败');
98return;
99}
100$res=array(
101'path'=>$res['path'],
102'video_data'=>$video_data,
103'size'=>$size,
104'guid'=>$guid,
105'video_id'=>$video[0],
106'file'=>$file,
107'id'=>$id
108);
109$this->response_msg(1,'视频上传成功',$res);
110return;
111}
112//分片上传成功(未全部分片上传完成)
113$res=array(
114'chunks'=>$chunks,
115'chunk'=>$chunk,
116);
117$this->response_msg(2, '分片上传成功',$res);
118}
119}
1/**
2*保存分片文件(注意先验证文件是否合法)
3*
4* @param array $file单个文件
5* @param string $attachdir上传文件路径
6* @param string $upload_type上传文件类型
7* @param bool $keepFileName是否保持文件名,默认不不保持
8* @return bool
9*/
10publicfunctionsaveFile_chunks($file,$chunks,$chunk,$guid)
11{
12if(empty($guid) ||empty($file) ){
13returnfalse;
14}
15$file_name= (string)$guid.$chunk;
16//保存分片文件
17$file_info=$this->saveFile($file, '', '',false,$file_name,true);
18if($file_info) {
19$cache= Cache::instance('memcache');
20//记录已上传的分片编号,上传顺序并不是按编号顺序进行上传
21$chunks_list_pre=$cache->get($guid);
22if(empty($chunks_list_pre)){
23$strarr=array();
24for($i=0;$i<$chunks;$i++){
25$strarr[] =$guid.$i;
26}
27$cache->set($guid,$strarr,60 * 60 * 24);
28}
29$file_path=$cache->set($guid.$chunk,$file_info['path'],60 * 60 * 24);
30
31$chunk_path_array=array();
32for($i=0;$i<$chunks;$i++){
33if($cache->get($guid.$i)){
34$chunk_path_array[$i] =$cache->get($guid.$i);
35}
36}
37list($Y,$M,$D,$H,$I,$S) =explode('-',date("Y-m-d-H-i-s",time()));
38$file_info['chunks_path_count'] =count($chunk_path_array);
39$file_info['last_chunk'] =false;
40if(count($chunk_path_array) ==$chunks) {
41//按目录类型存储
42$dirType=substr($file_info['type'], 1,strlen($file_info['type']));;
43//目录类型前面加上前缀url
44$dirType=$this->pre_url.$dirType;
45//按年月二级存储
46$month_file_path=$Y.'/'.$M;
47$saveName='upload/mp4/'.$month_file_path.'/original/'.$guid.$file_info['type'];
48$join_file_name=$this->attachDIR.$saveName;
49if(!is_dir($this->attachDIR.'upload/mp4/'.$month_file_path.'/original/')){
50mkdir($this->attachDIR.'upload/mp4/'.$month_file_path.'/original/',0755,true);
51}
52if(!file_exists($join_file_name)){
53$fp=fopen($join_file_name, "ab");
54//合并过程中对文件加锁,防止同时操作而出错
55if(flock($fp,LOCK_EX)){
56for($i= 0;$i<$chunks;$i++) {
57$tmp_file=$this->attachDIR .$chunk_path_array[$i];
58$handle=fopen($tmp_file, "rb");
59fwrite($fp,fread($handle,filesize($tmp_file)));
60fclose($handle);
61unset($handle);
62unlink($tmp_file);//合并完毕的文件就删除
63}//组装分片
64$cache->delete($guid);
65for($i=0;$i<$chunks;$i++){
66$cache->delete($guid.$i);
67}
68$time=$this->getTime($join_file_name,$file_info['type']);
69$file_info['time'] =$time;
70$file_info['path'] =$saveName;
71$file_info['size'] =filesize($join_file_name);
72$file_info['last_chunk'] =true;
73
74$model_mod=newModel_Base();
75$model_mod->disconnect();
76$pid= pcntl_fork();
77//父进程和子进程都会执行下面代码
78if($pid== -1) {
79//错误处理:创建子进程失败时返回-1.
80die('could not fork');
81}elseif($pid) {
82$model_mod->connect();
83//对上传完成的视频进行排队转码
84$this->thread($join_file_name,$file_info['type'],$guid);
85//父进程会得到子进程号,所以这里是父进程执行的逻辑
86pcntl_wait($status);//等待子进程中断,防止子进程成为僵尸进程。
87}else{
88return$file_info;
89//子进程得到的$pid为0, 所以这里是子进程执行的逻辑。
90}
91
92}
93}
94
95}
96return$file_info;
97}else{
98$this->error[] = '分片上传失败';
99returnfalse;
100}
101/*}}}*/
102}
1,实现了分片上传;
2,同时在上传前检查视频md5 是否在库,如已存在可实现“秒传” 功能,即直接复制数据信息,指向同一个文件,不必再上传;
3,可实现断点续传,上传过程中中断;之前上传的分片已保留在服务器,只需重新上传尚未上传的分片即可;
参考文章:/wordpress//08/12/java-http%E5%A4%A7%E6%96%87%E4%BB%B6%E6%96%AD%E7%82%B9%E7%BB%AD%E4%BC%A0%E4%B8%8A%E4%BC%A0/