Bootstrap

FFmpeg的简单使用【Windows】--- 简单的视频混合拼接

实现功能

点击【选择文件】按钮在弹出的对话框中选择多个视频,这些视频就是一会将要混剪的视频素材,点击【开始处理】按钮之后就会开始对视频进行处理,处理完毕之后会将处理后的文件路径返回,并在页面展示处理后的视频。

视频所作处理:从上传的视频素材中里边的每个视频里边随机抽取 2s,然后将抽取出来的这几个 2s 的视频,随机排序然后进行拼接。

效果展示

需要混剪的素材
处理后的视频

代码实现

说明:

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

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

前端代码

<template>
  <div id="app">
    <!-- 显示上传的视频 -->
    <div>
      <h2>将要处理的视频</h2>
      <video
        v-for="video in uploadedVideos"
        :key="video.src"
        :src="video.src"
        controls
        style="width: 100px"
      ></video>
    </div>

    <!-- 上传视频按钮 -->
    <input type="file" @change="uploadVideo" multiple accept="video/*" />
    <hr />
   
    <hr />
    <!-- 显示处理后的视频 -->
    <div>
      <h2>已处理后的视频</h2>
      <video
        v-for="video in processedVideos"
        :key="video.src"
        :src="video.src"
        controls
        style="width: 100px"
      ></video>
    </div>

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

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

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

const uploadVideo = async (e) => {
  const files = e.target.files;
  console.log(e);
  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 processVideos = async () => {
  const formData = new FormData();
  for (const video of uploadedVideos.value) {
    // formData.append("video", video.file); // 使用实际的文件对象
    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 processing video:", error);
  }
};
</script>

补充说明:

 accept="video/*"指定了只接受视频文件类型,这将过滤掉非视频文件,使得用户在选择文件时只能看到并选择视频文件。

video/*是一个通配符,表示所有已知的视频文件类型。如果你只想接受特定的视频格式(例如MP4和WebM),你可以指定他们,如下所示:

accept=".mp4, .webm"

或者,如果你想要更精确地控制,可以使用MIME类型:

accept="video/mp4, video/webm"

 multiple:表明这个文件输入框允许多个文件的选择。如果没有这个属性,用户每次只能选择一个文件。

后端代码

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 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 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('/process', upload.array('videos', 10), (req, res) => {
  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 => {
    // 构建 concat.txt 文件内容
    let concatFileContent = '';

    // 定义一个函数来随机选择视频片段
    function getRandomSegment(videoPath, length) {
      const segmentLength = 2; // 每个片段的长度为2秒
      const startTime = Math.floor(Math.random() * (length - segmentLength));
      return {
        videoPath,
        startTime,
        endTime: startTime + segmentLength
      };
    }

    // 随机选择视频片段
    const segments = [];
    for (let i = 0; i < lengths.length; i++) {
      const videoPath = videoPaths[i];
      const length = lengths[i];
      const segment = getRandomSegment(videoPath, length);
      segments.push(segment);
    }

    // 打乱视频片段的顺序
    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');

    // 使用 ffmpeg 合并多个视频
    ffmpeg()
      .input(concatFilePath)
      .inputOptions([
        '-f concat',
        '-safe 0'
      ])
      .output(outputPath)
      .outputOptions([
        '-y', // 覆盖已存在的输出文件
        '-c:v libx264', // 视频编码器
        '-preset veryfast', // 编码速度
        '-crf 23', // 视频质量控制
        '-map 0:v', // 选择所有输入文件的视频流
        '-an' // 禁用音频
      ])
      .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 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');
});

module.exports = router;

注意:

关于multer配置项 和 ffmpeg() 的说明可移步进行查看FFmpeg的简单使用【Windows】--- 视频倒叙播放-CSDN博客

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;

;