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

目录

功能描述

效果展示

代码实现

前端代码

后端代码

routers =》users.js

routers =》 index.js

app.js


功能描述

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

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

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

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

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

效果展示

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

代码实现

说明:

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

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

前端代码

<template><div id="app"><!-- 显示上传的音频 --><div><h2>上传的背景音乐</h2><audiov-for="audio in uploadedaudios":key="audio.src":src="audio.src"controlsstyle="width: 300px"></audio></div><!-- 上传视频音频 --><input type="file" @change="uploadaudio" accept="audio/*" /><hr /><!-- 显示上传的视频 --><div><h2>将要处理的视频</h2><videov-for="video in uploadedVideos":key="video.src":src="video.src"controlsstyle="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><videov-for="video in processedVideos":key="video.src":src="video.src"controlsstyle="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 developmentres.locals.message = err.message;res.locals.error = req.app.get('env') === 'development' ? err : {};// render the error pageres.status(err.status || 500);res.render('error');
});module.exports = app;

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

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

相关文章

docker环境安装mongoDB实现平滑迁移实战

docker环境安装mongoDB实现平滑迁移实战 一、备份原始数据&#xff08;从别的服务器备份到当前服务器&#xff09;二、数据迁移三、迁移过程日志打印四、验证迁移数据准确性 一、备份原始数据&#xff08;从别的服务器备份到当前服务器&#xff09; 使用mongodump工具对原始mo…

Java发送请求实现QPS限制

业务场景 在开发中&#xff0c;当有业务需求需要调用第三方服务实现功能&#xff0c;但是服务接口有QPS限制&#xff0c;所以我们需要在发送请求时限制发送频率防止请求失败。 实现方法 使用 Java Semaphore 类来实现控制请求QPS QPS限制 Component public class QPSControll…

MySQL 8.4修改user的host属性值

MySQL 8.4修改user的host属性值 update mysql.user set host localhost where user mysql用户名; MySQL 8.4修改初始化后的默认密码-CSDN博客文章浏览阅读804次&#xff0c;点赞6次&#xff0c;收藏11次。先下载mysql的zip压缩包&#xff1a;MySQL :: Download MySQL Communi…

STL-string

STL的六大组件&#xff1a; string // string constructor #include <iostream> #include <string> using namespace std; int main() {// 构造std::string s0("Initial string");std::string s1; //nullptrstd::string s2("A character sequenc…

【在Linux世界中追寻伟大的One Piece】Jsoncpp|序列化

目录 1 -> Jsoncpp 1.1 -> 特性 1.2 -> 安装 2 -> 序列化 3 -> 反序列化 4 -> Json::Value 1 -> Jsoncpp Jsoncpp是一个用于处理JSON数据的C库。它提供了将JSON数据序列化为字符串以及从字符串反序列化为C数据结构的功能。Jsoncpp是开源的&#xf…

RHCSA复习题

第一~七章 1.创建以下目录和文件结构&#xff0c;并将/yasuo目录拷贝4份到/目录下。 [rootlocalhost ~]# mkdir /yasuo [rootlocalhost ~]# mkdir /yasuo/dir1 [rootlocalhost ~]# cd /yasuo/dir1 [rootlocalhost dir1]# touch hostname hostname02 passwd ssh_config sshd [r…

F5-TTS开源项目详解:非自回归语音合成技术革新与应用场景

在现代科技的推动下&#xff0c;语音合成技术取得了长足的进步。随着越来越多的场景开始依赖语音交互&#xff0c;如何高效、自然地生成语音成为了行业的一个重要课题。今天我们要讨论的是一个在语音合成领域备受瞩目的开源项目——F5-TTS。它不仅采用了非自回归架构&#xff0…

Arduino配置ESP32环境

Arduino配置ESP32环境 引言一、IDE下载教程操作取巧方法 二、社区安装包三、官方手动安装 引言 最近入手了一款ESP32-C3的开发板&#xff0c;想继续沿用现有Arduino IDE&#xff0c;网上看了很多方法&#xff0c;大致分了三类&#xff1a;IDE下载、社区安装包、github手动配置…

南京邮电大学电工电子A实验十一(数据选择器及逻辑电路的动态测试)

文章目录 一、实验报告预览二、Word版本报告下载 一、实验报告预览 二、Word版本报告下载 点我

2.Java--入门程序

一、开发Java程序 步骤&#xff1a; 1.编写代码 其中第一行的HelloWorld叫类名&#xff0c;下面的框架叫main()方法&#xff0c; 类名要和文件名一致&#xff0c; 2.编译代码 用Javac进行编译&#xff0c;将编写的代码保存之后&#xff0c;打开WindowsR输入cmd 用cd文件夹…

rom定制系列------小米6x_澎湃os1.0.28安卓13定制固件修改 刷写过程与界面预览

&#x1f49d;&#x1f49d;&#x1f49d; 在接待很多定制化系统过程中。小米6x机型为很多工作室客户使用。但官方低版本固件无法适应新应用的使用。有些第三方固件却可以完美解决。此固件是客户分享的卡刷固件。需要修改为可以批量刷写的线刷固件。去除一些内置应用。需要自带…

Spring使用注解进行依赖注入

一、原则 不要把选择留给Spring 二、Autowired&#xff08;Spring注解&#xff09; 流程图&#xff1a; Spring依赖注入-Autowired 截图&#xff1a; 三、Resource&#xff08;JDK注解&#xff09; 流程图&#xff1a; Spring依赖注入-Resource 截图&#xff1a;

Docker-Harbor概述及构建

文章目录 一、Docker Harbor概述1.Harbor的特性2.Harbor的构成 二、搭建本地私有仓库三、部署 Docker-Harbor 服务四、在其他客户端上传镜像五、维护管理Harbor 一、Docker Harbor概述 Harbor 是 VMware 公司开源的企业级 Docker Registry 项目&#xff0c;其目标是帮助用户迅…

LLM - 使用 Neo4j 可视化 GraphRAG 构建的 知识图谱(KG) 教程

欢迎关注我的CSDN&#xff1a;https://spike.blog.csdn.net/ 本文地址&#xff1a;https://spike.blog.csdn.net/article/details/142938982 免责声明&#xff1a;本文来源于个人知识与公开资料&#xff0c;仅用于学术交流&#xff0c;欢迎讨论&#xff0c;不支持转载。 Neo4j …

向日葵下载教程以及三款远程控制工具推荐!!!

向日葵远程控制下载教程&#xff01;&#xff01; 亲爱的朋友们&#xff0c;如果你对远程控制软件有所需求&#xff0c;那么向日葵绝对是一个不错的选择。现在我将带你走一遍向日葵的下载流程。 1. 打开你的浏览器&#xff0c;输入“向日葵官方网站”&#xff0c;进入官方网站…

线性回归逻辑回归-笔记

一、线性回归&#xff08;Linear Regression&#xff09; 1. 定义 线性回归是一种用于回归问题的算法&#xff0c;旨在找到输入特征与输出值之间的线性关系。它试图通过拟合一条直线来最小化预测值与真实值之间的误差。 2. 模型表示 线性回归模型假设目标变量&#xff08;输…

【黑苹果】记录MacOS升级Sonoma的过程

【黑苹果】记录MacOS升级Sonoma的过程 一、硬件二、提前说明三、准备OC四、选择驱动五、选择ACPI六、下载内核扩展七、其他问题 一、硬件 设备是神舟zx6-ct5da 具体参照下图 二、提前说明 本机器已经安装过 macOS Monterey 12.6&#xff0c;这次是升级到 macOS Sonoma 14。 …

测试教程分享

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

火车头采集器易优cms采集发布模块下载

火车头采集器发布数据到易优cms&#xff08;Eyoucms&#xff09;的详细流程&#xff1a; 1. 火车头采集器易优cms&#xff08;Eyoucms&#xff09;发布模块下载地址&#xff1a; 火车头Eyoucms发布模块插件下载-CSDN 2. 在火车头导入下载的易优cms采集发布模块&#xff1b; …

uniapp学习(003-2 vue3学习 Part.2)

零基础入门uniapp Vue3组合式API版本到咸虾米壁纸项目实战&#xff0c;开发打包微信小程序、抖音小程序、H5、安卓APP客户端等 总时长 23:40:00 共116P 此文章包含第15p-第p20的内容 文章目录 事件监听以及组件内置事件处理自定义模板快速创建uniapp条件渲染 v-if和v-elsev-e…