// ==UserScript==
// @name 视频压缩器
// @namespace http://tampermonkey.net/
// @version 1.0
// @description 通用视频压缩工具 - 选择本地视频文件进行压缩处理
// @author WorkBuddy
// @match *://*/*
// @grant none
// @license MIT
// ==/UserScript==
(function() {
'use strict';
// ==================== 配置区 ====================
const CONFIG = {
// 压缩质量: ultrafast, superfast, veryfast, faster, fast, medium, slow, slower, veryslow
preset: 'medium',
// 输出格式
outputFormat: 'mp4',
// 默认视频编码
videoCodec: 'libx264',
// 默认音频编码
audioCodec: 'aac',
// 视频码率 (空则自动)
videoBitrate: '',
// 音频码率 (空则自动)
audioBitrate: '128k',
// crf 值 0-51,越高质量越差文件越小,23-28 是常用范围
crf: 28,
// 是否移除音频
removeAudio: false
};
// ==================== 样式 ====================
const STYLE = `
.vc-float-btn {
position: fixed;
bottom: 20px;
right: 20px;
width: 56px;
height: 56px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border-radius: 50%;
box-shadow: 0 4px 20px rgba(102, 126, 234, 0.4);
cursor: pointer;
z-index: 2147483647;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.3s ease;
border: none;
}
.vc-float-btn:hover {
transform: scale(1.1);
box-shadow: 0 6px 30px rgba(102, 126, 234, 0.6);
}
.vc-float-btn svg {
width: 28px;
height: 28px;
fill: white;
}
.vc-panel {
position: fixed;
bottom: 90px;
right: 20px;
width: 380px;
max-height: 80vh;
background: #1a1a2e;
border-radius: 16px;
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.3);
z-index: 2147483647;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
display: none;
overflow: hidden;
border: 1px solid rgba(255, 255, 255, 0.1);
}
.vc-panel.show {
display: block;
animation: vc-slide-up 0.3s ease;
}
@keyframes vc-slide-up {
from { opacity: 0; transform: translateY(20px); }
to { opacity: 1; transform: translateY(0); }
}
.vc-header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
padding: 16px 20px;
color: white;
display: flex;
justify-content: space-between;
align-items: center;
}
.vc-header h3 {
margin: 0;
font-size: 16px;
font-weight: 600;
}
.vc-close {
background: none;
border: none;
color: white;
font-size: 24px;
cursor: pointer;
line-height: 1;
opacity: 0.8;
transition: opacity 0.2s;
}
.vc-close:hover {
opacity: 1;
}
.vc-body {
padding: 20px;
color: #e0e0e0;
max-height: calc(80vh - 60px);
overflow-y: auto;
}
.vc-drop-zone {
border: 2px dashed #4a4a6a;
border-radius: 12px;
padding: 40px 20px;
text-align: center;
cursor: pointer;
transition: all 0.3s ease;
margin-bottom: 16px;
}
.vc-drop-zone:hover, .vc-drop-zone.dragover {
border-color: #667eea;
background: rgba(102, 126, 234, 0.1);
}
.vc-drop-zone svg {
width: 48px;
height: 48px;
fill: #667eea;
margin-bottom: 12px;
}
.vc-drop-zone p {
margin: 0;
color: #888;
font-size: 14px;
}
.vc-drop-zone .hint {
font-size: 12px;
color: #666;
margin-top: 8px;
}
.vc-file-input {
display: none;
}
.vc-file-info {
background: rgba(102, 126, 234, 0.1);
border-radius: 8px;
padding: 12px;
margin-bottom: 16px;
display: none;
}
.vc-file-info.show {
display: block;
}
.vc-file-name {
font-weight: 600;
color: #fff;
word-break: break-all;
margin-bottom: 4px;
}
.vc-file-size {
font-size: 12px;
color: #888;
}
.vc-options {
margin-bottom: 16px;
}
.vc-option-row {
display: flex;
align-items: center;
margin-bottom: 12px;
}
.vc-option-row label {
flex: 0 0 100px;
font-size: 13px;
color: #aaa;
}
.vc-option-row select, .vc-option-row input {
flex: 1;
background: #2a2a3e;
border: 1px solid #3a3a5a;
border-radius: 6px;
padding: 8px 12px;
color: #fff;
font-size: 13px;
}
.vc-option-row select:focus, .vc-option-row input:focus {
outline: none;
border-color: #667eea;
}
.vc-checkbox-row {
display: flex;
align-items: center;
margin-bottom: 12px;
}
.vc-checkbox-row input[type="checkbox"] {
margin-right: 8px;
width: 16px;
height: 16px;
accent-color: #667eea;
}
.vc-checkbox-row label {
font-size: 13px;
color: #aaa;
}
.vc-compress-btn {
width: 100%;
padding: 12px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border: none;
border-radius: 8px;
color: white;
font-size: 14px;
font-weight: 600;
cursor: pointer;
transition: all 0.3s ease;
}
.vc-compress-btn:hover:not(:disabled) {
transform: translateY(-2px);
box-shadow: 0 4px 15px rgba(102, 126, 234, 0.4);
}
.vc-compress-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.vc-progress {
display: none;
margin-top: 16px;
}
.vc-progress.show {
display: block;
}
.vc-progress-bar {
height: 8px;
background: #2a2a3e;
border-radius: 4px;
overflow: hidden;
margin-bottom: 8px;
}
.vc-progress-fill {
height: 100%;
background: linear-gradient(90deg, #667eea, #764ba2);
width: 0%;
transition: width 0.3s ease;
}
.vc-progress-text {
font-size: 12px;
color: #888;
text-align: center;
}
.vc-status {
padding: 12px;
border-radius: 8px;
margin-top: 16px;
display: none;
}
.vc-status.show {
display: block;
}
.vc-status.success {
background: rgba(76, 175, 80, 0.2);
color: #81c784;
}
.vc-status.error {
background: rgba(244, 67, 54, 0.2);
color: #e57373;
}
.vc-download-btn {
display: block;
width: 100%;
padding: 10px;
margin-top: 12px;
background: #4caf50;
border: none;
border-radius: 6px;
color: white;
font-size: 13px;
font-weight: 600;
cursor: pointer;
text-align: center;
text-decoration: none;
}
.vc-download-btn:hover {
background: #45a049;
}
.vc-ffmpeg-status {
font-size: 11px;
color: #666;
margin-top: 8px;
text-align: center;
}
.vc-ffmpeg-status.loading {
color: #667eea;
}
.vc-original-info {
display: flex;
justify-content: space-between;
font-size: 12px;
color: #888;
margin-top: 4px;
}
`;
// ==================== 初始化 ====================
let ffmpeg = null;
let ffmpegLoaded = false;
let isCompressing = false;
function init() {
addStyles();
createUI();
loadFFmpeg();
}
function addStyles() {
const styleEl = document.createElement('style');
styleEl.textContent = STYLE;
document.head.appendChild(styleEl);
}
function createUI() {
// 悬浮按钮
const floatBtn = document.createElement('button');
floatBtn.className = 'vc-float-btn';
floatBtn.innerHTML = `
`;
floatBtn.title = '视频压缩器';
floatBtn.onclick = togglePanel;
document.body.appendChild(floatBtn);
// 主面板
const panel = document.createElement('div');
panel.className = 'vc-panel';
panel.id = 'vc-panel';
panel.innerHTML = `
`;
document.body.appendChild(panel);
// 事件绑定
const dropZone = document.getElementById('vc-drop-zone');
const fileInput = document.getElementById('vc-file-input');
dropZone.addEventListener('click', () => fileInput.click());
dropZone.addEventListener('dragover', (e) => {
e.preventDefault();
dropZone.classList.add('dragover');
});
dropZone.addEventListener('dragleave', () => {
dropZone.classList.remove('dragover');
});
dropZone.addEventListener('drop', (e) => {
e.preventDefault();
dropZone.classList.remove('dragover');
const files = e.dataTransfer.files;
if (files.length > 0 && files[0].type.startsWith('video/')) {
handleFileSelect(files[0]);
}
});
fileInput.addEventListener('change', (e) => {
if (e.target.files.length > 0) {
handleFileSelect(e.target.files[0]);
}
});
document.getElementById('vc-compress-btn').addEventListener('click', startCompress);
}
function togglePanel() {
const panel = document.getElementById('vc-panel');
panel.classList.toggle('show');
}
let selectedFile = null;
function handleFileSelect(file) {
selectedFile = file;
const fileInfo = document.getElementById('vc-file-info');
const fileName = document.getElementById('vc-file-name');
const fileSize = document.getElementById('vc-file-size');
const compressBtn = document.getElementById('vc-compress-btn');
fileName.textContent = file.name;
fileSize.textContent = formatFileSize(file.size);
fileInfo.classList.add('show');
compressBtn.disabled = !ffmpegLoaded;
// 清除之前的结果
hideStatus();
}
function formatFileSize(bytes) {
if (bytes < 1024) return bytes + ' B';
if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(2) + ' KB';
if (bytes < 1024 * 1024 * 1024) return (bytes / (1024 * 1024)).toFixed(2) + ' MB';
return (bytes / (1024 * 1024 * 1024)).toFixed(2) + ' GB';
}
// ==================== FFmpeg 加载 ====================
async function loadFFmpeg() {
const statusEl = document.getElementById('vc-ffmpeg-status');
const btn = document.getElementById('vc-compress-btn');
if (statusEl) {
statusEl.textContent = '正在加载视频处理引擎...';
statusEl.classList.add('loading');
}
try {
// 动态加载 ffmpeg.wasm
const { FFmpeg } = await import('https://cdn.jsdelivr.net/npm/@ffmpeg/ffmpeg@0.12.7/+esm');
const { fetchFile } = await import('https://cdn.jsdelivr.net/npm/@ffmpeg/util@0.12.1/+esm');
ffmpeg = new FFmpeg();
ffmpeg.on('progress', ({ progress }) => {
const fill = document.getElementById('vc-progress-fill');
const text = document.getElementById('vc-progress-text');
if (fill) fill.style.width = Math.round(progress * 100) + '%';
if (text) text.textContent = `处理中: ${Math.round(progress * 100)}%`;
});
ffmpeg.on('log', ({ message }) => {
console.log('[FFmpeg]', message);
});
await ffmpeg.load({
coreURL: 'https://cdn.jsdelivr.net/npm/@ffmpeg/core@0.12.6/dist/esm/ffmpeg-core.js',
wasmURL: 'https://cdn.jsdelivr.net/npm/@ffmpeg/core@0.12.6/dist/esm/ffmpeg-core.wasm'
});
ffmpegLoaded = true;
if (statusEl) {
statusEl.textContent = '✓ 引擎加载完成,可以开始压缩';
statusEl.classList.remove('loading');
statusEl.style.color = '#4caf50';
}
if (btn && selectedFile) btn.disabled = false;
} catch (error) {
console.error('FFmpeg load error:', error);
if (statusEl) {
statusEl.textContent = '引擎加载失败,请刷新页面重试';
statusEl.classList.remove('loading');
}
}
}
// ==================== 视频压缩 ====================
async function startCompress() {
if (!selectedFile || !ffmpegLoaded || isCompressing) return;
isCompressing = true;
const btn = document.getElementById('vc-compress-btn');
const progress = document.getElementById('vc-progress');
const fill = document.getElementById('vc-progress-fill');
const text = document.getElementById('vc-progress-text');
const status = document.getElementById('vc-status');
const statusText = document.getElementById('vc-ffmpeg-status');
btn.disabled = true;
btn.textContent = '压缩中...';
progress.classList.add('show');
fill.style.width = '0%';
text.textContent = '读取文件...';
hideStatus();
if (statusText) {
statusText.textContent = '';
statusText.classList.remove('loading');
}
try {
const { fetchFile } = await import('https://cdn.jsdelivr.net/npm/@ffmpeg/util@0.12.1/+esm');
// 写入输入文件
text.textContent = '读取视频文件...';
const inputName = 'input' + getExtension(selectedFile.name);
await ffmpeg.writeFile(inputName, await fetchFile(selectedFile));
// 构建 ffmpeg 命令
const format = document.getElementById('vc-format').value;
const preset = document.getElementById('vc-preset').value;
const crf = document.getElementById('vc-crf').value;
const removeAudio = document.getElementById('vc-remove-audio').checked;
const outputName = 'output.' + format;
const args = ['-i', inputName];
// 视频编码设置
if (format === 'mp4') {
args.push('-c:v', 'libx264', '-preset', preset, '-crf', crf);
} else if (format === 'webm') {
args.push('-c:v', 'libvpx-vp9', '-crf', crf, '-b:v', '0');
} else if (format === 'mkv') {
args.push('-c:v', 'libx264', '-preset', preset, '-crf', crf);
}
// 音频设置
if (removeAudio) {
args.push('-an');
} else {
if (format === 'mp4') {
args.push('-c:a', 'aac', '-b:a', '128k');
} else {
args.push('-c:a', 'libopus', '-b:a', '128k');
}
}
args.push('-y', outputName);
text.textContent = '正在压缩视频...';
await ffmpeg.exec(args);
// 读取输出文件
text.textContent = '生成压缩文件...';
const data = await ffmpeg.readFile(outputName);
const blob = new Blob([data.buffer], { type: getMimeType(format) });
const url = URL.createObjectURL(blob);
const originalSize = selectedFile.size;
const compressedSize = blob.size;
const ratio = ((1 - compressedSize / originalSize) * 100).toFixed(1);
// 显示成功状态
status.className = 'vc-status show success';
status.innerHTML = `
🎉
压缩完成!
原始大小: ${formatFileSize(originalSize)} → 压缩后: ${formatFileSize(compressedSize)}
体积减少 ${ratio}%
⬇️ 下载压缩后的视频
`;
text.textContent = '完成!';
} catch (error) {
console.error('Compression error:', error);
status.className = 'vc-status show error';
status.innerHTML = `
❌
压缩失败
${error.message || '未知错误'}
`;
text.textContent = '失败';
} finally {
isCompressing = false;
btn.disabled = false;
btn.textContent = '开始压缩';
}
}
function hideStatus() {
const status = document.getElementById('vc-status');
status.className = 'vc-status';
}
function getExtension(filename) {
return '.' + filename.split('.').pop().toLowerCase();
}
function getMimeType(format) {
const mimeTypes = {
'mp4': 'video/mp4',
'webm': 'video/webm',
'mkv': 'video/x-matroska'
};
return mimeTypes[format] || 'video/mp4';
}
// ==================== 启动 ====================
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
})();