// ==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 = `

🎬 ${CONFIG.title}

🎥
拖放视频文件或点击选择
🔊
拖放音频文件或点击选择
准备中...
💡 支持格式: 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); }); })();