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.rhkb.cn/news/451321.html

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

相关文章

Java基于SSM微信小程序物流仓库管理系统设计与实现(lw+数据库+讲解等)

选题背景 随着社会的发展&#xff0c;社会的方方面面都在利用信息化时代的优势。互联网的优势和普及使得各种系统的开发成为必需。 本文以实际运用为开发背景&#xff0c;运用软件工程原理和开发方法&#xff0c;它主要是采用java语言技术和mysql数据库来完成对系统的设计。整个…

mysql的各种存储引擎

文章目录 前言1. InnoDB特点 2. MyISAM特点innodb与myisam引擎之间的区别 3. MEMORY特点 4. ARCHIVE特点 5. NDBCluster特点 6. FEDERATED特点 7. CSV特点 总结 前言 MySQL 支持多种存储引擎&#xff0c;每种引擎都有其独特的功能和适用场景。存储引擎是指数据库管理系统用来存…

[PHP]__callStatic

第一种&#xff1a;以下代码不会触发__callStatic&#xff0c;也不会报错 test是空方法 <?php class A {public function test(){}public static function __callStatic($method, $args){print_r(aaaaaaaaaaaaaaaaaaaaa);} }A::test();第二种&#xff1a;以下代码不会触发…

MYSQL-多表查询和函数

第一题讲解 # 1. 查出至少有一个员工的部门&#xff0c;显示部门编号、部门名称、部门位置、部门人数。 分析:(分析要查的表): (显示的列):(关联条件):(过滤条件):[分组条件]:[排序条件]:[分页条件]:SELECT d.deptno, dname, loc, count(empno) FROM dept d JOIN emp e ON d…

C#从零开始学习(基本语法概念)(2)

深入C# 本章所有的代码都放在 https://github.com/hikinazimi/head-first-Csharp 控制台项目结构 每个C#程序采用同样的方式组织,命名空间,类和方法 using System;namespace helloworld//命名空间 {class Program//类{static void Main(string[] args)//程序入口{Console.Writ…

YOLOv11改进-卷积-空间和通道重构卷积SCConv

本篇文章将介绍一个新的改进模块——SCConv&#xff08;小波空间和通道重构卷积&#xff09;&#xff0c;并阐述如何将其应用于YOLOv11中&#xff0c;显著提升模型性能。为了减少YOLOv11模型的空间和通道维度上的冗余&#xff0c;我们引入空间和通道重构卷积。首先&#xff0c;…

C语言笔记(指针的进阶)

目录 1.字符指针 2.指针数组 3.数组指针 3.1.创建数组指针 3.2.&数组名和数组名 1.字符指针 int main() { char ch w;char* pc &ch;const char *p "abcdef";//常量字符串 产生的值就是首元素的地址//常量字符串不能被修改 因此需要加上一个…

10月18日

二次型矩阵要是对称矩阵 通解要带入特解 集体化 逆反思维 先定特解&#xff0c;再求通解 反函数...我谢谢你 依旧是原函数

视频的编解码格式

文章目录 视频的编解码格式概念术语视频处理流程视频封装格式视频编码格式视频编解码器&#xff0c;视频容器和视频文件格式之间的区别补充视频码率 参考资料 视频的编解码格式 概念术语 两大组织主导视频压缩的组织及其联合(joint)组织 ITU-T(VCEG) ITU-T的中文名称是国际电信…

【动手学深度学习】6.2 图像卷积(个人向笔记)

1. 互相关运算 严格来说&#xff0c;卷积层是一个错误的叫法&#xff0c;因为它本质上是互相关运算而不是卷积运算。我们暂时忽略通道看看二维图像数据和隐藏表示。那么输出大小可以表示为 我们自己实现一个二维互相关运算 2. 卷积层 卷积层中有两个参数&#xff1a;卷积核权…

工业物联网关-TCP透传

TCP透传功能提供类似于DTU(Data Transmit Unit)的功能&#xff0c;用户在网络端使用TCP协议连接网关&#xff0c;与串口通道绑定&#xff0c;建立起TCP与串口的通道&#xff0c;网关相当于一个中转点。 菜单选择"数据上行-tcp透传"&#xff0c;查看当前透传通道列表&…

QtCreator14调试Qt5.15出现 Launching Debugger 错误

1、问题描述 使用QtCreator14调试程序&#xff0c;Launching Debugger 显示红色&#xff0c;无法进入调试模式。 故障现象如下&#xff1a; 使能Debugger Log窗口&#xff0c;显示&#xff1a; 325^error,msg"Error while executing Python code." 不过&#xff…

反走样算法(MSAA、TAA、FXAA、DLSS)

光栅化的采样过程会导致图形走样,走样有很多种形式: 锯齿 摩尔纹 走样的本质原因是采样速度跟不上信号变化的速度 采样频率低,使得我们将连续变化的信号离散化. 反走样方法 anti-alisaing MSAA 多重采样反走样 超采样 优点&#xff1a; 对几何反走样效果良好 缺点…

【Python语言进阶(二)】

一、函数的使用方式 将函数视为“一等公民” 函数可以赋值给变量函数可以作为函数的参数函数可以作为函数的返回值 高阶函数的用法&#xff08;filter、map以及它们的替代品&#xff09; items1 list(map(lambda x: x ** 2, filter(lambda x: x % 2, range(1, 10)))) # filter…

uniapp uni.uploadFile errMsg: “uploadFile:fail

uniapp 上传后一直显示加载中 1.检查前后端上传有无问题 2.检查失败信息 await uni.uploadFile({url,filePath,name,formData,header,timeout: 30000000, // 自定义上传超时时间fail: async function(err) {$util.hideAll()// 失败// err 返回 {errMsg: "uploadFile:fai…

stata基本操作

文章目录 数据导入及存储变量的标签、审视数据变量的标签审视数据数据删除数据排序 画图直方图使用帮助文件散点图 统计分析描述性分析频数分析相关分析 生成新变量、计算器、终止命令生成新变量设置哑变量修改变量名更改变量内容调用命令和终止命令 日志命令库更新、学习资源 …

如何用pyhton修改1000+图片的名字?

import os oldpath input("请输入文件路径&#xff08;在windows中复制那个图片文件夹的路径就可以):") #注意window系统中的路径用这个‘\分割&#xff0c;但是编程语言中一般都是正斜杠也就是’/‘ #这里写一个代码&#xff0c;将 \ > / path "" fo…

JMeter之mqtt-jmeter 插件介绍

前言 mqtt-jmeter插件是JMeter中的一个第三方插件&#xff0c;用于支持MQTT&#xff08;Message Queuing Telemetry Transport&#xff09;协议的性能测试。MQTT是一种轻量级的发布/订阅消息传输协议&#xff0c;广泛应用于物联网和传感器网络中。 一、安装插件 mqtt-jmeter项目…

用Java爬虫API,轻松获取电商商品SKU信息

在电子商务的精细化运营时代&#xff0c;SKU信息的重要性不言而喻。SKU&#xff08;Stock Keeping Unit&#xff09;信息不仅包含了商品的规格、价格、库存等关键数据&#xff0c;还直接影响到库存管理、价格策略和市场分析等多个方面。如何高效、准确地获取这些信息&#xff0…

LLM 的推理优化技术纵览

推理是 LLM 应用的重要一环&#xff0c;在部署服务环节影响重大&#xff0c;本文将讨论主流的 LLM 的推理优化技术。 一、子图融合&#xff08;subgraph fusion&#xff09; 图融合技术即通过将多个 OP&#xff08;算子&#xff09;合并成一个 OP&#xff08;算子&#xff09;&…