【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
{{ props.title }} {{ props.number }}/{{ props.count }} {{ props.number }}/{{ props.count }} 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 } .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; } }
resource-item.vue
无图 播 删 mp4 mp3 {{ file.name }} {{ file.durationStr }} { file.sizeStr }}--> {{ file.sizeStr }} { file.lastModifiedDateStr }}--> 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') } @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; } } }
time-item.vue
{{ props.name }} 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}, }) @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; } }
tool-tab.vue
字母 添加 渲染 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') }
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
添加媒体 添加字体 添加文件夹 {{ file.name }} {{ nowFile.name }} 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 { 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 @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; } }
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})` }