mirror of
https://github.com/Geniusay/ChopperBot.git
synced 2026-08-29 02:10:43 +08:00
在线剪辑demo
This commit is contained in:
Generated
-1
@@ -1,4 +1,3 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ASMPluginConfiguration">
|
||||
<asm skipDebug="false" skipFrames="false" skipCode="false" expandFrames="false" />
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
<svg xml:space="preserve" viewBox="0 0 100 100" y="0" x="0" xmlns="http://www.w3.org/2000/svg" version="1.1" style="margin: initial; display: block; shape-rendering: auto;" preserveAspectRatio="xMidYMid" width="200" height="200"><g class="ldl-scale" style="transform-origin: 50% 50%; transform: rotate(0deg) scale(1.2, 1.2);"><g class="ldl-ani"><g class="ldl-layer"><g class="ldl-ani" style="transform: matrix3d(0.91, 0, 0, 0, 0, 0.91, 0, 0, 0, 0, 0.91, 0, 0, 0, 0, 1); transform-box: view-box; opacity: 1; animation: 1s linear -0.75s infinite normal forwards running animate; transform-origin: 50px 50px;"><circle stroke="#333" stroke-width="8" fill="#fff" stroke-miterlimit="10" r="37" cy="50" cx="50" style="stroke-width: 8; fill: rgb(255, 255, 255); stroke: rgb(51, 51, 51);"></circle></g></g><g class="ldl-layer"><g class="ldl-ani" style="transform: matrix3d(0.91, 0, 0, 0, 0, 0.91, 0, 0, 0, 0, 0.91, 0, 0, 0, 0, 1); transform-box: view-box; opacity: 1; animation: 1s linear -1s infinite normal forwards running animate; transform-origin: 50px 50px;"><path fill="#333" d="M73.265 51.254l-35.594 20.55A1.447 1.447 0 0 1 35.5 70.55v-41.1a1.448 1.448 0 0 1 2.171-1.254l35.594 20.55c.965.558.965 1.95 0 2.508z" style="stroke-width: 1; fill: rgb(51, 51, 51);"></path></g></g>
|
||||
</g></g></svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
@@ -0,0 +1,2 @@
|
||||
<svg xml:space="preserve" viewBox="0 0 100 100" y="0" x="0" xmlns="http://www.w3.org/2000/svg" version="1.1" style="margin: initial; display: block; shape-rendering: auto;" preserveAspectRatio="xMidYMid" width="200" height="200"><g class="ldl-scale" style="transform-origin: 50% 50%; transform: rotate(0deg) scale(1.2, 1.2);"><g class="ldl-ani"><g class="ldl-layer"><g class="ldl-ani" style="transform: matrix3d(0.91, 0, 0, 0, 0, 0.91, 0, 0, 0, 0, 0.91, 0, 0, 0, 0, 1); transform-box: view-box; opacity: 1; animation: 1s linear -0.666667s infinite normal forwards running animate; transform-origin: 50px 50px;"><circle stroke="#333" stroke-width="8" fill="#fff" stroke-miterlimit="10" r="37" cy="50" cx="50" style="stroke-width: 8; fill: rgb(255, 255, 255); stroke: rgb(51, 51, 51);"></circle></g></g><g class="ldl-layer"><g class="ldl-ani" style="transform: matrix3d(0.91, 0, 0, 0, 0, 0.91, 0, 0, 0, 0, 0.91, 0, 0, 0, 0, 1); transform-box: view-box; opacity: 1; animation: 1s linear -0.833333s infinite normal forwards running animate; transform-origin: 50px 50px;"><path fill="#333" d="M45.39 70H34.841c-.666 0-1.205-.54-1.205-1.205v-37.59c0-.666.54-1.205 1.205-1.205H45.39c.666 0 1.205.54 1.205 1.205v37.59c0 .665-.54 1.205-1.205 1.205z" style="stroke-width: 1; fill: rgb(51, 51, 51);"></path></g></g><g class="ldl-layer"><g class="ldl-ani" style="transform: matrix3d(0.91, 0, 0, 0, 0, 0.91, 0, 0, 0, 0, 0.91, 0, 0, 0, 0, 1); transform-box: view-box; opacity: 1; animation: 1s linear -1s infinite normal forwards running animate; transform-origin: 50px 50px;"><path fill="#333" d="M65.159 70H54.61c-.666 0-1.205-.54-1.205-1.205v-37.59c0-.666.54-1.205 1.205-1.205h10.549c.666 0 1.205.54 1.205 1.205v37.59c0 .665-.54 1.205-1.205 1.205z" style="stroke-width: 1; fill: rgb(51, 51, 51);"></path></g></g>
|
||||
</g></g></svg>
|
||||
|
After Width: | Height: | Size: 1.8 KiB |
@@ -22,12 +22,12 @@ export default [
|
||||
},
|
||||
{
|
||||
path: "/ai/Online_Clip",
|
||||
component: () => import("@/views/chatgpt/OnlineClip.vue"),
|
||||
component: () => import("@/views/chatgpt/OnlineClip.vue"),
|
||||
meta: {
|
||||
requiresAuth: true,
|
||||
layout: "ui",
|
||||
category: "AI",
|
||||
title: "ChatBotV2",
|
||||
title: "OnlineClip",
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
@@ -60,3 +60,21 @@ export function getTimeAgo(input: string): string {
|
||||
return 'now';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 将number类型的时间转换成“00:00:00:00”格式
|
||||
*/
|
||||
function padZero(num: number, size = 2): string {
|
||||
let s = num.toString();
|
||||
while (s.length < size) s = '0' + s;
|
||||
return s;
|
||||
}
|
||||
export function formatDuration(durationInSeconds: number): string {
|
||||
const totalMilliseconds = Math.round(durationInSeconds * 1000); // 四舍五入到最近的毫秒
|
||||
const seconds = Math.floor((totalMilliseconds / 1000) % 60);
|
||||
const minutes = Math.floor((totalMilliseconds / (1000 * 60)) % 60);
|
||||
const hours = Math.floor((totalMilliseconds / (1000 * 60 * 60)) % 24);
|
||||
const milliseconds = ('00' + (totalMilliseconds % 1000)).slice(-2); // 保留毫秒的前两位
|
||||
|
||||
return `${padZero(hours)}:${padZero(minutes)}:${padZero(seconds)}:${milliseconds}`;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<!--
|
||||
* @Component:
|
||||
* @Component:
|
||||
* @Maintainer: J.K. Yang
|
||||
* @Description:
|
||||
* @Description:
|
||||
-->
|
||||
<script setup lang="ts">
|
||||
import { useSnackbarStore } from "@/stores/snackbarStore";
|
||||
|
||||
@@ -1,13 +1,30 @@
|
||||
<!--
|
||||
* @Component: 在线剪辑页面
|
||||
* @Component: 在线剪辑
|
||||
* @Maintainer: Lisianthus
|
||||
* @Description:
|
||||
-->
|
||||
<script setup lang="ts">
|
||||
import { useSnackbarStore } from "@/stores/snackbarStore";
|
||||
const snackbarStore = useSnackbarStore();
|
||||
import Player from "./component/Player.vue";
|
||||
import Timeline from "./component/Timeline.vue";
|
||||
import VideoChoose from "./component/VideoChoose.vue";
|
||||
import {useEditingStore} from "@/views/chatgpt/editingStore";
|
||||
const editingStore = useEditingStore()
|
||||
editingStore.initEditing()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div></div>
|
||||
<v-card class="editing">
|
||||
<!--视频选择组件-->
|
||||
<VideoChoose></VideoChoose>
|
||||
<!--播放组件-->
|
||||
<Player></Player>
|
||||
<!--时间轴选择组件-->
|
||||
<Timeline></Timeline>
|
||||
</v-card>
|
||||
</template>
|
||||
<style scoped>
|
||||
.editing{
|
||||
height:450px;
|
||||
width:700px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
<!--
|
||||
* @Component: 播放组件
|
||||
* @Maintainer: Lisianthus
|
||||
* @Description:控制视频播放状态
|
||||
-->
|
||||
<script setup lang="ts">
|
||||
import {useEditingStore} from "@/views/chatgpt/editingStore";
|
||||
import PlayIcon from '@/assets/icons/play.svg'
|
||||
import PauseIcon from '@/assets/icons/pause.svg'
|
||||
|
||||
const editingStore = useEditingStore();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="player flex">
|
||||
<div class="time-box">
|
||||
<label>{{ editingStore.formattedPlayStart }} </label>
|
||||
<label style="color:#c0c0c0"> / {{ editingStore.formattedVideoLong }}</label>
|
||||
</div>
|
||||
<button @click="editingStore.setPlayStatus" class="player-btn">
|
||||
<img :src="editingStore.play_status ? PlayIcon : PauseIcon" alt="Play Icon"/>
|
||||
</button>
|
||||
<div class="time-box">
|
||||
<label>{{ editingStore.formattedStartTime }}</label>
|
||||
<label> / {{ editingStore.formattedEndTime }}</label>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.flex{
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-evenly;
|
||||
}
|
||||
.player{
|
||||
position: relative;
|
||||
left:10px;
|
||||
margin-top:15px;
|
||||
width:680px;
|
||||
height:45px;
|
||||
box-shadow: #D7D7D7 0 0 5px;
|
||||
}
|
||||
.player-btn{
|
||||
width:35px;
|
||||
height:35px;
|
||||
border-radius: 50px;
|
||||
box-shadow: 0 0 5px #D7D7D7;
|
||||
overflow: hidden;
|
||||
}
|
||||
.time-box{
|
||||
width:200px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,156 @@
|
||||
<!--
|
||||
* @Component: 时间轴组件
|
||||
* @Maintainer: Lisianthus
|
||||
* @Description: 按时间轴剪辑视频
|
||||
-->
|
||||
<script setup lang="ts">
|
||||
import { useEditingStore } from "@/views/chatgpt/editingStore";
|
||||
|
||||
const editingStore = useEditingStore()
|
||||
let width = ref(400) // 视频分帧默认初始显示宽度
|
||||
// 比例因子:1秒 = {scaleFactor}px
|
||||
const scaleFactor = width.value / editingStore.getLong;
|
||||
|
||||
// 剪辑开始时间的像素位置
|
||||
let start_time = ref(scaleFactor * editingStore.start_time)
|
||||
// 剪辑结束时间的像素位置
|
||||
let end_time = ref(scaleFactor * editingStore.end_time)
|
||||
|
||||
// 绘制时间刻度轴
|
||||
function drawTimeline() {
|
||||
const canvas = document.getElementById('timelineCanvas') as HTMLCanvasElement
|
||||
if(!canvas)return;
|
||||
const ctx = canvas.getContext('2d');
|
||||
if(!ctx)return
|
||||
// 假设每个大格的高度是 20px,每个小格的高度是 5px
|
||||
const bigGridHeight = 20;
|
||||
const smallGridHeight = 5;
|
||||
|
||||
// 清除画布
|
||||
ctx.clearRect(0, 0, canvas.width, bigGridHeight);
|
||||
|
||||
// 绘制大格
|
||||
for (let i = 0; i * scaleFactor <= canvas.width; i++) {
|
||||
const x = i * scaleFactor * 60; // 默认一分钟一大格
|
||||
ctx.fillRect(x, 0, 1, bigGridHeight);
|
||||
ctx.fillText(i +":00",x+5,17)
|
||||
}
|
||||
|
||||
// 绘制小格(如果需要的话)
|
||||
for (let i = 0; i <= canvas.width; i++) {
|
||||
const x = i * scaleFactor * 6; // 默认6秒一小格
|
||||
ctx.fillRect(x, 0, 1, smallGridHeight);
|
||||
}
|
||||
}
|
||||
// 监听 editingStore.video_long 的变化
|
||||
watch(() => editingStore.getLong, (newVal) => {
|
||||
drawTimeline();
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
drawTimeline();
|
||||
})
|
||||
|
||||
|
||||
// 拖动句柄选择剪辑区域
|
||||
const dragging = ref<'start' | 'end' | null>(null);
|
||||
let startX = 0;
|
||||
let endX = 0;
|
||||
|
||||
const container = ref<HTMLDivElement | null>(null);
|
||||
// 拖动头句柄
|
||||
const startDragStart = (e: MouseEvent) => {
|
||||
if (!container.value) return;
|
||||
dragging.value = 'start';
|
||||
// e.clientX是鼠标相对于浏览器窗口的X坐标
|
||||
// container.value.offsetLeft是相对于其最近的已定位父元素的左边界的距离
|
||||
startX = e.clientX - container.value.getBoundingClientRect().left
|
||||
console.log(e.clientX,container.value.getBoundingClientRect().left)
|
||||
document.addEventListener('mousemove', drag);
|
||||
document.addEventListener('mouseup', stopDrag);
|
||||
};
|
||||
// 拖动尾句柄
|
||||
const startDragEnd = (e: MouseEvent) => {
|
||||
if (!container.value) return;
|
||||
dragging.value = 'end';
|
||||
endX = e.clientX - container.value.getBoundingClientRect().left;
|
||||
document.addEventListener('mousemove', drag);
|
||||
document.addEventListener('mouseup', stopDrag);
|
||||
};
|
||||
// 拖动事件
|
||||
const drag = (e: MouseEvent) => {
|
||||
if (!dragging.value || !container.value) return;
|
||||
if(e.clientX < container.value.getBoundingClientRect().left ||
|
||||
e.clientX - container.value.getBoundingClientRect().left > width.value) return;
|
||||
if (dragging.value === 'start') {
|
||||
start_time.value = e.clientX - container.value.getBoundingClientRect().left;
|
||||
} else {
|
||||
end_time.value = e.clientX - container.value.getBoundingClientRect().left;
|
||||
}
|
||||
start_time.value = Math.min(start_time.value,end_time.value)
|
||||
editingStore.setStartTime(start_time.value / scaleFactor)
|
||||
end_time.value = Math.max(start_time.value,end_time.value)
|
||||
editingStore.setEndTime(end_time.value / scaleFactor)
|
||||
};
|
||||
// 停止拖动事件
|
||||
const stopDrag = () => {
|
||||
dragging.value = null;
|
||||
// 清除拖动监听
|
||||
document.removeEventListener('mousemove', drag);
|
||||
document.removeEventListener('mouseup', stopDrag);
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<div class="timeline-container">
|
||||
<div class="timeline-bar" >
|
||||
<canvas id="timelineCanvas" width="680" height="20"></canvas>
|
||||
</div>
|
||||
<div ref="container" class="frame-bar" :style="{ width: `${width}px` }">
|
||||
<div class="framing"></div>
|
||||
<div class="cut-choose" :style="{ left: `${start_time}px`,width: `${end_time - start_time}px` }"></div>
|
||||
<div @mousedown="startDragStart" class="cut-hand" :style="{left: `${start_time}px`}"></div>
|
||||
<div @mousedown="startDragEnd" class="cut-hand" :style="{left: `${end_time}px`}"></div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<style scoped lang="scss">
|
||||
.timeline-container{
|
||||
background-color: whitesmoke;
|
||||
}
|
||||
.timeline-bar{
|
||||
position: relative;
|
||||
left:10px;
|
||||
margin-top:15px;
|
||||
width:680px;
|
||||
height:20px;
|
||||
}
|
||||
.frame-bar{
|
||||
position: relative;
|
||||
left:10px;
|
||||
margin-top:5px;
|
||||
height:60px;
|
||||
background-color: #cccccc;
|
||||
}
|
||||
.framing{
|
||||
position: absolute;
|
||||
height: 100%;
|
||||
width:100%;
|
||||
}
|
||||
.cut-choose{
|
||||
position: absolute;
|
||||
background-color: #3fc7f3;
|
||||
height:100%;
|
||||
}
|
||||
.cut-hand {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
user-select: none; /* 禁止选择文本 */
|
||||
width: 10px;
|
||||
height: 100%;
|
||||
background-color: white;
|
||||
cursor: ew-resize;
|
||||
border: solid 1px #e8e8e8;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,83 @@
|
||||
<!--
|
||||
* @Component: 视频选择
|
||||
* @Maintainer: Lisianthus
|
||||
* @Description:选择要编辑的视频
|
||||
-->
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
import { useEditingStore } from "@/views/chatgpt/editingStore";
|
||||
|
||||
const editingStore = useEditingStore()
|
||||
const videoRef = ref<HTMLVideoElement | null>(null);
|
||||
// 控制视频暂停和播放
|
||||
function playMedia(status: boolean) {
|
||||
if (videoRef.value) {
|
||||
if(status){
|
||||
videoRef.value.play().catch(error => {
|
||||
console.error('Error playing video:', error);
|
||||
});
|
||||
}else{
|
||||
videoRef.value.pause();
|
||||
}
|
||||
}
|
||||
}
|
||||
// 通过监听store播放状态来控制播放
|
||||
watch(() => editingStore.play_status, (newVal, oldVal) => {playMedia(newVal)}, { immediate: true })
|
||||
// 上传视频文件
|
||||
function onFileChange(event: Event) {
|
||||
const fileInput = event.target as HTMLInputElement;
|
||||
const file = fileInput.files?.[0];
|
||||
if (file) {
|
||||
console.log(URL.createObjectURL(file))
|
||||
editingStore.setVideo(URL.createObjectURL(file))
|
||||
}
|
||||
}
|
||||
// 获取视频时长
|
||||
function handleVideoMetadata() {
|
||||
if (videoRef.value) {
|
||||
editingStore.setDuration(videoRef.value.duration)
|
||||
}
|
||||
}
|
||||
// 更新播放位置
|
||||
function updateTime() {
|
||||
if (videoRef.value) {
|
||||
editingStore.play_start = videoRef.value.currentTime
|
||||
if(editingStore.play_start === editingStore.video_long){
|
||||
editingStore.play_status = false
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<div class="video-box">
|
||||
<input type="file" accept="video/*" @change="onFileChange" v-show="!editingStore.video_url"/>
|
||||
<video v-show="editingStore.getUrl"
|
||||
ref="videoRef"
|
||||
:src="editingStore.getUrl"
|
||||
@loadedmetadata="handleVideoMetadata"
|
||||
@timeupdate = "updateTime"
|
||||
></video>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.video-box{
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position:relative;
|
||||
left:125px;
|
||||
margin-top:15px;
|
||||
width:450px;
|
||||
height:250px;
|
||||
}
|
||||
input{
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: #F2F2F2;
|
||||
}
|
||||
video{
|
||||
height: 250px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,93 @@
|
||||
import { defineStore } from "pinia";
|
||||
import { Editing } from "./editingTypes"
|
||||
import {sessionStorage} from "@/utils/storage";
|
||||
import {formatDuration} from "@/utils/timeUtils"
|
||||
|
||||
export const useEditingStore = defineStore({
|
||||
id: "editing",
|
||||
state:()=>({
|
||||
video_url: '',
|
||||
video_long: 0, //视频时长
|
||||
start_time: 0, //剪辑起始时间
|
||||
end_time: 0, //剪辑结束时间
|
||||
timeline_accuracy: 1,// 时间轴比例
|
||||
play_status: false, // 播放状态
|
||||
play_start: 0 //播放开始时间
|
||||
}),
|
||||
getters:{
|
||||
getUrl:(state)=>{
|
||||
return state.video_url
|
||||
},
|
||||
getLong:(state)=>{
|
||||
return state.video_long
|
||||
},
|
||||
formattedVideoLong: (state) => {
|
||||
return formatDuration(state.video_long);
|
||||
},
|
||||
formattedStartTime: (state) => {
|
||||
return formatDuration(state.start_time);
|
||||
},
|
||||
formattedEndTime: (state) => {
|
||||
return formatDuration(state.end_time);
|
||||
},
|
||||
formattedPlayStart: (state) => {
|
||||
return formatDuration(state.play_start);
|
||||
},
|
||||
},
|
||||
actions:{
|
||||
initEditing(){
|
||||
// 从本地获取之前保存的编辑信息
|
||||
const editingMessage:Editing = sessionStorage.get('editingMessage') as Editing
|
||||
if (editingMessage) {
|
||||
console.log("刷新")
|
||||
this.video_url = editingMessage.video_url;
|
||||
this.video_long = editingMessage.video_long;
|
||||
this.start_time = editingMessage.start_time;
|
||||
this.end_time = editingMessage.end_time;
|
||||
}
|
||||
},
|
||||
updateMessage(){
|
||||
let message = <Editing>{}
|
||||
message.video_url= this.video_url;
|
||||
message.video_long = this.video_long
|
||||
message.start_time = this.start_time;
|
||||
message.end_time = this.end_time;
|
||||
console.log("更新",message.video_long,message.start_time,message.end_time)
|
||||
sessionStorage.set('editingMessage', message)
|
||||
console.log("存入")
|
||||
},
|
||||
// 改变播放状态
|
||||
setPlayStatus() {
|
||||
this.play_status = !this.play_status;
|
||||
},
|
||||
// 改变播放开始时间
|
||||
setPlayStart(start: number) {
|
||||
this.play_start = start;
|
||||
},
|
||||
// 添加视频
|
||||
setVideo(url:string) {
|
||||
this.video_url = url
|
||||
this.updateMessage()
|
||||
},
|
||||
setDuration(duration:number) {
|
||||
this.video_long = duration
|
||||
this.end_time = duration
|
||||
this.updateMessage()
|
||||
},
|
||||
// 改变剪辑开始位置
|
||||
setStartTime(start: number) {
|
||||
this.start_time = start;
|
||||
this.updateMessage()
|
||||
},
|
||||
// 改变剪辑结束位置
|
||||
setEndTime(end: number) {
|
||||
this.end_time = end;
|
||||
this.updateMessage()
|
||||
},
|
||||
// 改变时间轴精度
|
||||
setTimeline(timeline: number) {
|
||||
this.timeline_accuracy = timeline;
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
export interface Editing{
|
||||
video_url: string //视频地址
|
||||
video_long: number //视频时长
|
||||
// video_head: number //视频位置
|
||||
start_time: number //剪辑起始时间
|
||||
end_time: number //剪辑结束时间
|
||||
}
|
||||
Reference in New Issue
Block a user