導語
使用MediaElement.js,在WEB上可以播放MP4文件,功能強大当船,定制性強,支持拖移播放默辨。若文件在工程目錄下德频,則可以直接使用tomcat的DefaultServlet來處理,則MediaElement.js可以完美播放缩幸,若是自定義的壹置,則繼續(xù)往下看。
原理
播放器是采用分塊請求下載的表谊,斷點續(xù)傳的方式钞护,其原理如下:
http協(xié)議中,服務端實現(xiàn)斷點續(xù)傳首先需要讀取客戶端傳送的Range頭信息爆办,比如“Range: bytes=12583394-”這個就是指原來正在下載的文件需要從第12583394字節(jié)繼續(xù)下載难咕,然后我們利用Java.io.File的skip方法,舍棄掉原文件的前n個字節(jié),接著就繼續(xù)慢慢write吧余佃。暮刃。。
但是客戶端又是如何判斷服務端是否支持斷點續(xù)傳的呢爆土?主要就是Accept-Ranges和Content-Length頭信息椭懊。比如“Accept-Ranges:bytes”和“Content-Length:99999999”。有了這兩個頭信息步势,客戶端就認為服務端是支持斷點續(xù)傳的了氧猬。
然后需要注意的是,假如客戶端剛才由于某些原因坏瘩,暫停了下載狂窑,現(xiàn)在恢復的時候,就會如前所述桑腮,傳來Range頭信息泉哈,這時候,我們的response就需要設置一下狀態(tài)碼破讨,這里應該設置成206(詳細解釋請看http://en.wikipedia.org/wiki/List_of_HTTP_status_codes)丛晦,還有就是Content-Range頭信息,格式為“bytes x-(y-1)/y”提陶,x就是客戶端傳來的開始字節(jié)位置烫沙,y就是文件長度。
實現(xiàn)
1.JSP頁面
<%@ page language="java" import="java.util.*" pageEncoding="UTF-8"%>
<%
String path = request.getContextPath();
String basePath = request.getScheme() + "://"
+ request.getServerName() + ":" + request.getServerPort()
+ path + "/";
%>
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<html>
<head>
<link rel="stylesheet" href="vender/mediaelement/mediaelementplayer.css">
</head>
<body style="width: 100%; height: 100%;">
<div class=warp">
<div style="text-align: center;margin-top: 10px">
<h2>${videoTitle}</h2>
</div>
<div style="margin: 0 auto;width: 1280px" data-options="border:false">
<video src="${videoUrl}" width="1280" height="720"
data-mejsoptions='{"stretching" : "auto","pluginPath" : "vender/mediaelement/", "alwaysShowControls": "true", "lang" : "zh-cn"}'></video>
</div>
</div>
<script src="vender/jquery.js"></script>
<script src="vender/mediaelement/mediaelement-and-player.js"></script>
<script src="vender/mediaelement/lang/zh-cn.js"></script>
<script type="text/javascript">
// 設置語言為中文
mejs.i18n.language('zh-CN');
// 創(chuàng)建播放器
$('video').mediaelementplayer({
stretching : "auto",
pluginPath : "vender/mediaelement/",
alwaysShowControls : true,
success : function(player, node) {
player.play();
}
});
</script>
</body>
</html>
2.Spring MVC的Controller
/**
* 大文件分塊下載
*
* @param request
* @param response
* @throws IOException
*/
@RequestMapping("/storage/**")
public void bigFileDownload(HttpServletRequest request,
HttpServletResponse response) throws IOException {
String uri = URLDecoder.decode(request.getRequestURI(), "UTF-8");
String filename = uri.substring(uri.indexOf(STORAGE) + STORAGE.length()
+ 1, uri.length());
File downloadFile = fileService.getFile(filename); // 要下載的文件
long fileLength = downloadFile.length();// 記錄文件大小
long pastLength = 0;// 記錄已下載文件大小
long toLength = 0;// 記錄客戶端需要下載的字節(jié)段的最后一個字節(jié)偏移量(比如bytes=27000-39000隙笆,則這個值是為39000)
long contentLength = 0;// 客戶端請求的字節(jié)總量
String rangeBytes = "";// 記錄客戶端傳來的形如“bytes=27000-”或者“bytes=27000-39000”的內(nèi)容
// ETag header
// The ETag is contentLength + lastModified
response.setHeader("ETag",
"W/\"" + fileLength + "-" + downloadFile.lastModified() + "\"");
// Last-Modified header
response.setHeader("Last-Modified",
new Date(downloadFile.lastModified()).toString());
if (request.getHeader("Range") != null) {// 客戶端請求的下載的文件塊的開始字節(jié)
response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT);
log.info("request.getHeader(\"Range\")="
+ request.getHeader("Range"));
rangeBytes = request.getHeader("Range").replaceAll("bytes=", "");
if (rangeBytes.indexOf('-') == rangeBytes.length() - 1) {// bytes=969998336-
rangeBytes = rangeBytes.substring(0, rangeBytes.indexOf('-'));
pastLength = Long.parseLong(rangeBytes.trim());
toLength = fileLength - 1;
} else {// bytes=1275856879-1275877358
String temp0 = rangeBytes.substring(0, rangeBytes.indexOf('-'));
String temp2 = rangeBytes.substring(
rangeBytes.indexOf('-') + 1, rangeBytes.length());
// bytes=1275856879-1275877358锌蓄,從第 1275856879個字節(jié)開始下載
pastLength = Long.parseLong(temp0.trim());
// bytes=1275856879-1275877358,到第 1275877358 個字節(jié)結束
toLength = Long.parseLong(temp2);
}
} else {// 從開始進行下載
toLength = fileLength - 1;
}
// 客戶端請求的是1275856879-1275877358 之間的字節(jié)
contentLength = toLength - pastLength + 1;
if (contentLength < Integer.MAX_VALUE) {
response.setContentLength((int) contentLength);
} else {
// Set the content-length as String to be able to use a long
response.setHeader("content-length", "" + contentLength);
}
WebApplicationContext webApplicationContext = ContextLoader
.getCurrentWebApplicationContext();
ServletContext servletContext = webApplicationContext
.getServletContext();
String contentType = servletContext.getMimeType(filename);
if (null != contentType) {
response.setContentType(contentType);
}
// 告訴客戶端允許斷點續(xù)傳多線程連接下載,響應的格式是:Accept-Ranges: bytes
response.setHeader("Accept-Ranges", "bytes");
// 必須先設置content-length再設置header
response.addHeader("Content-Range", "bytes " + pastLength + "-"
+ toLength + "/" + fileLength);
response.setBufferSize(2048);
InputStream istream = null;
OutputStream os = null;
try {
os = response.getOutputStream();
istream = new BufferedInputStream(
new FileInputStream(downloadFile), 2048);
try {
copyRange(istream, os, pastLength, toLength);
} catch (IOException ie) {
/**
* 在寫數(shù)據(jù)的時候撑柔, 對于 ClientAbortException 之類的異常瘸爽,
* 是因為客戶端取消了下載,而服務器端繼續(xù)向瀏覽器寫入數(shù)據(jù)時铅忿, 拋出這個異常剪决,這個是正常的。
* 尤其是對于迅雷這種吸血的客戶端軟件檀训, 明明已經(jīng)有一個線程在讀取 bytes=1275856879-1275877358柑潦,
* 如果短時間內(nèi)沒有讀取完畢,迅雷會再啟第二個峻凫、第三個渗鬼。。荧琼。線程來讀取相同的字節(jié)段譬胎, 直到有一個線程讀取完畢肛循,迅雷會 KILL
* 掉其他正在下載同一字節(jié)段的線程, 強行中止字節(jié)讀出银择,造成服務器拋 ClientAbortException。
* 所以累舷,我們忽略這種異常
*/
// ignore
}
} catch (Exception e) {
log.error(e.getMessage(), e);
} finally {
if (istream != null) {
try {
istream.close();
} catch (IOException e) {
log.error(e.getMessage(), e);
}
}
}
}
protected void copyRange(InputStream istream, OutputStream ostream,
long start, long end) throws IOException {
long skipped = 0;
skipped = istream.skip(start);
if (skipped < start) {
throw new IOException("skip fail: skipped=" + Long.valueOf(skipped)
+ ", start=" + Long.valueOf(start));
}
long bytesToRead = end - start + 1;
byte buffer[] = new byte[2048];
int len = buffer.length;
while ((bytesToRead > 0) && (len >= buffer.length)) {
try {
len = istream.read(buffer);
if (bytesToRead >= len) {
ostream.write(buffer, 0, len);
bytesToRead -= len;
} else {
ostream.write(buffer, 0, (int) bytesToRead);
bytesToRead = 0;
}
} catch (IOException e) {
len = -1;
throw e;
}
if (len < buffer.length)
break;
}
}