ajax 异步上传视频带进度条并提取缩略图

简介: 最近在做一个集富媒体功能于一身的项目。需要上传视频。这里我希望做成异步上传,并且有进度条,响应有状态码,视频连接,缩略图。 服务端响应 1 { 2 "thumbnail": "/slsxpt//upload/thumbnail/6f05d4985598160c548e6e8f537247c8.jpg", 3 "success": true, 4 "link": "/slsxpt//upload/video/6f05d4985598160c548e6e8f537247c8.mp4" 5 } 并且希望我的input file控件不要被form标签包裹。

最近在做一个集富媒体功能于一身的项目。需要上传视频。这里我希望做成异步上传,并且有进度条,响应有状态码,视频连接,缩略图。

服务端响应

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> &nbsp;选择文件" />
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"/>

 

目录
相关文章
|
6月前
|
前端开发 JavaScript Java
使用Ajax进行异步交互:提升Java Web应用的用户体验
【4月更文挑战第3天】Ajax技术在Web开发中提升UX,通过与服务器异步交互实现页面局部更新,无需完整刷新。核心组件包括XMLHttpRequest、JavaScript、HTML/CSS及服务器端脚本。在Java Web应用中,可使用原生JavaScript或框架如jQuery、AngularJS实现Ajax请求。Ajax减少页面刷新,实现实时数据更新,即时表单验证和动态UI,显著改善用户体验,是现代Web开发不可或缺的一部分。
85 0
|
6月前
|
前端开发 JavaScript API
Ajax技术的秘密揭秘:异步传输,高效交互
Ajax技术的秘密揭秘:异步传输,高效交互
|
3月前
|
前端开发 JavaScript Java
Ajax进行异步交互:提升Java Web应用的用户体验
Ajax 技术允许在不重载整个页面的情况下与服务器异步交换数据,通过局部更新页面内容,极大提升了 Java Web 应用的响应速度和用户体验。本文介绍 Ajax 的基本原理及其实现方式,包括使用 XMLHttpRequest 对象发送请求、处理响应数据,并在 Java Web 应用中集成 Ajax。此外,还探讨了 Ajax 如何通过减少页面刷新、实时数据更新等功能改善用户体验。
71 3
|
5月前
|
XML 移动开发 前端开发
JS设置Ajax为同步或异步
JS设置Ajax为同步或异步
66 0
|
5月前
1.ajax同步和异步区别 2.post和get区别
1.ajax同步和异步区别 2.post和get区别
31 0
|
6月前
|
XML JSON 前端开发
学习Ajax使用异步对象发送请求
Ajax,全称Asynchronous JavaScript and XML(异步JavaScript和XML),是一种用于创建更好、更快以及交互性更强的Web应用程序的技术。
68 3
|
6月前
|
JSON 前端开发 JavaScript
探秘 AJAX:让网页变得更智能的异步技术(下)
探秘 AJAX:让网页变得更智能的异步技术(下)
探秘 AJAX:让网页变得更智能的异步技术(下)
|
6月前
|
XML 前端开发 JavaScript
探秘 AJAX:让网页变得更智能的异步技术(上)
探秘 AJAX:让网页变得更智能的异步技术(上)
探秘 AJAX:让网页变得更智能的异步技术(上)
|
前端开发 Java
Ajax下载文件添加进度条教程
Ajax下载文件添加进度条教程
258 1
|
前端开发 JavaScript 关系型数据库
宝塔设置PHP定时任务实战记录(定时任务、ajax异步刷新API、shell脚本、访问url)
宝塔设置PHP定时任务实战记录(定时任务、ajax异步刷新API、shell脚本、访问url)
745 0