大文件上传
创始人
2025-06-01 19:25:36
0

上图就是大致的流程


一、标题图片

  1. 上传课程的标题图片

  1. Ajax发送请求到后端

  1. 后端接收到图片使用IO流去保存图片,返回图片的信息对象

  1. JS回调函数接收对象通过$("元素id").val(值),方式给页面form表达img标签src属性值,达到上传图片并回显

二、大文件上传(流媒体、音频、.zip文件等)

  1. 页面div点击事件,选中一个视频文件

  1. JS判断文件大小,判断文件类型是否合法

3.JS循环切片,计算总片数,计算每一片的起始位置,循环上传

4.发送Ajax转给后端切片

5.后端接收并创建临时目录存放

    /*** 分片上传,前端调用此方法* @param request* @param guid* @param chunk* @param file* @return*/@PostMapping("/uploadSlice")@ResponseBodypublic ResponseResult uploadSlice2(HttpServletRequest request, @RequestParam("guid") String guid,@RequestParam("chunk") Integer chunk,@RequestParam("file") MultipartFile file) {if (this.uploadSlice(request, guid, chunk, file)){return ResponseResultUtils.genResult("上传成功","");}else{return ResponseResultUtils.genErrorResult("上传失败");}}/*** 分片上传的具体方法* @param request* @param guid* @param chunk* @param file* @return*/private boolean uploadSlice(HttpServletRequest request, String guid, Integer chunk, MultipartFile file) {try {boolean isMultipart = ServletFileUpload.isMultipartContent(request);logger.info("isMultipart = {}",isMultipart);if (isMultipart) {if (chunk == null){chunk = 0;}// 临时目录用来存放所有分片文件String tempFileDir =  rootFilePath + bigPath + guid;File parentFileDir = new File(tempFileDir);if (!parentFileDir.exists()) {parentFileDir.mkdirs();}logger.info("接到上传的分片文件,{},{},{}",guid,chunk,tempFileDir);// 分片处理时,前台会多次调用上传接口,每次都会上传文件的一部分到后台File tempPartFile = new File(parentFileDir, guid + "_" + chunk + ".part");FileUtils.copyInputStreamToFile(file.getInputStream(), tempPartFile);}} catch (Exception e) {return false;}return true;}

6.计数器变量值=总片数时,发送合并请求

7.后端合并

/*** 分片文件合并,前端调用此方法* @param guid* @param fileName* @return*/@RequestMapping("/uploadMerge")@ResponseBodypublic ResponseResult uploadMerge2(@RequestParam("guid") String guid, @RequestParam("fileName") String fileName) {// 得到 destTempFile 就是最终的文件SpaceImage image = this.uploadMerge(guid, fileName);if(null != imageSpaceImage){return ResponseResultUtils.genResult(imageSpaceImage,"合并成功");}else{return ResponseResultUtils.genErrorResult("合并文件失败");}}private ImageSpaceImage uploadMerge(String guid, String fileName){SpaceImage image = mergeFile(guid, fileName);//此处需要注意,OSS需要再次切片上传,但minIO是不用得,它默认5M超过就会自动切片String path = "";//移除文件poolTaskExecutor.execute(() -> {com.eyang.ecpp.utils.FileUtils.deleteFile(rootFilePath+bigPath);});return imageSpaceImage;}private ImageSpaceImage mergeFile(String guid, String fileName) {logger.info("接到上传的分片文件合并请求,{},{}",guid,fileName);try {String sName = fileName.substring(fileName.lastIndexOf("."));//时间格式化格式Date currentTime = new Date();SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyyMMddHHmmssSSS");//获取当前时间并作为时间戳String timeStamp = simpleDateFormat.format(currentTime);//拼接新的文件名String newName = timeStamp + sName;simpleDateFormat = new SimpleDateFormat("yyyyMM");String tempPath = rootFilePath + bigPath+guid;String margePath = rootFilePath + bigPath+simpleDateFormat.format(currentTime);File parentFileDir = new File(tempPath);if (parentFileDir.isDirectory()) {File destTempFile = new File(margePath, newName);if (!destTempFile.exists()) {//先得到文件的上级目录,并创建上级目录,在创建文件destTempFile.getParentFile().mkdir();destTempFile.createNewFile();}for (int i = 0; i < Objects.requireNonNull(parentFileDir.listFiles()).length; i++) {File partFile = new File(parentFileDir, guid + "_" + i + ".part");FileOutputStream destTempfos = new FileOutputStream(destTempFile, true);//遍历"所有分片文件"到"最终文件"中FileUtils.copyFile(partFile, destTempfos);destTempfos.close();}// 删除临时目录中的分片文件FileUtils.deleteDirectory(parentFileDir);String[] resultArr = FileStorageUtils.saveBigFile(Files.readAllBytes(Paths.get(destTempFile.getPath())), com.eyang.ecpp.utils.FileUtils.getExtension(destTempFile.getName()));ImageSpaceImage imageSpaceImage = new ImageSpaceImage();//第一个是组名 第二个是改后的文件名imageSpaceImage.setImgUrl(margePath+"/"+newName);//imageSpaceImage.setName(destTempFile.getName());imageSpaceImage.setName(fileName);return imageSpaceImage;//destTempFile.getAbsolutePath();}} catch (Exception e) {logger.error("切片文件合并,失败原因e:{}", e.getMessage());}return null;}

8.合并成功后进行转码

转码需要下载一个ffmpeg,下载完解压打开bin目录,打开电脑的环境变量往path中添加上bin的目录即可。

还需要引入依赖

 

com.google.code.gson
gson
2.8.6

9.转码发送请求

10.后端转码

/*** 视频编码** @param absolutePath 绝对路径* @return {@link ResponseResult}<{@link ?}>*/@RequiresPermissions("cms:article:edit")@RequestMapping(value = "video/coding")@ResponseBodypublic ResponseResult videoCoding(@RequestParam String absolutePath) {TranscodeConfig transcodeConfig = new TranscodeConfig();//设置视频封面transcodeConfig.setPoster("1");transcodeConfig.setTsSeconds("60");ResponseResult transResult = TranscodeFileUtils.transCodeFile(absolutePath, transcodeConfig);String retUrl = "";if (null != transResult) {Map data = (Map) transResult.getData();if (null != data) {Map videoInfo = (Map) data.get("data");if (null != videoInfo) {retUrl = (String) videoInfo.get("m3u8");}}}return ResponseResultUtils.genResult(retUrl,"转码成功");}

视频转码配置实体类

package com.utils;public class TranscodeConfig {private String poster; // 截取封面的时间private String tsSeconds; // ts分片大小,单位是秒private String cutStart; // 视频裁剪,开始时间private String cutEnd; // 视频裁剪,结束时间public String getPoster() {return poster;}public void setPoster(String poster) {this.poster = poster;}public String getTsSeconds() {return tsSeconds;}public void setTsSeconds(String tsSeconds) {this.tsSeconds = tsSeconds;}public String getCutStart() {return cutStart;}public void setCutStart(String cutStart) {this.cutStart = cutStart;}public String getCutEnd() {return cutEnd;}public void setCutEnd(String cutEnd) {this.cutEnd = cutEnd;}public TranscodeConfig() {}public TranscodeConfig(String poster, String tsSeconds, String cutStart, String cutEnd) {this.poster = poster;this.tsSeconds = tsSeconds;this.cutStart = cutStart;this.cutEnd = cutEnd;}
}

转码工具类

public class TranscodeFileUtils {/*** 视频根路径*/private static String videoFolder= Global.getConfig("video.folder");private static final Logger LOGGER = LoggerFactory.getLogger(TranscodeFileUtils.class);public static ResponseResult> transCodeFile(String filePath, TranscodeConfig transcodeConfig){try {// 按照日期生成子目录String today = DateTimeFormatter.ofPattern("yyyyMMdd").format(LocalDate.now());String s = Identities.uuid2();Path targetFolder = Paths.get(videoFolder, today, s);// 执行转码操作LOGGER.info("开始转码");FFmpegUtils.transcodeToM3u8(filePath, targetFolder.toString(), transcodeConfig);// 封装结果Map videoInfo = new HashMap<>();videoInfo.put("m3u8", String.join("\\", targetFolder.toString(), "index.m3u8"));videoInfo.put("poster", String.join("\\", targetFolder.toString(), "poster.jpg"));Map result = new HashMap<>();result.put("success", true);result.put("data", videoInfo);return ResponseResultUtils.genResult(result,"转码成功!");} catch (Exception e) {e.printStackTrace();}return null;}public static void main(String[] args){System.out.println(TranscodeFileUtils.transCodeFile("E:\\录屏\\shipin\\test.mp4", new TranscodeConfig("00:00:00.001","15","","")));}}

转码工具类

public class FFmpegUtils {private static final Logger LOGGER = LoggerFactory.getLogger(FFmpegUtils.class);// 跨平台换行符private static final String LINE_SEPARATOR = System.getProperty("line.separator");/*** 生成随机16个字节的AESKEY* @return*/private static byte[] genAesKey ()  {try {KeyGenerator keyGenerator = KeyGenerator.getInstance("AES");keyGenerator.init(128);return keyGenerator.generateKey().getEncoded();} catch (NoSuchAlgorithmException e) {return null;}}/*** 在指定的目录下生成key_info, key文件,返回key_info文件* @param folder* @throws IOException*/private static Path genKeyInfo(String folder) throws IOException {// AES 密钥byte[] aesKey = genAesKey();// AES 向量String iv = Hex.encodeHexString(genAesKey());// key 文件写入Path keyFile = Paths.get(folder, "key");Files.write(keyFile, aesKey, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING);// key_info 文件写入StringBuilder stringBuilder = new StringBuilder();stringBuilder.append("key").append(LINE_SEPARATOR);                    // m3u8加载key文件网络路径stringBuilder.append(keyFile.toString()).append(LINE_SEPARATOR);    // FFmeg加载key_info文件路径stringBuilder.append(iv);                                            // ASE 向量Path keyInfo = Paths.get(folder, "key_info");Files.write(keyInfo, stringBuilder.toString().getBytes(), StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING);return keyInfo;}/*** 指定的目录下生成 master index.m3u8 文件* @param file            master m3u8文件地址* @param indexPath            访问子index.m3u8的路径* @param bandWidth            流码率* @throws IOException*/private static void genIndex(String file, String indexPath, String bandWidth) throws IOException {StringBuilder stringBuilder = new StringBuilder();stringBuilder.append("#EXTM3U").append(LINE_SEPARATOR);stringBuilder.append("#EXT-X-STREAM-INF:BANDWIDTH=" + bandWidth).append(LINE_SEPARATOR);  // 码率stringBuilder.append(indexPath);Files.write(Paths.get(file), stringBuilder.toString().getBytes(StandardCharsets.UTF_8), StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING);}/*** 转码视频为m3u8* @param source                源视频* @param destFolder            目标文件夹* @param config                配置信息* @throws IOException* @throws InterruptedException*/public static void transcodeToM3u8(String source, String destFolder, TranscodeConfig config) throws IOException, InterruptedException {// 判断源视频是否存在if (!Files.exists(Paths.get(source))) {throw new IllegalArgumentException("文件不存在:" + source);}// 创建工作目录Path workDir = Paths.get(destFolder, "ts");Files.createDirectories(workDir);// 在工作目录生成KeyInfo文件Path keyInfo = genKeyInfo(workDir.toString());// 构建命令List commands = new ArrayList<>();commands.add("ffmpeg");commands.add("-i");commands.add(source);                    // 源文件commands.add("-c:v");commands.add("libx264");                // 视频编码为H264commands.add("-c:a");commands.add("copy");                    // 音频直接copycommands.add("-hls_key_info_file");commands.add(keyInfo.toString());        // 指定密钥文件路径commands.add("-hls_time");commands.add(config.getTsSeconds());    // ts切片大小commands.add("-hls_playlist_type");commands.add("vod");                    // 点播模式commands.add("-hls_segment_filename");commands.add("%06d.ts");                // ts切片文件名称if (StringUtils.hasText(config.getCutStart())) {commands.add("-ss");commands.add(config.getCutStart());    // 开始时间}if (StringUtils.hasText(config.getCutEnd())) {commands.add("-to");commands.add(config.getCutEnd());        // 结束时间}commands.add("index.m3u8");                                                        // 生成m3u8文件// 构建进程Process process = new ProcessBuilder().command(commands).directory(workDir.toFile()).start();// 读取进程标准输出new Thread(() -> {try (BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(process.getInputStream()))) {String line = null;while ((line = bufferedReader.readLine()) != null) {LOGGER.info(line);}} catch (IOException e) {}}).start();// 读取进程异常输出new Thread(() -> {try (BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(process.getErrorStream()))) {String line = null;while ((line = bufferedReader.readLine()) != null) {LOGGER.info(line);}} catch (IOException e) {}}).start();// 阻塞直到任务结束if (process.waitFor() != 0) {throw new RuntimeException("视频切片异常");}// 切出封面if (!screenShots(source, String.join(File.separator, destFolder, "poster.jpg"), config.getPoster())) {throw new RuntimeException("封面截取异常");}// 获取视频信息MediaInfo mediaInfo = getMediaInfo(source);if (mediaInfo == null) {throw new RuntimeException("获取媒体信息异常");}// 生成index.m3u8文件genIndex(String.join(File.separator, destFolder, "index.m3u8"), "ts/index.m3u8", mediaInfo.getFormat().getBitRate());// 删除keyInfo文件Files.delete(keyInfo);}/*** 获取视频文件的媒体信息* @param source* @return* @throws IOException* @throws InterruptedException*/public static MediaInfo getMediaInfo(String source) throws IOException, InterruptedException {List commands = new ArrayList<>();commands.add("ffprobe");commands.add("-i");commands.add(source);commands.add("-show_format");commands.add("-show_streams");commands.add("-print_format");commands.add("json");Process process = new ProcessBuilder(commands).start();MediaInfo mediaInfo = null;try (BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(process.getInputStream()))) {mediaInfo = new Gson().fromJson(bufferedReader, MediaInfo.class);} catch (IOException e) {e.printStackTrace();}if (process.waitFor() != 0) {return null;}return mediaInfo;}/*** 截取视频的指定时间帧,生成图片文件* @param source        源文件* @param file            图片文件* @param time            截图时间 HH:mm:ss.[SSS]* @throws IOException* @throws InterruptedException*/public static boolean screenShots(String source, String file, String time) throws IOException, InterruptedException {List commands = new ArrayList<>();commands.add("ffmpeg");commands.add("-i");commands.add(source);commands.add("-ss");commands.add(time);commands.add("-y");commands.add("-q:v");commands.add("1");commands.add("-frames:v");commands.add("1");commands.add("-f");commands.add("image2");commands.add(file);Process process = new ProcessBuilder(commands).start();// 读取进程标准输出new Thread(() -> {try (BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(process.getInputStream()))) {String line = null;while ((line = bufferedReader.readLine()) != null) {LOGGER.info(line);}} catch (IOException e) {}}).start();// 读取进程异常输出new Thread(() -> {try (BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(process.getErrorStream()))) {String line = null;while ((line = bufferedReader.readLine()) != null) {LOGGER.error(line);}} catch (IOException e) {}}).start();return process.waitFor() == 0;}}

11.转码成功返回m3u8文件的路径,放到页面input隐藏标签中

编码成功后执行回调函数,m3u8文件赋给_data变量如下图

使用$(#页面元素id).val(值),下图是把m3u8赋值给页面的input

12.最后页面form提交,保存视频转码后m3u8的路径。

相关内容

热门资讯

“分鞋破镜”的意思 “分鞋破镜”的意思 成语拼音: [fēn xié pò jìng] ...
“幺幺小丑”的意思 “幺幺小丑”的意思 成语拼音: [yāo mó xiǎo chǒu] ...
“谋臣武将”的意思 “谋臣武将”的意思 成语拼音: [móu chén wǔ jiàng] ...
“攒三集五”的意思 “攒三集五”的意思 成语拼音: [zǎn sān jí wǔ] ...
“虎视鹰瞵”的意思 “虎视鹰瞵”的意思 成语拼音: [hǔ shì yīng lín] ...
“逾淮为枳”的意思 “逾淮为枳”的意思 成语拼音: [yú huái wéi zhǐ] ...
“鸣珂锵玉”的意思 “鸣珂锵玉”的意思 成语拼音: [míng kē qiāng yù] ...
寻人启事 寻人启事范文(通用8篇)  寻人启事,汉语词语,发布寻人启事寻人,是失踪者及其亲友间相互寻找的一种基...
“乌合之卒”的意思 “乌合之卒”的意思 成语拼音: [wū hé zhī zú] ...
入木三分成语解释 入木三分成语解释  入木三分,是汉语的一则成语,出自唐·张怀瓘《书断·王羲之》。下面是小编为大家整理...
“逾闲荡检”的意思 “逾闲荡检”的意思 成语拼音: [yú xián dàng jiǎn] ...
形容勇敢的词语 形容勇敢的词语大全  词语,是词和短语的合称,包括词(含单词、合成词)和词组(又称短语),组成语句文...
“磨拳擦掌”的意思 “磨拳擦掌”的意思 成语拼音: [mó quán cā zhǎng] ...
小学生元旦表演什么节目好   2016年元旦晚会目创意推荐:  1、成语连珠,就是先说个成语每个组或每个人用最后一个字接成语…...
岩的成语及解释 关于岩的成语及解释大全  以“岩”字开头的成语及解释如下:  [岩穴之士] 指隐士。古时隐士多山居,...
“胸无成算”的意思 “胸无成算”的意思 成语拼音: [xiōng wú chéng suàn] ...
“导德齐礼”的意思 “导德齐礼”的意思 成语拼音: [dǎo dé qí lǐ] ...
描写人物外貌的成语 有关描写人物外貌的成语  导语:描写人物外貌的成语有很多,你都知道哪些呢?下面是小编为你准备的有关描...
《公孙仪相鲁而嗜鱼》文言文阅... 《公孙仪相鲁而嗜鱼》文言文阅读答案  公孙仪相鲁而嗜鱼,一国尽争买鱼而献之。公孙仪不受。其弟子谏曰:...
“不可磨灭”的意思 “不可磨灭”的意思 成语拼音: [bù kě mó miè] ...