vue分片上传视频并转换为m3u8文件并播放

news2024/11/28 12:32:40

开发环境:

基于若依开源框架的前后端分离版本的实践,后端java的springboot,前端若依的vue2,做一个分片上传视频并分段播放的功能,因为是小项目,并没有专门准备文件服务器和CDN服务,后端也是套用的若依的上传功能

实现思路:

  • 前端根据视频文件计算出文件md5值
  • 前端按照指定大小截取视频,执行分片上传(可优化,先使用文件MD5检查文件是否已上传)
  • 后端实现接收分片的接口,当已上传分片数等于总分片数时执行合并分片,得到原视频文件
  • 后端使用ffmpeg按照时间进行视频分割,切割时间根据视频清晰度不同而不同,得到m3u8文件和ts文件列表
  • 后端保存视频信息和文件实际保存地址,并提供查询接口
  • 前端使用流播放器播放视频文件

代码实现

1. vue的分片上传

前端分片上传功能按照以下步骤实现:

1.1,先要写一个上传组件,这里使用elementUI的上传组件

:auto-upload 设置的视频直接不解释上传,即选择好本地文件就上传
:before-upload 中需要计算好文件的md5值,然后去后端查看文件是否已被上传
:http-request 中实现具体的分片上传逻辑
:action 虽然设置了上传地址,但是任然是以http-request设置的方法为准,只是不设置会报错

<el-form-item label="视频文件" prop="file" v-if="form.id==null">
 <el-upload ref="upload"
             :action="uploadUrl"
             :on-error="onError"
             :before-upload="beforeUpload"
             :before-remove="beforeRemove"
             :auto-upload="true"
             :limit="1"
             :http-request="chunkedUpload"
             :on-progress="onProgress"
  >
    <div style="border: 1px dashed #c0ccda;padding: 1rem;">
      <i class="el-icon-upload"></i>
      <div class="el-upload__text">将文件拖到此处,或<em>点击上传</em></div>
    </div>
    <div class="el-upload__tip" slot="tip">只能上传mp4文件,且不超过500M</div>
    <el-progress :percentage="uploadPercentage" status="success"></el-progress>
  </el-upload>
</el-form-item>

1.2,上传方法的js

我使用了两个后端接口,
一个是 testUploadVideo 判断文件是否存在,是若依分装的请求
一个是 process.env.VUE_APP_BASE_API + ‘/manage/video/upload’,单独用axios执行上传分片

在这里插入图片描述

<script>
import { addVideo, getVideo, testUploadVideo, updateVideo } from '@/api/manage/video'
import SparkMD5 from 'spark-md5'
import axios from 'axios'

export default {
  name: 'videoWin',
  data() {
    return {
      uploadUrl: process.env.VUE_APP_BASE_API + '/manage/video/upload', //文件上传的路径
      uploadPromises: [], // 记录并发上传分片的线程
      uploadPercentage:0 //上传进度
    }
  },,
  methods: {
    beforeUpload: async function(file) {
      // 在上传之前获取视频的宽高和分辨率
      const video = document.createElement('video')
      video.src = URL.createObjectURL(file)
      video.preload = 'metadata'
      const loadedMetadata = new Promise(resolve => {
        video.onloadedmetadata = () => {
          window.URL.revokeObjectURL(video.src)
          const width = video.videoWidth
          const height = video.videoHeight
          console.log('视频宽高:', width, height)
          this.form.width = width
          this.form.height = height
          resolve();
        }
      });
      // 等待视频的宽高和分辨率获取完成
      await loadedMetadata;

      // 计算文件的md5值
      const reader = new FileReader()
      const md5Promise = new Promise(resolve => {
        reader.onload = () => {
          const spark = new SparkMD5.ArrayBuffer()
          spark.append(reader.result)
          const md5 = spark.end(false)
          this.form.identifier = md5 // 将MD5值存储到form中
          resolve(md5);
        }
      });
      reader.readAsArrayBuffer(file); // 读取文件内容并计算MD5值
      const md5 = await md5Promise;

      // 检查文件是否已被上传
      const response = await testUploadVideo(md5);
      console.log("判断文件是否存在", response)
      if (response.msg === "文件已存在,秒传成功") {
        console.log("文件已存在")
        // 取消上传
        this.$refs.upload.abort(file);
        return false;
      } else {
        return true;
      }
    },
    chunkedUpload({ file }) {
      const totalSize = file.size
      const chunkCount = Math.ceil(totalSize / (5 * 1024 * 1024)) // 每个分片5MB
      // 创建分片上传请求数组
      // 上传分片
      for (let i = 0; i < chunkCount; i++) {
        const start = i * (5 * 1024 * 1024)
        const end = Math.min((i + 1) * (5 * 1024 * 1024), totalSize)
        const chunk = file.slice(start, end)
        const formData = new FormData()
        formData.append('file', chunk)
        formData.append('filename', file.name)
        formData.append('totalChunks', chunkCount)
        formData.append('chunkNumber', i)
        formData.append('identifier', this.form.identifier) // 添加文件的MD5值作为参数
        // 发送分片上传请求
        const source = axios.CancelToken.source() // 创建cancelToken
        const uploadPromise = this.uploadChunk(formData, source.token, (progressEvent) => {
          console.log('更新进度', progressEvent)
          this.uploadPercentage = Math.round((progressEvent.loaded / progressEvent.total) * 100) // 更新进度条的值;
        }).catch(error => {
          console.error('分片上传失败', error)
          // 弹出告警消息
          this.$message({
            type: 'error',
            message: '视频上传失败!'
          })
        })
        this.uploadPromises.push({ promise: uploadPromise, source }) // 保存cancelToken
      }
      // 等待所有分片上传完成
      return Promise.all(this.uploadPromises)
        .then(responses => {
          console.log('分片上传完成', responses)
        }).catch(error => {
          console.error('分片上传失败', error)
        })
    },
    /**更新进度*/
    onProgress(event, file) {
      this.uploadPercentage = Math.floor((event.loaded / event.total) * 100);
    },
    /**上传分片*/
    uploadChunk(formData, onProgress) {
      return axios.post(process.env.VUE_APP_BASE_API + '/manage/video/upload', formData, {
        onUploadProgress: onProgress // 添加进度回调
      }).then(response => {
        console.log('分片上传成功', response.data)
      })
    },
    /**上传分片失败*/
    onError(error, file, fileList) {
      console.error('上传失败', error)
    },
    // 取消上传请求
    beforeRemove(file, fileList) {
      this.form.identifier = null
      return true
    }
  }
}
</script>

2. 后端接口实现

2.1 控制层代码

@RestController
@RequestMapping("/manage/video")
@CrossOrigin // 允许跨域
public class ManageVideoController extends BaseController {
    @Autowired
    private IManageVideoService manageVideoService;

    /**
     * 上传分片前校验文件是否存在
     *
     * @return
     */
    @GetMapping("/preUpload")
    public AjaxResult preUpload(@RequestParam("fileMd5") String fileMd5) {
        return manageVideoService.checkExists(fileMd5);
    }

    /**
     * 上传分片
     *
     * @return
     */
    @PostMapping("/upload")
    public AjaxResult fragmentation(@ModelAttribute UploadPO uploadPO) {
        return manageVideoService.uploadChunk(uploadPO);
    }
}

2.1 服务层代码

接收到分片上传文件后经历以下步骤:

  1. 再次校验是否文件已存在,不存在就保存临时分片文件;
  2. 校验已上传分片数是否等于总分篇数,如果是则合并;
  3. 将临时文件合并和源mp4文件;
  4. 获取视频的时长和大小,因为ffmpeg不支持按照大小拆分,如果只是按照固定时长拆分,20s可能是2M也可能是34M,无法达到拆分视频以缩短预览视频等待时间的目的;
  5. 执行视频拆分,生成playlist.m3u8和一系列ts文件
  6. 重写m3u8文件的ts地址,1是因为若依开发环境和线上环境的指定前缀不一致,2是因为本地开发没开nginx转发静态资源,线上也没开文件服务
@Override
    public AjaxResult checkExists(String fileMd5) {
        String fileUploadDir = RuoYiConfig.getProfile() + "/video";
        //判断文件是否已被上传
        String videoFile = fileUploadDir + "/" + fileMd5 + ".mp4";
        File file = new File(videoFile);
        if (file.exists()) {
            return AjaxResult.success("文件已存在,秒传成功");
        }
        return AjaxResult.success();
    }

    @Override
    public AjaxResult uploadChunk(UploadPO uploadPO) {
        String fileUploadTempDir = RuoYiConfig.getProfile() + "/videotmp";
        String fileUploadDir = RuoYiConfig.getProfile() + "/video";
        // 获得文件分片数据
        MultipartFile fileData = uploadPO.getFile();
        // 分片第几片
        int index = uploadPO.getChunkNumber();
        //总分片数
        int totalChunk = uploadPO.getTotalChunks();
        // 文件md5标识
        String fileMd5 = uploadPO.getIdentifier();
        //判断文件是否已被上传
        String videoFile = fileUploadDir + "/" + fileMd5 + ".mp4";
        File file = new File(videoFile);
        if (file.exists()) {
            return AjaxResult.success("文件已存在,秒传成功");
        }
        String newName = fileMd5 + index + ".tem";
        File uploadFile = new File(fileUploadTempDir + "/" + fileMd5, newName);
        if (!uploadFile.getParentFile().exists()) {
            uploadFile.getParentFile().mkdirs();
        }
        try {
            fileData.transferTo(uploadFile);
            // 判断总分片数是否等于当前目录下的分片文件数量
            int currentChunkCount = getChunkCount(fileUploadTempDir + "/" + fileMd5);
            if (totalChunk == currentChunkCount) {
                // 调用合并方法
                merge(fileMd5, fileUploadTempDir, fileUploadDir);
                //根据运行环境分别调用ffmpeg
                String os = System.getProperty("os.name").toLowerCase();
                String m3u8Dir = fileUploadDir + "/" + fileMd5;
                File m3u8FileDir = new File(m3u8Dir);
                if (!m3u8FileDir.exists()) {
                    m3u8FileDir.mkdirs();
                }
                //计算视频总时长和视频大小,确定视频的分段时长
                String mp4File = fileUploadDir + "/" + fileMd5 + ".mp4";
                //每个2M分片的毫秒数
                long duration = getTsDuration(mp4File);
                // 异步执行视频拆分
                if (os.contains("win")) {
                    mp4ToM3u8ForWindow(fileMd5, mp4File, m3u8Dir, duration);
                } else {
                    mp4ToM3u8ForLinux(fileMd5, mp4File, m3u8Dir, duration);
                }
            }
            //执行成功返回 url
            return AjaxResult.success();
        } catch (IOException | InterruptedException e) {
            log.error("上传视频失败:{}", e.toString());
            FileUtil.del(fileUploadTempDir + "/" + fileMd5); //删除临时文件
            FileUtil.del(videoFile); //删除视频源文件
            FileUtil.del(fileUploadDir + "/" + fileMd5); //删除分段ts视频
            return AjaxResult.error(502, "上传视频失败");
        } catch (EncoderException e) {
            log.error("视频切割时计算分段时长失败:{}", e.toString());
            FileUtil.del(fileUploadTempDir + "/" + fileMd5); //删除临时文件
            FileUtil.del(videoFile); //删除视频源文件
            FileUtil.del(fileUploadDir + "/" + fileMd5); //删除分段ts视频
            return AjaxResult.error(502, "上传视频失败");
        }
    }

    /**
     * 获取当前目录下的分片文件数量
     *
     * @param directoryPath
     * @return
     */
    private int getChunkCount(String directoryPath) {
        File directory = new File(directoryPath);
        if (!directory.exists() || !directory.isDirectory()) {
            return 0;
        }
        File[] files = directory.listFiles((dir, name) -> name.endsWith(".tem"));
        return files != null ? files.length : 0;
    }

    /**
     * 合并分片
     *
     * @param uuid
     * @return
     */
    public void merge(String uuid, String fileUploadTempDir, String fileUploadDir) throws IOException {
        File dirFile = new File(fileUploadTempDir + "/" + uuid);
        //分片上传的文件已经位于同一个文件夹下,方便寻找和遍历(当文件数大于十的时候记得排序用冒泡排序确保顺序是正确的)
        String[] fileNames = dirFile.list();
        Arrays.sort(fileNames, (o1, o2) -> {
            int i1 = Integer.parseInt(o1.substring(o1.indexOf(uuid) + uuid.length()).split("\\.tem")[0]);
            int i2 = Integer.parseInt(o2.substring(o2.indexOf(uuid) + uuid.length()).split("\\.tem")[0]);
            return i1 - i2;
        });

        //创建空的合并文件,以未见md5为文件名
        File targetFile = new File(fileUploadDir, uuid + ".mp4");
        if (!targetFile.getParentFile().exists()) {
            targetFile.getParentFile().mkdirs();
        }
        RandomAccessFile writeFile = new RandomAccessFile(targetFile, "rw");
        long position = 0;
        for (String fileName : fileNames) {
            System.out.println(fileName);
            File sourceFile = new File(fileUploadTempDir + "/" + uuid, fileName);
            RandomAccessFile readFile = new RandomAccessFile(sourceFile, "rw");
            int chunksize = 1024 * 3;
            byte[] buf = new byte[chunksize];
            writeFile.seek(position);
            int byteCount;
            while ((byteCount = readFile.read(buf)) != -1) {
                if (byteCount != chunksize) {
                    byte[] tempBytes = new byte[byteCount];
                    System.arraycopy(buf, 0, tempBytes, 0, byteCount);
                    buf = tempBytes;
                }
                writeFile.write(buf);
                position = position + byteCount;
            }
            readFile.close();
        }
        writeFile.close();
        cn.hutool.core.io.FileUtil.del(dirFile);
    }

    /**
     * 视频拆分
     *
     * @param inputFilePath   D:/home/dxhh/uploadPath/video/md5.mp4
     * @param outputDirectory D:/home/dxhh/uploadPath/video/md5
     */
    @Async
    public void mp4ToM3u8ForWindow(String fileMd5, String inputFilePath, String outputDirectory, long ms) throws IOException {
        File uploadFile = new File(outputDirectory);
        if (!uploadFile.exists()) {
            uploadFile.mkdirs();
        }
        Path outputDirPath = Paths.get(outputDirectory);
        //我的ffmpeg.exe放在 项目的/resources/script目录下
        Path resourcePath = Paths.get("./script/ffmpeg.exe");
        FFmpeg.atPath(resourcePath.getParent())
                .addInput(UrlInput.fromPath(Paths.get(inputFilePath)))
                .addOutput(UrlOutput.toPath(outputDirPath.resolve("output_%03d.ts")))
                .addArguments("-f", "segment")
                .addArguments("-segment_time", ms + "ms") // 分片时长为30s
                .addArguments("-segment_list", outputDirPath.resolve("playlist.m3u8").toString())
                .addArguments("-c:v", "copy") // 优化视频编码参数
                .addArguments("-c:a", "copy") // 优化音频编码参数
                .execute();
        // 修改生成的m3u8文件,将ts链接替换为完整URL
        updateM3u8File(fileMd5, outputDirectory);
    }

    /**
     * 视频拆分
     *
     * @param fileMd5         adw1dwdadadwdadasd
     * @param inputFilePath   /home/dxhh/uploadPath/video/md5.mp4
     * @param outputDirectory /home/dxhh/uploadPath/video/md5
     * @throws IOException
     * @throws InterruptedException
     */
    public void mp4ToM3u8ForLinux(String fileMd5, String inputFilePath, String outputDirectory, long ms) throws IOException, InterruptedException {
        String command = "ffmpeg -i " + inputFilePath + " -c copy -map 0 -f segment -segment_time " + ms + "ms -segment_list " + outputDirectory + "/playlist.m3u8 " + outputDirectory + "/output_%03d.ts";
        //ffmpeg -i /home/dxhh/uploadPath/video/md5.mp4 -c copy -map 0 -f segment -segment_time 1236ms -segment_list /home/dxhh/uploadPath/video/md5/playlist.m3u8 /home/dxhh/uploadPath/video/md5/output_%03d.ts
        log.info("视频分割脚本:{}", command);
        ProcessBuilder builder = new ProcessBuilder(command.split(" "));
        builder.redirectErrorStream(true);
        Process process = builder.start();
        BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()));
        String line;
        while ((line = reader.readLine()) != null) {
            System.out.println(line);
        }
        int exitCode = process.waitFor();
        if (exitCode == 0) {
            System.out.println("FFmpeg command executed successfully");
            updateM3u8File(fileMd5, outputDirectory);
        } else {
            System.out.println("FFmpeg command failed with exit code " + exitCode);
        }
    }

    private void updateM3u8File(String fileMd5, String outputDirectory) throws IOException {
        String m3u8FilePath = outputDirectory + "/playlist.m3u8";
        List<String> lines = Files.readAllLines(Paths.get(m3u8FilePath));
        List<String> newLines = new ArrayList<>();
        for (String line : lines) {
            if (line.endsWith(".ts")) {
                if ("dev".equals(active)) {
                    newLines.add("/dev-api/profile/video/" + fileMd5 + "/" + line);
                } else {
                    newLines.add("/stage-api/profile/video/" + fileMd5 + "/" + line);
                }
            } else {
                newLines.add(line);
            }
        }
        Files.write(Paths.get(m3u8FilePath), newLines);
    }

   public long getTsDuration(String filePath) throws EncoderException {
        int targetSize = 2 * 1024 * 1024; // 2MB
        File videoFile = new File(filePath);
        long fileSize = videoFile.length();

        Encoder encoder = new Encoder();
        MultimediaInfo multimediaInfo = encoder.getInfo(videoFile);
        long duration = multimediaInfo.getDuration();
        System.out.println("Duration: " + duration + " ms");
        System.out.println("File size: " + fileSize + " bytes");

        // Calculate target duration for a 2MB video
        long targetDuration = (duration * targetSize) / fileSize;

        System.out.println("Target duration for a 2MB video: " + targetDuration + " ms");
        return targetDuration;
    }

获取视频时长需要用到jave工具包,想上传资源的提示已存在,应该可以在csdn搜到;
还需要ffmpeg软件,如果是windows环境运行,只需要调用本地的ffmpeg.exe就好,如果是在linux运行,需要安装ffmpeg;

   <!--视频切割-->
   <dependency>
       <groupId>com.github.kokorin.jaffree</groupId>
       <artifactId>jaffree</artifactId>
       <version>2023.09.10</version>
   </dependency>
   <dependency>
       <groupId>it.sauronsoftware.jave</groupId>
       <artifactId>jave2</artifactId>
       <version>1.0.2</version>
       <scope>system</scope>
       <systemPath>${project.basedir}/lib/jave-1.0.2.jar</systemPath>
   </dependency>

2.3 linux中安装ffmpeg

  1. 下载 ffmpeg 工具包并解压
wget http://www.ffmpeg.org/releases/ffmpeg-4.2.tar.gz
tar -zxvf ffmpeg-4.2.tar.gz
  1. 进入工具包文件夹并进行安装,将 ffmpeg 安装至 / usr/local/ffmpeg 下
 cd ffmpeg-4.2
 
./configure --prefix=/usr/local/ffmpeg
./configure --prefix=/usr/local/ffmpeg --enable-openssl --disable-x86asm
make && make install

注意:若出现以下报错,请跳至第五步,待第五步安装成功后再返回第二步。
在这里插入图片描述

  1. 配置环境变量,使其 ffmpeg 命令生效
 #利用vi编辑环境变量
vi /etc/profile
 
#在最后位置处添加环境变量,点击i进入编辑模式,esc键可退出编辑模式
export PATH=$PATH:/usr/local/ffmpeg/bin
 
#退出编辑模式后,:wq 保存退出
#刷新资源,使其生效
source /etc/profile
  1. 查看 ffmpeg 版本,验证是否安装成功
ffmpeg -version

若出现以下内容,则安装成功。

在这里插入图片描述

  1. 若第二步出现图片中的错误信息,则需要安装 yasm

记得退出 ffmpeg 工具包文件夹,cd … 返回上一层

 #下载yasm工具包
wget http://www.tortall.net/projects/yasm/releases/yasm-1.3.0.tar.gz
 
#解压
tar -zxvf yasm-1.3.0.tar.gz
 
#进入工具包文件夹并开始安装
cd yasm-1.3.0
./configure
make && make install

安装完成后直接返回第二步即可,此时命令就不会报错了。

2.4 视频资源地址

因为是基于若依框架开发的,其实只要上传的的时候是往 RuoYiConfig.getProfile() 这个指定配置目录保存文件,都是能直接访问不需要额外开发,这里就简单过一下
若依的自定义参数配置类从yml文件读取用户配置

@Component
@ConfigurationProperties(prefix = "xxx")
public class RuoYiConfig {
    /**
     * 上传路径 /home/user/xxxx/upload
     */
    private static String profile;
}

在通用配置定义一个静态资源路由前缀

/**
 * 通用常量定义
 *
 * @author li.dh
 */
public class CommonConstant {

    /**
     * 资源映射路径 前缀
     */
    public static final String RESOURCE_PREFIX = "/profile";
}

在mvc配置中添加静态资源的转发映射,将/profile前缀的请求转发到RuoYiConfig.getProfile()路径下

@Configuration
public class ResourcesConfig implements WebMvcConfigurer {

    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        /** 本地文件上传路径 */
        registry.addResourceHandler(CommonConstant.RESOURCE_PREFIX + "/**")
                .addResourceLocations("file:" + RuoYiConfig.getProfile() + "/");

        /** swagger配置 */
        registry.addResourceHandler("/swagger-ui/**")
                .addResourceLocations("classpath:/META-INF/resources/webjars/springfox-swagger-ui/")
                .setCacheControl(CacheControl.maxAge(5, TimeUnit.HOURS).cachePublic());
    }
}

3. vue播放流视频

我的需求是在列表上点击视频弹出播放弹窗

<!-- 播放视频 -->
    <el-dialog :title="title" :visible.sync="open" width="800px" append-to-body @close="open=false">
      <video-player class="video-player vjs-custom-skin"
                    ref="videoPlayer"
                    :playsinline="true"
                    :options="playerOptions"
      >
      </video-player>
    </el-dialog>
import 'video.js/dist/video-js.css'

data(){
	return {
		// 弹出层标题
      	title: '',
	    m3u8Url: '',
	    // 是否显示弹出层
	    open: false,
		playerOptions: {
	        playbackRates: [0.5, 1.0, 1.5, 2.0], // 可选的播放速度
	        autoplay: true, // 如果为true,浏览器准备好时开始回放。
	        muted: false, // 默认情况下将会消除任何音频。
	        loop: false, // 是否视频一结束就重新开始。
	        preload: 'auto', // 建议浏览器在<video>加载元素后是否应该开始下载视频数据。auto浏览器选择最佳行为,立即开始加载视频(如果浏览器支持)
	        language: 'zh-CN',
	        aspectRatio: '16:9', // 将播放器置于流畅模式,并在计算播放器的动态大小时使用该值。值应该代表一个比例 - 用冒号分隔的两个数字(例如"16:9"或"4:3")
	        fluid: true, // 当true时,Video.js player将拥有流体大小。换句话说,它将按比例缩放以适应其容器。
	        sources: [{
	          type: 'application/x-mpegURL', // 类型
	          src: this.m3u8Url
	        }],
	        poster: '', // 封面地址
	        notSupportedMessage: '此视频暂无法播放,请稍后再试', // 允许覆盖Video.js无法播放媒体源时显示的默认信息。
	        controlBar: {
	          timeDivider: true, // 当前时间和持续时间的分隔符
	          durationDisplay: true, // 显示持续时间
	          remainingTimeDisplay: false, // 是否显示剩余时间功能
	          fullscreenToggle: true // 是否显示全屏按钮
	        }
      }
	}
},
methods: {
   openVideo(picurl, url, title) {
     this.title = title
     let videourl = process.env.VUE_APP_BASE_API + url
     let imgurl = process.env.VUE_APP_BASE_API + picurl
     // console.log("视频地址:" , videourl)
     this.m3u8Url = videourl
     this.playerOptions.sources[0].src = videourl // 重新加载视频
     this.playerOptions.poster = imgurl // 封面
     // this.$refs.videoPlayer.play() // 播放视频
     this.open = true
   }
}

4. 实现效果

在这里插入图片描述

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

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

相关文章

使用 Socks5 来劫持 HTTPS(TCP-TLS) 之旅

MITM 劫持的过程中&#xff0c;HTTP 协议并不是唯一选择。 实际在 MITM 使用过程中&#xff0c;BurpSuite 和 Yakit 提供的交互式劫持工具只能劫持 HTTP 代理的 TLS 流量&#xff1b;但是这样是不够的&#xff0c;有时候我们并不能确保 HTTP 代理一定生效&#xff0c;或者说特…

力扣每日一题 ---- 2906. 构造乘积矩阵

这题很简单(一下就能想到是前缀和的提米)&#xff0c;但是在处理12345上面需要仔细一点&#xff0c;本来我最开始想到的时候全部累乘在除掉当前数&#xff0c;但是这样就没有把12345考虑进去&#xff0c;如果他本身是12345的话&#xff0c;那么除他以外的乘积并不一定是0&#…

CSS3 边框、圆角、背景

CSS3是最新的CSS标准。CSS3被拆分为“模块”。一些最重要的CSS3模块如下&#xff1a;选择器、盒模型、背景和边框、文字特效、2D/3D转换、动画、多列布局、用户界面。 一、CSS3边框&#xff1a; 用CSS3&#xff0c;可以创建圆角边框、添加阴影框&#xff0c;并作为边界的形象而…

碳酚醛防热复合材料渗透率测试方法及其精密压差控制解决方案

摘要&#xff1a;气体渗透率是树脂基纤维防热和烧蚀复合材料的关键性能参数&#xff0c;基于现有的稳态法渗透率测试技术相关研究报道&#xff0c;本文提出了更详细和切实可行的渗透率测试中的真空压力差精密控制解决方案。解决方案采用了两个真空度可精密控制的缓冲罐布置在被…

死亡游戏:密室互猜硬币规则及其破解方法

今天听到一个有点小恐怖的死亡游戏 规则是 将你和最好的朋友 分别关进两个不同的房间 要关 100天 在被关的时间里 你们无法进行任何的沟通 每一天 会有一个人在你和朋友的房间分别抛一次硬币 你们需要去猜对方硬币的正反面 只需要一个人猜对了 则 相安无事 如果两个人都猜错了…

nuxt3踩坑

1、安装nuxt3报错&#xff1a;Error: Failed to download template from registry: fetch failed &#xff08;1&#xff09;以管理员身份打开cmd &#xff08;2&#xff09;进入hosts所在目录&#xff0c;默认是在C:\Windows\System32\drivers\etc下&#xff1a; cd C:\Wi…

Mac代码文本编辑器Sublime Text 4

Sublime Text 4 for Mac拥有快速响应的功能&#xff0c;可以快速加载文件和执行命令&#xff0c;并提供多种语言支持&#xff0c;包括C 、Java、Python、HTML、CSS等。此外&#xff0c;该编辑器还支持LaTeX、Markdown、JSON、XML等技术领域。 Sublime Text 4 for Mac的插件丰富…

centos 上redis以及远程连接工具rdm安装与使用

目录 一 安装包准备 二 安装 三 启动 redis 四 rdm 连接 redis 一 安装包准备 redis 6.2.4 网盘资源&#xff1a; 链接: https://pan.baidu.com/s/1R120Va9FEyraLdiPe9fBHg?pwdgq9i 提取码: gq9i rdm 网盘资源&#xff1a; 链接: https://pan.baidu.com/s/1GiYnfIuQdSUmM…

解决EnableKeyword(“_Emission“)运行状态不起作用

我使用的是Standard Shader&#xff0c;一开始“_Emission”没有开启 当我运行下面的代码&#xff0c;可以看到Inspector窗口已经生效&#xff0c;然而物体本身并没有重新开始渲染&#xff0c;反射没有开启 public GameObject go; // Start is called before the first frame…

spring boot 中@Value读取中文配置时乱码

1.spring boot 读取application.properties 该文件是iso8859编码 如果是直接写中文 读取时会乱码 显示成?? 必须得转ascii码才能正常显示 其他方法测试也不行 Value("${apig.order.tiaokong.qianzi}") private String apigOrderTiaokongQianzi;

【k8s-1】基于docker Desktop一键式搭建k8s环境

在docker desktop中一键启动k8s环境很简单。 下面介绍如何启动dashboard&#xff0c;dashboard仪表盘是新手学习k8s至关重要的一个工具。 1、配置控制台 kubectl apply -f https://raw.githubusercontent.com/kubernetes/dashboard/v2.5.1/aio/deploy/recommended.yaml 2、开…

Bcc-tools 中文件系统相关工具介绍

之前简单介绍过了Linux下的工具集 bcc-tools&#xff0c; 本次详细的介绍一下和文件系统相关的一些实用工具&#xff0c;以下工具可以快速的帮我们定位文件系统相关问题&#xff0c;比如&#xff1a; 当前系统在读写哪些文件&#xff1f;哪些文件读写耗时比较长&#xff1f;哪…

electron+vite+vue3项目打包

1.安装打包依赖&#xff0c;调整打包命令 npm install electron-builder -D 2.安装完成后&#xff0c;在 package json 中&#xff0c;配置 build 命令 "build": "vite build && electron-builder", 3.electron/index.js修改 win.loadURL 中…

C语言指针详解与应用

写在前面&#xff1a;本文是基于哔哩哔哩江协科技的[C语言] 指针的详解与应用-理论结合实践&#xff0c;真正理解指针&#xff01;学习时写的笔记&#xff0c;复习查阅方便&#xff0c;如有侵权&#xff0c;联系删除。 另外本人也是初学者&#xff0c;有很多理解不透彻的或者错…

聚焦谋发展,筑梦新征程——云起无垠乔迁新址

2021年7月&#xff0c;网络安全新锐企业北京云起无垠科技有限公司&#xff08;以下简称&#xff1a;云起无垠&#xff09;注册成立。云起无垠致力于研究漏洞挖掘尖端技术和打造卓越漏挖工具&#xff0c;并在业界迅速崭露头角&#xff0c;受到了广泛瞩目。 发展至今&#xff0c…

如何查看笔记本电脑电池损耗

1.下载图吧工具箱 在官网下&#xff0c;不要下错了&#xff0c;不然会有很多垃圾捆绑软件&#xff0c;我放一个百度云链接&#xff0c;安装包上传上去了 链接&#xff1a;https://pan.baidu.com/s/18dguF5OGktbPkW7EszZZqA 提取码&#xff1a;1024 2.安装打开后点击主办工具-…

基于SSM的公务用车管理智慧云服务监管平台

末尾获取源码 开发语言&#xff1a;Java Java开发工具&#xff1a;JDK1.8 后端框架&#xff1a;SSM 前端&#xff1a;Vue 数据库&#xff1a;MySQL5.7和Navicat管理工具结合 服务器&#xff1a;Tomcat8.5 开发软件&#xff1a;IDEA / Eclipse 是否Maven项目&#xff1a;是 目录…

零基础产品经理如何迅速学习Axure原型制作?快速上手攻略!

如果Adobe xd是一个简单易用的UI设计软件&#xff0c;那么Axure应该是一个强大的原型软件。Axure不仅可以制作静态界面原型&#xff0c;还可以在此基础上增加交互效果。虽然Axure的功能比较复杂&#xff0c;但在学习和掌握之后&#xff0c;可以完美实现产品经理心目中的原型体验…

“产业大数据”助推园区实现可持续发展!

​产业园区在现代经济体系中扮演着重要角色&#xff0c;不仅是地方经济的重要支柱&#xff0c;更是企业发展的舞台。产业园区要想实现可持续的长远发展&#xff0c;不仅需要不断的招引优质企业入驻&#xff0c;更要时刻关注园内的企业&#xff0c;培育有潜力的企业&#xff0c;…

22款奔驰GLS450升级中规主机 实现导航地图 中文您好奔驰

很多平行进口的奔驰GLS都有这么一个问题&#xff0c;原车的地图在国内定位不了&#xff0c;语音交互功能也识别不了中文&#xff0c;原厂记录仪也减少了&#xff0c;使用起来也是很不方便的。星骏汇小许 Xjh15863 其实很简单&#xff0c;我们只需要更换一台中规的新主机就可以实…