// ==UserScript==
// @name 音视频合并工具
// @namespace http://tampermonkey.net/
// @version 1.0
// @description 在线合并音频和视频文件,支持 MP4、MKV、AVI、MP3、WAV、AAC、OGG 等格式
// @author YourName
// @match *://*/*
// @grant GM_registerMenuCommand
// @grant GM_addStyle
// @grant GM_download
// @run-at context-menu
// @license MIT
// ==/UserScript==
(function() {
'use strict';
// 配置
const CONFIG = {
title: '音视频合并工具',
version: '1.0',
maxFileSize: 500 * 1024 * 1024, // 500MB
ffmpegUrl: 'https://unpkg.com/@ffmpeg/core@0.12.6/dist/umd/ffmpeg-core.js'
};
// 样式
const STYLES = `
#avm-container {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 420px;
max-width: 95vw;
background: #1a1a2e;
border-radius: 16px;
box-shadow: 0 25px 80px rgba(0,0,0,0.5);
z-index: 999999;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
display: none;
overflow: hidden;
}
#avm-container.show {
display: block;
animation: avm-fadeIn 0.3s ease;
}
@keyframes avm-fadeIn {
from { opacity: 0; transform: translate(-50%, -50%) scale(0.9); }
to { opacity: 1; transform: translate(-50%, -50%) scale(1); }
}
#avm-header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
padding: 20px;
color: white;
display: flex;
justify-content: space-between;
align-items: center;
}
#avm-header h3 {
margin: 0;
font-size: 18px;
font-weight: 600;
}
#avm-close {
background: rgba(255,255,255,0.2);
border: none;
color: white;
width: 32px;
height: 32px;
border-radius: 50%;
cursor: pointer;
font-size: 18px;
display: flex;
align-items: center;
justify-content: center;
transition: background 0.2s;
}
#avm-close:hover {
background: rgba(255,255,255,0.3);
}
#avm-body {
padding: 20px;
max-height: 70vh;
overflow-y: auto;
}
.avm-drop-zone {
border: 2px dashed #4a4a6a;
border-radius: 12px;
padding: 30px 20px;
text-align: center;
margin-bottom: 15px;
transition: all 0.3s;
cursor: pointer;
background: #16162a;
}
.avm-drop-zone:hover, .avm-drop-zone.dragover {
border-color: #667eea;
background: rgba(102, 126, 234, 0.1);
}
.avm-drop-zone.has-file {
border-color: #4ade80;
background: rgba(74, 222, 128, 0.1);
}
.avm-drop-zone-icon {
font-size: 40px;
margin-bottom: 10px;
}
.avm-drop-zone-text {
color: #888;
font-size: 14px;
}
.avm-drop-zone-text strong {
color: #667eea;
}
.avm-file-info {
color: #4ade80;
font-size: 13px;
margin-top: 8px;
word-break: break-all;
}
.avm-file-input {
display: none;
}
.avm-label {
display: block;
color: #aaa;
font-size: 13px;
margin-bottom: 6px;
font-weight: 500;
}
.avm-options {
margin: 15px 0;
padding: 15px;
background: #16162a;
border-radius: 10px;
}
.avm-option-row {
display: flex;
align-items: center;
margin-bottom: 12px;
}
.avm-option-row:last-child {
margin-bottom: 0;
}
.avm-option-row label {
color: #aaa;
font-size: 13px;
min-width: 70px;
}
.avm-option-row select, .avm-option-row input {
flex: 1;
background: #1a1a2e;
border: 1px solid #333;
color: #fff;
padding: 8px 12px;
border-radius: 6px;
font-size: 13px;
}
.avm-option-row select:focus, .avm-option-row input:focus {
outline: none;
border-color: #667eea;
}
.avm-btn {
width: 100%;
padding: 14px;
border: none;
border-radius: 10px;
font-size: 15px;
font-weight: 600;
cursor: pointer;
transition: all 0.2s;
margin-top: 10px;
}
.avm-btn-primary {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
}
.avm-btn-primary:hover {
transform: translateY(-2px);
box-shadow: 0 5px 20px rgba(102, 126, 234, 0.4);
}
.avm-btn-primary:disabled {
opacity: 0.5;
cursor: not-allowed;
transform: none;
box-shadow: none;
}
.avm-btn-secondary {
background: #333;
color: #fff;
}
.avm-btn-secondary:hover {
background: #444;
}
#avm-progress {
display: none;
margin-top: 15px;
}
#avm-progress.show {
display: block;
}
.avm-progress-bar {
height: 8px;
background: #333;
border-radius: 4px;
overflow: hidden;
}
.avm-progress-fill {
height: 100%;
background: linear-gradient(90deg, #667eea, #764ba2);
width: 0%;
transition: width 0.3s;
}
.avm-progress-text {
color: #888;
font-size: 12px;
margin-top: 8px;
text-align: center;
}
#avm-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0,0,0,0.7);
z-index: 999998;
display: none;
}
#avm-overlay.show {
display: block;
}
.avm-status {
padding: 10px;
border-radius: 8px;
margin-top: 10px;
font-size: 13px;
display: none;
}
.avm-status.show {
display: block;
}
.avm-status.success {
background: rgba(74, 222, 128, 0.2);
color: #4ade80;
}
.avm-status.error {
background: rgba(248, 113, 113, 0.2);
color: #f87171;
}
.avm-status.info {
background: rgba(96, 165, 250, 0.2);
color: #60a5fa;
}
.avm-hint {
color: #666;
font-size: 11px;
margin-top: 10px;
text-align: center;
}
`;
// 添加样式
GM_addStyle(STYLES);
// 状态
let videoFile = null;
let audioFile = null;
let ffmpeg = null;
let ffmpegLoaded = false;
// 创建UI
function createUI() {
const overlay = document.createElement('div');
overlay.id = 'avm-overlay';
document.body.appendChild(overlay);
const container = document.createElement('div');
container.id = 'avm-container';
container.innerHTML = `
💡 支持格式: MP4, MKV, AVI, WebM, MP3, WAV, AAC, OGG 等
最大文件: 500MB | 使用 FFmpeg.wasm 处理
`;
document.body.appendChild(container);
// 绑定事件
bindEvents();
}
// 绑定事件
function bindEvents() {
const overlay = document.getElementById('avm-overlay');
const closeBtn = document.getElementById('avm-close');
const videoZone = document.getElementById('avm-video-zone');
const audioZone = document.getElementById('avm-audio-zone');
const videoInput = document.getElementById('avm-video-input');
const audioInput = document.getElementById('avm-audio-input');
const mergeBtn = document.getElementById('avm-merge-btn');
const resetBtn = document.getElementById('avm-reset-btn');
// 关闭
overlay.addEventListener('click', hidePanel);
closeBtn.addEventListener('click', hidePanel);
// 视频拖放
setupDropZone(videoZone, videoInput, 'video', (file) => {
videoFile = file;
document.getElementById('avm-video-info').textContent = file.name + ` (${formatSize(file.size)})`;
videoZone.classList.add('has-file');
});
// 音频拖放
setupDropZone(audioZone, audioInput, 'audio', (file) => {
audioFile = file;
document.getElementById('avm-audio-info').textContent = file.name + ` (${formatSize(file.size)})`;
audioZone.classList.add('has-file');
});
// 合并按钮
mergeBtn.addEventListener('click', startMerge);
// 重置按钮
resetBtn.addEventListener('click', resetAll);
}
// 设置拖放区域
function setupDropZone(zone, input, type, onFile) {
const handleFile = (file) => {
if (type === 'video' && !file.type.startsWith('video/')) {
showStatus('请选择视频文件', 'error');
return;
}
if (type === 'audio' && !file.type.startsWith('audio/') && !file.type.startsWith('video/')) {
showStatus('请选择音频文件', 'error');
return;
}
if (file.size > CONFIG.maxFileSize) {
showStatus('文件超过500MB限制', 'error');
return;
}
onFile(file);
};
zone.addEventListener('click', () => input.click());
zone.addEventListener('dragover', (e) => {
e.preventDefault();
zone.classList.add('dragover');
});
zone.addEventListener('dragleave', () => {
zone.classList.remove('dragover');
});
zone.addEventListener('drop', (e) => {
e.preventDefault();
zone.classList.remove('dragover');
const file = e.dataTransfer.files[0];
if (file) handleFile(file);
});
input.addEventListener('change', (e) => {
const file = e.target.files[0];
if (file) handleFile(file);
});
}
// 显示/隐藏面板
function showPanel() {
document.getElementById('avm-overlay').classList.add('show');
document.getElementById('avm-container').classList.add('show');
}
function hidePanel() {
document.getElementById('avm-overlay').classList.remove('show');
document.getElementById('avm-container').classList.remove('show');
}
// 显示状态
function showStatus(msg, type = 'info') {
const el = document.getElementById('avm-status');
el.textContent = msg;
el.className = `avm-status show ${type}`;
setTimeout(() => el.classList.remove('show'), 5000);
}
// 格式化文件大小
function formatSize(bytes) {
if (bytes < 1024) return bytes + ' B';
if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB';
if (bytes < 1024 * 1024 * 1024) return (bytes / (1024 * 1024)).toFixed(1) + ' MB';
return (bytes / (1024 * 1024 * 1024)).toFixed(1) + ' GB';
}
// 加载FFmpeg
async function loadFFmpeg() {
if (ffmpegLoaded) return true;
showStatus('正在加载 FFmpeg...', 'info');
const progressEl = document.getElementById('avm-progress');
const progressFill = document.getElementById('avm-progress-fill');
const progressText = document.getElementById('avm-progress-text');
progressEl.classList.add('show');
progressFill.style.width = '10%';
progressText.textContent = '加载 FFmpeg 核心文件...';
try {
const { FFmpeg } = await import('@ffmpeg/ffmpeg');
const { fetchFile, toBlobURL } = await import('@ffmpeg/util');
ffmpeg = new FFmpeg();
ffmpeg.on('progress', ({ progress }) => {
const percent = Math.round(progress * 100);
progressFill.style.width = percent + '%';
progressText.textContent = `处理中... ${percent}%`;
});
progressFill.style.width = '30%';
progressText.textContent = '下载 FFmpeg WASM...';
const baseURL = 'https://unpkg.com/@ffmpeg/core@0.12.6/dist/umd';
await ffmpeg.load({
coreURL: await toBlobURL(`${baseURL}/ffmpeg-core.js`, 'text/javascript'),
wasmURL: await toBlobURL(`${baseURL}/ffmpeg-core.wasm`, 'application/wasm'),
});
progressFill.style.width = '100%';
progressText.textContent = '准备完成!';
// 暴露到全局供其他函数使用
window.ffmpegFetchFile = fetchFile;
setTimeout(() => progressEl.classList.remove('show'), 500);
ffmpegLoaded = true;
return true;
} catch (err) {
console.error('FFmpeg load error:', err);
progressEl.classList.remove('show');
showStatus('FFmpeg 加载失败: ' + err.message, 'error');
return false;
}
}
// 开始合并
async function startMerge() {
if (!videoFile) {
showStatus('请选择视频文件', 'error');
return;
}
if (!audioFile) {
showStatus('请选择音频文件', 'error');
return;
}
const mergeBtn = document.getElementById('avm-merge-btn');
const progressEl = document.getElementById('avm-progress');
const progressFill = document.getElementById('avm-progress-fill');
const progressText = document.getElementById('avm-progress-text');
const format = document.getElementById('avm-format').value;
const outputName = document.getElementById('avm-output-name').value || 'merged_video';
mergeBtn.disabled = true;
progressEl.classList.add('show');
progressFill.style.width = '0%';
progressText.textContent = '准备文件...';
try {
// 加载FFmpeg
if (!await loadFFmpeg()) {
throw new Error('FFmpeg 加载失败');
}
progressFill.style.width = '20%';
progressText.textContent = '读取视频文件...';
// 写入文件
const videoData = await videoFile.arrayBuffer();
const audioData = await audioFile.arrayBuffer();
await ffmpeg.writeFile('input_video', new Uint8Array(videoData));
await ffmpeg.writeFile('input_audio', new Uint8Array(audioData));
progressFill.style.width = '40%';
progressText.textContent = '合并中...';
// 执行合并
const outputFile = `${outputName}.${format}`;
await ffmpeg.exec([
'-i', 'input_video',
'-i', 'input_audio',
'-c:v', 'copy',
'-c:a', 'aac',
'-strict', 'experimental',
'-shortest',
outputFile
]);
progressFill.style.width = '80%';
progressText.textContent = '生成输出文件...';
// 读取输出
const data = await ffmpeg.readFile(outputFile);
const blob = new Blob([data.buffer], { type: `video/${format}` });
// 下载
progressFill.style.width = '100%';
progressText.textContent = '准备下载...';
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = outputFile;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
showStatus('✅ 合并完成! 文件已下载', 'success');
// 清理
await ffmpeg.deleteFile('input_video');
await ffmpeg.deleteFile('input_audio');
await ffmpeg.deleteFile(outputFile);
} catch (err) {
console.error('Merge error:', err);
showStatus('合并失败: ' + err.message, 'error');
} finally {
mergeBtn.disabled = false;
setTimeout(() => progressEl.classList.remove('show'), 3000);
}
}
// 重置
function resetAll() {
videoFile = null;
audioFile = null;
document.getElementById('avm-video-info').textContent = '';
document.getElementById('avm-audio-info').textContent = '';
document.getElementById('avm-video-zone').classList.remove('has-file');
document.getElementById('avm-audio-zone').classList.remove('has-file');
document.getElementById('avm-video-input').value = '';
document.getElementById('avm-audio-input').value = '';
document.getElementById('avm-status').classList.remove('show');
}
// 注册菜单
GM_registerMenuCommand('🎬 ' + CONFIG.title, showPanel);
// 初始化
createUI();
// 页面加载后自动显示面板
window.addEventListener('load', () => {
setTimeout(showPanel, 500);
});
})();