【JS】纯web端使用ffmpeg实现的视频编辑器
废话不多,先上视频。
ffmpeg编辑器
这是一个纯前端实现的视频编辑器,用的ffmpeg的wasm,web框架用的vue3。界面手撸。
界面效果
开发过程
初始化vue3框架
用vite的vue3模板创建一个就可以。
安装的依赖
package.json
"@ffmpeg/core": "^0.11.0",
"@ffmpeg/ffmpeg": "^0.11.5",
"dayjs": "^1.11.6",
"less": "^4.1.2",
"less-loader": "^11.1.0",
创建页面和路由,用的vue-router,简单的添加一下。
router.js
{
path: "/ffmpeg/app",
name: "ffmpeg-app",
component: () => import("../view/ffmpeg/app.vue")
},
开发编辑器
主要项目结构
组件代码
progress-dialog.vue
<template>
<div class="dialog-content">
<div class="dialog-box">
<div class="header">
{{ props.title }}
</div>
<div class="content">
<div class="progress">
<div class="box">
<div class="value" >
{{ props.number }}/{{ props.count }}
</div>
<div class="percent" :style="progress()">
{{ props.number }}/{{ props.count }}
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
const props = defineProps({
title: { type: String, required: false,default:'加载中' },
number:{type:Number,required:true,default:0},
count:{type:Number,required:true,default:100},
})
const progress = () => {
let percent = props.number / props.count
percent = percent > 1 ? 1 : percent
percent = percent * 460
// console.log('百分比',percent)
const style = {
clip:'rect(0px, ' + percent + 'px, 20px, 0px)'
}
// console.log('style',style)
return style
}
</script>
<style lang="less" scoped>
.dialog-content{
position: fixed;
top: 0;
bottom: 0;
left: 0;
right: 0;
background-color: rgba(0,0,0,0.1);
backdrop-filter: blur(10px);
display: flex;
justify-content: center;
align-items: center;
}
.dialog-box{
width: 500px;
//height: 100px;
background-color: #fff;
box-shadow: 0 0 10px #222222;
border-radius: 5px;
.header{
height: 30px;
line-height: 30px;
text-align: center;
font-size: 14px;
font-weight: bold;
border-bottom: 1px solid #999;
}
.content{
display: flex;
align-items: center;
justify-content: center;
height: 50px;
}
}
.progress{
height: 20px;
margin-left: 20px;
margin-right: 20px;
border:1px solid #222;
width: 100%;
position: relative;
.box{
position: relative;
}
.percent{
position: absolute;
top: 0;
left: 0;
right: 0;
background-color: #dc3562;
color:#fff;
height: 20px;
transition: all 0.1s;
width: 100%;
text-align: center;
clip:rect(0px,0px,20px,0px);
}
.value{
position: absolute;
top: 1px;
left: 0;
right: 0;
text-align: center;
height: 20px;
line-height: 20px;
color:#000;
}
}
</style>
resource-item.vue
<template>
<div class="line" :title="file.toString()">
<div class="icon">
<img v-if="file.cover" :src="file.cover"/>
<span v-else>无图</span>
</div>
<div class="info">
<div class="play" @click="handlePlay">播</div>
<div class="del" @click="handleDel">删</div>
<div class="file-type">
<div class="mp4" v-if="file.ext === 'mp4'">mp4</div>
<div class="mp3" v-if="file.ext === 'mp3'">mp3</div>
</div>
<div class="filename" :title="file.name">{{ file.name }}</div>
<div class="k-box-flex">
<div class="size k-flex-1">{{ file.durationStr }}</div>
<!-- <div class="size k-flex-1">{{ file.sizeStr }}</div>-->
<div class="date">{{ file.sizeStr }}</div>
<!-- <div class="date">{{ file.lastModifiedDateStr }}</div>-->
</div>
</div>
</div>
</template>
<script setup>
import ResourceFile from '@/view/ffmpeg/app/type/file.js'
import { toRef } from 'vue'
const emit = defineEmits(['del','play'])
const props = defineProps({
file: { type: Object, required: true,default:() => new ResourceFile() }
})
const file = toRef(props, 'file')
const handleDel = () => {
console.log("点删除")
emit('del')
}
const handlePlay = () => {
console.log("点播放")
emit('play')
}
</script>
<style lang="less" scoped>
@import "../index.less";
.line{
user-select: none;
box-sizing: border-box;
border: @resource-border-color solid 1px;
height: 50px;
display: flex;
.icon{
width: 50px;
box-sizing: border-box;
border: 1px dashed #999;
display: flex;
justify-content: center;
align-items: center;
margin-right: 10px;
img{
max-width: 100%;
max-height: 100%;
}
}
.info{
flex: 1;
position: relative;
padding-top: 5px;
.del{
position: absolute;
right: 5px;
top: 5px;
width: 15px;
height: 15px;
line-height: 15px;
text-align: center;
background-color: palevioletred;
font-size: 10px;
color:#fff;
cursor: pointer;
border-radius: 2px;
}
.play{
position: absolute;
right: 25px;
top: 5px;
width: 15px;
height: 15px;
line-height: 15px;
text-align: center;
background-color: palevioletred;
font-size: 10px;
color:#fff;
cursor: pointer;
border-radius: 2px;
}
.file-type{
position: absolute;
right: 40px;
top: 5px;
height: 15px;
line-height: 15px;
text-align: center;
font-size: 10px;
border-radius: 2px;
padding:0 5px;
.mp4{
color:#fff;
background-color: #07b3c9;
}
.mp3{
color:#fff;
background-color: #d9b608;
}
}
.filename{
font-size: 6px;
width: 220px;
height: 20px;
overflow: hidden;
white-space:nowrap;/*不显示的地方用省略号...代替*/
text-overflow:ellipsis;/* 支持 IE */
}
.size{
font-size: 6px;
text-align: left;
}
.date{
font-size: 6px;
text-align: right;
margin-right: 5px;
}
}
}
</style>
time-item.vue
<template>
<div class="line"
draggable="true"
:title="props.name"
:style="{
width:props.width + 'px',
'margin-left':props.left + 'px',
background:props.color
}">
{{ props.name }}
</div>
</template>
<script setup>
const props = defineProps({
name: { type: String, required: false,default:'文件名' },
color: { type: String, required: false,default:'' },
left:{type:Number,required:true,default:0},
width:{type:Number,required:true,default:10},
})
</script>
<style lang="less" scoped>
@import "../index.less";
.line{
cursor: move;
height: 20px;
white-space: nowrap; /*不显示的地方用省略号...代替*/
text-overflow: ellipsis; /* 支持 IE */
line-height: 20px;
padding-left: 10px;
box-sizing: border-box;
border-bottom: @border-color 1px solid;
background-color: rgba(248, 235, 174, 0.78);
user-select: none;
overflow: hidden;
&:last-child{
border-bottom: none;
}
}
</style>
tool-tab.vue
<template>
<div class="tab">
字母
</div>
<div class="tool-content">
<input type="text" v-model="text">
<button @click="handleCreate">添加</button>
<button @click="handleRender">渲染</button>
</div>
</template>
<script setup>
import {ref} from 'vue'
const text = ref('')
const emit = defineEmits(['create','render'])
const handleCreate = () => {
console.log("添加",text.value)
emit('create', text.value)
text.value = ''
}
const handleRender = () =>{
console.log('渲染')
emit('render')
}
</script>
<style scoped>
</style>
class代码
file.js
import dayjs from 'dayjs'
export default class ResourceFile {
constructor(file) {
this.file = file
this.key = dayjs().unix() + '_' +file.name
this.name = file.name
this.size = file.size
this.sizeStr = file.size
this.type = file.type
this.lastModified = file.lastModified
this.lastModifiedDate = file.lastModifiedDate
this.lastModifiedDateStr = file.lastModifiedDate
this.webkitRelativePath = file.webkitRelativePath
// 外加
// 扩展名
this.ext = ''
// this.baseName = dayjs().format('YYYYMMDDHHmmss') + '_' + file.name
this.baseName = this.key
this.fileType = ''
this.mime = ''
this.cover = ''
this.url = ''
this.durationStr = ''
this.duration = ''
this.bitRate = ''
this.majorBrand = ''
this.encoder = ''
this.resolution = ''
this.fps = ''
this.videoInfo = ''
this.audioType = ''
this.audioRate = ''
this.audioInfo = ''
this.setDate()
}
setUrl(url) {
this.url = url
}
setCover(url){
this.cover = url
}
isVideo() {
return this.mime.indexOf('video') !== -1
}
isAudio() {
return this.mime.indexOf('audio') !== -1
}
setMedia() {
this.fileType ='media'
this.mime = this.file.type.split(',')[0]
this.ext = this.name.split('.')[this.name.split('.').length - 1]
}
setFont() {
this.fileType ='font'
this.mime = 'font'
this.ext = this.name.split('.')[this.name.split('.').length - 1]
}
getFile() {
return this.file
}
getFSName() {
return this.baseName
}
setDate() {
this.lastModifiedDateStr = dayjs(this.lastModifiedDate).format('YYYY-MM-DD HH:mm:ss')
}
setInfo(info) {
this.durationStr = info.durationStr
this.duration = info.duration
this.bitRate = info.bitRate
this.majorBrand = info.majorBrand
this.encoder = info.encoder
this.resolution = info.resolution
this.fps = info.fps
this.videoInfo = info.videoInfo
this.audioType = info.audioType
this.audioRate = info.audioRate
this.audioInfo = info.audioInfo
}
setSize(type = ''){
let str = ''
console.log('size',type,this.size,this.size/1024)
switch (type) {
case 'AUTO':
let G = this.size/1024/1024/1024
let M = this.size/1024/1024
let K = this.size/1024
console.log(G,M,K)
if(G > 1){
str = G.toFixed(2) + 'GB'
}else if(M >1) {
str = M.toFixed(2) + 'MB'
}else if(K > 1) {
str = K.toFixed(2) + 'KB'
}else{
str = this.size + 'B'
}
break
case 'B':
str = this.size + 'B'
break
case 'KB':
str = (this.size/1024).toFixed(2) + 'KB'
break
case 'MB':
str = (this.size/1024/1024).toFixed(2) + 'MB'
break
case 'GB':
str = (this.size/1024/1024/1024).toFixed(2) + 'GB'
break
default:
str = this.size + 'B'
}
this.sizeStr = str
}
toString() {
let str = ''
str += '文件名:' + this.name +'\r\n'
str += '时长:' + this.durationStr +'\r\n'
str += '时长:' + this.duration +'\r\n'
str += '比特率:' + this.bitRate +'\r\n'
str += '格式:' + this.majorBrand +'\r\n'
str += '编码器:' + this.encoder +'\r\n'
str += '分辨率:' + this.resolution +'\r\n'
str += '帧率:' + this.fps +'\r\n'
str += '视频信息:' + this.videoInfo +'\r\n'
str += '音频类型:' + this.audioType +'\r\n'
str += '采样率:' + this.audioRate +'\r\n'
str += '音频信息:' + this.audioInfo +'\r\n'
str += '文件唯一标识:' + this.key +'\r\n'
str += '文件大小:' + this.size +'\r\n'
str += '文件大小:' + this.sizeStr +'\r\n'
str += '文件类型:' + this.type +'\r\n'
str += '最后修改:' + this.lastModified +'\r\n'
str += '最后修改时间:' + this.lastModifiedDate +'\r\n'
str += '最后修改时间:' + this.lastModifiedDateStr +'\r\n'
str += 'webkit路径:' + this.webkitRelativePath +'\r\n'
// 外加
str += '基本名:' + this.baseName +'\r\n'
str += '文件类型:' + this.fileType +'\r\n'
str += 'mime信息:' + this.mime +'\r\n'
str += '扩展名:' + this.ext +'\r\n'
str += '封面:' + this.cover +'\r\n'
return str
}
}
line.js
import { randColor } from '@/utils/color.js'
import { uuid } from '@/utils/key.js'
/**
* 时间揍单个数据
*/
export default class Line{
leftTime = 2
constructor(file) {
// 时间轴唯一
this.key = uuid()
this.name = file.name
this.type = ''
this.duration = file.duration
this.left = 0
this.width = file.duration * this.leftTime
this.color = randColor()
// 原始资源文件名
this.fileKey = file.key
this.font = ''
}
setMedia() {
this.type = 'media'
}
setText() {
this.type = 'text'
}
setFont(path) {
this.font = path
}
getFont() {
return this.font
}
getLeftSecond() {
return parseInt((this.left/this.leftTime))
}
getFile() {
return '/'+this.fileKey
}
}
主要的代码
ffmpeg.js
import { clearEmpty } from '@/utils/string.js'
import { createFFmpeg , fetchFile } from '@ffmpeg/ffmpeg'
import dayjs from 'dayjs'
/**
* ======================================
* 说明:需要用到的ffmpeg操作封装一下
* 作者: YYDS
* 文件: ffmpeg.js
* 日期: 2023/3/29 11:08
* ======================================
*/
export default class Ffmpeg {
static ffmpeg = ''
// 进度输出
static progress = {
/*
* ratio is a float number between 0 to 1.
*/
ratio:0,
time:0
}
// 日志输出
static message = []
// 资源目录
static resourceDir = 'resource'
// 缓存目录
static tmpDir = 'mediaTmp'
// 渲染完的文件名
static renderFileName = 'render.mp4'
static async instance () {
this.ffmpeg = createFFmpeg( {
log: true
})
await this.ffmpeg.load();
this.ffmpeg.FS('mkdir',this.resourceDir)
this.ffmpeg.FS('mkdir',this.tmpDir)
// 设置日志
this.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') {
this.message.push(clearEmpty(message))
}
});
// 设置进度
this.ffmpeg.setProgress((progress) => {
this.progress.ratio = progress.ratio * 100
this.progress.time = progress.time
console.log('进度',progress);
console.log('进度',this.progress);
this.updateProgress(this.progress)
})
}
static updateProgress(progress) {
console.log('进度更新了',progress)
}
static clearMessage() {
this.message = []
}
static loadFile(file){
// console.log('加载的文件',file)
return new Promise(async (resolve) => {
const filePath = '/' + this.resourceDir + '/' + file.getFSName()
const fileData = await fetchFile(file.getFile())
console.log('fileData',fileData)
this.ffmpeg.FS( 'writeFile' , filePath , fileData );
if(file.mime){
let url = URL.createObjectURL( new Blob( [fileData.buffer] , { type: file.mime } ) );
file.setUrl(url)
}
if(file.isVideo()) {
this.readCover(filePath).then(url => {
file.setCover(url)
// console.log('file',file)
console.log('全部日志',this.message)
file.setInfo(this.fileInfoFilter(this.message))
this.clearMessage()
resolve()
})
}else if(file.isAudio()) {
this.readInfo(filePath).then(() => {
console.log('全部日志',this.message)
file.setInfo(this.fileInfoFilter(this.message))
this.clearMessage()
resolve()
})
}else{
resolve()
}
})
}
static readFile(filePath) {
return new Promise(async (resolve) => {
const data = this.ffmpeg.FS( 'readFile' , filePath );
let url = URL.createObjectURL( new Blob( [data.buffer] , { type: 'video/mp4' } ) );
resolve(url)
})
}
static async readCover (path) {
return new Promise(async (resolve, reject) => {
const fileName = dayjs().valueOf()+'.jpg'
const tmpPath = '/'+this.tmpDir +'/'+ fileName
let cmd = '-i ' + path + ' -ss 1 -f image2 ' + tmpPath
let args = cmd.split(' ')
console.log('args',args)
this.ffmpeg.run(...args).then(() => {
// console.log(this.readDir(this.tmpDir))
const data = this.ffmpeg.FS( 'readFile' , tmpPath );
// console.log("文件数据",data)
const fileUrl = URL.createObjectURL( new Blob( [data.buffer] , { type: 'image/jpeg' } ) );
// console.log('文件url',fileUrl)
resolve(fileUrl)
})
})
}
static async readInfo (path) {
return new Promise(async (resolve, reject) => {
const fileName = dayjs().valueOf()+'.jpg'
let cmd = '-i ' + path
let args = cmd.split(' ')
console.log('args',args)
this.ffmpeg.run(...args).then(() => {
resolve()
})
})
}
static readDir (path = '') {
let list = this.ffmpeg.FS( 'readdir' , '/' + path )
console.log('list',list)
return list
}
static messageGetDataCutLastR(message,key) {
let str = message.substring(message.indexOf(key) + key.length)
return str.replace(':','')
}
static fileInfoFilter (messageList) {
const data = {
durationStr:'',
duration:'',
bitRate:'',
majorBrand:'',
encoder:'',
resolution:'',
fps:'',
videoInfo:'',
audioType:'',
audioRate:'',
audioInfo:''
}
messageList.forEach(message => {
if(message.indexOf('Duration') !== -1) {
let duration = message.substring(message.indexOf('Duration:') + 'Duration:'.length ,message.indexOf('Duration:')+ 'Duration:'.length + '00:00:20.48'.length)
console.log("时长",duration)
let time = duration.split(':')
console.log('time',time)
data.durationStr = duration
data.duration = parseInt(time[0])*120 + parseInt(time[1]) *60 +parseFloat(time[2])
}
if(message.indexOf('Duration') !== -1 && message.indexOf('bitrate') !== -1) {
let bitRate = this.messageGetDataCutLastR(message,'bitrate')
console.log("比特率",bitRate)
data.bitRate = bitRate
}
if(message.indexOf('major_brand') !== -1) {
let majorBrand = this.messageGetDataCutLastR(message,'major_brand')
console.log("格式",majorBrand)
data.majorBrand = majorBrand
}
if(message.indexOf('encoder') !== -1) {
let encoder = this.messageGetDataCutLastR(message,'encoder')
console.log("编码器",encoder)
data.encoder = 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)
console.log("分辨率",arrList[2].substring(0,arrList[2].indexOf('[')))
data.resolution=arrList[2].substring(0,arrList[2].indexOf('['))
arrList.forEach(v=>{
if(v.indexOf('fps') !== -1) {
console.log("帧率",v)
data.fps=v
}
})
data.videoInfo=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,)
console.log("音频格式",arrList[0])
console.log("音频采样率",arrList[1])
data.audioType=arrList[0]
data.audioRate=arrList[1]
data.audioInfo=arr
}
})
console.log('信息',data)
return data
}
static generateArgs(timelineList) {
const cmd = []
console.log('时间轴数据',timelineList)
console.log("文件1",this.readDir())
console.log("文件2",this.readDir(this.resourceDir))
let textCmdList = []
timelineList.forEach(time => {
console.log('time',time,time.getLeftSecond())
if(time.type === 'media') {
cmd.push('-i /' + this.resourceDir + time.getFile())
}
if(time.type === 'text') {
// 阶段切换
// cmd.push('-vf drawtext=fontsize=60:fontfile=\'/' + this.resourceDir +'/' +time.getFont() + '\':text=' + time.name + ':fontcolor=green:enable=lt(mod(t\\,3)\\,1):box=1:boxcolor=yellow')
// 显示
cmd.push('-vf drawtext=fontsize=60:fontfile=\'/' + this.resourceDir +'/' +time.getFont() + '\':text=' + time.name + ':fontcolor=green:enable=\'between(t,' + time.getLeftSecond() +','+(time.getLeftSecond() + 6)+')\':box=1:boxcolor=yellow ')
// 多条
// textCmdList.push('drawtext=fontsize=60:fontfile=\'/' + this.resourceDir +'/' +time.getFont() + '\':text=' + time.name + ':fontcolor=green:enable=\'between(t,' + time.getLeftSecond() +','+(time.getLeftSecond() + 6)+')\':box=1:boxcolor=yellow')
}
})
// const textCmd = '-vf "' + textCmdList.join(',') + '"'
// console.log('文字命令',textCmd)
// cmd.push(textCmd)
// 添加最后输出文明
cmd.push(this.renderFileName)
// 命令生成
let args = cmd.join(' ')
args = args.split(' ')
console.log('命令',args)
// 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)
return args
}
static async run(args) {
console.log("运行命令",args)
await this.ffmpeg.run(...args)
}
}
index.less
@border-color:#222;
@resource-border-color:#999;
@resource-width:300px;
::-webkit-scrollbar {
width: 5px;
height: 10px;
background-color: #ebeef5;
}
::-webkit-scrollbar-thumb {
box-shadow: inset 0 0 6px rgba(0, 0, 0, .3);
-webkit-box-shadow: inset 0 0 6px rgba(0, 0, 0, .3);
background-color: #ccc;
}
::-webkit-scrollbar-track{
box-shadow: inset 0 0 5px rgba(0, 0, 0, 0.2);
border-radius: 3px;
background: rgba(255, 255, 255, 1);
}
index.vue
<template>
<div class="app-container">
<div class="resource-list">
<div class="btn-bar">
<input ref="uploadInput" v-show="false" type="file" multiple @change="changeFile"/>
<button @click="addFile('media')">添加媒体</button>
<button @click="addFile('font')">添加字体</button>
<button @click="addDirectory">添加文件夹</button>
</div>
<div class="file-list">
<resource-item
draggable="true"
v-for="(item,index) in mediaList"
:file="item"
:key="item.key"
@play="handlePlay(item)"
@del="handleDel(index)"
@dragstart.private="fileDragStart($event,item)"
@dragend.private="fileDragEnd($event,item)"
@dblclick="appendFile(item)"
/>
</div>
</div>
<div class="view">
<div class="window">
<div class="screen">
<video :src="previewSrc" controls autoplay></video>
</div>
<div class="screen">
<video :src="renderSrc" controls autoplay></video>
</div>
</div>
<div class="time-line"
@dragenter="lineDragEnter"
@dragleave="lineDragLeave"
@dragover="lineDragOver"
@drop="lineDropFile">
<div
class="line"
draggable="true"
v-for="(file,index) in timeLineList"
:title="file.name"
:style="{
width:file.width + 'px',
'margin-left':file.left + 'px',
background:file.color
}"
:key="'timeLine' + file.key"
@dragstart="lineDragStart($event,index)"
@dragend="lineDragEnd"
@dragenter="lineItemDragEnter($event,index)"
@dragleave="lineItemDragLeave"
@dragover="lineItemDragMove"
@drop.prevent.stop="lineItemDropFile(index)"
>{{ file.name }}
</div>
</div>
<div class="tool-bar">
<tool-tab @create="handleCreateText" @render="handleRender"/>
</div>
</div>
<!-- 加载弹窗-->
<progress-dialog :title="progressTitle" v-if="progressVisible" :number="progressNumber" :count="progressCount"/>
<!-- 时间轴示例-->
<div class="hidden">
<div id="move" ref="moveBlock">
{{ nowFile.name }}
</div>
</div>
</div>
</template>
<script setup>
import ResourceFile from '@/view/ffmpeg/app/type/file.js'
import { checkFontFile , checkMediaFile } from '@/view/ffmpeg/app/util.js'
import ft from './ffmpeg.js'
import { reactive , ref } from 'vue'
import ResourceItem from './component/resource-item.vue'
import ToolTab from './component/tool-tab.vue'
import ProgressDialog from '@/view/ffmpeg/app/component/progress-dialog.vue'
ft.instance()
const uploadInput = ref(null)
const previewSrc = ref('')
const renderSrc = ref('')
// 进度条
const progressTitle = ref('')
const progressNumber = ref(0)
const progressCount = ref(100)
const progressVisible = ref(false)
// 媒体资源 图片 视频 音频
const mediaList = reactive([])
// 字体资源
const fontList = reactive([])
// 添加文件
let addType = ''
const addDirectory = () => {
console.log("未实现")
alert('未实现')
}
const addFile = (type) => {
addType = type
uploadInput.value.click()
}
// 选择文件
const changeFile = function (e) {
const files = e.target.files
const mediaLoadList = []
const fontLoadList = []
console.log('文件列表',files)
for ( let i = 0 ; i < files.length ; i++ ) {
const file = new ResourceFile(files[i])
console.log('文件',file)
file.setSize('AUTO')
if(addType === 'media'){
if(checkMediaFile(file)){
file.setMedia()
mediaLoadList.push(file)
// mediaList.push(file)
continue
}
}
if(addType === 'font'){
if(checkFontFile(file)){
file.setFont()
fontLoadList.push(file)
// fontList.push(file)
}
}
}
if(addType === 'media'){
loadMediaFile(mediaLoadList)
}
if(addType === 'font'){
loadFontFile(fontLoadList)
}
}
// 加载文件
const loadMediaFile = async (list) => {
console.log('加载文件',list)
openLoadProgress(list.length,'加载资源文件')
let i = 0
for ( const file of list ) {
await ft.loadFile(file)
mediaList.push(file)
i++
setLoadProgressNumber(i)
}
setTimeout(() => {
closeLoadProgress()
},100)
}
const loadFontFile = async (list) => {
console.log('加载文件',list)
openLoadProgress(list.length,'加载字体文件')
let i = 0
for ( const file of list ) {
await ft.loadFile(file)
fontList.push(file)
i++
setLoadProgressNumber(i)
}
setTimeout(() => {
closeLoadProgress()
},100)
}
const handlePlay = (file) => {
console.log("播放文件",file)
previewSrc.value=file.url
}
const handleDel = (index) => {
console.log("删除文件",index)
mediaList.splice(index,1)
}
// 打开进度条
const openLoadProgress = (count,title = '加载中') => {
progressVisible.value = true
progressCount.value = count
progressNumber.value = 0
progressTitle.value = title
}
// 设置进度条值
const setLoadProgressNumber = (val) => {
progressNumber.value = val
}
// 关闭进度条
const closeLoadProgress = () => {
progressVisible.value = false
progressCount.value = 0
progressNumber.value = 0
progressTitle.value = ''
}
import Line from './type/line.js'
// 时间轴
const timeLineList = ref([])
const moveStartPosition = ref({x:0,y:0})
let moveIndex = ''
let moveIn = ''
// 拖动类型
let dragType = 'create'
// 拖动的当前文件
const nowFile = ref({})
// 拖动的dom
const moveBlock = ref( null )
// 是否拖入时间揍
const lineIn = ref( false )
/**
* 文件列表拖拽开始
* @param $event
* @param file
*/
const fileDragStart = ( $event , file ) => {
console.log( '文件列表拖拽开始' , $event , file )
dragType = 'create'
nowFile.value = file
let width = nowFile.value.duration * 2
moveBlock.value.style.width = (width > 270 ? 270 : width) + 'px'
$event.dataTransfer.setDragImage( moveBlock.value , 0 , 0 )
}
/**
* 文件列表拖拽结束
* @param $event
* @param file
*/
const fileDragEnd = ( $event , file ) => {
console.log( '文件列表拖拽结束' , $event , file )
lineIn.value = false
}
/**
* 添加到时间轴最后
* @param item
*/
const appendFile = (item)=> {
console.log('双击添加',item)
const file = new Line(item)
file.setMedia()
console.log( 'file' , file )
timeLineList.value.push( file )
}
/**
* 时间轴放入
* @param index
*/
const lineItemDropFile = ( index ) => {
console.log( '时间轴放入' , index )
// 放在某个轴上
if ( dragType === 'create' ) {
const file = new Line(mediaList[index])
file.setMedia()
console.log( 'file' , file )
timeLineList.value.splice(index,0,file)
}
// 移动
if ( dragType === 'move' ) {
console.log( '移动' ,moveIndex,index)
let list = timeLineList.value
if(moveIndex > index){
const item = timeLineList.value[moveIndex]
list.splice(moveIndex,1)
list.splice(index,0,item)
}else{
const item = timeLineList.value[moveIndex]
console.log('移动',item,list)
list.splice(moveIndex,1)
list.splice(index,0,item)
console.log("移动到后面",list)
}
timeLineList.value = list
}
}
const lineDragStart = ( $event , index ) => {
console.log( '时间轴拖动开始' , index,$event )
dragType = 'move'
moveIndex = index
moveStartPosition.value.x = $event.pageX
moveStartPosition.value.y = $event.pageY
}
/**
* 时间轴拖动结束
* @param $event
* @constructor
*/
const lineDragEnd = ( $event ) => {
console.log( '时间轴拖动结束' , $event )
console.log('移动了',$event.pageX - moveStartPosition.value.x,$event.pageY-moveStartPosition.value.y)
timeLineList.value[moveIndex].left+=$event.pageX - moveStartPosition.value.x
moveStartPosition.value.x = 0
moveStartPosition.value.y = 0
moveIndex = ''
}
/**
* 时间轴内容进入
* @param $event
* @constructor
*/
const lineItemDragEnter = ( $event,index ) => {
console.log( '时间轴进入' , $event )
$event.preventDefault(); //阻止默认事件
moveIn = index
}
/**
* 时间轴内容离开
* @param $event
* @constructor
*/
const lineItemDragLeave = ( $event ) => {
console.log( '时间轴离开' , $event )
$event.preventDefault(); //阻止默认事件
moveIn = ''
}
/**
* 时间轴内容移动
* @param $event
*/
const lineItemDragMove = ($event) => {
console.log("拖拽移动",$event)
console.log('移动了',$event.pageX - moveStartPosition.value.x,$event.pageY-moveStartPosition.value.y)
}
/**
* 时间轴进入
* @param $event
* @constructor
*/
const lineDragEnter = ( $event ) => {
console.log( '时间列表进入' , $event )
$event.preventDefault(); //阻止默认事件
lineIn.value = true
}
/**
* 时间轴离开
* @param $event
* @param file
* @constructor
*/
const lineDragLeave = ( $event , file ) => {
console.log( '时间列表离开' , $event )
$event.preventDefault(); //阻止默认事件
lineIn.value = false
}
/**
* 时间轴阻止默认
* @param $event
* @constructor
*/
const lineDragOver = ( $event ) => {
$event.preventDefault(); //阻止默认事件
}
/**
* 时间轴放入
* @param $event
*/
const lineDropFile = ( $event ) => {
console.log( '时间列表放入' ,dragType, $event )
// 放在空的地方
if ( dragType === 'create' ) {
let file = new Line(nowFile.value)
file.setMedia()
console.log( 'file' , file )
timeLineList.value.push( file )
}
}
const handleCreateText = (text) => {
let data = {
key : '',
name : text,
duration : 30,
left : 0
}
const item = new Line(data)
item.setText()
item.setFont(fontList[0].getFSName())
console.log('添加',item,fontList[0].getFSName())
timeLineList.value.push(item)
}
const handleRender = () => {
console.log("渲染视频")
let args = ft.generateArgs(timeLineList.value)
ft.run( args )
console.log('ft.progress',ft.progress)
openLoadProgress(100,'渲染中')
ft.updateProgress = updateRender
}
const updateRender = (progress) => {
setLoadProgressNumber(parseInt(progress.ratio))
if(progress.ratio >= 100) {
setTimeout(() => {
closeLoadProgress()
previewRender()
},1000)
}
}
const previewRender = () => {
ft.readFile(ft.renderFileName).then(res => {
console.log("文件",res)
renderSrc.value = res
})
}
window.ft = ft
</script>
<style lang="less" scoped>
@import "index.less";
.app-container{
width: 100vw;
height: 100vh;
display: flex;
position: relative;
}
.resource-list{
display: flex;
flex-direction:column;
width: @resource-width;
height: 100%;
box-sizing: border-box;
border-right: @border-color 1px solid;
.btn-bar{
display: flex;
border-bottom: @border-color 1px solid;
button{
flex: 1;
margin:5px;
}
}
.file-list{
width: 300px;
height: 100%;
overflow-y: auto;
overflow-x: hidden;
}
}
.view{
flex: 1;
display: flex;
flex-direction:column;
.window{
flex:1;
box-sizing: border-box;
border-bottom: @border-color 1px solid;
display: flex;
.screen{
flex: 1;
box-sizing: border-box;
display: flex;
justify-content: center;
align-items: center;
&:first-child{
border-right: @border-color 1px solid;
}
video{
width: 100%;
max-height: 100%;
//max-width: 1024px;
//max-height: 768px;
}
}
}
.time-line{
width: calc(100vw - @resource-width);
height: 300px;
box-sizing: border-box;
border-bottom: @border-color 1px solid;
overflow-x: scroll;
.line{
cursor: move;
height: 20px;
white-space: nowrap; /*不显示的地方用省略号...代替*/
text-overflow: ellipsis; /* 支持 IE */
line-height: 20px;
padding-left: 10px;
box-sizing: border-box;
border-bottom: @border-color 1px solid;
background-color: rgba(248, 235, 174, 0.78);
user-select: none;
overflow: hidden;
&:last-child{
border-bottom: none;
}
}
}
.tool-bar{
height: 100px;
box-sizing: border-box;
}
}
.hidden {
position: fixed;
left: 0;
top: -100px;
#move {
min-width: 20px;
height: 20px;
background: red;
border: 1px solid #07b3c9;
overflow: hidden;
}
}
</style>
util.js
const filterType = ['audio','video','image']
const fontExt = ['ttc','ttf','fon']
export function checkMediaFile(file) {
let status = false
filterType.forEach(type => {
if(file.type.toLowerCase().indexOf(type) !== -1) {
status = true
}
})
return status
}
export function checkFontFile(file) {
if(file.type){
return false
}
let status = false
let nameSplit = file.name.split('.')
let fileExt = nameSplit[nameSplit.length-1].toLowerCase()
fontExt.forEach(type => {
if(fileExt.indexOf(type) !== -1) {
status = true
}
})
return status
}
string.js
/**
* ======================================
* 说明:string处理
* 作者:SKY
* 文件:string.js
* 日期:2022/11/22 16:30
* ======================================
*/
export function clearEmpty(val) {
val = val.replace(' ','')
if(val.indexOf(' ') !== -1) {
return clearEmpty( val )
}else{
return val
}
}
key.js
/**
* 生成UUID
* @return {string}
*/
export function uuid() {
return +new Date() + Math.random()*10+ Math.random()*10+ Math.random()*10+ Math.random()*10 + 'a'
}
color.js
/**
* 随机生成颜色
*/
export function randColor() {
const r = parseInt(Math.random() * 255)
const g = parseInt(Math.random() * 255)
const b = parseInt(Math.random() * 255)
return `rgb(${r},${g},${b})`
}
版权归原作者 飘逸者打瞌睡 所有, 如有侵权,请联系我们删除。