项目中需要用户选择本地视频,上传到服务器,而后调用算法返回剪辑后的结果。本周主要进行的工作就是与后端联通,实现视频文件的分块上传。因为视频文件通常很大,上传中很可能会出现由于网络或者其他一些原因上传中断失败的情况。
于是决定采用分块上传的策略:大文件切割成n块小文件,然后上传这些小文件,所有小文件全部上传成功后再在服务器上进行拼接。
基本思路为:
通过前后端沟通,首先确定好每一个文件块的大小(最终设定的是 512*1024)。文件分块这里,使用了 RandomAccessFile ,其支持“随机访问”的方式,程序可以直接跳转到文件的任意地方来读写数据
public static byte[] getBlock(long offset, File file, int blockSize) {
byte[] result = new byte[blockSize];
RandomAccessFile accessFile = null;
try {
accessFile = new RandomAccessFile(file, "r");
accessFile.seek(offset);
int readSize = accessFile.read(result);
if (readSize == -1) {
return null;
} else if (readSize == blockSize) {
return result;
} else {
byte[] tmpByte = new byte[readSize];
System.arraycopy(result, 0, tmpByte, 0, readSize);
return tmpByte;
}
} catch (IOException e) {
e.printStackTrace();
} finally {
if (accessFile != null) {
try {
accessFile.close();
} catch (IOException e1) {
}
}
}
return null;
}
获取文件MD5值主要分为三个步骤,第一步获取文件的byte信息,第二步通过MessageDigest类进行MD5加密,第三步转换成16进制的MD5码值。
其中 MessageDigest 类可以应用程序提供信息摘要算法的功能,如 MD5 或 SHA 算法。信息摘要是安全的单向哈希函数,它接收任意大小的数据,输出固定长度的哈希值。
public static String getFileMD5(String path) {
// 进行进制转换
BigInteger bi = null;
try {
byte[] buffer = new byte[8192];
int len = 0;
MessageDigest md = MessageDigest.getInstance("MD5");
File f = new File(path);
FileInputStream fis = new FileInputStream(f);
while ((len = fis.read(buffer)) != -1) {
// 处理数据
md.update(buffer, 0, len);
}
fis.close();
byte[] b = md.digest();
bi = new BigInteger(1, b);
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
return bi.toString(16);
}
上传视频这一过程,需要时刻监听任务完成的状况,这里定义了一个接口,来负责监听上传视频的状态,包括以下四种方法:
void onUploading(UploadTask uploadTask, String percent, int position);
void onPause(UploadTask uploadTask);
void onUploadSuccess(UploadTask uploadTask, File file);
void onError(UploadTask uploadTask, int errorCode, int position);
UploadTask 使用了 Builder 设计模式,因为 UploadTask 作为复杂对象,其构建需要多步初始化或赋值才能完成,使用Builder模式可以用来替代多参数构造函数。
public static class Builder {
private String id;
private String url; // 上传接口url
private String fileName;
private int uploadStatus = UploadStatus.UPLOAD_STATUS_INIT;
private int chunck; // 第几块
private UploadTaskListener listener;
// 作为上传task开始、删除、停止的key值
public Builder setId(String id) {
this.id = id;
return this;
}
public Builder setUrl(String url) {
this.url = url;
return this;
}
// 设置上传状态
public Builder setUploadStatus(int uploadStatus) {
this.uploadStatus = uploadStatus;
return this;
}
public Builder setChunck(int chunck) {
this.chunck = chunck;
return this;
}
public Builder setFileName(String fileName) {
this.fileName = fileName;
return this;
}
public Builder setListener(UploadTaskListener listener) {
this.listener = listener;
return this;
}
public UploadTask build() {
return new UploadTask(this);
}
}
而 UploadTask 的构造函数为
public UploadTask(Builder builder) {
mBuilder = builder;
mClient = new OkHttpClient();
this.id = mBuilder.id;
this.url = mBuilder.url;
this.fileName = mBuilder.fileName;
this.uploadStatus = mBuilder.uploadStatus;
this.chunck = mBuilder.chunck;
this.setmListener(mBuilder.listener);
}
具体的 run 方法如下,首先根据固定的 block 大小,计算出文件总共分的块数。而后遍历每一块,将参数放入 RequestBody 。最后根据返回结果改变上传状态 uploadStatus ,并且回调方法 onCallBack:
@Override
public void run() {
try {
int blockLength = 512 * 1024; // 以kb为计算单位
File file = new File(fileName);
String md5 = getFileMD5(fileName);
if (file.length() % blockLength == 0) { // 算出总块数
chuncks = (int) file.length() / blockLength;
} else {
chuncks = (int) file.length() / blockLength + 1;
}
Log.i(TAG,"chuncks =" +chuncks+ "fileName =" +fileName+ "uploadStatus =" +uploadStatus);
Log.i(TAG,"chunck =" +chunck);
Log.i(TAG,"md5 =" +md5);
while (chunck <= chuncks
&& uploadStatus != UploadStatus.UPLOAD_STATUS_PAUSE
&& uploadStatus != UploadStatus.UPLOAD_STATUS_ERROR) {
uploadStatus = UploadStatus.UPLOAD_STATUS_UPLOADING;
Map<String, String> params = new HashMap<String, String>();
params.put("name", fileName);
params.put("md5", md5);
params.put("size", file.length() + "");
params.put("chunks", chuncks + "");
params.put("chunk", chunck + "");
Log.i(TAG,"chunck =" +chunck+ "chuncks =" +chuncks);
final byte[] mBlock = FileUtils.getBlock((chunck - 1)
* blockLength, file, blockLength);
Log.i(TAG,"mBlock == " +mBlock.length);
// 生成RequestBody
MultipartBody.Builder builder = new MultipartBody.Builder();
addParams(builder, params);
String fileType = "file/*";
RequestBody requestBody = RequestBody.create(
MediaType.parse(fileType), mBlock);
builder.addFormDataPart("file", fileName, requestBody);
Log.i(TAG,"url =" +url);
//获得Request实例
Request request = new Request.Builder()
.url(url)
.post(builder.build())
.build();
Log.i(TAG,"RequestBody execute~");
Response response = null;
response = mClient.newCall(request).execute();
Log.i(TAG,"isSuccessful =" +response.isSuccessful());
if(response.isSuccessful()) {
String ret = response.body().string();
Log.d(TAG,"uploadVideo UploadTask ret:" +ret);
onCallBack();
chunck++;
} else {
uploadStatus = UploadStatus.UPLOAD_STATUS_ERROR;
onCallBack();
}
}
} catch (IOException e) {
Log.i(TAG,"run IOException");
uploadStatus = UploadStatus.UPLOAD_STATUS_ERROR;
onCallBack();
Log.i(TAG,"e error: =" +e.toString());
e.printStackTrace();
}
}
回调处理如下:
private void onCallBack() {
mHandler.sendEmptyMessage(uploadStatus);
}
Handler mHandler = new Handler(Looper.getMainLooper()) {
@Override
public void handleMessage(android.os.Message msg) {
int code = msg.what;
switch(code) {
// 上传失败
case UploadStatus.UPLOAD_STATUS_ERROR:
mListener.onError(UploadTask.this, errorCode, position);
break;
// 正在上传
case UploadStatus.UPLOAD_STATUS_UPLOADING:
mListener.onUploading(UploadTask.this, getDownLoadPercent(), position);
break;
// 暂停上传
case UploadStatus.UPLOAD_STATUS_PAUSE:
mListener.onPause(UploadTask.this);
break;
}
};
};