目前正在做一个视频相关的项目,里面有个需求是:安卓手机端需要随时可以录制视频,时间可能是几分钟或者几个小时,然后录制的适配需要传到服务器上。如何录制这里暂时不说,我们主要研究一个如何上传的问题。
按照用户的需求,视频的分辨率要达到720p,最大码率设定为2.5Mbps,这样一分钟的大小大概是20MB左右,一个小时在1200MB。如果直接上传1200M的文件,肯定会存在:
针对上面的问题,我们提出了响应的解决对策:
对于5分钟录制一个文件,在安卓端启动一个定时器即可。每5分钟执行一次录制停止,指定新文件名,然后重新启动录制的操作。
安卓端上传的核心代码:
while (
chunck <= chuncks
&&uploadStatus!= UploadStatus.UPLOAD_STATUS_PAUSE
&&uploadStatus!= UploadStatus.UPLOAD_STATUS_ERROR)
{
uploadStatus = UploadStatus.UPLOAD_STATUS_UPLOADING;
//分块读取并传输,避免一次性读入的内存开销。
final byte[] mBlock = FileUtils.getBlock((chunck - 1) * blockLength, file, blockLength);
//对数据做md5校验,如果服务器收到数据的md5和终端的不一致,这个数据需要重新传输。
String md5 = MD5Utils.getMD5String(mBlock);
Map
params.put("name", file.getName());//fileName
params.put("chunks", chuncks + "");
params.put("chunk", chunck + "");
params.put("filelength", file.length() + "");//文件的总大小,服务器校验大小是否一致
params.put("md5str", md5);
params.put("debugstr", debugstr);
MultipartBody.Builder builder = new MultipartBody.Builder()
.setType(MultipartBody.FORM);
addParams(builder, params);
RequestBody requestBody = RequestBody.create(MEDIA_TYPE_MARKDOWN, mBlock);
builder.addFormDataPart("mFile", file.getName(), requestBody);//filename
Log.i("onUploadSuccessurl",url);
Request request = new Request.Builder()
.url(url)
.post(builder.build())
.build();
Response response = null;
response = mClient.newCall(request).execute();
服务器端收到数据后,先校验md5值是否一致,不一致的话,会给终端反馈失败的标识位。如检测到当前文件以及传输完毕,就会把收到的所有分段合并成一个文件。核心代码如下:
//计算md5是否一致
String server_md5 = MD5Utils.getFileMD5String(savedFile);
Logger.info("MD5校验:终端"+md5str+">>>服务器"+server_md5+" "+(server_md5.equals(md5str)));
if (md5str!=null && md5str.length()>0 && server_md5.equals(md5str)==false) {
throw new Exception("收到数据的md5校验失败");
}
//文件传输完毕
if (schunk != null && schunk.intValue() == schunks.intValue()) {
List
//写临时文件,如果直接写最终的文件,那处理失败的时候,就会导致正常文件也错误了。
String tmpFile = newFileName+"_temp";
outputStream = new BufferedOutputStream(new FileOutputStream(new File(tmp, tmpFile)));
// 遍历文件合并
for (int i = 1; i <= schunks; i++) {
File partFile = new File(tmp, i + "_" + name);
byte[] bytes = FileUtils.readFileToByteArray(partFile);
System.out.println("文件合并:" + i + "/" + schunks+"..."+bytes.length);
outputStream.write(bytes);
outputStream.flush();
toDelete.add(partFile);
try {
//确保缓存写入
Thread.sleep(10);
}catch(Exception e) {}
}
outputStream.flush();
try {
outputStream.close();//关闭流
}catch(Exception ee) {}
try {
//确保缓存写入
Thread.sleep(50);
}catch(Exception e) {}
File _file = new File(tmp, tmpFile);
System.out.println("生成的文件长度"+_file.length()+", 终端反馈的长度:"+fileLength);
//判断长度是否一致,不一致的话,需要重新上传
if (fileLength>0 && _file.length()!=fileLength) {
System.out.println("生成的文件长度和终端反馈的不一致,终端"+fileLength+",服务器"+_file.length());
}
//判断原来是否有
File dst = new File(tmp, newFileName);
if (dst.exists()==false) {
_file.renameTo(dst);
System.out.println("重命名到最终的文件"+dst.getName()+"---"+dst.length());
}else {
//用更大的文件,存在一边传一边写的情况
if (_file.length()>dst.length()) {
_file.renameTo(dst);
System.out.println("将较大的文件重命名到最终的文件"+dst.getName()+"---"+dst.length());
}else {
//比已有的还小就不用处理了
System.out.println("文件大小一致,不处理"+dst.getName()+"---"+dst.length());
}
}
//删除分片文件
for (File file : toDelete) {
//不能因为个别文件删除失败导致所有文件不删除
try {
System.out.println("删除"+file.getName()+">>>"+file.length());
file.delete();
}catch(Exception eee) {}
}
response.getWriter().write("{\"status\":true,\"url\":\"" + getUrl(dst) + "\"}");
}else{
response.getWriter().write("{\"status\":true,\"newName\":\"" + newFileName + "\"}");
}
通过上面的机制,基本可以保证终端录制的大文件,能及时、准确的上传到服务器上。当然在实际调试过程中,也遇到了不少问题: