【前端】使用ffmpeg+vue3实现简单的视频编辑器

使用ffmpeg+vue3实现简单的视频编辑器

主要依赖 ‘@ffmpeg/ffmpeg’,‘vue’

实现功能

视频转码,添加文字水印,添加图片水印,处理进度条和用时,文件编码信息,生成序列帧。

界面效果

ffmpeg演示

在这里插入图片描述

渲染成品

fmpeg渲染成品

实现代码

<template>
  <div class="menu-bar k-box-flex">
    菜单
    <input @change="changFile" type="file" id="uploader" placeholder="视频文件">
    <input @change="changImage" type="file" id="uploaderImage" placeholder="图片水印">
    <input @change="changFont" type="file" id="uploaderFont" placeholder="字体文件">
    <input v-model="subtitle" placeholder="字幕">
  </div>
  <div class="video-screen k-box-flex">
    <video id="screen-video" ref="screenVideo" controls></video>
  </div>
  <div class="track">
    <img v-for="img in track" :src="img" alt="序列帧">
  </div>
  <div class="message k-text-center">
    {{ message }}
  </div>
  <div class="progress">
    <progress max="1" :value="percent"/>用时:{{ eTime }}
  </div>
  <div class="btn-bar k-box-flex">
    <button @click="handleLoad">
      加载视频
    </button>
    <button @click="handleFrame">
      生成序列帧
    </button>
    <button @click="handleSubtitle">
      添加字幕
    </button>
    <button @click="handleRender">
      添加图片
    </button>
  </div>
  <div>
    <div class="file-info">
      <p>文件名:{{ filename }}</p>
      <p>时长:{{ duration }}</p>
      <p>格式:{{ majorBrand }}</p>
      <p>编码器:{{ encoder }}</p>
      <p>分辨率:{{ resolution }}</p>
      <p>比特率:{{ bitRate }}</p>
      <p>帧率:{{ fps }}</p>
      <p>音频格式:{{ audio }}</p>
      <p>音频采样率:{{ audioRate }}</p>
    </div>
  </div>
</template>

<script setup>
import { clearEmpty } from '@/utils/string.js'
import { createFFmpeg , fetchFile } from '@ffmpeg/ffmpeg'
import { onMounted , reactive , ref } from 'vue'
// 配置
const frameDir = 'frame'


// 变量
let step = ''
// 轨道
const track = ref([])
// 文件名
const filename = ref('')
// 时长
const duration = ref('')
// 格式
const majorBrand = ref('')
// 编码器
const encoder = ref('')
// 分辨率
const resolution = ref('')
// 比特率
const bitRate = ref('')
// 帧率
const fps = ref('')
// 音频格式
const audio = ref('')
// 音频采样率
const audioRate = ref('')
// 输出屏幕图片
const image = ref('')
// 字幕
const subtitle = ref('飘逸者打瞌睡')
// 进度条
const percent = ref(0)
const eTime = ref('')

const message = ref('ffmpeg 开发环境只能localhost访问')
const ffmpeg = createFFmpeg( {
  log: true
})
ffmpeg.setLogger(({ type, message }) => {
  // console.log('日志',type, message);
  /*
   * type can be one of following:
   *
   * info: internal workflow debug messages
   * fferr: ffmpeg native stderr output
   * ffout: ffmpeg native stdout output
   */
  if(type === 'fferr') {
    message = clearEmpty(message)
    // 文件信息处理
    fileInfoFilter(message)
  }
});
ffmpeg.setProgress((progress) => {
  console.log('进度',progress);
  percent.value = progress.ratio
  eTime.value = progress.time
  /*
   * ratio is a float number between 0 to 1.
   */
});
const fileInfoFilter = (message) => {
  // 加载视频时候才处理
  if(step === 'load'){
    if(message.indexOf('Duration') !== -1) {
      duration.value = message.substring(message.indexOf('Duration:') + 'Duration:'.length ,message.indexOf('Duration:')+ 'Duration:'.length + '00:00:20.48'.length)
      console.log("时长",duration)
    }
    if(message.indexOf('Duration') !== -1 && message.indexOf('bitrate') !== -1) {
      bitRate.value = message.substring(message.indexOf('bitrate:') + 'bitrate:'.length)
      console.log("比特率",bitRate)
    }
    if(message.indexOf('major_brand') !== -1) {
      let key = 'major_brand:'
      majorBrand.value = message.substring(message.indexOf(key) + key.length)
      console.log("格式",majorBrand)
    }
    if(message.indexOf('encoder') !== -1) {
      let key = 'encoder:'
      encoder.value = message.substring(message.indexOf(key) + key.length)
      console.log("编码器",encoder)
    }
    if(message.indexOf('Video:') !== -1) {
      let key = 'Video:'
      let arr = message.substring(message.indexOf(key) + key.length)
      let arrList =  arr.split(',')
      console.log("视频信息",arr,)
      resolution.value = arrList[2].substring(0,arrList[2].indexOf('['))
      console.log("分辨率",arr)
      fps.value = arrList[3]
      console.log("帧率",arr)
    }
    if(message.indexOf('Audio:') !== -1) {
      let key = 'Audio:'
      let arr = message.substring(message.indexOf(key) + key.length)
      let arrList =  arr.split(',')
      console.log("音频信息",arr,)
      audio.value = arrList[0]
      console.log("音频格式",arr)
      audioRate.value = arrList[1]
      console.log("音频采样率",arr)
    }
  }
}
const screenVideo = ref(null)
let video = ''
onMounted(() => {
  console.log("screenVideo",screenVideo)
})

const changFile = (file) => {
  console.log("选择文件",file.target.files)
  video = file.target.files[0]
  filename.value = video.name
}

// 水印
let  watermark = ''
const changImage = async (file) => {
  console.log("选择水印文件",file.target.files)
  watermark = file.target.files[0]
  ffmpeg.FS( 'writeFile' , 'watermark.png' , await fetchFile(watermark ) );
  const data = ffmpeg.FS( 'readFile' , 'watermark.png' );
  console.log("文件数据",data)
}

// 字体  未实现 报错
const changFont = async (file) => {
  console.log("选择字体文件",file.target.files)
  let  font = ''
  font = file.target.files[0]
  ffmpeg.FS( 'writeFile' , 'font.ttf' , await fetchFile(font) );
  const data = ffmpeg.FS( 'readFile' , 'font.ttf' );
  console.log("文件数据",data)
}

const handleLoad = async () => {
  console.log("加载视频",video)
  if(!video) {
    alert('请选择视频')
    return
  }
  if(ffmpeg.isLoaded()) {
    alert('视频已加载')
    return
  }
  step = 'load'
  await ffmpeg.load();
  ffmpeg.FS( 'writeFile' , 'infile' , await fetchFile(video ) );
  await ffmpeg.run('-i' , 'infile')
  step = ''
}

const handleRender = async () => {
  console.log('screenVideo',screenVideo)
  if(!ffmpeg.isLoaded()) {
    alert('请加载视频')
    return
  }
  // const fontData = ffmpeg.FS( 'readFile' , 'font' );
  // console.log("文件数据",fontData)
  // const cmd = '-i infile -vf "drawtext=fontsize=60:fontfile=ariali.ttf:text=\'%{localtime\\:%Y\\-%m\\-%d%H-%M-%S}\':fontcolor=green:box=1:boxcolor=yellow" outfile.mp4'
  // 图片水印
  const cmd = '-i infile -vf movie=watermark.png,colorkey=white:0.01:1.0[wm];[in][wm]overlay=30:10[out] outfile.mp4'
  // const cmd = '-re -i infile -vf drawtext=fontsize=60:fontfile=\'font\':text=\'%{localtime\\:%Y\\-%m\\-%d%H-%M-%S}\':fontcolor=green:box=1:boxcolor=yellow outfile.mp4'
  let args = cmd.split(' ')
  console.log('args',args)
  await ffmpeg.run( ...args )
  const data = ffmpeg.FS( 'readFile' , 'outfile.mp4' );
  console.log("文件数据",data)
  screenVideo.value.src = URL.createObjectURL( new Blob( [data.buffer] , { type: 'video/mp4' } ) );
}

const handleFrame = async () => {
  console.log("生成序列帧")
  if(!ffmpeg.isLoaded()) {
    alert('请加载视频')
    return
  }
  ffmpeg.FS('mkdir',frameDir)
  let cmd = '-i infile -r 1 -q:v 2 -f image2 /'+frameDir+'/%3d.jpeg'
  let args = cmd.split(' ')
  console.log('args',args)
  await ffmpeg.run(...args);
  // const data = ffmpeg.FS( 'readFile' , frameDir+'/001.jpeg' );
  // console.log("文件数据",data)
  // image.value = URL.createObjectURL( new Blob( [data.buffer] , { type: 'video/mp4' } ) );
  const fileList = ffmpeg.FS('readdir','/'+frameDir)
  console.log("文件列表",fileList)
  track.value = []
  fileList.forEach(v=>{
    if(v !== '.' && v !=='..') {
      const path = frameDir + '/' + v
      const img = ffmpeg.FS( 'readFile' ,path );
      let imgData = URL.createObjectURL( new Blob( [img.buffer] , { type: 'image/jpeg' } ) );
      track.value.push(imgData)
    }
  })
  console.log("轨道数据",track)
}

const handleSubtitle =  async () => {
  console.log("添加字幕",subtitle)
  if(!ffmpeg.isLoaded()) {
    alert('请加载视频')
    return
  }
  //  const cmd = '-re -i infile -vf drawtext=fontsize=60:fontfile=font.ttf:text=\'%{localtime\\:%Y\\-%m\\-%d%H-%M-%S}\':fontcolor=green:box=1:boxcolor=yellow outfile.mp4'
   const cmd = '-re -i infile -vf drawtext=fontsize=60:fontfile=font.ttf:text=' + subtitle.value + ':fontcolor=green:box=1:boxcolor=yellow outfile.mp4'
  let args = cmd.split(' ')
  console.log('args',args)
  await ffmpeg.run( ...args )
  const data = ffmpeg.FS( 'readFile' , 'outfile.mp4' );
  console.log("文件数据",data)
  screenVideo.value.src = URL.createObjectURL( new Blob( [data.buffer] , { type: 'video/mp4' } ) );
}
</script>

<style lang="less" scoped>
.video-screen{
  width: 100%;
  height: 384px;
  #screen-video{
    width: 100%;
    height: 384px;
  }
}
.out-screen{
  width: 300px;
  height: 240px;
  img{
    max-height: 100%;
    max-width: 100%;
  }
}
.track{
  width: 100%;
  height: 130px;
  overflow-x: scroll;
  white-space: nowrap;
  img{
    display: inline-block;
    width: auto;
    height: 100px;
  }
}
</style>

string.js
···
export function clearEmpty(val) {
val = val.replace(’ ‘,’‘)
if(val.indexOf(’ ') !== -1) {
return clearEmpty( val )
}else{
return val
}
}

···

视频分片上传是一种常见的大文件上传方式,可以有效地避免上传过程中网络不稳定、服务器压力过大等问题。下面是使用Vue、Spring Boot和FFmpeg实现视频分片上传的大致流程: 1. 前端使用Vue编写上传组件,将视频文件进行分片并上传到服务器。 2. 后端使用Spring Boot接收前端上传的视频分片,并将分片存储到服务器上。 3. 在所有分片上传完成后,后端使用FFmpeg将分片合并成一个完整的视频文件。 下面是具体实现步骤: 前端: 1. 安装vue-upload-component组件,在Vue组件中引入该组件。 2. 在Vue组件中编写上传方法,将视频文件进行分片并上传到服务器。分片的大小可以根据实际情况进行设置,一般为1MB ~ 2MB。 3. 在上传过程中,可以实现进度条、暂停上传、继续上传等功能,以提升用户体验。 后端: 1. 使用Spring Boot编写接收上传分片的接口,将分片存储到服务器上。可以使用Spring Boot提供的MultipartFile类来接收前端上传的文件。 2. 在接收到所有分片后,使用FFmpeg将分片合并成一个完整的视频文件。可以使用FFmpeg的命令行工具,也可以使用FFmpeg的Java API。 3. 合并完成后,可以将视频文件存储到服务器的指定路径下,或者将视频文件存储到云存储中。 综上所述,使用Vue、Spring Boot和FFmpeg实现视频分片上传可以有效地解决大文件上传过程中遇到的问题,提升用户体验,并且保证视频文件的完整性。
评论 27
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值