使用 HTML +JavaScript 从零构建视频帧提取器
在视频编辑、内容分析和多媒体处理领域,常常需要从视频中提取关键帧。手动截取不仅效率低下,还容易遗漏重要画面。本文介绍的视频帧提取工具通过 HTML5 技术栈实现了一个完整的浏览器端解决方案,用户可以轻松选择视频文件并进行手动或自动帧捕获。
效果演示
核心功能
手动帧捕获
用户可以通过点击“捕获帧”按钮,在视频播放过程中随时抓取当前帧。捕获的画面会实时显示在预览区域,并生成可下载的 PNG 图像。
自动帧捕获
支持设定时间间隔(如每秒一张)自动捕获帧的功能,适用于批量提取视频中的关键画面。进度条实时反映当前处理进度,增强用户体验。
帧管理
- 缩略图展示:所有捕获的帧以网格形式展示,鼠标悬停时显示操作按钮。
- 下载与删除:每个帧都支持独立下载和删除,方便用户整理和导出所需内容。
- 预览切换:点击缩略图即可在主画布上查看高清版本。
空状态提示
当没有任何帧被捕获时,提供友好的空状态提示,提升交互体验。
页面结构
视频上传与播放区域
用户选择本地视频文件,上传后在 video 中播放。
<div class="file-input-wrapper"><button class="file-input-button" id="uploadButton"><svg class="icon" viewBox="0 0 24 24"><path d="M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"/></svg>选择视频文件</button><input type="file" id="videoUpload" class="file-input" accept="video/*">
</div>
<div class="video-container"><video id="videoElement" controls></video>
</div>
操作控制区域
【捕获帧】按钮用于手动截取当前视频画面;【自动捕获】按钮开启定时连续提取帧的功能;【停止】按钮用于终止自动捕获过程;下方的输入框用于设置自动捕获时每帧之间的时间间隔(单位为秒)。 整体提供了用户与视频帧提取功能交互的主要控件。
<div class="controls"><button id="captureBtn" class="btn btn-primary" disabled><svg class="icon" viewBox="0 0 24 24"><path d="M4 8h4V4h12v16H8v-4H4V8zm12 6v2h2v-2h-2zm-4-3v5h2v-5h-2zm-4-3v8h2V8h-2z"/></svg>捕获帧</button><button id="autoCaptureBtn" class="btn btn-primary" disabled><svg class="icon" viewBox="0 0 24 24"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8zm.5-13H11v6l5.2 3.2.8-1.3-4.5-2.7V7z"/></svg>自动捕获</button><button id="stopAutoCaptureBtn" class="btn btn-danger" disabled><svg class="icon" viewBox="0 0 24 24"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8zM8 8h8v8H8z"/></svg>停止</button><div class="input-group"><input type="number" id="frameInterval" class="number-input" value="1" min="0.1" step="0.1"><span class="input-label">秒/帧</span></div>
</div>
帧预览与导出区域
显示当前捕获帧的预览区域,用户可以查看具体画面;提供“下载当前帧”按钮,支持将当前预览帧保存为图片文件;展示已捕获帧的缩略图列表,方便浏览和管理。
<div class="panel"><h2 class="panel-title"><svg class="icon" viewBox="0 0 24 24"><path d="M19 3H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm0 16H5V5h14v14z"/><path d="M8.5 15H10V9H7v1.5h1.5zM13.5 12.75L15.25 15H17l-2.25-3L17 9h-1.75l-1.75 2.25V9H12v6h1.5z"/></svg>帧预览与导出</h2><div class="preview-container"><div class="canvas-wrapper"><canvas id="canvasElement"></canvas></div><button id="downloadBtn" class="btn btn-primary" disabled><svg class="icon" viewBox="0 0 24 24"><path d="M19 9h-4V3H9v6H5l7 7 7-7zM5 18v2h14v-2H5z"/></svg>下载当前帧</button></div><h3 class="panel-title" style="margin-top: 20px; font-size: 16px;"><svg class="icon" viewBox="0 0 24 24"><path d="M4 6H2v14c0 1.1.9 2 2 2h14v-2H4V6zm16-4H8c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2V4c0-1.1-.9-2-2-2zm-8 12.5v-9l6 4.5-6 4.5z"/></svg>已捕获的帧 (共<span id="frameCount">0</span>张)</h3><div class="thumbnails-container" id="thumbnails"><div class="empty-state" id="emptyState"><svg class="icon" viewBox="0 0 24 24"><path d="M10 20v-6h4v6h5v-8h3L12 3 2 12h3v8z"/></svg><p>尚未捕获任何帧</p></div></div>
</div>
核心功能实现
初始化与事件绑定
使用 FrameExtractor
类封装所有逻辑,构造函数中初始化 DOM 元素和状态变量:
this.elements = {videoUpload: document.getElementById('videoUpload'),uploadButton: document.getElementById('uploadButton'),videoElement: document.getElementById('videoElement'),canvasElement: document.getElementById('canvasElement'),captureBtn: document.getElementById('captureBtn'),autoCaptureBtn: document.getElementById('autoCaptureBtn'),stopAutoCaptureBtn: document.getElementById('stopAutoCaptureBtn'),downloadBtn: document.getElementById('downloadBtn'),frameInterval: document.getElementById('frameInterval'),thumbnailsContainer: document.getElementById('thumbnails'),emptyState: document.getElementById('emptyState'),frameCount: document.getElementById('frameCount'),progressBar: document.getElementById('progressBar'),progress: document.getElementById('progress')
};
this.state = {autoCaptureInterval: null,capturedFrames: [],isAutoCapturing: false,captureStartTime: 0
};
this.elements.videoUpload.addEventListener('change', (e) => this.handleVideoUpload(e));
this.elements.uploadButton.addEventListener('click', () => this.elements.videoUpload.click());// 按钮事件
this.elements.captureBtn.addEventListener('click', () => this.captureFrame());
this.elements.autoCaptureBtn.addEventListener('click', () => this.toggleAutoCapture());
this.elements.stopAutoCaptureBtn.addEventListener('click', () => this.stopAutoCapture());
this.elements.downloadBtn.addEventListener('click', () => this.downloadCurrentFrame());
视频上传与播放
用户选择视频后,通过 URL.createObjectURL
创建临时链接加载视频。
handleVideoUpload(e) {const file = e.target.files[0];if (file) {const videoURL = URL.createObjectURL(file);this.elements.videoElement.src = videoURL;// 启用按钮this.elements.captureBtn.disabled = false;this.elements.autoCaptureBtn.disabled = false;// 重置状态this.resetCaptureState();}
}
手动帧捕获
将当前视频帧绘制到 canvas 上,供用户查看和下载。
captureFrame() {if (this.elements.videoElement.readyState === 0) return;const ctx = this.elements.canvasElement.getContext('2d');// 设置canvas尺寸与视频帧相同this.elements.canvasElement.width = this.elements.videoElement.videoWidth;this.elements.canvasElement.height = this.elements.videoElement.videoHeight;// 绘制视频帧到canvasctx.drawImage(this.elements.videoElement, 0, 0,this.elements.canvasElement.width, this.elements.canvasElement.height);// 启用下载按钮this.elements.downloadBtn.disabled = false;// 创建缩略图this.createThumbnail(this.elements.canvasElement.toDataURL('image/jpeg', 0.8));// 更新进度条(自动捕获时)if (this.state.isAutoCapturing) {const currentTime = this.elements.videoElement.currentTime;const duration = this.elements.videoElement.duration;const progress = (currentTime / duration) * 100;this.elements.progress.style.width = `${progress}%`;}
}
自动帧捕获
根据用户设置的时间间隔启动定时任务:
startAutoCapture() {const interval = parseFloat(this.elements.frameInterval.value) * 1000;if (interval > 0) {this.state.isAutoCapturing = true;this.state.captureStartTime = this.elements.videoElement.currentTime;this.elements.stopAutoCaptureBtn.disabled = false;this.elements.autoCaptureBtn.textContent = '暂停捕获';this.elements.autoCaptureBtn.classList.add('btn-danger');// 显示进度条this.elements.progressBar.style.display = 'block';this.elements.progress.style.width = '0%';// 先捕获一帧this.captureFrame();// 设置定时器this.state.autoCaptureInterval = setInterval(() => {this.captureFrame();// 检查是否到达视频末尾if (this.elements.videoElement.currentTime >= this.elements.videoElement.duration - 0.1) {this.stopAutoCapture();}}, interval);}
}
缩略图与交互
每次捕获的帧都会生成缩略图,并添加下载和删除功能:
createThumbnail(dataURL) {const thumbnailDiv = document.createElement('div');thumbnailDiv.className = 'thumbnail';// ...downloadBtn.addEventListener('click', (e) => {e.stopPropagation();const link = document.createElement('a');link.download = `frame_${frameId}.png`;link.href = dataURL;link.click();});// ...this.elements.thumbnailsContainer.appendChild(thumbnailDiv);
}
技术亮点
- Canvas 操作:利用 HTML5 Canvas 实现图像捕获与动态渲染。
- 对象 URL:通过
URL.createObjectURL
高效加载本地视频资源。 - 响应式设计:使用 CSS Grid 和 Flexbox 构建灵活布局,适配不同屏幕尺寸。
- 模块化结构:将功能封装在类中,提高代码组织清晰度和可维护性。
- 性能优化:自动捕获时限制帧率,避免过度消耗资源。
扩展建议
-
支持多视频格式:目前仅支持
video/*
,未来可扩展支持更多格式如.mkv
、.avi
,并通过 FFmpeg WASM 解码。 -
添加帧过滤功能:允许用户对已捕获帧进行筛选,例如按时间范围、相似度去重等。
-
导出为 ZIP 文件:集成 JSZip 库,一键打包所有帧为 ZIP 文件,便于批量下载。
-
云端存储:集成云存储 API(如 Firebase 或阿里云 OSS),实现帧图片的持久化保存与分享。
-
AI 关键帧识别:引入机器学习模型(如 TensorFlow.js),自动识别视频中的关键帧进行智能提取。
完整代码
<!DOCTYPE html>
<html lang="zh-CN">
<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>视频帧提取工具</title><style>:root {--primary-color: #4361ee;--secondary-color: #3f37c9;--accent-color: #4895ef;--danger-color: #f72585;--light-color: #f8f9fa;--dark-color: #212529;--border-radius: 8px;--box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);--transition: all 0.3s ease;}* {box-sizing: border-box;margin: 0;padding: 0;}body {font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;line-height: 1.6;color: var(--dark-color);background-color: #f5f7fa;padding: 20px;}.app-container {max-width: 1200px;margin: 0 auto;background-color: white;border-radius: var(--border-radius);box-shadow: var(--box-shadow);overflow: hidden;}.app-header {background-color: var(--primary-color);color: white;padding: 20px;text-align: center;}.app-header h1 {margin-bottom: 10px;font-weight: 600;}.app-header p {opacity: 0.9;}.main-content {display: grid;grid-template-columns: 1fr 1fr;gap: 20px;padding: 20px;}@media (max-width: 768px) {.main-content {grid-template-columns: 1fr;}}.panel {background-color: white;border-radius: var(--border-radius);box-shadow: var(--box-shadow);padding: 20px;display: flex;flex-direction: column;}.panel-title {font-size: 18px;font-weight: 600;margin-bottom: 15px;color: var(--secondary-color);display: flex;align-items: center;gap: 10px;padding-bottom: 10px;border-bottom: 1px solid #eee;}.panel-title svg {width: 20px;height: 20px;}.video-container {position: relative;width: 100%;background-color: black;border-radius: var(--border-radius);overflow: hidden;margin-bottom: 15px;aspect-ratio: 16/9;}video {width: 100%;height: 100%;object-fit: contain;display: block;}.file-input-wrapper {margin-bottom: 15px;}.file-input-button {width: 100%;padding: 12px;background-color: var(--primary-color);color: white;border: none;border-radius: var(--border-radius);cursor: pointer;transition: var(--transition);display: flex;align-items: center;justify-content: center;gap: 8px;font-weight: 500;}.file-input-button:hover {background-color: var(--secondary-color);}.file-input {display: none;}.controls {display: grid;grid-template-columns: 1fr 1fr;gap: 10px;margin-top: 10px;}.btn {padding: 10px;border: none;border-radius: var(--border-radius);cursor: pointer;transition: var(--transition);font-weight: 500;display: flex;align-items: center;justify-content: center;gap: 6px;}.btn-primary {background-color: var(--primary-color);color: white;}.btn-primary:hover {background-color: var(--secondary-color);}.btn-danger {background-color: var(--danger-color);color: white;}.btn-danger:hover {opacity: 0.9;}.btn:disabled {background-color: #ddd;color: #999;cursor: not-allowed;}.input-group {display: flex;align-items: center;gap: 8px;margin-top: 10px;}.input-label {font-size: 14px;color: #555;white-space: nowrap;}.number-input {padding: 8px 12px;border: 1px solid #ddd;border-radius: var(--border-radius);width: 100%;text-align: center;}.preview-container {flex-grow: 1;display: flex;flex-direction: column;min-height: 0;}.canvas-wrapper {flex-grow: 1;display: flex;align-items: center;justify-content: center;background-color: #f0f0f0;border-radius: var(--border-radius);overflow: hidden;margin-bottom: 15px;position: relative;}canvas {max-width: 100%;max-height: 100%;display: block;background-color: white;}.thumbnails-container {display: grid;grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));gap: 10px;margin-top: 15px;max-height: 300px;overflow-y: auto;padding: 5px;}.thumbnail {position: relative;border-radius: var(--border-radius);overflow: hidden;box-shadow: var(--box-shadow);transition: var(--transition);aspect-ratio: 16/9;}.thumbnail:hover {transform: translateY(-3px);box-shadow: 0 6px 12px rgba(0, 0, 0, 0.15);}.thumbnail img {width: 100%;height: 100%;object-fit: cover;display: block;}.thumbnail-actions {position: absolute;top: 5px;right: 5px;display: flex;gap: 5px;opacity: 0;transition: var(--transition);}.thumbnail:hover .thumbnail-actions {opacity: 1;}.thumbnail-btn {width: 28px;height: 28px;border-radius: 50%;border: none;background-color: rgba(0, 0, 0, 0.7);color: white;display: flex;align-items: center;justify-content: center;cursor: pointer;transition: var(--transition);}.thumbnail-btn:hover {background-color: rgba(0, 0, 0, 0.9);transform: scale(1.1);}.thumbnail-btn.download {background-color: rgba(67, 97, 238, 0.7);}.thumbnail-btn.download:hover {background-color: rgba(67, 97, 238, 0.9);}.thumbnail-btn.delete {background-color: rgba(247, 37, 133, 0.7);}.thumbnail-btn.delete:hover {background-color: rgba(247, 37, 133, 0.9);}.empty-state {text-align: center;padding: 20px;color: #999;grid-column: 1 / -1;}.empty-state svg {width: 50px;height: 50px;margin-bottom: 10px;opacity: 0.5;}.progress-bar {width: 100%;height: 6px;background-color: #eee;border-radius: 3px;margin-top: 10px;overflow: hidden;display: none;}.progress {height: 100%;background-color: var(--primary-color);width: 0%;transition: width 0.3s ease;}::-webkit-scrollbar {width: 8px;height: 8px;}::-webkit-scrollbar-track {background: #f1f1f1;border-radius: 4px;}::-webkit-scrollbar-thumb {background: #ccc;border-radius: 4px;}::-webkit-scrollbar-thumb:hover {background: #aaa;}.icon {width: 18px;height: 18px;vertical-align: middle;fill: currentColor;}</style>
</head>
<body>
<div class="app-container"><header class="app-header"><h1>视频帧提取工具</h1><p>轻松从视频中提取关键帧并保存为图片</p></header><div class="main-content"><div class="panel"><h2 class="panel-title"><svg class="icon" viewBox="0 0 24 24"><path d="M18 3v2h-2V3H8v2H6V3H4v18h2v-2h2v2h8v-2h2v2h2V3h-2zM8 17H6v-2h2v2zm0-4H6v-2h2v2zm0-4H6V7h2v2zm6 10h-4V5h4v14zm4-2h-2v-2h2v2zm0-4h-2v-2h2v2zm0-4h-2V7h2v2z"/></svg>视频控制</h2><div class="file-input-wrapper"><button class="file-input-button" id="uploadButton"><svg class="icon" viewBox="0 0 24 24"><path d="M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"/></svg>选择视频文件</button><input type="file" id="videoUpload" class="file-input" accept="video/*"></div><div class="video-container"><video id="videoElement" controls></video></div><div class="controls"><button id="captureBtn" class="btn btn-primary" disabled><svg class="icon" viewBox="0 0 24 24"><path d="M4 8h4V4h12v16H8v-4H4V8zm12 6v2h2v-2h-2zm-4-3v5h2v-5h-2zm-4-3v8h2V8h-2z"/></svg>捕获帧</button><button id="autoCaptureBtn" class="btn btn-primary" disabled><svg class="icon" viewBox="0 0 24 24"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8zm.5-13H11v6l5.2 3.2.8-1.3-4.5-2.7V7z"/></svg>自动捕获</button><button id="stopAutoCaptureBtn" class="btn btn-danger" disabled><svg class="icon" viewBox="0 0 24 24"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8zM8 8h8v8H8z"/></svg>停止</button><div class="input-group"><input type="number" id="frameInterval" class="number-input" value="1" min="0.1" step="0.1"><span class="input-label">秒/帧</span></div></div><div class="progress-bar" id="progressBar"><div class="progress" id="progress"></div></div></div><div class="panel"><h2 class="panel-title"><svg class="icon" viewBox="0 0 24 24"><path d="M19 3H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm0 16H5V5h14v14z"/><path d="M8.5 15H10V9H7v1.5h1.5zM13.5 12.75L15.25 15H17l-2.25-3L17 9h-1.75l-1.75 2.25V9H12v6h1.5z"/></svg>帧预览与导出</h2><div class="preview-container"><div class="canvas-wrapper"><canvas id="canvasElement"></canvas></div><button id="downloadBtn" class="btn btn-primary" disabled><svg class="icon" viewBox="0 0 24 24"><path d="M19 9h-4V3H9v6H5l7 7 7-7zM5 18v2h14v-2H5z"/></svg>下载当前帧</button></div><h3 class="panel-title" style="margin-top: 20px; font-size: 16px;"><svg class="icon" viewBox="0 0 24 24"><path d="M4 6H2v14c0 1.1.9 2 2 2h14v-2H4V6zm16-4H8c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2V4c0-1.1-.9-2-2-2zm-8 12.5v-9l6 4.5-6 4.5z"/></svg>已捕获的帧 (共<span id="frameCount">0</span>张)</h3><div class="thumbnails-container" id="thumbnails"><div class="empty-state" id="emptyState"><svg class="icon" viewBox="0 0 24 24"><path d="M10 20v-6h4v6h5v-8h3L12 3 2 12h3v8z"/></svg><p>尚未捕获任何帧</p></div></div></div></div>
</div><script>class FrameExtractor {constructor() {// 初始化DOM元素this.elements = {videoUpload: document.getElementById('videoUpload'),uploadButton: document.getElementById('uploadButton'),videoElement: document.getElementById('videoElement'),canvasElement: document.getElementById('canvasElement'),captureBtn: document.getElementById('captureBtn'),autoCaptureBtn: document.getElementById('autoCaptureBtn'),stopAutoCaptureBtn: document.getElementById('stopAutoCaptureBtn'),downloadBtn: document.getElementById('downloadBtn'),frameInterval: document.getElementById('frameInterval'),thumbnailsContainer: document.getElementById('thumbnails'),emptyState: document.getElementById('emptyState'),frameCount: document.getElementById('frameCount'),progressBar: document.getElementById('progressBar'),progress: document.getElementById('progress')};// 状态变量this.state = {autoCaptureInterval: null,capturedFrames: [],isAutoCapturing: false,captureStartTime: 0};// 初始化事件监听this.initEventListeners();}initEventListeners() {// 视频上传处理this.elements.videoUpload.addEventListener('change', (e) => this.handleVideoUpload(e));this.elements.uploadButton.addEventListener('click', () => this.elements.videoUpload.click());// 按钮事件this.elements.captureBtn.addEventListener('click', () => this.captureFrame());this.elements.autoCaptureBtn.addEventListener('click', () => this.toggleAutoCapture());this.elements.stopAutoCaptureBtn.addEventListener('click', () => this.stopAutoCapture());this.elements.downloadBtn.addEventListener('click', () => this.downloadCurrentFrame());}handleVideoUpload(e) {const file = e.target.files[0];if (file) {const videoURL = URL.createObjectURL(file);this.elements.videoElement.src = videoURL;// 启用按钮this.elements.captureBtn.disabled = false;this.elements.autoCaptureBtn.disabled = false;// 重置状态this.resetCaptureState();// 监听视频元数据加载// this.elements.videoElement.onloadedmetadata = () => {// this.elements.videoElement.play().catch(e => console.log("自动播放被阻止:", e));// };}}captureFrame() {if (this.elements.videoElement.readyState === 0) return;const ctx = this.elements.canvasElement.getContext('2d');// 设置canvas尺寸与视频帧相同this.elements.canvasElement.width = this.elements.videoElement.videoWidth;this.elements.canvasElement.height = this.elements.videoElement.videoHeight;// 绘制视频帧到canvasctx.drawImage(this.elements.videoElement, 0, 0,this.elements.canvasElement.width, this.elements.canvasElement.height);// 启用下载按钮this.elements.downloadBtn.disabled = false;// 创建缩略图this.createThumbnail(this.elements.canvasElement.toDataURL('image/jpeg', 0.8));// 更新进度条(自动捕获时)if (this.state.isAutoCapturing) {const currentTime = this.elements.videoElement.currentTime;const duration = this.elements.videoElement.duration;const progress = (currentTime / duration) * 100;this.elements.progress.style.width = `${progress}%`;}}toggleAutoCapture() {if (this.state.isAutoCapturing) {this.stopAutoCapture();} else {this.startAutoCapture();}}startAutoCapture() {const interval = parseFloat(this.elements.frameInterval.value) * 1000;if (interval > 0) {this.state.isAutoCapturing = true;this.state.captureStartTime = this.elements.videoElement.currentTime;this.elements.stopAutoCaptureBtn.disabled = false;this.elements.autoCaptureBtn.textContent = '暂停捕获';this.elements.autoCaptureBtn.classList.add('btn-danger');// 显示进度条this.elements.progressBar.style.display = 'block';this.elements.progress.style.width = '0%';// 先捕获一帧this.captureFrame();// 设置定时器this.state.autoCaptureInterval = setInterval(() => {this.captureFrame();// 检查是否到达视频末尾if (this.elements.videoElement.currentTime >= this.elements.videoElement.duration - 0.1) {this.stopAutoCapture();}}, interval);}}stopAutoCapture() {if (this.state.autoCaptureInterval) {clearInterval(this.state.autoCaptureInterval);this.state.autoCaptureInterval = null;}this.state.isAutoCapturing = false;this.elements.stopAutoCaptureBtn.disabled = true;this.elements.autoCaptureBtn.textContent = '自动捕获';this.elements.autoCaptureBtn.classList.remove('btn-danger');// 隐藏进度条this.elements.progressBar.style.display = 'none';}downloadCurrentFrame() {if (this.elements.canvasElement.width > 0) {const link = document.createElement('a');link.download = `frame_${new Date().getTime()}.png`;link.href = this.elements.canvasElement.toDataURL('image/png');link.click();}}createThumbnail(dataURL) {// 隐藏空状态if (this.elements.emptyState) {this.elements.emptyState.style.display = 'none';}const frameId = Date.now();this.state.capturedFrames.push({id: frameId, dataURL});// 更新帧计数this.elements.frameCount.textContent = this.state.capturedFrames.length;const thumbnailDiv = document.createElement('div');thumbnailDiv.className = 'thumbnail';thumbnailDiv.dataset.id = frameId;const img = document.createElement('img');img.src = dataURL;img.alt = `Captured frame ${frameId}`;const actionsDiv = document.createElement('div');actionsDiv.className = 'thumbnail-actions';const downloadBtn = document.createElement('button');downloadBtn.className = 'thumbnail-btn download';downloadBtn.title = '下载此帧';downloadBtn.innerHTML = '<svg class="icon" viewBox="0 0 24 24"><path d="M19 9h-4V3H9v6H5l7 7 7-7zM5 18v2h14v-2H5z"/></svg>';downloadBtn.addEventListener('click', (e) => {e.stopPropagation();const link = document.createElement('a');link.download = `frame_${frameId}.png`;link.href = dataURL;link.click();});const deleteBtn = document.createElement('button');deleteBtn.className = 'thumbnail-btn delete';deleteBtn.title = '删除此帧';deleteBtn.innerHTML = '<svg class="icon" viewBox="0 0 24 24"><path d="M19 4h-3.5l-1-1h-5l-1 1H5v2h14zM6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12z"/></svg>';deleteBtn.addEventListener('click', (e) => {e.stopPropagation();thumbnailDiv.remove();this.state.capturedFrames = this.state.capturedFrames.filter(frame => frame.id !== frameId);this.elements.frameCount.textContent = this.state.capturedFrames.length;// 如果没有帧了,显示空状态if (this.state.capturedFrames.length === 0 && this.elements.emptyState) {this.elements.emptyState.style.display = 'block';}});// 点击缩略图预览大图thumbnailDiv.addEventListener('click', () => {this.elements.canvasElement.width = 0;this.elements.canvasElement.height = 0;const img = new Image();img.onload = () => {this.elements.canvasElement.width = img.width;this.elements.canvasElement.height = img.height;const ctx = this.elements.canvasElement.getContext('2d');ctx.drawImage(img, 0, 0);this.elements.downloadBtn.disabled = false;};img.src = dataURL;});actionsDiv.appendChild(downloadBtn);actionsDiv.appendChild(deleteBtn);thumbnailDiv.appendChild(img);thumbnailDiv.appendChild(actionsDiv);this.elements.thumbnailsContainer.appendChild(thumbnailDiv);// 滚动到底部this.elements.thumbnailsContainer.scrollTop = this.elements.thumbnailsContainer.scrollHeight;}resetCaptureState() {// 停止自动捕获this.stopAutoCapture();// 清除画布const ctx = this.elements.canvasElement.getContext('2d');ctx.clearRect(0, 0, this.elements.canvasElement.width, this.elements.canvasElement.height);this.elements.canvasElement.width = 0;this.elements.canvasElement.height = 0;// 禁用下载按钮this.elements.downloadBtn.disabled = true;// 清除所有缩略图this.elements.thumbnailsContainer.innerHTML = '';this.state.capturedFrames = [];this.elements.frameCount.textContent = '0';// 显示空状态if (this.elements.emptyState) {this.elements.emptyState.style.display = 'block';}// 隐藏进度条this.elements.progressBar.style.display = 'none';}}// 初始化应用document.addEventListener('DOMContentLoaded', () => {new FrameExtractor();});
</script>
</body>
</html>
相关文章:

使用 HTML +JavaScript 从零构建视频帧提取器
在视频编辑、内容分析和多媒体处理领域,常常需要从视频中提取关键帧。手动截取不仅效率低下,还容易遗漏重要画面。本文介绍的视频帧提取工具通过 HTML5 技术栈实现了一个完整的浏览器端解决方案,用户可以轻松选择视频文件并进行手动或自动帧捕…...
基于若依前后分离版-用户密码错误锁定
sys_config配置参数 user.password.maxRetryCount:最大错误次数 user.password.lockTime:锁定时长 //SysLoginController//登录 PostMapping("/login") public AjaxResult login(RequestBody LoginBody loginBody) {AjaxResult ajax AjaxR…...

论文速读《DexWild:野外机器人策略的灵巧人机交互》
项目链接:https://dexwild.github.io/ 论文链接:https://arxiv.org/pdf/2505.07813 0. 简介 2025年5月,卡内基梅隆大学(CMU)发布了一篇突破性论文《DexWild: Dexterous Human Interactions for In-the-Wild Robot Pol…...

Bug问题
一、list 页面 import React, { useEffect, useState } from react; import { shallowEqual, useHistory, useSelector } from dva; import { Button, message } from choerodon-ui/pro; import formatterCollections from hzero-front/lib/utils/intl/formatterCollections; …...

【数据结构】5. 双向链表
文章目录 一、链表的分类1、双向链表的结构 二、双向链表的实现0、准备工作1、初始化2、打印3、尾插4、头插5、尾删6、头删7、查找8、在指定位置之后插入数据9、删除指定位置10、销毁 一、链表的分类 链表总共分为8种,具体的分组方式如图所示: 带头指的…...

【Linux手册】冯诺依曼体系结构
目录 前言 五大组件 数据信号 存储器(内存)有必要吗 常见面试题 前言 冯诺依曼体系结构是当代计算机基本架构,冯诺依曼体系有五大组件,通过这五大组件直观的描述了计算机的工作原理;学习冯诺依曼体系可以让给我们更…...

Mobile App UI自动化locator
在开展mobile app UI层自动化测试时,编写目标元素的locator是比较耗时的一个环节,弄清楚locator背后的逻辑,可以有效降低UI层测试维护成本。此篇博客以webdriverioappium作为UI自动化工具为例子,看看有哪些selector方法࿰…...
PaloAlto-Expedition OS命令注入漏洞复现(CVE-2025-0107)
免责申明: 本文所描述的漏洞及其复现步骤仅供网络安全研究与教育目的使用。任何人不得将本文提供的信息用于非法目的或未经授权的系统测试。作者不对任何由于使用本文信息而导致的直接或间接损害承担责任。如涉及侵权,请及时与我们联系,我们将尽快处理并删除相关内容。 前…...

(LeetCode 每日一题) 1061. 按字典序排列最小的等效字符串 (并查集)
题目:1061. 按字典序排列最小的等效字符串 思路:使用并查集,来将等价的字符连起来,形成一棵树。这棵树最小的字母,就代表整颗树,时间复杂度0(n),细节看注释。 C版本: class Solutio…...

linux 安装mysql8.0;支持国产麒麟,统信uos系统
一:使用我已经改好的mysql linux mysql8.0解压可用,点我下载 也在国产麒麟系统,统信uos系统也测试过,可用; 下载后,上传mysql.tar.gz 然后使用root角色去执行几个命令即可;数据库密码…...

C#实现远程锁屏
前言 这是一次提前下班没有锁屏进而引发的一次思考后的产物,思考的主要场景是当人离开电脑后,怎么能控制电脑锁屏,避免屏幕上的聊天记录被曝光。 首先想到通过系统的电源计划设置闲置超时时间熄屏,这可能是最接近场景的解决方案&a…...
历史记录隐藏的安全风险
引言 在数字化生活与工作场景中,历史记录功能广泛存在于浏览器、办公软件、移动应用等各类平台。它通过记录用户的搜索内容、操作痕迹、访问路径等信息,为用户提供便捷的操作体验和个性化服务。然而,这种看似便利的功能背后,却隐藏…...

SpringBoot3整合MySQL8的注意事项
版权声明 本文原创作者:谷哥的小弟作者博客地址:http://blog.csdn.net/lfdfhl 注意事项 1、请添加添加如下依赖: <dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId><…...
网络安全大模型理解
一、网络安全大模型的概述 网络安全大模型是一种用于识别和应对各种网络安全威胁的模型。它通过分析网络数据包、网络行为等信息,识别潜在的网络安全事件,并采取相应的措施进行防御。网络安全大模型主要包括以下几个部分: 1. 数据预处理&am…...

智语心桥:当AI遇上“星星的孩子”,科技如何点亮沟通之路?
目录: 引言:当科技的温度,遇见“星星的孩子”“智语心桥”:一座为孤独症儿童搭建的AI沟通之桥核心技术探秘:AI如何赋能“读心”与“对话”?个性化魔法:AI如何实现“千人千面”的精准干预?应用场景畅想:从家庭到机构,AI的全方位支持为什么是“智语心桥”?——价值、可…...

itop-3568开发板机器视觉opencv开发手册-图像绘制-画线
本小节代码在配套资料“iTOP-3568 开发板\03_【iTOP-RK3568 开发板】指南教程 \04_OpenCV 开发配套资料\11”目录下,如下图所示: cv2.line 函数功能: 绘制一条直线。 函数原型: cv2.line(img,pt1,pt2,color,thicknessNone,lin…...
【高频面试题】快慢指针及相关应用
文章目录 1 简介2 相关应用3 相关题目4 典型例题4.1 判断链表是否有环4.2 寻找链表的入环点4.3 寻找链表的中点4.4 寻找链表的倒数第k个节点4.5 重排链表 (反转链表找链表中点合并链表)4.6 寻找重复数(快慢指针 or 二分)4.7 回文链…...

sudo docker exec -it backend bash 以交互方式(interactive)进入正在运行的 Docker 容器的命令行环境
sudo docker exec -it backend bash🔍 总体作用 这条命令的作用是: 以交互方式(interactive)进入名为 backend 的正在运行的 Docker 容器的命令行环境。 你会进入容器的“终端”,就像登录到一个 Linux 系统一样&#…...
[论文阅读] 人工智能 | 当AI遇见绿色软件工程:可持续AI实践的研究新方向
【论文解读】当AI遇见绿色软件工程:可持续AI实践的研究新方向 论文信息 作者:Maja H. Kirkeby, Enrique Barba Roque, Justus Bogner等 标题:Greening AI-enabled Systems with Software Engineering: A Research Agenda for Environment…...
[论文阅读] 人工智能 | 用大语言模型抓虫:如何让网络协议实现与RFC规范对齐
用大语言模型抓虫:如何让网络协议实现与RFC规范对齐? 论文信息 arXiv:2506.01249 SysLLMatic: Large Language Models are Software System Optimizers Huiyun Peng, Arjun Gupte, Ryan Hasler, Nicholas John Eliopoulos, Chien-Chou Ho, Rishi Mantr…...

浅析EXCEL自动连接PowerBI的模板
浅析EXCEL自动连接PowerBI的模板 之前我分享过:PowerBI链接EXCEL实现自动化报表 ,其中一个关键工具就是提到的EXCEL链接模板,即宏工作薄。 今天就大概来聊一聊这个宏工作簿的底层原理是啥,怎么实现的。 第一步: 打开…...
DeepSeek 赋能金融反洗钱:AI 驱动的风险监测革新之路
目录 一、引言二、金融反洗钱监测的现状与挑战2.1 现状概述2.2 面临的挑战 三、DeepSeek 技术原理剖析3.1 核心架构3.2 关键技术 四、DeepSeek 在金融反洗钱监测中的应用优势4.1 强大的数据处理与分析能力4.2 精准的风险识别与预警4.3 提升工作效率与降低成本 五、DeepSeek 在金…...

java32
1.反射 获取类: 获取构造方法: 获取权限修饰符: 获取参数信息: 利用反射出来的构造器来创建对象: 获取成员变量: 获取成员方法: 综合练习: 动态代理:...

【Redis】zset 类型
zset 一. zset 类型介绍二. zset 命令zaddzcard、zcountzrange、zrevrange、zrangebyscorezpopmax、zpopminzrank、zrevrank、zscorezrem、zremrangebyrank、zremrangebyscorezincrby阻塞版本命令:bzpopmax、bzpopmin集合间操作:zinterstore、zunionstor…...

从Gartner报告看Atlassian在生成式AI领域的创新路径与实践价值
本文来源atlassian.com,由Atlassian全球白金合作伙伴——龙智翻译整理。 二十余年来,Atlassian始终是创新领域的领军者。凭借对团队协作本质的深刻理解,Atlassian在AI时代仍持续引领协作方式的革新。如今,这一领先地位再次获得权威…...

Kafka 安装教程(支持 Windows / Linux / macOS)
一、下载 1、kafka官网下载地址:https://kafka.apache.org/downloads 根据实际情况下载对应的版本 2、JDK的版本最好是17+ JDK下载地址:https://www.oracle.com/java/technologies/javase/jdk17-0-13-later-archive-downloads.html 二、安装 前置条件 安装 Java(至少 Jav…...

OpenCV种的cv::Mat与Qt种的QImage类型相互转换
一、首先了解cv::Mat结构体 cv::Mat::step与QImage转换有着较大的关系。 step的几个类别区分: step:矩阵第一行元素的字节数step[0]:矩阵第一行元素的字节数step[1]:矩阵中一个元素的字节数step1(0):矩阵中一行有几个通道数step1(1):一个元素有几个通道数(channel()) cv::Ma…...
机器学习——什么时候使用决策树
无论是决策树,包括集成树还是神经网络都是非常强大、有效的学习方法。 下面是各自的优缺点: 决策树和集成树通常在表格数据上表现良好,也称为结构化数据,这意味着如果你的数据集看起来像一个巨大的电子表格,那么决策…...
llm-d:面向Kubernetes的高性能分布式LLM推理框架
在生成式AI(GenAI)浪潮中,高效、经济地部署和扩展大型语言模型(LLM)推理服务是企业面临的核心挑战。传统基于Kubernetes的横向扩展(Scale-out)和负载均衡策略在处理独特的LLM推理工作负载时往往…...

前端没有“秦始皇“,但可以做跨端的王[特殊字符]
前端各领域的 “百家争鸣” 框架之争:有 React、Vue、Angular 等多种框架。它们各有优缺点,开发者之间还存在鄙视链,比如 Vue 嫌 React 难用,React 嫌 Vue 不够灵活。样式处理: CSS 预处理器:像 Sass、Les…...