React + SpringBoot实现图片预览和视频在线播放,其中视频实现切片保存和分段播放

图片预览和视频在线播放

需求描述

实现播放视频的需求时,往往是前端直接加载一个mp4文件,这样做法在遇到视频文件较大时,容易造成卡顿,不能及时加载出来。我们可以将视频进行切片,然后分段加载。播放一点加载一点,这样同一时间内只会加载一小部分的视频,不容易出现播放卡顿的问题。下面是实现方法。

对视频切片使用的是 ffmpeg,可查看我的这个文章安装使用

后端接口处理

后端需要处理的逻辑有

  1. 根据视频的完整地址找到视频源文件
  2. 根据视频名称进行MD5,在同级目录下创建MD5文件夹,用于存放生成的索引文件和视频切片
  3. 前端调用视频预览接口时先判断有没有索引文件
    1. 如果没有,则先将mp4转为ts,然后对ts进行切片处理并生成index.m3u8索引文件,然后删除ts文件
    2. 如果有,则直接读取ts文件写入到响应头,以流的方式返回给浏览器
  4. 加载视频分片文件时会重复调用视频预览接口,需要对请求进来的参数做判断,判断是否是请求的索引还是分片

首先定义好接口,接收一个文件ID获取到对应的文件信息

@ApiOperation("文件预览")
@GetMapping("preview/{fileId}")
public void preview(@PathVariable String fileId, HttpServletResponse response) {if (fileId.endsWith(".ts")) {filePanService.readFileTs(fileId, response);} else {LambdaUpdateWrapper<FilePan> qw = new LambdaUpdateWrapper<>();qw.eq(FilePan::getFileId, fileId);FilePan one = filePanService.getOne(qw);if (ObjectUtil.isEmpty(one)) {throw new CenterExceptionHandler("文件不存在");}filePanService.preview(one, response);}
}

视频信息如下图

image-20240608111547732

在磁盘上对应的视频

image-20240608111647759

数据库中存放是视频信息

image-20240608111720899

当点击视频时,前端会拿到当前的文件ID请求上面定义好的接口,此时 fielId 肯定不是以 ts 结尾,所以会根据这个 fileId 查询数据库中保存的这条记录,然后调用 filePanService.preview(one, response) 方法

preview方法

preview方法主要处理的几个事情

  1. 首先判断文件类型是图片还是视频
  2. 如果是图片是直接读取图片并返回流
  3. 如果是视频
    1. 首先拿到视频名称,对名称进行md5处理,并生成文件夹
    2. 创建视频ts文件,并对ts进行切片和生成索引
  4. 加载分片文件时调用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];LambdaUpdateWrapper<FilePan> 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} className="toolbar-wrapper"><DownloadOutlined 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.js
import 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 id="video"></div><button className="ant-image-preview-close" onClick={hideModal}><span role="img" aria-label="close" className="anticon anticon-close"><svgfill-rule="evenodd"viewBox="64 64 896 896"focusable="false"data-icon="close"width="1em"height="1em"fill="currentColor"aria-hidden="true"><path d="M799.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);
}

实现效果

图片预览效果

image-20240608121403097

视频预览效果

image-20240608121438971

并且在播放过程中是分段加载的视频

image-20240608121550836

查看源文件,根据文件名创建一个MD5的文件夹

image-20240608121012668

文件夹中对视频进行了分片处理,每一片都是以文件ID开头,方便加载分片时找到分片对应的位置

image-20240608121809812

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/web/24505.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

tcp aimd 窗口的推导

旧事重提&#xff0c;今天用微分方程的数值解观测 tcp aimd 窗口值。 设系统 AI&#xff0c;MD 参数分别为 a 1&#xff0c;b 0.5&#xff0c;丢包率由 buffer 大小&#xff0c;red 配置以及线路误码率共同决定&#xff0c;设为 p&#xff0c;窗口为 W&#xff0c;则有&…

HAL STM32F1 通过查表方式实现SVPWM驱动无刷电机测试

HAL STM32F1 通过查表方式实现SVPWM驱动无刷电机测试 &#x1f4cd;相关篇《基于开源项目HAL STM32F4 DSP库跑SVPWM开环速度测试》 ✨针对STM32F1系列&#xff0c;没有专门的可依赖的DSP库&#xff0c;为了实现特定函数的浮点运算快速计算&#xff0c;通过查表方式来实现&#…

番外篇 | 利用华为2023最新Gold-YOLO中的Gatherand-Distribute对特征融合模块进行改进

前言:Hello大家好,我是小哥谈。论文提出一种改进的信息融合机制Gather-and-Distribute (GD) ,通过全局融合多层特征并将全局信息注入高层,以提高YOLO系列模型的信息融合能力和检测性能。通过引入MAE-style预训练方法,进一步提高模型的准确性。🌈 目录 🚀1.论文解…

如何解锁植物大战僵尸杂交版v2.0.88所有植物

如何解锁植物大战僵尸杂交版v2.0.88所有植物 前言安装相关软件快速解锁方法 前言 经过探索植物大战僵尸杂交版植物解锁和关卡有关&#xff0c;所以通过所有关卡就可以解锁所有植物。 安装相关软件 1.安装植物大战僵尸 2.安装Hex Editor Neo 快速解锁方法 本文参考如何修改…

<vs2022><问题记录>visual studio 2022使用console打印输出时,输出窗口不显示内容

前言 本文为问题记录。 问题概述 在使用visual studio 2022编写代码时&#xff0c;如C#&#xff0c;在代码中使用console.writeline来打印某些内容&#xff0c;以便于观察&#xff0c;但发现输出窗口不显示&#xff0c;而代码是完全没有问题的。 解决办法 根据网上提供的办法…

Java Web学习笔记23——Vue项目简介

Vue项目简介&#xff1a; Vue项目-创建&#xff1a; 命令行&#xff1a;vue create vue-project01 图形化界面&#xff1a;vue ui 在命令行中切换到项目文件夹中&#xff0c;然后执行vue ui命令。 只需要路由功能。这个路由功能&#xff0c;开始不是很理解。 创建项目部保存…

【原创】海为PLC与RS-WS-ETH-6传感器的MUDBUS_TCP通讯

点击“蓝字”关注我们吧 一、关于RS-WS-ETH-6传感器的准备工作 要完成MODBUS_TCP通讯,我们必须要知道设备的IP地址如何分配,只有PLC和设备的IP在同一网段上,才能建立通讯。然后还要选择TCP的工作模式,来建立设备端和PC端的端口号。接下来了解设备的报文格式,方便之后发送…

前端:快捷 复制chrome 控制台打印出来的 数组对象

程序中console.log出来的对象。按照以下步骤操作 1.右键点击需要处理的对象&#xff0c;会出现Store as global variable&#xff0c;点击 2.点击 Store as global variable 控制台会出现 3.在控制台 输入 copy(temp1) 这样对象就复制到了你的黏贴面板里面 在代码中直接 c…

基于STM32开发的智能语音控制系统

目录 引言环境准备智能语音控制系统基础代码实现&#xff1a;实现智能语音控制系统 4.1 语音识别模块数据读取4.2 设备控制4.3 实时数据监控与处理4.4 用户界面与反馈显示应用场景&#xff1a;语音控制的家居设备管理问题解决方案与优化收尾与总结 1. 引言 随着人工智能技术…

Vuepress 2从0-1保姆级进阶教程——标准化流程

Vuepress 2 专栏目录 1. 入门阶段 Vuepress 2从0-1保姆级入门教程——环境配置篇Vuepress 2从0-1保姆级入门教程——安装流程篇Vuepress 2从0-1保姆级入门教程——文档配置篇Vuepress 2从0-1保姆级入门教程——范例与部署 2.进阶阶段 Vuepress 2从0-1保姆级进阶教程——全文搜索…

Inpaint9.1软件下载附加详细安装教程

软件简介: Inpaint 是个人开发者Max开发的图片处理软件&#xff0c;可以高效去除水印&#xff0c;修复照片等。使用方法和操作都很简单&#xff0c;非常适合不会PS等软件的小白用户。 安 装 包 获 取 地 址&#xff1a; Iinpaint win版&#xff1a;​​https://souurl.cn/b…

了解JVM中的Server和Client参数

了解JVM中的Server和Client参数 Java虚拟机&#xff08;Java Virtual Machine&#xff0c;JVM&#xff09;作为Java程序运行的核心&#xff0c;提供了多种参数来优化和调整程序的性能和行为。其中&#xff0c;-server和-client是两个重要的参数&#xff0c;分别用于配置JVM在服…

微生物共生与致病性:动态变化与识别挑战

谷禾健康 细菌耐药性 抗生素耐药性细菌感染的发生率正在上升&#xff0c;而新抗生素的开发由于种种原因在制药行业受重视程度下降。 最新在《柳叶刀-微生物》&#xff08;The Lancet Microbe&#xff09;上&#xff0c;科学家提出了基于细菌适应性、竞争和传播的生态原则的跨学…

Tongweb7重置密码优化版*(by lqw )

如图所示&#xff0c;输入初始密码是会报错的&#xff0c;说明已经修改了密码 首先我们先备份一下tongweb的安装目录&#xff0c;避免因为修改过程中出现的差错而导致tongweb无法启动&#xff1a; 备份好了之后&#xff0c;我们关闭掉tongweb。 方式一&#xff1a; Cd 到tong…

C# WPF入门学习主线篇(十)—— DataGrid常见属性和事件

C# WPF入门学习主线篇&#xff08;十&#xff09;—— DataGrid常见属性和事件 欢迎来到C# WPF入门学习系列的第十篇。在前面的文章中&#xff0c;我们已经学习了 Button、TextBox、Label、ListBox 和 ComboBox 控件。今天&#xff0c;我们将探讨 WPF 中的另一个重要控件——D…

Python私教张大鹏 Vue3整合AntDesignVue之Anchor 锚点

用于跳转到页面指定位置。 何时使用 需要展现当前页面上可供跳转的锚点链接&#xff0c;以及快速在锚点之间跳转。 案例&#xff1a;锚点的基本使用 核心代码&#xff1a; <template><a-anchor:items"[{key: part-1,href: #part-1,title: () > h(span, {…

大学国学搜题软件?分享7个软件和公众号,来对比看看吧 #经验分享#微信#媒体

在大学里&#xff0c;高效的学习工具可以帮助我们更好地管理时间和资源&#xff0c;提高学习效果。 1.彩虹搜题 这是个老公众号了 多语言查询支持&#xff0c;满足国际用户需求。全球通用&#xff0c;无障碍搜题。 下方附上一些测试的试题及答案 1、某酸碱指示剂的&#xf…

uniapp自定义的下面导航

uniapp自定义的下面导航 看看效果图片吧 文章目录 uniapp自定义的下面导航 看看效果图片吧 ![在这里插入图片描述](https://img-blog.csdnimg.cn/direct/6aa0e964741d4dd3a58f4e86c4bf3247.png) 前言一、写组件、我这里就没有写组件了直接写了一个页面&#xff1f;总结 前言 在…

软件架构x86 、 x86_64、 arm64、aarch64

看系统信息: 大多数Linux发行版都提供如 uname -a命令 arch命令用于显示当前主机的硬件架构类型。 例如 下面的是Kylin Linux Advanced Server for Kunpeng V10 操作系统 (鲲鹏处理器是华为在2019年1月向业界发布的高性能数据中心处理器 ) 下面这个是 ubuntu 18.04.6 …

CMakeLists如何多行注释

在使用Visual Studio编写CMakeLists的时候你可能会遇到需要多行注释的情况&#xff0c;可又不知道快捷键是什么。。。 其实你只需要敲个 #[[ 就行了&#xff0c;另外一般方括号VS会自动帮你补全&#xff0c;之后将需要注释的内容放在第二个方括号与第三个方括号之间就完成注释…