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提供了大量能使我们快速便捷地处理视频数据的函数和方法。