uc网站怎么做,t么做文献索引ot网站,郑州仿站定制模板建站,网站运营效果分析怎么做图片预览和视频在线播放
需求描述
实现播放视频的需求时#xff0c;往往是前端直接加载一个mp4文件#xff0c;这样做法在遇到视频文件较大时#xff0c;容易造成卡顿#xff0c;不能及时加载出来。我们可以将视频进行切片#xff0c;然后分段加载。播放一点加载一点往往是前端直接加载一个mp4文件这样做法在遇到视频文件较大时容易造成卡顿不能及时加载出来。我们可以将视频进行切片然后分段加载。播放一点加载一点这样同一时间内只会加载一小部分的视频不容易出现播放卡顿的问题。下面是实现方法。
对视频切片使用的是 ffmpeg可查看我的这个文章安装使用
后端接口处理
后端需要处理的逻辑有
根据视频的完整地址找到视频源文件根据视频名称进行MD5在同级目录下创建MD5文件夹用于存放生成的索引文件和视频切片前端调用视频预览接口时先判断有没有索引文件 如果没有则先将mp4转为ts然后对ts进行切片处理并生成index.m3u8索引文件然后删除ts文件如果有则直接读取ts文件写入到响应头以流的方式返回给浏览器 加载视频分片文件时会重复调用视频预览接口需要对请求进来的参数做判断判断是否是请求的索引还是分片
首先定义好接口接收一个文件ID获取到对应的文件信息
ApiOperation(文件预览)
GetMapping(preview/{fileId})
public void preview(PathVariable String fileId, HttpServletResponse response) {if (fileId.endsWith(.ts)) {filePanService.readFileTs(fileId, response);} else {LambdaUpdateWrapperFilePan qw new LambdaUpdateWrapper();qw.eq(FilePan::getFileId, fileId);FilePan one filePanService.getOne(qw);if (ObjectUtil.isEmpty(one)) {throw new CenterExceptionHandler(文件不存在);}filePanService.preview(one, response);}
}视频信息如下图 在磁盘上对应的视频 数据库中存放是视频信息 当点击视频时前端会拿到当前的文件ID请求上面定义好的接口此时 fielId 肯定不是以 ts 结尾所以会根据这个 fileId 查询数据库中保存的这条记录然后调用 filePanService.preview(one, response) 方法
preview方法
preview方法主要处理的几个事情
首先判断文件类型是图片还是视频如果是图片是直接读取图片并返回流如果是视频 首先拿到视频名称对名称进行md5处理并生成文件夹创建视频ts文件并对ts进行切片和生成索引 加载分片文件时调用readFileTs方法
/*** 文件预览*/
Override
public void preview(FilePan filePan, HttpServletResponse response) {// 区分图片还是视频if (FileTypeUtil.isImage(filePan.getFileName())) {previewImg(filePan, response);} else if (FileTypeUtil.isVideo(filePan.getFileName())) {previewVideo(filePan, response);} else {throw new CenterExceptionHandler(该文件不支持预览);}
}/*** 图片预览** param filePan* param response*/
private void previewImg(FilePan filePan, HttpServletResponse response) {if (StrUtil.isEmpty(filePan.getFileId())) {return;}// 源文件路径String realTargetFile filePan.getFilePath();File file new File(filePan.getFilePath());if (!file.exists()) {return;}readFile(response, realTargetFile);
}/*** 视频预览** param filePan* param response*/
private void previewVideo(FilePan filePan, HttpServletResponse response) {// 根据文件名称创建对应的MD5文件夹String md5Dir FileChunkUtil.createMd5Dir(filePan.getFilePath());// 去这个目录下查看是否有index.m3u8这个文件String m3u8Path md5Dir / FileConstants.M3U8_NAME;if (!FileUtil.exist(m3u8Path)) {// 创建视频ts文件createVideoTs(filePan.getFilePath(), filePan.getFileId(), md5Dir, response);} else {// 读取切片文件readFile(response, m3u8Path);}
}// 创建视频切片文件
private void createVideoTs(String videoPath, String fileId, String targetPath, HttpServletResponse response) {// 1.生成ts文件String video_2_TS ffmpeg -y -i %s -vcodec copy -acodec copy -bsf:v h264_mp4toannexb %s;String tsPath targetPath / FileConstants.TS_NAME;String cmd String.format(video_2_TS, videoPath, tsPath);ProcessUtils.executeCommand(cmd, false);// 2.创建切片文件String ts_chunk ffmpeg -i %s -c copy -map 0 -f segment -segment_list %s -segment_time 60 %s/%s_%%4d.ts;String m3u8Path targetPath / FileConstants.M3U8_NAME;cmd String.format(ts_chunk, tsPath, m3u8Path, targetPath, fileId);ProcessUtils.executeCommand(cmd, false);// 删除index.ts文件FileUtil.del(tsPath);// 读取切片文件readFile(response, m3u8Path);
}// 加载视频切片文件
Override
public void readFileTs(String tsFileId, HttpServletResponse response) {String[] tsArray tsFileId.split(_);String videoFileId tsArray[0];LambdaUpdateWrapperFilePan qw new LambdaUpdateWrapper();qw.eq(FilePan::getFileId, videoFileId);FilePan one this.getOne(qw);// 获取文件对应的MD5文件夹地址String md5Dir FileChunkUtil.createMd5Dir(one.getFilePath());// 去MD5目录下读取ts分片文件String tsFile md5Dir / tsFileId;readFile(response, tsFile);
}用到的几个工具类代码
FileTypeUtil
package com.szx.usercenter.util;/*** author songzx* create 2024-06-07 13:39*/
public class FileTypeUtil {/*** 是否是图片类型的文件*/public static boolean isImage(String fileName) {String[] imageSuffix {jpg, jpeg, png, gif, bmp, webp};String suffix fileName.substring(fileName.lastIndexOf(.) 1);for (String s : imageSuffix) {if (s.equals(suffix)) {return true;}}return false;}/*** 是否是视频文件*/public static boolean isVideo(String fileName) {String[] videoSuffix {mp4, avi, rmvb, mkv, flv, wmv};String suffix fileName.substring(fileName.lastIndexOf(.) 1);for (String s : videoSuffix) {if (s.equals(suffix)) {return true;}}return false;}
}FileChunkUtil
package com.szx.usercenter.util;import cn.hutool.core.io.FileUtil;
import cn.hutool.crypto.digest.MD5;import java.io.File;/*** 文件上传后的各种处理操作* author songzx* create 2024-06-07 13:25*/
public class FileChunkUtil {/*** 合并完文件后根据文件名称创建MD5目录* 用于存放文件缩略图*/public static String createMd5Dir(String filePath) {File targetFile new File(filePath);String md5Dir MD5.create().digestHex(targetFile.getName());String targetDir targetFile.getParent() File.separator md5Dir;FileUtil.mkdir(targetDir);return targetDir;}
}readFile
/*** 读取文件方法** param response* param filePath*/
public static void readFile(HttpServletResponse response, String filePath) {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) {e.printStackTrace();} finally {if (out ! null) {try {out.close();} catch (IOException e) {e.printStackTrace();}}if (in ! null) {try {in.close();} catch (IOException e) {e.printStackTrace();}}}
}ProcessUtils
这个方法用于执行CMD命令
package com.szx.usercenter.util;import com.szx.usercenter.handle.CenterExceptionHandler;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;/*** 可以执行命令行命令的工具** author songzx* create 2024-06-06 8:56*/
public class ProcessUtils {private static final Logger logger LoggerFactory.getLogger(ProcessUtils.class);public static String executeCommand(String cmd, Boolean outPrintLog) {if (StringUtils.isEmpty(cmd)) {logger.error(--- 指令执行失败---);return null;}Runtime runtime Runtime.getRuntime();Process process null;try {process Runtime.getRuntime().exec(cmd);// 取出输出流PrintStream errorStream new PrintStream(process.getErrorStream());PrintStream inputStream new PrintStream(process.getInputStream());errorStream.start();inputStream.start();// 获取执行的命令信息process.waitFor();// 获取执行结果字符串String result errorStream.stringBuffer.append(inputStream.stringBuffer \n).toString();// 输出执行的命令信息if (outPrintLog) {logger.info(执行命令{}已执行完毕执行结果{}, cmd, result);} else {logger.info(执行命令{}已执行完毕, cmd);}return result;} catch (Exception e) {e.printStackTrace();throw new CenterExceptionHandler(命令执行失败);} finally {if (null ! process) {ProcessKiller processKiller new ProcessKiller(process);runtime.addShutdownHook(processKiller);}}}private static class ProcessKiller extends Thread {private Process process;public ProcessKiller(Process process) {this.process process;}Overridepublic void run() {this.process.destroy();}}static class PrintStream extends Thread {InputStream inputStream null;BufferedReader bufferedReader null;StringBuffer stringBuffer new StringBuffer();public PrintStream(InputStream inputStream) {this.inputStream inputStream;}Overridepublic void run() {try {if (null inputStream) {return;}bufferedReader new BufferedReader(new InputStreamReader(inputStream));String line null;while ((line bufferedReader.readLine()) ! null) {stringBuffer.append(line);}} catch (Exception e) {logger.error(读取输入流出错了错误信息 e.getMessage());} finally {try {if (null ! bufferedReader) {bufferedReader.close();}if (null ! inputStream) {inputStream.close();}} catch (IOException e) {logger.error(关闭流时出错);}}}}
}前端方法实现
前端使用的是React
定义图片预览组件 PreviewImage
import React, { forwardRef, useImperativeHandle } from react;
import {DownloadOutlined,UndoOutlined,RotateLeftOutlined,RotateRightOutlined,SwapOutlined,ZoomInOutlined,ZoomOutOutlined,
} from ant-design/icons;
import { Image, Space } from antd;const PreviewImage: React.FC forwardRef((props, ref) {const [src, setSrc] React.useState();const showPreview (fileId: string) {setSrc(/api/pan/preview/${fileId});document.getElementById(previewImage).click();};useImperativeHandle(ref, () {return {showPreview,};});const onDownload () {fetch(src).then((response) response.blob()).then((blob) {const url URL.createObjectURL(new Blob([blob]));const link document.createElement(a);link.href url;link.download image.png;document.body.appendChild(link);link.click();URL.revokeObjectURL(url);link.remove();});};return (Imageid{previewImage}style{{ display: none }}src{src}preview{{toolbarRender: (_,{transform: { scale },actions: {onFlipY,onFlipX,onRotateLeft,onRotateRight,onZoomOut,onZoomIn,onReset,},},) (Space size{12} classNametoolbar-wrapperDownloadOutlined onClick{onDownload} /SwapOutlined rotate{90} onClick{onFlipY} /SwapOutlined onClick{onFlipX} /RotateLeftOutlined onClick{onRotateLeft} /RotateRightOutlined onClick{onRotateRight} /ZoomOutOutlined disabled{scale 1} onClick{onZoomOut} /ZoomInOutlined disabled{scale 50} onClick{onZoomIn} /UndoOutlined onClick{onReset} //Space),}}/);
});export default PreviewImage;定义视频预览组件
视频预览用到了 dplayer 安装
pnpm add dplayer hls.jsimport React, { forwardRef, useEffect, useImperativeHandle, useRef, useState } from react;
import DPlayer from dplayer;
import ./style/video-model.less;const Hls require(hls.js);const PreviewVideo forwardRef((props, ref) {let dp useRef();const [modal2Open, setModal2Open] useState(false);const [fileId, setFileId] useState();const showPreview (fileId) {setFileId(fileId);setModal2Open(true);};const hideModal () {setModal2Open(false);};const clickModal (e) {if (e.target.dataset.tagName parentBox) {hideModal();}};useEffect(() {if (modal2Open) {console.log(fileId, videovideovideo);dp.current new DPlayer({container: document.getElementById(video), // 注意这里一定要写div的domlang: zh-cn,video: {url: /api/pan/preview/${fileId}, // 这里填写.m3u8视频连接type: customHls,customType: {customHls: function (video) {const hls new Hls();hls.loadSource(video.src);hls.attachMedia(video);},},},});dp.current.play();}}, [modal2Open]);useImperativeHandle(ref, () {return {showPreview,};});return ({modal2Open (div className{video-box} data-tag-name{parentBox} onClick{clickModal}div idvideo/divbutton classNameant-image-preview-close onClick{hideModal}span roleimg aria-labelclose classNameanticon anticon-closesvgfill-ruleevenoddviewBox64 64 896 896focusablefalsedata-iconclosewidth1emheight1emfillcurrentColoraria-hiddentruepath dM799.86 166.31c.02 0 .04.02.08.06l57.69 57.7c.04.03.05.05.06.08a.12.12 0 010 .06c0 .03-.02.05-.06.09L569.93 512l287.7 287.7c.04.04.05.06.06.09a.12.12 0 010 .07c0 .02-.02.04-.06.08l-57.7 57.69c-.03.04-.05.05-.07.06a.12.12 0 01-.07 0c-.03 0-.05-.02-.09-.06L512 569.93l-287.7 287.7c-.04.04-.06.05-.09.06a.12.12 0 01-.07 0c-.02 0-.04-.02-.08-.06l-57.69-57.7c-.04-.03-.05-.05-.06-.07a.12.12 0 010-.07c0-.03.02-.05.06-.09L454.07 512l-287.7-287.7c-.04-.04-.05-.06-.06-.09a.12.12 0 010-.07c0-.02.02-.04.06-.08l57.7-57.69c.03-.04.05-.05.07-.06a.12.12 0 01.07 0c.03 0 .05.02.09.06L512 454.07l287.7-287.7c.04-.04.06-.05.09-.06a.12.12 0 01.07 0z/path/svg/span/button/div)}/);
});export default PreviewVideo;父组件引入并使用
import PreviewImage from /components/Preview/PreviewImage;
import PreviewVideo from /components/Preview/PreviewVideo;const previewRef useRef();
const previewVideoRef useRef();// 点击的是文件
const clickFile async (item) {// 预览图片if (isImage(item.fileType)) {previewRef.current.showPreview(item.fileId);return;}// 预览视频if (isVideo(item.fileType)) {previewVideoRef.current.showPreview(item.fileId);return;}message.error(暂不支持预览该文件);
};// 点击的文件夹
const clickFolder (item) {props.pushBread(item); // 更新面包屑
};// 点击某一行时触发
const clickRow (item: { fileType?: string }) {if (item.fileType) {clickFile(item);} else {clickFolder(item);}
};PreviewImage ref{previewRef} /
PreviewVideo ref{previewVideoRef} /判断文件类型的方法
// 判断文件是否为图片
export function isImage(fileType): boolean {const imageTypes [.jpg, .png, .jpeg, .gif, .bmp, .webp]return imageTypes.includes(fileType);
}// 判断是否为视频
export function isVideo(fileType): boolean {const videoTypes [.mp4, .avi, .rmvb, .mkv, .flv, .wmv]return videoTypes.includes(fileType);
}实现效果
图片预览效果 视频预览效果 并且在播放过程中是分段加载的视频 查看源文件根据文件名创建一个MD5的文件夹 文件夹中对视频进行了分片处理每一片都是以文件ID开头方便加载分片时找到分片对应的位置