视频转换工具栏页面 #32

This commit is contained in:
Klein
2023-11-14 01:17:32 +08:00
parent b80fd7024a
commit 6bd5f996ad
11 changed files with 6926 additions and 76 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -10,6 +10,9 @@
"coverage": "vitest run --coverage"
},
"dependencies": {
"@ffmpeg/core": "^0.12.4",
"@ffmpeg/ffmpeg": "^0.12.7",
"@ffmpeg/util": "^0.12.1",
"@mdi/font": "7.2.96",
"@tiptap/pm": "^2.0.2",
"@tiptap/starter-kit": "^2.0.2",

File diff suppressed because one or more lines are too long

Binary file not shown.

View File

@@ -0,0 +1 @@
"use strict";var Module={};var ENVIRONMENT_IS_NODE=typeof process=="object"&&typeof process.versions=="object"&&typeof process.versions.node=="string";if(ENVIRONMENT_IS_NODE){var nodeWorkerThreads=require("worker_threads");var parentPort=nodeWorkerThreads.parentPort;parentPort.on("message",data=>onmessage({data:data}));var fs=require("fs");Object.assign(global,{self:global,require:require,Module:Module,location:{href:__filename},Worker:nodeWorkerThreads.Worker,importScripts:function(f){(0,eval)(fs.readFileSync(f,"utf8")+"//# sourceURL="+f)},postMessage:function(msg){parentPort.postMessage(msg)},performance:global.performance||{now:function(){return Date.now()}}})}var initializedJS=false;function threadPrintErr(){var text=Array.prototype.slice.call(arguments).join(" ");if(ENVIRONMENT_IS_NODE){fs.writeSync(2,text+"\n");return}console.error(text)}function threadAlert(){var text=Array.prototype.slice.call(arguments).join(" ");postMessage({cmd:"alert",text:text,threadId:Module["_pthread_self"]()})}var err=threadPrintErr;self.alert=threadAlert;Module["instantiateWasm"]=(info,receiveInstance)=>{var module=Module["wasmModule"];Module["wasmModule"]=null;var instance=new WebAssembly.Instance(module,info);return receiveInstance(instance)};self.onunhandledrejection=e=>{throw e.reason??e};function handleMessage(e){try{if(e.data.cmd==="load"){let messageQueue=[];self.onmessage=e=>messageQueue.push(e);self.startWorker=instance=>{Module=instance;postMessage({"cmd":"loaded"});for(let msg of messageQueue){handleMessage(msg)}self.onmessage=handleMessage};Module["wasmModule"]=e.data.wasmModule;for(const handler of e.data.handlers){Module[handler]=function(){postMessage({cmd:"callHandler",handler:handler,args:[...arguments]})}}Module["wasmMemory"]=e.data.wasmMemory;Module["buffer"]=Module["wasmMemory"].buffer;Module["ENVIRONMENT_IS_PTHREAD"]=true;if(typeof e.data.urlOrBlob=="string"){importScripts(e.data.urlOrBlob)}else{var objectUrl=URL.createObjectURL(e.data.urlOrBlob);importScripts(objectUrl);URL.revokeObjectURL(objectUrl)}createFFmpegCore(Module)}else if(e.data.cmd==="run"){Module["__emscripten_thread_init"](e.data.pthread_ptr,0,0,1);Module["__emscripten_thread_mailbox_await"](e.data.pthread_ptr);Module["establishStackSpace"]();Module["PThread"].receiveObjectTransfer(e.data);Module["PThread"].threadInitTLS();if(!initializedJS){initializedJS=true}try{Module["invokeEntryPoint"](e.data.start_routine,e.data.arg)}catch(ex){if(ex!="unwind"){throw ex}}}else if(e.data.cmd==="cancel"){if(Module["_pthread_self"]()){Module["__emscripten_thread_exit"](-1)}}else if(e.data.target==="setimmediate"){}else if(e.data.cmd==="checkMailbox"){if(initializedJS){Module["checkMailbox"]()}}else if(e.data.cmd){err("worker.js received unknown command "+e.data.cmd);err(e.data)}}catch(ex){if(Module["__emscripten_thread_crashed"]){Module["__emscripten_thread_crashed"]()}throw ex}}self.onmessage=handleMessage;

View File

@@ -1,5 +1,5 @@
import unsplashRoutes from "@/views/app/unsplash/UnsplashRoutes";
import toolRoutes from "@/views/app/tool/toolRoutes";
import emailRoutes from "@/views/app/email/emailRoutes";
import taskRoutes from "@/views/app/taskcenter/taskRoutes";
import hotGuardRoutes from "@/views/app/hot/hot_guard/hotGuardRoutes";
@@ -190,11 +190,12 @@ export default [
},
{
path: "/apps/tool",
name: "tools",
name: "app-tool",
component: () =>
import(
/* webpackChunkName: "utility-board" */ "@/views/app/tool/ToolApp.vue"
),
children: [...toolRoutes],
meta: {
requiresAuth: true,
title: "Tools",

View File

@@ -1,21 +1,17 @@
<script>
import ToolMenu from "./component/ToolMenu";
export default {
name: "ToolApp.vue"
name: "ToolApp.vue",
components: {ToolMenu}
}
</script>
<template>
<div class="app-container">
<!-- ---------------------------------------------- -->
<!-- Side Bar -->
<!-- ---------------------------------------------- -->
<div class="d-none d-md-block sidebar">
<ToolMenu></ToolMenu>
</div>
<!-- ---------------------------------------------- -->
<!-- List User-->
<!-- ---------------------------------------------- -->
<div class="main">
<router-view v-slot="{ Component }">
<transition name="fade">
@@ -23,8 +19,6 @@ export default {
</transition>
</router-view>
</div>
</div>
</template>

View File

@@ -0,0 +1,38 @@
<template>
<v-card class="pa-3" height="100%">
<!-- ---------------------------------------------- -->
<!-- Write Email -->
<!-- ---------------------------------------------- -->
<EmailCompose />
<v-list class="mt-2 pa-0" nav>
<v-list-item
active-class="text-primary"
link
prepend-icon="mdi-inbox"
title="VideoTool"
to="/apps/tool/video"
>
</v-list-item>
<v-list-item
active-class="text-primary"
link
prepend-icon="mdi-send-outline"
title="balabala"
to="/apps/email/send"
>
</v-list-item>
</v-list>
</v-card>
</template>
<script>
export default {
name: "ToolMenu"
}
</script>
<style scoped>
</style>

View File

@@ -0,0 +1,220 @@
<template>
<div ref="VideoTool" class="VideoTool">
<el-upload
:before-upload="beforeUpload"
:file-list="fileList"
:on-change="handleChange"
:on-remove="handleRemove"
action=""
class="upload"
drag
multiple>
<i class="el-icon-upload"></i>
<div class="el-upload__text">将文件拖到此处<em>点击上传</em></div>
</el-upload>
<div class="SelectContainer">
<div class="select">
<el-select v-model="CurrentType">
<el-option
v-for="item in options"
:key="item.value"
:label="item.label"
:value="item.value">
</el-option>
</el-select>
<span style="margin:0 20px 0 20px;display: inline-block;font-family: 'Hiragino Sans GB';font-weight: 700;line-height: 32px">
转成
</span>
<el-select v-model="TargetType">
<el-option
v-for="item in options2"
:key="item.value"
:label="item.label"
:value="item.value">
</el-option>
</el-select>
</div>
<div class="button" @click="transform">
<el-button style="width: 80px;position: relative;background-color: rgb(255,44,85);color: white">开始转换</el-button>
</div>
<p ref="messageRef"></p>
<video ref="video" controls>
</video>
</div>
</div>
</template>
<script>
import { FFmpeg } from '@ffmpeg/ffmpeg';
import {fetchFile, toBlobURL} from '@ffmpeg/util';
let ffmpegRef = new FFmpeg()
let ffmpeg = ffmpegRef;
export default {
name: "VideoTool",
data () {
return {
ffmpeg:[],
options:[
{
label: 'flv',
value: 'flv'
},
{
label: 'm3u8',
value: 'm3u8'
},
{
label: 'webm',
value: 'webm'
},
{
label: 'avi',
value: 'avi'
},
{
label: 'mp4',
value: 'mp4'
}
],
options2:[
{
label: 'flv',
value: 'flv'
},
{
label: 'm3u8',
value: 'm3u8'
},
{
label: 'webm',
value: 'webm'
},
{
label: 'avi',
value: 'avi'
},
{
label: 'mp4',
value: 'mp4'
}
],
CurrentType: '',
TargetType: '',
file: null,
fileList: [
]
}
},
methods: {
beforeUpload(file){
this.file = file
return false
},
handleChange(file, fileList) {
this.fileList = [{name:file.name}];
},
handleRemove(file){
this.fileList = []
this.file = null
},
async transform() {
if(!this.file){
this.$message.warning("请选择文件")
return
}
if(!this.CurrentType||!this.TargetType){
this.$message.warning("请选择格式")
return
}
if(!this.CurrentType === this.TargetType){
this.$message.warning("格式相同")
return
}
let file = this.file
let that = this
let reader = new FileReader()
reader.addEventListener(
"load",
function () {
(async () => {
await ffmpeg.writeFile(`input.${that.CurrentType}`,await fetchFile(reader.result));
await ffmpeg.exec(['-i', `input.${that.CurrentType}`,'-c','copy', `output.${that.TargetType}`]);
const data = await ffmpeg.readFile(`output.${that.TargetType}`);
let downloadFile =new Blob([data.buffer], {type: `video/${that.TargetType}`})
const downloadA = document.createElement("a");
downloadA.href = URL.createObjectURL(downloadFile);
downloadA.download = `output.${that.TargetType}`
document.body.appendChild(downloadA);
downloadA.click()
document.body.removeChild(downloadA);
that.$refs.video.setAttribute('src',URL.createObjectURL(new Blob([data.buffer], {type: `video/${that.TargetType}`})))
})()
},
false,
);
if (file) {
reader.readAsDataURL(file);
}
},
init(){
let that = this;
(async () => {
let messageRef = that.$refs.messageRef
ffmpeg.on('progress', ({ progress, time }) => {
messageRef.innerHTML = `${(progress * 100).toFixed(5)} % `;
});
// toBlobURL is used to bypass CORS issue, urls with the same
// domain can be used directly.
const baseURL = '/ffmpeg-core'
await ffmpeg.load({
coreURL: await toBlobURL(`${baseURL}/ffmpeg-core.js`, 'text/javascript'),
wasmURL: await toBlobURL(`${baseURL}/ffmpeg-core.wasm`, 'application/wasm'),
workerURL: await toBlobURL(`${baseURL}/ffmpeg-core.worker.js`, 'text/javascript'),
});
})()
}
},
mounted() {
this.init()
}
}
</script>
<style lang="scss" scoped>
.VideoTool{
position: relative;
height: 100%;
width: 100%;
.upload{
margin-top: 20px;
position: relative;
width: 80%;
left: 50%;
transform: translate(-50%);
}
.SelectContainer{
margin-top: 20px;
position: relative;
width: 100%;
height: 100px;
.select{
display: flex;
position: relative;
width: 500px;
left: 50%;
transform: translate(-50%);
}
.button{
margin-top: 10px;
position: relative;
display: flex;
width: 110px;
left: 50%;
transform: translate(-50%)
}
}
}
</style>

View File

@@ -0,0 +1,15 @@
export default [
{
path: "",
redirect: "/apps/tool/video",
},
{
path: "video",
name: "apps-tool-video",
component: () =>
import(
/* webpackChunkName: "apps-tool-video" */ "@/views/app/tool/pages/VideoTool.vue"
),
},
];

View File

@@ -35,6 +35,9 @@ export default defineConfig({
},
extensions: [".js", ".json", ".jsx", ".mjs", ".ts", ".tsx", ".vue"],
},
optimizeDeps: {
exclude: ['@ffmpeg/ffmpeg', '@ffmpeg/util']
},
server: {
port: 4399,
proxy: {