最近在做一个集富媒体功能于一身的项目。需要上传视频。这里我希望做成异步上传,并且有进度条,响应有状态码,视频连接,缩略图。
服务端响应
1 { 2 "thumbnail": "/slsxpt//upload/thumbnail/6f05d4985598160c548e6e8f537247c8.jpg", 3 "success": true, 4 "link": "/slsxpt//upload/video/6f05d4985598160c548e6e8f537247c8.mp4" 5 }
并且希望我的input file控件不要被form标签包裹。原因是form中不能嵌套form,另外form标签在浏览器了还是有一点点默认样式的,搞不好又要写css。
以前用ajaxFileUpload做过文件异步上传。不过这个东西好久未更新,代码还有bug,虽然最后勉强成功用上了,但总觉不好。而且ajaxFileUpload没有直接添加xhr2的progress事件响应,比较麻烦。
上网找了一下,发现方法都是很多。
比如在文件上传后,将上传进度放到session中,轮询服务器session。但我总觉的这个方法有问题,我认为这种方法看到的进度,应该是我的服务端应用程序代码(我的也就是action)从服务器的临时目录复制文件的进度,因为所有请求都应该先提交给服务器软件,也就是tomcat,tomcat对请求进行封装session,request等对象,并且文件实际上也应该是它来接收的。也就是说在我的action代码执行之前,文件实际上已经上传完毕了。
后来找到个比较好的方法使用 jquery.form.js插件的ajaxSubmit方法。这个方法以表单来提交,也就是 $.fn.ajaxSubmit.:$(form selector).ajaxSubmit({}),这个api的好处是它已经对xhr2的progress时间进行了处理,可以在调用时传递一个uploadProgress的function,在function里就能够拿到进度。而且如果不想input file被form包裹也没关系,在代码里createElement应该可以。不过这个方法我因为犯了个小错误最后没有成功,可惜了。
ajaxSubmit源码
最后,还是使用了$.ajax 方法来做。$.ajax 不需要关联form,有点像个静态方法哦。唯一的遗憾就是$.ajax options里没有对progress的响应。不过它有一个参数为 xhr ,也就是你可以定制xhr,那么久可以通过xhr添加progress的事件处理程序。再结合看一看ajaxSubmit方法里对progress事件的处理,顿时豁然开朗
那么我也可以在$.ajax 方法中添加progress事件处理函数了。为了把对dom的操作从上传业务中抽取出来,我决定以插件的形式写。下面是插件的代码
1 ;(function ($) { 2 var defaults = { 3 uploadProgress : null, 4 beforeSend : null, 5 success : null, 6 }, 7 setting = { 8 9 }; 10 11 var upload = function($this){ 12 $this.parent().on('change',$this,function(event){ 13 //var $this = $(event.target), 14 var formData = new FormData(), 15 target = event.target || event.srcElement; 16 //$.each(target.files, function(key, value) 17 //{ 18 // console.log(key); 19 // formData.append(key, value); 20 //}); 21 formData.append('file',target.files[0]); 22 settings.fileType && formData.append('fileType',settings.fileType); 23 $.ajax({ 24 url : $this.data('url'), 25 type : "POST", 26 data : formData, 27 dataType : 'json', 28 processData : false, 29 contentType : false, 30 cache : false, 31 beforeSend : function(){ 32 //console.log('start'); 33 if(settings.beforeSend){ 34 settings.beforeSend(); 35 } 36 }, 37 xhr : function() { 38 var xhr = $.ajaxSettings.xhr(); 39 if(xhr.upload){ 40 xhr.upload.addEventListener('progress',function(event){ 41 var total = event.total, 42 position = event.loaded || event.position, 43 percent = 0; 44 if(event.lengthComputable){ 45 percent = Math.ceil(position / total * 100); 46 } 47 if(settings.uploadProgress){ 48 settings.uploadProgress(event, position, total, percent); 49 } 50 51 }, false); 52 } 53 return xhr; 54 }, 55 success : function(data,status,jXhr){ 56 if(settings.success){ 57 settings.success(data); 58 } 59 }, 60 error : function(jXhr,status,error){ 61 if(settings.error){ 62 settings.error(jXhr,status,error); 63 } 64 } 65 }); 66 }); 67 68 }; 69 $.fn.uploadFile = function (options) { 70 settings = $.extend({}, defaults, options); 71 // 文件上传 72 return this.each(function(){ 73 upload($(this)); 74 }); 75 76 77 } 78 })($ || jQuery);
下面就可以在我的jsp页面里面使用这个api了。
1 <div class="col-sm-5"> 2 <input type="text" name="resource_url" id="resource_url" hidden="hidden"/> 3 <div class="progress" style='display: none;'> 4 <div class="progress-bar progress-bar-success uploadVideoProgress" role="progressbar" 5 aria-valuenow="0" aria-valuemin="0" aria-valuemax="100" style="width: 0%"> 6 7 </div> 8 </div> 9 <input type="file" class="form-control file2 inline btn btn-primary uploadInput uploadVideo" 10 accept="video/mp4" 11 data-url="${baseUrl}/upload-video.action" 12 data-label="<i class='glyphicon glyphicon-circle-arrow-up'></i> 选择文件" /> 13 <script> 14 (function($){ 15 $(document).ready(function(){ 16 var $progress = $('.uploadVideoProgress'), 17 start = false; 18 $('input.uploadInput.uploadVideo').uploadFile({ 19 beforeSend : function(){ 20 $progress.parent().show(); 21 }, 22 uploadProgress : function(event, position, total, percent){ 23 $progress.attr('aria-valuenow',percent); 24 $progress.width(percent+'%'); 25 if(percent >= 100){ 26 $progress.parent().hide(); 27 $progress.attr('aria-valuenow',0); 28 $progress.width(0+'%'); 29 } 30 }, 31 success : function(data){ 32 if(data.success){ 33 setTimeout(function(){ 34 $('#thumbnail').attr('src',data.thumbnail); 35 },800); 36 } 37 } 38 }); 39 }); 40 })(jQuery); 41 </script> 42 </div>
这里在响应succes的时候设置超时800毫秒之后获取图片,因为提取缩量图是另一个进程在做可能响应完成的时候缩略图还没提取完成
看下效果
提取缩量图
下面部分就是服务端处理上传,并且对视频提取缩量图下面是action的处理代码
1 package org.lyh.app.actions; 2 3 import org.apache.commons.io.FileUtils; 4 import org.apache.struts2.ServletActionContext; 5 import org.lyh.app.base.BaseAction; 6 import org.lyh.library.SiteHelpers; 7 import org.lyh.library.VideoUtils; 8 9 import java.io.File; 10 import java.io.IOException; 11 import java.security.KeyStore; 12 import java.util.HashMap; 13 import java.util.Map; 14 15 /** 16 * Created by admin on 2015/7/2. 17 */ 18 public class UploadAction extends BaseAction{ 19 private String saveBasePath; 20 private String imagePath; 21 private String videoPath; 22 private String audioPath; 23 private String thumbnailPath; 24 25 private File file; 26 private String fileFileName; 27 private String fileContentType; 28 29 // 省略setter getter方法 30 31 public String video() { 32 Map<String, Object> dataJson = new HashMap<String, Object>(); 33 System.out.println(file); 34 System.out.println(fileFileName); 35 System.out.println(fileContentType); 36 String fileExtend = fileFileName.substring(fileFileName.lastIndexOf(".")); 37 String newFileName = SiteHelpers.md5(fileFileName + file.getTotalSpace()); 38 String typeDir = "normal"; 39 String thumbnailName = null,thumbnailFile = null; 40 boolean needThumb = false,extractOk = false; 41 if (fileContentType.contains("video")) { 42 typeDir = videoPath; 43 // 提取缩量图 44 needThumb = true; 45 thumbnailName = newFileName + ".jpg"; 46 thumbnailFile 47 = app.getRealPath(saveBasePath + thumbnailPath) + "/" + thumbnailName; 48 } 49 String realPath = app.getRealPath(saveBasePath + typeDir); 50 File saveFile = new File(realPath, newFileName + fileExtend); 51 // 存在同名文件,跳过 52 if (!saveFile.exists()) { 53 if (!saveFile.getParentFile().exists()) { 54 saveFile.getParentFile().mkdirs(); 55 } 56 try { 57 FileUtils.copyFile(file, saveFile); 58 if(needThumb){ 59 extractOk = VideoUtils.extractThumbnail(saveFile, thumbnailFile); 60 System.out.println("提取缩略图成功:"+extractOk); 61 } 62 dataJson.put("success", true); 63 } catch (IOException e) { 64 System.out.println(e.getMessage()); 65 dataJson.put("success", false); 66 } 67 }else{ 68 dataJson.put("success", true); 69 } 70 if((Boolean)dataJson.get("success")){ 71 dataJson.put("link", 72 app.getContextPath() + "/" + saveBasePath + typeDir + "/" + newFileName + fileExtend); 73 if(needThumb){ 74 dataJson.put("thumbnail", 75 app.getContextPath() + "/" + saveBasePath + thumbnailPath + "/" + thumbnailName); 76 } 77 } 78 this.responceJson(dataJson); 79 return NONE; 80 } 81 82 }
action配置
1 <action name="upload-*" class="uploadAction" method="{1}"> 2 <param name="saveBasePath">/upload</param> 3 <param name="imagePath">/images</param> 4 <param name="videoPath">/video</param> 5 <param name="audioPath">/audio</param> 6 <param name="thumbnailPath">/thumbnail</param> 7 </action>
这里个人认为,如果文件的名称跟大小完全一样的话,它们是一个文件的概率就非常大了,所以我这里取文件名跟文件大小做md5运算,应该可以稍微避免下重复上传相同文件了。
转码的时候用到FFmpeg。需要的可以去这里下载。
1 package org.lyh.library; 2 3 import java.io.File; 4 import java.io.IOException; 5 import java.io.InputStream; 6 import java.util.ArrayList; 7 import java.util.List; 8 9 /** 10 * Created by admin on 2015/7/15. 11 */ 12 public class VideoUtils { 13 public static final String FFMPEG_EXECUTOR = "C:/Software/ffmpeg.exe"; 14 public static final int THUMBNAIL_WIDTH = 400; 15 public static final int THUMBNAIL_HEIGHT = 300; 16 17 public static boolean extractThumbnail(File inputFile,String thumbnailOutput){ 18 List<String> command = new ArrayList<String>(); 19 File ffmpegExe = new File(FFMPEG_EXECUTOR); 20 if(!ffmpegExe.exists()){ 21 System.out.println("转码工具不存在"); 22 return false; 23 } 24 25 System.out.println(ffmpegExe.getAbsolutePath()); 26 System.out.println(inputFile.getAbsolutePath()); 27 command.add(ffmpegExe.getAbsolutePath()); 28 command.add("-i"); 29 command.add(inputFile.getAbsolutePath()); 30 command.add("-y"); 31 command.add("-f"); 32 command.add("image2"); 33 command.add("-ss"); 34 command.add("10"); 35 command.add("-t"); 36 command.add("0.001"); 37 command.add("-s"); 38 command.add(THUMBNAIL_WIDTH+"*"+THUMBNAIL_HEIGHT); 39 command.add(thumbnailOutput); 40 41 ProcessBuilder builder = new ProcessBuilder(); 42 builder.command(command); 43 builder.redirectErrorStream(true); 44 try { 45 long startTime = System.currentTimeMillis(); 46 Process process = builder.start(); 47 System.out.println("启动耗时"+(System.currentTimeMillis()-startTime)); 48 return true; 49 } catch (IOException e) { 50 e.printStackTrace(); 51 return false; 52 } 53 } 54 55 }
另外这里由java启动了另外一个进程,在我看来他们应该是互不相干的,java启动了ffmpeg.exe之后,应该回来继续执行下面的代码,所以并不需要单独起一个线程去提取缩量图。测试看也发现耗时不多。每次长传耗时也区别不大,下面是两次上传同一个文件耗时
第一次
第二次
就用户体验来说没有很大的区别。
另外这里上传较大文件需要对tomcat和struct做点配置
修改tomcat下conf目录下的server.xml文件,为Connector节点添加属性 maxPostSize="0"表示不显示上传大小
另外修改 struts.xml添加配置,这里的value单位为字节,这里大概300多mb
<constant name="struts.multipart.maxSize" value="396014978"/>