如何利用纯前端技术,实现一个网页版视频编辑器?

纯网页版视频编辑器

一、前言

介绍:本篇文章打算利用纯前端的技术,来实现一个网页版的视频编辑器。为什么突然想做一个这么项目来呢,主要是最近一直在利用手机剪映来剪辑一些照片或者视频之类的,在剪辑的过程中,突然想到,有没有一种纯网页版的视频剪辑网站呢?于是搜了下,大多为 sass 成熟版(需要花钱的那种),然后再加上最近一直在看前端技术,于是就打算利用现学的前端技术,来实现一个纯前端的纯网页版的视频编辑器demo。

先给大家看下整体效果图:

tips:整体看上去像模像样的。

二、功能实现

这里就先简单列下具体的功能包括哪些:

  • 支持深色模式(白天/黑夜)

  • 支持云素材 (暂为mock模拟)以及本地上传素材

  • 支持拖拽添加资源

  • 支持多轨道

  • 支持表单调整资源位置、属性

  • 支持音视频裁剪

  • 支持手动添加贴图、文字

  • 支持时间轴缩放(ctrl+滚轮),最多显示30帧

  • 支持播放预览

  • 支持导出

  • 支持操作撤销、重做功能

  • 支持持久化存储功能

三、所需技术

这里也先简单列下项目中具体用到的技术包括哪些:

  • axios(^1.4.0)
  • element-plus(^2.3.4)
  • mockjs (^1.1.0)
  • pinia (^2.1.3)
  • vue(^3.2.47)
  • typescript(^5.0.2)
  • vite(^4.3.2)

插件包括:

  • commitlint(^17.6.3)
  • ffmpeg(^0.11.6)核心插件
  • cross-env(^7.0.3)
  • eslint(^8.40.0)
  • husky(^8.0.3)
  • postcss(^8.4.23)
  • prettier(^2.8.8)
  • stylelint(^15.6.1)
  • types/node(^20.1.4)
  • element-plus(^2.1.0)

四、部分功能实现

4.1 素材预设

素材预设功能,我们这里是利用了 mock 技术,来代替后端传输的数据。

先利用mock 来模拟一些素材或者进行预设,比如:

js 复制代码
const mockMethods: MockMethod[] = [
  {
    url: '/api/getResources',
    method: 'get',
    response: ({ query }) => {
      const type = query.type
      let data: ResourcesList = []
      if (type === 'video') {
        data = [
          {
            title: '转场',
            type: 'video',
            items: [
              {
                name: '故障雪花屏.mp4',
                format: 'mp4',
                cover: '/image/video/故障雪花屏.jpg',
                source: '/video/故障雪花屏.mp4',
                width: 1920,
                height: 1080,
                fps: 30,
                frameCount: 30,
                time: 1000
              }
            ]
          }
        ]
      } else if (type === 'audio') {
        data = [
          {
            title: '旋律',
            type: 'audio',
            items: [
              {
                cover: '/image/audio/Charms.jpg',
                time: 244000,
                format: 'mp3',
                name: 'Charms.mp3',
                singer: 'Abel Korzeniowski',
                source: '/audio/Abel Korzeniowski - Charms.mp3'
              }
             
            ]
          }
        ]
      } else if (type === 'text') {
        data = [
          {
            title: '热门',
            type: 'text',
            items: [
              {
                name: 'CherryBombOne.ttf',
                templateId: 0,
                source: '/text/CherryBombOne-Regular.ttf',
                format: 'truetype'
              }
          }
        ]
      } else if (type === 'image') {
        data = [
          {
            title: '热门',
            type: 'image',
            items: [
              {
                name: '666.gif',
                cover: '/image/image/666.gif',
                source: '/image/image/666.gif',
                format: 'gif',
                width: 199,
                height: 200,
                sourceFrame: 8
              }
          },
          {
            title: '经典',
            type: 'image',
            items: [
              {
                name: '喇叭.gif',
                cover: '/image/image/喇叭.gif',
                source: '/image/image/喇叭.gif',
                format: 'gif',
                width: 199,
                height: 200,
                sourceFrame: 6
              },
              {
                name: '马赛克.gif',
                cover: '/image/image/马赛克.gif',
                source: '/image/image/马赛克.gif',
                format: 'gif',
                width: 199,
                height: 200,
                sourceFrame: 6
              },
              {
                name: '马赛克小人.gif',
                cover: '/image/image/马赛克小人.gif',
                source: '/image/image/马赛克小人.gif',
                format: 'gif',
                width: 199,
                height: 200,
                sourceFrame: 6
              },
              {
                name: '闪光.gif',
                cover: '/image/image/闪光.gif',
                source: '/image/image/闪光.gif',
                format: 'gif',
                width: 199,
                height: 200,
                sourceFrame: 6
              }
        ]
      }
      return {
        code: 200,
        data
      }
    }
  }
]

export default mockMethods

代码写完后,不要忘记把素材也要放到项目文件夹里

4.2 多轨道剪辑

什么是多轨道剪辑?

多轨道编辑即是将不同的素材放置在不同的轨道上,通过调整它们在时间线上的位置和长度,达到叠加、剪辑和混合的效果。 我们可以通过拖拽素材到时间线上的不同轨道来进行多轨道编辑。

通常情况下,视频素材放置在视频轨道上,音频素材放置在音频轨道上。这样,我们可以通过调整素材在时间线上的位置和长度来控制视频和音频的播放顺序、时长和重叠关系

从技术角度来实现的话,这里就通过用 ffmpeg 技术,来实现 多轨道剪辑功能。

  1. 首先创建一个任务队列对象,来存储多轨道的数据,比如视频、音乐、文本等等素材轨道。
js 复制代码
  private ffmpeg: FFmpeg
  private taskQueue = reactive<Task[]>([]) // 任务队列
  private running = ref(false) // 运行状态
  public showLog = true
  public playTimeCache = new Map()
  public audioCache: string[] = []
  public baseCommand = new Command()
  1. 然后我们可以对其创建任务,并判断任务队列中是否有执行任务的命令,如果有则返回任务存在,如果没有则返回 undefined。
js 复制代码
 createTask(commands: string[]) {
    const task = this.existTask(commands)
    if (task) {
      return task.instance
    } else {
      const callbacks = {}
      const instance = new Promise((resolve, reject) => {
        Object.assign(callbacks, {
          resolve,
          reject
        })
      })
      this.taskQueue.push({
        instance,
        commands,
        ...callbacks
      } as Task)
      return instance
    }
  }
  1. 用户把素材资源从本地拖拽到页面内,需要获取到文件内容
js 复制代码
 // 获取文件Blob
  getFileBlob(filePath: string, fileName: string, format: string) {
    const fileBuffer = this.getFileBuffer(filePath, fileName, format)
    return new Blob([fileBuffer], {
      type: FileTypeMap[format as keyof typeof FileTypeMap]
    })
  }
  1. 最重要最核心的音频合成功能:
js 复制代码
// 音频合成
  async mergeAudio(
    start: number,
    itemList: TrackItem[],
    fileName: string,
    filePath: string
  ) {
    const { commands } = this.baseCommand.mergeAudio(
      this.pathConfig,
      start,
      itemList
    )
    if (this.audioCache.indexOf(commands.join('')) > -1) return false
    this.audioCache = [commands.join('')]
    if (this.fileExist(filePath, fileName)) this.rmFile(filePath)
    return this.createTask(commands)
  }
  1. 获取视频每一帧
js 复制代码
 // 获取视频帧图片
  getFrame(videoName: string, frameIndex: number) {
    const framePath = `${this.pathConfig.framePath}${videoName}`
    const fileName = `/pic-${frameIndex}`
    // return this.getFileBlob(framePath, fileName, 'jpg')
    return this.getFileBuffer(framePath, fileName, 'jpg')
  }

目前只是一个简易的demo,如果有需要的话,可以私戳后台,谢谢。

相关推荐
沉默璇年21 分钟前
react中useMemo的使用场景
前端·react.js·前端框架
yqcoder27 分钟前
reactflow 中 useNodesState 模块作用
开发语言·前端·javascript
2401_8827275736 分钟前
BY组态-低代码web可视化组件
前端·后端·物联网·低代码·数学建模·前端框架
会发光的猪。1 小时前
css使用弹性盒,让每个子元素平均等分父元素的4/1大小
前端·javascript·vue.js
天下代码客1 小时前
【vue】vue中.sync修饰符如何使用--详细代码对比
前端·javascript·vue.js
猫爪笔记2 小时前
前端:HTML (学习笔记)【1】
前端·笔记·学习·html
前端李易安2 小时前
Webpack 热更新(HMR)详解:原理与实现
前端·webpack·node.js
红绿鲤鱼2 小时前
React-自定义Hook与逻辑共享
前端·react.js·前端框架
周全全2 小时前
Spring Boot + Vue 基于 RSA 的用户身份认证加密机制实现
java·vue.js·spring boot·安全·php
Domain-zhuo2 小时前
什么是JavaScript原型链?
开发语言·前端·javascript·jvm·ecmascript·原型模式