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