最近在做一個集富媒體功能於一身的項目。需要上傳視頻。這里我希望做成異步上傳,並且有進度條,響應有狀態碼,視頻連接,縮略圖。
服務端響應
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"/>