FFmpeg的简单使用【Windows】--- 指定视频的时长

news2024/12/23 17:55:05

目录

功能描述

效果展示

代码实现

前端代码

后端代码

routers =》users.js

routers =》 index.js

app.js


功能描述

此案例是在上一个案例【FFmpeg的简单使用【Windows】--- 视频混剪+添加背景音乐-CSDN博客】的基础上的进一步完善,可以先去看上一个案例然后再看这一个,这些案例都是每次在上一个的基础上加一点功能。

在背景音乐区域先点击【选择文件】按钮上传生成视频的背景音乐素材;

然后在视频区域点击【选择文件】按钮选择要混剪的视频素材,最多可选择10个;

然后可以在文本框中输入你想要生成多长时间的视频,此处我给的默认值是 10s 即你要是不修改的话就是默认生成 10s 的视频;

最后点击【开始处理】按钮,此时会先将选择的视频素材上传到服务器,然后将视频按照指定的时间进行混剪并融合背景音乐。

效果展示

处理完毕的视频
上传的视频素材

代码实现

说明:

前端代码是使用vue编写的。

后端接口的代码是使用nodejs进行编写的。

前端代码

<template>
  <div id="app">
    <!-- 显示上传的音频 -->
    <div>
      <h2>上传的背景音乐</h2>
      <audio
        v-for="audio in uploadedaudios"
        :key="audio.src"
        :src="audio.src"
        controls
        style="width: 300px"
      ></audio>
    </div>

    <!-- 上传视频音频 -->
    <input type="file" @change="uploadaudio" accept="audio/*" />
    <hr />
    <!-- 显示上传的视频 -->
    <div>
      <h2>将要处理的视频</h2>
      <video
        v-for="video in uploadedVideos"
        :key="video.src"
        :src="video.src"
        controls
        style="width: 120px"
      ></video>
    </div>

    <!-- 上传视频按钮 -->
    <input type="file" @change="uploadVideo" multiple accept="video/*" />
    <hr />
    <h1>设置输出视频长度</h1>
    <input type="number" v-model="timer" class="inputStyle" />
    <hr />

    <!-- 显示处理后的视频 -->
    <div>
      <h2>已处理后的视频</h2>
      <video
        v-for="video in processedVideos"
        :key="video.src"
        :src="video.src"
        controls
        style="width: 120px"
      ></video>
    </div>

    <button @click="processVideos">开始处理</button>
  </div>
</template>

<script setup>
import axios from "axios";
import { ref } from "vue";

const uploadedaudios = ref([]);
const processedAudios = ref([]);
let audioIndex = 0;
const uploadaudio = async (e) => {
  const files = e.target.files;
  for (let i = 0; i < files.length; i++) {
    const file = files[i];
    const audioSrc = URL.createObjectURL(file);
    uploadedaudios.value = [{ id: audioIndex++, src: audioSrc, file }];
  }
  await processAudio();
};
// 上传音频
const processAudio = async () => {
  const formData = new FormData();
  for (const audio of uploadedaudios.value) {
    formData.append("audio", audio.file); // 使用实际的文件对象
  }
  try {
    const response = await axios.post(
      "http://localhost:3000/user/single/audio",
      formData,
      {
        headers: {
          "Content-Type": "multipart/form-data",
        },
      }
    );
    const processedVideoSrc = response.data.path;
    processedAudios.value = [
      {
        id: audioIndex++,
        src: processedVideoSrc,
      },
    ];
  } catch (error) {
    console.error("音频上传失败:", error);
  }
};

const uploadedVideos = ref([]);
const processedVideos = ref([]);
let videoIndex = 0;

const uploadVideo = async (e) => {
  const files = e.target.files;
  for (let i = 0; i < files.length; i++) {
    const file = files[i];
    const videoSrc = URL.createObjectURL(file);
    uploadedVideos.value.push({ id: videoIndex++, src: videoSrc, file });
  }
};
const timer = ref(10);

const processVideos = async () => {
  const formData = new FormData();
  formData.append("audioPath", processedAudios.value[0].src);
  formData.append("timer", timer.value);
  for (const video of uploadedVideos.value) {
    formData.append("videos", video.file); // 使用实际的文件对象
  }
  try {
    const response = await axios.post(
      "http://localhost:3000/user/process",
      formData,
      {
        headers: {
          "Content-Type": "multipart/form-data",
        },
      }
    );
    const processedVideoSrc = response.data.path;
    processedVideos.value.push({
      id: videoIndex++,
      src: "http://localhost:3000/" + processedVideoSrc,
    });
  } catch (error) {
    console.error("视频处理失败:", error);
  }
};
</script>
<style lang="scss" scoped>
.inputStyle {
  padding-left: 20px;
  font-size: 20px;
  line-height: 2;
  border-radius: 20px;
  border: 1px solid #ccc;
}
</style>

后端代码

说明:

此案例的核心就是针对于视频的输出长度的问题。
我在接口中书写的视频混剪的逻辑是每个视频中抽取的素材都是等长的,这就涉及到一个问题,将时间平均(segmentLength)到每个素材上的时候,有可能素材视频的长度(length)要小于avaTime,这样的话就会导致从这样的素材中随机抽取视频片段的时候有问题。

我的解决方案是这样的:

首先对视频片段进行初始化的抽取,如果segmentLength>length的时候,就将整个视频作为抽取的片段传入,如果segmentLength<length的时候再进行从该素材中随机抽取指定的视频片段。

当初始化完毕之后发现初始化分配之后的视频长度(totalLength)<设置的输出视频长度(timer),则通过不断从剩余的视频素材中随机选择片段来填补剩余的时间,直到总长度达到目标长度为止。每次循环都会计算剩余需要填补的时间,并从随机选择的视频素材中截取一段合适的长度。

routers =》users.js
var express = require('express');
var router = express.Router();
const multer = require('multer');
const ffmpeg = require('fluent-ffmpeg');
const path = require('path');
const { spawn } = require('child_process')
// 视频
const upload = multer({
  dest: 'public/uploads/',
  storage: multer.diskStorage({
    destination: function (req, file, cb) {
      cb(null, 'public/uploads'); // 文件保存的目录
    },
    filename: function (req, file, cb) {
      // 提取原始文件的扩展名
      const ext = path.extname(file.originalname).toLowerCase(); // 获取文件扩展名,并转换为小写
      // 生成唯一文件名,并加上扩展名
      const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1E9);
      const fileName = uniqueSuffix + ext; // 新文件名
      cb(null, fileName); // 文件名
    }
  })
});
// 音频
const uploadVoice = multer({
  dest: 'public/uploadVoice/',
  storage: multer.diskStorage({
    destination: function (req, file, cb) {
      cb(null, 'public/uploadVoice'); // 文件保存的目录
    },
    filename: function (req, file, cb) {
      // 提取原始文件的扩展名
      const ext = path.extname(file.originalname).toLowerCase(); // 获取文件扩展名,并转换为小写
      // 生成唯一文件名,并加上扩展名
      const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1E9);
      const fileName = uniqueSuffix + ext; // 新文件名
      cb(null, fileName); // 文件名
    }
  })
});

const fs = require('fs');

// 处理文件上传
router.post('/upload', upload.single('video'), (req, res) => {
  const videoPath = req.file.path;
  const originalName = req.file.originalname;
  const filePath = path.join('uploads', originalName);
  fs.rename(videoPath, filePath, (err) => {
    if (err) {
      console.error(err);
      return res.status(500).send("Failed to move file.");
    }
    res.json({ message: 'File uploaded successfully.', path: filePath });
  });
});

// 处理单个视频文件
router.post('/single/process', upload.single('video'), (req, res) => {
  console.log(req.file)
  const videoPath = req.file.path;
  // 使用filename进行拼接是为了防止视频被覆盖
  const outputPath = `public/processed/reversed_${req.file.filename}`;

  ffmpeg()
    .input(videoPath)
    .outputOptions([
      '-vf reverse'// 反转视频帧顺序
    ])
    .output(outputPath)
    .on('end', () => {
      res.json({ message: 'Video processed successfully.', path: outputPath.replace('public', '') });
    })
    .on('error', (err) => {
      console.log(err)
      res.status(500).json({ error: 'An error occurred while processing the video.' });
    })
    .run();
});


// 处理多个视频文件上传
router.post('/process', upload.array('videos', 10), (req, res) => {
  // 要添加的背景音频
  const audioPath = path.join(path.dirname(__filename).replace('routes', 'public'), req.body.audioPath)
  //要生成多长时间的视频
  const { timer } = req.body

  // 格式化上传的音频文件的路径
  const videoPaths = req.files.map(file => path.join(path.dirname(__filename).replace('routes', 'public/uploads'), file.filename));

  // 输出文件路径
  const outputPath = path.join('public/processed', 'merged_video.mp4');
  // 要合并的视频片段文件
  const concatFilePath = path.resolve('public', 'concat.txt').replace(/\\/g, '/');//绝对路径

  // 创建 processed 目录(如果不存在)
  if (!fs.existsSync("public/processed")) {
    fs.mkdirSync("public/processed");
  }

  // 计算每个视频的长度
  const videoLengths = videoPaths.map(videoPath => {
    return new Promise((resolve, reject) => {
      ffmpeg.ffprobe(videoPath, (err, metadata) => {
        if (err) {
          reject(err);
        } else {
          resolve(parseFloat(metadata.format.duration));
        }
      });
    });
  });

  // 等待所有视频长度计算完成
  Promise.all(videoLengths).then(lengths => {
    console.log('lengths', lengths)
    // 构建 concat.txt 文件内容
    let concatFileContent = '';
    // 定义一个函数来随机选择视频片段
    function getRandomSegment(videoPath, length, segmentLength) {
      // 如果该素材的长度小于截取的长度,则直接返回整个视频素材
      if (segmentLength >= length) {
        return {
          videoPath,
          startTime: 0,
          endTime: length
        };
      }
      const startTime = Math.floor(Math.random() * (length - segmentLength));
      return {
        videoPath,
        startTime,
        endTime: startTime + segmentLength
      };
    }

    // 随机选择视频片段
    const segments = [];
    let totalLength = 0;

    // 初始分配
    for (let i = 0; i < lengths.length; i++) {
      const videoPath = videoPaths[i];
      const length = lengths[i];
      const segmentLength = Math.min(timer / lengths.length, length);
      const segment = getRandomSegment(videoPath, length, segmentLength);
      segments.push(segment);
      totalLength += (segment.endTime - segment.startTime);
    }
    console.log("初始化分配之后的视频长度", totalLength)

    /* 
      这段代码的主要作用是在初始分配后,如果总长度 totalLength 小于目标长度 targetLength,则通过不断从剩余的视频素材中随机选择片段来填补剩余的时间,直到总长度达到目标长度为止。每次循环都会计算剩余需要填补的时间,并从随机选择的视频素材中截取一段合适的长度。
    */
    // 如果总长度小于目标长度,则从剩余素材中继续选取随机片段
    while (totalLength < timer) {
      // 计算还需要多少时间才能达到目标长度
      const remainingTime = timer - totalLength;
      // 从素材路径数组中随机选择一个视频素材的索引
      const videoIndex = Math.floor(Math.random() * videoPaths.length);
      // 根据随机选择的索引,获取对应的视频路径和长度
      const videoPath = videoPaths[videoIndex];
      const length = lengths[videoIndex];

      // 确定本次需要截取的长度
      // 这个长度不能超过剩余需要填补的时间,也不能超过素材本身的长度,因此选取两者之中的最小值
      const segmentLength = Math.min(remainingTime, length);

      // 生成新的视频片段
      const segment = getRandomSegment(videoPath, length, segmentLength);
      // 将新生成的视频片段对象添加到片段数组中
      segments.push(segment);
      // 更新总长度
      totalLength += (segment.endTime - segment.startTime);
    }

    // 打乱视频片段的顺序
    function shuffleArray(array) {
      for (let i = array.length - 1; i > 0; i--) {
        const j = Math.floor(Math.random() * (i + 1));
        [array[i], array[j]] = [array[j], array[i]];
      }
      return array;
    }

    shuffleArray(segments);

    // 构建 concat.txt 文件内容
    segments.forEach(segment => {
      concatFileContent += `file '${segment.videoPath.replace(/\\/g, '/')}'\n`;
      concatFileContent += `inpoint ${segment.startTime}\n`;
      concatFileContent += `outpoint ${segment.endTime}\n`;
    });

    fs.writeFileSync(concatFilePath, concatFileContent, 'utf8');

    // 获取视频总时长
    const totalVideoDuration = segments.reduce((acc, segment) => acc + (segment.endTime - segment.startTime), 0);
    console.log("最终要输出的视频总长度为", totalVideoDuration)

    // 获取音频文件的长度
    const getAudioDuration = (filePath) => {
      return new Promise((resolve, reject) => {
        const ffprobe = spawn('ffprobe', [
          '-v', 'error',
          '-show_entries', 'format=duration',
          '-of', 'default=noprint_wrappers=1:nokey=1',
          filePath
        ]);

        let duration = '';

        ffprobe.stdout.on('data', (data) => {
          duration += data.toString();
        });

        ffprobe.stderr.on('data', (data) => {
          console.error(`ffprobe stderr: ${data}`);
          reject(new Error(`Failed to get audio duration`));
        });

        ffprobe.on('close', (code) => {
          if (code !== 0) {
            reject(new Error(`FFprobe process exited with code ${code}`));
          } else {
            resolve(parseFloat(duration.trim()));
          }
        });
      });
    };
    getAudioDuration(audioPath).then(audioDuration => {
      // 计算音频循环次数
      const loopCount = Math.floor(totalVideoDuration / audioDuration);

      // 使用 ffmpeg 合并多个视频
      ffmpeg()
        .input(audioPath) // 添加音频文件作为输入
        .inputOptions([
          `-stream_loop ${loopCount}`, // 设置音频循环次数
        ])
        .input(concatFilePath)
        .inputOptions([
          '-f concat',
          '-safe 0'
        ])
        .output(outputPath)
        .outputOptions([
          '-y', // 覆盖已存在的输出文件
          '-c:v libx264', // 视频编码器
          '-preset veryfast', // 编码速度
          '-crf 23', // 视频质量控制
          '-map 0:a', // 选择第一个输入(即音频文件)的音频流
          '-map 1:v', // 选择所有输入文件的视频流(如果有)
          '-c:a aac', // 音频编码器
          '-b:a 128k', // 音频比特率
          '-t', totalVideoDuration.toFixed(2), // 设置输出文件的总时长为视频的时长
        ])
        .on('end', () => {
          const processedVideoSrc = `/processed/merged_video.mp4`;
          console.log(`Processed video saved at: ${outputPath}`);
          res.json({ message: 'Videos processed and merged successfully.', path: processedVideoSrc });
        })
        .on('error', (err) => {
          console.error(`Error processing videos: ${err}`);
          console.error('FFmpeg stderr:', err.stderr);
          res.status(500).json({ error: 'An error occurred while processing the videos.' });
        })
        .run();
    }).catch(err => {
      console.error(`Error getting audio duration: ${err}`);
      res.status(500).json({ error: 'An error occurred while processing the videos.' });
    });
  }).catch(err => {
    console.error(`Error calculating video lengths: ${err}`);
    res.status(500).json({ error: 'An error occurred while processing the videos.' });
  });

  // 写入 concat.txt 文件
  const concatFileContent = videoPaths.map(p => `file '${p.replace(/\\/g, '/')}'`).join('\n');
  fs.writeFileSync(concatFilePath, concatFileContent, 'utf8');
});

// 处理单个音频文件
router.post('/single/audio', uploadVoice.single('audio'), (req, res) => {
  const audioPath = req.file.path;
  console.log(req.file)
  res.send({
    msg: 'ok',
    path: audioPath.replace('public', '').replace(/\\/g, '/')
  })
})
module.exports = router;
routers =》 index.js
var express = require('express');
var router = express.Router();

router.use('/user', require('./users'));

module.exports = router;
app.js
var createError = require('http-errors');
var express = require('express');
var path = require('path');
var cookieParser = require('cookie-parser');
var logger = require('morgan');

var indexRouter = require('./routes/index');

var app = express();

// view engine setup
app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'jade');

app.use(logger('dev'));
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.use(cookieParser());
app.use(express.static(path.join(__dirname, 'public')));

// 使用cors解决跨域问题
app.use(require('cors')());

app.use('/', indexRouter);

// catch 404 and forward to error handler
app.use(function (req, res, next) {
  next(createError(404));
});

// error handler
app.use(function (err, req, res, next) {
  // set locals, only providing error in development
  res.locals.message = err.message;
  res.locals.error = req.app.get('env') === 'development' ? err : {};

  // render the error page
  res.status(err.status || 500);
  res.render('error');
});

module.exports = app;

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

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

相关文章

Scrapy | 使用Scrapy进行数据建模和请求

scrapy数据建模与请求 数据建模1.1 为什么建模1.2 如何建模1.3如何使用模板类1.4 开发流程总结 目标&#xff1a; 1.应用在scrapy项目中进行建模 2.应用构造Request对象&#xff0c;并发送请求 3.应用利用meta参数在不同的解析函数中传递数据 数据建模 | 通常在做项目的过程中…

周易解读:八卦01,八卦的卦象与记忆口诀

八卦01 在前面呢&#xff0c;我们是讲解了太极&#xff0c;两仪&#xff0c;四象的知识。这些个知识呢&#xff0c;两仪还算是比较有用一些。太极&#xff0c;四象&#xff0c;其实就是一种了解性的知识。而本节的八卦的知识&#xff0c;那可就不一样了。八卦的知识&#xff0…

nvm安装,node多版本管理

卸载nodejs win R 输入 appwiz.cpl 删除 node.js查看node.js安装路径是否有残留&#xff0c;有就删除文件夹 删除下列路径文件&#xff0c;一定要检查&#xff0c;没删干净&#xff0c;nvm安装会失败 C:\Program Files (x86)\NodejsC:\Program Files\NodejsC:\Users{User}\…

04 线性结构——栈(特性、进栈与出栈、栈顶指针、顺序栈和链式栈、相关功能的定义与代码实现)

目录 1 栈的定义 2 相关概念 2.1 栈顶&#xff08;Top&#xff09; 2.2 栈底&#xff08;Bottom&#xff09; 2.3 空栈 2.4 栈的序列 2.5 出栈入栈与栈顶指针移动的关系 2.5.1 初始情况 top -1 2.5.2 初始情况 top 0 3 栈的应用&#xff1a;函数调用 4 栈的存储结…

一个月学会Java 第17天 常用类(包装类、数学类、日期类、可变长字符串等等)

Day17 常用类(包装类、数学类、日期类、可变长字符串等等) 这一节会非常的简单和轻松&#xff0c;可以当做今天的学习就是在放松自己&#xff0c;因为这些常用类只需要记住类名和大概的方法在里面即可&#xff0c;翻一下看一下方法名都可以看出来的 目录 包装类数学类Math日期…

五、【智能体】满满干货,RAG技术在智能体中的神奇应用,你知道几个?

1. 智能问答系统 RAG技术广泛应用于问答系统中&#xff0c;特别是在需要即时获取最新信息或专业知识的场合。传统生成模型可能无法应对最新或特定领域的复杂问题&#xff0c;而RAG通过检索相关资料并增强生成过程&#xff0c;能够提供准确、具体的回答。 客户服务&#xff1a…

OpenCV-人脸检测

文章目录 一、人脸检测流程二、关键方法三、代码示例四、注意事项 OpenCV是一个开源的计算机视觉和机器学习软件库&#xff0c;它提供了多种人脸检测方法&#xff0c;以下是对OpenCV人脸检测的详细介绍&#xff1a; 一、人脸检测流程 人脸检测是识别图像中人脸位置的过程&…

2023年甘肃省职业院校技能大赛 网络建设与运维赛项 网络搭建及安全部署竞赛报告单

2023年甘肃省职业院校技能大赛 网络建设与运维赛项 网络搭建及安全部署竞赛报告单 示例&#xff1a;(仅供参考&#xff0c;具体以正式赛卷报告单为准) 在SW1上执行show vlan命令收集信息&#xff0c;截图如下&#xff1a; 请注意尽量保证格式、图片排列整齐 请注意尽量窗…

IO进程---day3

1、完成标准io的单字符实现两个文件的拷贝&#xff1b; #include<myhead.h>//标准io的单字符现两个文件的拷贝&#xff1b; int main(int argc, const char *argv[]) {//判断是否有3个文件传入if(3 ! argc){fputs("input file error\n",stderr);return -1;}//打…

测试教程分享

前几年在腾讯课堂上发布了不少课程&#xff0c;后来腾讯课堂改革&#xff0c;要收会员费&#xff0c;课程还要抽提程&#xff0c;这么下来就相当于白干了。就放弃了在上面发课程&#xff0c;再后来腾讯课堂就关闭了&#xff0c;以前发布的视频就没有地方发了&#xff0c;于是我…

IDEA中git如何快捷的使用Cherry-Pick功能

前言 我们在使用IDEA开发时&#xff0c;一般是使用GIT来管理我们的代码&#xff0c;有时候&#xff0c;我们需要在我们开发的主分支上合并其他分支的部分提交代码。注意&#xff0c;是部分&#xff0c;不是那个分支的全部提交&#xff0c;这时候&#xff0c;我们就需要使用Che…

在Linux中搭建WordPress并实现Windows主机远程访问

WordPreWordPress是一个基于PHP开发的开源平台&#xff0c;适用于在支持PHP与MySQL数据库的服务器上搭建个性化博客或网站。同时&#xff0c;它也能够作为功能强大的内容管理系统&#xff08;CMS&#xff09;被广泛应用。 虚拟机&#xff1a;VirtualBox 虚拟机安装&#x1f449…

【更新】中国地区粮食播种、粮食产量、灾害等数据(1990-2023年)

数据为中国地区粮食播种、粮食产量、灾害等数据&#xff0c;包括369个指标&#xff0c;各类农作物播种面积、粮食产量、牲畜饲养、受灾面积等。这些指标综合反映了中国农业生产、粮食安全的相关情况 一、数据介绍 数据名称&#xff1a;中国地区粮食播种、粮食产量、灾害等数据…

RHCE---使用邮箱客户端s-nail

使用邮箱客户端s-nail 方案一&#xff1a;使用网易邮箱 1&#xff0c;挂载虚拟镜像 [rootlocalhost ~]# mount /dev/sr1 /mnt mount: /mnt: /dev/sr1 already mounted on /run/media/root/RHEL-9-3-0-BaseOS-x86_64.2&#xff0c;编辑环境文件 [rootlocalhost ~]# vim /et…

部署harbor问题(缺少ssl认证证书)

在部署harbor服务&#xff0c;/install.sh启动时&#xff0c;缺少ssl认证 1. 创建证书目录 首先&#xff0c;创建 /usr/local/harbor/ssl 目录&#xff1a; mkdir -p /usr/local/harbor/ssl 2. 生成私钥 生成一个 4096 位的 RSA 私钥&#xff1a; openssl genrsa -out /us…

【优秀Python大屏】全球肺癌患病人数分析与可视化展示

1. 项目背景 肺癌是全球范围内影响人类健康的重大疾病之一&#xff0c;了解不同地区、不同收入水平国家的肺癌患病人数分布以及不同年龄段的患病趋势&#xff0c;有助于全球卫生组织和研究人员制定更有效的防治策略。本次数据分析利用全球各洲和国家的肺癌患病数据&#xff0c…

了解专用代理服务器的功能

在当今数字化的环境中&#xff0c;确保安全高效的互联网连接变得至关重要。这种需求催生了专用代理服务器&#xff0c;这是一种确保在线隐私、安全和可访问性的强大工具。了解专用代理服务器的细微差别和功能对于寻求增强在线保护和访问的个人和企业是十分重要的。 一、什么是…

中标麒麟v5安装qt512.12开发软件

注意 需要联网操作 遇到问题1&#xff1a;yum提示没有可用软件包问题 终端执行如下命令 CentOS7将yum源更换为国内源保姆级教程 中标麒麟V7-yum源的更换&#xff08;阿里云源&#xff09; wget -O /etc/yum.repos.d/CentOS-Base.repo http://mirrors.aliyun.com/repo/Cento…

Ingress-nginx中HTTPS的强制转发

文章目录 在使用aws 的NLB转发流量到ingress时&#xff0c;发现NLP上生成的转发配置不符合正常预期&#xff0c;如下图&#xff1a; ingress-nginx service 配置如下&#xff1a; apiVersion: v1 kind: Service metadata:annotations:service.beta.kubernetes.io/aws-load-b…

无人机集群路径规划:5种优化算法(SFOA、APO、GOOSE、CO、PIO)求解无人机集群路径规划,提供MATLAB代码

一、单个无人机路径规划模型介绍 无人机三维路径规划是指在三维空间中为无人机规划一条合理的飞行路径&#xff0c;使其能够安全、高效地完成任务。路径规划是无人机自主飞行的关键技术之一&#xff0c;它可以通过算法和模型来确定无人机的航迹&#xff0c;以避开障碍物、优化…