前言:
本项目非原创,我也只是作为一名初学者跟着成熟的up主一起敲代码而已。视频来源于:
在哔哩哔哩闲逛看到这个项目,感觉还不错,于是想要学习一下这个项目怎么写。项目日记也会同步更新。(本人不分享本项目源码,支持项目付费)
本项目大量采用了先前项目中已经写好的代码,而且UP主讲的也很快。因此不适合新手作为自己的第一个项目。
目录
今日完结任务:
1.实现了合并文件的接口
上一篇我们讲到了大文件的分片上传,今天我们来讲一下大文件的合并。
在接收分片文件的末尾,我们调用了一个异步方法来实现 分片文件 的 合并。
我们来看看这个方法的具体实现:
我们来看看这个方法(transferFile)的具体实现,这个方法可以分为三大模块:
1.先对分片文件进行合并:
2.针对视频文件进行切割:
3.保存文件信息:
而分片文件合并和针对视频文件切割的具体代码我们是自己封装了一个方法,我们来看一看:
合并分片文件:
public static void union(String dirPath, String toFilePath, String fileName, boolean delSource) throws BusinessException {
File dir = new File(dirPath);
if (!dir.exists()) {
throw new BusinessException("目录不存在");
}
//获取所有分片文件
File fileList[] = dir.listFiles();
//创建合并后的目标文件
File targetFile = new File(toFilePath);
//设置使用RandomAccessFile类以读写模式来打开文件
RandomAccessFile writeFile = null;
try {
//创建writeFile类,以读写的方式打开文件
writeFile = new RandomAccessFile(targetFile, "rw");
//设置缓冲区,临时存储数据,统一写入TargetFile文件中
byte[] b = new byte[1024 * 10];
//遍历所有的分片文件
for (int i = 0; i < fileList.length; i++) {
int len = -1;
//得到具体的分片文件{File.separator实际上就是"/"}
File chunkFile = new File(dirPath + File.separator + i);
//创建一个新的randomAccessFile类来读文件
RandomAccessFile readFile = null;
try {
//尝试从chunkfile中读文件
readFile = new RandomAccessFile(chunkFile, "r");
//循环{把读取到的字节数赋值给{len},只要结果不是-1,那么就一直读取文件}
while ((len = readFile.read(b)) != -1) {
//把b里面的数据写到target中
writeFile.write(b, 0, len);
}
} catch (Exception e) {
logger.error("合并分片失败", e);
throw new BusinessException("合并文件失败");
} finally {
readFile.close();
}
}
} catch (Exception e) {
logger.error("合并文件:{}失败", fileName, e);
throw new BusinessException("合并文件" + fileName + "出错了");
} finally {
try {
if (null != writeFile) {
writeFile.close();
}
} catch (IOException e) {
logger.error("关闭流失败", e);
}
if (delSource) {
if (dir.exists()) {
try {
FileUtils.deleteDirectory(dir);
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
其实就是io不断的读取所有的分片文件,把所有的数据都存储到目标文件中。
切割视频文件:
private void cutFile4Video(String fileId, String videoFilePath) {
//创建同名切片目录
File tsFolder = new File(videoFilePath.substring(0, videoFilePath.lastIndexOf(".")));
//如果说不存在就创建
if (!tsFolder.exists()) {
tsFolder.mkdirs();
}
final String CMD_TRANSFER_2TS = "ffmpeg -y -i %s -vcodec copy -acodec copy -vbsf h264_mp4toannexb %s";
final String CMD_CUT_TS = "ffmpeg -i %s -c copy -map 0 -f segment -segment_list %s -segment_time 30 %s/%s_%%4d.ts";
//先把文件转为ts文件
String tsPath = tsFolder + "/" + Constants.TS_NAME;
//生成.ts
String cmd = String.format(CMD_TRANSFER_2TS, videoFilePath, tsPath);
ProcessUtils.executeCommand(cmd, false);
//生成索引文件.m3u8 和切片.ts
cmd = String.format(CMD_CUT_TS, tsPath, tsFolder.getPath() + "/" + Constants.M3U8_NAME, tsFolder.getPath(), fileId);
ProcessUtils.executeCommand(cmd, false);
//删除index.ts
new File(tsPath).delete();
}
这里我们使用的是ffmpeg来实现切割视频文件,其实代码不难,背后的逻辑就是:我们把一个大的视频文件切割成为小的ts文件和一个m3u8索引文件。
这样其实有很多的优点,例如:
-
流媒体播放支持: 将视频切割成小的 TS 文件和一个 M3U8 索引文件是为了实现流媒体播放。M3U8 索引文件是一种描述多媒体播放列表的格式,其中包含了各个 TS 文件的地址和播放顺序,可以通过这个索引文件实现视频的分段加载和播放,适应不同网络环境下的动态码率调整。
-
实现边下边播: 将视频文件切割成小的 TS 文件和一个 M3U8 索引文件可以实现边下边播的功能,即用户可以在下载视频的同时开始播放已下载的部分,而不必等待整个视频完全下载完毕。
-
方便管理和传输: 将视频切割成小的 TS 文件可以方便管理和传输。相比于一个大的视频文件,多个小的 TS 文件更容易分发和存储,也更容易实现断点续传和加速下载。
我们再来看一看生成视频缩略图:
/**
* 生成视频封面
* @param sourceFile
* @param width
* @param targetFile
*/
public static void createCover4Video(File sourceFile, Integer width, File targetFile) {
try {
String cmd = "ffmpeg -i %s -y -vframes 1 -vf scale=%d:%d/a %s";
ProcessUtils.executeCommand(String.format(cmd, sourceFile.getAbsoluteFile(), width, width, targetFile.getAbsoluteFile()), false);
} catch (Exception e) {
logger.error("生成视频封面失败", e);
}
}
生成图片缩略图:
/**
* 压缩图片,生成图片缩略图
* @param sourceFile
* @param width
* @param targetFile
* @param delSource
*/
public static void compressImage(File sourceFile, Integer width, File targetFile, Boolean delSource) {
try {
String cmd = "ffmpeg -i %s -vf scale=%d:-1 %s -y";
ProcessUtils.executeCommand(String.format(cmd, sourceFile.getAbsoluteFile(), width, targetFile.getAbsoluteFile()), false);
if (delSource) {
FileUtils.forceDelete(sourceFile);
}
} catch (Exception e) {
logger.error("压缩图片失败");
}
}
2.文件的预览
文件的预览其实就是把文件从存储磁盘中取出来,发送给前端。而在这个过程中,我们要区分当前预览的是视频还是其他文件。因为:如果是文件的话,我们就需要先发送m3u8文件,然后根据需要选择发送哪一个ts视频文件。
我们来看看代码,一共可以分为两部分:预览视频的处理方案和预览其他文件的处理方案
1.预览视频的处理方案
上面判断主要是针对用户分享做的鉴权。当我们鉴权做完了之后,就需要根据给出信息,拼接所需要的ts文件地址filePath。
之后返回当前视频段所需要的filePath就好了。
如果是第一次发送视频文件,我们就要先发送m3u8文件,
拼接m3u8地址给前端发送。
如果不是视频文件,那我们就是简单的读取全部文件,然后发回给前端
需要注意的是:
我们这里只是获取获取到了目标文件的地址,而我们需要把文件转为 字节流发送给前端,因此我们在返回的时候,还需要读取目标地址的文件。我们调用的就是下面这个方法。
protected void readFile(HttpServletResponse response, String filePath) {
if (!StringTools.pathIsOk(filePath)) {
return;
}
OutputStream out = null;
FileInputStream in = null;
try {
File file = new File(filePath);
if (!file.exists()) {
return;
}
in = new FileInputStream(file);
byte[] byteData = new byte[1024];
out = response.getOutputStream();
int len = 0;
while ((len = in.read(byteData)) != -1) {
out.write(byteData, 0, len);
}
out.flush();
} catch (Exception e) {
logger.error("读取文件异常", e);
} finally {
if (out != null) {
try {
out.close();
} catch (IOException e) {
logger.error("IO异常", e);
}
}
if (in != null) {
try {
in.close();
} catch (IOException e) {
logger.error("IO异常", e);
}
}
}
}
这样我们就实现了文件的预览功能。
总结:
这个项目的核心其实就写完了:后端处理文件的分片上传和分片文件合并 。以及文件的预览功能。这个项目引入了ffmpeg来实现对视频文件的处理。后续如果时间的话,会考虑写一下限速下载功能。但主要还是要先更完回收站功能,分享功能
这个项目涉及到了大量的IO,所以我在后续要学习一下NIO, 尝试改造这个项目。