前端压缩视频(@ffmpeg/ffmpeg)和图片(browser-image-compression)

1.压缩图片

安装插件
npm install browser-image-compression --save
引入插件
import { createWorker } from 'browser-image-compression';
定义方法
   // 压缩图片
    async compressImage(file, maxSizeMB) {
      const options = {
        maxSizeMB,
        maxWidthOrHeight: 1920,
        useWebWorker: true
      };
      try {
        const compressedFile = await imageCompression(file, options);
        const fileExtension = file.name.split('.').pop();
        const compressedFileName = `${Date.now()}.${fileExtension}`;
        return new File([compressedFile], compressedFileName, { type: file.type });
      } catch (error) {
        console.error('图片压缩失败:', error);
        return file; // 压缩失败则使用原文件
      }
    },

2.压缩视频

安装插件
npm install @ffmpeg/ffmpeg@0.12.6 --save
npm install @ffmpeg/util@0.12.1 --save
npm install @ffmpeg/core@0.12.4 --save
引入插件
import { FFmpeg } from '@ffmpeg/ffmpeg'
import { fetchFile ,toBlobURL} from '@ffmpeg/util'  // 注意来源包变化
定义方法
   // 初始化 FFmpeg
    async initFFmpeg() {
      this.initStatus = 'loading'
      try {
        this.ffmpeg = new FFmpeg()
        // 加载关键资源
        await this.ffmpeg.load({
          coreURL: '/public/ffmpeg-core.js',
          wasmURL: '/public/ffmpeg-core.wasm',
          workerURL: '/public/ffmpeg-core.worker.js'
        })

        // 验证初始化完成
        if (!this.ffmpeg.loaded) {
          throw new Error('FFmpeg 核心加载失败')
        }
        console.log('FFmpeg 初始化完成');
        this.initStatus = 'ready'
        this.retryCount = 0
      } catch (err) {
        this.initStatus = 'error'
        console.error('初始化失败:', err)
      }
    }
       // 压缩视频
    async compressVideo() {
      if (!this.ffmpeg?.loaded) {
          alert('FFmpeg 未初始化完成,请稍后重试')
          return
      }
      try {
          await this.ffmpeg.writeFile('input.mp4', await fetchFile(this.currentFile))
           await this.executeCompression(10) 
            return await this.outputResult()
        } catch (err) {
          console.log(222);
          // this.handleError(err)
      }

    },
        // 开始压缩
    async executeCompression(maxSizeMB) {
      const bitrate = maxSizeMB * 8 * 1024 * 1024 / 10; // 计算目标比特率
      const crf = 23; // 常用的 CRF 值,数值越小质量越高
      console.log('executeCompression');
      try {
      await this.ffmpeg.exec([
      '-i', 'input.mp4',
      '-b:v', `${6000}k`,
      '-vf', 'scale=640:-1', // 调整分辨率,
      '-preset', 'fast', // 编码速度
      'output.mp4'
    ]);
    console.log('Compression completed successfully');
  } catch (err) {
    console.error('Compression failed:', err);
    // 处理错误,例如显示错误消息
    this.status = '压缩失败: ' + err.message;
  }
    },
    // 输出压缩结果
    async outputResult() {
      console.log('outputResult');
      const data = await this.ffmpeg.readFile('output.mp4')
      const fileExtension = this.currentFile.name.split('.').pop();
      const compressedFileName = `${Date.now()}.${fileExtension}`;
      return new File([data], compressedFileName, { type: 'video/mp4' });
      // const url = URL.createObjectURL(new Blob([data], { type: 'video/mp4' }))
      // 处理下载或预览...
    },
完整的代码
<template>
  <div class="container">
    <!-- <input type="file" @change="handleFileUpload" accept="video/*" /> -->
    <input type="file" @change="handleFileUpload"  />
    <button :disabled="isProcessing" @click="compress">
      {{ isProcessing ? '处理中...' : '开始压缩' }}
    </button>
   <!-- 进度条 -->
    <div v-for="(progress, index) in uploadProgresses" :key="index" class="progress-container">
        <div class="file-name">{{ progress.fileName }}</div>
        <div class="progress-bar">
          <div class="progress" :style="{ width: progress.percentage + '%' }"></div>
        </div>
        <div class="progress-text">{{ progress.percentage }}%</div>
      </div>
    <!-- 状态提示 -->
    <div class="status">{{ status }}</div>
    <!-- 压缩结果预览 -->
    <video v-if="compressedUrl" :src="compressedUrl" controls class="preview"></video>
    <a v-if="compressedUrl" :href="compressedUrl" download="compressed.mp4">下载压缩文件</a>
  </div>
</template>

<script>
import imageCompression from 'browser-image-compression';
import { FFmpeg } from '@ffmpeg/ffmpeg'
import axios from 'axios';
import { fetchFile ,toBlobURL} from '@ffmpeg/util'  // 注意来源包变化
// const baseURL = 'https://cdn.jsdelivr.net/npm/@ffmpeg/core-mt@0.12.9/dist/esm'
export default {
  data() {
    return {
      isProcessing: false,
      progress: 0,
      status: '',
      compressedUrl: null,
      ffmpeg: null,
      currentFile: null,
      retryCount: 0,
      initStatus: 'loading', // 初始化状态:loading, ready, error
      uploadProgresses:[]
      
    }
  },
  methods: {
    // 更新上传进度
    updateUploadProgress(index, percentage) {
        this.uploadProgresses[index].percentage = percentage;
      },
    // 上传文件
    async uploadFiles(files) {
      const uploadPromises = files.map((file, index) => {
        const formData = new FormData();
        formData.append('files', file);
        return axios.post('http://localhost:9080/upload', formData, {
          headers: {
            'Content-Type': 'multipart/form-data',
          },
          onUploadProgress: (progressEvent) => {
            console.log(progressEvent);
            const percentCompleted = Math.round((progressEvent.loaded * 100) / progressEvent.total);
            this.updateUploadProgress(index, percentCompleted);
          }
        });
      });
      try {
        await Promise.all(uploadPromises);
        alert('文件上传成功');
      } catch (error) {
        console.error('上传失败:', error);
        alert('文件上传失败');
      }
    },
    //获取上传文件
    async handleFileUpload(e) {
      this.currentFile = e.target.files[0]
      this.uploadProgresses.push({
            fileName: this.currentFile.name,
            percentage: 0
          });
    },
    // 压缩图片
    async compressImage(file, maxSizeMB) {
      const options = {
        maxSizeMB,
        maxWidthOrHeight: 1920,
        useWebWorker: true
      };
      try {
        const compressedFile = await imageCompression(file, options);
        const fileExtension = file.name.split('.').pop();
        const compressedFileName = `${Date.now()}.${fileExtension}`;
        return new File([compressedFile], compressedFileName, { type: file.type });
      } catch (error) {
        console.error('图片压缩失败:', error);
        return file; // 压缩失败则使用原文件
      }
    },
    // 压缩视频
    async compressVideo() {
      if (!this.ffmpeg?.loaded) {
          alert('FFmpeg 未初始化完成,请稍后重试')
          return
      }
      try {
          await this.ffmpeg.writeFile('input.mp4', await fetchFile(this.currentFile))
           await this.executeCompression(10) 
            return await this.outputResult()
        } catch (err) {
          console.log(222);
          // this.handleError(err)
      }

    },
    // 开始压缩
    async compress() {
      if (!this.currentFile) {
        alert('请选择一个视频文件')
        return
      }
       this.isProcessing = true
       const compressedFiles = [];
       let compressedFile;
      if(this.currentFile.type.includes('image')){
        compressedFile = await this.compressImage(this.currentFile, 1); // 设置最大文件大小为1MB
      }else if(this.currentFile.type.includes('video')){
        compressedFile =  await this.compressVideo()
      }
        compressedFiles.push(compressedFile);
        this.isProcessing = false
        this.uploadFiles(compressedFiles);
 
   
    },
    // 开始压缩
    async executeCompression(maxSizeMB) {
      const bitrate = maxSizeMB * 8 * 1024 * 1024 / 10; // 计算目标比特率
      const crf = 23; // 常用的 CRF 值,数值越小质量越高
      console.log('executeCompression');
      try {
      await this.ffmpeg.exec([
      '-i', 'input.mp4',
      '-b:v', `${6000}k`,
      '-vf', 'scale=640:-1', // 调整分辨率,
      '-preset', 'fast', // 编码速度
      'output.mp4'
    ]);
    console.log('Compression completed successfully');
  } catch (err) {
    console.error('Compression failed:', err);
    // 处理错误,例如显示错误消息
    this.status = '压缩失败: ' + err.message;
  }

    },
    // 输出压缩结果
    async outputResult() {
      console.log('outputResult');
      const data = await this.ffmpeg.readFile('output.mp4')
      const fileExtension = this.currentFile.name.split('.').pop();
      const compressedFileName = `${Date.now()}.${fileExtension}`;
      return new File([data], compressedFileName, { type: 'video/mp4' });
      // const url = URL.createObjectURL(new Blob([data], { type: 'video/mp4' }))
      // 处理下载或预览...
    },
    async initFFmpeg() {
      this.initStatus = 'loading'
      try {
        this.ffmpeg = new FFmpeg()
        // 加载关键资源
        await this.ffmpeg.load({
          coreURL: '/public/ffmpeg-core.js',
          wasmURL: '/public/ffmpeg-core.wasm',
          workerURL: '/public/ffmpeg-core.worker.js'
          // coreURL: await toBlobURL(`${baseURL}/ffmpeg-core.js`, 'text/javascript'),
          // wasmURL: await toBlobURL(`${baseURL}/ffmpeg-core.wasm`, 'application/wasm'),
          // workerURL: await toBlobURL(`${baseURL}/ffmpeg-core.worker.js`, 'text/javascript')
        })

        // 验证初始化完成
        if (!this.ffmpeg.loaded) {
          throw new Error('FFmpeg 核心加载失败')
        }
        console.log('FFmpeg 初始化完成');
        this.initStatus = 'ready'
        this.retryCount = 0
      } catch (err) {
        this.initStatus = 'error'
        console.error('初始化失败:', err)
      }
    }
  },
  async mounted() {
    try {
      this.initFFmpeg()
    } catch (err) {
      console.error('FFmpeg 初始化失败:', err)
    }
  }
}
</script>

<style>
.container {
  max-width: 600px;
  margin: 2rem auto;
  padding: 20px;
}

input[type="file"] {
  margin-bottom: 1rem;
}

button {
  background: #42b983;
  color: white;
  border: none;
  padding: 10px 20px;
  border-radius: 4px;
  cursor: pointer;
}

button:disabled {
  background: #ccc;
  cursor: not-allowed;
}

.progress-container {
  margin: 1rem 0;
  height: 20px;
  border-radius: 10px;
  position: relative;
}

.progress-bar {
  height: 100%;
  background: #42b983;
  border-radius: 10px;
  transition: width 0.3s ease;
}

.progress-container span {
  position: absolute;
  right: 10px;
  top: 1px;
  color: #333;
}

.status {
  margin: 1rem 0;
  color: #666;
}

.preview {
  width: 100%;
  margin-top: 1rem;
  border: 1px solid #ddd;
}

a {
  display: inline-block;
  margin-top: 1rem;
  color: #42b983;
  text-decoration: none;
}

.file-upload-container {
    max-width: 800px;
    margin: 0 auto;
    padding: 20px;
  }
  .progress-container {
    margin: 10px 0;
  }
  .file-name {
    margin-bottom: 5px;
    font-size: 14px;
  }
  .progress-bar {
    height: 20px;
    border: 1px solid #ccc;
    border-radius: 5px;
    overflow: hidden;
    background-color: #f5f5f5;
  }
  .progress {
    height: 100%;
    background-color: #4CAF50;
    width: 0;
  }
  .progress-text {
    margin-top: 5px;
    text-align: center;
  }
</style>
本地起node+express服务
const express = require('express');
const multer = require('multer');
const cors = require('cors');
const path = require('path');
const fs = require('fs');

const app = express();
const port = 9080;

// 配置 CORS
app.use(cors());
app.use((req, res, next) => {
  res.setHeader('Cross-Origin-Embedder-Policy', 'require-corp');
  res.setHeader('Cross-Origin-Opener-Policy', 'same-origin');
  next();
});
// 配置 multer 存储
const storage = multer.diskStorage({
  destination: function (req, file, cb) {
    cb(null, 'uploads/'); // 上传文件的存储目录
  },
  filename: function (req, file, cb) {
    cb(null, Date.now() + path.extname(file.originalname)); // 重命名文件,避免文件名冲突
  }
});

// 配置文件过滤
const fileFilter = (req, file, cb) => {
  if (file.mimetype.startsWith('image/') || file.mimetype.startsWith('video/') || file.mimetype === 'application/gzip') {
    cb(null, true); // 接受文件
  } else {
    cb(null, false); // 拒绝文件
    cb(new Error('文件类型不被允许'));
  }
};

// 配置 multer
const upload = multer({
  storage: storage,
  limits: {
    fileSize: 1024 * 1024 * 100 // 限制文件大小为 100MB
  },
  fileFilter: fileFilter
});

// 创建上传目录
if (!fs.existsSync('uploads')) {
  fs.mkdirSync('uploads');
}

// 多文件上传接口
app.post('/upload', upload.array('files'), (req, res) => {
  if (req.files && req.files.length > 0) {
    const files = req.files.map(file => ({
      filename: file.filename,
      originalname: file.originalname,
      mimetype: file.mimetype,
      size: file.size
    }));
    res.status(200).json({
      message: '文件上传成功',
      data: files
    });
  } else {
    res.status(400).json({ message: '文件上传失败' });
  }
});

// 启动服务器
app.listen(port, () => {
  console.log(`服务器运行在 http://localhost:${port}`);
});
Logo

中国智能体开发者社区,聚焦智能体与大模型开发,提供前沿资讯、实用工具链、开源项目及行业案例。通过技术沙龙、开发者大赛等活动,促进经验交流与协作,助力开发者快速构建创新智能应用。

更多推荐