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