【视频混剪Demo】FFmpeg的利用【Windows】

[复制链接]
发表于 2026-1-4 12:31:41 | 显示全部楼层 |阅读模式

马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。

您需要 登录 才可以下载或查看,没有账号?立即注册

×
#1024步伐员节 | 征文#

目次
一、简介
二、音频素材页
2.1 功能形貌
  搜索
  添加
  删除
2.2 结果展示
2.3 代码实现
  前端
  后端
三、视频素材页
3.1 功能形貌
  搜索
  添加
  编辑
  删除
3.2 结果展示
3.3 代码实现
  前端
  后端
四、分组管理页
4.1 功能形貌
  搜索
  添加
  编辑
  删除
4.2 结果展示
4.3 代码实现
  前端
  后端
五、视频混剪页
5.1 功能形貌
  搜索
  添加
  删除
5.2 结果展示
5.3 代码实现
  前端
  后端


一、简介

此篇文章带来的是 利用 FFmpeg 实现视频混剪而且可以指定天生视频的数目以及天生视频的时长,此案例也是对前边几篇文章功能的综合。
   阐明:
  此案例中的代码部分,前端利用的是 Vue + ElementPlus + Vite、后端利用的是 Nodejs + FFmpeg + MySQL
  
  下方提供的代码为每一部分的核心代码,假如须要案例的完备代码,可以在批评区留言我来私信你!!!
  二、音频素材页

2.1 功能形貌

搜索

在搜索栏中输入 音频名称、上传时间 后点击搜索按钮即可,也可以只输入一个条件搜索。
添加

点击页面上的【添加音频】按钮,然后在弹出的上传音频对话框中输入 音频名称,并选择要上传的音频文件之后直接点击确定按钮即可完成音频文件的上传。
删除

点击表格中的【删除】按钮即可删除对应的音频文件
2.2 结果展示

   
     音频素材页面      
     点击上一张图片中的添加音频按钮弹出的对话框      
     选择要上传的音频文件    2.3 代码实现

前端

  1. <template>
  2.   <div>
  3.     <el-button type="primary" @click="dialogVisible = true">添加音频</el-button>
  4.     <el-row style="margin: 10px 0">
  5.       <el-form v-model="queryParams" inline>
  6.         <el-form-item label="音频名称">
  7.           <el-input
  8.             v-model="queryParams.name"
  9.             placeholder="输入要查询的文件名称"
  10.             style="width: 200px"
  11.           ></el-input>
  12.         </el-form-item>
  13.         <el-form-item label="上传时间">
  14.           <el-date-picker
  15.             v-model="daterange"
  16.             type="daterange"
  17.             unlink-panels
  18.             range-separator="至"
  19.             start-placeholder="开始日期"
  20.             end-placeholder="结束日期"
  21.             :shortcuts="shortcuts"
  22.           />
  23.         </el-form-item>
  24.       </el-form>
  25.       <div style="margin-left: 20px">
  26.         <el-button type="primary" @click="handleSearch">搜索</el-button>
  27.         <el-button type="info" plain @click="handleReset">重置</el-button>
  28.       </div>
  29.     </el-row>
  30.     <el-table
  31.       :data="tableData"
  32.       stripe
  33.       border
  34.       :style="{ height: tableHeight ? tableHeight + 'px' : '' }"
  35.     >
  36.       <el-table-column prop="id" label="ID" align="center"  />
  37.       <el-table-column prop="name" label="音频名称" align="center" />
  38.       <el-table-column
  39.         prop="path"
  40.         label="音频文件"
  41.         align="center"
  42.         min-
  43.       >
  44.         <template #default="scope">
  45.           <audio :src="scope.row.path" controls class="audioStyle"></audio>
  46.         </template>
  47.       </el-table-column>
  48.       <el-table-column
  49.         prop="type"
  50.         label="音频类型"
  51.         align="center"
  52.         
  53.       />
  54.       <el-table-column prop="size" label="文件大小" align="center" >
  55.         <template #default="scope"> {
  56.   
  57.   { scope.row.size }}M </template>
  58.       </el-table-column>
  59.       <el-table-column
  60.         prop="upload_time"
  61.         label="上传时间"
  62.         align="center"
  63.         
  64.       />
  65.       <el-table-column label="操作" align="center"  fixed="right">
  66.         <template #default="scope">
  67.           <el-button
  68.             v-if="choose"
  69.             type="success"
  70.             size="small"
  71.             @click="handleChoose(scope.row)"
  72.             >选择</el-button
  73.           >
  74.           <el-button
  75.             v-else
  76.             type="danger"
  77.             size="small"
  78.             @click="handleDelete(scope.row)"
  79.             >删除</el-button
  80.           >
  81.         </template>
  82.       </el-table-column>
  83.     </el-table>
  84.     <el-row style="margin-top: 10px; justify-content: end; align-items: center">
  85.       <span style="margin-right: 20px; color: #606266">共 {
  86.   
  87.   { total }} 条</span>
  88.       <el-pagination
  89.         background
  90.         layout="prev, pager, next"
  91.         :page-size="queryParams.size"
  92.         :total="total"
  93.         @current-change="handleCurrentChange"
  94.       ></el-pagination>
  95.     </el-row>
  96.     <!-- 添加音频对话框 -->
  97.     <el-dialog v-model="dialogVisible" title="上传音频">
  98.       <el-form :model="formData" :rules="rules" ref="ruleFormRef">
  99.         <el-form-item label="音频名称" prop="name">
  100.           <el-input
  101.             v-model="formData.name"
  102.             placeholder="请输入音频名称"
  103.           ></el-input>
  104.         </el-form-item>
  105.         <el-form-item label="音频文件" prop="file">
  106.           <el-input
  107.             v-model="formData.path"
  108.             placeholder="请选择音频文件"
  109.             class="input-with-select"
  110.             disabled
  111.           >
  112.             <template #append>
  113.               <el-upload
  114.                 v-model:file-list="fileList"
  115.                 action=""
  116.                 :limit="1"
  117.                 :before-upload="beforeUpload"
  118.                 :show-file-list="false"
  119.                 accept="audio/*"
  120.               >
  121.                 <el-button :icon="Upload" class="upload-btn">上传</el-button>
  122.               </el-upload>
  123.             </template>
  124.           </el-input>
  125.         </el-form-item>
  126.       </el-form>
  127.       <template #footer>
  128.         <div class="dialog-footer">
  129.           <el-button @click="handleCancel">取消</el-button>
  130.           <el-button type="primary" @click="handleSubmit"> 确定 </el-button>
  131.         </div>
  132.       </template>
  133.     </el-dialog>
  134.   </div>
  135. </template>
  136. <script setup>
  137. import { onMounted, ref } from "vue";
  138. import { Upload } from "@element-plus/icons-vue";
  139. import { post, get, del } from "@/utils/http";
  140. import { useStore } from "@/store";
  141. const porp = defineProps(["choose"]);
  142. const store = useStore();
  143. const tableData = ref([]);
  144. const total = ref(0);
  145. // 查询条件
  146. const queryParams = ref({
  147.   name: undefined,
  148.   startTime: undefined,
  149.   endTime: undefined,
  150.   page: 1,
  151.   size: 10,
  152. });
  153. const daterange = ref([]);
  154. const shortcuts = [
  155.   {
  156.     text: "最近一周",
  157.     value: () => {
  158.       const end = new Date();
  159.       const start = new Date();
  160.       start.setTime(start.getTime() - 3600 * 1000 * 24 * 7);
  161.       return [start, end];
  162.     },
  163.   },
  164.   {
  165.     text: "最近一个月",
  166.     value: () => {
  167.       const end = new Date();
  168.       const start = new Date();
  169.       start.setTime(start.getTime() - 3600 * 1000 * 24 * 30);
  170.       return [start, end];
  171.     },
  172.   },
  173.   {
  174.     text: "最近三个月",
  175.     value: () => {
  176.       const end = new Date();
  177.       const start = new Date();
  178.       start.setTime(start.getTime() - 3600 * 1000 * 24 * 90);
  179.       return [start, end];
  180.     },
  181.   },
  182. ];
  183. // 获取音频列表
  184. const getList = () => {
  185.   get("/audio/list", queryParams.value).then((res) => {
  186.     tableData.value = res.data;
  187.     total.value = res.total;
  188.   });
  189. };
  190. // 搜索
  191. const handleSearch = () => {
  192.   queryParams.value.startTime = daterange.value[0];
  193.   queryParams.value.endTime = daterange.value[1];
  194.   console.log(queryParams.value);
  195.   getList();
  196. };
  197. // 重置
  198. const handleReset = () => {
  199.   queryParams.value = {
  200.     name: undefined,
  201.     startTime: undefined,
  202.     endTime: undefined,
  203.     page: 1,
  204.     size: 10,
  205.   };
  206.   daterange.value = [];
  207.   getList();
  208. };
  209. // 切换页
  210. const handleCurrentChange = (page) => {
  211.   queryParams.value.page = page;
  212.   getList();
  213. };
  214. const dialogVisible = ref(false); //弹出对话框是否显示
  215. const fileList = ref([]);
  216. const ruleFormRef = ref(null);
  217. const formData = ref({
  218.   name: "",
  219.   path: "",
  220.   file: "",
  221. });
  222. // 校验规则
  223. const rules = {
  224.   name: [{ required: true, message: "请输入音频名称", trigger: "blur" }],
  225.   file: [
  226.     { required: true, message: "请选择要上传的音频文件", trigger: "blur" },
  227.   ],
  228. };
  229. // 点击取消按钮
  230. const handleCancel = () => {
  231.   dialogVisible.value = false;
  232.   // 重置表单
  233.   formData.value = {
  234.     name: "",
  235.     file: "",
  236.   };
  237. };
  238. // 点击确定按钮
  239. const handleSubmit = () => {
  240.   const data = new FormData();
  241.   data.append("name", formData.value.name);
  242.   data.append("audio", formData.value.file);
  243.   console.log(formData.value);
  244.   // 先进行表单的验证
  245.   ruleFormRef.value.validate((valid) => {
  246.     if (valid) {
  247.       post("/audio/single/audio", data).then((res) => {
  248.         ElMessage.success("上传成功");
  249.         console.log("上传成功", res);
  250.         dialogVisible.value = false;
  251.         getList();
  252.       });
  253.     }
  254.   });
  255. };
  256. // 上传文件之前
  257. const beforeUpload = (file) => {
  258.   formData.value.path = URL.createObjectURL(file);
  259.   formData.value.file = file;
  260.   return false;
  261. };
  262. // 删除
  263. const handleDelete = (item) => {
  264.   del(`/audio/delete/${item.id}`).then((res) => {
  265.     ElMessage.success("删除成功");
  266.     getList();
  267.   });
  268. };
  269. // 选择音频
  270. const handleChoose = (item) => {
  271.   store.chooseBgm(item);
  272.   store.setAudioDialog();
  273. };
  274. onMounted(() => {
  275.   getList();
  276. });
  277. </script>
  278. <style lang="scss" scoped>
  279. .el-table {
  280.   margin-top: 10px;
  281. }
  282. .el-button.upload-btn {
  283.   height: 100%;
  284.   background: #f56c6c;
  285.   color: #fff;
  286.   &:hover {
  287.     background: #f89898;
  288.     color: #fff;
  289.   }
  290. }
  291. .el-input-group__append > div {
  292.   display: flex;
  293. }
  294. .audioStyle {
  295.   width: 250px;
  296.   height: 40px;
  297. }
  298. </style>
复制代码
后端

  1. var express = require('express');
  2. var router = express.Router();
  3. const multer = require('multer');
  4. const path = require('path');
  5. const connection = require('../config/db.config')
  6. const baseURL = 'http://localhost:3000'
  7. // 音频
  8. const uploadVoice = multer({
  9.   dest: 'public/uploadVoice/',
  10.   storage: multer.diskStorage({
  11.     destination: function (req, file, cb) {
  12.       cb(null, 'public/uploadVoice'); // 文件保存的目录
  13.     },
  14.     filename: function (req, file, cb) {
  15.       // 提取原始文件的扩展名
  16.       const ext = path.extname(file.originalname).toLowerCase(); // 获取文件扩展名,并转换为小写
  17.       // 生成唯一文件名,并加上扩展名
  18.       const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1E9);
  19.       const fileName = uniqueSuffix + ext; // 新文件名
  20.       cb(null, fileName); // 文件名
  21.     }
  22.   })
  23. });
  24. // 上传单个音频文件
  25. router.post('/single/audio', uploadVoice.single('audio'), (req, res) => {
  26.   const audioPath = req.file.path.replace('public', '').replace(/\\/g, '/');
  27.   const { name } = req.body
  28.   const type = req.file.filename.split('.')[1]
  29.   const size = (req.file.size / (1024 * 1024)).toFixed(2)
  30.   console.log(req.file)
  31.   console.log(name, audioPath, type, size)
  32.   const insertSql = 'insert into audio (name,path,type,size) values (?,?,?,?)'
  33.   const insertValues = [name, audioPath, type, size]
  34.   connection.query(insertSql, insertValues, (err, results) => {
  35.     if (err) {
  36.       console.error('插入数据失败:', err.message);
  37.       res.send({
  38.         status: 500,
  39.         msg: err.message
  40.       })
  41.       return;
  42.     }
  43.     console.log('音频插入成功');
  44.     res.send({
  45.       status: 200,
  46.       msg: 'ok',
  47.       path: audioPath// 返回新插入记录的ID
  48.     })
  49.   })
  50. })
  51. // 获取音频列表
  52. router.get('/list', (req, res) => {
  53.   let { name, startTime, endTime, page, size, isAll } = req.query
  54.   startTime = startTime ? (new Date(startTime)).toLocaleDateString() : ''
  55.   endTime = endTime ? (new Date(endTime)).toLocaleDateString() : ''
  56.   console.log({ name, startTime, endTime, page, size })
  57.   let selectSql = ''
  58.   // 按照名称查询并按照id进行倒序排列
  59.   if (name && !startTime) {
  60.     selectSql = `select * from audio where name like '%${name}%' order by id desc`
  61.   } else if (!name && startTime) {
  62.     // 按照上传时间并按照id进行倒序排列
  63.     selectSql = `select * from audio where Date(upload_time) between '${startTime}' and '${endTime}' order by id desc`
  64.   } else if (name && startTime) {
  65.     // 按照时间时间和名称并按照id进行倒序排列
  66.     selectSql = `select * from audio where name like '%${name}%' and Date(upload_time) between '${startTime}' and '${endTime}' order by id desc`
  67.   } else {
  68.     selectSql = 'select * from audio order by id desc'
  69.   }
  70.   connection.query(selectSql, (err, results) => {
  71.     if (err) {
  72.       console.error('查询数据失败:', err.message);
  73.       res.send({
  74.         status: 500,
  75.         msg: err.message
  76.       })
  77.       return;
  78.     }
  79.     const data = results.map(item => {
  80.       item.path = baseURL + item.path
  81.       item['upload_time'] = item['upload_time'].toLocaleString()
  82.       return item
  83.     })
  84.     res.send({
  85.       status: 200,
  86.       msg: 'ok',
  87.       data: isAll ? data : data.slice((page - 1) * size, page * size),
  88.       total: data.length
  89.     })
  90.   })
  91. })
  92. // 删除音频
  93. router.delete('/delete/:id', (req, res) => {
  94.   const { id } = req.params
  95.   const deleteSql = `delete from audio where id = ?`
  96.   connection.query(deleteSql, [id], (err, results) => {
  97.     if (err) {
  98.       console.error('删除数据失败:', err.message);
  99.       res.send({
  100.         status: 500,
  101.         msg: err.message
  102.       })
  103.       return;
  104.     }
  105.     console.log('音频删除成功')
  106.     res.send({
  107.       status: 200,
  108.       msg: 'ok',
  109.     })
  110.   })
  111. })
  112. module.exports = router;
复制代码
三、视频素材页

3.1 功能形貌

搜索

在搜索栏中输入 视频名称、视频分组、上传时间 后点击搜索按钮即可,也可以只输入一个大概两个条件搜索。
添加

点击页面上的【上传视频】按钮,然后在弹出的上传视频对话框中输入 视频名称、选择 视频分组(选填),并选择要上传的视频文件之后,点击确定按钮即可完成视频文件的上传。
编辑

点击页面中的【编辑】按钮,即可在弹出的对话框中编辑该条视频的 名称和分组。
删除

点击页面中的【删除】按钮,即可删除对应的视频文件。
3.2 结果展示

   
     视频素材页面      
     点击上一张图片中的上传视频按钮弹出的对话框      
     选择要上传的视频文件    3.3 代码实现

前端

  1. <template>
  2.   <div>
  3.     <el-button type="primary" @click="handleAdd">上传视频</el-button>
  4.     <el-row style="margin-top: 10px">
  5.       <el-form v-model="queryParams" inline>
  6.         <el-form-item label="视频名称">
  7.           <el-input
  8.             v-model="queryParams.name"
  9.             placeholder="输入要查询的文件名称"
  10.             style="width: 200px"
  11.           ></el-input>
  12.         </el-form-item>
  13.         <el-form-item label="视频分组">
  14.           <el-select
  15.             v-model="queryParams.group_id"
  16.             clearable
  17.             placeholder="请选择分组"
  18.             style="width: 200px"
  19.           >
  20.             <el-option
  21.               v-for="item in groups"
  22.               :key="item.id"
  23.               :label="item.group_name"
  24.               :value="item.id"
  25.             />
  26.           </el-select>
  27.         </el-form-item>
  28.         <el-form-item label="上传时间">
  29.           <el-date-picker
  30.             v-model="daterange"
  31.             type="daterange"
  32.             unlink-panels
  33.             range-separator="至"
  34.             start-placeholder="开始日期"
  35.             end-placeholder="结束日期"
  36.             :shortcuts="shortcuts"
  37.           />
  38.         </el-form-item>
  39.       </el-form>
  40.       <div style="margin-left: 20px">
  41.         <el-button type="primary" @click="handleSearch">搜索</el-button>
  42.         <el-button type="info" plain @click="handleReset">重置</el-button>
  43.       </div>
  44.     </el-row>
  45.     <el-button type="success" @click="handleChooseMul" v-if="choose"
  46.       >选择</el-button
  47.     >
  48.     <el-table
  49.       ref="tableRef"
  50.       :data="tableData"
  51.       stripe
  52.       border
  53.       @selection-change="handleSelectionChange"
  54.       :style="{ height: tableHeight ? tableHeight + 'px' : '' }"
复制代码
回复

使用道具 举报

登录后关闭弹窗

登录参与点评抽奖  加入IT实名职场社区
去登录
快速回复 返回顶部 返回列表