当前位置: 首页 > news >正文

vue纯静态实现 视频转GIF 功能(附源码)

提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档

文章目录

  • 前言
  • 一、实现后的效果
  • 二、使用步骤
    • 1.引入库
    • 2.下载or复制出来js
    • 3. 前端实现
  • 总结


前言

一天一个小demo 今天来一个vue纯静态实现 视频转GIF 功能

上一篇我们讲到了使用html+node.js+sealos实现了一个 多人实时在线聊天的简单小demo ,这次我们来写一个 视频转换成GIF的小功能 使用到的技术为 vue3 + @ffmpeg 很简单。


一、实现后的效果

下面是实现的效果,是我录制视频后转换成GIF的效果,当然下面这个GIF就是转换出来的真实效果哦。
在这里插入图片描述

二、使用步骤

1.引入库

代码如下(初始化一个vue3项目):
我来安装依赖 下面版本号是我现在demo的版本号

 "@ffmpeg/core": "0.12.4",
 "@ffmpeg/ffmpeg": "0.12.7",
 "@ffmpeg/util": "0.12.1",
npm i  @ffmpeg/core@0.12.4
npm i  @ffmpeg/ffmpeg@0.12.7
npm i  @ffmpeg/util@0.12.1

2.下载or复制出来js

我这里把这些给放到public项目里面了

https://unpkg.com/@ffmpeg/core@0.12.4/dist/esm/ffmpeg-core.js
https://unpkg.com/@ffmpeg/core@0.12.4/dist/esm/ffmpeg-core.wasm
https://unpkg.com/@ffmpeg/core@0.12.4/dist/esm/ffmpeg-core.worker.js

在这里插入图片描述

3. 前端实现

<template>
  <div class="video-conversion">
    <div class="hero-section">
      <h1>视频转GIF工具</h1>
      <p class="subtitle">简单、快速地将视频转换为高质量GIF动画</p>
    </div>

    <div class="content-wrapper">
      <div class="upload-section" :class="{ 'has-video': videoUrl }">
        <div class="upload-area" v-if="!videoUrl" @click="triggerFileInput">
          <el-icon class="upload-icon">
            <Upload />
          </el-icon>
          <p class="upload-text">点击或拖拽视频文件到这里</p>
          <p class="upload-hint">支持 MP4、WebM、MOV 格式 (最大50MB)</p>
        </div>
        <input type="file" @change="handleFileSelect" accept="video/*" ref="fileInput" class="hidden-input">
      </div>

      <div v-if="videoUrl" class="preview-container">
        <div class="video-preview">
          <div class="video-header">
            <h3>视频预览</h3>
            <el-button type="primary" size="small" @click="triggerFileInput" class="change-video-btn">
              <el-icon>
                <VideoCamera />
              </el-icon>
              更换视频
            </el-button>
          </div>
          <video :src="videoUrl" controls ref="videoPreview"></video>
        </div>

        <div class="settings-panel">
          <h3>转换设置</h3>
          <div class="settings-group">
            <el-form :model="settings" label-position="top">
              <el-form-item label="GIF宽度">
                <el-slider v-model="width" :min="100" :max="800" :step="10" show-input />
              </el-form-item>
              <el-form-item label="帧率 (FPS)">
                <el-slider v-model="fps" :min="1" :max="30" :step="1" show-input />
              </el-form-item>
            </el-form>
          </div>

          <el-button type="primary" :loading="isConverting" @click="convertToGif" class="convert-btn">
            {{ isConverting ? '正在转换...' : '开始转换' }}
          </el-button>
        </div>
      </div>

      <div v-if="gifUrl" class="result-section">
        <div class="result-preview">
          <h3>预览</h3>
          <img :src="gifUrl" alt="转换后的GIF">
          <div class="action-buttons">
            <el-button type="success" :href="gifUrl" download="converted.gif" @click="downloadGif">
              下载GIF
            </el-button>
            <el-button @click="resetConverter">重新转换</el-button>
          </div>
        </div>
      </div>
    </div>

    <el-alert v-if="error" :title="error" type="error" show-icon class="error-alert" />
  </div>
</template>

<script setup>
import { ref, onMounted } from 'vue'
import { FFmpeg } from '@ffmpeg/ffmpeg'
import { fetchFile, toBlobURL } from '@ffmpeg/util'
import { Upload, VideoCamera } from '@element-plus/icons-vue'

const ffmpeg = new FFmpeg()
const loaded = ref(false)
const isConverting = ref(false)
const videoUrl = ref('')
const gifUrl = ref('')
const error = ref('')
const width = ref(480)
const fps = ref(10)
const fileInput = ref(null)

const load = async () => {
  try {
    const baseURL = window.location.origin;
    // 修改核心文件的加载方式
    await ffmpeg.load({
      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')
    });
    loaded.value = true;
    console.log('FFmpeg 加载成功');
  } catch (err) {
    console.error('FFmpeg 加载错误:', err);
    error.value = '加载转换工具失败:' + err.message;
  }
};

// 修改处理文件选择的方法
const handleFileSelect = (event) => {
  const file = event.target.files[0]
  if (!file) return

  if (file.size > 50 * 1024 * 1024) {
    error.value = '文件大小不能超过50MB'
    return
  }

  // 如果已经有之前的视频URL,先释放它
  if (videoUrl.value) {
    URL.revokeObjectURL(videoUrl.value)
  }

  videoUrl.value = URL.createObjectURL(file)
  gifUrl.value = ''
  error.value = ''
  width.value = 480 // 重置设置
  fps.value = 10
}

// 转换为GIF
const convertToGif = async () => {
  if (!loaded.value) {
    error.value = '转换工具尚未加载完成'
    return
  }

  try {
    isConverting.value = true
    error.value = ''

    // 写入文件到FFmpeg虚拟文件系统
    const videoFile = await fetch(videoUrl.value)
    const videoData = await videoFile.arrayBuffer()
    await ffmpeg.writeFile('input.mp4', new Uint8Array(videoData))

    // 执行转换命令
    await ffmpeg.exec([
      '-i', 'input.mp4',
      '-vf', `scale=${width.value}:-1:flags=lanczos,fps=${fps.value}`,
      '-c:v', 'gif',
      'output.gif'
    ])

    // 读取转换后的文件
    const data = await ffmpeg.readFile('output.gif')
    const blob = new Blob([data], { type: 'image/gif' })
    gifUrl.value = URL.createObjectURL(blob)
  } catch (err) {
    error.value = '转换失败:' + err.message
  } finally {
    isConverting.value = false
  }
}

const triggerFileInput = () => {
  fileInput.value.click()
}

const downloadGif = () => {
  const link = document.createElement('a')
  link.href = gifUrl.value
  link.download = 'converted.gif'
  document.body.appendChild(link)
  link.click()
  document.body.removeChild(link)
}

const resetConverter = () => {
  videoUrl.value = ''
  gifUrl.value = ''
  error.value = ''
  width.value = 480
  fps.value = 10
}

onMounted(() => {
  load()
})
</script>

<style scoped>
.video-conversion {
  max-width: 1200px;
  margin: 0 auto;
  padding: 40px 20px;
  color: #1d1d1f;
}

.hero-section {
  text-align: center;
  margin-bottom: 60px;
}

.hero-section h1 {
  font-size: 48px;
  font-weight: 600;
  margin-bottom: 16px;
  background: linear-gradient(135deg, #1a1a1a 0%, #4a4a4a 100%);
  -webkit-background-clip: text;
  -webkit-text-fill-color: transparent;
}

.subtitle {
  font-size: 24px;
  color: #86868b;
  font-weight: 400;
}

.content-wrapper {
  background: white;
  border-radius: 20px;
  box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1);
  overflow: hidden;
}

.upload-section {
  padding: 40px;
  transition: all 0.3s ease;
}

.upload-area {
  border: 2px dashed #d2d2d7;
  border-radius: 12px;
  padding: 40px;
  text-align: center;
  cursor: pointer;
  transition: all 0.3s ease;
}

.upload-area:hover {
  border-color: #0071e3;
  background: rgba(0, 113, 227, 0.05);
}

.upload-icon {
  font-size: 48px;
  color: #86868b;
  margin-bottom: 20px;
}

.upload-text {
  font-size: 20px;
  color: #1d1d1f;
  margin-bottom: 8px;
}

.upload-hint {
  font-size: 14px;
  color: #86868b;
}

.hidden-input {
  display: none;
}

.preview-container {
  display: grid;
  grid-template-columns: 2fr 1fr;
  gap: 30px;
  padding: 30px;
  background: #f5f5f7;
}

.video-preview {
  background: white;
  padding: 20px;
  border-radius: 12px;
  box-shadow: 0 4px 6px rgba(0, 0, 0, 0.05);
}

.video-preview video {
  width: 100%;
  border-radius: 8px;
}

.settings-panel {
  background: white;
  padding: 30px;
  border-radius: 12px;
  box-shadow: 0 4px 6px rgba(0, 0, 0, 0.05);
}

.settings-panel h3 {
  font-size: 20px;
  margin-bottom: 24px;
  color: #1d1d1f;
}

.settings-group {
  margin-bottom: 30px;
}

.convert-btn {
  width: 100%;
  height: 44px;
  font-size: 16px;
}

.result-section {
  padding: 40px;
  background: white;
}

.result-preview {
  text-align: center;
}

.result-preview img {
  max-width: 100%;
  border-radius: 12px;
  margin: 20px 0;
  box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}

.action-buttons {
  display: flex;
  gap: 16px;
  justify-content: center;
  margin-top: 24px;
}

.error-alert {
  position: fixed;
  top: 20px;
  right: 20px;
  z-index: 1000;
}

.video-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-bottom: 15px;
}

.video-header h3 {
  margin: 0;
  font-size: 18px;
  color: #1d1d1f;
}

.change-video-btn {
  display: flex;
  align-items: center;
  gap: 5px;
  padding: 8px 15px;
  font-size: 14px;
}

.change-video-btn .el-icon {
  font-size: 16px;
}

.has-video .upload-area {
  border: 2px solid #d2d2d7;
  margin-bottom: 20px;
}

.has-video .upload-area:hover {
  border-color: #0071e3;
}

@media (max-width: 768px) {
  .preview-container {
    grid-template-columns: 1fr;
  }

  .hero-section h1 {
    font-size: 32px;
  }

  .subtitle {
    font-size: 18px;
  }
}
</style>


总结

以上就是今天要讲的内容,本文仅仅简单介绍了@ffmpeg 库 的使用,而@ffmpeg提供了大量能使我们快速便捷地处理视频数据的函数和方法。

相关文章:

  • HARCT 2025 分论坛10:Intelligent Medical Robotics智能医疗机器人
  • 详解df -h命令
  • BERT文本分类(PyTorch和Transformers)畅用七个模型架构
  • win11 MBR 启动 如何把我的硬盘改 GPT win11 的 UEFI 启动
  • Springboot3与openApi
  • Golang 语言的内存管理
  • android 安装第三方apk自动赋予运行时权限
  • 二次封装axios解决异步通信痛点
  • Electron 全面解析:跨平台桌面应用开发指南
  • Web前端开发--HTML
  • css: 针对属性left/right/top/bottom为啥设置transition动画不起作用
  • Mysql中使用sql语句生成雪花算法Id
  • Linux内核模块参数与性能优化:__read_mostly属性的深度剖析
  • 前端开发所需参考文档—重中之中
  • postman登录cookie设置
  • 【目标检测xml2txt】label从VOC格式xml文件转YOLO格式txt文件
  • 利用IDEA将Java.class文件反编译为Java文件:原理、实践与深度解析
  • 建筑兔零基础自学python记录18|实战人脸识别项目——视频检测07
  • Docker 部署 MongoDB | 国内阿里镜像
  • 大模型Deepseek的使用_基于阿里云百炼和Chatbox
  • 消息人士称泽连斯基已启程前往土耳其
  • 小耳朵等来了春天:公益义诊筛查专家走进安徽安庆
  • 【社论】公平有序竞争,外卖行业才能多赢
  • 上海国际电影节纪录片单元,还世界真实色彩
  • 香港根据《维护国家安全条例》订立附属法例
  • 图讯丨习近平出席中国-拉美和加勒比国家共同体论坛第四届部长级会议开幕式