mirror of
				https://github.com/pure-admin/vue-pure-admin.git
				synced 2025-11-03 13:44:47 +08:00 
			
		
		
		
	feat: 添加视频帧截取-WebAssembly版,支持MP4、MOV、AVI、WebM、MKV等主流格式
				
					
				
			This commit is contained in:
		
							parent
							
								
									611b591da8
								
							
						
					
					
						commit
						fe5ed68ff7
					
				
							
								
								
									
										2
									
								
								.gitattributes
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										2
									
								
								.gitattributes
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@ -0,0 +1,2 @@
 | 
				
			|||||||
 | 
					public/wasm/capture.worker.js linguist-language=Vue
 | 
				
			||||||
 | 
					public/wasm/index.js linguist-language=Vue
 | 
				
			||||||
@ -68,6 +68,7 @@ menus:
 | 
				
			|||||||
  hsguide: Guide
 | 
					  hsguide: Guide
 | 
				
			||||||
  hsAble: Able
 | 
					  hsAble: Able
 | 
				
			||||||
  hsMenuTree: Menu Tree
 | 
					  hsMenuTree: Menu Tree
 | 
				
			||||||
 | 
					  hsVideoFrame: Video Frame Capture
 | 
				
			||||||
  hsWavesurfer: Audio Visualization
 | 
					  hsWavesurfer: Audio Visualization
 | 
				
			||||||
  hsOptimize: Debounce、Throttle、Copy、Longpress Directives
 | 
					  hsOptimize: Debounce、Throttle、Copy、Longpress Directives
 | 
				
			||||||
  hsWatermark: Water Mark
 | 
					  hsWatermark: Water Mark
 | 
				
			||||||
 | 
				
			|||||||
@ -68,6 +68,7 @@ menus:
 | 
				
			|||||||
  hsguide: 引导页
 | 
					  hsguide: 引导页
 | 
				
			||||||
  hsAble: 功能
 | 
					  hsAble: 功能
 | 
				
			||||||
  hsMenuTree: 菜单树结构
 | 
					  hsMenuTree: 菜单树结构
 | 
				
			||||||
 | 
					  hsVideoFrame: 视频帧截取-wasm版
 | 
				
			||||||
  hsWavesurfer: 音频可视化
 | 
					  hsWavesurfer: 音频可视化
 | 
				
			||||||
  hsOptimize: 防抖、截流、复制、长按指令
 | 
					  hsOptimize: 防抖、截流、复制、长按指令
 | 
				
			||||||
  hsWatermark: 水印
 | 
					  hsWatermark: 水印
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										11349
									
								
								public/wasm/capture.worker.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11349
									
								
								public/wasm/capture.worker.js
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								public/wasm/capture.worker.wasm
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								public/wasm/capture.worker.wasm
									
									
									
									
									
										Executable file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										5477
									
								
								public/wasm/index.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5477
									
								
								public/wasm/index.js
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							@ -10,6 +10,15 @@ export default {
 | 
				
			|||||||
    rank: able
 | 
					    rank: able
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  children: [
 | 
					  children: [
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					      path: "/able/videoFrame",
 | 
				
			||||||
 | 
					      name: "VideoFrame",
 | 
				
			||||||
 | 
					      component: () => import("@/views/able/video-frame/index.vue"),
 | 
				
			||||||
 | 
					      meta: {
 | 
				
			||||||
 | 
					        title: $t("menus.hsVideoFrame"),
 | 
				
			||||||
 | 
					        extraIcon: "IF-pure-iconfont-new svg"
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
      path: "/able/wavesurfer",
 | 
					      path: "/able/wavesurfer",
 | 
				
			||||||
      name: "Wavesurfer",
 | 
					      name: "Wavesurfer",
 | 
				
			||||||
 | 
				
			|||||||
@ -8,6 +8,13 @@ type Events = {
 | 
				
			|||||||
  tagViewsShowModel: string;
 | 
					  tagViewsShowModel: string;
 | 
				
			||||||
  logoChange: boolean;
 | 
					  logoChange: boolean;
 | 
				
			||||||
  changLayoutRoute: string;
 | 
					  changLayoutRoute: string;
 | 
				
			||||||
 | 
					  imageInfo: {
 | 
				
			||||||
 | 
					    img: HTMLImageElement;
 | 
				
			||||||
 | 
					    height: number;
 | 
				
			||||||
 | 
					    width: number;
 | 
				
			||||||
 | 
					    x: number;
 | 
				
			||||||
 | 
					    y: number;
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const emitter: Emitter<Events> = mitt<Events>();
 | 
					export const emitter: Emitter<Events> = mitt<Events>();
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										185
									
								
								src/views/able/video-frame/canvasRenderer.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										185
									
								
								src/views/able/video-frame/canvasRenderer.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,185 @@
 | 
				
			|||||||
 | 
					// import { throttle } from "@pureadmin/utils";
 | 
				
			||||||
 | 
					import { emitter } from "@/utils/mitt";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export class CanvasRenderer {
 | 
				
			||||||
 | 
					  private canvas: HTMLCanvasElement;
 | 
				
			||||||
 | 
					  private ctx: CanvasRenderingContext2D;
 | 
				
			||||||
 | 
					  private images: {
 | 
				
			||||||
 | 
					    img: HTMLImageElement;
 | 
				
			||||||
 | 
					    x: number;
 | 
				
			||||||
 | 
					    y: number;
 | 
				
			||||||
 | 
					    width: number;
 | 
				
			||||||
 | 
					    height: number;
 | 
				
			||||||
 | 
					  }[];
 | 
				
			||||||
 | 
					  private container: HTMLElement;
 | 
				
			||||||
 | 
					  private positionX: number;
 | 
				
			||||||
 | 
					  private isDragging: boolean;
 | 
				
			||||||
 | 
					  private startX: number;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  constructor(containerId: string) {
 | 
				
			||||||
 | 
					    this.canvas = document.createElement("canvas");
 | 
				
			||||||
 | 
					    this.ctx = this.canvas.getContext("2d")!;
 | 
				
			||||||
 | 
					    this.images = [];
 | 
				
			||||||
 | 
					    this.positionX = 0;
 | 
				
			||||||
 | 
					    this.isDragging = false;
 | 
				
			||||||
 | 
					    this.startX = 0;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    this.container = document.getElementById(containerId);
 | 
				
			||||||
 | 
					    if (this.container) {
 | 
				
			||||||
 | 
					      this.container.appendChild(this.canvas);
 | 
				
			||||||
 | 
					      this.canvas.width = this.container.clientWidth;
 | 
				
			||||||
 | 
					      this.canvas.height = this.container.clientHeight;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  public addImage(
 | 
				
			||||||
 | 
					    url: string,
 | 
				
			||||||
 | 
					    x: number,
 | 
				
			||||||
 | 
					    y: number,
 | 
				
			||||||
 | 
					    width: number,
 | 
				
			||||||
 | 
					    height: number
 | 
				
			||||||
 | 
					  ) {
 | 
				
			||||||
 | 
					    const img = new Image();
 | 
				
			||||||
 | 
					    img.src = url;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    this.images.push({
 | 
				
			||||||
 | 
					      img,
 | 
				
			||||||
 | 
					      x,
 | 
				
			||||||
 | 
					      y,
 | 
				
			||||||
 | 
					      width,
 | 
				
			||||||
 | 
					      height
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    this.render();
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  public render() {
 | 
				
			||||||
 | 
					    this.clearRect();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    this.images.forEach(imgProps => {
 | 
				
			||||||
 | 
					      const x = imgProps.x + this.positionX;
 | 
				
			||||||
 | 
					      this.ctx.drawImage(
 | 
				
			||||||
 | 
					        imgProps.img,
 | 
				
			||||||
 | 
					        x,
 | 
				
			||||||
 | 
					        imgProps.y,
 | 
				
			||||||
 | 
					        imgProps.width,
 | 
				
			||||||
 | 
					        imgProps.height
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  public clearImages() {
 | 
				
			||||||
 | 
					    this.images = [];
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  public clearRect() {
 | 
				
			||||||
 | 
					    this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  public drawTick(event) {
 | 
				
			||||||
 | 
					    this.render();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // 当前勾选图片的索引
 | 
				
			||||||
 | 
					    const index =
 | 
				
			||||||
 | 
					      Math.ceil(
 | 
				
			||||||
 | 
					        (Math.abs(this.positionX) + event.offsetX) / this.images[0].width
 | 
				
			||||||
 | 
					      ) - 1;
 | 
				
			||||||
 | 
					    const x = event.offsetX;
 | 
				
			||||||
 | 
					    const y = event.offsetY;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // 绘制样式
 | 
				
			||||||
 | 
					    this.ctx.strokeStyle = "red";
 | 
				
			||||||
 | 
					    this.ctx.lineWidth = 4;
 | 
				
			||||||
 | 
					    this.ctx.lineCap = "round";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // 绘制对勾
 | 
				
			||||||
 | 
					    this.ctx.beginPath();
 | 
				
			||||||
 | 
					    this.ctx.moveTo(x - 10, y);
 | 
				
			||||||
 | 
					    this.ctx.lineTo(x, y + 10);
 | 
				
			||||||
 | 
					    this.ctx.lineTo(x + 15, y - 10);
 | 
				
			||||||
 | 
					    this.ctx.stroke();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    emitter.emit("imageInfo", this.images[index]);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  public addListener() {
 | 
				
			||||||
 | 
					    if (!this.canvas) return;
 | 
				
			||||||
 | 
					    this.canvas.addEventListener("click", this.handleClick);
 | 
				
			||||||
 | 
					    this.canvas.addEventListener("mousedown", this.handleMouseDown);
 | 
				
			||||||
 | 
					    this.canvas.addEventListener("mousemove", this.handleMouseMove);
 | 
				
			||||||
 | 
					    this.canvas.addEventListener("mouseup", this.handleMouseUp);
 | 
				
			||||||
 | 
					    this.canvas.addEventListener("touchstart", this.handleTouchStart);
 | 
				
			||||||
 | 
					    this.canvas.addEventListener("touchmove", this.handleTouchMove);
 | 
				
			||||||
 | 
					    this.canvas.addEventListener("touchend", this.handleTouchEnd);
 | 
				
			||||||
 | 
					    // window.addEventListener("resize", throttle(this.handleWindowResize, 200));
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  private handleClick = (event: MouseEvent) => {
 | 
				
			||||||
 | 
					    this.drawTick(event);
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  private handleMouseDown = (event: MouseEvent) => {
 | 
				
			||||||
 | 
					    this.startDrag(event.clientX);
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  private handleMouseMove = (event: MouseEvent) => {
 | 
				
			||||||
 | 
					    this.drag(event.clientX);
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  private handleMouseUp = () => {
 | 
				
			||||||
 | 
					    this.endDrag();
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  private handleTouchStart = (event: TouchEvent) => {
 | 
				
			||||||
 | 
					    if (event.touches.length === 1) {
 | 
				
			||||||
 | 
					      event.preventDefault();
 | 
				
			||||||
 | 
					      this.startDrag(event.touches[0].clientX);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  private handleTouchMove = (event: TouchEvent) => {
 | 
				
			||||||
 | 
					    if (event.touches.length === 1) {
 | 
				
			||||||
 | 
					      event.preventDefault();
 | 
				
			||||||
 | 
					      this.drag(event.touches[0].clientX);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  private handleTouchEnd = () => {
 | 
				
			||||||
 | 
					    this.endDrag();
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  private startDrag(clientX: number) {
 | 
				
			||||||
 | 
					    this.canvas.style.cursor = "grabbing";
 | 
				
			||||||
 | 
					    this.canvas.style.userSelect = "none";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    this.startX = clientX;
 | 
				
			||||||
 | 
					    this.isDragging = true;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  private drag(clientX: number) {
 | 
				
			||||||
 | 
					    if (!this.isDragging) return;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const deltaX = clientX - this.startX;
 | 
				
			||||||
 | 
					    const maxPositionX =
 | 
				
			||||||
 | 
					      this.images.length * this.images[0].width - this.container.clientWidth;
 | 
				
			||||||
 | 
					    this.positionX = Math.max(
 | 
				
			||||||
 | 
					      Math.min(this.positionX + deltaX, 0),
 | 
				
			||||||
 | 
					      -maxPositionX
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					    this.startX = clientX;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    this.render();
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  private endDrag() {
 | 
				
			||||||
 | 
					    this.canvas.style.cursor = "grab";
 | 
				
			||||||
 | 
					    this.canvas.style.userSelect = "auto";
 | 
				
			||||||
 | 
					    this.isDragging = false;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // private handleWindowResize = () => {
 | 
				
			||||||
 | 
					  //   this.canvas.width = this.container.clientWidth;
 | 
				
			||||||
 | 
					  //   this.canvas.height = this.container.clientHeight;
 | 
				
			||||||
 | 
					  //   this.render();
 | 
				
			||||||
 | 
					  // };
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										172
									
								
								src/views/able/video-frame/index.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										172
									
								
								src/views/able/video-frame/index.vue
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,172 @@
 | 
				
			|||||||
 | 
					<script setup lang="ts">
 | 
				
			||||||
 | 
					import { emitter } from "@/utils/mitt";
 | 
				
			||||||
 | 
					import { useLoader } from "@pureadmin/utils";
 | 
				
			||||||
 | 
					import { CanvasRenderer } from "./canvasRenderer";
 | 
				
			||||||
 | 
					import { ref, onMounted, onBeforeUnmount } from "vue";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					defineOptions({
 | 
				
			||||||
 | 
					  name: "VideoFrame"
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const num = 200;
 | 
				
			||||||
 | 
					const curImg = ref("");
 | 
				
			||||||
 | 
					const renderer = ref();
 | 
				
			||||||
 | 
					const captureUtil = ref();
 | 
				
			||||||
 | 
					const loading = ref(false);
 | 
				
			||||||
 | 
					const { loadScript } = useLoader();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const { VITE_PUBLIC_PATH } = import.meta.env;
 | 
				
			||||||
 | 
					const getPath = path => `${VITE_PUBLIC_PATH}wasm/${path}`;
 | 
				
			||||||
 | 
					const src = getPath("index.js");
 | 
				
			||||||
 | 
					const workerPath = getPath("capture.worker.js");
 | 
				
			||||||
 | 
					const wasmPath = getPath("capture.worker.wasm");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					loadScript({
 | 
				
			||||||
 | 
					  src
 | 
				
			||||||
 | 
					}).then(mgs => {
 | 
				
			||||||
 | 
					  if (mgs === "success") {
 | 
				
			||||||
 | 
					    // @ts-expect-error
 | 
				
			||||||
 | 
					    captureUtil.value = cheetahCapture.initCapture({
 | 
				
			||||||
 | 
					      workerPath,
 | 
				
			||||||
 | 
					      wasmPath
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					onMounted(() => {
 | 
				
			||||||
 | 
					  renderer.value = new CanvasRenderer("canvas-container");
 | 
				
			||||||
 | 
					  emitter.on("imageInfo", info => (curImg.value = info.img.src));
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function beforeUpload(file) {
 | 
				
			||||||
 | 
					  curImg.value = "";
 | 
				
			||||||
 | 
					  loading.value = true;
 | 
				
			||||||
 | 
					  renderer.value.clearImages();
 | 
				
			||||||
 | 
					  // api参考 https://github.com/wanwu/cheetah-capture#api
 | 
				
			||||||
 | 
					  captureUtil.value.then(res => {
 | 
				
			||||||
 | 
					    res.capture({
 | 
				
			||||||
 | 
					      // 视频文件
 | 
				
			||||||
 | 
					      file,
 | 
				
			||||||
 | 
					      // 抽取指定数目的帧图片,传递`number`是按照数目抽帧,传递数组是指定抽帧的时间,单位毫秒(抽帧策略:https://github.com/wanwu/cheetah-capture/issues/6#issuecomment-1634384486)
 | 
				
			||||||
 | 
					      info: 16,
 | 
				
			||||||
 | 
					      // 当抽帧结果变化的回调
 | 
				
			||||||
 | 
					      onChange: (list, { url }) => {
 | 
				
			||||||
 | 
					        renderer.value.addImage(url, num * list.url.length, 0, num, num);
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      // 当抽帧结束并成功的回调
 | 
				
			||||||
 | 
					      onSuccess: () => {
 | 
				
			||||||
 | 
					        renderer.value.addListener();
 | 
				
			||||||
 | 
					        // 默认选中第一张
 | 
				
			||||||
 | 
					        renderer.value.drawTick({ offsetX: num / 2, offsetY: num / 2 });
 | 
				
			||||||
 | 
					        loading.value = false;
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      // 当抽帧过程出现错误的回调
 | 
				
			||||||
 | 
					      onError: () => {
 | 
				
			||||||
 | 
					        loading.value = false;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return false;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					onBeforeUnmount(() => {
 | 
				
			||||||
 | 
					  // 解绑`imageInfo`公共事件,防止多次触发
 | 
				
			||||||
 | 
					  emitter.off("imageInfo");
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<template>
 | 
				
			||||||
 | 
					  <el-card shadow="never">
 | 
				
			||||||
 | 
					    <template #header>
 | 
				
			||||||
 | 
					      <div class="card-header">
 | 
				
			||||||
 | 
					        <span class="font-medium">
 | 
				
			||||||
 | 
					          <p>
 | 
				
			||||||
 | 
					            基于自定义编译
 | 
				
			||||||
 | 
					            <el-link
 | 
				
			||||||
 | 
					              href="https://github.com/FFmpeg/FFmpeg"
 | 
				
			||||||
 | 
					              target="_blank"
 | 
				
			||||||
 | 
					              style="margin: 0 4px 5px; font-size: 16px"
 | 
				
			||||||
 | 
					            >
 | 
				
			||||||
 | 
					              FFmpeg
 | 
				
			||||||
 | 
					            </el-link>
 | 
				
			||||||
 | 
					            的截帧工具,支持MP4、MOV、AVI、WebM、MKV等主流格式,支持
 | 
				
			||||||
 | 
					            H.264(AVC)、H.265(HEVC)、MPEG-2、MPEG-4、VP8、VP9、WMV3编码格式
 | 
				
			||||||
 | 
					          </p>
 | 
				
			||||||
 | 
					          当然还可以支持更多视频格式,只要FFmpeg支持的,按理都能支持,您也可参考
 | 
				
			||||||
 | 
					          <el-link
 | 
				
			||||||
 | 
					            href="https://github.com/wanwu/cheetah-capture"
 | 
				
			||||||
 | 
					            target="_blank"
 | 
				
			||||||
 | 
					            style="margin: 0 4px 5px; font-size: 16px"
 | 
				
			||||||
 | 
					          >
 | 
				
			||||||
 | 
					            cheetah-capture
 | 
				
			||||||
 | 
					          </el-link>
 | 
				
			||||||
 | 
					          和
 | 
				
			||||||
 | 
					          <el-link
 | 
				
			||||||
 | 
					            href="https://github.com/jordiwang/web-capture"
 | 
				
			||||||
 | 
					            target="_blank"
 | 
				
			||||||
 | 
					            style="margin: 0 4px 5px; font-size: 16px"
 | 
				
			||||||
 | 
					          >
 | 
				
			||||||
 | 
					            web-capture
 | 
				
			||||||
 | 
					          </el-link>
 | 
				
			||||||
 | 
					          修改并编译wasm等文件(强烈推荐在Ubuntu系统进行编译)
 | 
				
			||||||
 | 
					          <p>
 | 
				
			||||||
 | 
					            mac系统推荐安装
 | 
				
			||||||
 | 
					            <el-link
 | 
				
			||||||
 | 
					              href="https://github.com/utmapp/UTM"
 | 
				
			||||||
 | 
					              target="_blank"
 | 
				
			||||||
 | 
					              style="margin: 0 4px 5px; font-size: 16px"
 | 
				
			||||||
 | 
					            >
 | 
				
			||||||
 | 
					              UTM
 | 
				
			||||||
 | 
					            </el-link>
 | 
				
			||||||
 | 
					            虚拟机,windows系统推荐安装VMware虚拟机
 | 
				
			||||||
 | 
					          </p>
 | 
				
			||||||
 | 
					          <p>
 | 
				
			||||||
 | 
					            当然这只是一个视频帧截取工具,如果您想要更多操作可以研究下
 | 
				
			||||||
 | 
					            <el-link
 | 
				
			||||||
 | 
					              href="https://ffmpegwasm.netlify.app/"
 | 
				
			||||||
 | 
					              target="_blank"
 | 
				
			||||||
 | 
					              style="margin: 0 4px 5px; font-size: 16px"
 | 
				
			||||||
 | 
					            >
 | 
				
			||||||
 | 
					              ffmpeg.wasm
 | 
				
			||||||
 | 
					            </el-link>
 | 
				
			||||||
 | 
					            ,它是基于 FFmpeg 的纯 WebAssembly / JavaScript
 | 
				
			||||||
 | 
					            工具,可以在浏览器内进行视频和音频录制、转换和流式传输等,不过通过一些实践,对于时长较长的视频性能还是不太行,不过用于时长较短的短视频还是可以上生产的
 | 
				
			||||||
 | 
					          </p>
 | 
				
			||||||
 | 
					        </span>
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					    </template>
 | 
				
			||||||
 | 
					    <div class="flex flex-wrap">
 | 
				
			||||||
 | 
					      <el-upload
 | 
				
			||||||
 | 
					        drag
 | 
				
			||||||
 | 
					        :show-file-list="false"
 | 
				
			||||||
 | 
					        accept=".mp4,.mov,.avi,.webm,.mkv"
 | 
				
			||||||
 | 
					        :before-upload="beforeUpload"
 | 
				
			||||||
 | 
					      >
 | 
				
			||||||
 | 
					        <div class="el-upload__text">
 | 
				
			||||||
 | 
					          可拖拽上传视频(默认截取16张帧图片,可在代码中自行修改)
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					      </el-upload>
 | 
				
			||||||
 | 
					      <el-image
 | 
				
			||||||
 | 
					        v-if="curImg"
 | 
				
			||||||
 | 
					        :src="curImg"
 | 
				
			||||||
 | 
					        :preview-src-list="Array.of(curImg)"
 | 
				
			||||||
 | 
					        class="w-[180px] h-[180px] ml-2 rounded-[6px]"
 | 
				
			||||||
 | 
					      />
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					    <div
 | 
				
			||||||
 | 
					      v-loading="loading"
 | 
				
			||||||
 | 
					      element-loading-text="温馨提示:可左右拖拽图片并单击选取所需的帧图片"
 | 
				
			||||||
 | 
					      id="canvas-container"
 | 
				
			||||||
 | 
					      class="w-full h-[200px] overflow-hidden mt-6"
 | 
				
			||||||
 | 
					    />
 | 
				
			||||||
 | 
					  </el-card>
 | 
				
			||||||
 | 
					</template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<style scoped lang="scss">
 | 
				
			||||||
 | 
					::v-deep(.el-upload-dragger) {
 | 
				
			||||||
 | 
					  display: flex;
 | 
				
			||||||
 | 
					  align-items: center;
 | 
				
			||||||
 | 
					  height: 180px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					</style>
 | 
				
			||||||
@ -43,6 +43,7 @@ function init() {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  // 当音频已解码并可以播放时触发
 | 
					  // 当音频已解码并可以播放时触发
 | 
				
			||||||
  wavesurfer.value.on("ready", () => {
 | 
					  wavesurfer.value.on("ready", () => {
 | 
				
			||||||
 | 
					    if (!wavesurfer.value) return;
 | 
				
			||||||
    const { duration } = wavesurfer.value;
 | 
					    const { duration } = wavesurfer.value;
 | 
				
			||||||
    const { m, s } = getTime(duration);
 | 
					    const { m, s } = getTime(duration);
 | 
				
			||||||
    totalTime.value = `${m}:${s}`;
 | 
					    totalTime.value = `${m}:${s}`;
 | 
				
			||||||
 | 
				
			|||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user