纯网页版视频编辑器
- 一、前言
- 二、功能实现
- 三、所需技术
- 四、部分功能实现
- 4.1 素材预设
- 4.2 多轨道剪辑
一、前言
介绍:本篇文章打算利用纯前端的技术,来实现一个网页版的视频编辑器。为什么突然想做一个这么项目来呢,主要是最近一直在利用手机剪映来剪辑一些照片或者视频之类的,在剪辑的过程中,突然想到,有没有一种纯网页版的视频剪辑网站呢?于是搜了下,大多为 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 来模拟一些素材或者进行预设,比如:
const mockMethods: MockMethod[] = [{url: '/api/getResources',method: 'get',response: ({ query }) => {const type = query.typelet 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
技术,来实现 多轨道剪辑功能。
- 首先创建一个任务队列对象,来存储多轨道的数据,比如视频、音乐、文本等等素材轨道。
private ffmpeg: FFmpegprivate taskQueue = reactive<Task[]>([]) // 任务队列private running = ref(false) // 运行状态public showLog = truepublic playTimeCache = new Map()public audioCache: string[] = []public baseCommand = new Command()
- 然后我们可以对其创建任务,并判断任务队列中是否有执行任务的命令,如果有则返回任务存在,如果没有则返回 undefined。
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}}
- 用户把素材资源从本地拖拽到页面内,需要获取到文件内容
// 获取文件BlobgetFileBlob(filePath: string, fileName: string, format: string) {const fileBuffer = this.getFileBuffer(filePath, fileName, format)return new Blob([fileBuffer], {type: FileTypeMap[format as keyof typeof FileTypeMap]})}
- 最重要最核心的音频合成功能:
// 音频合成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 falsethis.audioCache = [commands.join('')]if (this.fileExist(filePath, fileName)) this.rmFile(filePath)return this.createTask(commands)}
- 获取视频每一帧
// 获取视频帧图片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,如果有需要的话,可以私戳后台,谢谢。