【JS】纯web端使用ffmpeg实现的视频编辑器
admin 阅读: 2024-03-29
后台-插件-广告管理-内容页头部广告(手机) |
【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",- 1
- 2
- 3
- 4
- 5
- 6
创建页面和路由,用的vue-router,简单的添加一下。
router.js
- 1
- 2
- 3
- 4
- 5
开发编辑器
主要项目结构
组件代码
progress-dialog.vue
{{ props.title }} {{ props.number }}/{{ props.count }} {{ props.number }}/{{ props.count }}- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
- 67
- 68
- 69
- 70
- 71
- 72
- 73
- 74
- 75
- 76
- 77
- 78
- 79
- 80
- 81
- 82
- 83
- 84
- 85
- 86
- 87
- 88
- 89
- 90
- 91
- 92
- 93
- 94
- 95
- 96
- 97
- 98
- 99
- 100
- 101
- 102
- 103
- 104
- 105
- 106
- 107
- 108
- 109
- 110
- 111
resource-item.vue
%20%20%20%20%20%20无图 %20%20%20%20 %20%20%20%20 %20%20%20%20%20%20播 %20%20%20%20%20%20删 %20%20%20%20%20%20 %20%20%20%20%20%20%20%20mp4 %20%20%20%20%20%20%20%20mp3 %20%20%20%20%20%20 %20%20%20%20%20%20{{%20file.name%20}} %20%20%20%20%20%20 %20%20%20%20%20%20%20%20{{%20file.durationStr%20}} {%20file.sizeStr%20}}--> %20%20%20%20%20%20%20%20{{%20file.sizeStr%20}} {%20file.lastModifiedDateStr%20}}--> %20%20%20%20%20%20 %20%20%20%20 %20%20- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
- 67
- 68
- 69
- 70
- 71
- 72
- 73
- 74
- 75
- 76
- 77
- 78
- 79
- 80
- 81
- 82
- 83
- 84
- 85
- 86
- 87
- 88
- 89
- 90
- 91
- 92
- 93
- 94
- 95
- 96
- 97
- 98
- 99
- 100
- 101
- 102
- 103
- 104
- 105
- 106
- 107
- 108
- 109
- 110
- 111
- 112
- 113
- 114
- 115
- 116
- 117
- 118
- 119
- 120
- 121
- 122
- 123
- 124
- 125
- 126
- 127
- 128
- 129
- 130
- 131
- 132
- 133
- 134
- 135
time-item.vue
%20 %20%20 %20%20%20%20{{%20props.name%20}} %20%20- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
tool-tab.vue
%20 %20%20 %20%20%20%20字母 %20%20 %20%20 %20%20%20%20 %20%20%20%20 %20%20%20%20 %20%20- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
class代码
%20file.js
%20 import%20dayjs%20from%20'dayjs' export%20default%20class%20ResourceFile%20{ %20%20%20%20constructor(file)%20{ %20%20%20%20%20%20%20%20this.file%20=%20file %20%20%20%20%20%20%20%20this.key%20=%20dayjs().unix()%20+%20'_'%20+file.name %20%20%20%20%20%20%20%20this.name%20=%20file.name %20%20%20%20%20%20%20%20this.size%20=%20file.size %20%20%20%20%20%20%20%20this.sizeStr%20=%20file.size %20%20%20%20%20%20%20%20this.type%20=%20file.type %20%20%20%20%20%20%20%20this.lastModified%20=%20file.lastModified %20%20%20%20%20%20%20%20this.lastModifiedDate%20=%20file.lastModifiedDate %20%20%20%20%20%20%20%20this.lastModifiedDateStr%20=%20file.lastModifiedDate %20%20%20%20%20%20%20%20this.webkitRelativePath%20=%20file.webkitRelativePath %20%20%20%20%20%20%20%20//%20外加 %20%20%20%20%20%20%20%20//%20扩展名 %20%20%20%20%20%20%20%20this.ext%20=%20'' %20%20%20%20%20%20%20%20//%20this.baseName%20=%20dayjs().format('YYYYMMDDHHmmss')%20+%20'_'%20+%20file.name %20%20%20%20%20%20%20%20this.baseName%20=%20this.key %20%20%20%20%20%20%20%20this.fileType%20=%20'' %20%20%20%20%20%20%20%20this.mime%20=%20'' %20%20%20%20%20%20%20%20this.cover%20=%20'' %20%20%20%20%20%20%20%20this.url%20=%20'' %20%20%20%20%20%20%20%20this.durationStr%20=%20'' %20%20%20%20%20%20%20%20this.duration%20=%20'' %20%20%20%20%20%20%20%20this.bitRate%20=%20'' %20%20%20%20%20%20%20%20this.majorBrand%20=%20'' %20%20%20%20%20%20%20%20this.encoder%20=%20'' %20%20%20%20%20%20%20%20this.resolution%20=%20'' %20%20%20%20%20%20%20%20this.fps%20=%20'' %20%20%20%20%20%20%20%20this.videoInfo%20=%20'' %20%20%20%20%20%20%20%20this.audioType%20=%20'' %20%20%20%20%20%20%20%20this.audioRate%20=%20'' %20%20%20%20%20%20%20%20this.audioInfo%20=%20'' %20%20%20%20%20%20%20%20this.setDate() %20%20%20%20} %20%20%20%20setUrl(url)%20{ %20%20%20%20%20%20%20%20this.url%20=%20url %20%20%20%20} %20%20%20%20setCover(url){ %20%20%20%20%20%20%20%20this.cover%20=%20url %20%20%20%20} %20%20%20%20isVideo()%20{ %20%20%20%20%20%20%20%20return%20this.mime.indexOf('video')%20!==%20-1 %20%20%20%20} %20%20%20%20isAudio()%20{ %20%20%20%20%20%20%20%20return%20this.mime.indexOf('audio')%20!==%20-1 %20%20%20%20} %20%20%20%20setMedia()%20{ %20%20%20%20%20%20%20%20this.fileType%20='media' %20%20%20%20%20%20%20%20this.mime%20=%20this.file.type.split(',')[0] %20%20%20%20%20%20%20%20this.ext%20=%20this.name.split('.')[this.name.split('.').length%20-%201] %20%20%20%20} %20%20%20%20setFont()%20{ %20%20%20%20%20%20%20%20this.fileType%20='font' %20%20%20%20%20%20%20%20this.mime%20=%20'font' %20%20%20%20%20%20%20%20this.ext%20=%20this.name.split('.')[this.name.split('.').length%20-%201] %20%20%20%20} %20%20%20%20getFile()%20{ %20%20%20%20%20%20%20%20return%20this.file %20%20%20%20} %20%20%20%20getFSName()%20{ %20%20%20%20%20%20%20%20return%20this.baseName %20%20%20%20} %20%20%20%20setDate()%20{ %20%20%20%20%20%20%20%20this.lastModifiedDateStr%20=%20dayjs(this.lastModifiedDate).format('YYYY-MM-DD%20HH:mm:ss') %20%20%20%20} %20%20%20%20setInfo(info)%20{ %20%20%20%20%20%20%20%20this.durationStr%20=%20info.durationStr %20%20%20%20%20%20%20%20this.duration%20=%20info.duration %20%20%20%20%20%20%20%20this.bitRate%20=%20info.bitRate %20%20%20%20%20%20%20%20this.majorBrand%20=%20info.majorBrand %20%20%20%20%20%20%20%20this.encoder%20=%20info.encoder %20%20%20%20%20%20%20%20this.resolution%20=%20info.resolution %20%20%20%20%20%20%20%20this.fps%20=%20info.fps %20%20%20%20%20%20%20%20this.videoInfo%20=%20info.videoInfo %20%20%20%20%20%20%20%20this.audioType%20=%20info.audioType %20%20%20%20%20%20%20%20this.audioRate%20=%20info.audioRate %20%20%20%20%20%20%20%20this.audioInfo%20=%20info.audioInfo %20%20%20%20} %20%20%20%20setSize(type%20=%20''){ %20%20%20%20%20%20%20%20let%20str%20=%20'' %20%20%20%20%20%20%20%20console.log('size',type,this.size,this.size/1024) %20%20%20%20%20%20%20%20switch%20(type)%20{ %20%20%20%20%20%20%20%20%20%20%20%20case%20'AUTO': %20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20let%20G%20=%20this.size/1024/1024/1024 %20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20let%20M%20=%20this.size/1024/1024 %20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20let%20K%20=%20this.size/1024 %20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20console.log(G,M,K) %20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20if(G%20>%201){ %20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20str%20=%20G.toFixed(2)%20+%20'GB' %20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20}else%20if(M%20>1)%20{ %20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20str%20=%20M.toFixed(2)%20+%20'MB' %20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20}else%20if(K%20>%201)%20{ %20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20str%20=%20K.toFixed(2)%20+%20'KB' %20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20}else{ %20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20str%20=%20this.size%20+%20'B' %20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20} %20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20break %20%20%20%20%20%20%20%20%20%20%20%20case%20'B': %20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20str%20=%20this.size%20+%20'B' %20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20break %20%20%20%20%20%20%20%20%20%20%20%20case%20'KB': %20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20str%20=%20(this.size/1024).toFixed(2)%20+%20'KB' %20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20break %20%20%20%20%20%20%20%20%20%20%20%20case%20'MB': %20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20str%20=%20(this.size/1024/1024).toFixed(2)%20+%20'MB' %20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20break %20%20%20%20%20%20%20%20%20%20%20%20case%20'GB': %20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20str%20=%20(this.size/1024/1024/1024).toFixed(2)%20+%20'GB' %20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20break %20%20%20%20%20%20%20%20%20%20%20%20default: %20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20str%20=%20this.size%20+%20'B' %20%20%20%20%20%20%20%20} %20%20%20%20%20%20%20%20this.sizeStr%20=%20str %20%20%20%20} %20%20%20%20toString()%20{ %20%20%20%20%20%20%20%20let%20str%20=%20'' %20%20%20%20%20%20%20%20str%20+=%20'文件名:'%20+%20this.name%20+'\r\n' %20%20%20%20%20%20%20%20str%20+=%20'时长:'%20+%20this.durationStr%20+'\r\n' %20%20%20%20%20%20%20%20str%20+=%20'时长:'%20+%20this.duration%20+'\r\n' %20%20%20%20%20%20%20%20str%20+=%20'比特率:'%20+%20this.bitRate%20+'\r\n' %20%20%20%20%20%20%20%20str%20+=%20'格式:'%20+%20this.majorBrand%20+'\r\n' %20%20%20%20%20%20%20%20str%20+=%20'编码器:'%20+%20this.encoder%20+'\r\n' %20%20%20%20%20%20%20%20str%20+=%20'分辨率:'%20+%20this.resolution%20+'\r\n' %20%20%20%20%20%20%20%20str%20+=%20'帧率:'%20+%20this.fps%20+'\r\n' %20%20%20%20%20%20%20%20str%20+=%20'视频信息:'%20+%20this.videoInfo%20+'\r\n' %20%20%20%20%20%20%20%20str%20+=%20'音频类型:'%20+%20this.audioType%20+'\r\n' %20%20%20%20%20%20%20%20str%20+=%20'采样率:'%20+%20this.audioRate%20+'\r\n' %20%20%20%20%20%20%20%20str%20+=%20'音频信息:'%20+%20this.audioInfo%20+'\r\n' %20%20%20%20%20%20%20%20str%20+=%20'文件唯一标识:'%20+%20this.key%20+'\r\n' %20%20%20%20%20%20%20%20str%20+=%20'文件大小:'%20+%20this.size%20+'\r\n' %20%20%20%20%20%20%20%20str%20+=%20'文件大小:'%20+%20this.sizeStr%20+'\r\n' %20%20%20%20%20%20%20%20str%20+=%20'文件类型:'%20+%20this.type%20+'\r\n' %20%20%20%20%20%20%20%20str%20+=%20'最后修改:'%20+%20this.lastModified%20+'\r\n' %20%20%20%20%20%20%20%20str%20+=%20'最后修改时间:'%20+%20this.lastModifiedDate%20+'\r\n' %20%20%20%20%20%20%20%20str%20+=%20'最后修改时间:'%20+%20this.lastModifiedDateStr%20+'\r\n' %20%20%20%20%20%20%20%20str%20+=%20'webkit路径:'%20+%20this.webkitRelativePath%20+'\r\n' %20%20%20%20%20%20%20%20//%20外加 %20%20%20%20%20%20%20%20str%20+=%20'基本名:'%20+%20this.baseName%20+'\r\n' %20%20%20%20%20%20%20%20str%20+=%20'文件类型:'%20+%20this.fileType%20+'\r\n' %20%20%20%20%20%20%20%20str%20+=%20'mime信息:'%20+%20this.mime%20+'\r\n' %20%20%20%20%20%20%20%20str%20+=%20'扩展名:'%20+%20this.ext%20+'\r\n' %20%20%20%20%20%20%20%20str%20+=%20'封面:'%20+%20this.cover%20+'\r\n' %20%20%20%20%20%20%20%20return%20str %20%20%20%20} }- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
- 67
- 68
- 69
- 70
- 71
- 72
- 73
- 74
- 75
- 76
- 77
- 78
- 79
- 80
- 81
- 82
- 83
- 84
- 85
- 86
- 87
- 88
- 89
- 90
- 91
- 92
- 93
- 94
- 95
- 96
- 97
- 98
- 99
- 100
- 101
- 102
- 103
- 104
- 105
- 106
- 107
- 108
- 109
- 110
- 111
- 112
- 113
- 114
- 115
- 116
- 117
- 118
- 119
- 120
- 121
- 122
- 123
- 124
- 125
- 126
- 127
- 128
- 129
- 130
- 131
- 132
- 133
- 134
- 135
- 136
- 137
- 138
- 139
- 140
- 141
- 142
- 143
- 144
- 145
- 146
- 147
- 148
- 149
line.js
%20 import%20{%20randColor%20}%20from%20'@/utils/color.js' import%20{%20uuid%20}%20from%20'@/utils/key.js' /** %20*%20时间揍单个数据 %20*/ export%20default%20class%20Line{ %20%20%20%20leftTime%20=%202 %20%20%20%20constructor(file)%20{ %20%20%20%20%20%20%20%20//%20时间轴唯一 %20%20%20%20%20%20%20%20this.key%20=%20uuid() %20%20%20%20%20%20%20%20this.name%20=%20file.name %20%20%20%20%20%20%20%20this.type%20=%20'' %20%20%20%20%20%20%20%20this.duration%20=%20file.duration %20%20%20%20%20%20%20%20this.left%20=%200 %20%20%20%20%20%20%20%20this.width%20=%20file.duration%20*%20this.leftTime %20%20%20%20%20%20%20%20this.color%20=%20randColor() %20%20%20%20%20%20%20%20//%20原始资源文件名 %20%20%20%20%20%20%20%20this.fileKey%20=%20file.key %20%20%20%20%20%20%20%20this.font%20=%20'' %20%20%20%20} %20%20%20%20setMedia()%20{ %20%20%20%20%20%20%20%20this.type%20=%20'media' %20%20%20%20} %20%20%20%20setText()%20{ %20%20%20%20%20%20%20%20this.type%20=%20'text' %20%20%20%20} %20%20%20%20setFont(path)%20{ %20%20%20%20%20%20%20%20this.font%20=%20path %20%20%20%20} %20%20%20%20getFont()%20{ %20%20%20%20%20%20%20%20return%20this.font %20%20%20%20} %20%20%20%20getLeftSecond()%20{ %20%20%20%20%20%20%20%20return%20parseInt((this.left/this.leftTime)) %20%20%20%20} %20%20%20%20getFile()%20{ %20%20%20%20%20%20%20%20return%20'/'+this.fileKey %20%20%20%20} }- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
主要的代码
%20ffmpeg.js
%20 import%20{%20clearEmpty%20}%20from%20'@/utils/string.js' import%20{%20createFFmpeg%20,%20fetchFile%20}%20from%20'@ffmpeg/ffmpeg' import%20dayjs%20from%20'dayjs' /** %20*%20====================================== %20*%20说明:需要用到的ffmpeg操作封装一下 %20*%20作者:%20YYDS %20*%20文件:%20ffmpeg.js %20*%20日期:%202023/3/29%2011:08 %20*%20====================================== %20*/ export%20default%20class%20Ffmpeg%20{ %20%20%20%20static%20ffmpeg%20=%20'' %20%20%20%20//%20进度输出 %20%20%20%20static%20progress%20=%20{ %20%20%20%20%20%20%20%20/* %20%20%20%20%20%20%20%20%20*%20ratio%20is%20a%20float%20number%20between%200%20to%201. %20%20%20%20%20%20%20%20%20*/ %20%20%20%20%20%20%20%20ratio:0, %20%20%20%20%20%20%20%20time:0 %20%20%20%20} %20%20%20%20//%20日志输出 %20%20%20%20static%20message%20=%20[] %20%20%20%20//%20资源目录 %20%20%20%20static%20resourceDir%20=%20'resource' %20%20%20%20//%20缓存目录 %20%20%20%20static%20tmpDir%20=%20'mediaTmp' %20%20%20%20//%20渲染完的文件名 %20%20%20%20static%20renderFileName%20=%20'render.mp4' %20%20%20%20static%20async%20instance%20%20()%20{ %20%20%20%20%20%20%20%20this.ffmpeg%20=%20createFFmpeg(%20{ %20%20%20%20%20%20%20%20%20%20%20%20log:%20true %20%20%20%20%20%20%20%20}) %20%20%20%20%20%20%20%20await%20this.ffmpeg.load(); %20%20%20%20%20%20%20%20this.ffmpeg.FS('mkdir',this.resourceDir) %20%20%20%20%20%20%20%20this.ffmpeg.FS('mkdir',this.tmpDir) %20%20%20%20%20%20%20%20//%20设置日志 %20%20%20%20%20%20%20%20this.ffmpeg.setLogger(({%20type,%20message%20})%20=>%20{ %20%20%20%20%20%20%20%20%20%20%20%20//%20console.log('日志',type,%20message); %20%20%20%20%20%20%20%20%20%20%20%20/* %20%20%20%20%20%20%20%20%20%20%20%20%20*%20type%20can%20be%20one%20of%20following: %20%20%20%20%20%20%20%20%20%20%20%20%20* %20%20%20%20%20%20%20%20%20%20%20%20%20*%20info:%20internal%20workflow%20debug%20messages %20%20%20%20%20%20%20%20%20%20%20%20%20*%20fferr:%20ffmpeg%20native%20stderr%20output %20%20%20%20%20%20%20%20%20%20%20%20%20*%20ffout:%20ffmpeg%20native%20stdout%20output %20%20%20%20%20%20%20%20%20%20%20%20%20*/ %20%20%20%20%20%20%20%20%20%20%20%20if(type%20===%20'fferr')%20{ %20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20this.message.push(clearEmpty(message)) %20%20%20%20%20%20%20%20%20%20%20%20} %20%20%20%20%20%20%20%20}); %20%20%20%20%20%20%20%20//%20设置进度 %20%20%20%20%20%20%20%20this.ffmpeg.setProgress((progress)%20=>%20{ %20%20%20%20%20%20%20%20%20%20%20%20this.progress.ratio%20=%20progress.ratio%20*%20100 %20%20%20%20%20%20%20%20%20%20%20%20this.progress.time%20=%20progress.time %20%20%20%20%20%20%20%20%20%20%20%20console.log('进度',progress); %20%20%20%20%20%20%20%20%20%20%20%20console.log('进度',this.progress); %20%20%20%20%20%20%20%20%20%20%20%20this.updateProgress(this.progress) %20%20%20%20%20%20%20%20}) %20%20%20%20} %20%20%20%20static%20updateProgress(progress)%20{ %20%20%20%20%20%20%20%20console.log('进度更新了',progress) %20%20%20%20} %20%20%20%20static%20clearMessage()%20{ %20%20%20%20%20%20%20%20this.message%20=%20[] %20%20%20%20} %20%20%20%20static%20%20loadFile(file){ %20%20%20%20%20%20%20%20//%20console.log('加载的文件',file) %20%20%20%20%20%20%20%20return%20new%20Promise(async%20(resolve)%20=>%20{ %20%20%20%20%20%20%20%20%20%20%20%20const%20filePath%20=%20'/'%20+%20this.resourceDir%20+%20'/'%20+%20file.getFSName() %20%20%20%20%20%20%20%20%20%20%20%20const%20fileData%20=%20await%20fetchFile(file.getFile()) %20%20%20%20%20%20%20%20%20%20%20%20console.log('fileData',fileData) %20%20%20%20%20%20%20%20%20%20%20%20this.ffmpeg.FS(%20'writeFile'%20,%20filePath%20,%20fileData%20); %20%20%20%20%20%20%20%20%20%20%20%20if(file.mime){ %20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20let%20url%20=%20URL.createObjectURL(%20new%20Blob(%20[fileData.buffer]%20,%20{%20type:%20file.mime%20}%20)%20); %20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20file.setUrl(url) %20%20%20%20%20%20%20%20%20%20%20%20} %20%20%20%20%20%20%20%20%20%20%20%20if(file.isVideo())%20{ %20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20this.readCover(filePath).then(url%20=>%20{ %20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20file.setCover(url) %20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20//%20console.log('file',file) %20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20console.log('全部日志',this.message) %20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20file.setInfo(this.fileInfoFilter(this.message)) %20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20this.clearMessage() %20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20resolve() %20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20}) %20%20%20%20%20%20%20%20%20%20%20%20}else%20if(file.isAudio())%20{ %20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20this.readInfo(filePath).then(()%20=>%20{ %20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20console.log('全部日志',this.message) %20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20file.setInfo(this.fileInfoFilter(this.message)) %20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20this.clearMessage() %20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20resolve() %20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20}) %20%20%20%20%20%20%20%20%20%20%20%20}else{ %20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20resolve() %20%20%20%20%20%20%20%20%20%20%20%20} %20%20%20%20%20%20%20%20}) %20%20%20%20} %20%20%20%20static%20readFile(filePath)%20{ %20%20%20%20%20%20%20%20return%20new%20Promise(async%20(resolve)%20=>%20{ %20%20%20%20%20%20%20%20%20%20%20%20const%20data%20=%20this.ffmpeg.FS(%20'readFile'%20,%20filePath%20); %20%20%20%20%20%20%20%20%20%20%20%20let%20url%20=%20URL.createObjectURL(%20new%20Blob(%20[data.buffer]%20,%20{%20type:%20'video/mp4'%20}%20)%20); %20%20%20%20%20%20%20%20%20%20%20%20resolve(url) %20%20%20%20%20%20%20%20}) %20%20%20%20} %20%20%20%20static%20async%20readCover%20(path)%20%20{ %20%20%20%20%20%20%20%20return%20new%20Promise(async%20(resolve,%20reject)%20=>%20{ %20%20%20%20%20%20%20%20%20%20%20%20const%20fileName%20=%20dayjs().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) } }- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
- 67
- 68
- 69
- 70
- 71
- 72
- 73
- 74
- 75
- 76
- 77
- 78
- 79
- 80
- 81
- 82
- 83
- 84
- 85
- 86
- 87
- 88
- 89
- 90
- 91
- 92
- 93
- 94
- 95
- 96
- 97
- 98
- 99
- 100
- 101
- 102
- 103
- 104
- 105
- 106
- 107
- 108
- 109
- 110
- 111
- 112
- 113
- 114
- 115
- 116
- 117
- 118
- 119
- 120
- 121
- 122
- 123
- 124
- 125
- 126
- 127
- 128
- 129
- 130
- 131
- 132
- 133
- 134
- 135
- 136
- 137
- 138
- 139
- 140
- 141
- 142
- 143
- 144
- 145
- 146
- 147
- 148
- 149
- 150
- 151
- 152
- 153
- 154
- 155
- 156
- 157
- 158
- 159
- 160
- 161
- 162
- 163
- 164
- 165
- 166
- 167
- 168
- 169
- 170
- 171
- 172
- 173
- 174
- 175
- 176
- 177
- 178
- 179
- 180
- 181
- 182
- 183
- 184
- 185
- 186
- 187
- 188
- 189
- 190
- 191
- 192
- 193
- 194
- 195
- 196
- 197
- 198
- 199
- 200
- 201
- 202
- 203
- 204
- 205
- 206
- 207
- 208
- 209
- 210
- 211
- 212
- 213
- 214
- 215
- 216
- 217
- 218
- 219
- 220
- 221
- 222
- 223
- 224
- 225
- 226
- 227
- 228
- 229
- 230
- 231
- 232
- 233
- 234
- 235
- 236
- 237
- 238
- 239
- 240
- 241
- 242
- 243
- 244
- 245
- 246
- 247
- 248
- 249
- 250
- 251
- 252
- 253
- 254
- 255
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); }- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
index.vue
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
- 67
- 68
- 69
- 70
- 71
- 72
- 73
- 74
- 75
- 76
- 77
- 78
- 79
- 80
- 81
- 82
- 83
- 84
- 85
- 86
- 87
- 88
- 89
- 90
- 91
- 92
- 93
- 94
- 95
- 96
- 97
- 98
- 99
- 100
- 101
- 102
- 103
- 104
- 105
- 106
- 107
- 108
- 109
- 110
- 111
- 112
- 113
- 114
- 115
- 116
- 117
- 118
- 119
- 120
- 121
- 122
- 123
- 124
- 125
- 126
- 127
- 128
- 129
- 130
- 131
- 132
- 133
- 134
- 135
- 136
- 137
- 138
- 139
- 140
- 141
- 142
- 143
- 144
- 145
- 146
- 147
- 148
- 149
- 150
- 151
- 152
- 153
- 154
- 155
- 156
- 157
- 158
- 159
- 160
- 161
- 162
- 163
- 164
- 165
- 166
- 167
- 168
- 169
- 170
- 171
- 172
- 173
- 174
- 175
- 176
- 177
- 178
- 179
- 180
- 181
- 182
- 183
- 184
- 185
- 186
- 187
- 188
- 189
- 190
- 191
- 192
- 193
- 194
- 195
- 196
- 197
- 198
- 199
- 200
- 201
- 202
- 203
- 204
- 205
- 206
- 207
- 208
- 209
- 210
- 211
- 212
- 213
- 214
- 215
- 216
- 217
- 218
- 219
- 220
- 221
- 222
- 223
- 224
- 225
- 226
- 227
- 228
- 229
- 230
- 231
- 232
- 233
- 234
- 235
- 236
- 237
- 238
- 239
- 240
- 241
- 242
- 243
- 244
- 245
- 246
- 247
- 248
- 249
- 250
- 251
- 252
- 253
- 254
- 255
- 256
- 257
- 258
- 259
- 260
- 261
- 262
- 263
- 264
- 265
- 266
- 267
- 268
- 269
- 270
- 271
- 272
- 273
- 274
- 275
- 276
- 277
- 278
- 279
- 280
- 281
- 282
- 283
- 284
- 285
- 286
- 287
- 288
- 289
- 290
- 291
- 292
- 293
- 294
- 295
- 296
- 297
- 298
- 299
- 300
- 301
- 302
- 303
- 304
- 305
- 306
- 307
- 308
- 309
- 310
- 311
- 312
- 313
- 314
- 315
- 316
- 317
- 318
- 319
- 320
- 321
- 322
- 323
- 324
- 325
- 326
- 327
- 328
- 329
- 330
- 331
- 332
- 333
- 334
- 335
- 336
- 337
- 338
- 339
- 340
- 341
- 342
- 343
- 344
- 345
- 346
- 347
- 348
- 349
- 350
- 351
- 352
- 353
- 354
- 355
- 356
- 357
- 358
- 359
- 360
- 361
- 362
- 363
- 364
- 365
- 366
- 367
- 368
- 369
- 370
- 371
- 372
- 373
- 374
- 375
- 376
- 377
- 378
- 379
- 380
- 381
- 382
- 383
- 384
- 385
- 386
- 387
- 388
- 389
- 390
- 391
- 392
- 393
- 394
- 395
- 396
- 397
- 398
- 399
- 400
- 401
- 402
- 403
- 404
- 405
- 406
- 407
- 408
- 409
- 410
- 411
- 412
- 413
- 414
- 415
- 416
- 417
- 418
- 419
- 420
- 421
- 422
- 423
- 424
- 425
- 426
- 427
- 428
- 429
- 430
- 431
- 432
- 433
- 434
- 435
- 436
- 437
- 438
- 439
- 440
- 441
- 442
- 443
- 444
- 445
- 446
- 447
- 448
- 449
- 450
- 451
- 452
- 453
- 454
- 455
- 456
- 457
- 458
- 459
- 460
- 461
- 462
- 463
- 464
- 465
- 466
- 467
- 468
- 469
- 470
- 471
- 472
- 473
- 474
- 475
- 476
- 477
- 478
- 479
- 480
- 481
- 482
- 483
- 484
- 485
- 486
- 487
- 488
- 489
- 490
- 491
- 492
- 493
- 494
- 495
- 496
- 497
- 498
- 499
- 500
- 501
- 502
- 503
- 504
- 505
- 506
- 507
- 508
- 509
- 510
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 }- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
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 } }- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
key.js
/** * 生成UUID * @return {string} */ export function uuid() { return +new Date() + Math.random()*10+ Math.random()*10+ Math.random()*10+ Math.random()*10 + 'a' }- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
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})` }- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
声明
1.本站遵循行业规范,任何转载的稿件都会明确标注作者和来源;2.本站的原创文章,请转载时务必注明文章作者和来源,不尊重原创的行为我们将追究责任;3.作者投稿可能会经我们编辑修改或补充。
在线投稿:投稿 站长QQ:1888636
后台-插件-广告管理-内容页尾部广告(手机) |