目录
一、简介
此篇文章带来的是 使用 FFmpeg 实现视频混剪并且可以指定生成视频的数量以及生成视频的时长,此案例也是对前边几篇文章功能的综合。
说明:
此案例中的代码部分,前端使用的是 Vue + ElementPlus + Vite、后端使用的是 Nodejs + FFmpeg + MySQL
👇下方提供的代码为每一部分的核心代码,如果需要案例的完整代码,可以在评论区留言我来私信你!!!👆
二、音频素材页
2.1 功能描述
👉 搜索
在搜索栏中输入 音频名称、上传时间 后点击搜索按钮即可,也可以只输入一个条件搜索。
👉 添加
点击页面上的【添加音频】按钮,然后在弹出的上传音频对话框中输入 音频名称,并选择要上传的音频文件之后直接点击确定按钮即可完成音频文件的上传。
👉 删除
点击表格中的【删除】按钮即可删除对应的音频文件
2.2 效果展示



2.3 代码实现
👉 前端
<template>
<div>
<el-button type="primary" @click="dialogVisible = true">添加音频</el-button>
<el-row style="margin: 10px 0">
<el-form v-model="queryParams" inline>
<el-form-item label="音频名称">
<el-input
v-model="queryParams.name"
placeholder="输入要查询的文件名称"
style="width: 200px"
></el-input>
</el-form-item>
<el-form-item label="上传时间">
<el-date-picker
v-model="daterange"
type="daterange"
unlink-panels
range-separator="至"
start-placeholder="开始日期"
end-placeholder="结束日期"
:shortcuts="shortcuts"
/>
</el-form-item>
</el-form>
<div style="margin-left: 20px">
<el-button type="primary" @click="handleSearch">搜索</el-button>
<el-button type="info" plain @click="handleReset">重置</el-button>
</div>
</el-row>
<el-table
:data="tableData"
stripe
border
:style="{ height: tableHeight ? tableHeight + 'px' : '' }"
>
<el-table-column prop="id" label="ID" align="center" width="80" />
<el-table-column prop="name" label="音频名称" align="center" />
<el-table-column
prop="path"
label="音频文件"
align="center"
min-width="260"
>
<template #default="scope">
<audio :src="scope.row.path" controls class="audioStyle"></audio>
</template>
</el-table-column>
<el-table-column
prop="type"
label="音频类型"
align="center"
width="100"
/>
<el-table-column prop="size" label="文件大小" align="center" width="90">
<template #default="scope"> {
{ scope.row.size }}M </template>
</el-table-column>
<el-table-column
prop="upload_time"
label="上传时间"
align="center"
width="200"
/>
<el-table-column label="操作" align="center" width="90" fixed="right">
<template #default="scope">
<el-button
v-if="choose"
type="success"
size="small"
@click="handleChoose(scope.row)"
>选择</el-button
>
<el-button
v-else
type="danger"
size="small"
@click="handleDelete(scope.row)"
>删除</el-button
>
</template>
</el-table-column>
</el-table>
<el-row style="margin-top: 10px; justify-content: end; align-items: center">
<span style="margin-right: 20px; color: #606266">共 {
{ total }} 条</span>
<el-pagination
background
layout="prev, pager, next"
:page-size="queryParams.size"
:total="total"
@current-change="handleCurrentChange"
></el-pagination>
</el-row>
<!-- 添加音频对话框 -->
<el-dialog v-model="dialogVisible" title="上传音频">
<el-form :model="formData" :rules="rules" ref="ruleFormRef">
<el-form-item label="音频名称" prop="name">
<el-input
v-model="formData.name"
placeholder="请输入音频名称"
></el-input>
</el-form-item>
<el-form-item label="音频文件" prop="file">
<el-input
v-model="formData.path"
placeholder="请选择音频文件"
class="input-with-select"
disabled
>
<template #append>
<el-upload
v-model:file-list="fileList"
action=""
:limit="1"
:before-upload="beforeUpload"
:show-file-list="false"
accept="audio/*"
>
<el-button :icon="Upload" class="upload-btn">上传</el-button>
</el-upload>
</template>
</el-input>
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button @click="handleCancel">取消</el-button>
<el-button type="primary" @click="handleSubmit"> 确定 </el-button>
</div>
</template>
</el-dialog>
</div>
</template>
<script setup>
import { onMounted, ref } from "vue";
import { Upload } from "@element-plus/icons-vue";
import { post, get, del } from "@/utils/http";
import { useStore } from "@/store";
const porp = defineProps(["choose"]);
const store = useStore();
const tableData = ref([]);
const total = ref(0);
// 查询条件
const queryParams = ref({
name: undefined,
startTime: undefined,
endTime: undefined,
page: 1,
size: 10,
});
const daterange = ref([]);
const shortcuts = [
{
text: "最近一周",
value: () => {
const end = new Date();
const start = new Date();
start.setTime(start.getTime() - 3600 * 1000 * 24 * 7);
return [start, end];
},
},
{
text: "最近一个月",
value: () => {
const end = new Date();
const start = new Date();
start.setTime(start.getTime() - 3600 * 1000 * 24 * 30);
return [start, end];
},
},
{
text: "最近三个月",
value: () => {
const end = new Date();
const start = new Date();
start.setTime(start.getTime() - 3600 * 1000 * 24 * 90);
return [start, end];
},
},
];
// 获取音频列表
const getList = () => {
get("/audio/list", queryParams.value).then((res) => {
tableData.value = res.data;
total.value = res.total;
});
};
// 搜索
const handleSearch = () => {
queryParams.value.startTime = daterange.value[0];
queryParams.value.endTime = daterange.value[1];
console.log(queryParams.value);
getList();
};
// 重置
const handleReset = () => {
queryParams.value = {
name: undefined,
startTime: undefined,
endTime: undefined,
page: 1,
size: 10,
};
daterange.value = [];
getList();
};
// 切换页
const handleCurrentChange = (page) => {
queryParams.value.page = page;
getList();
};
const dialogVisible = ref(false); //弹出对话框是否显示
const fileList = ref([]);
const ruleFormRef = ref(null);
const formData = ref({
name: "",
path: "",
file: "",
});
// 校验规则
const rules = {
name: [{ required: true, message: "请输入音频名称", trigger: "blur" }],
file: [
{ required: true, message: "请选择要上传的音频文件", trigger: "blur" },
],
};
// 点击取消按钮
const handleCancel = () => {
dialogVisible.value = false;
// 重置表单
formData.value = {
name: "",
file: "",
};
};
// 点击确定按钮
const handleSubmit = () => {
const data = new FormData();
data.append("name", formData.value.name);
data.append("audio", formData.value.file);
console.log(formData.value);
// 先进行表单的验证
ruleFormRef.value.validate((valid) => {
if (valid) {
post("/audio/single/audio", data).then((res) => {
ElMessage.success("上传成功");
console.log("上传成功", res);
dialogVisible.value = false;
getList();
});
}
});
};
// 上传文件之前
const beforeUpload = (file) => {
formData.value.path = URL.createObjectURL(file);
formData.value.file = file;
return false;
};
// 删除
const handleDelete = (item) => {
del(`/audio/delete/${item.id}`).then((res) => {
ElMessage.success("删除成功");
getList();
});
};
// 选择音频
const handleChoose = (item) => {
store.chooseBgm(item);
store.setAudioDialog();
};
onMounted(() => {
getList();
});
</script>
<style lang="scss" scoped>
.el-table {
margin-top: 10px;
}
.el-button.upload-btn {
height: 100%;
background: #f56c6c;
color: #fff;
&:hover {
background: #f89898;
color: #fff;
}
}
.el-input-group__append > div {
display: flex;
}
.audioStyle {
width: 250px;
height: 40px;
}
</style>
👉 后端
var express = require('express');
var router = express.Router();
const multer = require('multer');
const path = require('path');
const connection = require('../config/db.config')
const baseURL = 'http://localhost:3000'
// 音频
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); // 文件名
}
})
});
// 上传单个音频文件
router.post('/single/audio', uploadVoice.single('audio'), (req, res) => {
const audioPath = req.file.path.replace('public', '').replace(/\\/g, '/');
const { name } = req.body
const type = req.file.filename.split('.')[1]
const size = (req.file.size / (1024 * 1024)).toFixed(2)
console.log(req.file)
console.log(name, audioPath, type, size)
const insertSql = 'insert into audio (name,path,type,size) values (?,?,?,?)'
const insertValues = [name, audioPath, type, size]
connection.query(insertSql, insertValues, (err, results) => {
if (err) {
console.error('插入数据失败:', err.message);
res.send({
status: 500,
msg: err.message
})
return;
}
console.log('音频插入成功');
res.send({
status: 200,
msg: 'ok',
path: audioPath// 返回新插入记录的ID
})
})
})
// 获取音频列表
router.get('/list', (req, res) => {
let { name, startTime, endTime, page, size, isAll } = req.query
startTime = startTime ? (new Date(startTime)).toLocaleDateString() : ''
endTime = endTime ? (new Date(endTime)).toLocaleDateString() : ''
console.log({ name, startTime, endTime, page, size })
let selectSql = ''
// 按照名称查询并按照id进行倒序排列
if (name && !startTime) {
selectSql = `select * from audio where name like '%${name}%' order by id desc`
} else if (!name && startTime) {
// 按照上传时间并按照id进行倒序排列
selectSql = `select * from audio where Date(upload_time) between '${startTime}' and '${endTime}' order by id desc`
} else if (name && startTime) {
// 按照时间时间和名称并按照id进行倒序排列
selectSql = `select * from audio where name like '%${name}%' and Date(upload_time) between '${startTime}' and '${endTime}' order by id desc`
} else {
selectSql = 'select * from audio order by id desc'
}
connection.query(selectSql, (err, results) => {
if (err) {
console.error('查询数据失败:', err.message);
res.send({
status: 500,
msg: err.message
})
return;
}
const data = results.map(item => {
item.path = baseURL + item.path
item['upload_time'] = item['upload_time'].toLocaleString()
return item
})
res.send({
status: 200,
msg: 'ok',
data: isAll ? data : data.slice((page - 1) * size, page * size),
total: data.length
})
})
})
// 删除音频
router.delete('/delete/:id', (req, res) => {
const { id } = req.params
const deleteSql = `delete from audio where id = ?`
connection.query(deleteSql, [id], (err, results) => {
if (err) {
console.error('删除数据失败:', err.message);
res.send({
status: 500,
msg: err.message
})
return;
}
console.log('音频删除成功')
res.send({
status: 200,
msg: 'ok',
})
})
})
module.exports = router;
三、视频素材页
3.1 功能描述
👉 搜索
在搜索栏中输入 视频名称、视频分组、上传时间 后点击搜索按钮即可,也可以只输入一个或者两个条件搜索。
👉 添加
点击页面上的【上传视频】按钮,然后在弹出的上传视频对话框中输入 视频名称、选择 视频分组(选填),并选择要上传的视频文件之后,点击确定按钮即可完成视频文件的上传。
👉 编辑
点击页面中的【编辑】按钮,即可在弹出的对话框中编辑该条视频的 名称和分组。
👉 删除
点击页面中的【删除】按钮,即可删除对应的视频文件。
3.2 效果展示



3.3 代码实现
👉 前端
<template>
<div>
<el-button type="primary" @click="handleAdd">上传视频</el-button>
<el-row style="margin-top: 10px">
<el-form v-model="queryParams" inline>
<el-form-item label="视频名称">
<el-input
v-model="queryParams.name"
placeholder="输入要查询的文件名称"
style="width: 200px"
></el-input>
</el-form-item>
<el-form-item label="视频分组">
<el-select
v-model="queryParams.group_id"
clearable
placeholder="请选择分组"
style="width: 200px"
>
<el-option
v-for="item in groups"
:key="item.id"
:label="item.group_name"
:value="item.id"
/>
</el-select>
</el-form-item>
<el-form-item label="上传时间">
<el-date-picker
v-model="daterange"
type="daterange"
unlink-panels
range-separator="至"
start-placeholder="开始日期"
end-placeholder="结束日期"
:shortcuts="shortcuts"
/>
</el-form-item>
</el-form>
<div style="margin-left: 20px">
<el-button type